4. 工程模板创建:从零创建DSP工程、链接器命令文件(CMD)解析、启动文件(Bootloader)配置

说实话,很多做DSP开发的朋友,一开始都习惯直接拿别人的工程改。我以前也这么干,结果有一次项目联调,程序跑飞了,查了三天才发现是CMD文件里内存分配出了错。从那以后,我坚持每个项目都从零创建工程模板。这活儿看着琐碎,但能帮你把整个芯片的“家底”摸清楚。

这一章,我们就手把手把工程模板搭起来。我会把链接器命令文件(CMD)和启动文件(Bootloader)这两个最容易被忽视的“幕后功臣”讲透。

4.1 从零创建DSP工程:三步走

我习惯用CCS(Code Composer Studio)做开发。不同版本界面略有差异,但核心逻辑不变。咱们以C2000系列为例,走一遍流程。

  1. 选型与创建:打开CCS,点击 Project → New CCS Project。在弹窗里选好你的DSP型号,比如TMS320F28335。编译器我建议选TI v20.2.x或更高版本,对C99支持更好。
  2. 配置工程属性:右键工程 → Properties。这里重点看两个地方:
    • Include Options:添加你存放头文件的路径。我个人习惯把所有外设驱动头文件放在 /include 目录下。
    • File Search Path:如果用到TI官方的库函数,比如IQmath,记得把库文件路径加进来。
  3. 添加核心文件:一个能跑起来的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里。

为了让你更直观地理解,我画了一张内存布局图:

DSP内存布局与CMD映射关系 物理内存(MEMORY) PAGE 0 - 程序空间 BEGIN (0x000000 - 0x000002) FLASHA (0x3F0000 - 0x3F1F80) RAML0 (0x008000 - 0x009000) PAGE 1 - 数据空间 RAMM0 (0x0000C0 - 0x0004C0) RAMM1 (0x000400 - 0x000800) 段分配(SECTIONS) 程序段 → PAGE 0 .text → FLASHA .cinit → FLASHA codestart → BEGIN 数据段 → PAGE 1 .stack → RAMM0 .bss → RAML0 .const → FLASHA 关键原则:可读写段(.stack/.bss)必须放在RAM,只读段(.text/.const)可放在Flash

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频率配置就行。省时省力,还不会踩坑。