灵动微MM32 芯片串口升级OTA功能开发5. 写入功能
- 一、数据下载
- 二、下载完跳转应用程序
-
- [1. 上位机指令实现](#1. 上位机指令实现)
- [2. bootloader对于jump指令解析](#2. bootloader对于jump指令解析)

一、数据下载
1. 上位机发送固件数据功能开发
作用是将固件文件分块发送,每块最多256字节。每次发送后等待应答,确认成功后再发送下一块。下面是主要程序:
py
"""
OTA 下载固件功能模块
实现固件数据下载的发送和应答处理。
"""
import os
import time
from typing import Callable
try:
from PyQt6.QtWidgets import QApplication
QT_AVAILABLE = True
except ImportError:
QT_AVAILABLE = False
QApplication = None # type: ignore
from .comm import OTAComm
from .protocol import CMD_DOWNLOAD, TYPE_DATA, COMM_TIMEOUT_DOWNLOAD
LogCallback = Callable[[str], None]
ProgressCallback = Callable[[int], None]
def send_download_firmware(
comm: OTAComm,
bin_path: str,
log: LogCallback,
set_progress: ProgressCallback,
stop_requested: Callable[[], bool],
start_addr: int = 0x08004000,
chunk_size: int = 256,
total_timeout: float = COMM_TIMEOUT_DOWNLOAD,
retry_interval: float = 0.1,
) -> bool:
"""
发送固件数据到Flash。
将固件文件分块发送,每块最多256字节。每次发送后等待应答,确认成功后再发送下一块。
Args:
comm: OTA通信对象
bin_path: 固件文件路径
log: 日志回调函数
set_progress: 进度回调函数(0-100)
stop_requested: 停止请求检查函数
start_addr: Flash起始地址,默认0x08004000(应用区起始地址)
chunk_size: 每块数据大小(字节),默认256字节(协议限制)
total_timeout: 每块数据的超时时间(秒),默认5秒
retry_interval: 重试间隔(秒),默认0.1秒(100ms)
Returns:
True 如果所有数据发送成功,False 如果失败
"""
if not comm.is_open():
raise RuntimeError("串口未打开")
# 检查文件是否存在
if not os.path.exists(bin_path):
log(f"❌ 固件文件不存在: {bin_path}")
return False
# 读取固件文件
try:
with open(bin_path, 'rb') as f:
firmware_data = f.read()
except Exception as e:
log(f"❌ 读取固件文件失败: {e}")
return False
firmware_size = len(firmware_data)
if firmware_size == 0:
log("❌ 固件文件为空")
return False
log(f"开始发送固件数据(文件大小: {firmware_size} 字节 / {firmware_size/1024:.2f}KB)...")
log(f"起始地址: 0x{start_addr:08X}, 每块大小: {chunk_size} 字节")
# 计算总块数
total_chunks = (firmware_size + chunk_size - 1) // chunk_size
log(f"总共需要发送 {total_chunks} 块数据")
# 逐块发送
current_addr = start_addr
sent_bytes = 0
for chunk_idx in range(total_chunks):
# 检查停止请求
if stop_requested():
log("收到停止请求,停止发送固件数据...")
return False
# 计算当前块的数据
offset = chunk_idx * chunk_size
chunk_data = firmware_data[offset:offset + chunk_size]
chunk_len = len(chunk_data)
# 确保数据大小是2的倍数(半字对齐),如果不是,补齐到偶数
if (chunk_len % 2) != 0:
# 如果最后一块数据是奇数,补齐一个字节(填充0)
chunk_data = chunk_data + b'\x00'
chunk_len = len(chunk_data)
# 更新进度(50% + 40% * 已发送比例)
progress = 50 + int(40 * sent_bytes / firmware_size)
set_progress(progress)
log(f"发送第 {chunk_idx + 1}/{total_chunks} 块 (地址: 0x{current_addr:08X}, 大小: {chunk_len} 字节)...")
# 发送数据包并等待应答
start_time = time.time()
attempt = 0
success = False
while (time.time() - start_time) < total_timeout:
# 检查停止请求
if stop_requested():
log("收到停止请求,停止发送固件数据...")
return False
attempt += 1
# 每次重试重新构造数据包(序列号不递增,只有收到应答后才递增)
packet = comm.build_frame(
cmd=CMD_DOWNLOAD,
cmd_type=TYPE_DATA,
addr=current_addr,
data=chunk_data,
auto_increment=False, # 不自动递增序号
)
# 发送数据包
if not comm.is_open():
raise RuntimeError("串口未打开")
sent = comm.serial.send(packet) # type: ignore
# 等待应答
response = None
check_start = time.time()
accumulated_data = b"" # 累积接收数据,处理分片接收的情况
while (time.time() - check_start) < 0.5: # 每次等待500ms检查应答(Flash写入需要较长时间)
# 检查停止请求
if stop_requested():
log("收到停止请求,停止发送固件数据...")
return False
if QT_AVAILABLE:
QApplication.processEvents()
# 检查串口是否仍然打开(可能在停止升级时被关闭)
if not comm.is_open():
break
rx_data = comm.serial.receive_available() # type: ignore
if len(rx_data) > 0:
# 累积接收数据
accumulated_data += rx_data
# 调试:打印接收到的原始数据(前32字节)
if len(accumulated_data) > 0 and len(accumulated_data) < 200: # 只在数据较少时打印,避免输出过多
debug_hex = accumulated_data[:32].hex(' ').upper()
if len(accumulated_data) > 32:
debug_hex += "..."
log(f"[调试] 累积数据 ({len(accumulated_data)} 字节): {debug_hex}")
# 从累积数据中提取协议帧(即使前面有日志文本)
frame, remaining = OTAComm.extract_protocol_frame(accumulated_data)
if frame is not None:
# 检查应答帧的CMD是否正确(应该是下载命令 0x12)
if len(frame) >= 7:
resp_cmd = frame[6] # 应答帧的CMD在索引6
if resp_cmd == CMD_DOWNLOAD:
response = frame
log(f"[调试] 找到下载应答帧 ({len(frame)} 字节): {frame[:20].hex(' ').upper()}...")
# 更新累积数据为剩余数据,避免重复处理
accumulated_data = remaining
break
else:
# 不是下载命令的应答,可能是其他命令的应答,忽略
log(f"[调试] 收到其他命令应答 (CMD:0x{resp_cmd:02X}),忽略")
# 从累积数据中移除这个帧,继续查找
accumulated_data = remaining
continue
else:
# 帧太短,可能是错误的帧,忽略
accumulated_data = remaining
continue
else:
# 没有找到协议帧,可能是文本日志,打印但不中断
# 只打印新接收的数据,避免重复打印
try:
text = rx_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
# 限制累积数据大小,避免内存占用过大(保留最后1KB用于查找帧头)
if len(accumulated_data) > 1024:
# 保留最后1KB,可能包含帧头
accumulated_data = accumulated_data[-1024:]
time.sleep(0.01) # 10ms检查间隔
# 如果收到应答,检查状态
if response is not None:
# 收到成功应答后,才递增序号
comm._increment_seq()
# TODO: 解析应答包,检查状态码
# 目前只要收到应答就认为成功
success = True
log(f"✅ 第 {chunk_idx + 1}/{total_chunks} 块发送成功")
break
# 检查是否已超时
if (time.time() - start_time) >= total_timeout:
break
# 处理UI事件,避免界面卡死
if QT_AVAILABLE:
QApplication.processEvents()
# 等待间隔后继续发送
time.sleep(retry_interval)
# 检查是否成功
if not success:
elapsed_total = time.time() - start_time
log(f"❌ 第 {chunk_idx + 1}/{total_chunks} 块发送失败:已发送 {attempt} 次,等待 {elapsed_total:.1f}秒,仍未收到主控回复")
return False
# 更新地址和已发送字节数
current_addr += chunk_len
sent_bytes += chunk_len
# 处理UI事件,避免界面卡死
if QT_AVAILABLE:
QApplication.processEvents()
# 在发送下一块之前,给主控端一些时间完成Flash写入操作
# Flash写入256字节(128个半字)可能需要几百毫秒,这里等待200ms
# 这样可以确保主控端有足够时间完成Flash写入,避免下一块数据被阻塞
if chunk_idx < total_chunks - 1: # 不是最后一块
time.sleep(0.2) # 等待200ms,给Flash写入足够的时间
# 所有块发送完成
log(f"✅ 固件数据发送完成(共 {total_chunks} 块,{sent_bytes} 字节)")
set_progress(90) # 发送完成,进度到90%
return True
对上位机程序进行一次重构,结构如下:
xundh-bootloader-desk/
├── main.py # 程序入口
├── ota/ # OTA 功能模块
│ ├── __init__.py # OTA 模块导出
│ ├── client.py # OTA 客户端主类(协调各个功能)
│ ├── comm.py # 底层通信层(协议帧构造、发送/接收)
│ ├── protocol.py # 协议常量定义(命令码、状态码、CRC计算)
│ ├── handshake.py # 握手功能实现
│ ├── erase.py # 擦除Flash功能实现
│ └── download.py # 下载固件功能实现
├── myserial/ # 串口管理模块
│ ├── __init__.py # 串口模块导出
│ └── manager.py # 串口通信封装(枚举、打开、发送、接收)
└── ui/ # 用户界面模块
├── __init__.py # UI 模块导出
└── mainwindow.py # 主窗口界面
2. bootloader 实现数据接收
(1)协议解析器
协议解析器状态机
协议解析器初始化后,解析器会按以下状态流转:
IDLE → HEAD0 → HEAD1 → SEQ → SEQ_XOR → LEN_H → LEN_L → DATA → CRC_H → CRC_L → COMPLETE
IDLE: 等待帧头字节0x5AHEAD0/HEAD1: 接收帧头SEQ/SEQ_XOR: 接收序号LEN_H/LEN_L: 接收数据包长度DATA: 接收数据包内容CRC_H/CRC_L: 接收 CRC 校验COMPLETE: 帧接收完成
OTA_Protocol_Init() 函数讲解
OTA_Protocol_Init() 用于初始化 OTA 协议解析器,将解析器状态重置为初始状态,准备接收新的协议帧。
(2)下载指令解析
c
/**
* @brief 处理下载固件命令
* @param cmd_packet 命令包指针
* @return true 处理成功,false 处理失败
*/
bool OTA_CmdHandler_HandleDownload(OTA_CommandPacket_t* cmd_packet)
{
OTA_ResponsePacket_t response = {0};
uint32_t write_addr;
uint32_t write_size;
BootFlash_Error_t flash_err;
char debug_msg[64];
if (cmd_packet == NULL) {
return false;
}
/* 初始化应答包 */
response.cmd = cmd_packet->cmd;
response.addr = cmd_packet->addr;
response.reserved = 0;
response.data_len = 0;
/* 检查命令类型 */
if (cmd_packet->type != OTA_TYPE_DATA) {
response.status = OTA_STATUS_PARAM_ERR;
return OTA_Protocol_SendResponse(&response);
}
/* 解析写入地址 */
write_addr = cmd_packet->addr;
/* 解析数据大小 */
write_size = cmd_packet->data_len;
/* 检查数据大小:不能超过最大数据包大小,且必须是半字对齐 */
if (write_size == 0 || write_size > OTA_MAX_PACKET_SIZE) {
response.status = OTA_STATUS_PARAM_ERR;
return OTA_Protocol_SendResponse(&response);
}
/* 检查数据大小是否为半字对齐(2字节的整数倍) */
if ((write_size % 2) != 0) {
response.status = OTA_STATUS_PARAM_ERR;
return OTA_Protocol_SendResponse(&response);
}
/* 调用Flash写入函数 */
flash_err = BootFlash_Write(write_addr, cmd_packet->data, write_size);
/* 根据错误码设置应答状态 */
switch (flash_err) {
case BOOT_FLASH_OK:
response.status = OTA_STATUS_SUCCESS;
break;
case BOOT_FLASH_ERR_PARAM:
case BOOT_FLASH_ERR_SIZE:
response.status = OTA_STATUS_PARAM_ERR;
break;
case BOOT_FLASH_ERR_ADDR:
response.status = OTA_STATUS_ADDR_ERR;
break;
case BOOT_FLASH_ERR_ERASE:
response.status = OTA_STATUS_FLASH_ERR;
break;
default:
response.status = OTA_STATUS_ERROR;
break;
}
/* 先发送应答,避免调试输出干扰应答识别 */
bool send_result = OTA_Protocol_SendResponse(&response);
return send_result;
}
二、下载完跳转应用程序
1. 上位机指令实现
py
"""
OTA 跳转应用功能模块
实现跳转应用命令的发送和应答处理。
"""
import time
from typing import Callable
try:
from PyQt6.QtWidgets import QApplication
QT_AVAILABLE = True
except ImportError:
QT_AVAILABLE = False
QApplication = None # type: ignore
from .comm import OTAComm
from .protocol import CMD_JUMP_APP, TYPE_CONTROL, COMM_TIMEOUT_DEFAULT, COMM_CHECK_INTERVAL, STATUS_SUCCESS
LogCallback = Callable[[str], None]
def send_jump_app(
comm: OTAComm,
log: LogCallback,
stop_requested: Callable[[], bool],
total_timeout: float = COMM_TIMEOUT_DEFAULT,
retry_interval: float = 0.2,
) -> bool:
"""
发送跳转应用命令,等待应答。
Args:
comm: OTA通信对象
log: 日志回调函数
stop_requested: 停止请求检查函数
total_timeout: 总超时时间(秒),默认2秒
retry_interval: 重试间隔(秒),默认0.2秒(200ms)
Returns:
True 如果跳转命令发送成功并收到成功应答,False 如果失败
"""
if not comm.is_open():
raise RuntimeError("串口未打开")
log(f"开始发送跳转应用命令(间隔 {retry_interval*1000:.0f}ms,总超时 {total_timeout:.0f}秒)...")
start_time = time.time()
attempt = 0
# 持续发送跳转命令,直到收到应答或达到总超时时间
while (time.time() - start_time) < total_timeout:
# 检查停止请求
if stop_requested():
log("收到停止请求,停止发送跳转命令...")
return False
attempt += 1
elapsed = time.time() - start_time
# 构造跳转应用命令包
packet = comm.build_frame(
cmd=CMD_JUMP_APP,
cmd_type=TYPE_CONTROL,
addr=0x00000000,
data=b"",
auto_increment=False, # 不自动递增序号,收到应答后才递增
)
log(f"已发送跳转应用命令 (第 {attempt} 次,已用时 {elapsed:.1f}秒): {packet.hex(' ').upper()}")
# 发送数据包
if not comm.is_open():
raise RuntimeError("串口未打开")
sent = comm.serial.send(packet) # type: ignore
# 等待应答
response = None
check_start = time.time()
accumulated_data = b"" # 累积接收数据,处理分片接收的情况
while (time.time() - check_start) < 1.0: # 每次等待1秒检查应答,给主控足够时间发送
# 检查停止请求
if stop_requested():
log("收到停止请求,停止等待应答...")
return False
if QT_AVAILABLE:
QApplication.processEvents()
# 检查串口是否仍然打开
if not comm.is_open():
break
data = comm.serial.receive_available() # type: ignore
if len(data) > 0:
# 累积接收数据
accumulated_data += data
# 从累积数据中提取协议帧
frame, remaining = OTAComm.extract_protocol_frame(accumulated_data)
if frame is not None:
response = frame
accumulated_data = remaining
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
# 限制累积数据大小
if len(accumulated_data) > 1024:
accumulated_data = accumulated_data[-1024:]
time.sleep(COMM_CHECK_INTERVAL)
# 如果收到应答,检查状态
if response is not None:
# 收到应答后,才递增序号
comm._increment_seq()
log(f"收到跳转应用应答 ({len(response)} 字节): {response.hex(' ').upper()}")
# 解析应答包,检查状态(应答包格式:帧头(2) + 序号(1) + 序号异或(1) + 长度(2) + 命令(1) + 状态(1) + 地址(4) + 保留(4) + CRC(2))
if len(response) >= 18:
status_byte = response[7] # 状态字节位置
if status_byte == STATUS_SUCCESS:
log("✅ 跳转应用命令成功,主控将跳转到应用程序")
return True
else:
log(f"⚠️ 跳转应用命令失败,状态码: 0x{status_byte:02X}")
# 即使状态不是成功,也继续重试
else:
log(f"⚠️ 应答包长度异常: {len(response)} 字节")
# 如果收到应答但状态不是成功,继续重试
# 注意:如果主控已经跳转,可能不会再收到应答,所以这里继续重试可能无意义
# 但为了完整性,还是继续重试直到超时
# 检查是否已超时
if (time.time() - start_time) >= total_timeout:
break
# 处理UI事件
if QT_AVAILABLE:
QApplication.processEvents()
# 等待间隔后继续发送
time.sleep(retry_interval)
# 超时提示
elapsed_total = time.time() - start_time
log(f"❌ 跳转应用命令失败:已发送 {attempt} 次,等待 {elapsed_total:.1f}秒,仍未收到主控回复")
log(f" 注意:如果主控已跳转到应用程序,可能不会再发送应答")
return False
2. bootloader对于jump指令解析
c
/**
* @brief 处理跳转应用命令
* @param cmd_packet 命令包指针
* @return true 处理成功,false 处理失败
*/
bool OTA_CmdHandler_HandleJumpApp(OTA_CommandPacket_t* cmd_packet)
{
OTA_ResponsePacket_t response = {0};
if (cmd_packet == NULL) {
return false;
}
/* 检查命令类型 */
if (cmd_packet->type != OTA_TYPE_CONTROL) {
response.status = OTA_STATUS_PARAM_ERR;
} else {
/* 检查应用程序是否有效 */
if (BootJump_IsApplicationValid()) {
response.status = OTA_STATUS_SUCCESS;
/* 发送应答后跳转 */
response.cmd = cmd_packet->cmd;
response.addr = cmd_packet->addr;
response.reserved = 0;
response.data_len = 0;
OTA_Protocol_SendResponse(&response);
/* 跳转到应用程序 */
BootJump_ToApplication();
} else {
response.status = OTA_STATUS_ERROR;
}
}
/* 构建应答包 */
response.cmd = cmd_packet->cmd;
response.addr = cmd_packet->addr;
response.reserved = 0;
response.data_len = 0;
/* 发送应答 */
return OTA_Protocol_SendResponse(&response);
}
本示例的应用程序,会闪烁LED,以看出是否启动主程序成功。
运行示例:

本代码开源地址:
https://gitee.com/xundh/xundh-arm-m0-ota
本示例代码主要由AI生成。
下一章节将实现应用中通过magic地址值写入实现OTA功能。