文章目录
- 一、RTOS引入
- 二、任务的引入
- 2.1 任务的定义
- 2.2 理解C函数的内部机制
- 2.3 ARM架构
- 2.4 汇编指令
- 2.5 怎么保存函数的现场
- ①要保存什么
- ②保存现场的几种场景
- 三、FreeRTOS中怎么创建任务
- 四、通过链表深入理解调度机制
- 4.1 优先级与状态
- 4.2 调度方法
- 五、创建任务—伪造现场
- 5.1 创建任务
- 5.1.1 定义任务栈
- 5.1.2 定义任务函数
- 5.1.3 定义任务控制块
- 5.2 启动任务
- 5.3 切换任务
- 六、多任务系统实战分析
一、RTOS引入
妈妈要一边给小孩喂饭,一边加班跟同事微信交流,怎么办?
用人类生活的示例来比喻单片机,妈妈要一边给小孩喂饭,一边加班跟同事微信交流,怎么办?
对于单线条的人,不能分心、不能同时做事,她只能这样做:
- 给小孩喂一口饭
- 瞄一眼电脑,有信息就去回复
- 再回来给小孩喂一口饭
- 如果小孩吃这口饭太慢,她回复同事的信息也就慢了,被同事催:你半天都不回我?
- 如果回复同事的信息要写一大堆,小孩就着急得大哭起来。
这种做法,在软件开发上就是一般的单片机开发,没有用操作系统。
对于眼明手快的人,她可以一心多用,她这样做:
- 左手拿勺子,给小孩喂饭
- 右手敲键盘,回复同事
- 两不耽误,小孩“以为”妈妈在专心喂饭,同事“以为”她在专心聊天
- 但是脑子只有一个啊,虽然说“一心多用”,但是谁能同时思考两件事?
- 只是她反应快,上一秒钟在考虑夹哪个菜给小孩,下一秒钟考虑给同事回复什么信息
这种做法,在软件开发上就是使用操作系统,在单片机里叫做使用RTOS。
程序简单示例:
在软件开发上就是使用操作系统,在单片机里叫做使用RTOS。RTOS的意思是:Real-time operating system,实时操作系统。
// 经典单片机程序
void main()
{
while (1)
{
喂一口饭();
回一个信息();
}
}
------------------------------------------------------
// RTOS程序
喂饭()
{
while (1)
{
喂一口饭();
}
}
回信息()
{
while (1)
{
回一个信息();
}
}
void main()
{
create_task(喂饭);
create_task(回信息);
start_scheduler();
while (1)
{
sleep();
}
}
二、任务的引入
2.1 任务的定义
从这个角度想:函数被暂停时,我们怎么保存它、保存什么?怎么恢复它、恢复什么?
- 任务是一个函数吗?
- 函数保存在Flash上
- Flash上的函数无需再次保存
- 所以:任务不仅仅是函数
- 任务时变量吗?
- 单纯通过变量无法做事
- 所以:任务不仅仅是变量
- 任务时一个运行中的函数
- 运行中:可以曾经运行,现在暂停了,但是未退出
- 怎么描述一个运行中的函数
- 假设在某一个瞬间时间停止,你怎么记录这个运行中的函数
- 要理解任务的本质,需要理解ARM架构、汇编
2.2 理解C函数的内部机制
main函数中代码和反汇编程序:
结合反汇编程序,分析此段代码可以理解C函数的内部机制。
2.3 ARM架构
什么是程序:指令与数据的集合。
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
- 对内存只有读、写指令
- 对于数据的运算是在CPU内部实现
- 使用RISC指令的CPU复杂度小一点,易于设计
比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:
细看这几个步骤,有些疑问:
- 读a,那么a的值读出来后保存在CPU里面哪里?
- 读b,那么b的值读出来后保存在CPU里面哪里?
- a+b的结果又保存在哪里?
我们需要深入ARM处理器的内部。简单概括如下,我们先忽略各种CPU模式(系统模式、用户模式等等)。
CPU运行时,先去Flash上取得指令,再执行指令:
- 把内存a的值读入CPU寄存器R0
- 把内存b的值读入CPU寄存器R1
- 把R0、R1累加,存入R0
- 把R0的值写入内存a
2.4 汇编指令
只需要记住5条汇编指令:
- 读内存:Load,LDR
- 写内存:Store,STR
- 加法:ADD
- 入栈:PUSH,实质上就是写内存STR
- 出栈:POP,实质上就是读内存LDR
①要读内存:读内存哪个地址?读到的数据保存在哪里?读多少字节?
- LDR R0, [R1, #0x00]
- 源地址:R1+0x00,注意:不是读R1,是把R1的值当做内存的地址
- 目的:R0,CPU的寄存器
- 长度:4字节,LDR指令就是读4字节,LDRH是读2字节,LDRB是读1字节
②要写内存:写内存哪个地址?从哪里得到数据?写多少字节?
- STR R0, [R1, #0x00]
- 目的地址:R1+0x00,注意:不是写R1,是把R1的值当做内存的地址
- 源:R0,CPU的寄存器
- 长度:4字节,STR指令就是读4字节,STRH是读2字节,STRB是读1字节
③入栈:把CPU的寄存器的值,写到内存上
-
PUSH {R3, LR}
- 源:CPU的寄存器R3、LR的值
- 目的:内存,内存哪里?使用CPU的SP寄存器指定内存地址
- 长度:大括号里所有寄存器的数据长度,每个寄存器4字节
- 注意:低编号的寄存器,保存在内存的低地址处
- 执行结果如下
④出栈:把内存中的数值,写到CPU的寄存器
- POP {R3, PC}
- 源:内存,内存哪里?使用CPU的SP寄存器指定内存地址
- 目的:CPU的寄存器R3、PC的值
- 长度:大括号里所有寄存器的数据长度,每个寄存器4字节
- 注意:内存的低地址处的数据,写到CPU低编号的寄存器
- 执行结果如下
⑤其他知识:
- CPU内部有R0、R1、……、R15共16个寄存器
- 某些寄存器有特殊作用
- R13,别名SP,栈寄存器,保存着栈的地址
- R14,别名LR,返回地址,保存着函数的返回地址
- R15,别名PC,程序计数器,也就是当期程序运行到哪了
程序运行分析:
2.5 怎么保存函数的现场
①要保存什么
-
程序运行到了哪里?
PC寄存器的值,R2的值:我辛辛苦苦从内存里读到的值放在R2里,函数继续运行时,R2的值不要被破坏了。
-
只需要保存R2吗?
答:切换任务的话,所有的寄存器都要保存。
-
保存在哪里?
答:内存的栈里。
②保存现场的几种场景
-
函数调用
-
中断处理
-
任务切换
三、FreeRTOS中怎么创建任务
详细请看博客:FreeRTOS——任务通知(基于百问网FreeRTOS教学视频)-CSDN博客
四、通过链表深入理解调度机制
-
可抢占:高优先级的任务先运行
-
时间片轮转:同优先级的任务轮流执行
-
空闲任务礼让:如果有同是优先级0的其他就绪任务,空闲任务主动放弃一次运行机会
4.1 优先级与状态
- 优先级不同
- 高优先级的任务,优先执行,可以抢占低优先级的任务
- 高优先级的任务不停止,低优先级的任务永远无法执行
- 同等优先级的任务,轮流执行:时间片轮转
- 状态
- 运行态:running
- 就绪态:ready
- 阻塞:blocked,等待某件事(时间、事件)
- 暂停:suspend,休息去了
- 怎么管理?
- 怎么取出要运行的任务?
- 找到最高优先级的运行态、就绪态任务,运行它
- 如果大家平级,轮流执行:排队,链表前面的先运行,运行1个tick后乖乖地去链表尾部排队
- 怎么取出要运行的任务?
4.2 调度方法
-
谁进行调度?
- TICK中断!
中断处理流程
五、创建任务—伪造现场
FreeRTOS中任务的操作:创建任务、启动任务 、切换任务
5.1 创建任务
5.1.1 定义任务栈
#define TASK_COUNT 32
static int task_stacks[TASK_COUNT];
在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空 间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM中。
5.1.2 定义任务函数
为任务函数伪造现场,如图所示:
任务是一个独立的函数,函数主体无限循环且不能返回。
示例代码:
static char stack_a[1024] __attribute__ ((aligned (4)));;
static char stack_b[1024] __attribute__ ((aligned (4)));;
static char stack_c[1024] __attribute__ ((aligned (4)));;
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;
}
5.1.3 定义任务控制块
在多任务系统中,任务的执行是由系统调度的。系统为了顺利 的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的 所有信息,比如任务的栈指针,任务名称,任务的形参等。
有了这个任务控制块之后,以后系统对任务的全部操作都 可以通过这个任务控制块来实现。
5.2 启动任务
方法:从栈里恢复到寄存器(读内存),设置标识位,让启动任务时修改标识位的值.
设置标识位代码示例:
static int task_running = 0;
int cur_task = -1;
void start_task(void)
{
task_running = 1;
while (1);
}
int is_task_running(void)
{
return task_running;
}
在中断中启动任务代码示例:
* 如果还没有创建好任务, 直接返回 */
if (!is_task_running())
{
return; // 表示无需切换
}
/* 启动第1个任务或者切换任务 */
if (cur_task == -1)
{
/* 启动第1个任务 */
cur_task = 0;
/* 从栈里恢复寄存器 */
/* 写汇编 */
stack = get_stack(cur_task);
StartTask_asm(stack, lr_bak);
return ; /* 绝对不会运行到这里 */
/* 其余代码省略 */
启动任务执行过程解释:
在寄存器中读取内存值,读取到的值写入其它寄存器需要触发中断返回(BX R1)。
5.3 切换任务
任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务。
在中断函数中的示例代码:
else
{
/* 切换任务 */
// 取出下一个任务
pre_task = cur_task;
new_task = get_next_task();
if (pre_task != new_task)
{
/* 保存 pre_task: 在汇编里已经保存了 */
/* 更新sp */
set_task_stack(pre_task, old_sp);
/* 切换 new_task */
stack = get_stack(new_task);
cur_task = new_task;
StartTask_asm(stack, lr_bak);
}
任务切换的反汇编代码示例:
FreeRTOS 中的任务切换机制是系统实时性的保证,也是实现多任务并行执行的基础。在进入和退出任务切换的关键代码段时,FreeRTOS 管理着临界区和中断优先级,以确保任务切换过程不受干扰,并且尽量减少关中断的时间,以降低对系统实时性的影响。在Cortex-M3这种具有独特硬件特性的平台上,FreeRTOS 利用这些硬件能力(如 PendSV 异常)来实现更高效、更低延迟的任务切换操作。
六、多任务系统实战分析
优点:多任务系统可以并发执行或者并行处理,使得在嵌入式开发中能够提升CPU利用率,多任务系统可以增强系统的响应性,通过抢占式或者轮询方式确保每个任务可以获得执行的机会。能够显著提升计算机系统的整体性能和效率。
在项目中的运用如:
- 智能家居对用户输入、设备状态监控、无线通信中的多个任务并行处理;
- 在自动化生产线中,FreeRTOS 可以管理传感器数据采集、马达控制、安全监控等多个任务,提高系统的响应速度和可靠性。
参考资料来源:
git clone https://gitee.com/jiang-jiawei123/doc_and_source_for_livestream.git