本篇主要讲述在STM32单片机开发板上通过STM32CubeIDE配置相关管脚和参数,来操作无源蜂鸣器播放歌曲《北京欢迎你》的步骤和原理。
硬件:STM32系列单片机开发板一块(附带开发板的原理图)、串口下载线、无源蜂鸣器模块一个;
软件:STM32CubeIDE、STM32CubeProgrammer。
以下是蜂鸣器模块的原理图:
图中的PG便是蜂鸣器,一般来说,3.3V直流产生的电信号功率很小,不足以驱动蜂鸣器发出可听见的响声,所以整个模块中有一个电流放大模块,这便是图中的晶体三极管。图中的放大电路是典型的共发射极接法,蜂鸣器接在集电极上,就实现了电流的放大(详细的晶体管放大电路原理请见《模拟电子技术》)
蜂鸣器分为有源和无源两种。所谓的源,指的是其中内部的振荡源,有源蜂鸣器中的振荡器一般是多谐振荡器,其原理就是模拟电路中RC振荡器的一般原理(放大电路、正反馈、相位差90°、稳压电路),有源蜂鸣器内部的振荡源频率是固定的,所以使用时不可调频,且输入的电信号必须是直流。
无源蜂鸣器没有内部振荡源,发声的基本原理是电磁感应,其内部的基本结构是一匝匝线圈以及一个微型扬声器。由于没有振荡源,且内部的主要结构是线圈,所以其工作时输入的电信号必须是交流,而不能是频率很低的直流(直流不能通过无负载的线圈,负责线圈会因为短路而被烧坏)。与有源蜂鸣器一样,无源蜂鸣器也必须使用放大电路来增大输入信号的功率。
信号在某段时间内的能量等于其瞬时值的平方在该段时间上的时间累积(信号能量的微积分定义)。因此,我们可以用矩形波来进行能量等效,使得等效矩形波通过负载时获得的能量与原信号通过负载时获得的能量相等。这种等效的矩形波信号便是脉冲宽度调制信号,简称PWM信号。
PWM信号的主要参数有两个,一个是占空比,一个是频率。PWM信号一个周期内高电平作用时间同整个周期时间的比值叫占空比,占空比是衡量PWM信号功率的主要参数,PWM信号的功率与其占空比成正相关。
PWM波控制灯亮度:在一定电压范围内,灯泡的亮度与其两端所加电压大小成正比,因而我们可以通过与灯泡串联滑动变阻器来实现灯泡亮度的控制,这是中学时学到的欧姆定律的基本原理。如果我们通过在LED灯两端输入PWM波的话,由于PWM波周期很短,人会因为视觉暂留效应察觉不到灯的闪烁,而是灯的亮度变小了,这便是PWM波调节LED等亮度的原理。采用PWM信号驱动LED灯,可以实现在不改变电压峰值的情况下控制LED等亮度,也减少了电路的硬件设计成本。
PWM波控制电机转速:通电导体在磁场中会受到力的作用,这是电机转动的基本原理,但又由电磁感应原理可知,当电机转一段时间后断开电源,电机会因为在线圈中产生感抗电流和惯性的作用,逐渐减速而停下。如果我们可以对电机一段时间通电一段时间断电,就可以实现电机转速的控制了,这就是PWM波在电机上工作的原理。
PWM波控制蜂鸣器:与电磁感应原理类似,调解PWM波的占空比,便可以控制无源蜂鸣器的响度;调解PWM波的频率,便可以调解无源蜂鸣器的发生频率,也就可以发出不同音调的声音,这同样也是蜂鸣器演奏歌曲的原理。
STM32单片机中的PWM信号可以通过定时器产生。通过对定时器产生的方波信号进行分频和设置脉冲宽度,便可以实现调频和调占空比。
STM32定时器中有两种重要的寄存器:
配置STM32定时器的步骤如下:
上图展示了STM32时钟树的部分。在配置系统时钟时,首先要使能其中的高速外部时钟(HSE),即选择时钟树中HSE中的选项;顺着HSE的时钟线走,可以发现一个**PLLCLK(锁相环频率)**选项,选择它;其次,我们可以看到一个HCLK频率的设置框,把这里的频率设置为80MHz(最大高速时钟频率),此时便可以看到Timer Clock(定时器时钟)的基频是80MHz,后面我们就会在这80MHz的频率上进行分频和最大计数设置,使其产生不同频率的PWM信号。
软件编程是此工程的核心,核心的代码如下:
main.c
#define ARRAY_LEN(a) (sizeof(a)/sizeof(a[0]))
const uint32_t tone[][2][3] = {
{ {261, 523, 1046}, {277, 554, 1109} },
{ {294, 587, 1175}, {311, 622, 1245} },
{ {330, 659, 1318}, {0, 0, 0} },
{ {349, 698, 1397}, {370, 740, 1480} },
{ {392, 784, 1568}, {415, 831, 1661} },
{ {440, 880, 1760}, {466, 932, 1865} },
{ {494, 988, 1976}, {0, 0, 0} }
};
uint8_t BeiJing1[][3] =
{
{3, 0, 1}, {5, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {0, 0, 0},
{3, 0, 1}, {2, 0, 1}, {6, 0, 0}, {1, 0, 1}, {3, 0, 1}, {2, 0, 1}, {0, 0, 0}, {0, 0, 0},
{2, 0, 1}, {1, 0, 1}, {6, 0, 0}, {1, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {2, 0, 1},
{3, 0, 1}, {6, 0, 1}, {5, 0, 1}, {6, 0, 0}, {2, 0, 1}, {1, 0, 1}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {0, 0, 0},
{3, 0, 1}, {2, 0, 1}, {6, 0, 0}, {1, 0, 1}, {3, 0, 1}, {2, 0, 1}, {0, 0, 0}, {0, 0, 0},
{2, 0, 1}, {1, 0, 1}, {6, 0, 0}, {1, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {2, 0, 1},
{3, 0, 1}, {6, 0, 1}, {5, 0, 1}, {6, 0, 0}, {2, 0, 1}, {1, 0, 1}, {0, 0, 0},
{2, 0, 1}, {1, 0, 1}, {6, 0, 0}, {1, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {2, 0, 1},
{3, 0, 1}, {6, 0, 1}, {5, 0, 1}, {5, 0, 1}, {3, 0, 1}, {0, 0, 0},
{2, 0, 1}, {3, 0, 1}, {2, 0, 1}, {1, 0, 1}, {5, 0, 1}, {6, 0, 1}, {2, 0, 1}, {0, 0, 0},
{5, 0, 0}, {3, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {0, 0, 0},
{3, 0, 1}, {2, 0, 1}, {6, 0, 0}, {1, 0, 1}, {3, 0, 1}, {2, 0, 1}, {0, 0, 0}, {0, 0, 0},
{2, 0, 1}, {1, 0, 1}, {6, 0, 0}, {1, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {2, 0, 1},
{3, 0, 1}, {6, 0, 1}, {5, 0, 1}, {6, 0, 0}, {2, 0, 1}, {1, 0, 1}, {0, 0, 0},
{2, 0, 1}, {1, 0, 1}, {6, 0, 0}, {1, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {2, 0, 1},
{3, 0, 1}, {6, 0, 1}, {5, 0, 1}, {5, 0, 1}, {3, 0, 1}, {0, 0, 0},
{2, 0, 1}, {3, 0, 1}, {2, 0, 1}, {1, 0, 1}, {5, 0, 1}, {6, 0, 1}, {2, 0, 1}, {0, 0, 0},
{6, 0, 0}, {3, 0, 1}, {2, 0, 1}, {2, 0, 1}, {1, 0, 1}, {1, 0, 1},
{0, 0, 0}, {0, 0, 0}, {3, 0, 1}, {5, 0, 1}, {0, 0, 0},
{1, 0, 2}, {5, 0, 1}, {6, 0, 1}, {0, 0, 0}, {6, 0, 1}, {5, 0, 1}, {3, 0, 1}, {3, 0, 1}, {5, 0, 1}, {5, 0, 1}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {6, 0, 1}, {1, 0, 2}, {2, 0, 2}, {1, 0, 2}, {5, 0, 1}, {3, 0, 1}, {2, 0, 1}, {5, 0, 1}, {3, 0, 1}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {1, 0, 2}, {5, 0, 1}, {6, 0, 1}, {0, 0, 0},
{1, 0, 2}, {2, 0, 2}, {1, 0, 2}, {5, 0, 1}, {3, 0, 1}, {5, 0, 1}, {7, 0, 1}, {6, 0, 1}, {0, 0, 0},
{3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {3, 0, 2}, {2, 0, 2}, {1, 0, 2}, {1, 0, 2}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}
};
uint8_t BeiJing2[][3] =
{
{3, 0, 1}, {5, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {0, 0, 0},
{3, 0, 1}, {2, 0, 1}, {6, 0, 0}, {1, 0, 1}, {3, 0, 1}, {2, 0, 1}, {0, 0, 0}, {0, 0, 0},
{2, 0, 1}, {1, 0, 1}, {6, 0, 0}, {1, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {2, 0, 1},
{3, 0, 1}, {6, 0, 1}, {5, 0, 1}, {6, 0, 0}, {2, 0, 1}, {1, 0, 1}, {0, 0, 0},
{2, 0, 1}, {1, 0, 1}, {6, 0, 0}, {1, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {2, 0, 1},
{3, 0, 1}, {6, 0, 1}, {5, 0, 1}, {5, 0, 1}, {3, 0, 1}, {0, 0, 0},
{2, 0, 1}, {3, 0, 1}, {2, 0, 1}, {1, 0, 1}, {5, 0, 1}, {6, 0, 1}, {2, 0, 1}, {0, 0, 0},
{6, 0, 0}, {3, 0, 1}, {2, 0, 1}, {2, 0, 1}, {1, 0, 1}, {1, 0, 1}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {1, 0, 2}, {5, 0, 1}, {6, 0, 1}, {0, 0, 0}, {6, 0, 1}, {5, 0, 1}, {3, 0, 1}, {3, 0, 1}, {5, 0, 1}, {5, 0, 1}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {6, 0, 1}, {1, 0, 2}, {2, 0, 2}, {1, 0, 2}, {5, 0, 1}, {3, 0, 1}, {2, 0, 1}, {5, 0, 1}, {3, 0, 1}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {1, 0, 2}, {5, 0, 1}, {6, 0, 1}, {0, 0, 0},
{1, 0, 2}, {2, 0, 2}, {1, 0, 2}, {5, 0, 1}, {3, 0, 1}, {5, 0, 1}, {7, 0, 1}, {6, 0, 1}, {0, 0, 0},
{3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {3, 0, 2}, {2, 0, 2}, {1, 0, 2}, {1, 0, 2}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {1, 0, 2}, {5, 0, 1}, {6, 0, 1}, {0, 0, 0},
{1, 0, 2}, {2, 0, 2}, {1, 0, 2}, {5, 0, 1}, {3, 0, 1}, {5, 0, 1}, {7, 0, 1}, {6, 0, 1}, {0, 0, 0},
{3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {3, 0, 2}, {2, 0, 2}, {1, 0, 2}, {1, 0, 2}, {0, 0, 0}, {0, 0, 0},
{3, 0, 1}, {5, 0, 1}, {1, 0, 2}, {5, 0, 1}, {6, 0, 1}, {0, 0, 0},
{1, 0, 2}, {2, 0, 2}, {1, 0, 2}, {5, 0, 1}, {3, 0, 1}, {5, 0, 1}, {7, 0, 1}, {6, 0, 1}, {0, 0, 0},
{3, 0, 1}, {2, 0, 1}, {3, 0, 1}, {5, 0, 1}, {3, 0, 2}, {2, 0, 2}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {1, 0, 2},
};
int main(void)
{
...
while (1)
{
for(i=0; i<ARRAY_LEN(BeiJing1); i++){
if(BeiJing1[i][0] != 0){
beep_set( tone[BeiJing1[i][0]-1][BeiJing1[i][1]][BeiJing1[i][2]] );
beep_cry(365);
}
else{
HAL_Delay(250);
}
}
for(i=0; i<ARRAY_LEN(BeiJing2); i++){
if(BeiJing2[i][0] != 0){
beep_set( tone[BeiJing2[i][0]-1][BeiJing2[i][1]][BeiJing2[i][2]] );
beep_cry(365);
}
else{
HAL_Delay(250);
}
}
beep_set(1046);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
HAL_Delay(3000);
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_2);
HAL_Delay(4000);
}
tim.c
void beep_set(uint32_t frq){
htim2.Init.Period = BASE_FRQ / frq;
if (HAL_TIM_PWM_Init(&htim2) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.Pulse = htim2.Init.Period / 2;
if (HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_MspPostInit(&htim2);
}
void beep_cry(uint32_t delay){
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
HAL_Delay(delay);
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_2);
}
说一下通过软件调节PWM信号频率和占空比的方法。
代码在管脚配置完成之后会自动生成,生成的代码包含配置定时器参数的相关部分,此部分在工程文件夹Src里的tim.c中,其初始化函数是void MX_TIM2_Init(void),我们将这个函数内部的部分代码截取过来再进行相关改写,便成为了设置蜂鸣器工作频率的函数void beep_set(uint32_t frq),htim2是一个抽象为定时器的结构体,其成员htim2.Init.Period本质上也就是ARR的值,只要已知定时器基频以及所调频率,便可以确定Period的值(Period = BaseFrq / F);sConfigOC.Pulse应该设置为(Period / q)其中Period即htim2中的Period,q为所调占空比,这个变量决定了PWM信号的占空比大小。
简述一下乐谱中的基本概念:
按照音阶频率的分部规律,我们可以用一个三维数组来进行表示不同音的频率,这也就是main.c代码中tone数组的含义,第一维表示音阶,第二维表示是否升高半音,第三维表示各音区频率。有些音阶不存在升高半音,所以其对应音区设置为0。
我们把音的频率通过三维数组抽象表示之后,就可以就《北京欢迎你》的乐谱来进行编排音了,本人通过两个二维数组来抽象表示乐谱里面所有的音调,第二维的三个数含义也就是确定其音阶、升高变音、音区罢了。乐谱在编排时只起着参考作用,不能完全照搬乐谱上的音调来,还得自己不断针对实际情况试验调音。
不同的单片机MCU型号不同,但大致的用户代码是类似的,此处本人给出试验之后最佳的音乐代码(仅供参考):
代码链接
作品展示链接(哔哩哔哩):
作品展示
真的希望疫情早点离开,让2022真正成为北京欢迎全世界的你的一年。