Статья Системные таймеры. Часть[1] – PIT и RTC

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

Более того, взяв за основу их тики, можно измерить время выполнения программных блоков, чтобы максимально оптимизировать свой код. Этот процесс назвали "профилированием", а сам контролирующий модуль - профайлером. Здесь имеются внешние факторы, которые искажают результаты тестирований – обсуждению этой проблемы и посвящена данная статья.

На первый взгляд, в век интеллектуальных процессоров оптимизация кажется не уместной. Однако не стоит переоценивать аппаратные возможности – по мере роста производительности CPU, синхронно растут и требования к нему со стороны программ. У последних версий Win, папка "Program Files" превратилась в колыбель голиафов, где рулит тяжеловесная платформа .NET, ..при этом системные ресурсы не безграничны и являются разделяемыми. Так-что тема в какой-то мере заслуживает внимания, а оптимизировать свой код или нет – дело хозяйское.

Материал получился обширным, поэтому я разделили его на 5-частей:


1. Общие сведения. Legacy-таймеры PIT и RTC.
---------------------------------------

С высоты птичьего полёта..

Любой чипсет имеет генератор опорной частоты 14.31818 МГц. Путём применения множителей в модулях PLL, из этой опорной можно получить любые частоты, вплоть до сотни МГц. Обратный процесс реализуют при помощи счётчиков-делителей. Разводка тактовых сигналов в архитектуре AMD выглядит примерно так:

ClockGen.png


Здесь видно, что тактовый сигнал 14.318 МГц поглощается чипсетом, а точнее его . Далее, она распределяется по девайсам, однако имеется и обычный шунт для унаследованных Legacy и PnP-устройств в числе которых и таймеры. Таким образом, опорная 14.31818 МГц является макс.возможной частотой для любого таймера в системе – запомним её.

Ритмично отстукивая как метроном, таймеры определяют общую подвижность системы – чем выше частота, тем быстрее всё шевелится. Их принято разделять на аппаратные и программные. Первые встроены в плату в виде девайсов со-своими портами, а вторые считаются виртуальными и вступают в игру только после загрузки ОС. Программные таймеры представляют собой обычные циклические счётчики, тактируемые аппаратными PIT, RTC, ACPI, HPET, LAPIC или TSC.


В помощь новичку

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

1. Генератор тактовых сигналов "ClockGen" состоит из кристалла кварца (XTAL, резонатор) и непосредственно генератора. Кристалл порождает колебания электрического тока, а генератор преобразует их в цифровой сигнал.

В своей массе, логика использует симметричные прямоугольные сигналы типа "Меандр", когда длительность положительного полупериода равна длительности нуля (см.рис.ниже). Примером несимметричных сигналов могут послужить синхроимпульсы, где после логической 1 вполне могут следовать несколько выстроившихся в ряд нулей. Рисунок ниже демонстрирует симметричную меандру, на основе которой построены таймеры:

meandr.png


2. Тактовая частота – измеряется в герцах и определяет кол-во тактов в одной секунде. Если на рисунке выше время Т принять за 1 сек, частота f будет равна 2 герц (такта). Поскольку время и частота являются обратными величинами, соотношение между ними выражается формулой: f = 1/T и соответственно T = 1/f.

Возьмём, к примеру, работающий на частоте 1.19318 МГц бородатый таймер PIT. Какова будет длительность его тактов в секундах? Для этого единицу делим на частоту в герцах (если надо, после запятой дополняем хвост нулями, делая его кратным шести для MHz, и трём для kHz), и получаем:

Код:
1 / 1193180 Гц = Т (длительность такта)
---------------------------------------
0.000000838 секунд       (s)
0.000838    милли\секунд (ms)
0.838       микро\секунд (µs)
838         нано\секунд  (ns)
838000      пико\секунд  (ps)

От сюда следует, что работающий на частоте 1.19318 MHz таймер будет подавать признаки жизни (тикать) один раз в 838 ns, а его счётчик будет уменьшаться со скоростью 1.193.180 раза в секунду. В контексте данной темы, значение 838 ns назвали "разрешением таймера" (resolution) и чем оно меньше, тем таймер считается эффективнее.

