Add WS2812B LED support via PIO with rainbow and color commands

MCU_PIO: generic PIO driver with config-driven program loading,
function-pointer init callbacks, blocking put, and DMA async put.
ws2812.pio written from scratch — 800 kHz, 10 cycles/bit, side-set.

HAL_LED: pixel buffer with intensity scaling, RGB byte order for
RP2040-Zero WS2812 variant. SetColor immediately pushes the strip.

APP_CLSW: rainbow HSV hue rotation on startup (auto-mode). Color
commands (red/green/blue/off/rainbow) stop the rainbow and set the
LED to a static color. Integer-only HSV-to-RGB conversion.

CMake: added hardware_pio link and pico_generate_pio_header for
ws2812.pio compilation. SYS_ECU init sequence updated.
This commit is contained in:
Mohamed Salem 2026-04-13 03:32:18 +02:00
parent 3d5e63c790
commit b49c7f60bb
17 changed files with 871 additions and 31 deletions

View File

@ -57,13 +57,23 @@ function(mcu_link_target target)
# Pick only the libraries we need:
# - pico_stdlib: core runtime, GPIO, clocks, basic init
# - hardware_uart: UART peripheral API (used by the MCU_UART driver)
# - hardware_dma: DMA controller API (used by MCU_UART non-blocking TX)
# - hardware_dma: DMA controller API (used by MCU_UART + MCU_PIO async)
# - hardware_pio: PIO state machine API (used by MCU_PIO for WS2812 etc.)
target_link_libraries(${target} PRIVATE
pico_stdlib
hardware_uart
hardware_dma
hardware_pio
)
# Generate C header from PIO assembly programs. The generated header
# provides the compiled program struct (ws2812_program) and the
# ws2812_program_init() helper used by MCU_PIO_cfg.c. The header is
# placed in the build directory and automatically added to the
# target's include path.
pico_generate_pio_header(${target}
${PROJECT_ROOT_DIR}/src/MCU_PIO/pio/ws2812.pio)
# Route stdio over USB-CDC: the Pico will appear as a virtual serial
# port on the host when plugged in, so printf/getchar are visible in
# any serial monitor without needing a USB-to-UART adapter.

View File

