工程下载地址为:http://download.csdn.net/detail/toraloo/5685109
此工程实现功能是为3个任务的切换运行与数码管动态显示,任务1:扫描获取按键值,然后通过串口发出对应的数据;任务2:控制led闪烁;任务3:控制蜂鸣器报警。
数据结构关联,如图1所示。Task_stack块由task1_stack、task2_stack、task3_stack三个数组组成,它们结构为0号元素保留,1号元素保存函数入口地址低8位,2号元素保存函数入口地址高8位。Task_sp块中的task_sp每个元素保存taskn_stack的栈元素(即数组最后位元素)。Funcation_address_map块为各函数入口地址。
图 1
图上Task_stack块数据与Task_sp映射关系通过如下两个宏实现:
#define SAVE_ENTRY do { \ task1_stack[1] = (unsigned int)scan_button & 0xff; \ task1_stack[2] = (unsigned int)scan_button >> 8; \ task2_stack[1] = (unsigned int)flash_led & 0xff; \ task2_stack[2] = (unsigned int)flash_led >> 8; \ task3_stack[1] = (unsigned int)control_bee & 0xff; \ task3_stack[2] = (unsigned int)control_bee >> 8; \ } while (0) #define SAVE_SP do { \ task_sp[0] = task1_stack; \ task_sp[0] += 2; \ task_sp[1] = task2_stack; \ task_sp[1] += 2; \ task_sp[2] = task3_stack; \ task_sp[2] += 2; \ } while (0)
接下来是关于函数调用与返回时,对于这次设计需要关心的部分进行讲述。当发生一个函数调用时(lcall/call),有如下至关重要的几步:
1. SP<--SP+1 2. (SP)<--PC低8位 3. SP<--SP+1 4.(SP)<--PC高8位
而当发生一个函数的返回时(ret/reti):
1. PC高8位<--SP 2. SP<--SP-1 3. PC低8位<--(SP) 4. SP<--SP-1
(注:在此只讲述了PC与SP在函数调用/返回的变化,实际设置一个调度器的话还要考虑其他变量的保存与恢复)
如上所述,即可得知要使函数真正实现调用与退出则主要关系到PC的变化,而这两个过程中影响PC变化的便是SP,那么我们人为的修改SP的值,即可掌控PC的跳转的方位了,因此我们的任务切换函数就可设计为如下样子:
void schedule(unsigned int task_number) { CLI; // 关中断 // RELOAD_ENTRY; // 重装函数入口地址 // RESAVE_SP; // 重装栈顶地址 task_sp[exec_num] = SP; // 保存上个函数入口地址 exec_num = task_number; // 运行任务号切换 SP = task_sp[exec_num]; // 得到对应函数入口地址 STI; // 开中断 }
上述的任务切换函数还算是合理的最起码将该段设置为了临界区,避免了切换时中断的干扰。而我所发的工程代码下,则不是这样设计,而是去掉了临界保护,加入RELOAD_ENTRY和SAVE_SP,如下所示:
void schedule(unsigned int task_number) { // CLI; // 关中断 RELOAD_ENTRY; // 重装函数入口地址 RESAVE_SP; // 重装栈顶地址 task_sp[exec_num] = SP; // 保存上个函数入口地址 exec_num = task_number; // 运行任务号切换 SP = task_sp[exec_num]; // 得到对应函数入口地址 // STI; // 开中断 }
取消临界保护为加入两个数据重载后整个切换函数执行时间加长,加入临界保护影响了中断的响应性;而加入两个数据重载的原因为,整个工程中使用了两个中断(timer0和uart),中断服务程序使用寄存器空间与全局变量存储空间发生冲突,出现全局变量数据的改写,所以每次切换时则进行数据的重装。(注:为解决上述空间冲突,1. 使用过指定中断程序工作寄存器组,结果无法解决;2. 使用人工指定全局变量存取地址,即_at_关键字的使用,结果未能解决;3. 使用xdata声明全局变量,结果未能成功,取值过程使用的是dptr指针,值传给SP时不正确,不知是什么原因。)
还有则是在工程中init_task_stack()函数,即使两个数据重装操作的包装。那为何不在schedule()函数中调用它,而是采用直接在schedule()函数中使用两个数据重装的宏,原因为在函数的调用和返回中都会涉及到PC和SP的修改,从而会用程序运行之不可预知的内存地址处。
整个系统的时钟片为5ms,实现数码管动态显示,与一些参量的改变。实现代码如下:
void timer0_interrupt(void) interrupt 1 { TH0 = 0xEE; TL0 = 0x00; my_dat.flag = !my_dat.flag; if (!my_dat.flag) { P2 = 0xfe; P0 = my_dat.recv_data[0]; } else { P2 = 0xfd; P0 = my_dat.recv_data[1]; } g_count.dly_cnt--; g_count.swap_time--; if (BEE_LED_ON == g_count.led_bee_flag) { g_count.dly_led--; g_count.dly_bee--; } }
函数中涉及到的g_count.swap_time、g_count.dly_led与g_count.dly_bee,实际上可以设计到对应任务的TCB中作为私有成员进行处理,这样可减少程序的内聚性。尤其是swap_time这个变量,它充当了正统操作系统中的任务享有的运行时间片的角色。在工程中我未设计TCB这个数据结构。
系统使用的第二个中断为串口中断,功能为实现接收上位机发送的两位数据,并判断和改变想要改变的标志位。中断服务程序如下:
void uart_interrupt(void) interrupt 4 { if (RI) { RI = 0; my_dat.recv_data;[my_dat.recv_cnt] = SBUF; my_dat.recv_cnt++; if (my_dat.recv_cnt > 1) { my_dat.recv_cnt = 0; } if ((my_dat.recv_data[0] == 0xC0) && (my_dat.recv_data[1] == 0xC0)) { g_count.led_bee_flag = BEE_LED_ON; } else { g_count.led_bee_flag = BEE_LED_OFF; } } else { while (!TI); TI = 0; } }
(注:中断服务程序中最好不要自己指定使用的寄存器组,因为这样更加容易造成数据的破坏。)
附上网上找到的关于全局变量存储空间与中断服务程序使用空间冲突的解决方案(都尝试过,但均未见效果。):
我查了查关于全局变量的使用,看到有个帖子说到全局变量会跟中断用的寄存器组发生冲突,也就是全局变量的地址会被KEIL分配到中断用的寄存器组里。
下面是我从网上搜集到的关于全局变量使用的注意点:
1. 全局变量要少用,能不用就不用;
2. 在主程序外面只对全局变量做声明,不做定义;
3. 使用中断时,要加上使用的寄存器组;
4. 裸露的全局变量全部用结构体封装起来;
5. 中断与主程序共享全局变量,用函数(含临界段)封装起来;
6. 使用全局变量出错时,可以给它指定一个地址(注意:不要和当前使用的寄存器组发生冲突);
7. 将大部分全局/静态变量(特别是数组)定义到xdata段中;
8. 有些变量可能会随时改变,例如:在中断中赋值的变量,以及硬件修改的输入/输出寄存器等,在程序中使用这些变量时,最好加上“volatile”关键词,告诉C51编译器:
(1)不要优化该变量,例如相连的两个相同的赋值语句,第二个不要优化掉,因其处于不同“时刻”赋值结果可能不一样。
(2)每次取该变量值时要从其实际地址的寄存器取,不要从内存的副本中取,因其值可能随时比改变了。