Firmware is the program that runs on your microcontroller — the reflexes of the robot. This lesson takes you past the blink into how real robot code is structured: the setup-and-loop model, reading inputs and driving outputs, generating analog-like signals with PWM, timing things without freezing the robot, and using the serial link to see what your code is actually doing. Get these right and your robot becomes responsive instead of stuttery.
- Structure a sketch with setup() and a non-blocking loop()
- Read digital and analog inputs and drive digital and PWM outputs
- Replace delay() with millis()-based timing so the robot stays responsive
- Use the serial link to debug what the firmware is seeing and doing
The setup-and-loop model
Every Arduino sketch has two functions. setup() runs once when the board powers up — you configure pins and start the serial link here. loop() runs forever afterwards, as fast as the board can go, and it is where the sense–think–act cycle lives. The mental model to hold: setup() is 'get ready', loop() is 'the robot's heartbeat'. Everything the robot does happens because loop() runs thousands of times a second, each pass reading inputs and updating outputs.
Pins are configured with pinMode(pin, MODE). An output pin drives something (OUTPUT); an input pin reads something (INPUT, or INPUT_PULLUP to use the board's internal resistor so a floating button reads a clean HIGH until pressed). Getting the mode wrong is a common bug: a pin left as input cannot drive an LED, and an input without a pull-up floats and reads random noise.
Reading inputs, driving outputs
There are two flavours of each direction. Digital is on/off: digitalRead(pin) returns HIGH or LOW, and digitalWrite(pin, HIGH/LOW) sets a pin fully on or off. Analog in reads a varying voltage: analogRead(pin) returns a number from 0 to 1023 representing 0 V to the board's reference, which is how you read a potentiometer, a light sensor, or a distance sensor's analog output. There is no true analog *out* on most boards — instead you use PWM (next section) via analogWrite.
digitalRead(pin)→ HIGH or LOW; read a button or a digital sensor.digitalWrite(pin, HIGH)→ drive an LED, a relay, or a motor-driver enable.analogRead(pin)→ 0–1023; read a potentiometer or analog sensor.analogWrite(pin, 0-255)→ PWM 'analog' output on a PWM-capable pin.
PWM — fake analog for speed and brightness
A digital pin can only be fully on or fully off, so how do you dim an LED or run a motor at half speed? Pulse-width modulation (PWM) switches the pin on and off very fast, and the fraction of time it spends on — the *duty cycle* — sets the effective level. Fifty per cent duty is half brightness or half speed. On Arduino, analogWrite(pin, value) sets PWM where value 0 is off, 255 is full, and 127 is about half. Only certain pins support it (marked with a ~).
This is exactly how you control motor speed through a driver: you send the driver a PWM signal, and it delivers that fraction of the motor's power. Brightness, speed, and servo-like control all come from PWM, so it is one of the most-used tools in robot firmware.
Timing without freezing the robot
The single biggest beginner firmware mistake is delay(). delay(1000) stops the *entire* program for a second — the robot cannot read its sensors, react to a wall, or do anything else while it waits. For a blink that is fine; for a robot it is fatal, because a robot must keep sensing while it acts. The professional pattern is non-blocking timing with millis(), which returns the milliseconds since power-on. Instead of waiting, you check whether enough time has passed and act if so, letting the rest of the loop keep running.
// Non-blocking blink: the loop stays free to do other work.
const int LED = 13;
unsigned long lastToggle = 0;
const unsigned long INTERVAL = 500; // ms
bool on = false;
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
if (now - lastToggle >= INTERVAL) { // time to flip?
on = !on;
digitalWrite(LED, on ? HIGH : LOW);
lastToggle = now;
}
// ...read sensors and drive motors here every pass, no waiting...
}If your robot 'ignores' its sensors for a moment, look for a delay(). While delay() runs, nothing else happens — no sensor reads, no motor updates. Convert timed behaviour to the millis() pattern and the robot becomes responsive.
Serial: your window into the firmware
You cannot see what a microcontroller is thinking — unless it tells you. Serial.begin(9600) in setup() opens a channel to your computer, and Serial.println(value) prints to the Serial Monitor. This is the most important debugging tool in robotics: when a robot misbehaves, printing the sensor values shows you what it is *actually* sensing, which is almost always different from what you assumed. Print the distance, the button state, the motor command — and the bug usually reveals itself.
A good habit is to structure the loop clearly: read all sensors into variables, compute the decision, then drive the outputs — and print the key values while developing. Keeping sense, think and act as distinct phases each loop makes the code readable and the robot predictable.