关于微控制器中的多任务的一些知识

有关多任务的一些知识


日复一日(或视情况而定)从事微控制器编程的每个人迟早都会面临这样一个问题:我应该使用多任务操作系统吗? 网络上有很多这样的设备,并且很多都是免费的(或几乎免费的)。 只要选择。


当您遇到一个项目,其中微控制器必须同时执行几种不同的操作时,也会产生类似的疑问。 他们中的一些人彼此之间没有联系,而其他人则不能没有彼此。 另外,两者可能太多。 什么是“太多”取决于谁将进行评估或由谁来进行开发。 好吧,如果是同一个人。


相反,这不是数量问题,而是任务在执行速度或其他一些要求方面的质的差异。 例如,当项目需要定期监视电源电压(是否丢失)时,可能经常会想到这种想法,经常读取并保存输入量的值(它们不会休息),偶尔监视温度并控制风扇(没有呼吸的余地),检查与您信任的人一起观看(对您有​​利),与操作员保持联系(尽量不要激怒他),检查程序永久记忆的校验和是否有痴呆症(打开时,每周一次或每天早上)。


依靠单个后台任务和计时器中断,可以非常有意义且成功地编程此类异构任务。 在这些中断的处理程序中,每次执行下一个任务的“部分”之一。 根据重要性,紧迫性或类似的考虑,这些挑战通常会在某些任务中重复出现,而在其他任务中很少重复出现。 但是,我们必须确保每个任务在短时间内完成一部分工作,然后为下一部分工作做准备,依此类推。 如果您习惯了这种方法,它似乎并不复杂。 当您要构建项目时,会带来不便。 或者,例如,突然转移到另一个。 应该注意的是,第二个通常更困难,并且没有任何伪多次任务。


但是,如果您将现成的操作系统用于微控制器该怎么办? 当然,很多人都这样做。 这是一个不错的选择。 但是,直到现在,这些思路的作者一直被这样的想法所束缚,并且一直认为,必须花很多时间,从我们设法获得的内容中进行选择,并仅使用真正需要的内容,才能理解这一点。 注意,仔细研究别人的代码! 不能肯定的是,在六个月内将不必重复此操作,因为它将被遗忘。


换句话说,如果在这里存放和使用自行车,为什么还需要一整套工具和固定装置?


因此,有一种愿望是仅对Cortex-M4进行简单的“切换”任务(当然,甚至对于M3和M7)。 但是,古老的,不费劲的愿望并没有消失。


因此,我们做的最简单。 少数任务平均分配执行时间。 如下图1所示,有四个任务可以做到这一点。 让主数为零,因为很难想象另一个。



通过这种方式,可以确保他们获得自己的时间段或时间跨度(刻度),并且无需了解其他任务的存在。 恰好在3个滴答声之后的每个任务将再次有机会做某事。


但是,另一方面,如果需要任何任务来等待外部事件(例如,按下按钮),则会愚蠢地花费微控制器的宝贵时间。 我们不同意这一点。 还有我们的蟾蜍(良心)-也是。 必须做些事情。


然后,让任务(如果到目前为止没有任何事可做),将剩余的时间留给the子留给其战友,这些战友很可能会全力以赴。


换句话说,共享是必要的。 让任务2做到这一点,如图2所示。



如果您仍然需要等待,为什么为什么不允许后台任务主体放弃其余时间呢? 让我们允许它。 如图3所示。



并且,如果您知道某些任务将很快不再需要您再次检查某些内容或只是工作? 她可以让自己多睡一会,而会浪费时间并站稳脚跟。 不是命令,它需要修复。 让任务3错过一分(或一千)时间。 如图4所示。



好了,正如我们所看到的,我们概述了任务或类似任务的公平共存。 现在,我们必须使我们的各个任务按规定执行。 而且,如果我们尝试珍惜时间,那么就值得记住一种低级语言(我不怕单词汇编器),而不是完全信任任何高级或高级语言的编译器。 的确,在我们内心深处,我们坚决反对一切依赖。 此外,我们不需要任何汇编程序,而仅需要Cortex-M4的事实,就简化了我们的生活。


对于堆栈,我们选择一个将要填充的RAM的公共区域,即朝着减少内存地址的方向。 怎么了 只是因为它没有不同的作用。 我们将根据声明的最大任务数将精神上的重要区域划分为相等的部分。 图5显示了这四个任务。



