SSI_RP2040_Encoder_Link/README.md
2026-04-30 10:05:06 +02:00

11 KiB
Raw Blame History

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, 132 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 1030 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

  1. Clone this repository.
  2. Open the folder in VS Code with the PlatformIO extension installed, or run pio run from the command line.
  3. Hold BOOT on the RP2040 and plug in USB for the first flash.
  4. 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 COM port (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, 132
  • half_us — duration of one CLK half-period in microseconds, 110000

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 12 MHz; 100200 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.