This project implements a lightweight HIL testing framework for the TI MSPM0G3507 LaunchPad. It demonstrates automated hardware validation by pairing a firmware test agent (running on the MCU) with a Python-based host controller.
As of v1.1, the firmware adds a Sensor Monitor mode that samples an external potentiometer, drives a 3-LED status indicator (breadboarded with current-limiting resistors), and streams telemetry over the same UART β a small demo of closed-loop sensor processing on top of the same framework.
Photo above shows the v1.0 HIL loopback setup (PB2 β PB3 jumper only). Updated photos covering the v1.1 Sensor Monitor breadboard (pot + 3 LEDs) are pending β tracked in Future Enhancements.
Key Features:
- Bare-Metal Firmware: Custom UART command parser written in C, direct TI DriverLib (no SysConfig).
- Automated Testing: Python
pytestsuite usingpyserialto drive inputs and verify outputs. - Hardware Loopback: Physically verifies GPIO logic levels using a closed-loop wiring setup.
- Sensor Monitor Extension (v1.1): ADC-driven threshold detection with LED indicators and UART telemetry; HIL commands remain available while in Sensor Monitor mode.
- Test Artifacts: JUnit XML reports and timestamped logs for CI/CD integration.
HIL loopback (v1.0):
- Board: TI LP-MSPM0G3507
- USB Cable: Micro-USB (included with LaunchPad)
- Jumper wire: 1Γ female-to-female, for the PB2 β PB3 loopback
Sensor Monitor (v1.1), additionally:
- Potentiometer: 1Γ 10 kΞ© linear
- LEDs: 1Γ green, 1Γ yellow, 1Γ red (standard 3 mm or 5 mm indicator LEDs, ~2.0β2.2 V Vf)
- Resistors: 3Γ 270 Ξ© (current-limiting; one per LED)
- Breadboard: half-size (~400 tie points, β₯30 columns) or larger
- Jumper wires: ~10 female-to-female (one female end plugs directly onto the LaunchPad's male header pins; the other female end is converted to a pin for the breadboard via a short male-male pin-header adapter) and ~13 male-to-male (breadboard-to-breadboard hops)
HIL loopback (v1.0):
LP-MSPM0G3507 (Header J1)
βββββββββββββββββββββββββββ
β β
β Pin 9 (PB2) βββββ β
β β β <- Jumper Wire
β Pin 10 (PB3) <ββββ β
β β
βββββββββββββββββββββββββββ
PB2 = Stimulus Output (firmware drives HIGH/LOW)
PB3 = Measurement Input (firmware reads state)
Sensor Monitor (v1.1):
Voltage divider β potentiometer to ADC:
J1.1 (3V3) ββββ
β
[ 10 kΞ© pot ]ββ wiper βββΆ J1.2 (PA25, ADC0.2)
β
J3.22 (GND) ββββ
LED chains β one per color, cathodes tied to the breadboard GND rail
(which is strapped to J3.22):
J1.4 (PA8) ββ 270 Ξ© βββΆ|ββ GND Green ("NORMAL")
J1.6 (PB24) ββ 270 Ξ© βββΆ|ββ GND Yellow ("WARNING")
J1.7 (PB9) ββ 270 Ξ© βββΆ|ββ GND Red ("ALERT")
Legend: βΆ| = LED (anode left, cathode right)
Active-high: GPIO HIGH β LED on.
HIL loopback (v1.0):
| Pin | Function | Direction | Config |
|---|---|---|---|
| PB2 (J1.9) | Stimulus | Output | Push-pull |
| PB3 (J1.10) | Measurement | Input | Pull-down* enabled |
*If the loopback wire is disconnected, the input reads a deterministic LOW (0) instead of floating garbage. This enables fault detection.
Sensor Monitor extension (v1.1):
| Pin | Header | Function | Direction | Config |
|---|---|---|---|---|
| PA25 | J1.2 | Potentiometer wiper | Analog input | ADC0 channel 2, VDDA reference |
| PA8 | J1.4 | Green LED ("NORMAL") | Output | Push-pull, active high (270 Ξ© β GND) |
| PB24 | J1.6 | Yellow LED ("WARNING") | Output | Push-pull, active high (270 Ξ© β GND) |
| PB9 | J1.7 | Red LED ("ALERT") | Output | Push-pull, active high (270 Ξ© β GND) |
| 3V3 | J1.1 | Pot supply | β | Powers the voltage divider |
| GND | J3.22 | Common ground | β | LED cathode returns + pot low end |
The potentiometer forms a voltage divider between the 3.3 V rail and GND; its wiper returns the analog sense voltage to PA25. Each LED is driven directly by its GPIO through a 270 Ξ© current-limiting resistor to the common GND rail (active-high: GPIO HIGH turns the LED on).
UART Settings: 115200 baud, 8 data bits, no parity, 1 stop bit (8N1)
- Input: Single character (newline optional;
\rand\nare ignored) - Output:
OK <payload>\non success,E <code>\non failure
| Cmd | Description | Success Response |
|---|---|---|
? |
Get firmware identity | OK MSPM0_HIL_v1.1 |
H |
Set PB2 HIGH (3.3V) | OK |
L |
Set PB2 LOW (0V) | OK |
R |
Read PB3 state | OK 0 or OK 1 |
S |
Get status | OK <uptime_ms> <cmd_count> |
M |
Toggle Sensor Monitor mode | OK SENSOR or OK HIL |
Unknown characters produce E BAD_CMD\n. HIL commands work in both HIL and Sensor Monitor modes.
Sensor Monitor turns the LaunchPad into a simple closed-loop sensor demo. Enter by sending M; send M again to return to HIL mode.
What it does
- Samples the external potentiometer (PA25, ADC0 ch2) every 200 ms.
- Maps the 12-bit reading into one of three bands and drives the matching LED:
| Band | Raw range | LED | Status label |
|---|---|---|---|
| Normal | 0 β 1364 |
Green (PA8) | NORMAL |
| Warning | 1365 β 2729 |
Yellow (PB24) | WARNING |
| Alert | 2730 β 4095 |
Red (PB9) | ALERT |
- Emits one telemetry line per sample over UART:
[<uptime_ms>ms] ADC: <raw_0_4095> | Status: NORMAL|WARNING|ALERT
Example transcript:
OK SENSOR
[12345ms] ADC: 812 | Status: NORMAL
[12545ms] ADC: 2011 | Status: WARNING
[12745ms] ADC: 3550 | Status: ALERT
HIL commands (H/L/R/S/?) still work while Sensor Monitor is active β they interleave with telemetry lines because the main loop polls UART non-blockingly between samples.
- Super-loop + two ISRs. The main loop runs two cooperative "tasks": a non-blocking UART poll and a periodic ADC/LED/telemetry step. TIMG0 provides a 1 ms tick (
g_uptime_ms) for scheduling; the UART RX ISR latches incoming bytes for the main loop to consume. - Why the firmware bypasses the TI Drivers UART. The SDK's
UART_readTimeout(..., 0)did not appear to return immediately on MSPM0 in this configuration β it looked like it was pending on aWAIT_FOREVERsemaphore insideUART_readBufferedMode, which breaks the super-loop pattern. Callback mode would likely fix that but requires DMA, which is disabled inti_drivers_config.c.uart_io.ctherefore talks to the UART directly via DriverLib (DL_UART_*) with its own interrupt handler and a single-byte RX slot β tiny, deterministic, non-blocking.Caveat: This diagnosis is based on reading the SDK source and symptom matching, not a deep instrumented trace, and the workaround was chosen under time pressure for the demo. Before building more on top of it, the root cause should be re-verified (e.g., step through
UART_readBufferedModewith the debugger to confirm theWAIT_FOREVERpath is actually entered) and the alternatives (enable DMA + callback mode, tweak the driver config, or open a TI E2E thread) re-evaluated.
Prerequisites: (other versions may be suitable as well, however these are the only tested versions)
- Code Composer Studio CCS Theia 20.04.0
- MSPM0 SDK 2.09.00.01
Steps:
- Open CCS and import the project from
firmware/ - Connect the LaunchPad via USB
- Build the project
- Flash
- Open a serial terminal to verify: you should see
MSPM0_HIL_v1.1: Ready (H/L/R/S/?, M=mode toggle)
Build/flash without CCS (optional):
- This repo does not include a standalone makefile; the supported build flow is CCS/Theia import.
Prerequisites:
- Python 3.10+
- Virtual environment (recommended)
Steps (in PowerShell 7.0+):
cd tests
python -m venv venv
venv\Scripts\Activate.ps1
pip install -r requirements.txtIf PowerShell blocks
Activate.ps1with an execution-policy error, run it once in your shell:Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned.
Run all tests:
New-Item -ItemType Directory -Force -Path results | Out-Null
pytest test_hil_loopback.py --port COM7 --junitxml=results/test_results.xml(Replace COM7 with your actual serial port)
Find your port:
- Windows: Device Manager β Ports β "XDS110 Class Application/User UART"
| Test | Description | Pass Criteria |
|---|---|---|
test_identity_returns_correct_version |
Verify firmware responds | Response = MSPM0_HIL_v1.1 |
test_set_high_read_high |
Loopback HIGH | H β R returns 1 |
test_set_low_read_low |
Loopback LOW | L β R returns 0 |
test_toggle_sequence |
Rapid toggle | H-L-H-L sequence reads 1-0-1-0 |
test_status_returns_uptime_and_count |
Status valid | uptime > 0, count β₯ 1 |
test_uptime_increases |
Timer running | uptime increases over 500ms |
test_invalid_command_returns_error |
Error handling | X returns E BAD_CMD |
test_device_responds_after_error |
Recovery | Valid command works after error |
| Symptom | Likely Cause | Fix |
|---|---|---|
TimeoutError: No response |
Wrong COM port | Check Device Manager, update --port |
TimeoutError: No response |
CCS terminal still open | Close CCS serial console |
test_set_high_read_high fails with OK 0 |
Loopback wire disconnected | Connect PB2 β PB3 |
test_set_low_read_low fails with OK 1 |
Wrong pins wired | Verify J1.9 β J1.10 |
| All tests fail | Firmware not flashed | Rebuild and flash firmware |
E BAD_CMD on valid command |
Baud rate mismatch | Ensure 115200 baud |
pytest shows UNEXPECTED: [<ms>] ADC: ... lines |
Firmware is still in Sensor Monitor mode from a prior interactive session | Press the LaunchPad reset button before running pytest (tracked as a known issue in Future Enhancements) |
| No LEDs light in Sensor Monitor mode | LED polarity reversed or 270 Ξ© resistor missing / miswired | Verify: GPIO pin β 270 Ξ© β LED anode β LED cathode β GND rail |
ADC stuck at 0 or 4095 regardless of pot position |
Pot 3V3 / GND ends swapped, or wiper not connected to PA25 |
Verify J1.1 (3V3) on one outer terminal, J3.22 (GND) on the other, wiper β J1.2 |
If tests fail, verify hardware manually:
- Open a serial terminal (PuTTY, CCS Terminal, etc.)
- Connect at 115200 baud
- Type
?β should seeOK MSPM0_HIL_v1.1 - Type
Hβ should seeOK - Type
Rβ should seeOK 1(with wire connected) - Type
Mβ should seeOK SENSOR, followed by a telemetry line every 200 ms (see Sensor Monitor Mode). TypeMagain to return to HIL mode.
$ pytest test_hil_loopback.py --port COM7
========================= test session starts =========================
collected 9 items / 1 deselected / 8 selected
test_hil_loopback.py::TestIdentity::test_identity_returns_correct_version PASSED
test_hil_loopback.py::TestGPIOLoopback::test_set_high_read_high PASSED
test_hil_loopback.py::TestGPIOLoopback::test_set_low_read_low PASSED
test_hil_loopback.py::TestGPIOLoopback::test_toggle_sequence PASSED
test_hil_loopback.py::TestStatus::test_status_returns_uptime_and_count PASSED
test_hil_loopback.py::TestStatus::test_uptime_increases PASSED
test_hil_loopback.py::TestErrorHandling::test_invalid_command_returns_error PASSED
test_hil_loopback.py::TestErrorHandling::test_device_responds_after_error PASSED
========================= 8 passed, 1 deselected in 1.44s =========================
2026-01-27 14:41:54,264 | INFO | Logging to: logs\hil_test_20260127_144154.log
2026-01-27 14:41:54,264 | INFO | Connecting to COM7...
2026-01-27 14:41:54,365 | INFO | Connected successfully
2026-01-27 14:41:54,366 | INFO | TEST: Identity check
2026-01-27 14:41:54,368 | INFO | Response: MSPM0_HIL_v1.1
2026-01-27 14:41:54,370 | INFO | Disconnecting...
2026-01-27 14:41:54,371 | INFO | Connecting to COM7...
2026-01-27 14:41:54,474 | INFO | Connected successfully
2026-01-27 14:41:54,474 | INFO | TEST: Set HIGH, read HIGH
2026-01-27 14:41:54,484 | INFO | H command: OK
2026-01-27 14:41:54,485 | INFO | R command: 1
2026-01-27 14:41:54,486 | INFO | Disconnecting...
See tests/sample_output/ for complete example files.
mspm0_hil_validation/
βββ firmware/ # CCS Theia project (bare-metal, no SysConfig)
β βββ hil_firmware.c # Main: command parser, mode toggle, super-loop
β βββ uart_io.c / uart_io.h # Non-blocking UART I/O (direct DriverLib; bypasses TI Drivers UART)
β βββ adc.c / adc.h # ADC driver (potentiometer on PA25, ADC0 ch2)
β βββ led.c / led.h # 3-LED indicator driver (green/yellow/red)
β βββ ti_drivers_config.c / .h # TI Drivers config (v1.0 scaffolding; UART handler intentionally dropped β see note in .c)
β βββ mspm0g3507.cmd # Linker command file
β βββ startup_mspm0g350x_ticlang.c # Cortex-M reset vector + startup
β βββ targetConfigs/ # CCS debugger target configuration
βββ tests/ # Python test suite
β βββ conftest.py # Pytest fixtures (serial port, logging)
β βββ hil_client.py # Serial client wrapping the command protocol
β βββ test_hil_loopback.py # Automated HIL loopback tests
β βββ test_client_manual.py # Manual/exploratory client for interactive smoke-testing
β βββ pytest.ini # Pytest configuration
β βββ requirements.txt # Python dependencies
β βββ sample_output/ # Example JUnit XML + log from a real run
βββ LICENSE
βββ README.md
-
Sresponse should include the current mode (HILorSENSOR) - Add photos of the v1.1 Sensor Monitor breadboard (pot voltage divider + 3 LED chains) alongside the existing v1.0 loopback photo
- Add test cases for the 1.1 changes
- Soak testing (12-hour continuous run)
- Fault injection (firmware-simulated delays/drops)
- Power cycle testing (USB relay integration)
- IΒ²C sensor validation
- Logic analyzer trace capture
- Auto-return to HIL mode on pytest connect.
g_modepersists across serial disconnects, so if the firmware is left in Sensor Monitor mode from a prior interactive session, the 200 ms telemetry lines can race ahead ofOK/Eresponses in the serial buffer and cause intermittentUNEXPECTED:parse failures. Proposed fix intests/hil_client.py: onconnect(), query mode (e.g., add a dedicated query command, or inspect the response to a probeMand toggle back if needed) so the test session always starts in a known HIL state. Until then, press the board's reset button before running pytest if you've been usingMinteractively.
MIT License - See LICENSE file for details.
