DaVinci DM644x UART引导与闪存编程:从原理到工程实践
1. 项目概述:深入解析DaVinci DM644x的UART引导与闪存编程
在嵌入式系统开发中,引导加载程序(Bootloader)是连接硬件上电与操作系统启动的桥梁,其稳定性和灵活性直接决定了产品的可维护性与生命周期。对于基于德州仪器DaVinci系列TMS320DM644x这类高度集成的数字媒体片上系统而言,掌握其多样化的引导机制,尤其是通过串口(UART)进行系统初始化和固件更新的技术,是每一位嵌入式工程师从“会用”到“精通”的必经之路。这个项目聚焦于一个非常具体但极其强大的场景:如何利用DM644x处理器的UART引导模式,配合用户引导加载程序(UBL)和主机端工具,实现对板上NAND或NOR闪存的远程编程与更新。
你可能已经熟悉了通过JTAG或SD卡进行固件烧录的传统方式,但在产品量产后的现场维护、产线批量烧录或缺乏其他调试接口的极端情况下,UART引导模式的价值就凸显出来了。它仅需一根串口线,就能完成从芯片初始化、内存配置到最终应用程序加载的全过程,甚至能对引导介质本身进行“重编程”。这不仅仅是技术文档里的一段描述,而是一套经过工程验证的、包含完整软硬件交互协议的解决方案。本文将带你深入这套方案的每一个细节,从DM644x的启动流程解剖开始,到UBL与主机应用程序(如DVFlasher)的协同工作原理解析,最后给出实际的闪存编程操作步骤与避坑指南。无论你是正在维护基于DM644x的旧有设备,还是希望借鉴其设计思想应用于新的平台,这里的内容都将提供扎实的参考。
2. DM644x启动流程深度剖析与UBL的核心角色
要理解UART引导模式下的闪存编程,我们必须先回到起点,彻底弄清楚DM644x芯片从上电到执行用户代码的完整链条。这个过程并非单一路径,而是一个由硬件状态和软件逻辑共同决定的、充满分支的选择题。
2.1 硬件引导引脚与启动模式判定
DM644x的启动之旅始于复位信号释放的瞬间。此时,芯片内部的ROM引导加载程序(RBL)开始工作,它首先要回答一个根本问题:“我从哪里加载最初的代码?”这个问题的答案由芯片外部两个特定的引脚BTSEL[1:0]的电平状态决定。在常见的开发板(如Spectrum Digital DVEVM)上,这两个引脚连接着物理拨码开关(例如S3-1和S3-2),方便开发者手动配置。
- NAND Flash引导模式 (
BTSEL[1:0] = 00): 这是成本敏感、需要大容量非易失存储的常见选择。RBL会尝试从连接到AEMIF(异步外部存储器接口)CS2片选信号上的NAND Flash芯片中读取最初的引导代码。RBL内置了针对特定型号NAND Flash的驱动,能够进行基础的页读取和坏块处理。 - NOR Flash引导模式 (
BTSEL[1:0] = 10): 当需要原地执行(XiP)代码以获得更快启动速度时,会选用此模式。RBL几乎不做任何处理,直接将程序计数器(PC)跳转到NOR Flash映射的地址空间(通常是CS2的基地址0x02000000)开始执行。这意味着存储在NOR中的代码必须包含完整的自举和初始化例程。 - UART引导模式 (
BTSEL[1:0] = 11): 这就是我们本文的重点。在此模式下,RBL将UART0控制器初始化为默认波特率(如115200 bps),然后进入一个简单的协议握手状态,等待主机通过串口发送一段小程序,即用户引导加载程序(UBL)。
关键细节:
BTSEL引脚的状态会在复位时被锁存到BOOTCFG寄存器的特定比特位中。这个状态不仅决定了启动路径,还可能影响一些外设的默认电源和时钟配置。例如,在NAND启动失败时,RBL有回退机制,会自动尝试UART启动,但BOOTCFG寄存器仍然反映原始的引脚配置,这可能影响后续外设的初始化参数。
2.2 从RBL到UBL:引导链条的第一次交接
在UART引导模式下,RBL扮演着一个极其精简的“快递员”角色。它通过UART接收来自主机(通常是PC)的数据流。这个数据流不是任意的,它必须遵循RBL定义的一个简单协议:以特定的“引导请求”字符串开始,后跟数据长度和CRC校验,最后才是真正的二进制代码。RBL会将这些代码搬运到芯片内部的一块SRAM(通常是ARM内核的IRAM)中,验证其完整性,然后跳转到这片内存的起始地址执行。
这段被RBL加载并执行的代码,就是第一阶段的用户引导加载程序(UART UBL)。它的体积受到RBL所能加载大小的限制(在DM644x上通常是14KB左右)。因此,这个UART UBL的核心职责是“搭桥”——初始化更复杂的外设(尤其是DDR2内存控制器),为加载更大、功能更全的第二阶段代码(可能是另一个UBL,也可能是直接的应用如U-Boot)做好准备。而我们今天讨论的闪存编程功能,正是这个UART UBL在完成基础初始化后,根据主机命令所执行的一项扩展任务。
2.3 UBL的“三位一体”:一份代码,三种角色
本项目提供的UBL设计精妙之处在于,它是一份可以编译成三个不同变体的源代码,分别适配UART、NAND和NOR三种引导场景,实现“三位一体”。
- 作为UART引导加载程序:这是它的本职工作。被RBL加载后,它初始化系统,然后通过UART与主机通信,接收并执行主机发送的命令。这些命令除了“加载并运行一个应用程序到DDR”之外,还包括了我们关心的“对NAND/NOR闪存进行编程”。
- 作为NAND引导加载程序:当芯片从NAND启动时,RBL会在NAND的特定位置(如块1-5)寻找一个特殊的UBL头(包含魔数、入口地址、所占页数等信息)。找到后,RBL会将UBL代码本身加载到IRAM并执行。此时,UBL会检测到自己处于NAND引导模式,转而执行
NAND_Copy()函数,从NAND的后续块(如块6开始)寻找应用头和应用程序,将其加载到DDR并跳转执行。 - 作为NOR引导加载程序:当芯片从NOR启动时,PC直接跳转到NOR起始地址执行。因此,NOR UBL的二进制映像在链接时,其最前端放置了一段自拷贝代码。这段代码首先将自己从相对较慢的NOR Flash中复制到快速的IRAM中,然后再跳转到IRAM中的主入口点继续执行。之后的过程与NAND模式类似,UBL会寻找NOR中的应用头并加载应用程序。
这种设计极大地提高了代码的复用性和系统的可靠性。通过条件编译(#ifdef),在构建时选择所需的驱动模块(nand.c或nor.c),最终生成两个独立的二进制文件:ubl_davinci_nand.bin和ubl_davinci_nor.bin。主机应用程序(DVFlasher)会将这两个二进制文件作为资源嵌入,根据用户命令行参数选择发送哪一个到目标板。
3. UART引导模式下的闪存编程命令详解
当UBL在UART模式下运行起来后,它就从一个被动的加载器变成了一个能与主机交互的“命令解释器”。它通过串口发送BOOTPSP\0提示符,告知主机“我已就绪,请下达指令”。主机则回复^^^^CMD\0(^代表空格)后跟一个32位的“魔数”命令字。UBL根据这个命令字执行不同的操作。除了常规的加载运行命令(UBL_MAGIC_SAFE),最重要的就是针对NAND和NOR闪存的编程命令。
3.1 NAND闪存操作命令集
NAND UBL支持三个核心命令,其命令值均为特定的“魔数”:
| 命令名 | 命令值 (十六进制) | 功能描述 |
|---|---|---|
UBL_MAGIC_NAND_SREC_BURN | 0xA1ACEDBB | 向NAND闪存烧写UBL和一个S-Record格式的应用程序映像 |
UBL_MAGIC_NAND_BIN_BURN | 0xA1ACEDCC | 向NAND闪存烧写UBL和一个二进制格式的应用程序映像 |
UBL_MAGIC_NAND_GLOBAL_ERASE | 0xA1ACEDDD | 全局擦除NAND闪存(通常跳过坏块标记的块0) |
NAND烧写流程剖析: 以UBL_MAGIC_NAND_BIN_BURN为例,其交互流程堪称一场精密的“双人舞”:
- 握手与命令下发:UBL发送
BOOTPSP\0,主机回复^^^^CMD\0+0xA1ACEDCC。 - UBL映像传输:UBL回复
SENDUBL\0,要求主机发送将要被写入NAND的UBL映像。主机随后发送一个应答头(包含魔数、应用起始地址等)和UBL的S-Record格式数据。UBL端的UARTGetHeaderAndData()函数负责接收并解码S-Record,在DDR内存中生成二进制映像。 - NAND初始化与UBL写入:UBL调用
NAND_Init()识别闪存型号并初始化驱动。接着,它根据接收到的UBL二进制大小,计算需要占用的页数,并填充一个NAND UBL头结构体。关键点在于写入位置:UBL头必须写入块1的页0(这是RBL在NAND引导时搜索的起始位置)。UBL数据则从块1的页1开始写入。NAND_WriteHeaderAndData()函数会处理这些细节,并自动跳过坏块(如果块1是坏块,它会尝试块2,最多到块5)。 - 应用映像传输:UBL发送
SENDAPP\0,请求应用程序映像。主机再次发送应答头和应用数据(此时可以是S-Record或二进制,由命令决定)。 - 应用头写入与应用数据写入:UBL根据映像格式(二进制或S-Record)设置NAND应用头中的魔数(
UBL_MAGIC_SAFE或UBL_MAGIC_BIN_IMG)。应用头和应用数据被写入到块6的页0开始的位置(同样支持坏块跳过)。NAND_Copy()函数在引导时,就会从这个区域开始搜索应用头。
关于ECC的致命细节: NAND闪存可靠性依赖ECC(纠错码)。RBL在从NAND读取UBL时,会使用AEMIF硬件生成的ECC值与存储在NAND页备用区的ECC值进行校验。因此,任何向NAND写入UBL或应用数据的工具,都必须按照RBL的预期规则计算并写入正确的ECC值,否则引导必定失败。
- 对于256字节/页和512字节/页的NAND:ECC值(4字节)应写入备用区的起始4个字节(偏移0x00-0x03)。
- 对于2048字节/页的NAND:每512字节数据对应一个ECC值,这四个ECC值应分别写入备用区的偏移0x08, 0x18, 0x28, 0x38处。
- 字节序:所有ECC值必须按照大端序存储。 项目中的
nand.c驱动已经妥善处理了这些ECC的生成与写入,但如果你要移植或编写自己的烧写工具,这是必须严格遵守的“生命线”。
3.2 NOR闪存操作命令集
NOR UBL支持四个命令,比NAND多一个特殊的恢复命令:
| 命令名 | 命令值 (十六进制) | 功能描述 |
|---|---|---|
UBL_MAGIC_NOR_RESTORE | 0xA1ACED77 | 恢复命令。向NOR起始地址直接写入一个应用程序(如U-Boot),无需UBL头。用于恢复一个可独立启动的映像。 |
UBL_MAGIC_NOR_SREC_BURN | 0xA1ACED88 | 向NOR闪存烧写UBL和一个S-Record格式的应用程序映像 |
UBL_MAGIC_NOR_BIN_BURN | 0xA1ACED99 | 向NOR闪存烧写UBL和一个二进制格式的应用程序映像 |
UBL_MAGIC_NOR_GLOBAL_ERASE | 0xA1ACEDAA | 全局擦除NOR闪存 |
NOR烧写流程与NAND的关键差异:
- 无头搜索:NOR引导时,RBL直接跳转到0x02000000执行,不存在“头”的概念。因此,NOR UBL被设计为包含自拷贝代码(见附录B),该代码位于二进制文件的最前端,负责将整个UBL复制到IRAM。
- UBL写入位置:NOR UBL被直接写入NOR的基地址(0x02000000)。应用头和应用数据则写入在UBL所占空间之后的第一个完整块起始处。
NOR_Copy()函数在引导时,会从这个约定位置读取应用头。 - 恢复命令的特殊性:
UBL_MAGIC_NOR_RESTORE命令不涉及UBL。它直接请求主机发送一个应用程序(SENDAPP\0),然后将其擦除并写入NOR起始地址。这个命令常用于将U-Boot这样的完整引导器直接写入NOR,使系统能够直接从NOR启动,绕过了UBL。写入的映像必须自己包含所有必要的初始化代码。
NOR硬件配置的注意事项: NOR通过AEMIF以并行总线方式连接。nor.c驱动通过查询CFI(通用闪存接口)来识别芯片并适配命令集(AMD或Intel)。硬件上需注意EM_WIDTH引脚配置,必须与NOR芯片的实际数据位宽(8位或16位)匹配,否则访问会失败。驱动支持多种配置,包括单颗8位/16位器件,以及两颗8位器件并联成16位总线等模式。
4. 主机应用程序DVFlasher的设计与实现
UBL的强大功能需要主机端一个同样可靠的“舞伴”来触发和配合。DVFlasher就是这个用C#编写的主机应用程序,它负责与DM644x的RBL和UBL进行所有串口协议交互。
4.1 跨平台架构与设计思路
选择C#和.NET/Mono框架是一个深思熟虑的决定,旨在实现跨平台。开发者可以在Windows上使用.NET Framework,或在Linux/macOS上使用Mono运行时来编译和运行同一个DVFlasher程序,极大方便了不同开发环境下的使用。其核心设计是一个双线程模型:
- 主线程:负责解析命令行参数、打开串口、创建工作者线程,并监控用户按键(如ESC)以提供中止操作的能力。
- 工作者线程:承担所有与目标板通信的重任。包括与RBL握手、传输UART UBL、与运行中的UBL交互、发送命令、传输应用数据等。
4.2 核心交互协议与工作流程
主机与UBL的通信基于一套预定义的8字节(含结束符\0)字符串序列,这与RBL的协议风格一脉相承:
| 序列 | 产生方 | 描述 |
|---|---|---|
BOOTPSP\0 | UBL | UBL已加载完毕,正在等待命令 |
^^^^CMD\0 | 主机 | 前缀,后跟要执行的32位命令值 |
SENDUBL\0 | UBL | 指示主机发送要写入闪存的UBL |
SENDAPP\0 | UBL | 指示主机发送要写入闪存或运行的应用映像 |
^^^^ACK\0 | 主机 | 前缀,附加在所有S-Record映像的头部之前 |
^^BEGIN\0 | UBL | 表示ACK头已接收,准备接收S-Record数据 |
^^^DONE\0 | UBL | 表示命令成功完成 |
工作流程如图2所示,是一个严格的“请求-响应”模型。工作者线程在发送任何数据后,都会调用类似waitForSequence()的函数,阻塞等待目标板返回预期的序列(如^^^DONE\0)或表示失败的替代序列。这种同步机制确保了每一步操作都得到确认,提高了可靠性。
4.3 关键函数与资源嵌入
TransmitUARTUBL(): 此函数实现了与RBL的完整握手协议,将正确的UBL(NAND或NOR版本)通过串口下载到目标板的IRAM中。这是所有闪存操作的第一步。TransmitCMDSuccessful(): 发送命令字给已运行的UBL,并等待其返回BOOTPSP\0和^^^DONE\0。TransmitFLASHUBLandAPP(): 处理需要先写UBL再写应用的复杂闪存烧写命令。它依次处理SENDUBL和SENDAPP请求。- 资源嵌入:编译生成的
ubl_davinci_nand.bin和ubl_davinci_nor.bin被作为资源文件嵌入到DVFlasher的可执行文件中。程序运行时,通过GetEmbeddedUBLStream()函数读取这些资源,无需用户额外管理UBL文件,降低了使用复杂度。
5. 实战指南:使用DVFlasher进行闪存编程
理解了原理,我���来实际操作。假设你有一个DM644x开发板,串口已连接,电源和启动模式开关(BTSEL[1:0])已设置为UART引导模式(11)。
5.1 环境准备与编译
- 获取源码:从TI官网或相关资源库获取
SPRAAI4的源码包。 - 编译UBL:进入
ubl/src目录,执行make。这需要ARM交叉编译工具链(如arm-none-eabi-gcc)。确保Makefile中的工具链路径正确。编译后会生成ubl_davinci_nand.bin和ubl_davinci_nor.bin。 - 编译DVFlasher:进入主机应用程序目录。在Windows上,你可以用Visual Studio或
csc命令行编译器;在Linux上,使用mcs(Mono C#编译器)。例如:mcs -out:DVFlasher.exe DVFlasher.cs CRC32.cs。编译过程会自动将上一步的两个.bin文件作为资源嵌入。
5.2 命令行参数详解
DVFlasher是一个命令行工具,其基本语法为:
DVFlasher.exe [options]关键选项包括:
-p <COMx>: 指定串口端口,如-p COM3或-p /dev/ttyUSB0。-b <rate>: 指定波特率,默认为115200。-f <filename>: 指定要烧写的应用程序二进制文件。- 核心命令选项:
-n: 执行NAND闪存操作(需配合子命令)。-o: 执行NOR闪存操作(需配合子命令)。-s: 烧写S-Record格式映像。-i: 烧写纯二进制格式映像。-e: 执行全局擦除。-r:恢复命令,仅用于NOR(-o -r),直接烧写二进制文件到NOR起始地址。-u: 仅通过UART下载并运行应用,不烧写闪存。
5.3 典型操作示例
示例1:将U-Boot的二进制文件烧写到NAND闪存
# 假设串口是COM3,U-Boot二进制文件为u-boot.bin DVFlasher.exe -p COM3 -b 115200 -n -i -f u-boot.bin过程分解:
- 工具打开COM3,波特率115200。
- 检测到
-n选项,选择嵌入的NAND UBL。 - 与板卡RBL握手,下载NAND UBL到目标板IRAM。
- UBL运行,发送
BOOTPSP。 - 工具发送
^^^^CMD和UBL_MAGIC_NAND_BIN_BURN命令。 - 遵循
SENDUBL-> 传输UBL ->SENDAPP-> 传输u-boot.bin的流程,完成烧写。 - 等待UBL返回
^^^DONE,操作成功。
示例2:擦除整个NOR闪存
DVFlasher.exe -p /dev/ttyUSB0 -o -e注意:NOR闪存的全局擦除非常耗时,对于大容量芯片可能需要数分钟,期间请保持串口连接稳定,不要断电。
示例3:通过UART直接下载并运行一个调试程序
DVFlasher.exe -p COM4 -u -f my_app.bin这个命令不操作闪存,仅仅是将my_app.bin通过UART下载到DDR内存并运行,适用于快速迭代调试。
6. 常见问题、调试技巧与避坑指南
在实际操作中,你几乎一定会遇到各种问题。以下是我从多年经验中总结出的关键排查点和技巧。
6.1 连接与通信失败
- 症状:DVFlasher卡在“Waiting for BOOTPSP...”或类似阶段,无响应。
- 排查步骤:
- 确认启动模式:这是最常出错的地方!再三检查
BTSEL[1:0]开关是否确实设置为UART模式(1,1)。最好在断电情况下设置。 - 确认串口参数:波特率(默认115200)、数据位(8)、停止位(1)、校验位(无)。确保主机工具配置与RBL/UBL的UART初始化代码一致。
- 确认串口线:使用直连串口线或可靠的USB转串口模块。避免使用劣质转换器。
- 观察上电信息:打开串口终端(如Putty、SecureCRT),给板卡上电。在UART模式下,你应该能看到RBL打印出的少量启动字符(可能是乱码,但应有数据流)。如果没有任何输出,检查串口TX/RX线是否接反,或芯片的UART引脚是否被其他配置占用。
- 使用
-v参数:运行DVFlasher时加上-v(verbose)选项,它会打印出更多收发数据的信息,有助于定位协议在哪一步失败。
- 确认启动模式:这是最常出错的地方!再三检查
6.2 NAND引导失败
- 症状:设置为NAND启动后,系统无反应或无法找到UBL。
- 排查步骤:
- 验证烧写结果:先用UART模式启动,使用DVFlasher的
-n -u命令尝试从NAND加载并运行你刚烧写的程序。这可以验证烧写过程本身和UBL头是否正确。 - 检查ECC:如果烧写由自制工具完成,首要怀疑ECC错误。使用TI原厂或经过验证的工具(如旧版CCS的Flash工具)重新烧写一次,如果成功,则基本可确定是ECC问题。仔细核对NAND页大小,确保ECC值被写入备用区的正确位置且为大端序。
- 检查UBL存放位置:确认UBL被烧写到了正确的起始块(块1)。如果块1是坏块,UBL应该被烧写到块2-5。使用NAND厂商工具或编写简单读取程序,检查这些块页0的前几个字节是否是有效的魔数(如
0xA1ACED00)。 - 检查NAND型号支持:查阅
nand.c源文件顶部的设备ID表(附录D),确认你的NAND芯片ID在支持列表中。如果不在,需要手动添加该型号的参数(块数、页数、页大小等)。
- 验证烧写结果:先用UART模式启动,使用DVFlasher的
6.3 NOR引导失败
- 症状:设置为NOR启动后,系统挂起或跑飞。
- 排查步骤:
- 检查自拷贝代码:NOR UBL最前端必须是自拷贝代码。使用二进制查看工具,确认烧写到NOR 0x02000000地址的数据,其开头是否是一段ARM汇编指令(通常是
MRC,MOV,MCR等操作协处理器c9的指令)。 - 检查AEMIF配置:确认
EM_WIDTH引脚设置与NOR芯片的位宽(8位或16位)匹配。配置错误会导致读出的指令码完全错误。 - 验证CFI:在UART模式下,可以增强UBL代码,使其在初始化NOR后,将读取到的CFI信息打印出来。确认芯片被正确识别。
- 使用恢复命令:尝试用
-o -r命令直接烧写一个已知良好的、可独立运行的二进制文件(如一个简单的LED闪烁测试程序)到NOR起始地址。如果这样能启动,说明是UBL本身或应用头的问题。
- 检查自拷贝代码:NOR UBL最前端必须是自拷贝代码。使用二进制查看工具,确认烧写到NOR 0x02000000地址的数据,其开头是否是一段ARM汇编指令(通常是
6.4 编译与链接问题
- UBL大小超限:原始设计将NAND和NOR驱动都编译进去会导致UBL超过14KB。项目通过条件编译生成两个独立的UBL变体来解决。如果你添加了新功能导致UBL变大,需要检查链接脚本(
ubl_davinci.lds),确保代码段、数据段等总和不超过IRAM可用空间(且小于RBL加载限制)。 - 地址对齐:在链接脚本和代码中,要特别注意地址对齐问题。例如,NOR的擦除和写入通常以扇区(Sector)或块(Block)为单位,这些操作需要地址按块大小对齐。不对齐的地址会导致擦除或写入失败。
6.5 性能与可靠性优化建议
- 增加超时与重试机制:在DVFlasher的串口等待函数(
waitForSequence)中,除了检测正确和错误序列,还应加入超时判断。如果长时间未收到响应,应主动重试或报错退出,避免程序假死。 - 实现进度反馈:在传输大型应用文件(如Linux内核)时,可以在UBL端每接收一定数量数据(如1KB)就向主机发送一个进度字符(如
.),主机端将其显示出来。这能极大提升用户体验,让用户知道传输仍在进行。 - 校验与验证:烧写完成后,可以增加一个“读取-校验”环节。UBL可以提供一个“读取闪存内容并返回”的命令,主机发送该命令,读取刚写入区域的数据,并与原始文件进行比对,确保烧写无误。
- 日志记录:在DVFlasher中实现简单的日志功能,将每次操作的时间、命令、成功与否记录到文件,便于后续追溯问题。
这套基于UART的引导和闪存编程方案,虽然源于十多年前的DaVinci平台,但其设计思想——清晰的层次划分(RBL/UBL/App)、严谨的握手协议、考虑周全的异常处理(坏块跳过、ECC)——在今天依然具有很高的参考价值。它展示了在资源受限的嵌入式环境中,如何通过最精简的接口(UART)实现最强大的系统管理功能。当你下次面对一个没有网络、没有USB、只有串口的“黑盒子”需要更新固件时,希望这篇文章和���中的经验能为你点亮一盏灯。