@ -39,6 +39,11 @@ set(PROJECT_INCLUDE_DIRS
${PROJECT_ROOT_DIR}/src/MCU_UART/prg
${PROJECT_ROOT_DIR}/src/MCU_UART/cfg
# MCU layer - PIO peripheral driver for programmable I/O state machines
${PROJECT_ROOT_DIR}/src/MCU_PIO/inc
${PROJECT_ROOT_DIR}/src/MCU_PIO/prg
${PROJECT_ROOT_DIR}/src/MCU_PIO/cfg
# MCU layer - hardware abstraction for the RP2040 USB-CDC peripheral
# (the Pico appears as a virtual serial port on the host computer)
${PROJECT_ROOT_DIR}/src/MCU_USB/inc
@ -51,6 +56,11 @@ set(PROJECT_INCLUDE_DIRS
${PROJECT_ROOT_DIR}/src/HAL_COM/prg
${PROJECT_ROOT_DIR}/src/HAL_COM/cfg
# HAL layer - LED strip/pixel abstraction over PIO-based WS2812 driver
${PROJECT_ROOT_DIR}/src/HAL_LED/inc
${PROJECT_ROOT_DIR}/src/HAL_LED/prg
${PROJECT_ROOT_DIR}/src/HAL_LED/cfg
# Application layer - color switcher application logic
${PROJECT_ROOT_DIR}/src/APP_CLSW/inc
${PROJECT_ROOT_DIR}/src/APP_CLSW/prg

View File

@ -20,4 +20,12 @@
/** @brief HAL_COM channel used for host communication. */
#define APP_CLSW_COM_CHANNEL ((u8)HAL_COM_CHANNEL_0)
/** @brief HAL_LED instance for the onboard LED.
* Maps to HAL_LED_INSTANCE_ONBOARD (0). */
#define APP_CLSW_LED_INSTANCE 0U
/** @brief Default LED brightness (0-255). 128 = half brightness to avoid
* blinding at close range and reduce current draw. */
#define APP_CLSW_LED_INTENSITY 128U
#endif /* APP_CLSW_CFG_H */

View File

@ -2,14 +2,12 @@
* File: APP_CLSW_prg.c
* Component: APP_CLSW
* Description: Color switcher application logic. Receives commands from the
* host via HAL_COM, parses them, and responds with confirmation.
* host via HAL_COM, parses them, and drives the onboard LED
* via HAL_LED.
*
* Supported commands:
* "red" "Color set to: RED\r\n"
* "green" "Color set to: GREEN\r\n"
* "blue" "Color set to: BLUE\r\n"
* "help" prints available commands
* other "Unknown command: <cmd>\r\n"
* On startup, runs a rainbow HSV hue rotation (auto-mode).
* When a color command is received ("red", "green", "blue"),
* the rainbow stops and the LED is set to the requested color.
*
* Layer: Application
*****************************************************************************/
@ -19,6 +17,7 @@
#include "APP_CLSW_cfg.h"
#include "HAL_COM.h"
#include "HAL_LED.h"
/* ------------------------------------------------------------------------ */
/* INTERNAL STATE */
@ -45,12 +44,6 @@ static STD_tBool bStrEqual(const u8 *pu8A, const u8 *pu8B)
{
bResultLoc = STD_FALSE;
}
/* Only advance if we haven't found a mismatch yet, OR if we
* need to reach the end of both strings to confirm equality.
* Actually: always advance we need to check all characters.
* But once we found a mismatch, we know the result. We still
* loop to avoid early return (single exit point). */
u8IndexLoc++;
}
@ -64,7 +57,6 @@ static void vSendString(const u8 *pu8Str)
{
u16 u16LenLoc = 0U;
/* Calculate string length */
while (pu8Str[u16LenLoc] != 0U)
{
u16LenLoc++;
@ -73,35 +65,115 @@ static void vSendString(const u8 *pu8Str)
HAL_COM_enuSendBuffer(APP_CLSW_COM_CHANNEL, pu8Str, u16LenLoc);
}
/**
* @brief Convert HSV color to RGB using integer-only math.
*
* Standard 6-sector HSV-to-RGB algorithm. All arithmetic uses u16/u32
* intermediates to avoid overflow no floating point.
*
* @param u16Hue Hue angle in degrees (0-359).
* @param u8Sat Saturation (0-255). 255 = fully saturated.
* @param u8Val Value / brightness (0-255). 255 = full brightness.
* @param pu8R Output: red channel (0-255).
* @param pu8G Output: green channel (0-255).
* @param pu8B Output: blue channel (0-255).
*/
static void vHsvToRgb(u16 u16Hue, u8 u8Sat, u8 u8Val,
u8 *pu8R, u8 *pu8G, u8 *pu8B)
{
u8 u8SectorLoc;
u16 u16RemainderLoc;
u8 u8PLoc; /* chroma minimum */
u8 u8QLoc; /* descending transition */
u8 u8TLoc; /* ascending transition */
/* p = V * (255 - S) / 255 */
u8PLoc = (u8)(((u16)u8Val * (255U - (u16)u8Sat)) / 255U);
/* Which 60-degree sector of the hue wheel (0-5) */
u8SectorLoc = (u8)(u16Hue / 60U);
/* Remainder within the sector, scaled to 0-255 range.
* (hue % 60) * 255 / 60 gives a 0-255 ramp within the sector. */
u16RemainderLoc = (u16)(((u32)(u16Hue % 60U) * 255U) / 60U);
/* q = V * (255 - S * remainder / 255) / 255 — descending edge */
u8QLoc = (u8)(((u16)u8Val * (255U - ((u16)u8Sat * u16RemainderLoc / 255U))) / 255U);
/* t = V * (255 - S * (255 - remainder) / 255) / 255 — ascending edge */
u8TLoc = (u8)(((u16)u8Val * (255U - ((u16)u8Sat * (255U - u16RemainderLoc) / 255U))) / 255U);
/* Map sector to RGB channels */
if (u8SectorLoc == 0U)
{
*pu8R = u8Val; *pu8G = u8TLoc; *pu8B = u8PLoc;
}
else if (u8SectorLoc == 1U)
{
*pu8R = u8QLoc; *pu8G = u8Val; *pu8B = u8PLoc;
}
else if (u8SectorLoc == 2U)
{
*pu8R = u8PLoc; *pu8G = u8Val; *pu8B = u8TLoc;
}
else if (u8SectorLoc == 3U)
{
*pu8R = u8PLoc; *pu8G = u8QLoc; *pu8B = u8Val;
}
else if (u8SectorLoc == 4U)
{
*pu8R = u8TLoc; *pu8G = u8PLoc; *pu8B = u8Val;
}
else
{
*pu8R = u8Val; *pu8G = u8PLoc; *pu8B = u8QLoc;
}
}
/**
* @brief Process a complete command string.
* Called when a delimiter (\r or \n) is received.
*/
static void vProcessCommand(void)
{
/* Null-terminate the command buffer */
strState.au8CmdBuffer[strState.u8CmdIndex] = 0U;
/* Skip empty commands (just pressing Enter) */
if (strState.u8CmdIndex == 0U)
{
/* Do nothing — no command to process */
/* Empty command — do nothing */
}
else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"red") == STD_TRUE)
{
strState.bAutoMode = STD_FALSE;
HAL_LED_enuSetColor(APP_CLSW_LED_INSTANCE, 0U, 255U, 0U, 0U, APP_CLSW_LED_INTENSITY);
vSendString((const u8 *)"Color set to: RED\r\n");
}
else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"green") == STD_TRUE)
{
strState.bAutoMode = STD_FALSE;
HAL_LED_enuSetColor(APP_CLSW_LED_INSTANCE, 0U, 0U, 255U, 0U, APP_CLSW_LED_INTENSITY);
vSendString((const u8 *)"Color set to: GREEN\r\n");
}
else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"blue") == STD_TRUE)
{
strState.bAutoMode = STD_FALSE;
HAL_LED_enuSetColor(APP_CLSW_LED_INSTANCE, 0U, 0U, 0U, 255U, APP_CLSW_LED_INTENSITY);
vSendString((const u8 *)"Color set to: BLUE\r\n");
}
else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"rainbow") == STD_TRUE)
{
strState.bAutoMode = STD_TRUE;
vSendString((const u8 *)"Rainbow mode enabled\r\n");
}
else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"off") == STD_TRUE)
{
strState.bAutoMode = STD_FALSE;
HAL_LED_enuSetColor(APP_CLSW_LED_INSTANCE, 0U, 0U, 0U, 0U, 0U);
vSendString((const u8 *)"LED off\r\n");
}
else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"help") == STD_TRUE)
{
vSendString((const u8 *)"Available commands: red, green, blue, help\r\n");
vSendString((const u8 *)"Commands: red, green, blue, rainbow, off, help\r\n");
}
else
{
@ -110,7 +182,6 @@ static void vProcessCommand(void)
vSendString((const u8 *)"\r\n");
}
/* Reset the buffer for the next command */
strState.u8CmdIndex = 0U;
}
@ -122,8 +193,9 @@ STD_tenuResult APP_CLSW_enuInit(void)
{
STD_tenuResult enuResultLoc = STD_OK;
/* Clear the command buffer state */
strState.u8CmdIndex = 0U;
strState.u16Hue = 0U;
strState.bAutoMode = STD_TRUE;
return enuResultLoc;
}
@ -138,29 +210,24 @@ void APP_CLSW_vRunnable(void)
u8 u8ByteLoc = 0U;
STD_tenuResult enuRxResultLoc = STD_OK;
/* Check if the host sent any data */
/* --- Process incoming commands --- */
bDataAvailableLoc = HAL_COM_bIsRxDataAvailable(APP_CLSW_COM_CHANNEL);
while (bDataAvailableLoc == STD_TRUE)
{
/* Read one byte */
enuRxResultLoc = HAL_COM_enuReadByte(APP_CLSW_COM_CHANNEL, &u8ByteLoc);
if (enuRxResultLoc == STD_OK)
{
/* Echo the character back so the user sees what they typed */
HAL_COM_enuSendByte(APP_CLSW_COM_CHANNEL, u8ByteLoc);
/* Check for command delimiter */
if ((u8ByteLoc == (u8)'\r') || (u8ByteLoc == (u8)'\n'))
{
/* Send a newline for display, then process the command */
vSendString((const u8 *)"\r\n");
vProcessCommand();
}
else
{
/* Accumulate into the command buffer (truncate if full) */
if (strState.u8CmdIndex < (APP_CLSW_CMD_BUFFER_SIZE - 1U))
{
strState.au8CmdBuffer[strState.u8CmdIndex] = u8ByteLoc;
@ -169,7 +236,21 @@ void APP_CLSW_vRunnable(void)
}
}
/* Check for more data */
bDataAvailableLoc = HAL_COM_bIsRxDataAvailable(APP_CLSW_COM_CHANNEL);
}
}
/* --- Rainbow auto-mode: cycle hue each tick --- */
if (strState.bAutoMode == STD_TRUE)
{
u8 u8RLoc = 0U;
u8 u8GLoc = 0U;
u8 u8BLoc = 0U;
vHsvToRgb(strState.u16Hue, 255U, 255U, &u8RLoc, &u8GLoc, &u8BLoc);
HAL_LED_enuSetColor(APP_CLSW_LED_INSTANCE, 0U,
u8RLoc, u8GLoc, u8BLoc, APP_CLSW_LED_INTENSITY);
strState.u16Hue = (strState.u16Hue + 1U) % 360U;
}
}

