查看: 927|回复: 5

RTOS在多旋翼飞控中的应用情况?

[复制链接]

45

主题

836

帖子

1668

积分

金牌飞友

Rank: 6Rank: 6

积分
1668
飞币
815
注册时间
2017-8-24
发表于 2022-10-27 16:19:55 | 显示全部楼层 |阅读模式
这里多指STM32,M3架构的飞控芯片(比如PIX,APM使用的2560的板子感觉性能不够)。
我想知道在多轴飞控中使用RTOS系统这种情况在实际应用中普遍吗?
是否具有意义?
PS:在画(抄袭)一块飞控板,打算采用STM32+MPU9150+MS5611+HMC5883L这一套组合。
看到crazyfile采用了RTOS,固有此问。
谢谢大家。

66

主题

863

帖子

1766

积分

金牌飞友

Rank: 6Rank: 6

积分
1766
QQ
飞币
866
注册时间
2017-9-19
发表于 2022-10-27 16:28:47 | 显示全部楼层
2022.08.31 更新一下,对不起大家了,最近很懒,懒得写文字,沉迷于开发而不能自拔
无人机的下行数据传输及飞行数据记录(黑匣子)有个比较尴尬的地方,那就是传输或读写的每个数据帧都不大,但是时间密集性很高,也就是帧数率fps要求比较高。这种情况下,dma 的优势不是特别明显。有效果,但是没有脱胎换骨般的效果。
目前,我们的flash读写频率是50hz,扩展到100hz没有任何问题,因为设计了flash 页面读写的缓冲机制。正因为如此,机载飞行数据记录将逐步取代地面站遥测数据记录成为飞行数据分析的主要来源。
——————
追更一下,目前ahrs 解算的那个线程在 stm32f429 mcu 上跑的实际耗时是263us,就是说很接近4000hz了。不过我们的实际运算频率定成1000 hz, 比较保守。不过比较尴尬的是,在h7 mcu 上跑的时间我记得大概也是200 多us, 没有明显差别。
Flash 写入及串口读写周期耗时300多 us, 实际工作频率设为50hz
另外,由于 rtos 的引入,我做了个简单的message loop, 能够在整个飞控框架中使用sendmessage和 postmessage,很方便,很有用。这是我们老古董mfc程序员的小爱好。
————
这个问题问得很好,很多年前我们刚开始做飞控的时候也问过自己这个问题,rtos在飞控软件设计中的必要性究竟如何?
而现在,我们正在开发基于 freertos 的、模块化且易于二次开发的飞控软件,目前进度已过大半。
简单的一句话概括:rtos的引入能够彻底解决MCU低速操作严重拖累快速操作执行的问题,以及进而形成的性能瓶颈。
这个问题,在未引入rtos而仅有状态机的情况下基本是无解的。

飞控当中典型的快速操作如ahrs数据的测量解算,典型的慢速操作如usart下行发送遥测数据,飞行log数据写入flash等。若不引入rtos,mcu在执行慢速操作尤其是与io有关的慢速同步操作时,不能同时(准确的说是近似同时)再执行其他需要高速运行的操作。状态机只能解决各个不同操作的运行时序控制,以及对单次loop运行耗时进行控制等问题,要想实现分时操作功能,最简单的方法就是引入rtos。

我们上一版基于stm32f4的固定翼/多轴飞控,ahrs解算频率在300hz左右,目前正在开发的,基于完全相同硬件平台,引入freertos的新一版飞控软件,ahrs解算频率轻松接近4000hz,且没有对算法及cache进行刻意优化。
不仅如此,上一版的log记录频率10hz,本版估计50hz轻松实现。

43

主题

860

帖子

1723

积分

金牌飞友

Rank: 6Rank: 6

积分
1723
飞币
846
注册时间
2017-8-21
发表于 2022-10-27 16:43:18 | 显示全部楼层
理论部分
前提

前面RTOS1-6中实现了从0开始理解嵌入式操作系统,前面都是纯理念的内容要学会RTOS就必须在实践中验证理论的知识。从本篇开始将从最流行的FreeRTOS入手配合STM32f103c8t6在实践中理解FreeRTOS源码并且验证前面所说的RTOS原理。
前提知识

STM32内存
Freertos中自带了内存管理机制,如果要想理解源码就必须对STM32的内存有一定的了解。

  • 存储器:STM32中程序存储器、数据存储器、寄存器、输出输入都被组织在同一个4GB的线性地址空间中。可访问的存储器空间被划分了8个主要块,每块为512MB.存储器本身不具备地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程称为存储器映射。如果给存储器再分配一个地址就叫存储器重映射。

RTOS在多旋翼飞控中的应用情况?-1.jpg

RTOS在多旋翼飞控中的应用情况?-2.jpg

RTOS在多旋翼飞控中的应用情况?-3.jpg

  • Block0:区域介绍主要用于设计片内的FLASH,我们使用的STM32F10C8T6的flash大小为64K。
  • Block1:Block1 用于设计片内的SRAM,STM32F103C8T6的SRAM的大小为48K.

RTOS在多旋翼飞控中的应用情况?-4.jpg

  • Block2:用于设计片内外设,根据外设的总线速度不同,Block被划分为APB和AHB两部分,APB被划分为APB1和APB2。
Flash与SRAM的基本理解
从上面的Block0-Block7可知我们最关心的flash和SRAM主要分布在Block0也就是代码区。为此我们将代码区Block0的Flash与SRAM进行详细的分析。
Flash

RTOS在多旋翼飞控中的应用情况?-5.jpg
  - flash:FLASH存储器又成为闪存,它与EEPROM都是掉电后数据不丢失的存储器,但是FLASH的存储容量都普遍的大于EEPROM,在存储控制上,最主要的区别是FLASH芯片只能一大片一大片地擦除,而EEPROM可以单个字节擦除。在存储器映射中flash的地址开始从0X80000开始+flash的实际内存。对于flash的内存包括主存模块、信息块、闪存存储器 接口寄存器,具体分区如上图所示。

  • 主存储器块:主存储块用于存储程序,我们写的程序一般存储在这里,其作用是存放指令和数据。
  • 信息块:信息块又分成两部分:系统存储器、选项字节。 系统存储器存储用于存放在系统存储器自举模式下的启动程序(BootLoader),当使用ISP方式加载程序时,就是由这个程序执行。这个区域由芯片厂写入BootLoader,然后锁死,用户是无法改变这个区域的。 选项字节存储芯片的配置信息及对主存储块的保护信息。

RTOS在多旋翼飞控中的应用情况?-6.jpg
以上的flash可以了解一下,我们主要关注一下STM32的flash主要存储什么数据。在keil中代码编译中我们可以看到一个信息,如上图所示。


RTOS在多旋翼飞控中的应用情况?-7.jpg

  • Flash=Code+RO-data+RW-data+ZI-data。由上表可知flash在单片中存储的内容如上。

RTOS在多旋翼飞控中的应用情况?-8.jpg
根据上面得flash图表,我们得出了STM32代码在flash中的存储结构如上图。(注意地址可能不同类型的单片机有所不同,我们只需知道大体的结构就可以)
拓展:页、扇、块/族

RTOS在多旋翼飞控中的应用情况?-9.jpg

  • 页:Flash存储器中一种区域划分的单元,好比一本书中一页(其中包含N个字)。如上图主存储块中就包含了32页,每一页都是1KB.

RTOS在多旋翼飞控中的应用情况?-10.jpg

  • 扇区:扇区和页类似,也是一种存储结构单元,只是扇区更常见,大部分Flash主要还是以扇区为最小的单元。如上图扇区的大小位4K。
  • 块(族) :比扇区更高一个等级,一般1块包含多个扇区。同样,以上图W25Q256芯片为例:1块包含16个扇区。
SRAM
静态随机存储器,STM32使用的RAM就属于SRAM,SRAM的特点就是存储速度快,掉电的数据会丢失。

RTOS在多旋翼飞控中的应用情况?-11.jpg

RTOS在多旋翼飞控中的应用情况?-12.jpg
由上图可知SRAM的起始地址为0X20000000,大小为0x0004448.SRAM重要由栈区(STACK)、堆区(HEAP)、静态数据区和全局变量区(.data和.bss)

RTOS在多旋翼飞控中的应用情况?-13.jpg

  • 栈区:由编译器自动分配释放,在单片机的sram中的ZI-data。保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但空间大小有限。由高地址向低地址生长。在C语言中,函数内部定义的局部变量属于栈空间,进入函数的时候向栈空间申请内存给局部变量,退出时释放局部变量,归还内存空间。其空间大小分配可以在文件startup_stm32f4xx.s中设置,如上图所示。

RTOS在多旋翼飞控中的应用情况?-14.jpg

  • 堆区:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。在单片机的sram中的ZI-data中。由malloc 系列函数或new 操作符分配的内存。其生命周期由free 或delete 决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错。向上生长,同理其大小也在startup_stm32f4xx.s中设置,如上图。
  • 静态数据区:保存全局变量和static 变量(包括static 全局和局部变量)。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(.data), 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bss),程序结束后由系统释放。这些数据也是可读可写的,和stack、heap一样,被包含在sram中。静态区的内容在总个程序的生命周期内都存在,由编译器在编译的时候分配。
内存的分配方式

  • 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。 例如全局变量,static变量。
  • 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存 分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
int a = 0;              //全局初始化区
char *p1;               //全局未初始化区
main()
{
  int b;                //栈
  char s[] = "abc";     //栈
  char *p2;             //栈
  char *p3 = "123456";  //123456\0在常量区,p3在栈上。
  static int c =0;//全局(静态)初始化区
  p1 = (char *)malloc(10);
  p2 = (char *)malloc(20);   //分配得来得10和20字节的区域就在堆区。
  strcpy(p1, "123456");      //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

STM32启动过程


RTOS在多旋翼飞控中的应用情况?-15.jpg
外部复位
按键按下时,RESET与地导通,从而使NRST引脚产生一个低电平,实现复位。除时钟控制寄存器(CSR)中的复位标志和备份域中的寄存器外系统复位会将其它寄存器设置为复位值,系统复位可以从软件和硬件实现。
启动方式

RTOS在多旋翼飞控中的应用情况?-16.jpg

RTOS在多旋翼飞控中的应用情况?-17.jpg
第一种启动方式
第一种就是我们最常用的模式,如上图左边的是存储器的Block0,右边的是代码区的Flash这里存储着程序的代码。当芯片上电后,从地址0X0000 0000 开始执行,然而Flash位于0x0800 0000。要实现Flash就需要将0X8000 0000 地址映射到0x0000 0000 的地址上。(这就是所谓的地址映射)从Flash启动,主闪存存储器被映射到启动空间(0x0000 0000),但仍然能够在它原 有的地址(0x0800 0000)访问它,即闪存存储器的内容可以在两个地址区域访问,0x0000 0000或0x0800 0000。

RTOS在多旋翼飞控中的应用情况?-18.jpg
如上图,既然选择了第一种启动方式。我们通过Flash结构分析一下第一种启动方式得程序运行的整个过程。

  • 从0X08000 0000开始首先是给MSP赋予初值,MSP是啥?在RTOS原理的时候我们说过Cortex-M3是属于双堆栈。这里的MSP属于主堆栈。(后面在提及到FreeRTOS的时候再进行分析)
  • 然后将 0x08000004 位置存放的向量地址装入 PC 程序计数器。从0X0800 0004 开始就进入了异常向量表。
中断向量表分析

CPU 从 PC 寄存器指向的物理地址取出第 1 条指令开始执行程序,也就是开始执行复位中断服务程序 Reset_Handler。复位中断服务程序会调用SystemInit()函数来配置系统时钟、配置FMC总线上的外部SRAM/SDRAM,然后跳转到 C 库中__main 函数。由 C 库中的__main 函数完成用户程序的初始化工作(比如:变量赋初值等),最后由__main 函数调用用户写的 main()函数开始执行 C 程序。

RTOS在多旋翼飞控中的应用情况?-19.jpg
为了更好分析中断向量表需要对中断向量表中的汇编语言有所了解,如上图是中断向量表中遇到的汇编指令。
(1)Stack_Size      EQU     0x00008000

(2)                AREA    STACK, NOINIT, READWRITE, ALIGN=3
(3)Stack_Mem       SPACE   Stack_Size
(4)__initial_sp

第一部分:如上代码实现开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等。栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你写的程序出现了莫名奇怪的错误,并进入了硬 fault的时候,这时你就要考虑下是不是栈不够大,溢出了。

  • 第(1)行,EQU 是表示宏定义的伪指令,类似于 C 语言中的#define。伪指令的意思是指这个“指令”并不会生成二进制程序代码,也不会引起变量空间分配。0x00008000 表示栈大小,注意这里是以字节为单位,0x00008000 =32768字节=32KB
  • 第(2)行,开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。ARER 后面的关键字表示这个段的属性

    • STACK :表示这个段的名字,可以任意命名。
    • NOINIT:表示此数据段不需要填入初始数据。
    • READWRITE:表示此段可读可写。
    • ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于0)。

  • 第(3)行,SPACE 这行指令告诉汇编器给 STACK 段分配 0x00000800 字节的连续内存空间。
  • 第(4)行, __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。__initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。
