这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界» 论坛首页» 嵌入式开发» MCU» 从业将近十年!手把手教你单片机程序框架(连载)

共146条 7/15 |‹ 5 6 7 8 9 10 ›| 跳转至
菜鸟
2014-06-09 11:54:19 打赏
61楼
第四十六节:利用AT24C02进行掉电后的数据保存。

开场白:
一个AT24C02可以存储256个字节,地址范围是(0至255)。利用AT24C02存储数据时,要教会大家六个知识点:
第一个:单片机操作AT24C02的通讯过程也就是IIC的通讯过程, IIC通讯过程是一个要求一气呵成的通讯过程,中间不能被其它中断影响时序出错,因此在整个通讯过程中应该先关闭总中断,完成之后再开中断。
第二个:在写入或者读取完一个字节之后,一定要加上一段延时时间。在11.0592M晶振的系统中,写入数据时经验值用delay_short(2000),读取数据时经验值用delay_short(800)。否则在连续写入或者读取一串数据时容易丢失数据。如果一旦发现丢失数据,应该适当继续把这个时间延长,尤其是在写入数据时。
第三个:如何初始化EEPROM数据的方法。系统第一次上电时,我们从EEPROM读取出来的数据有可能超出了范围,可能是ff。这个时候我们应该给它填入一个初始化的数据,这一步千万别漏了。
第四个:在时序中,发送ACK确认信号时,要记得把数据线eeprom_sda_dr_s设置为输入的状态。对于51单片机来说,只要把eeprom_sda_dr_s=1就可以。而对于PIC或者AVR单片机来说,它们都是带方向寄存器的,就不能直接eeprom_sda_dr_s=1,而要直接修改方向寄存器,把它设置为输入状态。在本驱动程序中,我没有对ACK信号进行出错判断,因为我这么多年一直都是这样用也没出现过什么问题。
第五个: 提醒各位读者在硬件上应该注意的问题,单片机跟AT24C02通讯的2根IO口都要加上一个4.7K左右的上拉电阻。凡是在IIC通讯场合,都要加上拉电阻。AT24C02的WP引脚一定要接地,否则存不进数据。
第六个:旧版的朱兆祺51学习板在硬件上有一个bug,AT24C02的第8个引脚VCC悬空了!!!,读者记得把它飞线连接到5V电源处。新版的朱兆祺51学习板已经改过来了。

具体内容,请看源代码讲解。

(1)硬件平台:
基于朱兆祺51单片机学习板。

(2)实现功能:
4个被更改后的参数断电后不丢失,数据可以保存,断电再上电后还是上一次最新被修改的数据。
显示和独立按键部分根据第29节的程序来改编,用朱兆祺51单片机学习板中的S1,S5,S9作为独立按键。
一共有4个窗口。每个窗口显示一个参数。
第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。

(3)源代码讲解如下:


#include "REG52.H" #define const_voice_short 40 //蜂鸣器短叫的持续时间 #define const_key_time1 20 //按键去抖动延时的时间 #define const_key_time2 20 //按键去抖动延时的时间 #define const_key_time3 20 //按键去抖动延时的时间 void initial_myself(void); void initial_peripheral(void); void delay_short(unsigned int uiDelayShort); void delay_long(unsigned int uiDelaylong); //驱动数码管的74HC595 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01); void display_drive(void); //显示数码管字模的驱动函数 void display_service(void); //显示的窗口菜单服务程序 //驱动LED的74HC595 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01); void start24(void); //开始位 void ack24(void); //确认位 void stop24(void); //停止位 unsigned char read24(void); //读取一个字节的时序 void write24(unsigned char dd); //发送一个字节的时序 unsigned char read_eeprom(unsigned int address); //从一个地址读取出一个字节数据 void write_eeprom(unsigned int address,unsigned char dd); //往一个地址存入一个字节数据 unsigned int read_eeprom_int(unsigned int address); //从一个地址读取出一个int类型的数据 void write_eeprom_int(unsigned int address,unsigned int uiWriteData); //往一个地址存入一个int类型的数据 void T0_time(void); //定时中断函数 void key_service(void); //按键服务的应用程序 void key_scan(void);//按键扫描函数 放在定时中断里 sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平 sbit beep_dr=P2^7; //蜂鸣器的驱动IO口 sbit eeprom_scl_dr=P3^7; //时钟线 sbit eeprom_sda_dr_sr=P3^6; //数据的输出线和输入线 sbit dig_hc595_sh_dr=P2^0; //数码管的74HC595程序 sbit dig_hc595_st_dr=P2^1; sbit dig_hc595_ds_dr=P2^2; sbit hc595_sh_dr=P2^3; //LED灯的74HC595程序 sbit hc595_st_dr=P2^4; sbit hc595_ds_dr=P2^5; unsigned char ucKeySec=0; //被触发的按键编号 unsigned int uiKeyTimeCnt1=0; //按键去抖动延时计数器 unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志 unsigned int uiKeyTimeCnt2=0; //按键去抖动延时计数器 unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志 unsigned int uiKeyTimeCnt3=0; //按键去抖动延时计数器 unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志 unsigned int uiVoiceCnt=0; //蜂鸣器鸣叫的持续时间计数器 unsigned char ucVoiceLock=0; //蜂鸣器鸣叫的原子锁 unsigned char ucDigShow8; //第8位数码管要显示的内容 unsigned char ucDigShow7; //第7位数码管要显示的内容 unsigned char ucDigShow6; //第6位数码管要显示的内容 unsigned char ucDigShow5; //第5位数码管要显示的内容 unsigned char ucDigShow4; //第4位数码管要显示的内容 unsigned char ucDigShow3; //第3位数码管要显示的内容 unsigned char ucDigShow2; //第2位数码管要显示的内容 unsigned char ucDigShow1; //第1位数码管要显示的内容 unsigned char ucDigDot8; //数码管8的小数点是否显示的标志 unsigned char ucDigDot7; //数码管7的小数点是否显示的标志 unsigned char ucDigDot6; //数码管6的小数点是否显示的标志 unsigned char ucDigDot5; //数码管5的小数点是否显示的标志 unsigned char ucDigDot4; //数码管4的小数点是否显示的标志 unsigned char ucDigDot3; //数码管3的小数点是否显示的标志 unsigned char ucDigDot2; //数码管2的小数点是否显示的标志 unsigned char ucDigDot1; //数码管1的小数点是否显示的标志 unsigned char ucDigShowTemp=0; //临时中间变量 unsigned char ucDisplayDriveStep=1; //动态扫描数码管的步骤变量 unsigned char ucWd1Update=1; //窗口1更新显示标志 unsigned char ucWd2Update=0; //窗口2更新显示标志 unsigned char ucWd3Update=0; //窗口3更新显示标志 unsigned char ucWd4Update=0; //窗口4更新显示标志 unsigned char ucWd=1; //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。 unsigned int uiSetData1=0; //本程序中需要被设置的参数1 unsigned int uiSetData2=0; //本程序中需要被设置的参数2 unsigned int uiSetData3=0; //本程序中需要被设置的参数3 unsigned int uiSetData4=0; //本程序中需要被设置的参数4 unsigned char ucTemp1=0; //中间过渡变量 unsigned char ucTemp2=0; //中间过渡变量 unsigned char ucTemp3=0; //中间过渡变量 unsigned char ucTemp4=0; //中间过渡变量 //根据原理图得出的共阴数码管字模表 code unsigned char dig_table[]= { 0x3f, //0 序号0 0x06, //1 序号1 0x5b, //2 序号2 0x4f, //3 序号3 0x66, //4 序号4 0x6d, //5 序号5 0x7d, //6 序号6 0x07, //7 序号7 0x7f, //8 序号8 0x6f, //9 序号9 0x00, //无 序号10 0x40, //- 序号11 0x73, //P 序号12 }; void main() { initial_myself(); delay_long(100); initial_peripheral(); while(1) { key_service(); //按键服务的应用程序 display_service(); //显示的窗口菜单服务程序 } } //AT24C02驱动程序 void start24(void) //开始位 { eeprom_sda_dr_sr=1; eeprom_scl_dr=1; delay_short(15); eeprom_sda_dr_sr=0; delay_short(15); eeprom_scl_dr=0; } void ack24(void) //确认位时序 { eeprom_sda_dr_sr=1; //51单片机在读取数据之前要先置一,表示数据输入 eeprom_scl_dr=1; delay_short(15); eeprom_scl_dr=0; delay_short(15); //在本驱动程序中,我没有对ACK信号进行出错判断,因为我这么多年一直都是这样用也没出现过什么问题。 //有兴趣的朋友可以自己增加出错判断,不一定非要按我的方式去做。 } void stop24(void) //停止位 { eeprom_sda_dr_sr=0; eeprom_scl_dr=1; delay_short(15); eeprom_sda_dr_sr=1; } unsigned char read24(void) //读取一个字节的时序 { unsigned char outdata,tempdata; outdata=0; eeprom_sda_dr_sr=1; //51单片机的IO口在读取数据之前要先置一,表示数据输入 delay_short(2); for(tempdata=0;tempdata<8;tempdata++) { eeprom_scl_dr=0; delay_short(2); eeprom_scl_dr=1; delay_short(2); outdata<<=1; if(eeprom_sda_dr_sr==1)outdata++; eeprom_sda_dr_sr=1; //51单片机的IO口在读取数据之前要先置一,表示数据输入 delay_short(2); } return(outdata); } void write24(unsigned char dd) //发送一个字节的时序 { unsigned char tempdata; for(tempdata=0;tempdata<8;tempdata++) { if(dd>=0x80)eeprom_sda_dr_sr=1; else eeprom_sda_dr_sr=0; dd<<=1; delay_short(2); eeprom_scl_dr=1; delay_short(4); eeprom_scl_dr=0; } } unsigned char read_eeprom(unsigned int address) //从一个地址读取出一个字节数据 { unsigned char dd,cAddress; cAddress=address; //把低字节地址传递给一个字节变量。 /* 注释一: * IIC通讯过程是一个要求一气呵成的通讯过程,中间不能被其它中断影响时序出错,因此 * 在整个通讯过程中应该先关闭总中断,完成之后再开中断。但是,这样就会引起另外一个新 * 问题,如果关闭总中断的时间太长,会导致动态数码管不能及时均匀的扫描,在操作EEPROM时, * 数码管就会出现闪烁的现象,解决这个问题最好的办法就是在做项目中尽量不要用动态扫描数码管 * 的方案,应该用静态显示的方案。那么程序上还有没有改善的方法?有的,下一节我会讲这个问题 * 的改善方法。 */ EA=0; //禁止中断 start24(); //IIC通讯开始 write24(0xA0); //此字节包含读写指令和芯片地址两方面的内容。 //指令为写指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定 ack24(); //发送应答信号 write24(cAddress); //发送读取的存储地址(范围是0至255) ack24(); //发送应答信号 start24(); //开始 write24(0xA1); //此字节包含读写指令和芯片地址两方面的内容。 //指令为读指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定 ack24(); //发送应答信号 dd=read24(); //读取一个字节 ack24(); //发送应答信号 stop24(); //停止 /* 注释二: * 在写入或者读取完一个字节之后,一定要加上一段延时时间。在11.0592M晶振的系统中, * 写入数据时经验值用delay_short(2000),读取数据时经验值用delay_short(800)。 * 否则在连续写入或者读取一串数据时容易丢失数据。如果一旦发现丢失数据, * 应该适当继续把这个时间延长,尤其是在写入数据时。 */ delay_short(800); //此处最关键,此处的延时时间一定要,而且要足够长,此处也是导致动态数码管闪烁的根本原因 EA=1; //允许中断 return(dd); } void write_eeprom(unsigned int address,unsigned char dd) //往一个地址存入一个字节数据 { unsigned char cAddress; cAddress=address; //把低字节地址传递给一个字节变量。 EA=0; //禁止中断 start24(); //IIC通讯开始 write24(0xA0); //此字节包含读写指令和芯片地址两方面的内容。 //指令为写指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定 ack24(); //发送应答信号 write24(cAddress); //发送写入的存储地址(范围是0至255) ack24(); //发送应答信号 write24(dd); //写入存储的数据 ack24(); //发送应答信号 stop24(); //停止 delay_short(2000); //此处最关键,此处的延时时间一定要,而且要足够长,此处也是导致动态数码管闪烁的根本原因 EA=1; //允许中断 } unsigned int read_eeprom_int(unsigned int address) //从一个地址读取出一个int类型的数据 { unsigned char ucReadDataH; unsigned char ucReadDataL; unsigned int uiReadDate; ucReadDataH=read_eeprom(address); //读取高字节 ucReadDataL=read_eeprom(address+1); //读取低字节 uiReadDate=ucReadDataH; //把两个字节合并成一个int类型数据 uiReadDate=uiReadDate<<8; uiReadDate=uiReadDate+ucReadDataL; return uiReadDate; } void write_eeprom_int(unsigned int address,unsigned int uiWriteData) //往一个地址存入一个int类型的数据 { unsigned char ucWriteDataH; unsigned char ucWriteDataL; ucWriteDataH=uiWriteData>>8; ucWriteDataL=uiWriteData; write_eeprom(address,ucWriteDataH); //存入高字节 write_eeprom(address+1,ucWriteDataL); //存入低字节 } void display_service(void) //显示的窗口菜单服务程序 { switch(ucWd) //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。 { case 1: //显示P--1窗口的数据 if(ucWd1Update==1) //窗口1要全部更新显示 { ucWd1Update=0; //及时清零标志,避免一直进来扫描 ucDigShow8=12; //第8位数码管显示P ucDigShow7=11; //第7位数码管显示- ucDigShow6=1; //第6位数码管显示1 ucDigShow5=10; //第5位数码管显示无 //先分解数据 ucTemp4=uiSetData1/1000; ucTemp3=uiSetData1%1000/100; ucTemp2=uiSetData1%100/10; ucTemp1=uiSetData1%10; //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好 if(uiSetData1<1000) { ucDigShow4=10; //如果小于1000,千位显示无 } else { ucDigShow4=ucTemp4; //第4位数码管要显示的内容 } if(uiSetData1<100) { ucDigShow3=10; //如果小于100,百位显示无 } else { ucDigShow3=ucTemp3; //第3位数码管要显示的内容 } if(uiSetData1<10) { ucDigShow2=10; //如果小于10,十位显示无 } else { ucDigShow2=ucTemp2; //第2位数码管要显示的内容 } ucDigShow1=ucTemp1; //第1位数码管要显示的内容 } break; case 2: //显示P--2窗口的数据 if(ucWd2Update==1) //窗口2要全部更新显示 { ucWd2Update=0; //及时清零标志,避免一直进来扫描 ucDigShow8=12; //第8位数码管显示P ucDigShow7=11; //第7位数码管显示- ucDigShow6=2; //第6位数码管显示2 ucDigShow5=10; //第5位数码管显示无 ucTemp4=uiSetData2/1000; //分解数据 ucTemp3=uiSetData2%1000/100; ucTemp2=uiSetData2%100/10; ucTemp1=uiSetData2%10; if(uiSetData2<1000) { ucDigShow4=10; //如果小于1000,千位显示无 } else { ucDigShow4=ucTemp4; //第4位数码管要显示的内容 } if(uiSetData2<100) { ucDigShow3=10; //如果小于100,百位显示无 } else { ucDigShow3=ucTemp3; //第3位数码管要显示的内容 } if(uiSetData2<10) { ucDigShow2=10; //如果小于10,十位显示无 } else { ucDigShow2=ucTemp2; //第2位数码管要显示的内容 } ucDigShow1=ucTemp1; //第1位数码管要显示的内容 } break; case 3: //显示P--3窗口的数据 if(ucWd3Update==1) //窗口3要全部更新显示 { ucWd3Update=0; //及时清零标志,避免一直进来扫描 ucDigShow8=12; //第8位数码管显示P ucDigShow7=11; //第7位数码管显示- ucDigShow6=3; //第6位数码管显示3 ucDigShow5=10; //第5位数码管显示无 ucTemp4=uiSetData3/1000; //分解数据 ucTemp3=uiSetData3%1000/100; ucTemp2=uiSetData3%100/10; ucTemp1=uiSetData3%10; if(uiSetData3<1000) { ucDigShow4=10; //如果小于1000,千位显示无 } else { ucDigShow4=ucTemp4; //第4位数码管要显示的内容 } if(uiSetData3<100) { ucDigShow3=10; //如果小于100,百位显示无 } else { ucDigShow3=ucTemp3; //第3位数码管要显示的内容 } if(uiSetData3<10) { ucDigShow2=10; //如果小于10,十位显示无 } else { ucDigShow2=ucTemp2; //第2位数码管要显示的内容 } ucDigShow1=ucTemp1; //第1位数码管要显示的内容 } break; case 4: //显示P--4窗口的数据 if(ucWd4Update==1) //窗口4要全部更新显示 { ucWd4Update=0; //及时清零标志,避免一直进来扫描 ucDigShow8=12; //第8位数码管显示P ucDigShow7=11; //第7位数码管显示- ucDigShow6=4; //第6位数码管显示4 ucDigShow5=10; //第5位数码管显示无 ucTemp4=uiSetData4/1000; //分解数据 ucTemp3=uiSetData4%1000/100; ucTemp2=uiSetData4%100/10; ucTemp1=uiSetData4%10; if(uiSetData4<1000) { ucDigShow4=10; //如果小于1000,千位显示无 } else { ucDigShow4=ucTemp4; //第4位数码管要显示的内容 } if(uiSetData4<100) { ucDigShow3=10; //如果小于100,百位显示无 } else { ucDigShow3=ucTemp3; //第3位数码管要显示的内容 } if(uiSetData4<10) { ucDigShow2=10; //如果小于10,十位显示无 } else { ucDigShow2=ucTemp2; //第2位数码管要显示的内容 } ucDigShow1=ucTemp1; //第1位数码管要显示的内容 } break; } } void key_scan(void)//按键扫描函数 放在定时中断里 { if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock1=0; //按键自锁标志清零 uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock1==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt1++; //累加定时中断次数 if(uiKeyTimeCnt1>const_key_time1) { uiKeyTimeCnt1=0; ucKeyLock1=1; //自锁按键置位,避免一直触发 ucKeySec=1; //触发1号键 } } if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock2=0; //按键自锁标志清零 uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock2==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt2++; //累加定时中断次数 if(uiKeyTimeCnt2>const_key_time2) { uiKeyTimeCnt2=0; ucKeyLock2=1; //自锁按键置位,避免一直触发 ucKeySec=2; //触发2号键 } } if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock3=0; //按键自锁标志清零 uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock3==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt3++; //累加定时中断次数 if(uiKeyTimeCnt3>const_key_time3) { uiKeyTimeCnt3=0; ucKeyLock3=1; //自锁按键置位,避免一直触发 ucKeySec=3; //触发3号键 } } } void key_service(void) //按键服务的应用程序 { switch(ucKeySec) //按键服务状态切换 { case 1:// 加按键 对应朱兆祺学习板的S1键 switch(ucWd) //在不同的窗口下,设置不同的参数 { case 1: uiSetData1++; if(uiSetData1>9999) //最大值是9999 { uiSetData1=9999; } write_eeprom_int(0,uiSetData1); //存入EEPROM 由于内部有延时函数,所以此处会引起数码管闪烁 ucWd1Update=1; //窗口1更新显示 break; case 2: uiSetData2++; if(uiSetData2>9999) //最大值是9999 { uiSetData2=9999; } write_eeprom_int(2,uiSetData2); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd2Update=1; //窗口2更新显示 break; case 3: uiSetData3++; if(uiSetData3>9999) //最大值是9999 { uiSetData3=9999; } write_eeprom_int(4,uiSetData3); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd3Update=1; //窗口3更新显示 break; case 4: uiSetData4++; if(uiSetData4>9999) //最大值是9999 { uiSetData4=9999; } write_eeprom_int(6,uiSetData4); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd4Update=1; //窗口4更新显示 break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; case 2:// 减按键 对应朱兆祺学习板的S5键 switch(ucWd) //在不同的窗口下,设置不同的参数 { case 1: uiSetData1--; if(uiSetData1>9999) { uiSetData1=0; //最小值是0 } write_eeprom_int(0,uiSetData1); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd1Update=1; //窗口1更新显示 break; case 2: uiSetData2--; if(uiSetData2>9999) { uiSetData2=0; //最小值是0 } write_eeprom_int(2,uiSetData2); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd2Update=1; //窗口2更新显示 break; case 3: uiSetData3--; if(uiSetData3>9999) { uiSetData3=0; //最小值是0 } write_eeprom_int(4,uiSetData3); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd3Update=1; //窗口3更新显示 break; case 4: uiSetData4--; if(uiSetData4>9999) { uiSetData4=0; //最小值是0 } write_eeprom_int(6,uiSetData4); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd4Update=1; //窗口4更新显示 break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; case 3:// 切换窗口按键 对应朱兆祺学习板的S9键 ucWd++; //切换窗口 if(ucWd>4) { ucWd=1; } switch(ucWd) //在不同的窗口下,在不同的窗口下,更新显示不同的窗口 { case 1: ucWd1Update=1; //窗口1更新显示 break; case 2: ucWd2Update=1; //窗口2更新显示 break; case 3: ucWd3Update=1; //窗口3更新显示 break; case 4: ucWd4Update=1; //窗口4更新显示 break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; } } void display_drive(void) { //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路 switch(ucDisplayDriveStep) { case 1: //显示第1位 ucDigShowTemp=dig_table[ucDigShow1]; if(ucDigDot1==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfe); break; case 2: //显示第2位 ucDigShowTemp=dig_table[ucDigShow2]; if(ucDigDot2==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfd); break; case 3: //显示第3位 ucDigShowTemp=dig_table[ucDigShow3]; if(ucDigDot3==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfb); break; case 4: //显示第4位 ucDigShowTemp=dig_table[ucDigShow4]; if(ucDigDot4==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xf7); break; case 5: //显示第5位 ucDigShowTemp=dig_table[ucDigShow5]; if(ucDigDot5==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xef); break; case 6: //显示第6位 ucDigShowTemp=dig_table[ucDigShow6]; if(ucDigDot6==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xdf); break; case 7: //显示第7位 ucDigShowTemp=dig_table[ucDigShow7]; if(ucDigDot7==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xbf); break; case 8: //显示第8位 ucDigShowTemp=dig_table[ucDigShow8]; if(ucDigDot8==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0x7f); break; } ucDisplayDriveStep++; if(ucDisplayDriveStep>8) //扫描完8个数码管后,重新从第一个开始扫描 { ucDisplayDriveStep=1; } } //数码管的74HC595驱动函数 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01) { unsigned char i; unsigned char ucTempData; dig_hc595_sh_dr=0; dig_hc595_st_dr=0; ucTempData=ucDigStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucDigStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } dig_hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); dig_hc595_st_dr=1; delay_short(1); dig_hc595_sh_dr=0; //拉低,抗干扰就增强 dig_hc595_st_dr=0; dig_hc595_ds_dr=0; } //LED灯的74HC595驱动函数 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01) { unsigned char i; unsigned char ucTempData; hc595_sh_dr=0; hc595_st_dr=0; ucTempData=ucLedStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucLedStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); hc595_st_dr=1; delay_short(1); hc595_sh_dr=0; //拉低,抗干扰就增强 hc595_st_dr=0; hc595_ds_dr=0; } void T0_time(void) interrupt 1 //定时中断 { TF0=0; //清除中断标志 TR0=0; //关中断 /* 注释三: * 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议. */ if(ucVoiceLock==0) //原子锁判断 { if(uiVoiceCnt!=0) { uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫 beep_dr=0; //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。 } else { ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。 beep_dr=1; //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。 } } key_scan(); //按键扫描函数 display_drive(); //数码管字模的驱动函数 TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b TL0=0x0b; TR0=1; //开中断 } void delay_short(unsigned int uiDelayShort) { unsigned int i; for(i=0;i9999) //不在范围内 { uiSetData1=0; //填入一个初始化数据 write_eeprom_int(0,uiSetData1); //存入uiSetData1,内部占用2个字节地址 } uiSetData2=read_eeprom_int(2); //读取uiSetData2,内部占用2个字节地址 if(uiSetData2>9999)//不在范围内 { uiSetData2=0; //填入一个初始化数据 write_eeprom_int(2,uiSetData2); //存入uiSetData2,内部占用2个字节地址 } uiSetData3=read_eeprom_int(4); //读取uiSetData3,内部占用2个字节地址 if(uiSetData3>9999)//不在范围内 { uiSetData3=0; //填入一个初始化数据 write_eeprom_int(4,uiSetData3); //存入uiSetData3,内部占用2个字节地址 } uiSetData4=read_eeprom_int(6); //读取uiSetData4,内部占用2个字节地址 if(uiSetData4>9999)//不在范围内 { uiSetData4=0; //填入一个初始化数据 write_eeprom_int(6,uiSetData4); //存入uiSetData4,内部占用2个字节地址 } }

