3. C2000内存映射与CMD文件解析

说实话,我刚接触C2000那会儿,最头疼的就是CMD文件。看着那一堆MEMORY和SECTIONS的配置,完全不知道在干嘛。后来有一次,我把一个变量定义到了RAM里,结果程序跑飞了——查了两天才发现,原来是CMD文件里没给这个段分配空间。

从那以后,我下定决心要把内存映射和CMD文件彻底搞明白。今天咱们就来聊聊这块内容。

3.1 C2000的存储器架构

C2000系列DSP用的是哈佛架构,说白了就是程序总线和数据总线是分开的。这样做的好处是——取指令和读写数据可以同时进行,效率高。

咱们先看看典型C2000芯片的内存布局:

地址范围 存储器类型 用途
0x000000 - 0x003FFF M0 SARAM (1K x 16) 低地址RAM,常用于堆栈
0x004000 - 0x007FFF M1 SARAM (1K x 16) 高地址RAM
0x080000 - 0x0FFFFF Flash (最大512K) 程序存储
0x006000 - 0x006FFF PieVectTable 中断向量表
0x00C000 - 0x00C3FF Peripheral Frame 1 外设寄存器

嗯,这里要注意——不同型号的C2000,内存大小和地址范围会有差异。比如F28035和F28379D,Flash差了4倍都不止。所以写CMD文件前,一定要先看芯片的datasheet。

3.2 CMD文件到底在干什么?

CMD文件,全称是Linker Command File。它的核心任务就两个:

  • 定义物理存储器(MEMORY指令)
  • 把程序段分配到这些存储器里(SECTIONS指令)

我打个比方你就明白了。MEMORY就像你家的房间布局——客厅、卧室、厨房各有多大面积。SECTIONS就是你要把什么东西放到哪个房间——沙发放客厅,床放卧室,锅碗瓢盆放厨房。

核心观点:没有正确的CMD文件,编译器生成的代码就是一堆无家可归的数据。程序跑不跑得起来,CMD文件说了算。

3.3 MEMORY指令详解

咱们直接看一个实际例子。这是我给F28035写的一个典型MEMORY配置:

MEMORY
{
    PAGE 0:    /* 程序存储器 */
        FLASH    : origin = 0x3F0000, length = 0x010000
        RAML0    : origin = 0x008000, length = 0x001000

    PAGE 1:    /* 数据存储器 */
        RAMM0    : origin = 0x000000, length = 0x000400
        RAMM1    : origin = 0x000400, length = 0x000400
        RAML1    : origin = 0x009000, length = 0x001000
}

这里有几个关键点:

  • PAGE 0:程序空间,放代码和只读数据
  • PAGE 1:数据空间,放变量和堆栈
  • origin:起始地址
  • length:长度(单位是16位字,不是字节!)

我的经验:我习惯把RAM分成多个小块,比如RAML0、RAML1。这样做的好处是——调试时能清楚看到每个段用了多少空间。有一次我发现RAML0快满了,但RAML1还很空,调整一下段分配就解决了。

3.4 SECTIONS指令详解

SECTIONS指令告诉链接器:你的代码和数据,分别放到哪个存储器里。看这个例子:

SECTIONS
{
    .text       : > FLASH,      PAGE = 0
    .cinit      : > FLASH,      PAGE = 0
    .const      : > FLASH,      PAGE = 0
    .ebss       : > RAML1,      PAGE = 1
    .stack      : > RAMM0,      PAGE = 1
    .sysmem     : > RAML0,      PAGE = 1
}

每个段的作用:

段名 内容 建议存放位置
.text 可执行代码 Flash
.cinit 全局变量初始值 Flash
.const 常量数据 Flash
.ebss 全局/静态变量 RAM
.stack 堆栈空间 RAM
.sysmem 动态内存分配 RAM

注意:我曾经犯过一个错误——把.stack段放到了Flash里。结果程序一运行就死机,因为堆栈需要读写,而Flash只能读不能写。嗯,这个坑我替你们踩过了。

3.5 内存映射的SVG结构图

下面这张图,是我根据实际项目经验画的。它展示了C2000典型的内存映射关系:

C2000内存映射结构图 Flash (PAGE 0) .text - 程序代码 .cinit - 初始值 .const - 常量 只读,掉电不丢失 RAM (PAGE 1) .ebss - 全局变量 .stack - 堆栈 .sysmem - 动态内存 可读写,掉电丢失 外设寄存器空间 (Peripheral Frame) GPIO、ADC、PWM、eCAP、eQEP、SCI、SPI、I2C... 通过特殊地址访问,不可用于普通变量存储 PIE中断向量表 (PieVectTable) 存放中断服务函数入口地址 通常映射到RAM,便于动态更新 注:不同型号C2000的地址范围和大小可能不同,请以datasheet为准

3.6 实战:编写一个完整的CMD文件

好了,理论说完了,咱们动手写一个。这是我在一个电机控制项目里用的CMD文件:

/* F28035 CMD文件 - 电机控制项目 v2.1 */
MEMORY
{
    PAGE 0:
        FLASH       : origin = 0x3F0000, length = 0x00FC00
        BOOTROM     : origin = 0x3FFC00, length = 0x000400
        RAML0       : origin = 0x008000, length = 0x001000

    PAGE 1:
        RAMM0       : origin = 0x000000, length = 0x000400
        RAMM1       : origin = 0x000400, length = 0x000400
        RAML1       : origin = 0x009000, length = 0x001000
        RAML2       : origin = 0x00A000, length = 0x001000
        RAML3       : origin = 0x00B000, length = 0x001000
}

SECTIONS
{
    /* 程序代码 - 放在Flash */
    .text          : > FLASH,      PAGE = 0
    .cinit         : > FLASH,      PAGE = 0
    .const         : > FLASH,      PAGE = 0
    .switch        : > FLASH,      PAGE = 0

    /* 关键变量 - 放在RAM */
    .ebss          : > RAML1,      PAGE = 1
    .stack         : > RAMM0,      PAGE = 1
    .sysmem        : > RAML0,      PAGE = 1

    /* 快速中断代码 - 复制到RAM运行 */
    .TI.ramfunc    : LOAD = FLASH,
                      RUN = RAML2,
                      LOAD_START(_RamfuncsLoadStart),
                      LOAD_END(_RamfuncsLoadEnd),
                      RUN_START(_RamfuncsRunStart),
                      PAGE = 0
}

关键点说明:

  • .TI.ramfunc:这个段很特殊。它把代码从Flash复制到RAM里运行,速度更快。我一般在中断服务函数或者时间要求严格的循环里用这个技巧。
  • LOAD和RUN:LOAD是代码存放的位置(Flash),RUN是实际运行的位置(RAM)。启动时由boot代码负责复制。

3.7 避坑指南

我这些年调试CMD文件,踩过的坑不少。挑几个典型的说说:

  • 堆栈溢出:.stack段分配太小,函数调用层级一深就崩。我建议至少分配0x200(512字)以上。
  • 段重叠:两个段分配到了同一个地址范围。链接器不会报错,但运行结果完全不可预测。
  • Flash写入次数:别把频繁修改的变量放Flash里。Flash有擦写寿命限制,而且写入速度慢。
  • 对齐问题:有些段要求对齐到特定地址。比如PieVectTable必须对齐到256字边界。

我曾经踩过的坑:有一次我把.const段分配到了RAM里,结果程序下载后一断电,所有常量都丢了。嗯,从那以后我每次写CMD文件都会检查:只读数据是不是真的放在了只读存储器里。

3.8 调试技巧

最后分享几个调试CMD文件的小技巧:

  • 编译后查看.map文件,里面详细记录了每个段的地址和大小
  • 用CCS的Memory Browser直接查看某个地址的内容
  • 在代码里用#pragma CODE_SECTION#pragma DATA_SECTION强制指定某个函数或变量的存放位置

举个例子,你想把某个中断服务函数放到RAM里跑:

#pragma CODE_SECTION(isr_timer0, ".TI.ramfunc")
void isr_timer0(void)
{
    // 快速中断处理
}

这样写,链接器就会自动把这个函数放到我们之前定义的.TI.ramfunc段里。

好了,关于内存映射和CMD文件,今天就聊到这儿。这些东西看着枯燥,但确实是C2000开发的基石。你花半小时把CMD文件搞明白,后面能省下好几个通宵的调试时间。


公众号:蓝海资料掘金营,微信deep3321