笔者在嵌入式领域深耕6年,对嵌入式项目构建,BLDC电机控制,产品上位机开发以及产品量产和产品售后维护有多年工作经验。经验分享,从0到1, 让我带你从实际工作的角度走进嵌入式成长之路。
原创不易,欢迎大家关注我的微信公众号:嵌入式工程师成长之路 或 扫下面二维码
所有文章总目录:【电子工程师 qt工程师】
原创视频总目录:【电子工程师 qt工程师】
系统运行后,默认使用的是MSP(主栈),当第一次切换任务,也就是从main函数切换到第一个初始任务时,触发PendSV异常,并设置PSP为0;当进入到PendSV异常处理函数时,会先判断PSP是否为0,如果为0,表示此时是第一次切换任务,直接跳转到PendSVHander_nosave中运行,恢复初始任务,然后返回时设置返回值为0x04,表示之后使用PSP。
任务运行起来之后再发生任务切换,此时PSP不为0,就直接往下运行,硬件会自动将某些寄存器保存在PSP中,另外一些不会自动保存的需要自己保存在任务自己的栈中。
#include "tinyOS.h"
// 当前任务:记录当前是哪个任务正在运行
tTask * currentTask;
// 下一个将即运行的任务:在进行任务切换前,先设置好该值,然后任务切换过程中会从中读取下一任务信息
tTask * nextTask;
// 所有任务的指针数组:简单起见,只使用两个任务
tTask * taskTable[2];
void tTaskInit (tTask * task, void (*entry)(void *), void *param, uint32_t * stack)
{
// 为了简化代码,tinyOS无论是在启动时切换至第一个任务,还是在运行过程中在不同间任务切换
// 所执行的操作都是先保存当前任务的运行环境参数(CPU寄存器值)到堆栈中(如果已经运行运行起来的话),然后再
// 取出从下一个任务的堆栈中取出之前的运行环境参数,然后恢复到CPU寄存器
// 对于切换至之前从没有运行过的任务,我们为它配置一个“虚假的”保存现场,然后使用该现场恢复。
// 注意以下两点:
// 1、不需要用到的寄存器,直接填了寄存器号,方便在IDE调试时查看效果;
// 2、顺序不能变,要结合PendSV_Handler以及CPU对异常的处理流程来理解
*(--stack) = (unsigned long)(1<<24); // XPSR, 设置了Thumb模式,恢复到Thumb状态而非ARM状态运行
*(--stack) = (unsigned long)entry; // 程序的入口地址
*(--stack) = (unsigned long)0x14; // R14(LR), 任务不会通过return xxx结束自己,所以未用
*(--stack) = (unsigned long)0x12; // R12, 未用
*(--stack) = (unsigned long)0x3; // R3, 未用
*(--stack) = (unsigned long)0x2; // R2, 未用
*(--stack) = (unsigned long)0x1; // R1, 未用
*(--stack) = (unsigned long)param; // R0 = param, 传给任务的入口函数
*(--stack) = (unsigned long)0x11; // R11, 未用
*(--stack) = (unsigned long)0x10; // R10, 未用
*(--stack) = (unsigned long)0x9; // R9, 未用
*(--stack) = (unsigned long)0x8; // R8, 未用
*(--stack) = (unsigned long)0x7; // R7, 未用
*(--stack) = (unsigned long)0x6; // R6, 未用
*(--stack) = (unsigned long)0x5; // R5, 未用
*(--stack) = (unsigned long)0x4; // R4, 未用
task->stack = stack; // 保存最终的值
}
void tTaskSched ()
{
// 这里的算法很简单。
// 一共有两个任务。选择另一个任务,然后切换过去
if (currentTask == taskTable[0])
{
nextTask = taskTable[1];
}
else
{
nextTask = taskTable[0];
}
tTaskSwitch();
}
void delay (int count)
{
while (--count > 0);
}
int task1Flag;
void task1Entry (void * param)
{
for (;;)
{
task1Flag = 1;
delay(100);
task1Flag = 0;
delay(100);
tTaskSched();
}
}
int task2Flag;
void task2Entry (void * param)
{
for (;;)
{
task2Flag = 1;
delay(100);
task2Flag = 0;
delay(100);
tTaskSched();
}
}
// 任务1和任务2的任务结构,以及用于堆栈空间
tTask tTask1;
tTask tTask2;
tTaskStack task1Env[1024];
tTaskStack task2Env[1024];
int main ()
{
// 初始化任务1和任务2结构,传递运行的起始地址,想要给任意参数,以及运行堆栈空间
tTaskInit(&tTask1, task1Entry, (void *)0x11111111, &task1Env[1024]);
tTaskInit(&tTask2, task2Entry, (void *)0x22222222, &task2Env[1024]);
// 接着,将任务加入到任务表中
taskTable[0] = &tTask1;
taskTable[1] = &tTask2;
// 我们期望先运行tTask1, 也就是void task1Entry (void * param)
nextTask = taskTable[0];
// 切换到nextTask, 这个函数永远不会返回
tTaskRunFirst();
return 0;
}
#include "tinyOS.h"
#include "ARMCM3.h"
// 在任务切换中,主要依赖了PendSV进行切换。PendSV其中的一个很重要的作用便是用于支持RTOS的任务切换。
// 实现方法为:
// 1、首先将PendSV的中断优先配置为最低。这样只有在其它所有中断完成后,才会触发该中断;
// 实现方法为:向NVIC_SYSPRI2写NVIC_PENDSV_PRI
// 2、在需要中断切换时,设置挂起位为1,手动触发。这样,当没有其它中断发生时,将会引发PendSV中断。
// 实现方法为:向NVIC_INT_CTRL写NVIC_PENDSVSET
// 3、在PendSV中,执行任务切换操作。
#define NVIC_INT_CTRL 0xE000ED04 // 中断控制及状态寄存器
#define NVIC_PENDSVSET 0x10000000 // 触发软件中断的值
#define NVIC_SYSPRI2 0xE000ED22 // 系统优先级寄存器
#define NVIC_PENDSV_PRI 0x000000FF // 配置优先级
#define MEM32(addr) *(volatile unsigned long *)(addr)
#define MEM8(addr) *(volatile unsigned char *)(addr)
// 下面的代码中,用到了C文件嵌入ARM汇编
// 基本语法为:__asm 返回值 函数名(参数声明) {....}, 更具体的用法见Keil编译器手册,此处不再详注。
__asm void PendSV_Handler ()
{
//注意:此时的上下文使用的是MSP堆栈
IMPORT currentTask // 使用import导入C文件中声明的全局变量
IMPORT nextTask // 类似于在C文文件中使用extern int variable
// PSP保留栈的地址
MRS R0, PSP // MRS加载PSP的值到R0,获取当前任务的堆栈指针,每次在PendSV异常退出时,都会将当前任务的堆栈
//地址赋值给PSP(当前任务的堆栈在PendSV_Handler函数的倒数第三句汇编代码中有赋值给PSP)
// 即,PSP只有一个,但每次PendSV异常退出时,都会保存当前任务堆栈到PSP中。
CBZ R0, PendSVHandler_nosave // 判断PSP是否为0,如果是0表示是由tTaskRunFirst触发,直接跳转到PendSVHandler_nosave;
STMDB R0!, {R4-R11} // 由tTaskSched函数切换时,PSP肯定不会0
LDR R1, =currentTask
LDR R1, [R1]
STR R0, [R1] // 更新一下,上一个任务的栈顶,R0的值是栈顶
PendSVHandler_nosave // 无论是tTaskSwitch和tTaskSwitch触发的,最后都要从下一个要运行的任务的堆栈中恢复
// CPU寄存器,然后切换至该任务中运行
LDR R0, =currentTask // currentTask和nextTask是一个用来存放tTask地址的变量
LDR R1, =nextTask
LDR R2, [R1] // nextTask指针解引用,将值读取到R2
STR R2, [R0] // 先将currentTask设置为nextTask,也就是下一任务变成了当前任务
LDR R0, [R2] // 从currentTask中加载stack(这句话有点难理解,得仔细琢磨)
LDMIA R0!, {R4-R11} // 将任务栈中的值依次恢复到{R4至R11}。为什么只恢复了这么点,
// 因为其余的寄存器在进入PendSV时被自动保存到了MSP,退出异常时自动恢复。
MSR PSP, R0 // 最后把栈顶指针恢复到PSP
ORR LR, LR, #0x04 // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP)
BX LR // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置
}
//在启动tinyOS时,调用该函数,将切换至第一个任务运行
void tTaskRunFirst()
{
// 这里设置了一个标记,PSP = 0, 用于与tTaskSwitch()区分,用于在PEND_SV
// 中判断当前切换是tinyOS启动时切换至第1个任务,还是多任务已经跑起来后执行的切换
__set_PSP(0);
MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI; // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET; // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
// 可以看到,这个函数是没有返回
// 这是因为,一旦触发PendSV后,将会在PendSV后立即进行任务切换,切换至第1个任务运行
// 此后,tinyOS将负责管理所有任务的运行,永远不会返回到该函数运行
}
void tTaskSwitch()
{
// 和tTaskRunFirst, 这个函数会在某个任务中调用,然后触发PendSV切换至其它任务
// 之后的某个时候,将会再次切换到该任务运行,此时,开始运行该行代码, 返回到
// tTaskSwitch调用处继续往下运行
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET; // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
}
// 标准头文件,里面包含了常用的类型定义,如uint32_t
#include
// Cortex-M的堆栈单元类型:堆栈单元的大小为32位,所以使用uint32_t
typedef uint32_t tTaskStack;
// 任务结构:包含了一个任务的所有信息
typedef struct _tTask {
// 任务所用堆栈的当前堆栈指针。每个任务都有他自己的堆栈,用于在运行过程中存储临时变量等一些环境参数
// 在tinyOS运行该任务前,会从stack指向的位置处,会读取堆栈中的环境参数恢复到CPU寄存器中,然后开始运行
// 在切换至其它任务时,会将当前CPU寄存器值保存到堆栈中,等待下一次运行该任务时再恢复。
// stack保存了最后保存环境参数的地址位置,用于后续恢复
tTaskStack * stack;
}tTask;
// 当前任务:记录当前是哪个任务正在运行
extern tTask * currentTask;
// 下一个将即运行的任务:在进行任务切换前,先设置好该值,然后任务切换过程中会从中读取下一任务信息
extern tTask * nextTask;
/**********************************************************************************************************
** Function name : tTaskRunFirst
** Descriptions : 在启动tinyOS时,调用该函数,将切换至第一个任务运行
** parameters : 无
** Returned value : 无
***********************************************************************************************************/
void tTaskRunFirst (void);
/**********************************************************************************************************
** Function name : tTaskSwitch
** Descriptions : 进行一次任务切换,tinyOS会预先配置好currentTask和nextTask, 然后调用该函数,切换至
** nextTask运行
** parameters : 无
** Returned value : 无
***********************************************************************************************************/
void tTaskSwitch (void);
#endif
第一步:
系统一上电,程序先运行的时main函数,此时用的是MSP,当调用tTaskRunFirst函数(试图切换到任务1)后,进入PendSV异常处理函数,此时依次发生以下动作:
(1) currentTask = nextTask;用汇编对指针进行赋值。
(2) 把currentTask栈里面的数据(这些数据在初始化任务时已经准备好)恢复到CPU寄存器中。
(3) 把currentTask栈的栈顶地址赋值给PSP,这句代码非常重要,以后每次进入PendSV异常函数都需要从PSP中获得当前任务的栈顶,然后将CPU数据保存到栈中,这里的栈地址为什么要从PSP中获得,就是因为在每次退出异常函数时,都会将栈顶地址赋值给PSP。
(4) 设置异常返回值,表示以后都是用PSP(如果在任务内部调用了函数,这里使用的是堆栈就是任务自己的堆栈,堆栈地址已经在切换任务时赋值给了PSP)。
(5) 执行BX LR指令,真正进行切换的指令。
第二步:
从任务1切到任务2:运行任务1时,发生PendSV任务切换异常,此时进入PendSV异常处理函数,因为PSP不为0,所以会依次执行汇编指令;先把PSP中任务1的栈地址取出,并将当前CPU的寄存器的值保存到栈中。然后再执行上面第一步的动作。
代码网盘地址 提取码:qxqg