RTOS多任务切换实现

news2025/1/18 7:40:23

实现任务需要的基础知识

1、程序内部细节

通过分析C语言程序的编码会发现程序都是一些指令和数据。
什么是程序?

  1. 指令
  2. 运行过程中的数据

2、常用汇编指令

汇编指令详解


3、ARM架构过程调用标准AAPCS

传参:

通过r0-r3传递,多于4个参数的部分用栈传递

返回值:

通过r0寄存器

C函数调用过程寄存器变化:

1、随意使用R0、R1、R2、R3、 R12 无需保护它们,硬件自动保存
2、r4-r11 可用,先保存,用完后要恢复原来的值

特殊的寄存器:

r13 - sp指针
r14 - LR链接寄存器,保存子程序返回地址
r15 - PC程序计数器,PC指向哪里,程序就运行到哪里

4、Cortex-M3中断异常处理流程

在这里插入图片描述

栈帧图中的返回地址保存中断执行结束后的返回地址,也就是中断结束后返回执行的第一条语句。

中断异常处理流程:

1、保存中断处理完成后的返回地址,这是由硬件自动保存的
2、中断处理,硬件自动调用中断服务函数,中断服务程序也是C函数,C语言函数调用过程会保证不破坏R4~R11
3、中断处理完成之后是恢复现场,硬件自动恢复R4-R11之外的寄存器,R4-R11在中断处理函数执行结束会恢复,所以保持不变

中断异常返回:

这里是引用8.1.4 EXC_RETURN
处理器进人异常处理或中断服务程序(ISR)时,链接寄存器(LR)的数值会被更新为EXC_RETURN数值。当利用BX、POP或存储器加载指令(LDR或LDM)被加中时,该数值用于触发异常返回机制。
EXC_RETURN中的一些位用于提高异常流程的其他信息。EXC_RETURN义如表8.1所示,EXC_RETURN的合法值则如表8.2所示。由于EXC_RETURN的编码格式,在地址区域0xF0000000一0xFFFFFFFF
行中断返回的。不过,由于系统空间中的地址区域已经被架构定义为不可执会带来什么问题。

—— 引用自《ARM Cortex-M3与Cortex-M4权威指南》
从这段话中可以知道,中断触发时硬件会自动会将LR寄存器设置为0xF0000000—0xFFFFFFFF这个范围的某个数值,这个数值用于中断返回
当使用BX之类的指令进行BX LR时,由于LR此时是个特殊值,不可执行,此时硬件就知道要触发硬件恢复R0-R3、R12等寄存器。


多任务切换实现

1、任务切换的核心 — 栈

任务切换的核心是切换任务的栈
创建任务实质就是伪造任务现场(栈帧)
任务能实现切换的核心也是栈。

Cortex-M3的栈帧的结构:
在这里插入图片描述
什么是现场? —— 当前执行程序被打断瞬间所有寄存器的值。
怎么保存现场? —— 存储到内存。
保存到内存什么地方? —— 栈

程序状态寄存器:
在这里插入图片描述
在这里插入图片描述
—— 引用自《ARM Cortex-M3与Cortex-M4权威指南》 4.2.3 特殊寄存器

PSR寄存器T位设置为1表示使用的是Thumb指令(16bit),0表示使用ARM指令(32bit),Cortex-M内核只支持Thumb指令。Thumb指令可以更好的节省空间。

2、伪造任务栈(现场)

伪造任务栈也就是所谓的创建任务(线程)。

void os_thread_create(thread_entry entry, void *arg, void *stack_addr, uint32_t stack_size)
{
    char *cstack = (char *)stack_addr;
    
    cstack += stack_size;    /*get stack top */
    
    uint32_t *stack =  (uint32_t *)cstack;
    
    stack -= 16;    /* 因为栈是向下生长;空出16x4的空间刚好能构造一个栈帧 */
    
    stack[0] = 0;   /* R4 */
    stack[1] = 0;   /* R5 */
    stack[2] = 0;   /* R6 */
    stack[3] = 0;   /* R7 */
    stack[4] = 0;   /* R8 */
    stack[5] = 0;   /* R9 */
    stack[6] = 0;   /* R10 */
    stack[7] = 0;   /* R11 */
    
    stack[8] = (uint32_t)arg;   /* R0 传递给线程函数的参数,只有一个参数,根据AAPCS规则是通过R0传递第一个参数 */
    stack[9] = 0;   /* R1 */
    stack[10] = 0;   /* R2 */
    stack[11] = 0;   /* R3 */
    
    stack[12] = 0;   /* R12 */
    stack[13] = 0;   /* LR */
    
    stack[14] = (uint32_t)entry;   /* 返回地址,中断产生执行结束后返回地址,从中断发挥执行任务第一条语句肯定是线程函数入口地址 */
    
    stack[15] = (1 << 24);   /* PSR, 设置24位为1表示使用Thumb指令 */
    
    thread_stacks_sp[thread_count] = (uint32_t)stack;    /* 记录栈恢复的位置 */
    thread_count++;                                   /* 线程计数+1 */
}
  • 对于寄存器位置的排布参考栈帧图才能更好理解。
  • R4-R11、R12、LR的值可以随便给,根据AAPCS规则,R0用来参数传递函数的第一个参数,任务函数也是函数,所以R0赋值的是给任务函数的参数。

