Replace MicroPython runtime with Pico C + W5500 firmware.

Ship UDP RGB panel firmware (eight WS2812 strips) and drop the ESP32 Wi-Fi/MicroPython stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-01 21:52:52 +12:00
parent 45a38c05b7
commit f7beac2095
66 changed files with 1601 additions and 7376 deletions

89
firmware/CMakeLists.txt Normal file
View File

@@ -0,0 +1,89 @@
cmake_minimum_required(VERSION 3.13)
set(PICO_BOARD pico CACHE STRING "Board type")
include(pico_sdk_import.cmake)
project(portal_panel C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
include(FetchContent)
FetchContent_Declare(
iolibrary
GIT_REPOSITORY https://github.com/Wiznet/ioLibrary_Driver.git
GIT_TAG master
)
FetchContent_MakeAvailable(iolibrary)
set(IOLIB_DIR ${iolibrary_SOURCE_DIR})
add_executable(panel
main.c
wizchip_spi.c
timer.c
ws2812_led.cpp
WS2812.cpp
${IOLIB_DIR}/Ethernet/socket.c
${IOLIB_DIR}/Ethernet/wizchip_conf.c
${IOLIB_DIR}/Ethernet/W5500/w5500.c
${IOLIB_DIR}/Internet/DHCP/dhcp.c
)
pico_generate_pio_header(panel ${CMAKE_CURRENT_LIST_DIR}/WS2812.pio)
target_include_directories(panel PRIVATE
${CMAKE_CURRENT_LIST_DIR}
${IOLIB_DIR}/Ethernet
${IOLIB_DIR}/Internet/DHCP
)
target_compile_definitions(panel PRIVATE
_WIZCHIP_=W5500
)
if (DEFINED PANEL_ID_BUILD)
target_compile_definitions(panel PRIVATE PANEL_ID=${PANEL_ID_BUILD})
endif ()
if (DEFINED WS2812_PIN_BUILD)
target_compile_definitions(panel PRIVATE PIN_WS2812=${WS2812_PIN_BUILD})
endif ()
if (DEFINED WS2812_PIN1_BUILD)
target_compile_definitions(panel PRIVATE PIN_WS2812_1=${WS2812_PIN1_BUILD})
endif ()
if (DEFINED STATIC_IP_BUILD)
target_compile_definitions(panel PRIVATE "STATIC_IP={${STATIC_IP_BUILD}}")
endif ()
if (DEFINED STATIC_SN_BUILD)
target_compile_definitions(panel PRIVATE "STATIC_SN={${STATIC_SN_BUILD}}")
endif ()
if (DEFINED STATIC_GW_BUILD)
target_compile_definitions(panel PRIVATE "STATIC_GW={${STATIC_GW_BUILD}}")
endif ()
if (DEFINED STATIC_DNS_BUILD)
target_compile_definitions(panel PRIVATE "STATIC_DNS={${STATIC_DNS_BUILD}}")
endif ()
option(PORTAL_USE_DHCP "Use DHCP for W5500" OFF)
if (PORTAL_USE_DHCP)
target_compile_definitions(panel PRIVATE USE_DHCP=1)
else ()
target_compile_definitions(panel PRIVATE USE_DHCP=0)
endif ()
target_link_libraries(panel
pico_stdlib
hardware_spi
hardware_pio
hardware_dma
)
pico_enable_stdio_usb(panel 1)
pico_enable_stdio_uart(panel 0)
pico_add_extra_outputs(panel)

147
firmware/Makefile Normal file
View File

@@ -0,0 +1,147 @@
# Portal panel firmware — build and USB deploy
#
# make # build only
# make deploy # build + flash over USB (picotool)
#
# Options (also via repo-root .env — see .env.example):
# make deploy PANEL_ID=0
# make deploy NETWORK=dhcp
# make deploy IP=10.1.1.10 GATEWAY=10.1.1.1
# make deploy WS2812_PIN=26 WS2812_PIN1=27
#
# Environment:
# PICO_SDK_PATH default: ~/pico-sdk (fallback ~/pico/pico-sdk)
MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
-include $(MAKEFILE_DIR)../.env
-include $(MAKEFILE_DIR).env
ifeq ($(origin PICO_SDK_PATH), undefined)
ifneq ($(wildcard $(HOME)/pico-sdk/pico_sdk_init.cmake),)
PICO_SDK_PATH := $(HOME)/pico-sdk
else
PICO_SDK_PATH := $(HOME)/pico/pico-sdk
endif
endif
BUILD_DIR := build
UF2 := $(BUILD_DIR)/panel.uf2
CMAKE_STAMP := $(BUILD_DIR)/.cmake_stamp
JOBS ?= $(shell nproc 2>/dev/null || echo 4)
SERIAL_PORT ?= $(shell ls /dev/ttyACM* 2>/dev/null | head -1)
ifeq ($(SERIAL_PORT),)
SERIAL_PORT := /dev/ttyACM0
endif
SERIAL_BAUD ?= 115200
NETWORK ?= static
comma := ,
dot_to_csv = $(subst .,$(comma),$(1))
CMAKE_ARGS :=
ifdef PANEL_ID
CMAKE_ARGS += -DPANEL_ID_BUILD=$(PANEL_ID)
endif
ifdef WS2812_PIN
CMAKE_ARGS += -DWS2812_PIN_BUILD=$(WS2812_PIN)
endif
ifdef WS2812_PIN1
CMAKE_ARGS += -DWS2812_PIN1_BUILD=$(WS2812_PIN1)
endif
ifdef IP
CMAKE_ARGS += -DSTATIC_IP_BUILD=$(call dot_to_csv,$(IP))
endif
ifdef NETMASK
CMAKE_ARGS += -DSTATIC_SN_BUILD=$(call dot_to_csv,$(NETMASK))
endif
ifdef GATEWAY
CMAKE_ARGS += -DSTATIC_GW_BUILD=$(call dot_to_csv,$(GATEWAY))
endif
ifdef DNS
CMAKE_ARGS += -DSTATIC_DNS_BUILD=$(call dot_to_csv,$(DNS))
endif
ifeq ($(NETWORK),dhcp)
CMAKE_ARGS += -DPORTAL_USE_DHCP=ON
else ifeq ($(NO_DHCP),0)
CMAKE_ARGS += -DPORTAL_USE_DHCP=ON
else
CMAKE_ARGS += -DPORTAL_USE_DHCP=OFF
endif
CMAKE_STAMP_BODY := $(strip $(CMAKE_ARGS))
.PHONY: all build configure reconfigure deploy flash upload reset monitor clean help
all: build
help:
@echo "Targets:"
@echo " make build Build panel.uf2"
@echo " make deploy Build and flash over USB"
@echo " make reset USB reboot Pico"
@echo " make monitor USB serial console"
@echo " make clean Remove build directory"
build: configure
@echo "==> Building panel firmware"
# Host GCC 15 needs cstdint for pico-sdk pioasm
CXXFLAGS='-include cstdint' PICO_SDK_PATH="$(PICO_SDK_PATH)" cmake --build $(BUILD_DIR) -j$(JOBS)
@test -f "$(UF2)" || (echo "Error: build did not produce panel.uf2" && exit 1)
@echo "==> Build OK: $(UF2)"
configure:
@test -d "$(PICO_SDK_PATH)" || (echo "Error: pico-sdk not found at $(PICO_SDK_PATH)" && exit 1)
@command -v cmake >/dev/null || (echo "Error: cmake is required" && exit 1)
@mkdir -p $(BUILD_DIR)
@STAMP='$(CMAKE_STAMP_BODY)'; \
if [ ! -f "$(BUILD_DIR)/CMakeCache.txt" ] || [ ! -f "$(CMAKE_STAMP)" ] || [ "$$(cat $(CMAKE_STAMP))" != "$$STAMP" ]; then \
echo "==> Configuring cmake in $(BUILD_DIR) $(CMAKE_ARGS)"; \
CXXFLAGS='-include cstdint' PICO_SDK_PATH="$(PICO_SDK_PATH)" cmake -S . -B $(BUILD_DIR) $(CMAKE_ARGS); \
echo "$$STAMP" > "$(CMAKE_STAMP)"; \
fi
reconfigure:
@mkdir -p $(BUILD_DIR)
CXXFLAGS='-include cstdint' PICO_SDK_PATH="$(PICO_SDK_PATH)" cmake -S . -B $(BUILD_DIR) $(CMAKE_ARGS)
@echo '$(CMAKE_STAMP_BODY)' > $(CMAKE_STAMP)
deploy: build
@command -v picotool >/dev/null || { echo "Error: picotool not found"; exit 1; }
@echo "==> Uploading $(UF2) over USB"
@picotool load -x -f "$(UF2)"
flash: deploy
upload: deploy
reset:
@command -v picotool >/dev/null || { echo "Error: picotool not found"; exit 1; }
@picotool reboot -f
@i=0; last=; \
while [ $$i -lt 50 ]; do \
cur=$$(ls /dev/ttyACM* 2>/dev/null | head -1); \
if [ -n "$$cur" ] && [ "$$cur" = "$$last" ]; then \
sleep 0.5; \
if [ -e "$$cur" ]; then echo "==> Ready ($$cur)"; exit 0; fi; \
fi; \
last=$$cur; \
i=$$((i + 1)); sleep 0.2; \
done; \
echo "==> Warning: serial not ready yet"; exit 0
monitor:
@command -v picocom >/dev/null || { echo "Error: picocom not found"; exit 1; }
@SERIAL=; i=0; \
while [ $$i -lt 40 ]; do \
for d in $(SERIAL_PORT) $$(ls /dev/ttyACM* 2>/dev/null); do \
[ -n "$$d" ] && [ -e "$$d" ] || continue; \
SERIAL=$$d; break; \
done; \
[ -n "$$SERIAL" ] && [ -e "$$SERIAL" ] && break; \
i=$$((i + 1)); sleep 0.25; \
done; \
if [ -z "$$SERIAL" ] || [ ! -e "$$SERIAL" ]; then echo "Error: no serial port"; exit 1; fi; \
echo "==> monitor $$SERIAL"; \
exec picocom --baud $(SERIAL_BAUD) --flow n --echo --noreset "$$SERIAL"
clean:
rm -rf $(BUILD_DIR)

170
firmware/WS2812.cpp Normal file
View File

@@ -0,0 +1,170 @@
/* WS2812 driver — from https://github.com/ForsakenNGS/Pico_WS2812 (BSD-style)
* Pixel push uses DMA → PIO TX FIFO (DREQ-paced).
*/
#include "WS2812.hpp"
#include "WS2812.pio.h"
#include "hardware/clocks.h"
#include "hardware/dma.h"
#include "hardware/gpio.h"
#include <cstdlib>
WS2812::WS2812(uint pin, uint length, PIO pio, uint sm) {
initialize(pin, length, pio, sm, NONE, GREEN, RED, BLUE);
}
WS2812::WS2812(uint pin, uint length, PIO pio, uint sm, DataFormat format) {
switch (format) {
case FORMAT_RGB:
initialize(pin, length, pio, sm, NONE, RED, GREEN, BLUE);
break;
case FORMAT_GRB:
initialize(pin, length, pio, sm, NONE, GREEN, RED, BLUE);
break;
case FORMAT_WRGB:
initialize(pin, length, pio, sm, WHITE, RED, GREEN, BLUE);
break;
}
}
WS2812::~WS2812() {
if (dma_chan >= 0) {
dma_channel_abort(dma_chan);
dma_channel_unclaim((uint)dma_chan);
dma_chan = -1;
}
delete[] data;
}
void WS2812::initialize(uint pin, uint length, PIO pio, uint sm, DataByte b1, DataByte b2,
DataByte b3, DataByte b4) {
this->pin = pin;
this->length = length;
this->pio = pio;
this->sm = sm;
this->data = new uint32_t[length];
this->bytes[0] = b1;
this->bytes[1] = b2;
this->bytes[2] = b3;
this->bytes[3] = b4;
this->bits = (b1 == NONE ? 24 : 32);
this->dma_chan = (int)dma_claim_unused_channel(true);
/* One copy of the program per PIO block — shared by all strips on that PIO. */
static int pio0_offset = -1;
static int pio1_offset = -1;
int *cached = (pio == pio0) ? &pio0_offset : &pio1_offset;
if (*cached < 0) {
*cached = (int)pio_add_program(pio, &ws2812_program);
}
this->offset = (uint)*cached;
applyPinConfig();
}
void WS2812::applyPinConfig() {
pio_sm_set_enabled(pio, sm, false);
pio_gpio_init(pio, pin);
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
pio_sm_config c = ws2812_program_get_default_config(offset);
sm_config_set_sideset_pins(&c, pin);
sm_config_set_out_shift(&c, false, true, bits);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
int cycles_per_bit = ws2812_T1 + ws2812_T2 + ws2812_T3;
float div = (float)clock_get_hz(clk_sys) / (800000.f * (float)cycles_per_bit);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
void WS2812::setPin(uint new_pin) {
if (new_pin == pin) {
return;
}
dma_wait();
pio_sm_set_enabled(pio, sm, false);
gpio_set_function(pin, GPIO_FUNC_SIO);
gpio_set_dir(pin, GPIO_IN);
pin = new_pin;
applyPinConfig();
}
uint32_t WS2812::convertData(uint32_t rgbw) {
uint32_t result = 0;
for (uint b = 0; b < 4; b++) {
switch (bytes[b]) {
case RED:
result |= (rgbw & 0xFF);
break;
case GREEN:
result |= (rgbw & 0xFF00) >> 8;
break;
case BLUE:
result |= (rgbw & 0xFF0000) >> 16;
break;
case WHITE:
result |= (rgbw & 0xFF000000) >> 24;
break;
default:
break;
}
result <<= 8;
}
return result;
}
void WS2812::setPixelColor(uint index, uint32_t color) {
if (index < length) {
data[index] = convertData(color);
}
}
void WS2812::setPixelColor(uint index, uint8_t red, uint8_t green, uint8_t blue) {
setPixelColor(index, RGB(red, green, blue));
}
void WS2812::fill(uint32_t color) {
color = convertData(color);
for (uint i = 0; i < length; i++) {
data[i] = color;
}
}
void WS2812::dma_wait(void) {
if (dma_chan >= 0 && dma_channel_is_busy((uint)dma_chan)) {
dma_channel_wait_for_finish_blocking((uint)dma_chan);
}
}
void WS2812::show() {
show(length);
}
void WS2812::show(uint count) {
if (count > length) {
count = length;
}
if (count == 0 || dma_chan < 0 || data == nullptr) {
return;
}
dma_wait();
dma_channel_config c = dma_channel_get_default_config((uint)dma_chan);
channel_config_set_transfer_data_size(&c, DMA_SIZE_32);
channel_config_set_read_increment(&c, true);
channel_config_set_write_increment(&c, false);
channel_config_set_dreq(&c, pio_get_dreq(pio, sm, true));
dma_channel_configure((uint)dma_chan, &c,
&pio->txf[sm], /* write address */
data, /* read address */
count, /* transfer count */
true); /* start immediately */
dma_channel_wait_for_finish_blocking((uint)dma_chan);
}

46
firmware/WS2812.hpp Normal file
View File

@@ -0,0 +1,46 @@
#ifndef WS2812_H
#define WS2812_H
#include "hardware/pio.h"
#include "pico/types.h"
class WS2812 {
public:
enum DataByte { NONE = 0, RED = 1, GREEN = 2, BLUE = 3, WHITE = 4 };
enum DataFormat { FORMAT_RGB = 0, FORMAT_GRB = 1, FORMAT_WRGB = 2 };
WS2812(uint pin, uint length, PIO pio, uint sm);
WS2812(uint pin, uint length, PIO pio, uint sm, DataFormat format);
~WS2812();
static uint32_t RGB(uint8_t red, uint8_t green, uint8_t blue) {
return (uint32_t)(blue) << 16 | (uint32_t)(green) << 8 | (uint32_t)(red);
}
void setPixelColor(uint index, uint32_t color);
void setPixelColor(uint index, uint8_t red, uint8_t green, uint8_t blue);
void fill(uint32_t color);
void show();
void show(uint count);
void setPin(uint pin);
uint getPin() const { return pin; }
private:
uint pin;
uint length;
PIO pio;
uint sm;
uint offset;
uint bits;
int dma_chan;
DataByte bytes[4];
uint32_t *data;
void initialize(uint pin, uint length, PIO pio, uint sm, DataByte b1, DataByte b2,
DataByte b3, DataByte b4);
uint32_t convertData(uint32_t rgbw);
void applyPinConfig();
void dma_wait(void);
};
#endif

44
firmware/WS2812.pio Normal file
View File

@@ -0,0 +1,44 @@
;
; WS2812 PIO — from https://github.com/ForsakenNGS/Pico_WS2812
;
.program ws2812
.side_set 1
.define public T1 2
.define public T2 5
.define public T3 3
.lang_opt python sideset_init = pico.PIO.OUT_HIGH
.lang_opt python out_init = pico.PIO.OUT_HIGH
.lang_opt python out_shiftdir = 1
.wrap_target
bitloop:
out x, 1 side 0 [T3 - 1]
jmp !x send_zero side 1 [T1 - 1]
send_one:
jmp bitloop side 1 [T2 - 1]
send_zero:
nop side 0 [T2 - 1]
.wrap
% c-sdk {
#include "hardware/clocks.h"
static inline void ws2812_program_init(PIO pio, uint sm, uint offset, uint pin, float freq, uint bits) {
pio_gpio_init(pio, pin);
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
pio_sm_config c = ws2812_program_get_default_config(offset);
sm_config_set_sideset_pins(&c, pin);
sm_config_set_out_shift(&c, false, true, bits);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
int cycles_per_bit = ws2812_T1 + ws2812_T2 + ws2812_T3;
float div = (float)clock_get_hz(clk_sys) / (freq * (float)cycles_per_bit);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}

74
firmware/board_config.h Normal file
View File

@@ -0,0 +1,74 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H
/* W5500 on SPI1; eight WS2812 strips (portal UDP protocol) */
#define PIN_LED_STATUS 25
#define SPI_PORT spi1
#define SPI_CLK_MHZ 15
/* This adapter board: CS=13, RST=9 (some portal PCBs use CS=9 / RST=13) */
#define PIN_CS 13
#define PIN_SCK 10
#define PIN_MOSI 11
#define PIN_MISO 12
#define PIN_RST 9
/* Strip 0 / 1 overridable; remaining pins fixed in DEFAULT_STRIP_PINS */
#ifndef PIN_WS2812
#define PIN_WS2812 18
#endif
#ifndef PIN_WS2812_1
#define PIN_WS2812_1 19
#endif
/* Strip / buffer capacity. Pixel count comes from each UDP frame length. */
#ifndef MAX_LEDS
#define MAX_LEDS 512
#endif
#define LED_RGB_BYTES (MAX_LEDS * 3)
/* Eight WS2812 outputs (one PIO SM each on RP2040). */
#ifndef MAX_STRIPS
#define MAX_STRIPS 8
#endif
#define DEFAULT_STRIP_PINS \
{ PIN_WS2812, PIN_WS2812_1, 20, 21, 22, 26, 27, 28 }
/* W5500 socket assignments */
#define SOCKET_DHCP 0
#define SOCKET_UDP 1
/* UDP port (same as CircuitPython adapter / portal) */
#define UDP_PORT 50007
/* Compile-time panel index for filtered frames (04). 255 = accept all. */
#ifndef PANEL_ID
#define PANEL_ID 255
#endif
#ifndef USE_DHCP
#define USE_DHCP 0
#endif
#ifndef STATIC_IP
#define STATIC_IP_OCT4 ((PANEL_ID) == 255 ? 19u : (10u + (unsigned)(PANEL_ID)))
#define STATIC_IP {10, 1, 1, (uint8_t)STATIC_IP_OCT4}
#endif
#ifndef STATIC_SN
#define STATIC_SN {255, 255, 255, 0}
#endif
#ifndef STATIC_GW
#define STATIC_GW {10, 1, 1, 1}
#endif
#ifndef STATIC_DNS
#define STATIC_DNS {10, 1, 1, 1}
#endif
/* Locally administered; last byte = PANEL_ID (unique per panel 04). */
#define MAC_ADDR {0x02, 0x50, 0x52, 0x54, 0x4C, (uint8_t)PANEL_ID}
#endif

365
firmware/main.c Normal file
View File

@@ -0,0 +1,365 @@
/**
* Portal panel firmware — W5500 UDP + WS2812 (Pico SDK).
*
* UDP protocol (port 50007):
* - N×3 bytes: raw RGB → strip 0 (N = 1…MAX_LEDS)
* - 1 + N×3 bytes: strip_id (byte 0) + RGB
* strip_id 0…MAX_STRIPS-1 → that strip
* strip_id 255 → all active strips
* - 5 bytes "PIN\\0" + gpio: bind strip 0 to gpio
* - 8 bytes "PIN\\0" + strip + gpio + len_lo + len_hi: bind strip (len=0 → unset)
* - 4 bytes "SHOW": reserved (frames already show on receive)
*
* One Pico can drive up to MAX_STRIPS displays on separate GPIOs.
* Each strips pixel count comes from its own UDP frame length (strips may differ).
* MAX_LEDS is per-strip capacity.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "board_config.h"
#include "wizchip_conf.h"
#include "dhcp.h"
#include "socket.h"
#include "timer.h"
#include "wizchip_spi.h"
#include "ws2812_led.h"
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#define SOCKET_UDP 1
#define SOCKET_DHCP 0
#define ETH_BUF_SIZE 2048
#define DHCP_RETRY_MAX 10
static wiz_NetInfo g_net_info = {
.mac = MAC_ADDR,
.ip = STATIC_IP,
.sn = STATIC_SN,
.gw = STATIC_GW,
.dns = STATIC_DNS,
#if USE_DHCP
.dhcp = NETINFO_DHCP,
#else
.dhcp = NETINFO_STATIC,
#endif
};
static uint8_t g_eth_buf[ETH_BUF_SIZE];
static uint8_t g_pixel_buf[MAX_STRIPS][LED_RGB_BYTES];
static uint16_t g_frame_pixels[MAX_STRIPS];
static uint16_t g_strip_leds[MAX_STRIPS];
static uint8_t g_frame_dirty[MAX_STRIPS];
static volatile uint16_t g_ms_tick;
static uint8_t g_dhcp_ready;
static void dhcp_timer_cb(void) {
g_ms_tick++;
if (g_ms_tick >= 999) {
g_ms_tick = 0;
DHCP_time_handler();
}
}
static void dhcp_assign(void) {
getIPfromDHCP(g_net_info.ip);
getGWfromDHCP(g_net_info.gw);
getSNfromDHCP(g_net_info.sn);
getDNSfromDHCP(g_net_info.dns);
g_net_info.dhcp = NETINFO_DHCP;
network_initialize(g_net_info);
print_network_information(g_net_info);
g_dhcp_ready = 1;
}
static void dhcp_conflict(void) {
printf("DHCP conflict\n");
}
static int network_bring_up(void) {
#if USE_DHCP
uint8_t retries = 0;
DHCP_init(SOCKET_DHCP, g_eth_buf);
reg_dhcp_cbfunc(dhcp_assign, dhcp_assign, dhcp_conflict);
wizchip_1ms_timer_initialize(dhcp_timer_cb);
while (!g_dhcp_ready && retries < DHCP_RETRY_MAX) {
int8_t rv = DHCP_run();
if (rv == DHCP_IP_LEASED) {
g_dhcp_ready = 1;
break;
}
if (rv == DHCP_FAILED) {
retries++;
}
wizchip_delay_ms(250);
}
if (!g_dhcp_ready) {
printf("DHCP failed, using static IP\n");
g_net_info.dhcp = NETINFO_STATIC;
network_initialize(g_net_info);
print_network_information(g_net_info);
}
#else
network_initialize(g_net_info);
print_network_information(g_net_info);
#endif
return 0;
}
static int udp_socket_open(void) {
int8_t sn = socket(SOCKET_UDP, Sn_MR_UDP, UDP_PORT, 0);
if (sn != SOCKET_UDP) {
printf("UDP socket open failed: %d\n", sn);
return -1;
}
printf("UDP listening on port %d\n", UDP_PORT);
return 0;
}
static void led_boot_test(unsigned int strip) {
if (!ws2812_strip_active(strip)) {
return;
}
uint8_t one[3] = {255, 0, 0};
ws2812_set_rgb(strip, one, 1);
ws2812_show(strip);
sleep_ms(150);
one[0] = 0;
one[1] = 255;
ws2812_set_rgb(strip, one, 1);
ws2812_show(strip);
sleep_ms(150);
one[1] = 0;
one[2] = 255;
ws2812_set_rgb(strip, one, 1);
ws2812_show(strip);
sleep_ms(150);
one[2] = 0;
ws2812_set_rgb(strip, one, 1);
ws2812_show(strip);
}
static int buffer_frame(unsigned int strip, const uint8_t *rgb, uint16_t nbytes) {
if (strip >= MAX_STRIPS || !ws2812_strip_active(strip)) {
return -1;
}
if (nbytes == 0 || (nbytes % 3u) != 0) {
return -1;
}
uint16_t pixels = nbytes / 3u;
if (pixels > MAX_LEDS) {
pixels = MAX_LEDS;
nbytes = (uint16_t)(pixels * 3u);
}
memcpy(g_pixel_buf[strip], rgb, nbytes);
if (nbytes < LED_RGB_BYTES) {
memset(g_pixel_buf[strip] + nbytes, 0, LED_RGB_BYTES - nbytes);
}
g_frame_pixels[strip] = pixels;
g_frame_dirty[strip] = 1;
return 0;
}
static void buffer_frame_all(const uint8_t *rgb, uint16_t nbytes) {
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (ws2812_strip_active(s)) {
buffer_frame(s, rgb, nbytes);
}
}
}
static void show_dirty_frames(void) {
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (!g_frame_dirty[s]) {
continue;
}
ws2812_set_rgb(s, g_pixel_buf[s], g_frame_pixels[s]);
ws2812_show(s);
g_frame_dirty[s] = 0;
}
}
static int pin_is_reserved(uint8_t pin) {
switch (pin) {
case PIN_CS:
case PIN_SCK:
case PIN_MOSI:
case PIN_MISO:
case PIN_RST:
case PIN_LED_STATUS:
return 1;
default:
return pin > 29;
}
}
static void handle_pin_command(uint8_t strip, uint8_t pin, uint16_t leds) {
if (strip >= MAX_STRIPS) {
printf("PIN strip %u out of range (max %u)\n", (unsigned)strip, MAX_STRIPS);
return;
}
if (pin_is_reserved(pin)) {
printf("PIN GP%u rejected (reserved)\n", (unsigned)pin);
return;
}
if (leds > MAX_LEDS) {
printf("PIN leds %u clamped to max_leds=%u\n", (unsigned)leds, MAX_LEDS);
leds = MAX_LEDS;
}
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (s != strip && ws2812_strip_active(s) && ws2812_get_pin(s) == pin) {
printf("PIN GP%u: releasing strip %u\n", (unsigned)pin, s);
ws2812_release_strip(s);
g_strip_leds[s] = 0;
g_frame_pixels[s] = 0;
g_frame_dirty[s] = 0;
}
}
if (ws2812_set_pin(strip, pin) != 0) {
printf("PIN strip %u GP%u failed (no PIO SM?)\n", (unsigned)strip, (unsigned)pin);
return;
}
g_strip_leds[strip] = leds;
if (leds) {
printf("strip %u ws2812=GP%u leds=%u\n", (unsigned)strip,
(unsigned)ws2812_get_pin(strip), (unsigned)leds);
} else {
printf("strip %u ws2812=GP%u\n", (unsigned)strip, (unsigned)ws2812_get_pin(strip));
}
}
static void handle_packet(const uint8_t *data, int16_t len) {
if (len == 4 && memcmp(data, "SHOW", 4) == 0) {
return;
}
if (len == 5 && data[0] == 'P' && data[1] == 'I' && data[2] == 'N' && data[3] == 0) {
handle_pin_command(0, data[4], 0);
return;
}
if (len == 8 && data[0] == 'P' && data[1] == 'I' && data[2] == 'N' && data[3] == 0) {
uint16_t leds = (uint16_t)data[6] | ((uint16_t)data[7] << 8);
handle_pin_command(data[4], data[5], leds);
return;
}
if (len >= 3 && (len % 3) == 0) {
buffer_frame(0, data, (uint16_t)len);
return;
}
if (len >= 4 && ((len - 1) % 3) == 0) {
uint8_t id = data[0];
uint16_t nbytes = (uint16_t)(len - 1);
if (id == 255) {
buffer_frame_all(data + 1, nbytes);
return;
}
if (id < MAX_STRIPS) {
buffer_frame(id, data + 1, nbytes);
return;
}
if (id == (uint8_t)PANEL_ID) {
buffer_frame(0, data + 1, nbytes);
return;
}
}
}
static void poll_udp(void) {
while (1) {
uint16_t rx = getSn_RX_RSR(SOCKET_UDP);
if (rx == 0) {
return;
}
if (rx > ETH_BUF_SIZE) {
rx = ETH_BUF_SIZE;
}
uint8_t src_ip[4];
uint16_t src_port;
int16_t n = recvfrom(SOCKET_UDP, g_eth_buf, rx, src_ip, &src_port);
if (n <= 0) {
return;
}
handle_packet(g_eth_buf, n);
}
}
static void print_strip_map(void) {
printf("strips=%d", MAX_STRIPS);
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (ws2812_strip_active(s)) {
if (g_strip_leds[s]) {
printf(" [%u]=GP%u/%u", s, ws2812_get_pin(s), g_strip_leds[s]);
} else {
printf(" [%u]=GP%u", s, ws2812_get_pin(s));
}
}
}
printf("\n");
}
int main(void) {
stdio_init_all();
sleep_ms(2000);
gpio_init(PIN_LED_STATUS);
gpio_set_dir(PIN_LED_STATUS, GPIO_OUT);
printf("portal panel firmware (Pico SDK)\n");
printf("max_leds=%d max_strips=%d panel_id=%d mac=02:50:52:54:4C:%02X "
"ip=%u.%u.%u.%u %s\n",
MAX_LEDS, MAX_STRIPS, PANEL_ID, (unsigned)PANEL_ID, g_net_info.ip[0],
g_net_info.ip[1], g_net_info.ip[2], g_net_info.ip[3],
USE_DHCP ? "dhcp" : "static");
wizchip_spi_initialize();
wizchip_cris_initialize();
wizchip_reset();
wizchip_initialize();
wizchip_check();
ws2812_init(MAX_LEDS);
memset(g_pixel_buf, 0, sizeof(g_pixel_buf));
memset(g_frame_pixels, 0, sizeof(g_frame_pixels));
memset(g_strip_leds, 0, sizeof(g_strip_leds));
memset(g_frame_dirty, 0, sizeof(g_frame_dirty));
static const unsigned int default_pins[MAX_STRIPS] = DEFAULT_STRIP_PINS;
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (ws2812_set_pin(s, default_pins[s]) == 0) {
led_boot_test(s);
} else {
printf("default strip %u GP%u failed\n", s, default_pins[s]);
}
}
print_strip_map();
network_bring_up();
if (udp_socket_open() != 0) {
while (1) {
gpio_xor_mask(1u << PIN_LED_STATUS);
sleep_ms(100);
}
}
uint32_t heartbeat = 0;
while (1) {
#if USE_DHCP
DHCP_run();
#endif
poll_udp();
show_dirty_frames();
if (++heartbeat >= 500) {
gpio_xor_mask(1u << PIN_LED_STATUS);
heartbeat = 0;
}
sleep_ms(1);
}
}

View File

@@ -0,0 +1,54 @@
# This can be dropped into any project as a standalone CMake file
# SPDX-License-Identifier: BSD-3-Clause
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{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})
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})
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 Pico SDK from git")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "Location to download Pico 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)
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG master
)
if (NOT 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
"PICO_SDK_PATH is not set. Clone pico-sdk and export PICO_SDK_PATH, "
"or set PICO_SDK_FETCH_FROM_GIT=ON.")
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 "pico_sdk_init.cmake not found in ${PICO_SDK_PATH}")
endif ()
include(${PICO_SDK_INIT_CMAKE_FILE})

21
firmware/timer.c Normal file
View File

@@ -0,0 +1,21 @@
#include "timer.h"
static struct repeating_timer g_timer;
static void (*g_callback)(void);
void wizchip_1ms_timer_initialize(void (*callback)(void)) {
g_callback = callback;
add_repeating_timer_us(-1000, wizchip_1ms_timer_callback, NULL, &g_timer);
}
bool wizchip_1ms_timer_callback(struct repeating_timer *t) {
(void)t;
if (g_callback != NULL) {
g_callback();
}
return true;
}
void wizchip_delay_ms(uint32_t ms) {
sleep_ms(ms);
}

10
firmware/timer.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef PANEL_TIMER_H
#define PANEL_TIMER_H
#include "pico/stdlib.h"
void wizchip_1ms_timer_initialize(void (*callback)(void));
bool wizchip_1ms_timer_callback(struct repeating_timer *t);
void wizchip_delay_ms(uint32_t ms);
#endif

121
firmware/wizchip_spi.c Normal file
View File

@@ -0,0 +1,121 @@
/**
* W5500 SPI port for portal panel adapter (SPI1, GP913).
* Derived from WIZnet-PICO-C port/ioLibrary_Driver (BSD-3-Clause).
*/
#include <stdio.h>
#include "board_config.h"
#include "wizchip_conf.h"
#include "wizchip_spi.h"
#include "hardware/gpio.h"
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/critical_section.h"
#include "pico/stdlib.h"
static critical_section_t g_wizchip_cri_sec;
static inline void wizchip_select(void) {
gpio_put(PIN_CS, 0);
}
static inline void wizchip_deselect(void) {
gpio_put(PIN_CS, 1);
}
static uint8_t wizchip_read(void) {
uint8_t rx = 0;
uint8_t tx = 0xff;
spi_read_blocking(SPI_PORT, tx, &rx, 1);
return rx;
}
static void wizchip_write(uint8_t tx) {
spi_write_blocking(SPI_PORT, &tx, 1);
}
static void wizchip_critical_section_lock(void) {
critical_section_enter_blocking(&g_wizchip_cri_sec);
}
static void wizchip_critical_section_unlock(void) {
critical_section_exit(&g_wizchip_cri_sec);
}
void wizchip_reset(void) {
gpio_init(PIN_RST);
gpio_set_dir(PIN_RST, GPIO_OUT);
gpio_put(PIN_RST, 0);
sleep_ms(100);
gpio_put(PIN_RST, 1);
sleep_ms(100);
bi_decl(bi_1pin_with_name(PIN_RST, "W5500 RESET"));
}
void wizchip_spi_initialize(void) {
spi_init(SPI_PORT, SPI_CLK_MHZ * 1000 * 1000);
gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
bi_decl(bi_3pins_with_func(PIN_MISO, PIN_MOSI, PIN_SCK, GPIO_FUNC_SPI));
gpio_init(PIN_CS);
gpio_set_dir(PIN_CS, GPIO_OUT);
gpio_put(PIN_CS, 1);
bi_decl(bi_1pin_with_name(PIN_CS, "W5500 CS"));
}
void wizchip_cris_initialize(void) {
critical_section_init(&g_wizchip_cri_sec);
reg_wizchip_cris_cbfunc(wizchip_critical_section_lock, wizchip_critical_section_unlock);
}
void wizchip_initialize(void) {
wizchip_deselect();
reg_wizchip_cs_cbfunc(wizchip_select, wizchip_deselect);
reg_wizchip_spi_cbfunc(wizchip_read, wizchip_write);
uint8_t memsize[2][8] = {
{2, 2, 2, 2, 2, 2, 2, 2},
{2, 2, 2, 2, 2, 2, 2, 2},
};
if (ctlwizchip(CW_INIT_WIZCHIP, (void *)memsize) == -1) {
printf("W5500 init failed\n");
return;
}
uint8_t link = PHY_LINK_OFF;
do {
if (ctlwizchip(CW_GET_PHYLINK, (void *)&link) == -1) {
printf("PHY link unknown\n");
return;
}
} while (link == PHY_LINK_OFF);
}
void wizchip_check(void) {
if (getVERSIONR() != 0x04) {
printf("W5500 version mismatch: 0x%02x\n", getVERSIONR());
while (1) {
tight_loop_contents();
}
}
}
void network_initialize(wiz_NetInfo net_info) {
ctlnetwork(CN_SET_NETINFO, (void *)&net_info);
}
void print_network_information(wiz_NetInfo net_info) {
ctlnetwork(CN_GET_NETINFO, (void *)&net_info);
printf("MAC %02X:%02X:%02X:%02X:%02X:%02X\n",
net_info.mac[0], net_info.mac[1], net_info.mac[2],
net_info.mac[3], net_info.mac[4], net_info.mac[5]);
printf("IP %d.%d.%d.%d\n",
net_info.ip[0], net_info.ip[1], net_info.ip[2], net_info.ip[3]);
}

14
firmware/wizchip_spi.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef WIZCHIP_SPI_H
#define WIZCHIP_SPI_H
#include "wizchip_conf.h"
void wizchip_spi_initialize(void);
void wizchip_cris_initialize(void);
void wizchip_reset(void);
void wizchip_initialize(void);
void wizchip_check(void);
void network_initialize(wiz_NetInfo net_info);
void print_network_information(wiz_NetInfo net_info);
#endif

176
firmware/ws2812_led.cpp Normal file
View File

@@ -0,0 +1,176 @@
/* Multi-strip WS2812 wrapper around ForsakenNGS/Pico_WS2812 */
#include "ws2812_led.h"
#include "board_config.h"
#include "WS2812.hpp"
#include "hardware/pio.h"
#include "pico/stdlib.h"
typedef struct {
WS2812 *strip;
PIO pio;
uint sm;
unsigned int pin;
unsigned int lit;
unsigned int show_n;
uint8_t active;
} strip_slot_t;
static strip_slot_t s_slots[MAX_STRIPS];
static unsigned int s_max_leds;
static int claim_sm(PIO *pio_out, uint *sm_out) {
int sm = pio_claim_unused_sm(pio0, false);
if (sm >= 0) {
*pio_out = pio0;
*sm_out = (uint)sm;
return 0;
}
sm = pio_claim_unused_sm(pio1, false);
if (sm >= 0) {
*pio_out = pio1;
*sm_out = (uint)sm;
return 0;
}
return -1;
}
extern "C" void ws2812_init(unsigned int max_leds) {
s_max_leds = max_leds;
for (unsigned int i = 0; i < MAX_STRIPS; i++) {
s_slots[i].strip = nullptr;
s_slots[i].pin = 0;
s_slots[i].lit = 0;
s_slots[i].show_n = 0;
s_slots[i].active = 0;
}
}
extern "C" void ws2812_release_strip(unsigned int strip) {
if (strip >= MAX_STRIPS) {
return;
}
strip_slot_t *slot = &s_slots[strip];
if (!slot->active) {
return;
}
pio_sm_set_enabled(slot->pio, slot->sm, false);
gpio_set_function(slot->pin, GPIO_FUNC_SIO);
gpio_set_dir(slot->pin, GPIO_IN);
delete slot->strip;
slot->strip = nullptr;
pio_sm_unclaim(slot->pio, slot->sm);
slot->pin = 0;
slot->lit = 0;
slot->show_n = 0;
slot->active = 0;
}
extern "C" int ws2812_set_pin(unsigned int strip, unsigned int pin) {
if (strip >= MAX_STRIPS || pin > 29) {
return -1;
}
strip_slot_t *slot = &s_slots[strip];
if (slot->active && slot->pin == pin) {
return 0;
}
/* Steal pin from any other strip that already owns it. */
for (unsigned int s = 0; s < MAX_STRIPS; s++) {
if (s != strip && s_slots[s].active && s_slots[s].pin == pin) {
ws2812_release_strip(s);
}
}
if (slot->active) {
/* Tear down and rebuild on the same PIO SM — more reliable than setPin alone. */
PIO pio = slot->pio;
uint sm = slot->sm;
unsigned int old_pin = slot->pin;
pio_sm_set_enabled(pio, sm, false);
gpio_set_function(old_pin, GPIO_FUNC_SIO);
gpio_set_dir(old_pin, GPIO_IN);
delete slot->strip;
slot->strip = new WS2812(pin, s_max_leds, pio, sm, WS2812::FORMAT_GRB);
slot->pin = pin;
slot->lit = 0;
slot->show_n = 0;
return 0;
}
PIO pio;
uint sm;
if (claim_sm(&pio, &sm) != 0) {
return -1;
}
slot->strip = new WS2812(pin, s_max_leds, pio, sm, WS2812::FORMAT_GRB);
slot->pio = pio;
slot->sm = sm;
slot->pin = pin;
slot->lit = 0;
slot->show_n = 0;
slot->active = 1;
return 0;
}
extern "C" unsigned int ws2812_get_pin(unsigned int strip) {
if (strip >= MAX_STRIPS || !s_slots[strip].active) {
return 0;
}
return s_slots[strip].pin;
}
extern "C" int ws2812_strip_active(unsigned int strip) {
return strip < MAX_STRIPS && s_slots[strip].active;
}
extern "C" void ws2812_set_rgb(unsigned int strip, const uint8_t *rgb, unsigned int count) {
if (strip >= MAX_STRIPS || !s_slots[strip].active || rgb == nullptr) {
return;
}
strip_slot_t *slot = &s_slots[strip];
if (count > s_max_leds) {
count = s_max_leds;
}
unsigned int old = slot->lit;
unsigned int i = 0;
for (; i < count; i++) {
slot->strip->setPixelColor(i, rgb[i * 3 + 0], rgb[i * 3 + 1], rgb[i * 3 + 2]);
}
for (; i < old; i++) {
slot->strip->setPixelColor(i, 0, 0, 0);
}
slot->lit = count;
slot->show_n = count > old ? count : old;
}
extern "C" void ws2812_fill(unsigned int strip, uint8_t r, uint8_t g, uint8_t b) {
if (strip >= MAX_STRIPS || !s_slots[strip].active) {
return;
}
strip_slot_t *slot = &s_slots[strip];
slot->strip->fill(WS2812::RGB(r, g, b));
slot->lit = s_max_leds;
slot->show_n = s_max_leds;
}
extern "C" void ws2812_show(unsigned int strip) {
if (strip >= MAX_STRIPS || !s_slots[strip].active) {
return;
}
strip_slot_t *slot = &s_slots[strip];
unsigned int n = slot->show_n;
if (n == 0) {
n = 1;
}
slot->strip->show(n);
sleep_us(300);
slot->show_n = slot->lit;
}
extern "C" void ws2812_show_all(void) {
for (unsigned int i = 0; i < MAX_STRIPS; i++) {
if (s_slots[i].active) {
ws2812_show(i);
}
}
}

33
firmware/ws2812_led.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef WS2812_LED_H
#define WS2812_LED_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef MAX_STRIPS
#define MAX_STRIPS 8
#endif
/* Create strip slots (none active until ws2812_set_pin). max_leds = capacity each. */
void ws2812_init(unsigned int max_leds);
/* Bind strip to gpio (creates/rebinds PIO SM). strip = 0 … MAX_STRIPS-1. */
int ws2812_set_pin(unsigned int strip, unsigned int pin);
/* Free PIO SM + GPIO so another strip can take the pin. */
void ws2812_release_strip(unsigned int strip);
unsigned int ws2812_get_pin(unsigned int strip);
int ws2812_strip_active(unsigned int strip);
void ws2812_set_rgb(unsigned int strip, const uint8_t *rgb, unsigned int count);
void ws2812_fill(unsigned int strip, uint8_t r, uint8_t g, uint8_t b);
void ws2812_show(unsigned int strip);
void ws2812_show_all(void);
#ifdef __cplusplus
}
#endif
#endif