Microcontroller Peripherals
Configure GPIO, timers, SPI/I2C, and PWM on typical Cortex-M boards.
Recipe
let mut led = gpioa.pa5.into_push_pull_output();
led.set_high().ok();
led.set_low().ok();When to reach for this: Any firmware that talks to LEDs, sensors, displays, or motors through MCU peripherals.
Working Example
use cortex_m::asm::delay;
use stm32f4xx_hal as hal;
use hal::{pac, prelude::*};
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let gpioa = dp.GPIOA.split();
let mut led = gpioa.pa5.into_push_pull_output();
let i2c = hal::i2c::I2c::new(
dp.I2C1,
gpioa.pa9.into_alternate_open_drain(),
gpioa.pa10.into_alternate_open_drain(),
400.kHz(),
&clocks,
);
loop {
led.set_high().unwrap();
delay(clocks.sysclk().raw() / 8);
led.set_low().unwrap();
delay(clocks.sysclk().raw() / 8);
let _ = i2c.write(0x48, &[0x00]);
}
}What this demonstrates:
- GPIO push-pull output for LEDs
- Busy-wait delay (replace with timers in production)
- I2C master at 400 kHz for sensor communication
- HAL splits peripherals into owned pins
Deep Dive
Peripheral Overview
| Peripheral | Typical use |
|---|---|
| GPIO | Digital I/O, chip selects |
| Timer | Delays, PWM, input capture |
| SPI | Flash, displays, RF modules |
| I2C | Sensors, EEPROM |
| USART | GPS, modems, debug serial |
| ADC | Analog sensors |
PWM Example Sketch
let mut pwm = timer.pwm_hz(pin, 1.kHz(), &clocks);
let mut channel = pwm.ch1;
channel.set_duty(percent);
channel.enable();SPI Full-Duplex
let mut spi = Spi::new(dp.SPI1, (sck, miso, mosi), MODE_0, 8.MHz(), &clocks);
let mut buffer = [0u8; 4];
spi.transfer(&mut buffer, &[0x9f, 0, 0, 0]).ok();Gotchas
- Floating inputs - Undefined reads on unconnected pins. Fix: Enable pull-up or pull-down.
- 5V tolerant pins - 3.3V MCUs die on 5V signals. Fix: Level shifters on inputs.
- SPI mode mismatch - MODE_0 vs MODE_3 breaks communication. Fix: Read sensor datasheet clock polarity/phase.
- I2C pull-ups missing - Bus stuck or intermittent. Fix: External pull-ups on SDA/SCL.
- Busy-wait delays - CPU spins at 100% during delay. Fix: Use timer interrupts or Embassy timers.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
bitbang GPIO | Missing hardware peripheral | High-speed SPI needed |
| DMA + SPI | Large transfers | Tiny register reads |
| RTIC software PWM | No hardware PWM pins | Precise frequency required |
FAQs
Open drain vs push pull?
Open drain for I2C and shared buses. Push-pull for LEDs and fast GPIO.
Alternate function pins?
Peripherals require pins set to AF mode via HAL into_alternate_* methods.
Clock configuration?
Wrong PLL settings make baud rates lie. Verify with logic analyzer.
Multiple I2C addresses?
Scan bus with write probes or use sensor default from datasheet.
USART printf?
defmt-rtt or serialport bridge preferred over println! in firmware.
ADC sampling rate?
Set sample time based on source impedance. Average readings for noise.
Watchdog?
Pet watchdog in main loop or dedicated task. Reset on hang.
Low power modes?
Stop peripherals and clocks between samples. Wake on interrupt or RTC.
Pin mux conflicts?
One pin, one function. Planner spreadsheet prevents impossible routing.
HAL vs PAC?
Application code uses HAL. PAC only for missing HAL coverage or ISR register tweaks.
Related
- embedded-hal - portable trait layer
- Interrupts & Concurrency - timer ISRs
- Embedded Basics - board setup
- Debugging Firmware - logic analyzer + probe-rs
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.