Project 12: UART Command Console + Boot Diagnostics

A UART command console that reports boot diagnostics, clock configuration, and reset causes.

Quick Reference

Attribute Value
Difficulty Level 2: Intermediate
Time Estimate 1-2 weeks
Main Programming Language C (Alternatives: C++, Rust, Ada)
Alternative Programming Languages C++, Rust, Ada
Coolness Level Level 3: Genuinely Clever
Business Potential 1. The “Resume Gold”
Prerequisites UART basics, ring buffers, boot sequence knowledge
Key Topics UART framing, command parsing, diagnostics

1. Learning Objectives

By completing this project, you will:

  1. Build a UART console with line editing and command dispatch.
  2. Report boot diagnostics including clock source and reset cause.
  3. Implement safe parsing and error handling.
  4. Use the console for runtime introspection.

2. All Theory Needed (Per-Concept Breakdown)

UART Framing, Baud Rate, and Command Parsing

Fundamentals UART is an asynchronous serial protocol that sends data as frames of bits at a configured baud rate. A typical frame includes a start bit, data bits, optional parity, and stop bits. Because there is no shared clock, the baud rate must match closely between devices. A UART command console builds on this to provide a human-readable interface for diagnostics and control.

Deep Dive into the concept UART timing is derived from the peripheral clock and a baud rate divider. Any error in clock configuration or divider calculation leads to framing errors. The receiver samples the line at the expected bit times, so errors accumulate over the frame. In STM32, UART peripherals include status flags for overrun, framing errors, and parity errors, which should be checked during diagnostics. A command console adds software structure: you need a line buffer, a parser, and a command dispatch table. The parser must handle backspace, line endings, and invalid commands gracefully. A good diagnostic console also includes a boot banner, version info, reset cause, and clock configuration printout. This turns the UART into a system observability tool, not just a debug print. To ensure deterministic behavior, you should separate the ISR from the parser: the ISR appends bytes to a ring buffer, while the main loop processes complete lines. This avoids long ISRs and makes command handling non-blocking. You also need to consider line noise. A robust console uses timeouts or a maximum line length to prevent buffer overflow. Finally, you should measure the actual baud rate by toggling a pin or using a known baud output and verifying with a logic analyzer. This reinforces the clock audit discipline: UART correctness is a function of clock correctness.

How this fit on projects In UART Command Console + Boot Diagnostics, you build a UART console with line editing, command dispatch, and boot-time diagnostics.

Definitions & key terms

  • Baud rate -> Symbols per second, defining the UART bit timing.
  • Start/stop bits -> Framing bits that delimit each UART byte.
  • Overrun -> Error when new data arrives before the previous byte is read.
  • Ring buffer -> Circular buffer used to store incoming bytes.
  • Command dispatch -> Mapping from parsed command strings to handler functions.

Mental model diagram (ASCII)

UART RX ISR -> ring buffer -> line parser -> command handlers

How it works (step-by-step, with invariants and failure modes)

  1. Configure UART baud rate and frame format (8N1 is common).
  2. Implement RX ISR to push bytes into a ring buffer.
  3. Parse lines in the main loop and dispatch commands.
  4. Print boot diagnostics and system status over UART.
  5. Invariant: buffer does not overflow; failure mode: dropped bytes or framing errors.

Minimal concrete example

// Simple command dispatch
if (strcmp(cmd, "clock" ) == 0) {
print_clock_status();
} else {
printf("ERR: unknown command\n");
}

Common misconceptions

  • UART is always reliable ignores baud mismatch and framing errors.
  • Parsing in ISR is fine ignores latency and buffer overflow risk.
  • If you see gibberish, it’s always wiring often it’s clock or baud configuration.

Check-your-understanding questions

  1. How does a baud rate error show up on the receiver?
  2. Why should the UART RX ISR be minimal?
  3. What is a safe maximum line length for a console?

Check-your-understanding answers

  1. It produces framing errors or garbled characters because sampling points drift.
  2. Long ISRs block other interrupts and increase latency; keep ISR to buffer insertion.
  3. Choose a length based on memory, e.g., 64-128 bytes, and enforce a limit.

