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?
- UART hardware receives 10 bits (1 start + 8 data + 1 stop)
- Places character in USART3->DR register
- Sets RXNE flag in USART3->SR
- Character waits until software reads it
- 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:
- 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 } - 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:
- Subtract smallest case value (‘1’ = 49)
- Check if result is within range
- Use result as index into address table
- 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:
- Send announcement message (52 characters = ~4.5ms at 115200 baud)
- Delay 10ms to ensure transmission completes
- Enter sleep mode
- CPU halts at WFI instruction
- Wake on any interrupt (UART RXNE)
- 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:
- WFI instruction executed
- CPU stops fetching instructions
- HSI oscillator disabled
- All clocks stop
- Voltage regulator enters low-power mode
- Flash memory powered down
- Only EXTI lines and RTC remain active
Wake-up sequence (takes ~5μs):
- EXTI interrupt from UART pin
- Voltage regulator returns to normal (~2μs)
- HSI oscillator restarts (~3μs)
- Flash memory powers up
- CPU resumes from WFI instruction
- 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:
- WKUP pin (PA0) – Rising edge
- RTC alarm – If configured
- External reset – NRST pin
- IWDG reset – If watchdog enabled
The wake-up sequence:
- Wake event detected
- System performs power-on reset
- Bootloader runs (~2ms)
- main() starts from beginning
- All initialization repeats
- 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:
- First character: task ID (0-5)
- 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:
- Character range check: ‘0’ <= char <= ‘5’
- Task existence check: taskId < registeredTaskCount
- Action validity check: action is ‘s’, ‘p’, or ‘r’
- 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:
- Testing system with multiple tasks
- Suspend tasks 1, 3, 4 for isolation
- Test task 0 in isolation
- Press ‘r’ to restore normal operation
- 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:
- Concise descriptions – One line per command
- Logical grouping:
- Power modes (1,2,3)
- Information (p,d,s)
- Control (t,r)
- Help (h,?)
- Visual structure – Separators and alignment
- 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:
| System | Line Ending | Hex Values | History |
|---|---|---|---|
| Unix/Linux | LF | 0x0A | Line Feed only |
| Windows | CR+LF | 0x0D 0x0A | Carriage Return + Line Feed |
| Old Mac | CR | 0x0D | Carriage Return only |
| Embedded | Varies | Any | Depends on terminal |
What happens without this handling: User presses Enter → Terminal sends CR+LF:
- CR (0x0D) processed → “Unknown command”
- LF (0x0A) processed → “Unknown command”
- 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:
- Brief – Not annoying on typos
- Helpful – Suggests solution
- Clear – States what happened
- 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:
| Operation | Time | Calculation |
|---|---|---|
| Character receive | 87μs | 10 bits @ 115200 baud |
| UART_IsDataAvailable() | <1μs | Single register read |
| UART_ReceiveChar() | <1μs | Register read + clear flag |
| UART_SendChar() | 87μs | 10 bits @ 115200 baud |
| Simple command (‘r’) | ~100μs | Function calls + loop |
| Status display (‘d’) | ~20ms | Building + sending ~500 chars |
| Power mode entry | 10ms | Wait 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:
| Method | RAM Usage | Code Size | Features |
|---|---|---|---|
| Single char | 1 byte | ~200 bytes | Instant response |
| Line buffer | 80+ bytes | ~500 bytes | Editing, parameters |
| Ring buffer | 256+ bytes | ~800 bytes | Multiple commands |
State machine for line buffering:
IDLE --char--> BUILDING --'\n'--> COMPLETE --process--> IDLE
^ | |
| +--'\b'-> BACKSPACE |
+--buffer full--> ERROR ----------------+
Why we chose single character:
- Simplicity – No buffer management
- Reliability – Can’t overflow
- Memory – Minimal RAM usage
- Response – Instant feedback
- 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 Type | Cause | Result | Recovery |
|---|---|---|---|
| HardFault | Null pointer | System halts | Reset required |
| MemManage | Stack overflow | System halts | Reset required |
| BusFault | Invalid address | System halts | Reset required |
| UsageFault | Undefined instruction | System halts | Reset 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
| Mode | Tasks | SysTick | Time Tracking | Wake Time |
|---|---|---|---|---|
| Run | Execute normally | Running | Accurate | N/A |
| Sleep | Paused | Running | Accurate | <1μs |
| Stop | Paused | Stopped | Lost | ~5μs |
| Standby | Lost | Lost | Lost | ~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:
| Aspect | Our Choice | Alternative | Trade-off |
|---|---|---|---|
| Input Method | Single character | Command lines | Simplicity vs features |
| Processing | Blocking | State machine | Simple code vs responsiveness |
| Feedback | Immediate echo | Silent until complete | User experience vs efficiency |
| Error Handling | Message + continue | Reset/ignore | Helpful vs overhead |
| Buffer | No buffer (1 char) | Line buffer | RAM vs functionality |
| Protocol | Human-readable | Binary | Debug 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:
- Command table instead of switch:
typedef struct { char cmd; void (*handler)(void); const char* help; } Command_t; - Non-blocking architecture:
- State machine for multi-char input
- DMA for long transmissions
- Command queue for processing
- 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.
Pingback: Part 7: Your Simple Guide to the STM32 Main Loop - Learn By Building
Pingback: Part 9: Your Ultimate Guide to Critical System Task Functions - Learn By Building