1. 项目概述:当无线遇上有线,构建混合式固件升级网络
在汽车电子和工业物联网项目中,我们常常会遇到一个混合网络:一部分节点(比如车载信息娱乐主机、工业网关)具备蓝牙或Wi-Fi等无线连接能力,可以方便地从云端服务器获取最新的固件;而另一部分节点(比如车门控制模块、传感器节点)则可能只配备了LIN或CAN这类可靠的有线总线接口,它们自身无法直接访问外部网络。当需要对整个网络中的所有设备进行固件升级时,如何让“有网”的节点帮助“没网”的节点完成更新,就成为一个非常实际的工程挑战。
NXP的KW36和KW38系列无线微控制器为解决这个问题提供了一个优雅的硬件平台。这两款芯片不仅集成了低功耗蓝牙5.0,还内置了支持LIN协议的LPUART模块和支持CAN FD的FlexCAN模块。这意味着,单个芯片就能同时扮演“无线下载网关”和“有线分发中心”两个角色。本方案的核心思路就是:让一个具备蓝牙OTAP(空中编程)能力的KW36/38节点(我们称之为主节点或Node A)通过蓝牙从手机或服务器下载新固件,然后通过LIN或CAN总线,将固件镜像可靠地传输给网络内其他不具备无线升级能力的从节点(Node B),最终引导从节点完成固件的存储与切换。
这不仅仅是简单的数据转发。整个流程涉及无线协议栈、有线总线驱动、非易失性存储管理、bootloader协同工作以及一套保证传输可靠性的应用层协议设计。我在多个汽车ECU升级项目中实践过类似架构,深知其中从镜像格式处理、存储空间划分到传输状态机设计的每一个细节都关乎升级的成败。接下来,我将拆解整个实现过程,分享从驱动移植到系统测试的完整经验,特别是那些在官方文档之外容易踩坑的实操要点。
2. 系统架构与核心设计思路拆解
在动手写代码之前,我们必须先厘清系统的数据流和各个模块的职责。一个清晰的架构是成功的一半,尤其是在这种涉及多协议、多状态切换的嵌入式系统中。
2.1 数据流向与节点角色定义
整个升级系统的数据流可以概括为“云端->无线->有线->节点”。首先,带有新固件的OTA文件通过手机APP(如NXP IoT Toolbox)或后台服务器,经由蓝牙连接下发到作为OTAP客户端的KW36主节点。主节点在完成蓝牙传输后,不会立即重启应用新固件,而是先解析OTA文件头中的一个关键字段——Image Identifier。这个标识符就像快递单上的“收件人地址”,告诉主节点这个固件是给它自己的,还是需要转发给总线上的其他兄弟节点。
如果Image Identifier指向主节点自身(例如默认值0x0001),那么流程就和标准的蓝牙OTAP一样:设置标志位,重启,由bootloader将固件从临时存储区搬移到程序闪存。但如果标识符指向的是总线从节点(例如我们定义为0x000A),故事就转向了第二部分:主节点启动LIN或CAN总线传输任务,将刚刚接收到的完整固件镜像,通过有线总线,分块发送给目标从节点。
从节点(LIN Slave或CAN Node B)在总线上持续监听。一旦收到主节点发来的“开始升级”命令,便进入数据接收模式。它需要将接收到的数据块暂存于RAM缓冲区,攒够一个块(比如1KB)后,再写入到外部EEPROM或内部Flash的指定区域。每成功接收并存储一个数据块,它都需要向主节点回复一个确认状态。当收到“结束升级”命令后,从节点需要像主节点一样,在存储区的头部写入镜像长度、扇区位图等信息,并设置自己的bootloader标志位,最后重启以完成固件切换。
2.2 关键设计决策与考量
为什么选择“先无线下载,再有线分发”的架构?最直接的原因是资源与成本约束。为网络中每一个节点都配备无线模块会显著增加BOM成本和功耗。而利用一个中心网关进行分发,是最经济高效的方案。LIN和CAN总线本身就是为了汽车这种高可靠、实时性要求高的环境设计的,它们的物理层和链路层已经保证了在复杂电磁环境下的通信可靠性,这为传输大体积的固件镜像提供了坚实的基础。
在存储方案上,我们面临内部Flash和外部EEPROM的选择。KW36/38的内部Flash通常为512KB或1MB,除了存放应用程序本身,还要划出一块区域作为OTAP缓存区。如果固件本身较大(超过200KB),再划出另一块区域来暂存待分发的镜像可能会非常紧张。因此,对于主节点,如果它需要缓存一个待转发的大镜像,使用外部SPI Flash(如板载的AT45DB041E)通常是更稳妥的选择。对于从节点,如果其固件较小,且内部Flash有充足余量,则可以使用内部Flash作为升级目标区,以节省一颗外置芯片的成本。这个选择需要在项目初期根据固件大小和硬件设计明确下来,因为它直接影响链接脚本和存储驱动层的配置。
传输协议的设计是另一个核心。无论是LIN还是CAN,其单帧数据负载都很有限(LIN为8字节,经典CAN也为8字节,CAN FD最多64字节)。直接逐帧发送固件数据效率极低,且每帧都等待应答会引入巨大开销。因此,我们必须采用“块传输”策略。主节点从存储区一次性读取一个数据块(例如1KB)到RAM,然后将其拆分成数十个甚至上百个总线数据帧连续发送。从节点在接收端同样用RAM做缓冲,攒够一个完整块后再执行耗时的Flash写入操作。块传输结束后,从节点回复一个针对整个块的确认状态和下一个期望的块序列号。这种设计在可靠性和效率之间取得了很好的平衡,也是本方案能实际应用的关键。
3. 开发环境搭建与驱动基础
工欲善其事,必先利其器。在开始实现升级逻辑之前,我们需要一个可工作的基础工程,它应该已经包含了蓝牙OTAP和LIN/CAN总线通信的基本能力。
3.1 SDK获取与基础工程准备
首先,从NXP官网的MCUXpresso SDK Builder页面,根据你的具体芯片型号(FRDM-KW36或FRDM-KW38)下载最新的SDK。我建议直接使用MCUXpresso IDE,因为它对NXP芯片的支持最为完整,包括链接脚本的图形化配置,这在后面划分存储区域时会非常方便。当然,使用IAR Embedded Workbench也是完全可行的,只是部分配置需要在选项对话框中手动完成。
我们需要两个基础工程模板:一个是用于主节点的“蓝牙OTAP客户端”工程,另一个是用于从节点的“基础框架”工程。在KW36 SDK中,wireless_examples\bluetooth\otac_att这个目录下的工程已经实现了完整的蓝牙OTAP客户端功能,它是我们主节点工程的完美起点。对于从节点,它不需要蓝牙功能,但需要LIN或CAN驱动以及存储操作功能。我们可以从driver_examples目录下复制LIN Slave或FlexCAN的驱动示例工程,但更高效的做法是,直接使用otac_att工程作为基础,然后移除其蓝牙相关的源文件和配置,只保留框架、RTOS(如果使用)和存储驱动部分。官方应用笔记AN12948中提供的示例代码采用了后一种方法,将不同功能的项目放在同一目录下管理,方便共享公用代码。
注意:在移植LIN/CAN驱动到蓝牙工程框架时,最大的挑战是中断和低功耗管理的协调。蓝牙协议栈有其自己的定时器和事件调度系统(如低功耗定时器服务)。LIN/CAN驱动,特别是中断模式下,会注册自己的中断服务例程。你需要确保两者不发生冲突,并且当总线通信活跃时,设备不会进入太深的低功耗模式而导致通信失败。一个实用的做法是,在LIN/CAN传输期间,通过调用
PWR_DisallowDeviceToSleep()函数临时禁止蓝牙栈进入深度睡眠。
3.2 LIN与CAN驱动关键配置解析
LIN和CAN的驱动配置是通信稳定的基石,这里有几个参数需要特别注意。
对于LIN总线,核心是配置主从节点的通信参数匹配。在lin_cfg.h和lin_cfg.c中,你需要定义任务调度表。在本方案中,我们至少需要定义三个无条件帧:
- 命令帧:用于主节点向从节点发送开始/结束升级等控制指令。
- 状态帧:用于从节点向主节点回复当前接收状态和下一个期望的块序列号。
- 数据帧:用于承载实际的固件数据。
你需要根据总线上实际的节点数量和网络负载来合理设置这些帧的调度位置和发送间隔。LIN的波特率通常设置为20kbps,但在固件升级这种对时间不敏感的后台任务中,为了更高的可靠性,可以适当降低到19.2kbps。调用LIN_GetMasterDefaultConfig()或LIN_GetSlaveDefaultConfig()后,务必检查并确认baudRate字段的值是否符合你的硬件设计。
对于CAN总线,配置更为灵活。首先,你需要决定使用经典CAN还是CAN FD。如果追求极致的升级速度,CAN FD是更好的选择,因为它单帧最大数据长度可达64字节,是经典CAN的8倍。在flexcan_interrupt_transfer.c中,通过定义USE_CANFD宏为1来启用CAN FD模式。其次,标准ID(11位)的分配需要规划。主节点(Node A)的发送ID和从节点(Node B)的接收ID必须相同,反之亦然。例如,可以定义:
// Node A #define CAN_TX_IDENTIFIER (0x123) // A发给B用的ID #define CAN_RX_IDENTIFIER (0x321) // A接收B回复用的ID // Node B #define CAN_TX_IDENTIFIER (0x321) // B回复A用的ID #define CAN_RX_IDENTIFIER (0x123) // B接收A数据用的IDCAN的波特率可以设置得很高,1Mbps是常见选择,这能极大缩短传输时间。在FLEXCAN_GetDefaultConfig()之后设置bitRate和bitRateFD(如果启用FD)时,要确保总线上的所有节点,包括可能存在的其他ECU,都支持并配置了相同的波特率,否则通信无法建立。
4. 镜像的获取、解析与存储管理
固件镜像在整个流程中经历了多次形态转换:从编译生成的二进制文件,到添加了OTA头信息的传输文件,再到被拆分成数据块在总线上传输,最后被重组写入存储介质。理解并处理好每一个环节是升级功能可靠的前提。
4.1 OTA文件生成与节点标识
编译器生成的是纯粹的应用程序二进制文件(.bin)。为了支持无线升级,我们需要为其添加一个OTA文件头。这个头文件包含了镜像的元数据,对于本方案至关重要。使用NXP Connectivity Test Tool(一个基于PC的实用工具)可以方便地生成OTA文件。在工具中,你需要加载.bin文件,并关键是要设置Image Identifier。
这个标识符就是整个升级流程的“路由标签”。在主节点的代码中(otap_interface.h),我们通常会定义两个常量:
#define gBleOtaImageIdForSelf_c (0x0001U) // 给自己升级的镜像ID #define gBleOtaImageIdForLinCanNode_c (0x000AU) // 给LIN/CAN从节点升级的镜像ID当主节点的蓝牙OTAP客户端完成下载并解析文件头时,它会调用OtapClient_IsImageFileHeaderValid()等函数来检查这个ID。如果匹配到gBleOtaImageIdForLinCanNode_c,它就知道这个镜像是需要转发的,从而触发后续的总线传输流程,而不是直接重启自己。
实操心得:务必在手机APP(如IoT Toolbox)上传OTA文件时,或在后台服务器生成OTA文件时,就正确设置这个Image Identifier。我遇到过因为测试时误用了“给自己升级”的镜像文件,导致主节点不断尝试把错误的固件发给从节点,最终导致从节点变砖的情况。建议在开发阶段,将主节点和从节点的镜像ID差异设置得大一些,并在串口日志中明确打印出来,便于调试。
4.2 存储方案选择与链接脚本配置
这是最容易出错的环节之一,需要分别在IDE的工程配置和源代码预编译定义两个层面进行设置。
方案一:使用内部Flash存储如果你的固件体积不大,且芯片Flash有充足空间,使用内部Flash是最简单、成本最低的方案。你需要从程序Flash中划出一块独立的区域,专门用于存放“待升级的镜像”。对于主节点,这块区域存放的是它从蓝牙接收到的、准备转发给从节点的镜像。对于从节点,这块区域存放的是它从总线接收到的、准备替换自身旧程序的镜像。
在IAR中配置:
- 在应用工程的
app_preinclude.h中定义:#define gEepromType_d gEepromDevice_InternalFlash_c。 - 在工程选项
Options > Linker > Config中,编辑链接器配置文件,确保定义了gUseInternalStorageLink_d=1和gEraseNVMLink_d=0。这告诉链接器为OTAP存储保留空间。 - 在bootloader工程的
Options > C/C++ Compiler > Preprocessor中添加相同的宏定义。
在MCUXpresso IDE中配置更为直观:
- 同样在
app_preinclude.h中定义gEepromType_d。 - 右键工程,进入
Properties > C/C++ Build > MCU Settings。在这里你可以图形化地管理内存布局。找到PROGRAM_FLASH,点击“Split”按钮,将其分割成两部分。一部分命名为APP_FLASH用于存放当前运行的程序,另一部分命名为OTAP_STORAGE用于存放新镜像。你需要根据芯片手册和固件大小,仔细计算并设置这两部分的大小和起始地址。 - 在应用工程的链接器杂项设置中,添加
--defsym=gUseInternalStorageLink_d=1。 - 修改
project/linkscripts/end_text.ldt文件,移除对OTAP存储区域的填充指令(即FILL(0xFFFFFFFF)和BYTE(0xFF)那几行),否则该区域无法被正确编程。
方案二:使用外部EEPROM存储当固件较大或内部空间不足时,外部EEPROM(如板载的AT45DB041E SPI Flash)是必选方案。其配置相对简单,因为不需要分割内部Flash。
在IAR中配置:
app_preinclude.h中定义:#define gEepromType_d gEepromDevice_AT45DB041E_c。- 链接器配置中设置
gUseInternalStorageLink_d=0。 - Bootloader中同样预定义
gEepromType_d=gEepromDevice_AT45DB041E_c。
在MCUXpresso IDE中配置:
app_preinclude.h定义同上。- 链接器杂项设置
--defsym=gUseInternalStorageLink_d=0。 - 确保
MCU Settings中PROGRAM_FLASH区域只包含应用程序本身,无需分割。 - 同样需要修改
end_text.ldt文件。
避坑指南:无论选择哪种方案,主节点和从节点的存储配置是独立的。一个常见的错误是,主节点用了外部Flash,而从节点的工程却错误地配置为使用内部Flash,导致从节点无法正确写入接收到的数据。务必为两个工程分别检查上述配置。另外,使用外部Flash时,首次下载程序前需要确保Flash驱动(在SDK的
middleware目录下)已正确添加到工程,并且SPI引脚配置与开发板原理图一致。
4.3 镜像大小优化技巧
固件镜像的大小直接决定了无线下载和有线传输的时间,也影响着对存储空间的需求。在资源紧张的嵌入式环境中,优化镜像体积是必修课。
编译器优化:这是最直接有效的方法。在IAR中,进入Options > C/C++ Compiler > Optimizations,将优化等级设置为High或Balanced。在MCUXpresso IDE中,进入Properties > C/C++ Build > Settings > Tool Settings > MCU C Compiler > Optimization,选择Optimize for size (-Os)。这通常能减少10%-30%的代码体积。
功能裁剪:仔细审视你的应用。用于升级的从节点固件,是否包含了所有调试日志、非必要的中间件组件或未使用的驱动?对于最终量产版本,可以移除调试接口 (DEBUG宏)、裁剪掉不用的蓝牙服务或文件系统模块。KW36的蓝牙协议栈本身是库文件,但你的应用层可以做到极简。
链接器垃圾回收:确保启用链接器的“垃圾回收”功能。在IAR中,勾选Options > Linker > Advanced > Enable dead code elimination。在MCUXpresso中,它通常是默认开启的。这可以移除从未被调用到的函数和数据,对于库文件尤其有效。
合理使用const和存储段:将大量的常量数据(如字体、图片、字符串)声明为const并放置到特定的Flash段,避免它们被拷贝到RAM中。同时,检查全局变量和缓冲区的大小,是否存在不必要的浪费。
经过这些优化,一个典型的从节点控制程序,完全有可能从200KB压缩到150KB甚至更小。这意味着更短的升级时间、更低的传输错误概率,以及可能让你从必须使用外部Flash的窘境,变回可以选择内部Flash的从容。
5. 总线传输协议与状态机实现
这是整个方案的核心逻辑层,它建立在稳定的驱动之上,负责将庞大的固件镜像拆解、传输、校验,并确保整个过程可靠。我们将分别设计LIN和CAN两套相似但略有不同的应用层协议。
5.1 LIN总线传输协议设计
LIN总线是主从架构,通信完全由主节点调度。我们设计三个专用的无条件帧,并安排它们在调度表中周期性地出现。
1. 帧定义与调度表配置在lin_cfg.h中定义帧ID和数据结构:
/* 定义用于OTA的LIN帧ID */ #define gID_OtapCmd_c 0x20 // 命令帧:主->从,发送控制命令 #define gID_OtapGetStatus_c 0x21 // 状态帧:从->主,回复状态 #define gID_OtapData_c 0x22 // 数据帧:主->从,发送镜像数据 /* 命令枚举 */ typedef enum { linOtaCmd_Start = 0x01, linOtaCmd_End = 0x02, linOtaCmd_Abort = 0x03 } lin_ota_cmd_t; /* 状态枚举 */ typedef enum { linOtaStatus_Idle = 0x00, linOtaStatus_Receiving = 0x01, linOtaStatus_BlockOk = 0x02, linOtaStatus_Error = 0xFF } lin_ota_status_t;在lin_cfg.c的调度表中,你需要将这三个帧添加进去,并设置合适的发布间隔。数据帧gID_OtapData_c的发布周期应该尽可能短,以最大化利用总线带宽。
2. 块传输与状态机由于LIN单帧只有8字节有效数据,我们采用“块传输”策略。主节点定义一个RAM缓冲区(如1KB),每次从存储区(Flash或EEPROM)读取一个块的数据到缓冲区,然后通过gID_OtapData_c帧连续发送出去。这里的关键是“连续发送”,即主节点在发送一个块的数据期间,不应被调度表中的其他帧(如状态查询)过度打断。这可以通过精心设计调度表,或将数据帧的优先级设为最高来实现。
从节点同样有一个接收缓冲区。它接收数据帧,并填充缓冲区。每收满一个块,它就执行一次耗时的非易失性存储写入操作。写入成功后,它通过gID_OtapGetStatus_c帧,将状态linOtaStatus_BlockOk和下一个期望的块序列号回复给主节点。主节点收到确认后,才继续发送下一个块。
我们需要一个状态机来管理这个流程。状态包括:空闲、等待开始确认、传输数据块、等待块确认、传输完成、错误处理等。状态机确保了传输过程的有序性和可恢复性。例如,如果主节点在发送一个块的过程中,没有在预期时间内收到从节点的BlockOk状态,它应该重发整个块,并在重试多次后触发错误处理流程。
3. 代码实现要点在主节点的lin_cfg.c中,你需要实现LinOtaStartCallback()函数,这是蓝牙OTAP完成后的入口。在这个函数里,初始化传输状态机,启动LIN调度表,并发送linOtaCmd_Start命令。
// 伪代码示例:主节点发送一个数据块 void SendOneBlock(uint32_t block_num) { uint32_t read_addr = base_addr + block_num * BLOCK_SIZE; EEPROM_Read(read_addr, g_ota_tx_buffer, BLOCK_SIZE); // 从存储读取一个块 for(int i=0; i<BLOCK_SIZE/DATA_PER_FRAME; i++) { // 将缓冲区数据拆分到多个LIN数据帧中 lin_frame_t data_frame; data_frame.id = gID_OtapData_c; memcpy(data_frame.data, &g_ota_tx_buffer[i*8], 8); data_frame.dlc = 8; LIN_SendFrame(MASTER_INSTANCE, &data_frame); // 发送一帧 // 这里可能需要根据调度表做短暂延时 } g_current_state = STATE_WAIT_BLOCK_ACK; // 进入等待确认状态 }从节点则在LIN中断或轮询中,检查接收到的帧ID。如果是gID_OtapData_c,就将数据存入接收缓冲区;如果是gID_OtapCmd_c,则根据命令切换状态。
5.2 CAN总线传输协议设计
CAN总线是对等多主架构,通信更灵活,带宽也更高。我们的协议设计可以更高效。
1. 报文ID与数据场定义我们定义几种专用的CAN报文。与LIN不同,CAN报文的ID本身不直接代表“命令帧”或“数据帧”,而是代表发送节点。命令类型和数据内容都放在数据场中。
// CAN通用命令,放在数据场的第一个字节 typedef enum { CAN_GEN_CMD_OTA_CMD = 0xA0, // 升级命令 CAN_GEN_CMD_OTA_DATA = 0xA1, // 升级数据 CAN_GEN_CMD_OTA_STATUS = 0xA2, // 升级状态 CAN_GEN_CMD_GET_DEV_ID = 0xA3 // 获取设备ID(用于多节点) } can_general_cmd_t; // 数据帧结构示例 (Node A -> Node B) // 使用CAN FD,假设数据场长度为11字节 // Byte0: CAN_GEN_CMD_OTA_DATA (0xA1) // Byte1-2: 帧序列号 (uint16_t, 大端或小端需统一) // Byte3-10: 8字节镜像数据CAN FD允许更长的数据场(最多64字节)。我们可以充分利用这一点,将每个CAN数据帧携带的有效数据提升到32甚至60字节,从而大幅减少传输所需的总帧数,提升效率。只需在flexcan_interrupt_transfer.c中定义USE_CANFD为1,并配置好FD的波特率即可。
2. 可靠传输与流控制CAN总线虽然有CRC校验和应答机制保证帧级别的可靠性,但在应用层,我们仍需确认每一个数据块都被正确接收和存储。因此,我们借鉴TCP的确认机制:Node B在成功接收并存储一个数据帧后,需要立即回复一个ACK帧。这个ACK帧可以复用CAN_GEN_CMD_OTA_DATA命令,但在数据场中用特定字节表示ACK(例如,Byte1 = 0x00表示ACK,0xFF表示NAK)。
为了进一步提升效率,可以采用“滑动窗口”协议。Node A可以连续发送多个数据帧(比如一个窗口,包含10帧),然后再等待这些帧的批量确认。这减少了等待ACK的空闲时间,尤其在高延迟或需要支持多个从节点的网络中效果显著。当然,这也会增加协议的复杂性,需要维护发送和接收窗口。
3. 多从节点升级支持这是CAN方案相比LIN的一个优势。在一条CAN总线上,可以挂载多个需要升级的相同型号的Node B。协议需要增加设备发现和寻址机制。
- 发现阶段:Node A广播
CAN_GEN_CMD_GET_DEV_ID命令。所有Node B收到后,随机延时(如0-1020ms)后回复自己的唯一设备ID(例如,可用蓝牙MAC地址的低16位)。Node A收集所有回复的ID。 - 寻址升级:Node A根据收集到的ID列表,依次对每个Node B进行升级。在发送数据帧或命令帧时,可以将目标设备的ID放入数据场的特定字节,实现逻辑寻址。每个Node B只处理目标ID与自己匹配的报文。
- 串行升级:必须采用串行方式,即升级完一个再升级下一个。如果同时向多个节点发送不同的数据块,会引发总线冲突和数据混乱。虽然CAN有多主仲裁机制,但用于固件升级这种强顺序性的数据流,串行是最简单可靠的。
5.3 状态机与错误处理通用设计
无论是LIN还是CAN,一个健壮的状态机都是必不可少的。状态机至少应包含以下状态:
- IDLE:空闲状态,等待升级开始命令。
- PREPARE:准备状态,初始化缓冲区,检查存储空间。
- TRANSFER:传输状态,正在发送/接收数据块。
- WAIT_ACK:等待对方确认状态。
- VERIFY:传输完成,进行完整性校验(如CRC校验)。
- SWITCH:校验通过,设置标志位,准备重启切换。
- ERROR:发生错误(超时、校验失败、存储错误等)。
错误处理策略必须明确:
- 超时重传:在
WAIT_ACK状态设置定时器。超时未收到确认,则重传当前数据块。重传次数应有上限(如3次)。 - 校验机制:除了总线自带的CRC,应用层应在每个数据块或整个镜像传输结束后,计算并比对CRC32。这能捕获存储介质读写错误。
- 断点续传:这是一个高级功能。可以在每个成功写入的块后,在存储器的特定位置(如EEPROM的最后一个扇区)记录当前已接收的块号或文件偏移。当升级意外中断(如断电)后重新上电,可以从该断点继续接收,而不是从头开始。这需要主从节点双方都支持此逻辑。
6. 镜像切换与Bootloader协同工作
当最后一个数据块传输并校验通过后,从节点收到了主节点发来的“结束传输”命令。此时,从节点内存中的镜像数据是完整的,但还不能直接运行。它需要完成最后一步:让bootloader在下次重启时,用新镜像替换旧程序。
6.1 Bootloader标志位机制
KW36/38的蓝牙OTAP bootloader会检查Flash中的一个特定区域——通常称为BootFlags或OtaFlags。这个区域存放着一些标志,告诉bootloader是否有新镜像可用、镜像存储在哪里、镜像大小是多少等信息。
对于从节点,当它完成镜像接收和校验后,需要做以下几件事:
- 写入镜像元数据:在存储区(内部Flash或外部EEPROM)的起始位置,写入一个特定的起始标记(例如
0xDE, 0xAD, 0xAC, 0xE5),紧接着写入镜像的长度和扇区位图。这些信息是bootloader识别和搬运镜像所必需的。 - 设置更新标志:在
BootFlags区域,写入一个预定义的值(例如gBootValueForTRUE_c,通常是0xAA或0x55AA),表示“有一个有效的新镜像等待切换”。 - 执行软复位:调用
ResetMCU()函数重启芯片。
重启后,bootloader开始运行。它首先检查BootFlags,如果发现更新标志被设置,就会根据标志位找到存储区,读取镜像元数据,然后将镜像数据搬运到应用程序区域(通常是内部Flash的起始地址)。搬运完成后,bootloader会清除更新标志,并再次重启芯片,这次就会跳转到新的应用程序入口点,完成升级。
6.2 防止错误切换的关键代码
这里有一个至关重要的安全细节:主节点在收到给从节点的镜像后,绝对不能设置自己的BootFlags为有效更新标志。否则,主节点自己也会在下次重启时错误地加载这个不属于它的镜像,导致系统崩溃。
在官方示例代码的OtaSupport.h中,定义了两个不同的标志值:
#define gBootValueForTRUE_c (0xAA55A55AUL) // 给自己升级的标志 #define gBootValueForLinCanNode_c (0xAA) // 给LIN/CAN节点升级的标志在主节点的otap_client.c文件中,当蓝牙OTAP完成时,会调用OTA_SetNewImageFlag()。我们需要在这个函数内部或调用它的地方,根据之前解析的Image Identifier来区分设置哪个标志。
// 伪代码逻辑 if (g_ota_for_lin_or_can_node == TRUE) { // 这是给从节点的镜像 SET_BOOT_FLAG(gBootValueForLinCanNode_c); // 设置一个“安全”的标志,bootloader会忽略它 StartLinCanOtaTransfer(); // 启动总线传输,而不是重启! } else { // 这是给自己的镜像 SET_BOOT_FLAG(gBootValueForTRUE_c); ResetMCU(); // 重启,让bootloader为自己升级 }同时,你还需要修改bootloader的源码(OtapBootloader.c),让它能够识别gBootValueForLinCanNode_c这个标志。当bootloader看到这个标志时,它应该什么都不做,直接跳转到现有的应用程序,或者至少不能尝试去搬运镜像。通常的做法是在检查标志的if语句中,排除掉这个特定的值。
// 在bootloader的检查逻辑中 if ( (sBootFlags.imageUpdateFlag == gBootValueForTRUE_c) /* && 其他条件... */ ) { // 执行镜像搬运和切换 } else { // 忽略,直接启动旧应用 }6.3 存储一致性保障
在写入镜像元数据和设置BootFlags之间,如果发生断电,系统可能会处于一个不一致的状态。例如,元数据写了一半,或者BootFlags设置了但元数据不完整。为了防止这种情况导致设备变砖,可以采取以下策略:
- 顺序写入:先完整写入镜像数据和元数据,最后再写入
BootFlags。因为bootloader以BootFlags为准。 - 备份标志区:使用两个
BootFlags区域,采用“预写-提交”的方式。先在一个备份区写入标志,然后写入元数据和镜像数据,最后在正式区写入标志。bootloader检查时,如果正式区标志有效就用正式区;如果无效但备份区有效,则用备份区,并在恢复后清理备份区。 - CRC校验:bootloader在搬运镜像前,不仅检查
BootFlags,还要对存储区的镜像元数据和镜像本身计算CRC,与存储的CRC值进行比对。只有全部校验通过,才执行切换操作。
这些措施增加了代码复杂度,但对于要求高可靠性的汽车或工业产品来说,是值得的。
7. 系统集成测试与性能分析
理论设计和代码编写完成后,必须通过严格的测试来验证整个升级流程的可靠性和性能。测试需要覆盖正常流程、异常处理以及边界情况。
7.1 硬件连接与测试准备
你需要准备至少两块FRDM-KW36或KW38开发板。一块作为主节点(LIN Master/CAN Node A),另一块作为从节点(LIN Slave/CAN Node B)。此外,还需要:
- LIN测试:两根杜邦线(连接LIN信号线LIN和地线GND),一个12V电源(为LIN总线提供上拉电源)。特别注意:对于从节点板,需要移除R34和R27这两个电阻,这是为了将板载的LIN收发器配置为从模式。
- CAN测试:两根双绞线(分别连接两个板的CAN_H和CAN_L),一个120欧姆的终端电阻(如果只有两个节点,通常在其中一个板上启用终端电阻即可,KW36开发板有相关跳线),同样需要12V电源。
- 调试:两根Micro-USB线,用于给板子供电和查看串口日志。
硬件连接务必仔细,错误的接线是导致通信失败最常见的原因。连接好后,使用USB线将两块板连接到电脑,打开两个串口终端工具(如Tera Term、Putty),配置为115200波特率、8数据位、无校验、1停止位、无流控。
7.2 测试流程与结果分析
- 程序烧录:首先,为两块板都烧录支持OTAP的bootloader(路径:
SDK\boards\frdmkw36\wireless_examples\framework\bootloader_otap)。然后,为主节点烧录集成了LIN/CAN传输功能的OTAP客户端应用工程(如lin_master或can_a),为从节点烧录对应的从节点应用工程(如lin_slave或can_b)。 - 启动与观察:按下开发板的复位键SW1。从串口终端你应该能看到启动日志,包括蓝牙地址、LIN/CAN初始化状态等。
- 蓝牙连接与文件传输:
- 在手机上打开NXP IoT Toolbox APP,进入OTAP功能。
- 按下主节点板上的SW2按钮(如果基于w_uart示例),使其开始蓝牙广播。
- 在APP中扫描并连接该设备。
- 在APP中选择事先准备好的OTA文件(注意:用于从节点的OTA文件,其Image ID必须设置为
0x000A或其他你定义的值)。 - 点击上传。此时,你可以从主节点的串口日志看到蓝牙传输进度。
- 总线传输观察:蓝牙传输完成后,主节点应自动开始LIN或CAN总线传输。此时,观察两个板的串口日志至关重要。主节点日志应显示正在发送第X个数据块,从节点日志应显示正在接收和写入。传输过程中,可以尝试拔插总线线缆模拟通信中断,观察重传机制是否生效。
- 升级完成与验证:传输完成后,从节点日志应显示设置boot标志并重启。重启后,从节点应运行新版本的固件。你可以在新固件中增加一个版本号打印功能来验证。
7.3 性能实测与优化建议
根据官方文档和我自己的实测,性能数据如下:
- LIN总线 (19.2 kbps):升级一个约200KB的镜像,大约需要6.5分钟。如果使用外部Flash,从节点在升级完成后重启并搬运镜像,还会额外增加约20秒。优化建议:在满足电磁兼容要求的前提下,可以尝试将LIN波特率提高到20kbps。更大的优化在于增大每个数据块的大小(如从1KB增加到2KB),减少状态确认帧的占比。但块越大,重传的代价也越高,需要权衡。
- CAN总线 (1 Mbps):升级同样的200KB镜像,仅需约13秒,速度提升非常显著。如果使用CAN FD并将数据场充分利用,时间还可以进一步缩短。外部Flash导致的额外重启时间同样是20秒左右。优化建议:对于CAN,首要的是启用CAN FD并设置尽可能高的数据场长度(如64字节)。其次,可以实现“滑动窗口”协议,让主节点连续发送多个帧后再等待批量确认,能进一步压榨总线带宽。
常见问题排查速查表:
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 蓝牙OTAP成功后,主节点无反应,不启动LIN/CAN传输。 | 1. OTA文件Image ID未设置为从节点ID。 2. 主节点代码中未正确解析ID并触发传输流程。 3. gOtaUseBusSelection_d宏未正确定义。 | 1. 检查OTA文件生成设置。 2. 在 OtapClient_IsImageFileHeaderValid()函数处设断点,查看解析结果。3. 检查 app_preinclude.h中总线选择宏。 |
| LIN/CAN传输开始后,很快停止,并报错。 | 1. 硬件连接错误(线接反、未供电)。 2. 主从节点波特率、帧ID等配置不一致。 3. 存储初始化失败(EEPROM未识别)。 | 1. 用万用表检查线路。 2. 对比双方代码中的 lin_cfg.h/flexcan_cfg.h。3. 检查串口日志中EEPROM初始化信息。 |
| 传输过程中,从节点收不到数据或数据错乱。 | 1. 总线干扰(CAN未加终端电阻)。 2. 缓冲区溢出或指针错误。 3. 调度或中断冲突(LIN调度表不合理)。 | 1. CAN总线两端加120Ω终端电阻。 2. 检查缓冲区大小和读写指针管理代码。 3. 简化LIN调度表,确保数据帧发送间隙足够小。 |
| 传输完成,从节点重启后,程序未更新或卡死。 | 1. BootFlags标志位设置错误或未设置。 2. 镜像元数据(起始标记、长度)写入不正确。 3. 新镜像本身有问题(链接地址错误)。 | 1. 调试bootloader,单步跟踪其检查BootFlags和搬运镜像的过程。2. 使用调试器或Flash读取工具,检查存储区头部数据是否正确。 3. 确认从节点新镜像的链接地址与bootloader期望的地址一致。 |
| 升级多个CAN从节点时,只有一个成功。 | 1. 多节点升级逻辑错误,所有节点响应了同一帧数据。 2. 设备ID冲突或获取逻辑有误。 | 1. 检查设备发现阶段,Node A是否正确收集到所有不同ID。 2. 检查寻址阶段,数据帧是否包含了目标ID,以及从节点是否只处理匹配自己ID的帧。 |
整个方案的实现是一个系统工程,涉及无线通信、有线总线、存储管理和bootloader多个模块的紧密配合。从我的经验来看,最耗费时间的往往不是核心传输逻辑,而是这些模块间的边界条件处理和异常状态的恢复。建议在开发时,为每个关键步骤添加详细的串口日志,并设计一套简单的命令行调试接口,可以手动触发各个状态和查询变量,这对后期排查问题有巨大帮助。当你看到通过手机一点,整个有线网络上的设备依次安静地完成升级时,这种混合网络架构带来的灵活性和价值就完全体现出来了。