Real-world applications

  • Boot-time diagnostics in embedded devices.
  • Command interfaces for sensors and instrumentation.
  • Firmware update consoles and debug shells.

Where you’ll apply it

References

  • STM32F3 Reference Manual (USART chapter).
  • ‘Making Embedded Systems’ (debugging and observability).
  • ST application notes on USART configuration.

Key insights

  • A UART console is only as trustworthy as your clock and buffer discipline.

Summary UART is the simplest window into a running system. A well-designed console provides reliable diagnostics and a safe command interface without compromising real-time behavior.

Homework/Exercises to practice the concept

  1. Implement a ring buffer for UART RX and test with random data.
  2. Add a ‘help’ command that lists available commands.
  3. Measure baud error by intentionally misconfiguring the divider and observing output.

Solutions to the homework/exercises

  1. A ring buffer uses head/tail indices and modulo arithmetic to avoid overflow.
  2. The help command prints a static table of commands and descriptions.
  3. A 5-10% baud error is enough to corrupt characters; fix by correcting clock settings.

Boot and Reset Sequence (Vector Table, Startup, and Memory Map)

Fundamentals On a Cortex-M MCU, power-up and reset are not vague events; they are a strict and observable sequence of reads and writes. The core starts by fetching the initial stack pointer and reset handler from the vector table at a fixed address. It then runs startup code that sets up memory, initializes .data and .bss, and finally calls main. If any of those steps are wrong, nothing else matters: your program never truly starts. For STM32F3, the memory map is fixed by the silicon, and your linker script determines where code and data live. Understanding how the vector table is laid out, how the stack pointer is initialized, and how the reset handler transitions into C code is the difference between ‘it sometimes boots’ and ‘it always boots’. Bring-up work relies on this knowledge because every later peripheral setup is layered on top of a correct reset path.

Deep Dive into the concept The reset sequence on Cortex-M is deterministic and documented, which makes it measurable. At reset, the core reads address 0x00000000 (after any memory remap) to load the initial Main Stack Pointer (MSP). The next word is the reset handler address. This means your linker script and vector table must be in the right place; otherwise, the CPU jumps into invalid memory. Startup code then performs three vital tasks: it configures the vector table location (VTOR if you remap), initializes the data segment by copying initial values from flash to SRAM, and clears the .bss segment to zero. Only after those steps does it call SystemInit() (often to set clocks) and then main(). If any of these steps are skipped, global variables may contain garbage and peripheral drivers may read invalid configuration. On STM32 devices, option bytes and BOOT pins determine whether the device boots from user flash, system memory (factory bootloader), or SRAM. That means a hardware strap or option byte can change the initial vector table base. For robust firmware, you should treat boot configuration as a first-class system requirement: document the expected BOOT pin state, verify it, and log it if possible. Another common subtlety is stack alignment. The Cortex-M requires 8-byte alignment on exception entry when the FPU is used. If your linker or startup code sets a misaligned stack, you will see hard faults only under interrupt load. Memory map knowledge is crucial for fault handling too. The STM32F303 has flash and SRAM ranges, plus peripheral address ranges. When a hard fault occurs, the fault status registers (CFSR, HFSR) and stacked registers point to an address. Knowing whether that address is in flash, SRAM, or peripheral space tells you whether the fault came from a bad pointer, an invalid register access, or an executing-from-data bug. Finally, boot-time initialization controls determinism. If your clock setup depends on external crystals, startup time may vary with temperature or load. If you use the system bootloader for DFU, it may reconfigure clocks or remap memory. A disciplined bring-up includes measuring the reset-to-main latency, capturing the clock source at startup, and asserting that vector table and stack pointers are in range. That is why a ‘boot checklist’ is not busywork; it is a measurable contract between the MCU and your firmware.

How this fit on projects In UART Command Console + Boot Diagnostics, you build a bring-up checklist that proves the reset path is correct. You verify that the vector table is in the right place, that the startup code reached main, and that the system can print or blink predictable outputs without random early faults.

