CH592F利用SPI+DMA驱动WS2812灯珠

前言

在上一篇《从零开始:用CH592F制作CS2生命值胸章》的文章中,我展示了如何利用CH592F这颗蓝牙芯片制作一个和游戏联动的生命值指示器.

而本文将介绍生命值计数器的一个技术细节:如何使用CH592F驱动WS2812.
虽然WS2812的时序要求比较严格,通常可以使用GPIO翻转配合精准延时来实现,但那样会占用大量的CPU资源,导致蓝牙协议栈或其他中断任务受阻.
为了实现“零”CPU占用的炫酷灯效,我决定利用CH592F的SPI外设配合DMA来模拟WS2812的时序.
WS2812Timing

原理分析

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.

NeoPixel.cpp - init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void NeoPixelController::begin() {
// 配置GPIO,PA12/13/14通常对应SPI0
GPIOA_ResetBits (GPIO_Pin_12);
GPIOA_ModeCfg (GPIO_Pin_12 |GPIO_Pin_13 | GPIO_Pin_14, GPIO_ModeOut_PP_5mA);

// 初始化SPI0
SPI0_MasterDefInit();
// 设置分频,15分频得到3.2MHz
// 注意:实际调试中可能需要根据示波器微调
SPI0_CLKCfg (15);

// 发送复位信号(WS2812需要 >50us 的低电平复位)
// 这里发送一段全0数据即可
memset (_spiBuffer, 0, 24);
SPI0_MasterDMATrans (_spiBuffer, 24);
}

2. 数据转换 (GRB -> SPI)

这是最关键的一步.我们需要将内存中紧凑的RGB(实际是GRB顺序)数据,展开为SPI总线需要的波形数据.
这里定义了两个宏来代表SPI发送的4位数据片段:

  • GRB_CODE_0: 0x8 (对应二进制 1000)
  • GRB_CODE_1: 0xE (对应二进制 1110)

为了节省空间,我们一个字节的SPI buffer存储两个WS2812位.

NeoPixel.cpp - convert
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
// 补充宏定义,用于生成波形
#define GRB_CODE_0 0x8
#define GRB_CODE_1 0xE

void NeoPixelController::convertGRBtoSPI (const uint8_t *grb, uint8_t *spi, uint16_t len) {
// len 是LED的数量
// 每个LED 3个字节颜色,每个颜色位需要4位SPI数据
// 所以SPI buffer长度 = len * 3 * 4 (bytes)
memset (spi, 0, len * 3 * 4);

for (uint16_t i = 0; i < len; i++) {
for (uint8_t j = 0; j < 3; j++) { // R, G, B 三个通道
for (uint8_t k = 0; k < 4; k++) { // 每个字节8位,分为4组,每组2位
for (uint8_t m = 0; m < 2; m++) { // 处理每组中的2位
// 检查GRB颜色数据的特定位是否为1
// 逻辑比较绕,本质就是从高位到低位通过掩码取值
if (grb[3 * i + j] & (0x80 >> (2 * k + m))) {
// 如果是1,SPI buffer填入 1110 (高位) 或 1110 (低位)
spi[3 * 4 * i + 4 * j + k] |= (GRB_CODE_1 >> (m * 4));
} else {
// 如果是0,SPI buffer填入 1000 (高位) 或 1000 (低位)
spi[3 * 4 * i + 4 * j + k] |= (GRB_CODE_0 >> (m * 4));
}
}
}
}
}
}

3. DMA 发送

数据转换完成后,发送过程就非常简单了.直接调用CH592F的DMA传输函数,CPU就可以去处理蓝牙连接或者睡觉了.

NeoPixel.cpp - show
1
2
3
4
5
6
7
void NeoPixelController::show() {
// 1. 将颜色数据转换为SPI波形数据
convertGRBtoSPI (_grbBuffer, _spiBuffer, _numLeds);
// 2. 启动DMA传输
// 长度计算:LED数量 * 3(RGB) * 4(膨胀系数)
SPI0_MasterDMATrans (_spiBuffer, _numLeds * 3 * 4);
}

封装与调用

为了方便使用,我将其封装成了一个NeoPixelController类,模仿了Arduino Adafruit_NeoPixel的接口风格.

头文件 NeoPixel.h:

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
#ifndef NEOPIXEL_H
#define NEOPIXEL_H

#include "CH59x_common.h"

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

#endif

在主程序 Main.c 中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NeoPixelController strip(32); // 控制32颗灯珠

int main() {
SetSysClock(CLK_SOURCE_PLL_48MHz);
strip.begin();
strip.setBrightness(50);

while(1) {
// 跑个彩虹特效
static uint8_t hue = 0;
strip.rainbow(hue++, 255, 255);
strip.show();
DelayMs(10);
}
}

总结

通过SPI+DMA的方式驱动WS2812,最大的优势在于时序极其稳定,且不消耗CPU算力.这对于CH592F这种单核蓝牙SoC来说非常重要,避免了因为关闭中断写时序而导致蓝牙连接不稳定的问题.

唯一的代价就是内存占用稍微大了一些(每个LED需要12字节的SPI buffer),但对于几十颗灯珠的装饰应用来说,CH592F的RAM绰绰有余.

补充说明

代码不仅仅可以运行在CH592系列芯片, 还可以运行在CH582系列芯片.

作者

Chaos Goo

发布于

2025-12-02

更新于

2025-12-01

许可协议