PIC微控制器上的多任务


17

如今,多任务处理很重要。我想知道如何在微控制器和嵌入式编程中实现它。我正在设计一个基于PIC微控制器的系统。我已经使用C在MplabX IDE中设计了其固件,然后使用C#在Visual Studio中为其设计了一个应用程序。

由于我已经习惯在台式机上的C#编程中使用线程来实现并行任务,因此有没有办法在我的微控制器代码中做到这一点?MplabX IDE提供了功能,pthreads.h但它只是一个存根,没有实现。我知道有FreeRTOS支持,但是使用它会使您的代码更复杂。一些论坛说,中断也可以用作多任务处理,但我认为中断不等同于线程。

我正在设计一个系统,该系统将一些数据发送到UART,同时需要通过(有线)以太网将数据发送到网站。用户可以通过网站控制输出,但是输出会以2-3秒的延迟打开/关闭。这就是我面临的问题。对于微控制器中的多任务有什么解决方案吗?


线程只能在运行OS的处理器上使用,因为线程是进程的一部分,而进程仅在OS中使用。
TicTacToe 2015年

@Zola是的,你是对的。但是对于控制器该怎么办?
飞机


1
您能否解释为什么需要真正的多任务处理,而又无法基于循环任务方法或select()循环或类似方法合理地实现软件?
whatsisname 2015年

2
好了,正如我已经说过的,我正在向uart发送和接收数据,同时又向以太网发送和接收数据。除此之外,我还需要将数据与时间一起保存在SD卡中,因此可以使用DS1307 RTC,还可以使用EEPROM。到现在为止,我只有1个UART,但可能几天后我将要从3个UART模块发送和接收数据。网站还将接收来自远程安装的5个不同系统的数据。所有这些都必须是并行的,但不是并行的,而是要延迟几秒钟。!
飞机2015年

Answers:


20

多任务操作系统主要有两种:抢占式和协作式。两者都允许在系统中定义多个任务,不同之处在于任务切换的工作方式。当然,只有一个核心处理器,一次实际上只运行一个任务。

两种类型的多任务操作系统都需要为每个任务使用单独的堆栈。因此,这意味着两件事:首先,处理器允许将堆栈放置在RAM中的任何位置,因此具有指令来移动堆栈指针(SP)-即,不存在像低端那样的专用硬件堆栈PIC的。这省去了PIC10、12和16系列。

您几乎可以完全用C语言编写OS,但是SP随处移动的任务切换器必须处于汇编状态。在不同的时候,我都为PIC24,PIC32、8051和80x86编写了任务切换器。胆量都完全不同,具体取决于处理器的体系结构。

第二个要求是要有足够的RAM来提供多个堆栈。通常,一个堆栈至少需要几百个字节。但是,即使每个任务只有128个字节,八个堆栈也将需要1K字节的RAM,尽管您不必为每个任务分配相同大小的堆栈。请记住,您需要足够的堆栈来处理当前任务以及对其嵌套子例程的任何调用,而且还需要一个中断调用的堆栈空间,因为您永远不知道何时会发生。

有相当简单的方法来确定每个任务要使用多少堆栈。例如,您可以将所有堆栈初始化为特定值,例如0x55,然后运行系统一段时间,然后停止并检查内存。

您没有说要使用哪种PIC。大多数PIC24和PIC32将有足够的空间来运行多任务OS。PIC18(唯一在RAM中具有堆栈的8位PIC)的最大RAM大小为4K。因此,这还真不容易。

使用协作式多任务处理(两者中的较简单者),仅当任务“将其控制权”交还给OS时才执行任务切换。每当任务需要调用OS例程以执行它将等待的某些功能(例如I / O请求或计时器调用)时,就会发生这种情况。这使操作系统更容易切换堆栈,因为不必保存所有寄存器和状态信息,因此可以将SP切换到另一个任务(如果没有其他任务可以运行,则空闲堆栈是给定控制权)。如果当前任务不需要进行OS调用但已经运行了一段时间,则需要自动放弃控制以保持系统响应。

协作式多任务处理的问题是,如果任务永不放弃控制权,它将使系统瘫痪。只有它和碰巧得到控制的所有中断例程都可以运行,因此操作系统似乎会锁定。这是这些系统的“合作”方面。如果实现了仅在执行任务切换时复位的看门狗定时器,则有可能捕获这些错误的任务。

