PIO proof-of-concept

This commit is contained in:
Allen Hill
2026-09-12 00:25:32 -07:00
parent 53c5abb3e9
commit 9efbb01ccf
5 changed files with 280 additions and 166 deletions
+1 -1
View File
@@ -61,7 +61,7 @@
},
"environment": {
"PICO_PLATFORM": "rp2350-arm-s",
"PICO_BOARD": "pimoroni_pico_plus2w_rp2350"
"PICO_BOARD": "pimoroni_pico_plus2_w_rp2350"
}
},
{
+13 -2
View File
@@ -1,4 +1,3 @@
list(APPEND PICO_CONFIG_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/pimoroni_pico_plus2w_rp2350.h)
pico_sdk_init()
target_sources(avclan PRIVATE
@@ -8,11 +7,15 @@ target_sources(avclan PRIVATE
board.cc
stdio.cc
)
pico_generate_pio_header(avclan ${CMAKE_CURRENT_LIST_DIR}/iebus.pio)
target_link_libraries(avclan PUBLIC
pico_stdlib pico_stdio_usb
pico_stdlib pico_stdio_usb hardware_pio hardware_dma pico_multicore
)
pico_enable_stdio_usb(avclan 1)
# Needs separate/new executable target because `pico_add_extra_outputs` only
# works on locally (to this file/directory) defined targets
add_executable(mockingboard_pico
${avclan-mockingboard_SOURCE_DIR}/src/sniffer.cc
)
@@ -30,3 +33,11 @@ unset(_tinyusb_sources)
pico_add_extra_outputs(mockingboard_pico)
pico_set_float_implementation(mockingboard_pico none)
pico_set_double_implementation(mockingboard_pico none)
pico_set_program_name(mockingboard_pico mockingboard)
pico_set_program_description(mockingboard_pico
"Emulate an external CD Changer on an AVCLAN bus to add Bluetooth and aux-input audio to older Toyota's"
)
pico_set_program_version(mockingboard_pico
"${avclan-mockingboard_VERSION}")
pico_set_program_url(mockingboard_pico "https://github.com/halleysfifthinc/AVCLAN-Mockingboard")
+186
View File
@@ -0,0 +1,186 @@
.define public parity_irq 7 ; (polled) Signal BAD_PARITY
.define public should_ack_irq 6 ; (system)
; This is used to set the clkdiv.
; The delay cycle counts in read_bit (AVCLAN_READBIT_THRESHOLD) and iebus_ack
; (AVCLAN_BIT0_LOGIC_0) must be updated in sync with this variable
.define public CYCLES_PER_READBIT_PERIOD 64
.pio_version 1
.program iebus_rx
; Initial TX Encoding
; | 19:4 | 3:0 |
; | Instr | # bit/instr |
;
; 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 |
;
; where each slot is encoded as
; | MSB .................... LSB |
; | 15-N zeros | N bits | parity |
.in 1 left
.out 0 right auto 20
.mov_status irq set should_ack_irq
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.
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
read_bit:
;;; Resynchronize at each 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
;;; 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 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
;;; ack
set x 3
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
.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
.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 |
;
; Parity is generated by the driver and appended as the last (least significant)
; bit of the data field
.in 1 left
.out 1 left
.define public nak_irq 7
.define public lost_arb_irq 6
handle_nak:
out x, 1
jmp x-- reset ; Reset if NAK was expected
irq wait nak_irq
reset:
.wrap_target
pull
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)
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
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.
;
; `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 ===
;;; Isolate first bit (for left shifting)
; mov osr, ::isr ; Reverse ISR so its LSB becomes the MSB in osr
; out x, 1 ; Set LSB of x from MSB of osr
;;; Incrementing (counting up)
; set x 0
; set x ~x ; x == 0xffffffff
; 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`)
+80 -1
View File
@@ -1,11 +1,90 @@
#include "hal/phy.h"
#include "avclan.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include "iebus.pio.h"
#include <hardware/clocks.h>
#define TICK_US 1000000
#include "timing.h"
using namespace avclan;
using enum detail::Error::Read;
using enum detail::Error::Send;
extern "C" void phy_init() {}
namespace {
constexpr int IEBUS_TX = 16;
constexpr int IEBUS_RX = 17;
// Activity indicators: each follows its bus pin exactly, so an LED wired
// supply -> resistor -> pin is lit while that line is dominant (low). The bus
// pins themselves stay unloaded.
constexpr int LED_RX = 8;
constexpr int LED_TX = 26;
PIO pio;
uint sm;
uint offset;
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);
}
// 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);
}
} // 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
bool success =
pio_claim_free_sm_and_add_program(&iebus_rx_program, &pio, &sm, &offset);
hard_assert(success);
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" void phy_mute(bool mute) {}
@@ -1,162 +0,0 @@
/*
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
// -----------------------------------------------------
// NOTE: THIS HEADER IS ALSO INCLUDED BY ASSEMBLER SO
// SHOULD ONLY CONSIST OF PREPROCESSOR DIRECTIVES
// -----------------------------------------------------
// This header may be included by other board headers as "boards/pimoroni_pico_plus2w_rp2350.h"
// pico_cmake_set PICO_PLATFORM=rp2350
#ifndef _BOARDS_PIMORONI_PICO_PLUS2W_RP2350_H
#define _BOARDS_PIMORONI_PICO_PLUS2W_RP2350_H
// For board detection
#define PIMORONI_PICO_PLUS2_RP2350
#define PIMORONI_PICO_PLUS2W_RP2350
// --- BOARD SPECIFIC ---
#define SPICE_SPI 0
#define SPICE_TX_MISO_PIN 32
#define SPICE_RX_CS_PIN 33
#define SPICE_NETLIGHT_SCK_PIN 34
#define SPICE_RESET_MOSI_PIN 35
#define SPICE_PWRKEY_BL_PIN 36
#define PIMORONI_PICO_PLUS2_USER_SW_PIN 45
#define PIMORONI_PICO_PLUS2_PSRAM_CS_PIN 47
// --- UART ---
#ifndef PICO_DEFAULT_UART
#define PICO_DEFAULT_UART 0
#endif
#ifndef PICO_DEFAULT_UART_TX_PIN
#define PICO_DEFAULT_UART_TX_PIN 0
#endif
#ifndef PICO_DEFAULT_UART_RX_PIN
#define PICO_DEFAULT_UART_RX_PIN 1
#endif
// --- LED ---
#ifndef PICO_DEFAULT_LED_PIN
#define PICO_DEFAULT_LED_PIN 25
#endif
// no PICO_DEFAULT_WS2812_PIN
// --- I2C ---
#ifndef PICO_DEFAULT_I2C
#define PICO_DEFAULT_I2C 0
#endif
#ifndef PICO_DEFAULT_I2C_SDA_PIN
#define PICO_DEFAULT_I2C_SDA_PIN 4
#endif
#ifndef PICO_DEFAULT_I2C_SCL_PIN
#define PICO_DEFAULT_I2C_SCL_PIN 5
#endif
// --- SPI ---
#ifndef PICO_DEFAULT_SPI
#define PICO_DEFAULT_SPI 0
#endif
#ifndef PICO_DEFAULT_SPI_SCK_PIN
#define PICO_DEFAULT_SPI_SCK_PIN SPICE_NETLIGHT_SCK_PIN
#endif
#ifndef PICO_DEFAULT_SPI_TX_PIN
#define PICO_DEFAULT_SPI_TX_PIN SPICE_RESET_MOSI_PIN
#endif
#ifndef PICO_DEFAULT_SPI_RX_PIN
#define PICO_DEFAULT_SPI_RX_PIN SPICE_TX_MISO_PIN
#endif
#ifndef PICO_DEFAULT_SPI_CSN_PIN
#define PICO_DEFAULT_SPI_CSN_PIN SPICE_RX_CS_PIN
#endif
// --- FLASH ---
#define PICO_BOOT_STAGE2_CHOOSE_W25Q080 1
#ifndef PICO_FLASH_SPI_CLKDIV
#define PICO_FLASH_SPI_CLKDIV 2
#endif
// pico_cmake_set_default PICO_FLASH_SIZE_BYTES = (16 * 1024 * 1024)
#ifndef PICO_FLASH_SIZE_BYTES
#define PICO_FLASH_SIZE_BYTES (16 * 1024 * 1024)
#endif
#ifndef CYW43_WL_GPIO_COUNT
#define CYW43_WL_GPIO_COUNT 3
#endif
#ifndef CYW43_WL_GPIO_LED_PIN
#define CYW43_WL_GPIO_LED_PIN 0
#endif
// If CYW43_WL_GPIO_VBUS_PIN is defined then a CYW43 GPIO has to be used to read VBUS.
// This can be passed to cyw43_arch_gpio_get to determine if the device is battery powered.
// PICO_VBUS_PIN and CYW43_WL_GPIO_VBUS_PIN should not both be defined.
// no CYW43_WL_GPIO_VBUS_PIN
// If CYW43_USES_VSYS_PIN is defined then CYW43 uses the VSYS GPIO (defined by PICO_VSYS_PIN) for other purposes.
// If this is the case, to use the VSYS GPIO it's necessary to ensure CYW43 is not using it.
// This can be achieved by wrapping the use of the VSYS GPIO in cyw43_thread_enter / cyw43_thread_exit.
// no CYW43_USES_VSYS_PIN
// The GPIO Pin used to read VBUS to determine if the device is battery powered.
#ifndef PICO_VBUS_PIN
#define PICO_VBUS_PIN 24
#endif
// The GPIO Pin used to monitor VSYS. Typically you would use this with ADC.
// There is an example in adc/read_vsys in pico-examples.
#ifndef PICO_VSYS_PIN
#define PICO_VSYS_PIN 43
#endif
#ifndef PICO_RP2350_A2_SUPPORTED
#define PICO_RP2350_A2_SUPPORTED 1
#endif
// PICO_CONFIG: CYW43_PIN_WL_DYNAMIC, flag to indicate if cyw43 SPI pins can be changed at runtime, type=bool, default=false, advanced=true, group=pico_cyw43_driver
#ifndef CYW43_PIN_WL_DYNAMIC
#define CYW43_PIN_WL_DYNAMIC 1
#endif
// PICO_CONFIG: CYW43_DEFAULT_PIN_WL_REG_ON, gpio pin to power up the cyw43 chip, type=int, default=23, advanced=true, group=pico_cyw43_driver
#ifndef CYW43_DEFAULT_PIN_WL_REG_ON
#define CYW43_DEFAULT_PIN_WL_REG_ON 23u
#endif
// PICO_CONFIG: CYW43_DEFAULT_PIN_WL_DATA_OUT, gpio pin for spi data out to the cyw43 chip, type=int, default=24, advanced=true, group=pico_cyw43_driver
#ifndef CYW43_DEFAULT_PIN_WL_DATA_OUT
#define CYW43_DEFAULT_PIN_WL_DATA_OUT 24u
#endif
// PICO_CONFIG: CYW43_DEFAULT_PIN_WL_DATA_IN, gpio pin for spi data in from the cyw43 chip, type=int, default=24, advanced=true, group=pico_cyw43_driver
#ifndef CYW43_DEFAULT_PIN_WL_DATA_IN
#define CYW43_DEFAULT_PIN_WL_DATA_IN 24u
#endif
// PICO_CONFIG: CYW43_DEFAULT_PIN_WL_HOST_WAKE, gpio (irq) pin for the irq line from the cyw43 chip, type=int, default=24, advanced=true, group=pico_cyw43_driver
#ifndef CYW43_DEFAULT_PIN_WL_HOST_WAKE
#define CYW43_DEFAULT_PIN_WL_HOST_WAKE 24u
#endif
// PICO_CONFIG: CYW43_DEFAULT_PIN_WL_CLOCK, gpio pin for the spi clock line to the cyw43 chip, type=int, default=29, advanced=true, group=pico_cyw43_driver
#ifndef CYW43_DEFAULT_PIN_WL_CLOCK
#define CYW43_DEFAULT_PIN_WL_CLOCK 29u
#endif
// PICO_CONFIG: CYW43_DEFAULT_PIN_WL_CS, gpio pin for the spi chip select to the cyw43 chip, type=int, default=25, advanced=true, group=pico_cyw43_driver
#ifndef CYW43_DEFAULT_PIN_WL_CS
#define CYW43_DEFAULT_PIN_WL_CS 25u
#endif
#endif