裸机多任务的一些摸索

编写程序去自动化地执行一些任务时,难免会遇到一些需要同时执行或者需要执行很久、在不同时刻执行内容不同的程序任务。在成熟的操作系统上,使用多线程的接口来把调度的活儿交给操作系统来完成是非常方便的。从底层看,如果是用的是资源较少的单片机,No-os裸机代码应该如何优雅地完成多任务其实是个比较棘手的问题,尤其是在任务复杂、情况众多并且需要不断迭代增减功能的需求上时更甚。本文将讨论我在编写四旋翼飞行器控制器(2018)与某工业检测设备电机控制器(2019)时,不断迭代中摸索出的一些不成熟的方法,在最近阅读Linux内核源码的过程中,算是对调度算法的一个背景先导总结,也试图为自己日后的裸机多任务开发提供一个更好的架构思路。

我把需要执行的任务分成三种类型,一是需要快速执行并且需要高频率反复执行的任务,二是需要长时间分不同步骤完成、执行周期较长的任务,三是需要条件触发执行的混合型任务。

一个典型的裸机C程序往往是前后台结构的,以main函数中的infinite loop作为后台,中断服务程序作为前台,用于处理系统的异步事件。以智能车比赛中的小车为例,中断来处理一些与传感器的通信,比如定时器中断去扫描接上了红外传感器的引脚,一方面根据左右偏移误差带入一个需要高频重复执行的电机调速控制来完成循迹,另一方面封装一个函数来记录整车通过的路线情况,通过全局变量把信息传递给后台做判断,在main函数里while1里堆叠while与break即可完成一整套已知地图的赛道程序。这里显然遇到了一个问题,我们在写较长流程的任务时,到底应该用while+break依靠main中while1一次循环时间可以较长的特点来完成长时任务,还是应该把程序设计成while1快速循环,使用if或者switch通过一个变量来切换较长周期内不同时刻的不同任务呢?

我的结论是,虽然while+break十分暴力直接,甚至可以直接暴力delay,不需要像第二种方法一样一直在脑内模拟任务执行从头到尾的每一个时刻的每一个变量的情况,但是顺序长时执行确实难以做到任务的快速切换,比如程序正在某个while中,这时我想切去一个高频任务,只能通过唤起定时器中断来做,又或者我想从此while中跳回前面的while中,在整个大的while1里,我需要把下面的所有while里都做一个if break来循环回去,这显然不科学。

那么我们来看第二种方法,主任务分布在快速循环的while1(时间不可控但也基本不限时)与高频的定时器中断(定时执行时间可控但要快入快出)里。以我之前写的四旋翼飞控为例,我在while1里什么都没做,所有的任务放到systemTickInterrupt中来调度,这是一个5ms的中断,我使用了n个static变量来计数,分别在10ms、15ms、25ms…来执行不同的高频任务,也就是不停地if变量==2、3、5,注意这里最好不要用20ms,因为20的时候刚好是10ms的第二次进入,这会造成第20个sysTick中断时间比较长。我在这个中断里配合其他的异步短时中断,完成了整个飞控的姿态控制与三维位置控制,在另一个定时器中断里,通过计时改变全局标志位来完成比赛中的长时流程、自动起飞、悬停……到降落的流程。因为流程并不复杂,所以在此定时器中的编写不需要再去跳跃地切换不同类型的任务,只需要快速重进不停判断。

去年写的某工业检测设备电机控制器程序,长时流程逻辑要复杂很多,不像四旋翼飞行器大部分时候单纯的底层控制是高频重复的,此控制器一方面要控制较长的流程以及流程分支,并且还要大量循环这个较长的流程(工业机器开机就要一直跑),对整个系统的robustness要求较高。一方面因为去年早期时候需求还没明确修修改改了好几个版本,另一方面因为我一心想着回家考研,所以以实现功能为目的没有做好前期的架构设计。最近我开始尝试重构底层代码,我将其称为workFlow模式,本质上它还是快速重进判断调度,在小林学弟修改的版本上,将耗时的工作分到while1中,将主流程放到定时器中断中switch(flag.workFlow)来切换不同的工作状态,只要确保一个工作流有生死闭环即可控制程序在任何你想要的时刻切入你想要执行的任务,并且程序各处都可以通过workFlow来判断流程进行到哪了,比如光电开关外部中断可以在不同的工作流完成不同目的的信息获取。这大大降低了二次开发的成本(改需求你懂的)。这个框架是我自己脑补出来的,我知道肯定有现成的更好的方法,所以需要再学习,多读大佬的代码。

计划是测试完这个No-osVer,我就不再更新框架了,毕竟还是RTOS比较香,想来也确实RTOS比较适合这个项目,有网络通信的部分,后期硬件迭代好上rt-thread试试,我又立了个flag……

下一篇研究一下除了全局变量,任务间有没有其他方法实现通信。