工具架构

OTA 桌面工具基于 .NET 10.0 + WPF 构建,采用严格的五层架构:

OTA.Models          ← 纯 POCO / 枚举,零依赖

OTA.Protocols       ← Modbus / YMODEM / 串口协议实现

OTA.Core            ← 业务服务层(升级协调、远程维护、端口发现)

OTA.ViewModels      ← MVVM 视图模型(CommunityToolkit.Mvvm)

OTA.UI              ← WPF 界面(DI 容器注入)
关键职责
ModelsFirmwareSlot 枚举、PortOption 端口描述、LocalUpgradeOptions 参数、OtaErrorCode 错误码
ProtocolsModbusCrc16ModbusRawFrameBuilderYModemProtocolRunningSlotProtocolLocalUpgradeTransport
CoreLocalUpgradeServiceLocalUpgradeCoordinatorUpgradeAbSupportRemoteOtaControlServiceRemoteMaintenanceService
ViewModelsSerialUpgradeViewModelBase(977 行共享逻辑)、LocalUpgradeViewModelRemoteUpgradeViewModelRemoteMaintenanceViewModel
UIMainWindow(WM_DEVICECHANGE 监听、空闲定时器)、各页面 View、DI 注册

UI 程序集名称为 OTA,产品名”STM32 OTA 升级工具”,版本 1.1.0。

本地升级流程

本地升级是最基础的升级路径:PC 通过 RS-485 直连设备,使用 YMODEM 协议传输固件。

Step 1:验证与准备

SerialUpgradeViewModelBase.StartUpgradeAsync() 编排整个流程:

  1. 读取并验证串口设置(波特率、数据位、停止位)
  2. 验证端口可用性(通过 PortDiscoveryService 枚举注册表 + WMI)
  3. 读取当前运行槽位(Modbus 寄存器 0x005A)
  4. 调用 LocalUpgradeCoordinator.PrepareLocalUpgrade() 检查 BIN 文件头,验证 A/B 槽位不冲突

Step 2:固件镜像检查

UpgradeAbSupport.InspectImage() 读取 BIN 文件前 8 字节:

var initialStackPointer = BinaryPrimitives.ReadUInt32LittleEndian(header[..4]);
var resetHandler = BinaryPrimitives.ReadUInt32LittleEndian(header[4..8]);

通过比较 resetHandler 与已知 Flash 地址范围判断目标槽位:

private const uint SlotABaseAddress = 0x08008000u;
private const uint SlotBBaseAddress = 0x08043000u;

同时与文件名推断交叉验证(App_A.bin → Slot A,App_B.bin → Slot B),冲突时抛出异常。

Step 3:发送升级命令

LocalUpgradeService.RunAsync() 使用硬编码的 Modbus RTU 帧:

private static readonly byte[] UnlockCommand = Convert.FromHexString("0A060030A55A73D5");
private static readonly byte[] EnterBootloaderCommand = Convert.FromHexString("0A0600310005197D");
  • 从站地址 0x0A,功能码 0x06(写单个寄存器)
  • 寄存器 0x0030 写入 0xA55A 解锁维护窗口
  • 寄存器 0x0031 写入 0x0005 触发进入 Bootloader

Step 4:传输层时序

LocalUpgradeTransport.Run() 控制精确时序:

打开串口 (8N1, ReadTimeout=100ms, WriteTimeout=1000ms)
  → 发送解锁命令
  → 等待 300ms
  → 发送进入 Bootloader 命令
  → 等待 2500ms(设备从 App 切换到 Bootloader)
  → 清空串口缓冲区
  → 等待 'C' 握手字符(20 秒超时)
  → YMODEM 文件传输

Step 5:YMODEM 协议细节

YModemProtocol.SendFile() 实现完整的 YMODEM-C 发送:

**头包(packet 0)**包含元数据字符串:

var metadata = $"{fileSize} 0x{crc32:X8} {targetFirmwareVersion} {sha256Hex}";
var payload = Encoding.ASCII.GetBytes(safeName + '\0' + metadata + '\0');

数据包优先使用 1024 字节(STX/0x02),不足时回退到 128 字节(SOH/0x01),每包 CRC-16/XMODEM 校验(多项式 0x1021),单包最大重试 10 次。