К примеру, хронометр с указанной частотой физически не сможет оповещать нас в промежутке времени 1000 ns, т.к. скважность его сигналов (аля длительность такта) имеет константу 838 – он будет или бежать (впереди паровоза) на 1000-838=162 ns, или-же отставать от него на (838+838)–1000=676 ns. Такая погрешность в современной архитектуре если и допустима, но не всегда. Поэтому в компетенции чипсета имеется целый зоопарк аппаратных таймеров с разрешениями на любой вкус. Рассмотрим основные из них..



PIT – Programmable Interval Timer
частота: 1.193180 MHz | счётчик: 16-бит

Интервальный таймер PIT является прародителем всех таймеров на мат.плате. Он имеет три идентичных канала 0-1-2, которые работают на частоте 1.19318 MHz (в 12-раз меньше опорной 14.318180 MHz). У каждого канала два входа, один из которых принимает на грудь тактовую частоту, а второй GATE через бит[0] порта 61h отключает канал-2. Входы GATE каналов 0 и 1 не используются, поэтому эти каналы активны всегда.

Выход нулевого канала 18.2-раза в секунду дёргает самое приоритетное из всех аппаратное прерывание IRQ-0. Выход первого канала не задействован и висит в воздухе. Второй канал заправляет системным бипером, подавая короткие сигналы BIOS об ошибках POST. Топология и географические координаты таймера PIT на материнской плате представлены рисунком ниже..

Значит имеются два контролёра прерываний – внешний IOAPIC (Advanced Programmable Interrupt Controller, с 24 входами) и локальный по отношению к ядру LAPIC. Когда современные девайсы PCI-Express требуют к себе внимания, они не используют тормознутый IOAPIC, а посредством
кидают клич сразу в LAPIC свободного ядра. Порты ввода-вывода PCIE-устройств находятся не в самих девайсах, а отображаются в памяти – запросы по такой схеме назвали MMIO (Memory-Mapped-Input/Output).

Однако PIT относится к Legacy-устройствам, поэтому с выхода его канала-0 сигнал на прерывание IRQ поступает именно в IOAPIC, а с выхода канала-2 через порт 61h непосредственно на системный динамик. Канал-0 – это единственный канал таймера, который генерит IRQ и мы можем перехватить его в своих программах:

PIT.png


В системном пространстве ввода-вывода, трём каналам таймера выделены порты 40-41-42h соответственно. Так-же имеется общий для всех каналов регистр-управления, доступ к которому осуществляется через порт 43h – итого у таймера PIT 4 порта. По умолчанию, все каналы работают в режиме[3] генератора непрерывной меандры, когда достигнув нуля счётчик опять восстанавливается в заданное значение. Каналы можно запрограммировать на работу в одном из шести режимов, только по окончании, прежний режим канала-0 обязательно нужно восстановить. Порт 43h доступен только для записи и при чтении всегда возвращает 0хFFFF:


C-подобный:
;// Порт конфигурации таймера 43h
;// • записать 0xb6 = 10.11.011.0 для настройки канала-2 (бипер)
;// • записать 0x36 = 00.11.011.0 для настройки канала-0 (таймер)
;//--------------------------------------------------------------
Биты [7:6] – Выбор канала для настройки:
              00 = канал-0,
              01 = канал-1,
              10 = канал-2,
Биты [5:4] – Режим доступа к портам 40-41-42h:
              00 = защёлка текущего значения,
              01 = отправляю только младший байт,
              10 =   ..только старший байт,
              11 = сначала младший, затем старший байты. //дефолт
Биты [3:1] - Режим работы счётчика:
             000 = прерывание по счётчику,
             001 = ждущий мультивибратор (одновибратор),
             010 = генератор коротких импульсов заданной частоты,
             011 = генератор меандр,   //дефолт
             100 = счётчик с разрешением,
             101 = счётчик с перезапуском.
Бит [0]    - Режим счёта:
               0 = Binary,   //дефолт
               1 = BCD.

• Канал - 0
PIT имеет 16-битные счётчики, которые выступают в роли делителя рабочей частоты 1.193180 MHz. Например если для этой частоты делитель =1, то таймер будет генерить IRQ со-скоростью 1193180 раз в сек. Если-же счётчик установить на значение 10, то логика таймера будет пропускать каждые 9-тиков, выстреливая IRQ на 10-ом. Программное обеспечение BIOS выставляет делитель канала-0 на 16 битный максимум =65535, в результате чего на его выходе получаем генератор IRQ-0 с частотой:
1193180 / 65535 =18.206 Hz.

