В статье рассматриваются технические детали системных таймеров и способы их программирования. Роль таймеров в системе глобальна – по их прерываниям логика чипсета контролирует длительность шинных циклов (что влияет на общую производительность), переводит процессор и ACPI-девайсы в энергосберегающий режим, организует ход часов, пробуждает разнообразные события Event и многое другое.
Более того, взяв за основу их тики, можно измерить время выполнения программных блоков, чтобы максимально оптимизировать свой код. Этот процесс назвали "профилированием", а сам контролирующий модуль - профайлером. Здесь имеются внешние факторы, которые искажают результаты тестирований – обсуждению этой проблемы и посвящена данная статья.
На первый взгляд, в век интеллектуальных процессоров оптимизация кажется не уместной. Однако не стоит переоценивать аппаратные возможности – по мере роста производительности CPU, синхронно растут и требования к нему со стороны программ. У последних версий Win, папка "Program Files" превратилась в колыбель голиафов, где рулит тяжеловесная платформа .NET, ..при этом системные ресурсы не безграничны и являются разделяемыми. Так-что тема в какой-то мере заслуживает внимания, а оптимизировать свой код или нет – дело хозяйское.
Материал получился обширным, поэтому я разделили его на 5-частей:
С высоты птичьего полёта..
Любой чипсет имеет генератор опорной частоты 14.31818 МГц. Путём применения множителей в модулях PLL, из этой опорной можно получить любые частоты, вплоть до сотни МГц. Обратный процесс реализуют при помощи счётчиков-делителей. Разводка тактовых сигналов в архитектуре AMD выглядит примерно так:
Здесь видно, что тактовый сигнал 14.318 МГц поглощается чипсетом, а точнее его
Ритмично отстукивая как метроном, таймеры определяют общую подвижность системы – чем выше частота, тем быстрее всё шевелится. Их принято разделять на аппаратные и программные. Первые встроены в плату в виде девайсов со-своими портами, а вторые считаются виртуальными и вступают в игру только после загрузки ОС. Программные таймеры представляют собой обычные циклические счётчики, тактируемые аппаратными PIT, RTC, ACPI, HPET, LAPIC или TSC.
В помощь новичку
В процессе экспериментов нужно будет производить некоторые расчеты, например в зависимости от текущей частоты f вычислить длительность такта в миллисекундах T. Поэтому сразу определимся с терминами и рассмотрим простые формулы.
В своей массе, логика использует симметричные прямоугольные сигналы типа "Меандр", когда длительность положительного полупериода равна длительности нуля (см.рис.ниже). Примером несимметричных сигналов могут послужить синхроимпульсы, где после логической 1 вполне могут следовать несколько выстроившихся в ряд нулей. Рисунок ниже демонстрирует симметричную меандру, на основе которой построены таймеры:
Возьмём, к примеру, работающий на частоте 1.19318 МГц бородатый таймер PIT. Какова будет длительность его тактов в секундах? Для этого единицу делим на частоту в герцах (если надо, после запятой дополняем хвост нулями, делая его кратным шести для MHz, и трём для kHz), и получаем:
От сюда следует, что работающий на частоте 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, а посредством
Однако PIT относится к Legacy-устройствам, поэтому с выхода его канала-0 сигнал на прерывание IRQ поступает именно в IOAPIC, а с выхода канала-2 через порт 61h непосредственно на системный динамик. Канал-0 – это единственный канал таймера, который генерит IRQ и мы можем перехватить его в своих программах:
В системном пространстве ввода-вывода, трём каналам таймера выделены порты
• Канал - 0
PIT имеет 16-битные счётчики, которые выступают в роли делителя рабочей частоты 1.193180 MHz. Например если для этой частоты делитель =1, то таймер будет генерить IRQ со-скоростью 1193180 раз в сек. Если-же счётчик установить на значение 10, то логика таймера будет пропускать каждые 9-тиков, выстреливая IRQ на 10-ом. Программное обеспечение BIOS выставляет делитель канала-0 на 16 битный максимум =65535, в результате чего на его выходе получаем генератор IRQ-0 с частотой:
Теперь зная частоту можно получить время, в промежутке которого таймер дёргает это прерывание:
За аппаратным IRQ-0 закреплён программный обработчик INT-08h. Его код просто увеличивает тики таймера в поле
Однако загрузчик 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 сек каждый. Для начала возьмём
Как только мы запишем старший байт делителя в порт 40h, счётчик таймера обновится и начнёт считать вниз от заданного значения, генерируя IRQ-0 при каждом достижении нуля – вот пример реализации:
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. Инфо-нагрузка массива представлена в таблице ниже:
Данные CMOS с диапазоном индексов от
В порту 71h нам интересны регистры с индексами A,B,C,D – именно они позволяют настраивать таймер и генератор IRQ-8. Вот что хранится в этих 8-битных регистрах (обратите внимание на атрибуты доступа):
Значит регистр
Примечательным является то, что RTC довольно древний девайс и на практике не способен выдавать стабильные IRQ на частотах свыше 1024 Hz (наблюдаются биения временных интервалов). По этой причине в битах[3:0] регистра 0Ah выставляется именно эта частота, хотя и на ней показания часов проседают за сутки на 1-2 сек. Win использует RTC только для периодической синхронизации со-своими системными часами.
Функции
Обратите внимание, что Win отключает IRQ-8 (почему обсуждается в следующей части), хотя в RM под чистым досом бит[6] находится у меня в взведённом состоянии. Как уже упоминалось, на это аппаратное.. биос вешает программный обработчик INT-70h. Можно было усовершенствовать код, например включить линию IRQ и перехватив INT-70h выводить на консоль дату\время, или что-то в этом духе.
Кстати из-под винды можно просмотреть, какими устройствами заняты 24-входные линии контролёра-прерываний IO/APIC (мин.линий 24, макс.256). Для этого комбинацией клавиш [Win+R] нужно запросить "msinfo32", которая работает вплоть до Win-10:
Рассмотренные два таймера примитивны и в наше время не представляют особого интереса у программистов. Им на смену уже давно пришли более производительные ACPI и HPET, детальный обзор которых планируется в следующей части данной статьи. До скорого..
Более того, взяв за основу их тики, можно измерить время выполнения программных блоков, чтобы максимально оптимизировать свой код. Этот процесс назвали "профилированием", а сам контролирующий модуль - профайлером. Здесь имеются внешние факторы, которые искажают результаты тестирований – обсуждению этой проблемы и посвящена данная статья.
На первый взгляд, в век интеллектуальных процессоров оптимизация кажется не уместной. Однако не стоит переоценивать аппаратные возможности – по мере роста производительности CPU, синхронно растут и требования к нему со стороны программ. У последних версий Win, папка "Program Files" превратилась в колыбель голиафов, где рулит тяжеловесная платформа .NET, ..при этом системные ресурсы не безграничны и являются разделяемыми. Так-что тема в какой-то мере заслуживает внимания, а оптимизировать свой код или нет – дело хозяйское.
Материал получился обширным, поэтому я разделили его на 5-частей:
1. Общие сведения. Legacy-таймеры PIT и RTC.
---------------------------------------С высоты птичьего полёта..
Любой чипсет имеет генератор опорной частоты 14.31818 МГц. Путём применения множителей в модулях PLL, из этой опорной можно получить любые частоты, вплоть до сотни МГц. Обратный процесс реализуют при помощи счётчиков-делителей. Разводка тактовых сигналов в архитектуре AMD выглядит примерно так:
Здесь видно, что тактовый сигнал 14.318 МГц поглощается чипсетом, а точнее его
Ссылка скрыта от гостей
. Далее, она распределяется по девайсам, однако имеется и обычный шунт для унаследованных Legacy и PnP-устройств в числе которых и таймеры. Таким образом, опорная 14.31818 МГц является макс.возможной частотой для любого таймера в системе – запомним её.Ритмично отстукивая как метроном, таймеры определяют общую подвижность системы – чем выше частота, тем быстрее всё шевелится. Их принято разделять на аппаратные и программные. Первые встроены в плату в виде девайсов со-своими портами, а вторые считаются виртуальными и вступают в игру только после загрузки ОС. Программные таймеры представляют собой обычные циклические счётчики, тактируемые аппаратными PIT, RTC, ACPI, HPET, LAPIC или TSC.
В помощь новичку
В процессе экспериментов нужно будет производить некоторые расчеты, например в зависимости от текущей частоты f вычислить длительность такта в миллисекундах T. Поэтому сразу определимся с терминами и рассмотрим простые формулы.
1. Генератор тактовых сигналов "ClockGen" состоит из кристалла кварца (XTAL, резонатор) и непосредственно генератора. Кристалл порождает колебания электрического тока, а генератор преобразует их в цифровой сигнал.
В своей массе, логика использует симметричные прямоугольные сигналы типа "Меандр", когда длительность положительного полупериода равна длительности нуля (см.рис.ниже). Примером несимметричных сигналов могут послужить синхроимпульсы, где после логической 1 вполне могут следовать несколько выстроившихся в ряд нулей. Рисунок ниже демонстрирует симметричную меандру, на основе которой построены таймеры:
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 и мы можем перехватить его в своих программах:
В системном пространстве ввода-вывода, трём каналам таймера выделены порты
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
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. Инфо-нагрузка массива представлена в таблице ниже:
Данные 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
Обратите внимание, что Win отключает IRQ-8 (почему обсуждается в следующей части), хотя в RM под чистым досом бит[6] находится у меня в взведённом состоянии. Как уже упоминалось, на это аппаратное.. биос вешает программный обработчик INT-70h. Можно было усовершенствовать код, например включить линию IRQ и перехватив INT-70h выводить на консоль дату\время, или что-то в этом духе.
Кстати из-под винды можно просмотреть, какими устройствами заняты 24-входные линии контролёра-прерываний IO/APIC (мин.линий 24, макс.256). Для этого комбинацией клавиш [Win+R] нужно запросить "msinfo32", которая работает вплоть до Win-10:
Рассмотренные два таймера примитивны и в наше время не представляют особого интереса у программистов. Им на смену уже давно пришли более производительные ACPI и HPET, детальный обзор которых планируется в следующей части данной статьи. До скорого..
Последнее редактирование: