在这里插入图片描述

一、创建一个上位机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分钟)"""

工作流程:

  1. 构造握手包

    packet = self.comm.build_frame(
        cmd=CMD_HANDSHAKE,      # 命令码: 0x10
        cmd_type=TYPE_CONTROL,  # 类型: 控制指令
        addr=0x00000000,        # 地址: 0
        data=b"",               # 数据: 空
    )
    
  2. 持续发送并等待应答

    • 每次发送后等待2秒应答
    • 未收到应答时,等待0.2秒后重试
    • 持续1分钟,直到收到应答或超时
  3. 超时处理

    • 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;
}

三、测试

首先打开上位机,点击开始升级。

主控上电,收到握手包,完成握手过程 。
上位机的日志:
在这里插入图片描述
下一节实现擦除应用区、接收固件的功能。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