操作STM32单片机蜂鸣器模块演奏歌曲《北京欢迎你》

本篇主要讲述在STM32单片机开发板上通过STM32CubeIDE配置相关管脚和参数,来操作无源蜂鸣器播放歌曲《北京欢迎你》的步骤和原理。

软硬件需求

硬件:STM32系列单片机开发板一块(附带开发板的原理图)、串口下载线、无源蜂鸣器模块一个;
软件:STM32CubeIDESTM32CubeProgrammer

蜂鸣器的发声原理

以下是蜂鸣器模块的原理图:
操作STM32单片机蜂鸣器模块演奏歌曲《北京欢迎你》_第1张图片
图中的PG便是蜂鸣器,一般来说,3.3V直流产生的电信号功率很小,不足以驱动蜂鸣器发出可听见的响声,所以整个模块中有一个电流放大模块,这便是图中的晶体三极管。图中的放大电路是典型的共发射极接法,蜂鸣器接在集电极上,就实现了电流的放大(详细的晶体管放大电路原理请见《模拟电子技术》)

蜂鸣器分为有源和无源两种。所谓的源,指的是其中内部的振荡源,有源蜂鸣器中的振荡器一般是多谐振荡器,其原理就是模拟电路中RC振荡器的一般原理(放大电路、正反馈、相位差90°、稳压电路),有源蜂鸣器内部的振荡源频率是固定的,所以使用时不可调频,且输入的电信号必须是直流。

无源蜂鸣器没有内部振荡源,发声的基本原理是电磁感应,其内部的基本结构是一匝匝线圈以及一个微型扬声器。由于没有振荡源,且内部的主要结构是线圈,所以其工作时输入的电信号必须是交流,而不能是频率很低的直流(直流不能通过无负载的线圈,负责线圈会因为短路而被烧坏)。与有源蜂鸣器一样,无源蜂鸣器也必须使用放大电路来增大输入信号的功率。

脉冲宽度调制信号(PWM)

信号在某段时间内的能量等于其瞬时值的平方在该段时间上的时间累积(信号能量的微积分定义)。因此,我们可以用矩形波来进行能量等效,使得等效矩形波通过负载时获得的能量与原信号通过负载时获得的能量相等。这种等效的矩形波信号便是脉冲宽度调制信号,简称PWM信号。

PWM信号的主要参数有两个,一个是占空比,一个是频率。PWM信号一个周期内高电平作用时间同整个周期时间的比值叫占空比,占空比是衡量PWM信号功率的主要参数,PWM信号的功率与其占空比成正相关。

PWM信号的运用

PWM波控制灯亮度:在一定电压范围内,灯泡的亮度与其两端所加电压大小成正比,因而我们可以通过与灯泡串联滑动变阻器来实现灯泡亮度的控制,这是中学时学到的欧姆定律的基本原理。如果我们通过在LED灯两端输入PWM波的话,由于PWM波周期很短,人会因为视觉暂留效应察觉不到灯的闪烁,而是灯的亮度变小了,这便是PWM波调节LED等亮度的原理。采用PWM信号驱动LED灯,可以实现在不改变电压峰值的情况下控制LED等亮度,也减少了电路的硬件设计成本。

PWM波控制电机转速:通电导体在磁场中会受到力的作用,这是电机转动的基本原理,但又由电磁感应原理可知,当电机转一段时间后断开电源,电机会因为在线圈中产生感抗电流和惯性的作用,逐渐减速而停下。如果我们可以对电机一段时间通电一段时间断电,就可以实现电机转速的控制了,这就是PWM波在电机上工作的原理。

PWM波控制蜂鸣器:与电磁感应原理类似,调解PWM波的占空比,便可以控制无源蜂鸣器的响度;调解PWM波的频率,便可以调解无源蜂鸣器的发生频率,也就可以发出不同音调的声音,这同样也是蜂鸣器演奏歌曲的原理。

PWM信号的产生

STM32单片机中的PWM信号可以通过定时器产生。通过对定时器产生的方波信号进行分频和设置脉冲宽度,便可以实现调频和调占空比。

