灵动微MM32 芯片串口升级OTA功能开发3. 串口指令解析
·
灵动微MM32 芯片串口升级OTA功能开发3. 串口指令解析

一、创建一个上位机demo
1. 准备环境
本示例用Python写的一个上位机软件,使用python3.11,需要安装依赖的库:
pip install PyQt6 pyserial
上位机运行截图:
2. 上位机架构
上位机采用分层架构,将代码分为以下模块:
┌─────────────────┐
│ UI 界面层 │ ← 用户交互
├─────────────────┤
│ OTA业务逻辑层 │ ← OTAClient: 实现握手、擦除、下载等业务
├─────────────────┤
│ 基础通信层 │ ← OTAComm: 协议帧构造、发送/接收
├─────────────────┤
│ 协议工具层 │ ← ota_protocol: CRC计算、常量定义
├─────────────────┤
│ 串口管理层 │ ← SerialPort: 串口底层操作
└─────────────────┘
项目架构:
xundh-bootloader-desk/
├── main.py # 程序入口
├── ui_mainwindow.py # UI界面层
├── ota_client.py # OTA业务逻辑层 ⭐
├── ota_comm.py # 基础通信层
├── ota_protocol.py # 协议工具层
└── serial_manager.py # 串口管理层
分层说明:
- UI层: 负责界面显示和用户交互
- 业务逻辑层 (
ota_client.py): 实现OTA升级的业务流程 - 通信层 (
ota_comm.py): 负责协议帧构造、发送/接收数据 - 协议层 (
ota_protocol.py): 提供协议常量、CRC计算等工具函数 - 串口层 (
serial_manager.py): 封装串口底层操作
UI部分使用 PyQt6,主要代码由AI生成,不作为本项目重点部分。
2.1 OTAClient 类详解
OTAClient 是OTA升级的核心业务逻辑类,负责与Bootloader的通信协议。
类定义
class OTAClient:
"""
OTA 客户端实现,负责 OTA 业务逻辑。
底层通信由 OTAComm 类负责,此类只关注 OTA 协议层面的业务逻辑。
"""
2.2 初始化方法
def __init__(self, port: str, bin_path: str, baudrate: int = 115200):
self.bin_path = bin_path
self.comm = OTAComm(port, baudrate) # 创建底层通信对象
设计说明:
OTAClient内部持有OTAComm对象,负责底层串口通信- 采用组合模式,将通信功能委托给专门的通信类
- 业务逻辑与底层通信解耦,便于测试和维护
2.3 握手方法 send_handshake()
def send_handshake(self, log: LogCallback, total_timeout: float = 60.0, retry_interval: float = 0.2) -> bool:
"""持续发送握手包直到收到应答或达到总超时时间(1分钟)"""
工作流程:
-
构造握手包
packet = self.comm.build_frame( cmd=CMD_HANDSHAKE, # 命令码: 0x10 cmd_type=TYPE_CONTROL, # 类型: 控制指令 addr=0x00000000, # 地址: 0 data=b"", # 数据: 空 ) -
持续发送并等待应答
- 每次发送后等待2秒应答
- 未收到应答时,等待0.2秒后重试
- 持续1分钟,直到收到应答或超时
-
超时处理
- 1分钟内未收到应答,输出错误提示
- 提示检查串口连接、主控状态、波特率等
2.4 上位机发送握手包完整代码
def send_handshake(self, log: LogCallback, total_timeout: float = 60.0, retry_interval: float = 0.2) -> bool:
"""
持续发送握手包直到收到应答或达到总超时时间(1分钟)。
以固定间隔(默认200ms)不停发送,直到超时或收到回应。
Args:
log: 日志回调函数
total_timeout: 总超时时间(秒),默认60秒(1分钟)
retry_interval: 重试间隔(秒),默认0.2秒(200ms)
Returns:
True 如果握手成功,False 如果失败
"""
if not self.comm.is_open():
raise RuntimeError("串口未打开")
# 构造握手包
packet = self.comm.build_frame(
cmd=CMD_HANDSHAKE,
cmd_type=TYPE_CONTROL,
addr=0x00000000,
data=b"",
)
log(f"开始发送握手包(间隔 {retry_interval*1000:.0f}ms,总超时 {total_timeout:.0f}秒)...")
import time as time_module
start_time = time_module.time()
attempt = 0
# 持续发送握手包,直到收到应答或达到总超时时间
while (time_module.time() - start_time) < total_timeout:
# 检查停止请求
if self._stop_requested:
log("收到停止请求,停止握手...")
return False
attempt += 1
elapsed = time_module.time() - start_time
# 只在第一次发送时打印,后续静默发送
# if attempt == 1:
log(f"已发送握手包 (第 {attempt} 次): {packet.hex(' ').upper()}")
# 发送数据包(不等待应答,立即继续)
if not self.comm.is_open():
raise RuntimeError("串口未打开")
sent = self.comm.serial.send(packet) # type: ignore
# 快速检查是否有应答(短暂等待)
response = None
check_start = time_module.time()
while (time_module.time() - check_start) < 0.05: # 每次只等待50ms检查应答
# 检查停止请求
if self._stop_requested:
log("收到停止请求,停止握手...")
return False
if QT_AVAILABLE:
QApplication.processEvents()
data = self.comm.serial.receive_available() # type: ignore
if len(data) > 0:
# 检查是否是协议帧(以 0x5A5A 开头)
if data.startswith(bytes([0x5A, 0x5A])):
response = data
break
else:
# 可能是文本日志,打印但不中断
try:
text = data.decode('ascii', errors='replace')
text_clean = ''.join(c if 32 <= ord(c) < 127 or c in '\r\n' else '' for c in text)
if text_clean.strip():
log(f"[主控] {text_clean.strip()}")
except:
pass
time.sleep(0.01) # 10ms检查间隔
# 如果收到应答,返回成功
if response is not None:
log(f"收到握手应答 ({len(response)} 字节): {response.hex(' ').upper()}")
log("握手成功!")
# TODO: 解析应答包,检查状态
return True
# 检查是否已超时
if (time_module.time() - start_time) >= total_timeout:
break
# 处理UI事件,避免界面卡死
if QT_AVAILABLE:
QApplication.processEvents()
# 等待间隔后继续发送
time.sleep(retry_interval)
# 超时提示
elapsed_total = time_module.time() - start_time
log(f"❌ 握手失败:已发送 {attempt} 次,等待 {elapsed_total:.1f}秒,仍未收到主控回复")
log(f" 请检查:")
log(f" 1. 串口连接是否正常")
log(f" 2. 主控是否已上电并进入bootloader模式")
log(f" 3. 波特率是否正确(115200)")
return False
握手包示例:
5A 5A 00 FF 00 0A 10 01 00 00 00 00 00 00 00 00 F2 AD
主控回复示例:
5A 5A 00 FF 00 0A 10 00 00 00 00 00 00 00 00 00 19 8E
二、主控接收握手包处理
boot_loader在前节基础上修改架构:
- boot_jump.c
- boot_ota_cmd_handler.c
- boot_ota_handshake.c
- boot_ota_protocol.c
- boot_ota_state.c
- boot_uart.c
- boot_utils.c
- main.c
1. 指令解析
boot_ota_cmd_handler 文件负责对串口指令的解析分流。
2. 握手数据处理
/**
* @brief 等待并处理握手包(完整接收、验证和回应)
* @param timeout_ms 超时时间(毫秒)
* @return true 成功接收并回应握手包,false 超时或失败
*/
bool OTA_Handshake_WaitAndRespond(uint32_t timeout_ms)
{
u32 elapsed_ms = 0;
bool frame_received = false;
/* 初始化协议解析器 */
OTA_Protocol_Init();
// BSP_LOG_Print("B"); /* 调试标记 */
/* 在超时时间内循环接收 */
while (elapsed_ms < timeout_ms) {
/* 持续接收数据,直到没有更多数据或接收到完整帧 */
bool frame_received_inner = false;
/* 快速连续处理所有可用数据,避免数据丢失 */
uint32_t no_data_count = 0;
while (no_data_count < 10) { /* 连续10次没有数据才退出,确保数据包完整接收 */
if (BSP_UART_DataAvailable()) {
no_data_count = 0; /* 有数据,重置计数 */
/* 接收并解析协议帧 */
if (OTA_Protocol_ReceiveFrame()) {
/* 接收到完整帧,标记并退出接收循环 */
frame_received_inner = true;
break; /* 退出内层接收循环,准备处理帧 */
}
} else {
no_data_count++; /* 没有数据,计数加1 */
/* 短暂延时,等待数据到达(避免CPU占用过高) */
volatile uint32_t delay = 100;
while (delay--);
}
}
/* 如果收到完整帧,在这里处理(此时不在接收循环中,可以安全发送) */
if (frame_received_inner) {
OTA_CommandPacket_t cmd_packet;
if (OTA_Protocol_GetCommandPacket(&cmd_packet)) {
/* 检查是否为握手命令 */
if (cmd_packet.cmd == OTA_CMD_HANDSHAKE) {
/* 处理握手命令并发送应答 */
if (OTA_CmdHandler_HandleHandshake(&cmd_packet)) {
frame_received = true;
break; /* 成功处理,退出外层循环 */
}
}
}
/* 重置协议解析器,准备接收下一帧 */
OTA_Protocol_Reset();
}
/* 延时1ms后继续检测 */
BootUtils_DelayMs(1);
elapsed_ms += 1;
}
return frame_received;
}
三、测试
首先打开上位机,点击开始升级。
主控上电,收到握手包,完成握手过程 。
上位机的日志:
下一节实现擦除应用区、接收固件的功能。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)