【STM32】蓝牙氛围灯

news2024/11/26 0:49:22

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.添加自定义延时函数

#include "stdint.h"

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

验证延时函数是否正确:

1)使用debugger

2)使用其他测试工具

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

2.测试相关的代码

/**
  * @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.测试

  while (1)
  {
		ws281x_sendLow();
  }

4.问题解决

最后还是使用__nop__

//自定义延时函数
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信号

  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

/**
 * @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位是在数据之间的间隔

/**
 * @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

/**
 * @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】--->从而调用发送数据
}
发送复位函数

发送失能位

/**
 * @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


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

3.完整代码

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

WS2812.c
#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();
}
#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
#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
#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
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)整除,如果可以才正常显示,要不然可能会出现无法正确显示的现象。

//呼吸灯
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)

//跑马灯
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. 添加串口重定向代码

#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的使用

  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值,然后求其平均值可以使得获得的值更加准确(起到滤波的作用)

//获取将声音划分为等级,然后通过亮几颗灯来反映
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值,先查看获取的是否正确

		//测试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.设置一个函数可以设置点亮个数
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是不会被点亮的。

	static uint8_t count=10;

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

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

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

  /* USER CODE END 2 */

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

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

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

将上面的函数进行了修改

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

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

思路:

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

2)先将其原来的清空

3)在点亮

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

//定时器中断处理函数
//当到时间就进入此函数
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【以后使用的时候也是最好配对使用】

