25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

24KB

Developing Your Own 32-Bit Operating System — Chapter-20: Timer Management Code

  • Source: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
  • PDF pages: 427–440
  • Pages without text: 427

Timer Management Code

/Q/r Part PV ■ The Operating System Source Code The timer code is the heart of all timing functions for the operating system. This file contains all of the code that uses or deals with timing functions in MMURTL. The timer interrupt service routine (ISR) is also documented here, along with a piece of the kernel, actually the scheduler. It's at the end of the timer ISR. Choosing a Standard Interval Most operating systems have a timer interrupt function that is called at a fixed interval to up- date a continuously running counter. MMURTL is no exception. I experimented with several different intervals. I began with 50ms, but determined I couldn't get the resolution required for timing certain events, and it also wasn't fast enough to provide smooth, preemptive multitasking. I ended up with 10ms. Back when I started this project, 20-MHz machines were “screamers,” and I was even worried about consuming bandwidth on machines that were slower than that. As you well know, with 100-MHz machines around, and anything less than 20-MHz all but disappearing, my worries were unfounded. I did some time-consuming calculations to figure what percentage of CPU bandwidth I was using with my timer ISR. I added up all of the clock cycles in the ISR for a maximum time- consuming interrupt, as well as the fastest one possible. Somewhere between these two was the average. The average really depends on program loading, because the timer interrupt sends messages for the Sleep ( ) and Alarm ( ) functions. I was consuming less than 0.2 percent of available bandwidth. Even with these calculations, however, testing is the only real way to be sure it will function properly. Your bandwidth is the percentage of clocks executed for a given period, divided by the total clocks for the same period. In a 10-ms period, we have 200,000 clocks on a 20-MHz machine. My calculated worst case was approximately 1 50us, or 3,000 clocks, which works out to about 1 .6 percent for the worst case. The average was far less. It was 0.2 percent. Even more important than total bandwidth was the effect on interrupt latency. The most time- critical interrupts are nonbuffered serial communications, especially synchronous. A single communications channel running at 19,200 bps will interrupt every 520us. Two of them will come in at 260us. The following are the instructions in the timer interrupt service routine that could be “looped” on for the maximum number of times in the worst case. The numbers before each instruction are the count of clocks to execute the instruction on a 20-MHz-386, a slow machine. This doesn't take into account nonaligned memory operands, certain problems with memory ac- cess such as noncached un-decoded instructions, memory wait-states, and so on, so I simply added 10 percent for the overhead, which is very conservative. See Listing 20.1. Listing 20.1. Loop in timer ISR. IntTmr01 : 6 CMP DWORD PTR [ EAX+f InUse ] , FALSE 3 JE IntTmr03

Chapter 20 ■ Timer Management Code H 6 CMP DWORD PTR [EAX+CountDown) ,0h 3 JNE IntTmr02 2 PUSH EAX 2 PUSH ECX 5 PUSH DWORD PTR [EAX+TmrRespExch] 2 MOV EAX, -1 2 PUSH EAX 2 PUSH EAX 40 CALL FWORD PTR ISendMsg 2 POP ECX 2 POP EAX 6 MOV DWORD PTR [EAX+flnUse] , FALSE 5 DEC nTmrBlksUsed 7 JMP IntTmr03 IntTmr02: 6 DEC DWORD PTR [EAX+CountDown] IntTmr03: 2 ADD EAX.sTmrBlk 11 LOOP IntTmr01 This works out to 125 clocks for each loop, including the 10 percent overhead, plus 130 clocks for the execution of ISendMsg ( ) in each loop. Multiply the total (255) times the maximum number of timer blocks (64), and it totals 16,320 clocks. That's a bunch! On a 20-MHz ma- chine, that would be 860us just for this worst-case interrupt. But hold on a minute! That only happens if all of the tasks on the system want an alarm or must wake up from sleep exactly ax. the same time. The odds of this happening are truly astronomical. The real worst case would probably be more like 150us, and I tried a hundred calculations with a spreadsheet and finally with test programs. This I could live with. The only additional time consumer is to tack on a task switch at the end if you must preempt someone. A task switch made by jumping to a TSS takes approximately 400 clocks on a 386, and 250 on a 486 or a Pentium. Four hundred clocks on a 20-MHz CPU is 50 nanoseconds times 400, or 20us. This doesn't cause a problem at all. Timer Data The following section is the data and constants defined for the timer interrupt and associated functions. SwitchTick and df Halted are flags for the task-scheduling portion of the timer in- terrupt. See Listing 20.2. Listing 20.2. Data and constants for timer code. .DATA .INCLUDE M0SEDF.INC .INCLUDE TSS. INC .ALIGN DWORD ; TIMER INTERRUPT COUNTER and TimerBlocks

