跳至内容
返回

CH592F利用SPI+DMA驱动WS2812灯珠

发布于:  at  07:20 下午

前言

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

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

原理分析

WS2812的通讯协议大家都烂熟于心了,核心就是通过高低电平的占空比来区分0码和1码.

如果我们将SPI的时钟频率设定为WS2812频率的4倍(约3.2MHz),那么发送一个字节(8位)的SPI数据所占用的时间,刚好对应2个WS2812的位周期(因为这里我们用4个SPI位来表示1个WS2812位).

这样,我们只需要在内存中开辟一块缓存,将RGB颜色数据“膨胀”转化为对应的SPI数据,然后通过DMA一键发送,即可彻底解放CPU.

核心代码实现

1. 初始化SPI

首先需要配置SPI0为主机模式.CH592F的系统主频通常为48MHz,为了凑出3.2MHz的SPI时钟,我们需要设置分频系数. 48MHz / 15 = 3.2MHz.

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位数据片段:

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

// 补充宏定义,用于生成波形
##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就可以去处理蓝牙连接或者睡觉了.

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:

##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 中调用:

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系列芯片.


在以下平台分享此文章: