没志青年
发布于 2025-09-16 / 27 阅读
0

FreeRTOS 信号量原理

手动触发上下文切换

像信号量等阻塞的,当满足条件不阻塞了,要调用这个,让高优先级的任务抢占。

#if (configUSE_PREEMPTION == 0)

/* 如果使用协作式调度(cooperative scheduling),
 * 那么就算有更高优先级的任务被唤醒,也不应该立即触发一次调度(yield)。
 */
#define queueYIELD_IF_USING_PREEMPTION()

#else

/* 如果使用抢占式调度(preemptive scheduling),
 * 一旦有更高优先级任务就绪,就要触发一次上下文切换。
 */
#define queueYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()

#endif

#ifndef portYIELD_WITHIN_API
#define portYIELD_WITHIN_API portYIELD
#endif

#define portYIELD()                         \
    {                                                     \
        /* 触发PendSV中断进行上下文切换 */     \
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;   \
        __dsb(portSY_FULL_READ_WRITE); /* 数据同步 */ \
        __isb(portSY_FULL_READ_WRITE); /* 指令同步 */ \
    }

Cortex-M 内核寄存器

Cortex-M 内核一共 16 个通用寄存器(R0–R15)

  • R0–R3:函数调用参数 / 临时变量

  • R4–R11:被调用者保存寄存器(callee-saved),任务切换时必须手动保存/恢复

  • R12:临时

  • R13 (SP):堆栈指针,两种情况共用该寄存器:

    • MSP (Main Stack Pointer) → 内核、异常模式用

    • PSP (Process Stack Pointer) → 用户任务用(RTOS 把任务栈挂在 PSP 上)

  • R14 (LR):链接寄存器(返回地址)

  • R15 (PC):程序计数器

  • xPSR:程序状态寄存器(标志位、执行状态)

重要的特殊寄存器:

  • BASEPRI:优先级屏蔽寄存器 → 限制某个阈值以下的中断进入(FreeRTOS 用它做“关中断”)。

  • CONTROL:决定当前用 MSP 还是 PSP。

汇编语言

mrs

ldr

dsb

isb

str

stmdb

bl

mov

ldmia

bx

任务切换原理

__asm void xPortPendSVHandler(void)
{
    extern uxCriticalNesting;  // 临界区嵌套深度
    extern pxCurrentTCB;       // 当前任务
    extern vTaskSwitchContext; // 选择下一个任务函数

    PRESERVE8  // ARM 8字节对齐,符合 ABI 规范

    //=========================== 保存当前任务 ==========================
    mrs r0, psp 				 // 将当前任务的堆栈指针读入r0
    isb      					// 指令同步
    ldr r3,= pxCurrentTCB 		// TCB地址赋值给r3
    ldr r2, [r3]            	// 根据TCB地址拿到任务堆栈指针
	stmdb r0!, {r4-r11}     	// 将寄存器r4-r11的值压入栈中
	str r0, [r2]				// 将新的堆栈指针保存到当前任务TCB中,这也是TCB中第一个成员必须为pxTopOfStack堆栈指针的原因,放其它地方无法实现
	stmdb sp!, {r3, r14}    	 // 将r3中的TCB地址和r14中的返回地址压入主堆栈
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0				// 关中断,和 vPortRaiseBASEPRI 函数实现原理一样
	dsb							// 数据同步
	isb							// 指令同步

	//=========================== 加载新任务 ==========================
	bl vTaskSwitchContext		// 调用vTaskSwitchContext函数,这个函数用于获取下一个要运行的任务并更新pxCurrentTCB
	mov r0, #0  				
	msr basepri, r0				// r0清零并开中断,和 vPortSetBASEPRI(0) 函数实现原理一样
	ldmia sp!, {r3, r14}       	// 从主堆栈中弹出当前TCB地址到r3和返回地址到r14中
	ldr r1, [r3]                // TCB地址赋值给r1
	ldr r0, [r1]				// 根据TCB地址拿到任务堆栈指针
	ldmia r0!, {r4-r11}			// 恢复寄存器r4-r11的值
	msr psp, r0					// 将新的栈顶赋值给任务栈指针
	isb                 		// 指令同步
	bx r14                      // 跳转到这个任务的返回地址,继续执行
	nop                         // 空指令,延时一个指令周期
}

切换任务时不能被其它中断打断,但是pendSV优先级不是最高的吗?

  • 系统异常(SVCall、PendSV、SysTick)都可以配置优先级。

  • FreeRTOS 把 PendSV 设置为最低优先级(也就是说,只有在没有别的中断在跑时,它才会跑)。

👉 所以 PendSV 并不是“最高优先级”,而是“最低优先级”。这样才能保证:

  • 先处理硬件中断(UART、SPI、定时器…)。

  • 等都处理完了,再切换任务,不会打断关键的中断处理。

为什么切换时还要关中断(BASEPRI)?

问题的核心是:

  • PendSV 自己不会被抢占(因为它最低),但在 PendSV 执行过程中,别的高优先级中断依然可能进来

  • 如果此时中断里调用了 RTOS API(比如信号量、任务切换相关),就会操作 pxCurrentTCB、任务链表等关键数据结构。

  • 那么这些数据结构可能在 保存旧任务 / 恢复新任务 之间被破坏,导致上下文恢复错误,系统崩溃。

SysTick 中断处理