## 一、原理:为什么用 PWM 驱动无源蜂鸣器? 无源蜂鸣器内部没有振荡源,必须施加特定频率的方波才能发声。而 STM32 的定时器 PWM 模式可以精确产生所需频率的方波,且不占用 CPU 忙等。 - **频率决定音调**:每个音符对应一个基频,例如 A4(中音 la)为 440Hz。 - **占空比影响音量**:通常占空比 50% 时响度最大,但也可通过调节占空比改变音色。 - **PWM 输出**:通过定时器自动翻转 GPIO 电平,无需中断干预,适合播放连续音符。 ## 二、STM32 PWM 配置步骤(以 TIM2 CH1 为例) 假设主频 72MHz,使用 PB0 作为 PWM 输出引脚,我们配置 TIM2 产生 1kHz 方波。 ### 1. 使能 GPIO 和定时器时钟 ```c RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); ``` ### 2. 配置 GPIO 复用推挽输出 ```c GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); ``` ### 3. 配置定时器时基与 PWM 模式 ```c TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 自动重装载值,决定频率 TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 预分频:72MHz/72=1MHz TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 500; // 占空比 50% TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); ``` > 频率计算:`Fpwm = 72MHz / ((Prescaler+1)*(Period+1))`。本例中 = 72e6 / (72 * 1000) = 1000Hz。 ## 三、音符频率与延时播放原理 音乐播放的关键是将每个音符的持续时间映射为延时。首先定义一个音符频率表: ```c // 低音 4~7、中音 1~7、高音 1~7 的频率(Hz) const uint16_t tone_table[] = { 262, 294, 330, 349, 392, 440, 494, // C4~B4 523, 587, 659, 698, 784, 880, 988, // C5~B5 1047, 1175, 1319, 1397, 1568, 1760, 1976 // C6~B6 }; ``` 播放时,先更新定时器周期和占空比,使 PWM 输出目标频率,然后延时对应节拍时间,最后停止输出(占空比设为 0)。 ### 更新频率函数 ```c void Buzzer_Set_Freq(uint16_t freq) { if (freq == 0) { TIM_SetCompare1(TIM2, 0); // 静音 return; } uint32_t period = 1000000 / freq; // 计数器频率为 1MHz,故周期值 = 1e6/freq TIM_SetAutoreload(TIM2, period - 1); TIM_SetCompare1(TIM2, (period - 1) / 2); // 50% 占空比 } ``` ## 四、完整代码示例:播放《小星星》 以下代码演示通过延时方式播放一段简单旋律,每段音符播放后均停止发声。 ```c #include "stm32f10x.h" #include "delay.h" void Buzzer_Init(void) { // 略:RCC、GPIO、TIM2 PWM 初始化,同上述步骤 } void Buzzer_Set_Freq(uint16_t freq) { if (freq == 0) { TIM_SetCompare1(TIM2, 0); return; } uint32_t period = 1000000 / freq; TIM_SetAutoreload(TIM2, period - 1); TIM_SetCompare1(TIM2, (period - 1) / 2); } // 音符定义:0 为休止符 #define N_C4 262 #define N_D4 294 #define N_E4 330 #define N_F4 349 #define N_G4 392 #define N_A4 440 #define N_B4 494 #define N_C5 523 // 乐谱:音符、节拍(单位:延时基础长度) const uint16_t score[][2] = { {N_C4, 1}, {N_C4, 1}, {N_G4, 1}, {N_G4, 1}, {N_A4, 1}, {N_A4, 1}, {N_G4, 2}, {N_F4, 1}, {N_F4, 1}, {N_E4, 1}, {N_E4, 1}, {N_D4, 1}, {N_D4, 1}, {N_C4, 2}, {0, 1} }; void Play_Music(void) { uint8_t i; for (i = 0; i < sizeof(score)/sizeof(score[0]); i++) { Buzzer_Set_Freq(score[i][0]); delay_ms(400 * score[i][1]); // 每个节拍 400ms Buzzer_Set_Freq(0); // 短暂停顿增加颗粒感 delay_ms(50); } } int main(void) { delay_init(); Buzzer_Init(); while (1) { Play_Music(); delay_ms(2000); } } ``` ## 五、延时播放的注意事项 - **延时阻塞 CPU**:使用 `delay_ms` 播放期间无法响应其他任务,适合简单应用;若需并发,应改用定时器中断或 RTOS。 - **频率切换瞬间**:改变自动重装载值时,可能会产生短暂毛刺,建议先停止定时器或设置预装载,再修改值。 - **音准误差**:定时器计数频率和预分频值需根据实际系统时钟调整,确保 1MHz 计数频率准确。 - **音量控制**:若需要调节音量,可修改 `TIM_SetCompare1` 的值,但注意过小占空比可能导致蜂鸣器驱动不足。 - **长时播放发热**:持续高音量发声会加速蜂鸣器老化,可考虑降低占空比或加入间歇。 ## 六、扩展:非阻塞播放思路 若需在播放音乐的同时控制 LED 或读取按键,可借助定时器中断:每 10ms 中断一次,在中断中计数节拍时间并更新 PWM 频率。这样主循环仅负责状态切换,实现伪多任务。 ```c // 伪代码示意:定时器中断服务函数 void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update)) { // 处理音符时长计数,并调用 Buzzer_Set_Freq() TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } } ``` ## 七、总结 通过 STM32 定时器的 PWM 输出配合延时控制,可以轻松实现无源蜂鸣器的音乐播放。核心在于掌握频率与音符的换算关系,以及合理更新定时器参数。延时播放虽然简单直观,但适用于对实时性要求不高的场景;更高阶的应用应使用中断或 DMA 驱动。希望本文能帮助你在项目中快速实现蜂鸣器发声功能。