From 48cd1588449581892e60dde3e002a2649fe3f4f2 Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Mon, 13 Apr 2026 03:51:38 +0200 Subject: [PATCH] Initial: MCU_PIO generic PIO driver with ws2812.pio --- cfg/MCU_PIO_cfg.c | 53 +++++++++++++++++++++ cfg/MCU_PIO_cfg.h | 40 ++++++++++++++++ inc/MCU_PIO.h | 107 +++++++++++++++++++++++++++++++++++++++++ pio/ws2812.pio | 73 ++++++++++++++++++++++++++++ prg/MCU_PIO_prg.c | 115 +++++++++++++++++++++++++++++++++++++++++++++ prg/MCU_PIO_priv.h | 34 ++++++++++++++ 6 files changed, 422 insertions(+) create mode 100644 cfg/MCU_PIO_cfg.c create mode 100644 cfg/MCU_PIO_cfg.h create mode 100644 inc/MCU_PIO.h create mode 100644 pio/ws2812.pio create mode 100644 prg/MCU_PIO_prg.c create mode 100644 prg/MCU_PIO_priv.h diff --git a/cfg/MCU_PIO_cfg.c b/cfg/MCU_PIO_cfg.c new file mode 100644 index 0000000..39c6d34 --- /dev/null +++ b/cfg/MCU_PIO_cfg.c @@ -0,0 +1,53 @@ +/****************************************************************************** + * File: MCU_PIO_cfg.c + * Component: MCU_PIO + * Description: Configuration array definition for the PIO driver. + * Wires each PIO instance to its compiled program and + * program-specific init function. The WS2812 init wrapper + * adapts the auto-generated ws2812_program_init() signature + * to match the generic MCU_PIO_tpfProgramInit callback type. + * + * Layer: MCU (hardware abstraction) - configuration + *****************************************************************************/ + +#include "MCU_PIO.h" +#include "MCU_PIO_cfg.h" + +/* Auto-generated header from ws2812.pio — provides ws2812_program struct + * and ws2812_program_init() helper. Generated at build time by + * pico_generate_pio_header() in mcu_config.cmake. */ +#include "ws2812.pio.h" + +/* ------------------------------------------------------------------------ */ +/* WS2812 INIT WRAPPER */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Adapts ws2812_program_init() to the MCU_PIO_tpfProgramInit + * signature by hardcoding the WS2812 bit frequency (800 kHz). + * + * The auto-generated ws2812_program_init() takes an extra `float freq` + * parameter that is not part of the generic callback type. This wrapper + * fills it in so the generic driver can call it without knowing about + * WS2812 specifics. + */ +static void vWs2812Init(PIO pstrPio, u8 u8Sm, u8 u8Pin, u32 u32Offset) +{ + ws2812_program_init(pstrPio, (u32)u8Sm, (u32)u32Offset, (u32)u8Pin, 800000.0f); +} + +/* ------------------------------------------------------------------------ */ +/* CONFIGURATION ARRAY */ +/* ------------------------------------------------------------------------ */ + +const MCU_PIO_tstrConfig MCU_PIO_astrConfig[MCU_PIO_NUM_INSTANCES] = +{ + [MCU_PIO_INSTANCE_WS2812] = + { + .pstrPio = pio0, /* use PIO block 0 */ + .u8Sm = 0U, /* state machine 0 within pio0 */ + .u8Pin = MCU_PIO_WS2812_PIN, + .pstrProgram = &ws2812_program, + .pfProgramInit = vWs2812Init, + }, +}; diff --git a/cfg/MCU_PIO_cfg.h b/cfg/MCU_PIO_cfg.h new file mode 100644 index 0000000..f676eda --- /dev/null +++ b/cfg/MCU_PIO_cfg.h @@ -0,0 +1,40 @@ +/****************************************************************************** + * File: MCU_PIO_cfg.h + * Component: MCU_PIO + * Description: Configuration header for the generic PIO driver. + * Defines which PIO programs are loaded and on which + * state machines / GPIO pins they operate. + * + * Layer: MCU (hardware abstraction) - configuration + *****************************************************************************/ + +#ifndef MCU_PIO_CFG_H +#define MCU_PIO_CFG_H + +#include "STD_TYPES.h" + +/* ------------------------------------------------------------------------ */ +/* INSTANCE ENUMERATION */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Enumeration of configured PIO program instances. + * + * Each entry represents one PIO program running on one state machine. + * The enumerator value is the array index into MCU_PIO_astrConfig[]. + */ +typedef enum +{ + MCU_PIO_INSTANCE_WS2812 = 0U, /**< WS2812 LED driver on GP16 */ + MCU_PIO_NUM_INSTANCES +} MCU_PIO_tenuInstance; + +/* ------------------------------------------------------------------------ */ +/* WS2812 INSTANCE CONFIGURATION */ +/* ------------------------------------------------------------------------ */ + +/** @brief GPIO pin for the WS2812B data line. + * GP16 is the onboard WS2812B on the Waveshare RP2040-Zero. */ +#define MCU_PIO_WS2812_PIN 16U + +#endif /* MCU_PIO_CFG_H */ diff --git a/inc/MCU_PIO.h b/inc/MCU_PIO.h new file mode 100644 index 0000000..600a9ec --- /dev/null +++ b/inc/MCU_PIO.h @@ -0,0 +1,107 @@ +/****************************************************************************** + * File: MCU_PIO.h + * Component: MCU_PIO + * Description: Public interface for the generic PIO driver component. + * Abstracts the RP2040's Programmable I/O hardware — loading + * PIO programs into instruction memory, configuring state + * machines, and pushing data through the TX FIFO. + * + * The driver is program-agnostic: each config entry holds a + * pointer to a compiled PIO program and a function pointer + * for the program-specific state machine init. WS2812 is one + * such program; future PIO uses (custom protocols, etc.) + * plug in the same way with zero driver code changes. + * + * Layer: MCU (hardware abstraction) + *****************************************************************************/ + +#ifndef MCU_PIO_H +#define MCU_PIO_H + +#include "STD_TYPES.h" +#include "hardware/pio.h" + +/* ------------------------------------------------------------------------ */ +/* PROGRAM INIT FUNCTION POINTER TYPE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Callback type for program-specific state machine configuration. + * + * Each PIO program has its own pin mapping, shift config, clock divider, + * etc. The generic MCU_PIO driver calls this function after loading the + * program into instruction memory. The function must fully configure and + * enable the state machine. + * + * @param pstrPio PIO instance (pio0 or pio1). + * @param u8Sm State machine index (0-3) within that PIO instance. + * @param u8Pin GPIO pin from the config struct. + * @param u32Offset Instruction memory offset where the program was loaded. + */ +typedef void (*MCU_PIO_tpfProgramInit)(PIO pstrPio, u8 u8Sm, u8 u8Pin, u32 u32Offset); + +/* ------------------------------------------------------------------------ */ +/* CONFIGURATION STRUCTURE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Per-instance PIO configuration. + * + * One entry per PIO program/state-machine pair, stored in + * MCU_PIO_astrConfig[]. The array index is used as the instance + * parameter in all public API calls. + */ +typedef struct +{ + PIO pstrPio; /**< PIO block: pio0 or pio1 */ + u8 u8Sm; /**< State machine index (0-3) */ + u8 u8Pin; /**< GPIO pin used by this program */ + const pio_program_t *pstrProgram; /**< Pointer to compiled PIO program */ + MCU_PIO_tpfProgramInit pfProgramInit; /**< Program-specific SM config callback */ +} MCU_PIO_tstrConfig; + +/* ------------------------------------------------------------------------ */ +/* PUBLIC API */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Initialize all configured PIO instances. + * + * For each config entry: loads the PIO program into the instruction + * memory of the selected PIO block, then calls the program-specific + * init callback to configure the state machine (pins, shift, clock). + * + * SYS_ECU calls this once during the init sequence. + * + * @return STD_OK on success, STD_NOK if any instance fails. + */ +STD_tenuResult MCU_PIO_enuInit(void); + +/** + * @brief Push a 32-bit word into a PIO state machine's TX FIFO (blocking). + * + * Blocks until the FIFO has space, then writes the data. For WS2812, + * each call sends one pixel (24-bit GRB left-justified in the 32-bit word). + * + * @param u8Instance Config array index (MCU_PIO_tenuInstance). + * @param u32Data The 32-bit value to push. + */ +void MCU_PIO_vPutBlocking(u8 u8Instance, u32 u32Data); + +/** + * @brief Push a buffer of 32-bit words to the TX FIFO via DMA (non-blocking). + * + * Starts a DMA transfer from pu32Data into the PIO TX FIFO. Returns + * immediately. The DMA channel was pre-claimed during Init. + * + * The caller MUST keep pu32Data valid until the transfer completes. + * + * @param u8Instance Config array index. + * @param pu32Data Pointer to array of 32-bit words. Must not be NULL. + * @param u16Count Number of 32-bit words to transfer. + * @return STD_OK transfer started, + * STD_NULL_POINTER_ERROR if pu32Data is NULL. + */ +STD_tenuResult MCU_PIO_enuPutBufferAsync(u8 u8Instance, const u32 *pu32Data, u16 u16Count); + +#endif /* MCU_PIO_H */ diff --git a/pio/ws2812.pio b/pio/ws2812.pio new file mode 100644 index 0000000..2c60c3d --- /dev/null +++ b/pio/ws2812.pio @@ -0,0 +1,73 @@ +; ============================================================================ +; ws2812.pio — WS2812B NZR protocol driver for RP2040 PIO +; ============================================================================ +; Written from scratch for the color_switcher project. +; +; WS2812 protocol: each bit is a fixed-length pulse (~1.25 us at 800 kHz). +; "1" bit: long high (~875 ns) + short low (~375 ns) +; "0" bit: short high (~375 ns) + long low (~875 ns) +; +; Timing: 10 PIO cycles per bit at 8 MHz PIO clock (sysclk 125 MHz / 15.625). +; "1" bit: high 7 cycles, low 3 cycles +; "0" bit: high 3 cycles, low 7 cycles +; +; Data: 24-bit GRB, MSB first, left-justified in 32-bit FIFO word. +; Autopull at 24 bits, shift left. Side-set pin drives the data line. +; ============================================================================ + +.program ws2812 +.side_set 1 + +.wrap_target +bitloop: + out x, 1 side 0 [2] ; shift 1 bit into X, drive LOW, 3 cycles total + jmp !x do_zero side 1 [1] ; if bit=0 jump, drive HIGH, 2 cycles total +do_one: + jmp bitloop side 1 [4] ; bit=1: stay HIGH 5 more (total HIGH=7), loop +do_zero: + nop side 0 [4] ; bit=0: drive LOW 5 more (total LOW from next out=3+5) +.wrap + +; ============================================================================ +; C SDK helper — emitted into ws2812.pio.h by pico_generate_pio_header. +; Configures the state machine for WS2812 output on a single GPIO pin. +; +; Parameters: +; pio — PIO instance (pio0 or pio1) +; sm — state machine index (0-3) +; offset — instruction memory offset where the program was loaded +; pin — GPIO pin connected to the WS2812 data line +; freq — bit frequency in Hz (800000.0f for standard WS2812) +; ============================================================================ + +% c-sdk { +#include "hardware/clocks.h" + +static inline void ws2812_program_init(PIO pio, uint sm, uint offset, uint pin, float freq) +{ + /* Configure the GPIO pin for PIO output */ + pio_gpio_init(pio, pin); + pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); + + /* Get the default config (sets wrap points and sideset count from the .pio) */ + pio_sm_config c = ws2812_program_get_default_config(offset); + + /* Side-set pin = the WS2812 data line */ + sm_config_set_sideset_pins(&c, pin); + + /* Shift left, autopull at 24 bits (GRB = 3 bytes). + * Data must be left-justified in the 32-bit FIFO word (bits [31:8]). */ + sm_config_set_out_shift(&c, false, true, 24); + + /* Join both FIFOs into a single 8-entry TX FIFO for deeper buffering */ + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + + /* Clock divider: 10 PIO cycles per bit, so PIO freq = bit_freq * 10. + * clkdiv = sysclk / (freq * 10). E.g., 125 MHz / 8 MHz = 15.625 */ + sm_config_set_clkdiv(&c, clock_get_hz(clk_sys) / (freq * 10.0f)); + + /* Apply the config and start the state machine */ + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); +} +%} \ No newline at end of file diff --git a/prg/MCU_PIO_prg.c b/prg/MCU_PIO_prg.c new file mode 100644 index 0000000..3867048 --- /dev/null +++ b/prg/MCU_PIO_prg.c @@ -0,0 +1,115 @@ +/****************************************************************************** + * File: MCU_PIO_prg.c + * Component: MCU_PIO + * Description: Generic PIO driver implementation. Loads PIO programs into + * instruction memory, calls program-specific init callbacks, + * claims DMA channels, and provides blocking + async writes. + * + * Layer: MCU (hardware abstraction) + *****************************************************************************/ + +#include "MCU_PIO.h" +#include "MCU_PIO_priv.h" +#include "MCU_PIO_cfg.h" + +#include "hardware/pio.h" +#include "hardware/dma.h" + +/* ------------------------------------------------------------------------ */ +/* RUNTIME STATE */ +/* ------------------------------------------------------------------------ */ + +static MCU_PIO_tstrControl strControl; + +/* ========================================================================= */ +/* INIT */ +/* ========================================================================= */ + +STD_tenuResult MCU_PIO_enuInit(void) +{ + STD_tenuResult enuResultLoc = STD_OK; + u8 u8IndexLoc; + + for (u8IndexLoc = 0U; u8IndexLoc < (u8)MCU_PIO_NUM_INSTANCES; u8IndexLoc++) + { + const MCU_PIO_tstrConfig *pstrCfgLoc = &MCU_PIO_astrConfig[u8IndexLoc]; + + /* Load the PIO program into instruction memory */ + u32 u32OffsetLoc = (u32)pio_add_program(pstrCfgLoc->pstrPio, + pstrCfgLoc->pstrProgram); + + /* Call the program-specific init callback to configure the SM */ + pstrCfgLoc->pfProgramInit(pstrCfgLoc->pstrPio, + pstrCfgLoc->u8Sm, + pstrCfgLoc->u8Pin, + u32OffsetLoc); + + /* Claim a DMA channel for async FIFO writes. Reserved for the + * lifetime of the application — never released. true = panic + * if no channels available (misconfiguration, not a runtime error). */ + strControl.as8DmaChannel[u8IndexLoc] = (s8)dma_claim_unused_channel(true); + } + + return enuResultLoc; +} + +/* ========================================================================= */ +/* PUT BLOCKING (SINGLE WORD) */ +/* ========================================================================= */ + +void MCU_PIO_vPutBlocking(u8 u8Instance, u32 u32Data) +{ + const MCU_PIO_tstrConfig *pstrCfgLoc = &MCU_PIO_astrConfig[u8Instance]; + + /* Blocks until the TX FIFO has space, then writes the 32-bit word */ + pio_sm_put_blocking(pstrCfgLoc->pstrPio, pstrCfgLoc->u8Sm, u32Data); +} + +/* ========================================================================= */ +/* PUT BUFFER ASYNC (DMA, NON-BLOCKING) */ +/* ========================================================================= */ + +STD_tenuResult MCU_PIO_enuPutBufferAsync(u8 u8Instance, const u32 *pu32Data, u16 u16Count) +{ + STD_tenuResult enuResultLoc = STD_OK; + + if (pu32Data == STD_NULL) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else + { + const MCU_PIO_tstrConfig *pstrCfgLoc = &MCU_PIO_astrConfig[u8Instance]; + s8 s8ChLoc = strControl.as8DmaChannel[u8Instance]; + + dma_channel_config strCfgLoc = dma_channel_get_default_config((u32)s8ChLoc); + + /* 32-bit transfers — matches the PIO FIFO word size */ + channel_config_set_transfer_data_size(&strCfgLoc, DMA_SIZE_32); + + /* Source: increment through the caller's buffer */ + channel_config_set_read_increment(&strCfgLoc, true); + + /* Destination: PIO TX FIFO register (fixed address) */ + channel_config_set_write_increment(&strCfgLoc, false); + + /* Pace by PIO TX FIFO DREQ — DMA only pushes when SM can accept */ + channel_config_set_dreq(&strCfgLoc, pio_get_dreq(pstrCfgLoc->pstrPio, + pstrCfgLoc->u8Sm, + true)); + + /* Start the DMA transfer. Runs autonomously until u16Count words + * have been pushed into the FIFO. Caller must keep pu32Data valid + * until transfer completes. */ + dma_channel_configure( + (u32)s8ChLoc, + &strCfgLoc, + &pstrCfgLoc->pstrPio->txf[pstrCfgLoc->u8Sm], /* dest: PIO TX FIFO */ + pu32Data, /* source: buffer */ + u16Count, /* word count */ + true /* start immediately */ + ); + } + + return enuResultLoc; +} \ No newline at end of file diff --git a/prg/MCU_PIO_priv.h b/prg/MCU_PIO_priv.h new file mode 100644 index 0000000..a50a9ce --- /dev/null +++ b/prg/MCU_PIO_priv.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * File: MCU_PIO_priv.h + * Component: MCU_PIO + * Description: Private header — extern config array and runtime control + * struct holding pre-claimed DMA channels per instance. + * + * Layer: MCU (hardware abstraction) - internal use only + *****************************************************************************/ + +#ifndef MCU_PIO_PRIV_H +#define MCU_PIO_PRIV_H + +#include "MCU_PIO.h" +#include "MCU_PIO_cfg.h" + +extern const MCU_PIO_tstrConfig MCU_PIO_astrConfig[MCU_PIO_NUM_INSTANCES]; + +/* ------------------------------------------------------------------------ */ +/* RUNTIME CONTROL STRUCTURE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Per-instance runtime state. + * + * as8DmaChannel — DMA channel claimed during Init for async FIFO writes. + * One channel per PIO instance, reserved for the lifetime + * of the application. + */ +typedef struct +{ + s8 as8DmaChannel[MCU_PIO_NUM_INSTANCES]; +} MCU_PIO_tstrControl; + +#endif /* MCU_PIO_PRIV_H */ \ No newline at end of file