Skip to main content

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.

FeatureSpecification
MCUNXP i.MX RT1062, ARM Cortex-M7 @ 600 MHz
Flash1 MB (+ 8 MB QSPI on Teensy 4.0)
RAM512 KB tightly-coupled + 512 KB general
Digital Pins40 (all 3.3V, 5V tolerant on most)
Analog Inputs14 (10-bit default, configurable to 12-bit)
PWM Outputs31 (FlexPWM with complementary pairs and fault inputs)
CAN Buses3 (CAN 2.0B, FlexCAN hardware)
Serial Ports7 (hardware UART)
EncoderHardware quadrature decoder
RTCBattery-backed real-time clock
TRNGHardware 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 RawHIDUSB Serial
FramingFixed 64-byte packetsByte stream — need sync bytes, escape sequences
ReliabilityUSB guarantees deliveryBytes can be lost if buffer overflows
Latency1 ms poll interval (USB full-speed)Variable — depends on OS buffering, FTDI latency timer
Multi-deviceEach Teensy gets unique hidraw deviceSerial ports can shuffle on reboot
No driver neededLinux 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

ParamTypeValues
nameSTRINGDevice name
pinINT0-39
modeINT0=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

ParamTypeDescription
pinINTPWM-capable pin
freqINTFrequency in Hz
resolutionINTBit 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.

ParamTypeDescription
pinHINTHigh-side PWM pin
pinLINTLow-side PWM pin (complementary)
freqHzINTSwitching frequency in Hz
deadtimeNsINTDead 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 with TEENSY_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

ParamTypeDescription
pinINTServo signal pin
angleINTPosition 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

ParamTypeDescription
portINTSPI port (0 = default SPI bus)
hexDataSTRINGHex-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

ParamTypeDescription
channelINTChannel 1-7 (Serial1-Serial7, fixed default pins)
baudRateINTBaud 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.

ParamTypeDescription
idINTAcceptance ID
maskINTBit 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

ParamTypeDescription
idINTLoop identifier (0, 1, 2, …)
inputPinINTAnalog input pin (process variable)
outputPinINTPWM output pin (control variable)
kpREALProportional gain
kiREALIntegral gain
kdREALDerivative gain
rateHzINTLoop rate in Hz (firmware tick)
outMinINTMinimum output value
outMaxINTMaximum 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

ParamTypeDescription
trigINTTrigger pin
echoINTEcho 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_INIT again 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

TierWhere It RunsLatencyUse Case
Teensy firmware (Rust)On-chip, Cortex-M7~1.7 ns/cycle (600 MHz)PID loop, PWM fault shutdown, encoder counting
FlexPWM hardwarePeripheral siliconSub-nanosecondDead time insertion, complementary outputs, fault response
USB RawHIDHost ↔ Teensy1-2 ms round-tripCommand/response, sensor reads
ControlForge scanHost CPU, ST interpreter1-50 msState machines, sequencing, CAN orchestration
ControlForge bossCluster coordination10-100 msHMI, 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:
    SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="0486", MODE="0666"
    ControlForge's install script creates this rule automatically.
  • 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

  1. Call TEENSY_BOOTLOADER('t1') to enter bootloader mode (or press the physical button on the Teensy).
  2. Flash new firmware with teensy_loader_cli --mcu=TEENSY40 firmware.hex or the Teensy Loader GUI.
  3. Teensy reboots with new firmware.
  4. 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

FunctionOpcodeParametersReturns
TEENSY_INITname:STRING, path:STRINGBOOL
TEENSY_CLOSEname:STRINGBOOL
TEENSY_STATUSname:STRINGSTRING
TEENSY_PIN_MODE0xC0name, pin:INT, mode:INTBOOL
TEENSY_DIGITAL_READ0xC1name, pin:INTBOOL
TEENSY_DIGITAL_WRITE0xC2name, pin:INT, value:BOOLBOOL
TEENSY_RESET_PINS0xC3nameBOOL
TEENSY_ANALOG_READ0xC4name, pin:INTINT
TEENSY_PWM_WRITE0xC5name, pin:INT, duty:INTBOOL
TEENSY_PWM_CONFIG0xC6name, pin:INT, freqHz:INT, alignment:INTBOOL
TEENSY_PWM_PAIR0xC7name, pinH:INT, pinL:INT, freqHz:INT, deadtimeNs:INTBOOL
TEENSY_PWM_FAULT0xC8name, faultPin:INTBOOL
TEENSY_SERVO0xC9name, pin:INT, angle:INTBOOL
TEENSY_I2C_SCAN0xCAname, port:INTSTRING
TEENSY_I2C_WRITE0xCBname, port:INT, addr:INT, value:INTBOOL
TEENSY_I2C_READ0xCCname, port:INT, addr:INTINT
TEENSY_I2C_WRITE_READ0xCDname, port:INT, addr:INT, reg:INT, readLen:INTSTRING (hex)
TEENSY_SPI_TRANSFER0xCEname, port:INT, hexData:STRINGSTRING (hex)
TEENSY_UART_INIT0xD0name, channel:INT, baudRate:INTBOOL
TEENSY_UART_SEND0xD1name, channel:INT, data:STRINGBOOL
TEENSY_UART_RECV0xD2name, channel:INT, maxLen:INTSTRING (hex)
TEENSY_OLED_INIT0xD3name, addr:INTBOOL
TEENSY_OLED_CLEAR0xD4name, addr:INTBOOL
TEENSY_OLED_PRINT0xD5name, addr:INT, row:INT, text:STRINGBOOL
TEENSY_NEOPIXEL0xD6name, r:INT, g:INT, b:INTBOOL
TEENSY_NEO_STRIP0xD7name, numLeds:INT, hexColors:STRINGBOOL
TEENSY_CAN_INIT0xD8name, port:INT, baudRate:INTBOOL
TEENSY_CAN_SEND0xD9name, port:INT, id:INT, hexData:STRINGBOOL
TEENSY_CAN_RECV0xDAname, port:INT, maxMsgs:INTSTRING (JSON)
TEENSY_CAN_FILTER0xDBname, port:INT, id:INT, mask:INTBOOL
TEENSY_CAN_STATUS0xDCname, port:INTSTRING (JSON)
TEENSY_ENCODER_INIT0xDDname, pinA:INT, pinB:INTBOOL
TEENSY_ENCODER_READ0xDEname, pinA:INTINT (signed)
TEENSY_ENCODER_RESET0xDFname, pinA:INTBOOL
TEENSY_FREQ_INIT0xE0name, pin:INTBOOL
TEENSY_FREQ_READ0xE1name, pin:INTINT (Hz)
TEENSY_PID_CONFIG0xE2name, id:INT, inputPin:INT, outputPin:INT, kp:REAL, ki:REAL, kd:REAL, rateHz:INT, outMin:INT, outMax:INTBOOL
TEENSY_PID_SETPOINT0xE3name, id:INT, setpoint:REALBOOL
TEENSY_PID_READ0xE4name, id:INTSTRING (JSON)
TEENSY_PID_TUNE0xE5name, id:INT, kp:REAL, ki:REAL, kd:REALBOOL
TEENSY_PID_STOP0xE6name, id:INTBOOL
TEENSY_RTC_GET0xE7nameINT (epoch seconds)
TEENSY_RTC_SET0xE8name, timestamp:INTBOOL
TEENSY_TRNG_READ0xE9name, len:INTSTRING (hex)
TEENSY_TEMP_READ0xEAnameINT (degrees C)
TEENSY_DISTANCE0xEBname, trigPin:INT, echoPin:INTINT (mm)
TEENSY_BOOTLOADER0xFFnameBOOL

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