Skip to main content

Peripherals and Drivers

Bringing up on-chip peripherals from the registers up, and writing drivers for them that survive a chip change.

šŸ“„ļøAnatomy of a Peripheral

An STM32F411RE has a UART, three SPIs, three I²C controllers, eight timers, an ADC, a USB controller, an RTC, two watchdogs and two DMA engines. The reference manual gives each of them thirty to eighty pages, and read front to back they look like thirteen unrelated pieces of hardware. They are not. They are thirteen instances of one design, drawn by the same team, wired onto the same two buses, and configured by the same six steps in the same order every time.

šŸ“„ļøPWM

A microcontroller pin has two output voltages and nothing in between. Pulse-width modulation is the trick that gets the third: switch fast enough and whatever is downstream — an LED and your eye, a motor and its inductance, an RC filter and a slow ADC — averages the square wave into a level. The pin is still only ever fully on or fully off, which is why it dissipates almost no power doing it. That is the whole reason PWM won over analogue drive for everything from a status LED to a 10 kW inverter.

šŸ“„ļøInput Capture and Encoders

PWM points the timer outward: the counter drives a pin. Input capture points it inward. The counter free-runs, an edge on a pin tells the hardware "now", and the value of CNT at that instant is copied into a capture register before software has had a chance to be late. That last clause is the entire value of the peripheral. A GPIO interrupt can also tell you an edge happened, but by the time your handler reads a counter it has been anywhere from 12 to several hundred cycles — jittering with whatever else the NVIC was doing — and the measurement carries that jitter. The capture unit's latch has no jitter at all.

šŸ“„ļøUART in Depth

A UART has no clock wire. That single fact generates every interesting property of the peripheral and every way it fails. SPI and I²C both ship a clock alongside the data, so the receiver is told exactly when to look; a UART receiver is told nothing. It sees a falling edge, starts its own counter, and from that moment guesses where the bit centres are using an oscillator the transmitter has never met. Everything below — the divider arithmetic, the oversampling modes, the tolerance budget, the overrun flag — is machinery built around that one guess.

šŸ“„ļøSPI in Depth

SPI is a shift register with a wire between two halves of it. That is the whole protocol, and holding it in mind explains everything the peripheral does. The controller has eight bits, the target has eight bits, and the clock the controller generates walks them past each other in a ring: the controller's MSB goes out on MOSI and into the target's LSB position, the target's MSB goes out on MISO and into the controller's. After eight clocks the two registers have swapped contents. There is no addressing, no acknowledgement, no error detection and no notion of a transaction — every one of those has to be built on top by whatever protocol the target's datasheet defines.

šŸ“„ļøADC and DAC Drivers

A successive-approximation ADC is, physically, a capacitor with a switch in front of it. Converting a voltage happens in two completely different phases: first the switch closes and the capacitor is allowed to charge towards your signal through the source impedance, and then the switch opens and a comparator plays twelve rounds of twenty questions against the trapped charge. The second phase is fixed by the hardware and takes exactly twelve clocks. The first phase is the one you configure, the one every tutorial leaves at its reset value, and the one that decides whether your reading means anything at all.

šŸ“„ļøExternal Memory and QSPI

The moment your data stops fitting on-chip, the interesting question is not which memory — it is whether the processor has to execute from it, or merely read it. Those two requirements lead to completely different hardware. Data you read into a buffer can live behind four wires and a software driver, and a plain SPI port is enough. Code the CPU fetches instructions from must appear in the address map, which means a controller that turns a bus read into a flash transaction with no software involved at all.

šŸ“„ļøWriting a Portable Driver

Most firmware drivers are written once, for one board, and thrown away at the next project — not because the author was careless but because the driver was never separable from the chip it was born on. It calls HALI2CMaster_Transmit. It hard-codes I2C1. It has a static state variable so there can only ever be one. It blocks forever waiting on a status bit. Each of those is a small local convenience and together they weld the driver to one MCU, one instance, one board and one timing assumption.