MODULE 7 · LESSON 7.2 · THEORY
While Loop in Embedded C: while, do-while, Polling, Sentinels and the Super Loop
Entry-controlled and exit-controlled loops with flow diagrams, choosing between for, while and do-while, sentinel values, polling status registers with timeouts, the super loop with interrupt flags, classic algorithms and the do-while(0) macro idiom. Keep this in mind whenever you write a while loop in embedded C.
Beginner → Intermediate
Level
What You Will Learn
✓ The while loop: an entry-controlled loop for an unknown number of passes
✓ The do-while loop: an exit-controlled loop that runs at least once
✓ Choosing between for, while and do-while
✓ Sentinel-controlled and event-controlled loops
✓ Polling a hardware flag, volatile, and bounding the wait with a timeout
✓ The super loop while (1) and how interrupts cooperate with it
✓ The do { ... } while (0) macro idiom
✓ Typical bugs: missing update, stray semicolon, assignment in the condition
Key Terms
whiledo-whileEntry-controlledExit-controlledSentinelPollingTimeoutSuper loopBusy-waitvolatileStatus registerEvent flagInfinite loopdo { } while (0)Euclid
1. The while Loop
The for loop of Lesson 7.1 is the right tool when you know how many passes you need. The while loop in embedded C is the right tool when you do not: repeat while a condition holds, however long that takes. Wait while a flag is clear. Read characters while they are not the end of a line. Divide while the number is not zero. Many bugs in a while loop in embedded C start here.
Syntax of the while loop
while (condition)
{
body; /* must change something the condition depends on */
}
Flow of a while loop: test first
condition ?
tested BEFORE each pass
false (zero)
↓
leave the loop
continue after the loop
Because the test comes first, a while loop may run zero times: if the condition is false at the start, the body is skipped entirely. This is the core rule of the while loop in embedded C.
while_basic.c
#include <stdio.h>
int main(void)
{
int n = 5;
printf(“countdown: “);
while (n > 0)
{
printf(“%d “, n);
n–; /* the update that eventually ends the loop */
}
printf(“liftoff, n=%d\n”, n);
int zero_passes = 0;
while (zero_passes > 0) /* false from the start */
{
printf(“never printed\n”);
}
printf(“second loop ran 0 times\n”);
unsigned value = 1234; /* count the decimal digits of a number */
int digits = 0;
while (value != 0)
{
value /= 10;
digits++;
}
printf(“1234 has %d digits\n”, digits);
return 0;
}
Output (verified by compiling and running)
countdown: 5 4 3 2 1 liftoff, n=0
second loop ran 0 times
1234 has 4 digits
A for loop is a while loop in disguise: for (init; cond; update) body behaves like init; while (cond) { body; update; }, except that continue in a for loop still runs the update (Lesson 7.3).
2. The do-while Loop
The do-while loop moves the test to the end. The body always runs at least once, and the condition decides whether to run it again. Note the semicolon after the closing parenthesis: it belongs to the syntax. Reviewers check every while loop in embedded C closely.
Syntax of the do-while loop
do
{
body; /* runs at least once */
}
while (condition); /* semicolon required */
Flow of a do-while loop: test last
↓
condition ?
tested AFTER each pass
true: go round again
↓
back to the body
do_while_basic.c
#include <stdio.h>
int main(void)
{
int x = 10;
while (x < 5) /* zero passes */
x++;
printf(“while : x=%d\n”, x);
x = 10;
do
{
x++; /* runs once even though the test is false */
}
while (x < 5);
printf(“do-while: x=%d\n”, x);
/* validate input: ask at least once, repeat until valid */
int attempts[] = { 99, –4, 7 }; /* simulated user entries */
int i = 0, value;
do
{
value = attempts[i++];
printf(“entered %d -> %s\n”, value, (value >= 1 && value <= 10) ? “accepted” : “rejected, try again”);
}
while (value < 1 || value > 10);
return 0;
}
Output (verified by compiling and running)
while : x=10
do-while: x=11
entered 99 -> rejected, try again
entered -4 -> rejected, try again
entered 7 -> accepted
Use do-while when the body must happen before the condition can even be evaluated: reading a value and then checking it, sending a byte and then checking the status, or sampling a sensor until the reading is valid. Interviewers test the while loop in embedded C on exactly this point.
3. for, while or do-while?
| for | while | do-while |
|---|
| Test position | Before each pass | Before each pass | After each pass |
| Minimum passes | 0 | 0 | 1 |
| Best for | Known count, index loops | Unknown count, event/flag/sentinel | Act first, then decide |
| Update | In the header | In the body | In the body |
| Typical embedded use | Arrays, bits, channels | Polling flags, parsing, main loop | Retry sequences, validated input |
Any loop can be written with any of the three. Pick the one that makes the intent obvious: a reader should see the loop form and know whether the number of passes is fixed.
4. Sentinel-Controlled Loops
A sentinel is a special value that marks the end of the data. The most common example in C is the NUL character that ends every string: you do not need to know the length, you loop until you meet the sentinel. Other examples are a 0xFF byte that ends a table or a negative number that ends a list of positive readings.
sentinel.c
#include <stdio.h>
#include <stdint.h>
static unsigned my_strlen(const char *s)
{
unsigned n = 0;
while (*s++ != ‘\0’) /* stop at the NUL sentinel */
n++;
return n;
}
static unsigned count_until_ff(const uint8_t *p)
{
unsigned n = 0;
while (p[n] != 0xFFu) /* 0xFF marks the end of the table */
n++;
return n;
}
int main(void)
{
printf(“strlen(\”STM32F407\”) = %u\n”, my_strlen(“STM32F407”));
uint8_t table[] = { 0x10, 0x20, 0x30, 0xFF, 0x55 };
printf(“entries before the 0xFF end marker: %u\n”, count_until_ff(table));
int readings[] = { 21, 23, 22, –1, 99 }; /* -1 ends the valid data */
int sum = 0, count = 0, i = 0;
while (readings[i] >= 0)
{
sum += readings[i];
count++;
i++;
}
printf(“average of %d valid readings = %d\n”, count, sum / count);
return 0;
}
Output (verified by compiling and running)
strlen(“STM32F407”) = 9
entries before the 0xFF end marker: 3
average of 3 valid readings = 22
Danger: If the sentinel is missing, for example a string without its NUL, the loop runs through memory until something unrelated happens to match, or until a bus fault. Always make sure the terminator is written (size buffers for the terminator too).
5. Polling Hardware: Busy-Wait Loops
A peripheral works at its own speed. After you start a transmission you must wait until the transmit register is empty, a conversion is done, or the clock is stable. The simplest way is a busy-wait: loop while the status flag says “not ready”.
Typical polling idioms (STM32F407)
while (!(USART2->SR & USART_SR_TXE)) /* wait until the transmit data register is empty */
;
USART2->DR = byte;
while (!(RCC->CR & RCC_CR_HSERDY)) /* wait for the external oscillator */
;
ADC1->CR2 |= ADC_CR2_SWSTART; /* start a conversion */
while (!(ADC1->SR & ADC_SR_EOC)) /* wait for end of conversion */
;
The register must be accessed as volatile (the CMSIS headers do that for you) so that the compiler re-reads it on every pass. Without volatile the compiler reads it once, sees the loop does not change it, and turns the test into an infinite loop. The program below demonstrates the idea with a simulated peripheral that becomes ready after a few reads:
polling.c
#include <stdio.h>
#include <stdint.h>
#define SR_TXE (1u << 7)
static uint32_t reads; /* how many times the “hardware” was read */
static uint32_t read_sr(void) /* the flag becomes 1 on the 4th read */
{
reads++;
return (reads >= 4) ? SR_TXE : 0u;
}
int main(void)
{
unsigned spins = 0;
while (!(read_sr() & SR_TXE)) /* busy-wait */
spins++;
printf(“flag set after %u extra polls (%u reads)\n”, spins, (unsigned)reads);
/* the same wait with a timeout, for a flag that never comes */
unsigned timeout = 1000, polls = 0;
uint32_t never_ready = 0;
while (!(never_ready & SR_TXE) && timeout > 0)
{
timeout–;
polls++;
}
if (timeout == 0)
printf(“timeout after %u polls: report an error\n”, polls);
return 0;
}
Output (verified by compiling and running)
flag set after 3 extra polls (4 reads)
timeout after 1000 polls: report an error
Always bound the wait
If the hardware fails, a clock never starts, the cable is unplugged, a device does not answer, an unbounded while hangs the whole system. A timeout turns that into a handled error. Use a decrementing counter, or a SysTick-based deadline when the elapsed time matters:
Timeout using the millisecond tick (pattern)
uint32_t start = HAL_GetTick();
while (!(USART2->SR & USART_SR_TXE))
{
if (HAL_GetTick() – start > 10) /* 10 ms limit; unsigned subtraction survives wrap-around */
return ERR_TIMEOUT;
}
Power: A busy-wait keeps the CPU at full speed. In battery products wait with __WFI() (wait for interrupt) in the loop body, or let the peripheral raise an interrupt or use DMA.
6. The Super Loop: while (1)
Bare-metal firmware has no operating system to run tasks. The classic structure is a one-time initialisation followed by an infinite loop, often called the super loop. Interrupt service routines handle urgent events and set flags; the loop notices the flags and does the longer work.
Super loop with interrupts
main()
init clocks, GPIO, UART
↓
↓
if (rx_flag) handle_rx()
↓
if (tick_flag) update()
ISR: USART2_IRQHandler
sets rx_flag = 1
↓
ISR: SysTick_Handler
sets tick_flag = 1
super_loop.c
#include <stdio.h>
#include <stdbool.h>
static volatile bool rx_flag = false; /* set by the USART interrupt (simulated) */
static volatile bool tick_flag = false; /* set by SysTick every 1 ms (simulated) */
static void fake_interrupts(int step) /* stands in for the hardware */
{
if (step == 2) rx_flag = true;
if (step % 3 == 0) tick_flag = true;
}
int main(void)
{
int blink = 0;
int step = 0;
while (step < 8) /* a real super loop uses while (1) */
{
fake_interrupts(step);
if (rx_flag)
{
rx_flag = false; /* clear before handling */
printf(“step %d: handle received byte\n”, step);
}
if (tick_flag)
{
tick_flag = false;
blink ^= 1;
printf(“step %d: tick, LED %s\n”, step, blink ? “on” : “off”);
}
step++;
}
return 0;
}
Output (verified by compiling and running)
step 0: tick, LED on
step 2: handle received byte
step 3: tick, LED off
step 6: tick, LED on
The flags are volatile because an ISR changes them behind the compiler’s back. The loop does short work and returns to the top quickly, so no event waits long. A long calculation inside the loop would delay every other task, which is where a real-time operating system or a state machine (Lesson 6.2) helps.
7. Classic while Loop Algorithms
while_algos.c
#include <stdio.h>
#include <stdint.h>
static unsigned gcd(unsigned a, unsigned b) /* Euclid’s algorithm */
{
while (b != 0)
{
unsigned t = a % b;
a = b;
b = t;
}
return a;
}
static unsigned reverse_digits(unsigned n)
{
unsigned r = 0;
while (n != 0)
{
r = r * 10 + n % 10;
n /= 10;
}
return r;
}
static int lowest_set_bit(uint32_t x) /* -1 if x == 0 */
{
if (x == 0) return –1;
int pos = 0;
while ((x & 1u) == 0)
{
x >>= 1;
pos++;
}
return pos;
}
static unsigned to_binary(unsigned n, char *out) /* writes the binary string, returns its length */
{
char tmp[33];
unsigned k = 0;
do
{
tmp[k++] = (char)(‘0’ + (n & 1u));
n >>= 1;
}
while (n != 0); /* do-while: n == 0 must still print “0” */
for (unsigned i = 0; i < k; i++) out[i] = tmp[k – 1 – i];
out[k] = ‘\0’;
return k;
}
int main(void)
{
char s[33];
printf(“gcd(48, 36) = %u, gcd(17, 5) = %u\n”, gcd(48, 36), gcd(17, 5));
printf(“reverse(12345) = %u\n”, reverse_digits(12345));
printf(“lowest set bit of 0x50 = %d, of 0 = %d\n”, lowest_set_bit(0x50), lowest_set_bit(0));
to_binary(0, s); printf(“0 in binary: %s\n”, s);
to_binary(10, s); printf(“10 in binary: %s\n”, s);
to_binary(255, s); printf(“255 in binary: %s\n”, s);
return 0;
}
Output (verified by compiling and running)
gcd(48, 36) = 12, gcd(17, 5) = 1
reverse(12345) = 54321
lowest set bit of 0x50 = 4, of 0 = -1
0 in binary: 0
10 in binary: 1010
255 in binary: 11111111
to_binary shows a good reason to prefer do-while: the value 0 must still produce the single digit “0”. A plain while loop would produce an empty string.
8. The do { } while (0) Macro Idiom
You will meet do { ... } while (0) in the ST HAL and in the Linux kernel. It is not a loop (it runs exactly once), but a trick to make a multi-statement macro behave like a single statement.
macro_idiom.c
#include <stdio.h>
#define SWAP_BAD(a, b) int t = a; a = b; b = t;
#define SWAP_OK(a, b) do { int t = (a); (a) = (b); (b) = t; } while (0)
int main(void)
{
int x = 1, y = 2;
if (x < y)
SWAP_OK(x, y); /* expands to ONE statement, ending with the semicolon */
else
printf(“not swapped\n”);
printf(“x=%d y=%d\n”, x, y);
return 0;
}
Output (verified by compiling and running)
x=2 y=1
macro_bad.c
#include <stdio.h>
#define SWAP_BAD(a, b) int t = a; a = b; b = t;
int main(void)
{
int x = 1, y = 2;
if (x < y)
SWAP_BAD(x, y);
else
printf(“not swapped\n”);
return 0;
}
The unsafe version breaks in if/else
macro_bad.c: In function ‘main’:
macro_bad.c:2:26: error: expected expression before ‘int’
2 | #define SWAP_BAD(a, b) int t = a; a = b; b = t;
| ^~~
macro_bad.c:7:9: note: in expansion of macro ‘SWAP_BAD’
7 | SWAP_BAD(x, y);
| ^~~~~~~~
macro_bad.c:2:26: warning: macro expands to multiple statements [-Wmultistatement-macros]
2 | #define SWAP_BAD(a, b) int t = a; a = b; b = t;
| ^~~
macro_bad.c:7:9: note: in expansion of macro ‘SWAP_BAD’
7 | SWAP_BAD(x, y);
| ^~~~~~~~
macro_bad.c:6:5: note: some parts of macro expansion are not guarded by this ‘if’ clause
6 | if (x < y)
| ^~
macro_bad.c:2:48: error: ‘t’ undeclared (first use in this function)
2 | #define SWAP_BAD(a, b) int t = a; a = b; b = t;
| ^
macro_bad.c:7:9: note: in expansion of macro ‘SWAP_BAD’
7 | SWAP_BAD(x, y);
| ^~~~~~~~
macro_bad.c:2:48: note: each undeclared identifier is reported only once for each function it appears in
2 | #define SWAP_BAD(a, b) int t = a; a = b; b = t;
| ^
macro_bad.c:7:9: note: in expansion of macro ‘SWAP_BAD’
7 | SWAP_BAD(x, y);
| ^~~~~~~~
macro_bad.c:8:5: error: ‘else’ without a previous ‘if’
8 | else
| ^~~~
Without the wrapper the macro expands into three statements; only the first belongs to the if, so the else has no matching if and the code does not compile. The wrapper plus the caller’s semicolon forms exactly one statement.
9. Common Mistakes
Mistake 1 — no update in the body: while (i < 10) { sum += i; } never ends. Make sure something in the body changes the condition.
Mistake 2 — semicolon after while: while (x > 0); is an empty loop. The block after it runs once, or the program hangs.
Mistake 3 — missing semicolon after do-while: } while (cond) without the semicolon does not compile.
Mistake 4 — assignment instead of comparison: while (x = 1) is an endless loop; while ((c = next()) != 0) is intentional and parenthesised.
Mistake 5 — flag not volatile: The compiler hoists the read out of the loop. The loop never sees the ISR change.
Mistake 6 — unbounded polling: A dead peripheral hangs the system.
Mistake 7 — clearing a status flag after handling instead of before: An event arriving while you handle it is lost.
Mistake 8 — off-by-one with unsigned countdown: while (n-- >= 0) with unsigned n never ends.
10. Best Practices for the while loop in Embedded C
- Use while for event and sentinel loops, for for counted loops.
- Always make the loop condition depend on something the body or the hardware changes.
- Bound every hardware wait with a timeout and handle the failure.
- Declare shared flags and hardware registers
volatile. - Keep the body of the super loop short and non-blocking.
- Prefer interrupts, DMA or
__WFI() to spinning when power matters. - Wrap multi-statement macros in
do { } while (0). - Put the assignment in the condition only with extra parentheses and a comment.
Key Takeaways — Lesson 7.2
- The while loop in embedded C tests first and may run zero times; do-while tests last and runs at least once.
- Use while when the number of passes is unknown: flags, sentinels, events.
- A busy-wait on a status register needs volatile access and a timeout.
- The super loop
while (1) plus interrupt-set flags is the basic bare-metal architecture. - Do-while is right when the first pass must happen before the test, for example converting the number 0.
do { } while (0) makes a multi-statement macro safe in if/else.
Frequently Asked Questions
What is the difference between while and do-while?
A while loop tests its condition before each pass and may run zero times. A do-while loop tests after each pass, so its body always runs at least once. Do-while needs a semicolon after the condition.
How do I write an infinite loop with while?
Use while (1) or for (;;). Embedded main functions use it as the super loop so the program never returns from main.
Why does my polling loop hang?
Either the flag never gets set (wrong register, clock not enabled, device missing) or the flag variable is not volatile and the compiler reads it only once. Add a timeout and check the clock enable bits.
Why is do { } while (0) used in macros?
It turns several statements into a single statement that needs the caller’s semicolon, so the macro works correctly inside an if/else without braces.
Is while (1) the same as for (;;)?
Yes. Both are accepted as infinite loops by the standard and by MISRA. Some static analysers prefer for (;;) because while (1) has a constant controlling expression.
Should I poll or use interrupts?
Polling is simple and good for short waits (microseconds). For long or unpredictable waits use interrupts or DMA so the CPU can sleep or do other work.
Ready to practise?
Next: worked examples with UART polling, parsing, retry loops, number conversion and the main loop on real output.
2 Comments