串口互斥:所有串口操作通过 SerialOperationGate(全局 SemaphoreSlim(1,1))序列化,防止升级流程与后台槽位轮询冲突。

远程升级流程

远程升级采用 HTTP API + MQTT 传输模型。桌面工具不直接传输固件字节——它上传固件到服务器,由服务器通过 MQTT 分片下发。

流程概览

桌面工具                          服务器                          设备
   │                               │                              │
   ├─ 登录获取 JWT ────────────────→│                              │
   ├─ 查询运行槽位 ────────────────→│←─ MQTT 遥测 ─────────────────┤
   ├─ 上传固件 ────────────────────→│                              │
   ├─ 启动 OTA 会话 ───────────────→│── MQTT ota_begin ───────────→│
   ├─ 轮询状态(每 3 秒)──────────→│── MQTT ota_chunk(分片)────→│
   │                               │── MQTT ota_commit ──────────→│
   ├─ 完成确认 ────────────────────→│←─ MQTT 遥测(新槽位)────────┤

API 端点

端点方法说明
/api/auth/loginPOST用户名密码登录,返回 JWT
/api/latest?dev=GET获取设备最新遥测数据(含 running_slot)
/api/firmware/uploadPOST上传固件文件,返回 fileId、size、crc32、sha256
/api/ota/startPOST启动 OTA 会话(deviceId、fileId、targetSlot)
/api/ota/{sessionId}/statusGET查询 OTA 进度(state、phase、writtenBytes、progress)
/api/ota/abortPOST中止 OTA 会话

状态轮询

PollOtaStatusAsync() 每 3 秒查询一次,最长等待 10 分钟。跟踪字段:statephasewrittenBytestotalBytesprogress。终态包括:ota_completedota_failedota_abortedota_timeout

完成后等待 5 秒,重试 3 次通过 MQTT 遥测确认新运行槽位。

运行槽位检测

运行槽位通过 Modbus RTU 读取寄存器 0x005A 实现:

private const byte DeviceAddress = 0x0A;
private const ushort RunningSlotRegisterAddress = 0x005A;

请求帧(8 字节):

[0x0A] [0x03] [0x00] [0x5A] [0x00] [0x01] [CRC_LO] [CRC_HI]

响应解析:

var registerValue = BinaryPrimitives.ReadUInt16BigEndian(response[3..5]);
return registerValue switch {
    1 => FirmwareSlot.A,
    2 => FirmwareSlot.B,
    _ => FirmwareSlot.Unknown
};

MainWindow 运行两个 DispatcherTimer:每 2 秒刷新 COM 端口列表,每 3 秒读取运行槽位。升级成功后限制轮询仅 3 次(避免对透明串口桥的过度访问)。

远程维护帧生成

远程维护页签是一个独立的 Modbus RTU 帧生成器,用于通过有人云平台的”网络调试”功能发送命令。

帧构建

ModbusRawFrameBuilder.BuildFrame() 生成标准 8 字节 Modbus RTU 帧:

public static byte[] BuildFrame(ModbusRawFrameData frameData)
{
    var frame = new byte[8];
    frame[0] = frameData.SlaveAddress;
    frame[1] = frameData.FunctionCode;
    frame[2] = (byte)(frameData.RegisterAddress >> 8);
    frame[3] = (byte)(frameData.RegisterAddress & 0xFF);
    frame[4] = (byte)(frameData.DataValue >> 8);
    frame[5] = (byte)(frameData.DataValue & 0xFF);
    var crc = ModbusCrc16.Compute(frame.AsSpan(0, 6));
    frame[6] = (byte)(crc & 0xFF);
    frame[7] = (byte)(crc >> 8);
    return frame;
}

帧导入与纠正

ModbusRawFrameParser.TryParse() 接受十六进制字符串(最多 16 个十六进制字符 = 8 字节),自动去除空格/破折号/逗号/0x 前缀。如果提供完整 16 字符,还会验证尾部 CRC 是否正确,不匹配时报告需要纠正。

默认示例

工具默认填充解锁命令的示例值:从站地址 10(0x0A)、功能码 06(写单个寄存器)、寄存器地址 0030、数据值 42330(0xA55A 的十进制),生成后可一键复制到剪贴板,粘贴到有人云平台发送。