ControlForge + Teensy 4.0: Hardware Interface Guide
James M. Belcher Founder, JMB Technical Services LLC June 2026 | ControlForge v1.0.1064
1. Architecture Overview
ControlForge treats the Teensy 4.0 as a smart I/O module connected over USB RawHID. The Teensy runs a Rust firmware that exposes 47 ST functions covering digital I/O, analog, PWM, CAN bus, hardware PID, encoders, and more. All hardware control flows through 64-byte HID reports at full-speed USB (1 kHz poll rate).
Unlike serial-based protocols, RawHID provides guaranteed delivery with no framing ambiguity — each USB transaction is exactly 64 bytes, eliminating sync-byte hunting and partial-frame recovery.
| Feature | Specification |
|---|---|
| MCU | NXP i.MX RT1062, ARM Cortex-M7 @ 600 MHz |
| Flash | 1 MB (+ 8 MB QSPI on Teensy 4.0) |
| RAM | 512 KB tightly-coupled + 512 KB general |
| Digital Pins | 40 (all 3.3V, 5V tolerant on most) |
| Analog Inputs | 14 (10-bit default, configurable to 12-bit) |
| PWM Outputs | 31 (FlexPWM with complementary pairs and fault inputs) |
| CAN Buses | 3 (CAN 2.0B, FlexCAN hardware) |
| Serial Ports | 7 (hardware UART) |
| Encoder | Hardware quadrature decoder |
| RTC | Battery-backed real-time clock |
| TRNG | Hardware true random number generator |
System Diagram
2. Connection and Wire Protocol
2.1 The Three Lifecycle Functions
(* Connect to Teensy — empty path for auto-discovery *)
ok := TEENSY_INIT('t1', '');
(* Or specify the exact HID device *)
ok := TEENSY_INIT('t1', '/dev/hidraw3');
(* Check connection health *)
status := TEENSY_STATUS('t1');
(* Returns: "connected" or "disconnected" *)
(* Disconnect *)
TEENSY_CLOSE('t1');
Auto-discovery scans /dev/hidraw* devices for the Teensy RawHID vendor/product ID. In most single-Teensy setups, an empty path is all you need. For multi-Teensy rigs, specify the exact device path.
2.2 Wire Protocol
Every function call is packed into a 64-byte HID report:
┌──────┬──────┬─────┬─────┬────────┬──────────────┬────────┐
│ 0xA5 │ 0x5A │ SEQ │ CMD │ LEN(2) │ PAYLOAD(0-N) │ CRC(2) │
│ sync │ sync │ 1B │ 1B │ LE │ LE fields │ MODBUS │
└──────┴──────┴─────┴─────┴────────┴──────────────┴────────┘
- CRC-16/MODBUS over SEQ + CMD + LEN + PAYLOAD
- Fixed 64-byte USB HID reports (zero-padded)
- All multi-byte values: little-endian
- Response uses same frame format
- Command opcodes: 0xC0-0xFF (Teensy namespace, no collision with P2 opcodes)
You never build frames manually — each TEENSY_* function handles packing/unpacking internally.
2.3 Why RawHID (Not Serial)
| USB RawHID | USB Serial | |
|---|---|---|
| Framing | Fixed 64-byte packets | Byte stream — need sync bytes, escape sequences |
| Reliability | USB guarantees delivery | Bytes can be lost if buffer overflows |
| Latency | 1 ms poll interval (USB full-speed) | Variable — depends on OS buffering, FTDI latency timer |
| Multi-device | Each Teensy gets unique hidraw device | Serial ports can shuffle on reboot |
| No driver needed | Linux HID subsystem (built-in) | Requires CDC-ACM or FTDI driver |
3. Function Reference
3.1 Device Lifecycle
TEENSY_INIT — Connect to Teensy
(* Auto-discover first Teensy *)
ok := TEENSY_INIT('t1', '');
(* Explicit device path *)
ok := TEENSY_INIT('t1', '/dev/hidraw3');
Returns TRUE on successful connection. The firmware responds with its version and capability flags.
TEENSY_STATUS — Connection Health
status := TEENSY_STATUS('t1');
(* Returns: "connected" or "disconnected" *)
TEENSY_CLOSE — Disconnect
ok := TEENSY_CLOSE('t1');
3.2 Digital I/O
TEENSY_PIN_MODE — Configure Pin Direction
| Param | Type | Values |
|---|---|---|
name | STRING | Device name |
pin | INT | 0-39 |
mode | INT | 0=INPUT, 1=OUTPUT, 2=INPUT_PULLUP, 3=INPUT_PULLDOWN |
(* Set pin 13 (onboard LED) as output *)
TEENSY_PIN_MODE('t1', 13, 1);
(* Set pin 2 as input with pullup *)
TEENSY_PIN_MODE('t1', 2, 2);
TEENSY_DIGITAL_READ — Read Pin State
state := TEENSY_DIGITAL_READ('t1', 2);
(* Returns: TRUE or FALSE *)
TEENSY_DIGITAL_WRITE — Set Output
TEENSY_DIGITAL_WRITE('t1', 13, TRUE); (* LED on *)
TEENSY_DIGITAL_WRITE('t1', 13, FALSE); (* LED off *)
TEENSY_RESET_PINS — Reset All Pins to Default
(* Emergency reset — all pins return to high-impedance input *)
TEENSY_RESET_PINS('t1');
Useful for fault recovery or safe shutdown. Every pin is set to floating input, all PWM stopped, all peripherals de-initialized.
Example: Digital I/O Scan Loop
PROGRAM POU_DigitalIO
VAR
sensor : BOOL;
ok : BOOL;
END_VAR
(* Read sensor on pin 2 *)
sensor := TEENSY_DIGITAL_READ('t1', 2);
(* Drive relay on pin 6 based on sensor *)
IF sensor THEN
ok := TEENSY_DIGITAL_WRITE('t1', 6, TRUE);
ELSE
ok := TEENSY_DIGITAL_WRITE('t1', 6, FALSE);
END_IF;
END_PROGRAM
3.3 Analog Input
The Teensy 4.0 has 14 analog inputs (pins 14-27 on default mapping) with 10-bit resolution (0-1023) by default.
TEENSY_ANALOG_READ — Read ADC Value
raw := TEENSY_ANALOG_READ('t1', 14);
(* Returns: 0-1023 (10-bit) or 0-4095 (12-bit if configured) *)
Example: Analog Monitoring
PROGRAM POU_AnalogMonitor
VAR
raw_value : INT;
voltage : REAL;
END_VAR
raw_value := TEENSY_ANALOG_READ('t1', 14);
(* Scale to voltage: 3.3V reference, 10-bit resolution *)
voltage := INT_TO_REAL(raw_value) * 3.3 / 1023.0;
END_PROGRAM
3.4 PWM
The Teensy 4.0's FlexPWM engine provides 31 PWM outputs with configurable frequency, resolution, complementary pairs, and hardware fault inputs — capabilities that rival dedicated motor drive ICs.
TEENSY_PWM_WRITE — Basic PWM Output
(* 50% duty on pin 3 (uses default frequency and resolution) *)
ok := TEENSY_PWM_WRITE('t1', 3, 128);
TEENSY_PWM_CONFIG — Configure Frequency and Resolution
| Param | Type | Description |
|---|---|---|
pin | INT | PWM-capable pin |
freq | INT | Frequency in Hz |
resolution | INT | Bit depth (8-16 typical) |
(* 20 kHz PWM at 12-bit resolution on pin 3 *)
ok := TEENSY_PWM_CONFIG('t1', 3, 20000, 12);
(* Now set duty: 0-4095 range (12-bit) *)
ok := TEENSY_PWM_WRITE('t1', 3, 2048); (* 50% *)
Note: Higher frequency reduces maximum resolution. At 600 MHz bus clock: 20 kHz allows ~15 bits, 100 kHz allows ~13 bits, 1 MHz allows ~9 bits.
TEENSY_PWM_PAIR — Complementary PWM with Dead Time
This is the industrial workhorse for half-bridge and full-bridge motor drives, where the high-side and low-side switches must never conduct simultaneously.
| Param | Type | Description |
|---|---|---|
pinH | INT | High-side PWM pin |
pinL | INT | Low-side PWM pin (complementary) |
freqHz | INT | Switching frequency in Hz |
deadtimeNs | INT | Dead time in nanoseconds |
(* Half-bridge: 20 kHz, 500ns dead time *)
ok := TEENSY_PWM_PAIR('t1', 2, 3, 20000, 500);
Setting duty: The complementary pair shares its duty cycle with the high-side pin's
TEENSY_PWM_WRITE. Configure the pair once, then drive the duty withTEENSY_PWM_WRITE('t1', pinH, duty).
Why this matters: Dead time prevents shoot-through — the catastrophic condition where both transistors in a half-bridge conduct simultaneously, creating a short circuit from supply to ground. The FlexPWM hardware inserts the dead time in silicon, with nanosecond precision that software timers cannot match.
TEENSY_PWM_FAULT — Hardware Fault Input
Connects a physical fault pin to the FlexPWM shutdown logic. When the fault pin triggers, PWM outputs are disabled in hardware within one clock cycle — no software latency.
(* Fault on pin 5 (typical for gate driver fault outputs) *)
ok := TEENSY_PWM_FAULT('t1', 5);
(* Fault on pin 7 *)
ok := TEENSY_PWM_FAULT('t1', 7);
Industrial application: Gate drivers for IGBTs and MOSFETs provide a fault output (overcurrent, desaturation, overtemperature). Wiring this to a FlexPWM fault input guarantees sub-microsecond shutdown — critical for protecting power electronics from destructive faults.
Example: Motor Drive with Protection
PROGRAM POU_MotorDrive
VAR
ok : BOOL;
speed_cmd : INT := 0; (* 0-65535 *)
running : BOOL := FALSE;
END_VAR
IF NOT running THEN
(* Configure fault input first — always set up protection before enabling drive *)
ok := TEENSY_PWM_FAULT('t1', 5);
(* Configure complementary PWM: 20 kHz, 1us dead time *)
ok := TEENSY_PWM_PAIR('t1', 2, 3, 20000, 1000);
running := TRUE;
END_IF;
(* Update duty from control logic — drive the high-side pin *)
ok := TEENSY_PWM_WRITE('t1', 2, speed_cmd);
END_PROGRAM
3.5 Servo
TEENSY_SERVO — Set Servo Angle
| Param | Type | Description |
|---|---|---|
pin | INT | Servo signal pin |
angle | INT | Position in degrees (0-180) |
(* Center servo *)
ok := TEENSY_SERVO('t1', 9, 90);
(* Full sweep *)
ok := TEENSY_SERVO('t1', 9, 0); (* min *)
ok := TEENSY_SERVO('t1', 9, 180); (* max *)
Pin sharing: Servo signals are generated by the same FlexPWM hardware as
TEENSY_PWM_WRITE. Configuring a pin for servo overrides any prior PWM configuration on that pin.
3.6 I2C
The Teensy 4.0 has dedicated I2C hardware with internal pullups available.
All I2C calls take a port argument (the Teensy 4.0 has multiple I2C buses — port 0 is the default Wire bus).
TEENSY_I2C_SCAN — Discover Devices
(* Scan bus on port 0 *)
devices := TEENSY_I2C_SCAN('t1', 0);
(* Returns: "3C,48,68" — comma-separated hex addresses *)
TEENSY_I2C_WRITE — Write a Byte
(* Write command byte 0xAE to OLED at 0x3C on port 0 *)
ok := TEENSY_I2C_WRITE('t1', 0, 16#3C, 16#AE);
TEENSY_I2C_READ — Read a Byte
(* Read one byte from temperature sensor at 0x48 on port 0 *)
value := TEENSY_I2C_READ('t1', 0, 16#48);
(* Returns: INT — the byte value 0-255 *)
TEENSY_I2C_WRITE_READ — Write Then Read (Repeated START)
Most I2C sensors require writing a register address, then reading the result without releasing the bus.
(* Write register 0x00, then read 2 bytes from device at 0x48 on port 0 *)
data := TEENSY_I2C_WRITE_READ('t1', 0, 16#48, 16#00, 2);
(* Returns: "0C80" — hex-encoded bytes *)
Example: Temperature Sensor (LM75/TMP102)
PROGRAM POU_TempSensor
VAR
raw_hex : STRING;
END_VAR
(* Write register 0x00, then read 2-byte temperature from LM75 at 0x48 on port 0 *)
raw_hex := TEENSY_I2C_WRITE_READ('t1', 0, 16#48, 16#00, 2);
(* Parse: raw_hex contains MSB:LSB, temperature = value / 256.0 *)
END_PROGRAM
3.7 SPI
TEENSY_SPI_TRANSFER — Full Duplex Transfer
| Param | Type | Description |
|---|---|---|
port | INT | SPI port (0 = default SPI bus) |
hexData | STRING | Hex-encoded transmit data |
(* Transfer 2 bytes on SPI port 0 *)
rx := TEENSY_SPI_TRANSFER('t1', 0, 'FF00');
(* Returns: hex-encoded received data *)
Example: MCP3008 ADC (8-channel, 10-bit)
PROGRAM POU_ExternalADC
VAR
result : STRING;
END_VAR
(* Read channel 0: send start bit + single-ended + channel *)
(* 0x01 = start, 0x80 = single-ended CH0, 0x00 = clock out result *)
result := TEENSY_SPI_TRANSFER('t1', 0, '018000');
(* Parse 10-bit result from response bytes *)
END_PROGRAM
3.8 UART
The Teensy 4.0 has 7 hardware serial ports. Channels 1-7 map to Serial1-Serial7 in the Teensy ecosystem.
TEENSY_UART_INIT — Open Channel
| Param | Type | Description |
|---|---|---|
channel | INT | Channel 1-7 (Serial1-Serial7, fixed default pins) |
baudRate | INT | Baud rate |
(* Serial1 at 9600 baud *)
ok := TEENSY_UART_INIT('t1', 1, 9600);
(* Serial2 for Modbus RTU at 19200 *)
ok := TEENSY_UART_INIT('t1', 2, 19200);
TEENSY_UART_SEND — Transmit Data
Data is hex-encoded. 48656C6C6F = "Hello".
(* Send Modbus query frame on Serial2 *)
ok := TEENSY_UART_SEND('t1', 2, '0103000000010A11');
TEENSY_UART_RECV — Receive Data
(* Read up to 32 bytes from Serial2 *)
data := TEENSY_UART_RECV('t1', 2, 32);
(* Returns: hex-encoded received bytes, empty string if nothing available *)
Example: RS-485 Modbus RTU Query
PROGRAM POU_ModbusQuery
VAR
ok : BOOL;
response : STRING;
state : INT := 0;
END_VAR
CASE state OF
0: (* Initialize UART for Modbus *)
ok := TEENSY_UART_INIT('t1', 2, 19200);
IF ok THEN state := 1; END_IF;
1: (* Send read holding registers: addr=1, func=3, start=0, count=10 *)
ok := TEENSY_UART_SEND('t1', 2, '010300000000A5CD');
state := 2;
2: (* Read response *)
response := TEENSY_UART_RECV('t1', 2, 64);
IF LEN(response) > 0 THEN
(* Parse Modbus response *)
state := 1; (* Continue polling *)
END_IF;
END_CASE;
END_PROGRAM
3.9 OLED Display (SSD1306)
Firmware-native text rendering on I2C OLED displays. 21 characters x 8 rows on a 128x64 OLED.
All OLED calls take the display's I2C address addr (0x3C is the common SSD1306 address).
(* Initialize OLED at I2C address 0x3C *)
ok := TEENSY_OLED_INIT('t1', 16#3C);
(* Clear screen *)
ok := TEENSY_OLED_CLEAR('t1', 16#3C);
(* Print status information — (name, addr, row, text) *)
ok := TEENSY_OLED_PRINT('t1', 16#3C, 0, 'ControlForge + Teensy 4.0');
ok := TEENSY_OLED_PRINT('t1', 16#3C, 2, 'CAN: 250kbps OK');
ok := TEENSY_OLED_PRINT('t1', 16#3C, 4, 'PID: Kp=2.0 Ki=0.1');
ok := TEENSY_OLED_PRINT('t1', 16#3C, 6, 'Temp: 42C');
3.10 NeoPixel (WS2812B)
TEENSY_NEOPIXEL — Set the Single Onboard Pixel
Sets the Teensy's single onboard NeoPixel to an RGB color.
(* Onboard pixel to red *)
ok := TEENSY_NEOPIXEL('t1', 255, 0, 0);
(* Onboard pixel to blue *)
ok := TEENSY_NEOPIXEL('t1', 0, 0, 255);
TEENSY_NEO_STRIP — Full Strip Update
Updates an entire NeoPixel strip in a single command. Colors are passed as a
hex string — one RRGGBB triple per LED, concatenated.
(* Update 4-pixel strip: red, green, blue, white *)
ok := TEENSY_NEO_STRIP('t1', 4, 'FF000000FF000000FFFFFFFF');
Example: Status Indicator Strip
PROGRAM POU_StatusLEDs
VAR
ok : BOOL;
can_ok : BOOL;
pid_ok : BOOL;
temp_ok : BOOL;
colors : STRING;
END_VAR
(* Green = OK (00FF00), Red = Fault (FF0000) *)
(* Build a 3-LED strip: pixel 0 = CAN, pixel 1 = PID, pixel 2 = Temp *)
IF can_ok THEN colors := '00FF00'; ELSE colors := 'FF0000'; END_IF;
IF pid_ok THEN colors := CONCAT(colors, '00FF00'); ELSE colors := CONCAT(colors, 'FF0000'); END_IF;
IF temp_ok THEN colors := CONCAT(colors, '00FF00'); ELSE colors := CONCAT(colors, 'FF0000'); END_IF;
ok := TEENSY_NEO_STRIP('t1', 3, colors);
END_PROGRAM
3.11 CAN Bus
The Teensy 4.0 has 3 hardware CAN 2.0B controllers (FlexCAN). This is the primary industrial fieldbus interface — CAN is the backbone of automotive, industrial automation, and robotics communication.
All CAN calls take a port argument selecting one of the 3 hardware CAN buses (0 = CAN1, 1 = CAN2, 2 = CAN3).
TEENSY_CAN_INIT — Initialize CAN Bus
(* Initialize CAN1 (port 0) at 250 kbps (typical industrial) *)
ok := TEENSY_CAN_INIT('t1', 0, 250000);
(* CAN2 (port 1) at 500 kbps for automotive *)
ok := TEENSY_CAN_INIT('t1', 1, 500000);
(* CAN1 at 1 Mbps for high-speed applications *)
ok := TEENSY_CAN_INIT('t1', 0, 1000000);
Hardware required: CAN needs an external transceiver (MCP2551, SN65HVD230, or similar) between the Teensy CAN TX/RX pins and the CAN bus. The Teensy provides the protocol controller; the transceiver handles the differential signaling and bus fault protection.
TEENSY_CAN_SEND — Transmit Frame
(* Send 8-byte CAN frame with ID 0x200 on CAN1 (port 0) *)
ok := TEENSY_CAN_SEND('t1', 0, 16#200, '0102030405060708');
(* Send 3-byte frame *)
ok := TEENSY_CAN_SEND('t1', 0, 16#100, '112233');
TEENSY_CAN_RECV — Receive Frames
(* Drain up to 8 queued frames from CAN1 (port 0) *)
frame := TEENSY_CAN_RECV('t1', 0, 8);
(* Returns JSON: [{"id":512,"data":"0102030405060708","len":8}] *)
(* Empty string / empty array when no frames are queued *)
TEENSY_CAN_FILTER — Set Hardware Acceptance Filter
Filters are applied in hardware — rejected frames never reach the firmware, reducing CPU load.
| Param | Type | Description |
|---|---|---|
id | INT | Acceptance ID |
mask | INT | Bit mask (1 = must match, 0 = don't care) |
(* Accept only ID 0x200 exactly on CAN1 (port 0) *)
ok := TEENSY_CAN_FILTER('t1', 0, 16#200, 16#7FF);
(* Accept IDs 0x300-0x30F (mask ignores low 4 bits) *)
ok := TEENSY_CAN_FILTER('t1', 0, 16#300, 16#7F0);
(* Accept all (no filter) *)
ok := TEENSY_CAN_FILTER('t1', 0, 0, 0);
TEENSY_CAN_STATUS — Bus Diagnostics
status := TEENSY_CAN_STATUS('t1', 0);
(* Returns JSON: {"state":"active","tx_errors":0,"rx_errors":0,"bus_off":false} *)
CAN states: active (normal), warning (error count > 96), passive (error count > 127), bus_off (error count > 255, bus disconnected).
Example: CANopen-Style I/O Module
PROGRAM POU_CANBridge
VAR
ok : BOOL;
frame : STRING;
state : INT := 0;
analog_value : INT;
END_VAR
CASE state OF
0: (* Initialize CAN1 (port 0) at 250 kbps *)
ok := TEENSY_CAN_INIT('t1', 0, 250000);
(* Accept PDO range 0x180-0x1FF *)
ok := TEENSY_CAN_FILTER('t1', 0, 16#180, 16#780);
IF ok THEN state := 1; END_IF;
1: (* Main loop: receive commands, send data *)
(* Check for incoming PDOs (drain up to 4) *)
frame := TEENSY_CAN_RECV('t1', 0, 4);
IF LEN(frame) > 0 THEN
(* Parse and apply digital outputs from CAN frame *)
END_IF;
(* Read local analog and broadcast as TPDO *)
analog_value := TEENSY_ANALOG_READ('t1', 14);
ok := TEENSY_CAN_SEND('t1', 0, 16#280,
CONCAT(INT_TO_HEX(analog_value), '0000000000000000'));
END_CASE;
END_PROGRAM
3.12 Quadrature Encoder
Uses the Teensy 4.0's hardware quadrature decoder for zero-CPU-overhead position tracking.
TEENSY_ENCODER_INIT — Configure Encoder
(* Encoder on pins 2 (A) and 3 (B) *)
ok := TEENSY_ENCODER_INIT('t1', 2, 3);
TEENSY_ENCODER_READ — Read Position
The encoder is identified by its A pin (the same pinA passed to TEENSY_ENCODER_INIT).
count := TEENSY_ENCODER_READ('t1', 2);
(* Returns: signed INT — positive for CW, negative for CCW *)
TEENSY_ENCODER_RESET — Zero the Counter
ok := TEENSY_ENCODER_RESET('t1', 2);
Example: Position Tracking
PROGRAM POU_Encoder
VAR
position : INT;
ok : BOOL;
initialized : BOOL := FALSE;
END_VAR
IF NOT initialized THEN
ok := TEENSY_ENCODER_INIT('t1', 2, 3);
ok := TEENSY_ENCODER_RESET('t1', 2);
initialized := TRUE;
END_IF;
position := TEENSY_ENCODER_READ('t1', 2);
(* position tracks cumulative counts — 4x decoding (both edges, both channels) *)
END_PROGRAM
3.13 Frequency Counter
TEENSY_FREQ_INIT — Configure Input Pin
ok := TEENSY_FREQ_INIT('t1', 10);
TEENSY_FREQ_READ — Read Frequency
The counter is identified by its input pin (the same pin passed to TEENSY_FREQ_INIT).
hz := TEENSY_FREQ_READ('t1', 10);
(* Returns: INT — frequency in Hz (whole hertz) *)
(* Example: 1000 for 1 kHz *)
Example: RPM Measurement
PROGRAM POU_RPM
VAR
freq : INT;
rpm : INT;
ok : BOOL;
initialized : BOOL := FALSE;
END_VAR
IF NOT initialized THEN
ok := TEENSY_FREQ_INIT('t1', 10);
initialized := TRUE;
END_IF;
freq := TEENSY_FREQ_READ('t1', 10);
(* 1 pulse per revolution: RPM = Hz * 60 *)
rpm := freq * 60;
END_PROGRAM
3.14 Hardware PID Controller
The PID loop runs in the Teensy firmware, not in the ControlForge scan cycle. This provides deterministic control at the firmware tick rate (typically 1 kHz) regardless of ControlForge scan time or USB latency.
The firmware supports multiple independent PID loops, each identified by a small
integer id. Every PID call takes that id as its first argument after name.
TEENSY_PID_CONFIG — Initialize PID Loop
| Param | Type | Description |
|---|---|---|
id | INT | Loop identifier (0, 1, 2, …) |
inputPin | INT | Analog input pin (process variable) |
outputPin | INT | PWM output pin (control variable) |
kp | REAL | Proportional gain |
ki | REAL | Integral gain |
kd | REAL | Derivative gain |
rateHz | INT | Loop rate in Hz (firmware tick) |
outMin | INT | Minimum output value |
outMax | INT | Maximum output value |
(* Loop 0 — temperature control: thermocouple on pin 14, heater PWM on pin 3 *)
(* gains, 100 Hz loop, output clamped 0-255 *)
ok := TEENSY_PID_CONFIG('t1', 0, 14, 3, 2.0, 0.1, 0.05, 100, 0, 255);
TEENSY_PID_SETPOINT — Set Target Value
(* Loop 0 setpoint — REAL value *)
ok := TEENSY_PID_SETPOINT('t1', 0, 512.0); (* mid-scale = ~1.65V *)
TEENSY_PID_READ — Read PID State
state := TEENSY_PID_READ('t1', 0);
(* Returns JSON: {"input":498,"output":178,"setpoint":512,"error":14} *)
TEENSY_PID_TUNE — Hot-Tune Gains
Change PID gains without stopping the control loop. Essential for field tuning.
(* Loop 0 — increase proportional gain (REAL gains) *)
ok := TEENSY_PID_TUNE('t1', 0, 3.0, 0.1, 0.05);
TEENSY_PID_STOP — Stop PID Loop
ok := TEENSY_PID_STOP('t1', 0);
(* Output pin goes to 0 — safe shutdown *)
Example: Closed-Loop Temperature Control
PROGRAM POU_TempControl
VAR
ok : BOOL;
pid_state : STRING;
setpoint : REAL := 512.0;
state : INT := 0;
END_VAR
CASE state OF
0: (* Configure PID loop 0: thermocouple on A0 (pin 14), heater on pin 3 *)
(* gains, 100 Hz loop, output clamped 0-255 *)
ok := TEENSY_PID_CONFIG('t1', 0, 14, 3, 2.0, 0.1, 0.05, 100, 0, 255);
ok := TEENSY_PID_SETPOINT('t1', 0, setpoint);
state := 1;
1: (* Monitor — PID runs autonomously in firmware *)
pid_state := TEENSY_PID_READ('t1', 0);
(* Log or display pid_state *)
(* Adjust setpoint from HMI if needed *)
99: (* Shutdown *)
ok := TEENSY_PID_STOP('t1', 0);
state := 100;
END_CASE;
END_PROGRAM
Why firmware PID matters: A ControlForge scan cycle runs at 1-50 ms. USB round-trip adds 1-2 ms. The firmware PID loop runs at the Teensy's internal tick rate (~1 kHz), giving 10-50x faster control response. For thermal control this may not matter; for motor speed or pressure regulation, it is the difference between stable control and oscillation.
3.15 RTC (Real-Time Clock)
The Teensy 4.0 has a battery-backed RTC that maintains time through power cycles (with a CR2032 coin cell on the VBAT pin).
TEENSY_RTC_GET — Read Current Time
epoch := TEENSY_RTC_GET('t1');
(* Returns: INT — Unix epoch seconds (e.g. 1775492200) *)
TEENSY_RTC_SET — Set Time
(* Set time from Unix epoch seconds *)
ok := TEENSY_RTC_SET('t1', 1775492200);
Example: Timestamped Event Logging
PROGRAM POU_EventLog
VAR
epoch : INT;
fault_active : BOOL;
last_fault : BOOL := FALSE;
END_VAR
fault_active := TEENSY_DIGITAL_READ('t1', 5);
(* Log rising edge of fault *)
IF fault_active AND NOT last_fault THEN
epoch := TEENSY_RTC_GET('t1');
(* Log: CONCAT('FAULT at epoch ', INT_TO_STRING(epoch)) *)
END_IF;
last_fault := fault_active;
END_PROGRAM
3.16 True Random Number Generator (TRNG)
The i.MX RT1062 contains a hardware entropy source that produces cryptographically random numbers from physical noise — not a PRNG seeded from a timer.
TEENSY_TRNG_READ — Get Random Bytes
Returns len bytes of hardware entropy, hex-encoded as a STRING.
rng := TEENSY_TRNG_READ('t1', 4);
(* Returns: STRING — 4 hex-encoded random bytes, e.g. "9F3A1C7E" *)
Example: Session Token Generation
PROGRAM POU_Security
VAR
token : STRING;
END_VAR
(* Generate 16 bytes (128 bits) of hardware entropy for a session token *)
token := TEENSY_TRNG_READ('t1', 16);
END_PROGRAM
Industrial use cases: Challenge-response authentication with field devices, nonce generation for encrypted CAN frames, randomized retry backoff for multi-master bus arbitration.
3.17 Sensors
TEENSY_TEMP_READ — Internal Die Temperature
temp_c := TEENSY_TEMP_READ('t1');
(* Returns: INT — internal temperature in degrees Celsius *)
Useful for thermal monitoring of the Teensy itself, especially in enclosed industrial panels.
TEENSY_DISTANCE — HC-SR04 Ultrasonic Distance
| Param | Type | Description |
|---|---|---|
trig | INT | Trigger pin |
echo | INT | Echo pin |
mm := TEENSY_DISTANCE('t1', 20, 21);
(* Returns: INT — distance in millimeters *)
Example: Proximity Detection
PROGRAM POU_Proximity
VAR
distance_mm : INT;
object_present : BOOL;
END_VAR
distance_mm := TEENSY_DISTANCE('t1', 20, 21);
object_present := (distance_mm > 0) AND (distance_mm < 300);
END_PROGRAM
3.18 System
TEENSY_BOOTLOADER — Enter Firmware Update Mode
ok := TEENSY_BOOTLOADER('t1');
(* Teensy enters bootloader — connection is lost *)
(* Re-flash with teensy_loader_cli or Teensy Loader GUI *)
Caution: This is a one-way trip. The Teensy disconnects from ControlForge and enters bootloader mode. You must re-flash firmware and call
TEENSY_INITagain to reconnect.
4. Industrial Application Patterns
4.1 Motor Drive with CAN Bus Feedback
Combines complementary PWM, encoder feedback, CAN bus communication, and hardware PID — a complete servo drive in ST code.
PROGRAM POU_ServoDrive
VAR
ok : BOOL;
position : INT;
pid_state : STRING;
can_frame : STRING;
can_status : STRING;
state : INT := 0;
END_VAR
CASE state OF
0: (* Initialize all subsystems *)
ok := TEENSY_CAN_INIT('t1', 0, 250000);
ok := TEENSY_CAN_FILTER('t1', 0, 16#200, 16#7FF);
ok := TEENSY_ENCODER_INIT('t1', 2, 3);
ok := TEENSY_PWM_FAULT('t1', 5);
(* PID loop 0: PV on pin 14, output on pin 6, 200 Hz, clamp 0-255 *)
ok := TEENSY_PID_CONFIG('t1', 0, 14, 6, 1.5, 0.05, 0.01, 200, 0, 255);
ok := TEENSY_PID_SETPOINT('t1', 0, 0.0);
state := 1;
1: (* Running — CAN receives setpoint, encoder provides position *)
(* Check for CAN command (drain up to 4 frames) *)
can_frame := TEENSY_CAN_RECV('t1', 0, 4);
IF LEN(can_frame) > 0 THEN
(* Parse new setpoint from CAN frame and apply *)
(* ok := TEENSY_PID_SETPOINT('t1', 0, new_setpoint); *)
END_IF;
(* Read encoder for position feedback (encoder A pin = 2) *)
position := TEENSY_ENCODER_READ('t1', 2);
(* Read PID state for diagnostics *)
pid_state := TEENSY_PID_READ('t1', 0);
(* Broadcast position on CAN *)
ok := TEENSY_CAN_SEND('t1', 0, 16#280,
CONCAT(INT_TO_HEX(position), '00000000'));
(* Monitor CAN bus health *)
can_status := TEENSY_CAN_STATUS('t1', 0);
99: (* Fault — stop everything *)
ok := TEENSY_PID_STOP('t1', 0);
ok := TEENSY_RESET_PINS('t1');
END_CASE;
END_PROGRAM
4.2 Multi-Sensor Data Acquisition
PROGRAM POU_DataAcq
VAR
ok : BOOL;
analog_ch : INT;
adc_values : ARRAY[0..3] OF INT;
die_temp : INT;
distance : INT;
encoder_pos : INT;
freq : INT;
epoch : INT;
END_VAR
(* Read 4 analog channels *)
adc_values[0] := TEENSY_ANALOG_READ('t1', 14);
adc_values[1] := TEENSY_ANALOG_READ('t1', 15);
adc_values[2] := TEENSY_ANALOG_READ('t1', 16);
adc_values[3] := TEENSY_ANALOG_READ('t1', 17);
(* Read other sensors *)
die_temp := TEENSY_TEMP_READ('t1');
distance := TEENSY_DISTANCE('t1', 20, 21);
encoder_pos := TEENSY_ENCODER_READ('t1', 2);
freq := TEENSY_FREQ_READ('t1', 10);
epoch := TEENSY_RTC_GET('t1');
(* Display on OLED at 0x3C — (name, addr, row, text) *)
ok := TEENSY_OLED_PRINT('t1', 16#3C, 0, CONCAT('T:', INT_TO_STRING(die_temp), 'C'));
ok := TEENSY_OLED_PRINT('t1', 16#3C, 2, CONCAT('D:', INT_TO_STRING(distance), 'mm'));
ok := TEENSY_OLED_PRINT('t1', 16#3C, 4, CONCAT('E:', INT_TO_STRING(encoder_pos)));
ok := TEENSY_OLED_PRINT('t1', 16#3C, 6, CONCAT('F:', INT_TO_STRING(freq), 'Hz'));
END_PROGRAM
5. Timing Tiers
| Tier | Where It Runs | Latency | Use Case |
|---|---|---|---|
| Teensy firmware (Rust) | On-chip, Cortex-M7 | ~1.7 ns/cycle (600 MHz) | PID loop, PWM fault shutdown, encoder counting |
| FlexPWM hardware | Peripheral silicon | Sub-nanosecond | Dead time insertion, complementary outputs, fault response |
| USB RawHID | Host ↔ Teensy | 1-2 ms round-trip | Command/response, sensor reads |
| ControlForge scan | Host CPU, ST interpreter | 1-50 ms | State machines, sequencing, CAN orchestration |
| ControlForge boss | Cluster coordination | 10-100 ms | HMI, logging, multi-device orchestration |
The key insight: time-critical operations run in the Teensy firmware or hardware peripherals. The PID loop does not wait for ControlForge. The PWM fault shutdown does not wait for USB. ControlForge handles the logic, sequencing, and coordination — the Teensy handles the microsecond-level control.
6. Hardware Notes for Teensy 4.0 Users
Pin Constraints
- Pin 13: Onboard LED — usable for general I/O but convenient for debug.
- Pins 0/1: Default Serial1 TX/RX. Available for GPIO if Serial1 is not used.
- Pins 18/19: Default I2C SDA/SCL. Available for GPIO if I2C is not used.
- CAN pins: CAN1 TX=22, RX=23. CAN2 TX=1, RX=0. CAN3 TX=31, RX=30.
Voltage Levels
- All I/O is 3.3V. Most pins are 5V tolerant (can read 5V input without damage), but output is always 3.3V.
- Analog reference: 3.3V, not adjustable. External voltage dividers needed for higher voltage signals.
- CAN transceiver: Must be 3.3V compatible (SN65HVD230) or use a 5V transceiver (MCP2551) with level shifting.
USB RawHID Gotchas
- Linux permissions: By default,
/dev/hidraw*devices require root access. Add a udev rule:ControlForge's install script creates this rule automatically.SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="0486", MODE="0666" - Device enumeration: hidraw device numbers can change on reboot. Use
TEENSY_INIT('t1', '')for auto-discovery, or write udev rules that create symlinks based on serial number. - 64-byte limit: Each HID report is exactly 64 bytes. Commands with payloads exceeding ~56 bytes (after header + CRC) are split across multiple reports automatically.
Firmware Update
- Call
TEENSY_BOOTLOADER('t1')to enter bootloader mode (or press the physical button on the Teensy). - Flash new firmware with
teensy_loader_cli --mcu=TEENSY40 firmware.hexor the Teensy Loader GUI. - Teensy reboots with new firmware.
- Call
TEENSY_INIT('t1', '')to reconnect.
Power Considerations
- USB power: 500 mA from USB. Sufficient for the Teensy itself plus moderate I/O.
- External power: Use VIN pin (5-24V) for higher current applications. The onboard regulator provides 3.3V at 250 mA.
- Motor drives: Always use external power for motors. Never power motors from the Teensy's regulator.
- CAN bus termination: Add 120-ohm termination resistors at each end of the CAN bus. The Teensy does not include built-in termination.
Appendix A: Complete Function Quick Reference
| Function | Opcode | Parameters | Returns |
|---|---|---|---|
TEENSY_INIT | — | name:STRING, path:STRING | BOOL |
TEENSY_CLOSE | — | name:STRING | BOOL |
TEENSY_STATUS | — | name:STRING | STRING |
TEENSY_PIN_MODE | 0xC0 | name, pin:INT, mode:INT | BOOL |
TEENSY_DIGITAL_READ | 0xC1 | name, pin:INT | BOOL |
TEENSY_DIGITAL_WRITE | 0xC2 | name, pin:INT, value:BOOL | BOOL |
TEENSY_RESET_PINS | 0xC3 | name | BOOL |
TEENSY_ANALOG_READ | 0xC4 | name, pin:INT | INT |
TEENSY_PWM_WRITE | 0xC5 | name, pin:INT, duty:INT | BOOL |
TEENSY_PWM_CONFIG | 0xC6 | name, pin:INT, freqHz:INT, alignment:INT | BOOL |
TEENSY_PWM_PAIR | 0xC7 | name, pinH:INT, pinL:INT, freqHz:INT, deadtimeNs:INT | BOOL |
TEENSY_PWM_FAULT | 0xC8 | name, faultPin:INT | BOOL |
TEENSY_SERVO | 0xC9 | name, pin:INT, angle:INT | BOOL |
TEENSY_I2C_SCAN | 0xCA | name, port:INT | STRING |
TEENSY_I2C_WRITE | 0xCB | name, port:INT, addr:INT, value:INT | BOOL |
TEENSY_I2C_READ | 0xCC | name, port:INT, addr:INT | INT |
TEENSY_I2C_WRITE_READ | 0xCD | name, port:INT, addr:INT, reg:INT, readLen:INT | STRING (hex) |
TEENSY_SPI_TRANSFER | 0xCE | name, port:INT, hexData:STRING | STRING (hex) |
TEENSY_UART_INIT | 0xD0 | name, channel:INT, baudRate:INT | BOOL |
TEENSY_UART_SEND | 0xD1 | name, channel:INT, data:STRING | BOOL |
TEENSY_UART_RECV | 0xD2 | name, channel:INT, maxLen:INT | STRING (hex) |
TEENSY_OLED_INIT | 0xD3 | name, addr:INT | BOOL |
TEENSY_OLED_CLEAR | 0xD4 | name, addr:INT | BOOL |
TEENSY_OLED_PRINT | 0xD5 | name, addr:INT, row:INT, text:STRING | BOOL |
TEENSY_NEOPIXEL | 0xD6 | name, r:INT, g:INT, b:INT | BOOL |
TEENSY_NEO_STRIP | 0xD7 | name, numLeds:INT, hexColors:STRING | BOOL |
TEENSY_CAN_INIT | 0xD8 | name, port:INT, baudRate:INT | BOOL |
TEENSY_CAN_SEND | 0xD9 | name, port:INT, id:INT, hexData:STRING | BOOL |
TEENSY_CAN_RECV | 0xDA | name, port:INT, maxMsgs:INT | STRING (JSON) |
TEENSY_CAN_FILTER | 0xDB | name, port:INT, id:INT, mask:INT | BOOL |
TEENSY_CAN_STATUS | 0xDC | name, port:INT | STRING (JSON) |
TEENSY_ENCODER_INIT | 0xDD | name, pinA:INT, pinB:INT | BOOL |
TEENSY_ENCODER_READ | 0xDE | name, pinA:INT | INT (signed) |
TEENSY_ENCODER_RESET | 0xDF | name, pinA:INT | BOOL |
TEENSY_FREQ_INIT | 0xE0 | name, pin:INT | BOOL |
TEENSY_FREQ_READ | 0xE1 | name, pin:INT | INT (Hz) |
TEENSY_PID_CONFIG | 0xE2 | name, id:INT, inputPin:INT, outputPin:INT, kp:REAL, ki:REAL, kd:REAL, rateHz:INT, outMin:INT, outMax:INT | BOOL |
TEENSY_PID_SETPOINT | 0xE3 | name, id:INT, setpoint:REAL | BOOL |
TEENSY_PID_READ | 0xE4 | name, id:INT | STRING (JSON) |
TEENSY_PID_TUNE | 0xE5 | name, id:INT, kp:REAL, ki:REAL, kd:REAL | BOOL |
TEENSY_PID_STOP | 0xE6 | name, id:INT | BOOL |
TEENSY_RTC_GET | 0xE7 | name | INT (epoch seconds) |
TEENSY_RTC_SET | 0xE8 | name, timestamp:INT | BOOL |
TEENSY_TRNG_READ | 0xE9 | name, len:INT | STRING (hex) |
TEENSY_TEMP_READ | 0xEA | name | INT (degrees C) |
TEENSY_DISTANCE | 0xEB | name, trigPin:INT, echoPin:INT | INT (mm) |
TEENSY_BOOTLOADER | 0xFF | name | BOOL |
June 2026 | ControlForge v1.0.1064 | Firmware: Rust (USB RawHID) | Teensy 4.0 (i.MX RT1062) @ 600 MHz 47 ST functions | Command opcodes 0xC0-0xFF
© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides