Channel 0 of the 8253/8254 timer chip divides its 1 193 182 Hz input by 65 536 and pulses IRQ 0 of the 8259 interrupt controller 18.2065 times a second, every 54.9 ms. The 8259 passes it to the CPU as vector 08h. Nobody executes INT 08h; it simply happens between two instructions whenever interrupts are enabled.
What the BIOS handler does
| Step | What happens |
|---|---|
| 1 | increment the 32-bit tick count at 0040:006Ch |
| 2 | at 1800B0h ticks (24 hours) reset it to 0 and set the midnight flag at 0040:0070h |
| 3 | count down the floppy motor timeout at 0040:0040h and switch the motor off at 0 |
| 4 | call INT 1Ch, the user timer hook (an IRET by default) |
| 5 | send the end of interrupt: MOV AL, 20h / OUT 20h, AL |
| 6 | IRET |
The end of interrupt (EOI) tells the 8259 the handler has finished; until it arrives the controller sends no further interrupts of this or lower priority, which is every other IRQ but none. Forgetting it is the classic way to freeze a PC: the keyboard and the clock stop at once.
Hooking the tick
A program that wants to run something on every tick (a clock on the screen, music, a timeout) hooks INT 1Ch rather than 08h: the BIOS has already counted the tick and will send the EOI. The example installs a 1Ch handler that counts, lets about a second of ticks go by, puts the old vector back and prints the count:
MOV AX, 351Ch ; DOS: get vector 1ChINT 21h ; ES:BX = the old handlerMOV WORD PTR OLD1, BXMOV WORD PTR OLD1+2, ESMOV AX, 251Ch ; DOS: set vector 1Ch to DS:DXMOV DX, OFFSET TICK1INT 21hMOV AX, 40hMOV ES, AX ; ES = the BIOS data areaCLI ; no tick between these two:MOV BX, ES:[6Ch] ; the start, low word of the tick countMOV COUNT1, 0 ; and our count from zeroSTIWAIT1:MOV AX, ES:[6Ch]SUB AX, BXCMP AX, 18 ; 18 ticks: about one secondJB WAIT1PUSH DSMOV DX, WORD PTR OLD1MOV DS, WORD PTR OLD1+2 ; DS:DX = the old handlerMOV AX, 251Ch ; put it backINT 21hPOP DSMOV DX, OFFSET RAN1MOV AH, 09h ; DOS: print the $-string at DS:DXINT 21hMOV AX, COUNT1CALL PDEC ; 18MOV DX, OFFSET TIMES1MOV AH, 09h ; DOS: print the $-string at DS:DXINT 21hTICK1 PROC FAR ; INT 1Ch: called by the BIOS on every tickINC CS:COUNT1 ; DS is unknown here: go through CSJMP DWORD PTR CS:OLD1 ; and on to the previous handlerTICK1 ENDPOLD1 DD 0COUNT1 DW 0RAN1 DB "INT 1Ch ran $"TIMES1 DB " times", 13, 10, "$"PDEC PROC NEAR ; print AX in decimal; keeps every registerPUSH AXPUSH BXPUSH CXPUSH DXMOV BX, 10XOR CX, CX ; digits on the stackDECD1:XOR DX, DXDIV BX ; AX = AX / 10, DX = the last digitPUSH DXINC CXTEST AX, AXJNZ DECD1DECD2:POP DX ; the digits come back first to lastADD DL, "0"MOV AH, 02hINT 21hLOOP DECD2POP DXPOP CXPOP BXPOP AXRETPDEC ENDP
Rules for a timer handler
It interrupts any code at any moment, so it must save every register it changes and may not assume anything about DS or ES. It must be short: the BIOS calls 1Ch before the EOI, so while it runs no other hardware interrupt gets through. It must not call DOS, which is not reentrant; the interrupted code may be in the middle of a DOS call itself. It should chain to the previous handler (here with JMP DWORD PTR), so that other resident programs hooked before it keep working. And the vector must be restored before the program exits: the memory the handler lives in is freed, and the next tick would jump into whatever is loaded there.
Faster ticks
Games and music players reprogram channel 0 for a higher rate (port 43h, then the divisor to port 40h) and hook 08h itself. Their handler must then call the old 08h only every n-th time, at the original 18.2 Hz, or the time of day runs fast, and send the EOI itself on the other calls. Before exit, the divisor goes back to 0 (65 536) and the vector to the old handler.