裸机条件下写一个基于时间片轮转的多任务并发程序
目录
- 前言
- A. 使用RTOS
- B.裸机多任务并发
前言
在学习各种MCU的时候,都是用在main函数里写一个while(1){/* 执行代码 */},这种方式只能一个函数运行完以后再运行另一个函数。
假设需求控制多个模块,如显示屏幕信息的同时控制电机,还要一边接收按键输入。如果用上面的方式每个模块要排队等待CPU运行,就会显的很卡。
那有没有办法每个模块运行固定的时间,时间到了运行下一个模块,这样单个模块即使特别耗时,也不影响其他模块的运行,这个方法叫时间片轮转。
想到这个办法很容易,但要怎么编写代码呢?
A. 使用RTOS
根据不要重复造轮子的理论,能用现有的开源代码当然是最好的了。如类STM32常用的操作系统有uCOS, RT-thread(国产),FreeRTOS。
用现成的去官网等地方搜移植教程,本文不再详述。
用这些开源代码的问题是如果MCU RAM或ROM太小,删减起来就不太方便了,这时候手搓一个多任务并发系统的作用来了。
B.裸机多任务并发
时间片轮转的基本思路就是通过定时器(最好是硬件定时器)将CPU的运行时间切片成一个个时间片,代码里叫tick,然后一个任务每运行一个时间片,tick计数加1,当前任务运行的tick数已经达到分配给任务的tick数,就不再执行当前任务执行下一个任务。
先定义一个最任务结构体,至少包括任务主体函数,分配给任务的时间片tick数和当前运行已经消耗的时间片tick计数。用结构体就是为了方便扩展用的,还可以增加参数比如任务使能,任务偏移量等。这样就使任务执行更加灵活。
typedef void (*Func)(void);
typedef struct task_info_t
{
Func func; /* 任务主体函数 */
uint16_t task_tick; /* 分配给任务的时间片tick数 */
uint16_t tast_tick_cnt; /* 当前运行已经消耗的时间片tick计数 */
} TaskInfo_t;
初学者 typedef void (*Func)(void); 看不懂,这个是定义一种函数类型,这种函数类型是void (*)(void)型的,给他取个别名叫Func. 相当于下面这种写法:
不清楚的看这篇文章typedef void *(Func)(void)用法
typedef struct task_info_t
{
void (*func)(void); /* 任务主体函数 */
uint16_t task_tick; /* 分配给任务的时间片tick数 */
uint16_t tast_tick_cnt; /* 当前运行已经消耗的时间片tick计数 */
} TaskInfo_t;
定义好结构体后创建一个结构体数组,在数组里初始化任务的参数,结构体数组不清楚的看这里结构体数组
TaskInfo_t TaskInfoArray[] = {
{IdleTask, 10, 0},
{LedTask, 20, 0},
{KeyTask, 5, 0},
{MotorTask, 50, 0},
/*任务名 执行时间 运行计数*/
}
然后要开启一个定时器中断(我是STM32的用这种方法,其他单片机可以用其他方法),比如将时间片定为1ms. 用STM32CubeMX直接配置一个1ms中断的定时器,也可以去问ChatGPT。我这里是开了一个定时器TIM3.
/**
* @brief TIM3 Initialization Function
* @param None
* @retval None
*/
static void MX_TIM3_Init(void)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 850;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 65535;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_OC_Init(&htim3) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_TOGGLE;
sConfigOC.Pulse = 0;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_OC_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_3) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_MspPostInit(&htim3);
}
然后设置一个任务运行1ms的标志位,定义成全局变量 task1ms_Flag,定时器里把这个标志位至1.
/**
* @brief This function handles TIM3 global interrupt.
*/
uint8_t task1ms_Flag = 0;
void TIM3_IRQHandler(void)
{
task1ms_Flag = 1;
HAL_TIM_IRQHandler(&htim3);
}
然后在while(1){…}里面写整个任务切换的程序,看懂逻辑不复杂
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/*初始化代码*/
uint8_t i;
TaskInfo_t *p_task;
while(1)
{
if (0 == task1ms_Flag)
return; //当前运行结束,1ms没到,不需要切换任务
else
task1ms_Flag = 0; //当task1ms_Flag为1时运行,先重置为0
for (i = 0; i < sizeof(TaskInfoArray) / sizeof(TaskInfo_t); i++) //sizeof(TaskInfoArray) / sizeof(TaskInfo_t)为任务数,每1ms时间片轮转一遍
{
p_task = &TaskInfoArray[i]; //从任务0开始任务轮转
if( p_task->task_tick_cnt >= tsk_ptr->tsk_tick ) //当前任务已经消耗tick大于分配的tick
{
tsk_ptr->func(); //执行当前任务i的函数主体
tsk_ptr->tst_tick_cnt = tsk_ptr->tst_tick_cnt % tsk_ptr->tsk_tick; //当前任务已经消耗tick减去分配的tick,相当于清零。
}
else //如果当前任务已经消耗tick小于分配的tick,则当前任务已经消耗tick加一
{
tsk_ptr->tst_tick_cnt++;
}
}
}
}
完成这些配置后就可以写每个任务的执行函数了,和RT-thread不一样,这些任务函数里不要写while(1)
void IdleTask(void)
{
}
void LedTask(void)
{
}
void KeyTask(void)
{
}
void MotorTask(void)
{
}
整个系统的逻辑就是时间片轮转执行Task程序,如IdleTask执行10ms,LedTask执行20ms,KeyTask执行5ms,MotorTask执行50ms。
注意每个任务里如果要写延时函数注意延时的总时间不要大于分配的时间片。