ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA驱动EMMC存储方案:从选型到Verilog实现全流程详解

FPGA驱动EMMC存储方案:从选型到Verilog实现全流程详解 我最早在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.5mmPCB加工门槛比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芯片为例控制面只需要这些信号信号方向说明CLKFPGA → EMMC时钟初始阶段不超过400kHz运行后可提升到50/100/200MHzCMD双向命令线与响应线复用DAT0~DAT7双向数据线支持1/4/8位模式RST_nFPGA → EMMC复位低有效上电后至少保持1us低电平VCC电源主供电3.3VVCCQ电源IO供电1.8V或3.3V市面上所谓153ball EMMC引脚定义资料上画得密密麻麻实际上你只要在原理图里把这几根信号和电源网络连对剩下的球按封装图接到地或悬空就行。第一次画板子的人最容易在这里产生错觉觉得153个脚很难搞其实大部分引脚定义里写了NCNot Connected或者GND真正需要动的不到20个。EMMC很多型号的VCCQ可以选1.8V或3.3V这决定了FPGA端IO的电平标准。如果FPGA的Bank电压是3.3VVCCQ也接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等几种。调试时最常打交道的是R132位状态CRC以及初始化阶段的R3OCR寄存器和R2CID/CSD寄存器。R1的CRC是卡对状态字段自己算好的主机在接收时校验CRC能顺便发现时钟线不稳定、采样沿不对这类底层问题所以接收端一定要做CRC检查不要怕麻烦。2.3 初始化流程一轮命令编排EMMC上电后不会立刻进到可以读写数据的状态它要先走一遍初始化把总线宽度、时钟频率、工作模式都谈好。标准流程是这样的上电后CLK以400kHz以下频率工作等待电源稳定RST_n释放。发送CMD0参数0xF0F0F0F0让设备进入Idle状态。循环发送CMD1SEND_OP_COND读取OCR寄存器直到busy位拉高表示设备内部上电完成。发送CMD2ALL_SEND_CID获取128位CID这个不需要解析得太细。发送CMD3SET_RELATIVE_ADDR给设备分配一个RCA地址。发送CMD9SEND_CSD读取CSD从这里能解析出容量、支持的最大时钟等关键参数。发送CMD7SELECT_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 2d0; localparam TX 2d1; localparam RX 2d2; reg [1:0] state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; busy 1b0; end else begin case (state) IDLE: begin if (cmd_start) begin cmd_buffer {1b0, 1b1, cmd_index, cmd_arg, crc7, 1b1}; state TX; busy 1b1; end end TX: begin // 按bit依次移出注意方向和时序 // ... state RX; // 发送完后切换方向等待响应 end RX: begin // 采样响应做CRC校验 // ... busy 1b0; resp_valid 1b1; 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三个核心字段网上能找到的例程写法各不相同有的用Access2表示写字节有的用例程里现成的常量。我最初用的是从某个开发板代码里抄来的常量在该开发板的EMMC型号上没问题换了一颗芯片就死锁。最后按照EMMC 5.1手册重新理解了CMD6的字段布局自己构造参数写EXT_CSD索引183BUS_WIDTH为28位模式问题解决。这个坑给的经验是EMMC不同型号对CMD6参数的兼容性确实有差异最稳妥的做法是以手册为准自己构造参数不要盲抄网上现成的常量。5.4 调试手段ILA加示波器调EMMC时我用的三件套ILA抓FPGA内部状态、逻辑分析仪抓CMD/DAT线上的协议波形、示波器看电源纹波和时序边沿。ILA适合看状态机和内部数据流逻辑分析仪适合核对协议层面的时序示波器则用来排查电源和信号完整性问题。我建议一开始把EMMC时钟降到1MHz甚至更低调试把协议跑通之后再慢慢往上拉频率。很多人一上来就跑50MHz结果波形一塌糊涂反而分不清是协议问题还是SI问题。6. 带宽实测与后续扩展建议6.1 实测带宽数据我在自己的一块低成本的Artix-7板子上实测过几组配置数据如下使用EMMC 5.18GB模式总线位宽时钟频率理论带宽实测读带宽实测写带宽默认1bit25MHz3.125MB/s2.8MB/s2.1MB/s4bit SDR4bit50MHz25MB/s22MB/s15MB/s8bit SDR8bit50MHz50MB/s45MB/s32MB/s8bit SDR8bit100MHz100MB/s88MB/s61MB/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的优势不在于单项性能有多极限而在于它把存储里最脏最复杂的部分全部封装掉了。只要协议走得通、电源做得稳、数据通路位序对它就是一个非常省心的低成本大容量存储方案。希望这篇文章能帮你少走我当年走过的弯路。
返回列表