From 8bca8c443017224f77aa7886e259e48aa12503c1 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Martin=20Mare=C5=A1?= Date: Sun, 21 Dec 2025 20:22:55 +0100 Subject: [PATCH] xmas-lights: init --- USB-IDS | 2 + xmas-lights/bootloader/Makefile | 7 + xmas-lights/bootloader/config.h | 24 ++ xmas-lights/daemon/Makefile | 28 ++ xmas-lights/daemon/led-strip.c | 269 +++++++++++++++++++ xmas-lights/firmware/Makefile | 10 + xmas-lights/firmware/README | 30 +++ xmas-lights/firmware/config.h | 14 + xmas-lights/firmware/interface.h | 11 + xmas-lights/firmware/main.c | 433 +++++++++++++++++++++++++++++++ 10 files changed, 828 insertions(+) create mode 100644 xmas-lights/bootloader/Makefile create mode 100644 xmas-lights/bootloader/config.h create mode 100644 xmas-lights/daemon/Makefile create mode 100644 xmas-lights/daemon/led-strip.c create mode 100644 xmas-lights/firmware/Makefile create mode 100644 xmas-lights/firmware/README create mode 100644 xmas-lights/firmware/config.h create mode 100644 xmas-lights/firmware/interface.h create mode 100644 xmas-lights/firmware/main.c diff --git a/USB-IDS b/USB-IDS index 8575939..104179f 100644 --- a/USB-IDS +++ b/USB-IDS @@ -19,5 +19,7 @@ USB IDs used by our gadgets 4242:0011 Heart monitor collar 4242:0012 Waiting Room controller bootloader 4242:0013 Waiting Room controller +4242:0014 X-mas LED Strip bootloader +4242:0015 X-mas LED Strip cafe:cafe KSP Space Alert thermometer cafe:caff KSP Space Alert accelerometer diff --git a/xmas-lights/bootloader/Makefile b/xmas-lights/bootloader/Makefile new file mode 100644 index 0000000..8204185 --- /dev/null +++ b/xmas-lights/bootloader/Makefile @@ -0,0 +1,7 @@ +ROOT=/home/mj/stm/home +BINARY=bootloader +OBJS= +LIB_OBJS=util-debug.o dfu-bootloader.o +MAX_SIZE=8192 + +include $(ROOT)/mk/bluepill.mk diff --git a/xmas-lights/bootloader/config.h b/xmas-lights/bootloader/config.h new file mode 100644 index 0000000..019fe50 --- /dev/null +++ b/xmas-lights/bootloader/config.h @@ -0,0 +1,24 @@ +/* + * X-mas Neopixel Strip Bootloader -- Configuration + * + * (c) 2025 Martin Mareš + */ + +// Processor clock + +#define CPU_CLOCK_MHZ 48 + +// Debugging port + +#define DEBUG_USART USART1 +#define DEBUG_LED_BLUEPILL + +// Bootloader settings + +#define BOOTLOADER_DEBUG +#define BOOTLOADER_APP_START 0x08002000 +#define BOOTLOADER_MFG_ID 0x4242 +#define BOOTLOADER_PROD_ID 0x0014 +#define BOOTLOADER_PROD_VERSION 0x0100 +#define BOOTLOADER_MFG_NAME "United Computer Wizards" +#define BOOTLOADER_PROD_NAME "X-mas Neopixel Strip (boot-loader)" diff --git a/xmas-lights/daemon/Makefile b/xmas-lights/daemon/Makefile new file mode 100644 index 0000000..5fffc46 --- /dev/null +++ b/xmas-lights/daemon/Makefile @@ -0,0 +1,28 @@ +PC=pkg-config +UCW_CFLAGS := $(shell $(PC) --cflags libucw) +UCW_LIBS := $(shell $(PC) --libs libucw) +USB_CFLAGS := $(shell $(PC) --cflags libusb-1.0) +USB_LIBS := $(shell $(PC) --libs libusb-1.0) +MOSQUITTO_CFLAGS := $(shell $(PC) --cflags libmosquitto) +MOSQUITTO_LIBS := $(shell $(PC) --libs libmosquitto) + +CFLAGS=-O2 -Wall -Wextra -Wno-sign-compare -Wno-parentheses -Wstrict-prototypes -Wmissing-prototypes $(UCW_CFLAGS) $(USB_CFLAGS) $(MOSQUITTO_CFLAGS) -g +LDLIBS=$(UCW_LIBS) $(USB_LIBS) $(MOSQUITTO_LIBS) -lpthread -lm + +all: led-strip + +led-strip: led-strip.o + +led-strip.o: led-strip.c ../firmware/interface.h + +install: led-strip + install led-strip /usr/local/sbin/ + +clean: + rm -f *.o led-strip + +deploy: + rsync -av --delete .. root@berry.lan:ksp/led-strip/ + ssh root@berry.lan 'cd ksp/led-strip/daemon/ && make install' + +.PHONY: all install clean upload diff --git a/xmas-lights/daemon/led-strip.c b/xmas-lights/daemon/led-strip.c new file mode 100644 index 0000000..73df90d --- /dev/null +++ b/xmas-lights/daemon/led-strip.c @@ -0,0 +1,269 @@ +/* + * Daemon for X-mas Led Strip + * + * (c) 2025 Martin Mares + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +typedef unsigned char byte; +typedef uint32_t u32; +typedef unsigned int uint; + +#include "../firmware/interface.h" + +static mtx_t light_mutex; +static cnd_t light_cond; +static byte lights[NPIX_NUM_LEDS][3]; +static bool light_refresh; + +/*** MQTT ***/ + +static struct mosquitto *mosq; +static bool mqtt_connected; + +static void mqtt_publish(const char *topic, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + + if (mqtt_connected) { + char m[256]; + int l = vsnprintf(m, sizeof(m), fmt, args); + int err = mosquitto_publish(mosq, NULL, topic, l, m, 0, true); + if (err != MOSQ_ERR_SUCCESS) + msg(L_ERROR, "Mosquitto: Publish failed, error=%d", err); + } + + va_end(args); +} + +static void mqtt_conn_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int status) +{ + if (!status) { + msg(L_DEBUG, "MQTT: Connection established"); + mqtt_connected = true; + mqtt_publish("status/led-strip", "ok"); + if (mosquitto_subscribe(mosq, NULL, "ksp/led-strip", 1) != MOSQ_ERR_SUCCESS) + die("Mosquitto: subscribe failed"); + } else if (mqtt_connected) { + msg(L_DEBUG, "MQTT: Connection lost"); + mqtt_connected = false; + } +} + +static void mqtt_log_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, int level, const char *message) +{ + msg(L_DEBUG, "MQTT(%d): %s", level, message); +} + +static void mqtt_msg_callback(struct mosquitto *mosq UNUSED, void *obj UNUSED, const struct mosquitto_message *m) +{ + char val[4096]; + if (m->payloadlen >= sizeof(val) - 1) { + msg(L_ERROR, "Invalid value for topic %s", m->topic); + return; + } + memcpy(val, m->payload, m->payloadlen); + val[m->payloadlen] = 0; + msg(L_DEBUG, "MQTT < %s %s", m->topic, val); + + if (strcmp(m->topic, "ksp/led-strip")) + return; + + mtx_lock(&light_mutex); + uint i = 0; + char *x = val; + while (*x) { + if (*x == ' ') + x++; + else { + char *end; + unsigned long val = strtoul(x, &end, 16); + if (end == x) + break; + if (i >= NPIX_NUM_LEDS) + break; + lights[i][0] = (val >> 16) & 0xff; + lights[i][1] = (val >> 8) & 0xff; + lights[i][2] = (val >> 0) & 0xff; + i++; + x = end; + } + } + if (i == 0) { + for (uint i=0; ilevels &= ~(1U << L_DEBUG); + + mtx_init(&light_mutex, mtx_plain); + cnd_init(&light_cond); + + mqtt_init(); + init_usb(); + + bool need_resend = true; + for (;;) { + if (!devh) { + msg(L_INFO, "Waiting for device to appear..."); + while (!devh) { + sleep(5); + open_device(); + } + need_resend = true; + } + + mtx_lock(&light_mutex); + while (!need_resend && !light_refresh) + cnd_wait(&light_cond, &light_mutex); + light_refresh = 0; + need_resend = 0; + mtx_unlock(&light_mutex); + + msg(L_DEBUG, "Sending USB packet"); + + uint batch = 63/3; + for (uint i=0; i < NPIX_NUM_LEDS; i += batch) { + byte packet[64]; + uint n = NPIX_NUM_LEDS - i; + if (n > batch) + n = batch; + packet[0] = i; + mtx_lock(&light_mutex); + for (uint j=0; j + */ + +// Processor clock + +#define CPU_CLOCK_MHZ 72 + +// Debugging port + +#define DEBUG_USART USART1 +#define DEBUG_LED_BLUEPILL diff --git a/xmas-lights/firmware/interface.h b/xmas-lights/firmware/interface.h new file mode 100644 index 0000000..7068e16 --- /dev/null +++ b/xmas-lights/firmware/interface.h @@ -0,0 +1,11 @@ +/* + * X-mas Neopixel Strip -- Interface Definitions + * + * (c) 2025 Martin Mareš + */ + +#define NPIX_USB_VENDOR 0x4242 +#define NPIX_USB_PRODUCT 0x0015 +#define NPIX_USB_VERSION 0x0100 + +#define NPIX_NUM_LEDS 150 diff --git a/xmas-lights/firmware/main.c b/xmas-lights/firmware/main.c new file mode 100644 index 0000000..2cb17f2 --- /dev/null +++ b/xmas-lights/firmware/main.c @@ -0,0 +1,433 @@ +/* + * X-mas Neopixel (WS2812B) Strip + * + * (c) 2025 Martin Mareš + */ + +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "interface.h" + +/*** Hardware init ***/ + +static void clock_init(void) +{ + rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]); + + rcc_periph_clock_enable(RCC_GPIOA); + rcc_periph_clock_enable(RCC_GPIOB); + rcc_periph_clock_enable(RCC_GPIOC); + rcc_periph_clock_enable(RCC_USART1); + rcc_periph_clock_enable(RCC_TIM4); + rcc_periph_clock_enable(RCC_DMA1); + + rcc_periph_reset_pulse(RST_GPIOA); + rcc_periph_reset_pulse(RST_GPIOB); + rcc_periph_reset_pulse(RST_GPIOC); + rcc_periph_reset_pulse(RST_USART1); + rcc_periph_reset_pulse(RST_TIM4); +} + +static void gpio_init(void) +{ + // PA9 = TXD1 for debugging console + // PA10 = RXD1 for debugging console + gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO9); + gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO10); + + // PC13 = BluePill LED + gpio_set_mode(GPIOC, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO13); + gpio_clear(GPIOC, GPIO13); + + // PB8 = data for Neopixel + gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN, GPIO8); +} + +static void usart_init(void) +{ + usart_set_baudrate(USART1, 115200); + usart_set_databits(USART1, 8); + usart_set_stopbits(USART1, USART_STOPBITS_1); + usart_set_mode(USART1, USART_MODE_TX); + usart_set_parity(USART1, USART_PARITY_NONE); + usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE); + + usart_enable(USART1); +} + +/*** System ticks ***/ + +static volatile u32 ms_ticks; + +void sys_tick_handler(void) +{ + ms_ticks++; +} + +static void tick_init(void) +{ + systick_set_frequency(1000, CPU_CLOCK_MHZ * 1000000); + systick_counter_enable(); + systick_interrupt_enable(); +} + +static void delay_ms(uint ms) +{ + u32 start_ticks = ms_ticks; + while (ms_ticks - start_ticks < ms) + ; +} + +/*** Neopixels ***/ + +#define NPIX_PERIOD 90 // timer runs on 72 MHz, so 90 periods = 1250 ns +#define NPIX_RESET 10 // length of reset pulse in LED slots + // The chip needs longer reset pulse than documented. +#define B0 30 +#define B1 60 + +static byte neopixel_leds[NPIX_NUM_LEDS][3]; +static byte neopixel_buf[2*24]; +static uint neopixel_index; + +static void neopixel_set(uint led, byte r, byte g, byte b) +{ + cm_disable_interrupts(); + neopixel_leds[led][0] = r; + neopixel_leds[led][1] = g; + neopixel_leds[led][2] = b; + cm_enable_interrupts(); +} + +static void neopixel_init(void) +{ + for (uint i=0; i < NPIX_NUM_LEDS; i++) + neopixel_set(i, 7, 7, 7); + + // neopixel_buf is zero-initialized, which is a start of the reset sequence + memset(neopixel_buf, 0, sizeof(neopixel_buf)); + neopixel_index = NPIX_NUM_LEDS; + + // TIM4 update is connected to DMA1 channel 7 + + // FIXME: Strange programming sequence as specified in manual + + dma_channel_reset(DMA1, 7); + + dma_set_peripheral_address(DMA1, 7, (u32) &TIM_CCR3(TIM4)); + dma_set_memory_address(DMA1, 7, (u32) neopixel_buf); + dma_set_number_of_data(DMA1, 7, ARRAY_SIZE(neopixel_buf)); + dma_set_priority(DMA1, 7, DMA_CCR_PL_VERY_HIGH); + + dma_set_read_from_memory(DMA1, 7); + dma_enable_circular_mode(DMA1, 7); + + dma_set_memory_size(DMA1, 7, DMA_CCR_MSIZE_8BIT); + dma_enable_memory_increment_mode(DMA1, 7); + + dma_set_peripheral_size(DMA1, 7, DMA_CCR_PSIZE_16BIT); + dma_disable_peripheral_increment_mode(DMA1, 7); + + dma_enable_half_transfer_interrupt(DMA1, 7); + dma_enable_transfer_complete_interrupt(DMA1, 7); + nvic_enable_irq(NVIC_DMA1_CHANNEL7_IRQ); + + dma_enable_channel(DMA1, 7); + + timer_set_prescaler(TIM4, 0); + timer_set_mode(TIM4, TIM_CR1_CKD_CK_INT, TIM_CR1_CMS_EDGE, TIM_CR1_DIR_UP); + timer_disable_preload(TIM4); + timer_set_period(TIM4, NPIX_PERIOD - 1); + + timer_set_oc_mode(TIM4, TIM_OC3, TIM_OCM_PWM1); + timer_set_oc_value(TIM4, TIM_OC3, 0); + timer_set_oc_polarity_high(TIM4, TIM_OC3); + timer_enable_oc_output(TIM4, TIM_OC3); + + timer_set_dma_on_update_event(TIM4); + TIM_DIER(TIM4) |= TIM_DIER_UDE; + + timer_enable_counter(TIM4); +} + +static inline void next_led(byte *buf) +{ + if (neopixel_index < NPIX_NUM_LEDS) { + byte *led = neopixel_leds[neopixel_index++]; + byte r = led[0], g = led[1], b = led[2]; + for (uint m=0x80; m; m >>= 1) + *buf++ = (g & m) ? B1 : B0; + for (uint m=0x80; m; m >>= 1) + *buf++ = (r & m) ? B1 : B0; + for (uint m=0x80; m; m >>= 1) + *buf++ = (b & m) ? B1 : B0; + } else { + for (uint i=0; i < 24; i++) + *buf++ = 0; + neopixel_index++; + if (neopixel_index == NPIX_NUM_LEDS + NPIX_RESET) + neopixel_index = 0; + } +} + +void dma1_channel7_isr(void) +{ + if (DMA1_ISR & DMA_ISR_HTIF7) { + // Half transfer + DMA1_IFCR |= DMA_IFCR_CHTIF7; + next_led(neopixel_buf); + } else if (DMA1_ISR & DMA_ISR_TCIF7) { + // Transfer completed + DMA1_IFCR |= DMA_IFCR_CTCIF7; + next_led(neopixel_buf + 24); + } +} + +/*** USB ***/ + +static usbd_device *usbd_dev; + +enum usb_string { + STR_MANUFACTURER = 1, + STR_PRODUCT, + STR_SERIAL, +}; + +static char usb_serial_number[13]; + +static const char *usb_strings[] = { + "United Computer Wizards", + "X-mas Neopixel Strip", + usb_serial_number, +}; + +static const struct usb_device_descriptor device = { + .bLength = USB_DT_DEVICE_SIZE, + .bDescriptorType = USB_DT_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0xFF, + .bDeviceSubClass = 0, + .bDeviceProtocol = 0, + .bMaxPacketSize0 = 64, + .idVendor = NPIX_USB_VENDOR, + .idProduct = NPIX_USB_PRODUCT, + .bcdDevice = NPIX_USB_VERSION, + .iManufacturer = STR_MANUFACTURER, + .iProduct = STR_PRODUCT, + .iSerialNumber = STR_SERIAL, + .bNumConfigurations = 1, +}; + +static const struct usb_endpoint_descriptor endpoints[] = {{ + // Bulk end-point for sending LED values + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = 0x01, + .bmAttributes = USB_ENDPOINT_ATTR_BULK, + .wMaxPacketSize = 64, + .bInterval = 1, +}}; + +static const struct usb_interface_descriptor iface = { + .bLength = USB_DT_INTERFACE_SIZE, + .bDescriptorType = USB_DT_INTERFACE, + .bInterfaceNumber = 0, + .bAlternateSetting = 0, + .bNumEndpoints = 1, + .bInterfaceClass = 0xFF, + .bInterfaceSubClass = 0, + .bInterfaceProtocol = 0, + .iInterface = 0, + .endpoint = endpoints, +}; + +static const struct usb_dfu_descriptor dfu_function = { + .bLength = sizeof(struct usb_dfu_descriptor), + .bDescriptorType = DFU_FUNCTIONAL, + .bmAttributes = USB_DFU_CAN_DOWNLOAD | USB_DFU_WILL_DETACH, + .wDetachTimeout = 255, + .wTransferSize = 1024, + .bcdDFUVersion = 0x0100, +}; + +static const struct usb_interface_descriptor dfu_iface = { + .bLength = USB_DT_INTERFACE_SIZE, + .bDescriptorType = USB_DT_INTERFACE, + .bInterfaceNumber = 1, + .bAlternateSetting = 0, + .bNumEndpoints = 0, + .bInterfaceClass = 0xFE, + .bInterfaceSubClass = 1, + .bInterfaceProtocol = 1, + .iInterface = 0, + + .extra = &dfu_function, + .extralen = sizeof(dfu_function), +}; + +static const struct usb_interface ifaces[] = {{ + .num_altsetting = 1, + .altsetting = &iface, +}, { + .num_altsetting = 1, + .altsetting = &dfu_iface, +}}; + +static const struct usb_config_descriptor config = { + .bLength = USB_DT_CONFIGURATION_SIZE, + .bDescriptorType = USB_DT_CONFIGURATION, + .wTotalLength = 0, + .bNumInterfaces = 2, + .bConfigurationValue = 1, + .iConfiguration = 0, + .bmAttributes = 0x80, + .bMaxPower = 100, // multiplied by 2 mA + .interface = ifaces, +}; + +static byte usb_configured; +static uint8_t usbd_control_buffer[64]; + +static void dfu_detach_complete(usbd_device *dev UNUSED, struct usb_setup_data *req UNUSED) +{ + // Reset to bootloader, which implements the rest of DFU + debug_printf("Switching to DFU\n"); + debug_flush(); + scb_reset_core(); +} + +static enum usbd_request_return_codes dfu_control_cb(usbd_device *dev UNUSED, + struct usb_setup_data *req, + uint8_t **buf UNUSED, + uint16_t *len UNUSED, + void (**complete)(usbd_device *dev, struct usb_setup_data *req)) +{ + if (req->bmRequestType != 0x21 || req->bRequest != DFU_DETACH) + return USBD_REQ_NOTSUPP; + + *complete = dfu_detach_complete; + return USBD_REQ_HANDLED; +} + +static byte usb_rx_buf[64]; +static bool got_first_message; + +static void ep01_cb(usbd_device *dev, uint8_t ep UNUSED) +{ + // We received a frame from the USB host + uint len = usbd_ep_read_packet(dev, 0x01, usb_rx_buf, sizeof(usb_rx_buf)); + debug_printf("USB: Host sent %u bytes\n", len); + byte *rx = usb_rx_buf; + if (len < 1) + return; + uint j = rx[0]; + for (uint i=1; i+3 <= len && j < NPIX_NUM_LEDS; i += 3) + neopixel_set(j++, rx[i], rx[i+1], rx[i+2]); + got_first_message = true; +} + +static void set_config_cb(usbd_device *dev, uint16_t wValue UNUSED) +{ + usbd_register_control_callback( + dev, + USB_REQ_TYPE_CLASS | USB_REQ_TYPE_INTERFACE, + USB_REQ_TYPE_TYPE | USB_REQ_TYPE_RECIPIENT, + dfu_control_cb); + usbd_ep_setup(dev, 0x01, USB_ENDPOINT_ATTR_BULK, 64, ep01_cb); + usb_configured = 1; +} + +static void reset_cb(void) +{ + debug_printf("USB: Reset\n"); + usb_configured = 0; +} + +static volatile bool usb_event_pending; + +void usb_lp_can_rx0_isr(void) +{ + /* + * We handle USB in the main loop to avoid race conditions between + * USB interrupts and other code. However, we need an interrupt to + * up the main loop from sleep. + * + * We set up only the low-priority ISR, because high-priority ISR handles + * only double-buffered bulk transfers and isochronous transfers. + */ + nvic_disable_irq(NVIC_USB_LP_CAN_RX0_IRQ); + usb_event_pending = 1; +} + +static void usb_init(void) +{ + // Simulate USB disconnect + gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_OPENDRAIN, GPIO11 | GPIO12); + gpio_clear(GPIOA, GPIO11 | GPIO12); + delay_ms(100); + + usbd_dev = usbd_init( + &st_usbfs_v1_usb_driver, + &device, + &config, + usb_strings, + ARRAY_SIZE(usb_strings), + usbd_control_buffer, + sizeof(usbd_control_buffer) + ); + usbd_register_reset_callback(usbd_dev, reset_cb); + usbd_register_set_config_callback(usbd_dev, set_config_cb); + usb_event_pending = 1; +} + +/*** Main ***/ + +int main(void) +{ + clock_init(); + gpio_init(); + usart_init(); + tick_init(); + neopixel_init(); + usb_init(); + + debug_printf("Hello, world!\n"); + + uint i=0; + u32 last_blink = 0; + + for (;;) { + if (ms_ticks - last_blink >= 100) { + last_blink = ms_ticks; + debug_led_toggle(); + if (!got_first_message) { + neopixel_set(i, 0, 0, 7); + i = (i+1) % NPIX_NUM_LEDS; + neopixel_set(i, 0, 255, 0); + } + } + if (usb_event_pending) { + usbd_poll(usbd_dev); + usb_event_pending = 0; + nvic_clear_pending_irq(NVIC_USB_LP_CAN_RX0_IRQ); + nvic_enable_irq(NVIC_USB_LP_CAN_RX0_IRQ); + } + wait_for_interrupt(); + } + + return 0; +} -- 2.47.3