(1)Heap_Size       EQU     0x00000400

(2)                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
(3)__heap_base
(4)Heap_Mem        SPACE   Heap_Size
(5)__heap_limit

第二部分:分配一片连续的内存空间给名字叫 HEAP 的段,也就是分配堆空间。堆的大小为 0x00000400,也就是1024字节=1KB。

  • __heap_base :表示堆的开始地址。
  • heap_limit  : 表示堆的结束地址。
(1)               PRESERVE8
(2)               THUMB

; Vector Table Mapped to Address 0 at Reset
(3)            AREA    RESET, DATA, READONLY
(4)            EXPORT  __Vectors
(5)            EXPORT  __Vectors_End
(6)            EXPORT  __Vectors_Size

第三部分:分配一片连续的内存空间给名字叫 HEAP 的段,也就是分配堆空间。堆的大小为 0x00000400,也就是1024字节=1KB。

  • 第(1)行:PRESERVE8 指定当前文件保持堆栈8字节对齐。
  • 第(2)行:THUMB 表示后面的指令是 THUMB 指令集 ,CM4 采用的是 THUMB - 2 指令集。
  • 第(3)行:AREA 定义一块代码段,只读,段名字是 RESET。READONLY 表示只读,缺省就表示代码段了。
  • 第(4)-(6)行:3 行 EXPORT 语句将 3 个标号申明为可被外部引用, 主要提供给链接器用于连接库文件或其他文件。当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了―向量表查表机制‖。这里使用一张向量表。向量表其实是一个WORD( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。要注意的是这里有个另类: 0 号类型并不是什么 入口地址,而是给出了复位后 MSP 的初值。
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler

                ; External Interrupts
                DCD     WWDG_IRQHandler            ; Window Watchdog
                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
                DCD     TAMPER_IRQHandler          ; Tamper
                DCD     RTC_IRQHandler             ; RTC
                DCD     FLASH_IRQHandler           ; Flash
                DCD     RCC_IRQHandler             ; RCC
                DCD     EXTI0_IRQHandler           ; EXTI Line 0
                DCD     EXTI1_IRQHandler           ; EXTI Line 1
                DCD     EXTI2_IRQHandler           ; EXTI Line 2
                DCD     EXTI3_IRQHandler           ; EXTI Line 3
                DCD     EXTI4_IRQHandler           ; EXTI Line 4
                DCD     DMA1_Channel1_IRQHandler   ; DMA1 Channel 1
                DCD     DMA1_Channel2_IRQHandler   ; DMA1 Channel 2
                DCD     DMA1_Channel3_IRQHandler   ; DMA1 Channel 3
                DCD     DMA1_Channel4_IRQHandler   ; DMA1 Channel 4
                DCD     DMA1_Channel5_IRQHandler   ; DMA1 Channel 5
                DCD     DMA1_Channel6_IRQHandler   ; DMA1 Channel 6
                DCD     DMA1_Channel7_IRQHandler   ; DMA1 Channel 7
                DCD     ADC1_2_IRQHandler          ; ADC1_2
                DCD     USB_HP_CAN1_TX_IRQHandler  ; USB High Priority or CAN1 TX
                DCD     USB_LP_CAN1_RX0_IRQHandler ; USB Low  Priority or CAN1 RX0
                DCD     CAN1_RX1_IRQHandler        ; CAN1 RX1
                DCD     CAN1_SCE_IRQHandler        ; CAN1 SCE
                DCD     EXTI9_5_IRQHandler         ; EXTI Line 9..5
                DCD     TIM1_BRK_IRQHandler        ; TIM1 Break
                DCD     TIM1_UP_IRQHandler         ; TIM1 Update
                DCD     TIM1_TRG_COM_IRQHandler    ; TIM1 Trigger and Commutation
                DCD     TIM1_CC_IRQHandler         ; TIM1 Capture Compare
                DCD     TIM2_IRQHandler            ; TIM2
                DCD     TIM3_IRQHandler            ; TIM3
                DCD     TIM4_IRQHandler            ; TIM4
                DCD     I2C1_EV_IRQHandler         ; I2C1 Event
                DCD     I2C1_ER_IRQHandler         ; I2C1 Error
                DCD     I2C2_EV_IRQHandler         ; I2C2 Event
                DCD     I2C2_ER_IRQHandler         ; I2C2 Error
                DCD     SPI1_IRQHandler            ; SPI1
                DCD     SPI2_IRQHandler            ; SPI2
                DCD     USART1_IRQHandler          ; USART1
                DCD     USART2_IRQHandler          ; USART2
                DCD     USART3_IRQHandler          ; USART3
                DCD     EXTI15_10_IRQHandler       ; EXTI Line 15..10
                DCD     RTCAlarm_IRQHandler        ; RTC Alarm through EXTI Line
                DCD     USBWakeUp_IRQHandler       ; USB Wakeup from suspend
__Vectors_End


RTOS在多旋翼飞控中的应用情况?-20.jpg
上面的这段代码是建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。如果程序在 Flash 运行,则中断向量表的起始地址是 0x08000000。以 MDK 为例,就是如下配置选项:

RTOS在多旋翼飞控中的应用情况?-21.jpg
上面代码中DCD表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码。中断向量表存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序。
(1)                AREA    |.text|, CODE, READONLY

; Reset handler
(2)Reset_Handler    PROC
(3)              EXPORT  Reset_Handler             [WEAK]
(4)    IMPORT  __main
(5)    IMPORT  SystemInit
                 LDR     R0, =SystemInit
                 BLX     R0
(6)             LDR     R0, =__main
                 BX      R0
                 ENDP

; Dummy Exception Handlers (infinite loops which can be modified)

NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .
                ENDP
第五部分代码

  • 第(1)行:AREA 定义一块代码段,只读,段名字是 .text 。READONLY 表示只读。
  • 第(2)行:利用 PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
  • 第(3)行:WEAK 声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。 这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。
  • 第(4)行:IMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义。但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。
  • 第(5)行:SystemInit()是一个标准的库函数,在 system_stm32f4xx.c这个库文件总定义。主要作用是配置系统时钟,这里调用这个函数之后,F429的系统时钟配被配置为 180M。
  • 第(6)行:__main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址。该程序的一个主要作用是初始化堆栈,并初始化映像文件,最后跳转到 C 程序中的 main 函数。这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。如果我们在这里不调用__main,那么程序最终就不会调用我们 C文件里面的 main,如果是调皮的用户就可以修改主函数的名称,然后在这里面 IMPORT 你写的主函数名称即可。这个时候你在 C文件里面写的主函数名称就不是 main 了,而是 __main 了。
NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
(1)             B       .
                ENDP
HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler          [WEAK]
                B       .
                ENDP
MemManage_Handler\
                PROC
                EXPORT  MemManage_Handler          [WEAK]
                B       .
                ENDP
BusFault_Handler\
                PROC
                EXPORT  BusFault_Handler           [WEAK]
                B       .
                ENDP
UsageFault_Handler\
                PROC
                EXPORT  UsageFault_Handler         [WEAK]
                B       .
                ENDP
SVC_Handler     PROC
                EXPORT  SVC_Handler                [WEAK]
                B       .
                ENDP
DebugMon_Handler\
                PROC
                EXPORT  DebugMon_Handler           [WEAK]
                B       .
                ENDP
PendSV_Handler  PROC
                EXPORT  PendSV_Handler             [WEAK]
                B       .
                ENDP
SysTick_Handler PROC
                EXPORT  SysTick_Handler            [WEAK]
                B       .
                ENDP

(2)Default_Handler PROC

                EXPORT  WWDG_IRQHandler            [WEAK]
                EXPORT  PVD_IRQHandler             [WEAK]
                EXPORT  TAMPER_IRQHandler          [WEAK]
                EXPORT  RTC_IRQHandler             [WEAK]
                EXPORT  FLASH_IRQHandler           [WEAK]
                EXPORT  RCC_IRQHandler             [WEAK]
                EXPORT  EXTI0_IRQHandler           [WEAK]
                EXPORT  EXTI1_IRQHandler           [WEAK]
                EXPORT  EXTI2_IRQHandler           [WEAK]
                EXPORT  EXTI3_IRQHandler           [WEAK]
                EXPORT  EXTI4_IRQHandler           [WEAK]
                EXPORT  DMA1_Channel1_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel2_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel3_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel4_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel5_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel6_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel7_IRQHandler   [WEAK]
                EXPORT  ADC1_2_IRQHandler          [WEAK]
                EXPORT  USB_HP_CAN1_TX_IRQHandler  [WEAK]
                EXPORT  USB_LP_CAN1_RX0_IRQHandler [WEAK]
                EXPORT  CAN1_RX1_IRQHandler        [WEAK]
                EXPORT  CAN1_SCE_IRQHandler        [WEAK]
                EXPORT  EXTI9_5_IRQHandler         [WEAK]
                EXPORT  TIM1_BRK_IRQHandler        [WEAK]
                EXPORT  TIM1_UP_IRQHandler         [WEAK]
                EXPORT  TIM1_TRG_COM_IRQHandler    [WEAK]
                EXPORT  TIM1_CC_IRQHandler         [WEAK]
                EXPORT  TIM2_IRQHandler            [WEAK]
                EXPORT  TIM3_IRQHandler            [WEAK]
                EXPORT  TIM4_IRQHandler            [WEAK]
                EXPORT  I2C1_EV_IRQHandler         [WEAK]
                EXPORT  I2C1_ER_IRQHandler         [WEAK]
                EXPORT  I2C2_EV_IRQHandler         [WEAK]
                EXPORT  I2C2_ER_IRQHandler         [WEAK]
                EXPORT  SPI1_IRQHandler            [WEAK]
                EXPORT  SPI2_IRQHandler            [WEAK]
                EXPORT  USART1_IRQHandler          [WEAK]
                EXPORT  USART2_IRQHandler          [WEAK]
                EXPORT  USART3_IRQHandler          [WEAK]
                EXPORT  EXTI15_10_IRQHandler       [WEAK]
                EXPORT  RTCAlarm_IRQHandler        [WEAK]
                EXPORT  USBWakeUp_IRQHandler       [WEAK]

WWDG_IRQHandler
PVD_IRQHandler
TAMPER_IRQHandler
RTC_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
DMA1_Channel1_IRQHandler
DMA1_Channel2_IRQHandler
DMA1_Channel3_IRQHandler
DMA1_Channel4_IRQHandler
DMA1_Channel5_IRQHandler
DMA1_Channel6_IRQHandler
DMA1_Channel7_IRQHandler
ADC1_2_IRQHandler
USB_HP_CAN1_TX_IRQHandler
USB_LP_CAN1_RX0_IRQHandler
CAN1_RX1_IRQHandler
CAN1_SCE_IRQHandler
EXTI9_5_IRQHandler
TIM1_BRK_IRQHandler
TIM1_UP_IRQHandler
TIM1_TRG_COM_IRQHandler
TIM1_CC_IRQHandler
TIM2_IRQHandler
TIM3_IRQHandler
TIM4_IRQHandler
I2C1_EV_IRQHandler
I2C1_ER_IRQHandler
I2C2_EV_IRQHandler
I2C2_ER_IRQHandler
SPI1_IRQHandler
SPI2_IRQHandler
USART1_IRQHandler
USART2_IRQHandler
USART3_IRQHandler
EXTI15_10_IRQHandler
RTCAlarm_IRQHandler
USBWakeUp_IRQHandler

(3)             B       .

(4)            ENDP

                ALIGN


RTOS在多旋翼飞控中的应用情况?-22.jpg

  • 第(1)行:死循环,用户可以在此实现自己的中断服务程序。不过很少在这里实现中断服务程序,一般多是在其它的 C 文件里面重新写一个同样名字的中断服务程序,因为这里是 WEEK 弱定义的。如果没有在其它文件中写中断服务器程序,且使能了此中断,进入到这里后,会让程序卡在这个地方。
  • 第(2)行:缺省中断服务程序(开始)
  • 第(3)行:死循环,如果用户使能中断服务程序,而没有在 C 文件里面写中断服务程序的话,都会进入到这里。比如在程序里面使能了串口 1 中断,而没有写中断服务程序 ART1_IRQHandle,那么串口中断来了,会进入到这个死循环。
  • 第(4)行:缺省中断服务程序(结束)。
                 IF      :DEF:__MICROLIB           
               
                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit
               
                 ELSE
               
                 IMPORT  __use_two_region_memory
                 EXPORT  __user_initial_stackheap
                 
__user_initial_stackheap

                 LDR     R0, =  Heap_Mem
                 LDR     R1, =(Stack_Mem + Stack_Size)
                 LDR     R2, = (Heap_Mem +  Heap_Size)
                 LDR     R3, = Stack_Mem
                 BX      LR

                 ALIGN

                 ENDIF

                 END

第六部分代码:简单的汇编语言实现 IF…….ELSE…………语句。如果定义了 MICROLIB,那么程序是不会执行 ELSE分支的代码。__MICROLIB 可能大家并不陌生,就在 MDK 的 Target Option 里面设置。

RTOS在多旋翼飞控中的应用情况?-23.jpg
目前分析的启动过程如下图所示

RTOS在多旋翼飞控中的应用情况?-24.jpg
PendSV 异常

寄存器组
在RTOS原理的时候介绍过CPU内部的寄存器组,寄存器组是CPU内部重要的数据存储资源,是汇编程序员能直接使用的硬件资源之一。由于寄存器的存取速度比内存快,所以,在用汇编语言编写程序时,要尽可能充分利用寄存器的存储功能。

RTOS在多旋翼飞控中的应用情况?-25.jpg

  • 通用寄存器:R0-R12,能够暂存数据存储器的数据也能作为ALU的输入操作数和ALU的结果。(其中R0-R3用作传入函数参数,传出函数返回值。在子程序调用之间,可以将 R0-R3 用于任何用途。被调用函数在返回之前不必恢复 R0-R3。如果调用函数需要再次使用 R0-R3的内容,则它必须保留这些内容。R4-R11 被用来存放函数的局部变量。如果被调用函数使用了这些寄存器,它在返回之前必须恢复这些寄存器的值。R12是内部调用暂时寄存器 ip。它在过程链接胶合代码(例如,交互操作胶合代码)中用于此角色。在过程调用之间,可以将它用于任何用途。被调用函数在返回之前不必恢复R12)
  • 堆栈寄存器:R13。也就是CPU内部的SP寄存,只不过在Cortex-M3中SP有两个一个是MSP称为"主堆栈指针"一个为PSP称为"进程堆栈"。这个是用于任务切换的。所Cortex-M3是双堆栈的。同一时间内有且只有一个堆栈指针在作用。
  • 程序计数器:R15。PC,主要在指令存储器是压入CPU的IR指令寄存器的作用。它通常指向下一条指令的地址。
  • 连接寄存器:R14,缩写为"LR"。在RTOS(1)的篇章中,我们讲到在函数嵌套的情况下发生函数跳转如果函数是多重嵌套可以使用堆栈来存储跳转前的函数的地址。如果嵌套比较少可以将跳转前的地址存在CPU的内部寄存器中。LR寄存器就是存储函数跳转前的地址。
除了通用寄存器外Cortex-M3还存在特殊寄存器如下。

RTOS在多旋翼飞控中的应用情况?-26.jpg
程序状态寄存器组(PSRs 或 xPSR)
同样在RTOS(1)一文中(上图的Arm内核结构图的ALU的状态寄存器。)提到在ALU运行后会产生一个Flag的标志,该标志会暂存于状态寄存器中。在Cortex-M3中记录程ALU输出的标志位是属于程序状态寄存器组,对于程序状态寄存器组来说存储ALU逻辑运算后的标志仅仅是它的功能之一,具体的功能如下图。


RTOS在多旋翼飞控中的应用情况?-27.jpg

  • APSR:应用程序状态寄存器组,位于程序状态寄存器的27-31分别为 Q,V,C,Z,N。它们分别代表:饱和标志、溢出标志、进位标志、零标志、负标志。
  • IPSR:中断程序状态寄存器组。位于程序状态寄存器的0-9位。其中M0-M4代表运行的模式、第5位为控制标志位T=0表示执行ARM指令 T=1表示执行Thumb指令 、第6位为快速中断禁止标志位F=1时禁止FIQ中断、第7位为中断禁止标志位 I=1时禁止IRQ中断、第8位为精确数据异常位A=1 禁止不精确的数据异常,第9位为大小端控制位。
  • EPAR:执行状态寄存器组,ICI/IT中断继续执行指令位,T代表Thumb状态,总是位1,清除此位回引起错误异常
ARM处理器工作模式
M0-M4运行模式寄存器。
ARM有七种工作模式,为什么要搞这么多模式?首先这个问题是由操作系统(Linux)决定的。在ARM诞生前,操作系统已经存在了,ARM为了满足操作系统对资源的调度和保护而设计了7种工作模式。主要是为了计算机安全考虑、计算机资源调度和管理、人机接口等外界事件的响应等等而设计的。对于M0-M4来说他们的组合会得到下面七种工作模式。

RTOS在多旋翼飞控中的应用情况?-28.jpg
上面说到Cortex-M3中含有通用的寄存器R0-R15,其实在不同工作模式下使用的通用寄存器都是不一样的如下图所示。

RTOS在多旋翼飞控中的应用情况?-29.jpg

RTOS在多旋翼飞控中的应用情况?-30.jpg
双堆栈
Cortex-M3/M4采用双堆栈机制,分别为MSP(主堆栈指针)PSP(进程堆栈指针)。权威手册上说的很清楚PSP主要是在Handler的模式下使用,MSP主要在线程模式下使用。这意味着同一个逻辑地址,实际上有两个物理寄存器,一个为MSP,一个为PSP,在不同的工作模式调用不同的物理寄存器。Cortex-M3内核有两个堆栈指针:MSP-主堆栈指针和PSP-进程堆栈指针,在任何一个时刻只能有一个堆栈指针起作用,也就是说任何一个时刻只能使用一个堆栈指针,要么使用MSP,要么使用PSP。何为堆栈指针,其实就是普通的指针,只是他们指向两个不同的堆栈。在ARM的工作模式中我们知道了ARM有7种工作模式
为什么要设置双堆栈?
我们知道ARM有7种工作模式,其中有两个工作模式是在RTOS中用到分别是系统模式和用户模式。因为其它的模式中可使用的寄存器都比这两个少,对于用户来说肯定是能操作寄存器数量越多越好。既然,两个可用的寄存器数量都一样直接二选一直接选用户模式不好么。从上面ARM工作模式的表中可以知道,系统模式是用于运行特权级的操作系统任务的,什么意思?在芯片上电复位的过程中首先是选择模式后,根据模式把Flash的地址赋值给MSP(主进程堆栈)然后进行一系列的系统操作(如时钟、中断向量表、复位异常函数跳转到_main最后死循环)这个都是在系统模式下完成的,所以上电过程中是不会使用到用户模式的,而且用户模式是不可以使用所有的中断的。
既然用户模式不行,直接处于用系统模式到底,就不需要用户模式了。这种情况确实可以比如裸机的时候一个系统模式足矣。
如果在RTOS中仅仅使用主进程栈,既要处理运行时中断、系统时钟、工作模式切换、任务上下文切换、以及系统硬件的调用。这并不利于我们对系统的管理。一般的操作系统都如下图所示的结构

RTOS在多旋翼飞控中的应用情况?-31.jpg
如上图,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就会产生一个SVC 异常,然后操作系统提供的SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。这种“提出要求——得到满足”的方式,很好、很强大、很方便、很灵活、很能可持续发展。

  • 首先,它使用户程序从控制硬件的繁文缛节中解脱出来,而是由操作系统 负责控制具体的硬件。
  • 第二,操作系统的代码可以经过充分的测试,从而能使系统更加健壮和可靠。
  • 第三,它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。
  • 第四,通过SVC 的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。开发应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且了解各个请求代号和参数表,然后就可以使用SVC 来提出要求了(事实上,为使用方便,操作系统往往会提供一层封皮,以使系统调用的形式看起来和普通的函数调用一致。各封皮函数会正确使用SVC指令来执行系统调用——译者注)。其实,严格地讲,操作硬件的工作是由设备驱动程序完成的,只是对应用程序来说,它们也是操作系统的一部分。
PendSV与SVC
SVC(系统服务调用,亦简称系统调用)和PendSV(可悬起系统调用),它们多用在上了操作系统的软件开发中。SVC用于产生系统函数的调用请求。例如,操作系统通常不允许用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用SVC发出对系统服务函数的调用请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC异常,然后操作系统提供的SVC异常服务程序得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。
为啥要用PendSV异常?
【两个任务间通过SysTick进行轮转调度的简单模式】

RTOS在多旋翼飞控中的应用情况?-32.jpg
如上图,在早期的时候任务切换实现是放在系统定时器中断中实现的且系统中断(IRQ)的优先级比系统定时器低,显然任务切换会打断IRQ的处理,这并不符合RTOS的实时性。
【发生IRQ时上下文切换的问题】

RTOS在多旋翼飞控中的应用情况?-33.jpg
这种方式被否决了,为了解决这个问题,早期的OS会检测中断是否活跃,当没有任何中断需要响应时才切换上下文,那么会有一个问题,当IRQ执行完才会切换任务,那么切换任务会被拖延。
【使用PendSV控制上下文切换】

RTOS在多旋翼飞控中的应用情况?-34.jpg
可以看到,当要发生一次任务切换时,并不是马上去切换,而且先挂起一个PendSV异常,如果此时没有优先级比PendSV高的中断,那么就响应PendSV异常,PendSV响应服务程序里面就会去切换任务。假如此时来了一个中断异常,那么就先执行中断服务例程,执行中断期间优先级更高的SysTick触发异常,就会先执行Systick异常服务例程,例程里面会挂起一个PendSV异常,然后退出Systick并进入到刚刚被打断的中断服务,中断服务完成后,就会去响应PendSV切换上下文。整个流程如下

  • 1、任务A呼叫SVC来请求任务切换(例如,等到某些工作完成);
  • 2、OS接收到请求,做好上下文切换的准备,并且悬起一个PendSV异常;
  • 3、当CPU退出SVC后,它立即进入PendSV,从而执行上下文切换;
  • 4、当PendSV执行完毕后,将返回到任务B,同时进入线程模式;
  • 5、发生了一个中断,并且中断服务程序已开始执行;
  • 6、在ISR执行过程中,发生SysTick异常,并且抢占了该ISR;
  • 7、OS执行必要的操作,然后悬起PendSV异常以作好上下文切换的准备;
  • 8、当SysTick退出后,回到先前被抢占的ISR中,ISR继续执行;
  • 9、ISR执行完毕并退出后,PendSV服务程序开始执行,并且在里面执行上下文切换;
  • 10、当PendSV执行完毕后,回到任务A,同时系统再次进入线程模式。
要实现第三种方法,那在初始化的时候要对PendSV异常与系统定时器异常的优先级设置为最低。
总结一下:

RTOS在多旋翼飞控中的应用情况?-35.jpg
数据原理部分

FreeRTOS数据结构知识

由于FreeRTOS源码中的定义非常的多,为了方便理解,我做了一个思维导图,下面的分析过程都是从根据思维导图来的(思维导图链接:https://github.com/WOLEN6914183/FreeRTOS-1-/tree/master/photo)
其实Freertos本质上就是一个双向链表,该链表的任务有三大属性:任务栈、任务控制块、链表
任务栈
啥是任务栈?
int main()
{
  LED_Init();   //LED初始化
  Delay_Init();  //延时函数
  while(1)
  {
    LED0=!LED0;
    delay(5000);
  }
}

如上图,在前面我们说过在裸机的系统中想点亮一个LED灯通常是在main()函数中先进行LED的初始化后接一个while(1)的死循环,在这个循环中CPU就会不断的将LED=!LED,以及delay(5000)读到SRAM中,然后再将这个两段代码交给CPU进行运算后,最后通过控制总线控制某一个IO地高低电平输出。将while循环中的数据压到SRAM的栈中,如果将while循环中的数据集中放在SRAM一段连续的地址中,我把这段地址给个新名称为“任务栈”,而Freertos将SRAM划分很多这种栈(多个while的死循环)并且把它进行集中管理。
StartTask_Handler=xTaskCreateStatic((TaskFunction_t )start_task,  //任务函数
          (const char*  )"start_task",  //任务名称
          (uint32_t   )START_STK_SIZE, //任务堆栈大小
          (void*      )NULL,           //传递给任务函数的参数
          (UBaseType_t  )START_TASK_PRIO,  //任务优先级
          (StackType_t*   )StartTaskStack, //任务堆栈
          (StaticTask_t*  )&StartTaskTCB); //任务控制块
FreeRTOS中的栈有啥?
从裸机的例子知道,裸机中的任务栈没有进行管理,Freertos是多任务的处理也代表要进行任务栈的管理。那任务栈有啥?如上图任务栈主要有6大部分:任务名称、任务栈大小、传递任务函数的参数、任务的优先级、任务堆栈、任务控制块

  • 任务的名称:一段字符串,函数名字就是CPU进入某个任务栈的地地址。
  • 任务堆栈的大小:任务栈是一段连续地址本质上就是一个数组,数组总有大小。
  • 任务优先级:Freertos任务的运行是抢占式的,因此同一时刻有且只有一个任务在运行只不过切换任务过快肉眼看来是一起在运行。因此,就要有个先来后到,优先级高的任务栈先运行后运行低优先级的。
  • 任务堆栈:任务栈是一个数组,那么用什么表示数组?那无疑是数组的名字同理也是数组的首地址。通过操作数组名(指针)来管理无疑是最好的。
  • 任务控制块:我把它当作“任务栈的保姆”,假如我们想在运行到一段时间后改变的任务的优先级时候就可以通过任务控制块来修改。
任务控制块(TCB)
根据FreeRTOS源码我将任务控制块主要分为了三部分属性。
typedef struct tskTaskControlBlock
{
  /*****************************************************任务属性*********************************************************/
volatile StackType_t *pxTopOfStack; /*栈顶指针 */

#if ( portUSING_MPU_WRAPPERS == 1 )
  xMPU_SETTINGS xMPUSettings;  /*由于没有初始化到MPU暂时不讨论*/
#endif
ListItem_t   xStateListItem;     /*任务的状态,不同的状态(就绪、延时、挂起)会挂在不同的链表上*/
ListItem_t   xEventListItem;  /*事件链表项,会挂接到不同事件链表下。*/
UBaseType_t   uxPriority;   /*优先级,值大优先级越高*/
StackType_t   *pxStack;   /*指向堆栈起始位置,这只是单纯的一个分配空间的地址,可以用来检测堆栈是否溢出*/
char    pcTaskName[ configMAX_TASK_NAME_LEN ];/*任务的名字*/
  /**********************************************************************************************************************/

  /**************************************************调试属性************************************************************/
#if ( portSTACK_GROWTH > 0 )
  StackType_t  *pxEndOfStack;     /*指向栈尾,可以用来检测堆栈是否溢出*/
#endif

#if ( portCRITICAL_NESTING_IN_TCB == 1 )
  UBaseType_t  uxCriticalNesting; /*记录临界段的嵌套层数*/
#endif

     // 跟踪调试用的变量
#if ( configUSE_TRACE_FACILITY == 1 )
  UBaseType_t  uxTCBNumber;  
  UBaseType_t  uxTaskNumber;  
#endif
   
    // 任务优先级被临时提高时,保存任务原本的优先级
#if ( configUSE_MUTEXES == 1 )
  UBaseType_t  uxBasePriority;  /*<最后分配给任务的优先级 - 由优先级继承机制使用. */
  UBaseType_t  uxMutexesHeld;
#endif

     // 任务的一个标签值,可以由用户自定义它的意义,例如可以传入一个函数指针可以用来做Hook(钩子)函数调用
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
  TaskHookFunction_t pxTaskTag;
#endif

    // 任务的线程本地存储指针,可以理解为这个任务私有的存储空间
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
  void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif

   // 运行时间变量
#if( configGENERATE_RUN_TIME_STATS == 1 )
  uint32_t  ulRunTimeCounter;
#endif
  /*************************************************************************************************************************/

  /********************************************************通知属性**********************************************************/
    // 支持NEWLIB的一个变量
#if ( configUSE_NEWLIB_REENTRANT == 1 )
  struct _reent xNewLib_reent;
#endif

    // 任务通知功能需要用到的变量
#if( configUSE_TASK_NOTIFICATIONS == 1 )
        // 任务通知的值
  volatile uint32_t ulNotifiedValue;
        // 任务通知的状态
  volatile uint8_t ucNotifyState;
#endif

    // 用来标记这个任务的栈是不是静态分配的
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
  uint8_t ucStaticallyAllocated;   
#endif

     // 延时是否被打断
#if( INCLUDE_xTaskAbortDelay == 1 )
  uint8_t ucDelayAborted;
#endif
  /****************************************************************************************************************************/

任务属性
任务属性就是TCB的组成,如上图我截取到FreeRTOS源码中TCB_t的结构。

  • 栈顶指针:在堆栈初始化的时候用到
  • 任务状态:任务状态有:运行态、就绪态、延时态、挂起态
  • 事件链表项:于链表相关,在下面介绍链表的时候会细说。
  • 优先级:来自于任务栈。
  • 堆栈起始地址:来自于于任务栈
  • 任务的名字:来自于任务栈
调试属性

  • 可视化跟踪:检查FreeRTOS当前控制块的数量以及任务的数量
  • 运行时间:用于检查CPU当前的占用率
通知属性

  • 互斥信号量:包括互斥体于当前优先级这部分在后续的信号量应用再细说。
  • 任务通知:用于检查CPU当前的占用率
  • 钩子函数:检测内存用到,后面细说。
链表
列表项
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE//检查数据完整性   
  configLIST_VOLATILE TickType_t xItemValue;   
  struct xLIST_ITEM * configLIST_VOLATILE pxNext;  
  struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
  void * pvOwner;               
  void * configLIST_VOLATILE pvContainer;   
  listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE//检查数据完整性     
};
typedef struct xLIST_ITEM ListItem_t;


RTOS在多旋翼飞控中的应用情况?-36.jpg
如上图列表项主要由5部分组成

  • xItemValue:列表排序用,记录链表中第几个任务。
  • pxPrevious: 指向前一个任务
  • pxNext:指向下一个任务
  • pxOwner:当前任务所属的TCB
  • pxContainer:当前任务所属的链表
列表项的初始化
/*******************************************
*  函数名:vListInitialiseItem
*  参  数:pxItem
*  返  回:无
*  功  能: 列表项的初始化(节点初始化)
*  步  骤:
*      1, 将节点的前后指针指向本身
*      2, 将链表指向为空
* ******************************************/
void vListInitialiseItem( ListItem_t * const pxItem )
{
    pxItem->pvContainer = NULL; //将节点指向的列表为空
    listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );//即是将第一个节点指向本身
    listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );//即是将第二节点(下一个节点)指向本身
}

