Простейший символьный драйвер PCI-устройства для FreeBSD

27 Апр 2012

Диагностическая POST-карта IC Book, модель IC80v5

Операционная система FreeBSD настолько покоряет умы и сердца своих поклонников, что находит при­ме­не­ние в таких раз­ных отраслях, как хостинг интернет-про­ек­тов и маршрутизация па­ке­тов данных. С успехом может работать на ком­пак­т­ных embedded-устройствах и прекрасно справляется с вычислениями в больших и сложных многоядерных сис­те­мах. А иногда может ис­поль­зо­вать­ся и в необычном ка­че­стве — например, для мо­ни­то­рин­га внутреннего состояния персонального компьютера, об­о­ру­до­ван­но­го PCI-шиной. Как это сделать мы расскажем и покажем сегодня.

В глубину

Системные инженеры начинают обычно знакомство с техникой с самого низа, т.е. с уровня firmware, в примении к семейству IBM PC-совместимых комьютеров, известным как PC BIOS. И поднимаясь всё выше по архитектурной лестнице, достигают уровня прикладного програмиста. Современные тенденции обучения показывают, что молодые специалисты зачастую хорошо представляют, как программировать UI-интерфейс, но впадают в панику при использовании терминов Low-Level Programming и x86 BIOS-calls. Очень зря, т.к. и первый, и второй термин достаточно активно используются при каждом старте FreeBSD. Давайте займемся более плотно описанием процесса загрузки операционной системы. Здесь и далее мы будем рассматривать x86-системы.

Итак, при подаче питания запускается CPU. Почти сразу происходит инициализация памяти. И вектор управления передается на адрес 0xFFFF0, либо на 0xFFFF.FFF0. Оба варианта правильны, т.к. при начальной инициализации, x86-совместимый процессор должен находиться в так называемом real-mode. На месте этого адресного пространства, а именно – от 0xE0000 до 0xFFFFF, в тот момент находится область памяти FlashROM. Где хранятся процедуры для инициализации различных компонентов PC – портов RS232, LPT, USB; также есть проверки правильности функционирования шин PCI и ISA.

Кроме перечисленных физических портов, в каждой системе должен быть и виртуальный диагностический порт 0x80. В него PC BIOS, например, AwardBIOS или PhoenixBIOS, в начале выполнения каждой процедуры пишет определенный номер, который можно прочитать на диагностическом оборудовании (его рассмотрим чуть позже). И присутствуют, наконец, процедуры для загрузки операционной системы, в частности векторы прерывания int 0x13, 0x18, 0x19.

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

Если все указанные подсистемы работают нормально, то следующий шаг – это настройка вектора int 0x19, так, чтобы можно было загрузить FreeBSD BTX Loader. Который в свою очередь загрузит саму систему. До этого момента работает firmware-код. И если у вас вдруг утром FreeBSD работала, а к вечеру отказывается загружаться, то определить, что же именно отказало в аппаратном обеспечении без внешней помощи трудно. Это может быть испорченный чип памяти, либо сгоревший USB-порт, который не проходит инициализацию. Либо просто испортились конденсаторы на плате. Вот в такой-то ситуации на помощь и приходит диагностическое оборудования, а именно POST (Power-On Self Test) карта. Ее задача – вывести на семисегментный индиктор номер процедуры, которая исполняется во FlashROM. Имея под рукой список кодов вы всегда можете сказать, какой компонент отказал и что нужно поменять.

IC80 POST-Card от IC Book Labs

Долго присматриваясь к такому специфическому рынку как POST-карты, обнаружилось следующее. Есть около дюжины различных производителей – от именитых грандов, до безымянных, сделанных где-то в далекой провинции Guangdong, КНР. У кого-то есть подробное описание продукта и сайт с техподдержкой, а у кого-то только сама безымянная, nonamed карта. Так как, на мой взгляд, лучше иметь продукт с максимальным числом возможностей и функций, и также грамотную техническую поддержку с живым специалистом, то остановил я свой выбор на небезызвестной компании IC Book Labs. И как оказалось не зря – плата IC80 за 10 лет прошла через несколько релизов и на сегодняшний день имеет версию V5.0. Умеет работать с AMIBIOS, AwardBIOS, PhoenixBIOS, InsydeBIOS. Показывает на двух LED-дисплеях диагностические сообщения как из виртуальных портов 0x80 и 0x81, так умеет с помощью переключателей на плате показывать значения таких специфических портов как: 0x84, 0x1080, 0x2080 используемых в платах Compaq (сейчас уже Hewlett-Packard) и ATI (сейчас AMD). И позволяет увидеть качество вольтажа линий +3.3V, +5V, +12V and -12V. Ведь правда, швейцарский нож :-)

