3. 通信协议选型:HTTPS、MQTT与DoIP/UDS的三角组合

说实话,每次做远程升级方案,通信协议选型都是最让人头疼的环节。你想想看,车上几十个ECU,有的在动力域,有的在座舱域,有的在车身域,它们的网络环境、算力资源、实时性要求完全不一样。用一套协议打天下?不现实。用太多协议?维护成本又上去了。

我个人习惯的做法是:按场景切分,各取所长。升级包下载用HTTPS,状态上报用MQTT,诊断集成走DoIP/UDS。这三个协议各司其职,配合起来刚刚好。下面我逐个展开聊聊。

3.1 HTTPS:升级包下载的“压舱石”

为什么选HTTPS?说白了就两个字:可靠

我在项目中遇到过好几次这样的情况:用普通HTTP下载升级包,结果传输过程中被中间人篡改,或者被运营商劫持插入了广告。嗯,这种问题在车联网场景下是绝对不能接受的。你想想看,一个刹车系统的固件被篡改,后果有多严重?

HTTPS的核心优势在于:

  • 加密传输:TLS握手后,所有数据都是密文,中间人无法窃听
  • 完整性校验:每个数据包都有MAC校验,篡改会被立即发现
  • 身份认证:客户端验证服务器证书,防止DNS劫持

这里有个细节要注意:证书管理。车端的根证书需要预置在安全芯片里,不能放在文件系统里。我曾经见过一个方案,把CA证书放在/data分区,结果OTA升级时证书被误删了,整批车都连不上服务器。嗯,这种坑踩过一次就记住了。

实际项目中的做法

我们当时用的是TLS 1.2,证书采用ECC算法(P-256),比RSA更省带宽。升级包分片下载,每片2MB,支持断点续传。车端用curl库集成,超时时间设成30秒,重试3次。

代码示例(车端下载逻辑):

// 伪代码:HTTPS下载升级包
int download_firmware(const char* url, const char* output_path) {
    CURL* curl = curl_easy_init();
    if (!curl) return -1;
    
    // 设置TLS选项
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
    curl_easy_setopt(curl, CURLOPT_CAINFO, "/security/certs/root.pem");
    
    // 设置超时和重试
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
    
    // 断点续传支持
    curl_easy_setopt(curl, CURLOPT_RESUME_FROM, downloaded_size);
    
    // 执行下载
    CURLcode res = curl_easy_perform(curl);
    curl_easy_cleanup(curl);
    return (res == CURLE_OK) ? 0 : -1;
}

避坑指南

我曾经遇到过车端时间不准导致TLS握手失败的问题。车机断电后RTC会跑偏,证书有效期验证通不过。解决方案是:在TLS握手前先通过NTP同步时间,或者使用证书固定(Certificate Pinning)绕过时间校验。

3.2 MQTT:状态上报的“轻骑兵”

升级过程中,车端需要频繁上报状态:下载进度、校验结果、刷写进度、错误码……如果用HTTPS轮询,服务器压力大,车端也费电。这时候MQTT就派上用场了。

MQTT的优势在于:

  • 轻量级:最小报文只有2字节,适合车载网络
  • 发布/订阅模型:服务器可以主动推送指令,车端不用轮询
  • QoS等级:状态上报用QoS 1(至少一次),确保不丢消息
  • 遗嘱消息:车端异常断线时,服务器能感知到

我建议的Topic设计如下:

Topic 方向 用途 QoS
vehicle/{vin}/ota/status 车→云 上报升级状态 1
vehicle/{vin}/ota/progress 车→云 上报下载进度 1
vehicle/{vin}/ota/error 车→云 上报错误码 2
cloud/{vin}/ota/command 云→车 下发升级指令 1

这里有个经验:MQTT的心跳间隔。设太短费电,设太长服务器容易误判离线。我一般设成60秒,配合Keep Alive机制。车端在休眠前会主动发送DISCONNECT报文,而不是直接断网。

个人技巧

状态上报的Payload用Protobuf序列化,比JSON省带宽。举个例子,一个升级进度消息用JSON要80字节,Protobuf只要12字节。你想想看,如果100万辆车同时上报,省下来的流量费可不是小数目。

3.3 DoIP/UDS:诊断集成的“硬骨头”

