自己动手写RTOS:02-在M3内核上实现pendsvc

自己动手写RTOS

自己动手写RTOS:01基础知识和理论部分
自己动手写RTOS:02-在M3内核上实现pendsvc



一、M3内核的相关知识

节选自M3权威指南

1.1寄存器

在这里插入图片描述
**R0-R12:**通用寄存器R0‐R12 都是 32 位通用寄存器,用于数据操作。但是注意:绝大多数 16 位 Thumb 指令只能访问 R0‐R7,而 32 位 Thumb‐2 指令可以访问所有寄存器。其中R4-R11,使用前必须前保存,用完后再恢复
Banked R13: 两个堆栈指针Cortex‐M3 拥有两个堆栈指针,然而它们是 banked,因此任一时刻只能使用其中的一个。
主堆栈指针( MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程)
进程堆栈指针( PSP):由用户的应用程序代码使用。堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。在 ARM 编程领域中,凡是打断程序顺序执行的事件,都被称为异常(exception)。除了外部中断外,当有指令执行了“非法操作”,或者访问被禁的内存区间,因各种错误产生的 fault,以及不可屏蔽中断发生时,都会打断程序的执行,这些情况统称为异常。在不严格的上下文中,异常与中断也可以混用。另外,程序代码也可以主动请求进入异常状态的(常用于系统调用)。
**R14:**连接寄存器当呼叫一个子程序时,由 R14 存储返回地址
**R15:**程序计数寄存器指向当前的程序地址。如果修改它的值,就能改变程序的执行流(很多高级技巧就在这里面——译注)。

1.2特殊寄存器

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

1.3堆栈

在这里插入图片描述
**

注意非常重要:Cortex‐M3 使用的是“向下生长的满栈”模型。堆栈指针 SP 指向最后一个被压入堆栈的 32位数值。在下一次压栈时, SP 先自减 4,再存入新的数值。POP 操作刚好相反:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4。

**

二、pendSVC实现

1.汇编语句

1.1存储器访问:
1)LDR指令

LDR格式:LDR{条件} 目的寄存器 <存储器地址>

LDR作用:将存储器地址所指地址处连续的4个字节(1个字)的数据传送到目的寄存器

比如想把数据从内存中某处读取到寄存器中,只能使用ldr

比如:
ldr r1, =0x40000000 ;将地址0x4000_0000赋值给r1
ldr r0, [#r1] ;将地址0x4000_0000处的数据赋值给

2)STR和LDRB指令

STR格式:STR{条件} 源寄存器,<存储器地址>

STR作用:STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。该指令在程序设计中比较常用,寻址方式灵活多样,使用方式可参考指令LDR。

LDRB:字节数据加载指令


3)MOV指令

MOV格式:mov source, destination

MOV作用:source 和 destination 的值可以是内存地址,存储在内存中的数据值,指令语句中定义的数据值,或者寄存器。

实例代码

main.c
#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)

void triggerPendSVC (void) 
{
    MEM8(NVIC_SYSPRI2) = NVIC_PENDSV_PRI;   // 向NVIC_SYSPRI2写NVIC_PENDSV_PRI,设置其为最低优先级
    MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET;    // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV
}

/**********************************************************************************************************
** Function name        :   PendSV_Handler
** Descriptions         :   PendSV异常处理函数。很有些会奇怪,看不到这个函数有在哪里调用。实际上,只要保持函数头不变
**                          void PendSV_Handler (), 在PendSV发生时,该函数会被自动调用
** parameters           :   无
** Returned value       :   无
***********************************************************************************************************/
typedef struct _BlockType_t 
{
    unsigned long * stackPtr;
}BlockType_t;

BlockType_t * blockPtr;

void delay (int count) 
{
    while (--count > 0);
}

int flag;

unsigned long stackBuffer[32];
BlockType_t block;

int main () 
{
    block.stackPtr = &stackBuffer[32];
    blockPtr = &block;
    for (;;) {
        flag = 0;
        delay(100);
        flag = 1;
        delay(100);
        
        triggerPendSVC();
    }
    
    return 0;
}

switch.c

__asm void PendSV_Handler ()
{
    IMPORT  blockPtr
    
    // 加载寄存器存储地址
    LDR     R0, =blockPtr
    LDR     R0, [R0]
    LDR     R0, [R0]

    // 保存寄存器
    STMDB   R0!, {R4-R11}
    
    // 将最后的地址写入到blockPtr中
    LDR     R1, =blockPtr
    LDR     R1, [R1]
    STR     R0, [R1]
    
    // 修改部分寄存器,用于测试
    ADD R4, R4, #1
    ADD R5, R5, #1
    
    // 恢复寄存器
    LDMIA   R0!, {R4-R11}
    
    // 异常返回
    BX      LR
}  

