使用 C++ 将半字写入闪存时出现 ARM Cortex-M HardFault 异常

Posted

技术标签:

【中文标题】使用 C++ 将半字写入闪存时出现 ARM Cortex-M HardFault 异常【英文标题】:ARM Cortex-M HardFault exception on writting halfword to flash using C++ 【发布时间】:2020-12-22 11:01:28 【问题描述】:

我已经使用 C++ 编写了一个在 ARM Cortex-M (STM32F0) 上运行的项目,但是我在将定义的缓冲区作为类成员访问时遇到了一些问题,尽管我通过将它们定义为全局变量来解决了这个问题。

但现在我完全被这个新问题困住了,我不知道该怎么办。

我有一个代码可以解锁闪存并在其中写入内容并关闭它。如果我在 C 文件中实现它并通过 C 特性运行它(从 main.c 调用),它会完美运行。但是通过 C++ 文件(无论是在 C 还是 C++ 源文件中编写)调用它都会抛出一个 HardFault 异常。

static uint32_t waitForLastOperation(uint32_t msDelay)

  while (READ_BIT(FLASH->SR, FLASH_SR_BSY) && msDelay)
  
    LL_mDelay(1);
    msDelay--;
  

  /* Check FLASH End of Operation flag  */
  if (READ_BIT((FLASH->SR), (FLASH_SR_EOP)))
  
    /* Clear FLASH End of Operation pending bit */
    (FLASH->SR) = (FLASH_SR_EOP);
  

  if (READ_BIT((FLASH->SR),
      (FLASH_SR_WRPERR)) || READ_BIT((FLASH->SR), (FLASH_SR_PGERR)))
  
    FLASH->SR = 0U;
    return 0;
  

  /* There is no error flag set */
  return 1;


uint32_t programHalfWord(uint16_t data, uint32_t address)

  uint32_t status;

  /* Proceed to program the new data */
  SET_BIT(FLASH->CR, FLASH_CR_PG);

  /* Write data in the address */
  *(__IO uint16_t*) address = data;

  /* Wait for last operation to be completed */
  status = waitForLastOperation(FLASH_TIMEOUT);

  if (READ_BIT(FLASH->SR, FLASH_SR_EOP))
    FLASH->SR = FLASH_SR_EOP;

  /* If the program operation is completed, disable the PG Bit */
  CLEAR_BIT(FLASH->CR, FLASH_CR_PG);

  return status;


uint32_t flash_unlock()

  if (READ_BIT(FLASH->CR, FLASH_CR_LOCK) == RESET)
    return 1;

  /* Authorize the FLASH Registers access */
  WRITE_REG(FLASH->KEYR, FLASH_KEY1);
  WRITE_REG(FLASH->KEYR, FLASH_KEY2);

  /* Verify Flash is unlocked */
  if (READ_BIT(FLASH->CR, FLASH_CR_LOCK) != RESET)
    return 0;

  return 1;

这就是我使用它的方式:

if(flash_unlock())

   programHalfWord(0x11, 0x8007C00);

在执行*(__IO uint16_t*) address = data;之后立即抛出异常。

闪存在这个地址被擦除,地址对齐(它实际上是一个扇区的开始)。我检查了一切以确保闪存已解锁,但似乎用 C++ 编译的代码有些问题。

我正在使用 arm-none-eabi-gcc 和 arm-none-eabi-g++ 来编译我的代码。

提前致谢

更新:

这是与 g++ 编译器一起使用的标志列表:

-mcpu=cortex-m0 -std=gnu++14 -g3 -DSTM32F030x6 -DHSE_STARTUP_TIMEOUT=100 -DLSE_STARTUP_TIMEOUT=5000 -DDEBUG -DLSE_VALUE=32768 -DDATA_CACHE_ENABLE=0 -DINSTRUCTION_CACHE_ENABLE=0 -DVDD_VALUE=3300 -DLSI_VALUE=40000 -DHSI_VALUE=8000000 -DUSE_FULL_LL_DRIVER -DPREFETCH_ENABLE=1 -DHSE_VALUE=2000000 -c -I../app/Inc -I../Inc -I../Drivers/STM32F0xx_HAL_Driver/Inc -I../Drivers/CMSIS/Include -I../Drivers/CMSIS/Device/ST/STM32F0xx/Include -I../app/Driver -Og -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics -fno-use-cxa-atexit -Wall -fno-short-enums -fstack-usage --specs=nano.specs -mfloat-abi=soft -mthumb

