先问个问题:在一个没有TBox、没有CANoe常驻台架的车载项目里,网关自身要刷写,挂在LIN总线上的低端从机也要刷写,你能接受开发阶段每次改固件都拆壳、搬TTL、手动烧录吗?大概率不能。所以我当时给自己定的目标很明确:一套CAN-LIN网关的刷写升级方案,从PC/诊断仪侧用最标准的CAN诊断服务出发,把网关自己的Bootloader跑通,再让网关当“二道贩子”把固件通过LIN调度表转给从机。整个链路上,CAN诊断是入口,LIN是末端,OTA是目的。
这篇实战笔记适合正在做车载网关、车身域控制器、LIN从机模块或者Bootloader开发的朋友。你会看到UDS刷写状态机怎么搭、ISO-TP分帧怎么调、LIN从机OTA的调度表和转发缓冲怎么设计,以及我踩过的坑——包括掉电恢复、双区回滚、Flash写得一半被看门狗咬死这种经典场景。
1. 先捋清楚整条升级链路:网关到底干了什么
1.1 不是所有OTA都叫“网关中转OTA”
先说清楚方案定位。现在行业内说“OTA”至少有三种形态:云端直接下发到TBox/T-Box,再由TBox通过以太网或CAN刷写ECU;产线/售后用诊断仪通过CAN诊断口直接刷写ECU;还有一种是网关本身作为小型的远程信息终端,没有独立TBox,它既要接收外部请求,也要管着下面一堆没有CAN控制器的LIN从机。
我项目里的场景就是第三种:CAN总线上挂着一块网关,LIN总线上挂着车窗、门锁、氛围灯、雨量传感器这类低成本节点。这些节点大多数是8位MCU或者低端Cortex-M0,没有CAN控制器,只有UART+LIN收发器。你说为了OTA给每个从机加个CAN收发器?成本和板面积都吃不消。于是“网关中转”就成了唯一合理的选择——网关在CAN侧用标准诊断服务接收升级请求和固件数据,到LIN侧转成从机能听懂的下载协议。
这套方案还有一个额外好处:产线和4S店用的诊断仪、售后工具基本都支持UDS,不需要专门开发一套专属刷写硬件。也就是说,你只要把网关侧UDS服务做实,整个售后体系的工具链都可以直接复用。这也是我在方案设计之初坚持“CAN诊断走UDS标准、LIN侧走轻量私有协议”的根本原因。
1.2 硬件与整体架构
主控我选的是NXP S32K144,理由不复杂:车身环境温度范围宽、AEC-Q100车规认证、SDK里CAN和LIN协议栈都比较成熟,而且引脚不贵。CAN收发器用TJA1042,LIN收发器用TJA1021,都是很常见的料。如果你是做低成本方案,STM32F105或者GD32F30x也可以,但要注意双CAN和Flash容量够不够。
架构上最核心的一条链路是这样:上位机(诊断仪/测试PC)通过CAN收发器连到网关的CAN口,网关MCU内部跑UDS诊断协议栈,同时在LIN口跑主节点调度。LIN从机不直接参与CAN总线,所有升级数据都由网关“翻译”过后再发出去。听起来简单,但实际落地时你会发现难点全在细节:CAN侧一次传输数据块可能是256字节,LIN侧一个8字节帧要拆成NAD、SID、序列号、有效负载,还得在从机Flash擦除期间不把总线上其他节点搞乱。
板级设计上有一点特别提醒:网关一定要把常电和点火电分开处理。OTA刷写经常发生在车辆熄火状态下,如果只有KL15供电,熄火后网关自己都断电了,还怎么升级从机?我是在电源入口做了KL30常电+KL15唤醒的双路设计,刷写任务触发时由KL30维持工作。
1.3 Flash分区与双备份策略
刷写这件事里最有“后悔药”性质的就是分区设计。我做了四区划分:Bootloader区、App区、参数区、备份区。
| 分区 | 内容 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 引导+升级服务 | 64KB | 上电校验、跳转、接收新固件 |
| App | 主应用固件 | 256KB | 正常工作区 |
| Param | 版本号、升级状态字、配置参数 | 16KB | 掉电恢复的关键依据 |
| Backup | 上一版可用固件 | 256KB | 回滚兜底,可选 |
为什么双备份要单独讲?因为OTA最怕的不是失败,而是失败后车趴窝。网关自己刷写时,我会先把新固件完整收到一个RAM缓冲或备份区,做CRC校验通过后,才允许擦除App区写新固件。如果擦写过程中掉电,Bootloader上电后读到Param区的“升级未完成”标志,自动从Backup区恢复。对LIN从机来说,很多从机Flash只有32KB~64KB,装不下双区,只能靠“升级不要断电+升级完成自校验”来兜底。后面4.2节我会专门聊掉电保护的实操设计,这里先把分区框架立住。
2. CAN诊断刷写链路:UDS那套状态机是怎么跑的
2.1 诊断ID与会话管理
UDS刷写的第一个关卡是诊断ID,也叫RAID地址映射。我在这套方案里用了常规的物理寻址做法:网关自身的请求ID是0x710,响应ID是0x718;LIN从机1映射为0x720/0x728,从机2映射为0x721/0x729,以此类推。功能寻址用0x7DF,主要用于广播会话切换和复位指令。
诊断会话管理是整个刷写状态机的骨架。刷写前必须先切换到编程会话(0x10 02),或者扩展会话(0x10 03)也可以,但更规范的做法是进编程会话。编程会话下,网关要启用一个S3Server超时定时器,通常5000ms。如果5000ms内没有收到任何带“响应抑制位”的请求,网关会自动退出编程会话,回到默认会话。这个机制很重要:刷写软件半天没动静,总线不能被一个“挂起会话”永久占住,否则生产线上相邻工位互相干扰就乱了。
我自己遇到过一次很隐蔽的坑:上位机发完0x10 02后,又发了一个带抑制正响应位的请求,导致网关认为总线一直有活动,S3Server一直不超时,而实际上上位机已经断线了。后来我把S3Server超时虽然不重置了,但如果连续超时两次,就直接强制回默认会话并置一个诊断标志位,问题才根治。
2.2 安全访问:种子与密钥
安全访问(0x27服务)本质上就是一道“防误操作”的门禁,防止产线上随手一个报文就把固件刷乱。它的流程是:客户端发0x27 01请求种子,服务端返回32位种子;客户端用约定的算法算出密钥,发0x27 02;服务端校验通过后,解锁升级通道。
种子算法不用搞得太玄幻,我在量产项目里用的是比较务实的方案:种子由伪随机数生成器产生,密钥算法用“种子异或固定盐值再叠加一个CRC16”这类方式。这种方式对绝大多数防误操作场景足够了。不过我要提醒一句:安全访问只是“防止误操作”,不是“防黑客破解”。如果真遇到整车级信息安全需求,得上HSM或者至少AES-CMAC,涉及密钥管理和安全启动,那工作量就不是一个Bootloader这么简单了。
还要设计安全访问失败计数器。同一个会话内连续3次密钥错误,就锁定30秒不响应种子请求。这个机制能有效防止诊断仪在产线上反复试探,也可以防止一些乱七八糟的脚本把总线打爆。
2.3 下载流程与ISO-TP分帧
UDS下载的核心流程是一条直线:切换会话→安全访问→请求下载→传输数据→退出传输→例程校验→复位。我把这条链路的每步UDS报文和含义整理成了表:
| 步骤 | UDS服务 | 报文示例 | 作用 |
|---|---|---|---|
| 1 | 10 02 | 02 10 02 00 00 00 00 00 | 切换编程会话 |
| 2 | 27 01 | 02 27 01 00 00 00 00 00 | 请求种子 |
| 3 | 27 02 | 06 27 02 11 22 33 44 00 | 发送密钥 |
| 4 | 34 | 0A 34 00 44 08 00 00 00 00 00 10 00 | 请求下载,指定起始地址和长度 |
| 5 | 36 | 0A 36 01 + 数据 | 传输数据块 |
| 6 | 37 | 02 37 01 00 00 00 00 00 | 请求退出传输 |
| 7 | 31 | 06 31 01 FF 00 00 00 00 | 例程校验固件完整性 |
| 8 | 11 01 | 02 11 01 00 00 00 00 00 | 复位 |
这里最容易让人懵的是0x34的参数解析。0x34后的第一个字节是数据格式标识符(dataFormatIdentifier),0x00表示不压缩不加密;第二个字节是地址和长度格式标识符(addressAndLengthFormatIdentifier),0x44表示地址长度4字节、块长度4字节。所以后面的8个字节里,前4字节是内存起始地址,后4字节是固件总长度。我一开始写解析代码时没注意字节序,把高字节在前还是低字节在前搞反了,结果每次刷写都从错误地址开始,直接把Bootloader区给刷废了。这种低级错误在台架上调试时最容易出现,建议统一用大端字节序,并在代码里加一个编译期静态断言。
数据传输阶段是0x36反复循环。每帧CAN报文最多8字节,ISO-TP单帧只能承载7字节有效载荷;多于一帧就要走首帧+流控帧+连续帧的机制。这里有个性能关键点:ISO-TP首帧可以携带最多4095字节的payload,但CAN FD没普及之前,经典CAN上首帧有效载荷最多是6字节+长度信息,真正的大块数据全靠连续帧运输。所以别把时间浪费在调首帧大小上,重点是把连续帧的间隔和流控块大小调好。
| PCI类型 | 值范围 | 说明 |
|---|---|---|
| 单帧 | 0x0n | n为数据长度(≤7) |
| 首帧 | 0x1n | n为数据总长度高字节 |
| 连续帧 | 0x2n | n为序列号 |
| 流控帧 | 0x3n | n为块大小和间隔时间 |
2.4 关键时间参数与超时设计
UDS刷写的超时设计直接决定刷写成功率。ISO 14229定义了P2Server和P2Server:P2Server是50ms,表示请求发出后最迟50ms内要有响应;P2Server是5000ms,表示某些耗时的服务最多可以拖5秒。Flash擦除通常超过50ms,所以网关侧在做擦除时必须在50ms内先回一个NRC 0x78(responsePending),“我正在忙,别急”,然后在P2*超时内完成真正的响应。
很多自研上位机踩的坑就是对0x78处理不严谨。SocketCAN或周立功的驱动收到0x78后,如果上位机不管它,直接按普通否定响应处理,就会误判为刷写失败。正确做法是在上位机维护一个状态机:收到0x78后继续等待,直到收到正响应或P2*真正超时。
网关侧这些时间参数不是写死在常量表里就行,还要考虑一个实际问题:擦写Flash时,中断和调度可能会被阻塞,导致CAN控制器接收FIFO溢出。我的做法是擦写期间关掉UDS请求的接收中断?不行,这样容易丢帧。正确方案是把擦写Flash的操作放到一个较低的优先级任务里,而CAN接收中断保持在最高优先级;同时,每擦完一个扇区就回到主循环喂一下看门狗、清一下接收FIFO,保证诊断仪发来的连续帧不丢。
3. LIN从机OTA:从CAN肚子里搬运固件到LIN总线上
3.1 LIN从机升级的难点
LIN从机OTA是整个方案里最“拧巴”的部分,难点不是写代码,而是理解LIN总线的“专制”本质。LIN是主从式总线,所有通信都由主节点发起,从机只能被动响应。也就是说,LIN从机不能像CAN节点那样主动说“我准备好了”或者“我再发一次”,它只能等主节点给它发帧头,然后才在规定的时隙里回数据。
这种机制直接带来两个后果。第一,网关必须负责把升级数据切成一片一片,按固定调度周期发给从机,并且每一片都要等从机回ACK;不能像在CAN上那样一口气连发几百个连续帧。第二,从机规模小,很多从机MCU连DMA都没有,Flash驱动代码写起来要“抠”。我甚至见过一个从机项目,RAM只有2KB,Bootloader里除法都不敢用,全靠循环移位做CRC,因为编译器一开优化就爆RAM。
还有带宽问题。LIN典型波特率是19200bps,一个完整LIN帧大约12字节(同步间隔、同步字段、PID、数据、校验和),算下来一帧约5ms,有效数据还不到8字节。如果你的从机固件是64KB,光传数据就得几分钟。这对用户体验和产线节拍都是灾难。所以我的原则是:LIN从机OTA只适用于小固件(几KB到几十KB),如果固件超过128KB,我建议要么提高LIN波特率到38400,要么换带CAN的从机芯片,别硬扛。
3.2 调度表与诊断帧设计
LIN总线的核心概念是调度表(Schedule Table)。平时网关跑正常调度表,里面放着车窗控制、灯光状态这些常规帧;一旦进入刷写模式,网关就要切换到编程调度表,把总线时间大部分让给诊断帧。
LIN诊断帧有两个固定ID:主节点请求帧0x3C(MasterReq)和从节点响应帧0x3D(SlaveResp)。主节点通过0x3C把请求和固件数据发出,数据格式一般是:NAD(节点地址)+ SID(服务ID)+ 补充字节 + 用户数据。从机的响应则通过0x3D回传。
我实际用的刷写调度表是这样的:先连续发若干个0x3C请求帧,再发一个0x3D帧等响应,中间穿插一两个其他控制帧,防止车窗等下位机在刷写期间“死掉”。这个穿插很关键——有一次我纯发诊断帧刷了一个从机,刷完后发现同一条LIN上的车窗模块因为长时间没收到自己的帧头,自己给自己报了个超时故障码,把我折腾了一天。后来在每个刷写周期里插入至少10%的正常控制帧,问题再没出现。
调度表的C语言定义大致长这样:
typedef struct { uint8_t frame_id; uint16_t slot_time_us; /* 每个帧占用的时隙,LIN 19200bps时至少5000us */ } lin_schedule_entry_t; const lin_schedule_entry_t normal_schedule[] = { {0x01, 10000}, /* 车窗状态 */ {0x02, 10000}, /* 灯光控制 */ {0x3C, 10000}, /* 诊断帧 */ {0x3D, 10000}, /* 诊断响应帧 */ }; const lin_schedule_entry_t programming_schedule[] = { {0x3C, 10000}, /* 主请求帧 */ {0x3C, 10000}, {0x3C, 10000}, {0x3D, 10000}, /* 等从机响应 */ {0x01, 10000}, /* 留一个窗口给车窗,避免超时 */ };从机端要识别自己是不是升级对象,靠NAD地址。网关收到诊断仪发来的UDS 0x34请求后,会根据目标地址判断是刷网关自己还是刷哪个LIN从机。如果是LIN从机,就把这个请求映射成对应的NAD地址,然后切换调度表到编程调度表。SocketCAN那一侧完全感知不到差异——诊断仪看到的还是一个标准UDS节点。
3.3 网关中转转发的状态机
网关中转是整个方案里最容易写乱的部分。我一开始想简单:CAN侧来一包0x36,我拆成LIN帧发出去不就完了吗。结果被现实狠狠教育了——CAN侧一包可能带21字节(ISO-TP多帧缓冲),LIN侧一帧最多能放8字节,还要去掉NAD和SID,有效payload只有4字节左右。如果只做“转发”,一次0x36可能会对应5、6个LIN帧,而且从机每个LIN帧都要ACK,总不能发完一个就干等5ms,那样一包0x36就得等30ms。
我的解决思路是在网关里加一个中转状态机:
typedef enum { LIN_IDLE, LIN_ERASE_REQ, LIN_ERASE_WAIT, LIN_DATA_SEND, LIN_DATA_ACK, LIN_CHECK_REQ, LIN_CHECK_WAIT, LIN_RESET_REQ, LIN_COMPLETE } lin_ota_state_t;状态机从LIN_IDLE开始,收到CAN侧UDS的下载请求后,进入LIN_ERASE_REQ,发擦除命令给从机;然后轮询0x3D等从机擦除完成。擦除完成后进入LIN_DATA_SEND,把CAN侧0x36收到的大块固件按4字节一个小包发到从机,每发一包等ACK,超时50ms就重发,连续3次失败直接终止升级并给CAN侧回报NRC。这中间CAN侧0x36的响应时机很讲究:不能从机还没ACK就回正响应给诊断仪,否则诊断仪认为数据已经写进去了,实际上从机可能早就掉线了。
一个很重要的小细节:诊断仪侧看0x36的响应时间,不能超过P2Server=50ms。但LIN侧每包5ms+ACK等待,有时候确实会超过。所以网关侧对0x36的响应要灵活处理:要么在中转状态机里把数据先缓存下来,等收到完整一块后再回响应;要么在超过50ms前先回NRC 0x78,等这一块真正写完成后再回正响应。我测试下来,后一种方式对诊断仪兼容性更好,因为很多诊断仪对“久等不回”非常敏感,但能正确处理0x78。
3.4 从机端Bootloader的实现要点
从机侧Bootloader是真正考验单片机基本功的地方。我从一个量产从机项目里提炼出的要点就三条:芯片选型阶段就要确认Flash擦写的最小粒度、擦除时间、以及写Flash期间是否必须关中断。
以我用的CH582为例,Flash按页擦除,每页大小256字节,整片擦除时间在几十毫秒量级。写Flash时必须把中断关掉,但关中断不能关太久,否则LIN接收也会丢数据。所以我把写Flash的过程拆成“每次写4字节,写完马上开中断,在时间片里跑一下协议栈,再关中断写下一批”。如果你用STM32G030这类芯片,注意不要在一个扇区写的时候被看门狗打断,最好把写Flash操作放到一个可以重新触发看门狗的循环里。
从机启动时判断要不要进Bootloader,这个逻辑不能太敏感。我的做法是:参数区里放一个4字节魔数和升级次数计数。正常情况下上电,Bootloader读到魔数不对就直接跳App;只有收到网关发来的“进入编程模式”命令,才会在参数区写上魔数并复位。这样既能保证正常启动不被拖慢,又能让刷写软件随时可以把从机拉回Bootloader。
从机端的LIN下载协议我做得尽量轻:请求版本号、擦除Flash、写Flash、校验CRC、跳转App,一共5个服务ID。不需要UDS那一整套,因为从机的资源不支持,也容易把Bootloader代码撑大。网关负责把CAN侧的UDS请求“翻译”成这5个服务,职责清晰,调试也方便。
4. 实操中的坑:故障排查与经验笔记
4.1 常见刷写失败排查清单
我把这段时间调试刷写链路遇到的高频问题整理成一张表,基本覆盖了90%的刷写失败场景:
| 现象 | 可能原因 | 排查手段与解决 |
|---|---|---|
| CAN无响应 | ID映射错误、波特率不一致、终端电阻缺失 | 用CAN抓包工具看诊断仪发出的报文ID,确认网关侧过滤ID配置 |
| 安全访问失败 | 种子/密钥算法不一致、字节序错误 | 在网关侧打印种子和计算出的密钥,对比诊断仪计算结果 |
| Flash擦写失败 | 地址越界、未对齐、Flash被锁 | 检查0x34中地址是否为扇区首地址,确认擦除扇区不越界 |
| LIN无响应 | NAD不对、调度表未切换、波特率超差 | 示波器抓LIN物理层波形,确认PID是否正确匹配目标从机 |
| 刷写中途断线 | 看门狗复位、接收FIFO溢出、总线上有其他干扰 | 把喂狗放到定时器中断;擦写期间提高CAN接收中断优先级 |
| 升级后从机功能异常 | App地址不对、中断向量表没重映射 | 确认编译链接脚本中APP起始地址和Bootloader跳转地址一致 |
最经典的一个坑是:LIN从机明明回ACK了,但网关显示发送失败。查到最后发现是LIN的波特率误差超过了2%。LIN协议要求从机时钟误差不能超过±1.5%,我用了一个国产芯片内部RC振荡器,在低温环境下跑了几个月后,波特率漂到了标的1.8%,就偶发失败。后来所有从机项目我都改用外部晶振,或者在Bootloader里加了一个自动波特率校正逻辑,问题才从根上解决。
4.2 掉电保护与恢复
掉电保护是OTA方案里“生死攸关”的一环。我设计的核心思想就一句话:系统里必须随时保留一个“能刷写的状态”,升级失败不能把Bootloader和App都搞没。网关和从机的Bootloader里都要有升级状态字,这个状态字在每次升级前先写“升级中”,升级成功后再写“升级完成”。
实际流程是这样的:网关收到新固件后,先把固件完整存在Backup区,校验CRC通过,然后把状态字置为“准备切换”,擦除App区并写入新固件。如果中途掉电,Bootloader上电后读状态字是“准备切换”,就知道App区是不可信的,直接从Backup区恢复。对内存不够做Backup区的小从机,我的方案是“先擦后写”的窗口尽量缩短,把新固件先缓存在外部EEPROM或者RAM的镜像区,从机Flash擦除后立即写,写满了再标记完成。这样做虽然不能100%避免掉电变砖,但能把风险窗口从“整个刷写过程”缩小到“几毫秒的Flash写操作”。
我还做了一款自动化掉电测试小工具:用一个继电器控制网关电源,在刷写的不同阶段随机断开,循环几千次,确认每次重启后Bootloader都能正确恢复。这个测试是量产前必须过的,别省。
4.3 测试与验证方案
台架测试的核心工具是“PC + USB-CAN分析仪 + Python脚本”,我用的是周立功的USBCAN-II,配合python-can库。这个组合的好处是比CANoe轻量,且易于集成到CI流程里。下面这段代码是我用来做CAN诊断刷写冒烟测试的简化版本:
import can import time bus = can.interface.Bus(channel=0, bustype='pcan', bitrate=500000) def uds_send(bus, can_id, data, wait=True): ext = False msg = can.Message(arbitration_id=can_id, data=data, is_extended_id=ext) bus.send(msg) if wait: # 等待响应,实际项目中要处理P2/P2*超时和NRC 0x78 resp = bus.recv(timeout=5) return resp # 切换到编程会话 uds_send(bus, 0x710, [0x02, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00]) time.sleep(0.1) # 请求种子 resp = uds_send(bus, 0x710, [0x02, 0x27, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]) # 计算出密钥并发送 seed = int.from_bytes(resp.data[3:7], 'big') key = (seed ^ 0x5A5A5A5A) & 0xFFFFFFFF key_bytes = key.to_bytes(4, 'big') uds_send(bus, 0x710, [0x06, 0x27, 0x02] + list(key_bytes) + [0x00])在实际项目里,我用CAPL脚本也在CANoe里跑过一版,主要是为了做图形化的波形分析和LIN总线干扰测试。但日常开发和缺陷定位,Python这套更快,出了问题直接打印数据,不用等CANoe工程加载。如果要从整车厂的OTA包里提取固件镜像,我参考过“OTA提取器”的思路:拿全量包解包后定位到目标ECU的payload,再做解压和格式转换,转成标准UDS下载序列。
4.4 性能与安全性补充
刷写性能是方案好不好的直接感受。CAN侧500kbps下,一个512KB的固件镜像,走ISO-TP连续帧,理论上不到20秒就能传完;但你要是把块大小设成每次只传4字节,时间会翻三四倍。我的经验是把块长度设成1024字节以上,连续帧发4个就等一次流控,这样既不会把总线打爆,又能保持较高吞吐。
LIN侧的传输性能要现实很多。19200bps下,一个LIN帧周期约10ms,还算上从机的ACK等待,每包有效数据假设4字节,传64KB需要约4分钟。这个速度在售后刷写场景可以接受,但在产线上就不太好看了。所以我对量产的从机固件做了压缩处理,在网关侧用LZ4或者LZSS压缩,从机Bootloader里解压回写,实测64KB的镜像能压到20KB左右,刷写时间直接砍半。如果你正在做LIN从机OTA,这一步建议提前规划,能省下大量生产节拍。
安全方面,我维护三件套:CAN侧用安全访问+完整性CRC32校验,LIN侧从机和网关之间用私有协议自带握手和每包ACK,整个升级包在进入网关前可以再做一层整车厂的签名校验。不管哪一层失败,都要在诊断仪上给出明确的故障码,方便质量部门追踪。这不是过度设计,量产车上因为刷写失败返工的案例太多了,多一道校验,少一堆售后电话。
最后分享一点实在的体会
整套方案做下来,我最深的感受是:网关刷自己其实不难,难的是网关当“中转站”刷别人,因为它手里握着总线调度权,一旦处理不好就把整条LIN上的节点都拖下水。所以我的建议是,第一版先别急着上OTA,把CAN侧UDS刷写跑通,再用一台仿真从机验证LIN转发逻辑,最后才接真实从机。每一步都留好调试口:网关侧留一个串口日志,从机侧留一个错误码DTC,诊断仪上能看到每一层发生了什么,排查问题才会快。
再分享一个小技巧,我的Bootloader里常驻一个“升级状态字”,每次升级开始和结束都会更新它。这个状态字看起来不起眼,但它救了我很多次——出问题后拿CANoe一读就知道是从机没进Bootloader、还是Flash写了一半、还是校验不过,不用盲猜。如果你也在做类似的升级项目,建议从第一天就把这个状态字设计进去。