View File

@ -27,8 +27,10 @@
*/
typedef struct
{
u8 au8CmdBuffer[APP_CLSW_CMD_BUFFER_SIZE]; /**< Accumulated command bytes */
u8 u8CmdIndex; /**< Next write position */
u8 au8CmdBuffer[APP_CLSW_CMD_BUFFER_SIZE]; /**< Accumulated command bytes */
u8 u8CmdIndex; /**< Next write position */
u16 u16Hue; /**< Current hue for rainbow (0-359) */
STD_tBool bAutoMode; /**< STD_TRUE = rainbow, STD_FALSE = manual color */
} APP_CLSW_tstrState;
#endif /* APP_CLSW_PRIV_H */

View File

@ -0,0 +1,19 @@
/******************************************************************************
* File: HAL_LED_cfg.c
* Component: HAL_LED
* Description: Configuration array definition for the LED abstraction.
*
* Layer: HAL - configuration
*****************************************************************************/
#include "HAL_LED.h"
#include "HAL_LED_cfg.h"
const HAL_LED_tstrConfig HAL_LED_astrConfig[HAL_LED_NUM_INSTANCES] =
{
[HAL_LED_INSTANCE_ONBOARD] =
{
.u8NumLeds = HAL_LED_ONBOARD_NUM_LEDS,
.u8PioInstance = HAL_LED_ONBOARD_PIO_INSTANCE,
},
};

