这些函数大家应该很熟悉的 1:端口初始化, 2:中断初始化, 3:等待外设,尤其是一些继电器类需要延时的 4:一个条件编译,因为飞利浦的芯片支持6clock和12clock,一般MCU都是12clock,采用6clock速度可以提高一倍,条件编译就是说假设定义过SPEEDUP,比如有#define SPEEDUP,那么就编译SETBIT(CKCON, 0),若没有#define SPEEDUP,则编译RESETBIT(CKCON, 0),条件编译可能大家理解不了,可以先不管。需要说的是,往往在一套软件中,有不同的应用,比如LCD屏有两种,就可以采用条件编译,是选择A,还是选择B编译。SETBIT和RESET是一个宏定义,是为了简化编程用的,它的原型大家可以查看 #define SETBIT(A,B) (A |= 1 << (B)) /*A=Register, B=Bitnumber (7..0)*/ #define RESETBIT(A,B) (A &= ~(1 << (B))) /*A=Register, B=Bitnumber (7..0)*/ #define GETBIT(A,B) ((A >> B) & 0x01)
5:中断优先级初始化,目前设置的是串口的优先级是最高的,其次是系统时钟Timer2 6:串口初始化,6clock是115200bps,12clcok是57600bps。 7:界面初始化。 8:定时器2初始化,它做为系统时钟,时间间隔是20mS,也可以设置,MS2中特意采用T2的16bit自动重载模式,它虽然受开关中断影响有瞬时误差,但没有积累误差,它是MS2中非常关键的一个,整个系统必须依赖它来运行,这类似于操作系统里的节拍的概念,因为按键检测,软件时钟,软件虚拟定时器,例行程序都靠它来实现的。任何优先级高于T2的中断时间不能大于系统节拍,不然有可能引起系统崩溃,这一点切记。
9:开中断,系统运行。
初始化完后,回到main函数,初始化了四个伪任务,如下 MSTimerStart(100, test0); MSTimerStart(110, test1); MSTimerStart(120, test2); MSTimerStart(130, test3);
按第一个分析: 参数一是时间,单位为20mS,也就是系统节拍,值为100,就是指100* 20mS = 2S,test0叫回调函数,也就是说这个MSTimerStart注册了一个函数test0,它在2S之后运行。这是本人学手机里面软件定时器的用法做的,非常好用,举个例子: 设置一个闹钟,10S后响,软件如下: /*闹钟响铃 函数*/ void naozhong(void) { Beep; }
main() { … 按键后: MSTimerStart(10000 / 20, naozhong); … }
也就是说MSTimerStart函数把闹钟这个函数naozhong放到了系统中,注册登记了,到了需要的时间,系统自动的调用这个函数,不需要我们关心了。这个也是系统节拍引入后得到的一个好处。等一下我们分析软件虚拟定时器是怎么做的,先还是谈以上四个函数,第一个函数延迟100个节拍,第二个延时110个节拍,第三个延时120个节拍,第四个延时130个节拍,它们对应的函数是test0,test1,test2,test3。效果就是上面第三张图片,那我们接下来看test0: void test0(void) { uprintf("liweifeng
"); MSTimerStart(100, test0); } 先打印了一句话“liweifeng
”,我记得好像是一个叫李卫峰的过来,给他演示的时候改的。之后又把自己注册到系统中,2S后又重新运行自身,这个叫自我循环,中间的间隔时间自己设置,这有点类似于操作系统中的任务的概念,独立自我循环,但毕竟不是真的任务,所以本人把它叫做伪任务。
接下来分析一下这个软件虚拟定时器是怎么设计的,先简单说一下原理: 设一个数组 static MSTimer idata MSTimerArray[MSTIMER_NUMBER]; 类型是MSTimer类型,MSTimer的类型原型如下: typedef struct { word delay; void (*CallBack) ( void); }MSTimer;
也就是说,定义了一个二维数组,其中一个是时间,即多少个节拍,另外一个是函数指针,存放函数指针用的,当然这儿有点技巧,若不懂,先承认它,或者去看看书。
MSTimerStart原型如下: byte MSTimerStart(word Delay, void ( *CallBack ) ( void )) { byte i; for(i = 0; i < MSTIMER_NUMBER; i++) { if(!GETBIT(MSTimerIDRegister, i)) { SETBIT(MSTimerIDRegister, i); MSTimerArray.delay = Delay; MSTimerArray.CallBack = CallBack; return(i); } } ERRprintf("MSStartTimer
"); } 定义了一个静态变量MSTimerIDRegister,它用于标识那个软件定时器被使用了,先查找,若有空的,就在它对应的数组里把两个参数 时间 和 函数指针放到数组里,OK。
再看软件虚拟定时器的服务器,原型如下 void MSTimer_server(void) { byte i = 0; byte MSTimerIDRegisterMap; MSTimerIDRegisterMap = MSTimerIDRegister; while(MSTimerIDRegisterMap) { if((MSTimerIDRegisterMap & 0x01) == 1) { if(!(--MSTimerArray.delay)) { RESETBIT(MSTimerIDRegister, i); (*(MSTimerArray.CallBack))(); } } MSTimerIDRegisterMap = MSTimerIDRegisterMap >> 1; i++; } }
每次系统节拍到了之后,都会去查询有没有软件定时器在工作,若有,每一路都检查,减去1,之后再检查,若减到为0了,说明时间到了,就去调用被注册的函数,就是上面说到的test0,test1之类的。其实很简单。需要说明的是这个被调函数test0是在系统中断中执行的,所以test0的费用不能太高,在MS3中引入了另外一种被调方式,可以在boot.c的大循环中处理,就没有这个费用问题了。
接下来就是系统的一个核心内容,整个系统关联的关键,消息机制。 while(TRUE) { switch(msg_queue_out()) { case MSG_KEY: mmi_key_process(g_MsgReturnValue); break; case MSG_UART: uart_process(); break; case MSG_TEST: //special for test break; case MSG_NULL: break; default: break; } }
不停的读取消息,若是MSG_KEY,则处理按键消息,若是MSG_UART,就处理串口程序之类的,反正是什么消息,处理什么事情,就这么简单,那么消息怎么来呢,等一下说,先谈一下消息机制。 消息机制在当前的编程中是最基本的,它是连接两个事件的纽带,可以把一个系统模块化,结构分得很清晰,所以合理的消息机制很关键,以前很多人在大循环体系中采用做标记也算是一种消息。 大凡的消息机制采用指针方式,有头(point),尾(tail),循环的,(MS3采用这种方式)本来也想采用这个,后来想着MS2的对象是初学者,取消了,用了一个比较笨的方法: 1:入消息 void msg_queue_in(MSGTYPE msgType, MSGPOINT msgPoint) { if(MsgPoolPoint == MSG_STACK_DEPTH) { ERRprintf("msg_queue_in
"); return; } bEA = EA; EA = 0; MsgPool[MsgPoolPoint][0] = msgType; MsgPool[MsgPoolPoint++][1] = msgPoint; EA = bEA; } 第一个参数是消息类型,第二个是消息值,里面的处理方法一样,也是定义了一个二维数组,把这两个数据存起来,之后开关中断注意点就行了。
2:取消息,有三种,FIFO,FILO,PRIORITY(消息优先级)就讲一种,FIFO的吧,三种消息也是条件编译的,便于用户选择, byte msg_queue_out(void) { if(MsgPoolPoint == 0) { return(MSG_NULL); } bEA = EA; EA = 0; g_MsgReturnValue = MsgPool[0][1]; msgType = MsgPool[0][0]; if(--MsgPoolPoint) { for(i = 0; i < MsgPoolPoint; i++) { MsgPool[0] = MsgPool[i + 1][0]; MsgPool[1] = MsgPool[i + 1][1]; } } EA = bEA; return(msgType); } 就是把消息值保存到MsgReturnValue的共用变量里,之后数组向上拷贝,函数返回消息类型,也很简单,一看就懂。
在单片机里,采用消息还带来一个非常大的好处,有些时候函数套的级数比较深,也就是说函数调函数,调的比较多,尤其是界面之类的编程,那很占用栈的资源,MCU51的资源本来就不多,很容易引起内存不足,我们可以采用消息,把一个很长的调用分成好几块,通过消息把它们连起来,这样就可以降低内存,效果很好。
刚才讲解了boot.c main函数这个主线,还有一个主线要讲,那就是系统节拍,在system.c里面,初始化后,每20mS MCU自动中断进入 static void Timer2Server(void) interrupt 5 /*不要带指定寄存器,否则将产生移位指令出错*/ { ET2 = 0; /*close interrupt*/ TF2 = 0; /*clear interrupt flag*/
if(++RTCCounter == 50) { RTCCounter = 0; rtc_soft_routine(); /*定时器例行程序*/ } key_check(); /*按键检测*/
if(MSTimerIDRegister > 0) /*软件虚拟定时器*/ { MSTimer_server(); } routine_process(); /*运行例行任务程序*/
ET2 = 1; }
上面注析很清楚了,软件虚拟定时器已经讲过,接下来讲讲key_check();本人自认为这一块写的不错,看原型 void key_check(void) /*5*5=25mS*/ { byte KeyWord; if(!g_bKeyEnable) /*是否开按键*/ return; /*采用P1口,4×4扫描,16个按键,先输出高四位为0,读取低四位数据,再输出低四位为0,读取高四位,把读取的两部分合并*/ P1 = 0x0F; KeyWord = P1 & 0x0F; P1 = 0xF0; KeyWord = KeyWord |( P1 & 0xF0); 检测按键,若是0xFF,则无按键 if(KeyWord==0xFF) { KeyCounter=0; if(KeyIntervalSafeguard) { KeyIntervalSafeguard--; } return; } 若有按键,静态计数器加一 KeyCounter++; 若计数器累加到KEY_SHORT_INTERVAL,,抛出一个按键消息 if(KeyCounter==KEY_SHORT_INTERVAL) { if(!KeyIntervalSafeguard) { msg_queue_in(MSG_KEY,KeyMap(KeyWord)); } KeyIntervalSafeguard=3; }
若计数器累加到KEY_ LONG _INTERVAL,,类似于PC按键抛出消息 if(KeyCounter==KEY_LONG_INTERVAL) { msg_queue_in(MSG_KEY,KeyMap(KeyWord)); KeyCounter-=4; } }
说明:一般的按键处理采用中断,检测到一个按键后采用delay函数延时20mS消抖动后再读取,这样做的一个坏处是白白浪费了系统20mS,MS2采用系统节拍来处理,它的间隔就是20mS,采用静态计数器,当有按键的时候,累加一次,达到要求就抛出消息,这样把按键处理独立化了,并且采用P1口,按键很容易达到16个,上面的代码跟MS2中有点区别,MS2中因为需要超过16个按键,所以增加了一个GPIO口T0,分析的时候就去掉了。这样的按键处理,在没有按键的时候也没增加多少费用,一检测到没有按键就退出了。需要提一下的是KeyIntervalSafeguard,这个可能大家不容易明白,它是为了保证按键的可靠性加入的,有这样一种情况会导致按键误触发,就是按键氧化,接触不良,在100mS内可能导致多次按键事件,这是不允许的,所以加入这个变量,效果非常好。
提到消息机制,我们可以看下面的程序 static void UartInterruptServer(void) interrupt 4 { ES = 0; RI = 0;
msg_queue_in(MSG_KEY,SBUF); ES=1; }
这一段就是把串口当作按键来处理了,上位机发送一个串口数据过来,当作按键处理,这是消息的灵活处。
好了,该讲的架构都讲完了,还有不清楚的再补充了,MS2相比MS3,一些细节点还没有处理好,在MS3中采用了循环指针的消息机制,并且是16bit,避免一些临界态问题,软件定时器增加了一个模式,可以在大循环里处理,还有就是软硬件分开了,把硬件独立出来了。 看完MS2,请看MS3,技巧性的稍多一些,学了一些uCOS的概念。
SI使用说明在MS2.rar内有包含。
请各位多提一些意见,便于本人的提高,当然不要怕争论,有争论才有民主,有争论才有真理!
|