drivers: led_strip: add rpi_pico's PIO based ws2812 driver

Add driver that based on RPI-PICO's PIO feature for ws2812.

This driver can handle WS2812 or compatible LED strips.
The single PIO node can handle up to 4 strips.
Any pins that can be configured for PIO can be used for strips.

I verified the samples/driver/led_ws2812 sample
working with WS2812(144 pcs) led strip using following patches.

- samples/drivers/led_ws2812/boards/rpi_pico.overlay

```
/ {
        aliases {
                led-strip = &ws2812;
        };
};

&pinctrl {
        ws2812_pio0_default: ws2812_pio0_default {
                ws2812 {
                        pinmux = <PIO0_P21>;
                };
        };
};

&pio0 {
        status = "okay";

        pio-ws2812 {
                compatible = "worldsemi,ws2812-rpi_pico-pio";
                status = "okay";
                pinctrl-0 = <&ws2812_pio0_default>;
                pinctrl-names = "default";
                bit-waveform = <3>, <3>, <4>;

                ws2812: ws2812 {
                        status = "okay";
                        output-pin = <21>;
                        chain-length = <144>;
                        color-mapping = <LED_COLOR_ID_GREEN
                                         LED_COLOR_ID_RED
                                         LED_COLOR_ID_BLUE>;
                        reset-delay = <280>;
                        frequency = <800000>;
                };
        };
};

```

- samples/drivers/led_ws2812/boards/rpi_pico.conf

```
CONFIG_WS2812_STRIP_RPI_PICO_PIO=y
```

Signed-off-by: TOKITA Hiroshi <tokita.hiroshi@gmail.com>
This commit is contained in:
TOKITA Hiroshi 2023-02-25 09:00:59 +09:00 committed by Carles Cufí
parent b0dd9a2980
commit 6699d4d4f9
4 changed files with 325 additions and 0 deletions

View file

@ -7,4 +7,5 @@ zephyr_library_sources_ifdef(CONFIG_LPD880X_STRIP lpd880x.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_GPIO ws2812_gpio.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_SPI ws2812_spi.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_I2S ws2812_i2s.c)
zephyr_library_sources_ifdef(CONFIG_WS2812_STRIP_RPI_PICO_PIO ws2812_rpi_pico_pio.c)
zephyr_library_sources_ifdef(CONFIG_TLC5971_STRIP tlc5971.c)

View file

@ -46,4 +46,11 @@ config WS2812_STRIP_GPIO
Note that this driver is not compatible with the Everlight B1414
controller.
config WS2812_STRIP_RPI_PICO_PIO
bool "Raspberry Pi Pico PIO"
depends on DT_HAS_WORLDSEMI_WS2812_RPI_PICO_PIO_ENABLED
select PICOSDK_USE_PIO
help
Use the PIO feature available on RaspberryPi Pico devices.
endchoice

View file