View File

@ -0,0 +1,46 @@
/******************************************************************************
* File: HAL_LED_cfg.h
* Component: HAL_LED
* Description: Configuration header for the LED abstraction layer.
* Defines LED strip instances, maximum strip length,
* and per-instance settings.
*
* Layer: HAL - configuration
*****************************************************************************/
#ifndef HAL_LED_CFG_H
#define HAL_LED_CFG_H
#include "STD_TYPES.h"
/* ------------------------------------------------------------------------ */
/* INSTANCE ENUMERATION */
/* ------------------------------------------------------------------------ */
typedef enum
{
HAL_LED_INSTANCE_ONBOARD = 0U, /**< Onboard WS2812B on RP2040-Zero */
HAL_LED_NUM_INSTANCES
} HAL_LED_tenuInstance;
/* ------------------------------------------------------------------------ */
/* BUFFER SIZING */
/* ------------------------------------------------------------------------ */
/** @brief Maximum number of LEDs any single instance can support.
* Determines the pixel buffer size in the control struct.
* Increase when adding longer strips. */
#define HAL_LED_MAX_LEDS_PER_INSTANCE 1U
/* ------------------------------------------------------------------------ */
/* INSTANCE 0 (ONBOARD) CONFIGURATION */
/* ------------------------------------------------------------------------ */
/** @brief Number of LEDs in the onboard strip (just 1 on the RP2040-Zero). */
#define HAL_LED_ONBOARD_NUM_LEDS 1U
/** @brief MCU_PIO instance index for the onboard LED.
* Maps to MCU_PIO_INSTANCE_WS2812 (defined in MCU_PIO_cfg.h). */
#define HAL_LED_ONBOARD_PIO_INSTANCE 0U
#endif /* HAL_LED_CFG_H */

