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;
top[0] = 0;
top[1] = 0;
top[2] = 0;
top[3] = 0;
top[4] = 0;
top[5] = 0;
top[6] = 0;
top[7] = 0;
top[8] = (int)param;
top[9] = 0;
top[10] = 0;
top[11] = 0;
top[12] = 0;
top[13] = 0;
top[14] = (int)f;
top[15] = (1<<24);
task_stacks[task_count++] = (int)top;
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/1463c0005d4b4596aa979d1c015e86ca.png)
2.2.2 开始调度 start_task
开始调度就相当于调度器的作用,当执行了start_task后,所有的任务开始运行,调度器是一个死循环,之后等待时钟中断的发生,对任务进行调度
void start_task(void)
{
task_running = 1;
while (1);
}
2.3 main.c
在main函数中,创建了三个任务,用于模拟任务的调度。
为这三个任务分别分配了1024字节的空间,三个任务的栈并不会有交集,因此,三个任务各有自己的环境
void task_a(void *param)
{
char c = (char)param;
while (1)
{
putchar(c);
}
}
void task_c(void *param)
{
int i;
int sum = 0;
for (i = 0; i <= 100; i++)
sum += i;
while (1)
{
put_s_hex("sum = ", sum);
}
}
int mymain()
{
create_task(task_a, 'a', stack_a, 1024);
create_task(task_a, 'b', stack_b, 1024);
create_task(task_c, 0, stack_c, 1024);
start_task();
return 0;
}
任务a和任务b虽然使用的是同一个函数,但是传入的参数是不一样的,而且栈也是不同的,因此是属于不同的任务。
任务a:一直打印字符 ‘a’
任务b:一直打印字符 ‘b’
任务c:计算0-100的和
此时的栈分布如下图
![](https://img-blog.csdnimg.cn/6caeed816c7c4b6fa402a4a39e178fe1.png)
3. 启动任务
在main函数中,我们已经创建了三个任务,并伪造了他们的环境,是时候启动任务了。
根据前面我们知道,在执行了start_task后,程序陷入了死循环中,此时只有中断能拯救,也就是时钟中断,即系统滴答。
在时钟设置时,设置了每1ms触发一次中断,当中断发生时,将会跳转到异常向量表的位置执行中断,
__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
就是SysTick_Handler_asm
这个函数
SysTick_Handler_asm PROC
; 在这里保存R4~R11
STMDB sp!, { r4 - r11 }
STMDB sp!, { lr }
MOV R0, LR ; LR是一个特殊值
ADD R1, SP, #4
BL SysTick_Handler ; 这个C函数保证不破坏R4~R11
LDMIA sp!, { r0 }
LDMIA sp!, { r4 - r11 }
BX R0
ENDP
END
在这个函数中,上来先保存了R4~R11中的寄存器,此时要注意一个问题!
在硬件上,会自动帮我们先保存下面的寄存器即,R0~R3、R12、LR、返回地址、PSR
,因此,在上面的函数中我们只保存了R4~R11,而且由于硬件帮我们自动保存了一部分东西,我们就要考虑怎么才能让他恢复呢?这时,就需要将LR
设置为一个特殊值,当我们利用LR返回时,如果发现是一个特殊值,我们就需要恢复硬件保存的寄存器。
- 硬件帮我们自动保存**
R0~R3、R12、LR、返回地址、PSR
** - 硬件保存后,LR寄存器被设置为一个特殊值
因此,基于上面两点,我们在汇编程序中,只保存了R4~R11以及LR寄存器
![在这里插入图片描述](https://img-blog.csdnimg.cn/3b2d2c908fd74588855ed8e777e6914a.png)
; 在这里保存R4~R11
STMDB sp!, { r4 - r11 }
STMDB sp!, { lr }
MOV R0, LR ; LR是一个特殊值
ADD R1, SP, #4
BL SysTick_Handler ; 这个C函数保证不破坏R4~R11
仔细理解一下上面的指令,在保存完寄存器后,后面接着需要运行一个C函数,传入的C函数根据AAPCS规范,汇编语言向C语言传入参数时,第一个参数保存在R0,第二个参数保存在R1,第三个参数保存在R2,第四个参数保存在R3。当多于四个时,就需要用栈来传递参数了。
因此上面的指令中,将LR以及SP传给了C函数,栈的分布如下
![在这里插入图片描述](https://img-blog.csdnimg.cn/0072a30a4a584c3da313ce37f5574ab0.png)
跳转到C函数中继续执行,下面分析下C函数的作用
int is_task_running(void)
{
return task_running;
}
void SysTick_Handler(int lr_bak, int old_sp)
{
int stack;
int pre_task;
int new_task;
SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;
SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk;
if (!is_task_running())
{
return;
}
if (cur_task == -1)
{
cur_task = 0;
stack = get_stack(cur_task);
StartTask_asm(stack, lr_bak);
return ;
}
······
}
- 因为,时钟中断可能随时发生,也可能在任务没有创建就已经发生了,因此,需要先判断是否已经创建好了任务,才能继续向下运行。
- 当
cur_task = -1
时,就意味着,因为刚刚创建好,还没有执行任务,因此接下来的事情就是将任务栈中的寄存器,全部恢复
#define TASK_COUNT 32
static int task_stacks[TASK_COUNT];
static int task_count;
int get_stack(int task_index)
{
return task_stacks[task_index];
}
可以看到,分配了一个数组,在这个数组中,我们在创建任务时,将任务栈顶保存在了这个数组中,因此,我们只需要获取到栈顶位置就行了
注意上面的StartTask_asm
函数,需要传入两个参数,第一个参数是栈顶的位置,另一个参数是LR的值,此时的LR是一个特殊值
stack = get_stack(cur_task);
StartTask_asm(stack, lr_bak);
在启动任务中,StartTask_asm是一个关键函数,下面按照流程看下这个函数
StartTask_asm PROC
; 从任务的栈里把R4~R11读出来写入寄存器
; r0 : 保存有任务的栈
; r1 : 保存有LR(特殊值)
LDMIA r0!, { r4 - r11 }
; 更新SP
MSR MSP, R0
;MOV SP, R0
; 触发硬件中断返回: 它会把栈里其他值读出来写入寄存器(R0,R1,R2,R3,R12,PSR)
BX R1
ENDP
程序中已经进行了注释,LDMIA
指令,将栈中的寄存器一个个恢复。**R0~R3、R12、LR、返回地址、PSR
**由硬件恢复,当执行完之后,该任务的环境就全部恢复了,此时的CPU就会运行这个任务
![在这里插入图片描述](https://img-blog.csdnimg.cn/64ef17ac2ae04c2a92888dabd3c16b3e.png)
任务启动完毕!
![在这里插入图片描述](https://img-blog.csdnimg.cn/71e50f1e7e2c4f35a256184661fa6797.png)
由于没有切换任务,此时只有任务a在疯狂运行。
4. 任务切换
任务切换是重点了,但是并没有想象中的复杂
任务切换的流程与任务启动的流程很类似,任务启动是由于cur_task == -1
才会启动任务,而任务切换cur_task
已经设置了,因此走的是另外一个分支
void SysTick_Handler(int lr_bak, int old_sp)
{
int stack;
int pre_task;
int new_task;
SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;
SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk;
if (!is_task_running())
{
return;
}
if (cur_task == -1)
{
cur_task = 0;
stack = get_stack(cur_task);
StartTask_asm(stack, lr_bak);
return ;
}
else
{
pre_task = cur_task;
new_task = get_next_task();
if (pre_task != new_task)
{
set_task_stack(pre_task, old_sp);
stack = get_stack(new_task);
cur_task = new_task;
StartTask_asm(stack, lr_bak);
}
}
}
可以看到任务启动和任务切换走的是不同的分支,在任务切换分支,由于需要切换任务,因此需要找到下一个任务的栈是什么,我们才能恢复下一个任务的环境。
int get_next_task(void)
{
int index = cur_task;
index++;
if (index >= task_count)
index = 0;
return index;
}
为了简化过程,我们依次取出任务
if (pre_task != new_task)
{
····
}
判断一下,之前任务的栈与新任务栈是否一致,不一致就意味着需要切换任务了。切换任务的本质就是保存当前栈与恢复新任务栈而已
在切换之前,我们需要先保存一下当前栈的位置,下次切换到时可以直接获得
void set_task_stack(int task, int sp)
{
task_stacks[task] = sp;
}
前面讲过,在这个数组中我们保存的就是栈顶的位置。
栈保存完了,后面就是易主了
获取新任务的栈
int get_stack(int task_index)
{
return task_stacks[task_index];
}
仔细想一下,之前的栈已经保存起来了,新任务的栈也准备好了,这个状态是不是与任务启动时栈的状态一样了!
因此很巧妙,我们可以直接调用任务启动的函数了!
![在这里插入图片描述](https://img-blog.csdnimg.cn/1be004f1e9fa49f3942058b91179b0b0.png)
当再次切换到这个任务时,保持之前切换前的寄存器,程序继续向下运行,恢复时钟中断发生前寄存器的状态,此时一个时钟中断就处理完了,要知道这个整个过程1ms运行一次,因此在CPU运行时,1ms就反复进行任务切换了。
![](https://img-blog.csdnimg.cn/9daa521cdb9946a59a13fd0f5ac6b4eb.png)
可以看到,三个任务在飞速切换中…
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)