Наши партнеры

UnixForum






Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

Ошибка базы данных: Table 'a111530_forumnew.rlf1_users' doesn't exist
На главную -> MyLDP -> Тематический каталог -> Программирование и алгоритмические языки в Linux

Пишем драйвер сетевого устройства для Linux

Оригинал: "Writing Network Device Drivers for Linux"
Автор: Мохан Лал Джангир (Mohan Lal Jangir)
Дата публикации: ноябрь 2008.
Перевод: Н.Ромоданов
Дата перевода: 19 октября 2009 г.

Введение

Настоящая статья была написана для тех, кто только начинает изучение ядра Linux и хочет знать о драйверах сетевых устройств. Предполагается, что читатель в должной мере знаком с языком C и средой Linux.

В настоящей статье описывается сетевой драйвер для сетевой платы RealTek 8139. Я выбрал чипсет RealTek по следующим двум причинам: Во-первых, компания RealTek бесплатно предоставляет технические спецификации на этот чипсет (спасибо, RealTek!). Во-вторых, он сравнительно дешев. В Индии его можно приобрести менее, чем за 300 рупий (приблизительно 7 долларов США).

Драйвер, представленный в статье, имеет минимум функций; он просто посылает и принимает пакеты и собирает некоторую статистику. Для ознакомления с полноценным драйвером, написанном на профессиональном уровне, пожалуйста, смотрите исходные тексты Linux.

Подготовка к разработке драйвера

Прежде, чем начать разработку драйвера, нам нужно для этого подготовить систему. Настоящая статья была написана и проверена для ядра Linux 2.4.18, в котором содержится исходный код драйвера чипсета RealTek8139. Может быть в ядре, с которым Вы работаете, драйвер включен в состав ядра, либо скомпилирован как модуль. Для того, чтобы избавиться от каких-либо сюрпризов, желательно собрать ядро, в котором не будет драйвера RealTek8139 ни в каком из вариантов. Если Вы не знаете, как откомпилировать ядро, я рекомендую обратиться по следующей ссылке.

С этого момента обсуждения предполагается, что у вас есть рабочее ядро, в котором нет драйвера RealTek8139. Вам также нужны технические спецификации на чипсет, которые Вы можете загрузить с http://www.realtek.com.tw/. Последнее, что вам нужно сделать при подготовке, это – правильно вставить сетевую плату в PCI разъем и теперь мы готовы идти дальше.

Настоятельно рекомендуется иметь книгу Rubini Linux Device Drivers в качестве справочника по API. В настоящий момент это лучший известный мне источник сведений для разработки драйверов устройств под Linux.

Начинаем разработку драйвера

Рассмотрим разработку драйвера поэтапно по следующим пунктам:

  1. Обнаружение устройства
  2. Включение устройства
  3. Что такое сетевые устройства
  4. Доступ к устройству не через шину
  5. Что такое конфигурационное адресное пространство PCI
  6. Инициализация net_device
  7. Какой механизм передачи используется в RealTek8139
  8. Какой механизм приема используется в RealTek8139
  9. Делаем устройство готовым к передаче пакетов
  10. Делаем устройство готовым к приему пакетов

Обнаружение устройства

В качестве первого шага нам нужно обнаружить устройство, которое нас интересует. В ядре Linux представлен богатый набор API для обнаружения устройств, использующих шину PCI (Plug & Play), но мы будет использовать самое простое API - pci_find_device.
#define REALTEK_VENDER_ID  0x10EC
#define REALTEK_DEVICE_ID   0x8139

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/stddef.h>
#include <linux/pci.h>
int init_module(void)
{
    struct pci_dev *pdev;
    pdev = pci_find_device(REALTEK_VENDER_ID, REALTEK_DEVICE_ID, NULL);
    if(!pdev)
        printk("<1>Device not found\n");
    else
        printk("<1>Device found\n");
    return 0;
}
Таблица 1: Обнаружение устройства

Каждый разработчик имеет уникальный, назначенный только ему идентификатор ID и назначает уникальный идентификатор ID каждому конкретному виду устройств. Макросы REALTEK_VENDER_ID и REALTEK_DEVICE_ID определяют эти идентификаторы ID. Вы можете найти эти значения в "PCI Configuration Space Table" в спецификациях RealTek8139.

Включение устройства

После того, как устройство обнаружено, то, прежде чем как-то с ним взаимодействовать, нам нужно его включить. Продолжим фрагмент кода, приведенный в таблице 1, который теперь обнаруживает устройство и включает его.
static struct pci_dev* probe_for_realtek8139(void) 
{
        struct pci_dev *pdev = NULL;
        /* Ensure we are not working on a non-PCI system *
        if(!pci_present( )) {
               LOG_MSG("<1>pci not present\n");
               return pdev;
        }

#define REALTEK_VENDER_ID  0x10EC
#define REALTEK_DEVICE_ID  0X8139

        /* Look for RealTek 8139 NIC */
        pdev = pci_find_device(REALTEK_VENDER_ID, REALTEK_DEVICE_ID, NULL);
        if(pdev) {
               /* device found, enable it */
               if(pci_enable_device(pdev)) {
                       LOG_MSG("Could not enable the device\n");
                       return NULL;
               }
               else
                       LOG_MSG("Device enabled\n");
        }
        else {
               LOG_MSG("device not found\n");
               return pdev;
        }
        return pdev;
}

int init_module(void) 
{
        struct pci_dev *pdev;
        pdev = probe_for_realtek8139();
        if(!pdev)
               return 0;

        return 0;
}
Таблица 2: Обнаружение и включение устройства
В таблице 2 функция probe_for_realtek8139 выполняет следующие задачи:
  • Проверяет, что мы работаем на PCI-совместимой системе
    • pci_present вернет NULL, если система не поддерживает PCI
  • Пытается найти устройство RealTek 8139 так, как это показано в таблице 1
  • Включает устройство (с помощью вызова функции pci_enable_device), если его найдет

Давайте на время приостановим процесс изучения кода драйвера; вместо этого мы рассмотрим несколько важных тем, чтобы понять, чем с точки зрения Linux является сетевое устройство. Мы рассмотрим сетевые устройства и разницу между вводом-выводом с отображением в память (memory-mapped I/O), вводом-выводом с отображением по портам (port-mapped I/O) и конфигурационным адресным пространством PCI (PCI configuration space).