总结陈词:
IIC通讯过程是一个要求一气呵成的通讯过程,中间不能被其它中断影响时序出错,因此,在整个通讯过程中应该先关闭总中断,完成之后再开中断。但是,这样就会引起另外一个新问题,如果关闭总中断的时间太长,会导致动态数码管不能及时均匀的扫描,在按键更改参数,内部操作EEPROM时,数码管就会出现短暂明显的闪烁现象,解决这个问题最好的办法就是在做项目中尽量不要用动态扫描数码管的方案,应该用静态显示的方案。那么在程序上还有没有改善这种现象的方法?当然有。欲知详情,请听下回分解-----操作AT24C02时,利用“一气呵成的定时器方式”改善数码管的闪烁现象。

(未完待续,下节更精彩,不要走开哦)



菜鸟
2014-06-09 11:59:27 打赏
62楼
第四十七节:操作AT24C02时,利用“一气呵成的定时器延时”改善数码管的闪烁现象。

开场白:
上一节在按键更改参数时,会出现短暂明显的数码管闪烁现象。这节通过教大家使用新型延时函数可以有效的改善闪烁现象。要教会大家三个知识点:
第一个:如何编写一气呵成的定时器延时函数。
第二个:如何编写检查EEPROM芯片是否存在短路,虚焊或者芯片坏了的监控程序。
第三个:经过网友“cjseng”的提醒,我建议大家以后在用EEPROM芯片时,如果单片机IO口足够多,WP引脚应该专门接一个IO口,并且加一个上拉电阻,需要更改EEPROM存储数据时置低,其他任何一个时刻都置高,这样可以更加有效地保护EEPROM内部数据不会被意外更改。

具体内容,请看源代码讲解。

(1)硬件平台:
基于朱兆祺51单片机学习板。旧版的朱兆祺51学习板在硬件上有一个bug,AT24C02的第8个引脚VCC悬空了!!!,读者记得把它飞线连接到5V电源处。新版的朱兆祺51学习板已经改过来了。

(2)实现功能:
4个被更改后的参数断电后不丢失,数据可以保存,断电再上电后还是上一次最新被修改的数据。如果AT24C02短路,虚焊,或者坏了,系统可以检查出来,并且蜂鸣器会间歇性鸣叫报警。按更改参数按键时,数码管比上一节大大降低了闪烁现象。
显示和独立按键部分根据第29节的程序来改编,用朱兆祺51单片机学习板中的S1,S5,S9作为独立按键。
一共有4个窗口。每个窗口显示一个参数。
第8,7,6,5位数码管显示当前窗口,P-1代表第1个窗口,P-2代表第2个窗口,P-3代表第3个窗口,P-4代表第1个窗口。
第4,3,2,1位数码管显示当前窗口被设置的参数。范围是从0到9999。S1是加按键,按下此按键会依次增加当前窗口的参数。S5是减按键,按下此按键会依次减少当前窗口的参数。S9是切换窗口按键,按下此按键会依次循环切换不同的窗口。

(3)源代码讲解如下:


