[ ← BLOG ]
Jul 13, 2026/ Viraj

A Secure Software-Defined Vehicle ECU, Built in Under 24 Hours

What we set out to do

Software-defined vehicles live or die on one link: the connection between the high-level interface a driver touches and the low-level ECU that runs the car. Getting that link right usually means standing up hardware-enforced isolation, and on Arm parts that means TrustZone.

Anyone who has set up a TrustZone project from scratch knows the first couple of days are mostly reading. Reference manuals, partition headers, working out which peripheral belongs to the Secure world and which one you are allowed to hand down to the Non-Secure side. Get one register wrong and the board faults with no message attached. You are left staring at a dead serial port trying to guess which attribution you missed.

We wanted to see how much of that friction Hydron could take off the table. So we built a full-stack secure SDV demo on the STM32 NUCLEO-U385RG-Q: a web dashboard talking to a TrustZone-isolated ECU, with live two-way telemetry running entirely over the board's onboard ST-LINK virtual COM port. No external gateway, no extra wiring. Laptop to board and back.

The whole thing came together in under 24 hours inside VS Code, using Hydron's Plan, Code, and Debug agents alongside the Hydron Monitor serial panel. Here is where Hydron actually earned its place.

Phase 1: Laying out the secure and non-secure split

The starting point was the architecture. We needed the Secure application to boot first, bring up the primary clocks, and then hand execution down to the Non-Secure application, which would own the vehicle state machine, parse inbound command strings, and emit JSON telemetry.

We described that intent to Hydron in Plan mode and asked for a workspace configuration map. What came back was the isolation layout: control routed straight through to the Non-Secure runtime, the baseline JSON telemetry structures generated, and the state machine arrays scaffolded inside main.c. That is the part that normally eats a full day of cross-referencing the partition header against the reference manual, and it was the first thing off our plate.

Phase 2: Moving the whole UART path across the TrustZone boundary

This is the phase that would have hurt the most by hand.

The first build ran telemetry over USART3 on pins PB10 and PC11, wired out to an external gateway. Partway through we scrapped the gateway entirely and decided to run the demo laptop-direct over the ST-LINK VCP instead. That sounds like a small change. It is not. It means dragging every UART reference from USART3 to USART1 on PA9 and PA10, and doing it cleanly across the secure boundary, where a peripheral cannot simply be moved without also moving its interrupt targeting and its GPIO attribution.

We used Hydron's Enhance Prompt to tighten the refactor instructions first, then let the Code agent run the migration end to end. The concrete edits it made:

  • NonSecure/Core/Src/main.c: switched every huart3 handle to huart1 and remapped the interrupt callbacks to USART1_IRQn.
  • Secure/Core/Inc/partition_stm32u385xx.h: shifted the Non-Secure interrupt target value (NVIC_INIT_ITNS1_VAL) to 0x20000000 so USART1 priorities landed in the right world.
  • Secure/Core/Src/main.c: updated the Secure GPIO isolation tables to release PA9 and PA10 to the Non-Secure domain.
  • sdvdemo_nocan2.ioc: flipped NUCLEO-U385RG-Q.VCP=true and retargeted the pins to their USART1 alternate functions.

The thing worth calling out here is that the interrupt targeting and the GPIO attribution are exactly the two edits that get silently forgotten in a manual migration. Skip either one and you get a board that flashes fine and then says nothing. Hydron kept them consistent with the rest of the change instead of leaving them as a landmine for later.

Phase 3: The bug that was not in the firmware

With the firmware flashing and telemetry flowing, we brought up the host side: a small Flask app polling the serial port on a background thread. It died on startup:

PermissionError(13, 'Access is denied.') on COM10

The instinct on an embedded project is to assume the fault is on the board. It was not. We handed the stack trace to Hydron in Debug mode and asked why Windows was blocking the port. It traced the conflict to Flask's own reloader: running with debug=True spins up a second file-watcher thread that re-launches the script, and the two threads then fight over the same COM10 handle. One wins, the other throws the permission error.

The fix was one line:

if __name__ == "__main__":
    app.run(debug=False, port=5005, threaded=True)

That is a good example of where grounding matters. The error looked like a hardware or driver problem, and the real cause was a host-side threading race that had nothing to do with the ECU. Hydron read the actual trace and pointed at the right layer instead of sending us back into the firmware.

Phase 4: Watching it run, without leaving the IDE

The last stretch was verification, and we kept it inside VS Code. We launched the host gateway from the integrated terminal and used the Hydron Monitor panel to watch the serial link, so the whole loop sat next to the code that produced it.

From the dashboard we fired the vehicle commands one at a time: ENGINE ON, HEADLIGHT ON, MODE SPORT, ENGINE OFF, HEADLIGHT OFF, MODE ECO. Each one showed up as an HTTP POST in the terminal, went down the serial line as a command, and came back on the next telemetry refresh as a JSON packet with the updated state. Speed, drive mode, engine and headlight status, all reflecting live. No context-switching between three different tools to confirm a single round trip worked.

What this actually says about the workflow

None of the individual steps here are impossible by hand. Experienced firmware engineers do all of this. The point is the time it takes and where the time goes. Setting up the secure and non-secure split, moving a peripheral across the TrustZone boundary without dropping an interrupt target, and correctly blaming the host instead of the board for a port conflict are all things that usually cost hours of manual cross-referencing and dead-end debugging.

Hydron did not build the vehicle for us. It laid out the isolation structure, carried a peripheral migration across the secure boundary consistently, and read a stack trace closely enough to point at the right layer. We stayed in the loop, checked the edits, and ran it on real silicon. What we got at the end of it was a working, secure SDV prototype, built and verified in under a day, without the datasheet fatigue that normally comes with the territory.