# 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][arduino-pico] and built with [PlatformIO]. PIO assembly is compiled at build time by `pioasm`, which ships with the framework. [arduino-pico]: https://github.com/earlephilhower/arduino-pico [PlatformIO]: https://platformio.org ### 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` ```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 ` Performs one SSI read. - `bits` — number of data bits to clock out, 1–32 - `half_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 `. ### 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 ```python 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.