Tvůrce webu je i pro tebe! Postav třeba web. Bez grafika. Bez kodéra. Hned.
wz

IO_CLK.ASM

Ovladač hodin

V soubor IO_CLK.ASM jsou obsaženy funkce pro nastavení a zjištění systémového data a času.

Systémové hodiny počítače IBM PC používají přerušení programovatelné děličky kmitočtu řízené generátorem hodin 1,193182 MHz. Z historických důvodů budeme dále počítat s méně přesnou hodnotou 1,193180 MHz (tuto hodnotu používá k výpočtům klasický IBM PC BIOS a umožňuje jednodušší výpočty při přepočtech času).

Hodinový kmitočet je dělen dělicím poměrem 1:65536. Vznikne tak přerušovací frekvence hodin 18,20648193 Hz (správná hodnota je 18,20651245 Hz), to odpovídá intervalu 54,92549322 ms (přesněji 54,92540115 ms). Za den to odpovídá 1 573 040 impulsům (přesněji 1 573 043), při dosažení tohoto čísla BIOS vynuluje čítač času a nastaví příznak, že nastal přechod času přes půlnoc.

Chceme-li navzájem přepočítávat údaj systémového čítače času (který čítá impulsy generátoru hodin) a DOS údaj času (který budeme počítat s přesností na setiny sekundy), stanovíme si přepočtový koeficient z poměru počtu impulsů za 1 sekundu. Počet impulsů hodinového generátoru je 1193180/65536. Počet impulsů setin sekundy je 100. Poměr frekvencí tedy je 100*(1193180/65536) = 100*65536/1193180. Po vykrácení zlomku nám vznikne převod 5*65536/59659, který obsahuje položky dobře použitelné v 16-bitové matematice.

Data zařízení hodin mají speciální formát popsaný strukturou CLOCKData. Na začátku struktury je slovo, představující počet dnů od 1.1.1980. Následuje bajt minuty aktuálního času, bajt hodiny, bajt setin sekund a nakonec bajt sekund.

Čtení času a data z hodinového zařízení obstarává funkce CLKRData. Nejdříve načte aktuální čas systémového čítače hodin funkcí GetTimer. Funkce současně zajistí inkrementaci data v proměnné Date (tj. počet dnů od 1.1.1980), pokud nastal přechod přes půlnoc. Aktuální datum je uloženo do datového bufferu beze změny. Čas je přepočítán ze tvaru čítače na rozložené položky funkcí Clock2Time.

Vstupem funkce Clock2Time je 32-bitová hodnota systémového čítače času (tj. počet impulsů 18,2.. Hz od půlnoci). Nejdříve je nutné hodnotu čítače přepočítat na počet setin sekundy. K tomu použijeme převodu odvozeného dříve. Nejdříve údaj vynásobíme číslem 5. Poté ho vydělíme číslem 59659 za současného vynásobení číslem 65536, což vlastně znamená posunutí výsledku o 1 slovo "nahoru". Tím jsme získali aktuální čas vyjádřený v setinách sekundy. Dále rozdělíme časový údaj na jednotlivé položky. Vydělením číslem 100 oddělíme setiny sekundy, zůstane nám čas vyjádřený v sekundách. Vydělením času v sekundách číslem 60 osamostatníme sekundy, dalším vydělením číslem 60 oddělíme od sebe minuty a hodiny.


; ============================================================================
;
;                         LT-DOS - Clock device
;
; ============================================================================

; PC Real-time clock:
;	Hardware clock generator: 1.193180 MHz (correct value is 1.193182 MHz)
;	Timer divisor: 65536
;	Interrupt frequency: 18.20648193 Hz (correct value 18.20651245 Hz)
;	Interrupt interval: 54,92549322 ms (correct value 54,92540115 ms)
;	Ticks per day: 1'573'040, 1800B0h (correct value 1'573'043, 1800B3h)
;	Hundredths second per day: 8'640'000
;	Recalc counter to hundredth second: 100*65536/1193180 = 5*65536/59659

; ------------- Strukture of data block of CLOCK$ device

struc		CLOCKData

