STM32加强篇,应用定时器来使得有关通道引脚输出不同频率的PWM波来播放音乐。
涵盖PSC、ARR、CNT、CCR定时器基础知识。
音乐曲目:洒水车快乐之歌《兰花草》
硬件平台:STM32F103系列
开发方式:库函数开发
1 蜂鸣器
1.1有源蜂鸣器
在一般的开发板说明文档当中,一般都会标注蜂鸣器的类型;在购物网站上,一般也都会有所标注蜂鸣器类型。
有源蜂鸣器的使用很简单,因为已经内置震荡源,只要是通电就能发声。
这里是正点原子关于蜂鸣器的驱动电路图,用了一个S8050的三极管来驱动。

市面上的蜂鸣器模块一般是由S8550的三极管驱动的,这种模块蜂鸣器就实际来看是既能带动无源蜂鸣器也能够带动有源蜂鸣器 。只需要I/O引脚出给予低电平即可。

1.2 无源蜂鸣器
无源蜂鸣器的发声主要是靠引脚输出PWM波来控制,PWM波有两个很重要的地方:频率 和占空比
占空比相同的PWM波的频率不一定相同,占空比 可以在宏观上决定给蜂鸣器电压的大小,来决定响度 ,频率决定音调。
2 乐理浅析
2.1 音调与频率
对于乐理,如果有什么不对的地方还请斧正。
对于音符,有对应的频率对照表C调音符与频率对照表

