我自己的原文哦~              https://blog.51cto.com/whaosoft/13976141

一、单片机开发中的RTOS

很多STM32单片机初学者都是从裸机开始的,裸机确实也能开发出好的产品。但是,作为一个嵌入式软件工程师,况且用的并不是51那种低端单片机,如果只会用裸机开发产品,那肯定是不够的。

    要从裸机的思维转变到RTOS(Real Time Operating System)的思维,其实需要一个过程,而且开始的一段时间会很痛苦。但过一段时间理解了一些内容,能写一些Demo之后,你会发现其实RTOS也不难,相x关demo推荐:使用STM32CubeMx工具,写FreeRTOS的demo程序。

    现在FreeRTOS在CubeMX工具中可以直接配置并使用,相当方便。

为什么需要RTOS

    为什么我们需要RTOS?就像最开始学C编程时,老师告诉我们,指针很重要,那时你肯定有一个大的疑问,指针到底有什么好?

    心里一直犯嘀咕着:不用指针不一样把程序编出来了? 现在想想看C语言没了指针,是不是“寸步难行”呢。

    回到正题,我们到底为什么需要RTOS?相x关文章推荐:详解选择RTOS的要点。

     一般的简单的嵌入式设备的编程思路是下面这样的:

    这是最常见的一种思路,对于简单的系统当然是够用了,但这样的系统实时性很差。

    比如“事务1”如果是一个用户输入的检测,当用户输入时,如果程序正在处理事务1下面的那些事务,那么这次用户输入将失效,用户的体验是“这个按键不灵敏,这个机器很慢”,而我们如果把事务放到中断里去处理,虽然改善了实时性但会导致另外一个问题,有可能会引发中断丢失,这个后果有时候比“慢一点”更加严重和恶劣!

    又比如事务2是一个只需要1s钟处理一次的任务,那么显然事务2会白白浪费CPU的时间。

改进思路

    看到上面裸机开发的局限了吗?

    这时,我们可能需要改进我们的编程思路,一般我们会尝试采用“时间片”的方式。这时候编程会变成下面的方式:

    可以看到,这种改进后的思路,使得事务的执行时间得到控制,事务只在自己的时间片到来后,才会去执行。但这种方式仍然不能彻底解决“实时性”的问题,因为某个事务的时间片到来后,也不能立即就执行,必须等到当前事务的时间片用完,并且后面的事务时间片没到来,才有机会获得“执行时间”。       

    这时候我们需要继续改进思路,为了使得某个事务的时间片到来后能立即执行,我们需要在时钟中断里判断完时间片后,改变程序的返回位置,让程序不返回到刚刚被打断的位置,而从最新获得了时间片的事务处开始执行,这样就彻底解决了事务的实时问题。    

    我们在这个思路上,进行改进,我们需要在每次进入时钟中断前,保存CPU的当前状态和当前事务用到的一些数据,然后我们进入时钟中断进行时间片处理,若发现有新的更紧急的事务的时间片到来了,则我们改变中断的返回的地址,并在CPU中恢复这个更紧急的事务的现场,然后返回中断开始执行这个更紧急的事务。

使用RTOS的好处

    上面那段话,对于初学者来说,可能有些不好理解。

    事实上,这是因为要实现这个过程是有些复杂和麻烦的,这时候我们就需要找一个操作系统(OS)帮我们做这些事了,如果你能自己用代码实现这个过程,事实上你就在自己写操作系统了。

    其实从这里也可也看出,操作系统的原理其实并不那么神秘,只是一些细节你很难做好。我们常见的RTOS基本都是这样的一个操作系统,它能帮你完成这些事情,而且是很优雅的帮你完成!

    事实上,RTOS的用处远不止帮你完成这个“事务时间片的处理”,它还能帮你处理各种超时,进行内存管理,完成任务间的通信等。

    有了RTOS,程序的层次也更加清晰,给系统添加功能也更方便,这一切在大型项目中越发的明显!​

1 FreeRTOS

    由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。

    相对于C/OS-II、 embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行,其最新版本为6.0版。

    作为一个轻量级的操作系统,FreeRTOS提供的功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能等,可基本满足较小系统的需要。

    FreeRTOS内核支持优先级调度算法,每个任务可根据重要程度的不同被赋予一定的优先级,CPU总是让处于就绪态的、优先级最高的任务先运行。

    FreeRT0S内核同时支持轮换调度算法,系统允许不同的任务使用相同的优先级,在没有更高优先级任务就绪的情况下,同一优先级的任务共享CPU的使用时间。

    FreeRTOS的不足:相对于常见的μC/OS—II操作系统,FreeRTOS操作系统既有优点也存在不足。其不足之处, 一方面体现在系统的服务功能上,如FreeRTOS只提供了消息队列和信号量的实现,无法以后进先出的顺序向消息队列发送消息;另一方 面,FreeRTOS只是一个操作系统内核,需外扩第三方的GUI(图形用户界面)、TCP/IP协议栈、FS(文件系统)等才能实现一个较复杂的系统, 不像μC/OS-II可以和μC/GUI、μC/FS、μC/TCP-IP等无缝结合。

    在STM32CubeIDE中直接可以配置FreeRTOS,如上图。STM32使用FreeRTOS的相x关文章,请移步此处:使用STM32CubeMx工具,写FreeRTOS的demo程序。

    FreeRTOS资料多、生态活跃,在Cube中通过配置界面,三两下上手这款操作系统,推荐拿来入门。

    基于不同的需求,下文再介绍下其他RTOS。