clock_date	resw	1		; 0: current date in days from 1/1/1980
clock_min	resb	1		; 2: minute (0 to 59)
clock_hour	resb	1		; 3: hour (0 to 23)
clock_hund	resb	1		; 4: hundredth second (0 to 99)
clock_sec	resb	1		; 5: second (0 to 59)

endstruc

CLOCKDATA_SIZE	EQU	CLOCKData_size	; 6 bytes

; ----------------------------------------------------------------------------
;                         Device service tables
; ----------------------------------------------------------------------------

; ------------- CLOCK$ service table

FNCCLK:		db	10		; number of functions
		dw	DevIntOK	; 0 init
		dw	DevIntOK	; 1 media check
		dw	DevIntOK	; 2 build BPB
		dw	DevIntInv	; 3 IOCTL input
		dw	CLKRData	; 4 read data
		dw	DevIntBusy	; 5 test read
		dw	DevIntOK	; 6 input status
		dw	DevIntOK	; 7 flush input
		dw	CLKWData	; 8 write data
		dw	CLKWData	; 9 write with verify

; ----------------------------------------------------------------------------
;               Device interrupt service - clock read data
; ----------------------------------------------------------------------------
; INPUT:	ES:DI = data buffer
;		DS = data segment
; OUTPUT:	AX = status word
; DESTROYS:	BX, CX, DX, DI
; ----------------------------------------------------------------------------

; ------------- Read system timer (-> CX:DX, max. 1573039)

CLKRData:	call	GetTimer	; read system timer

; ------------- Store current date in days from 1/1/1980

		cld			; direction up
		mov	ax,[Date]	; AX <- current date
		stosw			; store current date

; ------------- Recalculate system timer to time (-> AX, CX)

		call	Clock2Time	; recalc timer to time

; ------------- Store time

		stosw			; store minute AL and hour AH
		xchg	ax,cx		; AL <- hudredth, AH <- second
		stosw			; store hundredth AL and seconds AH

; ------------- Status OK

		mov	ah,S_DONE	; status OK
		ret

; ----------------------------------------------------------------------------
;               Device interrupt service - clock write data
; ----------------------------------------------------------------------------
; INPUT:	ES:DI = data buffer
;		DS = data segment
; OUTPUT:	AX = status word
; DESTROYS:	BX, CX, DX, SI
; ----------------------------------------------------------------------------

; ------------- Read system timer and flush date-overflow flag

CLKWData:	call	GetTimer	; read system timer

; ------------- Set current date from 1/1/1980

		cld			; direction up
		mov	si,di		; SI <- buffer
		es lodsw		; AX <- current date
		mov	[Date],ax	; store current date

; ------------- Set new time to RTC clock

		es lodsw		; AL <- minute, AH <- hour
		push	ax		; push AX
		mov	dx,[es:si]	; DL <- hundredth, DH <- second
		push	dx		; push DX
		call	BinBCDD		; pack to BCD
		xchg	ax,cx		; CL <- minute, CH <- hour
		mov	dl,0		; no daylight savings
		mov	ah,3		; AH <- function code
		int	1ah		; set current time
		pop	cx		; pop CX (CL=hundredth, CH=second)
		pop	bx		; pop BX (BL=minute, BH=hour)

; ------------- Recalculate time to system timer (-> CX:DX)

		call	Time2Clock	; recalc time to system timer

; ------------- Set new time to system timer

		mov	ah,1		; AH <- function code
		int	1ah		; set new time

; ------------- Recalc DOS date to date (-> CX, DX)

		call	DOS2Date	; recalc DOS date to date

; ------------- Set new date to RTC clock

		xchg	ax,cx		; AX <- date LOW
		call	BinBCDD		; pack to BCD
		xchg	ax,cx		; CX <- date LOW
		mov	ah,5		; AH <- function code
		int	1ah		; set new date

; ------------- Status OK

		mov	ah,S_DONE	; status OK
		ret

; ----------------------------------------------------------------------------
;                  Recalculate system timer to time
; ----------------------------------------------------------------------------
; INPUT:	CX:DX = timer
; OUTPUT:	CL = hundredth
;		CH = second
;		AL = minute
;		AH = hour
; DESTROYS:	BX, DX
; ----------------------------------------------------------------------------

; ------------- Calculate CX:DX * 5 (-> DX:AX, max. 7865195)