В целом, решение оказалось правильным, хотя и отличается в стоимости от nonamed-устройств “Made in China” на порядок. В любом случае, приступаем к запуску.

Последовательность загрузки

Нажимаем кнопку Power и смотрим, что же выводится на LED-dsiplay. У меня использовалась материнская карта Intel D945GCLF2, поэтому я ориентировался на список кодов, который используется в платах Intel [2]. При запуске, последовательность запуска процедур инициализации была следующей:

22, 23, 25, 28, 34, 12, 58, 50, 51, EB, 58, 92, 90, 94, 95, BB, B8, BA, 5A, 92, 90, 94, BB, BA, EB, BB, BA, 5A, BB, BA, E7, E9, и, наконец, 00

  •          22 - Reading SPD from memory DIMMs
  •          23 - Detecting presence of memory DIMMs
  •          25 - Configuring memory
  •          28 - Testing memory
  •          34 - Loading recovery capsule
  •          12 - Starting Application processor initialization
  •          58 - Resetting USB bus
  •          50 - Enumerating PCI busses
  •          51 - Allocating resources to PCI bus
  •          EB - Calling Legacy Option ROMs
  •          ...
  •          58 - Resetting USB bus
  •          92 - Detecting presence of keyboard
  •          90 - Resetting keyboard
  •          94 - Clearing keyboard input buffer
  •          95 - Instructing keyboard controller to run Self Test (PS2 only)
  •          BB - reserved by Intel
  •          B8 - Resetting removable media
  •          BA - Detecting presence of a removable media (IDE, CD-ROM detection, etc.)
  •          5A - Resetting PATA/SATA bus and all devices
  •          ...
  •          E7- Waiting for user input
  •          E9 - Entering BIOS setup
  •          ...
  •          5A - Resetting PATA/SATA bus and all devices
  •          BA - Detecting presence of a removable media (IDE, CD-ROM detection, etc.)
  •          00 -  Ready to boot

После этого управление было передано BTX Loader и происходит обычный запуск FreeBSD.

После инициализации BIOS управление передано BTX-загрузчику

Рис 1. После инициализации BIOS управление передано BTX-загрузчику

FreeBSD система загружена и фунционирует

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

[root@a-bsd ~]# pciconf -l

hostb0@pci0:0:0:0:      class=0x060000 card=0x464c8086 chip=0x27708086 rev=0x02 hdr=0x00

vgapci0@pci0:0:2:0:     class=0x030000 card=0x464c8086 chip=0x27728086 rev=0x02 hdr=0x00

none0@pci0:0:27:0:      class=0x040300 card=0xd6048086 chip=0x27d88086 rev=0x01 hdr=0x00

pcib1@pci0:0:28:0:      class=0x060400 card=0x00000000 chip=0x27d08086 rev=0x01 hdr=0x01

pcib2@pci0:0:28:2:      class=0x060400 card=0x00000000 chip=0x27d48086 rev=0x01 hdr=0x01

pcib3@pci0:0:28:3:      class=0x060400 card=0x00000000 chip=0x27d68086 rev=0x01 hdr=0x01

uhci0@pci0:0:29:0:      class=0x0c0300 card=0x464c8086 chip=0x27c88086 rev=0x01 hdr=0x00

uhci1@pci0:0:29:1:      class=0x0c0300 card=0x464c8086 chip=0x27c98086 rev=0x01 hdr=0x00

uhci2@pci0:0:29:2:      class=0x0c0300 card=0x464c8086 chip=0x27ca8086 rev=0x01 hdr=0x00

uhci3@pci0:0:29:3:      class=0x0c0300 card=0x464c8086 chip=0x27cb8086 rev=0x01 hdr=0x00

