Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Руководство по созданию простой UNIX-подобной ОС

5. Запросы на прерывания IRQ и таймер PIT

Оригинал: "5. IRQs and the PIT"
Автор: James Molloy
Дата публикации: 2008
Перевод: Н.Ромоданов
Дата перевода: январь 2012 г.

В этой главе мы собираемся изучить запросы на прерывания (IRQ) и программируемый интервальный таймер (PIT).

5.1. Запросы прерываний (теория)

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

Опрос устройств

Выполняется в цикле, случай от случая и проверяя, готово ли устройство.

Использование прерываний

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

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

Низкоуровневые концепции, лежащие за понятием внешних прерываний, не очень сложны. Во всех устройствах, которые способны вызывать прерывания, есть цепь, соединяющая их с устройством PIC (программируемый контроллер прерываний). PIC является единственным устройством, которое напрямую связано с выводом процессора, на который поступает прерывание. PIC используется в качестве мультиплексора и он может расставлять приоритеты между устройствами, посылающими прерывания. По существу, это хорошо известный мультиплексор вида 8-1. В определенный момент стало понятно, что 8 линий IRQ просто недостаточно и к основному устройству PIC было подключено как гирлянда еще одно устройство PIC вида 8-1. Итак, во всех современных компьютерах у вас есть 2 устройства PIC, основное и подчиненное, позволяющие обслуживать в общей сложности 15 устройств, использующих прерывания (одна линия используется для связи с подчиненным устройством PIC).

Другой важной возможностью в PIC является то, что вы можете изменить номер прерывания, которое поступает по каждой линии IRQ. Это называется переназначением PIC и, на самом деле, является чрезвычайно полезным свойством. когда компьютер загружается, отображение прерываний по умолчанию устанавливается следующим образом:

  • IRQ 0..7 - INT 0x8..0xF
  • IRQ 8..15 - INT 0x70..0x77

Из-за этого у нас могут возникнуть некоторые проблемы. Отображение IRQ в основном устройстве PIC (0x8-0xF) конфликтует с номерами прерываний, используемыми процессором для того, чтобы сигнализировать об исключительных состояниях и отказах устройств (смотрите предыдущую главу). Обычная ситуация, чтобы сделать переназначить прерывания в устройствах PIC так, что прерывания 0..15 были переназначены на прерывания 32..47 (прерывание 31 последнее, которое используется процессором).

5.2. Запросы прерываний (практика)

Взаимодействие устройств PIC осуществляется через шину ввода/вывода. В каждом устройстве есть порт команд и порт данных:

  • Главное устройство: команда - 0x20, данные - 0x21
  • Подчиненное устройство: команда - 0xA0, данные - 0xA1

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

static void init_idt()
{
  ...
  // Remap the irq table.
  outb(0x20, 0x11);
  outb(0xA0, 0x11);
  outb(0x21, 0x20);
  outb(0xA1, 0x28);
  outb(0x21, 0x04);
  outb(0xA1, 0x02);
  outb(0x21, 0x01);
  outb(0xA1, 0x01);
  outb(0x21, 0x0);
  outb(0xA1, 0x0);
  ...
  idt_set_gate(32, (u32int)irq0, 0x08, 0x8E);
  ...
  idt_set_gate(47, (u32int)irq15, 0x08, 0x8E);
} 

Заметьте, что сейчас мы также задаем в шлюзе IDT номера 32-47 для нашего обработчика IRQ. Поэтому мы также должны добавить в файл interrupt.s фрагменты кода для каждого прерывания. Также нам нужен новый макрос в interrupt.s - во фрагментах кода для каждого прерывания указываются два числа связанные с этим прерыванием - это номер IRQ (0-15) и номер прерывания (32-47):

; Этот макрос создает кусок кода для IRQ - первым параметром 
; является номер IRQ, вторым - номер ISR, на который осуществляется переназначение.
%macro IRQ 2
  global irq%1
  irq%1:
    cli
    push byte 0
    push byte %2
    jmp irq_common_stub
%endmacro
 
... 
IRQ   0,    32
IRQ   1,    33
...
IRQ  15,    47

