简介:面向新能源汽车直流充电桩开发的嵌入式工程资料,适合具备一定STM32基础、希望快速上手充电桩控制与通信程序设计的开发者,可用于毕业设计、竞赛或产品预研阶段。资源围绕STM32F10x平台实现充电控制、通信交互、用户界面、安全监测与数据记录等核心模块,源码层次清晰,便于实际项目移植与二次开发。压缩包共包含192个文件,以91个.h头文件、82个.c源文件和9个.s启动文件为主,另有uvprojx、cproject等工程配置文件以及JLinkSettings.ini调试设置、BAT批处理脚本,整体体积约609KB,结构紧凑,可直接导入MDK或Eclipse环境查看。内容预览可见stm32f10x系列驱动、uip协议栈、lcd_ui界面等关键代码,覆盖从底层驱动到上层交互的完整程序链路,省去从零搭建框架的繁琐过程。目前已有224人学习下载,适合正在做充电桩相关项目的开发者作为程序框架与排错参考。
1. 直流充电桩为什么选 STM32F10x 做主控
一台直流充电桩摆在现场,主控要同时干的事比大多数人想的杂:和 BMS 握手、看电压电流、控输出功率、走以太网上传数据、驱动 LCD 和按键、还要处理急停和绝缘检测。高端的可以用 Cortex-A 加 Linux,但大量模块化直流桩,尤其功率模块外置的机型,一颗 STM32F103 级别的 MCU 就能扛下来。ARM Cortex-M3 内核、72 MHz 主频,跑 uIP 协议栈和 Modbus 轮询足够;标准外设库的函数级封装,让 ADC、TIM、Flash 这些外设的开发门槛压得很低,出了问题翻代码也比翻 HAL 的抽象层快。
项目压缩包里能看到 stm32f10x_rcc.c、stm32f10x_adc.c、stm32f10x_tim.c 这些标准库文件,还有 stm32_eth.c、uip.c、lcd_ui.c、fw_update.bin,说明这套程序走的是芯片选型务实、软件分层清晰的路线。适合的对象是做桩控、充电模块控制器、或者想拿 STM32 入门电力电子控制的人——你不需要懂 Linux 设备树,但能把中断、DMA、Flash 擦写这些底层玩明白。
2. 充电控制核心:ADC 采样链路与定时器 PWM 输出
2.1 电压电流采样为什么走 ADC + DMA,而不是定时器里轮询
直流充电桩输出电压范围常见 200 V 到 1000 V,电流最大能到 250 A,这些量不可能直接进 MCU,都得先经过霍尔传感器或隔离运放,变成 0~3.3 V 的模拟信号,再进 STM32 的 ADC。F103 的 ADC 是 12 位,采样率最高 1 MHz,对充电控制这种毫秒级响应场景绰绰有余。
很多初学者第一个坑是把 ADC 采集放在主循环里用 delay 轮询。问题在于,充电过程中电压电流波形有纹波,特别是功率模块开关频率在 20 kHz 到 100 kHz 时,单次采样结果抖动非常明显,而且主循环里一旦跑 LCD 刷新或 uIP 协议栈,采集周期就变得不稳定,PID 调节器拿到的数据时间基准是乱的。
常见做法是用 ADC1 的注入组或规则组多通道扫描,配合 DMA 循环模式,让硬件自动把电压、电流、温度三路数据搬进内存缓冲区,主循环只去读最新值。代码骨架如下:
void ADC_DMA_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // PA0: 充电电压采样, PA1: 充电电流采样 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 2; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &DMA_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 2; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }DMA 缓冲区adc_buf[0]始终保存最近的电压原始值,adc_buf[1]是电流原始值,不需要中断干预。选ADC_SampleTime_239Cycles5是为了降低信号源内阻带来的采样误差,充电桩的采样信号虽然经过运放跟随,但长线传输引入的阻抗不可忽略,采样时间拉长能让内部采样电容充分充电。注意DMA_BufferSize必须和通道数一致,这里两个通道设 2,多一个少一个都会导致缓冲区错位。
2.2 定时器 PWM 与充电策略怎么配合
直流充电桩控制功率模块,一般是 MCU 输出 PWM 给移相全桥或 LLC 变换器的驱动芯片,通过调节占空比或移相角来改变输出电压电流。F103 的 TIM1 是高级定时器,带互补输出和死区插入,可以直接驱动半桥电路,这也是项目里出现 stm32f10x_tim.c 的原因。
充电策略可以拆成三个阶段:恒流阶段、恒压阶段、涓流阶段。BMS 在握手阶段会告诉桩端需求的充电曲线,主控根据当前电压/电流反馈,实时调整 PWM 目标值。
| 阶段 | 判定条件 | 控制目标 | 功率模块状态 |
|---|---|---|---|
| 恒流 | 电池电压低于恒压阈值 | 输出电流恒定在设定值 | CC 模式 |
| 恒压 | 电池电压达到阈值 | 输出电压恒定,电流自然下降 | CV 模式 |
| 涓流 | 电流下降到 0.1 C 以下 | 维持最小充电电流 | 小功率输出 |
TIM1 产生中心对齐 PWM,频率 20 kHz,波形质量适合 LLC;死区时间按功率管爬坡时间算,一般设 100 ns 到 300 ns。初始化代码里的关键参数是 PSC 和 ARR:
void TIM1_PWM_Init(uint16_t freq_khz, uint16_t deadtime_ns) { TIM_TimeBaseInitTypeDef TIM_BaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 72 MHz / (PSC + 1) / (ARR + 1) = PWM 频率 TIM_BaseInitStructure.TIM_Prescaler = 0; TIM_BaseInitStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned2; TIM_BaseInitStructure.TIM_Period = 3600 - 1; // 72MHz / 3600 = 20kHz TIM_BaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, &TIM_BaseInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse = 1800; // 初始占空比 50% TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set; TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime = deadtime_ns; // 按寄存器换算 TIM_BDTRInitStructure.TIM_Break = TIM_Break_Enable; TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }TIM_Prescaler = 0意味着定时器时钟就是 72 MHz,TIM_Period = 3600 - 1配合中心对齐模式,实际 PWM 频率是 72 MHz / 3600 = 20 kHz。占空比通过改变TIM_Pulse实现,范围 0 到 3599。调试时用示波器看 CH1 和 CH1N 之间的死区时间,如果发现上下管直通炸机,先查TIM_BDTRInitStructure.TIM_DeadTime的寄存器换算值——它不是直接填纳秒数,不同芯片版本换算关系有差异,F103 的 DT 寄存器每个步进约等于 tCK_INT 的倍数,仔细对照参考手册的表格。
2.3 闭环参数在串口上怎么调
电流环和电压环的闭环,F103 上跑数字 PID 完全够。实际工程项目里更常见的是 PI 控制器,D 项对采样噪声太敏感,充电桩这种大感性负载场景,微分项放大了反而容易震荡。
建议在串口调试助手输出调节过程中的关键变量:目标电压、实际电压、PWM 占空比、PI 输出值。把这个调试通道做成独立中断,不占用主循环,用 DMA 发送,避免和 uIP 协议栈抢 CPU 时间。参数整定从纯 P 开始,P 值逐渐增大到系统开始震荡,然后取临界值的一半,再加 I 消除稳态误差。这是齐格勒-尼科尔斯经验的简化流程,对充电桩这种响应速度要求不极端的对象足够。
3. 通信链路:uIP 协议栈与 STM32 以太网实现
3.1 为什么 uIP 比 lwIP 更适合 F103
直流充电桩需要和后台监控系统通信,上报充电状态、计费信息、故障代码,同时接收远程启停指令。以太网是主流选择,而 F103 资源有限——SRAM 只有 20 KB 到 64 KB,跑完整 lwIP 虽然可行,但留给协议栈的缓冲就会挤压控制任务的生存空间。uIP 是专为 8/16 位 MCU 设计的轻量级 TCP/IP 协议栈,代码量小、内存占用低,配合 stm32_eth.c 驱动文件,正好塞进 F103 的资源预算。
压缩包里的 stm32_eth.c 和 uip.c 是一对:前者负责 STM32 内部以太网 MAC 的寄存器操作,后者实现 TCP/IP 协议逻辑。两者的连接点在 uip_input 和 uip_periodic 两个函数上,前者处理网卡收到的报文,后者驱动 TCP 定时器。
3.2 以太网驱动与 uIP 轮询的接线方式
初始化流程分三层:MAC 初始化、PHY 复位、uIP 配置。F103 系列有的型号带内部 MAC,外部接 PHY 芯片,常见是 DP83848 或 LAN8720;不带内部 MAC 的型号会用 SPI 接口外挂 ENC28J60。项目里既然直接叫 stm32_eth.c,大概率走的是内部 MAC 加外部 PHY 路线。
MAC 初始化核心是设置 MAC 地址、配置 RMII 接口、开启接收中断。PHY 部分要读取 ID 寄存器确认芯片在位,然后配置自动协商。uIP 侧主要做三件事:设置 IP 和网关、注册 TCP 端口监听、在 while(1) 循环里周期调用 uip_periodic。
void uIP_Server_Init(uint8_t *ip_addr, uint16_t port) { uip_ipaddr_t ipaddr; uip_init(); uip_ipaddr(ipaddr, ip_addr[0], ip_addr[1], ip_addr[2], ip_addr[3]); uip_sethostaddr(ipaddr); // 监听充电桩上位机端口,例如 502 (Modbus TCP) uip_listen(HTONS(port)); } void uIP_Poll_Task(void) { uip_len = stm32_eth_read_packet(); // 从 MAC 接收 FIFO 读一包 if (uip_len > 0) { uip_input(); // 处理收到的包 if (uip_len > 0) { stm32_eth_send_packet(); // 有响应数据则发出 } } for (int i = 0; i < UIP_CONNS; i++) { uip_periodic(i); // 驱动每个 TCP 连接 if (uip_len > 0) { stm32_eth_send_packet(); } } }uIP 的设计哲学是事件驱动,收到报文后 uip_input 里会自动调用应用层回调,响应数据存放在 uip_appdata 缓冲区,uip_len标记数据长度。如果回调里没有设置 uip_len,协议栈就认为应用层没有响应,不会发出空包。这里容易踩的坑是uip_periodic的调用频率——TCP 超时重传依赖这个周期性驱动,常见做法是放在 100 ms 的定时器中断标志里触发,延迟太长会导致 TCP 建连超时。
3.3 充电桩通信协议怎么定
充电桩和后台的通信,国标里对直流充电桩有 GB/T 27930 的 CAN 通信要求,但以太网层面的协议并没有统一强制格式,很多厂家用 Modbus TCP 或私有 TCP 协议。Modbus TCP 的好处是调试工具多、现成上位机组件多,wireshark 抓包解析直观。
| 功能码 | 寄存器地址 | 含义 | 读写属性 |
|---|---|---|---|
| 0x03 | 0x0000 | 输出电压值(0.1V) | 只读 |
| 0x03 | 0x0001 | 输出电流值(0.1A) | 只读 |
| 0x03 | 0x0002 | 充电状态字 | 只读 |
| 0x06 | 0x0010 | 启动充电命令 | 读写 |
| 0x06 | 0x0011 | 停机命令 | 读写 |
| 0x10 | 0x0020 | 充电参数下发(电压/电流) | 读写 |
Modbus 报文解析放在 uIP 回调里做,收到完整一帧后,校验 CRC16,再分发到功能码处理函数。如果项目文件里有 freemodbus 移植痕迹,那么驱动层可以直接复用,只把串口收发换成 TCP 收发。这里注意 Modbus TCP 的报文和 RTU 不同,MPAB 头占 6 个字节,CRC 校验去掉了,解析时不要套用 RTU 的帧格式。
4. 从 autotools 到 J-Link:构建环境与在线调试
4.1 STM32 标准库工程的构建方式
这套程序用的是 STM32F10x 标准外设库,工程组织方式和现在主流的 STM32CubeMX 生成的 HAL 库工程有明显区别。标准库工程更直接:每个外设对应一个 .c/.h 文件,启动文件 startup_stm32f10x_cl.lst 里列出向量表,编译时直接包含 stm32f10x_rcc.c、stm32f10x_flash.c 这些源文件即可,不需要额外生成初始化代码。
构建系统方面,项目里有 .autotools 目录和 Target 1.BAT。autotools 是 Linux 系的构建工具链,用来生成 Makefile;但在 Windows 环境下开发 STM32,更多人用 Keil MDK 或 IAR。Target 1.BAT 是批处理脚本,常见做法是设置编译器路径、调用 make 或者直接启动 Keil 的命令行编译。一个简单的构建脚本长这样:
@echo off set UV4=C:\Keil_v5\UV4\UV4.exe set PROJECT=dc_charger.uvprojx set TARGET="输出 Hex/Bin 文件路径" echo === STM32 DC Charger Build === %UV4% -b %PROJECT% -o build_log.txt if errorlevel 1 ( echo Build FAILED type build_log.txt exit /b 1 ) echo Build OK fromelf --bin --output %TARGET% .\Objects\dc_charger.axfKeil 的 UV4 命令行编译参数-b是 build 当前工程,-r是 rebuild。如果机器上装的是 Keil C51 兼容版本,要先用 Pack Installer 安装 STM32F1 系列的芯片包,否则打开工程会提示找不到设备。这个步骤卡住很多人:JLinkSettings.ini 文件存在说明调试器已经配置过,但换电脑后 SW Device 里如果显示 No STM32 target found,先别急着怀疑板子,可能是调试器固件和芯片的 debug authentication 设置不匹配,或者目标板上的 SWDIO/SWCLK 引脚被程序复用成了 GPIO。
4.2 把 SWD 调试接口锁死的恢复办法
F103 的 PA13/PA14 默认是 SWDIO/SWCLK,但很多工程师在初始化代码里把这两个脚重映射成了普通 GPIO,导致第一次下载程序后调试器就连不上了。这是项目里 stm32f10x_rcc.c 中 GPIO 配置的常见隐患。
| 引脚 | SWD 功能 | JTAG 功能 | 常见误用 |
|---|---|---|---|
| PA13 | SWDIO | JTMS | 按键输入 |
| PA14 | SWCLK | JTCK | LED 输出 |
| PA15 | — | JTDI | 电机方向控制 |
| PB3 | — | JTDO | PWM 输出 |
| PB4 | — | JTRST | 蜂鸣器 |
调试器连不上的时候,用 J-Flash 或 STM32 ST-LINK Utility 执行 connect 时的 reset 选项,选择 Hardware Reset,在芯片上电瞬间按住复位键再点连接。如果这样也不行,需要把 BOOT0 拉高进入 ISP 模式,用串口下载一段擦除程序把 Flash 清了。具体做法是把 BOOT0 跳线跳到 1,BOOT1 跳到 0,复位后芯片从系统存储器启动,此时 SWD 引脚释放,再用工具连接即可。这段经验对刚接触 STM32 的人来说是必须过的坎,对老手而言则是习惯性的预防检查点。
更稳妥的做法是在工程初始化里显式关闭 JTAG,只保留 SWD:
void GPIO_Config_SWD(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 关闭 JTAG 复用功能,释放 PA15/PB3/PB4 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // PA13/PA14 保持 SWD 功能,不要做 GPIO 初始化 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); }GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)只关闭 JTAG,SWD 不受影响。用这个函数之后,PA15、PB3、PB4 才能当普通 IO 用,但 PA13/PA14 仍然留给调试器。调试环境里如果开了 RTT 或者其他实时跟踪功能,注意别在低功耗模式下唤醒时重新初始化 GPIO,那会短暂拉低 SWCLK 导致调试器掉线。
4.3 mcuviewer 和串口打印的配合
F103 的调试手法之一是用 SEGGER RTT 而不是串口打印。RTT 走 J-Link 的 SWD 接口,不占用 UART 引脚,输出速度远高于串口,在调试 PID 波形和充电策略状态机时非常有用。用 J-Link RTT Viewer 或 MCUViewer 查看变量,把采样值以文本流形式输出,比 LCD 上翻页效率高得多。串口打印用于现场问题定位,RTT 用于开发阶段的实时观测,两者分工明确。注意 stm32 的串口调试如果发现 delay 函数卡死,先查 SysTick 中断优先级,如果被 uIP 的临界区关掉了,delay 就永远等不到中断,这是 F103 上非常隐蔽的时序坑。
5. 固件升级:fw_update.bin 的 Flash 分区与 IAP 流程
5.1 Flash 分区怎么划
直流充电桩部署在现场,远程升级固件是刚需。基于 STM32 的升级方案核心是 IAP:Bootloader 程序放在 Flash 起始地址,App 程序放在后面的分区,通过通信接口接收新固件写入 App 分区。fw_update.bin 就是编译好的 App 固件,Bin 格式直接可烧写,体积比 Hex 小,适合走以太网传输。
F103 的 Flash 是 256 KB 到 512 KB 不等,以 512 KB 为例,分区可以这样设计:
| 分区 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 32 KB | 上电引导、升级入口 |
| App 主程序 | 0x08008000 | 448 KB | 充电桩业务逻辑 |
| 参数区 | 0x0807F000 | 4 KB | 充电参数、校准数据 |
App 工程需要改两个地方:链接脚本里 ROM 起始地址改为 0x08008000,中断向量表偏移在 SystemInit 后设置SCB->VTOR = 0x08008000。烧录方式和普通程序不同,先用 J-Link 烧 Bootloader,然后通过以太网或串口把 fw_update.bin 传给 Bootloader。调试时用 J-Flash 读取 Flash 内容,先确认 App 分区的起始字节是栈顶地址,和向量表偏移值对上,才能保证跳转不跑飞。
5.2 升级流程的状态机怎么写
Bootloader 的升级流程本质上是个状态机:接收帧、校验帧、写入 Flash、跳转。每个数据帧可以定义成 1024 字节数据块加上头部信息,头部包含帧序号和数据长度。收到完整帧后做 CRC32 校验,通过后执行 Flash 擦写。
typedef struct { uint32_t total_len; // 固件总长度 uint32_t crc32; // 固件 CRC32 校验值 uint32_t block_size; // 数据块大小 uint32_t blocks; // 数据块总数 } fw_header_t; void Bootloader_ProcessFrame(uint8_t *buf, uint16_t len) { fw_header_t *hdr = (fw_header_t *)buf; if (len < sizeof(fw_header_t)) return; if (hdr->total_len > APP_MAX_SIZE) { // 固件大小超限,拒绝升级 return; } FLASH_Unlock(); FLASH_ErasePage(APP_BASE_ADDR); // 擦除第一个页 // 接收完所有数据块后写入 for (uint32_t i = 0; i < hdr->blocks; i++) { uint8_t *block = GetNextBlock(); FLASH_ProgramWord(APP_BASE_ADDR + i * hdr->block_size, *(uint32_t *)block); } FLASH_Lock(); }这段是示意代码,实际上每个 1024 字节块需要单独擦写页,F103 的页大小是 1 KB 或 2 KB,具体看型号。写 Flash 前必须FLASH_Unlock(),写完后FLASH_Lock(),中途不能断电,否则 Brick。更稳妥的做法是双备份:先写到参数区后的备份区,全部校验通过后再拷贝到 App 区,这样升级失败还能回滚到旧版本。
5.3 验证升级结果的方法
固件升级完成后,Bootloader 跳转到 App,第一件事是读SCB->VTOR确认向量表偏移生效。然后在 App 的初始化里把版本号写入参数区的一个固定地址,上位机通过 Modbus 寄存器 0x0100 读取版本号,如果版本号和 fw_update.bin 的 build 号一致,说明升级成功。如果跳转后程序跑飞,大概率是中断向量表没有重映射,或者 App 编译时使用的 Flash 起始地址没有在链接脚本里同步修改。用 J-Flash 读回 Flash 内容做逐字节对比是最可靠的验证手段,确认 bin 文件每一字节都落在正确地址上,再结合上位机的版本号回读,整个升级链路才算闭环。
本文还有配套的精品资源,点击获取