Skip to main content

ControlForge + Waveshare RP2040-Zero: Hardware Interface Guide

James M. Belcher Founder, JMB Technical Services LLC April 2026 | ControlForge v1.0.533


1. Architecture Overview

ControlForge treats the RP2040-Zero as a smart I/O module — not a compilation target. The board runs a precompiled Rust firmware that you flash once via UF2. All hardware control flows through USB CDC serial using a binary frame protocol, identical in concept to the Arduino and Teensy drivers.

The RP2040-Zero is a compact board based on the Raspberry Pi RP2040 chip with an onboard NeoPixel LED, making it ideal for small-footprint I/O expansion.

System Diagram


2. Device Lifecycle

RP2040_INIT — Connect to Board

ok := RP2040_INIT('rp', '/dev/ttyACM0');
ParamTypeDescription
nameSTRINGDevice handle name (used by all other RP2040_* calls)
portSTRINGSerial port path

Port Discovery: Use SERIAL_FIND('RP2040') or SERIAL_PORTS() to locate the device automatically.

RP2040_STATUS — Connection Health

status := RP2040_STATUS('rp');
(* Returns JSON: {"connected":true,"ping_us":280,...} *)

RP2040_CLOSE — Disconnect

ok := RP2040_CLOSE('rp');

RP2040_BOOTLOADER — Enter UF2 Flash Mode

ok := RP2040_BOOTLOADER('rp');

Reboots the RP2040 into UF2 bootloader mode for firmware updates. The device disconnects — you must re-flash and call RP2040_INIT again to reconnect.

Example: Safe Init with Port Discovery

PROGRAM POU_RP2040Init
VAR
port : STRING;
ok : BOOL;
state : INT := 0;
END_VAR

CASE state OF
0: (* Find RP2040 *)
port := SERIAL_FIND('RP2040');
IF LEN(port) > 0 THEN
state := 1;
END_IF;

1: (* Connect *)
ok := RP2040_INIT('rp', port);
IF ok THEN
state := 10;
END_IF;

10: (* Ready for I/O *)
(* ... *)
END_CASE;
END_PROGRAM

3. Digital I/O

The RP2040-Zero has 29 GPIO pins (GP0-GP28).

RP2040_PIN_MODE — Configure Pin Direction

ParamTypeValues
nameSTRINGDevice handle
pinINT0-28
modeINT0=INPUT, 1=OUTPUT, 2=INPUT_PULLUP, 3=PWM, 4=ADC
RP2040_PIN_MODE('rp', 15, 1); (* Output *)
RP2040_PIN_MODE('rp', 14, 2); (* Input with pull-up *)

RP2040_DIGITAL_READ — Read Digital State

sensor := RP2040_DIGITAL_READ('rp', 14);
(* Returns: TRUE or FALSE *)

Auto-configures pin as input if not already set.

RP2040_DIGITAL_WRITE — Set Digital Output

RP2040_DIGITAL_WRITE('rp', 15, TRUE); (* High *)
RP2040_DIGITAL_WRITE('rp', 15, FALSE); (* Low *)

Auto-configures pin as output if not already set.

RP2040_RESET_PINS — Release All Pins

ok := RP2040_RESET_PINS('rp');

Returns all pins to their default (unconfigured) state.


4. Analog I/O

4.1 Analog Input (ADC)

4 ADC-capable pins: GP26, GP27, GP28, GP29. 12-bit resolution scaled to 16-bit (0-65535).

raw := RP2040_ANALOG_READ('rp', 26); (* GP26 / A0 — returns 0-65535 *)
raw := RP2040_ANALOG_READ('rp', 27); (* GP27 / A1 *)
ParamTypeDescription
nameSTRINGDevice handle
pinINT26-29 (ADC-capable pins only)

Voltage: The RP2040 ADC reference is 3.3V. Voltage = raw * 3.3 / 65535.

4.2 PWM Output

Any GPIO pin can output PWM. 16-bit duty resolution (0-65535).

RP2040_PWM_WRITE('rp', 15, 32768); (* 50% duty *)
RP2040_PWM_WRITE('rp', 15, 65535); (* Full on *)
RP2040_PWM_WRITE('rp', 15, 0); (* Off *)
ParamTypeDescription
nameSTRINGDevice handle
pinINTAny GPIO pin
dutyINT0-65535 (16-bit)

