Day 2 of 5
⏱ ~60 minutes
Firmware Development in 5 Days — Day 2

Registers and Peripherals

Every peripheral on a microcontroller is a set of memory-mapped registers. Today you'll configure GPIO, timers, and UART entirely through direct register writes.

GPIO Register Architecture

GPIO registers follow a consistent pattern across ARM MCUs. Key registers: MODER (pin mode: input/output/alternate/analog), ODR (output data register: read/write), IDR (input data register: read only), BSRR (bit set/reset: atomic, upper 16 bits reset, lower 16 bits set), PUPDR (pull-up/pull-down). Always enable the GPIO clock in the RCC register before accessing GPIO registers.

Timers

General-purpose timers are the most versatile peripheral. Configure: PSC (prescaler) divides the clock, ARR (auto-reload register) sets the period. Timer fires interrupt (update event) every ARR+1 prescaled clocks. For PWM: set channel to PWM mode, set CCR (capture/compare register) for duty cycle. 16-bit timer at 72MHz: PSC=71, ARR=999 → 1kHz PWM with CCR=499 = 50% duty cycle.

UART Communication

UART config: BRR (baud rate register) = fCLK / baud. At 72MHz, 9600 baud: BRR = 7500. Set word length, stop bits, parity in CR1/CR2. Enable TX and RX in CR1. Poll TXE flag before writing to DR. Poll RXNE flag before reading from DR. For interrupt-driven UART, enable RXNE interrupt in CR1, write ISR, enable in NVIC. Never busy-poll in production — always use interrupt or DMA.

c
// UART transmit without HAL — STM32F1
#include "stm32f1xx.h"

void uart1_init(uint32_t baud) {
    // 1. Clock enable: USART1 on APB2, GPIOA on APB2
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN;

    // 2. Configure PA9 (TX) as alternate function push-pull
    GPIOA->CRH &= ~(0xF << 4);
    GPIOA->CRH |=  (0xB << 4);  // AF output push-pull, 50MHz

    // 3. Configure PA10 (RX) as input floating
    GPIOA->CRH &= ~(0xF << 8);
    GPIOA->CRH |=  (0x4 << 8);  // input floating

    // 4. Set baud rate (PCLK2 = 72MHz assumed)
    USART1->BRR = 72000000UL / baud;

    // 5. Enable USART, TX, RX
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
}

void uart1_putchar(char c) {
    while (!(USART1->SR & USART_SR_TXE));  // wait for TX buffer empty
    USART1->DR = c;
}

void uart1_puts(const char *s) {
    while (*s) uart1_putchar(*s++);
    // Wait for last byte to finish transmitting
    while (!(USART1->SR & USART_SR_TC));
}

int main(void) {
    uart1_init(9600);
    uart1_puts("Hello from bare-metal UART!
");

    char buf[32];
    uint32_t count = 0;
    while (1) {
        // Simple integer to string (no sprintf without libc)
        int i = 0, n = count;
        if (n == 0) { buf[i++] = '0'; }
        while (n > 0) { buf[i++] = '0' + (n % 10); n /= 10; }
        buf[i++] = '
'; buf[i++] = '
'; buf[i] = '';
        // Reverse
        for (int a=0, b=i-3; a<b; a++,b--) {
            char t=buf[a]; buf[a]=buf[b]; buf[b]=t;
        }
        uart1_puts(buf);
        count++;
        for (volatile int d=0; d<1000000; d++);
    }
}
💡
Use the atomic BSRR register for GPIO, not ODR. Writing to ODR is a read-modify-write (non-atomic). If an interrupt modifies the same port between your read and write, you corrupt the other pins. BSRR allows setting/clearing individual bits atomically in a single write.
📝 Day 2 Exercise
Implement a Timer-Driven PWM
  1. Configure TIM2 for PWM output on PA0 (TIM2_CH1). Set PWM frequency to 1kHz. Set duty cycle via CCR1.
  2. Connect a LED with a 330Ω resistor to PA0. Verify PWM by measuring with a multimeter (should show ~1.65V at 50% duty cycle).
  3. Write a function void pwm_set_duty(uint8_t percent) that maps 0–100% to 0–ARR.
  4. Create a sine wave pattern: update CCR1 every 10ms with values from a 100-element sine lookup table. The LED should pulse smoothly.
  5. Add UART logging: transmit the current duty cycle value over UART every 100ms. Read it with a serial terminal.

Day 2 Summary

  • GPIO clock must be enabled in RCC before accessing GPIO registers — common beginner mistake
  • Timers: PSC divides clock, ARR sets period, CCR sets PWM duty cycle
  • UART BRR = fCLK / baud; poll TXE before write, RXNE before read
  • BSRR is atomic — always use it for GPIO in code where interrupts might fire
Challenge

Implement interrupt-driven UART receive. Enable the RXNE interrupt in USART1 CR1 and in the NVIC. Write an ISR that reads each byte into a circular buffer. In main(), check if a complete line (ending in \n) is in the buffer and process the command. Implement: 'LED ON', 'LED OFF', 'STATUS' commands. This is the pattern used in every command-line interface on embedded hardware.

Finished this lesson?