该程序是纯手敲,非Cube生成!所有代码均注释。
源码在文章后面获取
WS2818 简介

Keyword: 单线通讯、归零码、Reset、RGB顺序

RGB一共有24bit位 ->相当于驱动一个灯要24bit位 ->驱动若干个灯要24* n bit位,通过Reset码决定数据终止(保持)
24bit位应该如何发送?


可见:
表示低电平需要 T0H和T0L的配合,其关键在于高电平的时间,图中所示T0H时间为 0.85us ± 150ns
Q:怎么控制高低电平的时间
数据发送速度可达 800Kbps,就是 1.25us 发送一位数据,因为协议有一定的兼容性,所以实际上一个位的周期在1.25us±300ns之间都能识别到,因为是 us 级延时,所以时间要控制精准很难,因此我们借助 SPI 来控制 WS2812,我们用 SPI 的 MOSI 接口的一个 Byte(8位)模拟 WS2812 的一个位,比如下面的 SPI 我 设置的 4.5Mhz 速率,一个字节约为 1.76us,所以可以通过发送了一个字节的数据控制电平时间,然后模拟 0 和 1 到 WS2812,比如下面发送的0xF8


编程思路
① 若选择STM32F103C8T6最小系统的话,SPI1的时钟为72Mhz -> 需要16分频 -> 4.5Mhz -> 一个指令周期0.22us -> 0.22us * 8 = 1.76us与一个数据传输周期差不多(在F1已是较为接近)
② SPI需要发送什么数据来表示低电平 -> 0码时,高电平时间为0.4us ->一个指令周期为0.22us,所以需要两次高电平就够时间 -> 可表示为1100 0000 -> 0xC0 ;
③ 表示高电平 ->0码时,高电平时间0.85us -> 一个指令周期为0.22us,所以需要四次高电平时间就够了 -> 可表示为 1111 0000 –> 0 x F0 ;
④ Reset码可通过延迟函数来实现
程序基于正点原子的模板
实现目标: 灯的单个点亮(或每个灯显示不同颜色)
文件目录

