ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

DSP与FPGA的EMIF16联调:时序配置、地址映射与读写验证

DSP与FPGA的EMIF16联调:时序配置、地址映射与读写验证

简介:《DSP与FPGA通过EMIF接口通信》是一份面向嵌入式硬件与信号处理开发者的技术文档,聚焦DSP如何借助外部存储器接口与FPGA完成数据交互,适合需要理解总线配置、片选映射及读写时序的初中级工程人员参考。压缩包内共1个docx文件,约597KB,篇幅集中,便于按主题查阅。内容围绕EMIF接口概念、寄存器配置、DSP与FPGA通信流程、接口初始化、地址分配和片选信号展开,并结合TMS320C6713平台说明位宽、读写保持时间及NOR/ASRAM等工作模式的选择思路,对DDR3初始化、数据搬运和cmd地址划分也有涉及,可帮助读者建立从接口配置到实际传输的完整认识,并作为调试时的排错参考。目前已有8329人学习。

1. 从「能写不能读」的板子说起:DSP 与 FPGA 的 EMIF 通道

板子第一次上电,DSP 写 FPGA 的寄存器一切正常,读回来却是清一色的 0xFFFF;示波器上片选有波形、写使能也有,数据线却像没接上。这种情形在 C6000 加 FPGA 的板子上出现得频繁。

DSP 与 FPGA 通过 EMIF 接口通信,本质是把 FPGA 当成一片挂在总线上的异步存储器:地址、数据、片选、读写选通全由 EMIF 控制器自动产生,软件只要往一个地址赋值。省事,可位宽差一位、片选差一个、时序差几个 ECLKOUT 周期,表现都是同一句「能写不能读」。

这套环境适合手上有 TMS320C6713 或同系列 C6000 板子、要让 FPGA 当协处理器或高速数据通道的人。下面按我装过一遍的顺序走:例程落地、EMIF16 配置、地址映射、翻车记录,最后是验证。

2. 环境搭建:CCS 工程与 K1_STK_v1.1 例程落地

这套联调环境不用虚拟机、不做交叉编译,全在 CCS 里完成。真正要装的东西五样,缺一样就是编译过不去或者下载后无法正常运行。最冤的一次是把例程解压到带空格的目录,编译脚本里的路径被截断,报了一屏找不到头文件的错,查了两个小时才发现是路径问题——这种错没有任何提示价值,只能靠事前规避。

2.1 五件套清单:少装一个都编不过

先把组件和版本基准列清楚,别一边装一边猜。

组件版本 / 来源基准作用缺了会怎样
CCS(Code Composer Studio)必须带 C6000 器件支持编译、下载、看 memory 窗口新建工程时选不到芯片型号
器件支持包 / 芯片支持库与 CCS 同批次安装提供 emif16_cfg、EMIF16_setConfig报 undefined symbol
仿真器驱动与手上仿真器型号对应连目标板connect 时扫不到 target
K1_STK_v1.1 例程包本文这套例程就指它emif 目录里的时钟与 EMIF 初始化参数只能凭空猜
sprugz3a.pdfEMIF16 寄存器手册校验时序参数、位宽、模式改了参数不知道对不对

例程是 K1_STK_v1.1 那一版,工程文件年代偏早,新版 CCS 导入时会提示编译器版本与工程不一致。按提示切到当前工具链重新编译即可,不要看到红叉就把工程删了重建——重建之后 include 路径全丢,更麻烦。器件型号在工程属性里务必核一遍,TMS320C6713 和其它 C6000 型号的 EMIF 寄存器排布并不完全一样,型号选错能编译过、能下载,但寄存器写进去像石沉大海。

还有一条几乎没人写在文档里:workspace 和例程包一定要放在纯英文、无空格、层级浅的路径下。我一般固定放/opt/work/,Windows 下就是D:\work\,不要用「桌面」「新建文件夹(2)」这种。

2.2 把 workspace 指到例程根目录,先跑通编译

例程给出的做法很直接:把 CCS 的 workspace 设置成 K1_STK_v1.1 所在路径,再打开 emif 文件夹里的工程。原因在于那批例程的头文件引用大多是相对 workspace 的路径,换个 workspace 它就找不到头文件,报错信息还指向一个不存在的绝对路径。