升级包下载到车端后,怎么刷进ECU?这就轮到DoIP和UDS登场了。

UDS(ISO 14229)是诊断服务的标准协议,刷写ECU本质上就是一系列UDS服务调用:

  • 0x10:诊断会话控制(切换到编程会话)
  • 0x27:安全访问(解锁ECU)
  • 0x34/0x36:请求下载/数据传输(传输固件数据)
  • 0x31:例程控制(执行刷写后校验)
  • 0x11:ECU复位(重启生效)

DoIP(ISO 13400)则是UDS在以太网上的传输层。它解决了传统CAN诊断的几个痛点:

  • 带宽大:100Mbps以太网,刷写速度比CAN快几十倍
  • 并发支持:可以同时诊断多个ECU
  • 远程诊断:通过IP网络,T-Box可以直接和云端诊断服务器通信

我在项目中遇到过一个问题:DoIP的激活线处理。DoIP要求ECU在收到诊断请求前先通过激活线(Activation Line)唤醒。有些ECU的激活线设计不合理,导致诊断请求发不过去。后来我们改成了逻辑寻址+物理寻址混合模式,才解决了这个问题。

实际项目中的刷写流程

  1. 车端通过MQTT收到升级指令,开始下载HTTPS升级包
  2. 下载完成后,车端通过DoIP连接到目标ECU
  3. 发送UDS 0x10 03(编程会话),等待ECU响应
  4. 发送UDS 0x27(安全访问),解锁刷写权限
  5. 发送UDS 0x34(请求下载),告知ECU固件大小和校验方式
  6. 循环发送UDS 0x36(数据传输),每次传输1KB数据
  7. 发送UDS 0x31 01 FF00(执行完整性校验)
  8. 发送UDS 0x11 01(硬复位),ECU重启加载新固件

代码示例(UDS刷写核心逻辑):

// 伪代码:通过DoIP发送UDS请求
int uds_write_data(uint16_t ecu_addr, uint8_t* data, uint32_t len) {
    // 构建DoIP报文头
    DoIPHeader header;
    header.protocol_version = 0x02;
    header.payload_type = 0x8001;  // 诊断消息
    header.payload_length = 4 + len;
    
    // 构建UDS请求
    uint8_t uds_request[] = {0x36, 0x01, (len >> 8) & 0xFF, len & 0xFF};
    memcpy(uds_request + 4, data, len);
    
    // 发送DoIP报文
    send_doip_packet(ecu_addr, &header, uds_request, sizeof(uds_request));
    
    // 等待UDS响应(0x76)
    uint8_t response[8];
    recv_doip_response(ecu_addr, response, 1000);  // 1秒超时
    return (response[1] == 0x76) ? 0 : -1;
}

避坑指南

我曾经遇到过ECU在刷写过程中掉电的情况。UDS协议本身没有断点续传机制,一旦中断就得从头刷。解决方案是:在刷写前先备份当前固件,刷写过程中每传输10%数据就记录一次进度。如果掉电重启,先检查备份区,再决定是继续刷还是回滚。

3.4 三个协议的协同工作

这三个协议不是孤立的,它们需要协同配合。我画了一张逻辑图(用文字描述):

  1. 云端下发指令:通过MQTT的command Topic,告诉车端“开始升级”
  2. 车端下载固件:通过HTTPS从CDN下载升级包,进度通过MQTT上报
  3. 车端刷写ECU:通过DoIP/UDS将固件刷入目标ECU,状态通过MQTT上报
  4. 云端确认结果:刷写完成后,车端通过MQTT上报最终状态,云端记录日志

这里有个关键点:状态机设计。车端的OTA管理器需要维护一个状态机,确保在任何异常情况下都能正确回滚或重试。我一般用有限状态机(FSM)实现,状态包括:IDLE、DOWNLOADING、VERIFYING、FLASHING、COMPLETED、ROLLBACK。

个人建议

如果你刚开始做这个方案,建议先跑通HTTPS下载+MQTT上报的链路,再集成DoIP/UDS。因为DoIP/UDS涉及ECU底层的刷写逻辑,调试起来比较麻烦。先把上层链路跑通,再往下层深入,这样效率更高。

好了,通信协议选型这块就聊到这里。下一章我会讲升级包的分包策略和校验机制,到时候再细聊。