1 /*
2 * Copyright (c) 2022 Jonathan Rico
3 *
4 * Adapted from the SPI driver, using the procedure in this blog post:
5 * https://electronut.in/nrf52-i2s-ws2812/
6 *
7 * Note: the word "word" refers to a 32-bit integer unless otherwise stated.
8 *
9 * WS/LRCK frequency:
10 * This refers to the "I2S word or channel select" clock.
11 * The I2C peripheral sends two 16-bit channel values for each clock period.
12 * A single LED color (8 data bits) will take up one 32-bit word or one LRCK
13 * period. This means a standard RGB led will take 3 LRCK periods to transmit.
14 *
15 * SPDX-License-Identifier: Apache-2.0
16 */
17
18 #define DT_DRV_COMPAT worldsemi_ws2812_i2s
19
20 #include <string.h>
21
22 #include <zephyr/drivers/led_strip.h>
23
24 #define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL
25 #include <zephyr/logging/log.h>
26 LOG_MODULE_REGISTER(ws2812_i2s);
27
28 #include <zephyr/device.h>
29 #include <zephyr/drivers/i2s.h>
30 #include <zephyr/dt-bindings/led/led.h>
31 #include <zephyr/kernel.h>
32 #include <zephyr/sys/util.h>
33
34 #define WS2812_I2S_PRE_DELAY_WORDS 1
35
36 struct ws2812_i2s_cfg {
37 struct device const *dev;
38 size_t tx_buf_bytes;
39 struct k_mem_slab *mem_slab;
40 uint8_t num_colors;
41 size_t length;
42 const uint8_t *color_mapping;
43 uint16_t reset_words;
44 uint32_t lrck_period;
45 uint32_t extra_wait_time_us;
46 bool active_low;
47 uint8_t nibble_one;
48 uint8_t nibble_zero;
49 };
50
51 /* Serialize an 8-bit color channel value into two 16-bit I2S values (or 1 32-bit
52 * word).
53 */
ws2812_i2s_ser(uint32_t * word,uint8_t color,const uint8_t sym_one,const uint8_t sym_zero)54 static inline void ws2812_i2s_ser(uint32_t *word, uint8_t color, const uint8_t sym_one,
55 const uint8_t sym_zero)
56 {
57 *word = 0;
58 for (uint16_t i = 0; i < 8; i++) {
59 if ((1 << i) & color) {
60 *word |= sym_one << (i * 4);
61 } else {
62 *word |= sym_zero << (i * 4);
63 }
64 }
65
66 /* Swap the two I2S values due to the (audio) channel TX order. */
67 *word = (*word >> 16) | (*word << 16);
68 }
69
ws2812_strip_update_rgb(const struct device * dev,struct led_rgb * pixels,size_t num_pixels)70 static int ws2812_strip_update_rgb(const struct device *dev, struct led_rgb *pixels,
71 size_t num_pixels)
72 {
73 const struct ws2812_i2s_cfg *cfg = dev->config;
74 uint8_t sym_one, sym_zero;
75 uint32_t reset_word;
76 uint32_t *tx_buf;
77 uint32_t flush_time_us;
78 void *mem_block;
79 int ret;
80
81 if (cfg->active_low) {
82 sym_one = (~cfg->nibble_one) & 0x0F;
83 sym_zero = (~cfg->nibble_zero) & 0x0F;
84 reset_word = 0xFFFFFFFF;
85 } else {
86 sym_one = cfg->nibble_one & 0x0F;
87 sym_zero = cfg->nibble_zero & 0x0F;
88 reset_word = 0;
89 }
90
91 /* Acquire memory for the I2S payload. */
92 ret = k_mem_slab_alloc(cfg->mem_slab, &mem_block, K_SECONDS(10));
93 if (ret < 0) {
94 LOG_ERR("Unable to allocate mem slab for TX (err %d)", ret);
95 return -ENOMEM;
96 }
97 tx_buf = (uint32_t *)mem_block;
98
99 /* Add a pre-data reset, so the first pixel isn't skipped by the strip. */
100 for (uint16_t i = 0; i < WS2812_I2S_PRE_DELAY_WORDS; i++) {
101 *tx_buf = reset_word;
102 tx_buf++;
103 }
104
105 /*
106 * Convert pixel data into I2S frames. Each frame has pixel data
107 * in color mapping on-wire format (e.g. GRB, GRBW, RGB, etc).
108 */
109 for (uint16_t i = 0; i < num_pixels; i++) {
110 for (uint16_t j = 0; j < cfg->num_colors; j++) {
111 uint8_t pixel;
112
113 switch (cfg->color_mapping[j]) {
114 /* White channel is not supported by LED strip API. */
115 case LED_COLOR_ID_WHITE:
116 pixel = 0;
117 break;
118 case LED_COLOR_ID_RED:
119 pixel = pixels[i].r;
120 break;
121 case LED_COLOR_ID_GREEN:
122 pixel = pixels[i].g;
123 break;
124 case LED_COLOR_ID_BLUE:
125 pixel = pixels[i].b;
126 break;
127 default:
128 return -EINVAL;
129 }
130 ws2812_i2s_ser(tx_buf, pixel, sym_one, sym_zero);
131 tx_buf++;
132 }
133 }
134
135 for (uint16_t i = 0; i < cfg->reset_words; i++) {
136 *tx_buf = reset_word;
137 tx_buf++;
138 }
139
140 /* Flush the buffer on the wire. */
141 ret = i2s_write(cfg->dev, mem_block, cfg->tx_buf_bytes);
142 if (ret < 0) {
143 k_mem_slab_free(cfg->mem_slab, mem_block);
144 LOG_ERR("Failed to write data: %d", ret);
145 return ret;
146 }
147
148 ret = i2s_trigger(cfg->dev, I2S_DIR_TX, I2S_TRIGGER_START);
149 if (ret < 0) {
150 LOG_ERR("Failed to trigger command %d on TX: %d", I2S_TRIGGER_START, ret);
151 return ret;
152 }
153
154 ret = i2s_trigger(cfg->dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN);
155 if (ret < 0) {
156 LOG_ERR("Failed to trigger command %d on TX: %d", I2S_TRIGGER_DRAIN, ret);
157 return ret;
158 }
159
160 /* Wait until transaction is over */
161 flush_time_us = cfg->lrck_period * cfg->tx_buf_bytes / sizeof(uint32_t);
162 k_usleep(flush_time_us + cfg->extra_wait_time_us);
163
164 return ret;
165 }
166
ws2812_strip_length(const struct device * dev)167 static size_t ws2812_strip_length(const struct device *dev)
168 {
169 const struct ws2812_i2s_cfg *cfg = dev->config;
170
171 return cfg->length;
172 }
173
ws2812_i2s_init(const struct device * dev)174 static int ws2812_i2s_init(const struct device *dev)
175 {
176 const struct ws2812_i2s_cfg *cfg = dev->config;
177 struct i2s_config config;
178 uint32_t lrck_hz;
179 int ret;
180
181 lrck_hz = USEC_PER_SEC / cfg->lrck_period;
182 LOG_DBG("Word clock: freq %u Hz period %u us",
183 lrck_hz, cfg->lrck_period);
184
185 /* 16-bit stereo, 100kHz LCLK */
186 config.word_size = 16;
187 config.channels = 2;
188 config.format = I2S_FMT_DATA_FORMAT_I2S;
189 config.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER;
190 config.frame_clk_freq = lrck_hz; /* WS (or LRCK) */
191 config.mem_slab = cfg->mem_slab;
192 config.block_size = cfg->tx_buf_bytes;
193 config.timeout = 1000;
194
195 ret = i2s_configure(cfg->dev, I2S_DIR_TX, &config);
196 if (ret < 0) {
197 LOG_ERR("Failed to configure I2S device: %d\n", ret);
198 return ret;
199 }
200
201 for (uint16_t i = 0; i < cfg->num_colors; i++) {
202 switch (cfg->color_mapping[i]) {
203 case LED_COLOR_ID_WHITE:
204 case LED_COLOR_ID_RED:
205 case LED_COLOR_ID_GREEN:
206 case LED_COLOR_ID_BLUE:
207 break;
208 default:
209 LOG_ERR("%s: invalid channel to color mapping."
210 "Check the color-mapping DT property",
211 dev->name);
212 return -EINVAL;
213 }
214 }
215
216 return 0;
217 }
218
219 static const struct led_strip_driver_api ws2812_i2s_api = {
220 .update_rgb = ws2812_strip_update_rgb,
221 .length = ws2812_strip_length,
222 };
223
224 /* Integer division, but always rounds up: e.g. 10/3 = 4 */
225 #define WS2812_ROUNDED_DIVISION(x, y) ((x + (y - 1)) / y)
226
227 #define WS2812_I2S_LRCK_PERIOD_US(idx) DT_INST_PROP(idx, lrck_period)
228
229 #define WS2812_RESET_DELAY_US(idx) DT_INST_PROP(idx, reset_delay)
230 /* Rounds up to the next 20us. */
231 #define WS2812_RESET_DELAY_WORDS(idx) WS2812_ROUNDED_DIVISION(WS2812_RESET_DELAY_US(idx), \
232 WS2812_I2S_LRCK_PERIOD_US(idx))
233
234 #define WS2812_NUM_COLORS(idx) (DT_INST_PROP_LEN(idx, color_mapping))
235
236 #define WS2812_I2S_NUM_PIXELS(idx) (DT_INST_PROP(idx, chain_length))
237
238 #define WS2812_I2S_BUFSIZE(idx) \
239 (((WS2812_NUM_COLORS(idx) * WS2812_I2S_NUM_PIXELS(idx)) + \
240 WS2812_I2S_PRE_DELAY_WORDS + WS2812_RESET_DELAY_WORDS(idx)) * 4)
241
242 #define WS2812_I2S_DEVICE(idx) \
243 \
244 K_MEM_SLAB_DEFINE_STATIC(ws2812_i2s_##idx##_slab, WS2812_I2S_BUFSIZE(idx), 2, 4); \
245 \
246 static const uint8_t ws2812_i2s_##idx##_color_mapping[] = \
247 DT_INST_PROP(idx, color_mapping); \
248 \
249 static const struct ws2812_i2s_cfg ws2812_i2s_##idx##_cfg = { \
250 .dev = DEVICE_DT_GET(DT_INST_PROP(idx, i2s_dev)), \
251 .tx_buf_bytes = WS2812_I2S_BUFSIZE(idx), \
252 .mem_slab = &ws2812_i2s_##idx##_slab, \
253 .num_colors = WS2812_NUM_COLORS(idx), \
254 .length = DT_INST_PROP(idx, chain_length), \
255 .color_mapping = ws2812_i2s_##idx##_color_mapping, \
256 .lrck_period = WS2812_I2S_LRCK_PERIOD_US(idx), \
257 .extra_wait_time_us = DT_INST_PROP(idx, extra_wait_time), \
258 .reset_words = WS2812_RESET_DELAY_WORDS(idx), \
259 .active_low = DT_INST_PROP(idx, out_active_low), \
260 .nibble_one = DT_INST_PROP(idx, nibble_one), \
261 .nibble_zero = DT_INST_PROP(idx, nibble_zero), \
262 }; \
263 \
264 DEVICE_DT_INST_DEFINE(idx, ws2812_i2s_init, NULL, NULL, &ws2812_i2s_##idx##_cfg, \
265 POST_KERNEL, CONFIG_LED_STRIP_INIT_PRIORITY, &ws2812_i2s_api);
266
267 DT_INST_FOREACH_STATUS_OKAY(WS2812_I2S_DEVICE)
268