3. 移植联调流程总览
DSP 上电之后,到底发生了什么?很多刚入行的朋友以为,只要电源稳定,程序就自动跑起来了。其实没那么简单。DSP 内部有一套固定的启动流程,你得先搞清楚它,才能让代码老老实实地跑起来。
我最早做 C2000 系列移植时,就吃过这个亏。板子焊好,上电,示波器一看,IO 口没反应。查了半天,原来是 Boot 模式选错了,DSP 压根没从 Flash 启动。嗯,从那以后,我每次画板子都会先确认 Boot 引脚电平。
3.1 DSP 上电启动流程
DSP 上电后,大致会经历这么几个阶段:
- 上电复位:电源稳定后,POR(上电复位)电路释放复位信号。
- 硬件初始化:CPU 内部寄存器复位到默认值,PLL 开始锁定。
- Boot ROM 执行:DSP 从片内 Boot ROM 的 0x0000 地址开始执行固化代码。
- Boot 模式采样:Boot ROM 读取 GPIO 状态或 OTP 值,决定从哪种介质加载用户程序。
- 加载并跳转:将用户代码从外部介质(Flash、SPI EEPROM、I2C EEPROM 等)搬运到 RAM 或 Flash,然后跳转到入口地址。
核心要点:Boot ROM 是出厂固化的,你改不了。你能控制的,只有 Boot 模式的选择和二次 Bootloader 的编写。
我习惯把启动流程画成一张图,方便团队新人理解。你看下面这张:
3.2 SPI Boot 与 I2C Boot 模式选择
Boot 模式的选择,说白了就是告诉 DSP:「你的程序放在哪,用什么方式读进来」。最常见的两种外部 Boot 模式是 SPI 和 I2C。怎么选?我列个表,你一看就明白。
| 对比项 | SPI Boot | I2C Boot |
|---|---|---|
| 通信速率 | 高(可达几十 MHz) | 低(标准 100/400 kHz) |
| 引脚数量 | 4 线(CS、CLK、MOSI、MISO) | 2 线(SDA、SCL) |
| 从设备寻址 | 片选 CS 独立选择 | 7 位/10 位地址 |
| 典型存储介质 | SPI Flash(如 W25Q64) | I2C EEPROM(如 AT24Cxx) |
| 代码容量 | 大(MB 级) | 小(KB 级,通常 ≤ 512KB) |
| 适用场景 | 程序较大、需要快速启动 | 引脚受限、程序较小 |
我的建议:如果项目对启动速度有要求,或者代码超过 64KB,优先选 SPI Boot。I2C Boot 更适合那些引脚紧张、代码量不大的小系统。我在一个传感器节点项目里用过 I2C Boot,省了两个 GPIO,但启动时间多了将近 200ms——你得权衡。
选好模式后,硬件上要注意 Boot 引脚的配置。以 TMS320F28335 为例,Boot 模式由 GPIO84~GPIO87 的电平决定。我见过有人把这几个引脚悬空,结果 DSP 每次上电都随机进入不同模式——你说这调试起来得多崩溃?
避坑指南:Boot 引脚一定要用上拉或下拉电阻固定电平,不要依赖芯片内部弱上拉。内部弱上拉在复位期间可能不稳定,导致采样错误。我曾经在一个量产项目上吃过这个亏,100 块板子有 3 块启动失败,最后发现是 Boot 引脚焊接不良。
3.3 二次 Bootloader 编写要点
Boot ROM 里的加载程序通常只支持固定格式(比如特定的数据头、校验方式)。如果你的程序需要加密、压缩、或者从非标准地址加载,就得自己写二次 Bootloader。
说白了,二次 Bootloader 就是一段小代码,由 Boot ROM 加载到 RAM 里执行,然后它再去加载真正的应用程序。我习惯把它叫做「搬运工」。
3.3.1 二次 Bootloader 的核心任务
- 初始化外设:配置 SPI/I2C 控制器时钟、引脚复用。
- 读取应用程序:从外部存储器读取数据,支持校验(CRC 或累加和)。
- 写入目标地址:将数据写入 Flash 或 RAM,注意擦除和编程时序。
- 跳转执行:关闭中断、重设堆栈指针,跳转到应用程序入口。
3.3.2 编写时的关键细节
我整理了几个容易踩坑的地方,你写代码时多留个心眼:
- 堆栈设置:二次 Bootloader 运行在 RAM 里,堆栈指针必须重新初始化。我见过有人直接沿用 Boot ROM 的堆栈,结果跳转后栈空间不够,程序跑飞。
- 中断处理:在加载过程中,务必关闭所有中断。跳转前要清除中断标志,否则应用程序一启动就进中断,而中断向量表还没准备好——死循环。
- Flash 擦除时序:如果目标介质是 Flash,擦除操作需要时间。SPI Flash 擦除一个扇区通常要几百毫秒,你得加延时或查询状态寄存器,不能直接写。
- 数据校验:我习惯在应用程序末尾附加 4 字节 CRC32。二次 Bootloader 读取完数据后做校验,不通过就报错或重新加载。别问我为什么——有一次产线烧录器接触不良,100 片里有 5 片数据不全,全靠 CRC 拦住了。
核心原则:二次 Bootloader 要尽量精简,因为它占用的是 RAM 空间。加载完成后,它占用的 RAM 区域可以被应用程序覆盖——前提是你设计好链接脚本。
3.3.3 一个简化的二次 Bootloader 流程
下面这段伪代码展示了我常用的二次 Bootloader 逻辑。实际项目中你还需要处理时钟配置、看门狗喂狗等细节。
// 二次 Bootloader 主流程(伪代码)
void main(void) {
// 1. 关闭看门狗,关闭全局中断
WDT_Disable();
DISABLE_INTERRUPTS();
// 2. 初始化 SPI 外设(假设使用 SPI Boot)
SPI_Init(SPI_BOOT_BAUDRATE);
// 3. 从 SPI Flash 读取应用程序头部
// 头部包含:起始地址、长度、CRC32
BootHeader_t header;
SPI_Read(APP_START_SECTOR, &header, sizeof(header));
// 4. 校验头部合法性
if (header.magic != BOOT_MAGIC) {
ERROR_HANDLER(); // 非法头部,进入错误处理
}
// 5. 读取应用程序数据到 RAM 缓冲区
uint8_t buffer[header.length];
SPI_Read(APP_DATA_SECTOR, buffer, header.length);
// 6. CRC 校验
uint32_t calc_crc = CRC32_Calculate(buffer, header.length);
if (calc_crc != header.crc32) {
ERROR_HANDLER(); // CRC 不匹配,停止加载
}
// 7. 将数据写入目标 Flash 地址
Flash_Erase(header.dest_addr, header.length);
Flash_Write(header.dest_addr, buffer, header.length);
// 8. 跳转前准备:关闭外设、重设堆栈
SPI_Deinit();
SET_STACK_POINTER(APP_STACK_ADDR);
// 9. 跳转到应用程序入口
JUMP_TO(header.entry_addr);
}
特别注意:跳转前一定要把用到的外设关干净。SPI 控制器如果不复位,应用程序初始化时可能读到残留的中断标志。我遇到过 SPI 中断在跳转后立刻触发,而应用程序还没配置中断向量表——直接进异常。
3.3.4 二次 Bootloader 的链接脚本
二次 Bootloader 本身也需要链接脚本。你要确保它被加载到正确的 RAM 地址,并且不占用应用程序的 RAM 空间。我一般这样划分:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader RAM | 0x008000 | 4KB | 二次 Bootloader 代码和数据 |
| App 向量表 | 0x009000 | 256B | 应用程序中断向量表 |
| App 代码区 | 0x009100 | 剩余 RAM | 应用程序代码和堆栈 |
你想想看,如果 Bootloader 和应用程序的 RAM 区域重叠了,加载时就会互相覆盖——这 bug 查起来可够你喝一壶的。
个人习惯:我会在二次 Bootloader 的末尾加一个跳转表,方便以后升级。跳转表里放三个函数指针:加载、校验、跳转。这样主程序也可以通过调用 Bootloader 的接口实现在线升级,不用每次都重新烧录。
嗯,关于 Bootloader 和启动流程,核心就是这些。记住:上电流程是硬件定死的,你改变不了;Boot 模式是你选的,别选错;二次 Bootloader 是你写的,要稳、要小、要可靠。把这三点拿捏住,DSP 启动这块就不会出大问题。