В этой статье мы рассмотрим как устроен драйвер сетевого адаптера для Linux.
Cтатью разделим на две части.
В первой части рассмотрим общую структуру сетевого адаптера, узнаем какие компоненты входят в его состав, что такое MAC и PHY, разберемся как подготовить адаптер к работе, сконфигурировать, и как в итоге получать сетевые пакеты.
Хотя при разработке драйверов необходимо использовать стандартные ядерные фреймворки, такие как clock, reset, libphy и пр., поначалу мы будем работать с адаптером напрямую через регистры состояния и управления. Это позволит нам детально разобраться с аппаратной частью.
Во второй части статьи приведем драйвер к нормальному виду, с использованием стандартных фреймворков и описанием того, что надо указать в Device Tree, и рассмотрим как передавать сетевые пакеты.
Нам нужна макетная плата с сетевым адаптером, на которую можно поставить Linux. Возьмем Orange Pi Zero на платформе Allwinner H2+. В состав платформы входят четыре процессорных ядра Cortex-A7, поддерживается ОЗУ стандартов LPDDR2, LPDDR3, DDR3, широкий спектр соединений и интерфейсов, в том числе сетевой адаптер, для которого мы будем разрабатывать драйвер. Подробное описание платформы тут, документация на платформу Allwinner H3 Datasheet.
Также нам понадобится переходник USB-to-COM для получения отладочного вывода платы. В качестве консольной программы можно использовать minicom.
В первой части статьи рассмотрим следующие вопросы:
запуск Linux на макетной плате
общая структура Ethernet сетевого адаптера
начнем разработку драйвера – рассмотрим порядок включения и настройки для приема пакетов
1. Запуск Linux на макетной плате
Для запуска Linux на плате необходимо:
подготовить средства разработки
подготовить корневую файловую систему (rootfs)
подготовить ядро Linux и дерево устройств для данной платформы (device tree)
подготовить загрузчик u-boot
сформировать загрузочную sd-карту и загрузиться с нее
1.1 Средства разработки
Хост-система, на которой ведем разработку - Debian 10. Для разработки установим следующие пакеты:
apt-get install libgmp3-dev
apt-get install libmpc-dev
apt install libyaml-dev
pip3 install dtschema
apt install gcc-arm-linux-gnueabi
apt-get install u-boot-tools
Целевая платформа у нас ARM, укажем ее и кросс-компилятор:
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabi-
1.2. Корневая файловая система (roosfs)
Есть два варианта подготовки корневой файловой системы – взять готовую или сделать самим. Выберем второй, соберем rootfs из busybox. Скачиваем его:
wget https://busybox.net/downloads/busybox-1.35.0.tar.bz2
Запускаем конфигуратор:
make menuconfig
и включаем статическую линковку:
Settings -> Build options -> Build static binary (no shared libs)
Собираем и устанавливаем. Для установки надо в CONFIG_PREFIX указать путь куда ставить:
make && make CONFIG_PREFIX=/path/to/install/busybox install
Допустим, мы установили busybox в каталог /tmp/build/bb/ramdisk. Если посмотреть на его содержание, увидим такую картину:
root@debian:/tmp/build/bb/ramdisk# ls -l
total 8
drwxr-xr-x 2 root root 4096 Apr 3 22:42 bin
lrwxrwxrwx 1 root root 11 Apr 3 22:42 linuxrc -> bin/busybox
drwxr-xr-x 2 root root 4096 Apr 3 22:42 sbin
drwxr-xr-x 4 root root 29 Apr 3 22:42 usr
Создадим точки монтирования для procfs и sysfs, каталог etc для конфигурационных файлов и dev для файлов устройств:
mkdir proc sys etc dev
В busybox присутствует mdev - аналог подсистемы udev. Его задача - динамически создавать и удалять файлы устройств в каталоге /dev. Подробнее о нем в документации https://git.busybox.net/busybox/plain/docs/mdev.txt.
В каталоге etc создадим конфигурационные файлы group, hosts, inittab, passwd:
# cat group
root:x:0:root
# cat hosts
127.0.0.1 localhost
# cat inittab
::sysinit:/etc/init.d/rc.S
console::respawn:-/bin/ash
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::halt:/bin/umount -a -r
# cat passwd
root::0:0:root:/root:/bin/ash
Также в etc создаем каталог init.d и в нем исполняемый файл rc.S:
#!/bin/sh
HOSTNAME=zero
VERSION=0.0.1
hostname $HOSTNAME
mount -n -t proc proc /proc
mount -n -t sysfs sysfs /sys
mount -n -t tmpfs mdev /dev
mdev -s
# Set PATH
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin
# Start other daemons
/sbin/syslogd
/usr/sbin/telnetd
Этот rc.S будет выполнен при старте системы. В результате будут смонтированы procfs в /proc, sysfs в /sys, tmpfs в /dev, и mdev создаст в /dev файлы устройств, которые он найдет в sysfs.
В rootfs необходимо в каталог /lib/modules добавить модули ядра, которые предварительно надо собрать. Об этом будет следующий пункт. По ссылке https://github.com/ubob74/net/raw/p1/files/ramdisk.tgz можно скачать готовый rootfs.
1.3. Ядро
Переходим к сборке ядра. Клонируем репозиторий и становимся на нужный комит:
git clone https://github.com/linux-sunxi/linux-sunxi.git
cd linux-sunxi
git checkout -b next c9a8f2f1da7b
Формируем конфигурационный файл для нашей платформы:
make sunxi_defconfig
Заходим в режим конфигурации и отключаем сборку сетевого драйвера, т.к. у нас будет свой:
make menuconfig
Device Drivers ->
Network Device Support ->
Ethernet driver support ->
STMicroelectronics devices
Собираем ядро и модули:
make -j4 zImage modules
Формируем Device Tree:
make sun8i-h2-plus-orangepi-zero.dtb
По итогу сборки получим сжатый бинарный образ ядра arch/arm/boot/zImage и дерево устройств (device tree blob) arch/arm/boot/dts/sun8i-h2-plus-orangepi-zero.dtb.
Устанавливаем собранные модули на rootfs:
make -j4 INSTALL_MOD_PATH=/path/to/install/busybox modules_install
После установки модулей пакуем rootfs и получаем сжатый образ initramfs.img:
cd /path/to/install/busybox
find . | cpio -o -H newc | gzip > ../newrootfs.img.gz
cd ..
mkimage -A arm -T ramdisk -C none -d newrootfs.img.gz initramfs.img
1.4. Загрузчик
Клонируем репозиторий:
git clone git://git.denx.de/u-boot.git
cd u-boot
Формируем конфигурационный файл и собираем:
make orangepi_zero_defconfig
make -j2
В итоге должны получить бинарный файл u-boot-sunxi-with-spl.bin.
1.5. Загрузочная sd карта
С помощью fdisk и mkfs формируем на sd карте раздел FAT32. Стартовый сектор раздела ставим 8192, т.к. перед разделом надо расположить загрузчик u-boot.
Создаем на sd карте каталог /boot и копируем туда собранные компоненты - бинарный образ ядра arch/arm/boot/zImage, дерево устройств arch/arm/boot/dts/sun8i-h2-plus-orangepi-zero.dtb и сжатый образ rootfs initramfs.img. Далее формируем файл сценария boot.cmd для u-boot:
# cat boot.cmd
load mmc 0 0x44000000 /boot/zImage
load mmc 0 0x46000000 /boot/sun8i-h2-plus-orangepi-zero.dtb
load mmc 0 0x48000000 /boot/initramfs.img
setenv bootargs console=ttyS0,115200 earlyprintk root=/dev/ram0 rdinit=/sbin/init rootwait panic=10
bootz 0x44000000 0x48000000 0x46000000
В этом файле мы указываем откуда, что и куда должен вгрузить u-boot и командную строку ядра. Корневая файловая система будет расположена на ramdisk. Теперь из boot.cmd формируем сценарий boot.src и копируем его на на sd карту:
mkimage -C none -A arm -T script -d boot.cmd boot.scr
В итоге на sd карте у нас должны находиться следующие файлы:
- boot.scr в корневом каталоге
- zImage, sun8i-h2-plus-orangepi-zero.dtb, initramfs.img в каталоге /boot
Например, sd карта смонтирована как устройство /dev/sdc1 в каталог /mnt/sdc:
# cd /mnt/sdc
# ls -l
total 8
drwxr-xr-x 2 root root 4096 Apr 10 19:22 boot
-rwxr-xr-x 1 root root 348 Mar 26 22:02 boot.scr
# ls -l boot
total 5896
-rwxr-xr-x 1 root root 1194170 Apr 9 19:31 initramfs.img
-rwxr-xr-x 1 root root 23332 Apr 1 20:33 sun8i-h2-plus-orangepi-zero.dtb
-rwxr-xr-x 1 root root 4813272 Apr 1 20:25 zImage
Последний этап подготовки sd карты - запись загрузчика. Тут надо действовать внимательно. Необходимо указать правильный файл устройства загрузочной sd карты, чтобы ничего не повредить. В нашем случае /dev/sdc. Переходим в каталог, в котором собирали u-boot, и выполняем команду:
dd if=./u-boot-sunxi-with-spl.bin of=/dev/sdc bs=1024 seek=8
Все готово, загрузчик записан. Загружаем систему - вставляем sd карту в слот платы и подаем питание, процесс загрузки отслеживаем через отладочную консоль. Можно приступать к разработке драйвера. Но вначале ознакомимся, что из себя представляет сетевой адаптер Ethernet.
2. Общая структура Ethernet сетевого адаптера
В состав Ethernet адаптера входят:
блок управления доступом к среде MAC (Media Access Controller)
блок DMA
приемный и передающий FIFO буферы
приемопередатчик PHY
Задача MAC - формирование кадров для передачи, инкапсуляция в них данных (payload), прием кадров, обработка адресной информации, фильтрация кадров, детектирование ошибок в данных, обнаружение коллизий (алгоритм CSMA/CD).
Задача DMA - обмен кадрами между системной памятью и MAC. DMA взаимодействует с MAC с помощью двух FIFO буферов - передающий (TX) и приемный (RX). Через TX FIFO передаваемый кадр считывается из системной памяти в MAC, а через RX FIFO наоборот - принятый кадр загружается из MAC в системную память.
Задача PHY - преобразование бинарного представления кадра в вид, пригодный для передачи по физическому носителю, т.е. по Ethernet кабелю, и наоборот - принятый по физическому носителю сигнал преобразовать в бинарный вид.
MAC соединяется с PHY с помощью двух интерфейсов - MII (Media Independent Interface) и SMI (Station Management Interface). К одному MAC можно подключить до 32-х PHY. Каждый PHY адресуется порядковым номером, размер адреса – 5 бит.
Через MII выполняется обмен данными между MAC и PHY, перечень и описание сигналов смотрите по ссылке.
SMI служит для управления PHY. Это двухпроводный интерфейс, состоящий из линии данных (Management Data Input/Output, MDIO) и линии тактирования (MDC). Через SMI можно установить режим работы PHY (дуплекс - полудуплекс), скорость обмена, проверить подключен или нет сетевой кабель и сделать другие полезные вещи. Выполняется все это с помощью регистров управления, входящих в состав PHY. Перечень регистров PHY и управляющие биты с функциональным назначением каждого можно посмотреть в заголовочном файле ядра <linux/mii.h>.
Для доступа к регистру указывается его адрес и адрес PHY. Размер адреса - 5 бит. Регистр PHY адресуется указанием его порядкового номер, также как и сам PHY. Адресная информация и данные передаются по линии MDIO. Формат MDIO кадра для режимов чтения и записи показан на рис. 5.
MAC также как и PHY содержит набор регистров. Их число и назначение определяет производитель. Рассматриваемый нами Ethernet MAC контроллер (далее EMAC) содержит набор регистров для управления устройством, полный перечень в п.8.9.3 "EMAC Core Register List" спецификации на плату. Каждый регистр адресуется указанием смещения относительно базового адреса EMAC.
Также для управления устройством потребуются блоки System Control и CCU. CCU - это Clock Control Unit, блок управления тактовыми генераторами. Он выполняет генерацию, распределение, синхронизацию и отключение сигналов тактовой частоты для всех компонентов платы.
3. Драйвер
В этой части разработка драйвера будет включать следующие шаги:
- подготовка к работе PHY
- подготовка к работе EMAC
- прием пакетов
- отображение результатов
3.1. Подготовка PHY
Подготовка PHY включает следующие шаги:
- подача тактовой частоты
- выполнить аппаратный сброс (reset)
- определить функции для работы с PHY
Подача частоты выполняется установкой бита 0 в регистре Bus Clock Gating Register 4, смещение 0x0070 относительно базового адреса CCU (п. 4.3.5.18 спецификации):
/* Set clock gating for EPHY - bit 0 in Bus Clock Gating Reg4, offset 0x70 */
volatile u32 val = readl(ccu_base_addr + CLK_GATING_REG4);
val |= BIT(EPHY_GATING);
writel(val, ccu_base_addr + CLK_GATING_REG4);
Аппаратный сброс выполняется сбросом, а затем установкой бита 2 в регистре Bus Software Reset Register 2, смещение 0x2C8 (п. 4.3.5.72):
/* Reset assert EPHY - bit 2 in Bus Software Reset Reg2, offset 0x2C8 */
val = readl(ccu_base_addr + SW_RST_REG2);
val &= ~BIT(EPHY_RST);
writel(val, ccu_base_addr + SW_RST_REG2);
/* Deassert EPHY */
val = readl(ccu_base_addr + SW_RST_REG2);
val |= BIT(EPHY_RST);
writel(val, ccu_base_addr + SW_RST_REG2);
Для обмена данными с PHY используются два регистра - регистр команд MII Command Register (MII_CMD) и регистр данных MII Data Register (MII_DATA). Оба входят в состав EMAC Core Register List. Смещение MII_CMD - 0x48, MII_DATA - 0x4C. В MII_CMD указываем адрес PHY, номер регистра PHY, значение делителя частоты для MDC и направление обмена - чтение или запись. В режиме записи в MII_DATA записываем передаваемые данные, в режиме чтения - считываем полученные от PHY данные. Перед каждой новой операцией ждем завершения предыдущей проверкой бита MII_BUSY - устанавливаем мы его сами, а сбрасывается он автоматически (аппаратно):
int phy_read(int phy_addr, int phy_reg)
{
int err;
u32 v, val = MII_BUSY;
int data;
val |= (phy_addr << MII_PHY_ADDR_SHIFT);
val |= (phy_reg << MII_PHY_REG_ADDR_SHIFT);
val |= (phy_mdc_div_ratio << MII_MDC_DIV_RATIO_SHIFT);
err = readl_poll_timeout(emac_base_addr + MII_CMD, v,
!(v & MII_BUSY), 100, 100000);
if (err)
return -EBUSY;
writel(val, emac_base_addr + MII_CMD);
err = readl_poll_timeout(emac_base_addr + MII_CMD, val,
!(val & MII_BUSY), 100, 100000);
if (err)
return -EBUSY;
data = (int)readl(emac_base_addr + MII_DATA);
return data;
}
int phy_write(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 |= (phy_mdc_div_ratio << MII_MDC_DIV_RATIO_SHIFT);
/* Wait until any existing MII operation is complete */
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);
err = readl_poll_timeout(emac_base_addr + MII_CMD, val,
!(val & MII_BUSY), 100, 100000);
if (err)
return -EFAULT;
return 0;
}
Выбор параметра phy_mdc_div_ratio - значение делителя частоты MDC
Разберемся с phy_mdc_div_ratio, какое значение принимает этот параметр.
Мы записываем его в 3-х битовое поле MDC_DIV_RATIO_M регистра MII_CMD. Оно задает значение делителя частоты MDC, и может принимать следующие значения:
|
|
|
|
|
|
|
|
|
|
Т.к. сигнал MDC формируется из частоты шины AHB1 (п. 8.9.4.15), то конкретное значение делителя зависит от номинала частоты AHB1 шины. Согласно стандарта 802.3 минимальный период тактового сигнала на линии MDC должен быть равен 400 нс, т.е. максимальная частота MDC равна 2.5 МГц. Значит должно выполнятся условие:
AHB1 / mdc_div_ratio = 2.5 МГц
Согласно схеме Bus Clock Tree (рис. 4-2 спецификации) формирователем частоты AHB1 является PLL_PERIPH0, и тогда номинал AHB1 вычисляется как:
AHB1 = PLL_PERIPH0 / AHB1_PRE_DIV / AHB1_CLK_DIV_RATIO
Значения делителей AHB1_PRE_DIV и AHB1_CLK_DIV_RATIO определяет CCU регистр AHB1_APB1_CFG_REG, а регистр PLL_PERIPH0_CTRL_REG - номинал частоты PLL_PERIPH0:
PLL Output = 24 MHz * N * K/2
Значения N и K определены в полях PLL_FACTOR_N и PLL_FACTOR_K регистра PLL_PERIPH0_CTRL_REG.
Для окончательного подсчета сдампим значения регистров AHB1_APB1_CFG_REG и PLL_PERIPH0_CTRL_REG:
AHB1_APB1_CFG_REG = 0x00003180
PLL_PERIPH0_CTRL_REG = 0x90041811
Биты 13 и 12 в AHB1_APB1_CFG_REG установлены, значит частота AHB1 формируется из PLL_PERIPH0/AHB1_PRE_DIV, мы изначально исходили из этого условия. Получаем значения делителей, устанавливаемых в AHB1_APB1_CFG_REG:
AHB1_CLK_DIV_RATIO = 1
AHB1_PRE_DIV = 3
Значения PLL_FACTOR_N и PLL_FACTOR_K:
PLL_FACTOR_N = 24, N = PLL_FACTOR_N + 1 = 25
PLL_FACTOR_K = 1, K = PLL_FACTOR_K + 1 = 2
Частота PLL:
PLL output = 24 MHz * N * K / 2 = 24 MHz * 25 * 2 / 2 = 600 MHz
600 MHz - такое же значение заявлено и в спецификации, мы это просто перепроверили. Теперь можно посчитать частоту AHB1:
AHB1 = PLL_PERIPH0 / AHB1_PRE_DIV = 600 MHz / 3 = 200 MHz
Вычисляем значение делителя MDC:
mdc_div_ratio = AHB1 / 2.5 MHz = 200 / 2.5 = 80
Какое значение выбрать? Обратимся за советом к FreeBSD драйверу, посмотрим как сделано там:
#define MDIO_FREQ 2500000
/* Determine MDC clock divide ratio based on AHB clock */
error = clk_get_freq(clk_ahb, &freq);
if (error != 0) {
device_printf(dev, "cannot get AHB clock frequency\n");
goto fail;
}
div = freq / MDIO_FREQ;
if (div <= 16)
sc->mdc_div_ratio_m = MDC_DIV_RATIO_M_16;
else if (div <= 32)
sc->mdc_div_ratio_m = MDC_DIV_RATIO_M_32;
else if (div <= 64)
sc->mdc_div_ratio_m = MDC_DIV_RATIO_M_64;
else if (div <= 128)
sc->mdc_div_ratio_m = MDC_DIV_RATIO_M_128;
Последнее условие нам подходит, поэтому выбираем номинал делителя 128, и в поле MDC_DIV_RATIO_M регистра MII_CMD записываем значение 3.
Перед началом работы выполняем программный сброс (soft-reset) PHY установкой бита BMCR_RESET в PHY регистре MII_BMCR (Basic mode control register). Установив этот бит, мы должны дождаться его сброса, опрашивая регистр MII_BMCR:
int phy_reset(void)
{
int ret = 0;
int phy_addr = PHY_ADDR;
int retries = 12;
phy_write(phy_addr, MII_BMCR, BMCR_RESET);
do {
msleep(50);
ret = phy_read(phy_addr, MII_BMCR);
if (ret < 0)
return ret;
} while (ret & BMCR_RESET && --retries);
if (ret & BMCR_RESET)
ret = -ETIMEDOUT;
return ret;
}
Теперь рассмотрим такой важный вопрос как согласования скорости и режима работы адаптера и порта роутера, к которому он подключается.
Тут можно применить два метода. Первый, самый простой - установить фиксированное значение скорости обмена и режима, такую же, как у роутера. Метод простой, но не удобный по очевидным причинам - нам надо заранее знать настройки порта роутера и при переключении на другой порт корректировать настройки адаптера. Поэтому применим метод автосогласования скоростей (auto-negotiation). Суть метода заключается в том, что адаптер и роутер обмениваются информацией о поддерживаемых режимах работы и выбирают оптимальный. Подробно вдаваться в детали процесса не будем, об этом можно почитать тут, а рассмотрим каким образом инициировать и выполнить процедуру автосогласования в драйвере.
PHY содержит регистры MII_BMSR и MII_ADVERTISE. MII_BMSR - это базовый регистр статуса (Basic mode status register), он содержит, помимо всего прочего, поддерживаемые режимы работы и скорости обмена, например:
#define BMSR_10HALF 0x0800 /* Can do 10mbps, half-duplex */
#define BMSR_10FULL 0x1000 /* Can do 10mbps, full-duplex */
#define BMSR_100HALF 0x2000 /* Can do 100mbps, half-duplex */
#define BMSR_100FULL 0x4000 /* Can do 100mbps, full-duplex */
Это неполный перечень режимов, подробнее в <linux/mii.h>.
MII_ADVERTISE - это Advertisement control register, через этот регистр адаптер может сообщить противоположной стороне о своих возможностях:
#define ADVERTISE_10HALF 0x0020 /* Try for 10mbps half-duplex */
#define ADVERTISE_10FULL 0x0040 /* Try for 10mbps full-duplex */
#define ADVERTISE_100HALF 0x0080 /* Try for 100mbps half-duplex */
#define ADVERTISE_100FULL 0x0100 /* Try for 100mbps full-duplex */
Записав нужные значения режимов в регистр MII_ADVERTISE, надо в регистре MII_BMCR установить флаги BMCR_ANRESTART и BMCR_ANENABLE для активизации режима автосогласования
#define BMCR_ANRESTART 0x0200 /* Auto negotiation restart */
#define BMCR_ANENABLE 0x1000 /* Enable auto negotiation */
и ждать завершения процесса, проверяя флаг BMSR_ANEGCOMPLETE в регистре MII_BMSR:
#define BMSR_ANEGCOMPLETE 0x0020 /* Auto-negotiation complete */
После установки флага читаем регистр MII_LPA Link - Partner Ability register. В нем будут установлены поддерживаемые режимы передачи и скорости, согласованные со второй стороной, например:
#define LPA_10HALF 0x0020 /* Can do 10mbps half-duplex */
#define LPA_10FULL 0x0040 /* Can do 10mbps full-duplex */
#define LPA_100HALF 0x0080 /* Can do 100mbps half-duplex */
#define LPA_100FULL 0x0100 /* Can do 100mbps full-duplex */
Оформим весь вышеописанный процесс в виде функций:
int phy_adjust_link(int *speed, int *duplex)
{
int i, status, adv;
status = phy_read(PHY_ADDR, MII_BMSR);
adv = phy_read(PHY_ADDR, MII_ADVERTISE);
/* Cleanup advertise */
adv &= ~ADVERTISE_ALL;
if (status & BMSR_100FULL)
adv |= ADVERTISE_100FULL;
if (status & BMSR_100HALF)
adv |= ADVERTISE_100HALF;
phy_write(PHY_ADDR, MII_ADVERTISE, adv);
phy_write(PHY_ADDR, MII_BMCR, BMCR_ANRESTART | BMCR_ANENABLE);
for (i = 0; i < 100; i++, msleep(50)) {
status = phy_read(PHY_ADDR, MII_BMSR);
if (status & BMSR_ANEGCOMPLETE) {
phy_aneg_complete(speed, duplex, status);
return 0;
}
}
return -1;
}
void phy_aneg_complete(int *speed, int *duplex, int status)
{
int lpa = phy_read(PHY_ADDR, MII_LPA);
*speed = SPEED10;
*duplex = HALF_DUPLEX;
if ((status & BMSR_100FULL) && (lpa & LPA_100FULL)) {
*speed = SPEED100;
*duplex = FULL_DUPLEX;
return;
}
if ((status & BMSR_100HALF) && (lpa & LPA_100HALF)) {
*speed = SPEED100;
*duplex = HALF_DUPLEX;
}
}
PHY готов к работе. Переходим к настройке EMAC.
3.2. Подготовка к работе EMAC
Подготовка EMAC включает в себя следующие шаги:
получение ресурсов адаптера - адрес ввода-вывода и номер прерывания
включение и аппаратный сброс
конфигурирование PHY
установка аппаратного адреса (MAC адрес)
формирование очереди для приема сетевых пакетов
настройка фильтрации пакетов
включение приемного тракта и разрешение прерываний
3.2.1. Получение ресурсов
Для работы с EMAC требуется получить номер прерывания и отобразить диапазон адресов ввода-вывода, начиная с базового адрес EMAC, на виртуальные адреса ядра, аналогично как было сделано для CCU и System Control:
emac_irq = platform_get_irq(pdev, 0);
emac_base_addr = devm_ioremap(&pdev->dev, EMAC_BASE_ADDR, EMAC_SIZE);
Необходимые ресурсы определены в Device Tree (полное описание см. в arch/arm/boot/dts/sunxi-h3-h5.dtsi):
emac: ethernet@1c30000 {
compatible = "allwinner,sun8i-h3-emac";
reg = <0x01c30000 0x10000>;
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "macirq";
3.2.2. Включение и аппаратный сброс
Подготовка к работе EMAC такая же как и для PHY - необходимо включить тактовую частоту и выполнить аппаратный сброс. Включаем тактовую частоту EMAС установкой бита 17 в CCU регистре Bus Clock Gating Register0, смещение 0x60 (п. 4.3.5.14):
/* Set clock gating for EMAC - bit 17 in Bus Clock Gating Reg0, offset 0x60 */
val = readl(ccu_base_addr + CLK_GATING_REG0);
val |= BIT(EMAC_GATING);
writel(val, ccu_base_addr + CLK_GATING_REG0);
Выполняем reset сбросом, а затем установкой бита 17 в регистре Bus Sotfware Reset Register0, смещение 0x2C0 (п. 4.3.5.70):
/* Reset assert EMAC - bit 17 in Bus Sotfware Reset Reg0, offset 0x2C0 */
val = readl(ccu_base_addr + SW_RST_REG0);
val &= ~BIT(EMAC_RST);
writel(val, ccu_base_addr + SW_RST_REG0);
/* Reset deassert EMAC */
val = readl(ccu_base_addr + SW_RST_REG0);
val |= BIT(EMAC_RST);
writel(val, ccu_base_addr + SW_RST_REG0);
3.2.3. Конфигурирование PHY
Теперь конфигурируем PHY - надо указать его адрес, тип, режим, частоту обмена при передаче данных и подать питание. Режим работы PHY - Normal, частота тактирования - 25 MHz, тип – внутренний (Internal), адрес - порядковый номер PHY, т.к. он единственный, то адрес равен 1. Эти параметры выставляем в регистре EMAC-EPHY Clock Register из System Control, смещение регистра 0x30 (п. 4.5.3.2):
val = readl(syscon_base_address + SYSCON_EMAC_CLK_REG);
/* Setup PHY address */
val |= (PHY_ADDR & 0x1F) << 20;
/* Select internal PHY */
val |= BIT(15);
/* Power up PHY */
if (val & BIT(16))
val &= ~BIT(16);
writel(val, syscon_base_addr + SYSCON_EMAC_CLK_REG);
Режим и частоту не выставляли явно, используем значения по умолчанию.
3.2.4. Установка аппаратного адреса (MAC адрес)
Аппаратный адрес хранится в регистрах MAC Address 0 High Register (смещение 0x50) и MAC Address 0 Low Register (offset 0x54). В первый регистр записываем старшие 16 бит адреса, во второй - младшие 32 бита.
void emac_set_mac_addr(u8 *addr)
{
u32 data = (addr[5] << 8) | addr[4];
writel(data, emac_base_addr + EMAC_ADDR_HIGH);
data = (addr[3] << 24) | (addr[2] << 16) | (addr[1] << 8) | addr[0];
writel(data, emac_base_addr + EMAC_ADDR_LOW);
}
Теперь готовим адаптер к приему пакетов. Но вначале немного о DMA операциях.
EMAC умеет выполнять DMA операции. Поступивший из PHY пакет блок DMA EMAC может самостоятельно положить в системную память и сообщить через прерывание о наличии новых данных. И наоборот - может забрать пакет из системной памяти и отправить его в PHY для дальнейшей передачи.
Блок DMA EMAC использует физические адреса для хранения пакета. Для доступа драйвера к данным необходимо отобразить физический адрес на виртуальный ядерный.
Для DMA операций ядро Linux поддерживает два типа отображения - постоянное (Consistent DMA mappings) и потоковое (Streaming DMA mappings). Постоянное отображение используется для данных, которые должны находиться в памяти на протяжении всего времени работы драйвера. Это отображение гарантирует одновременный доступ к данным как устройству, так и драйверу. Любое изменение данных видно без каких-либо дополнительных операций программной синхронизации (сброс буферов, кешей etc). Такое отображение еще называют синхронным.
Потоковое отображение создается для одной короткой транзакции, например прием сетевого пакета (наш случай). Для такого вида отображения есть одна характерная особенность - кто владеет данными. Когда отображение создано, данными владеет устройство. Драйвер явно должен запросить доступ к данным и вернуть его после обработки. Для этого есть спецальные API ядра, мы ниже рассмотрим как ими пользоваться.
Дополнительно о DMA можно прочитать в официальной документации ядра и тут.
3.2.5. Формирование очереди для приема сетевых пакетов
Чтобы EMAC умел выполнять DMA операции, его надо соответствующим образом настроить. Ему надо знать откуда брать и куда сохранять сетевые пакеты. Для хранения пакетов выделим буфер памяти. Адрес буфера сохраним в специальном дескрипторе. Буферов может быть несколько, соответственно, для каждого буфера будет свой дескриптор. Все дескрипторы формируют связаный список. Адрес начала списка хранится в регистре Receive DMA Descriptor List Address, смещение 0x34 относительно базового адреса EMAC (п. 8.9.4.11). Когда приходит пакет, DMA блок получает адрес списка дескрипторов из этого регистра, находит в списке свободный буфер и копирует в него пакет.
Дескриптор состоит из четырех 32-х разрядных полей. Первое поле содержит управляющие и контрольные флаги, второе - размер буфера для принятого пакета, третье - адрес буфера, четвертое - адрес следующего дескриптора в списке. Представить дескриптор можно в виде структуры:
struct dma_desc {
u32 status;
u32 st;
u32 buf_addr;
u32 next;
};
Получается такая картина:
В поле status важным является бит #31, RX_DESC_CTL. Этот бит определяет, может ли данный дескриптор быть использован блоком DMA, т.е. может ли DMA скопировать принятый пакет по адресу, на который указывает третье поле дескриптора. Если бит установлен, то дескриптор поступает в распоряжение DMA блока. При инициализации дескрипторов этот бит устанавливается.
Скопировав принятый пакет в буфер, DMA блок сбрасывает этот бит и генерирует прерывание. Обработчик прерывания драйвера найдет дескриптор с новым пакетом, ориентируясь на сброшенный бит. Обработав пакет, драйвер вернет дескриптор DMA блоку установкой бита RX_DESC_CTL.
Хранить принятый пакет будем в буфере сокета, который определен структурой sk_buff. Это основная структура в сетевом стеке ядра Linux, она содержит все данные о принимаемом или передаваемом пакете, включая заголовки всех уровней и другую информацию. Структура sk_buff определена в <linux/skbuff.h>, о ней можно прочитать тут и тут.
Более подробнее работу с этой структурой мы рассмотрим в следующей части, коснемся, в частности, такого вопроса как линеаризация, а пока нам интересно только поле data. Это поле - адрес буфера для принятого пакета. Так как буферов может быть несколько (RX_SIZE), то понадобится массив для хранения указателей на них:
struct sk_buff **sk_buff = kmalloc_array(RX_SIZE,
sizeof(struct sk_buff *), GFP_KERNEL);
for (i = 0; i < RX_SIZE; i++)
sk_buff[i] = alloc_skb(DMA_BUF_SIZE, GFP_ATOMIC);
А чтобы DMA блок имел доступ к буферу пакета, надо для каждого sk_buff[i]->data получить аппаратный адрес с помощью dma_map_single:
dma_addr_t *sk_buff_dma = kmalloc_array(RX_SIZE,
sizeof(dma_addr_t), GFP_KERNEL);
for (i = 0; i < RX_SIZE; i++) {
sk_buff_dma[i] = dma_map_single(dev, sk_buff[i]->data,
DMA_BUF_SIZE,DMA_FROM_DEVICE);
}
Тут мы используем потоковое DMA отображение, о котором говорили выше.
Теперь нам нужна память для хранения списка дескрипторов:
struct dma_desc *dma_rx = dma_alloc_coherent(dev,
RX_SIZE * sizeof(struct dma_desc), &dma_rx_phy, GFP_KERNEL);
Здесь используем постоянное отображение, т.к. список дескрипторов нужен на все время работы драйвера. dma_rx_phy - аппаратный адрес списка дескрипторов, который мы должны записать в регистр Receive DMA Descriptor List Address.
Инициализируем список дескрипторов. В каждом дескрипторе надо установить флаг RX_DESC_CTL, указать размер буфера, его адрес (аппаратный), и ссылку на следующий дескриптор в списке, последний элемент замыкаем на первый:
struct dma_desc *p;
dma_addr_t dma_phy = dma_rx_phy;
for (i = 0; i < RX_SIZE; i++) {
p = dma_rx + i;
p->status |= BIT(31);
p->st = DMA_BUF_SIZE;
p->buf_addr = sk_buff_dma[i];
dma_phy += sizeof(struct dma_desc);
p->next = dma_phy;
}
p->next = dma_rx_phy;
Передаем адаптеру адрес списка дескрипторов:
writel(dma_rx_phy, emac_base_addr + EMAC_RX_DMA_LIST);
Все готово к приему пакетов.
3.2.6. Структура net_device
Поговорим немного о структуре net_device. Хотя в этой части мы ее особо трогать не будем, но упомянуть надо, т.к. в дальнейшем она нам понадобится.
Эта структура является ключевой структурой драйвера сетевого адаптера. Она содержит кучу разной информации, такую как имя сетевого интерфейса, адреса ввода-вывода и номер прерывания, которые использует устройство, статистическая информация, функции обратного вызова (callbacks) для взаимодействия с сетевым стеком ядра и многое другое. Подробно с содержанием это стурктуры можно ознакомиться в заголовочном файле <linux/netdevice.h>.
Память под эту структуру выделяется на этапе инициализации драйвера вызовом alloc_etherdev. Параметр вызова - размер частных (private) данных. Частные данные – это, как правило, структура, которая содержит данные, необходимые для работы конкретного драйвера. В нашем примере частные данные это структура вида
struct emac_priv {
void __iomem *emac_base_addr;
int emac_irq;
struct net_device *ndev;
struct device *dev;
struct rx_queue rx_q;
};
struct rx_queue - эта структура описывает очередь приема сетевых пакетов:
struct rx_queue {
struct emac_priv *priv;
struct sk_buff **sk_buff;
dma_addr_t *sk_buff_dma;
struct dma_desc *dma_rx;
dma_addr_t dma_rx_phy;
unsigned int cur_rx;
};
Поля этой структуры нам знакомы, о них мы говорили выше, когда формировали очередь для приема пакетов, а cur_rx - это номер текущего дескриптора.
При выделении памяти функция alloc_etherdev выделяет sizeof(struct net_device) + private size байт, с учетом выравнивания, и частные данные располагаются сразу за структурой net_device. Для получения ссылки на эти данные используется макрос netdev_priv:
/**
* netdev_priv - access network device private data
* @dev: network device
*
* Get network device private data
*/
static inline void *netdev_priv(const struct net_device *dev)
{
return (char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN);
}
3.2.7. Прием пакетов
При приеме пакетов мы исходим из того, что устройство будет генерировать прерывание при поступлении пакета. Чтобы устройство могло сгенерировать прерывание, когда блок DMA поместит принятый пакет в выделенный буфер, в регистре разрешения прерываний (Interrupt Enable Register, п.8.9.4.4) установим бит RX_INT_EN. В результате при возникновении прерывания в регистре состояния прерываний (Interrupt Status Register, п.8.9.4.3) будет установлен бит RX_INT. Обработчик прерывания проверит этот бит и вызовет функцию-обработчик принятого пакета.
Устанавливаем обработчик прерывания, последний параметр - стуруктура net_device:
request_irq(emac_irq, emac_interrupt, IRQF_SHARED, "emac-irq", ndev);
Функция-обработчик прерывания:
static irqreturn_t emac_interrupt(int irq, void *dev_id)
{
int status;
struct net_device *ndev = (struct net_device *)dev_id;
struct emac_priv *priv = netdev_priv(ndev);
/* Read Interrupt Status Register and drop interrupt flags */
status = readl(emac_base_addr + EMAC_INT_STA);
writel(status, emac_base_addr + EMAC_INT_STA);
/* Check RX_INT flag */
if (status & EMAC_RX_INT)
emac_rx(priv);
return IRQ_HANDLED;
}
Здесь мы проверяем установку флага RX_INT и вызываем функцию-обработчик принятого пакета emac_rx:
static int emac_rx(struct emac_priv *priv)
{
struct rx_queue *rx_q = &priv->rx_q;
unsigned int entry = rx_q->cur_rx;
struct dma_desc *p;
int status, frame_len;
Функция принимает указатель на частные данные адаптера, получает указатель на очередь приема пакетов и номер текущего дескриптора. Далее адресуем текущий дескриптор из массива, и проверям, не занят ли он DMA блоком. Для этого смотрим бит RX_DESC_CTL в поле status. Если занят, то не трогаем его и выходим:
p = rx_q->dma_rx + entry;
status = p->status;
if (status & BIT(31))
return 0;
Определяем размер принятого пакета, это биты 29:16 поля status. Размер пакета не должен превышать 1536 байт (размер Ethernet кадра):
frame_len = (status & FRAME_LEN_MASK) >> FRAME_LEN_SHIFT;
if (frame_len > 1536)
goto out;
Если все хорошо, то получаем данные пакета. Данные будут в буфере sk_buff[entry]->data. Это виртуальный адрес, который отображен на физический sk_buff_dma[entry]. Отображение потоковое (см. выше), надо явно запрашивать доступ к данным:
dma_sync_single_for_cpu(priv->dev, rx_q->sk_buff_dma[entry],
frame_len, DMA_FROM_DEVICE);
Выводим в лог первые 30 байт принятых данных:
for (i = 0; i < 30; i++)
pr_cont("%.2x ", skb->data[i]);
После возвращаем доступ устройству:
dma_sync_single_for_device(priv->dev, rx_q->sk_buff_dma[entry],
frame_len, DMA_FROM_DEVICE);
Вычисляем следующий дескриптор и возвращаем текущий DMA блоку, установив бит 31 в поле status:
rx_q->cur_rx = (rx_q->cur_rx + 1) % RX_SIZE;
p->status |= BIT(31);
3.2.8. Фильтрация пакетов
В составе EMAC есть регистр Receive Frame Filter Regsiter (смещение 0x38, п.8.9.4.12), который управляет режимом фильтрации принимаемых пакетов. Опций фильтрации достаточно много, но нас пока интересует только прием широковещательных (broadcast) пакетов, для этого бит #17 надо сбросить в 0. По умолчанию значение регистра 0x0, поэтому оставим его в исходном состоянии.
3.2.9. Включение приемного тракта и разрешение прерываний.
Последний этап настройки. Активируем прием пакетов, установив бит #31 (RX_EN, Enable receiver) в регистре Receive Control 0 Register (п.8.9.4.9) и биты #30 и #1 в регистре Receive Control 1 Register (п.8.9.4.10). Бит #30 включает RX DMA, бит #1 устанавливает размер данных, копируемых из RX FIFO в системную память. Если бит установлен, то блок DMA скопирует данные их RX FIFO в память после получения целого пакета. Если бит сброшен, то DMA блок скопирует данные из FIFO в память при достижении установленного порога. Значение порога определяет поле RX_TH (threshold value of RX DMA FIFO). Принимать мы будем целый пакет, значение порога не используем.
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) | BIT(31);
writel(v, emac_base_addr + EMAC_RX_CTL1);
}
Прерывание разрешаем установкой бита RX_INT_EN (бит 8) в регистре Interrupt Enable Register (п. 8.9.4.4):
writel(EMAC_RX_INT_EN, emac_base_addr + EMAC_INT_EN);
4. Сборка и загрузка драйвера
Для сборки драйвера достаточно ввести команду make, предварительно определив переменную окружения KSRC, в которой указать путь к исходным текстам ядра. После сборки у нас появится файл net.ko. Его скопируем на загрузочную SD карту – ее можно извлечь после загрузки платы, т.к. rootfs расположена на ram-диске. Копируем net.ko на карту, вставляем ее обратно в слот и монтируем:
mount /dev/mmcblk0p1 /mnt && cd /mnt
Теперь загрузим драйвер и в логе ядра увидим примерное такую картину:
Лог загрузки драйвера
/mnt # insmod net.ko
[ 42.956146] net: loading out-of-tree module taints kernel.
[ 42.962760] net-test 1c30000.ethernet: CCU setup..
[ 42.967580] ccu_base_addr=d0890000 clk gating reg4: 00000000 clk gating reg0: 33800140 rst reg2: 00000004 rst reg0: 33824140
[ 42.978946] net-test 1c30000.ethernet: Syscon setup..
[ 42.984248] ccu_base_addr=d0890000 clk gating reg4: 00000000 clk gating reg0: 33800140 rst reg2: 00000004 rst reg0: 33824140
[ 42.996757] net-test 1c30000.ethernet: PHY setup..
[ 43.101561] net-test 1c30000.ethernet: syscon=d0892000 val=148000
[ 43.107664] net-test 1c30000.ethernet: syscon val=148000
[ 43.112971] ccu_base_addr=d0890000 clk gating reg4: 00000001 clk gating reg0: 33800040 rst reg2: 00000004 rst reg0: 33824040
[ 43.124290] net-test 1c30000.ethernet: EMAC hardware setup..
[ 43.129996] net-test 1c30000.ethernet: emac_irq=43
[ 43.134797] net-test 1c30000.ethernet: emac_base_addr=d08a0000
[ 43.140640] ccu_base_addr=d0890000 clk gating reg4: 00000001 clk gating reg0: 33820040 rst reg2: 00000004 rst reg0: 33824040
[ 43.247669] net-test 1c30000.ethernet: PHY reset done
[ 43.253515] net-test 1c30000.ethernet: 0: physid1=00000044 physid2=00001400 phyid=00441400
[ 43.264049] net-test 1c30000.ethernet: 1: physid1=00000044 physid2=00001400 phyid=00441400
[ 44.807690] ccu_base_addr=d0890000 clk gating reg4: 00000001 clk gating reg0: 33820040 rst reg2: 00000004 rst reg0: 33824040
[ 44.819059] net-test 1c30000.ethernet: EMAC reset done
[ 44.824524] net-test 1c30000.ethernet: rx_q: sk_buff=c1ee5300 sk_buff_dma=c1ee5340
[ 44.832143] net-test 1c30000.ethernet: rx_q: dma_rx=cf050000 dma_rx_phy=4f050000
[ 44.839594] net-test 1c30000.ethernet: 0: p=cf050000 skb=c1e48000 skb_dma=41f27700
[ 44.847174] net-test 1c30000.ethernet: 1: p=cf051000 skb=c1e480c0 skb_dma=41f26dc0
[ 44.854770] net-test 1c30000.ethernet: 2: p=cf052000 skb=c1e48180 skb_dma=41f26480
[ 44.862363] net-test 1c30000.ethernet: 3: p=cf053000 skb=c1e48240 skb_dma=41f25b40
[ 44.869962] 0: desc=cf050000 status=80000000 st=000007ff buf_addr=41f27700 next=4f051000
[ 44.878070] 1: desc=cf051000 status=80000000 st=000007ff buf_addr=41f26dc0 next=4f052000
[ 44.886158] 2: desc=cf052000 status=80000000 st=000007ff buf_addr=41f26480 next=4f053000
[ 44.894263] 3: desc=cf053000 status=80000000 st=000007ff buf_addr=41f25b40 next=4f050000
[ 44.902920] net-test 1c30000.ethernet: 02:42:8d:01:14:9b
[ 44.909078] phy_adjust_link: status=79ed advertise=de1
[ 44.914440] phy_adjust_link: advertise=de1
[ 46.467891] phy_aneg_complete: auto-neg complete, lpa=4de1
[ 46.473389] net-test 1c30000.ethernet: duplex full, speed 100Mb/s
[ 46.479516] net-test 1c30000.ethernet: EMAC_CTL0=0000000d
[ 46.484918] net-test 1c30000.ethernet: Frame control: v=00000000
Все готово к тестированию.
5. Тестирование.
Для тестирования напишем приложение, которое будет формировать широковещательные пакеты и передавать их в сеть. Из широковещательного в этих пакетах только адрес назначения, остальное место заполним простым паттерном, чтобы было видно что это наш пакет.
#include <linux/if_packet.h>
#include <linux/if_ether.h>
int main(void) {
int i;
int psock;
const char *ifname = "enp4s0"; /* интерфейс через который передаем пакеты */
unsigned char buf[32];
struct sockaddr_ll s_ll;
struct ifreq ifr;
memset(buf, 0, sizeof(buf));
for (i = 0; i < ETH_ALEN; i++)
buf[i] = 0xFF;
for (; i < sizeof(buf); i++)
buf[i] = 0xAC;
psock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
memset((void *)&s_ll, 0, sizeof(struct sockaddr_ll));
sprintf(ifr.ifr_name, "%s", ifname);
ioctl(psock, SIOCGIFINDEX, &ifr);
s_ll.sll_family = PF_PACKET;
s_ll.sll_protocol = htons(ETH_P_ALL);
s_ll.sll_ifindex = ifr.ifr_ifindex;
bind(psock, (struct sockaddr *)&s_ll, sizeof(struct sockaddr_ll));
for (;;sleep(2), sendto(psock, buf, sizeof(buf), 0, NULL, 0));
close(psock);
return 0;
}
Собрав и запустив тестовую утилиту (запускать с правами root), увидим как драйвер принимает наши пакеты:
[ 295.301141] Received data:
[ 295.303847] ff ff ff ff ff ff ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac ac 00 00 00 00 00 00 00 00 64 bytes
На этом первую часть статьи завершим. Мы рассмотрели все ранее запланированные вопросы, разобрали структуру сетевого адаптера, узнали как подготовить его к работе и принимать сетевые пакеты. Во второй части продолжим изучение драйвера, перейдем к использованию стандартных ядерных фреймворков clock, reset и пр., и разберем как передавать сетевые пакеты.
Материалы к статье:
исходные тексты драйвера из первой части git clone –b p1 https://github.com/ubob74/net.git
корневая файловая система https://github.com/ubob74/net/raw/p1/files/ramdisk.tgz
загрузочный образ SD карты https://github.com/ubob74/net/raw/p1/files/image.gz