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