【STM32】蓝牙氛围灯

Docs

一、项目搭建和开发流程

一、项目需求和产品定义

1.需求梳理和产品定义

  1. 一般由甲方公司提出,或由本公司市场部提出

  2. 需求的重点是:这个产品究竟应该做成什么样?有哪些功能?具体要求和参数怎样?此外还要考虑售价和成本、可靠性和质保期、是否防水等各种因素。

  3. 提需求的人不需要懂技术,关键是懂市场、懂产品。

2.项目需求书编写

  1. 产品核心功能:RGB LED全彩色控制、音乐节奏控制、手机APP蓝牙控制

  2. 分类级别:消费级

  3. 具体参数:譬如亮度范围、颜色变化速率、声音敏感度范围、功耗等

3.方案设计(需求-->技术)

  1. RGB LED使用WS2812灯条实现【专门控制串行灯条】

  2. 音乐节奏控制使用麦克风 结合单片机ADC实现

  3. 手机app蓝牙控制使用串口蓝牙模块实现(想想是否最佳方案?有没更适合的?)

二、根据项目需求对项目拆解规划

1.硬件选型在意什么?

消费级、工业级、车规级**【使用年限】**

1、产品质量

2、使用环境

温度范围有要求

性能是都满足要求

供货稳定性

PinToPin

在硬件设计领域,"PinToPin" 可能指的是两个设备或芯片之间的引脚对应关系。这可以涉及到确保一个芯片的引脚与另一个芯片的引脚相匹配,以便在连接它们时能够正确传递信号。

2.硬件选型的平台

采购芯片要注意什么?

是否是翻新?

1、价格

2、看公司,合法合规的代理商

3、找人鉴定

1、嘉立创 https://www.szlcsc.com/

2、淘宝

3、融创芯城 http://digiic.com/

3.实现上述功能需要哪些硬件?

1、stm32f103(主控)

2、蓝牙模块

3、麦克风模块

4.软件框架

分层

三、开发环境搭建

如果装过MDK其他版本怎么办?

MDK安装(PACK安装)

STM32CubeMX安装

【STM32】两个版本MDK搭建和三种调试器的使用-CSDN博客

ST-Link驱动安装

四、硬件设计

1.原理图和PCB设计

设计流程

原理图设计的思路:参考芯片公司提供的数据手册

评审:开会,讨论设计是否合理

设计PCB:

  1. 制作封装(可选)

  2. 布局,考虑器件摆放、散热问题

  3. 布线

原理图和PCB设计设计软件:

立创EDA https://lceda.cn/editor

AD 上手比较容易

PADS 快,轻,教程没有AD那么多,上手难

原理图设计软件:

ORCAD 上手难度低、它能兼容市面上常见的PCB画图软件

2. 通过接线的方式替代PCB绘制和焊接

解答不同stm32和不同电源兼容使用方式

1、stm32f103xxx你只要按照视频中

PA8->WS2812的数据线

PB0->麦克风的OUT

PA10(RX)->蓝牙模块的TXD

PA9(TX)->蓝牙模块的RXD

2、用其他供电线供电

确定电压是5V,只要找到正负极,按照视频中的接线方式来接就可以

蓝牙模块:广州汇承信息科技有限公司 (hc01.com)

二、WS2812

1.为什么可以发出那么多颜色:三基色

WS2812 内部集成了处理芯片和3颗不同颜色的led灯(红,绿,蓝) ,通过单总线协议分别控制三个灯的亮度强弱,达到全彩的效果,每一个灯需要 8 bits(1 byte) 的数据,所以一颗 ws2812 共需要24 bits(3 bytes) 的数据。

在线调色板,调色板工具,颜色选择器 (sojson.com)

2.数据手册

1.逻辑电压 VS 电源电压

逻辑电平实际上是在一定范围内将其视为高电平(1)或者低电平(0)。

2.时序波形图

注意点:RES是us为单位的、

因为此时的时间都是ns,us为单位,所以不能使用HAL_Delay【ms为单位】

ws2812 的特点是可以多个灯珠串联起来,这样就可以通过一个总线控制多个灯珠:

其实可以看成:

逻辑1:高电平的时间占2/3,低电平的时间占了1/3

逻辑0:低电平的时间占2/3,高电平的时间占1/3

3.数据传输方式

1)100个颜色数据对应100个灯珠,串行顺序接收,每一个灯珠只能获得一个24位数据,每一个24位的数据被获取之后就相当于没有

2)REST信号也是级联传输的

3)级联------>串联

4.LED特性参数

通过输入电流,可以计算出最多可以承载多少个led

5.24位数据结构

1)高位先发

2)数据越大,则颜色越深,越亮

3)按照G----R----B

3.CubeMX设置

4.WS2812三种驱动方式之一:GPIO+延时

1.添加自定义延时函数

cpp 复制代码
#include "stdint.h"

//自定义延时函数
void delay_ns(uint32_t nus){//单位为us
	while(nus--);
}

验证延时函数是否正确:

1)使用debugger

2)使用其他测试工具

3)使用GPIO电平高低变化观察

2.测试相关的代码

cpp 复制代码
/**
  * @brief  根据WS281x芯片时序图编写的发送0码,1码RESET码的函数
  * @param  
  * @retval None
  */
void ws281x_sendLow(void)   //发送0码
{	
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
		delay_ns(1);    //示波器测试约为440ns
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
		delay_ns(2);
}
void ws281x_sendHigh(void)   //发送1码
{
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
		delay_ns(2);
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
		delay_ns(1);
}
void ws2811_Reset(void)        //发送RESET码
{ 
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
		delay_ns(60);  
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
}

3.测试

cpp 复制代码
  while (1)
  {
		ws281x_sendLow();
  }

4.问题解决

最后还是使用__nop__

cpp 复制代码
//自定义延时函数
void delay_ns(uint32_t nus){//单位为us
	//while(nus--);//测试后发现还是不准确
	__nop();
}

/**
  * @brief  根据WS281x芯片时序图编写的发送0码,1码RESET码的函数
  * @param  
  * @retval None
  */
void ws281x_sendLow(void)   //发送0码
{
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
		__nop();
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
		delay_ns(2);
}
void ws281x_sendHigh(void)   //发送1码
{
		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);
}
void ws2811_Reset(void)        //发送RESET码
{ 
		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);
}

5.代码编写

1)在发送数据前,要先发一个RESET信号

cpp 复制代码
  while (1)
  {
		uint16_t i=0;
		ws2811_Reset();
		for(i=0;i<8;i++){//0xff G
			ws281x_sendHigh();
		}
		for(i=0;i<8;i++){//0x00 R
			ws281x_sendLow();
		}
		for(i=0;i<8;i++){//0x00 B
			ws281x_sendLow();
		}
  }