408 I The Operating System Source Code Listing 20.2. continued The Timer Block is a structure that contains the exchange number of a task that is sleeping, or one that has requested an alarm. Each TimerBlock is 12 Bytes long These are the offsets into the structure sTmrBlk EQU 12 flnUse EQU 0 ;DD 00000000h TmrRespExch EQU 4 ;DD 00000000h CountDown EQU 8 ;DD 00000000h EXTRN SwitChTick DD EXTRN dfHalted DD PUBLIC TimerTick DD 0 ; Incremented every 10ms (0 on bootup) PUBLIC nTmrBlksUsed DD 0 ;Number of timer blocks in use PUBLIC rgTmrBlks DB (sTmrBlk * nTmrBlks) DUP (0) Timer Code The timer interrupt and timing related function are all defined in the file TmrCode.ASM. The three external near calls, the first things defined in this code segment, allow the timer interrupt to access kernel helper functions from Kernel. ASM. The timer interrupt is part of the schedul- ing mechanism. See Listing 20.3. Listing 20.3. External functions for timer code. .CODE EXTRN ChkRdyQ NEAR EXTRN enQueueRdy NEAR EXTRN deQueueRdy NEAR The Timer Interrupt The timer interrupt checks the timer blocks for values to decrement. The timer interrupt fires off every 1 0 milliseconds. The Sleep ( ) and Alarm ( ) functions set these blocks as callers require. The timer interrupt code also performs the important function of keeping tabs on CPU hogs. It's really the only part of task scheduling that isn't cooperative. It is a small, yet very important part. At all times on the system, only one task is actually executing. You have now interrupted that task. Other tasks may be waiting to run at the ready queue. They may have been placed there

Chapter 20 ■ Timer Management Code |
409 | by other ISRs, and they may be of an equal, or higher, priority than the task that is now run- ning, which is the one you interrupted. You check to see if the same task has been running for 30ms or more. If so, you call ChkRdyQ and check the priority of that task. If it is the same or higher, you switch to it. The task you interrupted is placed back on the ready queue in exactly the state you interrupted it. You couldn't reschedule tasks this way if you were using interrupt tasks because this would nest hardware task switches. It would require manipulating items in the TSS to make it look as if they weren't nested. Keep this in mind if you use interrupt tasks instead of interrupt procedures as I do. See Listing 20.4. Listing 20.4. The timer interrupt service routine. PUBLIC IntTimer: PUSHAD INC TimerTick CMP nTmrBlksUsed, 0 JE SHORT TaskCheck IntTmr00: LEA EAX.rgTmrBlks MOV ECX.nTmrBlks CLD ;INTS are disabled automatically ; Timer Tick, INT 20 ;Anyone sleeping or have an alarm set’ ;No... ;Yes! ;EAX has addr of Timer Blocks ;ECX has count of Timer Blocks ;Move forward thru the blocks IntTmr01 CMP DWORD PTR [EAX+f InUse] .FALSE JE IntTmr03 ;No CMP DWORD PTR [EAX+CountDown] ,0h ;No ;Timer Block found? goto next block ;Yes - is count at zero? goto decrement it ;save ptr to rgTmpBlk ;save current count [EAX+TmrRespExch] ;Yes - ISend Message ;FFFFFFFFh ; bogus msg ; bogus msg ;tell him his times up! ;get count back ;get ptr back ;Free up the timer block ;Correct count of used blocks ;skip decrement - empty blk JNE IntTmr02 PUSH EAX PUSH ECX PUSH DWORD PTR MOV EAX, -1 PUSH EAX PUSH EAX CALL FWORD PTR POP ECX POP EAX MOV DWORD PTR [EAX+f InUse] .FALSE DEC nTmrBlksUsed JMP IntTmr03 ISendMsg IntTmr02: DEC DWORD PTR [EAX+CountDown] ;10ms more gone. IntTmr03: ADD EAX.sTmrBlk LOOP IntTmr01 ;next block please! ;unless were done ;We will now check to see if this guy has been running ;for more then 30ms (3 ticks). If so, we will ; switch him out if someone with an equal or higher pri ;is on the RdyQ

