OS 特性之 SVC 异常

在嵌入式 OS 的设计与实现中, SVC(请求管理调用)和 PendSV(可挂起的系统调用)异常非常重要,今天我们先来看一下 SVC 异常。根据之前的文章,我们已经知道 SVC 的异常编号为 11,其优先级也是可编程的。

SVC 异常由 SVC 指令触发,尽管可以利用 NVIC 寄存器的写入操作来触发一个中断(如软件触发寄存器 NVIC->STIR),但其实际行为可能会略有不同:即中断是不准确的。这意味着在设置挂起状态后到中断实际产生前可能会执行多条指令。换句话说,SVC 是精准的。SVC 处理必须在 SVC 指令后执行,除非同时有另一个更高优先级的中断出现。

在许多系统中,SVC 机制可以作为一个 API ,用于实现让应用任务能够访问系统资源,如下图所示:

图片

对于需要高可靠性的系统,应用任务可以运行在非特权访问等级,同时一些硬件资源可被设置为只支持特权访问(利用 MPU),此时应用任务只能通过 OS 提供的服务来访问这些受保护的资源。按照这种方式,由于应用任务无法获得关键硬件的访问权限,嵌入式系统就更加健壮和安全。

在一些情况下这也会使得应用任务的编程更简单,因为有了 OS 提供的服务后,应用任务就无需了解依赖于硬件的编程细节。

由于应用任务无需了解 OS 服务函数的确切地址,因此有了 SVC,这使得应用任务可以独立于 OS 来进行开发。应用任务只需知道 SVC 的服务编号以及 OS 服务所需的参数,实际的硬件级编程则由设备驱动来处理,正如上图所示的那样。

SVC 异常由 SVC 指令产生,该指令需要一个立即数,这也是参数传递的一种方式。SVC 异常处理可以提取出参数并确定它所要执行的动作。如:

SVC #0x3 ; 调用 SVC 服务 3

