nuttx/drivers/leds/ws2812.c
Diego Herranz 441180b08c drivers/leds/ws2812: make frequency selection more flexible
Previously only 4 MHz and 8 MHz were allowed. Given the tolerance
allowed in the WS2812 timing spec, frequency ranges around those
two can be used too which is useful for boards in which it is
difficult to generate those specific frequencies.
2020-12-07 21:15:35 -08:00

487 lines
14 KiB
C

/****************************************************************************
* drivers/sensors/ws2812.c
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership. The
* ASF licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <stdlib.h>
#include <errno.h>
#include <debug.h>
#include <nuttx/kmalloc.h>
#include <nuttx/fs/fs.h>
#include <nuttx/spi/spi.h>
#include <nuttx/leds/ws2812.h>
#ifdef CONFIG_WS2812
/****************************************************************************
* Pre-processor Definitions
****************************************************************************/
/* In order to meet the signaling timing requirements, the waveforms required
* to represent a 0/1 symbol are created by specific SPI bytes defined here.
*
* Only two target frequencies: 4 MHz and 8 MHz. However, given the tolerance
* allowed in the WS2812 timing specs, two ranges around those target
* frequencies can be used for better flexibility. Extreme frequencies rounded
* to the nearest multiple of 100 kHz which meets the specs. Try to avoid
* using the extreme frequencies.
*
* If using an LED different to the WS2812 (e.g. WS2812B) check its timing
* specs, which may vary slightly, to decide which frequency is safe to use.
*
* WS2812 specs:
* T0H range: 200ns - 500ns
* T1H range: 550ns - 850ns
* Reset: low signal >50us
*/
#if CONFIG_WS2812_FREQUENCY >= 3600000 && CONFIG_WS2812_FREQUENCY <= 5000000
# define WS2812_ZERO_BYTE 0b01000000 /* 200ns at 5 MHz, 278ns at 3.6 MHz */
# define WS2812_ONE_BYTE 0b01110000 /* 600ns at 5 MHz, 833ns at 3.6 MHz */
#elif CONFIG_WS2812_FREQUENCY >= 5900000 && CONFIG_WS2812_FREQUENCY <= 9000000
# define WS2812_ZERO_BYTE 0b01100000 /* 222ns at 9 MHz, 339ns at 5.9 MHz */
# define WS2812_ONE_BYTE 0b01111100 /* 556ns at 9 MHz, 847ns at 5.9 MHz */
#else
# error "Unsupported SPI Frequency"
#endif
/* Reset bytes
* Number of empty bytes to create the reset low pulse
* Aiming for 60 us, safely above the 50us required.
*/
#define WS2812_RST_CYCLES (CONFIG_WS2812_FREQUENCY * 60 / 1000000 / 8)
#define WS2812_BYTES_PER_LED (8 * 3)
#define WS2812_RW_PIXEL_SIZE 4
/* Transmit buffer looks like:
* [<----reset bytes---->|<-RGBn->...<-RGB0->|<----reset bytes---->]
*
* It is important that this is shipped as close to one chunk as possible
* in order to meet timing requirements and to keep MOSI from going high
* between transactions. Some chips will leave MOSI at the state of the
* MSB of the last byte for this reason it is recommended to shift the
* bits that represents the zero or one waveform so that the MSB is 0.
* The reset clocks will pad the shortened low at the end.
*/
#define TXBUFF_SIZE(n) (WS2812_RST_CYCLES * 2 + n * WS2812_BYTES_PER_LED)
/****************************************************************************
* Private Types
****************************************************************************/
struct ws2812_dev_s
{
FAR struct spi_dev_s *spi; /* SPI interface */
uint16_t nleds; /* Number of addressable LEDs */
uint8_t *tx_buf; /* Buffer for write transaction and state */
sem_t exclsem; /* Assures exclusive access to the driver */
};
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
static inline void ws2812_configspi(FAR struct spi_dev_s *spi);
static void ws2812_pack(FAR uint8_t *buf, uint32_t rgb);
/* Character driver methods */
static int ws2812_open(FAR struct file *filep);
static int ws2812_close(FAR struct file *filep);
static ssize_t ws2812_read(FAR struct file *filep, FAR char *buffer,
size_t buflen);
static ssize_t ws2812_write(FAR struct file *filep, FAR const char *buffer,
size_t buflen);
static off_t ws2812_seek(FAR struct file *filep, off_t offset, int whence);
/****************************************************************************
* Private Data
****************************************************************************/
static const struct file_operations g_ws2812fops =
{
ws2812_open, /* open */
ws2812_close, /* close */
ws2812_read, /* read */
ws2812_write, /* write */
ws2812_seek, /* seek */
NULL, /* ioctl */
NULL, /* poll */
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
NULL /* unlink */
#endif
};
/****************************************************************************
* Private Functions
****************************************************************************/
/****************************************************************************
* Name: ws2812_configspi
*
* Description:
* Set the SPI bus configuration
*
****************************************************************************/
static inline void ws2812_configspi(FAR struct spi_dev_s *spi)
{
/* Configure SPI for the WS2812
* There is no CS on this device we just use MOSI and it is exclusive
*/
SPI_LOCK(spi, true); /* Exclusive use of the bus */
SPI_SETMODE(spi, SPIDEV_MODE3);
SPI_SETBITS(spi, 8);
SPI_HWFEATURES(spi, 0);
SPI_SETFREQUENCY(spi, CONFIG_WS2812_FREQUENCY);
}
/****************************************************************************
* Name: ws2812_pack
*
* Description:
* This writes the expanded SPI transaction to the transaction buffer
* for a given 24bit RGB value.
*
* Input Parameters:
* buf - The location in the transmit buffer to write.
* rgb - A 24bit RGB color 8bit red, 8-bit green, 8-bit blue
*
****************************************************************************/
static void ws2812_pack(FAR uint8_t *buf, uint32_t rgb)
{
uint8_t bit_idx;
uint8_t byte_idx;
uint8_t offset = 0;
uint8_t color;
uint32_t grb;
grb = (rgb & 0x00ff00) << 8;
grb |= (rgb & 0xff0000) >> 8;
grb |= rgb & 0x0000ff;
for (byte_idx = 0; byte_idx < 3; byte_idx++)
{
color = (uint8_t)(grb >> (8 * (2 - byte_idx)));
for (bit_idx = 0; bit_idx < 8; bit_idx++)
{
if (color & (1 << (7 - bit_idx)))
{
buf[offset] = WS2812_ONE_BYTE;
}
else
{
buf[offset] = WS2812_ZERO_BYTE;
}
offset++;
}
}
}
/****************************************************************************
* Name: ws2812_open
*
* Description:
* This function is called whenever the WS2812 device is opened.
*
****************************************************************************/
static int ws2812_open(FAR struct file *filep)
{
return OK;
}
/****************************************************************************
* Name: ws2812_close
*
* Description:
* This routine is called when the WS2812 device is closed.
*
****************************************************************************/
static int ws2812_close(FAR struct file *filep)
{
return OK;
}
/****************************************************************************
* Name: ws2812_read
****************************************************************************/
static ssize_t ws2812_read(FAR struct file *filep, FAR char *buffer,
size_t buflen)
{
return -ENOSYS;
}
/****************************************************************************
* Name: ws2812_write
*
* Description:
* This routine is called when writing to the WS2812 device. Data buffer
* should be an array of 32bit values holding 24bits of color information
* in host byte ordering 0x**rrggbb.
*
****************************************************************************/
static ssize_t ws2812_write(FAR struct file *filep, FAR const char *buffer,
size_t buflen)
{
FAR struct inode *inode = filep->f_inode;
FAR struct ws2812_dev_s *priv = inode->i_private;
FAR uint8_t *tx_pixel;
FAR uint32_t *pixel_buf = (FAR uint32_t *)buffer;
size_t cur_led;
size_t start_led;
size_t end_led;
size_t written = 0;
if (buffer == NULL)
{
lederr("ERROR: Buffer is null\n");
set_errno(EINVAL);
return -1;
}
/* We need at least one LED, so 1 byte */
if (buflen < 1)
{
lederr("ERROR: You need to control at least 1 LED!\n");
set_errno(EINVAL);
return -1;
}
if ((buflen % WS2812_RW_PIXEL_SIZE) != 0)
{
lederr("ERROR: LED values must be 24bit packed in 32bit\n");
set_errno(EINVAL);
return -1;
}
nxsem_wait(&priv->exclsem);
start_led = filep->f_pos / WS2812_RW_PIXEL_SIZE;
tx_pixel = priv->tx_buf + WS2812_RST_CYCLES + \
start_led * WS2812_BYTES_PER_LED;
end_led = start_led + (buflen / WS2812_RW_PIXEL_SIZE) - 1;
ledinfo("Start: %d End: %d\n", start_led, end_led);
if (end_led > (priv->nleds -1))
{
end_led = priv->nleds - 1;
}
for (cur_led = start_led; cur_led <= end_led; cur_led++)
{
ws2812_pack(tx_pixel, *pixel_buf & 0xffffff);
pixel_buf++;
tx_pixel += WS2812_BYTES_PER_LED;
written += WS2812_RW_PIXEL_SIZE;
}
SPI_SNDBLOCK(priv->spi, priv->tx_buf, TXBUFF_SIZE(priv->nleds));
/* Update LED position and handle case were we wrote the last LED */
filep->f_pos += written;
if (end_led == (priv->nleds - 1))
{
filep->f_pos -= WS2812_RW_PIXEL_SIZE;
}
nxsem_post(&priv->exclsem);
return written;
}
/****************************************************************************
* Name: ws2812_seek
*
* Description:
* This routine is called when seeking the WS2812 device. This can be used
* to address the starting LED to write. This should be done on a full
* color boundary which is 32bits. e.g. LED0 - offset 0, LED 8 - offset 32
*
****************************************************************************/
static off_t ws2812_seek(FAR struct file *filep, off_t offset, int whence)
{
FAR struct inode *inode = filep->f_inode;
FAR struct ws2812_dev_s *priv = inode->i_private;
off_t maxpos;
off_t pos;
if ((offset % WS2812_RW_PIXEL_SIZE) != 0)
{
return (off_t)-EINVAL;
}
nxsem_wait(&priv->exclsem);
maxpos = (priv->nleds - 1) * WS2812_RW_PIXEL_SIZE;
pos = filep->f_pos;
switch (whence)
{
case SEEK_CUR:
pos += offset;
if (pos > maxpos)
{
pos = maxpos;
}
else if (pos < 0)
{
pos = 0;
}
filep->f_pos = pos;
break;
case SEEK_SET:
pos = offset;
if (pos > maxpos)
{
pos = maxpos;
}
else if (pos < 0)
{
pos = 0;
}
filep->f_pos = pos;
break;
case SEEK_END:
pos = maxpos + offset;
if (pos > maxpos)
{
pos = maxpos;
}
else if (pos < 0)
{
pos = 0;
}
filep->f_pos = pos;
break;
default:
/* Return EINVAL if the whence argument is invalid */
pos = (off_t)-EINVAL;
break;
}
nxsem_post(&priv->exclsem);
return pos;
}
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: ws2812_leds_register
*
* Description:
* Register the WS2812 character device as 'devpath'
*
* Input Parameters:
* devpath - The full path to the driver to register. E.g., "/dev/leds0"
* spi - An instance of the SPI interface to use to communicate with
* WS2812
* nleds - Number of addressable LEDs
*
* Returned Value:
* Zero (OK) on success; a negated errno value on failure.
*
****************************************************************************/
int ws2812_leds_register(FAR const char *devpath, FAR struct spi_dev_s *spi,
uint16_t nleds)
{
FAR struct ws2812_dev_s *priv;
int ret;
int led;
/* Initialize the WS2812 device structure */
priv = (FAR struct ws2812_dev_s *)kmm_malloc(sizeof(struct ws2812_dev_s));
if (!priv)
{
lederr("ERROR: Failed to allocate instance\n");
return -ENOMEM;
}
priv->nleds = nleds;
priv->tx_buf = (FAR uint8_t *)kmm_zalloc(TXBUFF_SIZE(priv->nleds));
if (!priv->tx_buf)
{
lederr("ERROR: Failed to allocate tx buffer\n");
kmm_free(priv);
return -ENOMEM;
}
/* Mark LED section of TX buffer as off */
for (led = 0; led < priv->nleds; led++)
{
ws2812_pack(
priv->tx_buf + WS2812_RST_CYCLES + led * WS2812_BYTES_PER_LED,
0);
}
priv->spi = spi;
ws2812_configspi(priv->spi);
nxsem_init(&priv->exclsem, 0, 1);
SPI_SNDBLOCK(priv->spi, priv->tx_buf, TXBUFF_SIZE(priv->nleds));
/* Register the character driver */
ret = register_driver(devpath, &g_ws2812fops, 0666, priv);
if (ret < 0)
{
lederr("ERROR: Failed to register driver: %d\n", ret);
kmm_free(priv->tx_buf);
kmm_free(priv);
}
return ret;
}
#endif /* CONFIG_WS2812 */