0


RTOS原理与实现01:RTOS基础知识

1. 前后台系统结构

1.1 概述

说明1:前后台结构实现为主循环 + ISR,我们需要自己实现主循环并处理ISR与主循环之间的交互

说明2:如果中断要处理的事情很简短,可以在ISR中完成;如果时间要处理的事情较多,则返回后台程序处理

1.2 前后台系统存在的问题

1.2.1 实时性不能保证

实时性不能保证,事件可能无法得到及时处理

示例中在处理flag1事件时延时2秒,会影响对flag2事件的处理

1.2.2 CPU利用率不高

存在CPU空转的情况,CPU利用率不高

说明:上述CPU空转就是因为当前要操作的资源尚未准备好,但是前后台系统又无法去处理其他事务

1.2.3 编程思维不自然

强迫人按照机器的顺序工作方式思考编码。当执行的任务越多,代码结构越复杂,编码难度越大。

不能并行处理,只能顺序处理

2. RTOS原理及功能简介

关键:为什么RTOS能解决前后台系统存在的问题

2.1 概述

RTOS是一种通用的任务管理框架,用于控制任务的运行和任务之间的交互,保证事件得到实时处理。

RTOS的三要素:实时性 + 操作系统 + 嵌入式

2.2 RTOS与前后台结构的比较

① RTOS相当于实现了后台的主循环,并能够处理ISR与主循环的交互

② 使得用户可以只考虑任务的设计

③ RTOS还提供了各种组件用于实现任务间交互及其他控制管理功能(e.g. 存储管理)

2.3 工作原理简介

2.3.1 提供任务概念

提供多个执行流,虽然实际只有一颗CPU,但通过"虚拟化",每个Task好像独占CPU

此处"虚拟"的CPU并非完全的虚拟,"独占"也并不是真正独占,而只是任务认为自己独占

任务认为自己独占的理解:task可以认为自己独占一颗CPU,所以可以实现为一个死循环。而CPU上实际的执行流,会由于操作系统的控制,在不同task之间进行切换。这样既简化了task的设计,也充分利用了CPU

这就引入了一个问题,即"操作系统的控制"本身在实现上也是一段代码,那么这段代码在什么时机运行呢 ? 其实就是在中断或者task主动交出CPU控制权的时候(e.g. task调用可能导致阻塞的函数)

2.3.2 提供任务调度机制

通过RTOS控制任务的运行时机,事件处理的实时性得到有效保证

说明1:一般中断ISR只会进行简短的预处理,而将耗时的操作交给task来执行。RTOS可以确保在ISR执行完成后,立即调度ISR的后续task执行(当然,这需要task优先级的保障)

说明2:ISR后续task在运行时,如果需要等待资源,RTOS会调度其他task执行

说明3:高优先级task可以抢占低优先级task

2.3.3 提供资源管理与通信组件

提供一些组件用于简化任务对资源的访问,事件的处理,以及任务之间的通信,有效降低任务之间的代码耦合

2.4 总结

下图体现了RTOS相对于前后台系统的优势

3. 调试工具使用

4. 芯片内核简介

4.1 为什么要了解硬件特性

RTOS作为系统软件,运行时必然与硬件相关,e.g.

① 任务切换时寄存器的保存

② 异常处理

③ 内核时钟节拍来自硬件定时器

4.2 内核概述

Cortex-M3内核是ARM公司开发的CPU内核。完整的MCU芯片集成了Cortex-M3内核及其他组件

4.3 内核特性介绍

4.3.1 工作模式及特权级别

① 前台程序(中断服务程序)只能在特权级运行

② 后台程序可以根据需要切换权限级别

说明:特权级的不同通常体现在栈指针的使用上,用户级使用PSP;特权级使用MSP

4.3.2 寄存器组

① 只有R13为Banked register,代码中均为R13,运行时根据当前运行的特权级确定使用的是MSP还是PSP

② 之所以区分低组和高组寄存器,是因为大部分16-bit Thumb-2指令只能访问低组寄存器(本质原因:16-bit指令编码长度限制)

注意:Cortex-M3有三个程序状态寄存器,分别是APSR(应用PSR)/ IPSR(中断PSR)/ EPSR(执行PSR)

上述三个程序状态寄存器其实是一个三合一寄存器,即可以单独访问,也可以组合访问

4.3.3 Cortex-M3预定义的存储器映射

说明:Cortex-M3此处的地址空间映射由处理器内核设置,而非芯片厂商设置,这点有利于简化系统移植

4.3.4 堆栈

Cortex-M3使用满递减栈,采用双堆栈机制

4.3.5 系统异常

4.3.5.1 系统异常列表

需要注意如下3种异常,

① 复位

② PendSV

③ SysTick

4.3.5.2 进入异常

说明1:此处要区分哪些寄存器由硬件保存,哪些寄存器由软件保存,由软件保存的寄存器需要软件自己恢复

特别注意:由硬件保存的寄存器也是保存在当前栈中

说明2:注意异常向量表的前2个成员,分别是MSP初始值和复位异常入口地址

注意1:Cortex-M3中需要保存LR,是因为LR只有一个,并非banked register(在Cortex-A系列中,LR为banked register)

而且保存后LR寄存器还要在异常返回时起特殊作用,这点和Cortex-A系列非常不同。Cortex-A系列异常处理的思路是通过banked LR保存返回地址,然后用LR恢复PC

注意2:进入异常时,LR寄存器值在入栈后,会被设置为特殊的EXC_RETURN值,这个值在异常退出时影响返回动作

经过仿真,Cortex-M3在首次进入PendSV异常时,LR值为0xFFFFFFF9,即退出异常时会返回线程模式,并使用MSP

4.3.5.3 退出异常

说明:将进入异常时设置的特殊LR(即EXC_RETURN值)写入PC,就会进入异常返回流程