5.WS2812三种驱动方式之:TIM+PWM+DMA

简而言之:修改DMA的传输数组,就可以修改PWM波的输出

DMA-->TIM--->PWM

ws2812 程序设计与应用(1)DMA 控制 PWM 占空比原理及实现(STM32)_ws2812 pwn-CSDN博客

【STM32F4系列】【HAL库】【自制库】WS2812(软件部分)(PWM+DMA)_ws2812数据手册_Hz1213825的博客-CSDN博客

STM32F1/F7使用HAL库DMA方式输出PWM详解(输出精确数量且可调周期与占空比)_hal输出pwm-CSDN博客

0.为什么使用到该些方法

TIM:因为如果我们手动的调节电平的高低会很浪费时间,并且会影响其他程序的执行,所以我们直接使用定时器

PWM(定时器中的输出比较):PWM可以自动生成波形,不需要程序手动的调整波形

DMA:用于传输大量数据的专门通道【如果我们使用到100颗灯珠,则1个灯珠需要1位24bit的二进制,则100*24=2400比特,需要的传输空间比较大,DMA最合适不过】

1.CubeMX设置

0.配置时钟

由于本次实验对时间控制很严格 ,所以我们要使用外部时钟才会比较精确

1.设置PWM(定时器输出比较模式)

要注意重载值的设置【注意是分频上面WS2812逻辑1和逻辑0的高低电平的时间分配】

2.配置DMA

本项目中最好只使用【Normal】,当触发一次才进入一次,不要让其自己重复进入

因为我们使用DMA通道传输数据,所以我们要记得开启DMA中断

3.开启调试

2.代码解读

TIM->PWM:将数据转换为PWM波

使用DMA传输数据很好理解,为什么DMA可以控制PWM脉冲数量和占空比呢?这里我们回归本质,在DMA控制PWM输出的过程中,DMA依然传输的是数据,只不过它送过去的是比较值,即捕获/比较寄存器(TIMx_CCRx)的值,这个值不用多解释了,和自动重装载寄存器(TIMx_ARR)的值分别决定周期和占空比。

我们代码颜色设置是RGB,而数据手册中是GRB

cpp 复制代码
/**
 * @brief 将uint32转为发送的数据
 * @param Data:颜色数据   0x ff 00 00
 * @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【最低8位】  00
    G = *(p + 1); // G【次高8位】  00
    R = *(p + 2); // R【最高8位】  ff
    zj = (G << 16) | (R << 8) | B;
    for (int i = 0; i < 24; i++)
    {
			/**
			#define WS2812_Code_0 (32u)
			#define WS2812_Code_1 (71u)
			*/
        if (zj & (1 << 23)){//判断此位与1位于(&)结果是否为1,如果为1表示此时是传输1
            Ret[i] = WS2812_Code_1;//  71/105
				}
        else{//表示此时传输0
            Ret[i] = WS2812_Code_0;//  32/105
				}
        zj <<= 1;//因为我们都是判断最高位,所以每一次判断完后,都要将整体数据左移
    }
    Ret[24] = 0;
}
为什么重载值设置为105

定时器初始为高电平

发送数据

1)这里我们使用两个数组来存储和发送数据,是因为DMA在使用时要读取数据还要发送数据,如果我们就操作一个数组会容易出现错误。所以我们使用两个数组交替存储

2)我们实际上是传输24个bit,我们多传输1位是在数据之间的间隔

cpp 复制代码
/**
 * @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;
					//在DMA模式下停止TIM PWM信号的产生
            HAL_TIM_PWM_Stop_DMA(&WS2812_TIM, WS2812_TIM_Channel);
            WS2812_En = 0;//失能
            return;
        }
        j++;//表示数据还未传输结束
        if (ins == 0)//我们使用两个数组来存放解码后【转换为PWM】的数据
        {
					/**
						实际上第一次进来的时候buf0中的数据是第1个灯【WS2812_Data[0]-->start中赋值的】的数据
						然后我们将这个数据传输给TIM,
						然后我们在将WS2812_Data[1]数据传输给buf1,此时buf0数据为空
					*/
					//我们实际上传输24bit作为一位,但是我们这里传输25是作为两个数据之间的间隔
					//从SendBuf0中取25个bit传输给time
            HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, WS2812_SendBuf0, 25);
					//这里我们将数据转换为对应的PWM波,然后发送给WS2812_SendBuf1
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf1);
            ins = 1;
        }
        else
        {
					//从SendBuf1中取25个bit传输给time
            HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, WS2812_SendBuf1, 25);
					  //这里我们将数据转换为对应的PWM波,然后发送给WS2812_SendBuf0
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf0);
            ins = 0;
        }
    }
}
起始函数

**我们要先发送一个初始位,然后发送第一灯的颜色数据【WS2812_Data[0]】的数据进入,从而触发PWM的生成,**才可以进入PWM的中断回调函数【HAL_TIM_PWM_PulseFinishedCallback】,然后调用SendByte

cpp 复制代码
/**
 * @brief 开始发送颜色数据
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-10-03 18:05:13
 */
void WS2812_Start(void)
{		//给WS2812发送一个RESET信号
    HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, (uint32_t *)WS2812_Rst, 240);
		//RGB--->TIM用于生成特定定时器
	
    WS2812_uint32ToData(WS2812_Data[0], WS2812_SendBuf0);//将第一个数据转换为PWM
    WS2812_En = 1;//使能
	
	//经过上面的开启PWM和DMA,从而进入【HAL_TIM_PWM_PulseFinishedCallback】--->从而调用发送数据
}
发送复位函数

发送失能位

