基于FPGA的千兆网卡实现(一)——回环测试

  前文对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
  • DMA分包读写实现

  由于网络数据包(以以太网数据包为例)大小从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下载。

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wjh776a68

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值