Windows 3.1和更早版本是协作操作系统,这部分是其性能不那么出色的原因。

抢占式多任务处理更难实现。在此,不需要手动放弃任务,而是可以为每个任务提供最大的运行时间(例如10毫秒),然后如果有一个任务,则将任务切换到下一个可运行任务。这要求任意停止一个任务,保存所有状态信息,然后将SP切换到另一个任务并启动它。这使任务切换器更加复杂,需要更多的堆栈,并使系统速度降低了一点。

对于协作式和抢占式多任务处理,中断可以随时发生,这将暂时抢占正在运行的任务。

正如supercat在评论中指出的那样,协作多任务处理的一个优点是共享资源更加容易(例如,诸如多通道ADC之类的硬件或诸如修改链表之类的软件)。有时,两个任务希望同时访问同一资源。通过抢占式调度,操作系统可能会使用资源在一项任务的中间切换任务。因此,必须使用来防止其他任务进入并访问同一资源。对于协作式多任务处理,这不是必需的,因为任务控制着何时将其自身释放回操作系统。


3
协作式多任务处理的一个优点是,在大多数情况下,不必使用锁来协调对资源的访问。确保任务在放弃控制时始终将资源保持在可共享状态就足够了。如果抢占式多任务处理在锁定另一个任务所需资源的同时可能被切换出去,则它会更加复杂。在某些情况下,第二个任务的终止时间可能会比在协作系统下更长,因为持有锁的任务将一直在系统的
专用位

1
...将全部资源用于完成(在抢先式系统上)需要锁定的操作,从而使受保护的对象可用于第二个任务。
2015年

1
尽管协作式多任务处理程序需要纪律,但在协作式多任务处理程序中,确保抢占计时要求有时比在抢先式多任务处理程序中容易。由于在一个任务开关之间只需要保持很少的锁,所以一个五任务循环任务开关系统要求任务在不屈服的情况下不超过10ms,并结合了一个小逻辑,即“如果任务X紧急,需要运行,然后再运行”,这将确保任务X发出信号后,不必再等待10毫秒以上才能开始运行。相比之下,如果某个任务需要锁定,则该任务X ...
超级猫

1
...将是需要的,但是在释放它之前先被抢占式切换器切换出,X可能无法做任何有用的事情,直到CPU调度程序开始执行第一个任务。除非调度程序包括识别和处理优先级反转的逻辑,否则它可能需要一段时间才能使第一个任务完成其业务并释放锁。这样的问题不是无法解决的,但是解决它们需要很多复杂性,而这在协作系统中是可以避免的。协作系统的工作原理很不错,除了一个陷阱:...

3
如果您连续编写代码,则不需要多个堆栈进行协作。本质上,您的代码按功能划分,void foo(void* context)控制器逻辑(内核)提取队列的一个指针和函数指针对,一次调用一个。该函数使用上下文存储其变量等,然后可以向队列添加提交延续。这些功能必须快速返回,以让其他任务在CPU中发挥作用。这是基于事件的方法,仅需要单个堆栈。
棘轮怪胎2015年

16

线程由操作系统提供。在嵌入式世界中,我们通常没有OS(“裸机”)。因此,这留下了以下选项:

  • 经典的主轮询循环。您的主要功能有一个while(1)执行任务1然后执行任务2 ...
  • 主循环+ ISR标志:您有一个ISR,它具有对时间要求严格的功能,然后通过标志变量向主循环发出警报,说明该任务需要服务。ISR可能在循环缓冲区中放置了一个新字符,然后在准备好时告诉主循环处理数据。
  • 所有ISR:此处的大部分逻辑是从ISR执行的。在具有多个优先级的现代控制器(如ARM)上。这可以提供功能强大的“类似于线程”的方案,但也可能使调试混乱,因此应仅将其保留用于关键的时序约束。
  • RTOS:RTOS内核(由计时器ISR促成)可以允许在多个执行线程之间进行切换。您提到了FreeRTOS。

