diff --git a/drivers/ethernet/CMakeLists.txt b/drivers/ethernet/CMakeLists.txt index 12b9142850b33..32314277300b5 100644 --- a/drivers/ethernet/CMakeLists.txt +++ b/drivers/ethernet/CMakeLists.txt @@ -45,6 +45,7 @@ zephyr_library_sources_ifdef(CONFIG_ETH_SY1XX eth_sensry_sy1xx_mac.c) zephyr_library_sources_ifdef(CONFIG_ETH_NXP_ENET eth_nxp_enet.c) zephyr_library_sources_ifdef(CONFIG_ETH_XILINX_AXIENET eth_xilinx_axienet.c) zephyr_library_sources_ifdef(CONFIG_ETH_VIRTIO_NET eth_virtio_net.c) +zephyr_library_sources_ifdef(CONFIG_ETH_XILINX_AXI_ETHERNET_LITE eth_xilinx_axi_ethernet_lite.c) if(CONFIG_ETH_NXP_S32_NETC) zephyr_library_sources(eth_nxp_s32_netc.c) diff --git a/drivers/ethernet/Kconfig b/drivers/ethernet/Kconfig index 15913fd1a8ba7..4b06e516bc433 100644 --- a/drivers/ethernet/Kconfig +++ b/drivers/ethernet/Kconfig @@ -78,6 +78,7 @@ source "drivers/ethernet/Kconfig.lan9250" source "drivers/ethernet/Kconfig.sy1xx_mac" source "drivers/ethernet/Kconfig.xilinx_axienet" source "drivers/ethernet/Kconfig.virtio_net" +source "drivers/ethernet/Kconfig.xilinx_axi_ethernet_lite" source "drivers/ethernet/eth_nxp_enet_qos/Kconfig" diff --git a/drivers/ethernet/Kconfig.xilinx_axi_ethernet_lite b/drivers/ethernet/Kconfig.xilinx_axi_ethernet_lite new file mode 100644 index 0000000000000..52e5c8be86cb3 --- /dev/null +++ b/drivers/ethernet/Kconfig.xilinx_axi_ethernet_lite @@ -0,0 +1,24 @@ +# +#Xilinx AXI Ethernet Lite +# +#Copyright(c) 2025, CISPA Helmholtz Center for Information Security +#SPDX-License-Identifier: Apache-2.0 +# + +menuconfig ETH_XILINX_AXI_ETHERNET_LITE + bool "Xilinx AXI Ethernet Lite Driver" + default y + depends on DT_HAS_XLNX_XPS_ETHERNETLITE_3_00_A_MAC_ENABLED + depends on LITTLE_ENDIAN # byte order operations might break for BE + help + Enable Xilinx AXI Ethernet Lite MAC driver, commonly found on Xilinx FPGAs and SoCs. + +config ETH_XILINX_AXI_ETHERNET_LITE_TIMER_PERIOD + int "Xilinx AXI Ethernet Lite Timer Period in ms" + default 1 + depends on ETH_XILINX_AXI_ETHERNET_LITE + help + Period for periodic RX timer task in AXI Ethernet Lite. + Useful when either no IRQs are available at all + or when IRQs are not handled reliably by the interrupt controller. + Set to 0 to disable this feature. diff --git a/drivers/ethernet/eth_xilinx_axi_ethernet_lite.c b/drivers/ethernet/eth_xilinx_axi_ethernet_lite.c new file mode 100644 index 0000000000000..41383240c1c46 --- /dev/null +++ b/drivers/ethernet/eth_xilinx_axi_ethernet_lite.c @@ -0,0 +1,584 @@ +/* + * Xilinx AXI Ethernet Lite driver + * + * Copyright(c) 2025, CISPA Helmholtz Center for Information Security + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(eth_axi_eth_lite, CONFIG_ETHERNET_LOG_LEVEL); + +#include +#include +#include +#include "eth.h" + +/* memory-mapped dual-port RAM for TX */ +#define AXI_ETH_LITE_TX_PING_START_REG_OFFSET 0x0000 +#define AXI_ETH_LITE_TX_PING_END_REG_OFFSET 0x07F0 + +#define AXI_ETH_LITE_TX_PONG_START_REG_OFFSET 0x0800 +#define AXI_ETH_LITE_TX_PONG_END_REG_OFFSET 0x0FFC + +/* memory-mapped dual-port RAM for RX */ +#define AXI_ETH_LITE_RX_PING_START_REG_OFFSET 0x1000 +#define AXI_ETH_LITE_RX_PING_END_REG_OFFSET 0x17F0 + +#define AXI_ETH_LITE_RX_PONG_START_REG_OFFSET 0x1800 +#define AXI_ETH_LITE_RX_PONG_END_REG_OFFSET 0x1FFC + +#define AXI_ETH_LITE_TX_PING_LENGTH_REG_OFFSET 0x07F4 +#define AXI_ETH_LITE_GIE_REG_OFFSET 0x07F8 +#define AXI_ETH_LITE_TX_PING_CTRL_REG_OFFSET 0x07FC +#define AXI_ETH_LITE_TX_PONG_LENGTH_REG_OFFSET 0x0FF4 +#define AXI_ETH_LITE_TX_PONG_CTRL_REG_OFFSET 0x0FFC +#define AXI_ETH_LITE_RX_PING_CTRL_REG_OFFSET 0x17FC +#define AXI_ETH_LITE_RX_PONG_CTRL_REG_OFFSET 0x1FFC + +#define AXI_ETH_LITE_TX_PING_CTRL_PROGRAM_MAC_MASK (BIT(0) | BIT(1)) +#define AXI_ETH_LITE_TX_PING_CTRL_BUSY_MASK (BIT(0) | BIT(1)) +/* transmit (bit 0) and interrupt enable (bit 3), loopback, program disabled */ +#define AXI_ETH_LITE_TX_PING_TX_MASK (BIT(0) | BIT(3)) + +#define AXI_ETH_LITE_RX_CTRL_IRQ_ENABLE_MASK BIT(3) +#define AXI_ETH_LITE_RX_CTRL_READY_ENABLE_MASK BIT(0) + +#define AXI_ETH_LITE_GIE_ENABLE_MASK BIT(31) + +struct axi_eth_lite_data; + +struct axi_eth_lite_config { + void (*config_func)(struct axi_eth_lite_data *data); + + const struct device *phy; + const uintptr_t reg; + + /* device tree properties */ + bool has_rx_ping_pong; + bool has_tx_ping_pong; + bool has_random_mac_address; + bool has_interrupt; +}; + +struct axi_eth_lite_data { + /* used between ISR and send routine */ + struct k_sem tx_sem; + /* used to trigger the RX ISR from time to time */ + struct k_timer rx_timer; + /* used to offload copying an RX packet outside of ISR context */ + struct k_work rx_work; + struct k_spinlock timer_lock; + struct net_if *interface; + + const struct axi_eth_lite_config *config; + + uint8_t mac_addr[NET_ETH_ADDR_LEN]; + bool tx_ping_toggle; + bool rx_ping_toggle; +}; + +static inline uint32_t axi_eth_lite_read_reg(const struct axi_eth_lite_config *config, + mem_addr_t reg) +{ + return sys_read32((mem_addr_t)config->reg + reg); +} + +static inline void axi_eth_lite_write_reg(const struct axi_eth_lite_config *config, mem_addr_t reg, + uint32_t value) +{ + sys_write32(value, (mem_addr_t)config->reg + reg); +} + +static inline void axi_eth_lite_wait_complete(const struct axi_eth_lite_config *config) +{ + while (axi_eth_lite_read_reg(config, AXI_ETH_LITE_TX_PING_CTRL_REG_OFFSET) & + AXI_ETH_LITE_TX_PING_CTRL_BUSY_MASK) { + k_busy_wait(1); + } +} + +static inline void axi_eth_lite_write_transmit_buffer(const struct axi_eth_lite_config *config, + mem_addr_t buffer_start, const uint8_t *data, + size_t data_length) +{ + uint32_t unaligned_buffer; + const int start_offset = buffer_start & (sizeof(uint32_t) - 1); + + if (start_offset) { + uint8_t *buffer_ptr; + /* + * unaligned - write first bytes, keeping whatever was in the buffer already and + * restore alignment + */ + unaligned_buffer = + axi_eth_lite_read_reg(config, buffer_start & ~(sizeof(uint32_t) - 1)); + buffer_ptr = (uint8_t *)&unaligned_buffer; + memcpy(&buffer_ptr[start_offset], data, sizeof(uint32_t) - start_offset); + axi_eth_lite_write_reg(config, buffer_start & ~(sizeof(uint32_t) - 1), + unaligned_buffer); + buffer_start += sizeof(uint32_t) - start_offset; + data += sizeof(uint32_t) - start_offset; + data_length -= sizeof(uint32_t) - start_offset; + } + unaligned_buffer = 0; + + __ASSERT((buffer_start & (sizeof(uint32_t) - 1)) == 0, "Buffer addr %p is not aligned", + (void *)buffer_start); + + /* validity of length must be checked by caller! */ + while (data_length >= sizeof(uint32_t)) { + /* in case of fragmented buffer, data alignment and output alignment might not match + */ + uint32_t transfer_buffer; + + memcpy(&transfer_buffer, data, sizeof(transfer_buffer)); + axi_eth_lite_write_reg(config, buffer_start, transfer_buffer); + data += sizeof(uint32_t); + buffer_start += sizeof(uint32_t); + data_length -= sizeof(uint32_t); + } + + if (data_length) { + memcpy(&unaligned_buffer, data, data_length); + axi_eth_lite_write_reg(config, buffer_start, unaligned_buffer); + } +} + +static inline void axi_eth_lite_program_mac_address(const struct axi_eth_lite_config *config, + const struct axi_eth_lite_data *data) +{ + /* ping buffer is always available, pong would be optional */ + axi_eth_lite_write_transmit_buffer(config, AXI_ETH_LITE_TX_PING_START_REG_OFFSET, + data->mac_addr, NET_ETH_ADDR_LEN); + + axi_eth_lite_write_reg(config, AXI_ETH_LITE_TX_PING_CTRL_REG_OFFSET, + AXI_ETH_LITE_TX_PING_CTRL_PROGRAM_MAC_MASK); + + /* no interrupt configured - just spin */ + axi_eth_lite_wait_complete(config); +} + +static const struct device *axi_eth_lite_get_phy(const struct device *dev) +{ + const struct axi_eth_lite_config *config = dev->config; + + return config->phy; +} + +static enum ethernet_hw_caps axi_eth_lite_get_caps(const struct device *dev) +{ + return ETHERNET_LINK_10BASE | ETHERNET_LINK_100BASE; +} + +static void axi_eth_lite_phy_link_state_changed(const struct device *phydev, + struct phy_link_state *state, void *user_data) +{ + struct axi_eth_lite_data *data = user_data; + + ARG_UNUSED(phydev); + + LOG_INF("Link state changed to: %s (speed %x)", state->is_up ? "up" : "down", state->speed); + + /* inform the L2 driver whether we can handle packets now */ + if (state->is_up) { + net_eth_carrier_on(data->interface); + } else { + net_eth_carrier_off(data->interface); + } +} + +static void axi_eth_lite_iface_init(struct net_if *iface) +{ + struct axi_eth_lite_data *data = net_if_get_device(iface)->data; + const struct axi_eth_lite_config *config = net_if_get_device(iface)->config; + int err; + + data->interface = iface; + + ethernet_init(iface); + + LOG_DBG("Programming initial MAC address!"); + axi_eth_lite_program_mac_address(config, data); + if (net_if_set_link_addr(data->interface, data->mac_addr, sizeof(data->mac_addr), + NET_LINK_ETHERNET)) { + LOG_ERR("Could not set initial link address!"); + } + LOG_DBG("MAC address set!"); + + if (device_is_ready(config->phy)) { + /* initially no carrier */ + net_eth_carrier_off(iface); + + err = phy_link_callback_set(config->phy, axi_eth_lite_phy_link_state_changed, data); + + if (err) { + LOG_ERR("Could not set PHY link state changed handler: %d", err); + } + } else { + /* fixed link - no way to know so assume it is on */ + net_eth_carrier_on(iface); + } + + if (CONFIG_ETH_XILINX_AXI_ETHERNET_LITE_TIMER_PERIOD) { + k_timer_start(&data->rx_timer, + K_MSEC(CONFIG_ETH_XILINX_AXI_ETHERNET_LITE_TIMER_PERIOD), + K_MSEC(CONFIG_ETH_XILINX_AXI_ETHERNET_LITE_TIMER_PERIOD)); + } + + axi_eth_lite_write_reg(config, AXI_ETH_LITE_RX_PING_CTRL_REG_OFFSET, + AXI_ETH_LITE_RX_CTRL_IRQ_ENABLE_MASK); + + LOG_DBG("Interface initialized!"); +} + +static int axi_eth_lite_set_config(const struct device *dev, enum ethernet_config_type type, + const struct ethernet_config *config) +{ + struct axi_eth_lite_data *data = dev->data; + const struct axi_eth_lite_config *dev_config = dev->config; + + switch (type) { + case ETHERNET_CONFIG_TYPE_MAC_ADDRESS: + memcpy(data->mac_addr, config->mac_address.addr, sizeof(data->mac_addr)); + LOG_DBG("Programming initial MAC address!"); + axi_eth_lite_program_mac_address(dev_config, data); + LOG_DBG("MAC address set!"); + return net_if_set_link_addr(data->interface, data->mac_addr, sizeof(data->mac_addr), + NET_LINK_ETHERNET); + default: + LOG_ERR("Unsupported configuration set: %u", type); + return -EINVAL; + } +} + +static inline bool axi_eth_lite_cursor_advance(struct net_pkt_cursor *cursor) +{ + if (!cursor->buf->frags) { + /* packet complete */ + return false; + } + cursor->buf = cursor->buf->frags; + return true; +} + +static int axi_eth_lite_send(const struct device *dev, struct net_pkt *pkt) +{ + const size_t mtu = NET_ETH_MTU + sizeof(struct net_eth_hdr); + mem_addr_t buffer_addr, length_addr, control_addr; + struct net_pkt_cursor *cursor = &pkt->cursor; + struct axi_eth_lite_data *data = dev->data; + const struct axi_eth_lite_config *config = dev->config; + + if (net_pkt_get_len(pkt) > mtu) { + LOG_DBG("Packet is too long: %zu bytes with MTU: %zu bytes!", net_pkt_get_len(pkt), + mtu); + return -EINVAL; + } + + if (config->has_tx_ping_pong && data->tx_ping_toggle) { + buffer_addr = AXI_ETH_LITE_TX_PONG_START_REG_OFFSET; + length_addr = AXI_ETH_LITE_TX_PONG_LENGTH_REG_OFFSET; + control_addr = AXI_ETH_LITE_TX_PONG_CTRL_REG_OFFSET; + } else { + buffer_addr = AXI_ETH_LITE_TX_PING_START_REG_OFFSET; + length_addr = AXI_ETH_LITE_TX_PING_LENGTH_REG_OFFSET; + control_addr = AXI_ETH_LITE_TX_PING_CTRL_REG_OFFSET; + } + + if (config->has_interrupt) { + (void)k_sem_take(&data->tx_sem, K_FOREVER); + } + + if (axi_eth_lite_read_reg(config, control_addr) & AXI_ETH_LITE_TX_PING_CTRL_BUSY_MASK) { + /* + * no interrupt -> try to transmit as many packets as the L2 wants, discard them if + * busy; otherwise, semaphore for flow control + */ + if (config->has_interrupt) { + LOG_WRN("Unexpectedly, %s buffer is busy!", + control_addr == AXI_ETH_LITE_TX_PING_CTRL_REG_OFFSET ? "ping" + : "pong"); + } + + net_pkt_unref(pkt); + return -EBUSY; + } + + data->tx_ping_toggle = !data->tx_ping_toggle; + + /* no need to linearize - can copy fragments one by one into transmit buffer */ + do { + int frag_len = cursor->buf->len; + const uint8_t *frag_data = cursor->buf->data; + + axi_eth_lite_write_transmit_buffer(config, buffer_addr, frag_data, frag_len); + + buffer_addr += frag_len; + } while (axi_eth_lite_cursor_advance(cursor)); + + axi_eth_lite_write_reg(config, length_addr, net_pkt_get_len(pkt)); + + /* as API is asynchronous, need not wait for TX completion */ + axi_eth_lite_write_reg(config, control_addr, AXI_ETH_LITE_TX_PING_TX_MASK); + + return 0; +} + +static const struct ethernet_api axi_eth_lite_api = {.get_phy = axi_eth_lite_get_phy, + .get_capabilities = axi_eth_lite_get_caps, + .iface_api.init = axi_eth_lite_iface_init, + .set_config = axi_eth_lite_set_config, + .send = axi_eth_lite_send}; + +static inline int axi_eth_lite_read_to_pkt(const struct axi_eth_lite_config *config, + struct net_pkt *pkt, mem_addr_t buffer_addr, + size_t bytes_to_read) +{ + int ret = 0; + + for (size_t read_bytes = 0; read_bytes < bytes_to_read; read_bytes += sizeof(uint32_t)) { + uint32_t current_data = axi_eth_lite_read_reg(config, buffer_addr); + size_t bytes_to_write_now = read_bytes + sizeof(uint32_t) > bytes_to_read + ? bytes_to_read - read_bytes + : sizeof(uint32_t); + + ret += net_pkt_write(pkt, ¤t_data, bytes_to_write_now); + + if (ret < 0) { + LOG_ERR("Write error bytes %zu/%zu (%zu)", read_bytes, bytes_to_read, + bytes_to_write_now); + } else { + LOG_DBG("Write OK bytes %zu/%zu (%zu) cursor %p remaining %d", read_bytes, + bytes_to_read, bytes_to_write_now, pkt->cursor.buf, + pkt->cursor.buf ? pkt->cursor.buf->size - pkt->cursor.buf->len : 0); + } + + buffer_addr += sizeof(uint32_t); + } + return ret; +} + +/* FIXME are there generic defines? */ +#define AXI_ETH_LITE_ARP_PACKET_LENGTH 28 + +#define HEADER_BUF_SIZE \ + (sizeof(struct net_eth_hdr) + MAX(sizeof(struct net_ipv4_hdr), sizeof(struct net_ipv6_hdr))) +#define HEADER_BUF_SIZE_ALIGNED \ + ((HEADER_BUF_SIZE) + sizeof(uint32_t) - (HEADER_BUF_SIZE) % sizeof(uint32_t)) + +static inline void axi_eth_lite_receive(const struct axi_eth_lite_config *config, + struct axi_eth_lite_data *data, mem_addr_t buffer_addr, + mem_addr_t status_addr) +{ + size_t packet_size = NET_ETH_MTU; + uint8_t header_buf[HEADER_BUF_SIZE_ALIGNED]; + uint16_t len; + + struct net_pkt *pkt; + const struct net_eth_hdr *hdr; + + if ((axi_eth_lite_read_reg(config, status_addr) & AXI_ETH_LITE_RX_CTRL_READY_ENABLE_MASK) == + 0) { + /* no data*/ + return; + } + + for (size_t read_bytes = 0; read_bytes < sizeof(header_buf); + read_bytes += sizeof(uint32_t)) { + uint32_t current_data = axi_eth_lite_read_reg(config, buffer_addr); + + memcpy(&header_buf[read_bytes], ¤t_data, sizeof(current_data)); + buffer_addr += sizeof(uint32_t); + } + + hdr = (struct net_eth_hdr *)header_buf; + len = hdr->type; + /* + * AXI Ethernet Lite cannot tell us the length of the received packet, so we try to parse it + * Also, FCS is not used by Zephyr stack + */ + switch (ntohs(len)) { + case NET_ETH_PTYPE_ARP: + /* fixed length */ + packet_size = sizeof(struct net_eth_hdr) + AXI_ETH_LITE_ARP_PACKET_LENGTH; + break; + case NET_ETH_PTYPE_IP: { + const struct net_ipv4_hdr *ip4_hdr = + (const struct net_ipv4_hdr *)&header_buf[sizeof(*hdr)]; + len = ip4_hdr->len; + packet_size = ntohs(len); + /* length includes ipv4 header length */ + packet_size += sizeof(struct net_eth_hdr); + break; + } + case NET_ETH_PTYPE_IPV6: { + const struct net_ipv6_hdr *ip6_hdr = + (const struct net_ipv6_hdr *)&header_buf[sizeof(*hdr)]; + /* payload + any optional extension headers */ + len = ip6_hdr->len; + packet_size = ntohs(len); + packet_size += sizeof(struct net_eth_hdr) + sizeof(*ip6_hdr); + break; + } + default: + /* use the full MTU... */ + break; + } + + pkt = net_pkt_rx_alloc_with_buffer(data->interface, packet_size, AF_UNSPEC, 0, K_NO_WAIT); + + if (!pkt) { + LOG_WRN("Could not alloc RX packet!"); + goto out; + } + + net_pkt_write(pkt, header_buf, MIN(sizeof(header_buf), packet_size)); + + LOG_DBG("Pkt allocated with size %zu written %zu", packet_size, + MIN(sizeof(header_buf), packet_size)); + + if (packet_size > HEADER_BUF_SIZE_ALIGNED && + axi_eth_lite_read_to_pkt(config, pkt, buffer_addr, + packet_size - HEADER_BUF_SIZE_ALIGNED)) { + /* this should never happen, ignore it if it does but warn */ + LOG_ERR("Could not read data to packet!"); + } + + if (net_recv_data(data->interface, pkt) < 0) { + LOG_ERR("Could not receive data!"); + net_pkt_unref(pkt); + } + +out: + /* re-sets status bit - buffer may be used again */ + axi_eth_lite_write_reg(config, status_addr, + status_addr == AXI_ETH_LITE_RX_PING_CTRL_REG_OFFSET + ? AXI_ETH_LITE_RX_CTRL_IRQ_ENABLE_MASK + : 0); +} + +static void axi_eth_lite_process_rx_packets(struct k_work *item) +{ + struct axi_eth_lite_data *data = CONTAINER_OF(item, struct axi_eth_lite_data, rx_work); + const struct axi_eth_lite_config *config = data->config; + + /* need to use the toggle to receive packets in correct sequence */ + if (config->has_rx_ping_pong && data->rx_ping_toggle) { + axi_eth_lite_receive(config, data, AXI_ETH_LITE_RX_PONG_START_REG_OFFSET, + AXI_ETH_LITE_RX_PONG_CTRL_REG_OFFSET); + } else { + axi_eth_lite_receive(config, data, AXI_ETH_LITE_RX_PING_START_REG_OFFSET, + AXI_ETH_LITE_RX_PING_CTRL_REG_OFFSET); + } + data->rx_ping_toggle = !data->rx_ping_toggle; +} + +/* the interrupt on this device is a bit limited: it cannot tell us which event triggered the IRQ */ +static void axi_eth_lite_isr(const struct device *dev) +{ + int tx_opportunities = 0; + struct axi_eth_lite_data *data = dev->data; + const struct axi_eth_lite_config *config = dev->config; + + /* might have been TX completion... */ + if ((axi_eth_lite_read_reg(config, AXI_ETH_LITE_TX_PING_CTRL_REG_OFFSET) & + AXI_ETH_LITE_TX_PING_CTRL_BUSY_MASK) == 0) { + tx_opportunities++; + } + if (config->has_tx_ping_pong && + (axi_eth_lite_read_reg(config, AXI_ETH_LITE_TX_PONG_CTRL_REG_OFFSET) & + AXI_ETH_LITE_TX_PING_CTRL_BUSY_MASK) == 0) { + tx_opportunities++; + } + while (k_sem_count_get(&data->tx_sem) < tx_opportunities) { + k_sem_give(&data->tx_sem); + } + /* no overflow */ + __ASSERT_NO_MSG(k_sem_count_get(&data->tx_sem) == tx_opportunities); + + /* do the copying in a thread context, where it can be interrupted if needed */ + k_work_submit(&data->rx_work); +} + +void axi_eth_lite_timer_fn(struct k_timer *timer) +{ + const struct device *dev = timer->user_data; + struct axi_eth_lite_data *data = dev->data; + + /* concurrent invocation of ISR would be a problem */ + k_spinlock_key_t key = k_spin_lock(&data->timer_lock); + + axi_eth_lite_isr(dev); + + k_spin_unlock(&data->timer_lock, key); +} + +/* Xilinx OUI (Organizationally Unique Identifier) for MAC */ +#define XILINX_OUI_BYTE_0 0x00 +#define XILINX_OUI_BYTE_1 0x0A +#define XILINX_OUI_BYTE_2 0x35 + +static int axi_eth_lite_init(const struct device *dev) +{ + const struct axi_eth_lite_config *config = dev->config; + struct axi_eth_lite_data *data = dev->data; + + config->config_func(data); + + if (config->has_random_mac_address) { + gen_random_mac(data->mac_addr, XILINX_OUI_BYTE_0, XILINX_OUI_BYTE_1, + XILINX_OUI_BYTE_2); + } + + if (config->has_interrupt) { + axi_eth_lite_write_reg(config, AXI_ETH_LITE_GIE_REG_OFFSET, + AXI_ETH_LITE_GIE_ENABLE_MASK); + /* start with 1 for ping-pong, as we can always start 2 transactions concurrently */ + if (k_sem_init(&data->tx_sem, config->has_tx_ping_pong ? 1 : 0, K_SEM_MAX_LIMIT)) { + LOG_ERR("Could not initialize semaphore!"); + return -EINVAL; + } + } else { + LOG_DBG("No interrupt configured - AXI Ethernet Lite will have to spin!"); + } + if (CONFIG_ETH_XILINX_AXI_ETHERNET_LITE_TIMER_PERIOD) { + k_timer_init(&data->rx_timer, axi_eth_lite_timer_fn, NULL); + data->rx_timer.user_data = (void *)(uintptr_t)dev; + } + + k_work_init(&data->rx_work, axi_eth_lite_process_rx_packets); + + return 0; +} + +#define SETUP_IRQS(inst) \ + IRQ_CONNECT(DT_INST_IRQN(inst), DT_INST_IRQ(inst, priority), axi_eth_lite_isr, \ + DEVICE_DT_INST_GET(inst), 0); \ + \ + irq_enable(DT_INST_IRQN(inst)) + +#define AXI_ETH_LITE_INIT(inst) \ + \ + static void axi_eth_lite_config_##inst(struct axi_eth_lite_data *dev) \ + { \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, interrupts), (SETUP_IRQS(inst)), \ + (LOG_DBG("No IRQs defined!"))); \ + } \ + \ + static const struct axi_eth_lite_config config_##inst = { \ + .config_func = axi_eth_lite_config_##inst, \ + .phy = DEVICE_DT_GET_OR_NULL(DT_INST_PHANDLE(inst, phy_handle)), \ + .reg = DT_REG_ADDR(DT_INST_PARENT(inst)), \ + .has_rx_ping_pong = DT_INST_PROP(inst, xlnx_rx_ping_pong), \ + .has_tx_ping_pong = DT_INST_PROP(inst, xlnx_tx_ping_pong), \ + .has_random_mac_address = DT_INST_PROP(inst, zephyr_random_mac_address), \ + .has_interrupt = DT_INST_NODE_HAS_PROP(inst, interrupts)}; \ + static struct axi_eth_lite_data data_##inst = { \ + .mac_addr = DT_INST_PROP_OR(inst, local_mac_address, {0}), \ + .config = &config_##inst}; \ + \ + ETH_NET_DEVICE_DT_INST_DEFINE(inst, axi_eth_lite_init, NULL, &data_##inst, &config_##inst, \ + CONFIG_ETH_INIT_PRIORITY, &axi_eth_lite_api, NET_ETH_MTU); + +#define DT_DRV_COMPAT xlnx_xps_ethernetlite_3_00_a_mac +DT_INST_FOREACH_STATUS_OKAY(AXI_ETH_LITE_INIT); diff --git a/drivers/mdio/CMakeLists.txt b/drivers/mdio/CMakeLists.txt index ee772e79f8072..669f1cbf260aa 100644 --- a/drivers/mdio/CMakeLists.txt +++ b/drivers/mdio/CMakeLists.txt @@ -20,4 +20,5 @@ zephyr_library_sources_ifdef(CONFIG_MDIO_RENESAS_RA mdio_renesas_ra.c) zephyr_library_sources_ifdef(CONFIG_MDIO_LAN865X mdio_lan865x.c) zephyr_library_sources_ifdef(CONFIG_MDIO_SENSRY_SY1XX mdio_sy1xx.c) zephyr_library_sources_ifdef(CONFIG_MDIO_XILINX_AXI_ENET mdio_xilinx_axienet.c) +zephyr_library_sources_ifdef(CONFIG_MDIO_XILINX_AXI_ETHERNET_LITE mdio_xilinx_axi_ethernet_lite.c) zephyr_library_sources_ifdef(CONFIG_MDIO_INTEL_IGC mdio_intel_igc.c) diff --git a/drivers/mdio/Kconfig b/drivers/mdio/Kconfig index 8b842ac6a8eda..cdfab89927498 100644 --- a/drivers/mdio/Kconfig +++ b/drivers/mdio/Kconfig @@ -41,6 +41,7 @@ source "drivers/mdio/Kconfig.renesas_ra" source "drivers/mdio/Kconfig.lan865x" source "drivers/mdio/Kconfig.sy1xx" source "drivers/mdio/Kconfig.xilinx_axienet" +source "drivers/mdio/Kconfig.xilinx_axi_ethernet_lite" source "drivers/mdio/Kconfig.intel_igc" config MDIO_INIT_PRIORITY diff --git a/drivers/mdio/Kconfig.xilinx_axi_ethernet_lite b/drivers/mdio/Kconfig.xilinx_axi_ethernet_lite new file mode 100644 index 0000000000000..a405006818eb4 --- /dev/null +++ b/drivers/mdio/Kconfig.xilinx_axi_ethernet_lite @@ -0,0 +1,9 @@ +# Copyright 2025 CISPA Helmholtz Center for Information Security +# SPDX-License-Identifier: Apache-2.0 + +config MDIO_XILINX_AXI_ETHERNET_LITE + bool "Xilinx AXI Ethernet Lite MDIO driver" + default y + depends on DT_HAS_XLNX_XPS_ETHERNETLITE_3_00_A_MDIO_ENABLED + help + Enable Xilinx AXI Ethernet Lite MDIO bus driver. diff --git a/drivers/mdio/mdio_xilinx_axi_ethernet_lite.c b/drivers/mdio/mdio_xilinx_axi_ethernet_lite.c new file mode 100644 index 0000000000000..56b1bb209b0ed --- /dev/null +++ b/drivers/mdio/mdio_xilinx_axi_ethernet_lite.c @@ -0,0 +1,198 @@ +/* + * Xilinx AXI Ethernet Lite MDIO + * + * Copyright(c) 2025, CISPA Helmholtz Center for Information Security + * SPDX-License-Identifier: Apache-2.0 + */ +#include +LOG_MODULE_REGISTER(mdio_axi_eth_lite, CONFIG_ETHERNET_LOG_LEVEL); + +#include +#include +#include + +#include + +#define AXI_ETH_LITE_MAX_PHY_DEVICES 32 + +#define AXI_ETH_LITE_MDIO_ADDRESS_REG_OFFSET 0x07e4 +#define AXI_ETH_LITE_MDIO_WRITE_DATA_REG_OFFSET 0x07e8 +#define AXI_ETH_LITE_MDIO_READ_DATA_REG_OFFSET 0x07ec +#define AXI_ETH_LITE_MDIO_CONTROL_REG_OFFSET 0x07f0 + +#define AXI_ETH_LITE_MDIO_CONTROL_REG_MDIO_ENABLE_MASK BIT(3) +#define AXI_ETH_LITE_MDIO_CONTROL_REG_MDIO_BUSY_MASK BIT(0) +#define AXI_ETH_LITE_MDIO_CONTROL_REG_MDIO_DISABLE_MASK 0 + +#define AXI_ETH_LITE_MDIO_ADDRESS_REG_OP_READ BIT(10) +#define AXI_ETH_LITE_MDIO_ADDRESS_REG_OP_WRITE 0 +#define AXI_ETH_LITE_MDIO_ADDRESS_REG_SHIFT_REGADDR 0 +#define AXI_ETH_LITE_MDIO_ADDRESS_REG_SHIFT_PHYADDR 5 + +struct axi_eth_lite_data { + struct k_mutex mutex; +}; + +struct axi_eth_lite_config { + void *reg; +}; + +static inline uint32_t axi_eth_lite_read_reg(const struct axi_eth_lite_config *config, + mem_addr_t reg) +{ + return sys_read32((mem_addr_t)config->reg + reg); +} + +static inline void axi_eth_lite_write_reg(const struct axi_eth_lite_config *config, mem_addr_t reg, + uint32_t value) +{ + sys_write32(value, (mem_addr_t)config->reg + reg); +} + +static inline int axi_eth_lite_check_busy(const struct axi_eth_lite_config *config) +{ + uint32_t mdio_control_reg_val = + axi_eth_lite_read_reg(config, AXI_ETH_LITE_MDIO_CONTROL_REG_OFFSET); + + return mdio_control_reg_val & AXI_ETH_LITE_MDIO_CONTROL_REG_MDIO_BUSY_MASK ? -EBUSY : 0; +} + +static inline void axi_eth_lite_set_addr(const struct axi_eth_lite_config *config, uint8_t prtad, + uint8_t regad, bool is_read) +{ + uint32_t mdio_addr_val = is_read ? AXI_ETH_LITE_MDIO_ADDRESS_REG_OP_READ + : AXI_ETH_LITE_MDIO_ADDRESS_REG_OP_WRITE; + + /* range check done below in read/write functions */ + mdio_addr_val |= (uint32_t)regad << AXI_ETH_LITE_MDIO_ADDRESS_REG_SHIFT_REGADDR; + mdio_addr_val |= (uint32_t)prtad << AXI_ETH_LITE_MDIO_ADDRESS_REG_SHIFT_PHYADDR; + + axi_eth_lite_write_reg(config, AXI_ETH_LITE_MDIO_ADDRESS_REG_OFFSET, mdio_addr_val); +} + +static inline void axi_eth_lite_bus_enable(const struct device *dev) +{ + const struct axi_eth_lite_config *config = dev->config; + + axi_eth_lite_write_reg(config, AXI_ETH_LITE_MDIO_CONTROL_REG_OFFSET, + AXI_ETH_LITE_MDIO_CONTROL_REG_MDIO_ENABLE_MASK); +} + +/* arbitrary but sufficient in testing */ +#define MDIO_MAX_WAIT_US 1000 + +static inline int axi_eth_lite_complete_transaction(const struct axi_eth_lite_config *config) +{ + int waited_cycles = 0; + + /* start transaction - everything set up */ + axi_eth_lite_write_reg(config, AXI_ETH_LITE_MDIO_CONTROL_REG_OFFSET, + AXI_ETH_LITE_MDIO_CONTROL_REG_MDIO_ENABLE_MASK | + AXI_ETH_LITE_MDIO_CONTROL_REG_MDIO_BUSY_MASK); + + while (axi_eth_lite_check_busy(config) && waited_cycles < MDIO_MAX_WAIT_US) { + /* no need to block the CPU */ + k_msleep(1); + waited_cycles++; + } + + if (waited_cycles == MDIO_MAX_WAIT_US) { + LOG_ERR("Timed out waiting for MDIO transaction to complete!"); + return -EIO; + } + /* busy went low - transaction complete */ + return 0; +} + +static int axi_eth_lite_read(const struct device *dev, uint8_t prtad, uint8_t regad, + uint16_t *value) +{ + const struct axi_eth_lite_config *config = dev->config; + struct axi_eth_lite_data *data = dev->data; + + if (prtad >= AXI_ETH_LITE_MAX_PHY_DEVICES) { + LOG_ERR("Requested read port address %" PRIu8 " not supported - max %d", prtad, + AXI_ETH_LITE_MAX_PHY_DEVICES); + return -ENOSYS; + } + + (void)k_mutex_lock(&data->mutex, K_FOREVER); + + if (axi_eth_lite_check_busy(config)) { + LOG_ERR("MDIO bus busy!"); + (void)k_mutex_unlock(&data->mutex); + return -ENOSYS; + } + + axi_eth_lite_set_addr(config, prtad, regad, true); + + if (axi_eth_lite_complete_transaction(config)) { + (void)k_mutex_unlock(&data->mutex); + return -EIO; + } + + *value = (uint16_t)axi_eth_lite_read_reg(config, AXI_ETH_LITE_MDIO_READ_DATA_REG_OFFSET); + + (void)k_mutex_unlock(&data->mutex); + + return 0; +} + +static int axi_eth_lite_write(const struct device *dev, uint8_t prtad, uint8_t regad, + uint16_t value) +{ + const struct axi_eth_lite_config *config = dev->config; + struct axi_eth_lite_data *data = dev->data; + + if (prtad >= AXI_ETH_LITE_MAX_PHY_DEVICES) { + LOG_ERR("Requested write port address %" PRIu8 " not supported - max %d", prtad, + AXI_ETH_LITE_MAX_PHY_DEVICES); + return -ENOSYS; + } + + (void)k_mutex_lock(&data->mutex, K_FOREVER); + + if (axi_eth_lite_check_busy(config)) { + LOG_ERR("MDIO bus busy!"); + (void)k_mutex_unlock(&data->mutex); + return -ENOSYS; + } + axi_eth_lite_set_addr(config, prtad, regad, false); + axi_eth_lite_write_reg(config, AXI_ETH_LITE_MDIO_WRITE_DATA_REG_OFFSET, value); + + if (axi_eth_lite_complete_transaction(config)) { + (void)k_mutex_unlock(&data->mutex); + return -EIO; + } + + (void)k_mutex_unlock(&data->mutex); + + return 0; +} + +static int axi_eth_lite_init(const struct device *dev) +{ + struct axi_eth_lite_data *data = dev->data; + + (void)k_mutex_init(&data->mutex); + + axi_eth_lite_bus_enable(dev); + + return 0; +} + +static DEVICE_API(mdio, axi_eth_lite_api) = {.read = axi_eth_lite_read, + .write = axi_eth_lite_write}; + +#define XILINX_AXI_ETHERNET_LITE_MDIO_INIT(inst) \ + \ + static const struct axi_eth_lite_config axi_eth_lite_config##inst = { \ + .reg = (void *)(uintptr_t)DT_REG_ADDR(DT_INST_PARENT(inst)), \ + }; \ + static struct axi_eth_lite_data axi_eth_lite_data##inst = {0}; \ + DEVICE_DT_INST_DEFINE(inst, axi_eth_lite_init, NULL, &axi_eth_lite_data##inst, \ + &axi_eth_lite_config##inst, POST_KERNEL, CONFIG_MDIO_INIT_PRIORITY, \ + &axi_eth_lite_api); + +#define DT_DRV_COMPAT xlnx_xps_ethernetlite_3_00_a_mdio +DT_INST_FOREACH_STATUS_OKAY(XILINX_AXI_ETHERNET_LITE_MDIO_INIT) diff --git a/dts/bindings/ethernet/xlnx,xps-ethernetlite-3.00.a-mac.yaml b/dts/bindings/ethernet/xlnx,xps-ethernetlite-3.00.a-mac.yaml new file mode 100644 index 0000000000000..0ca6ed9b07785 --- /dev/null +++ b/dts/bindings/ethernet/xlnx,xps-ethernetlite-3.00.a-mac.yaml @@ -0,0 +1,20 @@ +# Copyright (c) 2025 CISPA Helmholtz Center for Information Security +# SPDX-License-Identifier: Apache-2.0 + +description: Xilinx AXI Ethernet Lite MAC driver bindings. + +include: [ethernet-controller.yaml] + +compatible: "xlnx,xps-ethernetlite-3.00.a-mac" + +properties: + interrupts: + description: Xilinx AXI Ethernet uses edge-triggered interrupts to signal RX/TX completion. + + xlnx,rx-ping-pong: + type: boolean + description: Does this device have a secondary RX buffer that is updated concurrently? + + xlnx,tx-ping-pong: + type: boolean + description: Does this device have a secondary TX buffer that is updated concurrently? diff --git a/dts/bindings/ethernet/xlnx,xps-ethernetlite-3.00.a.yaml b/dts/bindings/ethernet/xlnx,xps-ethernetlite-3.00.a.yaml new file mode 100644 index 0000000000000..e7cc2a3c0f9e4 --- /dev/null +++ b/dts/bindings/ethernet/xlnx,xps-ethernetlite-3.00.a.yaml @@ -0,0 +1,9 @@ +# Copyright (c) 2025 CISPA Helmholtz Center for Information Security +# SPDX-License-Identifier: Apache-2.0 + +description: Xilinx AXI Ethernet Lite subsystem (MAC and MDIO) bindings. + +# only "reg" property is used +include: [base.yaml] + +compatible: "xlnx,xps-ethernetlite-3.00.a" diff --git a/dts/bindings/mdio/xlnx,xps-ethernetlite-3.00.a-mdio.yaml b/dts/bindings/mdio/xlnx,xps-ethernetlite-3.00.a-mdio.yaml new file mode 100644 index 0000000000000..e3e9c1aefbe26 --- /dev/null +++ b/dts/bindings/mdio/xlnx,xps-ethernetlite-3.00.a-mdio.yaml @@ -0,0 +1,10 @@ +# Copyright (c) 2025 CISPA Helmholtz Center for Information Security +# SPDX-License-Identifier: Apache-2.0 + +# Common fields for MDIO controllers + +include: mdio-controller.yaml + +compatible: xlnx,xps-ethernetlite-3.00.a-mdio + +description: MDIO interface of Xilinx AXI Ethernet Lite.