STM32F103C8T6驱动无源蜂鸣器播放《两只老虎》完整教程(附源码)
蜂鸣器作为嵌入式开发中最基础的外设之一,常被用于系统报警、状态提示等场景。但你是否想过,通过精确控制PWM频率和节奏,可以让这个简单的元件演奏出熟悉的旋律?本文将带你用STM32的TIM定时器,实现无源蜂鸣器播放《两只老虎》的完整过程。
1. 硬件准备与原理分析
1.1 无源蜂鸣器工作特性
无源蜂鸣器与有源蜂鸣器的核心区别在于驱动方式:
- 有源蜂鸣器:内置振荡电路,只需提供直流电压即可发声
- 无源蜂鸣器:需要外部提供方波信号才能工作
音乐播放场景必须使用无源蜂鸣器,因为:
- 音高由驱动频率决定(C4=261Hz,D4=294Hz等)
- 音长由信号持续时间控制
- 可通过PWM精确调节波形特性
1.2 STM32的PWM生成机制
STM32F103C8T6通过TIM定时器产生PWM信号的关键配置参数:
| 参数 | 作用 | 计算公式 |
|---|---|---|
| ARR (Auto-reload) | 决定PWM周期 | 频率 = 定时器时钟/(PSC+1)/(ARR+1) |
| PSC (Prescaler) | 时钟预分频系数 | 实际时钟 = 72MHz/(PSC+1) |
| CCR (Capture Compare) | 决定占空比 | 占空比 = CCR/(ARR+1) |
提示:音乐播放时通常固定50%占空比,重点调节ARR值改变频率
2. 音乐编程基础
2.1 音阶频率对照表
《两只老虎》主要使用中音区(C4-B4)的七个基本音阶:
| 音符 | 频率(Hz) | 计算值(ARR) @PSC=71 |
|---|---|---|
| C4 | 261.63 | 3830 |
| D4 | 293.66 | 3412 |
| E4 | 329.63 | 3040 |
| F4 | 349.23 | 2870 |
| G4 | 392.00 | 2558 |
| A4 | 440.00 | 2279 |
| B4 | 493.88 | 2032 |
计算公式:
ARR = (72000000 / (PSC+1)) / frequency - 1 = (72000000 / 72) / frequency - 1 = 1000000 / frequency - 12.2 节拍时间控制
四四拍歌曲的典型节拍时长(BPM=120时):
| 节拍类型 | 持续时间(ms) |
|---|---|
| 全音符 | 2000 |
| 二分音符 | 1000 |
| 四分音符 | 500 |
| 八分音符 | 250 |
《两只老虎》简谱对应的节拍序列:
C4(1) D4(1) E4(1) C4(1) | C4(1) D4(1) E4(1) C4(1) | E4(1) F4(1) G4(2) | E4(1) F4(1) G4(2) | G4(0.5) A4(0.5) G4(0.5) F4(0.5) E4(1) C4(1) | G4(0.5) A4(0.5) G4(0.5) F4(0.5) E4(1) C4(1) | C4(1) G3(1) C4(2) | C4(1) G3(1) C4(2) |3. 工程实现步骤
3.1 硬件连接
使用STM32F103C8T6最小系统板与无源蜂鸣器模块连接:
蜂鸣器+ → PA0 (TIM2_CH1) 蜂鸣器- → GND注意:无源蜂鸣器没有极性,但需串联100Ω限流电阻
3.2 代码实现
3.2.1 PWM初始化配置
// pwm.h #define BUZZER_TIM TIM2 #define BUZZER_CHANNEL TIM_CHANNEL_1 void PWM_Init(void); void PWM_SetFreq(uint16_t freq); void PWM_PlayNote(uint16_t freq, uint32_t duration);// pwm.c #include "stm32f10x.h" void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_OCInitTypeDef TIM_OCInitStruct; // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 2. 配置GPIO GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. 配置定时器基础 TIM_InitStruct.TIM_Period = 999; // 初始10kHz TIM_InitStruct.TIM_Prescaler = 71; // 72MHz/72=1MHz TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(BUZZER_TIM, &TIM_InitStruct); // 4. 配置PWM输出 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStruct.TIM_Pulse = 500; // 50%占空比 TIM_OC1Init(BUZZER_TIM, &TIM_OCInitStruct); TIM_Cmd(BUZZER_TIM, ENABLE); } void PWM_SetFreq(uint16_t freq) { uint16_t arr = (uint16_t)(1000000 / freq) - 1; TIM_SetAutoreload(BUZZER_TIM, arr); TIM_SetCompare1(BUZZER_TIM, arr / 2); // 保持50%占空比 }3.2.2 音乐播放逻辑
// music.h typedef struct { uint16_t freq; uint16_t duration; } Note; #define TEMPO 120 // BPM #define QUARTER_NOTE (60000 / TEMPO) extern const Note song[]; extern const uint16_t song_length;// music.c #include "music.h" const Note song[] = { {262, QUARTER_NOTE}, // C4 {294, QUARTER_NOTE}, // D4 {330, QUARTER_NOTE}, // E4 {262, QUARTER_NOTE}, // C4 // ... 完整乐谱 }; const uint16_t song_length = sizeof(song)/sizeof(Note);3.2.3 主程序
// main.c #include "stm32f10x.h" #include "pwm.h" #include "music.h" #include "delay.h" int main(void) { Delay_Init(); PWM_Init(); while(1) { for(int i=0; i<song_length; i++) { PWM_SetFreq(song[i].freq); Delay_ms(song[i].duration); } PWM_SetFreq(0); // 停止发声 Delay_ms(2000); // 间隔2秒 } }4. 进阶优化技巧
4.1 节拍精度提升
使用SysTick定时器替代Delay_ms实现更精确的节奏控制:
void SysTick_Handler(void) { static uint32_t counter = 0; if(counter++ >= note_duration) { counter = 0; play_next_note(); } }4.2 多任务处理
在RTOS环境中创建独立播放任务:
void buzzer_task(void *params) { while(1) { play_song(&two_tigers); vTaskDelay(pdMS_TO_TICKS(5000)); } } xTaskCreate(buzzer_task, "Buzzer", 128, NULL, 2, NULL);4.3 音效增强
通过调制PWM占空比实现音色变化:
void PWM_SetEnvelope(uint16_t freq, uint16_t duration) { // 淡入效果 for(int i=1; i<=10; i++) { TIM_SetCompare1(BUZZER_TIM, (ARR/i)); Delay_ms(duration/20); } // 淡出效果 for(int i=10; i>=1; i--) { TIM_SetCompare1(BUZZER_TIM, (ARR/i)); Delay_ms(duration/20); } }