Что такое сетевые устройства

Мы обнаружили устройство PCI и включили его, но в Linux сетевые устройства рассматриваются как интерфейсы в сетевом стеке. Для этого используется структура net_device. В таблице 3 перечисляются некоторые важные поля структуры net_device, которая будут использоваться далее в настоящей статье.
struct net_device 
{
  char *name;
  unsigned long base_addr;
  unsigned char addr_len;
  unsigned char dev_addr[MAX_ADDR_LEN];
  unsigned char broadcast[MAX_ADDR_LEN];
  unsigned short hard_header_len;
  unsigned char irq;
  int (*open) (struct net_device *dev);
  int (*stop) (struct net_device *dev);
  int (*hard_start_xmit) (struct sk_buff *skb,  struct net_device *dev);
  struct net_device_stats* (*get_stats)(struct net_device *dev);
  void *priv;
};
Таблица 3: Структура net_device

Хотя полей в этой структуре значительно больше, для нашего минимального драйвера вполне достаточно перечисленных. Рассмотрим их подробнее:

  • name – имя устройства. Если первый символ устройства равен null, то register_netdev назначает ему имя "ethn", где n – подходящий номер, Например, если в вашей системе уже есть eth0 и eth1, то ваше устройство будет поименовано как eth2.
  • base_addr – базовый адрес ввода/вывода. Мы обсудим адресацию ввода/вывода далее в настоящей статье.
  • addr_len – длина адреса платы (MAC адреса). Для Ethernet-интерфейсов она равна 6.
  • dev_addr – адрес платы (Ethernet-адрес или MAC-адрес).
  • broadcast – аппаратный адрес широковещательной передачи. Для Ethernet-интерфейсов – это FF:FF:FF:FF:FF:FF.
  • hard_header_len - "hardware header length" – количество восьмеричных символов, которые предваряют передаваемый пакет и идут перед заголовком IP или другой информацией протокола. Для Ethernet-интерфейсов длина hard_header_len равна 14.
  • irq – номер назначенного прерывания.
  • open – указатель на функцию, которая открывает устройство. Эта функция вызывается всякий раз, когда ifconfig активирует устройство (например, "ifconfig eth0 up"). Метод open должен регистрировать все необходимые системные ресурсы (порты ввода/вывода, IRQ, DMA и т.п.), включать устройство и увеличивать на единицу счетчик использования модуля.
  • stop – указатель на функцию, которая останавливает интерфейс. Эта функция вызывается всякий раз, когда ifconfig деактивирует устройство (например, "ifconfig eth0 down"). Метод stop освобождает все ресурсы, запрошенные функцией open.
  • hard_start_xmit – с помощью этой функции заданный пакет передается в сеть. Первым аргументом функции является указатель на структуру sk_buff. Структура sk_buff используется для хранения пакетов в сетевых стеках Linux. Хотя в этой статье не требуется детальное знание структуры sk_buff's, подробности можно найти по ссылке http://www.tldp.org/LDP/khg/HyperNews/get/net/net-intro.html.
  • get_stats – эта функция предоставляет статистику интерфейса. В выходных данных команды "ifconfig eth0" большая часть полей содержит данные из get_stats.
  • priv – приватные данные драйвера. Это личное поле драйвера и он может использовать его по своему усмотрению. Далее мы увидим, как наш драйвер будет использовать это поле для хранения данных, относящихся к PCI устройствам.

Хотя мы не упомянули все поля структуры net_device, пожалуйста, обратите внимание на то, что нет никаких ссылок на функцию, принимающую пакеты. Это делается обработчиком прерываний устройства, что мы также рассмотрим далее в настоящей статье.

Доступ к устройству не через шину

Замечание: Этот раздел был взят из книги Алана Кокса (Alan Cox) "Bus-Independent Device Accesses", которая доступна по ссылке http://tali.admingilde.org/linux-docbook/deviceiobook.pdf

Ввод-вывод с отображением в память (Memory-Mapped I/O)

Наиболее широко используемый способ ввода/вывода – ввод/вывод с отображением в память (memory-mapped I/O). Т.е. часть адресного пространства CPU интерпретируется не как адреса памяти, а используется для доступа к устройству. В некоторых системах с определенной архитектурой требуется, чтобы устройства имели фиксированные адреса, но в большинстве систем имеется некоторый способ обнаружения устройств. Хорошим примером такой схемы является обход шины PCI. В настоящей статье не рассматривается, как получить такой адрес, но предполагается, что изначально он у вас есть.

Физический адрес является беззнаковым числом типа long. Эти адреса не используются напрямую. Вместо этого для того, чтобы получить адрес, который можно было передать в функцию так, как это описано ниже, вам следует вызвать ioremap. В ответ Вы получите адрес, который можно использовать для доступа к устройству.

После того, как Вы закончите использовать устройство (скажем, в вашей подпрограмме выхода из модуля), вызовите iounmap для того, чтобы вернуть ядру адресное пространство. Архитектура большинства систем позволяет выделять новое адресное пространство каждый раз, когда Вы вызываете ioremap, и использовать его до тех пор, пока Вы не вызовете iounmap.

Доступ к устройству

Часть интерфейса, наиболее используемая драйверами, это чтение из регистров устройства, отображаемых в память, и запись в них. Linux предоставляет интерфейс для чтения и записи блоков размером 8, 16, 32 или 64 бита. Исторически сложилось так, что они называются доступом к байту (byte), к слову (word), к длинному числу (long) и к двойному слову или четверке слов (quad). Названия функций следующие - readb, readw, readl, readq, writeb, writew, writel и writeq.

Для некоторых устройств (работающих, например, с буферами кадров) было бы удобнее за один раз передавать блоки, значительно большие чем 8 байтов. Для этих устройств предлагается использовать функции memcpy_toio, memcpy_fromio и memset_io. Не используйте memset или memcpy для работы с адресами ввода/вывода; они не гарантируют копирование данных в правильном порядке.

Работа функций чтения и записи должна происходить в определенном порядке. Т.е. компилятору не разрешается выполнять переупорядочивание последовательностей ввода-вывода. Если компилятору разрешается оптимизировать порядок, то Вы можете использовать функцию __readb и ей подобные с тем, чтобы не требовать строгого сохранения порядка выполнения операций. Пользуйтесь этим с осторожностью. Операция rmb блокирует чтение памяти. Операция wmb блокирует запись в память.

