Cortex-M内核知识点总结_cortex内核-程序员宅基地

技术标签: HardFault  内核  Cortex-M  嵌入式  单片机  

总览

在这里插入图片描述

Cortex内核 基础

寄存器组

在这里插入图片描述

  • 程序在经过编译后,生成可执行二进制文件,如上图,是截取某个函数在flash中存储的内容 (反汇编文件)
  • 可以看到以下信息:
    • 指令的存储地址 ,指令的二进制内容 , 指令代表的汇编类容
    • 指令存在32位指令和 16位指令 ,具体可参考权威指南中的 :Thumb指令集 和 ARM指令集
  • 此函数的执行过程细节:
    • PC指针指向函数开始执行地址 0x0800d130 , 内核的取指单元根据地址,将内容 f24754a0 由flash读取到内核
    • 译码单元翻译 f24754a0 ----> MOV r4 , #0x75a0 , 可以看出二进制格式和汇编是相关的,内核存在一套译码规则,比如 f2 表示 MOV
    • 执行单元执行此指令 ,将立即数 0x75a0 存到 通用寄存器 R0
    • PC 增加,继续上述过程,直到整个函数执行完成

===================================================================================================================

===================================================================================================================
在这里插入图片描述

指令流水线

在这里插入图片描述

流水线的引入充分利用到了3个指令控制单元,提高了指令处理效率,同时也引入了一些问题。在时钟周期1完成了指令1的取指 ,在时钟周期3才执行指令1,若指令1是个条件分支语句 ,那个指令1的下一条指令是不确定的,这时候内核的取指2和取指3 是进行分支预测的,若预测失败,那么这两次的预先取指将无效,将花费更多的时间重新取指,因此在写if else 时,尽可能将最大可能发生的条件放在首位,这样能减小分支预测错误的概率

复位序列

启动文件分析
  • 栈与堆空间分配 (在不使用标准库中的malloc时,堆空间被优化)

在这里插入图片描述

  • 中断向量表

在这里插入图片描述

  • 复位处理函数

    • SystemInit : 初始化时钟,初始化中断向量表偏移
    • __main
      • 将未初始化内存区(.BSS or ZI)清零
      • 调用下面的堆栈初始化函数 __user_initial_stackheap
      • 进入main函数

在这里插入图片描述

  • 堆栈初始化函数

在这里插入图片描述

复位过程

在这里插入图片描述

​ 中断向量表的存储地址 0x08000000 + 4 , 0x08000000存储的为MSP的值 , ARM会将flash的0地址自动映射到 0x8000000。这里有一个需要思考的问题, 为什么需要将MSP的初值放到0地址?这个初值又是多少?原因如下:

在Reset_Hander中 , 首先会调用 SystemInit 函数,这是一个C函数,且函数里边还会调用其他函数,此时汇编文件中的栈还未进行初始化(_main), 因此需要一个栈空间来保存函数调用过成,仿真可发现 初值MSP是一段内存0x20000000很靠前的一个地址,MSP会零时使用此值,待SystemInit 执行完后归还,在调用 __main后 MSP的值初始化成map文件中的值。

数据结构-栈

img

走迷宫的过程:

  • 每次经过一个分叉口 , 给分叉口一个编号 , 选择一个岔路口走下去
  • 循环上诉过程,直到走出迷宫,或者走到死路
  • 若走到死路,则根据记录的信息,回退到上一个岔路口,走之前未选的岔路 , 循环直到走出迷宫或者走到死路
  • 重复此过程,直到找到出口
/*函数调用过程与走迷宫类似,下面的函数 func1在执行过程中 , 会打断当前执行流程,去执行func2 , 
 去执行func2前,应该记录当前func1的执行环境,如记录c的值 ,记录参数a和b , 当func2执行完后,
 要退回func1继续执行*/

uint16_t func1(uint8_t a , uint8_t b)
{
    
    uint16_t c = 0;
    c = a + b;           /*a 和 b 作为参数 通过 R0 和 R1传递 , 计算结果会存在 R0 , R1 , R3*/
    func_2(c);           /*在跳转执行func2 前 , 参数 a ,b ,c 需要保存 ,*/
    return c;
}

void func2(uint16_t d)
{
    
    uint32_t e;
    e = d*d +2*d;
}
栈在内存中的地方

在这里插入图片描述
在这里插入图片描述

函数调用过程中栈帧结构

在这里插入图片描述

如上图所示,函数调用过程 为 main -> func2->func1 , 最终执行到 func1中后 栈的结构如图 ,下图表示函数func2的栈帧

在这里插入图片描述

  • FP 栈帧底部,用于返回查找,FP将所有的栈帧构建成一个链表
  • LR 连接寄存器,保存了函数退出后的下一个PC地址,用于函数返回
  • 局部变量:函数中定义的局部变量
  • 函数参数:当函数参数大于4个 ,大于的部分需要入栈,其余通过R0-R3传递
示例一:无参数,无返回值的函数调用

在这里插入图片描述

可以看到左边为C 程序 ,右边为其对应的汇编程序 , 看到汇编可能会慌张,但其实观察可以看到 , 只是在重复几条指令而已。