#include "REG52.H" #define const_voice_short 40 //蜂鸣器短叫的持续时间 #define const_key_time1 20 //按键去抖动延时的时间 #define const_key_time2 20 //按键去抖动延时的时间 #define const_key_time3 20 //按键去抖动延时的时间 #define const_eeprom_1s 400 //大概1秒的时间 void initial_myself(void); void initial_peripheral(void); void delay_short(unsigned int uiDelayShort); void delay_long(unsigned int uiDelaylong); void delay_timer(unsigned int uiDelayTimerTemp); //一气呵成的定时器延时方式 //驱动数码管的74HC595 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01); void display_drive(void); //显示数码管字模的驱动函数 void display_service(void); //显示的窗口菜单服务程序 //驱动LED的74HC595 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01); void start24(void); //开始位 void ack24(void); //确认位 void stop24(void); //停止位 unsigned char read24(void); //读取一个字节的时序 void write24(unsigned char dd); //发送一个字节的时序 unsigned char read_eeprom(unsigned int address); //从一个地址读取出一个字节数据 void write_eeprom(unsigned int address,unsigned char dd); //往一个地址存入一个字节数据 unsigned int read_eeprom_int(unsigned int address); //从一个地址读取出一个int类型的数据 void write_eeprom_int(unsigned int address,unsigned int uiWriteData); //往一个地址存入一个int类型的数据 void T0_time(void); //定时中断函数 void key_service(void); //按键服务的应用程序 void key_scan(void);//按键扫描函数 放在定时中断里 void eeprom_alarm_service(void); //EEPROM出错报警 sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平 sbit beep_dr=P2^7; //蜂鸣器的驱动IO口 sbit eeprom_scl_dr=P3^7; //时钟线 sbit eeprom_sda_dr_sr=P3^6; //数据的输出线和输入线 sbit dig_hc595_sh_dr=P2^0; //数码管的74HC595程序 sbit dig_hc595_st_dr=P2^1; sbit dig_hc595_ds_dr=P2^2; sbit hc595_sh_dr=P2^3; //LED灯的74HC595程序 sbit hc595_st_dr=P2^4; sbit hc595_ds_dr=P2^5; unsigned char ucKeySec=0; //被触发的按键编号 unsigned int uiKeyTimeCnt1=0; //按键去抖动延时计数器 unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志 unsigned int uiKeyTimeCnt2=0; //按键去抖动延时计数器 unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志 unsigned int uiKeyTimeCnt3=0; //按键去抖动延时计数器 unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志 unsigned int uiVoiceCnt=0; //蜂鸣器鸣叫的持续时间计数器 unsigned char ucVoiceLock=0; //蜂鸣器鸣叫的原子锁 unsigned char ucDigShow8; //第8位数码管要显示的内容 unsigned char ucDigShow7; //第7位数码管要显示的内容 unsigned char ucDigShow6; //第6位数码管要显示的内容 unsigned char ucDigShow5; //第5位数码管要显示的内容 unsigned char ucDigShow4; //第4位数码管要显示的内容 unsigned char ucDigShow3; //第3位数码管要显示的内容 unsigned char ucDigShow2; //第2位数码管要显示的内容 unsigned char ucDigShow1; //第1位数码管要显示的内容 unsigned char ucDigDot8; //数码管8的小数点是否显示的标志 unsigned char ucDigDot7; //数码管7的小数点是否显示的标志 unsigned char ucDigDot6; //数码管6的小数点是否显示的标志 unsigned char ucDigDot5; //数码管5的小数点是否显示的标志 unsigned char ucDigDot4; //数码管4的小数点是否显示的标志 unsigned char ucDigDot3; //数码管3的小数点是否显示的标志 unsigned char ucDigDot2; //数码管2的小数点是否显示的标志 unsigned char ucDigDot1; //数码管1的小数点是否显示的标志 unsigned char ucDigShowTemp=0; //临时中间变量 unsigned char ucDisplayDriveStep=1; //动态扫描数码管的步骤变量 unsigned char ucWd1Update=1; //窗口1更新显示标志 unsigned char ucWd2Update=0; //窗口2更新显示标志 unsigned char ucWd3Update=0; //窗口3更新显示标志 unsigned char ucWd4Update=0; //窗口4更新显示标志 unsigned char ucWd=1; //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。 unsigned int uiSetData1=0; //本程序中需要被设置的参数1 unsigned int uiSetData2=0; //本程序中需要被设置的参数2 unsigned int uiSetData3=0; //本程序中需要被设置的参数3 unsigned int uiSetData4=0; //本程序中需要被设置的参数4 unsigned char ucTemp1=0; //中间过渡变量 unsigned char ucTemp2=0; //中间过渡变量 unsigned char ucTemp3=0; //中间过渡变量 unsigned char ucTemp4=0; //中间过渡变量 unsigned char ucDelayTimerLock=0; //原子锁 unsigned int uiDelayTimer=0; unsigned char ucCheckEeprom=0; //检查EEPROM芯片是否正常 unsigned char ucEepromError=0; //EEPROM芯片是否正常的标志 unsigned char ucEepromLock=0;//原子锁 unsigned int uiEepromCnt=0; //间歇性蜂鸣器报警的计时器 //根据原理图得出的共阴数码管字模表 code unsigned char dig_table[]= { 0x3f, //0 序号0 0x06, //1 序号1 0x5b, //2 序号2 0x4f, //3 序号3 0x66, //4 序号4 0x6d, //5 序号5 0x7d, //6 序号6 0x07, //7 序号7 0x7f, //8 序号8 0x6f, //9 序号9 0x00, //无 序号10 0x40, //- 序号11 0x73, //P 序号12 }; void main() { initial_myself(); delay_long(100); initial_peripheral(); while(1) { key_service(); //按键服务的应用程序 display_service(); //显示的窗口菜单服务程序 eeprom_alarm_service(); //EEPROM出错报警 } } void eeprom_alarm_service(void) //EEPROM出错报警 { if(ucEepromError==1) //EEPROM出错 { if(uiEepromCnt=0x80)eeprom_sda_dr_sr=1; else eeprom_sda_dr_sr=0; dd<<=1; delay_short(2); eeprom_scl_dr=1; delay_short(4); eeprom_scl_dr=0; } } unsigned char read_eeprom(unsigned int address) //从一个地址读取出一个字节数据 { unsigned char dd,cAddress; cAddress=address; //把低字节地址传递给一个字节变量。 EA=0; //禁止中断 start24(); //IIC通讯开始 write24(0xA0); //此字节包含读写指令和芯片地址两方面的内容。 //指令为写指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定 ack24(); //发送应答信号 write24(cAddress); //发送读取的存储地址(范围是0至255) ack24(); //发送应答信号 start24(); //开始 write24(0xA1); //此字节包含读写指令和芯片地址两方面的内容。 //指令为读指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定 ack24(); //发送应答信号 dd=read24(); //读取一个字节 ack24(); //发送应答信号 stop24(); //停止 EA=1; //允许中断 delay_timer(2); //一气呵成的定时器延时方式,在延时的时候还可以动态扫描数码管 return(dd); } void write_eeprom(unsigned int address,unsigned char dd) //往一个地址存入一个字节数据 { unsigned char cAddress; cAddress=address; //把低字节地址传递给一个字节变量。 EA=0; //禁止中断 start24(); //IIC通讯开始 write24(0xA0); //此字节包含读写指令和芯片地址两方面的内容。 //指令为写指令。地址为"000"的信息,此信息由A0,A1,A2的引脚决定 ack24(); //发送应答信号 write24(cAddress); //发送写入的存储地址(范围是0至255) ack24(); //发送应答信号 write24(dd); //写入存储的数据 ack24(); //发送应答信号 stop24(); //停止 EA=1; //允许中断 delay_timer(4); //一气呵成的定时器延时方式,在延时的时候还可以动态扫描数码管 } unsigned int read_eeprom_int(unsigned int address) //从一个地址读取出一个int类型的数据 { unsigned char ucReadDataH; unsigned char ucReadDataL; unsigned int uiReadDate; ucReadDataH=read_eeprom(address); //读取高字节 ucReadDataL=read_eeprom(address+1); //读取低字节 uiReadDate=ucReadDataH; //把两个字节合并成一个int类型数据 uiReadDate=uiReadDate<<8; uiReadDate=uiReadDate+ucReadDataL; return uiReadDate; } void write_eeprom_int(unsigned int address,unsigned int uiWriteData) //往一个地址存入一个int类型的数据 { unsigned char ucWriteDataH; unsigned char ucWriteDataL; ucWriteDataH=uiWriteData>>8; ucWriteDataL=uiWriteData; write_eeprom(address,ucWriteDataH); //存入高字节 write_eeprom(address+1,ucWriteDataL); //存入低字节 } void display_service(void) //显示的窗口菜单服务程序 { switch(ucWd) //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。 { case 1: //显示P--1窗口的数据 if(ucWd1Update==1) //窗口1要全部更新显示 { ucWd1Update=0; //及时清零标志,避免一直进来扫描 ucDigShow8=12; //第8位数码管显示P ucDigShow7=11; //第7位数码管显示- ucDigShow6=1; //第6位数码管显示1 ucDigShow5=10; //第5位数码管显示无 //先分解数据 ucTemp4=uiSetData1/1000; ucTemp3=uiSetData1%1000/100; ucTemp2=uiSetData1%100/10; ucTemp1=uiSetData1%10; //再过渡需要显示的数据到缓冲变量里,让过渡的时间越短越好 if(uiSetData1<1000) { ucDigShow4=10; //如果小于1000,千位显示无 } else { ucDigShow4=ucTemp4; //第4位数码管要显示的内容 } if(uiSetData1<100) { ucDigShow3=10; //如果小于100,百位显示无 } else { ucDigShow3=ucTemp3; //第3位数码管要显示的内容 } if(uiSetData1<10) { ucDigShow2=10; //如果小于10,十位显示无 } else { ucDigShow2=ucTemp2; //第2位数码管要显示的内容 } ucDigShow1=ucTemp1; //第1位数码管要显示的内容 } break; case 2: //显示P--2窗口的数据 if(ucWd2Update==1) //窗口2要全部更新显示 { ucWd2Update=0; //及时清零标志,避免一直进来扫描 ucDigShow8=12; //第8位数码管显示P ucDigShow7=11; //第7位数码管显示- ucDigShow6=2; //第6位数码管显示2 ucDigShow5=10; //第5位数码管显示无 ucTemp4=uiSetData2/1000; //分解数据 ucTemp3=uiSetData2%1000/100; ucTemp2=uiSetData2%100/10; ucTemp1=uiSetData2%10; if(uiSetData2<1000) { ucDigShow4=10; //如果小于1000,千位显示无 } else { ucDigShow4=ucTemp4; //第4位数码管要显示的内容 } if(uiSetData2<100) { ucDigShow3=10; //如果小于100,百位显示无 } else { ucDigShow3=ucTemp3; //第3位数码管要显示的内容 } if(uiSetData2<10) { ucDigShow2=10; //如果小于10,十位显示无 } else { ucDigShow2=ucTemp2; //第2位数码管要显示的内容 } ucDigShow1=ucTemp1; //第1位数码管要显示的内容 } break; case 3: //显示P--3窗口的数据 if(ucWd3Update==1) //窗口3要全部更新显示 { ucWd3Update=0; //及时清零标志,避免一直进来扫描 ucDigShow8=12; //第8位数码管显示P ucDigShow7=11; //第7位数码管显示- ucDigShow6=3; //第6位数码管显示3 ucDigShow5=10; //第5位数码管显示无 ucTemp4=uiSetData3/1000; //分解数据 ucTemp3=uiSetData3%1000/100; ucTemp2=uiSetData3%100/10; ucTemp1=uiSetData3%10; if(uiSetData3<1000) { ucDigShow4=10; //如果小于1000,千位显示无 } else { ucDigShow4=ucTemp4; //第4位数码管要显示的内容 } if(uiSetData3<100) { ucDigShow3=10; //如果小于100,百位显示无 } else { ucDigShow3=ucTemp3; //第3位数码管要显示的内容 } if(uiSetData3<10) { ucDigShow2=10; //如果小于10,十位显示无 } else { ucDigShow2=ucTemp2; //第2位数码管要显示的内容 } ucDigShow1=ucTemp1; //第1位数码管要显示的内容 } break; case 4: //显示P--4窗口的数据 if(ucWd4Update==1) //窗口4要全部更新显示 { ucWd4Update=0; //及时清零标志,避免一直进来扫描 ucDigShow8=12; //第8位数码管显示P ucDigShow7=11; //第7位数码管显示- ucDigShow6=4; //第6位数码管显示4 ucDigShow5=10; //第5位数码管显示无 ucTemp4=uiSetData4/1000; //分解数据 ucTemp3=uiSetData4%1000/100; ucTemp2=uiSetData4%100/10; ucTemp1=uiSetData4%10; if(uiSetData4<1000) { ucDigShow4=10; //如果小于1000,千位显示无 } else { ucDigShow4=ucTemp4; //第4位数码管要显示的内容 } if(uiSetData4<100) { ucDigShow3=10; //如果小于100,百位显示无 } else { ucDigShow3=ucTemp3; //第3位数码管要显示的内容 } if(uiSetData4<10) { ucDigShow2=10; //如果小于10,十位显示无 } else { ucDigShow2=ucTemp2; //第2位数码管要显示的内容 } ucDigShow1=ucTemp1; //第1位数码管要显示的内容 } break; } } void key_scan(void)//按键扫描函数 放在定时中断里 { if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock1=0; //按键自锁标志清零 uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock1==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt1++; //累加定时中断次数 if(uiKeyTimeCnt1>const_key_time1) { uiKeyTimeCnt1=0; ucKeyLock1=1; //自锁按键置位,避免一直触发 ucKeySec=1; //触发1号键 } } if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock2=0; //按键自锁标志清零 uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock2==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt2++; //累加定时中断次数 if(uiKeyTimeCnt2>const_key_time2) { uiKeyTimeCnt2=0; ucKeyLock2=1; //自锁按键置位,避免一直触发 ucKeySec=2; //触发2号键 } } if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock3=0; //按键自锁标志清零 uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock3==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt3++; //累加定时中断次数 if(uiKeyTimeCnt3>const_key_time3) { uiKeyTimeCnt3=0; ucKeyLock3=1; //自锁按键置位,避免一直触发 ucKeySec=3; //触发3号键 } } } void key_service(void) //按键服务的应用程序 { switch(ucKeySec) //按键服务状态切换 { case 1:// 加按键 对应朱兆祺学习板的S1键 switch(ucWd) //在不同的窗口下,设置不同的参数 { case 1: uiSetData1++; if(uiSetData1>9999) //最大值是9999 { uiSetData1=9999; } write_eeprom_int(0,uiSetData1); //存入EEPROM 由于内部有延时函数,所以此处会引起数码管闪烁 ucWd1Update=1; //窗口1更新显示 break; case 2: uiSetData2++; if(uiSetData2>9999) //最大值是9999 { uiSetData2=9999; } write_eeprom_int(2,uiSetData2); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd2Update=1; //窗口2更新显示 break; case 3: uiSetData3++; if(uiSetData3>9999) //最大值是9999 { uiSetData3=9999; } write_eeprom_int(4,uiSetData3); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd3Update=1; //窗口3更新显示 break; case 4: uiSetData4++; if(uiSetData4>9999) //最大值是9999 { uiSetData4=9999; } write_eeprom_int(6,uiSetData4); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd4Update=1; //窗口4更新显示 break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; case 2:// 减按键 对应朱兆祺学习板的S5键 switch(ucWd) //在不同的窗口下,设置不同的参数 { case 1: uiSetData1--; if(uiSetData1>9999) { uiSetData1=0; //最小值是0 } write_eeprom_int(0,uiSetData1); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd1Update=1; //窗口1更新显示 break; case 2: uiSetData2--; if(uiSetData2>9999) { uiSetData2=0; //最小值是0 } write_eeprom_int(2,uiSetData2); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd2Update=1; //窗口2更新显示 break; case 3: uiSetData3--; if(uiSetData3>9999) { uiSetData3=0; //最小值是0 } write_eeprom_int(4,uiSetData3); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd3Update=1; //窗口3更新显示 break; case 4: uiSetData4--; if(uiSetData4>9999) { uiSetData4=0; //最小值是0 } write_eeprom_int(6,uiSetData4); //存入EEPROM,由于内部有延时函数,所以此处会引起数码管闪烁 ucWd4Update=1; //窗口4更新显示 break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; case 3:// 切换窗口按键 对应朱兆祺学习板的S9键 ucWd++; //切换窗口 if(ucWd>4) { ucWd=1; } switch(ucWd) //在不同的窗口下,在不同的窗口下,更新显示不同的窗口 { case 1: ucWd1Update=1; //窗口1更新显示 break; case 2: ucWd2Update=1; //窗口2更新显示 break; case 3: ucWd3Update=1; //窗口3更新显示 break; case 4: ucWd4Update=1; //窗口4更新显示 break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; } } void display_drive(void) { //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路 switch(ucDisplayDriveStep) { case 1: //显示第1位 ucDigShowTemp=dig_table[ucDigShow1]; if(ucDigDot1==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfe); break; case 2: //显示第2位 ucDigShowTemp=dig_table[ucDigShow2]; if(ucDigDot2==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfd); break; case 3: //显示第3位 ucDigShowTemp=dig_table[ucDigShow3]; if(ucDigDot3==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfb); break; case 4: //显示第4位 ucDigShowTemp=dig_table[ucDigShow4]; if(ucDigDot4==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xf7); break; case 5: //显示第5位 ucDigShowTemp=dig_table[ucDigShow5]; if(ucDigDot5==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xef); break; case 6: //显示第6位 ucDigShowTemp=dig_table[ucDigShow6]; if(ucDigDot6==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xdf); break; case 7: //显示第7位 ucDigShowTemp=dig_table[ucDigShow7]; if(ucDigDot7==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xbf); break; case 8: //显示第8位 ucDigShowTemp=dig_table[ucDigShow8]; if(ucDigDot8==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0x7f); break; } ucDisplayDriveStep++; if(ucDisplayDriveStep>8) //扫描完8个数码管后,重新从第一个开始扫描 { ucDisplayDriveStep=1; } } //数码管的74HC595驱动函数 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01) { unsigned char i; unsigned char ucTempData; dig_hc595_sh_dr=0; dig_hc595_st_dr=0; ucTempData=ucDigStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucDigStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } dig_hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); dig_hc595_st_dr=1; delay_short(1); dig_hc595_sh_dr=0; //拉低,抗干扰就增强 dig_hc595_st_dr=0; dig_hc595_ds_dr=0; } //LED灯的74HC595驱动函数 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01) { unsigned char i; unsigned char ucTempData; hc595_sh_dr=0; hc595_st_dr=0; ucTempData=ucLedStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucLedStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); hc595_st_dr=1; delay_short(1); hc595_sh_dr=0; //拉低,抗干扰就增强 hc595_st_dr=0; hc595_ds_dr=0; } void T0_time(void) interrupt 1 //定时中断 { TF0=0; //清除中断标志 TR0=0; //关中断 if(ucVoiceLock==0) //原子锁判断 { if(uiVoiceCnt!=0) { uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫 beep_dr=0; //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。 } else { ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。 beep_dr=1; //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。 } } if(ucDelayTimerLock==0) //原子锁判断 { if(uiDelayTimer>0) { uiDelayTimer--; //一气呵成的定时器延时方式的计时器 } } if(ucEepromError==1) //EEPROM出错 { if(ucEepromLock==0)//原子锁判断 { uiEepromCnt++; //间歇性蜂鸣器报警的计时器 } } key_scan(); //按键扫描函数 display_drive(); //数码管字模的驱动函数 TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b TL0=0x0b; TR0=1; //开中断 } void delay_short(unsigned int uiDelayShort) { unsigned int i; for(i=0;i9999) //不在范围内 { uiSetData1=0; //填入一个初始化数据 write_eeprom_int(0,uiSetData1); //存入uiSetData1,内部占用2个字节地址 } uiSetData2=read_eeprom_int(2); //读取uiSetData2,内部占用2个字节地址 if(uiSetData2>9999)//不在范围内 { uiSetData2=0; //填入一个初始化数据 write_eeprom_int(2,uiSetData2); //存入uiSetData2,内部占用2个字节地址 } uiSetData3=read_eeprom_int(4); //读取uiSetData3,内部占用2个字节地址 if(uiSetData3>9999)//不在范围内 { uiSetData3=0; //填入一个初始化数据 write_eeprom_int(4,uiSetData3); //存入uiSetData3,内部占用2个字节地址 } uiSetData4=read_eeprom_int(6); //读取uiSetData4,内部占用2个字节地址 if(uiSetData4>9999)//不在范围内 { uiSetData4=0; //填入一个初始化数据 write_eeprom_int(6,uiSetData4); //存入uiSetData4,内部占用2个字节地址 } }

