Initial: MCU_PIO generic PIO driver with ws2812.pio

This commit is contained in:
Mohamed Salem 2026-04-13 03:51:38 +02:00
commit 48cd158844
6 changed files with 422 additions and 0 deletions

53
cfg/MCU_PIO_cfg.c Normal file
View File

@ -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,
},
};

40
cfg/MCU_PIO_cfg.h Normal file
View File

@ -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 */

107
inc/MCU_PIO.h Normal file
View File

@ -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 */

73
pio/ws2812.pio Normal file
View File

@ -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);
}
%}

115
prg/MCU_PIO_prg.c Normal file
View File

@ -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;
}

34
prg/MCU_PIO_priv.h Normal file
View File

@ -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 */