Хотя, по определению, основные функции синхронны относительно друг друга, устройства, которые установлены в шинах, сами по себе асинхронны. В частности многим авторам драйверов неудобно, что запись в PCI шину осуществляется асинхронно. Они должны выполнить операцию чтения из устройства с тем, чтобы удостовериться, что запись была сделана так, как хотел автор. Эта особенность скрыта от авторов драйверов в API.

Доступ к пространству портов

Еще один широко применяемый вариант ввода-вывода, это - пространство портов. Это диапазон адресов, отличающихся от адресного пространства обычной памяти. Доступ к этим адресам обычно не столь быстр, поскольку эти адреса отображаются в адреса памяти, к тому же пространство портов потенциально меньше адресного пространства.

В отличие от ввода-вывода с отображением в память, для доступа к пространству портов подготовка не требуется.

Доступ к пространству портов или устройства с отображением ввода-вывода

Доступ к этому пространству обеспечивается с помощью набора функций, в которых допускается доступ к 8, 16 и 32 битам, известных как байт (byte), слово (word ) и длинное слово (long). Это следующие функции - inb, inw, inl, outb, outw и outl.

Эти функции имеют несколько вариантов. Для некоторых устройств требуется, чтобы доступ к их портам происходил со сниженной скоростью. Эта функциональность обеспечивается при помощи добавления _p в конце команды. Имеются также эквиваленты команды memcpy. Функции ins и out копируют байты, слова и длинные слова в заданный порт и из него.

Что такое конфигурационноеадресное пространство PCI (PCI Configuration Space)

В этом разделе мы рассмотрим конфигурационное адресное пространство PCI. Устройства PCI имеют 256 байтное адресное пространство. Первые 64 байта используются стандартным образом, тогда как использование оставшихся байтов зависит от устройства. На рис.1. показано стандартное конфигурационное адресное пространство PCI

