GD32F30x Keil 环境下在 FreeRTOS 任务中使用浮点运算报 HardFault 异常的问题(二)

示例工程代码库地址如下:

1 问题描述

1. 1 环境

类别版本
系统WIN10
KeilKeil MDK 5.15.0
开发板星空派GD32F303开发板
GD32F30x 固件库V2.1.2
GD32F30x Keil 5 支持包V2.1.0
FreeRTOSV10.4.3-LTS-Patch-2

1. 2 问题

书接上回《FreeRTOS 任务中使用浮点运算报 HardFault 异常的问题(一)》
上回只描述了问题的表象,并巧合的使用编译优化暂时解决了问题;
本回则彻底深入探究其背后的来龙去脉。

2 参考资料

  • FreeRTOS - STM32F2 hard fault

  • FreeRTOS - Float and double cause hardfault handler on STM32F417
    幸运的在官网社区中检索到以上文章,其讨论的问题跟我极其相似,
    内心顿时燃起希望之火,文章问答中大意是说:

    /* xPortPendSVHandler 不能按照如下方式调用,
     * 当发生 PendSV 中断时需要直接调用 xPortPendSVHandler 
     * 经过验证确实如此,但是内心的疑惑并没有解除
    */
    void PendSV_Handler(void)
    {
        xPortPendSVHandler();
    }
    

    但至于为什么不能按照以上方式调用,具体解说只有如下一段话:

    When you use floating point instructions a bit gets set in one of the control registers, which in turn changes the stack frame when you enter interrupts. I would have to study the code output by the compiler to see exactly why you were getting the symptoms you were, but it is likely you actually have a problem in all cases but were only noticing when the stack frame was different and/or when floating point registers corruptions were causing faults.

    大意就是:当使用浮点指令时,控制寄存器中的一个位会被设置,进入中断时会根据该位改变堆栈帧,我必须去研究编译器输出的代码,以确切的了解为什么会出现该问题。

  • 使用Cortex-M4的FPU需要知道这个知识点!
    浅显易懂的介绍了 FPU 相关知识点,主要了解 lazy stacking 特性

  • The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors
    Contex-M4 全英文官方权威指南

3 来龙去脉

建议往下看之前,先了解 lazy stacking 特性。
接下来将始终围绕如下代码调用导致 HardFault 来进行解说:

void PendSV_Handler(void)
{
    xPortPendSVHandler();
}

整个解说过程按照倒叙,即从问题反推原因。

3.1 定位问题

在线调试参考文档:

启动在线调试,全速运行,当进入 HardFault 断点后,查看异常原因
在这里插入图片描述
发现 HardFault 异常是上访造成的(FORCED 位),而真实的错误原因是由 IACCVIOL 引起的,查看官方权威指南,其描述如下:

  1. Violation to memory access protection, which is defined by MPU setup. For example, user application (unprivileged) trying to access privileged-only region. Stacked PC might able to locate the code that has caused the problem.
  2. Branch to non-executable regions, which could be
    caused by
    – simple coding error.
    – use of incorrect instruction for exception return
    – corruption of stack, which can affect stacked LR which is used for normal function returns, or corruption of stack frame which contains return address.
    – Invalid entry in exception vector table. For example, loading of an executable image for traditional ARM processor core into the memory, or exception happen
    before vector table in SRAM is set up.

大意如下:

  1. 违反了 MPU 定义的内存访问保护。例如,用户应用程序(非特权)试图访问特权区域。可通过 PC 堆栈找到问题的代码。
  2. 分支到了不可执行区域,这可能由以下原因引起
    - 简单的编码错误
    - 不正确的异常返回值 EXC_RETURN
    - 栈的损坏,它会影响用于正常函数返回的堆栈 LR,或者包含返回地址的栈帧的损坏
    - 无效的异常向量表条目,例如,将传统 ARM 处理器内核的可执行映像加载到内存中,或者在 SRAM 中的向量表建立之前发生异常。

查看 Call Stack 窗口,如下图:
在这里插入图片描述
发现是在空闲任务函数中触发了 HardFault,
根据断点一步一步调试直到 HardFault 异常,程序运行大致如下:

系统初始化 -> SVC 中断启动第一个任务 -> 运行浮点任务(优先级高于空闲任务) 
-> PendSV 中断切换任务 -> 运行空闲任务 -> PendSV 中断切换任务 
-> 切换不成功进入 HardFault 异常

一旦浮点任务中不使用 FPU,整个系统运行正常,
而空闲任务是 FreeRTOS 定义的任务,没有任何浮点运算。

猜想如下:

  • 可能从浮点任务切换到非浮点任务正常,从非浮点任务切换到浮点任务则不正常;
  • 可能仅第一次任务切换正常,第二次即以后的任务切换都不正常。

根据以上种种迹象表明,初步可以断定问题出在任务切换处理过程中

3.2 xPortPendSVHandler

任务切换处理函数 xPortPendSVHandler 解读如下:

__asm void xPortPendSVHandler( void ){
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;
	
	/* 指定堆栈八字节对齐 */
	PRESERVE8
	
	/* 读取 psp 的值到 r0 */
	mrs r0, psp
	/* 指令同步隔离。
	 * 清洗流水线,保证它前面的指令都执行完毕之后才执行它后面的指令
    */
	isb
	/* 获取当前任务控制块,即获取任务栈顶 */
	ldr	r3, =pxCurrentTCB
	ldr	r2, [r3]
	/* FPU 处理,检测 r14(LR) 即 EXC_RETURN 的 Bit[4] 是否为 0,
	 * 若为 0 表明线程使用了 FPU,当对 S16 - S31 进行处理后,
	 * 内核会自动对 S0 - S15 和 FPSCR 寄存器值进行入栈操作
	 * (需要了解 lazy stacking 特性,才能理解此处过程)
	 */
	tst r14, #0x10 //按位与运算,运算结果为 1,Z = 0,运算结果为 0,Z = 1
	it eq //如果 Z == 1,则执行下面语句
	vstmdbeq r0!, {s16-s31} // 把 s16-s31 入栈
	/* 保存内核余下未自动保存的寄存器 */
	stmdb r0!, {r4-r11, r14}
	/* 保存当前任务栈顶,把栈顶指针入栈 */
	str r0, [r2]
	stmdb sp!, {r3}
	/* 屏蔽 FreeRTOS 能控制的所有中断 */
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	/* 数据同步隔离。
	 * 仅当所有在它前面的存储器访问操作都执行完毕后,才执行在它后面的指令 
	*/
	dsb
	isb
	/* 执行任务上下文切换 */
	bl vTaskSwitchContext
	/* 使能已屏蔽的中断 */
	mov r0, #0
	msr basepri, r0
	/* 恢复任务控制块指向的栈顶 */
	ldmia sp!, {r3}
	/* 获取当前栈顶 */
	ldr r1, [r3]
	ldr r0, [r1]
	/* 出栈*/
	ldmia r0!, {r4-r11, r14}
	/* FPU 相关寄存器出栈*/
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}
	/* 更新 PSP 指针 */
	msr psp, r0
	isb
	/* 跳转到 r14 地址处 */
	bx r14
}

整个处理逻辑如下:
当前任务的寄存器值入栈 -> 切换任务上下文 -> 新任务的寄存器值出栈 -> 跳转到新任务

3.3 EXC_RETURN

当处理器进入异常处理程序或中断服务程序 (ISR) 时,链接寄存器 (LR) 的值更新为 EXC_RETURN 的值。当使用 BX、POP 或内存加载指令(LDR 或 LDM)将该值加载到程序计数器(PC)时,该值用于触发异常返回机制。具体位说明描述如下:
在这里插入图片描述
在这里插入图片描述

3.4 寄存器

R0 - R3、R12、LR 和 PSR 被称为调用者保存寄存器,即进入异常中断前这些寄存器已自动入栈保存

官方权威指南有如下描述:

In order to allow a C function to be used as an exception handler, the exception
mechanism needs to save R0 to R3, R12, LR, and PSR at exception entrance automatically, and restore them at exception exit under the control of the processor’s hardware.In this way when returned to the interrupted program, all the registers would have the same value as when the interrupt entry sequence started. In addition, since the value of the return address (PC) is not stored in LR as in normal C function calls (the exception mechanism puts an EXC_RETURN code in LR at exception entry, which is used in exception return), the value of the return address also needs to be saved by the exception sequence. So in total eight registers need to be saved during the exception handling sequence on the Cortex-M3 or Cortex-M4 processors without a floating point unit.

谷歌翻译如下:

为了允许将 C 函数用作异常处理程序,异常机制需要在异常入口处自动保存R0到R3、R12、LR、PSR,并在处理器硬件的控制下在异常出口处恢复它们。这样当返回到被中断程序时,所有的寄存器都一样 值作为中断进入序列开始时的值。 此外,由于返回地址 (PC) 的值不像普通 C 函数调用那样存储在 LR 中(异常机制在异常入口处将 EXC_RETURN 代码放在 LR 中,用于异常返回),所以 返回地址也需要通过异常序列保存。 因此,在没有浮点单元的 Cortex-M3 或 Cortex-M4 处理器上,在异常处理序列期间总共需要保存八个寄存器。

没有使用 FPU 时的自动入栈处理如下图:
在这里插入图片描述
使用了 FPU 时自动入栈处理如下图:
在这里插入图片描述
寄存器说明如下:

寄存器说明
R0 - R3、R12通用寄存器
LR(R14)Link Register 链接寄存器,用于在调用函数或子例程时保存返回地址,另外在异常处理过程中,保存 EXC_RETURN 异常返回值
Return Address记录函数返回地址,即程序计数器 PC 值
xPSRProgram Status Register 程序状态寄存器
S0 - S15单精度浮点通用寄存器 (“S” for single precision)
FPSCRFloating point status and control register 浮点状态和控制寄存器
FPCARFloating point context address register 浮点内容地址寄存器

3.5 探索真像

3.5.1 浮点任务切换到空闲任务

首先在浮点任务运行时发生 PendSV 中断,进入 PendSV_Handler,寄存器值如下:
在这里插入图片描述
R14(LR) = 0xFFFFFFED,Bit4 = 0,表明使用了 FPU,符合预期。
进入 xPortPendSVHandler,执行到 tst r14, #0x10 语句,寄存器值如下:
在这里插入图片描述
R14(LR) = 0x08000767,恰好 Bit4 = 0,表明使用了 FPU 指令,执行 vstmdbeq r0!, {s16-s31} 语句,入栈 FPU 相关寄存器值,继而触发内核对 S0 - S15、FPSCR 寄存器的入栈,紧接着把 R14(LR) = 0x8000767 的值入栈保存,最终把浮点任务的所有寄存器入栈。

0x08000767 是 xPortPendSVHandler 函数的返回地址(0x08000766 + 1,Bit0 为 1 表明是 thumb 状态,16位指令),如下图:
在这里插入图片描述
紧接着从空闲任务的堆栈中恢复相关寄存器值,此时 R14(LR) = 0xFFFFFFFD,最后执行 bx r14 语句(xPortPendSVHandler 根本不会返回到 PendSV_Handler),触发 PendSV 异常返回机制,任务切换成功,开始执行空闲任务。

至于首次运行空闲任务时 R14(LR) = 0xFFFFFFFD 的设置代码如下:

/* Constants required to set up the initial stack. */
#define portINITIAL_XPSR                      ( 0x01000000 )
#define portINITIAL_EXC_RETURN                ( 0xfffffffd )

/*
 * See header file for description.
 */
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
                                     TaskFunction_t pxCode,
                                     void * pvParameters )
{
    /* Simulate the stack frame as it would be created by a context switch
     * interrupt. */

    /* Offset added to account for the way the MCU uses the stack on entry/exit
     * of interrupts, and to ensure alignment. */
    pxTopOfStack--;

    *pxTopOfStack = portINITIAL_XPSR;                                    /* xPSR */
    pxTopOfStack--;
    *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
    pxTopOfStack--;
    *pxTopOfStack = ( StackType_t ) prvTaskExitError;                    /* LR */

    /* Save code space by skipping register initialisation. */
    pxTopOfStack -= 5;                            /* R12, R3, R2 and R1. */
    *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */

    /* A save method is being used that requires each task to maintain its
     * own exec return value. */
    pxTopOfStack--;
    *pxTopOfStack = portINITIAL_EXC_RETURN;

    pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */

    return pxTopOfStack;
}

以上函数被调用过程如下:
vTaskStartScheduler -> xTaskCreate( prvIdleTask)-> prvInitialiseNewTask -> pxPortInitialiseStack

3.5.2 空闲任务切换到浮点任务

