Control Flow and Expressions
Embedded firmware spends most of its life making decisions: poll a status flag, branch on a state, retry a transaction, exit on timeout, or saturate a value into a safe range. C gives you familiar constructs for that, but the hardware consequences still matter.
Learning Objectives
By the end of this lesson, you should be able to:
- use
if,switch,while,for,break, andcontinuepredictably; - understand how boolean-like expressions are represented in C;
- write conditions that read hardware flags correctly;
- avoid hidden side effects in expressions.
C Does Not Have a Native Boolean-Only Machine Model
In C, a condition is true if it evaluates to nonzero and false if it evaluates to zero.
if (status) {
/* true if status != 0 */
}
This is simple, but it means expression clarity matters.
Basic Control Structures
if and else
Use if when the decision is binary or priority-ordered.
if (adc_code > threshold) {
alarm = 1u;
} else {
alarm = 0u;
}
switch
Use switch when one variable selects between distinct states or commands.
switch (command) {
case CMD_START:
start_motor();
break;
case CMD_STOP:
stop_motor();
break;
default:
report_error();
break;
}
loops
whilefor indefinite repetition until a condition changes;forfor counted loops or clear iteration structure;do ... whilewhen the body must run at least once.
Reading Hardware Flags Correctly
Registers often contain multiple bits. Avoid writing vague tests when a bit-specific test is intended.
Good:
if ((UCSR0A & (1u << RXC0)) != 0u) {
byte = UDR0;
}
Risky:
if (UCSR0A) {
byte = UDR0;
}
The second version does not test the intended flag. It tests whether any bit in the register is nonzero.
Side Effects in Expressions
C allows assignments and increments inside expressions. That does not mean you should use them casually in embedded code.
Example:
if ((status = read_status()) & READY_MASK) {
...
}
This is legal, but it mixes assignment, masking, and testing in one line. Clearer code is easier to review and harder to misuse.
Prefer:
status = read_status();
if ((status & READY_MASK) != 0u) {
...
}
Short-Circuit Logic
Logical operators short-circuit:
a && b:bis evaluated only ifais true.a || b:bis evaluated only ifais false.
This matters when the right-hand side has side effects or expensive work.
Example:
if ((buffer != NULL) && (buffer->count > 0u)) {
...
}
This is safe because buffer->count is not evaluated if buffer is NULL.
State Machines in C
Embedded systems often use explicit states rather than deep nested conditionals.
In C this often becomes:
switch (state) {
case STATE_IDLE:
if (start_requested) {
state = STATE_RUNNING;
}
break;
case STATE_RUNNING:
if (fault_detected) {
state = STATE_FAULT;
} else if (stop_requested) {
state = STATE_IDLE;
}
break;
case STATE_FAULT:
if (fault_cleared) {
state = STATE_IDLE;
}
break;
default:
state = STATE_FAULT;
break;
}
Expressions and Operator Precedence
Never rely on memory alone for subtle precedence in register code.
This is wrong:
if (status & READY_MASK == 0u) {
...
}
because == binds more tightly than the author likely intended.
Write:
if ((status & READY_MASK) == 0u) {
...
}
Parentheses are cheap. Ambiguity in hardware code is expensive.
Busy-Wait Versus Timeout Loops
A dangerous pattern:
while ((SPI->STATUS & DONE_MASK) == 0u) {
}
If the hardware never sets the bit, firmware hangs forever.
Safer:
uint32_t timeout = 100000u;
while (((SPI->STATUS & DONE_MASK) == 0u) && (timeout > 0u)) {
timeout--;
}
Even better: use a timer or event-driven scheme when the architecture allows it.
Common Mistakes
- Using
=where==was intended. - Testing an entire register when a single bit matters.
- Hiding side effects inside conditions.
- Writing infinite polling loops with no timeout strategy.
- Building unreadable nested
ifchains instead of a state machine.
Summary
Control flow in embedded C is simple in syntax but powerful in consequences. Conditions test nonzero versus zero, hardware flags must be masked explicitly, and readability is part of correctness. Good control-flow code makes firmware behavior predictable, reviewable, and easier to debug on real hardware.
Further Reading
- cppreference: Statements and Control Flow
- Barr Group Embedded C Coding Standard
- SEI CERT C Coding Standard