PIC 数字电子时钟设计
时间:2010-11-02 来源:583893280
4.4.1 程序流程图
首先来看看整个程序的流程图。对整个程序的流程、功能有了全面掌握之后,我们才能全盘考虑,才能写出好的代码,减少BUG的出现。
main主函数主循环如图4-5所示。
由于本实例中涉及了定时器中断,所以,在此也要设计有关定时器中断的流程图,如图4-6所示。
当进入中断后,首先是保护现场,即保存变量等,并屏蔽中断,防止中断处理过程中,进入另一个中断,从而打乱程序的顺序。之后,将时间累计值加1,表 示又一次时间中断。之后,对定时器参数进行重新设置,包括TMR0溢出值的设置,才能保证下一次中断的准确产生。之后,恢复现场、使能中断,最后退出中 断。
图4-5 全局循环流程 |
图4-6 中断处理函数流程 |
4.4.2 预定义及全局变量
前面说到了多用变量来衔接各个函数,从而分工明确,也易于代码维护。本小节,我们来编写一下预定义,后面的代码就能根据这些预定义来编写不同的分支,调用不同的变量。
我们采用枚举类型来定义菜单,代码如下:
- enum my_menu
- {
- normol_run,
- clock_hour,
- clock_min,
- clock_sec,
- alarm_hour,
- alarm_min,
- alarm_sec
- };
上述代码定义了正常运行状态normol_run,用来表示正处于正常的运行状态,没有用户输入;调整时钟小时状态clock_hour用来表示现 在处于用户输入状态,正在更改时钟的小时数值;类似地,还有clock_min和clock_sec。闹钟部分,则有变量alarm_hour、 alarm_min和alarm_sec。
使用同样的方法,我们也可以用枚举类型来定义运行过程中用到的不同变量,代码如下:
- enum my_param
- {
- curr_cl_hour,
- curr_cl_min,
- curr_cl_sec,
- curr_al_hour,
- curr_al_min,
- curr_al_sec,
- temp_cl_hour,
- temp _cl_min,
- temp _cl_sec,
- temp _al_hour,
- temp _al_min,
- temp _al_sec
- }
上述代码,我们定义了当前正常运行状态下的时钟小时curr_cl_hour、时钟分钟curr_cl_min、时钟秒 数 curr_cl_sec、闹钟小时curr_al_hour、闹钟分钟curr_al_min、闹钟秒数curr_al_sec,以及临时状态下,即 用户调整时间/闹钟状态下的时钟小时temp_cl_hour、时钟分钟temp _cl_min、时钟秒数temp _cl_sec、闹钟小时temp_al_hour、闹钟分钟temp _al_min和闹钟秒数temp _al_sec。
下列代码是对键盘的定义,分别表示模式切换、增加、减少和确定:
- enum my_keyborad
- {
- mode,
- add,
- subtract,
- ok
-
}
4.4.3 main主函数及初始化
本小节,我们来讲述main主函数如何编写,代码如下:
- void main()
- {
- init();
- while(1)
- {
- count_time();
- keyboard();
- key_cmd();
- lcd_refresh();
- }
- }
上面的main函数,调理很清晰,程序开始后,进行初始化,之后就进入一个while大循环,依次进行时间计算、键盘扫描、键盘响应和液晶显示刷新函数。各个函数之间通过变量来衔接。
接下来,我们来看看初始化函数的编写。我们设计定时器是200次时钟周期溢出。在“硬件电路设计”一节中,我们采用4MHz的外部晶振,而PIC16F877内部的定时器采用晶振频率的1/4。故而定时器中断周期是 。
- void init()
- {
- OPTION=0b00000111 ;
- TMR0=56; //设置200次时钟周期溢出
- INTCON =0b10100000 ; //使能定时器中断
- TRISD=0;
- lcd_init();
- time_count = 0;
- curr_menu = normol_run;
- curr_cl_hour = 0;
- curr_cl_min = 0;
- curr_cl_sec = 0;
- curr_al_hour = 0;
- curr_al_min = 0;
- curr_al_sec = 0;
- temp_cl_hour = 0;
- temp_cl_min = 0;
- temp_cl_sec = 0;
- temp_al_hour = 0;
- temp_al_min = 0;
- temp_al_sec = 0;
- }
其中,液晶初始化函数的代码如下:
- void lcd_init()
- {
- delayMs(300); //最好延时300ms以上
- lcd_write(0x429,12); //专用初始化命令
- lcd_write(0x418,12); //内部RC振荡器
- lcd_write(0x401,12); //开启振荡器
- lcd_write(0x403,12); //开启显示器
- }
.4 定时器中断函数
为了保证定时准确,定时中断函数中只累计最小的时间单位,更高的时间单位放在while循环中累计,即count_time()函数(参见4.4.5节),以防妨碍定时中断的准确。
- void interrupt ISR(void)
- {
- ... //保护现场,屏蔽中断(根据实际工程修改)
- if(time_out>0)
- time_out--;
- time_count++;
- if((INTCON&0b00000100)!=0)
- {
- INTCONINTCON=INTCON&0b11111011;
- TMR0=0;
- PORTD++; }
- }
- ... //恢复现场,使能中断(根据实际工程修改)
- }
设计这个函数是为了保证定时的准确,不能由于定时中断函数中运行过多的程序,而耽误了中断时间,导致中断时间不准确。所以,时间的运算、进位及超时的判断都在while循环中进行。我们用count_time函数来表示。
- void count_time()
- {
- if(time_count == 5000)
- {
- time_count = 0;
- curr_cl_sec++;
- }
- if(curr_cl_sec == 60)
- {
- curr_cl_sec = 0;
- curr_cl_min++;
- }
- if(curr_cl_min == 60)
- {
- curr_cl_min = 0;
- curr_cl_hour++;
- }
- if(curr_cl_hour == 24)
- {
- curr_cl_sec = 0;
- }
- if( (curr_cl_hour == curr_al_hour)
- && (curr_cl_min == curr_al_min)
- &&( curr_cl_sec == curr_al_sec)
- {
- bell(); //蜂鸣器响起
- }
- if(time_out<=0)
- {
- curr_menu = normal_run;
- time_out = 0;
- }
- else if(time_out%800==0)
- {
- flag_Flash = ~flag_Flash; //每0.8秒闪烁一次
- }
- }
本小节,我们来看看段式液晶底层驱动的编写,有关LCM0825的指令,请参考数据手册。下列是写数据/命令给LCM0825的函数:
- void lcd_write(int dat,int length)
- {
- int temp,i;
- nCS;
- for(i=0;i=length;i++)
- {
- temp = dat<<i;
- nWR;
- if( length = 12)
- {
- if( temp & 0x800 ==0x800)
- DATA = 1;
- else
- DATA =0;
- }
- if( length = 13)
- {
- if( temp & 0x1000 ==0x1000)
- DATA = 1;
- else
- DATA =0;
- }
- WR;
- delayMs(100);
- }
- CS;
- WR
- DATA;
-
}
液晶刷新函数,顾名思义,就是根据状态用来刷新屏幕上的显示。我们可以将其分为4个部分,即刷新前置字符、刷新小时、刷新分钟和刷新秒数。代码如下所示:
- void lcd_refresh()
- {
- display_front();
- display_hour();
- display_min();
- display_sec();
- }
1. 显示前端提示子函数
前端显示子函数,也就是显示段式液晶上第1位和第2位的函数,用来表示当前是正常运行状态,或是调节时钟状态,或是调节闹钟状态,效果图如图4-7 所示。图的左侧是显示“CL”,表示CLOCK,调节时钟状态;中间是“AL”,表示ALARM,调节闹钟状态;右侧无显示,表示正常运行状态。
(点击查看大图)图4-7 前端显示效果图 |
- void display_front()
- {
- switch( curr_menu )
- {
- case normal_run :
- display_number(13,1,0); //第1个数码管不显示任何数字
- display_number(13,2,0); //第2个数码管不显示任何数字
- break;
- case clock_hour :
- case clock_min :
- case clock_sec :
- display_number(10,1,0); //第1个数码管显示“A”
- display_number(12,2,0); //第2个数码管显示“L”
- break;
- case alarm_hour :
- case alarm_min :
- case alarm_sec :
- display_number(11,1,0); //第1个数码管显示“C”
- display_number(12,2,0); //第2个数码管显示“L”
- break;
- default: break;
- }
- }
2. 显示小时子函数
显示小时子函数,在段式液晶的第3位和第4位上将小时显示出来,显示的效果如图4-8所示。注意,小时显示的第四位要加个小点DOT,以便和分钟区分。
图4-8 显示小时效果图 |
- void display_hour()
- {
- if(curr_menu == clock_hour)
- {
- display_number(curr_cl_hour/10,3,0);
- display_number(curr_cl_hour%10,4,1);
- }
- if(curr_menu == alarm_hour)
- {
- display_number(curr_al_hour/10,3,0);
- display_number(curr_al_hour%10,4,1);
- }
- }
3. 显示分钟子函数
相同的道理,分钟的显示是在段式液晶的第5位和第6位上显示的。效果图和小时显示一样,代码如下所示:
- void display_min()
- {
- if(curr_menu == clock_min)
- {
- display_number(curr_cl_min/10,5,0);
- display_number(curr_cl_min%10,6,1);
- }
- if(curr_menu == alarm_min)
- {
- display_number(curr_al_min/10,5,0);
- display_number(curr_al_min%10,6,1);
- }
- }
4. 显示秒数子函数
相同的道理,秒数的显示是在段式液晶的第7位和第8位上显示的。效果图和小时显示一样,代码如下所示:
- void display_sec()
- {
- if(curr_menu == clock_sec)
- {
- display_number(curr_cl_sec/10,7,0);
- display_number(curr_cl_sec%10,8,1);
- }
- if(curr_menu == alarm_sec)
- {
- display_number(curr_al_sec/10,7,0);
- display_number(curr_al_sec%10,8,1);
- }
- }
5. 显示数字子函数
我们来讲述前几小节经常调用的显示数字子函数display_number(),代码如下所示:
- void display_number(int number, int section, int dot)
- {
- int temp_dat;
- if(dot ==1)
- show_arry[0]=array[2*number]&0x1;
- temp_dat = 0x1400 + (2*section)<<4 + show_array[0];
- lcd_write(temp_dat, 13);
- temp_dat = 0x1400 + (2*section+1)<<4 + show_array[1];
- lcd_write(temp_dat, 13);
- }
4.4.8 键值读入程序
我们来讲述键盘扫描程序。根据“硬件电路设计”一节的设计,4个按键是连接在PORTB上面的。所以,键值读入程序的代码如下所示:
- int keyboard(void)
- {
- int keyvalue,key;
- keyvalue = PORTB;
- keyvaluekeyvalue = keyvalue & 0x0f
- switch(keyvalue)
- {
- case 0x1:
- key = OK;
- break;
- case 0x2:
- key = subtract;
- break;
- case 0x4:
- key = add;
- break;
- case 0x8:
- key = mode;
- break;
- default: break;
- }
- return keyvalue;
- }
4.4.9 键盘响应程序
上一小节,我们讲述了键盘扫描程序,已获取了当前输入的键值,之后,程序就进入键盘响应函数。
1. 键盘响应总分支
我们用switch来分支表示,这样,层次更加分明。代码如下所示:
- void key_cmd()
- {
- switch key:
- {
- case mode:
- mode_fun();
- time_out = 50000;
- break;
- case add:
- add_fun();
- time_out = 50000;
- break;
- case subtract:
- subtract_fun();
- time_out = 50000;
- break;
- case ok:
- ok_fun();
- time_out = 50000;
- break;
- default:break;
- }
- }
2. “模式切换”按键响应函数
“模式切换”按键,表示切换模式,即“调节时钟”、“调节闹钟”和“正常运行”互相切换。代码如下所示:
- void mode_fun()
- {
- if(curr_menu<alarm_sec)
- curr_menu++;
- if(curr_menu == alarm_sec)
- curr_menu = normol_run;
- }
3. “增加”按键响应函数
“增加”按键,表示增加输入值。代码如下所示:
- void add_fun()
- {
- switch( curr_menu )
- {
- case clok_hour:
- temp_cl_hour++;
- break;
- case clok_min:
- temp_cl_min++;
- break;
- case clok_sec:
- temp_cl_sec++;
- break;
- case alarm_hour:
- temp_al_hour++;
- break;
- case alarm_min:
- temp_al_min++;
- break;
- case alarm_sec:
- temp_al_sec++;
- break;
- }
- }
4. “减少”按键响应函数
“减少”按键,表示减少输入值。代码如下所示:
- void subtract_fun()
- {
- switch( curr_menu )
- {
- case clok_hour:
- temp_cl_hour--;
- break;
- case clok_min:
- temp_cl_min--;
- break;
- case clok_sec:
- temp_cl_sec--;
- break;
- case alarm_hour:
- temp_al_hour--;
- break;
- case alarm_min:
- temp_al_min--;
- break;
- case alarm_sec:
- temp_al_sec--;
- break;
- }
- }
5. “确认”按键响应函数
“确认”按键,表示确认当前输入的值,并保存到最终变量上。代码如下所示:
- void enter_fun()
- {
- if(time_out >= 0)
- {
- curr_cl_hour = temp_cl_hour;
- curr_cl_min = temp_cl_min;
- curr_cl_sec = temp_cl_sec;
- curr_al_hour = temp_al_hour;
- curr_al_min = temp_al_min;
- curr_al_sec = temp_al_sec;
- }
-
}
本章讲述了如何设计一个基于PIC单片机的数字电子闹钟。实现了时钟、闹钟功能,并具备调整时钟/闹钟的功能。读者学习之后,可以根据实际应用的需 要,更换器件,添加更多的软件设置,以达到符合自己需要的功能。比如:采用点阵式液晶,可显示图片;添加多个定时日程管理;采用矩阵键盘,增加输入功能按 键(reset等);软件上,更加细化定时,可以做成码表,精确到百分之一秒;支持二十四进制和十二进制并可切换。