mov R0 , #1           ;将立即数1(也就是常数)写入到 R0 , MOV只能赋值8位的数值,32位的需要用伪指令LDR
ldr R3 , [fp,#-8]     ;将 [FP-8] 内存地址的内容加载到 R3      
str R3 , [sp,#-12]    ;将 R3的值写入到 [FP-12] 内存地址
add fp , sp ,#0       ; fp = sp + 0
sub sp , sp ,#28      ; sp = sp - 28
push {fp ,lr}         ;使用 push 指令是 默认从左往右依次入栈(但是R0-R12通用寄存器会先入大的编号) , 
                      ;栈指针 sp 会自动"增加" , 压fp 和 lr 则 sp = sp -8 (思考为何是减而不是加)
                      
pop  {fp}             ;pop 指令会将栈内数据依次弹出 , 赋值给{}内的寄存器 , 同时栈指针会自动"减小"
bl <func>             ; 跳转指令 ,调整PC指针到对应的跳转地址 , 并且自动将跳转地址的下一个地址写入到 lr 寄存器

sp我们知道是栈指针 , 每次使用 push 指令,sp都将自动生长(减小,栈是向低地址方向生长),每次使用pop指令时,sp都将增大。fp是什么呢,它是frame point的缩写 , 直译过来就是栈帧指针。我们在进入main 函数后,sp和fp都指向栈底,此时栈是空的,如下图所示。

在这里插入图片描述

从main函数的汇编开始分析

在这里插入图片描述

我们假设栈底在内存中的地址是 0x20005000 ,栈的大小为 1K 。 在执行 push {fp , lr} 后,栈的变化情况如下(当栈是空的时使用push指令sp的偏移会少一个单元,如下图,push两个单元进入,正常sp = sp-8 ,但这里是sp=sp-4 , 只要记住一点,sp指针永远指向栈顶,这里就是lr所在地址):

在这里插入图片描述

接下来的 两条指令 add fp , sp , #4 和 sub sp , sp , #16 执行后的结果为,这个16的由来是因为main函数里有4个局部变量需要存到栈中。

在这里插入图片描述

接下来就是一直到 bl 跳转前,都是让局部变量入栈,注意局部变量的入栈顺序,最先定义的局部变量反而最后入栈 , fp指针和sp指针之间我们称为main函数的栈帧。

在这里插入图片描述

接下来跳转到 函数func , 具体看右侧注释过程,可以分为4个部分 , 我们分别将4个过程 栈的变化情况用图形表示出来

在这里插入图片描述

第一部分,划分出函数func 的栈帧空间(移动fp 和sp) , 这里有一点需要注意,在main函数中,定义了4个局部变量,在分配栈帧空间时,刚好分配4个32位空间来存储4个整形的 , 而在func 中,有5个整型局部变量,但这里却分配了6个单位(4*6=24字节),因为栈空间需要8字节对齐。一般我们都是熟悉的4字节对齐,但是由于浮点数的原因 (double)需要8字节 , 所以使用8字节对齐。还有一点与main函数的栈帧不同 , func函数的栈帧未将lr 寄存器入栈,因为func函数已经是调用最深的函数 , 它内部没有其他调用函数,因此lr的值不会变,当函数执行完后 , 直接令 pc = lr 即可返回。

在这里插入图片描述

第二部分,func函数的局部变量入栈

在这里插入图片描述

第三部分,依次求和,最终将就和结果保存到e , 也就是上图数值5的地方。

第四部分,出栈,首先调整栈指针 sp = fp + 0 , 在弹出保存的main_fp = 0x20005000 到 fp 指针 ,sp指针因为pop操作上移一个单元。最终的栈空间如下图右侧部分。虽然func的值依旧保留 , 但是由于sp 的值改变,当再次使用push压栈时,将覆盖旧值。func函数没有返回值 , 所以直接跳转到main 函数。也就是bx lr 。 还记得lr 保存得内容吗。在使用bl 指令时 , 会将bl 的下一条指令的地址自动写入 lr 寄存器。所以 返回时就可以接着执行了。

在这里插入图片描述

中断发生后栈的变化

中断和函数调用的区别在于中断任何时刻都可能发生,发生中断后,需要保存现场环境,然后执行中断服务函数,执行完后,需要恢复现场环境。从寄存器组章节可知,和当前环境相关的寄存器有 8个 ,这8个寄存器硬件上会按以下顺序自动入栈 ,若中断服务函数中还有局部变量或函数调用,按栈帧形式继续入栈
在这里插入图片描述

HardFault 问题查找步骤 : 发生HardFault时 ,当前PC 和 LR参考意义不大 ,应该根据当前SP指针的地址 , 获取此8个寄存器的值 ,其中PC为发生异常时的PC,

LR为发生异常代码所在函数的下一条执行的代码 , R0-R4 为过程值 ,可能为参数 ,返回值 ,内存加载值等等 ,具体依靠PC和LR能分析其内容。

以下为实际发生HardFault的Jlink读取情况

在这里插入图片描述

根据PSP(裸机使用MSP) 查找发生 入栈的 8个寄存器 PC : 0x080249AA LR:0x0801E895

在这里插入图片描述

在这里插入图片描述

最终定位到发生问题的函数: shell_version_cmd --> unti_strcmp --> LDRB r2 ,[r1,#0] 。 进一步分析原因可继续追溯栈的内容,查出 until_strcmp 参数输入内容,最终发现为until_strcmp 的第二个参数在出异常是为非法地址,访问非法地址导致HardFault

深入理解中断

中断输入和悬起行为

在这里插入图片描述

我们使用外部中断的高电平触发方式来解释中断的过程 , 如上图中断请求表示外部输入引脚高电平持续的时间 ,内核检测到中断请求后,将悬起对应的中断(中断标志置位) , 中断悬起后,可能不是立马执行服务程序 , 比如当前在临界区,退出临界区后,处理器模式切换成Handler 模式,并开始执行中断服务程序,清除中断标志,执行完成后 ,退出服务,处理器模式切换成线程模式。若中断请求不是期望中的只请求一次能,比如一直存在请求 , 多次间断请求等又会是怎样的情况呢

请求信号一直保持

首先中断悬起后,只会在服务程序中清除 , 因此在清除了悬起后,等待中断返回 , 由于请求信号一直有 ,中断会再次被悬起,然后再次执行中断服务程序。

在这里插入图片描述

执行服务前多次请求

因为在第一次请求时中断就被悬起,且在下次请求前没有进入服务程序清除悬起,因此只会执行一次中断服务。

在这里插入图片描述

执行服务后再次请求

在下图中,进入中断服务程序后,悬起状态便被清除,但是还未退出中断后,中断请求再次到来,此时中断被再次悬起,且在服务程序退出后,立即又执行中断服务程序,这里内核会取消退出服务程序的出栈,以及进入服务程序的入栈(后面会具体介绍),这种中断情况称为:中断咬尾

在这里插入图片描述

中断优先级分组

在配置中断前 ,我们首先需要设置的就是优先级分组 , 然后设置抢占优先级和从优先级 , 数值越小优先级越高 , 类似下面的配置:

//选择使用优先级分组第1组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

// 使能EXTI0中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQChannel;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 指定抢占式优先级别1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 指定响应优先级别0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

// 使能EXTI9_5中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQChannel;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 指定抢占式优先级别0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 指定响应优先级别1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

在这里插入图片描述

首先是分组 , 如上表,分组决定了抢占优先级和从优先级 (响应优先级)的取值范围 。抢占优先级好理解,抢占优先级高的中断可以打断抢占优先级低的中断,响应优先级呢? 在两个中断的抢占优先级相同时 , 它们不具备抢占功能。这时若两个中断同时到达 , 则按照从优先级的高低决定先执行哪个。 中断的同时到达很容易产生误解,同时触发?网上很少有详细解释这一点的 , 同时触发的机率是很小的(如外部中断同时触发),不管误差有多小总有个先来后到 , 如果只是这样,还有必要设计从优先级吗 , 概率极小,多数情况有个先来后到,完全可以像RTOS 线程优先级只一个抢占优先级那样处理。 在了解了中断悬起行为后 , 就能理解,为什么需要从优先级。通过下图理解所谓的中断同时到达。

在这里插入图片描述

从上图可以看到,在进入临界区后 , 中断1的请求先到达 , 所以中断1先被悬挂 , 但是此时无法执行服务程序,中断2随后到达,也被悬挂,也无法执行。在退出临界区后 , 由于中断2的从优先级高 , 所以先得到执行,因此上面说的两个中断同时到达 ,并不是说中断同时请求 , 而是在执行服务前,存在两个相同抢占优先级的中断同时处于悬挂状态,这时根据从优先级决定先执行谁

中断咬尾

在上面的相同抢占优先级,不同从优先级中 , IRQ2 服务和 IRQ1 服务 按一般的中断流程,在执行服务程序前需要保护现场(入栈),在退出中断服务程序前要恢复现场(出栈),但是上面的这种情况是可以省略这个过程的 , 因为栈的数据在这个过程中没变化(主程序没来得及执行,之前保存的现场未发生改变,另一个中断服务又开始了),将数据取出来,然后一模一样的又存进去完全没必要 。这样看起来就像是前一个中断的尾巴(出栈)被下一个中断咬住(去尾去头将两个中断连在一起), 咬尾中断有效的减少了中断的延迟。

在这里插入图片描述

在这里插入图片描述

中断的具体行为

前面我们介绍了中断的请求与悬起 , 中断过程中处理器的模式和特权等级的变化。但是具体过程没有说明,比如中断过程寄存器是如何变化的 , 在中断跳转前,要进行哪些操作 ,中断返回后有需要哪些操作。理解了中断的具体行为,就能解释为何要求不要频繁进入中断。

入栈

在响应中断前,最先做的就是保护现场 ,比如当前正在计算 c = a + b , 我们已经将 a 的值从内存中加载到 R0 , 将 b 的值加载到 R1 然后进行了求和运算 , 并把结果存在R0中 , 若a , b 特别大,特殊功能寄存器程序状态字中的进位标志会被触发 , 此时还没有将结果从R0 写入到内存中(c在内存的地址),发生了中断,在中断中,可能会有运算,或者函数调用,也会用到R0-R15 以及特殊功能寄存器,但是前面的运算还没有结束 , 不能破坏了现场 ,不然中断返回后,就不能接着执行了 , 这时就需要保存寄存器中的值 , 保存到哪里?保存到栈中 , 栈在哪里?栈在内存中。由于我们做运算或者加载写入数值,函数调用一般只会用到 R0-R3 ,因此,R0-R3 ,R12(内部调用暂时寄存器 ) , R14(连接寄存器LR) , 程序状态字寄存器XPSR , PC 等将会被硬件自动保存到栈中 , 若用到了R4-R11 ,则需要用PUSH 指令手动保存。这些在第二部分会有详细介绍。 还有一点要注意,到底是 入哪个栈 , MSP 还是PSP , 裸机系统我们一般只使用MSP,而RTOS中线程会使用PSP 。

取向量

在入栈的同时 , 指令总线会获取程序状态字中的异常号 ,在中断向量表中查询对应的 中断服务地址入口。

寄存器更新

更新栈指针SP , 选择栈指针 MSP, 更新连接寄存器LR , 程序计数器PC,在入栈完成后,栈指针会更新(PSP或者MSP因为压栈操作而生长), 在执行服务程序时,将由MSP 负责对堆栈的访问。更新LR 比较特殊,前面我们说过LR 寄存器在函数调用过程中的作用 : 它记录了被调函数的下一条指令地址,这样函数返回后就可以继续执行。但在中断服务中LR 会有特殊的用法 ,在进入中断服务程序后 , LR 会被计算赋值为 EXC_RETURN (异常返回)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

LR的取值只有以上3种(以上为CM3的异常返回,其余需要找对应的ARM核) , 他能修改中断服务返回后的模式和使用哪个堆栈指针 , 还记得CONTROL 寄存器吗 它在特权模式下,也能修改模式和选择栈指针。只不过,CONTROL 寄存器是主动修改 , 这里的LR 值是系统自己判断的 , 判断依据如下:

  • 主程序在线程模式下 , 并且使用MSP 被中断 , 则 EXC_RETURN 的 值为 0xFFFFFFF9 (裸机系统为这个)

在这里插入图片描述

  • 主程序在线程模式下 , 并且使用PSP 被中断 , 则 EXC_RETURN 的 值为 0xFFFFFFFD (RTOS系统为这个)

在这里插入图片描述

异常返回时,只需要令PC 等于 LR 即可, 系统会自动切换模式 和 选择栈指针 , 并且从栈中恢复先前寄存器的值(包括PC重新回到打断的地方)。可以看到,中断始终使用的是主堆栈 , 因此中断的嵌套深度,局部变量,函数调用都会消耗主堆栈,在RTOS设计时,不仅需要考虑每个线程的栈空间大小,还需要考虑主堆栈的空间大小。

调度器

为什么需要调度器
实时性

在顺序执行结构中,常见的任务处理模式如下:

在这里插入图片描述

int main(void)
{
    
    while(1)
    {
    
        do_something1();
        do_something2();
        do_something3();
    }
}
void ISR(void)
{
    
    do_something4();
}

可以发现,每一个任务的执行都受其余任务执行时间的影响,虽然有任务放在中断中执行 , 但其余非中断任务实时性得不到保证。为了提高实时性,基于一个事实,一般任务都有一个执行周期,比如按键检测任务,可以5ms扫描一次,显示任务可以50ms 刷新一次 ,传感器采集任务可以500ms采集一次。

在这里插入图片描述

int main(void)
{
    
    while(1)
    {
    
        if(Timer5ms_flag){
    
        	do_something1();
        }if(Timer10ms_flag){
    
            do_something2();
        }if(Timer8ms_flag){
    
            do_something3();
        }
    }
}
void ISR(void)
{
    
    do_something4();
}

按照上述方式 , 由于将各个任务指定执行周期 , 在同一时刻让任务就绪错开 , 这样单个任务的实时性得到了提高。

按上述方法,实时性有一定的提高,多数情况下不会出现多个任务就绪的情况,但是极端情况下也会出现,如某个任务执行时间过长或执行时间不确定,破环了上图的任务错开时间运行的平衡 , 或者周期互为公约数,且启动时时间未使用质数错开 ,最终会出现类似 “共振”的现象,隔一段时间多个任务同时就绪。

在这里插入图片描述

多个任务同时就绪 , 我们更期望的时并行处理,而不是串行,如上图所示。但是MCU为单核,无法并行处理,为了实现类并行处理,时间片轮转调度由此而生。

在这里插入图片描述

时间片即任务允许执行的最长时间,时间到了就需要切换到下一个任务执行,时间片轮转需要考虑的是任务执行时间到了,如何切换到下一个任务,如何保存当前任务执行环境,如何恢复下一任务的执行环境等,当前主流的RTOS都支持时间片调度。虽然任务切换会消耗一定的时间,但是任务本身在时间片足够小时,几乎可以任务是并行执行。

解耦合

调度器能提供统一的接口,管理任务执行时序,避免了各种就绪标志位胡乱分布耦合。后文中将介绍几种常见的调度器实现,可以发现基于调度器框架开发的优势

裸机下的调度器
使用软件定时器实现调度
/***************************************************************************//**
 * @brief         设置ctimer定时器
 * @details       
 *
 * @param[in]     period    运行周期
 * @param[in]     counter   运行次数
 * @param[in]     p_func    函数指针
 * @param[in]     p_arg     函数
 * @return        分配的定时器指针,如果没有定时器分配,返回NULL
 *
 * @note          
 * @see           
 * @warning       
 ******************************************************************************/
struct ctimer* ctimer_set(UINT32 ulPeriod, UINT32 ulCount, void (*pfFunc)(void* p), void* pArg);
void ctimer_mainloop(void);
void task1_process(void* pArg)
{
    
    
}
void task2_process(void* pArg)
{
    
    
}
int main(void)
{
    
    ctimer_set( 5,  0  , task1_process, NULL);
    ctimer_set( 20,  0 , task1_process, NULL);
    while(1)
    {
    
        ctimer_mainloop();
    }
}
使用事件驱动的调度器
  • QP状态机框架: https://www.state-machine.com/
/********************************************************************
upgrade fsm:

       <--------------------------------------------------
      /                                            \      \
    idle -----> info ----->transfer ------------> finsh    \
                  \            \                            \
                   \            \                            \
                    --------------------------------------->error
                    
*********************************************************************/
R_state_t ecu_upgrade_state_idle(fsm_t* me, uint32_t evt)
{
    
    switch(evt)
    {
    
        case SYS_ENTER_EVT:
        {
    
            log_d("enter idle state");
            memset(&upgrade_info , 0 , sizeof(struct ecu_upgrade_info));
            return S_HANDLE();
        }
        case ECU_UPGRADE_REQUEST_EVT:
        {
    
            return S_TRAN(ecu_upgrade_state_transfer);
            //return S_TRAN(ecu_upgrade_state_info);
        }
        default:
        {
    
             return S_IGNORE();
        }
    }
}
R_state_t ecu_upgrade_state_info(fsm_t* me, uint32_t evt)
{
    
    switch(evt)
    {
    
        case SYS_ENTER_EVT:
        {
    
            /*step1:   session control*/
            /*step2:   security_access*/
            /*step3:   file_download request*/
            /*period:  tester present*/
            fsm_set_timeout_evt(me , 3000 , SYS_TIMEOUT_EVT);
            return S_HANDLE();
        }
        case SYS_TIMEOUT_EVT:
        {
    
            /*period:  tester present*/
            fsm_set_timeout_evt(me , 3000 , SYS_TIMEOUT_EVT);
            return S_HANDLE();
        }   
        default:
        {
    
            return S_IGNORE();
        }
    }
}
...
  • OSAL事件驱动框架 :https://github.com/recheal-zhang/ZStack-CC2530-2.3.0-1.4.0/tree/master/Components/osal
/***********用户任务事件处理函数***********/
uint16 SimpleBLETest_ProcessEvent( uint8 task_id, uint16 events )
{
    
    VOID task_id;
    
    if ( events & SYS_EVENT_MSG )
    {
    
        uint8 *pMsg;
        /*  读取发送来的消息            */
        if ( (pMsg = osal_msg_receive( SimpleBLETest_TaskID )) != NULL ){
    
            /*  消息有效数据处理函数    */
            simpleBLETest_ProcessOSALMsg( (osal_msg_send_dat *)pMsg );
            /*  清除消息内存            */
            VOID osal_msg_deallocate( pMsg );
        } 
        return (events ^ SYS_EVENT_MSG); 
    }
    if ( events & SBP_START_DEVICE_EVT )
    {
    
        /*  每500毫秒发送一个事件       */
        if ( SBP_PERIODIC_EVT_PERIOD ){
    
            osal_start_reload_timer( SimpleBLETest_TaskID, SBP_PERIODIC_EVT, SBP_PERIODIC_EVT_PERIOD );
        }  
        return ( events ^ SBP_START_DEVICE_EVT );   
    }
    if ( events & SBP_PERIODIC_EVT)
    {
     
        static uint8 byTmp = 0;
        
        if(byTmp > 7) byTmp = 0;
        /*  发送消息及有效数据  byTmp   */
        osalSendUserDat(byTmp++);
        
        return (events ^ SBP_PERIODIC_EVT);
    }
    return 0;
}

基于硬件定时器调度器

基于时间或事件的调度器已经能处理绝大多数裸机的应用情况 , 但有些特殊场景,上述两种调度器无法满足实时性。裸机的实时性需要靠中断来保证,上面两种调度器的本质都是定时产生就绪标志 ,然后在主循环里轮询标志 , 标志满足则执行,虽然任务错开时间运行对响应速度有一定的提高,但是还是避免不了排队执行,若某个任务本身的执行时间非常长,比如1S才能执行完,即使其执行周期长,但是在他执行时,其他任务都需要排队。

需求描述:

  • 在仪器仪表行业,一般使用低端MCU (主频16MHZ),无法提供足够资源 跑 RTOS
  • 如供电线路检测仪表,可能需要实现,线路电压采集,电流采集,温度采集 ,显示,无线传输等任务
  • 要求 50ms 采集一次电压电流 , 1s采集一次温度,500ms刷新一次显示
  • 采集的电压电流进行 FFT变换,并将结果存储到外部SPI flash
  • 温度,电压,电流异常预警 , 从检查到异常发生,需要在 10ms内控制继电器断电,报警鸣笛

需求拆解:

  • 16MHZ主频跑 FFT极其耗时间,在无硬件加速情况下,执行一遍可能到达秒级
  • 电流电压50ms采集 是硬要求,异常初始优先级最高,须立即处理
  • 电压电流 FFT变换对实时性要求不高,只是需要保存,类似一个历史数据,可供回查。

以上需求若使用上面两种调度方式无法满足实时性,需要使用主动中断的方式强行切换任务,处理流程如下图:

在这里插入图片描述

/*
创建任务:
	1)定义任务句柄
	2)定义任务处理函数 
	3)定义任务类型:周期任务或触发任务
	4)定义任务优先级 (根据任务优先级,任务处理函数会被注册到不同的硬件定时器)
*/

int task_create(TaskHander_t handle ,  TaskCallBack_t cb , uint8_t type , uint8_t prior);

/*
启动某个任务的调度
   1)任务句柄:根据任务句柄,能得知任务优先级,处理函数等,然后启动对应的定时器
   2)延时多久执行任务调度
*/
int task_schedule(TaskHander_t handle , uint32_t time);

/*
停止某个任务的调度
   1)任务句柄:根据任务句柄,能得知任务优先级,处理函数等,然后启动对应的定时器
   2)延时多久停止任务调度
*/
int task_schedule(TaskHander_t handle , uint32_t time);
RTOS调度器

在裸机的几种调度器中,时间与事件调度器的本质是相同的,都是设置任务的就绪标志,在主循环里边查询就绪标志,执行就绪任务,而基于硬件定时器的调度器相较于前两者增加了抢占功能,它已经初具RTOS的雏形了,但还存在以下问题:

  • 相同优先级的任务需要排队执行
  • 高优先级的任务在执行时,无法主动释放cpu使用权,即高优先级任务执行完某一步,需要sleep一段时间再执行不好实现
  • 高优先级任务(定时器中断注册的任务)和低优先级任务(主循环注册的任务)性质不同,不完全等价
  • 会引入更多的中断嵌套,且调度定时器中断优先级要最低,否则会导致其他中断受影响
  • 所有任务使用同一个栈,一崩全崩

为了解决上述问题,RTOS便应运而生

思考

有了前面几种调度器的铺垫 ,我们对 RTOS 需要实现哪些功能有个大概的认识,应该有以下几点:

  • 各个任务性质相同 ,但有优先级区分 ,且同一优先级的任务,不能排队运行,而是时间片轮转

  • 任务任何时间都能切换到其它任务 , 而有这种能力的只有中断 ,也就是需要有一个接口,供任务主动触发中断,然后在中断里边切换到其他任务

  • 任务本身是函数 , 所谓的任务切换,其实就是切换当前运行的函数, 通过修改PC指针就可以实现

  • 由A任务(函数)切换到 B任务(函数),不能直接令PC指针等于任务B(函数)的地址,这样先前B执行的过程和A执行的过程会丢失

在这里插入图片描述

PendSV(可悬起系统调用)

一般设置成最低优先级,这样可以保证 优先级顺序: 高优先级中断 >低优先级中断 > 高优先级任务 > 低优先级任务

线程栈与主栈

在上面的思考中。有一点没有明确:在task_a中主动触发中断,task_a的环境将保存到栈中,然后切换到task_b执行,再次触发中断,将task_b的环境保存到栈中,再回到task_a, 若a和b的环境都保存在同一个栈中,那么根据栈的特性先入后出,由b到a的切换必须先弹出b的环境,再弹出a的环境,这样显然不行。我们知道栈本身是一块连续的内存,PUSH 和 POP指令控制将内容压入栈中,然后移动对应的SP指针 ,若为每一个任务都定义一个栈和SP指针,就能解决上述问题。

在这里插入图片描述

在多线程模式下 系统SP指针使用的是 PSP , 若发生中断,在将线程环境压入当前 SP (PSP) 后 ,SP将切换成 MSP,后续的中断局部变量,中断函数调用以及中断嵌套,都将使用MSP 。将系统使用的栈和线程使用的栈分开,可减小全面崩溃的风险。(在中断的具体行为章节有详细描述)

线程切换汇编分析
;*************************************************************************
; 全局变量(4)
;*************************************************************************
IMPORT rt_thread_switch_interrupt_flag
IMPORT rt_interrupt_from_thread
IMPORT rt_interrupt_to_thread

;*************************************************************************
; 常量(5)
;*************************************************************************
;-------------------------------------------------------------------------
; 有关内核外设寄存器定义可参考官方文档:STM32F10xxx Cortex-M3 programming manual
; 系统控制块外设SCB 地址范围:0xE000ED00-0xE000ED3F
;-------------------------------------------------------------------------
SCB_VTOR        EQU 0xE000ED08  ; 向量表偏移寄存器
NVIC_INT_CTRL   EQU 0xE000ED04  ; 中断控制状态寄存器
NVIC_SYSPRI2    EQU 0xE000ED20  ; 系统优先级寄存器(2)
NVIC_PENDSV_PRI EQU 0x00FF0000  ; PendSV 优先级值(lowest)
NVIC_PENDSVSET  EQU 0x10000000  ; 触发PendSV exception 的值

; *-----------------------------------------------------------------------
; * 函数原型:void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * 该函数用于开启第一次线程切换
; *-----------------------------------------------------------------------
rt_hw_context_switch_to PROC
    EXPORT rt_hw_context_switch_to   ; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C 文件调用
    LDR r1, =rt_interrupt_to_thread  ; 将rt_interrupt_to_thread 的地址加载到r1
    STR r0, [r1]                     ; 将r0 的值存储到rt_interrupt_to_thread
    LDR r1, =rt_interrupt_from_thread; 将rt_interrupt_from_thread 的地址加载到r1
    MOV r0, #0x0                     ; 配置r0 等于0 , ;设置rt_interrupt_from_thread 的值为0,表示启动第一次线程切换
    STR r0, [r1]                     ; 将r0 的值存储到rt_interrupt_from_thread
    LDR r1, =rt_thread_switch_interrupt_flag  ; 设置中断标志位rt_thread_switch_interrupt_flag 的值为1 
    MOV r0, #1                       ; 配置r0 等于1
    STR r0, [r1]                     ; 将r0 的值存储到rt_thread_switch_interrupt_flag
 
    ; 设置PendSV 异常的优先级
    LDR r0, =NVIC_SYSPRI2
    LDR r1, =NVIC_PENDSV_PRI
    LDR.W r2, [r0,#0x00] ; 读
    ORR r1,r1,r2 ; 改
    STR r1, [r0] ; 写
 
	; 触发PendSV 异常(产生上下文切换)
    LDR r0, =NVIC_INT_CTRL
    LDR r1, =NVIC_PENDSVSET
    STR r1, [r0]

    ; 开中断
    CPSIE F
    CPSIE I
; *-----------------------------------------------------------------------
; * void PendSV_Handler(void);
; * r0 --> switch from thread stack
; * r1 --> switch to thread stack
; * psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
; *-----------------------------------------------------------------------
PendSV_Handler PROC
	EXPORT PendSV_Handler
	MRS r2, PRIMASK  ; 失能中断,为了保护上下文切换不被中断
	CPSID I
	
	LDR r0, =rt_thread_switch_interrupt_flag   ;加载rt_thread_switch_interrupt_flag 的地址到r0
    LDR r1, [r0]                               ; 加载rt_thread_switch_interrupt_flag 的值到r1
	CBZ r1, pendsv_exit                        ;判断r1 是否为0,为0 则跳转到pendsv_exit
	MOV r1, #0x00                              ; r1 不为0 则清0 
    STR r1, [r0]                               ; 将r1 的值存储到rt_thread_switch_interrupt_flag,即清0
    LDR r0, =rt_interrupt_from_thread          ; 加载rt_interrupt_from_thread 的地址到r0
    LDR r1, [r0]                               ; 加载rt_interrupt_from_thread 的值到r1
	CBZ r1, switch_to_thread ; 判断r1 是否为0,为0 则跳转到switch_to_thread ,第一次线程切换时肯定为0,则跳转到switch_to_thread
    ;================================================================================================
    ;上文保存
    ;================================================================================================
    ; 当进入PendSVC Handler 时,上一个线程运行的环境即:
    ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
    ; 这些CPU 寄存器的值会自动保存到线程的栈中,剩下的r4~r11 需要手动保存
     MRS r1, psp             ; 获取线程栈指针到r1
     STMFD r1!, {r4 - r11}   ; 将CPU 寄存器r4~r11 的值存储到r1 指向的地址(每操作一次地址将递减一次)
     LDR r0, [r0]            ; 加载r0 指向值到r0,即r0=rt_interrupt_from_thread
     STR r1, [r0]            ; 将r1 的值存储到r0,即更新线程栈sp
 	;========================================================================================================
 	;下文切换
 	;========================================================================================================
 	switch_to_thread
		LDR r1, =rt_interrupt_to_thread ; 加载rt_interrupt_to_thread 的地址到r1,  它 是一个全局变量,里面存的是线程栈指针SP 的指针
		LDR r1, [r1] 		  ; 加载rt_interrupt_to_thread 的值到r1,即sp 指针的指针
		LDR r1, [r1] 		  ; 加载rt_interrupt_to_thread 的值到r1,即sp
 		LDMFD r1!, {r4 - r11} ; 将线程栈指针r1(操作之前先递减) 指向的内容加载到CPU 寄存器r4~r11
 		MSR psp, r1     ; 将线程栈指针更新到PSP
 
 	pendsv_exit
	MSR PRIMASK, r2 ; 恢复中断
	ORR lr, lr, #0x04    ; 确保异常返回使用的栈指针是PSP,即LR 寄存器的位2 要为1
    ; 异常返回,这个时候栈中的剩下内容将会自动加载到CPU 寄存器:
    ; xPSR,PC(线程入口地址),R14,R12,R3,R2,R1,R0(线程的形参)
    ; 同时PSP 的值也将更新,即指向线程栈的栈顶
    BX lr 
    ENDP    ; PendSV_Handler 子程序结束
任务优先级与时间片

优先级实现了高优先级任务抢占低优先级任务 ,时间片实现了同等优先级的任务分时间片运行。RTOS由中断 ,优先级 ,时间片决定了 其 RealTime OS 中的RealTime特性。下图中,任务a,b,c,d 允许抢占发生 , b1, b2,b3 允许时间片轮转发生

在这里插入图片描述

任务就绪表与系统滴答时钟

上图定义的就是任务优先级就绪表 ,就绪表的更新如下步骤:

  • 任务在创建后,会根据任务的优先级,将任务插入到就绪表中
  • 启动调度后,系统将从优先级最高的链表(header0)开始查询就绪任务,并执行最高优先级任务 task_a
  • task_a中 调用sleep函数,等待下一个system_tick中断到来,在中断处理函数中获取优先级最高的就绪任务执行(b1,b2 ,b3)
  • 依次执行完c1 ,c2 , d , 完成所有任务都运行一遍,此时,再次触发 system_stick 可能没有任务处于就绪态
  • 在system_stick 中断处理函数中遍历任务,若任务剩余阻塞tick不为0 (vTaskDelay和其他阻塞函数设置tick),则tick-- , 否则调度任务
  • c1 , c2 处于同一优先级 , 若c1运行一个周期需要 40ms(未调用阻塞函数,例如运行复杂计算),c2运行周期60ms
  • 当任务时间片设置为5ms时 , c1运行5ms后,在system_tick中会判断c1剩余时间片为0 ,切换到c2运行5ms , 然后切换到c1运行,知道c1,c2运行完再运行低优先级任务

在这里插入图片描述

多线程编程模式

阻塞式编程

顺序逻辑结构
请求应答逻辑结构
发布与订阅逻辑结构

临界区与互斥访问

函数的可重入性
临界区使用场景
互斥访问使用场景
互锁的形成与避免

实时性窗口

推荐此系列文章: https://blog.51cto.com/u_15288275/2975971?articleABtest=0

以下是个人对此系列文章理解:

异常排查方法与工具

jlink-Command(在线)

SystemView(在线)

在这里插入图片描述

Ozone(在线)

在这里插入图片描述

离线分析工具

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_36969440/article/details/131457492

智能推荐

什么是内部类?成员内部类、静态内部类、局部内部类和匿名内部类的区别及作用?_成员内部类和局部内部类的区别-程序员宅基地

文章浏览阅读3.4k次,点赞8次,收藏42次。一、什么是内部类?or 内部类的概念内部类是定义在另一个类中的类;下面类TestB是类TestA的内部类。即内部类对象引用了实例化该内部对象的外围类对象。public class TestA{ class TestB {}}二、 为什么需要内部类?or 内部类有什么作用?1、 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据。2、内部类可以对同一个包中的其他类隐藏起来。3、 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。三、 内部类的分类成员内部_成员内部类和局部内部类的区别

分布式系统_分布式系统运维工具-程序员宅基地

文章浏览阅读118次。分布式系统要求拆分分布式思想的实质搭配要求分布式系统要求按照某些特定的规则将项目进行拆分。如果将一个项目的所有模板功能都写到一起,当某个模块出现问题时将直接导致整个服务器出现问题。拆分按照业务拆分为不同的服务器,有效的降低系统架构的耦合性在业务拆分的基础上可按照代码层级进行拆分(view、controller、service、pojo)分布式思想的实质分布式思想的实质是为了系统的..._分布式系统运维工具

用Exce分析l数据极简入门_exce l趋势分析数据量-程序员宅基地

文章浏览阅读174次。1.数据源准备2.数据处理step1:数据表处理应用函数:①VLOOKUP函数; ② CONCATENATE函数终表:step2:数据透视表统计分析(1) 透视表汇总不同渠道用户数, 金额(2)透视表汇总不同日期购买用户数,金额(3)透视表汇总不同用户购买订单数,金额step3:讲第二步结果可视化, 比如, 柱形图(1)不同渠道用户数, 金额(2)不同日期..._exce l趋势分析数据量

宁盾堡垒机双因素认证方案_horizon宁盾双因素配置-程序员宅基地

文章浏览阅读3.3k次。堡垒机可以为企业实现服务器、网络设备、数据库、安全设备等的集中管控和安全可靠运行,帮助IT运维人员提高工作效率。通俗来说,就是用来控制哪些人可以登录哪些资产(事先防范和事中控制),以及录像记录登录资产后做了什么事情(事后溯源)。由于堡垒机内部保存着企业所有的设备资产和权限关系,是企业内部信息安全的重要一环。但目前出现的以下问题产生了很大安全隐患:密码设置过于简单,容易被暴力破解;为方便记忆,设置统一的密码,一旦单点被破,极易引发全面危机。在单一的静态密码验证机制下,登录密码是堡垒机安全的唯一_horizon宁盾双因素配置

谷歌浏览器安装(Win、Linux、离线安装)_chrome linux debian离线安装依赖-程序员宅基地

文章浏览阅读7.7k次,点赞4次,收藏16次。Chrome作为一款挺不错的浏览器,其有着诸多的优良特性,并且支持跨平台。其支持(Windows、Linux、Mac OS X、BSD、Android),在绝大多数情况下,其的安装都很简单,但有时会由于网络原因,无法安装,所以在这里总结下Chrome的安装。Windows下的安装:在线安装:离线安装:Linux下的安装:在线安装:离线安装:..._chrome linux debian离线安装依赖

烤仔TVの尚书房 | 逃离北上广?不如押宝越南“北上广”-程序员宅基地

文章浏览阅读153次。中国发达城市榜单每天都在刷新,但无非是北上广轮流坐庄。北京拥有最顶尖的文化资源,上海是“摩登”的国际化大都市,广州是活力四射的千年商都。GDP和发展潜力是衡量城市的数字指...

随便推点

java spark的使用和配置_使用java调用spark注册进去的程序-程序员宅基地

文章浏览阅读3.3k次。前言spark在java使用比较少,多是scala的用法,我这里介绍一下我在项目中使用的代码配置详细算法的使用请点击我主页列表查看版本jar版本说明spark3.0.1scala2.12这个版本注意和spark版本对应,只是为了引jar包springboot版本2.3.2.RELEASEmaven<!-- spark --> <dependency> <gro_使用java调用spark注册进去的程序

汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用_uds协议栈 源代码-程序员宅基地

文章浏览阅读4.8k次。汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用,代码精简高效,大厂出品有量产保证。:139800617636213023darcy169_uds协议栈 源代码

AUTOSAR基础篇之OS(下)_autosar 定义了 5 种多核支持类型-程序员宅基地

文章浏览阅读4.6k次,点赞20次,收藏148次。AUTOSAR基础篇之OS(下)前言首先,请问大家几个小小的问题,你清楚:你知道多核OS在什么场景下使用吗?多核系统OS又是如何协同启动或者关闭的呢?AUTOSAR OS存在哪些功能安全等方面的要求呢?多核OS之间的启动关闭与单核相比又存在哪些异同呢?。。。。。。今天,我们来一起探索并回答这些问题。为了便于大家理解,以下是本文的主题大纲:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JCXrdI0k-1636287756923)(https://gite_autosar 定义了 5 种多核支持类型

VS报错无法打开自己写的头文件_vs2013打不开自己定义的头文件-程序员宅基地

文章浏览阅读2.2k次,点赞6次,收藏14次。原因:自己写的头文件没有被加入到方案的包含目录中去,无法被检索到,也就无法打开。将自己写的头文件都放入header files。然后在VS界面上,右键方案名,点击属性。将自己头文件夹的目录添加进去。_vs2013打不开自己定义的头文件

【Redis】Redis基础命令集详解_redis命令-程序员宅基地

文章浏览阅读3.3w次,点赞80次,收藏342次。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。当数据量很大时,count 的数量的指定可能会不起作用,Redis 会自动调整每次的遍历数目。_redis命令

URP渲染管线简介-程序员宅基地

文章浏览阅读449次,点赞3次,收藏3次。URP的设计目标是在保持高性能的同时,提供更多的渲染功能和自定义选项。与普通项目相比,会多出Presets文件夹,里面包含着一些设置,包括本色,声音,法线,贴图等设置。全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,主光源和附加光源在一次Pass中可以一起着色。URP:全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,一次Pass可以计算多个光源。可编程渲染管线:渲染策略是可以供程序员定制的,可以定制的有:光照计算和光源,深度测试,摄像机光照烘焙,后期处理策略等等。_urp渲染管线

推荐文章

热门文章

相关标签