Zephyr AXI Ethernet Driver Implementation

This page gives an overview of the AXI Ethernet Zephyr driver which is available as part of the zephyr-amd repo.

Overview

The Xilinx AXI 1G/2.5G Ethernet Subsystem driver (eth_xilinx_axienet.c) provides network connectivity for Xilinx FPGA-based systems. The driver implements the Zephyr Ethernet API and integrates with the AXI DMA controller for high-performance packet transfer.

Driver Compatibility: xlnx,axi-ethernet-1.00.a

Architecture Overview

Component Integration

┌─────────────────────────────────────────────────────────┐
│           Zephyr Network Stack (L2/L3)                  │
└────────────────┬────────────────────────────────────────┘
                 │ ethernet_api
┌────────────────▼────────────────────────────────────────┐
│      eth_xilinx_axienet.c (This Driver)                 │
│  ┌──────────────┐  ┌────────────────┐  ┌─────────────┐ │
│  │ TX Path      │  │ RX Path        │  │ Control     │ │
│  │ (DMA-based)  │  │ (DMA-based)    │  │ & Config    │ │
│  └──────┬───────┘  └────────┬───────┘  └──────┬──────┘ │
└─────────┼──────────────────┼──────────────────┼────────┘
          │                  │                  │
┌─────────▼──────────────────▼──────────────────▼────────┐
│        Xilinx AXI DMA Driver (dma_xilinx_axi_dma.h)    │
└────────────────┬───────────────────────────────────────┘
                 │ AXI Stream Interface
┌────────────────▼───────────────────────────────────────┐
│      AXI Ethernet Controller (Hardware)                │
│                     ┌─────────────┐                     │
│                     │  PHY Layer  │                     │
└─────────────────────┴─────────────┴─────────────────────┘

Data Structures

Device Data (struct xilinx_axienet_data)

Runtime state maintained per Ethernet instance:

struct xilinx_axienet_data {
    struct xilinx_axienet_buffer tx_buffer[CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_TX];
    struct xilinx_axienet_buffer rx_buffer[CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_RX];
    size_t rx_populated_buffer_index;   // Next buffer to populate for RX
    size_t rx_completed_buffer_index;   // Next buffer to process after RX completion
    size_t tx_populated_buffer_index;   // Next buffer to populate for TX
    size_t tx_completed_buffer_index;   // Next buffer to free after TX completion
    struct net_if *interface;           // Network interface handle
    uint8_t mac_addr[NET_ETH_ADDR_LEN]; // Device MAC address
    bool dma_is_configured_rx;          // RX DMA initialization flag
    bool dma_is_configured_tx;          // TX DMA initialization flag
};

Device Configuration (struct xilinx_axienet_config)

Compile-time configuration per device instance:

struct xilinx_axienet_config {
    void (*config_func)(const struct xilinx_axienet_data *dev);
    const struct device *dma;           // AXI DMA device reference
    const struct device *phy;           // PHY device reference
    mem_addr_t reg;                     // Base register address
    int irq_num;                        // Interrupt number
    bool have_irq;                      // Interrupt support flag
    bool have_rx_csum_offload;          // RX checksum offload capability
    bool have_tx_csum_offload;          // TX checksum offload capability
    bool have_random_mac;               // Generate random MAC address
};

Buffer Structure (struct xilinx_axienet_buffer)

Cache-aligned DMA buffers:

struct xilinx_axienet_buffer {
    uint8_t buffer[XILINX_AXIENET_ETH_BUFFER_SIZE];
} __aligned(XILINX_AXIENET_ETH_ALIGN);
Alignment: CONFIG_DCACHE_LINE_SIZE (if D-cache enabled) or sizeof(void*) otherwise
Size: NET_ETH_MAX_FRAME_SIZE rounded up to alignment boundary

Initialization Flow

#. Device Probe (xilinx_axienet_probe)

