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典型的内存映射关系:
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