Linux Audio

Check our new training course

Embedded Linux Audio

Check our new training course
with Creative Commons CC-BY-SA
lecture materials

Bootlin logo

Elixir Cross Referencer

Loading...
/*
 * 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);