--- /dev/null
+/*
+ * Waiting Room Controller
+ *
+ * (c) 2025 Martin Mareš <mj@ucw.cz>
+ */
+
+#include "util.h"
+
+#include <libopencm3/cm3/cortex.h>
+#include <libopencm3/cm3/nvic.h>
+#include <libopencm3/cm3/systick.h>
+#include <libopencm3/cm3/scb.h>
+#include <libopencm3/stm32/dma.h>
+#include <libopencm3/stm32/gpio.h>
+#include <libopencm3/stm32/rcc.h>
+#include <libopencm3/stm32/timer.h>
+#include <libopencm3/stm32/usart.h>
+#include <libopencm3/usb/dfu.h>
+#include <libopencm3/usb/usbd.h>
+
+#include <string.h>
+
+#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_clock_enable(RCC_I2C2);
+
+ 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);
+ rcc_periph_reset_pulse(RST_I2C2);
+}
+
+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);
+
+ // PB10 and PB11 = I2C to display
+ gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_OPENDRAIN, GPIO10 | GPIO11);
+}
+
+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 ***/
+
+#if 0
+
+#define NPIX_PERIOD 90 // timer runs on 72 MHz, so 90 periods = 1250 ns
+#define NPIX_RESET 64 // the chip needs longer reset pulse than documented
+#define NPIX_B0 30
+#define NPIX_B1 60
+
+static byte led_rgb[NPIX_NUM_LEDS][3];
+
+static byte neopixel_buf[NPIX_RESET + NPIX_NUM_LEDS*24 + 1];
+static bool neopixel_dma_running;
+static bool neopixel_want_send;
+
+static void neopixel_run_dma(void)
+{
+ // When STM32 is programmed using ST-Link, the DMA sometimes keeps running.
+ dma_channel_reset(DMA1, 7);
+
+ // This order of register writes is recommended in the manual.
+ 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_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_clear_interrupt_flags(DMA1, 7, DMA_TCIF);
+ dma_enable_channel(DMA1, 7);
+ neopixel_dma_running = 1;
+}
+
+static void neopixel_recalc(void)
+{
+ byte *buf = neopixel_buf;
+ for (uint i = 0; i < NPIX_RESET; i++)
+ *buf++ = 0;
+ for (uint i = 0; i < NPIX_NUM_LEDS; i++) {
+ // The order is GRB, MSB first
+ for (uint m = 0x80; m; m >>= 1)
+ *buf++ = ((led_rgb[i][1] & m) ? NPIX_B1 : NPIX_B0);
+ for (uint m = 0x80; m; m >>= 1)
+ *buf++ = ((led_rgb[i][0] & m) ? NPIX_B1 : NPIX_B0);
+ for (uint m = 0x80; m; m >>= 1)
+ *buf++ = ((led_rgb[i][2] & m) ? NPIX_B1 : NPIX_B0);
+ }
+ *buf++ = NPIX_PERIOD;
+
+ neopixel_run_dma();
+ neopixel_want_send = 0;
+}
+
+static void neopixel_init(void)
+{
+ // TIM4 is always running and producing DMA requests on each update
+ // (connected to DMA1 channel 7). When we have something to send,
+ // the DMA is enabled.
+
+ 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;
+
+ led_rgb[NPIX_NUM_LEDS-1][1] = 0xaa;
+
+ timer_enable_counter(TIM4);
+ neopixel_recalc();
+}
+
+static bool neopixel_ready(void)
+{
+ if (!neopixel_dma_running)
+ return 1;
+
+ if (!dma_get_interrupt_flag(DMA1, 7, DMA_TCIF))
+ return 0;
+
+ dma_disable_channel(DMA1, 7);
+ neopixel_dma_running = 0;
+ return 1;
+}
+
+#endif
+
+/*** 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",
+ "Waiting Room Controller",
+ 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
+ .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 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);
+}
+
+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();
+
+ u32 last_blink = 0;
+ // u32 last_send = 0;
+
+ for (;;) {
+ if (ms_ticks - last_blink >= 300) {
+ debug_led_toggle();
+ last_blink = ms_ticks;
+ }
+
+ 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);
+ }
+
+#if 0
+ if (neopixel_ready()) {
+ if (neopixel_want_send || ms_ticks - last_send >= 100) {
+ // Re-send every 100 ms
+ neopixel_recalc();
+ last_send = ms_ticks;
+ }
+ }
+#endif
+
+ wait_for_interrupt();
+ }
+
+ return 0;
+}