Skip to content

Commit 9997bd0

Browse files
committed
neopixel: Support using SPI to send data
This change allows the use of a SPI bus to send correctly timed data to a string of neopixels. The current bitbang approach cannot be used directly from a Raspberry Pi secondary MCU as the bitbang timing is not precise enough. Using the 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 and STM32F427) 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 devices. Signed-off-by: Russell Cloran <rcloran@gmail.com>
1 parent 1f4806a commit 9997bd0

5 files changed

Lines changed: 201 additions & 13 deletions

File tree

docs/Config_Reference.md

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3886,17 +3886,41 @@ Neopixel (aka WS2812) LED support (one may define any number of
38863886
sections with a "neopixel" prefix). See the
38873887
[command reference](G-Codes.md#led) for more information.
38883888

3889-
Note that the [linux mcu](RPi_microcontroller.md) implementation does
3890-
not currently support directly connected neopixels. The current design
3891-
using the Linux kernel interface does not allow this scenario because
3892-
the kernel GPIO interface is not fast enough to provide the required
3893-
pulse rates.
3889+
A SPI bus may optionally be used to generate the data to send to the
3890+
neopixels. They aren't really SPI devices, but this allows offloading
3891+
the timing required to hardware. By default a SPI speed of 6MHz (6000000)
3892+
is requested -- not all hardware supports arbitrary speeds, and you may
3893+
need to specify an exact speed that is supported by your hardware. Speeds
3894+
between about 4Mhz and about 8MHz should be tolerated by most neopixels.
3895+
3896+
Note that the [linux mcu](RPi_microcontroller.md) implementation only
3897+
supports directly connected neopixels using SPI, as the GPIO interface
3898+
is not fast enough to provide the required pulse rates.
38943899

38953900
```
38963901
[neopixel my_neopixel]
38973902
pin:
38983903
# The pin connected to the neopixel. This parameter must be
3899-
# provided.
3904+
# provided if not using SPI bus.
3905+
spi_bus:
3906+
spi_speed:
3907+
# See the "common SPI settings" section for a description of the
3908+
# above parameters. Changing `spi_speed` will change the timing of
3909+
# pulses sent to the neopixel chain; the voltage high time for a 0 is
3910+
# represented by 2 SPI bits, and a 1 by 5 SPI bits, with the remaining
3911+
# bits in each SPI byte being low. At the default `spi_speed` of 6MHz
3912+
# this results in 2/6e6 = 333ns high time and 1000ns low time for a 0,
3913+
# and 6/6e6 = 833ns high time and 500ns low time for a 1. Experimenting
3914+
# with slightly different `spi_speed` (usually between 4MHz and 8MHz)
3915+
# may resolve compatibility problems with different neopixel chips.
3916+
# On Raspberry Pi 4 and older the actual SPI bus speed is a fraction
3917+
# of the current core clock speed, which can vary, so you may need to
3918+
# set the core clock to a fixed speed. See
3919+
# https://github.com/raspberrypi/linux/issues/3381 for more.
3920+
cs_pin:
3921+
# If using a SPI bus this is required in order to determine which
3922+
# MCU the SPI bus is on, but may be specified as "mcu:None" if no
3923+
# actual CS pin needs to be set.
39003924
#chain_count:
39013925
# The number of Neopixel chips that are "daisy chained" to the
39023926
# provided pin. The default is 1 (which indicates only a single

klippy/extras/neopixel.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#
55
# This file may be distributed under the terms of the GNU GPLv3 license.
66
import logging
7+
from . import bus
78

89
BACKGROUND_PRIORITY_CLOCK = 0x7FFFFFFF00000000
910

@@ -18,12 +19,20 @@ def __init__(self, config):
1819
self.printer = printer = config.get_printer()
1920
self.mutex = printer.get_reactor().mutex()
2021
# Configure neopixel
21-
ppins = printer.lookup_object("pins")
22-
pin_params = ppins.lookup_pin(config.get("pin"))
23-
self.mcu = pin_params["chip"]
22+
if config.get("pin", None) is not None:
23+
ppins = printer.lookup_object("pins")
24+
pin_params = ppins.lookup_pin(config.get("pin"))
25+
self.mcu = pin_params["chip"]
26+
self.pin = pin_params["pin"]
27+
self.mcu.register_config_callback(self.build_config_bitbang)
28+
else:
29+
self.spi = bus.MCU_SPI_from_config(
30+
config, 0, default_speed=6_000_000
31+
)
32+
self.mcu = self.spi.mcu
33+
self.mcu.register_config_callback(self.build_config_spi)
34+
2435
self.oid = self.mcu.create_oid()
25-
self.pin = pin_params["pin"]
26-
self.mcu.register_config_callback(self.build_config)
2736
self.neopixel_update_cmd = self.neopixel_send_cmd = None
2837
# Build color map
2938
chain_count = config.getint("chain_count", 1, minval=1)
@@ -54,7 +63,7 @@ def __init__(self, config):
5463
self.mcu.get_non_critical_reconnect_event_name(), self.send_data
5564
)
5665

57-
def build_config(self):
66+
def build_config_bitbang(self):
5867
bmt = self.mcu.seconds_to_clock(BIT_MAX_TIME)
5968
rmt = self.mcu.seconds_to_clock(RESET_MIN_TIME)
6069
self.mcu.add_config_cmd(
@@ -73,6 +82,24 @@ def build_config(self):
7382
cq=cmd_queue,
7483
)
7584

85+
def build_config_spi(self):
86+
rmt = self.mcu.seconds_to_clock(RESET_MIN_TIME)
87+
self.mcu.add_config_cmd(
88+
"config_neopixel_spi oid=%d bus_oid=%s data_size=%d"
89+
" reset_min_ticks=%d"
90+
% (self.oid, self.spi.oid, len(self.color_data), rmt)
91+
)
92+
cmd_queue = self.mcu.alloc_command_queue()
93+
self.neopixel_update_cmd = self.mcu.lookup_command(
94+
"neopixel_update_spi oid=%c pos=%hu data=%*s", cq=cmd_queue
95+
)
96+
self.neopixel_send_cmd = self.mcu.lookup_query_command(
97+
"neopixel_send_spi oid=%c",
98+
"neopixel_result oid=%c success=%c",
99+
oid=self.oid,
100+
cq=cmd_queue,
101+
)
102+
76103
def update_color_data(self, led_state):
77104
color_data = self.color_data
78105
for cdidx, (lidx, cidx) in self.color_map:

src/Kconfig

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ config WANT_NEOPIXEL
138138
bool
139139
depends on HAVE_GPIO
140140
default y
141+
config WANT_NEOPIXEL_SPI
142+
bool
143+
depends on WANT_SPI
144+
default y
141145
config WANT_PULSE_COUNTER
142146
bool
143147
depends on HAVE_GPIO
@@ -236,8 +240,11 @@ config WANT_TMCUART
236240
bool "Support Trinamic stepper motor driver UART communication"
237241
depends on HAVE_GPIO
238242
config WANT_NEOPIXEL
239-
bool "Support 'neopixel' type LED control"
243+
bool "Support 'neopixel' type LED control using bitbang"
240244
depends on HAVE_GPIO
245+
config WANT_NEOPIXEL_SPI
246+
bool "Support 'neopixel' type LED control using SPI"
247+
depends on WANT_SPI
241248
config WANT_PULSE_COUNTER
242249
bool "Support measuring fan tachometer GPIO pins"
243250
depends on HAVE_GPIO

src/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ src-$(CONFIG_HAVE_GPIO_SDIO) += sdiocmds.c
1212
src-$(CONFIG_WANT_BUTTONS) += buttons.c
1313
src-$(CONFIG_WANT_TMCUART) += tmcuart.c
1414
src-$(CONFIG_WANT_NEOPIXEL) += neopixel.c
15+
src-$(CONFIG_WANT_NEOPIXEL_SPI) += neopixel_spi.c
1516
src-$(CONFIG_WANT_PULSE_COUNTER) += pulse_counter.c
1617
src-$(CONFIG_WANT_ST7920) += lcd_st7920.c
1718
src-$(CONFIG_WANT_HD44780) += lcd_hd44780.c

src/neopixel_spi.c

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Support for WS2812 type "neopixel" LEDs using SPI hardware for timing
2+
//
3+
// Copyright (C) 2025 Russell Cloran <rcloran@gmail.com>
4+
//
5+
// This file may be distributed under the terms of the GNU GPLv3 license.
6+
7+
#include "basecmd.h" // oid_alloc
8+
#include "board/irq.h" // irq_poll
9+
#include "board/misc.h" // timer_read_time
10+
#include "command.h" // DECL_COMMAND
11+
#include "sched.h" // shutdown
12+
#include <string.h> // memcpy
13+
#include "spicmds.h" // spidev_transfer
14+
15+
// This code uses a SPI bus to generate neopixel-compatible data by sending
16+
// different bytes on the SPI bus, each of which represents a bit in the
17+
// neopixel data.
18+
//
19+
// According to one source, a neopixel 0 can be represented by holding the line
20+
// high for anywhere between 200 and 500 ns, and a 1 for at least 550ns. The
21+
// amount of time the line must then be held low is actually fairly tolerant:
22+
// https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
23+
//
24+
// Another source shows that high times as short as 62.5ns are valid 0s:
25+
// https://cpldcpu.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/
26+
//
27+
// Some LEDs may require as much as 800ns high time for a 1:
28+
// https://github.com/Klipper3d/klipper/pull/7113
29+
//
30+
// 2 bits of SPI data take 200ns at 10MHz and 500ns at 4MHz
31+
// 5 bits of SPI data take 550ns at 9.09MHz, or longer at slower rates
32+
// 5 bits of SPI data take 800ns at 6.25MHz
33+
34+
#define ONE_BIT 0b01111100
35+
#define ZERO_BIT 0b01100000
36+
37+
#define NP_BYTES_PER_TRANSFER 3
38+
39+
/****************************************************************
40+
* Neopixel interface
41+
****************************************************************/
42+
43+
struct neopixel_spi_s {
44+
struct spidev_s *spi;
45+
uint32_t last_req_time, reset_min_ticks;
46+
uint16_t data_size;
47+
uint8_t data[0];
48+
};
49+
50+
void
51+
command_config_neopixel_spi(uint32_t *args)
52+
{
53+
uint16_t data_size = args[2];
54+
if (data_size & 0x8000)
55+
shutdown("Invalid neopixel data_size");
56+
struct neopixel_spi_s *n = oid_alloc(args[0], command_config_neopixel_spi
57+
, sizeof(*n) + data_size);
58+
59+
n->spi = spidev_oid_lookup(args[1]);
60+
61+
n->data_size = data_size;
62+
n->reset_min_ticks = args[3];
63+
}
64+
DECL_COMMAND(command_config_neopixel_spi, "config_neopixel_spi oid=%c"
65+
" bus_oid=%u data_size=%hu reset_min_ticks=%u");
66+
67+
static int
68+
send_data_spi(struct neopixel_spi_s *n)
69+
{
70+
// Make sure the reset time has elapsed since last request
71+
uint32_t last_req_time = n->last_req_time, rmt = n->reset_min_ticks;
72+
uint32_t cur = timer_read_time();
73+
while (cur - last_req_time < rmt) {
74+
irq_poll();
75+
cur = timer_read_time();
76+
}
77+
78+
// Transmit data
79+
uint8_t *data = n->data;
80+
uint_fast16_t data_len = n->data_size;
81+
uint8_t msg[NP_BYTES_PER_TRANSFER * 8] = {0};
82+
83+
while (data_len) {
84+
uint_fast8_t i;
85+
for (i = 0; i < NP_BYTES_PER_TRANSFER && data_len; i++) {
86+
uint_fast8_t byte = *data++;
87+
data_len--;
88+
for (uint_fast8_t bit = 0; bit < 8; bit++) {
89+
if (byte & 0x80) {
90+
msg[i * 8 + bit] = ONE_BIT;
91+
} else {
92+
msg[i * 8 + bit] = ZERO_BIT;
93+
}
94+
byte <<= 1;
95+
}
96+
}
97+
uint_fast8_t msg_len = i * 8;
98+
spidev_transfer(n->spi, 0, msg_len, msg);
99+
}
100+
101+
n->last_req_time = timer_read_time(); // + transfer time?
102+
return 0;
103+
}
104+
105+
void
106+
command_neopixel_update_spi(uint32_t *args)
107+
{
108+
uint8_t oid = args[0];
109+
struct neopixel_spi_s *n = oid_lookup(oid, command_config_neopixel_spi);
110+
uint_fast16_t pos = args[1];
111+
uint_fast8_t data_len = args[2];
112+
uint8_t *data = command_decode_ptr(args[3]);
113+
if (pos & 0x8000 || pos + data_len > n->data_size)
114+
shutdown("Invalid neopixel update command");
115+
memcpy(&n->data[pos], data, data_len);
116+
}
117+
DECL_COMMAND(command_neopixel_update_spi,
118+
"neopixel_update_spi oid=%c pos=%hu data=%*s");
119+
120+
void
121+
command_neopixel_send_spi(uint32_t *args)
122+
{
123+
uint8_t oid = args[0];
124+
struct neopixel_spi_s *n = oid_lookup(oid, command_config_neopixel_spi);
125+
int ret = 1;
126+
ret = send_data_spi(n);
127+
sendf("neopixel_result oid=%c success=%c", oid, ret ? 0 : 1);
128+
}
129+
DECL_COMMAND(command_neopixel_send_spi, "neopixel_send_spi oid=%c");

0 commit comments

Comments
 (0)