RTOS在多旋翼飞控中的应用情况?-37.jpg
从上面所述可知列表项本质上就是一个变量名为ListItem_t的结构体,为了方便理解我将结构体变量以及结构体内的成员的地址分别用0000-0005来表示。

  • 列表项初始化函数会传入列表项结构体变量的指针指向结构体变量ListItem_t(也就是地址0000)处
  • xItemValue的值等于0
  • pxNext与pxPrevious本质上是与ListItem_t相同数据类型(xLIST_ITEM)的指针,因为初始化的时候只有它自己一个任务所以pxNext与pxPrevious指向同一个地址ListItem_t(也就是地址0000)处
  • pxOwner:指向TCB结构体变量的地址。
迷你列表项
struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE   
configLIST_VOLATILE TickType_t xItemValue;
struct xLIST_ITEM * configLIST_VOLATILE pxNext;
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;


RTOS在多旋翼飞控中的应用情况?-38.jpg
如上图,迷你列表项仅有3部分组成,它用于双向链表的第一个列表项或最后的列表项,不与TCB有关联,其结构存在于链表结构体中因此不需要指向某特定的链表。
链表
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE   
configLIST_VOLATILE UBaseType_t uxNumberOfItems;   
ListItem_t * configLIST_VOLATILE pxIndex;  
MiniListItem_t xListEnd;      
listSECOND_LIST_INTEGRITY_CHECK_VALUE   
} List_t;


