ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【凌鸥LKS32】应用篇(一)·Timer定时器模拟任务调度

【凌鸥LKS32】应用篇(一)·Timer定时器模拟任务调度 目录1. 任务处理方式1.1 裸机系统1.1.1 轮询系统1.1.2 前后台系统1.2 多任务系统2. 时间片轮转2.1 定时器2.2 外设初始化2.3 任务调度设计2.4 按键任务设计2.5 LED任务设计2.6 主函数1. 任务处理方式1.1 裸机系统在裸机系统中系统的主体就是 main 函数里面顺序执行的无限循环这个无限循环里面 CPU 按照顺序完成各种事情。对于裸机的任务1.1.1 轮询系统轮询系统即是在裸机编程的时候先初始化好相关的硬件然后让主程序在一个死循环里面不断循环顺序地做各种事情int main(void) { //硬件初始化 Init(); //..... while(1) { test1();//任务1 test2();//任务2 test3();//任务3 } }轮询系统是一种非常简单的软件结构通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情。对应上述代码如果只是实现 LED 翻转串口输出液晶显示等这些操作那么使用轮询系统将会非常完美。但是如果加入了按键操作等需要检测外部信号的事件用来模拟紧急报警那么整个系统的实时响应能力就不会那么好了。假设test3 是按键扫描当外部按键被按下相当于一个警报这个时候需要立马响应并做紧急处理而这个时候程序刚好执行到 test1要命的是 test1需要执行的时间比较久久到按键释放之后都没有执行完毕那么当执行到 test3的时候就会丢失掉一次事件。足见轮询系统只适合顺序执行的功能代码当有外部事件驱动时实时性就会降低。1.1.2 前后台系统相比轮询系统前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成事件的处理还是回到轮询系统中完成中断在这里我们称为前台main 函数里面的无限循环我们称为后台int flag1 0; int flag2 0; int flag3 0; int main(void) { /* 硬件相关初始化 */ HardWareInit(); /* 无限循环 */ for (;;) { if (flag1) { /* 处理事情 1 */ DoSomething1(); } if (flag2) { /* 处理事情 2 */ DoSomething2(); } if (flag3) { /* 处理事情 3 */ DoSomething3(); } } } void ISR1(void) { /* 置位标志位 */ flag1 1; /* 如果事件处理时间很短则在中断里面处理 如果事件处理时间比较长在回到前台处理 */ DoSomething1(); } void ISR2(void) { /* 置位标志位 */ flag2 1; /* 如果事件处理时间很短则在中断里面处理 如果事件处理时间比较长在回到前台处理 */ DoSomething2(); } void ISR3(void) { /* 置位标志位 */ flag3 1; /* 如果事件处理时间很短则在中断里面处理 如果事件处理时间比较长在回到前台处理 */ DoSomething3(); }在顺序执行后台程序的时候如果有中断来临那么中断会打断后台程序的正常执行流转而去执行中断服务程序在中断服务程序里面标记事件如果事件要处理的事情很简短则可在中断服务程序里面处理如果事件要处理的事情比较多则返回到后台程序里面处理。虽然事件的响应和处理是分开了但是事件的处理还是在后台里面顺序执行的但相比轮询系统前后台系统确保了事件不会丢失再加上中断具有可嵌套的功能这可以大大的提高程序的实时响应能力。在大多数的中小型项目中前后台系统运用的好堪称有操作系统的效果。1.2 多任务系统在多任务系统中我们根据功能的不同把整个系统分割成一个个独立的且无法返回的函数这个函数我们称为任务。在多任务系统中两个任务不断切换的即两个变量的波形是完全一样的就好像 CPU 在同时干两件事一样相比前后台系统多任务系统的事件响应也是在中断中完成的但是事件的处理是在任务中完成的。在多任务系统中任务跟中断一样也具有优先级优先级高的任务会被优先执行。当一个紧急的事件在中断被标记之后如果事件对应的任务的优先级足够高就会立马得到响应。相比前后台系统多任务系统的实时性又被提高了int flag1 0; int flag2 0; int flag3 0; int main(void) { /* 硬件相关初始化 */ HardWareInit(); /* OS 初始化 */ RTOSInit(); /* OS 启动开始多任务调度不再返回 */ RTOSStart(); } void ISR1(void) { /* 置位标志位 */ flag1 1; } void ISR2(void) { /* 置位标志位 */ flag2 2; } void ISR3(void) { /* 置位标志位 */ flag3 1; } void DoSomething1(void) { /* 无限循环不能返回 */ for (;;) { /* 任务实体 */ if (flag1) { } } } void DoSomething2(void) { /* 无限循环不能返回 */ for (;;) { /* 任务实体 */ if (flag2) { } } } void DoSomething3(void) { /* 无限循环不能返回 */ for (;;) { /* 任务实体 */ if (flag3) { } } }每个任务都是独立的互不干扰的且具备自身的优先级它由操作系统调度管理。加入操作系统后我们在编程的时候不需要精心地去设计程序的执行流不用担心每个功能模块之间是否存在干扰。加入了操作系统我们的编程反而变得简单了。整个系统随之带来的额外开销就是操作系统占据的那一丁点的 FLASH 和 RAM。现如今单片机的 FLASH 和 RAM 是越来越大完全足以抵挡 RTOS 那点开销。无论是裸机系统中的轮询系统、前后台系统和多任务系统我们不能一锤子的敲定孰优孰劣它们是不同时代的产物在各自的领域都还有相当大的应用价值只有合适才是最好。有关这三者的软件模型区别模型事件响应事件处理特点轮询系统主程序主程序轮询响应事件轮询处理事件前后台系统中断主程序实时响应事件轮询处理事件多任务系统中断任务实时响应事件实时处理事件2. 时间片轮转这里我们通过定时器用来模拟多任务系统的任务调度机制创建一个结构体数组描述任务{就绪标志, 倒计时, 周期, 函数指针}其实际上仍是裸机的操作方式没有上下文切换和优先级抢占机制通过给每个任务分配时间时间完成后切换下一个任务以此来实现任务调度。2.1 定时器首先先来完成整个任务调度的核心设置一个1ms的定时器仿照RTOS的节拍周期统一由 1ms tick 驱动各任务节拍一致void UTimer_Task_Init(void) { TIM_TimerInitTypeDef TIM_InitStruct; TIM_TimerStrutInit(TIM_InitStruct); TIM_InitStruct.Timer_CH0_WorkMode TIMER_OPMode_CMP; // 比较模式 TIM_InitStruct.Timer_CH0Output 0; // 不输出波形 TIM_InitStruct.Timer_CH1_WorkMode TIMER_OPMode_CMP; TIM_InitStruct.Timer_CH1Output 0; TIM_InitStruct.Timer_TH 48000; // 周期重载值 TIM_InitStruct.Timer_CMP0 24000; // 比较值0未用输出可忽略 TIM_InitStruct.Timer_CMP1 24000; // 比较值1 TIM_InitStruct.Timer_FLT 0; TIM_InitStruct.Timer_ClockDiv TIMER_CLK_DIV2; // 时钟2分频 TIM_InitStruct.Timer_IRQEna Timer_IRQEna_ZC; // 使能中断 TIM_TimerInit(UTIMER3, TIM_InitStruct); TIM_TimerCmd(UTIMER3, ENABLE); // 启动定时器 NVIC_SetPriority(TIMER3_IRQn, 1); // 优先级设为 1较高 NVIC_EnableIRQ(TIMER3_IRQn); // 使能 UTIMER3 中断 }中断服务函数进行持续累加extern volatile u16 nTimeCnt1ms;// 全局毫秒计数器 extern uint8_t task_flag; // 任务调度使能开关 void TIMER3_IRQHandler(void) { UTIMER3_IF | TIMER_ZC_IF; // 写1清0清除中断标志 nTimeCnt1ms; // 全局毫秒计数器1 if (task_flag 1) { // 任务调度使能开关 TaskRemarks(); // 执行所有任务的倒计时 } }2.2 外设初始化初始化LED和按键下面会对这两个外设设计任务功能void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; GPIO_StructInit(GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStruct.GPIO_Pin LED1_PIN; GPIO_Init(LED1_PORT, GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStruct.GPIO_Pin LED2_PIN; GPIO_Init(LED2_PORT, GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStruct.GPIO_Pin LED3_PIN; GPIO_Init(LED3_PORT, GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStruct.GPIO_Pin LED4_PIN; GPIO_Init(LED4_PORT, GPIO_InitStruct); LED_All_OFF();//初始化阶段led灯全关闭 } void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; GPIO_StructInit(GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN; GPIO_InitStruct.GPIO_Pin KEY_POWER_PIN; GPIO_Init(KEY_POWER_PORT, GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN; GPIO_InitStruct.GPIO_Pin KEY_TEMP_PIN; GPIO_Init(KEY_TEMP_PORT, GPIO_InitStruct); }宏定义部分根据自己引脚修改#define KEY_POWER_PORT GPIO2 #define KEY_POWER_PIN GPIO_Pin_4 #define KEY_POWER_SOURCE GPIO_PinSource_4 #define KEY_POWER_SCAN() GPIO_ReadInputDataBit(KEY_POWER_PORT, KEY_POWER_PIN) #define KEY_TEMP_PORT GPIO0 #define KEY_TEMP_PIN GPIO_Pin_11 #define KEY_TEMP_SOURCE GPIO_PinSource_11 #define KEY_TEMP_SCAN() GPIO_ReadInputDataBit(KEY_TEMP_PORT, KEY_TEMP_PIN) #define LED1_PIN GPIO_Pin_8 #define LED1_PORT GPIO1 #define LED1_ON GPIO_ResetBits(LED1_PORT, LED1_PIN) #define LED1_OFF GPIO_SetBits(LED1_PORT, LED1_PIN) #define LED2_PIN GPIO_Pin_7 #define LED2_PORT GPIO1 #define LED2_ON GPIO_ResetBits(LED2_PORT, LED2_PIN) #define LED2_OFF GPIO_SetBits(LED2_PORT, LED2_PIN) #define LED3_PIN GPIO_Pin_6 #define LED3_PORT GPIO1 #define LED3_ON GPIO_ResetBits(LED3_PORT, LED3_PIN) #define LED3_OFF GPIO_SetBits(LED3_PORT, LED3_PIN) #define LED4_PIN GPIO_Pin_5 #define LED4_PORT GPIO1 #define LED4_ON GPIO_ResetBits(LED4_PORT, LED4_PIN) #define LED4_OFF GPIO_SetBits(LED4_PORT, LED4_PIN) #define LED1_Toggle() do { if (GPIO_ReadOutputDataBit(LED1_PORT, LED1_PIN)) \ LED1_ON; else LED1_OFF; } while(0) #define LED2_Toggle() do { if (GPIO_ReadOutputDataBit(LED2_PORT, LED2_PIN)) \ LED2_ON; else LED2_OFF; } while(0) #define LED3_Toggle() do { if (GPIO_ReadOutputDataBit(LED3_PORT, LED3_PIN)) \ LED3_ON; else LED3_OFF; } while(0) #define LED4_Toggle() do { if (GPIO_ReadOutputDataBit(LED4_PORT, LED4_PIN)) \ LED4_ON; else LED4_OFF; } while(0)2.3 任务调度设计这里我创建两个任务采用枚举类型方便后续任务扩展typedef enum _TASK_LIST { TASK_KEY 0, TASK_LED, //后续想要添加别的任务直接在这里加即可 TASKS_MAX } TASK_LIST;然后分别对任务赋值相关参数默认都不运行任务周期根据宏定义自行选择这里我就10ms的任务#define TASK_PERIOD_1MS 1 #define TASK_PERIOD_2MS 2 #define TASK_PERIOD_5MS 5 #define TASK_PERIOD_10MS 10 #define TASK_PERIOD_20MS 20 #define TASK_PERIOD_30MS 30 #define TASK_PERIOD_40MS 40 #define TASK_PERIOD_50MS 50 #define TASK_PERIOD_60MS 60 #define TASK_PERIOD_100MS 100 #define TASK_PERIOD_360MS 360 #define TASK_PERIOD_500MS 500 #define TASK_PERIOD_1000MS 1000 typedef struct _TASK_COMPONENTS { uint8_t Run; // 运行标志 uint16_t Timer; // 当前倒计时 uint16_t ItvTime; // 任务周期 void (*TaskHook)(void); // 函数指针指向任务函数 } TASK_COMPONENTS; static TASK_COMPONENTS TaskComps[] { {false, TASK_PERIOD_10MS, TASK_PERIOD_10MS, TaskKey}, // 按键 10ms {false, TASK_PERIOD_10MS, TASK_PERIOD_10MS, TaskLED}, // LED 10ms };其中两个任务索引可以先创建空的下面在做补充void TaskProcess(void) { } void TaskLED(void) { }任务框架搭载完了我们这里开始实现任务调度的核心函数首先初始化任务调度器根据枚举循环调用写入任务参数uint8_t TaskInit(void) { uint8_t i; for (i 0; i TASKS_MAX; i) { TaskComps[i].Timer TaskComps[i].ItvTime; // 每个任务的定时器装载其间隔值 TaskComps[i].Run false; // 清除运行标志暂不执行 } return 1; }然后更新任务定时器当周期到时置位运行标志若标志位置位在定时器每1ms循环调用每次调用任务时间减1若是减到0则重装载定时器置运行标志位void TaskRemarks(void) { uint8_t i; for (i 0; i TASKS_MAX; i) { if (TaskComps[i].Timer) // 若定时器未到0 { TaskComps[i].Timer--; // 倒计时减1 if (TaskComps[i].Timer 0) // 减到0说明一个周期到了 { TaskComps[i].Timer TaskComps[i].ItvTime; // 重装定时器 TaskComps[i].Run true; // 置位运行标志等待执行 } } } }最后主函数循环调用如下函数执行所有待运行的任务void TaskProcess(void) { uint8_t i; for (i 0; i TASKS_MAX; i) { if (TaskComps[i].Run) // 若该任务被标记为可运行 { TaskComps[i].TaskHook(); // 调用任务函数指针实际执行任务 TaskComps[i].Run false; // 执行完毕清除运行标志 } } }2.4 按键任务设计首先初始化按键枚举方便管理typedef enum { KEY_TEMP, KEY_POWER, KEY_COMB, KEY_TYPE_MAX } keyType_e; extern keyType_e g_keyEvent; // 按键事件然后创建按键检测函数keyType_e g_keyEvent KEY_TYPE_MAX; // 初始无事件 //按键检测函数判断哪个那件被按下 keyType_e Detect_Key(void) { if ((KEY_POWER_SCAN() Bit_RESET) (KEY_TEMP_SCAN() Bit_RESET)) //双按键按下 { return KEY_COMB; } if (KEY_POWER_SCAN() Bit_RESET) //电源键 { return KEY_POWER; } if (KEY_TEMP_SCAN() Bit_RESET) //模式选择键 { return KEY_TEMP; } return KEY_TYPE_MAX; //无按键边界值 }完善按键任务函数循环扫描按键任务void TaskKey(void) { static keyType_e key_last KEY_TYPE_MAX; // 上一次扫描值 static uint8_t stable_cnt 0; // 连续相同计数 keyType_e key_now Detect_Key(); // 读取当前按键 if (key_now key_last) { if (stable_cnt 255) stable_cnt; } else { stable_cnt 0; key_last key_now; } if (stable_cnt 2 key_now ! KEY_TYPE_MAX g_keyEvent KEY_TYPE_MAX) { g_keyEvent key_now; // 投递事件等待LED任务取走 } }2.5 LED任务设计做一个简单封装#include led_app.h #include delay.h void LED_All_ON(void) { LED1_ON; LED2_ON; LED3_ON; LED4_ON; } void LED_All_OFF(void) { LED1_OFF; LED2_OFF; LED3_OFF; LED4_OFF; } void LED_All_Toggle(void) { LED1_Toggle(); LED2_Toggle(); LED3_Toggle(); LED4_Toggle(); }LED通过读取按键键值的变换实现LED灯的变换操作void TaskLED(void) { if (g_keyEvent KEY_TYPE_MAX) return; // 无事件直接退出 switch (g_keyEvent) { case KEY_POWER: LED1_Toggle(); break; case KEY_TEMP: LED2_Toggle(); break; case KEY_COMB: LED_All_Toggle(); break; default: break; } g_keyEvent KEY_TYPE_MAX; // 事件已处理清零允许下一次投递 }2.6 主函数主函数非常简单就调用任务函数即可#include lks32mc07x_lib.h #include hardware_init.h #include task.h #include led_app.h #include key_app.h uint8_t task_flag 0;// 任务调度使能开关 int main(void) { Hardware_init(); TaskInit(); // 任务调度器初始化装载各任务周期 task_flag 1; // 使能任务调度1ms中断里才会调用TaskRemarks while(1) { TaskProcess(); } }最终实现的功能按键单独按下实现一颗LED电平反转两个按键同时按下实现全部LED电平反转由于无法上传视频实验现象可以自行测试。完整工程凌鸥LKS32MC071系列单片机·Timer仿造RTOS实现任务调度实验资源-CSDN下载凌鸥LKS32_时光の尘的博客-CSDN博客
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进