Linux I/O port programming mini-HOWTO

Riku Saikkonen

     [email protected]
   

Перевод: Павел Гашев, SWSoft Pte Ltd.

v, 28 December 1997

Этот документ описывает программирование аппаратных портов ввода/вывода и организацию коротких задержек в пользовательских программах, работающих на архитектуре Intel x86.


1. Введение

Это HOWTO описывает программирование аппаратных портов ввода/вывода и организацию коротких задержек в пользовательских программах, работающих на архитектуре Intel x86. Этот документ представляет из себя дополненное IO-Port mini-HOWTO того же автора.

Copyright 1995-1997 Riku Saikkonen. See Linux HOWTO copyright.

Если у вас есть исправления или добавления, напишите мне ([email protected])...

Изменения относительно последней версии (30 марта 1997):

  • Даны разъяснения относительно inb_p/outb_p и port 0x80.

  • Убрана информация о udelay(), т.к. лучше использовать nanosleep().

  • Преобразовано в формат Linuxdoc-SGML, и кое-что реорганизовано.

  • Масса незначительных дополнений и изменений.


1.1. Авторские права

Авторские права на русский перевод этого текста принадлежат © 2000 SWSoft Pte Ltd. Все права зарезервированы.

Этот документ является частью проекта Linux HOWTO.

Авторские права на документы Linux HOWTO принадлежат их авторам, если явно не указано иное. Документы Linux HOWTO, а также их переводы, могут быть воспроизведены и распространены полностью или частично на любом носителе, физическом или электронном, при условии сохранения этой заметки об авторских правах на всех копиях. Коммерческое распространение разрешается и поощряется; но, так или иначе, автор текста и автор перевода желали бы знать о таких дистрибутивах.

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

Мы бы хотели распространить эту информацию по всем возможным каналам. Но при этом сохранить авторские права и быть уведомленными о всех планах распространения HOWTO. Если у вас возникли вопросы, пожалуйста, обратитесь к координатору проекта Linux HOWTO по электронной почте: или к координатору русского перевода Linux HOWTO компании SWSoft Pte Ltd. по адресу


2. Использование портов ввода/вывода в программах на C

2.1. Стандартный способ

Процедуры доступа к портам ввода/выводы находятся в /usr/include/asm/io.h (или в linux/include/asm-i386/io.h в некоторых дистрибутивах ядра). Процедуры представляют собой встроенные (inline) макроопределения, так что вам не нужны никакие библиотеки, и достаточно просто добавить #include <asm/io.h>.

Из-за ограничения в gcc (присутствующего, как минимум, в версии 2.7.2.3 и ниже) и в egcs (всех версий), вы должны компилировать любые исходные тексты, использующие эти процедуры, с включенной оптимизацией (gcc -O1 или выше), или же определить пустое #define extern перед #include <asm/io.h>..

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

Перед тем, как вы получите доступ к какому-нибудь порту, вы должны дать вашей программе права на это. Это выполняется, при помощи функции ioperm(from, num, turn_on) (определенной в unistd.h и находящейся в ядре), где from это первый порт, а num это количество подряд идущих портов, которым нужно дать доступ. Например, ioperm(0x300, 5, 1) дает доступ к порту с 0x300 по 0x304 (всего 5 портов). Последний аргумент - это двоичное значение, определяющее, дать ли доступ к портам (истина (1)) или запретить его (ложь (0)). Для включения портов, идущих не подряд, вы можете вызывать ioperm() несколько раз. Для дополнительной информации читайте руководство ioperm(2).

Для вызова ioperm() необходимо иметь права root; таким образом, вы должны запускать программу от пользователя root или установить на файл флаг setuid. После того, как определен доступ к портам, права суперпользователя больше вам не нужны. Вам не нужно непосредственно освобождать порты при помощи ioperm(..., 0), т.к. это делается автоматически, когда программа заканчивает работу.

Выполнение setuid() для переключения на другого пользователя не отключает доступ к портам, данный ioperm(), но это происходит при fork (наследованный процесс теряет доступ, когда как у порождающего процесса он остается).

