
简介面向DSP与FPGA协同开发者的EMIF接口通信学习资料聚焦TMS320C6000系列DSP如何通过外部存储器接口与FPGA完成数据交互适合已具备基本嵌入式基础、希望打通DSP高速外设扩展链路的工程师与高年级学生。资源包共1个docx文件约597KB内容按EMIF接口概念、寄存器配置、片选与地址映射、初始化参数、DDR3数据装载格式等模块展开知识点集中便于在单文件中顺序查阅。文档结合TMS320C6713与EMIF16实例说明总线位宽、读写时序、CE片选空间划分及.cmd文件地址分配方式并给出wait、strobe、turnAroundCycles等初始化配置取值参考与sprugz3a.pdf手册指引对调试中易混淆的小端存储与位宽对应问题亦有记录。目前已有8329人学习下载可作为DSP与FPGA通信联调的入门索引与配置速查材料。1. 从一句指针赋值说起DSP 写出去的那个 0x01FPGA 到底什么时候能收到DSP 与 FPGA 通过 EMIF 接口通信在信号处理板卡上是再常见不过的组合DSP 跑算法和调度FPGA 做采集、预处理和高速搬运两者之间就靠一组 16 位数据线、若干地址线和几根控制线连起来。软件侧写起来简单到只有一行向某个固定地址赋一个值就行读也一样取回来即可。但真到联调现场示波器上 CE、WE 都在动FPGA 侧计数器却一动不动或者读回来的数永远差半个字这才是多数人第一次碰 EMIF 的真实体验。下面按“这是条什么总线、DSP 侧怎么配、FPGA 侧怎么接、哪里容易翻车、怎么改成自己的通道”的顺序把它完整拆一遍。2. 架构定位EMIF 不是自定义总线是把 FPGA 映射成一段外部存储器先把这个思路摆正后面所有代码才好理解。EMIFExternal Memory Interface本质上是 DSP 上一个可配置的存储器总线控制器原本是为了接 SRAM、Flash、DDR 这类器件设计的。它对外提供的信号线基本上就是各类存储器读写接口的并集地址线、双向数据线、片选、写使能、读使能、字节使能外加时钟和等待信号。正因为是“存储器接口”当对面挂的是 FPGA 时DSP 并不会觉得对面是个逻辑器件它只当那是一段内存——往某个地址写就是往那根地址总线上发一个值。这个定位带来一个很实在的好处DSP 侧的读写时序由硬件自动产生CPU 只需要执行一条 store 或 load 指令剩下的片选拉低、地址送出、写脉冲打宽、数据保持全由 EMIF 控制器按寄存器里配好的参数自动完成。软件不用自己打拍子也不占用 CPU 去翻转 GPIO。代价是灵活性被时序寄存器框死了改一次访问节奏就得回去改配置而且 FPGA 侧必须在给定的窗口内响应窗口一过数据就没了。提示把 FPGA 当存储器看是这类方案能跑通的前提。如果你的场景需要双向流式握手、不定长突发EMIF 直接裸用会很别扭得在它上面再加一层协议第 6 章会讲怎么加。2.1 为什么不用 GPIO 模拟或自定义并口新手常问既然只是传数据为什么不用几根 GPIO 自己定义个并口协议答案在吞吐和 CPU 占用上。GPIO 模拟一个 16 位并口一次传输要翻转二十几根线每根线都得软件写寄存器一次 16 位数据传完CPU 已经跑了几十条指令。EMIF 一次访问就是一个总线周期硬件并行送出全部信号CPU 只发一条指令中间还能挂 DMA 搬运CPU 完全不参与。数据量大起来之后这个差距是数量级的。自定义并口还有个别扭的地方时序全靠软件延时编译器一优化、中断一打断波形就变形。EMIF 的时序是硬件计数器产生的只要寄存器不改波形就是确定的这对调试和长期稳定性都友好。当然EMIF 也不是没脾气它的时序参数是按 ECLKOUT 周期算的频率一变实际纳秒数就变了这一点后面配置章节会细说。2.2 一组信号线各自的分工与时序窗口把常用信号按方向列清楚FPGA 侧写代码时才知道每根线该干什么。下表是这套方案里真正会用到的部分实际板子上可能还有几根复用信号以原理图为准。信号方向作用FPGA 侧怎么处理ECLKIN输入 DSP外部参考时钟源一般不接属于 DSP 自己的时钟输入ECLKOUTDSP 输出对外同步时钟基准可作为采样参考但更稳的是用本地晶振再同步CE_nDSP 输出片选一段地址空间对应一根译码后作为本模块的选中条件EA[x:0]DSP 输出地址线拼接、移位后作为内部 RAM 地址ED[15:0]双向数据线三态驱动读写方向要严格切换WE_nDSP 输出写使能低有效抓下降沿产生内部写脉冲OE_nDSP 输出读使能低有效抓下降沿产生内部读脉冲同时控制三态BE_nDSP 输出字节使能16 位访问时可忽略或用于字节写WAITFPGA 输出插入等待周期不用硬件握手时保持无效电平一次访问拆成三段窗口setup 段是片选有效到选通信号有效之间的时间strobe 段是选通信号保持有效的宽度hold 段是选通释放到片选释放之间的时间。读访问后面还要加一段 turnaround用于总线从读方向切回写方向防止两边同时驱动数据线。这三段加起来就是 FPGA 侧必须完成响应的全部时间预算。2.3 异步 NOR/ASRAM 模式与 8/16 位位宽怎么定EMIF 支持好几种工作模式接 FPGA 一般选异步 NOR/ASRAM 模式。原因很直接这个模式下地址和数据是分开的每次访问就是一个干净的读写周期FPGA 侧只要在窗口内把数据准备好或者把数据收走不需要实现 NAND 那种命令、地址、数据分多个周期送进去的流程也不需要跟着同步时钟做突发。NAND 模式适合接存储颗粒接逻辑器件只会给自己找麻烦。位宽选 8 位还是 16 位取决于 FPGA 内部的数据组织方式和板子布线。16 位的好处是一次访问搬两个字节同样数据量总线周期减半坏处是地址换算会变得不直观——字节地址和字地址的对应关系容易搞混第 3 章会专门讲这个坑。如果 FPGA 内部数据本来就是按 16 位字组织的别犹豫选 16 位省下来的总线时间很可观。还有一项是选通模式。SS_STROBE 表示选通信号跟随片选整个片选期间都有效另一种模式会缩短选通宽度。接 FPGA 建议先用前者窗口宽一点给 FPGA 内部逻辑留够同步拍数等功能稳定了再考虑收窄。2.4 片选空间与 .cmd 里的地址分配DSP 的地址空间是按片选切的每根片选对应一段基址固定的区间。以常见的 EMIF16 为例CS2 到 CS5 四段各 64MB基址分别是 0x70000000、0x74000000、0x78000000、0x7C000000。这里最关键的一句话是某根片选对应的地址段是由芯片内部固定映射决定的而你那块板子上 FPGA 到底接在哪根片选上是由硬件连线决定的。软件里写哪个地址实际打出去的是哪根片选两者必须对上否则数据会打到别的器件上或者打在空气里。链接脚本里要把这几段声明出来链接器和调试器才知道这块地址是合法的可访问内存MEMORY { LOCAL_L2_SRAM : o 0x00800000 l 0x00080000 /* 512KB 片内 L2 */ LOCAL_L1P_SRAM: o 0x00E00000 l 0x00008000 /* 32KB 片内 L1P */ LOCAL_L1D_SRAM: o 0x00F00000 l 0x00008000 /* 32KB 片内 L1D */ SHRAM : o 0x0C000000 l 0x00400000 /* 4MB 多核共享内存 */ EMIF16_CS2 : o 0x70000000 l 0x04000000 /* 64MB CS2 */ EMIF16_CS3 : o 0x74000000 l 0x04000000 /* 64MB CS3 */ EMIF16_CS4 : o 0x78000000 l 0x04000000 /* 64MB CS4本板接 FPGA */ EMIF16_CS5 : o 0x7C000000 l 0x04000000 /* 64MB CS5 */ DDR3 : o 0x80000000 l 0x80000000 /* 2GB 外部 DDR3 */ }声明完之后缓冲区就可以用段属性直接放到对应空间里不用手写绝对地址/* 把发送缓冲区放到 CS4 段链接器负责分配具体地址 */ #pragma DATA_SECTION(fpga_tx_buf, EMIF16_CS4) volatile unsigned short fpga_tx_buf[1024];这么做的好处是地址由链接器算改了基址不用满工程搜魔法数字。但要注意链接器只保证段落在那个区间内具体偏移每次编译可能不同所以如果 FPGA 侧对地址有硬性约定比如命令寄存器固定在偏移 0 处还是得用基址加固定偏移的方式访问别依赖链接结果。3. DSP 侧落地EMIF16 初始化、指针读写和 DDR3 数据搬运DSP 侧要干三件事把总线形状配好、用指针完成读写、把要发的数据从 DDR3 搬到 EMIF 地址空间。三步里最容易出问题的是第一步因为参数多、单位不直观配错一个字段可能表现为“偶尔能通、偶尔不通”非常难查。3.1 初始化结构体先把总线的“形状”定死初始化代码一般长这样字段名各家用各自的 CSL 封装但含义基本一致#include csl_emif16.h EMIF16_Config emif16_cfg; EMIF16_CEConfig *ce_cfg; void emif16_init(void) { /* 等待引脚极性低有效。FPGA 侧不接 WAIT 时这个配置只影响兜底逻辑 */ emif16_cfg.maxWait 0x80; /* 最长等待周期防止总线死等 */ emif16_cfg.wait0Polarity EMIF_LOW_WAIT; emif16_cfg.wait1Polarity EMIF_LOW_WAIT; /* ceCfg 数组下标与片选对应关系要查清楚通常 0 对应 CS2、2 对应 CS4 */ ce_cfg emif16_cfg.ceCfg[2]; ce_cfg-busWidth EMIF_BUS_16BIT; /* 数据位宽 16 位 */ ce_cfg-opMode NOR_ASRAM_MODE; /* 接 FPGA 用异步 NOR/ASRAM */ ce_cfg-strobeMode SS_STROBE; /* 选通跟随片选窗口宽一些 */ ce_cfg-waitMode EMIF_WAIT_NONE; /* 不用硬件 WAIT 握手 */ /* 以下六个参数单位都是 ECLKOUT 周期不是纳秒 */ ce_cfg-wrSetup 15; /* 片选有效 - WE 有效 */ ce_cfg-wrStrobe 63; /* WE 有效宽度写数据在这个窗口内稳定 */ ce_cfg-wrHold 7; /* WE 释放 - 片选释放 */ ce_cfg-rdSetup 15; ce_cfg-rdStrobe 63; /* 读窗口必须给 FPGA 留够准备数据的时间 */ ce_cfg-rdHold 7; ce_cfg-turnAroundCycles 3; /* 读转写换向周期防止总线冲突 */ ce_cfg-nor_pg_Cfg NULL;/* NOR 页模式用不上 */ EMIF16_init(emif16_cfg); }逐项说一下怎么估。先确认 ECLKOUT 的频率假设是 100MHz一个周期 10ns那么 wrStrobe 配 63 就是 630ns 的写脉冲宽度非常宽裕功能调试阶段这么配没问题。等稳定之后想提速把三个字段按比例往下压压到示波器上数据建立时间还剩两三成余量就够别贴着边配。maxWait 这个字段容易被忽略。它的作用是当 WAIT 引脚一直被拉有效时控制器最多等多少个周期就强制结束本次访问避免 CPU 卡在总线上一动不动。就算你不用硬件握手也建议给个非零值相当于给总线留了个超时。waitMode配成 EMIF_WAIT_NONE 的前提是 FPGA 侧真的没接 WAIT或者接了但保持无效电平。如果板上 WAIT 是悬空的输入电平不确定偶尔被拉低一次DSP 就会插一堆等待周期表现为“绝大多数时候正常偶尔一次访问特别慢”这种玄学问题查起来很折磨。稳妥做法是硬件上加个上拉软件上把等待模式关掉。3.2 一次写操作的完整链路从 C 语句到 CE/WE 波形配置好之后读写就是一行指针操作/* CS4 基址 0x78000000偏移 0x2E 处写一个 16 位字 */ *(volatile unsigned short *)(0x78000000 0x2E) 0x0001; /* 从同一位置读回来 */ unsigned short val *(volatile unsigned short *)(0x78000000 0x2E);这三个要素缺一不可。0x78000000是片选基址写别的基址就会打到别的片选上去0x2E是偏移FPGA 侧靠它区分寄存器volatile是必须加的否则编译器看你这块内存没人读过直接把写操作优化掉你会对着示波器怀疑人生unsigned short对应 16 位位宽用unsigned int编译器会拆成两次 16 位访问或者报对齐问题。这条语句编译出来大概是一条存储指令CPU 把它丢给 EMIF 控制器就继续往下走控制器按寄存器配置自动完成后面的事把地址送上地址线、拉低对应片选、拉低写使能、把数据放到数据线上、保持 strobe 个周期、依次释放。整个过程 CPU 不参与也不需要软件延时。注意连续两次写同一个地址如果中间没有插入足够的换向周期第二次的数据可能覆盖不上。读写交替频繁的场景turnAroundCycles 别配成 0。3.3 DDR3 初始化与 load memory 的数据格式实际项目里要发给 FPGA 的数据一般先放在 DDR3 里因为容量大、算法侧直接往里写方便。用之前得先初始化 DDR3 控制器把时钟、刷新周期、位宽、时序参数配好这部分各个平台的初始化代码差别很大一般芯片厂商的例程包里都有现成的照抄再改时钟参数即可。初始化完之后调试阶段常用 load memory 的方式直接把数据文件烧进 DDR3。这类文件首行是格式说明加一段元信息后面每行是地址加数据形如1651 9 80000000 0 A200 4 80000000 00010203 04050607 08090A0B首行那几个十六进制数字含义比较隐晦不同工具版本生成的还不完全一样。稳妥做法是先用调试器把一小段已知内容 dump 成文件看看它自己生成的头长什么样再照着改别手搓。第二行开始是地址加数据地址是字节地址数据宽度取决于文件里声明的格式32 位格式就一次四个字。3.4 字节序与小端0x0203 到底落在哪个地址这里有个几乎每个人都会愣一下的地方。同样是 32 位 hex 数据00010203 04050607 08090A0B如果按 16 位格式看会变成0203 0001 0607 0405——因为 DSP 是小端32 位字的最低半字放在最低地址所以 0x00010203 拆开是低半字 0x0203 在前、高半字 0x0001 在后。真正让人困惑的是地址。假设起始地址是 0x80000000位宽 16 位你会看到 0x80000000 是 0x02030x80000001 是 0x00010x80000003 是 0x06070x80000004 是 0x0405。按字节地址去想0x80000004 应该是 0x07 那一位怎么变成 0x0405 了这是把“字地址”和“字节地址”混用了。EMIF16 在 16 位位宽下地址线每加一对应的是一个 16 位字而不是一个字节。软件里写*(unsigned short *)(base n)指针算术里的 n 是以 2 字节为步长走的而 load memory 文件里的地址是按字节算的。两套计数方式一混就出现了“看起来不对”的现象。我的验证习惯是从一个基址开始连续写三个已知码型比如 0x1111、0x2222、0x3333然后让 FPGA 侧把收到的数据和地址一起打出来对照 DSP 侧写的顺序看。如果顺序反了说明位宽或者字节序理解有偏差如果地址跳着走说明地址重排没做对第 4 章会讲。这么点一遍比对着手册推半小时强。4. FPGA 侧落地用 Verilog 把 EMIF 时序翻译成内部握手FPGA 侧的角色很明确被动从设备。它要做的就是盯着片选和读写选通在窗口内把数据收下来或者送出去。听上去简单但跨时钟域、地址重排、三态切换三件事里任何一件没处理好表现出来都是“数据偶尔对偶尔错”。4.1 三级同步与写使能脉冲的产生DSP 送过来的控制信号相对 FPGA 内部时钟是异步的直接拿来当条件用会踩亚稳态这是血泪经验。先打两拍/* 控制信号异步先进 FPGA 时钟域打两拍再用 */ always (posedge clk_fpga) begin dsp_emif_wen_t1 dsp_emif_wen_n; /* WE_n 第一级同步 */ dsp_emif_wen_t dsp_emif_wen_t1; /* WE_n 第二级同步 */ dsp_emif_oen_t1 dsp_emif_oen_n; /* OE_n 第一级同步 */ dsp_emif_oen_t dsp_emif_oen_t1; /* OE_n 第二级同步 */ end /* 写使能脉冲WE 由高变低且本板 CS4 选中 emif_ce_n 是把两路片选拼成的 2 位向量01 表示 CS4具体编码看你的连线 */ assign wstb_o (dsp_emif_wen_t1 1b1 dsp_emif_wen_t 1b0 emif_ce_n 2b01) ? 1b1 : 1b0;为什么抓下降沿而不是用电平因为在写周期里数据线在 WE 变低之前就已经稳定了沿到来的那一刻数据一定是有效的直接锁存最省事。而且沿能产生单周期脉冲后面拿去写 BRAM 或者塞 FIFO 都很方便不用再处理“电平持续了多少拍”的问题。clk_fpga的频率要够快至少在 WE 有效的整个 strobe 窗口里能采到这次跳变。前面 DSP 侧 wrStrobe 配了 63 个 ECLKOUT 周期就算 FPGA 跑几十兆也绰绰有余所以调试阶段尽量把 strobe 配宽别一上来就压时序。4.2 读通道OE 沿只做触发数据要在窗口内稳定读通道的写法看着和写通道对称/* 读使能脉冲OE 由高变低且 CS4 选中 */ assign rstb_o (dsp_emif_oen_t1 1b1 dsp_emif_oen_t 1b0 emif_ce_n 2b01) ? 1b1 : 1b0;但这里有个容易翻车的认知差rstb_o只能用来触发内部动作比如锁存读地址、从 FIFO 里弹一个数据出来它不能用来“等沿到了再把数据放上总线”。DSP 的读窗口是从 OE 有效就开始的OE 一拉低控制器就开始在 strobe 段采样数据线你要是等沿采到、再走两级逻辑、再把数据送出去采样点早就过了。正确做法是把“驱动数据”和“更新读指针”分开地址译码一旦稳定且片选有效就把当前数据送到三态门的输入端让数据线立刻有值而rstb_o只负责在读周期结束时把内部读地址加一或者从 FIFO 里预取下一个字。这样即使读指针更新慢一拍本次读到的数据也是对的。如果 FPGA 内部数据来自一个慢速模块来不及在窗口内准备好那就得用 WAIT 引脚做硬件握手让 DSP 插等待周期。这时候 DSP 侧的waitMode就不能配成关闭得改成对应的等待模式两边配置必须成对改只改一边等于没改。4.3 地址重排那两位被挪走的地址线地址这块是整篇文章里最容易出问题、也最依赖原理图的地方。DSP 给出来的是字节地址而 FPGA 内部通常按 16 位字存两者差一位所以直觉上应该把地址右移一位再用。但实际板子上为了布线方便地址线经常不是顺序接的。我手上这版的做法是把 DSP 地址的高两位挪到 FPGA 地址的低两位去代码大概长这样/* EMIF16 送出来的是字节地址FPGA 内部按 16 位字存理论上右移一位即可 但这块板子把 EA[23:22] 布到了 FPGA 地址线的低位直接右移会串段 所以按实际连线重新拼一次地址才能连续。 这一行必须对着原理图核别照抄。 */ wire [21:0] emif_word_addr { dsp_emif_addr[23:22], dsp_emif_addr[21:2] };按手册的说法这种调整是 8 位位宽才需要的16 位位宽下地址线本身就该对齐。但实际情况是 16 位位宽也得这么调一遍否则 DSP 侧连续写两个相邻地址FPGA 侧收到的地址会跳着走。我的推测是布线时地址线并没有按顺序接到 FPGA 的引脚上具体哪一位接哪一位只有原理图说得清。所以验证方法比代码本身更重要DSP 侧从基址开始连写 0、1、2、3……FPGA 侧把每次收到的地址打到逻辑分析仪或者内部计数器上看地址是不是连续递增。连续就对了跳着就说明拼接顺序有问题。这一步建议在新板子第一次上电时就做掉别等业务逻辑写完再回头查。4.4 双向数据线的三态与释放时机数据线是双向的FPGA 侧必须用三态门而且切换时机要卡准/* 只在读周期驱动数据线其余时间必须高阻否则会和 DSP 的写驱动打架 */ assign data_oe (~emif_ce_cs4_n) (~dsp_emif_oen_n); assign dsp_emif_data data_oe ? data_out : 16hzzzz;data_oe用组合本文还有配套的精品资源点击获取