ARM 工具链中的传统 SVC 语法也是可以使用的(没有“#”):

SVC 0x3 ; 调用 SVC 服务 3

对于 ARM 工具链的 C 语言编程(KEIL MDK 和 ARM Development Studio 5), SVC 可由 _svc 函数生成。对于 gcc 和其他的一些工具链,它可以由内联汇编产生。

在执行 SVC 处理时,可以在读取压栈的程序计数器(PC)数值后从该地址读出指令并屏蔽掉不需要的位,以确定 SVC 指令中的立即数。不过,执行 SVC 的程序可以使用主栈也可以使用进程栈。因此,在提取压栈的 PC 数值前,需要确定压栈过程中使用的是哪个栈,此时可以查看进入异常处理时链接寄存器的数值(这里涉及到 EXC_RETURN 的相关知识点),如下图所示:

图片

对于汇编编程,可以确定实际使用的栈,并利用下面的代码提取出 SVC 服务编号:

SVC_Handler
TST LR, #4 ; 测试 EXC_RETURN 的第 2 位
ITE EQ
MRSEQ R0, MSP ; 若为 0, 压栈使用 MSP, 复制到 R0
MRSNE R0, PSP ; 若为 1, 压栈使用 PSP, 复制到 R0
LDR R0, [R0, #24] ; 从栈帧中得到压栈的 PC
                  ; (压栈的 PC = SVC 后指令的地址)
LDRB R0, [R0, #-2] ; 读取 SVC 指令的第一个字节
                   ; 现在 SVC 编号位于 R0
...

对于 C 编程环境,需要将 SVC 处理分为两个部分:

  • 第一部分提取栈帧的起始地址,并将其作为输入参数传递给第二部分。该处理要用汇编实现,这是因为需要检查 LR 的数值(EXC_RETURN),而无法用 C 实现。

  • 第二部分从栈帧中提取压栈的 PC 数值,然后从程序代码中得到 SVC 编号。它还可以提取出压栈的寄存器数值等其他信息。

假设正在使用 KEIL MDK-ARM 工具链,则可以按下方代码创建第一部分的处理:

__asm void SVC_Handler(void)
{
    TST LR, #4 ; 测试 EXC_RETURN 的第 2 位
    ITE EQ
    MRSEQ R0, MSP ; 若为 0, 压栈使用 MSP, 复制到 R0
    MRSNE R0, PSP ; 若为 1, 压栈使用 MSP, 复制到 R0
    B __cpp(SVC_Handler_C)
    ALIGN 4
}

SVC_Handler 为 CMSIS-Core 中的标准函数名,在得到栈帧的起始地址后,它会传递给 SVC 处理中的 C 程序部分 “SVC_Handler_C”。

void SVC_Handler_C(unsigned int * svc_args)
{
    uint8_t svc_number;
    uint32_t stacked_r0, stacked_r1, stacked_r2, stacked_r3;
    svc_number = ((char *) svc_args[6])[-2]; //内存[(压栈的 PC)-2]
    stacked_r0 = svc_args[0];
    stacked_r1 = svc_args[1];
    stacked_r2 = svc_args[2];
    stacked_r3 = svc_args[3];
    // ... 其他处理
    ...
    // 返回结果 (如前两个参数相加)
    svc_args[0] = stacked_r0 + stacked_r1;
    return;
}

传递栈帧地址的好处在于,C 处理函数可以提取栈帧中的任何信息,包括压入栈寄存器值。若要将参数传递给 SVC 服务并获得 SVC 服务的返回值,那么这一点是非常重要的。由于异常处理实际可以为普通的 C 函数,若调用了 SVC 服务,且同时产生了更高优先级的中断,则更高优先级的 ISR 会先执行,但这样就会修改 R0~R3 以及 R12 等数值。为了确保 SVC 处理能够得到正确的参数,就需要从栈帧中获取到参数值。

若 SVC 服务需要返回一个数值,则需要利用栈帧进行数值的返回。否则,存储在寄存器组中的返回值会在异常返回过程中被出栈操作覆盖。传递参数且返回数值的一个 SVC 服务示例代码如下:

SVC #0x3 ; 调用 SVC 服务 3

SVC 0x3 ; 调用 SVC 服务 3

SVC_Handler
TST LR, #4 ; 测试 EXC_RETURN 的第 2 位
ITE EQ
MRSEQ R0, MSP ; 若为 0, 压栈使用 MSP, 复制到 R0
MRSNE R0, PSP ; 若为 1, 压栈使用 PSP, 复制到 R0
LDR R0, [R0, #24] ; 从栈帧中得到压栈的 PC
                  ; (压栈的 PC = SVC 后的指令)
LDRB R0, [R0, #-2] ; 读取 SVC 指令的第一个字节
                   ; SVC 编号目前位于 R0
...


__asm void SVC_Handler(void)
{
    TST LR, #4 ; 测试 EXC_RETURN 的第 2 位
    ITE EQ
    MRSEQ R0, MSP ; 若为 0, 压栈使用 MSP, 复制到 R0
    MRSNE R0, PSP ; 若为 1, 压栈使用 MSP, 复制到 R0
    B __cpp(SVC_Handler_C)
    ALIGN 4
}

void SVC_Handler_C(unsigned int * svc_args)
{
    uint8_t svc_number;
    uint32_t stacked_r0, stacked_r1, stacked_r2, stacked_r3;
    svc_number = ((char *) svc_args[6])[-2]; //内存[(压栈的 PC)-2]
    stacked_r0 = svc_args[0];
    stacked_r1 = svc_args[1];
    stacked_r2 = svc_args[2];
    stacked_r3 = svc_args[3];
    // ... 其他处理
    ...
    // 返回结果 (如前两个参数相加)
    svc_args[0] = stacked_r0 + stacked_r1;
    return;
}

/--------------------------------------------------------------------/

/* SVC 服务示例, 具有参数传递和返回值 - 基于 Keil MDK-ARM */
#include <stdio.h>
// 定义 SVC 函数
int __svc(0x00) svc_service_add(int x, int y); // 服务 #0 : 加法
int __svc(0x01) svc_service_sub(int x, int y); // 服务 #1 : 减法
int __svc(0x02) svc_service_incr(int x); // 服务 #2 : 自增
void SVC_Handler_main(unsigned int * svc_args);

// 函数声明
int main(void)
{
    int x, y, z;

    x = 3; y = 5;
    z = svc_service_add(x, y);
    printf ("3+5 = %d \n", z);

    x = 9; y = 2;
    z = svc_service_sub(x, y);
    printf ("9-2 = %d \n", z);

    x = 3;
    z = svc_service_incr(x);
    printf ("3++ = %d \n", z);

    while(1);
}

// SVC 处理 - 提取栈帧起始地址的汇编包装器

__asm void SVC_Handler(void)
{
    TST LR, #4 ; 检查 EXC_RETURN 的第 2 位
    ITE EQ
    MRSEQ R0, MSP ; 如果为 0, 压栈使用 MSP, 复制到 R0
    MRSNE R0, PSP ; 如果为 1, 压栈使用 PSP, 复制到 R0
    B
    __cpp(SVC_Handler_C)
    ALIGN 4
}

// SVC handler - 处理主逻辑
// 输入参数为从汇编包装器中得到的栈帧起始地址

void SVC_Handler_main(unsigned int * svc_args)
{
    // 栈帧中包含:
    // r0, r1, r2, r3, r12, r14, 返回地址以及 xPSR
    // - Stacked R0 = svc_args[0]
    // - Stacked R1 = svc_args[1]
    // - Stacked R2 = svc_args[2]
    // - Stacked R3 = svc_args[3]
    // - Stacked R12 = svc_args[4]
    // - Stacked LR = svc_args[5]
    // - Stacked PC = svc_args[6]
    // - Stacked xPSR= svc_args[7]

    unsigned int svc_number;
    svc_number = ((char *)svc_args[6])[-2];
    switch(svc_number)
    {
        case 0: svc_args[0] = svc_args[0] + svc_args[1];
        break;
        case 1: svc_args[0] = svc_args[0] - svc_args[1];
        break;
        case 2: svc_args[0] = svc_args[0] + 1;
        break;
        default: // 未知的 SVC 请求
        break;
    }
    return;
}

由于异常优先级模型的限制,无法在 SVC 处理内部使用 SVC (因为优先级和当前优先级相同)。非要这么做的话就会导致使用错误异常,并且由于同样的原因,也无法在 NMI 处理或 HardFault 处理中使用 SVC 。

若使用 ARM7TDMI 等传统的 ARM 处理器,则你有可能知道这些处理器中存在一个名为 SWI 的软件中断指令。SVC 的功能与其类似,实际上 SVC 的二进制编码和 SWI 指令一样。在较新的架构中,SWI 已经变成了 SVC。不过由于 ARM7TDMI 和 Cortex-M 的异常模型间存在许多差异,因此两者的 SVC 处理代码也是不同的。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

WKJay_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值