| .vscode | ||
| include | ||
| lib | ||
| src | ||
| test | ||
| .gitignore | ||
| platformio.ini | ||
| README.md | ||
ssi-pico-bridge
A DIY reader that connects SSI (Synchronous Serial Interface) absolute encoders to a PC over USB, using a Raspberry Pi Pico (RP2040) and two cheap MAX485 modules.
Built because off-the-shelf SSI interfaces are expensive and most general-purpose serial devices can't talk SSI: SSI is a synchronous, clocked protocol with two differential pairs (CLOCK and DATA). This project bridges the gap with about €5 of parts and exposes the encoder's position as plain text over a USB serial port — easy to script from Python, log to disk, or feed into any other tool.
How it works
The Pico acts as the SSI master: it drives the CLOCK line, samples DATA on each falling edge, and assembles the position word. Two MAX485 modules act as RS-422 line drivers/receivers — one transmits the clock to the encoder, the other receives the data from it. The Pico exposes a simple text protocol over USB-CDC serial that lets a host request a read of N bits at a chosen clock speed.
┌──────────────────────────────────────────────────┐
│ │
┌──────────┐ │ ┌───────────┐ CLK+/CLK- ┌─────────────┐ │
│ │ CLK ─┤ MAX485 #1 ├──────────────────┤ │ │
│ RP2040 │ │ │ (TX only) │ (twisted pair) │ │ │
│ │ │ └───────────┘ │ │ │
│ Pico │ │ │ SSI Encoder │ │
│ │ │ ┌───────────┐ DATA+/DATA- │ │ │
│ │ DATA─┤ MAX485 #2 ├──────────────────┤ │ │
│ │ │ │ (RX only) │ (twisted pair) │ │ │
└──────────┘ │ └───────────┘ └─────────────┘ │
│ └──────────────────────────────────────────────────┘
│
│ USB
│ "READ 25 5\n" →
│ ← "OK bits=25 hex=0x1A2B3C4 ..."
▼
PC
Features
- Reads any standard SSI encoder, 1–32 bits per frame
- Configurable clock speed (1 µs to 10 ms half-period)
- PIO-based timing with sub-100 ns jitter
- Works with both Gray-coded and binary encoders (decoding done host-side
or in firmware — see
examples/) - USB-CDC serial — appears as a plain COM port on Windows /
/dev/ttyACM*on Linux //dev/cu.usbmodem*on macOS - WS2812 status LED for at-a-glance health
- Simple ASCII protocol — easy to script from Python, Node, shell, etc.
Hardware
Bill of materials
| Qty | Part | Where to buy | Approx. price |
|---|---|---|---|
| 1 | Raspberry Pi Pico / RP2040-Zero / RP2040 dev board | TinyTronics, Kiwi Electronics, Opencircuit | €5 |
| 2 | MAX485 module (HW-097 or equivalent, blue PCB) | Opencircuit, AliExpress | €1.60 each |
| 1 | Breadboard + jumper wires | any electronics shop | €5 |
| 1 | SSI encoder | varies | varies |
| - | Twisted-pair cable for the differential lines (Cat5e is fine) |
The MAX485 modules typically have a 120 Ω termination resistor already on board. This is correct for the DATA receiver and harmless on the CLOCK driver over short cables.
Pin assignments
The reference firmware uses the following GPIO assignments on the RP2040.
CLOCK module (MAX485 #1, transmit only)
| MAX485 pin | Connects to | Notes |
|---|---|---|
| VCC | Pico VBUS (5 V) | MAX485 needs 5 V; RP2040 GPIOs are 5 V tolerant |
| GND | Pico GND | shared ground is required |
| DI | Pico GP0 | clock output, driven by PIO |
| DE | Pico GP1 | tied HIGH (driver always on) |
| RE | Pico GP2 | tied HIGH (receiver disabled) |
| RO | Pico GP3 | unused |
| A | Encoder CLK+ | twisted pair to encoder |
| B | Encoder CLK– | twisted pair to encoder |
DATA module (MAX485 #2, receive only)
| MAX485 pin | Connects to | Notes |
|---|---|---|
| VCC | Pico VBUS (5 V) | |
| GND | Pico GND | |
| DI | Pico GP26 | unused, parked LOW |
| DE | Pico GP27 | tied LOW (driver disabled, A/B in high-Z) |
| RE | Pico GP28 | tied LOW (receiver always on) |
| RO | Pico GP29 | data input, sampled by PIO |
| A | Encoder DATA+ | twisted pair to encoder |
| B | Encoder DATA– | twisted pair to encoder |
Other connections
| Pin | Function |
|---|---|
| GP16 | WS2812 status LED (on RP2040-Zero this is the on-board NeoPixel) |
| Encoder GND | Must be tied to Pico GND |
| Encoder V+ | Powered separately according to its datasheet (often 5 V or 10–30 V) |
A note on differential pair polarity
If you wire the encoder's CLK+/CLK– to A/B by name and get garbage data, swap them — RS-422/485 polarity conventions vary between manufacturers and some label A and B opposite to what the MAX485 expects. The DATA pair can also be swapped if needed; that simply inverts the bits, which is easy to detect in the captured value.
Software
Build environment
The firmware is written in C++ for the Arduino-Pico core and
built with PlatformIO. PIO assembly is compiled at build time by pioasm,
which ships with the framework.
Building and flashing
- Clone this repository.
- Open the folder in VS Code with the PlatformIO extension installed,
or run
pio runfrom the command line. - Hold BOOT on the RP2040 and plug in USB for the first flash.
- Run
pio run -t upload.
After the first flash, subsequent uploads use picotool over USB and don't
need the BOOT button — PlatformIO resets the board into bootloader mode
automatically.
platformio.ini
[env:rp2040zero]
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board = waveshare_rp2040_zero
framework = arduino
board_build.core = earlephilhower
upload_protocol = picotool
monitor_speed = 115200
lib_deps =
adafruit/Adafruit NeoPixel @ ^1.12.0
Project layout
ssi-pico-bridge/
├── platformio.ini
├── README.md
└── src/
├── main.cpp # entry point, serial protocol handler
└── ssi.pio # PIO state machine for SSI master timing
Connecting from a PC
After flashing, plug the Pico into your PC over USB. It enumerates as a USB-CDC serial device:
- Windows — appears as a new
COMport (e.g.COM5). Check Device Manager. - Linux —
/dev/ttyACM0(or higher number if you have other ACM devices). - macOS —
/dev/cu.usbmodemXXXX.
Open it at 115200 baud, 8N1 with any serial terminal (PuTTY, screen, minicom, the Arduino IDE's Serial Monitor, PlatformIO's monitor, etc.) and start sending commands.
Serial protocol
Commands
READ <bits> <half_us>
Performs one SSI read.
bits— number of data bits to clock out, 1–32half_us— duration of one CLK half-period in microseconds, 1–10000
Example:
> READ 25 5
< OK bits=25 half_us=5 hex=0x1A2B3C4 dec=27439556 took=292us
A successful read replies with OK followed by the parameters echoed back,
the captured value as both hex and decimal, and the wall-clock time the read
took. Errors reply with ERR <message>.
Notes for typical encoders
| Encoder type | Bits | Suggested half_us |
|---|---|---|
| 13-bit single-turn | 13 | 5 |
| 25-bit multi-turn (12 turns + 13 single) | 25 | 5 |
| 24-bit BiSS-C-compatible | 24 + CRC | check datasheet |
Many encoders are Gray-coded; the host or firmware must convert to binary
before the value represents a usable position. See examples/gray_to_binary.py.
Example: Python host
import serial
ssi = serial.Serial('/dev/ttyACM0', 115200, timeout=1)
def read_position(bits=25, half_us=5):
ssi.write(f"READ {bits} {half_us}\n".encode())
line = ssi.readline().decode().strip()
if not line.startswith("OK"):
raise RuntimeError(line)
parts = dict(p.split('=') for p in line.split()[1:])
raw = int(parts['hex'], 16)
return gray_to_binary(raw) # if your encoder is Gray-coded
def gray_to_binary(g):
b = g
while g := g >> 1:
b ^= g
return b
while True:
print(read_position())
Status LED
The on-board WS2812 indicates state:
- Yellow — booting
- Green — idle, ready
- Blue — read in progress
- Red — last operation failed (loopback test only)
Troubleshooting
All bits read as 0 or 0xFFFF... when no encoder is connected. This is correct — the MAX485 receiver biases the line to one rail when nothing is driving it. Real data only appears once the encoder is wired up and powered.
Encoder reads return constant nonsense values. Most likely the differential pair polarity is reversed on one of the lines. Swap A and B on the affected module.
Reads work but the number doesn't match shaft rotation. Check the
bits parameter against the encoder's datasheet — many encoders include
a leading zero or trailing alarm/parity bit that you must mask off. If the
number changes monotonically with rotation but jumps around in unexpected
ways, the encoder is probably Gray-coded; convert before interpreting.
Reads occasionally fail or return garbage at high speeds. Try a larger
half_us value. Encoder cable length, twisted-pair quality, and
termination all affect maximum reliable clock speed. SSI specs typically
allow up to 1–2 MHz; 100–200 kHz is a safe starting point.
The Pico hangs after the first read. PIO state-machine reconfiguration between reads of different speeds requires a clean restart. Make sure you're on the latest firmware — early versions had a FIFO drain bug.
The serial port doesn't show up after flashing. The first flash via BOOTSEL exposes the Pico as a mass-storage device, not a serial port. After the firmware is running, unplug and replug the USB cable — it should then enumerate as a COM/ACM device.
License
MIT — do whatever you want with this, attribution appreciated but not required.
Acknowledgements
Born out of frustration trying to read SSI absolute encoders without paying for an industrial gateway, and the realization that the cheapest commercial solution costs more than this entire build does ten times over.