# 解压后先确认目录结构,别急着开 CCS cd /opt/work/K1_STK_v1.1 ls # emif/ 时钟初始化 + EMIF 初始化,本次要用的就是它 # gel/ 调试用的 GEL 脚本 # 其它外设目录,本次不用管 # 确认 emif 目录下两份初始化源文件都在 ls emif # 全路径不能含中文和空格,编译脚本对空格极其敏感 realpath .

上面这段不是形式主义。第一步ls是确认解压完整,有些压缩包在 Windows 上解压会丢一层目录;第二步确认 emif 目录里时钟和 EMIF 两份初始化都在,缺一份后面就得自己补;第三步打印绝对路径,肉眼扫一遍有没有空格和中文。三步加起来不到十秒,能省掉后面半小时的迷惑。

CCS 里的操作对应三步:File > Switch Workspace指到/opt/work/K1_STK_v1.1;Project > Import CCS Projects选中 emif 目录;导入后右键工程看 Properties,确认器件型号和 include 路径。第一次编译只验证工具链通不通,不接板子,能出 .out 文件就算这一步过了。

2.3 时钟初始化必须排在 EMIF 初始化之前

例程的 emif 文件夹里同时放了时钟初始化和 EMIF 初始化,顺序绝对不能反。EMIF 的所有时序参数单位都是 ECLKOUT 周期,而 ECLKOUT 是从外部 ECLKIN 经内部 PLL 倍频出来的。PLL 没锁定、ECLKOUT 没起振的时候写 EMIF 寄存器,值能写进去,但换算出来的实际时间是错的,而且错得很隐蔽。

/* main 里的调用顺序,按这个次序来 */ int main(void) { board_init(); /* 板级:DDR3、PLL、引脚复用,最先做 */ clk_init(); /* 时钟:等 PLL 锁定,确认 ECLKOUT 起振 */ emif_init(); /* EMIF16:所有时序参数以 ECLKOUT 周期为单位 */ ddr3_init(); /* 外部 DDR3 初始化,数据缓存在这里面 */ /* 之后才是业务代码 */ return 0; }

clk_init()和emif_init()都不是我编的名字,是例程里就有的两份初始化,直接把调用顺序抄过来即可。关键是clk_init()之后要有一段等待 PLL 锁定的延时,例程里通常写成一个空转循环或者查锁定位,别嫌它丑。

这里有个我强烈建议加的动作:ECLKOUT 起振后,用示波器在 ECLKOUT 引脚上量一次频率,记下来。第 3.3 节算时序要用这个数,很多人图省事直接按标称值算,量出来差个几兆,后面时序余量全是虚的。这块属于典型的玄学——数字都对,就是不稳。

2.4 建 target configuration 并连上板子

下载之前还得有一个 target configuration 文件,选仿真器型号和目标芯片,保存后Test Connection。连接失败的常见原因就三个:仿真器驱动没装对、板子没上电、复位方式选错。这三种我都遇到过,其中复位方式选错最折磨人——能连上,一下载就断开。

连上之后先别急着跑业务代码,加载一个空工程单步走一遍,看 PC 能不能正常跑、memory 窗口能不能读到片内 RAM 的已知值。这一步过了,说明下载链路是通的,后面出问题就只可能是 EMIF 配置或者 FPGA 侧的事,排查范围一下缩小一半。

3. EMIF16 初始化:位宽、模式和那串时序数字

EMIF 之所以好用,是因为它把 SRAM、Flash、DDR 那一堆读写接口信号合并成一套,软件配寄存器就能让它呈现成对应存储器的接口形式。异步模式下真正要动的就几项:CE 对应的数据位宽、工作模式、选通方式、wait 策略,以及 setup/strobe/hold 这几段周期数。地址、数据、片选、字节使能由硬件自动产生,软件不用管。

3.1 emif16_cfg 逐字段拆:哪些必须改,哪些别动

配置代码本身不长,难的是知道每个字段为什么要取这个值。