5. NeoPixel

The RP2040-Zero has an onboard WS2812B NeoPixel on GP16.

RP2040_NEOPIXEL — Set Onboard LED Color

RP2040_NEOPIXEL('rp', 255, 0, 0); (* Red *)
RP2040_NEOPIXEL('rp', 0, 255, 0); (* Green *)
RP2040_NEOPIXEL('rp', 0, 0, 255); (* Blue *)
RP2040_NEOPIXEL('rp', 0, 0, 0); (* Off *)
ParamTypeDescription
nameSTRINGDevice handle
rINTRed (0-255)
gINTGreen (0-255)
bINTBlue (0-255)

RP2040_NEO_STRIP — Drive External NeoPixel Strip

Drives up to 64 NeoPixels on GP16. Colors are provided as a hex string with one RGB triplet per LED.

(* 3 LEDs: red, green, blue *)
ok := RP2040_NEO_STRIP('rp', 3, 'FF0000 00FF00 0000FF');

(* 5 LEDs: all white at half brightness *)
ok := RP2040_NEO_STRIP('rp', 5, '808080 808080 808080 808080 808080');
ParamTypeDescription
nameSTRINGDevice handle
num_ledsINTNumber of LEDs (1-64)
colorsSTRINGSpace-separated hex RGB values per LED

6. I2C

The RP2040-Zero uses I2C0 on GP4 (SDA) and GP5 (SCL).

RP2040_I2C_SCAN — Scan Bus for Devices

devices := RP2040_I2C_SCAN('rp');
(* Returns: "0x3C,0x68" — comma-separated hex addresses *)

RP2040_I2C_WRITE_BYTE — Write Single Byte