cpp 复制代码
/**
 * @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;//失能
}
中断回调函数

此程序我们就是简单的点亮led

cpp 复制代码
//定时器+PWM中断回调函数【在TIM中】
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
  WS2812_Send();
}

3.完整代码

GitHub - HZ1213825/HAL_STM32F4_WS2812: WS28112驱动(软件模拟和PWM+DMA)

WS2812.c
cpp 复制代码
#include "WS2812.h"
uint32_t WS2812_Data[WS2812_Num] = {0};

uint32_t WS2812_SendBuf0[25] = {0};   //发送缓冲区0
uint32_t WS2812_SendBuf1[25] = {0};   //发送缓冲区1
const uint32_t WS2812_Rst[240] = {0}; //复位码缓冲区
uint32_t WS2812_En = 0;               //发送使能
/**
 * @brief 将uint32转为发送的数据
 * @param Data:颜色数据   0x ff 00 00
 * @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【最低8位】  00
    G = *(p + 1); // G【次高8位】  00
    R = *(p + 2); // R【最高8位】  ff
    zj = (G << 16) | (R << 8) | B;
    for (int i = 0; i < 24; i++)
    {
			/**
			#define WS2812_Code_0 (32u)
			#define WS2812_Code_1 (71u)
			*/
        if (zj & (1 << 23)){//判断此位与1位于(&)结果是否为1,如果为1表示此时是传输1
            Ret[i] = WS2812_Code_1;//  71/105
				}
        else{//表示此时传输0
            Ret[i] = WS2812_Code_0;//  32/105
				}
        zj <<= 1;//因为我们都是判断最高位,所以每一次判断完后,都要将整体数据左移
    }
		//两个数值之间的间隔
    Ret[24] = 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;
					//在DMA模式下停止TIM PWM信号的产生
            HAL_TIM_PWM_Stop_DMA(&WS2812_TIM, WS2812_TIM_Channel);
            WS2812_En = 0;//失能
            return;
        }
        j++;//表示数据还未传输结束
        if (ins == 0)//我们使用两个数组来存放解码后【转换为PWM】的数据
        {
					//我们实际上传输24bit作为一位,但是我们这里传输25是作为两个数据之间的间隔
            HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, WS2812_SendBuf0, 25);
					//这里我们将数据发送给WS2812_SendBuf1
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf1);
            ins = 1;
        }
        else
        {
            HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, WS2812_SendBuf1, 25);
					//这里我们将数据发送给WS2812_SendBuf1
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf0);
            ins = 0;
        }
    }
}
/**
 * @brief 开始发送颜色数据
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-10-03 18:05:13
 */
void WS2812_Start(void)
	{		//给WS2812发送一个RESET信号
    HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, (uint32_t *)WS2812_Rst, 240);
		//RGB--->TIM用于生成特定定时器
	//经过上面的开启PWM和DMA,从而进入【HAL_TIM_PWM_PulseFinishedCallback】--->从而调用发送数据
    WS2812_uint32ToData(WS2812_Data[0], WS2812_SendBuf0);//将第一个数据转换为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;//失能
}

//定时器+PWM中断回调函数【在TIM中】
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
  WS2812_Send();
}
cpp 复制代码
#include "ws2812.h"

uint32_t WS2812_SendBuf0[25] = {0};   //发送缓冲区0
uint32_t WS2812_SendBuf1[25] = {0};   //发送缓冲区1
const uint32_t WS2812_Rst[240] = {0}; //复位码缓冲区
uint32_t WS2812_En = 0;               //发送使能


uint32_t WS2812_Data[WS2812_Num] = {0};


/**
 * @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
    G = *(p + 1); // G
    R = *(p + 2); // R
    zj = (G << 16) | (R << 8) | B;
    for (int i = 0; i < 24; i++)
    {
        if (zj & (1 << 23))
            Ret[i] = WS2812_Code_1;
        else
            Ret[i] = WS2812_Code_0;
        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_uint32ToData(WS2812_Data[0], WS2812_SendBuf0); //把WS2812_Data[0]用于生成位PWM的波形的数据,然后存储到buffer中
    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)
        {
						//此语句的意思:
						//将buffer0中的25个数据传输给TIM
            HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, WS2812_SendBuf0, 25);//24bit的RGB数据
						//将第j个数据转换为PWM波,传输给buffer1中
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf1);
            ins = 1;
        }
        else
        {
            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();
}
WS2812.h
cpp 复制代码
#ifndef __WS2182__H__
#define __WS2182__H__
#include "main.h"
/*
硬件定时器PWM+DMA:
需要:
    1.定时器:
        PWM输出一个通道
        不分频
        计数值为 1.25us(公式: 1.25 *系统频率(单位MHz))
        输出IO设置为开漏浮空输出(外接5V上拉)
    2.DMA
        内存到外设
        字(word)模式
        开启DMA中断
0码的是 0.4us,1码是0.8us
公式是 t(us)*系统频率(单位MHz)
 */
extern TIM_HandleTypeDef htim1;
#define WS2812_Hardware
#define WS2812_TIM htim1
#define WS2812_TIM_Channel TIM_CHANNEL_1
#define WS2812_Code_0 (32u)
#define WS2812_Code_1 (71u)

#define WS2812_Num 8

extern uint32_t WS2812_Data[WS2812_Num];
void WS2812_Code_Reast(void);
void WS2812_Send(void);

void WS2812_Start(void);


#endif
cpp 复制代码
#ifndef __WS2812__H__
#define __WS2812__H__

#include "stdint.h"
#include "main.h"

extern TIM_HandleTypeDef htim1;

#define WS2812_TIM htim1
#define WS2812_TIM_Channel TIM_CHANNEL_1

#define WS2812_Code_0 (32u)
#define WS2812_Code_1 (71u)


#define WS2812_Num 60
extern uint32_t WS2812_Data[WS2812_Num];



#endif
main.c
cpp 复制代码
int main(void)
{
  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_DMA_Init();
  MX_TIM1_Init();
  MX_USART1_UART_Init();

		  for (int i = 0; i < 8; i++)//			00   ff   00
				WS2812_Data[i] = 0x0000FF; //    R    G    B

  while (1)
  {
					WS2812_Start();
					HAL_Delay(1000);
  }
}

由下图可知,我们代码还是存在问题

1)问题一:我们明明要求点亮8颗,可实际上却点亮9颗

2)问题二:我们点亮的是蓝色,但却出现了其他颜色

参考代码

code/new_complment/1.light_up_led · 林何/stm32 bluetooth ambient light - 码云 - 开源中国 (gitee.com)

4.实现呼吸灯

注意点:

因为RGB三位颜色都是uint8_t,所以计数范围从0-255,所以我们在逐渐增强颜色的时候要注意这个递增数值最后是否可以被255(256)整除,如果可以才正常显示,要不然可能会出现无法正确显示的现象。

cpp 复制代码
//呼吸灯
void color_breath(){
		static uint32_t color = 0x000000;	
		if (0x00ff00 == color)//这里我们设置呼吸灯为绿色
		{
			color = 0;
		}
		for (int i = 0; i < WS2812_Num; i++)
		{
			WS2812_Data[i] = 0x000000;
		}
		/**
			这里的color++,要注意点
			因为rgb是255,所以加的这个值要可以被255整除
		*/
		color += 0x001100;
		for (int i = 0; i < WS2812_Num; i++)
		{
			WS2812_Data[i] = color;
		}
		WS2812_Start();	
		HAL_Delay(50);
		
}

5.跑马灯

LDSCITECHE/WS281X (gitee.com)