2 μClinux

    μClinux是一种优秀的嵌入式Linux版本,其全称为micro-control Linux,从字面意思看是指微控制Linux。

    同标准的Linux相比,μClinux的内核非常小,但是它仍然继承了Linux操作系统的主要特性,包括良好的稳定性和移植性、强大的网络功能、出色的文件系统支持、标准丰富的API,以及TCP/IP网络协议等。因为没有MMU内存管理单元,所以其多任务的实现需要一定技巧。

    μClinux在结构上继承了标准Linux的多任务实现方式,分为实时进程和普通进程,分别采用先来先服务和时间片轮转调度,仅针对中低档嵌入式CPU特点进行改良,且不支持内核抢占,实时性一般。

    综上可知,μClinux最大特点在于针对无MMU处理器设计,这对于没有MMU功能的STM32F103来说是合适的,但移植此系统需要至少512KB的RAM空间,1MB的ROM/FLASH空间,而STM32F103拥有256K的FLASH,需要外接存储器,这就增加了硬件设计的成本。

    μClinux结构复杂,移植相对困难,内核也较大,其实时性也差一些,若开发的嵌入式产品注重文件系统和与网络应用则μClinux是一个不错的选择。

图片

3 μC/OS-II

    μC/OS-II是在μC/OS的基础上发展起来的,是用C语言编写的一个结构小巧、抢占式的多任务实时内核。μC/OS-II能管理64个任务,并提供任务调度与管理、内存管理、任务间同步与通信、时间管理和中断服务等功能,具有执行效率高、占用空间小、实时性能优良和扩展性强等特点。

    在文件系统的支持方面,由于μC/OS-II是面向中小型嵌入式系统的,即使包含全部功能,编译后内核也不到10 KB,所以系统本身并没有提供对文件系统的支持。但是μC/OS-II具有良好的扩展性能,如果需要也可自行加入文件系统的内容。

    在对硬件的支持上,μC/OS-II能够支持当前流行的大部分CPU,μC/OS-II由于本身内核就很小,经过裁剪后的代码最小可以为2KB,所需的最小数据RAM空间为4 KB,μC/OS-II的移植相对比较简单,只需要修改与处理器相关的代码就可以。

    综上可知,μC/OS-II是一个结构简单、功能完备和实时性很强的嵌入式操作系统内核,针对于没有MMU功能的CPU,它是非常合适的。它需要很少的内核代码空间和数据存储空间,拥有良好的实时性,良好的可扩展性能,并且是开源的,网上拥有很多的资料和实例,所以很适合向STM32F103这款CPU上移植。

图片

4 eCos

    eCos(embedded Configurable operating system),即嵌入式可配置操作系统。它是一个源代码开放的可配置、可移植、面向深度嵌入式应用的实时操作系统。

    最大特点是配置灵活,采用模块化设计,核心部分由小同的组件构成,包括内核、C语言库和底层运行包等。

    每个组件可提供大量的配置选项(实时内核也可作为可选配置),使用eCos提供的配置工具可以很方便地配置,并通过不同的配置使得eCos能够满足不同的嵌入式应用要求。

    eCos操作系统的可配置性非常强大,用户可以自己加入所需的文件系统。eCos操作系统同样支持当前流行的大部分嵌入式CPU,eCos操作系统可以在16位、32位和64位等不同体系结构之间移植。

    eCos由于本身内核就很小,经过裁剪后的代码最小可以为10 KB,所需的最小数据RAM空间为10 KB。

    在系统移植方面 eCos操作系统的可移植性很好,要比μC/OS-II和μClinux容易。

    综上所述,eCos最大特点是配置灵活,并且支持无MMU的CPU的移植,开源且具有很好的移植性,也比较合适于移植到STM32平台的CPU上。但eCOS的应用还不是太广泛,还没有像μC/OS-II那样普遍,并且资料也没有μC/OS-II多。eCos适合用于一些商业级或工业级对成本敏感的嵌入式系统,例如消费电子领域中的一些应用。​

5 mbed OS

    开源嵌入式操作系统,ARM公司将mbed OS免费提供给所有厂商使用,mbed提供了一个相对更加系统和更加全面的智能硬件开发环境。

    主要功能:

    提供用于开发物联网设备的通用操作系统基础,以解决嵌入式设计的碎片化问题。支持所有重要的连接性与设备管理开放标准,以实现面向未来的设计。使安全可升级的边缘设备支持新增处理能力与功能。通过自动电源管理解决复杂的能耗问题。

    主要特点:

    开发速度快,功能强大,安全性高,为了量产化而设计,可离线开发,也可以在网页上编辑。

图片

6 RTX

    是ARM公司的一款嵌入式实时操作系统,使用标准的C结构编写,运用RealView编译器进行编译。不仅仅是一个实时内核,还具备丰富的中间层组件,不但免费,而且代码也是开放的。

    主要功能:

    开始和停止任务(进程),除此之外还支持进程通信,例如任务的同步、共享资源(外设或内存)的管理、任务之间消息的传递。开发者可以使用基本函数去开启实时运行器,去开始和终结任务,以及去传递任务间的控制(轮转调度)。开发者可以赋予任务优先级。

    主要特点:

    支持时间片,抢占式和合作式调度。不限制数量的任务,每个任务都具有254的优先级。不限制数量的信号量,互斥信号量,消息邮箱和软定时器。支持多线程和线程安全操作。使用MDK基于对话框的配置向导,可以很方便的完成MDK的配置。​

7 都江堰

    都江堰操作系统,简称djyos,得名于一个伟大的水利工程:都江堰。

    与传统操作系统不同,djyos不是以线程而是以事件为调度核心,这种调度算法使程序员摆脱模拟计算机执行过程编写程序的思维方式,而是按人类认知世界的方式编写应用程序,就如同在嵌入式编程中引入了VC似的。

    djyos的调度算法使程序员可以摆脱线程和进程的束缚,djyos没有有关线程的api,一个完全不懂线程知识的程序员也可以顺利地在djyos下编写应用程序。

    djyos 操作系统是以事件为核心进行调度的,这种调度策略使程序员可以按人类认知事物的习惯而不是计算机的习惯来编程。

图片