@ -0,0 +1,249 @@
/*
* Copyright (c) 2023 TOKITA Hiroshi
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/drivers/led_strip.h>
#include <zephyr/drivers/misc/pio_rpi_pico/pio_rpi_pico.h>
#include <zephyr/dt-bindings/led/led.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(ws2812_rpi_pico_pio, CONFIG_LED_STRIP_LOG_LEVEL);
#define DT_DRV_COMPAT worldsemi_ws2812_rpi_pico_pio
struct ws2812_led_strip_data {
uint32_t sm;
};
struct ws2812_led_strip_config {
const struct device *piodev;
uint32_t output_pin;
uint8_t num_colors;
uint32_t frequency;
const uint8_t *const color_mapping;
uint16_t reset_delay;
uint32_t cycles_per_bit;
};
struct ws2812_rpi_pico_pio_config {
const struct device *piodev;
const struct pinctrl_dev_config *const pcfg;
struct pio_program program;
};
static int ws2812_led_strip_sm_init(const struct device *dev)
{
const struct ws2812_led_strip_config *config = dev->config;
const float clkdiv =
sys_clock_hw_cycles_per_sec() / (config->cycles_per_bit * config->frequency);
pio_sm_config sm_config = pio_get_default_sm_config();
PIO pio;
int sm;
pio = pio_rpi_pico_get_pio(config->piodev);
sm = pio_claim_unused_sm(pio, false);
if (sm < 0) {
return -EINVAL;
}
sm_config_set_sideset(&sm_config, 1, false, false);
sm_config_set_sideset_pins(&sm_config, config->output_pin);
sm_config_set_out_shift(&sm_config, false, true, (config->num_colors == 4 ? 32 : 24));
sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX);
sm_config_set_clkdiv(&sm_config, clkdiv);
pio_sm_set_consecutive_pindirs(pio, sm, config->output_pin, 1, true);
pio_sm_init(pio, sm, -1, &sm_config);
pio_sm_set_enabled(pio, sm, true);
return sm;
}
/*
* Latch current color values on strip and reset its state machines.
*/
static inline void ws2812_led_strip_reset_delay(uint16_t delay)
{
k_usleep(delay);
}
static int ws2812_led_strip_update_rgb(const struct device *dev, struct led_rgb *pixels,
size_t num_pixels)
{
const struct ws2812_led_strip_config *config = dev->config;
struct ws2812_led_strip_data *data = dev->data;
PIO pio = pio_rpi_pico_get_pio(config->piodev);
for (size_t i = 0; i < num_pixels; i++) {
uint32_t color = 0;
for (size_t j = 0; j < config->num_colors; j++) {
switch (config->color_mapping[j]) {
/* White channel is not supported by LED strip API. */
case LED_COLOR_ID_WHITE:
color |= 0;
break;
case LED_COLOR_ID_RED:
color |= pixels[i].r << (8 * (2 - j));
break;
case LED_COLOR_ID_GREEN:
color |= pixels[i].g << (8 * (2 - j));
break;
case LED_COLOR_ID_BLUE:
color |= pixels[i].b << (8 * (2 - j));
break;
}
}
pio_sm_put_blocking(pio, data->sm, color << (config->num_colors == 4 ? 0 : 8));
}
ws2812_led_strip_reset_delay(config->reset_delay);
return 0;
}
static int ws2812_led_strip_update_channels(const struct device *dev, uint8_t *channels,
size_t num_channels)
{
LOG_DBG("update_channels not implemented");
return -ENOTSUP;
}
static const struct led_strip_driver_api ws2812_led_strip_api = {
.update_rgb = ws2812_led_strip_update_rgb,
.update_channels = ws2812_led_strip_update_channels,
};
/*
* Retrieve the channel to color mapping (e.g. RGB, BGR, GRB, ...) from the
* "color-mapping" DT property.
*/
static int ws2812_led_strip_init(const struct device *dev)
{
const struct ws2812_led_strip_config *config = dev->config;
struct ws2812_led_strip_data *data = dev->data;
int sm;
if (!device_is_ready(config->piodev)) {
LOG_ERR("%s: PIO device not ready", dev->name);
return -ENODEV;
}
for (uint32_t i = 0; i < config->num_colors; i++) {
switch (config->color_mapping[i]) {
case LED_COLOR_ID_WHITE:
case LED_COLOR_ID_RED:
case LED_COLOR_ID_GREEN:
case LED_COLOR_ID_BLUE:
break;
default:
LOG_ERR("%s: invalid channel to color mapping."
" Check the color-mapping DT property",
dev->name);
return -EINVAL;
}
}
sm = ws2812_led_strip_sm_init(dev);
if (sm < 0) {
return sm;
}
data->sm = sm;
return 0;
}
static int ws2812_rpi_pico_pio_init(const struct device *dev)
{
const struct ws2812_rpi_pico_pio_config *config = dev->config;
PIO pio;
if (!device_is_ready(config->piodev)) {
LOG_ERR("%s: PIO device not ready", dev->name);
return -ENODEV;
}
pio = pio_rpi_pico_get_pio(config->piodev);
pio_add_program(pio, &config->program);
return pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
}
#define CYCLES_PER_BIT(node) \
(DT_PROP_BY_IDX(node, bit_waveform, 0) + DT_PROP_BY_IDX(node, bit_waveform, 1) + \
DT_PROP_BY_IDX(node, bit_waveform, 2))
#define WS2812_CHILD_INIT(node) \
static const uint8_t ws2812_led_strip_##node##_color_mapping[] = \
DT_PROP(node, color_mapping); \
struct ws2812_led_strip_data ws2812_led_strip_##node##_data; \
\
static const struct ws2812_led_strip_config ws2812_led_strip_##node##_config = { \
.piodev = DEVICE_DT_GET(DT_PARENT(DT_PARENT(node))), \
.output_pin = DT_PROP(node, output_pin), \
.num_colors = DT_PROP_LEN(node, color_mapping), \
.color_mapping = ws2812_led_strip_##node##_color_mapping, \
.reset_delay = DT_PROP(node, reset_delay), \
.frequency = DT_PROP(node, frequency), \
.cycles_per_bit = CYCLES_PER_BIT(DT_PARENT(node)), \
}; \
\
DEVICE_DT_DEFINE(node, &ws2812_led_strip_init, NULL, &ws2812_led_strip_##node##_data, \
&ws2812_led_strip_##node##_config, POST_KERNEL, \
CONFIG_LED_STRIP_INIT_PRIORITY, &ws2812_led_strip_api);
#define SET_DELAY(op, inst, i) \
(op | (((DT_INST_PROP_BY_IDX(inst, bit_waveform, i) - 1) & 0xF) << 8))
/*
* This pio program runs [T0+T1+T2] cycles per 1 loop.
* The first `out` instruction outputs 0 by [T2] times to the sideset pin.
* These zeros are padding. Here is the start of actual data transmission.
* The second `jmp` instruction output 1 by [T0] times to the sideset pin.
* This `jmp` instruction jumps to line 3 if the value of register x is true.
* Otherwise, jump to line 4.
* The third `jmp` instruction outputs 1 by [T1] times to the sideset pin.
* After output, return to the first line.
* The fourth `jmp` instruction outputs 0 by [T1] times.
* After output, return to the first line and output 0 by [T2] times.
*
* In the case of configuration, T0=3, T1=3, T2 =4,
* the final output is 1110000000 in case register x is false.
* It represents code 0, defined in the datasheet.
* And outputs 1111110000 in case of x is true. It represents code 1.
*/
#define WS2812_RPI_PICO_PIO_INIT(inst) \
PINCTRL_DT_INST_DEFINE(inst); \
\
DT_INST_FOREACH_CHILD_STATUS_OKAY(inst, WS2812_CHILD_INIT); \
\
static const uint16_t rpi_pico_pio_ws2812_instructions_##inst[] = { \
SET_DELAY(0x6021, inst, 2), /* 0: out x, 1 side 0 [T2 - 1] */ \
SET_DELAY(0x1023, inst, 0), /* 1: jmp !x, 3 side 1 [T0 - 1] */ \
SET_DELAY(0x1000, inst, 1), /* 2: jmp 0 side 1 [T1 - 1] */ \
SET_DELAY(0x0000, inst, 1), /* 3: jmp 0 side 0 [T1 - 1] */ \
}; \
\
static const struct ws2812_rpi_pico_pio_config rpi_pico_pio_ws2812_##inst##_config = { \
.piodev = DEVICE_DT_GET(DT_INST_PARENT(inst)), \
.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst), \
.program = \
{ \
.instructions = rpi_pico_pio_ws2812_instructions_##inst, \
.length = ARRAY_SIZE(rpi_pico_pio_ws2812_instructions_##inst), \
.origin = -1, \
}, \
}; \
\
DEVICE_DT_INST_DEFINE(inst, &ws2812_rpi_pico_pio_init, NULL, NULL, \
&rpi_pico_pio_ws2812_##inst##_config, POST_KERNEL, \
CONFIG_LED_STRIP_INIT_PRIORITY, NULL);
DT_INST_FOREACH_STATUS_OKAY(WS2812_RPI_PICO_PIO_INIT)

View file

@ -0,0 +1,68 @@
# Copyright (c) 2023, TOKITA Hiroshi
# SPDX-License-Identifier: Apache-2.0
description: |
The pio node configured for ws2812.
compatible: "worldsemi,ws2812-rpi_pico-pio"
include: pinctrl-device.yaml
properties:
bit-waveform:
type: array
description: |
This property defines the waveform for sending 1-bit data.
The program uses the first three elements of the array.
The T0 is equal to T0H in the datasheet.
The T2 is equal to T1L in the datasheet.
The T1 is equal to (T1H-T0H) or (T0L-T1L) in the datasheet.
Code-0
+------+ +---
| | |
| T0 | T1+T2 |
| | |
---+ +-----------------+
Code-1
+---------------+ +---
| | |
| T0+T1 | T2 |
| | |
---+ +--------+
The frequency determines the wave period.
The T0T2 means ratio in one period.
For example, T0=3, T1=3, T2=4 and the frequency is 800kHz case,
T0H is
(1 / 800kHz) * (3/10) = 375ns
T0L is
(1 / 800kHz) * ((4+3)/10) = 875ns
child-binding:
description: |
Worldsemi WS2812 or compatible LED strip driver based on RaspberryPi Pico's PIO
The LED strip node can put up to 4 instances under a single PIO node.
include: ws2812.yaml
properties:
output-pin:
type: int
required: true
description: |
Select the output pin.
Note: This driver does not configure the output pin.
You need to configure the pin with pinctrl that is in the parent node configuration
for use by PIO.
frequency:
type: int
description: |
Specify the number of times a waveform representing 1 bit is
transmitted per second. It is same meaning as bit-per-seconds.
WS2812 works with 800000. Set the value 400000 if use with WS2811.