在这里插入图片描述
在这里插入图片描述
执行到main.c 65行时跳转到triggerPendSVC函数,然后执行语句MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET,就自动跳转到__asm void PendSV_Handler ()里;
依次执行后
LDR R0, =blockPtr
LDR R0, [R0]
LDR R0, [R0]
R0的值分别为0x2000_0000,0x2000_0008,0x2000_0090,即最后为数组stackBuffer[32]的地址,可能很多人问,为什么要用stackBuffer[32]的地址而不是stackBuffer[31]的,这和接下来的语句有关
STMDB R0!, {R4-R11} ;这条语句的意思是,先R0减去4的地址,也就是stackBuffer[31]的地址,然后依次将R11-R4存进这个数组,最后R0保存执行完的地址0x2000_0070
最后执行15-17后,栈变量的指针就指向栈顶地址也就是R1=0x2000_0070

2.实现任务A和B的切换

main.c

  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;
    }

tTaskInit 函数的实现

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 task1Entry (void * param) 
{
    for (;;) 
    {
        task1Flag = 1;
        delay(100);
        task1Flag = 0;
        delay(100);
        tTaskSched();
    }
}
//任务执行完后的调用切换函数
void tTaskSched () 
{    
    // 这里的算法很简单。
    // 一共有两个任务。选择另一个任务,然后切换过去
    if (currentTask == taskTable[0]) 
    {
        nextTask = taskTable[1];
    }
    else 
    {
        nextTask = taskTable[0];
    }
    
    tTaskSwitch();
}

不管是tTaskSwitch()还是tTaskRunFirst(),主要就是执行如下语句
MEM32(NVIC_INT_CTRL) = NVIC_PENDSVSET; // 向NVIC_INT_CTRL写NVIC_PENDSVSET,用于PendSV

接下来,就是最重要的PendSV处理语句了,在这里我们实现当前任务的现场保存,并恢复下一任务的现场,执行下一任务

__asm void PendSV_Handler ()
{   
    IMPORT  currentTask               // 使用import导入C文件中声明的全局变量
    IMPORT  nextTask                  // 类似于在C文文件中使用extern int variable
    
    MRS     R0, PSP                   // 获取当前任务的堆栈指针
    CBZ     R0, PendSVHandler_nosave  // if 这是由tTaskSwitch触发的(此时,PSP肯定不会是0了,0的话必定是tTaskRunFirst)触发
                                      // 不清楚的话,可以先看tTaskRunFirst和tTaskSwitch的实现
    STMDB   R0!, {R4-R11}             //     那么,我们需要将除异常自动保存的寄存器这外的其它寄存器自动保存起来{R4, R11}
                                      //     保存的地址是当前任务的PSP堆栈中,这样就完整的保存了必要的CPU寄存器,便于下次恢复
    LDR     R1, =currentTask          //     保存好后,将最后的堆栈顶位置,保存到currentTask->stack处    
    LDR     R1, [R1]                  //     由于stack处在结构体stack处的开始位置处,显然currentTask和stack在内存中的起始
    STR     R0, [R1]                  //     地址是一样的,这么做不会有任何问题

PendSVHandler_nosave                  // 无论是tTaskSwitch和tTaskSwitch触发的,最后都要从下一个要运行的任务的堆栈中恢复
                                      // CPU寄存器,然后切换至该任务中运行
    LDR     R0, =currentTask          // 好了,准备切换了
    LDR     R1, =nextTask             
    LDR     R2, [R1]  
    STR     R2, [R0]                  // 先将currentTask设置为nextTask,也就是下一任务变成了当前任务
 
    LDR     R0, [R2]                  // 然后,从currentTask中加载stack,这样好知道从哪个位置取出CPU寄存器恢复运行
    LDMIA   R0!, {R4-R11}             // 恢复{R4, R11}。为什么只恢复了这么点,因为其余在退出PendSV时,硬件自动恢复

    MSR     PSP, R0                   // 最后,恢复真正的堆栈指针到PSP  
    ORR     LR, LR, #0x04             // 标记下返回标记,指明在退出LR时,切换到PSP堆栈中(PendSV使用的是MSP) 
    BX      LR                        // 最后返回,此时任务就会从堆栈中取出LR值,恢复到上次运行的位置
}  

参考连接

1、https://blog.csdn.net/qq_36300069/article/details/123850429

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小薛1988

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值