我最早在FPGA上做数据采集存储的时候,最头疼的就是存储这一环。SDRAM带宽高但掉电就丢,SD卡便宜但插拔接触不可靠,协议还得自己做文件系统,真正到了要量产或者长时间记录数据的场景,往往只能捏着鼻子选一颗EMMC。这几年陆续做了几个FPGA驱动EMMC的板子,从最开始的几百K时钟一路调到HS200,中间踩了不少坑,也积累了一套比较顺手的Verilog模块写法。这篇文章就把我从选型、协议、Verilog实现到硬件设计、实测排错的全过程拆开讲,给正在做FPGA存储方案的朋友一个能直接落地的参考。
1. 为什么选择EMMC:低成本大容量存储的选型逻辑
1.1 三类存储方案的横向对比
FPGA项目里需要大容量非易失存储时,大部分人第一反应是SD卡,其次是NAND Flash,最后才会想到EMMC。但这三样东西的实际工程成本差得很远。
SD卡的问题在于接口面向热插拔场景,连接器占面积、震动后会接触不良,而且很多工业级SD卡单价并不便宜。更麻烦的是SD卡协议里保留了SPI模式这种历史包袱,导致网上教程鱼龙混杂,照着抄经常被卡在奇怪的时序细节上。NAND Flash则是另一个极端,裸片没有坏块管理、没有ECC、没有擦写均衡,全要FPGA逻辑自己扛,写一个稳定可靠的NAND控制器工程量不比写业务逻辑小。
EMMC本质上就是一颗"内置主控的NAND Flash",对外暴露的接口是标准MMC命令集。你不需要知道里面有多少个Die、坏块在哪里、要用什么ECC算法,FPGA只需要发命令、搬数据,剩下的脏活累活芯片自己消化。这是它在工程效率上最核心的优势。
1.2 EMMC帮你省掉的那部分脏活
我见过不少团队在裸NAND上栽跟头,最后迫不得已把项目改成EMMC。其实仔细算一笔账就明白:一批板子在产线上偶然出现坏块,如果用的是裸NAND,你得有坏块管理表、有备用块替换策略、有异常掉电保护,这一套逻辑做扎实了少说一两个月工作量。换成EMMC,主控内部全部搞定,FPGA这边只需要处理"写了失败重试几次"这种最粗粒度的容错。
另一个容易被忽略的点是容量与封装。EMMC常见的153ball BGA封装只有11.5x13mm左右,8GB起步、128GB不稀奇,一颗芯片直接焊死在板子上,不需要考虑插拔机械结构。对于要做外壳、要做抗震、要过温湿度循环的设备来说,焊接式存储天然比卡座可靠得多。
当然EMMC不是没有代价。它的引脚间距0.5mm,PCB加工门槛比SD卡座高,而且协议比SPI Flash复杂不少,初始化序列长、命令响应也有多种格式。但这些成本是一次性的——把Verilog控制器写好后,换容量换批次基本不用动逻辑,这比每次换NAND颗粒都重新调一次坏块策略要省心太多。
1.3 什么场景下该用EMMC
从我的项目经验看,EMMC最适合这几类FPGA场景:图像/视频采集缓存、长时间数据记录、需要掉电保存配置或日志的产品。这些场景的共同特点是写多读少(或者读写比例接近)、单次数据量从几KB到几十MB不等、对成本敏感但又不希望为存储可靠性投入过多开发时间。
如果你的项目只是存个几百KB的配置参数,用SPI NOR就够了;如果追求极致带宽且接受掉电丢数据,DDR颗粒更合适;但如果你要的是"掉电不丢、容量上G、开发周期可控",EMMC就是性价比最高的那条路。
2. 从引脚到协议:EMMC到底怎么被控制的
2.1 153ball封装:真正需要用到的引脚其实不多
EMMC虽然封了153个球,但绝大多数是电源、地和空脚,真正和FPGA打交道的信号很少。以常见的EMMC 5.1芯片为例,控制面只需要这些信号:
| 信号 | 方向 | 说明 |
|---|---|---|
| CLK | FPGA → EMMC | 时钟,初始阶段不超过400kHz,运行后可提升到50/100/200MHz |
| CMD | 双向 | 命令线与响应线复用 |
| DAT0~DAT7 | 双向 | 数据线,支持1/4/8位模式 |
| RST_n | FPGA → EMMC | 复位,低有效,上电后至少保持1us低电平 |
| VCC | 电源 | 主供电3.3V |
| VCCQ | 电源 | IO供电1.8V或3.3V |
市面上所谓"153ball EMMC引脚定义",资料上画得密密麻麻,实际上你只要在原理图里把这几根信号和电源网络连对,剩下的球按封装图接到地或悬空就行。第一次画板子的人最容易在这里产生错觉,觉得153个脚很难搞,其实大部分引脚定义里写了NC(Not Connected)或者GND,真正需要动的不到20个。
EMMC很多型号的VCCQ可以选1.8V或3.3V,这决定了FPGA端IO的电平标准。如果FPGA的Bank电压是3.3V,VCCQ也接3.3V最省事;如果为了降低功耗接1.8V,那么CMD、DAT、CLK这些线就必须走1.8V电平,FPGA的Bank要单独供电并配置成LVCMOS18。这个选择要在画板子前定死,否则后面改电平会连带改一堆东西。
2.2 命令格式与基本时序
EMMC的一切操作都是通过命令发起的。命令由主机(FPGA)发出,总长度48位,格式如下:
// 命令格式(从高位到低位) // [47] 起始位 0 // [46] 传输位 1(表示主机->设备) // [45:40] 命令索引 6bit // [39:8] 参数 32bit // [7:1] CRC7 校验 // [0] 结束位 1这里最容易出错的是CRC7。我最初调试时偷懒没算CRC,结果每次命令发出去都石沉大海。CRC7的生成多项式是x^7 + x^3 + 1,初值0x00,覆盖范围是命令索引加参数共38个bit,从最高位开始逐个bit移入。很多人拿SD卡的CRC算法代码直接套,发现算出来对不上,多半是覆盖范围或者初始值没对齐。
响应则按命令不同分为R1、R2、R3、R6、R1b等几种。调试时最常打交道的是R1(32位状态+CRC),以及初始化阶段的R3(OCR寄存器)和R2(CID/CSD寄存器)。R1的CRC是卡对状态字段自己算好的,主机在接收时校验CRC能顺便发现时钟线不稳定、采样沿不对这类底层问题,所以接收端一定要做CRC检查,不要怕麻烦。
2.3 初始化流程:一轮命令编排
EMMC上电后不会立刻进到可以读写数据的状态,它要先走一遍初始化,把总线宽度、时钟频率、工作模式都谈好。标准流程是这样的:
- 上电后CLK以400kHz以下频率工作,等待电源稳定,RST_n释放。
- 发送CMD0(参数0xF0F0F0F0),让设备进入Idle状态。
- 循环发送CMD1(SEND_OP_COND),读取OCR寄存器,直到busy位拉高,表示设备内部上电完成。
- 发送CMD2(ALL_SEND_CID),获取128位CID,这个不需要解析得太细。
- 发送CMD3(SET_RELATIVE_ADDR),给设备分配一个RCA地址。
- 发送CMD9(SEND_CSD),读取CSD,从这里能解析出容量、支持的最大时钟等关键参数。
- 发送CMD7(SELECT_CARD),用上一步的RCA选中设备。
- 发送CMD6切换总线宽度和工作模式,比如从1线切换到4线或8线,从默认模式切到高速模式。
- 之后就可以用CMD16设置块长度,用CMD17/CMD24读写数据了。
这套流程我建议做成一张状态机表贴在工位上,每个状态对应发什么命令、期待什么响应、超时多久算失败。实际项目里初始化不通过的案例,十有八九是这串流程里某一步的响应没等到就急着发下一步,或者CMD1的轮询条件写反了。
2.4 单块读写的完整交互
初始化完成后,最常用的两个操作就是单块读(CMD17)和单块写(CMD24)。
读单块的交互时序大致是:主机发CMD17,参数是目标字节地址;卡回R1响应;稍等片刻后,卡在DAT线上先发一个起始位,再连续输出512字节数据,最后带16位CRC,结束后拉高结束位。主机要做的就是把DAT线上的串行数据收下来,同时算CRC16做校验。
写单块则稍有不同:主机发CMD24,卡回R1;接下来主机要在DAT线上主动发送起始位+512字节数据+CRC16;卡收到完整数据块后,会在DAT0上返回一个数据响应token,表示数据CRC是否通过;如果没问题,卡会拉低DAT0进入busy状态,这时候主机不能发下一条命令,必须等DAT0恢复高电平。
这里有个新手容易踩的坑:CMD17/CMD24的参数虽然是32位,但EMMC是按字节寻址的,如果你内部用块号管理存储空间,一定要记得把块号左移9位(512字节/块)再填进参数。我见过好几份代码直接传块号进去,结果读写位置全错位了,还在那调了半天FIFO。
3. Verilog控制器搭建:命令引擎、数据通路与状态机
3.1 控制器总体架构
写EMMC控制器前,先想清楚模块分层。我的习惯是拆成三层:最底层的物理层负责把命令和数据按时序打到引脚上;中间层是命令引擎,负责打包命令、接收响应、计算CRC;最上层是应用状态机,负责编排初始化流程和读写流程。用户逻辑只跟最上层打交道,不关心底层时序。
这样做的好处是显而易见的。换不同厂家的EMMC时,物理层和命令引擎基本不用动,最多改一下初始化参数和时钟分频。如果后面想从单块读写升级到多块读写(CMD18/CMD25),也只需要在应用层状态机里扩展,底层数据通路不用推翻重来。
我曾经见过有人把EMMC控制器写成一个巨大的状态机,CMD和DATA的时序耦合在一起,结果调一个问题牵一发动全身。分层设计看着多写几个module,实际上调试效率高得多。
3.2 命令层实现:48位打包与CRC7
命令引擎的核心就是两个功能:把(索引、参数)打包成48位串行发出,以及接收48位响应并解析。这里给一个简化的命令发送模块骨架:
module mmc_cmd_engine #( parameter CLK_DIV = 125 // 分频系数,根据主时钟和目标时钟计算 )( input wire clk, input wire rst_n, // 用户接口 input wire cmd_start, input wire [5:0] cmd_index, input wire [31:0] cmd_arg, output wire [31:0] resp_data, output wire resp_valid, output reg busy, // 物理接口 inout wire mmc_cmd ); reg [6:0] shift_cnt; reg [47:0] cmd_buffer; wire [6:0] crc7 = calc_crc7({cmd_index, cmd_arg}); localparam IDLE = 2'd0; localparam TX = 2'd1; localparam RX = 2'd2; reg [1:0] state; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; busy <= 1'b0; end else begin case (state) IDLE: begin if (cmd_start) begin cmd_buffer <= {1'b0, 1'b1, cmd_index, cmd_arg, crc7, 1'b1}; state <= TX; busy <= 1'b1; end end TX: begin // 按bit依次移出,注意方向和时序 // ... state <= RX; // 发送完后切换方向,等待响应 end RX: begin // 采样响应,做CRC校验 // ... busy <= 1'b0; resp_valid <= 1'b1; state <= IDLE; end endcase end end endmoduleCRC7的生成逻辑建议单独拉一个函数或模块,不要用for循环在always块里实时算,那样在时序收敛上容易出问题。我习惯的做法是预生成一个128项的查找表,索引由每8位查一次表递推得到。开销小、时序干净。
还有一个细节是方向和时序。EMMC的CMD线是双向的,主机发命令时是输出方向,发完命令必须立刻把IO方向切回输入,否则响应会被自己的驱动拉死。用Verilog的inout口时,方向控制信号一定要和状态机的切态严格同步,这个切换点就是我早期低概率死机的重灾区。
3.3 数据通路:FIFO、CRC16与busy检测
数据通路的任务比命令层直观:写数据时从用户FIFO读出8/4/1位宽,并行转串行,同时算CRC16随数据一起发出;读数据时串行转并行,算CRC16和卡发出的CRC比对,把数据写入用户FIFO。
CRC16的多项式是x^16 + x^12 + x^5 + 1,初值0x0000,覆盖整个数据块。需要注意EMMC的CRC16是按"bit流"连续计算的,不是按字节字节分块算完再拼接,所以移位寄存器每拍移入的bit顺序必须严格一致。这个顺序问题在代码里极难用肉眼看出来,但表现出来就是"偶尔读写成功,偶尔失败,失败率还和扇区号有关"。
写路径上还要处理卡返回的数据响应token,只有token里的状态字段等于010(即无错误)才算写成功。很多控制器代码漏了这一步,写完数据就默认成功,结果脏数据在设备内部被主控标记坏块,用户层毫无感知。对需要高可靠性的项目,这属于必须补的一环。
busy处理则简单粗暴:写命令发出后,只要DAT0为低就说明卡还在忙,状态机停在等待状态,直到DAT0恢复高电平。这里建议加一个超时计数器,防止卡异常卡死导致整个控制器假死。
3.4 初始化状态机与用户接口
初始化状态机就是把“CMD0 → 轮询CMD1 → CMD2 → CMD3 → CMD9 → CMD7 → CMD6”这套流程用状态转移实现。状态机的每个状态只做三件事:发命令、等响应、根据响应跳到下一个状态。
用户接口我推荐做成类似寄存器的形式,不搞复杂的总线协议:
| 偏移 | 方向 | 功能 |
|---|---|---|
| 0x00 | 写 | 控制寄存器:bit0启动初始化,bit1软复位 |
| 0x04 | 写 | 目标块地址 |
| 0x08 | 写 | 操作命令:0读单块,1写单块 |
| 0x0C | 读 | 状态寄存器:init_done、busy、错误码 |
| 0x10 | 写/读 | 数据FIFO端口 |
这样一个简单的内存映射寄存器组,挂到任何MCU总线上都很方便,后续接软核或者DMA也不费劲。
module emmc_controller ( input wire clk, input wire rst_n, // EMMC物理接口 output wire emmc_clk, inout wire emmc_cmd, inout wire [7:0] emmc_data, output wire emmc_rst_n, // 简化用户接口 input wire wr_req, input wire rd_req, input wire [31:0] block_addr, input wire [7:0] wr_data, output wire [7:0] rd_data, output reg busy, output reg init_done ); // 内部例化cmd_engine、data_path、init_fsm... endmodule这里要强调一个设计原则:用户接口的信号尽量简单直白。不要一上来就搞AXI、Wishbone这种复杂总线,除非你确定项目后面要接DMA。寄存器接口虽然效率不是最高,但调试时用ILA一眼就能看出问题在哪。
4. 硬件设计别踩坑:原理图、供电与PCB走线
4.1 电源与去耦:VCC、VCCQ到底怎么接
EMMC的供电看似简单,实际是板卡返修的重灾区。VCC主供电通常接3.3V,峰值电流在写操作时能到几百毫安,而且瞬间变化很快,电源纹波稍微大一点就会随机产生写失败。我踩过的教训是:VCC这路不能跟FPGA的3.3V IO电源共用一个LDO输出直接拉过来,最好单独走一个DC-DC或者至少加一级LC滤波。
VCCQ是IO供电,决定CMD和DAT线的电平,接1.8V还是3.3V要看FPGA对应Bank的电平和EMMC手册的支持范围。很多EMMC芯片的VCCQ支持1.8V/3.3V双模,但功耗和时序余量有差异。从时序安全角度,如果用3.3V的FPGA Bank,老老实实接3.3V;如果用1.8V,记得把FPGA相应Bank的VCCO也配成1.8V,同时约束IO标准为LVCMOS18。
去耦电容方面,每个电源引脚旁边放一个100nF小电容,同时在芯片附近放几颗4.7uF~10uF的钽电容或者陶瓷电容稳住瞬态。很多人只放小电容,结果写数据时电压塌陷,表现为“写进去就坏”,这不是代码问题而是电源问题。
4.2 信号完整性与走线
EMMC的153ball封装引脚间距0.5mm,从焊盘引线出来只能从两个方向出线,这个在画原理图符号时就要规划好,不然到PCB阶段会发现绕线绕到怀疑人生。
信号线走线建议:CLK、CMD、DAT0~DAT7尽量等长,控制在50欧姆阻抗附近。实际低成本四层板条件下,CLK在50MHz以下时等长可以放宽到正负5mm,但如果上到100MHz甚至200MHz,就需要认真做等长和阻抗控制了。我这里给个低成本的参考:四层板叠层设为信号-地-电源-信号,所有EMMC信号走顶层,连续地平面在第二层,这样能避免大部分信号完整性问题。
CMD和DAT线建议串联22欧姆的匹配电阻,CLK线串联33欧姆。电阻要靠近FPGA引脚放,不是靠近EMMC引脚。这个方向很多人放反,效果大打折扣。匹配电阻的作用是抑制振铃,尤其是CLK线上的过冲会直接导致EMMC内部时序错乱。
4.3 参考设计:Intel XAPP523能帮你什么
Intel有一份公开参考设计XAPP523,标题就是如何用FPGA驱动EMMC,基于Virtex-5平台,给了完整的VHDL/Verilog源码和原理图。虽然芯片型号老,但MMC协议的思路是通用的,初始化序列、命令封装、数据通路的框架都可以直接参考。
我看这份文档最大的收获不是代码本身,而是它把EMMC的调试时序画得非常清楚:什么时候采样CMD,什么时候切换方向,什么时候检测busy,全是时序图画出来的。你在自己代码里写不清楚的临界关系,对着这份时序图一步步对,基本能找出问题。
不过要提醒一点,XAPP523里的代码默认走的是比较低的速度模式,直接搬来跑高速会有问题。正确的用法是借鉴它的框架和时序,数据通路的位宽、FIFO深度、时钟分频还是要根据自己项目的EMMC型号和主时钟频率重新设计。
5. 实测排错记录:三个卡了很久的问题
5.1 案例一:初始化卡死在CMD1
第一次调EMMC,初始化走到CMD1就卡住了,ILA抓到的波形显示CMD线一直没等到响应。排查过程如下:
第一步用示波器量CMD引脚,发现命令确实发出去了,波形上有明显的48位信号,但后面一片安静。第二步怀疑是卡没上电完成,测了VCC和VCCQ电压,稳定。第三步怀疑是CRC算错,把抓到的波形里命令位用逻辑分析仪解码,对比手册上算出来的期望值,发现CRC字段完全对不上。追到代码里才发现,我偷懒复制的SD卡CRC7算法,多项式虽然一样,但覆盖范围少算了一位。修正后用ILA再看,CMD1正常返回R3。
这个案例说明,EMMC的命令时序里,CRC不是可有可无的点缀,而是设备端判断是否执行命令的依据。命令带错CRC,卡会直接忽略整条命令,而且不会给任何错误提示。
5.2 案例二:写入成功但读回全是0xFF
这个案例更玄学:写操作命令返回正常,数据也发了,卡也回了无错误token,但读回全是0xFF。我用ILA对比了写和读的数据,写进去的数据确实在FIFO出错了——对,问题出在我自己的数据通路上。
排查发现,写入时8bit数据被拆成串行bit流,我的并行转串行逻辑低位和高位顺序搞反了,导致写入EMMC的数据本来就是乱的。EMMC主控不关心内容,只负责把收到的bit按原样写进NAND,所以它毫无怨言地完成了整个流程。这种事如果你只读回做CRC校验,可能根本发现不了,因为写路径和读路径的位顺序都错了,错进错出反而对上了。
解决方法是写一个已知数据模式(比如0x55、0xAA交替),读回后直接和期望值比对。不做这一步,很多数据通路的位序问题会被隐藏掉。
5.3 案例三:8位模式切换后直接死锁
初始化在1位模式下跑得稳稳的,一执行CMD6切8位模式,卡就完全不理人了。排查到最后发现是EXT_CSD的BUS_WIDTH写入方式有问题。
EMMC的CMD6参数里有ACCESS、INDEX、VALUE三个核心字段,网上能找到的例程写法各不相同,有的用Access=2表示写字节,有的用例程里现成的常量。我最初用的是从某个开发板代码里抄来的常量,在该开发板的EMMC型号上没问题,换了一颗芯片就死锁。
最后按照EMMC 5.1手册,重新理解了CMD6的字段布局,自己构造参数写EXT_CSD索引183(BUS_WIDTH)为2(8位模式),问题解决。这个坑给的经验是:EMMC不同型号对CMD6参数的兼容性确实有差异,最稳妥的做法是以手册为准自己构造参数,不要盲抄网上现成的常量。
5.4 调试手段:ILA加示波器
调EMMC时我用的三件套:ILA抓FPGA内部状态、逻辑分析仪抓CMD/DAT线上的协议波形、示波器看电源纹波和时序边沿。
ILA适合看状态机和内部数据流,逻辑分析仪适合核对协议层面的时序,示波器则用来排查电源和信号完整性问题。我建议一开始把EMMC时钟降到1MHz甚至更低调试,把协议跑通之后再慢慢往上拉频率。很多人一上来就跑50MHz,结果波形一塌糊涂,反而分不清是协议问题还是SI问题。
6. 带宽实测与后续扩展建议
6.1 实测带宽数据
我在自己的一块低成本的Artix-7板子上实测过几组配置,数据如下(使用EMMC 5.1,8GB):
| 模式 | 总线位宽 | 时钟频率 | 理论带宽 | 实测读带宽 | 实测写带宽 |
|---|---|---|---|---|---|
| 默认 | 1bit | 25MHz | 3.125MB/s | 2.8MB/s | 2.1MB/s |
| 4bit SDR | 4bit | 50MHz | 25MB/s | 22MB/s | 15MB/s |
| 8bit SDR | 8bit | 50MHz | 50MB/s | 45MB/s | 32MB/s |
| 8bit SDR | 8bit | 100MHz | 100MB/s | 88MB/s | 61MB/s |
写带宽明显低于读带宽,这是NAND的固有特性——写之前可能要擦除、要搬移有效数据,EMMC主控内部的FTL逻辑也要花时间。如果你的应用是高频写数据,建议在FIFO预留足够深度,写多点数据时用多块写命令(CMD25),单块写命令的交互开销会吃掉不少带宽。
6.2 后续扩展:DMA、文件系统与更高性能
纯RTL的寄存器接口虽然简单,但带宽做上去之后CPU逐个寄存器塞数据是不现实的。如果要追求高吞吐,可以把用户接口升级成AXI-Stream或者自带的简单DMA,让数据直接批量搬进DDR再刷进EMMC,这样FPGA侧的吞吐瓶颈就不在存储控制器,而在DDR带宽了。
另外,如果项目需要文件系统,不要试图用纯Verilog去解析FAT32——工程量不划算。更合理的方案是在FPGA里跑一个MicroBlaze或Nios II软核,挂上DMA和EMMC控制器,跑FatFS这类开源文件系统。EMMC对软核来说就是一个块设备,驱动层已经验证过,文件系统代码成熟稳定,开发效率高很多。
如果还想压榨EMMC 5.1的性能,可以研究HS200和HS400模式。但这两个模式需要tuning流程,对FPGA实现的时序收敛要求很高,低成本方案我建议先跑到8bit SDR 100MHz,板子设计和逻辑复杂度都在可控范围内。等把SDR模式彻底吃透了,再考虑要不要上HS系列,不要一上来就追求最高规格。
我个人在实际项目中总结出的经验是:EMMC的优势不在于单项性能有多极限,而在于它把存储里最脏最复杂的部分全部封装掉了。只要协议走得通、电源做得稳、数据通路位序对,它就是一个非常省心的低成本大容量存储方案。希望这篇文章能帮你少走我当年走过的弯路。