70
src/HAL_LED/inc/HAL_LED.h Normal file
View File

@ -0,0 +1,70 @@
/******************************************************************************
* File: HAL_LED.h
* Component: HAL_LED
* Description: Public interface for the LED abstraction layer.
* Manages a pixel buffer for WS2812-style addressable LEDs
* and pushes color data through MCU_PIO. Supports indexed
* LED strips with per-pixel intensity scaling.
*
* SetColor sets one LED's color and immediately pushes the
* entire strip to the hardware no separate Update call needed.
*
* Layer: HAL (hardware abstraction, one level above MCU drivers)
*****************************************************************************/
#ifndef HAL_LED_H
#define HAL_LED_H
#include "STD_TYPES.h"
/* ------------------------------------------------------------------------ */
/* CONFIGURATION STRUCTURE */
/* ------------------------------------------------------------------------ */
/**
* @brief Per-instance LED strip/array configuration.
*
* One entry per LED strip, stored in HAL_LED_astrConfig[].
* The array index is the instance parameter in all public API calls.
*/
typedef struct
{
u8 u8NumLeds; /**< Number of LEDs in this strip (1 for single LED) */
u8 u8PioInstance; /**< MCU_PIO config index for the data output */
} HAL_LED_tstrConfig;
/* ------------------------------------------------------------------------ */
/* PUBLIC API */
/* ------------------------------------------------------------------------ */
/**
* @brief Initialize the LED abstraction layer.
*
* Clears the internal pixel buffer to all-off (black). Does NOT init
* MCU_PIO SYS_ECU must call MCU_PIO_enuInit() before this.
*
* @return STD_OK on success.
*/
STD_tenuResult HAL_LED_enuInit(void);
/**
* @brief Set the color of a single LED and push the entire strip.
*
* Scales each color channel by u8Intensity (0-255), stores the result
* in the internal pixel buffer, then immediately pushes all LEDs in
* this strip to the PIO state machine.
*
* @param u8Instance HAL_LED config instance.
* @param u8LedIndex LED position in the strip (0-based).
* @param u8Red Red intensity (0-255, before scaling).
* @param u8Green Green intensity (0-255, before scaling).
* @param u8Blue Blue intensity (0-255, before scaling).
* @param u8Intensity Global brightness scaler (0-255). 255 = full.
* @return STD_OK on success,
* STD_INDEX_OUT_OF_RANGE_ERROR if u8LedIndex >= u8NumLeds.
*/
STD_tenuResult HAL_LED_enuSetColor(u8 u8Instance, u8 u8LedIndex,
u8 u8Red, u8 u8Green, u8 u8Blue,
u8 u8Intensity);
#endif /* HAL_LED_H */

