Skip to content

Repository files navigation

MicroTensile Testing Machine - System Integration

HDR Lab, University of Michigan

What This System Does

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.

Hardware Stack

                    ┌──────────────────────┐
                    │   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)

Signal Paths

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

Network

  • 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: 115200 baud

Project Structure

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

Architecture Details

Firmware Loop (main.cpp)

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

UDP Packet Format (38 bytes, big-endian)

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)

Serial Command Protocol

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

State Machine

IDLE ──ARM──> ARMED ──START_PULSE──> HEATING ──(auto)──> COOLING ──(auto)──> ARMED
  ^             │                                                              │
  │             ├──SET_VOLTAGE──> MANUAL                                       │
  │             │                                                              │
  └──DISARM─────┴──────────────────────────────────────────────────────────────┘

Any state ──(fault)──> FAULT ──DISARM──> IDLE
Any state ──ABORT──> IDLE

Python Driver Stack

# 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)

Build & Flash

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

Key Constants to Know

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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages