STM32音乐同步氛围灯指南
Gitee:WS2812: STM32f103C8T6拾音炫彩灯项目 (gitee.com)
项目的由来
最近刚刚学完STM32想找找点项目来实践一下 ,在逛淘宝时突然间遇到了这个(毕竟年轻人都喜欢光污染),突然间就想做这个,于是是就在网上找到的相关资料。废话不多说开干。
硬件的分析和项目实现分析
我使用stm32f103c8t6来开发,RGB灯这方面使用最流行WS2812B,拾音器是MAX4466。主要就桌子上的这些,STM32就不详细介绍,网上的资料一大堆,这里主要讲解的是WS2812和MAX4466的分析。
WS2812B
WS281B的基本电气特性
由上图我们得知,单颗WS2812B的供电电压是3.7~5.3v,也就说开发板上的5v供电足以支撑单颗灯珠,但是我们点亮肯定不止单颗灯珠,是一连串联RGB灯珠,如果都用开发板肯定造成供不足的(效果如图1),最好就是独立供电(效果如图2)能让灯珠有充足的电力,因此这里我多买了一个USB转DC(5v1a)的线供电。
另外,正常情况下WS2812B是不会接上VCC和GND是不会发亮,得从单片机给OUT输出信号(0码,1码,RESET码)他才会发亮。要是想了解他的具体硬件构成可以参考下面的博客。(记得WS2812B多引一条GND出来要与单片机共GND)
参考博客:【模块介绍】WS2812(硬件部分)-CSDN博客
图1
图2
WS2812的传输方式机及工作原理
下图是WS2812B的一个的数据传输方式,第一个WS2812 LED接收到24位数据后,会将其存储并转发给下一个RGB,第二个WS2812 RGB接收到24位数据后,会将其存储并转发给下一个RGB,如此类推,这样可以实现RGB之间的级联。
参考博文:在线调色板,调色板工具,颜色选择器 (sojson.com)
同时我们看到WS2812B灯珠是由24bit的数据组成的,每8bit就单独控制一种原色(光学三原色,不了解参考上面的博文链接),通过光学三原色的不同颜色比例配置发出不同颜色的光,从而形成多彩效果。另外WS2812是高位先发送,就是G7开始先发送,这就是单颗灯珠的工作原理。
G7~G0代表绿色
R7~R0代表红色
B7~B0代表蓝色
然而WS2812B每一位bit并不是用电平信号的 “0”、“1” 来表示,而是用一段的持续的高低电平来表示 “0”、“1” 和 "RESET",就是数据手册上说的0码、1码及RESET码。也就是说它实际上是以一个特定脉冲信号来表示这三种信号。(如下图)
以下是用CudeMX配置方式来点亮单颗RGB示例代码
/* USER CODE BEGIN Includes */
#include "stdint.h"
/* USER CODE END Includes */
/* USER CODE BEGIN 0 */
//延时函数
void delay_ns(uint32_t nus){
while(nus--);
}
//0码
void ws2812_sendLow(void)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
delay_ns(1);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
delay_ns(2);
}
//1码
void ws2812_sendHigh(void)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
delay_ns(3);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
delay_ns(2);
}
//RESET码
void ws2812_Reset(void)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
delay_ns(3400);
//HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
}
/* USER CODE END 0 */
/******************************************************main****************************************************/
int main(void)
{
/* USER CODE BEGIN 1 */
uint8_t i = 0;
/* USER CODE END 1 */
....
....
/* USER CODE BEGIN 2 */
ws2812_Reset();
for(i=0; i<24; i++)
{
ws2812_sendHigh();
}
/* USER CODE END 2 */
}
另外我在我实际测试中发现 “0码”、“1码”和“RESET码”,只要在一段脉冲内构成对应占空比关系就能形成信号,不一定严格按照数据手册上的值来配置。可能多个10us~1ns都可以,以下是我测试德大概的比例关系,大家可以参考一下。
0码:T0H大概是30%~35%左右。
1码:T1H大概是70%~65%左右。
RESET码:占空比为0,持续的低电平。
MAX4466麦克风
MAX4466的电气特性机工作原理
MAX4466大概就长这个这个样子,从以下图片我们看到它就三根线,分别VCC(电源),GND(公共端),OUT(信号输出)。他的工作原理就是通过采集外界声音,内部边路再做一个放大信号处理,最后把处理完的模拟信号传递给的MCU。
背面还有一个十字螺丝位用于调节灵敏度。往右边是灵敏度往最大调整,往有左边则是最小值调整,通常在-44dB至-3dB之间。
MAX4466的拾音数字信号
网上我找了很多资料都没有说明传感器的分贝采集范围,于是我就换了个思路。直接用工具获取数字信号范围。这里我就用vofa+模拟软件来获取,MAX4466的采集灵敏度调节在中灵敏度。就简单用一首DJ歌曲测了一下。大概就是在3500~2000范围,基本上载2000左右就已经没反应了,后面再也没有啥的数据输出。
PS:不过值得注意的这个模块虽然写着工作电压2.4v~5.5v。但是我买过好几个MAX4466,他的理想工作电压是3.3v。要不然就是不灵敏或者一直发热,我自己其中有一颗就直接烧了。
项目的实现思路
参考博客:【STM32F4系列】【HAL库】【自制库】WS2812(软件部分)(PWM+DMA)_ws2812数据手册-CSDN博客
参考博客:STM32 SPI+DMA驱动WS2812_stm32 ws2812-CSDN博客
这里我用 TIM(定时器)+DMA(内存直接访问)+PWM(宽度脉冲) 方式来实现。当然你也可以用SPI + PWM的方式来传送,之所以考虑到使用“TIM+DMA+PWM”的方式是因为可以让CPU腾空时间来可以去处理别的事情 ,所以没有用delay延时的方式进行实现。毕竟CPU的资源很珍贵,要为后面的开发拓展做别的事情做准备。
整体工作原理图
硬件结构上比较简单,信号的来源通过前端设备MAX4466来接受,通过MAX4466拾音器作为前端设备采集音乐的来源,使用GPIO模拟输入方式把采集到的模拟信号做ADC处理得到数字信号。并对数字信号做了分级,由高到低分别对应或多或少的灯珠个数。如果采集到的声音比较高时,那么ADC转换出来的数据就比较大,对应地就点亮多颗灯珠;反之如果声音比较低就会点亮较少的灯珠。同时STM32实时监控前端设备的数据,并对通过函数的形式跟WS2812B交互。当函数接受到灯珠的个数后,就会通过DMA的方式周期性地把灯光颜色的数据往TIM的输入比较功能做搬运,形成WS2812B所能识别的PWM波形。最后通过GPIO推挽输出的方式把数据传给WS2812B,WS2812B识别到据后就会相对的灯珠和颜色点。
/******************************************************WS2812B****************************************************/
/**
* @brief 将uint32转为发送的数据
* @param Data:颜色数据
* @param Ret:解码后的数据(PWM占空比)
* @return
* @author HZ12138
* @date 2022-10-03 18:03:17
*/
void WS2812_uint32ToData(uint32_t Data, uint32_t *Ret)
{
uint32_t zj = Data;
uint8_t *p = (uint8_t *)&zj;
uint8_t R = 0, G = 0, B = 0;
B = *(p); // B 00
G = *(p + 1); // G 00
R = *(p + 2); // R ff
zj = (G << 16) | (R << 8) | B;
for (int i = 0; i < 24; i++)
{
if (zj & (1 << 23))
Ret[i] = WS2812_Code_1; //71/105
else
Ret[i] = WS2812_Code_0; //32/105
zj <<= 1;
}
Ret[24] = 0;
}
/**
* @brief 开始发送颜色数据
* @param 无
* @return 无
* @author HZ12138
* @date 2022-10-03 18:05:13
*/
void WS2812_Start(void)
{
HAL_TIM_PWM_Start_DMA(WS2812_TIM, WS2812_TIM_CHANNEL, (uint32_t *)WS2812_Rst, 240); //给WS2812发送一个RESET码
WS2812_uint32ToData(WS2812_Data[0], WS2812_SendBuf0); //RGB->TIM用于生成特定PWM的数据
WS2812_En = 1;
}
/**
* @brief 发送复位码
* @param 无
* @return 无
* @author HZ12138
* @date 2022-10-03 18:05:33
*/
void WS2812_Code_Reast(void)
{
HAL_TIM_PWM_Start_DMA(WS2812_TIM, WS2812_TIM_CHANNEL, (uint32_t *)WS2812_Rst, 240);
WS2812_En = 0;
}
/**
* @brief 发送函数(DMA中断调用)
* @param 无
* @return 无
* @author HZ12138
* @date 2022-10-03 18:04:50
*/
void WS2812_Send(void)
{
static uint32_t j = 0;
static uint32_t ins = 0;
if (WS2812_En == 1)
{
if (j == WS2812_Num)
{
j = 0;
HAL_TIM_PWM_Stop_DMA(WS2812_TIM, WS2812_TIM_CHANNEL);
WS2812_En = 0;
return;
}
j++;
if (ins == 0)
{
HAL_TIM_PWM_Stop_DMA(WS2812_TIM, WS2812_TIM_CHANNEL);
HAL_TIM_PWM_Start_DMA(WS2812_TIM, WS2812_TIM_CHANNEL, WS2812_SendBuf0, 25);
WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf1); //71 71 32 32
ins = 1;
}
else
{
HAL_TIM_PWM_Stop_DMA(WS2812_TIM, WS2812_TIM_CHANNEL);
HAL_TIM_PWM_Start_DMA(WS2812_TIM, WS2812_TIM_CHANNEL, WS2812_SendBuf1, 25);
WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf0);
ins = 0;
}
}
}
//使用定时器触发事件
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
WS2812_Send();
}
void light_mode_breath(void)
{
static uint32_t color = 0;
if (0x00ff00 == color)
{
color = 0;
}
color += 0x001100;
for (int i = 0; i < 50; i++)
{
WS2812_Data[i] = color;
}
WS2812_Start();
WS2812_DELAY(200);
}
for (int i=0; i<WS2812_Num; i++)
{
WS2812_Data[i] = 0;
}
for (int i = 0; i < history_light_count; i++)
{
WS2812_Data[i] = color;
}
if (history_light_count > 0)
{
history_light_count --;
}
WS2812_Start();
}
void light_mode_music_process2(void)
{
static uint8_t history_light_count = 0; //当前灯珠个数
//接受MAX4466的反馈回来的的灯珠数
if (g_light_count > history_light_count)
{
history_light_count = g_light_count;
g_light_count = 0;
}
//先给RGB数组清零
for (int i=0; i<WS2812_Num; i++)
{
WS2812_Data[i] = 0;
}
if(WS2812_Data[history_light_count] == 0)
{
int i = 0;
if(i < history_light_count)
{
for (int i = 0; i <= history_light_count; i++)
{
WS2812_Data[i] = RGB[i];
}
}
}
if (history_light_count > 0)
{
history_light_count --;
}
WS2812_Start();
}
参考视频:桌搭好物-拾音节奏灯横向评测! 十几倍的差价有什么区别?哔哩哔哩bilibili
但是这样子并没有没达我们的需求,没有那种随着音乐的节奏高低而随之跳动效果。那怎么才能达到这样的效果呢?
我的想法就是把之前用模拟软件测试到的数字信号范围来实现!声音高的时候数字信号就会随之变大,声音高的时候数字信号就会随之变小。那么按照这个规律,我可以做一个判断,当数字信号变大的时候,那我们就点亮多颗RGB;当数字信号变小的时候,那我们就点亮较少RGB。
再进一步,我把3000~2000的数字信号分成5个等级,每个等级的数字信号的又对应着点亮或多或少的RGB。那么这样一来结合我上面实现点亮RGB的方法。就会形成那种会随着根据声音的高低不同就会点亮或多或少的灯珠效果。
以下是用“TIM + DMA + PWM”的实现代码以及分级实现的代码:
/******************************************************MAX4466****************************************************/
uint8_t mic_get_grade(void)
{
uint32_t adc_value=0, a=0, b=0, c=0;
//这里做3次采样,取最高值。
for (int i = 0; i<1; i++)
{
HAL_ADC_Start(&hadc1);
a = HAL_ADC_GetValue(&hadc1);
for (int i = 0; i<1; i++)
{
HAL_ADC_Start(&hadc1);
b = HAL_ADC_GetValue(&hadc1);
for (int i = 0; i<1; i++)
{
HAL_ADC_Start(&hadc1);
c = HAL_ADC_GetValue(&hadc1);
}
}
adc_value = c > (a > b ? a : b) ? c : (a > b ? a : b);
}
if (adc_value > 3000)
{
return 60;
}
else if (adc_value > 2400)
{
return 50;
}
else if (adc_value > 2300)
{
return 40;
}
else if (adc_value > 2200)
{
return 30;
}
else if (adc_value > 2100)
{
return 20;
}
else
{
return 0;
}
}
/******************************************************main****************************************************/
int main(void)
{
/*******/
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM1_Init();
MX_USART1_UART_Init();
MX_ADC1_Init();
MX_TIM2_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//接受收麦克风反馈值
heighest_grade = mic_get_grade();
if (heighest_grade > 0)
{
light_mode_music_set(heighest_grade);
heighest_grade = 0;
}
}
/* USER CODE END 3 */
}
另外这边我们使用到HAL里的另一个函数“HAL_TIM_PWM_Start_DMA”,这个函数可以启动定时器的PWM输出,并同时启动DMA传输。
以下是他的函数原型:
void HAL_TIM_PWM_Start_DMA(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t *pData, uint32_t Length);
这里的参数说明如下:
TIM_HandleTypeDef *htim: 指向一个TIM_HandleTypeDef结构体实例的指针,该结构体包含了所使用的定时器的所有配置信息。
uint32_t Channel: 表示要启动PWM输出的定时器通道。通常是一个预定义的常量,例如TIM_CHANNEL_1、TIM_CHANNEL_2等。
uint32_t *pData: 指向一个包含要通过DMA传输到定时器的CCR寄存器的数据数组的指针。
uint32_t Length: 数组中元素的数量,也就是要通过DMA传输的数据数量。
CudeMX的配置
GPIO的配置
配置MAX4466、ST-Link、UART等外设。
RCC的配置
配置时钟树使用外部晶振
TIM的配置
配置定时器“TIM+PWM+DMA”的初始化配置
TIM2的配置
配置第二个定时器,做周期性的信号触发
开发中的问题和后续
这次开发过程在中在使用发送函数“WS2812_Send”时遇到一个Bug。每次在给WS2812B发送下一轮数据是时,数据都会很莫名奇妙地乱码。发不出一轮数据是一直发布出去,最后每次在发送下一轮数据数据时加一个停止信号再开始。那才能在发送下一轮数据时才能识别。
有懂的可以解释一下。
参考视频:【Arduino项目案例】WS2812B灯带开源项目哔哩哔哩bilibili
虽然已经实现了效果,但是还没有实现RGB会随着音乐的高低忽明忽暗的渐变效果。后期根据光学中的HSV颜色模型(Hue, Saturation, Value)打算实先一个算法。这个算法可以根据当前色调、饱和度、亮度来配置WS2812B的灯珠RGB占比来实现效果。
作者:stactgl