ehci0@pci0:0:29:7:      class=0x0c0320 card=0x464c8086 chip=0x27cc8086 rev=0x01 hdr=0x00

pcib4@pci0:0:30:0:      class=0x060401 card=0x464c8086 chip=0x244e8086 rev=0xe1 hdr=0x01

isab0@pci0:0:31:0:      class=0x060100 card=0x464c8086 chip=0x27b88086 rev=0x01 hdr=0x00

atapci0@pci0:0:31:1:    class=0x01018a card=0x464c8086 chip=0x27df8086 rev=0x01 hdr=0x00

atapci1@pci0:0:31:2:    class=0x01018f card=0x464c8086 chip=0x27c08086 rev=0x01 hdr=0x00

ichsmb0@pci0:0:31:3:    class=0x0c0500 card=0x464c8086 chip=0x27da8086 rev=0x01 hdr=0x00

re0@pci0:1:0:0: class=0x020000 card=0x00018086 chip=0x816810ec rev=0x02 hdr=0x00

none1@pci0:4:0:0:       class=0x118000 card=0x00000000 chip=0x001cb00c rev=0x05 hdr=0x00

Listing 1. Команда pciconf показывает устройства, доступные системе

Видим, что помимо интегрированных устройств – видео и сетевой карт – присутствует также устройства, для которых не загружены драйверы: none0@pci0:0:27:0 и none1@pci0:4:0:0

Запустим pciconf в более информативном режиме:

[root@a-bsd ~]# pciconf -lv | grep none -A3

none0@pci0:0:27:0:      class=0x040300 card=0xd6048086 chip=0x27d88086 rev=0x01 hdr=0x00

    vendor     = 'Intel Corporation'

    device     = 'IDT High Definition Audio Driver  (BA101897)'

    class      = multimedia

--

none1@pci0:4:0:0:       class=0x118000 card=0x00000000 chip=0x001cb00c rev=0x05 hdr=0x00

    vendor     = 'IC Book Labs'

    device     = 'IC80+PCI POST Diagnostics Card'

    class      = dasp

Listing 2. Информация в расширенном режиме для устройств без драйверов

Отлично. Первое устройство, для которого не нашлось драйвера – это HD аудио-модуль. И второе – наша POST-карта. Убедимся, что драйвер для диагностической карты действительно не загружен.

[root@a-bsd ~]# dmesg | grep pci4

pci4: <ACPI PCI bus> on pcib4

pci4: <dasp> at device 0.0 (no driver attached)

Listing 3. Во FreeBSD нет драйвера для диагностической POST-карты IC80+PCI 

По большому счету, функционал у карты достаточно простой – показывать на дисплеях, что отправлено в порт 0x80 и 0x81. Поэтому мы в состоянии написать его самостоятельно. Чем и займемся в следующей секции.

Проектируем простейший драйвер символьного устройства

 В качестве примера мы сначала рассмотрим самый простейший образец [3] драйвера, который при загрузке будет писать, что драйвер загружен в адресное пространство ядра. А при выгрузке, соответственно, что он выгружен. Это будет банальный “Hello, world!”, но с учетом специфики области применения.

Но сначала убедитесь, что следующие пакеты в системе установлены: gcc, make, source kernel, source share. Обычно, первые 2 пакета уже бывают в системе устанавлены. Поэтому запускайте утилиту sysinstall и устанавливайте дополнительные исходные тексты:

sysinstall->Configure->Distributions->src->share

sysinstall->Configure->Distributions->src->sys

В качестве начальной точки построения драйвера можно также взглянуть на поставляемый пример из состава пакета: /usr/src/share/examples/kld/syscall/module/syscall.c

KMOD=   hello_world

SRCS=   hello_world.c

.include <bsd.kmod.mk>

Listing 4. Файл Makefile для драйвера ядра 'hello_world'

#include <sys/types.h>

#include <sys/param.h>

#include <sys/proc.h>

#include <sys/module.h>

#include <sys/sysproto.h>

#include <sys/sysent.h>

#include <sys/kernel.h>

#include <sys/systm.h>

/*

 * The function for implementing the syscall.

 */

static int

hello (struct thread *td, void *arg)

{

        printf ("hello kernel world\n");

        return 0;

}

/*

 * The `sysent' for the new syscall

 */