ioperm() может дать доступ только к портам с 0x000 по 0x3ff; для других портов вам нужно использовать iopl() (который дает доступ ко всем портам сразу). Используйте уровень 3 (iopl(3)), чтобы дать доступ вашей программе ко всем портам ввода/вывода (но будьте осторожны --- доступ к неправильным портам может сделать некоторые нехорошие вещи с вашим компьютером). Опять таки, вам необходимы привилегии root. Дополнительно читайте руководство iopl(2).

Теперь собственно о доступе к портам: Чтобы считать байт (8 бит) из порта, вызовите функцию inb(port), возвращающую считанный байт. Чтобы вывести байт в порт, вызовите процедуру outb(value, port) (обратите внимание на порядок аргументов). Чтобы считать компьютерное слово (16 бит) из портов x и x+1 (по одному байту из каждого образуют слово), вызовите функцию inw(x). Чтобы вывести слово в два порта, используйте outw(value, x). Если вы не уверены, работать с одинарным или двойным портом, вам, скорее всего, нужны inb() и outb(), т.к. порты большинства устройств имеют однобайтный доступ. Также замечу, что все функции, работающие с портами, требуют, как минимум, около микросекунды для выполнения.

Макросы inb_p(), outb_p(), inw_p() и outw_p() работают аналогично вышеуказанным, но добавляют короткую (около микросекунды) задержку после доступа к порту; вы можете установить задержку около четырех микросекунд при помощи #define REALLY_SLOW_IO перед #include <asm/io.h>. Обычно эти макросы (за исключением случаев с #define SLOW_IO_BY_JUMPING, при которых выполнение становится менее точным) используют вывод в порт 0x80 в качестве задержки, так что вам необходимо сначала дать доступ к порту 0x80, при помощи ioperm() (вывод в порт 0x80 никак не влияет на систему). О других способах задержки см. руководства.

См. руководства по ioperm(2), iopl(2) и вышеуказанных макросах.


2.2. Альтернативный способ: /dev/port

Другой путь доступа к портам ввода/вывода лежит через открытие файла /dev/port (символьное устройство - major 1, minor 4) на чтение и/или запись (функции f*() из stdio.h работают через буфер, поэтому избегайте их). Затем выполняем lseek() на необходимый байт в файле (позиция файла 0 = порт 0x00, позиция файла 1 = порт 0x01, и т.д.), и считываем/записываем байт или слово при помощи read() или write().

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


3. Доступ к прерываниям (IRQ) и DMA

В своих программах вы не можете напрямую использовать прерывания (IRQ) или DMA. Вам нужно написать драйвер для ядра; для получения дополнительной информации и примеров см. The Linux Kernel Hacker's Guide.

Кроме того, вы не можете отключить прерывания из пользовательской программы.


4. Очень точная синхронизация

4.1. Задержки

Прежде всего, я должен сказать, что в следствии многозадачной природы Linux, вы не можете гарантировать точную синхронизацию в пользовательской программе. Ваш процесс может выйти из расписания выполняемых задач на интервал от 10 миллисекунд до нескольких секунд (на системе с высокой загрузкой). Тем не менее, к большинству приложений, использующих порты ввода/вывода, это не относится. Чтобы свести это к минимуму, вы можете выполнять свой процесс при высоком приоритете (см. руководство nice(2)) или использовать расписание задач в реальном времени (см. ниже).

