Build detail · every stage, in order

Every stage of the build, in the order it happened

A supervisory controls stack for a microcontroller-based concept car, taken end-to-end through the software V-model — requirements, architecture, model-in-the-loop, processor-in-the-loop, run-on-board — to show what a formal embedded software process actually costs and produces.

Arduino Nano 33 IoT C — Embedded Coder Model-Based Design MATLAB / Simulink Stateflow PlantUML / SysML V-Model PID control I²C
Scope solo, requirements → hardware Feedback wheel encoders only Target Arduino Nano 33 IoT · SAMD21

01 — Premise

Automotive software is safety-critical and under-taught

Mechanical engineers moving into software-defined vehicles inherit a discipline nobody handed them a process for. This project treats that gap as the actual problem: not "can a rover follow a line," but "what does the full formal path from requirement to flashed binary look like, and where does it break?"

The deliverable is a concept car — an Arduino Nano 33 IoT on a MotorCarrier board, driving DC motors with wheel-speed encoder feedback through a PID controller — developed in both virtual and physical form and verified against written acceptance criteria at every stage.

The engineering claim is the process, not the vehicle. Every artifact a production automotive team would be asked for was produced: a requirements specification with traceable IDs, a layered architecture decomposition, UML block and class diagrams, a plant model derived from kinematics, three distinct test stages, and a verification table closing each requirement.

CAD render of the Arduino Nano 33 IoT mounted on the MotorCarrier board
Fig 47 — Nano 33 IoT and MotorCarrier
Physical run-on-board path following test setup with taped track
Fig 46 — Run-on-board test setup

02 — Process

The V-model, executed by hand

Each design activity on the left arm was bound to a verification activity on the right arm before implementation started. I mapped a specific tool to every box — this figure is the reason the project is worth rerunning, because each right-hand box is a job that a CI system could run and nobody was running.

V-model diagram annotated with the tool used at each stage: Excel for requirements and acceptance testing, Visio and PlantUML for system design, MATLAB and Simulink for implementation and testing, Simulink Data Inspector for system testing
The V — each right-arm activity verifies the left-arm one at the same height. Tooling per stage: Excel for requirements, Visio and PlantUML for design, MATLAB/Simulink for implementation and test, Data Inspector for signal comparison. Redrawn.

Design — descending

REQRequirements engineering — classified, ID-tagged, constraint-bound
SYSSystem design — plant equations, kinematics, variable set
ARCHSoftware architecture — layered decomposition, UML
FUNCFunctional design — motor curve, controller structure
IMPLImplementation — Simulink models, generated C

Test — ascending

MILModel-in-the-Loop — plant and controller in simulation
PILProcessor-in-the-Loop — generated C on target, host-tethered
ROBRun-on-Board — untethered, physical track
VEREvaluation — per-stage verification and validation
VERVerification table — requirements closed or not

03 — Requirements

Traceable IDs, constraints written down

Requirements were classified functional or non-functional, tagged SR for "software requirement" so a mixed mechanical/software artifact stays filterable, and layered as system, component, or software level. Constraints — including licensing and toolchain limits — were recorded alongside the requirement rather than discovered later.

IDRequirementAcceptance criterionLevel
SR-F-01System modelling in MATLAB / SimulinkCreate and simulate dynamic models spanning ≥10 mechanical and electrical component typesSystem
SR-F-02PID control for motor positionHold motor position within ±1°Component
SR-F-03Simulink ↔ Arduino support package compatibilityLocate Arduino block library and compile for targetSystem
SR-F-04Follow a prescribed path in simulationDesired versus simulated concept-car behaviour match within ±1%Software
SR-F-05Text-based programming in MATLABSupport ≥100 custom functions and scriptsSystem
SR-F-06Visual programming in Simulink≥50 pre-built blocks plus custom block creationSystem
SR-F-07Apply signal filtering to sensor inputHandle noisy encoder feedback within the simulation host's capabilitySoftware