У нас также есть новый общий код - irq_common_stub. Это связано с тем, что прерывания будут вести себя чуть-чуть по-другому - прежде чем вернуться из обработчика IRQ, вы должны сообщить устройству PIC о том, что обработка закончена, для того чтобы можно было быстро перейти к следующему прерыванию (если оно находится в состоянии ожидания). Эта операция известна как о как EOI (end of interrupt - завершение прерывания). Однако, есть небольшая сложность. Если главное устройство PIC посылает прерывание IRQ (номер 0-7), мы должны (что очевидно) отправить EOI в главное устройство. Если подчиненное устройство PIC посылает IRQ (8-15), мы должны отправить EOI как в главное, так и в подчиненное устройства (из-за последовательного подключения второго).

Первый наш фрагмент общего кода на ассемблере. Он почти идентичен isr_common_stub.

; В файле isr.c
[EXTERN irq_handler]

; Это наш общий фрагмент для обработки IRQ. Он сохраняет состояние процессора, настраивает
; сегменты режима ядра, вызывает обработчик прерываний уровня языка C и, в конце концов,
; восстанавливает состояние стека.
irq_common_stub:
   pusha                    ; Помещает в стек edi,esi,ebp,esp,ebx,edx,ecx,eax

   mov ax, ds               ; Младшие 16 битов регистра eax = ds.
   push eax                 ; сохраняет дескриптор сегмента данных

   mov ax, 0x10  ; Загрузка дескриптора сегмента данных ядра
   mov ds, ax
   mov es, ax
   mov fs, ax
   mov gs, ax

   call irq_handler

   pop ebx        ; Перезагрузка исходного дескриптора сегмента данных
   mov ds, bx
   mov es, bx
   mov fs, bx
   mov gs, bx

   popa                     ; Выталкивает из стека значения edi,esi,ebp...
   add esp, 8     ; Очищает код ошибки, помещенный в стек, и помещает в стек номер ISR
   sti
   iret           ; выталкивает из стека следующие пять значений: CS, EIP, EFLAGS, SS и ESP 

Теперь код на языке C (переходим в файл isr.c):

// Этот фрагмент вызывается из кода обработчика прерывания, написанного на ассемблере.
void irq_handler(registers_t regs)
{
   // Посылает сигнал EOI (завершение прерывания) в устройства PIC.
   // Если к возникновению прерывания причастно подчиненное устройство.
   if (regs.int_no >= 40)
   {
       // Send reset signal to slave.
       outb(0xA0, 0x20);
   }
   // Посылает сигнал перезагрузки в главное устройство (а также в подчиненное устройство, если это необходимо).
   outb(0x20, 0x20);

   if (interrupt_handlers[regs.int_no] != 0)
   {
       isr_t handler = interrupt_handlers[regs.int_no];
       handler(regs);
   }
} 

Здесь все довольно очевидно - если IRQ было больше 7 (номер прерывания > 40), мы посылаем сигнал перезагрузки в подчиненное устройство. В любом случае мы также сигнал в главное устройство.

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

Нужно несколько других объявлений:

5.2.1. Файл isr.h

// Несколько определений, которые сделаю жизнь немного проще
#define IRQ0 32
...
#define IRQ15 47

// Разрешает регистрировать обратные вызовы (callbacks) для прерываний или IRQ.
// Чтобы избежать неразберихи используйте для IRQ в качестве первого параметра 
// определения #define, укаазанные выше.
typedef void (*isr_t)(registers_t);
void register_interrupt_handler(u8int n, isr_t handler); 

5.2.2. Файл isr.c

isr_t interrupt_handlers[256];

void register_interrupt_handler(u8int n, isr_t handler)
{
  interrupt_handlers[n] = handler;
} 

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

5.3. Таймер PIT (теория)

Программируемый интервальный таймер - микросхема, подключенная к IRQ0. Он может прервать работу процессора с частотой, определяемой пользователем (между 18,2 Hz и 11931 МГц). PIT является основным способом, используемым для реализации системных часов и единственным доступным методом реализации многозадачности (переключение процессов при прерывании).

