Использование памяти ввода/вывода

Предыдущая  Содержание  Следующая V*D*V

Несмотря на популярность портов вводы/вывода в мире x86, основным механизмом, используемым для обмена данными с устройствами, являются связанные с памятью регистры и память устройства. Оба называются памятью ввода/вывода, так как разница между регистрами и памятью для программного обеспечения не видна.

 

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

 

Способ доступа к памяти ввода/вывода зависит от компьютерной архитектуры, шины и используемого устройства, хотя принципы везде одинаковы. Обсуждение в этой главе касается в основном памяти ISA и PCI, а также пытается передать общую информацию. Хотя доступ к памяти PCI приводится здесь, тщательное обсуждение PCI откладывается до Главы 12.

 

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

 

Независимо от того, требуется или нет для доступа к памяти ввода/вывода ioremap, непосредственное использование указателей на память ввода/вывода не рекомендуется. Хотя даже если (как описывалось в разделе "Порты ввода/вывода и память ввода/вывода") память ввода/вывода адресуется на аппаратном уровне как обычное ОЗУ, дополнительная предосторожность, изложенная в разделе "Регистры ввода/вывода и обычная память", предлагает избегать обычных указателей. Интерфейсные функции, используемые для доступа к памяти ввода/вывода, безопасны на всех платформах и являются сразу оптимизированными, когда операцию может выполнить прямое разыменование указателя.

 

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

Получение памяти ввода/вывода и отображение

Области памяти ввода/вывода должны быть выделены до начала использования. Интерфейсом для получения областей памяти (определённым в <linux/ioport.h>) является:

 

struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);

 

Эта функция выделяет область памяти len байт, начиная со start. Если всё идёт хорошо, возвращается не NULL указатель; в противном случае возвращается значение NULL. Вся выделенная для ввода/вывода память перечислена в /proc/iomem.

 

Когда больше не нужны, области памяти должны освобождаться:

 

void release_mem_region(unsigned long start, unsigned long len);

 

Существует также старая функция для проверки на наличие области памяти ввода/вывода:

 

int check_mem_region(unsigned long start, unsigned long len);

 

Но, как и check_region, эта функция является небезопасной и её следует избегать.

 

Выделение памяти ввода/вывода не только необходимый шаг перед тем, как к этой памяти можно выполнять доступ. Необходимо также убедиться, что эта память ввода/вывода стала доступна для ядра. Получение памяти ввода/вывода не только вопрос разыменования указателя; на многих системах память ввода/вывода совсем не доступна напрямую таким образом. Так что отображение должно быть выполнено первым. Это роль функции ioremap, представленной в разделе "vmalloc и друзья" в Главе 8. Функция предназначена специально для присвоения виртуальных адресов для областей памяти ввода/вывода.

 

После выполнения ioremapiounmap), драйвер устройства может получить доступ к любому адресу памяти ввода/вывода, независимо от того, связан ли он напрямую с виртуальным адресным пространством. Однако, следует помнить, что адреса, возвращаемые ioremap, не должны быть разыменованы напрямую; вместо этого должны быть использованы функции доступа, предоставляемые ядром. Прежде чем мы углубимся в эти функции, мы пересмотрим получше прототипы ioremap и познакомимся с несколькими деталями, которые мы пропустили в предыдущей главе.

 

Функции вызываются в соответствии со следующим определением:

 

#include <asm/io.h>

void *ioremap(unsigned long phys_addr, unsigned long size);

void *ioremap_nocache(unsigned long phys_addr, unsigned long size);

void iounmap(void * addr);

 

Прежде всего, обратите внимание на новую функцию ioremap_nocache. Мы не давали её в Главе 8, поскольку её смысл, безусловно, связан с работой оборудования. Цитируем один из заголовков ядра: "она полезна, если некоторые регистры управления находятся в таких областях, где объединение записи или кэширование чтения не желательны". Действительно, реализация функции идентична ioremap на большинстве компьютерных платформ: в ситуациях, когда вся память ввода/вывода уже видна через некэшируемые адреса, нет оснований для реализации отдельной некэширующей версии ioremap.

Доступ к памяти ввода/вывода

На некоторых платформах вы можете осуществить использование возвращаемого значения ioremap как указателя. Такое использование не является переносимым и разработчики ядра ведут работу по устранению любого такого использования всё больше и больше. Правильным способом доступа к памяти ввода/вывода является набор функций (определённых с помощью <asm/io.h>), предусмотренных для этой цели.

 

Для чтения из памяти ввода/вывода воспользуйтесь одной из следующих:

 

unsigned int ioread8(void *addr);

unsigned int ioread16(void *addr);

unsigned int ioread32(void *addr);

 

Здесь, addr должен быть адресом, полученным от ioremap (возможно, с целочисленным смещением); возвращаемое значение является тем, что было прочитано из данной памяти ввода/вывода.

 

Существует аналогичный набор функций для записи в память ввода/вывода:

 

void iowrite8(u8 value, void *addr);

void iowrite16(u16 value, void *addr);

void iowrite32(u32 value, void *addr);

 

