本次要实现的是利用ZYNQ-7000板子上自带的16个矩阵键盘来实现每按一次发出一个音符的声音,从而实现电子琴的效果。
一、原理部分:
矩阵键盘:用8位来存储4*4的矩阵键盘的信息,初始化的值为00001111;
先将高四位设为输出,低四位设为输入,当第一行有按键按下时 ,第四位相应的位值变为0,但此时只能判定是哪一行的按键被按下,并不能判断具体是哪一个。
将高四位设为输入,低四位设为输出,对应的那一列的值变为1,从而获取按下的按键值。
扬声器发生原理:计算机中发声普遍使用的是MIDI音乐,MIDI不是具体发声的部件,只是编码来驱动相应发声器件发声的。
本实验中发声的端口是DR6,DR6输出高电平会按照最高频率发声,按照固定频率输出高低电平会发出固定频率的声音。
为了得到准确的频率就需要能够准确的定时得到T/2的时间间隔,DR6在每次定时时间到之后将输出反向,即可得到准确的方波脉冲也就可以得到准确的发声频率。
音乐基本理论:
(1)音调:
音阶为等比数列,即下一个音阶高八度的频率是上一个音阶频率的两倍。国际标准音规定,钢琴的a1的频率是为440Hz;
又规定每相邻半音的频率比值为2^1/12≈1.059463,根据这规定,就可以得出钢琴上每一个琴键音的频率。简谱中的1、2、3、4、5、6、7对应钢琴琴键C、D、E、F、G、A、B由于每个音阶是12个半音所以这七个音调之间不全是半音关系即频率倍数为1.059463,只有E和F以及B和C1之间是半音关系即1.059463倍频率。C、D、E之间会有两个半音标记为#C和#DF、G、A、B之间会有三个半音标记为#F、#G和#A。
(2)节拍:在MIDI演奏中小节可以不作为参数考虑进程序中,而一拍的时间长度由演奏速度决定,如每分钟60拍则代表每拍时长1s。采用定时器进行精确定时用以产生对应频率的声音根据节奏和演奏速度决定某个频率的声音要演奏的时间长度每一个演奏的频率不能占100%的时长应该有间隔用来模仿人演奏时的音符间隔。
二、VIvado部分:
块设计部分:用一个一位的AXI GPIO来接发声的口(设为输出),用一个8位的AXI GPIO的IP来连接矩阵键盘(双向的),允许中断连接到PS上->创建HDL Wrapper->综合->配置管脚约束->生成bit流->export hardware->启动SDK。
I/O ports 在初始化设定pull type时,pullup 默认值为1,pulldown默认值为0,根据我们的设定,我们要将初始化值设为相反的即不输出信号的值。
三、代码实现部分(详解):
钢琴琴键:
#include "xil_exception.h"
//System Timer Tick
#define TIMER_FRQ 333333333 //计数频率同时也是1s的周期数
//钢琴琴键10倍频率,每秒中断间隔次数的5倍
#define KEY_A2 275
#define KEY_SA2 291
#define KEY_B2 309
#define KEY_C1 327
#define KEY_SC1 346
#define KEY_D1 367
#define KEY_SD1 389
#define KEY_E1 412
#define KEY_F1 437
#define KEY_SF1 462
#define KEY_G1 490
#define KEY_SG1 519
#define KEY_A1 550
#define KEY_SA1 583
#define KEY_B1 617
#define KEY_C 654
#define KEY_SC 693
#define KEY_D 734
#define KEY_SD 778
#define KEY_E 824
#define KEY_F 873
#define KEY_SF 925
#define KEY_G 980
#define KEY_SG 1038
#define KEY_A 1100
#define KEY_SA 1165
#define KEY_B 1235
#define KEY_c 1308
#define KEY_Sc 1386
#define KEY_d 1468
#define KEY_Sd 1556
#define KEY_e 1648
#define KEY_f 1746
#define KEY_Sf 1850
#define KEY_g 1960
#define KEY_Sg 2077
#define KEY_a 2200
#define KEY_Sa 2331
#define KEY_b 2469
#define KEY_c1 2616 //中音哆
#define KEY_Sc1 2772
#define KEY_d1 2937
#define KEY_Sd1 3111
#define KEY_e1 3296
#define KEY_f1 3492
#define KEY_Sf1 3700
#define KEY_g1 3920
#define KEY_Sg1 4153
#define KEY_a1 4400
#define KEY_Sa1 4662
#define KEY_b1 4939
#define KEY_c2 5233
#define KEY_Sc2 5544
#define KEY_d2 5873
#define KEY_Sd2 6223
#define KEY_e2 6593
#define KEY_f2 6985
#define KEY_Sf2 7400
#define KEY_g2 7840
#define KEY_Sg2 8306
#define KEY_a2 8800
#define KEY_Sa2 9323
#define KEY_b2 9878
#define KEY_c3 10470
#define KEY_Sc3 11090
#define KEY_d3 11750
#define KEY_Sd3 12450
#define KEY_e3 13190
#define KEY_f3 13970
#define KEY_Sf3 14800
#define KEY_g3 15680
#define KEY_Sg3 16610
#define KEY_a3 17600
#define KEY_Sa3 18650
#define KEY_b3 19760
#define KEY_c4 20930
#define KEY_Sc4 22170
#define KEY_d4 23490
#define KEY_Sd4 24890
#define KEY_e4 26370
#define KEY_f4 27940
#define KEY_Sf4 29600
#define KEY_g4 31360
#define KEY_Sg4 33220
#define KEY_a4 35200
#define KEY_Sa4 37290
#define KEY_b4 39510
#define KEY_c5 41860
#define KEY_NOP 0
/*
* //几分音符*/
#define Beat_1Q1 1
#define Beat_1Q2 2
#define Beat_1Q4 4
#define Beat_1Q8 8
#define Beat_1Q16 16
//Musical note
typedef struct Music_Note
{
u32 Note;
u8 Beat;
float Dotted;
} Muc_N;
初始化两个GPIO的配置以及初始化定时器的代码解析部分在这里就省略了。
当有键按下时允许全局中断,并且将flag值设为1.
void GpioHandler(void *CallbackRef)
{
XGpio *GpioPtr = (XGpio *)CallbackRef;
XGpio_InterruptGlobalDisable(&Gpio_BTN);
XGpio_InterruptClear(&Gpio_BTN,INTR_MASK);
ReadData=XGpio_DiscreteRead(&Gpio_BTN, LEDS_CHANNEL)&0xF;
if((ReadData&0xF)!=0xF)
{
IntrFlag=1;//全局变量
}
else
{
IntrFlag=0;
XGpio_InterruptGlobalEnable(&Gpio_BTN);
}
}
按下矩阵键盘,获取按下键的值:
XGpio_SetDataDirection(&Gpio_BTN, SEG7_CHANNEL, LOW4_MASK); //将独立按键的4位引脚设置为输入
XGpio_SetDataDirection(&Gpio_BTN, LEDS_CHANNEL, LOW4_MASK); //将矩阵键盘的8位分为高四位输出低四位输入
XGpio_DiscreteWrite(&Gpio_BTN, LEDS_CHANNEL, LOW4_MASK); //在高四位输出'0'
for(int i=0;i<Delay_Time;i++);//延迟再读一次
u8 GpioData=XGpio_DiscreteRead(&Gpio_BTN, LEDS_CHANNEL)&0xF;
u8 Leds_Group;
if(GpioData==ReadData) //如果两次读的一样,就说明是按键而不是抖动产生的,如果不相等则要滤掉
{
XGpio_SetDataDirection(&Gpio_BTN, LEDS_CHANNEL, 0XF0); //将矩阵键盘的8位分为高四位输入低四位输出
XGpio_DiscreteWrite(&Gpio_BTN, LEDS_CHANNEL, 0XF0); //在低四位输出'1'
GpioData=XGpio_DiscreteRead(&Gpio_BTN,LEDS_CHANNEL)&0xF0;
针对每一个按键发出一个音符:
switch(ReadData& 0x0F)
{
case 0x0E:
switch(GpioData& 0xF0)
{
case 0xE0:
LoadIntTime(KEY_c2,Beat_1Q4,0.9);//根据音符设定相应的定时器频率
XScuTimer_LoadTimer(&TimerInstance, ReloadTime);//重装定时器
XScuTimer_Start(&TimerInstance);//启动定时器
/*后面的十五种情况省略*/
}
}
根据设定的音符和节拍,计算出定时器中断的频率以及停止时间:
void LoadIntTime(u32 Note_Play,u8 Beat_Play,float Dotted_Play)
{
ReloadTime=CalReloadTime(Note_Play,Beat_Play,Dotted_Play);
TimerExpired=CalIntTime(ReloadTime,Beat_Play,Dotted_Play);
//ReloadTime=0xFFFFFFFF-100;
}
static u32 CalReloadTime(u32 Note_Play,u8 Beat_Play,float Dotted_Play)
{
if(Note_Play!=KEY_NOP)
{
return TIMER_FRQ/Note_Play*5*Dotted_Play;
}
else
{
return TIMER_FRQ/Beat_Play/PLAY_SPEED*MUS_BEAT*60*Dotted_Play;//
}
}
static u32 CalIntTime(u32 Note_Play,u8 Beat_Play,float Dotted_Play)
{
return CalReloadTime(0,Beat_Play,Dotted_Play)/Note_Play;
}
定时器处理函数,并输出扬声器电平,当TimerExpired减为0时将定时器停下来:
static void TimerIntrHandler(void *CallbackRef)
{
XScuTimer *TimerInstancePtr = (XScuTimer *) CallbackRef;
if (XScuTimer_IsExpired(TimerInstancePtr))
{
XScuTimer_ClearInterruptStatus(TimerInstancePtr);
TimerExpired--;
WriteData=~WriteData;
if(TimerExpired==0)//该音符已经演奏完毕
{
XScuTimer_Stop(&TimerInstance);
WriteData=0;
}
XGpio_DiscreteWrite(&Gpio_DEV, 1, WriteData);//输出扬声器电平
}
}