diff --git a/src/avclan/avclan.h b/src/avclan/avclan.h index d8db98f..ba0550b 100644 --- a/src/avclan/avclan.h +++ b/src/avclan/avclan.h @@ -158,6 +158,8 @@ struct Error { NAK_ADDRESS, NAK_TOO_LONG, NAK, // generic NAK has max severity + CONTENDED_BUS, + LOST_ARBITRATION, BUSY, MUTED, }; @@ -178,12 +180,6 @@ struct SendError { }; #endif -enum AVCLAN_ENUM_CLASS Bit : uint8_t { - bit_zero = 0x00, - bit_one = 0x01, - bit_start = 0x10 -}; - #ifdef __cplusplus } // namespace detail } // namespace avclan diff --git a/src/avclan/bus.cc b/src/avclan/bus.cc index 79b1621..bf9f83d 100644 --- a/src/avclan/bus.cc +++ b/src/avclan/bus.cc @@ -305,6 +305,10 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send { switch (err.type) { case MUTED: fputs(": Device muted", stdout); break; case BUSY: fputs(": Busy bus", stdout); break; + case CONTENDED_BUS: + fputs(": bus contended after arbitration", stdout); + break; + case LOST_ARBITRATION: fputs(": lost arbitration", stdout); break; case NAK_ADDRESS: case NAK_CONTROL: case NAK_MESSAGE_LENGTH: @@ -320,6 +324,8 @@ auto Bus::send(const Frame &out, Frame::Print print) -> Send { case NAK_TOO_LONG: fputs("too long", stdout); break; case NAK: case MUTED: + case CONTENDED_BUS: + case LOST_ARBITRATION: case BUSY: __builtin_unreachable(); } break; diff --git a/src/avclan/hal/stdio.h b/src/avclan/hal/stdio.h index 1693a48..7545e1b 100644 --- a/src/avclan/hal/stdio.h +++ b/src/avclan/hal/stdio.h @@ -12,8 +12,8 @@ extern "C" { // Generic stdio interface initialization. All user I/O goes through // functions. Assumptions/invariants: -// - stdin MUST be non-blocking (ie. getchar() returns EOF immediately when -// empty). Necessary to avoid stalling the REPL poll loop. +// - stdin MUST be non-blocking (ie. a libc read yields EOF immediately when no +// input is buffered). Necessary to avoid stalling the REPL poll loop. // - stdout is *raw*. There is no '\n' -> "\r\n" translation. The port does not // change a bare LF or a binary frame payload. Writes through // and writes through stdio_write_nonblock() must reach the same diff --git a/src/avclan/target/pico2/CMakeLists.txt b/src/avclan/target/pico2/CMakeLists.txt index 39631f8..204b201 100644 --- a/src/avclan/target/pico2/CMakeLists.txt +++ b/src/avclan/target/pico2/CMakeLists.txt @@ -14,6 +14,11 @@ target_link_libraries(avclan PUBLIC ) pico_enable_stdio_usb(avclan 1) +# stdio_write_nonblock() is all-or-nothing and the largest buffer is a ~186 byte +# frame log line; the SDK default of 64 would drop every one. PUBLIC so the +# TinyUSB sources compiled into `mockingboard_pico` agree on the FIFO size. +target_compile_definitions(avclan PUBLIC CFG_TUD_CDC_TX_BUFSIZE=256) + # Needs separate/new executable target because `pico_add_extra_outputs` only # works on locally (to this file/directory) defined targets add_executable(mockingboard_pico diff --git a/src/avclan/target/pico2/iebus.pio b/src/avclan/target/pico2/iebus.pio index 233e04b..93e833f 100644 --- a/src/avclan/target/pico2/iebus.pio +++ b/src/avclan/target/pico2/iebus.pio @@ -1,5 +1,5 @@ -.define public parity_irq 7 ; (polled) Signal BAD_PARITY -.define public should_ack_irq 6 ; (system) +.define public ack_latch 4 ; (PIO0, internal) frame is addressed to us +.define public ack_irq 5 ; (PIO1) rx -> iebus_ack: hold this slot low ; This is used to set the clkdiv. ; The delay cycle counts in read_bit (AVCLAN_READBIT_THRESHOLD) and iebus_ack @@ -10,15 +10,15 @@ .program iebus_rx -; Initial TX Encoding -; | 19:4 | 3:0 | -; | Instr | # bit/instr | +; TX encoding: one read per word +; | 31 | 30:16 | 15:0 | +; | has ACK | zeros | Instr | +; +; Instr must be `out x, (14 - N)` for a read of N bits + parity, or `jmp rx_startbit`. +; The `out` zeroes x (the parity accumulator) and leaves exactly N + 1 bits before +; the OSR reaches its threshold, which ends the read loop. "has ACK" marks a field +; followed by an acknowledge slot. ; -; Proper behavior for reads is dependent on encoding and executing a `in null, (15 - x)` -; instruction to initialize the bit count correctly. -; An even number of instructions must be executed to maintain the correct shift-count/autopull -; when returning to normal reads after exec'ing. Pad instructions with `nop` as needed. -; ; RX encoding: one read per word (RX FIFO can hold max 4 reads) ; | 31:16 | 15:0 | ; | zeros | slot | @@ -26,75 +26,129 @@ ; where each slot is encoded as ; | MSB .................... LSB | ; | 15-N zeros | N bits | parity | +; +; Parity is validated by this SM, but is still reported in the LSB. `y` is reserved +; for the address match below. .in 1 left -.out 0 right auto 20 -.mov_status irq set should_ack_irq +; OSR threshold set to 31 so that the OSRE is reached when all the data+parity bits +; have been read +.out 0 right 31 +.mov_status irq set ack_latch -bad_parity: - irq set parity_irq ; Flag *before* the push: the app samples the flag right - ; after taking the word, so setting it after would race - push ; Push the failing slot; blocks, so the app must keep - ; draining while it waits for the stall below -public parity_stall: - wait 0 irq parity_irq ; Stall until the app clears it. Public so the app can - ; poll for the SM parking here before draining the FIFO - ; -- the push above lands ~2 cycles after the flag. +;;; Start-bit read (auto-resets until a proper start bit is observed) +; A start bit is dominant for AVCLAN_STARTBIT_LOGIC_0 (~169us) and recessive for +; AVCLAN_STARTBIT_LOGIC_0 (~20us) more. +; The bus state is polled as fast as target bounds (80% and 120%) can be evenly +; divided (must be faster than shortest recessive period AVCLAN_BIT0_LOGIC_1 to +; detect/distinguish normal bits mid-frame). +public rx_startbit: + wait 1 pin 0 +leading_edge: + wait 0 pin 0 + set x 29 +dominant: + jmp pin leading_edge [10] ; Pin HIGH == released before 0.8x: not a start bit + ; Polled every 11 cycles, starting 2 cycles after leading edge + ; Last poll at 332 cycles (~80% * AVCLAN_STARTBIT_LOGIC_0) + jmp x-- dominant ; Cycle 333 on fall-through + set x 18 ; 334 +recessive: + jmp pin valid_startbit ; Pin HIGH == released within bounds + jmp x-- recessive [8] ; + 19 * 9 = 506 total (~120% * AVCLAN_STARTBIT_LOGIC_0) + ; Delay on decrement jmp to keep the release jmp quick + jmp rx_startbit ; Still dominant: too long to be proper start bit +valid_startbit: + irq clear ack_latch ; Once per frame, before any address can set it -wait_read: .wrap_target - set y 0 ; Reset the parity accumulator - out x, 4 ; Load bit count; the loop will have x + 1 iterations - out exec, 16 ; Pad or jmp to do_exec +wait_read: + pull + out exec, 16 ; Set up the read (zero x and set up output shift + ; count as bit count), or jmp to rx_startbit read_bit: -;;; Resynchronize at each recessive => dominant edge +;;; Resynchronize at each leading (recessive => dominant) edge wait 1 pin 0 ; Worst-case (slowest) path to return here is for non-ACK flow. - wait 0 pin 0 [31] ; Delay for mid-data period (~AVCLAN_READBIT_THRESHOLD) after - nop [31] ; bus goes dominant - in pins, 1 ; Read bus state - jmp pin count_one ; count only if bus is recessive (logical 1) - ; EXECCTRL_JMP_PIN == RX pin - jmp count_bit -count_one: - mov y, ~y ; Toggle on each 1 bit: zero is even, all-ones is odd. -count_bit: - jmp x-- read_bit ; Last use of x as bit count + wait 0 pin 0 [31] + out null, 1 [31] ; Count this bit + ; Read bus state ~AVCLAN_READBIT_THRESHOLD after bus goes dominant + in pins, 1 + jmp pin counted ; Fall through for recessive (i.e. a one bit) + mov x, ~x ; Accumulate parity over the zero bits +counted: + jmp !osre read_bit -;;; check parity - jmp y-- bad_parity ; Even parity over data+parity means an even count of 1 - ; bits, so a nonzero y is an error. (clobbers y) - push ; Parity good +;;; Check parity +; Every parity-checked field is an odd number of bits, so even parity (an even +; number of ones) is an odd number of zeros: x == ~0. The broadcast bit has no +; parity; its verdict can only clear a latch that was never set. + jmp x-- parity_ok + irq clear ack_latch ; NAK this field and the rest of the frame -;;; check if we should ack - mov x, status ; Load should_ack_irq into x (clobbers x) - jmp !x wait_read ; Fall through if should_ack_irq is set +;;; Check if we're the frame recipient +; Our peripheral address can only appear in individual 12-bit reads as the +; controller or peripheral address. We should only ack if we're the frame recipient. +; y is preset in phy_init to our peripheral address + parity, and temporarily +; inverted when we are sending (so we don't self-ACK) +parity_ok: + mov x, isr ; Save read value before clobbering the ISR with push + jmp x!=y push_slot ; Fall-through to set ACK latch only if the read matches + ; our address + parity + irq set ack_latch ; Set before the push, so the driver's veto can't be overwritten -;;; ack - set x 3 +push_slot: + push + +;;; Consume the ack slot if has ACK, driving it if ACK latch is set + out x, 1 ; Load "has ACK" + jmp !x wait_read wait 1 pin 0 - wait 0 pin 0 ; Synchronize with sender - set pins, 0 [13] ; Drive bus (dominant) for ~AVCLAN_BIT0_LOGIC_0 -loop: ; Total loop length (1+15)*3 cycles - jmp x-- loop [15] - set pins, 1 ; Release bus + wait 0 pin 0 [31] ; Sender begins the ACK bit, and will hold it dominant + ; for ~48 cycles (~AVCLAN_BIT1_LOGIC_0) before releasing + ; for peripheral to take over + ; Delay reloading ack_latch to give the driver as much + ; time as possible to veto an ACK (i.e. clear it for + ; e.g. a bad length) + mov x, status ; Load ack_latch state into x + irq next set ack_irq ; iebus_ack released 33 cycles after sender began bit + ; (~65% of AVCLAN_BIT1_LOGIC_0), well + ; iebus_ack, on the other (TX) PIO, holds it + ; dominant. .wrap -; Executes one streamed instruction every 3 cycles -public do_exec: - out exec, 16 ; Cycle 1: Load the streamed instruction - ; Cycle 2: Execute the streamed instruction - jmp x-- do_exec ; Cycle 3: Restart loop (if x != 0) - jmp wait_read +; Can execute one streamed instruction every 3 cycles +; An even number of instructions must be executed to maintain the correct shift-count/autopull +; when returning to normal reads after exec'ing. Pad instructions with `nop` as needed. +; public do_exec: +; out exec, 16 ; Cycle 1: Load the streamed instruction +; ; Cycle 2: Execute the streamed instruction +; jmp x-- do_exec ; Cycle 3: Restart loop (if x != 0) +; 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 .fifo tx ; TX Encoding (8 deep TX FIFO): -; | MSB ............................................ LSB | -; | 4 bits | N bits | 1 bit | 1 bit | 26 - N bits | -; | Length (N) | Data+P | ACK slot | NAK | padding | +; | MSB ............................................. LSB | +; | 4 bits | N bits | 1 bit | 1 bit | 26 - N bits | +; | Length (N) | Data+P | has ACK | NAK ok | padding | ; ; Parity is generated by the driver and appended as the last (least significant) ; bit of the data field @@ -105,9 +159,25 @@ public do_exec: .define public nak_irq 7 .define public lost_arb_irq 6 +;;; Emit or synchronize to a start bit +; Reached by a CPU initiated force exec jmp +public tx_startbit: + jmp pin originate ; Recessive == bus idle => we originate + jmp joined + +originate: + set x 24 + set pins, 0 [15] ; 16 cycles, then +hold: + jmp x-- hold [15] ; 25 * 16 = 416 total (~AVCLAN_STARTBIT_LOGIC_0) + set pins, 1 ; Release +joined: + 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 + ; `set pins, 0` (~AVCLAN_STARTBIT_LOGIC_1) + handle_nak: - out x, 1 - jmp x-- reset ; Reset if NAK was expected + jmp x-- reset ; Reset if NAK was expected irq wait nak_irq reset: @@ -116,58 +186,49 @@ reset: out x, 4 send_bit: - set pins, 0 [31] - out y, 1 [15] ; OUT pin remains low/dominant for 48 cycles (~AVCLAN_BIT1_LOGIC_0) - mov pins, y [15] - jmp pin bit_one ; Sampled at ~AVCLAN_READBIT_THRESHOLD (64 cycles from bit start) - ; JMP_PIN must be the IEBUS_RX pin (TX activity will - ; mirror back to the RX pin) + set pins, 0 [31] ; OUT pin remains low/dominant for 32+16 cycles (~AVCLAN_BIT1_LOGIC_0) + out y, 1 [15] ; Save output bit to y + mov pins, y [15] ; Output the bit + ; Read bus state ~AVCLAN_READBIT_THRESHOLD after bus goes dominant + jmp pin bit_end [13] ; JMP_PIN must be the IEBUS_RX pin (TX activity will mirror + ; back to the RX pin). The delay burns the common duration + ; between the one and zero bits. bit_zero: - jmp !y bit_end [15] ; Delay 16 more cycles before releasing bus at ~AVCLAN_BIT0_LOGIC_0 - ; Fall through means we lost arbitration + ; This branch catches a difference between what we intended to send and what is actually + ; present on the bus (a difference is only expected/allowed during send arbitration, + ; where lower addresses have higher priority: since one bits are recessive, another + ; 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 + ; Fall through means we lost arbitration (we read a zero, when we expected a one) irq wait lost_arb_irq - jmp reset -bit_one: - ; Bus already released by `mov pins, y`, which caused/allowed the jmp - nop [12] - -bit_end: ; 15 cycles (~AVCLAN_BIT0_LOGIC_1) of recessive before the next leading - ; edge, on all three exits: the loop (via next_bit), reset, and the ack - ; slot below. Each spends a different number of instructions getting - ; there, so the delay is split between here and the padding they carry. - set pins, 1 [11] - jmp x-- send_bit ; Fall through once x (the bit count) is exhausted - out x, 1 - jmp !x reset ; Jump to reset if x (fill ack slot) is zero - -;;; read ack - nop [1] ; Match the 2 cycles the reset path spends in - ; pull/out before its first `set pins, 0`, so the last - ; bit gets a full period on both exits - set pins, 0 [31] - nop [15] - set pins, 1 [15] - jmp pin handle_nak [29] ; Sampled at 64 cycles (~AVCLAN_READBIT_THRESHOLD), - ; then held out to a full 96-cycle logic-0 period: an - ; ACKing peripheral drives dominant until cycle 81 - ; (~AVCLAN_BIT0_LOGIC_0), so starting the next bit any - ; sooner swallows its leading edge .wrap -; Copy one input pin to one output pin, forever, at the SM clock. Used to drive -; the activity LEDs from the bus pins without hanging any DC load on them: the -; LED current comes out of the mirror pin's pad, and the bus pin only ever sees -; a (already-enabled) input. -; +bit_end: + set pins, 1 [9] ; Delay must be common to all following control flow paths + jmp x-- slow_jmp ; Fall through once x (the bit count) is exhausted + out x, 1 + jmp !x reset ; Fall through to read ACK if "has ACK" is set + +;;; read ack + set pins, 0 [31] ; Start ACK bit with a AVCLAN_BIT1_LOGIC_0 length pulse + out x, 1 [15] ; Preload NAK ok + set pins, 1 [15] ; Release bus + jmp pin handle_nak [28] ; Sampled at 64 cycles (~AVCLAN_READBIT_THRESHOLD), + ; then delayed for the full 96-cycle logic-0 period + ; (Both flow paths add one more instr) + jmp reset + +slow_jmp: +;;; The recessive ("prep") period in the send_bit loop is 2 instr's shorter than +; the non-"has ACK" branch and + jmp send_bit [3] + ; `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; ; give each pair its own SM with its own pin mapping. .program pin_mirror - -.wrap_target mov pins, ~pins -.wrap ; === IDIOMS === @@ -181,6 +242,11 @@ bit_end: ; 15 cycles (~AVCLAN_BIT0_LOGIC_1) of recessive before the next leading ; jmp x-- label ; "Increment" ; set x ~x ; Re-inversing gives the actual count -; SHIFTCTRL_IN_COUNT = 0x01 (mask for number of pins/LSBs to read/set by `mov x, pins`) +;;; Multiplying delays/counts (e.g. 2*20) +; set y 1 +; loop1: +; set x 19 +; loop2: +; jmp x-- loop2 diff --git a/src/avclan/target/pico2/phy.cc b/src/avclan/target/pico2/phy.cc index 1deb7b9..b3be62d 100644 --- a/src/avclan/target/pico2/phy.cc +++ b/src/avclan/target/pico2/phy.cc @@ -1,9 +1,15 @@ -#include "hal/phy.h" +#include +#include +#include +#include +#include + #include "avclan.h" +#include "hal/phy.h" #include "hardware/gpio.h" #include "hardware/pio.h" #include "iebus.pio.h" -#include +#include "phy_debug.hpp" #define TICK_US 1000000 #include "timing.h" @@ -22,111 +28,835 @@ constexpr int IEBUS_RX = 17; constexpr int LED_RX = 8; constexpr int LED_TX = 26; -PIO pio; -uint sm; -uint offset; +constexpr uint32_t parity(uint32_t val) { + static_assert(sizeof(uint32_t) == sizeof(unsigned int)); + if consteval { + int N = 0; + for (; val != 0UL; N++) { + val = val & (val - 1); + } + return (N & 1); + } -inline void iebus_rx_program_init(PIO pio, uint sm, uint offset, uint pin_rx, - uint pin_tx) { - pio_sm_set_consecutive_pindirs(pio, sm, pin_rx, 1, false); - pio_sm_set_consecutive_pindirs(pio, sm, pin_tx, 1, true); - pio_sm_set_pins_with_mask(pio, sm, (1U << pin_tx), (1U << pin_tx)); - pio_gpio_init(pio, pin_rx); - pio_gpio_init(pio, pin_tx); - - pio_sm_config cfg = iebus_rx_program_get_default_config(offset); - sm_config_set_in_pins(&cfg, pin_rx); - sm_config_set_jmp_pin(&cfg, pin_rx); - sm_config_set_set_pins(&cfg, pin_tx, 1); - - // CYCLES_PER_READBIT_PERIOD PIO cycles should take - // ~AVCLAN_READBIT_THRESHOLD μs - float div = clock_get_hz(clk_sys) / - (CYCLES_PER_READBIT_PERIOD / (float)AVCLAN_READBIT_THRESHOLD); - sm_config_set_clkdiv(&cfg, div); - - pio_sm_init(pio, sm, offset, &cfg); - pio_sm_set_enabled(pio, sm, true); + return __builtin_parity(val); } -// Point one mirror SM at one src -> dst pair. Only the destination gets -// `pio_gpio_init`: taking the function select of a source pin would hand it to -// this PIO block and cut whoever actually drives it (IEBUS_TX) loose. 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. -void pin_mirror_sm_init(PIO mpio, uint msm, uint moffset, uint src, uint dst) { - gpio_set_input_enabled(src, true); - pio_gpio_init(mpio, dst); - pio_sm_set_consecutive_pindirs(mpio, msm, dst, 1, true); - - pio_sm_config cfg = pin_mirror_program_get_default_config(moffset); - sm_config_set_in_pins(&cfg, src); - sm_config_set_out_pins(&cfg, dst, 1); - // Default clkdiv: one copy per system clock, so the LED tracks the line far - // faster than a bit period. - - pio_sm_init(mpio, msm, moffset, &cfg); - pio_sm_set_enabled(mpio, msm, true); +// CYCLES_PER_READBIT_PERIOD PIO cycles should take ~AVCLAN_READBIT_THRESHOLD +// μs. All three bus SMs share it: iebus_rx and iebus_ack are cross-PIO IRQ +// partners, which the datasheet (S11.4) requires to have equal dividers, +// synchronised by a single CTRL write -- see the enable in Phy::init. +inline float bus_clkdiv() { + return clock_get_hz(clk_sys) / + (CYCLES_PER_READBIT_PERIOD / (float)AVCLAN_READBIT_THRESHOLD); } + +// The reader runs from the PIO's RX-FIFO interrupt rather than from the calling +// thread, so slots are drained and the next script queued no matter what the +// main loop is doing. That is not polish: the RX FIFO is 4 deep and printf over +// USB CDC blocks for milliseconds, which is exactly the failure rx_log_push +// exists to dodge. It is also what lets a frame we lost arbitration to still +// arrive -- the engine never stopped receiving. +// +// The frames it completes are handed out whole; hal/phy.h's read calls pick +// their field out of one. +class IEBusRx { + // Field widths, in the SM's "data bits" terms. The broadcast bit carries no + // parity, so it is the one field read as a bare bit (count 0 => 1 bit). + static constexpr uint8_t W_BROADCAST = 0; + static constexpr uint8_t W_ADDR = 12; + static constexpr uint8_t W_CONTROL = 4; + static constexpr uint8_t W_BYTE = 8; + + enum class RxField : uint8_t { + Broadcast, + Controller, + Peripheral, + Control, + Length, + Data, + }; + + // Must be a power of two (the index wrap is a mask). + static constexpr uint32_t RXQ_N = 4; + static_assert((RXQ_N & (RXQ_N - 1)) == 0, + "RX buffer size must be a power of 2"); + +public: + struct RxFrame { + uint16_t controller_addr; + uint16_t peripheral_addr; + uint8_t control; + uint8_t length; + uint8_t data[32]; + bool is_unicast; + Read err; + }; + + IEBusRx() = default; + IEBusRx(const IEBusRx &) = delete; + IEBusRx &operator=(const IEBusRx &) = delete; + + ~IEBusRx() { + if (instance_ != this) + return; + irq_set_enabled(irq_, false); + irq_remove_handler(irq_, irq_handler); + pio_set_irq0_source_enabled( + pio_, pio_get_rx_fifo_not_empty_interrupt_source(sm_), false); + pio_sm_set_enabled(pio_, sm_, false); + pio_remove_program_and_unclaim_sm(&iebus_rx_program, pio_, sm_, offset_); + instance_ = nullptr; + } + + // Claims and configures the iebus_rx SM and its interrupt, but doesn't start + // the SM. + void init(PIO pio, uint pin_rx, uint16_t address) { + hard_assert(instance_ == nullptr); + instance_ = this; + pio_ = pio; + pin_ = pin_rx; + self_addrp_ = ((uint32_t)address << 1) | parity(address); + offset_ = (uint)pio_add_program(pio_, &iebus_rx_program); + sm_ = (uint)pio_claim_unused_sm(pio_, true); + + gpio_init(pin_); + gpio_set_dir(pin_, false); // CAN/AVCLAN RX; PIO reads it without a claim + gpio_set_input_enabled(pin_, true); + pio_sm_set_consecutive_pindirs(pio_, sm_, pin_, 1, false); + + pio_sm_config cfg = iebus_rx_program_get_default_config(offset_); + sm_config_set_in_pins(&cfg, pin_); + sm_config_set_jmp_pin(&cfg, pin_); + sm_config_set_clkdiv(&cfg, bus_clkdiv()); + + // Entry is `wait_read`, not the program start -- offset 0 is rx_startbit, + // which would fall through into the header reads before the driver's own + // jmp to it, leaving every later field one read out of step. + pio_sm_init(pio_, sm_, offset_ + iebus_rx_wrap_target, &cfg); + prepare_ack(); + begin_frame(); + + pio_set_irq0_source_enabled( + pio_, pio_get_rx_fifo_not_empty_interrupt_source(sm_), true); + irq_ = (uint)pio_get_irq_num(pio_, 0); + irq_set_exclusive_handler(irq_, irq_handler); + irq_set_priority(irq_, PICO_HIGHEST_IRQ_PRIORITY); + irq_set_enabled(irq_, true); + } + + PIO pio() const { return pio_; } + uint32_t sm_mask() const { return 1U << sm_; } + + // Muted means we still listen, we just don't answer: no ack, ever. Disarming + // the latch is what makes that true of the SM as well. + void mute(bool mute) { + muted_ = mute; + sync_ack_arming(); + } + + // Disarm RX ACK'ing behavior; called prior to frame TX to prevent + // self-ACK'ing. Safe to rearm any time after sending controller addr. + void disarm_ack() { + transmitting_ = true; + sync_ack_arming(); + } + + // Rearm RX ACK'ing behavior; called after sending controller addr. + void rearm_ack() { + transmitting_ = false; + sync_ack_arming(); + } + + // Buffered frames exist to be read + bool frame_pending() const { return rxq_tail_ != rxq_head_; } + + // Only valid while frame_pending(). + const RxFrame &frame() const { return rxq_[rxq_tail_]; } + + void release() { rxq_tail_ = (rxq_tail_ + 1) & (RXQ_N - 1); } + +private: + // True when publish() would have nowhere to put the frame being received. + bool queue_full() const { + return ((rxq_head_ + 1) & (RXQ_N - 1)) == rxq_tail_; + } + + static void __time_critical_func(irq_handler)() { instance_->isr(); } + + void __time_critical_func(isr)() { + while (!pio_sm_is_rx_fifo_empty(pio_, sm_)) { + const auto slot = (uint16_t)pio_sm_get(pio_, sm_); + uint16_t value = 0; + + switch (state_) { + case RxField::Broadcast: + building_.is_unicast = (slot & 1U) != 0U; + state_ = RxField::Controller; + break; + + case RxField::Controller: + // Judged on the peripheral slot: that read is already running, and a + // restart now would take its slot for the next frame's broadcast bit. + controller_ok_ = update_value(slot, &value); + building_.controller_addr = value; + state_ = RxField::Peripheral; + break; + + case RxField::Peripheral: { + // Past arbitration our own frame is the tx SM's business: it reads + // its own acks, and a copy here would only cost a queue slot. A frame + // we owe an ack for is refused when there is no room for it -- the + // NAK asks the sender to send it again, rather than losing it behind + // an ack we can't honour. Both come before the field checks: a frame + // being given up needs no parity verdict, and reporting one would + // name the wrong cause for the same NAK. + const bool ours = + building_.controller_addr == (uint16_t)(self_addrp_ >> 1); + const bool refuse = + !ours && queue_full() && pio_interrupt_get(pio_, ack_latch); + + if (ours || refuse) { + if (refuse) + rxq_refused_ = rxq_refused_ + 1; + // Withdrawing the ack is part of giving the frame up; its slot is + // 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 + // 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() + // 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); + begin_frame(); + break; + } + + if (!controller_ok_) { + publish(BAD_CONTROLLER_PARITY); + break; + } + if (!update_value(slot, &value)) { + publish(BAD_PERIPHERAL_PARITY); + break; + } + building_.peripheral_addr = value; + next_after_ack(RxField::Control, W_CONTROL); + break; + } + + case RxField::Control: + if (!update_value(slot, &value)) { + publish(BAD_CONTROL_PARITY); + break; + } + building_.control = (uint8_t)value; + next_after_ack(RxField::Length, W_BYTE); + break; + + case RxField::Length: + if (!update_value(slot, &value)) { + publish(BAD_LENGTH_PARITY); + break; + } + building_.length = (uint8_t)value; + if (value == 0 || value > sizeof(building_.data)) { + publish(BAD_LENGTH_RANGE); + break; + } + next_after_ack(RxField::Data, W_BYTE); + break; + + case RxField::Data: + if (!update_value(slot, &value)) { + publish(BAD_DATA_PARITY); + break; + } + building_.data[data_i_++] = (uint8_t)value; + if (data_i_ >= building_.length) + publish(Read{0}); + else + next_after_ack(RxField::Data, W_BYTE); + break; + } + } + } + + // Start (or restart) a frame. The start-bit block is dispatched like any + // other field, except the streamed instruction is a jmp instead of a read's + // setup. It only falls through on a start bit, so the header's reads queue + // right behind it. Four words fill the FIFO, which is empty here: the SM + // takes each word before pushing the slot that ends a frame. + void __time_critical_func(begin_frame)() { + state_ = RxField::Broadcast; + building_ = {}; + data_i_ = 0; + pio_sm_put(pio_, sm_, + pio_encode_jmp(offset_ + iebus_rx_offset_rx_startbit)); + enqueue_rx(W_BROADCAST, false); + enqueue_rx(W_ADDR, false); + enqueue_rx(W_ADDR, true); + } + + // Load our address (with parity) into the SM's Y register. + // The SM compares read values to Y to set the ack latch and trigger ACK'ing. + // SM must be stopped with an empty FIFO. + void prepare_ack() { + pio_sm_put(pio_, sm_, self_addrp_); + pio_sm_exec(pio_, sm_, pio_encode_pull(false, true)); + pio_sm_exec(pio_, sm_, pio_encode_out(pio_y, 32)); + ack_armed_ = true; + } + + // Bring the latch's arming in line with the reasons to withhold an ack. + // Disarming inverts the Y register (self address + parity), which leaves the + // upper 19 bits set and so prevents any read from matching. + void sync_ack_arming() { + const bool arm = !muted_ && !transmitting_; + if (arm == ack_armed_) + return; + pio_sm_exec_wait_blocking(pio_, sm_, pio_encode_mov_not(pio_y, pio_y)); + ack_armed_ = arm; + } + + // One read's script word: [has_ack 31][zeros 30:16][instr 15:0]. The SM reads + // bits + 1 (the field plus its parity); the instruction zeroes its parity + // accumulator and leaves that many bits before the OSR threshold. `bits` must + // be <= 13: at 14 the instruction would encode as `out x, 0`, which the ISA + // reads as 32. `has_ack` has the SM consume the ack slot after the field. + static constexpr uint32_t encode_rx(uint8_t bits, bool has_ack) { + return ((uint32_t)has_ack << 31) | pio_encode_out(pio_x, 14 - bits); + } + + // Update `*value` with the `read` value. Returns true for a correct parity. + template static bool update_value(uint16_t read, uint16_t *value) { + *value = (uint16_t)((read >> 1) & ((1U << N) - 1U)); + return parity(*value) == (unsigned)(read & 1U); + } + + void __time_critical_func(enqueue_rx)(uint8_t bits, bool has_ack) { + pio_sm_put_blocking(pio_, sm_, encode_rx(bits, has_ack)); + } + + // New frames are dropped if the rx queue is full. A frame we acked is not + // among them: there was room for it at the peripheral slot, and from here the + // queue only gains space. What is left to lose is what no ack was owed for. + void __time_critical_func(publish)(Read err) { + // The SM NAKs bad parity itself; this vetoes what only we can judge + // (controller parity, length range) before its late read of the latch. + if (err != Read{0}) + pio_interrupt_clear(pio_, ack_latch); + building_.err = err; + if (queue_full()) { + rxq_drops_ = rxq_drops_ + 1; // Never stall the bus for a lagging reader + } else { + rxq_[rxq_head_] = building_; + rxq_head_ = (rxq_head_ + 1) & (RXQ_N - 1); + } + 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 + // context. + static inline IEBusRx *instance_ = nullptr; + + PIO pio_; + uint sm_; + uint offset_; + uint pin_; + uint irq_; + + // Our address as the rx SM sees it: a slot is the field plus its parity bit, + // and parity is a function of the field, so there is exactly one legal slot + // value for us. + uint32_t self_addrp_; + + bool ack_armed_ = false; // hardware: y holds self_addrp_, or its complement + bool muted_ = false; // we don't answer on the bus + bool transmitting_ = false; // our own frame is on the wire + + std::array rxq_ = {}; + volatile uint32_t rxq_head_ = 0; // written by the ISR only + volatile uint32_t rxq_tail_ = 0; // written by the reader only + // Lost: no ack was owed, so there was nothing to refuse -- a broadcast, or a + // unicast addressed elsewhere. + volatile uint32_t rxq_drops_ = 0; + // NAK'd for want of room; the sender still owns the frame and sends it again. + volatile uint32_t rxq_refused_ = 0; + + RxFrame building_ = {}; + RxField state_ = RxField::Broadcast; + uint8_t data_i_ = 0; + 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 +// ack slot on the rx engine's behalf. Both live on one PIO because they share +// the TX pin. +// +// Everything past the arbitration window is queued, not sent, so the fields +// report nothing and the frame's verdict comes from send_done. +class IEBusTx { + // The flag the SM parked on. Sticky until reported; nothing more is queued + // meanwhile. + enum class Fault : uint8_t { None, Nak, Mismatch }; + +public: + // The ack match lives in the rx SM but exists for our sake: it has to be + // parked while our own address is on the wire, and put back the moment we + // stop transmitting -- including when we lose arbitration mid-field. + explicit IEBusTx(IEBusRx &rx) : rx_(rx) {} + IEBusTx(const IEBusTx &) = delete; + IEBusTx &operator=(const IEBusTx &) = delete; + + // Hands both SMs and their program memory back. Stopping them leaves the pad + // at its last driven level, which is recessive -- the same property mute + // relies on. The pad keeps its PIO function select: handing it back to SIO + // would float the line, and a floating bus reads dominant. + ~IEBusTx() { + if (!claimed_) + return; + pio_set_sm_mask_enabled(pio_, sm_mask(), false); + pio_remove_program_and_unclaim_sm(&iebus_tx_program, pio_, tx_sm_, + tx_offset_); + pio_remove_program_and_unclaim_sm(&iebus_ack_program, pio_, ack_sm_, + ack_offset_); + } + + // Claims and configures both SMs, but doesn't start them. + void init(PIO pio, uint pin_rx, uint pin_tx) { + pio_ = pio; + 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); + ack_sm_ = (uint)pio_claim_unused_sm(pio_, true); + + // Drive the pad recessive *before* handing its function select to this PIO, + // so the handover cannot glitch the bus dominant. Level and direction are + // block-wide registers, so setting them through one SM covers both. + pio_sm_set_pins_with_mask(pio_, tx_sm_, 1U << pin_tx, 1U << pin_tx); + pio_sm_set_consecutive_pindirs(pio_, tx_sm_, pin_tx, 1, true); + pio_gpio_init(pio_, pin_tx); + + pio_sm_config tx_cfg = iebus_tx_program_get_default_config(tx_offset_); + sm_config_set_out_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. + sm_config_set_jmp_pin(&tx_cfg, pin_rx); + sm_config_set_clkdiv(&tx_cfg, bus_clkdiv()); + + // Entry is `reset`, not the program start -- offset 0 is handle_nak, which + // 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_config ack_cfg = iebus_ack_program_get_default_config(ack_offset_); + sm_config_set_sideset_pins(&ack_cfg, pin_tx); + sm_config_set_clkdiv(&ack_cfg, bus_clkdiv()); + + pio_sm_init(pio_, ack_sm_, ack_offset_, &ack_cfg); + claimed_ = true; + } + + PIO pio() const { return pio_; } + uint32_t sm_mask() const { return (1U << tx_sm_) | (1U << ack_sm_); } + + // Must only be called between transactions to ensure the TX pin is left + // high/recessive. + void mute(bool mute) { + pio_set_sm_mask_enabled(pio_, sm_mask(), !mute); + muted_ = mute; + } + + bool is_muted() const { return muted_; } + + // Send start and broadcast bits. + Send send_header(bool is_unicast) { + if (muted_) + return MUTED; + + rx_.disarm_ack(); + + // Jump to the tx_startbit section from the default "reset" stall on pull + pio_sm_exec(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 + // 0 (one bit) with the parity slot standing in for the bit itself. + return arbitrate(is_unicast ? 1U : 0U, 0); + } + + Send send_controller_addr(uint16_t addr) { + // Last field of the arbitration window; no acknowledge slot follows it. + const Send err = arbitrate(addr, 12); + 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_) + return MUTED; + if (!check()) + put(encode_tx(len, (uint16_t)bits, true, expect_ack)); + return Send{0}; + } + + Send send_done(uint8_t *data_index) { + wait_done(); + check(); + const Fault fault = fault_; + fault_ = Fault::None; + + if (fault == Fault::None) + return Send{0}; + if (fault == Fault::Mismatch) + return CONTENDED_BUS; + switch (failed_word_) { + case 0: return NAK_ADDRESS; + case 1: return NAK_CONTROL; + case 2: return NAK_MESSAGE_LENGTH; + default: *data_index = (uint8_t)(failed_word_ - 3); return NAK_DATA; + } + } + +private: + // One field's word: [len:4][data+parity:len+1][has_ack:1][nak_ok:1][pad]. The + // SM sends count + 1 bits, so the count is `len` and the parity bit the + // driver appends rides along as the extra one. `has_ack` emits the + // acknowledge slot; a NAK in it raises nak_irq unless `nak_ok`, i.e. unless + // we don't `expect_ack`. + static constexpr uint32_t encode_tx(size_t len, uint16_t bits, bool has_ack, + bool expect_ack) { + const auto n = (uint8_t)(len + 1); + // Masked so a bit above the field (the broadcast bit's copy of itself, when + // len is 0) can't spill into the count. + const uint16_t with_parity = + ((bits << 1) | parity(bits)) & ((1U << n) - 1U); + uint32_t word = (uint32_t)len << 28; + word |= with_parity << (28 - n); + word |= (uint32_t)has_ack << (27 - n); + word |= (uint32_t)!expect_ack << (26 - n); + 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 + // the frame has failed, now or earlier. + bool check() { + const bool lost_arb = pio_interrupt_get(pio_, iebus_tx_lost_arb_irq); + if (!lost_arb && !pio_interrupt_get(pio_, iebus_tx_nak_irq)) + return fault_ != Fault::None; + + // 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 + // frame. + failed_word_ = + (uint8_t)(words_ - pio_sm_get_tx_fifo_level(pio_, tx_sm_) - 1U); + pio_sm_clear_fifos(pio_, tx_sm_); + + if (lost_arb) { + pio_interrupt_clear(pio_, iebus_tx_lost_arb_irq); + // The winner's frame is still arriving; put the ack match back so we can + // answer it if it turns out to be addressed to us. + rx_.rearm_ack(); + fault_ = Fault::Mismatch; + } else { + pio_interrupt_clear(pio_, iebus_tx_nak_irq); + fault_ = Fault::Nak; + } + return true; + } + + // Only a full FIFO waits, and not on a parked SM: it never frees a slot, + // which is why this isn't pio_sm_put_blocking. + void put(uint32_t word) { + while (pio_sm_is_tx_fifo_full(pio_, tx_sm_)) { + if (check()) + return; + tight_loop_contents(); + } + pio_sm_put(pio_, tx_sm_, word); + 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_; + + PIO pio_; + uint tx_sm_; + uint tx_offset_; + uint ack_sm_; + uint ack_offset_; + bool claimed_ = false; // init() ran, so the destructor has something to undo + + bool muted_ = false; + + // Words put since arbitration was won, taken by the SM or not. Its flags park + // it, which freezes the FIFO, so the word it failed on is the last one it + // pulled: words_ - level - 1. Wrapping is harmless; the FIFO holds at most 8. + uint8_t words_; + uint8_t failed_word_; + Fault fault_; +}; + +// The bus as a whole. Only it can hold the invariants that span the two +// engines: they sit on different PIOs and so must be started by one +// synchronised CTRL write, and muting has to reach both. +class Phy { +public: + Phy() = default; + Phy(const Phy &) = delete; + Phy &operator=(const Phy &) = delete; + + ~Phy() { activity_leds_deinit(); } + + void init(uint16_t address) { + rx_.init(pio0, IEBUS_RX, address); + tx_.init(pio1, IEBUS_RX, IEBUS_TX); + + // Cross-PIO IRQ partners must share a clock divider *and* have it restarted + // in the same cycle (S11.4). One CTRL write starts all three in step. + // Reception runs from here on, independent of the main loop. + pio_enable_sm_multi_mask_in_sync(rx_.pio(), 0, rx_.sm_mask(), + tx_.sm_mask()); + + activity_leds_init(); + } + + // "Muted" means we still listen but neither transmit nor ACK. Ordered so no + // ack request can be stranded across the transition: the rx engine stops + // asking before its driver goes away, and any flag it did leave behind is + // dropped before that driver comes back -- the iebus_ack SM's `wait 1 irq` + // clears an already-set flag and drives the slot immediately. + void mute(bool mute) { + muted_ = mute; + if (mute) { + rx_.mute(true); + tx_.mute(true); + } else { + pio_interrupt_clear(tx_.pio(), ack_irq); + tx_.mute(false); + rx_.mute(false); + } + } + + bool is_muted() const { return muted_; } + + IEBusRx &rx() { return rx_; } + IEBusTx &tx() { return tx_; } + +private: + // Point one mirror SM at one src -> dst pair. Only the destination gets + // `pio_gpio_init`: taking the function select of a source pin would hand it + // to this PIO block and cut whoever actually drives it (IEBUS_TX) loose. + // 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. + void pin_mirror_sm_init(uint sm, uint offset, uint src, uint dst) { + PIO pio = tx_.pio(); + gpio_set_input_enabled(src, true); + pio_gpio_init(pio, dst); + pio_sm_set_consecutive_pindirs(pio, sm, dst, 1, true); + + pio_sm_config cfg = pin_mirror_program_get_default_config(offset); + sm_config_set_in_pins(&cfg, src); + sm_config_set_out_pins(&cfg, dst, 1); + // Default clkdiv: one copy per system clock, so the LED tracks the line far + // faster than a bit period. + + pio_sm_init(pio, sm, offset, &cfg); + pio_sm_set_enabled(pio, sm, true); + } + + // Initialize hardware to display bus TX/RX activity on two LEDs + // The bus idles HIGH, so the PIO program mirrors inverted pin state from the + // 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 + void activity_leds_init() { + PIO pio = tx_.pio(); // iebus_rx fills its PIO + // Indicators are cosmetic; never fail the bus bring-up for them. + if (!pio_can_add_program(pio, &pin_mirror_program)) + return; + const int msm_rx = pio_claim_unused_sm(pio, false); + if (msm_rx < 0) + return; + const int msm_tx = pio_claim_unused_sm(pio, false); + if (msm_tx < 0) { + pio_sm_unclaim(pio, (uint)msm_rx); + return; + } + + led_offset_ = (uint)pio_add_program(pio, &pin_mirror_program); + led_sm_rx_ = (uint)msm_rx; + led_sm_tx_ = (uint)msm_tx; + leds_claimed_ = true; + pin_mirror_sm_init(led_sm_rx_, led_offset_, IEBUS_RX, LED_RX); + pin_mirror_sm_init(led_sm_tx_, led_offset_, IEBUS_TX, LED_TX); + } + + // The two mirrors share one copy of the program, so the slots go back + // individually but the program memory only once -- which is why this isn't + // two pio_remove_program_and_unclaim_sm calls. + void activity_leds_deinit() { + if (!leds_claimed_) + return; + PIO pio = tx_.pio(); + pio_sm_set_enabled(pio, led_sm_rx_, false); + pio_sm_set_enabled(pio, led_sm_tx_, false); + pio_sm_unclaim(pio, led_sm_rx_); + pio_sm_unclaim(pio, led_sm_tx_); + pio_remove_program(pio, &pin_mirror_program, led_offset_); + leds_claimed_ = false; + } + + IEBusRx rx_; + IEBusTx tx_{rx_}; + bool muted_ = false; + + uint led_sm_rx_; + uint led_sm_tx_; + uint led_offset_; + bool leds_claimed_ = false; +}; + +Phy phy; } // namespace -extern "C" void phy_init() { - gpio_init(IEBUS_TX); - gpio_init(IEBUS_RX); - gpio_set_dir(IEBUS_TX, true); // CAN/AVCLAN TX - gpio_set_dir(IEBUS_RX, false); // CAN/AVCLAN RX +extern "C" void phy_init(uint16_t address) { phy.init(address); } - bool success = - pio_claim_free_sm_and_add_program(&iebus_rx_program, &pio, &sm, &offset); - hard_assert(success); +extern "C" void phy_mute(bool mute) { phy.mute(mute); } - iebus_rx_program_init(pio, sm, offset, IEBUS_RX, IEBUS_TX); - pio_sm_exec(pio, sm, pio_encode_irq_set(false, should_ack_irq)); +extern "C" bool phy_is_muted() { return phy.is_muted(); } -} - - -extern "C" void phy_mute(bool mute) {} - -extern "C" bool phy_is_muted() { return true; } - -extern "C" bool phy_active() { return false; } +extern "C" bool phy_frame_pending() { return phy.rx().frame_pending(); } extern "C" void phy_guard_enter() {} - extern "C" void phy_guard_leave() {} -extern "C" Read phy_read_startbit() { return BAD_STARTBIT; } +// --- Reads: served from the buffered frame ---------------------------------- +// +// The engine has already checked every parity bit and driven every ack slot, so +// these only hand back fields. The error it recorded is reported by the first +// call of the frame; the caller abandons the frame on it, which releases it. -extern "C" Send phy_send_startbit() { return MUTED; } +extern "C" Read phy_read_header(bool *is_unicast) { + const IEBusRx::RxFrame &frame = phy.rx().frame(); + if (frame.err != Read{0}) { + phy.rx().release(); + return frame.err; + } + *is_unicast = frame.is_unicast; + return Read{0}; +} -extern "C" Send phy_read_ack() { return MUTED; } +extern "C" Read phy_read_controller_addr(uint16_t *addr) { + *addr = phy.rx().frame().controller_addr; + return Read{0}; +} -extern "C" void phy_send_ack() {} +extern "C" Read phy_read_peripheral_addr(uint16_t *addr) { + *addr = phy.rx().frame().peripheral_addr; + return Read{0}; +} -extern "C" void phy_send_bit(Bit bit) {} +extern "C" Read phy_read_control(uint8_t *control) { + *control = phy.rx().frame().control; + return Read{0}; +} -extern "C" Bit phy_send_bits_u8(const uint8_t *bits, int8_t len) { - return Bit::bit_zero; -}; +extern "C" Read phy_read_length(uint8_t *length) { + *length = phy.rx().frame().length; + return Read{0}; +} -extern "C" Bit phy_send_bits_u16(const uint16_t *bits, int8_t len) { - return Bit::bit_zero; -}; +extern "C" Read phy_read_data(uint8_t *data) { + static uint8_t idx; + const IEBusRx::RxFrame &frame = phy.rx().frame(); + if (idx >= frame.length) + idx = 0; + *data = frame.data[idx++]; + if (idx >= frame.length) { // Frame consumed + idx = 0; + phy.rx().release(); + } + return Read{0}; +} -extern "C" Bit phy_send_byte(const uint8_t *byte) { return Bit::bit_zero; }; +extern "C" Send phy_send_header(bool is_unicast) { + return phy.tx().send_header(is_unicast); +} -extern "C" Bit phy_read_bits_u8(uint8_t *bits, uint8_t len) { - return Bit::bit_zero; -}; +extern "C" Send phy_send_controller_addr(uint16_t addr) { + return phy.tx().send_controller_addr(addr); +} -extern "C" Bit phy_read_bits_u16(uint16_t *bits, int8_t len) { - return Bit::bit_zero; -}; +extern "C" Send phy_send_peripheral_addr(uint16_t addr, bool expect_ack) { + return phy.tx().send_field(12, addr, expect_ack); +} -extern "C" Bit phy_read_byte(uint8_t *byte) { return Bit::bit_zero; }; +extern "C" Send phy_send_control(uint8_t control, bool expect_ack) { + return phy.tx().send_field(4, control, expect_ack); +} -#if !defined(NDEBUG) && defined(MEASURE_BUS) +extern "C" Send phy_send_length(uint8_t length, bool expect_ack) { + return phy.tx().send_field(8, length, expect_ack); +} + +extern "C" Send phy_send_data(uint8_t data, bool expect_ack) { + return phy.tx().send_field(8, data, expect_ack); +} + +extern "C" Send phy_send_done(uint8_t *data_index) { + return phy.tx().send_done(data_index); +} + +#ifndef NDEBUG + +void phy_set_dominant() {} +void phy_set_recessive() {} + + #ifdef MEASURE_BUS // Sample and dump bus bit timing over the serial link (REPL `M`). -void phy_measure(void); +void phy_measure(void) {}; + #endif #endif diff --git a/src/avclan/target/pico2/stdio.cc b/src/avclan/target/pico2/stdio.cc index 7de9405..347adf0 100644 --- a/src/avclan/target/pico2/stdio.cc +++ b/src/avclan/target/pico2/stdio.cc @@ -1,9 +1,83 @@ +#include +#include +#include + #include "hal/stdio.h" +#include "pico/stdio.h" #include "pico/stdio_usb.h" +#include "pico/time.h" +#include "tusb.h" // IWYU pragma: keep -extern "C" void stdio_init() { stdio_usb_init(); } +// stdio_write_nonblock() is all-or-nothing. The CDC TX FIFO must hold the +// largest buffer the app writes; that is a text frame log line (Frame::print), +// ~186 bytes at MAXLENGTH=32. +static_assert(CFG_TUD_CDC_TX_BUFSIZE >= 256, + "CDC TX FIFO too small to hold a whole frame log line"); + +namespace { +// Set when a dropped buffer had no room for its indicator; the next call emits +// it. The AVR port instead overwrites the last three queued bytes, which +// collapses a burst of drops to a single '!' line in exactly the same way. +bool drop_indicator_pending = false; + +// stdout's only backend here is the usb driver, which writes into the same CDC +// TX FIFO measured below, so printf and this share one stream and one order. +void queue(const char *str, int len) { + stdio_put_string(str, len, false, false); +} +} // namespace + +extern "C" void stdio_init() { + stdio_usb_init(); + stdio_set_translate_crlf(&stdio_usb, false); + // pico_stdio wraps printf/puts/putchar straight onto the CDC, but not + // fputs/fwrite. Unbuffered stdout keeps the newlib path in step with them + // instead of stranding whole strings in the FILE buffer. + setvbuf(stdout, nullptr, _IONBF, 0); +} + +// Overrides the SDK's weak newlib hook, which waits forever. EAGAIN rather than +// a 0-length read: newlib's refill skips a stream that has ever seen EOF. +extern "C" int _read(int handle, char *buffer, int length) { + if (handle != STDIN_FILENO) { + errno = EBADF; + return -1; + } + + const int count = stdio_get_until(buffer, length, make_timeout_time_us(0)); + if (count < 0) { + errno = EAGAIN; + return -1; + } + + return count; +} extern "C" bool stdio_write_nonblock(const void *buf, uint8_t len) { + uint32_t avail = tud_cdc_write_available(); + + if (drop_indicator_pending) { + if (avail < 3) + return false; + queue("!\n", 2); + drop_indicator_pending = false; + avail -= 2; + } + + if (len == 0) + return true; + + if (len <= avail) { + queue(static_cast(buf), len); + return true; + } + + // Every buffer ends with '\n', so the FIFO already sits at a line boundary. + if (avail >= 3) + queue("!\n", 2); + else + drop_indicator_pending = true; + return false; } diff --git a/src/sniffer.cc b/src/sniffer.cc index f26cf44..fbb4ae9 100644 --- a/src/sniffer.cc +++ b/src/sniffer.cc @@ -103,7 +103,7 @@ int main() { } // stdin must be non-blocking: yielding EOF when idle/empty - if (int readkey = getchar(); readkey != EOF) { + if (int readkey = fgetc(stdin); readkey != EOF) { switch (readkey) { case '?': print_help(); break; case 'v': toggle_flag(&verbose, "Verbose errors:"); break; @@ -219,7 +219,19 @@ int main() { hexChars[0] = hexChars[1] = 0; seqIsUnicast = false; break; + case '\r': // Eat carriage return too + if (readBinary) + goto DEFAULT; + [[fallthrough]]; case '\n': + if (readSeq && !readBinary && seqIdx == 0) { + // Nothing to send, so leave the mode instead: an escape for an + // entry started by accident. + readSeq = false; + hexDigit = hexChars[0] = hexChars[1] = 0; + printAllFrames = lastPrintAllFrames; + break; + } if (readSeq && seqIdx > 0) { if (readBinary) { if (data_tmp[seqIdx - 1] == 0x17) { @@ -278,6 +290,10 @@ int main() { if (readBinary) { data_tmp[seqIdx++] = readkey; } else { + // Only take valid hex digits + if (isxdigit(readkey) == 0) + break; + hexChars[hexDigit++] = readkey; if (hexDigit == 2) { @@ -302,6 +318,8 @@ int main() { } } } // switch (readkey) + } else { + clearerr(stdin); } // if (readkey != EOF) } return 0;