我建议您使用适用于您的应用程序的上述最简单的方案。根据您的描述,我将让主循环生成数据包并将其放入循环缓冲区中。然后有一个基于UART ISR的驱动程序,该驱动程序将在发送完前一个字节时触发,直到发送缓冲区为止,然后等待更多缓冲区内容。以太网的类似方法。


3
这是一个非常有用的答案,因为它解决了问题的根源(如何在小型嵌入式系统上执行多任务,而不是将线程作为解决方案)。关于如何将其应用于原始问题的段落非常棒,也许包括每种情况的利弊。
戴维(David)

8

就像在任何单核处理器中一样,不可能进行真正的软件多任务处理。因此,您必须小心以一种方式在多个任务之间切换。不同的RTOS正在处理此问题。他们有一个调度程序,并基于系统的时间刻度它们将在不同的任务之间切换,从而为您提供多任务处理能力。

这样做涉及的概念(上下文保存和还原)非常复杂,因此手动执行此操作可能会很困难,并且会使您的代码更加复杂,并且因为您以前从未这样做过,所以会有错误。我的建议是像FreeRTOS一样使用经过测试的RTOS。

您提到中断提供了一定程度的多任务处理。这是真的。中断将在任何时候中断当前程序并在那里执行代码,这相当于两个任务系统,在该系统中,您有一个低优先级的任务和另一个高优先级的任务,该任务在调度程序的一个时间片内完成。

因此,您可以为循环计时器编写一个中断处理程序,该程序将通过UART发送一些数据包,然后让程序的其余部分执行几毫秒,然后发送接下来的几个字节。这样,您就获得了有限的多任务处理能力。但是您也会有一个相当长的中断,这可能是一件坏事。

在单核MCU上同时执行多个任务的唯一真实方法是使用DMA和外设,因为它们独立于内核工作(DMA和MCU共享同一总线,因此当它们工作时速度会慢一些)两者都处于活动状态)。因此,当DMA将字节改组到UART时,您的内核可以自由地将内容发送到以太网。


2
谢谢,DMA听起来很有趣。我一定会搜索的!
飞机

并非所有系列的PIC都有DMA。
马特·杨

1
我正在使用PIC32;)
飞机

6

其他答案已经描述了最常用的选项(主循环,ISR,RTOS)。这是另一个折衷方案:Protothreads。它基本上是线程的轻量级库,它使用主循环和一些C宏来“模拟” RTOS。当然,它不是完整的操作系统,但是对于“简单”线程来说可能很有用。


从哪里可以下载Windows的源代码?我认为它仅适用于Linux。
飞机

@CZAbhinav它应该独立于操作系统,您可以在这里获取最新下载。
erebos 2015年

我现在在Windows中并使用MplabX,我认为它在这里没有用。还是谢谢你。!
飞机

尚未听说过原型线程,听起来像是一种有趣的技术。
阿森纳

@CZAbhinav你在说什么?它是C代码,与您的操作系统无关。
马特·杨

3

我的最小时间分段RTOS的基本设计在几个微型系列中并没有太大变化。从本质上讲,这是驱动状态机的计时器中断。中断服务程序是OS内核,而主循环中的switch语句是用户任务。设备驱动程序是I / O中断的中断服务程序。

基本结构如下:

unsigned char tick;

void interrupt HANDLER(void) {
    device_driver_A();
    device_driver_B();
    if(T0IF)
    {
        TMR0 = TICK_1MS;
        T0IF = 0;   // reset timer interrupt
        tick ++;
    }
}

void main(void)
{
    init();

    while (1) {
        // periodic tasks:
        if (tick % 10 == 0) { // roughly every 10 ms
            task_A();
            task_B();    
        }
        if (tick % 55 == 0) { // roughly every 55 ms
            task_C();
            task_D();    
        }

        // tasks that need to run every loop:
        task_E();
        task_F();
    }
}

这基本上是一个协作式多任务处理系统。任务被编写为永远不会进入无限循环,但是我们不在乎,因为任务在事件循环内运行,因此无限循环是隐式的。这与面向事件/非阻塞语言(如javascript或go)的编程风格类似。

您可以在我的RC发射器软件中看到这种架构风格的示例(是的,我实际上使用它来飞行RC飞机,因此,对于防止我坠毁飞机并可能杀死人来说,对安全性至关重要):https : //github.com / slebetman / pic-txmod。它基本上有3个任务-2个作为有状态设备驱动程序实现的实时任务(请参阅ppmio资料)和1个实现混合逻辑的后台任务。因此,基本上它与您的Web服务器类似,因为它具有2个I / O线程。