ok := RP2040_I2C_WRITE_BYTE('rp', 16#3C, 16#AE);
ParamTypeDescription
nameSTRINGDevice handle
addrINT7-bit I2C device address
valueINTByte to write (0-255)

RP2040_I2C_READ_BYTE — Read Single Byte

val := RP2040_I2C_READ_BYTE('rp', 16#48);
(* Returns: byte value 0-255, or -1 on error *)

RP2040_I2C_WRITE_READ — Write Register Then Read

data := RP2040_I2C_WRITE_READ('rp', 16#48, 16#00, 2);
(* Writes register 0x00 to device 0x48, reads back 2 bytes *)
(* Returns: comma-separated decimal values, e.g. "12,128" *)
ParamTypeDescription
nameSTRINGDevice handle
addrINT7-bit I2C device address
regINTRegister address to write first
read_lenINTNumber of bytes to read

7. SPI

Fixed pinout: GP10 (SCK), GP11 (MOSI), GP12 (MISO), GP13 (CS).

RP2040_SPI_TRANSFER — Full-Duplex Transfer

(* Send 3 bytes, receive 3 bytes simultaneously *)
rx := RP2040_SPI_TRANSFER('rp', 'FF 00 A5');
(* Returns: hex string of received bytes, e.g. "00 42 FF" *)
ParamTypeDescription
nameSTRINGDevice handle
dataSTRINGSpace-separated hex bytes to send

8. UART

Two UART channels with fixed pin assignments:

ChannelTXRX
0GP0GP1
1GP4GP5

RP2040_UART_INIT — Initialize UART Channel

ok := RP2040_UART_INIT('rp', 0, 9600); (* Channel 0 at 9600 baud *)
ok := RP2040_UART_INIT('rp', 1, 115200); (* Channel 1 at 115200 baud *)
ParamTypeDescription
nameSTRINGDevice handle
channelINT0 or 1
baudINTBaud rate

RP2040_UART_SEND — Send Data

ok := RP2040_UART_SEND('rp', 0, 'Hello World');

RP2040_UART_RECV — Receive Data (Non-Blocking)

data := RP2040_UART_RECV('rp', 0, 64);
(* Returns: received string, or empty if nothing available *)
ParamTypeDescription
nameSTRINGDevice handle
channelINT0 or 1
max_lenINTMaximum bytes to read

9. Servo

Standard hobby servos on any GPIO pin. Uses 50Hz PWM with 1-2ms pulse width.

RP2040_SERVO('rp', 15, 90); (* Center *)
RP2040_SERVO('rp', 15, 0); (* Min position *)
RP2040_SERVO('rp', 15, 180); (* Max position *)
ParamTypeDescription
nameSTRINGDevice handle
pinINTGPIO pin
angleINTPosition in degrees (0-180)

10. Sensors

RP2040_TEMP_READ — Internal Temperature Sensor

raw := RP2040_TEMP_READ('rp');
(* Returns: temperature in degrees C x 100 *)
(* Example: 2534 = 25.34 degrees C *)

temp_c := INT_TO_REAL(raw) / 100.0;

RP2040_DISTANCE — HC-SR04 Ultrasonic Distance

dist_mm := RP2040_DISTANCE('rp', 7, 8);
(* Returns: distance in millimeters, 0 on error *)
ParamTypeDescription
nameSTRINGDevice handle
trig_pinINTTrigger pin (output)
echo_pinINTEcho pin (input)

11. OLED Display (SSD1306)

Drives an SSD1306 128x64 OLED via I2C (GP4=SDA, GP5=SCL).

RP2040_OLED_INIT — Initialize Display

ok := RP2040_OLED_INIT('rp', 16#3C); (* Standard address 0x3C *)

RP2040_OLED_CLEAR — Clear Screen

ok := RP2040_OLED_CLEAR('rp', 16#3C);

RP2040_OLED_PRINT — Print Text

RP2040_OLED_PRINT('rp', 16#3C, 0, 'Temperature:');
RP2040_OLED_PRINT('rp', 16#3C, 1, '25.3 C');
RP2040_OLED_PRINT('rp', 16#3C, 3, 'ControlForge Running');
ParamTypeDescription
nameSTRINGDevice handle
addrINTI2C address (typically 0x3C)
rowINTText row (0-7, 8 rows of 5x7 font)
textSTRINGText to display (21 chars per line max)

12. Complete Example: Sensor Station

PROGRAM POU_RP2040Station
VAR
state : INT := 0;
port : STRING;
ok : BOOL;
scan_count : DINT := 0;

(* Sensors *)
distance_mm : INT;
light_raw : INT;
temp_raw : INT;
temp_c : REAL;

(* Outputs *)
led_duty : INT;
msg : STRING;
END_VAR

CASE state OF
0: (* Discover and connect *)
port := SERIAL_FIND('RP2040');
IF LEN(port) > 0 THEN
ok := RP2040_INIT('rp', port);
IF ok THEN state := 1; END_IF;
END_IF;

1: (* Configure pins + OLED *)
RP2040_PIN_MODE('rp', 15, 1); (* LED output *)
RP2040_PIN_MODE('rp', 14, 2); (* Button input w/ pull-up *)
RP2040_OLED_INIT('rp', 16#3C);
RP2040_OLED_CLEAR('rp', 16#3C);
RP2040_OLED_PRINT('rp', 16#3C, 0, 'ControlForge RP2040 Station');
state := 10;

10: (* Main loop *)
scan_count := scan_count + 1;

(* Internal temperature *)
temp_raw := RP2040_TEMP_READ('rp');
temp_c := INT_TO_REAL(temp_raw) / 100.0;

(* Light sensor on GP26 *)
light_raw := RP2040_ANALOG_READ('rp', 26);

(* Ultrasonic distance *)
distance_mm := RP2040_DISTANCE('rp', 7, 8);

(* NeoPixel: green = close, red = far *)
IF distance_mm > 0 AND distance_mm < 500 THEN
RP2040_NEOPIXEL('rp', 0, 255, 0);
ELSIF distance_mm > 0 THEN
RP2040_NEOPIXEL('rp', 255, 0, 0);
ELSE
RP2040_NEOPIXEL('rp', 0, 0, 50); (* Blue = no reading *)
END_IF;

(* PWM LED brightness from light sensor *)
led_duty := 65535 - light_raw;
RP2040_PWM_WRITE('rp', 15, led_duty);

(* Update OLED every 50 scans *)
IF (scan_count MOD 50) = 0 THEN
msg := CONCAT('Temp: ', REAL_TO_STRING(temp_c), ' C');
RP2040_OLED_PRINT('rp', 16#3C, 2, msg);
msg := CONCAT('Dist: ', INT_TO_STRING(distance_mm), ' mm');
RP2040_OLED_PRINT('rp', 16#3C, 3, msg);
msg := CONCAT('Light: ', INT_TO_STRING(light_raw));
RP2040_OLED_PRINT('rp', 16#3C, 4, msg);
END_IF;

(* Heartbeat *)
RP2040_DIGITAL_WRITE('rp', 15, (scan_count MOD 10) < 5);
END_CASE;
END_PROGRAM

13. Hardware Notes

Pin Constraints

  • GP0/GP1: UART0 TX/RX. Available for GPIO if UART0 not used.
  • GP4/GP5: I2C0 SDA/SCL and UART1 TX/RX. Shared — use one or the other.
  • GP10-GP13: SPI0 pins. Available for GPIO if SPI not used.
  • GP16: Onboard NeoPixel. Also used for external NeoPixel strips.
  • GP26-GP29: ADC-capable. Can also be used as digital GPIO.
  • GP23-GP25: Used internally on some RP2040 boards — check your specific board pinout.

ADC Resolution

The RP2040 has a 12-bit ADC (0-4095) but the firmware scales to 16-bit (0-65535) for consistency with other ControlForge hardware drivers.

USB CDC Serial

  • Port: Typically /dev/ttyACM0 on Linux.
  • Protocol: Binary frames with CRC-16 (same structure as Arduino/Teensy drivers).
  • Firmware: Rust-based, flashed via UF2.

Power

  • USB power: 5V from host, 3.3V logic on all GPIO.
  • Current: Max ~300mA total from 3.3V regulator. Budget for NeoPixels (60mA per LED at full white).
  • Servo power: Use external supply for servos — do not power from the board's 3.3V.

Appendix A: Function Quick Reference

FunctionParametersReturnsDescription
RP2040_INIT(name, port)BOOLConnect to RP2040 board
RP2040_CLOSE(name)BOOLDisconnect
RP2040_STATUS(name)STRINGJSON status and board info
RP2040_BOOTLOADER(name)BOOLEnter UF2 flash mode
RP2040_PIN_MODE(name, pin, mode)BOOLSet pin: 0=IN, 1=OUT, 2=PULLUP, 3=PWM, 4=ADC
RP2040_DIGITAL_READ(name, pin)BOOLRead digital pin
RP2040_DIGITAL_WRITE(name, pin, value)BOOLWrite digital pin
RP2040_ANALOG_READ(name, pin)INTRead ADC (pins 26-29, 0-65535)
RP2040_PWM_WRITE(name, pin, duty)BOOLSet PWM duty (0-65535)
RP2040_NEOPIXEL(name, r, g, b)BOOLSet onboard NeoPixel color
RP2040_NEO_STRIP(name, num_leds, colors)BOOLDrive NeoPixel strip (GP16, max 64)
RP2040_TEMP_READ(name)INTInternal temp (degrees C x 100)
RP2040_RESET_PINS(name)BOOLRelease all pins to default
RP2040_I2C_SCAN(name)STRINGComma-separated hex addresses
RP2040_I2C_WRITE_BYTE(name, addr, value)BOOLWrite byte to I2C device
RP2040_I2C_READ_BYTE(name, addr)INTRead byte (-1 on error)
RP2040_I2C_WRITE_READ(name, addr, reg, read_len)STRINGWrite register, read N bytes
RP2040_SPI_TRANSFER(name, hex_data)STRINGFull-duplex SPI transfer
RP2040_SERVO(name, pin, angle)BOOLSet servo angle (0-180)
RP2040_DISTANCE(name, trig_pin, echo_pin)INTHC-SR04 distance in mm
RP2040_UART_INIT(name, channel, baud)BOOLInit UART (ch 0: GP0/1, ch 1: GP4/5)
RP2040_UART_SEND(name, channel, data)BOOLSend string via UART
RP2040_UART_RECV(name, channel, max_len)STRINGReceive from UART (non-blocking)
RP2040_OLED_INIT(name, addr)BOOLInit SSD1306 OLED
RP2040_OLED_CLEAR(name, addr)BOOLClear OLED screen
RP2040_OLED_PRINT(name, addr, row, text)BOOLPrint text at row (0-7)

ControlForge v1.0.533 | Firmware: Rust (UF2) | Waveshare RP2040-Zero @ 133 MHz Protocol: Binary frame (CRC-16) over USB CDC

© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides