UART 升级传输
UART 升级传输是杰理 AC63 系列 MCU(AC632N/AC635N/AC636N/AC638N)在双备份(Dual Bank)固件升级架构下,通过 UART 串口通道完成固件升级包传输的完整机制。本文档基于 sdk/bsp/dual_bank_update_uart.c 及其头文件,详细说明协议帧格式、命令集、接收状态机、重传机制以及与应用主循环的集成方式。
Purpose and Scope
本文档覆盖以下内容:
- UART 升级传输的协议层设计:帧同步头、长度字段、CRC16 校验的完整帧格式。
- 命令集:设备端与升级上位机(Host)之间交互的全部命令定义。
- 接收状态机:
uart_loop_rx_handler如何逐字节解析数据流并完成校验。 - 升级流程:从启动初始化、波特率切换、数据块请求重传,到升级完成后的复位。
- 文件操作抽象:
update_op_api_t接口(f_open/f_read/f_seek/f_stop)如何将 UART 通道伪装成升级引擎眼中的"文件"。 - 应用集成:
main.c中user_main与user_msg_handler对升级模块的挂载方式。
以下内容属于其他目录页的范畴,本文档不展开:
- 双备份 Flash 擦写、启动信息(boot_info)管理的核心实现,请参阅 "Dual Bank 升级引擎"(
sdk/bsp/dual_bank_update.c、dual_bank_update_loop.h、dual_bank_updata_api.h)。 - UART 底层驱动(寄存器配置、DMA、波特率设置),请参阅 BSP 的
uart.c/debug_uart.c。
Overview
在双备份升级架构中,设备侧固件(Boot 区)需要从某个"通道"读取升级包数据并写入非活动备份区。通道可以是 USB、SD 卡、BLE 或 UART。UART 升级传输模块即为其中的 UART 通道实现:它把一条串口链路封装为"只读文件"(f_read/f_seek),供升级引擎 active_update() 以统一的方式逐块拉取固件数据。
其核心设计思路是**"设备主动拉取 + CRC 校验重传"**:
- 上位机(Host,通常是 PC 工具或另一颗 MCU)先通过 UART 发送同步帧握手。
- 设备收到握手后切换波特率,并启动升级引擎。
- 升级引擎通过
uart_f_read()发出CMD_UPDATE_READ(携带地址addr和长度len),Host 收到后回传对应数据块。 - 设备对每个数据块做 CRC16 校验,失败则重发请求,最多重试 4 次(
RETRY_TIME)。 - 全部数据写入并校验完成后,升级引擎回调
uart_update_state_cbk,设备关闭 UART 并执行cpu_reset()重启进入新固件。
Architecture
flowchart TD
subgraph sg_Host["上位机 (Host / 升级工具)"]
Host["PC 工具或主控 MCU<br/>发送固件数据块"]
end
subgraph sg_App["应用层 (sdk/apps/main.c)"]
UserMain["user_main()"]
MsgHandler["user_msg_handler()"]
end
subgraph sg_UartCh["UART 升级传输通道 (dual_bank_update_uart.c)"]
UartInit["dual_bank_update_init()<br/>uart_init + RX IRQ 注册"]
RxHandler["uart_loop_rx_handler()<br/>帧同步 / CRC16 校验"]
Dispatch["update_download_opt()<br/>命令分发"]
CmdProc["uart_update_recv()<br/>CMD_UPDATE_START 波特率切换"]
UartOp["uart_ch_update_op<br/>(f_open / f_read / f_seek / f_stop)"]
StateCbk["uart_update_state_cbk()<br/>升级结束处理"]
end
subgraph sg_Engine["双备份升级引擎 (dual_bank_update.c)"]
ActiveUpdate["active_update()<br/>状态机 START/DATA/VERIFY"]
WriteFlash["写入非活动备份区 + 校验"]
end
Host -->|"同步帧 0xAA 0x55 + CRC16"| RxHandler
UserMain -->|"DUAL_BANK_UPDATE_BY_UFW 使能时调用"| UartInit
UartInit -->|"注册 RX IRQ"| RxHandler
MsgHandler -->|"轮询调用"| Dispatch
RxHandler -->|"整帧校验通过后置 RECV_DATA 标志"| Dispatch
Dispatch -->|"CMD_UART_UPDATE_READY / START"| CmdProc
CmdProc -->|"active_update(info)"| ActiveUpdate
ActiveUpdate -->|"f_read / f_seek"| UartOp
UartOp -->|"CMD_UPDATE_READ 请求 + 等待响应"| Host
Host -->|"数据块 (带 CRC16)"| UartOp
ActiveUpdate -->|"UPDATE_CH_EXIT 回调"| StateCbk
StateCbk -->|"成功则 cpu_reset()"| UserMain
ActiveUpdate --> WriteFlash
架构要点说明:
dual_bank_update_init()是传输通道的入口,负责打开 UART 设备并将uart_loop_rx_handler注册为接收中断回调(dual_bank_update_uart.c#L283-L293)。uart_ch_update_op是update_op_api_t接口的具体实现(dual_bank_update_uart.c#L221-L227),升级引擎只依赖这组回调,不关心底层通道是 UART 还是其他介质——这是典型的策略模式解耦。update_download_opt()在应用主循环中轮询执行,只有当接收标志为CMD_UART_UPDATE_FLAG_RECV_DATA时才分发命令,因此握手帧的处理是"非阻塞、事件驱动"的。
协议帧格式
UART 升级协议采用自定义的定长数据帧 + CRC16 校验结构。帧定义见头文件中的 protocal_frame_t 联合体(dual_bank_update_uart.h#L13-L26):
#define PROTOCAL_SIZE 528
#define SYNC_SIZE 6
#define SYNC_MARK0 0xAA
#define SYNC_MARK1 0x55
typedef union {
u8 raw_data[PROTOCAL_SIZE + SYNC_SIZE];
struct {
u8 mark0;
u8 mark1;
u16 length;
u8 data[PROTOCAL_SIZE + 2]; // 最后CRC16
} data;
} protocal_frame_t;
Source: dual_bank_update_uart.h
帧布局
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | mark0 | 1 字节 | 同步头 1,固定 0xAA |
| 1 | mark1 | 1 字节 | 同步头 2,固定 0x55 |
| 2 | length | 2 字节(小端) | 数据域长度(不含同步头,不含 CRC) |
| 4 | data[] | length 字节 | 负载:1 字节命令 + 命令参数 |
| 4 + length | CRC16 | 2 字节 | 对 同步头 + length + data 整体计算的 CRC16 |
单帧最大负载为 PROTOCAL_SIZE(528 字节),因此整帧最大为 534 字节(PROTOCAL_SIZE + SYNC_SIZE)。发送端封帧逻辑如下(dual_bank_update_uart.c#L45-L62):
static bool uart_send_packet(u8 *buf, u16 length)
{
bool ret = TRUE;
u16 crc;
u8 *buffer;
buffer = (u8 *)&protocal_frame;
protocal_frame.data.mark0 = SYNC_MARK0;
protocal_frame.data.mark1 = SYNC_MARK1;
protocal_frame.data.length = length;
memcpy((char *)&buffer[4], buf, length);
crc = CRC16(buffer, length + SYNC_SIZE - 2);
memcpy(buffer + 4 + length, &crc, 2);
log_debug("tx(%d)", length + SYNC_SIZE);
log_debug_hexdump((u8 *)&protocal_frame, length + SYNC_SIZE);
uart_tx_buf(dual_uart_config.id, (u8 *)&protocal_frame, length + SYNC_SIZE);
return ret;
}
Source: dual_bank_update_uart.c
CRC 计算范围覆盖同步头与长度字段(length + SYNC_SIZE - 2 字节),即除 CRC 自身外的所有字节,发送时通过 uart_tx_buf 一次性发出,保证帧在链路层不被拆分。
命令集
设备端(发送方)命令
设备向上位机发出的命令定义在实现文件顶部(dual_bank_update_uart.c#L5-L10):
// 命令
#define CMD_UPDATE_START 0x01
#define CMD_UPDATE_READ 0x02
#define CMD_UPDATE_END 0x03
#define CMD_SEND_UPDATE_LEN 0x04
#define CMD_KEEP_ALIVE 0x05
Source: dual_bank_update_uart.c
| 命令 | 值 | 方向 | 载荷 | 用途 |
|---|---|---|---|---|
CMD_UPDATE_START | 0x01 | 设备→Host | 4 字节波特率(u32) | 应答握手 / 通知 Host 已切换波特率 |
CMD_UPDATE_READ | 0x02 | 设备→Host | struct file_info(cmd+addr+len,10 字节) | 请求指定偏移、指定长度的固件数据块 |
CMD_UPDATE_END | 0x03 | 设备→Host | 1 字节错误码 | 升级中止,携带失败原因 |
CMD_SEND_UPDATE_LEN | 0x04 | 设备→Host | — | 请求 Host 上报固件总长度(预留) |
CMD_KEEP_ALIVE | 0x05 | 设备→Host | — | 保活探测(预留) |
上位机(接收方)命令
设备侧解析 Host 发来的命令,枚举定义于 dual_bank_update_uart.c#L29-L36:
enum {
CMD_UART_UPDATE_START = 0x1,
CMD_UART_UPDATE_READ,
CMD_UART_UPDATE_END,
CMD_UART_UPDATE_UPDATE_LEN,
CMD_UART_JEEP_ALIVE,
CMD_UART_UPDATE_READY,
};
Source: dual_bank_update_uart.c
| 命令 | 值 | 方向 | 载荷 | 用途 |
|---|---|---|---|---|
CMD_UART_UPDATE_START | 0x1 | Host→设备 | 4 字节目标波特率 | 发起升级并通知设备切换波特率 |
CMD_UART_UPDATE_READ | 0x2 | Host→设备 | — | 应答 CMD_UPDATE_READ 的数据帧(数据紧随命令字节) |
CMD_UART_UPDATE_END | 0x3 | Host→设备 | — | 通知升级结束(预留) |
CMD_UART_UPDATE_UPDATE_LEN | 0x4 | Host→设备 | — | 上报固件总长度(预留) |
CMD_UART_JEEP_ALIVE | 0x5 | Host→设备 | — | 保活探测(预留) |
CMD_UART_UPDATE_READY | 0x6 | Host→设备 | — | 握手就绪:设备收到后回 CMD_UPDATE_START |
接收状态标志
dual_bank_update_flag 是连接中断处理与主循环的桥梁(dual_bank_update_uart.c#L38-L43):
enum {
CMD_UART_UPDATE_FLAG_NONE,
CMD_UART_UPDATE_FLAG_RECV_DATA,
CMD_UART_UPDATE_FLAG_RETRY,
CMD_UART_UPDATE_FLAG_INIT_ERR = 0xff,
};
Source: dual_bank_update_uart.c
CMD_UART_UPDATE_FLAG_NONE(0):空闲,无待处理帧。CMD_UART_UPDATE_FLAG_RECV_DATA(1):已收到完整且 CRC 正确的帧,主循环可分发。CMD_UART_UPDATE_FLAG_RETRY(2):收到的帧 CRC 错误,需重发请求。CMD_UART_UPDATE_FLAG_INIT_ERR(0xff):UART 初始化失败,此后丢弃所有接收数据。
核心流程
接收状态机(字节级)
uart_loop_rx_handler 是 UART 接收中断回调,实现了一个逐字节推进的帧同步状态机(dual_bank_update_uart.c#L79-L118):
static void uart_loop_rx_handler(u8 *buf, u16 len)
{
u16 crc, crc0, i, ch;
for (i = 0; i < len; i++) {
ch = buf[i];
__recheck:
if (rx_cnt == 0) {
if (ch == SYNC_MARK0) { protocal_frame.raw_data[rx_cnt++] = ch; }
} else if (rx_cnt == 1) {
protocal_frame.raw_data[rx_cnt++] = ch;
if (ch != SYNC_MARK1) { rx_cnt = 0; goto __recheck; }
} else if (rx_cnt < 4) {
protocal_frame.raw_data[rx_cnt++] = ch;
} else {
protocal_frame.raw_data[rx_cnt++] = ch;
if (rx_cnt == (protocal_frame.data.length + SYNC_SIZE)) {
log_debug("rx(%d)", rx_cnt);
log_debug_hexdump((u8 *)&protocal_frame, rx_cnt);
rx_cnt = 0;
crc = CRC16(protocal_frame.raw_data, protocal_frame.data.length + SYNC_SIZE - 2);
memcpy(&crc0, &protocal_frame.raw_data[protocal_frame.data.length + SYNC_SIZE - 2], 2);
if (CMD_UART_UPDATE_FLAG_INIT_ERR != dual_bank_update_flag) {
if (crc0 == crc) {
dual_bank_update_flag = CMD_UART_UPDATE_FLAG_RECV_DATA;
} else {
dual_bank_update_flag = CMD_UART_UPDATE_FLAG_RETRY;
}
}
if (uart_update_resume_hdl && (CMD_UART_UPDATE_READ == protocal_frame.data.data[0])) {
uart_update_resume_hdl(NULL);
}
}
}
}
}
Source: dual_bank_update_uart.c
状态机四个阶段:
rx_cnt == 0(等同步头 1):只有遇到0xAA才进入下一阶段,其余字节全部丢弃——实现帧头捕获。rx_cnt == 1(校验同步头 2):若第二个字节不是0x55,回退到阶段 1 并对当前字节重新检查(goto __recheck),可处理0xAA 0xAA 0x55这类连续同步头场景。rx_cnt < 4(收长度):累计length字段(2 字节小端),据此推算整帧应接收的字节数。rx_cnt >= 4(收数据 + CRC):持续累积直至rx_cnt == length + SYNC_SIZE,随后计算 CRC16 并与帧尾携带值比对。校验通过置RECV_DATA,失败置RETRY;若初始化失败(INIT_ERR)则直接丢弃。
值得注意的是,当收到的命令是 CMD_UART_UPDATE_READ(即 Host 对设备数据请求的应答)时,会立即调用 uart_update_resume_hdl(NULL) 唤醒阻塞在 uart_dev_receive_data 中的升级引擎——这是中断上下文与升级任务之间的信号量式唤醒。
升级全流程时序
sequenceDiagram
participant H as 上位机 (Host)
participant U as UART 模块 (dual_bank_update_uart.c)
participant E as 升级引擎 (active_update)
participant F as Flash 写接口
Note over H,U: 阶段一:握手与波特率切换
H->>U: CMD_UART_UPDATE_READY (0xAA 0x55 ...)
U->>U: CRC 校验通过,置 RECV_DATA
U->>H: CMD_UPDATE_START (波特率=9600)
H->>U: CMD_UART_UPDATE_START + 新波特率(如 115200)
U->>U: uart_update_set_baud() 切换波特率
U->>E: active_update(info) 启动升级引擎
Note over U,E: 阶段二:数据拉取与写入
loop 每个数据块
E->>U: f_read() / uart_dev_receive_data()
U->>H: CMD_UPDATE_READ (addr + len)
H-->>U: 数据帧 (带 CRC16)
U->>U: CRC 校验
alt CRC 正确
U->>E: 返回数据长度,offset 递增
E->>F: 写入非活动备份区
else CRC 错误
U->>H: 重发 CMD_UPDATE_READ (最多 4 次)
end
end
Note over U,E: 阶段三:收尾
E->>F: 整包校验 (VERIFY)
E->>U: UPDATE_CH_EXIT 状态回调
U->>U: uart_close() 关闭 UART
alt 升级成功
U->>U: cpu_reset() 重启进入新固件
else 失败
U->>H: CMD_UPDATE_END + 错误码
end
数据块请求与重传
uart_dev_receive_data 实现"请求—应答—校验—重试"的核心循环(dual_bank_update_uart.c#L152-L189):
static u32 uart_dev_receive_data(void *buf, u32 relen, u32 addr)
{
u8 i;
struct file_info file_cmd;
for (i = 0; i < RETRY_TIME; i++) {
if (i > 0) { putchar('r'); }
file_cmd.cmd = CMD_UPDATE_READ;
file_cmd.addr = addr;
file_cmd.len = relen;
uart_send_packet((u8 *)&file_cmd, sizeof(file_cmd));
if (uart_update_sleep_hdl) {
if (uart_update_sleep_hdl(NULL)) {
log_error("uart_time_out\n");
return -1;
}
if (CMD_UART_UPDATE_FLAG_RETRY == dual_bank_update_flag) {
// crc错误,重发
dual_bank_update_flag = CMD_UART_UPDATE_FLAG_NONE;
continue;
}
}
memcpy(&file_cmd, protocal_frame.data.data, sizeof(file_cmd));
if ((file_cmd.cmd != CMD_UPDATE_READ) || (file_cmd.addr != addr) || (file_cmd.len != relen)) {
continue;
}
memcpy(buf, &protocal_frame.data.data[sizeof(file_cmd)], protocal_frame.data.length - sizeof(file_cmd));
return (protocal_frame.data.length - sizeof(file_cmd));
}
if (i == RETRY_TIME) {
log_error("receive data is err\n");
relen = -1;
}
putchar('R');
return relen;
}
Source: dual_bank_update_uart.c
设计要点:
- 应答校验三重比对:除 CRC 外,还校验应答帧中的
cmd、addr、len是否与请求一致,防止错位数据块被误写入 Flash。 - 超时机制:
uart_update_sleep_hdl是外部注入的等待函数(内部基于time_out_msec),返回非零表示超时,立即返回-1让升级引擎中止。 - CRC 错误重发:
RETRY标志被清除后continue重新发起请求;重试 4 次仍失败则打印错误并以-1返回。 - 串口调试时可用
putchar('r')/putchar('R')观察重传行为(小写 r = 单次重试,大写 R = 放弃)。
文件操作抽象
升级引擎通过 update_op_api_t 与通道解耦,UART 通道注册的回调表如下(dual_bank_update_uart.c#L221-L227):
static const update_op_api_t uart_ch_update_op = {
.ch_init = uart_update_hdl_register,
.f_open = uart_f_open,
.f_read = uart_f_read,
.f_seek = uart_f_seek,
.f_stop = uart_f_stop,
};
Source: dual_bank_update_uart.c
| 回调 | 实现 | 语义 |
|---|---|---|
ch_init | uart_update_hdl_register | 注入 resume/sleep 钩子,供数据请求阻塞与唤醒 |
f_open | uart_f_open | 恒返回 1(UART 无需打开语义,通道已由 dual_bank_update_init 初始化) |
f_read | uart_f_read | 按当前 uart_file_offset 请求一块数据,成功则推进偏移 |
f_seek | uart_f_seek | 支持 SEEK_SET / SEEK_CUR,用于随机访问固件数据 |
f_stop | uart_f_stop | 发送 CMD_UPDATE_END 并携带错误码,复位波特率为 9600 |
f_read / f_seek 的实现(dual_bank_update_uart.c#L191-L212):
static u16 uart_f_read(void *handle, void *buf, u32 relen)
{
u32 len;
log_info("%s\n", __func__);
len = uart_dev_receive_data(buf, relen, uart_file_offset);
if ((u32) - 1 == len) {
log_error("%s err\n", __func__);
return -1;
}
uart_file_offset += len;
return 0;
}
static int uart_f_seek(void *fp, u8 type, u32 offset)
{
if (type == SEEK_SET) {
uart_file_offset = offset;
} else if (type == SEEK_CUR) {
uart_file_offset += offset;
}
return 0;
}
Source: dual_bank_update_uart.c
uart_file_offset 是 volatile u32 全局变量,记录当前"文件"读取位置;SEEK_END 分支未实现(UART 通道无法预知固件总长),固件长度信息由升级引擎通过其他途径获取。
应用集成
UART 升级传输模块由应用主程序挂载,开关为头文件中的宏 DUAL_BANK_UPDATE_BY_UFW(dual_bank_update_uart.h#L6):
#define DUAL_BANK_UPDATE_BY_UFW 0//双备份模块宏
Source: dual_bank_update_uart.h
默认关闭(0);需要启用时由构建系统或上层配置覆盖为 1。启用后,main.c 中的两处集成生效(main.c#L8-L12、main.c#L51-L53):
static void user_msg_handler()
{
#if (DUAL_BANK_UPDATE_BY_UFW)
update_download_opt();
#endif
...
}
int user_main()
{
...
#if (DUAL_BANK_UPDATE_BY_UFW)
dual_bank_update_init();
#endif
while (1) {
wdt_clear();
user_msg_handler();
__asm__ volatile("idle");
}
}
Source: main.c
集成方式的两个关键设计:
- 启动时初始化:
user_main()在系统启动早期调用dual_bank_update_init(),打开 UART 并注册接收中断,此后上位机随时可发起升级握手。 - 主循环轮询分发:
update_download_opt()挂在user_msg_handler()中,与按键等消息处理共用同一主循环。由于接收中断只负责"收帧 + 置标志",命令解析与升级启动都在主循环上下文完成,避免了在中断里调用重量级流程(如active_update())。
配置选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
DUAL_BANK_UPDATE_BY_UFW | 宏 (int) | 0 | 双备份模块总开关;置 1 启用 UART 升级传输 |
RETRY_TIME | 宏 (int) | 4 | 单个数据块请求的最大重试次数 |
PROTOCAL_SIZE | 宏 (int) | 528 | 单帧数据域最大长度(字节) |
SYNC_SIZE | 宏 (int) | 6 | 帧头开销:2 同步头 + 2 长度 + 2 CRC |
SYNC_MARK0 / SYNC_MARK1 | 宏 (u8) | 0xAA / 0x55 | 帧同步头字节 |
update_baudrate | static u32 | 9600 | 当前升级波特率;CMD_UPDATE_START 握手时上报 |
dual_uart_config | const struct uart_platform_data | BSP 板级配置 | UART 通道的端口号、默认波特率、引脚等(extern 于头文件,实现在板级文件) |
MSG_UART_UPDATE_READY/START/START_RSP/READ_RSP | 宏 (u8) | 0x1–0x4 | 头文件中预留的消息号定义(当前版本未在传输通道内使用) |
波特率切换通过 uart_update_set_baud(int id, u32 baudrate) 完成,切换后设备回发 CMD_UPDATE_START 确认(dual_bank_update_uart.c#L231-L256):
static void uart_update_recv(u8 cmd, u8 *buf, u32 len)
{
u32 baudrate = 9600;
switch (cmd) {
case CMD_UPDATE_START:
memcpy(&baudrate, buf, 4);
log_info("CMD_UPDATE_START:%d\n", baudrate);
if (update_baudrate != baudrate) {
update_baudrate = baudrate;
uart_update_set_baud(dual_uart_config.id, baudrate);
time_out_msec(500, NULL);
uart_update_cmd(CMD_UPDATE_START, (u8 *)&update_baudrate, 4);
} else {
update_mode_info_t info = {
.type = UART_UPDATA,
.state_cbk = uart_update_state_cbk,
.p_op_api = &uart_ch_update_op,
.en = 1,
};
if (active_update(&info)) {
dual_bank_update_init();
}
}
break;
}
}
Source: dual_bank_update_uart.c
波特率协商逻辑:若 Host 上报的波特率与当前不同,设备先切换波特率、延时 500ms 让链路稳定、再回发 CMD_UPDATE_START(携带新波特率)确认;若波特率一致(或已完成切换后的第二次握手),则直接以 UART_UPDATA 类型、uart_ch_update_op 回调集和 uart_update_state_cbk 状态回调构造 update_mode_info_t 并调用 active_update() 启动升级引擎。若 active_update() 失败,会重新 dual_bank_update_init() 以便再次接收握手。
API 参考
void dual_bank_update_init(void)
初始化升级 UART 通道:打开 dual_uart_config 配置的串口,并将 uart_loop_rx_handler 注册为 RX 中断回调;初始化失败时置 CMD_UART_UPDATE_FLAG_INIT_ERR(dual_bank_update_uart.c#L283-L293)。
调用时机:user_main() 启动阶段;active_update() 失败后的重新初始化。
void update_download_opt(void)
主循环轮询入口:当 dual_bank_update_flag == CMD_UART_UPDATE_FLAG_RECV_DATA 时按帧首字节分发命令,处理完毕清除标志(dual_bank_update_uart.c#L258-L281)。未知命令打印 unknow cmd...。
分发行为:
CMD_UART_UPDATE_START→uart_update_recv()处理波特率协商与升级启动。CMD_UART_UPDATE_READY→ 回发CMD_UPDATE_START(空载荷),进入握手。CMD_UART_UPDATE_READ/END/UPDATE_LEN/JEEP_ALIVE→ 当前版本为空分支(数据块应答由 RX 中断内的resume_hdl直接唤醒处理)。
static void uart_update_state_cbk(int type, u32 state, void *priv)
升级引擎状态回调(dual_bank_update_uart.c#L120-L139):
- 处理
UPDATE_CH_EXIT:清零uart_file_offset、恢复默认波特率、uart_close()关闭串口;若ret_code->stu == 0 && ret_code->err_code == 0(升级成功)则cpu_reset()重启。 priv指向update_ret_code_t,包含stu(状态)与err_code(错误码)。
static void uart_loop_rx_handler(u8 *buf, u16 len)
UART 接收中断回调(帧解析状态机,详见"核心流程"章节)。在中断上下文执行,因此只做字节累积、CRC 校验和标志置位,不做协议处理。
static void uart_update_hdl_register(int (*resume_hdl)(void *priv), int (*sleep_hdl)(void *priv))
注入两个钩子(ch_init 回调):
resume_hdl:收到CMD_UART_UPDATE_READ应答时被 RX 中断调用,唤醒阻塞中的数据请求。sleep_hdl:数据请求等待应答时调用,内部提供超时控制;返回非零表示超时。
全局状态变量
| 变量 | 类型 | 作用 |
|---|---|---|
protocal_frame | protocal_frame_t(4 字节对齐) | 收发共用的帧缓冲区(单缓冲设计,收/发串行使用) |
uart_file_offset | volatile u32 | 固件"虚拟文件"当前读取偏移 |
dual_bank_update_flag | volatile u8 | 中断与主循环之间的帧状态标志 |
rx_cnt | volatile u16 | 接收状态机的字节计数器 |
update_baudrate | u32 | 当前协商的升级波特率,默认 9600 |
故障模式、边界情况与并发
帧错误与重传
- CRC 校验失败:RX 中断置
CMD_UART_UPDATE_FLAG_RETRY,uart_dev_receive_data在下一次循环中清除该标志并重新发送CMD_UPDATE_READ。连续失败达到RETRY_TIME(4)次后返回-1,升级引擎中止,uart_f_stop发送CMD_UPDATE_END上报错误码。 - 应答错位:即使 CRC 正确,若应答帧的
cmd/addr/len与请求不一致(例如 Host 乱序回包),同样视为失败并重试,防止数据块错位写入 Flash。 - 超时:
uart_update_sleep_hdl返回非零即判定超时,打印uart_time_out并返回-1。超时上限由注入的 sleep 钩子内部决定(模块内使用time_out_msec(500, NULL)作为波特率切换后的链路稳定延时)。
初始化失败
dual_bank_update_init() 中 uart_init() 返回的端口号与 dual_uart_config.id 不一致时,置 CMD_UART_UPDATE_FLAG_INIT_ERR = 0xff。此后 RX 中断不再更新 RECV_DATA/RETRY 标志,即整条升级链路静默关闭——这是防止在硬件异常时反复进入升级流程的保护措施。
单缓冲并发约束
protocal_frame 是收发共用的单帧缓冲区。发送(uart_send_packet)与接收(RX 中断累积)不同时发生:数据请求流程中设备先发 CMD_UPDATE_READ 再阻塞等待应答,此时不会发起新的发送;握手帧处理也在主循环串行执行。因此不存在收发竞争,但不支持全双工并发收发——新增发送逻辑时必须保证与 RX 中断互斥。
中断上下文约束
uart_loop_rx_handler 在中断上下文运行,直接访问 protocal_frame、dual_bank_update_flag、rx_cnt 等共享状态;主循环通过 volatile 标志轮询消费。不得在中断内调用 active_update()、uart_close() 等耗时操作——当前实现将协议处理全部放在主循环(update_download_opt),这是正确性的关键。
波特率切换窗口
CMD_UPDATE_START 处理中切换波特率后延时 500ms 再回发确认,且切换后 time_out_msec 是忙等实现——该窗口内设备不响应其他命令。Host 侧必须等待收到 CMD_UPDATE_START 确认后再用新波特率发送数据。
升级失败后的恢复
active_update()返回失败:重新dual_bank_update_init(),链路复位,可再次握手重试。- 升级完成但校验失败:
uart_update_state_cbk中ret_code非零则不复位,调用uart_f_stop发送错误码,设备留在旧固件(双备份架构的核心价值:失败不损坏当前运行固件)。
性能与运维注意事项
- 单帧大小:
PROTOCAL_SIZE = 528字节是数据吞吐的关键参数。扣除命令头(struct file_info10 字节),单帧净荷约 518 字节;若需提升升级速度,可在上位机与设备同步修改该宏(注意帧缓冲区与 CRC 计算范围需一致)。 - 默认波特率 9600:这是握手阶段的低速链路,
CMD_UPDATE_START协商后切换至高速(如 115200 以上)进行数据传输。握手阶段只交换命令帧,数据阶段才走大块帧,兼顾可靠性与速度。 - 重传代价:每次 CRC 失败都重新发送完整请求帧并重新等待,重试 4 次的最坏耗时约为正常传输的 4 倍。上位机应优先保证传输质量(流控、降噪)以减少重传。
- 调试观测:串口日志
log_debug("tx(%d)")/log_debug("rx(%d)")及 hexdump 可观察帧收发;putchar('r')(小写,重试中)与putchar('R')(大写,放弃)是轻量级重传指示,便于在无日志系统时用示波器/串口助手快速判断链路质量。
扩展点
- 更换传输介质:实现另一组
update_op_api_t回调(类似uart_ch_update_op)即可复用active_update()升级引擎,例如 USB 或 SPI 通道。 - 自定义握手流程:
update_download_opt()的分支中CMD_UART_UPDATE_READ、CMD_UART_UPDATE_UPDATE_LEN、CMD_UART_JEEP_ALIVE目前是空分支,可在此补充固件总长度校验(CMD_SEND_UPDATE_LEN对端)、保活超时踢出等增强逻辑。 - 注入超时/唤醒钩子:
uart_update_hdl_register的 resume/sleep 钩子由外部传入,可在不修改协议的前提下替换超时策略(如改为带看门狗喂狗的可抢占等待)。 - 波特率策略:
update_baudrate与uart_update_set_baud构成可配置的协商点,可扩展多级速率握手(如 9600 → 460800 分级确认)。
相关链接
- dual_bank_update_uart.c(UART 升级传输实现)
- dual_bank_update_uart.h(协议定义与接口)
- main.c(应用集成点)
- dual_bank_update.c(双备份升级引擎,升级状态机与 Flash 写入)
- UART 底层驱动:
sdk/bsp/AC632N/src/uart.c等各芯片型号的uart.c/debug_uart.c - 升级模式相关结构(
update_op_api_t、update_mode_info_t、update_ret_code_t)定义于dual_bank_updata_api.h,请参阅 "Dual Bank 升级引擎" 目录页