From 6688a7bc5a56fcc806e732650b33539b859fd274 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Tue, 15 Apr 2025 14:51:30 -0700 Subject: [PATCH] neopixel: Support using SPI hardware to send data This change allows the use of a SPI bus to send correctly timed data to a string of neopixels. The idea is to take advantage of hardware to do the timing; this does not magically make these SPI devices. As such, it is only allowed on platforms that support hardware SPI. The main motivation is that the bitbang approach cannot be used directly from a Raspberry Pi secondary MCU as the bitbang timing is not precise enough. Using a SPI bus to send data to neopixels is a commonly used technique among other open source projects, and seems to work well in Kalico, too. On the devices I have tested (Raspberry Pi 5) the available SPI speeds work with the data patterns that I have hardcoded, but it is conceivable that it may be necessary to make the data configurable or automatically generated based on requested bus speed in order to support some neopixel-like devices. In order to support Neopixel strings longer than 8-10 pixels (depending on the number of colours), this also adds a `spidev_transfer_large` to the linux platform which uses a `size_t` data length instead of `uint8_t`. Signed-off-by: Russell Cloran --- README.md | 2 + docs/Config_Reference.md | 28 +++++++--- klippy/extras/neopixel.py | 40 ++++++++++++-- src/Kconfig | 9 +++ src/Makefile | 1 + src/linux/Kconfig | 1 + src/linux/gpio.h | 3 + src/linux/spidev.c | 7 +++ src/neopixel_spi.c | 112 ++++++++++++++++++++++++++++++++++++++ src/spicmds.c | 18 ++++++ src/spicmds.h | 3 + 11 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 src/neopixel_spi.c diff --git a/README.md b/README.md index 015f02374..5e1fca714 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,8 @@ See the [Kalico Additions document](https://docs.kalico.gg/Kalico_Additions.html - [extruder: cold_extrude](https://github.com/KalicoCrew/kalico/pull/750) +- [neopixel: Support using SPI hardware to send data](https://github.com/KalicoCrew/kalico/pull/771) + If you're feeling adventurous, take a peek at the extra features in the bleeding-edge-v2 branch [feature documentation](docs/Bleeding_Edge.md) and [feature configuration reference](docs/Config_Reference_Bleeding_Edge.md): diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index a08945124..441fe69a1 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3957,17 +3957,31 @@ Neopixel (aka WS2812) LED support (one may define any number of sections with a "neopixel" prefix). See the [command reference](G-Codes.md#led) for more information. -Note that the [linux mcu](RPi_microcontroller.md) implementation does -not currently support directly connected neopixels. The current design -using the Linux kernel interface does not allow this scenario because -the kernel GPIO interface is not fast enough to provide the required -pulse rates. - +If connected to a [linux mcu](RPi_microcontroller.md), a SPI bus must be used +to generate the data to send to the neopixels, as the GPIO interface is not +fast enough to provide the required pulse rates. They aren't really SPI +devices, but this allows offloading the required timing to hardware. By default +a SPI speed of 6MHz (6000000) is requested -- this should generate timings that +most neopixel-like devices support, but altering this may help you make some +devices work if the default timings are not compatible. Values in the range +4000000 - 10000000 are probably reasonable. ``` [neopixel my_neopixel] pin: # The pin connected to the neopixel. This parameter must be -# provided. +# provided if not using SPI bus. +spi_bus: +spi_speed: +# See the "common SPI settings" section for a description of the +# above parameters. Changing `spi_speed` will change the timing of +# pulses sent to the neopixel chain. On Raspberry Pi 4 and older the +# actual SPI bus speed is a fraction of the current core clock speed, +# which can vary, so you may need to set the core clock to a fixed +# speed. See https://github.com/raspberrypi/linux/issues/3381 for more. +cs_pin: +# If using a SPI bus this is required in order to determine which +# MCU the SPI bus is on, but the pin is not actually set directly by +# this code, and so should be specified as "mcu:None". #chain_count: # The number of Neopixel chips that are "daisy chained" to the # provided pin. The default is 1 (which indicates only a single diff --git a/klippy/extras/neopixel.py b/klippy/extras/neopixel.py index 4eb69be67..b7743ba3f 100644 --- a/klippy/extras/neopixel.py +++ b/klippy/extras/neopixel.py @@ -5,6 +5,8 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import logging +from . import bus + BACKGROUND_PRIORITY_CLOCK = 0x7FFFFFFF00000000 BIT_MAX_TIME = 0.000004 @@ -18,12 +20,20 @@ def __init__(self, config): self.printer = printer = config.get_printer() self.mutex = printer.get_reactor().mutex() # Configure neopixel - ppins = printer.lookup_object("pins") - pin_params = ppins.lookup_pin(config.get("pin")) - self.mcu = pin_params["chip"] + if config.get("pin", None) is not None: + ppins = printer.lookup_object("pins") + pin_params = ppins.lookup_pin(config.get("pin")) + self.mcu = pin_params["chip"] + self.pin = pin_params["pin"] + self.mcu.register_config_callback(self.build_config_bitbang) + else: + self.spi = bus.MCU_SPI_from_config( + config, 0, default_speed=6_000_000 + ) + self.mcu = self.spi.mcu + self.mcu.register_config_callback(self.build_config_spi) + self.oid = self.mcu.create_oid() - self.pin = pin_params["pin"] - self.mcu.register_config_callback(self.build_config) self.neopixel_update_cmd = self.neopixel_send_cmd = None # Build color map chain_count = config.getint("chain_count", 1, minval=1) @@ -54,7 +64,7 @@ def __init__(self, config): self.mcu.get_non_critical_reconnect_event_name(), self.send_data ) - def build_config(self): + def build_config_bitbang(self): bmt = self.mcu.seconds_to_clock(BIT_MAX_TIME) rmt = self.mcu.seconds_to_clock(RESET_MIN_TIME) self.mcu.add_config_cmd( @@ -73,6 +83,24 @@ def build_config(self): cq=cmd_queue, ) + def build_config_spi(self): + rmt = self.mcu.seconds_to_clock(RESET_MIN_TIME) + self.mcu.add_config_cmd( + "config_neopixel_spi oid=%d bus_oid=%s data_size=%d" + " reset_min_ticks=%d" + % (self.oid, self.spi.oid, len(self.color_data), rmt) + ) + cmd_queue = self.mcu.alloc_command_queue() + self.neopixel_update_cmd = self.mcu.lookup_command( + "neopixel_update_spi oid=%c pos=%hu data=%*s", cq=cmd_queue + ) + self.neopixel_send_cmd = self.mcu.lookup_query_command( + "neopixel_send_spi oid=%c", + "neopixel_result oid=%c success=%c", + oid=self.oid, + cq=cmd_queue, + ) + def update_color_data(self, led_state): color_data = self.color_data for cdidx, (lidx, cidx) in self.color_map: diff --git a/src/Kconfig b/src/Kconfig index 3b17b80a7..a66d43942 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -138,6 +138,10 @@ config WANT_NEOPIXEL bool depends on HAVE_GPIO default y +config WANT_NEOPIXEL_SPI + bool + depends on HAVE_GPIO_HARD_SPI + default y config WANT_PULSE_COUNTER bool depends on HAVE_GPIO @@ -238,6 +242,9 @@ config WANT_TMCUART config WANT_NEOPIXEL bool "Support 'neopixel' type LED control" depends on HAVE_GPIO +config WANT_NEOPIXEL_SPI + bool "Support 'neopixel' type LED control using hardware SPI" + depends on WANT_SPI && HAVE_GPIO_HARD_SPI config WANT_PULSE_COUNTER bool "Support measuring fan tachometer GPIO pins" depends on HAVE_GPIO @@ -349,6 +356,8 @@ config HAVE_GPIO_ADC bool config HAVE_GPIO_SPI bool +config HAVE_GPIO_HARD_SPI + bool config HAVE_GPIO_SDIO bool config HAVE_GPIO_I2C diff --git a/src/Makefile b/src/Makefile index a8254da8b..9c9666568 100644 --- a/src/Makefile +++ b/src/Makefile @@ -12,6 +12,7 @@ src-$(CONFIG_HAVE_GPIO_SDIO) += sdiocmds.c src-$(CONFIG_WANT_BUTTONS) += buttons.c src-$(CONFIG_WANT_TMCUART) += tmcuart.c src-$(CONFIG_WANT_NEOPIXEL) += neopixel.c +src-$(CONFIG_WANT_NEOPIXEL_SPI) += neopixel_spi.c src-$(CONFIG_WANT_PULSE_COUNTER) += pulse_counter.c src-$(CONFIG_WANT_ST7920) += lcd_st7920.c src-$(CONFIG_WANT_HD44780) += lcd_hd44780.c diff --git a/src/linux/Kconfig b/src/linux/Kconfig index 661f3c302..c9d57e98f 100644 --- a/src/linux/Kconfig +++ b/src/linux/Kconfig @@ -9,6 +9,7 @@ config LINUX_SELECT select HAVE_GPIO select HAVE_GPIO_ADC select HAVE_GPIO_SPI + select HAVE_GPIO_HARD_SPI select HAVE_GPIO_I2C select HAVE_GPIO_HARD_PWM diff --git a/src/linux/gpio.h b/src/linux/gpio.h index d72007c2c..405a8beae 100644 --- a/src/linux/gpio.h +++ b/src/linux/gpio.h @@ -1,6 +1,7 @@ #ifndef __LINUX_GPIO_H #define __LINUX_GPIO_H +#include // size_t #include // uint8_t struct gpio_out { @@ -35,6 +36,8 @@ struct spi_config spi_setup(uint32_t bus, uint8_t mode, uint32_t rate); void spi_prepare(struct spi_config config); void spi_transfer(struct spi_config config, uint8_t receive_data , uint8_t len, uint8_t *data); +void spi_transfer_large(struct spi_config config, uint8_t receive_data + , size_t len, uint8_t *data); struct gpio_pwm { int duty_fd, enable_fd; diff --git a/src/linux/spidev.c b/src/linux/spidev.c index 7fdd468da..79000c452 100644 --- a/src/linux/spidev.c +++ b/src/linux/spidev.c @@ -91,6 +91,13 @@ spi_prepare(struct spi_config config) void spi_transfer(struct spi_config config, uint8_t receive_data , uint8_t len, uint8_t *data) +{ + spi_transfer_large(config, receive_data, len, data); +} + +void +spi_transfer_large(struct spi_config config, uint8_t receive_data + , size_t len, uint8_t *data) { if (!len) return; diff --git a/src/neopixel_spi.c b/src/neopixel_spi.c new file mode 100644 index 000000000..092c38f21 --- /dev/null +++ b/src/neopixel_spi.c @@ -0,0 +1,112 @@ +// Support for WS2812 type "neopixel" LEDs using SPI hardware for timing +// +// Copyright (C) 2025 Russell Cloran +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include "basecmd.h" // oid_alloc +#include "board/irq.h" // irq_poll +#include "board/misc.h" // timer_read_time +#include "command.h" // DECL_COMMAND +#include "sched.h" // shutdown +#include // memcpy +#include "spicmds.h" // spidev_transfer + +// This code uses a SPI bus to generate neopixel-compatible data by sending +// different bytes on the SPI bus, each of which represents a bit in the +// neopixel data. +// +// According to one source, a neopixel 0 can be represented by holding the line +// high for anywhere between 200 and 500 ns, and a 1 for at least 550ns. The +// amount of time the line must then be held low is actually fairly tolerant: +// https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ +// +// Another source shows that high times as short as 62.5ns are valid 0s: +// https://cpldcpu.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/ +// +// Some LEDs may require as much as 800ns high time for a 1: +// https://github.com/Klipper3d/klipper/pull/7113 +// +// 2 bits of SPI data take 200ns at 10MHz and 500ns at 4MHz +// 5 bits of SPI data take 550ns at 9.09MHz, or longer at slower rates +// 5 bits of SPI data take 800ns at 6.25MHz + +#define ONE_BIT 0b01111100 +#define ZERO_BIT 0b01100000 + +#define NP_BYTES_PER_TRANSFER 3 + +/**************************************************************** + * Neopixel interface + ****************************************************************/ + +struct neopixel_spi_s { + struct spidev_s *spi; + uint32_t last_req_time, reset_min_ticks; + size_t buf_size; + uint8_t buf[0]; +}; + +void +command_config_neopixel_spi(uint32_t *args) +{ + uint16_t data_size = args[2]; + if (data_size > 0x1000) + shutdown("Invalid neopixel data_size"); + struct neopixel_spi_s *n = oid_alloc(args[0], command_config_neopixel_spi + , sizeof(*n) + (data_size * 8)); + + n->spi = spidev_oid_lookup(args[1]); + + n->buf_size = data_size * 8; + n->reset_min_ticks = args[3]; +} +DECL_COMMAND(command_config_neopixel_spi, "config_neopixel_spi oid=%c" + " bus_oid=%u data_size=%hu reset_min_ticks=%u"); + +void +command_neopixel_update_spi(uint32_t *args) +{ + uint8_t oid = args[0]; + struct neopixel_spi_s *n = oid_lookup(oid, command_config_neopixel_spi); + uint16_t pos = args[1]; + uint8_t data_len = args[2]; + uint8_t *data = command_decode_ptr(args[3]); + if (pos & 0x8000 || (pos + data_len > (n->buf_size >> 3))) + shutdown("Invalid neopixel update command"); + while (data_len) { + uint_fast8_t byte = *data++; + for (uint_fast8_t bit = 0; bit < 8; bit++) { + if (byte & 0x80) { + n->buf[pos * 8 + bit] = ONE_BIT; + } else { + n->buf[pos * 8 + bit] = ZERO_BIT; + } + byte <<= 1; + } + data_len--; + pos++; + } +} +DECL_COMMAND(command_neopixel_update_spi, + "neopixel_update_spi oid=%c pos=%hu data=%*s"); + +void +command_neopixel_send_spi(uint32_t *args) +{ + uint8_t oid = args[0]; + struct neopixel_spi_s *n = oid_lookup(oid, command_config_neopixel_spi); + // Make sure the reset time has elapsed since last request + uint32_t last_req_time = n->last_req_time; + uint32_t rmt = n->reset_min_ticks; + uint32_t cur = timer_read_time(); + while (cur - last_req_time < rmt) { + irq_poll(); + cur = timer_read_time(); + } + + spidev_transfer_large(n->spi, 0, n->buf_size, n->buf); + n->last_req_time = timer_read_time(); // + transfer time? + sendf("neopixel_result oid=%c success=%c", oid, 1); +} +DECL_COMMAND(command_neopixel_send_spi, "neopixel_send_spi oid=%c"); diff --git a/src/spicmds.c b/src/spicmds.c index 94ef3d0aa..dc9979e12 100644 --- a/src/spicmds.c +++ b/src/spicmds.c @@ -127,6 +127,24 @@ spidev_transfer(struct spidev_s *spi, uint8_t receive_data gpio_out_write(spi->pin, !(flags & SF_CS_ACTIVE_HIGH)); } +void +spidev_transfer_large(struct spidev_s *spi, uint8_t receive_data + , size_t data_len, uint8_t *data) +{ + uint_fast8_t flags = spi->flags; + if (!(flags & (SF_SOFTWARE|SF_HARDWARE))) + // Not yet initialized + return; + + // Large transfers require hardware support, so software SPI is not + // supported, and CS pin management must be done by the hardware + + if (flags & SF_SOFTWARE) + try_shutdown("Software SPI does not support large transfers"); + + spi_transfer_large(spi->spi_config, receive_data, data_len, data); +} + void command_spi_transfer(uint32_t *args) { diff --git a/src/spicmds.h b/src/spicmds.h index f7cf83fd6..09770445a 100644 --- a/src/spicmds.h +++ b/src/spicmds.h @@ -2,6 +2,7 @@ #define __SPICMDS_H #include // uint8_t +#include struct spidev_s *spidev_oid_lookup(uint8_t oid); struct spi_software; @@ -10,5 +11,7 @@ int spidev_have_cs_pin(struct spidev_s *spi); struct gpio_out spidev_get_cs_pin(struct spidev_s *spi); void spidev_transfer(struct spidev_s *spi, uint8_t receive_data , uint8_t data_len, uint8_t *data); +void spidev_transfer_large(struct spidev_s *spi, uint8_t receive_data + , size_t data_len, uint8_t *data); #endif // spicmds.h