Back to Embedded Systems Hardware Engineering Series

Part 10: Embedded Firmware Integration

April 17, 2026 Wasil Zafar 50 min read

Bridge the hardware-firmware gap — configure STM32 peripherals with HAL, build FreeRTOS tasks, and implement UART, I2C, SPI, and ADC drivers.

Table of Contents

  1. Firmware Architecture
  2. GPIO & Clock Config
  3. UART Communication
  4. I2C & SPI Drivers
  5. ADC & DMA
  6. FreeRTOS Integration
  7. Exercises
  8. Firmware Config Tool
  9. Conclusion & Next Steps

Firmware Architecture

Analogy Think of embedded firmware as a conductor leading an orchestra. Each peripheral — UART, I2C, SPI, ADC — is a musician who knows how to play their instrument, but needs precise timing cues to play in harmony. The conductor (firmware) doesn’t build the instruments (hardware), but decides when each musician plays, how fast they play (clock configuration), and what notes to play (data). Without the conductor, you have talented musicians sitting idle on a circuit board. Without the musicians, you have a conductor waving at empty chairs.

A Brief History of Embedded Firmware

1971 Intel 4004 — the first commercial microprocessor. Firmware was literally “firm” — stored in mask ROM, burned at the factory, impossible to update after manufacturing.
1978 Intel 8051 popularised flash-programmable microcontrollers. Engineers could now update firmware without replacing chips, revolutionising the development cycle.
2007 STMicroelectronics launched the STM32 family with ARM Cortex-M cores. The free Standard Peripheral Library (later HAL) dramatically lowered the barrier for 32-bit embedded development.
2014 ARM released CMSIS-RTOS v2, providing a standardised API for real-time operating systems. FreeRTOS became the de facto RTOS with over 220,000 downloads per year.

Embedded firmware is the software that directly controls hardware peripherals. For STM32, you have three abstraction levels: HAL (Hardware Abstraction Layer) for portability, LL (Low-Level) for performance, and direct register access for maximum control.

ApproachPortabilityPerformanceCode SizeBest For
HAL (ST)HighModerateLargeRapid prototyping, complex peripherals
LL (ST)MediumHighSmallPerformance-critical drivers
CMSIS + RegistersLowMaximumMinimalTight timing, minimal footprint

Firmware Project Structure

# Standard STM32CubeIDE project layout
my_firmware/
├── Core/
│   ├── Inc/                    # Application headers
│   │   ├── main.h
│   │   ├── stm32f4xx_hal_conf.h
│   │   └── FreeRTOSConfig.h
│   └── Src/                    # Application sources
│       ├── main.c
│       ├── stm32f4xx_it.c      # Interrupt handlers
│       ├── stm32f4xx_hal_msp.c # HAL MSP (MCU support package)
│       └── system_stm32f4xx.c  # System init (clock, vectors)
├── Drivers/
│   ├── CMSIS/                  # ARM CMSIS headers
│   └── STM32F4xx_HAL_Driver/   # ST HAL library
├── Middlewares/
│   └── Third_Party/FreeRTOS/   # FreeRTOS kernel
├── App/                        # Custom application modules
│   ├── sensor_driver.c
│   ├── sensor_driver.h
│   ├── comms.c
│   └── comms.h
├── STM32F411CEUX_FLASH.ld      # Linker script
└── Makefile                    # Build system (or .cproject)

GPIO & Clock Configuration

Clock Tree Setup

/* Clock configuration for STM32F411 — 100 MHz from 8 MHz HSE
 * PLL: HSE/4 * 200 /2 = 100 MHz SYSCLK
 * AHB = 100 MHz, APB1 = 50 MHz, APB2 = 100 MHz
 */

#include "stm32f4xx_hal.h"

