type
status
slug
date
summary
tags
category
password
icon
任务切换
这里,可以对照找参考手册—来自pm0214-stm32-cortexm4-mcus-and-mpus-programming-manual-stmicroelectronics
对于它,我们这里置位:
0001 0000 0000 0000 0000 0000 0000 0000
实机出来,设置产生:
0001 0000 1000 0000 1110 0000 0000 0000
对应设置28位为1,其余全0
由此,目前在里面pending的是E第14号exception(也就是pendsv)
会转去执行PendSV中断
PendSV中断的全称是Pendable Service Call Interrupt。在ARM Cortex-M系列的微处理器中,PendSV中断通常用于操作系统的任务切换,因为它可以被延迟到所有其他中断处理完毕后再执行,这使得它非常适合用于任务切换。
PendSV PendSV is an interrupt-driven request for system-level service. In an OS environment, use PendSV for context switching when no other exception is active.
进来了,看看寄存器变化:0x80e
知道问题的原因了,我们没有指定PSP的值,因为没有按照分配任务块的方式来执行,所以解决方法就是让我们把任务创建的模块也加入进来。
额外看看关于exception和interrupt,在stm32里面
To simplify the software layer, the CMSIS only uses IRQ numbers and therefore uses negative values for exceptions other than interrupts. The IPSR(Interrupt Program status register) returns the Exception number.
Configurable priority values are in the range 0-15. This means that the Reset, Hard fault, and NMI exceptions, with fixed negative priority values, always have higher priority than any other exception.
If multiple pending exceptions have the same priority, the pending exception with the lowest exception number takes precedence. For example, if both IRQ[0] and IRQ[1] are pending and have the same priority, then IRQ[0] is processed before IRQ[1].
ok,我们可以实现pendSV_Handler了,参考官方移植的文件,名字是OS_CPU_PendSVHandler
这里有个非常非常奇怪的立即数:
#:lower16:OS_KA_BASEPRI_Boundary
为了解释这一点,我们先看BASEPRI,上手册:
The BASEPRI register defines the minimum priority for exception processing. WhenBASEPRI is set to a nonzero value, it prevents the activation of all exceptions with same or lower priority level as the BASEPRI value.
关于OS_KA_BASEPRI_Boundary
MOVT
是ARM汇编语言中的一条指令,全称是 "Move Top"。这条指令用于将一个16位的立即数(即直接给出的数值,而不是存储在寄存器中的数值)移动到一个32位寄存器的高16位,同时保持寄存器的低16位不变。
其实就是这里把OS_KA_BASEPRI_Boundary变量的地址给了R2,这样做是避免32位立即数没办法用8位加上4位移位的循环右移表示(但是感觉不严谨,16位也有可能无法表示)
这一截都是在设置屏蔽中断,注意移位的操作,以及对于BASEPRI的理解
对于OSPrioCur这个是当前任务优先级
其语法格式为:
MOVT Rd, #imm16
,其中 Rd
是目标寄存器,#imm16
是一个16位的立即数。例如,
MOVT R0, #0x1234
会将 0x1234
这个值放入寄存器 R0
的高16位,而 R0
的低16位的值不会改变。这里面的
OSPrioHighRdy
是表示查找到需要切换的最高优先级的任务的prio:算法是优先级位图法,这里我们暂且只研究64个优先级支持
根据我们的配置,同时可以给出sched的删减版:
还有关于OSTCBCur变量,它是指向当前正在运行的TCB块的指针,同样还有OSTCBHighRdy,它指向最高优先级
下面是之前在OS_Sched 函数里面的操作,得到当前最高优先级的TCB块
后面这里很重要:
调整PSP到新的栈指针,R2从上次的操作而来,存的是OSTCBHighRdy的内容,是指向当前切换的TCB的指针,它的第一个位置内容是栈帧SP,
那么对于
ORR LR, R4, #0x04
的理解很重要:0000 0100
在手册2.3.7 Exception entry and return
进入异常的时候会压栈:
有8个寄存器会被压入:
In parallel to the stacking operation, the processor performs a vector fetch that reads the exception handler start address from the vector table. When stacking is complete, the processor starts executing the exception handler. At the same time, the processor writes an EXC_RETURN value to the LR. This indicates which stack pointer corresponds to the stack frame and what operation mode the was processor was in before the entry occurred.
Exception return
EXC_RETURN is the value loaded into the LR on exception entry. The exception
mechanism relies on this value to detect when the processor has completed an exception handler. The lowest five bits of this value provide information on the return stack and processor mode.
这个时候再看看处理器的模式有哪些:
All EXC_RETURN values have bits[31:5] set to one. When this value is loaded into the PC it indicates to the processor that the exception is complete, and the processor initiates the appropriate exception return sequence.
后面在实机上测试看看
让我们的OS能部分跑起来
主要是要支持任务的调度
首先需要 void OSStartHighRdy(void)
实现这个函数,用于最开始启动第一个task
This function triggers a PendSV exception (essentially, causes a context switch) to cause the first task to start.
看看这个函数具体如何实现吧:
第一步 把PendSV的exception优先级设置为最低:
参考一下手册
这里设置成最大,感觉原来的写错了,应该是:
搞错了,是伪指令,并且NVIC_PENDSV_PRI是一个宏
第二步:
Set initial PSP to 0, to tell context switcher this is first run;
第三步:
Set the main stack to OS_CPU_ExceptStkBase
下面是OS_CPU_ExceptStkBase的定义
第四步:
Set OSRunning to TRUE;
第五步:
Get current high priority, OSPrioCur = OSPrioHighRdy;
第六步:
Get current ready thread TCB, OSTCBCur = OSTCBHighRdy;
第七步:
Get new process SP from TCB, SP = OSTCBHighRdy->OSTCBStkPtr;
第八步:
MRS R0, CONTROL
ORR R0, R0, #2
MSR CONTROL, R0
更换使用PSP栈
第九步:
Restore R0-R11 and R14 from new process stack;
这里的顺序需要深究,我们后续讨论,这里可以提一嘴,是来自异常的压栈顺序,和我们设定的任务切换流程相关。
ok,继续想办法让OS启动
看看idle
hook函数我就暂且把他定义为空函数
而OS_TaskStat创建一个statistic函数来收集信息,我们暂且不需要,从osInit中先暂时注释掉
通过我们的初步配置,目前就只需要这些函数来启动我们的OS
接着:OS启动!
通过SchedNew更新OsprioHighRdy得到最新的任务优先级:
来了,对于之前的一些猜想和解释在这里可以慢慢得到验证了:
上面这个伪指令能从下面得到解答
多写写,它这个位置是在所有代码之后(本文件/模块),比如我们在本文件中还有
这个地方同样用到了这个伪指令,你能看到汇编器把它放到了(注意第二个数字是能用mov指令的立即数直接使用的)
这不得不让我开始思考原来的写法是否可以不用多占一部分空间,后面会对比
再来看看往下的效果
记得这里是把PENDSV中断的优先级设置为最低,但是优先级的设置(在401里面)只有最高4位有意义(8位一组),手册上写了的
再往下是设置MSP的值改成我们设置的OS_CPU_ExceptStkBase
r0存的是0x20001744,很怪
现在不奇怪了,乐
所以这三句里面,R1才是存的OS_CPU_ExceptStkBase的值,而R0是存的它的地址
执行是成功的。
跳过一些简单的内容。。。
OSRunning被变成True了
重要的来了:
重头戏:
Exception return occurs when the processor is in Handler mode and executes one of the
following instructions to load the EXC_RETURN value into the PC:
• an LDM or POP instruction that loads the PC
• an LDR instruction with PC as the destination
• a BX instruction using any register.
EXC_RETURN is the value loaded into the LR on exception entry. The exception
mechanism relies on this value to detect when the processor has completed an exception
handler. The lowest five bits of this value provide information on the return stack and
processor mode. Table 18 shows the EXC_RETURN values with a description of the exception return behavior.
All EXC_RETURN values have bits[31:5] set to one. When this value is loaded into the PC it indicates to the processor that the exception is complete, and the processor initiates the appropriate exception return sequence.
ok,我们再看看原来没有用伪指令的写法有什么不一样:
这里一个收获是movw可以接受16位的立即数,但是分析了这种写法:
我觉得意义不大,同样这里为了传递一个数字,需要占用8字节,而之前的方法,伪代码本身指令占用4字节,然后数字会占用4字节,区别不大,速度上也没占很大优势,就是少了一次去取数字的过程,但是感觉这样可读性不如之前的。
为了让我们的这个系统走上正轨,打算切换时钟源为外部HSE配置84Mhz的主频,再搞一个点灯
说干就干
上面是设置PLL模块的分频系数等参数
接下来还需要配置AHB以及APB上的系数:
注意这里FLASH_LATENCY的问题
ok,回到我们的点灯环节
这里,需要实现最重要的延时操作,这里的延时需要结合OS,因为OS的任务切换也是在SYSTick里面💥
前期准备:
这里TCB有个地方和书上的不一样:
在OSTCBStatPend这个变量,在老师的书上,这个变量是一个布尔值,名字叫:OSTCBPendTO用于标识是否任务超时,超时任务标记为true(对应OS_STAT_PEND_TO这个宏),正常状态为false,没有超时(对应OS_STAT_PEND_OK),在这里取消了布尔值而是缓存一个INT8U的变量,是因为还有一个状态需要标定:就是OS_STAT_PEND_ABORT标识为任务取消。
所以我们来看核心的OSTimeTick函数代码,这里删除了宏定义做选择的部分,只保留我们的选择:
解释ptcb->OSTCBStat & OS_STAT_PEND_ANY) != OS_STAT_RDY
这里是在查看是否是在等待一个事件发生,如果是,那么就指明等待超时并且清空所等待的事件,反之,就说明任务不再等待,Pend状态走向OK,下一步再检查是否任务被挂起,如果没有被挂起,那么说明延时时间到了,可以进入就绪队列,在OSRdyGrp和OSRdyTbl上做下记录。
很重要的pend状态信息
再看看它是怎么初始化tick的滴答定时的
所以OS_TICKS_PER_SEC这个变量就是定下来我们想要频率是多少hz,最开始定的是100hz,也就是1s100次tick。传入的参数是我们cpu的主频
对于寄存器的设置,还真的一个个查手册了:(SYStick编号是15)
这里有个比较大的问题是,SYSTick设置的优先级是在被Basepri屏蔽的中断里面的最高优先级,但是如果ENTER_CRITICAL()了的话,真的不能在那里面触发systick,感觉为什么不把systick抬到最高优先级呢?让他不能被BASEpri设置的值屏蔽,不是很理解。
还有
TICKINT开启了中断触发
Reload的值有24位:
l To generate a multi-shot timer with a period of N processor clock cycles, use a RELOAD value of N-1. For example, if the SysTick interrupt is required every 100 clock pulses, set RELOAD to 99.
l To deliver a single SysTick interrupt after a delay of N processor clock cycles, use a RELOAD of value N. For example, if a SysTick interrupt is required after 100 clock pulses, set RELOAD to 99.
☝️上面解释了为啥我们要设置cnts-1
看看要点哪个灯来着:
为了点亮输出到PA5,这里我们使用GPIO的几个寄存器
第一个是输出模式选择(输入还是输出还是复用)
第二个是输出类型—开漏,推挽
再设置速度
设置为没有上下拉
最后设置输出状态:
出了一个小bug,之前时钟配置的时候外设的时钟没有开
完成了,挺好的,但是接下来需要去实现任务的pending操作:
任务的Pending
核心函数是OSTimeDly()
简单,就是把当前任务踢出就绪队列(这是抽象的,实际上的做法是在RdyTbl上设置0,以及可能会设置RdyGrp的行位为0),并且设置OSTCBDly变量的值为设定的tick,然后执行重调度
一个很骚的实现5s闪烁灯:
探究任务切换的栈的问题:
这是刚刚进入PendSV_Handler之后的情况,
这个时候sp的值更新为msp,这是源于进入中断后,control寄存器的特定位会置位,使得sp使用msp的指针,而之前的xPSR, PC, LR, R12, R0-R3 8个寄存器已经被保存了
这里可以看到,因为这里是第一个任务调用,是主动调用的任务切换,保存的用于恢复的PC是指向OS_Sched代码的(估计是,不知道咋根据这个找具体的行号)
懂了,之前一直被绕,回归到我们任务切换的原始目的:把当前任务的一切都保存
来探讨一下关于第二种ENTER_CRITICAL的方式的问题:
上课老师是说对于函数传参过程会造成影响,这里我们实验来检验:
函数如下:
cli是汇编写的函数:
来打断点执行吧:
参数传递:
多加一个参数看看
跳入函数:
先把lr和r7入栈,因为后面要是用
然后就用sub sp,#24分配了24字节的空间
顺带提一下:cortex-M系列只有thumb指令集,不过cortex-M3以后的因为32位支持所以支持thumb2指令集。说这个是因为你可以看到lr返回的值是0x0800042f原因就是在于thumb指令集的标定
来自armv7的参考手册
执行a3=a1+a2,先要取出来,这个取出是来自于r7的相对寻址:
同样的,要执行a4 = a3+a5
再执行局部变量a7=a6+1
把返回值给到r0
看起来r7已经完成了所有的过程,以它为基准,来标定传入的参数和局部变量的地址,没什么问题。
可以多几个局部变量看看:
很怪,这里就把之前没有用的16偏移量的位置用上了
我的猜想是,栈空间是8字节对齐的,我们的链接脚本是这么写的:
再多加入一个32位(4字节)的局部变量你可以看到,sp增加到了32字节的分配
例子如下:
你会发现r7+16的地址是空着的没有使用
然后你再加一个变量,凑齐32字节:
任务通信
在uc/OS II 中,所有的信号都被看作事件(event),这些事件可以是信号量、邮箱或者消息队列等。即用于通讯的数据结构叫做事件控制块。
这里强调一个成员:OSEventTbl[OS_EVENT_TBL_SIZE]
这是等待任务队列表,它是一个位图表,OS_EVENT_TBL_SIZE表示此位图表的最大行数(当最低优先级小于63时,位图表最多为8行)
这里有个很重要的点:
与任务就绪表一样,每个等待事件发生的任务都被加入事件控制块的任务等待队列中,该列表包括OSEventGrp和OSEventTbl[]两个域
ECB初始化:
☝️上面这个有些宏选项,看起烦,我删减出我们的选择(OS_MAX_EVENTS=10 、OS_EVENT_NAME_EN=0):
初始化代码
这里的操作和之前构建OSTCBFreelist很像
OS_InitEventList 根据所定义的最大事件数量(OS_MAX_EVENTS)创建空闲的事件控制块链表OSEventFreeList
event的type选择
event的type选择有如下:
至此,我们的空闲事件控制块链表就创建好了
例子,信号量的创建,从Free里面取:
同样的,做一下删减,筛选我们的选择:
信号量创建代码
OSEventFreeList->OSEventPtr;这里是将指针移向下一个空闲,因为第一个空闲已经出链表了,注意它的next指针叫做:OSEventPtr
初始化事件控制块的任务等待队列
通过调用OS_EventWaitListInit()函数,可以对事件控制块的任务等待队列进行初始化。该函数初始化一个空的任务等待队列,其中没有任何任务。该函数只有一个参数,就是指向需要初始化的时间控制块的指针pevent
代码:OS_EventWaitListInit
这里,提一句:OSEventGrp 和OSEventTbl的算法功能和之前OSRdyGrp和OSRdyTbl是一样的,但是后者是两个全局变量,而前者是每个Event(包括信号量、邮箱、消息队列)
这样就能理解了,信号量是对应好几个任务的,等待信号量的任务都位于OSEventGrp和OSEventTbl内,但是根据这个怎么实现信号量的同步操作,不是很懂,再往后面看看,这里留下问题。
那信号量创建了,过程中调用了OS_EventWaitListInit把事件等待队列清空了,我们肯定就有一个操作来把任务加入到事件的等待队列里面:
将一个任务加入事件的等待队列,以信号量为例子
调用OSSemPend()
代码简化结果OSSemPend
上面主要是:
设置了TCB在pend 一个信号量 pend on semaphore(通过OSTCBStat),并设置(OSTCBStatPend)来等待tcb等待状态,开始是正常,没有超时,也没有abort(关于abort这个状态目前还不是很懂用来干嘛的),再设置了等待时间,按照tick来算
接下来是对它调用的几个函数的重点解析:
OS_EventTaskWait
此函数将此任务从就绪队列中删除,并将此任务加入信号量控制块的任务等待队列中
REMOVE TASK FROM EVENT WAIT LIST
OS_EventTaskRemove
Description: Remove a task from an event's wait list.
Note : This function is INTERNAL to uC/OS-II and your application should not call it.
就是把任务从事件的等待队列中去除
那除了超时,还可能这个事件被触发(对信号量来说就是信号量增加),这个时候需要有操作来将一个任务挂载到就绪队列
事件触发
以信号量为例子:
OSSemPost筛选版
这里面最重要的是在有任务等待时,转而去调用的OS_EventTaskRdy函数,这个函数就是去找最高优先级的在事件等待队列的任务,这里注意一点,后面也会再重复说:该最高优先级虽然能得到事件(这里是信号量),但不一定能进入就绪状态,也许该任务已经由于其它原因被挂起了。
(void)OS_EventTaskRdy(pevent, (void *)0, OS_STAT_SEM, OS_STAT_PEND_OK);
OS_EventTaskRdy筛选版
MAKE TASK READY TO RUN BASED ON EVENT OCCURING
Description: This function is called by other uC/OS-II services and is used to ready a task that was waiting for an event to occur.
Note : This function is INTERNAL to uC/OS-II and your application should not call it.
函数在os_core.c里面
无等待地请求一个信号量
OSSemAccept筛选版
把这个和之前的pend对比一下
就是取消了后续将任务加入等待队列,并且切换任务,当任务返回的时候工具OSTCBStatPend来处理是否将TCB从事件等待队列里面移除。
互斥锁
暂停一下
- 作者:liamY
- 链接:https://liamy.clovy.top/article/school/ucosII/start
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。