View File

@ -0,0 +1,117 @@
/******************************************************************************
* File: HAL_LED_prg.c
* Component: HAL_LED
* Description: LED abstraction implementation. Manages a pixel buffer with
* intensity scaling and pushes color data to the hardware via
* MCU_PIO. SetColor updates one LED and immediately pushes the
* entire strip no separate Update call needed.
*
* Layer: HAL
*****************************************************************************/
#include "HAL_LED.h"
#include "HAL_LED_priv.h"
#include "HAL_LED_cfg.h"
#include "MCU_PIO.h"
/* ------------------------------------------------------------------------ */
/* RUNTIME STATE */
/* ------------------------------------------------------------------------ */
static HAL_LED_tstrControl strControl;
/* ------------------------------------------------------------------------ */
/* INTERNAL HELPERS */
/* ------------------------------------------------------------------------ */
/**
* @brief Push the entire pixel buffer for one instance to the PIO FIFO.
*
* Iterates through the pixel buffer, packs each pixel into a 32-bit
* GRB word (left-justified: G in [31:24], R in [23:16], B in [15:8],
* bits [7:0] unused), and sends it via MCU_PIO_vPutBlocking.
*
* @param u8Instance HAL_LED instance index.
*/
static void vPushStrip(u8 u8Instance)
{
u8 u8PioInstLoc = HAL_LED_astrConfig[u8Instance].u8PioInstance;
u8 u8NumLedsLoc = HAL_LED_astrConfig[u8Instance].u8NumLeds;
u8 u8LedLoc;
for (u8LedLoc = 0U; u8LedLoc < u8NumLedsLoc; u8LedLoc++)
{
HAL_LED_tstrPixel *pstrPixLoc = &strControl.astrPixels[u8Instance][u8LedLoc];
/* Pack RGB into bits [31:8] of the 32-bit word.
* The WS2812 variant on the RP2040-Zero uses RGB byte order
* (red first, green second, blue third) rather than the standard
* GRB. Bits [7:0] are padding (shifted out but ignored). */
u32 u32RgbLoc = ((u32)pstrPixLoc->u8Red << 24U)
| ((u32)pstrPixLoc->u8Green << 16U)
| ((u32)pstrPixLoc->u8Blue << 8U);
MCU_PIO_vPutBlocking(u8PioInstLoc, u32RgbLoc);
}
}
/* ========================================================================= */
/* INIT */
/* ========================================================================= */
STD_tenuResult HAL_LED_enuInit(void)
{
STD_tenuResult enuResultLoc = STD_OK;
u8 u8InstLoc;
u8 u8LedLoc;
/* Clear all pixels to off (black) */
for (u8InstLoc = 0U; u8InstLoc < (u8)HAL_LED_NUM_INSTANCES; u8InstLoc++)
{
for (u8LedLoc = 0U; u8LedLoc < HAL_LED_astrConfig[u8InstLoc].u8NumLeds; u8LedLoc++)
{
strControl.astrPixels[u8InstLoc][u8LedLoc].u8Green = 0U;
strControl.astrPixels[u8InstLoc][u8LedLoc].u8Red = 0U;
strControl.astrPixels[u8InstLoc][u8LedLoc].u8Blue = 0U;
}
}
return enuResultLoc;
}
/* ========================================================================= */
/* SET COLOR */
/* ========================================================================= */
STD_tenuResult HAL_LED_enuSetColor(u8 u8Instance, u8 u8LedIndex,
u8 u8Red, u8 u8Green, u8 u8Blue,
u8 u8Intensity)
{
STD_tenuResult enuResultLoc = STD_OK;
u8 u8NumLedsLoc = HAL_LED_astrConfig[u8Instance].u8NumLeds;
if (u8LedIndex >= u8NumLedsLoc)
{
enuResultLoc = STD_INDEX_OUT_OF_RANGE_ERROR;
}
else
{
/* Scale each channel by intensity: (channel * intensity) / 255.
* Use u16 intermediate to avoid overflow (255 * 255 = 65025,
* which fits in u16 but not u8). */
u8 u8ScaledRLoc = (u8)(((u16)u8Red * (u16)u8Intensity) / 255U);
u8 u8ScaledGLoc = (u8)(((u16)u8Green * (u16)u8Intensity) / 255U);
u8 u8ScaledBLoc = (u8)(((u16)u8Blue * (u16)u8Intensity) / 255U);
/* Store the scaled pixel in the buffer */
strControl.astrPixels[u8Instance][u8LedIndex].u8Red = u8ScaledRLoc;
strControl.astrPixels[u8Instance][u8LedIndex].u8Green = u8ScaledGLoc;
strControl.astrPixels[u8Instance][u8LedIndex].u8Blue = u8ScaledBLoc;
/* Immediately push the entire strip to the hardware */
vPushStrip(u8Instance);
}
return enuResultLoc;
}

