在图3-20所示仿真电路中,同时使用了TIMER0、TIMER1及INT0中断。按下K1时14个音符组成的音阶将被逐一输出,输出控制由T0中断函数实现。如果输出端连接了虚拟示波器,可观察到输出信号脉宽逐步缩小、频率不断升高的变化过程。如果CPU因连接虚拟示波器而过载,导致声音播放失真,建议断开虚拟示波器再播放。K2用于播放/停止当前音乐片段;K3用于音乐片段选择。在播放音乐片段时,所设计程序中添加了节拍控制,实现了较为逼真的演播效果。
图3-20 TIMER0、TIMER1及INT0控制音阶及多段音乐输出电路
本案例的T0、T1均工作于12T模式0。程序分别使用T0、T1中断控制音阶及音乐片断输出。无论是音阶输出还是音乐片段输出,均要先获取各音符信号频率对应的定时器延时(定时)值。音符信号频率及对应的定时器12T模式0下的定时/计数初值(16位自动重装载)如表3-7所示。
音符信号周期: p >1/ f >1 000 000
音符信号半周期: P > p /2=1/ f >1 000 000/2
12T模式0定时/计数初值:TxMS=65 536- P >(FOSC/12/1 000 000)
定时器每两次连续中断触发才会形成一次完整的蜂鸣器振荡信号输出(1次输出高电平,1次输出低电平,可通过取反、取非、异或1实现),针对给出的音符信号频率对应的周期,实际中断触发周期(定时时长)应设定为音符信号周期的一半(半周期)。
在12MHz系统时钟下仿真输出音符时,可能会有卡顿现象,为此本案例将FOSC下调为6MHz,通过Excel表格计算,可得各音符定时/计数初值对应的常量数组定义如下:
code u16 TONE1_LIST[]={ 0, 65058, 65110, 65156, 65177, 65217, 65251, 65283, 65297, 65323, 65346, 65357, 65376, 65394, 65408 };
数组中除最前面填充的0以外,后面越大的初值意味着越快的累加至溢出,从而对应于越高的信号频率输出。为便于编程选择,表3-7对两种频率均给出了各音符的定时/计数初值。
表3-7 音符信号频率及定时器在12T模式0下的定时/计数初值(16位自动重装载)
① 控制音阶。
当按下K1时,for循环语句控制14个音符逐一输出,循环控制变量Tone_i同时也是当前音符序号变量。for循环语句内首先将TR0置1,启动T0。T0持续计数累加直至溢出,自动触发中断。在每趟for循环的400ms延时期间,T0中断将被持续反复触发。T0中断子程序Timer0_INT中的语句SPK ^=1输出音频信号。在PRE_i变量的控制下,每个音频信号仅在首次被软件重置定时/计数初值。在同一音频信号持续400ms的播放过程中,T0的每次溢出重载都由其模式0的16位自动重载功能实现。400ms后,主程序将TR0置0,使当前第i个音符信号输出停止,随后延时语句使音频输出停顿50ms,从而形成了14个音符信号每个音符输出达400ms便停顿50ms的演奏效果。
② 控制多段音乐输出。
当按下K2时,主程序将TR1置1,启动T1。在定时/计数溢出时,T1中断子程序Timer1_INT被调用。T1中断子程序内部代码工作原理与T0控制音阶输出相似,其差别主要有两个:一是主程序内不是固定400ms延时,而是用delay_ms(500 * Len[Song_idx][Tone_idx])使延时动态变化,从而形成不同节拍;二是重装载定时/计数初值不是直接从各音符信号定时/计数初值表中读取,而是先通过i=Song[Song_idx][Tone_idx]获取当前音符(节拍)索引,然后再去读取对应的定时/计数初值并重装。
另外,由于3段音乐长度不同,不宜用for循环语句控制。为此本案例程序在各段音乐数组末尾添加循1(0xFF)作为结束标志。当while循环语句遇到该标志时即认为一段音乐播放结束。while循环语句的条件中还添加了K2==1 && TR1==1,这样可使得按下K2时(指播放/停止按键被按下时),音乐播放可提前停止。另外,当按下K3选择播放音乐片段时,触发的中断使TR1置0,可见按下K3选择音乐片段时也能使音乐播放停止。关于K3触发的INT0中断子程序编写这里不再赘述。
① 另添加一段自编音乐数据并测试将其播放效果。
② 添加一组LED,使点亮的LED个数与当前输出音符信号频率同步变化。
③ 添加键盘矩阵,实现简易电子琴演奏功能。