(Прим. пер.: В оригинале статьи Рис. 1 пропущен. Краткое описание конфигурационного адресного пространства PCI на русском языке можно найти в Википедии http://ru.wikipedia.org/wiki/PCI_configuration_space. Недостающий рисунок был взят по ссылке http://en.wikipedia.org/wiki/File:Pci-config-space.svg.)

Конфигурационное адресное пространство

Рис.1: Конфигурационное адресное пространство

Поля "Vendor ID" и "Device ID" содержат уникальные идентификаторы, присвоенные разработчику устройств и устройству соответственно. Мы уже обсуждали их в разделе "Обнаружение устройства". Отметим еще поле - "Base Address Registers" (базовые адресные регистры), известное еще как BAR. Мы кратко расскажем, как использовать регистры BAR.

Инициализация net_device

Теперь можно вернуться к разработке драйвера. Перед этим я напомню вам о поле priv в структуре net_device. Мы объявим структуру, в которой будут храниться приватные данные вашего устройства, а указатель на эту структуру должен храниться в поле priv. В структуре должны быть следующие поля (по мере разработки мы будет добавлять в структуру новые компоненты).
struct rtl8139_private 
{
        struct pci_dev *pci_dev;  /* PCI device */
        void *mmio_addr; /* memory mapped I/O addr */
        unsigned long regs_len; /* length of I/O or MMI/O region */
};

Таблица 4: Структура rtl8139_private

В остальной части функции init_module мы теперь определяем указатель net_device и инициализируем его.
#define DRIVER "rtl8139"
static struct net_device *rtl8139_dev;

static int rtl8139_init(struct pci_dev *pdev, struct net_device **dev_out) 
{
        struct net_device *dev;
        struct rtl8139_private *tp;

        /* 
         * alloc_etherdev allocates memory for dev and dev->priv.
         * dev->priv shall have sizeof(struct rtl8139_private) memory
         * allocated.
         */
        dev = alloc_etherdev(sizeof(struct rtl8139_private));
        if(!dev) {
               LOG_MSG("Could not allocate etherdev\n");
               return -1;
        }

        tp = dev->priv;
        tp->pci_dev = pdev;
        *dev_out = dev;

        return 0;
}

int init_module(void) 
{
        struct pci_dev *pdev;
        unsigned long mmio_start, mmio_end, mmio_len, mmio_flags;
        void *ioaddr;
        struct rtl8139_private *tp;
        int i;

        pdev = probe_for_realtek8139( );
        if(!pdev)
               return 0;

        if(rtl8139_init(pdev, &rtl8139_dev)) {
               LOG_MSG("Could not initialize device\n");
               return 0;
        }

        tp = rtl8139_dev->priv; /* rtl8139 private information */
        
        /* get PCI memory mapped I/O space base address from BAR1 */
        mmio_start = pci_resource_start(pdev, 1);
        mmio_end = pci_resource_end(pdev, 1);
        mmio_len = pci_resource_len(pdev, 1);
        mmio_flags = pci_resource_flags(pdev, 1);

        /* make sure above region is MMI/O */
        if(!(mmio_flags & I/ORESOURCE_MEM)) {
               LOG_MSG("region not MMI/O region\n");
               goto cleanup1;
        }
        
        /* get PCI memory space */
        if(pci_request_regions(pdev, DRIVER)) {
               LOG_MSG("Could not get PCI region\n");
               goto cleanup1;
        }

        pci_set_master(pdev);

        /* ioremap MMI/O region */
        ioaddr = ioremap(mmio_start, mmio_len);
        if(!ioaddr) {
               LOG_MSG("Could not ioremap\n");
               goto cleanup2;
        }

        rtl8139_dev->base_addr = (long)ioaddr;
        tp->mmio_addr = ioaddr;
        tp->regs_len = mmio_len;

        /* UPDATE NET_DEVICE */

        for(i = 0; i < 6; i++) {  /* Hardware Address */
               rtl8139_dev->dev_addr[i] = readb(rtl8139_dev->base_addr+i);
               rtl8139_dev->broadcast[i] = 0xff;
        }
        rtl8139_dev->hard_header_len = 14;

        memcpy(rtl8139_dev->name, DRIVER, sizeof(DRIVER)); /* Device Name */
        rtl8139_dev->irq = pdev->irq;  /* Interrupt Number */
        rtl8139_dev->open = rtl8139_open;
        rtl8139_dev->stop = rtl8139_stop;
        rtl8139_dev->hard_start_xmit = rtl8139_start_xmit;
        rtl8139_dev->get_stats = rtl8139_get_stats;

        /* register the device */
        if(register_netdev(rtl8139_dev)) {
               LOG_MSG("Could not register netdevice\n");
               goto cleanup0;
        }

        return 0;
}

Таблица 5: Инициализация net_device

Теперь давайте объясним, что мы сделали в таблице 5. Функцию probe_for_realtek8139 мы уже рассматривали. Функция rtl8139_init распределяет память для локального указателя rtl8139_dev, который мы должны использовать как net_device. Вдобавок эта функция заполняет компоненту pci_dev of rtl8139_private для обнаруженного устройства.

Наша следующая цель – получить поле base_addr для net_device. Это начало памяти регистров устройства. Мы напишем драйвер только для ввода-вывода с отображением в память. Чтобы получить базовый адрес для ввода-вывода с отображением в память, мы используем PCI API такие как pci_resource_start, pci_resource_end, pci_resource_len, pci_resource_flags и т.п. Эти API позволяют нам читать конфигурационное пространство PCI, не зная о деталях его реализации. Второй аргумент в этих API – номер BAR. Если Вы видели спецификации RealTek8139, то знаете, что первый BAR (нумеруемый как 0) - I/OAR, тогда как второй BAR (нумеруемый как 1) - MEMAR. Поскольку в этом драйвере используется ввод-вывод с отбражением в память, то в качестве второго аргумента мы передаем 1. Прежде, чем получить доступ к адресам, возвращаемыми указанными выше API, мы должны сделать две вещи. Во-первых, зарезервировать в драйвере указанные выше ресурсы (пространство памяти); это делается с помощью вызова функции pci_request_regions. Во-вторых, отобразить адреса ввода-вывода, описанные в этом разделе ранее, так, чтобы они использовались при вводе-выводе с отображением в память. Адрес io_addr назначается компоненте base_addr в структуре net_device, и это то место, откуда мы можем начинать читать регистры устройства или писать в них.

В оставшейся части кода, приведенного в таблице 5, выполняется обычная инициализация структуры net_device. Заметьте, что теперь мы читаем аппаратный адрес из устройства и записываем его в dev_addr. Если Вы смотрели раздел "Описания регистров" в спецификации RealTek8139, то знаете, что первые 6 байтов являются аппаратным адресом устройства. Также мы должны иметь проинициализированные компоненты указателя на функцию, но не должны определять какую-либо соответствующую функцию. Теперь для компиляции модуля мы добавим фиктивные функции.

static int rtl8139_open(struct net_device *dev) { LOG_MSG("rtl8139_open is
called\n"); return 0; }

static int rtl8139_stop(struct net_device *dev) 
{
        LOG_MSG("rtl8139_open is called\n");
        return 0;
}

static int rtl8139_start_xmit(struct sk_buff *skb, struct net_device *dev) 
{
        LOG_MSG("rtl8139_start_xmit is called\n");
        return 0;
}

static struct net_device_stats* rtl8139_get_stats(struct net_device *dev) 
{
        LOG_MSG("rtl8139_get_stats is called\n");
        return 0;
}

Таблица 6: Фиктивные функции

Обратите внимание, что в init_module пропущена часть, обрабатывающая ошибки. Вы можете написать эту обработку, заглянув для этого в модуль cleanup_module, приведенный ниже:

void cleanup_module(void) 
{
        struct rtl8139_private *tp;
        tp = rtl8139_dev->priv;

        iounmap(tp->mmio_addr);
        pci_release_regions(tp->pci_dev);

        unregister_netdev(rtl8139_dev);
        pci_disable_device(tp->pci_dev);
        return;
}

Таблица 7: Функция cleanup_module

Теперь мы имеем готовый фиктивный драйвер или драйвер-шаблон. Откомпилируйте модуль и вставьте его в ядро так, как показано в таблице 8 (предполагается, что исходный код ядра - /usr/src/linux-2.4.18 ).

  gcc   - c  rtl8139.c   - D__KERNEL__  -DMODULE   - I /usr/src/linux-2.4.18/include
  insmod  rtl8139.o

Таблица 8: Компиляция драйвера

Теперь выполним последовательность команд "ifconfig", "ifconfig - a", "ifconfig rtl8139 up", "ifconfig" и "ifconfig rtl8139 down" и посмотрим на результат. Эти команды покажут, когда каждая функция вызывается. Если все в порядке, то когда вы выполняете команду "ifconfig - a", вы должны обнаружить устройство rtl8139 и должны получить сообщение "function rtl8139_get_stat called ". Когда выполняете команду "ifconfig rtl8139 up", Вы должны получить сообщение "function rtl8139_open called". Аналогичным образом, когда Вы выполняете команду "ifconfig rtl8139 down", Вы должны получить сообщение "function rtl8139_stop called".

Снова остановим разработку драйвера и попытаемся лучше понять, какой механизм передачи и приема данных используется в устройстве.

Какой механизм передачи используется в RealTek 8139

В этом разделе описывается механизм передачи данных в устройстве RealTek8139; однако рекомендуется загрузить руководство по программированию "RTL8139 (A/B) Programming Guide", в котором приведены все подробности. В RealTek8139 имеется 4 дескриптора передачи, каждый дескриптор имеет фиксированное смещение адреса ввода-вывода. Четыре дескриптора используются циклически. Это означает, что для передачи четырех пакетов драйвер будет использовать в циклическом порядке дескриптор 0, дескриптор 1, дескриптор 2 и дескриптор 3. Для передачи следующего пакета драйвер снова будет использовать дескриптор 0 (при условии, что он свободен). В спецификациях RealTek8139 в разделе "Описание регистров" указывается, что регистры TSAD0, TSAD1, TSAD2 и TSAD3 имеют смещения 0x20, 0x24, 0x28, 0x2C, соответственно. В этих регистрах хранится "начальный адрес дескрипторов передачи ", т.е. в них хранится стартовый адрес (в памяти) пакетов, которые должны быть переданы. Позже устройство считает содержимое пакетов из этих адресов DMA, перепишет их в свой собственный стек FIFO, а затем выполнит передачу данных в сеть.

Мы скоро увидим, что этот драйвер выделяет память прямого доступа (доступ DMA), где будут храниться пакеты, и записывает адрес этой памяти в регистры TSAD.

Какой механизм приема используется в RealTek 8139

Приемная часть RTL8139 спроектирована как кольцевой буфер (линейная память, управление которой осуществляется как кольцевой памятью). Всякий раз, когда устройство принимает пакет, содержимое пакета запоминается в память кольцевого буфера и изменяется место, куда будет записываться содержимое следующего пакета (начальный адрес первого пакета + длина первого пакета). Устройство продолжает так запоминать пакеты до тех пор, пока не исчерпается линейная память. В этом случае устройство снова начинает запись с начального адреса линейной памяти, реализуя, таким образом, кольцевой буфер.

Делаем устройство готовым к передаче пакетов

В этом разделе мы рассмотрим исходный код драйвера, переводящего устройство в режим готовности передачи. Обсуждение кода, связанного с приемом пакетов, отложим до следующего раздела. В этом разделе мы рассмотрим функции rtl8139_open и rtl8139_stop. Сначала мы добавим в нашу структуру rtl8139_private поля, в которых будут храниться данные, связанные с передачей пакетов.
#define NUM_TX_DESC 4
struct rtl8139_private 
{
        struct pci_dev *pci_dev;  /* PCI device */
        void *mmio_addr; /* memory mapped I/O addr */
        unsigned long regs_len; /* length of I/O or MMI/O region */
        unsigned int tx_flag;
        unsigned int cur_tx;
        unsigned int dirty_tx;
        unsigned char *tx_buf[NUM_TX_DESC];   /* Tx bounce buffers */
        unsigned char *tx_bufs;        /* Tx bounce buffer region. */
        dma_addr_t tx_bufs_dma;
};
Таблица 9: Структура rtl8139_private
Компонента tx_flag должна содержать флаги передачи, уведомляющие устройство о некоторых параметрах, о которых мы скоро расскажем. Поле cur_tx должно хранить текущий дескриптор передачи, а dirty_tx указывает на первый из дескрипторов передачи, для которых передача еще не завершена (это означает, что мы можем использовать эти занятые дескрипторы для следующей передачи пакетов до тех пор, пока предыдущий пакет не будет полностью передан). В массиве tx_buf хранятся адреса четырех "дескрипторов передачи". Как мы скоро увидим, поле tx_bufs используется для тех же самых целей. В tx_buf и в tx_bufs должен храниться именно виртуальный адрес, который может использоваться драйвером, но устройство не может использовать такие адреса. Устройству для доступа нужен физический адрес, который запоминается в поле tx_bufs_dma. Ниже приведен список смещений регистров, используемых в исходном коде. Более подробную информацию об этих значениях Вы можете получить из спецификаций RealTek8139.
#define TX_BUF_SIZE  1536  /* should be at least MTU + 14 + 4 */
#define TOTAL_TX_BUF_SIZE  (TX_BUF_SIZE * NUM_TX_SIZE)

/* 8139 register offsets */
#define TSD0          0x10
#define TSAD0       0x20
#define RBSTART  0x30
#define CR               0x37
#define CAPR         0x38
#define IMR            0x3c
#define ISR             0x3e
#define TCR           0x40
#define RCR           0x44
#define MPC           0x4c
#define MULINT    0x5c

/* TSD register commands */
#define TxHostOwns    0x2000
#define TxUnderrun    0x4000
#define TxStatOK      0x8000
#define TxOutOfWindow 0x20000000
#define TxAborted     0x40000000
#define TxCarrierLost 0x80000000

/* CR register commands */
#define RxBufEmpty 0x01
#define CmdTxEnb   0x04
#define CmdRxEnb   0x08
#define CmdReset   0x10

/* ISR Bits */
#define RxOK       0x01
#define RxErr      0x02
#define TxOK       0x04
#define TxErr      0x08
#define RxOverFlow 0x10
#define RxUnderrun 0x20
#define RxFIFOOver 0x40
#define CableLen   0x2000
#define TimeOut    0x4000
#define SysErr     0x8000

#define INT_MASK (RxOK | RxErr | TxOK | TxErr | \
               RxOverFlow | RxUnderrun | RxFIFOOver | \
               CableLen | TimeOut | SysErr)

Таблица 10: Определения регистров RTL 8139

Рассмотрим функцию rtl8139_open с учетом сделанного выше определения:
static int rtl8139_open(struct net_device *dev) 
{
        int retval;
        struct rtl8139_private *tp = dev->priv;

        /* get the IRQ
         * second arg is interrupt handler
         * third is flags, 0 means no IRQ sharing
         */
        retval = request_irq(dev->irq, rtl8139_interrupt, 0, dev->name, dev);
        if(retval)
               return retval;

        /* get memory for Tx buffers
         * memory must be DMAable
         */
        tp->tx_bufs = pci_alloc_consistent(
                       tp->pci_dev, TOTAL_TX_BUF_SIZE, &tp->tx_bufs_dma);
        
        if(!tp->tx_bufs) {
               free_irq(dev->irq, dev);
               return -ENOMEM;
        }

        tp->tx_flag = 0;
        rtl8139_init_ring(dev);
        rtl8139_hw_start(dev);
        
        return 0;
}

static void rtl8139_init_ring (struct net_device *dev)
{
        struct rtl8139_private *tp = dev->priv;
        int i;

        tp->cur_tx = 0;
        tp->dirty_tx = 0;

        for (i = 0; i < NUM_TX_DESC; i++)
               tp->tx_buf[i] = &tp->tx_bufs[i * TX_BUF_SIZE];
        
        return;
}

static void rtl8139_hw_start (struct net_device *dev)
{
        struct rtl8139_private *tp = dev->priv;
        void *ioaddr = tp->mmio_addr;
        u32 i;

        rtl8139_chip_reset(ioaddr);

        /* Must enable Tx before setting transfer thresholds! */
        writeb(CmdTxEnb, ioaddr + CR);

        /* tx config */
        writel(0x00000600, ioaddr + TCR); /* DMA burst size 1024 */

        /* init Tx buffer DMA addresses */
        for (i = 0; i < NUM_TX_DESC; i++) {
               writel(tp->tx_bufs_dma + (tp->tx_buf[i] - tp->tx_bufs),
                                      ioaddr + TSAD0 + (i * 4));
        }

        /* Enable all known interrupts by setting the interrupt mask. */
        writew(INT_MASK, ioaddr + IMR);

        netif_start_queue (dev);
        return;
}

static void rtl8139_chip_reset (void *ioaddr)
{
        int i;

        /* Soft reset the chip. */
        writeb(CmdReset, ioaddr + CR);

        /* Check that the chip has finished the reset. */
        for (i = 1000; i > 0; i--) {
               barrier();
               if ((readb(ioaddr + CR) & CmdReset) == 0)
                       break;
               udelay (10);
        }
        return;
}

Таблица 11: Пишем функцию открытия устройства

Поясним код, приведенный в таблице 11. Функция function rtl8139_open начинается с запроса IRQ, что делается вызовом API request_irq. В этой функции мы регистрируем обработчик прерываний rtl8139_interrupt. Эта функция будет вызываться ядром всякий раз, когда устройство генерирует прерывание. Теперь мы выделим память, куда будут записываться исходящие пакеты, прежде чем они будут переданы в сеть. Обратите внимание, что API pci_allocate_consistant возвращает виртуальный адрес ядра. Физический адрес возвращается в третьем аргументе, который потом будет использован драйвером. Также обратите внимание на то, что мы выделяем память для всех четырех дескрипторов. Функция rtl8139_init_ring распределяет эту память для четырех дескрипторов. Теперь мы вызываем функцию rtl8139_hw_start, которая делает устройство готовым для передачи пакетов. Сначала мы перезагружаем устройство для того, чтобы оно было в предсказуемом и известном состоянии. Это делается с помощью записи в регистр команд CR (Command Register) значения reset (описано в спецификациях). Мы подождем, пока записанное значение можно будет считать обратно, что означает, что устройство перезагружено. Следующая функция, barrier (), вызывается с тем, чтобы ядро выделило память, требуемую для ввода-выхода, без какой-либо оптимизации. Поскольку устройство перезагружено, мы можем включить в устройстве режим передачи, записав для этого в регистр CR значение transmission enable (включена передача). Затем мы сконфигурируем регистр TCR (Transmission Configuration Register - конфигурационный регистр передачи). Единственное, что мы установим в регистре TCR , это - "Max DMA Burst Size per Tx DMA Burst" (максимальный размер буфера DMA для каждого сохраняемого в DMA пакета). Все остальные значения мы оставим установленными по умолчанию (подробности смотрите в спецификациях). Теперь мы запишем DMA адрес всех четырех дескрипторов в регистры TSAD (Transmission Start Address Descriptor – дескриптор начального адреса передачи). Далее мы включаем прерывания, сделав для этого запись в регистр IMR (Interrupt Mask Register – регистр маски прерывания). Этот регистр позволит нам задать, какие прерывания будет генерировать устройство. Наконец, мы вызываем netif_start_queue с тем, чтобы сообщить ядру, что устройство готово. Осталось лишь написать функцию rtl8139_interrupt. Давайте сейчас это пропустим. Устройство уже готово посылать пакеты, но функция, которая посылает пакеты, отсутствует (вспомните hard_start_xmit). Так что давайте ее напишем.
static int rtl8139_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
        struct rtl8139_private *tp = dev->priv;
        void *ioaddr = tp->mmio_addr;
        unsigned int entry = tp->cur_tx;
        unsigned int len = skb->len;
#define ETH_MIN_LEN 60  /* minimum Ethernet frame size */
        if (len < TX_BUF_SIZE) {
               if(len < ETH_MIN_LEN)
                       memset(tp->tx_buf[entry], 0, ETH_MIN_LEN);
               skb_copy_and_csum_dev(skb, tp->tx_buf[entry]);
               dev_kfree_skb(skb);
        } else {
               dev_kfree_skb(skb);
               return 0;
        }
        writel(tp->tx_flag | max(len, (unsigned int)ETH_MIN_LEN), 
                       ioaddr + TSD0 + (entry * sizeof (u32)));
        entry++;
        tp->cur_tx = entry % NUM_TX_DESC;

        if(tp->cur_tx == tp->dirty_tx) {
               netif_stop_queue(dev);
        }
        return 0;
}

Таблица 12: Пишем функцию start_xmit

Функция rtl8139_start_xmit, показанная в таблице 12, исключительно тривиальная. Сначала она ищет имеющийся дескриптор передачи, а затем проверяет, чтобы размер пакета был, по меньшей мере, 60 байтов (поскольку размер пакетов Ethernet не может быть меньше 60 байтов). Как только это будет сделано, будет вызвана функция skb_copy_and_csum_dev, которая скопирует содержимое пакетов в имеющуюся память DMA. Следующей функцией writel мы информируем устройство о длине пакета. После этого пакеты передаются в сеть. Затем мы определяем имеющиеся в наличии следующие дескрипторы передачи и, если так случится, что он будет равен дескриптору, для которого передача еще не завершена, то мы остановим устройство; в противном случае мы просто выйдем из функции.

Теперь наше устройство готово отсылать пакеты (помните, что мы еще не можем принимать пакеты). Откомпилируйте драйвер и попытайтесь послать пакеты ping. На другом конце Вы должны увидеть несколько пакетов ARP. Даже если удаленные хосты будут отвечать на пакеты ARP, они для нас будут бесполезными, поскольку мы не готовы их принимать.

Делаем устройство готовым к приему пакетов

Теперь мы сделаем так, чтобы устройство могло принимать пакеты. Для этого мы снова взглянем на уже ранее рассмотренные функции, а затем рассмотрим обработчик прерываний. Сначала мы добавим в структуру rtl8139_private переменные, которые нужны для приема пакетов.
struct rtl8139_private 
{
        struct pci_dev *pci_dev;  /* PCI device */
        void *mmio_addr; /* memory mapped I/O addr */
        unsigned long regs_len; /* length of I/O or MMI/O region */
        unsigned int tx_flag;
        unsigned int cur_tx;
        unsigned int dirty_tx;
        unsigned char *tx_buf[NUM_TX_DESC];   /* Tx bounce buffers */
        unsigned char *tx_bufs;        /* Tx bounce buffer region. */
        dma_addr_t tx_bufs_dma;

        struct net_device_stats stats;
        unsigned char *rx_ring;
        dma_addr_t rx_ring_dma;
        unsigned int cur_rx;
};

Таблица 13: Расширяем структуру rtl8139_private

Компонента stats должна хранить статистику с устройства (большая часть статистики, выдаваемой ifconfig, берется из этой структуры). Следующая компонента, rx_ring, является адресом памяти в ядре, где запоминаются принятые пакеты, а rx_ring_dma – физический адрес этой памяти. Как мы скоро увидим, компонент cur_rx используется для отслеживания места, куда будет записываться следующий пакет.

Теперь мы вновь рассмотрим функцию rtl8139_open, где мы выделили память только для передающей части. Сейчас мы выделим память также для принимаемых пакетов.
/* Size of the in-memory receive ring. */
#define RX_BUF_LEN_IDX 2         /* 0==8K, 1==16K, 2==32K, 3==64K */
#define RX_BUF_LEN     (8192 < RX_BUF_LEN_IDX)
#define RX_BUF_PAD     16           /* see 11th and 12th bit of RCR: 0x44 */
#define RX_BUF_WRAP_PAD 2048   /* spare padding to handle pkt wrap */
#define RX_BUF_TOT_LEN  (RX_BUF_LEN + RX_BUF_PAD + RX_BUF_WRAP_PAD)

/* this we have already done */
tp->tx_bufs = pci_alloc_consistent(tp->pci_dev, TOTAL_TX_BUF_SIZE, &tp->tx_bufs_dma);

/* add this code to rtl8139_function */
tp->rx_ring = pci_alloc_consistent(tp->pci_dev, RX_BUF_TOT_LEN, &tp->rx_ring_dma);

if((!tp->tx_bufs)  || (!tp->rx_ring)) {
        free_irq(dev->irq, dev);

        if(tp->tx_bufs) {
                       pci_free_consistent(tp->pci_dev, TOTAL_TX_BUF_SIZE, tp->tx_bufs, tp->tx_bufs_dma);
                       tp->tx_bufs = NULL;
               }
        if(tp->rx_ring) {
                       pci_free_consistent(tp->pci_dev, RX_BUF_TOT_LEN, tp->rx_ring, tp->rx_ring_dma);
                       tp->rx_ring = NULL;
               }
        return -ENOMEM;
}

Таблица 14: Расширяем функцию rtl8139_open

Код в таблице 14 вычисляет размер памяти, нужный для кольцевого буфера. Вычисление RX_BUF_TOT_LEN зависит от некоторых конфигурационных параметров устройства. Как мы скоро увидим, в функции rtl8139_hw_start устанавливаются значения битов 12 – 11 регистра RCR в 10, что задает длину приемного буфера равной 32K+16. Таким образом, мы выделяем этот объем памяти для приемного буфера. Также мы устанавливаем значение бита с 7 в 1, что означает, что RTL8139 переместит остальные пакетные данные в память, которая идет сразу после конца приемного буфера. Поэтому мы дополнительно выделяем 2048 байтов с тем, чтобы справиться с такими ситуациями.

Теперь, когда мы рассмотрели функцию rtl8139_open, рассмотрим функцию rtl8139_hw_start, в которой сконфигурируем устройство для приема пакетов.
static void rtl8139_hw_start (struct net_device *dev)
{
        struct rtl8139_private *tp = dev->priv;
        void *ioaddr = tp->mmio_addr;
        u32 i;

        rtl8139_chip_reset(ioaddr);

        /* Must enable Tx/Rx before setting transfer thresholds! */
        writeb(CmdTxEnb | CmdRxEnb, ioaddr + CR);

        /* tx config */
        writel(0x00000600, ioaddr + TCR); /* DMA burst size 1024 */

        /* rx config */
        writel(((1 < 12) | (7 < 8) | (1 < 7) | 
                               (1 < 3) | (1 < 2) | (1 < 1)), ioaddr + RCR);

        /* init Tx buffer DMA addresses */
        for (i = 0; i < NUM_TX_DESC; i++) {
               writel(tp->tx_bufs_dma + (tp->tx_buf[i] - tp->tx_bufs),
                                      ioaddr + TSAD0 + (i * 4));
        }

        /* init RBSTART */
        writel(tp->rx_ring_dma, ioaddr + RBSTART);

        /* initialize missed packet counter */
        writel(0, ioaddr + MPC);

        /* no early-rx interrupts */
        writew((readw(ioaddr + MULINT) & 0xF000), ioaddr + MULINT);

        /* Enable all known interrupts by setting the interrupt mask. */
        writew(INT_MASK, ioaddr + IMR);

        netif_start_queue (dev);
        return;
}

Таблица 15: Расширяем функцию rtl8139_hw_start

Как видно в таблице 15, первое изменение в функции rtl8139_hw_start - мы записываем в регистр CR значения CmdTxEnb | CmdRxEnb, что означает, что устройство будет передавать и принимать пакеты. Следующее изменение – конфигурирование приемной части устройства. Я не использовал в коде макросы, но, если вы смотрели спецификации rtl8139, они понятны. В этом месте используются следующие биты:

  • Бит 1 – Принимаются пакеты с проверкой физического адреса (адреса MAC – прим.пер.).
  • Бит 2 – Принимаются многоадресные пакеты (посылаемые на несколько адресов – прим.пер.)
  • Бит 3 – Принимаются широковещательные пакеты (посылаемые на все адреса – прим.пер.)
  • Бит 7 - WRAP. Когда установлен в 1, то RTL8139 будет перемещать оставшуюся часть пакетных данных в память, которая следует непосредственно за концом премного буфера.
  • Биты 8-10 - Максимальный размер буфера DMA для каждого сохраняемого в DMA пакета. Мы устанавливаем это значение равным 111, что означает – неограниченный.
  • Биты 11-12 – Длина буфера приема. Мы устанавливаем это значение равным 10, что означает 32K+16 байтов.

Следующее крупное изменение – конфигурирование регистра RBSTART. В этом регистре содержится стартовый адрес приемного буфера. Далее мы обнуляем регистр MPC (Missed Packet Counter – счетчик ошибочных пакетов) и конфигурируем устройство так, чтобы не генерировались ранние прерывания.

Последняя большая функция, которую мы хотим обсудить – обработчик прерываний устройства. Этот обработчик прерываний ответственен за прием пакетов, а также за обновление необходимой статистики. Ниже приведен исходный код обработчика прерываний.

static void rtl8139_interrupt (int irq, void *dev_instance, struct pt_regs *regs) 
{
        struct net_device *dev = (struct net_device*)dev_instance;
        struct rtl8139_private *tp = dev->priv;
        void *ioaddr = tp->mmio_addr;
        unsigned short isr = readw(ioaddr + ISR);
        
        /* clear all interrupt.
         * Specs says reading ISR clears all interrupts and writing
         * has no effect. But this does not seem to be case. I keep on
         * getting interrupt unless I forcibly clears all interrupt :-(
         */
        writew(0xffff, ioaddr + ISR);

        if((isr & TxOK) || (isr & TxErr)) 
        {
               while((tp->dirty_tx != tp->cur_tx) || netif_queue_stopped(dev))
               {
                       unsigned int txstatus = 
                               readl(ioaddr + TSD0 + tp->dirty_tx * sizeof(int));

                       if(!(txstatus & (TxStatOK | TxAborted | TxUnderrun)))
                               break; /* yet not transmitted */

                       if(txstatus & TxStatOK) {
                               LOG_MSG("Transmit OK interrupt\n");
                               tp->stats.tx_bytes += (txstatus & 0x1fff);
                               tp->stats.tx_packets++;
                       }
                       else {
                               LOG_MSG("Transmit Error interrupt\n");
                               tp->stats.tx_errors++;
                       }
                               
                       tp->dirty_tx++;
                       tp->dirty_tx = tp->dirty_tx % NUM_TX_DESC;

                       if((tp->dirty_tx == tp->cur_tx) & netif_queue_stopped(dev))
                       {
                               LOG_MSG("waking up queue\n");
                               netif_wake_queue(dev);
                       }
               }
        }

        if(isr & RxErr) {
               /* TODO: Need detailed analysis of error status */
               LOG_MSG("receive err interrupt\n");
               tp->stats.rx_errors++;
        }

        if(isr & RxOK) {
               LOG_MSG("receive interrupt received\n");
               while((readb(ioaddr + CR) & RxBufEmpty) == 0)
               {
                       unsigned int rx_status;
                       unsigned short rx_size;
                       unsigned short pkt_size;
                       struct sk_buff *skb;

                       if(tp->cur_rx > RX_BUF_LEN)
                               tp->cur_rx = tp->cur_rx % RX_BUF_LEN;
        
                       /* TODO: need to convert rx_status from little to host endian
                        * XXX: My CPU is little endian only :-)
                        */
                       rx_status = *(unsigned int*)(tp->rx_ring + tp->cur_rx);
                       rx_size = rx_status >> 16;
                       
                       /* first two bytes are receive status register 
                        * and next two bytes are frame length
                        */
                       pkt_size = rx_size - 4;

                       /* hand over packet to system */
                       skb = dev_alloc_skb (pkt_size + 2);
                       if (skb) {
                               skb->dev = dev;
                               skb_reserve (skb, 2); /* 16 byte align the IP fields */

                               eth_copy_and_sum(
                                      skb, tp->rx_ring + tp->cur_rx + 4, pkt_size, 0);

                               skb_put (skb, pkt_size);
                               skb->protocol = eth_type_trans (skb, dev);
                               netif_rx (skb);

                               dev->last_rx = jiffies;
                               tp->stats.rx_bytes += pkt_size;
                               tp->stats.rx_packets++;
                       } 
                       else {
                               LOG_MSG("Memory squeeze, dropping packet.\n");
                               tp->stats.rx_dropped++;
                       }
               
                       /* update tp->cur_rx to next writing location  * /
                       tp->cur_rx = (tp->cur_rx + rx_size + 4 + 3) & ~3;

                       /* update CAPR */
                       writew(tp->cur_rx, ioaddr + CAPR);
               }
        }
        
        if(isr & CableLen)
               LOG_MSG("cable length change interrupt\n");
        if(isr & TimeOut)
               LOG_MSG("time interrupt\n");
        if(isr & SysErr)
               LOG_MSG("system err interrupt\n");
        return;
}

Таблица 16: Обработчик прерываний

Как видно из таблицы 16, регистр ISR будет считан в переменную isr. Любое дальнейшее демультиплексирование прерывания – это работа обработчика прерываний. Если мы примем прерывания TxOK, TxErr или RxErr, то нам потребуется обновить статистику. Прием прерывания RxOK означает, что мы приняли кадр успешно и драйвер обработал его. Мы будем читать из приемного буфера до тех пор, пока не прочитаем все данные (эту работу выполняет цикл while ((readb (ioaddr + CR) & RxBufEmpty) == 0)). Сначала проверим, не вышло ли значение tp->cur_rx за длину RX_BUF_LEN. Если это так, мы выставляем значение wrap. Первые два байта указывают статус кадра, а следующие два байта указывают длину кадра (длина также включает первые 4 байта). Эти значения всегда в обратном порядке и их следует преобразовать. Затем для принятого пакета выделяем место для skb, копируем содержимое кадров в skb и ставим skb в очередь для дальнейшей обработки. Затем обновляем CAPR (Current Address of Packet Read - текущий адрес чтения пакета) с тем, чтобы устройство RTL8139 знало о месте следующей операции записи. Обратите внимание, что мы уже зарегистрировали этот обработчик прерываний в функции rtl8139_open. До сих пор мы имели фиктивное определение; теперь мы заменим его новым определением.

Последняя функция, которую мы хотим добавить - rtl8139_get_stats, она просто возвращает tp->stats.

static struct net_device_stats* rtl8139_get_stats(struct net_device *dev)
{
  struct rtl8139_private *tp = dev->priv;
  return &(tp->stats);
}

Таблица 17: Функция rtl8139_get_stats

Этим завершается разработка нашего драйвера. Откомпилируйте его, снова вставьте его в ядро (Вы должны с помощью команды rmmod выгрузить предыдущий модуль) и пропингуйте другой хост. Вы должны получить ответ на пинги.

Хотя драйвер профессионального уровня содержит гораздо больше возможностей, чем имеется в нашем драйвере, последний даст вам хорошее представление о сетевых драйверах и поможет понять процесс разработки драйверов.



Комментарии