Compare commits
2 commits
main
...
pio-implem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a618cb238f | ||
|
|
a8633962f2 |
4 changed files with 142 additions and 368 deletions
285
README.md
285
README.md
|
|
@ -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, 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 <bits> <half_us>`
|
||||
|
||||
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 <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 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.
|
||||
150
src/main.cpp
150
src/main.cpp
|
|
@ -1,7 +1,8 @@
|
|||
#include <Arduino.h>
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include <hardware/structs/sio.h>
|
||||
#include <pico/multicore.h>
|
||||
#include <hardware/pio.h>
|
||||
#include <hardware/clocks.h>
|
||||
#include "ssi.pio.h" // generated from ssi.pio at build time
|
||||
|
||||
// CLOCK module (TX)
|
||||
const uint8_t TX_DI = 0;
|
||||
|
|
@ -17,24 +18,9 @@ const uint8_t RX_RO = 29;
|
|||
const uint8_t LED_PIN = 16;
|
||||
Adafruit_NeoPixel led(1, LED_PIN, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
// Pre-computed bit masks for the SIO registers.
|
||||
// Writing to gpio_set sets a pin HIGH atomically in one cycle.
|
||||
// Writing to gpio_clr clears it LOW. Reading gpio_in gives all GPIO states.
|
||||
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;
|
||||
};
|
||||
PIO ssi_pio = pio0;
|
||||
uint ssi_sm = 0;
|
||||
uint ssi_offset = 0;
|
||||
|
||||
void setStatus(uint8_t r, uint8_t g, uint8_t 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).
|
||||
// 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
|
||||
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 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);
|
||||
|
|
@ -59,66 +46,60 @@ void setPositionColor(uint64_t value, uint64_t maxVal) {
|
|||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CORE 1: SSI worker
|
||||
// =========================================================================
|
||||
void ssi_pio_init(uint half_us) {
|
||||
// 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) {
|
||||
uint64_t value = 0;
|
||||
pio_sm_config c = ssi_master_program_get_default_config(ssi_offset);
|
||||
|
||||
sio_hw->gpio_clr = TX_DI_MASK; // first falling edge: latch
|
||||
busy_wait_us_32(half_us);
|
||||
// Side-set drives the CLK pin (TX_DI)
|
||||
sm_config_set_sideset_pins(&c, TX_DI);
|
||||
|
||||
for (uint8_t i = 0; i < bits; i++) {
|
||||
sio_hw->gpio_set = TX_DI_MASK; // rising: encoder shifts
|
||||
busy_wait_us_32(half_us);
|
||||
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);
|
||||
}
|
||||
// 'in pins, 1' samples starting at RX_RO
|
||||
sm_config_set_in_pins(&c, RX_RO);
|
||||
sm_config_set_in_shift(&c, false /* shift_left */, false /* autopush */, 32);
|
||||
|
||||
sio_hw->gpio_set = TX_DI_MASK; // back to idle
|
||||
busy_wait_us_32(30); // monoflop
|
||||
// 'out x, 32' pulls 32 bits from OSR; shift direction doesn't matter for full word
|
||||
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() {
|
||||
// Core 1 setup: nothing to do, GPIOs already configured by core 0.
|
||||
// Importantly: no Serial, no USB, no millis IRQ active here by default
|
||||
// when running in this dual-core mode.
|
||||
void ssi_pio_reconfigure_speed(uint half_us) {
|
||||
pio_sm_set_enabled(ssi_pio, ssi_sm, false);
|
||||
|
||||
// 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() {
|
||||
// Block until core 0 sends a packed request word.
|
||||
// Word layout: bits in upper 8, half_us in next 16, 8 unused
|
||||
uint32_t req = rp2040.fifo.pop();
|
||||
uint8_t bits = (req >> 24) & 0xFF;
|
||||
uint16_t half_us = (req >> 8) & 0xFFFF;
|
||||
uint32_t ssi_pio_read(uint8_t bits) {
|
||||
pio_sm_put_blocking(ssi_pio, ssi_sm, bits - 1);
|
||||
uint32_t result = pio_sm_get_blocking(ssi_pio, ssi_sm);
|
||||
|
||||
uint32_t t0 = time_us_32();
|
||||
uint64_t value = ssi_read_core1(bits, half_us);
|
||||
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;
|
||||
// ISR shifts left → first bit in MSB. Right-align.
|
||||
return result >> (32 - bits);
|
||||
}
|
||||
|
||||
void handleCommand(const String& cmd) {
|
||||
|
|
@ -139,9 +120,9 @@ void handleCommand(const String& cmd) {
|
|||
int bits = cmd.substring(firstSpace + 1, secondSpace).toInt();
|
||||
int halfUs = cmd.substring(secondSpace + 1).toInt();
|
||||
|
||||
if (bits < 1 || bits > 64) {
|
||||
if (bits < 1 || bits > 32) {
|
||||
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;
|
||||
}
|
||||
if (halfUs < 1 || halfUs > 10000) {
|
||||
|
|
@ -150,16 +131,17 @@ void handleCommand(const String& cmd) {
|
|||
return;
|
||||
}
|
||||
|
||||
ssi_pio_reconfigure_speed(halfUs);
|
||||
|
||||
setStatus(0, 0, 16); // blue = reading
|
||||
uint64_t value;
|
||||
uint32_t took;
|
||||
requestSsiRead((uint8_t)bits, (uint16_t)halfUs, value, took);
|
||||
uint32_t t0 = micros();
|
||||
uint32_t value = ssi_pio_read((uint8_t)bits);
|
||||
uint32_t took = micros() - t0;
|
||||
|
||||
// Show encoder position as hue (full range = 2^bits - 1)
|
||||
uint64_t maxVal = (bits < 64) ? ((1ull << bits) - 1) : UINT64_MAX;
|
||||
setPositionColor(value, maxVal);
|
||||
setPositionColor(value, (1ul << bits) - 1);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +150,6 @@ void setup() {
|
|||
led.begin();
|
||||
setStatus(8, 8, 0);
|
||||
|
||||
pinMode(TX_DI, OUTPUT);
|
||||
pinMode(TX_DE, OUTPUT); pinMode(TX_RE, OUTPUT);
|
||||
pinMode(RX_DE, OUTPUT); pinMode(RX_RE, OUTPUT);
|
||||
pinMode(RX_DI, OUTPUT);
|
||||
|
|
@ -179,11 +160,14 @@ void setup() {
|
|||
digitalWrite(RX_DE, LOW);
|
||||
digitalWrite(RX_RE, 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);
|
||||
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>");
|
||||
setStatus(0, 16, 0);
|
||||
}
|
||||
|
|
|
|||
24
src/ssi.pio
Normal file
24
src/ssi.pio
Normal 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
51
src/ssi.pio.h
Normal 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
|
||||
|
||||
Loading…
Reference in a new issue