
做FPGA开发的人一提到大容量存储条件反射就是SD卡、SATA或者直接挂DDR。实际上很多数据采集、图像处理、日志记录的项目EMMC才是性价比最合适的那条路。EMMC和SD卡流着同样的血但它在内部把坏块管理、均衡磨损、ECC纠错全做完了FPGA这边只要按照协议发命令、按块读写就行开发难度直接降了一个量级。这篇文章把我用Verilog从零驱动EMMC的完整思路和踩坑记录整理出来从协议细节、引脚定义、状态机实现到调试经验尽量讲得让刚接触FPGA的人也能照葫芦画瓢。我做过的项目里EMMC大多用来做图像的原始数据记录和一些关键日志的长期存储。之前用SD卡方案卡槽接触不良、文件系统崩溃、掉电丢数据这些问题在工业现场太难受了。换到EMMC之后板子上就是几颗芯片焊在PCB上连接可靠性完全由焊接保证。至于文件系统FPGA这边一般只做裸块读写需要文件的时候再在外面套一个FAT32或者ext4的软核模块。这个方案理解了之后你会发现FPGA驱动EMMC并不神秘核心就是命令时序、数据搬运和状态机设计这几件事。1. 为什么选EMMC低成本大容量存储方案的选型思路1.1 从SD卡到NANDEMMC凭什么更合适很多工程师习惯性地把存储方案分成两个极端小数据量用Flash芯片大数据量用SATA硬盘。而EMMC正好卡在中间这个甜点区容量从4GB到128GB都有价格比同容量的SD卡稍微贵一点但比SATA盘便宜不少。它的内部结构就是一颗NAND Flash加一颗控制器芯片再加一堆固件外面的接口是MMC协议和SD协议同源但又不太一样。如果你直接用裸NAND那就意味着你要自己维护坏块表、自己做ECC纠错、自己做擦写均衡。这些事在Linux内核里有现成代码但在FPGA里做会拖很长开发周期。EMMC把这些脏活累活全包了芯片出厂就经过完整的测试和标记内部控制器会自动做逻辑地址到物理地址的映射你写0x0000_0000这个逻辑块它内部可能写到了物理块的任何一个位置你根本不需要关心。对你的FPGA逻辑来说这就是一个按块读写的“大号Flash”粒度是512字节效率很高。SD卡的优势在于可拆卸、随处可得但劣势也很明显卡座占用面积、震动接触不良、工业温度范围窄、文件系统频繁写入容易掉数据。EMMC全部是BGA封装焊死在板子上抗震动能力和温度特性都要好很多。对数据采集、飞控记录、图像缓存这类需要连续写入的场景EMMC的可靠性几乎是碾压SD卡的。1.2 系统整体架构与外部电路设计FPGA驱动EMMC的硬件连接并不复杂参考电路大致是FPGA的一个IO Bank引出CLK、CMD、DAT0到DAT7、DSHS400模式可选、RST_nCMD和DAT线各接上拉电阻到VCCQVCC供电3.3VVCCQ根据IO电压选1.8V或3.3V每个电源附近放几个0.1uF和1uF的退耦电容。EMMC的CLK信号由FPGA的PLL输出可以动态调整频率初始化阶段跑低一些比如400kHz稳定后切换到高速模式。VCC和VCCQ要分清VCC是整个NAND存储阵列的供电电流需求较大VCCQ是控制器IO部分的供电决定CMD和DAT线的电平。如果FPGA的Bank电压是3.3VVCCQ就接3.3V如果你用的是1.8V的BankVCCQ也得跟着接1.8V两边必须匹配。这个电压搞错了轻则通讯不稳定重则直接把EMMC芯片打死。EMMC采用的是BGA封装最常见的是153ball和169ball还有少数100ball。153ball封装是0.5mm球间距手工焊接非常痛苦建议直接找工厂贴片。PCB走线方面EMMC工作频率一旦上了100MHz信号完整性就要上心了CMD和DAT线尽量等长CLK线要短而且包地VCC和VCCQ的电源要考虑足够宽。很多调试不顺的板子最后排查半天发现就是电源纹波太大或者走线太长导致的。2. EMMC协议核心细节命令、响应与时序拆解2.1 引脚定义与硬件连接我以153ball封装的EMMC为例关键引脚如下表引脚信号方向功能说明CLK输入时钟信号由主机提供CMD双向命令线和响应线共用DAT0~DAT7双向8位数据总线可配置为1位或8位模式DS输入/输出数据选通信号HS400模式必备RST_n输入硬件复位低有效eMMC 5.1新增功能VCC电源3.3V存储阵列供电VCCQ电源IO供电1.8V或3.3V取决于接口电平GND地接地引脚CMD线是半双工的主机发命令时它输出等待响应时它变成输入。DAT线是真正的双向总线读数据时卡方输出写数据时主机方输出。这就意味着在Verilog代码里CMD和DAT管脚必须处理三态逻辑高阻输入和强驱动输出要切换得当。很多新手在这里翻车方向切换没处理好导致读写数据错乱。RST_n这个引脚不是所有EMMC都有老一点的版本可能没有。如果你的芯片有这个引脚务必从FPGA的IO引出来控制上电时拉低让芯片复位然后再释放同时准备74个时钟周期的空闲时间让芯片完成内部初始化。如果没有硬件复位引脚就靠软件CMD0复位同样有效。2.2 命令、响应和CRC校验EMMC协议和SD协议的命令格式是一样的都是48位起始位0、传输位1、6位命令索引、32位命令参数、7位CRC校验、结束位0。发送的时候从第一位开始逐位移出发送频率由CLK决定。给你一个直观的记忆方式命令路由CMD线先走主机把48位打包最后一位是0表示结束CMD线释放回高阻状态准备接收卡的响应。响应类型常见的有R1状态响应、R2CID寄存器内容、R3OCR寄存器内容、R6RCA地址响应、R7接口条件响应。绝大多数数据读写命令响应都是R1长度同样是48位。你可以用一个通用的接收模块把响应解析出来提取出状态寄存器的值重点关注错误位。数据线的校验用的是CRC16多项式是x^16x^12x^51对应二进制0x11021。而命令线上的CRC7多项式是x^7x^31对应二进制0x89。这些多项式在Verilog里可以用线性反馈移位寄存器实现也可以直接用查表法。对于行波检测来说CRC算错了通常说明硬件连接或仿真激励有问题先别急着怪芯片仔细查一遍CRC计算逻辑。2.3 初始化序列MMC特有的“登卡”流程EMMC初始化流程和SD卡最大的区别在于SD卡用ACMD41做电压协商而EMMC用CMD1。如果你把SD卡的代码直接拿过来大概率卡死在初始化。这是两个协议最容易混淆的地方务必记住。初始化的标准流程如下步骤命令作用1CMD0软件复位让EMMC进入Idle状态2CMD1读取/设置OCR查询设备是否忙3CMD2读取CID寄存器获取设备唯一标识4CMD3获取相对地址RCA5CMD7使用RCA选中设备卡6CMD8读取EXT_CSD寄存器获取设备能力信息7CMD6切换工作模式使能高速模式8CMD13查询状态确认进入Transfer状态整个流程的关键点是CMD1的busy位。设备上电后内部要初始化这个时间有长有短你的状态机必须循环发送CMD1一直读到OCR的最高位变成1才算初始化完成。不能只发一次也不能设置太短的超时否则就是“卡在初始化”的坑。另外要注意的是EMMC的CMD8和SD卡的CMD8含义完全不同。EMMC的CMD8是SEND_EXT_CSD用来读取512字节的扩展寄存器里面包含了设备类型、分区大小、最大读写速度等信息。SD卡的CMD8是SEND_IF_COND用来检测电压范围和主机支持。你用SD卡的流程去驱动EMMC命令参数和响应解析完全对不上。我在调试的时候就在这上面耗了差不多一个下午。3. Verilog模块实现从状态机到可复用代码3.1 顶层模块划分与接口设计写FPGA驱动EMMC一开始就要把模块划分清楚。我习惯于分成四个部分顶层控制、命令引擎、数据引擎、时钟复位管理。顶层控制模块负责任务调度比如用户发起一个写请求它要知道先发CMD23设置块数再发CMD25进入写模式然后通知数据引擎开始搬运数据。我给一个简单的顶层接口示例module emmc_driver_top( input wire clk, // 系统时钟 input wire rst_n, // 异步复位 // 用户命令接口 input wire wr_start, // 写启动 input wire [31:0] wr_block_addr, // 写起始块地址 input wire [23:0] wr_block_cnt, // 写块数量每块512字节 input wire [31:0] wr_data_0, // 写入数据按512bit整块 input wire [31:0] wr_data_1, input wire wr_data_valid, output reg wr_busy, // 写忙标志 output reg wr_done, // 写完成 // 用户读接口 input wire rd_start, input wire [31:0] rd_block_addr, input wire [23:0] rd_block_cnt, output reg [31:0] rd_data_0, output reg [31:0] rd_data_1, output reg rd_data_valid, output reg rd_busy, output reg rd_done, // EMMC物理接口 output wire emmc_clk, inout wire emmc_cmd, inout wire [7:0] emmc_data, output wire emmc_rst_n );上面这个接口是一次传输一整块数据适合定点数据搬运。实际项目如果涉及高速流水线建议在顶层加一个同步FIFO把数据接口改成AXI4-StreamDMA连续搬运吞吐量会高很多。3.2 初始化状态机与关键代码初始化状态机是整个驱动的主干设计成一个Moore状态机。复位后首先进入预初始化状态把CLK频率降到400kHz左右等待至少8个时钟周期再开始发CMD0。状态跳转可以这样设计localparam S_INIT_PRE 5d0; localparam S_INIT_CMD0 5d1; localparam S_INIT_CMD1 5d2; localparam S_INIT_CMD2 5d3; localparam S_INIT_CMD3 5d4; localparam S_INIT_CMD7 5d5; localparam S_INIT_CMD8 5d6; localparam S_INIT_CMD6 5d7; localparam S_INIT_READY 5d8;每个状态做两件事把命令字段拼好送到命令发送器然后等待响应/超时信号。以CMD1为例它需要循环重发直到OCR的busy位置1所以状态机里有个内部计数器如果超时没收到有效响应就重新发一遍。超时时间建议给到1秒级别EMMC卡上电初始化有的确实比较慢。实现时最需要注意的是命令发送器内部有分频机制。初始化阶段用低速时钟稳定后把CLK切回高速但切换必须在一个命令完整结束后进行不能在命令传输过程中跳变否则EMMC会直接死锁。我一般会把时钟分频值做成寄存器映射状态机发完CMD8、确认设备支持高速模式后再改分频值。3.3 数据读写命令的实现细节读单块用CMD17写单块用CMD24连续多块读用CMD18多块写用CMD25。在做多块写之前必须先发CMD23SET_BLOCK_COUNT设定要写的块数然后发CMD25进入写数据传输模式然后连续发送数据块。如果不发CMD23EMMC会认为你要写入无限长的数据流直到收到CMD12停止命令这样容易出问题。多块读也一样推荐先发CMD23设定块数再发CMD18这样可以避免使用open-ended方式需要手动发CMD12收尾的麻烦。写数据的时序细节是CMD25的响应返回后主机开始连续发送数据块。每个数据块由512字节数据加16位CRC加上结束位1组成。数据块之间可以有间隔但不能太长否则EMMC会超时退出。数据块发送速度由你的FIFO深度决定如果你的数据源填充速度跟不上建议FIFO多留点余量。CRC16的计算可以用一个简单的移位寄存器实现function [15:0] cal_crc16; input [511:0] data; integer i; reg [15:0] crc; begin crc 16h0000; for (i 511; i 0; i i - 1) begin crc {crc[14:0], 1b0} ^ ({16{(crc[15] ^ data[i])}} 16h1021); end cal_crc16 crc; end endfunction注意这个算法是从数据的最高位开始算还是从最低位开始算不同资料可能有差异以JEDEC标准为准。我一开始用了网上一个LFSR模板算出来的CRC和EMMC对不上后来对照标准文档逐位验证才改对。写完数据之后EMMC进入programming状态你没法立刻发下一条写命令必须通过CMD13查询状态等READY_FOR_DATA位变成1才能继续。读写流程中还有一个容易忽视的点DAT线上的busy信号一般以DAT0低电平表示设备正忙在连续读写切换时要等busy释放。如果用CMD13轮询可以忽略DAT0的busy检测但寄存器轮询会产生额外的时间开销高性能系统建议二者结合。3.4 性能估算与实际带宽怎么算FPGA驱动EMMC的最终性能既取决于EMMC芯片的代际和总线模式也取决于你的控制逻辑有多高效。以常见的EMMC 5.1芯片为例HS200模式跑8-bit SDR时钟200MHz理论带宽就是200MHz乘以8bit也就是200MB/s。但实际跑不到这个值因为命令切换、CRC开销、内部NAND Program时间都吃掉了不少带宽。我在实际项目里连续写能达到140MB/s左右连续读能到170MB/s。如果只跑默认的50MHz SDR模式理论带宽是50MB/s实际读大概40MB/s写可能只有20MB/s到30MB/s。对于图像采集来说如果源数据率超过40MB/s你就必须切高速模式或者缩小采集分辨率。选型阶段先算清楚需求不要等板子回来了才发现带宽不够。控制逻辑对带宽的影响主要体现在写路径上如果每写完一块都用CMD13轮询那性能会非常难看。更好的做法是采用乒乓缓存数据引擎一边写FIFO控制引擎一边发命令等FIFO积累到预定深度就启动下一次突发写把设备busy时间掩盖掉。这个思路和DMA设计是一模一样的。4. 实测问题与调试记录4.1 卡死在CMD0或CMD1怎么排查最常遇到的现象就是初始化状态机一直卡在CMD1循环不出去。排查顺序建议是先量EMMC的CLK引脚有没有波形频率对不对再看CMD线上有没有上拉电阻电平是否正常。上电时序也很关键VCC和VCCQ要先稳定然后再释放复位中间要有间隔。我遇到过一块板子VCCQ的LDO启动太慢EMMC上电后一直处于欠压状态CMD1永远不回busy位最后往VCCQ上加了一个大的放电电阻上电时序才正常。CMD0发不出去的情况多半是主机侧时钟分频还没产生EMMC需要至少74个时钟周期的准备时间。很多人在初始化状态机里从复位释放到发CMD0之间只隔了极短时间这在高速时钟下可能会出错。解决办法就是严格按照上电顺序硬件复位释放后先跑几百个CLK周期的空转再发CMD0不要省这几行代码。4.2 CRC校验失败的常见原因CRC校验失败在数据阶段比命令阶段更常见。写命令响应正常但数据写入开启后马上报CRC错误通常是数据线方向切换出了问题。举个例子你的数据引擎在发送完最后一个字节后应该把DAT三态释放但如果你释放得太晚命令引擎发下一条CMD时DAT线还在被FPGA强驱总线冲突就产生了。解决的办法是在数据路径上加一个方向寄存器写状态时置1发完数据统计完字节数和CRC后置0严格同步到CLK确保释放操作发生在一个周期的边界上。还有一点CMD和DAT的上拉电阻一定不要省BGA焊盘到芯片内部的上拉在有些型号里是弱上拉不够稳定外部加10k到47k的上拉会可靠很多。4.3 跑HS200或HS400时信号完整性问题高速模式跑不上去的表现一般是初始化阶段SDR模式完全正常一切HS200就大量CRC错误或者干脆无响应。这基本就是信号完整性问题了。检查PCB上CLK到EMMC的距离如果绕了很远波形会变差DAT和CMD的走线有没有和其他高速信号交叉有没有良好的回流地平面。还有个容易忽略的点就是FPGA的IO输出强度设置不同Bank的驱动强度不同过强过弱都会影响波形质量可以在工程里调整IOStandard和Slew Rate选Slow Slew往往能改善信号完整性。HS400模式需要DS信号线而且要求更严格CLK和DS要成对做等长控制一般工程上不建议FPGA直接用HS400除非你非常熟悉DDR时序的约束写法。HS200的收益已经够了逻辑难度比HS400低很多。4.4 写放大与寿命不能忽略的工程细节EMMC内部虽然做了均衡磨损但不代表你可以随便写。频繁的小块随机写会造成内部写放大缩短芯片寿命。如果你的应用是日志型数据尽量攒够一个缓冲再突发写入同时保证数据块地址尽量连续。对图像数据来说按帧记录天然就是连续大块写入这点问题不大。另一个经验是掉电保护。EMMC在写数据途中突然掉电有可能会出现数据损坏甚至逻辑块映射表异常。工业产品建议在EMMC的VCC前面加一个掉电检测电路检测到电源跌落立刻把主机侧停在安全状态至少不要在新的周期启动写命令。如果你的产品对可靠性要求极高还可以预留一个备份分区定期把关键文件复制过去。4.5 调试工具与仿真验证方法FPGA调试EMMC最有效的工具是ILA或SignalTap直接抓内部状态机的跳转和总线上的信号。我一般会抓这几组信号当前状态值、命令发送器的移位寄存器输出、CMD线采样值、响应接收到的索引字段、数据FIFO的写使能和满信号。把这几组信号拉出来看波形基本所有问题都能定位。特别是卡在哪个状态一眼就能看出来。如果没有FPGA板子可以先写一个EMMC的仿真模型用行为级代码模拟初始化流程和数据读写验证你的状态机逻辑是否和协议匹配。网上有一些现成的Verilog EMMC model但质量参差不齐最好自己写一个简单的异步收发模型。注意仿真模型只验证协议时序不验证信号完整性所以上板之后还是要以ILA抓到的波形为准。用ILA抓波形时别只抓低速信号要把CLK、CMD、DAT一起抓采样时钟选择你给EMMC的实际工作时钟或更高频率的时钟。如果条件允许最好在CMD和DAT每个通路加上延迟链的约束保证采样点稳定。就我个人实际下来的经验EMMC调试三大坑分别是初始化流程和SD卡搞混、写数据后的busy状态没等、上电时序不满足。头两个理清楚之后基本就成功了八成。板上调试的时候再把ILA用起来剩下的事就是不断读写测试直到连续跑24小时不出错。最后再给你一个小建议正式量产前用同一批EMMC芯片做温度和电压拉偏测试数据线走线阻抗匹配和电源噪声这些小问题在极端环境下会无限放大提前暴露比售后发现要好太多。FPGA驱动EMMC这个方向核心其实不是代码量有多大而是对协议和硬件的理解够不够深把这两点补上你的低成本大容量存储方案就可以稳定落地了。