В PIT есть внутренний генератор частоты, работающий приблизительно на частоте 11931 MHz. Этот тактовый сигнал подается через делитель частоты для получения в результате модуляции окончательной выходной частоты. В таймере есть 3 канала, каждый со своим собственным делителем частоты.

  • Канал 0 - наиболее часто используемый. Его выход подключен к IRQ0.
  • Канал 1 - чаще всего не используется и в современной аппаратуре просто не реализован. Используется для управления частотой регенерации памяти DRAM.
  • Канал 2 управляет работой динамика компьютера.

Канал 0 является единственным полезным для нас на данный момент.

Итак, мы хотим настроить PIT так, чтобы он прерывает нас через регулярные промежутки времени с частотой f. Я обычно устанавливаю частоту f равной приблизительно 100 Гц (одно прерывание каждые 10 миллисекунд), но вы можете установить любое другое значение. Для этого мы посылаем в PIT делитель 'divisor'. Это число, на которое нужно разделить его основную частоту (19131 MHz). Он определяется следующим образом:

divisor = 1193180 Hz / частота (в Hz) 

Кроме того следует отметить, что в PIT есть 4 регистра в пространстве ввода/вывода. 0x40-0x42 являются портами данных для каналов 0-2 соответственно, а 0x43 является портом команды.

5.4. Таймер PIT (практика)

Теперь нам нужно несколько новых файлов. В файле Timer.h находятся только объявления:

// timer.h -- Определения интерфейса для всех функций, относящихся к PIT.
// Написано для руководств по разработке ядра - автор James Molloy

#ifndef TIMER_H
#define TIMER_H

#include "common.h"

void init_timer(u32int frequency);

#endif

Не так много функций и в файле timer.c:

// timer.c -- Инициализирует PIT и обрабатывает обновления значений таймера.
// Написано для руководств по разработке ядра - автор James Molloy

#include "timer.h"
#include "isr.h"
#include "monitor.h"

u32int tick = 0;

static void timer_callback(registers_t regs)
{
   tick++;
   monitor_write("Tick: ");
   monitor_write_dec(tick);
   monitor_write("\n");
}

void init_timer(u32int frequency)
{
   // Прежде всего регистрируем обратный вызов (callback) нашего таймера.
   register_interrupt_handler(IRQ0, &timer_callback);

   // Значение, которое мы посылаем в PIT, является значением, на которое будет делиться основная частота
   // (1193180 Hz) для того, чтобы получить необходимую нам частоту. Важно отметить, что делитель должен
   // быть достаточно маленьким с тем, чтобы уместиться в 16 битов разрядной сетки.
   u32int divisor = 1193180 / frequency;

   // Посылаем байт команды.
   outb(0x43, 0x36);

   // Делитель должен посылаться побайтно, так что мы делим его на старший и младший байты.
   u8int l = (u8int)(divisor & 0xFF);
   u8int h = (u8int)( (divisor>>8) & 0xFF );

   // Посылаем делитель частоты.
   outb(0x40, l);
   outb(0x40, h);
} 

Хорошо, давайте пробежимся по этому коду. Во-первых, у нас есть функция init_timer. С ее помощью нашему механизму прерывания сообщается, что мы хотим обрабатывать IRQ0 с помощью функции timer_callback. Она будет вызываться всякий раз, когда от таймера будет поступать прерывание. Затем мы рассчитываем делитель, который будет отправлен в PIT (смотрите теорию, приведенную выше). Затем мы посылаем байт команды в командный порт PIT. Этот байт (0x36) устанавливает PIT в режим повторителя (так что, когда значение счетчика достигнет нуля, он автоматически будет обновлен) и сообщаем ему, что мы хотим установить значение делителя.

Затем мы отправляем значение делителя. Обратите внимание, что он должен отсылаться ​​в виде двух отдельных байтов, а не как одно 16-битное значение.

Когда все будет сделано, мы должны добавить в файл main.c одну следующую строчку:

init_timer(50); // Initialise timer to 50Hz 

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

Полный исходный код для этого руководства можно найти здесь.


Назад К началу Вперед