Работа процессора в одиночестве бессмысленна сама по себе. Есть множество периферийных устройств, выполняющих различные функции, с которыми необходимо осуществлять информационный обмен.
Работа процессора с внешними устройствами происходит с помощью друх схем
Механизм обслуживания устройства по прерыванию во многих случаях оказывается наиболее предпочтителен. Если устройств много, их опрос может занимать приличное время, да и иногда он бывает затруднен большой ресурсоемкостью алгоритмов, не связанных с работой внешних устройтств.
Был в моей практике электровозный дисплейный блок Gersys, под управлением MS DOS, который обслуживал контролер CAN-шины именно используя обработку прерываний, иначе сделать было просто нельзя. Вообще же выполнение "фоновых" задач в однозадачной ОС не обходится без использования прерываний.
Процессоры семейства x86 физически имеют один единственный вход для приема запросов прерываний от внешних устройств. Поэтому для мультиплексирования этого сигнала от многих устройств совместно с процессором использовался программируемый контролер прерываний - PIC. Вместе с сигналом запроса прерывания от контролера процессору передается так же вектор прерывания, то есть адрес его обработчика. Рассмотрим этот процесс более детально
На первых IBM PC/XT в качестве контролера прерываний работала микросхема Intel 8259 и имела она восемь входных линий для приема запросов прерываний от устройств: IRQ0 - IRQ7. Постепенно этих восьми входов стало не хватать, и к модели IBM PC/AT было принято решение расширить число входов до 16-ти.
Эта модификация была выполнена в духе технических казусов, преследующих платформу x86 - на материнскую плату установили два PIC Intel 8259, соединив их каскадно - выход ведомого контролера подключался к IRQ2 ведущего. Таким образом доступны оказались 15 линий: IRQ0, IRQ1, IRQ3 - IRQ7 и IRQ8 - IRQ15.
Разумеется такое решение было продиктовано вопросами экономии - поставить ещё одну готовую микросхему проще, чем разработать новую, пусть и более продвинутую. С технической точки зрения оно спорно. Однако вернемся к нашему вопросу.
Описанная схема просуществовала достаточно долго, однако колоссальное усложнение ПК, появление многоядерных систем сыграло свою роль - была разработана замена в виде Advanced Programming Interrupt Controler (APIC), но в целях совместимости со старыми ОС, традиционный PIC продолжает поддерживаться. Современные PIC входят в состав южного моста чипсета материнской платы и лишены некоторых совсем уж рудиментарных функций, вроде поддержки процессора 8080 и т.п.
Каждой линии соответствует прерывание от определенного устройства
Таблица 11. Аппаратные прерывания x86
Номера прерываний, поступающих по каждой линии IRQ можно изменять, и это для нас является принципиальным, так как по умолчанию отображение прерываний выглядит так: IRQ0 - IRQ7 соответствуют INT 0x08 - INT 0x0F, а IRQ8 - IRQ15 отображаются на INT 0x70 - INT 0x77. Видно что прерывания ведущего контролера конфликтуют с прерываниями процессора, которыми он сигнализирует о возникновении исключительных ситуаций. Поэтому на придет перепрограммировать PIC.
Таблица 12. Команды инициализации PIC
Работа процессора с внешними устройствами происходит с помощью друх схем
- Периодический опрос
- Работа по прерыванию
Механизм обслуживания устройства по прерыванию во многих случаях оказывается наиболее предпочтителен. Если устройств много, их опрос может занимать приличное время, да и иногда он бывает затруднен большой ресурсоемкостью алгоритмов, не связанных с работой внешних устройтств.
Был в моей практике электровозный дисплейный блок Gersys, под управлением MS DOS, который обслуживал контролер CAN-шины именно используя обработку прерываний, иначе сделать было просто нельзя. Вообще же выполнение "фоновых" задач в однозадачной ОС не обходится без использования прерываний.
Процессоры семейства x86 физически имеют один единственный вход для приема запросов прерываний от внешних устройств. Поэтому для мультиплексирования этого сигнала от многих устройств совместно с процессором использовался программируемый контролер прерываний - PIC. Вместе с сигналом запроса прерывания от контролера процессору передается так же вектор прерывания, то есть адрес его обработчика. Рассмотрим этот процесс более детально
1. Программируемый контролер прерываний PIC
На первых IBM PC/XT в качестве контролера прерываний работала микросхема Intel 8259 и имела она восемь входных линий для приема запросов прерываний от устройств: IRQ0 - IRQ7. Постепенно этих восьми входов стало не хватать, и к модели IBM PC/AT было принято решение расширить число входов до 16-ти.
Эта модификация была выполнена в духе технических казусов, преследующих платформу x86 - на материнскую плату установили два PIC Intel 8259, соединив их каскадно - выход ведомого контролера подключался к IRQ2 ведущего. Таким образом доступны оказались 15 линий: IRQ0, IRQ1, IRQ3 - IRQ7 и IRQ8 - IRQ15.
Описанная схема просуществовала достаточно долго, однако колоссальное усложнение ПК, появление многоядерных систем сыграло свою роль - была разработана замена в виде Advanced Programming Interrupt Controler (APIC), но в целях совместимости со старыми ОС, традиционный PIC продолжает поддерживаться. Современные PIC входят в состав южного моста чипсета материнской платы и лишены некоторых совсем уж рудиментарных функций, вроде поддержки процессора 8080 и т.п.
Каждой линии соответствует прерывание от определенного устройства
Таблица 11. Аппаратные прерывания x86
Линия | Устройство | Линия | Устройство |
IRQ0 | Программируемый интервальный таймер (высокоточный таймер событий 0) | IRQ8 | Часы реального времени(высокоточный таймер событий 1) |
IRQ1 | Клавиатура PS/2 | IRQ9 | Произвольное устройство |
IRQ2 | Запрос прерывания от ведомого контролера (каскадирование) | IRQ10 | Произвольное устройство |
IRQ3 | Произвольное устройство (IBM PC/AT - последовательный порт) | IRQ11 | Произвольное устройство или высокоточный таймер событий 2 |
IRQ4 | Произвольное устройство (IBM PC/AT - последовательный порт) | IRQ12 | Произвольное устройство, мышь PS/2 или высокоточный таймер событий 3 |
IRQ5 | Произвольное устройство (IBM PC/AT - параллельный порт) | IRQ13 | Ошибка арифметического сопроцессора |
IRQ6 | Произвольное устройство (IBM PC/AT - контроллер FDD) | IRQ14 | Произвольное устройство, первый контролер ATA (или контролер SATA в режиме совместимости) |
IRQ7 | Произвольное устройство (IBM PC/AT - параллельный порт) | IRQ15 | Произвольное устройство, второй контролер ATA (или контролер SATA в режиме совместимости) |
Номера прерываний, поступающих по каждой линии IRQ можно изменять, и это для нас является принципиальным, так как по умолчанию отображение прерываний выглядит так: IRQ0 - IRQ7 соответствуют INT 0x08 - INT 0x0F, а IRQ8 - IRQ15 отображаются на INT 0x70 - INT 0x77. Видно что прерывания ведущего контролера конфликтуют с прерываниями процессора, которыми он сигнализирует о возникновении исключительных ситуаций. Поэтому на придет перепрограммировать PIC.
Таблица 12. Команды инициализации PIC
Команда | Описание |
ICW1 | бит 0: ICW4 будет послана бит 1: каскадирования нет, ICW3 не будет послано бит 2: 1/0 размер вектора прерывания 4/8 байт (1 - для 8086, 0 - для 80386 и выше в защищенном режиме) бит 3: 1/0 срабатывание по уровню/фронту сигнала (принято 0) биты 7-4: 0001b |
ICW2 | номер обработчика прерывания для IRQ0/IRQ8 (кратный восьми) (0x08 - для ведущего, 0x70 - для ведомого контролера) |
ICW3 | Для ведущего контролера: биты 7-0: к выходу 7-0 присоеденён ведомый контролер (0x04 для PC) Для ведомого контролера: биты 3-0: номер выхода ведущего контролера, к которому подсоединен ведомый (0x02 для PC) |
ICW4 | бит 0: режим совместимости с 8085 бит 1: режим с автоматическим EOI биты 3-2: режим: 00, 01 - небуферированный 10 - буферированный/подчиненный 11 - буферированный/ведущий бит 4: контролер в режиме фиксированных приоритетов биты 7-5: 0 |
Обращение к ведущему контролеру происходит через порты с номерами 0x20 и 0x21, а к ведомому - через порты 0xA0 и 0xA1.
Процедура инициализации PIC протекает в следующем порядке:
- В порт 0x20/0xA0 посылаем ICW1
- В порт 0x21/0xA1 посылаем ICW2
- В порт 0x21/0xA1 посылаем ICW3
- В порт 0x21/0xA1 посылаем ICW4
Нас интересуют все 15 линий прерываний, поэтому мы будем работать в режиме каскадирования. В реальности в современном компьютере нет никакого каскадирования, а лишь эмуляция работы двух PIC. Кроме того мы переопределяем IRQ0 на обработчик с номером 32 (0x20), а IRQ8 - на обработчик с номером 40 (0x28). Размер вектора прерывания в нашем случае - 8 байт, помните, что вектор прерывания в защищенном режиме не просто адрес, а 8-байтный дескриптор, расположенный в IDT. Ведомый контролер подключен в нашей логической схеме к выводу IRQ2 ведущего. Контролеры будут работать в режиме нефиксированных приоритетов без буферизации. Таким образом вид команд будет таким
Для ведущего:
ICW1 = 00010001b = 0x11
ICW2 = 0x20
ICW3 = 0x04
ICW4 = 0x00
Для ведомого
ICW1 = 00010001b = 0x11
ICW2 = 0x28
ICW3 = 0x02
ICW4 = 0x00
Итак, это касается инициализации PIC, которую мы просто вынуждены будем произвести. Кроме того существуют так же команды управления OCW1 - OCW3
Таблица 13. Команды управления PIC
Команда | Описание |
OCW1 | Запрет/разрешение прерываний по линиям биты 7-0: прерывание 7-0/15-8 запрещено |
OCW2 | Команды конца прерывания и сдвига приоритетов биты 7-5: команда 000b: запрещение сдвигов приоритетов в режиме без EOI 001b: неспецифичный EOI (конец прерывания в режиме с приоритетами) 010b: нет операции 011b: специфичный EOI 100b: разрешение сдвигов приоритетов в режиме без EOI 101b: сдвиг приоритетов в режиме с неспецифичным EOI 110b: сдвиг приоритетов 111b: сдвиг приоритетов со специфичным EOI биты 4-3: 00b (указывают что это OCW2) биты 2-0: номер IRQ для команд 011b, 110b, 111b |
OCW3 | Чтение состояния контролера и режим специального маскирования бит 7: 0 биты 6-5: режим специального маскирования 00 - не изменять 10 - выключить 11 - включить биты 4-3: 01 - указывает что это OCW3 бит 2: режим опроса биты 1-0: чтение состояния контролера 00 - не читать 01 - читать регистр запросов прерывания 11 - читать регистр обслуживаемых прерываний |
Ну и для того чтобы уже совсем покончить с теорией, разберемся какие команды в каким портам соответствуют
порт 0x20/0xA0 для записи: OCW2, OCW3, ICW1
порт 0x20/0xA0 для чтения: результат OCW3
порт 0x21/0xA1 для чтения и записи: маскирование прерываний OCW1
порт 0x21/0xA1 для записи: ICW2, ICW3, ICW4 сразу после ICW1
порт 0x20/0xA0 для чтения: результат OCW3
порт 0x21/0xA1 для чтения и записи: маскирование прерываний OCW1
порт 0x21/0xA1 для записи: ICW2, ICW3, ICW4 сразу после ICW1
Теперь мы готовы к тому чтобы реализовать работу с аппаратными прерываниями.
2. Реализация обработки аппаратных прерываний
Сначала необходимо перенастроить контролер прерываний. Для этого выполним процедуру его инициализации. Для наглядности в файле common.h определим ряд констант
#define PIC1_ICW1 0x11
#define PIC1_ICW2 0x20
#define PIC1_ICW3 0x04
#define PIC1_ICW4 0x01
#define PIC2_ICW1 0x11
#define PIC2_ICW2 0x28
#define PIC2_ICW3 0x02
#define PIC2_ICW4 0x01
Так будет проще понять что мы делаем, да и переопределить настройки удобнее не разыскивая долго нужный код. Инициализацию выполним в функции init_idt()
Листинг 36. Инициализация PIC (файл descriptor_tables.c)
#define PIC1_ICW2 0x20
#define PIC1_ICW3 0x04
#define PIC1_ICW4 0x01
#define PIC2_ICW1 0x11
#define PIC2_ICW2 0x28
#define PIC2_ICW3 0x02
#define PIC2_ICW4 0x01
Так будет проще понять что мы делаем, да и переопределить настройки удобнее не разыскивая долго нужный код. Инициализацию выполним в функции init_idt()
Листинг 36. Инициализация PIC (файл descriptor_tables.c)
/*--------------------------------------------------
//
//------------------------------------------------*/
void init_idt(void)
{
idt_ptr.limit = sizeof(idt_entry_t)*256 - 1;
idt_ptr.base = (u32int) &idt_entries;
memset(&idt_entries, 0, sizeof(idt_entry_t)*256);
/* Инициализация обоих PIC */
outb(0x20, PIC1_ICW1); /* ICW1 */
outb(0xA0, PIC2_ICW1);
outb(0x21, PIC1_ICW2); /* ICW2 */
outb(0xA1, PIC2_ICW2);
outb(0x21, PIC1_ICW3); /* ICW3 */
outb(0xA1, PIC2_ICW3);
outb(0x21, PIC1_ICW4); /* ICW4 */
outb(0xA1, PIC2_ICW4);
/* Разрешаем прерывания на всех линиях */
outb(0x21, 0x00); /* OCW1 */
outb(0xA1, 0x00);
/* Определяем первые 32 обработчика */
idt_set_gate(0, (u32int)isr0, 0x08, 0x8E);
.
.
.
idt_set_gate(31, (u32int)isr31, 0x08, 0x8E);
/* Определяем обработчки для IRQ */
idt_set_gate(32, (u32int)irq0, 0x08, 0x8E);
idt_set_gate(33, (u32int)irq1, 0x08, 0x8E);
idt_set_gate(34, (u32int)irq2, 0x08, 0x8E);
.
.
idt_set_gate(31, (u32int)isr31, 0x08, 0x8E);
/* Определяем обработчки для IRQ */
idt_set_gate(32, (u32int)irq0, 0x08, 0x8E);
idt_set_gate(33, (u32int)irq1, 0x08, 0x8E);
idt_set_gate(34, (u32int)irq2, 0x08, 0x8E);
.
.
.
idt_set_gate(47, (u32int)irq15, 0x08, 0x8E);
.
idt_set_gate(47, (u32int)irq15, 0x08, 0x8E);
/* Загружаем IDTR */
idt_flush((u32int) &idt_ptr);
}
Естественно в этом примере не перечислены все обработчики дабы не загромождать описание.
Теперь нам надо определить обработчики для IRQ0 - IRQ15 в файле interrupt.s.
Листинг 37. Определяем обработчики для IRQ (файл interrupt.s)
/*-------------------------------------------
// Макрос для обработчика IRQ
//-----------------------------------------*/
.macro IRQ irq_num, isr_num /* Два параметра: номер IRQ и */
/* номер обработчика */
.global irq\irq_num
irq\irq_num:
cli /* Запрещаем прерывания */
push $0 /* Фиктивный код ошибки в стек */
push $\isr_num /* Номер обработчика в стек */
jmp irq_common_stub /* Переход к общей части обработчика */
.endm
/*-------------------------------------------
// Сами обработчики все до единого
//-----------------------------------------*/
IRQ 0, 32
IRQ 1, 33
IRQ 2, 34
/*-------------------------------------------
// Макрос для обработчика IRQ
//-----------------------------------------*/
.macro IRQ irq_num, isr_num /* Два параметра: номер IRQ и */
/* номер обработчика */
.global irq\irq_num
irq\irq_num:
cli /* Запрещаем прерывания */
push $0 /* Фиктивный код ошибки в стек */
push $\isr_num /* Номер обработчика в стек */
jmp irq_common_stub /* Переход к общей части обработчика */
.endm
/*-------------------------------------------
// Сами обработчики все до единого
//-----------------------------------------*/
IRQ 0, 32
IRQ 1, 33
IRQ 2, 34
.
.
.
IRQ 15, 47
IRQ 15, 47
/*-------------------------------------------
// Общая часть обработчика IRQ
//-----------------------------------------*/
.extern irq_handler
irq_common_stub:
pusha /* Спасаем РОН в стеке */
mov %ds, %ax /* Спасаем селектор сегмента данных */
push %eax
mov $0x10, %ax /* Загружаем сегмент кода ядра */
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
call irq_handler /* Передаем управление обработчику IRQ */
pop %ebx /* Выталкиваем из стека селектор */
mov %bx, %ds /* Восстанавливаем сегмент данных */
mov %bx, %es
mov %bx, %fs
mov %bx, %gs
popa /* Восстанавливаем РОН */
add $8, %esp /* Убираем из стека код ошибки */
/* и помещаем туда номер ISR */
sti /* Вновь разрешаем прерывания */
iret /* Возврат из обработчика */
/* с восстановлением состояния */
/* процессора */
// Общая часть обработчика IRQ
//-----------------------------------------*/
.extern irq_handler
irq_common_stub:
pusha /* Спасаем РОН в стеке */
mov %ds, %ax /* Спасаем селектор сегмента данных */
push %eax
mov $0x10, %ax /* Загружаем сегмент кода ядра */
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
call irq_handler /* Передаем управление обработчику IRQ */
pop %ebx /* Выталкиваем из стека селектор */
mov %bx, %ds /* Восстанавливаем сегмент данных */
mov %bx, %es
mov %bx, %fs
mov %bx, %gs
popa /* Восстанавливаем РОН */
add $8, %esp /* Убираем из стека код ошибки */
/* и помещаем туда номер ISR */
sti /* Вновь разрешаем прерывания */
iret /* Возврат из обработчика */
/* с восстановлением состояния */
/* процессора */
Этот код практически не отличается от того что мы писали для обработки прерываний процессора. Точно так же запоминается состояние регистров процессора и текущие селектор сегмента данных, затем вызывается обработчик, а после - восстанавливается состояние процессора и сегментной памяти.
Осталось реализовать на C средства для работы с прерываниями. Начнем с необходимых определений
Листинг 38. Заголовочный файл isr.h
#define IRQ0 32
#define IRQ1 33
#define IRQ2 34
#define IRQ3 35
#define IRQ4 36
#define IRQ5 37
#define IRQ6 38
#define IRQ7 39
#define IRQ8 40
#define IRQ9 41
#define IRQ10 42
#define IRQ11 43
#define IRQ12 44
#define IRQ13 45
#define IRQ14 46
#define IRQ15 47
typedef void (*isr_t)(registers_t);
/* Регистрация обработчика прерывания */
void register_interrupt_handler(u8int n, isr_t handler);
#define IRQ1 33
#define IRQ2 34
#define IRQ3 35
#define IRQ4 36
#define IRQ5 37
#define IRQ6 38
#define IRQ7 39
#define IRQ8 40
#define IRQ9 41
#define IRQ10 42
#define IRQ11 43
#define IRQ12 44
#define IRQ13 45
#define IRQ14 46
#define IRQ15 47
typedef void (*isr_t)(registers_t);
/* Регистрация обработчика прерывания */
void register_interrupt_handler(u8int n, isr_t handler);
Константы необходимы нам чтобы избежать путаницы - совершенно необязательно для нас помнить номера обработчиков, на которые мы отобразили аппаратные прерывания. Так же необходимо определить специальный тип для передачи функции в качестве параметра - isr_t - этот тип будет описывать нашу функцию-обработчик, которую мы захотим написать для обслуживания прерывания.
Теперь займемся реализацией
Листинг 39. Функции работы с прерываниями (файл isr.c)
#include "isr.h"
#include "text_framebuffer.h"
/* Таблица зарегистрированных в системе обработчиков прерываний */
isr_t interrupt_handlers[256];
/*---------------------------------------------
// Обработка ISR
//-------------------------------------------*/
void isr_handler(registers_t regs)
{
/* Если обработчки существует */
if (interrupt_handlers[regs.int_num] != 0)
{
/* Получаем его из таблицы по номеру прерывания */
isr_t handler = interrupt_handlers[regs.int_num];
/* и вызываем */
handler(regs);
}
}
/*---------------------------------------------
// Обработка IRQ
//-------------------------------------------*/
void irq_handler(registers_t regs)
{
/* Если прерывание номер 40 и более */
if (regs.int_num >= 40)
{
outb(0xA0, 0x20); /* Послылаем ведомому PIC EOI */
}
/* В любом случае посылаем EOI ведущему PIC */
outb(0x20, 0x20);
/* Ищем обработчик по номеру и вызываем его */
if (interrupt_handlers[regs.int_num] != 0)
{
isr_t handler = interrupt_handlers[regs.int_num];
handler(regs);
}
}
/*-----------------------------------------------
// Регистрация обработчика в системе
//---------------------------------------------*/
void register_interrupt_handler(u8int n, isr_t handler)
{
interrupt_handlers[n] = handler;
}
Как видите, мы полностью переделали этот код, по сравнению с предыдущим примером, теперь он позволяет зарегистрировать произвольный обработчик на каждое из 256 прерываний. Для этого мф определяем таблицу функций обработчиков interrupt_handlers и функцию register_interrupt_handler(...), которая помещает в эту таблицу наш обработчик.
Обработка IRQ отличается тем, что в процессе обработки необходимо сбрасывать контролер прерываний, посылая ему EOI - сигнал конца прерывания, посредством команды OCW2.
Заключение
Итак, теперь мы можем произвольным образом обрабатывать все прерывания, которые могут возникнуть в системе, и это очень мощный инструмент для дальнейшего нашего продвижения.
В этой статье обойдется без примера, поскольку самый простой пример - таймер, требует к себе большего внимания, чем быть просто частью и так довольно большой статьи.
Так что вся практика по вопросу аппаратных прерываний будет в следующий раз.
Комментариев нет:
Отправить комментарий