Теперь зная частоту можно получить время, в промежутке которого таймер дёргает это прерывание:
1 / 18.206 =0.0549 сек, или ~55 ms.

За аппаратным IRQ-0 закреплён программный обработчик INT-08h. Его код просто увеличивает тики таймера в поле 0:046C, одновременно прибавляя ко времени RTC (Real-Time-Clock) указанные 55 ms. Таким макаром, с настройками BIOS часы в системе тикают 18.2-раза в секунду, и с каждым тиком увеличиваются на 55 миллисекунд.

Однако загрузчик Win перепрограммирует канал-0 таймера так, что продолжительность одного тика составляет уже не 55, а 10 ms (частота 100 Hz, вместо 18.2). Пока не появился таймер HPET, этот канал парсил виндовый планировщик-потоков, каждые 10 ms передавая управление следующему потоку в очереди (10 ms считаются квантом потока). Для отсчёта-же времени, система стремится использовать более точные таймеры, например ACPI или тот-же HPET.


• Канал - 2
Выход этого канала включает бипер на мат.плате. Изменяя рабочую частоту 1.193180 MHz делителем от 1 до 65535, канал можно настроить на вывод звука с такой-же частотой. В результате получаем диапазон звуковых частот от 1193 kHz (делитель 1, высокий тон) до 18.2 Hz (делитель 65535, баритон). Длительность тона определяется задержкой включённого состояния канала-2. По такой схеме BIOS издаёт сигналы об ошибках, например 2-коротких и 1-длинный.

Попробуем запрограммировать PIT на 10 ms вместо 55, после чего подадим 3-длинных сигнала по 0.5 сек каждый. Для начала возьмём
и переведём 10 ms в герцы.. получили 100 Hz. Теперь находим делитель для этой частоты: 1193180 /100 =11932 или 2E9Bh. На заключительном этапе, нужно в cfg-порт таймера 43h записать значение 36h (дефолт для канала-0), и следом в порт 40h отправить значение самого делителя 0х2Е9В, причём сначала мл.байт 0х9B, а потом старший 0x2E. Если в настройках мы указали передачу мл/ст.байта (см.биты 5:4 в табл.выше), то обязательно нужно передавать их вместе, иначе PIT окажется в неопределённом состоянии и вообще перестанет генерить IRQ.

Как только мы запишем старший байт делителя в порт 40h, счётчик таймера обновится и начнёт считать вниз от заданного значения, генерируя IRQ-0 при каждом достижении нуля – вот пример реализации:


C-подобный:
;// fasm (Real Mode)
;// настраивает канал-0 таймера PIT на частоту 100 Hz,
;// после чего издаёт 3-длинных сигнала бипером.
;//---------------------------------------------------
org  100h            ;// собираем 16-битный *.com-файл
jmp  start           ;// прыжок на точку входа..
;//-------
capt    db   13,10,' PIT info v.01'
        db   13,10,' ================='
        db   13,10,' Chanel-2. Speaker'
        db   13,10,'   Port 61h bit[0] GATE.............: $'
bit1    db   13,10,'   Port 61h bit[1] speaker on/off...: $'
bit5    db   13,10,'   Port 61h bit[5] counter status...: $'
begin   db   13,10
        db   13,10,' Chanel-0. Timer'
        db   13,10,'   Counter random value.............: $'
deflt   db   13,10,'   Default frequency (please wait)..: ~ $'
fix     db   13,10,'   Fix timer freq on 100 Hz ? y/n  $'
ok      db   13,10
        db   13,10,'   Frequency fix OK!'
        db   13,10,'   New IRQ-0 delay = 10 ms (100.00 Hz)',13,10,'$'
abort   db   13,10,'   Frequency not chanded. Press any key for exit...$'
hz      db   ' Hz $'
count   dw   0
;//-------

start:
;//************************************
;//*** КАНАЛ-2 бипер ******************
;//************************************
       mov   dx,capt       ;// шапка
       call  Message       ;// вызвать fn.09 вывода строк! (см.ниже)

       xor   ah,ah         ;// AH= 0
       in    al,61h        ;// AL= байт из порта динамика 61h
       xchg  ax,bx         ;// отправить его в ВХ.

;// проверяем биты 0,1,5 в регистре ВХ
;// в СХ лежит номер тестируемого бита
       xor   cx,cx         ;// бит(0) из порта 61h = GATE канала-2
       call  CheckBit      ;// проверить и показать!

       mov   dx,bit1       ;// мессага..
       call  Message       ;//
       mov   cx,1          ;// бит(1) = Speaker on/off
       call  CheckBit      ;// проверить и показать!

       mov   dx,bit5       ;//
       call  Message       ;//
       mov   cx,5          ;// бит(5) = значение "меандра" на текущий момент
       call  CheckBit      ;// проверить и показать!

;//************************************
;//*** КАНАЛ-0 таймер *****************
;//************************************
       mov   dx,begin      ;//
       call  Message       ;//
;// Читаем рандом с порта 40h (там крутится счётчик)
       and   al,0          ;// AL= 0. Команда "защёлка счётчика в канале-0"
       out   43h,al        ;//   ..отправить её в порт управления!
       in    al,40h        ;// AL= мл.байт счётчика
       xchg  ah,al         ;//   ..обменять байты местами (запомнить AL в AH)
       in    al,40h        ;// AL= ст.байт счётчика
       xchg  ah,al         ;//   ..восстановить порядок байт.
                           ;// AX = 16-битный Random счётчика!
       mov   bx,10         ;// система-счисления для вывода АХ на консоль
       call  Hex2Asc       ;// перевести число в строку! (см.fn.ниже)

;// Вычисляем кол-во вызовов IRQ-0 в течении 1-сек.
;// для этого нужно перехватить его обработчик INT-8,
;// внутри которого и будем считать вызовы в переменную.
;// (алгоритм оставляет желать лучшего, и срабатывает ~1/5)
       mov   dx,deflt       ;//
       call  Message        ;//
       push  es             ;// запомнить ES.
       mov   ax,3508h       ;// AL= номер функции.
       int   21h            ;// получить адрес ориг.обработчика INT-8 из IVT.
       mov   [old08],bx     ;// ВХ= его смещение
       mov   [old08+2],es   ;// ES= сегмент обработчика - запомнить!
       pop   es             ;// ES на родину.

       mov   ax,2508h       ;// Перехват INT-8!!!
       mov   dx,new08       ;// DS:DX= адрес нового обработчика.
       int   21h            ;// записать новый адрес в табл.векторов IVT.
       jmp   @wait          ;// прыгаем на метку ниже..

;//--- Новый обработчик INT-8
;//--- будет получать управление при каждом IRQ-0
;//********************************
new08: inc   [count]           ;//* считаем вызовы в свою переменную
       db    0EAh              ;//* опкод JMP-FAR (дальний переход),
old08  dw    0,0               ;//*  ..на оригинальный обработчик!!!
;//********************************

@wait:                   ;// 4-секундная пауза..
       xor   eax,eax     ;//    +-- сериализация
       cpuid             ;//    +----> очистить конвейер процессора!
       mov   ah,2        ;// INT-1Ah/AH=2 - запросить время у BIOS.
       int   1Ah         ;//
       mov   bl,dh       ;// DH= секунды в BCD - запомнить их в BL.
       add   bl,4        ;//  ..прибавить к текущим 4 сек.
       cmp   bl,59h      ;// проверить на переполнение BCD.
       jb    @f          ;// если меньше - вперёд (forward).
       sub   bl,59h      ;// иначе: коррекция.
@@:    mov   ah,2        ;// запрос секунд в цикле
       int   1Ah         ;//
       cmp   bl,dh       ;// сравнить с установленным порогом..
       jae   @b          ;// повторить, если порог больше/равно (backward).

;// Иначе 4-сек прошло и в переменной COUNT лежит кол-во вызовов IRQ-0.
;// восстанавливаем оригинальный обработчик INT-8.
       push  ds          ;//
       mov   ax,2508h    ;// AL= номер функции.
       lds   dx,dword[old08]  ;// DS:DX = адрес старого обработчика.
       int   21h              ;// восстановить!
       pop   ds          ;//

;// Вычисляем кол-во вызовов IRQ за 1-сек.
;//-----------------
       mov   ax,[count]  ;// АХ= кол-во за 4-сек
       mov   bx,4        ;//  ..делитель.
       xor   dx,dx       ;// здесь будет остаток.
       div   bx          ;// АХ / ВХ = вызовов за 1-сек = Hz.
       push  dx          ;// запомнить остаток (десятые)

       mov   bx,10       ;// система-счисления для вывода на консоль.
       call  Hex2Asc     ;// Print AX (целые)
       mov   al,'.'      ;// точка-разделитель..
       int   29h         ;//
       pop   ax          ;// АХ= десятые из стека
       mov   bx,10       ;// сс
       call  Hex2Asc     ;// Print AX (остаток)
       mov   dx,hz       ;//
       call  Message     ;// единицы измерения частоты.

;//*** Запрос на изменение частоты таймера: 'yes/no' ***
;//-----------------
       mov   dx,fix      ;//
       call  Message     ;//
@key:  call  GetKey      ;// ждём клавишу.. (см.fn.ниже)
       cmp   al,'y'      ;// это 'yes' ???
       je    @setFreq    ;//   ..если да!
       cmp   al,'n'      ;// это 'no' ???
       jne   @key        ;// если нет - остальные клавиши игнорируем.

;// иначе: юзер нажал 'n'..
       int   29h         ;// показать её
       mov   dx,abort    ;// мессага 'Press any key..'
       call  Message     ;//
       jmp   @exit       ;// на выход!

;// юзер нажал 'y'..
;// Задаём конфиг Канала-0 таймера -----------
@setFreq:                ;//
       int   29h         ;// показать нажатую клавишу 'Y'
       mov   al,36h      ;// канал(0),мл/ст.байт,режим(3),Hex
       out   43h,al      ;// запись в порт-управления таймером!

       mov   ax,2E9Bh    ;// делитель для 100 Hz: 1193180 / 100 = 11931
       out   40h,al      ;// отправить мл.байт 9Вh в порт-данных канала(0)
       shr   ax,8        ;//  ..сдвинуть АХ на байт вправо
       out   40h,al      ;// отправить в порт-данных ст.байт делителя 2Еh.
                         ;// *** с этого момента таймер перенастроен! ***
;//************************************
;//*** Активируем системный динамик ***
;//************************************
       mov   dl,3          ;// кол-во сигналов
@beep: in    al,61h        ;// AL= байт с порта динамика
       or    al,00000011b  ;// взвести биты 0/1 = GATE и ON
       out   61h,al        ;// включить бипер!
       call  Delay         ;//  ..задержка 0.5 сек (длительность сигнала)
       in    al,61h        ;//
       and   al,11111100b  ;// сбросить биты 0/1 = GATE и ON
       out   61h,al        ;// отключить бипер!
       call  Delay         ;//  ..задержка..
       dec   dl            ;// кол-во сигналов бипера -1
       jnz   @beep         ;// повторить, если DL не нуль..

       mov   dx,ok         ;// иначе: мессага ОК!
       call  Message       ;//
@exit: call  GetKey        ;// ждём клаву..
       int   20h           ;// выйти в DOS!!! ** Конец программы **

;//***************************************************
;//***** ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ *********************
;//***************************************************
;// Функция AH=9/INT-21h - вывод строки на консоль.
;// Аргумент: DX = адрес строки.
Message:                   ;//
       pusha               ;// запомнить все регистры!
       mov   ah,9          ;//
       int   21h           ;// сервис DOS.
       popa                ;// восстановить их.
retn                       ;// Return в вызывающий CALL.
;//-------------
;// Функция AH=0/INT-16h - ввод символа с клавиатуры.
GetKey:                    ;//
       xor   ax,ax         ;// fn.0
       int   16h           ;// сервис BIOS.
retn                       ;//
;//-------------
;// Функция проверки установленного бита в ВХ.
;// Аргумент: СХ = номер бита, начиная с нуля.
CheckBit:
       pusha
       mov   al,'1'        ;// в дефолте выводить символ 1
       bt    bx,cx         ;// проверить бит!
       jc    @f            ;// если он взведён (CF=1)..
       dec   al            ;// иначе: меняем символ на 0
@@:    int   29h           ;// вывести AL на консоль!
       popa
retn
;//-------------
;// Функция 86h/INT-15h - пауза в микросекундах.
;// задаёт длительность звука бипера.
Delay: mov   cx,7          ;// ст.слово мкс
       mov   dx,0xA120     ;// мл.слово: CX:DX = 7A120h = 500.000 мкс
       mov   ah,86h        ;// fn.86h
       int   15h           ;// пауза 0.5 сек..