STM32定时器中有两种重要的寄存器:

  1. ARR,自动重载寄存器,装载着计数器能计数的最大值,使能定时器中断之后,当计数值大于ARR的值时,会产生溢出中断。由数字电子技术中时序逻辑电路定时器的基本原理可以知道,ARR的值与所产生的PWM信号的频率相关,F == BaseFrq / ARR(BaseFrq指的是预分频后定时器的基频);
  2. CCR,捕获/比较寄存器,定时器当前计数值与CCR的值进行比较,如果大则输出高电平,否则输出低电平(可以实现此功能的硬件模块是滞回比较器,有兴趣的读者可以深入去理解)CCR的值是可调的,因此我们可以通过设置CCR的值实现PWM信号占空比的控制

配置STM32定时器的步骤如下:

  • 设置系统时钟

操作STM32单片机蜂鸣器模块演奏歌曲《北京欢迎你》_第2张图片
上图展示了STM32时钟树的部分。在配置系统时钟时,首先要使能其中的高速外部时钟(HSE),即选择时钟树中HSE中的选项;顺着HSE的时钟线走,可以发现一个**PLLCLK(锁相环频率)**选项,选择它;其次,我们可以看到一个HCLK频率的设置框,把这里的频率设置为80MHz(最大高速时钟频率),此时便可以看到Timer Clock(定时器时钟)的基频是80MHz,后面我们就会在这80MHz的频率上进行分频和最大计数设置,使其产生不同频率的PWM信号。

  • 设置管脚参数
    打开CubeIDE中的Pinout & Configuration,对照自己的开发板原理图,配置时钟管脚和蜂鸣器管脚
    操作STM32单片机蜂鸣器模块演奏歌曲《北京欢迎你》_第3张图片
    这里的HSE选择“Crystal Resonator”(晶体共振),其他的设置默认。
    操作STM32单片机蜂鸣器模块演奏歌曲《北京欢迎你》_第4张图片
    本人蜂鸣器所接管脚是PA1,其对应的定时器是TIM2,这里我们可以先点击TIM2进行设置,图中设置定时器时直接把定时器对应的通道(channel 2)进行使能即可。然后点击图中PA1的管脚,点击其中的TIM2_CH2。定时器的参数在播放音乐时是时刻会改变的,这里我们只把Prescaler(预分频器)设置为80,此时,定时器TIM2默认输出的PWM信号的频率为1MHz**(80MHz/80 == 1MHz)**其他参数将在播放歌曲的函数里面进行动态配置。
  • 生成目标代码
    点击Project Manager此时按住CTL+S键,在Code Generator中勾选Generated files中的第一项,即自动生成代码。
    操作STM32单片机蜂鸣器模块演奏歌曲《北京欢迎你》_第5张图片
    之后按CTL+S保存设置,然后选择“生成目标代码”。

编程环节

软件编程是此工程的核心,核心的代码如下:
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信号的占空比大小。

简述一下乐谱中的基本概念:

  1. 音阶,用“1,2,3,4,5,6,7”表示,即所谓的“do ri mi fa so la si”,每一个音阶还会在其上下加上一个点,表示音阶对应的高音区和低音区(音阶之上为高音区,之下为低音区,无点标记为中音区)每一个音阶的频率是不同的,同一个音阶不同音区的频率也是不同的;
  2. 休止符,用“|”表示,此时需停止发声,针对用蜂鸣器发音乐的情况,休止的时间需要调整;
  3. 升高半音,用“#”表示,指的是同一音阶不同音区之间中部的音,这在音阶频率对应表中有体现。

按照音阶频率的分部规律,我们可以用一个三维数组来进行表示不同音的频率,这也就是main.c代码中tone数组的含义,第一维表示音阶,第二维表示是否升高半音,第三维表示各音区频率。有些音阶不存在升高半音,所以其对应音区设置为0。

我们把音的频率通过三维数组抽象表示之后,就可以就《北京欢迎你》的乐谱来进行编排音了,本人通过两个二维数组来抽象表示乐谱里面所有的音调,第二维的三个数含义也就是确定其音阶、升高变音、音区罢了。乐谱在编排时只起着参考作用,不能完全照搬乐谱上的音调来,还得自己不断针对实际情况试验调音。

代码整合与作品展示

不同的单片机MCU型号不同,但大致的用户代码是类似的,此处本人给出试验之后最佳的音乐代码(仅供参考):

代码链接
作品展示链接(哔哩哔哩):
作品展示

后记

真的希望疫情早点离开,让2022真正成为北京欢迎全世界的你的一年。

你可能感兴趣的:(操作STM32单片机蜂鸣器模块演奏歌曲《北京欢迎你》)