跳至内容
返回

DWM3001CDK点亮WS2812 RGB LED

发布于:  at  11:05 下午

背景

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

开发板介绍

DWM3001CDK Dev Board

DWM3001C内部具有一个nRF52833芯片,实际的代码其实就是运行在这颗芯片上. 开发工具也都是基于nRF52833的SDK.

控制普通LED

nRF的文档和资料都很齐全, 不仅有点灯的例子, 连板子的LED和Button的引脚定义也都预定义好了.

话不多说 , 直接打开官方的例子看看. 虽然之前完全没接触过nRF的SDK, 但是基于以前ESP开发经验, 理解示例代码并不困难.

示例工程启动时候会看到板载的4颗LED会闪烁, 对着启动代码查抄到实际代码是

void BoardInit(void)
{
    bsp_board_init(BSP_INIT_LEDS | BSP_INIT_BUTTONS);
    peripherals_init();

    for (int i = 0; i < 6; i++)
    {
        bsp_board_led_invert(BSP_BOARD_LED_0);
        bsp_board_led_invert(BSP_BOARD_LED_1);
        bsp_board_led_invert(BSP_BOARD_LED_2);
        bsp_board_led_invert(BSP_BOARD_LED_3);
        nrf_delay_ms(250);
    }
}

甚至连LED控制也贴心的封装好了,bsp_board_led_on和bsp_board_led_off,

同时还看到了在串口刷新时候闪烁LED的代码

static 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代码

void 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);
}

编写发送函数代码

  NRF_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大小的数据导致颜色异常.

编写颜色设置函数, 增加颜色转换函数.

void 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;
  }
}

最后加上经典的渐变函数

void ws2812_row_gradient(uint16_t i) {
  for (uint8_t row = 0; row < WS2812_ROWS; row++) {
    for (uint8_t col = 0; col < WS2812_COLS; col++) {
      // 计算当前 LED 的色相值
      uint8_t hue = (i + row * 256 / WS2812_ROWS) % 256;
      uint8_t r, g, b;

      // 将 HSV 转换为 RGB,亮度设置为 50
      hsv_to_rgb(hue, 255, 16, &r, &g, &b);

      // 计算 LED 索引
      uint16_t led = row * WS2812_COLS + col;

      // 设置 LED 颜色
      ws2812_pixel(led, r, g, b);
    }
  }
}

void ws2812_col_gradient(uint16_t i) {
  for (uint8_t col = 0; col < WS2812_COLS; col++) {
    for (uint8_t row = 0; row < WS2812_ROWS; row++) {
      // 计算当前 LED 的色相值
      uint8_t hue = (i + col * 256 / WS2812_COLS) % 256;
      uint8_t r, g, b;

      // 将 HSV 转换为 RGB,亮度设置为 50
      hsv_to_rgb(hue, 255, 50, &r, &g, &b);

      // 计算 LED 索引
      uint16_t led = row * WS2812_COLS + col;

      // 设置 LED 颜色
      ws2812_pixel(led, r, g, b);
    }
  }
}

第一颗灯珠颜色异常修复

上面的代码编写完成烧录运行后, 发现第一个灯珠总是绿色, 一番搜索后找到了修复办法, 核心思想是就是在发送数据前, 先发送几个零字节.3

所以我们为buffer增加头部零字节

#define WS2812_NUM_LEDS 60      // 6x10 灯板,共 60 个 LED
#define WS2812_RESET_PULSE 60
#define WS2812_EXTRA_ZEROS 4    // 添加 4 个零字节
#define WS2812_BUFFER_SIZE (WS2812_NUM_LEDS * 24 + WS2812_RESET_PULSE + WS2812_EXTRA_ZEROS)

同时更新初始化代码, 颜色设置和发送函数

void 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);
}

再次编写测试代码,检查最终效果是否正常

  ws2812_init();
  uint16_t i = 0;
  while (1) {
    // 列渐变效果
    ws2812_col_gradient(i);
    ws2812_send_spi();
    nrf_delay_ms(5);

    // 更新色相值
    i = (i + 1) % 256;
  }

这一次第一个LED终于听话了.

数字显示

手动编写一个字库, 并且按照对应的位置填充到buffer中. 实际运行如下. ws2812_test 这里我接了一个温湿度传感器, 屏幕会用不同颜色展示温度和湿度数值. 上图中展示的是当前湿度53%.

总结

通过 SPI 模拟时序驱动 WS2812 是一种在硬件资源受限且无 RMT 外设的情况下的绝佳选择。在 DWM3001CDK 上实现这一功能,不仅达到了 FastBond 项目的要求,也为后续开发更复杂的 UWB 定位可视化界面打下了基础。

Footnotes

  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/


在以下平台分享此文章: