вторник, 16 июля 2013 г.

PhantomEx: Программируемый интервальный таймер

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

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

Для чего системе нужен таймер? Позвольте, а как мы будем реализовывать многозадачность? Ведь многозадачность в однопроцессорной системе (о многопроцессорных мы пока и не заикаемся) - это просто разделенное во времени выполнение нескольких процессов на единственном процессоре. Значит нам необходим инструмент, чтобы это самое время измерять.

Кроме того, если бы мы говорили об операционной системе жесткого реального времени (ОСРВ), так там вообще требуется обеспечивать гарантированный отклик любой задачи в течении строго фиксированного временного промежутка. В случае отсутствия такого отклика система должна принять по отношению к этой уже "мертвой" с её точки зрения задаче соответствующие меры. И тут тоже не обойтись без измерения временных интервалов.

Поэтому, как всегда, начнем с теории.


1. Устройство и программная модель PIT


Программируемый интервальный таймер железка довольно таки хитрая, в своем составе содержащая 3 канала таймера, каждый из которых можно запрограммировать для работы в одном из шести режимов. Кроме того на современных материнских платах таких таймера установлено уже два, то есть в нашем распоряжении шесть программируемых каналов.

Для работы с PIT в пространстве портов ввода/вывода выделена область от 0x40 до 0x5F.

  • порт 0x40 - канал 0 (генерирует IRQ0)
  • порт 0x41 - канал 1, раньше занимавшийся обновлением динамического ОЗУ, сейчас эта функция реализуется самой памятью, из-за чего современное ОЗУ называется псевдостатическим.
  • порт 0x42 - канал 2, управляет динамиком
  • порт 0x43 - управляющие регистр первого таймера
  • порты 0x44 - 0x47 - второй таймер компьютеров с шиной Microchannel
  • порты 0x48 - 0x4B - второй таймер компьютеров с шиной EISA
 Управление таймером сводится к отправке одного байта в порт 0x43.

Таблица 14. Управляющий байт PIT в режиме программирования канала

БитыНазначение
7-6если не 11 - это номер канала, который будет программироваться

00 - канал 0
01 - канал 1
10 - канал 2
5-400 - зафиксировать текущее значение счетчика для чтения (в этом случае биты 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-611 - код команды чтения счетчиков
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

#ifndef        TIMER_H
#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); 
}


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

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

В конец функции main() помещаем такой код

print_text("\n\n");
 
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);

То есть отключим как раз линию IRQ0. Что получилось?


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

Заключение


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

Но не думайте что на этом сложности заканчиваются. Всё только начинается на самом деле :)