HDR Lab, University of Michigan
Controls an SMA (Shape Memory Alloy) micro-actuator while simultaneously measuring force, displacement, voltage, current, and impedance. The Arduino Portenta H7 runs the actuator and streams sensor data over UDP at up to 500 Hz. A PC orchestrates everything and also triggers LCR impedance measurements via USB/GPIB.
┌──────────────────────┐
│ PC (Python/Jupyter) │
│ │
│ Serial ──── UDP ──── USB/GPIB
└───┬──────────┬────────┬──┘
│ │ │
┌───────▼──────────▼───┐ ┌─▼──────────────┐
│ Arduino Portenta H7 │ │ Keysight E4980AL│
│ │ │ LCR Meter │
│ SMA_Driver (PWM) │ └─────────────────┘
│ Internal ADC (16-bit)│
│ SPI bus │
└──┬───┬───┬───────┬───┘
│ │ │ │
│ │ │ ┌──▼──────────┐
│ │ │ │ ADS1263 │
│ │ │ │ 32-bit ADC │
│ │ │ │ (SPI) │
│ │ │ └──┬───────────┘
│ │ │ │
▼ ▼ ▼ ▼
Shunt Laser LDO Load Cell
(A1) (A7) (A6) (via INA181 200x)
| Sensor | Signal | Amplification | ADC | Resolution |
|---|---|---|---|---|
| Load cell | 0-10 mV differential | INA181 (200x) -> 0-2V | ADS1263 (AIN0-AIN1) | 32-bit |
| Laser displacement | 0-3V analog | None | H7 internal (A7) | 16-bit |
| SMA shunt current | Voltage across shunt | INA181 (200x) | H7 internal (A1) | 16-bit |
| SMA coil voltage | Voltage divider | Resistor divider | H7 internal (A0) | 16-bit |
| LDO output | 0-5V analog | None | H7 internal (A6) | 16-bit |
| Impedance (Z, L, R) | N/A | N/A | Keysight E4980AL | Instrument-grade |
- Dedicated Ethernet cable between H7 and PC (no router)
- H7 IP:
192.168.1.177/ PC IP:192.168.1.100 - UDP port:
5000/ Serial:115200baud
SystemIntegration/
│
├── firmware/ # All embedded code (PlatformIO)
│ ├── platformio.ini # Build environments
│ ├── libs/ # Reusable PlatformIO libraries
│ │ ├── SMA_Driver/ # SMA actuator control + HAL
│ │ │ ├── sma_driver.h/cpp # State machine: STANDBY->HEATING->COOLING
│ │ │ ├── sma_hal.h # ADC constants: INA_GAIN=200, VREF=3.1V
│ │ │ └── sensor_cal.h # Calibration: voltage->force, voltage->mm
│ │ ├── ADS1263_Driver/ # 32-bit external ADC (SPI)
│ │ │ ├── ADS1263_driver.h/cpp # Non-blocking init, continuous read
│ │ │ └── ADS1263_hal.h # Register map, gain/rate enums
│ │ ├── CommandParser/ # ASCII serial command parser
│ │ │ ├── command_parser.h/cpp # "ARM", "START_PULSE 3.0 500 1000"
│ │ │ └── command_defs.h # CommandType enum, ParsedCommand struct
│ │ ├── COMM/ # Serial + UDP communications
│ │ │ ├── comms_manager.h/cpp # Serial rx/tx, UDP packet streaming
│ │ │ └── comms_protocol.h # SensorPacket (38 bytes), SystemState enum
│ │ └── HardwareConfig/ # Board-specific pin definitions
│ │ ├── hardware_def.h # Board selector (#ifdef)
│ │ └── boards/
│ │ ├── h7_pins.h # Portenta H7 pin assignments
│ │ └── mega2560_pins.h # Mega2560 pin assignments
│ └── src/
│ ├── main.cpp # Full integrated firmware
│ └── test_sketches/ # Single-purpose test firmware
│ ├── test_sma_standalone.cpp
│ ├── test_ads1263_standalone.cpp
│ ├── test_comms_standalone.cpp
│ ├── test_laser_standalone.cpp
│ └── test_shunt_ldo_standalone.cpp
│
├── python_drivers/ # PC-side instrument drivers
│ ├── arduino_interface/
│ │ ├── serial_comm.py # ArduinoSerialComm - serial command API
│ │ └── udp_receiver.py # UDPReceiver - background packet receiver
│ ├── LCRMeter/
│ │ ├── E4980ALDriver.py # Keysight E4980AL driver (pyvisa)
│ │ └── config_loader.py # YAML config loader
│ └── utils/
│ ├── timing_sync.py # MeasurementCoordinator (H7 + LCR)
│ └── data_validation.py # Packet validation, loss detection
│
├── notebooks/ # Jupyter test & analysis notebooks
│ ├── tests/ # Component-level (flash test firmware first)
│ │ ├── 01_test_sma_only.ipynb
│ │ ├── 02_test_ads1263_only.ipynb
│ │ ├── 03_test_lcr_only.ipynb
│ │ ├── 04_test_comm_only.ipynb
│ │ └── 05_test_laser_sensor.ipynb
│ ├── integration/ # System-level (flash main firmware)
│ │ ├── 06_integrated_test.ipynb
│ │ └── 07_full_measurement.ipynb
│ └── analysis/
│ └── data_analysis.ipynb
│
└── data/ # Experimental CSV/PNG output
setup():
SMA_HAL::init() // analogReadResolution(16)
sma.begin() // SMA state machine
loadcellAdc.beginAsync() // Non-blocking ADS1263 SPI init
comms.begin(config) // Serial + Ethernet + UDP
loop(): ← runs as fast as possible
comms.update() // Process serial commands, send UDP if due
loadcellAdc.updateInit() // Continue ADS1263 init (if still in progress)
sma.tick(micros()) // SMA heating/cooling state machine
readAllSensors() // H7 ADC + ADS1263 readings
handleCommand() // Dispatch parsed serial commands
updateSystemState() // Fault detection (overcurrent, overload)
comms.updateSensors() // Push latest readings into UDP packet
Offset Type Field
0 uint32 header (0xDEADBEEF)
4 uint32 sequence number
8 uint32 timestamp_us (micros since boot)
12 float loadcell_mg
16 float displacement_mm
20 float voltage (SMA coil)
24 float current_ma (SMA current)
28 float resistance (SMA)
32 float ldo_voltage
36 uint8 state (0=IDLE,1=ARMED,2=HEATING,3=COOLING,4=MANUAL,5=FAULT)
37 uint8 error_flags (bitmask)
Commands are ASCII, newline-terminated. Responses follow LEVEL:CODE (detail) format.
| Command | Parameters | Response |
|---|---|---|
PING |
none | PONG |
ARM |
none | INFO:ACK_ARM |
DISARM |
none | INFO:ACK_DISARM |
START_PULSE |
<voltage> <on_ms> <off_ms> |
INFO:ACK_START_PULSE |
SET_VOLTAGE |
<voltage> |
INFO:ACK_SET_VOLTAGE |
ABORT |
none | INFO:ACK_ABORT |
STATUS |
none | INFO:STATUS (STATE=... ERR=... ADS=...) |
GET_STATE |
none | INFO:STATE=IDLE |
GET_SENSORS |
none | INFO:SENSORS (LC=... D=... V=... I=... R=... LDO=...) |
STREAM_ON |
none | INFO:ACK_STREAM_ON |
STREAM_OFF |
none | INFO:ACK_STREAM_OFF |
STREAM_RATE |
<hz> |
INFO:ACK_STREAM_RATE |
IDLE ──ARM──> ARMED ──START_PULSE──> HEATING ──(auto)──> COOLING ──(auto)──> ARMED
^ │ │
│ ├──SET_VOLTAGE──> MANUAL │
│ │ │
└──DISARM─────┴──────────────────────────────────────────────────────────────┘
Any state ──(fault)──> FAULT ──DISARM──> IDLE
Any state ──ABORT──> IDLE
# Serial commands
from python_drivers.arduino_interface import ArduinoSerialComm
arduino = ArduinoSerialComm('/dev/ttyACM0')
arduino.connect()
arduino.ping() # True
arduino.arm() # True
arduino.start_pulse(voltage=3.0, on_ms=500, off_ms=1000)
# UDP sensor streaming
from python_drivers.arduino_interface import UDPReceiver
with UDPReceiver(port=5000) as udp:
time.sleep(5)
packets = udp.get_buffer() # List of dicts
stats = udp.get_stats() # loss rate, jitter
# LCR meter
from python_drivers.LCRMeter import E4980ALDriver
lcr = E4980ALDriver()
lcr.set_frequency(1000)
primary, secondary = lcr.measure()
# Coordinated measurement (H7 streams continuously, LCR triggered on demand)
from python_drivers.utils import MeasurementCoordinator
coord = MeasurementCoordinator(arduino, lcr, udp)
df = coord.run_measurement_series(n_measurements=100, interval_s=0.5)cd firmware
# Build specific target
pio run -e main # Full system
pio run -e test_sma # SMA only
# Build and upload
pio run -e main -t upload
# Serial monitor
pio device monitor -b 115200| Constant | Value | File | Notes |
|---|---|---|---|
INA_GAIN |
200.0 | sma_hal.h |
INA181 amplifier gain |
ADC_VREF |
3.1V | sma_hal.h |
Measured on H7 (verify with DMM) |
UDP_PORT |
5000 | comms_protocol.h |
|
SERIAL_BAUD |
115200 | comms_protocol.h |
|
DEFAULT_STREAM_INTERVAL_US |
2000 (500Hz) | comms_protocol.h |
|
VOLTAGE_MAX |
5.0V | command_defs.h |
SMA voltage limit |
PULSE_ON_MAX |
10000ms | command_defs.h |
Safety limit |
FAULT_CURRENT_LIMIT_MA |
2000 | main.cpp |
Overcurrent shutoff |
FAULT_LOAD_LIMIT_MG |
500000 (500g) | main.cpp |
Overload shutoff |