A real-time Advanced Driver Assistance System for an electric vehicle, built on an STM32F103C8T6 "Blue Pill" in bare-metal Embedded C with the STM32 HAL. It runs a deterministic vehicle state machine, computes time-to-collision from three HC-SR04 ultrasonic sensors, drives a 20 kHz motor PWM with hardware fault cut-out, and streams binary telemetry over UART to a live Python instrument cluster.
Built during the Embedded Systems Internship at Emertxe Information Technologies, Bangalore (June – July 2026), as the capstone project against a formal requirements and design specification.
📺 Project walkthrough video · 📄 Engineering report · 🔧 Run it yourself in PICSimLab
| Subsystem | Behaviour |
|---|---|
| EV dynamics | Physics-based inertia model — torque → acceleration → speed, with aerodynamic drag, mechanical braking and regenerative braking |
| Energy model | SOC by energy integration (depletes under load, recovers under regen); range estimated from SOC and drive-mode efficiency |
| Forward collision | TTC from front distance and speed; WARNING <50 cm or TTC <3 s, CRITICAL <20 cm or TTC <1.5 s |
| Blind-spot | Left/right detection under 30 cm, gated above 20 km/h |
| Parking assist | Below 10 km/h, a progressive 0–100 proximity score from the nearest of three sensors |
| State machine | PARKED → READY → DRIVING → REGEN → FAULT, exit from FAULT only by explicit command |
| Fault management | Over-temperature, low SOC and collision-critical latch a hard fault and cut motor PWM within one loop cycle; sensor and comms timeouts degrade gracefully as warnings |
| Alarms | Four-level priority (P0–P3) driving LEDs and a passive buzzer with distinct tone/pattern per level |
| Telemetry | Four binary packet types at 10 Hz over DMA-backed UART |
| Shell | 15 commands for live inspection and fault injection over the same link |
| Watchdog | IWDG resets the system if the main loop stalls beyond 1 s |
flowchart LR
subgraph Sensing
POT["4x Potentiometer<br/>PA0-PA3"]
US["3x HC-SR04<br/>PB0-PB5"]
end
subgraph MCU["STM32F103C8T6 @ 72 MHz"]
direction TB
SCHED["main.c<br/>TIM3 10 ms tick<br/>÷10 → 100 ms"]
EV["ev_control.c<br/>speed · SOC · torque · range"]
ULT["ultrasonic.c<br/>TIM2 µs echo timing"]
ADAS["adas.c<br/>TTC · FCW · BSD · parking"]
FLT["fault.c<br/>flag latch · safe state"]
BUZ["buzzer.c<br/>TIM4 tone PWM"]
TLM["telemetry.c<br/>packet serialiser"]
SH["uart_shell.c<br/>ring buffer · parser"]
SCHED --> EV
SCHED --> ULT
ULT --> ADAS
EV --> ADAS
ADAS --> FLT
EV --> FLT
ADAS --> BUZ
EV --> TLM
ADAS --> TLM
FLT --> TLM
SH --> EV
SH --> ADAS
SH --> FLT
end
subgraph Actuation
PWM["Motor PWM<br/>PA8 · TIM1 · 20 kHz"]
LED["4x Status LED<br/>PB8-PB11"]
SPK["Buzzer<br/>PB6"]
end
HOST["Python Dashboard<br/>matplotlib + pyserial"]
POT --> SCHED
US --> ULT
EV --> PWM
ADAS --> LED
FLT --> LED
BUZ --> SPK
TLM -->|"USART1 DMA<br/>115200 8N1"| HOST
HOST -->|"shell commands"| SH
A single TIM3 interrupt at 100 Hz is the only periodic source. The main
loop drains owed ticks from a counter rather than a flag, so if a job
overruns the missed ticks are caught up and the physics integrator still
receives exactly one dt per 10 ms of real time.
| Job | Period | Work |
|---|---|---|
| 10 ms | 100 Hz | ADC sampling, EV model, motor PWM duty, buzzer sequencer, shell, UART pump, watchdog refresh |
| 100 ms | 10 Hz | Ultrasonic scan (all 3), ADAS evaluation, fault check, telemetry publish |
Nothing in the main loop blocks. The buzzer patterns, the alarm test
sequence and all UART transmission are tick-driven state machines rather
than HAL_Delay() calls.
firmware/ STM32 project (STM32CubeIDE-compatible)
Core/Inc module headers
Core/Src module sources
Drivers/ STM32F1 HAL + CMSIS
Makefile portable build — no IDE required
ev_dash.ioc CubeMX project file
dashboard/
dashboard.py live instrument cluster
test_protocol.py protocol round-trip tests
make_preview.py regenerates the README screenshots
docs/
PROJECT_REPORT.md full engineering report
SIMULATION_GUIDE.md run it in PICSimLab, no hardware
PROTOCOL.md UART wire format
TRACEABILITY.md requirement → code map, deviations, defects fixed
WIRING.md pin assignment and PICSimLab setup
TEST_PLAN.md manual test procedures
.github/workflows/
build.yml CI: builds both configs, runs protocol tests
| File | Responsibility |
|---|---|
main.c |
Peripheral init, scheduler, ISR callbacks |
ev_control.c/.h |
Speed, SOC, torque, power, range, drive modes, motor PWM |
adas.c/.h |
TTC, collision, blind-spot, parking assist, alarm priority |
ultrasonic.c/.h |
HC-SR04 trigger/echo timing, distance conversion |
fault.c/.h |
Fault evaluation, latching, safe-state enforcement |
buzzer.c/.h |
TIM4 tone synthesis, non-blocking beep patterns |
telemetry.c/.h |
Packet serialisation, CRC, DMA ring buffer |
uart_shell.c/.h |
RX ring buffer, command parser |
common.h |
Shared types, pin aliases, fault bitmask |
Requires arm-none-eabi-gcc and make.
cd firmware
makeProduces build/ev_adas.elf, .hex and .bin. If your toolchain is not on
PATH (for example the one bundled with STM32CubeIDE):
make GCC_PATH=/c/ST/STM32CubeIDE_1.19.0/STM32CubeIDE/plugins/com.st.stm32cube.ide.mcu.externaltools.gnu-tools-for-stm32.13.3.rel1.win32_1.0.0.202411081344/tools/binBuild for size with make DEBUG=0 (-Os). Current footprint — comfortable
headroom on 64 KB flash / 20 KB SRAM:
text data bss dec
debug -Og 43236 476 4340 48052
release -Os 40196 476 4332 45004
Both configurations build clean under -Wall -Wextra.
File → Import → Existing Projects into Workspace → select firmware/.
Build normally; the CubeMX .ioc is included for regenerating peripheral
init.
st-flash write firmware/build/ev_adas.bin 0x8000000or use ST-Link via CubeIDE / STM32CubeProgrammer.
pip install pyserial matplotlib numpy
python dashboard/dashboard.py --port COM3 # hardware or PICSimLab bridge
python dashboard/dashboard.py --demo # no hardware neededThe decoder handles both the binary packets and the legacy ASCII lines on the same link, so no configuration is needed.
Run the protocol tests:
python dashboard/test_protocol.py115200 8N1. Type help for the list.
mode <eco|normal|sport> drive mode / torque scale
speed set <kmh> inject vehicle speed
soc set <pct> override state of charge
temp set <degC> override motor temperature
obstacle <cm> inject front distance
obstacle clear release front sensor
blindspot <on|off> simulate side vehicles
sim clear release ALL injections
fault inject <motor|soc|col> force a fault condition
fault clear FAULT -> PARKED
stream <on|off> telemetry stream
format <bin|ascii> telemetry encoding
alarm test cycle buzzer P3 -> P1
status full system state
reset software reset via IWDG
Example — reproduce the collision-critical path:
speed set 72
obstacle 15
status
fault clear
The project targets PICSimLab with a Blue Pill board. See
docs/WIRING.md for the component list, pin connections
and bring-up order. A virtual COM bridge (VSPE or similar) exposes
PICSimLab's UART to the host so dashboard.py can attach.
- docs/PROJECT_REPORT.md — the full engineering report: architecture, design decisions, how the three contradictions in the specification were resolved, verification results, resource budget and limitations. Start here if you want the engineering story rather than the code.
- docs/SIMULATION_GUIDE.md — run the whole system with no hardware: PICSimLab setup, part-by-part wiring, the UART bridge, an 11-step demo sequence and a troubleshooting table.
- docs/TRACEABILITY.md — every requirement mapped to the code that implements it, the twelve places the implementation deliberately departs from the specification and why, and the twenty defects found and fixed.
- docs/PROTOCOL.md — UART frame format and packet layouts.
- docs/WIRING.md — pin map, timer allocation, PICSimLab and real-hardware notes.
- docs/TEST_PLAN.md — 18 manual test procedures covering all five use cases and the acceptance criteria.
The requirements document contains a small number of internal contradictions — most notably TIM1 being assigned both a 20 kHz PWM carrier and a 100 Hz interrupt, and PB0 being listed as both the buzzer and the front sensor TRIG. Each is resolved explicitly and recorded in TRACEABILITY.md rather than silently.
Embedded C · STM32 HAL · STM32CubeIDE · CubeMX · ARM Cortex-M3 · GPIO / ADC / Timers / PWM / UART / DMA / IWDG · HC-SR04 · PICSimLab · Python · matplotlib · pyserial