8 RT-Thread

    嵌入式操作系统RTOS介绍,RT-Thread是一个集实时操作系统(RTOS)内核、中间件组件和开发者社区于一体的技术平台,由熊谱翔先生带领并集合开源社区力量开发而成,RT-Thread也是一个组件完整丰富、高度可伸缩、简易开发、超低功耗、高安全性的物联网操作系统。

    RT-Thread具备一个IoT OS平台所需的所有关键组件,例如GUI、网络协议栈、安全传输、低功耗组件等等。经过11年的累积发展,RT-Thread已经拥有一个国内最大的嵌入式开源社区,同时被广泛应用于能源、车载、医疗、消费电子等多个行业,累积装机量超过两千万台,成为国人自主开发、国内最成熟稳定和装机量最大的开源RTOS。

    国内最有可能成为Top 1,优势在于丰富的组件,中立立场!赶上了时机,得到诸多芯片厂商的支持,也挺受开发者喜欢的。缺点在于本身的教程文档和freertos等之类的比还是很弱。

二、详解选择RTOS的要点

对于许多嵌入式项目来说,在采用非实时操作系统(non-RTOS)的任何场合,也都可采用RTOS。但是,要找到一款具有完全相同应用编程接口(API)的匹配RTOS就相当困难了。因此,许多传统的操作系统(OS)在其内嵌入了一个RTOS。例如,Lynux-Works LynxOS和Bluecat Linux共享一个Linux API。LynxOS是一款硬RTOS,而Bluecat是Linux的一个衍生产品。

    Linux继续在努力改善其实时性能,但其最长中断时延仍无法满足对RTOS来说至关重要的硬(hard)实时要求。这些问题最后都会归结为服务质量(QoS)。像RTLinux Free这样的平台补充了Linux,因为它们可提供硬实时级别的QoS。

    要指出的很重要一点是:这类补充常常是在原始OS上集成一个RTOS编程环境。与传统台式或服务器OS相比,RTOS通常要小很多。RTOS常常针对更小和资源有限的MCU。例如,CMX的CMX-RTX和CMX-Tiny+可运行在8位MCU到64位处理器上。​

RTOS核心:调度和分割

    大多数程序员不熟悉RTOS的限制和要求。大多数人通常因其性能选择RTOS。大多数RTOS产品代码少和速度快,现在RTOS还提升了一致性。RTOS除能很快完成任务外,还能保证很好地完成任务。

    在许多应用中,一个迟到的结果可以是灾难性的。因此,人们宁愿在一个要求的时限内获得较差的结果。这些应用通常被称为硬实时系统。硬实时不是指系统响应有多快或多快一个系统能响应,而是指系统能多可靠地满足特定的要求。

    一个硬实时系统可能有一个一分钟的固定周期时间,它要求的响应时间为一秒。理论上,这样的要求几乎所有的操作系统都能实现。但事实并非总是如此,正如任何一个人都能证明等待台式计算机应用在一分钟之内做出响应需要等多久。

    硬实时系统通常具有更短的周期时间和更严苛的响应要求。更快的处理器速度总是有帮助的,多内核平台也能改善响应速度。对开发人员来说,窍门在于把系统需求与硬件和软件匹配起来,然后才是RTOS在嵌入式应用中的重要性。

    一个RTOS可以实现一系列调度策略,但应用经常会制约一个程序员的选择(见表)。非优先式调度(non-preemptive scheduling)的实现虽不重要,但在一些应用中很有用。另一方面,任务内的非优先式调度可在优先式系统的顶部实现。

    不应该轻忽非优先式调度,特别在新型多内核处理器出现以后。这里,硬件可被调整到处理一个基于事件的操作,其中线程将等待外部事件的发生。对处理多线程的单核处理器来说,该方法一般不适用。但对有许多内核的多核系统说,典型情况是为一个外设指定一个核。所以,在等待事件发生期间,使该核空闲起来是有意义的。

    其结果是,优先式、中断驱动的RTOS架构占据了业已部署的大部分平台。虽然借助硬件手段(多个寄存器组合、硬件调度、任务切换、以及分层中断优先级系统等)可显著缩短中断时延,但该时延永远是一个问题。

    优先式处理会带来若干问题。它们大多是与时序关联的,如竞争条件、死循环、空耗等待和优先级转换,它们发生在低优先级任务A拥有更高优先级任务B的同步资源,而优先级比A高的任务C正在运行。

    如果没有像优先级置顶(priority ceilings)这样的特性,任务C就可以阻止任务A和任务C运行。优先级置顶特性可以把任务A的优先级改变成与任务C的优先级一样,从而允许任务A运行并最终释放任务C所需的资源。至此,任务A的优先级复原,任务C就可以继续运行。

    程序员必须解决的其它与时序相关的问题通常是难以定位和纠正的缺陷源。在定位这些缺陷时跟踪工具就变成了很有价值的手段,因为诸如受阻的任务等症候是这些问题的唯一表现形式。

    就操作系统所需的特性来看,重入库(reentrant library)特性在RTOS环境下是可有可无的。但在一个典型的操作系统中,由于任务和程序常常是随机的和变化的,而且常公用库,因此重入库是一个必须的特性。

    在嵌入式环境中,对在系统中运行的程序和任务一般会有更多的控制要求。通常,除操作系统接口(可以是重入也可能是非重入的)外,各任务从不共享任何代码。程序员(特别是那些负责设备驱动程序的)需要注意这一重入性问题。

    现在业内已有很多的任务同步机制,从互斥(mutex)到消息系统。从RTOS的角度,这些机制在诸如竞争条件此类的同步问题上,没有什么差异。

    在MCU和操作系统中,定时器很常见。至少,一个定时器可被用作时钟。但由于定时器是如此的有用,以至于它常以一种特殊方式实现出来。POSIX规范甚至把定时器定义为组件。定时器还可当作看门狗来用。

    在许多MCU中,一个定时器可以设置用来唤醒处在休眠模式的系统。一些实现允许操作系统把其用作一个通用定时器,尽管这一唤醒特性独立于操作系统。

    一些系统具有带不同特性的多种定时器来满足不同的要求。一些定时器可被同步用以为电机控制应用提供同时的脉宽调制(PWM)流。对RTOS来说,一个定时器通常可用以实现时钟和提供时间切片支持。

    定时器也支持时间切片。时间切片常见于时间共享系统,它给每种应用一个合理的时间片断来执行。可在任一中断层级上实现这种轮询调度。

    通常,由时钟提供的时间切片是固定时长的,每个任务在获得优先权前将被给予同样长度的时间切片来执行。当然,该策略是随机的且可有多种实现。例如,可变的时间切片宽度将允许时间以每个任务为单位进行分配,其中一些任务获得的时间会比另一些长;而若采用任务优先级方法,则有可能使低优先级任务得不到响应。

    许多RTOS采用固定调度器。其它RTOS则允许替换或定制,但RTOS中的另一部分支持各种策略。这一灵活方法使得像Linux这样的操作系统能够提供实时支持,与此同时,它们还能在时间切片环境下运行多种应用。实时任务具有高优先级,且在一般用户任务前得到执行。

    Linux实际上具有一个更复杂的调度系统,它对任务是通过轮询方法把控制权转交给具有相同优先级的其它任务还是一直运行到结束做出了具体约定。像Open Kernel Labs的OKL4虚拟化RTOS平台解决了该问题。​

