单片机多任务调度

单片机多任务调度

mcu由于内部资源的限制,软件设计有其特殊性,程序一般没有复杂的算法以及数据结构,代码量也不大, 通常不会使用OS (Operating System),  因为对于一个只有 若干K ROM, 一百多byte RAM 的 mcu 来说,一个简单OS  也会吃掉大部分的资源。

对于无 os 的系统,流行的设计是主程序(主循环 ) + (定时)中断,这种结构虽然符合自然想法,不过却有很多不利之处,首先是中断可以在主程序的任何地方发生,随意打断主程序。其次主程序与中断之间的耦合性(关联度)较大,这种做法 使得主程序与中断缠绕在一起,必须仔细处理以防不测。

那么换一种思路,如果把主程序全部放入(定时)中断中会怎么样?这么做至少可以立即看到几个好处: 系统可以处于低功耗的休眠状态,将由中断唤醒进入主程序; 如果程序跑飞,则中断可以拉回;没有了主从之分(其他中断另计),程序易于模块化。

(题外话:这种方法就不会有何处喂狗的说法,也没有中断是否应该尽可能的简短的争论了)

为了把主程序全部放入(定时)中断中,必须把程序化分成一个个的模块,即任务,每个任务完成一个特定的功能,例如扫描键盘并检测按键。 设定一个合理的时基 (tick), 例如  5, 10 或 20 ms,  每次定时中断,把所有任务执行一遍,为减少复杂性,一般不做动态调度(最多使用固定数组以简化设计,做动态调度就接近 os 了),这实际上是一种无优先级时间片轮循的变种。来看看主程序的构成:

void main()

{

….   // Initialize

while (true) {

IDLE;     //sleep

}

}

这里的 IDLE 是一条sleep 指令,让 mcu 进入低功耗模式。中断程序的构成

void Timer_Interrupt()

{

SetTimer();

ResetStack();

Enable_Timer_Interrupt;

….

进入中断后,首先重置Timer, 这主要针对8051, 8051 自动重装分频器只有 8-bit, 难以做到长时间定时;复位 stack ,即把stack 指针赋值为栈顶或栈底(对于 pic, TI DSP 等使用循环栈的 mcu 来说,则无此必要),用以表示与过去决裂,而且不准备返回到中断点,保证不会保留程序在跑飞时stack 中的遗体。Enable_Timer_Interrupt 也主要是针对8051。8051 由于中断控制较弱,只有两级中断优先级,而且使用了如果中断程序不用 reti 返回,则不能响应同级中断这种偷懒方法,所以对于 8051, 必须调用一次 reti 来开放中断:

_Enable_Timer_Interrupt:

acall       _reti

_reti:        reti

下面就是任务的执行了,这里有几种方法。第一种是采用固定顺序,由于mcu 程序复杂度不高,多数情况下可以采用这种方法:

Enable_Timer_Interrupt;

ProcessKey();

RunTask2();

RunTaskN();

while (1) IDLE;

可以看到中断把所有任务调用一遍,至于任务是否需要运行,由程序员自己控制。另一种做法是通过函数指针数组:

#define CountOfArray(x) (sizeof(x)/sizeof(x[0]))

typedef void (*FUNCTIONPTR)();

const FUNCTIONPTR[] tasks = {

ProcessKey,

RunTask2,

RunTaskN

};

void Timer_Interrupt()

{

SetTimer();

ResetStack();

Enable_Timer_Interrupt;

for (i=0; i<CountOfArray (tasks), i++)

(*tasks[i])();

while (1) IDLE;

}

使用const 是让数组内容位于 code segment (ROM) 而非 data segment (RAM) 中,8051 中使用 code 作为 const 的替代品。

(题外话:关于函数指针赋值时是否需要取地址操作符 & 的问题,与数组名一样,取决于 compiler. 对于熟悉汇编的人来说,函数名和数组名都是常数地址,无需也不能取地址。对于不熟悉汇编的人来说,用 & 取地址是理所当然的事情。Visual C++ 2005对此两者都支持)

这种方法在汇编下表现为散转, 一个小技巧是利用 stack 获取跳转表入口:

mov                A, state

acall                MultiJump

ajmp               state0

ajmp               state1

...

MultiJump:                  pop                DPH

pop                DPL

rl                    A

jmp                @A+DPTR

还有一种方法是把函数指针数组(动态数组,链表更好,不过在 mcu 中不适用)放在 data segment 中,便于修改函数指针以运行不同的任务,这已经接近于动态调度了:

FUNCTIONPTR[COUNTOFTASKS] tasks;

tasks[0] = ProcessKey;

tasks[0] = RunTaskM;

tasks[0] = NULL;

...

FUNCTIONPTR pFunc;

for (i=0; i< COUNTOFTASKS; i++)  {

pFunc = tasks[i]);

if (pFunc != NULL)

(*pFunc)();

}

