Skip to main content
emobility.academy
M.TechEmbedded software architectureAdvanced About 10 weeksKit + add-ons

Hardware-in-the-Loop Test Bench with the Kit and a PC Plant Model

Students build a hardware-in-the-loop bench: a Python plant model of an EV's motor, vehicle and battery runs on the PC, exchanges signals with the kit's controller over UART and CAN in fixed time steps, and closed-loop tests with fault injection run automatically against the real firmware.

Uses: UART · CAN · PWM

The problem

Testing EV control firmware on a real vehicle is slow, costly and risky, especially for faults. Hardware-in-the-loop testing runs the real controller against a simulated plant, so tests are repeatable and faults can be injected safely. The bench's timing and fidelity limit what it can prove.

Objectives

  • Build a discrete-time plant model of a DC motor, vehicle longitudinal dynamics and a battery.
  • Connect it to the STM32 controller with a deterministic exchange of sensor and actuator signals.
  • Measure the controller's real PWM output and feed it back into the plant.
  • Automate test cases, including fault injection, with pass and fail criteria.
  • Compare HIL results with a model-in-the-loop baseline and quantify timing jitter.

What you need

From the EV Embedded Development Kit

  • STM32F103C8T6 main controller
  • Serial UART interface
  • CAN bus interface with screw terminal
  • Motor driver for PWM motor-control experiments
  • Analog potentiometer input
  • 16×2 LCD
  • RGB LEDs

Software

  • STM32CubeIDE with HAL drivers
  • Embedded C
  • Python 3 with NumPy, SciPy, pyserial, python-can and pytest
  • MATLAB/Simulink (optional) for the model-in-the-loop baseline

Extra parts

  • USB-CAN adapter — The board has one CAN node; the PC uses the adapter to act as the other vehicle ECUs (BMS, dashboard) on the bus.
  • Jumper wire — To loop the PWM output pin back to a spare timer input-capture pin so the controller's actual output is measured rather than trusted from software.
  • Small brushed DC motor within the motor driver's rating (optional) — For a final check of the controller against real hardware after HIL tests.

Key concepts

Hardware-in-the-loop (HIL)
The real controller runs its production firmware while a simulator provides its inputs and responds to its outputs as the vehicle would.
Model-in-the-loop (MIL)
Both controller and plant run as models on the PC. Comparing MIL and HIL shows effects of fixed-point maths, timing and real I/O.
Fixed-step co-simulation
Plant and controller exchange data every fixed step (for example 10 ms). Late or jittery exchanges distort the result, so timing must be measured.
Input capture
A timer mode that timestamps signal edges; with the PWM looped back, it measures the duty cycle the hardware actually produced.
Fault injection
Deliberately corrupting signals, such as a stuck throttle sensor or a missing BMS CAN frame, to check the controller's fault handling.

