APP代码移植(1)
197.2.5 APP代码移植(1)
/*
* 本课目标:把 04Can_DHT11_OLED 复制为 10Can_DHT11_OLED_IAP,并移入升级触发功能。
* 参考工程:09Ymodem_APP。
* 本课边界:完成 USART1 中断、串口协议、内部 Flash 模块及 main.c 业务代码的移植;
* APP 链接地址、向量表和 APP.bin 生成命令留到第 198 课处理。
* 代码说明:以下均为本课画面和配套工程中实际出现的代码片段;没有补写后续课程代码。
*/
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); /* CubeMX 生成:把 USART1 中断的抢占优先级和子优先级都设为 0。 */
HAL_NVIC_EnableIRQ(USART1_IRQn); /* CubeMX 生成:在 NVIC 中使能 USART1 全局中断通道。 */
void USART1_IRQHandler(void) /* CubeMX 在 stm32f1xx_it.c 中生成 USART1 中断入口。 */
{ /* USART1 中断服务函数开始。 */
HAL_UART_IRQHandler(&huart1); /* 交给 HAL 分析中断状态并推进当前 USART1 收发过程。 */
} /* USART1 中断服务函数结束。 */
typedef struct{ /* 定义课程使用的串口接收状态结构体。 */
uint8_t rx_buf[50]; /* 保存一帧升级命令,容量为 50 字节。 */
uint8_t rx_cnt; /* 记录当前已经收到多少个字节。 */
int8_t rx_flag; /* 保留的接收状态字段,本课协议代码没有实际使用它。 */
}UART_RX_typeDef; /* 为该结构体定义课程源码中的类型名。 */
void uart_receive_enable(); /* usart.h:声明启动 USART1 单字节中断接收的函数。 */
uint8_t uart_get_ota_flag(); /* usart.h:声明读取 RAM 升级标志的函数。 */
static uint8_t ota_flag = 0; /* usart.c:RAM 升级标志,上电初始化为 0。 */
static uint8_t uart1_rec_byte = 0; /* usart.c:保存 USART1 本次中断收到的一个字节。 */
static UART_RX_typeDef uart1_rx_data; /* usart.c:保存升级命令缓冲区及接收进度。 */
void uart_receive_enable() /* 封装 USART1 中断接收的首次启动操作。 */
{ /* 串口接收启动函数开始。 */
HAL_UART_Receive_IT(&huart1, (uint8_t*)&uart1_rec_byte, 1); /* 要求 HAL 用中断方式接收 1 字节。 */
} /* 串口接收启动函数结束。 */
uint8_t uart_get_ota_flag() /* 向 main.c 提供读取 RAM 升级标志的接口。 */
{ /* 升级标志读取函数开始。 */
return ota_flag; /* 返回回调函数设置的 RAM 标志。 */
} /* 升级标志读取函数结束。 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) /* HAL 收满请求的 1 字节后调用此回调。 */
{ /* 串口接收完成回调开始。 */
if(huart ->Instance == USART1) /* 确认本次回调来自 USART1。 */
{ /* USART1 接收分支开始。 */
uart1_rx_data.rx_buf[uart1_rx_data.rx_cnt] = uart1_rec_byte; /* 把刚收到的字节追加到帧缓冲区。 */
uart1_rx_data.rx_cnt ++; /* 接收计数加 1,指向下一个待写位置。 */
if(uart1_rx_data.rx_buf[0] == 0xAF) /* 检查一帧的第一个字节是否为帧头 0xAF。 */
{ /* 帧头正确分支开始。 */
if(uart1_rx_data.rx_cnt >= 48) /* 达到课程设定的接收上限时认为数据过长。 */
{ /* 数据过长分支开始。 */
uart1_rx_data.rx_cnt = 0; /* 丢弃当前帧并从缓冲区起点重新接收。 */
printf("ota data len error\r\n"); /* 打印长度错误信息。 */
} /* 数据过长分支结束。 */
else /* 当前接收长度尚未达到上限。 */
{ /* 正常长度分支开始。 */
if(uart1_rx_data.rx_cnt > 2) /* 收到帧头、长度及至少一个后续字节后再解析。 */
{ /* 基础字段已收到分支开始。 */
uint8_t data_len = uart1_rx_data.rx_buf[1]; /* 读取第 2 字节声明的 DATA 长度。 */
if((data_len > 0) && (data_len < 50)) /* 检查课程代码规定的长度范围。 */
{ /* 长度字段通过检查。 */
if(uart1_rx_data.rx_cnt >= (data_len + 3)) /* 完整帧长度等于 DATA 长度再加 3 字节。 */
{ /* 当前缓冲区已包含一整帧。 */
if(uart1_rx_data.rx_buf[data_len + 2] == 0xFA) /* 检查 DATA 后是否为帧尾 0xFA。 */
{ /* 帧尾正确,课程代码认为升级命令有效。 */
ota_flag = 1; /* 把 RAM 升级标志置 1,通知主循环切换到 Bootloader。 */
uart1_rx_data.rx_cnt = 0; /* 本帧处理完成,清零接收计数。 */
printf("OTA FLAG SET, VER:%d.%d.%d\r\n",uart1_rx_data.rx_buf[2],uart1_rx_data.rx_buf[3],uart1_rx_data.rx_buf[4]); /* 打印 DATA 前三字节表示的版本号。 */
} /* 正确帧尾分支结束。 */
else /* 指定位置不是帧尾 0xFA。 */
{ /* 帧尾错误分支开始。 */
uart1_rx_data.rx_cnt = 0; /* 丢弃当前错误帧。 */
printf("ota data tail error\r\n"); /* 打印帧尾错误信息。 */
} /* 帧尾错误分支结束。 */
} /* 完整帧判断结束。 */
} /* 合法长度字段分支结束。 */
else /* DATA 长度为 0 或不小于 50。 */
{ /* 长度字段错误分支开始。 */
uart1_rx_data.rx_cnt = 0; /* 丢弃当前错误帧。 */
printf("ota data len error\r\n"); /* 打印长度错误信息。 */
} /* 长度字段错误分支结束。 */
} /* 基础字段解析结束。 */
} /* 正常长度分支结束。 */
} /* 帧头正确分支结束。 */
else /* 缓冲区第一个字节不是 0xAF。 */
{ /* 错误帧头分支开始。 */
uart1_rx_data.rx_cnt = 0; /* 丢弃该字节,让下一字节重新作为帧头。 */
} /* 错误帧头分支结束。 */
HAL_UART_Receive_IT(&huart1, &uart1_rec_byte, 1); /* 再次挂起 1 字节接收,形成连续接收链。 */
} /* USART1 接收分支结束。 */
} /* 串口接收完成回调结束。 */
#define INTER_FLASH_PARAM_ADDR (0x8006000U) /* inter_flashif.h:参数区地址,等价于 0x08006000。 */
#define INTER_FLASH_APP_ADDR (0x8008000U) /* inter_flashif.h:APP 地址,等价于 0x08008000。 */
#pragma pack(1) /* inter_flash_cfg.h:按 1 字节边界排列参数成员。 */
typedef struct { /* 定义 APP 和 Bootloader 共同使用的 Flash 参数格式。 */
uint8_t magic[4]; /* 4 字节魔数,课程约定为 AA BB CC DD。 */
uint32_t ota_bin_version; /* 保存待升级固件版本信息。 */
uint8_t ota_flag; /* Flash 升级标志:0 不升级,1 请求升级。 */
uint8_t checksum; /* 保存有效字段的 8 位累加和。 */
uint8_t format[2]; /* 2 字节占位,使结构体总长保持为 12 字节。 */
}inter_flash_cfg_param_typeDef; /* 定义课程使用的 Flash 参数类型名。 */
#pragma pack() /* 恢复编译器默认的结构体对齐设置。 */
uint8_t inter_flash_cfg_init(void); /* inter_flash_cfg.h:声明参数区初始化与校验函数。 */
int8_t inter_flash_cfg_get_app_update_flag(); /* inter_flash_cfg.h:声明读取 Flash 升级标志的函数。 */
uint8_t inter_flash_cfg_set_app_update_flag(uint8_t flag); /* inter_flash_cfg.h:声明写 Flash 升级标志的函数。 */
#include "inter_flashif.h" /* main.c:引入内部 Flash 底层读写接口。 */
#include "inter_flash_cfg.h" /* main.c:引入参数结构和升级标志接口。 */
#include <stdio.h> /* main.c:引入标准格式化输出声明;课程源码保留此行。 */
__set_PRIMASK(0); /* main.c:清除全局中断屏蔽,使 APP 可以响应可屏蔽中断。 */
uart_receive_enable(); /* main.c:外设初始化完成后启动 USART1 单字节中断接收。 */
Can_Filter_Init(); /* main.c:保留原工程的 CAN 过滤器初始化。 */
printf(" APP CAN Start\r\n"); /* main.c:打印新 APP 的启动标识。 */
printf("----------------------------------------------------------------\r\n"); /* main.c:打印分隔线。 */
printf("flash cfg size:%d\r\n", sizeof(inter_flash_cfg_param_typeDef)); /* main.c:打印参数结构体长度,当前应为 12。 */
printf("----------------------------------------------------------------\r\n"); /* main.c:再次打印分隔线。 */
inter_flash_cfg_init(); /* main.c:读取并校验 0x08006000 的参数。 */
#if ZET6 /* main.c:接收端宏为真时启用 OLED 显示。 */
OLED_Init(); /* main.c:先初始化 OLED 控制器和通信接口。 */
OLED_Clear(); /* main.c:初始化后再清除屏幕旧内容。 */
OLED_ShowString(22, 0, "APP Can Test",16); /* main.c:最后显示新 APP 的辨识文字。 */
#endif /* main.c:结束 ZET6 条件编译区域。 */
if(uart_get_ota_flag() == 1) /* main.c 的 while(1):轮询串口回调设置的 RAM 标志。 */
{ /* 收到升级命令后的处理分支开始。 */
inter_flash_cfg_set_app_update_flag(1); /* 把内部 Flash 中可跨复位保存的升级标志写为 1。 */
printf("Restarting...\r\n"); /* 输出即将复位的提示。 */
HAL_Delay(1000); /* 延时 1 秒,保留课程源码的复位前等待。 */
HAL_NVIC_SystemReset(); /* 请求系统复位,让位于 Flash 起始处的 Bootloader 重新运行。 */
} /* 升级命令处理分支结束;其后仍执行原工程的 CAN/OLED 业务。 */
本课不是从空白工程重新编写 IAP,而是演示怎样把已经验证过的升级触发模块移入一个普通业务工程。普通工程继续负责 CAN 接收、DHT11 与 OLED 显示;新增代码只负责接收升级指令、保存升级请求和复位。
一、移植APP代码
1.1 任务资源
本课涉及三个工程,角色不能混淆:
| 工程 | 角色 | 本课用途 |
|---|---|---|
04Can_DHT11_OLED | 原始普通业务工程 | 被复制的移植起点 |
09Ymodem_APP | 已能触发 IAP 的参考 APP | 提供 USART1 和内部 Flash 相关代码 |
10Can_DHT11_OLED_IAP | 新目标工程 | 接收本课的移植结果 |
视频先复制第 4 个工程,再把新文件夹命名为 10Can_DHT11_OLED_IAP。原笔记中的“IP 后缀”是识别错误,应为 IAP。文件夹虽然改名,Keil 工程和 target 在画面中仍叫 01Can_Test,这不妨碍编译,但日后维护时最好让工程名也能反映实际用途。
思维导图给出的完整移植流程共有 7 个大步骤:
- 用 CubeMX 初始化带中断的 USART1;
- 移植
usart.c、usart.h中的升级命令接收代码; - 加入两个内部 Flash
.c文件; - 加入两个内部 Flash
.h文件及头文件搜索路径; - 修改
main.c; - 修改 Keil 中的 APP 地址、向量表等设置;
- 生成可用于升级的 BIN 文件。
第 197 课只完成第 1~5 步。 第 6、7 步是第 198 课内容,因此本课结束时“代码能编译”还不等于它已经能被 Bootloader 正确加载和运行。
1.2 CubeMX初始化串口1
原工程已经配置 USART1,但没有打开 USART1 全局中断。视频中的正确操作是:
- 打开新复制出来的第 10 个工程中的
.ioc,不要误改第 4 或第 9 个工程; - 进入 USART1 配置,确认原有串口基本参数;
- 打开
NVIC Settings; - 勾选
USART1 global interrupt的Enabled; - 生成代码,关闭 CubeMX;
- 回到 Keil 时同意重新加载被更新的工程文件。
配套工程的 .ioc 对比能够看到新增项:
NVIC.USART1_IRQn=true:0:0:false:false:true:true:true:true
CubeMX 同时生成三处关键连接:
HAL_NVIC_EnableIRQ(USART1_IRQn)
↓
USART1_IRQHandler()
↓
HAL_UART_IRQHandler(&huart1)
勾选 NVIC 只负责让处理器可以进入 USART1 中断入口;它不会自动开始接收数据。真正启动接收的是后面调用的 HAL_UART_Receive_IT()。ST 对 CubeMX 的定位是“外设配置和初始化 C 代码生成工具”,NVIC 页面可以使能中断并配置优先级,参见 STM32CubeMX 用户手册 UM1718。
1.3 添加串口代码
原笔记中的 UART.c、UTTHH、KB_MX 都是转录错误,正确名称分别是:
usart.c:USART1 初始化以及本课移入的接收实现;usart.h:接收结构体、函数声明等公共接口;STM32CubeMX:根据.ioc生成初始化代码的工具。
参考 APP 在 CubeMX 生成的串口代码之外,又增加了三部分用户代码:
ota_flag、单字节变量和帧缓冲区等状态;uart_receive_enable()与uart_get_ota_flag();HAL_UART_RxCpltCallback()中的升级命令组帧逻辑。
这些新增内容位于 CubeMX 的 USER CODE 区域中,正常重新生成代码时应被保留。
1.4 打开APP工程
视频打开第 9 个工程 09Ymodem_APP作为参考。它不是最终目标工程,而是已经跑通过上一课流程的“代码来源”。查看它的 usart.c可以得到完整调用关系:
main.c 调用 uart_receive_enable()
→ HAL 准备接收 1 字节并打开所需接收中断
→ USART1 收到字节
→ USART1_IRQHandler()
→ HAL_UART_IRQHandler()
→ 收满 1 字节后调用 HAL_UART_RxCpltCallback()
→ 回调保存字节、解析帧并再次启动下一字节接收
“回调函数由中断触发”不等于硬件会直接调用用户回调。中间还经过芯片中断入口和 HAL 通用中断处理函数。
1.5 接收升级指令
本课沿用第 196 课的串口帧格式:
┌────────┬────────┬──────────────┬────────┐
│ 0xAF │ LEN │ DATA │ 0xFA │
│ 帧头 │ 数据长 │ LEN 个字节 │ 帧尾 │
└────────┴────────┴──────────────┴────────┘
当回调收到形式符合要求的帧时,它把 RAM 中的 ota_flag置为 1。main.c的主循环通过 uart_get_ota_flag()查询这个值,再把内部 Flash 参数区中的持久化升级标志写为 1并复位。
原转录中的“窗口一”“进入 IP 升级模式”分别应为“串口一”和“进入 IAP 升级模式”。这里收到的是升级控制命令,不是经 USART1 直接接收整个新固件;复位后,Bootloader 才经其固件传输通道执行真正升级。
1.6 添加USART1文件
除了复制函数,还必须复制 usart.c顶部使用的状态变量,以及 usart.h中的UART_RX_typeDef和函数声明。只复制回调正文会导致类型、变量或函数原型缺失。
需要迁移的用户层对象包括:
| 对象 | 所在文件 | 作用 |
|---|---|---|
UART_RX_typeDef | usart.h | 定义 50 字节帧缓冲区和计数器 |
ota_flag | usart.c | 在 RAM 中记录是否收到升级命令 |
uart1_rec_byte | usart.c | 暂存当前收到的 1 字节 |
uart1_rx_data | usart.c | 保存正在组装的完整帧 |
uart_receive_enable() | usart.c/.h | 启动第一次中断接收 |
uart_get_ota_flag() | usart.c/.h | 供主循环读取 RAM 标志 |
HAL_UART_RxCpltCallback() | usart.c | 逐字节组帧并设置标志 |
1.7 复制并粘贴USART1文件
视频采用的快捷方法是:把第 9 个工程的 usart.c和usart.h分别全选复制,替换第 10 个工程的同名文件。这个做法在本课可行,是因为两个工程都只使用同一套 USART1 配置:115200波特率、8 数据位、1 停止位、无校验、收发模式,GPIO 为 PA9=TX、PA10=RX。
它不是通用移植方法。若目标工程还有 USART2/USART3、DMA、其他回调、自定义引脚或不同波特率,整文件覆盖会把目标工程原有配置和业务代码一起删除。更安全的做法是比较差异,只把 USER CODE区域中的变量、类型、函数和回调逻辑逐项移入。
视频完成替换后进行编译,画面显示的是 0 errors、17 warnings。因此可以说“当前能够链接”,不能说“代码没有任何问题”。警告至少反映了旧式空参数列表、无符号返回类型返回 -1以及部分非 void函数缺少返回值等问题,后续做产品代码时不能忽略。
1.8 添加两个flash头文件
第 9 个工程的 inter_flashif目录实际包含 4 个文件:
inter_flashif/
├─ inter_flashif.c 底层 Flash 读取、擦除、写入和累加和
├─ inter_flashif.h 参数区与 APP 地址、底层函数声明
├─ inter_flash_cfg.c 参数读取、校验、升级标志修改
└─ inter_flash_cfg.h 12 字节参数结构体及配置层接口
正确的 Keil 工程操作分为两个概念:
- 新建
inter_flashif代码组,把两个.c文件加入工程,使编译器真正编译它们; - 在
Options for Target → C/C++ → Include Paths中加入..\inter_flashif,使#include "inter_flashif.h"和#include "inter_flash_cfg.h"能被找到。
两个 .h文件不一定要显示在 Keil 工程树里才能生效;决定 #include能否找到它们的是头文件搜索路径。原笔记把“加入工程组”和“加入头文件路径”混成了同一件事。
复制模块而不是重新手敲代码还有一个关键原因:APP 和 Bootloader 必须使用相同的参数地址、结构体字段顺序、对齐规则、magic 和 checksum 算法。只要任意一项不一致,两边就会用不同方式解释 0x08006000中的同一组字节。
1.9 更改main.c
视频按思维导图在 main.c完成 5 项修改。放置位置比画面中的绝对行号更重要,因为重新生成代码或增加内容后行号会改变。
1.9.1 添加头文件
在 /* USER CODE BEGIN Includes */与/* USER CODE END Includes */之间加入:
#include "inter_flashif.h"
#include "inter_flash_cfg.h"
#include <stdio.h>
前两个头文件分别提供 Flash 底层接口和参数结构接口。目标工程的 usart.h本身已经包含 <stdio.h>并定义 printf宏,所以当前归档工程在 main.c再次包含 <stdio.h>属于重复但无害;顶部代码仍按课程源码原样保留。
1.9.2 添加打开所有终端
原笔记中的“打开所有终端”“enable_irq()”都是识别错误。课程实际复制的是:
__set_PRIMASK(0);
它位于 SystemClock_Config()之后、各外设初始化之前的 USER CODE BEGIN SysInit区域。其准确含义是把 Cortex-M3 的 PRIMASK清为 0,解除对可配置优先级异常的全局屏蔽,而不是“初始化所有中断”。
本工程通常由 Bootloader 跳转进入,而配套 Bootloader 跳转前执行过全局关中断,所以 APP 必须恢复全局中断。USART1 真正能进入接收回调还需要另外两个条件:CubeMX 已使能 USART1_IRQn,并且 HAL_UART_Receive_IT()已经打开本次接收所需的外设中断。三者缺一不可。
1.9.3 串口一中断接收
在 MX_USART1_UART_Init()等外设初始化完成之后调用:
uart_receive_enable();
该函数只请求接收 1 字节。收到后,回调末尾再次请求下一个字节,所以它形成的是“1 字节完成 → 回调处理 → 再接收 1 字节”的连续链路。如果忘记回调末尾的再次调用,程序只会收到第一个字节,永远拼不出完整升级命令。
1.9.4 flash初始化
视频加入了启动日志、结构体大小打印和inter_flash_cfg_init()。参数区正确地址是 0x08006000,读取长度是 12 字节;转录中的“0x4000 630”是语音识别错误。
初始化函数完成两项检查:
- 参数结构体长度是否能按 32 位整字写回;
- 参数区的
AA BB CC DD魔数及累加和是否正确。
任一检查失败,课程源码都会停在错误循环中,因此首次运行前仍必须保证参数区已经烧入合法的 info.bin。打印 APP CAN Start和flash cfg size:12有助于判断当前运行的是新 APP,以及参数结构是否与 Bootloader 一致。
1.9.5 添加升级控制代码
升级控制 if被放在 while(1)开头,后面原有的 CAN 接收和 OLED 数据显示逻辑保持不变。执行顺序为:
查询 RAM ota_flag
├─ 不是 1:继续执行原 CAN/OLED 业务
└─ 等于 1:写 Flash ota_flag=1
↓
打印 Restarting...
↓
请求系统复位
视频还把 OLED 固定标题改成 APP Can Test,用于直观看出当前固件是移植后的 APP。讲解中一度准备把显示调用放在初始化前,随后立即纠正;最终正确顺序是:
OLED_Init():配置 OLED;OLED_Clear():清除显存和旧画面;OLED_ShowString():写入要显示的文字。
未初始化就发送显示命令,OLED 通信接口和控制器状态可能尚未就绪,不能因为函数在 C 语法上可以调用就认为硬件一定能正确响应。
二、中断接收为什么需要三层使能
对初学者来说,“已经勾选中断但回调不运行”很常见。本课的 USART1 接收链路有三道门:
| 层级 | 本课代码 | 缺失后的现象 |
|---|---|---|
| Cortex-M 全局屏蔽 | __set_PRIMASK(0) | 可屏蔽中断整体不能被 CPU 响应 |
| NVIC 通道 | HAL_NVIC_EnableIRQ(USART1_IRQn) | USART1 请求到不了中断服务函数 |
| UART 接收过程 | HAL_UART_Receive_IT(..., 1) | 没有接收缓冲和长度,也不会因收到 1 字节完成而回调 |
因此排查时不要只看一个“中断开关”,应依次确认全局状态、NVIC 通道、外设接收状态和中断入口是否都正确。
三、USART1硬件连接
配套源码把 USART1 配置为:
PA9:USART1_TX,单片机发送;PA10:USART1_RX,单片机接收;115200波特率、8 数据位、1 停止位、无校验、无硬件流控。
若使用外接 USB 转串口模块,信号必须交叉连接:
STM32 PA9 (TX) ─────────► USB-TTL RX
STM32 PA10 (RX) ◄───────── USB-TTL TX
STM32 GND ────────── USB-TTL GND
双方必须共地,并使用与 STM32 兼容的 3.3 V UART 逻辑电平。若开发板和串口模块各自供电,不要在不了解板上电源结构时再随意连接两个 VCC。串口只有 TX/RX 而不共地时,接收端缺少共同电压参考,数据可能乱码或完全无法接收。
四、本课结束时还不能做什么
本课结束时只是把升级触发逻辑移入业务工程。若 APP 仍按普通工程链接在 0x08000000,它会与 Bootloader 占用同一地址;若中断向量表仍指向原位置,即使代码跳到了 APP,USART、SysTick 等中断也可能进入错误入口。
因此此时只验证“工程结构和 C 代码能编译”,不要提前宣称 IAP 已经完成。第 198 课还必须处理:
- APP ROM 起始地址和可用大小;
SCB->VTOR中断向量表地址;- 编译宏;
- 编译后生成供 YMODEM 传输的纯 BIN 文件。
五、学习检查
完成本课后,应能回答:
- 第 4、第 9、第 10 个工程在移植中分别承担什么角色?
- 为什么只勾选
USART1 global interrupt还不能收到完整升级命令? - 为什么本课可以整文件替换
usart.c/.h,其他工程却未必可以? - 两个 Flash
.c文件和两个.h文件应分别怎样加入 Keil 工程? __set_PRIMASK(0)与HAL_NVIC_EnableIRQ()有什么区别?- 为什么
uart_receive_enable()必须放在 USART1 初始化之后? - 为什么 OLED 显示必须遵循“初始化 → 清屏 → 显示”的顺序?
- 为什么本课编译成功仍不能证明 APP 已能与 Bootloader 正确配合?