/* EMIF16 初始化,字段与例程 emif 目录下的初始化函数一致 */ EMIF16_Config emif16_cfg; EMIF16_getConfig(&emif16_cfg); /* 先取默认值,别整块清零 */ emif16_cfg.maxWait = 0x80; /* 硬件 wait 的最大等待计数 */ emif16_cfg.wait0Polarity = EMIF_LOW_WAIT;/* WAIT0 低有效 */ emif16_cfg.wait1Polarity = EMIF_LOW_WAIT;/* WAIT1 低有效 */ emif16_cfg.ceCfg[0]->busWidth = EMIF_BUS_16BIT; /* 数据线 16 位 */ emif16_cfg.ceCfg[0]->opMode = NOR_ASRAM_MODE; /* FPGA 按异步 SRAM 挂 */ emif16_cfg.ceCfg[0]->strobeMode = SS_STROBE; /* 片选选通方式 */ emif16_cfg.ceCfg[0]->waitMode = EMIF_WAIT_NONE; /* 先不用硬件 wait */ /* 时序:单位是 ECLKOUT 周期,寄存器实际周期一般是「值 + 1」 */ emif16_cfg.ceCfg[0]->wrSetup = 15; emif16_cfg.ceCfg[0]->wrStrobe = 63; emif16_cfg.ceCfg[0]->wrHold = 7; emif16_cfg.ceCfg[0]->rdSetup = 15; emif16_cfg.ceCfg[0]->rdStrobe = 63; emif16_cfg.ceCfg[0]->rdHold = 7; emif16_cfg.ceCfg[0]->turnAroundCycles = 3; /* 读写方向切换的空转周期 */ emif16_cfg.ceCfg[0]->nor_pg_Cfg = NULL; /* NOR 页模式不用,置空 */ EMIF16_setConfig(&emif16_cfg); /* 真正写寄存器 */

逐项拆一下取舍:

字段取值含义什么时候要改
busWidthEMIF_BUS_16BIT每个地址承载 16 位FPGA 侧只接 8 根数据线时改成 8 位
opModeNOR_ASRAM_MODE异步 SRAM / NOR 时序接 NAND Flash 才换 NAND 模式
strobeModeSS_STROBE用片选作为选通换成其它选通方式会影响时序基准
waitModeEMIF_WAIT_NONE不使用硬件等待FPGA 还没做 wait 逻辑时必须保持 NONE
maxWait0x80wait 最大计数只有开了 wait 模式才有意义
turnAroundCycles3读写切换空转总线负载重、FPGA 侧切换慢时加大
nor_pg_CfgNULLNOR 页模式配置不用页模式就别碰

ceCfg[0]对应你硬件上实际用的那个片选,例程里默认配的是第一个使能的 CE。硬件连线定死了 DSP 的哪根 CE 接到 FPGA,这条信息只能从原理图上拿,猜不出来。配置代码里的数组下标必须和原理图对上,否则你配的是 CS2、实际写的是 CS4,波形上啥都看不到。

3.2 位宽 8 位还是 16 位:决定后面地址怎么挪

位宽是这套配置里影响面最大的一个参数,它同时决定三件事:地址线接几根、字节使能怎么用、FPGA 侧地址要不要移位。

选 16 位的情况:FPGA 侧数据通路本来就是 16 位或更宽,一次传输两个字节,带宽利用率高。代价是地址线少接一根,而且 DSP 发出的地址在 16 位模式下每个地址代表 2 个字节,FPGA 内部如果按字节索引寄存器,就必须做移位,这一条后面 4.4 节展开。

选 8 位的情况:FPGA 侧逻辑资源紧张、只需要传命令字或者小数据量,8 位接线最简单,一个地址一个字节,地址线一一对应不用换算。缺点是传大数据块的时候带宽直接腰斩。

模式上,FPGA 只可能是NOR_ASRAM_MODE。NAND 模式有自己的一套命令/地址/数据复用时序,FPGA 不是 NAND,硬套 NAND 模式会出现命令周期和数据周期错位,读回来的值毫无规律。这条别试,试了就是浪费一晚上。

3.3 时序参数反算:setup / strobe / hold 落在多少纳秒

寄存器里写的是周期数,板子上跑的是纳秒,中间差一个 ECLKOUT。写完之后一定要反算一遍,确认 FPGA 侧能接住。

