本章节讲述如何使用ZYNQ(纯C语言)实现NVME SSD硬盘的读写控制。
参考:
二、实战篇-NVME SSD控制之ZYNQ实现(一)-CSDN博客
二、实战篇-NVME SSD控制之ZYNQ实现(二)-CSDN博客
二、实战篇-NVME SSD控制之ZYNQ实现(三)-CSDN博客
二、实战篇-NVME SSD控制之ZYNQ实现(四)-CSDN博客
二、实战篇-NVME SSD控制之ZYNQ实现(五)-CSDN博客

前面完成了PCIE的建链和枚举、FPGA和NVME SSD的PCIE CFG空间的配置以及NVME SSD的寄存器配置。创建提交队列和完成队列,完成NvmeIdentifyCtrl信息读取,以及ADMIN命令创建IO提交和完成队列。本节进行NVME SSD硬盘数据读写
NVM命令集(NVM Command Set)


先定义操作码
cpp
#define NVME_CMD_WRITE 0x01
#define NVME_CMD_READ 0x02
写数据和读数据的门铃寄存器和admin不同
cpp
#define RegOffset_IO_SQ0TDBL 0x1008
#define RegOffset_IO_CQ0HDBL 0x100c
#define RegOffset_IO_SQ1TDBL 0x1010
#define RegOffset_IO_CQ1HDBL 0x1014
写数据、读数据提交队列函数,基本和ADMIN相同,只有写门铃寄存器位置不同
cpp
// 提交命令到 IOWR SQ
void NvmeSubmitIOWRCmd(NvmeSqe *sqe)
{
// 新增:刷新栈上临时SQE
Xil_DCacheFlushRange(sqe, sizeof(NvmeSqe));
IowrSq[Iowr_SqTail] = *sqe;
Xil_DCacheFlushRange((void*)&IowrSq[Iowr_SqTail], sizeof(NvmeSqe));
Iowr_SqTail = (Iowr_SqTail + 1) % IO_Q_DEPTH;
// 写门铃:SQ Tail Doorbell (偏移 0x1000 + 4*SQID)
XAxiPcie_WriteReg(XPAR_AXI_PCIE_0_AXIBAR_0, RegOffset_IO_SQ0TDBL, Iowr_SqTail) ;
}
// 提交命令到 IORD SQ
void NvmeSubmitIORDCmd(NvmeSqe *sqe)
{
// 新增:刷新栈上临时SQE
Xil_DCacheFlushRange(sqe, sizeof(NvmeSqe));
IordSq[Iord_SqTail] = *sqe;
Xil_DCacheFlushRange((void*)&IordSq[Iord_SqTail], sizeof(NvmeSqe));
Iord_SqTail = (Iord_SqTail + 1) % IO_Q_DEPTH;
// 写门铃:SQ Tail Doorbell (偏移 0x1000 + 4*SQID)
XAxiPcie_WriteReg(XPAR_AXI_PCIE_0_AXIBAR_0, RegOffset_IO_SQ1TDBL, Iord_SqTail) ;
}
写数据、读数据完成队列函数
cpp
// 轮询 IOWR CQE,等待命令完成
int NvmePollIOWRCqe(NvmeCqe *cqe)
{
int timeout = 2000;
while(timeout--)
{
// 失效缓存,读取硬件DMA更新后的CQ内存
Xil_DCacheInvalidateRange((void*)&IowrCq[Iowr_CqHead], sizeof(NvmeCqe));
*cqe = IowrCq[Iowr_CqHead];
u8 curr_cqe_pha = cqe->status & 0x1;
// 条件1:当前CQE Phase 和软件缓存的全局Phase不等 → 有效新条目
if (curr_cqe_pha != IoWrCqLastPha)
{
// 读取成功,Head前进一格
Iowr_CqHead = (Iowr_CqHead + 1) % IO_Q_DEPTH;
// 关键:判断是否走完一整圈队列,下一轮硬件Phase会翻转
// 走完一圈后,下一条CQE的Phase必然取反,提前更新软件缓存
if (Iowr_CqHead == 0)
{
IoWrCqLastPha ^= 1;
}
// 更新CQ门铃,通知SSD主机已消费当前CQE
XAxiPcie_WriteReg(XPAR_AXI_PCIE_0_AXIBAR_0, RegOffset_IO_CQ0HDBL, Iowr_CqHead);
return 0;
}
// 条件2:curr_cqe_pha = IoWrCqLastPha → 无新完成条目,继续等待
usleep(500);
}
return -1; // 超时无CQE
}
// 轮询 IORD CQE,等待命令完成
int NvmePollIORDCqe(NvmeCqe *cqe)
{
int timeout = 2000;
while(timeout--)
{
// 失效缓存,读取硬件DMA更新后的CQ内存
Xil_DCacheInvalidateRange((void*)&IordCq[Iord_CqHead], sizeof(NvmeCqe));
*cqe = IordCq[Iord_CqHead];
u8 curr_cqe_pha = cqe->status & 0x1;
// 条件1:当前CQE Phase 和软件缓存的全局Phase不等 → 有效新条目
if (curr_cqe_pha != IoRdCqLastPha)
{
// 读取成功,Head前进一格
Iord_CqHead = (Iord_CqHead + 1) % IO_Q_DEPTH;
// 关键:判断是否走完一整圈队列,下一轮硬件Phase会翻转
// 走完一圈后,下一条CQE的Phase必然取反,提前更新软件缓存
if (Iord_CqHead == 0)
{
IoRdCqLastPha ^= 1;
}
// 更新CQ门铃,通知SSD主机已消费当前CQE
XAxiPcie_WriteReg(XPAR_AXI_PCIE_0_AXIBAR_0, RegOffset_IO_CQ1HDBL, Iord_CqHead);
return 0;
}
// 条件2:curr_cqe_pha = IoWrCqLastPha → 无新完成条目,继续等待
usleep(500);
}
return -1; // 超时无CQE
}
提交和完成队列函数写好后,下面在读数据和写数据函数中调用
写数据函数
cpp
// NVMe 写命令
int NvmeWrite(u32 nsid, u64 lba, u16 nlb, void *buf)
{
NvmeSqe sqe = {0};
NvmeCqe cqe;
sqe.cdw0 = NVME_CMD_WRITE;
sqe.nsid = nsid;
sqe.dptr[0] = (u64)buf;
sqe.dptr[1] = 0;
sqe.cdw10 = (u32)lba;
sqe.cdw11 = (u32)(lba >> 32);
sqe.cdw12 = (nlb - 1) & 0xFFFF;
Xil_DCacheFlushRange(buf, 4096);
NvmeSubmitIOWRCmd(&sqe);
usleep(2000);
if(NvmePollIOWRCqe(&cqe) != 0)
{
xil_printf("NvmeWrite 4. CQE轮询超时,函数退出\r\n");
return -1;
}
xil_printf("NvmeWrite 5. 成功读到CQE\r\n");
u16 real_status = (cqe.status >> 1) & 0x7FFF;
if(real_status != 0)
{
xil_printf("NvmeWrite failed, status=0x%04X\r\n", real_status);
return -1;
}
xil_printf("NvmeWrite Done\r\n");
return 0;
}
读数据函数
cpp
// NVMe 读命令
int NvmeRead(u32 nsid, u64 lba, u16 nlb, void *buf)
{
NvmeSqe sqe = {0};
NvmeCqe cqe;
sqe.cdw0 = NVME_CMD_READ;
sqe.nsid = nsid;
sqe.dptr[0] = (u64)buf;
sqe.dptr[1] = 0;
sqe.cdw10 = (u32)lba;
sqe.cdw11 = (u32)(lba >> 32);
sqe.cdw12 = (nlb - 1) & 0xFFFF;
NvmeSubmitIORDCmd(&sqe);
usleep(2000);
if(NvmePollIORDCqe(&cqe) != 0)
{
xil_printf("NvmeRead 4. CQE轮询超时,函数退出\r\n");
return -1;
}
xil_printf("NvmeRead 5. 成功读到CQE\r\n");
u16 real_status = (cqe.status >> 1) & 0x7FFF;
if(real_status != 0)
{
xil_printf("NvmeRead failed, status=0x%04X\r\n", real_status);
return -1;
}
xil_printf("NvmeRead Done\r\n");
return 0;
}
先定义读数据和写数据的地址空间
cpp
// 数据缓冲区(和队列彻底分开)
#define WRITE_DATA_BUF 0x20000000
#define READ_DATA_BUF 0x28000000
在main函数中调用如下
cpp
u8 *w_buf = (u8 *)WRITE_DATA_BUF;
u8 *read_buf = (u8 *)READ_DATA_BUF;
// 填充测试数据
memset(w_buf, 0x5A, 4096);
ret = NvmeWrite(1, 0, 1, w_buf);
if(ret != 0) goto err_end;
//read
if(NvmeRead(1, 0, 1, read_buf) == 0)
{
// 读取成功,打印前若干字节验证
for(int i = 0; i < 16; i++)
{
xil_printf("0x%02X ", read_buf[i]);
}
xil_printf("\r\n");
}
else
{
xil_printf("Read failed\r\n");
}
本次读写空间为一个LB,512字节
先将数据0x5A写入NVME SSD,再读取打印前16个数据验证是否为0x5A。