待从空闲任务运行过程中进入 PendSV 中断,如下图:
在这里插入图片描述
R14(LR) = 0xFFFFFFFD,Bit4 = 1,空闲任务没有使用 FPU,符合预期。
进入 xPortPendSVHandler,执行到 tst r14, #0x10 语句,寄存器值如下:
在这里插入图片描述
R14(LR) = 0x08000767,Bit4 = 0,执行 vstmdbeq r0!, {s16-s31} 语句,入栈 FPU 相关寄存器值(但此处的操作是由于 R14(LR) 此时已不是 EXC_RETURN,导致误判入栈了 FPU 寄存器值,其 tst r14, #0x10 语句判断时的值应该是进入 xPortPendSVHandler 函数前的 0xFFFFFFFD,该值表明没有使用 FPU,而不是该函数的 Return Address),紧接着把 R14(LR) = 0x8000767 的值入栈保存,最终把空闲任务的所有寄存器入栈。

紧接着从浮点任务的堆栈中恢复相关寄存器值,R14(LR) = 0x8000767,最后执行 bx r14 语句,从结果看是从 xPortPendSVHandler 函数返回到了 PendSV_Handler 函数,如下图:
在这里插入图片描述
当执行 POP {r4,pc} 语句时,由于是从空闲任务进入的 PendSV 中断,那么 PUSH {r4,lr} 语句中的 lr = 0xFFFFFFFD,Bit4 = 1,表明没有使用 FPU,而实际上切换到浮点任务时,希望 Bit4 = 0,即 lr = 0xFFFFFFED,所以即使 xPortPendSVHandler 函数内部结尾处恰好因为其 Return Address 地址 lr = 0x08000767,Bit4 = 0 对 S16 - S31 进行了出栈处理,但最终 PendSV 异常返回时却因为 Bit4 = 1,而不会对 S0 - S15、FPSCR 寄存器进行出栈,导致 FPU 相关寄存器值恢复不完整,才会出现本节开头描述的使用了浮点运算就会进入 HardFault 异常的问题。

因此,在从空闲任务切换到浮点任务过程中,如下 PendSV 中断处理实现:

void PendSV_Handler(void)
{
    xPortPendSVHandler();
}

将导致寄存器相关操作出现如下问题:

  • 空闲任务没有使用 FPU 却入栈了 S16 - S31,也错把 xPortPendSVHandler 函数 Return Address 当成 EXC_RETURN 进行了入栈保存;
  • 浮点任务巧合的出栈了 S16 - S31,但在退出 PendSV 异常中断时,EXC_RETURN 的值是从空闲任务进入 PendSV 中断的 0xFFFFFFFD,而实际应该是 0xFFFFFFED,导致 S0 - S15、FPSCR 没有出栈,即浮点相关寄存器出栈不完全。

4 解决办法

以下方法,都是保证发生 PendSV 中断时,直接调用 xPortPendSVHandler。
操作时取其中一种方法即可。

  • 《FreeRTOS 任务中使用浮点运算报 HardFault 异常的问题(一)》
    按照以上博文中的处理方法启用编译优化,即不使用编译优化 Level 0 等级,
    启用编译优化生成的最终代码丢弃了 PendSV_Handler 函数,发生中断直接调用 xPortPendSVHandler 函数,反汇编也能看出端倪。

    使用编译优化 Level 0 等级反汇编代码如下:
    在这里插入图片描述
    使用编译优化 Level 1 - 3 等级反汇编代码如下:
    在这里插入图片描述
    但该方法有局限性,当 PendSV_Handler 函数内部除了调用 xPortPendSVHandler 函数之外还有其它代码,会因编译无法优化而失效。

  • FreeRTOSConfig.h 文件中增加如下宏定义

    #define vPortSVCHandler 		SVC_Handler
    #define xPortPendSVHandler 		PendSV_Handler
    #define xPortSysTickHandler 	SysTick_Handler
    

    并删除 gd32f30x_it.c 文件中调用 FreeRTOS 内核 vPortSVCHandler、xPortPendSVHandler、xPortSysTickHandler 的代码,以及屏蔽函数 SVC_Handler、PendSV_Handler、SysTick_Handler 的定义。

  • startup_gd32f30x_hd.s 启动文件中进行如下替换

    原函数名新函数名
    SVC_HandlervPortSVCHandler
    PendSV_HandlerxPortPendSVHandler
    SysTick_HandlerxPortSysTickHandler

    并删除 gd32f30x_it.c 文件中调用 FreeRTOS 内核 vPortSVCHandler、xPortPendSVHandler、xPortSysTickHandler 的代码。

5 总结

总算拨云见日,理清了来龙去脉,
一路过来,面对问题刨根问底的心态让我坚持了下来,
而坚持就会有收获,不说别的至少解决了心中疑惑。

一切没有解决的问题都是大问题,一切解决了的问题都是小问题。

  • 37
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值