【PWM】无源蜂鸣器播放音乐
下载例程代码:
前置知识
请参考视频教程 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中音符的频率,使用宏定义来表示每个音符的频率。 主要,这里的低中高音频率是基于标准音高的,小伙伴们如果需要其他音高的音符,可以自行查询相应音高的频率后修改这些宏定义。
1 /* USER CODE BEGIN Includes */2 #include "main.h"3 #include "stm32f1xx_hal.h"4```c5 /* USER CODE BEGIN PD */6 #define P0 0// 休止符频率78 #define L1 262 // 低音频率9 #define L2 29410 #define L3 33011 #define L4 34912 #define L5 39213 #define L6 44014 #define L7 4941516 #define M1 523 // 中音频率17 #define M2 58718 #define M3 65919 #define M4 69820 #define M5 78421 #define M6 88022 #define M7 9882324 #define H1 1047 // 高音频率25 #define H2 117526 #define H3 131927 #define H4 139728 #define H5 156829 #define H6 176030 #define H7 1976
- 2. 定义结构体,存储歌曲中每个音符的频率与持续时间
1typedef struct2 {3 uint16_t frequency; // 音符频率4 float period; // 音符持续时间,单位为拍5 } Bate;
- 3. 定义歌曲的音符
定义一个Bate数组,存储一首歌的每个音符的持续时间。(最近我比较喜欢《莫愁乡》,所以这里以《莫愁乡》为例)
本文后面会介绍如何将其他歌曲转换为这种格式的音符数组
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},1213 // ......1415 // 娃儿抬头望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时钟树动画讲解
1 /**2 * 计算定时器计数频率3 */4 uint32_t TIM_GetCounterFreq(TIM_HandleTypeDef *htim) {5 uint32_t timer_clock;6 // 高级定时器是APB27 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 // 其他定时器是APB115 timer_clock = HAL_RCC_GetPCLK1Freq();16 // 如果APB分频不为1,定时器时钟会翻倍17 if (HAL_RCC_GetPCLK1Freq() != (HAL_RCC_GetHCLKFreq() / 1)) {18 timer_clock *= 2;19 }20 }2122 uint32_t prescaler = htim->Instance->PSC;23 return timer_clock / (prescaler + 1);24 }25265.在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 */4142 /* 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);7374 // 下一个音符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拍。所以转换到程序中就是:
1 {M6, 1}, {M5, 1}, {M3, 1},
而如果数字下面有一根横线,就表示这个音符是一个八分音符,持续的时间便是四分音符的一半,也就是0.5拍。
例如第一句的后面的 5 5 就是两个八分音符,持续0.5拍,所以转换到程序中就是:
1{M5, 0.5f}, {M5, 0.5f},
而假若数字下面带有两个横线,就表示这个音符是一个十六分音符,持续的时间便是四分音符的四分之一,也就是0.25拍。 同理,若数字下有三个横线,则表示三十二分音符,持续的时间便是四分音符的八分之一,也就是0.125拍。
例如简谱中“呼吸声”三个字对应的 1 3 5,其中1 3下有三个横线 5下有两个横线,转换到程序中就是:
1 {M1, 0.125f}, {M3, 0.125f}, {M5, 0.25f},
假若数字后有•,则表示这个音符延长半拍,也就是四分音符的1.5倍时间。例如“不能理顺我自己的疑问”中的“疑”对应的音符5•,转换到程序中就是:
1 {M5, 1.5f},
而若数字后有横杠-,则表示音频延长一拍,也就是四分音符的2倍时间。而且延音符号可以叠加,例如数字后面若有--•,则表示延长2.5拍。
4. 耐心
按照以上规则,就可以将简谱转换为程序中的音符数组了~ 扒谱是一个比较繁琐的工作,小伙伴要有耐心和细心哦~