DWM3001CDK点亮WS2812 RGB LED
背景
去年参加了FastBond的第四期活动, 为了能够白嫖到这块开发板子, 不得不掏出这块板子在Deadline之前完成一个项目.
看了下活动要求, 感觉点灯是最简单的. 不过既然点灯了, 就点个稍微有点难度的吧.
比如点亮能让性能增加200%的WS2812 RGB LED.
开发板介绍

- 板子上带有DWM3001C模块
- 可以和Apple U1 chip & U2 互操作
- 支持UWB channels 5 (6.5 GHz) and 9 (8 GHz)
- 板载J-Link用于调试和烧录
- USB接口用于直接连接DWM3001C USB接口
- 26 pin 树莓派兼容接口
- 带有reset和一个用户按钮,板载LED
- 所有的DWM3001C GPIOs和接口都引出
DWM3001C内部具有一个nRF52833芯片,实际的代码其实就是运行在这颗芯片上.
开发工具也都是基于nRF52833的SDK.
控制普通LED
nRF的文档和资料都很齐全, 不仅有点灯的例子, 连板子的LED和Button的引脚定义也都预定义好了.
话不多说 , 直接打开官方的例子看看.
虽然之前完全没接触过nRF的SDK, 但是基于以前ESP开发经验, 理解示例代码并不困难.
示例工程启动时候会看到板载的4颗LED会闪烁, 对着启动代码查抄到实际代码是
1 | void BoardInit(void) |
甚至连LED控制也贴心的封装好了,bsp_board_led_on和bsp_board_led_off,
同时还看到了在串口刷新时候闪烁LED的代码1
2
3
4
5
6
7
8
9
10
11
12static void logger_thread(void *arg) {
UNUSED_PARAMETER(arg);
while (1) {
NRF_LOG_FLUSH();
bsp_board_led_on(BSP_BOARD_LED_1);
nrf_delay_ms(1);
bsp_board_led_off(BSP_BOARD_LED_1);
nrf_delay_ms(1);
vTaskSuspend(NULL); // Suspend myself
}
}
编写WS2812的驱动
驱动WS2812的办法有很多, 视情况选择最合适的驱动方式.
IO模拟(Bit-Banging)
这是最经典的方式, 如果芯片比较简单, 没有SPI接口, 或者SPI接口被占用了, 可以考虑使用IO模拟. 缺点是消耗CPU资源. Arduino Uno就是用这种方式驱动WS2812的1.
PWM + DMA模拟
这个方式相当于IO模拟的优化版本, 引入DMA之后, 释放了CPU资源压力.
SPI模拟
这是一种曲线救国办法, 利用SPI的发送机制来模拟WS2812的协议, 适合SPI接口空闲的场景.
配合SPI DMA的情况下, 几乎不消耗CPU资源.
RMT模拟
这是ESP32芯片的驱动方式, 利用RMT外设来模拟WS2812的协议.最为优雅, 模拟的时序非常精准, 且不需要CPU干预.2.
最终选择
考虑到实现难度和实际效果, 我决定采用SPI模拟的办法在DWM3001CDK上实现WS2812的驱动模拟.
驱动编写
参照https://www.cnblogs.com/milton/p/17892606.html, 将其中的spi驱动修改为nrf52833版本.
编写初始化SPI代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void ws2812_init(void) {
nrf_gpio_cfg_output(30);
nrf_gpio_cfg_output(27);
nrf_gpio_cfg_output(31);
memset(ws2812_buffer, 0, WS2812_BUFFER_SIZE);
nrf_drv_spi_config_t spi_config = NRF_DRV_SPI_DEFAULT_CONFIG;
spi_config.ss_pin = NRF_GPIO_PIN_MAP(0, 30);
spi_config.miso_pin = NRF_DRV_SPI_PIN_NOT_USED;
spi_config.mosi_pin = NRF_GPIO_PIN_MAP(0, 27);
spi_config.sck_pin = NRF_GPIO_PIN_MAP(0, 31);
spi_config.frequency = NRF_DRV_SPI_FREQ_4M;
spi_config.mode = NRF_DRV_SPI_MODE_0; // SPI 模式 0
int spi_ret = nrf_drv_spi_init(&spi, &spi_config, spi_event_handler, NULL);
NRF_LOG_ERROR("ws2812_init() = %d", spi_ret);
}
编写发送函数代码1
2
3
4
5
6
7
8
9NRF_LOG_INFO("ws2812_send_spi()");
APP_ERROR_CHECK(nrf_drv_spi_transfer(&spi, ws2812_buffer, WS2812_BUFFER_SIZE, NULL, 0));
while (!spi_xfer_done) {
__WFE();
}
// 发送复位信号(保持 MOSI 低电平至少 50µs)
nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(0, 27));
nrf_delay_us(50);
需要修改nrf_drv_spi_transfer声明和定义,将第三个参数修改为size_t类型, 不然无法一次性传输WS2812_BUFFER_SIZE大小的数据导致颜色异常.
编写颜色设置函数, 增加颜色转换函数.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66void ws2812_pixel(uint16_t led_no, uint8_t r, uint8_t g, uint8_t b) {
uint8_t *ptr = &ws2812_buffer[led_no * 24];
WS2812_FILL_BUFFER(g);
WS2812_FILL_BUFFER(r);
WS2812_FILL_BUFFER(b);
}
void ws2812_pixel_all(uint8_t r, uint8_t g, uint8_t b) {
uint8_t *ptr = ws2812_buffer;
for (uint16_t i = 0; i < WS2812_NUM_LEDS; ++i) {
WS2812_FILL_BUFFER(g);
WS2812_FILL_BUFFER(r);
WS2812_FILL_BUFFER(b);
}
}
void hsv_to_rgb(uint8_t h, uint8_t s, uint8_t v, uint8_t *r, uint8_t *g, uint8_t *b) {
uint8_t region, remainder, p, q, t;
if (s == 0) {
*r = v;
*g = v;
*b = v;
return;
}
region = h / 43;
remainder = (h - (region * 43)) * 6;
p = (v * (255 - s)) >> 8;
q = (v * (255 - ((s * remainder) >> 8))) >> 8;
t = (v * (255 - ((s * (255 - remainder)) >> 8))) >> 8;
switch (region) {
case 0:
*r = v;
*g = t;
*b = p;
break;
case 1:
*r = q;
*g = v;
*b = p;
break;
case 2:
*r = p;
*g = v;
*b = t;
break;
case 3:
*r = p;
*g = q;
*b = v;
break;
case 4:
*r = t;
*g = p;
*b = v;
break;
default:
*r = v;
*g = p;
*b = q;
break;
}
}
最后加上经典的渐变函数
1 | void ws2812_row_gradient(uint16_t i) { |
第一颗灯珠颜色异常修复
上面的代码编写完成烧录运行后, 发现第一个灯珠总是绿色,
一番搜索后找到了修复办法, 核心思想是就是在发送数据前, 先发送几个零字节.3
所以我们为buffer增加头部零字节1
2
3
4
同时更新初始化代码, 颜色设置和发送函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void ws2812_send_spi(void) {
NRF_LOG_INFO("ws2812_send_spi()");
memset(ws2812_buffer, 0, WS2812_EXTRA_ZEROS);
APP_ERROR_CHECK(nrf_drv_spi_transfer(&spi, ws2812_buffer, WS2812_BUFFER_SIZE, NULL, 0));
while (!spi_xfer_done) {
__WFE();
}
// 发送复位信号(保持 MOSI 低电平至少 50µs)
nrf_gpio_pin_clear(NRF_GPIO_PIN_MAP(0, 27));
nrf_delay_us(50);
}
void ws2812_pixel(uint16_t led_no, uint8_t r, uint8_t g, uint8_t b) {
uint8_t *ptr = &ws2812_buffer[WS2812_EXTRA_ZEROS + led_no * 24];
WS2812_FILL_BUFFER(g);
WS2812_FILL_BUFFER(r);
WS2812_FILL_BUFFER(b);
}
再次编写测试代码,检查最终效果是否正常1
2
3
4
5
6
7
8
9
10
11ws2812_init();
uint16_t i = 0;
while (1) {
// 列渐变效果
ws2812_col_gradient(i);
ws2812_send_spi();
nrf_delay_ms(5);
// 更新色相值
i = (i + 1) % 256;
}
这一次第一个LED终于听话了.
数字显示
手动编写一个字库, 并且按照对应的位置填充到buffer中.
实际运行如下.
这里我接了一个温湿度传感器, 屏幕会用不同颜色展示温度和湿度数值.
上图中展示的是当前湿度53%.
总结
通过 SPI 模拟时序驱动 WS2812 是一种在硬件资源受限且无 RMT 外设的情况下的绝佳选择。在 DWM3001CDK 上实现这一功能,不仅达到了 FastBond 项目的要求,也为后续开发更复杂的 UWB 定位可视化界面打下了基础。
1. https://github.com/adafruit/Adafruit_NeoPixel/blob/master/Adafruit_NeoPixel.cpp ↩
2. https://docs.espressif.com/projects/arduino-esp32/en/latest/api/rmt.html ↩
3. https://www.reddit.com/r/arduino/comments/499ods/strip_of_144_ws2812b_leds_first_led_stuck_green/ ↩
DWM3001CDK点亮WS2812 RGB LED
