Module 2/5 · Weeks 4–6 · 27 h

C/C++ on embedded systems

UAT 204 Microcontrollers and Embedded Systems

About 80 minDraft, awaiting reviewLast updated 27 September 2026

Lesson

By the end of this module you will be able to

  1. Use fixed-width data types and explain the effect of overflow
  2. Set, clear, toggle and read register bits with bitwise operators
  3. Explain the use of volatile with registers and variables shared with interrupts
  4. Explain the memory map, stack and heap, and the MISRA C and BARR-C coding standards

Prerequisites: UAT 204 module 1 · UAT 104 (programming)

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.

Four rows of an 8-bit register. Start 00101001; set bit 4 with OR 1 shifted by 4 gives 00111001; clear bit 0 with AND NOT 1 gives 00111000; toggle bit 7 with XOR gives 10111000. The changed bit is shown in yellow
Figure 1 Setting, clearing and toggling bits in an 8-bit register

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

A memory map from high to low addresses: peripherals and registers at the top, then SRAM with the stack growing down, free space, the heap growing up and data and bss, and at the bottom Flash with code and constants. A box on the right warns that the stack meeting the heap is a hard-to-find bug
Figure 2 A simple microcontroller memory map

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

  1. Run Examples 1 and 2, changing the starting values and predicting the results before running.
  2. On the Pico 2, write C code that sets and clears GPIO control bits with the SDK functions, comparing with hand-calculated bits.
  3. Write a program using a uint8_t counter and show the wrap-around over serial.
  4. Try a program using an interrupt flag with and without volatile, compiled with optimisation on, and record the behaviour.
  5. Read three BARR-C rules about data types and variables, and review the group’s code against them.

Common mistakes

Watch out

  • Using int without knowing its size on that chip
  • Forgetting that unsigned integers wrap without warning
  • Overwriting a whole register instead of changing only the bit
  • Forgetting volatile on 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
  • volatile forces 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

  1. What is the largest value a uint16_t can hold?
  2. A uint8_t holds 200. What is it after adding 100?
  3. What is register 0b00001111 after clearing bit 2?
  4. Why must a variable modified by an ISR be declared volatile?
  5. Which C coding standard is a free download?
Answers
  1. 0b00001011
  2. So the compiler reads it from memory every time instead of using a remembered value
  3. BARR-C:2018

Key formulas

Range of an N-bit unsigned integer
Set, clear and toggle bit k

Key references

  1. White, E. (2024). Making embedded systems: Design patterns for great software (2nd ed.). O'Reilly Media.
  2. Arm. Effect of the volatile keyword on compiler optimization. Arm Compiler software development guide. link
  3. MISRA. (2025). MISRA C:2025 — Guidelines for the use of the C language in critical systems. link
  4. 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

Module quiz

This is a formative self-check, not a graded exam

Knowledge domain: Programming and digital technology · Sensors and embedded systems