简介:这是一份面向DSP开发者的EMIF(外部存储器接口)实验例程资源,围绕TMS320C6000等平台讲解如何配置EMIF以连接SDRAM、SRAM等外部存储设备,既适合初学者理解接口时序,也适合有经验的工程师快速对照参考。压缩包共21个文件,整体仅266KB,包含C语言源程序、DSP工程文件(pjt)、链接命令文件(cmd/lkf)、GEL调试脚本以及map/out等编译中间产物,可从寄存器配置一路跟踪到实际编译输出,便于完整梳理EMIF初始化流程。目前已有190人学习下载。通过该例程,可以掌握EMIF地址映射、时钟控制、读写周期参数设置、刷新与数据对齐等关键操作,并参考其中的初始化序列与调试脚本排查常见的外部存储器交互问题;对希望深入理解DSP存储接口原理或优化系统稳定性的开发者,是一份小巧但实用的参考材料。
1. 一个EMIF例程,解决DSP外部存储的时序问题
拿到一个标注着EMIF.rar_EMIF_dsp_dsp_emif的压缩包,里面的文件后缀是.pjt、.cmd、.gel、.c和一堆 Debug 目录下的.obj、.out、.map,几乎可以断定是 TI Code Composer Studio(CCS)下的一个 DSP 工程。而工程名里反复出现的 EMIF,就是 External Memory Interface,外部存储器接口。这个接口负责把 DSP 核心和 SDRAM、SRAM、Flash 这类外部器件连接起来,它的配置直接决定了数据读写的正确性和速度。对初学者来说,EMIF 往往是 DSP 系统里最容易踩坑的地方:看起来就是几个寄存器,但时序差一点,数据就错位;刷新周期设错,SDRAM 直接丢数据。这个例程的宝贵之处在于,它用 C5509 的完整工程展示了从寄存器配置、存储器映射到链接脚本和 GEL 初始化的全套流程,既可以照着改到自己的板子上,也能作为排查时序问题的参照。
2. C5509 的 EMIF 模块:寄存器、时序与存储器映射
2.1 EMIF 的工作机制与选型理由
DSP 核心运算再快,数据进不来就没意义。C5509 的 EMIF 模块是专门管理外部存储访问的硬件单元,它做的事情可以拆成三块:地址译码、时序生成、数据缓冲。地址译码决定了 DSP 发出的地址落在哪个片选空间,C5509 提供了 CE0~CE3 四个片选空间,每个空间可以独立配置为不同类型的存储器;时序生成是指 EMIF 根据配置寄存器产生读/写使能、片选、字节使能等控制信号,并插入等待周期;数据缓冲则是为了匹配不同位宽存储器(8/16/32 位)与 CPU 总线之间的数据对齐。
在 C5509 这样的 C5000 系列 DSP 上,EMIF 还承担了一个特殊任务:系统的引导加载(bootloader)也依赖它。如果外部接了 Flash,上电后 ROM 里的引导代码会通过 EMIF 把程序搬到 RAM 里,这个过程的时序参数和存储器类型必须和 Flash 的实际规格匹配,否则 boot 直接失败。所以学 EMIF 不只是配几个寄存器,它直接影响整个系统能否跑起来。
2.2 C5509 EMIF 的关键寄存器组
C5509 的 EMIF 控制寄存器分布在 CPU 外设寄存器空间,每个片选空间有独立的配置寄存器。理解寄存器比背地址更关键,因为例程的emif.c里主要就是这些寄存器赋值。下面这张表列出了最常见的几个:
| 寄存器名 | 作用 | 关键位段 |
|---|---|---|
| EMIF_CE0_CTL | CE0 空间控制 | MTYPE(存储器类型)、读/写建立/选通/保持时间 |
| EMIF_CE1_CTL | CE1 空间控制 | 同上 |
| EMIF_CE2_CTL | CE2 空间控制 | 同上 |
| EMIF_CE3_CTL | CE3 空间控制 | 同上 |
| EMIF_SDRAM_CTL | SDRAM 全局控制 | SREF(自刷新)、CAS 延迟、刷新计数值 |
| EMIF_SDRAM_TIM | SDRAM 时序 | 初始化周期、行/列选通延时 |
| EMIF_ARR_CTL | 地址重映射控制 | 仅在特定启动模式下使用 |
MTYPE字段是最先要定的:它告诉 EMIF 这个片选上挂的是什么器件。比如 0x01 表示 16 位异步 SRAM,0x08 表示 SDRAM 等。如果把 Flash 的类型设置成了 SRAM,EMIF 会在读 Flash 时用 SRAM 的时序,通常也能读,但等待周期和使能信号极性不对,长时间运行容易出错。例程里如果只看到emif.c而没有区分memtype,多半是用了固定的宏定义,后面会看到。
2.3 异步存储器的时序参数与计算
异步 SRAM/NOR Flash 的访问时序由三个时间决定:建立时间(Setup)、选通时间(Strobe)、保持时间(Hold)。C5509 的 EMIF 寄存器里,这三个时间分别用若干位段表示,单位是 CPU 时钟周期 T。例如对于 16 位异步 SRAM,读周期时序要求片选和读使能在地址有效后至少等待一定的建立时间,然后拉低读使能,数据在选通结束后被锁存,最后再保持一段时间。
常见的计算方法如下,假设 CPU 主频为 144MHz,即 T≈6.94ns,而某型号 SRAM 的读周期最小建立时间为 10ns,则需要配置为至少 2 个 T(约 13.9ns)。写周期类似,但要注意写使能脉冲宽度,C5509 的 EMIF 寄存器给的是写选通长度可以比读选通多一个周期。我一般会先按数据手册取整多留 1 个 T 作为裕量,因为板级走线电容会让信号边沿变缓,时序太紧容易出现偶发的数据错误,而这种错误用逻辑分析仪都不一定抓得到。
下面是一段针对异步 SRAM 的配置代码,常见做法是先在emif.h中定义时钟参数:
#define CPU_CLK_MHZ 144 #define ONE_T_NS (1000.0f / CPU_CLK_MHZ) #define SETUP_CYCLES ((10 + ONE_T_NS - 1) / ONE_T_NS) // 向上取整 #define STROBE_CYCLES ((35 + ONE_T_NS - 1) / ONE_T_NS) #define HOLD_CYCLES ((5 + ONE_T_NS - 1) / ONE_T_NS)这里用向上取整保证实际的建立时间不小于器件需求。STROBE_CYCLES和SETUP_CYCLES的最终值要再拼进 EMIF_CE0_CTL 寄存器的对应位段。但 C5509 的寄存器位段宽度有限,比如 Setup 是 4 位、Strobe 是 6 位,配置前要检查计算值是否超出最大值,否则必须降低 CPU 主频或换更快的存储器。
2.4 从 .cmd 文件看存储器映射
例程里的emif.cmd是链接命令文件,它把程序段分配到 DSP 的地址空间。C5509 内部 RAM 和外部 CE 空间都是统一编址的,.cmd里常定义MEMORY描述各区域,再用SECTION把代码段和数据段映射进去。一份典型配置可能长这样:
MEMORY { DARAM (RWI) : origin = 0x10000, length = 0x10000 SARAM (RWI) : origin = 0x20000, length = 0x20000 CE0_EXT (RW) : origin = 0x400000, length = 0x400000 } SECTIONS { .text > DARAM .bss > DARAM .cinit > DARAM .external > CE0_EXT }这里的CE0_EXT起始地址就是硬件上 CE0 片选的 base address。C5509 的 CE0 地址空间通常从 0x400000 开始,长度由引脚配置和 SDRAM 大小决定。如果外部挂的是 16 位 SDRAM,长度往往设成 0x400000(4M 字节),但这里的“长度”含义要小心:它表示地址空间范围,不直接等于实际器件容量。地址线有限时,大地址会回卷(wrap-around)到同一物理存储,这常用于地址解复用。
这个.cmd文件也决定了例程里emif.out的段布局。如果程序里直接把变量定义到了CE0_EXT区,访问该变量就等价于操作外部存储器,这种用法常在存储器测试程序中出现,下面一章会讲具体实现。
3. 从EMIF.c到初始化代码:寄存器配置实操
3.1 工程文件结构说明
压缩包里的文件有很强的 CCS 工程特征。.pjt是工程文件,记录编译选项和源文件列表;Debug.lkv、Debug.lkf是链接器命令的缓存文件;.wks是工作区文件;c5509.gel是 CCS 扩展语言脚本,用来在调试器连接后初始化目标板;emif.c是核心源码;readme.txt一般是作者留下的编译和运行说明。SYMBOL.DBF、FILE.DBF这些是早期 CCS 的数据库文件,记录符号和源文件索引,现代的 CCS 已经不再依赖它们,但保留不影响重新编译。
cc_build_Debug.log是上一次编译的日志,里面会记录编译命令和警告。遇到环境问题,比如找不到头文件或者格式错误,先查这个 log 比直接问人快得多。我一般会把它当作文档阅读:编译器的版本、优化级别、是否包含调试符号都会体现在里面。
3.2 初始化代码的完整骨架
假设例程面向一块挂载了 16 位异步 SRAM 和 SDRAM 的评估板,emif.c里的初始化函数通常长这样(这是根据该场景补全的常见实现,不是原包内容):
#include "c5509.h" #include "emif.h" void EMIF_Init(void) { // CE0: 16位异步SRAM,主频144MHz // 建立时间10ns -> 2T, 选通时间30ns -> 5T, 保持时间5ns -> 1T *(volatile unsigned int *)0x080000 = (1 << 14) // SB: 禁止等待控制(外部WAIT引脚忽略) | (2 << 12) // TA: 总线周转时间 = 2T | (1 << 8) // HOLD: 保持时间 = 1T | (4 << 4) // STROBE: 选通时间 = 4+1 = 5T | (2 << 0) // SETUP: 建立时间 = 2T | (0x01 << 16); // MTYPE: 16-bit异步SRAM }注意这里用了直接地址0x080000,但在不同的 C5509 外设基地址上可能有差异,实际应该查芯片 datasheet 确认。代码里的注释已经把各位段对齐了。MTYPE是七域写在最高的位段,通过<<16设置,这个设计容易踩坑,因为很多参考代码只改低 16 位,导致存储器类型还是默认的异步 SRAM,恰好和实际器件一致所以不出问题,看起来“能用”但配置其实是错的。
3.3 参数取值与性能/稳定性的权衡
前面的代码里选通时间被设置成4+1 = 5T,即 5 个 CPU 周期。为什么是加 1?因为 C5509 的 STROBE 位段写入数值 N 表示实际选通时间为 N+1 个周期。这是为了满足最短选通脉冲宽度的要求,最小值是 1T,所以写 0 也能工作。如果你发现代码里 STROBE 位段为(5 << 4),那实际是 6T,不是 5T,计算访问时间时一定要把 +1 算进去。
读写时间和SB(Super Byte)位还有一个交互:SB=1时忽略外部 WAIT 引脚,EMIF 按固定延时完成访问;SB=0时外部器件可以通过拉低 WAIT 延长访问周期,代价是增加了等待状态。例程要演示稳定的外部存储器交互,一般会先设SB=1固定时序,等基本功能通了再改SB=0接入慢速器件。这算是个调试技巧:先把时序调死,有问题就是 EMIF 配置本身的问题,而不是外部器件时序波动干扰了判断。
SDRAM 的初始化比异步 SRAM 复杂得多,需要先向所有 bank 发送预充电命令,然后执行多个刷新周期,最后设置模式寄存器。下面这段是常见的 SDRAM 初始化序列:
#define SDRAM_TIM (*(volatile unsigned int *)0x080008) #define EMIF_SDRAM_CTL (*(volatile unsigned int *)0x08000C) void SDRAM_Init(void) { // 先设置时序寄存器:INIT=10, RC=10, RAS=5, RP=3 SDRAM_TIM = (10 << 12) | (10 << 8) | (5 << 4) | (3 << 0); // 使能自刷新,CAS=2,刷新周期为1120个CPU周期 EMIF_SDRAM_CTL = (1 << 12) | (0x01 << 8) | (0x20 << 0); // 等待至少100us,让SDRAM完成初始化 Delay(1000); }Delay(1000)是粗略的等待函数,具体延时由 CPU 主频决定。这里最常出错的是刷新周期,SDRAM 要求每个 bank 在 64ms 内至少刷新 4096 次,换算到 CPU 周期时必须留足余量,否则高负载下会丢数据。例程如果只测异步 SRAM,这部分代码可先注释掉,避免因为 SDRAM 未配置就访问其空间产生总线上不可预测的错误。
3.4 写一个简单的读写校验函数
完整的 EMIF 初始化例程一般会带一个自测函数,目的是确认配置有效。核心思路非常简单:往一段地址写一个特征值,再读出来比较。但要注意,编译器会做优化,可能把这种读写删除或缓存到寄存器里,必须使用volatile指针:
#define SRAM_BASE 0x400000 #define TEST_COUNT 0x1000 int EMIF_MemTest(void) { volatile unsigned short *p = (volatile unsigned short *)SRAM_BASE; unsigned int i; // 先写后读:地址取反再异或,制造交替校验 for (i = 0; i < TEST_COUNT; i++) { unsigned short pattern = (unsigned short)((i << 4) ^ (i >> 3)); p[i] = pattern; } for (i = 0; i < TEST_COUNT; i++) { unsigned short expect = (unsigned short)((i << 4) ^ (i >> 3)); if (p[i] != expect) { return -1; // 返回出错地址 } } return 0; }这个函数写完后读,可以同时发现数据线和地址线的固有问题。若只写同一个值(如全0xAA),地址线 A0~A15 之间短路可能测不出来。使用 i 参与运算的伪随机模式,能让数据线和地址线都得到充分翻转。跑出错误时,先看是哪个地址错,再对照原理图检查对应地址线,往往比一步步调试寄存器快得多。
4. 基于 CCS 的编译、链接与加载调试
4.1 工程配置文件与 CCS 版本选择
这个例程的工程文件扩展名属于较老一代的 CCS(如 CCS 2.x/3.x)。在 CCS 6 及以后版本中,打开.pjt工程通常需要选择“Import Legacy Project”,导入后会自动转换为新的工程格式。如果直接添加.c和.cmd文件到空工程,语法和链接选项不会有太大变化,所以不必纠结版本,只要能编译出.out即可。
.gel文件是调试阶段使用的脚本,会在仿真器连接目标板后自动执行,通常用来配置 CPU 时钟、初始化 EMIF 以及下载代码到指定地址。c5509.gel里常见的函数名是OnTargetConnect(),它会在每次连接目标时被 CCS 调用。一个典型的 GEL 文件片段:
StartUp() { SetupEmif(); } SetupEmif() { *(int *)0x080000 = 0x0001; *(int *)0x080008 = 0x4DB7; }这里用 GEL 语言直接写寄存器地址,写法与 C 代码不同,但效果一样。GEL 的好处是不需要编译,改完直接在 CCS 里加载就生效,适合调试初期快速尝试不同时序参数。注意 GEL 文件里的0x080000必须是 C5509 的寄存器物理地址,如果该地址在片内 SRAM 区域,写入不会报错但寄存器和内存同时被写,会产生非常隐蔽的 bug。
4.2 编译链接过程与常见错误
在 CCS 中执行 Build 时,emif.c会被 C5500 的编译器编译成.obj,链接器根据.cmd文件将多个.obj合并成.out。Debug.lkv和Debug.lkf记录了上一次成功链接的完整命令行参数,如果后续手改.cmd后链接行为没有变化,先清理掉这两个缓存文件再重新 Build,有时能解决“改了没生效”的玄学问题。
比较典型的错误有两类。一类是C5000 C Compiler Error,提示undefined symbol,这通常是因为emif.c里引用了某个函数但工程没包含相应源文件。另一类是链接器报cannot align section,说明.cmd中段的起始地址不能对齐,比如length = 0x10000却把段放在0x10001的位置。查这类问题要打开.map文件,它列出每个段最终落在哪个地址,直接对地址数字就能看出来是不是说好放在 RAM 的段被挤到了外部。
4.3 用 CCS 连接仿真器并加载程序
连接仿真器的标准步骤是:先把c5509.gel加入到工程的“GEL Files”列表中,然后选择仿真器型号,配置硬件平台,点击 Connect。连接成功后,CCS 会执行 GEL 里的StartUp(),目标板开始运行时钟,初始化 EMIF,否则直接 Load Program 会失败,因为代码要写入的外部 SDRAM 还没初始化。
加载.out文件时,可以在 CCS 的 Console 窗口看到类似“Loaded .text section to 0x10000”的提示。这一步是验证.cmd配置是否正确的关键时刻。如果加载后 PC 指针停在 0 地址或一个意外值,多半不是代码问题,而是 GEL 初始化的 EMIF 和.cmd里定义的存储器映射不一致。比如 GEL 里把 CE0 配成了异步 SRAM,但.cmd把一个段放到了 CE0 的 SDRAM 区域,程序就会在访问该段时陷入总线忙等待。
4.4 使用 .map 和 .out 文件回溯配置
工程自带的emif.map是链接器的映射输出,里面有着最原始的符号地址。对于经验丰富的人来说,这个文件的参考价值超过代码里的注释。例如你看到某函数EMIF_Init被放在0x10010,而0x10000是内部 DARAM 的起始地址,就能确认.cmd的段配置成功;如果函数地址出现在0x400000,说明emif.c本身被分配到了外部 SRAM,这在调试外部存储器时才合理,否则程序运行时必须先初始化 EMIF 才能执行后续代码,这形成了一个先有鸡还是先有蛋的问题。解决方法是把初始化函数放在内部 RAM 段,再在数据初始化完成后跳转到外部存储执行。这个技巧在下一章还会展开。
5. 进阶:EMIF 时序调优与驻留 RAM 函数的注意事项
5.1 SDRAM 刷新与性能的平衡
SDRAM 的刷新请求由 EMIF 内部定时器周期触发,刷新期间外部访问会被挂起。如果EMIF_SDRAM_CTL中的刷新计数值设置偏小,刷新过于频繁,DSP 访问效率会下降;偏大则可能超出器件要求的最大刷新间隔,导致数据退化。C5509 的参考手册里通常给出一个公式:刷新计数(Refresh Count)= 刷新间隔 / CPU 周期 - 1。例如 64ms / 8192 行约等于 7.8us,若 CPU 主频 144MHz,则约为 1123 个周期,设置值接近 1120。
验证刷新配置是否合理的一种方法是谁也跑不了的:直接放一个长时间运算,同时访问 SDRAM 中的一个环形缓冲区,加入一个校验和,运行几小时后如果校验失败,多半是刷新配置太临界。真正高效的调优不是不断改变计数值去试错,而是先根据 datasheet 算出理论值,再设置为理论值的 90%,给温度漂移留一点余量。
5.2 把函数放到内部 RAM 运行
在 TI DSP 上,__attribute__((ramfunc))是一个极为实用的属性,它告诉链接器把指定函数从 Flash 搬运到 RAM 中执行。这在 EMIF 相关的系统里尤其重要:如果系统从 Flash 启动,而 Flash 的读速度远低于内部 RAM,执行时间敏感的 ISR(中断服务例程)或者初始化函数就可能被 Flash 访问延缓。满速运行的关键代码应该驻留 RAM。
__attribute__((ramfunc)) void CriticalInit(void) { // 这段函数将被复制到RAM中执行 EMIF_Init(); }链接时需要确保.cmd文件里有对应的代码段定义,例如.ramfunc段:
MEMORY { ... DARAM : origin = 0x10000, length = 0x10000 } SECTIONS { .ramfunc > DARAM }如果.cmd里没有分配.ramfunc,链接器会把该属性函数当作普通代码放到 Flash,属性不生效。验证是否真正驻留 RAM,可以在编译后查emif.map,看该函数的地址是否落在 DARAM 的origin范围内。
5.3 利用 .gel 快速验证时序边界
最后一个值得记录的技巧是使用 GEL 脚本来扫描 EMIF 的等待周期参数。调试时,最怕的是“这次改了不知道好坏”。用小脚本自动切换 Setup/Strobe 值,然后跑一段内存自检,比手动多次加载.out再点运行要高效。下面的 GEL 脚本示例演示了如何自动扫描 Strobe 配置:
int scanStrobe() { int s; for (s = 0; s < 8; s++) { *(int *)0x080000 = ((s & 0x3F) << 4) | 0x0001; if (TestEmif() == 0) return s; } return -1; }TestEmif()是目标板上已加载的测试函数。GEL 脚本可以调用程序里的全局函数,只要该函数在.out中导出即可。这个扫描方法能快速找出最优 Strobe 值,但要注意:扫描过程中外部管脚上的时序是变化的,某些慢速器件会在时序不满足的瞬间产生错误访问,可能干扰后续操作。因此扫描完成后,一定要把最终满意的参数写回工程源码,重新编译并通过完整测试,而不是直接依赖 GEL 的临时修改。
本文还有配套的精品资源,点击获取