
简介一份面向FPGA嵌入式开发者的eMMC读写工程范例基于Zynq UltraScale MPSoC XCZU4EV平台使用VITIS工具链完成硬件设计与软件驱动联调。资源适合有一定FPGA基础、想学习MPSoC系统集成或存储接口开发的工程师也适用于XCZU2CG、XCZU2EG等相近芯片的迁移参考。整个压缩包包含1896个文件大小36.86MB。核心源码集中在740个h头文件、406个c源文件以及17个v/12个sv硬件描述文件中并带有60个makefile、17个tcl脚本、20个so动态库和16个a静态库等编译构建相关文件另有XSA硬件描述文件、XDC约束文件和BIF启动文件便于复现完整工程。文件类型覆盖源码、编译中间文件、工程配置与日志整体结构较完整。通过学习可掌握eMMC控制器设计、Vitis平台下软硬件协同开发、驱动编写与系统验证方法。已有198人学习下载对入门MPSoC存储应用具有参考价值。1. XCZU4EV 上做 eMMC 读写数据为什么先看 Vitis 工程一块 XCZU4EV 的 PS 里本来就有 SD/eMMC 控制器但“eMMC 能识别”和“eMMC 数据能稳定读写”是两回事。很多人拿到打包好的 Vitis 工程第一反应是直接烧到板卡上结果读回来的数据要么全是 0xFF要么在 CRC 校验上随机失败。这是因为 eMMC 的引脚电平、MIO 分配、DMA 缓存一致性和速度模式每一环都影响最终结果。这类工程的核心是把 Zynq UltraScale PS 的 SD 控制器完整用起来Vitis 裸机环境下初始化 eMMC通过控制器内置 DMA 搬运块数据再配合 ext_csd 里的速度模式配置把带宽拉上去。相比在 Linux 下挂 mmcblk 设备裸机或者轻量 RTOS 的路径更短也更容易把问题定位到寄存器一级。适合谁需要“上电即读写、掉电数据还在”的存储节点但又不想为 eMMC 单独部署一套 Linux 的工程师。下面按硬件挂法、驱动代码、HS400 时序和 fstrim 边界依次展开。2. 从硬件挂法说起eMMC 走 PS 的 SD 控制器还是 PL 侧控制器2.1 PS 内置 SD/eMMC 控制器与 PL 方案的取舍Zynq UltraScale 的 PS 集成两个 SD 控制器SD0 和 SD1均支持 SD 卡和 eMMC 两种协议。对 XCZU4EV 来说优先用 PS 控制器而不是在 PL 里例化额外的 eMMC 控制器是因为 PS 侧已经处理了命令重试、DMA 描述符和中断逻辑PL 侧的方案则需要额外的 IP 授权、FIFO 深度规划和时序约束成本。另一个原因在数据通路上PS 控制器可以把 DMA 直接指向 DDR读写的复杂度落在 PS 内存子系统中。PL 方案要经过 AXI 总线和 DDR 控制器延时更高带宽也容易被其他 PL 主设备挤占。确认使用方案时可以按下面这张表做快速判断方案控制器位置数据线宽度HS400 支持PL 资源消耗PS SD0/SD1 直连PS 内置4/8 bit取决于 MIO bank 电压和走线质量0 LUTPS 控制器经 EMIO 引出PS 控制器4 bit基本不支持中等PL 侧 AXI eMMC 控制器PL 逻辑4 bit不推荐高我在实际项目中几乎都走第一行。eMMC 的 CMD、CLK、DATA0-DATA7 和 DQS 直接连到 MIO所有协议时序都由 PS 侧寄存器完成PL 逻辑只在需要做高速数据预处理时才介入。2.2 核对 MIO 分配、电压域和复位脚拿到板卡原理图后第一件事不是开 Vitis而是查 eMMC 挂在哪个 SD 控制器上。SD0 和 SD1 对应的 MIO 引脚组不同连到哪一组的依据是 PS 引脚复用表不同板卡设计差异很大。要重点确认三件事eMMC 的 CMD/CLK/DATA0-DATA7/DQS 当前是否已被其他外设复用MIO 对应 bank 的电平是 1.8V 还是 3.3V这决定后续能否进入 HS200/HS400 高速档eMMC 的复位信号和供电控制是否接在 PS MIO 上如果悬空掉电再上电时可能出现卡状态不确定。最容易出的问题就是电压域。XCZU4EV 的 MIO bank 电压由板级 PMIC 决定如果控制器输出 3.3V 而 eMMC 工作在 1.8V识别阶段不会立刻失败但高速读写时会产生误码。HS400 模式强制要求 1.8V 信号电平这点要在原理图阶段就确认好。2.3 从 Vivado 配置到 Vitis 打开已有工程的路径Vivado 里配置好 PS 后在 Zynq UltraScale MPSoC IP 配置界面的 I/O Configuration 中启用 SD0 或 SD1。eMMC 不需要 CD/WP 检测引脚这两个引脚可以留空或拉到无效电平。随后导出硬件时要包含比特流否则 Vitis 平台里 PS 初始化代码拿不到完整的引脚配置信息。导出 xsa 后在 Vitis 里建 Platform新建 Platform Project选择“从 XSA 创建”Vitis 会自动生成带 BSP 的平台。如果拿到的压缩包里已有 .xpr先在 Vivado 里打开重新导出一次只有 .xsa 的话直接导入即可。这里有一个小技巧打开已有工程后先搜一下 xparameters.h 里有没有 XPAR_XSDPSU_0_BASEADDR 这个宏。能搜到说明 SD 控制器已经被 BSP 识别后面驱动挂载就有着落。命令行方式也一样vivado -mode batch -source export_hw.tcl vitis -workspace ./workspace export_hw.tcl 里只需要一行关键命令write_hw_platform -fixed -include_bit -file system.xsa其中 -fixed 表示锁定硬件描述-include_bit 把比特流打包进去。导出后打开 Vitis workspace按前面步骤建立平台工程即可继续。3. Vitis 里把 eMMC 初始化与扇区读写代码跑通3.1 先确认 BSP 里的 SD 驱动是 sdpsu 还是 xilsdVitis 2019.2 之后的 standalone BSPZynq UltraScale 的 SD/eMMC 控制器统一由 sdpsu 驱动管理。老一点工程里可能还残留 xilsd 的调用方式两者函数名不同但基本流程一致查询配置、初始化控制器、设置时钟、设置总线宽度、执行数据搬移。我拿到工程后的第一步是在 platform 里的 BSP 设置中确认驱动名称。如果使用 xilsd函数前缀会是 XSd_ 或 XSdPs_如果使用 sdpsu则前缀是 XSdPsu_。下面代码按 XSdPsu 来写对应 2020.x 左右的常见 Vitis 版本。3.2 eMMC 初始化为什么必须先 400kHz 再抬时钟eMMC 上电后处于未知状态控制器必须以 400kHz 识别时钟发送 CMD1 等命令读取 OCR等卡退出上电时序后才能提高时钟频率。BSP 的初始化函数通常只配置控制器本身切换总线宽度和速度模式的节奏需要由应用代码控制。下面是一段可直接参考的初始化骨架#include xsdpsu.h #include xil_cache.h #include xil_printf.h #define EMMC_DEV_ID XPAR_XSDPSU_0_DEVICE_ID static XSdPsu Sd; int emmc_init(void) { XSdPsu_Config *Cfg; Cfg XSdPsu_LookupConfig(EMMC_DEV_ID); if (Cfg NULL) { xil_printf(sdpsu config not found\r\n); return -1; } if (XSdPsu_CfgInitialize(Sd, Cfg, Cfg-BaseAddress) ! XST_SUCCESS) { xil_printf(sdpsu init failed\r\n); return -2; } /* eMMC 识别阶段固定使用 400kHz 时钟 */ XSdPsu_SetClk(Sd, 400000); /* 切到 8bit 总线eMMC 才能达到 HS400 所需的带宽 */ XSdPsu_SetBusWidth(Sd, XSDPSU_BUS_WIDTH_8BIT); /* 总线宽度就绪后再升到 50MHz SDR 模式 */ XSdPsu_SetSpeedMode(Sd, XSDPSU_SPEED_MODE_SDR); XSdPsu_SetClk(Sd, 50000000); return 0; }这段代码的前两个调用是基础后两个调用决定了 eMMC 工作在哪一档。XSdPsu_SetClk(Sd, 400000)传入的是 HzBSP 内部会换算成分频系数XSDPSU_SPEED_MODE_SDR表示单沿采样模式稳妥但带宽上限低。如果目标只是验证读写通路50MHz SDR 足够要跑性能则还需进入第 4 章的高速模式配置。注意不同 Vitis 版本里 XSdPsu_SetBusWidth 的参数名可能是 XSDPSU_8BIT 而不是 XSDPSU_BUS_WIDTH_8BIT。以你安装版本的 xsdpsu.h 头文件为准运行时以函数返回值确认是否设置成功。3.3 读 eMMC 扇区缓冲区对齐与 cache invalidate 缺一不可读操作的正确顺序是先把 CPU 之前写过数据的 cache 行刷到 DDR再发起 DMA 读最后对缓冲区做 invalidate。如果漏掉最后的 invalidateCPU 会从 cache 里拿到旧数据表现为“数据读回来了但和 eMMC 里的不一样”。int emmc_read_sector(u32 lba, u32 *dst) { s32 ret; /* DMA 写回 DDR 前确保 CPU 对目标地址的旧 cache 不干扰 */ Xil_DCacheFlush(); /* 从 lba 地址读取 1 个扇区共 512 字节 */ ret XSdPsu_Read(Sd, lba, 1, (UINTPTR)dst); if (ret ! XST_SUCCESS) { xil_printf(read lba %lu fail: %d\r\n, lba, ret); return -1; } /* DMA 已经完成使 CPU 能看到 DDR 中的新数据 */ Xil_DCacheInvalidate(); return 0; }dst 缓冲区必须满足 DMA 对齐要求通常是对齐到 64 字节。如果使用普通数组建议用__attribute__((aligned(64)))显式声明或在链接脚本里预留一段 64 字节对齐的 DDR 空间。XSdPsu_Read 的第三个参数是扇区数而不是字节数这个容易写混。扇区数乘以 512 就是实际字节数读取大块数据时扇区数可以达到几千缓冲区和 cache 操作都要按真实长度处理。3.4 写 eMMC 扇区flush 的时机比你想的更敏感写操作的 cache 问题出现在 DMA 读取源地址的时刻。CPU 先往缓冲区写入数据这些数据可能还停在 cache 里没有真正到达 DDR。此时发起 DMA 写控制器从 DDR 搬运的是旧内容。int emmc_write_sector(u32 lba, u32 *src) { s32 ret; /* 把 CPU 写入的源数据刷到 DDRDMA 才能拿到最新值 */ Xil_DCacheFlush(); ret XSdPsu_Write(Sd, lba, 1, (UINTPTR)src); if (ret ! XST_SUCCESS) { xil_printf(write lba %lu fail: %d\r\n, lba, ret); return -1; } return 0; }这里可能遇到另一个问题XSdPsu_Write 返回 XST_SUCCESS 时数据是否已经写入 eMMC 内部介质答案不是绝对的。驱动返回的完成状态一般表示数据已经进入控制器 FIFO 并完成命令握手但 eMMC 自己还有一层内部缓存和垃圾回收逻辑。如果需要严格保证掉电不丢数据还要在写完最后一块后检查 SD 控制器的状态位这在第 6 章会细说。4. eMMC HS400 模式时序调校与示波器实测4.1 速度档位决定了能用多少带宽eMMC 的速度模式不是随便选的控制器、MIO 电平、板级走线三者同时满足才能运行在 HS400。以下是一张常用档位对比表模式总线宽度时钟频率峰值带宽是否使用 DQS信号电压SDR 25/504/8 bit25/50 MHz50 MB/s否3.3V/1.8VDDR504/8 bit50 MHz DDR100 MB/s否1.8VHS2004/8 bit200 MHz200 MB/s否1.8VHS4008 bit200 MHz DDR400 MB/s是1.8V表中 HS200 到 HS400 带宽翻倍代价是信号线从 4 根变 8 根并且多出 DQS 差分采样信号。XCZU4EV 的 PS SD 控制器在硬件上支持这些模式但板级 PCB 走线等长、串阻端接和 MIO bank 电压决定了实际能跑稳定的是哪一档。4.2 HS400 模式下的时钟相位与采样窗调整HS400 是 200MHz 双沿采样DQS 作为读数据随路时钟。控制器的输出时钟相位和输入采样窗必须各自独立调整否则即使信号电气正常采样点也会落在数据翻转边缘上。调整的入口在 SD 控制器的 tuning 寄存器组。常见做法是先读取 ext_csd 中的 HS400 支持位确认 eMMC 支持 HS400 后再写 tuning 寄存器。代码片段如下#define SD_TUNING_CTRL_REG (EMMC_BASE 0x00000A40u) #define TUNING_CLK_PHASE_SEL_MASK 0x0000000Fu #define TUNING_CLK_PHASE_SEL_SHIFT 0 u32 reg *(volatile u32 *)SD_TUNING_CTRL_REG; /* 先把输出采样相位设为中间值再在示波器上微调 */ reg ~TUNING_CLK_PHASE_SEL_MASK; reg | 0x7u TUNING_CLK_PHASE_SEL_SHIFT; *(volatile u32 *)SD_TUNING_CTRL_REG reg;0x7 不是通用最优值。相位分度在不同控制器实现中不同实际项目中我会用一组循环把相位从 0 扫到最大值每档做一次读写校验记录通过范围再取中间值。这个流程比拍脑袋定一个数要可靠得多。提示tuning 寄存器偏移会因为 PS 版本和 Vitis 驱动版本不同而略有变化。更稳妥的方式是直接在驱动提供的结构体里找 TuningCtrl 对应字段而不是用固定地址。4.3 示波器实测 eMMC HS400 时序的选点与判断实测时探针不建议直接点在 eMMC 焊盘上而应该点靠近控制器引脚的测试点或过孔。测量 HS400 读时序时DQS 和 DATA 要同时测量示波器带宽至少 500MHz采样率设置到 5GS/s 以上。需要重点观察三组参数CLK 周期是否为 5ns占空比稳定度DQS 在数据包开始前的 preamble 长度是否正常DATA 信号相对 DQS 边沿的建立时间和保持时间是否满足 eMMC 芯片要求。如果 DQS 翻转位置明显偏离数据眼图中心就调节前面提到的相位值。每次调整后重新做读写校验而不是只看眼图因为控制器采样点可能和示波器观测点存在偏移。实测时常见的一个现象CLK 和 DQS 相位都正常但 DATA0 或 DATA7 比其他数据线慢半拍。这种情况多半是 PCB 等长设计没做好调相位只能缓解不能根除。可在驱动里把该数据线对应的采样延迟微调如果数值超过阈值就要回改板卡。5. eMMC 分区、掉电安全与 fstrim 的实际边界5.1 裸机读写也要区分 eMMC 内部区域eMMC 内部不是一块纯线性存储器而是分成 Boot1、Boot2、RPMB 和 User Data Area。Vitis 裸机场景下默认操作的是 UDA 区域地址从 0 开始前几个扇区是 GPT/MBR 信息直接覆盖会破坏后续启动数据。操作 Boot 分区要显式发送 partconf 相关命令。如果在 Vitis 里需要写 Boot1通常会先读 ext_csd 的 PARTITION_CONFIG 字段切到目标分区再按普通读写流程操作。切回来之后UDA 的 LBA 映射关系不受影响。分区选择上只需要记住一点把不需要频繁改写的程序和需要频繁读写的日志数据分开。这样在掉电测试时损坏范围会被限制在写入区域的物理块上程序分区依然可启动。5.2 fstrim 会作用到 eMMC 吗命令层面会效果看 FTLfstrim 是文件系统层向块设备发送 Discard 操作在 eMMC 上对应为 CMD38也就是 TRIM。Linux 里执行 fstrim 时ext4 会对每个空闲块组发起 Discard块设备驱动把它转换成 eMMC 的 TRIM 命令进而让 eMMC 内部的 FTL 标记这些 LBA 为无效。这个操作对 eMMC 的影响在不同使用阶段不一样对刚释放的文件区域做 TrimFTL 可以把对应物理块加入垃圾回收队列后续写入延迟降低Trim 本身不立即擦除物理块实际擦除由 eMMC 内部 GC 调度如果文件系统长期不执行 fstrimFTL 可能由于误判忙闲比例而做大量搬迁表现为写入放大上升。所以在 Linux 侧管理 eMMC 时fstrim 是有效的维护手段。前提是内核的 mmc 驱动支持 discard 并且 ext_csd 中 TRIM_MULT 字段非零。可以用下面的命令直接确认# 读取 eMMC 的 ext_csd查看 TRIM 与生命周期信息 mmc extcsd read /dev/mmcblk1 | grep -E TRIM|LIFE_TIME|ERASE_CNT # 对已挂载分区执行 fstrim观察实际回收到多少 fstrim -v /mnt/emmcTRIM_MULT 字段为 0 时fstrim 虽然发出 Discard但 eMMC 内部不会做有效回收。LIFE_TIME_EST 是设备健康度估算值可以粗略判断 GC 是否频繁。看过这些字段之后才能判断当前板卡上的 eMMC 是否适合依赖 fstrim 来延长寿命。5.3 U-Boot 里验证 eMMC 读写时要注意缓存问题U-Boot 的 mmc read 和 mmc write 命令经常被用来快速验证 eMMC。要注意 U-Boot 环境下同样存在 cache 一致性问题尤其在 A53 上跑 U-Boot 时读取前需要执行 dcache flush 或直接关闭 D-cache 来测试。常用的验证命令如下mmc dev 1 mmc read 0x10000000 0x800 0x1000 mmc write 0x10001000 0x800 0x10000x800 是起始 LBA0x1000 是扇区数对应 2MB 数据。读写后先对比源缓冲区和目的缓冲区内容确认 DMA 地址没有被 DDR 内存管理单元改映射。U-Boot 测试通过后再进入 Vitis 工程排查裸机代码可以缩小问题范围。6. 用连续回环校验把 eMMC 读写可靠性收口6.1 回环测试的代码结构完成基础读写后不要只测一次就收工。eMMC 的内部状态随写入量变化缓存失效、GC 触发、温度漂移都会导致偶发错误。我会在 Vitis 工程里加入一个连续回环覆盖三个维度写入模式变化、掉电恢复、扇区间隔。写回环测试代码时先在缓冲区里填入多组伪随机数连续写 100 次同一区域后再读回比对。然后用不同的 LBA 偏移重复重点检查跨分区边界的地址。以下是一个最小化的循环结构#define TEST_BUF_ADDR 0x10001000u #define TEST_BLOCKS 4096u int emmc_loop_test(void) { u32 *buf (u32 *)TEST_BUF_ADDR; u32 lba; s32 ret; for (lba 0x1000; lba 0x2000; lba TEST_BLOCKS) { /* 生成并写入再读回比对 */ ret emmc_write_sector(lba, buf); if (ret ! 0) return -1; ret emmc_read_sector(lba, buf); if (ret ! 0) return -2; } return 0; }这里的重点不是代码量而是地址范围。如果只测 LBA 0 附近读写缓存命中率高很多问题测不出来。把测试区域放宽到不同 bank并且交叉写入不同长度才更接近真实使用。6.2 掉电测试的关键动作掉电测试不是简单地按下电源键。正确顺序是先写入一段带校验和的数据等待写返回接着立即断电等待 5 秒再上电。重新上电后读取该区域并比较校验和。如果数据一致说明 eMMC 内部的写回策略能兜住这次掉电。如果数据不一致需要进一步确认是控制器没等到写完成还是 eMMC 断电时正在做内部写入。通过打印 SD 控制器的状态寄存器可以区分这两种情况。判断写是否真正完成的指标是控制器状态位中的 data transfer done 和 card busy 位。返回值 XST_SUCCESS 代表命令层完成不代表介质层完成。在严格场景下写完最后一块后还要轮询 read wait 和小块读返回直到确认介质不再忙。6.3 三个直接影响回环测试结果的经验参数DDR 缓冲区地址保持在 64KB 边界上减少 cache alias 出现的概率每次写前 flush每次读后 invalidate不要只在初始化时做一次测试时钟先固定在 50MHz SDR回环通过后再切到 HS200/HS400避免把时序和 cache 问题混在一起。实际项目里最常见的回环失败原因是 D-Cache 已开启而驱动的 DMA 描述符恰好落在 cache 行冲突区域。此时测试会发现固定偏移地址读写失败换一个缓冲区地址就正常。遇到这种现象先做 cache 对齐检查再回头调时序。用这套回环验证完再回到 Linux 下执行一次完整的分区读写并把 System本文还有配套的精品资源点击获取