基本通信

    一些文献把任务同步和通信分开来说,但总的来说,它们是一回事。实际上就是讲信息是如何交换的。基于消息传递的RTOS最清楚地体现出这点。这里,消息系统处理所有通信且不区分通信和同步。

    至少,RTOS必须提供一个相互排斥的本原,如互斥。其它东西可构建在该本原上。在许多场合,如消息传递系统,对相互排斥的支持隐藏在操作系统内。只有更高级别的消息功能显露于外。

    消息系统有各种名称,从管道到队列。其实现可横跨从单处理器、单存储器模式到多内核群集系统。Enea的OSE RTOS和QNX的Neutrino是基于消息传递的两个主线RTOS。

    不管选择了什么方法或API,通信系统必须在某一程度上被整合进操作系统。因此,若主动队列中的任务必须等待一个事件,则该任务可被移走。类似,引发一个事件从而导致另一个任务活动的任务将产生一个调度行为。

    通信、事件和调度可与硬件关联起来,这是RTOS必须处理的其它一些事。TI的DSP/BIOS是一款RTOS,它设计用于运行在像TI的DaVinci双核系统的DSP上。DSP/BIOS的一个主要功能是处理 ARM 核和DSP 核间的通信。

    向更多大内核的发展将很可能会保留RTOS或OS。不过,小内核阻止或限制了采用RTOS的可能性。Intellasys的SEAforth 40C18芯片带有40个运行Forth的小型18位内核。指令很精简,每个字包含四条指令。

    每个内核有64个字的 ROM和RAM,该芯片只能容纳10,000指令。当然,这只够装下一个程序,安装RTOS是不可能的。不过,整个芯片上有足够空间安装一个操作环境的特定部分。同样,适于该平台的应用常常是特定的。于是,由于硬件可处理内核之间通信和任务调度,因此RTOS类的支持并不需要。​

资源管理

    使RTOS脱颖而出的是其管理资源(包括时间和存储器)的能力。时序问题与中断响应时间有关,但资源管理时序问题也会出现。虽然中断解决了一系列时序问题,但各应用仍必须利用资源。

    考虑存储器分配情况。许多实时应用不采用动态存储器分配,以确保存储器分配和回收时所产生的不同不会变成一个问题。需要动态存储器分配的应用常把存储器划分为实时和非实时。后者处理动态存储器分配。典型情况下,在使用前,实时部分必须被分配有足够的存储器。

    在实时嵌入式应用中采用C和C++是因为存储器和其它资源的用法是显式的。实时任务需要避免采用C和C++。特别是,当存储器分配和回收更容易隐藏时采用C++是很困难的。

    像Java和C#这样的语言带来的挑战更大,它们与生俱来地采用动态存储器分配。程序员可控制存储器分配和回收。在某些情况下,编程环境可以强化存储器分配和回收。

    Java实时规范(RTSJ)定义了创建不需要垃圾回收的Java应用的方法。RTSJ是在Java框架内这样做的,从而使程序员在不被存储器分配限制的条件下享有Java的好处。

    Sun和DDC-I都实现了RTSJ。DDC-I的实现支持x86和PowerPC平台。Aonix有一个称为PERC的类似平台。这些平台以实时、同时的垃圾回收为特征,从而使在不受存储器分配限制的情况下,在Java内编写实时应用成为可能。

    但因系统必须允许线程为垃圾回收器进行转换,所以实时要求并非那么紧迫。另一方面,垃圾回收器将耗费时序资源,所以,只有实时任务方可保证满足一定的期限要求。快是好事,但及时才是RTOS的天条。

    考察实时平台时,考虑之一是存储器分配对系统的整体影响。许多系统可工作在从不改变的静态分配环境,但更多的动态系统可从实时垃圾回收中获益。研究表明,垃圾回收的效益与确定的存储器分配是可比的。

    围绕诸如Java和C#等虚拟机类型平台的另一个问题是对just-in-time(JIT)编译器的使用限制。基于这些系统的实时系统必须采用类似C和C++等所用的提前(ahead-of time,AOT)编译器。

    设计师会因其更高的生产力、更低的出错率以及安全性等特点选用Java 或C#。所以,对制定一个称为 JSR-302的用于对安全有至高要求应用的Java规范就不足为奇了。​

保护RTOS

    RTOS受到其运行的硬件平台的限制。可对缺少存储器保护的硬件加以保护,但安全级别会受到限制。但存储器和虚拟机可以更高水平的安全性支持引导。诸如SE Linux、Green Hills Integrity和 LynuxWorks LynxSecure Embedded Hypervisor以及 LynxOS-SE RTOS内的安全策略可比典型RTOS提供可靠得多的保护。但成本也高,所以开发者需对此进行权衡。

    实时系统开发者不得不应对策略实现和边界问题。取决于信息的来所去处,安全支持会花很长时间。正是为此引入了分区系统,所以,可在边界采取安全措施且把应用的非实时部分放在这部分空间内。 ​