I
410 Part IV ■ The Operating System Source Code Listing 20.4. continued TaskCheck: MOV EAX, df Halted OR EAX, EAX JNZ TaskCheckDone ;Is this from a HALTed state? ;YES.. no need to check tasks MOV EAX, TimerTick MOV EBX, SwitchTick SUB EAX, EBX CMP EAX, 3 JL SHORT TaskCheckDone CALL ChkRdyQ OR EAX, EAX JZ TaskCheckDone MOV ESI, pRunTSS MOV DL, [ESI+Priority] CMP DL, [EAX+Priority] JC TaskCheckDone CALL deQueueRdy MOV EDI, EAX MOV EAX, ESI CALL enQueueRdy MOV pRunTSS.EDI MOV BX,[EDI+Tid] MOV TSS_Sel,BX INC _nSwitches INC _nSlices MOV EAX, TimerTick MOV SwitchTick, EAX PUSH 0 CALL FWORD PTR _EndOfIRQ JMP FWORD PTR [TSS] POPAD IRETD ;Change this to change the “time slice” ;Hasn't been 30ms yet for this guy! ;Get next highest Pri in EAX (Leave Queued) ;Is there one at all?? ;No. .. ;DL is pri of current ;Compare current pri to highest queued ;The CMP subtracts Pri of Queued from ;current pri. If the Queued Pri is LOWER ;or Equal, we want to Switch. That means ;if we got a CARRY on the subtract, his ; number was higher and we DON'T ;If current is higher(lower num), keep going! Get high priority TSS off the RdyQ and save in EDI Put current one in EAX and on the RdyQ Make the TSS in EDI the Running TSS Get the task ID Put it in the JumpAddr for Task Swtich Keep track of how many switches for stats Keep track of how many preemptive switches Save time of this switch for scheduler ;Must do this before we switch! JMP TSS (This is the task swtich) TaskCheckDone: PUSH 0 CALL FWORD PTR POPAD IRETD EndOflRQ ;Must do this before we switch! SleepO The sleep routine delays the calling task by setting up a timer block with a countdown value and by setting up an exchange to send a message to when the countdown readies zero. The

