时基驱动的协作式调度
我使用的是裸机操作,并没有使用RTOS操作系统帮助协同工作,但是为了提高效率,本次代码并不以“前后台代码风格”来书写代码,而是以时基代码作为本次课程的讲解。
在RTOS中我们知道存在一个机制,即每1ms可以切换不同的任务执行,让程序看起来并没有阻塞,而在裸机中,想要实现这样的操作很困难,我就退而求其次,制作一个任务结构体,这个结构体里面有:任务函数、任务规定运行时间、任务已运行的时间,通过结构体中的这三个元素,来让任务在规定时间内运行,虽无法达到类似RTOS那样1ms内的多次任务切换,但对于大部分裸机任务而言已经足够。
在本课程中,我使用的是裸机开发,没有引入任何 RTOS。为了更高的执行效率,也避免传统“前后台(Foreground/Background)”风格带来的复杂性,我采用了时基驱动的协作式调度方式来演示。
- 我用一个 1 ms 一次的定时器中断递增全局时基
g_sys_tick。 - 定义一个任务控制块
Task_t,包含func:任务入口函数,period:周期(ms),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,写一个函数直接放在屏幕显示的任务中既可。
电机编码器
电机的编码器用来测量实际的位置和速度。