void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    /* Enable HSE oscillator */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState       = RCC_HSE_ON;
    RCC_OscInitStruct.PLL.PLLState   = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource  = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM       = 4;    /* HSE / 4 = 2 MHz */
    RCC_OscInitStruct.PLL.PLLN       = 200;  /* 2 * 200 = 400 MHz VCO */
    RCC_OscInitStruct.PLL.PLLP       = RCC_PLLP_DIV4; /* 400 / 4 = 100 MHz */
    RCC_OscInitStruct.PLL.PLLQ       = 8;    /* USB: 400/8 = 50 MHz (close to 48) */
    HAL_RCC_OscConfig(&RCC_OscInitStruct);

    /* Configure bus clocks */
    RCC_ClkInitStruct.ClockType      = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                     | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource   = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider  = RCC_SYSCLK_DIV1;   /* 100 MHz */
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;     /* 50 MHz */
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;     /* 100 MHz */
    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_3);
}
PLL Calculation Verification
STM32F411 Clock Tree
========================================
HSE Crystal:      8.000 MHz
PLL Input (M=4):  2.000 MHz  ✓ (must be 1-2 MHz)
VCO (N=200):    400.000 MHz  ✓ (must be 100-432 MHz)
SYSCLK (P=4):   100.000 MHz  ✓ (max 100 MHz for F411)
USB CLK (Q=8):   50.000 MHz  ✗ (need 48 MHz ±0.25%)

Bus Clocks:
  AHB  (÷1):    100.000 MHz  ✓
  APB1 (÷2):     50.000 MHz  ✓ (max 50 MHz)
  APB2 (÷1):    100.000 MHz  ✓ (max 100 MHz)

Flash Latency:   3 wait states ✓ (for 100 MHz @ 3.3V)

⚠ USB clock is 50 MHz — needs 48 MHz.
  Fix: Use Q=9 → 44.4 MHz (too low), or adjust N.
  Better: Use PLLM=8, PLLN=384, PLLP=4, PLLQ=8
          → SYSCLK=96 MHz, USB=48 MHz ✓

GPIO Initialization

/* GPIO initialization — LED on PA5, button on PC13 */

#include "stm32f4xx_hal.h"

void GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    /* Enable GPIO clocks */
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();

    /* PA5 — LED output (push-pull, no pull, low speed) */
    GPIO_InitStruct.Pin   = GPIO_PIN_5;
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull  = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* PC13 — Button input (interrupt, pull-up) */
    GPIO_InitStruct.Pin   = GPIO_PIN_13;
    GPIO_InitStruct.Mode  = GPIO_MODE_IT_FALLING;
    GPIO_InitStruct.Pull  = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    /* Enable EXTI interrupt for PC13 */
    HAL_NVIC_SetPriority(EXTI15_10_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}

UART Communication

/* UART2 initialization and transmit/receive
 * PA2 = TX, PA3 = RX, 115200 baud, 8N1
 */

#include "stm32f4xx_hal.h"
#include <string.h>
#include <stdio.h>

UART_HandleTypeDef huart2;

void UART2_Init(void)
{
    huart2.Instance          = USART2;
    huart2.Init.BaudRate     = 115200;
    huart2.Init.WordLength   = UART_WORDLENGTH_8B;
    huart2.Init.StopBits     = UART_STOPBITS_1;
    huart2.Init.Parity       = UART_PARITY_NONE;
    huart2.Init.Mode         = UART_MODE_TX_RX;
    huart2.Init.HwFlowCtl    = UART_HWCONTROL_NONE;
    huart2.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart2);
}

/* printf redirect via UART */
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart2, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    return len;
}

/* Usage: printf("Hello from STM32! Tick=%lu\r\n", HAL_GetTick()); */
Serial Terminal (115200 baud)
Hello from STM32! Tick=0
Hello from STM32! Tick=1000
Hello from STM32! Tick=2000

--- UART Configuration ---
Instance:      USART2
Baud Rate:     115200
Word Length:   8 bits
Stop Bits:     1
Parity:        None
Pins:          PA2 (TX) → PC serial RX
               PA3 (RX) ← PC serial TX
APB1 Clock:    50 MHz
Actual Baud:   115107 (error: -0.08%)
               ✓ Within ±2% tolerance