Если вам нужна более точная синхронизация в обычных пользовательских программах, существует несколько возможностей поддержки `реального времени' в пользовательском режиме. Ядра Linux версий 2.x имеют относительную поддержку реального времени; см. руководство по sched_setscheduler(2). Кроме того, существует специальное ядро, жестко поддерживающее реальное время; см. http://luz.cs.nmt.edu/~rtlinux/.


4.1.1. Засыпание: sleep() и usleep()

Итак, начнем с самых простых процедур синхронизации. Для задержки в несколько секунд, лучше всего использовать sleep(). Для задержек, с точностью до десятков миллисекунд может работать usleep(). Во время выполнения этих функций, процессор приостанавливает данный процесс на заданное время (``засыпает'') и выполняет другие процессы. Для детальной информации см. руководство sleep(3) и usleep(3).

Задержки менее 50 миллисекунд (в зависимости от скорости процессора, машины, и загрузки системы) не представляются возможными, т.к. для возвращения управления процессу в Linux тратится, как минимум, 10-30 миллисекунд. Указание таких задержек для usleep(3), на самом деле, приведет к большей задержке (уж, как минимум, в 10 мс).


4.1.2. nanosleep()

В серии 2.0.x ядр Linux существует системный вызов nanosleep() (см. руководство nanosleep(2)), который позволяет выполнить самую короткую задержку (в несколько микросекунд и выше).

Задержки <= 2 мс, если (и только если) ваш процесс находится в относительном режиме реального времени (soft real time scheduling), nanosleep() выполняет при помощи цикла; иначе он работает так же, как и usleep().

В циклах используется udelay()(внутренняя фунция ядра), а длина цикла вычисляется в зависимости от значения BogoMips. Для более детальной информации, как это работает см. /usr/include/asm/delay.h.


4.1.3. Задержка через порты ввода/вывода

Другой способ обеспечить задержку в несколько микросекунд - это порты ввода/вывода. Запись или чтение любого значения из порта 0x80 (о том, как это сделать, см. выше) обеспечивает задержку почти точно равную одной микросекунде, независимо от типа и скорости вашего процессора. Чтобы обеспечить задержку в несколько микросекунд, вы можете выполнить эту операцию несколько раз. Запись в этот порт не оказывает никакого влияния на компьютер (и некоторые драйверы используют это). Таким образом, обычно выполняется задержка в {in|out}[bw]_p() (см. asm/io.h).

На самом деле, операции ввода/вывода на большинство портов в диапазоне от 0 до 0x3ff тоже дают задержку около одной микросекунды. Так что, если вы, например, производите запись в параллельный порт, для обеспечения задержки, просто добавьте дополнительные inb() из порта.


4.1.4. Задержка при помощи команд ассемблера

Если вы знаете тип и частоту процессора машины, на которой будет работать программа, вы можете вставить меньшие задержки, путем выполнения определенных инструкций ассемблера (но помните, что ваш процесс может быть в любое время исключен из расписания, так что задержка может быть на много больше, чем вы хотите). В нижеуказанной таблице скорость процессора определяет число необходимых тактовых циклов для задержки; например, для процессора с частотой 50МГц (напр. 486DX-50 или 486DX2-50), один такт дает 1/50000000 секунд (=200 наносекунд).

Инструкция        такты i386         такты i486
nop                   3                   1
xchg %ax,%ax          3                   3
or %ax,%ax            2                   1
mov %ax,%ax           2                   1
add %ax,0             2                   1

(Прошу прощения, но я не знаю о Пентиуме; скорее всего, они близки к i486. На i386 я не смог найти инструкцию, выполняемую за один такт. В общем случае, лучше всего использовать одно-тактовые инструкции, иначе конвейерная обработка в современных процессорах может сократить задержку.)

Инструкции nop и xchg не должны давать посторонние эффекты. Остальные команды могут изменять регистр флагов процессора. Впрочем, это уже не имеет значение, т.к. gcc должен заметить это. Самый лучший выбор это nop.

Чтобы использовать это метод, выполните в вашей программе функцию asm("инструкция"). Синтаксис инструкций указан в таблице выше; если вы хотите указать несколько инструкций в одном asm(), разделите их точкой с запятой. Например, asm("nop ; nop ; nop ; nop") выполняет четыре инструкции nop, делая задержку в четыре такта на i486 или Pentium (или 12 тактов на i386).

Gcc непосредственно вставляет код из asm() в программу (inline), так что здесь нет никаких других задержек, связанных с вызовом функций.

Задержки менее одного такта невозможны на архитектуре Intel x86.


4.1.5. rdtsc на процессорах Пентиум

На процессорах Пентиум вы можете узнать количество тактов, прошедших со времени последней перезагрузки, при помощи следующей функции:

   extern __inline__ unsigned long long int rdtsc()
   {
     unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
   }

Вы можете отслеживать это значение для задержки на любое количество тактов, на сколько хотите.


4.2. Точность

Для задержек с точностью до одной секунды, лучше всего использовать time(). Для более точных задержек, gettimeofday() имеет точность около одной микросекунды (см. выше, насчет многозадачности). На пентиумах же функция rdtsc (см. выше) дает точность до одного такта.

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


5. Другие языки программирования

Все вышесказанное может быть использовано на C++ и Objective C без каких-либо комментариев. На ассемблере (как и на C) перед операциями с портами вы должны выполнить ioperm() или iopl().

На других языках, если вы не можете использовать текст на ассемблере или на C, или выполнить упомянутые системные вызовы, самое простое - написать необходимые процедуры на C и собрать их вместе с оставшейся программой. Или же использовать файл /dev/port.


6. Некоторые полезные порты

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

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

Если вы хотите работать с таким распространенным устройством, как сканер, сначала поищите для него драйвер под Linux. Начать лучше всего с Hardware-HOWTO.

http://www.hut.fi/Misc/Electronics/


6.1. Параллельный порт

Основной адрес параллельного порта (далее "БАЗА") - 0x3bc для /dev/lp0, 0x378 для /dev/lp1 и 0x278 для /dev/lp2. Если вы хотите управлять чем-то вроде обычного принтера, см. Printing-HOWTO.

В дополнение к стандартному режиму только вывода, описываемому ниже, в большинстве параллельных портов существует `расширенный' двунаправленный режим. Для дополнительной информации по этому и более новому режиму ECP/EPP (и стандарт IEEE 1284 в общих чертах), см. http://www.fapo.com/ и http://www.senet.com.au/~cpeacock/parallel.htm. Только помните, что вы не можете в ваших программах использовать IRQ и DMA, и для использования ECP/EPP. Вам скорее всего придется написать свой драйвер ядра; хотя, я думаю, что кто-нибудь уже написал его.

