串行灯带的应用十分广泛,其中以WS2812最为经典,这种灯带一般都是通过单总线的方式来驱动,也就是由一根数据线按照特定的时序输出,继而驱动灯带。这种方式在硬件和软件上都非常简单,但是如果软件用GPIO模拟时序的话比较占用主线程的资源,因此,如果能用硬件外设(比如PWM、SPI、串口)来模拟出这个时序,就能节省MCU的资源。 本文以SPI+DMA为例介绍如何驱动WS2812。
1 硬件介绍
1.1 WS2812介绍
1.1.1 芯片简介
WS2812是一款智能控制LED光源,其外观采用最新的MOLDING封装技术、控制电路和RGB芯片集成在2020组件的封装中。其内部包括智能数字端口数据锁存和信号整形放大驱动电路。还包括精密内部振荡器和电压可编程恒流控制部分,有效保证像素点光源的颜色。
1.1.2 引脚描述
| 引脚 | 名称 | 描述 |
|---|---|---|
| DO | 数据输出 | 控制数据输出到下一个芯片 |
| GND | 地 | 电源负极 |
| DI | 数据输入 | 控制数据输入 |
| VDD | 电源 | 电源正极 |
1.1.3 工作原理
通过级联法把每个灯的DI和DO引脚首尾相连,数据可以从第一个IC开始,不断的传输到后面每一个IC,从而实现整条灯带的控制。
1.1.4 时序
WS2812通过不同的时序来表示0码、1码和复位码,如下图所示:
其中各信号的电平如下图所示:
注:不同型号的芯片在时序上会有点差异,具体以芯片数据手册为准。
1.1.5 传输协议
传输过程如下图所示:
每一个灯珠的RGB数据排列如下:
1.2 电路设计
WS2812的控制方法很简单,每个灯珠首尾相接进行级联即可,如下图所示: 其中,第一个灯珠的DI引脚接入到MCU的一个GPIO上面。
我这里使用STM32F103来作为主控MCU,引脚接线如下:
| MCU引脚 | 灯带引脚 | 描述 |
|---|---|---|
| PB15 | DI | 由MCU发送控制信号输入到灯带 |
| PB14 | 无 | MCU输出的SPI CLK,即使是用SPI的MOSI模拟时序,对于MCU来说依然是SPI接口,因此CLK还是会输出时钟信号,但是该信号对于LED灯来说是无用的,因此LED端不需要接 |
注:使用SPI模拟LED时序时,SPI的CLK引脚不能作为普通IO使用,因为SPI和CLK和MOSI是硬件一起输出的,无法单独只输出MOSI,因此即使实际上只有MOSI一个引脚用于驱动LED灯,CLK引脚也不能作为其他功能引脚使用。软件设置SPI为单线SPI输出时,MISO可以单独作为普通GPIO使用。
2 软件编程
2.1 软件原理
通过DMA可以控制SPI连续输出自定义数据,然后通过调节SPI速率以及数据组合凑出0码、1码和复位码等波形,从而实现灯珠的驱动。
举个例子:按照上面的手册的时序要求,每一个逻辑电平周期在1.25us左右,那么SPI输出的频率就可以设置为2.25M(72M/32)。然后用SPI数据的3个bit表示灯的1个bit,就可以区分编码“0”和编码“1”,因为编码“0”和编码“1”的高低电平脉宽比例约为1:2和2:1,而SPI的3个bit可以设置成100和110,对应LED灯的编码。
编码“0”的高电平脉宽和低电平脉宽分别为0.4us和0.85us,那么对应到SPI的1个bit脉宽是0.44us(1/2.25M),2个bit脉宽是0.88,3个bit为一个周期1.33us,在LED灯的容错范围内,因此可以通过DMA+SPI的方式模拟时序连续传输RGB数据就可以实现灯带的颜色和亮度控制。
时序示意图:
测试电平时序如下:
| 逻辑电平 | 脉宽 | SPI数据 |
|---|---|---|
| 逻辑0高电平 | 0.40±0.15us | 0.44us(1bit) |
| 逻辑0低电平 | 0.85±0.15us | 0.88us(2bit) |
| 逻辑1高电平 | 0.85±0.15us | 0.88us(2bit) |
| 逻辑1低电平 | 0.40±0.15us | 0.44us(1bit) |
| 复位低电平 | 1.25±0.60us | 1.33us(3bit) |
2.2 测试代码
根据上述原理,编写测试代码。
2.2.1 底层驱动
ws2812_driver.h :
#ifndef __WS2812_DRIVER_H#define __WS2812_DRIVER_H#include "stm32f10x.h"#include "stm32f10x_conf.h"#define SPI2_BOUNDARY_ADDR 0x40003800 // spi2 base address#define SPI2_DR_ADDR SPI2_BOUNDARY_ADDR + 0x0C // spi2 data address offset 0x0C#define LED_NUM 8#define RGB_BIT 24void led_display(uint8_t (*led_buf)[3], uint8_t led_num);void ws2812_init(void);#endif
ws2812_driver.c :
#include "ws2812_driver.h"#include "string.h"uint8_t spi_buf[LED_NUM][9];uint8_t dma_txbuf[LED_NUM*9];void spi_init(void){ GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );//PORTB时钟使能 RCC_APB1PeriphClockCmd( RCC_APB1Periph_SPI2, ENABLE );//SPI2时钟使能 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; //设置SPI单向或者双向的数据模式:SPI设置为单向发送 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI工作模式:设置为主SPI SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置SPI的数据大小:SPI发送接收8位帧结构 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //串行同步时钟的空闲状态为低电平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步时钟的第一个跳变沿(上升或下降)数据被采样 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; //定义波特率预分频的值:波特率预分频值为32,速率:2.25MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式 SPI_Init(SPI2, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器 SPI_Cmd(SPI2, ENABLE); //使能SPI外设 }void spi_dma_init(void){ DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)SPI2_DR_ADDR; //设置发送外设(0x4000380C) 地址(源地址) DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)dma_txbuf; //设置 SRAM 存储地址(源地址) DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //传输方向 内存-外设 DMA_InitStructure.DMA_BufferSize = sizeof(dma_txbuf); //设置 SPI2 接收长度 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址增量(不变) DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址增量(变化) DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设传输宽度(字节) DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //内存传输宽度(字节) DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输方式,一次传输完停止,不重新加载 DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //中断方式-高(三级) DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //内存到内存方式禁止 DMA_Init(DMA1_Channel5, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE); //开启 DMA1_Channel5 传输完成中断 // DMA_ITConfig(DMA1_Channel5, DMA_IT_TE, ENABLE); //开启 DMA1_Channel5 传输错误中断 /* Enable DMA TX request */ SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE); //发送缓冲区DMA使能 DMA_Cmd(DMA1_Channel5, DISABLE); //暂时不开启DMA 通道 DMA1_Channel5}void dma_nvic_init(void){ NVIC_InitTypeDef NVIC_InitStructure; /* Enable DMA1 channel5 IRQ Channel */ NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);}void DMA1_Channel5_IRQHandler(void){ /* Test on DMA1 Channel5 Transfer Complete interrupt */ if(DMA_GetITStatus(DMA1_IT_TC5)) { DMA_Cmd(DMA1_Channel5, DISABLE); /* Clear DMA1 Channel5 Half Transfer, Transfer Complete and Global interrupt pending bits */ DMA_ClearFlag(DMA1_FLAG_TC5); DMA_ClearITPendingBit(DMA1_IT_GL5); }}uint32_t ws2812_rgb_encoding(uint32_t *value){ // 把GRB的每一个bit扩展为3bits,0b110代表WS2812B的逻辑'1',0b100代表WS2812B的逻辑'0' uint32_t encoding=0; int index = 0; while (index < 8) { encoding = encoding << 3; if (*value & (1 << 23)) { encoding |= 6; // 0b110 } else { encoding |= 4; // 0b100 } *value <<= 1; index++; } return encoding;}void ws2812_data_pack(uint8_t led_idx, uint32_t value){ uint32_t encoding; // Process the GREEN byte encoding = ws2812_rgb_encoding(&value); spi_buf[led_idx][0] = ((encoding >> 16) & 0xFF); spi_buf[led_idx][1] = ((encoding >> 8) & 0xFF); spi_buf[led_idx][2] = (encoding & 0xFF); // Process the RED byte encoding = ws2812_rgb_encoding(&value); spi_buf[led_idx][3] = ((encoding >> 16) & 0xFF); spi_buf[led_idx][4] = ((encoding >> 8) & 0xFF); spi_buf[led_idx][5] = (encoding & 0xFF); // Process the BLUE byte encoding = ws2812_rgb_encoding(&value); spi_buf[led_idx][6] = ((encoding >> 16) & 0xFF); spi_buf[led_idx][7] = ((encoding >> 8) & 0xFF); spi_buf[led_idx][8] = (encoding & 0xFF);}void spi_dma_send(void){ memcpy(dma_txbuf, spi_buf, sizeof(spi_buf)); DMA_SetCurrDataCounter(DMA1_Channel5, sizeof(dma_txbuf)); DMA_Cmd(DMA1_Channel5, ENABLE); }void led_display(uint8_t (*led_buf)[3], uint8_t led_num){ uint8_t i; uint32_t color_value; // led_buf -> spi_buf for(i = 0; i < led_num; i++) {// N led color_value = (led_buf[i][1] << 16) | (led_buf[i][0] << 8) | led_buf[i][2]; ws2812_data_pack(i, color_value); } // spi start spi_dma_send();}void ws2812_init(void){ spi_init(); dma_nvic_init(); spi_dma_init();}
2.2.2 灯效应用
ws2812_app.h :
#ifndef __WS2812_APP_H#define __WS2812_APP_H#include "stm32f10x.h"#include "stm32f10x_conf.h"#include "ws2812_driver.h"typedef enum { LED_MODE_OFF, LED_MODE_ALL_ON, LED_MODE_BREATHE, LED_MODE_GRADIENT, LED_MODE_FLOW, }led_mode_t;typedef struct _led_config_t{ led_mode_t mode; uint8_t g; uint8_t r; uint8_t b; uint8_t brightness; }led_config_t;void led_init(void);void led_handle(void);#endif
ws2812_app.c :
#include "ws2812_driver.h"#include "string.h"uint8_t spi_buf[LED_NUM][9];uint8_t dma_txbuf[LED_NUM*9];void spi_init(void){ GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );//PORTB时钟使能 RCC_APB1PeriphClockCmd( RCC_APB1Periph_SPI2, ENABLE );//SPI2时钟使能 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; //设置SPI单向或者双向的数据模式:SPI设置为单向发送 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI工作模式:设置为主SPI SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置SPI的数据大小:SPI发送接收8位帧结构 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //串行同步时钟的空闲状态为低电平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步时钟的第一个跳变沿(上升或下降)数据被采样 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; //定义波特率预分频的值:波特率预分频值为32,速率:2.25MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式 SPI_Init(SPI2, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器 SPI_Cmd(SPI2, ENABLE); //使能SPI外设 }void spi_dma_init(void){ DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)SPI2_DR_ADDR; //设置发送外设(0x4000380C) 地址(源地址) DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)dma_txbuf; //设置 SRAM 存储地址(源地址) DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //传输方向 内存-外设 DMA_InitStructure.DMA_BufferSize = sizeof(dma_txbuf); //设置 SPI2 接收长度 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址增量(不变) DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址增量(变化) DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设传输宽度(字节) DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //内存传输宽度(字节) DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输方式,一次传输完停止,不重新加载 DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //中断方式-高(三级) DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //内存到内存方式禁止 DMA_Init(DMA1_Channel5, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE); //开启 DMA1_Channel5 传输完成中断 // DMA_ITConfig(DMA1_Channel5, DMA_IT_TE, ENABLE); //开启 DMA1_Channel5 传输错误中断 /* Enable DMA TX request */ SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE); //发送缓冲区DMA使能 DMA_Cmd(DMA1_Channel5, DISABLE); //暂时不开启DMA 通道 DMA1_Channel5}void dma_nvic_init(void){ NVIC_InitTypeDef NVIC_InitStructure; /* Enable DMA1 channel5 IRQ Channel */ NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);}void DMA1_Channel5_IRQHandler(void){ /* Test on DMA1 Channel5 Transfer Complete interrupt */ if(DMA_GetITStatus(DMA1_IT_TC5)) { DMA_Cmd(DMA1_Channel5, DISABLE); /* Clear DMA1 Channel5 Half Transfer, Transfer Complete and Global interrupt pending bits */ DMA_ClearFlag(DMA1_FLAG_TC5); DMA_ClearITPendingBit(DMA1_IT_GL5); }}uint32_t ws2812_rgb_encoding(uint32_t *value){ // 把GRB的每一个bit扩展为3bits,0b110代表WS2812B的逻辑'1',0b100代表WS2812B的逻辑'0' uint32_t encoding=0; int index = 0; while (index < 8) { encoding = encoding << 3; if (*value & (1 << 23)) { encoding |= 6; // 0b110 } else { encoding |= 4; // 0b100 } *value <<= 1; index++; } return encoding;}void ws2812_data_pack(uint8_t led_idx, uint32_t value){ uint32_t encoding; // Process the GREEN byte encoding = ws2812_rgb_encoding(&value); spi_buf[led_idx][0] = ((encoding >> 16) & 0xFF); spi_buf[led_idx][1] = ((encoding >> 8) & 0xFF); spi_buf[led_idx][2] = (encoding & 0xFF); // Process the RED byte encoding = ws2812_rgb_encoding(&value); spi_buf[led_idx][3] = ((encoding >> 16) & 0xFF); spi_buf[led_idx][4] = ((encoding >> 8) & 0xFF); spi_buf[led_idx][5] = (encoding & 0xFF); // Process the BLUE byte encoding = ws2812_rgb_encoding(&value); spi_buf[led_idx][6] = ((encoding >> 16) & 0xFF); spi_buf[led_idx][7] = ((encoding >> 8) & 0xFF); spi_buf[led_idx][8] = (encoding & 0xFF);}void spi_dma_send(void){ memcpy(dma_txbuf, spi_buf, sizeof(spi_buf)); DMA_SetCurrDataCounter(DMA1_Channel5, sizeof(dma_txbuf)); DMA_Cmd(DMA1_Channel5, ENABLE); }void led_display(uint8_t (*led_buf)[3], uint8_t led_num){ uint8_t i; uint32_t color_value; // led_buf -> spi_buf for(i = 0; i < led_num; i++) {// N led color_value = (led_buf[i][1] << 16) | (led_buf[i][0] << 8) | led_buf[i][2]; ws2812_data_pack(i, color_value); } // spi start spi_dma_send();}void ws2812_init(void){ spi_init(); dma_nvic_init(); spi_dma_init();}
main.c :
#include "stm32f10x.h"#include "stm32f10x_conf.h"#include "ws2812_app.h"uint32_t fac_us, fac_ms;void delay_init(void){ /* 配置时钟源 --> 72MHz / 8 = 9MHz, * 滴答定时器每计数一次所需时间为 T = 1/(9MHz) s,即 1us = 10^(-6)s , * T = (1/9) * 10^(-6) s = 1/9 us , 即每计数一次的时间为1/9微秒 * 换言之,系统时钟频率为72MHz时,1us需要计数9次 */ SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); /* fac_us 和 fac_ms 是定义的全局变量,是倍频因子*/ fac_us = SystemCoreClock / 8000000; fac_ms = 1000 * fac_us;}void delay_us(unsigned int us){ unsigned int temp = 0; /*1us需要计数9次,计数初值为9*/ /*设置重装载值*/ SysTick->LOAD = fac_us * us; /*当前值寄存器清0,即清空计数器*/ SysTick->VAL = 0x00; /*滴答定时器控制寄存器,使能滴答定时器*/ SysTick->CTRL |= 0x01; do { /*获取控制寄存器的当前状态*/ temp = SysTick->CTRL; }while((temp & 0x01) && !(temp & (0x01 << 16))); SysTick->CTRL &= 0x00; SysTick->VAL = 0x00;}void delay_ms(unsigned int ms){ unsigned int temp = 0; /*设置重装载值*/ if(fac_ms * ms <= ((0x01 << 24) - 1)) { SysTick->LOAD = fac_ms * ms; } else { return ; } /*设置重装载值*/ SysTick->VAL = 0x00; /*滴答定时器控制寄存器,使能滴答定时器*/ SysTick->CTRL |= 0x01; do { /*获取控制寄存器的当前状态*/ temp = SysTick->CTRL; }while((temp & 0x01) && !(temp & (0x01 << 16))); SysTick->CTRL &= 0x00; SysTick->VAL = 0x00;}int main(void){ SystemInit(); delay_init(); led_init(); while(1) { led_handle(); delay_ms(10); }}
2.3 运行测试
2.3.1 时序测试
使用逻辑分析仪抓取信号,得到的结果如下:
- 8个LED连续写入RGB值:
- 编码1电平:
- 编码0电平:
结论:实际输出的波形和理论基本一致,周期和脉宽稍微有点出入属于正常误差范围。
结束语
关于stm32如何使用SPI+DMA驱动WS2812的讲解就到这里,如果还有什么问题,欢迎在评论区留言。
3250