| /* |
| * Copyright (c) 2022 Esco Medical ApS |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT ti_tlc5971 |
| |
| #include <zephyr/kernel.h> |
| #include <zephyr/drivers/spi.h> |
| #include <zephyr/drivers/led_strip.h> |
| #include <zephyr/drivers/led_strip/tlc5971.h> |
| #include <zephyr/dt-bindings/led/led.h> |
| #include <zephyr/sys/util.h> |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(tlc5971, CONFIG_LED_STRIP_LOG_LEVEL); |
| |
| struct tlc5971_config { |
| struct spi_dt_spec bus; |
| const uint8_t *color_mapping; |
| uint8_t num_pixels; |
| uint8_t num_colors; |
| }; |
| |
| struct tlc5971_data { |
| uint8_t *data_buffer; |
| uint8_t gbc_color_1; |
| uint8_t gbc_color_2; |
| uint8_t gbc_color_3; |
| uint8_t control_data; |
| }; |
| |
| /** SPI operation word constant, SPI mode 0, CPOL = 0, CPHA = 0 */ |
| #define TLC5971_SPI_OPERATION (SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8)) |
| |
| /** Number of supported colors */ |
| #define TLC5971_NUMBER_OF_COLORS 3 |
| |
| /** |
| * @brief Number of RGB pixels per TLC5791 device |
| * |
| * The TLC5971 has 4x RGB outputs per device, where each RGB group constitues a pixel from this |
| * drivers point of view. |
| */ |
| #define TLC5971_PIXELS_PER_DEVICE 4 |
| |
| /** Length in bytes of data packet per TLC5791 device */ |
| #define TLC5971_PACKET_LEN 28 |
| |
| /** write command for writing control data and GS data to internal registers */ |
| #define TLC5971_WRITE_COMMAND 0x25 |
| |
| /** GS reference clock select bit in FC data (0 = internal oscillator clock, 1 = SCKI clock). */ |
| #define TLC5971_BYTE27_CTRL_BIT_EXTGCK BIT(0) |
| |
| /** GS reference clock edge select bit for OUTXn on-off timing control in FC data */ |
| #define TLC5971_BYTE27_CTRL_BIT_OUTTMG BIT(1) |
| |
| /** Constant-current output enable bit in FC data (0 = output control enabled, 1 = blank). */ |
| #define TLC5971_BYTE26_CTRL_BIT_BLANK BIT(5) |
| |
| /** Auto display repeat mode enable bit in FC data (0 = disabled, 1 = enabled). */ |
| #define TLC5971_BYTE26_CTRL_BIT_DSPRPT BIT(6) |
| |
| /** Display timing reset mode enable bit in FC data (0 = disabled, 1 = enabled). */ |
| #define TLC5971_BYTE26_CTRL_BIT_TMGRST BIT(7) |
| |
| /** Bit mask for write cmd in data byte 27 */ |
| #define TLC5971_BYTE27_WRITE_CMD_MASK GENMASK(7, 2) |
| |
| /** Bit mask for control bits in data byte 27 */ |
| #define TLC5971_BYTE27_CTRL_MASK GENMASK(1, 0) |
| |
| /** Bit mask for control bits in data byte 26 */ |
| #define TLC5971_BYTE26_CTRL_MASK GENMASK(7, 5) |
| |
| /** Bit mask for global brightness control for color 1 in data byte 26, upper 5 bits of GBC */ |
| #define TLC5971_BYTE26_GBC1_MASK GENMASK(4, 0) |
| |
| /** Bit mask for global brightness control for color 1 in data byte 25, lower 2 bits of GBC */ |
| #define TLC5971_BYTE25_GBC1_MASK GENMASK(7, 6) |
| |
| /** Bit mask for global brightness control for color 2 in data byte 25, upper 6 bits of GBC */ |
| #define TLC5971_BYTE25_GBC2_MASK GENMASK(5, 0) |
| |
| /** Bit mask for global brightness control for color 2 in data byte 24, lower 1 bits of GBC */ |
| #define TLC5971_BYTE24_GBC2_MASK BIT(7) |
| |
| /** Bit mask for global brightness control for color 3 in data byte 24, all 7 bits of GBC */ |
| #define TLC5971_BYTE24_GBC3_MASK GENMASK(6, 0) |
| |
| /** |
| * @brief create data byte 27 from control data |
| * |
| * @param control_data control bits |
| * @return uint8_t the serialized data byte 27 |
| */ |
| static inline uint8_t tlc5971_data_byte27(uint8_t control_data) |
| { |
| return FIELD_PREP(TLC5971_BYTE27_WRITE_CMD_MASK, TLC5971_WRITE_COMMAND) | |
| FIELD_PREP(TLC5971_BYTE27_CTRL_MASK, control_data); |
| } |
| |
| /** |
| * @brief create data byte 26 from control data and color 1 GBC |
| * |
| * @param control_data control bits |
| * @param gbc_color_1 global brightness control for color 1 LEDs |
| * @return uint8_t the serialized data byte 26 |
| */ |
| static inline uint8_t tlc5971_data_byte26(uint8_t control_data, uint8_t gbc_color_1) |
| { |
| return FIELD_PREP(TLC5971_BYTE26_CTRL_MASK, control_data) | |
| FIELD_PREP(TLC5971_BYTE26_GBC1_MASK, gbc_color_1 >> 2); |
| } |
| |
| /** |
| * @brief create data byte 25 from color 1 and 2 GBC |
| * |
| * @param gbc_color_1 global brightness control for color 1 LEDs |
| * @param gbc_color_2 global brightness control for color 2 LEDs |
| * @return uint8_t the serialized data byte 25 |
| */ |
| static inline uint8_t tlc5971_data_byte25(uint8_t gbc_color_1, uint8_t gbc_color_2) |
| { |
| return FIELD_PREP(TLC5971_BYTE25_GBC1_MASK, gbc_color_1 << 6) | |
| FIELD_PREP(TLC5971_BYTE25_GBC2_MASK, gbc_color_2 >> 1); |
| } |
| |
| /** |
| * @brief create data byte 24 from color 2 and 3 GBC |
| * |
| * @param gbc_color_2 global brightness control for color 2 LEDs |
| * @param gbc_color_3 global brightness control for color 3 LEDs |
| * @return uint8_t the serialized data byte 24 |
| */ |
| static inline uint8_t tlc5971_data_byte24(uint8_t gbc_color_2, uint8_t gbc_color_3) |
| { |
| return FIELD_PREP(TLC5971_BYTE24_GBC2_MASK, gbc_color_2 << 7) | |
| FIELD_PREP(TLC5971_BYTE24_GBC3_MASK, gbc_color_3); |
| } |
| |
| /** |
| * @brief map user colors to tlc5971 color order |
| * |
| * @param color_id color id from color mapping |
| * @param pixel_data rgb data to be mapped |
| * @return the mapped color value |
| */ |
| static uint8_t tlc5971_map_color(int color_id, const struct led_rgb *pixel_data) |
| { |
| uint8_t temp = 0; |
| |
| switch (color_id) { |
| case LED_COLOR_ID_RED: |
| temp = pixel_data->r; |
| break; |
| case LED_COLOR_ID_GREEN: |
| temp = pixel_data->g; |
| break; |
| case LED_COLOR_ID_BLUE: |
| temp = pixel_data->b; |
| break; |
| default: |
| temp = 0; |
| break; |
| } |
| |
| return temp; |
| } |
| |
| /** |
| * @brief serialize control data and pixel data for device daisy chain |
| * |
| * the serializer only supports "full" devices, meaning each device is expected |
| * to be mounted with all 4 LEDs. |
| * |
| * @param dev device pointer |
| * @param pixels pixel RGB data for daisy chain |
| * @param num_pixels number of pixels in daisy chain |
| */ |
| static void tlc5971_fill_data_buffer(const struct device *dev, struct led_rgb *pixels, |
| size_t num_pixels) |
| { |
| const struct tlc5971_config *cfg = dev->config; |
| struct tlc5971_data *data = dev->data; |
| uint8_t *data_buffer = data->data_buffer; |
| int count = 0; |
| |
| /* |
| * tlc5971 device order is reversed as the rgb data for the last device in the daisy chain |
| * should be transmitted first. |
| */ |
| for (int device = (num_pixels / TLC5971_PIXELS_PER_DEVICE) - 1; device >= 0; device--) { |
| /* |
| * The SPI frame format expects a BGR color order for the global brightness control |
| * values, but since the led_strip API allows custom color mappings, we simply use |
| * color_x terms to keep things generic. |
| */ |
| data_buffer[count++] = tlc5971_data_byte27(data->control_data); |
| data_buffer[count++] = tlc5971_data_byte26(data->control_data, data->gbc_color_1); |
| data_buffer[count++] = tlc5971_data_byte25(data->gbc_color_1, data->gbc_color_2); |
| data_buffer[count++] = tlc5971_data_byte24(data->gbc_color_2, data->gbc_color_3); |
| |
| for (int pixel = (TLC5971_PIXELS_PER_DEVICE - 1); pixel >= 0; pixel--) { |
| /* data is "reversed" so RGB0 comes last, i.e at byte 0 */ |
| const struct led_rgb *pixel_data = |
| &pixels[(device * TLC5971_PIXELS_PER_DEVICE) + pixel]; |
| |
| /* |
| * Convert pixel data into SPI frames, mapping user colors to tlc5971 |
| * data frame color order (BGR). |
| */ |
| for (int color = 0; color < cfg->num_colors; color++) { |
| uint8_t temp = |
| tlc5971_map_color(cfg->color_mapping[color], pixel_data); |
| |
| /* |
| * The tlc5971 rgb values are 16 bit but zephyr's rgb values are |
| * 8 bit. Simply upscale to 16 bit by using the 8 bit value for both |
| * LSB and MSB of the 16 bit word. |
| */ |
| data_buffer[count++] = temp; |
| data_buffer[count++] = temp; |
| } |
| } |
| } |
| } |
| |
| static int tlc5971_transmit_data(const struct device *dev, size_t num_pixels) |
| { |
| const struct tlc5971_config *cfg = dev->config; |
| struct tlc5971_data *data = dev->data; |
| |
| struct spi_buf buf = { |
| .buf = data->data_buffer, |
| .len = (num_pixels / TLC5971_PIXELS_PER_DEVICE) * TLC5971_PACKET_LEN, |
| }; |
| |
| const struct spi_buf_set tx = { |
| .buffers = &buf, |
| .count = 1, |
| }; |
| |
| return spi_write_dt(&cfg->bus, &tx); |
| } |
| |
| static int tlc5971_update_rgb(const struct device *dev, struct led_rgb *pixels, size_t num_pixels) |
| { |
| const struct tlc5971_config *cfg = dev->config; |
| |
| if (num_pixels > cfg->num_pixels) { |
| LOG_ERR("invalid number of pixels, %zu vs actual %i", num_pixels, cfg->num_pixels); |
| return -EINVAL; |
| } |
| |
| tlc5971_fill_data_buffer(dev, pixels, num_pixels); |
| |
| return tlc5971_transmit_data(dev, num_pixels); |
| } |
| |
| static int tlc5971_update_channels(const struct device *dev, uint8_t *channels, size_t num_channels) |
| { |
| ARG_UNUSED(dev); |
| ARG_UNUSED(channels); |
| ARG_UNUSED(num_channels); |
| |
| return -ENOTSUP; |
| } |
| |
| int tlc5971_set_global_brightness(const struct device *dev, struct led_rgb pixel) |
| { |
| const struct tlc5971_config *cfg = dev->config; |
| struct tlc5971_data *data = dev->data; |
| int res = -EINVAL; |
| |
| if ((pixel.r <= TLC5971_GLOBAL_BRIGHTNESS_CONTROL_MAX) && |
| (pixel.g <= TLC5971_GLOBAL_BRIGHTNESS_CONTROL_MAX) && |
| (pixel.b <= TLC5971_GLOBAL_BRIGHTNESS_CONTROL_MAX)) { |
| data->gbc_color_1 = tlc5971_map_color(cfg->color_mapping[0], &pixel); |
| data->gbc_color_2 = tlc5971_map_color(cfg->color_mapping[1], &pixel); |
| data->gbc_color_3 = tlc5971_map_color(cfg->color_mapping[2], &pixel); |
| res = 0; |
| } |
| |
| return res; |
| } |
| |
| static int tlc5971_init(const struct device *dev) |
| { |
| const struct tlc5971_config *cfg = dev->config; |
| struct tlc5971_data *data = dev->data; |
| |
| if (!spi_is_ready_dt(&cfg->bus)) { |
| LOG_ERR("%s: SPI device %s not ready", dev->name, cfg->bus.bus->name); |
| return -ENODEV; |
| } |
| |
| if ((cfg->num_pixels % TLC5971_PIXELS_PER_DEVICE) != 0) { |
| LOG_ERR("%s: chain length must be multiple of 4", dev->name); |
| return -EINVAL; |
| } |
| |
| if (cfg->num_colors != TLC5971_NUMBER_OF_COLORS) { |
| LOG_ERR("%s: the tlc5971 only supports %i colors", dev->name, |
| TLC5971_NUMBER_OF_COLORS); |
| return -EINVAL; |
| } |
| |
| for (int i = 0; i < cfg->num_colors; i++) { |
| switch (cfg->color_mapping[i]) { |
| case LED_COLOR_ID_RED: |
| case LED_COLOR_ID_GREEN: |
| case LED_COLOR_ID_BLUE: |
| break; |
| default: |
| LOG_ERR("%s: invalid color mapping", dev->name); |
| return -EINVAL; |
| } |
| } |
| |
| /* |
| * set up sane defaults for control data. |
| * unblanks leds, enables auto display repeat, enables timing resetm uses internal clock for |
| * PWM generation and sets the GS reference clock edge select to rising edge |
| */ |
| data->control_data = TLC5971_BYTE27_CTRL_BIT_OUTTMG | TLC5971_BYTE26_CTRL_BIT_DSPRPT | |
| TLC5971_BYTE26_CTRL_BIT_TMGRST; |
| |
| return 0; |
| } |
| |
| static const struct led_strip_driver_api tlc5971_api = { |
| .update_rgb = tlc5971_update_rgb, |
| .update_channels = tlc5971_update_channels, |
| }; |
| |
| #define TLC5971_DATA_BUFFER_LENGTH(inst) \ |
| (DT_INST_PROP(inst, chain_length) / TLC5971_PIXELS_PER_DEVICE) * TLC5971_PACKET_LEN |
| |
| #define TLC5971_DEVICE(inst) \ |
| static const uint8_t tlc5971_##inst##_color_mapping[] = DT_INST_PROP(inst, color_mapping); \ |
| static const struct tlc5971_config tlc5971_##inst##_config = { \ |
| .bus = SPI_DT_SPEC_INST_GET(inst, TLC5971_SPI_OPERATION, 0), \ |
| .num_pixels = DT_INST_PROP(inst, chain_length), \ |
| .num_colors = DT_INST_PROP_LEN(inst, color_mapping), \ |
| .color_mapping = tlc5971_##inst##_color_mapping, \ |
| }; \ |
| static uint8_t tlc5971_##inst##_data_buffer[TLC5971_DATA_BUFFER_LENGTH(inst)]; \ |
| static struct tlc5971_data tlc5971_##inst##_data = { \ |
| .data_buffer = tlc5971_##inst##_data_buffer, \ |
| .gbc_color_1 = TLC5971_GLOBAL_BRIGHTNESS_CONTROL_MAX, \ |
| .gbc_color_2 = TLC5971_GLOBAL_BRIGHTNESS_CONTROL_MAX, \ |
| .gbc_color_3 = TLC5971_GLOBAL_BRIGHTNESS_CONTROL_MAX, \ |
| }; \ |
| DEVICE_DT_INST_DEFINE(inst, tlc5971_init, NULL, &tlc5971_##inst##_data, \ |
| &tlc5971_##inst##_config, POST_KERNEL, \ |
| CONFIG_LED_STRIP_INIT_PRIORITY, &tlc5971_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(TLC5971_DEVICE) |