Skip to content
Kiln Forged
All guides

Beginner · 7 min read

How firmware talks to hardware

Pins, signals and the few ideas that make a chip blink a light or read a button.

Firmware is the program that lives on a device's microcontroller: the small chip that acts as its brain. Unlike an app on your phone, firmware talks directly to the hardware around it. It does that through a handful of simple ideas.

Everything starts with pins

A microcontroller has metal pins (or pads) around its edge. Most of them are GPIO pins, short for general-purpose input/output. In firmware, each one is just a number. On the ESP32-C3 used in our IR Remote Cloner, GPIO4 is wired to the infrared receiver and GPIO5 drives the IR LEDs.

That mapping between pin numbers and what's physically connected is called the pin map, and it's the single most important link between the board and the code. Get it wrong and the code toggles a pin that isn't connected to anything.

// pins.h: generated from the circuit board
constexpr uint8_t PIN_IR_RX = 4;  // infrared receiver
constexpr uint8_t PIN_IR_TX = 5;  // IR LED driver

Kiln Forged generates this file straight from the board design, so the two can't drift apart.

Outputs: turning things on

Set a pin as an output and the chip can drive it high (3.3 V) or low (0 V):

pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, HIGH);  // LED on

A pin can only supply a small current, around 20 mA. Anything hungrier, like a motor or a bright IR LED, gets switched by a transistor that the pin controls. That's why the IR remote has a MOSFET (Q1) between the chip and its LEDs.

Inputs: reading the world

Set a pin as an input and the chip reads whether it's high or low. A button is the classic example: one side goes to the pin, the other to ground. With the chip's internal pull-up resistor switched on, the pin reads HIGH normally and LOW while the button is pressed.

pinMode(PIN_BUTTON, INPUT_PULLUP);
if (digitalRead(PIN_BUTTON) == LOW) {
  // pressed
}

Real buttons "bounce", flickering between on and off for a few milliseconds when pressed, so firmware usually waits a moment and reads again. This is called debouncing.

Analog: more than on and off

Some pins can measure a voltage, not just high or low. An ADC (analog-to-digital converter) turns a voltage into a number. Our remote uses one to check its battery: two resistors halve the battery voltage so it's safe for the pin, and the firmware doubles the reading back.

Talking to other chips

Sensors and displays usually speak a bus, a shared set of wires with a simple protocol:

  • I²C uses two wires (data and clock) and lets many devices share them, each with its own address. Great for sensors.
  • SPI uses four wires and is faster. Common for displays and memory.
  • UART is a simple two-wire serial link, like the one your computer uses to read debug messages.

Libraries hide most of the detail. You call bme.readTemperature() and the library handles the I²C conversation.

Timing and sleep

Firmware runs in a loop, forever. Good firmware spends most of that time asleep, waking only when something happens (a button press, a timer). Sleep is how a small battery lasts months instead of days.

Putting it together

The IR Remote Cloner's firmware uses every idea above: inputs for buttons, an output (through a transistor) for the IR LEDs, a timing-sensitive input for the IR receiver, an ADC for the battery, flash storage for learned codes, and light sleep between presses. Open the demo and look at the firmware stage. Each file has comments explaining what it does.