Robotics & Arduino
Lesson 2 · Refinement

Optional — Header files.

Skip →

From blinking an LED to a component anyone can use.

01 — We know how to blink an LED.

Let’s write a MorseComponent.

Morse represents characters through short and long pulses of light or sound.

Short pulses → dots.
Long pulses → dashes — called “lines” in our function names.
No pulse → pause.

02 — Start with DECLARE: the timings.

First, give our four timings names. All values are in milliseconds.

A buffer is a short OFF interval around each dot or line.

Only the four timing constants

These are our classroom timings. Standard Morse uses different timing ratios; the dot/dash character patterns stay the same.

03 — Choose the LED pin. Then INIT.

Now add the LED pin to DECLARE.

Add the LED pin

Start Serial. Set the LED pin to OUTPUT. Initialize the LED OFF.

Our first setup()

04 — Write the three basic actions.

Line: OFF buffer → ON line → OFF buffer.
Dot: OFF buffer → ON dot → OFF buffer.
Pause: OFF buffer → OFF pause → OFF buffer.

blinkLine()

blinkDot()

blinkPause()

4.1 — Could we organize this further? When to say “enough.”

Pause is different: the LED stays OFF. Dot and line do the same thing; only their ON time changes.

So we could keep one pause function and create one shared blink(duration). Pass it DOT_LENGTH or LINE_LENGTH.

Option 1: share the dot / line logic

We could go further: one function takes a duration and an ON/OFF flag. An if in the middle decides whether to light the LED.

Then our three named functions — dot, line, pause — each call that one function.

Option 2: one shared action, three named functions

These are alternative versions, not additions to paste together. Both preserve the original timings and behavior.

Is it logically correct? Definitely. Yes.

Avoid repeating logic. Consolidate shared work. Keep functions focused on one concept. These are useful principles.

Is there a NEED for this here? A big NO.

For this small teaching example, the extra organization adds parameters, a decision, and calls to follow. The most generalized version is longer, and understanding a simple blink now requires jumping between functions.

That is where organization can become overthinking, overcoding, and overcomplication. The extra structure is not worth it for us here.

There is a point where we should say: enough.
Stop when simplicity and understanding start to suffer just to uphold modularization and encapsulation rules.

Those rules should help us understand the program. For the rest of this lesson, we keep the three straightforward functions from step 4.

05 — Build the character functions.

A is dot, line.
B is line, dot, dot, dot.

Use those same actions to build all the letters and numbers.

The 26 letters

The 10 numbers

06 — Now add the PWM input and map.

26 letters + 10 numbers + 1 pause = 37 commands.

Give each command six values.
A = 0–5. B = 6–11. And so on.

0–255 contains 256 values, so 37 × 6 leaves 34 spare values at the end.

Now we need a signal input pin. Add these declarations, then add one line to the setup we already wrote.

Add the PWM pin and range constants

Update setup() with this line

INPUT_PULLUP is an input with a weak pull-up, so an unconnected wire stays at our idle HIGH level.

Divide the received value by six. Choose the corresponding character function.

The map: signal range → character

07 — Read the signal. Pass it to the map.

Now connect the pieces: read PWM → map → blink.

The core read-and-blink flow

Call it from loop()

readPWM() measures the HIGH/LOW pulse timing and returns a 0–255 value. The complete example adds timeouts and command boundaries so repeated letters work. Those details are collected at the end.

08 — We made a standalone Morse component.

A little LED and a little programmed chip.

It gets 5V, GND, and a PWM signal, and knows how to translate that signal into Morse blinks.

The component owns the blinking work.

09 — But the user wants something simpler.

The user does not care about our code, PWM values, or component internals.

They want to write “A” and see it blink.

So we create a handler program.
It accepts a character or string and translates it into the component’s signal.

10 — A new file: declare and initialize the output.

Start a separate handler sketch. It only needs a PWM output to send the signal.

Handler: DECLARE

Handler: INIT

255 is our idle signal. The component and handler run on separate boards.

11 — blinkChar(): send one character.

First reverse the map: character → PWM value.
Send near the center of each six-value range.

Reverse the map

Then send that signal. An optional pause flag defaults to false.

Add blinkChar()

blinkChar('A') → A.
blinkChar('A', true) → A, then an extra pause.
blinkChar(' ') → the pause character itself.

sendPWMCommand() sends the command and leaves time for the component to finish. Its timing implementation is at the end.

12 — blinkStr(): break the string into characters.

For each character, call blinkChar().

A space in the text sends a pause.
If the flag is true, add one extra pause at the end of the whole string.

Add blinkStr()

The hidden zero character marks the end of a C string. This is why the loop knows when to stop.

13 — Give the user only the functions they need.

The user still does not need to see all that implementation.

List the useful functions — blinkChar and blinkStr — and bundle their declarations into a header file.

The header is the interface. The existing function bodies go in a matching .cpp file.

14 — Create MorseBlinker.h

Keep the two blink functions. Add beginMorse() for the initialization we already wrote.

The header file

The default false arguments now live in the header. Remove those defaults from the function definitions in MorseBlinker.cpp.

The initialization moves into beginMorse(). The other function bodies stay together in the implementation file; we do not rewrite them here.

15 — Tie it together: hello world.

The user’s Arduino program includes our header, initializes the handler, and asks it to send a message.

The small user program

That’s the user-facing program. The handler handles translation. The component handles blinking.

16 — Now look at Servo.h.

A servo is a pulse-controlled motor component, just as ours is a signal-controlled Morse LED component.

With Servo.h, we request an angle.
The implementation turns that request into servo command pulses.

With MorseBlinker.h, we request characters.
Our implementation turns them into the Morse component’s PWM commands.

The servo’s controller handles motor positioning and feedback.
Our Morse component handles blinking.
The motor itself spins. The LED itself lights.

User → handler → component.

Each has a specific job. The header gives the user an easy way to ask.

The analogy is the separation of jobs. The two components use different pulse protocols, and the header exposes functions whose implementation runs on the user’s Arduino.

Complete files and implementation details

The lesson above introduces one piece at a time. These are the supporting details for the complete two-board example.

One command, then time to blink

The sender holds each command for 80 ms, then holds the signal HIGH for 3000 ms. The receiver accepts a command once and re-arms after idle. This preserves repeated letters such as the two Ls in “hello.” The longest blink sequence takes 2500 ms.

Commands use six-value bins from 0 to 221; send their center values (3, 9, …, 219). Values 222–254 are unused; 255 is idle. Never send 0 for A: constant LOW has no pulse to measure.

Full PWM-reading helper

Complete read-and-blink function with command boundaries

MorseBlinker.cpp: the assembled handler implementation

Files and wiring

Receiver: MorseComponent/MorseComponent.ino.
Sender: HelloMorse.ino, MorseBlinker.h, and MorseBlinker.cpp in one HelloMorse folder.

Sender D9 → receiver D2. Join GND. Use two 5V Uno R3 boards with default timer settings. Power each from USB; leave their 5V pins unconnected. Upload the receiver first, then reset the sender. The message plays once.

Keep this sender’s D9 PWM separate from Servo.h, which uses Timer1 on the Uno. The example is deliberately slow and has no acknowledgment/retry.

Letters, digits, and spaces are supported; lowercase is accepted. Other characters are ignored. These are classroom blink timings, not standard Morse timing.

Download all complete examples · Wiring and usage notes

Arduino compilation and hardware behavior have not been verified.

Reference notes

End of the optional section.

Ready to return to the main lesson.