Clock2Time:	mov	bx,5		; BX <- 5
		xchg	ax,dx		; AX <- timer LOW
		xchg	ax,cx		; AX <- timer HIGH, CX <- timer LOW
		mul	bx		; AX <- timer HIGH*5
		xchg	ax,cx		; AX <- timer LOW, CX <- timer HIGH*5
		mul	bx		; DX:AX <- timer LOW*5
		add	dx,cx		; add timer HIGH

; ------------- Calculate DX:AX / 59659 * 65536 (-> CX:AX, max. 8639994)

		mov	bx,59659	; BX <- 59659
		div	bx		; AX <- quotient, DX <- remainder
		xchg	cx,ax		; CX <- hundredths HIGH
		xor	ax,ax		; AX <- 0, DX:AX = remainder * 65536
		div	bx		; AX <- hundredths LOW

; ------------- Calculate seconds (->DX:AX, max.86399)+hundredths (->CL, 0-99)

		xchg	ax,cx		; AX <- hundredths HIGH, CX <- LOW
		xor	dx,dx		; DX <- 0
		mov	bx,100		; BX <- divisor
		div	bx		; AX <- seconds HIGH, DX <- reaminder
		xchg	ax,cx		; AX <- hundredths LOW, CX <- sec. HIGH
		div	bx		; AX <- seconds LOW, DX <- hundredths
		xchg	dx,cx		; DX <- seconds HIGH, CL <- hundredths

; ------------- Calculate second (-> CH, 0 to 59)+minutes (-> AX, max. 1439)

		mov	bl,60		; BX <- divisor
		div	bx		; calculate minute and second
		mov	ch,dl		; CH <- second

; ------------- Calculate hour (0 to 23) and minute (0 to 59)

		div	bl		; AL <- hour, AH <- minute
		xchg	ah,al		; AL <- minute, AH <- hour
		ret

; ----------------------------------------------------------------------------
;                  Recalculate time to system timer
; ----------------------------------------------------------------------------
; INPUT:	BL = minute
;		BH = hour
;		CL = hundredth
;		CH = second
; OUTPUT:	CX:DX = timer
; DESTROYS:	AX, BX
; ----------------------------------------------------------------------------

; ------------- Merge second with hundredth (-> CX, max. 5999 hundredths)

Time2Clock:	mov	al,100		; AL <- 100 hundredths in second
		mul	ch		; AX <- second in hundredths
		mov	ch,0		; CX = hundredth
		add	cx,ax		; CX <- second*100 + hundredth

; ------------- Merge minute with hour (-> AX, max. 1439 minutes)

		mov	al,60		; AL <- 60 minutes in hour
		mul	bh		; AX <- recalc hour to minutes
		mov	bh,0		; BX = minute
		add	ax,bx		; AX = hour*60 + minute

; ------------- Merge all items (-> DX:CX, max. 8639999 hundredths)

		mov	dx,6000		; DX <- 6000 hundredths in minute
		mul	dx		; DX:AX <- hour & minute in hundredths
		add	cx,ax		; CX <- hundredths LOW
		adc	dx,byte 0	; DX <- hundredths HIGH

; ------------- Calculate DX:CX * 59659 / 65536 (-> DX:AX, max. 7865199)

		xchg	ax,dx		; AX <- hundredths HIGH
		xchg	ax,cx		; AX <- hundredths LOW, CX <- HIGH
		mov	bx,59659	; BX <- 59659
		mul	bx		; DX <- result LOW (e.g. / 65536)
		xchg	ax,dx		; AX <- result LOW
		xchg	ax,cx		; CX <- result LOW, AX <- hundr. HIGH
		mul	bx		; DX:AX <- result
		add	ax,cx		; AX <- result LOW
		adc	dx,byte 0	; DX <- result HIGH

; ------------- Calculate DX:AX / 5 (-> CX:DX, max. 1573039)

		mov	bx,5		; BX <- divisor
		xchg	ax,dx		; AX <- HIGH (= max. 24)
		div	bl		; AL <- result HIGH
		xchg	ax,cx		; CL <- result HIGH
		mov	al,ch		; AL <- remainder
		mov	ch,0		; CX = result HIGH
		cbw			; AX = remainder
		xchg	ax,dx		; AX <- LOW, DX <- HIGH
		div	bx		; calculate result LOW
		xchg	ax,dx		; DX <- result LOW		
		ret

; ----------------------------------------------------------------------------
;                           Pack 4 bytes to BCD
; ----------------------------------------------------------------------------
; INPUT:	DH,DL,AH,AL = binary numbers to pack
; OUTPUT:	DH,DL,AH,AL = BCD numbers
; ----------------------------------------------------------------------------

; ------------- Pack DX

BinBCDD:	xchg	ax,dx		; prepare words
		call	BinBCDW		; pack word in AX to BCD
		xchg	ax,dx		; return words

; ------------- Pack AH

BinBCDW:	xchg	al,ah		; prepare bytes
		call	BinBCDB		; pak byte in AL to BCD
		xchg	al,ah		; return bytes

; ------------- Push registers

BinBCDB:	push	cx		; push CX
		mov	cx,ax		; CH <- push registers AH

; ------------- Pack number in AL

		aam			; unpack digits
		mov	cl,4		; CL <- number of rotations
		shl	ah,cl		; shift AH 4 bits left
		or	al,ah		; AL <- BCD number

; ------------- Pop registers

		mov	ah,ch		; pop register AH
		pop	cx		; pop CX
		ret

; ----------------------------------------------------------------------------
;                             Unpack 4 BCD bytes
; ----------------------------------------------------------------------------
; INPUT:	DH,DL,AH,AL = BCD numbers to unpack
; OUTPUT:	DH,DL,AH,AL = binary numbers
; ----------------------------------------------------------------------------

; ------------- Unpack DX

BCDBinD:	xchg	ax,dx		; prepare words
		call	BCDBinW		; unpack BCD word in AX
		xchg	ax,dx		; return words

; ------------- Unpack AH

BCDBinW:	xchg	al,ah		; prepare bytes
		call	BCDBinB		; unpack BCD byte in AL
		xchg	al,ah		; return bytes

; ------------- Push registers

BCDBinB:	push	cx		; push CX
		xchg	ax,cx		; CH <- push register AH

; ------------- Unpack number in AL

		mov	cl,4		; CL <- number of rotations
		mov	ah,al		; AH <- number to unpack
		shr	ah,cl		; AH = BCD digit HIGH
		and	al,0fh		; AL = BCD digit LOW
		aad			; pack digits to binary

; ------------- Pop registers

		mov	ah,ch		; pop register AH
		pop	cx		; pop CX
		ret

; ----------------------------------------------------------------------------
;                        Recalc DOS date to date
; ----------------------------------------------------------------------------
; INPUT:	AX = DOS date (days from 1/1/1980)
;		DS = data segment
; OUTPUT:	CH = century (19 or 20)
;		CL = year (0 to 99)
;		DH = month (1 to 12)
;		DL = day (1 to 31)
; DESTROYS:	AX, BX, SI
; ----------------------------------------------------------------------------

; ------------- Add whole quadrenniums

DOS2Date:	mov	bx,3*365+366	; BX <- days in one quadrennium
		xor	dx,dx		; DX <- 0
		div	bx		; AX <- calc quadrennium, DX <- rest
		shl	ax,1		; quadrennium * 2
		shl	ax,1		; quadrennium * 4
		add	ax,19*256+80	; AX <- add year 1980
		xchg	ax,cx		; CX <- year
		xchg	ax,dx		; AX <- days in last quadrennium

; ------------- Add years in last quadrennium (first year is leap)

		mov	bx,366		; BX <- days in leap year
DOS2Date2:	cmp	ax,bx		; is it whole year ?
		jb	DOS2Date4	; it is not whole year
		inc	cx		; add one year to CL
		sub	ax,bx		; subtract one year from days
		mov	bx,365		; BX <- days in non-leap year
		jmp	short DOS2Date2	; next year

; ------------- Correct years over 2000

DOS2Date4:	cmp	cl,99		; overflow year in century?
		jbe	DOS2Date5	; year is OK
		sub	cl,100		; CL <- correct year
		mov	ch,20		; CH <- 20th century

; ------------- Prepare days in February