Extract of the full specification (Table 7). The complete table, including non-functional requirements and per-row constraints, ships in the repository as the source spreadsheet.

A requirement discovered the hard way

A MATLAB release update moved a folder on the critical path and silently broke the link to the Arduino support library. Several days of debugging with MathWorks support produced a requirement that had never been written down: the Arduino library and the Simulink library must remain compatible for the duration of the project. Toolchain version pinning is not an implementation detail — it is a requirement, and it surfaced only because something broke.

04 — Design

Decomposition, kinematics, architecture

The design was decomposed iteratively — an initial split, then a layered redecomposition — before any model was drawn. Differential-drive kinematics were derived into a plant model with a documented variable set, and a DC gear motor was characterised experimentally to give the controller a real response curve to work against.

Concept car kinematics diagram showing wheel geometry and body frame
Differential drive kinematics, with the governing relations. Redrawn.
Motor control system flow chart
Fig 16 — Motor control system flow

Motor characterisation

Rather than assume a transfer function, the motor was driven across its PWM range and its response recorded, producing the curve the controller was later tuned against. The characterisation scripts and captured response data are in the repository.

DC motor characterization curve produced by running the Simulink characterisation model
Measured DC motor steady-state response. Replotted from the original motorResponse data, not traced.

Where the software boundary sits

The controller does not talk to the motors directly. The Nano is I²C master to a co-processor on the MotorCarrier board that runs the motor loop, so part of the control problem is deciding what belongs above that bus and what belongs below it. Everything above it is the work described here; everything below it is a vendor device with a fixed interface.

I2C master-slave relationship between the Nano 33 IoT and the MotorCarrier co-processor
I²C topology. The Nano is master; the motor carrier co-processor closes the innermost motor loop. Redrawn for this hardware.

Below the bus the interface is fixed by someone else, which makes reading it part of the design work rather than an afterthought. The dependency was documented as a class diagram before any control model was built, so the constraints it imposes — fixed-point gain types, a control loop running on the co-processor's own schedule, encoder counts rather than angles — were known while the architecture was still movable.

UML class diagram documenting the Arduino library that provides the concept-car control interface
Class structure of the vendor control interface — my documentation of the dependency boundary, drawn to fix the contract before designing above it. The library itself is Arduino AG, LGPL-2.1, and is not reproduced here. Redrawn.

UML and SysML

Interfaces were captured as UML rather than described in prose, and the diagrams were written as PlantUML source so they live in version control as text instead of as a binary drawing. Three of the diagrams and their source are reproduced below.

The model-in-the-loop pipeline as three stereotyped rectangles — signal creation, kinematics, visualisation. Deliberately coarse: this is the diagram that fixes the boundaries before any block is placed.

docs/uml/mil-dataflow.pumlPlantUML
@startuml
skinparam rectangle {
   BackgroundColor<<Input>> White
   BackgroundColor<<Calculation>> White
   BackgroundColor<<Visualization>> White
   Dimension 300x200
}

rectangle "Create velocity and angular velocity signals" <<Input>> as input
rectangle "Vehicle Kinematics" <<Calculation>> as calculation
rectangle "Visualization of the simulated path" <<Visualization>> as visualization

input -right-> calculation
calculation -right-> visualization
@enduml

05 — Implementation

Three stages, increasing reality

Each stage keeps the same controller and swaps out how much of the world is real. Simulation first, then real generated code on the real chip with a host watching, then the tether cut.

Stage 1

Everything is simulated

Plant model and controller run entirely in simulation. Commanded linear and angular velocity are generated from a timing chart, pushed through differential kinematics, and the resulting path is plotted against the intended track. Nothing here touches hardware, so an error is a modelling error and nothing else — which is the whole point of doing it first.