Step-by-step guide

  1. 1

    Define the system under test and test goals

    The controller under test is a speed or torque controller on the main STM32: it reads a driver demand (the potentiometer or a value from the plant), receives battery limits on CAN, computes PWM for the motor driver and reports status. Write requirements as testable statements: reaches set speed within a time limit, respects the battery current limit, cuts output within 50 ms of a BMS fault frame, handles a stuck throttle. Define pass and fail criteria for each.

    Checkpoint: A requirements and test-case table with numeric pass criteria.

  2. 2

    Build the plant model

    In Python write a discrete-time model: a DC motor (armature resistance, inductance, back-EMF and torque constants), a gearbox and vehicle longitudinal dynamics (mass, rolling resistance, aerodynamic drag, road gradient), and a battery as an open-circuit voltage with internal resistance. Integrate with a fixed step of 1 ms inside each 10 ms exchange step. Parameters are illustrative for a small EV and should be stated in the report.

    Checkpoint: Open-loop runs give plausible speed and current responses to step changes in duty cycle.

  3. 3

    Run a model-in-the-loop baseline

    Write the controller algorithm (for example a PI speed loop with anti-windup and current limiting) in Python and run it against the plant. Tune it here and record responses for every test case. This is the reference against which HIL results are compared. Include the same 10 ms controller step, saturation and rate limits that the firmware will use, so the baseline is a fair comparison. Store MIL outputs in the same format as HIL logs so the comparison scripts can read both.

    Checkpoint: MIL results for all test cases saved, with the controller meeting requirements.

  4. 4

    Configure the controller hardware

    In CubeMX set the main STM32 to 72 MHz, configure a timer channel for PWM to the motor driver (for example 20 kHz), a second timer in input-capture mode on a spare pin connected by jumper wire to the PWM pin, a USART at 921600 or 460800 baud for the plant link (check the reliable rate for your USB-serial path), and CAN at 500 kbit/s. Set the DIP switches for the motor driver, CAN and UART blocks. Keep the motor disconnected during HIL so the model is the only load.

    Checkpoint: The input capture measures the generated PWM duty within one timer count across the range.

  5. 5

    Implement the controller firmware

    Port the PI controller to C in fixed-point (Q15) since there is no FPU. Run it in a 10 ms timer interrupt or RTOS task. Its inputs are the simulated speed sensor and driver demand from the plant frame, and battery limits from CAN; its output is PWM duty. Report status on the LCD and RGB LEDs. Measure the controller's execution time with the DWT cycle counter.

    Checkpoint: Fed with recorded MIL inputs, the firmware's duty outputs match the Python controller within a stated tolerance.

  6. 6

    Design the exchange protocol

    Each 10 ms step the PC sends a frame with step number, simulated speed and driver demand, and the STM32 replies with step number, commanded duty, measured duty from input capture and status. Use binary frames with CRC. The PC waits for the reply before integrating the next step, making the loop deterministic; log any late reply. Timestamp both sides to measure round-trip time.

    Checkpoint: A 10-minute run shows no CRC errors, no missing steps and a round-trip histogram well inside 10 ms.

  7. 7

    Emulate other ECUs on CAN

    Using python-can and the USB-CAN adapter, send BMS frames every 100 ms with allowed discharge current and fault bits computed from the plant's battery state, and receive the controller's status frames. This makes the bench test CAN behaviour as well as control. Add a heartbeat counter to the BMS frame and make the firmware treat a stalled counter as a lost BMS. Log every CAN frame sent and received with timestamps so test reports can show the exact sequence of events around each fault.

    Checkpoint: Changing the allowed current in the BMS frame visibly limits the controller's output in the plant response.

  8. 8

    Automate test cases with pytest

    Write each test case as a pytest function: set the scenario (speed profile, gradient, battery state), run the closed loop for a fixed time, then assert the requirement (rise time, overshoot, current limit, reaction time). Save logs and plots for every run so failures can be inspected. Reset the controller at the start of each test through a UART command or the reset line so tests do not affect each other.

    Checkpoint: The full suite runs unattended and produces a pass or fail summary and plots.

  9. 9

    Add fault injection

    Add tests that inject faults: a stuck or out-of-range throttle signal, a frozen speed sensor value, a missing BMS heartbeat frame, a BMS fault bit, and a corrupted UART frame. Assert that the controller detects each and moves to its defined safe state within the required time. Inject faults at different operating points, such as standstill, acceleration and steady speed, and repeat each test several times. Record detection time as the step at which the controller reports the fault and cuts output.

    Checkpoint: Each fault test passes, or a failure exposes a firmware bug that is then fixed and retested.

  10. 10

    Compare HIL with MIL

    Overlay HIL and MIL responses for each test case. Quantify differences in rise time, overshoot and steady-state error, and explain them: fixed-point quantisation, one-step delay from the exchange, PWM resolution. Report exchange timing jitter and its effect. Then add the measured one-step delay and fixed-point rounding to the MIL model and rerun it. If the modified MIL matches HIL closely, you have shown which effects explain the difference, which is a strong result for the report.

    Checkpoint: A comparison table and overlaid plots with each difference explained.

  11. 11

    Validate on real hardware and document

    Optionally connect a small brushed DC motor to the driver and run a simple open-loop check to confirm the PWM output drives a real load as expected (the motor has no speed feedback unless an encoder is added). Write up the bench architecture, model, protocol, test suite, results and limitations. Keep the motor test short and within the driver's ratings.

    Checkpoint: A report with the architecture diagram, test results table, MIL–HIL comparison and limitations.

Testing & validation

  • Input capture confirms the hardware PWM duty matches the commanded duty.
  • Exchange timing histogram shows no step overruns in long runs.
  • Every requirement has an automated test with a recorded pass or fail.
  • MIL and HIL differences are quantified and explained.

Expected results

A working HIL bench where real STM32 firmware is tested in closed loop against a PC plant model and emulated CAN nodes, with an automated, fault-injecting test suite and a quantified comparison with model-in-the-loop results.

Research angle for your M.Tech thesis

Research questions: how do exchange delay, jitter and fixed-point quantisation make HIL results differ from MIL, and how low-cost can a HIL bench be while still finding the faults that matter? Metrics: response differences (rise time, overshoot, steady-state error), round-trip jitter, fault detection coverage and test run time. Baseline: model-in-the-loop with the same controller and plant. A strong thesis would quantify bench fidelity limits and show seeded firmware faults caught by HIL but missed by MIL.

Take it further

  • Move the plant model to a real-time process or a second microcontroller for tighter timing.
  • Add an encoder-equipped motor and compare HIL predictions with measured speed.
  • Use the bench for regression testing of every firmware commit.

Viva and review questions

Why loop the PWM back into input capture?

So the plant uses the duty the hardware actually produced, which catches timer configuration errors that a software-reported value would hide.

Why does HIL differ from MIL?

Fixed-point maths, PWM resolution and the extra delay of the serial exchange change the response slightly; the comparison quantifies this.

How do you keep the loop deterministic?

The PC advances the model only after receiving the controller's reply for each step, and logs any late reply.

Why emulate the BMS on CAN?

It lets the bench test how the controller reacts to battery limits and faults without needing a real BMS node.

Related EV projects

All 100 EV projects