工具架构
OTA 桌面工具基于 .NET 10.0 + WPF 构建,采用严格的五层架构:
OTA.Models ← 纯 POCO / 枚举,零依赖
↑
OTA.Protocols ← Modbus / YMODEM / 串口协议实现
↑
OTA.Core ← 业务服务层(升级协调、远程维护、端口发现)
↑
OTA.ViewModels ← MVVM 视图模型(CommunityToolkit.Mvvm)
↑
OTA.UI ← WPF 界面(DI 容器注入)
| 层 | 关键职责 |
|---|---|
| Models | FirmwareSlot 枚举、PortOption 端口描述、LocalUpgradeOptions 参数、OtaErrorCode 错误码 |
| Protocols | ModbusCrc16、ModbusRawFrameBuilder、YModemProtocol、RunningSlotProtocol、LocalUpgradeTransport |
| Core | LocalUpgradeService、LocalUpgradeCoordinator、UpgradeAbSupport、RemoteOtaControlService、RemoteMaintenanceService |
| ViewModels | SerialUpgradeViewModelBase(977 行共享逻辑)、LocalUpgradeViewModel、RemoteUpgradeViewModel、RemoteMaintenanceViewModel |
| UI | MainWindow(WM_DEVICECHANGE 监听、空闲定时器)、各页面 View、DI 注册 |
UI 程序集名称为 OTA,产品名”STM32 OTA 升级工具”,版本 1.1.0。
本地升级流程
本地升级是最基础的升级路径:PC 通过 RS-485 直连设备,使用 YMODEM 协议传输固件。
Step 1:验证与准备
SerialUpgradeViewModelBase.StartUpgradeAsync() 编排整个流程:
- 读取并验证串口设置(波特率、数据位、停止位)
- 验证端口可用性(通过
PortDiscoveryService枚举注册表 + WMI) - 读取当前运行槽位(Modbus 寄存器 0x005A)
- 调用
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/login | POST | 用户名密码登录,返回 JWT |
/api/latest?dev= | GET | 获取设备最新遥测数据(含 running_slot) |
/api/firmware/upload | POST | 上传固件文件,返回 fileId、size、crc32、sha256 |
/api/ota/start | POST | 启动 OTA 会话(deviceId、fileId、targetSlot) |
/api/ota/{sessionId}/status | GET | 查询 OTA 进度(state、phase、writtenBytes、progress) |
/api/ota/abort | POST | 中止 OTA 会话 |
状态轮询
PollOtaStatusAsync() 每 3 秒查询一次,最长等待 10 分钟。跟踪字段:state、phase、writtenBytes、totalBytes、progress。终态包括:ota_completed、ota_failed、ota_aborted、ota_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 的十进制),生成后可一键复制到剪贴板,粘贴到有人云平台发送。