可感知OS的调度器

    当考虑选用操作系统时,对调试器的支持是个关键。这种支持体现在两个方面:内核和设备驱动器调试以及操作系统感知。

    内核调试对设备驱动器的创建和支持以及内核强化很重要。在许多情况,为处理RTOS的内核,需要专用调试器。它也要求能理解内核环境以及应用环境。

    OS感知可更深入地了解操作系统。支持方式可以是从提供有关OS服务状态的信息到调整任务调度等方方面面。同样,能感知OS的调试器可在停止其它应用或线程的同时允许其它应用或线程的运行。

三、RTOS在STM32中的应用

介绍实时操作系统RTOS,分析为什么在嵌入式系统中使用它。

什么是实时操作系统

    操作系统的定义:

    一个计算机系统可以大致分为四个部分:硬件(Hardware),操作系统(operating system),应用程序(application programs),使用者(user)。

    硬件包含了CPU,存储空间,输入输出设备等为整个系统提供了基础的计算资源。

    操作系统是一个控制程序,作为硬件和应用程序之间的桥梁,主要是和硬件打交道,负责协调分配计算资源和内存资源给不同的应用程序使用,并防止系统出现故障。面对来自不同应用程序的大量且互相竞争的资源请求,操作系统通过一个调度算法和内存管理算法尽可能把资源公平,有效率地分配给不同的程序。

    应用程序则通过调用操作系统提供的API接口获得相应资源完成指定的任务。

    一个通用操作系统提供的主要功能有(可见下图蓝色的部分):

  • 任务(应用程序)的调度管理
  • 堆栈和内存管理
  • 文件管理
  • 队列管理
  • 中断和定时器管理
  • 资源管理
  • 输入输出管理

图片

    值得注意的是,操作系统自身需要占用一定的计算资源和内存资源,对于片上资源有限的单片机,可能需要根据需求剪裁操作系统的大小和功能。

实时性的含义

    实时操作系统(RTOS-Real Time Operating System)中实时(Real Time)指的是任务(Task)或者说实现一个功能的线程(Thread)必须在给定的时间(Deadline)内完成。

    业界总有种误解认为如果能堆砌更多的处理器核心数目,更高的处理器频率,更大的内存,更快的总线速度系统就能一定达到实时性的表现。但是强大的计算能力并不能保证系统的实时性。

    举一个简单例子比如汽车中的安全气囊,在传感器检测到汽车发生碰撞后,安全气囊需要在30ms内完全打开,不然司机和乘客的人身安全将受到极大的威胁。即使车载ECU有很强大的计算能力,但是如果因为要执行其他复杂计算任务或者任务调度的问题导致对汽车状态的监测和安全气囊的响应时间超过规定的时间,实时性无法得到保障导致系统失效和人员伤亡,这将会是非常严重的系统问题。

    再举个例子比如特斯拉的车载系统:车机系统采用的是Linux系统,而汽车的驱动模块和刹车模块使用的是各自独自的系统。整个娱乐系统,触摸屏以及智能驾驶系统,都与汽车的驱动完全分开,确保了系统的实时性。

    一些其它的实时任务包括导弹的制导,无人驾驶等。为了保障这些实时任务能在给定的时间内完成,需要一个实时系统对这些任务进行调度和管理。一个实时操作系统能尽力保障每个任务能在一个已知的最大运行时间(Maximum Response Time)内完成,包括:

  • 对中断和内部异常的处理
  • 对安全相关的事件的处理
  • 任务调度机制等

    当然系统的稳定性不能仅仅依靠实时操作系统,安全功能(fail-safe function)相关的设计也是必须要考虑到的。在Netflix的纪录片《一落千丈:波音大调查》中就提到,导致两起波音最新机型737 Max的失事事故的直接原因在于当飞机的迎角传感器发生故障,机上装载的机动特性增强系统(MCAS)误认为飞机处于失速状态,无视飞行员的操纵强制自动接管飞机,持续压机头导致了惨剧的发生。可见纵使波音的飞控系统非常稳定,但如果对传感器的数值判定和使用不当并缺少安全性冗余设计,系统的整体稳定性依然无法得到保障。

图片

操作系统的种类

    目前比较流行的实时操作系统包括黑莓QNX,FreeRTOS,uCOS,RT-Thread等。在上图中可以看出,除了Linux和Windows通用操作系统外,接下来便是FreeRTOS受到广大开发者的青睐。图表中也罗列了一些国产的系统。比如腾讯发布和开源的TencentOS tiny,这是一个面向物联网的实时操作系统。华为也推出了鸿蒙LiteOS, 万物互联未来可期。和实时操作系统(Real Time Operating System)相对应的是通用操作系统(General Purpose Operating System)。

    通用操作系统包括Linux,Windows,MAC等主流的操作系统。这些操作系统大家每天都在使用,功能也十分强大,只是它们有时为了保障系统的流畅运行,就不能保证每个程序都能实时响应,在易用性和实时性之间有所取舍。而且单片机有限的片上资源也不足以支撑通用操作系统的运行。

    正所谓术业有专攻,在嵌入式领域中,嵌入式实时操作系统(RTOS)可以更合理、更有效地利用CPU的资源,简化应用软件的设计,缩短系统开发时间,从而更好地保证系统的实时性和可靠性。

FreeRTOS的介绍

    FreeRTOS 是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行。

    FreeRTOS的官网地址:https://www.freertos.org/

    官网上会有关于FreeRTOS的最新新闻和技术文档,如果大家吃透了FreeRTOS的技术文档的话,其实也就不需要读文章了哈哈。

FreeRTOS的特性

  • 具有抢占式或者合作式的实时操作系统内核
  • 功能可裁剪,最小占用10kB左右rom空间,0.5kB ram空间
  • 具有低功耗模式
  • 有互斥锁、信号量、消息队列等功能
  • 运行过程可追踪

