Real-time telemetry dashboard for PC racing simulators running on an STM32H750B-DK dev board.
SimHub on the PC streams telemetry over a high-speed serial link (UART/DMA). The MCU renders a multi-screen TouchGFX UI (swipe between views) and drives external inputs/outputs: WS2812B shift lights + a set of physical buttons exposed to the PC as a USB HID gamepad.
Demo video (hosted on my collaborator's channel): https://www.youtube.com/watch?v=GPmhzNYBqZ4
- TouchGFX UI with 4 swipeable views: main dashboard, radar, pedal positions + history graph, live-generated track map
- Telemetry pipeline: SimHub -> fixed-length packet -> UART (921600 baud) -> DMA -> FreeRTOS queues -> TouchGFX (MVP)
- WS2812B LED strip driven via SPI + DMA (shift lights, flags, TC/ABS status)
- 6 physical buttons read via GPIO + debounce and sent as an 8-bit USB Custom HID report (gamepad buttons)
- Display: 4.3" touchscreen, 480x272 (STM32H750B-DK)
- UI refresh: VSync (60 Hz target)
- Telemetry link: UART (USART3) @ 921600 baud using DMA
- Telemetry packet: fixed-length
DataPacket(585 bytes) framed with "S" ... "E" - LEDs: WS2812B strip (16 LEDs) driven via SPI + DMA
- Buttons: 6 tactile buttons exposed to the PC as a USB gamepad (Custom HID)
- Enclosure: custom Fusion 360 design, 3D printed at home (button caps are from an external model)
- DMA-first data path: UART RX via DMA into a fixed-size packet struct, with RTOS queues sized to 1 element so the UI always renders the latest telemetry
- Framed packet protocol: packets are framed with start/end markers ("S" ... "E"); a low-priority resync task restarts DMA when the stream is misaligned
- UI architecture: TouchGFX MVP (Model reads from queue, Presenter updates only the active page to reduce invalidations)
- Live track map: builds a 1000-sample lookup table during driving (0.1% lap steps), interpolates missed samples, then scales/rotates and renders a dynamic bitmap
- WS2812B without bit-banging: encodes WS2812B timing into SPI bit patterns and transmits via SPI + DMA (shift lights + flag/TC/ABS indicators)
- Plug-and-play buttons: GPIO scan + debounce, exposed as an 8-bit USB Custom HID report (appears as a gamepad)
flowchart LR
subgraph PC[PC]
SIM[Racing simulator]
SH[SimHub<br/>Custom Serial Device]
HID[USB HID<br/>Gamepad]
SIM --> SH
end
subgraph MCU[STM32H750B-DK]
UART[USART3 RX<br/>DMA]
ISR[HAL_UART_RxCpltCallback]
DQ[(dataQueue<br/>DataPacket)]
LQ[(LEDQueue<br/>DataPacketLED)]
SQ[(dataSyncQueue<br/>startChar)]
DT[dataTask<br/>DMA restart + resync]
MODEL[TouchGFX Model::tick]
PRES[Presenter<br/>screen logic]
VIEW[View<br/>UI widgets]
LEDT[LEDTask<br/>WS2812 effects]
SPI[SPI2 TX<br/>DMA]
LEDS[WS2812B<br/>LED strip]
BT[buttonTask<br/>GPIO scan + debounce]
USB[USB Device<br/>Custom HID]
UART --> ISR
ISR --> DQ
ISR --> LQ
ISR --> SQ
SQ --> DT
DT --> UART
DQ --> MODEL --> PRES --> VIEW
LQ --> LEDT --> SPI --> LEDS
BT --> USB
end
SH -->|UART @ 921600| UART
USB -->|HID reports| HID
Notes:
- Telemetry is a fixed-size, NUL-padded packet framed with start/end markers so the MCU can DMA directly into a C struct.
- The track map is generated during driving from position samples and scaled/rotated to fit the 480x272 display.
Telemetry receive + resync sequence (Mermaid)
sequenceDiagram
participant SH as SimHub
participant UART as USART3 + DMA
participant ISR as RX complete IRQ
participant DT as dataTask (resync)
participant DQ as dataQueue
participant TG as TouchGFX Model::tick
participant UI as Presenter/View
SH->>UART: Send fixed-size packet ("S" ... "E")
UART-->>ISR: DMA complete
ISR->>DQ: Overwrite latest DataPacket
ISR->>DT: Post startChar to dataSyncQueue
DT->>DT: Wait (timeout) for startChar
alt startChar != 'S' or timeout
DT->>UART: Abort + restart DMA RX (resync)
end
TG->>DQ: osMessageQueueGet()
TG->>UI: updateData(DataPacket)
- Dashboard: speed/gear/RPM + tyres/fuel/laps/electronics
- Radar: left/right proximity indicators based on angle + distance
- Pedals: throttle/brake/clutch bars + scrolling history graph
- Track map: live-generated track outline + driver positions
| Dashboard | Radar |
|---|---|
![]() |
![]() |
| Pedals + graphs | Track map |
|---|---|
![]() |
![]() |
Mechanical design (Fusion 360)
Custom enclosure parts were modeled in Fusion 360 and 3D printed at home. Button caps were also 3D printed from an external model.
| LED top piece | Side button holder | Mounting bracket |
|---|---|---|
![]() |
![]() |
![]() |
Core/Inc/main.h-DataPacket/DataPacketLED(telemetry packet layout)Core/Src/main.c- FreeRTOS tasks, UART DMA receive + resync, button scanning + HID reports, LED queueingTouchGFX/gui/src/model/Model.cpp- queue -> presenter handoffTouchGFX/gui/src/screen1_screen/Screen1Presenter.cpp- per-screen updates, radar math, track-map orchestrationSTM32CubeIDE/Application/User/TrackMapClass.cpp- track sampling, scaling/rotation, dynamic bitmap generationSTM32CubeIDE/Application/User/WS2812_SPI.c- WS2812B encoding over SPI + DMA and LED effectsUSB_DEVICE/App/usbd_custom_hid_if.c- custom HID report descriptor
Telemetry packet format
- Packet is a fixed-length C struct (
DataPacket, 585 bytes) framed with start/end markers ("S" ... "E") so DMA can write directly into the receive buffer. - Most numeric fields are transmitted as fixed-length, NUL-padded strings (converted on-device using
atoi/strtof). - RX path uses DMA + a 1-element message queue so the UI renders the newest sample and never lags behind.
Related code:
Core/Inc/main.hCore/Src/main.c
Live track-map generation
- Samples car position while driving and builds a 1000-entry lookup table (0.1% lap steps).
- Interpolates missed samples to keep the outline continuous.
- Scales/centers the map to screen bounds and conditionally rotates for vertical tracks, then draws a dynamic bitmap used as the map background.
Related code:
STM32CubeIDE/Application/User/TrackMapClass.cppTouchGFX/gui/src/screen1_screen/Screen1Presenter.cpp
WS2812B shift lights via SPI + DMA
- Encodes WS2812B timing into SPI bit patterns (no CPU bit-banging) and transmits the whole frame via SPI DMA.
- Outer LEDs indicate flags/TC/ABS; center LEDs act as shift lights with color transitions and a checkered-flag animation.
Related code:
STM32CubeIDE/Application/User/WS2812_SPI.c
USB HID buttons
- 6 GPIO buttons are sampled periodically, debounced, and packed into an 8-bit report.
- Device enumerates as a Custom HID gamepad, so buttons can be mapped in the sim without custom drivers.
Related code:
Core/Src/main.cUSB_DEVICE/App/usbd_custom_hid_if.c
- MCU/Board: STM32H750B-DK (Cortex-M7)
- RTOS: FreeRTOS (CMSIS-RTOS v2)
- UI: TouchGFX (MVP pattern)
- Peripherals: LTDC/DMA2D, QSPI + SDRAM, UART (DMA), SPI (DMA), USB Device (Custom HID)
- This repo contains the STM32 firmware + TouchGFX project.
- The PC-side SimHub Custom Serial Device formatter script and Fusion 360 source files are not included in this snapshot (only screenshots).
This repository is published as a portfolio/demo artifact.
- Third-party components are under their respective licenses (see
Drivers/**/LICENSE*,Middlewares/**/LICENSE*, and individual file headers). - Unless a file header states otherwise, project-specific application code is provided for viewing/evaluation only (all rights reserved) and is not licensed for reuse or redistribution. If you want to reuse parts of it, please contact the authors.
Co-developed with my brother using a pair-programming workflow (I was usually the "driver" writing firmware while we jointly designed/debugged/tested the system).