retn
;//-------------
;// Универсальная функция для перевода чисел в символы.
;// Аргументы: АХ = число для вывода, ВХ = система счисления.
;// Выход: на экране.
Hex2Asc:
       xor   cx,cx       ;// СХ= 0
@@:    xor   dx,dx       ;// DX= 0, под остаток деления
       div   bx          ;// АХ / ВХ
       push  dx          ;// запомнить остаток в стеке!
       inc   cx          ;// счётчик пушей +1
       or    ax,ax       ;// всё число разделили???
       jnz   @b          ;// нет - повторить..
@@:    pop   ax          ;// иначе: AL= очередной остаток из стека
       cmp   al,9        ;// проверить на цифру 0-9
       jle   noHex       ;// если меньше/равно..
       add   al,7        ;// иначе: это A-F и коррекция +7
noHex: add   al,'0'      ;// перевести число в символ
       int   29h         ;// вывести его на консоль!
       loop  @b          ;// снять оставшиеся числа со-стека..
retn

FreqChange.png


Win закрывает прямой доступ ко всем девайсам, поэтому этот код будет работать только в реальном режиме процессора. Здесь нет никаких API и всё приходится делать в ручную. В данном случае я запустил его из-под NTVDM на системе х32, которая эмулирует DOS и обращение к портам. Так-что на реальный PIT эти изменения не влияют, и при очередном запуске cmd.exe дефолтная частота 18.2 Hz опять восстанавливается – хороший полигон для тестов. Однако системы х64 не поддерживают 16-битные приложения, поэтому нужно будет грузиться досом с USB, или поверить на-слово.


Таймер RTC – Real Time Clock
частота: 32.768 KHz


Этот таймер дергает аппаратное прерывание IRQ-8 (вектор INT-70h). В тандеме таймеров он стоит особняком, т.к. имеет свой кварц на 32.768 KHz. Способен "вести огонь" с частотой от 2-х до 8192 Hz, что является не плохим показателем (макс.122 микросек, мин.0.5 сек). Основное назначение – обновление часов в питающейся от батарейки CMOS-памяти, причём на аппаратном уровне. IRQ-8 можно отключить и это никак не подействует на ход часов RTC. Биос активирует IRQ только при работе будильника, что бывает крайне редко.

По умолчанию, таймер генерит прерывание с частотой 1024-раза в секунду, каждый раз обновляя часы в CMOS на 976.6 микро-секунд (у PIT это значение ~55 милли-сек). В системном пространстве I/O ему выделены два порта – конфигурации 70h, и данных 71h. Порт 71h открывает доступ к 128-байтному массиву данных, первые 14 из которых стандартизированы и относятся к RTC, а остальные 114 – это память CMOS (данные определяет производитель BIOS).

Адресация в массиве – индексная, чтение\запись осуществляется в два этапа, сначала записываем в порт 70h индекс требуемого байта (его порядковый номер), и только потом считываем этот байт с порта 71h. Инфо-нагрузка массива представлена в таблице ниже:

port_71.png


Данные CMOS с диапазоном индексов от 10h до 20h защищены контрольной суммой CRC, которая хранится там-же в слове по адресу 2Eh. Поэтому если мы планируем залезть в CMOS и подправить там хотя-бы 1 бит, действие нужно сопровождать пересчетом CRC – иначе при сл.загрузке рискуем получить ругательное сообщением типа "CMOS check-sum error".

В порту 71h нам интересны регистры с индексами A,B,C,D – именно они позволяют настраивать таймер и генератор IRQ-8. Вот что хранится в этих 8-битных регистрах
(обратите внимание на атрибуты доступа):

C-подобный:
;//• Индекс 0Ah - чтение\запись
;//  По-умолчанию возвращает: х.010.0110
;//--------------------------------------
бит[7]    = если 1, часы заняты (происходит обновление)
биты[6:4] = делитель опорной частоты 32.768 KHz:
            010 = 1/1  = 32.768 KHz  ;// дефолт (лучше не трогать)
            011 = 1/5  =  6.554 KHz
            100 = 1/10 =  3.276 KHz
            101 = 1/15 =  2.184 KHz
