|
Pashix Опубликовано: dl, 20.02.06 02:34 Доброго времени суток всем, кто читает первую статью из цикла "Программирование на аппаратном уровне". Сперва, немного предистории... Интерес к написанию драйверов возник у меня довольно давно, но то ли из-за нехватки времени, то ли из-за отсутствия необходимости я почти никогда этим не занимался. К тому же в настоящее время очень сложно найти хорошую техническую документациюю, т.к. компании обычно сами выпускают ПО для своей продукции. Большинство необходимых услуг по работе с железом предоставляла операционная система. Но потом ситуация изменилась для меня, поскольку мой друг и коллега (Фёдоров О. С. aka Legos) предложил мне подключиться к работе над его новой операционной системой FOS (её он так изначально назвал по своим инициалам =)), которую он начал писать не задолго до этого. FOS - это 32-битная многозадачная операционная система, распространяющаяся под лицензией GNU Public License v2). На тот момент, 03.2005, она включала в себя драйвер для работы с файловой системой FAT12 (только для чтения), драйвер флоппи-дисковода и пока ещё плохо работающую реализацию многозадачности. FOS - это не клон Linux-а. FOS писалась абсолютно с нуля, и, на настоящий момент поддерживается двумя людьми: мной и Legos-ом. В этой серии статей я постараюсь показать на примере методы программирования драйверов устройств и других компонентов ядра для операционных систем. Перед написанием любой программы, в том числе и драйвера, необходимо выбрать подходящий язык программирования. Во всех последующих исходных текстах программ я буду пользоваться языками C и C++, а также языком Assembler. C/C++ дают необходимую гибкость и удобство при написании программы, а с помощью Assembler-а можно добиться максимальной скорости работы и минимального размера кода. На самом деле я допускаю использование и других языков программирования для реализации нашей цели, таких как Pascal, но в этом случае мы лишаем себя этих двух преимуществ. Что касается компиляторов, основным средством разработки будет gcc (есть версии под Windows, Linux и Unix), и ещё иногда Borland C++ v3.1 под DOS. Все примеры, распространающиеся вместе с этой серией статей разработаны под лицензией GNU Public License v2 если иное не оговорено заранее. Ознакомиться с этой лицензией можно на сайте www.gnu.org. В первой статье речь пойдёт о создании драйвера адаптера асинхронной последовательной связи, работающего по стандартам RS-232. Говоря человеческим языком, мы будем программировать адаптер, управляющий так называемыми COM-портами. Наиболее часто интерфейс RS-232 используют для соединения двух компьютеров по телефонной линии с использованием модема (модем = МОдулятор/ДЕМодулятор). Модулятор преобразует поступающий цифровой сигнал в аналоговый, и посылает его по телефонной линии удалённому демодулятору, который преобразует его обратно в цифровой. Информация передаётся последовательным кодом (то есть бит за битом, которые на приёмнике собираются обратно в байты) с заданной частотой (количество бит в секунду) и с фиксированным размером кадра (1 старт-бит, от 5 до 8 информационных битов, не обязательный бит контроля чётности и 1 или 2 стоп-битов) с использованием так называемого старт-стопного метода. Старт-стопный метод заключается в том, что любая передаваемая информация начинается со старт-бита (всегда равен 1), и заканчивается одним или двумя стоп-битами (всегда равны 0). Такой способ позволяет проверять правильность передачи: если приёмник ожидает получения стоп-бита, равного нулю, а вместо него получает единицу (как правило +12В), то регистрируется ошибка кадрирования (framing error). Ещё один вариант контроля ошибок заключается в использовании бита контроля чётности: непосредственно за передаваемыми данными находится бит, который определяет, чётное или не чётное число получается при сложении информационных битов. Такой способ, однако, может выявить только нечётное число ошибок, и по этому редко используется на практике. Кроме того, современные адаптеры асинхронной последовательной связи обладают возможностью аппаратной буфферизации (размер буфера - до 16 байт) и возможностью использования DMA (Direct Memory Access - Прямой доступ к памяти) для уменьшения нагрузки на CPU. Стандарт RS-232C описывает 25 линий, хотя на практике используется значительно меньшее количество (по крайней мере при работе с модемом): 2 линии для передачи битов данных (TXD и RXD: от адаптера и к адаптеру), 5 линий для определения готовности (DSR, CTS, DCD, DTR и RTS: готовность подключённого устройства и готовность самого интерфейиса RS-232 передавать данные), индикатор вызова (RI) и логический нуль (GND). На самом деле для организации передачи данных через RS-232 было бы достаточно всего трёх линий: TXD, RXD и GND, но тогда бы нам пришлось передавать данные "в слепую", не дожидаясь поддтверждения, да и не зная вообще, слышит ли нас приёмник. Используемые сигналы интерфейса RS-232:
Теперь перейдём к практике. В нашу задачу входит написание драйвера для микросхемы 8250, который в последствии будет использован как компонент ядра FOS, DOS, либо для любой другой ОС. На самом деле, взаимодействие с интерфейсом RS-232 нам могут предоставить функции BIOSа через прерывание 0x14, но во-первых они имеют некоторые ограничения, а во-вторых нам нужно заставить наш драйвер работать в защищённом режиме процессора (ну... ето для FOS). Будем всё писать в ручную, используя механизм портов ввода/вывода. Базовым адресом (БА) мы будем называть номер первого порта для доступа к устройству. Для первого интерфейса RS-232 (то есть COM1 в DOS или ttyS0 в Linux) базовый адрес обычно равен 0x03F8. Список внутренних регистров микросхемы 8250 и 16550 UART:
Внутренний регистр разрешения прерываний (БА+1): (только запись) Этот регистр позволяет включить режим генерации прерывания при изменении состояния адаптера. Использование такого аппаратного прерывания (IRQ3 - для COM2, COM4 ... ; IRQ4 - для COM1, COM3 ...) бывает полезно, когда у нас нет возможности постоянно опрашивать RS-232, например, с целью определения готовность передачи/приёма следующего байта. Этот регистр доступен тогда и только тогда, когда включен режим передачи/приёма данных, то есть если бит 7 регистра управления линией (БА+3) установлен в 0
Внутренний регистр идентификации прерываний (БА+2): (только чтение) С помощью этого регистра обработчик прерывания может определить причину аппаратного прерывания. Устранение причины прерывания происходит при чтении или записи из/в соответствующий регистр. Например, если прерывание произошло по причине готовности передатчика (биты 0, 1 и 2 равны 0, 0 и 1 соответственно), то при записи в этот регистр сбрасывается прерывание по этой причине.
Следует также обратить внимание на то, что для вызова прерывания может быть несколько причин одновременно, и обработчик не должен завершать свою работу до тех пор, пока не обработает все причины, то есть пока бит 0 не будет аппаратно установлен в еденицу. При налачии нескольких причин прерывания, они будут обрабатываться согласно своему приоритету: 1) изменение в регистре состояния модема 2) готовность принятия данных 3) готовность отправки данных 4) изменение регистра состояния линии. Внутренний регистр контроля FIFO и DMA микросхемы 16550 UART (БА+2): (только запись) Этот регистр не доступен для записи при использовании более старой микросхемы 8250! При помощи этого регистра драйвер устройства может задавать режим работы FIFO и DMA
Внутренний регистр управления линией (БА+3): Используется для задания параметров передачи данных, установки сигнала BREAK (приостановка передачи) и для выбора содержимого в регистрах БА+0 и БА+1
Внутренний регистр управления модемом (БА+4):
Внутренний регистр состояния линии (БА+5): (только чтение) Благодаря этому регистру драйвер может отслеживать изменения состояния интерфейса
Внутренний регистр состояния модема (БА+6): (только чтение) Благодаря этому регистру драйвер может отслеживать изменения состояния модема (или другого устройства). С помощью него можно определить сигналы на соответствующих линиях, а также наличие изменения состояния линий с момента последнего обращения к регистру.
Перед первым использованием адаптера асинхронной последовательной связи, совместимого с 8250, нам необходимо произвести его инициализацию, указав ему следующие параметры: частоту передачи, количество информационных битов (ради которых мы всё это и затеяли), количество стоп-битов и наличие бита чётности в кадре, а для более продвинутых контроллеров (16550 UART) можно также определить размер буфера FIFO и разрешить/запретить DMA. Эту опреацию проводит BIOS при запуске компьютера, но когда мы захотим изменить параметры по умолчанию, нам придётся переинициализировать адаптер ещё раз. Детально это выглядит так:
/* * 8250.c v0.1 * RS-232 I/O Driver. * Copyright (c) Pashix, 2005-06, mailto: pashix(at)pochta.ru */ #define MAX_RS232_IF 5 // Максимальное количество адаптеров RS-232 #define _ADAPTER_FREQ 1843200L // Частота источника синхронизации, константа #define _16550_UART_FIFO // Этот дефайн я использую для включения/выключения буферизации unsigned long RS232_base_ports [MAX_RS232_IF]; // В этом массиве мы будем хранить базовые адреса для каждого интерфейса /* Использование такого массиа позволит нам указать базовый адрес только один раз, в функции RS232_init(), а в остальных функциях указывать лишь номер адаптера. Просто так удобнее =) */ char RS232_init(unsigned char num, unsigned short base_port, unsigned char parity, unsigned char stop_bits, unsigned char word_length, unsigned long baud_rate) // Функция инициализациии RS-232 // num - номер интерфейса (от 0 до MAX_RS232_IF), base_port - БА, parity - контроль по чётности // stop_bits - количество стоп-битов, word_length - длина слова, baud_rate - желаемая скорость передачи/приёма { char format_byte; union { int baud_divisor; char bytes[2]; }word; // Контроль ошибок: if ((num > MAX_RS232_IF-1)||(word_length > 3)||(stop_bits > 1)||(parity > 7)||(!baud_rate)) return RS232_INVALID_PARAM; RS232_base_ports[num] = base_port; // Запомнили БА для данного интерфейса, он нам пригодится в других функциях format_byte = 0x3F & ((word_length)|(stop_bits << 2)|(parity << 3)); word.baud_divisor = (_ADAPTER_FREQ / baud_rate) >> 4; outportb(base_port+3, format_byte | 0x80); // (Шаг 1) outportb(base_port+0, word.bytes[0]); // (Шаг 2) outportb(base_port+1, word.bytes[1]); outportb(base_port+3, format_byte); // (Шаг 3) #ifdef _16550_UART_FIFO // Если мы разрешили FIFO для микросхемы 16550 UART outportb(base_port+2, 0xC7); // Включаем режим FIFO и устанавливаем размер буфера в 16 байт (Шаг 4) #endif return RS232_NOERROR; } Опишем, заодно, функцию проверки инициализации RS-232. Эта функция поможет установить, был ли инициализирован контроллер: char isRS232_init(unsigned char num) { // Проверка инициализации адаптера (была ли использована функция RS232_init()?) return (RS232_base_ports[num] != 0)?RS232_NOERROR:RS232_NOT_INIT; } После этого данные, записанные в регистр БА+0 будут передаваться через интерфейс к устройству, а данные, прочитанные из этого регистра будут приниматься из интерфейса RS-232. Но не всё так просто! Не стоит забывать, что при работе с каким-либо устройством нам сначала нужно его инициализировать (попросту говоря, включить и подготовить), а также про то, что перед любой передачей/приёмом данных нам нужно дожидаться аппаратного подтверждения готовности. Для того, что бы проверить готовность интерфейса передать данные драйверу на обработку и готовность устройства получить данные от драйвера через интерфейс RS-232 необходимо прочитать биты готовности из соответствующих портов ввода/вывода (адаптер должен быть инициализирован функцией RS232_init() перед этим). Если нужная функция возвращает значение RS232_NOERROR - можно передавать следующий байт char RS232_receive_ready(unsigned char num) { // Функция возвращает признак готовности приёмника (проверяет бит 0 в БА+5) return (inportb(RS232_base_ports[num]+5) & 0x01)?RS232_NOERROR:RS232_NOT_READY; } char RS232_send_ready(unsigned char num) { // Функция возвращает признак готовности передатчика (проверяет бит 4 в БА+6) return (inportb(RS232_base_ports[num]+6) & 0x10)?RS232_NOERROR:RS232_NOT_READY; } А теперь - самые важные функции: функции отправки и получения байта в/из адаптера RS-232. Обращаю внимание на то, что в них отсутствует проверка правильности инициализации. RS232_receivebyte() и RS232_sendbyte() используют массив RS232_base_ports[], в котором хранятся базовые адреса для инициализированных функцией RS232_init() адаптеров. char RS232_receivebyte(unsigned char num) { // Чтение байта из интерфейса return inportb(RS232_base_ports[num]); } void RS232_sendbyte(unsigned char num, char byte) { // Отправка байта в интерфейс outportb(RS232_base_ports[num], byte); } В принципе, написанных функций уже будет достаточно для обмена информацией по линиям интерфейса RS-232, но перед тем, как мы начнём работу с устройством (например, с модемом или с мышкой), не плохо бы было произвести его инициализацию, да и просто проконтролировать его работоспособность. С точки зрения программирования бывает 2 типа устройств для RS-232: умные и не очень =). Различие в том, что умные устройства (например, модемы) могут аппаратно поддтверждать своё включение и готовность, выставляя сигналы на соответствующих линиях (см. таблицу "сигналы интерфейса RS-232"). Не очень умные (например, мышки) - не могут. Для инициализации не очень умного устройства можно воспользоваться схемой:
Схема инициализации умного, такого как модем, устройства выглядит несколько сложнее:
Исходя из этих двух алгоритмов, напишем ещё четыре функции инициализации и деинициализации различных устройств, подключаемых к RS232 (хотя к самому микроконтроллеру 8250 они не относятся, они необходимы для выставления и проверки соответствующих сигналов на линиях): char RS232_modem_on(unsigned char num) { // Инициализация модема char mcr; unsigned long wait; if(num>MAX_RS232_IF-1) return RS232_INVALID_PARAM; mcr = inportb(RS232_base_ports[num]+4); outportb(RS232_base_ports[num]+4, mcr | 0x01); // Выставляем DTR (Data Terminal Ready) wait = _WAIT; while(wait--); // Задержка if(!(inportb(RS232_base_ports[num]+6) & 0x20)) { // Если сигнал DSR (Data Set Ready) не поступил - модем не отвечает outportb(RS232_base_ports[num]+4, mcr); // Восстанавливаем регистр return RS232_NO_MODEM; } outportb(RS232_base_ports[num]+4, mcr | 0x03); // Выставляем DTR и RTS (Request To Send) wait = _WAIT; while(wait--); // Задержка if(!(inportb(RS232_base_ports[num]+6) & 0x10)) { // Если нет сигнала CTS (Clear To Send) - модем есть, но не готов outportb(RS232_base_ports[num]+4, mcr); // Восстанавливаем регистр return RS232_MODEM_NOT_READY; } outportb(RS232_base_ports[num]+4, mcr | 0x0B); // Выставляем сигналы DTR, RTS и OUT2 // Очищаем все возможные события аппаратного прерывания: inportb(RS232_base_ports[num]+0); inportb(RS232_base_ports[num]+5); inportb(RS232_base_ports[num]+6); return RS232_NOERROR; } void RS232_modem_off(unsigned char num) { // Деинициализация (выключение) модема RS232_device_off(num); // Вызываем функцию RS232_device_off(), описанную ниже } char RS232_device_on(unsigned char num) { // Инициализация устройства (из разряда не очень умных =) ) char mcr; if(num>MAX_RS232_IF-1) return RS232_INVALID_PARAM; mcr = inportb(RS232_base_ports[num]+4); outportb(RS232_base_ports[num]+4, mcr | 0x03); // Выставляем DTR и RTS return RS232_NOERROR; } void RS232_device_off(unsigned char num) { // Деинициализация устройства (подходит и для модемов) if(num>MAX_RS232_IF-1) return; outportb(RS232_base_ports[num]+4, 0x00); // Очищаем все биты управления // Очищаем все возможные события аппаратного прерывания: inportb(RS232_base_ports[num]+0); inportb(RS232_base_ports[num]+5); inportb(RS232_base_ports[num]+6); }_WAIT - это некоторое время задержки между тем, как модем (или другое "умное" устройство) обрабатывает наш сигнал и отвечает на него. Если значение _WAIT слишком мало, устройство может не успеть ответить на наш запрос, и функция вернёт ошибку. Если слишком велико - будет ощутимая пауза при работе драйвера, что также не допустимо. Задавать его рекомендую в дефайне в начале программы таким образом: #define _WAIT 0x0FF. При инициализации другого устройства (не модема), задержки не требуются, поскольку ответа в любом случае не будет. В некоторых случаях (особенно, если мы используем обработчик аппаратного прерывания) нам будет необходимо контролировать ошибки передачи данных по интерфейсу. Для этого воспользуемся достаточно примитивными функциями обнаружения ошибки кадрирования RS232_framing_error() и ошибки паритета, то есть контроля по чётности/нечётности RS232_parity_error(): char RS232_framing_error(unsigned char num) { // Определяем наличие ошибки кадрирования return (inportb(RS232_base_ports[num]+5) & 0x08)?RS232_FRAMING_ERROR:RS232_NOERROR; } char RS232_parity_error(unsigned char num) { // Определяем наличие ошибки паритета return (inportb(RS232_base_ports[num]+5) & 0x04)?RS232_PARITY_ERROR:RS232_NOERROR; } Следующая функция также является очень интересной для программиста. С технической стороны, она всего лишь определяет наличие сигнала RI на линии интерфейса. При подключении к адаптеру RS-232 модема, сигнал будет генерироваться последним в случае звонка на телефонной линии. Это позволит нам программно среагировать на входящий звонок (например, снять трубку и установить соединение с удалённым модемом). char RS232_modem_ring(unsigned char num) { if(num>MAX_RS232_IF-1) return RS232_INVALID_PARAM; return (inportb(RS232_base_ports[num]+6) & 0x40)?RS232_NOERROR:RS232_MODEM_NO_RING; } Ну и последнее (уже предчувствую радостный вздох читателя =) ). При написании драйвера многозадачной операционной системы нужно учитывать то, что вряд ли у нас в распоряжении будет много процессорного времени, и наш драйвер не должен постоянно (в цикле) опрашивать контроллер RS-232. Например, при попытке отправки/получения байта некой программой через наш драйвер, последний, вместо того, что бы "подвесив" всю систему ожидать готовности интерфейса, должен будет активизироваться только когда контроллер сам сообщит ему о готовности. Для этого мы будем пользоваться обработчиком аппаратного прерывания, который сможет автоматически активизировать драйвер по изменении состояния интерфейса. Говоря человеческим языком, мы повесим свой обработчик на аппаратное прерывание. Про то, как это делается, можно прочитать в соответствующей документации по программированию. А в этой статье я расскажу про то, как заставить контроллер генерировать такое прерывание (IRQ). Итак, функция, устанавливающая события, при которых сгенерируется IRQ: void RS232_IRQ_init(unsigned char num, unsigned char can_receive, unsigned char can_send, unsigned char line_status_change, unsigned char modem_status_change) { // Устанавливаем события для генерации IRQ outportb(RS232_base_ports[num]+1, (can_receive?1:0)|(can_send?2:0)|(line_status_change?4:0)|(modem_status_change?8:0)); /* can_receive=1 - генерировать IRQ в случае готовности получения данных контроллером can_send=1 - генерировать IRQ в случае готовности отправления данных контроллером line_status_change=1 - генерировать IRQ в случае изменения состояния линии modem_status_change=1 - генерировать IRQ в случае изменения состояния модема Если эти параметры равны нулю - прерывание не будет генерироваться по этим событиям. Если установить все параметры (кроме номера интерфейса num) в ноль - IRQ контроллером не сгенерируется. */ } Вот, собственно, и всё, что можно сказать про программирование драйвера RS-232. В заключении можно отметить то, что практически все функции возвращают коды ошибок (либо значение RS232_NOERROR, что свидетельствует об отсутствии ошибки), про которые мы не сказали ни слова. Опишем наши коды завершения в отдельнов файле rs232err.h, который подключим к основному модулю с помощью дерективы #include "rs232err.h": /* * rs232err.h * Copyright (c) Pashix, 2005-06, mailto: pashix(at)pochta.ru */ #ifndef __RS232ERR_H #define __RS232ERR_H #define RS232_NOERROR 0 #define RS232_INVALID_PARAM -1 #define RS232_NOT_INIT -2 #define RS232_NOT_READY -3 #define RS232_NO_MODEM -4 #define RS232_MODEM_NOT_READY -5 #define RS232_MODEM_NO_RING -6 #define RS232_FRAMING_ERROR -7 #define RS232_PARITY_ERROR -8 #endifВ своей версии драйвера я ещё добавил дополнительную функцию, возвращающую строку с описанием ошибки в удобочитаемой форме. Заметим, также, что всё взаимодействие с контроллером 8250 происходит через порты ввода/вывода с помощью функций inportb() и outportb(). Эти функции также должны быть описаны в драйвере (описание этих функций на встроенном ассемблере дано с учётом компиляции в GCC и не подходит для Borland C v3 и др.): void outportb(unsigned short port, unsigned char value) { // Запись в порт ввода/вывода asm("outb %b0,%w1"::"a"(value), "d"(port)); } unsigned char inportb(unsigned short port) { // Чтение из порта ввода/вывода unsigned char value; asm("inb %w1, %b0":"=a"(value): "d"(port)); return value; }Если Вы хотите использовать аналогичные функции при компиляции в Borland C, замените данный исходный текст на текст, с использованием ключевого слова _asm или используйте библиотечный функции. Про то, как это сделать прочитайте в документации по _asm, inportb и outportb во встроенной справочной состеме. В целях оптимизации этого исходного текста по скорости вместо описания функций можно использовать директиву #define, за счёт которой в программе не будет не нужных переходов. Самостоятельно =) Заключение Все (или почти все) вопросы, касающиеся программирования микроконтроллера 8250, мы рассмотрели. Вопрос взаимодействия драйвера с ядром операционной системы и с пользовательскими приложениями остаётся открытым, поскольку он ещё до конца не решён. Тем не менее, описаный здесь программный код можно использовать как реальный драйвер для, практически, любой ОС. Немного о самой статье и о FOS. Начал я её писать ещё в середине прошлого, 2005 года, а закончить - всё руки не доходили =) В настоящий момент, операционная система FOS была полностью переделана своим создателем, а я вообще перестал учавствовать в разработке из-за нехватки свободного времени. Поэтому, описав механизм работы микроконтроллера 8250 я абсолютно не вдавался в проблему включения его (механизма) в ядро FOS, расчитывая на то, что читатель сам сможет найти ему применение, добавив описанный код в свои программные продукты. Если этот мануал заинтересует разработчиков, я буду продолжать писать про программирование на аппаратном уровне. Буду рад вопросам и дополнениям с Вашей стороны, отправленным на почтовый ящик pashix(at)pochta.ru
Copyright (c) Pashix, 2005-06, pashix(at)pochta.ru
|
обсудить | все отзывы (3) | |
[86807; 72; 7.62] |
|
|
|