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 PROCEXPORT rt_hw_context_switch_to   ; 导出rt_hw_context_switch_to,让其具有全局属性,可以在C 文件调用LDR r1, =rt_interrupt_to_thread  ; 将rt_interrupt_to_thread 的地址加载到r1STR r0, [r1]                     ; 将r0 的值存储到rt_interrupt_to_threadLDR r1, =rt_interrupt_from_thread; 将rt_interrupt_from_thread 的地址加载到r1MOV r0, #0x0                     ; 配置r0 等于0 , ;设置rt_interrupt_from_thread 的值为0,表示启动第一次线程切换STR r0, [r1]                     ; 将r0 的值存储到rt_interrupt_from_threadLDR r1, =rt_thread_switch_interrupt_flag  ; 设置中断标志位rt_thread_switch_interrupt_flag 的值为1 MOV r0, #1                       ; 配置r0 等于1STR r0, [r1]                     ; 将r0 的值存储到rt_thread_switch_interrupt_flag; 设置PendSV 异常的优先级LDR r0, =NVIC_SYSPRI2LDR r1, =NVIC_PENDSV_PRILDR.W r2, [r0,#0x00] ; 读ORR r1,r1,r2 ; 改STR r1, [r0] ; 写; 触发PendSV 异常(产生上下文切换)LDR r0, =NVIC_INT_CTRLLDR r1, =NVIC_PENDSVSETSTR r1, [r0]; 开中断CPSIE FCPSIE 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 PROCEXPORT PendSV_HandlerMRS r2, PRIMASK  ; 失能中断,为了保护上下文切换不被中断CPSID ILDR r0, =rt_thread_switch_interrupt_flag   ;加载rt_thread_switch_interrupt_flag 的地址到r0LDR r1, [r0]                               ; 加载rt_thread_switch_interrupt_flag 的值到r1CBZ r1, pendsv_exit                        ;判断r1 是否为0,为0 则跳转到pendsv_exitMOV r1, #0x00                              ; r1 不为0 则清0 STR r1, [r0]                               ; 将r1 的值存储到rt_thread_switch_interrupt_flag,即清0LDR r0, =rt_interrupt_from_thread          ; 加载rt_interrupt_from_thread 的地址到r0LDR r1, [r0]                               ; 加载rt_interrupt_from_thread 的值到r1CBZ 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             ; 获取线程栈指针到r1STMFD r1!, {r4 - r11}   ; 将CPU 寄存器r4~r11 的值存储到r1 指向的地址(每操作一次地址将递减一次)LDR r0, [r0]            ; 加载r0 指向值到r0,即r0=rt_interrupt_from_threadSTR r1, [r0]            ; 将r1 的值存储到r0,即更新线程栈sp;========================================================================================================;下文切换;========================================================================================================switch_to_threadLDR 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,即spLDMFD r1!, {r4 - r11} ; 将线程栈指针r1(操作之前先递减) 指向的内容加载到CPU 寄存器r4~r11MSR psp, r1     ; 将线程栈指针更新到PSPpendsv_exitMSR 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(在线)

在这里插入图片描述

离线分析工具

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

centos版本的EDA虚拟机搭建3

文章目录 0、参考博客1、配置虚拟机与主机共享文件夹。2、安装unrar和rar3、EDA软件正式安装4、gtkwave与iverilog安装5、安装vscode6、安装wine软件7、notepad安装 0、参考博客 1、CentOS 7 下 rar unrar的安装 1、配置虚拟机与主机共享文件夹。 **前提&#xff0c;虚拟机关…

GLP-1爆火2023:神药显雏形,争夺引内卷

2023年过半&#xff0c;如果要从创新药角度做一份总结&#xff0c;什么赛道、哪类药物会是“当红炸子鸡”&#xff1f;答案一定是GLP-1类药物。 原本用于治疗二型糖尿病的药物&#xff0c;在国内社交媒体上&#xff0c;关于司美格鲁肽的减肥奇效&#xff0c;甚至引发了一股抢药…