I2C & SPI Drivers

I2C Bus

/* I2C1 driver — read temperature from LM75 sensor
 * PB6 = SCL, PB7 = SDA, 400 kHz Fast Mode
 */

#include "stm32f4xx_hal.h"

I2C_HandleTypeDef hi2c1;
#define LM75_ADDR  (0x48 << 1)  /* 7-bit address shifted left */
#define LM75_TEMP_REG  0x00

void I2C1_Init(void)
{
    hi2c1.Instance             = I2C1;
    hi2c1.Init.ClockSpeed      = 400000;  /* 400 kHz */
    hi2c1.Init.DutyCycle       = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1     = 0;
    hi2c1.Init.AddressingMode  = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode   = I2C_NOSTRETCH_DISABLE;
    HAL_I2C_Init(&hi2c1);
}

float LM75_ReadTemp(void)
{
    uint8_t buf[2];
    HAL_I2C_Mem_Read(&hi2c1, LM75_ADDR, LM75_TEMP_REG,
                     I2C_MEMADD_SIZE_8BIT, buf, 2, 100);
    int16_t raw = (buf[0] << 8) | buf[1];
    raw >>= 5;  /* 11-bit resolution */
    return raw * 0.125f;  /* 0.125°C per LSB */
}
I2C Bus Transaction (Logic Analyzer)
I2C1 @ 400 kHz — LM75 Temperature Read
========================================
[START] 0x90 W ACK          ← Address 0x48 + Write bit
        0x00   ACK          ← Temperature register pointer
[START] 0x91 R ACK          ← Address 0x48 + Read bit (repeated start)
        0x19   ACK          ← MSB: 0x19 = 25
        0x80   NACK         ← LSB: 0x80 (bit 7 = 0.5°C)
[STOP]

Decode: raw = (0x19 << 8) | 0x80 = 0x1980
        raw >>= 5 → 0xCC = 204
        temp = 204 × 0.125 = 25.5°C ✓

Bus timing:  SCL = 2.5 µs period (400 kHz)
Transaction: 3 bytes = ~67.5 µs total

SPI Bus

/* SPI1 driver — communicate with external flash (W25Q128)
 * PA5 = SCK, PA6 = MISO, PA7 = MOSI, PA4 = CS (GPIO)
 */

#include "stm32f4xx_hal.h"

SPI_HandleTypeDef hspi1;
#define FLASH_CS_LOW()   HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define FLASH_CS_HIGH()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
#define W25Q_READ_ID     0x9F

void SPI1_Init(void)
{
    hspi1.Instance               = SPI1;
    hspi1.Init.Mode              = SPI_MODE_MASTER;
    hspi1.Init.Direction         = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize          = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity       = SPI_POLARITY_LOW;   /* CPOL = 0 */
    hspi1.Init.CLKPhase          = SPI_PHASE_1EDGE;     /* CPHA = 0 */
    hspi1.Init.NSS               = SPI_NSS_SOFT;
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; /* 25 MHz */
    hspi1.Init.FirstBit          = SPI_FIRSTBIT_MSB;
    HAL_SPI_Init(&hspi1);
}

uint32_t W25Q_ReadID(void)
{
    uint8_t cmd = W25Q_READ_ID;
    uint8_t id[3];
    FLASH_CS_LOW();
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
    HAL_SPI_Receive(&hspi1, id, 3, 100);
    FLASH_CS_HIGH();
    return (id[0] << 16) | (id[1] << 8) | id[2];
    /* W25Q128: returns 0xEF4018 */
}
SPI Bus Transaction (Logic Analyzer)
SPI1 @ 25 MHz — W25Q128 Read JEDEC ID
========================================
CS ──┐                              ┌── CS
     └──────────────────────────────┘
CLK  ___/‾‾‾\___/‾‾‾\___/‾‾‾\___/‾‾‾\___
MOSI [  0x9F  ][  0x00  ][  0x00  ][  0x00  ]
MISO [  0xFF  ][  0xEF  ][  0x40  ][  0x18  ]