Definitions & key terms

  • Vector table -> An array of exception and interrupt handler addresses located at a fixed base address.
  • Reset handler -> The first code executed after reset; it initializes memory and calls main.
  • MSP/PSP -> Main and Process Stack Pointers used by the core; MSP is used during reset and exceptions.
  • .data/.bss -> Memory sections for initialized and zero-initialized globals in SRAM.
  • BOOT pins -> Hardware straps that select boot source (flash, system memory, or SRAM).

Mental model diagram (ASCII)

Reset -> Read MSP -> Read Reset Handler -> Init data/bss -> SystemInit -> main()
   |               |                     |                     |
   |               v                     v                     v
 Vector Table    Stack Pointer       Memory Layout        App Logic

How it works (step-by-step, with invariants and failure modes)

  1. Reset is asserted; core fetches MSP and reset handler from vector table.
  2. Startup code initializes .data and clears .bss, ensuring globals are valid.
  3. SystemInit configures clocks and vector table relocation if needed.
  4. main() runs; peripheral drivers assume memory and clock configuration are stable.
  5. Invariant: vector table address and stack pointer must point to valid memory; failure mode: hard fault before main.

Minimal concrete example

extern unsigned long _estack;
void Reset_Handler(void);
__attribute__((section(".isr_vector")))
const void* vector_table[] = {
(void*)&_estack,
Reset_Handler,
};
void Reset_Handler(void) {
SystemInit();
main();
}

Common misconceptions

  • Reset just jumps to main ignores memory initialization and vector table requirements.
  • If code compiles, it will boot ignores linker script and BOOT pin configuration.
  • Stack alignment only matters for floating point’ is false; misalignment can break exception entry.

Check-your-understanding questions

  1. Why does the MCU read two words from address 0x00000000 during reset?
  2. What happens if the vector table is placed in the wrong memory region?
  3. How can you prove that .bss was cleared correctly?
  4. Why might a firmware boot correctly when debugging but fail after power cycle?

Check-your-understanding answers

  1. The first word initializes MSP and the second provides the reset handler address so the core knows where to execute.
  2. The CPU jumps to an invalid address, often leading to a hard fault before main.
  3. Place a global variable without initialization and check that it starts at zero after reset.
  4. Debug tools can remap memory or configure clocks; power cycle uses raw BOOT configuration.

Real-world applications

  • Bootloader designs that switch between factory and application firmware.
  • Field-upgradable devices that must always recover from partial updates.
  • Safety-critical systems that log reset causes for post-mortem analysis.

Where you’ll apply it

References

  • Joseph Yiu, ‘The Definitive Guide to ARM Cortex-M3/M4’ (startup and vector tables).
  • STMicroelectronics STM32F3 Reference Manual (memory map and system configuration).
  • Elecia White, ‘Making Embedded Systems’ (bring-up discipline).

Key insights

  • Boot is just a deterministic memory fetch sequence; if you can validate each step, you can trust every later subsystem.

Summary A reliable firmware starts with a reliable reset path. The vector table, stack pointer, and startup code are the first contracts your MCU executes. Proving they are correct is the foundation for every timing, peripheral, and safety claim you make later.

Homework/Exercises to practice the concept

  1. Open the linker map file and identify where the vector table is located.
  2. Write a test that prints the MSP value at boot and verify it is within SRAM.
  3. Simulate a wrong BOOT pin configuration and observe the effect.

Solutions to the homework/exercises

  1. The vector table should live at the flash base address unless you intentionally remap.
  2. On STM32F303, a valid MSP should fall inside the SRAM address range; if not, fix the linker script.
  3. With BOOT0 asserted, the device enters system memory and your application in flash will not start.

Timebase, SysTick, and Measurement Discipline

