前文对Xilinx Ultrascale+系列FPGA上PCIE4C集成块进行了基本使用,并实现了简单的DMA传输和中断模块,整体结构框图简化如下:
本文在前文代码的基础上进行拓展,实现一个简单的千兆网卡,整体结构简化如下:
本系列介绍如何利用FPGA实现一个基本的千兆网卡。FPGA使用Xilinx Ultrascale+ VCU128开发板,操作系统使用RHEL8.8(RedHat)。本文主要介绍除以太网子系统(Ethernet Subsystem)外其他部分的实现,共分为FPGA代码实现、驱动程序实现、上板测试三个部分。
FPGA代码实现
PCIe DMA模块
PCIE DMA模块使用PCIE4C集成块构建,在前文实现的PCIe DMA的基础上,增加了字节粒度读写和大规模DMA分包读写的功能。同时,DMA会将从PC读取的数据包循环写入TX Buffer,并将描述符(包起始地址,包长度)传递给下游模块,在PC需要时循环读取RX Buffer的数据。
由于网络数据包大小以字节为单位,因此需要在前文DMA以PCIe传输单位DW(4字节)的基础上进一步细化传输粒度。这一部分参考PCIE4C手册(https://docs.amd.com/r/zh-CN/pg213-pcie4-ultrascale-plus)。
字节粒度读较为简单,主要依靠m_axis_cc_tuser[15:0],其每1比特代表对应字节有效。
ram_senddma_we[dma_trans_function] <= m_axis_rc_tuser[15:0];
字节粒度写较为复杂,需要用户使用m_axis_cq_tuser中的first_be[3:0]和last_be[3:0]、以及m_axis_cq_tkeep协同指定。
always_comb begin
s_axis_rq_tuser = {
usr_hi_seq_num_r,
usr_parity_r,
usr_lo_seq_num_r,
usr_tph_st_tag_r,
usr_tph_indirect_tag_en_r,
usr_tph_type_r,
usr_tph_present_r,
usr_discontinue_r,
usr_addr_offset_r,
usr_last_be_r,
usr_first_be_r
};
end
if (dma_trans_transfer_len_iszero == 1'b1) begin
usr_first_be_r <= 4'b0000;
usr_last_be_r <= 4'b0000;
end else if (desc_mm_dword_count_r != (max_payload_size >> 2)) begin
if (desc_mm_dword_count_r == 1) begin
case (dma_trans_transfer_len_rest_byte)
2'b00: usr_first_be_r <= 4'b1111;
2'b01: usr_first_be_r <= 4'b0001;
2'b10: usr_first_be_r <= 4'b0011;
2'b11: usr_first_be_r <= 4'b0111;
endcase
usr_last_be_r <= 4'b0000;
end else begin
usr_first_be_r <= 4'b1111;
case (dma_trans_transfer_len_rest_byte)
2'b00: usr_last_be_r <= 4'b1111;
2'b01: usr_last_be_r <= 4'b0001;
2'b10: usr_last_be_r <= 4'b0011;
2'b11: usr_last_be_r <= 4'b0111;
endcase
end
end else begin
usr_first_be_r <= 4'b1111;
usr_last_be_r <= 4'b1111;
end
由于网络数据包(以以太网数据包为例)大小从96字节-1500字节大小不等,若考虑巨型帧可能会达到更大。因此需要在前文DMA的基础上增加DMA分包读写的支持。TLP包存在 最大读取请求大小 和 最大 TLP 有效载荷大小 的两个限制。一次TLP存储器读请求的长度不能超过最大读取请求大小,一次TLP返回的载荷大小不能超过最大 TLP 有效载荷大小。这意味着,当一次DMA读大小超过 最大读取请求大小 时,需要发送多个DMA读请求。此时考虑到 最大 TLP 有效载荷大小 的限制,主机可能发送多个响应TLP包,用户逻辑需要考虑对多个数据包的结果进行统一处理。当一次DMA写大小超过 最大 TLP 有效载荷大小 时,需要发送多个DMA写请求。支持分包读写的DMA状态机如下:
always_comb begin
case (fsm_r)
RESET: begin
fsm_s = IDLE;
end
IDLE: begin
if (dma_trans_valid) begin
if (dma_trans_mode) begin
fsm_s = DMA_SEND_START_PRE1; // write to pc memory
end else begin
fsm_s = DMA_RECV_START_PRE; // read from pc memory
end
end else begin
fsm_s = IDLE;
end
end
DMA_SEND_START_PRE1: begin
fsm_s = DMA_SEND_START_PRE2;
end
DMA_SEND_START_PRE2: begin
fsm_s = DMA_SEND_START_PRE3;
end
DMA_SEND_START_PRE3: begin
fsm_s = DMA_SEND_START;
end
DMA_SEND_START: begin // write to pc memory, each time less than cfg_max_payload
if (s_axis_rq_tready[0]) begin
fsm_s = DMA_SEND_MID;
end else begin
fsm_s = DMA_SEND_START;
end
end
DMA_SEND_MID: begin
if (s_axis_rq_tready[0] & s_axis_rq_tlast & s_axis_rq_tvalid) begin
if (total_times_cnt_r == times_cnt_r) begin
fsm_s = DMA_UPDATE_STATUS;
end else begin
fsm_s = DMA_SEND_START;
end
end else begin
fsm_s = DMA_SEND_MID;
end
end
DMA_RECV_START_PRE: begin
fsm_s = DMA_RECV_START;
end
DMA_RECV_START: begin // read from pc memory, each time less than cfg_max_read_req
if (s_axis_rq_tready[0]) begin
fsm_s = DMA_RECV_MID;
end else begin
fsm_s = DMA_RECV_START;
end
end
DMA_RECV_MID: begin // read from pc memory, each time less than cfg_max_payload
if (recv_rcb_fin_r) begin
if (recvdata_cur_cnt_r == {desc_mm_dword_count_rr, 2'b0}) begin
if (total_times_cnt_r == times_cnt_r) begin
fsm_s = DMA_UPDATE_STATUS;
end else begin
fsm_s = DMA_RECV_MIDSTART;
end
end else begin
fsm_s = DMA_RECV_MID;
end
end else begin
fsm_s = DMA_RECV_MID;
end
end
DMA_RECV_MIDSTART: begin // read from pc memory, each time less than cfg_max_read_req
if (s_axis_rq_tready[0]) begin
fsm_s = DMA_RECV_MID;
end else begin
fsm_s = DMA_RECV_MIDSTART;
end
end
DMA_UPDATE_STATUS: begin
fsm_s = IDLE;
end
default: fsm_s = RESET;
endcase
end
上述两个大小限制在系统上电后由RC和终端设备协商决定,可通过dmesg打印看到,作者系统支持256 字节最大有效载荷大小、512 字节最大读取请求大小。
PCIE4C集成块在 配置状态接口 部分使用cfg_max_read_req、cfg_max_payload两个指示信号告知用户逻辑协商结果。用户可以按照最小值固定进行,也可以上电后根据协商结果进行。
always_ff @(posedge clk) begin
case (cfg_max_payload)
2'b00: max_payload_size <= 11'd128; // Byte
2'b01: max_payload_size <= 11'd256;
2'b10: max_payload_size <= 11'd512;
2'b11: max_payload_size <= 11'd1024;
endcase
case (cfg_max_payload)
2'b00: max_payload_size_sub1 <= 10'd127;
2'b01: max_payload_size_sub1 <= 10'd255;
2'b10: max_payload_size_sub1 <= 10'd511;
2'b11: max_payload_size_sub1 <= 10'd1023;
endcase
case (cfg_max_payload)
2'b00: max_payload_size_log2 <= 4'd7;
2'b01: max_payload_size_log2 <= 4'd8;
2'b10: max_payload_size_log2 <= 4'd9;
2'b11: max_payload_size_log2 <= 4'd10;
endcase
end
always_ff @(posedge clk) begin
case (cfg_max_read_req)
3'b000: max_read_request_size <= 13'd128;
3'b001: max_read_request_size <= 13'd256;
3'b010: max_read_request_size <= 13'd512;
3'b011: max_read_request_size <= 13'd1024;
3'b100: max_read_request_size <= 13'd2048;
3'b101: max_read_request_size <= 13'd4096;
default: max_read_request_size <= 13'd128; // shouldn't occur
endcase
case (cfg_max_read_req)
3'b000: max_read_request_size_sub1 <= 13'd127;
3'b001: max_read_request_size_sub1 <= 13'd255;
3'b010: max_read_request_size_sub1 <= 13'd511;
3'b011: max_read_request_size_sub1 <= 13'd1023;
3'b100: max_read_request_size_sub1 <= 13'd2047;
3'b101: max_read_request_size_sub1 <= 13'd4095;
default: max_read_request_size_sub1 <= 13'd127; // shouldn't occur
endcase
case (cfg_max_read_req)
3'b000: max_read_request_size_log2 <= 13'd7;
3'b001: max_read_request_size_log2 <= 13'd8;
3'b010: max_read_request_size_log2 <= 13'd9;
3'b011: max_read_request_size_log2 <= 13'd10;
3'b100: max_read_request_size_log2 <= 13'd11;
3'b101: max_read_request_size_log2 <= 13'd12;
default: max_read_request_size_log2 <= 13'd7; // shouldn't occur
endcase
end
存储读写转数据流输入输出模块
这部分涉及MM2SS、SS2MM模块,MM2SS模块用于根据DMA模块传递的描述符将数据包从TX Buffer取出转为AXIS数据流形式传递给Ethernet Subsystem的s_axis端口(这里使用回环回传给SS2MM)。状态机如下:
always_comb begin
case (fsm_r)
RESET: begin
fsm_s = IDLE;
end
IDLE: begin
if (packet_tx_valid) begin
fsm_s = CONVERT_PRE4;
end else begin
fsm_s = IDLE;
end
end
CONVERT_PRE4: begin
fsm_s = CONVERT_PRE3;
end
CONVERT_PRE3: begin
fsm_s = CONVERT_PRE2;
end
CONVERT_PRE2: begin
fsm_s = CONVERT_PRE1;
end
CONVERT_PRE1: begin
fsm_s = CONVERT;
end
CONVERT: begin
if (m_axis_tvalid & m_axis_tready & m_axis_tlast) begin
fsm_s = IDLE;
end else begin
fsm_s = CONVERT;
end
end
default: fsm_s = RESET;
endcase
end
SS2MM模块用于将从Ethernet Subsystem的m_axis端口(这里使用回环接收MM2SS的数据流)的数据流存入RX Buffer,并将存好的数据流的描述符告知PC驱动程序。在驱动长时间未响应的情况下,认为主机暂时无法处理数据包允许丢包。模块引脚如下:
module ss2mm (
input logic clk,
input logic rst,
input logic [127:0] ram_mem_dout,
output logic [63:0] ram_mem_addr,
output logic [127:0] ram_mem_din,
output logic [15:0] ram_mem_we,
output logic ram_mem_en,
output logic [31:0] packet_rx_length,
output logic [63:0] packet_rx_baseaddr,
output logic packet_rx_valid,
// output logic [1:0] packet_rx_func,
input logic [127:0] s_axis_tdata,
input logic [15:0] s_axis_tkeep,
input logic s_axis_tlast,
input logic s_axis_tvalid,
output logic s_axis_tready
);
驱动程序实现
作者对驱动接触较少,代码不保证性能最佳。
初始化
网卡驱动程序由网络驱动相关函数和PCI驱动相关函数构成,初始化过程在前文PCI驱动的基础上,增加了注册网络设备的相关函数。此外使用了3个MSI中断用于表示FPGA的工作状态:DMA读完成(TXDMA)、DMA写完成(RXDMA)、FPGA收到新数据包(RX)。
网络驱动的核心在于net_device和net_device_ops结构体,这里例化为test_nic_dev和test_nic_dev_ops变量。net_device结构体用于定义网络适配器(网卡)的基本属性,如显示名称、队列数量、MTU、速率等。net_device_ops用于定义网卡收发包所涉及的发包收包函数。 alloc_etherdev_mqs用于按照创建以太网网络驱动的模板配置test_nic_dev结构体内容,由于本文设计的网卡功能简单,未使用net_device的网卡私有结构体priv_data,故第一个参数设为0。后面两个参数代表发送接收队列长度,这里设为1。netdev_ops必须定义不能为NULL,否则在注册网络设备时会死机图片,eth_hw_addr_random用于系统分配一个随机不冲突的MAC地址,netif_carrier_off用于关闭网卡,在注册网络设备(register_netdev)后会自动启动。
const struct net_device_ops test_nic_dev_ops = {
.ndo_init = test_nic_dev_init,
.ndo_open = test_nic_dev_open,
.ndo_stop = test_nic_dev_stop,
.ndo_start_xmit = test_nic_dev_start_xmit,
};
printk("allocating netdev");
test_nic_dev = alloc_etherdev_mqs(0, 1, 1);
if (test_nic_dev == NULL) {
printk("cannot allocate netdev");
goto test_nic_dev_alloc_err;
}
SET_NETDEV_DEV(test_nic_dev, &dev->dev);
printk("allocated netdev");
test_nic_dev->netdev_ops = &test_nic_dev_ops;
eth_hw_addr_random(test_nic_dev);
netif_carrier_off(test_nic_dev);
ret = register_netdev(test_nic_dev);
if (ret) {
printk("cannot register netdev");
goto register_netdev_err;
}
printk("registed netdev");
在执行完register_netdev后,可在系统中(设置、ifconfig)看到网卡,作者系统中网卡显示为Xilinx Ethernet(eth0)。
发包函数
发包函数为test_nic_dev_ops中定义的test_nic_dev_start_xmit,系统使用指定网卡发包时会主动调用该函数,并输入sk_buff结构体的数据包和net_device结构体的网卡实例。函数接收后会首先通过TXDMA中断是否变化判断上个数据包是否已由FPGA接收,若已接收则将新数据包的DMA描述符写给FPGA,由FPGA自行DMA接收并发送。
int test_nic_dev_start_xmit(struct sk_buff *skb, struct net_device *ndev) {
if (last_txdma_irq_cnt == txdma_irq_cnt) {
printk("last txpacket haven't finished process, drop tx packet");
++txdma_watchdogs_timer;
if (txdma_watchdogs_timer == DMA_TIMEOUT_HOUSHOLD) {
netif_carrier_off(ndev);
}
} else {
last_txdma_irq_cnt = txdma_irq_cnt;
txdma_watchdogs_timer = 0;
printk("test_nic_dev_start_xmit");
memcpy(dma_cpuregion_txaddr, skb->data, skb->len);
txdma_session = ioread32((u8*)bar32_dma + 0x000114) + 1;
iowrite32(next_tx_baseaddr & 0xffffffff, (u8*)bar32_dma + 0x000108);
iowrite32((next_tx_baseaddr >> 32) & 0xffffffff, (u8*)bar32_dma + 0x00010C);
iowrite32(skb->len, (u8*)bar32_dma + 0x000110);
iowrite32(txdma_session, (u8*)bar32_dma + 0x000114);
next_tx_baseaddr += (((skb->len + 15) >> 4) << 4);
printk("skb->len: %d", skb->len);
}
dev_kfree_skb(skb);
return 0;
}
收包函数
由于PCIe往返延迟最低也需要40us,这远远超过了CPU累加计数的处理时间,因此收包过程分为将数据包从FPGA搬到内存、从内存提交数据包两个过程。
收包过程通过中断进行,当FPGA收到新数据包时会向主机发送RX中断,驱动在RX中断处理程序中,RX中断处理程序会首先通过RXDMA中断是否变化判断上个数据包是否已接收完毕,若已接收则通过查询FPGA的dma_status寄存器得知新数据包的起始地址与长度,并将新数据包的DMA描述符写给FPGA,由FPGA DMA写入PC的指定地址。
void test_nic_rx_deal(struct net_device *ndev) {
__uint128_t dma_status;
if (last_rxdma_irq_cnt == rxdma_irq_cnt) {
printk("last rxpacket haven't finished process, drop rx packet");
++rxdma_watchdogs_timer;
if (rxdma_watchdogs_timer == DMA_TIMEOUT_HOUSHOLD) {
netif_carrier_off(ndev);
}
} else {
last_rxdma_irq_cnt = rxdma_irq_cnt;
rxdma_watchdogs_timer = 0;
dma_status = ((__uint128_t)ioread32((u8*)bar32_dma + 0x00000C) << 96) |
((__uint128_t)ioread32((u8*)bar32_dma + 0x000008) << 64) |
((__uint128_t)ioread32((u8*)bar32_dma + 0x000004) << 32) |
(__uint128_t)ioread32((u8*)bar32_dma + 0x000000);
printk("dma_status %x %x %x %x", (u32)(dma_status >> 96) & 0xffffffff, (u32)(dma_status >> 64) & 0xffffffff, (u32)(dma_status >> 32) & 0xffffffff, (u32)(dma_status >> 0) & 0xffffffff);
rxpacket_seqnum = (dma_status >> 16) & 0xffff;
rxpacket_length = (dma_status >> 32) & 0xffffffff;
rxpacket_baseaddr = (dma_status >> 64) & 0xffffffffffffffff;
rxdma_session = ioread32((u8*)bar32_dma + 0x000214) + 1;
iowrite32(rxpacket_baseaddr & 0xffffffff, (u8*)bar32_dma + 0x000208);
iowrite32((rxpacket_baseaddr >> 32) & 0xffffffff, (u8*)bar32_dma + 0x00020C);
iowrite32(rxpacket_length, (u8*)bar32_dma + 0x000210);
iowrite32(rxdma_session, (u8*)bar32_dma + 0x000214);
}
return;
}
FPGA发送完写入PC内存的数据包后,会发送RXDMA中断,驱动的RXDMA中断处理程序会将内存的数据包提交给系统,完成收包过程。
static irqreturn_t rxdma_irq_handler(int irq, void* dev) {
struct sk_buff *skb;
skb = dev_alloc_skb(rxpacket_length + 16);
printk("test_nic rxdma finish alloc");
if (!skb) {
printk("no enough rxbuf mem");
return IRQ_HANDLED;
}
skb->dev = test_nic_dev;
skb->protocol = eth_type_trans(skb, test_nic_dev);
skb->ip_summed = CHECKSUM_NONE;
memcpy(skb_put(skb, rxpacket_length), dma_cpuregion_rxaddr, rxpacket_length);
printk("test_nic rxdma commiting skb");
netif_rx(skb);
printk("commit rx packet");
++rxdma_irq_cnt;
printk("test_nic rxdma interrupt");
return IRQ_HANDLED;
}
上板测试
本文在FPGA侧搭建了一个简易的回环,Linux通过FPGA网卡发出的数据包会通过回环作为收到的数据包回传给Linux,wireshark测试结果如下。
参考链接
1. www.oreilly.com
2. docs.kernel.org
3. corundum
4. docs.kernel.org
完整代码
完整代码可于同名公众号回复NIC_1000下载。