总结陈词:
下一节开始讲关于单片机驱动实时时钟芯片的内容,欲知详情,请听下回分解-----利用DS1302做一个实时时钟 。
(未完待续,下节更精彩,不要走开哦)



菜鸟
2014-06-09 12:01:51 打赏
63楼
第四十八节:利用DS1302做一个实时时钟 。

开场白:
DS1302有两路独立电源输入,我们只要在其中一路电源上挂一个纽扣电池就可以实现掉电时钟继续跑的功能,纽扣电池作为备用电源必须比主电源的电压低一点。DS1302还给我们预留了一片RAM区,我们可以把一些数据存入到DS1302,只要DS1302的电池有电,那么它就相当于一个EEPROM。这个RAM区有什么用呢?因为RAM区的数据只要一掉电,所有的数据都会变成0x00或者0xff,也就是数据掉电会丢失,我们可以利用这个特点,可以在里面存入标志位数据,一旦发现这个数据改变了,就知道时钟的数据需要重新设置过,或者说明电池没电了。
在移植DS1302驱动程序中,有一个地方最容易出错,就是DS1302芯片的数据线DIO。我们编程时要特别留意这个IO口什么时候作为数据输入,什么时候作为数据输出,以便及时更改方向寄存器。对于51单片机,IO口在读取数据之前,要先置1。
这一节要教会大家六个知识点:
第一个:DS1302做实时时钟时,修改时间和读取时间的常见程序框架。
第二个:如何编写监控备用电池电量耗尽的监控程序。
第三个:在往DS1302写入数据修改时间之前,必须注意调整一下“日”的变量,因为每个月的最大天数是不一样的,有的一个月28天,有的一个月29天,有的一个月30天,有的一个月31天。
第四个:本程序第一次出现了电平按键,跟之前讲的下降沿按键不一样,请留意我是何如用软件滤波的,以及它具体的实现代码。
第五个:本程序第一次出现了一个按键按下去后,如果不松手就会触发两次事件,第一次是短按,第二次是长按3秒。请留意我是如何在之前的按键上略做修改就实现此功能的具体代码。
第六个:继续加深了解按键与显示是如何紧密关联起来的程序框架。

具体内容,请看源代码讲解。

(1) 硬件平台.
基于朱兆祺51单片机学习板。
旧版的朱兆祺51学习板在硬件上有一个bug,DS1302芯片附近的R43,R42两个电阻应该去掉,并且把R41的电阻换成0欧姆的电阻,或者直接短接起来。新版的朱兆祺51学习板已经改过来了。

(2)实现功能:
本程序有2两个窗口。
第1个窗口显示日期。显示格式“年-月-日”。注意中间有“-”分开。
第2个窗口显示时间。显示格式“时 分 秒”。注意中间没“-”,只有空格分开。
系统上电后,默认显示第2个窗口,实时显示动态的“时 分 秒”时间。此时按下S13按键不松手就会切换到显示日期的第1个窗口。松手后自动切换回第2个显示动态时间的窗口。
需要更改时间的时候,长按S9按键不松手超过3秒后,系统将进入修改时间的状态,切换到第1个日期窗口,并且显示“年”的两位数码管会闪烁,此时可以按S1或者S5加减按键修改年的参数,修改完年后,继续短按S9按键,会切换到“月”的参数闪烁状态,只要依次不断按下S9按键,就会依次切换年,月,日,时,分,秒的参数闪烁状态,最后修改完秒的参数后,系统会自动把我们修改设置的日期时间一次性写入DS1302芯片内部,达到修改日期时间的目的。
S13是电平变化按键,用来切换窗口的,专门用来查看当前日期。按下S13按键时显示日期窗口,松手后返回到显示实时时间的窗口。

本程序在使用过程中的注意事项:
(a)第一次上电时,蜂鸣器会报警,只要DS1302芯片的备用电池电量充足,这个时候断电再重启一次,就不会报警了。
(b)第一次上电时,时间没有走动,需要重新设置一下日期时间才可以。长按S9按键可以进入修改日期时间的状态。

(3)源代码讲解如下:


#include "REG52.H" #define const_dpy_time_half 200 //数码管闪烁时间的半值 #define const_dpy_time_all 400 //数码管闪烁时间的全值 一定要比const_dpy_time_half 大 #define const_voice_short 40 //蜂鸣器短叫的持续时间 #define const_key_time1 20 //按键去抖动延时的时间 #define const_key_time2 20 //按键去抖动延时的时间 #define const_key_time3 20 //按键去抖动延时的时间 #define const_key_time4 20 //按键去抖动延时的时间 #define const_key_time17 1200 //长按超过3秒的时间 #define const_ds1302_0_5s 200 //大概0.5秒的时间 #define const_ds1302_sampling_time 360 //累计主循环次数的时间,每次刷新采样时钟芯片的时间 #define WRITE_SECOND 0x80 //DS1302内部的相关地址 #define WRITE_MINUTE 0x82 #define WRITE_HOUR 0x84 #define WRITE_DATE 0x86 #define WRITE_MONTH 0x88 #define WRITE_YEAR 0x8C #define WRITE_CHECK 0xC2 //用来检查芯片的备用电池是否用完了的地址 #define READ_CHECK 0xC3 //用来检查芯片的备用电池是否用完了的地址 #define READ_SECOND 0x81 #define READ_MINUTE 0x83 #define READ_HOUR 0x85 #define READ_DATE 0x87 #define READ_MONTH 0x89 #define READ_YEAR 0x8D #define WRITE_PROTECT 0x8E void initial_myself(void); void initial_peripheral(void); void delay_short(unsigned int uiDelayShort); void delay_long(unsigned int uiDelaylong); //驱动数码管的74HC595 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01); void display_drive(void); //显示数码管字模的驱动函数 void display_service(void); //显示的窗口菜单服务程序 //驱动LED的74HC595 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01); void T0_time(void); //定时中断函数 void key_service(void); //按键服务的应用程序 void key_scan(void);//按键扫描函数 放在定时中断里 void ds1302_alarm_service(void); //ds1302出错报警 void ds1302_sampling(void); //ds1302采样程序,内部每秒钟采集更新一次 void Write1302 ( unsigned char addr, unsigned char dat );//修改时间的驱动 unsigned char Read1302 ( unsigned char addr );//读取时间的驱动 unsigned char bcd_to_number(unsigned char ucBcdTemp); //BCD转原始数值 unsigned char number_to_bcd(unsigned char ucNumberTemp); //原始数值转BCD //日调整 每个月份的日最大取值不同,有的最大28日,有的最大29日,有的最大30,有的最大31 unsigned char date_adjust(unsigned char ucYearTemp,unsigned char ucMonthTemp,unsigned char ucDateTemp); //日调整 sbit SCLK_dr =P1^3; sbit DIO_dr_sr =P1^4; sbit DS1302_CE_dr =P1^5; sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键 sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平 sbit beep_dr=P2^7; //蜂鸣器的驱动IO口 sbit eeprom_scl_dr=P3^7; //时钟线 sbit eeprom_sda_dr_sr=P3^6; //数据的输出线和输入线 sbit dig_hc595_sh_dr=P2^0; //数码管的74HC595程序 sbit dig_hc595_st_dr=P2^1; sbit dig_hc595_ds_dr=P2^2; sbit hc595_sh_dr=P2^3; //LED灯的74HC595程序 sbit hc595_st_dr=P2^4; sbit hc595_ds_dr=P2^5; unsigned int uiSampingCnt=0; //采集Ds1302的计时器,每秒钟更新采集一次 unsigned char ucKeySec=0; //被触发的按键编号 unsigned int uiKeyTimeCnt1=0; //按键去抖动延时计数器 unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志 unsigned int uiKeyTimeCnt2=0; //按键去抖动延时计数器 unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志 unsigned int uiKeyTimeCnt3=0; //按键去抖动延时计数器 unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志 unsigned int uiKey4Cnt1=0; //在软件滤波中,用到的变量 unsigned int uiKey4Cnt2=0; unsigned char ucKey4Sr=1; //实时反映按键的电平状态 unsigned char ucKey4SrRecord=0; //记录上一次按键的电平状态 unsigned int uiVoiceCnt=0; //蜂鸣器鸣叫的持续时间计数器 unsigned char ucVoiceLock=0; //蜂鸣器鸣叫的原子锁 unsigned char ucDigShow8; //第8位数码管要显示的内容 unsigned char ucDigShow7; //第7位数码管要显示的内容 unsigned char ucDigShow6; //第6位数码管要显示的内容 unsigned char ucDigShow5; //第5位数码管要显示的内容 unsigned char ucDigShow4; //第4位数码管要显示的内容 unsigned char ucDigShow3; //第3位数码管要显示的内容 unsigned char ucDigShow2; //第2位数码管要显示的内容 unsigned char ucDigShow1; //第1位数码管要显示的内容 unsigned char ucDigDot8; //数码管8的小数点是否显示的标志 unsigned char ucDigDot7; //数码管7的小数点是否显示的标志 unsigned char ucDigDot6; //数码管6的小数点是否显示的标志 unsigned char ucDigDot5; //数码管5的小数点是否显示的标志 unsigned char ucDigDot4; //数码管4的小数点是否显示的标志 unsigned char ucDigDot3; //数码管3的小数点是否显示的标志 unsigned char ucDigDot2; //数码管2的小数点是否显示的标志 unsigned char ucDigDot1; //数码管1的小数点是否显示的标志 unsigned char ucDigShowTemp=0; //临时中间变量 unsigned char ucDisplayDriveStep=1; //动态扫描数码管的步骤变量 unsigned char ucWd=2; //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。 unsigned char ucPart=0;//本程序的核心变量,局部显示变量。类似于二级菜单的变量。代表显示不同的局部。 unsigned char ucWd1Update=0; //窗口1更新显示标志 unsigned char ucWd2Update=1; //窗口2更新显示标志 unsigned char ucWd1Part1Update=0; //在窗口1中,局部1的更新显示标志 unsigned char ucWd1Part2Update=0; //在窗口1中,局部2的更新显示标志 unsigned char ucWd1Part3Update=0; //在窗口1中,局部3的更新显示标志 unsigned char ucWd2Part1Update=0; //在窗口2中,局部1的更新显示标志 unsigned char ucWd2Part2Update=0; //在窗口2中,局部2的更新显示标志 unsigned char ucWd2Part3Update=0; //在窗口2中,局部3的更新显示标志 unsigned char ucYear=0; //原始数据 unsigned char ucMonth=0; unsigned char ucDate=0; unsigned char ucHour=0; unsigned char ucMinute=0; unsigned char ucSecond=0; unsigned char ucYearBCD=0; //BCD码的数据 unsigned char ucMonthBCD=0; unsigned char ucDateBCD=0; unsigned char ucHourBCD=0; unsigned char ucMinuteBCD=0; unsigned char ucSecondBCD=0; unsigned char ucTemp1=0; //中间过渡变量 unsigned char ucTemp2=0; //中间过渡变量 unsigned char ucTemp4=0; //中间过渡变量 unsigned char ucTemp5=0; //中间过渡变量 unsigned char ucTemp7=0; //中间过渡变量 unsigned char ucTemp8=0; //中间过渡变量 unsigned char ucDelayTimerLock=0; //原子锁 unsigned int uiDelayTimer=0; unsigned char ucCheckDs1302=0; //检查Ds1302芯片是否正常 unsigned char ucDs1302Error=0; //Ds1302芯片的备用电池是否用完了的报警标志 unsigned char ucDs1302Lock=0;//原子锁 unsigned int uiDs1302Cnt=0; //间歇性蜂鸣器报警的计时器 unsigned char ucDpyTimeLock=0; //原子锁 unsigned int uiDpyTimeCnt=0; //数码管的闪烁计时器,放在定时中断里不断累加 //根据原理图得出的共阴数码管字模表 code unsigned char dig_table[]= { 0x3f, //0 序号0 0x06, //1 序号1 0x5b, //2 序号2 0x4f, //3 序号3 0x66, //4 序号4 0x6d, //5 序号5 0x7d, //6 序号6 0x07, //7 序号7 0x7f, //8 序号8 0x6f, //9 序号9 0x00, //无 序号10 0x40, //- 序号11 0x73, //P 序号12 }; void main() { initial_myself(); delay_long(100); initial_peripheral(); while(1) { key_service(); //按键服务的应用程序 ds1302_sampling(); //ds1302采样程序,内部每秒钟采集更新一次 display_service(); //显示的窗口菜单服务程序 ds1302_alarm_service(); //ds1302出错报警 } } /* 注释一: * 系统不用时时刻刻采集ds1302的内部数据,每隔一段时间更新采集一次就可以了。 * 这个间隔时间应该小于或者等于1秒钟的时间,否则在数码管上看不到每秒钟的时间变化。 */ void ds1302_sampling(void) //ds1302采样程序,内部每秒钟采集更新一次 { if(ucPart==0) //当系统不是处于设置日期和时间的情况下 { ++uiSampingCnt; //累计主循环次数的时间 if(uiSampingCnt>const_ds1302_sampling_time) //每隔一段时间就更新采集一次Ds1302数据 { uiSampingCnt=0; ucYearBCD=Read1302(READ_YEAR); //读取年 ucMonthBCD=Read1302(READ_MONTH); //读取月 ucDateBCD=Read1302(READ_DATE); //读取日 ucHourBCD=Read1302(READ_HOUR); //读取时 ucMinuteBCD=Read1302(READ_MINUTE); //读取分 ucSecondBCD=Read1302(READ_SECOND); //读取秒 ucYear=bcd_to_number(ucYearBCD); //BCD转原始数值 ucMonth=bcd_to_number(ucMonthBCD); //BCD转原始数值 ucDate=bcd_to_number(ucDateBCD); //BCD转原始数值 ucHour=bcd_to_number(ucHourBCD); //BCD转原始数值 ucMinute=bcd_to_number(ucMinuteBCD); //BCD转原始数值 ucSecond=bcd_to_number(ucSecondBCD); //BCD转原始数值 ucWd2Update=1; //窗口2更新显示时间 } } } //修改ds1302时间的驱动 ,注意,此处写入的是BCD码, void Write1302 ( unsigned char addr, unsigned char dat ) { unsigned char i,temp; //单片机驱动DS1302属于SPI通讯方式,根据我的经验,不用关闭中断 DS1302_CE_dr=0; //CE引脚为低,数据传送中止 delay_short(1); SCLK_dr=0; //清零时钟总线 delay_short(1); DS1302_CE_dr = 1; //CE引脚为高,逻辑控制有效 delay_short(1); //发送地址 for ( i=0; i<8; i++ ) //循环8次移位 { DIO_dr_sr = 0; temp = addr; if(temp&0x01) { DIO_dr_sr =1; } else { DIO_dr_sr =0; } delay_short(1); addr >>= 1; //右移一位 SCLK_dr = 1; delay_short(1); SCLK_dr = 0; delay_short(1); } //发送数据 for ( i=0; i<8; i++ ) //循环8次移位 { DIO_dr_sr = 0; temp = dat; if(temp&0x01) { DIO_dr_sr =1; } else { DIO_dr_sr =0; } delay_short(1); dat >>= 1; //右移一位 SCLK_dr = 1; delay_short(1); SCLK_dr = 0; delay_short(1); } DS1302_CE_dr = 0; delay_short(1); } //读取Ds1302时间的驱动 ,注意,此处读取的是BCD码, unsigned char Read1302 ( unsigned char addr ) { unsigned char i,temp,dat1; DS1302_CE_dr=0; //单片机驱动DS1302属于SPI通讯方式,根据我的经验,不用关闭中断 delay_short(1); SCLK_dr=0; delay_short(1); DS1302_CE_dr = 1; delay_short(1); //发送地址 for ( i=0; i<8; i++ ) //循环8次移位 { DIO_dr_sr = 0; temp = addr; if(temp&0x01) { DIO_dr_sr =1; } else { DIO_dr_sr =0; } delay_short(1); addr >>= 1; //右移一位 SCLK_dr = 1; delay_short(1); SCLK_dr = 0; delay_short(1); } /* 注释二: * 51单片机IO口的特点,在读取数据之前必须先输出高电平, * 如果是PIC,AVR等单片机,这里应该把IO方向寄存器设置为输入 */ DIO_dr_sr =1; //51单片机IO口的特点,在读取数据之前必须先输出高电平, temp=0; for ( i=0; i<8; i++ ) { temp>>=1; if(DIO_dr_sr==1) { temp=temp+0x80; } DIO_dr_sr =1; //51单片机IO口的特点,在读取数据之前必须先输出高电平 delay_short(1); SCLK_dr = 1; delay_short(1); SCLK_dr = 0; delay_short(1); } DS1302_CE_dr=0; delay_short(1); dat1=temp; return (dat1); } unsigned char bcd_to_number(unsigned char ucBcdTemp) //BCD转原始数值 { unsigned char ucNumberResult=0; unsigned char ucBcdTemp10; unsigned char ucBcdTemp1; ucBcdTemp10=ucBcdTemp; ucBcdTemp10=ucBcdTemp10>>4; ucBcdTemp1=ucBcdTemp; ucBcdTemp1=ucBcdTemp1&0x0f; ucNumberResult=ucBcdTemp10*10+ucBcdTemp1; return ucNumberResult; } unsigned char number_to_bcd(unsigned char ucNumberTemp) //原始数值转BCD { unsigned char ucBcdResult=0; unsigned char ucNumberTemp10; unsigned char ucNumberTemp1; ucNumberTemp10=ucNumberTemp; ucNumberTemp10=ucNumberTemp10/10; ucNumberTemp10=ucNumberTemp10<<4; ucNumberTemp10=ucNumberTemp10&0xf0; ucNumberTemp1=ucNumberTemp; ucNumberTemp1=ucNumberTemp1%10; ucBcdResult=ucNumberTemp10|ucNumberTemp1; return ucBcdResult; } //日调整 每个月份的日最大取值不同,有的最大28日,有的最大29日,有的最大30,有的最大31 unsigned char date_adjust(unsigned char ucYearTemp,unsigned char ucMonthTemp,unsigned char ucDateTemp) //日调整 { unsigned char ucDayResult; unsigned int uiYearTemp; unsigned int uiYearYu; ucDayResult=ucDateTemp; switch(ucMonthTemp) //根据不同的月份来修正不同的日最大值 { case 2: //二月份要计算是否是闰年 uiYearTemp=2000+ucYearTemp; uiYearYu=uiYearTemp%4; if(uiYearYu==0) //闰年 { if(ucDayResult>29) { ucDayResult=29; } } else { if(ucDayResult>28) { ucDayResult=28; } } break; case 4: case 6: case 9: case 11: if(ucDayResult>30) { ucDayResult=30; } break; } return ucDayResult; } void ds1302_alarm_service(void) //ds1302出错报警 { if(ucDs1302Error==1) //备用电池的电量用完了报警提示 { if(uiDs1302Cnt>const_ds1302_0_5s) //大概0.5秒钟蜂鸣器响一次 { ucDs1302Lock=1; //原子锁加锁 uiDs1302Cnt=0; //计时器清零 ucDs1302Lock=0; //原子锁解锁 ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //蜂鸣器声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt } } } void display_service(void) //显示的窗口菜单服务程序 { switch(ucWd) //本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。 { case 1: //显示日期窗口的数据 数据格式 NN-YY-RR 年-月-日 if(ucWd1Update==1) //窗口1要全部更新显示 { ucWd1Update=0; //及时清零标志,避免一直进来扫描 ucDigShow6=11; //显示一杠"-" ucDigShow3=11; //显示一杠"-" ucWd1Part1Update=1; //局部年更新显示 ucWd1Part2Update=1; //局部月更新显示 ucWd1Part3Update=1; //局部日更新显示 } if(ucWd1Part1Update==1)//局部年更新显示 { ucWd1Part1Update=0; ucTemp8=ucYear/10; //年 ucTemp7=ucYear%10; ucDigShow8=ucTemp8; //数码管显示实际内容 ucDigShow7=ucTemp7; } if(ucWd1Part2Update==1)//局部月更新显示 { ucWd1Part2Update=0; ucTemp5=ucMonth/10; //月 ucTemp4=ucMonth%10; ucDigShow5=ucTemp5; //数码管显示实际内容 ucDigShow4=ucTemp4; } if(ucWd1Part3Update==1) //局部日更新显示 { ucWd1Part3Update=0; ucTemp2=ucDate/10; //日 ucTemp1=ucDate%10; ucDigShow2=ucTemp2; //数码管显示实际内容 ucDigShow1=ucTemp1; } //数码管闪烁 switch(ucPart) //相当于二级菜单,根据局部变量的值,使对应的参数产生闪烁的动态效果。 { case 0: //都不闪烁 break; case 1: //年参数闪烁 if(uiDpyTimeCnt==const_dpy_time_half) { ucDigShow8=ucTemp8; //数码管显示实际内容 ucDigShow7=ucTemp7; } else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大 { ucDpyTimeLock=1; //原子锁加锁 uiDpyTimeCnt=0; //及时把闪烁记时器清零 ucDpyTimeLock=0; //原子锁解锁 ucDigShow8=10; //数码管显示空,什么都不显示 ucDigShow7=10; } break; case 2: //月参数闪烁 if(uiDpyTimeCnt==const_dpy_time_half) { ucDigShow5=ucTemp5; //数码管显示实际内容 ucDigShow4=ucTemp4; } else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大 { ucDpyTimeLock=1; //原子锁加锁 uiDpyTimeCnt=0; //及时把闪烁记时器清零 ucDpyTimeLock=0; //原子锁解锁 ucDigShow5=10; //数码管显示空,什么都不显示 ucDigShow4=10; } break; case 3: //日参数闪烁 if(uiDpyTimeCnt==const_dpy_time_half) { ucDigShow2=ucTemp2; //数码管显示实际内容 ucDigShow1=ucTemp1; } else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大 { ucDpyTimeLock=1; //原子锁加锁 uiDpyTimeCnt=0; //及时把闪烁记时器清零 ucDpyTimeLock=0; //原子锁解锁 ucDigShow2=10; //数码管显示空,什么都不显示 ucDigShow1=10; } break; } break; case 2: //显示时间窗口的数据 数据格式 SS FF MM 时 分 秒 if(ucWd2Update==1) //窗口2要全部更新显示 { ucWd2Update=0; //及时清零标志,避免一直进来扫描 ucDigShow6=10; //显示空 ucDigShow3=10; //显示空 ucWd2Part3Update=1; //局部时更新显示 ucWd2Part2Update=1; //局部分更新显示 ucWd2Part1Update=1; //局部秒更新显示 } if(ucWd2Part1Update==1)//局部时更新显示 { ucWd2Part1Update=0; ucTemp8=ucHour/10; //时 ucTemp7=ucHour%10; ucDigShow8=ucTemp8; //数码管显示实际内容 ucDigShow7=ucTemp7; } if(ucWd2Part2Update==1)//局部分更新显示 { ucWd2Part2Update=0; ucTemp5=ucMinute/10; //分 ucTemp4=ucMinute%10; ucDigShow5=ucTemp5; //数码管显示实际内容 ucDigShow4=ucTemp4; } if(ucWd2Part3Update==1) //局部秒更新显示 { ucWd2Part3Update=0; ucTemp2=ucSecond/10; //秒 ucTemp1=ucSecond%10; ucDigShow2=ucTemp2; //数码管显示实际内容 ucDigShow1=ucTemp1; } //数码管闪烁 switch(ucPart) //相当于二级菜单,根据局部变量的值,使对应的参数产生闪烁的动态效果。 { case 0: //都不闪烁 break; case 1: //时参数闪烁 if(uiDpyTimeCnt==const_dpy_time_half) { ucDigShow8=ucTemp8; //数码管显示实际内容 ucDigShow7=ucTemp7; } else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大 { ucDpyTimeLock=1; //原子锁加锁 uiDpyTimeCnt=0; //及时把闪烁记时器清零 ucDpyTimeLock=0; //原子锁解锁 ucDigShow8=10; //数码管显示空,什么都不显示 ucDigShow7=10; } break; case 2: //分参数闪烁 if(uiDpyTimeCnt==const_dpy_time_half) { ucDigShow5=ucTemp5; //数码管显示实际内容 ucDigShow4=ucTemp4; } else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大 { ucDpyTimeLock=1; //原子锁加锁 uiDpyTimeCnt=0; //及时把闪烁记时器清零 ucDpyTimeLock=0; //原子锁解锁 ucDigShow5=10; //数码管显示空,什么都不显示 ucDigShow4=10; } break; case 3: //秒参数闪烁 if(uiDpyTimeCnt==const_dpy_time_half) { ucDigShow2=ucTemp2; //数码管显示实际内容 ucDigShow1=ucTemp1; } else if(uiDpyTimeCnt>const_dpy_time_all) //const_dpy_time_all一定要比const_dpy_time_half 大 { ucDpyTimeLock=1; //原子锁加锁 uiDpyTimeCnt=0; //及时把闪烁记时器清零 ucDpyTimeLock=0; //原子锁解锁 ucDigShow2=10; //数码管显示空,什么都不显示 ucDigShow1=10; } break; } break; } } void key_scan(void)//按键扫描函数 放在定时中断里 { if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock1=0; //按键自锁标志清零 uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock1==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt1++; //累加定时中断次数 if(uiKeyTimeCnt1>const_key_time1) { uiKeyTimeCnt1=0; ucKeyLock1=1; //自锁按键置位,避免一直触发 ucKeySec=1; //触发1号键 } } if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock2=0; //按键自锁标志清零 uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock2==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt2++; //累加定时中断次数 if(uiKeyTimeCnt2>const_key_time2) { uiKeyTimeCnt2=0; ucKeyLock2=1; //自锁按键置位,避免一直触发 ucKeySec=2; //触发2号键 } } /* 注释三: * 注意,此处把一个按键的短按和长按的功能都实现了。 */ if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock3=0; //按键自锁标志清零 uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock3==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt3++; //累加定时中断次数 if(uiKeyTimeCnt3>const_key_time3) { uiKeyTimeCnt3=0; ucKeyLock3=1; //自锁按键置位,避免一直触发 ucKeySec=3; //短按触发3号键 } } else if(uiKeyTimeCnt3const_key_time4) { uiKey4Cnt2=0; ucKey4Sr=1; //实时反映按键松手时的电平状态 } } else { uiKey4Cnt2=0; //在软件滤波中,非常关键的语句!!!类似按键去抖动程序的及时清零 uiKey4Cnt1++; if(uiKey4Cnt1>const_key_time4) { uiKey4Cnt1=0; ucKey4Sr=0; //实时反映按键按下时的电平状态 } } } void key_service(void) //按键服务的应用程序 { switch(ucKeySec) //按键服务状态切换 { case 1:// 加按键 对应朱兆祺学习板的S1键 switch(ucWd) //在不同的窗口下,设置不同的参数 { case 1: switch(ucPart) //在不同的局部变量下,相当于二级菜单 { case 1: //年 ucYear++; if(ucYear>99) { ucYear=99; } ucWd1Part1Update=1; //更新显示 break; case 2: //月 ucMonth++; if(ucMonth>12) { ucMonth=12; } ucWd1Part2Update=1; //更新显示 break; case 3: //日 ucDate++; if(ucDate>31) { ucDate=31; } ucWd1Part3Update=1; //更新显示 break; } break; case 2: switch(ucPart) //在不同的局部变量下,相当于二级菜单 { case 1: //时 ucHour++; if(ucHour>23) { ucHour=23; } ucWd2Part1Update=1; //更新显示 break; case 2: //分 ucMinute++; if(ucMinute>59) { ucMinute=59; } ucWd2Part2Update=1; //更新显示 break; case 3: //秒 ucSecond++; if(ucSecond>59) { ucSecond=59; } ucWd2Part3Update=1; //更新显示 break; } break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; case 2:// 减按键 对应朱兆祺学习板的S5键 switch(ucWd) //在不同的窗口下,设置不同的参数 { case 1: switch(ucPart) //在不同的局部变量下,相当于二级菜单 { case 1: //年 ucYear--; if(ucYear>99) { ucYear=0; } ucWd1Part1Update=1; //更新显示 break; case 2: //月 ucMonth--; if(ucMonth<1) { ucMonth=1; } ucWd1Part2Update=1; //更新显示 break; case 3: //日 ucDate--; if(ucDate<1) { ucDate=1; } ucWd1Part3Update=1; //更新显示 break; } break; case 2: switch(ucPart) //在不同的局部变量下,相当于二级菜单 { case 1: //时 ucHour--; if(ucHour>23) { ucHour=0; } ucWd2Part1Update=1; //更新显示 break; case 2: //分 ucMinute--; if(ucMinute>59) { ucMinute=0; } ucWd2Part2Update=1; //更新显示 break; case 3: //秒 ucSecond--; if(ucSecond>59) { ucSecond=0; } ucWd2Part3Update=1; //更新显示 break; } break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; case 3://短按设置按键 对应朱兆祺学习板的S9键 switch(ucWd) //在不同的窗口下,设置不同的参数 { case 1: ucPart++; if(ucPart>3) { ucPart=1; ucWd=2; //切换到第二个窗口,设置时分秒 ucWd2Update=1; //窗口2更新显示 } ucWd1Update=1; //窗口1更新显示 break; case 2: if(ucPart>0) //在窗口2的时候,要第一次激活设置时间,必须是长按3秒才可以,这里短按激活不了第一次 { ucPart++; if(ucPart>3) //设置时间结束 { ucPart=0; /* 注释五: * 每个月份的天数最大值是不一样的,在写入ds1302时钟芯片内部数据前,应该做一次调整。 * 有的月份最大28天,有的月份最大29天,有的月份最大30天,有的月份最大31天, */ ucDate=date_adjust(ucYear,ucMonth,ucDate); //日调整 避免日的数值在某个月份超范围 ucYearBCD=number_to_bcd(ucYear); //原始数值转BCD ucMonthBCD=number_to_bcd(ucMonth); //原始数值转BCD ucDateBCD=number_to_bcd(ucDate); //原始数值转BCD ucHourBCD=number_to_bcd(ucHour); //原始数值转BCD ucMinuteBCD=number_to_bcd(ucMinute); //原始数值转BCD ucSecondBCD=number_to_bcd(ucSecond); //原始数值转BCD Write1302 (WRITE_PROTECT,0X00); //禁止写保护 Write1302 (WRITE_YEAR,ucYearBCD); //年修改 Write1302 (WRITE_MONTH,ucMonthBCD); //月修改 Write1302 (WRITE_DATE,ucDateBCD); //日修改 Write1302 (WRITE_HOUR,ucHourBCD); //小时修改 Write1302 (WRITE_MINUTE,ucMinuteBCD); //分钟修改 Write1302 (WRITE_SECOND,ucSecondBCD); //秒位修改 Write1302 (WRITE_PROTECT,0x80); //允许写保护 } ucWd2Update=1; //窗口2更新显示 } break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; case 17://长按3秒设置按键 对应朱兆祺学习板的S9键 switch(ucWd) //在不同的窗口下,设置不同的参数 { case 2: if(ucPart==0) //处于非设置时间的状态下,要第一次激活设置时间,必须是长按3秒才可以 { ucWd=1; ucPart=1; //进入到设置日期的状态下 ucWd1Update=1; //窗口1更新显示 } break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; } /* 注释六: * 注意,此处就是第一次出现的电平按键程序,跟以往的下降沿按键不一样。 * ucKey4Sr是经过软件滤波处理后,直接反应IO口电平状态的变量.当电平发生 * 变化时,就会切换到不同的显示界面,这里多用了一个ucKey4SrRecord变量 * 记录上一次的电平状态,是为了避免一直刷新显示。 */ if(ucKey4Sr!=ucKey4SrRecord) //说明S13的切换按键电平状态发生变化 { ucKey4SrRecord=ucKey4Sr;//及时记录当前最新的按键电平状态 避免一直进来触发 if(ucKey4Sr==1) //松手后切换到显示时间的窗口 { ucWd=2; //显示时分秒的窗口 ucPart=0; //进入到非设置时间的状态下 ucWd2Update=1; //窗口2更新显示 } else //按下去切换到显示日期的窗口 { ucWd=1; //显示年月日的窗口 ucPart=0; //进入到非设置时间的状态下 ucWd1Update=1; //窗口1更新显示 } } } void display_drive(void) { //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路 switch(ucDisplayDriveStep) { case 1: //显示第1位 ucDigShowTemp=dig_table[ucDigShow1]; if(ucDigDot1==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfe); break; case 2: //显示第2位 ucDigShowTemp=dig_table[ucDigShow2]; if(ucDigDot2==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfd); break; case 3: //显示第3位 ucDigShowTemp=dig_table[ucDigShow3]; if(ucDigDot3==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfb); break; case 4: //显示第4位 ucDigShowTemp=dig_table[ucDigShow4]; if(ucDigDot4==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xf7); break; case 5: //显示第5位 ucDigShowTemp=dig_table[ucDigShow5]; if(ucDigDot5==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xef); break; case 6: //显示第6位 ucDigShowTemp=dig_table[ucDigShow6]; if(ucDigDot6==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xdf); break; case 7: //显示第7位 ucDigShowTemp=dig_table[ucDigShow7]; if(ucDigDot7==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xbf); break; case 8: //显示第8位 ucDigShowTemp=dig_table[ucDigShow8]; if(ucDigDot8==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0x7f); break; } ucDisplayDriveStep++; if(ucDisplayDriveStep>8) //扫描完8个数码管后,重新从第一个开始扫描 { ucDisplayDriveStep=1; } } //数码管的74HC595驱动函数 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01) { unsigned char i; unsigned char ucTempData; dig_hc595_sh_dr=0; dig_hc595_st_dr=0; ucTempData=ucDigStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucDigStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } dig_hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); dig_hc595_st_dr=1; delay_short(1); dig_hc595_sh_dr=0; //拉低,抗干扰就增强 dig_hc595_st_dr=0; dig_hc595_ds_dr=0; } //LED灯的74HC595驱动函数 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01) { unsigned char i; unsigned char ucTempData; hc595_sh_dr=0; hc595_st_dr=0; ucTempData=ucLedStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucLedStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); hc595_st_dr=1; delay_short(1); hc595_sh_dr=0; //拉低,抗干扰就增强 hc595_st_dr=0; hc595_ds_dr=0; } void T0_time(void) interrupt 1 //定时中断 { TF0=0; //清除中断标志 TR0=0; //关中断 if(ucVoiceLock==0) //原子锁判断 { if(uiVoiceCnt!=0) { uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫 beep_dr=0; //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。 } else { ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。 beep_dr=1; //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。 } } if(ucDs1302Error>0) //EEPROM出错 { if(ucDs1302Lock==0)//原子锁判断 { uiDs1302Cnt++; //间歇性蜂鸣器报警的计时器 } } if(ucDpyTimeLock==0) //原子锁判断 { uiDpyTimeCnt++; //数码管的闪烁计时器 } key_scan(); //按键扫描函数 display_drive(); //数码管字模的驱动函数 TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b TL0=0x0b; TR0=1; //开中断 } void delay_short(unsigned int uiDelayShort) { unsigned int i; for(i=0;i
        