Fundamentals A reliable timebase is how you reason about time in firmware. SysTick is a dedicated timer tied to the core clock that can generate periodic interrupts. If you configure it for 1 kHz, you get a millisecond tick that becomes your system heartbeat. This tick drives delays, scheduling, timeouts, and timestamps. But SysTick is only reliable if you know the core clock frequency and if you measure the actual timing. Without measurement, a ‘1 ms tick’ is just a guess. A bring-up project must therefore treat SysTick as a calibration point and validate it with real-world observation.

Deep Dive into the concept SysTick is a 24-bit down-counter integrated into the Cortex-M core. It takes the core clock (or core clock divided by 8) and counts down from a reload value to zero, then sets a flag and optionally fires an interrupt. Because it is in the core, it is unaffected by peripheral bus prescalers, which makes it a good reference for measuring the system clock. To configure SysTick, you load the reload register with (core_clock / desired_tick) - 1, select the clock source, enable the counter, and enable its interrupt. The interrupt handler typically increments a global tick counter. On STM32, HAL_Delay and other drivers are often built on this tick. The main danger is implicit coupling: if SysTick is configured incorrectly, every delay and timeout in your system is wrong. Another hazard is jitter. SysTick interrupts can be delayed by higher-priority interrupts, which means the tick is not a precise real-time clock, but a best-effort scheduler. For measurement, you should use SysTick only as a reference and then validate it with a GPIO toggle or timer output. A robust timebase audit logs the computed reload value, the configured clock source, and the observed period. The audit should also include a drift test: toggle a pin every N ticks for several minutes and measure drift relative to a stopwatch or logic analyzer. If drift is observed, the root cause is often clock source accuracy (HSI vs HSE) or an incorrect reload value due to a wrong core clock assumption. You also need to consider wraparound. A 24-bit counter at 72 MHz will wrap quickly if used without an interrupt, and a 32-bit tick counter will wrap after ~49 days at 1 kHz. In embedded systems, you typically handle wraparound by using unsigned arithmetic and comparing time differences rather than absolute values. Finally, you must decide what ‘good enough’ measurement means. For LED blinking, 1-2% accuracy is acceptable. For UART baud or ADC sampling, you need tighter tolerances. The discipline is to tie every time-dependent feature to a measured reference rather than an assumption.

How this fit on projects In UART Command Console + Boot Diagnostics, SysTick is your baseline for clock validation. You compute the reload value, log it, and then validate the resulting tick with physical measurement.

Definitions & key terms

  • SysTick -> Core-integrated timer used for periodic interrupts and timekeeping.
  • Reload value -> The count value loaded into SysTick before it starts counting down.
  • Tick -> A periodic time event, commonly 1 ms in embedded systems.
  • Jitter -> Variation in the timing of periodic events due to interrupt latency.
  • Drift -> Long-term timing error relative to a reference clock.

Mental model diagram (ASCII)

Core clock -> SysTick down-counter -> interrupt -> tick++
                  |
                  v
             timing reference

How it works (step-by-step, with invariants and failure modes)

  1. Compute reload value based on expected core clock and desired tick rate.
  2. Configure SysTick to use core clock and enable interrupt.
  3. Increment a global tick counter in the SysTick handler.
  4. Toggle a GPIO every N ticks and measure the period.
  5. Invariant: tick increments at configured frequency; failure mode: drift or jitter due to wrong clock or interrupt priority.

Minimal concrete example

void SysTick_Handler(void) {
g_tick_ms++;
}
void delay_ms(uint32_t ms) {
uint32_t start = g_tick_ms;
while ((uint32_t)(g_tick_ms - start) < ms) {
    __WFI();
}
}

Common misconceptions

  • SysTick gives precise real-time scheduling ignores interrupt latency and jitter.
  • If LED blinks, tick is correct ignores the need to measure drift over time.
  • Tick counters never overflow ignores wraparound in long-running systems.

Check-your-understanding questions

  1. Why might SysTick interrupts be delayed even if configured correctly?
  2. How do you compute reload for 1 kHz at 72 MHz?
  3. What is the safest way to compare timeouts with wraparound?

Check-your-understanding answers

  1. Higher-priority interrupts can preempt SysTick, causing jitter.
  2. Reload = (72,000,000 / 1,000) - 1 = 71,999.
  3. Use unsigned subtraction: if (now - start) >= timeout.

