对于单片机单片机程序来说,大家都不陌生,但是真正使用架构开发,考虑架构的恐怕不多,平时写代码都是想到什么写什么,导致程序代码非常复杂并且不易读懂,下面介绍几种常用的架构开发方案。
一、前后台顺序执行法。
这使初学者常用代码的程序设计框架,不用考虑太多东西,代码简单,或者对系统的整体实时性和并发性要求不高:初始化后通过while(1){}或者for()等循环不断的调用自己编写完成的函数,也基本不考虑每个函数执行所需要的时间,打部分时间在函数中存在毫秒级别延迟。
优点:对初学者来说,这是最容易也是最直观的程序架构,逻辑简单明了,适用于单模块调试或者复杂度较低的软件开发。
缺点:实时性低,由于每个函数或多或少存在毫秒延迟,导致其他函数间隔执行时间不同,虽然可以通过定时器中断方式,但是中断的执行函数必须短。当程序逻辑复杂度提升时,会导致后来维护的人员无法理解,难以分析。
二、时间片轮训法
介于前后台顺序执行和操作系统(RTOS)之间的一种程序设计方案。改设计方案帮助换在学习裸机和操作系统之间的开发者,更好的提高开发效率,主要特点有以下几个:
1:目前的需求设计完全没有必要上操作系统
2:任务的函数无需时刻都执行,存在间隔(比如按键软件防抖,初学者会用延迟去检测按键状态,这部分时间对于CPU来说使比较浪费的,这部分时间完全可以去执行其他任务)
3:对实时性有一定的要求。可以根据任务的重要程度,来规定任务的执行周期。
改设计方案需要使用一个定时器,一般情况下定时1ms就可以了,定时太长影响实时性(频繁的进入中断,效率不高),因此需要考虑每个任务的执行时间。同时要求主函数和任务函数中不能存在毫秒级别的延迟。
下面介绍两种比较常见的任务调度方案
一、使用标志位进行任务切换的设计方案
/*
* \brief 主函数
*
* \param[in] 无
*
* \retval 无
*
*/
int main()
{
System_Init();
whlie(1)
{
if(FLAG0)
{
App_Task();
FLAG0=0;
}
if(FLAG1)
{
App_Task1();
FLAG1=0;
}
if(FLAG2)
{
App_Task2();
FLAG2=0;
}
if(FLAG3)
{
App_Task3();
FLAG3=0;
}
if(FLAG4)
{
App_Task4();
FLAG4=0;
}
}
}
/*
* \brief 定时器中断
*
* \param[in] 无
*
* \retval 无
*
*/
void TIM2_IRQHandler(void)
{
if(TIM_GetITStasus(TIM2,TIM_IT_UPdate) == SET)
{
timCount++;
//这里用条件编译,方便代码书写。
timCount % 1 ==0 ? FLAG0 = 1 : 0;
timCount % 10 ==0 ? FLAG1 = 1 : 0;
timCount % 20 ==0 ? FLAG2 = 1 : 0;
timCount % 100 ==0 ? FLAG3 = 1 : 0;
timCount % 500 ==0 ? FLAG4 = 1 : 0;
}
TIM_ClearITPendingBit(TIM2,TIM_IT_UPdate);
}
如果需要更加精确的检测,放置程序是否跑飞,可以使用看门狗来进行定时喂狗。
二、使用函数指针的设计方案
/*
* \brief 定义任务相关结构体
*/
typedef struct
{
uint8_t run; // 调度标志,1:调度,0:挂起
uint16_t timCount; // 时间片计数值
uint16_t timRload; // 时间片重载值
void (*pTaskFuncCb)(void); // 函数指针变量,用来保存业务功能模块函数地址
} TaskComps_t;
/*
* \brief 定义任务数量
*/
#define TASK_NUM_MAX (sizeof(g_taskComps) / sizeof(g_taskComps[0]))
static TaskComps_t g_taskComps[] =
{
{0, 1, 1, AppTask},
{0, 10, 10, AppTask1},
{0, 20, 20, AppTask2},
{0, 100,100, AppTask3},
{0, 500, 500, AppTask4},
/* 添加业务功能模块 */
};
/**
* @brief 任务调度函数
* @param
* @return
*/
static void TaskHandler(void)
{
for (uint8_t i = 0; i < TASK_NUM_MAX; i++)
{
if (g_taskComps[i].run) // 判断时间片标志
{
g_taskComps[i].run = 0; // 标志清零
g_taskComps[i].pTaskFuncCb(); // 执行调度业务功能模块
}
}
}
/**
* @brief 在定时器中断服务函数中被间接调用,设置时间片标记,需要定时器1ms产生1次中断
* @param
* @return
*/
static void TaskScheduleCb(void)
{
for (uint8_t i = 0; i < TASK_NUM_MAX; i++)
{
if (g_taskComps[i].timCount)
{
g_taskComps[i].timCount--;
if (g_taskComps[i].timCount == 0)
{
g_taskComps[i].run = 1;
g_taskComps[i].timCount = g_taskComps[i].timRload;
}
}
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStasus(TIM2,TIM_IT_UPdate) == SET)
{
g_pTaskScheduleFunc();
}
TIM_ClearITPendingBit(TIM2,TIM_IT_UPdate);
}
static void (*g_pTaskScheduleFunc)(void); // 函数指针变量,保存任务调度的函数地址
/**
* @brief 注册任务调度回调函数
* @param pFunc, 传入回调函数地址
* @return
*/
void TaskScheduleCbReg(void (*pFunc)(void))
{
g_pTaskScheduleFunc = pFunc;
}
int main ()
{
System_Init();
TaskScheduleCbReg(TaskScheduleCb);
while(1)
{
TaskHandler();
}
}
这里需要注意,在调用的时候需要使用函数指针来实现函数的回调,对函数指针不熟悉的人需要去了解下。
三、操作系统。
嵌入式操作系统,使一种用途广泛的系统软件,对单片机来说,常见的有UCOS、FreeRTOS、RT-Thread、等多种操作系统。操作系统中的时间片轮训法和上面介绍的任务调度有类似的地方,但这类操作系统中往往集成ICP等,复杂功能,初学者接触比较困难。在此不再介绍。