DOS2Date5:	call	PrepareFeb	; prepare days in February

; ------------- Add days in months

		cld			; direction up
		xchg	ax,bx		; BX <- days
		mov	si,DayMonthTab	; days in months
		xor	dx,dx		; DH <- 0 month counter
DOS2Date6:	inc	dh		; DH <- add next month
		lodsb			; AL <- load next days in month
		cbw			; AX <- days in month
		sub	bx,ax		; decrease number of days
		jnc	DOS2Date6	; next month
		add	bx,ax		; return last days

; ------------- Days in last month

		inc	bx		; day correction
		mov	dl,bl		; DL <- day
		ret

; ----------------------------------------------------------------------------
;                        Recalc date to DOS date
; ----------------------------------------------------------------------------
; INPUT:	CH = century (19 or 20)
;		CL = year (0 to 99)
;		DH = month (1 to 12)
;		DL = day (1 to 31)
;		DS = data segment
; OUTPUT:	AX = DOS date (days from 1/1/1980)
; DESTROYS:     BX, CX, DX, SI
; ----------------------------------------------------------------------------

; ------------- Prepare days in February

Date2DOS:	call	PrepareFeb	; prepare days in February

; ------------- Prepare year from 1980 (-> AX, value 0 to 119)

		xchg	ax,cx		; AX <- year
		cmp	ah,20		; is it century 20xx?
		jne	Date2DOS2	; it is not century 20xx		
		add	al,100		; add 100 years
Date2DOS2:	sub	al,80		; offset from 1980
		cbw			; AX = years from 1980
		
; ------------- Calculate quadrennium (-> AX) and year in quadrennium (-> CL)

		mov	bl,4		; divisor
		div	bl		; AH <- quadrennium, AL <- year
		mov	cl,ah		; CL <- year in quadrennium
		cbw			; AX <- quadrennium

; ------------- Days in all quadrenniums (-> CX)

		mov	bx,dx		; BL <- day, BH <- month
		mov	dx,3*365+366	; DX <- days in one quadrennium
		mul	dx		; AX <- days in all quadrenniums
		xchg	ax,cx		; CX <- days, AL <- year

; ------------- Days in years in last quadrennium (-> CX)

		cbw			; AX <- years in last quadrennium
		or	ax,ax		; is it first year?
		jz	Date2DOS3	; it is first year
		mov	dx,365		; DX <- days in one year
		mul	dx		; AX <- days in last years
		inc	ax		; correction for leap year
Date2DOS3:	add	cx,ax		; CX <- days in years

; ------------- Add days in last month

		mov	al,bl		; AL <- day
		cbw			; AX <- day
		dec	ax		; date correction
		add	ax,cx		; AX <- days in year and day
		xchg	ax,bx		; BX <- days, AH <- month

; ------------- Add days in months

		mov	al,ah		; AL <- month
		cbw			; AX <- month
		xchg	ax,cx		; CX <- month
		dec	cx		; correction
		jz	Date2DOS5	; it is first month
		cld			; direction up
		mov	si,DayMonthTab	; SI <- days in months
Date2DOS4:	lodsb			; AL <- load next days in month
		cbw			; AX <- days in month
		add	bx,ax		; increase number of days
		loop	Date2DOS4	; next month
Date2DOS5:	xchg	ax,bx		; AX <- days from 1/1/1980
		ret

; ----------------------------------------------------------------------------
;                         Read system timer
; ----------------------------------------------------------------------------
; OUTPUT:	CX:DX = system timer (number of 1/18 sec ticks)
; NOTES:	It shifts "current date" when timer overflows 24 hours.
; ----------------------------------------------------------------------------

GetTimer:	push	ax		; push AX
		mov	ah,0		; AH <- function code
		int	1ah		; read system timer
		mov	ah,0		; AX = flag 24-hours overflow
		add	[cs:Date],ax	; shift current date
		pop	ax		; pop AX
		ret

; ----------------------------------------------------------------------------
;                       Prepare days in February
; ----------------------------------------------------------------------------
; INPUT:	CL = year (may be relative from 1980 or 1900)
; ----------------------------------------------------------------------------