# 按 ECLKOUT 反算异步接口的实际时间,确认 FPGA 侧能对上 F_ECLK = 100e6 # ECLKOUT 频率,填第 2.3 节实测的那个值,别照抄 T = 1.0 / F_ECLK def cyc(n): """寄存器值 -> 实际周期数,异步接口一般是 n + 1,以手册为准""" return n + 1 wr_setup = cyc(15) # 写建立 wr_strobe = cyc(63) # 写选通 wr_hold = cyc(7) # 写保持 turn = 3 # 读写切换 print(f"1 个 ECLKOUT = {T*1e9:.2f} ns") print(f"写建立 {wr_setup*T*1e9:.1f} ns") print(f"写选通 {wr_strobe*T*1e9:.1f} ns <- 这决定 FPGA 能拿到多宽的写脉冲") print(f"写保持 {wr_hold*T*1e9:.1f} ns") print(f"单次写事务约 {(wr_setup+wr_strobe+wr_hold)*T*1e9:.1f} ns") print(f"读写切换额外 {turn*T*1e9:.1f} ns")

这段脚本的输出里,最该盯的是「写选通」那一行。FPGA 侧是靠检测写使能的下降沿来采样数据的,写选通的实际宽度决定了 FPGA 内部的组合逻辑有没有时间把数据打进去。宽度不够的典型症状是:DSP 端看波形一切正常,FPGA 内部寄存器偶尔抓到上一笔数据,出错率跟温度、跟相邻代码都有关系,非常难查。

cyc()里那个+1是这类异步接口的常见规则,具体以 sprugz3a.pdf 里对应字段的描述为准,不同字段的规则未必一致。我一般会把手册里那几个寄存器的位定义截图贴在工程目录里,改参数前先看一眼,省得凭记忆写错。

时序配完的策略是:先保守,后收紧。第一次跑通时把 strobe 拉到偏大(比如上面这个 63),确认数据全对之后,再一格一格往下压,压到开始出错就回退两格。这样既知道能跑多快,也知道余量边界在哪。反过来先配紧再放松,你永远不知道当前这组参数离崩溃有多远。

4. 地址映射与数据对齐:.cmd、片选指针和 load memory

配置对了、时序也够了,还是可能读不到数——问题往往出在地址上。DSP 这一侧看地址是「空间」,FPGA 那一侧看地址是「偏移」,中间还夹着一个位宽换算。这三层对不齐,波形上什么毛病都看不出来。

4.1 .cmd 里的四个 64MB 窗口与片选

链接命令文件里给 EMIF16 划了四个窗口,每个 64MB,对应四根片选。这段内容不要改动地址,改了链接能过、跑起来却写不到 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 EMIF16 CS2 */ EMIF16_CS3 : o = 0x74000000 l = 0x04000000 /* 64MB EMIF16 CS3 */ EMIF16_CS4 : o = 0x78000000 l = 0x04000000 /* 64MB EMIF16 CS4 */ EMIF16_CS5 : o = 0x7C000000 l = 0x04000000 /* 64MB EMIF16 CS5 */ DDR3 : o = 0x80000000 l = 0x80000000 /* 2GB 外部 DDR3 */ }
段名起始地址长度对应片选
EMIF16_CS20x700000000x04000000(64MB)CS2
EMIF16_CS30x740000000x04000000(64MB)CS3
EMIF16_CS40x780000000x04000000(64MB)CS4
EMIF16_CS50x7C0000000x04000000(64MB)CS5

这里要分清两件事:.cmd只是把地址窗口留出来,让链接器知道有这么一块空间;真正决定拉哪根片选的,是硬件连线加上你访问的地址落在哪个窗口。地址落在 0x78000000 到 0x7BFFFFFF 之间,硬件就拉 CS4,这是地址译码器干的活,软件没法通过改.cmd改变它。所以原理图上 FPGA 接的是哪根 CE,代码里就只能用那个窗口,写错窗口 = 对着空气读写。

4.2 指针赋值就是一次总线事务

异步接口用起来最舒服的地方,一次写操作就一行。

/* CS4 窗口基址 0x78000000,FPGA 内部寄存器偏移 0x2E,16 位宽访问 */ #define FPGA_CS4_BASE 0x78000000u #define FPGA_REG_CTRL 0x2Eu *(volatile unsigned short *)(FPGA_CS4_BASE + FPGA_REG_CTRL) = 0x0001;