2、任务切换实现

任务切换基本要靠汇编实现,也可以C内联汇编实现,但还是使用汇编方便。
任务调度器实现:

static uint8_t os_is_starting = 0;

static uint32_t thread_stacks_sp[OS_MAX_THREADS];
static uint32_t thread_count = 0;

static uint32_t cur_thread = -1;

void os_thread_scheduler(uint32_t lr, uint32_t new_sp)
{
    uint32_t prev_thread;
    uint32_t sp;
    
    if (os_is_starting == 0) return;   /* 没有启动 */
    
    if (cur_thread == -1)
    {
        cur_thread = 0;
        
        sp = thread_stacks_sp[cur_thread]; /* 得到当前任务的sp */
        Thread_Switch_Context(sp, lr);
    }
    else 
    {
        prev_thread = cur_thread;
        uint32_t next_thread = (cur_thread + 1) % thread_count;
        if (prev_thread != next_thread)  /* 当它们相等时只有一个任务,不用切换 */
        {
            thread_stacks_sp[prev_thread] = new_sp;  /* 更新上一个任务的栈位置 */
            sp = thread_stacks_sp[next_thread];  /* 获取当前任务SP位置 */
            cur_thread = next_thread;  /* 指向下一个要执行的任务 */
            Thread_Switch_Context(sp, lr);  /* 触发任务切换 */
        }
    }
}
  • thread_stacks_sp[OS_MAX_THREADS],保存创建的任务的sp。
  • thread_count ,任务计数
  • cur_thread ,指向当前任务,当其为-1时,表示第一次切换任务。
  • os_is_starting ,系统启动后再进行任务切换。
  • Thread_Switch_Context,切换到下一个任务,汇编函数实现。

任务切换汇编实现:

; 在SysTick中断中实现保存上一个任务的现场
SysTick_Handler PROC
				IMPORT SysTick_IRQ
					
				STMDB sp!, {r4 - r11}    ; 将当前任务的r4-r11寄存器内容到其栈中
				STMDB sp!, { lr }   ; 保存LR到栈中
				
				MOV R0, LR      ; 此时LR是个特殊值,保存此时的LR值,通过R0传递给函数SysTick_IRQ
				ADD R1, SP, #4  ; R1 =  sp + 4 得到栈的真正位置,因为多保存了LR,栈向下生长,所以需要+4才得到真正的栈点
				BL SysTick_IRQ
				
				LDMIA sp!, { r0 }     ; 当系统没有,没有任务的时候则执行到这里,所以要从栈中恢复原来的寄存器内容
				LDMIA sp!, {r4 - r11}
				
				ENDP
				
;切换到下一个任务				
Thread_Switch_Context PROC
				EXPORT Thread_Switch_Context
				LDMIA r0!, {r4 - r11}  ; 从栈中恢复r4-r11的内容
				
				MSR MSP, R0 ; 将MSP设置为任务的sp
				
				BX r1     ; 通过特殊的LR值跳转触发硬件自动恢复r0、r1、r2等寄存器的
				
				ENDP
  • ADD R1, SP, #4 才能得到任务的SP:
    在这里插入图片描述

  • r0、r2、r3、r12等寄存器的值中断触发会由自动保存。

  • STMDB sp!, { lr } , 在调用SysTick_IRQ函数前保存LR,LR此时是个特殊值,后续通过其触发硬件自动恢复自动恢复r0、r2、r3、r12等寄存器的值。为什么要保存LR?因为SysTick_IRQ是C函数,C函数调用会破坏原来的LR值。

  • Thread_Switch_Context 函数传入LR值,此时lr值是个特殊值,通过BX r1 触发硬件自动恢复r0、r2、r3、r12等寄存器的值。

  • Cortex-M内核有两个SP:MSP和PSP,使用哪一个实现任务切换都可以,在同一时刻只能使用其中一个,这里使用MSP。

  • 在中断处理函数,如果直接访问sp,sp指向的MSP。

只是为了实现多任务切换,为了方便直接在Systick中断中进行执行调度器:

