Part 8: How to Build Amazing UART Command Control

In Part 7, we saw how the main loop checks for UART input every millisecond. When a character is received, the command processing begins. Let’s dive deep into how this interactive command system works and how it controls our task scheduler and power management.

The Command Processing Structure

When UART_IsDataAvailable() returns true, we enter the command handling section:

if (UART_IsDataAvailable()) {
    char cmd = UART_ReceiveChar();
    UART_SendChar(cmd);  /* Echo back */
    
    switch(cmd) {
        // ... cases for each command ...
    }
}

Why process single characters instead of complete command strings? Processing one character at a time:

  • No buffer management needed (saves RAM)
  • Instant response (no waiting for Enter key)
  • Can’t overflow a command buffer
  • Simpler error handling
  • Common in embedded systems with limited resources

What happens at the hardware level when a character arrives?

  1. UART hardware receives 10 bits (1 start + 8 data + 1 stop)
  2. Places character in USART3->DR register
  3. Sets RXNE flag in USART3->SR
  4. Character waits until software reads it
  5. Reading DR automatically clears RXNE flag

Understanding Character Echo

char cmd = UART_ReceiveChar();
UART_SendChar(cmd);  /* Echo back */

What exactly happens during echo? Let’s trace the complete flow:

  1. UART_ReceiveChar() reads from USART3->DR: char UART_ReceiveChar(void) { while (!(USART3->SR & USART_SR_RXNE)); // Wait if no data return (char)(USART3->DR & 0xFF); // Read lowest 8 bits }
  2. UART_SendChar() writes to USART3->DR: void UART_SendChar(char c) { while (!(USART3->SR & USART_SR_TXE)); // Wait if TX busy USART3->DR = c; // Send character }

Timing analysis:

  • Read character: ~1μs (register access)
  • Check TXE flag: ~1μs (usually empty)
  • Send character: ~87μs at 115200 baud
  • Total echo time: ~89μs

What if the user types faster than echo completes? The UART hardware has:

  • 1-character receive buffer (DR register)
  • 1-character transmit buffer
  • If second character arrives during echo: stored in DR
  • If third character arrives: overrun error (data lost)

At 115200 baud, characters must be >87μs apart. Human typing is typically >100ms between characters, so no problem.

Why not echo in the terminal software instead? Device-side echo:

  • Works with any terminal
  • Confirms device received character
  • Shows actual system responsiveness
  • Industry standard for simple interfaces

The Command Switch Statement

The switch statement processes single-character commands:

switch(cmd) {
    case '1':
        /* Sleep mode */
        break;
    case '2':
        /* Stop mode */
        break;
    case '3':
        /* Standby mode */
        break;
    case 'p':
        /* Power info */
        break;
    case 'd':
        /* Scheduler status */
        break;
    case 't':
        /* Control specific task */
        break;
    case 's':
        /* System stats */
        break;
    case 'r':
        /* Resume all tasks */
        break;
    case 'h':
    case '?':
        /* Help menu */
        break;
    case '\r':
    case '\n':
        /* Ignore newlines */
        break;
    default:
        /* Unknown command */
        break;
}

Why use switch instead of if-else chains? The compiler optimizes switch statements:

  • Creates a jump table for dense values
  • O(1) lookup time vs O(n) for if-else
  • More readable for many cases
  • Compiler can warn about missing breaks

How does the jump table work? For our commands, compiler might generate:

  1. Subtract smallest case value (‘1’ = 49)
  2. Check if result is within range
  3. Use result as index into address table
  4. Jump directly to case code

What’s the memory cost?

  • Jump table: ~40 entries × 4 bytes = 160 bytes
  • If-else chain: more code but no table
  • Switch is faster but uses more Flash

Power Mode Commands (1, 2, 3)

Command ‘1’ – Sleep Mode

case '1':
    UART_SendString("\r\nEntering Sleep mode - press any key to wake up...\r\n");
    SysTick_Delay(10);
    PowerMgmt_EnterLowPowerMode(POWER_MODE_SLEEP);
    UART_SendString("Woke up from Sleep mode!\r\n");
    break;

Step-by-step execution:

  1. Send announcement message (52 characters = ~4.5ms at 115200 baud)
  2. Delay 10ms to ensure transmission completes
  3. Enter sleep mode
  4. CPU halts at WFI instruction
  5. Wake on any interrupt (UART RXNE)
  6. Continue from instruction after WFI

What happens in hardware during Sleep mode?

Before Sleep:          During Sleep:         After Wake:
CPU: Running (15mA)    CPU: Stopped (5mA)    CPU: Running (15mA)
APB1: 16MHz           APB1: 16MHz            APB1: 16MHz
APB2: 16MHz           APB2: 16MHz            APB2: 16MHz
SysTick: Counting     SysTick: Counting      SysTick: Continued
UART: Active          UART: Active           UART: Active
Flash: Active         Flash: Low-power       Flash: Active

Current consumption breakdown:

  • CPU running: ~9-10mA
  • Peripherals: ~4-5mA
  • Flash: ~1mA
  • Total Sleep mode: ~5mA (CPU savings only)

Wake-up latency:

  • Interrupt occurs (UART RXNE)
  • CPU wakes in ~2 clock cycles
  • Total wake time: <1μs
  • No clock reconfiguration needed

What’s the WFI instruction? WFI (Wait For Interrupt):

void PowerMgmt_EnterLowPowerMode(PowerMode_t mode) {
    if (mode == POWER_MODE_SLEEP) {
        __WFI();  // ARM instruction: stops CPU until interrupt
    }
}

The CPU pipeline stops, but all peripherals continue running.

Command ‘2’ – Stop Mode

case '2':
    UART_SendString("\r\nEntering Stop mode - press any key to wake up...\r\n");
    SysTick_Delay(10);
    PowerMgmt_EnterLowPowerMode(POWER_MODE_STOP);
    UART_SendString("Woke up from Stop mode!\r\n");
    UART_SendString("Note: System clock was restored\r\n");
    break;

Hardware state in Stop mode:

Before Stop:           During Stop:          After Wake:
CPU: Running           CPU: Stopped          CPU: Running
HSI: 16MHz            HSI: Stopped          HSI: 16MHz (restarted)
APB1/APB2: Active     APB1/APB2: Stopped    APB1/APB2: Active
SysTick: Counting     SysTick: Stopped      SysTick: Resumes
UART: Active          UART: Clock stopped   UART: Active
Flash: Active         Flash: Powered down   Flash: Active
Voltage Reg: Normal   Voltage Reg: Low-pwr  Voltage Reg: Normal
Current: ~15mA        Current: ~3μA         Current: ~15mA

Stop mode entry sequence in hardware:

  1. WFI instruction executed
  2. CPU stops fetching instructions
  3. HSI oscillator disabled
  4. All clocks stop
  5. Voltage regulator enters low-power mode
  6. Flash memory powered down
  7. Only EXTI lines and RTC remain active

Wake-up sequence (takes ~5μs):

  1. EXTI interrupt from UART pin
  2. Voltage regulator returns to normal (~2μs)
  3. HSI oscillator restarts (~3μs)
  4. Flash memory powers up
  5. CPU resumes from WFI instruction
  6. Software must reconfigure any affected peripherals

Impact on systick_counter:

Enter Stop at: systick_counter = 5000
Stop duration: 30 seconds (real time)
Wake up at:    systick_counter = 5000 (unchanged!)

Time tracking is lost because all clocks stopped.

Command ‘3’ – Standby Mode

case '3':
    UART_SendString("\r\nEntering Standby mode - system will reset on wake up!\r\n");
    UART_SendString("Press RESET button or WKUP pin to wake up...\r\n");
    SysTick_Delay(10);
    PowerMgmt_EnterLowPowerMode(POWER_MODE_STANDBY);
    /* Code never reaches here - system resets on wake */
    break;

What’s preserved in Standby mode?

Lost on Standby:           Preserved:
All RAM contents          RTC registers
All register values       Backup domain (20 bytes)
GPIO configurations       WKUP pin configuration
Clock configurations      Reset flags
Peripheral states         
Program counter           
Stack pointer             

Standby mode power consumption:

  • Core: 0μA (completely off)
  • Backup domain: ~1.5μA (RTC + backup registers)
  • WKUP pin logic: ~0.5μA
  • Total: ~2μA

Wake-up sources in Standby:

  1. WKUP pin (PA0) – Rising edge
  2. RTC alarm – If configured
  3. External reset – NRST pin
  4. IWDG reset – If watchdog enabled

The wake-up sequence:

  1. Wake event detected
  2. System performs power-on reset
  3. Bootloader runs (~2ms)
  4. main() starts from beginning
  5. All initialization repeats
  6. Can check PWR->CSR for standby flag

How to detect wake from Standby:

if (PWR->CSR & PWR_CSR_SBF) {
    // Woke from standby
    PWR->CR |= PWR_CR_CSBF;  // Clear flag
    // Restore context from backup registers
}

Why include Standby in a scheduler demo? Real IoT applications might:

  • Sleep 23 hours, wake for 1 minute
  • Take sensor reading, transmit, standby
  • Battery life: years instead of days

Command ‘p’ – Power Management Info

case 'p': {
    /* Show power management information */
    char msg[200];
    uint32_t rtc_time = RTC_GetCounter();
    
    UART_SendString("\r\n=== Power Management Info ===\r\n");
    
    snprintf(msg, sizeof(msg), 
             "RTC Time: %lu seconds\r\n"
             "Wake-up source: %s\r\n"
             "Current power mode: RUN\r\n",
             rtc_time,
             PowerMgmt_GetWakeupSourceString());
    UART_SendString(msg);
    
    /* Show RTC alarm if set */
    if (RTC->CRH & RTC_CRH_ALRIE) {
        uint32_t alarm_time = (RTC->ALRH << 16) | RTC->ALRL;
        snprintf(msg, sizeof(msg), 
                 "RTC Alarm set for: %lu seconds\r\n", 
                 alarm_time);
        UART_SendString(msg);
    }
    
    UART_SendString("=============================\r\n");
    break;
}

Why declare msg[200] inside a brace block?

case 'p': {  // Opening brace creates new scope
    char msg[200];  // Allocated on stack
    // ... use msg ...
}  // Closing brace deallocates msg

Without braces, you get a compile error:

case 'p':
    char msg[200];  // ERROR: declaration after label

Stack usage analysis:

  • msg[200]: 200 bytes
  • rtc_time: 4 bytes
  • alarm_time: 4 bytes
  • Total: 208 bytes

Stack frame during command:

Higher addresses
+----------------+
| Previous data  |
+----------------+
| Return address |
+----------------+
| msg[199]       |
| ...            |
| msg[0]         | <- 200 bytes
+----------------+
| rtc_time       | <- 4 bytes
+----------------+
| alarm_time     | <- 4 bytes
+----------------+ <- Stack pointer
Lower addresses

After leaving scope, stack pointer moves up 208 bytes, reclaiming space.

How snprintf prevents buffer overflow:

snprintf(msg, sizeof(msg), format, ...);
  • First parameter: destination buffer
  • Second parameter: maximum bytes to write (including ‘\0’)
  • Returns: number of bytes that would be written (excluding ‘\0’)
  • Guarantee: never writes more than size parameter

Reading RTC registers:

uint32_t alarm_time = (RTC->ALRH << 16) | RTC->ALRL;

RTC alarm is stored in two 16-bit registers:

  • ALRH: High 16 bits (bits 31-16)
  • ALRL: Low 16 bits (bits 15-0)
  • Combined: 32-bit seconds value

Why check ALRIE bit?

if (RTC->CRH & RTC_CRH_ALRIE)
  • ALRIE = Alarm Interrupt Enable
  • If set, alarm is configured
  • If clear, alarm time is meaningless
  • Prevents displaying garbage values

Command ‘d’ – Scheduler Status

case 'd': {
    TaskSchedulerStatus_t status;
    TaskScheduler_GetStatus(&status);
    UART_SendString(status.statusMessage);
    break;
}

This calls TaskScheduler_GetStatus() which builds a comprehensive status report:

void TaskScheduler_GetStatus(TaskSchedulerStatus_t* status) {
    char* ptr = status->statusMessage;
    int remaining = sizeof(status->statusMessage);
    int written;
    
    /* Header */
    written = snprintf(ptr, remaining, 
                      "\r\n=== Task Scheduler Status ===\r\n"
                      "Registered tasks: %u/%u\r\n"
                      "System uptime: %lu ms\r\n\r\n",
                      registeredTaskCount, MAX_TASKS, systick_counter);

Understanding the buffer management pattern:

char* ptr = status->statusMessage;        // Start of buffer
int remaining = sizeof(status->statusMessage);  // 1024 bytes

After each snprintf:

if (written > 0 && written < remaining) {
    ptr += written;         // Move pointer forward
    remaining -= written;   // Reduce available space
}

Visual representation of buffer filling:

Initial state:
[________________________________] 1024 bytes available
^ptr

After header (written=80):
[Header text....................] 944 bytes remaining
                ^ptr

After task 0 info (written=120):
[Header text...Task 0 info......] 824 bytes remaining
                            ^ptr

Why track ‘remaining’ separately?

  • snprintf needs to know buffer space left
  • sizeof() gives total size, not remaining
  • Prevents writing past buffer end
  • Standard pattern for safe string building

The complete status includes:

=== Task Scheduler Status ===
Registered tasks: 6/8
System uptime: 45678 ms

Task 0: LED1_Green
  State: READY, ID: 0
  Period: 500ms, Priority: HIGH
  Last run: 45500ms ago
  Max execution: 0ms, Max jitter: 2ms

[Similar for all 6 tasks...]

Memory layout of TaskSchedulerStatus_t:

typedef struct {
    char statusMessage[1024];
} TaskSchedulerStatus_t;

Stack allocation: 1024 bytes temporarily

Command ‘t’ – Task Control Submenu

case 't': {
    UART_SendString("\r\nTask Control:\r\n");
    UART_SendString("Enter task ID (0-5) followed by action:\r\n");
    UART_SendString("  s: stop, p: suspend, r: resume\r\n");
    UART_SendString("Example: '0s' stops task 0\r\n> ");
    
    /* Wait for task ID */
    while (!UART_IsDataAvailable()) {
        SysTick_Delay(10);
    }
    char taskIdChar = UART_ReceiveChar();
    UART_SendChar(taskIdChar);

This implements a two-stage command parser:

  1. First character: task ID (0-5)
  2. Second character: action (s/p/r)

State machine visualization:

IDLE --'t'--> WAIT_FOR_ID --'0-5'--> WAIT_FOR_ACTION --'s/p/r'--> EXECUTE --> IDLE
                |                           |
                +--(invalid)----------------+--(invalid)--> ERROR --> IDLE

Why use blocking waits here?

while (!UART_IsDataAvailable()) {
    SysTick_Delay(10);
}

During this wait:

  • Main loop is blocked
  • Tasks don’t execute
  • System appears frozen

Trade-offs:

  • Simpler code (no state variables)
  • User expects immediate response
  • Command is quick (two characters)
  • Alternative: Non-blocking state machine

The 10ms delay impact: Without delay:

while (!UART_IsDataAvailable());  // Tight loop
  • CPU at 100% checking UART
  • Millions of register reads per second
  • No power saving
  • Same responsiveness

With 10ms delay:

  • Check UART 100 times/second
  • CPU can sleep between checks
  • Power consumption reduced
  • 10ms max latency (imperceptible)

Processing the task command:

    if (taskIdChar >= '0' && taskIdChar <= '5') {
        uint8_t taskId = taskIdChar - '0';
        
        /* Wait for action */
        while (!UART_IsDataAvailable()) {
            SysTick_Delay(10);
        }
        char action = UART_ReceiveChar();
        UART_SendChar(action);

ASCII to integer conversion explained:

Character | ASCII Value | Calculation    | Result
----------|-------------|----------------|--------
   '0'    |     48      | 48 - 48        |   0
   '1'    |     49      | 49 - 48        |   1
   '2'    |     50      | 50 - 48        |   2
   '3'    |     51      | 51 - 48        |   3
   '4'    |     52      | 52 - 48        |   4
   '5'    |     53      | 53 - 48        |   5

Why subtract ‘0’ instead of 48?

  • More readable/self-documenting
  • Compiler replaces ‘0’ with 48
  • Works for any character encoding
  • Standard C idiom

Input validation layers:

  1. Character range check: ‘0’ <= char <= ‘5’
  2. Task existence check: taskId < registeredTaskCount
  3. Action validity check: action is ‘s’, ‘p’, or ‘r’
  4. State compatibility: Can’t resume a running task

Task actions:

        switch(action) {
            case 's':  /* Stop task */
                if (TaskScheduler_StopTask(taskId) == TASK_OK) {
                    snprintf(msg, sizeof(msg), 
                            "\r\nTask %u stopped\r\n", taskId);
                } else {
                    snprintf(msg, sizeof(msg), 
                            "\r\nFailed to stop task %u\r\n", taskId);
                }
                break;

Task state transitions:

READY <--Resume-- SUSPENDED --Suspend--> READY
  |                                        |
  +------------Stop--> STOPPED <--Start----+

What each action does:

  • Stop: Sets state to STOPPED, task won’t run
  • Suspend: Sets state to SUSPENDED, preserves timing
  • Resume: Returns SUSPENDED task to READY

Timing preservation example:

Task period: 1000ms
Last run: 5000ms
Suspend at: 5200ms
Resume at: 8200ms
Next run: 6000ms (not 9200ms!)

The task maintains its original schedule.

Command ‘s’ – System Statistics

case 's': {
    /* Show detailed system statistics */
    char msg[150];
    UART_SendString("\r\n=== System Statistics ===\r\n");
    
    snprintf(msg, sizeof(msg),
             "CPU Frequency: %lu Hz\r\n"
             "System Uptime: %lu ms\r\n"
             "Total RAM: 256 KB\r\n"
             "Total Flash: 2 MB\r\n",
             SystemCoreClock,
             systick_counter);
    UART_SendString(msg);

Where does SystemCoreClock come from? SystemCoreClock is a CMSIS standard variable:

uint32_t SystemCoreClock = 16000000U;  // Set during init

Updated by:

  • SystemInit() at startup
  • SystemCoreClockUpdate() after clock changes
  • Reflects actual running frequency

Memory organization on STM32F429ZI:

Flash Memory Map:              RAM Memory Map:
0x08000000 +---------+        0x20000000 +---------+
           | Vector  |                   | Stack   | <- Grows down
           | Table   |                   |   ↓     |
0x080000C0 +---------+                   |         |
           | Code    |                   | Free    |
           |         |                   |         |
           |         |                   |   ↑     |
           | Data    |                   | Heap    | <- Grows up
           |         |        0x20030000 +---------+
0x081FFFFF +---------+                   | CCM RAM | <- 64KB
           2MB Total          0x2003FFFF +---------+
                                         256KB Total

Why hardcode memory sizes?

"Total RAM: 256 KB\r\n"
"Total Flash: 2 MB\r\n"

These are chip constants:

  • STM32F429ZI always has these sizes
  • No runtime API to query
  • Linker knows actual usage
  • Could calculate free space using symbols

More useful statistics could include:

extern uint32_t _estack;     // Top of stack
extern uint32_t _Min_Stack_Size;  // Required stack
extern uint32_t _end;        // End of BSS
extern uint32_t _sdata;      // Start of data

uint32_t heap_used = (uint32_t)sbrk(0) - (uint32_t)&_end;
uint32_t stack_used = (uint32_t)&_estack - (uint32_t)__get_MSP();

But our simple demo keeps it basic.

Command ‘r’ – Resume All Tasks

case 'r':
    UART_SendString("\r\nResuming all tasks...\r\n");
    /* Resume all tasks */
    for (uint8_t i = 0; i < MAX_TASKS; i++) {
        TaskScheduler_ResumeTask(i);
    }
    UART_SendString("All tasks resumed\r\n");
    break;

Loop execution analysis:

for (uint8_t i = 0; i < MAX_TASKS; i++) {
    TaskScheduler_ResumeTask(i);
}

With MAX_TASKS = 8:

  • 8 function calls
  • Each call ~10μs
  • Total: ~80μs
  • Negligible compared to UART messages

Inside TaskScheduler_ResumeTask():

TaskError_t TaskScheduler_ResumeTask(uint8_t taskId) {
    for (uint8_t i = 0; i < MAX_TASKS; i++) {
        if (tasks[i].taskId == taskId) {
            if (tasks[i].state == TASK_STATE_SUSPENDED) {
                tasks[i].state = TASK_STATE_READY;
                // Timing preserved - lastExecuted unchanged
            }
            return TASK_OK;
        }
    }
    return TASK_ERROR_NOT_FOUND;
}

State transitions for all tasks:

Task | Before Resume | After Resume | Effect
-----|---------------|--------------|--------
  0  | READY         | READY        | None
  1  | SUSPENDED     | READY        | Resumed
  2  | STOPPED       | STOPPED      | None
  3  | SUSPENDED     | READY        | Resumed
  4  | READY         | READY        | None
  5  | READY         | READY        | None

Only SUSPENDED tasks change state.

Use case scenario:

  1. Testing system with multiple tasks
  2. Suspend tasks 1, 3, 4 for isolation
  3. Test task 0 in isolation
  4. Press ‘r’ to restore normal operation
  5. All suspended tasks resume immediately

Why not track which tasks to resume?

  • Simpler to attempt all
  • Resume is idempotent (safe to call multiple times)
  • Fast enough (~80μs)
  • No memory needed for tracking

Command ‘h’ or ‘?’ – Help Menu

case 'h':
case '?':
    UART_SendString("\r\n=== Command Menu ===\r\n");
    UART_SendString("1: Enter Sleep mode\r\n");
    UART_SendString("2: Enter Stop mode\r\n");
    UART_SendString("3: Enter Standby mode (reset on wake)\r\n");
    UART_SendString("p: Show power management info\r\n");
    UART_SendString("d: Display task scheduler status\r\n");
    UART_SendString("t: Task control (stop/suspend/resume)\r\n");
    UART_SendString("s: System statistics\r\n");
    UART_SendString("r: Resume all tasks\r\n");
    UART_SendString("h/?: Show this help menu\r\n");
    UART_SendString("===================\r\n");
    break;

Why support both ‘h’ and ‘?’:

  • ‘h’ = “help” (Unix/Linux convention)
  • ‘?’ = Universal “what can I do?” symbol
  • Different users have different habits
  • Zero cost to support both (fall-through case)

Help menu design principles:

  1. Concise descriptions – One line per command
  2. Logical grouping:
    • Power modes (1,2,3)
    • Information (p,d,s)
    • Control (t,r)
    • Help (h,?)
  3. Visual structure – Separators and alignment
  4. Self-documenting – Shows ‘h/?’ options

Transmission time:

11 lines × ~30 chars/line = 330 characters
330 chars × 87μs/char = 28.7ms

During help display:

  • Tasks delayed by ~29ms
  • Acceptable for user-initiated action
  • Could reduce by removing blank lines

Handling Newlines

case '\r':
case '\n':
    /* Ignore carriage return and newline */
    break;

Why different line endings exist:

SystemLine EndingHex ValuesHistory
Unix/LinuxLF0x0ALine Feed only
WindowsCR+LF0x0D 0x0ACarriage Return + Line Feed
Old MacCR0x0DCarriage Return only
EmbeddedVariesAnyDepends on terminal

What happens without this handling: User presses Enter → Terminal sends CR+LF:

  1. CR (0x0D) processed → “Unknown command”
  2. LF (0x0A) processed → “Unknown command”
  3. Two error messages for one Enter!

Terminal behavior variations:

// PuTTY might send:
'h' <CR>         // Just CR
'h' <LF>         // Just LF  
'h' <CR><LF>     // Both

// Arduino Serial Monitor sends:
'h' <CR><LF>     // Always both

// Raw terminal might send:
'h'              // Nothing!

By ignoring both CR and LF, we handle all cases gracefully.

Unknown Commands

default:
    UART_SendString("\r\nUnknown command. Press 'h' for help.\r\n");
    break;

Default case behavior:

  • Catches all unhandled characters
  • ASCII values 0-255 possible
  • Includes control characters
  • Non-printable characters

What triggers default case:

Character | ASCII | Printable | Result
----------|-------|-----------|--------
   'x'    |  120  |    Yes    | Unknown command
   'X'    |  88   |    Yes    | Unknown command  
   ' '    |  32   |    Yes    | Unknown command
  ESC     |  27   |    No     | Unknown command
  NULL    |   0   |    No     | Unknown command

Message design choices:

  1. Brief – Not annoying on typos
  2. Helpful – Suggests solution
  3. Clear – States what happened
  4. Newlines – Maintains formatting

Alternative approaches:

// Silent ignore
default:
    break;

// Echo invalid character  
default:
    UART_SendString("\r\nUnknown: '");
    UART_SendChar(cmd);
    UART_SendString("'\r\n");
    break;

// Beep/bell
default:
    UART_SendChar(0x07);  // ASCII BEL
    break;

Our approach balances helpfulness with brevity.

Command Processing Performance

Timing analysis for each operation:

OperationTimeCalculation
Character receive87μs10 bits @ 115200 baud
UART_IsDataAvailable()<1μsSingle register read
UART_ReceiveChar()<1μsRegister read + clear flag
UART_SendChar()87μs10 bits @ 115200 baud
Simple command (‘r’)~100μsFunction calls + loop
Status display (‘d’)~20msBuilding + sending ~500 chars
Power mode entry10msWait for UART + mode switch

Detailed timing for status command:

Building message: ~500μs
- snprintf calls: ~50μs each
- String operations: ~100μs total
- Loop overhead: ~100μs

Sending message: ~19.5ms
- 500 characters × 87μs/char = 43.5ms
- But UART has 16-byte FIFO
- Effective: ~20ms with FIFO

Impact on task scheduling: During 20ms status display:

Time    Expected Task    Actual      Delay
1000ms  LED1            1000ms      0ms (before cmd)
1020ms  ---             blocked     ---
1500ms  LED1            1520ms      20ms (after cmd)

All tasks experience 20ms jitter when status is displayed.

CPU cycles per operation at 16MHz:

Register read:      ~3 cycles    (0.2μs)
Register write:     ~3 cycles    (0.2μs)
Function call:      ~15 cycles   (0.9μs)
Switch jump:        ~5 cycles    (0.3μs)
snprintf (simple):  ~800 cycles  (50μs)

Command Buffer Considerations

Our system processes single characters immediately. Alternative designs:

Line buffering implementation:

#define CMD_BUFFER_SIZE 80
typedef struct {
    char buffer[CMD_BUFFER_SIZE];
    uint8_t index;
    bool complete;
} CommandBuffer_t;

CommandBuffer_t cmd_buf = {0};

void ProcessUART(void) {
    if (UART_IsDataAvailable()) {
        char c = UART_ReceiveChar();
        
        if (c == '\r' || c == '\n') {
            cmd_buf.buffer[cmd_buf.index] = '\0';
            cmd_buf.complete = true;
        } else if (c == '\b' && cmd_buf.index > 0) {
            // Backspace handling
            cmd_buf.index--;
            UART_SendString("\b \b");  // Erase character
        } else if (cmd_buf.index < CMD_BUFFER_SIZE - 1) {
            cmd_buf.buffer[cmd_buf.index++] = c;
            UART_SendChar(c);  // Echo
        }
    }
    
    if (cmd_buf.complete) {
        ProcessCommand(cmd_buf.buffer);
        cmd_buf.index = 0;
        cmd_buf.complete = false;
    }
}

Memory comparison:

MethodRAM UsageCode SizeFeatures
Single char1 byte~200 bytesInstant response
Line buffer80+ bytes~500 bytesEditing, parameters
Ring buffer256+ bytes~800 bytesMultiple commands

State machine for line buffering:

IDLE --char--> BUILDING --'\n'--> COMPLETE --process--> IDLE
        ^          |                             |
        |          +--'\b'-> BACKSPACE          |
        +--buffer full--> ERROR ----------------+

Why we chose single character:

  1. Simplicity – No buffer management
  2. Reliability – Can’t overflow
  3. Memory – Minimal RAM usage
  4. Response – Instant feedback
  5. Sufficient – Meets our needs

Error Recovery

What if UART gets corrupted data?

UART error detection:

Start  D0 D1 D2 D3 D4 D5 D6 D7  Parity  Stop
  0    1  0  1  1  0  1  0  1     ?      1

Hardware checks:

  • Framing error: Stop bit not detected
  • Overrun error: New data before old data read
  • Parity error: Parity bit incorrect (if enabled)
  • Noise error: Inconsistent bit sampling

Error flags in USART3->SR:

if (USART3->SR & USART_SR_FE) {  // Framing error
    // Read DR to clear
    volatile uint32_t dummy = USART3->DR;
}

Our simple system’s resilience:

  • Single character commands tolerate errors
  • Bad character → “Unknown command” → recover
  • Next character starts fresh
  • No command state to corrupt

What if a command crashes?

Fault scenarios and outcomes:

Fault TypeCauseResultRecovery
HardFaultNull pointerSystem haltsReset required
MemManageStack overflowSystem haltsReset required
BusFaultInvalid addressSystem haltsReset required
UsageFaultUndefined instructionSystem haltsReset required

Minimal fault handler:

void HardFault_Handler(void) {
    // Save fault info
    uint32_t *stack_ptr = (uint32_t*)__get_MSP();
    uint32_t pc = stack_ptr[6];  // Program counter
    uint32_t lr = stack_ptr[5];  // Link register
    
    // Could save to backup registers
    // Then reset
    NVIC_SystemReset();
}

Watchdog protection:

// Enable IWDG with 1 second timeout
IWDG->KR = 0x5555;  // Enable access
IWDG->PR = 4;       // Prescaler /64
IWDG->RLR = 625;    // 1 second @ 40kHz/64
IWDG->KR = 0xCCCC;  // Start watchdog

// In main loop
IWDG->KR = 0xAAAA;  // Feed watchdog

If command hangs, watchdog resets system after 1 second.

Integration with Task Scheduler

Commands interact with scheduler in several ways:

1. Direct Task Control

TaskScheduler_StopTask(0)     // Changes tasks[0].state to STOPPED
TaskScheduler_SuspendTask(1)  // Changes tasks[1].state to SUSPENDED  
TaskScheduler_ResumeTask(1)   // Changes tasks[1].state to READY

State transitions are atomic – no race conditions since we’re not using interrupts for scheduling.

2. Timing Impact

Normal operation:          With command processing:
Time  Action               Time  Action
1000  Check tasks          1000  Check tasks
1001  Delay 1ms            1001  Process 'd' command
1002  Check tasks          1021  Still processing...
1003  Check tasks          1022  Back to normal

The 20ms command blocks all task execution.

3. Power Mode Effects

ModeTasksSysTickTime TrackingWake Time
RunExecute normallyRunningAccurateN/A
SleepPausedRunningAccurate<1μs
StopPausedStoppedLost~5μs
StandbyLostLostLost~2ms + boot

4. Resource Sharing Both commands and tasks use UART:

// Task prints status
UART_SendString("[LED1] Status\r\n");

// User types 'd' during transmission
// Character buffered in hardware
// Processed after task message completes

No conflict because:

  • Single-threaded execution
  • UART operations are blocking
  • Hardware buffers one character

5. Debug Visibility Commands provide window into scheduler state:

  • Task states (READY/RUNNING/SUSPENDED/STOPPED)
  • Execution counts via static counters
  • Performance metrics (jitter, execution time)
  • System health indicators

This bidirectional interaction makes the system both controllable and observable – essential for embedded development.

Command Interface Design Trade-offs

Our Design Choices:

AspectOur ChoiceAlternativeTrade-off
Input MethodSingle characterCommand linesSimplicity vs features
ProcessingBlockingState machineSimple code vs responsiveness
FeedbackImmediate echoSilent until completeUser experience vs efficiency
Error HandlingMessage + continueReset/ignoreHelpful vs overhead
BufferNo buffer (1 char)Line bufferRAM vs functionality
ProtocolHuman-readableBinaryDebug ease vs efficiency

Performance Summary:

  • Minimal RAM usage: ~350 bytes for messages
  • Fast response: <1ms for simple commands
  • Acceptable blocking: ~30ms for status
  • No dynamic allocation
  • Deterministic timing

Scalability Limits: Current design works well for:

  • Up to ~20 commands (switch statement)
  • Short interactions (<100ms)
  • Human-speed input
  • Single user

Would need redesign for:

  • Complex command syntax
  • Multiple simultaneous users
  • High-speed automation
  • Long-running operations

Production Enhancements:

  1. Command table instead of switch: typedef struct { char cmd; void (*handler)(void); const char* help; } Command_t;
  2. Non-blocking architecture:
    • State machine for multi-char input
    • DMA for long transmissions
    • Command queue for processing
  3. Security:
    • Command authentication
    • Rate limiting
    • Input validation
    • Privilege levels

Our simple design prioritizes understandability and reliability – perfect for learning and development systems.


Next: Part 9 – Task Functions Deep Dive →

This is Part 8 of the STM32 IoT Framework series.

Leave a Comment

Your email address will not be published. Required fields are marked *

2 thoughts on “Part 8: How to Build Amazing UART Command Control”

  1. Pingback: Part 7: Your Simple Guide to the STM32 Main Loop - Learn By Building

  2. Pingback: Part 9: Your Ultimate Guide to Critical System Task Functions - Learn By Building