Real-world applications

  • Scheduling periodic sensor sampling.
  • Timeouts in communication protocols.
  • Measuring CPU load and real-time performance.

Where you’ll apply it

References

  • ARM Cortex-M4 Technical Reference Manual (SysTick).
  • Joseph Yiu, ‘The Definitive Guide to ARM Cortex-M3/M4’ (timers and exceptions).
  • Elecia White, ‘Making Embedded Systems’ (timing discipline).

Key insights

  • A tick is only trustworthy when you can measure it and bound its jitter and drift.

Summary SysTick provides a convenient timebase, but it is only as accurate as your clock configuration and interrupt discipline. Treat it as a calibrated instrument, not a magic delay source.

Homework/Exercises to practice the concept

  1. Compute the reload value for 2 kHz and verify it in code.
  2. Measure the drift of a 1 Hz LED blink over 10 minutes.
  3. Experiment with interrupt priorities and observe SysTick jitter.

Solutions to the homework/exercises

  1. Reload for 2 kHz at 72 MHz is 35,999.
  2. A 1 Hz blink should be 600 seconds over 10 minutes; log the error and compute percent drift.
  3. Setting a higher-priority timer interrupt increases SysTick jitter; adjust priorities if needed.

3. Project Specification

3.1 What You Will Build

A UART command console with commands for status, clock report, and reset cause, plus boot-time banners.

3.2 Functional Requirements

  1. Console Input: Receive lines with backspace and line editing.
  2. Command Dispatch: Support commands: help, clock, reset, status.
  3. Boot Diagnostics: Print clock source, reset cause, firmware version.
  4. Error Handling: Graceful response to unknown commands.

3.3 Non-Functional Requirements

  • Performance: Commands processed within 50 ms.
  • Reliability: No buffer overflow on long input lines.
  • Usability: Readable command responses and help menu.

3.4 Example Usage / Output

BOOT v1.0
RESET_CAUSE=POR
CLK=PLL SYSCLK=72MHz
> help
Commands: help, status, clock, reset

3.5 Data Formats / Schemas / Protocols

Command format: <cmd> [args] Error format: ERR: <message>

3.6 Edge Cases

  • Baud mismatch causing garbled input.
  • Input line longer than buffer.
  • Backspace handling with empty buffer.
  • Reset cause flags not cleared.

3.7 Real World Outcome

You will interact with the device over UART and obtain clear boot diagnostics.

3.7.1 How to Run (Copy/Paste)

$ make flash
$ screen /dev/tty.usbmodem* 115200

3.7.2 Golden Path Demo (Deterministic)

  • Power cycle the board and run status and clock commands.

3.7.3 CLI Transcript (Success)

BOOT v1.0
RESET_CAUSE=POR
> clock
CLK_SRC=PLL SYSCLK=72000000
> status
OK
RESULT=PASS
# Exit code: 0

3.7.4 Failure Demo (Bad Baud)

??
ERR: invalid input
RESULT=FAIL
# Exit code: 2

4. Solution Architecture

UART RX ISR fills a ring buffer; main loop parses lines and executes commands.

4.1 High-Level Design

UART RX -> Ring Buffer -> Line Parser -> Command Handlers

4.2 Key Components

Component Responsibility Key Decisions
RX Buffer Stores incoming bytes Ring buffer with overflow protection
Parser Builds lines and tokenizes commands Whitespace-delimited tokens
Diagnostics Provides system info Read reset cause and clock registers

4.3 Data Structures (No Full Code)

typedef struct {
char buf[128];
uint16_t head;
uint16_t tail;
} ringbuf_t;

4.4 Algorithm Overview

Line Parsing

  1. Read bytes from ring buffer.
  2. Handle backspace and line ending.
  3. Dispatch command handler.

Complexity: O(N) per line.


5. Implementation Guide

5.1 Development Environment Setup

make init
make flash
screen /dev/tty.usbmodem* 115200

5.2 Project Structure

