扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

STM32应用开发——使用SPI+DMA驱动WS2812

01/15 14:26
3250
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

串行灯带的应用十分广泛,其中以WS2812最为经典,这种灯带一般都是通过单总线的方式来驱动,也就是由一根数据线按照特定的时序输出,继而驱动灯带。这种方式在硬件和软件上都非常简单,但是如果软件用GPIO模拟时序的话比较占用主线程的资源,因此,如果能用硬件外设(比如PWMSPI、串口)来模拟出这个时序,就能节省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 时序测试

使用逻辑分析仪抓取信号,得到的结果如下:

  1. 8个LED连续写入RGB值: 在这里插入图片描述
  2. 编码1电平: 在这里插入图片描述
  3. 编码0电平: 在这里插入图片描述

结论:实际输出的波形和理论基本一致,周期和脉宽稍微有点出入属于正常误差范围。

结束语

关于stm32如何使用SPI+DMA驱动WS2812的讲解就到这里,如果还有什么问题,欢迎在评论区留言。

相关推荐