| /* |
| * Copyright (c) 2017 Linaro Limited |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <led_strip.h> |
| |
| #include <string.h> |
| |
| #define SYS_LOG_LEVEL CONFIG_SYS_LOG_LED_STRIP_LEVEL |
| #include <logging/sys_log.h> |
| |
| #include <zephyr.h> |
| #include <device.h> |
| #include <spi.h> |
| #include <misc/util.h> |
| |
| /* |
| * WS2812-ish SPI master configuration: |
| * |
| * - mode 0 (the default), 8 bit, MSB first (arbitrary), one-line SPI |
| * - no shenanigans (don't hold CS, don't hold the device lock, this |
| * isn't an EEPROM) |
| */ |
| #define SPI_OPER (SPI_OP_MODE_MASTER | \ |
| SPI_TRANSFER_MSB | \ |
| SPI_WORD_SET(8) | \ |
| SPI_LINES_SINGLE) |
| |
| #define SPI_FREQ CONFIG_WS2812_STRIP_SPI_BAUD_RATE |
| #define ONE_FRAME CONFIG_WS2812_STRIP_ONE_FRAME |
| #define ZERO_FRAME CONFIG_WS2812_STRIP_ZERO_FRAME |
| #define RED_OFFSET (8 * sizeof(u8_t) * CONFIG_WS2812_RED_ORDER) |
| #define GRN_OFFSET (8 * sizeof(u8_t) * CONFIG_WS2812_GRN_ORDER) |
| #define BLU_OFFSET (8 * sizeof(u8_t) * CONFIG_WS2812_BLU_ORDER) |
| #ifdef CONFIG_WS2812_HAS_WHITE_CHANNEL |
| #define WHT_OFFSET (8 * sizeof(u8_t) * CONFIG_WS2812_WHT_ORDER) |
| #else |
| #define WHT_OFFSET -1 |
| #endif |
| |
| /* |
| * Despite datasheet claims (see blog post link in Kconfig.ws2812), a |
| * 6 microsecond pulse is enough to reset the strip. Convert that into |
| * a number of 8 bit SPI frames, adding another just to be safe. |
| */ |
| #define RESET_NFRAMES ((size_t)ceiling_fraction(3 * SPI_FREQ, 4000000) + 1) |
| |
| struct ws2812_data { |
| struct device *spi; |
| struct spi_config config; |
| }; |
| |
| /* |
| * Convert a color channel's bits into a sequence of SPI frames (with |
| * the proper pulse and inter-pulse widths) to shift out. |
| */ |
| static inline void ws2812_serialize_color(u8_t buf[8], u8_t color) |
| { |
| int i; |
| |
| for (i = 0; i < 8; i++) { |
| buf[i] = color & BIT(7 - i) ? ONE_FRAME : ZERO_FRAME; |
| } |
| } |
| |
| /* |
| * Convert a pixel into SPI frames, returning the number of bytes used. |
| */ |
| static size_t ws2812_serialize_pixel(u8_t px[32], struct led_rgb *pixel) |
| { |
| ws2812_serialize_color(px + RED_OFFSET, pixel->r); |
| ws2812_serialize_color(px + GRN_OFFSET, pixel->g); |
| ws2812_serialize_color(px + BLU_OFFSET, pixel->b); |
| if (IS_ENABLED(CONFIG_WS2812_HAS_WHITE_CHANNEL)) { |
| ws2812_serialize_color(px + WHT_OFFSET, 0); /* unused */ |
| return 32; |
| } |
| return 24; |
| } |
| |
| /* |
| * Latch current color values on strip and reset its state machines. |
| */ |
| static int ws2812_reset_strip(struct ws2812_data *data) |
| { |
| u8_t reset_buf[RESET_NFRAMES]; |
| const struct spi_buf reset = { |
| .buf = reset_buf, |
| .len = sizeof(reset_buf), |
| }; |
| const struct spi_buf_set tx = { |
| .buffers = &reset, |
| .count = 1 |
| }; |
| |
| memset(reset_buf, 0x00, sizeof(reset_buf)); |
| |
| return spi_write(data->spi, &data->config, &tx); |
| } |
| |
| static int ws2812_strip_update_rgb(struct device *dev, struct led_rgb *pixels, |
| size_t num_pixels) |
| { |
| struct ws2812_data *drv_data = dev->driver_data; |
| struct spi_config *config = &drv_data->config; |
| u8_t px_buf[32]; /* 32 are needed when a white channel is present. */ |
| struct spi_buf buf = { |
| .buf = px_buf, |
| }; |
| const struct spi_buf_set tx = { |
| .buffers = &buf, |
| .count = 1 |
| }; |
| size_t i; |
| int rc; |
| |
| for (i = 0; i < num_pixels; i++) { |
| buf.len = ws2812_serialize_pixel(px_buf, &pixels[i]); |
| rc = spi_write(drv_data->spi, config, &tx); |
| if (rc) { |
| /* |
| * Latch anything we've shifted out first, to |
| * call visual attention to the problematic |
| * pixel. |
| */ |
| (void)ws2812_reset_strip(drv_data); |
| SYS_LOG_ERR("can't set pixel %u: %d", i, rc); |
| return rc; |
| } |
| } |
| |
| return ws2812_reset_strip(drv_data); |
| } |
| |
| static int ws2812_strip_update_channels(struct device *dev, u8_t *channels, |
| size_t num_channels) |
| { |
| struct ws2812_data *drv_data = dev->driver_data; |
| struct spi_config *config = &drv_data->config; |
| u8_t px_buf[8]; /* one byte per bit */ |
| const struct spi_buf buf = { |
| .buf = px_buf, |
| .len = sizeof(px_buf), |
| }; |
| const struct spi_buf_set tx = { |
| .buffers = &buf, |
| .count = 1 |
| }; |
| size_t i; |
| int rc; |
| |
| for (i = 0; i < num_channels; i++) { |
| ws2812_serialize_color(px_buf, channels[i]); |
| rc = spi_write(drv_data->spi, config, &tx); |
| if (rc) { |
| /* |
| * Latch anything we've shifted out first, to |
| * call visual attention to the problematic |
| * pixel. |
| */ |
| (void)ws2812_reset_strip(drv_data); |
| SYS_LOG_ERR("can't set channel %u: %d", i, rc); |
| return rc; |
| } |
| } |
| |
| return ws2812_reset_strip(drv_data); |
| } |
| |
| static int ws2812_strip_init(struct device *dev) |
| { |
| struct ws2812_data *data = dev->driver_data; |
| struct spi_config *config = &data->config; |
| |
| data->spi = device_get_binding(CONFIG_WS2812_STRIP_SPI_DEV_NAME); |
| if (!data->spi) { |
| SYS_LOG_ERR("SPI device %s not found", |
| CONFIG_WS2812_STRIP_SPI_DEV_NAME); |
| return -ENODEV; |
| } |
| |
| config->frequency = SPI_FREQ; |
| config->operation = SPI_OPER; |
| config->slave = 0; /* MOSI only. */ |
| config->cs = NULL; |
| |
| return 0; |
| } |
| |
| static struct ws2812_data ws2812_strip_data; |
| |
| static const struct led_strip_driver_api ws2812_strip_api = { |
| .update_rgb = ws2812_strip_update_rgb, |
| .update_channels = ws2812_strip_update_channels, |
| }; |
| |
| DEVICE_AND_API_INIT(ws2812_strip, CONFIG_WS2812_STRIP_NAME, |
| ws2812_strip_init, &ws2812_strip_data, |
| NULL, POST_KERNEL, CONFIG_LED_STRIP_INIT_PRIORITY, |
| &ws2812_strip_api); |