21–22. Debouncing · Finite State Machine Patterns · Ring Buffer and Interrupt-Driven UART Receive
With peripherals learned, we move to firmware patterns. The first two appear in nearly every project — debouncing and finite state machines (FSM). A mechanical button's contacts bounce for a few ms on press (chatter), so one press reads as many. Debouncing filters the jitter to keep only real input; an FSM expresses behavior cleanly with "state+input→next state". It's pure logic with no hardware, verified exactly with gcc. When UART data pours in, a receive interrupt fires per byte. Handling each fully inside the ISR makes it long, and a slow main lets the next byte overwrite the previous — data loss. The fix: the ISR only enqueues; the idle main does the processing. The key data structure is the ring buffer. If producer (ISR) and consumer (main) touch different indices, it's safe without disabling interrupts.
What you'll learn
- 1Explain the cause and symptoms of button chatter (bounce)
- 2Implement an integrator (consecutive-count) debouncer
- 3Split the debouncer into a reusable·testable module
- 4Understand an FSM's parts (states·inputs·transitions)
- 5Drive an FSM from confirmed debounced edges
- 6Explain why interrupt receive loses data
- 7Understand the ring buffer's head/tail FIFO structure
- 8Implement empty/full checks and power-of-two masking
- 9Understand SPSC safety and volatile
- 10Know how to combine the ring buffer with a UART RX interrupt
21. Debouncing · Finite State Machine (FSM) Patterns
With peripherals learned, we move to firmware patterns. The first two appear in nearly every project — debouncing and finite state machines (FSM). A mechanical button's contacts bounce for a few ms on press (chatter), so one press reads as many. Debouncing filters the jitter to keep only real input; an FSM expresses behavior cleanly with "state+input→next state". It's pure logic with no hardware, verified exactly with gcc.
Introduction
A button's metal contacts micro-bounce on contact, so one press is electrically 0→1→0→1… for a few ms. Read raw, it counts as many. This lesson is pure, hardware-independent logic — a debouncer module (debounce.c/.h) with an LED-mode FSM on top.
Key concepts
1) Integrator debouncer
Sample the pin at a fixed period (e.g. SysTick 1–5ms), and change state only when a value differing from the stable state repeats THRESHOLD times. Any return to the original resets the counter, filtering glitches (non-blocking·robust).
uint8_t debounce_update(debounce_t *db, uint8_t raw) {
uint8_t r = raw ? 1u : 0u;
if (r == db->stable) { db->count = 0u; return 0u; } /* no jitter */
if (++db->count >= DEBOUNCE_THRESHOLD) { /* N in a row */
db->stable = r; db->count = 0u; return 1u; /* confirmed change */
}
return 0u;
}2) Finite state machine (FSM)
| Current state | Input (press confirmed) | Next state |
|---|---|---|
| OFF | press | SLOW |
| SLOW | press | FAST |
| FAST | press | OFF |
In C, implement with enum + switch. Explicit states make debugging·extension easy.
The FSM takes only the "press edge" confirmed by the debouncer. Separating input cleanup (debounce) from decision (FSM) keeps code clean. On a board, just feed raw = (GPIOA->IDR >> pin) & 1 from a periodic interrupt.
Core example
/* noisy raw stream → debounce → cycle OFF→SLOW→FAST on each confirmed press edge */
typedef enum { MODE_OFF=0, MODE_SLOW=1, MODE_FAST=2 } led_mode_t;
for (i = 0; i < n; i++) {
if (debounce_update(&db, raw[i]) && debounce_state(&db) == 1u) {
mode = (led_mode_t)((mode + 1) % 3); /* advance only on confirmed edge */
}
}Verify: a short glitch (1 sample) is ignored and the mode changes only on a real press (THRESHOLD in a row). Build: gcc pc_test.c debounce.c -o pc_test.
Common mistakes
Q. Even after debouncing it sometimes double-presses.
A. THRESHOLD is too small or the sample period too fast. Aim for sample period × THRESHOLD = 10–30ms (e.g. 5ms × 3 = 15ms).
Q. The mode keeps changing while I hold the button.
A. Act on the "changed-to-pressed edge", not the "pressed state". Check both debounce_update's return (changed) and that the new state is 1.
Q. My if-else FSM gets tangled.
A. Make states an enum and gather transitions in a switch. Scattered state across the code is hard to trace.
Summary
- Chatter is contact bounce — one press reads as many
- The integrator debouncer confirms only after N consecutive equal values, filtering glitches
- A debouncer module is reusable·testable on any chip
- An FSM expresses behavior with states·inputs·transitions via enum+switch
- Separating input cleanup (debounce) from decision (FSM) keeps code clean
Exercises
- Vary THRESHOLD to 2/5 and observe how glitch filtering changes
- Add a long-press state to the FSM
- Debounce two buttons independently to drive separate FSMs
22. Ring Buffer and Interrupt-Driven UART Receive
When UART data pours in, a receive interrupt fires per byte. Handling each fully inside the ISR makes it long, and a slow main lets the next byte overwrite the previous — data loss. The fix: the ISR only enqueues; the idle main does the processing. The key data structure is the ring buffer. If producer (ISR) and consumer (main) touch different indices, it's safe without disabling interrupts.
Introduction
The RX interrupt fires per byte. If the ISR is long or main doesn't fetch in time, the next byte overwrites the receive register and is lost. You need "receive fast, stash somewhere, process later".
Key concepts
1) Structure and empty/full checks
Use a fixed array circularly. head (write)·tail (read) wrap to 0 at the end (FIFO). Empty is head==tail; full is ((head+1)&MASK)==tail. Leaving one slot for the full check means capacity is N-1 for size N.
#define RB_SIZE 8u
#define RB_MASK (RB_SIZE - 1u)
next = (head + 1) & RB_MASK; /* fast wrap with & if power-of-two */2) SPSC safety and volatile
The producer (ISR) writes only head, the consumer (main) only tail. Since they update different variables, single-producer/single-consumer (SPSC) is safe without locks or disabling interrupts. But head/tail are shared, so declare them volatile.
3) Combining with the UART RX interrupt
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
uint8_t b = (uint8_t)USART1->DR; /* reading clears RXNE */
rb_push(&rx_rb, b); /* producer: head only */
}
}
/* main: if (rb_pop(&rx_rb, &c)) { ...process leisurely... } (consumer: tail only) */Enable USART_CR1_RXNEIE in CR1 and call NVIC_EnableIRQ(USART1_IRQn) for the RX interrupt. Main does the processing.
Core example
uint8_t rb_push(ringbuffer_t *rb, uint8_t data) {
uint16_t next = (uint16_t)((rb->head + 1u) & RB_MASK);
if (next == rb->tail) return 0u; /* full — reject (avoid loss) */
rb->buf[rb->head] = data; rb->head = next; return 1u;
}
uint8_t rb_pop(ringbuffer_t *rb, uint8_t *out) {
if (rb->head == rb->tail) return 0u; /* empty */
*out = rb->buf[rb->tail];
rb->tail = (uint16_t)((rb->tail + 1u) & RB_MASK); return 1u;
}Verify: with size 8 (capacity 7), the 8th push is rejected as FULL, and popping returns in insertion order (FIFO). Build: gcc pc_test.c ringbuffer.c -o pc_test.
Common mistakes
Q. It works on PC without volatile on head/tail.
A. On a single-threaded PC the problem hides, but in real embedded where ISR·main share them, the compiler caches and misses updates. Always add volatile.
Q. Size 10 makes wrap behave oddly.
A. & (N-1) masking only works when N is a power of two. Use % 10, or pick 8·16.
Q. push overwrites when the buffer is full.
A. push must reject (return 0) when ((head+1)&MASK)==tail. Otherwise it overtakes tail and overwrites unread data.
Summary
- Interrupt receive is safe when the ISR enqueues and main dequeues to process
- A ring buffer is a head/tail FIFO; empty/full are decided by indices
- A power-of-two size wraps fast with & MASK (capacity N-1)
- SPSC is safe without locks via head/tail separation + volatile
- The UART RX ISR only rb_push; main rb_pop then processes
Exercises
- Confirm rb_count() returns the correct count even in wrap cases
- Grow the buffer to size 16 and fix MASK accordingly
- Add line_assemble() that gathers bytes up to a newline (\n) into one line
All lecture materials and example code (with homework and answers) are openly available on GitHub.
View on GitHub ↗Comments 0
No comments yet. Be the first.
Only members can comment.
Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy