1. 项目概述:深入CC26x0/CC13x0的Bootloader世界
在嵌入式开发,尤其是物联网设备开发中,Bootloader(引导加载程序)是一个既基础又至关重要的组件。它就像是设备的“开机自检程序”和“系统安装向导”的结合体,负责在芯片上电复位后,完成最底层的硬件初始化,并决定接下来要运行什么代码。对于需要远程更新、现场升级的设备来说,一个稳定、可靠的Bootloader更是保障产品生命周期的基石。德州仪器(TI)的CC26x0和CC13x0系列无线微控制器,凭借其超低功耗和强大的射频性能,在蓝牙、Zigbee、Thread等物联网应用中占据重要地位。其内置的ROM Bootloader,为开发者提供了一个开箱即用的、通过串行接口更新固件的标准方案。
然而,官方技术手册往往侧重于寄存器描述和命令列表,读起来像是冰冷的说明书。在实际项目中,仅仅知道命令格式是远远不够的。你可能会遇到这些问题:为什么我的UART连接后毫无反应?SSI通信时第一个字节总是丢失?发送了擦除命令,但设备状态却不对?这些坑,我都踩过。本文将从一个一线开发者的视角,带你穿透TI官方文档的表层,深入剖析CC26x0/CC13x0 Bootloader的UART和SSI双接口工作机制、数据包协议的每一个细节,以及12个核心命令的实战应用。我们不止讲“是什么”,更重点拆解“为什么”和“怎么做”,并分享那些手册上不会写的调试经验和避坑指南。
2. Bootloader核心架构与启动逻辑拆解
在深入接口和协议之前,我们必须先理解CC26x0/CC13x0 Bootloader的顶层设计思路。它不是一个运行在Flash中的复杂程序,而是一段固化在芯片ROM(只读存储器)中的代码。这意味着它不可修改,稳定性极高,但也功能固定。它的核心使命非常明确:通过简单的串行接口,接收外部主机发送的命令和数据,实现对内部Flash存储器的编程(烧录)、读取、擦除等操作,最终引导或更新用户应用程序。
2.1 Bootloader的使能与后门机制
Bootloader虽然方便,但也带来了潜在的安全风险——攻击者可能利用它来读取Flash中的敏感代码或数据。因此,TI设计了一套精细的开关控制。
2.1.1 如何彻底禁用Bootloader?安全永远是第一位的。对于量产产品,如果你确定不需要后期通过串口更新固件,强烈建议禁用Bootloader。这是通过配置芯片的CCFG(Customer Configuration,客户配置)区域中的一个特定参数BOOTLOADER_ENABLE来实现的。当这个参数被设置为禁用状态后,芯片复位启动时,将完全跳过ROM Bootloader的代码执行路径,直接尝试从Flash的应用程序入口点启动。这样一来,即使有人试图通过硬件手段将程序计数器(PC)强制指向Bootloader的ROM地址,也无法执行任何命令,从根本上堵死了这个攻击面。在您的项目工程中,通常可以在ccfg.c或类似的配置文件中找到并修改这个参数。
2.1.2 灵活的后门(Backdoor)入口禁用Bootloader虽然安全,但也失去了现场更新的灵活性。为此,TI提供了一个“后门”机制。即使Bootloader在CCFG中被禁用,只要后门功能(通过BL_ENABLE参数)被开启,你仍然可以在特定条件下强制芯片进入Bootloader模式。
这个后门的原理很巧妙:它允许你指定一个普通的GPIO引脚(通过BL_PIN_NO配置)和一个期望的电平(高或低,通过BL_LEVEL配置)。在芯片启动过程中,Bootloader会先检查这个引脚的实际电平。如果检测到的电平与BL_LEVEL配置的期望电平一致,那么无论Flash中是否存在有效的应用程序,Bootloader都会夺取控制权,等待串口命令,而不是跳转到应用程序。
重要实操心得:在使用后门功能时,有一个极易忽略的细节。手册中提到,当检查后门电平时,无论
BL_LEVEL配置的是高还是低,这个指定的GPIO引脚都会被内部上拉电阻使能。这意味着,如果你希望用低电平触发后门(BL_LEVEL = 0),那么你的外部电路必须能够提供一个足够强的下拉(例如直接接地),以克服内部上拉,确保引脚被可靠地拉低。如果外部是悬空或高阻态,内部上拉会导致引脚始终为高,后门条件永不满足。这是我早期调试时浪费了半天时间才发现的坑。
另外,如果后门引脚恰好与UART0或SSI0的引脚复用,你必须在开始通过UART/SSI通信之前,撤销后门触发信号(即让引脚恢复到非触发电平)。否则,Bootloader可能会持续处于“等待后门触发”的状态而无法正常通信。
2.2 双接口策略:UART与SSI的选型考量
CC26x0/CC13x0的Bootloader支持UART0和SSI0(即SPI)两种物理接口。这不是简单的二选一,而是一种“先到先得”的智能选择策略,理解这一点对硬件设计和调试至关重要。
Bootloader启动后,并不会立即初始化所有接口的输出引脚(TX)。它只会先初始化UART0_RX和SSI0_RX这两个输入引脚,并同时监听它们。此时,两个接口的TX引脚都处于高阻态。当外部主机(比如你的烧录工具)首次通过任何一个接口发送数据时,Bootloader会检测到该接口RX引脚上的活动,随即锁定使用这个接口,并初始化对应的TX引脚,同时彻底禁用另一个未使用的接口模块。一旦选择,在本次Bootloader运行周期内无法切换,必须复位芯片才能选择另一个接口。
2.2.1 UART vs. SSI:如何选择?
- UART(2线制):优势在于接线简单,只需RX、TX两根线,兼容绝大多数USB转串口工具。其波特率通过自动检测机制适应主机,最高支持约1.6 Mbps。缺点是速率相对较低,且是异步通信,对时钟精度有一定要求。适合对烧录速度要求不高、追求硬件连接简单的场景,例如通过USB进行小批量生产烧录或开发调试。
- SSI/SPI(4线制):优势是速度更快、更稳定,最高时钟可达4 MHz(系统时钟48 MHz的1/12)。它是同步通信,由主机提供时钟,数据传输更可靠。缺点是需要连接CLK、FSS(片选)、RX(MOSI)、TX(MISO)四根线,硬件稍复杂。适合在生产线上的高速烧录器,或者对固件体积较大、要求快速升级的场景。
2.2.2 引脚映射的硬件依赖接口使用的物理引脚是固定的,与芯片的具体封装型号相关。例如,对于常见的4x4 QFN封装,其引脚映射如下表所示。在设计电路板时,必须根据你选用的芯片封装,正确地将这些引脚连接到你的连接器或烧录器座上。
| 信号 | 4 × 4 QFN (RSM) 封装引脚 |
|---|---|
| UART0 RX | DIO1 |
| UART0 TX | DIO2 |
| SSI0 CLK | DIO8 |
| SSI0 FSS | DIO7 |
| SSI0 RX | DIO9 |
| SSI0 TX | DIO0 |
硬件设计避坑指南:务必在原理图设计阶段就确认好芯片封装和对应的Bootloader引脚。我曾遇到一个案例,工程师参考了7x7封装的引脚图来设计4x4封装的板子,导致UART线接错,Bootloader根本无法通信。另外,即使你只计划使用UART,也建议将SSI的引脚(特别是DIO0)预留测试点或确保其未被其他电路拉死,以免意外影响Bootloader的初始状态检测。
3. 通信协议与数据包格式深度解析
Bootloader与主机之间所有的交互,都基于一个精心设计的、带确认机制的数据包协议。这个协议是保证数据传输可靠性的核心,无论是UART还是SSI,其上层数据包格式是完全一致的。
3.1 数据包的结构与收发流程
你可以把这个协议想象成两个人之间严谨的对话,每一句话(数据包)都必须得到对方的确认(ACK),否则就要重说。
一个完整的数据包由三部分组成:
- 长度字节(Size):1字节,表示整个数据包的字节数。注意,这个数值是“数据字节数 + 2”。例如,如果你要发送3个字节的数据(比如一个命令),那么长度字节就是 3 + 2 = 5。
- 校验和字节(Checksum):1字节,用于验证数据在传输过程中是否出错。算法极其简单:将所有数据字节(不包括长度和校验和本身)的值相加,然后取结果的低8位(即和值 & 0xFF)。这种校验方式虽然不能纠正错误,但能高效地检测出单字节错误和大多数多字节错误。
- 数据区(Data):可变长度,内容由具体的命令决定。第一个数据字节通常是命令码(Command Value)。
3.1.1 发送数据包的流程假设主机(Sender)要向Bootloader(Receiver)发送一个数据包。
- 主机先发送长度字节。
- 接着发送计算好的校验和字节。
- 然后依次发送所有的数据字节。
- 发送完成后,主机进入等待状态,直到从Bootloader收到一个非零的响应字节。这个响应要么是ACK(0xCC),表示成功接收;要么是NAK(0x33),表示接收出错(如校验和失败)。
3.1.2 接收数据包的流程当Bootloader要向主机返回数据时。
- 主机持续读取串口,忽略所有收到的0x00字节。Bootloader可能在数据包之间发送0x00作为填充。
- 当读到第一个非零字节时,这个字节就是长度字节。
- 读取下一个字节,作为校验和字节。
- 根据长度字节计算需要接收的数据字节数(长度 - 2),然后读取相应数量的数据字节。
- 主机根据收到的数据字节计算校验和,与收到的校验和字节比较。
- 如果一致,主机发送ACK(0xCC);如果不一致,主机发送NAK(0x33)。Bootloader收到NAK后,通常会期待主机重发上一个数据包。
为了更直观,我们来看一个实际例子:主机发送一个COMMAND_PING命令(命令码0x20)。这个命令只有1个数据字节(即0x20本身)。
- 长度 = 数据字节数(1) + 2 = 3 -> 0x03
- 校验和 = 数据字节(0x20)的和 = 0x20 -> 0x20
- 数据 = 0x20 因此,主机发送的字节序列为:
0x03, 0x20, 0x20。然后等待ACK(0xCC)。
3.2 传输层:UART与SSI的底层差异
数据包协议是上层建筑,而UART和SSI是底层的“运输方式”。两者在电气特性和初始行为上有显著区别。
3.2.1 UART接口的自动波特率检测UART通信需要双方约定相同的波特率。Bootloader的巧妙之处在于它支持自动波特率检测,主机无需预先知道芯片的系统时钟配置。
其检测机制如下:Bootloader的UART模块会将其RX引脚(DIO1或DIO2,取决于封装)配置为GPIO中断模式,用于检测边沿。主机需要连续发送两个字节的同步头:0x55(二进制为01010101)。这个序列会产生一个标准的、周期性的方波。Bootloader通过测量这个方波的周期,就能反推出主机的波特率。检测成功后,Bootloader会返回两个字节的确认序列:0x00, 0xCC。
关键限制与实操要点:
- 最高波特率:理论上是系统时钟(48MHz)的1/16,即3 Mbps。但由于固件检测算法的限制,实际支持的最高可靠波特率约为1.6 Mbps。在115200、921600、1M等常用波特率下工作都很稳定。
- 格式固定:数据格式固定为8个数据位,无奇偶校验位,1个停止位(8N1)。这是无法更改的。
- 发送时机:一定要在芯片复位并进入Bootloader模式后,再发送同步头。如果发送过早,边沿会被错过。
3.2.2 SSI接口的特殊初始化处理SSI是同步接口,通信由主机驱动的时钟(CLK)主导。这里有一个非常重要的硬件初始化时序问题,是很多开发者首次使用SSI Bootloader时失败的主要原因。
如前所述,Bootloader启动后,SSI的TX引脚(MISO)是未配置的(高阻态)。只有当Bootloader通过SSI的RX引脚(MOSI)成功接收到第一个完整字节后,它才会去初始化并启用SSI的TX引脚输出。
这就导致了一个现象:主机发送的第一个数据包(例如PING命令)的第一个字节(长度字节)时,Bootloader的TX线是没有响应的,主机读回的数据是未定义的(通常是0x00或0xFF)。Bootloader此时正在内部处理这个字节,并配置TX引脚。
因此,协议设计允许在数据包之间发送任意数量的0x00。主机在发送完第一个字节后,必须插入一个短暂的延迟(通常几微秒到几十微秒即可,具体取决于主控速度),等待Bootloader完成TX引脚的配置,然后再发送校验和及后续字节。从第二个字节开始,通信就会恢复正常。
3.2.3 SSI的通信格式SSI需配置为Motorola格式,且SPO(时钟极性)和SPH(时钟相位)均设置为1。这通常被称为SPI模式3。在这种模式下:
- 时钟空闲时为高电平(SPO=1)。
- 数据在时钟的第二个边沿(即下降沿)被采样(SPH=1)。 绝大多数MCU的SPI主模块都支持模式3的配置。
4. 十二大核心命令实战详解与代码实现
理解了协议和接口,我们就可以驾驭Bootloader的十二个核心命令了。这些命令是操作芯片的“遥控器”。下面我将逐一解析每个命令的用途、数据包格式,并给出清晰的C语言伪代码示例和实战注意事项。
4.1 基础命令:握手、状态与复位
这三个命令是任何交互的基础。
4.1.1 COMMAND_PING (0x20) - 连接测试这是最简单的命令,用于测试通信链路是否建立。它没有参数。
// 发送PING命令的数据包构建 unsigned char ping_packet[3]; ping_packet[0] = 3; // Size: 1 data byte + 2 ping_packet[1] = 0x20; // Checksum: only data byte 0x20 ping_packet[2] = 0x20; // Command: COMMAND_PING // 通过UART或SSI发送 ping_packet 数组 // 等待并确认收到 ACK (0xCC)实操提示:任何新的通信会话开始前,先发一个PING命令。如果收到ACK,说明物理连接、波特率/时钟配置、协议层都基本正常。这是后续所有操作的前提。
4.1.2 COMMAND_GET_STATUS (0x23) - 获取状态这个命令用于查询上一个命令的执行结果。在发送除GET_STATUS和PING之外的任何命令后,都必须发送此命令来确认操作是否成功。
unsigned char status_packet[3]; status_packet[0] = 3; // Size status_packet[1] = 0x23; // Checksum status_packet[2] = 0x23; // Command: COMMAND_GET_STATUS // 发送 status_packet // 接收返回包,返回包格式为 [size, checksum, status_byte]返回的状态字节(status_byte)含义如下:
| 状态值 | 宏定义 | 含义 |
|---|---|---|
| 0x40 | COMMAND_RET_SUCCESS | 上一个命令执行成功 |
| 0x41 | COMMAND_RET_UNKNOWN_CMD | 未知命令(命令码错误) |
| 0x42 | COMMAND_RET_INVALID_CMD | 无效命令(例如数据包长度不符) |
| 0x43 | COMMAND_RET_INVALID_ADR | 无效地址(地址越界或未对齐) |
| 0x44 | COMMAND_RET_FLASH_FAIL | Flash操作失败(编程或擦除错误) |
4.1.3 COMMAND_RESET (0x25) - 系统复位让芯片执行一次软复位。在完成固件下载后,发送此命令将使芯片复位并跳转到新烧录的应用程序执行。
unsigned char reset_packet[3]; reset_packet[0] = 3; reset_packet[1] = 0x25; reset_packet[2] = 0x25; // Command: COMMAND_RESET // 发送 reset_packet // Bootloader会先回复ACK,然后立即复位。主机无需等待其他响应。重要提醒:发送
RESET命令后,Bootloader会立刻复位,当前通信会话终止。如果你的主机程序还需要进行其他操作(比如验证),请在发送RESET前确保所有步骤已完成。
4.2 Flash存储操作命令
这是Bootloader最核心的功能:管理Flash存储器。
4.2.1 COMMAND_DOWNLOAD (0x21) - 下载准备此命令通知Bootloader:准备接收一段数据,并将其编程到Flash的指定位置。它需要两个32位参数:起始地址和数据的总字节数。
unsigned char download_packet[11]; uint32_t start_address = 0x00000000; // Flash起始地址 uint32_t total_data_size = 1024; // 准备下载1024字节 download_packet[0] = 11; // Size: 1 cmd + 8 bytes params + 2 download_packet[1] = 0x21 + (start_address>>24&0xFF) + (start_address>>16&0xFF) + (start_address>>8&0xFF) + (start_address&0xFF) + (total_data_size>>24&0xFF) + (total_data_size>>16&0xFF) + (total_data_size>>8&0xFF) + (total_data_size&0xFF); download_packet[1] &= 0xFF; // 计算校验和(伪代码,需实际计算) download_packet[2] = 0x21; // Command // 填入地址(大端序,MSB first) download_packet[3] = (start_address >> 24) & 0xFF; download_packet[4] = (start_address >> 16) & 0xFF; download_packet[5] = (start_address >> 8) & 0xFF; download_packet[6] = start_address & 0xFF; // 填入数据总大小 download_packet[7] = (total_data_size >> 24) & 0xFF; download_packet[8] = (total_data_size >> 16) & 0xFF; download_packet[9] = (total_data_size >> 8) & 0xFF; download_packet[10] = total_data_size & 0xFF; // 发送download_packet,然后必须发送COMMAND_GET_STATUS检查地址和大小是否有效。关键点:此命令仅做预约,并不执行擦除或编程。Flash目标区域必须在编程前已是擦除状态(全为0xFF)。
4.2.2 COMMAND_SEND_DATA (0x24) - 发送数据在DOWNLOAD命令之后,使用此命令发送实际的数据块。数据直接包含在命令包中。
// 假设要发送252字节的数据(最大数据负载) unsigned char data_packet[255]; // 252 data + 2 header + 1 cmd uint8_t data[252]; // ... 用你的固件数据填充 data 数组 ... uint8_t packet_size = 252 + 3; // data(252) + cmd(1) + 2 = 255 data_packet[0] = packet_size; data_packet[2] = 0x24; // Command // 计算校验和 (cmd + all data bytes) uint16_t sum = 0x24; for(int i=0; i<252; i++) { data_packet[3+i] = data[i]; sum += data[i]; } data_packet[1] = sum & 0xFF; // 校验和 // 发送 data_packet // 发送后,必须发送 COMMAND_GET_STATUS 确认本块数据编程成功。核心机制:
- 地址自动递增:Bootloader内部维护一个“当前编程地址”。
DOWNLOAD命令将其设置为起始地址。每成功执行一个SEND_DATA命令,这个地址就会自动增加本次编程的字节数。因此,你可以用多个SEND_DATA命令连续发送数据,无需每次都指定地址。 - 总量检查:Bootloader会累计已接收的数据字节数。当累计值达到
DOWNLOAD命令指定的total_data_size时,下载过程自动结束。如果后续再发送SEND_DATA,会返回错误状态。 - 错误重传:如果
SEND_DATA后收到NAK,说明传输或编程出错。此时当前编程地址不会递增,主机应重发上一个SEND_DATA数据包。
4.2.3 COMMAND_SECTOR_ERASE (0x26) - 扇区擦除擦除Flash中一个指定的4KB扇区。参数是扇区的起始地址(必须是4KB对齐的)。
unsigned char erase_packet[7]; uint32_t sector_address = 0x00001000; // 例如,擦除第二个4KB扇区 erase_packet[0] = 7; // 计算校验和: 0x26 + 4字节地址 erase_packet[1] = 0x26 + (sector_address>>24&0xFF) + (sector_address>>16&0xFF) + (sector_address>>8&0xFF) + (sector_address&0xFF); erase_packet[1] &= 0xFF; erase_packet[2] = 0x26; // 填入地址 erase_packet[3] = (sector_address >> 24) & 0xFF; erase_packet[4] = (sector_address >> 16) & 0xFF; erase_packet[5] = (sector_address >> 8) & 0xFF; erase_packet[6] = sector_address & 0xFF; // 发送,并用GET_STATUS确认。安全限制:如果目标扇区被FCFG1或CCFG中的写保护位保护,擦除操作将不会执行。特别注意:如果擦除包含CCFG区域的扇区(通常是最后一个扇区),擦除完成后,Bootloader会自动用出厂默认值重新编程CCFG区域。这会覆盖你所有的自定义配置(包括Bootloader使能、后门设置等)!
4.2.4 COMMAND_BANK_ERASE (0x2C) - 整片擦除擦除主Flash Bank中所有未被写保护位保护的扇区。这是一个“重量级”操作。
unsigned char bank_erase_packet[3]; bank_erase_packet[0] = 3; bank_erase_packet[1] = 0x2C; // Checksum bank_erase_packet[2] = 0x2C; // Command // 发送,并用GET_STATUS确认。致命警告:手册中明确提到,执行此命令后,Flash模块的有限状态机(FSM)会被锁定。这意味着在此之后,无法再执行任何Flash操作命令(如下载、扇区擦除等)。唯一的恢复方法是发送COMMAND_RESET命令或触发硬件复位,让芯片重新启动。在设计烧录流程时,如果需要先全片擦除再编程,正确的顺序是:BANK_ERASE->GET_STATUS->RESET-> (等待芯片重启并重新进入Bootloader) ->DOWNLOAD->SEND_DATA... 切勿在BANK_ERASE后尝试直接下载。
4.3 存储与系统信息查询命令
4.3.1 COMMAND_GET_CHIP_ID (0x28) - 获取芯片ID读取芯片的唯一标识符(从AON_WUC_JTAGUSERCODE寄存器)。常用于在生产线上验证芯片型号或记录设备身份。
unsigned char get_id_packet[3]; get_id_packet[0] = 3; get_id_packet[1] = 0x28; get_id_packet[2] = 0x28; // 发送后,Bootloader会返回一个6字节的包: [size=6, checksum, ID_byte3, ID_byte2, ID_byte1, ID_byte0] // ID以MSB first方式传输。4.3.2 COMMAND_CRC32 (0x27) - 计算CRC32计算指定内存区域(通常是Flash)的CRC32校验值,用于验证固件完整性。参数包括起始地址、数据长度和读取重复次数。
unsigned char crc32_packet[15]; uint32_t crc_address = 0x00000000; uint32_t crc_length = 4096; uint32_t repeat_count = 0; // 0表示只读一次 crc32_packet[0] = 15; // 计算校验和(略) crc32_packet[2] = 0x27; // 填入地址、长度、重复次数(均为大端序) // ... 赋值代码 ... // 发送后,Bootloader会返回一个6字节的包: [size=6, checksum, CRC32_byte3, CRC32_byte2, CRC32_byte1, CRC32_byte0]注意:crc_length参数必须大于8,否则返回的CRC32值固定为0xFFFFFFFF。
4.4 内存直接读写命令
这两个命令功能强大但需谨慎使用,它们允许直接读写芯片的内存映射空间,包括SRAM和外设寄存器。
4.4.1 COMMAND_MEMORY_READ (0x2A) - 内存读取从指定地址读取一定数量的8位或32位数据。
unsigned char mem_read_packet[9]; uint32_t read_address = 0x20000000; // SRAM起始地址 uint8_t access_type = 0; // 0: 8-bit, 1: 32-bit uint8_t num_accesses = 10; // 读取10个元素(字节或字) mem_read_packet[0] = 9; // 计算校验和 mem_read_packet[2] = 0x2A; // 填入地址、访问类型、访问次数 mem_read_packet[3] = (read_address >> 24) & 0xFF; // ... 赋值代码 ... mem_read_packet[7] = access_type; mem_read_packet[8] = num_accesses; // 发送后,Bootloader会返回一个数据包,包含读取到的数据。限制:单次读取的数据总量不能超过一个数据包的最大容量。对于8位访问,最多253次(253字节);对于32位访问,最多63次(252字节)。
4.4.2 COMMAND_MEMORY_WRITE (0x2B) - 内存写入向指定地址写入8位或32位数据。这是最危险的命令之一。
// 示例:向SRAM地址0x20000100写入4个32位字 unsigned char mem_write_packet[9 + 4*4]; // 9 header + 16 data uint32_t write_address = 0x20000100; uint8_t access_type = 1; // 32-bit uint32_t data_words[4] = {0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x87654321}; // 构建数据包...绝对禁区(手册明确警告):
- 不要写入Flash:此命令不能用于写Flash!写Flash必须使用
DOWNLOAD/SEND_DATA命令。 - 不要写入Bootloader使用的SRAM区域:低4KB的SRAM(0x20000000 - 0x20000FFF)被Bootloader自身使用,写入可能导致其崩溃。
- 不要写入当前正在使用的串口外设寄存器:如果你正在用UART通信,却去写UART的配置寄存器,通信会立刻中断,设备变砖。
这个命令的合理用途是在调试时,向应用程序的特定变量地址写入值,或者配置某些启动所需的外设(需极其小心)。对于绝大多数固件升级场景,根本不需要用到这个命令。
5. 实战流程、常见问题与深度调试技巧
掌握了所有命令,我们来看一个完整的固件升级流程应该如何编排,以及如何应对可能出现的各种问题。
5.1 标准固件升级流程
一个健壮的升级流程应该包含以下步骤,并充分考虑错误处理和超时机制:
- 初始化通信:主机配置好UART/SSI,发送同步头(UART需发0x55, 0x55)并与Bootloader完成同步。
- 连接测试:发送
COMMAND_PING,确认收到ACK。 - 获取芯片ID(可选但推荐):发送
COMMAND_GET_CHIP_ID,验证连接的芯片型号是否正确。这在多型号产品线上非常重要。 - 擦除Flash:
- 如果需要全片擦除,发送
COMMAND_BANK_ERASE,等待ACK,然后**必须发送COMMAND_RESET**让芯片重启,并重新从步骤1开始。 - 如果采用扇区擦除,针对需要更新的固件区域,依次发送
COMMAND_SECTOR_ERASE命令,每发一个都要用GET_STATUS确认成功。
- 如果需要全片擦除,发送
- 下载固件:
- 发送
COMMAND_DOWNLOAD,指定起始地址和固件总大小。用GET_STATUS确认。 - 将固件二进制文件分块(每块最大252字节),循环发送
COMMAND_SEND_DATA命令。每发送一块,必须紧跟一个COMMAND_GET_STATUS命令,确认该块编程成功。如果收到NAK或错误状态,应重发当前数据块。
- 发送
- 校验固件(强烈推荐):使用
COMMAND_CRC32命令,计算刚下载的Flash区域的CRC值,与主机端计算的CRC进行比对。不一致则说明烧录失败,需重试。 - 复位并运行:发送
COMMAND_RESET命令。芯片将复位,并启动新烧录的应用程序。
5.2 典型问题排查与解决思路
在实际开发中,你几乎一定会遇到下面这些问题。
5.2.1 问题:发送同步头或PING命令后,完全没有响应。
- 检查1:物理连接。用万用表或示波器检查TX、RX(或SPI四根线)是否连通,电压电平是否匹配(CC26xx是3.3V CMOS电平)。
- 检查2:Bootloader是否真的启动了。确认芯片是否处于Bootloader模式(例如,通过后门引脚触发,或者Flash为空)。可以尝试在芯片完全断电再上电后,第一时间发送命令。
- 检查3:接口选择冲突。如果你连接了UART线,但SSI的FSS/片选引脚被意外拉低(或拉高,取决于你的主机配置),Bootloader可能会误认为SSI是活动接口,从而不响应UART。确保不用的接口引脚处于非活动状态。
- 检查4:波特率/时钟。对于UART,确保主机波特率在Bootloader支持的范围内(≤1.6Mbps)。对于SSI,确认时钟极性/相位为模式3,且时钟频率≤4MHz。
- 检查5:SSI首个字节延迟。如果是SSI,主机在发送第一个数据包的首字节后,是否添加了足够长的延迟(>10us)?
5.2.2 问题:通信一开始正常,但发送几个命令后突然无响应。
- 可能原因1:Flash操作锁死。你是否在
COMMAND_BANK_ERASE之后没有复位,就尝试发送其他Flash命令?这会导致Bootloader内部状态机锁死。唯一的恢复方法是硬件复位。 - 可能原因2:写入了非法内存地址。是否使用了
COMMAND_MEMORY_WRITE,并意外写入了Bootloader使用的SRAM区域或串口控制寄存器?这会导致Bootloader崩溃。 - 可能原因3:电源不稳定。Flash编程和擦除是功耗较高的操作,可能引起电源电压跌落,导致芯片复位或工作异常。确保电源有足够的余量和去耦电容。
5.2.3 问题:SEND_DATA或擦除命令后,GET_STATUS返回COMMAND_RET_FLASH_FAIL(0x44)。
- 可能原因1:Flash未擦除。编程前必须确保目标区域已被擦除(全为0xFF)。检查你的擦除流程。
- 可能原因2:地址不对齐。虽然Bootloader的编程是按字节的,但Flash物理编程可能有页对齐要求(通常是4字节或8字节)。确保
DOWNLOAD的起始地址是合适的边界(通常4字节对齐是安全的)。 - 可能原因3:写保护。尝试编程或擦除的扇区被CCFG或FCFG1中的写保护位保护。检查你的CCFG配置。
5.2.4 问题:使用后门功能无法进入Bootloader。
- 检查1:CCFG配置。确认
BL_ENABLE已设置为启用,BL_PIN_NO和BL_LEVEL配置正确,并且你的应用程序没有在启动后重新配置这个引脚。 - 检查2:内部上拉。记住,检查后门时引脚内部上拉始终使能。如果你配置
BL_LEVEL=0(低电平触发),必须确保在复位期间,该引脚被外部电路强有力地拉低(例如直接接地),而不是仅仅悬空或通过大电阻下拉。 - 检查3:时序。后门电平需要在芯片复位后的很短时间内保持稳定。确保你的触发信号在复位引脚释放前就已建立,并在Bootloader检查期间保持。
5.3 高级调试技巧与工具
- 逻辑分析仪是你的最佳朋友:投资一个哪怕是最基础的逻辑分析仪(如Saleae Logic系列)。同时抓取UART的TX、RX线,或者SPI的四根线,可以直观地看到每一个字节的传输时序、数据包结构、ACK/NAK响应。绝大部分通信问题都可以通过分析波形瞬间定位。
- 实现一个简单的命令行工具:不要一开始就集成到复杂的生产工具中。先用Python(
pyserial库)或C在电脑上写一个简单的命令行程序,实现PING、读ID、擦除、编程等基本功能。交互式的调试能让你快速验证逻辑。 - 添加详细的日志和超时:在你的主机Bootloader驱动代码中,在每个发送和接收步骤都添加打印日志,并设置合理的超时(例如,等待ACK超时500ms)。当通信失败时,通过日志能清晰看到是在哪一步卡住的。
- 理解VIMS寄存器(STAT/CTL):虽然Bootloader本身不直接暴露这些寄存器给你配置,但当你开发运行在Flash上的应用程序时,如果需要优化性能(比如启用指令缓存),就需要配置VIMS模块。
STAT寄存器告诉你当前模式(Cache、GPRAM或Off),CTL寄存器用于切换模式。从Cache模式切换到GPRAM模式时,必须经过OFF模式,这是手册里强调的要点,直接切换可能导致不可预知的行为。
最后,分享一个我个人的深刻体会:Bootloader的稳定性和可靠性,是产品可维护性的基石。在项目早期就花时间彻底吃透其协议和特性,设计出容错率高、日志完备的升级流程,会在后期的量产、现场维护和问题排查中节省无数的时间和精力。把每一次与Bootloader的交互都当作一次严谨的对话,确认好每一步的回应,才能确保在复杂的现场环境中,固件升级这件“小事”能万无一失。