zl程序教程

您现在的位置是:首页 >  其他

当前栏目

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

2023-03-20 14:55:37 时间

目录

1. 前后台系统结构

1.1 概述

1.2 前后台系统存在的问题

1.2.1 实时性不能保证

1.2.2 CPU利用率不高

1.2.3 编程思维不自然

2. RTOS原理及功能简介

2.1 概述

2.2 RTOS与前后台结构的比较

2.3 工作原理简介

2.3.1 提供任务概念

2.3.2 提供任务调度机制

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

2.4 总结

3. 调试工具使用

4. 芯片内核简介

4.1 为什么要了解硬件特性

4.2 内核概述

4.3 内核特性介绍

4.3.1 工作模式及特权级别

4.3.2 寄存器组

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

4.3.4 堆栈

4.3.5 系统异常

4.3.6 指令系统

5. 内核编程实践

5.1 需求说明

5.2 代码说明

5.2.1 寄存器操作

5.2.2 triggerSV函数说明

5.2.3 PendSV_Handler函数说明


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,而中断返回是默认返回中断触发前的运行状态