音阶频率对照表
百度就可以查到,我对照的是下面网址中的:
http://blog.csdn.net/u012266559/article/details/51512616
单片机产生音乐的原理
音乐的产生主要是通过单片机的I/O口输出高低不同的脉冲信号来控制蜂鸣器发音,要想产生音频脉冲信号,需要算出某音频的周期(1/频率),然后将此周期除以2,即为半周期的时间。利用单片机定时器计时这个半周期的时间,每当计时到后就输出脉冲的I/O口反相,这样就在此I/O口上得到此脉冲的频率。
我是直接利用stm32的PWM端口输出PWM波(设置成占空比为50%)就可以实现,关键是每个音阶对应频率的方波如何求。
从下图看,只要理解原理,那么不管单片机的时钟频率多大,我们都可以自己求出相应的音阶所需要的arr值。
程序用到的单片机引脚
我用的是stm32开发板,利用PF9端口输出PWM波,PF8端口连着的是蜂鸣器,所以只需要用杜邦线把PF9和PF8端口连在一起就可以了。
程序中定义的的宏定义
10000的来历上图中有计算过程,然后音阶频率对照表中可以查到
低1 DO 的频率是262,所以我们得到
低1 DO 对应的arr值是 (R/262)-1 ,其余音符对应arr的计算方式同理。
#define R 10000 //84MHz/(psc+1)=10000
#define L1 (R/262)-1 //低1 DO
关于音符0的处理以及改进
在我写完的程序中,我是这样处理的,一旦遇到音符0,让PWM停止输出。有想过关闭TIM14和PORTF时钟,但是没有用——因为用同样的方法,在一首歌结束之后就是一段相同频率的杂音。所以这种方法是不可以让PWM输出停止的。最后换了个方法,把PF9设置成普通IO口而且是输入模式就可以了,就不会有噪声了。
但是这种方法很麻烦,后来想到其实在遇到音符0的时候,只需要将PWM输出频率变大,让蜂鸣器发出一个人耳听不到的超声波就可以了——程序已经写完了,懒得改了。
主要程序代码
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "timer.h"
#include "key.h"
#define ZERO 3000//
#define R 10000 //F_CLOCK/(psc+1)=10000
#define L1 (R/262)-1 //低1 DO
#define half_L1 (R/277)-1 //#1 DO#
#define L2 (R/294)-1
#define half_L2 (R/311)-1
#define L3 (R/330)-1
#define L4 (R/349)-1
#define half_L4 (R/370)-1
#define L5 (R/392)-1
#define half_L5 (R/410)-1
#define L6 (R/440)-1
#define half_L6 (R/466)-1
#define L7 (R/494)-1
#define M1 (R/523)-1 //中1 DO
#define half_M1 (R/554)-1 //#1 DO#
#define M2 (R/587)-1
#define half_M2 (R/622)-1
#define M3 (R/659)-1
#define M4 (R/698)-1
#define half_M4 (R/740)-1
#define M5 (R/784)-1
#define half_M5 (R/831)-1
#define M6 (R/880)-1
#define half_M6 (R/932)-1
#define M7 (R/988)-1
#define H1 (R/1046)-1 //高1 DO
#define half_H1 (R/1109)-1 //#1 DO#
#define H2 (R/1175)-1
#define half_H2 (R/1245)-1
#define H3 (R/1318)-1
#define H4 (R/1397)-1
#define half_H4 (R/1480)-1
#define H5 (R/1568)-1
#define half_H5 (R/1661)-1
#define H6 (R/1760)-1
#define half_H6 (R/1865)-1
#define H7 (R/1967)-1
int flag=0;//标志
int x;
int tune[] =
{
M6,M7,H1,M7,H1,H3,M7,M7,M7,M3,M3,
M6,M5,M6,H1,M5,M5,M5,M3,M4,M3,M4,H1,
M3,M3,ZERO,H1,H1,H1,M7,half_M4,M4,M7,M7,M7,ZERO,M6,M7,
H1,M7,H1,H3,M7,M7,M7,M3,M3,M6,M5,M6,H1,
M5,M5,M5,M2,M3,M4,H1,M7,M7,H1,H1,H2,H2,H3,H1,H1,H1,
H1,M7,M6,M6,M7,half_M5,M6,M6,M6,H1,H2,H3,H2,H3,H5,
H2,H2,H2,M5,M5,H1,M7,H1,H3,H3,H3,H3,H3,
M6,M7,H1,M7,H2,H2,H1,M5,M5,M5,H4,H3,H2,H1,
H3,H3,H3,H3,H6,H6,H5,H5,H3,H2,H1,H1,ZERO,H1,
H2,H1,H2,H2,H5,H3,H3,H3,H3,H6,H6,H5,H5,
H3,H2,H1,H1,ZERO,H1,H2,H1,H2,H2,M7,M6,M6,M6,M6,M7
};
float duration[]=
{
0.5,0.5, 1.5,0.5,1,1, 1,1,1,0.5,0.5,
1.5,0.5,1,1, 1,1,1,1, 1.5,0.5,1,1,
1,1,0.5,0.5,0.5,0.5, 1+0.5,0.5,1,1, 1,1,1,0.5,0.5,
1+0.5,0.5,1,1, 1,1,1,0.5,0.5, 1+0.5,0.5,1,1,
1,1,1,0.5,0.5, 1,0.5,0.25,0.25,0.25,0.5, 0.5,0.5,0.5,0.25,0.5,1,
0.5,0.5,0.5,0.5,1,1, 1,1,1,0.5,0.5, 1+0.5,0.5,1,1,
1,1,1,0.5,0.5, 1.5,0.5,1,1, 1,1,1,1,
0.5,0.5,1,1,0.5,0.5, 1.5,0.25,0.5,1, 1,1,1,1,
1,1,1,1, 1,1,1,1, 0.5,0.5,1,1,0.5,0.5,
1,0.5,0.5,1,1, 1,1,1,1, 1,1,1,1,
0.5,0.5,1,1,0.5,0.5, 1,0.5,0.25,0.5,1, 1,1,1,0.5,0.5
};//这部分是整首曲子的节拍部分,也定义个序列duration,浮点(数组的个数和前面音符的个数是一样的,一一对应么)
int length = sizeof(tune)/sizeof(tune[0]);//这里用了一个sizeof函数, 可以查出tone序列里有多少个音符
//int length;//这里定义一个变量,后面用来表示共有多少个音符
void main(void)
{
Stm32_Clock_Init(336,8,2,7);//设置时钟,168Mhz
delay_init(168); //延时初始化
for(x=0;x//循环音符的次数
{
if(flag==1)
//上一个音符是0,在遇到下一个音符前重新使用IO口的复用功能
{
//RCC->AHB1ENR|=1<<5; //使能PORTF时钟
//RCC->APB1ENR|=1<<8; //使能TIM14时钟
GPIO_Set(GPIOF,PIN9,GPIO_MODE_AF,GPIO_OTYPE_PP,GPIO_SPEED_100M,GPIO_PUPD_PU);//复用功能,上拉输出
GPIO_AF_Set(GPIOF,9,9); //PF9,AF9
flag=0;//将标志置0
}
if(tune[x]==ZERO)
{
//RCC->APB1ENR|=0<<8; //关闭TIM14时钟
//RCC->AHB1ENR|=0<<5;
//关闭PORTF时钟使PF9引脚无法输出PWM波
GPIO_Set(GPIOF,PIN9,GPIO_MODE_IN,GPIO_OTYPE_PP,GPIO_SPEED_100M,GPIO_PUPD_PU);
//将PF9设置成普通IO口,输入
flag=1;//将关闭标志置1
}
GPIO_Set(GPIOF,PIN9,GPIO_MODE_AF,GPIO_OTYPE_PP,GPIO_SPEED_100M,GPIO_PUPD_PU);//复用功能,上拉输出
GPIO_AF_Set(GPIOF,9,9); //PF9,AF9
//设置成复用模式(此处就是PWM端口),输出
TIM14_PWM_Init(tune[x],8400-1); //(arr,psc)
if(flag==1)
delay_ms(300*duration[x]);//否则延时会很长
else
delay_ms(400*duration[x]);
//每个音符持续的时间,即节拍duration
//设置成一个全拍400ms
}
//RCC->APB1ENR|=0<<8; //关闭TIM14时钟
//RCC->AHB1ENR|=0<<5; //关闭PORTF时钟
GPIO_Set(GPIOF,PIN9,GPIO_MODE_IN,GPIO_OTYPE_PP,GPIO_SPEED_100M,GPIO_PUPD_PU);
//把PF9设置成普通IO口输入就可以了,就不会有噪声了。
//delay_ms(10000);//还是会有声音,不知道为什么
//GPIO_AF_Set(GPIOF,9,9); //PF9,AF9
while(1);//防止程序跑飞
}
//TIM14 PWM部分初始化函数
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void TIM14_PWM_Init(u32 arr,u32 psc)
{
//此部分需手动修改IO口设置
RCC->APB1ENR|=1<<8; //TIM14时钟使能
RCC->AHB1ENR|=1<<5; //使能PORTF时钟
//GPIO_Set(GPIOF,PIN9,GPIO_MODE_AF,GPIO_OTYPE_PP,GPIO_SPEED_100M,GPIO_PUPD_PU);//复用功能,上拉输出
//GPIO_AF_Set(GPIOF,9,9); //PF9,AF9
TIM14->ARR=arr; //设定计数器自动重装值
TIM14->PSC=psc; //预分频器分频
TIM14->CCMR1|=6<<4; //CH1 PWM1模式
TIM14->CCMR1|=1<<3; //CH1 预装载使能
TIM14->CCER|=1<<0; //OC1 输出使能
TIM14->CCER|=1<<1; //OC1 低电平有效
TIM14->CR1|=1<<7; //ARPE使能
TIM14->CR1|=1<<0; //使能定时器14
TIM14->CCR1=arr*0.5; //占空比= TIM14->CCR1 / arr(单位:%)
//设置占空比为50%
}