cpp 复制代码
//跑马灯
void color_run(){
	
	static uint32_t first=0,second=0,third=0,flag=0;
		uint32_t color_red=0x550000;//红色
		uint32_t color_green=0x005500;//绿色
		uint32_t color_bule=0x000055;//蓝色
	if(3==flag){
		flag=0;
	}
	if(0==flag){
		first=color_red;
		second=color_green;
		third=color_bule;
	}else if(1==flag){
		first=color_bule;
		second=color_red;
		third=color_green;
	}else{
		first=color_green;
		second=color_bule;
		third=color_red;
	}
	flag++;
	for(int i=0;i<65;i+=3){
		WS2812_Data[i]=first;
		WS2812_Data[i+1]=second;
		WS2812_Data[i+2]=third;
	}
	WS2812_Start();
	HAL_Delay(200);
}

代码参考:

code/new_complment/2.breath_and_marquee · 林何/stm32 bluetooth ambient light - 码云 - 开源中国 (gitee.com)

6.WS2812三种驱动方式之:SPI

STM32 SPI+DMA驱动WS2812_stm32 ws2812-CSDN博客

使用STM32F103的SPI+DMA驱动ws2812 LED_spi控制ws2812_二狗正在赶来路上的博客-CSDN博客

三、串口模块:USART,printf(重定向)

中断函数里为什么不能调用printf - PingCode

注意点:

如果我们使用了中断,则在中断后面使用是无法在串口中打印的,因为一旦进入中断,就不会继续执行后面的代码

1. USB转TTL的接线

USB转TTL 单片机

TX PA10(RX)

RX PA9(TX)

GND GND(G)

2. printf重定位

参考博客:STM32-HAL库-printf函数重定向(USART应用实例)_hal库printf重定向_Calvin Haynes的博客-CSDN博客

  1. 使用stm32cubemx新增串口外设并重新生成工程

  2. 添加串口重定向代码

cpp 复制代码
#ifdef __GNUC__
	#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
	#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
  return ch;
}
  1. 包含stdio.h

  2. 勾选 use microlib

四、麦克风模块;ADC

https://www.cnblogs.com/dongxiaodong/p/14355843.html

1.原理讲解

在我们生活的世界中,一切都是模拟量,你说话,你唱歌模拟量,如何让声音可以被计算处理?

这时可以通过麦克风将你的声音转换成电压信号,再让单片机通过ADC把电压信号转换数字信号,这时计算机就能听懂你说话了。

2.CubeMX的设置

3.相关HAL的使用

cpp 复制代码
  while (1)
  {
		HAL_ADC_Start(&hadc1);
		adc_value=HAL_ADC_GetValue(&hadc1);
		printf("hello world");
		printf("adc_vlaue=%d\r\n",adc_value);
		HAL_Delay(100);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

4.Vofa的使用

  1. 让数据能被软件接收

    1. 波特率、停止位、数据位等是否与单片机配置的一致
  1. 打开串口
  1. 数据格式
  1. 组件和数据添加

如果看不到数据点击Auto,让数据显示在中间

调节时间(Y轴)拖动Auto下方的红点、紫点、绿点

5.代码编写

我们通过上面使用Vofa的测试知道了大概的声音取值范围

我们此时的任务:通过编写代码获取ADC值,然后进行判断(使用返回值),返回多少我们就点亮响应的led个数

1)我们使用多次获取的ADC值,然后求其平均值可以使得获得的值更加准确(起到滤波的作用)

cpp 复制代码
//获取将声音划分为等级,然后通过亮几颗灯来反映
uint8_t mic_get_grade(){
	//记录adc
	uint32_t adc_value = 0;
	
	for (int i = 0; i<3; i++)
	{
		HAL_ADC_Start(&hadc1);
		adc_value += HAL_ADC_GetValue(&hadc1);
		
	}
	adc_value = adc_value/3;
	//get_adc=HAL_ADC_GetValue(&hadc1);
	//注意点:我们要从最大值进行判断,要不然如果从最小开始判断
	//则后面会一致都满足条件
	if(adc_value>2100){
		return 10;
	}else if(adc_value>2080){
		return 8;
	}else if(adc_value>2076){
		return 6;
	}else{
		return 4;
	}
}

我们使用串口打印adc值,先查看获取的是否正确

cpp 复制代码
		//测试adc转换代码
		static uint32_t adc_current=0;
		for(int i=0;i<5;i++){
			test_grade=	mic_get_grade();
			//将获取得到的最大值分给led进行显示
			if(test_grade>max_grade){
				max_grade=test_grade;
				printf("adc_vlaue:%d\n",max_grade);
				adc_current=HAL_ADC_GetValue(&hadc1);
				printf("adc_current:%d\n",adc_current);
				HAL_Delay(200);
			}
		}

【待补Vofa的adc变化图】

五、Version1-代码编写

1.思路

1.WS2812模块

总思路:增加一个函数,当我们调用这个函数的时候传入一个参数,这个参数就表示当前应该点亮的LED灯的个数,并且让LED灯实现缓慢熄灭

1)编写一个可以设置当前应该点亮led的个数函数

2)编写一个实现向上冲缓慢跌落的函数--->通过定时器来实现【定义一个时间,当超过这个时间还没有传入一个新的值(点亮led)的个数,则将依次熄灭】

2.麦克风模块

增加一个函数,滤波,检测当前的声音有多大,返回声音的大小值

3.蓝牙模块

通过手机连接串口(蓝牙)模块,当用户在手机输入我们设置好的字母,可以选择灯的闪烁模式--->本流程使用到了串口和printf(重定向)

2.流程图绘制

3.WS2812代码开发

1.CubeMX设置

定时器

1)随便使用一个定时器即可

2)因为这个点亮和熄灭过程,是需要人眼可以看清楚,所以刷新率不能太高,要不然人眼难以观察。那么控制刷新率一个关键因素就是:定时器的频率【设置的预分频器】-->STM32F10xxx系列标志频率为72MHZ,所以我们在设置预分频时,为了方便计算,所以我们使用7200-1【分频后的频率为:72 000 000 /7200=10,000】--->表示1s计算10 000个数值

当达到重载值的时候,就会溢出,则就会取执行中断回调函数【当我们设置的值越大的时候,则进入中断的时间越长】

因为要在中断回调函数中调用点亮/熄灭led,所以要记得使能中断

外部时钟设置

2.代码编写

1.设置一个函数可以设置点亮个数
cpp 复制代码
void test_music_set(uint8_t count){
	
			static uint32_t color=0x770000;
	
	
			//先清空数组
			for(int i=0;i<WS2812_Num;i++){
				WS2812_Data[i]=0;
			}
			
		  for (int i = 0; i < count; i++)//			00   ff   00
				WS2812_Data[i] = color; //    R    G    B
			WS2812_Start();
			HAL_Delay(1000);
	
}
2.编写一个使灯缓慢熄灭/点亮的过程

1)这里我初始化count==0;表示我这个是灯的上升过程,如果想要设置下降过程,则初始化count==10【你想要设置的最大高度值】

