波特律动
课程中心/STM32基础教程/【PWM】无源蜂鸣器播放音乐

【PWM】无源蜂鸣器播放音乐

下载例程代码

注意

CubeIDE:请按照 例程使用方法🔗 导入例程,否则下载的可能不是例程而是其他工程。

Keil:请使用 ArmCC V6 编译,否则可能会出现编译错误。点击此处查看切换编译器方法🔗

前置知识

请参考视频教程 STM32】使用蜂鸣器播放美妙旋律~与文档【PWM】无源蜂鸣器

如何使用例程

下载程序,即可听到效果

程序效果

  • 烧录例程后,按下 KEY1 会开始使用无源蜂鸣器播放《莫愁乡》的曲子,再次按下KEY1会暂停播放
  • 可以尝试修改例程代码,播放其他曲子

例程讲解

下面介绍了如何自己实现该例程的功能

1、工程配置

  • 开启外部晶振:在Pinout&Configuration -> System Core -> RCC 页面,将 High Speed Clock (HSE) 配置为 Crystal/Ceramic Resonator
  • 配置时钟频率:在Clock Configuration 页面,将PLL Source 选择为 HSE,将System Clock Mux 选择为 PLLCLK,然后在HCLK (MHz) 输入72并回车,将HCLK频率配置为 72 MHz
  • 分配引脚
  • 在Pinout&Configuration页面,配置如下引脚
    • 将PB9配置为TIM4_CH4,
    • 将PB12设置为GPIO_Input,并分别设置User Label为KEY1
  • 配置TIM4:在Pinout&Configuration -> Timers -> TIM4
    • 勾选 Internal Clock,开启 TIM4 的内部时钟源
    • Configuration -> Mode,将 Channel4 配置为 PWM Generation CH4
    • Configuration -> Parameter Settings -> Counter Settings,将 Prescaler 配置为 72-1

2、代码

  • 1. 定义不同每个音调的频率
    这里定义了低音、中音和高音的各7中音符的频率,使用宏定义来表示每个音符的频率。 主要,这里的低中高音频率是基于标准音高的,小伙伴们如果需要其他音高的音符,可以自行查询相应音高的频率后修改这些宏定义。