1
我不会真正称其为“协作多任务”,因为这与必须执行多项操作的任何其他微控制器程序并没有实质性区别。
whatsisname 2015年

2

尽管我理解该问题专门询问了嵌入式RTOS的使用,但我想到的是,提出的更广泛的问题是“如何在嵌入式平台上实现多任务处理”。

我强烈建议您至少暂时不要使用嵌入式RTOS。我建议这样做,因为我认为至关重要的是,首先要通过由简单的任务调度程序和状态机组成的极其简单的编程技术来学习如何实现任务“并发”。

为了非常简要地解释这个概念,需要完成的每个工作模块(即每个“任务”)都有一个特定的功能,必须定期调用该功能(“选中”),以使该模块完成某些工作。模块保留其自己的当前状态。然后,您有一个主无限循环(调度程序),该循环调用模块函数。

粗插图:

for(;;)
{
    main_lcd_ui_tick();
    networking_tick();
}


...

// In your LCD UI module:
void main_lcd_ui_tick(void)
{
    check_for_key_presses();
    update_lcd();
}

...

// In your networking module:
void networking_tick(void)
{
    //'Tick' the TCP/IP library. In this example, I'm periodically
    //calling the main function for Keil's TCP/IP library.
    main_TcpNet();
}

像这样的单线程编程结构使您可以从主调度程序循环中定期调用主状态机功能,这在嵌入式编程中无处不在,这就是为什么我强烈鼓励OP首先熟悉并熟悉OP的原因,然后再直接使用它。 RTOS任务/线程。

我在一种嵌入式设备上工作,该设备具有硬件LCD界面,内部Web服务器,电子邮件客户端,DDNS客户端,VOIP和许多其他功能。尽管我们确实使用了RTOS(Keil RTX),但使用的单个线程(任务)的数量非常少,并且如上所述,大多数“多任务”都可以实现。

举例说明这个概念的图书馆:

  1. Keil网络库。整个TCP / IP堆栈可以单线程运行。您会定期调用main_TcpNet(),它会迭代TCP / IP堆栈和从库(例如Web服务器)中编译的任何其他网络选项。请参阅http://www.keil.com/support/man/docs/rlarm/rlarm_main_tcpnet.htm。诚然,在某些情况下(可能不在此答案的范围内),您确实达到了开始使用线程或变得有用的地步(特别是在使用阻塞的BSD套接字的情况下)。(更多说明:新的V5 MDK-ARM实际上产生了一个专用的以太网线程-但我只是想提供一个例子。)

  2. Linphone VOIP库。linphone库本身是单线程的。您iterate()有足够的时间间隔调用该函数。参见http://www.linphone.org/docs/liblinphone-javadoc/org/linphone/core/LinphoneCore.html#iterate()。(这是一个不好的例子,因为我在嵌入式Linux平台上使用了它,而linphone的依赖库无疑会产生线程,但这再次说明了这一点。)

回到OP概述的特定问题,问题似乎是UART通信必须与某些网络(通过TCP / IP传输数据包)同时进行。我不知道您实际使用的是哪个网络库,但是我认为它具有需要经常调用的主要功能。您将需要编写处理UART数据发送/接收的代码,以类似的方式进行结构化,作为可以通过定期调用主函数进行迭代的状态机。


2
感谢您的精彩解释,我使用的是微芯片提供的TCP / IP库,它是非常庞大的复杂代码。我设法将其分解为多个部分,并根据我的要求使其可用。我一定会尝试您的方法之一!
飞机

玩得开心:)在许多情况下,使用RTOS绝对可以让生活变得更轻松。在我看来,使用线程(任务)从某种意义上讲简化了编程工作,因为您可以避免将任务分解为状态机。相反,您只需像在C#程序中一样编写任务代码,就可以像创建唯一的任务代码一样创建任务代码。探索这两种方法都是必不可少的,并且随着您进行更多的嵌入式编程,您开始了解哪种方法在每种情况下都是最佳的。
Trevor Page

我也更喜欢使用线程选项。:)
飞机
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.