RTOS在多旋翼飞控中的应用情况?-39.jpg
链表主要有3个部分组成:

  • uxNumberOfItems:任务数量
  • pxIndex:是一个独立的索引,用来遍历链表。
  • MiniListItem_t:迷你列表项

RTOS在多旋翼飞控中的应用情况?-40.jpg
链表初始化
/***************************************************************
*  函数名 : vListInitialise
*
*  参  数 : pxList
*
*  返  回 : 无
*
*  功  能 :链表初始化
* *************************************************************/

void vListInitialise( List_t * const pxList )
{
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
pxList->xListEnd.xItemValue = portMAX_DELAY;
pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
pxList->uxNumberOfItems = ( UBaseType_t ) 0U;

listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
}


RTOS在多旋翼飞控中的应用情况?-41.jpg
由于迷你列表项位于链表结构体当中所以,链表初始化的过程也代表着迷你列表的初始化。

  • uxNumberOfItems:为统计当前链表中的列表项数量,迷你列表项不统计内,所以uxNumberOfItems为0.
  • 链表中的pxIndex本质上是一个列表类型的指针,可以指向链表中的所有的列表项,它是用来遍历整个链表中的列表项的,链表初始化只有一个列表项所以pxIndex指向迷你列表项结构体变量,再链表结构体中这个变量被定义为ListEnd。
  • 同理pxPrevious与pxNext都会指向ListEnd
  • xItemValue:迷你列表项在链表中既是第一个又是最后一个(双向链表),所以xItemValue的值为最大portMAX_DELAY。
将任务插入就绪列表

RTOS在多旋翼飞控中的应用情况?-42.jpg
FreeRTOS的链表本质上就是一个双向链表,如上图所示。在链表List中插入两个任务
对于链表根列表项目

  • uxNumberOfitems:链表插入了两个任务所以值为2U
  • pxNext:指向了第一个任务。
  • pxPrevious:指向了第二个任务。
对于第一个任务

  • xItemValue:因为这是第一个任务所以xItemValue=1
  • pxNext:指向了第二个任务。
  • pxPrevious:指向了根列表项。
  • pxContainer:指向了当前链表结构体变量的地址
  • pxOwner:指向了任务一所属的TCB1结构体变量的地址
对于第二个任务

  • xItemValue:因为这是第二个任务所以xItemValue=1
  • pxNext:指向了根列表项。
  • pxPrevious:指向了第一个任务。
  • pxContainer:指向了当前链表结构体变量的地址
  • pxOwner:指向了任务一所属的TCB2结构体变量的地址
源码阅读

创建任务


RTOS在多旋翼飞控中的应用情况?-43.jpg
此分析只对任务是如何实现切换进行分析(只讨论思维导图里的任务控制块的任务属性),对于信号量、邮箱、嵌套这些后续会分析
/*********************************************************************************
*  函数名:start_task
*
*  参  数:*pvParameters
*
*  返  回:无
*
*  功  能 1, 开启任务函数
*         2, 将开启任务加入到就绪列表中
*         3.  启动调度器。
* *******************************************************************************/
StartTask_Handler=xTaskCreateStatic((TaskFunction_t )start_task,  //任务函数
                                          (const char*  )"start_task",  //任务名称
                                          (uint32_t   )START_STK_SIZE, //任务堆栈大小
                                          (void*      )NULL,    //传递给任务函数的参数
                                          (UBaseType_t  )START_TASK_PRIO,  //任务优先级
                                          (StackType_t*   )StartTaskStack, //任务堆栈
                                          (StaticTask_t*  )&StartTaskTCB); //任务控制块
vTaskStartScheduler();//启动任务调度器