这是给 gcc 的:

-mcpu=cortex-m0 -std=gnu11 -g3 -DSTM32F030x6 -DHSE_STARTUP_TIMEOUT=100 -DLSE_STARTUP_TIMEOUT=5000 -DDEBUG -DLSE_VALUE=32768 -DDATA_CACHE_ENABLE=0 -DINSTRUCTION_CACHE_ENABLE=0 -DVDD_VALUE=3300 -DLSI_VALUE=40000 -DHSI_VALUE=8000000 -DUSE_FULL_LL_DRIVER -DPREFETCH_ENABLE=1 -DHSE_VALUE=2000000 -c -I../app/Inc -I../Inc -I../Drivers/STM32F0xx_HAL_Driver/Inc -I../Drivers/CMSIS/Include -I../Drivers/CMSIS/Device/ST/STM32F0xx/Include -I../app/Driver -Og -ffunction-sections -fdata-sections -Wall -fno-short-enums -fstack-usage --specs=nano.specs -mfloat-abi=soft -mthumb

和 g++ 链接器:

-mcpu=cortex-m0 -T"./STM32F030K6TX_FLASH.ld" -Wl,-Map="$ProjName.map" -Wl,--gc-sections -static --specs=nano.specs -mfloat-abi=soft -mthumb -Wl,--start-group -lc -lm -lstdc++ -lsupc++ -Wl,--end-group

【问题讨论】:

如果相同的代码适用于 gcc 而不是 g++,这可能是由于您的编译方式不同(传递的选项?)或其他代码(RTOS 在不兼容 C++ 的项目中编译? )。你使用 STL 吗? C++ 在 STL 中具有高级机制(异常、构造函数/析构函数中的分配器等)可能不适合微控制器 您是否尝试过在程序集中调试 HardFault?通常这些类型的错误很难调试,您可以在此处阅读更多相关信息freertos.org/…,如果您按照步骤操作,您可以在解散 pc 寄存器后看到确切的行(在 c 的汇编和调用它的行中),此外,如果错误是由 impressise 错误引起的,您需要禁用所有 impresice 代码,这样您的代码运行速度会变慢,但它会帮助您找到错误。 @Clonk Checkout 更新我已经包含了标志,而且我没有在这个项目中使用 freeRTOS,并且使用的每个机制对 MCU 都是安全的,所以没有例外,alloc 和 const/destr用过。 您是否启用了 MemManage Exception?尝试启用它并查看会发生什么,也许 t 会升级为硬故障。此外,尝试使用 -O0 进行编译以消除任何优化 您使用标准外设库吗?或者像_IOREAD_BIT 这样的宏定义在哪里? 【参考方案1】:

由于在没有访问您的硬件/软件设置的情况下很难分析问题,我只能做出疯狂的猜测并提供一些提示,最近在 STM32 闪存编程方面也遇到了一些麻烦(在不同的STM32 型号(STM32F215RET6))。 - 但我根本不是这方面的专家,到目前为止我只使用供应商提供的 HAL 驱动程序来访问内部闪存。

该错误可能是由内存总线错误引起的。

使用调试器验证是否是这种情况会很有趣(例如,在错误发生后立即读取闪存状态寄存器 (FLASH_SR))。

问题是:为什么您的 C 代码在使用 gcc 编译时可以工作,而在使用 g++ 编译时为什么不行?我想,这可能与技术细节有关,即编译器“不知道”架构/内存模型的底层限制。

STM32F030K6T reference manual (RM0360) 在“3.2.2 Flash 编程和擦除操作,主 Flash 存储器编程”一节中说:

一次可对主闪存进行 16 位编程。当 CPU 在 FLASH_CR 寄存器的 PG 位置位的情况下,将半字写入主 Flash 存储器地址时,程序操作开始。任何写入非半字长数据的尝试都将导致产生硬故障中断的总线错误。

因此,对内部闪存的 32 位写访问将导致硬故障中断。

当您在启用汇编列表生成的情况下编译项目时,您可以分析 C++ 变体中究竟发生了什么,并将其与 C 变体的生成机器代码进行比较。

由于我最近也一直在处理与 STM32 闪存相关的问题,因此我查看了供应商在我的案例中提供的闪存代码 (stm32f2xx_hal_flash.c) 中发生的情况,结果发现,主要对闪存 (*(__IO uint16_t*)Address = Data;) 的写操作被转换为匹配的 ARM 半字存储指令 strh,如预期的那样:

strh r1, [r0] 

这可以通过查看 stm32f2xx_hal_flash.c 中 ST 提供的 FLASH_Program_HalfWord() 函数的自动生成的汇编列表来验证。看起来是这样的(用GCC编译,没有优化和调试信息-Og):

 662:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c **** static void FLASH_Program_HalfWord(uint32_t Address, uint16_t Data)
 663:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c **** 
 140                    .loc 1 663 1 is_stmt 1 view -0
 141                    .cfi_startproc
 142                    @ args = 0, pretend = 0, frame = 0
 143                    @ frame_needed = 0, uses_anonymous_args = 0
 144                    @ link register save eliminated.
 664:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c ****   /* Check the parameters */
 665:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c ****   assert_param(IS_FLASH_ADDRESS(Address));
 145                    .loc 1 665 3 view .LVU27
 666:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c ****   
 667:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c ****   /* If the previous operation is completed, proceed to program the new data */
 668:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c ****   CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE);
 146                    .loc 1 668 3 view .LVU28
 147 0000 074B          ldr r3, .L9
 148 0002 1A69          ldr r2, [r3, #16]
 149 0004 22F44072      bic r2, r2, #768
 150 0008 1A61          str r2, [r3, #16]
 669:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c ****   FLASH->CR |= FLASH_PSIZE_HALF_WORD;
 151                    .loc 1 669 3 view .LVU29
 152                    .loc 1 669 13 is_stmt 0 view .LVU30
 153 000a 1A69          ldr r2, [r3, #16]
 154 000c 42F48072      orr r2, r2, #256
 155 0010 1A61          str r2, [r3, #16]
 670:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c ****   FLASH->CR |= FLASH_CR_PG;
 156                    .loc 1 670 3 is_stmt 1 view .LVU31
 157                    .loc 1 670 13 is_stmt 0 view .LVU32
 158 0012 1A69          ldr r2, [r3, #16]
 159 0014 42F00102      orr r2, r2, #1
 160 0018 1A61          str r2, [r3, #16]
 671:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c **** 
 672:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c ****   *(__IO uint16_t*)Address = Data;
 161                    .loc 1 672 3 is_stmt 1 view .LVU33
 162                    .loc 1 672 28 is_stmt 0 view .LVU34
 163 001a 0180          strh    r1, [r0]    @ movhi
 673:Drivers/STM32F2xx_HAL_Driver/Src/stm32f2xx_hal_flash.c **** 
 164                    .loc 1 673 1 view .LVU35
 165 001c 7047          bx  lr
 166                .L10:
 167 001e 00BF          .align  2
 168                .L9:
 169 0020 003C0240      .word   1073888256
 170                    .cfi_endproc

生成的机器码可以用objdump反汇编和检查,没有所有的注释,像这样:

$ arm-none-eabi-objdump -d -j .text.FLASH_Program_HalfWord build/stm32f2xx_hal_flash.o

build/stm32f2xx_hal_flash.o:     file format elf32-littlearm


Disassembly of section .text.FLASH_Program_HalfWord:

00000000 <FLASH_Program_HalfWord>:
   0:   4b07        ldr r3, [pc, #28]   ; (20 <FLASH_Program_HalfWord+0x20>)
   2:   691a        ldr r2, [r3, #16]
   4:   f422 7240   bic.w   r2, r2, #768    ; 0x300
   8:   611a        str r2, [r3, #16]
   a:   691a        ldr r2, [r3, #16]
   c:   f442 7280   orr.w   r2, r2, #256    ; 0x100
  10:   611a        str r2, [r3, #16]
  12:   691a        ldr r2, [r3, #16]
  14:   f042 0201   orr.w   r2, r2, #1
  18:   611a        str r2, [r3, #16]
  1a:   8001        strh    r1, [r0, #0]
  1c:   4770        bx  lr
  1e:   bf00        nop
  20:   40023c00    .word   0x40023c00

如果你能在编译为 C++ 的目标文件中找出它的样子,那将会很有趣。是不是也用strh指令?

顺便说一句,所有 ARM 指令都记录在STM32F0xxx Cortex-M0 programming manual (PM0215) 中也是 ST:

Cortex-M0 处理器实现了 ARMv6-M 架构,该架构基于 16 位 Thumb® 指令集并包含 Thumb-2 技术。

STRHRt, [Rn, ] 将寄存器存储为半字

作为参考,当然也可以在ARM®v6-M Architecture Reference Manual 中。


旁注1:

reference manual 表示地址0x8007C00 位于闪存第 31 页的开头,在闪存扇区 7 中,假设使用的是 STM32F030K6Tx 芯片:

忘记这一点可能会导致问题,如果该扇区是通过闪存选项字节写保护的(但显然情况并非如此,因为它在 C 变体中工作正常)。只是为了完整起见(您已经对此发表了评论),引用reference manual,“4.1.3 写保护选项字节”:

这组寄存器用于对 Flash 存储器进行写保护。 清除 WRPx 字段中的一个位(同时设置一个 nWRPx 字段中的相应位)将对给定内存进行写保护 部门。对于 STM32F030x4、STM32F030x6、STM32F070x6、STM32F030x8 和 STM32F070xB 器件,从 0 到 31 的 WRP 位正在保护 4 kB 扇区的闪存。

(可能不相关,但也值得一提:当读取保护 (RDP) 级别 2 或级别 3 处于活动状态时,请注意不同的情况。RDP 是一种不同的保护机制,通过闪存选项字节与扇区保护分开,或者闪存的锁定状态。当使用 RDP 级别 2 或 3 时,从调试器或执行表单 RAM 读取闪存将导致硬故障。记录在 reference manual,“3.3.1 读取保护”部分。)


旁注2:

你可以尝试将官方的HAL C驱动代码或者你自己测试过的flash相关的C代码,和项目中新的C++部分混合起来,看看问题是否依然存在。

(混合使用 C 和 C++ 时要小心,并始终注意使用 extern "C" ... 进行命名混乱,相关帖子:https://***.com/a/1041880/5872574)


旁注3:

如前所述,我最近也遇到了一个与闪存编程无关的问题。并看到奇怪的总线错误(在硬故障后​​的状态寄存器中)。我还确保闪光灯已解锁,并且没有写保护。如果我没记错的话,我必须在我的擦除/写入操作之前添加它(但我不记得确切并且现在找不到它)。这是一个必要但奇怪的修复,因为除了常规程序执行(从闪存)之外,没有任何操作正在进行。

    while (FLASH_WaitForLastOperation(100) != HAL_OK) 
        HAL_IWDG_Refresh(&hiwdg);
    

此问题可能与 STM32 使用带有预取缓冲区/等待状态/指令缓存和数据缓存的方式有关,如reference manual(另见:FLASH_ACR 寄存器)。我没有进一步调查这个问题。只需确保在启动写/擦除访问时没有挂起/活动的闪存操作。

另外值得注意的是,编程/擦除操作将阻止对总线(闪存)的任何读取访问,但它们不会导致错误,如 reference manual 中所述,在“3.2.2 Flash 程序和擦除操作”:

正在进行的闪存操作不会阻塞 CPU,只要 CPU 不访问 Flash 存储器。

相反,在对闪存进行编程/擦除操作期间, 任何读取闪存的尝试都会停止总线。读 一旦编程/擦除操作有 完全的。这意味着代码或数据提取不能进行,而 正在进行编程/擦除操作。

对于闪存的编程和擦除操作(写/擦除), 内部 RC 振荡器 (HSI) 必须开启。


编辑:

为了检查是否真的有足够的闪存可供写入,以及该区域是否真的未被正在运行的二进制文件本身使用,这些命令可以派上用场,作为未来参考(使用我的测试二进制文件STM32F215RET 在这里):

$ arm-none-eabi-strip build/prj.elf 
$ arm-none-eabi-objdump -h build/prj.elf 

build/prj.elf:     file format elf32-littlearm

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .isr_vector   00000184  08000000  08000000  00010000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .text         000134a0  08000188  08000188  00010188  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .rodata       00002968  08013628  08013628  00023628  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .ARM          00000008  08015f90  08015f90  00025f90  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .init_array   00000004  08015f98  08015f98  00025f98  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  5 .fini_array   00000004  08015f9c  08015f9c  00025f9c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  6 .data         000002c0  20000000  08015fa0  00030000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
  7 .bss          0000149c  200002c0  08016260  000302c0  2**3
                  ALLOC
  8 ._user_heap_stack 00000604  2000175c  08016260  0003175c  2**0
                  ALLOC
  9 .ARM.attributes 00000029  00000000  00000000  000302c0  2**0
                  CONTENTS, READONLY
 10 .comment      0000001e  00000000  00000000  000302e9  2**0
                  CONTENTS, READONLY

0x08016260 用二进制标记已用闪存的结束。

这可以通过arm-none-eabi-size进行验证:

$ arm-none-eabi-size build/prj.elf 
   text    data     bss     dec     hex filename
  90004     712    6816   97532   17cfc build/prj.elf
$ echo $((90004 + 712))
90716
$ echo $((0x08016260 - 0x08000000 - (90004 + 712)))
4

因此,使用 2**3 -> 8 字节对齐和0x08000000 的闪存基地址,这意味着二进制文件实际使用了 90720 字节的闪存。

要找出哪些闪存扇区未被使用,现在可以很容易地直接在参考手册的“闪存结构”表中查找地址。

在我的例子中,链接器脚本被修改以确保只使用了一半的闪存,就像这样:

$ cat STM32F215RETx_FLASH.ld
(...)
MEMORY

RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 256K /* keep 256K free at the end */
/* FLASH (rx)      : ORIGIN = 0x8000000, LENGTH = 512K */

(...)

这样,如果二进制文件太大,您将收到链接器错误。

【讨论】:

您好@rel 感谢您为本文付出的所有时间和精力。现在我无法访问硬件,但我明天会检查它 嗨 Nixmd,不客气,很抱歉这篇文章很长,我希望那里有一些有用的东西可以帮助你了解你的情况。我已经添加了一些关于我所做的链接描述文件更改的附加信息以及一些可能很方便的检查,以供将来参考。最近我也遇到了类似的 STM32 闪存问题,但我不确定这些发现是否有助于解决您的具体问题。 - 在 C++ 的情况下,您可能需要更改一些编译器/链接器标志......我对此没有任何建议,因为到目前为止我从未在 STM32 上使用过 C++。 这里是尝试写入用 C++ 编写的闪存的反汇编指令 => 131 *(__IO uint16_t*) LINE_NUMBER_FLASH_ADDRESS = lineNumber; 08002098: uxth r2, r0 0800209a: ldr r3, [pc, #68] ; (0x80020e0 &lt;setLineNumber(unsigned long)+76&gt;) 0800209c: strh r2, [r3, #0] 它正在使用strh 指令,但仍会产生硬故障,使其成为misterius:/ 在这种情况下,如果所有其他提到的错误源都已检查,您可以尝试进一步隔离问题,从项目的工作 C 变体开始。也许您可以以小的增量步骤添加新部分,直到出现这个奇怪的问题......使用调试器单步执行程序以查看发生了什么(状态寄存器?回溯?)会很有趣。您可以在通常已经生成的名为 HardFault_Handler() 的 ISR 中捕获硬故障中断,该 ISR 在 Src/stm32f0xx_it.c 中定义(如果使用了 STM32CubeMX)并将其用于调试。【参考方案2】:

我有同样的问题。我的错误是无效对齐。 我确信 90% 的此类错误是对齐问题,尤其是在 Cortex-M0 上。 我已经在 C++ 类中创建了proper alignment 的结构,这就足够了。 注意:函数在 RAM 中执行,如写半页,should better be insideextern "C" 表达式。

【讨论】:

以上是关于使用 C++ 将半字写入闪存时出现 ARM Cortex-M HardFault 异常的主要内容,如果未能解决你的问题,请参考以下文章

在 C++ 中使用向量时出现分段错误

C++写入INI时的一个错误

全局写入时出现分段错误

裸机嵌入式 C++:将闪存写入 ram 时未定义对 memcpy 和 memset 的引用

高效交换半字

为什么在使用具有大_Size的memset时出现访问写入冲突?