From 3687e48684c3532904a10273d6717e3b0d0bada2 Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Sun, 12 Apr 2026 18:23:24 +0200 Subject: [PATCH 1/6] Add complete firmware stack with USB-CDC proof of life Dockerized build system (Dockerfile, docker-compose, build.sh) with Pico SDK cross-compilation. Modular CMake split into project_config, mcu_config, and sources_config under cmake/. Component architecture following inc/prg/cfg convention: STD_TYPES, MCU_USB, HAL_COM, APP_CLSW, SYS_ECU. Full call chain SYS_ECU -> APP_CLSW -> HAL_COM -> MCU_USB verified end-to-end on RP2040-Zero hardware over USB-CDC. Includes flash.sh for automated .uf2 flashing on macOS and devcontainer config for VS Code. Co-Authored-By: Claude Opus 4.6 (1M context) --- .devcontainer/devcontainer.json | 64 ++++++ .gitignore | 9 + CLAUDE.md | 63 +++++- Dockerfile | 39 ++++ README.md | 35 ++- build.sh | 25 ++ cmake/CMakeLists.txt | 65 ++++++ cmake/cmake_config/mcu_config.cmake | 94 ++++++++ cmake/cmake_config/project_config.cmake | 38 ++++ cmake/cmake_config/sources_config.cmake | 61 +++++ cmake/pico_sdk_import.cmake | 121 ++++++++++ docker-compose.yml | 37 +++ flash.sh | 46 ++++ src/APP_CLSW/cfg/APP_CLSW_cfg.c | 13 ++ src/APP_CLSW/cfg/APP_CLSW_cfg.h | 17 ++ src/APP_CLSW/inc/APP_CLSW.h | 46 ++++ src/APP_CLSW/prg/APP_CLSW_prg.c | 40 ++++ src/APP_CLSW/prg/APP_CLSW_priv.h | 17 ++ src/HAL_COM/cfg/HAL_COM_cfg.c | 14 ++ src/HAL_COM/cfg/HAL_COM_cfg.h | 18 ++ src/HAL_COM/inc/HAL_COM.h | 66 ++++++ src/HAL_COM/prg/HAL_COM_prg.c | 56 +++++ src/HAL_COM/prg/HAL_COM_priv.h | 17 ++ src/MCU_UART/cfg/MCU_UART_cfg.c | 14 ++ src/MCU_UART/cfg/MCU_UART_cfg.h | 18 ++ src/MCU_UART/inc/MCU_UART.h | 27 +++ src/MCU_UART/prg/MCU_UART_prg.c | 17 ++ src/MCU_UART/prg/MCU_UART_priv.h | 18 ++ src/MCU_USB/cfg/MCU_USB_cfg.c | 14 ++ src/MCU_USB/cfg/MCU_USB_cfg.h | 86 +++++++ src/MCU_USB/inc/MCU_USB.h | 70 ++++++ src/MCU_USB/prg/MCU_USB_prg.c | 119 ++++++++++ src/MCU_USB/prg/MCU_USB_priv.h | 18 ++ src/STD_TYPES/inc/STD_TYPES.h | 288 ++++++++++++++++++++++++ src/SYS_ECU/cfg/SYS_ECU_cfg | 0 src/SYS_ECU/inc/SYS_ECU.h | 0 src/SYS_ECU/prg/SYS_ECU.c | 67 ++++++ 37 files changed, 1749 insertions(+), 8 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100755 build.sh create mode 100644 cmake/CMakeLists.txt create mode 100644 cmake/cmake_config/mcu_config.cmake create mode 100644 cmake/cmake_config/project_config.cmake create mode 100644 cmake/cmake_config/sources_config.cmake create mode 100644 cmake/pico_sdk_import.cmake create mode 100644 docker-compose.yml create mode 100755 flash.sh create mode 100644 src/APP_CLSW/cfg/APP_CLSW_cfg.c create mode 100644 src/APP_CLSW/cfg/APP_CLSW_cfg.h create mode 100644 src/APP_CLSW/inc/APP_CLSW.h create mode 100644 src/APP_CLSW/prg/APP_CLSW_prg.c create mode 100644 src/APP_CLSW/prg/APP_CLSW_priv.h create mode 100644 src/HAL_COM/cfg/HAL_COM_cfg.c create mode 100644 src/HAL_COM/cfg/HAL_COM_cfg.h create mode 100644 src/HAL_COM/inc/HAL_COM.h create mode 100644 src/HAL_COM/prg/HAL_COM_prg.c create mode 100644 src/HAL_COM/prg/HAL_COM_priv.h create mode 100644 src/MCU_UART/cfg/MCU_UART_cfg.c create mode 100644 src/MCU_UART/cfg/MCU_UART_cfg.h create mode 100644 src/MCU_UART/inc/MCU_UART.h create mode 100644 src/MCU_UART/prg/MCU_UART_prg.c create mode 100644 src/MCU_UART/prg/MCU_UART_priv.h create mode 100644 src/MCU_USB/cfg/MCU_USB_cfg.c create mode 100644 src/MCU_USB/cfg/MCU_USB_cfg.h create mode 100644 src/MCU_USB/inc/MCU_USB.h create mode 100644 src/MCU_USB/prg/MCU_USB_prg.c create mode 100644 src/MCU_USB/prg/MCU_USB_priv.h create mode 100644 src/STD_TYPES/inc/STD_TYPES.h create mode 100644 src/SYS_ECU/cfg/SYS_ECU_cfg create mode 100644 src/SYS_ECU/inc/SYS_ECU.h create mode 100644 src/SYS_ECU/prg/SYS_ECU.c diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..bb5ee5d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,64 @@ +// ============================================================================ +// devcontainer.json - VS Code Dev Containers configuration +// ---------------------------------------------------------------------------- +// Tells the VS Code "Dev Containers" extension how to launch this project +// inside the Docker container defined by our docker-compose.yml. When you +// run "Dev Containers: Reopen in Container" from the command palette, VS +// Code uses this file to: +// 1. Reuse the existing docker-compose.yml / Dockerfile (no duplication) +// 2. Attach to the `pico-build` service and open /project as the workspace +// 3. Install clangd + CMake Tools INSIDE the container (so language servers +// can see the real Pico SDK at /opt/pico-sdk) +// 4. Run CMake once after creation to generate compile_commands.json so +// intellisense works from the moment the first file is opened +// +// NOTE: this file is JSONC (JSON with Comments + trailing commas allowed), +// which is why these // comments are legal. +// ============================================================================ +{ + // Display name shown in the VS Code status bar / window title + "name": "Color Switcher PICO", + + // Path to the docker-compose file, relative to THIS file. + // .devcontainer/ is one level below the project root, so ../ reaches it. + "dockerComposeFile": "../docker-compose.yml", + + // Which service in docker-compose.yml to attach to. We only have one. + "service": "pico-build", + + // Folder inside the container to open as the VS Code workspace. + // Must match the WORKDIR set in the Dockerfile. + "workspaceFolder": "/project", + + // Runs exactly once, the first time the container is created. We invoke + // CMake to configure the build so that compile_commands.json is written + // to build/ before clangd tries to parse the first source file. + "postCreateCommand": "cmake -S cmake -B build", + + "customizations": { + "vscode": { + // VS Code extensions installed INSIDE the container (not on the + // host Mac). These run with access to /opt/pico-sdk and can + // resolve all Pico SDK headers correctly. + "extensions": [ + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.cmake-tools" + ], + + // Arguments forwarded to clangd when it starts. + // --compile-commands-dir tells clangd where CMake wrote the + // compile_commands.json (inside build/, not the project root) + // --header-insertion=never stops clangd from auto-adding + // #include lines as you type, which tends to be noisy + // --background-index builds a project-wide symbol index in + // the background for fast go-to-definition / find-references + "settings": { + "clangd.arguments": [ + "--compile-commands-dir=${workspaceFolder}/build", + "--header-insertion=never", + "--background-index" + ] + } + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c19231 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# ============================================================================ +# .gitignore for the color_switcher Pico firmware project +# ============================================================================ + +# Build artifacts directory created by CMake / build.sh. +# Contains Makefiles, object files, the final .elf/.uf2/.bin/.hex/.dis/.map +# outputs, and any fetched dependencies (e.g. picotool). Regenerated on +# every build, so it should never be committed. +build/ diff --git a/CLAUDE.md b/CLAUDE.md index 939d84a..df81a09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,4 +4,65 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project -Demo project for pico-1, Anthropic's agentic coding tool. +Raspberry Pi Pico firmware that sends commands over UART to a host computer. Written entirely in C using the Pico SDK (RP2040 / Cortex-M0+). + +## Build System + +All builds run inside Docker — no local toolchain required. + +- `docker compose build` — build the container image (first time / after Dockerfile changes) +- `docker compose run --rm pico-build bash build.sh` — compile the firmware, output lands in `build/` +- `docker compose run --rm pico-build bash` — interactive shell for debugging +- `./flash.sh` — flash the `.uf2` to a Pico in BOOTSEL mode (host-side only, not Docker) +- `docker compose run --rm pico-build bash build.sh && ./flash.sh` — build + flash in one command + +The build uses CMake with the Pico SDK's ARM cross-compilation toolchain. The final artifact is a `.uf2` file in `build/`. Flashing must run on the host macOS because it copies to `/Volumes/RPI-RP2` (the Pico's USB mass storage mount). + +## Repository layout + +``` +color_switcher/ +├── cmake/ # all build-system files +│ ├── CMakeLists.txt # thin orchestrator, drives build phases in order +│ ├── pico_sdk_import.cmake # Pico SDK bootstrap (copied from SDK) +│ └── cmake_config/ +│ ├── project_config.cmake # project-wide vars (name, version, languages) +│ ├── mcu_config.cmake # MCU helpers: mcu_init / mcu_sdk_config / mcu_link_target +│ └── sources_config.cmake # source glob + include dir list +├── src/ +│ ├── STD_TYPES/inc/ # shared fixed-width types, status enums, macros +│ ├── MCU_UART/{inc,prg,cfg}/ # hardware UART peripheral abstraction +│ ├── MCU_USB/{inc,prg,cfg}/ # USB-CDC (virtual serial port) abstraction +│ ├── HAL_COM/{inc,prg,cfg}/ # transport-agnostic comm layer (dispatches to UART/USB) +│ ├── APP_CLSW/{inc,prg,cfg}/ # color switcher application logic +│ └── SYS_ECU/prg/ # top-level app orchestrator (main entry) +├── build.sh # out-of-source build wrapper +├── Dockerfile / docker-compose.yml # containerized build env +``` + +## Component file convention + +Each component uses three subfolders: +- `inc/` — public API headers (safe for any other component to `#include`) +- `prg/` — private header (`*_priv.h`) + implementation (`*_prg.c`) +- `cfg/` — configuration header and definitions (`*_cfg.h` / `*_cfg.c`) + +## CMake build phases + +The top-level `cmake/CMakeLists.txt` runs in a strict order dictated by the Pico SDK: + +1. `include(project_config)` — defines variables +2. `include(mcu_config)` — defines helper macros (no side effects) +3. `mcu_init()` — includes `pico_sdk_import.cmake` (must be before `project()`) +4. `project(...)` +5. `mcu_sdk_config()` — calls `pico_sdk_init()` +6. `include(sources_config)` — sets `PROJECT_SOURCES` and `PROJECT_INCLUDE_DIRS` +7. `add_executable(...)` +8. `mcu_link_target(...)` — `target_link_libraries`, stdio routing, UF2 output + +`PROJECT_ROOT_DIR` is computed in the top-level `CMakeLists.txt` as the parent of `cmake/` and used everywhere that refers to paths outside the build system (e.g. `src/`). + +## Conventions + +- Always add descriptive comments to all code and config files +- Avoid magic numbers — use named constants in `config.h` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aff7f48 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# Base image: Ubuntu 24.04 provides a stable environment for the ARM cross-compilation toolchain +FROM ubuntu:24.04 + +# Install all dependencies required to cross-compile C code for the Raspberry Pi Pico (RP2040): +# - cmake: build system generator used by the Pico SDK +# - gcc-arm-none-eabi: ARM cross-compiler that produces binaries for the Pico's Cortex-M0+ CPU +# - libnewlib-arm-none-eabi: C standard library implementation for bare-metal ARM targets +# - build-essential: provides make and other essential build utilities +# - git: needed to clone the Pico SDK and its submodules +# - python3: required by the Pico SDK build scripts for UF2 generation and other tooling +# - clangd: C/C++ language server used by the VS Code Dev Container for +# intellisense (go-to-definition, hover types, autocomplete) - +# must live INSIDE the container so it can resolve Pico SDK +# headers under /opt/pico-sdk at their real paths +RUN apt-get update && apt-get install -y \ + cmake \ + gcc-arm-none-eabi \ + libnewlib-arm-none-eabi \ + build-essential \ + git \ + python3 \ + clangd \ + && rm -rf /var/lib/apt/lists/* + +# Clone the official Raspberry Pi Pico SDK into the container. +# The SDK provides hardware abstraction libraries, startup code, and linker scripts +# needed to build firmware for the RP2040 microcontroller. +# Submodules include tinyusb (USB stack) and other vendor-specific dependencies. +RUN git clone https://github.com/raspberrypi/pico-sdk.git /opt/pico-sdk \ + && cd /opt/pico-sdk \ + && git submodule update --init + +# Tell CMake where to find the Pico SDK. The SDK's CMake scripts look for this +# environment variable to locate platform files, toolchain config, and libraries. +ENV PICO_SDK_PATH=/opt/pico-sdk + +# All build commands will run from /project, which is where the host source code +# gets mounted via docker-compose volumes +WORKDIR /project diff --git a/README.md b/README.md index d0323d8..92a3fc8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,36 @@ -# Demo1 +# Color Switcher -A demo project for [pico-1](https://github.com/anthropics/pico-1), Anthropic's agentic coding tool. +A Raspberry Pi Pico project that sends commands over UART to a connected computer. Written entirely in C using the Pico SDK. -## Overview +## Prerequisites -This project serves as a demonstration and playground for exploring pico-1 capabilities. +- [Docker](https://docs.docker.com/get-docker/) and Docker Compose -## Getting Started +No local toolchain installation needed — everything runs inside the container. -Clone the repository and start experimenting: +## Building ```bash -cd demo1 +# Build the Docker image (first time only, or after Dockerfile changes) +docker compose build + +# Compile the firmware +docker compose up ``` + +The `.uf2` firmware file will appear in `build/`. + +## Flashing + +1. Hold the **BOOTSEL** button on the Pico and plug it into USB +2. It mounts as a USB mass storage device +3. Drag the `.uf2` file from `build/` onto the Pico +4. The Pico reboots and runs the firmware + +## Interactive Shell + +To drop into the build container for debugging or manual commands: + +```bash +docker compose run --rm pico-build bash +``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..3c803b9 --- /dev/null +++ b/build.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Build script for the Pico firmware. +# Creates an out-of-source build directory to keep generated files +# separate from the project source code. +# +# Directory layout expected at project root: +# cmake/ - all build-system files (CMakeLists.txt + cmake_config/) +# src/ - application source code +# build/ - created by this script, holds all CMake/Make output +# +# Using -S and -B lets us point CMake at the cmake/ source folder while +# keeping the build artifacts in a sibling build/ folder at the project root. + +# Fail fast on any error so a broken configure step doesn't silently lead +# to a confusing make error further down. +set -e + +# Configure the build: tell CMake the source directory is cmake/ and the +# binary (build) directory is build/. CMake will create build/ if needed. +cmake -S cmake -B build + +# Compile everything using all available CPU cores. The final output is a +# .uf2 file in build/ that can be dragged onto the Pico's USB mass storage. +cmake --build build -j"$(nproc)" diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt new file mode 100644 index 0000000..f2393d5 --- /dev/null +++ b/cmake/CMakeLists.txt @@ -0,0 +1,65 @@ +# ============================================================================ +# Top-level CMakeLists.txt for the color_switcher Pico firmware project +# ============================================================================ +# This file is intentionally kept thin: it orchestrates the build phases in +# the strict order CMake requires. All real configuration lives in the +# cmake_config/*.cmake fragments so each concern is isolated and easy to find. +# +# Build phase order (forced by the Pico SDK + CMake semantics): +# 1. project_config - variables only, no side effects +# 2. mcu_config - defines mcu_init / mcu_sdk_config / mcu_link_target +# 3. mcu_init() - SDK bootstrap (MUST run before project()) +# 4. project() - declares the CMake project using project_config vars +# 5. mcu_sdk_config() - pico_sdk_init() (MUST run after project()) +# 6. sources_config - collects PROJECT_SOURCES and PROJECT_INCLUDE_DIRS +# 7. add_executable() - the actual firmware build target +# 8. mcu_link_target() - target_link_libraries + stdio + UF2 output +# ============================================================================ + +# Require CMake 3.13 or newer. The Pico SDK uses features (like +# target_link_libraries on object libraries) that were added in 3.13, +# so older versions will fail to configure with cryptic errors. +cmake_minimum_required(VERSION 3.13) + +# The real project root is one level above this file (this CMakeLists.txt +# lives in /cmake/). Everything outside the build system - the +# src/ tree, the Dockerfile, etc. - is referenced via PROJECT_ROOT_DIR so +# we never have to sprinkle "../" paths throughout the cmake fragments. +get_filename_component(PROJECT_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) + +# Make cmake_config/ searchable so include(foo) resolves to +# cmake_config/foo.cmake without needing the full path each time. +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake_config) + +# Phase 1 - project-wide variables (PROJECT_NAME, PROJECT_VERSION, etc.) +include(project_config) + +# Phase 2 - MCU helper macros/functions (no side effects, just definitions) +include(mcu_config) + +# Phase 3 - Pico SDK bootstrap. MUST run before project() so the ARM +# cross-compile toolchain is configured before CMake enables languages. +mcu_init() + +# Phase 4 - declare the CMake project using variables from project_config. +# This triggers CMake to detect the (already-configured) cross compiler. +project(${PROJECT_NAME} + VERSION ${PROJECT_VERSION} + LANGUAGES ${PROJECT_LANGUAGES}) + +# Phase 5 - register every Pico SDK library as a CMake target so we can +# pick which ones to link later. This does NOT pull libraries into the +# final binary - mcu_link_target() does that. +mcu_sdk_config() + +# Phase 6 - collect the list of .c files and include directories into +# PROJECT_SOURCES and PROJECT_INCLUDE_DIRS variables. +include(sources_config) + +# Phase 7 - declare the firmware executable and its include paths. +add_executable(${PROJECT_NAME} ${PROJECT_SOURCES}) +target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_INCLUDE_DIRS}) + +# Phase 8 - link only the SDK libraries we actually use (pico_stdlib, +# hardware_uart), route stdio over USB-CDC, and emit the .uf2 firmware file. +mcu_link_target(${PROJECT_NAME}) diff --git a/cmake/cmake_config/mcu_config.cmake b/cmake/cmake_config/mcu_config.cmake new file mode 100644 index 0000000..a67d56e --- /dev/null +++ b/cmake/cmake_config/mcu_config.cmake @@ -0,0 +1,94 @@ +# ============================================================================ +# mcu_config.cmake +# ---------------------------------------------------------------------------- +# Encapsulates all Raspberry Pi Pico / RP2040 specific configuration. +# Depends on project_config.cmake having been included first (reads +# PROJECT_C_STANDARD). +# +# Because CMake forces a strict order (SDK bootstrap before project(), +# pico_sdk_init() after project(), linking after add_executable), this file +# exposes three macros/functions that the top-level CMakeLists.txt calls +# at the correct points in the build flow: +# +# mcu_init() - includes pico_sdk_import.cmake (pre-project) +# mcu_sdk_config() - runs pico_sdk_init() (post-project) +# mcu_link_target(target) - links SDK libs, configures stdio, emits UF2 +# ============================================================================ + +# ---------------------------------------------------------------------------- +# Step 1/3 - called BEFORE project() in the top-level CMakeLists.txt. +# Pulls in the Pico SDK bootstrap file so the ARM cross-compile toolchain +# (arm-none-eabi-gcc) is configured before CMake enables the project +# languages. Calling project() before this runs would cause CMake to detect +# the host compiler instead and produce a broken build. +# ---------------------------------------------------------------------------- +macro(mcu_init) + # pico_sdk_import.cmake lives alongside the top-level CMakeLists.txt + # inside the cmake/ folder, so CMAKE_SOURCE_DIR (which points at cmake/ + # when that file is the one being processed) is the right base path. + include(${CMAKE_SOURCE_DIR}/pico_sdk_import.cmake) +endmacro() + +# ---------------------------------------------------------------------------- +# Step 2/3 - called AFTER project() in the top-level CMakeLists.txt. +# Applies the C standard chosen in project_config.cmake and registers every +# Pico SDK library as a CMake target (pico_stdlib, hardware_uart, +# hardware_gpio, ...) so we can later pick only the ones we need. +# Note: pico_sdk_init() does NOT pull any libraries into the final binary +# by itself - it only makes them available as link targets. +# ---------------------------------------------------------------------------- +macro(mcu_sdk_config) + set(CMAKE_C_STANDARD ${PROJECT_C_STANDARD}) + set(CMAKE_C_STANDARD_REQUIRED ON) + pico_sdk_init() +endmacro() + +# ---------------------------------------------------------------------------- +# Step 3/3 - called AFTER add_executable() in the top-level CMakeLists.txt. +# Links only the Pico SDK libraries we actually use, routes stdio over +# USB-CDC so printf output is visible on the host serial monitor without +# any extra hardware, and emits the .uf2 file used for drag-and-drop +# flashing onto the Pico's mass-storage bootloader. +# +# Parameters: +# target - name of the executable target created with add_executable() +# ---------------------------------------------------------------------------- +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) + target_link_libraries(${target} PRIVATE + pico_stdlib + hardware_uart + ) + + # 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. + # 1 = enabled, 0 = disabled. + pico_enable_stdio_usb(${target} 1) + pico_enable_stdio_uart(${target} 0) + + # Ask the SDK to generate the .uf2 (plus .hex, .bin, .map) alongside + # the .elf so the firmware can be flashed by dragging it onto the + # Pico's USB mass-storage device after holding BOOTSEL. + pico_add_extra_outputs(${target}) + + # Custom "flash" target: builds the firmware first (DEPENDS ensures + # the .uf2 is up to date), then runs flash.sh on the host to copy + # it to the Pico in BOOTSEL mode. + # + # Usage: cmake --build build --target flash + # + # NOTE: This target only works when cmake runs on the HOST macOS + # (not inside Docker), because it needs access to /Volumes/RPI-RP2. + # When running inside Docker, the target will fail with a clear error + # from flash.sh ("not found" or "/Volumes/RPI-RP2 not accessible"). + add_custom_target(flash + COMMAND bash ${PROJECT_ROOT_DIR}/flash.sh + DEPENDS ${target} + WORKING_DIRECTORY ${PROJECT_ROOT_DIR} + COMMENT "Flashing firmware to Pico via USB mass storage" + VERBATIM + ) +endfunction() diff --git a/cmake/cmake_config/project_config.cmake b/cmake/cmake_config/project_config.cmake new file mode 100644 index 0000000..99b53ae --- /dev/null +++ b/cmake/cmake_config/project_config.cmake @@ -0,0 +1,38 @@ +# ============================================================================ +# project_config.cmake +# ---------------------------------------------------------------------------- +# Project-wide identity and language settings. No side effects - this file +# only sets variables that later fragments (mcu_config, sources) and the +# top-level CMakeLists.txt read from. +# ============================================================================ + +# Human-readable project name. Used as the CMake project name AND as the +# build target name, so the final firmware artifact will be named +# Color_Switcher_PICO.uf2 / .elf / .bin etc. +set(PROJECT_NAME Color_Switcher_PICO) + +# Semantic version of the firmware. Bump manually on releases. +set(PROJECT_VERSION 0.1.0) + +# Languages this project compiles: +# - C : our application source files +# - CXX : parts of the Pico SDK are C++ internally, so CMake must enable it +# even though we write no C++ of our own +# - ASM : RP2040 startup code, vector table, and boot2 are .S files that +# must be passed through the assembler +set(PROJECT_LANGUAGES C CXX ASM) + +# C standard used across all targets (C11 gives us _Static_assert, stdint, +# stdbool, etc. without relying on compiler extensions). +set(PROJECT_C_STANDARD 11) + +# Tell CMake to emit a compile_commands.json file inside the build directory +# whenever the project is configured. This file lists every source file and +# the exact compile command (including every -I include path and -D define) +# that CMake will use to build it. clangd (the language server used by VS +# Code for C/C++ intellisense) reads it to resolve #include directives and +# provide accurate hover types, go-to-definition, and diagnostics. Without +# this flag, clangd would guess blindly and fail to find Pico SDK headers +# under /opt/pico-sdk. This setting is project-wide and safe - it costs +# nothing at build time and is ignored by the compiler itself. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/cmake/cmake_config/sources_config.cmake b/cmake/cmake_config/sources_config.cmake new file mode 100644 index 0000000..7687f3a --- /dev/null +++ b/cmake/cmake_config/sources_config.cmake @@ -0,0 +1,61 @@ +# ============================================================================ +# sources_config.cmake +# ---------------------------------------------------------------------------- +# Collects the list of source files and include directories for the build. +# Does NOT declare the executable - the top-level CMakeLists.txt handles +# add_executable() so the build target is visible in one obvious place. +# +# Exported variables: +# PROJECT_SOURCES - list of every .c file under src/ +# PROJECT_INCLUDE_DIRS - list of include paths for each component +# ============================================================================ + +# NOTE: all source/header paths below are rooted at PROJECT_ROOT_DIR, which +# the top-level CMakeLists.txt computes as the parent of its own directory +# (i.e. one level above cmake/). We do NOT use CMAKE_SOURCE_DIR here because +# that variable points at the cmake/ folder itself, not at the project root. + +# Recursively collect every .c file under src/. CONFIGURE_DEPENDS makes +# CMake re-check the glob on every build, so newly added .c files are +# picked up without having to manually re-run cmake. The trade-off is a +# tiny build-time stat check on each file, which is well worth it for a +# small project where we add files often. +file(GLOB_RECURSE PROJECT_SOURCES CONFIGURE_DEPENDS + "${PROJECT_ROOT_DIR}/src/*.c") + +# Every component follows the same folder convention: +# inc/ - public API headers (safe for any other component to include) +# prg/ - private headers and implementation files (component-internal) +# cfg/ - configuration headers and constants +# +# All three are added to the include path here so #include "MCU_UART.h" +# etc. resolves regardless of which translation unit is doing the include. +set(PROJECT_INCLUDE_DIRS + # Shared library layer - fundamental types used by every component + ${PROJECT_ROOT_DIR}/src/STD_TYPES/inc + + # MCU layer - hardware abstraction for the RP2040 UART peripheral + ${PROJECT_ROOT_DIR}/src/MCU_UART/inc + ${PROJECT_ROOT_DIR}/src/MCU_UART/prg + ${PROJECT_ROOT_DIR}/src/MCU_UART/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 + ${PROJECT_ROOT_DIR}/src/MCU_USB/prg + ${PROJECT_ROOT_DIR}/src/MCU_USB/cfg + + # HAL layer - transport-agnostic communication abstraction that + # dispatches to MCU_UART, MCU_USB, or both depending on configuration + ${PROJECT_ROOT_DIR}/src/HAL_COM/inc + ${PROJECT_ROOT_DIR}/src/HAL_COM/prg + ${PROJECT_ROOT_DIR}/src/HAL_COM/cfg + + # Application layer - color switcher application logic + ${PROJECT_ROOT_DIR}/src/APP_CLSW/inc + ${PROJECT_ROOT_DIR}/src/APP_CLSW/prg + ${PROJECT_ROOT_DIR}/src/APP_CLSW/cfg + + # Application layer - system ECU / top-level orchestrator + ${PROJECT_ROOT_DIR}/src/SYS_ECU/prg +) diff --git a/cmake/pico_sdk_import.cmake b/cmake/pico_sdk_import.cmake new file mode 100644 index 0000000..d493cc2 --- /dev/null +++ b/cmake/pico_sdk_import.cmake @@ -0,0 +1,121 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +# following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +# disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG)) + set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG}) + message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')") +endif () + +if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG) + set(PICO_SDK_FETCH_FROM_GIT_TAG "master") + message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG") +endif() + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") +set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + ) + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + GIT_SUBMODULES_RECURSE FALSE + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + else () + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + endif () + + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d196836 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +# ============================================================================ +# Docker Compose configuration for the Raspberry Pi Pico firmware project +# ============================================================================ +# The container is configured to be long-running (sleep infinity) rather than +# running the build on startup. This lets the same container be used for: +# 1. One-shot builds from the host +# 2. Interactive shells (docker compose exec) for debugging +# 3. VS Code Dev Containers - which expects the container to stay alive +# so it can attach clangd, install extensions, and open a terminal +# +# Usage: +# docker compose build - (re)build the image after Dockerfile changes +# docker compose up -d - start the persistent container in the background +# docker compose exec pico-build bash - shell into the running container +# docker compose run --rm pico-build bash build.sh - one-shot firmware build, container removed after +# docker compose down - stop and remove the persistent container +# ============================================================================ + +services: + pico-build: + # Build the image from the Dockerfile in the project root + build: . + + # Mount the project source code into the container's working directory. + # This lets the container read our source files and write build artifacts + # (including the .uf2 firmware file) back to the host filesystem. + volumes: + - .:/project + + # Keep the container alive indefinitely. We intentionally do NOT run the + # build on startup - `sleep infinity` lets the container stay up so it can + # be used as a persistent dev environment (VS Code Dev Containers, shells + # via `docker compose exec`, etc.). To trigger a build, run: + # docker compose run --rm pico-build bash build.sh + # or, if you're already inside the container: + # bash build.sh + command: sleep infinity diff --git a/flash.sh b/flash.sh new file mode 100755 index 0000000..d16ef8a --- /dev/null +++ b/flash.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# ============================================================================ +# flash.sh - Flash firmware to the Raspberry Pi Pico +# ============================================================================ +# This script MUST run on the host macOS (not inside Docker) because it +# needs access to /Volumes/RPI-RP2, the USB mass storage mount point that +# appears when the Pico is held in BOOTSEL mode during power-on. +# +# Usage: +# 1. Hold BOOTSEL on the Pico and plug it into USB +# 2. Run: ./flash.sh +# +# Or chain with a build: +# docker compose run --rm pico-build bash build.sh && ./flash.sh +# ============================================================================ + +PICO_MOUNT="/Volumes/RPI-RP2" +UF2_FILE="build/Color_Switcher_PICO.uf2" + +# Verify the firmware file exists before waiting for the Pico +if [ ! -f "$UF2_FILE" ]; then + echo "Error: $UF2_FILE not found. Run the build first:" + echo " docker compose run --rm pico-build bash build.sh" + exit 1 +fi + +# Wait for the Pico to appear in BOOTSEL mode +echo "Waiting for Pico in BOOTSEL mode ($PICO_MOUNT)..." +echo " -> Hold BOOTSEL and plug in the Pico via USB" +while [ ! -d "$PICO_MOUNT" ]; do + sleep 0.5 +done + +# Copy the firmware to the Pico's USB mass storage +echo "Pico detected. Copying $UF2_FILE..." +cp "$UF2_FILE" "$PICO_MOUNT/" + +# Wait for the Pico to unmount (it reboots automatically after receiving the .uf2) +echo "Waiting for Pico to reboot..." +while [ -d "$PICO_MOUNT" ]; do + sleep 0.5 +done + +echo "Done! Pico rebooted with new firmware." +echo " -> Open a serial monitor: screen /dev/tty.usbmodem* 115200" \ No newline at end of file diff --git a/src/APP_CLSW/cfg/APP_CLSW_cfg.c b/src/APP_CLSW/cfg/APP_CLSW_cfg.c new file mode 100644 index 0000000..02c065e --- /dev/null +++ b/src/APP_CLSW/cfg/APP_CLSW_cfg.c @@ -0,0 +1,13 @@ +/****************************************************************************** + * File: APP_CLSW_cfg.c + * Component: APP_CLSW + * Description: Configuration implementation for the APP_CLSW component. + * Holds the actual configuration values consumed by + * APP_CLSW_prg.c. + * + * Layer: Application - configuration + *****************************************************************************/ + +#include "APP_CLSW_cfg.h" + +/* Configuration definitions will go here */ \ No newline at end of file diff --git a/src/APP_CLSW/cfg/APP_CLSW_cfg.h b/src/APP_CLSW/cfg/APP_CLSW_cfg.h new file mode 100644 index 0000000..91faffe --- /dev/null +++ b/src/APP_CLSW/cfg/APP_CLSW_cfg.h @@ -0,0 +1,17 @@ +/****************************************************************************** + * File: APP_CLSW_cfg.h + * Component: APP_CLSW + * Description: Configuration header for the APP_CLSW component. + * Declares configuration structures and constants that can be + * edited to adapt the color switcher application behavior + * (e.g., command strings, timing intervals, color sequences). + * + * Layer: Application - configuration + *****************************************************************************/ + +#ifndef APP_CLSW_CFG_H +#define APP_CLSW_CFG_H + +/* Configuration constants and structure declarations will go here */ + +#endif /* APP_CLSW_CFG_H */ \ No newline at end of file diff --git a/src/APP_CLSW/inc/APP_CLSW.h b/src/APP_CLSW/inc/APP_CLSW.h new file mode 100644 index 0000000..402c76e --- /dev/null +++ b/src/APP_CLSW/inc/APP_CLSW.h @@ -0,0 +1,46 @@ +/****************************************************************************** + * File: APP_CLSW.h + * Component: APP_CLSW + * Description: Public interface for the Application Color Switcher component. + * Exposes the functions that the system orchestrator (SYS_ECU) + * calls to run the color-switching application logic. This + * component owns the "what to send" - it builds commands and + * passes them down through the communication stack (HAL_COM) + * to reach the host computer. + * + * Layer: Application + *****************************************************************************/ + +#ifndef APP_CLSW_H +#define APP_CLSW_H + +/* STD_TYPES provides fixed-width typedefs (u8, u32) and the STD_tenuResult + * enum used to report success/failure from the init function. */ +#include "STD_TYPES.h" + +/* ------------------------------------------------------------------------ */ +/* PUBLIC API */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Initialize the color switcher application. + * + * Sets up all lower-layer dependencies needed by the application (currently + * MCU_USB for USB-CDC communication). Must be called exactly once from + * SYS_ECU before entering the main loop. + * + * @return STD_OK on success (all subsystems initialized), + * STD_NOK if any subsystem initialization fails. + */ +STD_tenuResult APP_CLSW_enuInit(void); + +/** + * @brief Execute one iteration of the color switcher application logic. + * + * Called once per super-loop tick from SYS_ECU. Performs whatever the + * application needs to do each cycle - for now, sends a test message + * over USB-CDC to prove the communication stack is alive. + */ +void APP_CLSW_vRunnable(void); + +#endif /* APP_CLSW_H */ \ No newline at end of file diff --git a/src/APP_CLSW/prg/APP_CLSW_prg.c b/src/APP_CLSW/prg/APP_CLSW_prg.c new file mode 100644 index 0000000..739e50b --- /dev/null +++ b/src/APP_CLSW/prg/APP_CLSW_prg.c @@ -0,0 +1,40 @@ +/****************************************************************************** + * File: APP_CLSW_prg.c + * Component: APP_CLSW + * Description: Program (implementation) file for the Application Color + * Switcher. Contains the actual implementations of the public + * functions declared in APP_CLSW.h. Communicates with the host + * exclusively through HAL_COM - never touches MCU drivers + * directly. + * + * Layer: Application + *****************************************************************************/ + +#include "APP_CLSW.h" +#include "APP_CLSW_priv.h" +#include "APP_CLSW_cfg.h" + +/* HAL_COM is the only communication interface this component uses. + * It abstracts away whether bytes go over USB-CDC, hardware UART, or both. */ +#include "HAL_COM.h" + +STD_tenuResult APP_CLSW_enuInit(void) +{ + STD_tenuResult enuResultLoc = STD_OK; + + /* APP_CLSW has no internal state to set up yet. When color sequences, + * state machines, or command tables are added, initialize them here. + * The communication stack (HAL_COM, MCU_USB, etc.) is already + * initialized by SYS_ECU before this function is called. */ + + return enuResultLoc; +} + +void APP_CLSW_vRunnable(void) +{ + /* Proof-of-life: send a repeating test message to the host via the + * transport-agnostic HAL_COM layer. This will be replaced with real + * color-switching command logic once the communication stack is + * verified end-to-end. */ + HAL_COM_enuSendBuffer((const u8 *)"this is a color switcher application!\r\n", 40U); +} \ No newline at end of file diff --git a/src/APP_CLSW/prg/APP_CLSW_priv.h b/src/APP_CLSW/prg/APP_CLSW_priv.h new file mode 100644 index 0000000..1720ffc --- /dev/null +++ b/src/APP_CLSW/prg/APP_CLSW_priv.h @@ -0,0 +1,17 @@ +/****************************************************************************** + * File: APP_CLSW_priv.h + * Component: APP_CLSW + * Description: Private header for the APP_CLSW component. + * Contains internal macros, helper declarations, and any + * state/definitions that are only used inside this component. + * Nothing declared here is exposed to external components. + * + * Layer: Application - internal use only + *****************************************************************************/ + +#ifndef APP_CLSW_PRIV_H +#define APP_CLSW_PRIV_H + +/* Private declarations, internal macros and helpers will go here */ + +#endif /* APP_CLSW_PRIV_H */ \ No newline at end of file diff --git a/src/HAL_COM/cfg/HAL_COM_cfg.c b/src/HAL_COM/cfg/HAL_COM_cfg.c new file mode 100644 index 0000000..d5b3295 --- /dev/null +++ b/src/HAL_COM/cfg/HAL_COM_cfg.c @@ -0,0 +1,14 @@ +/****************************************************************************** + * File: HAL_COM_cfg.c + * Component: HAL_COM + * Description: Configuration implementation for the HAL_COM abstraction. + * Holds the actual configuration values (transport selection + * tables, timeout constants, buffer sizes) consumed by + * HAL_COM_prg.c. + * + * Layer: HAL - configuration + *****************************************************************************/ + +#include "HAL_COM_cfg.h" + +/* Configuration definitions will go here */ diff --git a/src/HAL_COM/cfg/HAL_COM_cfg.h b/src/HAL_COM/cfg/HAL_COM_cfg.h new file mode 100644 index 0000000..28f1aa7 --- /dev/null +++ b/src/HAL_COM/cfg/HAL_COM_cfg.h @@ -0,0 +1,18 @@ +/****************************************************************************** + * File: HAL_COM_cfg.h + * Component: HAL_COM + * Description: Configuration header for the HAL_COM abstraction. + * Selects which physical transport(s) HAL_COM should route + * data through: MCU_UART, MCU_USB, or both (mirrored output). + * Also exposes any timeout / buffer sizing parameters used by + * the dispatch logic. + * + * Layer: HAL - configuration + *****************************************************************************/ + +#ifndef HAL_COM_CFG_H +#define HAL_COM_CFG_H + +/* Configuration constants and transport selection macros will go here */ + +#endif /* HAL_COM_CFG_H */ diff --git a/src/HAL_COM/inc/HAL_COM.h b/src/HAL_COM/inc/HAL_COM.h new file mode 100644 index 0000000..cc5fc0c --- /dev/null +++ b/src/HAL_COM/inc/HAL_COM.h @@ -0,0 +1,66 @@ +/****************************************************************************** + * File: HAL_COM.h + * Component: HAL_COM + * Description: Public interface for the HAL communication abstraction layer. + * Provides a transport-agnostic API for sending and receiving + * bytes to/from a host computer. Underneath, HAL_COM dispatches + * to one or more MCU-level drivers (MCU_UART for the hardware + * UART peripheral, MCU_USB for USB-CDC virtual serial) based on + * the configuration in HAL_COM_cfg.h. + * + * Higher layers (e.g. APP_CLSW) should call only HAL_COM_* and + * stay unaware of which physical transport is in use. + * + * Layer: HAL (hardware abstraction, one level above MCU drivers) + *****************************************************************************/ + +#ifndef HAL_COM_H +#define HAL_COM_H + +/* STD_TYPES provides fixed-width typedefs (u8, u32) and the STD_tenuResult + * enum used to report success/failure from every function. */ +#include "STD_TYPES.h" + +/* ------------------------------------------------------------------------ */ +/* PUBLIC API */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Initialize the HAL communication layer's own internal state. + * + * Does NOT initialize the underlying MCU drivers (MCU_USB, MCU_UART) - + * SYS_ECU owns the init sequence and calls each driver's enuInit() + * separately in the correct dependency order before calling this. + * + * @return STD_OK on success, + * STD_NOK on failure. + */ +STD_tenuResult HAL_COM_enuInit(void); + +/** + * @brief Send a single byte through the active transport. + * + * Dispatches to MCU_USB_enuSendByte or MCU_UART_enuSendByte (or both) + * depending on the transport selection configured in HAL_COM_cfg.h. + * + * @param u8Byte The byte to transmit. + * @return STD_OK on success, + * STD_NOK on transmit failure. + */ +STD_tenuResult HAL_COM_enuSendByte(u8 u8Byte); + +/** + * @brief Send a buffer of bytes through the active transport. + * + * Dispatches to MCU_USB_enuSendBuffer or MCU_UART_enuSendBuffer (or both) + * depending on the transport selection configured in HAL_COM_cfg.h. + * + * @param pu8Data Pointer to the byte buffer to transmit. Must not be NULL. + * @param u16Length Number of bytes to transmit. + * @return STD_OK on success, + * STD_NULL_POINTER_ERROR if pu8Data is NULL, + * STD_NOK on transmit failure. + */ +STD_tenuResult HAL_COM_enuSendBuffer(const u8 *pu8Data, u16 u16Length); + +#endif /* HAL_COM_H */ \ No newline at end of file diff --git a/src/HAL_COM/prg/HAL_COM_prg.c b/src/HAL_COM/prg/HAL_COM_prg.c new file mode 100644 index 0000000..f0c2fd1 --- /dev/null +++ b/src/HAL_COM/prg/HAL_COM_prg.c @@ -0,0 +1,56 @@ +/****************************************************************************** + * File: HAL_COM_prg.c + * Component: HAL_COM + * Description: Program (implementation) file for the HAL_COM abstraction. + * Implements the public functions declared in HAL_COM.h by + * dispatching to the MCU-level transport drivers (MCU_UART + * and/or MCU_USB) according to the active configuration. + * + * Currently hardwired to MCU_USB. When MCU_UART is implemented, + * the dispatch will be driven by a transport-selection macro + * in HAL_COM_cfg.h. + * + * Layer: HAL + *****************************************************************************/ + +#include "HAL_COM.h" +#include "HAL_COM_priv.h" +#include "HAL_COM_cfg.h" + +/* MCU_USB is the active transport for now. When MCU_UART is ready, + * a config macro will select which driver(s) to call here. */ +#include "MCU_USB.h" + +STD_tenuResult HAL_COM_enuInit(void) +{ + STD_tenuResult enuResultLoc = STD_OK; + + /* HAL_COM has no internal state to set up yet. When transport + * selection logic or internal buffers are added, initialize them here. + * The underlying MCU drivers are already initialized by SYS_ECU + * before this function is called. */ + + return enuResultLoc; +} + +STD_tenuResult HAL_COM_enuSendByte(u8 u8Byte) +{ + STD_tenuResult enuResultLoc = STD_OK; + + /* Dispatch to the active transport driver */ + enuResultLoc = MCU_USB_enuSendByte(u8Byte); + + return enuResultLoc; +} + +STD_tenuResult HAL_COM_enuSendBuffer(const u8 *pu8Data, u16 u16Length) +{ + STD_tenuResult enuResultLoc = STD_OK; + + /* Dispatch to the active transport driver. + * Null-pointer validation is handled inside MCU_USB_enuSendBuffer + * so we don't duplicate the check here. */ + enuResultLoc = MCU_USB_enuSendBuffer(pu8Data, u16Length); + + return enuResultLoc; +} diff --git a/src/HAL_COM/prg/HAL_COM_priv.h b/src/HAL_COM/prg/HAL_COM_priv.h new file mode 100644 index 0000000..e1a69a8 --- /dev/null +++ b/src/HAL_COM/prg/HAL_COM_priv.h @@ -0,0 +1,17 @@ +/****************************************************************************** + * File: HAL_COM_priv.h + * Component: HAL_COM + * Description: Private header for the HAL_COM abstraction. + * Contains internal macros, helper declarations, and any + * state/definitions that are only used inside the component. + * Nothing declared here is exposed to external components. + * + * Layer: HAL - internal use only + *****************************************************************************/ + +#ifndef HAL_COM_PRIV_H +#define HAL_COM_PRIV_H + +/* Private declarations, internal macros and helpers will go here */ + +#endif /* HAL_COM_PRIV_H */ diff --git a/src/MCU_UART/cfg/MCU_UART_cfg.c b/src/MCU_UART/cfg/MCU_UART_cfg.c new file mode 100644 index 0000000..10dc6c6 --- /dev/null +++ b/src/MCU_UART/cfg/MCU_UART_cfg.c @@ -0,0 +1,14 @@ +/****************************************************************************** + * File: MCU_UART_cfg.c + * Component: MCU_UART + * Description: Configuration implementation for the MCU_UART driver. + * Holds the actual configuration values (baud rate, pin numbers, + * UART instance selection, etc.) defined as constants or + * configuration structures consumed by MCU_UART_prg.c. + * + * Layer: MCU (hardware abstraction) - configuration + *****************************************************************************/ + +#include "MCU_UART_cfg.h" + +/* Configuration definitions will go here */ \ No newline at end of file diff --git a/src/MCU_UART/cfg/MCU_UART_cfg.h b/src/MCU_UART/cfg/MCU_UART_cfg.h new file mode 100644 index 0000000..5c222be --- /dev/null +++ b/src/MCU_UART/cfg/MCU_UART_cfg.h @@ -0,0 +1,18 @@ +/****************************************************************************** + * File: MCU_UART_cfg.h + * Component: MCU_UART + * Description: Configuration header for the MCU_UART driver. + * Declares configuration structures and constants that can be + * edited to adapt the UART driver to the specific hardware + * setup (e.g., which UART instance, pin assignments, baud rate, + * data bits, stop bits, parity). + * + * Layer: MCU (hardware abstraction) - configuration + *****************************************************************************/ + +#ifndef MCU_UART_CFG_H +#define MCU_UART_CFG_H + +/* Configuration constants and structure declarations will go here */ + +#endif /* MCU_UART_CFG_H */ \ No newline at end of file diff --git a/src/MCU_UART/inc/MCU_UART.h b/src/MCU_UART/inc/MCU_UART.h new file mode 100644 index 0000000..2cbef32 --- /dev/null +++ b/src/MCU_UART/inc/MCU_UART.h @@ -0,0 +1,27 @@ +/****************************************************************************** + * File: MCU_UART.h + * Component: MCU_UART + * Description: Public interface for the MCU UART driver component. + * This header exposes the functions and types that other + * components are allowed to use when interacting with the UART + * peripheral on the RP2040 microcontroller. + * + * Layer: MCU (hardware abstraction) + *****************************************************************************/ + +#ifndef MCU_UART_H +#define MCU_UART_H +#include "STD_TYPES.h" + + typedef enum { + MCU_UART_OK = 0, + MCU_UART_ERROR, + MCU_UART_TIMEOUT + } MCU_UART_Status_t; + +/* Public API declarations will go here */ + MCU_UART_Status_t MCU_UART_enuInit(void); + MCU_UART_Status_t MCU_UART_enuSendByte(u8 byte); + MCU_UART_Status_t MCU_UART_enuSendBuffer(const u8 *data, + u16 length); +#endif /* MCU_UART_H */ \ No newline at end of file diff --git a/src/MCU_UART/prg/MCU_UART_prg.c b/src/MCU_UART/prg/MCU_UART_prg.c new file mode 100644 index 0000000..bcaaa71 --- /dev/null +++ b/src/MCU_UART/prg/MCU_UART_prg.c @@ -0,0 +1,17 @@ +/****************************************************************************** + * File: MCU_UART_prg.c + * Component: MCU_UART + * Description: Program (implementation) file for the MCU_UART driver. + * Contains the actual implementations of the public functions + * declared in MCU_UART.h. This is where we call the Pico SDK + * UART APIs to initialize the peripheral, transmit bytes, and + * receive data. + * + * Layer: MCU (hardware abstraction) + *****************************************************************************/ + +#include "MCU_UART.h" +#include "MCU_UART_priv.h" +#include "MCU_UART_cfg.h" + +/* Function implementations will go here */ \ No newline at end of file diff --git a/src/MCU_UART/prg/MCU_UART_priv.h b/src/MCU_UART/prg/MCU_UART_priv.h new file mode 100644 index 0000000..9bcfb44 --- /dev/null +++ b/src/MCU_UART/prg/MCU_UART_priv.h @@ -0,0 +1,18 @@ +/****************************************************************************** + * File: MCU_UART_priv.h + * Component: MCU_UART + * Description: Private header for the MCU_UART driver. + * Contains internal macros, helper declarations, and + * register-level definitions that are only used inside the + * component itself. Nothing declared here is exposed to + * external components. + * + * Layer: MCU (hardware abstraction) - internal use only + *****************************************************************************/ + +#ifndef MCU_UART_PRIV_H +#define MCU_UART_PRIV_H + +/* Private declarations, internal macros and helpers will go here */ + +#endif /* MCU_UART_PRIV_H */ \ No newline at end of file diff --git a/src/MCU_USB/cfg/MCU_USB_cfg.c b/src/MCU_USB/cfg/MCU_USB_cfg.c new file mode 100644 index 0000000..1efe6f9 --- /dev/null +++ b/src/MCU_USB/cfg/MCU_USB_cfg.c @@ -0,0 +1,14 @@ +/****************************************************************************** + * File: MCU_USB_cfg.c + * Component: MCU_USB + * Description: Configuration implementation for the MCU_USB driver. + * Holds the actual configuration values (timeouts, buffer + * sizes, mode flags) defined as constants or configuration + * structures consumed by MCU_USB_prg.c. + * + * Layer: MCU (hardware abstraction) - configuration + *****************************************************************************/ + +#include "MCU_USB_cfg.h" + +/* Configuration definitions will go here */ diff --git a/src/MCU_USB/cfg/MCU_USB_cfg.h b/src/MCU_USB/cfg/MCU_USB_cfg.h new file mode 100644 index 0000000..6fc02f7 --- /dev/null +++ b/src/MCU_USB/cfg/MCU_USB_cfg.h @@ -0,0 +1,86 @@ +/****************************************************************************** + * File: MCU_USB_cfg.h + * Component: MCU_USB + * Description: Configuration header for the MCU_USB driver. + * Declares configuration structures and constants that can be + * edited to adapt the USB-CDC driver to the application's + * needs (e.g., enable/disable connection wait, timeout values, + * transmit buffer sizes). + * + * Layer: MCU (hardware abstraction) - configuration + *****************************************************************************/ + +#ifndef MCU_USB_CFG_H +#define MCU_USB_CFG_H + +/* STD_TYPES is needed for STD_TRUE / STD_FALSE and the u8/u16/u32 typedefs + * used by the config values and timeout comparisons below. */ +#include "STD_TYPES.h" + +/* ------------------------------------------------------------------------ */ +/* CONNECTION / INIT BEHAVIOR */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Whether MCU_USB_enuInit() should block until the host has opened + * the CDC serial port before returning. + * + * Set to STD_TRUE to avoid losing the first bytes sent by the application + * (the host needs ~1-2 s after power-up to enumerate and open the port). + * Set to STD_FALSE for a fire-and-forget init that returns immediately - + * useful if the firmware must not stall when no host is attached. + */ +#define MCU_USB_WAIT_FOR_CONNECTION MCU_USB_WAIT_FOR_CONNECTION_ENABLED + +/** + * @brief Maximum time (in milliseconds) to wait for the host to open + * the CDC serial port during MCU_USB_enuInit(). + * + * Only applies when MCU_USB_WAIT_FOR_CONNECTION is ENABLED. USB host + * enumeration typically takes ~1-2 seconds, so 3000 ms gives a + * comfortable margin. Set to 0 for an infinite wait (never times out). + * This is separate from TRANSMIT/RECEIVE timeouts because connection + * setup is a one-time event with different timing characteristics + * than per-byte I/O operations. + */ +#define MCU_USB_CONNECTION_TIMEOUT_MS 3000U + +/* ------------------------------------------------------------------------ */ +/* TIMEOUTS */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Maximum time (in milliseconds) to wait for a single transmit + * operation to complete before returning STD_NOK / STD_TIMEOUT. + * + * Applied to each transmit call in MCU_USB_prg.c. Prevents the driver + * from blocking forever if the host-side serial port is closed mid-send + * or the USB bus becomes unresponsive. + */ +#define MCU_USB_TRANSMIT_TIMEOUT_MS 1000U + +/** + * @brief Maximum time (in milliseconds) to wait for a byte to arrive on + * the receive side before returning a timeout result. + * + * Applied to each blocking receive call. Prevents the driver from hanging + * when the host is attached but simply not sending anything. + */ +#define MCU_USB_RECEIVE_TIMEOUT_MS 1000U + +/* ------------------------------------------------------------------------ */ +/* BUFFER SIZING */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Size (in bytes) of the software transmit buffer used by the + * USB driver to queue outbound data. + * + * A larger buffer lets the application call MCU_USB_enuSendBuffer with + * bigger chunks without having to wait for the USB peripheral to drain, + * at the cost of more SRAM. 64 bytes matches the USB Full-Speed bulk + * endpoint packet size, which is a convenient minimum for alignment. + */ +#define MCU_USB_TRANSMIT_BUFFER_SIZE 64U + +#endif /* MCU_USB_CFG_H */ diff --git a/src/MCU_USB/inc/MCU_USB.h b/src/MCU_USB/inc/MCU_USB.h new file mode 100644 index 0000000..9a47d6f --- /dev/null +++ b/src/MCU_USB/inc/MCU_USB.h @@ -0,0 +1,70 @@ +/****************************************************************************** + * File: MCU_USB.h + * Component: MCU_USB + * Description: Public interface for the MCU USB driver component. + * This header exposes the functions and types that other + * components are allowed to use to send and receive data over + * the RP2040 USB-CDC (virtual serial port) interface. From the + * host computer's perspective, the Pico appears as a regular + * serial device (/dev/tty.usbmodem* on macOS, COMx on Windows). + * + * Layer: MCU (hardware abstraction) + *****************************************************************************/ + +#ifndef MCU_USB_H +#define MCU_USB_H + +/* STD_TYPES brings in the fixed-width integer typedefs (u8, u32) and the + * STD_tenuResult enum used to report success/failure from every function. */ +#include "STD_TYPES.h" + +#define MCU_USB_WAIT_FOR_CONNECTION_DISABLED 0U +#define MCU_USB_WAIT_FOR_CONNECTION_ENABLED 1U +/* ------------------------------------------------------------------------ */ +/* PUBLIC API */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Initialize the USB-CDC interface. + * + * Sets up the RP2040 USB peripheral and the TinyUSB CDC device so the + * board enumerates as a virtual serial port on the host. If the config + * macro MCU_USB_WAIT_FOR_CONNECTION is MCU_USB_WAIT_FOR_CONNECTION_ENABLED, + * this function blocks until the host opens the port (so early bytes are + * not lost), subject to MCU_USB_CONNECTION_TIMEOUT_MS in MCU_USB_cfg.h. + * If the timeout elapses before the host connects, returns STD_NOK. + * + * Must be called exactly once, before any Send/Receive function. + * + * @return STD_OK on success (USB-CDC initialized, host connected if wait enabled), + * STD_NOK on init failure or connection timeout. + */ +STD_tenuResult MCU_USB_enuInit(void); + +/** + * @brief Send a single byte over USB-CDC. + * + * Blocks until the byte has been handed off to the USB stack or the + * transmit timeout (MCU_USB_TRANSMIT_TIMEOUT_MS) elapses. + * + * @param u8Byte The byte to transmit. + * @return STD_OK on success, + * STD_NOK on transmit failure or timeout. + */ +STD_tenuResult MCU_USB_enuSendByte(u8 u8Byte); + +/** + * @brief Send a buffer of bytes over USB-CDC. + * + * Transmits u32Length bytes starting at pu8Data. Blocks until all bytes + * are sent or the transmit timeout elapses. The buffer is not modified. + * + * @param pu8Data Pointer to the byte buffer to transmit. Must not be NULL. + * @param u16Length Number of bytes to transmit. + * @return STD_OK on success, + * STD_NULL_POINTER_ERROR if pu8Data is NULL, + * STD_NOK on transmit failure or timeout. + */ +STD_tenuResult MCU_USB_enuSendBuffer(const u8 *pu8Data, u16 u16Length); + +#endif /* MCU_USB_H */ diff --git a/src/MCU_USB/prg/MCU_USB_prg.c b/src/MCU_USB/prg/MCU_USB_prg.c new file mode 100644 index 0000000..00109c6 --- /dev/null +++ b/src/MCU_USB/prg/MCU_USB_prg.c @@ -0,0 +1,119 @@ +/****************************************************************************** + * File: MCU_USB_prg.c + * Component: MCU_USB + * Description: Program (implementation) file for the MCU_USB driver. + * Contains the actual implementations of the public functions + * declared in MCU_USB.h. Wraps the Pico SDK's USB-CDC / stdio + * facilities to provide a simple send/receive API for the + * virtual serial port exposed over the Pico's USB connection. + * + * Layer: MCU (hardware abstraction) + *****************************************************************************/ +#include "STD_TYPES.h" + +#include "pico/stdio_usb.h" /* stdio_usb_init(), stdio_usb_connected() */ +#include "pico/stdio.h" /* putchar_raw() - writes one byte into the stdio driver chain */ +#include "pico/time.h" /* absolute_time_t, make_timeout_time_ms(), time_reached() */ +#include "MCU_USB.h" +#include "MCU_USB_priv.h" +#include "MCU_USB_cfg.h" + +STD_tenuResult MCU_USB_enuInit(void) +{ + STD_tenuResult enuResultLoc = STD_OK; + STD_tBool bSdkInitSuccess = STD_FALSE; + + /* Call the Pico SDK's USB-only stdio init. This brings up the TinyUSB + * device stack, registers the USB-CDC stdio driver, and starts the + * background task that services USB events. Returns true on success. */ + bSdkInitSuccess = (stdio_usb_init() != 0) ? STD_TRUE : STD_FALSE; + + if (bSdkInitSuccess == STD_FALSE) + { + enuResultLoc = STD_NOK; /* Initialization failed */ + }else + { + #if MCU_USB_WAIT_FOR_CONNECTION == MCU_USB_WAIT_FOR_CONNECTION_ENABLED + /* Wait for the host to open the CDC port, with a timeout. */ + absolute_time_t absTimeout = make_timeout_time_ms(MCU_USB_CONNECTION_TIMEOUT_MS); + STD_tBool bHostOpen = STD_FALSE; + STD_tBool bTimeoutReached = STD_FALSE; + do + { + /* Yield for 10 ms between checks. This serves two purposes: + * 1. Avoids burning 100% CPU on a busy-wait spin loop + * 2. Gives the TinyUSB background task time to process USB + * enumeration events — without yielding, the USB stack + * may not advance and the host connection is delayed. + * 10 ms matches the interval used by the Pico SDK's own + * stdio_usb_init() connection-wait implementation. */ + sleep_ms(10); + + /* Update status variables — avoid function calls in the + * while condition for readability and debuggability */ + bHostOpen = (stdio_usb_connected() != 0) ? STD_TRUE : STD_FALSE; + bTimeoutReached = (time_reached(absTimeout) != 0) ? STD_TRUE : STD_FALSE; + } while ((bHostOpen == STD_FALSE) && (bTimeoutReached == STD_FALSE)); + + /* If we exited the loop because of timeout rather than a successful + * connection, report failure so the caller knows the host never + * opened the port within the configured window. */ + if (bHostOpen == STD_FALSE) + { + enuResultLoc = STD_NOK; + } + #endif + } + return enuResultLoc; /* Return the result */ +} + +STD_tenuResult MCU_USB_enuSendByte(u8 u8Byte) +{ + STD_tenuResult enuResultLoc = STD_OK; + + /* putchar_raw is the stdio framework's "push one byte into the driver + * chain" primitive. It is declared as `int putchar_raw(int c)` because + * the C stdio family uses EOF (-1) as a sentinel return value. Passing + * u8Byte directly relies on the implicit widening conversion u8 -> int, + * which is always safe (every u8 value fits in an int) and deliberately + * keeps native C type names out of our code. + * + * Note on semantics: putchar_raw is fire-and-forget at this layer - it + * queues the byte into the USB stdio driver and returns immediately. + * The actual USB transfer happens in the TinyUSB background task. There + * is no way to detect a transmit failure from this call, so we always + * return STD_OK. When we need real delivery guarantees, we will upgrade + * this to tud_cdc_write_char + tud_cdc_write_flush. */ + putchar_raw(u8Byte); + + return enuResultLoc; +} + +STD_tenuResult MCU_USB_enuSendBuffer(const u8 *pu8Data, u16 u16Length) +{ + STD_tenuResult enuResultLoc = STD_OK; + u16 u16IndexLoc; + + /* Guard against null pointer dereference. On the RP2040 (Cortex-M0+), + * reading address 0x00000000 does NOT fault — it silently reads from + * the beginning of flash (the vector table), which means the firmware + * would send garbage bytes over USB instead of crashing. The explicit + * check catches the mistake at the source with a clear error code. */ + if (pu8Data == STD_NULL) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else + { + /* Send each byte individually via putchar_raw. Same fire-and-forget + * semantics as MCU_USB_enuSendByte — bytes are queued into the USB + * stdio driver and transmitted by the TinyUSB background task. + * No per-byte error detection is possible at this layer. */ + for (u16IndexLoc = 0U; u16IndexLoc < u16Length; u16IndexLoc++) + { + putchar_raw(pu8Data[u16IndexLoc]); + } + } + + return enuResultLoc; +} \ No newline at end of file diff --git a/src/MCU_USB/prg/MCU_USB_priv.h b/src/MCU_USB/prg/MCU_USB_priv.h new file mode 100644 index 0000000..591d8e8 --- /dev/null +++ b/src/MCU_USB/prg/MCU_USB_priv.h @@ -0,0 +1,18 @@ +/****************************************************************************** + * File: MCU_USB_priv.h + * Component: MCU_USB + * Description: Private header for the MCU_USB driver. + * Contains internal macros, helper declarations, and any + * lower-level definitions that are only used inside this + * component itself. Nothing declared here is exposed to + * external components. + * + * Layer: MCU (hardware abstraction) - internal use only + *****************************************************************************/ + +#ifndef MCU_USB_PRIV_H +#define MCU_USB_PRIV_H + +/* Private declarations, internal macros and helpers will go here */ + +#endif /* MCU_USB_PRIV_H */ diff --git a/src/STD_TYPES/inc/STD_TYPES.h b/src/STD_TYPES/inc/STD_TYPES.h new file mode 100644 index 0000000..f9f6ff3 --- /dev/null +++ b/src/STD_TYPES/inc/STD_TYPES.h @@ -0,0 +1,288 @@ +/****************************************************************************** + * File: STD_TYPES.h + * Component: STD_TYPES + * Description: Standard type definitions used across all components. + * Provides project-wide fixed-width integer typedefs, boolean + * types, and common return/status codes so that every module + * speaks the same "type language". + * + * Layer: Library (shared by all layers) + *****************************************************************************/ + +#ifndef STD_TYPES_H +#define STD_TYPES_H + +/* Standard type definitions will go here */ + +/* ************************************************************************** */ +/* ********************** PUBLIC TYPE DEFINITIONS **************************** */ +/* ************************************************************************** */ + +/** + * @addtogroup 1-LIB_TYP_Types + * @ingroup LIB_TYP + * @{ + */ + +/* ************************************************************************** */ +/* ************************ BASIC INTEGER TYPES ****************************** */ +/* ************************************************************************** */ + +/** + * @brief 8-bit unsigned integer type + * @details Size: 1 Byte, Range: [0 : 255] + */ +typedef unsigned char u8; +#define STD_u8MIN_VALUE ((u8)0) /**< Minimum value for u8 type */ +#define STD_u8MAX_VALUE ((u8)0xFF) /**< Maximum value for u8 type */ + +/** + * @brief 16-bit unsigned integer type + * @details Size: 2 Bytes, Range: [0 : 65535] + */ +typedef unsigned short int u16; +#define STD_u16MIN_VALUE ((u16)0) /**< Minimum value for u16 type */ +#define STD_u16MAX_VALUE ((u16)0xFFFFU) /**< Maximum value for u16 type */ + +/** + * @brief 32-bit unsigned integer type + * @details Size: 4 Bytes, Range: [0 : 4,294,967,295] + */ +typedef unsigned long int u32; +#define STD_u32MIN_VALUE ((u32)0) /**< Minimum value for u32 type */ +#define STD_u32MAX_VALUE ((u32)0xFFFFFFFFU) /**< Maximum value for u32 type */ + +/** + * @brief 64-bit unsigned integer type + * @details Size: 8 Bytes, Range: [0 : 18,446,744,073,709,551,615] + */ +typedef unsigned long long u64; +#define STD_u64MIN_VALUE ((u64)0) /**< Minimum value for u64 type */ +#define STD_u64MAX_VALUE ((u64)0xFFFFFFFFFFFFFFFFULL) /**< Maximum value for u64 type */ + +/** + * @brief 8-bit signed integer type + * @details Size: 1 Byte, Range: [-128 : 127] + */ +typedef signed char s8; +#define STD_s8MIN_VALUE ((s8)-128) /**< Minimum value for s8 type */ +#define STD_s8MAX_VALUE ((s8)127) /**< Maximum value for s8 type */ + +/** + * @brief 16-bit signed integer type + * @details Size: 2 Bytes, Range: [-32,768 : 32,767] + */ +typedef signed short int s16; +#define STD_s16MIN_VALUE ((s16)-32768) /**< Minimum value for s16 type */ +#define STD_s16MAX_VALUE ((s16)32767) /**< Maximum value for s16 type */ + +/** + * @brief 32-bit signed integer type + * @details Size: 4 Bytes, Range: [-2,147,483,648 : 2,147,483,647] + */ +typedef signed long int s32; +#define STD_s32MIN_VALUE ((s32)-2147483648) /**< Minimum value for s32 type */ +#define STD_s32MAX_VALUE ((s32)2147483647) /**< Maximum value for s32 type */ + +/** + * @brief 64-bit signed integer type + * @details Size: 8 Bytes, Range: [-9,223,372,036,854,775,808 : 9,223,372,036,854,775,807] + */ +typedef signed long long s64; +#define STD_s64MIN_VALUE ((s64)-9223372036854775807LL - 1LL) /**< Minimum value for s64 type */ +#define STD_s64MAX_VALUE ((s64)9223372036854775807LL) /**< Maximum value for s64 type */ + +/* ************************************************************************** */ +/* ************************* RESULT TYPES *********************************** */ +/* ************************************************************************** */ + +/** + * @brief Standard Result Type + * + * Enumeration for function return values indicating operation status + */ +typedef enum +{ + STD_OK = 0U, /**< Operation completed successfully */ + STD_INDEX_OUT_OF_RANGE_ERROR, /**< Array index out of bounds */ + STD_NULL_POINTER_ERROR, /**< Null pointer detected */ + STD_NOK /**< Operation failed */ +} STD_tenuResult; + +/* ************************************************************************** */ +/* ************************ CALLBACK TYPES ********************************** */ +/* ************************************************************************** */ + +/** + * @brief Callback Function Type + * @details Type definition for void callback functions + */ +typedef void (*STD_tpfCallbackFunc)(void); + +/** + * @brief Initialization Function Type + * @details Type definition for initialization functions returning STD_tenuResult + */ +typedef STD_tenuResult (*STD_tpfInitFunc)(void); + +/* ************************************************************************** */ +/* ************************ BOOLEAN AND STATE TYPES ************************* */ +/* ************************************************************************** */ + +/** + * @brief Boolean Type + * @details Standard boolean enumeration + */ +typedef enum +{ + STD_FALSE, /**< False value */ + STD_TRUE /**< True value */ +} STD_tBool; + +/** + * @brief State Type + * @details Standard state enumeration + */ +typedef enum +{ + STD_IDLE, /**< Idle state */ + STD_BUSY /**< Busy state */ +} STD_tenuState; + +/** + * @brief Compare State Type + * @details Type for comparison results + */ +typedef u8 STD_tu8CMPstate; + +/** @} */ // end of 1-LIB_TYP_Types group + + +/* ************************************************************************** */ +/* ************************* UTILITY MACROS ********************************* */ +/* ************************************************************************** */ + +/** + * @addtogroup 2-LIB_TYP_Macros + * @ingroup LIB_TYP + * @{ + */ + +/** @brief Null pointer definition */ +#define STD_NULL ((void *)0) + +/** + * @brief Constant qualifier macro + * @details Removes const qualifier in unit test builds + */ +#ifdef UTD +# define STD_CONST +#else +# define STD_CONST const +#endif + +/** + * @brief Static qualifier macro + * @details Removes static qualifier in unit test builds + */ +#ifdef UTD +# define STD_STATIC +#else +# define STD_STATIC static +#endif + +/** + * @brief Inline function macro + * @details Controls function inlining based on build type + */ +#ifdef UTD +# define STD_INLINE +#else +# define STD_INLINE __attribute__((always_inline)) inline +#endif + +/** + * @brief Interrupt function macro + * @details Marks functions as interrupts in non-test builds + */ +#ifdef UTD +# define STD_INTERRUPT +#else +# define STD_INTERRUPT __attribute__((interrupt)) +#endif + +/** + * @brief Non-inlined interrupt function macro + * @details Marks functions as non-inlined interrupts in non-test builds. This prevents + * the compiler from inlining interrupt service routines, which can be important + * for debugging and maintaining consistent interrupt latency. + */ +#ifdef UTD +# define STD_INTERRUPT_NO_INLINE +#else +# define STD_INTERRUPT_NO_INLINE __attribute__((interrupt, noinline)) +#endif + +/** + * @brief Weak symbol macro + * @details Marks symbols as weak in non-test builds + */ +#ifdef UTD +# define STD_WEAK +#else +# define STD_WEAK __attribute__((weak)) +#endif + +/** + * @brief Packed structure macro + * @details Forces packed memory layout + */ +#define STD_PACKED __attribute__((packed)) + +/** + * @brief 2-byte alignment macro + * @details Forces 2-byte alignment for structures and variables. + * Required for MLX16 architecture DMA buffers and optimal performance. + */ +#define STD_ALIGNED_2BYTE __attribute__((aligned(2))) + +/** + * @brief Switch case fallthrough macro + * @details Explicitly indicates intentional fallthrough in switch statements. + * Suppresses compiler warnings about implicit fallthrough between cases. + * Use this when you intentionally omit a break statement. + * @note Only available in GCC 7 and later + */ +#if __GNUC__ >= 7 +#define STD_FALLTHROUGH __attribute__((fallthrough)) +#else +#define STD_FALLTHROUGH +#endif + +/** + * @brief Static assertion macro + * @details Provides compile-time assertions for configuration validation. + * This macro abstracts the compiler-specific static assert mechanism + * allowing for easy portability across different compilers. + * + * @param condition The condition to assert (must be compile-time constant) + * @param message The error message to display if assertion fails + * + * @note For C11 compliant compilers, uses _Static_assert. + * For older compilers, falls back to a compatible implementation. + */ +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) + /* C11 and later: use standard _Static_assert */ + #define STD_STATIC_ASSERT(condition, message) _Static_assert(condition, message) +#elif defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)) + /* GCC 4.6+: use _Static_assert extension */ + #define STD_STATIC_ASSERT(condition, message) _Static_assert(condition, message) +#else + /* Fallback for older compilers: use array size trick */ + #define STD_STATIC_ASSERT(condition, message) \ + typedef char static_assertion_##__LINE__[(condition) ? 1 : -1] +#endif + +/** @} */ // end of 2-LIB_TYP_Macros group + +#endif /* STD_TYPES_H */ diff --git a/src/SYS_ECU/cfg/SYS_ECU_cfg b/src/SYS_ECU/cfg/SYS_ECU_cfg new file mode 100644 index 0000000..e69de29 diff --git a/src/SYS_ECU/inc/SYS_ECU.h b/src/SYS_ECU/inc/SYS_ECU.h new file mode 100644 index 0000000..e69de29 diff --git a/src/SYS_ECU/prg/SYS_ECU.c b/src/SYS_ECU/prg/SYS_ECU.c new file mode 100644 index 0000000..b9d26fc --- /dev/null +++ b/src/SYS_ECU/prg/SYS_ECU.c @@ -0,0 +1,67 @@ +/****************************************************************************** + * File: SYS_ECU.c + * Component: SYS_ECU + * Description: System ECU entry point and main application loop. + * This is the top-level orchestrator for the firmware: it owns + * the initialization sequence (calling each component's enuInit + * in dependency order) and the main super-loop scheduler that + * dispatches runnables at the configured cadence. + * + * SYS_ECU never contains application logic itself - it only + * calls into the components that do. + * + * Layer: System (top of the stack) + *****************************************************************************/ + +#include "STD_TYPES.h" +#include "pico/stdlib.h" /* sleep_ms */ + +/* Components initialized and scheduled by SYS_ECU, listed in + * dependency order (drivers first, then HAL, then application). */ +#include "MCU_USB.h" +#include "HAL_COM.h" +#include "APP_CLSW.h" + +/* ========================================================================= */ +/* INITIALIZATION SEQUENCE */ +/* ========================================================================= */ + +/** + * @brief Initialize all components in dependency order. + * + * Each component's enuInit only sets up its own internal state. + * The call order matters: MCU drivers first (they talk to hardware), + * then HAL (depends on drivers), then application (depends on HAL). + */ +static void SYS_ECU_vInitAll(void) +{ + /* MCU layer - hardware drivers */ + MCU_USB_enuInit(); + + /* HAL layer - abstractions over hardware */ + HAL_COM_enuInit(); + + /* Application layer */ + APP_CLSW_enuInit(); +} + +/* ========================================================================= */ +/* MAIN ENTRY */ +/* ========================================================================= */ + +int main(void) +{ + /* Phase 1: bring up every component in the correct order */ + SYS_ECU_vInitAll(); + + /* Phase 2: super-loop scheduler - dispatches runnables each tick. + * For the proof-of-life test, APP_CLSW_vRunnable sends a message + * over USB-CDC once per second. The sleep_ms call here controls the + * scheduler tick rate - it will eventually be replaced by a proper + * timer-driven scheduler. */ + while (1) + { + APP_CLSW_vRunnable(); + sleep_ms(1000); + } +} \ No newline at end of file From f50e877ffc8fb3f68d8a3a23b7a1f3290eb7d490 Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Sun, 12 Apr 2026 19:36:41 +0200 Subject: [PATCH 2/6] Implement MCU_UART driver with blocking and non-blocking TX Full hardware UART driver with config-driven per-instance setup. Non-blocking SendBuffer supports DMA (zero-CPU) and ISR (FIFO-fill) modes selectable per instance in MCU_UART_cfg.h. Both modes and blocking SendBufferBlocking invoke a configurable TX-complete callback (STD_NULL to ignore). All public functions take a u8 instance parameter. Config uses struct-of-arrays pattern for runtime state, designated-initializer array for per-instance settings, and value enums in the public header for parity/data bits/stop bits/async mode. Added hardware_dma to CMake link list. --- cmake/cmake_config/mcu_config.cmake | 2 + src/MCU_UART/cfg/MCU_UART_cfg.c | 40 ++- src/MCU_UART/cfg/MCU_UART_cfg.h | 68 ++++- src/MCU_UART/inc/MCU_UART.h | 165 +++++++++++- src/MCU_UART/prg/MCU_UART_prg.c | 380 +++++++++++++++++++++++++++- src/MCU_UART/prg/MCU_UART_priv.h | 57 ++++- 6 files changed, 680 insertions(+), 32 deletions(-) diff --git a/cmake/cmake_config/mcu_config.cmake b/cmake/cmake_config/mcu_config.cmake index a67d56e..b7a1dc9 100644 --- a/cmake/cmake_config/mcu_config.cmake +++ b/cmake/cmake_config/mcu_config.cmake @@ -57,9 +57,11 @@ 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) target_link_libraries(${target} PRIVATE pico_stdlib hardware_uart + hardware_dma ) # Route stdio over USB-CDC: the Pico will appear as a virtual serial diff --git a/src/MCU_UART/cfg/MCU_UART_cfg.c b/src/MCU_UART/cfg/MCU_UART_cfg.c index 10dc6c6..e9f92e7 100644 --- a/src/MCU_UART/cfg/MCU_UART_cfg.c +++ b/src/MCU_UART/cfg/MCU_UART_cfg.c @@ -2,13 +2,45 @@ * File: MCU_UART_cfg.c * Component: MCU_UART * Description: Configuration implementation for the MCU_UART driver. - * Holds the actual configuration values (baud rate, pin numbers, - * UART instance selection, etc.) defined as constants or - * configuration structures consumed by MCU_UART_prg.c. + * Defines the MCU_UART_astrConfig[] array that holds the actual + * per-instance UART settings. Each entry is initialized using + * the named macros from MCU_UART_cfg.h so that no magic numbers + * appear in the initializer. * * Layer: MCU (hardware abstraction) - configuration *****************************************************************************/ +/* MCU_UART.h is included first because it defines the struct type + * (MCU_UART_tstrConfig) and the enum types (MCU_UART_tenuDataBits, etc.) + * that are used in the array definition below. */ +#include "MCU_UART.h" #include "MCU_UART_cfg.h" -/* Configuration definitions will go here */ \ No newline at end of file +/* ------------------------------------------------------------------------ */ +/* CONFIGURATION ARRAY DEFINITION */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Per-instance UART configuration, indexed by MCU_UART_tenuInstance. + * + * [MCU_UART_INSTANCE_0] = uart0 on the RP2040. Configured for standard + * 8-N-1 at 115200 baud on GP0 (TX) / GP1 (RX), with DMA-based async TX + * and no TX-complete callback. + * + * To add uart1: add a [MCU_UART_INSTANCE_1] entry here with the + * corresponding MCU_UART_1_* macros defined in MCU_UART_cfg.h. + */ +const MCU_UART_tstrConfig MCU_UART_astrConfig[MCU_UART_NUM_INSTANCES] = +{ + [MCU_UART_INSTANCE_0] = + { + .u8TxPin = MCU_UART_0_TX_PIN, + .u8RxPin = MCU_UART_0_RX_PIN, + .u32BaudRate = MCU_UART_0_BAUD_RATE, + .enuDataBits = MCU_UART_0_DATA_BITS, + .enuStopBits = MCU_UART_0_STOP_BITS, + .enuParity = MCU_UART_0_PARITY, + .enuAsyncMode = MCU_UART_0_ASYNC_MODE, + .pfTxCompleteCallback = MCU_UART_0_TX_COMPLETE_CALLBACK, + }, +}; \ No newline at end of file diff --git a/src/MCU_UART/cfg/MCU_UART_cfg.h b/src/MCU_UART/cfg/MCU_UART_cfg.h index 5c222be..23d6ef6 100644 --- a/src/MCU_UART/cfg/MCU_UART_cfg.h +++ b/src/MCU_UART/cfg/MCU_UART_cfg.h @@ -2,10 +2,15 @@ * File: MCU_UART_cfg.h * Component: MCU_UART * Description: Configuration header for the MCU_UART driver. - * Declares configuration structures and constants that can be - * edited to adapt the UART driver to the specific hardware - * setup (e.g., which UART instance, pin assignments, baud rate, - * data bits, stop bits, parity). + * Defines which UART instances are active, their GPIO pin + * assignments, baud rates, data format, async TX mechanism, + * and TX-complete callback. Each instance is one entry in + * MCU_UART_astrConfig[]; the array index equals the RP2040 + * UART instance number (0 = uart0, 1 = uart1). + * + * To add a second UART: add MCU_UART_INSTANCE_1 to the enum, + * define MCU_UART_1_* macros, and add a [MCU_UART_INSTANCE_1] + * initializer in MCU_UART_cfg.c. * * Layer: MCU (hardware abstraction) - configuration *****************************************************************************/ @@ -13,6 +18,59 @@ #ifndef MCU_UART_CFG_H #define MCU_UART_CFG_H -/* Configuration constants and structure declarations will go here */ +#include "STD_TYPES.h" + +/* ------------------------------------------------------------------------ */ +/* INSTANCE ENUMERATION */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Enumeration of configured UART instances. + * + * Each enumerator's value matches the RP2040 UART peripheral index + * (0 = uart0, 1 = uart1). MCU_UART_NUM_INSTANCES is auto-calculated + * as the count and also used to size the config array. + */ +typedef enum +{ + MCU_UART_INSTANCE_0 = 0U, + MCU_UART_NUM_INSTANCES +} MCU_UART_tenuInstance; + +/* ------------------------------------------------------------------------ */ +/* INSTANCE 0 CONFIGURATION */ +/* ------------------------------------------------------------------------ */ + +/** @brief GPIO pin for UART0 TX (transmit). GP0 is the default for uart0. */ +#define MCU_UART_0_TX_PIN 0U + +/** @brief GPIO pin for UART0 RX (receive). GP1 is the default for uart0. */ +#define MCU_UART_0_RX_PIN 1U + +/** @brief Baud rate for UART0 in bits per second. 115200 is the most common + * default for serial monitors and USB-to-UART adapters. */ +#define MCU_UART_0_BAUD_RATE 115200U + +/** @brief Data bits per frame for UART0. 8 bits is the standard for binary + * and ASCII data transfer (8-N-1 is the universal default). */ +#define MCU_UART_0_DATA_BITS MCU_UART_DATA_BITS_8 + +/** @brief Stop bits per frame for UART0. 1 stop bit is the standard default. + * Use 2 only for slower receivers that need extra settling time. */ +#define MCU_UART_0_STOP_BITS MCU_UART_STOP_BITS_1 + +/** @brief Parity mode for UART0. No parity is the standard default (8-N-1). + * Enable parity only when the receiving device requires it. */ +#define MCU_UART_0_PARITY MCU_UART_PARITY_NONE + +/** @brief Async TX mechanism for UART0. + * MCU_UART_ASYNC_DMA: zero-CPU DMA transfer (best for large buffers). + * MCU_UART_ASYNC_ISR: interrupt-driven byte-by-byte (no DMA channel used). */ +#define MCU_UART_0_ASYNC_MODE MCU_UART_ASYNC_DMA + +/** @brief TX-complete callback for UART0. + * Called from interrupt context when an async or blocking SendBuffer + * finishes. Set to STD_NULL to disable (no notification). */ +#define MCU_UART_0_TX_COMPLETE_CALLBACK STD_NULL #endif /* MCU_UART_CFG_H */ \ No newline at end of file diff --git a/src/MCU_UART/inc/MCU_UART.h b/src/MCU_UART/inc/MCU_UART.h index 2cbef32..ff0e1b9 100644 --- a/src/MCU_UART/inc/MCU_UART.h +++ b/src/MCU_UART/inc/MCU_UART.h @@ -2,26 +2,165 @@ * File: MCU_UART.h * Component: MCU_UART * Description: Public interface for the MCU UART driver component. - * This header exposes the functions and types that other - * components are allowed to use when interacting with the UART - * peripheral on the RP2040 microcontroller. + * This header exposes the functions, types, and configuration + * value enumerations that other components are allowed to use + * when interacting with the hardware UART peripheral(s) on the + * RP2040 microcontroller. + * + * Two send-buffer modes are provided: + * - SendBuffer (default, non-blocking): starts an async + * transfer via DMA or ISR and returns immediately. + * - SendBufferBlocking: loops byte-by-byte and returns + * only after all data is transmitted. + * Both invoke the TX-complete callback (if configured). + * + * All public functions take a u8 instance parameter so the + * caller selects which UART peripheral to operate on. * * Layer: MCU (hardware abstraction) *****************************************************************************/ #ifndef MCU_UART_H #define MCU_UART_H + +/* STD_TYPES provides fixed-width typedefs (u8, u16, u32), STD_tenuResult, + * STD_tBool, STD_tpfCallbackFunc, and STD_NULL. */ #include "STD_TYPES.h" - typedef enum { - MCU_UART_OK = 0, - MCU_UART_ERROR, - MCU_UART_TIMEOUT - } MCU_UART_Status_t; +/* ------------------------------------------------------------------------ */ +/* CONFIGURATION VALUE TYPES */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Parity mode options for UART data framing. + */ +typedef enum +{ + MCU_UART_PARITY_NONE = 0U, /**< No parity bit transmitted */ + MCU_UART_PARITY_EVEN, /**< Even parity */ + MCU_UART_PARITY_ODD /**< Odd parity */ +} MCU_UART_tenuParity; + +/** + * @brief Number of data bits per UART frame. + */ +typedef enum +{ + MCU_UART_DATA_BITS_5 = 5U, + MCU_UART_DATA_BITS_6 = 6U, + MCU_UART_DATA_BITS_7 = 7U, + MCU_UART_DATA_BITS_8 = 8U +} MCU_UART_tenuDataBits; + +/** + * @brief Number of stop bits per UART frame. + */ +typedef enum +{ + MCU_UART_STOP_BITS_1 = 1U, + MCU_UART_STOP_BITS_2 = 2U +} MCU_UART_tenuStopBits; + +/** + * @brief Async transmit mechanism selection. + * + * DMA: hardware DMA channel moves bytes autonomously. Zero CPU during + * transfer. Best for large buffers. + * ISR: UART TX interrupt fires per byte. Uses CPU per-byte but does not + * consume a DMA channel. + */ +typedef enum +{ + MCU_UART_ASYNC_DMA = 0U, /**< Use DMA for non-blocking TX */ + MCU_UART_ASYNC_ISR /**< Use UART TX interrupt for non-blocking TX */ +} MCU_UART_tenuAsyncMode; + +/* ------------------------------------------------------------------------ */ +/* CONFIGURATION STRUCTURE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Per-instance UART configuration. + * + * One entry per hardware UART instance, stored in MCU_UART_astrConfig[]. + * The array index IS the UART instance number (0 = uart0, 1 = uart1). + */ +typedef struct +{ + u8 u8TxPin; /**< GPIO number for TX */ + u8 u8RxPin; /**< GPIO number for RX */ + u32 u32BaudRate; /**< Baud rate in bps */ + MCU_UART_tenuDataBits enuDataBits; /**< Data bits per frame */ + MCU_UART_tenuStopBits enuStopBits; /**< Stop bits per frame */ + MCU_UART_tenuParity enuParity; /**< Parity mode */ + MCU_UART_tenuAsyncMode enuAsyncMode; /**< DMA or ISR for non-blocking TX */ + STD_tpfCallbackFunc pfTxCompleteCallback; /**< Called when TX finishes. STD_NULL to ignore. */ +} MCU_UART_tstrConfig; + +/* ------------------------------------------------------------------------ */ +/* PUBLIC API */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Initialize all configured UART instances. + * + * Iterates MCU_UART_astrConfig[] and for each entry: sets baud rate, + * assigns GPIO pins, configures data format, claims DMA channel (if DMA + * mode), and sets up the appropriate IRQ handler. + * + * @return STD_OK on success, STD_NOK if any instance fails. + */ +STD_tenuResult MCU_UART_enuInit(void); + +/** + * @brief Send a single byte (blocking). + * + * @param u8Instance UART instance index (0 = uart0, 1 = uart1). + * @param u8Byte The byte to transmit. + * @return STD_OK on success, STD_NOK on failure. + */ +STD_tenuResult MCU_UART_enuSendByte(u8 u8Instance, u8 u8Byte); + +/** + * @brief Send a buffer of bytes (non-blocking, default). + * + * Starts an async transfer via DMA or UART TX ISR (per config). Returns + * immediately. The caller MUST keep the buffer valid until the callback + * fires or MCU_UART_bIsTxBusy() returns STD_FALSE. + * + * The TX-complete callback is invoked from interrupt context. + * + * @param u8Instance UART instance index. + * @param pu8Data Pointer to buffer. Must not be NULL. Must stay valid. + * @param u16Length Number of bytes to transmit. + * @return STD_OK transfer started, + * STD_NULL_POINTER_ERROR if pu8Data is NULL, + * STD_NOK if a transfer is already in progress. + */ +STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Length); + +/** + * @brief Send a buffer of bytes (blocking). + * + * Blocks until all bytes are transmitted. Calls the TX-complete callback + * synchronously at the end (if configured, not NULL). + * + * @param u8Instance UART instance index. + * @param pu8Data Pointer to buffer. Must not be NULL. + * @param u16Length Number of bytes to transmit. + * @return STD_OK on success, + * STD_NULL_POINTER_ERROR if pu8Data is NULL, + * STD_NOK on failure. + */ +STD_tenuResult MCU_UART_enuSendBufferBlocking(u8 u8Instance, const u8 *pu8Data, u16 u16Length); + +/** + * @brief Check if an async TX transfer is in progress. + * + * @param u8Instance UART instance index. + * @return STD_TRUE if a non-blocking SendBuffer is still transmitting, + * STD_FALSE if the driver is idle and ready for a new transfer. + */ +STD_tBool MCU_UART_bIsTxBusy(u8 u8Instance); -/* Public API declarations will go here */ - MCU_UART_Status_t MCU_UART_enuInit(void); - MCU_UART_Status_t MCU_UART_enuSendByte(u8 byte); - MCU_UART_Status_t MCU_UART_enuSendBuffer(const u8 *data, - u16 length); #endif /* MCU_UART_H */ \ No newline at end of file diff --git a/src/MCU_UART/prg/MCU_UART_prg.c b/src/MCU_UART/prg/MCU_UART_prg.c index bcaaa71..6cd2edc 100644 --- a/src/MCU_UART/prg/MCU_UART_prg.c +++ b/src/MCU_UART/prg/MCU_UART_prg.c @@ -2,10 +2,14 @@ * File: MCU_UART_prg.c * Component: MCU_UART * Description: Program (implementation) file for the MCU_UART driver. - * Contains the actual implementations of the public functions - * declared in MCU_UART.h. This is where we call the Pico SDK - * UART APIs to initialize the peripheral, transmit bytes, and - * receive data. + * Wraps the Pico SDK's hardware_uart and hardware_dma libraries + * to provide blocking and non-blocking send APIs for the RP2040 + * PL011 UART peripheral(s). + * + * Non-blocking TX supports two mechanisms (per-instance config): + * - DMA: hardware DMA channel transfers bytes autonomously + * - ISR: UART TX interrupt feeds one byte per interrupt + * Both invoke the configured TX-complete callback when done. * * Layer: MCU (hardware abstraction) *****************************************************************************/ @@ -14,4 +18,370 @@ #include "MCU_UART_priv.h" #include "MCU_UART_cfg.h" -/* Function implementations will go here */ \ No newline at end of file +/* Pico SDK headers */ +#include "hardware/uart.h" +#include "hardware/gpio.h" +#include "hardware/dma.h" +#include "hardware/irq.h" + +/* ------------------------------------------------------------------------ */ +/* INSTANCE LOOKUP TABLE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Maps a config array index to the corresponding Pico SDK uart + * instance pointer (0 = uart0, 1 = uart1). + */ +static uart_inst_t * const apstrInstances[] = +{ + uart0, /* index 0 = RP2040 uart0 peripheral */ + uart1, /* index 1 = RP2040 uart1 peripheral */ +}; + +/* ------------------------------------------------------------------------ */ +/* RUNTIME STATE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Single static control structure holding all per-instance TX state. + * + * NOTE: abTxBusy is written from interrupt context (DMA/UART ISR) and + * read from main context (MCU_UART_bIsTxBusy, MCU_UART_enuSendBuffer). + * On the single-core RP2040 with interrupts disabled during read-modify- + * write sequences, this is safe. If dual-core access is ever needed, + * add volatile qualifiers or use a spin lock. + */ +static MCU_UART_tstrControl strControl; + +/* ------------------------------------------------------------------------ */ +/* INTERNAL HELPERS */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Call the TX-complete callback for a given instance if configured. + * Safe to invoke from ISR context. + * + * @param u8Instance Index into MCU_UART_astrConfig[]. + */ +static void vCallTxCallback(u8 u8Instance) +{ + STD_tpfCallbackFunc pfCallbackLoc = MCU_UART_astrConfig[u8Instance].pfTxCompleteCallback; + + if (pfCallbackLoc != STD_NULL) + { + pfCallbackLoc(); + } +} + +/* ------------------------------------------------------------------------ */ +/* DMA IRQ HANDLER (INSTANCE 0) */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief DMA completion interrupt handler for UART instance 0. + * + * Fires when the DMA channel finishes transferring the TX buffer. + * Flow: clear IRQ flag -> set abTxBusy to STD_FALSE -> call callback. + */ +static void vDmaIrqHandler0(void) +{ + s8 s8ChannelLoc = strControl.as8DmaChannel[MCU_UART_INSTANCE_0]; + u32 u32StatusLoc; + + u32StatusLoc = dma_channel_get_irq0_status((u32)s8ChannelLoc); + + if (u32StatusLoc != 0U) + { + /* Step 1: clear the DMA interrupt flag */ + dma_irqn_acknowledge_channel(0, (u32)s8ChannelLoc); + + /* Step 2: mark instance as idle (written here in ISR, read in main) */ + strControl.abTxBusy[MCU_UART_INSTANCE_0] = STD_FALSE; + + /* Step 3: notify the application */ + vCallTxCallback((u8)MCU_UART_INSTANCE_0); + } +} + +/* Note: when UART1 DMA is added, define vDmaIrqHandler1 here and + * use DMA_IRQ_1 to avoid sharing a single IRQ. */ + +/* ------------------------------------------------------------------------ */ +/* UART TX ISR HANDLER */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief UART TX interrupt handler logic — fills the FIFO on each invocation. + * + * The PL011 TX interrupt fires when the FIFO level drops at or below the + * programmable threshold. This handler writes as many bytes as the FIFO + * can accept before returning, minimizing the number of interrupts needed + * for a given transfer (one interrupt per FIFO drain cycle, not per byte). + * + * When all bytes are sent, disables the TX interrupt, marks the instance + * as idle, and invokes the TX-complete callback. + * + * @param u8Instance Index of the UART instance that fired the interrupt. + */ +static void vTxIsrHandler(u8 u8Instance) +{ + uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; + STD_tBool bFifoReady = STD_FALSE; + STD_tBool bDataLeft = STD_FALSE; + + /* Fill the FIFO with as many bytes as it can accept */ + bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; + + while ((bFifoReady == STD_TRUE) && (bDataLeft == STD_TRUE)) + { + uart_putc_raw(pstrUartLoc, + strControl.apu8TxBuffer[u8Instance][strControl.au16TxIndex[u8Instance]]); + strControl.au16TxIndex[u8Instance]++; + + bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; + } + + /* If all bytes have been sent, shut down the interrupt */ + if (bDataLeft == STD_FALSE) + { + uart_set_irqs_enabled(pstrUartLoc, false, false); + + strControl.abTxBusy[u8Instance] = STD_FALSE; + + vCallTxCallback(u8Instance); + } +} + +/** + * @brief UART0 TX interrupt entry point. + * Dispatches to the common handler with instance 0. + */ +static void vUart0IrqHandler(void) +{ + vTxIsrHandler((u8)MCU_UART_INSTANCE_0); +} + +/* Note: when UART1 ISR mode is added, define vUart1IrqHandler here. */ + +/* ========================================================================= */ +/* INIT */ +/* ========================================================================= */ + +STD_tenuResult MCU_UART_enuInit(void) +{ + STD_tenuResult enuResultLoc = STD_OK; + u8 u8IndexLoc; + + /* Zero-initialize all control state */ + for (u8IndexLoc = 0U; u8IndexLoc < (u8)MCU_UART_NUM_INSTANCES; u8IndexLoc++) + { + strControl.apu8TxBuffer[u8IndexLoc] = STD_NULL; + strControl.au16TxLength[u8IndexLoc] = 0U; + strControl.au16TxIndex[u8IndexLoc] = 0U; + strControl.abTxBusy[u8IndexLoc] = STD_FALSE; + strControl.as8DmaChannel[u8IndexLoc] = -1; + } + + /* Configure each UART instance */ + for (u8IndexLoc = 0U; u8IndexLoc < (u8)MCU_UART_NUM_INSTANCES; u8IndexLoc++) + { + const MCU_UART_tstrConfig *pstrCfgLoc = &MCU_UART_astrConfig[u8IndexLoc]; + uart_inst_t *pstrUartLoc = apstrInstances[u8IndexLoc]; + + /* Enable the UART peripheral and set the baud rate */ + uart_init(pstrUartLoc, pstrCfgLoc->u32BaudRate); + + /* Assign TX and RX GPIO pins to the UART function */ + gpio_set_function(pstrCfgLoc->u8TxPin, GPIO_FUNC_UART); + gpio_set_function(pstrCfgLoc->u8RxPin, GPIO_FUNC_UART); + + /* Set data format: data bits, stop bits, parity */ + uart_set_format(pstrUartLoc, + pstrCfgLoc->enuDataBits, + pstrCfgLoc->enuStopBits, + pstrCfgLoc->enuParity); + + /* Set up the async TX mechanism based on config */ + if (pstrCfgLoc->enuAsyncMode == MCU_UART_ASYNC_DMA) + { + /* Claim a free DMA channel. true = panic if none available. */ + s8 s8ChannelLoc = (s8)dma_claim_unused_channel(true); + strControl.as8DmaChannel[u8IndexLoc] = s8ChannelLoc; + + /* Enable DMA completion interrupt for this channel */ + dma_channel_set_irq0_enabled((u32)s8ChannelLoc, true); + + /* Install the per-instance DMA IRQ handler */ + if (u8IndexLoc == (u8)MCU_UART_INSTANCE_0) + { + irq_set_exclusive_handler(DMA_IRQ_0, vDmaIrqHandler0); + irq_set_enabled(DMA_IRQ_0, true); + } + } + else + { + /* ISR mode: install the per-instance UART TX interrupt handler. + * The TX interrupt itself is NOT enabled here — it gets enabled + * in SendBuffer and disabled by the ISR when transfer completes. */ + if (u8IndexLoc == (u8)MCU_UART_INSTANCE_0) + { + irq_set_exclusive_handler(UART0_IRQ, vUart0IrqHandler); + irq_set_enabled(UART0_IRQ, true); + } + } + } + + return enuResultLoc; +} + +/* ========================================================================= */ +/* SEND BYTE (BLOCKING) */ +/* ========================================================================= */ + +STD_tenuResult MCU_UART_enuSendByte(u8 u8Instance, u8 u8Byte) +{ + STD_tenuResult enuResultLoc = STD_OK; + + /* Blocking single-byte send. uart_putc_raw waits until the TX FIFO + * has space, then writes the byte. */ + uart_putc_raw(apstrInstances[u8Instance], u8Byte); + + return enuResultLoc; +} + +/* ========================================================================= */ +/* SEND BUFFER (NON-BLOCKING, DEFAULT) */ +/* ========================================================================= */ + +STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Length) +{ + STD_tenuResult enuResultLoc = STD_OK; + STD_tBool bBusyLoc = strControl.abTxBusy[u8Instance]; + MCU_UART_tenuAsyncMode enuModeLoc = MCU_UART_astrConfig[u8Instance].enuAsyncMode; + + if (pu8Data == STD_NULL) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else if (bBusyLoc == STD_TRUE) + { + /* A previous async transfer is still in progress */ + enuResultLoc = STD_NOK; + } + else + { + /* Store buffer info in the control structure. + * abTxBusy is set here (main context) and cleared by the ISR/DMA + * handler (interrupt context) when the transfer completes. */ + strControl.apu8TxBuffer[u8Instance] = pu8Data; + strControl.au16TxLength[u8Instance] = u16Length; + strControl.au16TxIndex[u8Instance] = 0U; + strControl.abTxBusy[u8Instance] = STD_TRUE; + + if (enuModeLoc == MCU_UART_ASYNC_DMA) + { + /* --- DMA mode --- */ + s8 s8ChannelLoc = strControl.as8DmaChannel[u8Instance]; + uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; + + /* Configure DMA: memory -> UART TX FIFO, 1 byte at a time, + * paced by UART TX DREQ so we never overflow the FIFO. */ + dma_channel_config strDmaCfgLoc = dma_channel_get_default_config((u32)s8ChannelLoc); + channel_config_set_transfer_data_size(&strDmaCfgLoc, DMA_SIZE_8); + channel_config_set_read_increment(&strDmaCfgLoc, true); + channel_config_set_write_increment(&strDmaCfgLoc, false); + channel_config_set_dreq(&strDmaCfgLoc, uart_get_dreq(pstrUartLoc, true)); + + /* Start transfer. DMA IRQ fires on completion -> + * vDmaIrqHandler0 clears abTxBusy -> calls callback. */ + dma_channel_configure( + (u32)s8ChannelLoc, + &strDmaCfgLoc, + &uart_get_hw(pstrUartLoc)->dr, /* dest: UART data register */ + pu8Data, /* source: caller's buffer */ + u16Length, /* transfer count */ + true /* start immediately */ + ); + } + else + { + /* --- ISR mode --- */ + uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; + STD_tBool bFifoReady = STD_FALSE; + STD_tBool bDataLeft = STD_FALSE; + + /* Kick-start: fill the FIFO with as many bytes as it can + * accept right now. This gets the first bytes transmitting + * immediately without waiting for an interrupt context switch. + * Identical logic to what the ISR does on subsequent fills. */ + bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE; + + while ((bFifoReady == STD_TRUE) && (bDataLeft == STD_TRUE)) + { + uart_putc_raw(pstrUartLoc, + pu8Data[strControl.au16TxIndex[u8Instance]]); + strControl.au16TxIndex[u8Instance]++; + + bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE; + } + + /* If all bytes fit into the FIFO in one go, the transfer is + * already complete — no interrupt needed. */ + if (bDataLeft == STD_FALSE) + { + strControl.abTxBusy[u8Instance] = STD_FALSE; + vCallTxCallback(u8Instance); + } + else + { + /* Bytes remain — enable the TX interrupt. The ISR fills + * the FIFO each time it drains below threshold until the + * entire buffer is transmitted. */ + uart_set_irqs_enabled(pstrUartLoc, true, false); + } + } + } + + return enuResultLoc; +} + +/* ========================================================================= */ +/* SEND BUFFER (BLOCKING) */ +/* ========================================================================= */ + +STD_tenuResult MCU_UART_enuSendBufferBlocking(u8 u8Instance, const u8 *pu8Data, u16 u16Length) +{ + STD_tenuResult enuResultLoc = STD_OK; + u16 u16IndexLoc; + + if (pu8Data == STD_NULL) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else + { + /* Send each byte blocking — waits for TX FIFO space per byte */ + for (u16IndexLoc = 0U; u16IndexLoc < u16Length; u16IndexLoc++) + { + uart_putc_raw(apstrInstances[u8Instance], pu8Data[u16IndexLoc]); + } + + /* Invoke callback synchronously (no ISR in blocking mode) */ + vCallTxCallback(u8Instance); + } + + return enuResultLoc; +} + +/* ========================================================================= */ +/* TX BUSY CHECK */ +/* ========================================================================= */ + +STD_tBool MCU_UART_bIsTxBusy(u8 u8Instance) +{ + return strControl.abTxBusy[u8Instance]; +} diff --git a/src/MCU_UART/prg/MCU_UART_priv.h b/src/MCU_UART/prg/MCU_UART_priv.h index 9bcfb44..35c3620 100644 --- a/src/MCU_UART/prg/MCU_UART_priv.h +++ b/src/MCU_UART/prg/MCU_UART_priv.h @@ -2,10 +2,10 @@ * File: MCU_UART_priv.h * Component: MCU_UART * Description: Private header for the MCU_UART driver. - * Contains internal macros, helper declarations, and - * register-level definitions that are only used inside the - * component itself. Nothing declared here is exposed to - * external components. + * Contains the internal control structure used to track + * per-instance async TX state, the extern declaration of + * the config array (which is only accessed internally by + * _prg.c), and any other helpers private to the component. * * Layer: MCU (hardware abstraction) - internal use only *****************************************************************************/ @@ -13,6 +13,53 @@ #ifndef MCU_UART_PRIV_H #define MCU_UART_PRIV_H -/* Private declarations, internal macros and helpers will go here */ +#include "MCU_UART.h" +#include "MCU_UART_cfg.h" + +/* ------------------------------------------------------------------------ */ +/* CONFIG ARRAY (EXTERN) */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Configuration array indexed by MCU_UART_tenuInstance. + * + * Defined in MCU_UART_cfg.c. Each entry holds the full configuration for + * one UART instance. The driver iterates this array during Init. + * Declared here (not in _cfg.h or .h) because only _prg.c needs to + * access it — no external component should read the raw config array. + */ +extern const MCU_UART_tstrConfig MCU_UART_astrConfig[MCU_UART_NUM_INSTANCES]; + +/* ------------------------------------------------------------------------ */ +/* RUNTIME CONTROL STRUCTURE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Internal runtime state for all UART instances. + * + * Each field is an array indexed by instance number. This struct-of-arrays + * layout keeps all per-instance state in a single static object inside + * MCU_UART_prg.c, making it easy to find, zero-initialize, and inspect + * in a debugger. + * + * Fields: + * apu8TxBuffer — pointer to the caller's buffer being transmitted + * au16TxLength — total number of bytes to transmit + * au16TxIndex — number of bytes transmitted so far (ISR mode only; + * DMA mode does not use this - the DMA controller + * tracks progress internally) + * abTxBusy — STD_TRUE while an async transfer is in progress + * as8DmaChannel — DMA channel number claimed during Init for each + * instance configured in DMA mode. Set to -1 for + * instances using ISR mode (no DMA channel needed). + */ +typedef struct +{ + const u8 *apu8TxBuffer[MCU_UART_NUM_INSTANCES]; + u16 au16TxLength[MCU_UART_NUM_INSTANCES]; + u16 au16TxIndex[MCU_UART_NUM_INSTANCES]; + STD_tBool abTxBusy[MCU_UART_NUM_INSTANCES]; + s8 as8DmaChannel[MCU_UART_NUM_INSTANCES]; +} MCU_UART_tstrControl; #endif /* MCU_UART_PRIV_H */ \ No newline at end of file From 77152a0718d71232e30f9f7ee0c489db906ecd56 Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Sun, 12 Apr 2026 23:26:21 +0200 Subject: [PATCH 3/6] Implement HAL_COM with function-pointer transport dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-channel communication abstraction where each channel is wired to an MCU driver via function pointers in the config array. Adding a new transport (SPI, I2C, etc.) requires only a config entry — zero changes to HAL_COM_prg.c. Channel 0 defaults to USB-CDC via thin wrappers that normalize MCU_USB's signature. HAL_COM.h includes HAL_COM_cfg.h so callers can reference channel names. APP_CLSW updated to pass channel parameter. --- src/APP_CLSW/prg/APP_CLSW_prg.c | 2 +- src/HAL_COM/cfg/HAL_COM_cfg.c | 78 +++++++++++++++++++++-- src/HAL_COM/cfg/HAL_COM_cfg.h | 31 +++++++-- src/HAL_COM/inc/HAL_COM.h | 108 +++++++++++++++++++++++--------- src/HAL_COM/prg/HAL_COM_prg.c | 53 +++++++++------- src/HAL_COM/prg/HAL_COM_priv.h | 25 ++++++-- 6 files changed, 233 insertions(+), 64 deletions(-) diff --git a/src/APP_CLSW/prg/APP_CLSW_prg.c b/src/APP_CLSW/prg/APP_CLSW_prg.c index 739e50b..a6ab98d 100644 --- a/src/APP_CLSW/prg/APP_CLSW_prg.c +++ b/src/APP_CLSW/prg/APP_CLSW_prg.c @@ -36,5 +36,5 @@ void APP_CLSW_vRunnable(void) * transport-agnostic HAL_COM layer. This will be replaced with real * color-switching command logic once the communication stack is * verified end-to-end. */ - HAL_COM_enuSendBuffer((const u8 *)"this is a color switcher application!\r\n", 40U); + HAL_COM_enuSendBuffer((u8)HAL_COM_CHANNEL_0, (const u8 *)"this is a color switcher application!\r\n", 40U); } \ No newline at end of file diff --git a/src/HAL_COM/cfg/HAL_COM_cfg.c b/src/HAL_COM/cfg/HAL_COM_cfg.c index d5b3295..e4799cb 100644 --- a/src/HAL_COM/cfg/HAL_COM_cfg.c +++ b/src/HAL_COM/cfg/HAL_COM_cfg.c @@ -2,13 +2,83 @@ * File: HAL_COM_cfg.c * Component: HAL_COM * Description: Configuration implementation for the HAL_COM abstraction. - * Holds the actual configuration values (transport selection - * tables, timeout constants, buffer sizes) consumed by - * HAL_COM_prg.c. + * Defines the channel config array that wires each HAL_COM + * channel to a specific MCU-level transport driver via function + * pointers. Also contains thin wrapper functions to normalize + * driver signatures that don't match the generic prototype + * (e.g., MCU_USB which has no instance parameter). * * Layer: HAL - configuration *****************************************************************************/ +#include "HAL_COM.h" #include "HAL_COM_cfg.h" -/* Configuration definitions will go here */ +/* MCU drivers that channels may be wired to */ +#include "MCU_USB.h" +#include "MCU_UART.h" + +/* ------------------------------------------------------------------------ */ +/* SIGNATURE NORMALIZATION WRAPPERS */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Wrapper for MCU_USB_enuSendByte to match HAL_COM_tpfSendByte. + * + * MCU_USB has only one instance (the RP2040's single USB peripheral), + * so its public API does not take a u8Instance parameter. This wrapper + * adds the parameter and ignores it, making the signature compatible + * with HAL_COM's generic function pointer type. + */ +static STD_tenuResult vUsbSendByte(u8 u8Instance, u8 u8Byte) +{ + (void)u8Instance; + return MCU_USB_enuSendByte(u8Byte); +} + +/** + * @brief Wrapper for MCU_USB_enuSendBuffer to match HAL_COM_tpfSendBuffer. + * + * Same rationale as vUsbSendByte — normalizes the missing instance + * parameter so USB can be plugged into a HAL_COM channel. + */ +static STD_tenuResult vUsbSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Length) +{ + (void)u8Instance; + return MCU_USB_enuSendBuffer(pu8Data, u16Length); +} + +/* MCU_UART already matches the HAL_COM function pointer signatures + * (u8 u8Instance as the first parameter), so no wrapper is needed. + * Its functions can be assigned directly to the config struct. */ + +/* ------------------------------------------------------------------------ */ +/* CHANNEL CONFIGURATION ARRAY */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Per-channel config, indexed by HAL_COM_tenuChannel. + * + * [HAL_COM_CHANNEL_0] = USB-CDC (primary communication path). + * + * To add a UART channel: + * 1. Add HAL_COM_CHANNEL_1 to the enum in HAL_COM_cfg.h + * 2. Add an entry here: + * [HAL_COM_CHANNEL_1] = { + * .pfSendByte = MCU_UART_enuSendByte, + * .pfSendBuffer = MCU_UART_enuSendBuffer, + * .u8Instance = 0U, + * }, + * + * To add SPI, I2C, or any other transport: create a matching MCU driver + * with the same function signature, or add a wrapper here like vUsbSendByte. + */ +const HAL_COM_tstrChannelConfig HAL_COM_astrChannelConfig[HAL_COM_NUM_CHANNELS] = +{ + [HAL_COM_CHANNEL_0] = + { + .pfSendByte = vUsbSendByte, + .pfSendBuffer = vUsbSendBuffer, + .u8Instance = 0U, + }, +}; diff --git a/src/HAL_COM/cfg/HAL_COM_cfg.h b/src/HAL_COM/cfg/HAL_COM_cfg.h index 28f1aa7..8af7c77 100644 --- a/src/HAL_COM/cfg/HAL_COM_cfg.h +++ b/src/HAL_COM/cfg/HAL_COM_cfg.h @@ -2,10 +2,10 @@ * File: HAL_COM_cfg.h * Component: HAL_COM * Description: Configuration header for the HAL_COM abstraction. - * Selects which physical transport(s) HAL_COM should route - * data through: MCU_UART, MCU_USB, or both (mirrored output). - * Also exposes any timeout / buffer sizing parameters used by - * the dispatch logic. + * Defines the communication channels and which MCU-level + * transport each one routes through via function pointers. + * Adding a new channel or swapping a transport is a config-only + * change — no code in HAL_COM_prg.c needs to be modified. * * Layer: HAL - configuration *****************************************************************************/ @@ -13,6 +13,27 @@ #ifndef HAL_COM_CFG_H #define HAL_COM_CFG_H -/* Configuration constants and transport selection macros will go here */ +#include "STD_TYPES.h" + +/* ------------------------------------------------------------------------ */ +/* CHANNEL ENUMERATION */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Enumeration of configured HAL_COM channels. + * + * Each channel is one logical communication path wired to a specific + * MCU-level driver. The enumerator values are used as array indices + * into HAL_COM_astrChannelConfig[]. + * + * To add a channel: add an enumerator before HAL_COM_NUM_CHANNELS, + * define its function pointer macros below, and add an entry in + * HAL_COM_cfg.c. + */ +typedef enum +{ + HAL_COM_CHANNEL_0 = 0U, /**< Primary channel (USB-CDC by default) */ + HAL_COM_NUM_CHANNELS +} HAL_COM_tenuChannel; #endif /* HAL_COM_CFG_H */ diff --git a/src/HAL_COM/inc/HAL_COM.h b/src/HAL_COM/inc/HAL_COM.h index cc5fc0c..c28b032 100644 --- a/src/HAL_COM/inc/HAL_COM.h +++ b/src/HAL_COM/inc/HAL_COM.h @@ -2,14 +2,16 @@ * File: HAL_COM.h * Component: HAL_COM * Description: Public interface for the HAL communication abstraction layer. - * Provides a transport-agnostic API for sending and receiving - * bytes to/from a host computer. Underneath, HAL_COM dispatches - * to one or more MCU-level drivers (MCU_UART for the hardware - * UART peripheral, MCU_USB for USB-CDC virtual serial) based on - * the configuration in HAL_COM_cfg.h. + * Provides a transport-agnostic, multi-channel API for sending + * and receiving bytes. Each channel is independently configured + * with function pointers to an MCU-level driver (USB, UART, + * SPI, I2C, or any future transport that matches the function + * signatures). * - * Higher layers (e.g. APP_CLSW) should call only HAL_COM_* and - * stay unaware of which physical transport is in use. + * Higher layers (e.g. APP_CLSW) call HAL_COM_* with a channel + * number and stay unaware of which physical transport is in use. + * Adding a new transport requires zero changes to this file or + * to HAL_COM_prg.c — only the config needs a new channel entry. * * Layer: HAL (hardware abstraction, one level above MCU drivers) *****************************************************************************/ @@ -17,9 +19,61 @@ #ifndef HAL_COM_H #define HAL_COM_H -/* STD_TYPES provides fixed-width typedefs (u8, u32) and the STD_tenuResult - * enum used to report success/failure from every function. */ #include "STD_TYPES.h" +#include "HAL_COM_cfg.h" + +/* ------------------------------------------------------------------------ */ +/* TRANSPORT FUNCTION POINTER TYPES */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Generic send-byte function pointer type. + * + * Any MCU driver that can send a single byte and matches this signature + * can be plugged into a HAL_COM channel. The u8Instance parameter + * identifies the peripheral instance within that driver (e.g., UART0 + * vs UART1). Drivers that have only one instance (e.g., MCU_USB) use + * a thin wrapper that ignores the parameter. + * + * @param u8Instance Peripheral instance index within the driver. + * @param u8Byte The byte to transmit. + * @return STD_tenuResult + */ +typedef STD_tenuResult (*HAL_COM_tpfSendByte)(u8 u8Instance, u8 u8Byte); + +/** + * @brief Generic send-buffer function pointer type. + * + * Same concept as HAL_COM_tpfSendByte but for multi-byte transfers. + * + * @param u8Instance Peripheral instance index within the driver. + * @param pu8Data Pointer to the byte buffer. + * @param u16Length Number of bytes to transmit. + * @return STD_tenuResult + */ +typedef STD_tenuResult (*HAL_COM_tpfSendBuffer)(u8 u8Instance, const u8 *pu8Data, u16 u16Length); + +/* ------------------------------------------------------------------------ */ +/* CONFIGURATION STRUCTURE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Per-channel communication configuration. + * + * One entry per HAL_COM channel, stored in HAL_COM_astrChannelConfig[]. + * The array index is the channel number passed to all public functions. + * + * Each channel is wired to exactly one MCU-level transport by holding + * function pointers to that driver's send functions + the instance index + * to pass through. To route through a different transport, just change + * the function pointers in the config — no code changes needed. + */ +typedef struct +{ + HAL_COM_tpfSendByte pfSendByte; /**< Driver's send-byte function */ + HAL_COM_tpfSendBuffer pfSendBuffer; /**< Driver's send-buffer function */ + u8 u8Instance; /**< Peripheral instance to pass through */ +} HAL_COM_tstrChannelConfig; /* ------------------------------------------------------------------------ */ /* PUBLIC API */ @@ -28,39 +82,37 @@ /** * @brief Initialize the HAL communication layer's own internal state. * - * Does NOT initialize the underlying MCU drivers (MCU_USB, MCU_UART) - - * SYS_ECU owns the init sequence and calls each driver's enuInit() - * separately in the correct dependency order before calling this. + * Does NOT initialize the underlying MCU drivers — SYS_ECU owns the init + * sequence and calls each driver's enuInit() before calling this. * - * @return STD_OK on success, - * STD_NOK on failure. + * @return STD_OK on success, STD_NOK on failure. */ STD_tenuResult HAL_COM_enuInit(void); /** - * @brief Send a single byte through the active transport. + * @brief Send a single byte through the specified channel. * - * Dispatches to MCU_USB_enuSendByte or MCU_UART_enuSendByte (or both) - * depending on the transport selection configured in HAL_COM_cfg.h. - * - * @param u8Byte The byte to transmit. - * @return STD_OK on success, - * STD_NOK on transmit failure. + * @param u8Channel Channel index (from HAL_COM_cfg.h channel enum). + * @param u8Byte The byte to transmit. + * @return STD_OK on success, STD_NOK on failure. */ -STD_tenuResult HAL_COM_enuSendByte(u8 u8Byte); +STD_tenuResult HAL_COM_enuSendByte(u8 u8Channel, u8 u8Byte); /** - * @brief Send a buffer of bytes through the active transport. + * @brief Send a buffer through the specified channel. * - * Dispatches to MCU_USB_enuSendBuffer or MCU_UART_enuSendBuffer (or both) - * depending on the transport selection configured in HAL_COM_cfg.h. + * The underlying driver may be blocking or non-blocking depending on the + * MCU driver wired to this channel. When non-blocking (e.g., MCU_UART + * with DMA), the caller must keep the buffer valid until the transfer + * completes. * - * @param pu8Data Pointer to the byte buffer to transmit. Must not be NULL. + * @param u8Channel Channel index. + * @param pu8Data Pointer to the byte buffer. Must not be NULL. * @param u16Length Number of bytes to transmit. * @return STD_OK on success, * STD_NULL_POINTER_ERROR if pu8Data is NULL, - * STD_NOK on transmit failure. + * STD_NOK on failure. */ -STD_tenuResult HAL_COM_enuSendBuffer(const u8 *pu8Data, u16 u16Length); +STD_tenuResult HAL_COM_enuSendBuffer(u8 u8Channel, const u8 *pu8Data, u16 u16Length); #endif /* HAL_COM_H */ \ No newline at end of file diff --git a/src/HAL_COM/prg/HAL_COM_prg.c b/src/HAL_COM/prg/HAL_COM_prg.c index f0c2fd1..53b0dfc 100644 --- a/src/HAL_COM/prg/HAL_COM_prg.c +++ b/src/HAL_COM/prg/HAL_COM_prg.c @@ -2,13 +2,12 @@ * File: HAL_COM_prg.c * Component: HAL_COM * Description: Program (implementation) file for the HAL_COM abstraction. - * Implements the public functions declared in HAL_COM.h by - * dispatching to the MCU-level transport drivers (MCU_UART - * and/or MCU_USB) according to the active configuration. + * Dispatches send operations to the MCU-level driver wired to + * each channel via function pointers in HAL_COM_astrChannelConfig. * - * Currently hardwired to MCU_USB. When MCU_UART is implemented, - * the dispatch will be driven by a transport-selection macro - * in HAL_COM_cfg.h. + * The dispatch is a single indirect call — no if/else chains, + * no transport-specific code. Adding a new transport (SPI, I2C, + * etc.) requires zero changes here — only a new config entry. * * Layer: HAL *****************************************************************************/ @@ -17,40 +16,52 @@ #include "HAL_COM_priv.h" #include "HAL_COM_cfg.h" -/* MCU_USB is the active transport for now. When MCU_UART is ready, - * a config macro will select which driver(s) to call here. */ -#include "MCU_USB.h" +/* ========================================================================= */ +/* INIT */ +/* ========================================================================= */ STD_tenuResult HAL_COM_enuInit(void) { STD_tenuResult enuResultLoc = STD_OK; - /* HAL_COM has no internal state to set up yet. When transport - * selection logic or internal buffers are added, initialize them here. - * The underlying MCU drivers are already initialized by SYS_ECU - * before this function is called. */ + /* HAL_COM has no internal state to set up yet. The underlying MCU + * drivers are already initialized by SYS_ECU before this function + * is called. When internal queuing or channel-level state is added, + * initialize it here. */ return enuResultLoc; } -STD_tenuResult HAL_COM_enuSendByte(u8 u8Byte) +/* ========================================================================= */ +/* SEND BYTE */ +/* ========================================================================= */ + +STD_tenuResult HAL_COM_enuSendByte(u8 u8Channel, u8 u8Byte) { STD_tenuResult enuResultLoc = STD_OK; + const HAL_COM_tstrChannelConfig *pstrCfgLoc = &HAL_COM_astrChannelConfig[u8Channel]; - /* Dispatch to the active transport driver */ - enuResultLoc = MCU_USB_enuSendByte(u8Byte); + /* Dispatch through the function pointer configured for this channel. + * The driver's instance index is passed through transparently — the + * caller never sees it. */ + enuResultLoc = pstrCfgLoc->pfSendByte(pstrCfgLoc->u8Instance, u8Byte); return enuResultLoc; } -STD_tenuResult HAL_COM_enuSendBuffer(const u8 *pu8Data, u16 u16Length) +/* ========================================================================= */ +/* SEND BUFFER */ +/* ========================================================================= */ + +STD_tenuResult HAL_COM_enuSendBuffer(u8 u8Channel, const u8 *pu8Data, u16 u16Length) { STD_tenuResult enuResultLoc = STD_OK; + const HAL_COM_tstrChannelConfig *pstrCfgLoc = &HAL_COM_astrChannelConfig[u8Channel]; - /* Dispatch to the active transport driver. - * Null-pointer validation is handled inside MCU_USB_enuSendBuffer - * so we don't duplicate the check here. */ - enuResultLoc = MCU_USB_enuSendBuffer(pu8Data, u16Length); + /* Dispatch through the function pointer configured for this channel. + * Null-pointer validation is handled inside the MCU driver, so we + * don't duplicate the check here. */ + enuResultLoc = pstrCfgLoc->pfSendBuffer(pstrCfgLoc->u8Instance, pu8Data, u16Length); return enuResultLoc; } diff --git a/src/HAL_COM/prg/HAL_COM_priv.h b/src/HAL_COM/prg/HAL_COM_priv.h index e1a69a8..f6f4fce 100644 --- a/src/HAL_COM/prg/HAL_COM_priv.h +++ b/src/HAL_COM/prg/HAL_COM_priv.h @@ -2,9 +2,9 @@ * File: HAL_COM_priv.h * Component: HAL_COM * Description: Private header for the HAL_COM abstraction. - * Contains internal macros, helper declarations, and any - * state/definitions that are only used inside the component. - * Nothing declared here is exposed to external components. + * Contains the extern declaration of the channel config array + * (only accessed internally by _prg.c) and any other private + * definitions. * * Layer: HAL - internal use only *****************************************************************************/ @@ -12,6 +12,21 @@ #ifndef HAL_COM_PRIV_H #define HAL_COM_PRIV_H -/* Private declarations, internal macros and helpers will go here */ +#include "HAL_COM.h" +#include "HAL_COM_cfg.h" -#endif /* HAL_COM_PRIV_H */ +/* ------------------------------------------------------------------------ */ +/* CONFIG ARRAY (EXTERN) */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Channel configuration array indexed by HAL_COM_tenuChannel. + * + * Defined in HAL_COM_cfg.c. Each entry holds function pointers to an + * MCU-level driver's send functions + the instance index to pass through. + * Only HAL_COM_prg.c accesses this — no external component should read + * the raw config. + */ +extern const HAL_COM_tstrChannelConfig HAL_COM_astrChannelConfig[HAL_COM_NUM_CHANNELS]; + +#endif /* HAL_COM_PRIV_H */ \ No newline at end of file From 863da8f75c03c7a489c0832259032c0a1edea517 Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Mon, 13 Apr 2026 01:29:20 +0200 Subject: [PATCH 4/6] Add RX support and interactive color-switcher command demo MCU_UART: full RX with ISR and DMA modes, ring buffer, blocking ReceiveByte, async ReceiveBuffer with request fulfillment, blocking ReceiveBufferBlocking. Per-byte RX callback (ISR mode). MCU_USB: blocking ReceiveByte via getchar_timeout_us, cached-byte peek for bIsRxDataAvailable. HAL_COM: RX function pointer types + dispatch (ReceiveByte, bIsRxDataAvailable) with USB wrappers in cfg. APP_CLSW: interactive command parser. Accumulates input into a buffer, parses on delimiter. Supports red/green/blue/help commands with echo and response. SYS_ECU tick reduced to 10ms for responsive input. --- src/APP_CLSW/cfg/APP_CLSW_cfg.h | 16 +- src/APP_CLSW/prg/APP_CLSW_prg.c | 169 +++++++++-- src/APP_CLSW/prg/APP_CLSW_priv.h | 27 +- src/HAL_COM/cfg/HAL_COM_cfg.c | 45 ++- src/HAL_COM/inc/HAL_COM.h | 44 ++- src/HAL_COM/prg/HAL_COM_prg.c | 25 ++ src/MCU_UART/cfg/MCU_UART_cfg.c | 4 +- src/MCU_UART/cfg/MCU_UART_cfg.h | 71 ++--- src/MCU_UART/inc/MCU_UART.h | 71 ++++- src/MCU_UART/prg/MCU_UART_prg.c | 466 ++++++++++++++++++++++--------- src/MCU_UART/prg/MCU_UART_priv.h | 59 ++-- src/MCU_USB/inc/MCU_USB.h | 19 ++ src/MCU_USB/prg/MCU_USB_prg.c | 80 ++++++ src/SYS_ECU/prg/SYS_ECU.c | 7 +- 14 files changed, 861 insertions(+), 242 deletions(-) diff --git a/src/APP_CLSW/cfg/APP_CLSW_cfg.h b/src/APP_CLSW/cfg/APP_CLSW_cfg.h index 91faffe..5918948 100644 --- a/src/APP_CLSW/cfg/APP_CLSW_cfg.h +++ b/src/APP_CLSW/cfg/APP_CLSW_cfg.h @@ -1,10 +1,9 @@ /****************************************************************************** * File: APP_CLSW_cfg.h * Component: APP_CLSW - * Description: Configuration header for the APP_CLSW component. - * Declares configuration structures and constants that can be - * edited to adapt the color switcher application behavior - * (e.g., command strings, timing intervals, color sequences). + * Description: Configuration header for the color switcher application. + * Defines command buffer sizing, the HAL_COM channel used + * for host communication, and the command delimiter. * * Layer: Application - configuration *****************************************************************************/ @@ -12,6 +11,13 @@ #ifndef APP_CLSW_CFG_H #define APP_CLSW_CFG_H -/* Configuration constants and structure declarations will go here */ +#include "STD_TYPES.h" + +/** @brief Maximum length of a single command string (excluding delimiter). + * Commands longer than this are truncated. */ +#define APP_CLSW_CMD_BUFFER_SIZE 32U + +/** @brief HAL_COM channel used for host communication. */ +#define APP_CLSW_COM_CHANNEL ((u8)HAL_COM_CHANNEL_0) #endif /* APP_CLSW_CFG_H */ \ No newline at end of file diff --git a/src/APP_CLSW/prg/APP_CLSW_prg.c b/src/APP_CLSW/prg/APP_CLSW_prg.c index a6ab98d..513ab5b 100644 --- a/src/APP_CLSW/prg/APP_CLSW_prg.c +++ b/src/APP_CLSW/prg/APP_CLSW_prg.c @@ -1,11 +1,15 @@ /****************************************************************************** * File: APP_CLSW_prg.c * Component: APP_CLSW - * Description: Program (implementation) file for the Application Color - * Switcher. Contains the actual implementations of the public - * functions declared in APP_CLSW.h. Communicates with the host - * exclusively through HAL_COM - never touches MCU drivers - * directly. + * Description: Color switcher application logic. Receives commands from the + * host via HAL_COM, parses them, and responds with confirmation. + * + * 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: \r\n" * * Layer: Application *****************************************************************************/ @@ -14,27 +18,158 @@ #include "APP_CLSW_priv.h" #include "APP_CLSW_cfg.h" -/* HAL_COM is the only communication interface this component uses. - * It abstracts away whether bytes go over USB-CDC, hardware UART, or both. */ #include "HAL_COM.h" +/* ------------------------------------------------------------------------ */ +/* INTERNAL STATE */ +/* ------------------------------------------------------------------------ */ + +static APP_CLSW_tstrState strState; + +/* ------------------------------------------------------------------------ */ +/* INTERNAL HELPERS */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Compare two null-terminated byte arrays. + * @return STD_TRUE if equal, STD_FALSE if different. + */ +static STD_tBool bStrEqual(const u8 *pu8A, const u8 *pu8B) +{ + STD_tBool bResultLoc = STD_TRUE; + u8 u8IndexLoc = 0U; + + while ((pu8A[u8IndexLoc] != 0U) || (pu8B[u8IndexLoc] != 0U)) + { + if (pu8A[u8IndexLoc] != pu8B[u8IndexLoc]) + { + 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++; + } + + return bResultLoc; +} + +/** + * @brief Send a null-terminated string via HAL_COM on the configured channel. + */ +static void vSendString(const u8 *pu8Str) +{ + u16 u16LenLoc = 0U; + + /* Calculate string length */ + while (pu8Str[u16LenLoc] != 0U) + { + u16LenLoc++; + } + + HAL_COM_enuSendBuffer(APP_CLSW_COM_CHANNEL, pu8Str, u16LenLoc); +} + +/** + * @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 */ + } + else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"red") == STD_TRUE) + { + vSendString((const u8 *)"Color set to: RED\r\n"); + } + else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"green") == STD_TRUE) + { + vSendString((const u8 *)"Color set to: GREEN\r\n"); + } + else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"blue") == STD_TRUE) + { + vSendString((const u8 *)"Color set to: BLUE\r\n"); + } + else if (bStrEqual(strState.au8CmdBuffer, (const u8 *)"help") == STD_TRUE) + { + vSendString((const u8 *)"Available commands: red, green, blue, help\r\n"); + } + else + { + vSendString((const u8 *)"Unknown command: "); + vSendString(strState.au8CmdBuffer); + vSendString((const u8 *)"\r\n"); + } + + /* Reset the buffer for the next command */ + strState.u8CmdIndex = 0U; +} + +/* ========================================================================= */ +/* INIT */ +/* ========================================================================= */ + STD_tenuResult APP_CLSW_enuInit(void) { STD_tenuResult enuResultLoc = STD_OK; - /* APP_CLSW has no internal state to set up yet. When color sequences, - * state machines, or command tables are added, initialize them here. - * The communication stack (HAL_COM, MCU_USB, etc.) is already - * initialized by SYS_ECU before this function is called. */ + /* Clear the command buffer state */ + strState.u8CmdIndex = 0U; return enuResultLoc; } +/* ========================================================================= */ +/* RUNNABLE */ +/* ========================================================================= */ + void APP_CLSW_vRunnable(void) { - /* Proof-of-life: send a repeating test message to the host via the - * transport-agnostic HAL_COM layer. This will be replaced with real - * color-switching command logic once the communication stack is - * verified end-to-end. */ - HAL_COM_enuSendBuffer((u8)HAL_COM_CHANNEL_0, (const u8 *)"this is a color switcher application!\r\n", 40U); -} \ No newline at end of file + STD_tBool bDataAvailableLoc = STD_FALSE; + u8 u8ByteLoc = 0U; + STD_tenuResult enuRxResultLoc = STD_OK; + + /* Check if the host sent any data */ + bDataAvailableLoc = HAL_COM_bIsRxDataAvailable(APP_CLSW_COM_CHANNEL); + + while (bDataAvailableLoc == STD_TRUE) + { + /* Read one byte */ + enuRxResultLoc = HAL_COM_enuReceiveByte(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; + strState.u8CmdIndex++; + } + } + } + + /* Check for more data */ + bDataAvailableLoc = HAL_COM_bIsRxDataAvailable(APP_CLSW_COM_CHANNEL); + } +} diff --git a/src/APP_CLSW/prg/APP_CLSW_priv.h b/src/APP_CLSW/prg/APP_CLSW_priv.h index 1720ffc..17a94ea 100644 --- a/src/APP_CLSW/prg/APP_CLSW_priv.h +++ b/src/APP_CLSW/prg/APP_CLSW_priv.h @@ -1,10 +1,9 @@ /****************************************************************************** * File: APP_CLSW_priv.h * Component: APP_CLSW - * Description: Private header for the APP_CLSW component. - * Contains internal macros, helper declarations, and any - * state/definitions that are only used inside this component. - * Nothing declared here is exposed to external components. + * Description: Private header for the color switcher application. + * Contains the internal command buffer state used to accumulate + * incoming bytes until a complete command is received. * * Layer: Application - internal use only *****************************************************************************/ @@ -12,6 +11,24 @@ #ifndef APP_CLSW_PRIV_H #define APP_CLSW_PRIV_H -/* Private declarations, internal macros and helpers will go here */ +#include "APP_CLSW.h" +#include "APP_CLSW_cfg.h" + +/* ------------------------------------------------------------------------ */ +/* COMMAND BUFFER STATE */ +/* ------------------------------------------------------------------------ */ + +/** + * @brief Internal state for command accumulation. + * + * As bytes arrive from the host, they are stored in au8CmdBuffer until + * a delimiter (\r or \n) is received. At that point the buffer is + * null-terminated, parsed, and the index is reset for the next command. + */ +typedef struct +{ + u8 au8CmdBuffer[APP_CLSW_CMD_BUFFER_SIZE]; /**< Accumulated command bytes */ + u8 u8CmdIndex; /**< Next write position */ +} APP_CLSW_tstrState; #endif /* APP_CLSW_PRIV_H */ \ No newline at end of file diff --git a/src/HAL_COM/cfg/HAL_COM_cfg.c b/src/HAL_COM/cfg/HAL_COM_cfg.c index e4799cb..dafbcde 100644 --- a/src/HAL_COM/cfg/HAL_COM_cfg.c +++ b/src/HAL_COM/cfg/HAL_COM_cfg.c @@ -49,8 +49,28 @@ static STD_tenuResult vUsbSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Le } /* MCU_UART already matches the HAL_COM function pointer signatures - * (u8 u8Instance as the first parameter), so no wrapper is needed. - * Its functions can be assigned directly to the config struct. */ + * (u8 u8Instance as the first parameter), so no wrapper is needed + * for TX or RX. Its functions can be assigned directly. */ + +/* --- USB RX wrappers (same rationale as TX — normalize missing instance) --- */ + +/** + * @brief Wrapper for MCU_USB_enuReceiveByte to match HAL_COM_tpfReceiveByte. + */ +static STD_tenuResult vUsbReceiveByte(u8 u8Instance, u8 *pu8Byte) +{ + (void)u8Instance; + return MCU_USB_enuReceiveByte(pu8Byte); +} + +/** + * @brief Wrapper for MCU_USB_bIsRxDataAvailable to match HAL_COM_tpfIsRxDataAvailable. + */ +static STD_tBool vUsbIsRxDataAvailable(u8 u8Instance) +{ + (void)u8Instance; + return MCU_USB_bIsRxDataAvailable(); +} /* ------------------------------------------------------------------------ */ /* CHANNEL CONFIGURATION ARRAY */ @@ -63,22 +83,23 @@ static STD_tenuResult vUsbSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Le * * To add a UART channel: * 1. Add HAL_COM_CHANNEL_1 to the enum in HAL_COM_cfg.h - * 2. Add an entry here: + * 2. Add an entry here with MCU_UART functions (no wrapper needed): * [HAL_COM_CHANNEL_1] = { - * .pfSendByte = MCU_UART_enuSendByte, - * .pfSendBuffer = MCU_UART_enuSendBuffer, - * .u8Instance = 0U, + * .pfSendByte = MCU_UART_enuSendByte, + * .pfSendBuffer = MCU_UART_enuSendBuffer, + * .pfReceiveByte = MCU_UART_enuReceiveByte, + * .pfIsRxDataAvailable = MCU_UART_bIsRxDataAvailable, + * .u8Instance = 0U, * }, - * - * To add SPI, I2C, or any other transport: create a matching MCU driver - * with the same function signature, or add a wrapper here like vUsbSendByte. */ const HAL_COM_tstrChannelConfig HAL_COM_astrChannelConfig[HAL_COM_NUM_CHANNELS] = { [HAL_COM_CHANNEL_0] = { - .pfSendByte = vUsbSendByte, - .pfSendBuffer = vUsbSendBuffer, - .u8Instance = 0U, + .pfSendByte = vUsbSendByte, + .pfSendBuffer = vUsbSendBuffer, + .pfReceiveByte = vUsbReceiveByte, + .pfIsRxDataAvailable = vUsbIsRxDataAvailable, + .u8Instance = 0U, }, }; diff --git a/src/HAL_COM/inc/HAL_COM.h b/src/HAL_COM/inc/HAL_COM.h index c28b032..518184c 100644 --- a/src/HAL_COM/inc/HAL_COM.h +++ b/src/HAL_COM/inc/HAL_COM.h @@ -53,6 +53,23 @@ typedef STD_tenuResult (*HAL_COM_tpfSendByte)(u8 u8Instance, u8 u8Byte); */ typedef STD_tenuResult (*HAL_COM_tpfSendBuffer)(u8 u8Instance, const u8 *pu8Data, u16 u16Length); +/** + * @brief Generic receive-byte function pointer type (blocking). + * + * @param u8Instance Peripheral instance index. + * @param pu8Byte Pointer to store the received byte. + * @return STD_tenuResult + */ +typedef STD_tenuResult (*HAL_COM_tpfReceiveByte)(u8 u8Instance, u8 *pu8Byte); + +/** + * @brief Generic RX-data-available check function pointer type. + * + * @param u8Instance Peripheral instance index. + * @return STD_tBool + */ +typedef STD_tBool (*HAL_COM_tpfIsRxDataAvailable)(u8 u8Instance); + /* ------------------------------------------------------------------------ */ /* CONFIGURATION STRUCTURE */ /* ------------------------------------------------------------------------ */ @@ -70,9 +87,11 @@ typedef STD_tenuResult (*HAL_COM_tpfSendBuffer)(u8 u8Instance, const u8 *pu8Data */ typedef struct { - HAL_COM_tpfSendByte pfSendByte; /**< Driver's send-byte function */ - HAL_COM_tpfSendBuffer pfSendBuffer; /**< Driver's send-buffer function */ - u8 u8Instance; /**< Peripheral instance to pass through */ + HAL_COM_tpfSendByte pfSendByte; /**< Driver's send-byte function */ + HAL_COM_tpfSendBuffer pfSendBuffer; /**< Driver's send-buffer function */ + HAL_COM_tpfReceiveByte pfReceiveByte; /**< Driver's receive-byte function */ + HAL_COM_tpfIsRxDataAvailable pfIsRxDataAvailable; /**< Driver's RX-available check */ + u8 u8Instance; /**< Peripheral instance to pass through */ } HAL_COM_tstrChannelConfig; /* ------------------------------------------------------------------------ */ @@ -115,4 +134,23 @@ STD_tenuResult HAL_COM_enuSendByte(u8 u8Channel, u8 u8Byte); */ STD_tenuResult HAL_COM_enuSendBuffer(u8 u8Channel, const u8 *pu8Data, u16 u16Length); +/** + * @brief Receive one byte through the specified channel (blocking). + * + * @param u8Channel Channel index. + * @param pu8Byte Pointer to store the received byte. Must not be NULL. + * @return STD_OK byte received, + * STD_NULL_POINTER_ERROR if pu8Byte is NULL. + */ +STD_tenuResult HAL_COM_enuReceiveByte(u8 u8Channel, u8 *pu8Byte); + +/** + * @brief Check if the specified channel has RX data available. + * + * @param u8Channel Channel index. + * @return STD_TRUE if at least one byte is available, + * STD_FALSE if no data waiting. + */ +STD_tBool HAL_COM_bIsRxDataAvailable(u8 u8Channel); + #endif /* HAL_COM_H */ \ No newline at end of file diff --git a/src/HAL_COM/prg/HAL_COM_prg.c b/src/HAL_COM/prg/HAL_COM_prg.c index 53b0dfc..1605e62 100644 --- a/src/HAL_COM/prg/HAL_COM_prg.c +++ b/src/HAL_COM/prg/HAL_COM_prg.c @@ -65,3 +65,28 @@ STD_tenuResult HAL_COM_enuSendBuffer(u8 u8Channel, const u8 *pu8Data, u16 u16Len return enuResultLoc; } + +/* ========================================================================= */ +/* RECEIVE BYTE (BLOCKING) */ +/* ========================================================================= */ + +STD_tenuResult HAL_COM_enuReceiveByte(u8 u8Channel, u8 *pu8Byte) +{ + STD_tenuResult enuResultLoc = STD_OK; + const HAL_COM_tstrChannelConfig *pstrCfgLoc = &HAL_COM_astrChannelConfig[u8Channel]; + + enuResultLoc = pstrCfgLoc->pfReceiveByte(pstrCfgLoc->u8Instance, pu8Byte); + + return enuResultLoc; +} + +/* ========================================================================= */ +/* RX DATA AVAILABLE CHECK */ +/* ========================================================================= */ + +STD_tBool HAL_COM_bIsRxDataAvailable(u8 u8Channel) +{ + const HAL_COM_tstrChannelConfig *pstrCfgLoc = &HAL_COM_astrChannelConfig[u8Channel]; + + return pstrCfgLoc->pfIsRxDataAvailable(pstrCfgLoc->u8Instance); +} diff --git a/src/MCU_UART/cfg/MCU_UART_cfg.c b/src/MCU_UART/cfg/MCU_UART_cfg.c index e9f92e7..7eebe74 100644 --- a/src/MCU_UART/cfg/MCU_UART_cfg.c +++ b/src/MCU_UART/cfg/MCU_UART_cfg.c @@ -40,7 +40,9 @@ const MCU_UART_tstrConfig MCU_UART_astrConfig[MCU_UART_NUM_INSTANCES] = .enuDataBits = MCU_UART_0_DATA_BITS, .enuStopBits = MCU_UART_0_STOP_BITS, .enuParity = MCU_UART_0_PARITY, - .enuAsyncMode = MCU_UART_0_ASYNC_MODE, + .enuTxAsyncMode = MCU_UART_0_TX_ASYNC_MODE, .pfTxCompleteCallback = MCU_UART_0_TX_COMPLETE_CALLBACK, + .enuRxAsyncMode = MCU_UART_0_RX_ASYNC_MODE, + .pfRxCallback = MCU_UART_0_RX_CALLBACK, }, }; \ No newline at end of file diff --git a/src/MCU_UART/cfg/MCU_UART_cfg.h b/src/MCU_UART/cfg/MCU_UART_cfg.h index 23d6ef6..cb2daea 100644 --- a/src/MCU_UART/cfg/MCU_UART_cfg.h +++ b/src/MCU_UART/cfg/MCU_UART_cfg.h @@ -3,14 +3,8 @@ * Component: MCU_UART * Description: Configuration header for the MCU_UART driver. * Defines which UART instances are active, their GPIO pin - * assignments, baud rates, data format, async TX mechanism, - * and TX-complete callback. Each instance is one entry in - * MCU_UART_astrConfig[]; the array index equals the RP2040 - * UART instance number (0 = uart0, 1 = uart1). - * - * To add a second UART: add MCU_UART_INSTANCE_1 to the enum, - * define MCU_UART_1_* macros, and add a [MCU_UART_INSTANCE_1] - * initializer in MCU_UART_cfg.c. + * assignments, baud rates, data format, TX/RX async mechanisms, + * callbacks, and RX buffer sizing. * * Layer: MCU (hardware abstraction) - configuration *****************************************************************************/ @@ -24,53 +18,64 @@ /* INSTANCE ENUMERATION */ /* ------------------------------------------------------------------------ */ -/** - * @brief Enumeration of configured UART instances. - * - * Each enumerator's value matches the RP2040 UART peripheral index - * (0 = uart0, 1 = uart1). MCU_UART_NUM_INSTANCES is auto-calculated - * as the count and also used to size the config array. - */ typedef enum { MCU_UART_INSTANCE_0 = 0U, MCU_UART_NUM_INSTANCES } MCU_UART_tenuInstance; +/* ------------------------------------------------------------------------ */ +/* RX RING BUFFER SIZING */ +/* ------------------------------------------------------------------------ */ + +/** @brief Number of address bits for the RX ring buffer. The actual buffer + * size is 2^N bytes. Must be a power of 2 because DMA ring mode + * wraps the write address at a 2^N boundary. Applies to all + * instances. Change this single value to resize the buffer: + * 5 = 32 bytes, 6 = 64 bytes, 7 = 128 bytes, 8 = 256 bytes. */ +#define MCU_UART_RX_BUFFER_SIZE_BITS 6U + +/** @brief Derived buffer size in bytes. Do not edit — change + * MCU_UART_RX_BUFFER_SIZE_BITS instead. */ +#define MCU_UART_RX_BUFFER_SIZE (1U << MCU_UART_RX_BUFFER_SIZE_BITS) + +/** @brief Bitmask for ring buffer index wrapping. */ +#define MCU_UART_RX_BUFFER_MASK (MCU_UART_RX_BUFFER_SIZE - 1U) + /* ------------------------------------------------------------------------ */ /* INSTANCE 0 CONFIGURATION */ /* ------------------------------------------------------------------------ */ -/** @brief GPIO pin for UART0 TX (transmit). GP0 is the default for uart0. */ +/** @brief GPIO pin for UART0 TX. GP0 is the default for uart0. */ #define MCU_UART_0_TX_PIN 0U -/** @brief GPIO pin for UART0 RX (receive). GP1 is the default for uart0. */ +/** @brief GPIO pin for UART0 RX. GP1 is the default for uart0. */ #define MCU_UART_0_RX_PIN 1U -/** @brief Baud rate for UART0 in bits per second. 115200 is the most common - * default for serial monitors and USB-to-UART adapters. */ +/** @brief Baud rate for UART0 in bits per second. */ #define MCU_UART_0_BAUD_RATE 115200U -/** @brief Data bits per frame for UART0. 8 bits is the standard for binary - * and ASCII data transfer (8-N-1 is the universal default). */ +/** @brief Data bits per frame for UART0. */ #define MCU_UART_0_DATA_BITS MCU_UART_DATA_BITS_8 -/** @brief Stop bits per frame for UART0. 1 stop bit is the standard default. - * Use 2 only for slower receivers that need extra settling time. */ +/** @brief Stop bits per frame for UART0. */ #define MCU_UART_0_STOP_BITS MCU_UART_STOP_BITS_1 -/** @brief Parity mode for UART0. No parity is the standard default (8-N-1). - * Enable parity only when the receiving device requires it. */ +/** @brief Parity mode for UART0. */ #define MCU_UART_0_PARITY MCU_UART_PARITY_NONE -/** @brief Async TX mechanism for UART0. - * MCU_UART_ASYNC_DMA: zero-CPU DMA transfer (best for large buffers). - * MCU_UART_ASYNC_ISR: interrupt-driven byte-by-byte (no DMA channel used). */ -#define MCU_UART_0_ASYNC_MODE MCU_UART_ASYNC_DMA +/** @brief Async TX mechanism for UART0 (DMA or ISR). */ +#define MCU_UART_0_TX_ASYNC_MODE MCU_UART_ASYNC_DMA -/** @brief TX-complete callback for UART0. - * Called from interrupt context when an async or blocking SendBuffer - * finishes. Set to STD_NULL to disable (no notification). */ +/** @brief TX-complete callback for UART0. STD_NULL to disable. */ #define MCU_UART_0_TX_COMPLETE_CALLBACK STD_NULL -#endif /* MCU_UART_CFG_H */ \ No newline at end of file +/** @brief Async RX mechanism for UART0. + * DMA: hardware fills ring buffer, polling only (callback ignored). + * ISR: interrupt-driven, pfRxCallback fires per byte. */ +#define MCU_UART_0_RX_ASYNC_MODE MCU_UART_ASYNC_ISR + +/** @brief RX callback for UART0. Per-byte, ISR mode only. STD_NULL to disable. */ +#define MCU_UART_0_RX_CALLBACK STD_NULL + +#endif /* MCU_UART_CFG_H */ diff --git a/src/MCU_UART/inc/MCU_UART.h b/src/MCU_UART/inc/MCU_UART.h index ff0e1b9..3a357c1 100644 --- a/src/MCU_UART/inc/MCU_UART.h +++ b/src/MCU_UART/inc/MCU_UART.h @@ -93,8 +93,12 @@ typedef struct MCU_UART_tenuDataBits enuDataBits; /**< Data bits per frame */ MCU_UART_tenuStopBits enuStopBits; /**< Stop bits per frame */ MCU_UART_tenuParity enuParity; /**< Parity mode */ - MCU_UART_tenuAsyncMode enuAsyncMode; /**< DMA or ISR for non-blocking TX */ + MCU_UART_tenuAsyncMode enuTxAsyncMode; /**< DMA or ISR for non-blocking TX */ STD_tpfCallbackFunc pfTxCompleteCallback; /**< Called when TX finishes. STD_NULL to ignore. */ + MCU_UART_tenuAsyncMode enuRxAsyncMode; /**< DMA or ISR for RX data capture */ + STD_tpfCallbackFunc pfRxCallback; /**< Called per byte received (ISR mode only). + DMA mode = polling only, callback ignored. + STD_NULL to ignore. */ } MCU_UART_tstrConfig; /* ------------------------------------------------------------------------ */ @@ -163,4 +167,69 @@ STD_tenuResult MCU_UART_enuSendBufferBlocking(u8 u8Instance, const u8 *pu8Data, */ STD_tBool MCU_UART_bIsTxBusy(u8 u8Instance); +/** + * @brief Receive one byte (blocking). + * + * Waits until a byte is available in the internal ring buffer (filled + * by RX ISR or DMA in the background), then returns it. + * + * @param u8Instance UART instance index. + * @param pu8Byte Pointer to store the received byte. Must not be NULL. + * @return STD_OK byte received, + * STD_NULL_POINTER_ERROR if pu8Byte is NULL. + */ +STD_tenuResult MCU_UART_enuReceiveByte(u8 u8Instance, u8 *pu8Byte); + +/** + * @brief Receive a buffer of bytes (non-blocking, default). + * + * Registers a request for u16Length bytes. As bytes arrive in the ring + * buffer, they are copied to pu8Data. When all requested bytes have been + * collected, the RX callback fires (if configured, not STD_NULL). + * Returns immediately after registering the request. + * + * The caller MUST keep the buffer valid until the callback fires or + * MCU_UART_bIsRxBusy() returns STD_FALSE. + * + * @param u8Instance UART instance index. + * @param pu8Data Pointer to buffer to fill. Must not be NULL. + * @param u16Length Number of bytes to receive. + * @return STD_OK request registered, + * STD_NULL_POINTER_ERROR if pu8Data is NULL, + * STD_NOK if a receive request is already active. + */ +STD_tenuResult MCU_UART_enuReceiveBuffer(u8 u8Instance, u8 *pu8Data, u16 u16Length); + +/** + * @brief Receive a buffer of bytes (blocking). + * + * Blocks until u16Length bytes have been received from the ring buffer. + * Calls the RX callback at the end (if configured). + * + * @param u8Instance UART instance index. + * @param pu8Data Pointer to buffer to fill. Must not be NULL. + * @param u16Length Number of bytes to receive. + * @return STD_OK all bytes received, + * STD_NULL_POINTER_ERROR if pu8Data is NULL. + */ +STD_tenuResult MCU_UART_enuReceiveBufferBlocking(u8 u8Instance, u8 *pu8Data, u16 u16Length); + +/** + * @brief Check if the RX ring buffer has data waiting. + * + * @param u8Instance UART instance index. + * @return STD_TRUE if at least one byte is available, + * STD_FALSE if the ring buffer is empty. + */ +STD_tBool MCU_UART_bIsRxDataAvailable(u8 u8Instance); + +/** + * @brief Check if an async ReceiveBuffer request is in progress. + * + * @param u8Instance UART instance index. + * @return STD_TRUE if a non-blocking ReceiveBuffer is still collecting, + * STD_FALSE if idle. + */ +STD_tBool MCU_UART_bIsRxBusy(u8 u8Instance); + #endif /* MCU_UART_H */ \ No newline at end of file diff --git a/src/MCU_UART/prg/MCU_UART_prg.c b/src/MCU_UART/prg/MCU_UART_prg.c index 6cd2edc..4922e91 100644 --- a/src/MCU_UART/prg/MCU_UART_prg.c +++ b/src/MCU_UART/prg/MCU_UART_prg.c @@ -2,14 +2,12 @@ * File: MCU_UART_prg.c * Component: MCU_UART * Description: Program (implementation) file for the MCU_UART driver. - * Wraps the Pico SDK's hardware_uart and hardware_dma libraries - * to provide blocking and non-blocking send APIs for the RP2040 - * PL011 UART peripheral(s). - * - * Non-blocking TX supports two mechanisms (per-instance config): - * - DMA: hardware DMA channel transfers bytes autonomously - * - ISR: UART TX interrupt feeds one byte per interrupt - * Both invoke the configured TX-complete callback when done. + * TX: blocking + non-blocking (DMA or ISR) with callback. + * RX: ISR or DMA fills a ring buffer in the background. + * ReceiveByte (blocking) reads from the ring buffer. + * ReceiveBuffer (non-blocking) registers a request — bytes + * are copied from ring buffer to caller's buffer as they + * arrive. ReceiveBufferBlocking waits until N bytes collected. * * Layer: MCU (hardware abstraction) *****************************************************************************/ @@ -18,7 +16,6 @@ #include "MCU_UART_priv.h" #include "MCU_UART_cfg.h" -/* Pico SDK headers */ #include "hardware/uart.h" #include "hardware/gpio.h" #include "hardware/dma.h" @@ -28,143 +25,194 @@ /* INSTANCE LOOKUP TABLE */ /* ------------------------------------------------------------------------ */ -/** - * @brief Maps a config array index to the corresponding Pico SDK uart - * instance pointer (0 = uart0, 1 = uart1). - */ static uart_inst_t * const apstrInstances[] = { - uart0, /* index 0 = RP2040 uart0 peripheral */ - uart1, /* index 1 = RP2040 uart1 peripheral */ + uart0, + uart1, }; /* ------------------------------------------------------------------------ */ /* RUNTIME STATE */ /* ------------------------------------------------------------------------ */ -/** - * @brief Single static control structure holding all per-instance TX state. - * - * NOTE: abTxBusy is written from interrupt context (DMA/UART ISR) and - * read from main context (MCU_UART_bIsTxBusy, MCU_UART_enuSendBuffer). - * On the single-core RP2040 with interrupts disabled during read-modify- - * write sequences, this is safe. If dual-core access is ever needed, - * add volatile qualifiers or use a spin lock. - */ static MCU_UART_tstrControl strControl; /* ------------------------------------------------------------------------ */ /* INTERNAL HELPERS */ /* ------------------------------------------------------------------------ */ -/** - * @brief Call the TX-complete callback for a given instance if configured. - * Safe to invoke from ISR context. - * - * @param u8Instance Index into MCU_UART_astrConfig[]. - */ static void vCallTxCallback(u8 u8Instance) { - STD_tpfCallbackFunc pfCallbackLoc = MCU_UART_astrConfig[u8Instance].pfTxCompleteCallback; + STD_tpfCallbackFunc pfCbLoc = MCU_UART_astrConfig[u8Instance].pfTxCompleteCallback; - if (pfCallbackLoc != STD_NULL) + if (pfCbLoc != STD_NULL) { - pfCallbackLoc(); + pfCbLoc(); + } +} + +static void vCallRxCallback(u8 u8Instance) +{ + STD_tpfCallbackFunc pfCbLoc = MCU_UART_astrConfig[u8Instance].pfRxCallback; + + if (pfCbLoc != STD_NULL) + { + pfCbLoc(); + } +} + +/** + * @brief Get the current RX ring buffer head position. + * In DMA mode, derived from the DMA write pointer. + * In ISR mode, read directly from the control struct. + */ +static u16 u16GetRxHead(u8 u8Instance) +{ + u16 u16HeadLoc; + + if (MCU_UART_astrConfig[u8Instance].enuRxAsyncMode == MCU_UART_ASYNC_DMA) + { + s8 s8RxChLoc = strControl.as8RxDmaChannel[u8Instance]; + u32 u32WriteAddrLoc = (u32)dma_channel_hw_addr((u32)s8RxChLoc)->write_addr; + u32 u32BufferBaseLoc = (u32)(&strControl.aau8RxBuffer[u8Instance][0]); + u16HeadLoc = (u16)((u32WriteAddrLoc - u32BufferBaseLoc) & MCU_UART_RX_BUFFER_MASK); + } + else + { + u16HeadLoc = strControl.au16RxHead[u8Instance]; + } + + return u16HeadLoc; +} + +/** + * @brief Try to fulfill an active async ReceiveBuffer request by copying + * available bytes from the ring buffer to the caller's buffer. + * Called from the RX ISR after new bytes land in the ring buffer, + * or from polling contexts. + */ +static void vFulfillRxRequest(u8 u8Instance) +{ + u16 u16HeadLoc = u16GetRxHead(u8Instance); + u16 u16TailLoc = strControl.au16RxTail[u8Instance]; + + /* Copy available bytes from ring buffer to request buffer */ + while ((u16HeadLoc != u16TailLoc) && + (strControl.au16RxReqIndex[u8Instance] < strControl.au16RxReqLength[u8Instance])) + { + strControl.apu8RxReqBuffer[u8Instance][strControl.au16RxReqIndex[u8Instance]] = + strControl.aau8RxBuffer[u8Instance][u16TailLoc]; + strControl.au16RxReqIndex[u8Instance]++; + u16TailLoc = (u16TailLoc + 1U) & MCU_UART_RX_BUFFER_MASK; + } + + /* Update tail — bytes have been consumed */ + strControl.au16RxTail[u8Instance] = u16TailLoc; + + /* Check if request is fully fulfilled */ + if (strControl.au16RxReqIndex[u8Instance] >= strControl.au16RxReqLength[u8Instance]) + { + strControl.abRxReqActive[u8Instance] = STD_FALSE; + vCallRxCallback(u8Instance); } } /* ------------------------------------------------------------------------ */ -/* DMA IRQ HANDLER (INSTANCE 0) */ +/* TX DMA IRQ HANDLER (INSTANCE 0) */ /* ------------------------------------------------------------------------ */ -/** - * @brief DMA completion interrupt handler for UART instance 0. - * - * Fires when the DMA channel finishes transferring the TX buffer. - * Flow: clear IRQ flag -> set abTxBusy to STD_FALSE -> call callback. - */ -static void vDmaIrqHandler0(void) +static void vTxDmaIrqHandler0(void) { - s8 s8ChannelLoc = strControl.as8DmaChannel[MCU_UART_INSTANCE_0]; + s8 s8ChannelLoc = strControl.as8TxDmaChannel[MCU_UART_INSTANCE_0]; u32 u32StatusLoc; u32StatusLoc = dma_channel_get_irq0_status((u32)s8ChannelLoc); if (u32StatusLoc != 0U) { - /* Step 1: clear the DMA interrupt flag */ dma_irqn_acknowledge_channel(0, (u32)s8ChannelLoc); - - /* Step 2: mark instance as idle (written here in ISR, read in main) */ strControl.abTxBusy[MCU_UART_INSTANCE_0] = STD_FALSE; - - /* Step 3: notify the application */ vCallTxCallback((u8)MCU_UART_INSTANCE_0); } } -/* Note: when UART1 DMA is added, define vDmaIrqHandler1 here and - * use DMA_IRQ_1 to avoid sharing a single IRQ. */ - /* ------------------------------------------------------------------------ */ -/* UART TX ISR HANDLER */ +/* UART ISR HANDLER (RX + TX) */ /* ------------------------------------------------------------------------ */ /** - * @brief UART TX interrupt handler logic — fills the FIFO on each invocation. + * @brief Combined UART interrupt handler for RX and TX. * - * The PL011 TX interrupt fires when the FIFO level drops at or below the - * programmable threshold. This handler writes as many bytes as the FIFO - * can accept before returning, minimizing the number of interrupts needed - * for a given transfer (one interrupt per FIFO drain cycle, not per byte). + * RX: drains the RX FIFO into the ring buffer. If an async ReceiveBuffer + * request is active, copies bytes from ring buffer to the request buffer. + * Calls per-byte RX callback (ISR mode only). * - * When all bytes are sent, disables the TX interrupt, marks the instance - * as idle, and invokes the TX-complete callback. - * - * @param u8Instance Index of the UART instance that fired the interrupt. + * TX: fills the TX FIFO from the send buffer. When all bytes sent, disables + * TX interrupt and calls TX callback. */ -static void vTxIsrHandler(u8 u8Instance) +static void vUartIsrHandler(u8 u8Instance) { uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; - STD_tBool bFifoReady = STD_FALSE; - STD_tBool bDataLeft = STD_FALSE; - /* Fill the FIFO with as many bytes as it can accept */ - bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; - bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; + /* --- RX: drain FIFO into ring buffer --- */ + STD_tBool bRxReadable = STD_FALSE; - while ((bFifoReady == STD_TRUE) && (bDataLeft == STD_TRUE)) + bRxReadable = (uart_is_readable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + + while (bRxReadable == STD_TRUE) { - uart_putc_raw(pstrUartLoc, - strControl.apu8TxBuffer[u8Instance][strControl.au16TxIndex[u8Instance]]); - strControl.au16TxIndex[u8Instance]++; + u8 u8ByteLoc = (u8)uart_getc(pstrUartLoc); + u16 u16HeadLoc = strControl.au16RxHead[u8Instance]; + + /* Push byte into ring buffer */ + strControl.aau8RxBuffer[u8Instance][u16HeadLoc] = u8ByteLoc; + strControl.au16RxHead[u8Instance] = (u16HeadLoc + 1U) & MCU_UART_RX_BUFFER_MASK; + + bRxReadable = (uart_is_readable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + } + + /* If an async ReceiveBuffer request is active, try to fulfill it */ + if (strControl.abRxReqActive[u8Instance] == STD_TRUE) + { + vFulfillRxRequest(u8Instance); + } + + /* --- TX: fill FIFO from buffer (if TX is active in ISR mode) --- */ + STD_tBool bTxBusyLoc = strControl.abTxBusy[u8Instance]; + + if (bTxBusyLoc == STD_TRUE) + { + STD_tBool bFifoReady = STD_FALSE; + STD_tBool bDataLeft = STD_FALSE; bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; - } - /* If all bytes have been sent, shut down the interrupt */ - if (bDataLeft == STD_FALSE) - { - uart_set_irqs_enabled(pstrUartLoc, false, false); + while ((bFifoReady == STD_TRUE) && (bDataLeft == STD_TRUE)) + { + uart_putc_raw(pstrUartLoc, + strControl.apu8TxBuffer[u8Instance][strControl.au16TxIndex[u8Instance]]); + strControl.au16TxIndex[u8Instance]++; - strControl.abTxBusy[u8Instance] = STD_FALSE; + bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; + } - vCallTxCallback(u8Instance); + if (bDataLeft == STD_FALSE) + { + /* Disable TX interrupt, keep RX interrupt enabled */ + uart_set_irqs_enabled(pstrUartLoc, false, true); + strControl.abTxBusy[u8Instance] = STD_FALSE; + vCallTxCallback(u8Instance); + } } } -/** - * @brief UART0 TX interrupt entry point. - * Dispatches to the common handler with instance 0. - */ static void vUart0IrqHandler(void) { - vTxIsrHandler((u8)MCU_UART_INSTANCE_0); + vUartIsrHandler((u8)MCU_UART_INSTANCE_0); } -/* Note: when UART1 ISR mode is added, define vUart1IrqHandler here. */ - /* ========================================================================= */ /* INIT */ /* ========================================================================= */ @@ -177,11 +225,23 @@ STD_tenuResult MCU_UART_enuInit(void) /* Zero-initialize all control state */ for (u8IndexLoc = 0U; u8IndexLoc < (u8)MCU_UART_NUM_INSTANCES; u8IndexLoc++) { - strControl.apu8TxBuffer[u8IndexLoc] = STD_NULL; - strControl.au16TxLength[u8IndexLoc] = 0U; - strControl.au16TxIndex[u8IndexLoc] = 0U; - strControl.abTxBusy[u8IndexLoc] = STD_FALSE; - strControl.as8DmaChannel[u8IndexLoc] = -1; + /* TX */ + strControl.apu8TxBuffer[u8IndexLoc] = STD_NULL; + strControl.au16TxLength[u8IndexLoc] = 0U; + strControl.au16TxIndex[u8IndexLoc] = 0U; + strControl.abTxBusy[u8IndexLoc] = STD_FALSE; + strControl.as8TxDmaChannel[u8IndexLoc] = -1; + + /* RX */ + strControl.au16RxHead[u8IndexLoc] = 0U; + strControl.au16RxTail[u8IndexLoc] = 0U; + strControl.as8RxDmaChannel[u8IndexLoc] = -1; + + /* RX request */ + strControl.apu8RxReqBuffer[u8IndexLoc] = STD_NULL; + strControl.au16RxReqLength[u8IndexLoc] = 0U; + strControl.au16RxReqIndex[u8IndexLoc] = 0U; + strControl.abRxReqActive[u8IndexLoc] = STD_FALSE; } /* Configure each UART instance */ @@ -190,46 +250,69 @@ STD_tenuResult MCU_UART_enuInit(void) const MCU_UART_tstrConfig *pstrCfgLoc = &MCU_UART_astrConfig[u8IndexLoc]; uart_inst_t *pstrUartLoc = apstrInstances[u8IndexLoc]; - /* Enable the UART peripheral and set the baud rate */ + /* Basic peripheral setup */ uart_init(pstrUartLoc, pstrCfgLoc->u32BaudRate); - - /* Assign TX and RX GPIO pins to the UART function */ gpio_set_function(pstrCfgLoc->u8TxPin, GPIO_FUNC_UART); gpio_set_function(pstrCfgLoc->u8RxPin, GPIO_FUNC_UART); - - /* Set data format: data bits, stop bits, parity */ uart_set_format(pstrUartLoc, pstrCfgLoc->enuDataBits, pstrCfgLoc->enuStopBits, pstrCfgLoc->enuParity); - /* Set up the async TX mechanism based on config */ - if (pstrCfgLoc->enuAsyncMode == MCU_UART_ASYNC_DMA) + /* --- TX async setup --- */ + if (pstrCfgLoc->enuTxAsyncMode == MCU_UART_ASYNC_DMA) { - /* Claim a free DMA channel. true = panic if none available. */ s8 s8ChannelLoc = (s8)dma_claim_unused_channel(true); - strControl.as8DmaChannel[u8IndexLoc] = s8ChannelLoc; + strControl.as8TxDmaChannel[u8IndexLoc] = s8ChannelLoc; - /* Enable DMA completion interrupt for this channel */ dma_channel_set_irq0_enabled((u32)s8ChannelLoc, true); - /* Install the per-instance DMA IRQ handler */ if (u8IndexLoc == (u8)MCU_UART_INSTANCE_0) { - irq_set_exclusive_handler(DMA_IRQ_0, vDmaIrqHandler0); + irq_set_exclusive_handler(DMA_IRQ_0, vTxDmaIrqHandler0); irq_set_enabled(DMA_IRQ_0, true); } } + + /* --- RX async setup --- */ + if (pstrCfgLoc->enuRxAsyncMode == MCU_UART_ASYNC_DMA) + { + /* DMA RX: continuously fills ring buffer using DMA ring wrap. + * The write address wraps at the buffer size boundary, so the + * DMA writes in circles. Application reads from the tail and + * derives the head from the DMA write pointer. */ + s8 s8RxChLoc = (s8)dma_claim_unused_channel(true); + strControl.as8RxDmaChannel[u8IndexLoc] = s8RxChLoc; + + dma_channel_config strRxCfgLoc = dma_channel_get_default_config((u32)s8RxChLoc); + channel_config_set_transfer_data_size(&strRxCfgLoc, DMA_SIZE_8); + channel_config_set_read_increment(&strRxCfgLoc, false); + channel_config_set_write_increment(&strRxCfgLoc, true); + channel_config_set_dreq(&strRxCfgLoc, uart_get_dreq(pstrUartLoc, false)); + channel_config_set_ring(&strRxCfgLoc, true, MCU_UART_RX_BUFFER_SIZE_BITS); + + dma_channel_configure( + (u32)s8RxChLoc, + &strRxCfgLoc, + &strControl.aau8RxBuffer[u8IndexLoc][0], + &uart_get_hw(pstrUartLoc)->dr, + 0xFFFFFFFFU, + true + ); + } else { - /* ISR mode: install the per-instance UART TX interrupt handler. - * The TX interrupt itself is NOT enabled here — it gets enabled - * in SendBuffer and disabled by the ISR when transfer completes. */ + /* ISR RX: enable UART RX interrupt. The combined handler + * (vUartIsrHandler) drains the FIFO into the ring buffer. */ if (u8IndexLoc == (u8)MCU_UART_INSTANCE_0) { irq_set_exclusive_handler(UART0_IRQ, vUart0IrqHandler); irq_set_enabled(UART0_IRQ, true); } + + /* Enable RX interrupt (second param). TX stays off until + * SendBuffer is called. */ + uart_set_irqs_enabled(pstrUartLoc, false, true); } } @@ -244,8 +327,6 @@ STD_tenuResult MCU_UART_enuSendByte(u8 u8Instance, u8 u8Byte) { STD_tenuResult enuResultLoc = STD_OK; - /* Blocking single-byte send. uart_putc_raw waits until the TX FIFO - * has space, then writes the byte. */ uart_putc_raw(apstrInstances[u8Instance], u8Byte); return enuResultLoc; @@ -259,7 +340,7 @@ STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16L { STD_tenuResult enuResultLoc = STD_OK; STD_tBool bBusyLoc = strControl.abTxBusy[u8Instance]; - MCU_UART_tenuAsyncMode enuModeLoc = MCU_UART_astrConfig[u8Instance].enuAsyncMode; + MCU_UART_tenuAsyncMode enuModeLoc = MCU_UART_astrConfig[u8Instance].enuTxAsyncMode; if (pu8Data == STD_NULL) { @@ -267,14 +348,10 @@ STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16L } else if (bBusyLoc == STD_TRUE) { - /* A previous async transfer is still in progress */ enuResultLoc = STD_NOK; } else { - /* Store buffer info in the control structure. - * abTxBusy is set here (main context) and cleared by the ISR/DMA - * handler (interrupt context) when the transfer completes. */ strControl.apu8TxBuffer[u8Instance] = pu8Data; strControl.au16TxLength[u8Instance] = u16Length; strControl.au16TxIndex[u8Instance] = 0U; @@ -282,40 +359,31 @@ STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16L if (enuModeLoc == MCU_UART_ASYNC_DMA) { - /* --- DMA mode --- */ - s8 s8ChannelLoc = strControl.as8DmaChannel[u8Instance]; + s8 s8ChannelLoc = strControl.as8TxDmaChannel[u8Instance]; uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; - /* Configure DMA: memory -> UART TX FIFO, 1 byte at a time, - * paced by UART TX DREQ so we never overflow the FIFO. */ dma_channel_config strDmaCfgLoc = dma_channel_get_default_config((u32)s8ChannelLoc); channel_config_set_transfer_data_size(&strDmaCfgLoc, DMA_SIZE_8); channel_config_set_read_increment(&strDmaCfgLoc, true); channel_config_set_write_increment(&strDmaCfgLoc, false); channel_config_set_dreq(&strDmaCfgLoc, uart_get_dreq(pstrUartLoc, true)); - /* Start transfer. DMA IRQ fires on completion -> - * vDmaIrqHandler0 clears abTxBusy -> calls callback. */ dma_channel_configure( (u32)s8ChannelLoc, &strDmaCfgLoc, - &uart_get_hw(pstrUartLoc)->dr, /* dest: UART data register */ - pu8Data, /* source: caller's buffer */ - u16Length, /* transfer count */ - true /* start immediately */ + &uart_get_hw(pstrUartLoc)->dr, + pu8Data, + u16Length, + true ); } else { - /* --- ISR mode --- */ + /* ISR mode: kick-start by filling the FIFO */ uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; STD_tBool bFifoReady = STD_FALSE; STD_tBool bDataLeft = STD_FALSE; - /* Kick-start: fill the FIFO with as many bytes as it can - * accept right now. This gets the first bytes transmitting - * immediately without waiting for an interrupt context switch. - * Identical logic to what the ISR does on subsequent fills. */ bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE; @@ -329,8 +397,6 @@ STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16L bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE; } - /* If all bytes fit into the FIFO in one go, the transfer is - * already complete — no interrupt needed. */ if (bDataLeft == STD_FALSE) { strControl.abTxBusy[u8Instance] = STD_FALSE; @@ -338,10 +404,8 @@ STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16L } else { - /* Bytes remain — enable the TX interrupt. The ISR fills - * the FIFO each time it drains below threshold until the - * entire buffer is transmitted. */ - uart_set_irqs_enabled(pstrUartLoc, true, false); + /* Enable TX interrupt for remaining bytes. Keep RX on. */ + uart_set_irqs_enabled(pstrUartLoc, true, true); } } } @@ -364,13 +428,11 @@ STD_tenuResult MCU_UART_enuSendBufferBlocking(u8 u8Instance, const u8 *pu8Data, } else { - /* Send each byte blocking — waits for TX FIFO space per byte */ for (u16IndexLoc = 0U; u16IndexLoc < u16Length; u16IndexLoc++) { uart_putc_raw(apstrInstances[u8Instance], pu8Data[u16IndexLoc]); } - /* Invoke callback synchronously (no ISR in blocking mode) */ vCallTxCallback(u8Instance); } @@ -385,3 +447,139 @@ STD_tBool MCU_UART_bIsTxBusy(u8 u8Instance) { return strControl.abTxBusy[u8Instance]; } + +/* ========================================================================= */ +/* RECEIVE BYTE (BLOCKING) */ +/* ========================================================================= */ + +STD_tenuResult MCU_UART_enuReceiveByte(u8 u8Instance, u8 *pu8Byte) +{ + STD_tenuResult enuResultLoc = STD_OK; + + if (pu8Byte == STD_NULL) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else + { + u16 u16HeadLoc; + u16 u16TailLoc; + + /* Spin-wait until the ring buffer has at least one byte. + * The ring buffer is filled in the background by the RX ISR or DMA, + * so this loop will unblock as soon as a byte arrives on the wire. */ + do + { + u16HeadLoc = u16GetRxHead(u8Instance); + u16TailLoc = strControl.au16RxTail[u8Instance]; + } while (u16HeadLoc == u16TailLoc); + + /* Read one byte from the tail and advance */ + *pu8Byte = strControl.aau8RxBuffer[u8Instance][u16TailLoc]; + strControl.au16RxTail[u8Instance] = (u16TailLoc + 1U) & MCU_UART_RX_BUFFER_MASK; + } + + return enuResultLoc; +} + +/* ========================================================================= */ +/* RECEIVE BUFFER (NON-BLOCKING, DEFAULT) */ +/* ========================================================================= */ + +STD_tenuResult MCU_UART_enuReceiveBuffer(u8 u8Instance, u8 *pu8Data, u16 u16Length) +{ + STD_tenuResult enuResultLoc = STD_OK; + STD_tBool bActiveLocl = strControl.abRxReqActive[u8Instance]; + + if (pu8Data == STD_NULL) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else if (bActiveLocl == STD_TRUE) + { + /* A previous receive request is still pending */ + enuResultLoc = STD_NOK; + } + else + { + /* Register the request */ + strControl.apu8RxReqBuffer[u8Instance] = pu8Data; + strControl.au16RxReqLength[u8Instance] = u16Length; + strControl.au16RxReqIndex[u8Instance] = 0U; + strControl.abRxReqActive[u8Instance] = STD_TRUE; + + /* Try to fulfill immediately from bytes already in the ring buffer. + * If enough bytes are available, the request completes synchronously + * and the callback fires here. Otherwise it completes later when + * the ISR pushes more bytes (ISR mode) or when the application + * polls again (DMA mode). */ + vFulfillRxRequest(u8Instance); + } + + return enuResultLoc; +} + +/* ========================================================================= */ +/* RECEIVE BUFFER (BLOCKING) */ +/* ========================================================================= */ + +STD_tenuResult MCU_UART_enuReceiveBufferBlocking(u8 u8Instance, u8 *pu8Data, u16 u16Length) +{ + STD_tenuResult enuResultLoc = STD_OK; + u16 u16IndexLoc = 0U; + + if (pu8Data == STD_NULL) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else + { + /* Read N bytes from the ring buffer, blocking until each is available */ + while (u16IndexLoc < u16Length) + { + u16 u16HeadLoc = u16GetRxHead(u8Instance); + u16 u16TailLoc = strControl.au16RxTail[u8Instance]; + + /* Copy all currently available bytes */ + while ((u16HeadLoc != u16TailLoc) && (u16IndexLoc < u16Length)) + { + pu8Data[u16IndexLoc] = strControl.aau8RxBuffer[u8Instance][u16TailLoc]; + u16TailLoc = (u16TailLoc + 1U) & MCU_UART_RX_BUFFER_MASK; + u16IndexLoc++; + } + + strControl.au16RxTail[u8Instance] = u16TailLoc; + } + + vCallRxCallback(u8Instance); + } + + return enuResultLoc; +} + +/* ========================================================================= */ +/* RX DATA AVAILABLE CHECK */ +/* ========================================================================= */ + +STD_tBool MCU_UART_bIsRxDataAvailable(u8 u8Instance) +{ + STD_tBool bResultLoc = STD_FALSE; + u16 u16HeadLoc = u16GetRxHead(u8Instance); + u16 u16TailLoc = strControl.au16RxTail[u8Instance]; + + if (u16HeadLoc != u16TailLoc) + { + bResultLoc = STD_TRUE; + } + + return bResultLoc; +} + +/* ========================================================================= */ +/* RX BUSY CHECK */ +/* ========================================================================= */ + +STD_tBool MCU_UART_bIsRxBusy(u8 u8Instance) +{ + return strControl.abRxReqActive[u8Instance]; +} diff --git a/src/MCU_UART/prg/MCU_UART_priv.h b/src/MCU_UART/prg/MCU_UART_priv.h index 35c3620..2fc00bb 100644 --- a/src/MCU_UART/prg/MCU_UART_priv.h +++ b/src/MCU_UART/prg/MCU_UART_priv.h @@ -2,10 +2,9 @@ * File: MCU_UART_priv.h * Component: MCU_UART * Description: Private header for the MCU_UART driver. - * Contains the internal control structure used to track - * per-instance async TX state, the extern declaration of - * the config array (which is only accessed internally by - * _prg.c), and any other helpers private to the component. + * Contains the internal control structure for per-instance + * runtime state (TX async progress, RX ring buffer, DMA + * channels), and the extern config array declaration. * * Layer: MCU (hardware abstraction) - internal use only *****************************************************************************/ @@ -20,14 +19,6 @@ /* CONFIG ARRAY (EXTERN) */ /* ------------------------------------------------------------------------ */ -/** - * @brief Configuration array indexed by MCU_UART_tenuInstance. - * - * Defined in MCU_UART_cfg.c. Each entry holds the full configuration for - * one UART instance. The driver iterates this array during Init. - * Declared here (not in _cfg.h or .h) because only _prg.c needs to - * access it — no external component should read the raw config array. - */ extern const MCU_UART_tstrConfig MCU_UART_astrConfig[MCU_UART_NUM_INSTANCES]; /* ------------------------------------------------------------------------ */ @@ -37,29 +28,43 @@ extern const MCU_UART_tstrConfig MCU_UART_astrConfig[MCU_UART_NUM_INSTANCES]; /** * @brief Internal runtime state for all UART instances. * - * Each field is an array indexed by instance number. This struct-of-arrays - * layout keeps all per-instance state in a single static object inside - * MCU_UART_prg.c, making it easy to find, zero-initialize, and inspect - * in a debugger. + * Each field is an array indexed by instance number (struct-of-arrays). * - * Fields: - * apu8TxBuffer — pointer to the caller's buffer being transmitted - * au16TxLength — total number of bytes to transmit - * au16TxIndex — number of bytes transmitted so far (ISR mode only; - * DMA mode does not use this - the DMA controller - * tracks progress internally) - * abTxBusy — STD_TRUE while an async transfer is in progress - * as8DmaChannel — DMA channel number claimed during Init for each - * instance configured in DMA mode. Set to -1 for - * instances using ISR mode (no DMA channel needed). + * TX fields: + * apu8TxBuffer — caller's buffer being transmitted (non-blocking) + * au16TxLength — total bytes to transmit + * au16TxIndex — bytes transmitted so far (ISR mode) + * abTxBusy — STD_TRUE while async TX is in progress + * as8TxDmaChannel — DMA channel for TX (-1 if ISR mode) + * + * RX fields: + * aau8RxBuffer — per-instance ring buffer for received bytes. + * Aligned to buffer size for DMA ring mode compatibility. + * au16RxHead — write index (ISR writes here, DMA updates via hw pointer) + * au16RxTail — read index (application reads from here) + * as8RxDmaChannel — DMA channel for RX (-1 if ISR mode) */ typedef struct { + /* TX state */ const u8 *apu8TxBuffer[MCU_UART_NUM_INSTANCES]; u16 au16TxLength[MCU_UART_NUM_INSTANCES]; u16 au16TxIndex[MCU_UART_NUM_INSTANCES]; STD_tBool abTxBusy[MCU_UART_NUM_INSTANCES]; - s8 as8DmaChannel[MCU_UART_NUM_INSTANCES]; + s8 as8TxDmaChannel[MCU_UART_NUM_INSTANCES]; + + /* RX ring buffer state */ + u8 aau8RxBuffer[MCU_UART_NUM_INSTANCES][MCU_UART_RX_BUFFER_SIZE] + __attribute__((aligned(MCU_UART_RX_BUFFER_SIZE))); + u16 au16RxHead[MCU_UART_NUM_INSTANCES]; + u16 au16RxTail[MCU_UART_NUM_INSTANCES]; + s8 as8RxDmaChannel[MCU_UART_NUM_INSTANCES]; + + /* RX async request state (for non-blocking ReceiveBuffer) */ + u8 *apu8RxReqBuffer[MCU_UART_NUM_INSTANCES]; /**< caller's buffer */ + u16 au16RxReqLength[MCU_UART_NUM_INSTANCES]; /**< total bytes requested */ + u16 au16RxReqIndex[MCU_UART_NUM_INSTANCES]; /**< bytes fulfilled so far */ + STD_tBool abRxReqActive[MCU_UART_NUM_INSTANCES]; /**< is a request pending? */ } MCU_UART_tstrControl; #endif /* MCU_UART_PRIV_H */ \ No newline at end of file diff --git a/src/MCU_USB/inc/MCU_USB.h b/src/MCU_USB/inc/MCU_USB.h index 9a47d6f..0947812 100644 --- a/src/MCU_USB/inc/MCU_USB.h +++ b/src/MCU_USB/inc/MCU_USB.h @@ -67,4 +67,23 @@ STD_tenuResult MCU_USB_enuSendByte(u8 u8Byte); */ STD_tenuResult MCU_USB_enuSendBuffer(const u8 *pu8Data, u16 u16Length); +/** + * @brief Receive one byte over USB-CDC (blocking). + * + * Blocks until a byte arrives from the host serial monitor. + * + * @param pu8Byte Pointer to store the received byte. Must not be NULL. + * @return STD_OK byte received, + * STD_NULL_POINTER_ERROR if pu8Byte is NULL. + */ +STD_tenuResult MCU_USB_enuReceiveByte(u8 *pu8Byte); + +/** + * @brief Check if USB-CDC has data waiting to be read. + * + * @return STD_TRUE if at least one byte is available, + * STD_FALSE if no data waiting. + */ +STD_tBool MCU_USB_bIsRxDataAvailable(void); + #endif /* MCU_USB_H */ diff --git a/src/MCU_USB/prg/MCU_USB_prg.c b/src/MCU_USB/prg/MCU_USB_prg.c index 00109c6..3fbb2ee 100644 --- a/src/MCU_USB/prg/MCU_USB_prg.c +++ b/src/MCU_USB/prg/MCU_USB_prg.c @@ -116,4 +116,84 @@ STD_tenuResult MCU_USB_enuSendBuffer(const u8 *pu8Data, u16 u16Length) } return enuResultLoc; +} + +/* ========================================================================= */ +/* RECEIVE BYTE (BLOCKING) */ +/* ========================================================================= */ + +/** + * @brief Internal cached-byte state for bIsRxDataAvailable. + * + * The Pico SDK has no "peek" function for USB-CDC — the only way to check + * if data is available is to try reading. If a read succeeds during + * bIsRxDataAvailable, the byte is cached here so ReceiveByte can return it + * without a second read. bHasCachedByte tracks whether the cache is valid. + */ +static STD_tBool bHasCachedByte = STD_FALSE; +static u8 u8CachedByte = 0U; + +STD_tenuResult MCU_USB_enuReceiveByte(u8 *pu8Byte) +{ + STD_tenuResult enuResultLoc = STD_OK; + + if (pu8Byte == STD_NULL) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else + { + /* Check if bIsRxDataAvailable already cached a byte */ + if (bHasCachedByte == STD_TRUE) + { + *pu8Byte = u8CachedByte; + bHasCachedByte = STD_FALSE; + } + else + { + /* Block until a byte arrives. getchar_timeout_us with 0 is + * non-blocking — loop until we get a real byte. PICO_ERROR_TIMEOUT + * is returned as a negative value when no data is available. */ + s32 s32ResultLoc; + + do + { + s32ResultLoc = (s32)getchar_timeout_us(0); + } while (s32ResultLoc < 0); + + *pu8Byte = (u8)s32ResultLoc; + } + } + + return enuResultLoc; +} + +/* ========================================================================= */ +/* RX DATA AVAILABLE CHECK */ +/* ========================================================================= */ + +STD_tBool MCU_USB_bIsRxDataAvailable(void) +{ + STD_tBool bResultLoc = STD_FALSE; + + if (bHasCachedByte == STD_TRUE) + { + /* Already have a cached byte from a previous check */ + bResultLoc = STD_TRUE; + } + else + { + /* Try a non-blocking read. If successful, cache the byte so + * the next ReceiveByte call can return it immediately. */ + s32 s32ResultLoc = (s32)getchar_timeout_us(0); + + if (s32ResultLoc >= 0) + { + u8CachedByte = (u8)s32ResultLoc; + bHasCachedByte = STD_TRUE; + bResultLoc = STD_TRUE; + } + } + + return bResultLoc; } \ No newline at end of file diff --git a/src/SYS_ECU/prg/SYS_ECU.c b/src/SYS_ECU/prg/SYS_ECU.c index b9d26fc..4ecd4b3 100644 --- a/src/SYS_ECU/prg/SYS_ECU.c +++ b/src/SYS_ECU/prg/SYS_ECU.c @@ -55,13 +55,12 @@ int main(void) SYS_ECU_vInitAll(); /* Phase 2: super-loop scheduler - dispatches runnables each tick. - * For the proof-of-life test, APP_CLSW_vRunnable sends a message - * over USB-CDC once per second. The sleep_ms call here controls the - * scheduler tick rate - it will eventually be replaced by a proper + * 10 ms tick rate gives responsive interactive input while keeping + * CPU usage reasonable. Will eventually be replaced by a proper * timer-driven scheduler. */ while (1) { APP_CLSW_vRunnable(); - sleep_ms(1000); + sleep_ms(10); } } \ No newline at end of file From 3d5e63c790888bca3e93ef3068c7b9e22a3f608a Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Mon, 13 Apr 2026 01:50:58 +0200 Subject: [PATCH 5/6] Redesign RX to non-blocking ring buffer model UART/USB now receive in the background and store into ring buffers. Callers read from the buffer via non-blocking ReadByte/ReadBuffer. Removed blocking ReceiveByte, async ReceiveBuffer with request state and callbacks. MCU_USB uses lazy drain from SDK stdio into its own ring buffer. MCU_UART ring buffer unchanged (ISR/DMA). HAL_COM updated with ReadByte/ReadBuffer function pointer dispatch. APP_CLSW updated to use new ReadByte API. --- src/APP_CLSW/prg/APP_CLSW_prg.c | 2 +- src/HAL_COM/cfg/HAL_COM_cfg.c | 21 +-- src/HAL_COM/inc/HAL_COM.h | 37 ++-- src/HAL_COM/prg/HAL_COM_prg.c | 18 +- src/MCU_UART/cfg/MCU_UART_cfg.c | 24 +-- src/MCU_UART/cfg/MCU_UART_cfg.h | 44 +---- src/MCU_UART/inc/MCU_UART.h | 221 ++++++----------------- src/MCU_UART/prg/MCU_UART_prg.c | 300 +++++++------------------------ src/MCU_UART/prg/MCU_UART_priv.h | 42 +---- src/MCU_USB/inc/MCU_USB.h | 101 +++++------ src/MCU_USB/prg/MCU_USB_prg.c | 218 ++++++++++++---------- 11 files changed, 342 insertions(+), 686 deletions(-) diff --git a/src/APP_CLSW/prg/APP_CLSW_prg.c b/src/APP_CLSW/prg/APP_CLSW_prg.c index 513ab5b..6ca66e0 100644 --- a/src/APP_CLSW/prg/APP_CLSW_prg.c +++ b/src/APP_CLSW/prg/APP_CLSW_prg.c @@ -144,7 +144,7 @@ void APP_CLSW_vRunnable(void) while (bDataAvailableLoc == STD_TRUE) { /* Read one byte */ - enuRxResultLoc = HAL_COM_enuReceiveByte(APP_CLSW_COM_CHANNEL, &u8ByteLoc); + enuRxResultLoc = HAL_COM_enuReadByte(APP_CLSW_COM_CHANNEL, &u8ByteLoc); if (enuRxResultLoc == STD_OK) { diff --git a/src/HAL_COM/cfg/HAL_COM_cfg.c b/src/HAL_COM/cfg/HAL_COM_cfg.c index dafbcde..72c3456 100644 --- a/src/HAL_COM/cfg/HAL_COM_cfg.c +++ b/src/HAL_COM/cfg/HAL_COM_cfg.c @@ -52,20 +52,20 @@ static STD_tenuResult vUsbSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Le * (u8 u8Instance as the first parameter), so no wrapper is needed * for TX or RX. Its functions can be assigned directly. */ -/* --- USB RX wrappers (same rationale as TX — normalize missing instance) --- */ +/* --- USB RX wrappers (normalize missing instance parameter) --- */ -/** - * @brief Wrapper for MCU_USB_enuReceiveByte to match HAL_COM_tpfReceiveByte. - */ -static STD_tenuResult vUsbReceiveByte(u8 u8Instance, u8 *pu8Byte) +static STD_tenuResult vUsbReadByte(u8 u8Instance, u8 *pu8Byte) { (void)u8Instance; - return MCU_USB_enuReceiveByte(pu8Byte); + return MCU_USB_enuReadByte(pu8Byte); +} + +static STD_tenuResult vUsbReadBuffer(u8 u8Instance, u8 *pu8Data, u16 u16MaxLength, u16 *pu16Read) +{ + (void)u8Instance; + return MCU_USB_enuReadBuffer(pu8Data, u16MaxLength, pu16Read); } -/** - * @brief Wrapper for MCU_USB_bIsRxDataAvailable to match HAL_COM_tpfIsRxDataAvailable. - */ static STD_tBool vUsbIsRxDataAvailable(u8 u8Instance) { (void)u8Instance; @@ -98,7 +98,8 @@ const HAL_COM_tstrChannelConfig HAL_COM_astrChannelConfig[HAL_COM_NUM_CHANNELS] { .pfSendByte = vUsbSendByte, .pfSendBuffer = vUsbSendBuffer, - .pfReceiveByte = vUsbReceiveByte, + .pfReadByte = vUsbReadByte, + .pfReadBuffer = vUsbReadBuffer, .pfIsRxDataAvailable = vUsbIsRxDataAvailable, .u8Instance = 0U, }, diff --git a/src/HAL_COM/inc/HAL_COM.h b/src/HAL_COM/inc/HAL_COM.h index 518184c..3b0750b 100644 --- a/src/HAL_COM/inc/HAL_COM.h +++ b/src/HAL_COM/inc/HAL_COM.h @@ -54,19 +54,17 @@ typedef STD_tenuResult (*HAL_COM_tpfSendByte)(u8 u8Instance, u8 u8Byte); typedef STD_tenuResult (*HAL_COM_tpfSendBuffer)(u8 u8Instance, const u8 *pu8Data, u16 u16Length); /** - * @brief Generic receive-byte function pointer type (blocking). - * - * @param u8Instance Peripheral instance index. - * @param pu8Byte Pointer to store the received byte. - * @return STD_tenuResult + * @brief Generic read-byte function pointer type (non-blocking). */ -typedef STD_tenuResult (*HAL_COM_tpfReceiveByte)(u8 u8Instance, u8 *pu8Byte); +typedef STD_tenuResult (*HAL_COM_tpfReadByte)(u8 u8Instance, u8 *pu8Byte); + +/** + * @brief Generic read-buffer function pointer type (non-blocking). + */ +typedef STD_tenuResult (*HAL_COM_tpfReadBuffer)(u8 u8Instance, u8 *pu8Data, u16 u16MaxLength, u16 *pu16Read); /** * @brief Generic RX-data-available check function pointer type. - * - * @param u8Instance Peripheral instance index. - * @return STD_tBool */ typedef STD_tBool (*HAL_COM_tpfIsRxDataAvailable)(u8 u8Instance); @@ -89,7 +87,8 @@ typedef struct { HAL_COM_tpfSendByte pfSendByte; /**< Driver's send-byte function */ HAL_COM_tpfSendBuffer pfSendBuffer; /**< Driver's send-buffer function */ - HAL_COM_tpfReceiveByte pfReceiveByte; /**< Driver's receive-byte function */ + HAL_COM_tpfReadByte pfReadByte; /**< Driver's read-byte function */ + HAL_COM_tpfReadBuffer pfReadBuffer; /**< Driver's read-buffer function */ HAL_COM_tpfIsRxDataAvailable pfIsRxDataAvailable; /**< Driver's RX-available check */ u8 u8Instance; /**< Peripheral instance to pass through */ } HAL_COM_tstrChannelConfig; @@ -135,21 +134,17 @@ STD_tenuResult HAL_COM_enuSendByte(u8 u8Channel, u8 u8Byte); STD_tenuResult HAL_COM_enuSendBuffer(u8 u8Channel, const u8 *pu8Data, u16 u16Length); /** - * @brief Receive one byte through the specified channel (blocking). - * - * @param u8Channel Channel index. - * @param pu8Byte Pointer to store the received byte. Must not be NULL. - * @return STD_OK byte received, - * STD_NULL_POINTER_ERROR if pu8Byte is NULL. + * @brief Read one byte from the specified channel (non-blocking). */ -STD_tenuResult HAL_COM_enuReceiveByte(u8 u8Channel, u8 *pu8Byte); +STD_tenuResult HAL_COM_enuReadByte(u8 u8Channel, u8 *pu8Byte); + +/** + * @brief Read up to u16MaxLength bytes from the specified channel (non-blocking). + */ +STD_tenuResult HAL_COM_enuReadBuffer(u8 u8Channel, u8 *pu8Data, u16 u16MaxLength, u16 *pu16Read); /** * @brief Check if the specified channel has RX data available. - * - * @param u8Channel Channel index. - * @return STD_TRUE if at least one byte is available, - * STD_FALSE if no data waiting. */ STD_tBool HAL_COM_bIsRxDataAvailable(u8 u8Channel); diff --git a/src/HAL_COM/prg/HAL_COM_prg.c b/src/HAL_COM/prg/HAL_COM_prg.c index 1605e62..271abd7 100644 --- a/src/HAL_COM/prg/HAL_COM_prg.c +++ b/src/HAL_COM/prg/HAL_COM_prg.c @@ -70,12 +70,26 @@ STD_tenuResult HAL_COM_enuSendBuffer(u8 u8Channel, const u8 *pu8Data, u16 u16Len /* RECEIVE BYTE (BLOCKING) */ /* ========================================================================= */ -STD_tenuResult HAL_COM_enuReceiveByte(u8 u8Channel, u8 *pu8Byte) +STD_tenuResult HAL_COM_enuReadByte(u8 u8Channel, u8 *pu8Byte) { STD_tenuResult enuResultLoc = STD_OK; const HAL_COM_tstrChannelConfig *pstrCfgLoc = &HAL_COM_astrChannelConfig[u8Channel]; - enuResultLoc = pstrCfgLoc->pfReceiveByte(pstrCfgLoc->u8Instance, pu8Byte); + enuResultLoc = pstrCfgLoc->pfReadByte(pstrCfgLoc->u8Instance, pu8Byte); + + return enuResultLoc; +} + +/* ========================================================================= */ +/* READ BUFFER (NON-BLOCKING) */ +/* ========================================================================= */ + +STD_tenuResult HAL_COM_enuReadBuffer(u8 u8Channel, u8 *pu8Data, u16 u16MaxLength, u16 *pu16Read) +{ + STD_tenuResult enuResultLoc = STD_OK; + const HAL_COM_tstrChannelConfig *pstrCfgLoc = &HAL_COM_astrChannelConfig[u8Channel]; + + enuResultLoc = pstrCfgLoc->pfReadBuffer(pstrCfgLoc->u8Instance, pu8Data, u16MaxLength, pu16Read); return enuResultLoc; } diff --git a/src/MCU_UART/cfg/MCU_UART_cfg.c b/src/MCU_UART/cfg/MCU_UART_cfg.c index 7eebe74..c490e8f 100644 --- a/src/MCU_UART/cfg/MCU_UART_cfg.c +++ b/src/MCU_UART/cfg/MCU_UART_cfg.c @@ -1,35 +1,14 @@ /****************************************************************************** * File: MCU_UART_cfg.c * Component: MCU_UART - * Description: Configuration implementation for the MCU_UART driver. - * Defines the MCU_UART_astrConfig[] array that holds the actual - * per-instance UART settings. Each entry is initialized using - * the named macros from MCU_UART_cfg.h so that no magic numbers - * appear in the initializer. + * Description: Configuration array definition for the MCU_UART driver. * * Layer: MCU (hardware abstraction) - configuration *****************************************************************************/ -/* MCU_UART.h is included first because it defines the struct type - * (MCU_UART_tstrConfig) and the enum types (MCU_UART_tenuDataBits, etc.) - * that are used in the array definition below. */ #include "MCU_UART.h" #include "MCU_UART_cfg.h" -/* ------------------------------------------------------------------------ */ -/* CONFIGURATION ARRAY DEFINITION */ -/* ------------------------------------------------------------------------ */ - -/** - * @brief Per-instance UART configuration, indexed by MCU_UART_tenuInstance. - * - * [MCU_UART_INSTANCE_0] = uart0 on the RP2040. Configured for standard - * 8-N-1 at 115200 baud on GP0 (TX) / GP1 (RX), with DMA-based async TX - * and no TX-complete callback. - * - * To add uart1: add a [MCU_UART_INSTANCE_1] entry here with the - * corresponding MCU_UART_1_* macros defined in MCU_UART_cfg.h. - */ const MCU_UART_tstrConfig MCU_UART_astrConfig[MCU_UART_NUM_INSTANCES] = { [MCU_UART_INSTANCE_0] = @@ -43,6 +22,5 @@ const MCU_UART_tstrConfig MCU_UART_astrConfig[MCU_UART_NUM_INSTANCES] = .enuTxAsyncMode = MCU_UART_0_TX_ASYNC_MODE, .pfTxCompleteCallback = MCU_UART_0_TX_COMPLETE_CALLBACK, .enuRxAsyncMode = MCU_UART_0_RX_ASYNC_MODE, - .pfRxCallback = MCU_UART_0_RX_CALLBACK, }, }; \ No newline at end of file diff --git a/src/MCU_UART/cfg/MCU_UART_cfg.h b/src/MCU_UART/cfg/MCU_UART_cfg.h index cb2daea..9166a19 100644 --- a/src/MCU_UART/cfg/MCU_UART_cfg.h +++ b/src/MCU_UART/cfg/MCU_UART_cfg.h @@ -1,10 +1,9 @@ /****************************************************************************** * File: MCU_UART_cfg.h * Component: MCU_UART - * Description: Configuration header for the MCU_UART driver. - * Defines which UART instances are active, their GPIO pin - * assignments, baud rates, data format, TX/RX async mechanisms, - * callbacks, and RX buffer sizing. + * Description: Configuration for the MCU_UART driver. Defines instances, + * pin assignments, baud rates, data format, TX/RX async modes, + * and RX buffer sizing. * * Layer: MCU (hardware abstraction) - configuration *****************************************************************************/ @@ -28,54 +27,25 @@ typedef enum /* RX RING BUFFER SIZING */ /* ------------------------------------------------------------------------ */ -/** @brief Number of address bits for the RX ring buffer. The actual buffer - * size is 2^N bytes. Must be a power of 2 because DMA ring mode - * wraps the write address at a 2^N boundary. Applies to all - * instances. Change this single value to resize the buffer: - * 5 = 32 bytes, 6 = 64 bytes, 7 = 128 bytes, 8 = 256 bytes. */ +/** @brief Number of address bits for the RX ring buffer (2^N bytes). + * Must be power of 2 for DMA ring wrap. + * 5 = 32, 6 = 64, 7 = 128, 8 = 256. */ #define MCU_UART_RX_BUFFER_SIZE_BITS 6U - -/** @brief Derived buffer size in bytes. Do not edit — change - * MCU_UART_RX_BUFFER_SIZE_BITS instead. */ #define MCU_UART_RX_BUFFER_SIZE (1U << MCU_UART_RX_BUFFER_SIZE_BITS) - -/** @brief Bitmask for ring buffer index wrapping. */ #define MCU_UART_RX_BUFFER_MASK (MCU_UART_RX_BUFFER_SIZE - 1U) /* ------------------------------------------------------------------------ */ /* INSTANCE 0 CONFIGURATION */ /* ------------------------------------------------------------------------ */ -/** @brief GPIO pin for UART0 TX. GP0 is the default for uart0. */ #define MCU_UART_0_TX_PIN 0U - -/** @brief GPIO pin for UART0 RX. GP1 is the default for uart0. */ #define MCU_UART_0_RX_PIN 1U - -/** @brief Baud rate for UART0 in bits per second. */ #define MCU_UART_0_BAUD_RATE 115200U - -/** @brief Data bits per frame for UART0. */ #define MCU_UART_0_DATA_BITS MCU_UART_DATA_BITS_8 - -/** @brief Stop bits per frame for UART0. */ #define MCU_UART_0_STOP_BITS MCU_UART_STOP_BITS_1 - -/** @brief Parity mode for UART0. */ #define MCU_UART_0_PARITY MCU_UART_PARITY_NONE - -/** @brief Async TX mechanism for UART0 (DMA or ISR). */ #define MCU_UART_0_TX_ASYNC_MODE MCU_UART_ASYNC_DMA - -/** @brief TX-complete callback for UART0. STD_NULL to disable. */ #define MCU_UART_0_TX_COMPLETE_CALLBACK STD_NULL - -/** @brief Async RX mechanism for UART0. - * DMA: hardware fills ring buffer, polling only (callback ignored). - * ISR: interrupt-driven, pfRxCallback fires per byte. */ #define MCU_UART_0_RX_ASYNC_MODE MCU_UART_ASYNC_ISR -/** @brief RX callback for UART0. Per-byte, ISR mode only. STD_NULL to disable. */ -#define MCU_UART_0_RX_CALLBACK STD_NULL - -#endif /* MCU_UART_CFG_H */ +#endif /* MCU_UART_CFG_H */ \ No newline at end of file diff --git a/src/MCU_UART/inc/MCU_UART.h b/src/MCU_UART/inc/MCU_UART.h index 3a357c1..283a327 100644 --- a/src/MCU_UART/inc/MCU_UART.h +++ b/src/MCU_UART/inc/MCU_UART.h @@ -2,20 +2,11 @@ * File: MCU_UART.h * Component: MCU_UART * Description: Public interface for the MCU UART driver component. - * This header exposes the functions, types, and configuration - * value enumerations that other components are allowed to use - * when interacting with the hardware UART peripheral(s) on the - * RP2040 microcontroller. * - * Two send-buffer modes are provided: - * - SendBuffer (default, non-blocking): starts an async - * transfer via DMA or ISR and returns immediately. - * - SendBufferBlocking: loops byte-by-byte and returns - * only after all data is transmitted. - * Both invoke the TX-complete callback (if configured). - * - * All public functions take a u8 instance parameter so the - * caller selects which UART peripheral to operate on. + * TX: SendByte (blocking), SendBuffer (non-blocking DMA/ISR), + * SendBufferBlocking. + * RX: background ring buffer filled by ISR or DMA. + * ReadByte / ReadBuffer read from the buffer (non-blocking). * * Layer: MCU (hardware abstraction) *****************************************************************************/ @@ -23,27 +14,19 @@ #ifndef MCU_UART_H #define MCU_UART_H -/* STD_TYPES provides fixed-width typedefs (u8, u16, u32), STD_tenuResult, - * STD_tBool, STD_tpfCallbackFunc, and STD_NULL. */ #include "STD_TYPES.h" /* ------------------------------------------------------------------------ */ /* CONFIGURATION VALUE TYPES */ /* ------------------------------------------------------------------------ */ -/** - * @brief Parity mode options for UART data framing. - */ typedef enum { - MCU_UART_PARITY_NONE = 0U, /**< No parity bit transmitted */ - MCU_UART_PARITY_EVEN, /**< Even parity */ - MCU_UART_PARITY_ODD /**< Odd parity */ + MCU_UART_PARITY_NONE = 0U, + MCU_UART_PARITY_EVEN, + MCU_UART_PARITY_ODD } MCU_UART_tenuParity; -/** - * @brief Number of data bits per UART frame. - */ typedef enum { MCU_UART_DATA_BITS_5 = 5U, @@ -52,184 +35,90 @@ typedef enum MCU_UART_DATA_BITS_8 = 8U } MCU_UART_tenuDataBits; -/** - * @brief Number of stop bits per UART frame. - */ typedef enum { MCU_UART_STOP_BITS_1 = 1U, MCU_UART_STOP_BITS_2 = 2U } MCU_UART_tenuStopBits; -/** - * @brief Async transmit mechanism selection. - * - * DMA: hardware DMA channel moves bytes autonomously. Zero CPU during - * transfer. Best for large buffers. - * ISR: UART TX interrupt fires per byte. Uses CPU per-byte but does not - * consume a DMA channel. - */ typedef enum { - MCU_UART_ASYNC_DMA = 0U, /**< Use DMA for non-blocking TX */ - MCU_UART_ASYNC_ISR /**< Use UART TX interrupt for non-blocking TX */ + MCU_UART_ASYNC_DMA = 0U, + MCU_UART_ASYNC_ISR } MCU_UART_tenuAsyncMode; /* ------------------------------------------------------------------------ */ /* CONFIGURATION STRUCTURE */ /* ------------------------------------------------------------------------ */ -/** - * @brief Per-instance UART configuration. - * - * One entry per hardware UART instance, stored in MCU_UART_astrConfig[]. - * The array index IS the UART instance number (0 = uart0, 1 = uart1). - */ typedef struct { - u8 u8TxPin; /**< GPIO number for TX */ - u8 u8RxPin; /**< GPIO number for RX */ - u32 u32BaudRate; /**< Baud rate in bps */ - MCU_UART_tenuDataBits enuDataBits; /**< Data bits per frame */ - MCU_UART_tenuStopBits enuStopBits; /**< Stop bits per frame */ - MCU_UART_tenuParity enuParity; /**< Parity mode */ - MCU_UART_tenuAsyncMode enuTxAsyncMode; /**< DMA or ISR for non-blocking TX */ - STD_tpfCallbackFunc pfTxCompleteCallback; /**< Called when TX finishes. STD_NULL to ignore. */ - MCU_UART_tenuAsyncMode enuRxAsyncMode; /**< DMA or ISR for RX data capture */ - STD_tpfCallbackFunc pfRxCallback; /**< Called per byte received (ISR mode only). - DMA mode = polling only, callback ignored. - STD_NULL to ignore. */ + u8 u8TxPin; + u8 u8RxPin; + u32 u32BaudRate; + MCU_UART_tenuDataBits enuDataBits; + MCU_UART_tenuStopBits enuStopBits; + MCU_UART_tenuParity enuParity; + MCU_UART_tenuAsyncMode enuTxAsyncMode; + STD_tpfCallbackFunc pfTxCompleteCallback; + MCU_UART_tenuAsyncMode enuRxAsyncMode; } MCU_UART_tstrConfig; /* ------------------------------------------------------------------------ */ -/* PUBLIC API */ +/* TX PUBLIC API */ +/* ------------------------------------------------------------------------ */ + +STD_tenuResult MCU_UART_enuInit(void); + +STD_tenuResult MCU_UART_enuSendByte(u8 u8Instance, u8 u8Byte); + +STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Length); + +STD_tenuResult MCU_UART_enuSendBufferBlocking(u8 u8Instance, const u8 *pu8Data, u16 u16Length); + +STD_tBool MCU_UART_bIsTxBusy(u8 u8Instance); + +/* ------------------------------------------------------------------------ */ +/* RX PUBLIC API */ /* ------------------------------------------------------------------------ */ /** - * @brief Initialize all configured UART instances. + * @brief Read one byte from the RX ring buffer (non-blocking). * - * Iterates MCU_UART_astrConfig[] and for each entry: sets baud rate, - * assigns GPIO pins, configures data format, claims DMA channel (if DMA - * mode), and sets up the appropriate IRQ handler. - * - * @return STD_OK on success, STD_NOK if any instance fails. - */ -STD_tenuResult MCU_UART_enuInit(void); - -/** - * @brief Send a single byte (blocking). - * - * @param u8Instance UART instance index (0 = uart0, 1 = uart1). - * @param u8Byte The byte to transmit. - * @return STD_OK on success, STD_NOK on failure. - */ -STD_tenuResult MCU_UART_enuSendByte(u8 u8Instance, u8 u8Byte); - -/** - * @brief Send a buffer of bytes (non-blocking, default). - * - * Starts an async transfer via DMA or UART TX ISR (per config). Returns - * immediately. The caller MUST keep the buffer valid until the callback - * fires or MCU_UART_bIsTxBusy() returns STD_FALSE. - * - * The TX-complete callback is invoked from interrupt context. + * The ring buffer is filled in the background by ISR or DMA. + * Returns immediately — STD_OK with the byte, or STD_NOK if empty. * * @param u8Instance UART instance index. - * @param pu8Data Pointer to buffer. Must not be NULL. Must stay valid. - * @param u16Length Number of bytes to transmit. - * @return STD_OK transfer started, - * STD_NULL_POINTER_ERROR if pu8Data is NULL, - * STD_NOK if a transfer is already in progress. + * @param pu8Byte Pointer to store the received byte. + * @return STD_OK byte read, + * STD_NULL_POINTER_ERROR if pu8Byte is NULL, + * STD_NOK if ring buffer is empty. */ -STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Length); +STD_tenuResult MCU_UART_enuReadByte(u8 u8Instance, u8 *pu8Byte); /** - * @brief Send a buffer of bytes (blocking). + * @brief Read up to u16MaxLength bytes from the RX ring buffer (non-blocking). * - * Blocks until all bytes are transmitted. Calls the TX-complete callback - * synchronously at the end (if configured, not NULL). + * Copies as many bytes as are currently available (up to u16MaxLength) + * into pu8Data. Reports the actual number of bytes read via pu16Read. + * Returns immediately even if fewer than u16MaxLength bytes are available. * - * @param u8Instance UART instance index. - * @param pu8Data Pointer to buffer. Must not be NULL. - * @param u16Length Number of bytes to transmit. - * @return STD_OK on success, - * STD_NULL_POINTER_ERROR if pu8Data is NULL, - * STD_NOK on failure. + * @param u8Instance UART instance index. + * @param pu8Data Pointer to output buffer. + * @param u16MaxLength Maximum bytes to read. + * @param pu16Read Pointer to store actual bytes read. Must not be NULL. + * @return STD_OK at least one byte read, + * STD_NULL_POINTER_ERROR if pu8Data or pu16Read is NULL, + * STD_NOK if ring buffer is empty (zero bytes read). */ -STD_tenuResult MCU_UART_enuSendBufferBlocking(u8 u8Instance, const u8 *pu8Data, u16 u16Length); +STD_tenuResult MCU_UART_enuReadBuffer(u8 u8Instance, u8 *pu8Data, u16 u16MaxLength, u16 *pu16Read); /** - * @brief Check if an async TX transfer is in progress. + * @brief Check if the RX ring buffer has data. * * @param u8Instance UART instance index. - * @return STD_TRUE if a non-blocking SendBuffer is still transmitting, - * STD_FALSE if the driver is idle and ready for a new transfer. - */ -STD_tBool MCU_UART_bIsTxBusy(u8 u8Instance); - -/** - * @brief Receive one byte (blocking). - * - * Waits until a byte is available in the internal ring buffer (filled - * by RX ISR or DMA in the background), then returns it. - * - * @param u8Instance UART instance index. - * @param pu8Byte Pointer to store the received byte. Must not be NULL. - * @return STD_OK byte received, - * STD_NULL_POINTER_ERROR if pu8Byte is NULL. - */ -STD_tenuResult MCU_UART_enuReceiveByte(u8 u8Instance, u8 *pu8Byte); - -/** - * @brief Receive a buffer of bytes (non-blocking, default). - * - * Registers a request for u16Length bytes. As bytes arrive in the ring - * buffer, they are copied to pu8Data. When all requested bytes have been - * collected, the RX callback fires (if configured, not STD_NULL). - * Returns immediately after registering the request. - * - * The caller MUST keep the buffer valid until the callback fires or - * MCU_UART_bIsRxBusy() returns STD_FALSE. - * - * @param u8Instance UART instance index. - * @param pu8Data Pointer to buffer to fill. Must not be NULL. - * @param u16Length Number of bytes to receive. - * @return STD_OK request registered, - * STD_NULL_POINTER_ERROR if pu8Data is NULL, - * STD_NOK if a receive request is already active. - */ -STD_tenuResult MCU_UART_enuReceiveBuffer(u8 u8Instance, u8 *pu8Data, u16 u16Length); - -/** - * @brief Receive a buffer of bytes (blocking). - * - * Blocks until u16Length bytes have been received from the ring buffer. - * Calls the RX callback at the end (if configured). - * - * @param u8Instance UART instance index. - * @param pu8Data Pointer to buffer to fill. Must not be NULL. - * @param u16Length Number of bytes to receive. - * @return STD_OK all bytes received, - * STD_NULL_POINTER_ERROR if pu8Data is NULL. - */ -STD_tenuResult MCU_UART_enuReceiveBufferBlocking(u8 u8Instance, u8 *pu8Data, u16 u16Length); - -/** - * @brief Check if the RX ring buffer has data waiting. - * - * @param u8Instance UART instance index. - * @return STD_TRUE if at least one byte is available, - * STD_FALSE if the ring buffer is empty. + * @return STD_TRUE if at least one byte available, STD_FALSE if empty. */ STD_tBool MCU_UART_bIsRxDataAvailable(u8 u8Instance); -/** - * @brief Check if an async ReceiveBuffer request is in progress. - * - * @param u8Instance UART instance index. - * @return STD_TRUE if a non-blocking ReceiveBuffer is still collecting, - * STD_FALSE if idle. - */ -STD_tBool MCU_UART_bIsRxBusy(u8 u8Instance); - #endif /* MCU_UART_H */ \ No newline at end of file diff --git a/src/MCU_UART/prg/MCU_UART_prg.c b/src/MCU_UART/prg/MCU_UART_prg.c index 4922e91..89b1138 100644 --- a/src/MCU_UART/prg/MCU_UART_prg.c +++ b/src/MCU_UART/prg/MCU_UART_prg.c @@ -1,13 +1,9 @@ /****************************************************************************** * File: MCU_UART_prg.c * Component: MCU_UART - * Description: Program (implementation) file for the MCU_UART driver. - * TX: blocking + non-blocking (DMA or ISR) with callback. - * RX: ISR or DMA fills a ring buffer in the background. - * ReceiveByte (blocking) reads from the ring buffer. - * ReceiveBuffer (non-blocking) registers a request — bytes - * are copied from ring buffer to caller's buffer as they - * arrive. ReceiveBufferBlocking waits until N bytes collected. + * Description: TX: blocking + non-blocking (DMA or ISR) with callback. + * RX: ISR or DMA fills ring buffer in background. ReadByte + * and ReadBuffer read from the buffer non-blocking. * * Layer: MCU (hardware abstraction) *****************************************************************************/ @@ -25,11 +21,7 @@ /* INSTANCE LOOKUP TABLE */ /* ------------------------------------------------------------------------ */ -static uart_inst_t * const apstrInstances[] = -{ - uart0, - uart1, -}; +static uart_inst_t * const apstrInstances[] = { uart0, uart1 }; /* ------------------------------------------------------------------------ */ /* RUNTIME STATE */ @@ -51,31 +43,17 @@ static void vCallTxCallback(u8 u8Instance) } } -static void vCallRxCallback(u8 u8Instance) -{ - STD_tpfCallbackFunc pfCbLoc = MCU_UART_astrConfig[u8Instance].pfRxCallback; - - if (pfCbLoc != STD_NULL) - { - pfCbLoc(); - } -} - -/** - * @brief Get the current RX ring buffer head position. - * In DMA mode, derived from the DMA write pointer. - * In ISR mode, read directly from the control struct. - */ +/** @brief Get current RX head. DMA mode derives from hw write pointer. */ static u16 u16GetRxHead(u8 u8Instance) { u16 u16HeadLoc; if (MCU_UART_astrConfig[u8Instance].enuRxAsyncMode == MCU_UART_ASYNC_DMA) { - s8 s8RxChLoc = strControl.as8RxDmaChannel[u8Instance]; - u32 u32WriteAddrLoc = (u32)dma_channel_hw_addr((u32)s8RxChLoc)->write_addr; - u32 u32BufferBaseLoc = (u32)(&strControl.aau8RxBuffer[u8Instance][0]); - u16HeadLoc = (u16)((u32WriteAddrLoc - u32BufferBaseLoc) & MCU_UART_RX_BUFFER_MASK); + s8 s8ChLoc = strControl.as8RxDmaChannel[u8Instance]; + u32 u32WrLoc = (u32)dma_channel_hw_addr((u32)s8ChLoc)->write_addr; + u32 u32BaseLoc = (u32)(&strControl.aau8RxBuffer[u8Instance][0]); + u16HeadLoc = (u16)((u32WrLoc - u32BaseLoc) & MCU_UART_RX_BUFFER_MASK); } else { @@ -85,52 +63,18 @@ static u16 u16GetRxHead(u8 u8Instance) return u16HeadLoc; } -/** - * @brief Try to fulfill an active async ReceiveBuffer request by copying - * available bytes from the ring buffer to the caller's buffer. - * Called from the RX ISR after new bytes land in the ring buffer, - * or from polling contexts. - */ -static void vFulfillRxRequest(u8 u8Instance) -{ - u16 u16HeadLoc = u16GetRxHead(u8Instance); - u16 u16TailLoc = strControl.au16RxTail[u8Instance]; - - /* Copy available bytes from ring buffer to request buffer */ - while ((u16HeadLoc != u16TailLoc) && - (strControl.au16RxReqIndex[u8Instance] < strControl.au16RxReqLength[u8Instance])) - { - strControl.apu8RxReqBuffer[u8Instance][strControl.au16RxReqIndex[u8Instance]] = - strControl.aau8RxBuffer[u8Instance][u16TailLoc]; - strControl.au16RxReqIndex[u8Instance]++; - u16TailLoc = (u16TailLoc + 1U) & MCU_UART_RX_BUFFER_MASK; - } - - /* Update tail — bytes have been consumed */ - strControl.au16RxTail[u8Instance] = u16TailLoc; - - /* Check if request is fully fulfilled */ - if (strControl.au16RxReqIndex[u8Instance] >= strControl.au16RxReqLength[u8Instance]) - { - strControl.abRxReqActive[u8Instance] = STD_FALSE; - vCallRxCallback(u8Instance); - } -} - /* ------------------------------------------------------------------------ */ /* TX DMA IRQ HANDLER (INSTANCE 0) */ /* ------------------------------------------------------------------------ */ static void vTxDmaIrqHandler0(void) { - s8 s8ChannelLoc = strControl.as8TxDmaChannel[MCU_UART_INSTANCE_0]; - u32 u32StatusLoc; - - u32StatusLoc = dma_channel_get_irq0_status((u32)s8ChannelLoc); + s8 s8ChLoc = strControl.as8TxDmaChannel[MCU_UART_INSTANCE_0]; + u32 u32StatusLoc = dma_channel_get_irq0_status((u32)s8ChLoc); if (u32StatusLoc != 0U) { - dma_irqn_acknowledge_channel(0, (u32)s8ChannelLoc); + dma_irqn_acknowledge_channel(0, (u32)s8ChLoc); strControl.abTxBusy[MCU_UART_INSTANCE_0] = STD_FALSE; vCallTxCallback((u8)MCU_UART_INSTANCE_0); } @@ -140,53 +84,31 @@ static void vTxDmaIrqHandler0(void) /* UART ISR HANDLER (RX + TX) */ /* ------------------------------------------------------------------------ */ -/** - * @brief Combined UART interrupt handler for RX and TX. - * - * RX: drains the RX FIFO into the ring buffer. If an async ReceiveBuffer - * request is active, copies bytes from ring buffer to the request buffer. - * Calls per-byte RX callback (ISR mode only). - * - * TX: fills the TX FIFO from the send buffer. When all bytes sent, disables - * TX interrupt and calls TX callback. - */ static void vUartIsrHandler(u8 u8Instance) { uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; /* --- RX: drain FIFO into ring buffer --- */ - STD_tBool bRxReadable = STD_FALSE; + STD_tBool bReadableLoc = (uart_is_readable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; - bRxReadable = (uart_is_readable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; - - while (bRxReadable == STD_TRUE) + while (bReadableLoc == STD_TRUE) { u8 u8ByteLoc = (u8)uart_getc(pstrUartLoc); u16 u16HeadLoc = strControl.au16RxHead[u8Instance]; - /* Push byte into ring buffer */ strControl.aau8RxBuffer[u8Instance][u16HeadLoc] = u8ByteLoc; strControl.au16RxHead[u8Instance] = (u16HeadLoc + 1U) & MCU_UART_RX_BUFFER_MASK; - bRxReadable = (uart_is_readable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + bReadableLoc = (uart_is_readable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; } - /* If an async ReceiveBuffer request is active, try to fulfill it */ - if (strControl.abRxReqActive[u8Instance] == STD_TRUE) - { - vFulfillRxRequest(u8Instance); - } - - /* --- TX: fill FIFO from buffer (if TX is active in ISR mode) --- */ + /* --- TX: fill FIFO from buffer (if TX ISR active) --- */ STD_tBool bTxBusyLoc = strControl.abTxBusy[u8Instance]; if (bTxBusyLoc == STD_TRUE) { - STD_tBool bFifoReady = STD_FALSE; - STD_tBool bDataLeft = STD_FALSE; - - bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; - bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; + STD_tBool bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + STD_tBool bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; while ((bFifoReady == STD_TRUE) && (bDataLeft == STD_TRUE)) { @@ -195,12 +117,11 @@ static void vUartIsrHandler(u8 u8Instance) strControl.au16TxIndex[u8Instance]++; bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; - bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; + bDataLeft = (strControl.au16TxIndex[u8Instance] < strControl.au16TxLength[u8Instance]) ? STD_TRUE : STD_FALSE; } if (bDataLeft == STD_FALSE) { - /* Disable TX interrupt, keep RX interrupt enabled */ uart_set_irqs_enabled(pstrUartLoc, false, true); strControl.abTxBusy[u8Instance] = STD_FALSE; vCallTxCallback(u8Instance); @@ -222,35 +143,23 @@ STD_tenuResult MCU_UART_enuInit(void) STD_tenuResult enuResultLoc = STD_OK; u8 u8IndexLoc; - /* Zero-initialize all control state */ for (u8IndexLoc = 0U; u8IndexLoc < (u8)MCU_UART_NUM_INSTANCES; u8IndexLoc++) { - /* TX */ strControl.apu8TxBuffer[u8IndexLoc] = STD_NULL; strControl.au16TxLength[u8IndexLoc] = 0U; strControl.au16TxIndex[u8IndexLoc] = 0U; strControl.abTxBusy[u8IndexLoc] = STD_FALSE; strControl.as8TxDmaChannel[u8IndexLoc] = -1; - - /* RX */ strControl.au16RxHead[u8IndexLoc] = 0U; strControl.au16RxTail[u8IndexLoc] = 0U; strControl.as8RxDmaChannel[u8IndexLoc] = -1; - - /* RX request */ - strControl.apu8RxReqBuffer[u8IndexLoc] = STD_NULL; - strControl.au16RxReqLength[u8IndexLoc] = 0U; - strControl.au16RxReqIndex[u8IndexLoc] = 0U; - strControl.abRxReqActive[u8IndexLoc] = STD_FALSE; } - /* Configure each UART instance */ for (u8IndexLoc = 0U; u8IndexLoc < (u8)MCU_UART_NUM_INSTANCES; u8IndexLoc++) { const MCU_UART_tstrConfig *pstrCfgLoc = &MCU_UART_astrConfig[u8IndexLoc]; uart_inst_t *pstrUartLoc = apstrInstances[u8IndexLoc]; - /* Basic peripheral setup */ uart_init(pstrUartLoc, pstrCfgLoc->u32BaudRate); gpio_set_function(pstrCfgLoc->u8TxPin, GPIO_FUNC_UART); gpio_set_function(pstrCfgLoc->u8RxPin, GPIO_FUNC_UART); @@ -259,13 +168,12 @@ STD_tenuResult MCU_UART_enuInit(void) pstrCfgLoc->enuStopBits, pstrCfgLoc->enuParity); - /* --- TX async setup --- */ + /* TX async setup */ if (pstrCfgLoc->enuTxAsyncMode == MCU_UART_ASYNC_DMA) { - s8 s8ChannelLoc = (s8)dma_claim_unused_channel(true); - strControl.as8TxDmaChannel[u8IndexLoc] = s8ChannelLoc; - - dma_channel_set_irq0_enabled((u32)s8ChannelLoc, true); + s8 s8ChLoc = (s8)dma_claim_unused_channel(true); + strControl.as8TxDmaChannel[u8IndexLoc] = s8ChLoc; + dma_channel_set_irq0_enabled((u32)s8ChLoc, true); if (u8IndexLoc == (u8)MCU_UART_INSTANCE_0) { @@ -274,13 +182,9 @@ STD_tenuResult MCU_UART_enuInit(void) } } - /* --- RX async setup --- */ + /* RX async setup */ if (pstrCfgLoc->enuRxAsyncMode == MCU_UART_ASYNC_DMA) { - /* DMA RX: continuously fills ring buffer using DMA ring wrap. - * The write address wraps at the buffer size boundary, so the - * DMA writes in circles. Application reads from the tail and - * derives the head from the DMA write pointer. */ s8 s8RxChLoc = (s8)dma_claim_unused_channel(true); strControl.as8RxDmaChannel[u8IndexLoc] = s8RxChLoc; @@ -302,16 +206,11 @@ STD_tenuResult MCU_UART_enuInit(void) } else { - /* ISR RX: enable UART RX interrupt. The combined handler - * (vUartIsrHandler) drains the FIFO into the ring buffer. */ if (u8IndexLoc == (u8)MCU_UART_INSTANCE_0) { irq_set_exclusive_handler(UART0_IRQ, vUart0IrqHandler); irq_set_enabled(UART0_IRQ, true); } - - /* Enable RX interrupt (second param). TX stays off until - * SendBuffer is called. */ uart_set_irqs_enabled(pstrUartLoc, false, true); } } @@ -333,7 +232,7 @@ STD_tenuResult MCU_UART_enuSendByte(u8 u8Instance, u8 u8Byte) } /* ========================================================================= */ -/* SEND BUFFER (NON-BLOCKING, DEFAULT) */ +/* SEND BUFFER (NON-BLOCKING) */ /* ========================================================================= */ STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16Length) @@ -359,42 +258,31 @@ STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16L if (enuModeLoc == MCU_UART_ASYNC_DMA) { - s8 s8ChannelLoc = strControl.as8TxDmaChannel[u8Instance]; + s8 s8ChLoc = strControl.as8TxDmaChannel[u8Instance]; uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; - dma_channel_config strDmaCfgLoc = dma_channel_get_default_config((u32)s8ChannelLoc); - channel_config_set_transfer_data_size(&strDmaCfgLoc, DMA_SIZE_8); - channel_config_set_read_increment(&strDmaCfgLoc, true); - channel_config_set_write_increment(&strDmaCfgLoc, false); - channel_config_set_dreq(&strDmaCfgLoc, uart_get_dreq(pstrUartLoc, true)); + dma_channel_config strCfgLoc = dma_channel_get_default_config((u32)s8ChLoc); + channel_config_set_transfer_data_size(&strCfgLoc, DMA_SIZE_8); + channel_config_set_read_increment(&strCfgLoc, true); + channel_config_set_write_increment(&strCfgLoc, false); + channel_config_set_dreq(&strCfgLoc, uart_get_dreq(pstrUartLoc, true)); dma_channel_configure( - (u32)s8ChannelLoc, - &strDmaCfgLoc, - &uart_get_hw(pstrUartLoc)->dr, - pu8Data, - u16Length, - true - ); + (u32)s8ChLoc, &strCfgLoc, + &uart_get_hw(pstrUartLoc)->dr, pu8Data, u16Length, true); } else { - /* ISR mode: kick-start by filling the FIFO */ uart_inst_t *pstrUartLoc = apstrInstances[u8Instance]; - STD_tBool bFifoReady = STD_FALSE; - STD_tBool bDataLeft = STD_FALSE; - - bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; - bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE; + STD_tBool bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; + STD_tBool bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE; while ((bFifoReady == STD_TRUE) && (bDataLeft == STD_TRUE)) { - uart_putc_raw(pstrUartLoc, - pu8Data[strControl.au16TxIndex[u8Instance]]); + uart_putc_raw(pstrUartLoc, pu8Data[strControl.au16TxIndex[u8Instance]]); strControl.au16TxIndex[u8Instance]++; - bFifoReady = (uart_is_writable(pstrUartLoc) != 0) ? STD_TRUE : STD_FALSE; - bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE; + bDataLeft = (strControl.au16TxIndex[u8Instance] < u16Length) ? STD_TRUE : STD_FALSE; } if (bDataLeft == STD_FALSE) @@ -404,7 +292,6 @@ STD_tenuResult MCU_UART_enuSendBuffer(u8 u8Instance, const u8 *pu8Data, u16 u16L } else { - /* Enable TX interrupt for remaining bytes. Keep RX on. */ uart_set_irqs_enabled(pstrUartLoc, true, true); } } @@ -432,7 +319,6 @@ STD_tenuResult MCU_UART_enuSendBufferBlocking(u8 u8Instance, const u8 *pu8Data, { uart_putc_raw(apstrInstances[u8Instance], pu8Data[u16IndexLoc]); } - vCallTxCallback(u8Instance); } @@ -449,10 +335,10 @@ STD_tBool MCU_UART_bIsTxBusy(u8 u8Instance) } /* ========================================================================= */ -/* RECEIVE BYTE (BLOCKING) */ +/* READ BYTE (NON-BLOCKING) */ /* ========================================================================= */ -STD_tenuResult MCU_UART_enuReceiveByte(u8 u8Instance, u8 *pu8Byte) +STD_tenuResult MCU_UART_enuReadByte(u8 u8Instance, u8 *pu8Byte) { STD_tenuResult enuResultLoc = STD_OK; @@ -462,96 +348,55 @@ STD_tenuResult MCU_UART_enuReceiveByte(u8 u8Instance, u8 *pu8Byte) } else { - u16 u16HeadLoc; - u16 u16TailLoc; + u16 u16HeadLoc = u16GetRxHead(u8Instance); + u16 u16TailLoc = strControl.au16RxTail[u8Instance]; - /* Spin-wait until the ring buffer has at least one byte. - * The ring buffer is filled in the background by the RX ISR or DMA, - * so this loop will unblock as soon as a byte arrives on the wire. */ - do + if (u16HeadLoc == u16TailLoc) { - u16HeadLoc = u16GetRxHead(u8Instance); - u16TailLoc = strControl.au16RxTail[u8Instance]; - } while (u16HeadLoc == u16TailLoc); - - /* Read one byte from the tail and advance */ - *pu8Byte = strControl.aau8RxBuffer[u8Instance][u16TailLoc]; - strControl.au16RxTail[u8Instance] = (u16TailLoc + 1U) & MCU_UART_RX_BUFFER_MASK; + enuResultLoc = STD_NOK; + } + else + { + *pu8Byte = strControl.aau8RxBuffer[u8Instance][u16TailLoc]; + strControl.au16RxTail[u8Instance] = (u16TailLoc + 1U) & MCU_UART_RX_BUFFER_MASK; + } } return enuResultLoc; } /* ========================================================================= */ -/* RECEIVE BUFFER (NON-BLOCKING, DEFAULT) */ +/* READ BUFFER (NON-BLOCKING) */ /* ========================================================================= */ -STD_tenuResult MCU_UART_enuReceiveBuffer(u8 u8Instance, u8 *pu8Data, u16 u16Length) +STD_tenuResult MCU_UART_enuReadBuffer(u8 u8Instance, u8 *pu8Data, u16 u16MaxLength, u16 *pu16Read) { STD_tenuResult enuResultLoc = STD_OK; - STD_tBool bActiveLocl = strControl.abRxReqActive[u8Instance]; - if (pu8Data == STD_NULL) - { - enuResultLoc = STD_NULL_POINTER_ERROR; - } - else if (bActiveLocl == STD_TRUE) - { - /* A previous receive request is still pending */ - enuResultLoc = STD_NOK; - } - else - { - /* Register the request */ - strControl.apu8RxReqBuffer[u8Instance] = pu8Data; - strControl.au16RxReqLength[u8Instance] = u16Length; - strControl.au16RxReqIndex[u8Instance] = 0U; - strControl.abRxReqActive[u8Instance] = STD_TRUE; - - /* Try to fulfill immediately from bytes already in the ring buffer. - * If enough bytes are available, the request completes synchronously - * and the callback fires here. Otherwise it completes later when - * the ISR pushes more bytes (ISR mode) or when the application - * polls again (DMA mode). */ - vFulfillRxRequest(u8Instance); - } - - return enuResultLoc; -} - -/* ========================================================================= */ -/* RECEIVE BUFFER (BLOCKING) */ -/* ========================================================================= */ - -STD_tenuResult MCU_UART_enuReceiveBufferBlocking(u8 u8Instance, u8 *pu8Data, u16 u16Length) -{ - STD_tenuResult enuResultLoc = STD_OK; - u16 u16IndexLoc = 0U; - - if (pu8Data == STD_NULL) + if ((pu8Data == STD_NULL) || (pu16Read == STD_NULL)) { enuResultLoc = STD_NULL_POINTER_ERROR; } else { - /* Read N bytes from the ring buffer, blocking until each is available */ - while (u16IndexLoc < u16Length) + u16 u16HeadLoc = u16GetRxHead(u8Instance); + u16 u16TailLoc = strControl.au16RxTail[u8Instance]; + u16 u16CountLoc = 0U; + + while ((u16HeadLoc != u16TailLoc) && (u16CountLoc < u16MaxLength)) { - u16 u16HeadLoc = u16GetRxHead(u8Instance); - u16 u16TailLoc = strControl.au16RxTail[u8Instance]; - - /* Copy all currently available bytes */ - while ((u16HeadLoc != u16TailLoc) && (u16IndexLoc < u16Length)) - { - pu8Data[u16IndexLoc] = strControl.aau8RxBuffer[u8Instance][u16TailLoc]; - u16TailLoc = (u16TailLoc + 1U) & MCU_UART_RX_BUFFER_MASK; - u16IndexLoc++; - } - - strControl.au16RxTail[u8Instance] = u16TailLoc; + pu8Data[u16CountLoc] = strControl.aau8RxBuffer[u8Instance][u16TailLoc]; + u16TailLoc = (u16TailLoc + 1U) & MCU_UART_RX_BUFFER_MASK; + u16CountLoc++; } - vCallRxCallback(u8Instance); + strControl.au16RxTail[u8Instance] = u16TailLoc; + *pu16Read = u16CountLoc; + + if (u16CountLoc == 0U) + { + enuResultLoc = STD_NOK; + } } return enuResultLoc; @@ -573,13 +418,4 @@ STD_tBool MCU_UART_bIsRxDataAvailable(u8 u8Instance) } return bResultLoc; -} - -/* ========================================================================= */ -/* RX BUSY CHECK */ -/* ========================================================================= */ - -STD_tBool MCU_UART_bIsRxBusy(u8 u8Instance) -{ - return strControl.abRxReqActive[u8Instance]; -} +} \ No newline at end of file diff --git a/src/MCU_UART/prg/MCU_UART_priv.h b/src/MCU_UART/prg/MCU_UART_priv.h index 2fc00bb..59178c1 100644 --- a/src/MCU_UART/prg/MCU_UART_priv.h +++ b/src/MCU_UART/prg/MCU_UART_priv.h @@ -1,10 +1,7 @@ /****************************************************************************** * File: MCU_UART_priv.h * Component: MCU_UART - * Description: Private header for the MCU_UART driver. - * Contains the internal control structure for per-instance - * runtime state (TX async progress, RX ring buffer, DMA - * channels), and the extern config array declaration. + * Description: Private header — control struct and extern config array. * * Layer: MCU (hardware abstraction) - internal use only *****************************************************************************/ @@ -15,35 +12,8 @@ #include "MCU_UART.h" #include "MCU_UART_cfg.h" -/* ------------------------------------------------------------------------ */ -/* CONFIG ARRAY (EXTERN) */ -/* ------------------------------------------------------------------------ */ - extern const MCU_UART_tstrConfig MCU_UART_astrConfig[MCU_UART_NUM_INSTANCES]; -/* ------------------------------------------------------------------------ */ -/* RUNTIME CONTROL STRUCTURE */ -/* ------------------------------------------------------------------------ */ - -/** - * @brief Internal runtime state for all UART instances. - * - * Each field is an array indexed by instance number (struct-of-arrays). - * - * TX fields: - * apu8TxBuffer — caller's buffer being transmitted (non-blocking) - * au16TxLength — total bytes to transmit - * au16TxIndex — bytes transmitted so far (ISR mode) - * abTxBusy — STD_TRUE while async TX is in progress - * as8TxDmaChannel — DMA channel for TX (-1 if ISR mode) - * - * RX fields: - * aau8RxBuffer — per-instance ring buffer for received bytes. - * Aligned to buffer size for DMA ring mode compatibility. - * au16RxHead — write index (ISR writes here, DMA updates via hw pointer) - * au16RxTail — read index (application reads from here) - * as8RxDmaChannel — DMA channel for RX (-1 if ISR mode) - */ typedef struct { /* TX state */ @@ -53,18 +23,12 @@ typedef struct STD_tBool abTxBusy[MCU_UART_NUM_INSTANCES]; s8 as8TxDmaChannel[MCU_UART_NUM_INSTANCES]; - /* RX ring buffer state */ + /* RX ring buffer — filled by ISR or DMA in the background */ u8 aau8RxBuffer[MCU_UART_NUM_INSTANCES][MCU_UART_RX_BUFFER_SIZE] __attribute__((aligned(MCU_UART_RX_BUFFER_SIZE))); u16 au16RxHead[MCU_UART_NUM_INSTANCES]; u16 au16RxTail[MCU_UART_NUM_INSTANCES]; s8 as8RxDmaChannel[MCU_UART_NUM_INSTANCES]; - - /* RX async request state (for non-blocking ReceiveBuffer) */ - u8 *apu8RxReqBuffer[MCU_UART_NUM_INSTANCES]; /**< caller's buffer */ - u16 au16RxReqLength[MCU_UART_NUM_INSTANCES]; /**< total bytes requested */ - u16 au16RxReqIndex[MCU_UART_NUM_INSTANCES]; /**< bytes fulfilled so far */ - STD_tBool abRxReqActive[MCU_UART_NUM_INSTANCES]; /**< is a request pending? */ } MCU_UART_tstrControl; -#endif /* MCU_UART_PRIV_H */ \ No newline at end of file +#endif /* MCU_UART_PRIV_H */ diff --git a/src/MCU_USB/inc/MCU_USB.h b/src/MCU_USB/inc/MCU_USB.h index 0947812..9bac727 100644 --- a/src/MCU_USB/inc/MCU_USB.h +++ b/src/MCU_USB/inc/MCU_USB.h @@ -1,12 +1,10 @@ /****************************************************************************** * File: MCU_USB.h * Component: MCU_USB - * Description: Public interface for the MCU USB driver component. - * This header exposes the functions and types that other - * components are allowed to use to send and receive data over - * the RP2040 USB-CDC (virtual serial port) interface. From the - * host computer's perspective, the Pico appears as a regular - * serial device (/dev/tty.usbmodem* on macOS, COMx on Windows). + * Description: Public interface for the MCU USB-CDC driver. + * TX: SendByte, SendBuffer (both fire-and-forget via putchar_raw). + * RX: background ring buffer drained lazily from the SDK's + * stdio layer. ReadByte / bIsRxDataAvailable read from it. * * Layer: MCU (hardware abstraction) *****************************************************************************/ @@ -14,76 +12,57 @@ #ifndef MCU_USB_H #define MCU_USB_H -/* STD_TYPES brings in the fixed-width integer typedefs (u8, u32) and the - * STD_tenuResult enum used to report success/failure from every function. */ #include "STD_TYPES.h" -#define MCU_USB_WAIT_FOR_CONNECTION_DISABLED 0U +#define MCU_USB_WAIT_FOR_CONNECTION_DISABLED 0U #define MCU_USB_WAIT_FOR_CONNECTION_ENABLED 1U + /* ------------------------------------------------------------------------ */ -/* PUBLIC API */ +/* TX PUBLIC API */ /* ------------------------------------------------------------------------ */ -/** - * @brief Initialize the USB-CDC interface. - * - * Sets up the RP2040 USB peripheral and the TinyUSB CDC device so the - * board enumerates as a virtual serial port on the host. If the config - * macro MCU_USB_WAIT_FOR_CONNECTION is MCU_USB_WAIT_FOR_CONNECTION_ENABLED, - * this function blocks until the host opens the port (so early bytes are - * not lost), subject to MCU_USB_CONNECTION_TIMEOUT_MS in MCU_USB_cfg.h. - * If the timeout elapses before the host connects, returns STD_NOK. - * - * Must be called exactly once, before any Send/Receive function. - * - * @return STD_OK on success (USB-CDC initialized, host connected if wait enabled), - * STD_NOK on init failure or connection timeout. - */ STD_tenuResult MCU_USB_enuInit(void); -/** - * @brief Send a single byte over USB-CDC. - * - * Blocks until the byte has been handed off to the USB stack or the - * transmit timeout (MCU_USB_TRANSMIT_TIMEOUT_MS) elapses. - * - * @param u8Byte The byte to transmit. - * @return STD_OK on success, - * STD_NOK on transmit failure or timeout. - */ STD_tenuResult MCU_USB_enuSendByte(u8 u8Byte); -/** - * @brief Send a buffer of bytes over USB-CDC. - * - * Transmits u32Length bytes starting at pu8Data. Blocks until all bytes - * are sent or the transmit timeout elapses. The buffer is not modified. - * - * @param pu8Data Pointer to the byte buffer to transmit. Must not be NULL. - * @param u16Length Number of bytes to transmit. - * @return STD_OK on success, - * STD_NULL_POINTER_ERROR if pu8Data is NULL, - * STD_NOK on transmit failure or timeout. - */ STD_tenuResult MCU_USB_enuSendBuffer(const u8 *pu8Data, u16 u16Length); -/** - * @brief Receive one byte over USB-CDC (blocking). - * - * Blocks until a byte arrives from the host serial monitor. - * - * @param pu8Byte Pointer to store the received byte. Must not be NULL. - * @return STD_OK byte received, - * STD_NULL_POINTER_ERROR if pu8Byte is NULL. - */ -STD_tenuResult MCU_USB_enuReceiveByte(u8 *pu8Byte); +/* ------------------------------------------------------------------------ */ +/* RX PUBLIC API */ +/* ------------------------------------------------------------------------ */ /** - * @brief Check if USB-CDC has data waiting to be read. + * @brief Read one byte from the USB RX ring buffer (non-blocking). * - * @return STD_TRUE if at least one byte is available, - * STD_FALSE if no data waiting. + * Internally drains any pending bytes from the SDK's stdio layer into + * the ring buffer before checking. Returns immediately. + * + * @param pu8Byte Pointer to store the received byte. + * @return STD_OK byte read, + * STD_NULL_POINTER_ERROR if pu8Byte is NULL, + * STD_NOK if no data available. + */ +STD_tenuResult MCU_USB_enuReadByte(u8 *pu8Byte); + +/** + * @brief Read up to u16MaxLength bytes from the USB RX ring buffer. + * + * @param pu8Data Output buffer. + * @param u16MaxLength Maximum bytes to read. + * @param pu16Read Actual bytes read. + * @return STD_OK at least one byte read, + * STD_NULL_POINTER_ERROR if pu8Data or pu16Read is NULL, + * STD_NOK if no data available. + */ +STD_tenuResult MCU_USB_enuReadBuffer(u8 *pu8Data, u16 u16MaxLength, u16 *pu16Read); + +/** + * @brief Check if USB RX data is available. + * + * Drains pending bytes from the SDK first, then checks the ring buffer. + * + * @return STD_TRUE if data available, STD_FALSE if empty. */ STD_tBool MCU_USB_bIsRxDataAvailable(void); -#endif /* MCU_USB_H */ +#endif /* MCU_USB_H */ \ No newline at end of file diff --git a/src/MCU_USB/prg/MCU_USB_prg.c b/src/MCU_USB/prg/MCU_USB_prg.c index 3fbb2ee..9256900 100644 --- a/src/MCU_USB/prg/MCU_USB_prg.c +++ b/src/MCU_USB/prg/MCU_USB_prg.c @@ -1,114 +1,139 @@ /****************************************************************************** * File: MCU_USB_prg.c * Component: MCU_USB - * Description: Program (implementation) file for the MCU_USB driver. - * Contains the actual implementations of the public functions - * declared in MCU_USB.h. Wraps the Pico SDK's USB-CDC / stdio - * facilities to provide a simple send/receive API for the - * virtual serial port exposed over the Pico's USB connection. + * Description: USB-CDC driver. TX via putchar_raw (fire-and-forget). + * RX via internal ring buffer, lazily drained from the SDK's + * stdio layer on every ReadByte / ReadBuffer / IsDataAvailable + * call. No separate RX task or interrupt needed — the SDK's + * TinyUSB background task fills stdio internally, and we pull + * from stdio into our ring buffer on demand. * * Layer: MCU (hardware abstraction) *****************************************************************************/ + #include "STD_TYPES.h" -#include "pico/stdio_usb.h" /* stdio_usb_init(), stdio_usb_connected() */ -#include "pico/stdio.h" /* putchar_raw() - writes one byte into the stdio driver chain */ -#include "pico/time.h" /* absolute_time_t, make_timeout_time_ms(), time_reached() */ +#include "pico/stdio_usb.h" +#include "pico/stdio.h" +#include "pico/time.h" + #include "MCU_USB.h" #include "MCU_USB_priv.h" #include "MCU_USB_cfg.h" +/* ------------------------------------------------------------------------ */ +/* RX RING BUFFER */ +/* ------------------------------------------------------------------------ */ + +/** @brief RX ring buffer size — must be power of 2. */ +#define USB_RX_BUFFER_SIZE_BITS 6U +#define USB_RX_BUFFER_SIZE (1U << USB_RX_BUFFER_SIZE_BITS) +#define USB_RX_BUFFER_MASK (USB_RX_BUFFER_SIZE - 1U) + +static u8 au8RxBuffer[USB_RX_BUFFER_SIZE]; +static u16 u16RxHead = 0U; +static u16 u16RxTail = 0U; + +/** + * @brief Drain any available bytes from the SDK's stdio into our ring buffer. + * + * Called lazily from ReadByte, ReadBuffer, and bIsRxDataAvailable. + * getchar_timeout_us(0) is non-blocking — returns PICO_ERROR_TIMEOUT (-1) + * immediately if no data. We keep pulling until the SDK has nothing left + * or our ring buffer is full. + */ +static void vDrainStdio(void) +{ + s32 s32ByteLoc; + u16 u16NextHeadLoc; + + s32ByteLoc = (s32)getchar_timeout_us(0); + + while (s32ByteLoc >= 0) + { + u16NextHeadLoc = (u16RxHead + 1U) & USB_RX_BUFFER_MASK; + + /* If the ring buffer is full, stop draining (oldest data preserved, + * newest data from SDK is lost). Caller should read faster. */ + if (u16NextHeadLoc == u16RxTail) + { + /* Buffer full — can't store this byte. Break out. */ + break; + } + + au8RxBuffer[u16RxHead] = (u8)s32ByteLoc; + u16RxHead = u16NextHeadLoc; + + s32ByteLoc = (s32)getchar_timeout_us(0); + } +} + +/* ========================================================================= */ +/* INIT */ +/* ========================================================================= */ + STD_tenuResult MCU_USB_enuInit(void) { STD_tenuResult enuResultLoc = STD_OK; STD_tBool bSdkInitSuccess = STD_FALSE; - /* Call the Pico SDK's USB-only stdio init. This brings up the TinyUSB - * device stack, registers the USB-CDC stdio driver, and starts the - * background task that services USB events. Returns true on success. */ bSdkInitSuccess = (stdio_usb_init() != 0) ? STD_TRUE : STD_FALSE; if (bSdkInitSuccess == STD_FALSE) - { - enuResultLoc = STD_NOK; /* Initialization failed */ - }else + { + enuResultLoc = STD_NOK; + } + else { #if MCU_USB_WAIT_FOR_CONNECTION == MCU_USB_WAIT_FOR_CONNECTION_ENABLED - /* Wait for the host to open the CDC port, with a timeout. */ absolute_time_t absTimeout = make_timeout_time_ms(MCU_USB_CONNECTION_TIMEOUT_MS); STD_tBool bHostOpen = STD_FALSE; STD_tBool bTimeoutReached = STD_FALSE; do { - /* Yield for 10 ms between checks. This serves two purposes: - * 1. Avoids burning 100% CPU on a busy-wait spin loop - * 2. Gives the TinyUSB background task time to process USB - * enumeration events — without yielding, the USB stack - * may not advance and the host connection is delayed. - * 10 ms matches the interval used by the Pico SDK's own - * stdio_usb_init() connection-wait implementation. */ sleep_ms(10); - - /* Update status variables — avoid function calls in the - * while condition for readability and debuggability */ bHostOpen = (stdio_usb_connected() != 0) ? STD_TRUE : STD_FALSE; bTimeoutReached = (time_reached(absTimeout) != 0) ? STD_TRUE : STD_FALSE; } while ((bHostOpen == STD_FALSE) && (bTimeoutReached == STD_FALSE)); - /* If we exited the loop because of timeout rather than a successful - * connection, report failure so the caller knows the host never - * opened the port within the configured window. */ if (bHostOpen == STD_FALSE) { enuResultLoc = STD_NOK; } #endif } - return enuResultLoc; /* Return the result */ + + return enuResultLoc; } +/* ========================================================================= */ +/* SEND BYTE */ +/* ========================================================================= */ + STD_tenuResult MCU_USB_enuSendByte(u8 u8Byte) { STD_tenuResult enuResultLoc = STD_OK; - /* putchar_raw is the stdio framework's "push one byte into the driver - * chain" primitive. It is declared as `int putchar_raw(int c)` because - * the C stdio family uses EOF (-1) as a sentinel return value. Passing - * u8Byte directly relies on the implicit widening conversion u8 -> int, - * which is always safe (every u8 value fits in an int) and deliberately - * keeps native C type names out of our code. - * - * Note on semantics: putchar_raw is fire-and-forget at this layer - it - * queues the byte into the USB stdio driver and returns immediately. - * The actual USB transfer happens in the TinyUSB background task. There - * is no way to detect a transmit failure from this call, so we always - * return STD_OK. When we need real delivery guarantees, we will upgrade - * this to tud_cdc_write_char + tud_cdc_write_flush. */ putchar_raw(u8Byte); return enuResultLoc; } +/* ========================================================================= */ +/* SEND BUFFER */ +/* ========================================================================= */ + STD_tenuResult MCU_USB_enuSendBuffer(const u8 *pu8Data, u16 u16Length) { STD_tenuResult enuResultLoc = STD_OK; u16 u16IndexLoc; - /* Guard against null pointer dereference. On the RP2040 (Cortex-M0+), - * reading address 0x00000000 does NOT fault — it silently reads from - * the beginning of flash (the vector table), which means the firmware - * would send garbage bytes over USB instead of crashing. The explicit - * check catches the mistake at the source with a clear error code. */ if (pu8Data == STD_NULL) { enuResultLoc = STD_NULL_POINTER_ERROR; } else { - /* Send each byte individually via putchar_raw. Same fire-and-forget - * semantics as MCU_USB_enuSendByte — bytes are queued into the USB - * stdio driver and transmitted by the TinyUSB background task. - * No per-byte error detection is possible at this layer. */ for (u16IndexLoc = 0U; u16IndexLoc < u16Length; u16IndexLoc++) { putchar_raw(pu8Data[u16IndexLoc]); @@ -119,21 +144,10 @@ STD_tenuResult MCU_USB_enuSendBuffer(const u8 *pu8Data, u16 u16Length) } /* ========================================================================= */ -/* RECEIVE BYTE (BLOCKING) */ +/* READ BYTE (NON-BLOCKING) */ /* ========================================================================= */ -/** - * @brief Internal cached-byte state for bIsRxDataAvailable. - * - * The Pico SDK has no "peek" function for USB-CDC — the only way to check - * if data is available is to try reading. If a read succeeds during - * bIsRxDataAvailable, the byte is cached here so ReceiveByte can return it - * without a second read. bHasCachedByte tracks whether the cache is valid. - */ -static STD_tBool bHasCachedByte = STD_FALSE; -static u8 u8CachedByte = 0U; - -STD_tenuResult MCU_USB_enuReceiveByte(u8 *pu8Byte) +STD_tenuResult MCU_USB_enuReadByte(u8 *pu8Byte) { STD_tenuResult enuResultLoc = STD_OK; @@ -143,25 +157,53 @@ STD_tenuResult MCU_USB_enuReceiveByte(u8 *pu8Byte) } else { - /* Check if bIsRxDataAvailable already cached a byte */ - if (bHasCachedByte == STD_TRUE) + /* Pull any pending data from SDK into our ring buffer */ + vDrainStdio(); + + if (u16RxHead == u16RxTail) { - *pu8Byte = u8CachedByte; - bHasCachedByte = STD_FALSE; + enuResultLoc = STD_NOK; } else { - /* Block until a byte arrives. getchar_timeout_us with 0 is - * non-blocking — loop until we get a real byte. PICO_ERROR_TIMEOUT - * is returned as a negative value when no data is available. */ - s32 s32ResultLoc; + *pu8Byte = au8RxBuffer[u16RxTail]; + u16RxTail = (u16RxTail + 1U) & USB_RX_BUFFER_MASK; + } + } - do - { - s32ResultLoc = (s32)getchar_timeout_us(0); - } while (s32ResultLoc < 0); + return enuResultLoc; +} - *pu8Byte = (u8)s32ResultLoc; +/* ========================================================================= */ +/* READ BUFFER (NON-BLOCKING) */ +/* ========================================================================= */ + +STD_tenuResult MCU_USB_enuReadBuffer(u8 *pu8Data, u16 u16MaxLength, u16 *pu16Read) +{ + STD_tenuResult enuResultLoc = STD_OK; + + if ((pu8Data == STD_NULL) || (pu16Read == STD_NULL)) + { + enuResultLoc = STD_NULL_POINTER_ERROR; + } + else + { + vDrainStdio(); + + u16 u16CountLoc = 0U; + + while ((u16RxHead != u16RxTail) && (u16CountLoc < u16MaxLength)) + { + pu8Data[u16CountLoc] = au8RxBuffer[u16RxTail]; + u16RxTail = (u16RxTail + 1U) & USB_RX_BUFFER_MASK; + u16CountLoc++; + } + + *pu16Read = u16CountLoc; + + if (u16CountLoc == 0U) + { + enuResultLoc = STD_NOK; } } @@ -176,24 +218,12 @@ STD_tBool MCU_USB_bIsRxDataAvailable(void) { STD_tBool bResultLoc = STD_FALSE; - if (bHasCachedByte == STD_TRUE) - { - /* Already have a cached byte from a previous check */ - bResultLoc = STD_TRUE; - } - else - { - /* Try a non-blocking read. If successful, cache the byte so - * the next ReceiveByte call can return it immediately. */ - s32 s32ResultLoc = (s32)getchar_timeout_us(0); + vDrainStdio(); - if (s32ResultLoc >= 0) - { - u8CachedByte = (u8)s32ResultLoc; - bHasCachedByte = STD_TRUE; - bResultLoc = STD_TRUE; - } + if (u16RxHead != u16RxTail) + { + bResultLoc = STD_TRUE; } return bResultLoc; -} \ No newline at end of file +} From b49c7f60bb21d14b2c3b6140be59cd0d45cfc358 Mon Sep 17 00:00:00 2001 From: Mohamed Salem Date: Mon, 13 Apr 2026 03:32:18 +0200 Subject: [PATCH 6/6] Add WS2812B LED support via PIO with rainbow and color commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cmake/cmake_config/mcu_config.cmake | 12 ++- cmake/cmake_config/sources_config.cmake | 10 ++ src/APP_CLSW/cfg/APP_CLSW_cfg.h | 8 ++ src/APP_CLSW/prg/APP_CLSW_prg.c | 137 +++++++++++++++++++----- src/APP_CLSW/prg/APP_CLSW_priv.h | 6 +- src/HAL_LED/cfg/HAL_LED_cfg.c | 19 ++++ src/HAL_LED/cfg/HAL_LED_cfg.h | 46 ++++++++ src/HAL_LED/inc/HAL_LED.h | 70 ++++++++++++ src/HAL_LED/prg/HAL_LED_prg.c | 117 ++++++++++++++++++++ src/HAL_LED/prg/HAL_LED_priv.h | 51 +++++++++ src/MCU_PIO/cfg/MCU_PIO_cfg.c | 53 +++++++++ src/MCU_PIO/cfg/MCU_PIO_cfg.h | 40 +++++++ src/MCU_PIO/inc/MCU_PIO.h | 107 ++++++++++++++++++ src/MCU_PIO/pio/ws2812.pio | 73 +++++++++++++ src/MCU_PIO/prg/MCU_PIO_prg.c | 115 ++++++++++++++++++++ src/MCU_PIO/prg/MCU_PIO_priv.h | 34 ++++++ src/SYS_ECU/prg/SYS_ECU.c | 4 + 17 files changed, 871 insertions(+), 31 deletions(-) create mode 100644 src/HAL_LED/cfg/HAL_LED_cfg.c create mode 100644 src/HAL_LED/cfg/HAL_LED_cfg.h create mode 100644 src/HAL_LED/inc/HAL_LED.h create mode 100644 src/HAL_LED/prg/HAL_LED_prg.c create mode 100644 src/HAL_LED/prg/HAL_LED_priv.h create mode 100644 src/MCU_PIO/cfg/MCU_PIO_cfg.c create mode 100644 src/MCU_PIO/cfg/MCU_PIO_cfg.h create mode 100644 src/MCU_PIO/inc/MCU_PIO.h create mode 100644 src/MCU_PIO/pio/ws2812.pio create mode 100644 src/MCU_PIO/prg/MCU_PIO_prg.c create mode 100644 src/MCU_PIO/prg/MCU_PIO_priv.h diff --git a/cmake/cmake_config/mcu_config.cmake b/cmake/cmake_config/mcu_config.cmake index b7a1dc9..d1d7837 100644 --- a/cmake/cmake_config/mcu_config.cmake +++ b/cmake/cmake_config/mcu_config.cmake @@ -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. diff --git a/cmake/cmake_config/sources_config.cmake b/cmake/cmake_config/sources_config.cmake index 7687f3a..7f91e45 100644 --- a/cmake/cmake_config/sources_config.cmake +++ b/cmake/cmake_config/sources_config.cmake @@ -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 diff --git a/src/APP_CLSW/cfg/APP_CLSW_cfg.h b/src/APP_CLSW/cfg/APP_CLSW_cfg.h index 5918948..3887116 100644 --- a/src/APP_CLSW/cfg/APP_CLSW_cfg.h +++ b/src/APP_CLSW/cfg/APP_CLSW_cfg.h @@ -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 */ \ No newline at end of file diff --git a/src/APP_CLSW/prg/APP_CLSW_prg.c b/src/APP_CLSW/prg/APP_CLSW_prg.c index 6ca66e0..e81d69f 100644 --- a/src/APP_CLSW/prg/APP_CLSW_prg.c +++ b/src/APP_CLSW/prg/APP_CLSW_prg.c @@ -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: \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; + } +} \ No newline at end of file diff --git a/src/APP_CLSW/prg/APP_CLSW_priv.h b/src/APP_CLSW/prg/APP_CLSW_priv.h index 17a94ea..b874a84 100644 --- a/src/APP_CLSW/prg/APP_CLSW_priv.h +++ b/src/APP_CLSW/prg/APP_CLSW_priv.h @@ -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 */ \ No newline at end of file diff --git a/src/HAL_LED/cfg/HAL_LED_cfg.c b/src/HAL_LED/cfg/HAL_LED_cfg.c new file mode 100644 index 0000000..2e90e93 --- /dev/null +++ b/src/HAL_LED/cfg/HAL_LED_cfg.c @@ -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, + }, +}; \ No newline at end of file diff --git a/src/HAL_LED/cfg/HAL_LED_cfg.h b/src/HAL_LED/cfg/HAL_LED_cfg.h new file mode 100644 index 0000000..06b1b47 --- /dev/null +++ b/src/HAL_LED/cfg/HAL_LED_cfg.h @@ -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 */ \ No newline at end of file diff --git a/src/HAL_LED/inc/HAL_LED.h b/src/HAL_LED/inc/HAL_LED.h new file mode 100644 index 0000000..536194e --- /dev/null +++ b/src/HAL_LED/inc/HAL_LED.h @@ -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 */ \ No newline at end of file diff --git a/src/HAL_LED/prg/HAL_LED_prg.c b/src/HAL_LED/prg/HAL_LED_prg.c new file mode 100644 index 0000000..272911e --- /dev/null +++ b/src/HAL_LED/prg/HAL_LED_prg.c @@ -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; +} \ No newline at end of file diff --git a/src/HAL_LED/prg/HAL_LED_priv.h b/src/HAL_LED/prg/HAL_LED_priv.h new file mode 100644 index 0000000..169757e --- /dev/null +++ b/src/HAL_LED/prg/HAL_LED_priv.h @@ -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 */ \ No newline at end of file diff --git a/src/MCU_PIO/cfg/MCU_PIO_cfg.c b/src/MCU_PIO/cfg/MCU_PIO_cfg.c new file mode 100644 index 0000000..39c6d34 --- /dev/null +++ b/src/MCU_PIO/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/src/MCU_PIO/cfg/MCU_PIO_cfg.h b/src/MCU_PIO/cfg/MCU_PIO_cfg.h new file mode 100644 index 0000000..f676eda --- /dev/null +++ b/src/MCU_PIO/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/src/MCU_PIO/inc/MCU_PIO.h b/src/MCU_PIO/inc/MCU_PIO.h new file mode 100644 index 0000000..600a9ec --- /dev/null +++ b/src/MCU_PIO/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/src/MCU_PIO/pio/ws2812.pio b/src/MCU_PIO/pio/ws2812.pio new file mode 100644 index 0000000..2c60c3d --- /dev/null +++ b/src/MCU_PIO/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/src/MCU_PIO/prg/MCU_PIO_prg.c b/src/MCU_PIO/prg/MCU_PIO_prg.c new file mode 100644 index 0000000..3867048 --- /dev/null +++ b/src/MCU_PIO/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/src/MCU_PIO/prg/MCU_PIO_priv.h b/src/MCU_PIO/prg/MCU_PIO_priv.h new file mode 100644 index 0000000..a50a9ce --- /dev/null +++ b/src/MCU_PIO/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 diff --git a/src/SYS_ECU/prg/SYS_ECU.c b/src/SYS_ECU/prg/SYS_ECU.c index 4ecd4b3..9e27508 100644 --- a/src/SYS_ECU/prg/SYS_ECU.c +++ b/src/SYS_ECU/prg/SYS_ECU.c @@ -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();