写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。
标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。
点击此处进入学习日记的总目录
2024.03.04
- 十四、UCOSIII:main()函数
- 1、编写函数并仿真
- 2、常见错误:
- 1. 只有一个波峰
- 2. 只有一条直线
- 十五、UCOSIII:前十五章总结
- 1、程序的关键
- 2、PendSV异常的作用
- 3、前六节代码的运行流程
- 1. 手动配置任务1为 优先级最高的任务
- 2.触发PendSV异常
- 3. 运行异常代码,把当前运行的任务改成优先级最高的任务
- 4. 运行任务1
- 5. 手动配置任务2为 优先级最高的任务,然后触发PendSV异常
- 6. 运行异常代码,把当前运行的任务改成优先级最高的任务
- 7.运行任务2
- 8. 手动配置任务1为 优先级最高的任务,然后触发PendSV异常
- 9. 运行异常代码,把当前运行的任务改成优先级最高的任务
- 10. 重复流程4 - 9,直至程序结束
- 4、第六节之后代码的运行流程
十四、UCOSIII:main()函数
1、编写函数并仿真
main()函数在文件app.c中编写,其中app.c文件如下
/*
*******************************************************************
* 包含的头文件
*******************************************************************
*/
#include"os.h"
#include"ARMCM3.h"
/*
*******************************************************************
* 宏定义
*******************************************************************
*/
/*
*******************************************************************
* 全局变量
*******************************************************************
*/
uint32_t flag1;
uint32_t flag2;
/*
*******************************************************************
* TCB & STACK &任务声明
*******************************************************************
*/
#define TASK1_STK_SIZE 20
#define TASK2_STK_SIZE 20
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
static OS_TCB Task1TCB;
static OS_TCB Task2TCB;
void Task1( void *p_arg );
void Task2( void *p_arg );
/*
*******************************************************************
* 函数声明
*******************************************************************
*/
void delay(uint32_t count);
/*
*******************************************************************
* main()函数
*******************************************************************
*/
/*
* 注意事项:1、该工程使用软件仿真,debug需选择 Ude Simulator
* 2、在Target选项卡里面把晶振Xtal(Mhz)的值改为25,默认是12,
* 改成25是为了跟system_ARMCM3.c中定义的__SYSTEM_CLOCK相同,
* 确保仿真的时候时钟一致
*/
int main(void)
{
OS_ERR err;
/* 初始化相关的全局变量 */
OSInit(&err);
/* 创建任务 */
OSTaskCreate ((OS_TCB*) &Task1TCB,
(OS_TASK_PTR ) Task1,
(void *) 0,
(CPU_STK*) &Task1Stk[0],
(CPU_STK_SIZE) TASK1_STK_SIZE,
(OS_ERR *) &err);
OSTaskCreate ((OS_TCB*) &Task2TCB,
(OS_TASK_PTR ) Task2,
(void *) 0,
(CPU_STK*) &Task2Stk[0],
(CPU_STK_SIZE) TASK2_STK_SIZE,
(OS_ERR *) &err);
/* 将任务加入到就绪列表 */
OSRdyList[0].HeadPtr = &Task1TCB;
OSRdyList[1].HeadPtr = &Task2TCB;
/* 启动OS,将不再返回 */
OSStart(&err);
}
/*
*******************************************************************
* 函数实现
*******************************************************************
*/
/* 软件延时 */
void delay (uint32_t count)
{
for (; count!=0; count--);
}
/* 任务1 */
void Task1( void *p_arg )
{
for ( ;; ) {
flag1 = 1;
delay( 100 );
flag1 = 0;
delay( 100 );
/* 任务切换,这里是手动切换 */
OSSched();
}
}
/* 任务2 */
void Task2( void *p_arg )
{
for ( ;; ) {
flag2 = 1;
delay( 100 );
flag2 = 0;
delay( 100 );
/* 任务切换,这里是手动切换 */
OSSched();
}
}
所有代码在本小节之前都有循序渐进的讲解,这里这是融合在一起放在main()函数中。
其实现在Task1和Task2并不会真正的自动切换,而是在各自的函数体里面加入了OSSched()函数来实现手动切换
/* 任务切换,实际就是触发PendSV异常,然后在PendSV异常中进行上下文切换 */
void OSSched (void)
{
if( OSTCBCurPtr == OSRdyList[0].HeadPtr )
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
OS_TASK_SW();
}
OSSched()函数的调度算法很简单,即如果当前任务是任务1,那么下一个任务就是任务2,如果当前任务是任务2,那么下一个任务就是任务1, 然后再调用OS_TASK_SW()函数触发PendSV异常,最后在PendSV异常里面实现任务的切换。
在往后的章节中,我们将继续完善,加入SysTick中断, 从而实现系统调度的自动切换。
OS_TASK_SW()函数其实是一个宏定义,具体是往中断及状态控制寄存器SCB_ICSR的位28(PendSV异常启用位)写入1, 从而触发PendSV异常。OS_TASK_SW()函数在os_cpu.h文件中实现
仿真后可得flag1 和 flag2的图像为
2、常见错误:
1. 只有一个波峰
这个问题主要是编译器优化等级太低,导致堆栈冲突,OSTCBHighRdyPtr指针
和 OSTCBCurPtr指针
在STMDB
指令运行时被错误覆盖
此时 任务的切换 被破坏,系统陷入死循环,示波器 中只有一个波峰
解决方法是修改编译器优化等级
修改为O1或其他,可以挨个试
改完一定要 Build 才会生效
2. 只有一条直线
这个问题主要是编译器优化等级太高,导致时延函数被“优化”了
前面有绿块才代表被编译,delay直接被跳过去了
NND,偷我代码是吧!
解决办法为使用volatile
关键字
volatile英文意思为易变的、易挥发的,在声明变量时加入这个关键字,意思就是告诉编译器这个变量随时能被外部修改,不要对此变量进行优化,代码中引用此变量必须访问内存中实际变量。
************************************************************************************************************************
* 函数实现
************************************************************************************************************************
*/
/* 软件延时 */
void delay (volatile uint32_t count)
{
for(; count!=0; count--);
}
给delay函数的 count变量加一个volatile
关键字。
编译!仿真!
搞定!
参考资料:
C语言volatile用法/Keil编译器优化/delay被编译器优化
题外话:
这几个错误足足耗费了我三天时间,一点一点跟着代码看寄存器和变量的变化,不懂就搜,就查,提出各种猜测又全部否决
深夜公司里只有一个人,倒一杯热水看代码,喝第一口时水已经凉了
一个人的学习是漫漫长征,荆棘密布,坎坷不断。
但还好我们还有时间,与诸位共勉
十五、UCOSIII:前十五章总结
1、程序的关键
如果从头到尾把前六节做下来的话,可以体会到目前程序的关键
那就是 触发PendSV异常
即以下语句:
NVIC_INT_CTRL = NVIC_PENDSVSET
其中NVIC_INT_CTRL声明如下,即中断控制及状态寄存器 SCB_ICSR
#ifndef NVIC_INT_CTRL
#define NVIC_INT_CTRL *((CPU_REG32 *)0xE000ED04) /* 中断控制及状态寄存器 SCB_ICSR */
#endif
NVIC_PENDSVSET声明如下,即一个第28位为1的十六进制数
#ifndef NVIC_PENDSVSET
#define NVIC_PENDSVSET 0x10000000 /* 触发PendSV异常的值 Bit28:PENDSVSET */
#endif
不管是在每个任务中都会用到的OS_TASK_SW()
函数
#define OS_TASK_SW() NVIC_INT_CTRL = NVIC_PENDSVSET
还是再启动函数OSStart(&err)
中的任务切换函数OSStartHighRdy()
OSStartHighRdy
LDR R0, = NVIC_SYSPRI14 ; 设置 PendSV 异常优先级为最低
LDR R1, = NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; 设置psp的值为0,开始第一次上下文切换
MSR PSP, R0
LDR R0, =NVIC_INT_CTRL ; 触发PendSV异常
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; 开中断
全部都是通过给中断控制及状态寄存器 SCB_ICSR
赋值0x10000000
来 触发PendSV异常
2、PendSV异常的作用
如果说程序的关键是触发PendSV异常
那么PendSV异常的作用就是 切换任务
在PendSV异常函数中,代码如下
;********************************************************************************************************
; PendSVHandler异常
;********************************************************************************************************
PendSV_Handler
; 任务的保存,即把CPU寄存器的值存储到任务的堆栈中
CPSID I ; 关中断,NMI和HardFault除外,防止上下文切换被中断
MRS R0, PSP ; 将psp的值加载到R0
CBZ R0, OS_CPU_PendSVHandler_nosave ; 判断R0,如果值为0则跳转到OS_CPU_PendSVHandler_nosave
; 进行第一次任务切换的时候,R0肯定为0
;-----------------------一、保存上文-----------------------------
; 任务的切换,即把下一个要运行的任务的栈内容加载到CPU寄存器中
; 在进入PendSV异常的时候,当前CPU的xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0会自动存储到当前任务堆栈,同时递减PSP的值
;--------------------------------------------------------------
STMDB R0!, {R4-R11} ; 手动存储CPU寄存器R4-R11的值到当前任务的堆栈
LDR R1, = OSTCBCurPtr ; 加载 OSTCBCurPtr 指针的地址到R1,这里LDR属于伪指令
LDR R1, [R1] ; 加载 OSTCBCurPtr 指针到R1,这里LDR属于ARM指令
STR R0, [R1] ; 存储R0的值到 OSTCBCurPtr->OSTCBStkPtr,这个时候R0存的是任务空闲栈的栈顶
;-----------------------二、切换下文-----------------------------
; 实现 OSTCBCurPtr = OSTCBHighRdyPtr
; 把下一个要运行的任务的栈内容加载到CPU寄存器中
; 任务的切换,即把下一个要运行的任务的堆栈内容加载到CPU寄存器中
;--------------------------------------------------------------
OS_CPU_PendSVHandler_nosave
; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R0, = OSTCBCurPtr ; 加载 OSTCBCurPtr 指针的地址到R0,这里LDR属于伪指令
LDR R1, = OSTCBHighRdyPtr ; 加载 OSTCBHighRdyPtr 指针的地址到R1,这里LDR属于伪指令
LDR R2, [R1] ; 加载 OSTCBHighRdyPtr 指针到R2,这里LDR属于ARM指令
STR R2, [R0] ; 存储 OSTCBHighRdyPtr 到 OSTCBCurPtr
LDR R0, [R2] ; 加载 OSTCBHighRdyPtr 到 R0
LDMIA R0!, {R4-R11} ; 加载需要手动保存的信息到CPU寄存器R4-R11
MSR PSP, R0 ; 更新PSP的值,这个时候PSP指向下一个要执行的任务的堆栈的栈底(这个栈底已经加上刚刚手动加载到CPU寄存器R4-R11的偏移)
ORR LR, LR, #0x04 ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
CPSIE I ; 开中断
BX LR ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在STM32中,堆栈是由高地址向低地址生长的。
NOP ; 为了汇编指令对齐,不然会有警告
END ; 汇编文件结束
可以清楚看到系统在这里只完成了一个操作
- 实现
OSTCBCurPtr = OSTCBHighRdyPtr
,即把当前运行的任务
改成优先级最高的任务
,实现任务的切换
然后在每个任务完成后再触发PendSV异常,实现整个系统的流转运行
3、前六节代码的运行流程
省略初始化、宏定义、变量定义等等一系列流程,我们只看任务运行流程
1. 手动配置任务1为 优先级最高的任务
void OSStart (OS_ERR *p_err)
{
if ( OSRunning == OS_STATE_OS_STOPPED ) {(1)
/* 手动配置任务1先运行 */
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;(2)
/* 启动任务切换,不会返回 */
OSStartHighRdy();(3)
/* 不会运行到这里,运行到这里表示发生了致命的错误 */
*p_err = OS_ERR_FATAL_RETURN;
}
else
{
*p_err = OS_STATE_OS_RUNNING;
}
}
2.触发PendSV异常
;********************************************************************************************************
; 开始第一次上下文切换
; 1、配置PendSV异常的优先级为最低
; 2、在开始第一次上下文切换之前,设置psp=0
; 3、触发PendSV异常,开始上下文切换
;********************************************************************************************************
OSStartHighRdy
LDR R0, = NVIC_SYSPRI14 ; 设置 PendSV 异常优先级为最低
LDR R1, = NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; 设置psp的值为0,开始第一次上下文切换
MSR PSP, R0
LDR R0, =NVIC_INT_CTRL ; 触发PendSV异常
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; 开中断
3. 运行异常代码,把当前运行的任务改成优先级最高的任务
4. 运行任务1
5. 手动配置任务2为 优先级最高的任务,然后触发PendSV异常
#define OS_TASK_SW() NVIC_INT_CTRL = NVIC_PENDSVSET
6. 运行异常代码,把当前运行的任务改成优先级最高的任务
7.运行任务2
8. 手动配置任务1为 优先级最高的任务,然后触发PendSV异常
9. 运行异常代码,把当前运行的任务改成优先级最高的任务
10. 重复流程4 - 9,直至程序结束
4、第六节之后代码的运行流程
在第七节及之后,我们将逐渐把任务切换交给SysTick中断, 从而实现系统调度的自动切换。