C语言
1 /* USER CODE BEGIN Includes */
2 #include "main.h"
3 #include "stm32f1xx_hal.h"
4```c
5 /* USER CODE BEGIN PD */
6 #define P0 0// 休止符频率
7
8 #define L1 262 // 低音频率
9 #define L2 294
10 #define L3 330
11 #define L4 349
12 #define L5 392
13 #define L6 440
14 #define L7 494
15
16 #define M1 523 // 中音频率
17 #define M2 587
18 #define M3 659
19 #define M4 698
20 #define M5 784
21 #define M6 880
22 #define M7 988
23
24 #define H1 1047 // 高音频率
25 #define H2 1175
26 #define H3 1319
27 #define H4 1397
28 #define H5 1568
29 #define H6 1760
30 #define H7 1976
  • 2. 定义结构体,存储歌曲中每个音符的频率与持续时间
C语言
1typedef struct
2 {
3 uint16_t frequency; // 音符频率
4 float period; // 音符持续时间,单位为拍
5 } Bate;
  • 3. 定义歌曲的音符
    定义一个Bate数组,存储一首歌的每个音符的持续时间。(最近我比较喜欢《莫愁乡》,所以这里以《莫愁乡》为例)
    本文后面会介绍如何将其他歌曲转换为这种格式的音符数组
C语言
1 const Bate MoChouXiang[] = {
2 // 我被困在了
3 {M6, 1}, {M5, 1}, {M3, 1}, {M5, 0.5f}, {M5, 0.5f},
4 // 这片混沌 柳暗
5 {M6, 0.5f}, {M5, 1}, {M5, 0.5f}, {M3, 0.5f}, {M3, 0.5f}, {M3, 0.5f}, {M3, 0.5f},
6 // 花明 一村一村一村
7 {M2, 0.5f}, {M3, 0.5f}, {M5, 0.5f}, {M3, 0.5f}, {M2, 0.5f}, {M3, 0.5f}, {M5, 0.5f}, {M3, 0.5f},
8 // 一村又一村
9 {M2, 0.5f}, {M3, 0.5f}, {M3, 0.5f}, {L7, 0.5f}, {M3, 1}, {M1, 1},
10 // 不能理顺我
11 {M2, 1}, {M3, 1}, {M2, 1}, {M3, 0.5f}, {M3, 0.5f},
12
13 // ......
14
15 // 娃儿抬头望
16 {M3, 0.5f}, {M2, 0.5f}, {M2, 0.5f}, {M3, 0.5f}, {M3, 0.5f}, {M2, 1.5f},
17 // 姥姥在天上
18 {M1, 0.5f}, {M3, 0.5f}, {M2, 0.5f}, {M3, 0.5f}, {M2, 1}, {M1, 1},
19 };
  • 4. 计算定时器计数频率
    因为我们需要知道定时器的频率来计算PWM的频率,因而封装了一个函数来计算定时器的计数频率。
    当然不优雅的方法是在程序里写死,因为我们知道用的定时器是TIM4,且Prescaler配置为72-1,那么定时器的计数频率就是72MHz / 72 = 1MHz
    不理解程序里为何×2的小伙伴请复习【STM32】超清晰STM32时钟树动画讲解
C语言
1 /**
2 * 计算定时器计数频率
3 */
4 uint32_t TIM_GetCounterFreq(TIM_HandleTypeDef *htim) {
5 uint32_t timer_clock;
6 // 高级定时器是APB2
7 if (htim->Instance == TIM1) {
8 timer_clock = HAL_RCC_GetPCLK2Freq();
9 // 如果APB分频不为1,定时器时钟会翻倍
10 if (HAL_RCC_GetPCLK2Freq() != (HAL_RCC_GetHCLKFreq() / 1)) {
11 timer_clock *= 2;
12 }
13 } else {
14 // 其他定时器是APB1
15 timer_clock = HAL_RCC_GetPCLK1Freq();
16 // 如果APB分频不为1,定时器时钟会翻倍
17 if (HAL_RCC_GetPCLK1Freq() != (HAL_RCC_GetHCLKFreq() / 1)) {
18 timer_clock *= 2;
19 }
20 }
21
22 uint32_t prescaler = htim->Instance->PSC;
23 return timer_clock / (prescaler + 1);
24 }
25
265.while循环中检测按键并遍历数组播放其中的音符
27 /* USER CODE BEGIN 2 */
28 // 开始PWM输出
29 HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_4);
30 // TIM4的计数频率
31 uint32_t timFrequency = TIM_GetCounterFreq(&htim4);
32 // 播放状态
33 uint8_t playState = 0;
34 // 播放进度
35 uint32_t playIndex = 0;
36 // 节拍速度(每分钟多少拍)
37 uint8_t bpm = 132;
38 // 每拍的持续时间
39 float noteDuration = 1000 * 60 / bpm;
40 /* USER CODE END 2 */
41
42 /* Infinite loop */
43 /* USER CODE BEGIN WHILE */
44 while (1)
45 {
46 // 按键检测,切换播放与暂停
47 if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET){
48 HAL_Delay(10);
49 if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET){
50 playState = !playState;
51 while(HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET);
52 }
53 }
54 // 播放
55 if (playState){
56 const Bate bate = MoChouXiang[playIndex];
57 if (bate.frequency == P0) {
58 // 休止符
59 __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0);
60 } else {
61 // 将频率转换为计数值, 设置到自动重装载寄存器
62 uint32_t arr = timFrequency / bate.frequency;
63 __HAL_TIM_SET_AUTORELOAD(&htim4,arr);
64 // 设置占空比为20%
65 __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, arr / 5); // 20%占空比
66 // 从0开始计数 重置PWM波形
67 __HAL_TIM_SetCounter(&htim4, 0);
68 }
69 // 延时该音符的持续时间 (5ms的空白以区分连续两个相同的音符)
70 HAL_Delay((uint32_t) (bate.period * noteDuration) - 5);
71 __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0);
72 HAL_Delay(5);
73
74 // 下一个音符
75 playIndex++;
76 // 播放结束
77 if (playIndex >= sizeof(MoChouXiang)){
78 playState = 0;
79 playIndex = 0;
80 }
81 }else{
82 __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0);
83 }

如何把其他歌曲转换为音符数组

首先需要从网上找到这首歌曲的简谱,例如莫愁乡的简谱如下:

1. 基本信息

在简谱中的 4/4 下面的4表示以四分音符为一个拍,上面的4表示每小节4拍。

然后bmp是指曲子的速度,单位是每分钟多少拍,这里的132就表示每分钟132拍。

我们的程序中就有bmp变量,可以通过修改这个变量来调整曲子的速度。

2. 高低中音

一半的数字我们都认为是中音,1 2 3使用M1 M2 M3宏定义的频率即可,如若数字下有·,则表示低音,我们使用L1 L2 L3宏定义的频率, 若数字上有·,则表示高音,我们使用H1 H2 H3宏定义的频率。

3. 音符的持续时间

在简谱中,一个普通的,下面没有带横线的数字,就是一个四分音符,持续1拍的时间。

例如谱子刚上来的 6 5 3 就是3个四分音符,分别是6、5、3,每个音符持续1拍。所以转换到程序中就是:

C语言
1 {M6, 1}, {M5, 1}, {M3, 1},

而如果数字下面有一根横线,就表示这个音符是一个八分音符,持续的时间便是四分音符的一半,也就是0.5拍。

例如第一句的后面的 5 5 就是两个八分音符,持续0.5拍,所以转换到程序中就是:

C语言
1{M5, 0.5f}, {M5, 0.5f},

而假若数字下面带有两个横线,就表示这个音符是一个十六分音符,持续的时间便是四分音符的四分之一,也就是0.25拍。 同理,若数字下有三个横线,则表示三十二分音符,持续的时间便是四分音符的八分之一,也就是0.125拍。

例如简谱中“呼吸声”三个字对应的 1 3 5,其中1 3下有三个横线 5下有两个横线,转换到程序中就是:

C语言
1 {M1, 0.125f}, {M3, 0.125f}, {M5, 0.25f},

假若数字后有•,则表示这个音符延长半拍,也就是四分音符的1.5倍时间。例如“不能理顺我自己的疑问”中的“疑”对应的音符5•,转换到程序中就是:

C语言
1 {M5, 1.5f},

而若数字后有横杠-,则表示音频延长一拍,也就是四分音符的2倍时间。而且延音符号可以叠加,例如数字后面若有--•,则表示延长2.5拍。

4. 耐心

按照以上规则,就可以将简谱转换为程序中的音符数组了~ 扒谱是一个比较繁琐的工作,小伙伴要有耐心和细心哦~