2)但是我们不能直接在while中编写这个代码(冗余),所以我们使用定时器,来定时器一段时间就进入中断,熄灭灯

3)注意点:如果你想要设置从大到小递减的,则要将count--写在test_music_set函数后面,要不然你先执行了,则最高的led是不会被点亮的。

cpp 复制代码
	static uint8_t count=10;

  while (1)
  {
		if(count==0){
			count=10;
		}
			music_set_count(count);
		
		if(count>0){
			count--;
		}
  }
3.启动定时器

注意点:我们这个是使用IT的使能中断,不能写成普通的中断定时器

cpp 复制代码
  /* USER CODE BEGIN 2 */
	//初始化定时器
	//HAL_TIM_Base_Init(&htim2);
	//使能中断
	HAL_TIM_Base_Start_IT(&htim2);

  /* USER CODE END 2 */

由下图可以知道我们确实在1ms的时候就触发了一次定时器中断

4.中断回调函数
cpp 复制代码
//定时器中断处理函数
//当到时间就进入此函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){

	
}
5.设置一个专门来设置个数的函数

将上面的函数进行了修改

我们设置了g_led_count这个变量,是因为要在多个函数中使用到

cpp 复制代码
void color_num_set(uint8_t count){
	g_led_count=count;//将该值赋值给全局的led,方便我们后面的调用
}
6.定义一个全局变量来给用户设置个数
cpp 复制代码
//设置一个全局变量设置要显示的led个数
static uint8_t g_led_count=0;
7.在中断函数中编写一个灯的点亮和熄灭

思路:

1)先判断用户要点亮多少

2)先将其原来的清空

3)在点亮

4)每一次进入中断,就减少一颗

cpp 复制代码
//定时器中断处理函数
//当到时间就进入此函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	
	static uint32_t color=0xff0000;
	static uint8_t history_light_count=0;
	
	//我们这里是为了判断下一次进入
	//点亮个数是否增加【即表示是否声音变大】
	if(history_light_count<g_led_count){
		//此时点亮个数不够
		history_light_count=g_led_count;
	}
	//清空原来的灯
	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;
	}
	//表示下一次进入中断的时候就会少亮一颗灯
	history_light_count--;
	WS2812_Start();
}
出现bug1:颜色显示不正确

我们前面的PWM波生成有问题

在调用HAL_TIM_PWM_Start_DMA函数之前加上HAL_TIM_PWM_Stop_DMA【以后使用的时候也是最好配对使用】

cpp 复制代码
/**
 * @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;
			//在DMA模式下停止TIM PWM信号的产生
            HAL_TIM_PWM_Stop_DMA(&WS2812_TIM, WS2812_TIM_Channel);
            WS2812_En = 0;//失能
            return;
        }
        j++;//表示数据还未传输结束
        if (ins == 0)//我们使用两个数组来存放解码后【转换为PWM】的数据
        {
			HAL_TIM_PWM_Stop_DMA(&WS2812_TIM, WS2812_TIM_Channel);
			//我们实际上传输24bit作为一位,但是我们这里传输25是作为两个数据之间的间隔
            HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, WS2812_SendBuf0, 25);
			//这里我们将数据发送给WS2812_SendBuf1
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf1);
            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_SendBuf1
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf0);
            ins = 0;
        }
    }
}
bug2:由于熄灭点亮太快,人眼,难以观察

添加一个延时函数【但是这个延时函数也要注意和呼吸灯等函数内部的延时是否匹配的问题】--->要不然会出现进入中断,然后调用呼吸灯函数,但是你呼吸灯函数中延时的时间过长,导致已经开始进入下一次中断的时候,你程序还在呼吸灯函数中进行延时,从而导致你灯来不及亮就进入下一次中断--->这个错误很难发现,我们后面有具体的例子可以讲讲。

8.抽取一个点亮(熄灭)灯的函数
cpp 复制代码
//灯的点亮(熄灭)
void light_mode_music_process(){
		
	static uint32_t color=0xff0000;
	static uint8_t history_light_count=0;
	
	//我们这里是为了判断下一次进入
	//点亮个数是否增加【即表示是否声音变大】
	if(history_light_count<g_led_count){
		//此时点亮个数不够
		history_light_count=g_led_count;
	}
	//清空原来的灯
	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;
	}
	//表示下一次进入中断的时候就会少亮一颗灯
	history_light_count--;
	WS2812_Start();
	
}
bug1:当我们按照上面的代码编写后,并没有正常的熄灭

我们对其进行调试

发现灯并没有正常熄灭,说明问题应该是出现在【全局变量值没有改变,所以才会导致每一次进入的时候,都是在使用之前设置的数值】上

bug2:最后一颗灯不会熄灭

如果放在点亮led的for循环后,则会导致最后history_light_count--的时候,已经变为0,但是实际上最后一个并没有熄灭就退出循环

bug3:最上面的那个灯不会亮
cpp 复制代码
void test_music_on(){
	static uint32_t color=0x0000ff;
	static uint8_t history_light_count=0;
	
	//我们这里是为了判断下一次进入
	//点亮个数是否增加【即表示是否声音变大】
	if(history_light_count<g_count){
		//此时点亮个数不够
		history_light_count=g_count;
		//记得将全局变量清空
		//要不然灯不会熄灭在点亮
		g_count=0;
	}
	//清空原来的灯
	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();
    HAL_Delay(500);
}
9.测试突然接收到一个很大的声音
cpp 复制代码
  while (1)
  {
			color_num_set(10);
			HAL_Delay(20);
			color_num_set(20);
			HAL_Delay(20);
  }

灯可以点亮到第20颗

3.完整代码

WS2812.c
cpp 复制代码
#include "WS2812.h"
#include "stdlib.h"
uint32_t WS2812_Data[WS2812_Num] = {0};

uint32_t WS2812_SendBuf0[25] = {0};   //发送缓冲区0
uint32_t WS2812_SendBuf1[25] = {0};   //发送缓冲区1
const uint32_t WS2812_Rst[240] = {0}; //复位码缓冲区
uint32_t WS2812_En = 0;               //发送使能

//因为这个用户设置的数值要在多个函数中使用,所以设置为全局变量
uint32_t g_count=0;
/**
 * @brief 将uint32转为发送的数据
 * @param Data:颜色数据   0x ff 00 00
 * @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【最低8位】  00
    G = *(p + 1); // G【次高8位】  00
    R = *(p + 2); // R【最高8位】  ff
    zj = (G << 16) | (R << 8) | B;
    for (int i = 0; i < 24; i++)
    {
			/**
			#define WS2812_Code_0 (32u)
			#define WS2812_Code_1 (71u)
			*/
			// if (zj & (1 << (23-i))) 如果这样设置zj就不需要移位
        if (zj & (1 << 23)){//判断此位与1位于(&)结果是否为1,如果为1表示此时是传输1
            Ret[i] = WS2812_Code_1;//  71/105
				}
        else{//表示此时传输0
            Ret[i] = WS2812_Code_0;//  32/105
				}
        zj <<= 1;//因为我们都是判断最高位,所以每一次判断完后,都要将整体数据左移
    }
		//两个数值之间的间隔
    Ret[24] = 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;
			//在DMA模式下停止TIM PWM信号的产生
            HAL_TIM_PWM_Stop_DMA(&WS2812_TIM, WS2812_TIM_Channel);
            WS2812_En = 0;//失能
            return;
        }
        j++;//表示数据还未传输结束
        if (ins == 0)//我们使用两个数组来存放解码后【转换为PWM】的数据
        {
			HAL_TIM_PWM_Stop_DMA(&WS2812_TIM, WS2812_TIM_Channel);
			//我们实际上传输24bit作为一位,但是我们这里传输25是作为两个数据之间的间隔
            HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, WS2812_SendBuf0, 25);
			//这里我们将数据发送给WS2812_SendBuf1
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf1);
            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_SendBuf1
            WS2812_uint32ToData(WS2812_Data[j], WS2812_SendBuf0);
            ins = 0;
        }
    }
}
/**
 * @brief 开始发送颜色数据
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-10-03 18:05:13
 */
