Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
28 changes: 21 additions & 7 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 34 additions & 6 deletions klippy/extras/neopixel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions src/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/linux/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/linux/gpio.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef __LINUX_GPIO_H
#define __LINUX_GPIO_H

#include <stddef.h> // size_t
#include <stdint.h> // uint8_t

struct gpio_out {
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/linux/spidev.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
112 changes: 112 additions & 0 deletions src/neopixel_spi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Support for WS2812 type "neopixel" LEDs using SPI hardware for timing
//
// Copyright (C) 2025 Russell Cloran <[email protected]>
//
// 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 <string.h> // 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");
18 changes: 18 additions & 0 deletions src/spicmds.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
3 changes: 3 additions & 0 deletions src/spicmds.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define __SPICMDS_H

#include <stdint.h> // uint8_t
#include <stddef.h>

struct spidev_s *spidev_oid_lookup(uint8_t oid);
struct spi_software;
Expand All @@ -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
Loading