Loading header...

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, and continue predictably;
  • 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

  • while for indefinite repetition until a condition changes;
  • for for counted loops or clear iteration structure;
  • do ... while when 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: b is evaluated only if a is true.
  • a || b: b is evaluated only if a is 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.

stateDiagram-v2 [*] --> IDLE IDLE --> RUNNING: start command RUNNING --> FAULT: error detected RUNNING --> IDLE: stop command FAULT --> IDLE: fault cleared

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 if chains 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

Mind Map

mindmap root((Control Flow)) Core idea Zero is false Nonzero is true Branches affect hardware Clarity prevents bugs Structures if else decisions switch for states while polling for counted loops break and continue Hardware checks Mask status bits Compare to zero Add timeouts Guard null pointers Keep side effects visible Design rules Parenthesize masks Avoid nested chains Use state machines Default to fault state Do not spin forever Practical checks Test each state Force timeout path Review operator order Enable warnings Trace register reads Mistakes Assignment in condition Whole register test Hidden increment Missing switch break No timeout escape