Если вы должны читать или писать ряд значений по данному адресу памяти ввода/вывода, можно использовать версии функций с повторением:

 

void ioread8_rep(void *addr, void *buf, unsigned long count);

void ioread16_rep(void *addr, void *buf, unsigned long count);

void ioread32_rep(void *addr, void *buf, unsigned long count);

void iowrite8_rep(void *addr, const void *buf, unsigned long count);

void iowrite16_rep(void *addr, const void *buf, unsigned long count);

void iowrite32_rep(void *addr, const void *buf, unsigned long count);

 

Функции ioread читают count значений из данного addr в заданный buf, а функции iowrite записывают count значений из данного buf по заданному addr. Обратите внимание, что count выражается в размере записываемых данных; ioread32_rep считывает count 32-х разрядных значений, начиная с buf.

 

Все вышеописанные функции выполняют ввод/вывод по заданному addr. Вместо этого, если требуется работать с блоком памяти ввода/вывода, можно использовать одну из следующих:

 

void memset_io(void *addr, u8 value, unsigned int count);

void memcpy_fromio(void *dest, void *source, unsigned int count);

void memcpy_toio(void *dest, void *source, unsigned int count);

 

Эти функции ведут себя подобно их аналогам библиотеки Си.

 

Если вы почитаете исходный код ядра, вы увидите при использовании памяти ввода/вывода много вызовов старого набора функций. Эти функции по-прежнему работают, но их применение в новом коде не рекомендуется. Среди прочего, они являются менее безопасными, поскольку они не выполняют некоторые виды проверки типа. Тем не менее, мы представляем их здесь:

 

unsigned readb(address);

unsigned readw(address);

unsigned readl(address);

 

Эти макросы используются для получения 8-ми разрядного, 16-ти разрядного и 32-х разрядного значений данных из памяти ввода/вывода.

 

void writeb(unsigned value, address);

void writew(unsigned value, address);

void writel(unsigned value, address);

 

Как и предыдущие функции, эти функции (макросы) используются для записи 8-ми разрядных, 16-ти разрядных и 32-х разрядных элементов данных.

 

Некоторые 64-х разрядные платформы также предлагают readq и writeq для операций памяти с учетверённым словом (8 байт) на шине PCI. Спецификация учетверённого слова исторически осталась со времени, когда все реальные процессоры имели 16-ти разрядные слова. На самом деле, имя L, используемое для 32-х разрядных значений, также стало неправильным, но переименование всего перепутало бы всё даже больше.

Порты как память ввода/вывода

Некоторое оборудование имеет интересную особенность: некоторые версии используют порты ввода/вывода, в то время как другие используют память ввода/вывода. Регистры, экспортируемые в процессор, являются в любом случае одинаковыми, но метод доступа различается. Как способ сделать жизнь проще для драйверов, имеющих дело с оборудованием такого рода, и как способ минимизации очевидных различий между портом ввода/вывода и доступом к памяти, ядро версии 2.6 предоставляет функцию, названную ioport_map:

 

void *ioport_map(unsigned long port, unsigned int count);

 

Эта функция пересвязывает count портов ввода/вывода и делает их видимыми как память ввода/вывода. С этого момента драйвер может использовать ioread8 и друзей с возвращёнными адресами и забыть, что он использует для всего порты ввода/вывода.

 

Когда больше не требуется, это отображение должно быть отменено:

 

void ioport_unmap(void *addr);

 

Эти функции делают порты ввода/вывода выглядящими подобно памяти. Заметьте, однако, что порты ввода/вывода всё ещё должны быть выделены request_region, прежде чем они могут быть переназначены этим способом.

Повторное использование short для памяти ввода/ввода

Модуль примера short, представленный ранее для доступа к портам ввода/вывода, может быть также использован для доступа к памяти ввода/вывода. С этой целью вы должны сказать ему во время загрузки использовать память ввода/вывода; также вы должны изменить базовый адрес, чтобы он указывал на вашу область ввода/вывода.

 

Например, вот как мы использовали short для зажигания отладочных светодиодов на отладочной плате MIPS:

 

mips.root# ./short_load use_mem=1 base=0xb7ffffc0

mips.root# echo -n 7 > /dev/short0

 

Использование short для памяти ввода/вывода такое же, как и для портов ввода/вывода.

 

Следующий фрагмент показывает цикл, используемый short во время записи по адресам памяти:

 

while (count--) {

    iowrite8(*ptr++, address);

    wmb( );

}

 

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

 

short использует inb и outb, чтобы показать, как это делается. Однако, простым упражнением для читателя было бы изменить short для переназначения портов ввода/вывода с помощью ioport_map и значительного упрощения остального кода.

ISA память ниже 1 Мб

Одной из самых известных областей памяти ввода/вывода является область ISA, имеющаяся на персональных компьютерах. Это область памяти между 640 Кб (0xA0000) и 1 Мб (0x100000). Таким образом, она находится прямо в середине обычной оперативной памяти системы. Это позиционирование может показаться немного странным; это артефакт решения, принятого в начале 1980-х, когда 640 Кб памяти казалось большим, чем кто-нибудь когда-нибудь сможет использовать.

 