/**
 * @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.抽取一个点亮(熄灭)灯的函数
//灯的点亮(熄灭)
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:最上面的那个灯不会亮

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.测试突然接收到一个很大的声音
  while (1)
  {
			color_num_set(10);
			HAL_Delay(20);
			color_num_set(20);
			HAL_Delay(20);
  }

灯可以点亮到第20颗

3.完整代码

WS2812.c
#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
#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

测试代码

注意点:

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

		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

#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

#ifndef __BLUETOOTH__H__
#define __BLUETOOTH__H__

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

#endif

 main.c

//定义麦克风接收过来的等级
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执行一次呼吸

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

//呼吸灯
/**
慢慢变亮又慢慢地变暗
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的数值,进行设置进入的次数

 

//跑马灯
/**
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.设置一个枚举

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

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

//用户设置模式
void light_mode_select(lgiht_mode_type mode);

3.函数编写

//用户选择模式:默认为呼吸灯
lgiht_mode_type g_mode=0;

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

4.在中断回调中调用

//使能定时器中断回调函数,当定时器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.测试:呼吸灯和跑马灯

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

6.测试:律动灯

注意点:

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

	//开启定时器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);】才可以反反复复的接收

#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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1296627.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Advanced Renamer

Advanced Renamer 安装链接 1.前后添加字符 2.字符转数字&#xff0c;编号整体加减

混合预编码(Hybrid Precoding)的全连接结构与子连接结构

A Survey on Hybrid Beamforming Techniques in 5G: Architecture and System Model Perspectives 全连接结构的混合预编码 子连接结构的混合预编码 Alternating Minimization Algorithms for HybridPrecoding in Millimeter Wave MIMO Systems

Rust测试字符串的移动,Move

代码创建了一个结构体&#xff0c;结构体有test1 字符串&#xff0c;还有指向字符串的指针。一共创建了两个。 然后我们使用swap 函数 交换两个结构体内存的内容。 最后如上图。相同的地址&#xff0c;变成了另外结构体的内容。注意看指针部分&#xff0c;还是指向原来的地址…

HttpComponents: 概述

文章目录 1. 概述2. 生态位 1. 概述 早期的Java想要实现HTTP客户端需要借助URL/URLConnection或者自己手动从Socket开始编码&#xff0c;需要处理大量HTTP协议的具体细节&#xff0c;不但繁琐还容易出错。 Apache Commons HttpClient的诞生就是为了解决这个问题&#xff0c;它…

【C++】仿函数在模板中的应用——【默认模板实参】详解(n)

前言 大家好吖&#xff0c;欢迎来到 YY 滴C系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 目录 一.引入&#xff1a;查看(容器)文档时常…

UE4 材质实现Glitch效果

材质实现Glitch效果 UE4 材质实现Glitch效果预览1预览2 UE4 材质实现Glitch效果 预览1 添加材质函数&#xff1a; MF_RandomNoise 添加材质&#xff1a; 预览2 添加材质函数MF_CustomPanner&#xff1a; 添加材质函数&#xff1a;MF_Glitch 材质添加&#xff1a; 下面用…

免费的网页数据抓取工具有哪些?【2024附下载链接】

在网络上&#xff0c;有许多网页数据抓取工具可供选择。本文将探讨其如何全网采集数据并支持指定网站抓取。我们将比较不同的数据采集工具&#xff0c;帮助您找到最适合您需求的工具。 网页数据抓取工具种类 在选择网页数据抓取工具之前&#xff0c;让我们先了解一下这些工具…

基于单片机音乐盒仿真仿真系统设计

**单片机设计介绍&#xff0c;基于单片机音乐盒仿真仿真系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的音乐盒仿真仿真系统是一种基于嵌入式系统技术的设计方案&#xff0c;用于模拟传统的音乐盒功能。它通…

pyside/qt03——人机协同的编程教学—直接面向chatGPT实战开发(做中学,事上练)

先大概有个草图框架&#xff0c;一点点丰富 我纠结好久&#xff0c;直接用Python写UI代码 还是用designer做UI 再转Python呢&#xff0c; 因为不管怎么样都要转成Python代码&#xff0c; 想了想还是学一下designer吧&#xff0c;有个中介&#xff0c;有直观理解。 直接这样也可…

RHEL网络服务器

目录 1.时间同步的重要性 2.配置时间服务器 &#xff08;1&#xff09;指定所使用的上层时间服务器。 (2&#xff09;指定允许访问的客户端 (3&#xff09;把local stratum 前的注释符#去掉。 3.配置chrony客户端 &#xff08;1&#xff09;修改pool那行,指定要从哪台时间…

nodejs+vue+微信小程序+python+PHP的游戏测评网站设计与实现-计算机毕业设计推荐

通过软件的需求分析已经获得了系统的基本功能需求&#xff0c;根据需求&#xff0c;将游戏测评网站功能模块主要分为管理员模块。管理员添加个人中心、管理员管理、基础数据管理、公告管理、用户管理、游戏管理、游戏测评管理、游戏攻略管理、轮播图信息等操作。  随着时代的…

Kafka在微服务架构中的应用:实现高效通信与数据流动

微服务架构的兴起带来了分布式系统的复杂性&#xff0c;而Kafka作为一款强大的分布式消息系统&#xff0c;为微服务之间的通信和数据流动提供了理想的解决方案。本文将深入探讨Kafka在微服务架构中的应用&#xff0c;并通过丰富的示例代码&#xff0c;帮助大家更全面地理解和应…

Windows 12 和 AI 计算机

据商业时报消息 &#xff0c;微软计划于 2024 年 6 月发布Windows 12。 新版本的操作系统将伴随集成人工智能。 该数据基于广达首席执行官林百里和宏基陈杰森在中国台北医疗科技展上的发言。 虽然这篇文章没有直接引用微软高管的话&#xff0c;但它是根据他们的评论得出的结…

Android View.inflate 和 LayoutInflater.from(this).inflate 的区别

前言 两个都是布局加载器&#xff0c;而View.inflate是对 LayoutInflater.from(context).inflate的封装&#xff0c;功能相同&#xff0c;案例使用了dataBinding。 View.inflate(context, layoutResId, root) LayoutInflater.from(context).inflate(layoutResId, root, fals…

人工智能原理复习--搜索策略(一)

文章目录 上一篇搜索概述一般图搜索盲目搜索下一篇 上一篇 人工智能原理复习–确定性推理 搜索概述 问题求解分为两大类&#xff1a;知识贫乏系统&#xff08;依靠搜索技术解决&#xff09;、知识丰富系统&#xff08;依靠推理技术&#xff09; 两大类搜索技术&#xff1a; …

实验3.5 路由器的单臂路由配置

实验3.5 路由器的单臂路由配置 一、任务描述二、任务分析三、具体要求四、实验拓扑五、任务实施1.SWA的基本配置2.RA的基本配置3.在RA上查看接口状态 六、任务验收七、任务小结 一、任务描述 某公司对部门划分了需VLAN之后&#xff0c;发现两个部门之间无法通信&#xff0c;但…

深入理解 Promise:前端异步编程的核心概念

深入理解 Promise&#xff1a;前端异步编程的核心概念 本文将帮助您深入理解 Promise&#xff0c;这是前端异步编程的核心概念。通过详细介绍 Promise 的工作原理、常见用法和实际示例&#xff0c;您将学会如何优雅地处理异步操作&#xff0c;并解决回调地狱问题。 异步编程和…

从阻抗匹配看拥塞控制

先来理解阻抗匹配&#xff0c;但我不按传统方式解释&#xff0c;因为传统方案你要先理解如何定义阻抗&#xff0c;然后再学习什么是输入阻抗和输出阻抗&#xff0c;最后再看如何让它们匹配&#xff0c;而让它们匹配的目标仅仅是信号不反射&#xff0c;以最大能效被负载接收。 …

【二分查找】LeetCode:2354.优质数对的数目

作者推荐 贪心算法LeetCode2071:你可以安排的最多任务数目 本文涉及的基础知识点 二分查找算法合集 题目 给你一个下标从 0 开始的正整数数组 nums 和一个正整数 k 。 如果满足下述条件&#xff0c;则数对 (num1, num2) 是 优质数对 &#xff1a; num1 和 num2 都 在数组 …

excel数据重复率怎么计算【保姆教程】

大家好&#xff0c;今天来聊聊excel数据重复率怎么计算&#xff0c;希望能给大家提供一点参考。 以下是针对论文重复率高的情况&#xff0c;提供一些修改建议和技巧&#xff1a; excel数据重复率怎么计算 在Excel中计算数据重复率可以通过以下步骤实现&#xff1a; 1. 确定重复…