Chapter 20 ■ Timer Management Code timer interrupt sends the message and clears the block when it reaches zero. This requires an exchange. The exchange used is the TSS_Exch in the TSS for the current task. See Listing 20.5. Listing 20.5. Code for the Sleep function. DelayCnt EQU [EBP+0Ch] PUBLIC DelayOl Delay02: Delay03: _Sleep: PUSH EBP MOV EBP, ESP MOV EAX, DelayCnt CMP EAX, 0 JE Delay03 LEA EAX.rgTmrBlks MOV ECX.nTmrBlks CLD CLI CMP DWORD PTR [EAX+flnUse] JNE Delay02 MOV EBX, DelayCnt MOV [EAX+CountDownJ.EBX MOV DWORD PTR [EAX+flnUse] INC nTmrBlksUsed MOV ECX.pRunTSS MOV EBX, [ECX+TSS_Exch] MOV [EAX+TmrRespExch],EBX STI PUSH EBX ADD ECX,TSS_Msg PUSH ECX CALL FWORD PTR _WaitMsg MOV EAX.ErcOk JMP Delay03 STI ADD EAX.sTmrBlk LOOP Delay01 MOV EAX.ErcNoMoreTBs MOV ESP, EBP POP EBP RETF 4 ;See if there's no delay ;EAX points to timer blocks ;Count of timer blocks ; clear direction flag ;can't let others interfere , FALSE ; Empty block? ;No - goto next block ;Get delay count JRUE ;Use the Timer Block ;Up the blocksInUse count ;Get TSS_Exch for our use ;put it in timer block! ;Pass exchange (for WaitMsg) ;Offset of msg area ;and Wait for it to come back ;all is well ; unless were done ;Sorry, out of timer blocks FAR return from publics AkrmQ Alarm ( ) sets up a timer block with a fixed message that will be sent when the countdown reaches zero. The message is not repeatable. It must be set up each time. The message will always be two dwords with OFFFFFFFFh (-1) in each. See Listing 20.6.

H PartIV The Operating System Source Code Listing 20.6. Code for the Alarm function. Procedural Interface: Alarm ( nAlarmExch , AlarmCnt ) : dErc AlarmExch AlarmCnt EQU [EBP+10h] EQU [EBP+0Ch] PUBLIC Alarm01 _Alarm: PUSH EBP MOV EBP, ESP MOV EAX, AlarmCnt CMP EAX, 0 JE Alarm03 LEA EAX.rgTmrBlks MOV ECX.nTmrBlks CLD CLI CMP DWORD PTR [EAX+flnUse] JNE Alarm02 MOV EBX, AlarmCnt MOV [EAX+CountDown],EBX MOV DWORD PTR [EAX+flnUse] INC nTmrBlksUsed MOV EBX, AlarmExch MOV [ EAX+TmrRespExch ) , EBX STI MOV EAX.ErcOk JMP Alarm03 ;See if there's no delay ;EAX points to timer blocks ;Count of timer blocks ;clear direction flag jcan't let others interfere FALSE ; Empty block? ;No - goto next block ;Get delay count TRUE ;Use the Timer Block ;Up the blocksInUse count ;put it in timer block! ;It's OK to interrupt now ;all is well Alarm02: STI ADD EAX.sTmrBlk LOOP Alarm01 MOV EAX.ErcNoMoreTBs Alarm03: MOV ESP, EBP POP EBP RETF 8 ;It's OK to interrupt now ; unless were done ;Sorry, out of timer blocks KillAkrmQ KillAlarm searches the timer blocks looking for any block that is destined for the specified Alarm exchange. All alarms that are set to fire off to that exchange are killed. If the alarm is already queued through the kernel, which means the message has already been sent, nothing will stop it. See Listing 20.7.

Chapter 20 Timer Management Code H Listing 20.7. Code for the KillAlarm function. ; Procedural Interface: ; KillAlarm ( nAlarmExch ) :dErc KAlarmExch EQU [EBP+0Ch] PUBLIC _KillAlarm: PUSH EBP MOV EBP, ESP CMP nTmrBlksUsed, 0 JE KAlarm03 MOV EBX, KAlarmExch LEA EAX.rgTmrBlks MOV ECX.nTmrBlks CLD No blocks in use so we get out! Get exchange for killing alarms to EAX points to timer blocks Count of timer blocks clear direction flag KAlarm01 CLI ;can't let others interfere CMP DWORD PTR [EAX+f InUse] ,TRUE ;Block in use? JNE KAlarm02 ;No - goto next block CMP [EAX+TmrRespExch] ,EBX ;Does this match the Exchange? JNE KAlarm02 MOV DWORD PTR [EAX+f InUse] , FALSE ;Make Empty DEC nTmrBlksUsed ;Make blocksInUse correct ST I ADD EAX.sTmrBlk LOOP KAlarm01 It's OK to interrupt now unless were done KAlarm03: XOR EAX, EAX MOV ESP, EBP POP EBP RETF 4 AL1 done ErcOk MicroDekyO MicroDelay ( ) provides small-value timing delays for applications and device drivers. The count is in 15us increments. The timing for this delay is based on the toggle of the refresh bit from the system status port. The refresh bit is based on the system's quartz crystal oscillator, which drives the processor clock. The task is actually not suspended at all. This forms an instruction loop checking the toggled value of an I/O port. This call will not be very accurate for values less than 3 or 4 (45 to 60 microseconds). But it's still very much needed. The call can also be inaccurate because of interrupts that are not dis- abled. See Listing 20.8.

I
414 Part PV ■ The Operating System Source Code Listing 20.8. Code for the MicroDelay function. Procedural Interface ; MicroDelay (dDelay) :derror PUBLIC _MicroDelay: PUSH EBP MOV EBP, ESP MOV ECX, [EBP+0Ch] CMP ECX, 0 JE MDL01 Get delay count get out if they came in with 0! MDL00: IN AL, 61h AND AL, 10h CMP AH, AL JE MDL00 MOV AH, AL LOOP MDL00 ;Get system status port ; check refrest bit ; Check toggle of bit ;No toggle yet ; Toggle! Move to AH for next compare MDL01 XOR EAX EAX MOV ESP EBP ; POP EBP RETF 4 GetCMOSTimeQ This reads the time from the CMOS clock on the PC-ISA machines. MMURTL doesn't keep the time internally. The time is returned from the CMOS clock as a dword. The low order byte is the seconds in Binary Coded Decimal (BCD); the next byte is the minutes in BCD; the next byte is the Hours in BCD; and the high order byte is 0. See Listing 20.9. Listing 20.9. Code to read the CMOS time. ; Procedural Interface: ; GetCMOSTime ( pdTimeRet ) : derror pCMOSTimeRet EQU [EBP+12) _GetCMOSTime: PUSH EBP MOV EBP, ESP XOR EBX, EBX Clear time return MOV EAX, 04 h OUT 70h,AL IN AL,71h MOV BL, AL ; Hours SHL EBX, 8 MOV EAX,02h

Chapter 20 ■ Timer Management Code R OUT 70h,AL IN AL,71h MOV BL.AL SHL EBX, 8 ;Seconds MOV EAX,00h OUT 70h,AL IN AL,71h MOV BL.AL MOV ESI, pCMOSTimeRet ;Give ‘em the time MOV [ESI], EBX XOR EAX, EAX ; No Error MOV ESP, EBP ; POP EBP ; RETF 4 ; GetCMOSDateO The date is returned from the CMOS clock as a DWord. The low-order byte is the day of the week (BCD, 0-6 0=Sunday); the next byte is the day (BCD 1-31); the next byte is the month (BCD 1-12); and the high-order byte is year (BCD 0-99). See Listing 20.10. Listing 20.10. Code to read the CMOS date. Procedural Interface: GetCMOSDate(pdTimeRet) :derror pCMOSDateRet EQU [EBP+12] PUBLIC _GetCMOSDate: PUSH EBP ; MOV EBP, ESP ; XOR EBX, EBX ;Clear date return MOV EAX,09h ;Year OUT 70h,AL IN AL,71h MOV BL, AL SHL EBX, 8 ; MOV EAX,08h ; Month OUT 70h,AL IN AL,71h MOV BL, AL SHL EBX, 8 MOV EAX,07h ;Day of month OUT 70h,AL IN AL,71h MOV BL.AL SHL EBX, 8 ;

Part IV ■ The Operating System Source Code Listing 20.10. continued MOV EAX,06h OUT 70h,AL IN AL,71h MOV BL.AL MOV ESI, pCMOSDateRet MOV [ESI], EBX XOR EAX, EAX MOV ESP, EBP POP EBP RETF 4 ;Day of week ;Give “em the time ; No Error GetTimerTickQ This returns the ever-increasing timer tick to the caller. MMURTL maintains a double word counter that begins at zero and is incremented until the machine is reset or powered down. Because it is a dword, there are over 4 billion ticks before rollover occurs. With a 10ms inter- val, this amounts to 262,800,000 ticks per month. This means the system tick counter will rollover approximately every 16 months. See Listing 20.1 1. Listing 20.1 1 . Code to return the timer tick. The procedural interface: GetTimerTick(pdTickRet) :derror The Current Timer Tick is returned (it's a DWord] pTickRet EQU [EBP+12] PUBLIC _GetTimerTick: PUSH EBP MOV EBP, ESP MOV ESI, pTickRet MOV EAX, Timer-Tick MOV [ESI], EAX XOR EAX, EAX POP EBP RETF 4 Beep_Work() Beep_Work() is an internal function (a helper) used by the public calls Tone ( ) andBeep(). Hard- ware timer number 2, which is connected to the system internal speaker, is used to generate

Chapter 20 M Timer Management Code tones. The system clock drives the timer so the formula to find the proper frequency is a func- tion of the clock's frequency. The length of time the tone is on is controlled by the Sleep ( ) function, which is driven by the system tick counter. See Listing 20.12. Listing 20.12. Helper function for BeepQ and ToneQ.

;The clock freq to Timer 2 is 1.193182 Mhz ;To find the divisor of the clock, divide 1.193182Mhz by Desired Freq. ;This does all work for BEEP and TONE ;EBX needs the desired tone frequency in HERTZ ;ECX needs length of tone ON-TIME in 10ms increments BEEP_Work: MOV AL, 10110110b ; Timer 2, LSB, MSB, Binary OUT 43h, AL XOR EDX, EDX MOV EAX, 1193182 ;1.193182Mhz DIV EBX ;DIVIS0R is in EBX (Freq) OUT 42h, AL ;Send quotient (left in AX) MOV AL, AH NOP NOP NOP NOP OUT 42h, AL IN AL, 61h OR AL, 00000011b PUSH EAX POP EAX OUT 61 h, AL PUSH ECX ; CALL FWORD PTR _Sleep ;ECX is TIME ON in 50ms incs. IN AL, 61 h NOP NOP NOP NOP AND AL, 11111100b OUT 61 h, AL RETN BeepQ This function has nothing to with the Road Runner; if it had, I would have called it Beep_Beep ( ) . (Sorry, I couldn't resist.) This provides a fixed tone from the system's internal speaker as a public call for applications to make noise at the user. It uses the helper function Beep_Work described earlier. See Listing 20.13.

418 Part W ■ The Operating System Source Code Listing 20.1 3. Code to produce a beep. Procedural Interface: Beep( ) PUBLIC _Beep: PUSH EBP MOV EBP, ESP MOV EBX, 800 MOV ECX, 35 CALL Beep_Work POP EBP RETF Freq 350ms ToneQ Tone allows the caller to specify a frequency and duration of a tone to be generated by the system's internal speaker. This call uses the helper function BeepWork described earlier. See Listing 20. 14. Listing 20.14. Code to produce a tone. Procedural Interface: Tone(dFreq, dTickseRet) :derror dFreq is a DWord with the FREQUENCY in HERTZ dTicks is a DWord with the duration of the tone in 10ms increments ToneFreq EQU [EBP+10h] ToneTime EQU [EBP+0Ch] PUBLIC Tone: PUSH EBF MOV EBP, ESP MOV EBX, ToneFreq MOV ECX, ToneTime CALL Beep Work POP EBP RETF 8

Powered by TurnKey Linux.