биты[3:0] = частота прерывания IRQ-8 (через степень двойки):
            0000 = выключено
            0011 = 8192 Hz   ;// 122.0 мкс
            0100 = 4096 Hz   ;// 244.1 мкс
            0101 = 2048 Hz   ;// 488.3 мкс
        --> 0110 = 1024 Hz   ;// 976.6 мкс  ;// дефолт
            0111 = 512  Hz   ;// 1.953 мс
            0001 = 256  Hz   ;// 3.906 мс
            0010 = 128  Hz   ;// 7.812 мс
            1010 = 64   Hz   ;// 15.62 мс
            1011 = 32   Hz   ;// 31.25 мс
            1100 = 16   Hz   ;// 62.50 мс
            1101 = 8    Hz   ;// 125.0 мс
            1110 = 4    Hz   ;// 250.0 мс
            1111 = 2    Hz   ;// 500.0 мс

;//• Индекс 0Bh - чтение\запись
;//-----------------------------
бит[7] = выключить обновление часов (перед записью)
бит[6] = вкл IRQ-8
бит[5] = вкл будильник
бит[4] = вкл обновление времени
бит[3] = вкл генерацию "меандр"
бит[2] = формат даты\времени - двоичный(1), в дефолте – BCD(0)
бит[1] = режим 24-часовой(1), 12-часовой(0)
бит[0] = вкл автопереход на летнее время (апрель-октябрь)

;//• Индекс 0Ch - только чтение
;//-----------------------------
бит[7] = произошло прерывание IRQ
бит[6] = разрешено периодическое прерывание IRQ
бит[5] = разрешено прерывание от будильника
бит[4] = разрешено прерывание по окончании обновления часов

;//• Индекс 0Dh - только чтение
;//-----------------------------
бит[7] = батарейка RTC/CMOS: 0 = разряжена.

Значит регистр 0Ah отвечает за частоту, 0Bh – вкл\выключает различные блоки таймера, в том числе битом[6] активирует прерывание IRQ-8. Два последних регистра являются информационными и доступны только для чтения.

Примечательным является то, что RTC довольно древний девайс и на практике не способен выдавать стабильные IRQ на частотах свыше 1024 Hz (наблюдаются биения временных интервалов). По этой причине в битах[3:0] регистра 0Ah выставляется именно эта частота, хотя и на ней показания часов проседают за сутки на 1-2 сек. Win использует RTC только для периодической синхронизации со-своими системными часами.

Функции INT-15h/AH=86h, AX=8300h, AX=8301h позволяют программировать RTC, ..вводить задержки и запускать таймер. Имеется так-же сервис биоса INT-1Ah, который оперирует временем и будильником. Однако у этих функций нет доступа к регистрам-состояния А,В,С,D – к ним нужно пробираться только через порты. Напишем небольшое приложение, чтобы потрогать их руками:


C-подобный:
;// fasm (Real Mode)
;// Выводит информацию о таймере RTC
;//-----------------------------------
org  100h
jmp  start
;//-------
caption    db  13,10,' RTC info v.01'
           db  13,10,' ================'
           db  13,10,'  Index 0Ah [6:4] - RTC frequency..: $'
irqFreq    db  13,10,'  Index 0Ah [3:0] - IRQ-8 frequency: $'
irqEnable  db  13,10,'  Index 0Ch [6]   - IRQ-8 enable...: $'
alarm      db  13,10,'  Index 0Ch [5]   - Alarm enable...: $'
press      db  13,10,' ================'
           db  13,10,' Press any key... $'

divTable   dw  0,0,d2,d3,d4,d5  ;// таблица указателей на сообщения
d2         db  '32.768 KHz $'
d3         db  '6.554 KHz $'
d4         db  '3.276 KHz $'
d5         db  '2.184 KHz $'

freqTable  dw  0,0,0,f3,f4,f5,f6,f7
f3         db  '8192 Hz = 122.0 mcs $'
f4         db  '4096 Hz = 244.1 mcs $'
f5         db  '2048 Hz = 488.3 mcs $'
f6         db  '1024 Hz = 976.6 mcs $'
f7         db  '512 Hz = 1.953 ms  $'   ;// можно добавить остальные
;//-------

start:  mov   dx,caption    ;// шапка
        call  Message       ;// позвать функцию(9) вывода строк!
