1.从任务调度说起
最开始我们在单片机写代码的样子是怎样的呢?在ch1那一章我们对模块和分层进行了讨论,模块是对功能代码的封装,分层是在平台层面封装,都是在解决项目复杂度控制的问题,但是我们拿单片机最主要的目的是来执行任务Task帮我们做事的,比如读取ADC采样数据,读取键盘按键,输出PWM,I2C通讯,运行PID控制,等等。
那在单片机里如何组织任务调度的设计?
大循环调度
最初的最初,我们的任务调度简单直接——也就是大循环方式,示例代码如下:
int main() { Dis_Interrupt(); System_Init(); En_Interrupt(); while(1) { Task0_Run(); Task1_Run(); Task2_Run(); Task3_Run(); Task4_Run(); } } void Task0_Run(void) { Pot1Calc(); //加速器信号计算 Pot2Calc(); //制动器信号计算(保留) TempCalc(); //电机及控制器温度计算 } .....
大循环方式的任务调度如图1所示,优点就是简单直接,适合比较简单的系统,带来的不好的地方:
每个任务的调度周期和时间是不固定的(if else的存在),无法保证确定的周期性执行任务
随着任务数量的增加,系统会越来越慢
如果遇上长时间任务,会拖累整个系统变慢
定时任务调度
为了克服大循环方式的缺点(任务调度周期性无法保证,任务数量增加系统会变慢),提出了定时的任务调度的方式,不过需要使用单片机一个定时器,来实现一个简单的任务调度器,利用定时器将CPU切割为一个等周期的时间片调度单元,然后利用标志位控制在每个时间片只调用一个任务。整个系统代码结构如下所示:
#define TASK_MAX_LENGTH 10 typedef struct { Int16 Flag[TASK_MAX_LENGTH]; Int16 Timer; Int32 Number; } USERTASK; USERTASK UserTask0={0,0,0,0,0,0,0,0,0,0,0,TASK_MAX_LENGTH};//任务初始化 //任务调度函数 void TaskScheduler(USERTASK* v) { v->Flag[v->Timer++] = 1; if(v->Timer >= v->Number) { v->Timer = 0; } } //主函数 int main() { Dis_Interrupt(); System_Init(); En_Interrupt(); while(1) { Task0_Run(); Task1_Run(); Task2_Run(); Task3_Run(); Task4_Run(); } } //1ms定时中断 __interrupt void Timer0_INT_MapedISR(void) { TaskScheduler(&UserTask0); } //单个任务示例函数 void Task0_Run(void) { if(UserTask0.Flag[0]) { Pot1Calc(); //加速器信号计算 Pot2Calc(); //制动器信号计算(保留) TempCalc(); //电机及控制器温度计算 UserTask0.Flag[0] = 0; } } ......
定时任务调度的流程图如图2所示。与大循环调度方式对比,这种方式能够实现周期性的任务调度,同时随着任务的增加,依然能够保证调度的周期性,这种调度能够应对大多数的控制系统,比如TI的PMSM电机控制器,一般小的家电控制器,都可以搞定。但是使用时有几点要注意:
单个任务的最长时间长度务必保证不超过单个时间片,否则会导致周期性延迟
对于严格实时的控制周期任务,定时调度器不能够保证
对于长周期任务(比如通讯等待等),定时任务调度器要么把任务切割为小任务,要么安排几个连续的空闲周期来执行
针对第1点,需要测试或者预估任务的最长执行时间,这个可以采用IO测试的方式解决(具体参见ch6)。
针对第2点,对于实时性要求高,并且周期控制快的任务(比如PID控制),只能将这个任务放到定时中断里做,示例代码如下:
//1ms定时中断 __interrupt void Timer0_INT_MapedISR(void) { TaskScheduler(&UserTask0); Task_SpeedPID_Control(); } //实时性要求高的任务,示例函数,如果控周期慢的话,也可以选择加入if(UserTask0.Flag[0])做判断 void Task_SpeedPID_Control(void) { SpeedPID_Input(); //读取输入指令和反馈信号 SpeedPID_Run(); //运行PID SpeedPID_Output(); //输出PWM控制 } ......
针对第3点,我们可以将长周期任务放在最后面,如图3所示,可以把最后几个空闲周期都留给Task4执行。但是要注意,如果有多个长周期任务,依然会拖慢整个调度周期,于是就出现了基于优先级的任务调度方式,高优先级的任务可以中断低优先级的任务,在保证长周期任务调度的同时,短周期任务的调度依然能够保证,这就是RTOS。
实时操作系统RTOS调度
实时操作系统,常用的小型RTOS有uCosII,FreeRTOS,Rt-thread,主要是任务优先级的调度方式不一样,这里感兴趣的同学,可以参见相关的专业书籍,对RTOS内核代码不做详细介绍。RTOS的对任务的调度方式如图4所示。Task0的优先级高,可以中断优先级低的Task1,等Task0执行完,然后RTOS会切换到Task1继续执行。
2.智能车总体任务调度
智能车调度平台总体上只有两个任务SpeedControlTask和ControlGraphTask,考虑到系统简单,没有用RTOS和任务调度器,直接中断配合While实现,代码示例如下,运行时序如图5所示。
//main主循环 void main(void) { DisableInterrupts; CarSystem_Init(); EnableInterrupts; Car_Test();//主循环在这里 while(1); } //主循环 void Car_Test(void) { while(1) { if(ImageOver)//图像DMA传输结束 { ImageOver=0; img_extract((uint8 *)Image_Data, (uint8 *)imgbuff0, CAMERA_SIZE);//解压图像 ControlGraphTask();//图像处理任务 DataLog_Add();//数据记录 if(DataLog_CheckEN()) DataLog_Print(); } } } //中断 #define CAM_VSYNC 29 void PORTA_handler(void) { uint32 flag = PORTA_ISFR; PORTA_ISFR = ~0; if(flag & (1 << CAM_VSYNC)) //PTA29触发摄像头帧中断 { ImageOver=0; //清除图像采集标志 camera_vsync(); gVar.time++; } } //DMA传输图像数据 void DMA0_IRQHandler() { camera_dma(); ImageOver=1; } //定时10ms中断,执行速度PID控制任务 void PIT0_IRQHandler(void) { SpeedControlTask(); PIT_Flag_Clear(PIT0); }
总体思路就是,每一幅图像的帧中断VSYNC触发PORTA_handler(PA29)中断函数,此时ImageOver清零,同时DMA开始传输图像,当DMA传输结束触发DMA0_IRQHandler中断,此时ImageOver=1,如果ControlGraphTask检测到的话,那就开始执行,如果ControlGraphTask在VSYNC到来清零ImageOver之前没有开始执行的话,那只能等待下一次DMA中断。最终测试结果,每两帧触发一次ControlGraphTask执行,控制周期为13.33ms。
3.嵌入式驱动层设计
嵌入式驱动层大部分复用了Vcan山外的板级库,新加入比较重要的库有EITMotorL,EITMotor_R,EIT_Steer和EIT_Log,封装在EITLib文件夹,总体的思路就是,.h负责接口,.c负责功能实现。
这里以Motor库为例,介绍一下嵌入式驱动库的封装。
考虑到速度控制用到Motor和Encode,所以把二者集成放到了一起,整体MotorR代码解析如下所示。
#ifndef __EIT_MOTORR_DEF__ #define __EIT_MOTORR_DEF__ #include "include.h" /*Motor Driver*/ #define MOTORR_PWM_MAX 1000 //PWM范围:-1000到1000 #define MOTORR_PWM_MIN (-1000) #define MOTORR_PWM_FREQ 15000 //PWM工作频率 #define MOTORR_FTM FTM0 #define MOTORR_EN PTA24 #define MOTORR_PWMA FTM_CH3 #define MOTORR_PWMB FTM_CH4 #define MOTORR_PWMAIO PTA6 #define MOTORR_PWMBIO PTA7 /*Encode */ #define MOTORR_ENCODE_FTM FTM2 //左编码器用FTM1 #define MOTORR_GEAR_N 36 //B车电机自带齿轮齿轮数 #define ENCODR_GEAR_N 40 //B车主动轴齿轮齿轮数 #define WHEELR_GEAR_N 105 //编码器比例系数 #define ENCODR_CYCLE 2000 //编码器一圈触发2000个脉冲 #define WHEELR_LENGTH 18 //17.8cm车轮周长 #define SPEEDR_FS 100 //速度采样频率Hz,周期10ms extern void MotorR_Init(void); //电机初始化 extern void MotorR_Run(int32 pwm); //电机PWM控制 extern void MotorR_Brake(void); //电机刹车 extern void MotorR_Slip(void); //电机滑行 extern int32 MotorR_GetWheelSpeed(int32 CntInTs); //speed单位为cm/s extern int32 MotorR_GetTsCount(void); //10ms周期内,编码器脉冲计数值 #endif