总结陈词:
下一节开始讲单片机驱动温度传感器芯片的内容,欲知详情,请听下回分解-----利用DS18B20做一个温控器 。

(未完待续,下节更精彩,不要走开哦)



菜鸟
2014-06-09 12:06:45 打赏
64楼
第四十九节:利用DS18B20做一个温控器 。

开场白:
DS18B20是一款常用的温度传感器芯片,它只占用单片机一根IO口,使用起来也特别方便。需要特别注意的是,正因为它只用一根IO口跟单片机通讯,因此读取一次温度值的通讯时间比较长,而且时序要求严格,在通讯期间不允许被单片机其它的中断干扰,因此在实际项目中,系统一旦选用了这款传感器芯片,就千万不要选用动态扫描数码管的显示方式。否则在关闭中断读取温度的时候,数码管的显示会有略微的“闪烁”现象。
DS18B20的测温范围是-55度至125度。在-10度至85度的温度范围内误差是+-0.5度,能满足大部分常用的测温要求。
这一节要教会大家三个知识点:
第一个:大概了解一下DS18B20的驱动程序。
第二个:做温控设备的时候,为了避免继电器在临界温度附近频繁跳动切换,应该设置一个缓冲温差。本程序的缓冲温差是2度。
第三个:继续加深了解按键,显示,传感器它们三者是如何紧密关联起来的程序框架。

具体内容,请看源代码讲解。

(1) 硬件平台.
基于朱兆祺51单片机学习板。

(2)实现功能:
本程序只有1个窗口。这个窗口有2个局部显示。
第1个局部是第7,6,5位数码管,显示设定的温度。
第2个局部是第4,3,2,1位数码管,显示实际环境温度。其中第4位数码管显示正负符号位。
S1按键是加键,S5按键是减键。通过它们可以直接设置“设定温度”。
一个LED灯用来模拟工控的继电器。
当实际温度低于或者等于设定温度2度以下时,模拟继电器的LED灯亮。
当实际温度等于或者大于设定温度时,模拟继电器的LED灯灭。
当实际温度处于设定温度和设定温度减去2度的范围内,模拟继电器的LED维持现状,这个2度范围用来做缓冲温差,避免继电器在临界温度附近频繁跳动切换。

(3)源代码讲解如下:

