STM32CUBEMX_SPI_驱动WS2811灯带
前言:
关于这种带芯片的之前我都是使用GPIO模拟时序,但是带来一个很大的弊端,那就是严重占用CPU资源,使得其他代码逻辑没办法正常执行了,想办法搞一个单片机的外设使用DMA功能,就可以解决占用资源的问题了,去网上了解了,还真有网友这么干的,参考完之后,自己来干一遍
WS2811芯片的一些重要参数:
讲解一下怎么使用SPI的发送数据来模拟这个时序:
1、首先说明一下,WS2812的时序中一个位大约需要1.25us的时间,这里面当高电平的时间占用时间位0.85us,低电平占用时间为0.4us时表示传输位为1,反过来则表示传输位为0。
2、我们使用SPI不是用他的SCLK引脚的信号,而是MOSI引脚的信号,因为SCK引脚的脉冲宽度时固定的,但是我们可以控制MOSI输出信号。例如我们输出 b1111 1000 时,(MOSI原本是低电平状态)产生了一个高电平脉冲,高电平的宽度与地电平的比值为 5:3,然后我们可以再输出 b1110 0000,这时候输出脉冲的高低电平的比值为3:5,好巧哦,我们可以控制输出信号的脉宽比了,如果觉得这个比值还是太粗糙可以使用多个字节进行脉冲高低电平时间的比例分配。
恰巧前面说过了,WS2812的位1是由0.85us的高电平加上0.4us的低电平组成的,高低电平的占例大约为2多一点,其实和上面说的输出 b1111 1000 数据时生成的脉冲高低电平比例差不多,反过来WS2812的位0也和 b1110 0000 数据的输出效果差不多。我们通过控制SPI的时钟频率可以实现SPI传输8个位的数据使用的时间大致等于1.25us,例如STM32的SPI1挂载的APB2总线频率为72Mhz,8分频之后得到的SPI1的SCK的频率为9Mhz,周期为0.11us,传输8个位的时间为0.89微妙,没法正好等于1.25us,所以取一个大致相近的值即可。还有要注意设置SPI的工作模式为SCK在第一个上升沿采样数据,SCK的极性无所谓。
3、基于上面的论证,假设我们需要控制的WS2812灯一共有60颗,我们可以建立一个 60 * 24 个字节的数组A,这个数组存放的都是 b1111 1000 或者 b1110 0000,也就是每个字节存放的其实是一个WS2812的单个位的数据,我们将这个数组都写成 b1111 1000 的时候,灯光效果就都是白色,数组全部写 b1110 0000 的时候,灯光效果就都是黑色的。但是好像控制灯光的时候有点不方便啊!我们可以再建立一个数组B,用于存放每个灯(像素)的颜色信息,这个数组是32位类型的,存放RGB888的数据,我们修改灯光效果的时候可以先修改数组B的内容,然后通过运算将数组B中的数据映射到数组A中去,然后在将数组A通过SPI发送出去。
到这里基本上就可以实现SPI控制WS2812灯了,但是每次调用SPI发送数据是有间隔时间的,并且会被中断打断,这时候DMA的作用就体现出来了。将DMA的内存地址设置为上面的数组A,长度设置为数组A的大小,开启DMA发送,你只需要喝杯咖啡,等待DMA发送完成标志位置位即可。
4、还有一个需要注意的点是WS2812需要有复位操作,也就是发送50us以上的低电平进行复位,每次传输数据的时候都需要先复位,要实现复位功能,我们可以发送 b0000 0000 即可,由于一个位的时间为0.89us,要实现50us以上,需要至少发送57次b0000 0000 ,我们可以创建一个数组C,C的内容全是 b0000 0000,C的长度设置为100保证有效复位,然后开启DMA,传输数组C的内容即可完成复位。
Surprise,没想到SPI+DMA可以这样用,其实同样的技巧可以用在很多其他的应用场景中去,例如控制步进电机需要特定个数的脉冲,我们也可以使用SPI的MOSI来实现(SCK可能频率太高,步进电机控制器无法接受),例如发送 b0011 1100 这个数据就可以在MOSI上产生一个高脉冲,通过DMA可以实现输出特定个数脉冲的功能。
实践我们的做法:
1、SPI模式使用
void MX_SPI1_Init(void)
{
/* USER CODE BEGIN SPI1_Init 0 */
/* USER CODE END SPI1_Init 0 */
/* USER CODE BEGIN SPI1_Init 1 */
/* USER CODE END SPI1_Init 1 */
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN SPI1_Init 2 */
/* USER CODE END SPI1_Init 2 */
}
下面是驱动源码:
ws281x.c
#include "ws281x.h"
#include "spi.h"
#include "log.h"
#include "string.h"
const ws2812xTypeDef_t RED = {255,0,0}; //全为最大亮度
const ws2812xTypeDef_t GREEN = {0,255,0};
const ws2812xTypeDef_t BLUE = {0,0,255};
const ws2812xTypeDef_t YELLOW = {255,255,0};
const ws2812xTypeDef_t MAGENTA = {255,0,255};
const ws2812xTypeDef_t BLACK = {0,0,0};
const ws2812xTypeDef_t WHITE = {255,255,255};
unsigned char ws2812x_array[WS281X_ARRAY_NUM] = {0};
void ws2812xInit(void)
{
ws2812xRgbBlack(WS281X_NUM);
HAL_Delay(2);
}
void ws2812xRgbRed(unsigned short num)
{
ws2812xSetColor(num,RED);
ws2812xUpdateDisplay();
}
void ws2812xRgbGreen(unsigned short num)
{
ws2812xSetColor(num,GREEN);
ws2812xUpdateDisplay();
}
void ws2812xRgbBlue(unsigned short num)
{
ws2812xSetColor(num,BLUE);
ws2812xUpdateDisplay();
}
void ws2812xRgbYellow(unsigned short num)
{
ws2812xSetColor(num,YELLOW);
ws2812xUpdateDisplay();
}
void ws2812xRgbMagenta(unsigned short num)
{
ws2812xSetColor(num,MAGENTA);
ws2812xUpdateDisplay();
}
void ws2812xRgbBlack(unsigned short num)
{
ws2812xSetColor(num,BLACK);
ws2812xUpdateDisplay();
}
void ws2812xRgbWhite(unsigned short num)
{
ws2812xSetColor(num,WHITE);
ws2812xUpdateDisplay();
}
void ws2812xGreenFlow(unsigned short num,unsigned short time)
{
for(int i = 0;i < num; i++)
{
ws2812xRgbRed(i+1);
Debug_debug("ws2812xRgbGreen:%d\r\n",i);
HAL_Delay(time);
}
}
void ws2812xSetColor(unsigned short numId, ws2812xTypeDef_t Color)
{
if(numId > WS281X_NUM) return;
int array_count = 0;
memset(ws2812x_array,CODE0,WS281X_ARRAY_NUM);
for(int i = 0; i < numId; i++)
{
for(int j = 7; j >= 0; j--)
{
if((Color.R & (1<<j)) == 0){
ws2812x_array[array_count] = CODE0;
}
else{
ws2812x_array[array_count] = CODE1;
}
array_count++;
}
for(int j = 7; j >= 0; j--)
{
if((Color.G & (1<<j)) == 0){
ws2812x_array[array_count] = CODE0;
}
else{
ws2812x_array[array_count] = CODE1;
}
array_count++;
}
for(int j = 7; j >= 0; j--)
{
if((Color.B & (1<<j)) == 0){
ws2812x_array[array_count] = CODE0;
}
else{
ws2812x_array[array_count] = CODE1;
}
array_count++;
}
}
}
void ws2812xUpdateDisplay(void)
{
// HAL_SPI_Transmit(&hspi1, ws2812x_array, WS281X_ARRAY_NUM, 10);
HAL_SPI_Transmit_DMA(&hspi1, ws2812x_array, WS281X_ARRAY_NUM);
}
ws281x.h
#ifndef __WS2812X_H
#define __WS2812X_H
typedef struct//颜色结构体
{
unsigned char R;
unsigned char G;
unsigned char B;
}ws2812xTypeDef_t;
#define WS281X_NUM 42 //灯RGB数量
#define WS281X_ARRAY_NUM WS281X_NUM*3*8 //14*3*8
//用SPI字节 来模拟bit(CODEx)码
#define CODE0 0xE0
#define CODE1 0xF8
void ws2812xRgbRed(unsigned short num); //红
void ws2812xRgbGreen(unsigned short num); //绿
void ws2812xRgbBlue(unsigned short num); //蓝
void ws2812xRgbYellow(unsigned short num);//黄
void ws2812xRgbMagenta(unsigned short num);//紫
void ws2812xRgbBlack(unsigned short num); //黑
void ws2812xRgbWhite(unsigned short num); //白
void ws2812xInit(void);
void ws2812xUpdateDisplay(void);
void ws2812xSetColor(unsigned short numId, ws2812xTypeDef_t Color);
void ws2812xGreenFlow(unsigned short num,unsigned short time);//绿色流动一周
#endif
补充问题:
问题1:出现第一颗灯珠点不亮,其他灯珠均可以正常点亮?
原因:时序有问题,停止信号不对,因为芯片是级联传输信号的,最后的这个停止信号就决定的信号传递的位置,检查SPI的模式是不是 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
问题2:出现灯珠乱闪,不能正常显示设置的颜色?
原因:时序错误,使用SPI模拟时序的时候被中断打断了,导致时序错乱,使用DMA解决问题