资讯动态

STM32F4基于RS-485的双Bank安全OTA升级方案

发布时间:2026/10/16 18:01:12 来源:尧图企业网站定制
简介本资源是一套面向STM32F4系列MCU开发者的完整OTA远程升级实践方案聚焦于通过串口或RS485总线实现安全可靠的固件更新适用于工业现场、智能终端等需离线升级的嵌入式场景适合具备C语言和Keil开发基础的中级以上嵌入式工程师。压缩包共277个文件涵盖90个头文件h与88个源码文件c构成完整的Bootloader与App双工程架构另有编译中间文件o/d、调试配置dbgconf、链接脚本sct、二进制镜像bin/hex/axf及自动化批处理脚本bat体现典型ARM Cortex-M4项目工程规范。资源包大小为2.86MB结构清晰、模块解耦便于理解跳转机制、Flash分区设计与通信协议对接逻辑。目前已有2540人学习下载读者可直接复用Bootloader框架、参考双工程协同启动流程并基于RS485物理层拓展多节点升级能力。1. STM32F4串口/485 OTA升级为什么不用USB或Wi-Fi而坚持用最“土”的物理接口你手头有一批部署在工业现场的STM32F4设备——它们装在配电柜里、嵌在PLC机架中、固定在户外防雨箱内离最近的网口有3米离Wi-Fi信号源隔着两堵承重墙USB口被胶封死唯一暴露在外的是DB9座子上那对RS-485 A/B线。这时候客户突然打来电话“固件有个紧急逻辑缺陷今晚必须全厂升级。”你翻出刚写完的OTA方案文档发现里面赫然写着“依赖ESP32 Wi-Fi模块”——那一刻你意识到再炫的云平台、再快的HTTP下载在真实产线里可能不如一根能扛2000米干扰的双绞线可靠。这套方案专为这类“无网、无USB、有485、要稳如磐石”的场景而生它不依赖外部通信模组不引入额外BOM成本不增加EMC风险点所有升级逻辑全部跑在STM32F4本体上它把Bootloader和Application严格隔离支持断电续传、校验回滚、地址映射保护更重要的是它用纯C实现不依赖HAL库版本适配F407/F429/F469全系列实测在115200bps波特率下128KB固件升级耗时稳定在10.3±0.2秒含三次CRC32校验。如果你正在做工业控制器、智能电表、楼宇DDC类毕设或课设或者正被甲方要求“不加新硬件、只改固件”的OTA需求压得喘不过气——这份方案就是你拆掉胶封、接上串口调试器后能立刻跑通的第一版可交付物。2. 总体架构设计双Bank分区 串口协议栈 Bootloader跳转三重保障2.1 存储布局规划为什么必须用双Bank而不是简单覆盖App区STM32F4的Flash擦除以扇区Sector为单位最小扇区大小为16KBF407为例而Application通常大于64KB。若采用单Bank覆盖式升级一旦升级中途断电旧固件被擦除但新固件未写满设备将彻底变砖。因此我们强制采用双Bank分区结构地址区间F407VGT6大小用途是否可执行0x0800000016KBBootloader固定不可擦✅0x08004000128KBBank0当前运行App✅0x08024000128KBBank1待升级/备用App✅0x080440004KBParameter Sector存储标志位、版本号、校验值❌提示Parameter Sector必须独立于两个Bank之外。原因在于若把它放在Bank0末尾升级时整个Bank0被擦除参数就丢了放在Bank1也不行——它本就是待写入区。我们选第5个扇区起始地址0x08044000仅用于存放32字节结构体永不擦除仅更新。关键代码定义flash_layout.h#define BOOTLOADER_BASE 0x08000000U #define BOOTLOADER_SIZE (16 * 1024U) #define BANK0_BASE 0x08004000U #define BANK0_SIZE (128 * 1024U) #define BANK1_BASE 0x08024000U #define BANK1_SIZE (128 * 1024U) #define PARAM_SECTOR_BASE 0x08044000U #define PARAM_SECTOR_SIZE (4 * 1024U) // 参数结构体存于PARAM_SECTOR_BASE typedef struct { uint8_t valid_flag; // 0xAA表示参数有效否则视为首次上电 uint8_t active_bank; // 0Bank0, 1Bank1 uint32_t app_version; // 4字节版本号如0x01000001表示v1.0.0.1 uint32_t app_crc32; // 当前激活Bank的完整CRC32 uint32_t upgrade_crc32; // Bank1中待升级固件的CRC32升级中写入 } param_t;这个布局不是拍脑袋定的。我带过的某楼宇自控项目曾因把Parameter塞进Bank0导致一次雷击浪涌后Bank0扇区损坏设备重启反复进入Bootloader却无法识别有效App现场更换整块PCB。从那以后我所有STM32 OTA方案都强制Parameter独立扇区——这是用硬件故障换来的血泪经验。2.2 Bootloader核心流程从串口接收、校验到安全跳转的七步闭环Bootloader不是一段“等命令”的死循环而是一个状态机驱动的闭环系统。它必须回答三个问题① 怎么确认收到的是合法升级包② 怎么防止写错地址导致锁死③ 怎么确保跳转后App能正确初始化以下是精简后的主干逻辑boot_main.c// 状态枚举 typedef enum { BOOT_IDLE, BOOT_WAIT_START, BOOT_RECEIVE_HEADER, BOOT_RECEIVE_PAYLOAD, BOOT_VERIFY_CRC, BOOT_WRITE_FLASH, BOOT_JUMP_TO_APP } boot_state_t; // 主循环精简版 void boot_main_loop(void) { static boot_state_t state BOOT_IDLE; static uint8_t rx_buf[256]; static uint16_t rx_len 0; static uint32_t target_addr 0; static uint32_t payload_len 0; static uint32_t crc_received 0; switch(state) { case BOOT_IDLE: if (is_upgrade_request()) { // 检测串口是否收到UPGR魔数 state BOOT_WAIT_START; HAL_UART_Receive_IT(huart2, rx_buf[0], 1); // 启动中断接收 } break; case BOOT_WAIT_START: if (rx_buf[0] 0x55) { // 升级包起始字节0x55 state BOOT_RECEIVE_HEADER; rx_len 0; HAL_UART_Receive_IT(huart2, rx_buf, 8); // 接收8字节头长度地址CRC } break; case BOOT_RECEIVE_HEADER: if (rx_len 8) { payload_len *(uint32_t*)(rx_buf); // 偏移0payload长度大端 target_addr *(uint32_t*)(rx_buf4); // 偏移4目标地址大端 crc_received *(uint32_t*)(rx_buf8); // 实际为rx_buf8但此处仅示意结构 if (is_valid_flash_addr(target_addr)) { state BOOT_RECEIVE_PAYLOAD; rx_len 0; HAL_UART_Receive_IT(huart2, rx_buf, payload_len); } else { send_error_response(ERR_INVALID_ADDR); state BOOT_IDLE; } } break; case BOOT_RECEIVE_PAYLOAD: if (rx_len payload_len) { uint32_t calc_crc crc32_calc(rx_buf, payload_len); if (calc_crc crc_received) { state BOOT_VERIFY_CRC; } else { send_error_response(ERR_CRC_MISMATCH); state BOOT_IDLE; } } break; case BOOT_VERIFY_CRC: // 此处擦除目标Bank注意只擦Bank1Bank0永不擦 if (target_addr BANK1_BASE target_addr (BANK1_BASE BANK1_SIZE)) { erase_sector(BANK1_BASE); // 封装好的扇区擦除函数 state BOOT_WRITE_FLASH; } break; case BOOT_WRITE_FLASH: write_flash_block(target_addr, rx_buf, payload_len); // 写完后立即读回校验 if (verify_flash_block(target_addr, rx_buf, payload_len)) { update_param_sector(1, payload_len, calc_crc); // 标记Bank1为待激活写CRC state BOOT_JUMP_TO_APP; } else { send_error_response(ERR_WRITE_FAIL); state BOOT_IDLE; } break; case BOOT_JUMP_TO_APP: // 关闭所有外设清空缓存设置MSP跳转 disable_all_periph(); SCB-ICSR | SCB_ICSR_CPUPEND_Msk; // 清指令缓存 __DSB(); __ISB(); jump_to_app(BANK1_BASE); // 汇编跳转函数 break; } }这段代码背后藏着几个硬性设计原则绝不信任串口数据每个环节都有校验起始魔数→头CRC→payload CRC→写后回读任何一环失败立即返回BOOT_IDLE并发送错误码写操作原子化擦除→写入→回读校验三步缺一不可且擦除前必校验地址合法性防止误写Bootloader区跳转前彻底清理关闭UART、TIM、DMA等所有可能干扰App初始化的外设手动触发ICache清空避免旧指令残留Bank切换由参数扇区控制Bootloader每次启动只读param_t.active_bank决定跳向Bank0还是Bank1升级成功后才更新该字段。很多初学者会把跳转写成((void(*)(void))(*((uint32_t*)BANK0_BASE1)))();——这看似简洁但忽略了MSP初始化。STM32复位后从0x08000000取MSP从0x08000004取Reset_Handler。若直接跳过去新App的MSP仍是Bootloader的栈顶极易在第一个函数调用时堆栈溢出。我们封装的jump_to_app()汇编函数会先从目标Bank首地址0处加载MSP再从4处取PC这才是安全跳转。2.3 串口/485协议设计为什么不用Modbus而自定义二进制帧工业现场常见误区直接套用Modbus RTU协议传输固件。问题在于——Modbus单帧最大256字节含地址/功能码/CRC而固件升级需高频收发Modbus的从机地址广播机制、超时重传逻辑、功能码解析开销会把115200bps实际吞吐压到不足30KB/s。我们采用极简二进制帧单帧承载256字节payload协议开销仅5字节字段长度说明Start Byte1B固定0x55Payload Len2B大端实际数据长度≤256Target Addr4B大端Flash写入起始地址PayloadN B原始固件二进制数据CRC16 (CCITT)2B覆盖Start到Payload的CRC16为什么用CRC16而非CRC32因为升级包本身已含完整CRC32校验见2.2节header帧级CRC16仅用于快速检测传输误码降低CPU计算负担。实测在485总线噪声环境下CRC16误检率1e-9完全满足工业要求。关键帧解析函数protocol_parser.c// 解析一帧返回0成功非0错误码 uint8_t parse_frame(uint8_t *frame, uint16_t len, frame_t *out) { if (len 9) return ERR_FRAME_TOO_SHORT; // 12429 if (frame[0] ! 0x55) return ERR_INVALID_START; out-payload_len (frame[1] 8) | frame[2]; // 大端转主机序 if (out-payload_len 256 || out-payload_len 0) return ERR_INVALID_LEN; out-target_addr (frame[3] 24) | (frame[4] 16) | (frame[5] 8) | frame[6]; // CRC16校验CCITT, poly0x1021, init0xFFFF uint16_t crc_calc crc16_ccitt(frame, 7); // 计算Start到Addr共7字节 uint16_t crc_recv (frame[7] 8) | frame[8]; if (crc_calc ! crc_recv) return ERR_CRC16_FAIL; // 拷贝payload注意frame[9]开始才是payload memcpy(out-payload, frame[9], out-payload_len); return 0; }这个协议设计经受过真实考验某水泥厂DCS系统升级时485总线与变频器共缆敷设工频干扰导致每100帧出现1~2帧乱码。由于我们每帧独立CRC16校验乱码帧被立即丢弃上层重传机制由PC端升级工具实现自动补发全程无一次升级失败。反观用Modbus的同事因RTU超时重传机制僵化升级耗时翻倍且偶发卡死。3. Bootloader与Application协同设计链接脚本、向量表偏移、中断重映射全实战3.1 链接脚本.ld文件如何让Bank0和Bank1拥有各自独立的地址空间多数初学者卡在第一步Application编译后仍试图从0x08000000启动结果Bootloader跳过去就跑飞。根源在于链接脚本未重定向。我们必须为Bank0和Bank1分别编写两套.ld文件并在编译时通过宏选择。Bank0 Application的stm32f407vg_b0.ld关键段MEMORY { RAM (xrw) : ORIGIN 0x20000000, LENGTH 128K FLASH (rx) : ORIGIN 0x08004000, LENGTH 128K /* ← 关键起始地址不再是0x08000000 */ } SECTIONS { .isr_vector : { . ALIGN(4); KEEP(*(.isr_vector)) /* 中断向量表 */ . ALIGN(4); } FLASH .text : { . ALIGN(4); *(.text) /* 代码段 */ *(.text*) /* 所有.text* */ . ALIGN(4); } FLASH ... }Bank1 Application的stm32f407vg_b1.ld只需改一行FLASH (rx) : ORIGIN 0x08024000, LENGTH 128K /* ← Bank1地址 */注意.isr_vector段必须显式保留且必须位于Flash起始位置即0x08004000或0x08024000。若被优化掉CPU复位后取不到正确的MSP和Reset_Handler必然跑飞。编译时通过Makefile传递# 编译Bank0 App APP_B0_HEX build/app_b0.hex $(APP_B0_HEX): $(OBJS_B0) arm-none-eabi-gcc -T stm32f407vg_b0.ld -Wl,--gc-sections $^ -o $ # 编译Bank1 App APP_B1_HEX build/app_b1.hex $(APP_B1_HEX): $(OBJS_B1) arm-none-eabi-gcc -T stm32f407vg_b1.ld -Wl,--gc-sections $^ -o $3.2 向量表偏移VTOR为什么Application必须手动设置SCB-VTOR即使链接脚本正确Application启动后仍可能响应错误中断——因为CM4内核默认从0x08000000取向量表而Bank0的向量表实际在0x08004000。解决方案是在Application的main()最开头强制重映射// app_main.c #include stm32f4xx.h int main(void) { // 关键重映射向量表到当前Bank起始地址 #ifdef BANK0_ACTIVE SCB-VTOR 0x08004000U; // Bank0向量表地址 #elif defined(BANK1_ACTIVE) SCB-VTOR 0x08024000U; // Bank1向量表地址 #endif __DSB(); __ISB(); // 确保VTOR生效 HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // ... 其余初始化 }这个SCB-VTOR赋值不能晚于任何外设初始化之前。某次调试中A同学把MX_USART2_UART_Init()放在了SCB-VTOR之前结果UART初始化时触发了NMI因NVIC配置异常而NMI向量仍在0x08000000处指向Bootloader的NMI Handler导致死循环。后来我们统一规定所有Application的main()第一行必须是VTOR设置第二行是__DSB(); __ISB();第三行才是HAL_Init()。3.3 中断服务函数ISR重定向如何让Bank0和Bank1共用同一套HAL驱动HAL库默认将所有ISR放在stm32f4xx_it.c中且强绑定到固定向量表索引。若Bank0和Bank1使用同一份stm32f4xx_it.c则中断向量表内容会冲突。我们的解法是在Bootloader中预留中断向量跳转桩Application自行提供ISR实现。Bootloader的向量表startup_stm32f407vg.s中修改.word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler .word 0 .word 0 .word 0 .word SVC_Handler .word DebugMon_Handler .word 0 .word PendSV_Handler .word SysTick_Handler /* 外设中断向量全部指向跳转桩 */ .word USART2_IRQHandler_STUB /* ← 不是真正的Handler而是跳转桩 */ .word TIM2_IRQHandler_STUB .word DMA1_Stream5_IRQHandler_STUB ...跳转桩实现boot_irq_stubs.c// 定义函数指针类型 typedef void (*pFunc)(void); // 声明Application提供的ISR函数指针由Application在运行时注册 extern pFunc app_usart2_handler; extern pFunc app_tim2_handler; // 跳转桩检查指针是否有效有效则跳转否则进入死循环 void USART2_IRQHandler_STUB(void) { if (app_usart2_handler ! NULL ((uint32_t)app_usart2_handler 0x08004000U) ((uint32_t)app_usart2_handler 0x08040000U)) { app_usart2_handler(); } else { while(1); // 无效指针死循环 } } void TIM2_IRQHandler_STUB(void) { if (app_tim2_handler ! NULL) app_tim2_handler(); else while(1); }Application在main()中注册// app_main.c extern void USART2_IRQHandler_STUB(void); extern void TIM2_IRQHandler_STUB(void); pFunc app_usart2_handler USART2_IRQHandler; pFunc app_tim2_handler TIM2_IRQHandler; int main(void) { SCB-VTOR BANK_BASE; // 如前所述 __DSB(); __ISB(); HAL_Init(); SystemClock_Config(); // 注册ISR关键必须在HAL初始化之后NVIC使能之前 register_app_irq_handlers(); MX_GPIO_Init(); MX_USART2_UART_Init(); // 此时HAL会配置NVIC触发的是STUB MX_TIM2_Init(); // ... }这套机制让Bootloader保持轻量不包含任何外设驱动又让Application完全掌控中断处理逻辑避免了HAL库版本兼容性问题——某次升级HAL库到1.27.0发现HAL_UART_IRQHandler内部调用顺序变更若Bootloader也链接HAL就会与Application的HAL版本冲突。用跳转桩双方彻底解耦。4. 升级工具与通信可靠性Python上位机、485总线抗干扰、断电续传策略4.1 Python上位机upgrade_tool.py如何实现带进度条、断点续传、自动重试的工业级工具不要用SecureCRT或XShell手动发bin——那是Demo不是交付。我们用Python写一个真正能进产线的工具核心能力① 自动识别可用串口② 按帧发送每帧等待ACK③ 断线后从断点继续④ 进度条实时显示⑤ 升级失败自动回滚。关键逻辑简化版import serial import time import sys from tqdm import tqdm def send_firmware(port, bin_path, bank1): ser serial.Serial(port, 115200, timeout2) # 1. 发送升级请求魔数 ser.write(bUPGR) time.sleep(0.1) resp ser.read(1) if resp ! bK: # Bootloader返回K表示准备就绪 raise Exception(Bootloader not ready) # 2. 读取bin文件 with open(bin_path, rb) as f: data f.read() total_len len(data) addr 0x08024000 if bank 1 else 0x08004000 # 3. 分帧发送每帧256字节payload for i in tqdm(range(0, total_len, 256), descUpgrading): chunk data[i:i256] payload_len len(chunk) # 构造帧0x55 len(2B) addr(4B) payload crc16(2B) frame bytearray([0x55]) frame payload_len.to_bytes(2, big) frame addr.to_bytes(4, big) frame chunk crc16_val crc16_ccitt(frame[0:9]) # 计算前9字节CRC16 frame crc16_val.to_bytes(2, big) # 发送并等待ACK for retry in range(3): ser.write(frame) ack ser.read(1) if ack bA: # Bootloader返回A表示本帧OK addr payload_len break time.sleep(0.05) else: raise Exception(fFrame {i//256} failed after 3 retries) # 4. 发送升级完成指令 ser.write(bDONE) final_resp ser.read(2) if final_resp ! bOK: raise Exception(Upgrade failed at final step) print(✅ Upgrade completed successfully!) ser.close() if __name__ __main__: if len(sys.argv) 3: print(Usage: python upgrade_tool.py COM3 firmware.bin [bank1]) sys.exit(1) send_firmware(sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv)3 else 1)这个工具的关键设计点tqdm进度条不是装饰而是真实反映发送进度方便产线工人预估时间三次重试机制485总线受干扰丢帧是常态重试比报错退出更符合工业逻辑地址自增addr payload_len确保连续写入无需Application侧拼接DONE指令通知Bootloader校验整个Bank并更新参数扇区是升级成功的最终确认。某次客户验收要求“升级过程中拔掉485线再插回必须能续传”。我们就在tqdm循环里加了ser.isOpen()检测断开时暂停进度条重连后自动从i位置继续——这就是产线思维不追求技术炫酷只求鲁棒可靠。4.2 485总线抗干扰实战终端电阻、TVS、软件滤波三道防线硬件层面485不是接上线就能用。我们总结出必须做的三件事终端电阻在总线最远两端各并联120Ω电阻非每台设备都接。某次调试16台设备挂同一总线未加终端电阻波特率超过57600bps就频繁丢帧。加上后115200bps稳定运行。TVS二极管在485收发器如SP3485的A/B引脚对GND加SM712双向TVS钳位电压±12V。某化工厂现场雷雨天多次出现485芯片击穿加TVS后三年零故障。软件滤波UART接收中断中对每个字节做“3取2”滤波——连续采样3次取相同值两次以上的作为有效值。虽增加1ms延迟但彻底消除毛刺干扰。软件滤波代码usart_driver.c// 在HAL_UART_RxCpltCallback中调用 uint8_t uart_filter_byte(uint8_t raw_byte) { static uint8_t hist[3] {0}; static uint8_t idx 0; hist[idx] raw_byte; idx (idx 1) % 3; // 统计3个历史值中出现次数≥2的值 if (hist[0] hist[1] || hist[0] hist[2]) return hist[0]; if (hist[1] hist[2]) return hist[1]; return raw_byte; // 无重复返回原始值极少发生 }4.3 断电续传与回滚机制如何让升级过程像数据库事务一样ACID工业设备升级最怕断电。我们的方案实现两级保护断电续传Bootloader在Parameter Sector中记录last_write_addr最后成功写入地址。上电后若检测到active_bank未更新且last_write_addr 0则从该地址继续接收剩余数据。自动回滚若升级完成后校验失败app_crc32不匹配Bootloader不更新active_bank而是清除last_write_addr下次启动仍运行旧App。Parameter Sector更新逻辑param_ops.c// 升级中记录当前写入地址 void update_last_write_addr(uint32_t addr) { param_t p; read_param_sector(p); p.last_write_addr addr; write_param_sector(p); // 整个sector擦除重写 } // 升级成功标记Bank1为激活清除last_write_addr void mark_bank1_active(uint32_t crc32) { param_t p; read_param_sector(p); p.active_bank 1; p.app_crc32 crc32; // Bank1的CRC p.upgrade_crc32 0; // 清除升级中CRC p.last_write_addr 0; // 清除续传标记 write_param_sector(p); } // Bootloader启动时检查 void check_upgrade_state(void) { param_t p; read_param_sector(p); if (p.valid_flag ! 0xAA) { // 首次上电初始化参数 p.active_bank 0; p.app_version 0x01000000; p.app_crc32 0; p.upgrade_crc32 0; p.last_write_addr 0; write_param_sector(p); return; } if (p.last_write_addr ! 0) { // 检测到未完成升级进入续传模式 enter_resume_mode(p.last_write_addr); } }这个设计让升级具备“事务性”要么全成功更新active_bank要么全失败保持原状绝不存在“半砖”状态。某次在电梯控制柜中升级物业人员误关配电箱设备断电。恢复供电后Bootloader检测到last_write_addr0x08028A00自动从该地址继续接收剩余64KB全程无人干预。5. 常见问题排查Bootloader不响应、跳转后跑飞、升级后CRC校验失败的五大翻车现场5.1 现象串口发送UPGR后Bootloader无任何响应不返回K原因Bootloader未启用串口时钟或GPIO复用配置错误。常见于使用CubeMX生成代码时未勾选USART2的Clock Configuration或PA2/PA3未设置为Alternate Function。解决检查RCC-APB1ENR寄存器确认USART2EN位为1用示波器测PA2是否有TX波形若无波形检查GPIOA-AFR[0]中PA2的AF值是否为0x7对应USART2_TX。5.2 现象Bootloader返回K但发送第一帧后返回X错误码原因帧头中的Target Addr超出Flash范围或Payload Len为0。Bootloader的is_valid_flash_addr()函数校验失败。解决用调试器查看parse_frame()中target_addr变量值。常见错误是PC端工具将地址传为小端而Bootloader按大端解析。例如期望0x08024000PC端误传为0x00400208导致校验失败。5.3 现象升级完成后跳转App运行几秒后HardFault原因Application的SCB-VTOR未设置或设置错误地址。此时CPU仍从0x08000000取向量表执行Bootloader的中断Handler造成栈溢出。解决在Application的main()第一行加printf(VTOR%08X\r\n, SCB-VTOR);确认输出为08004000或08024000。若为08000000检查链接脚本是否生效或SCB-VTOR赋值语句是否被编译器优化掉加__attribute__((used))。5.4 现象升级后App能运行但UART收不到数据原因Application的HAL_UART_MspInit()中__HAL_RCC_USART2_CLK_ENABLE()被调用两次——Bootloader已开启时钟Application再次开启导致寄存器状态异常。解决在Application的MspInit中先检查时钟是否已使能void HAL_UART_MspInit(UART_HandleTypeDef* huart) { if (huart-Instance USART2) { if (!(RCC-APB1ENR RCC_APB1ENR_USART2EN)) { __HAL_RCC_USART2_CLK_ENABLE(); } // ... GPIO初始化 } }5.5 现象升级工具显示100%但设备重启后仍运行旧固件原因Parameter Sector写入失败。常见于未擦除Parameter Sector就直接写入Flash写入前必须擦除或写入地址越界写到了0x08044000之外。解决在write_param_sector()中强制先擦除整个扇区HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR); if (HAL_FLASHEx_Erase(eraseInitStruct, pageError) ! HAL_OK) { // 报错 } // 再逐字写入 for (int i 0; i sizeof(param_t); i 4) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, PARAM_SECTOR_BASE i, *(uint32_t*)((uint8_t*)p本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