完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
1.问题缘由
产品的嵌入式软件框架一般是bootloader + app + 上位机的形式。这里的app不是手机app,而是产品的业务逻辑代码,也是最核心的代码。 bootoader和app是打包在一起,作为整个产品的固件烧入到产品中。bootloader主要负责对芯片加密(防止产品软件被破解),对app进行校验以及对app进行升级。上位机就是电脑端的软件,主要用于对产品的参数进行标定(所谓的标定是指下位机参数的清除或设置,不同的参数组合会导致系统的运行效果不一样),并且可以结合bootloader对app进行升级,也就是iap升级;故障诊断以及下线测试。 这个框架有两个地方涉及到对flash的读写。 (1)用bootloader+上位机对app进行升级时,也就是做iap升级时,需要写flash;在系统启动时,bootloader需要对app进行crc校验,此时需要读flash。 (2)用上位机对下位机进行标定后,在本次系统关机时(为什么在关机时才写flash,而不是在系统正常运行过程中写?因为我们没有外接其他存储介质,所以我们的代码也是在flash(irom:nor flash)中运行的,flash在写的时候,cpu不能同时从flash里面读取指令,如果是在实时性要求非常高的领域,譬如电机控制,由于写flash的这十几毫秒会导致电机失步,所以在关机时统一写一次flash(不是每次关机都会写,只有参数修改了才会写)),需要写flash;在系统启动并跳转到app时,需要读取flash保存的参数,用于初始化各种硬件,此时需要读flash。 原有的下位机用的是stm32f103为平台进行软件开发。该芯片一次性只能写16bit的数据,并且往flash写完16bit的数据之后,不计算和保存ecc。所以在读取flash时,存在数据出错的风险,但好在风险不大,对于绝大多数产品不会有太大影响。但如果是通勤类产品,譬如电动车,就需要非常好的安全性。 后来出于成本考虑,公司将stm32f103平台换成了stm32g071平台,后者可以节省2到3元。如果每年出货1000万台,就可以节省2000到3000万物料成本。并且g071平台的flash读写会做ecc校验,比较可靠,所以后面就主推g071平台。 2.Ecc校验是什么意思? stm32g071平台一次可以往flash写64bit的数据(最少是64bit,其他的没有测试过),写64bit后,flash模块会自己根据这64bit的数据计算出一个8bit的ecc校验码,然后再把校验码也写到flash里面。校验码写到flash哪里?原来该芯片的flash的每一个单元不是64bit,而是72bit,前64bit是真实的数据,后8bit就是用来存ecc的,只不过这8bit的地址对于我们来说是不可见的。 读flash时,会先根据要读地址空间的64bit数据计算出一个ecc校验码,然后将其和写数据时保存的ecc校验码进行比对,如果是一样的,就可以正常读取;如果不一样,如果是只有1bit的数据出错,ecc模块会自动校正;否则,会产生不可屏蔽中断,即NMI_Handler函数会被调用。 3.存在的风险 下位机下面的两种情况会导致这个风险:flash来不急将ECC校验码写到flash中,导致下次读取该地址时,ECC校验失败,系统跳到NMI不可屏蔽中断中,使产品成为砖头。 (1)因为会通过上位机更新app,如果在更新的过程中断电,很有可能会导致flash模块来不急计算ecc或来不急保存ecc,下次启动时,Bootloader在校验app crc时,crc模块需要读取flash中app的数据进行计算crc值,如果读取flash中如果出现ecc校验失败,也会跳转到NMI_Handler中。因为每次启动都会读flash,并且每次都会触发NMI异常。如果不处理这种情况,此时产品就成砖头了。 (2)在初始化各个设备之前,会先从flash中读取通过上位机写入的系统参数,如果在通过上位机写参数的过程中,系统断电了,很有可能会导致flash模块来不急计算ecc或来不急保存ecc。再次上电后,在下位机读取该参数时,ecc校验失败,系统就会直接跳转到NMI_Handler函数中。因为每次启动都会读取参数用于初始化硬件,并且每次都会触发NMI异常,如果不处理这种情况,此时产品就成砖头了。 4.如何验证上述理论 根据以上理论,需要设计一种框架来验证以上情况是否会发生,需要做以下工作: 首先要明白我们要干什么?人为的制造一个flash ecc校验错误,看每次重启系统,执行到读取flash时会不会都跳转到NIM_Handler。 思路: 写flash大概需要十几毫秒,并且这个动作是flash模块自己完成的,期间不要cpu,也就是写flash时,我们只需要将数据送到指定的flash寄存器(因为flash和内存统一编址,这里其实也是使用C语言的指针操作直接往该地址写数据的,这里说flash寄存器,只是为了方便表述),flash就开始将数据真正写死到flash的各个存储单元中。 但cpu核执行一条指令大概是十几纳秒,所以cpu需要等flash写完,因为在写flash过程中,cpu不能从flash读取数据的(虽然irom flash是直接和内存统一编码的,但某一个时刻,只能是单向的);这对于cpu来说,是非常漫长的过程。所以可以将写flash的这一个函数搬到内存(iram)中,一执行完写68bit的指令,就立即重启系统,这很有可能会导致flash来不急写ecc而被直接重启。下次启动时,直接读取刚刚写地址处的数据,看看会不会跳转到NIM_Handler函数中。 5.代码流程 主要涉及到以下知识点: (1)怎么把flash的代码搬到iram中。 (2)怎么写flash会产生ecc错误。 6.具体步骤 (1)在STM32cubeIDE中新建一个带hal库的空工程。 (2)在链接文件中自定义一个段。 _iram_function_addr = LOADADDR(.iram_function); .iram_function : { . = ALIGN(4); _iram_function_start = .; *(.iram_function) . = ALIGN(4); _iram_function_end = .; } >RAM AT> FLASH (3)在main.c中添加以下代码。 添加头文件#include “stm32g0xx.h”。在函数前面必须加入段名,这样编译器才会将该函数链接到指定的段中。 ① 添加写flash的函数。 __attribute__(( section( ".iram_function" ) )) void vWriteFlash( void ) { uint64_t data = 0x1234567812345678; HAL_FLASH_Unlock(); /* 将一个64bit的数据0x1234567812345678写到0x8014008地址 */ SET_BIT(FLASH->CR, FLASH_CR_PG); *(uint32_t *)0x8014008 = (uint32_t)data; __ISB(); *(uint32_t *)(0x8014008 + 4U) = (uint32_t)(data >> 32U); /* 让cpu空转几百个纳秒,确保数据写进去了 */ for (int i = 0; i < 50; i++) __NOP(); /* 重启系统:flash模块来不急计算0x1234567812345678的ECC校验码就被重启了, * 所以0x8014008地址里面的数据肯定是有问题的 */ SCB->AIRCR = ((0x5FAUL << SCB_AIRCR_VECTKEY_Pos) | SCB_AIRCR_SYSRESETREQ_Msk); HAL_FLASH_Lock(); } ② 添加main函数 int main(void) { uint32_t data = 0; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); /* 每次执行一次新的测试时,都需要把这个函数打开 */ // FlashBsp_vPageErase( 0x8014000 ); if ((*(uint32_t*)(0x8014000)) == 0xFFFFFFFF) { FlashBsp_vPageErase( 0x8014000 ); HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, 0x8014000, 0x5a5a5a5a); HAL_FLASH_Lock(); /*操作寄存器,往0x8014008地址出64bit数据*/ vWriteFlash(); } else { data = *(uint32_t*)(0x8014008); data = data; } while (1); } ③ 擦除flash指定页的函数。 void FlashBsp_vPageErase(uint32_t addr) { /* 根据地址算出在哪一页 */ uint8_t number = 0; FLASH_EraseInitTypeDef erase; uint32_t pageError; number = (addr - 0x8000000) / 2048; if (((addr - 0x8000000) % 2048) != 0) { number = number + 1; } if (number >= 63) return; erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.Page = number; erase.NbPages = 1;/*2KB*/ HAL_FLASH_Unlock(); FLASH->CR = FLASH->CR & ~(1); HAL_FLASHEx_Erase(&erase, &pageError); HAL_FLASH_Lock(); } (4)在汇编文件startup_stm32g071cbtx.s中添加“将irom代码复制到iram”的代码。 .global g_pfnVectors .global Default_Handler /* start address for the initialization values of the .data section. defined in linker script */ /* 下面三行是我加的 */ .word _iram_function_addr .word _iram_function_start .word _iram_function_end .word _sidata /* start address for the .data section. defined in linker script */ .word _sdata /* end address for the .data section. defined in linker script */ .word _edata /* start address for the .bss section. defined in linker script */ .word _***ss /* end address for the .bss section. defined in linker script */ .word _ebss .section .text.Reset_Handler .weak Reset_Handler .type Reset_Handler, %function Reset_Handler: ldr r0, =_estack mov sp, r0 /* set stack pointer */ /* Copy the data segment initializers from flash to SRAM */ movs r1, #0 b LoopCopyDataInit CopyDataInit: ldr r3, =_sidata ldr r3, [r3, r1] str r3, [r0, r1] adds r1, r1, #4 LoopCopyDataInit: ldr r0, =_sdata ldr r3, =_edata adds r2, r0, r1 cmp r2, r3 bcc CopyDataInit movs r1, #0 b LoopCopyIramFunction /* 这里就是我额外添加的,本来在操作r0 - r3之前需要先将其压入栈中,使用完之后在 恢复,但我观察到在这个位置添加代码,可以不用管r0 - r3的值,为了简单就直接放在这 个位置了 */ /* 自己添加代码 开始*/ CopyFunction: ldr r3, =_iram_function_addr ldr r3, [r3, r1] str r3, [r0, r1] adds r1, r1, #4 LoopCopyIramFunction: ldr r0, =_iram_function_start ldr r3, =_iram_function_end adds r2, r0, r1 cmp r2, r3 bcc CopyFunction /* 自己添加代码 结束 */ ldr r2, =_***ss b LoopFillZerobss /* Zero fill the bss segment. */ FillZerobss: movs r3, #0 str r3, [r2] adds r2, r2, #4 LoopFillZerobss: ldr r3, = _ebss cmp r2, r3 bcc FillZerobss /* Call the clock system intitialization function.*/ bl SystemInit /* Call static constructors */ bl __libc_init_array /* Call the application's entry point.*/ bl main LoopForever: b LoopForever (5)在NMI中断处理函数中随便添加几行无用代码。 volatile uint32_t count = 0; void NMI_Handler(void) { /* USER CODE BEGIN NonMaskableInt_IRQn 0 */ count++; FLASH->ECCR |= 0x80000000; /* USER CODE END NonMaskableInt_IRQn 0 */ /* USER CODE BEGIN NonMaskableInt_IRQn 1 */ /* USER CODE END NonMaskableInt_IRQn 1 */ } 7.测试 (1)先擦除0x8014000地址处的flash数据。 在擦除函数的后一句代码中加一个断点,执行到断点处再停止调试,把擦除0x8014000的代码注释掉,然后重新编译并进行调试模式。 可以看到这一页的数据都被擦除了。 打两个断点,再次编译运行。 分析: vWriteFlash是一个函数指针,里面存放的地址是0x80014f8,我们去看看0x80014f8里面是什么。 点击一下红框中的按钮:单步c语句变成单步汇编语句。 点击单步执行,可以看到,再调用一个函数时,编译器会帮我们做很多事情:它会将我们用到的那些cpu 寄存器压栈,最后执行bx r12跳转指令。问题的关键,怎么知道当前执行的函数体就是内存里的那一份呢? 点击单步调试,只要确认r12的值,就可以知道是不是内存里面的那一份函数体了。如果是内存的那一份,其值肯定是0x20开头的,如果是flash存的那份,其值肯定是0x80开头的。 正如我们猜想的那样,确实运行的是内存的那一份函数体。点击全是运行(在main函数的136行打了一个断点)。 可以看到这个flash里面的数据是有问题的,我们原本写入的数据是0x1234567812345678。点击单步调试,发现只要一读取该地址的数据,cpu就立马跳转到了NMI异常处理函数中。 测试结果符合预期,更为致命的是,即使全速运行,发现程序一直在触发NMI异常(因为ECC校验失败的中断标志位ECCD没有被清0),确实存在重大但难以发现的bug。 8.解决思路 既然是由于没有清标志位导致程序一直在NMI中出不来,那是不是可以在NMI中将该标志位清除,并且做些处理(如果是因为读取flash中的参数导致该问题,那是不是可以在NMI中设置一个标志位,当程序返回到mian中后,检查该标志位,如果被置位了,说明发生了ECC错误,需要使用默认参数来初始化各个硬件,并且把该页flash都擦除了,这样以后还可以用。 如果是在bootloader中读取app进行crc校验时发生ECC错误,直接在NMI中断处理函数中将app所在的页全部擦除,并且让cpu不发生跳转(不跳转到app)中即可),然后会返回到main函数中正常运行呢?经过验证,确实如此。 将该标志位清除之后,发现程序可以回到原来的地方运行。 可以看到cpu又恢复正常了。 |
|
|
|
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1632 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1559 浏览 1 评论
985 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
688 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1605 浏览 2 评论
1869浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
655浏览 4评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
525浏览 3评论
540浏览 3评论
stm32cubemx生成mdk-arm v4项目文件无法打开是什么原因导致的?
512浏览 3评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-25 18:32 , Processed in 0.876151 second(s), Total 78, Slave 61 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号