6.4 基础程序讲解(时基驱动的协作式调度)

时基驱动的协作式调度

我使用的是裸机操作,并没有使用RTOS操作系统帮助协同工作,但是为了提高效率,本次代码并不以“前后台代码风格”来书写代码,而是以时基代码作为本次课程的讲解。

在RTOS中我们知道存在一个机制,即每1ms可以切换不同的任务执行,让程序看起来并没有阻塞,而在裸机中,想要实现这样的操作很困难,我就退而求其次,制作一个任务结构体,这个结构体里面有:任务函数、任务规定运行时间、任务已运行的时间,通过结构体中的这三个元素,来让任务在规定时间内运行,虽无法达到类似RTOS那样1ms内的多次任务切换,但对于大部分裸机任务而言已经足够。

在本课程中,我使用的是裸机开发,没有引入任何 RTOS。为了更高的执行效率,也避免传统“前后台(Foreground/Background)”风格带来的复杂性,我采用了时基驱动的协作式调度方式来演示。

  • 我用一个 1 ms 一次的定时器中断递增全局时基 g_sys_tick。
  • 定义一个任务控制块 Task_t,包含
    1. func:任务入口函数,
    2. period:周期(ms),
    3. last_run:上次执行的时刻(tick 值)。
  • 在主循环里不断读取当前时基 now = g_sys_tick,遍历所有任务:
    if ((now − last_run) ≥ period) 就执行该任务,并更新 last_run = now。

这种方法是协作式(co-operative)、非抢占式(non-preemptive)调度,不依赖操作系统的上下文切换。虽然它达不到 RTOS 那样严格的每 1 ms 切换多任务,但对于绝大多数裸机场景下的定时/周期性任务,精度和效率都已绰绰有余。

裸机时基代码示例

typedef void (*TaskFunc)(void);

typedef struct {
    TaskFunc  func;      // 任务入口
    uint32_t  period;    // 周期,单位 ms
    uint32_t  last_run;  // 上次执行时刻(tick)
} Task_t;

#define TASK_NUM  2

extern volatile uint32_t g_sys_tick;  // 1ms 增加一次

void Task1(void);
void Task2(void);

Task_t tasks[TASK_NUM] = {
    { Task1, 1, 0 },
    { Task2, 1, 0 },
};

int main(void) {
    Timer1_Init_1ms();   // 启动 1 ms 定时中断
    while (1) {
        uint32_t now = g_sys_tick;
        for (int i = 0; i < TASK_NUM; i++) {
            if ((uint32_t)(now - tasks[i].last_run) >= tasks[i].period) {
                tasks[i].last_run = now;
                tasks[i].func();
            }
        }
    }
}

非阻塞式按键

因为我们并不是严格的按照ms来进行计数的,所以Count的具体值需要自己进行调试,但是最简单的方法是把Key_Tick放在时基定时器的中断中,而不是在任务中调用,这样Count可以更加精确的接近ms的定义。

非阻塞按键代码示例

void Key_Tick(void)
{
	static uint8_t Count;
	static uint8_t CurrState, PrevState;
	Count ++;
	
	/* 累计到一定次数(约 20 ms)才去做一次扫描,起到粗略防抖和减少 GPIO 读取频次的作用 */
	if (Count >= 20)
	{
		Count = 0;
		//滚动记录状态
		PrevState = CurrState; // 上一次扫描时的状态(查看是否为0,不为0,如果为1~4则代表有按键被按下)
		CurrState = Key_GetState();//本次扫描:哪个键被按下(0~4)
		
		/*检测 上一次按下(PrevState!=0)→ 本次全部松开(CurrState==0)的“抬起沿”
			这样可以保证一次完整的“按下→抬起”后才产生一次按键事件*/
		if (CurrState == 0 && PrevState != 0)
		{
			Key_Num = PrevState;
		}
	}
}

卧式电位器

旋钮(卧式电位器),我们甚至不需要新建任务,使用AD来测量电位,最左端是0,最右端是4095,写一个函数直接放在屏幕显示的任务中既可。

电机编码器

电机的编码器用来测量实际的位置和速度。