Порт БАЗА+0 (Порт данных) соответственно управляет сигналами данных на параллельном порту (от D0 до D7 для битов от 0 до 7, соответствуют значения: 0 = 0В, 1 = 5В). Запись в этот порт устанавливает соответствующие уровни напряжения на контактах разъема принтера. При чтении возвращается последнее записанное значение в стандартном или расширенном режиме записи, или данные на разъеме от другого устройства в расширенном режиме чтения.

Порт БАЗА+1 (Порт состояния) предназначен только для чтения и возвращает следующие входные сигналы:

  • Биты 0 и 1 зарезервированы.

  • Бит 2 состояние IRQ (его нет на разъеме и я не знаю как он работает)

  • Бит 3 ERROR (1=ОШИБКА)

  • Бит 4 SLCT (1=ВЫБОР)

  • Бит 5 PE (1=ГОТОВ)

  • Бит 6 ACK (1=ПОДТВЕРЖДЕНИЕ)

  • Бит 7 -BUSY (0=ЗАНЯТ)

(Я не уверен насчет их значений)

Порт БАЗА+2 (Порт управления) предназначен только для записи (чтение возвращает последнее записанное значение) и управляет следующими сигналами:

  • Bit 0 -STROBE (0=СТРОБ)

  • Бит 1 AUTO_FD_XT (1=АВТО_ПЕРЕВОД_СТРОКИ)

  • Бит 2 -INIT (0=СБРОС)

  • Бит 3 SLCT_IN (1=ВЫБРАН)

  • Бит 4 включает IRQ параллельно порта (которое возникает при изменении ACK с нуля на еденицу) (1=IRQ включено).

  • Бит 5 управляет направлением работы порта в расширенным режиме (0 = запись, 1 = чтение) и предназначен только для записи (чтение не возвращает ничего полезного).

  • Биты 6 и 7 зарезервированы.

(Опять же, я не уверен насчет этих значений)

Разводка (25-разъемный разъем (мама)) (i=ввод, o=вывод):

1io -STROBE, 2io D0, 3io D1, 4io D2, 5io D3, 6io D4, 7io D5, 8io D6,
9io D7, 10i ACK, 11i -BUSY, 12i PE, 13i SLCT, 14o AUTO_FD_XT,
15i ERROR, 16o -INIT, 17o SLCT_IN, 18-25 Ground

Спецификация IBM говорит, что контакты 1, 14, 16 и 17 (управляющий вывод) подключены через открытый коллектор на 5В через резистор 4.7 КОм (рабочий ток 20 mA, холостой 0.55 mA, рабочее напряжение 5.0 В). Остальные контакты дают рабочий ток 24 mA, холостой 15 mA, и рабочее напряжение 2.4 В. В низком состоянии оба они дают максимум 0.5 В. Параллельный порты не-IBM могут отличаться от этого стандарта. Для дополнительной информации см. http://www.hut.fi/Misc/Electronics/circuits/lptpower.html.

