我使用嵌入式(C)编写了一个使用8051微控制器的腕表程序。总共有6个7段显示器:
_______________________
| | | | two 7-segments for showing HOURS
| HR | MIN | SEC | two 7-segments for showing MINUTES and
|______._______.________| two 7-segments for showing SECONDS
7-segment LED display
要更新小时,分钟和秒,我们使用了3个for
循环。这意味着首先会更新秒数,然后是分钟数,然后是小时数。然后我问我的教授为什么我们不能同时更新(我的意思是一小时后增加小时数而不等待更新的会议记录)。他告诉我,由于指令的顺序执行,我们无法进行并行处理。
问题:
数字生日卡,可同时连续播放音乐,同时闪烁LED。数字闹钟会在特定时间发出哔哔声。虽然它正在产生声音,但时间将继续更新。所以声音和时间增量都是并行运行的。他们是如何通过顺序执行来实现这些结果的?
如何在微控制器中同时运行多个任务(调度)?
答案 0 :(得分:27)
首先,这个顺序执行的内容是什么。只有一个核心,一个程序空间,一个计数器。 MPU一次执行一条指令,然后依次移动到另一条指令。在这个系统中,没有任何固有的机制可以让它停止做一件事并开始做另一件事 - 这是一个程序,它完全掌握在程序员的手中,序列将是什么以及什么它会做;只要MPU正在运行,它将按顺序一次持续一条指令,除非程序员首先发生,否则不会发生任何其他事情。
现在,多任务处理:
通常,操作系统提供多任务处理,具有非常复杂的调度算法。
通常,微控制器在没有操作系统的情况下运行。
那么,您如何在微控制器中实现多任务处理?
简单的答案是"你没有"。但通常情况下,简单的答案很少涵盖超过5%的案例......
您在编写真正的抢先式多任务处理时非常困难。大多数微控制器都没有这方面的设施,而英特尔CPU通过一些特定指令执行的操作则需要您编写数英里的代码。最好不要忘记微控制器的经典多任务处理,除非你真的没有更好的时间与你相处。
现在,经常使用两种常用的方法,而不是那么麻烦。
大多数微控制器都有不同的中断源,通常包括定时器。因此,主循环连续运行一个任务,当计时器计数到零时,发出中断。主循环停止,执行跳转到称为“中断向量”的地址。在那里,启动了一个不同的程序,执行不同的一次性任务。一旦完成(如果需要可能重置定时器),您将从中断返回并恢复主循环。
微控制器通常有一些定时器,你可以为每个定时器分配一个任务,更不用说其他外部中断的任务(比如键盘输入 - 按下键,或通过RS232到达的数据。)
虽然这种方法非常有限,但对于大多数情况来说它确实足够了;特别是你的:设置定时器到1s循环,在中断时计算新的小时,改变显示,然后保留中断。在主循环中等待日期到达生日,当它开始播放音乐并闪烁LED时。
这就是早期的做法。你需要写下你的任务'作为子程序,每个子程序内部都有一个有限状态机(或一个循环的单程),以及" OS"是按顺序跳转到连续任务的简单循环。
每次跳转后,MPU开始执行给定任务,并将继续执行,直到任务返回控制,在首次保存状态后,在再次启动时恢复它。任务工作的每次通过都应该非常短。任何延迟循环必须用有限状态引擎中的等待状态替换(如果条件不满足则返回。如果是,则更改状态。)所有较长的循环必须展开到不同的状态("状态:复制数据块,复制字节N,增加N,N =结束?是:下一状态,否:返回控制)
写这种方式比较困难,但解决方案更加强大。在您的情况下,您可能有四个任务:
如果没有新的第二个到达,则时钟返回控制。如果是,则重新计算秒数,分钟数,小时数,日期数,然后返回。
显示更新显示的值。如果您对8段显示器上的数字进行多路复用,则每次传递将更新一个数字,下一个传递 - 下一个数字等。
在没有生日的情况下播放声音会等待(收益)。如果是生日,请从内存中选择样本值,将其输出到扬声器,然后输出。如果您之前调用的声音比您应该输出下一个声音的话,可以选择性地产生。
闪烁 - 好吧,输出正确的状态到LED,产量。
非常短的循环 - 比方说,5行的10次迭代 - 仍然被允许,但任何更长的循环都应该转换为有限状态引擎的状态。
现在,如果你感觉很难,你可以尝试一下......
每个任务都是一个通常无限执行的程序,只做自己的事情。写得正常,尽量不要踩到其他程序'内存,但否则使用资源,好像世界上没有其他东西可以需要它们。
您的操作系统任务是从定时器中断启动的。
在中断开始时,OS任务必须保存最后一个任务的当前易失性状态 - 寄存器,中断返回地址(应该从中恢复任务),当前堆栈指针,保持在记录中那个任务。
然后,使用调度程序算法,它从列表中选择另一个进程,该进程应该立即开始;恢复其所有状态,然后在先前被抢占时覆盖自己的中断返回地址和该进程停止的地址。在结束中断时,恢复被抢占进程的正常操作,直到另一个中断再次将控制权切换到OS。
正如您所看到的,有很多开销,包括保存和恢复程序的完整状态,而不仅仅是当前任务需要的内容,但程序不需要编写作为有限状态机 - 正常的顺序样式就足够了。
答案 1 :(得分:12)
虽然SF提供了对多任务处理的完美概述,但大多数微控制器还有一些额外的硬件可以让他们同时做事。
同时执行的错觉 - 从技术上讲,您的教授是正确的,并且无法同时进行更新。但是,处理器非常快。对于许多任务,它们可以按顺序执行,例如一次更新一个7段显示,但它的速度非常快,以至于人类感知无法分辨每个显示是否按顺序更新。这同样适用于声音。大多数可听声音在千赫范围内,而处理器在兆赫范围内运行。处理器有足够的时间播放部分声音,做其他事情,然后返回播放声音,而不会让你的耳朵能够发现声音。
中断 - SF很好地介绍了中断的执行情况,因此我会对机制进行掩饰并更多地讨论硬件问题。大多数微控制器都有小型硬件模块,可与指令执行同时运行。定时器,UARTS和SPI是执行特定操作的常用模块,而处理器的主要部分执行指令。当给定模块完成其任务时,它通知处理器并且处理器跳转到模块的中断代码。这种机制允许你执行诸如在执行指令时通过uart(相对较慢)传输字节的事情。
PWM - PWM(脉冲宽度调制)是一个基本上产生方波的硬件模块,一次两个,但方块不必是均匀的(我正在简化)这里)。一个可能比另一个长,或者它们可以是相同的大小。您在硬件中配置方块的大小,然后PWM连续生成它们。该模块可用于驱动电机甚至产生声音,其中电机的速度或声音的频率取决于两个方块的比例。要播放音乐,处理器只需要在音符改变的时候(可能基于定时器中断)改变比率,并且在此期间它可以执行其他指令。
DMA - DMA(直接内存访问)是一种特定类型的硬件,可自动将字节从一个内存位置复制到另一个内存位置。像ADC这样的东西可能会不断地将转换后的值写入内存中的特定寄存器。 DMA控制器可配置为从一个地址(ADC输出)连续读取,同时顺序写入一系列存储器(如缓冲器,以便在求平均值之前接收多个ADC转换)。当主处理器执行指令时,所有这些都发生在硬件中。
定时器,UART,SPI,ADC等 - 还有许多其他硬件模块(这里要介绍的太多)在程序执行的同时执行特定任务。
TL / DR - 虽然程序指令只能按顺序执行,但处理器通常可以足够快地执行它们,使它们看起来同时发生。同时,大多数微控制器都有额外的硬件,可以在程序执行的同时完成特定的任务。
答案 2 :(得分:6)
Zack和SF.的答案很好地涵盖了整体情况。但有时一个有效的例子是有价值的。
虽然我可以明智地建议浏览Linux内核的源代码包(它既是开源的,也可以在单核机器上提供多任务处理),但这不是开始理解如何实际实现调度器。
更好的起点是使用源工具包到数百个(如果不是数千个)实时操作系统中的一个。其中许多是开源的,大多数都可以在极小的处理器上运行,包括8051.我将在这里更详细地描述Micrium的uC / OS-II,因为它具有一组典型的功能,而且它是我的'广泛使用。我过去评估过的其他内容包括OS-9,eCos和FreeRTOS。以这些名称为起点以及“RTOS”之类的关键词,Google将奖励您许多其他人的名字。
我第一次接触RTOS内核将是uC/OS-II(或者它的新家庭成员uC / OS-III)。这是一款商业产品,它开始作为嵌入式系统设计杂志读者的教育活动。杂志文章及其附带的源代码成为关于该主题的更好书之一的主题。操作系统是开源的,但确实对商业用途有许可限制。为了披露,我是ColdFire MCF5307的uC / OS-II端口的作者。
由于它最初是作为教育工具编写的,因此源代码已有详细记录。教科书(至少在我的架子上的第2版,至少在这里)也写得很好,并且在它支持的每个功能上都有很多理论背景。
我在几个产品开发项目中成功地使用了它,并且会再次考虑它需要多任务但不需要像Linux这样的完整操作系统的重量。
uC / OS-II提供抢占式任务调度程序,以及一组有用的任务间通信原语(信号量,互斥,邮箱,消息队列),定时器和线程安全的池内存分配器。它还支持任务优先级,如果使用正确,还包括死锁预防。
它完全是在标准C的一个子集中编写的(满足MISRA-C:1998指南的几乎所有要求),这使得它有可能获得各种安全关键认证。
虽然我的应用程序从不在安全关键系统中,但我很高兴知道我所站的操作系统内核已达到了这些评级。它保证了我有一个错误的最可能的原因是对原语如何工作或可能更可能的误解实际上是我的应用程序逻辑中的错误。
大多数RTOS(尤其是uC / OS-II)能够在有限的资源下运行。 uC / OS-II可以只用6KB的代码构建,OS结构只需要1KB的RAM。
底线是明显的并发性可以通过多种方式实现,其中一种方法是使用OS内核,通过在所有方面共享顺序CPU的资源来并行地调度和执行每个并发任务任务。对于简单的情况,您可能需要的只是中断处理程序和主循环,但是当您的需求增长到实现多个协议,管理显示,管理用户输入,后台计算和监视整体系统运行状况时,站在井上设计的RTOS内核以及已知的工作通信原语可以节省大量的开发和调试工作。
答案 3 :(得分:6)
嗯,我看到其他答案涵盖了很多理由;所以,希望我最终不会把它变成比我想要的更大的东西。 (TL; DR:女孩救援!:D)。但是,我确实(我相信是)一个非常好的解决方案;所以我希望你能利用它。我只对8051 [☆] 有一点经验;虽然我在另一台微控制器上工作了大约3个月(加上大约3个全职),但取得了一定的成功。在此过程中,我最终做了几乎所有小东西所提供的一切:串行通信,SPI,PWM信号,伺服控制,DIO,热电偶等等。当我正在研究它的时候,我很幸运,遇到了一个优秀的(IMO)解决方案,用于(协作)'线程'调度,它与PIC中断完成的一些额外的实时内容很好地混合在一起。当然还有其他设备的其他中断处理程序。
pt_thread:由Adam Dunkels(与Oliver Schmidt合作)(2005年2月发布的v1.0)发明,他的网站是对他们的一个很好的介绍,魔杖包括从2006年10月开始的v1.4下载;因为我找到了,我很高兴再去看看;但是有一个项目从2009年1月开始,说Larry Ruane使用事件驱动的技术“[用于]完整的重新实现[使用GCC;并且]一个非常好的语法”,and available on sourceforge。不幸的是,从2009年左右开始,似乎没有任何更新;但2006版本对我很有帮助。最后一条新闻(自2009年12月起)注意到“Sonic Unleashed”在其手册中指出使用了protothreads!
我认为pt_threads非常棒的事情之一就是它们如此简单;而且,无论新版(Ruane)版本的好处如何,它肯定更复杂。虽然值得一看,但我会坚持使用Dunkels的原始实现。他原来的pt_threads
“库”包括:五个头文件。而且,真的,这似乎是一种夸大其词,因为一旦我缩小了一些宏和其他东西,删除了doxygen部分,示例,并将评论剔除到最低限度,我仍然觉得给出了解释,它只是在115行(包括在下面。)
源tarball中包含一些示例,并且在他的站点上提供了非常好的.pdf文档(或.html)(上面链接)。但是,让我通过一个简单的例子来阐明一些概念。 (不是宏本身,我花了一些时间来理解这些,并且他们并不是真正需要使用这些功能。:D)
不幸的是,我今晚已经没时间了;但我会在明天的某个时候尝试重新写下一个小例子;无论哪种方式,他的网站上都有吨资源,上面链接;这是一个相当简单的过程,对我来说是棘手的部分(因为我认为它与任何合作的多线程; Win 3.1任何人?:D)确保我对代码进行了正确的循环计数,以免超时我需要在产生pt_thread之前处理下一件事。
我希望这会给你一个开始;如果你试一试,请告诉我它是怎么回事!
FILE: pt.h
#ifndef __PT_H__ #define __PT_H__ #include "lc.h" // NOTE: the enums are mine to compress space; originally all were #defines enum PT_STATUS_ENUM { PT_WAITING, PT_YIELDED, PT_EXITED, PT_ENDED }; struct pt { lc_t lc; } // protothread control structure (pt_thread) #define PT_INIT(pt) LC_INIT((pt)->lc) // initializes pt_thread prior to use // you can use this to declare pt_thread functions #define PT_THREAD(name_args) char name_args // NOTE: looking at this, I think I might define my own macro as follows, so as not // to have to redclare the struct pt *pt every time. //#define PT_DECLARE(name, args) char name(struct pt *pt, args) // start/end pt_thread (inside implementation fn); must always be paired #define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc) #define PT_END(pt) LC_END((pt)->lc);PT_YIELD_FLAG = 0;PT_INIT(pt);return PT_ENDED;} // {block, yield} 'pt' {until,while} 'c' is true #define PT_WAIT_UNTIL(pt,c) do { \ LC_SET((pt)->lc); if(!(c)) {return PT_WAITING;} \ } while(0) #define PT_WAIT_WHILE(pt, cond) PT_WAIT_UNTIL((pt), !(cond)) #define PT_YIELD_UNTIL(pt, cond) \ do { PT_YIELD_FLAG = 0; LC_SET((pt)->lc); \ if((PT_YIELD_FLAG == 0) || !(cond)) { return PT_YIELDED; } } while(0) // NOTE: no corresponding "YIELD_WHILE" exists; oversight? [shelleybutterfly] //#define PT_YIELD_WHILE(pt,cond) PT_YIELD_UNTIL((pt), !(cond)) // block pt_thread 'pt', waiting for child 'thread' to complete #define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread)) // spawn pt_thread 'ch' as child of 'pt', waiting until 'thr' exits #define PT_SPAWN(pt,ch,thr) do { \ PT_INIT((child)); PT_WAIT_THREAD((pt),(thread)); } while(0) // block and cause pt_thread to restart its execution at its PT_BEGIN() #define PT_RESTART(pt) do { PT_INIT(pt); return PT_WAITING; } while(0) // exit the pt_thread; if a child, then parent will unblock and run #define PT_EXIT(pt) do { PT_INIT(pt); return PT_EXITED; } while(0) // schedule pt_thread: fn ret != 0 if pt is running, or 0 if exited #define PT_SCHEDULE(f) ((f) lc); \ if(PT_YIELD_FLAG == 0) { return PT_YIELDED; } } while(0)
FILE: lc.h
#ifndef __LC_H__ #define __LC_H__ #ifdef LC_INCLUDE #include LC_INCLUDE #else #include "lc-switch.h" #endif /* LC_INCLUDE */ #endif /* __LC_H__ */
FILE: lc-switch.h
// WARNING: implementation using switch() won't work with an LC_SET() inside a switch()! #ifndef __LC_SWITCH_H__ #define __LC_SWITCH_H__ typedef unsigned short lc_t; #define LC_INIT(s) s = 0; #define LC_RESUME(s) switch(s) { case 0: #define LC_SET(s) s = __LINE__; case __LINE__: #define LC_END(s) } #endif /* __LC_SWITCH_H__ */
FILE: lc-addrlabels.h
#ifndef __LC_ADDRLABELS_H__ #define __LC_ADDRLABELS_H__ typedef void * lc_t; #define LC_INIT(s) s = NULL #define LC_RESUME(s) do { if(s != NULL) { goto *s; } } while(0) #define LC_CONCAT2(s1, s2) s1##s2 #define LC_CONCAT(s1, s2) LC_CONCAT2(s1, s2) #define LC_END(s) #define LC_SET(s) \ do {LC_CONCAT(LC_LABEL, __LINE__):(s)=&&LC_CONCAT(LC_LABEL,__LINE__);} while(0) #endif /* __LC_ADDRLABELS_H__ */
FILE: pt-sem.h
#ifndef __PT_SEM_H__ #define __PT_SEM_H__ #include "pt.h" struct pt_sem { unsigned int count; }; // macros to initiaize, await, and signal a pt_sem semaphore #define PT_SEM_INIT(s, c) (s)->count = c #define PT_SEM_WAIT(pt, s) do \ { PT_WAIT_UNTIL(pt, (s)->count > 0); -(s)->count; } while(0) #define PT_SEM_SIGNAL(pt, s) ++(s)->count #endif /* __PT_SEM_H__ */
[☆] *大约一周了解微控制器 [†] 并在评估过程中与它一起玩一周,以确定它是否能满足我们的需求用于可更换线路的远程I / O单元。 (长话短说:不)
[†] The 8051 Microcontroller, Third Edition *被建议给我作为8051编程“圣经”我不知道是不是是不是,但我当然能够解决使用它的问题。 [‡]
[‡] 甚至现在再看一遍,我看不太喜欢它。 :)好吧,我的意思是......我希望我没有买两份;但他们所以便宜!
LICENSE AGREEMENT (where applicable)
This post contains code based on (or taken from) 'The Protothreads Library' (referred to herein and henceforth as "PTLIB"; including v1.4 and earlier revisions) relying extensively on the source code as well as the documentation for PTLIB. PTLIB original source code and documentation was received from, and freely available for download at the author's PTLIB site 'http://dunkels.com/adam/pt/', available through a link on the downloads page at 'http://dunkels.com/adam/pt/download.html' or directly via 'http://dunkels.com/adam/download/pt-1.4.tar.gz'. This post consists of original text, for which I hereby give to you (with love!) under a full waiver of whatever copyright interest I may have, under the following terms: "copyheart ♥ 2014, shelleybutterfly, share with love!"; or, if you prefer, a fully non-restrictive, attribution-only license appropriate to the material (such as Apache 2.0 for software; or CC-BY license for text) so that you may use it as you see fit, so that it may best suit your needs. This post also contains source code, almost entirely created from the original source by removing explanatory material, reformatting, and paraphrasing the in-line documentation/comments, as well as a few modifications/additions by me (shelleybutterfly on the stackexchange network). Anything derivative of PTLIB for which I may have, legally, gained any copyright or other interest, I hereby cede all such interest back to and all copyright interest in the original work to the original copyright holder, as specified in the license from PTLIB, which follows this agreement. In any jurisdiction where it is not possible for the terms above to apply to you for whatever reason, then, for whatever interest I have in the material, I hereby offer it to you under any non-restrictive, attribution-only, license of your choosing; or, should this also not be possible, then I give permission to stack exchange inc to provide it to you under whatever terms the y determine to be acceptable in your jurisdiction. All source code from PTLIB, and that which is derivative of PTLIB, that is not covered under other terms detailed above hereby provided to 'stack exchange inc' and to you under the following agreement:
LICENSE AGREEMENT for "The Protothreads Library"
Copyright (c) 2004-2005, Swedish Institute of Computer Science. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the Institute nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS `AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Author: Adam Dunkels
答案 4 :(得分:2)
这里有一些非常好的答案,但在挖掘更长的答案之前,关于你的生日卡片例子的更多背景可能是一个很好的领导。
单个cpu似乎可以同时执行多项操作的方式是在任务之间快速切换,以及使用定时器,中断和独立硬件单元的协助,这些单元可以独立于cpu执行操作。 (请参阅@Zack的答案以获得一个很好的讨论和HW的入门名单)所以对于你的生日卡,cpu可能会讲一些音频硬件"播放这一块声音"然后去闪烁LED,然后返回并在第一部分完成播放之前加载下一个声音。在这种情况下,cpu可能需要1毫秒的时间来加载可能播放5毫秒实时的音频,让你在加载下一个声音之前有4毫秒的时间做其他事情。
数字时钟可能会发出一声PWM硬件,以某个频率输出到压电蜂鸣器,中断停止发出蜂鸣声,然后关闭并检查实时计数器以查看时间显示LED需要更新。当计时器触发中断时,代码会关闭PWM。
详细信息将根据芯片的硬件而有所不同,通过数据表可以了解给定微控制器可能具有的功能以及如何访问它。
答案 5 :(得分:1)
我对Freertos有很好的体验,尽管它使用了相当多的记忆。 Freertos为您提供真正的先发制人线程,如果您想要升级那些尘土飞扬的旧8051,那里有信号量和消息队列,优先级和各种各样的东西,并且它完全免费。我个人只与arduino端口合作,但它似乎是最受欢迎的免费rtosses之一。
我认为他们出售的书籍不是免费的,但是在他们的网站和arduino示例中有足够的信息可以解决这个问题。