В первой части статьи мы ознакомились со структурой сетевого адаптера и научились принимать сетевые пакеты. Во второй части продолжим изучение этой темы, перейдем, как и планировалось, на использование ядерных фреймворков (clocks, reset, libphy), рассмотрим что есть в DeviceTree, добавим передачу пакетов и в итоге получим полноценный (почти) драйвер сетевого адаптера.
Общий план статьи следующий:
подготавливаем макетную плату
подключаем ядерные фреймворки к проекту
добавляем очередь передачи пакетов
подключаем драйвер к сетевому стеку ядра
собираем, запускаем, тестируем
1. Подготовка макетной платы
Подготовим загрузочный образ для тестовой платы OrangePi Zero. В отличии от первой части мы не будет собирать образ вручную из исходников, а сделаем это при помощи buildroot.
Скачиваем пакет, распаковываем и ставим переменную окружения BR к корневому каталогу, куда распаковали пакет:
wget https://buildroot.org/downloads/buildroot-2026.02.tar.gz tar xfz buildroot-2026.02.tar.gz cd buildroot-2026.02 export BR=$PWD echo $BR
В каталог $(BR)/board/orangepi/orangepi-zero/patches/linux копируем патч, для чего он нужен объясним ниже.
Формируем конфигурацию для платы:
make orangepi_zero_defconfig
Заходим в конфигуратор ядра:
make linux-defconfig
Отключаем сетевой адаптер:
Device Drivers -> Network Device Support -> Ethernet driver support -> STMicroelectronics devices
Выходим из режима конфигурирования с сохранением всех изменений и запускаем сборку:
make
Сборка занимает некоторое время, возможно потребуется доустановить некоторые зависимости. Если все прошло успешно, то в каталоге $BR/output/images будет файл sdcard.img, который прошиваем на microSD карту:
sudo dd if=sdcard.img of=/dev/sda
Далее прошитую карту вставляем в слот платы и подаем питание, процесс загрузки наблюдаем на отладочной консоли:
Процесс загрузки макетной платы
1 USB Device(s) found scanning bus usb@1c1a400 for devices... 1 USB Device(s) found scanning bus usb@1c1b000 for devices... 1 USB Device(s) found scanning bus usb@1c1b400 for devices... 1 USB Device(s) found scanning usb for storage devices... 0 Storage Device(s) found Hit any key to stop autoboot: 0 switch to partitions #0, OK mmc0 is current device Scanning mmc 0:1... Found /boot/extlinux/extlinux.conf Retrieving file: /boot/extlinux/extlinux.conf 1: default Retrieving file: /boot/zImage append: root=PARTUUID=fce358a3-1b42-487f-8e35-b596deee660f rootwait console=ttyS0,115200 rootfstype=ext4 quiet panic=10 Retrieving file: /boot/sun8i-h2-plus-orangepi-zero.dtb Kernel image @ 0x42000000 [ 0x000000 - 0x5bb438 ] ## Flattened Device Tree blob at 43000000 Booting using the fdt blob at 0x43000000 Working FDT set to 43000000 Loading Device Tree to 49ff7000, end 49fffdc4 ... OK Working FDT set to 49ff7000 Starting kernel ... [ 0.001571] /cpus/cpu@0 missing clock-frequency property [ 0.001605] /cpus/cpu@1 missing clock-frequency property [ 0.001624] /cpus/cpu@2 missing clock-frequency property [ 0.001642] /cpus/cpu@3 missing clock-frequency property [ 0.236948] lima 1c40000.gpu: error -ENODEV: _opp_set_regulators: no regulator (mali) found Seeding 256 bits and crediting Saving 256 bits of creditable seed for next boot Starting syslogd: OK Starting klogd: OK Running sysctl: OK Starting mdev... OK Starting network: OK Starting dhcpcd... dhcpcd-10.2.4 starting DUID 00:01:00:01:c7:92:bc:ce:00:00:00:00:00:00 no interfaces have a carrier Starting crond: OK Welcome to Buildroot for the Orange Pi Zero OrangePi_Zero login: root # # uname -a Linux OrangePi_Zero 6.12.3 #2 SMP Mon Jun 22 17:22:34 MSK 2026 armv7l GNU/Linux
2. Подключение ядерных фреймворков
2.1. libphy
Подключим ядерные фреймворки, и начнем с libphy. Эта подсистема предназначена для управления и настройки PHY. PHY, как мы уже знаем из первой части, нужен для преобразование бинарного представления кадра в вид, пригодный для передачи по физическому носителю (Ethernet кабелю). PHY соединен с MAC шиной MII (Media Independent Interface), в состав которой входит SMI (Station Management Interface), через который выполняется управление PHY. SMI это двухпроводный интерфейс, состоящий из линии данных (Management Data Input/Output, MDIO) и линии тактирования (MDC).
В ядре шину MII описывает структура struct mii_bus:
struct mii_bus { const char *name; void *priv; /** @read: Perform a read transfer on the bus */ int (*read)(struct mii_bus *bus, int addr, int regnum); /** @write: Perform a write transfer on the bus */ int (*write)(struct mii_bus *bus, int addr, int regnum, u16 val); /** @mdio_map: list of all MDIO devices on bus */ struct mdio_device *mdio_map[PHY_MAX_ADDR]; ... }
name - имя шины
priv - частные данные
read/write - callback-функции для чтения/записи по MDIO для доступа к регистрам PHY, мы должны определить их самостоятельно.
mdio_map - список устройств PHY, подключенных к шине.
Порядок работы с шиной следующий:
создать шину
зарегистрировать ее в системе
просканировать и зарегистрировать подключенные к шине устройства (PHY)
подключить выбранный PHY к MAC
Для всех этих шагов подсистема libphy содержит готовые функции. Создание и регистрация шины выполняется при помощи вызовов mdiobus_alloc и mdiobus_register. mdiobus_alloc вызывается без параметров, возвращает указатель на структуру типа struct mii_bus. Этот указатель затем передается в функцию mdiobus_register для регистрации шины. При регистрации шины выполняется сканирование и регистрация подключенных к шине устройств (PHY):
for (i = 0; i < PHY_MAX_ADDR; i++) struct phy_device *phydev = mdiobus_scan(bus, i);
Параметры вызова mdiobus_scan - указатель на структуру шины mii_bus и адрес (порядковый номер) устройства. Всего к шине можно подключить до 32 устройств (PHY_MAX_ADDR == 32). При сканировании считывается идентификатор PHY из регистров MII_PHYSID1 и MII_PHYSID2. Для доступа к этим регистрам шина использует callback-вызовы read и write, которые мы передали ей в структуре mii_bus при регистрации. Формат кадра чтения/записи в PHY был рассмотрен в первой части. Если идентификатор устройства в основном состоит из символов 0xF, то шина считает что устройство по этому адресу отсутствует. В противном случае новое устройство регистрируется на шине - заполняется массив mdio_map в соответствующей адресу позиции (индексу).
Как устройство знает к какой шине оно принадлежит?
Хотя массив mdio_map состоит из указателей типа struct mdio_device, само устройство PHY описывается структурой struct phy_device. Как устройство знает к какой шине оно принадлежит? Тут все просто - структура phy_device содержит struct mdio_device mdio:
struct phy_device { struct mdio_device mdio; ...
А сама mdio_device содержит указатель на шину:
struct mdio_device { struct device dev; struct mii_bus *bus; ...
Когда при сканировании шина находит устройство, создается структура phy_device в функции phy_device_create:
struct phy_device *phy_device_create(struct mii_bus *bus, int addr, u32 phy_id) { struct phy_device *dev; struct mdio_device *mdiodev; /* We allocate the device, and initialize the default values */ dev = kzalloc(sizeof(*dev), GFP_KERNEL); mdiodev = &dev->mdio; mdiodev->bus = bus; ....
и затем регистрируется на шине в функции phy_device_register:
int phy_device_register(struct phy_device *phydev) { mdiobus_register_device(&phydev->mdio); .... int mdiobus_register_device(struct mdio_device *mdiodev) { .... mdiodev->bus->mdio_map[mdiodev->addr] = mdiodev; }
Завершающий этап - подключение MAC (если с привязкой к схеме из первой части) к PHY. Делает это вызов phy_connect_direct:
int phy_connect_direct(struct net_device *dev, struct phy_device *phydev, void (*handler)(struct net_device *, phy_interface_t interface);
dev - сетевое устройство, которое подключается к PHY
phydev - собственно PHY, к которому подключается dev
handler - callback, отслеживающий изменения в состоянии соединения (режим дуплекс/полудуплекс, скорость соединения и пр.)
interface - интерфейс PHY, в нашем примере PHY_INTERFACE_MODE_MII, остальные доступные режимы прописаны в <linux/phy.h>.
Получить указатель на структуру phy_device, зарегистрированного на шине устройства, можно при помощи вызова mdiobus_get_phy, параметры вызова - указатель на шину и адрес устройства (индекс в массиве mdio_map):
struct phy_device *mdiobus_get_phy(struct mii_bus *bus, int addr)
Отображаем информацию о подключении и стартуем PHY:
phy_attached_info(phydev); phy_start(phydev);
Итоговая функция драйвера для настройки PHY выглядит следующим образом (обработку ошибок пропустим):
static int emac_mdio_init(struct net_device *ndev) { int addr, err; struct emac_priv *priv = netdev_priv(ndev); struct mii_bus *mii_bus; struct phy_device *phydev;
Создаем шину:
priv->mii_bus = mdiobus_alloc(); mii_bus = priv->mii_bus;
Имя шины и callback-функции чтения/записи:
mii_bus->name = "emac-mdio"; mii_bus->read = emac_phy_read; mii_bus->write = emac_phy_write; snprintf(mii_bus->id, MII_BUS_ID_SIZE, "%s", mii_bus->name); mii_bus->priv = priv;
Регистрируем шину и ищем устройства, подключенные к ней:
mdiobus_register(mii_bus);
Получаем указатель на устройство, зарегистрированное на шине:
addr = PHY_ADDR; phydev = mdiobus_get_phy(mii_bus, addr); priv->phy_addr = addr; priv->phydev = phydev; return 0; }
Запись с именем ‘emac-mdio’ появится в sysfs (/sys/class/mdio_bus). callback-функции для чтения/записи в PHY практически не изменились по сравнению с первой частью, поменялся только прототип вызова, добавилась структура struct mii_bus:
emac_phy_read
int emac_phy_read(struct mii_bus *mii_bus, int phy_addr, int phy_reg) { u32 v, val = MII_BUSY; int data; val |= (phy_addr << MII_PHY_ADDR_SHIFT ); val |= (phy_reg << MII_PHY_REG_ADDR_SHIFT); val |= (3 << 20); readl_poll_timeout(emac_base_addr + MII_CMD, v, !(v & MII_BUSY), 100, 100000); writel(0, emac_base_addr + MII_DATA); writel(val, emac_base_addr + MII_CMD); readl_poll_timeout(emac_base_addr + MII_CMD, val, !(val & MII_BUSY), 100, 100000); data = (int)readl(emac_base_addr + MII_DATA); return data; }
emac_phy_write
int emac_phy_write(struct mii_bus *mii_bus, int phy_addr, int phy_reg, u16 data) { int err; u32 v, val = MII_BUSY; val |= (phy_addr << MII_PHY_ADDR_SHIFT); val |= (phy_reg << MII_PHY_REG_ADDR_SHIFT) | MII_WRITE; val |= (3 << 20); /* Ждем пока завершатся текущие операции на шине */ readl_poll_timeout(emac_base_addr + MII_CMD, v, !(v & MII_BUSY), 100, 100000)); writel(data, emac_base_addr + MII_DATA); writel(val, emac_base_addr + MII_CMD); readl_poll_timeout(emac_base_addr + MII_CMD, val, !(val & MII_BUSY), 100, 100000); return 0; }
Callback-функция emac_phylink_handler для отслеживания изменения состояния соединения между MAC и PHY:
emac_phylink_handler
static void emac_phylink_handler(struct net_device *ndev) { int v; struct emac_priv *priv = netdev_priv(ndev); struct phy_device *phydev = priv->phydev; int duplex, speed; phy_print_status(phydev); netdev_info(ndev, "duplex %s, speed %s\n", phy_duplex_to_str(phydev->duplex), phy_speed_to_str(phydev->speed)); switch (phydev->speed) { case SPEED_10: speed = SPEED10; break; case SPEED_100: speed = SPEED100; break; default: break; } duplex = phydev->duplex; v = (speed << 2) | duplex; writel(v, emac_base_addr + EMAC_CTL0); }
2.2. Clocks и reset
Теперь перейдем на использование фреймворка для управления тактовыми генераторами. В первой части для включения генератора мы напрямую обращались к регистру ‘Bus Clock Gating Reg0’ по смещениею 0x60 относительно базового адреса и устанавливали в нем бит номер 17. При работе с фреймворком все эти детали скрыты в коде поддержки платформы, нам предоставляется только символьное имя генератора и структура struct clk.
Зачем нужен фреймворк clk
Зачем вообще нужен фреймворк clk? Почему нельзя просто включать/выключать тактовый генератор установкой/сбросом управляющего бита? Ответ очевиден - тактовый генератор может тактировать несколько устройств, или служить генератором опорной частоты. Простое включение/выключение может нарушить работу всей системы. Поэтому в фреймворке предусмотрен счетчик использования генератора. При каждом вызове clk_enable() счетчик инкрементируется, при clk_disable() - декрементируется. При нулевом значении счетчика генератор отключается. Таким образом фреймворк решает задачи по обеспечению стабильной работы системы и снижению энергопотребления, что важно для встраиваемых систем.
Символьное имя задается в device tree, там же детализировано, где в платформенном коде находятся управляющие генеартором структуры:
emac: ethernet@1c30000 { compatible = "allwinner,sun8i-h3-emac"; ... clocks = <&ccu CLK_BUS_EMAC>; clock-names = "stmmaceth"; ...
Итак, из device tree мы получаем имя генератора “stmmaceth”, и процесс включения выглядит следующим образом:
struct clk *emac_clk = devm_clk_get(dev, "stmmaceth"); clk_prepare_enable(emac_clk);
Структура struct clk определена в файле /drivers/clk/clk.c, обсуждать тут реализацию фреймворка не будем, вещь достаточно громоздкая, кому интересно могут изучить заголовочник include/linux/clk-provider.h, в нем struct clk_ops (callback-функции для работы с генераторами), struct clk_hw (для перехода от общесистемной структуры struct clk к платформенно-зависимому коду) и пр.
Параметр CLK_BUS_EMAC это индекс в массиве, который связывает общесистемный код с платформенно-зависимым:
static struct clk_hw_onecell_data sun8i_h3_hw_clks = { .hws = { ... [CLK_BUS_EMAC] = &bus_emac_clk.common.hw, ...
bus_emac_clk - это структура ccu_gate, которая завернута в макрос SUNXI_CCU_GATE (drivers/clk/sunxi-ng/ccu-sun8i-h3.c):
static SUNXI_CCU_GATE(bus_emac_clk, "bus-emac", "ahb2", 0x060, BIT(17), 0);
Сама структура и макрос определены в drivers/clk/sunxi-ng/ccu_gate.h:
struct ccu_gate { u32 enable; struct ccu_common common; }; #define SUNXI_CCU_GATE(_struct, _name, _parent, _reg, _gate, _flags) \ struct ccu_gate _struct = { \ .enable = _gate, \ .common = { \ .reg = _reg, \ .hw.init = CLK_HW_INIT(_name, \ _parent, \ &ccu_gate_ops, \ _flags), \ } \ }
В этом макросе поля _reg и _gate - смещение к регистру относительно базового адреса CCU (Clocks Control Unit) и управляющий бит в нем для включения генератора. Значение эти нам знакомы - 0x60 и 17. Таким образом, вызов clk_prepare_enable(emac_clk) установит бит номер 17 в регистре ‘Bus Clock Gating Reg0’ по смещению 0x60.
Базовый адрес CCU получим из соответствующего узла device tree:
ccu: clock@1c20000 { reg = <0x01c20000 0x400>; .... };
Также из device tree получим информацию для выполнения аппаратного сброса контроллера, за это отвечает фреймворк reset, основная структура это struct reset_control. Запись из device tree:
resets = <&ccu RST_BUS_EMAC>; reset-names = "stmmaceth";
RST_BUS_EMAC - индекс в массиве struct ccu_reset_map sun8i_h3_ccu_resets[]:
static struct ccu_reset_map sun8i_h3_ccu_resets[] = { ... [RST_BUS_EMAC] = { 0x2c0, BIT(17) }, ... }
Это указывает на 17-й бит регистра Bus Sotfware Reset Reg0, который расположен по смещению 0x2c0 относительно базового адреса CCU. Аппаратный сброс EMAC выполняется установкой и последующим сбросом этого бита.
Получаем управляющую структуру и выполняем аппаратный сброс EMAC:
struct reset_control *emac_rst = devm_reset_control_get(dev, "stmmaceth"); reset_control_reset(emac_rst);
Аналогичным образом для PHY из device tree получим данные и сформируем управляющие структуры struct clk и struct reset_control, но вначале немного упростим саму запись в device tree, приведем ее к такому виду:
mii-phy { compatible = "allwinner,sun8i-h3-mdio-mux"; clocks = <&ccu CLK_BUS_EPHY>; resets = <&ccu RST_BUS_EPHY>; };
Это тот патч, с которым мы собирали образ для платы.
Парсим узел:
struct device_node *mii_phy = of_get_child_by_name(dev->of_node, "mii-phy"); struct clk *ephy_clk = of_clk_get(mii_phy, 0); struct reset_control *ephy_rst = of_reset_control_get_exclusive(mii_phy, NULL);
Включаем генератор и делаем сброс PHY:
clk_prepare_enable(ephy_clk); reset_control_reset(ephy_rst);
3. Буфер сокета sk_buff и работа с ним
Поскольку драйвер плотно работает с сетевым стеком, то необходимо рассмотреть такую структуру как sk_buff. О ней мы уже говорили в первой части, это основная структура в сетевом стеке ядра Linux, она содержит все данные о принимаемом или передаваемом пакете, включая заголовки всех уровней и другую информацию.
Буфер сокета имеет определенный формат, несколько служебных полей и набор управляющих функций, при помощи которых создается буфер сокета, размещаются и удаляются данные в нем и т.п.
Вот перечень некоторых полей структуры sk_buff (см. <linux/skbuff.h>):
struct sk_buff { struct sk_buff *next; // Linked list pointers struct sk_buff *prev; unsigned char *head; // Начало буфера unsigned char *data; // Начало актуальных данных unsigned char *tail; // Конец данных unsigned char *end; // Конец буфера unsigned int len; // Общий размер данных unsigned int data_len; // Размер фрагментированных (paged) данных ... };
Расмотрим как меняется формат буфера в памяти во время выполнения стандартных операций.
Для выделения память под буфер сокета используется вызов alloc_skb(), параметры вызова - размер выделяемой памяти. После вызова формат буфера в памяти выглядит следующим образом:
Формат буфера сокета после вызова alloc_skb(): +----------------------------------------------------+ | tailroom | +----------------------------------------------------+ ^ ^ | | head end data tail
Указатели head, data и tail показывают на начало буфера. Далее делаем вызов skb_reserver() для резервирования места под заголовки транспортного, сетевого, канального уровней, параметр вызова - размер зарезервированного места:
Формат буфера сокета после вызова skb_reserve(): +-------------+---------------------------------------+ | headroom | tailroom | +-------------+---------------------------------------+ ^ ^ ^ | | | head data end tail
Теперь забронируем место для пользовательских данных вызовом skb_put(), параметр - размер данных size. Этот вызов сдвигает указатель tail на size байт:
Формат буфера сокета после вызова skb_put: +------------+------------------------+----------------+ | headroom | linear data | tailroom | +------------+------------------------+----------------+ ^ ^ ^ ^ | | | | head data tail end
Вызов skb_put можно использовать только для случая линейных данных, т.е. когда размер данных соответствует размеру выделенной под буфер сокета памяти и размещены в пределах этого буфера.
Если размер данных превышает размер буфера сокета, то данные фрагментируются - распределяются по нескольким страницам.
Для хранения информации о фрагментах используется структура skb_shared_info:
struct skb_shared_info { .... __u8 nr_frags; .... /* must be last field, see pskb_expand_head() */ skb_frag_t frags[MAX_SKB_FRAGS]; };
nr_frags - общее число фрагментов, frags - массив элементов типа
struct bio_vec { struct page *bv_page; unsigned int bv_len; unsigned int bv_offset; };
bv_page - указатель на страницу, где расположены данные фрагмента
bv_len - размер данных
bv_offset - смещение к данным в пределах страницы
Формат буфера сокета при фрагментации данных: +----------+------------------+------------------+----------+ | headroom | linear data | tailroom | skb_info | +----------+------------------+------------------+----------+ ^ ^ ^ ^ [page frag] | | | | [page frag] head data tail end [page frag]
Поле data_len содержит размер фрагментированных данных, поле len - общий размер данных, т.е. размер линейных данных + размер фрагментированных данных. Для определения размера линейных данных используется ф-ия skb_headlen() (см. <linux/skbuff.h>):
static inline unsigned int skb_headlen(const struct sk_buff *skb) { return skb->len - skb->data_len; }
Если данные не фрагментированы, то data_len равно 0, и размер линейных данных равен общему размеру len.
В самом вызове skb_put() есть проверка на линейность:
BUG_ON(skb_is_nonlinear(skb)) static inline bool skb_is_nonlinear(const struct sk_buff *skb) { return skb->data_len; }
Если данные фрагментированы, то skb->data_len > 0, skb_is_nonlinear() вернет true и BUG_ON вызовет аварийную остановку ядра (kernel panic).
Чтобы добавить к пакету заголовок используется вызов skb_push. Этот вызов сдвигает указатель data к head, размещая заголовки перед данными, UDP например:
Формат буфера сокета после вызова skb_push: +----------+--------------------------+-------------+ | headroom | UDP header | linear data | tailroom | +-------------------------------------+-------------+ ^ ^ ^ ^ | | | | head data tail end
Для большей наглядности еще раз изобразим формат буфера:
Complete sk_buff Memory Organization: Main SKB Buffer: +----------+------------------+------------------+----------+ | headroom | linear data | tailroom |shared_info| +----------+------------------+------------------+----------+ ^ ^ ^ ^ | | | | head data tail end |<--- headlen ---->| |<------- total linear space -------->| Fragment Pages (referenced by shared_info): Page 1: Page 2: Page N: +------------------+ +------------------+ +------------------+ | Fragment Data 1 | | Fragment Data 2 | | Fragment Data N | +------------------+ +------------------+ +------------------+ ^ ^ ^ | | | frags[0].page frags[1].page frags[n-1].page offset: frags[0].offset offset: frags[1].offset ... size: frags[0].size size: frags[1].size ...
4. Очередь передачи
В первой части мы упомянули структуру net_device, сказали зачем она нужна, как под нее выделять память с учетом частных данных. Что такое частные данные мы тоже знаем - это структура, которая содержит данные для работы конкретного драйвера. По сравнению с первой частью мы в частные данные драйвера добавим дескриптор очереди пакетов для передачи struct tx_queue и данные по PHY:
struct emac_priv { void __iomem *emac_base_addr; int emac_irq; struct net_device *ndev; struct device *dev; struct rx_queue rx_q; struct tx_queue tx_q; /* PHY stuff */ struct mii_bus *mii_bus; struct phy_device *phydev; int phy_addr; unsigned int link; unsigned int speed; unsigned int duplex; };
Очередь пакетов для передачи почти идентична очереди приема пакетов из первой части, появились два дополнительных поля dirty_tx и len, их назначение рассмотрим чуть ниже, когда коснемся вопроса непосредственно передачи:
struct tx_queue { struct emac_priv *priv; struct sk_buff **sk_buff; dma_addr_t *sk_buff_dma; struct dma_desc *dma_tx; dma_addr_t dma_tx_phy; unsigned int cur_tx; unsigned int dirty_tx; unsigned int len; };
5. Взаимодействие с сетевым стеком ядра
Структура net_device содержит callback-функции для взаимодействия с сетевым стеком ядра. Все они сгруппированы в структуре struct net_device_ops. Функций достаточно много, почти все опциональны, кроме одной ndo_start_xmit, как указано в комментариях.
В нашем драйвере структура c callback-функциями выглядит следующим образом:
static struct net_device_ops emac_netdev_ops = { .ndo_open = emac_start, .ndo_start_xmit = emac_xmit, .ndo_stop = emac_stop, };
ndo_open - вызывается при переходе устройства в состояние UP, например при вызове ifconfig eth0 up
ndo_start_xmit - функция для передачи пакета в сеть
ndo_stop - вызывается при переходе устройства в состояние DOWN, например при вызове ifconfig eth0 down
Рассмотрим подробно каждую функцию.
- ndo_open (emac_start)
callback-функция реализована в emac_start, входные параметры - структура net_device.
Первое, что делаем в этой функции - формируем очереди для приема и передачи пакетов:
/* Allocate RX and TX queues */ emac_rx_queue_init(ndev); emac_tx_queue_init(ndev);
Формирование очереди для приема пакетов мы рассмотрели в первой части. Очередь для передачи пакетов формируется схожим образом.
Передаваемый пакет хранится в выделенном буфере сокета. Физический адрес буфера содержится в дескрипторе передачи. Дескриптор передачи, как и дескриптор приема, состоит четырех 32-х разрядных полей. Первое поле - управляющие и контрольные флаги, второе - размер буфера, третье поле - адрес буфера пакета для передачи, четвертое - указатель на следующий дескриптор в списке. Голова списка дескрипторов хранится в регистре ‘Transmit DMA Descriptor List Address Register’.
В виде структуры дескриптор выглядит следующим образом:
struct dma_desc { u32 status; u32 st; u32 buf_addr; u32 next; };
Для формирования очереди передачи нам потребуется выделить массив для хранения указателей на буферы передаваемых пакетов и их аппаратные адреса, количество буферов равно TX_SIZE:
struct sk_buff *sk_buff = kmalloc_array(TX_SIZE, sizeof(struct sk_buff *), GFP_KERNEL); dma_addr_t *sk_buff_dma = kmalloc_array(TX_SIZE, sizeof(dma_addr_t), GFP_KERNEL);
В отличии от приема пакетов, при передаче пакетов память для каждого буфера выделять не надо, т.к. готовый пакет уже передается параметром в функцию emac_xmit. Нам только надо получить из виртуального ядерного адреса аппаратный адрес вызовом dma_map_single.
Также нужен массив для хранения дескрипторов передачи, всего их у нас TX_SIZE. Доступ к массиву должен быть и у драйвера и у DMA блока адаптера, используем постоянное отображение, т.к. список дескрипторов нужен на все время работы драйвера:
dma_addr_t dma_tx_phy; struct dma_desc *dma_tx = dma_alloc_coherent(dev, TX_SIZE * sizeof(struct dma_desc), &dma_tx_phy, GFP_KERNEL);
Драйвер работает с виртуальным адресом dma_tx, адаптер - с аппаратным (физическим) dma_tx_phy. Инициализируем массив дескрипторов:
dma_addr_t dma_phy = dma_tx_phy; for (i = 0; i < TX_SIZE; i++) { struct dma_desc *p = dma_tx + i; dma_phy += sizeof(struct dma_desc); p->next = dma_phy; } p->next = dma_tx_phy;
Адрес dma_tx_phy мы должны записать в регистр Transmit DMA Descriptor List Address.
После формирования очередей приема и передачи пакетов генерируем аппаратный адрес адаптера и сохраняем его в поле dev_addr структуры net_device:
eth_hw_addr_random(ndev);
Записывает адрес в регистры адаптера (функцию установки аппаратного адреса мы рассмотрели в первой части):
emac_set_mac_addr(ndev->dev_addr);
Следующий шаг - регистрация обработчика прерывания. Адаптер генерирует прерывание при приеме и отправке пакета (такой механизм как NAPI пока пропустим):
request_irq(emac_irq, emac_interrupt, IRQF_SHARED, "emac-irq", ndev);
emac_irq - номер прерывания, его получаем из узла device tree так же как и в первой части:
emac: ethernet@1c30000 { .... interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>; interrupt-names = "macirq"; .... emac_irq = platform_get_irq_byname(pdev, "macirq");
emac_interrupt - функция-обрабочик прерывания. При возникновении прерывания первым делом считываем регистр состояния прерываний, сохраняем состояние и сбрасываем установленные флаги:
status = readl(emac_base_addr + EMAC_INT_STA); writel(status, emac_base_addr + EMAC_INT_STA);
Проверяем причину прерывания и вызываем соответствующую функцию:
- пакет принят:
if (status & EMAC_RX_INT) emac_rx(ndev);
- пакет передан:
if (status & EMAC_TX_INT) emac_tx_clean(ndev);
Функции-обработчики рассмотрим ниже.
После регистрации обработчика прерывания установим режим работы адаптера при передаче и приема пакета.
При приеме пакета DMA блок адаптера копирует данные из FIFO в системную память, и тут возможны два сценария:
данные копируются после получения полного пакета
данные копируются при достижении определенного порога
Управляет этим поведением бит #1 регистра Receive Control 1 Register. Значение порога определяет поле RX_TH (threshold value of RX DMA FIFO). Принимать мы будет целый пакет, значение порога не используем, бит устанавливаем в 1:
void emac_set_rx_operation_mode(void) { unsigned long v = readl(emac_base_addr + EMAC_RX_CTL1); /* RX_MD RX DMA reads data from RX DMA FIFO to host memory after a complete frame has been written to RX DMA FIFO */ v |= BIT(1); writel(v, emac_base_addr + EMAC_RX_CTL1); }
Аналогично для режима передачи - отправляем целый пакет, значение порога не используем, этим управляет бит #1 в регистре Transmit Control 1 Register:
void emac_set_tx_operation_mode(void) { unsigned long v = readl(emac_base_addr + EMAC_TX_CTL1); /* TX_MD Transmission starts after full frame located in TX DMA FIFO */ v |= BIT(1); writel(v, emac_base_addr + EMAC_TX_CTL1); }
Теперь подключаем MAC к PHY, этот вызов мы выше уже рассмотрели:
phy_connect_direct(ndev, phydev, emac_phylink_handler, PHY_INTERFACE_MODE_MII);
и стартуем PHY:
phy_start(phydev);
Активируем прием пакетов, установив бит #31 (RX_EN, Enable receiver) в регистре Receive Control 0 Register и бит #30 (RX_DMA_EN, Start and run RX DMA) в регистре Receive Control 1 Register:
void emac_enable_rx(void) { u32 v = readl(emac_base_addr + EMAC_RX_CTL0); v |= BIT(31); writel(v, emac_base_addr + EMAC_RX_CTL0); } void emac_start_rx(void) { u32 v = readl(emac_base_addr + EMAC_RX_CTL1); v |= BIT(30); writel(v, emac_base_addr + EMAC_RX_CTL1); }
Для включения передающего тракта устанавливаем бит #31 (TX_EN, Enable transmitter) в регистре Transmit Control 0 Register и бит #30 (TX_DMA_EN, Start and run TX DMA) в регистре Transmit Control 1 Register.
void emac_enable_tx(void) { u32 v = readl(emac_base_addr + EMAC_TX_CTL0); v |= BIT(31); writel(v, emac_base_addr + EMAC_TX_CTL0); } void emac_start_tx(void) { u32 v = readl(emac_base_addr + EMAC_TX_CTL1); v |= BIT(30); writel(v, emac_base_addr + EMAC_TX_CTL1); }
Разрешаем прерывания установкой бит RX_INT_EN (бит 8) и TX_INT_EN (бит 0) в регистре Interrupt Enable Register:
void emac_enable_irq(void) { writel(EMAC_RX_INT | EMAC_TX_INT, emac_base_addr + EMAC_INT_EN); }
- ndo_start_xmit (emac_xmit)
Эта функция вызывается верхними уровнями сетевого стека для передачи пакета в сеть.
Параметры функции:
struct sk_buff *skb - буфер сокета с данными для передачи
struct net_device *ndev - дескриптор сетевого устройства, через которое отправляем пакет
При передаче надо учесть размер очереди пакетов, и в случае ее превышения - приостановить передачу. Остановка передачи выполняется вызовом netif_stop_queue, в результате чего верхние уровни сетевого стека не будут вызывать функцию start_xmit. Передача пакетов возобновится, когда драйвер разблокирует очередь вызовом netif_wake_queue.
Определить доступное количество дескрипторов передачи можно при помощи функции emac_tx_avail:
static u32 emac_tx_avail(struct emac_priv *priv) { struct tx_queue *tx_q = &priv->tx_q; u32 avail; if (tx_q->dirty_tx > tx_q->cur_tx) avail = tx_q->dirty_tx - tx_q->cur_tx - 1; else avail = TX_SIZE - tx_q->cur_tx + tx_q->dirty_tx - 1; return avail; }
Посмотрим наглядно как работает эта функция.
Предположим, что очередь пакетов состоит из четырех дескрипторов. В структуре очереди у нас есть индексы сur_tx и dirty_tx. Индекс cur_tx указывает на текущий дескриптор, в который будет записан пакет для последующей передачи. Индекс dirty_tx указывает на “грязный” дескриптор, из которого данные уже были переданы в сеть. Изначально, когда очередь передачи пустая, позиции индексов совпадают.
По мере передачи пакетов индекс cur_tx будет уходить вперед, и все дескрипторы, находящиеся между dirty_tx и cur_tx, будут считаться “грязными”. Записывать в них новые данные нельзя. Сначала их надо очистить от старых данных и привести в соответствие индексы cur_tx и dirty_tx.
1) d +----avail ----+ | | | +----+----+----+----+ | 0 | 1 | 2 | 3 | +----+----+----+----+ | c
Вариант 1. Очередь передачи пуста, доступно для записи три дескриптора, не считая текущего, cur_tx ( c ) и dirty_tx (d) указывают на один и тот же дескриптор.
2) d +--avail -+ | | | +----+----+----+----+ | 0 | 1 | 2 | 3 | +----+----+----+----+ | c
Вариант 2. В дескриптор #0 был записан и затем передан в сеть пакет. Индекс cur_tx сместился на дескриптор #1, дескрииптор #0 помечен как “грязный”. Для записи доступны два дескриптора.
3) d avail | | | +----+----+----+----+ | 0 | 1 | 2 | 3 | +----+----+----+----+ | c
Вариант 3. Аналогично предыдущему варианту, записали и передали пакет из дескриптора #1, текущий индекс сместился на дескриптор #2, для записи доступен один дескриптор.
4) c +- avail -+ | | | +----+----+----+----+ | 0 | 1 | 2 | 3 | +----+----+----+----+ | d
Вариант 4. Этот вариант отличается от предыдущих тем, что индекс “грязного” дескриптора больше чем индекс текущего. Для записи доступны 2 дескриптора, т.к. выполняется первое условие функции emac_tx_avail.
Получаем указатель на первый свободный дескриптор:
entry = tx_q->cur_tx; desc = tx_q->dma_tx + entry;
Вычисляем индекс нового свободного дескриптора:
tx_q->cur_tx = (tx_q->cur_tx + 1) % TX_SIZE;
Заполняем очередь передачи:
tx_q->sk_buff[entry] = skb;
Смотрим, сколько осталось доступных дескрипторов. Если их нет - просим верхний уровень приостановить передачу:
avail = emac_tx_avail(priv); if (avail <= 1) netif_stop_queue(ndev);
Размер линейных данных в буфере пакета:
tx_q->len = skb_headlen(skb);
Выполняем потоковое отображение и получаем аппаратный адрес данных, чтобы DMA блок адаптера имел к ним доступ:
dma_phy = dma_map_single(priv->dev, skb->data, len, DMA_TO_DEVICE);
Сохраняем этот адрес в очереди передачи:
tx_q->sk_buff_dma[entry] = dma_phy;
Заполняем дескриптор передачи. Аппаратный адрес данных:
desc->buf_addr = dma_phy;
Размер данных:
desc->st = cpu_to_le32(len & TX_BUF_SIZE_MASK);
Сгенерировать прерывание по окончании передачи пакета:
desc->st |= BIT(31); /* TX_INT in ISR when current frame have been transmitted */
Указываем на отсутствие сегментации данных пакета, все находится в пределах одной непрерывной области памяти (для scatter-gather опции адаптера):
desc->st |= BIT(30) | BIT(29); /* first and last segment */
Передаем дескриптор DMA блоку адаптера:
desc->status |= BIT(31);
Команда разрешения передачи (см. выше):
emac_start_tx();
После передачи пакета в сеть генерируется прерывание, в регистре состояния прерываний устанавливается флаг EMAC_TX_INT, и далее из обработчика будет вызвана ф-ия emac_tx_clean. Ее задача - очистить “грязные” дескрипторы и подготовить место для новых пакетов, подлежащих передаче:
static void emac_tx_clean(struct net_device *ndev) { struct emac_priv *priv = netdev_priv(ndev); struct tx_queue *tx_q = &priv->tx_q; u32 entry;
Проходим по всем “грязным” дескрипторам:
entry = tx_q->dirty_tx; while (entry != tx_q->cur_tx) {
Получаем указатель на буфер пакета и дескриптор очереди:
struct sk_buff *skb = tx_q->sk_buff[entry]; struct dma_desc *p = tx_q->dma_tx + entry;
Если с пакетом работает DMA блок адаптера, то не мешаем ему и выходим:
if (p->status & BIT(31)) break;
Возвращаем дескриптор под контроль CPU:
if (tx_q->sk_buff_dma[entry]) { dma_unmap_single(priv->dev, tx_q->sk_buff_dma[entry], tx_q->len, DMA_TO_DEVICE ); }
Освобождаем память, выделенную под буфер пакета:
if (skb != NULL) { dev_consume_skb_any(skb); tx_q->sk_buff[entry] = NULL; }
Индекс следующего “грязного” дескриптора:
entry = (entry + 1) % TX_SIZE; }
Сохраняем новое значение первого “грязного” дескриптора:
tx_q->dirty_tx = entry;
Eсли приостанавливали передачу - просим верхний уровень возобновить процесс отправки пакетов:
if (netif_queue_stopped(ndev) && (emac_tx_avail(priv) > 1)) netif_wake_queue(ndev);
- ndo_stop (emac_stop)
Функция вызывается при завершении работы драйвера. В ней все достаточно просто - выполняется очистка ресурсов:
static int emac_stop(struct net_device *ndev) { struct emac_priv *priv = netdev_priv(ndev); struct phy_device *phydev = priv->phydev; emac_stop_tx(); emac_stop_rx(); phy_stop(phydev); phy_disconnect(phydev); free_irq(priv->emac_irq, ndev); emac_rx_queue_release(ndev); emac_tx_queue_release(ndev); return 0; }
6. Прием пакетов
Осталось расмотреть функцию приема пакета. Многое повторяет вариант из первой части статьи, но есть и отличия:
static int emac_rx(struct net_device *ndev) {
Получаем указатель на частные данные и на приемную очередь:
struct emac_priv *priv = netdev_priv(ndev); struct rx_queue *rx_q = &priv->rx_q;
Индекс текущего приемного дескриптора:
unsigned int entry = rx_q->cur_rx; unsigned int next_entry;
Счетчик принятых пакетов:
unsigned int count = 0;
Запускаем цикл приема пакетов:
while (1) {
Адресуем дескриптор:
struct dma_desc *p = rx_q->dma_rx + entry;
Проверяем что дескриптор не занят DMA блоком адаптера:
if (p->status & BIT(31)) break; count++;
Получаем размер принятого пакета:
int frame_len = (status & FRAME_LEN_MASK) >> FRAME_LEN_SHIFT; if (frame_len > 1536) break;
Принятый пакет сидит в приемной очереди, нам нужна структура sk_buff чтобы его сохранить и передать дальше наверх по сетевому стеку. Выделяем память:
struct sk_buff *skb = netdev_alloc_skb(priv->ndev, frame_len);
Запрашиваем у адаптера доступ к данным:
dma_sync_single_for_cpu(priv->dev, rx_q->sk_buff_dma[entry], frame_len, DMA_FROM_DEVICE);
Копируем принятые данные в буфер skb:
skb_copy_to_linear_data(skb, rx_q->sk_buff[entry]->data, frame_len);
Т.к. skb_copy_to_linear_data это просто оболочка над memcpy, нам надо настроить поля в структуре skb, а именно сместить указатель tail на конец линейных данных и увеличить размер данных буфера. Это выполняет вызов skb_put:
skb_put(skb, frame_len);
Возвращаем адаптеру доступ к данным:
dma_sync_single_for_device(priv->dev, rx_q->sk_buff_dma[entry], frame_len, DMA_FROM_DEVICE);
Устанавливаем идентификатор протокола (ETH_P_802_3 например):
skb->protocol = eth_type_trans(skb, priv->ndev);
Отправляем принятый пакет вверх по сетевому стеку для дальнейшей обработки:
netif_rx(skb);
Обновляем значение индекса текущего дескриптора:
rx_q->cur_rx = (rx_q->cur_rx + 1) % RX_SIZE; next_entry = rx_q->cur_rx; entry = next_entry;
Передаем дескриптор во владение DMA блока адаптера, для приема очередного пакета:
p->status |= BIT(31); }
При выходе из цикла возвращаем количество принятых пакетов:
return count;
7. Сборка и запуск.
Исходный текст драйвера можно скачать тут. Для сборки нам потребуются полный путь к собранным исходным текстам ядра KSRC и к средствам сборки TOOLCHAIN. С привязкой к корневому каталогу buildroot это:
KSRC=$BR/output/build/linux-6.12.3 TOOLCHAIN=$BR/output/host/bin/arm-buildroot-linux-gnueabihf-
Для сборки драйвера вводим команду:
make ARCH=arm -C $KSRC M=$(pwd) modules CROSS_COMPILE=$TOOLCHAIN
В результате получим модуль ядра net.ko. Запишем его на загрузочную SD карту (например в каталог mnt), установим карту в слот платы и подадим питание. После старта переходим в каталог /mnt и загружаем модуль:
# cd /mnt # lsmod Module Size Used by Tainted: G xradio_wlan 122880 0 # ifconfig eth0 ifconfig: eth0: error fetching interface information: Device not found # insmod net.ko # lsmod Module Size Used by Tainted: G net 16384 0 xradio_wlan 122880 0 # ifconfig eth0 eth0 Link encap:Ethernet HWaddr E2:BC:73:FB:89:C8 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
Командой ifconfig проверяем наличие сетевого интерфейса eth0. После загрузки модуля интерфейс появляется в системе, и ему можно назначить IP адрес. Делается это либо при помощи команды ifconfig, либо через dhcpcd, если он был сконфигурирован при сборке buildroot:
# ps ax | grep dhcp 155 dhcpcd dhcpcd: [manager] [ip4] 156 root dhcpcd: [privileged proxy] 157 dhcpcd dhcpcd: [network proxy] 158 dhcpcd dhcpcd: [control proxy] 230 dhcpcd dhcpcd: [BPF ARP] eth0 192.168.1.29 241 dhcpcd dhcpcd: [BPF ARP] eth0 192.168.1.28 282 root grep dhcp
После назначения IP адреса интерфейс готов к работе, можно запустить утилиту ping для тестирования:
# ping -c3 192.168.1.1 PING 192.168.1.1 (192.168.1.1): 56 data bytes 64 bytes from 192.168.1.1: seq=0 ttl=64 time=0.948 ms 64 bytes from 192.168.1.1: seq=1 ttl=64 time=0.790 ms 64 bytes from 192.168.1.1: seq=2 ttl=64 time=0.743 ms --- 192.168.1.1 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 0.743/0.827/0.948 ms
8. Итог.
Итоги подведем. Мы изучили устройство драйвера сетевого адаптера, рассмотрели как использовать ядерные фрейморки при разработке, ознакомились с ключевыми структурами сетевой подсистемы ядра, такими как буфер сокета, и разработали функционирующий драйвер сетевого адаптера.
За бортом повествования остались вопросы передачи фрагментированного пакета, механизм NAPI, управление энергопотреблением (suspend/resume) и блокировки. Все это рассмотрим в третьей части.