#include <sys/module.h> #include <sys/bus.h> #include <machine/bus.h> #include <machine/resource.h> #include <sys/rman.h> #include <isa/isavar.h> #include <isa/pnpvar.h>
Глава 10. Драйверы устройств ISA
Этот перевод может быть устаревшим. Для того, чтобы помочь с переводом, пожалуйста, обратитесь к Сервер переводов FreeBSD.
Содержание
10.1. Обзор
Эта глава знакомит с вопросами, относящимся к написанию драйвера устройства ISA. Представленный здесь псевдокод довольно детализирован и напоминает реальный код, но всё же остаётся псевдокодом. Он избегает деталей, не относящихся к теме обсуждения. Реальные примеры можно найти в исходном коде настоящих драйверов. В частности, драйверы ep
и aha
являются хорошими источниками информации.
10.2. Основная информация
Типичному драйверу ISA могут потребоваться следующие включаемые файлы:
Они описывают особенности, специфичные для подсистемы ISA и обобщенной шины.
Подсистема шины реализована в объектно-ориентированном стиле, её основные структуры доступны через методы, связанные с объектами.
Список методов шины, реализуемых драйвером ISA, аналогичен списку для любой другой шины. Для гипотетического драйвера с именем "xxx" они будут:
static void xxx_isa_identify (driver_t *, device_t);
Обычно используется для драйверов шины, а не для драйверов устройств. Однако для устройств ISA этот метод может иметь особое применение: если устройство предоставляет специфический (не PnP) способ автоматического обнаружения устройств, эта процедура может его реализовывать.static int xxx_isa_probe (device_t dev);
Проверка наличия устройства в известном (или PnP) расположении. Эта процедура также может учитывать автоопределение параметров, специфичных для устройства, в случае частично настроенных устройств.static int xxx_isa_attach (device_t dev);
Подключение и инициализация устройства.static int xxx_isa_detach (device_t dev);
Отсоединение устройства перед выгрузкой модуля драйвера.static int xxx_isa_shutdown (device_t dev);
Выполняет завершение работы устройства перед выключением системы.static int xxx_isa_suspend (device_t dev);
Приостанавливает устройство перед переходом системы в энергосберегающий режим. Также может прервать переход в энергосберегающий режим.static int xxx_isa_resume (device_t dev);
Возобновляет активность устройства после возврата из энергосберегающего состояния.
xxx_isa_probe()
и xxx_isa_attach()
являются обязательными, остальные процедуры опциональны и зависят от потребностей устройства.
Драйвер связан с системой следующим набором описаний.
/* table of supported bus methods */ static device_method_t xxx_isa_methods[] = { /* list all the bus method functions supported by the driver */ /* omit the unsupported methods */ DEVMETHOD(device_identify, xxx_isa_identify), DEVMETHOD(device_probe, xxx_isa_probe), DEVMETHOD(device_attach, xxx_isa_attach), DEVMETHOD(device_detach, xxx_isa_detach), DEVMETHOD(device_shutdown, xxx_isa_shutdown), DEVMETHOD(device_suspend, xxx_isa_suspend), DEVMETHOD(device_resume, xxx_isa_resume), DEVMETHOD_END }; static driver_t xxx_isa_driver = { "xxx", xxx_isa_methods, sizeof(struct xxx_softc), }; static devclass_t xxx_devclass; DRIVER_MODULE(xxx, isa, xxx_isa_driver, xxx_devclass, load_function, load_argument);
Здесь структура xxx_softc
— это специфичная для устройства структура, которая содержит приватные данные драйвера и дескрипторы ресурсов драйвера. Код шины автоматически выделяет один дескриптор softc для каждого устройства по мере необходимости.
Если драйвер реализован в виде загружаемого модуля, то load_function()
вызывается для выполнения специфичной для драйвера инициализации или очистки при загрузке или выгрузке драйвера, а load_argument
передаётся в качестве одного из её аргументов. Если драйвер не поддерживает динамическую загрузку (другими словами, он всегда должен быть связан с ядром), то эти значения должны быть установлены в 0, и последнее определение будет выглядеть следующим образом:
DRIVER_MODULE(xxx, isa, xxx_isa_driver, xxx_devclass, 0, 0);
Если драйвер предназначен для устройства с поддержкой PnP, то должна быть определена таблица поддерживаемых PnP ID. Таблица состоит из списка PnP ID, поддерживаемых этим драйвером, и удобочитаемых описаний типов аппаратного обеспечения и моделей, имеющих эти ID. Это выглядит следующим образом:
static struct isa_pnp_id xxx_pnp_ids[] = { /* a line for each supported PnP ID */ { 0x12345678, "Our device model 1234A" }, { 0x12345679, "Our device model 1234B" }, { 0, NULL }, /* end of table */ };
Если драйвер не поддерживает устройства PnP, ему все равно нужна пустая таблица идентификаторов PnP, например:
static struct isa_pnp_id xxx_pnp_ids[] = { { 0, NULL }, /* end of table */ };
10.3. Указатель device_t
device_t
— это тип указателя на структуру устройства. Здесь мы рассматриваем только методы, представляющие интерес с точки зрения разработчика драйверов устройств. Методы для работы со значениями в структуре устройства следующие:
device_t device_get_parent(dev)
Получить родительскую шину устройства.driver_t device_get_driver(dev)
Получить указатель на структуру его драйвера.char *device_get_name(dev)
Получить имя драйвера, например"xxx"
в нашем примере.int device_get_unit(dev)
Получить номер устройства (устройства нумеруются с 0 для устройств, связанных с каждым драйвером).char *device_get_nameunit(dev)
Получить имя устройства, включая номер юнита, например, "xxx0", "xxx1" и так далее.char *device_get_desc(dev)
Получить описание устройства. Обычно оно описывает точную модель устройства в удобочитаемом виде.device_set_desc(dev, desc)
Установить описание. Это заставляет описание устройства указывать на строку desc, которая не может быть освобождена или изменена после этого.device_set_desc_copy(dev, desc)
Установить описание. Описание копируется во внутренний динамически выделяемый буфер, поэтому строка desc может быть изменена впоследствии без негативных последствий.void *device_get_softc(dev)
Получить указатель на дескриптор устройства (структураxxx_softc
), связанный с данным устройством.u_int32_t device_get_flags(dev)
Получить флаги, указанные для устройства в файле конфигурации.
Функция для удобства device_printf(dev, fmt, …)
может использоваться для вывода сообщений из драйвера устройства. Она автоматически добавляет имя устройства и двоеточие перед сообщением.
Методы device_t реализованы в файле kern/bus_subr.c.
10.4. Файл конфигурации и порядок определения и проверки при автоматической настройке
Устройства ISA описываются в файле конфигурации ядра следующим образом:
device xxx0 at isa? port 0x300 irq 10 drq 5 iomem 0xd0000 flags 0x1 sensitive
Значения порта, IRQ и т. д. преобразуются в ресурсы, связанные с устройством. Они являются необязательными, в зависимости от потребностей устройства и его способностей к автонастройке. Например, некоторым устройствам вообще не нужен DRQ, а некоторые позволяют драйверу читать настройку IRQ из портов конфигурации устройства. Если в машине несколько шин ISA, точная шина может быть указана в строке конфигурации, например isa0
или isa1
, иначе устройство будет искаться на всех шинах ISA.
sensitive
— это ресурс, указывающий, что данное устройство должно быть проверено перед всеми нечувствительными устройствами. Он поддерживается, но, похоже, не используется ни в одном текущем драйвере.
Для устаревших устройств ISA во многих случаях драйверы всё ещё могут определять параметры конфигурации. Однако каждое устройство, которое необходимо настроить в системе, должно иметь строку конфигурации. Если в системе установлено два устройства одного типа, но для соответствующего драйвера есть только одна строка конфигурации, например:
device xxx0 at isa?
тогда будет настроено только одно устройство.
Однако для устройств, поддерживающих автоматическую идентификацию с помощью Plug-n-Play или какого-либо проприетарного протокола, достаточно одной строки конфигурации для настройки всех устройств в системе, как в примере выше или просто:
device xxx at isa?
Если драйвер поддерживает как автоматически определяемые, так и устаревшие устройства, и оба типа установлены одновременно в одной машине, то достаточно описать в конфигурационном файле только устаревшие устройства. Автоматически определяемые устройства будут добавлены автоматически.
При автоматической настройке шины ISA события происходят в следующем порядке:
Все процедуры идентификации драйверов (включая процедуру идентификации PnP, которая определяет все устройства PnP) вызываются в случайном порядке. Когда они идентифицируют устройства, они добавляют их в список на шине ISA. Обычно процедуры идентификации драйверов связывают свои драйверы с новыми устройствами. Процедура идентификации PnP пока не знает о других драйверах, поэтому не связывает ни один из них с новыми устройствами, которые она добавляет.
Устройства PnP переводятся в режим сна с использованием протокола PnP, чтобы предотвратить их обнаружение как устаревших устройств.
Вызываются процедуры обнаружения для устройств, не поддерживающих PnP, помеченных как sensitive
. Если процедура обнаружения для устройства завершилась успешно, вызывается процедура присоединения для него.
Вызов процедур обнаружения и присоединения всех устройств, не поддерживающих PNP, выполняется аналогичным образом.
Устройства PnP выводятся из состояния сна и получают запрошенные ресурсы: диапазоны адресов ввода-вывода и памяти, IRQ и DRQ, причем все они не конфликтуют с подключенными устаревшими устройствами.
Затем для каждого устройства PnP вызываются процедуры обнаружения всех присутствующих драйверов ISA. Первый драйвер, который заявит о поддержке устройства, будет присоединен. Возможна ситуация, когда несколько драйверов заявят о поддержке устройства с разным приоритетом; в этом случае побеждает драйвер с наивысшим приоритетом. Процедуры обнаружения должны вызывать ISA_PNP_PROBE()
для сравнения фактического PnP ID со списком ID, поддерживаемых драйвером, и если ID отсутствует в таблице, возвращать ошибку. Это означает, что абсолютно каждый драйвер, даже те, которые не поддерживают никакие PnP устройства, должны вызывать ISA_PNP_PROBE()
, хотя бы с пустой таблицей PnP ID, чтобы возвращать ошибку для неизвестных PnP устройств.
Процедура обнаружения возвращает положительное значение (код ошибки) в случае ошибки, ноль или отрицательное значение в случае успеха.
Отрицательные возвращаемые значения используются, когда устройство PnP поддерживает несколько интерфейсов. Например, старый совместимый интерфейс и новый расширенный интерфейс, которые поддерживаются разными драйверами. В этом случае оба драйвера обнаружат устройство. Драйвер, возвращающий большее значение в процедуре обнаружения, получает приоритет (другими словами, драйвер, возвращающий 0, имеет наивысший приоритет, возвращающий -1 — следующий, возвращающий -2 — за ним и так далее). В результате устройства, поддерживающие только старый интерфейс, будут обрабатываться старым драйвером (который должен возвращать -1 из процедуры обнаружения), тогда как устройства, поддерживающие также новый интерфейс, будут обрабатываться новым драйвером (который должен возвращать 0 из процедуры обнаружения). Если несколько драйверов возвращают одинаковое значение, побеждает тот, который был вызван первым. Таким образом, если драйвер возвращает значение 0, он может быть уверен, что выиграл арбитраж приоритетов.
Процедуры идентификации, специфичные для устройства, также могут назначать устройству не драйвер, а класс драйверов. Затем все драйверы в этом классе проверяются на совместимость с устройством, как в случае с PnP. Эта возможность не реализована ни в одном существующем драйвере и далее в этом документе не рассматривается.
Поскольку устройства PnP отключены при проверке устаревших устройств, они не будут присоединены дважды (один раз как устаревшие и один раз как PnP). Однако в случае процедур идентификации, зависящих от устройства, ответственность за то, чтобы одно и то же устройство не было присоединено драйвером дважды (один раз как настроенное пользователем устаревшее и один раз как автоматически идентифицированное), лежит на драйвере.
Еще одно практическое следствие для автоматически определяемых устройств (как PnP, так и специфичных для устройства) заключается в том, что флаги не могут быть переданы им из файла конфигурации ядра. Поэтому они либо не должны использовать флаги вообще, либо использовать флаги из устройства unit 0 для всех автоматически определяемых устройств, либо использовать интерфейс sysctl вместо флагов.
Другие нестандартные конфигурации могут быть реализованы путем прямого доступа к ресурсам конфигурации с использованием функций семейств resource_query_*()
и resource_*_value()
. Их реализации находятся в kern/subr_bus.c. Примеры такого использования есть в старом драйвере диска IDE i386/isa/wd.c. Однако стандартные методы конфигурации всегда должны быть предпочтительны. Оставьте разбор ресурсов конфигурации коду настройки шины.
10.5. Ресурсы
Информация, которую пользователь вводит в файл конфигурации ядра, обрабатывается и передаётся ядру в виде ресурсов конфигурации. Эта информация анализируется кодом конфигурации шины и преобразуется в значение структуры device_t
и связанные с ней ресурсы шины. Драйверы могут напрямую обращаться к ресурсам конфигурации, используя функции resource_*
для более сложных случаев конфигурации. Однако, как правило, в этом нет необходимости, и это не рекомендуется, поэтому данный вопрос далее не рассматривается.
Ресурсы шины связаны с каждым устройством. Они идентифицируются по типу и номеру внутри типа. Для шины ISA определены следующие типы:
SYS_RES_IRQ - номер прерывания
SYS_RES_DRQ - номер канала ISA DMA
SYS_RES_MEMORY - диапазон памяти устройства, отображенный в системное адресное пространство
SYS_RES_IOPORT - диапазон регистров ввода-вывода устройства
Перечисление внутри типов начинается с 0, поэтому если устройство имеет две области памяти, оно будет иметь ресурсы типа SYS_RES_MEMORY
с номерами 0 и 1. Тип ресурса не связан с типом языка C, все значения ресурсов имеют тип unsigned long
в языке C и должны быть приведены по мере необходимости. Номера ресурсов не обязательно должны быть последовательными, хотя для ISA они обычно таковыми являются. Допустимые номера ресурсов для устройств ISA:
IRQ: 0-1 DRQ: 0-1 MEMORY: 0-3 IOPORT: 0-7
Все ресурсы представлены в виде диапазонов с начальным значением и количеством. Для ресурсов IRQ и DRQ количество обычно равно 1. Значения для памяти относятся к физическим адресам.
Три типа действий могут выполняться над ресурсами:
Установка — set/get
Выделение — allocate/release
Активация — activate/deactivate
Установка задает диапазон, используемый ресурсом. Выделение резервирует запрошенный диапазон, чтобы никакой другой драйвер не смог его зарезервировать (и проверяет, что никакой другой драйвер уже не зарезервировал этот диапазон). Активация делает ресурс доступным для драйвера, выполняя все необходимые для этого действия (например, для памяти это может быть отображение в виртуальное адресное пространство ядра).
Функции для управления ресурсами:
int bus_set_resource(device_t dev, int type, int rid, u_long start, u_long count)
Устанавливает диапазон для ресурса. Возвращает 0 при успешном выполнении, в противном случае — код ошибки. Обычно эта функция возвращает ошибку только в том случае, если одно из значений
type
,rid
,start
илиcount
выходит за пределы допустимого диапазона.dev - устройство драйвера
тип - тип ресурса, SYS_RES_*
rid - номер ресурса (ID) в пределах типа
начало, количество - диапазон ресурсов
int bus_get_resource(device_t dev, int type, int rid, u_long *startp, u_long *countp)
Получает диапазон ресурса. Возвращает 0 при успехе, код ошибки, если ресурс ещё не определён.
u_long bus_get_resource_start(device_t dev, int type, int rid) и u_long bus_get_resource_count (device_t dev, int type, int rid)
Удобные функции для получения только начала или количества. Возвращают 0 в случае ошибки, поэтому если начало ресурса может законно содержать 0, невозможно определить, является ли значение 0 или произошла ошибка. К счастью, ни один ресурс ISA для дополнительных драйверов не может иметь начальное значение, равное 0.
void bus_delete_resource(device_t dev, int type, int rid)
Удаляет ресурс, делает его неопределённым.
struct resource * bus_alloc_resource(device_t dev, int type, int *rid, u_long start, u_long end, u_long count, u_int flags)
Выделяет ресурс как диапазон значений count, не выделенных никем другим, где-то между start и end. Увы, выравнивание не поддерживается. Если ресурс ещё не был установлен, он автоматически создаётся. Специальные значения start равное 0 и end равное ~0 (все единицы) означают, что должны использоваться фиксированные значения, ранее установленные
bus_set_resource()
: start и count как есть, а end=(start+count). В этом случае, если ресурс не был определён ранее, возвращается ошибка. Хотя rid передаётся по ссылке, он нигде не устанавливается кодом выделения ресурсов шины ISA. Другие шины могут использовать иной подход и изменять его.
Флаги представляют собой битовую карту. Интересные для вызывающей стороны флаги:
RF_ACTIVE - приводит к автоматической активации ресурса после его выделения.
RF_SHAREABLE - ресурс может использоваться одновременно несколькими драйверами.
RF_TIMESHARE - ресурс может разделяться по времени несколькими драйверами, т.е. выделяться одновременно многими, но активироваться только одним в любой момент времени.
Возвращает 0 при ошибке. Выделенные значения могут быть получены из возвращённого дескриптора с использованием методов
rhand_*()
.int bus_release_resource(device_t dev, int type, int rid, struct resource *r)
Освобождает ресурс, r — это дескриптор, возвращённый
bus_alloc_resource()
. Возвращает 0 при успехе, код ошибки в противном случае.int bus_activate_resource(device_t dev, int type, int rid, struct resource *r) int bus_deactivate_resource(device_t dev, int type, int rid, struct resource *r)
Активирует или деактивирует ресурс. Возвращает 0 при успехе, в противном случае — код ошибки. Если ресурс разделяемый и в данный момент активирован другим драйвером, возвращается
EBUSY
.int bus_setup_intr(device_t dev, struct resource *r, int flags, driver_intr_t *handler, void *arg, void **cookiep) int bus_teardown_intr(device_t dev, struct resource *r, void *cookie)
Связывает или разрывает связь обработчика прерывания с устройством. Возвращает 0 при успехе, код ошибки в противном случае.
r - активированный обработчик ресурсов, описывающий IRQ
flags - уровень приоритета прерывания, один из:
INTR_TYPE_TTY
- терминалы и другие аналогичные символьные устройства. Для их маскировки используйтеspltty()
.(INTR_TYPE_TTY | INTR_TYPE_FAST)
- терминальные устройства с малым буфером ввода, критичные к потере данных на входе (например, устаревшие последовательные порты). Для их маскирования используйтеspltty()
.INTR_TYPE_BIO
- блочные устройства, за исключением тех, что подключены к контроллерам CAM. Для их маскирования используйтеsplbio()
.INTR_TYPE_CAM
- контроллеры шины CAM (Common Access Method). Для их маскирования используйтеsplcam()
.INTR_TYPE_NET
- контроллеры сетевых интерфейсов. Для их маскирования используйтеsplimp()
.INTR_TYPE_MISC
— прочие устройства. Нет другого способа их маскировки, кромеsplhigh()
, который маскирует все прерывания.
Когда обработчик прерывания выполняется, все другие прерывания, соответствующие его уровню приоритета, будут заблокированы. Единственное исключение — уровень MISC, для которого никакие другие прерывания не блокируются и который сам не блокируется другими прерываниями.
handler - указатель на функцию-обработчик, тип driver_intr_t определён как
void driver_intr_t(void *)
arg - аргумент, передаваемый обработчику для идентификации конкретного устройства. Приводится обработчиком от void* к фактическому типу. Старая конвенция для обработчиков прерываний ISA предполагала использование номера устройства в качестве аргумента, новая (рекомендуемая) конвенция предполагает использование указателя на структуру softc устройства.
cookie[p] - значение, полученное из
setup()
, используется для идентификации обработчика при передаче вteardown()
Определены несколько методов для работы с обработчиками ресурсов (struct resource *). Вот те из них, которые представляют интерес для разработчиков драйверов устройств:
u_long rman_get_start(r) u_long rman_get_end(r)
Получают начало и конец выделенного диапазона ресурсов.void *rman_get_virtual(r)
Получает виртуальный адрес активированного ресурса памяти.
10.6. Отображение памяти шины
Во многих случаях данные передаются между драйвером и устройством через память. Возможны два варианта:
(а) память расположена на карте устройства
(b) память — это основная память компьютера
В случае (a) драйвер всегда копирует данные между памятью на карте и основной памятью по мере необходимости. Для отображения памяти на карте в виртуальное адресное пространство ядра физический адрес и длина памяти на карте должны быть определены как ресурс SYS_RES_MEMORY
. Этот ресурс может быть затем выделен и активирован, а его виртуальный адрес получен с помощью rman_get_virtual()
. Более старые драйверы использовали для этой цели функцию pmap_mapdev()
, которую больше не следует использовать напрямую. Теперь это один из внутренних шагов активации ресурса.
Большинство ISA-карт имеют память, настроенную на физическое расположение в диапазоне 640 КБ–1 МБ. Некоторые ISA-карты требуют большего диапазона памяти, который должен быть размещён ниже 16 МБ (из-за 24-битного ограничения адресации на шине ISA). В таком случае, если в машине больше памяти, чем начальный адрес памяти устройства (другими словами, они пересекаются), необходимо настроить "дыру" в памяти по диапазону адресов, используемому устройствами. Многие BIOS позволяют настроить "дыру" в памяти размером 1 МБ, начиная с 14 МБ или 15 МБ. FreeBSD корректно обрабатывает "дыры" в памяти, если BIOS правильно их сообщает (эта функция может не работать в старых BIOS).
В случае (b) только адрес данных отправляется на устройство, и устройство использует DMA для фактического доступа к данным в основной памяти. Существуют два ограничения: во-первых, карты ISA могут обращаться только к памяти ниже 16 МБ. Во-вторых, непрерывные страницы в виртуальном адресном пространстве могут не быть непрерывными в физическом адресном пространстве, поэтому устройству может потребоваться выполнять операции scatter/gather. Подсистема шины предоставляет готовые решения для некоторых из этих проблем, остальное должно быть реализовано самими драйверами.
Для выделения памяти DMA используются две структуры: bus_dma_tag_t
и bus_dmamap_t
. Тег (tag
) описывает свойства, необходимые для памяти DMA. Карта (map
) представляет собой блок памяти, выделенный в соответствии с этими свойствами. С одним тегом может быть связано несколько карт.
Теги организованы в иерархию в виде дерева с наследованием свойств. Дочерний тег наследует все требования родительского тега и может делать их более строгими, но никогда более мягкими.
Обычно создается один корневой тег (без родителя) для каждого устройства. Если для каждого устройства требуется несколько областей памяти с разными требованиями, то для каждой из них может быть создан тег как дочерний по отношению к родительскому тегу.
Теги могут быть использованы для создания карты двумя способами.
Сначала может быть выделен (а затем освобожден) блок непрерывной памяти, соответствующий требованиям тега. Обычно это используется для выделения относительно долгоживущих областей памяти для взаимодействия с устройством. Загрузка такой памяти в карту тривиальна: она всегда рассматривается как один блок в соответствующем диапазоне физической памяти.
Второй момент: произвольная область виртуальной памяти может быть загружена в карту. Каждая страница этой памяти будет проверяться на соответствие требованиям карты. Если она соответствует, то остается на своем исходном месте. Если нет, то выделяется новая соответствующая промежуточная страница (bounce page), которая используется как промежуточное хранилище. При записи данных с несоответствующих исходных страниц они сначала копируются на свои промежуточные страницы, а затем передаются с промежуточных страниц на устройство. При чтении данные поступают с устройства на промежуточные страницы, а затем копируются на свои несоответствующие исходные страницы. Процесс копирования между исходными и промежуточными страницами называется синхронизацией. Обычно это используется для каждой передачи: буфер для каждой передачи загружается, передача выполняется, и буфер выгружается.
Функции, работающие с памятью DMA:
int bus_dma_tag_create(bus_dma_tag_t parent, bus_size_t alignment, bus_size_t boundary, bus_addr_t lowaddr, bus_addr_t highaddr, bus_dma_filter_t *filter, void *filterarg, bus_size_t maxsize, int nsegments, bus_size_t maxsegsz, int flags, bus_dma_tag_t *dmat)
Создать новый тег. Возвращает 0 при успехе, код ошибки в противном случае.
parent - родительский тег, или NULL для создания тега верхнего уровня.
alignment - требуемое физическое выравнивание области памяти, которая будет выделена для этого тега. Используйте значение 1 для "без специфического выравнивания". Применяется только к будущим вызовам
bus_dmamem_alloc()
, но неbus_dmamap_create()
.boundary - физическая граница адреса, которую нельзя пересекать при выделении памяти. Используйте значение 0 для обозначения "нет границы". Применяется только к будущим вызовам
bus_dmamem_alloc()
, но неbus_dmamap_create()
. Должна быть степенью 2. Если память планируется использовать в некаскадном режиме DMA (т.е. адреса DMA будут предоставляться не самим устройством, а контроллером DMA ISA), то граница не должна превышать 64 КБ (64*1024) из-за ограничений аппаратного обеспечения DMA.lowaddr, highaddr - названия немного вводят в заблуждение; эти значения используются для ограничения допустимого диапазона физических адресов, используемых для выделения памяти. Точное значение зависит от предполагаемого будущего использования:
Для
bus_dmamem_alloc()
все адреса от 0 до lowaddr-1 считаются разрешёнными, а более высокие — запрещёнными.Для
bus_dmamap_create()
все адреса вне включительного диапазона [lowaddr; highaddr] считаются доступными. Адреса страниц внутри диапазона передаются в функцию-фильтр, которая определяет, доступны ли они. Если функция-фильтр не предоставлена, то весь диапазон считается недоступным.Для устройств ISA обычные значения (без функции фильтрации) следующие:
lowaddr = BUS_SPACE_MAXADDR_24BIT
highaddr = BUS_SPACE_MAXADDR
filter, filterarg - функция фильтра и её аргумент. Если передаётся NULL для filter, то весь диапазон [lowaddr, highaddr] считается недоступным при выполнении
bus_dmamap_create()
. В противном случае физический адрес каждой страницы в диапазоне [lowaddr; highaddr] передаётся в функцию фильтра, которая определяет, доступна ли она. Прототип функции фильтра:int filterfunc(void *arg, bus_addr_t paddr)
. Функция должна вернуть 0, если страница доступна, и ненулевое значение в противном случае.maxsize - максимальный размер памяти (в байтах), который может быть выделен через этот тег. Если сложно оценить или он может быть произвольно большим, для устройств ISA следует использовать значение
BUS_SPACE_MAXSIZE_24BIT
.nsegments - максимальное количество сегментов scatter-gather, поддерживаемых устройством. Если ограничений нет, следует использовать значение
BUS_SPACE_UNRESTRICTED
. Это значение рекомендуется для родительских тегов, фактические ограничения затем будут указаны для дочерних тегов. Теги с nsegments равнымBUS_SPACE_UNRESTRICTED
не могут использоваться для фактической загрузки отображений, они могут применяться только как родительские теги. Практический предел для nsegments составляет около 250-300, более высокие значения вызовут переполнение стека ядра (аппаратное обеспечение обычно не поддерживает такое большое количество scatter-gather буферов в любом случае).maxsegsz — максимальный размер сегмента scatter-gather, поддерживаемый устройством. Максимальное значение для устройства ISA будет
BUS_SPACE_MAXSIZE_24BIT
.flags - битовая маска флагов. Единственный интересный флаг:
BUS_DMA_ALLOCNOW - запрашивает выделение всех потенциально необходимых промежуточных страниц при создании тега.
dmat - указатель на хранилище для нового возвращаемого тега.
int bus_dma_tag_destroy(bus_dma_tag_t dmat)
Уничтожить тег. Возвращает 0 при успехе, код ошибки в противном случае.
dmat - тег, который должен быть уничтожен.
int bus_dmamem_alloc(bus_dma_tag_t dmat, void** vaddr, int flags, bus_dmamap_t *mapp)
Выделить область непрерывной памяти, описанную тегом. Размер выделяемой памяти соответствует maxsize тега. Возвращает 0 при успехе, иначе код ошибки. Результат всё ещё должен быть загружен с помощью
bus_dmamap_load()
перед использованием для получения физического адреса памяти.dmat - тег
vaddr - указатель на хранилище для возвращаемого виртуального адреса ядра выделенной области.
flags - битовая карта флагов. Единственный интересный флаг:
BUS_DMA_NOWAIT - если память недоступна немедленно, вернуть ошибку. Если этот флаг не установлен, то процедуре разрешено ожидать до тех пор, пока память не станет доступной.
mapp - указатель на хранилище для возвращаемой новой карты.
void bus_dmamem_free(bus_dma_tag_t dmat, void *vaddr, bus_dmamap_t map)
Освободить память, выделенную
bus_dmamem_alloc()
. В настоящее время освобождение памяти, выделенной с ограничениями ISA, не реализовано. В связи с этим рекомендуется сохранять и повторно использовать выделенные области как можно дольше. Не следует без необходимости освобождать область и вскоре снова её выделять. Это не означает, чтоbus_dmamem_free()
не следует использовать вовсе: есть надежда, что вскоре она будет реализована должным образом.dmat - тег
vaddr - виртуальный адрес памяти ядра
map - карта памяти (как возвращается из
bus_dmamem_alloc()
)
int bus_dmamap_create(bus_dma_tag_t dmat, int flags, bus_dmamap_t *mapp)
Создать карту для тега, которая будет использоваться в
bus_dmamap_load()
позже. Возвращает 0 при успехе, в противном случае — код ошибки.dmat - тег
flags - теоретически, битовая карта флагов. Однако пока никакие флаги не определены, поэтому в настоящее время значение всегда будет 0.
mapp - указатель на хранилище для новой карты, которая будет возвращена
int bus_dmamap_destroy(bus_dma_tag_t dmat, bus_dmamap_t map)
Уничтожить карту. Возвращает 0 при успехе, в противном случае — код ошибки.
dmat - тег, с которым ассоциирована карта
map - карта, подлежащая уничтожению
int bus_dmamap_load(bus_dma_tag_t dmat, bus_dmamap_t map, void *buf, bus_size_t buflen, bus_dmamap_callback_t *callback, void *callback_arg, int flags)
Загрузить буфер в карту (карта должна быть предварительно создана с помощью
bus_dmamap_create()
илиbus_dmamem_alloc()
). Все страницы буфера проверяются на соответствие требованиям тега, и для несоответствующих выделяются промежуточные страницы. Создается массив дескрипторов физических сегментов и передается в подпрограмму обратного вызова. Ожидается, что эта подпрограмма обработает его каким-либо образом. Количество промежуточных буферов в системе ограничено, поэтому, если эти буферы требуются, но недоступны немедленно, запрос будет поставлен в очередь, и обратный вызов будет выполнен, когда промежуточные буферы станут доступны. Возвращает 0, если обратный вызов был выполнен немедленно, илиEINPROGRESS
, если запрос был поставлен в очередь для выполнения в будущем. В последнем случае синхронизация с подпрограммой обратного вызова, поставленной в очередь, является обязанностью драйвера.dmat - тег
map - карта
buf - виртуальный адрес буфера в пространстве ядра
buflen - длина буфера
callback,
callback_arg
- функция обратного вызова и её аргументПрототип функции обратного вызова:
void callback(void *arg, bus_dma_segment_t *seg, int nseg, int error)
arg - то же самое, что и callback_arg, переданный в
bus_dmamap_load()
seg - массив дескрипторов сегментов
nseg - количество дескрипторов в массиве
error - указание на переполнение номера сегмента: если установлено значение
EFBIG
, значит буфер не поместился в максимальное количество сегментов, разрешённых тегом. В этом случае в массиве будет только разрешённое количество дескрипторов. Обработка этой ситуации зависит от драйвера: в зависимости от желаемой семантики он может либо считать это ошибкой, либо разделить буфер на две части и обработать вторую часть отдельноКаждая запись в массиве segments содержит поля:
ds_addr - физический адрес шины сегмента
ds_len - длина сегмента
void bus_dmamap_unload(bus_dma_tag_t dmat, bus_dmamap_t map)
выгрузить карту.
dmat - тег
map - загруженная карта
void bus_dmamap_sync (bus_dma_tag_t dmat, bus_dmamap_t map, bus_dmasync_op_t op)
Синхронизировать загруженный буфер с его промежуточными страницами до и после физической передачи на устройство или с устройства. Это функция, которая выполняет все необходимое копирование данных между исходным буфером и его отображенной версией. Буферы должны быть синхронизированы как до, так и после выполнения передачи.
dmat - тег
map - загруженная карта
op - тип операции синхронизации для выполнения:
BUS_DMASYNC_PREREAD
- перед чтением с устройства в буферBUS_DMASYNC_POSTREAD
- после чтения из устройства в буферBUS_DMASYNC_PREWRITE
- перед записью буфера в устройствоBUS_DMASYNC_POSTWRITE
- после записи буфера в устройство
На данный момент PREREAD и POSTWRITE являются пустыми операциями, но это может измениться в будущем, поэтому их нельзя игнорировать в драйвере. Синхронизация не требуется для памяти, полученной из bus_dmamem_alloc()
.
Перед вызовом функции обратного вызова из bus_dmamap_load()
массив сегментов сохраняется в стеке. Он предварительно выделяется для максимального количества сегментов, разрешенного тегом. В результате этого практический предел количества сегментов на архитектуре i386 составляет около 250-300 (размер стека ядра — 4 КБ минус размер структуры пользователя, размер элемента массива сегментов — 8 байт, и необходимо оставить некоторое пространство). Поскольку массив выделяется исходя из максимального числа, это значение не должно быть установлено выше, чем действительно необходимо. К счастью, для большинства оборудования максимально поддерживаемое количество сегментов значительно ниже. Но если драйвер должен обрабатывать буферы с очень большим количеством сегментов scatter-gather, он должен делать это по частям: загрузить часть буфера, передать его устройству, загрузить следующую часть буфера и так далее.
Еще одно практическое следствие заключается в том, что количество сегментов может ограничивать размер буфера. Если все страницы в буфере окажутся физически несмежными, то максимальный поддерживаемый размер буфера для такого фрагментированного случая будет равен (nsegments * page_size). Например, если поддерживается максимальное количество сегментов, равное 10, то на i386 максимальный гарантированно поддерживаемый размер буфера составит 40K. Если требуется больший размер, то в драйвере следует использовать специальные приемы.
Если оборудование не поддерживает scatter-gather вообще или драйвер хочет поддерживать некоторый размер буфера, даже если он сильно фрагментирован, то решение состоит в выделении непрерывного буфера в драйвере и использовании его в качестве промежуточного хранилища, если исходный буфер не подходит.
Ниже представлены типичные последовательности вызовов при использовании карты в зависимости от её назначения. Символы → используются для обозначения последовательности во времени.
Для буфера, который остается практически неизменным в течение всего времени между присоединением и отсоединением устройства:
bus_dmamem_alloc → bus_dmamap_load → …use buffer… → → bus_dmamap_unload → bus_dmamem_free
Для буфера, который часто изменяется и передается извне драйвера:
bus_dmamap_create -> -> bus_dmamap_load -> bus_dmamap_sync(PRE...) -> do transfer -> -> bus_dmamap_sync(POST...) -> bus_dmamap_unload -> ... -> bus_dmamap_load -> bus_dmamap_sync(PRE...) -> do transfer -> -> bus_dmamap_sync(POST...) -> bus_dmamap_unload -> -> bus_dmamap_destroy
При загрузке карты, созданной bus_dmamem_alloc()
, переданные адрес и размер буфера должны быть такими же, как использованные в bus_dmamem_alloc()
. В этом случае гарантируется, что весь буфер будет отображен как один сегмент (так что обратный вызов может основываться на этом предположении) и запрос будет выполнен немедленно (EINPROGRESS никогда не будет возвращен). Все, что нужно сделать обратному вызову в этом случае, — это сохранить физический адрес.
Типичный пример:
static void alloc_callback(void *arg, bus_dma_segment_t *seg, int nseg, int error) { *(bus_addr_t *)arg = seg[0].ds_addr; } ... int error; struct somedata { .... }; struct somedata *vsomedata; /* virtual address */ bus_addr_t psomedata; /* physical bus-relative address */ bus_dma_tag_t tag_somedata; bus_dmamap_t map_somedata; ... error=bus_dma_tag_create(parent_tag, alignment, boundary, lowaddr, highaddr, /*filter*/ NULL, /*filterarg*/ NULL, /*maxsize*/ sizeof(struct somedata), /*nsegments*/ 1, /*maxsegsz*/ sizeof(struct somedata), /*flags*/ 0, &tag_somedata); if(error) return error; error = bus_dmamem_alloc(tag_somedata, &vsomedata, /* flags*/ 0, &map_somedata); if(error) return error; bus_dmamap_load(tag_somedata, map_somedata, (void *)vsomedata, sizeof (struct somedata), alloc_callback, (void *) &psomedata, /*flags*/0);
Выглядит немного длинно и сложно, но это правильный способ. Практическое следствие таково: если несколько областей памяти выделяются всегда вместе, было бы отличной идеей объединить их все в одну структуру и выделять как единое целое (если ограничения выравнивания и границ позволяют).
При загрузке произвольного буфера в карту, созданную bus_dmamap_create()
, необходимо принять специальные меры для синхронизации с обратным вызовом, если он будет задержан. Код будет выглядеть следующим образом:
{ int s; int error; s = splsoftvm(); error = bus_dmamap_load( dmat, dmamap, buffer_ptr, buffer_len, callback, /*callback_arg*/ buffer_descriptor, /*flags*/0); if (error == EINPROGRESS) { /* * Do whatever is needed to ensure synchronization * with callback. Callback is guaranteed not to be started * until we do splx() or tsleep(). */ } splx(s); }
Два возможных подхода для обработки запросов:
Если запросы завершаются путём явной пометки их как выполненных (например, запросы CAM), то было бы проще поместить всю дальнейшую обработку в драйвер обратного вызова, который отмечал бы запрос по его завершении. В этом случае не потребуется много дополнительной синхронизации. По соображениям управления потоком может быть полезно заморозить очередь запросов до завершения этого запроса.
Если запросы завершаются при возврате функции (например, классические запросы на чтение или запись для символьных устройств), то в дескрипторе буфера должен быть установлен флаг синхронизации и вызвана функция
tsleep()
. Позже, когда будет вызван обратный вызов, он выполнит свою обработку и проверит этот флаг синхронизации. Если флаг установлен, обратный вызов должен инициировать пробуждение. При таком подходе функция обратного вызова может либо выполнить всю необходимую обработку (как в предыдущем случае), либо просто сохранить массив сегментов в дескрипторе буфера. Затем после завершения обратного вызова вызывающая функция может использовать этот сохранённый массив сегментов и выполнить всю обработку.
10.7. DMA
Прямой доступ к памяти (DMA) реализован в шине ISA через контроллер DMA (на самом деле их два, но это несущественная деталь). Чтобы сделать ранние устройства ISA простыми и дешёвыми, логика управления шиной и генерации адресов была сосредоточена в контроллере DMA. К счастью, FreeBSD предоставляет набор функций, которые в основном скрывают раздражающие детали работы контроллера DMA от драйверов устройств.
Самый простой случай — для достаточно интеллектуальных устройств. Например, устройства с bus mastering на PCI могут сами генерировать шинные циклы и адреса памяти. Единственное, что им действительно нужно от контроллера DMA, — это арбитраж шины. Для этой цели они притворяются каскадированными подчинёнными контроллерами DMA. И единственное, что требуется от системного контроллера DMA, — это включить каскадный режим на канале DMA, вызвав следующую функцию при присоединении драйвера:
void isa_dmacascade(int channel_number)
Все последующие действия выполняются путем программирования устройства. При отсоединении драйвера нет необходимости вызывать функции, связанные с DMA.
Для более простых устройств всё становится сложнее. Используются следующие функции:
int isa_dma_acquire(int chanel_number)
Зарезервировать канал DMA. Возвращает 0 при успехе или EBUSY, если канал уже зарезервирован этим или другим драйвером. Большинство устройств ISA не способны совместно использовать каналы DMA, поэтому обычно эта функция вызывается при присоединении устройства. Это резервирование стало избыточным с появлением современного интерфейса ресурсов шины, но всё ещё должно использоваться в дополнение к последнему. Если резервирование не использовать, то в дальнейшем другие процедуры DMA вызовут панику ядра.
int isa_dma_release(int chanel_number)
Освободить ранее зарезервированный канал DMA. На момент освобождения канала не должно быть активных передач (дополнительно устройство не должно пытаться инициировать передачу после освобождения канала).
void isa_dmainit(int chan, u_int bouncebufsize)
Выделить промежуточный буфер для использования с указанным каналом. Запрашиваемый размер буфера не может превышать 64 КБ. Этот промежуточный буфер будет автоматически использован в дальнейшем, если передаваемый буфер окажется не физически непрерывным, находится вне памяти, доступной шине ISA, или пересекает границу 64 КБ. Если передача всегда будет выполняться из буферов, соответствующих этим условиям (например, выделенных с помощью
bus_dmamem_alloc()
с соответствующими ограничениями), то вызовisa_dmainit()
не требуется. Однако довольно удобно передавать произвольные данные с использованием контроллера DMA. Промежуточный буфер автоматически решит проблемы в ситуациях, когда данные разбросаны в памяти, и их надо собирать.chan - номер канала
bouncebufsize - размер промежуточного буфера в байтах
void isa_dmastart(int flags, caddr_t addr, u_int nbytes, int chan)
Подготовка к началу передачи DMA. Эта функция должна быть вызвана для настройки контроллера DMA перед фактическим началом передачи на устройстве. Она проверяет, что буфер является непрерывным и попадает в диапазон памяти ISA, если нет, то автоматически используется промежуточный буфер. Если требуется промежуточный буфер, но он не настроен с помощью
isa_dmainit()
или слишком мал для запрошенного размера передачи, система перейдет в состояние паники. В случае запроса на запись с промежуточным буфером данные будут автоматически скопированы в этот буфер.flags - битовая маска, определяющая тип выполняемой операции. Бит направления B_READ и B_WRITE являются взаимоисключающими.
B_READ - чтение с шины ISA в память
B_WRITE - запись из памяти на шину ISA
B_RAW - если установлен, то контроллер DMA запомнит буфер и после завершения передачи автоматически переинициализирует себя для повторной передачи того же буфера (конечно, драйвер может изменить данные в буфере перед инициированием следующей передачи на устройстве). Если не установлен, то параметры будут работать только для одной передачи, и перед инициированием следующей передачи снова потребуется вызвать
isa_dmastart()
. Использование B_RAW имеет смысл только если промежуточный буфер не используется.
addr - виртуальный адрес буфера
nbytes - длина буфера. Должна быть меньше или равна 64 КБ. Длина 0 не допускается: контроллер DMA интерпретирует это как 64 КБ, в то время как код ядра поймёт это как 0, что приведёт к непредсказуемым последствиям. Для каналов номер 4 и выше длина должна быть чётной, так как эти каналы передают по 2 байта за раз. В случае нечётной длины последний байт не будет передан.
chan - номер канала
void isa_dmadone(int flags, caddr_t addr, int nbytes, int chan)
Синхронизировать память после того, как устройство сообщает о завершении передачи. Если это была операция чтения с промежуточным буфером, то данные будут скопированы из этого буфера в исходный буфер. Аргументы такие же, как у
isa_dmastart()
. Флаг B_RAW разрешён, но он никак не влияет наisa_dmadone()
.int isa_dmastatus(int channel_number)
Возвращает количество оставшихся для передачи байт в текущей передаче. Если флаг B_READ был установлен в
isa_dmastart()
, возвращаемое значение никогда не будет равно нулю. В конце передачи оно автоматически сбрасывается обратно к длине буфера. Обычное использование — проверка количества оставшихся байт после того, как устройство сигнализирует о завершении передачи. Если количество байт не равно 0, то, вероятно, в передаче произошла ошибка.int isa_dmastop(int channel_number)
Прерывает текущую передачу и возвращает количество непереданных байтов.
10.8. xxx_isa_probe
Эта функция проверяет наличие устройства. Если драйвер поддерживает автоматическое определение некоторых параметров конфигурации устройства (таких как вектор прерывания или адрес памяти), это автоматическое определение должно выполняться в данной процедуре.
Как и для любой другой шины, если устройство не может быть обнаружено, или обнаружено, но не прошло самопроверку, или возникла другая проблема, то возвращается положительное значение ошибки. Значение ENXIO
должно возвращаться, если устройство отсутствует. Другие значения ошибок могут означать иные условия. Нулевые или отрицательные значения означают успех. Большинство драйверов возвращают ноль в случае успеха.
Отрицательные возвращаемые значения используются, когда устройство PnP поддерживает несколько интерфейсов. Например, старый совместимый интерфейс и новый расширенный интерфейс, которые поддерживаются разными драйверами. В этом случае оба драйвера обнаружат устройство. Драйвер, который возвращает большее значение в процедуре обнаружения, получает приоритет (другими словами, драйвер, возвращающий 0, имеет наивысший приоритет, возвращающий -1 — следующий, возвращающий -2 — за ним и так далее). В результате устройства, поддерживающие только старый интерфейс, будут обрабатываться старым драйвером (который должен возвращать -1 из процедуры probe), а устройства, поддерживающие также новый интерфейс, будут обрабатываться новым драйвером (который должен возвращать 0 из процедуры обнаружения).
Структура дескриптора устройства xxx_softc
выделяется системой до вызова процедуры обнаружения. Если процедура обнаружения возвращает ошибку, дескриптор автоматически освобождается системой. Поэтому при возникновении ошибки обнаружения драйвер должен убедиться, что все ресурсы, использованные во время обнаружения, освобождены и ничто не мешает безопасному освобождению дескриптора.Если обнаружение завершается успешно, дескриптор сохраняется системой и позже передаётся в процедуру xxx_isa_attach()
. Если драйвер возвращает отрицательное значение, он не может быть уверен, что получит наивысший приоритет и его процедура присоединения будет вызвана. Поэтому в этом случае он также должен освободить все ресурсы перед возвратом и, если необходимо, выделить их снова в процедуре присоединения. Когда xxx_isa_probe()
возвращает 0, освобождение ресурсов перед возвратом также является хорошей практикой, и корректно работающий драйвер должен так поступать. Однако в случаях, когда возникают проблемы с освобождением ресурсов, драйверу разрешается сохранять ресурсы между возвратом 0 из процедуры обнаружения и выполнением процедуры присоединения.
Типичная процедура обнаружения начинается с получения дескриптора устройства и номера устройства:
struct xxx_softc *sc = device_get_softc(dev); int unit = device_get_unit(dev); int pnperror; int error = 0; sc->dev = dev; /* link it back */ sc->unit = unit;
Затем проверьте устройства PnP. Проверка осуществляется с помощью таблицы, содержащей список PnP ID, поддерживаемых этим драйвером, и удобочитаемые описания моделей устройств, соответствующих этим ID.
pnperror=ISA_PNP_PROBE(device_get_parent(dev), dev, xxx_pnp_ids); if(pnperror == ENXIO) return ENXIO;
Логика работы ISA_PNP_PROBE
следующая: если данная карта (устройство) не была обнаружена как PnP, то будет возвращено ENOENT
. Если она была обнаружена как PnP, но её обнаруженный ID не совпадает ни с одним из ID в таблице, то возвращается ENXIO
. Наконец, если устройство поддерживает PnP и его ID совпадает с одним из ID в таблице, возвращается 0
, а соответствующее описание из таблицы устанавливается с помощью device_set_desc()
.
Если драйвер поддерживает только устройства PnP, то условие будет выглядеть следующим образом:
if(pnperror != 0) return pnperror;
Для драйверов, которые не поддерживают PnP, не требуется специальной обработки, так как они передают пустую таблицу идентификаторов PnP и всегда будут получать ENXIO при вызове на PnP-карте.
Функция обнаружения обычно требует как минимум некоторый минимальный набор ресурсов, например, номер порта ввода-вывода, чтобы найти карту и проверить её. В зависимости от оборудования драйвер может автоматически обнаружить другие необходимые ресурсы. Устройства PnP имеют все ресурсы, предварительно установленные подсистемой PnP, поэтому драйверу не нужно обнаруживать их самостоятельно.
Обычно минимальная информация, необходимая для доступа к устройству, — это номер порта ввода-вывода. Затем некоторые устройства позволяют получить остальную информацию из регистров конфигурации устройства (хотя не все устройства это поддерживают). Поэтому сначала мы пытаемся получить начальное значение порта:
sc->port0 = bus_get_resource_start(dev, SYS_RES_IOPORT, 0 /*rid*/); if(sc->port0 == 0) return ENXIO;
Базовый адрес порта сохраняется в структуре softc для последующего использования. Если он будет использоваться очень часто, то вызов функции ресурса каждый раз будет неприемлемо медленным. Если мы не получаем порт, мы просто возвращаем ошибку. Некоторые драйверы устройств могут вместо этого быть умнее и попытаться обнаружить все возможные порты, например:
/* table of all possible base I/O port addresses for this device */ static struct xxx_allports { u_short port; /* port address */ short used; /* flag: if this port is already used by some unit */ } xxx_allports = { { 0x300, 0 }, { 0x320, 0 }, { 0x340, 0 }, { 0, 0 } /* end of table */ }; ... int port, i; ... port = bus_get_resource_start(dev, SYS_RES_IOPORT, 0 /*rid*/); if(port !=0 ) { for(i=0; xxx_allports[i].port!=0; i++) { if(xxx_allports[i].used || xxx_allports[i].port != port) continue; /* found it */ xxx_allports[i].used = 1; /* do probe on a known port */ return xxx_really_probe(dev, port); } return ENXIO; /* port is unknown or already used */ } /* we get here only if we need to guess the port */ for(i=0; xxx_allports[i].port!=0; i++) { if(xxx_allports[i].used) continue; /* mark as used - even if we find nothing at this port * at least we won't probe it in future */ xxx_allports[i].used = 1; error = xxx_really_probe(dev, xxx_allports[i].port); if(error == 0) /* found a device at that port */ return 0; } /* probed all possible addresses, none worked */ return ENXIO;
Конечно, обычно для таких вещей следует использовать процедуру identify()
драйвера. Однако может быть одна веская причина, почему лучше сделать это в probe()
: если этот обнаружение может привести к сбою другого чувствительного устройства. Процедуры обнаружения упорядочены с учетом флага sensitive
: чувствительные устройства проверяются первыми, а остальные устройства — позже. Но процедуры identify()
вызываются до любого обнаружения, поэтому они не учитывают чувствительные устройства и могут вызвать их сбой.
Вот, после того как мы получили начальный порт, необходимо установить количество портов (за исключением устройств PnP), так как в файле конфигурации ядра эта информация отсутствует.
if(pnperror /* only for non-PnP devices */ && bus_set_resource(dev, SYS_RES_IOPORT, 0, sc->port0, XXX_PORT_COUNT)<0) return ENXIO;
Наконец, выделите и активируйте часть адресного пространства порта (специальные значения start и end означают "используйте те, что мы установили через bus_set_resource()
"):
sc->port0_rid = 0; sc->port0_r = bus_alloc_resource(dev, SYS_RES_IOPORT, &sc->port0_rid, /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE); if(sc->port0_r == NULL) return ENXIO;
Теперь, имея доступ к регистрам с отображением на порты, мы можем каким-либо образом взаимодействовать с устройством и проверить, реагирует ли оно так, как ожидается. Если этого не происходит, вероятно, по этому адресу находится другое устройство или его там нет вовсе.
Обычно драйверы не настраивают обработчики прерываний до вызова процедуры присоединения. Вместо этого они выполняют проверки в режиме опроса, используя функцию DELAY()
для таймаута. Процедура проверки никогда не должна зависать навсегда, все ожидания ответа от устройства должны выполняться с таймаутами. Если устройство не отвечает в течение заданного времени, вероятно, оно неисправно или неправильно настроено, и драйвер должен вернуть ошибку. При определении интервала таймаута следует давать устройству дополнительное время для надежности: хотя предполагается, что DELAY()
задерживает выполнение на одинаковое время на любой машине, существует некоторая погрешность, зависящая от конкретного процессора.
Если процедура проверки действительно хочет убедиться, что прерывания работают, она может также настроить и провести обнаружение прерываний. Однако это не рекомендуется.
/* implemented in some very device-specific way */ if(error = xxx_probe_ports(sc)) goto bad; /* will deallocate the resources before returning */
Функция xxx_probe_ports()
также может устанавливать описание устройства в зависимости от конкретной модели обнаруженного устройства. Но если поддерживается только одна модель устройства, это можно сделать и жёстко заданным способом. Конечно, для PnP-устройств поддержка PnP автоматически устанавливает описание из таблицы.
if(pnperror) device_set_desc(dev, "Our device model 1234");
Затем процедура обнаружения должна либо определить диапазоны всех ресурсов, читая регистры конфигурации устройства, либо убедиться, что они были явно заданы пользователем. Мы рассмотрим это на примере встроенной памяти. Процедура обнаружения должна быть как можно менее навязчивой, поэтому выделение и проверку функциональности остальных ресурсов (кроме портов) лучше оставить для процедуры присоединения.
Адрес памяти может быть указан в конфигурационном файле ядра, а на некоторых устройствах он может быть предварительно настроен в энергонезависимых конфигурационных регистрах. Если доступны оба источника, и они различаются, какой из них следует использовать? Вероятно, если пользователь явно указал адрес в конфигурационном файле ядра, он знает, что делает, и этот адрес должен иметь приоритет. Пример реализации может выглядеть так:
/* try to find out the config address first */ sc->mem0_p = bus_get_resource_start(dev, SYS_RES_MEMORY, 0 /*rid*/); if(sc->mem0_p == 0) { /* nope, not specified by user */ sc->mem0_p = xxx_read_mem0_from_device_config(sc); if(sc->mem0_p == 0) /* can't get it from device config registers either */ goto bad; } else { if(xxx_set_mem0_address_on_device(sc) < 0) goto bad; /* device does not support that address */ } /* just like the port, set the memory size, * for some devices the memory size would not be constant * but should be read from the device configuration registers instead * to accommodate different models of devices. Another option would * be to let the user set the memory size as "msize" configuration * resource which will be automatically handled by the ISA bus. */ if(pnperror) { /* only for non-PnP devices */ sc->mem0_size = bus_get_resource_count(dev, SYS_RES_MEMORY, 0 /*rid*/); if(sc->mem0_size == 0) /* not specified by user */ sc->mem0_size = xxx_read_mem0_size_from_device_config(sc); if(sc->mem0_size == 0) { /* suppose this is a very old model of device without * auto-configuration features and the user gave no preference, * so assume the minimalistic case * (of course, the real value will vary with the driver) */ sc->mem0_size = 8*1024; } if(xxx_set_mem0_size_on_device(sc) < 0) goto bad; /* device does not support that size */ if(bus_set_resource(dev, SYS_RES_MEMORY, /*rid*/0, sc->mem0_p, sc->mem0_size)<0) goto bad; } else { sc->mem0_size = bus_get_resource_count(dev, SYS_RES_MEMORY, 0 /*rid*/); }
Ресурсы для IRQ и DRQ легко проверить по аналогии.
Если всё прошло успешно, то освободите все ресурсы и верните успешный статус.
xxx_free_resources(sc); return 0;
Наконец, обработайте проблемные ситуации. Все ресурсы должны быть освобождены перед возвратом. Мы используем тот факт, что перед передачей нам структуры softc
она обнуляется, поэтому мы можем определить, был ли выделен какой-либо ресурс: если его дескриптор не равен нулю.
bad: xxx_free_resources(sc); if(error) return error; else /* exact error is unknown */ return ENXIO;
Вот и всё для процедуры обнаружения. Освобождение ресурсов выполняется из нескольких мест, поэтому оно вынесено в функцию, которая может выглядеть так:
static void xxx_free_resources(sc) struct xxx_softc *sc; { /* check every resource and free if not zero */ /* interrupt handler */ if(sc->intr_r) { bus_teardown_intr(sc->dev, sc->intr_r, sc->intr_cookie); bus_release_resource(sc->dev, SYS_RES_IRQ, sc->intr_rid, sc->intr_r); sc->intr_r = 0; } /* all kinds of memory maps we could have allocated */ if(sc->data_p) { bus_dmamap_unload(sc->data_tag, sc->data_map); sc->data_p = 0; } if(sc->data) { /* sc->data_map may be legitimately equal to 0 */ /* the map will also be freed */ bus_dmamem_free(sc->data_tag, sc->data, sc->data_map); sc->data = 0; } if(sc->data_tag) { bus_dma_tag_destroy(sc->data_tag); sc->data_tag = 0; } ... free other maps and tags if we have them ... if(sc->parent_tag) { bus_dma_tag_destroy(sc->parent_tag); sc->parent_tag = 0; } /* release all the bus resources */ if(sc->mem0_r) { bus_release_resource(sc->dev, SYS_RES_MEMORY, sc->mem0_rid, sc->mem0_r); sc->mem0_r = 0; } ... if(sc->port0_r) { bus_release_resource(sc->dev, SYS_RES_IOPORT, sc->port0_rid, sc->port0_r); sc->port0_r = 0; } }
10.9. xxx_isa_attach
Процедура присоединения фактически подключает драйвер к системе, если процедура обнаружения вернула успех, и система решила подключить этот драйвер. Если процедура обнаружения вернула 0, то процедура присоединения может ожидать, что получит структуру устройства softc
в неизменном виде, как она была установлена процедурой обнаружения. Также, если обнаружение возвращает 0, она можно ожидать, что процедура присоединения для этого устройства будет вызвана в какой-то момент в будущем. Если процедура обнаружения возвращает отрицательное значение, то драйвер не может делать никаких из этих предположений.
Процедура присоединение возвращает 0 при успешном завершении или код ошибки в противном случае.
Процедура присоединения начинается так же, как и процедура обнаружения, с помещения часто используемых данных в более доступные переменные.
struct xxx_softc *sc = device_get_softc(dev); int unit = device_get_unit(dev); int error = 0;
Затем выделите и активируйте все необходимые ресурсы. Обычно диапазон портов освобождается перед возвратом из обнаружения, поэтому его необходимо выделить снова. Мы предполагаем, что процедура обнаружения корректно установила все диапазоны ресурсов, а также сохранила их в структуре softc. Если процедура обнаружения оставила некоторые ресурсы выделенными, то их не нужно выделять снова (это будет считаться ошибкой).
sc->port0_rid = 0; sc->port0_r = bus_alloc_resource(dev, SYS_RES_IOPORT, &sc->port0_rid, /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE); if(sc->port0_r == NULL) return ENXIO; /* on-board memory */ sc->mem0_rid = 0; sc->mem0_r = bus_alloc_resource(dev, SYS_RES_MEMORY, &sc->mem0_rid, /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE); if(sc->mem0_r == NULL) goto bad; /* get its virtual address */ sc->mem0_v = rman_get_virtual(sc->mem0_r);
Канал запроса DMA (DRQ) выделяется аналогично. Для его инициализации используйте функции семейства isa_dma*()
. Например:
isa_dmacascade(sc→drq0);
Строка запроса прерывания (IRQ) является особенной. Помимо выделения, обработчик прерывания драйвера должен быть связан с ней. Исторически в старых драйверах ISA аргумент, передаваемый системой обработчику прерывания, был номером устройства. Однако в современных драйверах принято передавать указатель на структуру softc
. Важная причина этого заключается в том, что когда структуры softc
выделяются динамически, получение номера устройства из softc
является простым, в то время как получение softc
из номера устройства затруднительно. Кроме того, такое соглашение делает драйверы для различных шин более однородными и позволяет им совместно использовать код: каждая шина получает свои собственные процедуры обнаружения, присоединения, отсоединения и другие специфичные для шины функции, в то время как основная часть кода драйвера может быть общей для них.
sc->intr_rid = 0; sc->intr_r = bus_alloc_resource(dev, SYS_RES_MEMORY, &sc->intr_rid, /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE); if(sc->intr_r == NULL) goto bad; /* * XXX_INTR_TYPE is supposed to be defined depending on the type of * the driver, for example as INTR_TYPE_CAM for a CAM driver */ error = bus_setup_intr(dev, sc->intr_r, XXX_INTR_TYPE, (driver_intr_t *) xxx_intr, (void *) sc, &sc->intr_cookie); if(error) goto bad;
Если устройству необходимо выполнять DMA в основную память, то эта память должна быть выделена, как описано ранее:
error=bus_dma_tag_create(NULL, /*alignment*/ 4, /*boundary*/ 0, /*lowaddr*/ BUS_SPACE_MAXADDR_24BIT, /*highaddr*/ BUS_SPACE_MAXADDR, /*filter*/ NULL, /*filterarg*/ NULL, /*maxsize*/ BUS_SPACE_MAXSIZE_24BIT, /*nsegments*/ BUS_SPACE_UNRESTRICTED, /*maxsegsz*/ BUS_SPACE_MAXSIZE_24BIT, /*flags*/ 0, &sc->parent_tag); if(error) goto bad; /* many things get inherited from the parent tag * sc->data is supposed to point to the structure with the shared data, * for example for a ring buffer it could be: * struct { * u_short rd_pos; * u_short wr_pos; * char bf[XXX_RING_BUFFER_SIZE] * } *data; */ error=bus_dma_tag_create(sc->parent_tag, 1, 0, BUS_SPACE_MAXADDR, 0, /*filter*/ NULL, /*filterarg*/ NULL, /*maxsize*/ sizeof(* sc->data), /*nsegments*/ 1, /*maxsegsz*/ sizeof(* sc->data), /*flags*/ 0, &sc->data_tag); if(error) goto bad; error = bus_dmamem_alloc(sc->data_tag, &sc->data, /* flags*/ 0, &sc->data_map); if(error) goto bad; /* xxx_alloc_callback() just saves the physical address at * the pointer passed as its argument, in this case &sc->data_p. * See details in the section on bus memory mapping. * It can be implemented like: * * static void * xxx_alloc_callback(void *arg, bus_dma_segment_t *seg, * int nseg, int error) * { * *(bus_addr_t *)arg = seg[0].ds_addr; * } */ bus_dmamap_load(sc->data_tag, sc->data_map, (void *)sc->data, sizeof (* sc->data), xxx_alloc_callback, (void *) &sc->data_p, /*flags*/0);
После выделения всех необходимых ресурсов устройство должно быть инициализировано. Инициализация может включать проверку работоспособности всех ожидаемых функций.
if(xxx_initialize(sc) < 0) goto bad;
Подсистема шины автоматически выводит на консоль описание устройства, установленное при проверке. Однако, если драйвер хочет вывести дополнительную информацию об устройстве, он может это сделать, например:
device_printf(dev, "has on-card FIFO buffer of %d bytes\n", sc->fifosize);
Если в процессе выполнения инициализации возникают какие-либо проблемы, рекомендуется выводить сообщения об этих проблемах перед возвратом ошибки.
Последним шагом процедуры присоединения является подключение устройства к его функциональной подсистеме в ядре. Конкретный способ зависит от типа драйвера: символьное устройство, блочное устройство, сетевое устройство, устройство шины CAM SCSI и так далее.
Если всё прошло успешно, вернуть успех.
error = xxx_attach_subsystem(sc); if(error) goto bad; return 0;
Наконец, обработаем проблемные ситуации. Все ресурсы должны быть освобождены перед возвратом ошибки. Мы используем тот факт, что перед передачей структуры softc
она обнуляется, поэтому мы можем определить, был ли выделен какой-либо ресурс: если его дескриптор ненулевой.
bad: xxx_free_resources(sc); if(error) return error; else /* exact error is unknown */ return ENXIO;
Вот и всё для процедуры присоединения.
10.10. xxx_isa_detach
Если эта функция присутствует в драйвере и драйвер скомпилирован как загружаемый модуль, то драйвер получает возможность быть выгруженным. Это важная функция, если оборудование поддерживает горячее подключение. Однако шина ISA не поддерживает горячее подключение, поэтому эта функция не особенно важна для устройств ISA. Возможность выгрузки драйвера может быть полезна при его отладке, но во многих случаях установка новой версии драйвера потребуется только после того, как старая версия каким-то образом заблокирует систему и перезагрузка все равно будет необходима, поэтому усилия, затраченные на написание процедуры отсоединения, могут не окупиться. Другой аргумент, что выгрузка позволит обновлять драйверы на рабочей машине, кажется в основном теоретическим. Установка новой версии драйвера — это опасная операция, которую никогда не следует выполнять на рабочей машине (и которая не разрешена, когда система работает в безопасном режиме). Тем не менее, процедура отсоединения может быть предоставлена для полноты.
Процедура отсоединения возвращает 0, если драйвер был успешно отсоединён, или код ошибки в противном случае.
Логика отсоединения является зеркальной по отношению к присоединению. Первое, что нужно сделать, — это отсоединить драйвер от его подсистемы ядра. Если устройство в настоящее время открыто, у драйвера есть два варианта: отказаться от отсоединения или принудительно закрыть устройство и продолжить отсоединение. Выбор зависит от возможности конкретной подсистемы ядра выполнить принудительное закрытие и от предпочтений автора драйвера. Как правило, принудительное закрытие кажется предпочтительным вариантом.
struct xxx_softc *sc = device_get_softc(dev); int error; error = xxx_detach_subsystem(sc); if(error) return error;
Далее драйвер может сбросить аппаратное обеспечение в согласованное состояние. Это включает остановку любых текущих передач, отключение каналов DMA и прерываний, чтобы избежать повреждения памяти устройством. Для большинства драйверов это именно то, что делает процедура выключения, поэтому, если она присутствует в драйвере, мы можем просто вызвать её.
xxx_isa_shutdown(dev);
И наконец освободить все ресурсы и вернуть успех.
xxx_free_resources(sc); return 0;
10.11. xxx_isa_shutdown
Эта процедура вызывается, когда система собирается быть выключена. Ожидается, что она приведет оборудование в согласованное состояние. Для большинства устройств ISA не требуется никаких специальных действий, поэтому функция не является действительно необходимой, так как устройство будет переинициализировано при перезагрузке в любом случае. Однако некоторые устройства должны быть выключены с помощью специальной процедуры, чтобы убедиться, что они будут правильно обнаружены после мягкой перезагрузки (это особенно актуально для многих устройств с проприетарными протоколами идентификации). В любом случае отключение DMA и прерываний в регистрах устройства и остановка любых текущих передач — это хорошая идея. Точные действия зависят от оборудования, поэтому мы не рассматриваем их здесь подробно.
10.12. xxx_intr
Обработчик прерывания вызывается при получении прерывания, которое может быть от данного конкретного устройства. Шина ISA не поддерживает разделение прерываний (за исключением некоторых специальных случаев), поэтому на практике, если вызывается обработчик прерывания, то прерывание почти наверняка поступило от его устройства. Тем не менее, обработчик прерывания должен опросить регистры устройства и убедиться, что прерывание было сгенерировано его устройством. Если нет, он должен просто вернуть управление.
Старая практика для драйверов ISA заключалась в получении номера устройства в качестве аргумента. Это устарело, и новые драйверы получают любой аргумент, указанный для них в процедуре присоединения при вызове bus_setup_intr()
. Согласно новой практике, это должен быть указатель на структуру softc. Таким образом, обработчик прерываний обычно начинается так:
static void xxx_intr(struct xxx_softc *sc) {
Он выполняется на уровне приоритета прерывания, указанном параметром типа прерывания в bus_setup_intr()
. Это означает, что все остальные прерывания того же типа, а также все программные прерывания, отключены.
Во избежание гонок это обычно записывается в виде цикла:
while(xxx_interrupt_pending(sc)) { xxx_process_interrupt(sc); xxx_acknowledge_interrupt(sc); }
Обработчик прерывания должен подтвердить прерывание только устройству, но не контроллеру прерываний, система позаботится о последнем.
Изменено: 14 октября 2025 г. by Vladlen Popolitov