понедельник, 15 июля 2013 г.

PhantomEx: Обработка аппаратных прерываний

Работа процессора в одиночестве бессмысленна сама по себе. Есть множество периферийных устройств, выполняющих различные функции, с которыми необходимо осуществлять информационный обмен.

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

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

Был в моей практике электровозный дисплейный блок 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/2IRQ9Произвольное устройство
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

Теперь мы готовы к тому чтобы реализовать работу с аппаратными прерываниями.

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)

/*--------------------------------------------------
//   
//------------------------------------------------*/

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(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 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                  /* Возврат из обработчика */
                            /* с восстановлением состояния */
                            /* процессора */

Этот код практически не отличается от того что мы писали для обработки прерываний процессора. Точно так же запоминается состояние регистров процессора и текущие селектор сегмента данных, затем вызывается обработчик, а после - восстанавливается состояние процессора и сегментной памяти.

Осталось реализовать на 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);

Константы необходимы нам чтобы избежать путаницы - совершенно необязательно для нас помнить номера обработчиков, на которые мы отобразили аппаратные прерывания. Так же необходимо определить специальный тип для передачи функции в качестве параметра - 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.

Заключение


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

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

Так что вся практика по вопросу аппаратных прерываний будет в следующий раз.