project-root/
|-- src/
|   |-- main.c
|   |-- drivers/
|   `-- app/
|-- include/
|-- Makefile
`-- README.md

5.3 The Core Question You’re Answering

“How do I get reliable diagnostics out of a running MCU?”

5.4 Concepts You Must Understand First

  1. UART framing and baud calculation.
  2. Ring buffer design.
  3. Boot and reset cause flags.

5.5 Questions to Guide Your Design

  1. What is the maximum command length you will support?
  2. How will you avoid blocking in the UART ISR?
  3. What diagnostics are most valuable at boot?

5.6 Thinking Exercise

Command Parser

Input: "clock" -> handler prints clock report
Input: "status" -> handler prints OK

5.7 The Interview Questions They’ll Ask

  1. How do you design a UART command console?
  2. What causes UART framing errors?
  3. How do you log reset cause on boot?

5.8 Hints in Layers

Hint 1: Keep ISR minimal; only push to buffer. Hint 2: Use a fixed line length to avoid overflow. Hint 3: Print boot banner before entering main loop.

5.9 Books That Will Help

Topic Book Chapter
UART details STM32 Reference Manual USART chapter
Debugging Making Embedded Systems Ch. 12

5.10 Implementation Phases

Phase 1: UART RX/TX (2-3 days)

Get basic input and output working.

Phase 2: Command Parser (4 days)

Add line parsing and command dispatch.

Phase 3: Diagnostics (3 days)

Add boot and clock reports.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Parsing Tokenized vs raw Tokenized Simpler command handlers
Buffer size 64 vs 128 128 Allows longer commands

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Parser logic Backspace handling
Integration Tests UART command flow Help and status commands
Edge Case Tests Overflow handling Long line input

6.2 Critical Test Cases

  1. Help Command: Lists all commands.
  2. Clock Command: Shows correct clock source and frequency.
  3. Overflow: Long input triggers error without crash.

6.3 Test Data

Input: help
Output: Commands: help, status, clock, reset

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Blocking in ISR Missed bytes Keep ISR minimal
No baud verification Garbled input Validate clock and baud
Uncleared reset flags Wrong reset cause Clear flags after reading

7.2 Debugging Strategies

  • Use a known-good terminal at 115200 baud.
  • Echo characters to validate RX.
  • Print raw reset flags for verification.

7.3 Performance Traps

Parsing large commands in the ISR will increase latency and drop bytes.


8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a version command.

8.2 Intermediate Extensions

  • Add a configuration command that writes to flash.

8.3 Advanced Extensions

  • Implement a mini shell with history and tab completion.

9. Real-World Connections

9.1 Industry Applications

  • Field diagnostics: Remote debug via UART console.
  • Factory test: Manufacturing tests using command interface.
  • mbed CLI examples: Simple embedded console patterns.
  • Zephyr shell: Embedded command framework.

9.3 Interview Relevance

  • UART framing and command parsing questions.
  • Boot diagnostics and reset cause discussion.

10. Resources

10.1 Essential Reading

  • STM32 Reference Manual by ST - USART and reset flags.
  • Making Embedded Systems by Elecia White - Diagnostics and observability.

10.2 Video Resources

  • Building a UART console on STM32.
  • UART baud rate calculation tutorial.

10.3 Tools & Documentation

  • Serial terminal (screen/minicom): Interact with UART console.
  • STM32CubeIDE: Build and flash.

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain UART framing and baud selection.
  • I can design a simple command parser.
  • I can interpret reset cause flags.

11.2 Implementation

  • Console accepts commands reliably.
  • Boot diagnostics print correctly.
  • Errors handled without crashing.

11.3 Growth

  • I can extend the console with new commands.
  • I can diagnose UART issues in the field.
  • I can explain this system in interviews.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • UART console works.
  • At least 3 commands implemented.
  • Boot diagnostics printed.

Full Completion:

  • Error handling and overflow protection.
  • Reset cause logging verified.

Excellence (Going Above & Beyond):

  • Configuration storage and advanced shell features.