Refine (further) the Phy API

- Refactor send/read data to read entire array instead of byte-oriented
  reads
- Simplify/improve consistency of asynchrony (all send functions now
  optionally async)
- Fix start-bit TX (now actually validate joinable start-bits)

Assisted-by: Claude Code (Opus 5)
This commit is contained in:
Allen Hill
2026-09-19 13:35:46 -07:00
parent 7252e6bfdd
commit fbf5811496
5 changed files with 236 additions and 207 deletions
+18 -17
View File
@@ -81,7 +81,9 @@ public:
}; };
Read read_control(uint8_t *control) { return phy_read_control(control); }; Read read_control(uint8_t *control) { return phy_read_control(control); };
Read read_length(uint8_t *length) { return phy_read_length(length); }; Read read_length(uint8_t *length) { return phy_read_length(length); };
Read read_data(uint8_t *data) { return phy_read_data(data); }; Read read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
return phy_read_data(data, length, data_index);
};
Send send_header(bool is_unicast) { return phy_send_header(is_unicast); }; Send send_header(bool is_unicast) { return phy_send_header(is_unicast); };
Send send_controller_addr(uint16_t addr) { Send send_controller_addr(uint16_t addr) {
@@ -96,8 +98,9 @@ public:
Send send_length(uint8_t length, bool expect_ack) { Send send_length(uint8_t length, bool expect_ack) {
return phy_send_length(length, expect_ack); return phy_send_length(length, expect_ack);
}; };
Send send_data(uint8_t data, bool expect_ack) { Send send_data(const uint8_t *data, uint8_t length, bool expect_ack,
return phy_send_data(data, expect_ack); uint8_t *data_index) {
return phy_send_data(data, length, expect_ack, data_index);
}; };
// NOLINTEND(readability-convert-member-functions-to-static) // NOLINTEND(readability-convert-member-functions-to-static)
}; };
@@ -183,14 +186,13 @@ auto Bus::read(Frame::Print print)
goto handle_err; goto handle_err;
} }
for (uint8_t i = 0; i < in->length; i++) { uint8_t data_i = 0;
err.type = handle.read_data(&in->data[i]); err.type = handle.read_data(in->data, in->length, &data_i);
if (err.type != Read{0}) { if (err.type != Read{0}) {
if (print.verbose) if (print.verbose)
err.val = in->data[i]; err.val = in->data[data_i];
goto handle_err; goto handle_err;
}
} }
} // destroy handle } // destroy handle
@@ -279,17 +281,16 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send {
if (err.type != Send{0}) if (err.type != Send{0})
goto handle_err; goto handle_err;
for (uint8_t i = 0; i < out.length; i++) { err.type =
err.type = handle.send_data(out.data[i], out.is_unicast); handle.send_data(out.data, out.length, out.is_unicast, &err.val);
if (err.type != Send{0}) { if (err.type != Send{0})
err.val = i; goto handle_err;
goto handle_err;
}
}
// A phy that only queued the fields above settles them here // A phy that only queued the fields above settles them here
uint8_t data_i = 0; uint8_t data_i = 0;
err.type = phy_send_done(&data_i); err.type = phy_send_done(&data_i);
if (err.type == BUSY || err.type == LOST_ARBITRATION) // Queued header
continue;
if (err.type != Send{0}) { if (err.type != Send{0}) {
err.val = data_i; err.val = data_i;
goto handle_err; goto handle_err;
+22 -17
View File
@@ -50,14 +50,13 @@ void phy_guard_leave(void);
/* Per-field frame I/O. /* Per-field frame I/O.
* *
* Excluding the header and controller_addr send functions, all other send * All send functions may have asynchronous implementations (e.g. return before
* functions may have asynchronous implementations (e.g. return before the send * the send has completed on the bus). Success is indicated by a zero value
* has completed on the bus). Success is indicated by a zero value `Read` or * `Read` or `Send` enum. Non-zero error codes indicate a synchronously
* `Send` enum. Non-zero error codes indicate a synchronously completed send * completed send failure. Otherwise, `phy_send_done` must be called to block
* failure. Otherwise, `phy_send_done` must be called to block until all queued * until all queued send's have completed, and may return the error code for a
* send's have completed, and may return the error code for a previous (queued) * previous (queued) send failure; a success return value indicates that all
* send failure; a success return value indicates that all queued send's have * queued send's have finished sending over the bus.
* finished sending over the bus.
* *
* The read functions are similarly optionally asynchronous, and may return the * The read functions are similarly optionally asynchronous, and may return the
* results of buffered reads. When this is the case, phy_read_header returns the * results of buffered reads. When this is the case, phy_read_header returns the
@@ -75,25 +74,31 @@ Read phy_read_controller_addr(uint16_t *addr);
Read phy_read_peripheral_addr(uint16_t *addr); Read phy_read_peripheral_addr(uint16_t *addr);
Read phy_read_control(uint8_t *control); Read phy_read_control(uint8_t *control);
Read phy_read_length(uint8_t *length); Read phy_read_length(uint8_t *length);
Read phy_read_data(uint8_t *data); // Read `length` data bytes. `data_index` is only written to on error, with the
// index of the failed byte.
Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index);
// Send start and broadcast bits. Always synchronous. Returns success or one of // Send start and broadcast bits. Returns success or one of these error values:
// these error values: MUTED, BUSY, or LOST_ARBITRATION (if another device // MUTED, BUSY, or LOST_ARBITRATION (if another device overrides our frame with
// overrides our frame with a broadcast). // a broadcast).
Send phy_send_header(bool is_unicast); Send phy_send_header(bool is_unicast);
// Send the controller address. Always synchronous. Returns success or // Send the controller address. Returns success or LOST_ARBITRATION (a device
// LOST_ARBITRATION (a device with a lower device is sending a frame). // with a lower device is sending a frame).
Send phy_send_controller_addr(uint16_t addr); Send phy_send_controller_addr(uint16_t addr);
Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack); Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack);
Send phy_send_control(uint8_t control, bool expect_ack); Send phy_send_control(uint8_t control, bool expect_ack);
Send phy_send_length(uint8_t length, bool expect_ack); Send phy_send_length(uint8_t length, bool expect_ack);
Send phy_send_data(uint8_t data, bool expect_ack); // Send `length` data bytes. `data_index` is only written to for NAK_DATA.
Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
uint8_t *data_index);
// Allows asynchronous ports to block until the phy has finished sending the // Allows asynchronous ports to block until the phy has finished sending the
// frame. Returns success or the relevant field-specific NAK (e.g. NAK_ADDRESS, // frame. Returns success or the relevant field-specific NAK (e.g. NAK_ADDRESS,
// etc) or CONTENDED_BUS. `data_index` is only written to for NAK_DATA. A fully // etc) or CONTENDED_BUS; a port that queues the header and controller address
// synchronous port should always report success. // also reports their BUSY or LOST_ARBITRATION here. `data_index` is only
// written to for NAK_DATA. A fully synchronous port should always report
// success.
Send phy_send_done(uint8_t *data_index); Send phy_send_done(uint8_t *data_index);
#ifndef NDEBUG #ifndef NDEBUG
+20 -8
View File
@@ -543,11 +543,15 @@ Read phy_read_length(uint8_t *length) {
return (Read)0; return (Read)0;
} }
Read phy_read_data(uint8_t *data) { Read phy_read_data(uint8_t *data, uint8_t length, uint8_t *data_index) {
const Read err = read_parity(phy_read_byte(data), BAD_DATA_PARITY); for (uint8_t i = 0; i < length; i++) {
if (err != (Read)0) const Read err = read_parity(phy_read_byte(&data[i]), BAD_DATA_PARITY);
return err; if (err != (Read)0) {
ack_slot(); *data_index = i;
return err;
}
ack_slot();
}
return (Read)0; return (Read)0;
} }
@@ -595,9 +599,17 @@ Send phy_send_length(uint8_t length, bool expect_ack) {
return send_ack_slot(expect_ack, NAK_MESSAGE_LENGTH); return send_ack_slot(expect_ack, NAK_MESSAGE_LENGTH);
} }
Send phy_send_data(uint8_t data, bool expect_ack) { Send phy_send_data(const uint8_t *data, uint8_t length, bool expect_ack,
phy_send_bit(phy_send_byte(&data)); uint8_t *data_index) {
return send_ack_slot(expect_ack, NAK_DATA); for (uint8_t i = 0; i < length; i++) {
phy_send_bit(phy_send_byte(&data[i]));
const Send err = send_ack_slot(expect_ack, NAK_DATA);
if (err != (Send)0) {
*data_index = i;
return err;
}
}
return (Send)0;
} }
// Every field above is on the wire, and has reported its own outcome, by the // Every field above is on the wire, and has reported its own outcome, by the
+51 -36
View File
@@ -2,8 +2,8 @@
.define public ack_irq 5 ; (PIO1) rx -> iebus_ack: hold this slot low .define public ack_irq 5 ; (PIO1) rx -> iebus_ack: hold this slot low
; This is used to set the clkdiv. ; This is used to set the clkdiv.
; The delay cycle counts in read_bit (AVCLAN_READBIT_THRESHOLD) and iebus_ack ; The delay cycle counts in read_bit (AVCLAN_READBIT_THRESHOLD) and iebus_tx's
; (AVCLAN_BIT0_LOGIC_0) must be updated in sync with this variable ; ack_drive (AVCLAN_BIT0_LOGIC_0) must be updated in sync with this variable
.define public CYCLES_PER_READBIT_PERIOD 64 .define public CYCLES_PER_READBIT_PERIOD 64
.pio_version 1 .pio_version 1
@@ -105,7 +105,7 @@ push_slot:
out x, 1 ; Load "has ACK" out x, 1 ; Load "has ACK"
jmp !x wait_read jmp !x wait_read
wait 1 pin 0 wait 1 pin 0
wait 0 pin 0 [31] ; Sender begins the ACK bit, and will hold it dominant wait 0 pin 0 [30] ; Sender begins the ACK bit, and will hold it dominant
; for ~48 cycles (~AVCLAN_BIT1_LOGIC_0) before releasing ; for ~48 cycles (~AVCLAN_BIT1_LOGIC_0) before releasing
; for peripheral to take over ; for peripheral to take over
; Delay reloading ack_latch to give the driver as much ; Delay reloading ack_latch to give the driver as much
@@ -113,10 +113,10 @@ push_slot:
; e.g. a bad length) ; e.g. a bad length)
mov x, status ; Load ack_latch state into x mov x, status ; Load ack_latch state into x
jmp !x wait_read jmp !x wait_read
irq next set ack_irq ; iebus_ack released 33 cycles after sender began bit irq next set ack_irq ; The ack SM (iebus_tx's ack_entry, on the other (TX)
; (~65% of AVCLAN_BIT1_LOGIC_0), well ; PIO) is released 32 cycles after sender began bit,
; iebus_ack, on the other (TX) PIO, holds it ; and its jmp to ack_drive takes the slot over at 34
; dominant. ; (~70% of AVCLAN_BIT1_LOGIC_0), holding it dominant.
.wrap .wrap
; Can execute one streamed instruction every 3 cycles ; Can execute one streamed instruction every 3 cycles
@@ -128,21 +128,6 @@ push_slot:
; jmp x-- do_exec ; Cycle 3: Restart loop (if x != 0) ; jmp x-- do_exec ; Cycle 3: Restart loop (if x != 0)
; jmp wait_read ; jmp wait_read
; Send an ack for iebus_rx. Must be loaded on the same PIO as iebus_tx to properly
; share the IEBUS_TX pin
; Drives bus dominant for *less than* a complete AVCLAN_BIT0_LOGIC_0 duration.
; Delays are coordinated with the iebus_rx release to ensure this SM releases the
; bus to recessive at the correct time
.program iebus_ack
.side_set 1
; Released by iebus_rx after bus goes dominant
wait 1 irq ack_irq side 1 ; Idle recessive; the wait clears the flag
set x, 1 side 0 [15] ; Dominant, taking the slot over from the sender
hold:
jmp x-- hold side 0 [15] ; 16 + 2*16 = 48 cycles, releasing at
; ~AVCLAN_BIT0_LOGIC_0 after the slot's leading edge
.program iebus_tx .program iebus_tx
.fifo tx .fifo tx
@@ -161,26 +146,45 @@ hold:
.define public lost_arb_irq 6 .define public lost_arb_irq 6
;;; Emit or synchronize to a start bit ;;; Emit or synchronize to a start bit
; Reached by a CPU initiated force exec jmp ; Reached by a CPU initiated force exec jmp to tx_startbit, with x = JOIN_SAMPLES
public tx_startbit: ; and y = IDLE_SAMPLES. We originate once the bus has stayed recessive for the
jmp pin originate ; Recessive == bus idle => we originate ; idle window. A dominant pulse is joined only if it outlasts every bit's
jmp joined ; dominant period (i.e. it's a start bit); otherwise the bus is busy, reported as
; a lost bid before any word is pulled.
.define public IDLE_SAMPLES 11 ; 12 * 16 = 192 cycles (~2 * AVCLAN_BIT_LENGTH_MAX)
; TODO: Explore tightening the idle window to 3 * AVCLAN_STARTBIT_LOGIC_1 (the
; longest recessive period within a frame)
.define public JOIN_SAMPLES 8 ; 9 * 12 = 108 cycles (> 1.2 * AVCLAN_BIT0_LOGIC_0)
originate: idle_ok:
set x 24 jmp y-- tx_startbit [14] ; Sampled every 16 cycles
set pins, 0 [15] ; 16 cycles, then set x 31 ; Idle: originate
set pins, 0 [30]
public tx_startbit:
jmp pin idle_ok
hold: hold:
jmp x-- hold [15] ; 25 * 16 = 416 total (~AVCLAN_STARTBIT_LOGIC_0) jmp pin lost ; Released too soon for a start bit (never while we drive)
jmp x-- hold [10] ; Sampled every 12 cycles, within a zero bit's recessive
; period (~AVCLAN_BIT0_LOGIC_1). Ours is 31 + 1 + 32 * 12
; = 416 total (~AVCLAN_STARTBIT_LOGIC_0)
set pins, 1 ; Release set pins, 1 ; Release
joined:
wait 1 pin 0 [31] ; The start bit's trailing edge, whoever drove it wait 1 pin 0 [31] ; The start bit's trailing edge, whoever drove it
jmp reset [16] ; 51 cycles from that edge to reset's first jmp reset [16] ; 51 cycles from that edge to reset's first
; `set pins, 0` (~AVCLAN_STARTBIT_LOGIC_1) ; `set pins, 0` (~AVCLAN_STARTBIT_LOGIC_1)
handle_nak: handle_nak:
jmp x-- reset ; Reset if NAK was expected jmp x-- ack_done ; Slot is over either way if a NAK was expected
irq wait nak_irq irq wait nak_irq
;;; Close the acknowledge slot
; The peripheral owns this slot's trailing edge, so the recessive period that
; ends the slot is timed from its release rather than from our leading edge --
; the one bit in a frame we don't drive to completion ourselves. A slot nobody
; answered is already recessive and falls straight through.
ack_done:
wait 1 pin 0 [12] ; 15 cycles from the release to `set pins, 0` below
; (~AVCLAN_BIT0_LOGIC_1)
reset: reset:
.wrap_target .wrap_target
pull pull
@@ -202,6 +206,7 @@ bit_zero:
; device sending a zero will "override" the value of that bit) ; device sending a zero will "override" the value of that bit)
jmp !y bit_end [2] ; Delay 3 more cycles before releasing bus at ~AVCLAN_BIT0_LOGIC_0 jmp !y bit_end [2] ; Delay 3 more cycles before releasing bus at ~AVCLAN_BIT0_LOGIC_0
; Fall through means we lost arbitration (we read a zero, when we expected a one) ; Fall through means we lost arbitration (we read a zero, when we expected a one)
lost:
irq wait lost_arb_irq irq wait lost_arb_irq
.wrap .wrap
@@ -212,19 +217,29 @@ bit_end:
jmp !x reset ; Fall through to read ACK if "has ACK" is set jmp !x reset ; Fall through to read ACK if "has ACK" is set
;;; read ack ;;; read ack
public ack_drive:
set pins, 0 [31] ; Start ACK bit with a AVCLAN_BIT1_LOGIC_0 length pulse set pins, 0 [31] ; Start ACK bit with a AVCLAN_BIT1_LOGIC_0 length pulse
out x, 1 [15] ; Preload NAK ok out x, 1 [15] ; Preload NAK ok
public ack_release:
set pins, 1 [15] ; Release bus set pins, 1 [15] ; Release bus
jmp pin handle_nak [28] ; Sampled at 64 cycles (~AVCLAN_READBIT_THRESHOLD), jmp pin handle_nak [13] ; Sampled at 64 cycles (~AVCLAN_READBIT_THRESHOLD);
; then delayed for the full 96-cycle logic-0 period ; both flow paths reach ack_done one instr later
; (Both flow paths add one more instr) jmp ack_done
jmp reset
slow_jmp: slow_jmp:
;;; The recessive ("prep") period in the send_bit loop is 2 instr's shorter than ;;; The recessive ("prep") period in the send_bit loop is 2 instr's shorter than
; the non-"has ACK" branch and ; the non-"has ACK" branch and
jmp send_bit [3] jmp send_bit [3]
;;; Send an ack for iebus_rx
; Run by its own SM, wrapping from ack_release back to ack_entry: it borrows
; ack_drive's 48-cycle pulse to hold the slot dominant for *less than* a complete
; AVCLAN_BIT0_LOGIC_0, releasing at ~AVCLAN_BIT0_LOGIC_0 after the slot's
; leading edge.
public ack_entry:
wait 1 irq ack_irq ; Released by iebus_rx after bus goes dominant
jmp ack_drive
; `mov pins, pins` reads the IN mapping (bit 0 == in base) and writes the OUT ; `mov pins, pins` reads the IN mapping (bit 0 == in base) and writes the OUT
; mapping (out base, 1 pin), so one program instance serves any src->dst pair; ; mapping (out base, 1 pin), so one program instance serves any src->dst pair;
; give each pair its own SM with its own pin mapping. ; give each pair its own SM with its own pin mapping.
+122 -126
View File
@@ -1,15 +1,15 @@
#include <array> #include <array>
#include <atomic>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <hardware/clocks.h> #include <hardware/clocks.h>
#include <limits>
#include "avclan.h" #include "avclan.h"
#include "hal/phy.h" #include "hal/phy.h"
#include "hardware/gpio.h" #include "hardware/gpio.h"
#include "hardware/pio.h" #include "hardware/pio.h"
#include "iebus.pio.h" #include "iebus.pio.h"
#include "phy_debug.hpp"
#define TICK_US 1000000 #define TICK_US 1000000
#include "timing.h" #include "timing.h"
@@ -160,13 +160,13 @@ public:
} }
// Disarm RX ACK'ing behavior; called prior to frame TX to prevent // Disarm RX ACK'ing behavior; called prior to frame TX to prevent
// self-ACK'ing. Safe to rearm any time after sending controller addr. // self-ACK'ing.
void disarm_ack() { void disarm_ack() {
transmitting_ = true; transmitting_ = true;
sync_ack_arming(); sync_ack_arming();
} }
// Rearm RX ACK'ing behavior; called after sending controller addr. // Rearm RX ACK'ing behavior; called once our frame is over or lost.
void rearm_ack() { void rearm_ack() {
transmitting_ = false; transmitting_ = false;
sync_ack_arming(); sync_ack_arming();
@@ -189,6 +189,13 @@ private:
static void __time_critical_func(irq_handler)() { instance_->isr(); } static void __time_critical_func(irq_handler)() { instance_->isr(); }
void __time_critical_func(isr)() { void __time_critical_func(isr)() {
// Every field after the controller address is followed by an ack slot,
// which the SM consumes whether or not it drives it.
const auto next_after_ack = [this](RxField next, uint8_t bits) {
state_ = next;
enqueue_rx(bits, true);
};
while (!pio_sm_is_rx_fifo_empty(pio_, sm_)) { while (!pio_sm_is_rx_fifo_empty(pio_, sm_)) {
const auto slot = (uint16_t)pio_sm_get(pio_, sm_); const auto slot = (uint16_t)pio_sm_get(pio_, sm_);
uint16_t value = 0; uint16_t value = 0;
@@ -227,9 +234,7 @@ private:
// still ahead of the SM. Re-dispatching the start-bit block hands // still ahead of the SM. Re-dispatching the start-bit block hands
// the rest of the frame back to it: those bits are far too short to // the rest of the frame back to it: those bits are far too short to
// read as a start bit, so it re-syncs on the next real one by // read as a start bit, so it re-syncs on the next real one by
// itself. This also settles a race on our own frames -- rearm_ack() // itself.
// restores y the moment we win the controller address, in time for
// that same read to have matched it.
pio_interrupt_clear(pio_, ack_latch); pio_interrupt_clear(pio_, ack_latch);
begin_frame(); begin_frame();
break; break;
@@ -359,13 +364,6 @@ private:
begin_frame(); begin_frame();
} }
// Every field after the controller address is followed by an ack slot, which
// the SM consumes whether or not it drives it.
void __time_critical_func(next_after_ack)(RxField next, uint8_t bits) {
state_ = next;
enqueue_rx(bits, true);
}
// The initialized instance, for irq_handler: SDK IRQ handlers take no // The initialized instance, for irq_handler: SDK IRQ handlers take no
// context. // context.
static inline IEBusRx *instance_ = nullptr; static inline IEBusRx *instance_ = nullptr;
@@ -401,16 +399,17 @@ private:
bool controller_ok_ = false; // parity verdict, held until the peripheral slot bool controller_ok_ = false; // parity verdict, held until the peripheral slot
}; };
// The transmit engine: the iebus_tx SM, plus the iebus_ack SM that drives the // The transmit engine: the iebus_tx SM, plus the ack SM that drives the ack
// ack slot on the rx engine's behalf. Both live on one PIO because they share // slot on the rx engine's behalf. Both run iebus_tx, sharing its ack-bit drive
// the TX pin. // and the TX pin.
// //
// Everything past the arbitration window is queued, not sent, so the fields // Every field, arbitration included, is queued, not sent, so the fields report
// report nothing and the frame's verdict comes from send_done. // nothing and the frame's verdict comes from send_done.
class IEBusTx { class IEBusTx {
// The flag the SM parked on. Sticky until reported; nothing more is queued // Word positions in a frame
// meanwhile. static constexpr uint8_t WORD_PERIPHERAL = 2; // After broadcast, controller
enum class Fault : uint8_t { None, Nak, Mismatch }; static constexpr uint8_t WORD_CONTROL = 3;
static constexpr uint8_t WORD_LENGTH = 4;
public: public:
// The ack match lives in the rx SM but exists for our sake: it has to be // The ack match lives in the rx SM but exists for our sake: it has to be
@@ -430,8 +429,7 @@ public:
pio_set_sm_mask_enabled(pio_, sm_mask(), false); pio_set_sm_mask_enabled(pio_, sm_mask(), false);
pio_remove_program_and_unclaim_sm(&iebus_tx_program, pio_, tx_sm_, pio_remove_program_and_unclaim_sm(&iebus_tx_program, pio_, tx_sm_,
tx_offset_); tx_offset_);
pio_remove_program_and_unclaim_sm(&iebus_ack_program, pio_, ack_sm_, pio_sm_unclaim(pio_, ack_sm_);
ack_offset_);
} }
// Claims and configures both SMs, but doesn't start them. // Claims and configures both SMs, but doesn't start them.
@@ -439,7 +437,6 @@ public:
pio_ = pio; pio_ = pio;
pin_tx_ = pin_tx; pin_tx_ = pin_tx;
tx_offset_ = (uint)pio_add_program(pio_, &iebus_tx_program); tx_offset_ = (uint)pio_add_program(pio_, &iebus_tx_program);
ack_offset_ = (uint)pio_add_program(pio_, &iebus_ack_program);
tx_sm_ = (uint)pio_claim_unused_sm(pio_, true); tx_sm_ = (uint)pio_claim_unused_sm(pio_, true);
ack_sm_ = (uint)pio_claim_unused_sm(pio_, true); ack_sm_ = (uint)pio_claim_unused_sm(pio_, true);
@@ -455,17 +452,21 @@ public:
sm_config_set_set_pins(&tx_cfg, pin_tx, 1); sm_config_set_set_pins(&tx_cfg, pin_tx, 1);
// Both the bit value and the arbitration check come from the readback. // Both the bit value and the arbitration check come from the readback.
sm_config_set_jmp_pin(&tx_cfg, pin_rx); sm_config_set_jmp_pin(&tx_cfg, pin_rx);
// The start bit's trailing-edge `wait` indexes the IN mapping, not JMP_PIN.
sm_config_set_in_pins(&tx_cfg, pin_rx);
sm_config_set_clkdiv(&tx_cfg, bus_clkdiv()); sm_config_set_clkdiv(&tx_cfg, bus_clkdiv());
// Entry is `reset`, not the program start -- offset 0 is handle_nak, which // Entry is `reset`; offset 0 is the start-bit block.
// would read the first word's top bit as the NAK flag.
pio_sm_init(pio_, tx_sm_, tx_offset_ + iebus_tx_wrap_target, &tx_cfg); pio_sm_init(pio_, tx_sm_, tx_offset_ + iebus_tx_wrap_target, &tx_cfg);
pio_sm_config ack_cfg = iebus_ack_program_get_default_config(ack_offset_); pio_sm_config ack_cfg = iebus_tx_program_get_default_config(tx_offset_);
sm_config_set_sideset_pins(&ack_cfg, pin_tx); sm_config_set_wrap(&ack_cfg, tx_offset_ + iebus_tx_offset_ack_entry,
tx_offset_ + iebus_tx_offset_ack_release);
sm_config_set_set_pins(&ack_cfg, pin_tx, 1);
sm_config_set_clkdiv(&ack_cfg, bus_clkdiv()); sm_config_set_clkdiv(&ack_cfg, bus_clkdiv());
pio_sm_init(pio_, ack_sm_, ack_offset_, &ack_cfg); pio_sm_init(pio_, ack_sm_, tx_offset_ + iebus_tx_offset_ack_entry,
&ack_cfg);
claimed_ = true; claimed_ = true;
} }
@@ -504,53 +505,67 @@ public:
return MUTED; return MUTED;
rx_.disarm_ack(); rx_.disarm_ack();
words_ = 0;
data_start_ = std::numeric_limits<uint8_t>::max();
// Jump to the tx_startbit section from the default "reset" stall on pull // Jump to the tx_startbit section from the default "reset" stall on pull,
pio_sm_exec(pio_, tx_sm_, // with the start-bit block's sample counts loaded
pio_encode_jmp(tx_offset_ + iebus_tx_offset_tx_startbit)); pio_sm_exec_wait_blocking(pio_, tx_sm_,
pio_encode_set(pio_x, iebus_tx_JOIN_SAMPLES));
pio_sm_exec_wait_blocking(pio_, tx_sm_,
pio_encode_set(pio_y, iebus_tx_IDLE_SAMPLES));
pio_sm_exec_wait_blocking(
pio_, tx_sm_, pio_encode_jmp(tx_offset_ + iebus_tx_offset_tx_startbit));
// The broadcast bit carries no parity, so it is sent as a bare bit: length // The broadcast bit carries no parity, so it is sent as a bare bit: length
// 0 (one bit) with the parity slot standing in for the bit itself. // 0 (one bit) with the parity slot standing in for the bit itself.
return arbitrate(is_unicast ? 1U : 0U, 0); return send_field(0, is_unicast ? 1U : 0U, false, false);
} }
Send send_controller_addr(uint16_t addr) { // A field is only queued, and its outcome left to send_done. Once one has
// Last field of the arbitration window; no acknowledge slot follows it. // failed, the rest of the frame is dropped.
const Send err = arbitrate(addr, 12); Send send_field(size_t len, uint32_t bits, bool has_ack, bool expect_ack) {
if (err == Send{0}) {
// We won: nobody else is transmitting, so the match can come back.
rx_.rearm_ack();
words_ = 0;
}
return err;
}
// Past arbitration a field is only queued, and its outcome left to
// send_done. Once one has failed, the rest of the frame is dropped.
Send send_field(size_t len, uint32_t bits, bool expect_ack) {
if (muted_) if (muted_)
return MUTED; return MUTED;
if (!check()) if (!check())
put(encode_tx(len, (uint16_t)bits, true, expect_ack)); put(encode_tx(len, (uint16_t)bits, has_ack, expect_ack));
return Send{0};
}
Send send_data(const uint8_t *data, uint8_t length, bool expect_ack) {
if (muted_)
return MUTED;
data_start_ = words_;
for (uint8_t i = 0; i < length && !check(); i++)
put(encode_tx(8, data[i], true, expect_ack));
return Send{0}; return Send{0};
} }
Send send_done(uint8_t *data_index) { Send send_done(uint8_t *data_index) {
wait_done(); const auto flagged = [this] {
check(); return pio_interrupt_get(pio_, iebus_tx_lost_arb_irq) ||
const Fault fault = fault_; pio_interrupt_get(pio_, iebus_tx_nak_irq);
fault_ = Fault::None; };
if (fault == Fault::None) // The second PC test confirms that SM has finished sending last value and
return Send{0}; // not in-progress (i.e. pulled the last FIFO value and still mid-send)
if (fault == Fault::Mismatch) const auto idle = [this] {
return CONTENDED_BUS; return pio_sm_is_tx_fifo_empty(pio_, tx_sm_) && // Read before the pc
switch (failed_word_) { pio_sm_get_pc(pio_, tx_sm_) == tx_offset_ + iebus_tx_wrap_target;
case 0: return NAK_ADDRESS; };
case 1: return NAK_CONTROL;
case 2: return NAK_MESSAGE_LENGTH; while (!(flagged() || idle()))
default: *data_index = (uint8_t)(failed_word_ - 3); return NAK_DATA; tight_loop_contents();
}
check();
// Our frame is over, won or not.
rx_.rearm_ack();
words_ = 0;
const Send err = fault_;
fault_ = Send{0};
if (err == NAK_DATA)
*data_index = (uint8_t)(failed_word_ - data_start_);
return err;
} }
private: private:
@@ -573,48 +588,46 @@ private:
return word; return word;
} }
bool flagged() const {
return pio_interrupt_get(pio_, iebus_tx_lost_arb_irq) ||
pio_interrupt_get(pio_, iebus_tx_nak_irq);
}
// The SM has nothing left to do: every path through a word ends back on
// `reset`'s pull. The pc test only counts once the FIFO is empty -- before
// the SM takes a word it is still sitting on that same pull. A parked SM is
// not idle; see flagged.
bool idle() const {
return pio_sm_is_tx_fifo_empty(pio_, tx_sm_) && // Read before the pc
pio_sm_get_pc(pio_, tx_sm_) == tx_offset_ + iebus_tx_wrap_target;
}
void wait_done() {
while (!(flagged() || idle()))
tight_loop_contents();
}
// Collect a flag if the SM has raised one, without waiting. Returns whether // Collect a flag if the SM has raised one, without waiting. Returns whether
// the frame has failed, now or earlier. // the frame has failed, now or earlier.
bool check() { bool check() {
const bool lost_arb = pio_interrupt_get(pio_, iebus_tx_lost_arb_irq); const bool lost_arb = pio_interrupt_get(pio_, iebus_tx_lost_arb_irq);
if (!lost_arb && !pio_interrupt_get(pio_, iebus_tx_nak_irq)) if (!lost_arb && !pio_interrupt_get(pio_, iebus_tx_nak_irq))
return fault_ != Fault::None; return fault_ != Send{0};
// Read the level before the clear throws it away, and clear before the // Read the level before the clear throws it away, and clear before the
// flag: releasing the SM with words still queued would send the rest of the // flag: releasing the SM with words still queued would send the rest of the
// frame. // frame.
failed_word_ = const auto pulled =
(uint8_t)(words_ - pio_sm_get_tx_fifo_level(pio_, tx_sm_) - 1U); (uint8_t)(words_ - pio_sm_get_tx_fifo_level(pio_, tx_sm_));
failed_word_ = (uint8_t)(pulled - 1U);
pio_sm_clear_fifos(pio_, tx_sm_); pio_sm_clear_fifos(pio_, tx_sm_);
if (lost_arb) { if (lost_arb) {
pio_interrupt_clear(pio_, iebus_tx_lost_arb_irq); pio_interrupt_clear(pio_, iebus_tx_lost_arb_irq);
// The winner's frame is still arriving; put the ack match back so we can // The other frame is still arriving; put the ack match back so we can
// answer it if it turns out to be addressed to us. // answer it if it turns out to be addressed to us.
rx_.rearm_ack(); rx_.rearm_ack();
fault_ = Fault::Mismatch; // A bid lost before any word was pulled: the start-bit block saw the bus
// dominant with something other than a start bit.
if (pulled == 0)
fault_ = BUSY;
else if (failed_word_ < WORD_PERIPHERAL && failed_word_ < data_start_)
fault_ = LOST_ARBITRATION;
else
fault_ = CONTENDED_BUS;
} else { } else {
pio_interrupt_clear(pio_, iebus_tx_nak_irq); pio_interrupt_clear(pio_, iebus_tx_nak_irq);
fault_ = Fault::Nak; if (failed_word_ >= data_start_)
fault_ = NAK_DATA;
else if (failed_word_ == WORD_PERIPHERAL)
fault_ = NAK_ADDRESS;
else if (failed_word_ == WORD_CONTROL)
fault_ = NAK_CONTROL;
else if (failed_word_ == WORD_LENGTH)
fault_ = NAK_MESSAGE_LENGTH;
else
fault_ = NAK;
} }
return true; return true;
} }
@@ -631,20 +644,6 @@ private:
words_++; words_++;
} }
// The arbitration window goes out a field at a time: a lost bid has to be
// known before anything more is queued.
Send arbitrate(uint32_t bits, uint8_t len) {
if (muted_)
return MUTED;
put(encode_tx(len, (uint16_t)bits, false, false));
wait_done();
// Reported here and now, so nothing is left for send_done. With no ack slot
// in these fields, the only fault is a lost bid.
const bool lost = check();
fault_ = Fault::None;
return lost ? LOST_ARBITRATION : Send{0};
}
IEBusRx &rx_; IEBusRx &rx_;
PIO pio_; PIO pio_;
@@ -652,17 +651,18 @@ private:
uint tx_sm_; uint tx_sm_;
uint tx_offset_; uint tx_offset_;
uint ack_sm_; uint ack_sm_;
uint ack_offset_;
bool claimed_ = false; // init() ran, so the destructor has something to undo bool claimed_ = false; // init() ran, so the destructor has something to undo
bool muted_ = false; bool muted_ = false;
// Words put since arbitration was won, taken by the SM or not. Its flags park // Words put since the header, taken by the SM or not. Its flags park it,
// it, which freezes the FIFO, so the word it failed on is the last one it // which freezes the FIFO, so the word it failed on is the last one it pulled:
// pulled: words_ - level - 1. Wrapping is harmless; the FIFO holds at most 8. // words_ - level - 1. Wrapping is harmless; the FIFO holds at most 8.
uint8_t words_; uint8_t words_;
uint8_t data_start_; // Position of the first data word
uint8_t failed_word_; uint8_t failed_word_;
Fault fault_; // Sticky until send_done reports it; nothing more is queued meanwhile.
Send fault_;
}; };
// The bus as a whole. Only it can hold the invariants that span the two // The bus as a whole. Only it can hold the invariants that span the two
@@ -718,7 +718,7 @@ private:
// Reading needs nothing but the pad's input buffer, which is on for both bus // Reading needs nothing but the pad's input buffer, which is on for both bus
// pins already -- asserted here so the mirror cannot go dark if that changes. // pins already -- asserted here so the mirror cannot go dark if that changes.
void pin_mirror_sm_init(uint sm, uint offset, uint src, uint dst) { void pin_mirror_sm_init(uint sm, uint offset, uint src, uint dst) {
PIO pio = tx_.pio(); PIO pio = pio2;
gpio_set_input_enabled(src, true); gpio_set_input_enabled(src, true);
pio_gpio_init(pio, dst); pio_gpio_init(pio, dst);
pio_sm_set_consecutive_pindirs(pio, sm, dst, 1, true); pio_sm_set_consecutive_pindirs(pio, sm, dst, 1, true);
@@ -738,7 +738,7 @@ private:
// IEBUS TX/RX pins to the LED pins so that dominant bus activity (i.e. LOW // IEBUS TX/RX pins to the LED pins so that dominant bus activity (i.e. LOW
// state for IEBUS TX/RX pins) lights the respective LED // state for IEBUS TX/RX pins) lights the respective LED
void activity_leds_init() { void activity_leds_init() {
PIO pio = tx_.pio(); // iebus_rx fills its PIO PIO pio = pio2; // iebus_rx and iebus_tx each fill a block
// Indicators are cosmetic; never fail the bus bring-up for them. // Indicators are cosmetic; never fail the bus bring-up for them.
if (!pio_can_add_program(pio, &pin_mirror_program)) if (!pio_can_add_program(pio, &pin_mirror_program))
return; return;
@@ -765,7 +765,7 @@ private:
void activity_leds_deinit() { void activity_leds_deinit() {
if (!leds_claimed_) if (!leds_claimed_)
return; return;
PIO pio = tx_.pio(); PIO pio = pio2;
pio_sm_set_enabled(pio, led_sm_rx_, false); pio_sm_set_enabled(pio, led_sm_rx_, false);
pio_sm_set_enabled(pio, led_sm_tx_, false); pio_sm_set_enabled(pio, led_sm_tx_, false);
pio_sm_unclaim(pio, led_sm_rx_); pio_sm_unclaim(pio, led_sm_rx_);
@@ -836,16 +836,10 @@ extern "C" Read phy_read_length(uint8_t *length) {
return Read{0}; return Read{0};
} }
extern "C" Read phy_read_data(uint8_t *data) { extern "C" Read phy_read_data(uint8_t *data, uint8_t length,
static uint8_t idx; [[maybe_unused]] uint8_t *data_index) {
const IEBusRx::RxFrame &frame = phy.rx().frame(); memcpy(data, phy.rx().frame().data, length);
if (idx >= frame.length) phy.rx().release();
idx = 0;
*data = frame.data[idx++];
if (idx >= frame.length) { // Frame consumed
idx = 0;
phy.rx().release();
}
return Read{0}; return Read{0};
} }
@@ -854,23 +848,25 @@ extern "C" Send phy_send_header(bool is_unicast) {
} }
extern "C" Send phy_send_controller_addr(uint16_t addr) { extern "C" Send phy_send_controller_addr(uint16_t addr) {
return phy.tx().send_controller_addr(addr); return phy.tx().send_field(12, addr, false, false);
} }
extern "C" Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) { extern "C" Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) {
return phy.tx().send_field(12, addr, expect_ack); return phy.tx().send_field(12, addr, true, expect_ack);
} }
extern "C" Send phy_send_control(uint8_t control, bool expect_ack) { extern "C" Send phy_send_control(uint8_t control, bool expect_ack) {
return phy.tx().send_field(4, control, expect_ack); return phy.tx().send_field(4, control, true, expect_ack);
} }
extern "C" Send phy_send_length(uint8_t length, bool expect_ack) { extern "C" Send phy_send_length(uint8_t length, bool expect_ack) {
return phy.tx().send_field(8, length, expect_ack); return phy.tx().send_field(8, length, true, expect_ack);
} }
extern "C" Send phy_send_data(uint8_t data, bool expect_ack) { extern "C" Send phy_send_data(const uint8_t *data, uint8_t length,
return phy.tx().send_field(8, data, expect_ack); bool expect_ack,
[[maybe_unused]] uint8_t *data_index) {
return phy.tx().send_data(data, length, expect_ack);
} }
extern "C" Send phy_send_done(uint8_t *data_index) { extern "C" Send phy_send_done(uint8_t *data_index) {