文章目录

  • 1 问题描述
    • 1. 1 环境
    • 1. 2 问题
  • 2 参考资料
  • 3 来龙去脉
    • 3.1 定位问题
    • 3.2 xPortPendSVHandler
    • 3.3 EXC_RETURN
    • 3.4 寄存器
    • 3.5 探索真像
      • 3.5.1 浮点任务切换到空闲任务
      • 3.5.2 空闲任务切换到浮点任务
  • 4 解决办法
  • 5 总结

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

  • Gitee
  • Git

1 问题描述

1. 1 环境

类别 版本
系统 WIN10
Keil Keil MDK 5.15.0
开发板 星空派GD32F303开发板
GD32F30x 固件库 V2.1.2
GD32F30x Keil 5 支持包 V2.1.0
FreeRTOS V10.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 定位问题

在线调试参考文档:

  • 手把手教你查找stm32 HardFault_Handler调试及问题方法
  • 【STM32】Fault 类异常_记一次STM32中HardFault问题的调试解决

启动在线调试,全速运行,当进入 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, =pxCurrentTCBldr   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 = 1it 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_PRIORITYmsr basepri, r0/* 数据同步隔离。* 仅当所有在它前面的存储器访问操作都执行完毕后,才执行在它后面的指令 */dsbisb/* 执行任务上下文切换 */bl vTaskSwitchContext/* 使能已屏蔽的中断 */mov r0, #0msr basepri, r0/* 恢复任务控制块指向的栈顶 */ldmia sp!, {r3}/* 获取当前栈顶 */ldr r1, [r3]ldr r0, [r1]/* 出栈*/ldmia r0!, {r4-r11, r14}/* FPU 相关寄存器出栈*/tst r14, #0x10it eqvldmiaeq r0!, {s16-s31}/* 更新 PSP 指针 */msr psp, r0isb/* 跳转到 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 值
xPSR Program Status Register 程序状态寄存器
S0 - S15 单精度浮点通用寄存器 (“S” for single precision)
FPSCR Floating point status and control register 浮点状态和控制寄存器
FPCAR Floating 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_Handler vPortSVCHandler
    PendSV_Handler xPortPendSVHandler
    SysTick_Handler xPortSysTickHandler

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

5 总结

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

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

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

  1. 基于keil环境下mm32f327单片机rtthread的移植

    基于keil环境下mm32f327单片机rtthread的移植 文章目录 基于keil环境下mm32f327单片机rtthread的移植 前言 一.所需资源 二.创建工程目录 三.复制所需文件到相应文 ...

  2. Keil环境下完成一个基于STM32汇编程序的编写

    本文内容:\color{red}{本文内容:}本文内容: 1)记录build生成的 hex文件各段的大小,了解Hex文件格式及其前8个字节内容含义: 2)学习在没有硬件条件下进行仿真调试的方法,观察A ...

  3. [转载]在中文Windows环境下,控制台窗口中也可以用特殊符号拼出漂亮的表格来。...

    原文地址:在中文Windows环境下,控制台窗口中也可以用特殊符号拼出漂亮的表格来. 比如:             ┌─┬─┐     │  │作者:wxl1990721 在中文Windows环境下 ...

  4. Keil环境下用STM32汇编语言工程分析HEX文件内容

    一.创建工程 1创建工程,点击Project,选择第一项创建新工程.并保存文件的地址和文件名. 2 配置环境 ①选择STM32F103RC芯片,点击OK. 然后在CMSIS下选择CORE:Device ...

  5. 本地化环境下ArcGIS Python API中的SSL及locale的bug修复过程

    本地化环境下ArcGIS Python API中的SSL及locale的bug修复过程 进来试用ArcGIS Pro和对应的ArcGIS Python API,遇见各种问题--新产品还是不成熟啊,特别 ...

  6. eclipse spring mysql,eclipse环境下的springboot框架+mybatis访问MySQL报错空指针

    "/")public classTestController { @RequestMapping("/login")publicString login() { ...

  7. 软件开发Linux环境下,java通过JNA调用so报错,求大神解答,感激不尽。

    软件开发Linux环境下,java通过JNA调用so报错,求大神解答,感激不尽. 图片说明 最佳答案: 专家已采纳 先用c等调用一下so,看函数能否正确调用 文章来源:https://ask.csdn ...

  8. Java中List.remove报UnsupportedOperationException异常

    Java中List.remove报UnsupportedOperationException异常 参考文章: (1)Java中List.remove报UnsupportedOperationExcep ...

  9. 【PCLint】 Keil环境下使用方式

    不说PCLint的安装 PCLint的配置 Keil环境搭建 检查方法 不说PCLint的安装 网上一大堆,自己找 PCLint的配置 找到PClint的安装路径,打开"CONFIG.exe ...

最新文章

  1. 优化思路千万种,基于下界函数的最优化效率如何?
  2. 为什么ConcurrentHashMap的读操作不需要加锁?
  3. 聚焦五大领域:浙江大学发布《重大领域交叉前沿方向2021》报告
  4. JS 常用函数二(改变HTML样式)
  5. RedisUtil - Redis功能介绍,五种数据类型的使用,Spring和Redis的集成
  6. Blocs 4 for Mac(可视化网页设计工具)
  7. 即学即用的30个python常用代码
  8. access 2016 迁移到 mysql_将ACCESS数据库迁移到SQLSERVER数据库两种方法(图文详解)
  9. 周纪三 周慎靓王元年(辛丑,公元前320年)——摘要
  10. python输出斐波那契数列_Python实现斐波那契数列
  11. 提高多表关联数据查询效率
  12. qt清空qtablew_qt项目:员工信息管理系统
  13. cadence如何导入gds_Tanner LEdit系列 | 导入GDSII文件
  14. 如何关闭windows杀毒软件
  15. 刘知远:NLP研究入门之道(三)如何通过文献掌握学术动态
  16. ibase4J分布式架构使用eclipse部署
  17. SRAM、PSRAM、SPI FLASH杂记【转】
  18. mui扩展图标-购物车图标无法正常显示问题
  19. 2021年CFA二三级机考题目大变!
  20. SpreadJS 纯前端表格控件应用案例:物业行业全面预算管理系统

热门文章

  1. Netflix 停止 APP 内付费绕开苹果税;百度开除虚假报销打车票员工 55 人
  2. 脑电特征中的微分熵(DE)计算
  3. Day33.爬虫基础之PyQuery
  4. python pyquery节点内所有文本_PyQuery详解
  5. 实施智能制造需要做好哪些准备?
  6. Python纳音五行计算
  7. 使用JavaScript来实现图片的切换
  8. 【总结】CSP2022总结
  9. MOS管进阶部分,那些你不了解的MOS管知识
  10. c linux root 后挂载读写,Linux-挂载-mount