diff --git a/pico1w/ampel/CMakeLists.txt b/pico1w/ampel/CMakeLists.txt new file mode 100644 index 0000000..e5bb780 --- /dev/null +++ b/pico1w/ampel/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.13) + +# Pico 1 W: RP2040 + CYW43439. +set(PICO_BOARD pico_w) + +include(pico_sdk_import.cmake) + +project(ampel C CXX ASM) + +pico_sdk_init() + +add_executable(ampel + ampel.c + pwm.c + hw_pwm.c + pio_pwm.c +) + +# PIO-Assembler: generiert pio_pwm.pio.h in den Build-Ordner. +pico_generate_pio_header(ampel ${CMAKE_CURRENT_LIST_DIR}/pio_pwm.pio) + +target_link_libraries(ampel + pico_stdlib + pico_cyw43_arch_none + hardware_pwm + hardware_pio + hardware_clocks +) + +# stdio ueber USB-CDC — praktisch fuers Debuggen ueber tio. +pico_enable_stdio_usb(ampel 1) +pico_enable_stdio_uart(ampel 0) + +pico_add_extra_outputs(ampel) diff --git a/pico1w/ampel/ampel.c b/pico1w/ampel/ampel.c new file mode 100644 index 0000000..304f109 --- /dev/null +++ b/pico1w/ampel/ampel.c @@ -0,0 +1,49 @@ +#include +#include "pico/stdlib.h" +#include "pico/cyw43_arch.h" +#include "pwm.h" + +// Alle 21 Kanaele synchron: +// 500 ms Ramp-Up, 500 ms voll an, 500 ms Ramp-Down, 500 ms aus. +static void set_all(uint16_t level) { + for (unsigned ch = 0; ch < PWM_TOTAL_COUNT; ch++) { + pwm_ch_set(ch, level); + } +} + +int main(void) { + stdio_init_all(); + + if (cyw43_arch_init()) { + printf("cyw43_arch_init fehlgeschlagen\n"); + return -1; + } + + pwm_all_init(); + + printf("ampel: alle %u Kanaele synchron Ramp-Up / An / Ramp-Down / Aus\n", + PWM_TOTAL_COUNT); + + const int ramp_steps = 50; + const int step_ms = 500 / ramp_steps; + + while (true) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true); + for (int i = 0; i <= ramp_steps; i++) { + set_all((uint16_t)((i * PWM_MAX_LEVEL) / ramp_steps)); + sleep_ms(step_ms); + } + + set_all(PWM_MAX_LEVEL); + sleep_ms(500); + + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false); + for (int i = ramp_steps; i >= 0; i--) { + set_all((uint16_t)((i * PWM_MAX_LEVEL) / ramp_steps)); + sleep_ms(step_ms); + } + + set_all(0); + sleep_ms(500); + } +} diff --git a/pico1w/ampel/build.sh b/pico1w/ampel/build.sh new file mode 100755 index 0000000..174d3a0 --- /dev/null +++ b/pico1w/ampel/build.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Baut die ampel-Firmware und flasht sie optional gleich auf den Pico. +# +# ./build.sh -> nur bauen +# ./build.sh -f -> bauen und flashen (Pico muss im BOOTSEL-Modus sein) +# ./build.sh --clean -> vorher build/ loeschen + +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +SDK_PATH="/home/irrlicht/Projekte/pico/reference/pico-sdk" +FLASH_SCRIPT="$HERE/../flash.sh" + +DO_FLASH=0 +DO_CLEAN=0 +for arg in "$@"; do + case "$arg" in + -f|--flash) DO_FLASH=1 ;; + --clean) DO_CLEAN=1 ;; + *) echo "Unbekanntes Argument: $arg" >&2; exit 1 ;; + esac +done + +cd "$HERE" + +if [ $DO_CLEAN -eq 1 ]; then + echo "Loesche build/ ..." + rm -rf build +fi + +# cmake nur bei fehlendem Build-Ordner explizit aufrufen — sonst reicht ninja, +# das erkennt CMakeLists.txt-Aenderungen selbst. +if [ ! -f build/build.ninja ]; then + cmake -S . -B build -G Ninja -DPICO_SDK_PATH="$SDK_PATH" +fi + +ninja -C build + +UF2="$HERE/build/ampel.uf2" +echo +echo "Fertig: $UF2 ($(stat -c%s "$UF2") Byte)" + +if [ $DO_FLASH -eq 1 ]; then + echo + "$FLASH_SCRIPT" "$UF2" +fi diff --git a/pico1w/ampel/hw_pwm.c b/pico1w/ampel/hw_pwm.c new file mode 100644 index 0000000..ae17103 --- /dev/null +++ b/pico1w/ampel/hw_pwm.c @@ -0,0 +1,28 @@ +#include "hw_pwm.h" +#include "pwm.h" +#include "hardware/pwm.h" +#include "hardware/gpio.h" +#include "hardware/clocks.h" + +void hw_pwm_init(void) { + // clkdiv so waehlen, dass (wrap+1) Ticks pro Periode 1 kHz ergeben. + // Bei 125 MHz sysclk und wrap=999 kommt exakt 125.0 raus. + float div = (float)clock_get_hz(clk_sys) / ((PWM_MAX_LEVEL + 1) * PWM_FREQ_HZ); + + for (unsigned pin = PWM_HW_FIRST_PIN; pin < PWM_HW_FIRST_PIN + PWM_HW_COUNT; pin++) { + gpio_set_function(pin, GPIO_FUNC_PWM); + } + + // 8 Slices decken GP0..GP15 komplett ab (je 2 Kanaele A/B pro Slice). + for (unsigned slice = 0; slice < 8; slice++) { + pwm_set_wrap(slice, PWM_MAX_LEVEL); + pwm_set_clkdiv(slice, div); + pwm_set_both_levels(slice, 0, 0); + pwm_set_enabled(slice, true); + } +} + +void hw_pwm_set(unsigned pin, uint16_t level) { + if (level > PWM_MAX_LEVEL) level = PWM_MAX_LEVEL; + pwm_set_chan_level(pwm_gpio_to_slice_num(pin), pwm_gpio_to_channel(pin), level); +} diff --git a/pico1w/ampel/hw_pwm.h b/pico1w/ampel/hw_pwm.h new file mode 100644 index 0000000..178a708 --- /dev/null +++ b/pico1w/ampel/hw_pwm.h @@ -0,0 +1,5 @@ +#pragma once +#include + +void hw_pwm_init(void); +void hw_pwm_set(unsigned pin, uint16_t level); diff --git a/pico1w/ampel/pico_sdk_import.cmake b/pico1w/ampel/pico_sdk_import.cmake new file mode 100644 index 0000000..65f8a6f --- /dev/null +++ b/pico1w/ampel/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# 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() + +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 () + +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") + +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 () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + 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/pico1w/ampel/pins.h b/pico1w/ampel/pins.h new file mode 100644 index 0000000..5d731a0 --- /dev/null +++ b/pico1w/ampel/pins.h @@ -0,0 +1,11 @@ +#pragma once + +// Erste 16 Kanaele: HW-PWM auf GP0..GP15 (8 Slices, je 2 Kanaele). +#define PWM_HW_FIRST_PIN 0 +#define PWM_HW_COUNT 16 + +// Kanaele 16..20: PIO-PWM auf GP16..GP20 (5 State Machines). +#define PWM_PIO_FIRST_PIN 16 +#define PWM_PIO_COUNT 5 + +#define PWM_TOTAL_COUNT (PWM_HW_COUNT + PWM_PIO_COUNT) diff --git a/pico1w/ampel/pio_pwm.c b/pico1w/ampel/pio_pwm.c new file mode 100644 index 0000000..ef36102 --- /dev/null +++ b/pico1w/ampel/pio_pwm.c @@ -0,0 +1,80 @@ +#include "pio_pwm.h" +#include "pwm.h" +#include "pins.h" +#include "hardware/pio.h" +#include "hardware/clocks.h" +#include "pico/error.h" +#include "pio_pwm.pio.h" + +// 5 State Machines koexistieren mit dem CYW43-Chip, der einen SM auf pio0 haelt. +// Wir claimen dynamisch: erst pio1 leerraeumen (4 SMs), dann eine SM aus pio0. +// Das Programm wird pro benutztem PIO-Block einmal geladen. +static struct { + PIO pio; + uint sm; + uint pin; +} g_channels[PWM_PIO_COUNT]; + +static int try_claim(PIO pio) { + // pio_claim_unused_sm(pio, false) liefert -1 statt panic bei Misserfolg. + return pio_claim_unused_sm(pio, false); +} + +void pio_pwm_init(void) { + int offset[2] = {-1, -1}; // Programm-Offset in pio0 / pio1, -1 = noch nicht geladen. + + for (unsigned i = 0; i < PWM_PIO_COUNT; i++) { + uint pin = PWM_PIO_FIRST_PIN + i; + + // Erst pio1 abgrasen (kein CYW43 dort), dann pio0. + PIO pio = pio1; + int sm = try_claim(pio1); + if (sm < 0) { + pio = pio0; + sm = try_claim(pio0); + if (sm < 0) { + panic("Keine freie PIO-SM fuer PWM-Kanal %u", i); + } + } + + unsigned idx = (pio == pio0) ? 0 : 1; + if (offset[idx] < 0) { + if (!pio_can_add_program(pio, &pio_pwm_program)) { + panic("PIO-Programm passt nicht mehr auf PIO%u", idx); + } + offset[idx] = (int)pio_add_program(pio, &pio_pwm_program); + } + + g_channels[i].pio = pio; + g_channels[i].sm = (uint)sm; + g_channels[i].pin = pin; + + // Cycle-Bilanz pro PWM-Periode: + // 3 Zyklen fuer pull/mov/mov (einmalig) + // + 3 Zyklen * (PWM_MAX_LEVEL + 1) fuer die Zaehlschleife + // Bei PWM_MAX_LEVEL=999 sind das 3003 PIO-Zyklen pro 1-kHz-Periode. + float cycles_per_period = 3.0f * (PWM_MAX_LEVEL + 1) + 3.0f; + float div = (float)clock_get_hz(clk_sys) / (cycles_per_period * PWM_FREQ_HZ); + + pio_pwm_program_init(pio, (uint)sm, (uint)offset[idx], pin, div); + + // Periode ins ISR laden: FIFO->OSR->ISR via SM-EXEC, ohne dass die + // Hauptschleife laeuft. Danach wird der eigentliche Level (Startwert 0) + // ins FIFO gelegt, den die Schleife via `pull noblock` in X uebernimmt. + pio_sm_set_enabled(pio, (uint)sm, false); + pio_sm_clear_fifos(pio, (uint)sm); + pio_sm_put_blocking(pio, (uint)sm, PWM_MAX_LEVEL); + pio_sm_exec(pio, (uint)sm, pio_encode_pull(false, false)); + pio_sm_exec(pio, (uint)sm, pio_encode_out(pio_isr, 32)); + + pio_sm_put_blocking(pio, (uint)sm, 0); + + pio_sm_set_enabled(pio, (uint)sm, true); + } +} + +void pio_pwm_set(unsigned index, uint16_t level) { + if (index >= PWM_PIO_COUNT) return; + if (level > PWM_MAX_LEVEL) level = PWM_MAX_LEVEL; + pio_sm_put_blocking(g_channels[index].pio, g_channels[index].sm, level); +} diff --git a/pico1w/ampel/pio_pwm.h b/pico1w/ampel/pio_pwm.h new file mode 100644 index 0000000..ceab047 --- /dev/null +++ b/pico1w/ampel/pio_pwm.h @@ -0,0 +1,5 @@ +#pragma once +#include + +void pio_pwm_init(void); +void pio_pwm_set(unsigned index, uint16_t level); diff --git a/pico1w/ampel/pio_pwm.pio b/pico1w/ampel/pio_pwm.pio new file mode 100644 index 0000000..0aeaff8 --- /dev/null +++ b/pico1w/ampel/pio_pwm.pio @@ -0,0 +1,30 @@ +; Klassisches PIO-PWM (Vorlage aus pico-examples). +; Pro Periode: pull noblock holt den neuen Level aus dem FIFO +; (oder recycelt den letzten aus X, wenn das FIFO leer ist). +; Y zaehlt von Periode runter auf 0; sobald X == Y wird der Pin per side-set hoch. +; Beim naechsten pull wird er wieder auf 0 gezogen. + +.program pio_pwm +.side_set 1 opt + + pull noblock side 0 + mov x, osr + mov y, isr +countloop: + jmp x!=y noset + jmp skip side 1 +noset: + nop +skip: + jmp y-- countloop + +% c-sdk { +static inline void pio_pwm_program_init(PIO pio, uint sm, uint offset, uint pin, float clkdiv) { + pio_gpio_init(pio, pin); + pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); + pio_sm_config c = pio_pwm_program_get_default_config(offset); + sm_config_set_sideset_pins(&c, pin); + sm_config_set_clkdiv(&c, clkdiv); + pio_sm_init(pio, sm, offset, &c); +} +%} diff --git a/pico1w/ampel/pwm.c b/pico1w/ampel/pwm.c new file mode 100644 index 0000000..3214e86 --- /dev/null +++ b/pico1w/ampel/pwm.c @@ -0,0 +1,17 @@ +#include "pwm.h" +#include "pins.h" +#include "hw_pwm.h" +#include "pio_pwm.h" + +void pwm_all_init(void) { + hw_pwm_init(); + pio_pwm_init(); +} + +void pwm_ch_set(unsigned channel, uint16_t level) { + if (channel < PWM_HW_COUNT) { + hw_pwm_set(PWM_HW_FIRST_PIN + channel, level); + } else if (channel < PWM_TOTAL_COUNT) { + pio_pwm_set(channel - PWM_HW_COUNT, level); + } +} diff --git a/pico1w/ampel/pwm.h b/pico1w/ampel/pwm.h new file mode 100644 index 0000000..e2ec910 --- /dev/null +++ b/pico1w/ampel/pwm.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include "pins.h" + +// Gemeinsame API fuer HW- und PIO-PWM. Kanalnummern: +// 0..15 -> HW-PWM auf GP0..GP15 +// 16..20 -> PIO-PWM auf GP16..GP20 +// Alle Kanaele laufen mit gleicher Frequenz und Aufloesung. + +#define PWM_FREQ_HZ 1000u +#define PWM_MAX_LEVEL 999u + +void pwm_all_init(void); +void pwm_ch_set(unsigned channel, uint16_t level);