STM32CubelDE

    STM32CubeIDE 是一个多功能的集成开发工具,集成了TrueSTUDIO和STM32CubeMX,它是STM32Cube软件生态系统的一部分。STM32CubeIDE是一个先进的C/C++开发平台,具有STM32微控制器的IP配置,代码生成,代码编译和调试功能。

    相对而言对使用STM32平台的童鞋们来说使用FreeRTOS非常方便和强大,因为它被集成到了STM32的开发环境当中。

图片

图片

    如上图所示,强烈安利使用STM32CubeIDE进行STM32平台项目的开发。通过举手之间的配置,FreeRTOS就被部署到了项目中去。在项目结构中中间件(Middlewares)你可以看到FreeRTOS这个文件。同时FreeRTOS的参数设置,添加删除任务,定时器,消息队列等都可以通过下面的图形化界面进行配置,之后文章会详细介绍。

图片

标准库和HAL库

    还有一点要说明的事情,STM的开发包括寄存器开发,标准库文件开发,还有HAL库开发三种开发方式。市面上很多应用教程都是针对标准库设计的。本系列教程将统一采用HAL(Hardware Abstraction Layer)库,这是一个痛苦的选择,但是因为意法半导体(ST)已经停止对标准库文件的维护和开发,为了与时俱和移植性便利性选择了HAL库。HAL库的原理和标准库大同小异,所以学起来其实并不会太累,只是需要跳离舒适区慢慢适应。

四、xxx

五、单片机裸机与RTOS多线程

   很多初学者都是从裸机开始学起,不能明白裸机和操作系统的区别。甚至有不少人认为用中断就能代替多任务处理,你认同吗?

裸机系统

    裸机系统通常分成轮询系统和前后台系统。

1 轮询系统

    轮询系统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情,大概的伪代码具体如代码清单所示:

int main(void)
{
  /* 硬件相关初始化 */
  HardWareInit();


  /* 无限循环 */
  for (;;) {
    /* 处理事情 1 */
    DoSomething1();


    /* 处理事情 2 */
    DoSomethingg2();


    /* 处理事情 3 */
    DoSomethingg3();
  }
}

    轮询系统是一种非常简单的软件结构,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情。在代码清单 1-1 中,如果只是实现 LED 翻转,串口输出,液晶显示等这些操作,那么使用轮询系统将会非常完美。但是,如果加入了按键操作等需要检测外部信号的事件,用来模拟紧急报警,那么整个系统的实时响应能力就不会那么好了。

    假设DoSomethingg3 是按键扫描,当外部按键被按下,相当于一个警报,这个时候,需要立马响 应 , 并 做 紧 急 处 理 , 而 这 个 时 候 程 序 刚 好 执 行 到 DoSomethingg1 , 要 命 的 是DoSomethingg1 需要执行的时间比较久,久到按键释放之后都没有执行完毕,那么当执行到 DoSomethingg3 的时候就会丢失掉一次事件。足见,轮询系统只适合顺序执行的功能代码,当有外部事件驱动时,实时性就会降低。

2 前后台系统

    相比轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断在这里我们称为前台, main 函数里面的无限循环我们称为后台,大概的伪代码见代码清单所示:

int flag1 = 0;
int flag2 = 0;
int flag3 = 0;


int main(void)
{
  /* 硬件相关初始化 */
  HardWareInit();


  /* 无限循环 */
  for (;;) {
    if (flag1) {
      /* 处理事情 1 */
      DoSomething1();
    }


    if (flag2) {
      /* 处理事情 2 */
      DoSomethingg2();
    }


    if (flag3) {
      /* 处理事情 3 */
      DoSomethingg3();
    }
  }
}


void ISR1(void)
{
  /* 置位标志位 */
  flag1 = 1;
  /* 如果事件处理时间很短,则在中断里面处理
  如果事件处理时间比较长,在回到后台处理 */
  DoSomething1();
}


void ISR2(void)
{
  /* 置位标志位 */
  flag2 = 2;


  /* 如果事件处理时间很短,则在中断里面处理
  如果事件处理时间比较长,在回到后台处理 */
  DoSomething2();
}


void ISR3(void)
{
  /* 置位标志位 */
  flag3 = 1;
  /* 如果事件处理时间很短,则在中断里面处理
  如果事件处理时间比较长,在回到后台处理 */
  DoSomething3();
}

    在顺序执行后台程序的时候,如果有中断来临,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序里面标记事件,如果事件要处理的事情很简短,则可在中断服务程序里面处理,如果事件要处理的事情比较多,则返回到后台程序里面处理。

    虽然事件的响应和处理是分开了,但是事件的处理还是在后台里面顺序执行的,但相比轮询系统,前后台系统确保了事件不会丢失,再加上中断具有可嵌套的功能,这可以大大的提高程序的实时响应能力。在大多数的中小型项目中,前后台系统运用的好,堪称有操作系统的效果。

RTOS多线程

    相比前后台系统,多线程系统的事件响应也是在中断中完成的,但是事件的处理是在线程中完成的。在多线程系统中,线程跟中断一样,也具有优先级,优先级高的线程会被优先执行。

    当一个紧急的事件在中断被标记之后,如果事件对应的线程的优先级足够高,就会立马得到响应。相比前后台系统,多线程系统的实时性又被提高了。

    多线程系统大概的伪代码具体见代码清单所示:

int flag1 = 0;
int flag2 = 0;
int flag3 = 0;


int main(void)
{
  /* 硬件相关初始化 */
  HardWareInit();


  /* OS 初始化 */
  RTOSInit();


  /* OS 启动,开始多线程调度,不再返回 */
  RTOSStart();
}


void ISR1(void)
{
  /* 置位标志位 */
  flag1 = 1;
}


void ISR2(void)
{
  /* 置位标志位 */
  flag2 = 2;
}


void ISR3(void)
{
  /* 置位标志位 */
  flag3 = 1;
}


void DoSomething1(void)
{
  /* 无限循环,不能返回 */
  for (;;) {
    /* 线程实体 */
    if (flag1) {


    }
  }
}


void DoSomething2(void)
{
  /* 无限循环,不能返回 */
  for (;;) {
    /* 线程实体 */
    if (flag2) {


    }
  }
}


