FreeRTOS 学习笔记(3)—— 中断配置和临界段

7 篇文章 0 订阅
7 篇文章 0 订阅

一、Cortex-M 中断

1. 中断介绍

Cortex-M3 和 M4 都有最支持 240 个 IRQ(中断请求),1 个不可屏蔽中断(NMI),1 个 SysTick(滴答定时器)定时器中断和多个系统异常。

Cortex-M 处理器有 3 个固定优先级和 256 个可编程的优先级,最多 128 个抢占优先级,实际由芯片厂商决定优先级数量(如 STM32 采用 4 位来控制,共 16 级)。

注:8 位宽优先级配置寄存器为什么最多 128 个抢占优先级?因为分为了抢占优先级和亚优先级。

而抢占优先级支持中断嵌套,即高优先级可以打断低优先级;亚优先级不支持中断嵌套,即如果抢占优先级相同,亚优先级高的中断无法打断正在运行的亚优先级低的中断。不过如果抢占优先级相同,亚优先级不同,而且中断同时到来,优先运行亚优先级高的中断。

Hard Fault、NMI、RESET 优先级为负数,高于普通中断优先级,且不可以配置。

FreeRTOS 没有处理亚优先级,所以配置 STM32 的优先级为组 4,全部为抢占优先级。

2. 优先级配置

每个外部中断优先级寄存器,分别都是 8 位,所以最大宽度是 8 位,而最小为 3 位。

4 个相邻的优先级寄存器组成 32 位寄存器。FreeRTOS 在设置 PendSV 和 SysTick 中断优先级的时候都是直接操作 0xE000_ED20 这个 32 位寄存器。该寄存器地址从低到高分别为调试监视器的优先级(0xE000_ED20)、-(0xE000_ED21)、PendSV 的优先级(0xE000_ED22)、SysTick 的优先级(0xE000_ED23)。

3. 重要寄存器

(1)PRIMASK 寄存器

用于禁止 NMI 和 HardFault 之外的所有异常和中断。汇编编程可以用 CPS(修改处理器状态)指令来修改 PRIMASK 寄存器的数值:

CPSIE  I;  // 使能中断(清除 PRIMASK)

CPSID  I;  // 禁止中断(设置 PRIMASK)

也可以通过 MRS 和 MSR 指令访问:

MOVS  R0,  #1;

MSR     PRIMASK, R0;  // 将 1 写入 PRIMASK 禁止所有中断

MOVS  R0,  #0;

MSR     PRIMASK, R0;  // 将 0 写入 PRIMASK 使能中断

(2)FAULTMASk 寄存器

除了 NMI 之外的所有异常和中断都屏蔽掉。汇编编程的时候可以利用 CPS 指令修改 FAULTMASK 的当前状态:

CPSIE  F;  // 使能中断(清除 FAULTMASk)

CPSID  F;  // 禁止中断(设置 FAULTMASk)

也可以用 MSR 和 MRS 指令访问,参考 PRIMASK。

(3)BASEPRI 寄存器

用于设置是否屏蔽中断的优先级阈值。所有优先级数值(优先级数值越大,优先级越低)大于等于该值的中断均被关闭。例如希望屏蔽优先级低于 0x60(即优先级数值大于等于 0x60)的中断,可以将该寄存器设置为 0x60。但是,如果写 0,则会停止屏蔽中断而不是屏蔽大于等于 0 的所有中断。

注:FreeRTOS 的开关中断通过操作 BASEPRI 寄存器实现的。关闭优先级小于该阈值的中断,打开大于该阈值的中断。

二、FreeRTOS 中断配置宏

在 FreeRTOSconfig.h 中设置。

1. configPRIO_BITS

用来设置 MCU 使用几位优先级,STM32 采用的是 4bits,所以是 4!

2. configLIBRARY_LOWEST_INTERRUPT_PRIORITY

用来设置最低优先级。STM32 采用 4bits,且 STM32 配置的使用组 4,也就是 4 位都是抢占优先级。因此优先级数就是 16 个,优先级从 0 到 15,最低优先级那就是 15。所以对于 STM32 该值设置为 15。

3. configKERNEL_INTERRUPT_PRIORITY

配置内核中断优先级。宏定义如下:

#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

代码表示:内核中断优先级配置为宏 configLIBRARY_LOWEST_INTERRUPT_PRIORITY(最低优先级)左移(8 - MCU采用优先级位数),即移到高四位。为什么这么设置呢?因为 STM32 采用高四位作为优先级设置位。

configKERNEL_INTERRUPT_PRIORITY 用来设置 PendSV 和滴答时钟的中断优先级,定义如下:

#define portNVIC_PENDSV_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )

可以看出,portNVIC_PENDSV_PRI 和 portNVIC_SYSTICK_PRI 都是使用了宏 configKERNEL_INTERRUPT_PRIORITY,为什么宏 portNVIC_PENDSV_PRI 是宏 configKERNEL_INTERRUPT_PRIORITY 左移 16 位呢?宏 portNVIC_SYSTICK_PRI 也同样是左移 24 位。PendSV 和 SysTcik 的中断优先级设置是操作 0xE000_ED20 地址的,这样一次写入的是个 32 位的数据, SysTick 和 PendSV 的优先级寄存器分别对应这个 32 位数据的最高 8 位和次高 8 位,对应的就是一个左移 16 位,一个左移 24 位。

在函数 xPortStartScheduler() 中定义了这两者的优先级,在文件 port.c 中,如下:

// portNVIC_SYSPRI2_REG寄存器包含了0xE000_ED20之后32位的寄存器
// 其中从低到高分别是调试监视器(0xE000_ED20)、--(0xE000_ED21)、PendSV(0xE000_ED22)和SysTick(0xE000_ED23)
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

由此可见,给 PendSV 和 SysTick 的优先级配置了最低优先级 15。

4. configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY

用来设置 FreeRTOS 系统可管理的最大中断优先级(不是任务优先级)。低于该阈值的优先级归 FreeRTOS 管理;高于该阈值的优先级不归 FreeRTOS 管理。将该值给 BASEPRI 寄存器赋值。FreeRTOS 的开关中断通过操作 BASEPRI 寄存器实现的。关闭优先级小于该阈值的中断,打开大于该阈值的中断。

5. configMAX_SYSCALL_INTERRUPT_PRIORITY

该宏由 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 左移 4 位得到,道理同 configKERNEL_INTERRUPT_PRIORITY。

#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

该宏设置完毕后,低于该优先级的中断可以安全的调用 FreeRTOS 的 API 函数,高于此优先级的中断 FreeRTOS 无法禁止,中断服务函数也不能调用 FreeRTOS 的 API 函数!

例如 STM32,有 16 个优先级,0 为最高优先级,15 为最低优先级,配置如下:

  • configMAX_SYSCALL_INTERRUPT_PRIORITY==5
  • configKERNEL_INTERRUPT_PRIORITY==15

结果如下图所示:

注:FreeRTOS 内核源码中有多处开关全局中断的地方,这些开关全局中断会加大中断延迟时间。比如在源码的某个地方关闭了全局中断,但是此时有外部中断触发,这个中断的服务程序就需要等到再次开启全局中断后才可以得到执行。开关中断之间的时间越长,中断延迟时间就越大,这样极其影响系统的实时性。如果这是一个紧急的中断事件,得不到及时执行的话,后果是可想而知的。

针对这种情况,FreeRTOS 就专门做了一种新的开关中断实现机制。关闭中断时仅关闭受 FreeRTOS 管理的中断,不受 FreeRTOS 管理的中断不关闭,这些不受管理的中断都是高优先级的中断,用户可以在这些中断里面加入需要实时响应的程序。

三、FreeRTOS 开关中断

FreeRTOS 开关中断函数为 portENABLE_INTERRUPTS() 和 portDISABLE_INTERRUPTS() ,这两个函数其实是宏定义,在 portmacro.h 中有定义,如下:

#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS()  vPortSetBASEPRI(0)

可以看出开关中断实际上是通过函数 vPortSetBASEPRI(0) 和 vPortRaiseBASEPRI() 来实现的,这两个函数如下:

static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
    __asm
    {
        msr basepri, ulBASEPRI
    }
}
/*-----------------------------------------------------------*/
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
    uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
    __asm
    {
        msr basepri, ulNewBASEPRI
        dsb
        isb
    }
}

函数 vPortSetBASEPRI() 是向寄存器 BASEPRI 写入一个值,此值作为参数 ulBASEPRI 传递进来,portENABLE_INTERRUPTS() 是开中断,它传递了个 0 给 vPortSetBASEPRI() ,根据我们前面讲解 BASEPRI 寄存器可知,结果就是开中断。

函数 vPortRaiseBASEPRI() 是向寄存器 BASEPRI 写入宏 configMAX_SYSCALL_INTERRUPT_PRIORITY,那么优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断就会被屏蔽!

四、临界段代码

临界段代码即临界区,指必须完整运行,而不能被打断的代码,比如有的外设初始化需要严格的时序,初始化过程不能被打断。FreeRTOS 进入临界区代码时,关闭中断,处理完临界区代码以后再打开中断。

FreeRTOS 与临界段代码保护有关的函数有 4 个:

  • taskENTER_CRITICAL()
  • taskEXIT_CRITICAL()
  • taskENTER_CRITICAL_FROM_ISR()
  • taskEXIT_CRITICAL_FROM_ISR()