main函数
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "led.h"
#include "spi_dma.h"
#include "rgb.h"
extern DMA_HandleTypeDef hdma1;
extern SPI_HandleTypeDef hspi1;
uint8_t textbuf[3] = {0xF0,0xF0,0xC0};
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
led_init(); /* LED初始化 */
Spi_Dma_Init();
while(1)
{
RGB_RED(50); // 50个红色灯珠亮
//HAL_SPI_Transmit_DMA(&hspi1,textbuf,8);
//HAL_SPI_Transmit(&hspi1,textbuf,24,1000);
//delay_us(60);
//RGB_RST();
delay_us(100);
}
}
SPI和DMA初始化 - 头文件
#ifndef __SPI_DMA_H
#define __SPI_DMA_H
#include "stm32f1xx.h"
extern SPI_HandleTypeDef hspi1; // SPI 1
extern DMA_HandleTypeDef hdma1; // DMA 1
void SPI_DMA_Init(void); // SPI 和 DMA初始化
#endif
SPI和DMA初始化 - 源文件
#include "spi_dma.h"
SPI_HandleTypeDef hspi1; // SPI 1
DMA_HandleTypeDef hdma1;
// SPI 和 DMA初始化
void SPI_DMA_Init(void)
{
hspi1.Instance = SPI1 ; // SPI1 基地址
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16 ; //波特率设置
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; //时钟相位
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; //是时钟极性
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//硬件CEC校验
hspi1.Init.CRCPolynomial = 10; //校验多项式
hspi1.Init.DataSize = SPI_DATASIZE_8BIT; //帧格式(8bit)
hspi1.Init.Direction = SPI_DIRECTION_2LINES; //全双工
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; //数据传输顺序
hspi1.Init.Mode = SPI_MODE_MASTER ; //SPI模式
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制SS
hspi1.Init.TIMode = SPI_TIMODE_DISABLE; // 帧格式(Motorola)
HAL_SPI_Init(&hspi1); //SPI1 初始化
HAL_NVIC_SetPriority(DMA1_Channel3_IRQn,0,0); //设置中断分组
HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);// 使能中断
}
// SPI 初始化回调函数
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
GPIO_InitTypeDef GPIO_InitTypeSture ={0};
__HAL_RCC_SPI1_CLK_ENABLE(); // 使能SPI1时钟
__HAL_RCC_GPIOA_CLK_ENABLE();// 使能GPIOA时钟
__DMA1_CLK_ENABLE(); // 使能DMA1时钟
if(hspi ->Instance ==SPI1) //判断基地址是否为SPI1
{
GPIO_InitTypeSture.Mode = GPIO_MODE_AF_PP; //一定要复用推挽输出
GPIO_InitTypeSture.Pin = GPIO_PIN_5 | GPIO_PIN_7;
GPIO_InitTypeSture.Pull = GPIO_PULLUP;
GPIO_InitTypeSture.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&GPIO_InitTypeSture); //GPIO 初始化
hdma1.Instance = DMA1_Channel3; //基地址为DMA1通道3
hdma1.Init.Direction = DMA_MEMORY_TO_PERIPH; // 内存到外设
hdma1.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE ; //一个字
hdma1.Init.MemInc = DMA_MINC_ENABLE; //内存自增
hdma1.Init.Mode = DMA_NORMAL; //不循环
hdma1.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE ; //一个字
hdma1.Init.PeriphInc = DMA_PINC_DISABLE ; //外设不自增
hdma1.Init.Priority = DMA_PRIORITY_MEDIUM; // 优先级
HAL_DMA_Init(&hdma1); //DMA 初始化
__HAL_LINKDMA(hspi,hdmatx,hdma1); // SPI与DMA进行连接
}
}
// DMA1通道3中断服务函数
void DMA1_Channel3_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma1); //处理DMA中断
}
WS2812 - 头文件
#ifndef __RGB_H
#define __RGB_H
#include "stm32f1xx.h"
#include "spi_dma.h"
typedef struct
{
uint8_t R;
uint8_t G;
uint8_t B;
}RGB_InitStruce;
// RGB数据发送程序
void RGB_Send(uint8_t* data);
// RGB 保持
void RGB_Rst(void);
// 存入缓存数组
void RGB_Set_Buf(uint8_t num,RGB_InitStruce color);
// RGB发送处理函数
void RGB_Send_Handler(uint8_t num);
// RGB 红色测试函数
void RGB_Red(uint8_t num);
// RGB 蓝色测试函数
void RGB_Blue(uint8_t num);
#endif
WS2812 - 源文件
#include "rgb.h"
#define RGB_NUM 60 // 灯珠数量
#define RGB_LowLevel 0xC0 // 0码
#define RGB_HighLevel 0xF0 // 1码
extern SPI_HandleTypeDef hspi1; // SPI 1
extern DMA_HandleTypeDef hdma1; // DMA 1
// 颜色定义
RGB_InitStruce RED = {30,0,0}; // 红色
RGB_InitStruce GREEN = {0,30,0}; // 绿色
RGB_InitStruce BLUE = {0,0,30}; // 蓝色
RGB_InitStruce RGB_TempBuf[RGB_NUM]; // 颜色缓存数组
// RGB数据发送程序
void RGB_Send(uint8_t* data)
{
while(HAL_DMA_GetState(&hdma1) != HAL_DMA_STATE_READY); // 判断DMA是否空闲
HAL_SPI_Transmit_DMA(&hspi1,data,24); // DMA发送
}
// RGB 保持
void RGB_Rst(void)
{
uint8_t RGB_RstBuf[100] = {0};
while(HAL_DMA_GetState(&hdma1) != HAL_DMA_STATE_READY); // 判断DMA是否空闲
HAL_SPI_Transmit_DMA(&hspi1,RGB_RstBuf,100); // DMA发送
}
// 存入缓存数组
// 颜色设置函数,传入ID和颜色,进而设置缓存区
void RGB_Set_Buf(uint8_t num,RGB_InitStruce Color)
{
if( num <= RGB_NUM )
{
RGB_TempBuf[num].R = Color.R;
RGB_TempBuf[num].G = Color.G;
RGB_TempBuf[num].B = Color.B;
}
}
// RGB发送处理函数
// 刷新函数,将颜色缓存区刷新到WS2812,输入参数是指定的刷新长度
void RGB_Send_Handler(uint8_t num)
{
static uint8_t RGB_Buf[24] = {0};
uint8_t i,j = 0;
uint8_t Temp_R,Temp_G,Temp_B;
//将数组颜色转化为24个要发送的字节数据
if((num > 0 ) && ( num <= RGB_NUM ))
{
for (i = 0 ; i < num ;i++)
{
Temp_R = RGB_TempBuf[i].R;
Temp_G = RGB_TempBuf[i].G;
Temp_B = RGB_TempBuf[i].B;
for ( j = 0 ; j < 8 ; j++ )
{
RGB_Buf[7 - j] = (Temp_G& 0x01)? RGB_HighLevel : RGB_LowLevel ;
RGB_Buf[15 - j] = (Temp_R& 0x01)? RGB_HighLevel : RGB_LowLevel ;
RGB_Buf[23 - j] = (Temp_B& 0x01)? RGB_HighLevel : RGB_LowLevel ;
Temp_R >>= 1;
Temp_G >>= 1;
Temp_B >>= 1;
}
RGB_Send(RGB_Buf);
}
}
}
// RGB 红色测试函数
void RGB_Red(uint8_t num)
{
uint8_t i = 0;
for ( i = 0 ; i < num ; i++ )
{
RGB_Set_Buf(i,RED);
}
RGB_Send_Handler(num);
}
void RGB_Blue(uint8_t num)
{
uint8_t i = 0;
for ( i = 0 ; i < num ; i++ )
{
RGB_Set_Buf(i,BLUE);
}
RGB_Send_Handler(num);
}
效果图

下期预告
基于本文章的扩展,WS2812灯带随着音乐而变化(STM32F103C8T6)
📺 [HAL]STM32 SPI+DMA驱动WS2812灯带随音乐变化
获取源码