Этот диапазон памяти относится к не-прямо-отображаемому классу памяти. (* На самом деле это не совсем верно. Диапазон памяти настолько мал и настолько часто используем, что во время загрузки системы для доступа к этим адресам ядро строит таблицы страниц. Тем не менее, виртуальный адрес, используемый для доступа к ним, не такой же, как физический адрес, и, таким образом, ioremap необходима в любом случае.) Вы можете читать/писать несколько байт в этот диапазон памяти, используя модуль short, как объяснялось ранее, то есть устанавливая во время загрузки use_mem.

 

Хотя ISA память ввода/вывода существует только на компьютерах класса x86, мы думаем, что она стоит нескольких слов и примера драйвера на ней.

 

Мы не собираемся обсуждать в этой главе PCI память, поскольку она является чистейшим видом памяти ввода/вывода: узнав физический адрес, вы можете просто переназначить его и получить доступ. "Проблемой" с PCI памятью ввода/вывода является то, что она не поддаётся рабочему примеру для этой главы, потому что мы не можем знать заранее физической адрес вашей PCI памяти для отображения, или какого-то другого безопасного доступа к этим областям. Мы выбрали для описания диапазон ISA памяти, потому что он и менее ясен и больше подходит для выполнения кода примера.

 

Чтобы продемонстрировать доступа к ISA памяти, мы используем ещё один небольшой глупый модуль (часть исходников примеров). По сути, он называется silly (глупым), как сокращение от Simple Tool for Unloading and Printing ISA Data (простого инструмента для Разгрузка и печати данных ISA), или нечто подобного.

 

Модуль дополняет функциональность short предоставлением доступа ко всему 384 Кб пространству памяти и показывая все различные функции ввода/вывода. Он содержит четыре узла устройств, которые выполняют те же задачи, используя различные функции передачи данных. Устройства silly действуют как окно над памятью ввода/вывода способом, похожем на /dev/mem. Вы может читать и записывать данные, а также выполнять произвольный доступ с помощью lseek для случайного адреса памяти ввода/вывода.

 

Так как silly предоставляет доступ к ISA памяти, он должен начать с отображения физических адресов ISA на виртуальные адреса ядра. Когда-то давно в ядре Linux можно было просто присвоить указатель на интересующий ISA адрес, затем разыменовать его напрямую. Однако, в современном мире мы должны работать с системой виртуальной памяти и сначала переназначить диапазон памяти. Как объяснялось ранее для short, это переназначение осуществляется ioremap:

 

#define ISA_BASE 0xA0000

#define ISA_MAX 0x100000 /* для доступа к обычной памяти */

 

    /* эта строка появляется в silly_init */

    io_base = ioremap(ISA_BASE, ISA_MAX - ISA_BASE);

 

ioremap возвращает указатель, который может быть использован с ioread8 и другими функциями, объяснёнными в разделе "Доступ к памяти ввода/вывода".

 

Давайте посмотрим назад на наш модуль примера, чтобы увидеть, каким образом могут быть использованы эти функции. /dev/sillyb, имеющий младший номер 0, адресует память ввода/вывода через ioread8 и iowrite8.

 

Следующий код показывает реализацию для read, которая делает диапазон адресов 0xA0000 - 0xFFFFF доступным как виртуальный файл в диапазоне 0 - 0x5FFFF. Функция read структурирована как оператор переключения (switch) между различным режимами доступа; здесь показан случай sillyb:

 

case M_8:

    while (count) {

        *ptr = ioread8(add);

        add++;

        count--;

        ptr++;

    }

    break;

 

Следующими двумя устройствами являются /dev/sillyw (младший номер 1) и /dev/sillyl (младший номер 2). Они работают подобно /dev/sillyb, только они используют 16-ти разрядные и 32-х разрядные функции. Вот реализация write для sillyl, снова часть переключателя:

 

case M_32:

    while (count >= 4) {

        iowrite32(*(u32 *)ptr, add);

        add += 4;

        count -= 4;

        ptr += 4;

    }

    break;

 

Последним устройством является /dev/sillycp (младший номер 3), которое для выполнения той же задачи использует функции memcpy_*io. Вот суть его реализации read:

 

case M_memcpy:

    memcpy_fromio(ptr, add, count);

    break;

 

Поскольку для обеспечения доступа к области памяти ISA была использована ioremap, silly должен вызвать iounmap при выгрузке модуля:

 

iounmap(io_base);

isa_readb и друзья

Взгляд на исходные коды ядра поднимет ещё один набор процедур с такими именами, как isa_readb. В самом деле, каждая из только что описанных функций имеет isa_ эквивалент. Эти функции обеспечивают доступ к памяти ISA без необходимости отдельного шага ioremap. Однако, разработчики ядра говорят, что эти функции предназначены быть временными вспомогательными средствами портирования драйверов и что они могут уйти в будущем. Таким образом, необходимо избегать их использования.

Предыдущая  Содержание  Следующая