Called during system initialization:

  1. Disable RX - Clear XILINX_AXIENET_RECEIVER_CONFIGURATION_WORD_1_REG_RX_EN_MASK

  2. Clear pending interrupts - Write to XILINX_AXIENET_INTERRUPT_STATUS_OFFSET

  3. Configure interrupts - Set XILINX_AXIENET_INTERRUPT_ENABLE_RXREJ_MASK and XILINX_AXIENET_INTERRUPT_ENABLE_OVR_MASK

  4. Enable flow control - Set XILINX_AXIENET_RECEIVER_CONFIGURATION_FLOW_CONTROL_EN_MASK

  5. Set MAC address - Call xilinx_axienet_set_mac_address() (random or from DT)

  6. Pre-populate RX buffers - Call setup_dma_rx_transfer() for (CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_RX - 1) buffers

  7. Enable RX - Set XILINX_AXIENET_RECEIVER_CONFIGURATION_WORD_1_REG_RX_EN_MASK

  8. Enable TX - Set XILINX_AXIENET_TX_CONTROL_TX_EN_MASK

  9. Configure IRQ handler - Call device-specific config_func()

#. Interface Initialization (xilinx_axienet_iface_init)

Called when the network interface is created:

  1. Store interface handle in data->interface

  2. Call ethernet_init(iface) to register with L2

  3. Set link address through net_if_set_link_addr()

  4. Mark carrier off through net_eth_carrier_off()

  5. Register PHY callback through phy_link_callback_set() with phy_link_state_changed


Transmit (TX) Path

API Entry Point: xilinx_axienet_send()

static int xilinx_axienet_send(const struct device *dev, struct net_pkt *pkt)

Flow:

  1. Get current TX buffer using data->tx_populated_buffer_index

  2. Copy packet data using net_pkt_read() into data->tx_buffer[current_descriptor].buffer

  3. Call setup_dma_tx_transfer() to initiate DMA transfer

DMA Configuration: setup_dma_tx_transfer()

First Transfer (Initial Configuration):

struct dma_block_config head_block = {
    .source_address = (uintptr_t)data->tx_buffer[current_descriptor].buffer,
    .dest_address = 0x0,  // AXI Stream (no memory destination)
    .block_size = buffer_len,
    .source_addr_adj = DMA_ADDR_ADJ_INCREMENT,
    .dest_addr_adj = DMA_ADDR_ADJ_INCREMENT
};
struct dma_config dma_conf = {
    .channel_direction = MEMORY_TO_PERIPHERAL,
    .complete_callback_en = 1,
    .dma_callback = xilinx_axienet_tx_callback,
    .linked_channel = XILINX_AXI_DMA_LINKED_CHANNEL_*_CSUM_OFFLOAD
};
dma_config(config->dma, XILINX_AXI_DMA_TX_CHANNEL_NUM, &dma_conf);

Subsequent Transfers (Fast Path):

dma_reload(config->dma, XILINX_AXI_DMA_TX_CHANNEL_NUM,
          (uintptr_t)data->tx_buffer[current_descriptor].buffer,
          0x0, buffer_len);

Start Transfer:

dma_start(config->dma, XILINX_AXI_DMA_TX_CHANNEL_NUM);

Buffer Management:

  • Increment data->tx_populated_buffer_index (modulo CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_TX)

  • Check for buffer exhaustion: next_descriptor == data->tx_completed_buffer_index → return -ENOSPC

TX Completion: xilinx_axienet_tx_callback()

DMA driver calls this on transmission complete:

  1. Increment data->tx_completed_buffer_index (modulo CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_TX)

  2. Update error statistics if status < 0 through eth_stats_update_errors_tx()


Receive (RX) Path

DMA Configuration: setup_dma_rx_transfer()

Pre-populates RX buffers for incoming packets.

First Transfer (Initial Configuration):

struct dma_block_config head_block = {
    .source_address = 0x0,  // AXI Stream (no memory source)
    .dest_address = (uintptr_t)data->rx_buffer[current_descriptor].buffer,
    .block_size = sizeof(data->rx_buffer[current_descriptor].buffer),
    .source_addr_adj = DMA_ADDR_ADJ_INCREMENT,
    .dest_addr_adj = DMA_ADDR_ADJ_INCREMENT
};
struct dma_config dma_conf = {
    .channel_direction = PERIPHERAL_TO_MEMORY,
    .complete_callback_en = 1,
    .dma_callback = xilinx_axienet_rx_callback,
    .linked_channel = XILINX_AXI_DMA_LINKED_CHANNEL_*_CSUM_OFFLOAD
};
dma_config(config->dma, XILINX_AXI_DMA_RX_CHANNEL_NUM, &dma_conf);