#include "REG52.H" #define const_voice_short 40 //蜂鸣器短叫的持续时间 #define const_key_time1 20 //按键去抖动延时的时间 #define const_key_time2 20 //按键去抖动延时的时间 #define const_ds18b20_sampling_time 180 //累计主循环次数的时间,每次刷新采样时钟芯片的时间 void initial_myself(void); void initial_peripheral(void); void delay_short(unsigned int uiDelayShort); void delay_long(unsigned int uiDelaylong); //驱动数码管的74HC595 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01); void display_drive(void); //显示数码管字模的驱动函数 void display_service(void); //显示的窗口菜单服务程序 //驱动LED的74HC595 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01); void T0_time(void); //定时中断函数 void key_service(void); //按键服务的应用程序 void key_scan(void);//按键扫描函数 放在定时中断里 void temper_control_service(void); //温控程序 void ds18b20_sampling(void); //ds18b20采样程序 void ds18b20_reset(); //复位ds18b20的时序 unsigned char ds_read_byte(void ); //读一字节 void ds_write_byte(unsigned char dat); //写一个字节 unsigned int get_temper(); //读取一次没有经过换算的温度数值 sbit dq_dr_sr=P2^6; //ds18b20的数据驱动线 sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit led_dr=P3^5; //LED灯,模拟工控中的继电器 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平 sbit beep_dr=P2^7; //蜂鸣器的驱动IO口 sbit dig_hc595_sh_dr=P2^0; //数码管的74HC595程序 sbit dig_hc595_st_dr=P2^1; sbit dig_hc595_ds_dr=P2^2; sbit hc595_sh_dr=P2^3; //LED灯的74HC595程序 sbit hc595_st_dr=P2^4; sbit hc595_ds_dr=P2^5; unsigned int uiSampingCnt=0; //采集Ds1302的计时器,每秒钟更新采集一次 unsigned char ucSignFlag=0; //正负符号。0代表正数,1代表负数,表示零下多少度。 unsigned long ulCurrentTemper=33; //实际温度 unsigned long ulSetTemper=26; //设定温度 unsigned int uiTemperTemp=0; //中间变量 unsigned char ucKeySec=0; //被触发的按键编号 unsigned int uiKeyTimeCnt1=0; //按键去抖动延时计数器 unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志 unsigned int uiKeyTimeCnt2=0; //按键去抖动延时计数器 unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志 unsigned int uiVoiceCnt=0; //蜂鸣器鸣叫的持续时间计数器 unsigned char ucVoiceLock=0; //蜂鸣器鸣叫的原子锁 unsigned char ucDigShow8; //第8位数码管要显示的内容 unsigned char ucDigShow7; //第7位数码管要显示的内容 unsigned char ucDigShow6; //第6位数码管要显示的内容 unsigned char ucDigShow5; //第5位数码管要显示的内容 unsigned char ucDigShow4; //第4位数码管要显示的内容 unsigned char ucDigShow3; //第3位数码管要显示的内容 unsigned char ucDigShow2; //第2位数码管要显示的内容 unsigned char ucDigShow1; //第1位数码管要显示的内容 unsigned char ucDigDot8; //数码管8的小数点是否显示的标志 unsigned char ucDigDot7; //数码管7的小数点是否显示的标志 unsigned char ucDigDot6; //数码管6的小数点是否显示的标志 unsigned char ucDigDot5; //数码管5的小数点是否显示的标志 unsigned char ucDigDot4; //数码管4的小数点是否显示的标志 unsigned char ucDigDot3; //数码管3的小数点是否显示的标志 unsigned char ucDigDot2; //数码管2的小数点是否显示的标志 unsigned char ucDigDot1; //数码管1的小数点是否显示的标志 unsigned char ucDigShowTemp=0; //临时中间变量 unsigned char ucDisplayDriveStep=1; //动态扫描数码管的步骤变量 unsigned char ucWd=1; //因为本程序只有1个窗口,在实际项目中,此处的ucWd也可以省略不要 unsigned char ucWd1Part1Update=1; //在窗口1中,局部1的更新显示标志 unsigned char ucWd1Part2Update=1; //在窗口1中,局部2的更新显示标志 unsigned char ucTemp1=0; //中间过渡变量 unsigned char ucTemp2=0; //中间过渡变量 unsigned char ucTemp3=0; //中间过渡变量 unsigned char ucTemp4=0; //中间过渡变量 unsigned char ucTemp5=0; //中间过渡变量 unsigned char ucTemp6=0; //中间过渡变量 unsigned char ucTemp7=0; //中间过渡变量 unsigned char ucTemp8=0; //中间过渡变量 //根据原理图得出的共阴数码管字模表 code unsigned char dig_table[]= { 0x3f, //0 序号0 0x06, //1 序号1 0x5b, //2 序号2 0x4f, //3 序号3 0x66, //4 序号4 0x6d, //5 序号5 0x7d, //6 序号6 0x07, //7 序号7 0x7f, //8 序号8 0x6f, //9 序号9 0x00, //无 序号10 0x40, //- 序号11 0x73, //P 序号12 }; void main() { initial_myself(); delay_long(100); initial_peripheral(); while(1) { key_service(); //按键服务的应用程序 ds18b20_sampling(); //ds18b20采样程序 temper_control_service(); //温控程序 display_service(); //显示的窗口菜单服务程序 } } /* 注释一: * 做温控设备的时候,为了避免继电器在临界温度附近频繁跳动切换,应该设置一个 * 缓冲温差。本程序的缓冲温差是2度。 */ void temper_control_service(void) //温控程序 { if(ucSignFlag==0) //是正数的前提下 { if(ulCurrentTemper>=ulSetTemper) //当实际温度大于等于设定温度时 { led_dr=0; //模拟继电器的LED灯熄灭 } else if(ulCurrentTemper<=(ulSetTemper-2)) //当实际温度小于等于设定温度2读以下时,这里的2是缓冲温差2度 { led_dr=1; //模拟继电器的LED灯点亮 } } else //是负数,说明是零下多少度的情况下 { led_dr=1; //模拟继电器的LED灯点亮 } } void ds18b20_sampling(void) //ds18b20采样程序 { ++uiSampingCnt; //累计主循环次数的时间 if(uiSampingCnt>const_ds18b20_sampling_time) //每隔一段时间就更新采集一次Ds18b20数据 { uiSampingCnt=0; ET0=0; //禁止定时中断 uiTemperTemp=get_temper(); //读取一次没有经过换算的温度数值 ET0=1; //开启定时中断 if((uiTemperTemp&0xf800)==0xf800) //是负号 { ucSignFlag=1; uiTemperTemp=~uiTemperTemp; //求补码 uiTemperTemp=uiTemperTemp+1; } else //是正号 { ucSignFlag=0; } ulCurrentTemper=0; //把int数据类型赋给long类型之前要先清零 ulCurrentTemper=uiTemperTemp; ulCurrentTemper=ulCurrentTemper*10; //为了先保留一位小数点,所以放大10倍, ulCurrentTemper=ulCurrentTemper>>4; //往右边移动4位,相当于乘以0.0625. 此时保留了1位小数点, ulCurrentTemper=ulCurrentTemper+5; //四舍五入 ulCurrentTemper=ulCurrentTemper/10; //四舍五入后,去掉小数点 ucWd1Part2Update=1; //局部2更新显示实时温度 } } //ds18b20驱动程序 unsigned int get_temper() //读取一次没有经过换算的温度数值 { unsigned char temper_H; unsigned char temper_L; unsigned int ds18b20_data=0; ds18b20_reset(); //复位ds18b20的时序 ds_write_byte(0xCC); ds_write_byte(0x44); ds18b20_reset(); //复位ds18b20的时序 ds_write_byte(0xCC); ds_write_byte(0xBE); temper_L=ds_read_byte(); temper_H=ds_read_byte(); ds18b20_data=temper_H; //把两个字节合并成一个int数据类型 ds18b20_data=ds18b20_data<<8; ds18b20_data=ds18b20_data|temper_L; return ds18b20_data; } void ds18b20_reset() //复位ds18b20的时序 { unsigned char x; dq_dr_sr=1; delay_short(8); dq_dr_sr=0; delay_short(80); dq_dr_sr=1; delay_short(14); x=dq_dr_sr; delay_short(20); } void ds_write_byte(unsigned char date) //写一个字节 { unsigned char i; for(i=0;i<8;i++) { dq_dr_sr=0; dq_dr_sr=date&0x01; delay_short(5); dq_dr_sr=1; date=date>>1; } } unsigned char ds_read_byte(void ) //读一字节 { unsigned char i; unsigned char date=0; for(i=0;i<8;i++) { dq_dr_sr=0; date=date>>1; dq_dr_sr=1; if(dq_dr_sr) { date=date|0x80; } delay_short(5); } return (date); } void display_service(void) //显示的窗口菜单服务程序 { switch(ucWd) //因为本程序只有1个窗口,在实际项目中,此处的ucWd也可以省略不要 { case 1: if(ucWd1Part1Update==1)//局部设定温度更新显示 { ucWd1Part1Update=0; ucTemp8=10; //显示空 if(ulSetTemper>=100) { ucTemp7=ulSetTemper%1000/100; //显示设定温度的百位 } else { ucTemp7=10; //显示空 } if(ulSetTemper>=10) { ucTemp6=ulSetTemper%100/10; //显示设定温度的十位 } else { ucTemp6=10; //显示空 } ucTemp5=ulSetTemper%10; //显示设定温度的个位 ucDigShow8=ucTemp8; //数码管显示实际内容 ucDigShow7=ucTemp7; ucDigShow6=ucTemp6; ucDigShow5=ucTemp5; } if(ucWd1Part2Update==1)//局部实际温度更新显示 { if(ucSignFlag==0) //正数 { ucTemp4=10; //显示空 } else //负数,说明是零下多少度的情况下 { ucTemp4=11; //显示负号- } if(ulCurrentTemper>=100) { ucTemp3=ulCurrentTemper%100/100; //显示实际温度的百位 } else { ucTemp3=10; //显示空 } if(ulCurrentTemper>=10) { ucTemp2=ulCurrentTemper%100/10; //显示实际温度的十位 } else { ucTemp2=10; //显示空 } ucTemp1=ulCurrentTemper%10; //显示实际温度的个数位 ucDigShow4=ucTemp4; //数码管显示实际内容 ucDigShow3=ucTemp3; ucDigShow2=ucTemp2; ucDigShow1=ucTemp1; } break; } } void key_scan(void)//按键扫描函数 放在定时中断里 { if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock1=0; //按键自锁标志清零 uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock1==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt1++; //累加定时中断次数 if(uiKeyTimeCnt1>const_key_time1) { uiKeyTimeCnt1=0; ucKeyLock1=1; //自锁按键置位,避免一直触发 ucKeySec=1; //触发1号键 } } if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位 { ucKeyLock2=0; //按键自锁标志清零 uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。 } else if(ucKeyLock2==0)//有按键按下,且是第一次被按下 { uiKeyTimeCnt2++; //累加定时中断次数 if(uiKeyTimeCnt2>const_key_time2) { uiKeyTimeCnt2=0; ucKeyLock2=1; //自锁按键置位,避免一直触发 ucKeySec=2; //触发2号键 } } } void key_service(void) //按键服务的应用程序 { switch(ucKeySec) //按键服务状态切换 { case 1:// 加按键 对应朱兆祺学习板的S1键 switch(ucWd) //因为本程序只有1个窗口,在实际项目中,此处的ucWd也可以省略不要 { case 1: //在窗口1下设置设定温度 ulSetTemper++; if(ulSetTemper>125) { ulSetTemper=125; } ucWd1Part1Update=1; //更新显示设定温度 break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; case 2:// 减按键 对应朱兆祺学习板的S5键 switch(ucWd) //因为本程序只有1个窗口,在实际项目中,此处的ucWd也可以省略不要 { case 1: //在窗口1下设置设定温度 if(ulSetTemper>2) //由于缓冲温差是2度,所以我人为规定最小允许设定的温度不能低于2度 { ulSetTemper--; } ucWd1Part1Update=1; //更新显示设定温度 break; } ucVoiceLock=1; //原子锁加锁,保护主函数与中断函数的共享变量uiVoiceCnt uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。 ucVoiceLock=0; //原子锁解锁,保护主函数与中断函数的共享变量uiVoiceCnt ucKeySec=0; //响应按键服务处理程序后,按键编号清零,避免一致触发 break; } } void display_drive(void) { //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路 switch(ucDisplayDriveStep) { case 1: //显示第1位 ucDigShowTemp=dig_table[ucDigShow1]; if(ucDigDot1==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfe); break; case 2: //显示第2位 ucDigShowTemp=dig_table[ucDigShow2]; if(ucDigDot2==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfd); break; case 3: //显示第3位 ucDigShowTemp=dig_table[ucDigShow3]; if(ucDigDot3==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfb); break; case 4: //显示第4位 ucDigShowTemp=dig_table[ucDigShow4]; if(ucDigDot4==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xf7); break; case 5: //显示第5位 ucDigShowTemp=dig_table[ucDigShow5]; if(ucDigDot5==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xef); break; case 6: //显示第6位 ucDigShowTemp=dig_table[ucDigShow6]; if(ucDigDot6==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xdf); break; case 7: //显示第7位 ucDigShowTemp=dig_table[ucDigShow7]; if(ucDigDot7==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xbf); break; case 8: //显示第8位 ucDigShowTemp=dig_table[ucDigShow8]; if(ucDigDot8==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0x7f); break; } ucDisplayDriveStep++; if(ucDisplayDriveStep>8) //扫描完8个数码管后,重新从第一个开始扫描 { ucDisplayDriveStep=1; } } //数码管的74HC595驱动函数 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01) { unsigned char i; unsigned char ucTempData; dig_hc595_sh_dr=0; dig_hc595_st_dr=0; ucTempData=ucDigStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucDigStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } dig_hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); dig_hc595_st_dr=1; delay_short(1); dig_hc595_sh_dr=0; //拉低,抗干扰就增强 dig_hc595_st_dr=0; dig_hc595_ds_dr=0; } //LED灯的74HC595驱动函数 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01) { unsigned char i; unsigned char ucTempData; hc595_sh_dr=0; hc595_st_dr=0; ucTempData=ucLedStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucLedStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); hc595_st_dr=1; delay_short(1); hc595_sh_dr=0; //拉低,抗干扰就增强 hc595_st_dr=0; hc595_ds_dr=0; } void T0_time(void) interrupt 1 //定时中断 { TF0=0; //清除中断标志 TR0=0; //关中断 if(ucVoiceLock==0) //原子锁判断 { if(uiVoiceCnt!=0) { uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫 beep_dr=0; //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。 } else { ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。 beep_dr=1; //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。 } } key_scan(); //按键扫描函数 display_drive(); //数码管字模的驱动函数 TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b TL0=0x0b; TR0=1; //开中断 } void delay_short(unsigned int uiDelayShort) { unsigned int i; for(i=0;i
        

总结陈词:
下一节开始讲单片机采集模拟信号的内容,欲知详情,请听下回分解-----利用ADC0832采集电压的模拟信号。

(未完待续,下节更精彩,不要走开哦)


菜鸟
2014-06-09 12:08:43 打赏
65楼
第五十节:利用ADC0832采集电压信号,用平均法和区间法进行软件滤波处理。

开场白:
ADC0832是一款常用的8位AD采样芯片,通过它可以把外部的模拟电压信号转换成数字信号,然后给单片机进行换算,显示等处理。
这一节要教会大家五个知识点:
第一个:分辨率的算法。有些书上说8位AD最高分辩可达到256级(0xff+1),当输入电压是0---5V时,电压精度为19.53mV(5000mV除以256),我认为这种说法是错误的。8位AD的最高分辨率应该是255级(0xff),当输入电压是0---5V时,电压精度为19.61mV(5000mV除以255)。
第二个:用求平均值的滤波法,可以使AD采样的数据更加圆滑,去除小毛刺。
第三个:用区间滤波法,在一些干扰很大的场合,可以避免末尾小数点的数据频繁跳动。
第四个:如何使系统可以采集到更高的电压。由于ADC0832直接采集的电压最大不能超过5V,如果要采集的最大电压是25V该怎么办?我们只要在外部多增加1个10K的电阻和1个40K的电阻组成分压电路,把25V分压成5V,然后再让ADC0832采样,这时采样到的数据只要乘以5的系数,就可以得到超过5V的实际电压。选择分压电阻时,阻值尽量不要太小,一般要10K级别以上,阻值大一点,对被采样的系统干扰影响就越小。
第五个:如何有效保护AD通道口。我在一些电压不稳定的工控场合,一般是在AD通道口对负极反接一个瞬变二极管SA5.0A。当电压超过5V时,瞬变二极管会导通吸收掉多余的能量,把电压降下来,避免AD通道口烧坏。

具体内容,请看源代码讲解。

(1) 硬件平台.
基于朱兆祺51单片机学习板。