void WS2812_Start(void)
	{		
    //给WS2812发送一个RESET信号
     HAL_TIM_PWM_Start_DMA(&WS2812_TIM, WS2812_TIM_Channel, (uint32_t *)WS2812_Rst, 240);	
	//RGB--->TIM用于生成特定定时器
	//经过上面的开启PWM和DMA,从而进入【HAL_TIM_PWM_PulseFinishedCallback】--->从而调用发送数据
    WS2812_uint32ToData(WS2812_Data[0], WS2812_SendBuf0);		//将第一个数据转换为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;//失能
}

//定时器+PWM中断回调函数【在TIM中】
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
  WS2812_Send();
}

//呼吸灯
void color_breath(){
		static uint32_t color = 0x000000;	
		if (0x00ff00 == color)
		{
			color = 0;
		}
		for (int i = 0; i < WS2812_Num; i++)
		{
			WS2812_Data[i] = 0x000000;
		}
		color += 0x001100;
		for (int i = 0; i < WS2812_Num; i++)
		{
			WS2812_Data[i] = color;
		}
		WS2812_Start();	
		HAL_Delay(50);
		
}
//跑马灯
void color_run(){
	
	static uint32_t first=0,second=0,third=0,flag=0;
		uint32_t color_red=0x550000;//红色
		uint32_t color_green=0x005500;//绿色
		uint32_t color_bule=0x000055;//蓝色
	if(3==flag){
		flag=0;
	}
	if(0==flag){
		first=color_red;
		second=color_green;
		third=color_bule;
	}else if(1==flag){
		first=color_bule;
		second=color_red;
		third=color_green;
	}else{
		first=color_green;
		second=color_bule;
		third=color_red;
	}
	flag++;
	for(int i=0;i<65;i+=3){
		WS2812_Data[i]=first;
		WS2812_Data[i+1]=second;
		WS2812_Data[i+2]=third;
	}
	WS2812_Start();
	HAL_Delay(200);
}


//点亮设置的灯数
void test_light_set(uint32_t t_count){
	g_count=t_count;
}

void test_music_on(){
	static uint32_t color=0x0000ff;
	static uint8_t history_light_count=0;
	
	//我们这里是为了判断下一次进入
	//点亮个数是否增加【即表示是否声音变大】
	if(history_light_count<g_count){
		//此时点亮个数不够
		history_light_count=g_count;
		//记得将全局变量清空
		//要不然灯不会熄灭在点亮
		g_count=0;
	}
	//清空原来的灯
	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();
    HAL_Delay(500);
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	static	uint32_t time_out =0;
	time_out++;
	if(time_out>50) {
		test_music_on();
		//别忘记重新赋值
		time_out=0;
	}
}
WS2812.h
cpp 复制代码
#ifndef __WS2182__H__
#define __WS2182__H__
#include "main.h"
/*
硬件定时器PWM+DMA:
需要:
    1.定时器:
        PWM输出一个通道
        不分频
        计数值为 1.25us(公式: 1.25 *系统频率(单位MHz))
        输出IO设置为开漏浮空输出(外接5V上拉)
    2.DMA
        内存到外设
        字(word)模式
        开启DMA中断
0码的是 0.4us,1码是0.8us
公式是 t(us)*系统频率(单位MHz)
 */
extern TIM_HandleTypeDef htim1;
#define WS2812_Hardware
#define WS2812_TIM htim1
#define WS2812_TIM_Channel TIM_CHANNEL_1
#define WS2812_Code_0 (32u)
#define WS2812_Code_1 (71u)

extern uint32_t g_count;
extern uint32_t *RGB;//设置彩色数组

//注意点:这里一定要设置与原有的灯数量一致,要不然会显示异常
#define WS2812_Num 60

extern uint32_t WS2812_Data[WS2812_Num];
void WS2812_Code_Reast(void);
void WS2812_Send(void);

void WS2812_Start(void);

//呼吸灯
void color_breath();

//跑马灯
void color_run();

//点亮设置的灯数
void test_light_set(uint32_t t_count);


#endif

六、蓝牙模块:HC-04

1.基本介绍

使用的是汇承HC-04蓝牙模块,网址:广州汇承信息科技有限公司 (hc01.com)

参考博客:hc04模块使用手册_hc04蓝牙模块_zhengyad123的博客-CSDN博客

2.结合串口使用

本项目中需要用户使用手机连接蓝牙来对氛围灯的模式控制,所以需要使用到蓝牙,**那我们单片机接收数据需要使用到串口,则我们需要使能串口模块,记得开启串口的中断。**我们使用的串口接收数据的TI,如果使用阻塞式,则会很浪费CPU的空间。

STM32CUBEMX设置串口波特率:

3.蓝牙的基本使用方式

由上图可以知道我们MCU使用的波特率是【115200】,则如果蓝牙要与其通信则两者的波特率一定要一致,要不然无法进行通信。

蓝牙模块的初始化参考:广州汇承信息科技有限公司 (hc01.com)

该蓝牙在出厂时默认的波特率为:9600,如果想要与我们上面设置的波特率对应则一定要先修改

查看当前蓝牙的波特率:AT+BAUD=?

修改波特率指令:AT+BAUD=115200,N【记得是英文状态下的】

当我们修改完波特率后,一定要先断开串口,然后将波特率修改为我们修改后的波特率才可以进行后面的操作!!!!!!

4.蓝牙与手机的通信