在Cortex M3中,只有bit 3 & bit 2是可变动的,各种组合情况如下,

bit 3

bit 2

含义

EXC_RETURN数值

0

0

返回handler模式,因为handler模式只能运行在特权级,所以只能使用MSP

0xFFFFFFF1

0

1

错误组合,handler模式无法使用PSP

1

0

返回thread模式,且使用MSP,即仍在特权级运行

0xFFFFFFF9

1

1

返回thread模式,且使用PSP,即在用户级运行

0xFFFFFFFD

4.3.5.4 复位异常的响应

说明:复位异常发生后,CPU将0x00000000和0x00000004中的内容(即异常向量表的前2项)分别加载MSP和PC,即可开始执行

右边的图画错了,赋值给PC的应该是0x00000101,即启动引导代码的位置;赋值给MSP的应该是0x20008000,即MSP初始值

4.3.5.5 PendSV异常的响应

作用:在PendSV中执行RTOS上下文切换(即不同任务间的切换)

工作原理:配置为最低优先级,上下文切换的请求将自动延迟到其他的ISR都完成后才处理,并且可以被其他异常 / 中断抢占。

说明:如果在SysTick中断中发现需要进行任务切换,则只是标记PendSV异常,SytTick中断处理结束时仍然返回之前的ISR。最后当没有比PendSV优先级更高的异常/中断时,才进行任务切换

注意:这一流程的实现需要依赖Cortex-M3提供的NVIC硬件支持,这种可抢占的异常也体现了RTOS的实时特性

4.3.6 指令系统

Cortex-M3使用Thumb-2指令集,长度可为16位或32位。指令可以携带后缀,如有条件执行。

下面仅介绍后续汇编代码中会使用到的指令。

5. 内核编程实践

5.1 需求说明

触发PendSV异常,在异常处理函数中,保存R4 ~ R11寄存器到缓冲区,再恢复R4 ~ R11寄存器,以模拟任务切换时的寄存器保存与恢复

5.2 代码说明

5.2.1 寄存器操作

#define NVIC_INT_CTRL   (0xE000ED04) //中断控制状态寄存器
#define NVIC_PENDSVSET  (0x10000000) //设置挂起pendSV位,将bit[28]置1
#define NVIC_SYSPRI2    (0xE000ED22) //系统处理器优先级寄存器,按字节访问
#define NVIC_PENDSV_PRI (0x000000FF) //设置pendSV优先级为最低

说明1:中断控制状态寄存器

设置该寄存器的bit 28即可将pendSV中断挂起,当没有更高优先级的中断需要处理时,将进入pendSV中断的ISR运行

说明2:系统处理器优先级寄存器

pendSV中断为第14号中断,由于可以按字节访问NVIC寄存器,所以将0xE000ED22设置为0xFF即可将pendSV中断设置为最低优先级

5.2.2 triggerSV函数说明

void triggerPendSV(void)
{
    // 设置pendSV为最低优先级
    MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;
    
    // 设置挂起pendSV位
    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;
}

triggerPendSV函数的作用就是先将pendSV中断的优先级设为最低,然后将该中断挂起,用于模拟请求任务切换的场景

5.2.3 PendSV_Handler函数说明

__asm void PendSV_Handler(void)
{
    // 汇编中使用C变量
    IMPORT blockPtr
    
    // 加载缓冲区地址
    LDR    R0, =blockPtr
    LDR    R0, [R0]
    LDR    R0, [R0]
    
    // 保存寄存器
    STMDB  R0!, {R4-R11}
    
    // 更新缓冲区指针
    LDR    R1, =blockPtr
    LDR    R1, [R1]
    STR    R0, [R1]
    
    // 修改部分寄存器,用于测试
    ADD    R4, R4, #1
    ADD    R5, R5, #1
    
    // 恢复寄存器
    LDMIA  R0!, {R4-R11}
    
    // 更新缓冲区指针
    STR    R0, [R1]
    
    // 异常返回
    BX     LR
}

说明1:PendSV_Handler函数名

该函数为pendSV异常的ISR,之所以使用该函数名,是因为在Keil初始化环境中将pendSV的异常向量设置为PendSV_Handler

说明2:缓冲区指针的维护

先来说明一下缓冲区的定义方式,

typedef struct _BlockType_t {
    unsigned long *stackPtr;
} BlockType_t;

BlockType_t block;
BlockType_t *blockPtr = NULL;

unsigned long stackBuffer[1024]; // 缓冲区

int main(void)
{
    block.stackPtr = stackBuffer + 1024;
    blockPtr = █
    ....
}

如此便可理解加载缓冲区地址的操作了,

LDR    R0, =blockPtr // 获取blockPtr变量的地址(blockPtr标号的地址)
                     // 变量名就是符号地址
LDR    R0, [R0]      // 获取block的地址(也就是blockPtr指针变量的值)
LDR    R0, [R0]      // 获取stackPtr指针变量的值(也就是缓冲区的地址)

之所以可以通过第2次{ LDR R0, [R0] }可以获取stackPtr指针变量的值,是因为stackPtr是BlockType_t结构的第1个成员,而C语言要求结构体变量的首地址与其第1个成员的首地址相同

说明3:EXC_RETURN值的设置

响应pendSV异常时,LR的值被设置为0xFFFFFFF9,即异常返回时进入线程模式且使用MSP(线程模式 + 特权级)

这是因为系统启动时默认为线程模式 + MSP,而中断返回是默认返回中断触发前的运行状态

标签:

本文转载自: https://blog.csdn.net/chenchengwudi/article/details/115473104
版权归原作者 麦兜的学习笔记 所有, 如有侵权,请联系我们删除。

“RTOS原理与实现01:RTOS基础知识”的评论:

还没有评论