![]() | ![]() |
![]() |
![]() |
![]() |
|||||||||||||||||||||||||||||||||||||
![]()
|
![]() | ![]() | ![]() | ![]() |
|
||||||||||||||||||||||||||||||||||||
![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
Иногда бывает необходимо замерять промежутки времени, меньшие 55 мс (это стандартная скорость BIOS-таймера). Например, в играх скорость синхронизируется именно по таймеру. Таким образом, если скорость должна быть, скажем, 24 кадра в секунду, необходимо убыстрять таймер (например, до скорости 140 Гц, как это сделано в DOOM-е). Чем быстрее "тикает" таймер, тем с большей точностью можно замерять время. Есть и другие приложения программирования таймера, например, работа с PC-SPEAKER-ом (со встроенным динамиком) в wave-режиме (скорость таймера тогда достигает величин порядка 10-20 КГц!)
Одним словом, если вы умеете программировать, то давно уже столкнулись с проблемой убыстрения таймера. Вот только литературу подходящую найти очень трудно. И простейшая проблема убыстрения таймера становится самой сложной и непостижимой задачей для программиста.
2. Механизм работы таймера 8254
Таймер 8254 устроен довольно сложно. Он имеет 3 так называемых канала, которые работают одновременно и "тикают" с разной скоростью. Например, 0-й канал вызывает генерацию прерывания 8, 1-й используется DMA, а 2-й - для генерации звука. Каждый канал может работать в нескольких режимах. Но вся эта информация практически не представляет ценности.
Рассмотрим, как же работает таймер. Он имеет кварцевый генератор (или часы), которые "тикают" ровно 119318 раз в секунду. Нулевой канал, который мы будем рассматривать, имеет так называемый счетчик. Этот счетчик возрастает на 1 каждый "тик" часов. Когда он достигает некоторого граничного значения (которое можно установить программно), он сбрасывается, и генерируется прерывание 8.
BIOS устанавливает это граничное значение в 0, что для таймера обозначает 65536. То есть каждый 65536-й "тик" часов генерируется прерывание 8, оно "срабатывает" примерно 119318/65536=18.2 раз в секунду.
Если мы хотим установить свое граничное значение cnt, можно воспользоваться командами:
Си
outportb(0x43,6); /* channel state */ outportb(0x40,(char)cnt); /* low byte */ outportb(0x40,(char)(cnt>>8)); /* hi byte */
Паскаль
Port[$43]:=6; Port[$40]:=Lo(cnt); { младший байт значения } Port[$40]:=Hi(cnt) { а затем старший байт }Запустим подобную программу на выполнение, скажем, со значнием cnt=65536/8. Когда она отработает, мы увидим странную вещь: часы в Нортоне станут идти в 8 раз быстрее! Это происходит оттого, что после выхода из программы необходимо установить старый счетчик, который устанавливал BIOS, то есть 0.
Однако и это не решит до конца проблему. Ведь на период работы программы часы все равно будут идти в 8 раз быстрее, то есть после выхода из программы собьется время. Этого можно избежать, если написать "заплату" на 8-е прерывание таким образом, чтобы BIOS-обработчик вызывался не каждый раз, а лишь каждый восьмой. При этом в остальные семь раз необходимо посылать в 20h-й порт значение 20h, чтобы разрешить следующие прерывания от таймера (если этого не делать, прерывание 8 вызовется только один раз!!! Помню, я один раз целый день выяснял, почему не работает программа - тогда я не знал этого приема).
Приведем пример на Си, иллюстрирующий работу с таймером.
unsigned BIOSTimerSpeed=1; unsigned TimerFreq=(unsigned)(1193181L/65536L); void interrupt (*SvInt08)(void)=NULL; void Set8254Counter(unsigned cnt) { long l=cnt; if(!cnt) l=65536L; /* если 0, то на самом деле 65536 */ BIOSTimerSpeed=(unsigned)(65536L/l); outportb(0x43,6); outportb(0x40,(char)cnt); outportb(0x40,(char)(cnt>>8)); } void interrupt NewInt08(void) { static cnt=0; cnt++; /* увеличить счетчик пропущенных тиков */ /* если пора вызывать обработчик BIOS...*/ if(cnt>=BIOSTimerSpeed) { cnt=0; SvInt08(); } / иначе разрешить следующие прерывания */ else outportb(0x20,0x20); } void DeactivateTimer(void); /* предварительное описание */ int SetTimer(unsigned cnt) { /* если передается 0, то отключить нашу процедуру обработки */ if(!cnt) { Set8254Counter(0); /* отключить от прерывания */ if(SvInt08) setvect(8,SvInt08); return 0; } TimerFreq=1193181L/cnt; Set8254Counter(cnt); SvInt08=getvect(8); setvect(8,NewInt08); atexit(DeactivateTimer); return 1; } void SetTimerFreq(unsigned freq) { SetTimer((unsigned)(1193181L/freq)); } void DeactivateTimer(void) { SetTimer(0); }Для инициализации таймера нужно из main() вызвать либо SetTimer() с граничным значением в параметре, либо (что лучше использовать при очень большой частоте таймера, чтобы избежать ошибок округления), функцию SetTimerFreq() с параметром частоты таймера в Гц.
На Паскале это делается совершенно аналогично.
3 ноября 2000, 17:21
Дмитрий Котеров
dkLab, ©1999-2012