Peripheral Programming
Writing firmware that controls MCU peripherals. Learn memory-mapped registers and bit manipulation, read-modify-write and atomic access, polling vs interrupts vs DMA, peripheral initialisation patterns, datasheet and reference-manual reading, hardware abstraction layers, and debugging with logic analysers.
Every MCU peripheral — GPIO, timer, UART, ADC — is controlled through registers at fixed addresses. Mastering bit manipulation and the reference manual lets you program any microcontroller, regardless of vendor library.
An analogy
Peripheral registers are like a mixing desk: each fader and switch (bit field) controls one feature, and you must set them in the right order to get sound.
Registers and bits
Each peripheral has control, status and data registers. Configuration usually means enabling the clock, setting mode fields, configuring pins, then enabling the peripheral. Status flags are polled or trigger interrupts.
Read-modify-write hazards
R |= x reads, modifies and writes — an interrupt in between can lose changes. Use atomic set/reset registers, bit-banding (Cortex-M3/M4), or disable interrupts briefly (critical sections).
Polling, interrupts, DMA
Polling is simple but wastes CPU; interrupts react to events; DMA moves bulk data with no CPU per byte. High-rate peripherals (ADC streams, UART at Mbaud, SPI displays) use DMA.
Drivers and debugging
Layered drivers (register → LL → HAL → application) and hardware abstraction layers make code portable. Debug with SWD breakpoints, register views, logic analysers for protocol timing, and oscilloscopes for analog signals.
Formulas
- Set / clear / toggle / test:
R |= (1u<<n); R &= ~(1u<<n); R ^= (1u<<n); (R >> n) & 1u - Write a field:
R = (R & ~(mask << pos)) | ((val & mask) << pos) - Register address:
addr = peripheral base + register offset
Worked example: Bit operations
Given: R = 0b1010_0000
Find: Set bit 0, clear bit 7, toggle bit 5.
- R |= 1 → 1010_0001
- R &= ~(1<<7) → 0010_0001
- R ^= (1<<5) → 0000_0001
Answer: 0000_0001
Worked example: Field write
Given: 4-bit mode field at bits 20–23, write 0x2
Find: Operation.
- R = (R & ~(0xF << 20)) | (0x2 << 20)
Answer: Clears the old field before writing the new value
Portable register access macros (C)
#include <stdint.h>
#define REG32(addr) (*(volatile uint32_t *)(addr))
#define BIT(n) (1u << (n))
#define FIELD_SET(r, m, p, v) ((r) = ((r) & ~((m) << (p))) | (((v) & (m)) << (p)))
#define GPIOA_BASE 0x40010800u /* example: STM32F1 GPIOA */
#define GPIOA_ODR REG32(GPIOA_BASE + 0x0C)
#define GPIOA_BSRR REG32(GPIOA_BASE + 0x10)
#define GPIOA_CRL REG32(GPIOA_BASE + 0x00)
void led_init(void) {
FIELD_SET(GPIOA_CRL, 0xFu, 5 * 4, 0x2u); /* PA5: output push-pull 2 MHz */
}
void led_on(void) { GPIOA_BSRR = BIT(5); } /* atomic set (no read-modify-write) */
void led_off(void) { GPIOA_BSRR = BIT(5 + 16); } /* atomic reset */
int led_is_on(void) { return (GPIOA_ODR >> 5) & 1u; }volatile forces real hardware accesses. Set/reset registers (BSRR) change single bits atomically — safe even if an interrupt modifies the same port.
Common mistakes
- Wrong: Missing volatile on registers. — Right: Compiler may optimise away hardware accesses.
- Wrong: Writing a field without clearing it. — Right: Mask the old bits first.
Applications
- Board bring-up: First register-level tests on new hardware.
- Performance-critical drivers: Tight timing without HAL overhead.
- Porting firmware: Abstraction layers across MCUs.
Key takeaways
- Set |=, clear &=~, toggle ^=.
- Clear fields before writing.
- Beware read-modify-write races.
- Polling → interrupts → DMA.
Going further
- Bit-banding: Alias addresses for atomic bits (M3/M4).
- SVD files: Machine-readable register maps.
- Register-level unit testing: Mocking hardware in host tests.