前两个是任务级的临界段代码保护,后两个是中断级的临界段代码保护。中断级的临界区代码保护用在中断函数中,并且该中断的优先级必须比 configMAX_SYSCALL_INTERRUPT_PRIORITY 数值大(即优先级小,也就是在调用之前会把该中断给关掉)。

1. 任务级临界段代码保护

taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 是任务级的临界代码保护,一个是进入临界段,一个是退出临界段,这两个函数是成对使用的,这函数的定义如下:

#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL()  portEXIT_CRITICAL()

而 portENTER_CRITICAL() 和 portEXIT_CRITICAL() 也是宏定义,在文件 portmacro.h 中有定义,如下:

#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL()  vPortExitCritical()

函数 vPortEnterCritical() 和 vPortExitCritical() 在文件 port.c 中,函数如下:

void vPortEnterCritical( void )
{
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;
    if( uxCriticalNesting == 1 )
    {
        configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
    }
}
void vPortExitCritical( void )
{
    configASSERT( uxCriticalNesting );
    uxCriticalNesting--;
    if( uxCriticalNesting == 0 )
    {
        portENABLE_INTERRUPTS();
    }
}

可以看出在进入函数 vPortEnterCritical() 以后会首先关闭中断,然后给变量 uxCriticalNesting 加一,uxCriticalNesting 是个全局变量,用来记录临界段嵌套次数的。函数 vPortExitCritical() 是退出临界段调用的,函数每次将 uxCriticalNesting 减一,只有当 uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS() 使能中断。这样保证了在有多个临界段代码的时候不会因为某一个临界段代码的退出而打乱其他临界段的保护,只有所有的临界段代码都退出以后才会使能中断!

任务级临界代码保护使用方法如下:

void taskcritical_test(void)
{
    while(1)
    {
        taskENTER_CRITICAL();  // 进入临界区
        total_num+=0.01f;
        printf("total_num 的值为: %.4f\r\n",total_num);
        taskEXIT_CRITICAL();   // 退出临界区
        vTaskDelay(1000);
    }
}

注意!临界区代码一定要精简!因为进入临界区会关闭中断,这样会导致优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及时的响应!

2. 中断级临界段代码保护

函数 taskENTER_CRITICAL_FROM_ISR() 和 taskEXIT_CRITICAL_FROM_ISR() 中断级别临界段代码保护,是用在中断服务程序中的,而且这个中断的优先级一定要低于 configMAX_SYSCALL_INTERRUPT_PRIORITY!这两个函数在文件 task.h 中有如下定义:

#define taskENTER_CRITICAL_FROM_ISR()   portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

portSET_INTERRUPT_MASK_FROM_ISR() 和 portCLEAR_INTERRUPT_MASK_FROM_ISR() ,这两个在文件 portmacro.h 中有如下定义:

#define portSET_INTERRUPT_MASK_FROM_ISR()    ulPortRaiseBASEPRI()
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)

vPortSetBASEPRI() 就是给 BASEPRI 寄存器中写入一个值。函数 ulPortRaiseBASEPRI() 在文件 portmacro.h 中定义的,如下:

static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
    uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
    __asm
    {
        mrs ulReturn, basepri      // 先读出BASEPRI的值,保存在ulReturn中
        msr basepri, ulNewBASEPRI  // 将configMAX_SYSCALL_INTERRUPT_PRIORITY写入到寄存器BASEPRI中
        dsb
        isb
    }
    return ulReturn;  // 返回ulReturn,退出临界区代码保护的时候要使用到此值!
}

中断级临界代码保护使用方法如下:

// 定时器3中断服务函数
void TIM3_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) // 溢出中断
    {
        status_value=taskENTER_CRITICAL_FROM_ISR();  // 进入临界区
        total_num+=1;
        printf("float_num 的值为: %d\r\n",total_num);
        taskEXIT_CRITICAL_FROM_ISR(status_value);  // 退出临界区
    }
    TIM_ClearITPendingBit(TIM3,TIM_IT_Update); // 清除中断标志位
}

为什么中断级和任务级不同?

区别:中断级进入前保存当前的 BASEPRI 值,并给其赋值 configMAX_SYSCALL_INTERRUPT_PRIORITY,实现关闭比configMAX_SYSCALL_INTERRUPT_PRIORITY 优先级低的中断。退出的时候需要给 BASEPRI 赋值进入临界区代码前的值。也就是中断级无法嵌套。

任务级进入前关闭中断,而不保存当前 BASEPRI 的初始值,每次嵌套都要保存嵌套了几次。当全部嵌套退出后,给 BASEPRI 赋值 0,打开中断。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值