Subsequent Transfers (Fast Path):

dma_reload(config->dma, XILINX_AXI_DMA_RX_CHANNEL_NUM, 0x0,
          (uintptr_t)data->rx_buffer[current_descriptor].buffer,
          sizeof(data->rx_buffer[current_descriptor].buffer));

RX Completion: xilinx_axienet_rx_callback()

DMA driver calls this when packet is received:

Flow:

  1. Validate interface state - Check net_if_is_up(data->interface)

  2. Handle errors - If status < 0, update eth_stats_update_errors_rx() and skip to setup new transfer

  3. Increment buffer index - data->rx_completed_buffer_index = next_descriptor

  4. Get packet size - Call dma_xilinx_axi_dma_last_received_frame_length(dma)

  5. Allocate net packet - net_pkt_rx_alloc_with_buffer(data->interface, packet_size, ...)

  6. Copy data - net_pkt_write(pkt, data->rx_buffer[current_descriptor].buffer, packet_size)

  7. Deliver to stack - net_recv_data(data->interface, pkt)

  8. Setup next transfer - Call setup_dma_rx_transfer() to replenish buffer

Buffer Management:

  • Increment data->rx_completed_buffer_index (modulo CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_RX)

  • Check for buffer exhaustion: next_descriptor == data->rx_completed_buffer_index → return -ENOSPC


Interrupt Handling

IRQ Handler: xilinx_axienet_isr()

Handles Ethernet controller interrupts (not DMA interrupts):

Monitored Events:

  • RX FIFO Overrun - XILINX_AXIENET_INTERRUPT_PENDING_RXFIFOOVR_MASK

    • Action: Log warning, call eth_stats_update_errors_rx()

  • RX Frame Rejected - XILINX_AXIENET_INTERRUPT_PENDING_RXRJECT_MASK

    • Action: Log warning, call eth_stats_update_errors_rx()

Interrupt Clearance:

status = xilinx_axienet_read_register(config, XILINX_AXIENET_INTERRUPT_PENDING_OFFSET);
xilinx_axienet_write_register(config, XILINX_AXIENET_INTERRUPT_STATUS_OFFSET, status);

PHY Integration

Configuration Management

Capabilities: xilinx_axienet_caps()

Reports hardware capabilities to network stack:

Base Capabilities:

  • ETHERNET_LINK_10BASE

  • ETHERNET_LINK_100BASE

  • ETHERNET_LINK_1000BASE

Conditional Capabilities:

  • ETHERNET_HW_RX_CHKSUM_OFFLOAD - If config->have_rx_csum_offload

  • ETHERNET_HW_TX_CHKSUM_OFFLOAD - If config->have_tx_csum_offload

Get Configuration: xilinx_axienet_get_config()

Supported Configuration Types:

ETHERNET_CONFIG_TYPE_RX_CHECKSUM_SUPPORT:

config->chksum_support = ETHERNET_CHECKSUM_SUPPORT_IPV4_HEADER |
                         ETHERNET_CHECKSUM_SUPPORT_TCP |
                         ETHERNET_CHECKSUM_SUPPORT_UDP |
                         ETHERNET_CHECKSUM_SUPPORT_IPV6_HEADER;

ETHERNET_CONFIG_TYPE_TX_CHECKSUM_SUPPORT:

config->chksum_support = ETHERNET_CHECKSUM_SUPPORT_IPV4_HEADER |
                         ETHERNET_CHECKSUM_SUPPORT_TCP |
                         ETHERNET_CHECKSUM_SUPPORT_UDP |
                         ETHERNET_CHECKSUM_SUPPORT_IPV6_HEADER;

Set Configuration: xilinx_axienet_set_config()

Supported Configuration Types:

ETHERNET_CONFIG_TYPE_MAC_ADDRESS:

memcpy(data->mac_addr, config->mac_address.addr, sizeof(data->mac_addr));
xilinx_axienet_set_mac_address(dev_config, data);
net_if_set_link_addr(data->interface, data->mac_addr, ...);

MAC Address Programming: xilinx_axienet_set_mac_address()