Decode:
  Manufacturer ID: 0xEF → Winbond
  Memory Type:     0x40 → SPI NOR Flash
  Capacity:        0x18 → 2^24 = 16 MB (128 Mbit)
  Device:          W25Q128JV ✓

SPI Config:  Mode 0 (CPOL=0, CPHA=0)
Bit order:   MSB first
Clock:       25 MHz (100 MHz APB2 / 4)
Transaction: 4 bytes = 1.28 µs

ADC & DMA

/* ADC1 with DMA — continuous multi-channel sampling
 * PA0 = ADC1_CH0 (sensor), PA1 = ADC1_CH1 (voltage divider)
 */

#include "stm32f4xx_hal.h"

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
uint16_t adc_buffer[2];  /* DMA writes here continuously */

void ADC1_Init(void)
{
    ADC_ChannelConfTypeDef sConfig = {0};

    hadc1.Instance                   = ADC1;
    hadc1.Init.ClockPrescaler        = ADC_CLOCK_SYNC_PCLK_DIV4;
    hadc1.Init.Resolution            = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode          = ENABLE;
    hadc1.Init.ContinuousConvMode    = ENABLE;
    hadc1.Init.NbrOfConversion       = 2;
    hadc1.Init.DMAContinuousRequests = ENABLE;
    hadc1.Init.EOCSelection          = ADC_EOC_SEQ_CONV;
    HAL_ADC_Init(&hadc1);

    /* Channel 0 — sensor input */
    sConfig.Channel      = ADC_CHANNEL_0;
    sConfig.Rank         = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    /* Channel 1 — voltage divider */
    sConfig.Channel = ADC_CHANNEL_1;
    sConfig.Rank    = 2;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    /* Start ADC with DMA */
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, 2);
}

/* Read values: adc_buffer[0] = CH0, adc_buffer[1] = CH1
 * Voltage = adc_buffer[x] * 3.3 / 4095.0
 */
ADC DMA Continuous Sampling
ADC1 Configuration
========================================
Resolution:     12-bit (0–4095)
Reference:      3.3V (VDDA)
LSB size:       3.3V / 4095 = 0.806 mV
Sample time:    84 cycles @ 25 MHz = 3.36 µs
Conversion:     12 cycles
Total/channel:  96 cycles = 3.84 µs
2-channel scan: 7.68 µs per sequence

DMA Circular Mode — adc_buffer[] updated automatically:
  adc_buffer[0] = 2482  → CH0 = 2482 × 0.806mV = 2.000V (sensor)
  adc_buffer[1] = 1241  → CH1 = 1241 × 0.806mV = 1.000V (divider)

Sample rate:  ~130 kSPS per channel (continuous)
CPU overhead: 0% (DMA handles all transfers)

FreeRTOS Integration

/* FreeRTOS task setup — sensor reading + UART reporting
 * Requires CMSIS-RTOS2 wrapper (STM32CubeIDE default)
 */

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>

SemaphoreHandle_t uart_mutex;

