Compare commits

..

2 commits

Author SHA1 Message Date
lucavanstraaten
a618cb238f LED: show encoder position as hue after each read
- Add setPositionColor(): maps SSI value to HSV hue (0=red, 120=green, 240=blue)
- LED takes the position hue as its resting colour between reads
- Blue during active read unchanged
- All error paths now flash red before printing ERR
2026-04-28 21:16:59 +02:00
lucavanstraaten
a8633962f2 Reimplement SSI read using RP2040 PIO state machine
Replace dual-core FIFO bit-bang approach with a dedicated PIO program:
- Add ssi.pio: PIO assembly for SSI master (side-set CLK, sample DATA)
- Add ssi.pio.h: pioasm-generated C header
- ssi_pio_init(): load program, wire CLK/DATA pins, set clock divider
- ssi_pio_reconfigure_speed(): live clock-divider update between reads
- ssi_pio_read(): push bit-count to TX FIFO, block on RX result word
- Remove dual-core headers, FIFO structs, setup1/loop1, requestSsiRead
- Bit limit reduced to 1..32 (single PIO ISR word)
- TX_DI pin ownership transferred to PIO (no pinMode/digitalWrite)
2026-04-28 21:12:54 +02:00
4 changed files with 142 additions and 368 deletions

285
README.md
View file

