Embedded HandbookApps
All sections › IX. The C language: from basics to advanced
STM32F103, STM32F407, STM32G431 · level basic · 15 min read
Open in the interactive handbook

65. Branches and loops

Branches choose what to do; loops repeat. That is enough to write down any algorithm — and this is exactly where most of the errors the compiler lets through live: mixed-up precedence, a stray semicolon, a forgotten break, a condition that never becomes false.

The good news: the compiler warns about half of these errors on its own. Along the way, the section shows which ones — and which are caught only by a separate option.

Everything said about warnings was taken from armclang (Keil). GCC without options warns about none of these errors: it catches assignment in a condition, x & 1 == 0 and the dangling else with -Wall, an empty if body and fall-through in switch with -Wextra. CubeIDE puts -Wall into the project by itself (section 4б.3); -Wextra is added by hand: a project starts with -Wall -Wextra (section 77.4).

On a microcontroller, loops also have a role of their own: the whole program is one infinite loop, and in memory it looks very recognisable.

Terms used in the documentation

Term Meaning
relational, equality operators operators that compare two values
logical operators NOT, AND, OR on truth values
short-circuit evaluation the right operand is evaluated only if needed
operator precedence, associativity which operator binds first, and in which direction they group
conditional operator the ternary operator ? :
dangling else an else that belongs to the nearest if
fall-through execution running on into the next case
loop body, iteration the repeated statements, one pass through them
pre-test, post-test loop a loop that checks its condition before, after the body
infinite loop a loop that never ends
busy-wait waiting by spinning in a loop
timeout a limit on how long to wait

65.1 Comparisons give 0 or 1

==, !=, <, >, <=, >= return a number of type int: 1 if the relation is true, and 0 if it is false. There is no separate boolean value here — so the result of a comparison can be added, multiplied or stored in a variable:

int a = (5 == 3);      /* 0 */
int b = (100 >= 1);    /* 1 */
int n_positive = (x > 0) + (y > 0) + (z > 0);

And the other way round: the condition of an if or a loop treats only zero as false, and everything else as true. if (count) means if (count != 0).

== and = differ by one character and mean completely different things. About if (a = 5) — below, in 65.4.


65.2 Logical operators and short-circuit evaluation

!, &&, || also give 0 or 1. Their main property is short-circuit evaluation: the right operand is evaluated only if the result depends on it.

Protection is built on this:

int n = 0, b = 5;
int r = (n != 0 && b / n);   /* 0 — и никакого деления на ноль */

The division never happened at all. The order here is guaranteed: after the left operand of && and || there is a sequence point (section 64.10), so "check first, then use" is a reliable technique.

Bitwise & and | do not short-circuit: both operands are always evaluated. Replacing && with & "for speed" means losing the protection.

⚠️ Integer division by zero on Cortex-M3, M4, M7 and M33 does not stop the program by default — it quietly gives zero. On a PC such a program would crash at once. On the chip the error goes unnoticed until the trap is enabled — bit 4 DIV_0_TRP in the SCB->CCR register (section 7): SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk;. When it fires, it raises a UsageFault, and if that handler is not enabled in SCB->SHCSR, a HardFault (section 35.2). Enabling it in a debug build is a good habit. Cortex-M0 and M0+ (F0, G0, L0, C0) have no hardware divider and no such bit: the line above will not build there, and what a division by zero yields is decided by the library division function.


65.3 Precedence: where parentheses are mandatory

Operators are applied in order of precedence — from highest to lowest:

Precedence Operators Grouping
1 () [] -> ., postfix ++ -- →
2 ! ~, prefix ++ --, unary + - * & sizeof (type) ←
3 * / % →
4 + - →
5 << >> →
6 < <= > >= →
7 == != →
8 & →
9 ^ →
10 \| →
11 && →
12 \|\| →
13 ? : ←
14 = += -= and the other assignments ←
15 , →