void SysTick_IRQ(uint32_t lr, uint32_t prev_thread_sp)
{
	SCB_Type * SCB = (SCB_Type *)SCB_BASE_ADDR;
	
	os_thread_scheduler(lr, prev_thread_sp);
	
	/* clear exception status */
	SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk;
}
  • prev_thread_sp上一个任务的sp,作为参数传进来是为了保存起来。

MDK仿真测试

uint32_t thread_a_stack[1024/4];
uint32_t thread_b_stack[1024/4];
uint32_t thread_c_stack[1024/4];

void thread_a_entry(void *arg)
{
    char a = 'a';
    
    while (1)
    {
        putchar(a);
//        puts("\r\n");
    }
}

void thread_b_entry(void *arg)
{
    char b = 'b';
    
    while (1)
    {
        putchar(b);
//        puts("\r\n");
    }
}

void thread_c_entry(void *arg)
{
    int i;
    int sum = 0;
    
    for (i = 0; i < 10; i++)
    {
        sum += i;
    }
    
    while (1)
    {
        put_s_hex("sum = ", sum);
    }
}

int mymain()
{
    puts("os start\r\n");
    
    os_thread_create(thread_a_entry, "Thread a", thread_a_stack, 1024);
    os_thread_create(thread_b_entry, "Thread b", thread_b_stack, 1024);
    os_thread_create(thread_c_entry, "Thread c", thread_c_stack, 1024);
    
    os_start(); 
     
    while(1);

	return 0;
}

由于SysTick设置的1s定时加之仿真时间也不准,所以数据会疯狂打印,很久才切换到下一个任务,实现多个任务切换是没问题的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/106490.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【矩阵论】6. 范数理论——基本概念——向量范数与矩阵范数

6.1 基本概念 6.1.1 向量范数 a. 模长&#xff08;二范数&#xff09; Cn中向量X(x1x2⋮xn)的模长为∣X∣(X,X)tr(AHA)∣x1∣2∣x2∣2⋯∣xn∣2C^n中向量 X\left( \begin{matrix} x_1\\x_2\\\vdots\\x_n \end{matrix} \right)的模长为 \vert X\vert\sqrt{(X,X)}\sqrt{tr(A^HA…

nginx+keeplived 实现高可用 Web 负载均衡

nginxkeeplived 实现高可用 Web 负载均衡 一、架构简介 在系统设计中&#xff0c;可以利用Nginx的反向代理和负载均衡实现后端应用的高可用性&#xff0c;同时我们还需要考虑Nginx的单点故障。如果Nginx所在服务器宕机&#xff0c;或者Nginx服务不可用就会造成整个系统的不可…

鸡蛋车的控制律

这个老头像幽灵&#xff0c; ​拿个鸡蛋来炸群。 ​只见动画不见车&#xff0c; ​骗人无果还丢人&#xff01; 温州一个做汽车驾驶模拟器的&#xff0c;在重庆为我约了美女&#xff0c;为鸡蛋车站台。他掏空口袋砸我&#xff0c;材料、工资、车马&#xff0c;都是他担。 余姚一…

代码随想录算法训练营第十天(字符串)| 232.用栈实现队列,225. 用队列实现栈

代码随想录算法训练营第九天&#xff08;字符串&#xff09;| 232.用栈实现队列&#xff0c;225. 用队列实现栈 232. 用栈实现队列 大家可以先看视频&#xff0c;了解一下模拟的过程&#xff0c;然后写代码会轻松很多。 题目链接/文章讲解/视频讲解&#xff1a; 看到题目的…

react笔记_08生命周期

目录生命周期(旧)生命周期componentWillMountcomponetdidMountshouldComponentUpdatecomponentWillUpdatecomponentDidUpdatecomponentWillUnmountcomponentWillReceiveProps组件的挂载、更新、销毁案例1渲染过程更新过程1-通过setState去修改数据更新过程2-通过forceUpdate强制…

用Python分析《阿凡达·水之道》的豆瓣短评

《阿凡达水之道》于2022年12月16日上映。第一部的口碑、评分等都非常高&#xff0c;第二部是否能延续呢&#xff0c;本文获取了该电影的豆瓣短评&#xff0c;进行了初步的分析&#xff0c;看下观众都是如何评价的。 数据获取 打开豆瓣首页&#xff0c;搜索电影名&#xff0c;进…

Spring(三): 使用注解来存储和读取Bean对象

目录一、存储Bean对象1.1 配置扫描路径1.2 使用注解存储Bean对象1.3 通过上下文读取Bean对象1.4 Bean命名规则1.5 方法注解 Bean1.5 重命名Bean二、获取Bean对象2.1 属性注入2.2 Setter注入2.3 构造方法注入2.4 Resource注解2.5 Resource注解中name参数的作用一、存储Bean对象 …

说明书丨艾美捷Annexin-V-Cy3凋亡检测试剂盒