这行的执行过程值得拆开看:编译器生成一条 16 位 store 指令;EMIF 控制器发现地址落在 CS4 窗口,拉低 CS4 片选;按 3.1 配好的时序依次输出地址、拉低写使能、送出 0x0001、保持、释放。整个过程软件一次循环都没参与,全部硬件完成。

volatile不能省。省了之后编译器看到你连着写同一个地址,可能把前一次优化掉;或者读回一个变量时直接从寄存器取值,根本不去总线上读。这类 bug 的表现是「单步调试正常、全速运行不对」,最消耗排查时间。

unsigned short对应 16 位访问,位宽改了它也得跟着改。8 位位宽用unsigned char,32 位位宽用unsigned int。用错了不会报编译错,只会产生两次总线事务或者只写一半,症状是数据被截断。

偏移 0x2E 是 FPGA 内部的寄存器地址,只有 FPGA 那一侧的译码逻辑知道它代表什么,务必和 FPGA 工程师对一遍表。同一个偏移在 DSP 代码里写一个值、在 FPGA 逻辑里译成另一个寄存器,这种对不上的情况很常见。

4.3 load memory 的文本格式与大小端排列

调数据通路的时候,经常要从文件往 DDR3 里灌一批测试数据。load memory 用的文本文件首行是格式声明,后面跟首地址、长度和数据,形如:

1651 9 80000000 0 A200 4 00010203 04050607 08090A0B

首行里的80000000是首地址(DDR3 窗口起点),后面的字段是格式标识和数据长度,具体位含义按 CCS 帮助里的说明对。第二行开始是数据本体,按 32 位一组写。

麻烦在显示上。DSP 的 DDR3 按 32 位存储时内容是00010203 04050607 08090A0B,把 CCS 的 memory 窗口切成 16 位显示,看到的是0203 0001 0607 0405。这不是数据被改了,是小端排列:一个 32 位值0x00010203在内存里低字节在前,低地址放0x0203,下一个地址放0x0001。

32 位视角的地址32 位数据16 位视角的地址16 位数据
0x800000000x000102030x800000000x0203
0x800000040x040506070x800000020x0001
0x800000080x08090A0B0x800000040x0607

表里第三行对应的就是那个让人困惑的现象:明明是 0x04050607 里的0x0405,却出现在 0x80000004 这个地址上。本质原因是两种显示宽度的地址步进不一样——16 位显示时每个地址走 2 字节,32 位显示时走 4 字节,两个视角混着看必然对不上。判断数据对不对,就统一在一个视角里看:要么全程 32 位 hex,要么全程 16 位,不要一半一半。

我在验证数据通路时固定用一组图样:0x0001、0x0203、0x0405、0xAA55、0x55AA。这几个值高低字节明显、位模式互补,大小端搞反、字节错位、位宽配错,一眼就能看出来。用递增序列(1、2、3、4)反而不好判断,因为错位之后看着还是递增的。

4.4 FPGA 侧:读写使能边沿提取与地址移位

DSP 这一侧配完,事情只做了一半。FPGA 那侧必须能把 EMIF 的选通信号翻译成一个干净的单周期使能,否则数据什么时候有效全靠猜。

// 用 EMIF 的时钟域采样读写选通,打一拍用于边沿判断 always @(posedge dsp_emif_clk) begin dsp_emif_wen_t1 <= dsp_emif_wen; // 写使能打一拍 dsp_emif_oen_t1 <= dsp_emif_oen; // 读使能打一拍 end // 写有效:写使能出现下降沿,且片选命中本 CE assign wstb_o = (dsp_emif_wen_t1 == 1'b1 && dsp_emif_wen == 1'b0 && emif_ce_n == 2'b01) ? 1'b1 : 1'b0; // 读有效:读使能出现下降沿,且片选命中本 CE assign rstb_o = (dsp_emif_oen_t1 == 1'b1 && dsp_emif_oen == 1'b0 && emif_ce_n == 2'b01) ? 1'b1 : 1'b0;

三个细节必须同时满足,缺一个就抓不到正确时序。第一,边沿判断要用同一时钟域下的前后两拍,dsp_emif_clk就用 ECLKOUT,别拿 FPGA 本地晶振去采样,跨时钟域采样出来的是毛刺不是边沿。第二,片选条件不能省。总线上可能挂着多个设备,只看写使能会把别人的事务也当成

本文还有配套的精品资源,点击获取

返回列表