Writes MAC address to hardware registers:

xilinx_axienet_write_register(config, XILINX_AXIENET_UNICAST_ADDRESS_WORD_0_OFFSET,
    (data->mac_addr[0]) | (data->mac_addr[1] << 8) |
    (data->mac_addr[2] << 16) | (data->mac_addr[3] << 24));
xilinx_axienet_write_register(config, XILINX_AXIENET_UNICAST_ADDRESS_WORD_1_OFFSET,
    (data->mac_addr[4]) | (data->mac_addr[5] << 8));

MAC Address Source:

  • From device tree (local-mac-address property)

  • Random generation using gen_random_mac() with Xilinx OUI (00:0A:35:xx:xx:xx)


Register Access

Helper Functions

Write Register:

static void xilinx_axienet_write_register(const struct xilinx_axienet_config *config,
                                          mem_addr_t reg_offset, uint32_t value)
{
    sys_write32(value, config->reg + reg_offset);
}

Read Register:

static uint32_t xilinx_axienet_read_register(const struct xilinx_axienet_config *config,
                                             mem_addr_t reg_offset)
{
    return sys_read32(config->reg + reg_offset);
}

Key Registers Used

Offset

Name

Purpose

0x0C

INTERRUPT_STATUS

Clear interrupts (write-1-to-clear)

0x10

INTERRUPT_PENDING

Read pending interrupt flags

0x14

INTERRUPT_ENABLE

Enable/disable interrupts

0x400

RX_CONFIG_WORD_0

RX configuration

0x404

RX_CONFIG_WORD_1

RX enable control

0x408

TX_CONTROL

TX enable control

0x40C

RX_FLOW_CONTROL

Flow control configuration

0x700

UNICAST_ADDR_WORD_0

MAC address bytes 0-3

0x704

UNICAST_ADDR_WORD_1

MAC address bytes 4-5


Ethernet API Implementation

API Structure: xilinx_axienet_api

static const struct ethernet_api xilinx_axienet_api = {
    .iface_api.init = xilinx_axienet_iface_init,
    .get_capabilities = xilinx_axienet_caps,
    .get_config = xilinx_axienet_get_config,
    .set_config = xilinx_axienet_set_config,
    .get_phy = xilinx_axienet_get_phy,
    .send = xilinx_axienet_send,
};

Device Tree Binding

Instantiation Macro: XILINX_AXIENET_INIT(inst)

Creates device instances from the device tree:

Configuration Extraction:

.dma = DEVICE_DT_GET(DT_INST_PHANDLE(inst, axistream_connected))
.phy = DEVICE_DT_GET(DT_INST_PHANDLE(inst, phy_handle))
.reg = DT_REG_ADDR(DT_INST_PARENT(inst))
.have_tx_csum_offload = DT_INST_PROP_OR(inst, xlnx_txcsum, 0x0) == 0x2
.have_rx_csum_offload = DT_INST_PROP_OR(inst, xlnx_rxcsum, 0x0) == 0x2
.have_random_mac = DT_INST_PROP(inst, zephyr_random_mac_address)

Device Registration:

ETH_NET_DEVICE_DT_INST_DEFINE(inst, xilinx_axienet_probe, NULL,
                              &data_##inst, &config_##inst,
                              CONFIG_ETH_INIT_PRIORITY,
                              &xilinx_axienet_api, NET_ETH_MTU);

Configuration Options (Kconfig)

CONFIG_ETH_XILINX_AXIENET

  • Type: Boolean

  • Default: y (if DT enabled)

  • Dependencies: DT_HAS_XLNX_AXI_ETHERNET_1_00_A_ENABLED, DMA_XILINX_AXI_DMA

CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_TX

  • Type: Integer

  • Default: DMA_XILINX_AXI_DMA_SG_DESCRIPTOR_NUM_TX

  • Range: 2 to DMA_XILINX_AXI_DMA_SG_DESCRIPTOR_NUM_TX

  • Purpose: Number of concurrent TX buffers (ring buffer depth)

CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_RX

  • Type: Integer

  • Default: DMA_XILINX_AXI_DMA_SG_DESCRIPTOR_NUM_RX

  • Range: 2 to DMA_XILINX_AXI_DMA_SG_DESCRIPTOR_NUM_RX

  • Purpose: Number of concurrent RX buffers (ring buffer depth)