И наконец, предупреждение: будьте осторожны с заземлением. Я сжег несколько параллельных портов, подключая принтеры к ним при включенном компьютере. Для таких случаев лучше всего использовать параллельный порты, не встроенные в материнскую плату. (Обычно вы можете установить второй параллельный порт при помощи дешевой `мультикарты'; просто отключите порты, которые вам не нужны, и установить адрес параллельного порта на карте на любой свободный адрес. Вам не нужно заботится о IRQ параллельного порта, если только он используется другим устройством)


6.2. Игровой порт (джойстик)

Игровой порт находится по адресам 0x200-0x207. Для управления обычным джойстиком существует драйвер на уровне ядра. См. ftp://sunsite.unc.edu/pub/Linux/kernel/patches/ , имя файла joystick-*.

Разводка (15-контактный разъем (мама)):

  • 1,8,9,15: +5В (питание)

  • 4,5,12: Земля

  • 2,7,10,14: Цифровые входы BA1, BA2, BB1 и BB2, соответственно

  • 3,6,11,13: ``Аналоговые'' входы AX, AY, BX и BY, соответственно

Контакты +5В обычно подключены прямо к питанию материнской платы и не обеспечивают большую загрузку (в зависимости от материнской платы, блока питания и игрового порта).

Цифровые входы используются для определения состояния кнопок на двух джойстиках (джойстик A и джойстик B, по две кнопки на каждом), подключаемых к порту. Это должны быть обычные входы TTL, и вы можете считывать их состояние непосредственно из регистра состояния (см. ниже). Обычные джойстики устанавливают ноль (0В), при нажатой кнопке, и единицу (5В от питания через резистор 1КОм), в противном случае.

Так называемые аналоговые входы, на самом деле, измеряют сопротивление. Игровой порт имеет четыре одноимпульсных мультивибратора, подключенных к четырем входам. На каждом входе стоит резистор 2.2КОм между выходом мультивибратора и землей. Обычные джойстики имеют потенциометр по каждой оси (X и Y), связанный с +5В и соответствующим входным контактом (AX или AY для джойстика A, и BX или BY для джойстика B).

Во время включения, мультивибратор устанавливает напряжение +5В и ждет, когда напряжение на конденсаторе достигнет 3.3В, а затем сбрасывает напряжение. Таким образом, длительность импульса мультивибратора прямопропорциональна сопротивлению потенциометра на джойстике (т.е. положению джойстика по соответствующей оси): "R = (t - 24.2) / 0.011," где R - сопротивление (Ом) потенциометра, а t - длительность импульса (секунд).

Таким образом, чтобы считать состояние аналогового входа, сначала нужно включить мультивибратор (путем записи в порт; см. ниже), а затем считывать состояние всех четырех осей, пока они сбросятся с 1 на 0, измеряя длительность сигнала. Такое считывание требует много процессорного времени и на многозадачных операционных системах, таких как Linux (для пользовательских программ), результат будет не очень точен, т.к. вы не можете считывать из порта постоянно (если только не используете драйвер на уровне ядра и отключаете прерывания, что, впрочем, требует еще больше процессорного времени). Если вы знаете, что сигнал будет продолжаться достаточно долгое время (десятки миллисекунд), вы можете использовать usleep() перед считыванием, передав процессорное время другим процессам.

Вам нужно использовать только один порт - 0x201 (другие порты идентичны или вообще ничего не делают). Любая запись в этот порт (не важно, что вы записываете) включает мультивибратор. Чтение из порта возвращает состояние входных сигналов:

  • Бит 0: AX (состояние на выходе мультивибратора)

  • Бит 1: AY (состояние на выходе мультивибратора)

  • Бит 2: BX (состояние на выходе мультивибратора)

  • Бит 3: BY (состояние на выходе мультивибратора)

  • Бит 4: BA1 (цифровой вход)

  • Бит 5: BA2 (цифровой вход

  • Бит 6: BB1 (цифровой вход)

  • Бит 7: BB2 (цифровой вход)


6.3. Последовательный порт

Если устройство, с которым вы хотите общаться, поддерживает что-то похожее на RS-232, вы можете использовать последовательный порт. Драйвер Linux для последовательного порта должен быть достаточен для всех приложений (вам не нужно иметь непосредственный доступ к порту, или же для этого нужно написать драйвер ядра); он достаточно универсален, т.к. использует нестандартные скорости обмена и т.д., так что у вас не должно быть проблем.

Для дополнительной информации см. руководство termios(3), исходные тексты драйвера (linux/drivers/char/serial.c), и http://www.easysw.com/~mike/serial/index.html.


7. Подсказки

Если вы хотите хороший аналоговый ввод/вывод, можете подключить АЦП и/или ЦАП к параллельному порту (подсказка: если у вас маломощное устройство, для питания используйте игровой порт или свободный разъем питания внутренних устройств, выведенный наружу, или же используйте внешний источник питания) или купите специальную карту аналого-цифрового/цифро-аналогого преобразования (большинство из старых/медленных карт управляются через порты ввода/вывода). Или, если вы удовлетворитесь одним или двумя каналами, погрешностью и (возможно) неправильным уровнем нуля, дешевая звуковая карта, поддерживаемая драйвером Linux, должна тоже сработать (и очень быстро).

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

Если вы ищете программное обеспечение под Linux для разработки вашей платы, есть бесплатная программа под названием Pcb (по крайней мере, если не делаете ничего уж очень навороченного). Она включен в большинство дистрибутивов Linux и доступна по адресу ftp://sunsite.unc.edu/pub/Linux/apps/circuits/ (имя файла pcb-*).


8. Устранение возможных проблем

Q1.

Я получаю segmentation faults, когда пытаюсь получить доступ к порту.

A1.

Т.к. ваша программа не имеет прав root, или по каким-то причинам ioperm() вызвана неудачно. Проверьте значение возвращаемое ioperm(). Также, проверьте обращаетесь ли вы к тем портам, которые разрешили с помощью ioperm() (см. Q3). Если вы используете макросы, добавляющие задержку (inb_p(), outb_p(), and so on), не забудьте вызвать ioperm(), чтобы получить доступ к порту 0x80 тоже.

Q2.

Я не могу найти функции in*(), out*() определенные где-нибудь, и gcc жалуется по поводу неопределенных ссылок.

A2.

Вы не компилировали свою программу со включенной оптимизацией (-O), и, таким образом, gcc не мог разобрать макросы в asm/io.h. Или вы не подключили #include <asm/io.h>.

Q3.

out*() не делает ничего, или делает что-то непонятное.

A3.

Проверьте порядок параметров; они должны быть outb(value, port), но не outb(value, port), как обычно в MS-DOS.

Q4.

Я хочу управлять стандартным устройством RS-232/параллельным портом/джойстиком...

A4.

Вы, вероятно, больше выиграете, используя существующие драйверы (в ядре Linux, в X server или где-то еще), чтобы сделать это. Драйверы обычно универсальные, такие, что даже нестандартные устройства могут работать с ними. См. информацию по стандартным портам, выше даны ссылки на документацию по ним.


9. Пример кода

Здесь приведен кусок простого примера для доступа к портам ввода/вывода:

/*
 * example.c: очень простой пример для порта ввода/вывода
 *
 * Этот код не делает ничего полезного, только запись в порт, пауза,
 * и чтение из порта. Откомпилируйте `gcc -O2 -o example example.c',
 * и запустите под root `./example'.
 */

#include <stdio.h>
#include <unistd.h>
#include <asm/io.h>

#define BASEPORT 0x378 /* lp1 */

int main()
{
  /* Получить доступ к порту */
  if (ioperm(BASEPORT, 3, 1)) {perror("ioperm"); exit(1);}

  /* Вывод в порт (0) */
  outb(0, BASEPORT);

  /* Задержка (100 мс) */
  usleep(100000);

  /* Чтение из порта (BASEPORT+1) и вывод результатов на экран */
  printf("статус: %d\n", inb(BASEPORT + 1));

  /* Мы больше не нуждаемся больше в порту */
  if (ioperm(BASEPORT, 3, 0)) {perror("ioperm"); exit(1);}

  exit(0);
}

/* конец example.c */



10. Благодарности

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