4. 工程模板创建:从零创建DSP工程、链接器命令文件(CMD)解析、启动文件(Bootloader)配置
说实话,很多做DSP开发的朋友,一开始都习惯直接拿别人的工程改。我以前也这么干,结果有一次项目联调,程序跑飞了,查了三天才发现是CMD文件里内存分配出了错。从那以后,我坚持每个项目都从零创建工程模板。这活儿看着琐碎,但能帮你把整个芯片的“家底”摸清楚。
这一章,我们就手把手把工程模板搭起来。我会把链接器命令文件(CMD)和启动文件(Bootloader)这两个最容易被忽视的“幕后功臣”讲透。
4.1 从零创建DSP工程:三步走
我习惯用CCS(Code Composer Studio)做开发。不同版本界面略有差异,但核心逻辑不变。咱们以C2000系列为例,走一遍流程。
- 选型与创建:打开CCS,点击
Project → New CCS Project。在弹窗里选好你的DSP型号,比如TMS320F28335。编译器我建议选TI v20.2.x或更高版本,对C99支持更好。 - 配置工程属性:右键工程 →
Properties。这里重点看两个地方:Include Options:添加你存放头文件的路径。我个人习惯把所有外设驱动头文件放在/include目录下。File Search Path:如果用到TI官方的库函数,比如IQmath,记得把库文件路径加进来。
- 添加核心文件:一个能跑起来的DSP工程,至少需要三个文件:
- 主程序文件(main.c):你的业务逻辑。
- 链接器命令文件(.cmd):告诉编译器内存怎么划分。
- 启动文件(DSP2833x_CodeStartBranch.asm 或类似):上电后的第一段代码。
我的小习惯:我会在工程根目录下建一个 /source 放.c文件,/include 放.h文件,/cmd 放链接文件。这样项目大了以后,找东西不抓瞎。
4.2 链接器命令文件(CMD)解析:内存的“房产证”
CMD文件说白了,就是告诉编译器:“这段代码放哪儿,那个变量存哪儿。” 你想想看,DSP内部有RAM、有Flash、还有各种外设寄存器空间。如果分配错了,程序要么跑不起来,要么跑起来也是“神经病”。
一个典型的CMD文件长这样:
MEMORY
{
PAGE 0: /* 程序空间 */
BEGIN : origin = 0x000000, length = 0x000002 /* 引导跳转向量 */
RAML0 : origin = 0x008000, length = 0x001000
FLASHA : origin = 0x3F0000, length = 0x001F80
PAGE 1: /* 数据空间 */
RAMM0 : origin = 0x0000C0, length = 0x000400
RAMM1 : origin = 0x000400, length = 0x000400
}
SECTIONS
{
/* 程序段 */
.text : > FLASHA, PAGE = 0
.cinit : > FLASHA, PAGE = 0
.switch : > FLASHA, PAGE = 0
/* 数据段 */
.stack : > RAMM0, PAGE = 1
.bss : > RAML0, PAGE = 1
.const : > FLASHA, PAGE = 1
/* 引导段 */
codestart : > BEGIN, PAGE = 0
}
这里有两个关键块:
- MEMORY:定义芯片物理内存的“地块”。
PAGE 0是程序空间,PAGE 1是数据空间。每个地块都有起始地址和长度。 - SECTIONS:把编译器生成的段(比如代码段
.text、未初始化变量段.bss)分配到具体地块里。
我曾经踩过的坑:有一次我把 .stack 段分配到了Flash里。结果程序一调用函数就死机。为什么?因为栈是需要读写的,而Flash只能读不能写!记住:.stack、.bss、.sysmem 这些可读写段,必须放在RAM里。
为了让你更直观地理解,我画了一张内存布局图:
4.3 启动文件(Bootloader)配置:芯片的“闹钟”
DSP上电后,CPU会从复位向量指向的地址开始执行。这个地址通常就是Flash的起始地址 0x3F0000。但问题来了——Flash里存的是你的应用程序,而应用程序需要先初始化C运行环境(比如清零BSS段、设置栈指针)才能跑main函数。谁来做这件事?就是启动文件。
我以TI官方例程中的 DSP2833x_CodeStartBranch.asm 为例,核心代码就几行:
.ref _c_int00 ; 引用C运行时入口
.global codestart
.sect "codestart" ; 这个段必须放在BEGIN地址
codestart:
LB _c_int00 ; 长跳转到C初始化函数
这段代码干了什么?
.sect "codestart":告诉编译器,这段代码属于codestart段。还记得CMD文件里我们把codestart分配到了BEGIN地址吗?对上了。LB _c_int00:一条无条件长跳转指令,跳到C运行时初始化函数_c_int00。这个函数由编译器提供,负责设置栈、初始化全局变量,最后调用你的main()。
这里有个细节:有些DSP型号(比如C2000)在上电后会自动从Flash的 0x3F0000 地址读取复位向量。这个向量必须指向 codestart 段。所以,CMD文件里 BEGIN 的起始地址必须等于复位向量地址。我见过有人把 BEGIN 地址写错了,结果芯片上电后直接“死机”,连仿真器都连不上。
除了跳转,启动文件有时还需要做硬件初始化。比如:
- 关闭看门狗(默认是开着的,如果不关,程序会不断复位)。
- 配置PLL锁相环,把CPU频率从默认的慢速提升到目标频率。
- 初始化外设时钟使能。
这部分代码通常放在一个单独的 InitSysCtrl() 函数里,在 main() 开头调用。但启动文件本身只负责跳转,不做这些复杂初始化——这是为了保持启动文件的“纯净”和可移植性。
我曾经犯过的错:有一次我把看门狗关闭代码写在了 main() 函数里,结果程序在进入 main() 之前,看门狗已经超时复位了。芯片一直在“上电→复位→上电→复位”的循环里打转。后来我把看门狗关闭代码放到了启动文件里,在跳转到 _c_int00 之前就执行,问题解决。记住:启动文件是芯片上电后第一个执行的代码,越早做关键配置越好。
4.4 工程模板的“骨架”检查清单
当你把上面三个文件都准备好后,我建议你做一个“冒烟测试”——编译通过后,用仿真器下载到芯片里,看能不能停在 main() 函数的第一行。如果能,恭喜你,模板搭好了。
下面这个表格是我每次创建新工程时都会对照的检查清单:
| 检查项 | 关键点 | 常见错误 |
|---|---|---|
| CMD文件 | MEMORY地址范围与芯片手册一致 | Flash/RAM地址写错,导致链接失败 |
| CMD文件 | SECTIONS中可读写段在RAM中 | .stack/.bss放在Flash,运行时写保护异常 |
| 启动文件 | codestart段定位在BEGIN地址 | 跳转地址不对,芯片上电后跑飞 |
| 启动文件 | 关闭看门狗在跳转前完成 | 看门狗在main()执行前超时复位 |
| 工程属性 | 包含路径指向正确的头文件目录 | 找不到外设寄存器定义,编译报错 |
| 工程属性 | 编译器版本与库文件兼容 | 链接时符号不匹配,出现未定义错误 |
嗯,到这里,你的工程模板就有了一个坚实的“骨架”。以后每开始一个新项目,直接复制这个模板,改改CMD文件里的内存大小和启动文件里的CPU频率配置就行。省时省力,还不会踩坑。