@ -1,285 +0,0 @@
# 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][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 <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
```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 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.

View file

@ -1,7 +1,8 @@
#include <Arduino.h> #include <Arduino.h>
#include <Adafruit_NeoPixel.h> #include <Adafruit_NeoPixel.h>
#include <hardware/structs/sio.h> #include <hardware/pio.h>
#include <pico/multicore.h> #include <hardware/clocks.h>
#include "ssi.pio.h" // generated from ssi.pio at build time
// CLOCK module (TX) // CLOCK module (TX)
const uint8_t TX_DI = 0; const uint8_t TX_DI = 0;
@ -17,24 +18,9 @@ const uint8_t RX_RO = 29;
const uint8_t LED_PIN = 16; const uint8_t LED_PIN = 16;
Adafruit_NeoPixel led(1, LED_PIN, NEO_GRB + NEO_KHZ800); Adafruit_NeoPixel led(1, LED_PIN, NEO_GRB + NEO_KHZ800);
// Pre-computed bit masks for the SIO registers. PIO ssi_pio = pio0;
// Writing to gpio_set sets a pin HIGH atomically in one cycle. uint ssi_sm = 0;
// Writing to gpio_clr clears it LOW. Reading gpio_in gives all GPIO states. uint ssi_offset = 0;
const uint32_t TX_DI_MASK = 1u << TX_DI;
const uint32_t RX_RO_MASK = 1u << RX_RO;
// Inter-core protocol
// Request: [bits:8 | half_us:16 | reserved:8] -> core 1
// Response: [hi32][lo32][duration_us] -> core 0
struct SsiRequest {
uint8_t bits;
uint16_t half_us;
};
struct SsiResponse {
uint64_t value;
uint32_t duration_us;
};
void setStatus(uint8_t r, uint8_t g, uint8_t b) { void setStatus(uint8_t r, uint8_t g, uint8_t b) {
led.setPixelColor(0, led.Color(r, g, b)); led.setPixelColor(0, led.Color(r, g, b));
@ -43,9 +29,10 @@ void setStatus(uint8_t r, uint8_t g, uint8_t b) {
// Map encoder position to a colour on the HSV wheel (brightness fixed at ~20/255). // Map encoder position to a colour on the HSV wheel (brightness fixed at ~20/255).
// hue 0-359: 0=red, 120=green, 240=blue // hue 0-359: 0=red, 120=green, 240=blue
void setPositionColor(uint64_t value, uint64_t maxVal) { void setPositionColor(uint32_t value, uint32_t maxVal) {
if (maxVal == 0) { setStatus(16, 0, 16); return; } // magenta = unknown range if (maxVal == 0) { setStatus(16, 0, 16); return; } // magenta = unknown range
uint16_t hue = (uint16_t)((value * 360) / maxVal); // 0-359 uint16_t hue = (uint16_t)(((uint32_t)value * 360) / maxVal); // 0-359
// HSV → RGB with S=1, V=20
uint8_t sector = hue / 60; uint8_t sector = hue / 60;
uint8_t frac = (uint8_t)(((hue % 60) * 255) / 60); uint8_t frac = (uint8_t)(((hue % 60) * 255) / 60);
uint8_t v = 20, p = 0, q = (uint8_t)(v * (255 - frac) / 255), t = (uint8_t)(v * frac / 255); uint8_t v = 20, p = 0, q = (uint8_t)(v * (255 - frac) / 255), t = (uint8_t)(v * frac / 255);
@ -59,66 +46,60 @@ void setPositionColor(uint64_t value, uint64_t maxVal) {
} }
} }
// ========================================================================= void ssi_pio_init(uint half_us) {
// CORE 1: SSI worker // pioasm generates these helpers in ssi.pio.h
// ========================================================================= ssi_offset = pio_add_program(ssi_pio, &ssi_master_program);
uint64_t ssi_read_core1(uint8_t bits, uint16_t half_us) { pio_sm_config c = ssi_master_program_get_default_config(ssi_offset);
uint64_t value = 0;
sio_hw->gpio_clr = TX_DI_MASK; // first falling edge: latch // Side-set drives the CLK pin (TX_DI)
busy_wait_us_32(half_us); sm_config_set_sideset_pins(&c, TX_DI);
for (uint8_t i = 0; i < bits; i++) { // 'in pins, 1' samples starting at RX_RO
sio_hw->gpio_set = TX_DI_MASK; // rising: encoder shifts sm_config_set_in_pins(&c, RX_RO);
busy_wait_us_32(half_us); sm_config_set_in_shift(&c, false /* shift_left */, false /* autopush */, 32);
sio_hw->gpio_clr = TX_DI_MASK; // falling: sample
uint32_t bit = (sio_hw->gpio_in & RX_RO_MASK) ? 1 : 0;
value = (value << 1) | bit;
busy_wait_us_32(half_us);
}
sio_hw->gpio_set = TX_DI_MASK; // back to idle // 'out x, 32' pulls 32 bits from OSR; shift direction doesn't matter for full word
busy_wait_us_32(30); // monoflop sm_config_set_out_shift(&c, true, false, 32);
return value; // Clock divider: 1 PIO cycle = half_us / 2 microseconds
// (so that 2 PIO cycles = half_us microseconds = one CLK half-period)
float div = (float)clock_get_hz(clk_sys) * ((float)half_us / 2.0f) / 1e6f;
sm_config_set_clkdiv(&c, div);
// Hand the CLK pin to PIO and set as output
pio_gpio_init(ssi_pio, TX_DI);
pio_sm_set_consecutive_pindirs(ssi_pio, ssi_sm, TX_DI, 1, true);
pio_sm_init(ssi_pio, ssi_sm, ssi_offset, &c);
pio_sm_set_enabled(ssi_pio, ssi_sm, true);
} }
void setup1() { void ssi_pio_reconfigure_speed(uint half_us) {
// Core 1 setup: nothing to do, GPIOs already configured by core 0. pio_sm_set_enabled(ssi_pio, ssi_sm, false);
// Importantly: no Serial, no USB, no millis IRQ active here by default
// when running in this dual-core mode. // Drain any leftover words from FIFOs
pio_sm_clear_fifos(ssi_pio, ssi_sm);
// Restart SM at the wrap target
pio_sm_restart(ssi_pio, ssi_sm);
pio_sm_clkdiv_restart(ssi_pio, ssi_sm);
float div = (float)clock_get_hz(clk_sys) * ((float)half_us / 2.0f) / 1e6f;
pio_sm_set_clkdiv(ssi_pio, ssi_sm, div);
// Jump SM back to the program start
pio_sm_exec(ssi_pio, ssi_sm, pio_encode_jmp(ssi_offset));
pio_sm_set_enabled(ssi_pio, ssi_sm, true);
} }
void loop1() { uint32_t ssi_pio_read(uint8_t bits) {
// Block until core 0 sends a packed request word. pio_sm_put_blocking(ssi_pio, ssi_sm, bits - 1);
// Word layout: bits in upper 8, half_us in next 16, 8 unused uint32_t result = pio_sm_get_blocking(ssi_pio, ssi_sm);
uint32_t req = rp2040.fifo.pop();
uint8_t bits = (req >> 24) & 0xFF;
uint16_t half_us = (req >> 8) & 0xFFFF;
uint32_t t0 = time_us_32(); // ISR shifts left → first bit in MSB. Right-align.
uint64_t value = ssi_read_core1(bits, half_us); return result >> (32 - bits);
uint32_t took = time_us_32() - t0;
// Push three words back: low32, high32, duration
rp2040.fifo.push((uint32_t)(value & 0xFFFFFFFF));
rp2040.fifo.push((uint32_t)(value >> 32));
rp2040.fifo.push(took);
}
// =========================================================================
// CORE 0: serial command handler
// =========================================================================
void requestSsiRead(uint8_t bits, uint16_t half_us, uint64_t& outValue, uint32_t& outTook) {
uint32_t req = ((uint32_t)bits << 24) | ((uint32_t)half_us << 8);
rp2040.fifo.push(req);
uint32_t lo = rp2040.fifo.pop();
uint32_t hi = rp2040.fifo.pop();
outTook = rp2040.fifo.pop();
outValue = ((uint64_t)hi << 32) | lo;
} }
void handleCommand(const String& cmd) { void handleCommand(const String& cmd) {
@ -139,9 +120,9 @@ void handleCommand(const String& cmd) {
int bits = cmd.substring(firstSpace + 1, secondSpace).toInt(); int bits = cmd.substring(firstSpace + 1, secondSpace).toInt();
int halfUs = cmd.substring(secondSpace + 1).toInt(); int halfUs = cmd.substring(secondSpace + 1).toInt();
if (bits < 1 || bits > 64) { if (bits < 1 || bits > 32) {
setStatus(16, 0, 0); // red = error setStatus(16, 0, 0); // red = error
Serial.println("ERR bits must be 1..64"); Serial.println("ERR bits must be 1..32 (PIO ISR limit)");
return; return;
} }
if (halfUs < 1 || halfUs > 10000) { if (halfUs < 1 || halfUs > 10000) {
@ -150,16 +131,17 @@ void handleCommand(const String& cmd) {
return; return;
} }
ssi_pio_reconfigure_speed(halfUs);
setStatus(0, 0, 16); // blue = reading setStatus(0, 0, 16); // blue = reading
uint64_t value; uint32_t t0 = micros();
uint32_t took; uint32_t value = ssi_pio_read((uint8_t)bits);
requestSsiRead((uint8_t)bits, (uint16_t)halfUs, value, took); uint32_t took = micros() - t0;
// Show encoder position as hue (full range = 2^bits - 1) // Show encoder position as hue (full range = 2^bits - 1)
uint64_t maxVal = (bits < 64) ? ((1ull << bits) - 1) : UINT64_MAX; setPositionColor(value, (1ul << bits) - 1);
setPositionColor(value, maxVal);
Serial.printf("OK bits=%d half_us=%d hex=0x%llX dec=%llu took=%luus\n", Serial.printf("OK bits=%d half_us=%d hex=0x%lX dec=%lu took=%luus\n",
bits, halfUs, value, value, took); bits, halfUs, value, value, took);
} }
@ -168,7 +150,6 @@ void setup() {
led.begin(); led.begin();
setStatus(8, 8, 0); setStatus(8, 8, 0);
pinMode(TX_DI, OUTPUT);
pinMode(TX_DE, OUTPUT); pinMode(TX_RE, OUTPUT); pinMode(TX_DE, OUTPUT); pinMode(TX_RE, OUTPUT);
pinMode(RX_DE, OUTPUT); pinMode(RX_RE, OUTPUT); pinMode(RX_DE, OUTPUT); pinMode(RX_RE, OUTPUT);
pinMode(RX_DI, OUTPUT); pinMode(RX_DI, OUTPUT);
@ -179,11 +160,14 @@ void setup() {
digitalWrite(RX_DE, LOW); digitalWrite(RX_DE, LOW);
digitalWrite(RX_RE, LOW); digitalWrite(RX_RE, LOW);
digitalWrite(RX_DI, LOW); digitalWrite(RX_DI, LOW);
digitalWrite(TX_DI, HIGH); // SSI idle HIGH
// TX_DI is owned by PIO - don't pinMode it
ssi_pio_init(5); // default 5 µs half-period
delay(200); delay(200);
while (!Serial && millis() < 3000) { delay(10); } while (!Serial && millis() < 3000) { delay(10); }
Serial.println("\nSSI bridge ready (dual-core: core 1 dedicated to SSI)"); Serial.println("\nSSI bridge ready (PIO state machine, .pio assembled)");
Serial.println("Send: READ <bits> <half_us>"); Serial.println("Send: READ <bits> <half_us>");
setStatus(0, 16, 0); setStatus(0, 16, 0);
} }

24
src/ssi.pio Normal file
View file

@ -0,0 +1,24 @@
.program ssi_master
.side_set 1
; Side-set bit drives CLK. Idle high.
;
; Protocol:
; TX FIFO: one word = (bit_count - 1)
; RX FIFO: one word = captured bits, left-aligned in 32-bit word
;
; Timing: 1 PIO cycle = half of one CLK level period.
; Each CLK level = 2 cycles, full bit period = 4 cycles.
.wrap_target
pull block side 1 ; wait for CPU command, CLK idle high
out x, 32 side 1 ; X = bit_count - 1
nop side 0 ; first falling edge: encoder latches
nop side 0 ; latch settle (CLK still low)
bit_loop:
nop side 1 ; CLK rises: encoder shifts new bit
nop side 1 ; data settles
in pins, 1 side 0 ; CLK falls, sample
jmp x--, bit_loop side 0 ; CLK still low, loop until done
push block side 1 ; CLK back to idle, return result
.wrap

51
src/ssi.pio.h Normal file
View file

@ -0,0 +1,51 @@
// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //
#pragma once
#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif
// ---------- //
// ssi_master //
// ---------- //
#define ssi_master_wrap_target 0
#define ssi_master_wrap 8
#define ssi_master_pio_version 0
static const uint16_t ssi_master_program_instructions[] = {
// .wrap_target
0x90a0, // 0: pull block side 1
0x7020, // 1: out x, 32 side 1
0xa042, // 2: nop side 0
0xa042, // 3: nop side 0
0xb042, // 4: nop side 1
0xb042, // 5: nop side 1
0x4001, // 6: in pins, 1 side 0
0x0044, // 7: jmp x--, 4 side 0
0x9020, // 8: push block side 1
// .wrap
};
#if !PICO_NO_HARDWARE
static const struct pio_program ssi_master_program = {
.instructions = ssi_master_program_instructions,
.length = 9,
.origin = -1,
.pio_version = ssi_master_pio_version,
#if PICO_PIO_VERSION > 0
.used_gpio_ranges = 0x0
#endif
};
static inline pio_sm_config ssi_master_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + ssi_master_wrap_target, offset + ssi_master_wrap);
sm_config_set_sideset(&c, 1, false, false);
return c;
}
#endif