1. RTOS引入
单片机性能越来越强,很多Linux程序在单片机上也可以运行了:这需要RTOS。
我们要开发的单片机产品,功能也越来越丰富:这也需要RTOS。
就个人技术发展来说,单片机开发的技术提升方向之一就是RTOS。
RTOS已经无处不在:
- ESP8266 WIFI模块,出厂自带FreeRTOS,可以在上面做二次开发;
- 4G模块CAT1,出厂自带FreeRTOS,可以在上面做二次开发;
- 想实现功能比较丰富的设备时,比如加上MQTT功能,就需要RTOS
- 比如已经被RT-Thread采用的kawaii-mqtt,默认就不支持裸机
- 你去看所有的智能设备:小度音箱、小爱闹钟、家居摄像头,都使用RTOS。
2. RTOS必需的几个文件
2.1 start.S
开发板上电运行的第一个文件。必需的文件之一。
- 首先需要先设置异常向量表。
- 开发板复位后,找到第一条执行的指令。
- 作为实时操作系统,肯定要实现任务切换的功能,而任务切换需要依赖于中断,因此,当中断发生时,需要硬件跳转到中断发生的异常地址处
- 初始化内存,必需操作之一
- 初始化串口
- 用于调试
- 用于观察程序的运行情况
- 初始化时钟
- 为了实现任务切换的功能,采用时间片轮转的方式进行任务切换,假定当一个任务运行1ms后,发生时钟中断,俗称tick中断,确保系统的心跳。
- 由于需要实现,每1ms进行一次任务切换,因此,任务启动的功能以及任务切换的功能就需要在时钟中断的函数中实现
- 跳转至main函数
上述功能作为RTOS的基础,必须要实现的
/* 设置异常向量表 */
__Vectors DCD 0
DCD Reset_Handler ; Reset Handler
DCD 0 ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD 0 ; MPU Fault Handler
DCD 0 ; Bus Fault Handler
DCD UsageFault_Handler_asm ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD 0 ; Debug Monitor Handler
DCD 0 ; Reserved
DCD 0 ; PendSV Handler
DCD SysTick_Handler_asm ; SysTick Handler
AREA |.text|, CODE, READONLY
/* 上电运行的第一个函数 */
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT mymain
IMPORT SystemInit
IMPORT uart_init
IMPORT UsageFaultInit
IMPORT SysTickInit
IMPORT LedInit
/* 由于会用到C函数,因此必须设置一个栈,在内存的顶部随便找了一个地方 */
LDR SP, =(0x20000000+0x10000)
/* 初始化内存,内存中的各个分区 */
BL SystemInit
/* 初始化串口 */
BL uart_init
/* 初始化中断控制器 */
BL UsageFaultInit
LDR R0, =0
LDR R1, =0x11111111
LDR R2, =0x22222222
LDR R3, =0x33333333
LDR R12, =0x44444444
LDR LR, =0x55555555
DCD 0xffffffff
SVC #1
/* 初始化系统时钟,并设置时钟中断1ms */
BL SysTickInit
/* 点灯指示 */
BL LedInit
/* 跳转到main函数执行,可以看到主函数的名字不一定非要设置为 "main" */
;BL mymain
LDR R0, =mymain
BLX R0
ENDP
2.2 task.c
在该文件中包含了任务创建、开始任务(开始调度)、获得当前任务栈、任务调度等,是RTOS中关键的主要文件。
2.2.1 创建任务 create_task
所谓的任务环境,指的就是当该任务运行时,寄存器中的值
首先,我们能想到的
- 新创建出来的任务无非是为了运行一个新的程序,而且这个新程序不需要返回
- 创建的任务也是一个C函数,因此需要有地方来存放自己的环境,比如局部变量,任务传入参数以及多个寄存器
- 任务的现场就是运行该任务时的寄存器,因此,当任务切换时,为了下次还能接着运行,需要将切换任务时寄存器的状态保存起来,需要保存到自己的任务栈里面,当下次调度到自己时,直接将栈里的环境弹出,恢复环境就可以接着运行了。
- 为了可以让任务参与调度,当创建任务时,任务的寄存器还没有使用,因此,我们需要替他伪造一个环境,让其可以参与调度,这样我们的任务就可以跟上其他的任务一起参与调度了。
所以,创建任务的关键就是伪造任务的环境
void create_task(task_function f, void *param, char *stack, int stack_len)
{
int *top = (int *)(stack + stack_len);
/* 伪造现场 */
top -= 16;
/* 新创建任务的栈的结构如下图所示 */
/* r4~r11 */
top[0] = 0; /* r4 */
top[1] = 0; /* r5 */
top[2] = 0; /* r6 */
top[3] = 0; /* r7 */
top[4] = 0; /* r8 */
top[5] = 0</