RTOS在多旋翼飞控中的应用情况?-44.jpg
如上代码xTaskCreateStatic函数大体流程为将任务栈的信息交给一个TCB。然后,通过TCB挂载到对应的就绪列表中
   /**************************************************************************
    *  函数名 :xTaskCreateStatic
    *
    *  参  数 :pxTaskCode:任务函数 、pcName:任务名称、ulStackDepth:任务堆栈大小
    *
    *          pvParameters:任务参数、uxPriority:任务优先级、puxStackBuffer:任务堆栈
    *
    *          pxTaskBuffer:任务控制块
    *  功  能 :任务的创建
    * ***********************************************************************/
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
         const char * const pcName,
         const uint32_t ulStackDepth,
         void * const pvParameters,
         UBaseType_t uxPriority,
         StackType_t * const puxStackBuffer,  /*任务栈*/
         StaticTask_t * const pxTaskBuffer )  /*任务控制块 */
{
        TCB_t *pxNewTCB; /*******************************************************************(1)*/
        TaskHandle_t xReturn;

        configASSERT( puxStackBuffer != NULL );  //任务栈
        configASSERT( pxTaskBuffer != NULL );    //任务控制块

        if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
        {
            pxNewTCB = ( TCB_t * ) pxTaskBuffer;  /********************************************(2)*/
            pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;/*****************************(3)*/

            /*用来标记任务是动态创建的还是静态创建的,如果是静态创建的此变量就为pdTURE,如果是动态创建的就为pdFALSE*/
            #if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
            {
                /*标识这个任务控制块和栈内存时静态的删除任务的时候, 系统不会做内存回收处理*/
                pxNewTCB->ucStaticallyAllocated = tskSTATICALLY_ALLOCATED_STACK_AND_TCB;
            }
            #endif

            /*初始化程序控制块*/
            prvInitialiseNewTask( pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, &xReturn, pxNewTCB, NULL );/*(4)*/
            /*把新任务插入就绪链表*/
            prvAddNewTaskToReadyList( pxNewTCB );/*************************************(5)*/
        }
        else xReturn = NULL;

        return xReturn;
}

#endif


RTOS在多旋翼飞控中的应用情况?-45.jpg
实现任务的就绪就必须将任务栈的内容挂载到某个链表中的过程,我们可以划分为5部分来理解。

  • 1,创建TCB_t类型的指针变量pxNewTCB。如上面代码(1)处。-->TCB_t *pxNewTCB;
  • 2,将任务栈中的TCB控制块StartTaskStack(pxTaskBuffer)转为TCB_t类型的指针,赋值新建的pxNewTCB。如上面代码(2)处.--> pxNewTCB = ( TCB_t * ) pxTaskBuffer;
  • 3, 将任务堆栈的任务栈赋值给TCB->pxStack. 如代码(3)处--> pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
  • 4, 初始化任务控制块,如上代码(4). -->prvInitialiseNewTask( pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, &xReturn, pxNewTCB, NULL );/(4)/
  • 5, 将任务控制块插入到就绪列表中。如上代码(5)--> prvAddNewTaskToReadyList( pxNewTCB );
TCB(任务控制块初始化)代码初始化实现
/********************************************************************************************
* 函数名 :prvInitialiseNewTask
*
* 参  数 :pxTaskCode
*
* 参  数 :pxTaskCode:任务函数、pcName:任务名称、ulStackDepth:堆栈大小、uxPriority:任务优先级
*         pxCreatedTask:任务创建的返回值 pxNewTCB:任务控制块(内存),xRegions:传入的为空值
*         
* 功  能 :任务初始化
* 调  用 :任务的动态创建函数、
* ******************************************************************************************/
static void prvInitialiseNewTask(  TaskFunction_t pxTaskCode,
         const char * const pcName,
         const uint32_t ulStackDepth,
         void * const pvParameters,
         UBaseType_t uxPriority,
         TaskHandle_t * const pxCreatedTask,
         TCB_t *pxNewTCB,
         const MemoryRegion_t * const xRegions
                                )
{
StackType_t *pxTopOfStack;   /************************************************(1)*/
UBaseType_t x;

#if( portUSING_MPU_WRAPPERS == 1 )   //portUSING_MPU_WRAPPERS位于portmacro.h中定义,然而在task文件中并未
                                         //初始化所以默认值为0,因此任务创建没有开启MPU所以暂时不分析
  /* 如果开启了 MPU, 判断任务是否运行在特权模式 */
  BaseType_t xRunPrivileged;
  if( ( uxPriority & portPRIVILEGE_BIT ) != 0U )
  {
     /* 优先级特权模式掩码置位,任务运行在特权模式 */
   xRunPrivileged = pdTRUE;
  }
  else
  {
   xRunPrivileged = pdFALSE;
  }
  uxPriority &= ~portPRIVILEGE_BIT;
#endif

  /* 如果使能了堆栈溢出检测功能或者可视化跟踪调试功能 */
  /*
   * void *memset(void *s ,int ch,size_t n)
  * 将s指向的某一内存的前n个字节全部设置为ch的ASII值
  * 此处为将
        * FreeRTOSconfig中设置:
        * #define configCHECK_FOR_STACK_OVERFLOW  0  大于0时启用堆栈溢出检测功能
        * #define configUSE_TRACE_FACILITY        1  启用可视化跟踪调试
        *
        * FreeRTOS.h中设置
        *
        * INCLUDE_uxTaskGetStackHighWaterMark     0  为1的时候检测任务栈的剩余量
   */
#if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) || ( configUSE_TRACE_FACILITY == 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark == 1 ) )
{
  /* 将申请的堆栈用tskSTACK_FILL_BYTE填充 ,其中tskSTACK_FILL_BYTE默认为 0xa5U */

           /* 将任务控制块与任务堆栈进行数据类型的转换并赋予pxNewTCB与
            *  pxNewTCB->pxStack 目的是将着任务栈与控制块进行内存的分配管理
   *  pxNewTCB = ( TCB_t * ) pxTaskBuffer;
   *  pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
            */
         /* 在上述的任务创建的过程中我们将主函数中传中用户传进来的任务堆栈赋给了任务控制块中的任务栈用于内存的申请
          * 此函数就是根据用户的传入的堆栈进行内存的分配(扩展:任务栈本质上就是一个数组)
          *
          * 此函数就是将pxNewTCB->pxStack[任务栈](数组)的前{( size_t ) ulStackDepth * sizeof( StackType_t )}个字节
          * (主函数设置堆栈大小)全部填充为tskSTACK_FILL_BYTE的ASII码( 0xa5U)。
          * */
  ( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
}
#endif

    /*计算栈顶地址。这取决于堆栈是否
     *从高内存增长到低内存(按照 80x86),反之亦然。
     *portSTACK_GROWTH 用于根据需要使结果为正或负
     *由港口*/
#if( portSTACK_GROWTH < 0 )   //Cortex-M3 堆栈的地址是向下生长的
{
  /*
   * 向下增长,计算堆栈栈顶,栈顶在内存的高位
   * pxNewTCB->pxStack固定为申请的堆栈起始位置,即对于向下增长的堆栈需要计算堆栈栈顶
   * pxTopOfStack随着堆栈的分配不断变化。
   * pxNewTCB->pxStack为uint32_t类型的指针,若指针加1,移动4个地址位
         * 问:为什么要堆栈深度地址-1获取栈顶地址?
         * 答:我们说过任务栈的本质是一个数组如果这个任务的堆栈大小设置为100,那么任务栈可以简单表示为Stack[99];
         *     数组为0开始要表示100的深度数组最大的地址处就是Stack[99]处,所以下面要进行 ulStackDepth - ( uint32_t ) 1
         *     操作赋于栈顶的指针
   */
  pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 ); //赋予栈顶地址位置为下面的堆栈空间分配做准备
   /*
   * 8字节对齐
   * 为了后续兼容浮点(64位)运行,向下8字节对齐
   * 假如pxTopOfStack是36,能被4整除,但不能整除8,进行向下8字节对齐就是32,那么就会空出4个字节不使用
         *
         * portBYTE_ALIGNMENT_MASK在portable.h文件中定义h当pxTopOfStack为8字节对齐则#define portBYTE_ALIGNMENT_MASK ( 0x0007 )
         * portPOINTER_SIZE_TYPE 为uint32_t 意思为将portBYTE_ALIGNMENT_MASK转换为32位。然后将栈顶指针(pxTopOfStack)和portBYTE_ALIGNMENT_MASK
         * 进行按位与操作.
   */
  pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

  /* (断言) 检查申请的堆栈是否符合8字节对齐。 与操作后在进行断言,判断栈顶指针的值是否为0UL(无符号长整型) */
  configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
}
#else /* portSTACK_GROWTH */
{
   /* 向上增长的堆栈,计算堆栈栈顶,栈顶在内存的低位 */
  pxTopOfStack = pxNewTCB->pxStack;

  /* (断言) 检查申请的堆栈是否符合8字节对齐 */
  configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxNewTCB->pxStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );

  /* 对于堆栈向上增长,设置上边界,用于检验堆栈是否溢出 */
  pxNewTCB->pxEndOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
}
#endif /* portSTACK_GROWTH */

/* 将任务名字保存到任务控制块中,方便调试 */
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
  pxNewTCB->pcTaskName[ x ] = pcName[ x ];

        /* 如果字符串短于,则不要复制所有 configMAX_TASK_NAME_LEN
        configMAX_TASK_NAME_LEN 字符以防万一之后的内存
        字符串不可访问(极不可能)。 */
  if( pcName[ x ] == 0x00 )
  {
   break;
  }
  else
  {
            /*************************************************
             * 每次有一个 if() 没有 else 时,我们可以以更自动化的
             * 方式看到代码是否采用了 if() 评估为 true 和 if()
             * 评估为的路径false - 而不仅仅是一条路径。覆盖测试未发布,
             * 但通用编码标准可在此处找
             * ************************************************/
   mtCOVERAGE_TEST_MARKER();
  }
}

/* 确保TCB中任务名字的最后一个字节为'\0',可以看出任务名字超出范围,将会无用 */
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';

/* 如果任务优先级大于或等于设置的最大优先级(32),限制任务优先级
     * UBaseType_t为 unsigned long类型 1U:无符号整型1
     * ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U操作后
     * 优先级变为31.
     */
if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
{
  uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
}
else
{
  mtCOVERAGE_TEST_MARKER(); /*与上面同理*/
}

pxNewTCB->uxPriority = uxPriority; /*将处理后的任务优先级赋给任务控制块优先级*/

/* 如果使用任务互斥量信号 (互斥信号量主要解决的就是共享资源访问的问题)
     * 比如A任务进行共享资源访问的时候,锁定次数就会+1,此时任务B占据信号量就会发生嵌套此时锁定次数就会再+1,
     * 接着任务B释放后锁定次数-1,然后任务A释放锁定次数-1,最后信号量完全被释放。
     * ******************************************************************/
#if ( configUSE_MUTEXES == 1 )
{
  pxNewTCB->uxBasePriority = uxPriority;
  pxNewTCB->uxMutexesHeld = 0;
}
#endif /* configUSE_MUTEXES */
   
    /******************************************************************************
     *  以下是对链表的操作:
     *  一,节点的初始化:FreeRTOS中一个节点包括什么元素?
     *  struct xLIST_ITEM    //节点结构体
     *   {
     *     TickType_t xItemValue;           // 节点的辅助值     
     *     struct xLIST_ITEM * pxNext;      // 下一节点
     *     struct xLIST_ITEM * pxPrevious;  // 上一节点
     *     void * pvOwner;                  // 节点归属于哪个内核TCB
     *     void * pvContainer;              // 节点归属于哪个链表
     *  };
     * 二 、任务控制块:什么是任务控制块,任务控制块可以理解为存储任务特性的数据结构
     *     任务具有状态、堆栈、优先级等特性,这些特性需要存储起来供系统使用
     *     比如需要通过任务控制块中存储的优先级判断任务切换的时候应该轮到那个任务具体的结构
     *     可以访问链接:https://blog.csdn.net/Hxj_CSDN/article/details/85165270
     * 三、链表,freertos中有四大运行状态:就绪态,运行态,延时态,挂起。其中就绪态、延时态
     *          与挂起态都有相应的队列。任务处于那种状态就必须通过TCB控制块将任务插入到相应的
     *          状态列表中(就绪、延时、挂起)
     * ***************************************************************************/

    /**********************************列表项操作*********************************************
     * 1,列表项的初始化
     * 2,列表项所属TCB
     * *********************************************************************************/
vListInitialiseItem( &( pxNewTCB->xStateListItem ) ); /* 初始化任务状态列表项(节点初始化,第一个节点与最后的节点都指向本身) */
vListInitialiseItem( &( pxNewTCB->xEventListItem ) ); /* 初始化任务事件列表项 */
/*
  * 将任务状态列表项的 pvOwner 指向所属的TCB
     * 将状态列表项挂载到pxNewTCB节点
  * 如任务切换到运行态,系统从就绪列表取出这一项,获得TCB(ListItem->pvOwner),切换到运行状态
  */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
     
    /****************************************TCB操作******************************************
     *  1, 确定列表项的优先级存入TCB中
     *  2, 初始化临界段嵌套层数,任务标签,事件统计,MPU
     *  3, 是否使用钩子函数
     *  4, 是否使用局部数据指针
     *  5, 是否使用通知功能
     *  6, 是否使用Newlib
     *  7, 是否使用延时打断功能
     * **************************************************************************************/