Cy3标记的重组人膜联蛋白V显示亮红色荧光&#xff08;Ex&#xff08;max&#xff09;:543nm&#xff1b;Em&#xff08;最大值&#xff09;&#xff1a;570nm&#xff09;。 艾美捷Annexin-V-Cy3凋亡检测试剂盒化学性质&#xff1a; Applications: Flow Cytometry, Fluorescen…

C++【修理之路】初识string

这里写目录标题为什么学习string类&#xff1f;标准库中的string类string的定义转换为C风格的字符串string类的输出和输出访问字符串中的字符字符串的拼接string 字符串的增删改查总结为什么学习string类&#xff1f; C语言中&#xff0c;字符串是以’\0’结尾的一些字符的集合…

3D数学之四元数 学习笔记

四元数有三个虚部&#xff0c;一个实部 [ w (x y z) ] w xi yj zk i j k ijk -1 用于表示&#xff0c;物体在空间中的任意角度旋转 四元数的模 Sqrt(pow(w, 2) pow(x, 2) pow(y, 2) pow(z, 2)) 四元数共轭&#xff1a;p a bi 共轭为&#xff1a; p a - bi …

一文梳理 | 电力企业网络安全管理及等级保护工作重点

前言 为加强电力行业网络安全监督管理&#xff0c;规范电力行业网络安全工作&#xff0c;国家能源局近日印发《电力行业网络安全管理办法》和《电力行业网络安全等级保护管理办法》&#xff08;以下简称“两办法”&#xff09;&#xff0c;有效期均为5年。随着这两部规范性文件…

门神 马丁内斯 要什么样的成就,才不枉这些年的颠沛流离

2022-12-18日 随着劳塔罗的最后一球波网而入 阿根廷成功获得了卡塔尔世界杯的冠军 结束后 所有人都去拥抱劳塔罗 只有梅西拉起了倒在地上的马丁内斯 很多人说 在这次比赛中 阿根廷有两个神 一个是球王梅西 另一个则是 门将 马丁内斯 当场上二比零时 所有人都以为阿根廷胜券在…

(二)汇编语言——寄存器

目录 通用寄存器 汇编指令 物理地址 接下来我们来介绍寄存器&#xff0c;同时会介绍一些基本的语句&#xff08;mov,add等)&#xff0c;好了&#xff0c;就让我们进入今天的学习吧&#xff01;同时&#xff0c;我们以8086为例子来介绍&#xff0c;参考来自王爽老师的书。 说…

JAVA架构与开发(从0开始搭建一个springCloud web项目)

从0开始搭建一个springCloud web项目。 一、首先需要的开发工具&#xff1a; 1、SpringToolSuite4 &#xff0c;个人目前用的最熟练的JAVA开发工具。 2、JDK1.8&#xff0c;按照网上安装jdk的步骤安装就行。 3、maven-3.6.1 4、springBoot官网 Spring Boot 5、配置中心 Apo…

架构师必读 —— 逻辑模型(14)

从"is/is not" "before / after”的角度思考 遇到问题时&#xff0c;为了研究“为什么会发生这种问题”&#xff0c;可以将问题发生之前和发生后做一个对比&#xff0c;这样效果会更明显。通过明确差距&#xff08;差异&#xff09;&#xff0c;可以比较"在…

前端学习-创建vue项目

1.下载vscode windows系统安装第一个 linux系统安装第二个 苹果系统安装第三个 vscode 官网下载地址 下载地址&#xff1a;https://code.visualstudio.com/Download 2.下载git 选择对应的系统下载&#xff0c;跟随官网提示操作 下载地址&#xff1a;https://git-scm.com/down…

C++多线程编程基础

1.创建线程Thread 首先要引入头文件#include&#xff0c;管理线程的函数和类在该头文件中声明&#xff0c;其中包括std::thread类。 语句"std::thread th1(proc1);"创建了一个名为th1的线程&#xff0c;并且线程th1开始执行。 实例化std::thread类对象时&#xff…

你知道什么是 @Component 注解的派生性吗?

对于 Component 注解在日常的工作中相信很多小伙伴都会使用到&#xff0c;作为一种 Spring 容器托管的通用模式组件&#xff0c;任何被 Component 注解标注的组件都会被 Spring 容器扫描。 那么有的小伙伴就要问了&#xff0c;很多时候我们并没有直接写 Component 注解呀&…

计算机毕设Python+Vue寻人系统设计(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Spring Boot日志文件

哈喽呀&#xff0c;你好呀&#xff0c;欢迎呀&#xff0c;快来看一下这篇宝藏博客吧~~~ 目录 1.日志快速扫盲 2.Spring Boot项目日志简单分析 3.自定义打印日志 4.通过设置日志的级别来筛选和控制日志输出的内容 5.日志持久化 1.日志快速扫盲 什么是日志?说白了就是控制…