View File

@ -0,0 +1,51 @@
/******************************************************************************
* File: HAL_LED_priv.h
* Component: HAL_LED
* Description: Private header pixel struct, control struct with the
* pixel buffer, and extern config array declaration.
*
* Layer: HAL - internal use only
*****************************************************************************/
#ifndef HAL_LED_PRIV_H
#define HAL_LED_PRIV_H
#include "HAL_LED.h"
#include "HAL_LED_cfg.h"
extern const HAL_LED_tstrConfig HAL_LED_astrConfig[HAL_LED_NUM_INSTANCES];
/* ------------------------------------------------------------------------ */
/* PIXEL STRUCTURE */
/* ------------------------------------------------------------------------ */
/**
* @brief Per-LED color data in GRB order (matching WS2812 protocol).
*
* Stored after intensity scaling has been applied, so these values
* are the actual brightnesses sent to the hardware.
*/
typedef struct
{
u8 u8Green;
u8 u8Red;
u8 u8Blue;
} HAL_LED_tstrPixel;
/* ------------------------------------------------------------------------ */
/* CONTROL STRUCTURE */
/* ------------------------------------------------------------------------ */
/**
* @brief Internal runtime state for all HAL_LED instances.
*
* astrPixels is a 2D array: [instance][led_index]. Each pixel holds
* the intensity-scaled GRB values ready to be packed into 32-bit words
* for the PIO FIFO.
*/
typedef struct
{
HAL_LED_tstrPixel astrPixels[HAL_LED_NUM_INSTANCES][HAL_LED_MAX_LEDS_PER_INSTANCE];
} HAL_LED_tstrControl;
#endif /* HAL_LED_PRIV_H */

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

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
src/MCU_PIO/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 */

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

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

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

View File

@ -19,7 +19,9 @@
/* Components initialized and scheduled by SYS_ECU, listed in
* dependency order (drivers first, then HAL, then application). */
#include "MCU_USB.h"
#include "MCU_PIO.h"
#include "HAL_COM.h"
#include "HAL_LED.h"
#include "APP_CLSW.h"
/* ========================================================================= */
@ -37,9 +39,11 @@ static void SYS_ECU_vInitAll(void)
{
/* MCU layer - hardware drivers */
MCU_USB_enuInit();
MCU_PIO_enuInit();
/* HAL layer - abstractions over hardware */
HAL_COM_enuInit();
HAL_LED_enuInit();
/* Application layer */
APP_CLSW_enuInit();