css对盒模型的理解

面试碰到的一个问题&#xff0c;记录一下 ’ CSS3中的盒模型有以下两种&#xff1a;标准盒子模型、IE盒子模型 盒模型都是由四个部分组成的: 分别是margin、border、padding和content。 标准盒模型和IE盒模型的区别在于设置width和height时&#xff0c;所对应的范围不同&#…

谷歌Play应用商店下架具有内置自行下载APK能力的应用

近日有程序员在V2EX论坛发帖表示自己用开源框架uni-app开发的App被 Google Play下架&#xff0c;而根据这位网友的说法&#xff0c;下架原因疑似是uni-app自带的SDK包含违反Google Play政策的“内置自行下载APK能力”代码及广告相应的代码。 据悉&#xff0c;uni-app是一个使用…

基于vue+Element Table 表格的封装

项目场景&#xff1a; 项目场景&#xff1a;需要频繁使用列表进行呈现数据&#xff0c;不可能每次都写一个表格&#xff0c;可以将表格封装为一个组件&#xff0c;在需要使用时可以直接调用。 效果展示&#xff1a; 项目结构&#xff1a; 具体实现&#xff1a; Table.vue <…

PHP 设备管理系统 mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP 设备管理系统 是一套完善的WEB设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 视频演示 PHP 设备管理系统 mysql数据库web结构apache 代码下载 https://download.csd…

NLP入门:word2vec self-attention transformer diffusion的技术演变

这一段时间大模型的相关进展如火如荼&#xff0c;吸引了很多人的目光&#xff1b;本文从nlp领域入门的角度来总结相关的技术路线演变路线。 1、introduction 自然语言处理&#xff08;Natural Language Processing&#xff09;&#xff0c;简称NLP&#xff0c;是通过统计学、…

预约直播 | 展心展力MetaApp:基于DeepRec的稀疏模型训练实践

MetaApp-推荐广告研发部&#xff0c;主要负责 MetaApp 拳头产品 233 乐园的首页信息流的推荐和广告系统&#xff0c;是比较传统的推广搜组。本次议题介绍了 MetaApp-推荐广告研发部 从传统的TensorFlow&#xff08;cpu&#xff09;自研分布式ps方案&#xff0c;逐步迁移到使用…

二进制搭建Kubernetes集群(三)——部署多master

本文将完成多master集群的部署&#xff0c;即部署master02&#xff0c;以及nginx负载均衡、keepalived高可用 多master集群架构图&#xff1a; 架构说明&#xff1a; node节点的kubelet只能对接一个master节点的apiserver&#xff0c;不可能同时对接多个master节点的apiserver…

Spring Cloud Alibaba体系使用Nacos作为服务注册发现与配置中心

文章目录 Nacos介绍服务注册发现Nacos Discovery引入Nacos DiscoveryProvider和Consumer示例ProviderConsumer Nacos Discovery Starter其他配置选项 服务注册发现Nacos Config引入Nacos Config快速接入配置自动刷新profile粒度控制自定义namespace配置支持自定义Group支持自定…

vue2和vue3的渲染过程简述版

文章目录 vue2渲染过程vue3渲染过程优化和扩充 vue2和vue3对比 vue2渲染过程 在Vue 2的渲染过程中&#xff0c;包括以下几个关键步骤&#xff1a; 解析模板&#xff1a;Vue 2使用基于HTML语法的模板&#xff0c;首先会将模板解析成抽象语法树&#xff08;AST&#xff09;&…

常用git操作总结

文章目录 一、git 分支命名规范&#xff08;1&#xff09;master 主分支&#xff08;2&#xff09;develop 开发分支&#xff08;3&#xff09;feature 分支&#xff08;一般简写为feat&#xff09;&#xff08;4&#xff09;hotfix 分支&#xff08;一般简写为fix&#xff09;…