目录
一、原理简述
二、系统硬件设计
1.stm32f103c8t6核心控制器
2.无线蓝牙模块
3.LM386音频放大模块
4.PWM水泵控制模块
三、系统软件设计
1.ADC初始化及使用
2.PWM初始化及使用
3.ADC检测与PWM反馈
四、实物展示
五、完整原理图
六、完整代码
所谓音乐喷泉就是喷泉水柱会随着音乐节奏的快慢或者声音的高低而起伏变化,要实现这个变化,从技术的角度上来说需要解决如下两个问题:
①如何感知音乐节奏的快慢或者声音的高低?
②获取到音乐的变化后,如何变化成水柱的变化?
事实上解决了上述两个问题,本设计就完成了一大半。按照常识,可以使用水泵来控制水柱的高低,音频可以使用模数转换将音乐这一连续变化的模拟量变成可以量化、方便处理的数字量,再将这个变化反馈给水泵,让水泵随着音频的变化而变化,这样就完成了本设计。
本设计,将以stm32为核心,设计一个音乐喷泉,使用蓝牙模块与手机相连后放大相应的音频,并将音乐音调高低的变化转换成水泵水柱喷射高低的变化。具体表现为:手机通过蓝牙,将音频信号传输至音频放大器,音频放大器将音频处理后,通过喇叭输出,同时将这个处理之后的信号送至stm32自带ADC模块采集,stm32将以此为依据,控制PWM输出,从而达到控制水泵水柱高度的目的。
本系统中,将以stm32为核心,配合LM386音频放大模块、无线蓝牙模块、PWM水泵控制模块,共同实现本设计所有功能设计。
系统的整体硬件设计框图如下:
图一 整体硬件框图
下面,将分别介绍这四个核心模块:
stm32f103c8t6的实物图如下:
图二 stm32最小系统板
图三 stm32最小系统原理图
stm32f103c8t6为意法半导体生产的一款高性能32位处理器,采用ARM cortex-M3为内核,在稳定运行的情况下,主频可高达72M,是传统51单片机的性能的几十倍,能完成许多复杂的功能。其最小系统主要包括:stm32芯片、复位电路、时钟电路、电源电路、代码烧录电路和boot选择电路。
stm32f103c8t6用着丰富的外设,例如GPIO、USART、ADC、PWM、TIMER、硬件SPI、硬件IIC、USB等。在本设计中,将会使用到的它的外设有:GPIO、ADC、TIMER和PWM。其中,ADC负责采集音频音调的高低,然后反馈给PWM,控制PWM占空比的高低,从而达到控制水柱高低的效果。
无线蓝牙模块实物图如下:
图四 无线蓝牙模块
该蓝牙模块输出音频可以达到双声道立体效果,且无线距离最远可达到20m,配对后可自动会连手机,且支持低功耗,且成本低廉。在本设计中,只需要用音频线,连接蓝牙模块的PHONE口和LM386模块的PHONE,打开手机音频就可以达到音频无线传输的效果,操作简单,易于控制,方便快捷。
LM386是专为低损耗电源所设计的功率放大器集成电路。其内置增益为20,透过pin 1 和pin8脚位间电容的搭配,增益最高可达200。LM386可使用电池为供应电源,输入电压范围可由4V~12V,无作动时仅消耗4mA电流,且失真低。其应用原理图如下:
图五 LM386应用电路
上图应用电路中,LM386的增益为200。将音频线一端连接蓝牙模块,一端连接图四中的PHONE中,这样,手机传输过来的音频就可以通过蓝牙,传输至LM386,经过LM386的放大之后,经过JP1上插着的喇叭播放。图中,RP1为电位器,可以通过旋转电位器,改变输入电阻,从而调节JP1喇叭的音量大小输出;E2为一个电解电容,起着“通交流、隔直流”的作用,可以滤除掉电路总的直流成分,这样,输出到JP1的声音就比较完整,且杂音相对小很多。同时,在VOUT引脚,连接着stm32的PB0脚,该引脚为stm32自带adc的输入引脚,起着采集音量大小的作用。
当stm32通过自带adc采集到音频大小后,需要反馈给水泵,控制水柱输出。这里的水泵,就是通过PWM控制输出功率高低的,其原理图如下:
图六 PWM水泵控制原理图
水泵通过两个三极管来驱动。其前级SS8050,为一个NPN三极管,它的基级连着stm32的PB5,后级是SS8550,为一个PNP管。当stm32的PB5输出一个高电平时,SS8050导通,此时,SS8550的基级电压被拉低,8550导通,JP2上面的水泵工作。同理,PB5输出低电平时,8050截止,8550的基级为高电平,8550也是截止的,水泵不工作。当PB5输出PWM时,水泵的输出功率就会随着PB5输出PWM的占空比的高低而高低变化,从而达到控制水柱高低的效果。
本设计中,软件设计流程明晰,具体系统的软件设计流程图七所示:
图七 软件设计流程图
上图中大致可以将软件代码分为三个部分:ADC初始化、PWM初始化、ADC检测及PWM的反馈。
stm32的ADC是12位ADC,stm32f103c8t6一共有三个ADC,每个ADC最多有18个通道,其最大转换速率可达到1Mhz。本设计中使用的ADC1的通道八进行音频音调高低信号的采集。在初始化任何外设之前,都需要先初始化其对应的外设时钟,然后再设置对应的ADC通道、设置ADC采样速率转换方式等。其具体代码如下:
void Adc_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB |RCC_APB2Periph_ADC1 , ENABLE ); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
//PB0 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOB, &GPIO_InitStructure);
ADC_DeInit(ADC1); //复位ADC1
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
}
初始化完成之后,在后续的代码中就可以使用PB0开始测量工作了。为了避免信号的干扰,使测量的数据更加准确,这里采用多次测量取平均值的方式。具体代码如下:
u16 Get_Adc(u8 ch)
{
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}
u16 Get_Adc_Average(u8 ch,u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t
PWM,译为脉冲宽度调制,简单说就是对脉冲宽度的控制。stm32有八个定时器,分为基本定时器、通用定时器和高级定时器。除了基本定时器,其他定时器都可以用来产生PWM,而TIM1和TIM8这两个高级定时器,可以同时产生7路PWM输出。本设计中使用的TIM3的通道2来产生PWM,对应stm32的PB5引脚。
首先是PWM的初始化,其具体代码如下:
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void TIM3_PWM_Init(u16 arr,u16 psc,u16 TIM_Pulse)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能定时器3时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); //使能GPIO外设和AFIO复用功能模块时钟
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); //Timer3部分重映射 TIM3_CH2->PB5
//设置该引脚为复用输出功能,输出TIM3 CH2的PWM脉冲波形 GPIOB.5
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //TIM_CH2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO
//初始化TIM3
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
//初始化TIM3 Channel2 PWM模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性:TIM输出比较极性高
TIM_OCInitStructure.TIM_Pulse = TIM_Pulse;
TIM_OC2Init(TIM3, &TIM_OCInitStructure); //根据T指定的参数初始化外设TIM3 OC2
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); //使能TIM3在CCR2上的预装载寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIM3
}
PWM初始化完成之后,就可以控制占空比输出了,可以直接调用下面函数,修改PWM的占空比:
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
完成上述操作之后,需要在主函数中调用初始化函数,设定PWM的频率,这里设置PWM的频率为1KHz,如下:
TIM3_PWM_Init(899,159, 800); /* PWM:72000/((899+1)(158+1)) = 1KHZ */
如果设置PWM的占空比为50%,可以这样:
TIM_SetCompare2(TIM3, 450);
TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);的第二个参数占TIM3_PWM_Init(899,159, 800);函数第一个参数的百分比,即为PWM所占的占空比。
初始化完成之后,接下来的就是根据要求完成具体功能逻辑了。在本设计中,需要根据ADC的值来调节PWM的输出。声音音频越大,对应的ADC值越大,那么输出的PWM占空比就越高。前面章节已经知道,stm32的ADC为12位,对应十进制最大值我4096,PWM的占空比为1-99%,这里用adc_value表示测量的ADC值,tim_pulse表示PWM的占空比,adc_value为输入,tim_pulse为输出,为了计算方便,这里不妨取ADC值的范围为0-4000,占空比取0-100(事实上,根据实际情况,他们并不能达到极限值,这么处理完全是可行的)。那么他们可以处理为一个简单的一元一次方程:
tim_pulse = 1/40 * adc_value
事实上,根据实际情况,其变化并不是根据上述式子来的,而且相差较大,下面是实际测试后效果比较好的处理:
adc_value = Get_Adc_Average(ADC_Channel_8, 20);
TIM_SetCompare2(TIM3,(adc_value*40 + 1));
main.c
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "adc.h"
#include "timer.h"
#include
#include "pwm.h"
int main(void)
{
u16 adc_value = 0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
delay_init(); //延时函数初始化
delay_ms(1500);
Adc_Init(); /* ADC 初始化 */
TIM3_PWM_Init(899,159,800);
while(1)
{
adc_value = Get_Adc_Average(ADC_Channel_8, 20);
TIM_SetCompare2(TIM3,(adc_value*40 + 1));
}
}
pwm.c
#include "pwm.h"
#include "led.h"
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void TIM3_PWM_Init(u16 arr,u16 psc,u16 TIM_Pulse)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能定时器3时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); //使能GPIO外设和AFIO复用功能模块时钟
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); //Timer3部分重映射 TIM3_CH2->PB5
//设置该引脚为复用输出功能,输出TIM3 CH2的PWM脉冲波形 GPIOB.5
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //TIM_CH2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO
//初始化TIM3
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
//初始化TIM3 Channel2 PWM模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性:TIM输出比较极性高
TIM_OCInitStructure.TIM_Pulse = TIM_Pulse;
TIM_OC2Init(TIM3, &TIM_OCInitStructure); //根据T指定的参数初始化外设TIM3 OC2
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); //使能TIM3在CCR2上的预装载寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIM3
}
pwm.h
#ifndef __PWM_H
#define __PWM_H
#include "sys.h"
void TIM3_PWM_Init(u16 arr,u16 psc,u16 TIM_Pulse);
#endif
adc.c
#include "adc.h"
#include "delay.h"
#include "led.h"
void Adc_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB |RCC_APB2Periph_ADC1 , ENABLE ); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
//PB0 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOB, &GPIO_InitStructure);
ADC_DeInit(ADC1); //复位ADC1
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
}
u16 Get_Adc(u8 ch)
{
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}
u16 Get_Adc_Average(u8 ch,u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t
adc.h
#ifndef __ADC_H
#define __ADC_H
#include "sys.h"
void Adc_Init(void);
u16 Get_Adc(u8 ch);
u16 Get_Adc_Average(u8 ch,u8 times);
#endif