From 9efbb01ccf47f3b110ce2f79eec778c7cf4d812e Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Sat, 12 Sep 2026 00:25:32 -0700 Subject: [PATCH] PIO proof-of-concept --- CMakePresets.json | 2 +- src/avclan/target/pico2/CMakeLists.txt | 15 +- src/avclan/target/pico2/iebus.pio | 186 ++++++++++++++++++ src/avclan/target/pico2/phy.cc | 81 +++++++- .../pico2/pimoroni_pico_plus2w_rp2350.h | 162 --------------- 5 files changed, 280 insertions(+), 166 deletions(-) create mode 100644 src/avclan/target/pico2/iebus.pio delete mode 100644 src/avclan/target/pico2/pimoroni_pico_plus2w_rp2350.h diff --git a/CMakePresets.json b/CMakePresets.json index 8054bb6..ba6dbdc 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -61,7 +61,7 @@ }, "environment": { "PICO_PLATFORM": "rp2350-arm-s", - "PICO_BOARD": "pimoroni_pico_plus2w_rp2350" + "PICO_BOARD": "pimoroni_pico_plus2_w_rp2350" } }, { diff --git a/src/avclan/target/pico2/CMakeLists.txt b/src/avclan/target/pico2/CMakeLists.txt index 0bcf0e1..39631f8 100644 --- a/src/avclan/target/pico2/CMakeLists.txt +++ b/src/avclan/target/pico2/CMakeLists.txt @@ -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") diff --git a/src/avclan/target/pico2/iebus.pio b/src/avclan/target/pico2/iebus.pio new file mode 100644 index 0000000..233e04b --- /dev/null +++ b/src/avclan/target/pico2/iebus.pio @@ -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`) + + diff --git a/src/avclan/target/pico2/phy.cc b/src/avclan/target/pico2/phy.cc index ecd0713..1deb7b9 100644 --- a/src/avclan/target/pico2/phy.cc +++ b/src/avclan/target/pico2/phy.cc @@ -1,11 +1,90 @@ #include "hal/phy.h" #include "avclan.h" +#include "hardware/gpio.h" +#include "hardware/pio.h" +#include "iebus.pio.h" +#include + +#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) {} diff --git a/src/avclan/target/pico2/pimoroni_pico_plus2w_rp2350.h b/src/avclan/target/pico2/pimoroni_pico_plus2w_rp2350.h deleted file mode 100644 index aa2c987..0000000 --- a/src/avclan/target/pico2/pimoroni_pico_plus2w_rp2350.h +++ /dev/null @@ -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 \ No newline at end of file