This project implements a real-time IMU data logging system on the STM32F407 using FreeRTOS through the CMSIS-RTOS2 API.
The system periodically samples 3-axis acceleration data from an MPU6500 at 50 Hz, distributes the sampled data to multiple RTOS tasks through message queues, displays runtime information through UART5, and stores selected samples into a W25Q64 SPI Flash.
The project also provides a UART command interface for controlling the logger, reading stored records, clearing Flash memory, checking system status, and injecting a simulated task-health fault.
A dedicated SupervisorTask monitors the health of critical tasks using Event Flags. The Independent Watchdog (IWDG) is refreshed only when all monitored tasks report healthy. If a task fails to report its health status, the watchdog is intentionally not refreshed and the MCU is automatically reset.
The main purpose of this project is to integrate multiple STM32 and RTOS concepts into one complete embedded system instead of demonstrating each peripheral or RTOS mechanism independently.
| Component | Usage |
|---|---|
| STM32F407G-DISC1 | Main development board |
| STM32F407VGT6U | Main MCU |
| MPU6500 | 3-axis acceleration sampling |
| W25Q64 | External SPI Flash storage |
| UART5 | PC terminal interface |
| PA0 Button | Start / stop logging |
| Green LED | System heartbeat indication |
| Orange LED | IMU output indication |
| IWDG | System fault recovery |
| Interface | Usage |
|---|---|
| SPI2 | Shared by MPU6500 and W25Q64 |
| UART5 | PC terminal communication |
| PA0 / EXTI0 | User button interrupt |
| IWDG | Hardware watchdog |
UART5 configuration:
Baud Rate : 115200
Data Bits : 8
Parity : None
Stop Bits : 1
The MPU6500 and W25Q64 share the same SPI2 bus and use separate chip-select signals.
Access to SPI2 is protected by spiMutex.
UART5 output is shared by multiple tasks and protected by uartMutex.
- Board: STM32F407G-DISC1
- MCU: STM32F407VGT6U
- IDE: Keil uVision / MDK-ARM
- Configuration Tool: STM32CubeMX
- RTOS: FreeRTOS
- RTOS API: CMSIS-RTOS2
- STM32 HAL
- Debugger / Programmer: ST-LINK
- FreeRTOS multitasking using CMSIS-RTOS2
- 50 Hz periodic MPU6500 acceleration sampling
- Fixed-period scheduling using
osDelayUntil() - Producer / consumer communication using Message Queues
- Separate queues for runtime data output and Flash logging
- Tagged logger messages for
SAMPLE,DUMP, andCLEAR - Shared SPI2 bus protected by Mutex
- Shared UART5 output protected by Mutex
- UART5 interrupt-based command reception
- PA0 external interrupt control
- Thread Flags for ISR-to-task notification
- Event Flags for multi-task health monitoring
- W25Q64 Page Program, Read, and Sector Erase
- 32-byte aligned Flash record format
- 8-sector / 32 KB logger region
- Up to 1024 stored IMU records
- UART commands for logger control
- Independent Watchdog fault recovery
- Watchdog reset-cause detection
- UART-based fault injection for watchdog testing
The application is divided into independent RTOS tasks with clearly separated responsibilities.
The system contains six tasks:
IMUTaskPrintTaskLoggerTaskCommandTaskHeartbeatTaskSupervisorTask
The main data path is:
MPU6500
|
v
IMUTask
|
+----> imuQueue ------> PrintTask ------> UART5
|
+----> loggerQueue ---> LoggerTask <---> W25Q64
The control path is:
PC Terminal ---- UART RX IRQ / Thread Flag ---\
\
> CommandTask
/
PA0 Button ----- EXTI0 IRQ / Thread Flag -----/
CommandTask controls logging state and can also send DUMP and CLEAR requests to LoggerTask.
| Task | Priority | Main Responsibility |
|---|---|---|
SupervisorTask |
High | Monitor task health and control IWDG refresh |
IMUTask |
Above Normal | Sample MPU6500 every 20 ms |
PrintTask |
Normal | Consume IMU data and output runtime information |
CommandTask |
Normal | Handle PA0 button and UART commands |
LoggerTask |
Below Normal | Handle W25Q64 logging, dump, and clear operations |
HeartbeatTask |
Low | Toggle heartbeat LED and report task health |
SupervisorTask has the highest priority because it is responsible for system health monitoring and watchdog control. It normally remains blocked while waiting for Event Flags, so it does not continuously consume CPU time.
IMUTask has an Above Normal priority because maintaining the 50 Hz sampling period is more time-sensitive than UART output or Flash storage.
PrintTask and CommandTask use Normal priority because they handle general runtime output and user interaction.
LoggerTask runs at Below Normal priority because Flash logging is less time-critical than sensor acquisition.
HeartbeatTask has the lowest priority because LED indication is not time-critical.
imuQueue transfers IMUData_t samples from IMUTask to PrintTask.
IMUTask
|
| IMUData_t
v
imuQueue
|
v
PrintTask
Queue depth:
16 samples
IMUTask inserts data using a zero timeout.
If the queue becomes full, the high-priority sampling task does not wait. Instead, the dropped-sample counter is incremented.
This prevents a slow consumer from delaying the 50 Hz sensor acquisition period.
loggerQueue transfers LoggerMessage_t messages to LoggerTask.
Queue depth:
32 messages
Each message contains a message type:
typedef enum
{
LOGGER_MESSAGE_SAMPLE = 0,
LOGGER_MESSAGE_DUMP,
LOGGER_MESSAGE_CLEAR
} LoggerMessageType_t;The logger queue therefore carries both data messages and control messages.
IMUTask -------- SAMPLE ------┐
|
|
v
loggerQueue --------> LoggerTask
^
|
|
CommandTask ----- DUMP -------|
----- CLEAR ------|
Because the queue is FIFO, previously queued samples are processed before a later DUMP or CLEAR request.
This keeps all W25Q64 operations serialized inside LoggerTask.
The MPU6500 and W25Q64 share SPI2.
Both IMUTask and LoggerTask may access the SPI peripheral, so the shared bus is protected using spiMutex.
IMUTask -----\
>---- spiMutex ---- SPI2
LoggerTask --/
This prevents two tasks from using SPI2 at the same time.
During W25Q64 Page Program or Sector Erase operations, the Flash performs part of the operation internally.
The SPI bus is released while the Flash is busy. LoggerTask periodically checks the W25Q64 BUSY status and uses osDelay(1) between checks so other tasks can continue running.
UART5 output is shared by multiple tasks.
uartMutex ensures that a complete UART message is transmitted without another task mixing its output into the same message.
IMUTask
PrintTask
LoggerTask
CommandTask
SupervisorTask
|
v
uartMutex
|
v
UART5
IMUTask reads acceleration data from the MPU6500 every 20 ms.
20 ms period
= 50 Hz sampling rate
The periodic timing is implemented using:
nextWakeTick += 20U;
osDelayUntil(nextWakeTick);Using osDelayUntil() keeps the sampling schedule based on an absolute timing reference and reduces long-term timing drift caused by task execution time.
Each successful sample contains:
typedef struct
{
uint32_t sequence;
uint32_t tick_ms;
uint32_t print_dropped_total;
uint32_t logger_dropped_total;
uint32_t spi_error_total;
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
} IMUData_t;IMUTask always sends the sample to imuQueue.
When logging is enabled, the same sample is also wrapped in a LOGGER_MESSAGE_SAMPLE message and sent to loggerQueue.
PrintTask consumes every sample successfully queued in imuQueue.
The task prints one sample for every 25 received samples.
Since the IMU sampling frequency is 50 Hz:
25 samples / 50 Hz
= one UART output every 500 ms
Example:
SEQ=250, T=5187 ms, AX=75 mg, AY=-137 mg, AZ=-932 mg, PDROP=0, LDROP=0, SPIERR=0
The counters provide runtime diagnostics:
| Field | Meaning |
|---|---|
PDROP |
Samples dropped because imuQueue was full |
LDROP |
Samples dropped because loggerQueue was full |
SPIERR |
MPU6500 SPI read errors |
The Orange LED toggles whenever a sample is printed.
Each stored sample uses a fixed-size FlashRecord_t.
The record structure is:
typedef struct
{
uint32_t magic;
uint32_t sequence;
uint32_t tick_ms;
uint32_t print_dropped_total;
uint32_t logger_dropped_total;
uint32_t spi_error_total;
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
uint16_t reserved;
} FlashRecord_t;The record size is:
32 Bytes
The reserved field keeps the structure aligned to exactly 32 bytes.
Every record contains:
#define FLASH_RECORD_MAGIC 0x31554D49ULThe value is checked when records are read back.
It helps distinguish valid logger records from erased or invalid Flash contents.
1 Record = 32 Bytes
1 Page
= 256 Bytes
= 8 Records
1 Sector
= 4096 Bytes
= 16 Pages
= 128 Records
Logger Region
= 8 Sectors
= 32 KB
= 1024 Records
The logger starts at:
0x000000
After every successful write:
writeAddress += sizeof(FlashRecord_t);Since a record is 32 bytes and one Flash Page is 256 bytes, exactly eight records fit into each Page.
This keeps each record inside a single Page and avoids crossing a Page boundary during Page Program operations.
The Flash driver uses the following W25Q64 commands:
| Operation | Command |
|---|---|
| Write Enable | 0x06 |
| Read Status Register-1 | 0x05 |
| Page Program | 0x02 |
| Sector Erase 4 KB | 0x20 |
| Read Data | 0x03 |
The Page Program function verifies that:
length <= 256 bytes
and that the requested data remains inside a single Page.
The configured logger region consists of eight consecutive 4 KB sectors.
Logging can be controlled using either:
start
stop
through UART, or by pressing the PA0 user button.
PA0 uses an external interrupt.
The interrupt callback does not perform the logger operation directly.
Instead, it sets a Thread Flag:
PA0 Button
|
v
EXTI0 Interrupt
|
v
COMMAND_BUTTON_FLAG
|
v
CommandTask
CommandTask then changes the logging state.
When logging is disabled:
- MPU6500 sampling continues.
imuQueuecontinues receiving samples.PrintTaskcontinues displaying IMU data.- No new samples are inserted into
loggerQueue.
When logging is enabled:
IMUTask
|
v
LOGGER_MESSAGE_SAMPLE
|
v
loggerQueue
|
v
LoggerTask
|
v
W25Q64
Available commands:
| Command | Description |
|---|---|
start |
Start Flash logging |
stop |
Stop Flash logging |
status |
Display logger and queue status |
dump N |
Read the latest N records (1 ~ 50) |
clear |
Erase the configured logger region |
fault imu |
Disable IMUTask health reporting for fault-injection testing |
help |
Display available commands |
Example:
status
Output:
[STATUS] ready=1, busy=0, logging=0, full=0, records=0/1024, imuQ=0, loggerQ=0
UART5 receives one byte at a time using interrupt mode.
The first receive operation is started using:
HAL_UART_Receive_IT(&huart5, &uartRxByte, 1U);Each received byte triggers:
HAL_UART_RxCpltCallback()The received characters are accumulated in uartCommandBuffer.
When \r or \n is received, the callback terminates the command string and sets:
COMMAND_UART_RX_FLAG
The flow is:
PC Terminal
|
v
UART5 RX Interrupt
|
v
HAL_UART_RxCpltCallback()
|
v
COMMAND_UART_RX_FLAG
|
v
CommandTask
|
v
Command_ProcessLine()
Command parsing and Flash operations are not performed inside the ISR.
The command:
dump 10
requests the latest ten stored records.
dump N accepts:
N = 1 ~ 50
Dump is rejected while logging is active.
Example:
Dump requested.
Dump last 10 records:
LOG[247] SEQ=709, T=14367, AX=108, AY=-188, AZ=-919
LOG[248] SEQ=710, T=14387, AX=108, AY=-190, AZ=-919
LOG[249] SEQ=711, T=14407, AX=111, AY=-191, AZ=-916
...
LOG[256] SEQ=718, T=14547, AX=111, AY=-195, AZ=-916
Dump complete.
The request is sent to LoggerTask as:
LOGGER_MESSAGE_DUMP
Because the request uses the same FIFO queue as sample messages, any samples already waiting in loggerQueue are processed before the dump operation.
This ensures the dump reads the latest samples that had already been queued for storage.
The command:
clear
requests all eight configured logger sectors to be erased.
Clear is rejected while logging is active.
The request is sent to LoggerTask as:
LOGGER_MESSAGE_CLEAR
Before the request is processed, the logger enters a Busy state so new logging operations cannot start.
LoggerTask then:
- Erases all eight sectors.
- Resets the message queue.
- Resets
writeAddress. - Resets
loggerRecordCount. - Clears the Full state.
- Returns the logger to the Ready state.
The write address returns to:
0x000000
After the clear operation, a new logging session starts from the beginning of the configured Flash region.
The project monitors five tasks using CMSIS-RTOS2 Event Flags.
| Health Flag | Task |
|---|---|
| Bit 0 | IMUTask |
| Bit 1 | PrintTask |
| Bit 2 | LoggerTask |
| Bit 3 | CommandTask |
| Bit 4 | HeartbeatTask |
The combined expected value is:
0x1F
or:
11111b
Each task periodically reports its health using:
osEventFlagsSet()SupervisorTask waits for all monitored tasks using:
osEventFlagsWait(
systemHealthEventHandle,
HEALTH_ALL_FLAGS,
osFlagsWaitAll,
HEALTH_WAIT_TIMEOUT_MS
);The IWDG is not refreshed by an independent unconditional watchdog task.
Instead, SupervisorTask refreshes the watchdog only when all monitored tasks report healthy.
Normal condition:
IMUTask -----------\
PrintTask ----------\
LoggerTask ----------\
CommandTask ----------> systemHealthEvent
HeartbeatTask -------/
|
v
SupervisorTask
|
All 5 flags received
|
v
HAL_IWDG_Refresh()
Failure condition:
Missing health flag
|
v
SupervisorTask detects failure
|
v
Do not refresh IWDG
|
v
IWDG timeout
|
v
MCU Reset
This allows the hardware watchdog to recover the system if one of the monitored tasks stops reporting its health state.
For demonstration and validation, the UART command:
fault imu
enables a simulated IMU health-reporting fault.
When the fault is active, IMUTask continues sampling the MPU6500 but intentionally stops setting:
HEALTH_IMU_FLAG
Therefore the expected health value changes from:
Expected : 0x1F
Actual : 0x1E
Example:
Fault injected: IMU health report disabled.
Health monitor: FAIL
flags=0x1E, expected=0x1F
Because the system is no longer considered healthy, SupervisorTask stops refreshing the IWDG.
The watchdog eventually resets the MCU.
The MCU checks the RCC reset flags during startup.
After an IWDG reset, the terminal displays:
--- MCU RESET ---
Reset cause: IWDG watchdog
For a normal power-on:
--- MCU RESET ---
Reset cause: Power-on
The fault-injection variable is initialized back to its default state after reset, so the system automatically returns to normal operation.
HeartbeatTask toggles the Green LED every:
500 ms
The task also reports:
HEALTH_HEARTBEAT_FLAG
to the health monitor.
PrintTask toggles the Orange LED whenever it outputs an IMU sample to UART.
status
Example:
[STATUS] ready=1, busy=0, logging=0, full=0, records=0/1024, imuQ=0, loggerQ=0
start
Output:
Logging started.
stop
Example:
Logging stopped. Records=637
dump 5
Example:
Dump requested.
Dump last 5 records:
LOG[632] SEQ=1046, T=21087, AX=95, AY=-165, AZ=-926
LOG[633] SEQ=1047, T=21107, AX=96, AY=-164, AZ=-925
LOG[634] SEQ=1048, T=21127, AX=95, AY=-163, AZ=-924
LOG[635] SEQ=1049, T=21147, AX=94, AY=-165, AZ=-925
LOG[636] SEQ=1050, T=21167, AX=95, AY=-165, AZ=-926
Dump complete.
clear
Output:
Clear requested.
Logger clear started.
Logger clear complete.
After clearing:
status
returns a record count of zero.
fault imu
Example:
Fault injected: IMU health report disabled.
Health monitor: FAIL
flags=0x1E, expected=0x1F
After the watchdog timeout:
--- MCU RESET ---
Reset cause: IWDG watchdog
A recommended demonstration sequence is:
status
start
status
stop
dump 5
clear
status
start
stop
fault imu
This demonstrates:
Logger Status
↓
Start Logging
↓
Flash Storage
↓
Stop Logging
↓
Flash Dump
↓
Flash Clear
↓
Restart Logging
↓
Fault Injection
↓
Health Monitor Failure
↓
Stop IWDG Refresh
↓
Watchdog Reset
↓
Automatic Recovery
The project has been tested with the following behavior:
- MPU6500 acceleration values update correctly.
- IMU sampling runs at approximately 50 Hz.
imuQueueandloggerQueueoperate correctly.- MPU6500 and W25Q64 share SPI2 without bus conflicts.
spiMutexprotects shared SPI access.uartMutexprevents UART message interleaving.startandstopcorrectly control Flash logging.- PA0 can also toggle logging.
statusreports logger and queue state.dump Nreads the latest stored records.clearerases the logger region and resets the write position.- Logging can restart correctly after
clear. fault imuproduces a missing IMU health flag.- Supervisor detects
0x1Einstead of expected0x1F. - IWDG resets the MCU when task health monitoring fails.
- The IWDG reset cause is detected after reboot.
This project integrates the following RTOS concepts:
- Task creation
- Task priority design
- Preemptive scheduling
- Ready / Running / Blocked states
- Fixed-period scheduling with
osDelayUntil() - Message Queues
- Mutexes
- Thread Flags
- Event Flags
- ISR-to-task communication
- Producer / consumer architecture
- Shared peripheral protection
- Task health monitoring
- Watchdog-based fault recovery
The project also integrates several embedded-system concepts:
- SPI peripheral communication
- Shared SPI bus arbitration
- External SPI Flash operation
- Page Program constraints
- Sector Erase constraints
- Fixed-size binary records
- Flash address management
- UART interrupt reception
- GPIO external interrupts
- Button debounce
- Hardware watchdog
- MCU reset-cause detection
- Runtime diagnostics and error counters
This project is designed as an RTOS and embedded-system demonstration project.
Current limitations include:
- The logger uses a fixed 32 KB Flash region.
- The logger region is erased during logger initialization after boot.
- Stored records are therefore not restored after an MCU reset or power cycle.
- No wear leveling is implemented.
- No filesystem is used.
- Only MPU6500 acceleration data is logged.
- UART commands use byte-by-byte interrupt reception and a single command buffer.
A future persistent-logger implementation could scan Flash records during startup and recover the latest valid write position instead of erasing the logger region.
This project helped me move from individual STM32 peripheral experiments to designing a complete RTOS-based embedded system.
The main lessons include:
- Dividing system functionality into tasks with clear responsibilities
- Assigning task priorities based on timing requirements
- Using message queues to decouple producers and consumers
- Protecting shared peripherals using mutexes
- Keeping interrupt service routines short
- Using Thread Flags to defer interrupt processing to tasks
- Using Event Flags to monitor multiple tasks
- Designing Flash records around Page and Sector constraints
- Serializing Flash operations inside a dedicated LoggerTask
- Handling Flash busy periods without unnecessarily blocking other tasks
- Using the watchdog as a system-level recovery mechanism
- Refreshing the watchdog only when the complete system is healthy
- Designing reproducible fault-injection tests for watchdog validation
STM32F407-FreeRTOS-IMU-Data-Logger/
│
├── Core/
│ ├── Inc/
│ └── Src/
│
├── Drivers/
│
├── MDK-ARM/ <-- Keil uVision project files
│
├── Middlewares/
│ └── Third_Party/
│ └── FreeRTOS/
│
├── docs/
│ ├── system_architecture.png
│ └── flash_memory_layout.png
│
├── .gitignore
├── README.md
└── STM32F407_FreeRTOS_IMU_Logger.ioc <-- STM32CubeMX configuration file
The configured logger region uses eight W25Q64 sectors:
8 × 4096 Bytes
= 32768 Bytes
= 32 KB
With a 32-byte FlashRecord_t:
32768 / 32
= 1024 Records
At a continuous 50 Hz logging rate, the configured logger region can hold approximately:
1024 / 50
≈ 20.48 seconds
of continuously logged data.
The current implementation intentionally uses a relatively small Flash region to keep testing, dumping, clearing, and demonstration cycles short.
This project is intended for learning, portfolio demonstration, and embedded-system development practice.