(2)实现功能:
本程序有2个局部显示。
第1个局部是第8,7,6,5位数码管,显示没有经过滤波处理的实际电压值。此时能观察到未经滤波的数据不太稳定,末尾小数点数据会有跳动的现象
第2个局部是第4,3,2,1位数码管,显示经过平均法,区间法滤波的实际电压值。此时能观察到经过滤波后的数据很稳定,没有跳动的现象

系统保留3位小数点。手动调节可调电阻时,可以看到显示的数据在变化。

(3)源代码讲解如下:

#include "REG52.H" #define const_voice_short 40 //蜂鸣器短叫的持续时间 void initial_myself(void); void initial_peripheral(void); void delay_short(unsigned int uiDelayShort); void delay_long(unsigned int uiDelaylong); //驱动数码管的74HC595 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01); void display_drive(void); //显示数码管字模的驱动函数 void display_service(void); //显示的窗口菜单服务程序 //驱动LED的74HC595 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01); void T0_time(void); //定时中断函数 void ad_sampling_service(void); //AD采样与处理的服务程序 sbit led_dr=P3^5; //LED灯 sbit beep_dr=P2^7; //蜂鸣器的驱动IO口 sbit dig_hc595_sh_dr=P2^0; //数码管的74HC595程序 sbit dig_hc595_st_dr=P2^1; sbit dig_hc595_ds_dr=P2^2; sbit hc595_sh_dr=P2^3; //LED灯的74HC595程序 sbit hc595_st_dr=P2^4; sbit hc595_ds_dr=P2^5; sbit adc0832_clk_dr = P1^2; // 定义adc0832的引脚 sbit adc0832_cs_dr = P1^0; sbit adc0832_data_sr_dr = P1^1; unsigned char ucDigShow8; //第8位数码管要显示的内容 unsigned char ucDigShow7; //第7位数码管要显示的内容 unsigned char ucDigShow6; //第6位数码管要显示的内容 unsigned char ucDigShow5; //第5位数码管要显示的内容 unsigned char ucDigShow4; //第4位数码管要显示的内容 unsigned char ucDigShow3; //第3位数码管要显示的内容 unsigned char ucDigShow2; //第2位数码管要显示的内容 unsigned char ucDigShow1; //第1位数码管要显示的内容 unsigned char ucDigDot8; //数码管8的小数点是否显示的标志 unsigned char ucDigDot7; //数码管7的小数点是否显示的标志 unsigned char ucDigDot6; //数码管6的小数点是否显示的标志 unsigned char ucDigDot5; //数码管5的小数点是否显示的标志 unsigned char ucDigDot4; //数码管4的小数点是否显示的标志 unsigned char ucDigDot3; //数码管3的小数点是否显示的标志 unsigned char ucDigDot2; //数码管2的小数点是否显示的标志 unsigned char ucDigDot1; //数码管1的小数点是否显示的标志 unsigned char ucDigShowTemp=0; //临时中间变量 unsigned char ucDisplayDriveStep=1; //动态扫描数码管的步骤变量 unsigned char ucWd1Part1Update=1; //在窗口1中,局部1的更新显示标志 unsigned char ucWd1Part2Update=1; //在窗口1中,局部2的更新显示标志 unsigned char ucTemp1=0; //中间过渡变量 unsigned char ucTemp2=0; //中间过渡变量 unsigned char ucTemp3=0; //中间过渡变量 unsigned char ucTemp4=0; //中间过渡变量 unsigned char ucTemp5=0; //中间过渡变量 unsigned char ucTemp6=0; //中间过渡变量 unsigned char ucTemp7=0; //中间过渡变量 unsigned char ucTemp8=0; //中间过渡变量 unsigned char ucAD=0; //AD值 unsigned char ucCheckAD=0; //用来做校验对比的AD值 unsigned long ulTemp=0; //参与换算的中间变量 unsigned long ulTempFilterV=0; //参与换算的中间变量 unsigned long ulBackupFilterV=5000; //备份最新采样数据的中间变量 unsigned char ucSamplingCnt=0; //统计采样的次数 本程序采样8次后求平均值 unsigned long ulV=0; //未经滤波处理的实时电压值 unsigned long ulFilterV=0; //经过滤波后的实时电压值 //根据原理图得出的共阴数码管字模表 code unsigned char dig_table[]= { 0x3f, //0 序号0 0x06, //1 序号1 0x5b, //2 序号2 0x4f, //3 序号3 0x66, //4 序号4 0x6d, //5 序号5 0x7d, //6 序号6 0x07, //7 序号7 0x7f, //8 序号8 0x6f, //9 序号9 0x00, //无 序号10 0x40, //- 序号11 0x73, //P 序号12 }; void main() { initial_myself(); delay_long(100); initial_peripheral(); while(1) { ad_sampling_service(); //AD采样与处理的服务程序 display_service(); //显示的窗口菜单服务程序 } } void ad_sampling_service(void) //AD采样与处理的服务程序 { unsigned char i; ucAD=0; //AD值 ucCheckAD=0; //用来做校验对比的AD值 /* 片选信号置为低电平 */ adc0832_cs_dr = 0; /* 第一个脉冲,开始位 */ adc0832_data_sr_dr = 1; adc0832_clk_dr = 0; delay_short(1); adc0832_clk_dr = 1; /* 第二个脉冲,选择通道 */ adc0832_data_sr_dr = 1; adc0832_clk_dr = 0; adc0832_clk_dr = 1; /* 第三个脉冲,选择通道 */ adc0832_data_sr_dr = 0; adc0832_clk_dr = 0; adc0832_clk_dr = 1; /* 数据线输出高电平 */ adc0832_data_sr_dr = 1; delay_short(2); /* 第一个下降沿 */ adc0832_clk_dr = 1; adc0832_clk_dr = 0; delay_short(1); /* AD值开始送出 */ for (i = 0; i < 8; i++) { ucAD <<= 1; adc0832_clk_dr = 1; adc0832_clk_dr = 0; if (adc0832_data_sr_dr==1) { ucAD |= 0x01; } } /* 用于校验的AD值开始送出 */ for (i = 0; i < 8; i++) { ucCheckAD >>= 1; if (adc0832_data_sr_dr==1) { ucCheckAD |= 0x80; } adc0832_clk_dr = 1; adc0832_clk_dr = 0; } /* 片选信号置为高电平 */ adc0832_cs_dr = 1; if(ucCheckAD==ucAD) //检验相等 { ulTemp=0; //把char类型数据赋值给long类型数据之前,必须先清零 ulTemp=ucAD; //把char类型数据赋值给long类型数据,参与乘除法运算的数据,为了避免运算结果溢出,我都用long类型 /* 注释一: * 因为保留3为小数点,这里的5000代表5.000V。ulTemp/255代表分辨率. * 有些书上说8位AD最高分辩可达到256级(0xff+1),我认为这种说法是错误的。 * 8位AD最高分辩应该是255级(0xff),所以这里除以255,而不是256. */ ulTemp=5000*ulTemp/255; //进行电压换算 ulV=ulTemp; //得到未经滤波处理的实时电压值 ucWd1Part1Update=1; //局部更新显示未经滤波处理的电压 ulTempFilterV=ulTempFilterV+ulTemp; //累加8次后求平均值 ucSamplingCnt++; //统计已经采样累计的次数 if(ucSamplingCnt>=8) { /* 注释二: * 求平均值滤波法,为了得到的数据更加圆滑,去除小毛刺。 * 向右边移动3位相当于除以8。 */ ulTempFilterV=ulTempFilterV>>3; //求平均值滤波法 /* 注释三: * 以下区间滤波法,为了避免末尾小数点的数据频繁跳动。 * 这里的20用于区间滤波法的正负偏差,这里的20代表0.020V。 * 意思是只要最近采集到的数据在正负0.020V偏差范围内,就不更新。 */ if(ulBackupFilterV>=20) //最近备份的上一次数据大于等于0.02V的情况下 { if(ulTempFilterV<(ulBackupFilterV-20)||ulTempFilterV>(ulBackupFilterV+20)) //在正负0.020V偏差范围外,更新 { ulBackupFilterV=ulTempFilterV; //备份最新采样的数据,方便下一次对比判断 ulFilterV=ulTempFilterV; //得到经过滤波处理的实时电压值 ucWd1Part2Update=1; //局部更新显示经过滤波处理的电压 } } else //最近备份的上一次数据小于0.02V的情况下 { if(ulTempFilterV>(ulBackupFilterV+20)) //在正0.020V偏差范围外,更新 { ulBackupFilterV=ulTempFilterV; //备份最新采样的数据,方便下一次对比判断 ulFilterV=ulTempFilterV; //得到经过滤波处理的实时电压值 ucWd1Part2Update=1; //局部更新显示经过滤波处理的电压 } } ucSamplingCnt=0; //清零,为下一轮采样滤波作准备。 ulTempFilterV=0; } } } void display_service(void) //显示的窗口菜单服务程序 { if(ucWd1Part1Update==1)//未经滤波处理的实时电压更新显示 { ucWd1Part1Update=0; ucTemp8=ulV%10000/1000; //显示电压值个位 ucTemp7=ulV%1000/100; //显示电压值小数点后第1位 ucTemp6=ulV%100/10; //显示电压值小数点后第2位 ucTemp5=ulV%10; //显示电压值小数点后第3位 ucDigShow8=ucTemp8; //数码管显示实际内容 ucDigShow7=ucTemp7; ucDigShow6=ucTemp6; ucDigShow5=ucTemp5; } if(ucWd1Part2Update==1)//经过滤波处理后的实时电压更新显示 { ucWd1Part2Update=0; ucTemp4=ulFilterV%10000/1000; //显示电压值个位 ucTemp3=ulFilterV%1000/100; //显示电压值小数点后第1位 ucTemp2=ulFilterV%100/10; //显示电压值小数点后第2位 ucTemp1=ulFilterV%10; //显示电压值小数点后第3位 ucDigShow4=ucTemp4; //数码管显示实际内容 ucDigShow3=ucTemp3; ucDigShow2=ucTemp2; ucDigShow1=ucTemp1; } } void display_drive(void) { //以下程序,如果加一些数组和移位的元素,还可以压缩容量。但是鸿哥追求的不是容量,而是清晰的讲解思路 switch(ucDisplayDriveStep) { case 1: //显示第1位 ucDigShowTemp=dig_table[ucDigShow1]; if(ucDigDot1==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfe); break; case 2: //显示第2位 ucDigShowTemp=dig_table[ucDigShow2]; if(ucDigDot2==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfd); break; case 3: //显示第3位 ucDigShowTemp=dig_table[ucDigShow3]; if(ucDigDot3==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xfb); break; case 4: //显示第4位 ucDigShowTemp=dig_table[ucDigShow4]; if(ucDigDot4==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xf7); break; case 5: //显示第5位 ucDigShowTemp=dig_table[ucDigShow5]; if(ucDigDot5==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xef); break; case 6: //显示第6位 ucDigShowTemp=dig_table[ucDigShow6]; if(ucDigDot6==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xdf); break; case 7: //显示第7位 ucDigShowTemp=dig_table[ucDigShow7]; if(ucDigDot7==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0xbf); break; case 8: //显示第8位 ucDigShowTemp=dig_table[ucDigShow8]; if(ucDigDot8==1) { ucDigShowTemp=ucDigShowTemp|0x80; //显示小数点 } dig_hc595_drive(ucDigShowTemp,0x7f); break; } ucDisplayDriveStep++; if(ucDisplayDriveStep>8) //扫描完8个数码管后,重新从第一个开始扫描 { ucDisplayDriveStep=1; } } //数码管的74HC595驱动函数 void dig_hc595_drive(unsigned char ucDigStatusTemp16_09,unsigned char ucDigStatusTemp08_01) { unsigned char i; unsigned char ucTempData; dig_hc595_sh_dr=0; dig_hc595_st_dr=0; ucTempData=ucDigStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucDigStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)dig_hc595_ds_dr=1; else dig_hc595_ds_dr=0; dig_hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); dig_hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } dig_hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); dig_hc595_st_dr=1; delay_short(1); dig_hc595_sh_dr=0; //拉低,抗干扰就增强 dig_hc595_st_dr=0; dig_hc595_ds_dr=0; } //LED灯的74HC595驱动函数 void hc595_drive(unsigned char ucLedStatusTemp16_09,unsigned char ucLedStatusTemp08_01) { unsigned char i; unsigned char ucTempData; hc595_sh_dr=0; hc595_st_dr=0; ucTempData=ucLedStatusTemp16_09; //先送高8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } ucTempData=ucLedStatusTemp08_01; //再先送低8位 for(i=0;i<8;i++) { if(ucTempData>=0x80)hc595_ds_dr=1; else hc595_ds_dr=0; hc595_sh_dr=0; //SH引脚的上升沿把数据送入寄存器 delay_short(1); hc595_sh_dr=1; delay_short(1); ucTempData=ucTempData<<1; } hc595_st_dr=0; //ST引脚把两个寄存器的数据更新输出到74HC595的输出引脚上并且锁存起来 delay_short(1); hc595_st_dr=1; delay_short(1); hc595_sh_dr=0; //拉低,抗干扰就增强 hc595_st_dr=0; hc595_ds_dr=0; } void T0_time(void) interrupt 1 //定时中断 { TF0=0; //清除中断标志 TR0=0; //关中断 display_drive(); //数码管字模的驱动函数 TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b TL0=0x0b; TR0=1; //开中断 } void delay_short(unsigned int uiDelayShort) { unsigned int i; for(i=0;i
        

总结陈词:
这节用区间滤波法虽然可以解决小数点后面的数据出现频繁跳动的现象,但是也存在一个小问题,就是精度受到了影响,比如我们设置的正负偏差是0.02V,那就意味着系统存在0.02V的误差。有没有更好的办法解决这个问题?如果系统的末尾数据一直不断处于频繁跳动中,那么只能牺牲一点精度,我认为用区间法已经是最好的解决办法了,但是经过本次实验,我观察到未经过滤波处理的数据只是偶尔跳动,并非频繁跳动,所以下一节我会给大家介绍一种不用牺牲精度,又可以很好滤波的方法。欲知详情,请听下回分解-----利用ADC0832采集电压信号,用连续N次一致性的方法进行滤波处理。


(未完待续,下节更精彩,不要走开哦)


高工
2014-06-09 14:32:59 打赏
66楼
赞了

菜鸟
2014-06-09 20:24:30 打赏
67楼
学习了,看完第一节我就不自卑了。

菜鸟
2014-06-12 08:14:45 打赏
68楼
好贴

高工
2014-06-12 23:21:20 打赏
69楼

可以出书了

这个真心要顶


菜鸟
2014-06-13 14:27:15 打赏
70楼
GOOD

共146条 7/15 |‹ 5 6 7 8 9 10 ›| 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册]