/*
  * 给任务事件列表项中成员xItemValue赋项值,等于configMAX_PRIORITIES - uxPriority。
  * 该值用于在对应事件列表中排序,按照升序排序,写入优先级的 “补数”,因此优先级越高越靠前
  * 即保证优先级高的任务,在列表中越靠前。
  */
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
/* 将事件列表项的 pvOwner 指向所属的TCB  */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );
  /* 初始化嵌套值为0 */
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
{
  pxNewTCB->uxCriticalNesting = ( UBaseType_t ) 0U;
}
#endif /* portCRITICAL_NESTING_IN_TCB */
  /* 如果使能任务标签功能 */
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
{
  pxNewTCB->pxTaskTag = NULL;
}
#endif /* configUSE_APPLICATION_TASK_TAG */
  /* 如果使能事件统计功能 */
#if ( configGENERATE_RUN_TIME_STATS == 1 )
{
  pxNewTCB->ulRunTimeCounter = 0UL;
}
#endif /* configGENERATE_RUN_TIME_STATS */
  /* 如果使用MPU功能 */
#if ( portUSING_MPU_WRAPPERS == 1 )
{
   /* 设置 MPU,任务内存访问权限设置 */
  vPortStoreTaskMPUSettings( &( pxNewTCB->xMPUSettings ), xRegions, pxNewTCB->pxStack, ulStackDepth );
}
#else
{
  /* 避免编译报 warning 没有使用变量 */
  ( void ) xRegions;
}
#endif
  /* 初始化任务局部数据指针 */
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0 )
{
  for( x = 0; x < ( UBaseType_t ) configNUM_THREAD_LOCAL_STORAGE_POINTERS; x++ )
  {
   pxNewTCB->pvThreadLocalStoragePointers[ x ] = NULL;
  }
}
#endif
  /* 如果使用了任务通知功能,初始化任务消息通知变量 */
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
{
  pxNewTCB->ulNotifiedValue = 0;
  pxNewTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
#endif
  /* 如果使用Newlib */
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
  _REENT_INIT_PTR( ( &( pxNewTCB->xNewLib_reent ) ) );
}
#endif

#if( INCLUDE_xTaskAbortDelay == 1 )
{
  pxNewTCB->ucDelayAborted = pdFALSE;
}
#endif

/*
  * 初始化栈空间
  * 执行一次该函数,相当于被调度器中断切换其它任务,而原函数入栈做了现场保护
  * 当任务再次被调度器取出后,可以直接执行出栈恢复现场,运行任务
  */
#if( portUSING_MPU_WRAPPERS == 1 )
{
  pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters, xRunPrivileged );
}
#else /* portUSING_MPU_WRAPPERS */
{
  pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
}
#endif /* portUSING_MPU_WRAPPERS */

if( ( void * ) pxCreatedTask != NULL )
{
  /* 让任务句柄指向任务控制块,可用于修改优先级,通知或者删除任务等 */
  *pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
}
else
{
  mtCOVERAGE_TEST_MARKER();
}
}

任务控制块的初始化代码比较多,我大体说一下它的流程。

RTOS在多旋翼飞控中的应用情况?-46.jpg
如上图,在任务栈初始化的时候已经将任务栈的内的元素(任务栈名字、栈大小)等通过地址保存在任务控制块上。在前面我们所过,任务控制块就如任务的保姆,可以通过任务控制块实现压栈、加入任务列表等操作。而任务控制块初始化的过程除了对任务栈的元素处理外最重要的就是压栈操作。其压栈过程如下图。

RTOS在多旋翼飞控中的应用情况?-47.jpg
讲这部分前再啰嗦介绍一点特殊寄存器中的功能。

  • xPSR:状态字寄存器。
  • PC:表被打断前任务指令即将执行指令的的地址,用于返回原来地址继续执行。
  • LR:表示本次任务执行完退出后该执行的地址,举例如下:若函数A调用了函数B,则在进入函数B之前PC内存了函数A下次将执行指令的地址,LR存储了函数A结束时该执行的地址。此时要进入B执行,在进入B执行前会将包括LR(但不包括PC)在内的部分寄存器入栈,然后进入函数B,此时LR更新为进入函数前的PC需要指的下一个指令地址,PC更新为函数B起始指令,LR会一直保持下去直到在调用其他函数,从而再次进栈,然后更新为B函数若不调用其他函数的话,下一个该执行的指令地址。当函数退出时,LR内存的指令地址会传给PC,从而PC接着调用函数B之前的指令执行。然后LR的值出栈。对于中断则有所不同,当函数A正在执行时,中断来临,此时会类似于调用函数B,会将寄存器入栈,但此时入栈的寄存器不仅包含LR还包含了PC,进入中断不同于调用函数B,LR会更新为一个表示中断结束后返回用户模式还是特权模式。当任务结束后向LR跳转就会根据LR的值到响应模式运行,其对应的值类型如下

RTOS在多旋翼飞控中的应用情况?-48.jpg
先初始化栈顶指针

RTOS在多旋翼飞控中的应用情况?-49.jpg
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
interrupt. */
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) pxCode; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11..R4. */

return pxTopOfStack;
}

上面堆栈初始化代码分析
堆栈是用来在进行上下文切换的时候保存现场的,一般在新创建好一个堆栈以后会对其先进行初始化处理,即对Cortex-M内核的某些寄存器赋初值。这些初值就保存在任务堆栈中,保存的顺序按照:XPSR、R15(PC)、R14(LR)、R12、R3 ~ RO、R11 ~ R14。

  • (1)、寄存器xPSR值为portINITIAL_XPSR,其值为0×01000000。xPSR是CPU内核的特殊寄存器,叫做程序状态寄存器,0x01000000表示这个寄存器的bit24为1,表示处于Thumb状态,即使用的Thumb指令。
  • (2)、寄存器PC初始化为任务函数pxCode。
  • (3)、寄存器LR初始化为函数prvTaskExitError。
  • (4)、跳过4个寄存器,R12、R3、R2、R1,这四个寄存器不初始化。
  • (5)、寄存器R0初始化为pvParameters,一般情况下,函数调用会将R0~R3作为输入参数, R0也可用作返回结果,如果返回值为64位,则R1也会用于返回结果,这里的pvParameters是作为任务函数的参数,保存在寄存器R0中。
  • (6)、保存EXC_RETURN值,用于退出SVC或PendSV中断的时候处理器应该处于什么态。处理器进入异常或中断服务程序(ISR)时,链接寄存器R14(LR)的数值会被更新为 EXC_RETURN数值,之后该数值会在异常处理结束时触发异常返回。这里人为的设置为OXFFFFFFD,表示退出异常以后CPU进入线程模式并且使用进程栈!
  • (7)、跳过8个寄存器,R11、R10、R8、R7、R6、R5、R4。

RTOS在多旋翼飞控中的应用情况?-50.jpg
上图栈顶初始化后的结果,注意栈顶初始化结构是通过栈顶指针来实现的,此时的操作只是为任务压入CPU内部寄存器前对相应的寄存器进行相应的初始化,本质上上图结构仍旧存在于flash中。
将任务插入到就绪列表

RTOS在多旋翼飞控中的应用情况?-51.jpg
/*****************************************************************************
* 函数名 :prvAddNewTaskToReadyList
*
* 参  数 :pxNewTCB
*
* 返  回 :无
*
* 注  意 :静态函数只能在本文件中调用与声明
*
* 说  明 :列表项插入某个列表中是由任务控制块实现的,将任务项目插入就绪列表中
*
* 步  骤 :1,进入临界区 2,任务数量+1
* ***************************************************************************/
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
{
/*进入临界区*/
taskENTER_CRITICAL();
{
  uxCurrentNumberOfTasks++;
  if( pxCurrentTCB == NULL ) //如果没有任务这个就是第一个任务,创建任务列表
  {
   /* There are no other tasks, or all the other tasks are in
   the suspended state - make this the current task. */
   pxCurrentTCB = pxNewTCB;

   if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
   {
    /* 这是要创建的第一个任务,所以做初步需要初始化。 */
    prvInitialiseTaskLists();
   }
   else
   {
    mtCOVERAGE_TEST_MARKER();//防止出现没有if操作的现象
   }
  }
  else
  {
   /* If the scheduler is not already running, make this task the
   current task if it is the highest priority task to be created
   so far. */
   if( xSchedulerRunning == pdFALSE )
   {
    if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
    {
     pxCurrentTCB = pxNewTCB;
    }
    else
    {
     mtCOVERAGE_TEST_MARKER();
    }
   }
   else
   {
    mtCOVERAGE_TEST_MARKER();
   }
  }

  uxTaskNumber++;

  #if ( configUSE_TRACE_FACILITY == 1 )
  {
   /* Add a counter into the TCB for tracing only. */
   pxNewTCB->uxTCBNumber = uxTaskNumber;
  }
  #endif /* configUSE_TRACE_FACILITY */
  traceTASK_CREATE( pxNewTCB );

  prvAddTaskToReadyList( pxNewTCB ); //将TCB添加到就绪列表

  portSETUP_TCB( pxNewTCB );
}
taskEXIT_CRITICAL(); //退出临界区

if( xSchedulerRunning != pdFALSE )
{
  /* 如果新创建任务的优先级大于当前的优先级则下一个运行的就是新创建的任务。
        If the created task is of a higher priority than the current task
  then it should run now. */
  if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )
  {
   taskYIELD_IF_USING_PREEMPTION();
  }
  else
  {
   mtCOVERAGE_TEST_MARKER();
  }
}
else
{
  mtCOVERAGE_TEST_MARKER();
}
}


/***********************************************************************
*  函数名 :prvInitialiseTaskLists()
*
*  参  数 :无
*
*  返  回 :无
*
*  作  用 :就绪列表初始化 ,以下的队列初始化后依然是列表项
* ************************************************************************/

static void prvInitialiseTaskLists( void )
{
UBaseType_t uxPriority;

for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
{
  vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );  //将32个优先级放进就绪列表中(存储所有的优先级)
                                                                  //方便以后调用,现在本质上依然是一个列表项,任务在创建的时候,
        //会根据任务的优先级将任务插入到就绪列表不同的位置,相同优先级的任务插入到就绪列表里面的同一条链表中。
}

   /***************************************************
    *  前面分析过Frertos有三种状态是需要队列来进行管理的
    *  分别是挂起态、阻塞态、就绪态。其中就绪态本质上就是
    *  阻塞和延时所以会有两个延时队列
    * ************************************************/
vListInitialise( &xDelayedTaskList1 );  //初始化延时队列1
vListInitialise( &xDelayedTaskList2 );  //初始化延时队列2
vListInitialise( &xPendingReadyList );  //初始化挂起队列调度程序挂起时已准备好的任务。
                                            //当调度程序恢复时,它们将被移动到就绪列表中。

#if ( INCLUDE_vTaskDelete == 1 )   //任务的删除
{
  vListInitialise( &xTasksWaitingTermination );  //任务已经删除但是内存还没有释放
                                                   //xTasksWaitingTermination为回收等待列表
                                                   //中任务的堆栈和任务控制块内存
}
#endif /* INCLUDE_vTaskDelete */

#if ( INCLUDE_vTaskSuspend == 1 )  //任务的挂起
{
  vListInitialise( &xSuspendedTaskList );   //任务暂停的列表。
}
#endif /* INCLUDE_vTaskSuspend */

    //pxDelayedTaskList指向当前正在使用的延迟任务列表。
pxDelayedTaskList = &xDelayedTaskList1;
    //指向当前用于保存已溢出当前滴答计数的任务的延迟任务列表。(就绪)
pxOverflowDelayedTaskList = &xDelayedTaskList2;
}

注意:这里初始化了32个优先级列表的原因是,在启动调度器的时候根据程序写的任务优先级而插入到优先级列表中的。

RTOS在多旋翼飞控中的应用情况?-52.jpg

RTOS在多旋翼飞控中的应用情况?-53.jpg
如上图,将通过判断找出当前最大优先级或者是第一次运行的任务后将任务控制块添加到就绪列表中。在PendSV的处理函数中我们也可以发现pxCurrentTCB来自于外部被用于PendSV异常处理时候实现任务切换。