PrepareFeb:	push	ax		; push AX
		mov	al,28		; AL <- 28 days in non-leap year
		test	cl,3		; is it leap year?
		jnz	PrepareFeb2	; it is not leap year
		inc	ax		; AL <- 29 days in leap year
PrepareFeb2:	mov	[cs:DayMonthFeb],al; store days in February
		pop	ax		; pop AX
		ret

; ----------------------------------------------------------------------------
;                                    Data
; ----------------------------------------------------------------------------

Date:		dw	0		; current date (days from 1/1/1980)

; ------------- Days in months

DayMonthTab:	db	31		; January
DayMonthFeb:	db	28		; February
		db	31		; March
		db	30		; April
		db	31		; May
		db	30		; June
		db	31		; July
		db	31		; August
		db	30		; September
		db	31		; October
		db	30		; November
		db	31		; December

Obrácený význam má funkce CLKWData. Opět voláme funkci GetTimer, ale jen z toho důvodu, abychom resetovali případný příznak přetečení času přes půlnoc. Z datového bufferu načteme aktuální datum vyjádřené v počtu dní od 1.1.1980 a uložíme ho do proměnné Date. Načteme rozložené položky času a nastavíme hodiny CMOS. Zápis do hodin CMOS se musí provádět v BCD kódu, proto jsou položky převáděny do BCD kódu funkcí BinBCDD. Následuje převod údaje času na formát systémového čítače času pomocí funkce Time2Clock.

Ve funkci Time2Clock postupujeme přesně obráceně oproti funkci Clock2Time. Nejdříve jsou všechny položky spojeny do jednoho údaje, představujícícho čas vyjádřený v setinách sekundy (po částech sekundy+setiny, minuty+hodiny, nakonec výsledky dohromady). Výsledný údaj je nejdříve vynásoben číslem 59659 za současného vydělení 65536 (tj. posun o 1 slovo "dolů") a poté je vydělen číslem 5.

Po nastavení systémového čítače času je DOS datum (tj. počet dnů od 1.1.1980) přepočten na jednotlivé položky data, aby mohl být nastaven datum do CMOS hodin. Převod je prováděn ve funkci DOS2Date. Použitelné rozpětí let je zhruba 100 let od roku 1980. V tomto období je cyklus přechodných roků platný i pro století, takže nemusíme na ně brát ohled. Počáteční rok, 1980, je přechodným rokem. Při výpočtu nejdříve zjistíme počet čtyřletí a odečteme je od celkového počtu dnů. Odečteme postupně počty dnů v letech posledního (neúplného) čtyřletí a zjistíme tak výsledný rok. Po korekci údaje roku (přetečení přes 99) si připravíme v tabulce počet dnů v únoru pro počítaný rok pomocí funkce PrepareFeb. Dále procházíme postupně tabulku počtu dnů v jednotlivých měsících roku a odečítáme je, čímž zjistíme číslo měsíce. Zbytek nám udává den v měsíci.

Po výpočtu data v rozloženém stavu převedeme údaje na BCD kód a nastavíme datum v CMOS hodinách.

Poslední funkcí je Date2DOS, která obráceně přepočítává rozložené datum na sloučený DOS tvar (tj. na počet dnů od 1.1.1980). Tato funkce není používána ovladačem, ale je volána při inicializaci systému k načtení data z CMOD hodin. V úvodu funkce připravíme počet dnů v měsíci únoru pomocí funkce PrepareFeb. Testuje pouze dělitelnost roku číslem 4, takže jí můžeme předávat jak absolutní hodnotu roku (postačí nižší bajt) tak i offset od roku 1980. Po přípravě měsíce února převedeme rok vyjádřený pomocí století a roku ve století na relativní rok vzhledem k roku 1980. Rok rozložíme na počet čtyřletí a roky v posledním čtyřletí. Vypočteme počet dnů připadajících na všechna celá čtyřletí (vynásobením číslem 3*365+366). K nim přičteme dny připadající na roky v posledním čtyřletí před počítaným rokem - takže pro první rok ve čtyřletí to nebude nic, pro další 366+(N-1)*365. K údaji přičteme počet dnů v aktuálním měsíci, tj. datum-1. Jako poslední přičteme dny v měsících předcházejících počítanému měsíci.

Zpět na stránku systému LT-DOS