void SensorTask(void *pvParameters)
{
    float temp;
    for (;;) {
        temp = LM75_ReadTemp();  /* From I2C driver above */

        if (xSemaphoreTake(uart_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
            printf("Temp: %.1f C\r\n", temp);
            xSemaphoreGive(uart_mutex);
        }
        vTaskDelay(pdMS_TO_TICKS(1000));  /* Read every 1 second */
    }
}

void HeartbeatTask(void *pvParameters)
{
    for (;;) {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  /* Toggle LED */
        vTaskDelay(pdMS_TO_TICKS(500));          /* 500ms blink */
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    GPIO_Init();
    UART2_Init();
    I2C1_Init();

    uart_mutex = xSemaphoreCreateMutex();

    xTaskCreate(SensorTask,    "Sensor",    256, NULL, 2, NULL);
    xTaskCreate(HeartbeatTask, "Heartbeat", 128, NULL, 1, NULL);

    vTaskStartScheduler();
    for (;;) {}  /* Should never reach here */
}
Serial Terminal — FreeRTOS Output
--- FreeRTOS v10.4.6 on STM32F411 ---
Tasks:  SensorTask    (prio 2, stack 256 words = 1024 bytes)
        HeartbeatTask (prio 1, stack 128 words = 512 bytes)
        IDLE          (prio 0, auto-created)
Total heap: 15360 bytes

[  500ms] LED ON                     ← HeartbeatTask
[ 1000ms] Temp: 25.5 C              ← SensorTask (1s interval)
[ 1000ms] LED OFF                    ← HeartbeatTask
[ 1500ms] LED ON
[ 2000ms] Temp: 25.6 C
[ 2000ms] LED OFF
[ 2500ms] LED ON
[ 3000ms] Temp: 25.5 C
[ 3000ms] LED OFF

Note: SensorTask (prio 2) preempts HeartbeatTask (prio 1)
      when both are ready simultaneously.
      uart_mutex prevents garbled output from concurrent printf.
Case Study
Boeing 787 Dreamliner — The 248-Day Integer Overflow (2015)

In 2015, the FAA issued an emergency Airworthiness Directive (AD 2015-10-07) requiring all Boeing 787 operators to power-cycle the aircraft’s Generator Control Units (GCUs) at least every 248 days. The reason: a 32-bit signed integer counter in the GCU firmware overflowed after exactly 231 hundredths of a second — precisely 248.55 days.

The firmware bug: The GCU tracked uptime using a 32-bit signed integer incremented every 10 milliseconds. After 2,147,483,647 ticks (231 − 1), the counter wrapped to a negative value, causing the GCU to enter a failsafe mode that simultaneously shut down all four main generators. On a fully electric aircraft like the 787 (no pneumatic systems, no hydraulic pumps), losing all generators means losing everything — flight controls, avionics, cabin pressure.

Firmware lesson: Always use uint32_t (not int32_t) for uptime counters — this doubles the overflow period to 497 days. Better yet, use uint64_t for any counter that should never overflow in the product’s lifetime. The STM32 HAL’s HAL_GetTick() uses uint32_t, which overflows after ~49.7 days at 1 ms resolution. If your product runs continuously, you must handle this.

Integer Overflow 248-Day Timer FAA Airworthiness Directive Safety-Critical
Case Study
Mars Curiosity Rover — Remote Firmware Update at 225 Million km (2013)

In February 2013, NASA performed a complete firmware update on the Curiosity rover’s main computer — a RAD750 processor running VxWorks — while it sat on Mars, 225 million km from Earth. The update was needed because a corrupted flash memory block was causing the rover to repeatedly enter safe mode.

The firmware architecture: Curiosity carries two identical flight computers (Side A and Side B), each with its own copy of the flight software. The team first switched from the failing Side A to Side B, diagnosed the flash corruption remotely, then uploaded a patch that instructed the file system to mark the corrupted block as unusable — similar to bad-sector remapping on a hard drive. The entire process took 4 days with a 14-minute one-way communication delay.

Firmware lesson: Design for remote updates from day one. Curiosity’s firmware included: (1) dual-bank flash with A/B partitioning, (2) a bootloader that could select either bank, (3) integrity checking (CRC32) on every flash write, and (4) automatic fallback to the previous version if the new firmware failed to boot. These patterns apply directly to any STM32 product using OTA updates — implement a bootloader with dual-bank support and integrity verification before you ship.

Remote Update 225M km Debug Dual-Bank Flash Bootloader Design

Exercises

Exercise 1: Clock Tree Configuration

Your STM32F411 board has an 8 MHz HSE crystal and you need to drive a WS2812B LED strip that requires an 800 kHz data signal generated by a timer. You also need USB (48 MHz) for debug.

  1. Calculate PLL settings (M, N, P, Q) that give you both a SYSCLK divisible by 800 kHz AND a valid 48 MHz USB clock. Show your work.
  2. What is the maximum SYSCLK you can achieve while meeting both constraints?
  3. At your chosen SYSCLK, what flash latency (wait states) is required? Reference the STM32F411 datasheet Table 6.
  4. Write the SystemClock_Config() function with your calculated values.

Hint: USB needs exactly 48 MHz from PLL_Q. Work backwards: VCO = 48 × Q. Try Q=4 → VCO=192 MHz → SYSCLK=192/P. With P=2 → 96 MHz (divisible by 800 kHz? 96,000/800 = 120 ✓). For 8 MHz HSE: M=8 → PLL_input=1 MHz, N=192 → VCO=192 MHz. Flash latency: 3 wait states for 96 MHz @ 2.7-3.6V.

Exercise 2: I2C Multi-Sensor Bus

You need to read from three sensors on the same I2C1 bus: LM75 temperature sensor (address 0x48), BMP280 pressure sensor (address 0x76), and SHT30 humidity sensor (address 0x44). All use 400 kHz Fast Mode.

  1. Calculate the total I2C bus time to read all three sensors in sequence. LM75 reads 2 bytes, BMP280 reads 6 bytes (temperature + pressure), SHT30 reads 6 bytes (temperature + humidity). Include address bytes and ACK/NACK overhead.
  2. One sensor occasionally NAKs its address byte. Write a retry function that attempts up to 3 reads with 10ms delays, returning an error code if all attempts fail.
  3. You add a fourth sensor and the bus starts failing intermittently. The SDA line shows a 2.8V high level instead of 3.3V. What is likely wrong and how do you fix it?

Hint: Each I2C byte = 9 bit periods (8 data + ACK). At 400 kHz, each bit = 2.5µs, each byte = 22.5µs. Include START (1 bit) and STOP (1 bit) conditions. For the voltage issue: I2C uses open-drain with pull-up resistors. Too many devices (excess bus capacitance) or too-high pull-up values cause slow rise times. Try reducing pull-ups from 4.7kΩ to 2.2kΩ.

Exercise 3: FreeRTOS Priority Inversion

You have three FreeRTOS tasks sharing a UART mutex:

  • HighPrioTask (priority 3): Reads GPS every 100ms, prints position via UART
  • MedPrioTask (priority 2): Processes sensor fusion, CPU-intensive, no UART use
  • LowPrioTask (priority 1): Logs battery voltage via UART every 5 seconds
  1. Describe the priority inversion scenario: LowPrioTask holds the UART mutex, HighPrioTask tries to acquire it, and MedPrioTask becomes ready. What happens and why does HighPrioTask starve?
  2. FreeRTOS provides xSemaphoreCreateMutex() which includes priority inheritance. Explain how priority inheritance solves this specific scenario step-by-step.
  3. Your HighPrioTask has a 100ms deadline. LowPrioTask holds the mutex for up to 50ms during a large log print. Even with priority inheritance, can HighPrioTask miss its deadline? If so, propose a design change that guarantees the deadline is met.

Hint: Priority inversion: Low holds mutex → High blocks on mutex → Medium preempts Low (since Medium > Low) → Low can’t release mutex → High starves behind Medium. Priority inheritance: when High blocks on mutex, Low’s priority is temporarily raised to 3 (High’s level), so Medium can’t preempt Low. For the deadline: even with inheritance, High must wait up to 50ms for Low to finish — reduce critical section length or use a dedicated UART task with a message queue.

Firmware Configuration Tool

Generate a firmware configuration document listing your peripheral assignments, clock settings, and pin mappings.

Firmware Configuration Sheet

Document your peripheral configuration and pin assignments. Download as Word, Excel, or PDF.

Draft auto-saved

Conclusion & Next Steps

You now have a complete firmware foundation: clock configuration, GPIO, UART for debug output, I2C and SPI for sensor/peripheral communication, ADC with DMA for analog sampling, and FreeRTOS for multitasking. These building blocks appear in every embedded product.

Next in the Series

In Part 11: Advanced Embedded Systems, we’ll explore RF design, ultra-low power techniques, TinyML at the edge, and reliability engineering for harsh environments.