C/C++ on embedded systems
UAT 204 Microcontrollers and Embedded Systems
Lesson
By the end of this module you will be able to
- Use fixed-width data types and explain the effect of overflow
- Set, clear, toggle and read register bits with bitwise operators
- Explain the use of volatile with registers and variables shared with interrupts
- Explain the memory map, stack and heap, and the MISRA C and BARR-C coding standards
Why this matters
C and C++ remain the main languages of embedded systems because they control memory and hardware directly and run fast; both ArduPilot and PX4 are written in C++. But that power brings risk: variables that overflow silently, or variables the compiler optimises away because it does not know the hardware can change them, cause the hardest bugs to find. This module uses Python to simulate those behaviours before writing on a real board.
Fixed-width data types
Embedded code should use types of known size from <stdint.h>, such as uint8_t (0 to 255), int16_t and uint32_t, rather than int, whose size depends on the chip. When a value exceeds the range, unsigned integers wrap around without warning.
Example 1 An 8-bit counter overflowing
Simulate a uint8_t counter counting pulses from an rpm sensor.
def as_uint8(x):
return x & 0xFF # keep only the low 8 bits, like uint8_t
count = 250
for pulse in range(8):
count = as_uint8(count + 1)
print(count, end=" ")
print()
print("elapsed pulses between readings 250 and 2:", as_uint8(2 - 250))
251 252 253 254 255 0 1 2
elapsed pulses between readings 250 and 2: 8
The counter jumps from 255 back to 0. If the difference is calculated in the same type (uint8_t), it is still correct even across the wrap. This technique is used with MCU timers that wrap periodically.
Manipulating register bits
Peripherals are controlled through registers, in which each bit has a meaning: one bit turns on an LED, another selects a mode. Changing one bit must not disturb the others.
Example 2 Setting, clearing, toggling and reading bits
reg = 0b00101001
print(f"start {reg:08b}")
reg |= 1 << 4 # set bit 4
print(f"set bit 4 {reg:08b}")
reg &= ~(1 << 0) & 0xFF # clear bit 0
print(f"clear bit 0 {reg:08b}")
reg ^= 1 << 7 # toggle bit 7
print(f"toggle bit 7 {reg:08b}")
print("bit 3 is", (reg >> 3) & 1, "| bits 5..4 as a field:", (reg >> 4) & 0b11)
start 00101001
set bit 4 00111001
clear bit 0 00111000
toggle bit 7 10111000
bit 3 is 1 | bits 5..4 as a field: 3
In C the pattern is the same, for example GPIO_OUT |= (1u << 4);. The u makes the constant unsigned, preventing surprises when shifting into the top bit.
volatile
Compilers speed up code by assuming a variable only changes when the program writes it. But hardware registers and variables modified by interrupts can change without the main program knowing. The volatile qualifier tells the compiler to read the value from memory every time. Per Arm’s guide, use it for peripheral registers, variables shared with interrupt routines and variables shared between threads.
volatile uint8_t data_ready = 0; /* ISR sets it, main loop reads it */
void sensor_isr(void) { data_ready = 1; }
int main(void) {
for (;;) {
if (data_ready) { /* without volatile the compiler may read this only once */
data_ready = 0;
read_sensor();
}
}
}
Memory and coding standards
An MCU has little SRAM. The stack holds function variables and grows downwards with nested calls; the heap holds memory requested at run time. If they grow into each other, the program fails unpredictably, so safety-critical systems often forbid dynamic allocation after start-up.
Coding standards help prevent such bugs. MISRA C (the current edition is MISRA C:2025) is used where safety matters, such as automotive and aviation, and Barr Group’s BARR-C:2018 is a free standard that makes a good starting point.
Module lab
Lab: bits, counters and volatile on the board
- Run Examples 1 and 2, changing the starting values and predicting the results before running.
- On the Pico 2, write C code that sets and clears GPIO control bits with the SDK functions, comparing with hand-calculated bits.
- Write a program using a
uint8_tcounter and show the wrap-around over serial. - Try a program using an interrupt flag with and without
volatile, compiled with optimisation on, and record the behaviour. - Read three BARR-C rules about data types and variables, and review the group’s code against them.
Common mistakes
Watch out
- Using
intwithout knowing its size on that chip - Forgetting that unsigned integers wrap without warning
- Overwriting a whole register instead of changing only the bit
- Forgetting
volatileon variables shared with interrupts - Repeated dynamic allocation on an MCU with little SRAM
Summary
- Use fixed-width types, and watch for unsigned wrap-around
- Set bits with OR, clear with AND of the complement, toggle with XOR
volatileforces a real read every time, for registers and interrupt-shared variables- Stack and heap share SRAM, and MISRA C and BARR-C help reduce bugs
Check your understanding
- What is the largest value a
uint16_tcan hold? - A
uint8_tholds 200. What is it after adding 100? - What is register 0b00001111 after clearing bit 2?
- Why must a variable modified by an ISR be declared
volatile? - Which C coding standard is a free download?
Answers
- 0b00001011
- So the compiler reads it from memory every time instead of using a remembered value
- BARR-C:2018
Key formulas
| Range of an N-bit unsigned integer | |
| Set, clear and toggle bit k |
Key references
- White, E. (2024). Making embedded systems: Design patterns for great software (2nd ed.). O'Reilly Media.
- Arm. Effect of the volatile keyword on compiler optimization. Arm Compiler software development guide. link
- MISRA. (2025). MISRA C:2025 — Guidelines for the use of the C language in critical systems. link
- Barr Group. (2018). Embedded C coding standard (BARR-C:2018). link
Further reading
Study the assigned knowledge units in advance, review media and take the module quiz
In class / field
Lab or field practice from worksheets with a safety checklist
Learning evidence: Checked worksheets and quiz results