Buffer Management Strategy

Ring Buffer Architecture

Both the TX and RX use circular buffer management with two pointers:

TX Buffers:

  • tx_populated_buffer_index - Next buffer to fill with outgoing data

  • tx_completed_buffer_index - Next buffer freed by DMA completion

RX Buffers:

  • rx_populated_buffer_index - Next buffer submitted to DMA for reception

  • rx_completed_buffer_index - Next buffer to process after DMA fills it

Full Condition Detection

Buffers are considered full when:

next_descriptor == data->xx_completed_buffer_index

This reserves one buffer slot to distinguish the full from the empty condition.

Cache Coherency

Buffers are cache-line aligned (XILINX_AXIENET_ETH_ALIGN) to enable selective cache invalidation on the RX, proper cache flushing on the TX and avoiding false sharing between buffers


DMA Integration

Channel Assignment

  • TX Channel: XILINX_AXI_DMA_TX_CHANNEL_NUM

  • RX Channel: XILINX_AXI_DMA_RX_CHANNEL_NUM

Checksum Offload Configuration

With Offload:

dma_conf.linked_channel = XILINX_AXI_DMA_LINKED_CHANNEL_FULL_CSUM_OFFLOAD;

Without Offload:

dma_conf.linked_channel = XILINX_AXI_DMA_LINKED_CHANNEL_NO_CSUM_OFFLOAD;

DMA API Usage

Initial Configuration: dma_config(dma, channel, &dma_conf)
Reconfiguration: dma_reload(dma, channel, src, dst, size) (faster)
Start Transfer: dma_start(dma, channel)
Get RX Length: dma_xilinx_axi_dma_last_received_frame_length(dma)

Error Handling

RX Errors

  • DMA Error: Log error, update eth_stats_update_errors_rx(), replenish buffer

  • FIFO Overrun: ISR logs warning, updates error stats

  • Frame Rejected: ISR logs warning, updates error stats

  • Buffer Allocation Failure: Log error, discard packet, replenish buffer

  • Packet Write Failure: Log error, free packet, replenish buffer

  • Stack Delivery Failure: Log error, free packet, replenish buffer

TX Errors

  • DMA Error: Log error, update eth_stats_update_errors_tx()

  • Buffer Exhaustion: Return -ENOSPC to caller

  • Packet Read Failure: Return -EIO to caller

Recovery Strategy

All RX errors trigger automatic buffer replenishment through setup_dma_rx_transfer() to make sure continuous reception capability.


Performance Considerations

Fast Path Optimizations

  1. DMA Reload API - After first transfer, use dma_reload() instead of full dma_config()

  2. Pre-populated RX Buffers - (CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_RX - 1) buffers ready before first packet

  3. Ring Buffer Management - Modulo arithmetic for constant-time index updates

  4. Cache-line Alignment - Minimize cache operations on buffer access

Bottleneck Analysis

Potential Bottlenecks:

  • Buffer copying in net_pkt_read()/net_pkt_write() (no zero-copy)

  • Limited by CONFIG_ETH_XILINX_AXIENET_BUFFER_NUM_RX/TX depth

  • Single-packet DMA transfers (no scatter-gather chaining in this implementation)


Known Limitations

  1. No VLAN support - Tagged frames not handled

  2. No PTP/timestamping - IEEE 1588 not implemented

  3. No multicast filtering - All multicast accepted or rejected

  4. No promiscuous mode - Cannot monitor all traffic

  5. No jumbo frames - Limited to NET_ETH_MAX_FRAME_SIZE

  6. No hardware statistics - Relies on software counters only

  7. Single unicast address - No MAC address table


Future Enhancement Opportunities

These potential enhancements are based on hardware capabilities not yet used:

  1. Implement VLAN tagging/filtering using appropriate registers

  2. Add PTP support with timestamp registers

  3. Expose hardware statistics counters

  4. Implement multicast address table management

  5. Add promiscuous mode configuration

  6. Support jumbo frame configuration

  7. Implement partial checksum offload modes

  8. Add scatter-gather DMA descriptor chains for efficiency