That is why *p++ is *(p++): the postfix ++ from row 1 binds tighter than the asterisk from row 2 (section 67.4).

You do not need to remember most of the table — it all works as in arithmetic. But two rows go against intuition, and both hit code for microcontrollers, where bitwise operations are at every step.

Comparison binds tighter than bitwise operators (rows 7 and 8):

if (x & 1 == 0)        /* на деле x & (1 == 0), то есть x & 0 — всегда ноль */
if ((x & 1) == 0)      /* правильно: младший бит равен нулю */

For an even x = 4 the first line gives 0, the second gives 1. The parity check "never fires", and it is hard to spot by eye.

Addition binds tighter than shift (rows 4 and 5): 1 << 2 + 1 is 1 << 3, that is 8, not 5.

Fortunately, armclang warns about x & 1 == 0 without any options: "& has lower precedence than ==; == will be evaluated first"; GCC does so with -Wall: "suggest parentheses around comparison in operand of '&'". A rule from practice: a bitwise operator next to a comparison, or a shift next to arithmetic, always goes in parentheses — even when the precedence happens to match what you intended.


65.4 if and else

if (условие)
    действие;
else if (другое условие)
    другое действие;
else
    действие по умолчанию;

Braces are needed when a branch has more than one statement. In practice they are always written: otherwise a second line added a month later quietly ends up outside the if.

A dangling else belongs to the nearest if, not to the one it was aligned under:

if (a)
    if (b)
        return 1;
else                   /* относится к if (b), хотя выровнен под if (a) */
    return 2;

armclang warns about this on its own: "add explicit braces to avoid dangling else"; GCC does with -Wall.

Assignment instead of comparison is the error discussed in section 64.9:

if (a = 5)             /* a становится 5, условие всегда истинно */

For this one armclang also has a warning without options: "using the result of an assignment as a condition without parentheses"; GCC has it with -Wall. If an assignment in a condition really is intended, it is put in double parentheses — a sign to both the compiler and the reader.

A semicolon after the condition is the third classic:

if (error);            /* пустой оператор — условие ни на что не влияет */
    stop_motor();      /* выполняется всегда */

Here the compiler warns too — "if statement has empty body", and the same for while — but only when the semicolon is on the same line as the condition. An empty statement on a separate line it considers intentional and stays silent. GCC catches an empty if body with -Wextra — on a separate line too, but says nothing about while; -Wall, in turn, notices the misleading indentation of stop_motor() under such an if.


65.5 The ternary operator

condition ? value1 : value2 is an if-else that returns a value and therefore fits inside an expression:

int max = a > b ? a : b;
printf("LED %s\n", led_on ? "ON" : "OFF");

Chains group from the right: a ? b : c ? d : e reads as a ? b : (c ? d : e).

A frequent question — is it faster than if? No. Functions that compute a maximum with if-else and with the ternary operator are turned by the compiler into the same machine code, instruction for instruction. Choose by readability: a short choice of a value — the ternary operator; everything else — if. Nested ternary operators read badly; it is better to avoid them.


65.6 switch

switch picks a branch by an integer value; the case labels must be constants:

switch (a) {
case 1:  printf("one ");   break;
case 2:  printf("two ");            /* break забыт */
case 3:
case 4:  printf("three "); break;
default: printf("default ");
}

break is not a formality. Without it, execution falls through into the next branch. Here is what this fragment prints:

a Output
1 one
2 two three — fall-through
3 three
4 three
5 default

Sometimes fall-through is needed: case 3: with no body sits right above case 4:, and both labels lead to the same branch — this is a common technique. It is bad when the fall-through is accidental, as with case 2.

And here is an unpleasant detail: armclang is silent about accidental fall-through — even with -Wall -Wextra. Only the separate option -Wimplicit-fallthrough catches it. In GCC this option is part of -Wextra, and fall-through is visible with that already. To keep an intended fall-through from causing a warning, it is marked:

case 1:
    prepare();
    __attribute__((fallthrough));   /* провал задуман */
