За прошедшее с начала цикла время мы достаточно серьезно продвинулись вперед: наше "игрушечное" ядро знает о сегментной организации памяти, и предоставляет возможности для обработки прерываний в защищенном режиме.
Следующим нашим шагом будет, во-первых, демонстрация работоспособности механизма обработки аппаратных прерываний, а во-вторых - изучение ещё одной важной железяки - программируемого интервального таймера - PIT.
Для чего системе нужен таймер? Позвольте, а как мы будем реализовывать многозадачность? Ведь многозадачность в однопроцессорной системе (о многопроцессорных мы пока и не заикаемся) - это просто разделенное во времени выполнение нескольких процессов на единственном процессоре. Значит нам необходим инструмент, чтобы это самое время измерять.
Кроме того, если бы мы говорили об операционной системе жесткого реального времени (ОСРВ), так там вообще требуется обеспечивать гарантированный отклик любой задачи в течении строго фиксированного временного промежутка. В случае отсутствия такого отклика система должна принять по отношению к этой уже "мертвой" с её точки зрения задаче соответствующие меры. И тут тоже не обойтись без измерения временных интервалов.
Поэтому, как всегда, начнем с теории.
1. Устройство и программная модель PIT
Программируемый интервальный таймер железка довольно таки хитрая, в своем составе содержащая 3 канала таймера, каждый из которых можно запрограммировать для работы в одном из шести режимов. Кроме того на современных материнских платах таких таймера установлено уже два, то есть в нашем распоряжении шесть программируемых каналов.
Для работы с PIT в пространстве портов ввода/вывода выделена область от 0x40 до 0x5F.
- порт 0x40 - канал 0 (генерирует IRQ0)
- порт 0x41 - канал 1, раньше занимавшийся обновлением динамического ОЗУ, сейчас эта функция реализуется самой памятью, из-за чего современное ОЗУ называется псевдостатическим.
- порт 0x42 - канал 2, управляет динамиком
- порт 0x43 - управляющие регистр первого таймера
- порты 0x44 - 0x47 - второй таймер компьютеров с шиной Microchannel
- порты 0x48 - 0x4B - второй таймер компьютеров с шиной EISA
Таблица 14. Управляющий байт PIT в режиме программирования канала
Биты | Назначение |
7-6 | если не 11 - это номер канала, который будет программироваться 00 - канал 0 01 - канал 1 10 - канал 2 |
5-4 | 00 - зафиксировать текущее значение счетчика для чтения (в этом случае биты 3-0 не используются 01 - чтение/запись только младшего байта 10 - чтение/запись только старшего байта 11 - чтение/запись сначала младшего, затем старшего байта |
3-1 | Режим работы канала 000 - прерывание IRQ0 при достижении нуля 001 - ждущий мультивибратор 010 - генератор импульсов 011 - генератор прямоугольных импульсов (основной режим) 100 - программно запускаемый одновибратор 101 - аппаратно запускаемый одновибратор |
0 | Формат счетчика 0 - двоичное 16-битное число (0x0000 - 0xFFFF) 1 - двоично-десятичное число (0000 - 9999) |
Если биты 7-6 равны 11, считается что байт посылаемый в 0x43 - команда чтения счетчиков, и её формат отличается от команды программирования.
Таблица 15. Управляющий байт PIT в режиме чтения канала
Биты | Назначение |
7-6 | 11 - код команды чтения счетчиков |
5-4 | Что читаем: 00 - сначала состояние, потом значение счетчика 01 - значение счетчика 10 - состояние канала |
3-1 | Команда относится к каналам 3-1 |
Если этой командой запрашивается состояние канала, то новые команды игнорируются, пока не будет прочитано состояние всех каналов, которые были заказаны битами 3-1.
Состояние и значение счетчика данного канала получают чтением из порта, соответствующего требуемому каналу. Формат байта состояния таков.
Таблица 16. Байт состояния канала PIT
Биты | Назначение |
7 | состояние входа OUTx на момент выполнения команды чтения счетчика. Так как в режиме 3 счетчик уменьшается на 2 за каждый цикл, состояние этого бита, замороженное командой фиксации значения счетчика, укажет, в каком полуцикле находился таймер |
6 | состояние счетчика не загружено/загружено (используется в режимах 1 и 5, а также после команды фиксации текущего значения |
5-0 | Совпадают с битами 5-0 последней команды, посланной в порт 0x43 |
PIT работает на частоте 1193180 Гц - это 1/4 часть тактовой частоты процессора 8088. Для программирования таймера в основной режим следует выполнить следующее
- послать в порт 0x43 команду 00110110b = 0x36 - то есть установить режим 3 для канала 0, установить режим работы в виде генератора прямоугольных импульсов с двоичным значением счетчика.
- Послать младший байт начального значения счетчика в порт соответствующего канала.
- Послать старший байт начального значения счетчика в порт соответствующего канала
После этого таймер начнет уменьшать введенное число с частотой 1193180 Гц, и после того как оно достигнет нуля, счетчик снова получит введенное нами начальное значение. Кроме того, при достижении нуля таймер на канале 0 прерывание IRQ0, а на канале 2 выдаст прямоугольный импульс на системный динамик. По умолчанию счетчик имеет значение 0xFFFF, максимально возможное, поэтому точная частота вызова прерывания IRQ0 по умолчанию равна 1193180/65536 = 18,2 Гц.
Таким образом, начальное значение счетчика - это своеобразный делитель основной частоты работы таймера, и теоретически мы можем запрограммировать таймер для работы в диапазоне частот от 18,2 до 1193180 Гц.
На этом с теорией можно покончить и перейти к практике.
2. Реализация функций работы с таймером
Заголовочный файл для тестирования таймера будет простым
Листинг 40. Заголовочный файл timer.h
Листинг 40. Заголовочный файл timer.h
#ifndef TIMER_H
#define TIMER_H
#include "common.h"
#define BASE_FREQ 1000
void init_timer(u32int frequency);
#endif
Всё что нам нужно, это инициализировать таймер для работы с заданной частотой вызова IRQ0. Кроме того нам необходим обработчик прерывания
#define TIMER_H
#include "common.h"
#define BASE_FREQ 1000
void init_timer(u32int frequency);
#endif
Всё что нам нужно, это инициализировать таймер для работы с заданной частотой вызова IRQ0. Кроме того нам необходим обработчик прерывания
Листинг 41. Модуль работы с таймером timer.c
#include "timer.h"
#include "isr.h"
#include "text_framebuffer.h"
u32int tick = 0;
u8int hour = 0;
u8int min = 0;
u8int sec = 0;
/*----------------------------------------------
// Обработчик прерывания IRQ0
//--------------------------------------------*/static void timer_callback(registers_t regs)
{
tick++;
if (tick == BASE_FREQ)
{
tick = 0;
sec++;
}
if (sec > 59)
{
sec = 0;
min++;
}
if (min > 59)
{
min = 0;
hour++;
}
print_text("Time of work: ");
if (hour < 10)
print_text("0");
print_dec_value((u32int) hour);
print_text(":");
if (min < 10)
print_text("0");
if print_dec_value((u32int) min);
print_text(":");
if (sec < 10)
print_text("0");
print_dec_value((u32int) sec);
print_text("\r");
}
/*----------------------------------------------
// Инициализация таймера
//--------------------------------------------*/
void init_timer(u32int frequency)
{
u32int divisor; /* Делитель частоты */
u8int low; /* Младший байт делителя */
u8int high; /* Старший байт делителя */
/* Регистрируем в системе обработчик для IRQ0 */
register_interrupt_handler(IRQ0, &timer_callback);
/* Расчитываем делитель по заданной частоте */
divisor = 1193180/frequency;
/* Задаем режим работы таймера */
outb(0x43, 0x36);
/* Разбираем делитель на байты */
low = (u8int) (divisor & 0xFF);
high = (u8int) ( (divisor >> 8) & 0xFF );
/* Отсылаем делитель в канал 0 PIT */
outb(0x40, low);
outb(0x40, high);
}
#include "isr.h"
#include "text_framebuffer.h"
u32int tick = 0;
u8int hour = 0;
u8int min = 0;
u8int sec = 0;
/*----------------------------------------------
// Обработчик прерывания IRQ0
//--------------------------------------------*/static void timer_callback(registers_t regs)
{
tick++;
if (tick == BASE_FREQ)
{
tick = 0;
sec++;
}
if (sec > 59)
{
sec = 0;
min++;
}
if (min > 59)
{
min = 0;
hour++;
}
print_text("Time of work: ");
if (hour < 10)
print_text("0");
print_dec_value((u32int) hour);
print_text(":");
if (min < 10)
print_text("0");
if print_dec_value((u32int) min);
print_text(":");
if (sec < 10)
print_text("0");
print_dec_value((u32int) sec);
print_text("\r");
}
/*----------------------------------------------
// Инициализация таймера
//--------------------------------------------*/
void init_timer(u32int frequency)
{
u32int divisor; /* Делитель частоты */
u8int low; /* Младший байт делителя */
u8int high; /* Старший байт делителя */
/* Регистрируем в системе обработчик для IRQ0 */
register_interrupt_handler(IRQ0, &timer_callback);
/* Расчитываем делитель по заданной частоте */
divisor = 1193180/frequency;
/* Задаем режим работы таймера */
outb(0x43, 0x36);
/* Разбираем делитель на байты */
low = (u8int) (divisor & 0xFF);
high = (u8int) ( (divisor >> 8) & 0xFF );
/* Отсылаем делитель в канал 0 PIT */
outb(0x40, low);
outb(0x40, high);
}
В обработчике не делается ничего особенного - пока что мы поручим ему отмерять время, прошедшее с момента запуска таймера и выводить его на экран. Каждый раз при срабатываний обработчика наращивается число "тиков", и когда это число будет равно частоте вызова прерывания IRQ0 то у нас прошла ровно 1 секунда.
При инициализации мы регистрируем обработчик аппаратного прерывания IRQ0 в системе, задаем работу таймера в режиме генератора прямоугольных импульсов, с последовательной отсылкой байтов начального значения счетчика и двоичным форматом счетчика.
В конец функции main() помещаем такой код
print_text("\n\n");
asm volatile ("sti");
set_bkground_color(BLACK);
init_timer(BASE_FREQ);
asm volatile ("sti");
set_bkground_color(BLACK);
init_timer(BASE_FREQ);
Вы помните, что у нас принудительно отключены прерывания командой cti сразу после загрузки ядра. Теперь их следует включить, дав команду sti иначе ничего работать не будет. Инициализируем таймер на рабочей частоте 1000 Гц и посмотрим что получилось
Неплохо :) Можем сверить наши самодельные часы и убедится что они нормально отщелкивают секунды работы нашего ядра.
Но это ещё не всё. Помните о том что мы можем избирательно отключать прерывания на определенных линиях посылая PIC команду OCW1. Модифицируем наш код чуть-чуть
print_text("\n\n");
asm volatile ("sti");
outb(0x21, 0x01);
set_bkground_color(BLACK);
init_timer(BASE_FREQ);
asm volatile ("sti");
outb(0x21, 0x01);
set_bkground_color(BLACK);
init_timer(BASE_FREQ);
То есть отключим как раз линию IRQ0. Что получилось?
Ничего не работает, при этом обработка других аппаратных прерываний будет вполне доступна.
Заключение
Заметка получилась довольно короткой, особенно в практическом плане. Что ж, основную часть работы мы проделали в других статьях.
Но не думайте что на этом сложности заканчиваются. Всё только начинается на самом деле :)
Комментариев нет:
Отправить комментарий