先去下载官方的蓝牙连接串口软件:广州汇承信息科技有限公司 (hc01.com)

记得打开手机的蓝牙

接线:RX--->TX,TX---->RX

蓝牙模块

串口通信模块

当我们手机与蓝牙连接成功后,板载上的灯会常亮。

5.电脑和蓝牙通信

接线:通过杜邦线将蓝牙模块和单片机连接起来,单片机在通过杜邦线与电脑连接起来

接线:RX--->TX,TX---->RX

测试代码

注意点:

我们要先将手机和蓝牙模块连接上,然后再将代码烧录到单片机中,然后再将单片机的复位按钮,才可以再手机上看到

cpp 复制代码
		printf("holle this is mcu\r\n");

6.代码编写

蓝牙模块分析:

1)用户使用手机连接蓝牙模块,在手机端输入"A","M"等指令可以进行控制

2)蓝牙模块的使用:要使用到串口,所以我们在CubeMX中要使用IT中断

3)因为我们不能使用阻塞式(浪费CPU资源),所以记得是IT,而且我们MCU是接收用户的输入,所以使用的是接收中断【HAL_UART_Receive_IT】

4)接收中断是使用【HAL_UART_TxCpltCallback】,所以我们要在这里面进行指令判断

5)我们还使用到一个标志位【music_mode_en】,判断如果是音乐律动灯,则就进入mic的判断【mian函数中的部分】

6)所以当用户进入了【LIGHT_MODE_MUSIC】则使能,如果不是则不使能

bluetooth.c

cpp 复制代码
#include "bluetooth.h"



extern uint8_t rx_buffer[2];
extern light_mode_type mode;
extern UART_HandleTypeDef huart1;
extern uint8_t music_mode_en;
//当进入这个中断回调函数的时候i,表示已经接到用户发送过来的指令
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
	
	//接收到,就开始判断
	//rx_buffer:表示接收到的数据的存放地址
	
	
	if(rx_buffer[0]=='B'){
		light_mode_select(LIGHT_MODE_BREATH);//呼吸灯
		music_mode_en=0;
	}else if(rx_buffer[0]=='A'){
		light_mode_select(LIGHT_MODE_MARQUEE);//跑马灯
		music_mode_en=0;
	}else if(rx_buffer[0]=='M'){
		light_mode_select(LIGHT_MODE_MUSIC);//跑马灯
		//使能为氛围灯模式,使得在mian函数中,可以获取到mic的值
		music_mode_en=1;
	}else{
		//用户输入不正确的字母
		printf("您输入有误,请重新输入('A,'M','B')");
	}
	
	//因为我们要不断的判断用户是否输入数据,所以我们需要在这个函数中接着使能中断
	//要不然用户下一次输入,会接收不到
	//当我们使用到这个函数的时候,就会进入中断回调函数
	HAL_UART_Receive_IT(&huart1,rx_buffer,1);
}

bluetooth.h

cpp 复制代码
#ifndef __BLUETOOTH__H__
#define __BLUETOOTH__H__

#include "main.h"
#include "stdint.h"
#include "WS2812.h"

#endif

main.c

cpp 复制代码
//定义麦克风接收过来的等级
static uint32_t test_grade=0,max_grade=0;
//定义一个数组用于接收串口发送来的数据【手机蓝牙】
//实际上用户只需要传输"M","A"等一个字母,但是我们为了防止溢出我们使用数组填充为2个
uint8_t rx_buffer[2]={0};
//判断用户是否想要进入氛围灯还是跑马灯【呼吸灯】
//如果进入氛围灯则使能,如果不是则不使能
uint8_t music_mode_en=0;


int main(void)
{
  HAL_Init();

  SystemClock_Config();


  MX_GPIO_Init();
  MX_DMA_Init();
  MX_TIM1_Init();
  MX_TIM2_Init();
  MX_USART1_UART_Init();
  MX_ADC1_Init();
		//使能定时器中断
		HAL_TIM_Base_Start_IT(&htim2);

		//使能串口中断【接收的中断】
		//rx_buffer:表示接收到的数据的存放地址
		//当我们使用到这个函数的时候,就会进入中断回调函数
		HAL_UART_Receive_IT(&huart1,rx_buffer,1);
		
//		light_mode_select(LIGHT_MODE_MUSIC);
		HAL_Delay(1000);
	
  while (1)
  {		
		if(music_mode_en){//表示用户此时想要进入氛围灯模式
			for(int i=0;i<5;i++){
				test_grade=	mic_get_grade();
				//将获取得到的最大值分给led进行显示
				if(test_grade>max_grade){
					max_grade=test_grade;
				}
			}
			if(test_grade>0){
				test_light_set(max_grade);
				HAL_Delay(10);
				max_grade=0;
			}
		}
		
		
  }
}

七、整合

1.选择模式

1.呼吸灯

因为我们将自动刷新交给了定时器,所以我们直接在中断回调函数中调用呼吸灯

此时我们查看效果,发现灯并没有亮,说明【可能是因为还没有来得及亮就灭了】---->对应我们前面讲到,当我们第一次进入中断,然后进入light_mode_breath,当我们还在light_mode_breath里面的时候,就开始了第二次中断,我们还未来得及出去就已经开始第二轮。

故我们将呼吸灯函数中的延时去除,查看结果确实可以实现呼吸效果,但是呼吸速度太快。

方法一:我们可以考虑在呼吸灯函数中,添加for循环,比如我们在中断函数中设置80ms进入一次设置一次呼吸灯,然后我们在呼吸灯中设置循环4次,在真正执行呼吸效果,则实际上是80*4=320ms执行一次呼吸

方法二:我们让颜色亮度升高的慢一点

cpp 复制代码
//呼吸灯
/**
慢慢变亮又慢慢地变暗
15张胶片(RGB)
  胶片1:0x110000 //很暗的红色
  胶片2:0x220000 
  胶片3:0x330000 
  胶片4:0x440000 
  胶片5:0x550000 
  胶片6:0x660000 

胶片15:0xFF0000 
*/
void light_mode_breath(void){
			static uint32_t color;
			if(0x00ff00==color){
				color=0;
			}
			color+=0x000500;
		  for (int i = 0; i < 8; i++)//			00   ff   00
				WS2812_Data[i] = color; //    R    G    B
			WS2812_Start();
		//	HAL_Delay(1000);	
}

2.跑马灯

与上面的呼吸灯一样,需要将跑马灯函数中的延时删除,才可以出现效果。

如果你的效果还是跑得太快,可以想上面一样,添加一个time,然后控制time的数值,进行设置进入的次数

