CH592F利用SPI+DMA驱动WS2812灯珠
前言
在上一篇《从零开始:用CH592F制作CS2生命值胸章》的文章中,我展示了如何利用CH592F这颗蓝牙芯片制作一个和游戏联动的生命值指示器.
而本文将介绍生命值计数器的一个技术细节:如何使用CH592F驱动WS2812.
虽然WS2812的时序要求比较严格,通常可以使用GPIO翻转配合精准延时来实现,但那样会占用大量的CPU资源,导致蓝牙协议栈或其他中断任务受阻.
为了实现“零”CPU占用的炫酷灯效,我决定利用CH592F的SPI外设配合DMA来模拟WS2812的时序.
原理分析
WS2812的通讯协议大家都烂熟于心了,核心就是通过高低电平的占空比来区分0码和1码.
- 0码:高电平时间短(~0.4us),低电平时间长.
- 1码:高电平时间长(~0.8us),低电平时间短.
- 整个周期大约在1.25us左右,即频率约为800kHz.
如果我们将SPI的时钟频率设定为WS2812频率的4倍(约3.2MHz),那么发送一个字节(8位)的SPI数据所占用的时间,刚好对应2个WS2812的位周期(因为这里我们用4个SPI位来表示1个WS2812位).
- 模拟0码:发送二进制
1000(0x8),即1个高电平+3个低电平. - 模拟1码:发送二进制
1110(0xE),即3个高电平+1个低电平.
这样,我们只需要在内存中开辟一块缓存,将RGB颜色数据“膨胀”转化为对应的SPI数据,然后通过DMA一键发送,即可彻底解放CPU.
核心代码实现
1. 初始化SPI
首先需要配置SPI0为主机模式.CH592F的系统主频通常为48MHz,为了凑出3.2MHz的SPI时钟,我们需要设置分频系数.48MHz / 15 = 3.2MHz.
1 | void NeoPixelController::begin() { |
2. 数据转换 (GRB -> SPI)
这是最关键的一步.我们需要将内存中紧凑的RGB(实际是GRB顺序)数据,展开为SPI总线需要的波形数据.
这里定义了两个宏来代表SPI发送的4位数据片段:
GRB_CODE_0:0x8(对应二进制 1000)GRB_CODE_1:0xE(对应二进制 1110)
为了节省空间,我们一个字节的SPI buffer存储两个WS2812位.
1 | // 补充宏定义,用于生成波形 |
3. DMA 发送
数据转换完成后,发送过程就非常简单了.直接调用CH592F的DMA传输函数,CPU就可以去处理蓝牙连接或者睡觉了.
1 | void NeoPixelController::show() { |
封装与调用
为了方便使用,我将其封装成了一个NeoPixelController类,模仿了Arduino Adafruit_NeoPixel的接口风格.
头文件 NeoPixel.h: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#
#
#
class NeoPixelController {
public:
// 构造函数,需要指定LED数量和SPI buffer大小
// 注意:_spiBuffer 最好在外部申请或者在类中动态申请
NeoPixelController (uint16_t numLeds, uint8_t spiInstance = 0);
void begin();
void show();
void setPixelColor(uint16_t index, uint32_t color);
void setPixelHSV(uint16_t index, uint8_t hue, uint8_t sat, uint8_t val);
void clear();
void setBrightness(uint8_t brightness);
// 简单的颜色工具
static uint32_t Color(uint8_t r, uint8_t g, uint8_t b);
private:
uint16_t _numLeds;
uint8_t _spiInstance;
uint8_t _brightness;
// 这里为了演示方便,假设最大支持一定数量,实际应动态分配
uint8_t _grbBuffer[100 * 3];
uint8_t _spiBuffer[100 * 3 * 4];
void convertGRBtoSPI(const uint8_t *grb, uint8_t *spi, uint16_t len);
uint32_t colorHSV(uint8_t hue, uint8_t sat, uint8_t val);
};
#
在主程序 Main.c 中调用:
1 | NeoPixelController strip(32); // 控制32颗灯珠 |
总结
通过SPI+DMA的方式驱动WS2812,最大的优势在于时序极其稳定,且不消耗CPU算力.这对于CH592F这种单核蓝牙SoC来说非常重要,避免了因为关闭中断写时序而导致蓝牙连接不稳定的问题.
唯一的代价就是内存占用稍微大了一些(每个LED需要12字节的SPI buffer),但对于几十颗灯珠的装饰应用来说,CH592F的RAM绰绰有余.
补充说明
代码不仅仅可以运行在CH592系列芯片, 还可以运行在CH582系列芯片.
CH592F利用SPI+DMA驱动WS2812灯珠