;//---- Регистр-состояния 0Аh --------
;// берём опорную частоту CMOS/RTC
        xor   ax,ax         ;// АХ= 0
        mov   al,0Ah        ;// регистр 0Аh
        out   70h,al        ;// в порт-конфигурации его.
        mov   cx,100        ;// небольшая пауза.
        loop  $             ;//  ..(маршируем на месте СХ-раз)
        in    al,71h        ;// AL= байт-данных с индекса 0Аh
        push  ax            ;//  ..запомнить его
        and   al,01110000b  ;// оставить только биты [6:4]
        shr   al,4          ;// сдвинуть вправо на 4-бита
        shl   ax,1          ;// умножить АХ на 2
                            ;//  ..(получили смещение в таблице).
        mov   si,divTable   ;// SI= адрес таблицы переходов.
        add   si,ax         ;// сместиться в ней по значению АХ.
        mov   dx,[si]       ;// взять в DX адрес сообщения из таблицы
        call  Message       ;// вывести его на консоль!

;//---- Частота генератора IRQ-8 (биты[3:0] регистра 0Аh)
        mov   dx,irqFreq    ;//
        call  Message       ;//
        pop   ax            ;// снимаем со-стека считанный раньше байт
        and   al,00001111b  ;// оставить в нём только биты[3:0]
        shl   ax,1          ;// умножить на 2 (получили смещение)
        mov   si,freqTable  ;// SI= адрес таблицы переходов
        add   si,ax         ;// смещаемся в ней по АХ
        mov   dx,[si]       ;// DX= указатель на мессагу
        call  Message       ;// print string!

;//---- Регистр-состояния 0Ch --------
;// проверяем вкл.состояние IRQ-8 и будильника
        mov   dx,irqEnable  ;//
        call  Message       ;//
        xor   ax,ax         ;//
        mov   al,0Ch        ;// индекс в массиве данных
        out   70h,al        ;// выбрать его.
        mov   cx,100        ;// пауза..
        loop  $             ;//
        in    al,71h        ;// читаем байт с регистра 0Сh
        mov   bx,ax         ;// отправить его в ВХ (для теста битов)
        push  bx            ;//   ..запомнить для сл.операции.
        mov   cx,6          ;// номер тестируемого бита
        call  CheckBit      ;// проверить на 1/0

;// тест на состояние будильника (индекс 0Сh)
        mov   dx,alarm      ;//
        call  Message       ;//
        pop   bx            ;// снять уже считанный байт-данных
        mov   cx,5          ;// номер тестируемого бита
        call  CheckBit      ;// проверить и вывести на консоль!

;//** Конец программы *******//
        mov   dx,press      ;//
        call  Message       ;//
@exit:  call  GetKey        ;// ждём клаву..
        int   20h           ;// выйти в DOS!!!

;//***************************************************
;//***** ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ *********************
;//***************************************************
Message:              ;// аргумент в DX.
       mov   ah,9     ;// fn.9
       int   21h      ;// сервис DOS.
retn                  ;// Return в вызывающий CALL.
GetKey:
       xor   ax,ax    ;// fn.0
       int   16h      ;// сервис BIOS.
retn
;// аргумент: cx = номер бита.
CheckBit:
       mov   al,'1'   ;// в дефолте выводить 1
       bt    bx,cx    ;// проверить бит!
       jc    @f       ;// если взведён..
       dec   al       ;// иначе: меняем 1 на 0
@@:    int   29h      ;// вывести AL на консоль!
retn

cmos_rtc.png


Обратите внимание, что Win отключает IRQ-8 (почему обсуждается в следующей части), хотя в RM под чистым досом бит[6] находится у меня в взведённом состоянии. Как уже упоминалось, на это аппаратное.. биос вешает программный обработчик INT-70h. Можно было усовершенствовать код, например включить линию IRQ и перехватив INT-70h выводить на консоль дату\время, или что-то в этом духе.

Кстати из-под винды можно просмотреть, какими устройствами заняты 24-входные линии контролёра-прерываний IO/APIC (мин.линий 24, макс.256). Для этого комбинацией клавиш [Win+R] нужно запросить "msinfo32", которая работает вплоть до Win-10:

msinfo32.png


Рассмотренные два таймера примитивны и в наше время не представляют особого интереса у программистов. Им на смену уже давно пришли более производительные ACPI и HPET, детальный обзор которых планируется в следующей части данной статьи. До скорого..
 
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!