RTOS在多旋翼飞控中的应用情况?-54.jpg

RTOS在多旋翼飞控中的应用情况?-55.jpg
如上图,通过TCB将任务插入到就绪列表中,其本质就是根据优先级插入相应得优先级列表中。

RTOS在多旋翼飞控中的应用情况?-56.jpg
如上图就是任务初始化的过程框图。其中的判断已经被我省去讨论。
启动调度器


RTOS在多旋翼飞控中的应用情况?-57.jpg
启动调度器的过程:前面我们已经将任务的栈初始化好了,并且初始化里相应的任务所需要的队列并且根据优先级将任务插入到相应的就绪列表中,接下来就是启动任务调度器。其要实现的内容如上图,Cortex-M3/M4内核实现过程基本一致。

RTOS在多旋翼飞控中的应用情况?-58.jpg
如上图,假如在任务创建阶段,通过栈顶指针在Flash中建立了两任务,如何实现任务A运行完毕顺利切换到任务B的设置就是启动调度器需要实现的。


如上图,是一般操作系统实现的过程(前面也提及过)。

  • 首先设置PendSV异常与系统定时器的优先级最低。
  • 接着从用户程序进入SVC异常(从主函数中触发SVC异常)。
  • SVC异常后模式就是特权级,此时要判断是否有比PendSV与系统定时器高的中断或异常。
  • 如果没有发出任务切换请求直接去处理高优先级的中断
  • 如果有任务切换请求去处理高优先级的中断同时退出SVC异常触发PendSV异常将切换任务悬挂高优先级中断完成后处理任务切换。我只分析关键的PenSV实现任务切换的过程

RTOS在多旋翼飞控中的应用情况?-60.jpg

RTOS在多旋翼飞控中的应用情况?-61.jpg

RTOS在多旋翼飞控中的应用情况?-62.jpg

RTOS在多旋翼飞控中的应用情况?-63.jpg
如上图,上电复位后程序运行到了主函数,在启动调度器器中,不难发现它进行了PendSV和系统滴答定时器的优先级设置为最低,接着产生了一个定时器服务函数与第一个任务运行函数的处理。
__asm void prvStartFirstTask( void )
{
    /* Cortext-M3硬件中,0xE000ED08 地址处为VTOR(向量表偏移量)寄存器,存储     向量表起始地址*/
    PRESERVE8
    /* 将 0xE000ED08 加载到 R0 */
    ldr r0, =0xE000ED08   
    /* 将 0xE000ED08 中的值,也就是向量表的实际地址加载到 R0 */
    ldr r0, [r0]
    /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针MSP的初始值*/
    ldr r0, [r0]   

    /* 将堆栈地址写入主堆栈指针 */
    msr msp, r0
    /* 使能全局中断*/
    cpsie i
    cpsie f
    dsb      /* 数据和指令同步隔离,将流水线中的数据和指令全部执行完毕 */
    isb
    /* 调用SVC启动第一个任务 */
    svc 0
    nop
    nop
}


RTOS在多旋翼飞控中的应用情况?-64.jpg

RTOS在多旋翼飞控中的应用情况?-65.jpg
在细说SVC异常前需要知道一点知识,如上图如果主程序在线程模式下运行,并且在使用MSP时被中断,则在服务例程中LR=0xFFFF_FFF9(主程序被打断前的LR已被自动入栈)。如果主程序在线程模式下运行,并且在使用PSP时被中断,则在服务例程中LR=0xFFFF_FFFD(主程序被打断前的LR已被自动入栈)。

RTOS在多旋翼飞控中的应用情况?-66.jpg
同理,如上图如果主程序在Handler模式下运行,则在服务例程中LR=0xFFFF_FFF1(主程序被打断前的 LR已被自动入栈)。这时的“主程序”,其实更可能是被抢占的服务例程。事实上,在嵌套 时,更深层ISR所看到的LR总是0xFFFF_FFF1
SVC异常仅仅在第一次任务启动的时候通过用户程序触发,所以SVC要做处理的问题有

RTOS在多旋翼飞控中的应用情况?-67.jpg

  • 将第一个任务中的数据从存储器中弹栈到CPU内部的寄存器中
  • SVC异常触发会变换为hander模式,退出SVC异常后需要切换到线程模式,同时为了下一步的PendSV异常需要将MSP变为PSP
  • 开始中断。
代码分析

RTOS在多旋翼飞控中的应用情况?-68.jpg
__asm void vPortSVCHandler( void )
{
PRESERVE8                  /*8字节对齐*/
   
ldr r3, =pxCurrentTCB     /*将存储器中pxCurrentTCB(TCB_t * volatile pxCurrentTCB)(地址读到)r3寄存器中*/
    ldr r1, [r3]       /*将r3寄存器所指地址中的数据读取到r1寄存器中,也就是TCB_t(获得了任务控制块) */
    ldr r0, [r1]             /*将r1所指的地址的数据读取到r0寄存器中,(任务控制块的首地址就是栈顶指针)r0获取到了栈顶地址*/
      
    ldmia r0!, {r4-r11}      /* 将r0的地址+4,后从存储器地址上的数据加载到寄存器r4-r11*/
msr psp, r0        /* 写r0指向地址里存储的值到psp,即切换为线程栈*/
isb                         /* 指令同步屏障 */
mov r0, #0                  /* 将0送到r0寄存器中。即r0寄存器存的值为0 */
msr basepri, r0             /* 使寄存器basepri为0,即开启中断 */
orr r14, #0xd               /* r14寄存器中的值与#0xd进行或运算保存后保存到r14寄存器*/
bx r14                      /* 告诉处理器 ISR 完成,需要返回,此刻处理器便会使用 PSP 做为堆栈指针,准备进行进行压栈操作*/
}


  • 首先将pxCurrentTCB 的 地址赋给r3,即 r3 = & pxCurrentTCB ; 然后把pxCurrentTCB 的值赋值给r1,即r1 = pxCurrentTCB 。
  • 最后pxCurrentTCB所指的TCB的第一个成员变量(任务堆栈地址)赋给r0,即r1 = [r3] = *pxCurrentTCB= pxCurrentTCB->pxTopOfStack 把人为入栈的寄存器r4 - r11手动出栈,剩下的 xPSR、PC、LR、R12、R3 - R0会自动出栈。把出栈完成之后的栈顶地址赋给psp,供任务使用。
/***********************************************************
* 函数名 :xPortSysTickHandler
*
* 返  回 :无
*
* 参  数 :无
*
* 功  能 :系统时钟定时器中断函数(任务切换)
* ********************************************************/
void xPortSysTickHandler( void )
{
uint32_t ulDummy;

ulDummy = portSET_INTERRUPT_MASK_FROM_ISR();
{
  /* Increment the RTOS tick. */
  if( xTaskIncrementTick() != pdFALSE )
  {
   /*挂起上下文切换.任务切换 */
   portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
  }
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( ulDummy );
}


RTOS在多旋翼飞控中的应用情况?-69.jpg
此时发生系统时钟定时器,在中断函数中触发PendSV异常。由于在先前的时候触发了SVC异常在CPU的内部寄存器中仍然存在着第一个任务的栈因此,需要将这些内容压入栈中,整个PendSV的内容如上图。其作用就是实现栈A的压栈以及栈B的弹栈。注意:处理R4-R11其它的CPU内部与任务栈相关的会由硬件自动操作(弹栈或压栈)
代码实现如下
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting; /*每个任务在临界嵌套中保持自己的中断状态多变的*/
extern pxCurrentTCB;      /*指向当前激活的任务*/
extern vTaskSwitchContext;

PRESERVE8

mrs r0, psp                 /*将psp寄存器的值读出保存到r0寄存器中*/
isb                         /* 指令同步隔离 */

ldr r3, =pxCurrentTCB  /*将存储器中pxCurrentTCB(TCB_t * volatile pxCurrentTCB)(地址读到)r3寄存器中*/
ldr r2, [r3]                /*将r3所指地址的数据读取到r2寄存器中也就是获得了任务控制块*/

stmdb r0!, {r4-r11}   /*将r0的地址+4,后寄存器上的数据加载到存储数据上(压栈),硬件自动将xPSR,PC,LR,R12,R0-R3自动存入存储器上 */
str r0, [r2]    /* 将任务控制块首地址的值加载到r0寄存器也就是r0获得了任务栈顶*/

stmdb sp!, {r3, r14}        /*将R3,R14临时压入堆栈,因为即将调用函数vTaskSwitch,调用函数时,返回地址自动保存在R14中,一旦调用发生,R14的值将被覆盖,因此需要入栈保护*/
                                /*R3保存当前激活的TCB指针(pxCurrentTCB)地址,函数调用后会用到因此也要入栈保护*/
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /*中断优先级号大于或者等于最大优先级的*/
msr basepri, r0            /* 使寄存器basepri为configMAX_SYSCALL_INTERRUPT_PRIORITY屏蔽中断,表示进入了临界区*/
dsb
isb
bl vTaskSwitchContext     /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换*/
mov r0, #0                /* 将0送到r0寄存器中。即r0寄存器存的值为0 */
msr basepri, r0           /* 使寄存器basepri为0,即开启中断 */
ldmia sp!, {r3, r14}      /* 恢复R3和R14,现在的R3保存变量pxCurrentTCB的地址,变量pxCurrentTCB函数在vTaskSwitchContext中可能已被修改*/
                              /* 指向新的最高优先级就绪任务;R14保存退出异常信息*/

ldr r1, [r3]              /* 将r3所指向地址中的数据给r1,就是获得了任务控制块变量*/
ldr r0, [r1]     /* 将r1首地址的变量给r0寄存器,就是获得了任务控制的栈顶指针变量*/
ldmia r0!, {r4-r11}    /* 将r0的地址+4,后从存储器地址上的数据加载到寄存器r4-r11*/
msr psp, r0               /* 写r0指向地址里存储的值到psp,即切换为线程栈*/
isb                       /* 指令同步隔离 */
bx r14                    /* 告诉处理器 ISR 完成,需要返回,此刻处理器便会使用 PSP 做为堆栈指针,准备进行进行压栈操作*/
nop
}

vTaskSwitchContext实现寻找最高优先级任务以及pxCurrentTCB变量的替换过程如下。

RTOS在多旋翼飞控中的应用情况?-70.jpg
在vTaskSwitchContext函数钟找到taskSELECT_HIGHEST_PRIORITY_TASK函数。

RTOS在多旋翼飞控中的应用情况?-71.jpg
在taskSELECT_HIGHEST_PRIORITY_TASK函数中继续寻找到portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority )函数。
   /*__clz()会被汇编指令CLZ替换掉,这个指令用来计算一个变量最高开始的连续零的个数*/
    /*假如uxTopReadyPriority为0x09(二进制为:0000 0000 0000 0000 0000 0000 0000 1001)即是bit3和bit0都为1*/
    /*表示存在优先级为0和3的就绪任务。则__clz((uxTopReadyPriority))的值为28,uxTopPriority = 31-28=3即优先级为3的任务是就绪态最高优先级任务*/
   
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

在portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority )函数的宏操作上寻找到就绪列表中最高优先级的任务,并把最高优先级的任务的TCB赋给pxCurrentTCB供PendSV实现任务切换。
调度器的所有代码如下
/*****************************************************************************
* 函数名 :vTaskStartScheduler(void)
* 返  回 :无
* 参  数 :无
* 功  能 :启动任务的调度
* ****************************************************************************/
void vTaskStartScheduler( void )
{
BaseType_t xReturn;

  /* 如果使能静态方式创建任务 */
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
  StaticTask_t *pxIdleTaskTCBBuffer = NULL;
  StackType_t *pxIdleTaskStackBuffer = NULL;
  uint32_t ulIdleTaskStackSize;

     /* 申请空闲任务空间,该函数需要用户自己定义 */
  vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
  /* 使用静态方式创建一个空闲任务,优先级为0 */
  xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
            "IDLE",
            ulIdleTaskStackSize,
            ( void * ) NULL,
            ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
            pxIdleTaskStackBuffer,
            pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
    /* 如果申请内存成功 */
  if( xIdleTaskHandle != NULL )
  {
   xReturn = pdPASS;
  }
  else
  {
   xReturn = pdFAIL;
  }
}
#else /* 如果不使用静态方式创建任务 */
{
    /* 使用动态方式创建一个空闲任务 */
    xReturn = xTaskCreate( prvIdleTask,
        "IDLE", configMINIMAL_STACK_SIZE,
        ( void * ) NULL,
        ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
        &xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
    /* 如果使用软件定时器功能 */
#if ( configUSE_TIMERS == 1 )
{
  if( xReturn == pdPASS )
  {
   xReturn = xTimerCreateTimerTask();
  }
  else
  {
   mtCOVERAGE_TEST_MARKER();
  }
}
#endif /* configUSE_TIMERS */

if( xReturn == pdPASS )
{
  /* 中断在此处关闭,以确保不会发生滴答声在调用 xPortStartScheduler() 之前或期间。的堆栈
           创建的任务包含一个状态字,中断打开所以中断将在第一个任务时自动重新启用开始运行. */
  portDISABLE_INTERRUPTS();

  #if ( configUSE_NEWLIB_REENTRANT == 1 )   //没有运行此处
  {
   /*将 Newlib 的 _impure_ptr 变量切换为指向 _reent
                特定于将首先运行的任务的结构。*/
   _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
  }
  #endif /* configUSE_NEWLIB_REENTRANT */

  xNextTaskUnblockTime = portMAX_DELAY;   /* 设置下一个唤醒任务系统节拍为portMAX_DELAY */
  xSchedulerRunning = pdTRUE;             /* 使能任务调度器 */
  xTickCount = ( TickType_t ) 0U;         /* 设置系统时钟片为0 */

  /* If configGENERATE_RUN_TIME_STATS is defined then the following
  macro must be defined to configure the timer/counter used to generate
  the run time counter time base. */
  portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); /* 空代码 */

  /* 设置计时器滴答是特定于硬件的,因此在便携式接口。在此函数中实现PendSVC的操作***重要***/
  if( xPortStartScheduler() != pdFALSE )
  {
   /* 如果调度器启动成功,不会允许到这里 */
  }
  else
  {
   /* 当调用 xTaskEndScheduler()才会运行到这里 */
  }
}
else
{
  /* 如果允许到这里,说明创建空闲任务或定时任务失败,内存分配出错 */
  configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}

  /* 防止编译器警告 */
( void ) xIdleTaskHandle;
}


/******************************************************************
* 函数名 :xPortStartScheduler
*
* 参  数 :无
*
* 返  回 :无
*
* 功  能 :系统时钟处理(内含PendSVC操作)
* *******************************************************************/
BaseType_t xPortStartScheduler( void )
{
configASSERT( ( configMAX_SYSCALL_INTERRUPT_PRIORITY ) );

#if( configASSERT_DEFINED == 1 )
{
         /* 中断优先级寄存器0: PRI_0 */
  volatile uint32_t ulOriginalPriority;
  volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
  volatile uint8_t ucMaxPriorityValue;

       /* 确定一个最高ISR优先级,在这个ISR或者更低优先级的ISR中可以安全的调用以FromISR结尾的API函数.*/
       /* 保存中断优先级值,因为下面要覆写这个寄存器(PRI_0) */
        ulOriginalPriority = *pucFirstUserPriorityRegister;
        /* 确定有效的优先级位个数. 首先向所有位写1,然后再读出来,由于无效的优先级位读出为0,
        然后数一数有多少个1,就能知道有多少位优先级.*/
  *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

  /* Read the value back to see how many bits stuck. */
  ucMaxPriorityValue = *pucFirstUserPriorityRegister;

  /* Use the same mask on the maximum system call priority. */
         /* 冗余代码,用来防止用户不正确的设置RTOS可屏蔽中断优先级值 */
  ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

  /* Calculate the maximum acceptable priority group value for the number
  of bits read back. */
        
        /* 计算最大优先级组值 */
  ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
  while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
  {
   ulMaxPRIGROUPValue--;
   ucMaxPriorityValue <<= ( uint8_t ) 0x01;
  }

  /* Shift the priority group value back to its position within the AIRCR
  register. */
        
        /* 将PRI_0寄存器的值复原*/
  ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
  ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

  /* Restore the clobbered interrupt priority register to its original
  value. */
  *pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* conifgASSERT_DEFINED */

/* 使 PendSV 和 SysTick 与内核具有相同的优先级,而 SVC处理程序具有更高的优先级,因此可用于退出临界区(其中
       较低的优先级被掩盖)。 */
   
    /* 将PendSV和SysTick中断设置为最低优先级*/
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

/* Configure the regions in the MPU that are common to all tasks. */
prvSetupMPU();

/* 启动生成滴答 ISR 的计时器。中断被禁用
       这里已经。*/
   
    /* 启动系统节拍定时器,即SysTick定时器,初始化中断周期并使能定时器*/
prvSetupTimerInterrupt();
   
    /* 初始化临界区嵌套计数器 */
/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;

/* Ensure the VFP is enabled - it should be anyway. */
vPortEnableVFP();

/* Lazy save always. */
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

   
    /* 启动第一个任务 */
/* Start the first task. */
prvStartFirstTask();

/* 永远不会运行到这里 */
return 0;
}


__asm void prvStartFirstTask( void )
{
PRESERVE8

/* Cortext-M3硬件中,0xE000ED08地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址*/
ldr r0, =0xE000ED08
ldr r0, [r0]   /* 将R0所保存的地址处的值赋值给R0 */
ldr r0, [r0]   /* 获取MSP初始值 */

msr msp, r0     /* 复位MSP */
cpsie i         /* 使能中断(清除PRIMASK) */
cpsie f         /* 使能中断(清除FAULTMASK) */
dsb             /* 数据同步隔离 */
isb             /* 指令同步隔离 */
svc 0           /* 调用SVC指令触发SVC异常,启动第一个任务保存现场(LR寄存器) */
nop             /* 空指令,运行到这里说明SVC异常处理完毕,等待PendSV异常发生*/
nop
}


__asm void vPortSVCHandler( void )
{
PRESERVE8                  /*8字节对齐*/
   
ldr r3, =pxCurrentTCB     /*将存储器中pxCurrentTCB(TCB_t * volatile pxCurrentTCB)(地址读到)r3寄存器中*/
    ldr r1, [r3]       /*将r3寄存器所指地址中的数据读取到r1寄存器中,也就是TCB_t(获得了任务控制块) */
    ldr r0, [r1]             /*将r1所指的地址的数据读取到r0寄存器中,(任务控制块的首地址就是栈顶指针)r0获取到了栈顶地址*/
      
    ldmia r0!, {r4-r11}      /* 将r0的地址+4,后从存储器地址上的数据加载到寄存器r4-r11*/
msr psp, r0        /* 写r0指向地址里存储的值到psp,即切换为线程栈*/
isb                         /* 指令同步屏障 */
mov r0, #0                  /* 将0送到r0寄存器中。即r0寄存器存的值为0 */
msr basepri, r0             /* 使寄存器basepri为0,即开启中断 */
orr r14, #0xd               /* r14寄存器中的值与#0xd进行或运算保存后保存到r14寄存器*/
bx r14                      /* 告诉处理器 ISR 完成,需要返回,此刻处理器便会使用 PSP 做为堆栈指针,准备进行进行压栈操作*/
}


__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting; /*每个任务在临界嵌套中保持自己的中断状态多变的*/
extern pxCurrentTCB;      /*指向当前激活的任务*/
extern vTaskSwitchContext;

PRESERVE8

mrs r0, psp                 /*将psp寄存器的值读出保存到r0寄存器中*/
isb                         /* 指令同步隔离 */

ldr r3, =pxCurrentTCB  /*将存储器中pxCurrentTCB(TCB_t * volatile pxCurrentTCB)(地址读到)r3寄存器中*/
ldr r2, [r3]                /*将r3所指地址的数据读取到r2寄存器中也就是获得了任务控制块*/

stmdb r0!, {r4-r11}   /*将r0的地址+4,后寄存器上的数据加载到存储数据上(压栈),硬件自动将xPSR,PC,LR,R12,R0-R3自动存入存储器上 */
str r0, [r2]    /* 将任务控制块首地址的值加载到r0寄存器也就是r0获得了任务栈顶*/

stmdb sp!, {r3, r14}        /*将R3,R14临时压入堆栈,因为即将调用函数vTaskSwitch,调用函数时,返回地址自动保存在R14中,一旦调用发生,R14的值将被覆盖,因此需要入栈保护*/
                                /*R3保存当前激活的TCB指针(pxCurrentTCB)地址,函数调用后会用到因此也要入栈保护*/
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /*中断优先级号大于或者等于最大优先级的*/
msr basepri, r0            /* 使寄存器basepri为configMAX_SYSCALL_INTERRUPT_PRIORITY屏蔽中断,表示进入了临界区*/
dsb
isb
bl vTaskSwitchContext     /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换*/
mov r0, #0                /* 将0送到r0寄存器中。即r0寄存器存的值为0 */
msr basepri, r0           /* 使寄存器basepri为0,即开启中断 */
ldmia sp!, {r3, r14}      /* 恢复R3和R14,现在的R3保存变量pxCurrentTCB的地址,变量pxCurrentTCB函数在vTaskSwitchContext中可能已被修改*/
                              /* 指向新的最高优先级就绪任务;R14保存退出异常信息*/

ldr r1, [r3]              /* 将r3所指向地址中的数据给r1,就是获得了任务控制块变量*/
ldr r0, [r1]     /* 将r1首地址的变量给r0寄存器,就是获得了任务控制的栈顶指针变量*/
ldmia r0!, {r4-r11}    /* 将r0的地址+4,后从存储器地址上的数据加载到寄存器r4-r11*/
msr psp, r0               /* 写r0指向地址里存储的值到psp,即切换为线程栈*/
isb                       /* 指令同步隔离 */
bx r14                    /* 告诉处理器 ISR 完成,需要返回,此刻处理器便会使用 PSP 做为堆栈指针,准备进行进行压栈操作*/
nop
}


/***********************************************************
* 函数名 :xPortSysTickHandler
*
* 返  回 :无
*
* 参  数 :无
*
* 功  能 :系统时钟定时器中断函数(任务切换)
* ********************************************************/
void xPortSysTickHandler( void )
{
uint32_t ulDummy;

ulDummy = portSET_INTERRUPT_MASK_FROM_ISR();
{
  /* Increment the RTOS tick. */
  if( xTaskIncrementTick() != pdFALSE )
  {
   /*挂起上下文切换.任务切换 */
   portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
  }
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( ulDummy );
}

<hr/>

54

主题

877

帖子

1744

积分

金牌飞友

Rank: 6Rank: 6

积分
1744
飞币
865
注册时间
2017-9-23
发表于 2022-10-27 16:49:56 | 显示全部楼层
如果是自己DIY飞控的话,写个好点的状态机已足够,加上操作系统的必要性不是很大,还不如花多点时间在算法上。
统计一下一些常见开源飞控的操作系统使用情况:
APM:裸奔
Pixhawk:Nuttx
MWC/Naze32:裸奔
Openpilot:PIOS
Autoquad:CoOS (an embedded real-time multi-task OS specially for ARM Cortex M series)
Paparazzi:ChibiOS
匿名飞控:RT-Thread(国产飞控+国产RTOS)
Crazyflie:FreeOS
以上飞控除APM和MWC外(AVR),均使用STM32F1或F4系列的处理器作为主控。

39

主题

819

帖子

1609

积分

金牌飞友

Rank: 6Rank: 6

积分
1609
飞币
788
注册时间
2017-9-25
发表于 2022-10-27 16:58:49 | 显示全部楼层
如果你的飞控中只需要做姿态级的解算和控制,不需要费这么大劲上系统,直接裸奔吧。
如果你的飞控中不止姿态级,包括并且不仅限于GPS/INS解算及导航、飞行数据链路、室内SLAM、视觉导航等多个任务需要调度,那么请毫不犹豫上系统。
我以前用STM32F1系列做飞控,裸奔情况下姿态控制尚且没有问题,不过加上GPS/INS之后就感觉靠状态机去控制时序不太可靠,出现过几次数据丢失甚至卡死的情况。

39

主题

805

帖子

1596

积分

金牌飞友

Rank: 6Rank: 6

积分
1596
飞币
789
注册时间
2017-8-17
发表于 2022-10-27 17:11:33 | 显示全部楼层
Pixhawk 应用的是Nuttx
据我所知很多商业飞控用的是ucos。这个讨论就不展开了。
系统对于复杂的飞控系统很有必要,但是对于自己花一下午列了几个方程式做了个定点增稳飞控来说没有意义。
您需要登录后才可以回帖 登录 | 加入联盟

本版积分规则

快速回复 返回顶部 返回列表