void DoSomething3(void)
{
  /* 无限循环,不能返回 */
  for (;;) {
    /* 线程实体 */
    if (flag3) {


    }
  }
}

    相比前后台系统中后台顺序执行的程序主体,在多线程系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的,无限循环且不能返回的小程序,这个小程序我们称之为线程。

    每个线程都是独立的,互不干扰的,且具备自身的优先级,它由操作系统调度管理。加入操作系统后,我们在编程的时候不需要精心地去设计程序的执行流,不用担心每个功能模块之间是否存在干扰。

    加入了操作系统,我们的编程反而变得简单了。整个系统随之带来的额外开销就是操作系统占据的那一丁点的 FLASH 和 RAM。现如今,单片机的 FLASH 和 RAM 是越来越大,完全足以抵挡 RTOS 那点开销。

    轮询、前后台和多线程系统软件模型区别:

图片

六、进入OS前的两步:PendSV(任务切换)

先了解下如何使用PendSV异常。为何要使用PendSV而不是其他的异常,请参考《cortex-M3权威指南》。​

1 如何设定PendSV优先级?

图片

NVIC_SYSPRI14 EQU 0xE000ED22
NVIC_PENDSV_PRI EQU 0xFF
    LDR R0, =NVIC_SYSPRI14 LDR R1, =NVIC_PENDSV_PRI
    STRB R1, [R0]

2 如何触发PendSV异常?

图片

    往ICSR第28位写1,即可将PendSV异常挂起。若是当前没有高优先级中断产生,那么程序将会进入PendSV handler

NVIC_INT_CTRL EQU 0xE000ED04
NVIC_PENDSVSET EQU 0x10000000


LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]

3 编写PendSV异常handler

    这里用PendSV_Handler来触发LED点亮,以此证明PendSV异常触发的设置是正确的。

#include "stm32f10x_conf.h"


#define LED0 *((volatile unsigned long *)(0x422101a0)) //PA8


unsigned char flag=0;
void LEDInit(void)
{
    RCC->APB2ENR|=1<<2;
    GPIOA->CRH&=0XFFFFFFF0;
    GPIOA->CRH|=0X00000003;
    GPIOA->ODR|=1<<8;
}


__asm void SetPendSVPro(void)
{
    NVIC_SYSPRI14 EQU 0xE000ED22
    NVIC_PENDSV_PRI EQU 0xFF
    LDR R1, =NVIC_PENDSV_PRI    
    LDR R0, =NVIC_SYSPRI14
    STRB R1, [R0]
    BX LR
}


__asm void TriggerPendSV(void)
{
    NVIC_INT_CTRL EQU 0xE000ED04
    NVIC_PENDSVSET EQU 0x10000000
    LDR R0, =NVIC_INT_CTRL
    LDR R1, =NVIC_PENDSVSET
    STR R1, [R0]
    BX LR
}


int main(void)
{
    SetPendSVPro();
    LEDInit();
    TriggerPendSV();
    while(1);
}


void PendSV_Handler(void)
{
    LED0 = 0;
}

    上述代码可以正常点亮LED,说明PendSV异常是正常触发了。

    OK,是时候挑战任务切换了。

    如何实现任务切换?三个步骤:

    步骤一:

    在进入中断前先设置PSP。

curr_task = 0;

    设置任务0为当前任务:

__set_PSP((PSP_array[curr_task] + 16*4));

    设置PSP指向task0堆栈的栈顶位置:

__set_CONTROL(0x3);

    设置为用户级,并使用PSP堆栈:

__ISB();

    指令同步隔离。

    步骤二:

    将当前寄存器的内容保存到当前任务堆栈中。进入ISR时,cortex-m3会自动保存八个寄存器到PSP中,剩下的几个需要我们手动保存。

    步骤三:

    在Handler中将下一个任务的堆栈中的内容加载到寄存器中,并将PSP指向下一个任务的堆栈。这样就完成了任务切换。

    要在PendSV 的ISR中完成这两个步骤,我们先需了解下在进入PendSV ISR时,cortex-M3做了什么?

    1入栈:

    会有8个寄存器自动入栈。入栈内容及顺序如下:

图片

    在步骤一中,我们已经设置了PSP,那这8个寄存器就会自动入栈到PSP所指地址处。

    2取向量:

    找到PendSV ISR的入口地址,这样就能跳到ISR了。,

    3更新寄存器内容:

    做完这三步后,程序就进入ISR了。

    进入ISR前,我们已经完成了步骤一,cortex-M3已经帮我们完成了步骤二的一部分,剩下的需要我们手动完成。

    在ISR中添加代码如下:

MRS R0, PSP

    保存PSP到R0。为什么是PSP而不是MSP。因为在OS启动的时候,我们已经把SP设置为PSP了。这样使得用户程序使用任务堆栈,OS使用主堆栈,不会互相干扰。不会因为用户程序导致OS崩溃。

STMDB R0!,{R4-R11}

    保存R4-R11到PSP中。C语言表达是*(--R0)={R4-R11},R0中值先自减1,然后将R4-R11的值保存到该值所指向的地址中,即PSP中。

    STMDB Rd!,{寄存器列表} 连续存储多个字到Rd中的地址值所指地址处。每次存储前,Rd先自减一次。

    若是ISR是从从task0进来,那么此时task0的堆栈中已经保存了该任务的寄存器参数。保存完成后,当前任务堆栈中的内容如下(假设是task0)

图片

    左边表格是预期值,右边是keil调试的实际值。可以看出,是一致的。在任务初始化时(步骤一),我们将PSP指向任务0的栈顶0x20000080。在进入PendSV之前,cortex-M3自动入栈八个值,此时PSP指向了0x20000060。然后我们再保存R4-R11到0x20000040~0x2000005C。

    这样很容易看明白,如果需要下次再切换到task0,只需恢复R4~R11,再将PSP指向0x20000060即可。

    所以切换到另一个任务的代码:

LDR R1,=__cpp(&curr_task)
LDR R3,=__cpp(&PSP_array)
LDR R4,=__cpp(&next_task)
LDR R4,[R4]

    获取下一个任务的编号:

STR R4,[R1]
Curr_task=next_task
LDR R0,[R3, R4, LSL #2]

    获得任务堆栈地址,若是task0,那么R0=0x20000040( R0=R3+R4*4)

LDMIA R0!,{R4-R11}

    恢复堆栈中的值到R4~R11。R4=*(R0++)。执行完后,R0中值变为0x20000060

LDMIA Rd! {寄存器列表} 先将Rd中值所指地址处的值送出寄存器中,Rd再自增1.*

MSR PSP, R0
PSP=R0。
BX LR

    中断返回。​

    完整代码:

#include "stm32f10x.h"
#include "stm32f10x_usart.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stdio.h"
#include "misc.h"


#define HW32_REG(ADDRESS)  (*((volatile unsigned long  *)(ADDRESS)))
#define LED0 *((volatile unsigned long *)(0x422101a0)) //PA8
void USART1_Init(void);
void task0(void) ; 
unsigned char flag=1;


uint32_t  curr_task=0;     // 当前执行任务
uint32_t  next_task=1;     // 下一个任务
uint32_t task0_stack[17];
uint32_t task1_stack[17];
uint32_t  PSP_array[4];


u8 task0_handle=1;
u8 task1_handle=1;


void task0(void) 
{ 
    while(1)
    {
        if(task0_handle==1)
        {
            printf("task0\n");
            task0_handle=0;
            task1_handle=1;
        }
    }
}


void task1(void)
{
    while(1)
    {
        if(task1_handle==1)
        {
            printf("task1\n");
            task1_handle=0;
            task0_handle=1;
        }
    }
}


void LEDInit(void)
{
    RCC->APB2ENR|=1<<2; 
    GPIOA->CRH&=0XFFFFFFF0;
    GPIOA->CRH|=0X00000003; 
      GPIOA->ODR|=1<<8;
}


__asm void SetPendSVPro(void)
{
NVIC_SYSPRI14   EQU     0xE000ED22
NVIC_PENDSV_PRI EQU           0xFF
    
    LDR     R1, =NVIC_PENDSV_PRI    
    LDR     R0, =NVIC_SYSPRI14    
    STRB    R1, [R0]
    BX      LR
}


__asm void TriggerPendSV(void)
{
NVIC_INT_CTRL   EQU     0xE000ED04                              
NVIC_PENDSVSET  EQU     0x10000000                              


    LDR     R0, =NVIC_INT_CTRL                                 
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR
}


int main(void)
{
    USART1_Init();


    SetPendSVPro();
    LEDInit();
    
    printf("OS test\n");
    
    PSP_array[0] = ((unsigned int) task0_stack) + (sizeof task0_stack) - 16*4;
    //PSP_array中存储的为task0_stack数组的尾地址-16*4,即task0_stack[1023-16]地址
    HW32_REG((PSP_array[0] + (14<<2))) = (unsigned long) task0; /* PC */
    //task0的PC存储在task0_stack[1023-16]地址  +14<<2中,即task0_stack[1022]中
    HW32_REG((PSP_array[0] + (15<<2))) = 0x01000000;            /* xPSR */
    PSP_array[1] = ((unsigned int) task1_stack) + (sizeof task1_stack) - 16*4;
    HW32_REG((PSP_array[1] + (14<<2))) = (unsigned long) task1; /* PC */
    HW32_REG((PSP_array[1] + (15<<2))) = 0x01000000;            /* xPSR */    
    
    /* 任务0先执行 */
    curr_task = 0; 
     
    /* 设置PSP指向任务0堆栈的栈顶 */
    __set_PSP((PSP_array[curr_task] + 16*4)); 
    
    SysTick_Config(9000000);
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);//72/8=9MHZ     
    /* 使用堆栈指针,非特权级状态 */
    __set_CONTROL(0x3);
    
    /* 改变CONTROL后执行ISB (architectural recommendation) */
    __ISB();
    
    /* 启动任务0 */
    task0();  
    //LED0=0;
    while(1);
}


__asm void PendSV_Handler(void)
{ 
    // 保存当前任务的寄存器内容
    MRS    R0, PSP     // 得到PSP  R0 = PSP
                       // xPSR, PC, LR, R12, R0-R3已自动保存
    STMDB  R0!,{R4-R11}// 保存R4-R11共8个寄存器得到当前任务堆栈
    
    // 加载下一个任务的内容
    LDR    R1,=__cpp(&curr_task)
    LDR    R3,=__cpp(&PSP_array)
    LDR    R4,=__cpp(&next_task)
    LDR    R4,[R4]     // 得到下一个任务的ID
    STR    R4,[R1]     // 设置 curr_task = next_task
    LDR    R0,[R3, R4, LSL #2] // 从PSP_array中获取PSP的值
    LDMIA  R0!,{R4-R11}// 将任务堆栈中的数值加载到R4-R11中
  //ADDS   R0, R0, #0x20
    MSR    PSP, R0     // 设置PSP指向此任务
 // ORR     LR, LR, #0x04   
    BX     LR          // 返回
                       // xPSR, PC, LR, R12, R0-R3会自动的恢复
    ALIGN  4
}


void SysTick_Handler(void)
{
    flag=~flag;
    LED0=flag;
    if(curr_task==0)
        next_task=1;
    else
        next_task=0;
    TriggerPendSV();
}


void USART1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    
    /* config USART1 clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
    
    /* USART1 GPIO config */
    /* Configure USART1 Tx (PA.09) as alternate function push-pull */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);    
    /* Configure USART1 Rx (PA.10) as input floating */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    /* USART1 mode config */
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No ;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(USART1, &USART_InitStructure); 
    USART_Cmd(USART1, ENABLE);
}


int fputc(int ch, FILE *f)
{
  USART_SendData(USART1, (unsigned char) ch);
  while (!(USART1->SR & USART_FLAG_TXE));
  return (ch);
}

    测试后结果如图:

图片

    可以看出,两个任务可以切换了。

七、xx

八、xx
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