接下来,我们选择存储每个任务的堆栈指针副本的位置。 现在,通过中断作为系统计时器的计时器,我们将当前任务的所有寄存器保存在其堆栈区域中(SP寄存器现在指向那里),然后将其堆栈指针保存在一个特殊位置(我们保存其值),得到下一个任务的堆栈指针(从我们的特殊位置向寄存器SP中写入一个新值,并恢复其所有寄存器。 现在,下一个任务的SP寄存器将指示其副本。 好吧,我们当然退出中断。 此外,列表中下一个任务的整个上下文显示在寄存器中。


也许多余的是,队列中task3之后的下一个将是main。 当然,要记住Cortex-M4已经有一个SysTick定时器和一个特殊的中断,这并不是多余的,许多微控制器制造商都知道这一点。 我们将按预期使用它和此中断。


为了启动该系统计时器,并进行所有必要的准备和检查,必须使用用于此目的的步骤。


U8 main_start_task_switcher(void); 

如果所有检查都通过,则此例程返回0;如果出现问题,则返回错误代码。 基本上检查堆栈是否正确对齐以及是否有足够的空间,以及我们所有的特殊位置都填充有初始值。 简而言之,无聊。


如果有人想看节目的文本,那么在叙述结束时,他将可以轻松地做到这一点,例如通过个人邮件。


是的,我完全忘记了我们生命中第一次从存储中取出下一个任务的寄存器时,有必要使它们获得有意义的原始值。 而且,由于她将从堆栈中取出它们,因此您需要提前将它们放在此处并移动堆栈指针,以便于拿取。 为此,我们需要一个程序


  U8 task_run_and_return_task_number(U32 taskAddress); 

对于此子例程,我们报告要运行的任务开始处的32位地址。 她(子例程)告诉我们任务的编号,该编号在特殊的通用表中显示,如果表中没有空间,则返回0。 然后,我们可以运行另一个任务,然后运行另一个任务,依此类推,即使这三个任务都是我们永无止境的主要任务的补充。 她将永远不会给任何人以零号。


关于优先事项的几句话。 最主要的优先事项是而且现在不会使读者负担过多不必要的细节。


但要认真地,我们必须记住,毕竟串行端口,几个SPI连接,模数转换器,另一个计时器都存在中断。 如果我们在某种中断的处理程序中要切换到另一个任务(切换上下文),将会发生什么。 毕竟,这将不是一项合法的任务,而是对该程序的暂时性影响。 我们将把这种奇怪的环境作为某种任务。 会有一个混乱:衣领没有系紧,帽子不合身。 停止,不,这是另一个故事。


在我们的情况下,这是完全不允许的。 我们一定不能允许我们在处理计划外中断期间切换上下文。 这是优先事项。 我们只需要稍等片刻,只有当这种空前的胆怯结束时,才能冷静地切换到另一项任务。 简而言之,我们的任务切换中断的优先级应该弱于所使用的任何其他中断的优先级。 顺便说一下,这也是在我们的启动过程中完成的,并且已经安装了它,这是所有可能中最不优先的。


我不想说话,但我不得不。 我们的处理器具有两种操作模式:特权和非特权。 还有两个寄存器用于堆栈指针:
SP和SP的主要过程。 因此,我们不会交换琐事,只会使用特权模式和主堆栈指针。 而且,所有这些已经在控制器启动时给出。 因此,我们不会让我们的生活变得复杂。


值得回顾的是,每一项任务当然都希望能够将所有事情都付诸东流以及如何放松。 这可能在工作日的任何时候发生,即在我们的滴答中发生。 在这种情况下,Cortex-M4提供了特殊的汇编器命令SVC,我们将根据情况进行调整。 它导致中断,这将导致我们达到目标。 我们不仅允许任务在午餐后离开工作场所,而且明天也不来。 为什么,放假后再来。 如果有必要,则在维修完成或根本不维修时让它来。 为此,任务本身会导致一个过程。


  void release_me_and_set_sleep_period(U32 ticks); 

该例程仅需要指示计划休息多少个滴答。 如果为0,则您只能休息当前刻度的其余部分。 如果为0xFFFFFFFF,则任务将“休眠”,直到有人醒来。 所有其他数字表示任务在睡眠状态下的滴答数。


为了使其他人能够从侧面醒来或使他入睡,我必须添加此类程序。


  void task_wake_up_action(U8 taskNumber); void set_task_sleep_period(U8 taskNumber, U32 ticks); 

而且,以防万一,即使是这样的子例程。


  void task_remove_action(U8 taskNumber); 

粗略地说,她从员工名单中删除一项任务。 老实说,我还不知道为什么写。 突然派上用场了吗?


现在该展示一个任务被另一个任务(即开关本身)替代的地方的样子。


为了以防万一,让我们回想一下,某些寄存器在进入中断时会自动保存在堆栈中,而无需我们的参与(这是Cortex-M4的习惯)。 因此,我们只需要保存其余部分。 可以在下面看到。 请勿惊慌,这就是IAR Embedded Workbench概述的Cortex-M4汇编程序指令(M3,M7)。


那些还没有遇到汇编说明的人,只要相信我,他们真的看起来像那样。 这些是构成ARM Cortex-M4下任何程序的分子。


 SysTick_Handler STMDB SP!,{R4-R11} ;   LDR R0,=timersTable ;    LDR R1,=stacksTable ;    LDR R2,[R0] ;R2   ()  STR SP,[R1,R2,LSL #2] ;   SP (R2 * 4) __st_next_check ADD R2,R2,#1 ;   CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT  BLO __st_no_border_yet ;   MOV R2,#0 ;    (main) LDR R3,[R1] ; main SP MOV SP,R3 B __st_timer_ok __st_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ;    (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ;  SP      CMP R3,#0 ; =0     BEQ __st_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ;  suspend timer CBZ R3,__st_timer_ok ; 0    ,   ; CMP R3,#0xFFFFFFFF ; ,   BEQ __st_next_check SUB R3,R3,#1 ;  1 STR R3,[R0,R2,LSL #2] ;  suspend timer B __st_next_check __st_timer_ok STR R2,[R0] ;     LDMIA SP!,{R4-R11} ;  R4-R11 BX LR 

当任务返回滴答的其余部分时,处理任务本身排序的中断看起来很相似。 唯一的区别是您仍然必须为以后再入睡(或彻底入睡)而烦恼。 有一个微妙之处。 必须执行两个操作,在睡眠定时器中写入所需的数字,并使SVC中断。 这两个动作不是原子发生的(即不是同时发生),这让我有些担心。 想象一毫秒,我们只是使计时器启动,那时是时候执行另一项任务了。 另一个开始花掉她的滴答声,而我们的任务是按预期的那样睡下一个滴答声(因为它的计时器不为零)。 然后,当它的时间到了时,我们的任务将收到它的滴答声并立即将其交给中断SVC,因为有两项动作尚需完成。 我认为不会发生任何可怕的事情,但是沉积物会保留下来。 因此,我们将这样做。 未来的睡眠计时器已放置在初步位置。 它由SVC的中断例程本身从那里获取。 达到了原子性。 如下所示。


 SVC_Handler LDR R0,__sysTickAddr ; SysTick  MOV R1,#6 ;   CSR ,   STR R1,[R0] ;Stop SysTimer MOV R1,#7 ; ,   STR R1,[R0] ;Start SysTimer ; STMDB SP!,{R4-R11} ;   LDR R0,=timersTable ;    LDR R1,=stacksTable ;    LDR R2,[R0] ;R2   ()  STR SP,[R1,R2,LSL #2] ;   SP (R2 * 4) LDR R3,=tmpTimersTable ;   tmpTimers LDR R3,[R3,R2,LSL #2] ;tmpTimer    STR R3,[R0,R2,LSL #2] ; timer  __svc_next_check ADD R2,R2,#1 ;   CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT  BLO __svc_no_border_yet ;   MOV R2,#0 ;    (main) LDR R3,[R1] ; main SP MOV SP,R3 B __svc_timer_ok __svc_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ;Restore SP does not work (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ;  SP      CMP R3,#0 ; =0     BEQ __svc_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ;  suspend timer CBZ R3,__svc_timer_ok ; 0    ,   B __svc_next_check __svc_timer_ok STR R2,[R0] ;     LDMIA SP!,{R4-R11} ; R4-R11 BX LR 

应当记得,所有这些子例程和中断处理程序都引用某个数据区域,该数据区域看起来是由作者执行的,如图7所示。


  DATA SECTION .taskSwitcher:CODE:ROOT(2) __topStack DCD sfe(CSTACK) __botStack DCD sfb(CSTACK) __dimStack DCD sizeof(CSTACK) __sysAIRCRaddr DCD 0xE000ED0C __sysTickAddr DCD 0xE000E010 __sysSHPRaddr DCD 0xE000ED18 __sysTickReload DCD RELOAD ;******************************************************************************* ; Task table for concurrent tasks (main is number 0). ;******************************************************************************* SECTION TABLE:DATA:ROOT(2) DS32 1 ;stack shift due to FPU mainCopyCONTROL DS32 1 ;Needed to determine if FPU is used mainPSRvalue DS32 1 ;Copy from main ;******************************************************************************* 

为了确保上述所有内容都是常识,作者不得不在IAR嵌入式工作台下编写一个小项目,在那里他设法详细检查和触摸所有内容。 一切都在STM32F303VCT6控制器(ARM Cortex-M4)上进行了测试。 或者更确切地说,使用STM32F3DISCOVERY板。 有足够的LED指示灯,分别用其自己的LED指示灯为每个任务提供充足的闪光。


我发现还有更多有用的功能。 例如,一个子例程在每个堆栈区域中计数不受影响的单词的数量,即保持等于零。 这在调试时很有用,当您需要检查用一个任务或另一个任务填充堆栈是否太接近限制级别时。


  U32 get_task_stack_empty_space(U8 taskNum); 

我想再提一个功能。 这是任务本身在列表中找到您的电话号码的机会。 您可以稍后再告诉别人。


 ;******************************************************************************* ; Example: U8 get_my_number(void); ;     (). ..    . ;******************************************************************************* get_my_number LDR R0,=timersTable ;    (currentTaskNumber) LDR R0,[R0] ;  BX LR ;============================================================== 

目前可能仅此而已。

Source: https://habr.com/ru/post/zh-CN454874/


All Articles