通过上面的手段,一个中断驱动的框架形成了,下面的事情就是保证每个 tick 内所有任务的运行时间总和不能超过一个tick 的时间。为了做到这一点,必须把每个任务切分成一个个的时间片,每个 tick 内运行一片。这里引入了状态机 (state machine) 来实现切分。关于 state machine,  很多书中都有介绍, 这里就不多说了。

(题外话:实践升华出理论,理论再作用于实践。我很长时间不知道我一直沿用的方法就是state machine,直到学习UML/C++,书中介绍 tachniques for identifying dynamic behvior,方才豁然开朗。功夫在诗外,掌握 C++, 甚至C# JAVA,对理解嵌入式程序设计,会有莫大的帮助)

状态机的程序实现相当简单,第一种方法是用 swich-case 实现:

void RunTaskN()

{

switch (state) {

case 0: state0(); break;

case 1: state1(); break;

case M: stateM(); break;

default:

state = 0;

}

}

另一种方法还是用更通用简洁的函数指针数组:

const FUNCTIONPTR[] states = { state0, state1, …, stateM };

void RunTaskN()

{

(*states[state])();

}

下面是 state machine 控制的例子:

void state0() { }

void state1() { state++; }   //  next state;

void state2() { state+=2; }   //  go to state 4;

void state3() { state--; }      //  go to previous state;

void state4() { delay = 100; state++; }

void state5() { delay--; if (delay <= 0) state++; }   //delay 100*tick

void state6() { state=0; }      //  go to the first state

一个小技巧是把第一个状态 state0 设置为空状态,即:

void state0() { }

这样,state =0可以让整个task 停止运行,如果需要投入运行,简单的让 state = 1 即可。

以下是一个键盘扫描的例子,这里假设 tick = 20 ms, ScanKeyboard() 函数控制口线的输出扫描,并检测输入转换为键码,利用每个state 之间 20 ms 的间隔去抖动。

enum EnumKey {

EnumKey_NoKey =  0,

};

struct StructKey {

int                keyValue;

bool                keyPressed;

} ;

struct StructKeyProcess key;

void ProcessKey() { (*states[state])(); }

void state0() { }

void state1() { key.keyPressed = false; state++; }

void state2() { if (ScanKey() != EnumKey_NoKey) state++; }  //next state if a key pressed

void state3()

{                                                               //debouncing state

key.keyValue = ScanKey();

if (key.keyValue == EnumKey_NoKey)

state--;

else {

key.keyPressed = true;

state++;

}

}

void state4() {  if (ScanKey() == EnumKey_NoKey) state++; }  //next state if the key released

void state5() {  ScanKey() == EnumKey_NoKey? state = 1 : state--; }

上面的键盘处理过程显然比通常使用标志去抖的程序简洁清晰,而且没有软件延时去抖的困扰。以此类推,各个任务都可以划分成一个个的state, 每个state 实际上占用不多的处理时间。某些任务可以划分成若干个子任务,每个子任务再划分成若干个状态。

(题外话:对于常数类型,建议使用 enum 分类组织,避免使用大量 #define 定义常数)

对于一些完全不能分割,必须独占的任务来说,比如我以前一个低成本应用中红外遥控器的软件解码任务,这时只能牺牲其他的任务了。两种做法:一种是关闭中断,完全的独占;

void RunTaskN()

{

Disable_Interrupt;

Enable_Interrupt;

}

第二种,允许定时中断发生,保证某些时基 register 得以更新;

void Timer_Interrupt()

{

SetTimer();

Enable_Timer_Interrupt;

UpdateTimingRegisters();

if (watchDogCounter = 0) {

ResetStack();

for (i=0; i<CountOfArray (tasks), i++)

(*tasks[i])();

while (1) IDLE;

}

else

watchDogCounter--;

}

只要watchDogCounter 不为 0,那么中断正常返回到中断点,继续执行先前被中断的任务,否则,复位 stack, 重新进行任务循环。这种状况下,中断处理过程极短,对独占任务的影响也有限。

中断驱动多任务配合状态机的使用,我相信这是mcu 下无os 系统较好的设计结构。对于绝大多数 mcu 程序设计来说,可以极大的减轻程序结构的安排,无需过多的考虑各个任务之间的时间安排,而且可以让程序简洁易懂。缺点是,程序员必须花费一定的时间考虑如何切分任务。

下面是一段用 C 改写的CD Player 中检测 disc 是否存在的伪代码,用以展示这种结构的设计技巧,原源代码为Z8 mcu 汇编, 基于 Sony 的 DSP, Servo and RF 处理芯片, 通过送出命令字来控制主轴/滑板/聚焦/寻迹电机,并读取状态以及 CD 的sub Q 码。这个处理任务只是一个大任务下用state machine切开的一个二级子任务,tick = 20 ms。

state1() { InitializeMotor(); state++; }

state2() {

if (innerSwitch != ON) {

SendCommand(EnumCommand_SlidingMotorBackward);

timeout = MILLISECOND(10000);

state++;                // 滑板电机向内运动, 直至触及最内开关。

}

else

state +=                2;

}

state3() {

if ((--timeout) == 0) {   //note: some C compliers do not support (--timeout) ==

SendCommand(EnumCommand_SlidingMotorStop)

systemErrorCode = EnumErrorCode_InnerSwitch;

state = 0;    // 10 s 超时错误,

}

else {

if (innerSwitch == ON) {

SendCommand(EnumCommand _SlidingMotorStop)

timeout = MILLISECOND(200);                  // 200ms电机停止时间

state++;

}

}

}

state4() { if ((--timeout) == 0) state++; }                  //等待电机完全停止

state5() {

SendCommand(EnumCommand_SlidingMotorForward);

timeout = MILLISECOND(2000);

state++;

}                // 滑板电机向外运动,脱离inner switch

state6() {

if ((--timeout) == 0) {

SendCommand(EnumCommand_SlidingMotorStop)

systemErrorCode = EnumErrorCode_InnerSwitch;

state = 0;              // 2 s 超时错误,

}

else {

if (innerSwitch == OFF) {

SendCommand(EnumCommand_SlidingMotorStop)

timeout = MILLISECOND(200);                  // 200ms电机停止时间

state++;

}

}

}

state7() { state4(); }

state8() { LaserOn(); state++; retryCounter = 3;}                 //打开激光器

state9() {

SendCommand(FocusUp);

state++;

timeout = MILLISECOND(2000);

}                  //光头上举,检测聚焦过零 3 次,判断cd 是否存在

state10() {

if (FocusCrossZero)  {

systemStatus.Disc = EnumStatus_DiscExist;

SendCommand(EnumCommand_AutoFocusOn);    //有cd, 打开自动聚焦。

state = 0;                             //本任务结束。

playProcess.state = 1;                //启动 play 任务

}

else if ((--timeout) == 0) {

SendCommand(EnumCommand_ FocusClose);                  //光头聚焦复位

if ((--retryCounter) == 0) {

systemStatus.Disc = EnumStatus_Nodisc;       //无盘

displayProcess.state = EnumDisplayState_NoDisc;  //显示闪烁的无盘

LaserOff();

state = 0;                //任务停止

}

else

state--;                                 //再试

}

}

stateStop() {

SendCommand(EnumCommand_SlidingMotorStop);

SendCommand(EnumCommand_FocusClose);

state = 0;

}

原文地址:https://www.cnblogs.com/tianqiang/p/9256542.html

时间: 2024-11-06 03:36:39

单片机多任务调度的相关文章

【SmartOS】轻量级多任务调度系统

SmartOS是一个完全由新生命团队设计的嵌入式操作系统,主要应用于智能家居.物联网.工业自动化控制等领域. ARM Cortex-M系列微处理器几乎全都做成单核心,对于业务逻辑较复杂的物联网就显得难以使用,因此SmartOS设计了两个多任务调度系统:1,多线程调度,重量级,逼近PC操作系统多线程用法.使用上需要特别小心,要合理分配每一个线程的栈空间大小,任务越多越容易出问题2,大循环,轻量级.每个任务注册一个函数指针,然后由主线程轮询各个任务函数,轮流执行 本文主要讲解第二种,轻量级多任务调度

在PHP中使用协程实现多任务调度

PHP5.5一个比较好的新功能是加入了对迭代生成器和协程的支持.对于生成器,PHP的文档和各种其他的博客文章已经有了非常详细的讲解.协程相对受到的关注就少了,因为协程虽然有很强大的功能但相对比较复杂, 也比较难被理解,解释起来也比较困难. 这篇文章将尝试通过介绍如何使用协程来实施任务调度, 来解释在PHP中的协程. 我将在前三节做一个简单的背景介绍.如果你已经有了比较好的基础,可以直接跳到“协同多任务处理”一节. 迭代生成器 生成器也是一个函数,不同的是这个函数的返回值是依次输出,而不是只返回一

动手写操作系统 -- 多任务调度

同步来自我的博客:http://fillzero.github.io/os/diy-os-12.html

小X教你写嵌入式操作系统之------(一)多任务抢占调节机制

/**************************************************** Title: 嵌入式系统多任务抢占机制 Framework:MyOS V 1.0 Bate Date:2014-9-18 21:31:54 Author:小X Remark:ARM实现系统任务的调度 *****************************************************/ 今天我给大家带来的是如何理解嵌入式系统多任务机制. 我们先来写一个多任务调节主架构

CFS调度器

一.前言 随着内核版本的演进,其源代码的膨胀速度也在递增,这让Linux的学习曲线变得越来越陡峭了.这对初识内核的同学而言当然不是什么好事情,满腔热情很容易被当头浇灭.我有一个循序渐进的方法,那就是先不要看最新的内核,首先找到一个古老版本的内核(一般都会比较简单),将其吃透,然后一点点的迭代,理解每个版本变更背后的缘由和目的,最终推进到最新内核版本. 本文就是从2.4时代的任务调度器开始,详细描述其实现并慢慢向前递进.当然,为了更好的理解Linux调度器设计和实现,我们在第二章给出了一些通用的概

μC/OS-Ⅱ在C8051F060上的移植及其应用

嵌入式操作系统是嵌入式应用的基础和核心.随着应用系统的不断复杂化和系统实时性需求的不断提高,对相应软件的逻辑结构.稳定性.实时性也提出了更高的要求,以传统的前后台编程模式编制软件将更加困难,而且容易出错,因此,嵌入式实时操作系统(简称RTOS)就成为一个比较好的解决方法.使用RTOS作为应用程序的运行平台,它将应用程序分解为多个任务,负责各个任务调度.资源管理.任务通信等,可使系统更加稳定可靠,程序易于扩展.移植和维护.[1] μC/OS-Ⅱ是一个源码公开的.非商业用途时免费的嵌入式实时操作系统

嵌入式人才培养难在哪儿 华清远见总裁发表观点

嵌入式人才培养难在哪儿 华清远见总裁发表观点   引 言 早些年,高等院校确确实实培养了一大批嵌入式系统师资队伍,但其中不少教师长期脱离一线科研,而从事嵌入式科研工作的教师,常常忙于繁重的科研工作,难以深入到嵌入式系统基础课程的教学改革之中.由此造成的局面是:一方面,毕业生苦于找不到合适的工作;另一方面,企业抱怨找不到急需的嵌入式人才.作为高校的有益补充,十多年前就涌现出不少专业的机构从事嵌入式培训,孜孜不倦地用心打造“精英人才”.这类培训针对性强,立竿见影,但嵌入式学习毕竟不是一蹴而就的,经过

SylixOS周期定时抖动分析

1.概述 自动化与控制行业中有很多场景需要循环周期控制,而运动控制领域对循环周期控制的时间确定性要求尤为严格,周期抖动的时间确定性和周期控制的极限范围直接影响运动控制产品的质量和性能.SylixOS作为一款嵌入式实时操作系统,针对不同循环周期和不同程度的时间确定性要求均有对应的周期控制方式,其中就有为周期性任务解决多任务调度冲突并且时间确定性良好的速率单调调度(Rate Monotonic Scheduling RMS),还有传统的以硬件平台定时器作为中断源,以中断服务程序作为实时任务运行载体的

Ansible API 2.0解析

import json from collections import namedtuple from ansible.parsing.dataloader import DataLoader from ansible.vars import VariableManager from ansible.inventory import Inventory from ansible.playbook.play import Play from ansible.executor.task_queue_