static struct sysent hello_sysent = {

        0,                      /* sy_narg */

        hello                   /* sy_call */

};

/*

 * The offset in sysent where the syscall is allocated.

 */

static int offset = NO_SYSCALL;

/*

 * The function called at load/unload.

 */

static int

load (struct module *module, int cmd, void *arg)

{

        int error = 0;

        switch (cmd) {

        case MOD_LOAD :

                printf ("Driver loaded at %d\n", offset); /* logging to syslog */

                uprintf ("Driver loaded at %d\n", offset);  /* logging to terminal */

                break;

        case MOD_UNLOAD :

                printf ("Driver unloaded from %d\n", offset); /* logging to syslog */

                uprintf ("Driver unloaded from %d\n", offset); /* logging to terminal */

                break;

        default :

                error = EOPNOTSUPP;

                break;

        }

        return error;

}

SYSCALL_MODULE(hello_world, &offset, &hello_sysent, load, NULL);

 

Listing 5. Исходный текст драйвера ядра 'hello_world'

Начинаем компиляцию:

         # make

         И загружаем в адресное пространство ядра:

         # kldload ./hello_world.ko

         # dmesg | grep Driver

         hello_world loaded at 210

Драйвер работает. Выгрузить из памяти его можно также просто:

         # kldunload hello_world

У нас получился простейший драйвер, а по сути заглушка. Теперь нужно нарастить мускулы, т.е. научить драйвер создавать символьное устройство в /dev/, и сделать так, чтобы запись значения типа int в это устройство отображалось на LED0-дисплей.  Для отображения в диагностическом порту 0x80 нужно будет отослать в устройство шестнадцатиричное значение. Да, так просто.

#include <sys/types.h>

#include <sys/module.h>

#include <sys/systm.h>  /* uprintf */

#include <sys/errno.h>

#include <sys/param.h>  /* defines used in kernel.h */

#include <sys/kernel.h> /* types used in module initialization */

#include <sys/conf.h>   /* cdevsw struct */

#include <sys/uio.h>    /* uio struct */

#include <sys/malloc.h>

#include <sys/types.h>

#define BUFFERSIZE 5

/* Function prototypes */

static d_open_t      ic80_open;

static d_close_t     ic80_close;

static d_read_t      ic80_read;

static d_write_t     ic80_write;

/* Character device entry points */

static struct cdevsw ic80_cdevsw = {

    .d_version = D_VERSION,

    .d_flags = D_PSEUDO | D_NEEDGIANT,

    .d_open = ic80_open,

    .d_close = ic80_close,

    .d_read = ic80_read,

    .d_write = ic80_write,

    .d_name = "ic80",

};

typedef struct s_ic80 {

    char msg[BUFFERSIZE];

    int len;

} t_ic80;

static struct cdev *ic80_dev;

static int count;

static t_ic80 *ic80msg;

MALLOC_DECLARE(M_IC80BUFFER);

MALLOC_DEFINE(M_IC80BUFFER, "ic80buffer", "a buffer for ic80 post card kernel module");

static int

ic80_loader(struct module *m, int what, void *arg)

{

    int err = 0;

    switch (what) {

    case MOD_LOAD:

        ic80_dev = make_dev(&ic80_cdevsw, 0, UID_ROOT, GID_WHEEL, 0666, "ic80");

        ic80msg = malloc(sizeof(t_ic80), M_IC80BUFFER, M_WAITOK);

        printf("Driver IC80 loaded\n");

        break;

    case MOD_UNLOAD:

        destroy_dev(ic80_dev);

        free(ic80msg, M_IC80BUFFER);

        printf("Driver IC80 unloaded\n");

        break;

    default:

        err = EOPNOTSUPP;

        break;

    }

    return(err);

}

static int

ic80_open(struct cdev *dev, int oflags, int devtype, struct thread *p)

{

    int err = 0;

    printf("Opening device \"ic80\" ...\n");

    return(err);

}

static int

ic80_close(struct cdev *dev, int fflag, int devtype, struct thread *p)

{

    printf("Closing device \"ic80\" ... \n");

    return(0);

}

/*

 * The read function just takes the buf that was saved via

 * echo_write() and returns it to userland for accessing.

 * uio(9)

 */

