--- /dev/null
+/*
+ * Neopixel (WS2812B) Test
+ *
+ * (c) 2022 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 <string.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 B0 30
+#define B1 60
+
+byte neopixel_buf[] = {
+ // 128 cycles low: reset (has to be longer than in the datasheet)
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+
+ // G7 G6 G5 G4 G3 G2 G1 G0
+ // R7 R6 R5 R4 R3 R2 R1 R0
+ // B7 B6 B5 B4 B3 B2 B1 B0
+
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+ B0, B0, B0, B0, B0, B0, B0, B0,
+
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+ B0, B0, B0, B0, B1, B1, B1, B1,
+};
+
+static void neopixel_init(void)
+{
+ // 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_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);
+}
+
+/*** Main ***/
+
+int main(void)
+{
+ clock_init();
+ gpio_init();
+ usart_init();
+ tick_init();
+ neopixel_init();
+
+ debug_printf("Hello, world!\n");
+
+#if 0
+ for (int i=0; i<24*64; i++)
+ neopixel_buf[128+i] = B1;
+#endif
+
+ for (;;) {
+ // wait_for_interrupt();
+ debug_led(1);
+ for (int i=4; i<8; i++)
+ neopixel_buf[128+24*63+i] = B1;
+ delay_ms(100);
+ debug_led(0);
+ for (int i=4; i<8; i++)
+ neopixel_buf[128+24*63+i] = B0;
+ delay_ms(500);
+ }
+
+ return 0;
+}