Simulink model implementing differential kinematics of the vehicle
Fig 22 — Differential kinematics in Simulink
Inside the vehicle simulation subsystem
Fig 26 — Inside the vehicle simulation subsystem
Plot of the simulated concept car course
Fig 30 — Simulated course. Commanded track versus modelled vehicle path.
Model-in-the-loop concept car path result
Simulated path — the model closes the square exactly. Replotted from the original.
PASS — the virtual concept car followed the commanded track inside the ±1% acceptance criterion of SR-F-04.

06 — Results

The honest number

Three run-on-board trials, same track, same starting position. Leaving the marked boundary is a failure. Completing the course in bounds would be 100%; the figures below are how far the car got before going out of bounds. Between trials, PID constants and the linear and angular speed targets were adjusted and the firmware redeployed.

TrialResultTrack completed
1Fail — left path30%
2Fail — left path50%
3Fail — left path70%
Chart of experimental run counts across the project
Fig 45 — Experimental run count across the project

Monotone improvement across three redeployments, and no fourth trial. The tuning loop was manual: change constants, rebuild, reflash, walk the track, watch with human eyes, decide pass or fail. Each iteration cost enough that three was the budget.

Why this is the interesting result

The controller was not wrong. The tuning process was the bottleneck. Six independent PID variables across two controllers, explored by hand, with a binary pass/fail judged by a person standing over a track — 59 tethered trials to build it, and only three untethered data points to validate it. The project reaches this conclusion on its own and says so in the Outlook.

The experiment could be enhanced by writing a script to automate the PID tuning process … having data from more trials, and numerical data on the degree of path deviation rather than just a pass/fail decision, would produce more information for tuning and optimizing the controller.

— my own writeup, at the time

That sentence is the brief for the second half of this project. The same build, run as a team pipeline →

07 — Provenance

What is mine, and what is only referenced

This project sits on top of vendor hardware libraries and a commercial modelling toolchain. Nothing borrowed is reproduced here — dependencies are named, not vendored, and the boundary is drawn explicitly so there is no ambiguity about authorship.

ArtifactOriginIn the repository
Simulink models — MiL, PiL, run-on-board, control panelWritten for this projectYes
Stateflow chart — track path state logicWritten for this projectYes
Requirements specification and verification tableWritten for this projectYes
UML / SysML sources (PlantUML), architecture diagramsWritten for this projectYes
Motor characterisation scripts and captured response dataWritten for this projectYes
Figures, timing charts, test procedure, recorded trial resultsProduced for this projectYes
Generated C and target configuration headersEmbedded Coder output from the models aboveYes — labelled as generated
UML class diagram of the vendor control interfaceDrawn for this project to document a dependencyYes — diagram only, not the library
Arduino MotorCarrier and device librariesArduino AG, LGPL-2.1No — named as a dependency
MATLAB, Simulink, Stateflow, Embedded Coder, Arduino support packageMathWorks, commercial licenceNo — named as a dependency
Arduino Engineering Kit course material and datasheetsArduino AG / component vendorsNo — not redistributable
How the line was drawn

Not by memory. Every candidate file was hashed with SHA-256 and compared against the stock Arduino Engineering Kit Rev 2 distribution; a file whose hash appears in the kit is stock, wherever it happens to sit in my folders. Eight files came back stock and were excluded — including one that reads as mine by filename, distanceClosedloop_hw.slx.r2020b, which turned out to be byte-identical to the kit's Exercise 3 model.

The dates then separate cleanly, and they describe what this project actually was: stock files date to December 2020 – June 2021; everything I built dates to December 2024 – January 2025, when the design was migrated onto a 2025 toolchain and block behaviour, support-package layout and code generation had all moved underneath it. The vendor libraries themselves were used exactly as supplied. What was rewritten is the control layer above them.

One artifact that did not survive

The generated C for the two run-on-board models no longer exists. Those build directories still hold the object files, the makefile, and the linked .elf and .bin — but the .c and .h were cleaned up after the build. The firmware that actually drove the car can still be executed and can no longer be read.

Nothing was lost through carelessness; that is simply what a local build directory does. It is also precisely the failure a pipeline removes, by archiving generated sources against the commit that produced them.