51单片机RAM区域划分
51单片机的RAM分为两个部分,一块是片内RAM,一块是片外RAM。
data: 片内RAM从 0x00 ~0x7F 寻址范围(0-127) 容量共128B
idata: 片外RAM从 0x00~0xFF 寻址范围(0-255) 容量共256B
pdata:片外RAM从 0x00~0xFF 寻址范围(0-255) 容量共256B
xdata:片外RAM从 0x0000~0xFFFF 寻址范围(0-65535)容量共65536B
从上述的范围可以看出,data是idata的一部分,pdata是xdata的一部分。
可以这么定义一个变量啊:unsigned char data a = 0,但事实上我们平时书写的时候是不写data的
因为在Keil默认的设置下,data是可以省略的。
片内RAM的访问速度会比片外的访问速度快,但是一般不用idata 0x80~0XFF这部分范围。因为这块通常用于中断与函数调用的堆栈。所以绝大部分情况下,使用内部RAM的时候,只用data就可以了。
STC89C52共512字节的RAM,分为256字节的片内RAM和256字节的片外RAM。一般情况下使用data区域,如果data不够用了,就用xdata。如果希望程序执行效率尽量高一点,就用pdata关键字来定义。
事实上真正的芯片外扩展很少用到了,虽然它还是叫片外RAM,但实际上它现在也在单片机内部,只是响应速度不太一样而已。
定时炸弹的基本要求
1:利用蜂鸣器鸣叫与点亮LED来表示炸弹爆炸。
2:可以用按键调整定时时间。长按调整按键可以是连续增加或减少定时时间。
3:ESC键清0暂停倒计时,Entel键开始倒计时,到了0秒爆炸。
上代码
#include <reg52.h>sbit BUZZ = P1^6;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
sbit KEY_IN_1 = P2^4;
sbit KEY_IN_2 = P2^5;
sbit KEY_IN_3 = P2^6;
sbit KEY_IN_4 = P2^7;
sbit KEY_OUT_1 = P2^3;
sbit KEY_OUT_2 = P2^2;
sbit KEY_OUT_3 = P2^1;
sbit KEY_OUT_4 = P2^0;unsigned char code LedChar[] = { //数码管显示字符转换表0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[7] = { //数码管+独立LED显示缓冲区0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到标准键盘键码的映射表{ 0x31, 0x32, 0x33, 0x26 }, //数字键1、数字键2、数字键3、向上键{ 0x34, 0x35, 0x36, 0x25 }, //数字键4、数字键5、数字键6、向左键{ 0x37, 0x38, 0x39, 0x28 }, //数字键7、数字键8、数字键9、向下键{ 0x30, 0x1B, 0x0D, 0x27 } //数字键0、ESC键、 回车键、 向右键
};
unsigned char KeySta[4][4] = { //全部矩阵按键的当前状态{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}};
pdata unsigned long KeyDownTime[4][4]= {{0, 0, 0, 0},{0, 0, 0, 0},{0, 0, 0, 0},{0, 0, 0, 0}};bit enBuzz = 0; //蜂鸣器使能标记bit flag1s = 0; //1s定时标志bit flagStart = 0; //倒计时启动标志unsigned char T0RH = 0; //T0重载值高字节unsigned char T0RL = 0; //T0重载值低字节unsigned char CountDown = 0; //倒计时计数器void ConfigTimer0(unsigned int ms); //定时器0初值设定函数void ShowNumber(unsigned long num); //倒计时调整时间,数码管显示函数void KeyDriver();void main(){EA = 1;ENLED = 0;ADDR3 = 1;ConfigTimer0(1); //定时1msShowNumber(0); //数码管显示0while(1){KeyDriver(); //调用按键驱动函数if(flagStart && flag1s) //倒计时启动且1秒定时到达时,处理倒计时{flag1s = 0;if(CountDown > 0) //倒计时未到0时,计时器递减{CountDown--; //ShowNumber(CountDown); //刷新倒计时数字显示if(CountDown == 0){enBuzz = 1; //启动蜂鸣器LedBuff[6] = 0x00; //点亮独立LED;} }}}}/*配置并启动T0,ms-T0定时时间 */void ConfigTimer0(unsigned int ms){ unsigned long tmp; //临时变量tmp = 11059200 / 12; //每秒机器周期数tmp = (tmp * ms)/1000; //计算传递实参的机器周期数tmp = 65536 - tmp ; //设置定时器重载初值tmp = tmp +28; //初值补偿T0RH = (unsigned char)(tmp >> 8); //初值高低字节分离T0RL = (unsigned char)tmp;TMOD &= 0xF0; //清零定时器0控制位TMOD |= 0x01; //选择定时器0的工作模式TH0 = T0RH; //定时器0高低字节赋值TL0 = T0RL; ET0 = 1; //定时器0中断使能TR0 = 1; //使能定时器0}/*将一个无符号长整型的数字显示到数码管伤,num位待显示数字 */void ShowNumber(unsigned long num){signed char i;unsigned char buf[6]; //把长整形数,每个进制位上的数转化成十进制的数共6个存入数组for(i = 0; i <6; i++){buf[i] = num %10;num = num / 10;}for(i = 5;i >=1;i--) //从高位起,遇到0转换为0xff(不显示),遇到非零则退出循环{if(buf[i] == 0 )LedBuff[i] = 0xFF; // 作用:高位是零则不显示elsebreak;}for(; i >= 0; i--) //剩余低位都如实转换成数码管要显示的数{LedBuff[i] = LedChar[buf[i]];}}/* 按键动作函数,根据键码执行相应的操作,keycode 为按键键码 */void KeyAction(unsigned char keycode){if(keycode == 0x26) //向上键,倒计时设定值每按一下加1{ if(CountDown < 9999) //最大计数9999{CountDown++;ShowNumber(CountDown);}}else if (keycode == 0x28) //向下键 倒计时设定值递减{if(CountDown >1) //最小计时1s{CountDown--;ShowNumber(CountDown);}}else if(keycode == 0x0D) //回车键 ,启动倒计时{flagStart = 1;}else if(keycode == 0x1B) //ESC 键 取消倒计时{enBuzz = 0;LedBuff[6] = 0xFF;flagStart = 0;CountDown = 0;ShowNumber(0);}}/*按键驱动函数,检测按键动作,调度相应动作函数,需要在主函数中调用 */ void KeyDriver(){unsigned char i,j;static unsigned char pdata backup[4][4] = { //按键值备份,保存前一次的值{1,1,1,1},{1,1,1,1},{1,1,1,1},{1,1,1,1}};static unsigned long pdata TimeThr[4][4] = { //快速输入执行的时间阈值{1000,1000,1000,1000},{1000,1000,1000,1000},{1000,1000,1000,1000},{1000,1000,1000,1000}};for(i = 0; i<4; i++) //循环扫描4*4的矩阵按键{for(j = 0; j<4; j++){if(backup[i][j] != KeySta[i][j]) //按键动作检查{ if(backup[i][j] != 0) //按键按下时执行{KeyAction(KeyCodeMap[i][j]); //调用按键动作函数}backup[i][j] = KeySta[i][j]; //刷新前一次备份值}if(KeyDownTime[i][j] > 0) //检测执行快速输入{if(KeyDownTime[i][j] >= TimeThr[i][j]){ //达到阈值时执行一次动作KeyAction(KeyCodeMap[i][j]); //调用按键动作函数TimeThr[i][j] += 200; //时间阈值增加200ms,以准备下一次执行}} else // 按键弹起时复位阈值时间{TimeThr[i][j] = 1000; // 恢复1s的初始阈值时间}}}}/*按键扫描函数 ,需要在定时中断中调用 */void KeyScan(){unsigned char i;static unsigned char keyout = 0;static unsigned char keybuf[4][4] = {{0xFF,0xFF,0xFF,0xFF},{0xFF,0xFF,0xFF,0xFF},{0xFF,0xFF,0xFF,0xFF},{0xFF,0xFF,0xFF,0xFF},};//将一行的4个按键值移入缓冲区keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;//消抖后更新按键状态for(i = 0; i < 4; i++){if((keybuf[keyout][i] & 0x0F) == 0x00){//连续4次烧苗值为0,即4x4ms内都是按下状态时,可以认为按键已稳定的按下KeySta[keyout][i] = 0;KeyDownTime[keyout][i] += 4;//按下的持续时间累加}else if((keybuf[keyout][i] & 0x0F) == 0x0F){ //连续4次扫描值为1,即4x4ms内都是弹起状态时,可认为按键已稳定的弹起KeySta[keyout][i] = 1;KeyDownTime[keyout][i] = 0;//按下的持续时间清零}}keyout++; //输出索引递增keyout &= 0x03; //索引值逢4归0switch(keyout) //根据索引,释放当前输出引脚,拉低下次的输出引脚{case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;default: break;}}/* 数码管与LED动态扫描函数,需要在定时中断中调用 */void LedScan(){static unsigned char i = 0; //动态扫描索引P0 = 0xFF; //消除鬼影P1 = (P1 & 0xF8) | i; // 0xF8 = 1111 1000,位选索引值赋值到P1口低3位P0 = LedBuff[i]; //缓冲区中索引位置的数据送到P0口if(i < 6) //索引递增循环,遍历整个缓冲区i++;elsei = 0;}/* T0中断服务函数,完成数码管、按键扫描与定时 */void interruptTimer0() interrupt 1{static unsigned int tmr1s = 0; //1秒定时器TH0 = T0RH;TL0 = T0RL;if(enBuzz)BUZZ = ~BUZZ; //蜂鸣器发声处理else //驱动蜂鸣器发声BUZZ = 1;LedScan(); //关闭蜂鸣器KeyScan(); //LED 扫描显示if(flagStart) //按键扫描{ //倒计时启动时处理1秒定时tmr1s++;if(tmr1s >= 1000){tmr1s = 0;flag1s = 1;}}else{tmr1s = 0; //倒计时未启动时1秒定时器始终归零}}
笔者的博文是单片机学习笔记:开发板和一些源代码都来自金沙滩工作室的产品,如果对代码中所有语句感兴趣,需要相关的资料(原理图,原代码)可以在该处下载,免费的:青岛金思特电子有限公司
代码主体是来自教材,不过一般笔者都会有些扩展。而且这些代码不是复制粘贴的,是笔者一个字一个字敲出来的。主要是笔者的C语言也是初学水平,哈哈哈。如果有小伙伴也用这套教材学习,有问题在相应的博文下可以留言交流下,毕竟初学者才知道初学者的难处。
前文提到了单片机的RAM区域的划分,编译一下程序。可以看到
这里data = 70.3就是片内RAM,xdata = 144是片外RAM。可以看到data不是一个正整数,是因为定义了三个位变量。因此是70.3
看源代码的数组关键字pdata
如果删除该关键字会如何,看下图
然后发现报错了,data的值变大了,xdata的值变小了。前文提到data的容量范围是128B,如果都要存入片内RAM需要加上关键字idata,看下图
可以看到data范围已经超过了128但是没有报错,是因为该数组用上了关键字idata,不过一般不用这个区域,因此本案函数是用pdata关键字。
然后分析一下程序的工作流程:
思维导图的地址 https://docs.qq.com/s/bktVAiM_bl91s3118HZurW
用的是腾讯文档免费的流程图,不过有图形限制因此分成了两章。
看下结果视频倒计时炸弹_哔哩哔哩_bilibili
可以看到功能都有都正常工作了,当然正常的倒计时炸弹是不会有ESC键的,启动按键肯定也不可能是按一下就触发,如果不小心碰到了那就完犊子了,因此Entel必然需要长按触发。程序需要一点改动。
如果对矩阵按键部分逻辑不清楚的可以看一下笔者之前关于矩阵按键的博文
初学51单片机矩阵按键与消抖_矩阵键盘消抖-CSDN博客
初学51单片机矩阵按键与消抖2_单片机矩阵键盘获取键值如何消抖-CSDN博客
初学51单片机之矩阵按键的应用末篇_矩阵按键能做些什么-CSDN博客
本案矩阵部分有一处变化但是主体和之前是一样的,数码管显示部分也包括在里面。因此不在详细分析。
接上述需要改动的有三处:
1:全局变量声明 bit LongPress = 0;//长按标志置0
2:是KeyDriver()函数里面的变化
void KeyDriver(){unsigned char i,j;static unsigned char pdata backup[4][4] = { //按键值备份,保存前一次的值{1,1,1,1},{1,1,1,1},{1,1,1,1},{1,1,1,1}};static unsigned long pdata TimeThr[4][4] = { //快速输入执行的时间阈值{1000,1000,1000,1000},{1000,1000,1000,1000},{1000,1000,1000,1000},{1000,1000,1000,1000}};for(i = 0; i<4; i++) //循环扫描4*4的矩阵按键{for(j = 0; j<4; j++){if(backup[i][j] != KeySta[i][j]) //按键动作检查{ if(backup[i][j] != 0) //按键按下时执行{KeyAction(KeyCodeMap[i][j]); //调用按键动作函数}backup[i][j] = KeySta[i][j]; //刷新前一次备份值}if(KeyDownTime[i][j] > 0) //检测执行快速输入{if(KeyDownTime[i][j] >= TimeThr[i][j]){ //达到阈值时执行一次动作KeyAction(KeyCodeMap[i][j]); //调用按键动作函数TimeThr[i][j] += 200; //时间阈值增加200ms,以准备下一次执行if(i == 3 && j == 2) //注意entel键是3行2列,不是4行3列因为第1行一1列是0,0{LongPress = 1;}}} else // 按键弹起时复位阈值时间{TimeThr[i][j] = 1000; // 恢复1s的初始阈值时间}}}}
这个位置使能长按标志置1。数组[3][2](4行3列)对应的是Entel键,注意不是[4][3]。因为数组是从[0][0]0行0列开始的,一开始笔者也是[4][3]花了笔者不少时间找问题,一度以为是不是逻辑哪里出错了,结果竟然是这个问题。对于初学者来说真是要注意的问题。逻辑认识上某行某列到程序上要减1。
3:KeyAction()函数里的变化
void KeyAction(unsigned char keycode){if(keycode == 0x26) //向上键,倒计时设定值每按一下加1{ if(CountDown < 9999) //最大计数9999{CountDown++;ShowNumber(CountDown);}}else if (keycode == 0x28) //向下键 倒计时设定值递减{if(CountDown >1) //最小计时1s{CountDown--;ShowNumber(CountDown);}}else if(keycode == 0x0D) //回车键 ,启动倒计时{if(LongPress){flagStart = 1;LongPress = 0;}}else if(keycode == 0x1B) //ESC 键 取消倒计时{enBuzz = 0;LedBuff[6] = 0xFF;flagStart = 0;CountDown = 0;ShowNumber(0);}}
进入Entel键把长按标志作为判断条件,实现了长按Entel键开始倒计时。
看结果视频:长按触发倒计时_哔哩哔哩_bilibili
可以看到短按无法触发倒计时了,只能长按才能触发倒计时。
在现实使用时,都希望能够较准确的控制长按时间,如果某个按键造成的后果很严重,必然要让长按的时间足够的长,来体现使用者强烈的主观意志。防止后悔,出现勿碰,不小心的说辞。保护开发者与使用者的基本权益。
本案应该如何操作呢:
看下程序
如图如果开关已经准确的按下了,之后每4个中断执行一次KeyDownTime[keyout][i] += 4;语句,而该语句是每执行一次加4,因此可以认为是每次进入中断加1。
KeyDownTime的值在KeyDriver();函数中与预先设定的值1000判断,因此可知:当按住开关,再经过1000次中断(1000ms)后进入长按功能:看下进入函数的后续语句
if(KeyDownTime[i][j] > 0) //检测执行快速输入{if(KeyDownTime[i][j] >= TimeThr[i][j]){ //达到阈值时执行一次动作KeyAction(KeyCodeMap[i][j]); //调用按键动作函数TimeThr[i][j] += 200; //时间阈值增加200ms,以准备下一次执行if(i == 3 && j == 2) //注意entel键是3行2列,不是4行3列因为第1行一1列是0,0{LongPress = 1;}}} else // 按键弹起时复位阈值时间{TimeThr[i][j] = 1000; // 恢复1s的初始阈值时间}
可以看到执行了1次按键调用函数,然后把比较值提高了200即1000变成1200。即下次再使能长按功能需要再经过200次中断。
因此这个函数可以这么设计:
设置一个变量cnt :让cnt >= 10,如此进入函数的时间是(200ms*10)2s加上之前的1s则长按该开关的时间判断就变成了3s,而且不影响其他开关的长按时间。
看代码:
if(KeyDownTime[i][j] > 0) //检测执行快速输入{if(KeyDownTime[i][j] >= TimeThr[i][j]){ //达到阈值时执行一次动作KeyAction(KeyCodeMap[i][j]); //调用按键动作函数TimeThr[i][j] += 200; //时间阈值增加200ms,以准备下一次执行cnt++;if(cnt >= 10){cnt = 0;if(i == 3 && j == 2) //注意entel键是3行2列,不是4行3列因为第1行一1列是0,0{LongPress = 1;}} }} else // 按键弹起时复位阈值时间{TimeThr[i][j] = 1000; // 恢复1s的初始阈值时间}
结果视频就不上了,笔者这个已经试过了,没有问题的。
至此本案倒计时的程序可以算基本完结了,但是以生活经验来说,对于生活中的电子产品,由于空间有限往往一个按键有两种不同的功能,短按的功能可能和长按的功能截然不同。以笔者的Switch游戏机来说:
短按电源键 :如果是黑屏,屏幕就变亮,如果是亮屏就变黑。
长按电源键:如果是黑屏,屏幕就变亮,如果是亮屏就进入关机选择界面。
对此,本案目前的程序需要些许改动才能实现长短键不同功能。对于开关动作可以这么设想,假设按住开关,短按功能你触不触发?如果你触发了长按功能怎么办?如果只使能长按功能,短按功能怎么办?毕竟长短按的功能不一样,如果是一样的可以按照本程序的逻辑来。
笔者前期的博文就有提到,一次开关动作包括两次状态变化:
1:从弹起状态进入按住状态
2:从按住状态回到弹起状态
开关的按键功能可以在状态1实现,也可以在状态2实现。之前的博文里笔者就演示了:按键开关按住加1和弹起加1的现象。因此长短按键的功能就可以分开实现了:
短按:开关动作状态2实现短按功能
长按:开关动作状态1实现长按功能
主要更改部分是KeyACtion()与void KeyDriver()函数
看代码
void KeyDriver(){unsigned char i,j,cnt;static unsigned char pdata backup[4][4] = { //按键值备份,保存前一次的值{1,1,1,1},{1,1,1,1},{1,1,1,1},{1,1,1,1}};static unsigned long pdata TimeThr[4][4] = { //快速输入执行的时间阈值{1000,1000,1000,1000},{1000,1000,1000,1000},{1000,1000,1000,1000},{1000,1000,1000,1000}};for(i = 0; i<4; i++) //循环扫描4*4的矩阵按键{for(j = 0; j<4; j++){if(backup[i][j] != KeySta[i][j]) //按键动作检查{ if(backup[i][j] == 0 && LongPress == 0) //前态如果是0那么现态是1,开关从按住弹起{if( Locksta == 0)KeyAction(KeyCodeMap[i][j]); //调用按键动作函数Locksta = 0;}backup[i][j] = KeySta[i][j]; //刷新前一次备份值}if(KeyDownTime[i][j] > 0) //检测执行快速输入{if(KeyDownTime[i][j] >= TimeThr[i][j]){ LongPress = 1; //长按标志置1KeyAction(KeyCodeMap[i][j]); //调用按键动作函数TimeThr[i][j] += 200; //时间阈值增加200ms,以准备下一次执行cnt++;if(cnt >= 10){cnt = 0;if(i == 3 && j == 2) //注意entel键是3行2列,不是4行3列因为第1行一1列是0,0{EntelLongPress = 1;//entel长按标志Locksta = 1; //按键锁标志防止开关弹起进入短按函数}} }} else // 按键弹起时复位阈值时间{TimeThr[i][j] = 1000; // 恢复1s的初始阈值时间}}}}
void KeyAction(unsigned char keycode){if(keycode == 0x26) //向上键,倒计时设定值每按一下加1{ if(CountDown < 9999) //最大计数9999{LongPress = 0; //长按标志清零CountDown++;ShowNumber(CountDown);}}else if (keycode == 0x28) //向下键 倒计时设定值递减{if(CountDown >1) //最小计时1s{LongPress = 0;CountDown--;ShowNumber(CountDown);}}else if(keycode == 0x0D) //回车键 ,启动倒计时{if(EntelLongPress | Locksta == 1){flagStart = 0;EntelLongPress = 0;LongPress = 0;}else{flagStart = 1;LongPress = 0;}}else if(keycode == 0x1B) //ESC 键 取消倒计时{LongPress = 0;enBuzz = 0;LedBuff[6] = 0xFF;flagStart = 0;CountDown = 0;ShowNumber(0);}}
该程序与之前的相比引入了2个新的变量,原先的LongPress改成EntelLongPress
新的变量是 LongPress LockSta, 这两个变量的作用是定义:一般长键状态,按键锁标志防止开关弹起进入短键函数
此函数把按键状态分为3种:
1:普通的短键触发功能
2:普通的长键触发功能
3:Entel键的加长长键触发功能
至此本篇定时炸弹长短键应用扩展结束,看下结果视频:长短键功能循环倒计时_哔哩哔哩_bilibili
可以看到Entel键的长短键切换,上下键的长短键切换正常,没有问题。
然后分享下最近关于中断方面的一些感受:51单片机是串行执行代码的,因此在视觉上的感受同时发生的事情也是1句1句执行的,只是速度很快罢了。如果真要说有什么好像与之并行的。那就是定时器,只要初值化设置好它就一直计时直到溢出停止。期间无论程序是在等待还是在执行什么都不会影响定时器计时。
程序执行如图:
一般来说程序是在主函数与中断之间互相穿插执行的。对于本案来说只有定时器0中断,因此它是在主函数与定时器0中断之间循环执行的。一般主函数循环执行某个函数,会需求中断函数提供相应参数在主函数中执行。因此就需要设置的中断时间T>(t+t0)。
中断的重载值一般都会在中断函数最前面执行,然后定时器就开始计时了。如果中断内部执行时间+主函数执行时间> 中断设置的间隔时间,那么就无法完整执行完一次主函数,又进入了中断函数,就可能让前一次中断传递的参数没有起作用。那么就会产生一些不好的结果。
如果笔者在中断函数里加一个时间延迟函数while(100--)近似1ms的延时函数,那么该程序就会无法正常工作,因为它一跳出中断,主函数没有执行,中断响应又到了。那么按键功能就无法正确执行了。
看视频:中断时间过长_哔哩哔哩_bilibili
可以看到主函数初始化0显示花的时间都变长了,并且按键不起作用。
至此博文到此结束。