case 2:
    run();
    break;

default can stand anywhere, but it is usually placed last. There break is formally not needed — but it is written anyway: if a new label is later added below, there will be no fall-through.


65.7 Three loops

Loop Condition checked Runs at least once?
while (condition) { … } before the body no
do { … } while (condition); after the body yes
for (init; condition; step) { … } before the body no

Flowcharts of while, do-while and for: where the condition is checked

for is a while gathered into one line:

for (i = 1; i <= 5; i++) {            i = 1;
    действие;                          while (i <= 5) {
}                                          действие;
                                           i++;
                                       }

Any of the three parts of the for header can be empty; for (;;) is a loop without a condition, that is, an infinite one.

A few checks of understanding — what is a after each loop:

int i, a = 1;
for (i = 1; i < 4; i++) a++;           /* 3 шага: a = 4 */

int a2 = 1;
for (i = 1; i < 4; i++) a2 = a2 + i;   /* 1 + 1 + 2 + 3: a2 = 7 */

int a3 = 1, b = 2;
for (i = 3; i >= 1; i--) a3 += b;      /* 3 шага по 2: a3 = 7 */

i++ or ++i in the header? For an integer counter there is no difference: the compiler produces the same code. Nobody uses the value of the expression here; only the increment itself remains.

The exit condition is written as an inequality, not an equality. The loop

n = 2;
while (n != 5) { …; n += 2; }          /* 2, 4, 6, 8, 10, … — бесконечно */

never ends: stepping by 2 from two, you cannot land on five. With n < 5 it would stop after 4.

A floating-point counter is even worse. Adding 0.1f to zero ten times gives not 1 but 1.000000119: 0.1 is an infinite fraction in binary (section 63.15), and the error accumulates. The loop for (x = 0; x != 1.0f; x += 0.1f) will not stop. Count with an integer counter and compute the floating-point value from it.

An unsigned counter counting down — for (uint8_t i = 10; i >= 0; i--) — is also endless: an unsigned value is always greater than or equal to zero. This case is covered in section 3.5.


65.8 break, continue, goto

continue moves to the next iteration, skipping the rest of the body; the for step is still executed:

for (i = 1; i < 5; i++) {
    if (i == 3) continue;
    printf("%d ", i);                  /* 1 2 4 */
}

break leaves the nearest loop or switch — only one level. You cannot leave two nested loops with one break.

goto exists in C, and it has one generally accepted use — leaving deep nesting for common cleanup code:

for (...) {
    for (...) {
        if (fatal) goto cleanup;
    }
}
cleanup:
    disable_peripheral();

Everything else — jumps backwards, into the middle of a loop, "to avoid writing a loop" — makes the program unreadable. The rule is simple: goto only forwards and only to the cleanup.


65.9 The infinite loop — the heart of a microcontroller program

On a PC a program ends by returning control to the system. On a microcontroller there is nowhere to return to: main never finishes.

int main(void)
{
    init_hardware();
    for (;;) {                /* или while (1) — это одно и то же */
        poll_buttons();
        update_display();
    }
}

The compiler turns an empty infinite loop for (;;) { } or while (1) { } into one two-byte instruction — a branch to itself. In machine code this is e7fe, and it is one of the few combinations that are worth recognising on sight.

Because you will meet it in the debugger. The default interrupt handler in the F103 and F407 startup files is built exactly the same way: one label for all unhandled interrupts, followed by a branch to itself. If you enable an interrupt and forget to write a handler — or misspell its name by one letter — the program ends up there at the very first interrupt and "hangs". Halt it with the debugger and you will see exactly e7fe (sections 7 and 35).


65.10 Wait loops

A delay loop without volatile disappears. The function

void delay(void) { for (uint32_t i = 0; i < 100000; i++) { } }

turns into a single instruction with optimisation — a return. The compiler saw that the loop does nothing and threw it away. With volatile on the counter the loop stays: the variable is honestly read from and written to memory at every step.

Delay loop after -O2: without volatile only the return is left, with volatile the loop goes through the stack

But even such a delay is bad: its duration depends on the core clock, the optimisation level and even the alignment of the code in Flash. Time is measured with a timer — the system one (section 7.4) or an ordinary one (section 13).

Waiting for a flag must have a limit. The loop

while (!(USART1->SR & USART_SR_TXE)) { }      /* а если флаг не поднимется? */

hangs forever if the peripheral fails. In production code the wait is limited in time:

uint32_t start = ticks;
while (!(USART1->SR & USART_SR_TXE)) {
    if (ticks - start > TIMEOUT_MS) return ERR_TIMEOUT;
}

And in case something unforeseen hangs, there is the watchdog timer: it restarts the chip if the program stops checking in (section 12). This is the answer to the frequent question "is there protection against an endless loop" — there is, and on a microcontroller it is in hardware.

Why the flag is declared volatile and what happens without it — section 3.4.


65.11 Loop exercises

Digits of a number — the remainder of division by 10 gives the last digit, the division drops it:

int n = 12345, count = 0, sum = 0;
while (n != 0) {
    count++;
    sum += n % 10;
    n /= 10;
}                               /* count = 5, sum = 15 */

Greatest common divisor. By subtraction: subtract the smaller from the larger until the numbers are equal. By remainder: replace the larger with the remainder of dividing it by the smaller until the remainder becomes zero.

while (b != 0) {
    int t = b;
    b = a % b;
    a = t;
}                               /* в a — НОД */

For 14 and 35 both methods give 7 in three steps. The difference shows on awkward numbers: for 1000000 and 3, subtraction needs 333335 steps, the remainder method — two. The same algorithm, a different way of writing the step — and a difference of hundreds of thousands of times. On estimating algorithms — section 76.

Brute force. How many numbers from 102 to 987 are written with three different digits? A loop over all of them, three digits via / and %, three comparisons — 648. Reasoning gives the same answer: the first digit can be chosen in nine ways, the second in nine, the third in eight.


65.12 A side-effects exercise

What do you get after each line if you start with a = 5, b = 7?

(a += 5) * (b -= 3);        /* 10 * 4 = 40;  a = 10, b = 4 */
--b / (a++ - 3);            /* 3 / 7 = 0;    a = 11, b = 3 */
(a -= 2) || 47 / (b - 7);   /* 1;            a = 9,  b = 3 */
(a *= b) + (b *= a);        /* НЕЛЬЗЯ */

In the third line the right operand of || was not evaluated: the left one is already true. The fourth is undefined behaviour: a and b are read and modified in one expression without a sequence point (section 64.10). Different compilers give different results, and it has no correct answer.


65.13 Pitfalls

A bit check never fires. x & 1 == 0 is x & (1 == 0). You need parentheses: (x & 1) == 0.

An if is always true. if (a = 5) — assignment instead of comparison.

An else fired under the wrong condition. A dangling else belongs to the nearest if.

An action is always executed. A semicolon after if (…) or while (…).

Two branches run in a switch. A forgotten break. The option -Wimplicit-fallthrough finds such places.

A loop does not end. A != condition with a step that jumps over the boundary; a floating-point counter; an unsigned counter counting down.

break did not leave the outer loop. It leaves only one level.

A delay vanished after optimisation was turned on. A counter without volatile; better still — a timer.

A delay "drifts" when the clock changes. The loop counts instructions, not time.

The program hung at e7fe. It landed in the default handler: the interrupt is enabled, but there is no handler or its name is misspelled.

The program hangs waiting for a flag. There is no limit on the waiting time.

Division by zero caused no error. On Cortex-M3, M4, M7 and M33 the result is zero by default; the trap is enabled with the DIV_0_TRP bit. M0 and M0+ do not have it: division is done by a library function.


65.14 Summary

Next: section 66 "Functions, scope, recursion".

Open in the interactive handbook