1. 问题背景
近期在评估国内某一款航模遥控器的时候,发现其遥控器 (STM32F407) 及接收机 (STM32F072) 均开启了 RDP 读保护,官方提供的固件更新方式是通过 USB 接口使用特定软件进行更新,通过 USB 接口描述(0483:df11 STMicroelectronics STM Device in DFU Mode)发现其使用的是 DFU 固件更新协议。
-
ST 公司提供的官方工具 DfuSe Demo
-- 可读取(Upload)设备固件,可以得到 DfuSe 格式的固件信息; 可通过工具 dfuse-extract 提取出原始的 flash 数据;
-- 修改 DfuSe 固件之后,需要重新计算尾部的 CRC,可修改脚本 dfuse_extract.py 来输出正确crc, 最后通过 DfuSE Demo 写回;
-- 修改固件写回(Download)的过程,成功更改 Flash 芯片中 "APP" 应用部分,但是存储于用户 "bootloader" 部分的数据未能更新成功; (而通过 JTAG 刷入是可以的) -
开源工具 dfu-util
-- dfu-util 可以读取(Upload)设备固件,读出的为原始 Flash 数据;
-- dfu-util 无法直接处理 DfuSe 格式的固件,可以通过 dfuse-extract 导出 raw binary; 而 raw binary通过 dfu-util 写入失败, 参数 -s 0x8000000:mass-erase:force -vvvvvvv 在 mass_erase 阶段失败
-- dfu-suffix -c 计算 DFU 固件是否正确, -a 可在尾部添加 CRC 校验和"版本信息",会多出16字节,且这里的版本号不被 DfuSe Demo 识别
这里我们可以发现一些问题:
-
为什么开启了 RDP,但是 DFU 却能读到 Flash 数据?
实际上后来的测试表明,这个板子的 DFU 是在 Flash memory 里由遥控器厂家自己实现的;通过 JTAG/SWD 调试可确认其 pc 指针指向的是 0x800xxxx 这样一个 flash 段地址,而非 Datasheet 里面描述的 bootloader 段(0x1FFFxxxx); 所以估计是哪里抄来的标准 DFU 实现,忘了读保护这回事了。 -
为什么没有使用芯片内置 bootloader 所提供的 DFU 功能?
这里可能的解释是,接收机 F072 芯片没有 DFU 支持,只能在 Flash 段自己实现 DFU;而为了代码一致性,或者就是单纯地偷懒,发射机选择了一样的方案,没有使用芯片内置的 DFU。 -
为什么通过 DFU 更新的时候,只修改成功 Flash 中的部分数据?
由于 DFU 是在用户 bootloader 里面实现的,而实际上该 bootloader 并不能更新自己,所以可能在逻辑上就只是完成 APP 应用部分的固件更新,而跳过了 bl 部分固件。
2. 验证 RDP 开启时 DFU 无法读取固件
这里我们主要需要确认 RDP 开启(level 1)时,芯片内置 DFU 功能无法读取固件
2.1 编写代码,启用 RDP
这里有个小插曲,一开始使用 Nueleo-L476RG 开发板,不过可能是由于代码写的有问题,后来把芯片搞坏了,部分存储块读写权限出了差错,导致各种奇奇怪怪的问题;然后在淘宝上购买了几块 F103 “绿药丸”(blue pill),但是老板错发成国产的 CKS32F103,是,我也是第一次见这芯片,以前只听过 GD32,可以说是非常有趣了。 原计划退货,但是既然国产芯意外“找上”了我,一定是上天安排好的,支持中国芯!自豪中国人! =_=
最后又重新下单了 STM32F103 和 STM32F401 “黑药丸”(Black pill), 这里又不得不说 “黑药丸” 是棒啊,黑色PCB, typeC 接口,背面没有乱七八糟的元件,还预留了 spi flash 焊盘,配置高了,价格却还和 “蓝药丸” 差不多,这性价比吹爆~
下面直接贴代码吧,不同芯片表现还有点差别,这些注释是多次实验得出来的结论:
(这里用的 RT-Thread 是为了方便实验,核心代码只调用了 HAL 函数,是完全可以裸跑的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
#include <time.h> #include <string.h> #include <rtthread.h> #define DBG_TAG "main" #define DBG_LVL DBG_LOG #include <rtdbg.h> #include <drv_common.h> #include <drivers/pin.h> #define LED GET_PIN(C, 13) #define LED_ON rt_pin_write(LED, 0); #define LED_OFF rt_pin_write(LED, 1); int Get_RDP(FLASH_OBProgramInitTypeDef *OBInit){ HAL_FLASHEx_OBGetConfig(OBInit); int rdp = OBInit->RDPLevel; switch(rdp){ case OB_RDP_LEVEL_0: rdp = 0; break; case OB_RDP_LEVEL_1: rdp = 1; break; case OB_RDP_LEVEL_2: rdp = 2; break; default: break; }; LOG_I("RDP level: 0x%x", rdp); LOG_I("OptionType: 0x%lx", OBInit->OptionType); return rdp; } void Enable_RDP(){ //https://community.st.com/s/question/0D50X00009XkiTzSAJ/stm32l100rct6-problem-with-read-out-protection FLASH_OBProgramInitTypeDef OBInit; if (Get_RDP(&OBInit) != 0){ LOG_I("RDP already set, now to continue..."); } else{ OBInit.RDPLevel = OB_RDP_LEVEL_1; OBInit.OptionType |= OPTIONBYTE_RDP; if(HAL_FLASH_Unlock() != HAL_OK) LOG_E("HAL_FLASH_Unlock Failed\r\n"); if(HAL_FLASH_OB_Unlock() != HAL_OK) LOG_E("HAL_FLASH_OB_Unlock Failed\r\n"); if(HAL_FLASHEx_OBProgram(&OBInit) != HAL_OK) LOG_E("HAL_FLASHEx_OBProgram Failed\r\n"); // for F103, HAL_FLASH_OB_Launch() returns void, and will cause a reset on CKS32F103 // for STM32F103 and STM32F401, it will not cause reset and freeze right here, we have to power cycle the chip LOG_I("Going to OB_Launch\r\n"); if(HAL_FLASH_OB_Launch() != HAL_OK) LOG_E("HAL_FLASH_OB_Launch failed\r\n"); // for STM32F103 and CKS32F103, whether to lock and when to lock make no difference // for STM32F401, can not lock before OB_Launch, or just leave it unlocked // if(HAL_FLASH_OB_Lock() != HAL_OK) // LOG_E("HAL_FLASH_OB_Lock Failed\r\n"); // if(HAL_FLASH_Lock() != HAL_OK) // LOG_E("HAL_FLASH_Lock Failed\r\n"); Get_RDP(&OBInit); } } int main(void) { LOG_I("Build Time: %s, %s\n", __DATE__, __TIME__); Enable_RDP(); int count = 0; char time_string[10] = {0}; rt_pin_mode(LED, PIN_MODE_OUTPUT); while (++count) { time_t t_now = time(NULL); strncpy(time_string, ctime(&t_now) + 11, 8); LOG_D("[%s] blink count: %d", time_string, count); for (int i=0; i<count; i++){ LED_ON; rt_thread_mdelay(200); LED_OFF; rt_thread_mdelay(200); } count = count == 5 ? 0: count; rt_thread_mdelay(1000); } return RT_EOK; } |
2.2 验证 DFU 读写功能
这里的结论就很简单了,在 F401 芯片上开启 RDP 之后,DFU 模式是不能进行读写的,必须先通过 JTAG/SWD 禁用 RDP (或 openocd 的 unlock flash)并自动触发 mass_erase 之后才可以正常从外部读写,结论符合预期。
《STM32 DFU 与 RDP 的一些实验》有1个想法