同样的频率,在不同的音调里面有不同的对应频率。
比如D调的低音1 DO在C调里面是2 RE,频率都是294Hz,如图所示,更多的对照表都在上面的链接中了。
之所以说明这个,是想提个醒,如果想让曲子的音调更准,需要对每个曲子的中低高音进行曲调的适配。
2.2 拍号与拍数
拍号( n m {\frac n m} mn )为曲谱小节书写标准。拍数为小节可书写的音符数。
1 4 {\frac 1 4} 41拍是4分音符为一拍,每小节1拍。
4 4 {\frac 4 4} 44 拍是4分音符为一拍,每小节4拍,可以有4个4分音符。
在 4 4 {\frac 4 4} 44 拍号下,一个标准小节时间为严格4拍,即仅能谱写4个四分音符(或等效的其他音符组合)。拍数与拍号,仅影响曲谱的小节音符书写规范,即一个小节存在多少拍(四分音符),与曲速无关。
2.3 简谱写法
如果一个曲子的拍号为 4 4 {\frac 4 4} 44 拍是4分音符为一拍,每小节4拍,那么就有:
| 音符名称 | 写法 | 拍数 |
|---|---|---|
| 全音符 | 4 − − − {4}{-}{-}{-} 4−−− | 四拍 |
| 二分音符 | 4 − − {4}{-}{-} 4−− | 二拍 |
| 四分音符 | 4 {4} 4 | 一拍 |
| 八分音符 | 4 ‾ \underline{4} 4 | 半拍 |
| 十六分音符 | = 4 \overset{4}{=} =4 | 四分之一拍 |
2.4 BPM
在音乐中,时间被分成均等的基本单位,每个单位叫做一个"拍子"或 称一拍。
BPM是Beat Per Minute的简称,中文名为拍子数,释义为每分钟 节拍数,也反映了曲速。
有的曲子会在谱子前面表明BPM,也有的不标。
拍子的时值是以音符的时值来表示的,一拍的时值可以是四分音符(即以四分音符为一拍),也可以是二分音符(以二分音符为一拍)或八分音符(以八分音符为一拍)。
2.5 代码谱曲
这首曲子是 4 4 {\frac 4 4} 44 拍,两条下划线的两个音符是两个十六分音符,算一个八分音符,也就是半拍,以此类推。可以看到用竖线分割的部分,每个部分都为4拍,参照[2.2.2 简谱写法](#2.2.2 简谱写法)。
依据曲子的调调和对应频率表,在beep.h文件中,用#define替换频率和音调名称。
c
//定义低音音名 (单位:HZ)
#define L1 262
#define L2 294
#define L3 330
#define L4 349
#define L5 392
#define L6 440
#define L7 494
//定义中音音名
#define M1 523
#define M2 587
#define M3 659
#define M4 698
#define M5 784
#define M6 880
#define M7 988
//定义高音音名
#define H1 1047
#define H2 1175
#define H3 1319
#define H4 1397
#define H5 1568
#define H6 1760
#define H7 1976
继续在beep.h文件中,用typedef定义用于存曲目的结构体和用来定义全音符时值的T。
100BPM,4/4拍,一拍为四分音符,一个四分音符600ms,全音符2400ms
c
//全音符所占的时值,单位ms,决定乐谱演奏速度
#define T 2400 //100BPM,4/4拍,一拍为四分音符,一个四分音符600ms,全音符2400ms
//#define T 2000 //120BPM,4/4拍,一拍四分音符,一个四分音符500ms,全音符2000ms
typedef struct
{
short nName;//音名:取值L1~L7、M1~M7、H1~H7分别表示低音、中音、高音的1234567,取0表示休止符
short nTime;//时值:取值T、T/2、T/4、T/8、T/16、T/32分别表示全音符、二分音符、四音符、八音符...取0表示演奏结束
}tNote;
在beep.c文件中,声明const tNote类型的数组,用于存储曲目《兰花草》。
c
const tNote LanHuaCao[]=
{
{L6,T/8},{M3,T/8},{M3,T/8},{M3,T/8},{M3,T/4},{M3,T/8},{M2,T/8},
{M1,T/8},{M1,T/16},{M2,T/16},{M1,T/8},{L7,T/8},{L6,T/4},
{M6,T/8},{M6,T/8},{M6,T/8},{M6,T/8},{M6,T/4},{M6,T/8},{M5,T/8},
{M3,T/8},{M5,T/8},{M5,T/8},{M4,T/8},{M3,T/2},
{M3,T/8},{M6,T/8},{M6,T/8},{M5,T/8},{M3,T/4},{M3,T/8},{M2,T/8},
{M1,T/8},{M2,T/8},{M1,T/8},{L7,T/8},{L6,T/4},{L3,T/4},
{L3,T/8},{M1,T/8},{M1,T/8},{L7,T/8},{L6,T/4},{L6,T/8},{M3,T/8},
{M2,T/8},{M2,T/16},{M1,T/16},{L7,T/8},{L5,T/8},{L6,T/2},
///////////////////////////////////////////////////////////////
{L6,T/8},{M3,T/8},{M3,T/8},{M3,T/8},{M3,T/4},{M3,T/8},{M2,T/8},
{M1,T/8},{M1,T/16},{M2,T/16},{M1,T/8},{L7,T/8},{L6,T/4},
{M6,T/8},{M6,T/8},{M6,T/8},{M6,T/8},{M6,T/4},{M6,T/8},{M5,T/8},
{M3,T/8},{M5,T/8},{M5,T/8},{M4,T/8},{M3,T/2},
{M3,T/8},{M6,T/8},{M6,T/8},{M5,T/8},{M3,T/4},{M3,T/8},{M2,T/8},
{M1,T/8},{M2,T/8},{M1,T/8},{L7,T/8},{L6,T/4},{L3,T/4},
{L3,T/8},{M1,T/8},{M1,T/8},{L7,T/8},{L6,T/4},{L6,T/8},{M3,T/8},
{M2,T/8},{M2,T/16},{M1,T/16},{L7,T/8},{L5,T/8},{L6,T/2},
{0,0}//结束
};
3 PWM波音乐输出
3.1 STM32定时器基础
想要确定定时器的定时频率,一般只需要确认三个量:定时器时钟频率、PSC预分频值、ARR自动装载值。
定时器时钟频率: 需要确认定时器的时钟源,来自哪里APB1还是APB2,高级定时器或可引入外部时钟源;有没有进行TIM_ClockDivision分频(基本定时器没有)。
PSC预分频值: 预分频器PSC,有输入时钟CK_PSC和输出时钟CK_CNT(输出给CNT计数器用来和ARR比较计数);一般为16位,实现0-65535分频。
ARR自动装载值: 自动重载寄存器ARR,用来存放与计数器CNT的比较值。计数器CNT有三种方式,向上、向下、向上向下混合(用来中心对齐PWM)。
定时器频率计算公式:
T o u t = ( a r r + 1 ) ∗ ( p s c + 1 ) T c l k Tout= \cfrac{(arr+1)*(psc+1)}{Tclk} Tout=Tclk(arr+1)∗(psc+1)
ARR和PSC配置完毕,除非更改ARR或PSC,否则其他的该定时器下所有通道都是同频的,包括PWM波和定时器中断。
想要确定占空比,只需要关注一个量就可以了:比较寄存器CCR 值。
当计数器CNT (被ARR值限制幅度)与比较寄存器CCR值相等时,参考信号OCxREF的信号的极性就会改变,产生比较中断CCxI,相应的标志位 CCxIF(SR 寄存器中)会置位。然后 OCxREF 再经过一系列的控制之后就成为真正的输出信号 OCx/OCxN。
占空比计算公式有:
D = c c r a r r + 1 D= \cfrac{ccr}{arr+1} D=arr+1ccr
该变量一般在输出时,通过TIM_SetComparex(TIMx,ccr);来配置。
3.2 代码播放
beep.c文件,配置定时器和输出引脚:
c
//如果是原子的代码,需要注意起sys.c中对于APB时钟的配置
//虽然TIM3在APB1总线上,APB1总线一般为36MHz,但定时器有倍频,TIM3时钟应为72MHz
//配置PB5引脚,并将其重定义为TIM3 CH2
void TIM3_PWM_Init(u16 arr,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStr;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrue;
TIM_OCInitTypeDef TIM_OCInitStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能定时器3时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO ,ENABLE);//RCC_APB2Periph_AFIO 涉及到重定义引脚必须使能AFIO时钟
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);//TIM3部分重映射
GPIO_InitStr.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStr.GPIO_Pin=GPIO_Pin_5;
GPIO_InitStr.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStr);//初始化GPIO
TIM_TimeBaseInitStrue.TIM_ClockDivision=TIM_CKD_DIV1;//设置时钟分割 00 为不分割,即TIM3为APB1时钟
TIM_TimeBaseInitStrue.TIM_CounterMode=TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseInitStrue.TIM_Period=arr;//自动重装载值
TIM_TimeBaseInitStrue.TIM_Prescaler=psc;// 预分频值
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStrue);//根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM2;//选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStruct.TIM_OCNPolarity=TIM_OCPolarity_Low;//输出极性:TIM输出比较极性低
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;//比较输出使能
TIM_OCInitStruct.TIM_Pulse=0;
TIM_OC2Init(TIM3,&TIM_OCInitStruct);//根据T指定的参数初始化外设TIM3 OC2
TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable);//使能TIM3在CCR2上的预装载寄存器
TIM_ARRPreloadConfig(TIM3,ENABLE);//使能TIM3在ARR上的预装载寄存器
TIM_Cmd(TIM3,ENABLE);//使能TIM3
}
虽然TIM3在APB1总线上,APB1总线一般为36MHz,但定时器有倍频,TIM3时钟应为72MHz。
c
//蜂鸣器发出指定频率声音
void beepSound(unsigned short usFrep)
{
GPIO_InitTypeDef GPIO_InitStr;
unsigned long ulVal;
//限频,防止数值非法,16位寄存器ARR 65536个数 定时器配置初始配置PSC为71
//arr+1=Tout*Tclk/(psc+1)
//根据本文实际情况简化得:arr = 1000000*Tout-1
//人耳频率范围20Hz-20000Hz(20kHz),限20kHz
if((usFrep<=1000000/65536UL)||(usFrep>20000)){
beepQuiet();//静音
}
else{
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);//TIM3部分重映射
GPIO_InitStr.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStr.GPIO_Pin=GPIO_Pin_5;
GPIO_InitStr.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStr);//初始化GPIO
ulVal=1000000/usFrep;
TIM3->ARR=ulVal;//设置自动重装载寄存器周期的值(音调)
//占空比设置与CCR寄存器和ARR寄存器有关,ulVal/2为占空比50%
TIM_SetCompare2(TIM3,ulVal/2);//设置比较值,调节占空比(音量)
TIM_Cmd(TIM3,ENABLE);//使能TIM3
}
对于输出期望频率的arr设定,有计算公式:
a r r = p s c + 1 T c l k ∗ T o u t − 1 arr= \cfrac{psc+1}{Tclk*Tout}-1 arr=Tclk∗Toutpsc+1−1
其中Tout为期望输出频率。
main函数设定定时器时配置psc为71,
故 a r r = 1000000 ∗ T o u t − 1 arr=1000000*Tout-1 arr=1000000∗Tout−1,
配置usFrep<=1000000/65536UL防止数值非法超过16位寄存器限制;
人耳频率范围20Hz-20000Hz(20kHz),限20kHz,
故usFrep>20000。
在beep.c文件,有演奏乐曲函数:
c
//演奏乐曲
void musicPlay(const tNote Note[])
{
while(1)
{
if(Note->nTime == 0)break;
beepSound(Note->nName);
delay_ms(Note->nTime);
Note++;
beepQuiet();
delay_ms(10);
}
}
在beep.h文件中,extern曲目数组(C99或不用extern)和声明函数musicPlay.
c
extern const tNote LanHuaCao[];
void musicPlay(const tNote Note[]);
main函数:
c
int main(void)
{
delay_init(72);
TIM3_PWM_Init(14399,71);//初始化PSC71,ARR值还要再更改,不重要
while(1){
musicPlay(&LanHuaCao[0]);//将指针放入,不能直接书写数组,新产生的实参会占用内存
delay_ms(1500);
}
}
3.3 原理示意
同响度不同音调和同音调不同响度的PWM波如图所示,图仅示意,不代表实际情况。


4 后记
本文为前文的扩充与修正,加强了对于定时器ARR、PSC、CNT、CRR和代码的一些讲解,使得文章更有深度,内容也更加得准确。
如有可能,这将是STM32加强篇系列的开山篇,未来将会编写更多关于STM32加强学习的案例,敬请期待。
如有错误,还请在评论区斧正。