static int

ic80_read(struct cdev *dev, struct uio *uio, int ioflag)

{

    int err = 0;

    int amt;

    /*

     * How big is this read operation?  Either as big as the user wants,

     * or as big as the remaining data

     */

    amt = MIN(uio->uio_resid, (ic80msg->len - uio->uio_offset > 0) ?

         ic80msg->len - uio->uio_offset : 0);

    if ((err = uiomove(ic80msg->msg + uio->uio_offset, amt, uio)) != 0) {

        uprintf("uiomove failed!\n");

    }

    return(err);

}

static int

ic80_write(struct cdev *dev, struct uio *uio, int ioflag)

{

    int err = 0;

    /* Copy the string in from user memory to kernel memory */

    err = copyin(uio->uio_iov->iov_base, ic80msg->msg, 4);

    /* Now we need to null terminate, then record the length */

    *(ic80msg->msg + 4) = 0;

    ic80msg->len = 4;

 

    if (err != 0) { uprintf("Write failed: bad address!\n"); }

    count++;

    outb( 0x80, strtol(ic80msg->msg, 0, 16) );

    return(err);

}

DEV_MODULE(ic80,ic80_loader,NULL);

Listing 6. Исходный текст для драйвера 'ic80'

Краткий комментарий к тексту. Мы создаем структуру с именем “ic80_cdevsw”, где объявляются функции, что будут вызваны при доступе к символьному устройству "/dev/ic_80". Например, при каждом открытии, закрытии, чтения, либо записи в устройство.

static struct cdevsw ic80_cdevsw = {

    .d_version = D_VERSION,

    .d_flags = D_PSEUDO | D_NEEDGIANT,

    .d_open = ic80_open,

    .d_close = ic80_close,

    .d_read = ic80_read,

    .d_write = ic80_write,

    .d_name = "ic80",

};

Внутри функции ic80_loader() мы создаем механизм регистрации модуля в ядре, а также обратный механизм, т.е. Выгрузки из ядра. В функции ic80_write() значение, посылаемое в порт 0x80, переводится из пользовательского пространства в область ядра и записывается непосредственно в порт 0x80. Внутренний буфер ic80msg специально сделан коротким, т.к. предназначен для хранения всего лишь 5 символов.

Драйвер готов к работе. Теперь, когда вы загружаете его в ядро, он создает новое символьное устройство /dev/ic80, с правами доступа 0666 и владельцем root:wheel. После этого, вы можете послать в устройство “/dev/ic80” любое значение в формате “0xXX” (например, 0xAB) и оно высветится на дисплее LED0. Я решил контролировать через POST-карту загрузку системы, поэтому подготовил следующий скрипт. Будучи запущен от имени пользователя, и при увеличивающейся нагрузке, вы сможете наблюдать изменение на дисплее в реальном времени. Задача, которая стала нагружать систему – это утилита burnMMX из порта   /usr/ports/sysutils/burn.

#!/bin/sh

        while true;

         do 

                   sleep 1s;

                   id=`top -d 1 | grep load | awk '{ print $6 }' | head -c 4 | tail -c 2`;

                   echo $id > /dev/ic80;

         done

Listing 7. Значение системной нагрузки посылается в новое символьное устройство

Вообще-то, на дисплей LED0 вы можете выводить всё, что вам заблагорассудится. Значения, правда, должны быть в диапазоне от 0x00 до 0xFF. Например, это может быть температура как процессора, так и системной платы. Более того, не забывайте о наличии индикатора LED1. При небольшой доработке драйвера, вы сможете показывать вдвое больше информаци, нежели сейчас. 

Выводы

Как вы смогли увидеть, проектирование драйвера не такая уж и сложная задача, как кажется на первый взгляд. Это увлекательное занятие, а если учесть, что под рукой есть и необычное устройство, так и вдвойне интересно. Тем более, что вы создаете новое, удивительное решение. В любом случае, такое начинание под силу каждому начинающему пользователю FreeBSD, а не только профессионалам, не правда ли?

Ресурсы

[1] icbook.com.ua/

[2] intel.com/support/motherboards/desktop/sb/CS-025434.htm

[3] redantigua.com/c-ex-kernel-freebsd-hello.html