Skip to content

Commit f4b6e68

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 f4b6e68

5 files changed

Lines changed: 180 additions & 13 deletions

File tree

docs/Config_Reference.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3886,17 +3886,30 @@ 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.
3909+
cs_pin:
3910+
# If using a SPI bus this is required in order to determine which
3911+
# MCU the SPI bus is on, but may be specified as "mcu:None" if no
3912+
# actual CS pin needs to be set.
39003913
#chain_count:
39013914
# The number of Neopixel chips that are "daisy chained" to the
39023915
# 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: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
// A neopixel 0 can be represented by holding the line high for anywhere
20+
// between 200 and 500 ns, and a 1 for at least 550ns. The amount of time the
21+
// line must then be held low is actually fairly tolerant:
22+
//
23+
// https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
24+
//
25+
// 2 bits of SPI data take 200ns at 10MHz and 500ns at 4MHz
26+
// 4 bits of SPI data take 550ns at 7.27MHz, or longer at slower rates
27+
28+
#define ONE_BIT 0b01111000
29+
#define ZERO_BIT 0b01100000
30+
31+
/****************************************************************
32+
* Neopixel interface
33+
****************************************************************/
34+
35+
struct neopixel_spi_s {
36+
struct spidev_s *spi;
37+
uint32_t last_req_time, reset_min_ticks;
38+
uint16_t data_size;
39+
uint8_t data[0];
40+
};
41+
42+
void
43+
command_config_neopixel_spi(uint32_t *args)
44+
{
45+
uint16_t data_size = args[2];
46+
if (data_size & 0x8000)
47+
shutdown("Invalid neopixel data_size");
48+
struct neopixel_spi_s *n = oid_alloc(args[0], command_config_neopixel_spi
49+
, sizeof(*n) + data_size);
50+
51+
n->spi = spidev_oid_lookup(args[1]);
52+
53+
n->data_size = data_size;
54+
n->reset_min_ticks = args[3];
55+
}
56+
DECL_COMMAND(command_config_neopixel_spi, "config_neopixel_spi oid=%c"
57+
" bus_oid=%u data_size=%hu reset_min_ticks=%u");
58+
59+
static int
60+
send_data_spi(struct neopixel_spi_s *n)
61+
{
62+
// Make sure the reset time has elapsed since last request
63+
uint32_t last_req_time = n->last_req_time, rmt = n->reset_min_ticks;
64+
uint32_t cur = timer_read_time();
65+
while (cur - last_req_time < rmt) {
66+
irq_poll();
67+
cur = timer_read_time();
68+
}
69+
70+
// Transmit data
71+
uint8_t *data = n->data;
72+
uint_fast16_t data_len = n->data_size;
73+
uint8_t msg[24] = {0};
74+
75+
while (data_len) {
76+
for (uint_fast8_t i = 0; i < 3; i++) {
77+
uint_fast8_t byte = *data++;
78+
data_len--;
79+
for (uint_fast8_t bit = 0; bit < 8; bit++) {
80+
if (byte & 0x80) {
81+
msg[i * 8 + bit] = ONE_BIT;
82+
} else {
83+
msg[i * 8 + bit] = ZERO_BIT;
84+
}
85+
byte <<= 1;
86+
}
87+
}
88+
spidev_transfer(n->spi, 0, sizeof(msg), msg);
89+
}
90+
91+
n->last_req_time = timer_read_time(); // + transfer time?
92+
return 0;
93+
}
94+
95+
void
96+
command_neopixel_update_spi(uint32_t *args)
97+
{
98+
uint8_t oid = args[0];
99+
struct neopixel_spi_s *n = oid_lookup(oid, command_config_neopixel_spi);
100+
uint_fast16_t pos = args[1];
101+
uint_fast8_t data_len = args[2];
102+
uint8_t *data = command_decode_ptr(args[3]);
103+
if (pos & 0x8000 || pos + data_len > n->data_size)
104+
shutdown("Invalid neopixel update command");
105+
memcpy(&n->data[pos], data, data_len);
106+
}
107+
DECL_COMMAND(command_neopixel_update_spi,
108+
"neopixel_update_spi oid=%c pos=%hu data=%*s");
109+
110+
void
111+
command_neopixel_send_spi(uint32_t *args)
112+
{
113+
uint8_t oid = args[0];
114+
struct neopixel_spi_s *n = oid_lookup(oid, command_config_neopixel_spi);
115+
int ret = 1;
116+
ret = send_data_spi(n);
117+
sendf("neopixel_result oid=%c success=%c", oid, ret ? 0 : 1);
118+
}
119+
DECL_COMMAND(command_neopixel_send_spi, "neopixel_send_spi oid=%c");

0 commit comments

Comments
 (0)