Урок 5. Аппаратные прерывания

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

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

Существует несколько методов обмена информацией с внешними устройствами. Наиболее распространенными являются 'опрос' и 'прерывания'.
Опрос
    В цикле опрашиваются все устройства по порядку.

Прерывания
    Когда устройство готово оно инициирует прерывание.

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

На низком уровне прерывания от внешних устройств инициируются следующим образом. Все устройства подсоединяются к программируемому контроллеру прерываний (programmable interrupt controller, PIC). PIC, в свою очередь, подсоединен к линии прерываний центрального процессора. Он используется как мультиплексор, а также может назначать приоритет прерываниям. На самом деле это и есть несколько усовершенствованный мультиплексор 8 к 1. Однако, кто-то где-то решил, что 8 линий для аппаратных прерываний не достаточно, и они присоединили к первому контроллеру второй такой же контроллер прерываний. Так что во всех современных ПК находится два контроллера прерываний: master и slave, обслуживающих в общем счете 15 различных устройств (одна линия master контроллера используется для подключения slave контроллера).

Когда компьютер загружается, таблица прерываний в PIC выглядит следующим образом:
  • IRQ 0..7 - INT 0x08..0x0F;
  • IRQ 8..15 - INT 0x70..0x77.
То есть прерывания от master контроллера конфликтуют с номерами прерываний, используемых процессором (смотри последний урок). Нам надо будет переназначить номера прерываний. Обычно IRQ 0..15 назначают на ISR 32..47 (31 - номер последнего прерываний, используемого процессором).

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

Обмен сообщениями с PIC осуществляется с помощью шины ввода-вывода. Каждый контроллер прерываний имеет свой порт данных и командный порт:
  • Master - команды: 0x20, данные: 0x21
  • Slave - команды: 0xA0, данные: 0xA1
Код для назначения новых номеров прерываний достаточно сложен для понимания. Если вас интересует что же происходит на самом деле - здесь есть замечательное объяснение.

    // Remap the IRQ table
    // Send initialization signal
    outb(0x20,0x11);
    outb(0xA0,0x11);
    // Set offset
    outb(0x21,0x20);
    outb(0xA1,0x28);
    // Set master-slave relation
    outb(0x21,0x04);
    outb(0xA1,0x02);
    // Set 8086 mode
    outb(0x21,0x01);
    outb(0x21,0x01);
    // End of mess
    outb(0x21,0x00);
    outb(0xA1,0x00);

    ...

    idt_set_gate( 32, (u32int)irq0, 0x08, 0x8E);
    ...
    idt_set_gate( 47, (u32int)irq15, 0x08, 0x8E);

Теперь мы должны установить обработчики для прерываний 32-47. Также мы должны добавить для них единый обработчик.

%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

Мы не можем использовать единый обработчик, с помощью которого мы обрабатывали прерывания до сих пор, потому что в этом обработчике мы должны послать в PIC сигнал EOI (End of Interrupt). Причем, если запрос на прерывание пришел от master контроллера, то мы должны послать сигнал EOI ему, если же от slave контроллера - то и в master и в slave.

[EXTERN irq_handler]

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
    add esp,8        ; очищаем стек от значений кода ошибки и номера вектора прерывания
    sti
    iret

И код на C:
void irq_handler(registers_t regs)
{
    // Посылаем контроллеру прерываний сигнал EOI (end of interrupt)
    // если прерываний пришло от второго контроллера (slave)
    if (regs.int_no >= 40)
    {
        // Посылаем сигнал reset второму контроллеру (slave)
        outb(0xA0,0x20);
    }
    // первому контроллеру (master) посылаем сигнал EOI в любом случае
    outb(0x20,0x20);

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

Тут все просто. Если номер прерывания > 40 (IRQ > 7), то мы посылаем сигнал EOI в slave контроллер. В master контроллер мы посылаем сигнал EOI в любом случае. Также, Вы наверное заметили, что я добавил простой механизм, позволяющий регистрировать и вызывать обработчики прерываний.

Нам понадобятся еще несколько объявлений:

5.2.1. isr.h

#define IRQ0 32
...
#define IRQ15 47

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. Программируемый Таймер (Теория)

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

Таймер оснащен встроенным осциллятором, частота которого около 1.1931 МГц. Сигнал от него проходит через делитель частоты для модуляции выходной частоты. У таймера три канала:
  • Канал 0. Его выход подключен к линии IRQ0;
  • Канал 1 ранее использовался для очистки DRAM. Современные ОС его не используют;
  • Канал 2 контроллирует динамик ПК.
Единственный канал. используемый ОС - это канал 0.

Если мы хотим, чтобы таймер генерировал прерывание через заданные промежутки времени, мы должны сообщить делителю частоты значение N:
freq = 1193180 Hz / N;
Для таймера задано четыре порта ввода-вывода: 0x40-0x42 - порты данных, 0x43 - командный.

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

Нам понадобятся несколько новых файлов.

timer.h:
#ifndef TIMER_H_
#define TIMER_H_

#include "common.h"

void init_timer(u32int frequency);

#endif

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

static 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 freq)
{
    // Для начала регистрируем наш callback
    register_interrupt_handler(IRQ0,&timer_callback);

    // Значение, сообщаемое в PIT
    u32int divisor = 1193180 / freq;

    // Послать команду
    outb(0x43,0x36);

    // Значение посылается в два этапа
    u8int l = (u8int)(divisor & 0xFF);
    u8int h = (u8int)((divisor>>8) & 0xFF);

    monitor_write("Low: ");
    monitor_write_dec(l);
    monitor_write(" High: ");
    monitor_write_dec(h);
    monitor_put('\n');
    // Посылаем на порт данных
    outb(0x40,l);
    outb(0x40,h);
}

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

Когда это сделано, просто надо добавить в нашу функцию main() следующий код:
init_timer(50);
Компилируем и проверяем результат.

Пример работы программы:


Именно. Программа просто каждый раз при прерывании от таймера увеличивает счетчик тиков на 1 и выводит его значение на экран.

На этом все. А в следующий раз мы займемся организацией виртуальной памяти.

Код уже на github.
Также код можно скачать по ссылке: http://dl.dropbox.com/u/40211944/Lesson5.tar.gz

Комментариев нет:

Отправить комментарий