这部分我们将会将按键与外部中断放在一起学习,实现音乐盒的切换歌曲-切歌。
首先我们先看看按键的电路图

考虑到51单片机的内部结构,这个按键只能在P1-P3口上用(自带上拉电阻),如果想在P0口上用,需要加上拉电阻(注意P0-3口三者IO内部结构是不同的)。
这里我们用P3.2口作为按键输入口,从电路图里,我们看到,
按键松开的时候,通过内部上拉输入为高电平,读取P3.2输入为1
按键按下的时候,通过外部短路接地为低电平,读取P3.2输入为0
下面我们先用按键,实现两种切歌功能:
1。电平控制
松开按键的时候,播放歌曲1,按下按键播放歌曲2,一直按着会一直播放歌曲2,直到松开后又播放歌曲1
2。边沿控制
每按下一次按键,切换一次歌曲
是不是有点懵逼,这两个功能难道不一样吗?????
NO。NO。NO。
下面我们看一下对比图:

看了是不是能明白,两种模式的玩法,其实这个在Linux的epoll网络编程里也有类似的玩法,电平触发还是事件触发模式,本质上是输入信号与输出状态的映射方式。
电平模式是输入什么状态会直接映射到输出上去,比如一直按着按键同时一直触发播放歌曲2,松开按键后改变状态,同时切歌到播放歌曲1。
边沿模式是不断检测输入变化从而触发输出状态变化,比如每按下一次按键,会产生一个下降沿触发,然后去改变播放歌曲的状态。
我们先做一个假设,已经实现了播放歌曲的函数(后面我们会想办法实现代码封装)
void playMusic(unsigned char *music,unsigned char i)//music为乐谱指针,i为播放第几个音符。
code unsigned char music1[]={} //歌曲1乐谱
code unsigned char music2[]={} //歌曲2乐谱
然后我们电平模式的切歌玩法怎么玩呢?
我们看一下代码吧(为了方便,这里代码跟前一小节略有不同,我们稍后会重构整个代码)
sbit KEY=P1^0;
//原始代码
while(1){
i=0;
while( i<33 ){
playMusic(music1,i);
i=i+1;
}
//修改后代码
while(1){
i=0;
while( i<33 ){
if(KEY==1)
playMusic(music1,i);
else
playMusic(music2,i);
i=i+1;
}
}
上面我们完成了通过判断KEY的状态,然后用一个简单的if—else分支就可以解决电平模式的按键切歌。那问题来了,如果我们想要实现边沿触发的模式怎么玩呢?
让我们想一想,边沿触发是一种变化检测,也就是我们要检测按键按下的这个动作变化,所以我们需要按键状态的去做判断,这就意味着我们需要两个值做对比。

于是我们可以实现一个函数,当检测到按键按下时,会返回1,其他的情况返回0
char readKey(void){
static char last=1;//默认初始化
char now=KEY; //读取当前状态
char ret=0;
if(last==1 && now==0)//上次为高电平,这次为低电平
ret=1;
last = now;//保存这次状态,方便下次使用
}
如果按键为多个,我们怎么修改代码呢
unsigned char readKey(void){
static unsigned char last=0xFF;//默认初始化,8个全部为1
unsigned char now=KEY; //读取当前状态
unsigned char ret;
ret = (last^now) & last;
return ret
}
//其中ret里的每一位代表当前有没有按键按下
我们现在完成了按键的状态检测,下面如何实现切歌呢,有的同学觉得,是不是可以这样玩。。。
//修改后,代码
while(1){
i=0;
while( i<33 ){
if(readKey()==1)
playMusic(music1,i);
else
playMusic(music2,i);
i=i+1;
}
}
NO! NO! NO!
这样写,会导致只有在按下按键的那一刻,播放music1,其他时候都播放music2。
我们需要一个全局状态变量,来确定目前在播放哪首歌曲
char curMusic=0;
#define MAX_MUSIC 2
//修改后,代码
while(1){
i=0;
while( i<33 ){
//切歌状态
if(readKey()==1){
curMusic+=1;//顺序切歌
if (curMusic >= MAX_MUSIC )
curMusic=0;
i=0;//从头播放
}
//根据状态播放歌曲
switch(curMusic){
case 0:
playMusic(music1,i);
break;
case 1:
playMusic(music2,i);
break;
default:
playMusic(music1,i);
break;
}
i=i+1;
}
到目前为止,我们实现了,用两种方式实现切歌的功能。
那问题来了。不是要讲外部中断检测按键吗??外部中断能做什么。。。。
我们正常设计一个单片机死循环程序一般的套路是
while(1){
task1_run();
task2_run();
task3_run();
}
上面的代码里有3个任务,比如我们上面音乐盒里面的任务(检测按键,播放音符等),我们不妨做个假设,任务1执行时间2ms,任务2执行时间3ms,任务3执行时间5ms,由于程序结构内部存在if-else,还回导致时间偏差,这样带来的问题就是我们整个循环的周期大概在10ms左右。
那假如我有一个很重要很重要的任务呢,一旦发生必须以最快速的速度进行,比如某村长突然接到某县长的任务,他一定是放下现在的一切事务,转而去做那件重要的事,因为关系到乌纱帽,这个时候,如果在原来大循环的结构下,就会发生,村长被撤职的问题,因为在接到县长任务后,他会依然选择执行完自己手头的任务,而不是立刻响应县长,所以不撤才怪!!
如果我放下自己手头的任务,立即响应更高级更紧急的任务,这就是中断,因为老子现在手里的活都停下来了,只为了服务好你!
那问题来了,在按键切歌这里我们怎么用上外部中断呢
之前我们检测IO按下状态需要用代码区实现,那现在就不需要了,因为单片机外部中断硬件会自动检测对应的IO状态,一旦符合中断条件,就会打断我们的主程序调用中断服务程序,所以我们可以试试。
变更后的代码:
void EXT0_ISR() interrupt 0 using 1
{
curMusic++;//顺序切歌
if (curMusic >= MAX_MUSIC )
curMusic=0;
}
//修改后,代码
while(1){
i=0;
while( i<33 ){
//根据状态播放歌曲
switch(curMusic){
case 0:
playMusic(music1,i);
break;
case 1:
playMusic(music2,i);
break;
default:
playMusic(music1,i);
break;
}
i++;
}
}
中断带来的好处是什么呢??
实际的外部中断,简单点说,就是响应速度快,一般用于快速外部事件处理上,比如报警,过压过流上。
就好比刚才我们举得3个任务总共10ms的例子,如果又来一个紧急任务,一旦出事,立马响应,你会发现,不管怎么玩,至少是10ms才能响应,要知道如果是开关电源过压或者短路过流,这个时间,已经BOOM爆炸了。。。。。
在我们这里暂时体现不出来,因为切歌按键本身不是一个紧急事件,不需要快速响应。
我自己觉得,其实最近单片机这两小节讲的东西,技术上没有什么,都是一些很基本的知识,不写吧,觉得缺点什么,写吧,又感觉太基础没逼格。。哈哈。
但是,但是,但是,我用三个但是来强调一下,这里主要说2点:
1。代码格式问题
变量与函数命名,这个问题算不上是技术,但是非常关键比如
我们是用驼峰命名,还是下划线命名
以函数名为例
驼峰法 readKey
下划线 read_key
这两种命名没有优劣,但是一个工程里,最好是统一为一种。
还有一个问题,千万不宜用中文拼音命名,比如
上面的函数 duQuAnAian
难道你自己不觉得有什么不对吗?函数和变量,都不宜用中文拼音命名,万一你开源被外国有人看了,会一脸懵逼的!
还有就是{}有两种玩法
//一种是
while(1){
//空4格,代码块
}
//另一种
while(1)
{
//空4格,代码块
}
唯一的区别就是,第一个{是放在同行,还是另一一行,各有各的好 第一个,代码短,第二个看着更整齐 我的建议是,两种都可以,随你喜欢,但是4个格务必要空出来!!!!!
但是切记不要如下码代码,乱成一锅粥了。。。。谁看了都想踹两脚
while(1)
{//你播放哪首歌曲,从那开始播放
gPlayState.curFu=0;
PlayModeRun();
//播放一首歌曲
while(gPlayState.curFu*3<100){
ReadPlayState();//读取音乐盒状态
Song(MUSIC[gPlayState.curSong]);//播放一个音符
LED_BlingMode();//LED 闪灯模式
gPlayState.curFu++;
}
}
2。函数封装问题
我们不管是做Web,还是做嵌入式,随时都要想着一个问题,就是控制代码的复杂度,必须要考虑这个事情,这个非常重要。
最基本的控制复杂度方式,就是封装函数,将类似行为的代码抽象出来作为函数,将可变因素作为函数参数,从而实现代码复用。
紧接着是模块化封装,将某些功能相对接近或者集中的,封装成相对集中的模块,模块对外部提供不同的函数接口。这里一般是用.c和.h文件构建成一个模块。
再接着就是根据系统层次把相近的模块放到一层级,进行分层设计,具体可以看我之前的写得这篇文章。
三郎:嵌入式小书1-系统分层设计zhuanlan.zhihu.com我觉得重构代码,是一个软件工程师时时刻刻都要考虑的事情,
写出优雅的代码来,就是你生命中最美的时刻!
愿我们,一路同行,成就更好的自己!!!!!!!!