cpp 复制代码
//跑马灯
/**
RGBRGBRGBRGBRGBRGBRGBRGBRGBRGB
GBRGBRGBRGBRGBRGBRGBRGBRGBRGBR
BRGBRGBRGBRGBRGBRGBRGBRGBRGBRG
RGBRGBRGBRGBRGBRGBRGBRGBRGBRGB
*/
void light_mode_amquee(void){
	//加上static是为了可以在整个程序中运行
		//flag:标志第几次进来
		static uint32_t first=0,second=0,third=0,flag=0;
		if(flag==3){
			flag=0;
		}
		else if(flag==0){
			first=0x550000;
			second=0x005500;
			third=0x0055ff;
		}
		else if(flag==1){
			first=0x0055ff;
			second=0x550000;
			third=0x005500;
		}
		else{
			first=0x005500;
			second=0x0055ff;
			third=0x550000;
		}
		flag++;
		for(int i=0;i<WS2812_Num;i+=3){
			WS2812_Data[i+0]=first;
			WS2812_Data[i+1]=second;
			WS2812_Data[i+2]=third;
		}
					WS2812_Start();
				//	HAL_Delay(100);	
}

2.用户选择

1.设置一个枚举

cpp 复制代码
//设置一个枚举,提供给用户选择
//注意点:设置枚举,记得给第一个变量赋值,防止他是一个未知的值
typedef enum{
	LIGHT_MODE_BREATH=0,
	LIGHT_MODE_MARQUEE,
	LIGHT_MODE_MUSIC,
}lgiht_mode_type;

2.添加一个函数提供给用户选择

cpp 复制代码
//用户设置模式
void light_mode_select(lgiht_mode_type mode);

3.函数编写

cpp 复制代码
//用户选择模式:默认为呼吸灯
lgiht_mode_type g_mode=0;

//用户设置模式
void light_mode_select(lgiht_mode_type mode){
	g_mode=mode;
}

4.在中断回调中调用

cpp 复制代码
//使能定时器中断回调函数,当定时器2溢出时,会进入这个函数
//去更新灯亮的个数【点亮/熄灭】
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	static uint8_t time_count=0;
	time_count++;
	if(time_count>=80){//延时
		//light_mode_music_off_on_light();
		//light_mode_breath();
		//light_mode_amquee();
		switch(g_mode){
			case LIGHT_MODE_BREATH:
				light_mode_breath();
				break;
			case LIGHT_MODE_MARQUEE:
				light_mode_amquee();
				break;
			case LIGHT_MODE_MUSIC:
				light_mode_music_off_on_light();
				break;
		}
		time_count=0;
	}
}

5.测试:呼吸灯和跑马灯

cpp 复制代码
  while (1)
  { 
		
		/**测试用户选择模式*/
		
		light_mode_select(LIGHT_MODE_BREATH);
		HAL_Delay(2000);
		light_mode_select(LIGHT_MODE_MARQUEE);
		HAL_Delay(2000);
}

6.测试:律动灯

注意点:

律动灯只能在while外面进行设置

cpp 复制代码
	//开启定时器2,当溢出时,会改变灯亮的个数
	HAL_TIM_Base_Start_IT(&htim2);
	printf("hello this mcu");
	light_mode_select(LIGHT_MODE_MUSIC);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  { 
		/**测试用户选择模式(律动灯)
		
		*/
		for(int i=0;i<10;i++){
			grade=mic_get_grade();
			if(grade>highest_grade){
				highest_grade=grade;
			}
		}
		light_mode_music_set(highest_grade);
		highest_grade=0;
		HAL_Delay(20);	
  }

3.使用串口中断结合手机蓝牙

用户在手机端输入"B"表示呼吸灯,"A"表示跑马灯,"M"表示音乐律动灯

1.开启中断

因为我们不知道用户什么时候要进行操作,使用我们要将串口开启中断(TI),才可以接收到用户发送来的消息,并且使用TI可以减少CPU的浪费

2.使能中断

3.编写中断回调函数

4.添加使能位

因为我们在mian中调用mic模块,通过mic来控制亮的个数,但是我们只需要在律动灯的时候在调用,所以我们使用一个使能位,但是这个使能位被标识的时候我们才去调用这个音乐检测。

由上面代码测试可得,我们只能输入一次,当用户输入第二次就无法进行设置。

5.问题解决

通过分析我们可以知道在主函数中调用了一次之后【HAL_UART_Receive_IT(&huart1,rx_buffer,1);】,就无法调用是因为设置只能一个中断接收用户输入,所以我们需要在中断回调函数中再一次调用【HAL_UART_Receive_IT(&huart1,rx_buffer,1);】才可以反反复复的接收

cpp 复制代码
#include "blue.h"

 uint8_t rx_buffer[2];
  uint8_t music_mode_en;

//编写中断回调函数,使得用户输入对应字母可以进行响应的操作
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
	if(rx_buffer[0]=='B'){
		light_mode_select(LIGHT_MODE_BREATH);
		music_mode_en=0;
	}else if(rx_buffer[0]=='A'){
		light_mode_select(LIGHT_MODE_MARQUEE);
		music_mode_en=0;
	}else if(rx_buffer[0]=='M'){
		light_mode_select(LIGHT_MODE_MUSIC);
		music_mode_en=1;
	}else{
		printf("您输入有误");
	}
	HAL_UART_Receive_IT(&huart1,rx_buffer,1);
}

由此我们才解决了问题

本项目完整代码:

code/new_complment/5.music_ · 林何/stm32 bluetooth ambient light - 码云 - 开源中国 (gitee.com)

相关推荐
小齿轮lsl23 分钟前
半波整流器原理
单片机·嵌入式硬件·学习·电力电子·电源·电源硬件
whaosoft-14326 分钟前
51c嵌入式~单片机合集2
单片机·嵌入式硬件
K1802539818730 分钟前
PHY6235超低功耗蓝牙和专有2.4G应用的SOC芯片内置MCU
单片机·嵌入式硬件
杨sir~2 小时前
硬件---4电感---基本概念与特性
嵌入式硬件
电子工程师UP学堂2 小时前
STM32+AI语音识别智能家居系统
嵌入式硬件
艾格北峰2 小时前
STM32 BootLoader 刷新项目 (九) 跳转指定地址-命令0x55
arm开发·stm32·单片机·嵌入式硬件
(●'◡'●)知2 小时前
在 STM32 使用 FreeRTOS 时如何重定位向量表实现 Bootloader 跳转
stm32·单片机·嵌入式硬件
LightningJie3 小时前
STM32(hal库)在串口中,USART和uart有什么区别?
stm32·单片机·嵌入式硬件
非概念3 小时前
STM32学习笔记------GPIO介绍
笔记·stm32·嵌入式硬件·学习
浮梦终焉3 小时前
单片机工程使用链接优化-flto找不到定义_链接静态库
单片机·链接·c/c++·cmakelists