File: PCTIM003.TXT Description: FAQ / Application notes: Timing on the PC family under DOS Author: Kris Heidenstrom (kheidens@actrix.gen.nz) Version: 19951220, Release 3 -------------------------------------------------------------------------------- ## 1 INTRODUCTION AND DOCUMENT INFORMATION ## 1.1 DOCUMENT OVERVIEW This article describes techniques for timing on the IBM PC family under MS-DOS, and many related subjects. Sample functions and programs are included. After the brief overview, the features of each technique are listed, so you can find the most appropriate one for your needs. Subjects covered in this document include: þ The DOS and BIOS date/time and alarm functions þ The BIOS tick count variable þ Trapping and handling critical errors þ Using interrupt 1C hex and interrupt 8 þ The counter/timer's internal operation þ Reprogramming the timer operating mode þ Measuring short time intervals (three techniques) þ Reading the timer count in progress þ Generating an absolute timestamp þ Reprogramming the timer tick rate þ Simulating a vertical retrace interrupt for triple buffering þ Using the serial and parallel port interrupts þ Reading the joystick position (three methods) þ Generating tones and sound. In addition to these timing techniques, this document covers the PC's timing hardware, and covers interrupts and interrupt considerations in some detail. Also included in this package is an archive containing executable versions of the sample programs, and an archive containing six illustrations in GIF format. ## 1.1.1 AUDIENCE This document is not aimed at programmers who wear suits and write database query programs in Cobol. It is aimed at the 'tinkerer' programmer or low-level programmer, who wants complete control of the computer, wants to work closely with the hardware, and who is familiar with, and interested in, real time concepts. Previous programming experience in C and assembly language, and familiarity with DOS and BIOS design, would be an advantage. ## 1.2 CONTENTS 1 INTRODUCTION AND DOCUMENT INFORMATION 1.1 Document Overview 1.1.1 Audience 1.2 Contents 1.3 Author and Distribution 1.4 Disclaimer and Legal stuff 1.5 Document Conventions 1.6 Sample Code Conventions 1.7 Acknowledgements 1.8 Quoter Program 1.9 Revision notes 1.10 Glossary 2 OVERVIEW OF TIMING TECHNIQUES 2.1 The Big Picture 2.2 Which Technique? 2.3 Comparison of Techniques 2.4 Other Subjects Covered in this Document 3 DOS AND BIOS TIME-OF-DAY AND ALARM FUNCTIONS 3.1 Reading the Date and Time from DOS 3.2 Reading the Date and Time from the BIOS 3.3 Sample Program: DOS Device Driver for the AT Clock 3.4 Other BIOS Time and Alarm Functions 3.5 Other Other BIOS Time Functions 3.6 The Times They Are A-Changin' 4 USING THE BIOS TICK COUNT VARIABLE 4.1 The BIOS Tick Count Variable 4.2 Change of Day 4.3 Reading and Setting the Tick Count 4.4 Special Requirements - None 4.5 Sample Program: Reading the Tick Count 4.6 Sample Code: Optimised Function to Read the Tick Count 4.7 Sample Program: Using the Tick Count for Timeout Checking 4.8 Simple Delays using the BIOS Tick Count 5 SPECIAL SOFTWARE PRECAUTIONS 5.1 The Ctrl-C and Ctrl-Break Interrupts 5.2 Handling the Ctrl-C Interrupt 5.3 The Critical Error Interrupt 5.4 Critical Error Handler Parameters 5.5 Critical Error Handler Operation 5.6 The Divide Overflow Interrupt 5.7 Error Handling System 5.8 Sample Code Module: Critical Error Handler module 6 INTERRUPTS 6.1 The Timer Tick Interrupts 6.2 Interrupt Vector Table 6.3 Intercepting an Interrupt 6.4 Interrupt Hardware 6.5 IRQ to Interrupt Mapping 6.6 Interrupt Flag, Interrupt Acceptance, Interrupt Nesting 6.7 EMM386 Interrupt Interception 6.8 Avoiding EMM386 Overhead 6.9 Long Timer Tick Interrupt Handlers 6.9.1 Danger of Long Timer Tick Interrupt Handlers 6.10 Interrupt Mask Register 6.11 Enabling and Disabling the Timer Tick Interrupt 6.12 Reading the Interrupt Request Register 6.13 Reading the Interrupt In Service Register 6.14 When You Should Disable Interrupts 6.15 When You Shouldn't Disable Interrupts 6.16 Causes of Interrupt Delivery Jitter and Fast Tick Loss 6.16.1 Interrupt Delivery Jitter due to Real Interrupts 6.16.2 Interrupt Delivery Jitter due to Software Interrupts 6.16.3 Interrupt Delivery Jitter due to Hardware Accesses 6.16.4 Avoiding Interrupt Delivery Jitter 6.17 Detecting Interrupt Delivery Jitter and Missed Fast Tick Interrupts 6.18 Disabling Interrupts for Longer than One Timer Tick 6.19 Disabling Interrupts for Long Periods of Time 6.20 Overhead of an Interrupt 6.21 Effect of Background Interrupts 6.22 Safe Control of Interrupts 6.23 Timer Tick Interrupt Handler Guidelines 6.24 Accessing Hardware Devices in an Interrupt Handler 6.25 Calling DOS and BIOS in an Interrupt Handler 6.26 Calling C Library Functions in an Interrupt Handler 6.27 Re-entry of Interrupt Handlers 6.28 The 'End Of Interrupt' Signal 6.28.1 Level-Triggered Interrupt Reset 6.29 Enabling and Disabling Interrupts in an Interrupt Handler 6.30 Stack Usage and Stack Checking in an Interrupt Handler 6.31 Chaining to the Old Interrupt Handler 6.32 Writing Interrupt Handlers in Assembly Language 6.32.1 Assembly Language Interrupt Handlers: Accessing Variables 6.32.2 Assembly Language Interrupt Handlers: Starting Condition 6.32.3 Assembly Language Interrupt Handlers: Preserve the Registers 6.33 Using Interrupt Eight in a TSR 6.34 Using int 8 Without Chaining 6.35 Using int 1C hex instead of int 8 6.36 Sample Program: Using int 1Ch With Critical Error and Ctrl-C Handling 6.37 Debugging Interrupt Handlers 7 HARDWARE INFORMATION AND PROGRAMMING 7.1 The 14.31818 MHz Clock 7.2 Clock Frequency Accuracy 7.3 The Counter/Timer Chip (CTC) 7.4 CTC Channels 7.4.1 CTC Channel Zero 7.4.2 CTC Channel Zero Default Operating Mode 7.4.3 CTC Channel One 7.4.4 CTC Channel Two 7.5 Speaker Interface 7.6 CTC Internal Registers 7.7 Access Modes 7.8 CTC Operating Modes 7.8.1 Operating Modes: Behaviour Common to All Modes 7.8.2 Operating Mode Zero: Interrupt on Terminal Count 7.8.3 Operating Mode One: Hardware-Retriggerable One-Shot 7.8.4 Operating Mode Two: Rate Generator 7.8.5 Operating Mode Three: Square Wave Generator 7.8.6 Operating Mode Four: Software-Triggered Strobe 7.8.7 Operating Mode Five: Hardware-Triggered Strobe 7.9 The 8254/8253 Registers 7.9.1 The Mode/Command Register 7.9.2 The Data Ports 7.9.3 Accessing the Registers 7.9.4 I/O Recovery Delays 7.10 Programming the Mode and Reload Register 7.11 Effect of Reprogramming Channel Zero on the Timer Tick Interrupt 7.12 Sample Program: Programming the Mode and Reload Value 7.13 Reading the Reload Register 7.14 Reading the Counting Register 7.15 The Latch Command 7.15.1 Meaning of Count Value in Mode Two 7.15.2 Meaning of Count Value in Mode Three 7.16 Sample Code: Reading the Count in Mode Two 7.17 The Lobyte/Hibyte Flag 7.18 The Read-back Command 7.19 Sample Code: Read-back 7.20 Reading the Count in Mode Three (8254 only) 7.21 Sample Code: Reading the Count in Mode Three 7.22 Sample Code: Optimised Mode Three Count Reading Function 7.23 Sample Program: Manipulate the CTC and Port B 7.24 Hardware Problems and Differences 7.24.1 Differences Between the Intel 8253 and 8254 7.24.2 Chipset Implementations 7.24.3 Intel 8253/8254/82C54 Clock Synchronisation Problems 7.25 Is the CTC an 8253 or an 8254? 7.26 Determining the Exact State of the CTC 7.27 Sample Program: Report Channel States 7.28 CTC Access under OS/2 7.28.1 OS/2 VTIMER.SYS: CTC Channel Zero 7.28.2 OS/2 VTIMER.SYS: CTC Channel One 7.28.3 OS/2 VTIMER.SYS: CTC Channel Two 7.29 Generating Audio Tones on the Speaker 7.30 Sample Program: Generating a Tone using CTC Channel Two 7.31 Timing Short Periods using CTC Channel Two 7.32 Timing Short Periods using Mode Three 7.33 Vertical Retrace 7.34 Sample Program: Timing Short Periods using Mode Three 7.35 The Real Time Clock (RTC) 7.35.1 Reading and Writing RTC Registers 7.35.2 Allocation of the RTC Registers 7.35.3 RTC Register A 7.35.4 RTC Register B 7.35.5 RTC Register C 7.35.6 RTC Register D 7.35.7 Reading the RTC 7.35.8 Sample Program: A TSR Clock using int 8 and the RTC 7.36 The RTC Interrupt and Related BIOS Functions 7.36.1 The BIOS Event Wait and Delay Functions 7.36.2 The BIOS RTC Interrupt Handler 7.36.3 Using the RTC Interrupt 7.36.4 Sample Program: Using the RTC Interrupt 7.37 Using CTC Channel One and Refresh Detect 7.37.1 Sample Program: Timing the Refresh Detect signal 7.37.2 Sample Code: delay(milliseconds) Function using Refresh Detect 8 SPEEDING UP THE TIMER TICK 8.1 The Fast Tick int 8 Handler 8.2 The Interface with the Mainline 8.3 Writing a Fast Tick Handler 8.4 Comments on Fast Timer Tick Interrupts 8.5 Sample Program: Morse Player using Fast Timer Tick 8.6 Dynamic Fast Tick Periods 8.7 Sample Program: Dynamic Fast Tick Interrupt Handler 9 READING AN ABSOLUTE TIMESTAMP 9.1 Sample Program: Absolute Time Reference (Timestamp) in Mode Two 9.2 Sample Program: Absolute Timestamp in Mode Two - Assembler 9.3 Handling the Midnight Boundary 10 OTHER TOPICS 10.1 The 586 Time Stamp Counter 10.2 Serial Port Regular Interrupt 10.2.1 Serial Port (UART) Documentation 10.2.2 Sample Program: Regular Interrupt using the Serial Port 10.2.3 Inserting Delays into Serial Port Transmitted Data 10.3 External Interrupt Sources 10.3.1 External Interrupt through Parallel Port 10.3.2 External Interrupt through Serial Port 10.3.3 External Interrupt through Sound Card 10.3.4 External Interrupt through Custom I/O Card 10.4 The Joystick Port 10.4.1 Joystick Port Hardware 10.4.2 Reading the Joystick Buttons and Position 10.4.3 Notes from the PC-GPE Article 10.4.4 Sample Program: Reading the Joystick Position 10.4.5 Using the Joystick Port for General Purpose Input 10.4.6 Joystick Left/Right and Up/Down Detection 10.5 The Mouse and Mouse Driver [not written] 10.6 Networks 10.7 Sound Generation 10.7.1 Pulse Width Modulation (PWM) Principle 10.7.2 PWM Audio Generation Implementation 10.7.3 Sample Program: DTMF Generation using PWM 10.7.3.1 Sample Program Explanation 10.7.3.2 Other Methods of Sound Generation 10.7.4 Peter Moylan's MUSIC Package 10.8 Related Software Packages 10.8.1 The ATIM Package 10.8.2 The MSCHRT and TCHRT Packages 10.8.3 The TCTIMER Package 10.8.4 The MILLISEC Package 10.8.5 The MSEC_12 Package 10.8.6 The ERTIMER Package 10.8.7 The FASTCLOK Package 10.9 Benchmarking Considerations 10.10 Granularity and Uncertainty 10.11 Converting between Microseconds and CTC Clocks 10.12 Maintaining a Millisecond or Microsecond Count 10.12.1 Sample Program: Millisecond Count using int 1Ch 10.13 Notes on Microsoft Windows 10.14 DOS File Date and Time Stamps 10.15 DOS and the Date and Time 10.15.1 DOS Date Rollover Bugs 10.16 Simulating a Vertical Retrace Interrupt 10.16.1 Vertical Retrace Interrupt Simulation Description 10.16.1.1 Measuring the Field Time 10.16.1.2 Controlling the CTC Interrupt 10.16.1.3 Significance of the SafeMargin Value 10.16.1.4 Overhead due to Large SafeMargin and Screen Update 10.16.1.5 Enhanced Handling of Missed Retrace Start 10.16.1.6 Other Notes 10.16.2 Sample Program: Simulating a Vertical Retrace Interrupt 10.16.3 Triple Buffering 11 QUESTIONS AND ANSWERS 11.1 Timing Accuracy 11.2 Timer Interrupts (int 8, int 1Ch, RTC Interrupt) 11.3 Interrupt Priorities and Nesting 11.4 Interrupt Handler Restrictions 11.5 High Speed Timer Tick 11.6 DOS Date and Time 11.7 Accessing Hardware 11.8 Miscellaneous 12 REFERENCES ## 1.3 AUTHOR AND DISTRIBUTION This document (including sample code and programs) is Copyright (c) 1994-1996 by K. Heidenstrom. Please send corrections/additions/comments/suggestions to: Email: kheidens@actrix.gen.nz Snail mail: K. Heidenstrom, c/- P.O. Box 27-103, Wellington, New Zealand. If you send me comments, corrections etc via email or on a disk, you may find the quoter program described in section ¯¯ 1.8 helpful. It will generate a quoted copy of this file, to help you with marking up the document with your comments. The archive may be freely distributed via any electronic medium provided that it is not modified in any way, and that no charge (other than the normal charge to cover the disk, CD, etc) is made. The sample code and sample programs may be freely used in any commercial or non-commercial software. If you find this document useful, I would appreciate a postcard, or an email message, especially if you tell me a bit about your project. I'm pretty sure of this stuff, and I've done a bit of research (not as much as I should have done :-), but don't take it all as gospel. I have had to work some things out by myself and I may have got something wrong. If you know better about anything in here, please please drop me a message, so that other readers of this document can benefit from your experience. Thanks! FILE_ID.DIZ contents and SimTel information: pctim003.zip FAQ / App notes: Timing on the PC under DOS This archive contains a technical document useful to PC programmers, with many sample programs. The document covers timing and related subjects on the IBM PC family under DOS. Subjects include BIOS and DOS functions, the BIOS tick count, hardware interrupts, timer tick interrupts, Port B, the 8253/8254 timer, speeding up the timer tick, dynamic tick periods, simulated vertical retrace interrupt, double and triple buffering, absolute timestamping, the RTC, other timing methods, reading the joystick, PWM sound generation. Freeware. 13400 lines, PC ASCII, 340K ZIP file. Release 3, February 1996. Author: Kris Heidenstrom, kheidens@actrix.gen.nz. Simtel directory: SimTel/msdos/info/ Keywords: 145818 8253 8254 8255 8259 AT B CTC BIOS Delay DOS I/O IBM Interrupt Joystick MS-DOS PC PIC PIT PWM Port RTC Tick Timestamping Timing This document should be named PCTIMxxx.TXT where xxx is the release number shown at the top of the file. The latest version will always be available on SimTel (ftp.coast.net), or mirrors (such as Oakland). The file's URL at SimTel is ftp://ftp.coast.net/SimTel/msdos/info/pctim*.zip. Your browser may not accept a wildcard specification (i.e. the asterisk), and may say that the file does not exist. If so, view a listing of the SimTel/ msdos/info directory, find the file name, and modify the URL accordingly. ## 1.4 DISCLAIMER AND LEGAL STUFF I make no warranty of any kind with regard to this information and sample code. In no event shall I be liable for any damages whatsoever for any loss involving the use of this information or sample code, or due to any errors or omissions. Trademarks and service marks mentioned in this document are the property of their respective owners. Most of them probably know who they are :-) ## 1.5 DOCUMENT CONVENTIONS This file is formatted for viewing on an IBM or compatible (American ASCII with high-ASCII box characters, i.e. codepage 437) with an 80-column monospaced (i.e. text-mode) display, using tab stops every 8 columns. I have designed the document to work with DOS file viewers such as Vern Buerg's famous LIST program. Sections are hierarchically numbered. The contents is near the start of the file, and each section or subsection is announced by two '#' characters, a space, and the section number, to facilitate searching. I have mostly used British spelling. There are six illustrations in GIF format, which are enclosed in the FIGURES archive. Since they are line drawings, they do not look good if rescaled, so try to view them at their original resolutions if possible. Currently only the plain ASCII text version, in English, exists. There does not seem to be a good widely-used alternative at the moment. I would try Tex but I don't have a spare hard drive and six spare months to figure out how to use it! Let me know if you would like a Word Perfect 6.0 (DOS) or Word Perfect for Windows version and if there is enough interest I may create one. Also if you want to create an HTML version of this document, please get in touch! Numbers are decimal unless indicated. Hex is indicated by '0x' prefix or 'h' suffix, e.g. 0x55AA, 1Ch. Throughout this document, I refer to the 8253/8254 timer chip as the 'CTC' (counter/timer chip, or counter/timer circuit). This term is not normally used for this particular chip. Intel calls it the PIT (programmable interval timer). I mention this because you may get corrected if you publically call it the CTC. I have had a great deal of trouble maintaining a logical organisation in this document. I welcome any suggestions for improving its readability and understandability :-) Some subjects are outside my experience and I have marked these with (*). If you can fill in any of these gaps, this would be much appreciated. ## 1.6 SAMPLE CODE CONVENTIONS The sample code is in C and assembler, but you could convert it to Pascal or convert the C code to assembler. In most cases, I have aimed to be instructive rather than highly optimal. The sample programs are starting points - they are complete stand-alone programs, but are not necessarily very useful. They have been briefly tested with Borland C++ 2.0, Borland TASM 3.1, and Borland TLINK version 4.0. Short sample functions are untested. Let me know if you have any trouble with them. I have used small model for the C programs, so code and data are near, but this could be changed easily. The assembly language programs are in tiny model and should assemble with either MASM or TASM; I have had to forgo TASM's Ideal Mode and all of my nice macros. :-( I have listed #defines in each sample program as required. When I have re-used already-documented functions I have kept the name and coding the same, but have removed the comments from all but the first occurrence of the function. MS-DOS (version 2.0 or later) or a compatible operating system is assumed. ## 1.7 ACKNOWLEDGEMENTS My thanks for suggestions, information, criticism, and/or encouragement, to: Michael Bishop mxbish2@lookout.ecte.uswc.uswest.com Gordon Burditt gordon@sneaky.lonestar.org Jan-Pieter Cornet cornet@duteca2.et.tudelft.nl Saul Cozens s.cozens@sheffield.ac.uk David Empson dempson@actrix.gen.nz Klaus Hartnegg klaus@mailserv.brain.uni-freiburg.de Gian Uberto Lauri saint@dei.unipd.it William Luitje luitje@m-net.arbornet.org Terje Mathisen Terje.Mathisen@hda.hydro.com Michael Mauch mauch@uni-duisburg.de John Mertus mertus@brownvm.brown.edu {JAM} Peter Moylan peter@fourier.newcastle.edu.au Anders Roar Nielsen aroni@night.ping.dk Philip O'Carroll poc@maths.tcd.ie {POC} James Ralph jim@grc.com Paul Ross pa-ross@uwe.ac.uk Tor Sjowall tor@oslonett.no {TOR} Bob Smith bobs@access.digex.net John Stockton jrs@dclf.npl.co.uk Louis Warshaw louis@gate.net Please tell me if your name should be on this list! To give credit where it is due, throughout the text I have flagged specific contributions with the names shown in squiggly brackets. In particular, I have used (with permission) information about sampled audio generation on the PC speaker from a PC speaker music package written by Peter Moylan with help from Tim Channon. The technique mentioned here was also described by Mark Feldman (the PC-GPE guru). See section ¯¯ 10.7. I have also used many invaluable pieces of information (again with permission) from a collection of papers by Prof. John Mertus. Prof. Mertus's papers deal with subject testing (e.g. reaction timing), timer accuracy, and statistical analysis techniques for validating correct and reliable performance on various machines in various configurations (e.g. in protected mode, or on networked machines) which I have not covered in this document. They are thorough, and very interesting. You can FTP his files, in PostScript and LaTeX formats, from: ftp://jam.cog.brown.edu/pub/timing/ (various files). I have paraphrased his comments to maintain continuity in my document, and used the marker {JAM} so that credit goes where it is due. Any mistakes in the interpretation are mine, however. Prof. John Mertus owns the copyright on the above mentioned documents, please respect the considerable amount of work which has gone into them, by giving him credit if you use them. ## 1.8 QUOTER PROGRAM To generate a quoted version of this file so you can report problems to me, I have included in the SAMPLES archive a small program called QUOTE.COM, which operates as a quoting filter. Entering "C:\> QUOTE QUOTED.TXT" (where 'xxx' is the release number) will generate a quoted copy of this document for you to edit and mark up. You will probably want to use an editor that can handle more than 80 columns when editing the quoted copy. ## 1.9 REVISION NOTES Release 1 19950417 Release 2 19950816 Release 3 19960201 This is the third release of this document. At this point, I have at last covered all the important timing-related subjects that I know about. If you would like to see any other subjects covered, or would like to submit documentation or code on other relevant subjects, please get in touch. Otherwise the only intended future changes will be for correctness and to resolve the items indicated with (*) if possible. Changes from release 2 to release 3: þ Added information and sample program for vertical retrace interrupt simulation þ Tidying up þ Improved comparison of techniques þ Various improvements suggested by Dr. John Stockton þ Important note relating to long timer tick interrupt handlers added, see section ¯¯ 6.9.1. þ Added questions and answers section þ Added six illustrations in GIF format (hoping CI$ don't sue me :-) þ Added discussion on int 8 versus int 1Ch þ Added info on the triple buffering technique that can be used in conjunction with vertical retrace interrupt simulation þ Brief mentions of Microchannel int 8 reset þ Brief description of joystick left/right and up/down under interrupt þ Several notes from Michael Mauch (mauch@uni-duisburg.de) included þ Much expanded explanation of I/O access and recovery delays (section ¯¯ 7.9.4) þ Version 1.1.0 of quoter program, proper tab handling Changes from release 1 to release 2: þ Added sample program to read and write CTC registers and Port B with a command-oriented interface þ Added information on timing-related software packages þ Added brief notes on benchmarking considerations þ Modified sample code for short period timing using channel 2 to generate a strobe pulse on the parallel port with a duration of 5 us plus overhead þ Added code for converting between microseconds and CTC clocks þ Corekkted twleve typoes þ Fixed various minor clumsy explanations and stupid mistakes þ Added notes on Windows considerations from {TOR} þ Added description and sample function for handling midnight boundary when calculating elapsed time from absolute timestamp values þ Added timing using Refresh Detect signal on Port B (thanks to William Luitje) þ Added sample code to determine keyboard interface type (PC/XT or AT and later) þ Documentation on resolution and uncertainty þ Documentation and sample program for millisecond count variable þ Added delay(milliseconds) function using Refresh Detect þ Added Refresh Detect method of reading the joystick position þ Added notes on generating delays in serially transmitted data þ Added sample program to generate DTMF using PWM audio techniques þ Added information on DOS internal handling of date and time þ Include sample programs in executable form in the distribution file ## 1.10 GLOSSARY ASIC Application Specific Integrated Circuit, a high density custom chip. BCD Binary Coded Decimal, an encoding scheme where each digit of a decimal number is represented by four adjacent bits in a register. For example in BCD the number ninety-seven would be represented by 10010111 binary. The binary representation of 97 is 01100001. BIOS Basic Input/Output System, software in ROM chips on the motherboard. Bit If you don't know what a bit is, you are reading the wrong document :-) Channel One of three independent counting or timing circuits in the CTC. Also referred to as a 'timer'. Clock [n] An electrical signal at a fixed frequency [in this context]. [v] To trigger to perform a certain action. For an electrical clock, the action is performed at the instant of the rising or falling edge of the clock signal. Count [n] The value in a counter at a given moment in time. [v] What it usually means :-) Counter A register which increments or decrements when clocked. Counting register The counter in a CTC channel. It decrements when clocked, and can be reloaded from the Reload register. See section ¯¯ 7.3 CTC Counter/Timer Chip (or Circuit), the 8253 (PC, XT) or 8254 (AT and later) chip or functional equivalent. I prefer the term 'CTC' and use it in this document, but the CTC is more commonly known as the 'Timer', the 'Counter', and the 'PIT' (Programmable Interval Timer), which is Intel's name for the chip. CTC clock The clock input frequency to the CTC, 1.193181666666... MHz. Decrement Count down (usually by 1). Divide [frequency] To generate a lower frequency from a higher frequency by counting pulses and producing an output pulse when a certain number of input pulses have occurred. Divisor register Another name for the Reload register when modes 2 or 3 are used. See section ¯¯ 7.3. DMA Direct Memory Access, a technique where hardware (e.g. a floppy disk drive adapter or sound card) transfers data directly to or from memory, without processor intervention. EISA Enhanced Industry Standard Architecture, the bus structure used in some more modern PCs. It is an extension of the ISA architecture. EOI End of Interrupt, a command to the PIC to indicate that an interrupt handler has completed, see section ¯¯ 6.28. Flag A single bit indicating yes/no, true/false, on/off, enabled/disabled, or any condition which has two possible (and usually opposite) states. Frequency How often something occurs, per second. 18.2065 Hz (hertz) means 18.2065 times per second. Hz Hertz, the unit of frequency. IMR Interrupt Mask Register in the PIC. Increment Count up (usually by 1). Interrupt [n] A hardware- or software-generated interruption to the processor. [v] To suspend processing and cause the processor to execute a special section of code (the interrupt handler). Interrupt Controller See PIC. Interrupt Handler See Interrupt Service Routine. Interrupt Service Routine A section of code which is executed in response to an interrupt which 'services' (attends to) the hardware device or software invocation which generated the interrupt. Interrupt Vector See Vector. IRQ Interrupt request, a hardware interrupt source, handled by the PIC(s). IRR Interrupt Request Register, part of the 8259 PIC, see section ¯¯ 6.12. ISA Industry Standard Architecture (Also Irritatingly Slow Architecture), the bus structure of the PC, XT, and AT. Contrast to EISA, MCA and PCI architectures. Despite its limitations, it is still the most common bus structure. Many of these limitations are avoided with the VESA Local Bus extension. ISR Interrupt Service Routine. Also In Service Register, section ¯¯ 6.13. IVT Interrupt Vector Table, a table of 256 interrupt vectors occupying the first 1024 bytes of physical memory (in real and 8086 emulation modes). {JAM} See section ¯¯ 1.7. Jitter Unevenness, inconsistency, fluctuation, variation, or irregularity. LSI Large Scale Integration, a high density chip, see ASIC MCA Microchannel Architecture, the bus structure used in most IBM PS/2 machines. Sort of a dead duck as far as architectures are concerned. MHz Megahertz, one million hertz. Mode Of a CTC channel, the operational algorithm, or definition of behaviour, which has been selected (programmed) for that channel. Monostable A circuit which has one stable state (in which it will remain until triggered externally) and one unstable state (in which it will remain for a given period of time). Also called a one-shot. When triggered, it switches to its unstable state, and after a period of time, it returns to its stable state until triggered again. ms Millisecond(s), one thousandth of a second. NMI Non-Maskable Interrupt, an emergency interrupt source that cannot be masked (cannot be disabled under software control). PIC Programmable Interrupt Controller, an Intel 8259 chip or functional equivalent, which arbitrates IRQs and issues hardware interrupt requests to the processor. The PC and XT have one PIC, the AT has two. See section ¯¯ 6.4. {POC} See section ¯¯ 1.7. Port A link between software and hardware. Allows software to 'talk' to hardware devices. Also a connector on the back of the PC (e.g. serial or parallel port). POST Power-On Self-Test, the initialisation and test functions of the BIOS. PPI Programmable Peripheral Interface, an Intel 8255, used on the PC and XT, replaced by the keyboard controller on the AT and later machines. ppm Parts Per Million. 10000 ppm is one percent. 1 ppm is 0.0001 percent. 1 ppm corresponds to 0.0864 seconds per day; 11.5741 ppm is one second per day. Prefetch queue A look-ahead buffer in the processor which 'pre-fetches' instructions ahead of the current execution point during gaps when memory is not being accessed (i.e. while instructions are being internally processed by the processor) so that the instructions are ready before they are needed. This method is based on the assumption that instructions are executed in sequence. A jump, call, return, interrupt, or conditional branch instruction (if the branch is taken) disrupt this sequence and cause the prefetch queue to be flushed, slowing execution. Processor The Intel 80x86 central processing unit or functional equivalent. Reload register Register which contains the value which is reloaded into the Counting register under certain circumstances (depending on the mode), see section ¯¯ 7.3. Register A group of bits, can be used to store and manipulate numbers. ROM Read-Only Memory, a chip containing factory programmed software. RTC Real Time Clock, also called RTC/RAM or CMOS. A Motorola MC146818 or workalike, containing real-time date and time registers and battery-backed-up storage for BIOS parameters (CMOS). Tick The timer interrupt which normally occurs 18.2065 times per second. Timer See 'Channel' and 'CTC'. TLA Itself {TOR} See section ¯¯ 1.7. TSR Terminate and Stay Resident, a memory-resident pop-up utility program. UART Universal Asynchronous Receiver/Transmitter; a chip which transmits and receives asynchronous serial data (e.g. to a modem). The UART used in the PC is the 8250 or one of its descendants. us Microsecond(s), one millionth of a second. Vector [n] A pointer to a section of code, often an interrupt service routine. [v] To execute the code pointed to by a vector. VGA A video adapter standard. It is the basic standard for most current video hardware. The name comes from Video Graphics Array, the ASIC that implements the video hardware in the PS/2. -WR An active low write signal. The '-' prefix means active low. When this line goes low, the processor is writing data into a peripheral. ## 2 OVERVIEW OF TIMING TECHNIQUES This section gives you the big picture, then presents the timing techniques that will be described in detail in later sections, so you can choose the technique that interests you. ## 2.1 THE BIG PICTURE Figure 1 (in the accompanying FIGURES archive) gives a general overview of the two main timing subsystems in the PC, and their interfaces to the processor. The 14.31818 MHz system clock is divided by 12 to give a 1.193182 MHz clock (period is 0.8381 microseconds) which clocks the three channels of the 8253/8254 counter/timer chip (CTC). The CTC divides this frequency to lower frequencies using programmable divisors, and produces three output signals. CTC channel zero's output is connected directly to IRQ0 on the primary PIC (8259 interrupt controller chip), and generates int 8, the timer tick interrupt, about 18.2065 times per second, or once every 54.9254 milliseconds. The timer tick is a regular interrupt which allows certain actions (such as updating the system time-of-day) to be executed periodically. Interrupt 8 is serviced by the ROM-BIOS. The BIOS's int 8 handler increments the BIOS tick count variable (a 32-bit variable used for timekeeping) and turns off the floppy disk drive motors two seconds after they were last accessed. It also issues int 1C hex, which may be used as a regular interrupt source by user programs. The BIOS tick count is a 32-bit counter at low memory address 0040:006C, which contains the number of timer ticks (units of 54.9254 ms) since midnight and is used by DOS to calculate the time of day. CTC channels 1 and 2 can also be used for timing, via the Refresh Detect and Timer 2 readback signals on Port B. Channel 2 also generates audio for the PC speaker, and can be used in conjunction with channel 0 for PWM audio generation. The CTC divides its 1.193182 MHz clock down to 18.2065 Hz using a 16-bit counter. It is possible to read the actual count in progress in the CTC. In combination with the tick count variable, this can give an absolute time value, in units of 0.8381 us, for timestamping, elapsed time calculation, etc. In some applications, a timer tick rate faster than 18.2065 times per second is required. This can be achieved by reprogramming the CTC. The CTC is told to generate the timer tick at a faster rate, and the program intercepts the timer tick interrupt (int 8). The int 8 handler does its thing, and calls the old int 8 handler at the correct rate (18.2065 times per second) to maintain the correct system time. The Real Time Clock (RTC) was introduced with the AT, and all hardware- compatible ATs and later machines have one. The RTC is completely independent of the CTC. It uses a 32.768 kHz watch crystal for timekeeping and is battery backed up (i.e. continues to keep time while the computer is powered off). It can be used to generate a periodic interrupt, usually at 1024 Hz (1024 interrupts per second). ## 2.2 WHICH TECHNIQUE? There are three basic approaches to timing. Often two approaches can be used together. The techniques are summarised and compared in section ¯¯ 2.3. þ ABSOLUTE TIME REFERENCE You can write a function for use by your program that returns a value representing the absolute time, with units and resolution of one tick (54.9254 ms), or 977 us (the RTC regular interrupt rate), or one CTC clock (0.8381 us). þ RELATIVE TIME REFERENCE Your program can use the CTC to measure short time durations, for example to generate a short pulse on an I/O port pin or measure an external signal. þ REGULAR INTERRUPT An interrupt handler is called at regular (or sometimes, irregular) intervals, e.g. the default rate of once every 54.9254 ms, or 1024 times per second using the RTC, or at a user-selectable rate if you reprogram the CTC. The interrupt handler can perform operations in the background and/or maintain an absolute time variable. ## 2.3 COMPARISON OF TECHNIQUES 'Special precautions' in the following table refers to intercepting the DOS Ctrl-C, Critical Error, and Divide Overflow vectors so that interrupt vectors and/or hardware states can be restored safely when the program is terminated (see section ¯¯ 5 and subsections). ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Call DOS to read time-of-day ³ ³ Type: Absolute time reference ³ ³ Resolution: 55 ms or one second ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Not without special TSR techniques ³ ³ Works under OS/2: Yes ³ ³ Notes: Portable to all DOS and DOS compatible systems ³ ³ Applications: Low resolution, absolute time value ³ ³ Described in: Section ¯¯ 3.1 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Call BIOS RTC functions to read time-of-day ³ ³ Type: Absolute time reference ³ ³ Resolution: One second ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Usually safe ³ ³ Works under OS/2: Yes ³ ³ Applications: Low resolution, absolute time value ³ ³ Described in: Section ¯¯ 3.2 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Read RTC time of day directly ³ ³ Type: Absolute time reference ³ ³ Resolution: One second ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Probably ³ ³ Applications: Low resolution, absolute time value ³ ³ Described in: Section ¯¯ 7.35 and subsections ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Use the BIOS tick count variable ³ ³ Type: Absolute time reference ³ ³ Resolution: 55 ms ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Yes ³ ³ Notes: Can be read from within an interrupt routine ³ ³ Applications: General absolute time value, low resolution ³ ³ Described in: Section ¯¯ 4 and subsections ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Use int 1C hex ³ ³ Type: Regular interrupt ³ ³ Resolution: 55 ms ³ ³ Special precautions: Required ³ ³ Use in TSRs: No (see section ¯¯ 6.35) ³ ³ Works under OS/2: Yes ³ ³ Applications: Low resolution regular interrupt ³ ³ Described in: Section ¯¯ 6.1, section ¯¯ 6.35 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Intercept int 8 (in TSRs) ³ ³ Type: Regular interrupt ³ ³ Resolution: 55 ms ³ ³ Special precautions: Not required if used in a TSR ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Yes ³ ³ Applications: Regular interrupt for timing and/or popup by TSRs ³ ³ Described in: Section ¯¯ 6.33 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Read CTC channel 0 on-the-fly in mode two ³ ³ Type: Absolute timestamp ³ ³ Resolution: 0.8381 us ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Only if HW_TIMER = ON ³ ³ Notes: Can be read from within an interrupt routine ³ ³ Applications: Absolute time value, high resolution ³ ³ Described in: Section ¯¯ 7.16 and section ¯¯ 9 and subsections ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Read CTC channel 0 on-the-fly in mode three ³ ³ Type: Absolute timestamp ³ ³ Resolution: 0.8381 us ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Only if HW_TIMER = ON ³ ³ Notes: Can be read from within an interrupt routine ³ ³ Will not work on a PC, XT, or PS/2 ³ ³ No advantages over using mode two ³ ³ Applications: Absolute time value, high resolution ³ ³ Described in: Section ¯¯ 7.20, section ¯¯ 7.21, section ¯¯ 7.22 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Use CTC channel 2 for timing short delays ³ ³ Type: Relative time reference ³ ³ Resolution: 0.8381 us ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Only if HW_TIMER = ON ³ ³ Notes: Can be used within an interrupt routine ³ ³ Good for implementing short timeouts ³ ³ Should only be used with interrupts locked out ³ ³ Disrupts the system beep if used under interrupt ³ ³ Applications: Short delays, useful in dedicated hardware control ³ ³ Described in: Section ¯¯ 7.31, section ¯¯ 10.4.4 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Read CTC channel 0 in mode three for short delays ³ ³ Type: Relative time reference ³ ³ Resolution: 0.8381 us ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Only if HW_TIMER = ON ³ ³ Notes: No advantages over using mode two ³ ³ Applications: Short delays, useful in dedicated hardware control ³ ³ Described in: Section ¯¯ 7.32 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Vertical Retrace (polled) ³ ³ Type: Relative time reference ³ ³ Resolution: Medium (1/60 or 1/72 of a second) ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Probably not ³ ³ Notes: Useful for synchronising to screen scan ³ ³ Applications: Screen scan synchronisation in games, graphics apps ³ ³ Described in: Section ¯¯ 7.33 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: RTC Periodic Interrupt ³ ³ Type: Regular interrupt ³ ³ Resolution: 976.5625 us ³ ³ Special precautions: Required ³ ³ Use in TSRs: Not really safe ³ ³ Works under OS/2: Probably not ³ ³ Notes: Doesn't interfere with the CTC ³ ³ Convenient resolution ³ ³ Won't work on PCs and XTs ³ ³ Applications: Programs that slow the machine or time other programs ³ ³ Described in: Section ¯¯ 7.36 and subsections ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: BIOS Delay and Event Wait functions ³ ³ Type: Relative delay ³ ³ Resolution: 976.5625 us ³ ³ Special precautions: May be required ³ ³ Use in TSRs: Not safe ³ ³ Works under OS/2: Probably not ³ ³ Notes: Doesn't interfere with the CTC ³ ³ Won't work on PCs and XTs ³ ³ Applications: General delays or timeouts with about 1ms resolution ³ ³ Described in: Section ¯¯ 7.36.1 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Refresh Detect (CTC channel 1 read-back) ³ ³ Type: Relative time reference ³ ³ Resolution: 15.0857 us ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: No ³ ³ Notes: High resolution ³ ³ Very tidy way to generate short delays ³ ³ Can be used to generate delays of 'at least x' with ³ ³ interrupts enabled ³ ³ Can be used within an interrupt routine ³ ³ Interrupts, if enabled, will lengthen the delay ³ ³ Won't work if the RAM refresh rate has been changed ³ ³ Won't work on old PCs and XTs ³ ³ Applications: Short delays, timeouts, timing input signals ³ ³ Described in: Section ¯¯ 7.37 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Speed up CTC channel 0 (timer tick) rate ³ ³ Type: Regular or irregular interrupt ³ ³ Resolution: Settable ³ ³ Special precautions: Required ³ ³ Use in TSRs: No ³ ³ Works under OS/2: Only if HW_TIMER = ON ³ ³ Notes: Can generate exact interrupt rate (e.g. 500us, 1ms) ³ ³ May affect other DOS sessions under OS/2 with HW_TIMER ³ ³ Applications: Fast regular interrupt source - used for games, etc ³ ³ Described in: Section ¯¯ 8 and subsections ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Intel 586 Time Stamp Counter ³ ³ Type: Absolute or relative time reference ³ ³ Resolution: Extremely high ³ ³ Special precautions: Not required ³ ³ Use in TSRs: Yes ³ ³ Works under OS/2: Probably ³ ³ Notes: Ridiculously high resolution ³ ³ Disadvantages: Doesn't work on 486 or lower ³ ³ Not guaranteed to work on future processors ³ ³ Timing unit depends on processor clock speed ³ ³ Applications: High resolution timestamping for usage billing ³ ³ Described in: Section ¯¯ 10.1 and subsections ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: Regular interrupt from serial port ³ ³ Type: Regular interrupt ³ ³ Resolution: Selectable ³ ³ Special precautions: Required ³ ³ Use in TSRs: Not reliably ³ ³ Works under OS/2: No ³ ³ Notes: User-selectable interrupt rate ³ ³ Doesn't affect the CTC or the RTC ³ ³ Requires a spare serial port ³ ³ Applications: Slow or fast regular interrupt ³ ³ Described in: Section ¯¯ 10.2 and subsections ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Technique: External regular or irregular interrupt source ³ ³ Type: Regular or irregular interrupt ³ ³ Resolution: Depends on external hardware ³ ³ Special precautions: Required ³ ³ Use in TSRs: May not be reliable ³ ³ Works under OS/2: Probably not ³ ³ Notes: Can be very versatile ³ ³ Requires special hardware ³ ³ Applications: Slow or fast interrupt using special hardware ³ ³ Described in: Section ¯¯ 10.3 and subsections ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ## 2.4 OTHER SUBJECTS COVERED IN THIS DOCUMENT I've included, in addition to timing related documentation, info on handling the DOS Ctrl-C, critical error, and divide overflow interrupts (required if you are going to intercept any other interrupts), see section ¯¯ 5 and subsections, lots of general information about interrupts, information on various relevant hardware devices, information on the joystick hardware, information on sound and music generation using a technique called PWM (see section ¯¯ 10.7), and information on vertical retrace interrupt emulation (section ¯¯ 10.16). ## 3 DOS AND BIOS TIME-OF-DAY AND ALARM FUNCTIONS In high level languages, library functions are available to get the time of day and should be used for portability. Internally they use the DOS time of day functions. Assembly language programmers can use the DOS and BIOS functions directly. ## 3.1 READING THE DATE AND TIME FROM DOS DOS functions 2A, 2B, 2C, and 2D hex relate to time of day. To use them, set AH to the function number, and set other registers as applicable, and issue int 21 hex. All values are accepted and returned in binary form (i.e. not BCD). Get Date : DOS functions (int 21h) Call with: AH = 2A hex Returns: AL = Day of week (0 to 6 correspond to Sun to Sat) CX = Year in full (1980 to 2099, 7BCh to 833h) DL = Day of month (1 to 31) DH = Month of year (1 to 12 correspond to Jan to Dec) Get Time : DOS functions (int 21h) Call with: AH = 2C hex Returns: CH = Hours (0 to 23, using 24-hour clock format) CL = Minutes (0 to 59) DH = Seconds (0 to 59) DL = Hundredths of seconds (0 to 99) (see note below) Set Date : DOS functions (int 21h) Call with: AH = 2B hex CX = Year in full (must be 1980 to 2099) DL = Day of month (1 to 31, depending on month) DH = Month of year (1 to 12) Returns: AL = Success/failure: 0 = OK, 0FFh = Bad date specified Set Time : DOS functions (int 21h) Call with: AH = 2D hex CH = Hours (0 to 23, 24-hour clock format) CL = Minutes (0 to 59) DH = Seconds (0 to 59) DL = Hundredths of seconds (0 to 99) (see note below) Returns: AL = Success/failure: 0 = OK, 0FFh = Bad time specified The time of day is calculated from the BIOS tick count variable. The hundredths of seconds value is approximated using an internal algorithm which apparently produces an even distribution of values, but its resolution is only as good as the tick counter, i.e. 54.9254 ms. See section ¯¯ 10.15 for more information. ## 3.2 READING THE DATE AND TIME FROM THE BIOS BIOS functions provide access to the tick count and the RTC (Real-Time Clock), accessed by issuing int 1A hex. (The BIOS tick count functions are also part of this interrupt, but should not be used - see section ¯¯ 4.3 for details). The RTC functions accept and return values in BCD form. The RTC functions are present on the AT and all later machines, but not on the original PC or XT (there may be some hybrid machines that do support them, but I don't know of any). Get RTC Date : int 1Ah Call with: AH = 04 hex Returns: CH = Hundreds of years (19h or 20h, BCD format) CL = Year (00h to 99h, BCD format) DH = Month (01h to 12h, BCD format) DL = Day of month (01h to 31h, BCD format) CF = Error status, carry is set if clock is not running Get RTC Time : int 1Ah Call with: AH = 02 hex Returns: CH = Hours (00h to 23h, BCD format) CL = Minutes (00h to 59h, BCD format) DH = Seconds (00h to 59h, BCD format) CF = Error status, carry is set if clock is not running Set RTC Date : int 1Ah Call with: AH = 05 hex CH = Hundreds of years (19h or 20h, BCD format) CL = Year (00h to 99h, BCD format) DH = Month (01h to 12h, BCD format) DL = Day of month (01h to 31h, BCD format) Returns: Nothing Set RTC Time : int 1Ah Call with: AH = 03 hex CH = Hours (00h to 23h, BCD format) CL = Minutes (00h to 59h, BCD format) DH = Seconds (00h to 59h, BCD format) DL = Daylight saving flag: 00 = Standard time 01 = Daylight saving time Returns: Nothing ## 3.3 SAMPLE PROGRAM: DOS DEVICE DRIVER FOR THE AT CLOCK The following program implements an installable DOS device driver for the AT clock, using the BIOS RTC functions. Save the following code section as ATRTC.ASM and assemble according to the instructions in the comment block. -------------------------------- snip snip snip -------------------------------- NAME ATRTC ; Sample program #1 ; DOS Device Driver for the AT Real Time Clock ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; ; This program assembles into ATRTC.SYS, an installable DOS device driver that ; removes DOS's dependence on the BIOS timer tick count variable, using the AT ; BIOS's Real Time Clock functions to get and set the current date and time. ; This program does not support the daylight saving feature of the RTC. ; At installation, it checks that the machine is an AT, and that the RTC is ; functional. If either check fails, it installs but remains inactive. ; ; Save this file to ATRTC.ASM and assemble with: ; masm atrtc; ; link atrtc; ; exe2bin atrtc.exe atrtc.sys ; or ; tasm atrtc; ; tlink atrtc; ; exe2bin atrtc.exe atrtc.sys ; ; Then place ATRTC.SYS in your root directory, DOS directory, or utilities ; directory, and add the line DEVICE=\ATRTC.SYS to your CONFIG.SYS ; file, where specifies the directory path to ATRTC.SYS. If you want ; to load ATRTC.SYS high, use DEVICEHIGH= or HIDEVICE= instead of DEVICE= to ; load the driver. BinFile SEGMENT ASSUME cs:BinFile,ds:nothing,es:nothing,ss:nothing ORG 0 Origin: ; Device driver header Header DD -1 ; Link to next device Attrib DW 8008h ; Attribute word DW Strategy ; Strategy entry point DW Interrupt ; Interrupt entry point DB "CLOCK$ " ; Device name ; When a request is made for this device, DOS calls the "Strategy" routine, ; passing a pointer to the request header in ES:BX. The strategy routine saves ; this pointer in ReqHdr and returns to DOS. DOS then calls the "Interrupt" ; routine, which executes the request specified by the request header. ReqHdr DD 0 ; Far pointer to request header InitPtr DW Init ; Address of init function MonthTbl1 DW 0,31,59,90,120,151,181,212,243,273,304,334,365 ; Normal MonthTbl2 DW 0,31,60,91,121,152,182,213,244,274,305,335,366 ; Leap yr Strategy PROC far ; Save address of Request Header mov WORD PTR ReqHdr+0,bx mov WORD PTR ReqHdr+2,es retf ; Back to DOS Strategy ENDP Interrupt PROC far push ds push si push dx push cx push bx push ax ; Preserve registers lds bx,ReqHdr ; Point DS:BX to Request Header mov WORD PTR ds:[bx+3],100h ; No errors, completed mov al,ds:[bx+2] ; Get command number from Request Header mov cx,OFFSET Read ; Prepare for Read command cmp al,4 ; Check for Read command je GotAdr ; If so mov cx,OFFSET Write ; Prepare for Write command cmp al,8 ; Check for Write command je GotAdr ; If so cmp al,9 ; Check for Write with Verify je GotAdr ; If so mov cx,InitPtr ; Prepare for Init command cmp al,0 ; Check for init command je GotAdr ; If so mov cx,OFFSET Null ; If none of above, use Null routine GotAdr: call cx ; Dispatch to appropriate handler pop ax pop bx pop cx pop dx pop si pop ds ; Restore all regs retf Interrupt ENDP ; These command code subroutines called by "Interrupt" Routine. They are called ; with DS:BX pointing to the request header. They do not return an error code. Read PROC near ; Function 4 = Read lds bx,ds:[bx+14] ; Point DS:BX to buffer area push bx ; Keep offset ; Get date, check clock is working mov ah,4 int 1Ah ; Read RTC date jnc NoRTCErr1 ; If alright, continue xor cx,cx ; Assume 1980 jmp SHORT StoreYear ; Don't do calculations ; Calculate year (1980 - 2099) in binary form ; Note - the above check for a date less than 1980 was suggested by Michael ; Mauch (mauch@uni-duisburg.de). He reports that his BIOS (AMI, 06/06/92) ; has a bug which causes years 20xx to be reported as 19xx. The following ; workaround handles this bug. NoRTCErr1: cmp cx,1980h ; Check for BIOS returning year jae YearValid ; 19xx when it should be 20xx mov ch,20h ; If so, fix it YearValid: mov al,cl ; Get years (00-99) call BCDToBinary ; Convert to binary cbw ; Zero AH push ax ; Keep it mov al,ch ; Get hundreds of years call BCDToBinary ; Convert to binary mov ah,100 ; Factor mul ah ; Get centuries x 100 pop cx ; Restore year 0-99 add ax,cx ; Now have absolute year in AX. xor cx,cx ; Zero day counter mov bx,1980 ; Starting year ; Year calculation stuff - AX is current year (1980 to 2099) read from RTC, ; BX is year being evaluated, CX is count of days so far. SI points to the ; appropriate month table for this year. ; Leap year algorithm: If the year is a multiple of four, it is a leap year, ; unless it's also a century, in which case it is not a leap year, except ; centuries that are a multiple of 400 years (e.g. 2000), in which case it ; is a leap year. In this case, the only century involved is 2000, thus just ; checking for a multiple of four is enough. If it's a multiple of four, it ; is a leap year, i.e. 366 days instead of 365. ; ; Note - There is a way to do this without looping and accumulating, using a ; clever little formula, but I will use this method, because I don't want to ; waste the time I spent getting this method to work :-) FindYearLp: mov si,OFFSET MonthTbl1 ; Prepare for not leap year test bl,3 ; Leap year? jnz NotLeap1 ; If not mov si,OFFSET MonthTbl2 ; If leap year, use leap year table NotLeap1: cmp bx,ax ; Got to this year yet? jae GotYear1 ; If so add cx,cs:[si+24] ; Add number of days in this year inc bx ; Increment year number jmp SHORT FindYearLp ; Loop to find year ; Now have BX containing number of days since 1st of January 1980 for the start ; of the current year - now incorporate the month and the day-of-month. GotYear1: mov al,dh ; Get month, 1-12, BCD call BCDToBinary ; Convert to binary cbw ; Zero AH shl ax,1 ; Double for word sized table mov bx,ax ; Month (1-12) to BX add cx,cs:[si+bx-2] ; Get month start, adjusted for 1-12 mov al,dl ; Get day of month in BCD, 1-31 call BCDToBinary ; Convert to binary dec ax ; Convert to zero-up cbw ; Zero hibyte add cx,ax ; Add in too. StoreYear: pop bx ; Restore offset of data structure mov ds:[bx+0],cx ; Store days since 1980 in structure mov ah,2 int 1Ah ; Read RTC time jnc NoRTCErr2 ; If alright xor cx,cx ; If bad, zero values xor dx,dx NoRTCErr2: mov al,ch ; Hours call BCDToBinary ; To binary mov ds:[bx+3],al ; Store in DOS's data structure mov al,cl ; Minutes call BCDToBinary ; To binary mov ds:[bx+2],al ; Store mov al,dh ; Seconds call BCDToBinary ; To binary mov ds:[bx+5],al ; Store seconds mov BYTE PTR ds:[bx+4],0 ; Hundredths of seconds are zero Null: ret ; Return to handler dispatcher Read ENDP BCDToBinary PROC near ; Convert AL BCD to binary push cx mov ch,al ; Copy value to CH mov cl,4 shr al,cl ; Shift top nibble down mov cl,10 mul cl ; Get ten times the high digit and ch,0Fh ; Low digit only in CH add al,ch ; Add low digit pop cx ret ; Destroys AX and flags only BCDToBinary ENDP Write PROC near ; Functions 8 and 9 = Write lds bx,ds:[bx+14] ; Point DS:BX to buffer area push bx ; Keep for later mov dx,ds:[bx+0] ; Get number of days since 1980 ; Determine the year, by successively accumulating days starting at 1980 until ; we exceed the number of days since 1980 that was provided by DOS. Once we ; pass the right year, adjust the number of days back again. We then have the ; year and the number of days within that year. mov ax,1980 ; Start at year 1980 xor cx,cx ; Clear day accumulator DayAddLp2: mov bx,365 ; Assume for 365 days this year test al,3 ; Is current year a leap year? jnz NotLeap2 ; If not, keep the 365 inc bx ; If so, use 366 NotLeap2: add cx,bx ; Add number of days in this year cmp cx,dx ; Have we gone past the year we want? ja GotYear2 ; If so, have current year in BX inc ax ; If not, increment the year jmp SHORT DayAddLp2 ; Loop GotYear2: sub cx,bx ; Get number of days up to start of year sub dx,cx ; Get remainder (Months and Days) mov si,OFFSET MonthTbl1 ; Prepare for not leap year test al,3 ; Leap year? jnz NotLeap3 ; If not mov si,OFFSET MonthTbl2 ; If leap year, use leap year table ; Here, AX contains the absolute year in binary, DX contains the number of ; days offset into that year, in the range 0 - 364 (or 0 - 365 for leap years) ; and SI points to the appropriate month table for the year being set. NotLeap3: mov bl,100 ; Divisor div bl ; Get AL = century (19 or 20), AH = year mov bx,ax call BinaryToBCD ; Convert century to BCD xchg al,bh ; To BH, get year within century call BinaryToBCD ; To BCD xchg al,bl ; To BL, and get year in binary to AL push bx ; Keep value for CX for Set RTC Date ; Now calculate month and day of month from number of days offset into year (DX) xor bx,bx ; Point to start of table CompareMonth: inc bx inc bx ; Move to next month entry cmp dx,cs:[si+bx] ; Compare to start of next month jae CompareMonth ; If DX is not less than table entry sub dx,cs:[si+bx-2] ; Subtract number of days in months ; Now have DL = day of month (zero-up), and BL = month of year (1-12) x 2. xchg ax,dx ; Get day of month (0-30) to AL inc ax ; Convert to 1-31 call BinaryToBCD ; Convert to BCD xchg ax,dx ; To DL xchg ax,bx ; Get month x 2 from BL shr al,1 ; Get month number, 1-12 call BinaryToBCD ; Convert to BCD mov dh,al ; To DH pop cx ; Restore years and hundreds of years mov ah,5 int 1Ah ; Set RTC date ; Now set the time pop bx ; Restore pointer to DOS's data buffer mov al,ds:[bx+5] ; Read seconds from DOS call BinaryToBCD ; Convert to BCD mov dh,al ; To DH xor dl,dl ; No daylight saving flag mov al,ds:[bx+3] ; Read hours call BinaryToBCD ; Convert to BCD mov ch,al ; To CH mov al,ds:[bx+2] ; Read minutes call BinaryToBCD ; Convert to BCD mov cl,al ; To CL mov ah,3 int 1Ah ; Set RTC time ret ; Return to handler dispatcher Write ENDP BinaryToBCD PROC near ; Convert AL binary to BCD xor ah,ah ; Zero hibyte mov cl,10 div cl ; Div 10 - quotient AL, remainder AH mov cl,4 shl al,cl ; Shift quotient to top nibble or al,ah ; Combine two nibbles into AL ret ; Destroys AX, CL and flags BinaryToBCD ENDP Discard: ; End of resident portion of driver SignOnMsg DB 13,10,"ATRTC - DOS Device Driver for the AT Real Time Clock" DB 13,10,9,"Part of the PC Timing FAQ / Application notes" DB 13,10,9,"By K. Heidenstrom (kheidens@actrix.gen.nz)" DB 13,10,"$" InstalledMsg DB 9,"Installed",13,10,"$" NoClockMsg DB 9,"Error - RTC not active",13,10,7,"$" Init PROC near ; Function 0 = Initialise Driver mov WORD PTR ds:[bx+14],OFFSET Discard ; Tell DOS where mov ds:[bx+16],cs ; free memory starts mov ax,0F000h ; BIOS code segment mov ds,ax cmp BYTE PTR ds:[0FFFEh],0FDh ; Check for AT pushf ; Preserve result push cs pop ds ; Point DS to our segment address ASSUME ds:BinFile mov WORD PTR InitPtr,OFFSET Null ; Point INIT at Null proc mov dx,OFFSET SignOnMsg mov ah,9 int 21h ; Display signon message popf ; Are we running on an AT? jae RTCError ; If not, error! mov ah,4 int 1Ah ; Read date mov dx,OFFSET InstalledMsg ; Point to 'installed' message jnc NoRTCError ; If RTC is working, skip error stuff RTCError: mov BYTE PTR Attrib,0 ; Error - clear CLOCK attribute bit mov dx,OFFSET NoClockMsg NoRTCError: mov ah,9 int 21h ; Display error or installation message ret Init ENDP BinFile ENDS END Origin -------------------------------- snip snip snip -------------------------------- {TOR} points out that using this driver will result in increased overhead, because: "the CLOCK$ device is read VERY often by DOS. I did look at this once, and _as_far_as_I_remember_, CLOCK$ is read on every file access". Though I don't believe this is a problem, the efficiency of this driver in cases where frequent file accesses are made could be improved by caching the date and time values and the BIOS tick count variable each time the date and time are requested, and only re-reading the RTC if the tick count has changed. You would use the following logic when the date and time are requested: Read the current BIOS tick count variable and compare to the stored value. If same, copy the cached date and time values into the data area and return. If different, copy the current BIOS tick count variable to the stored value, read the RTC and recalculate the date and time values, store the new values to the variables and copy them to the data area and return. This method would ensure that the RTC is actually accessed no more often than 18.2065 times per second. If frequent file accesses are made, the overhead of reading the RTC is avoided for most of them. Michael Bishop (mxbish2@lookout.ecte.uswc.uswest.com) reports that DOS loses time noticeably on his machine which is: "an IBM PS/Note laptop 25MHz 386, essentially a PS/2 Model 70/80". While the machine is running, time runs slow. After a reboot, the time is restored correctly. This symptom indicates that the machine is missing timer ticks (see sections ¯¯ 4.1, ¯¯ 6.1, and ¯¯ 10.15 for details). Michael was unable to find the IBM driver 'CMOSCLK.SYS' to fix this, but reports that ATRTC fixed the problem. ## 3.4 OTHER BIOS TIME AND ALARM FUNCTIONS The RTC can generate an alarm at a specific time of day (i.e. every 24 hours) until disabled by software. The hardware is more flexible than this (see section ¯¯ 7.35) but the BIOS function only supports one alarm per day. The alarm is signalled via int 4A hex, which is invoked by the BIOS when the alarm triggers. Normally int 4Ah points to an IRET. Int 4Ah is invoked under interrupt, so the normal considerations for hardware interrupt handlers apply (see section ¯¯ 6.23 through ¯¯ 6.26). Int 4Ah will normally be called with interrupts disabled, but don't count on it. Disable interrupts explicitly if required. The int 4Ah handler must not destroy any working registers. The related BIOS functions are as follows. Note that these functions are only supported on the AT and later machines - the PC and XT do not support them. Set 24-Hour Alarm Time of Day : int 1Ah Call with: AH = 06 hex CH = Hours (00h to 23h, BCD format) CL = Minutes (00h to 59h, BCD format) DH = Seconds (00h to 59h, BCD format) Returns: Nothing Note: When alarm occurs, int 4Ah is invoked Disable 24-Hour Alarm : int 1Ah Call with: AH = 07 hex Returns: Nothing Functions 8, 9, 0Ah, and 0Bh are supported on some IBM models. See Ralf Brown's Interrupt List (see section ¯¯ 12) for more information. ## 3.5 OTHER OTHER BIOS TIME FUNCTIONS The BIOS on the AT and later provides int 15h functions 83h and 86h which use the RTC interrupt (1024 interrupts per second on IRQ8, int 70h). See section ¯¯ 7.35 for more information about the RTC chip, section ¯¯ 7.36 for details of the RTC interrupt and how to use it, and section ¯¯ 7.36.1 for information on these BIOS functions. ## 3.6 THE TIMES THEY ARE A-CHANGIN' Any technique that makes use of a time taken from the RTC or derived from the tick count should take into account the fact that the time can be changed by the user, or even by other software. This can cause the time to go forwards or backwards slightly, or even jump to a totally different time. Under real DOS, normally this will only happen to a TSR or a program that shells to DOS, where the user may change the time via the TIME command, or a program that allows the user to change the time. A networked computer may automatically update its time from the server, via the resident network software. On a machine running a multitasking operating system such as OS/2, Linux, Win95, and even Windoze, changing the system date and time in one session will change the time in all sessions. ## 4 USING THE BIOS TICK COUNT VARIABLE The BIOS tick count variable gives an absolute time reference with a resolution of 54.9254 milliseconds. ## 4.1 THE BIOS TICK COUNT VARIABLE The BIOS tick count variable is a 32-bit unsigned longword or DWORD, stored at low memory address 0040:006C (can also be addressed as 0000:046C), maintained by the BIOS's int 8 handler. It contains the number of timer ticks (units of 54.9254 ms) since midnight, in the current day. The maximum value in this variable is 1800AF hex, so only the bottom 21 bits can ever be nonzero. The PC and XT have no special real-time clock support in the BIOS, so the tick counter is initialised to zero on every reboot. In ATs and later machines, the BIOS's power-on initialisation code reads the real-time clock and sets the tick count variable to the equivalent number of ticks. See section ¯¯ 10.15. There are approximately 65536 ticks in an hour (65543.4265 to be exact), so the high word of the tick count corresponds _approximately_ to the hour of the day. ## 4.2 CHANGE OF DAY There are 1,573,042.24 ticks in a day, but the BIOS writers approximated the CTC clock to 1.193180 MHz, so the BIOS uses 1,573,040 (001800B0 hex) ticks per day. This gives a 1.42166 ppm error (0.123 seconds per day), which is fairly insignificant compared to the clock frequency inaccuracy (see section ¯¯ 7.2). The tick count increments up to 001800AF hex, then 'rolls over' to zero at midnight. When midnight passes, the BIOS sets the one-byte 'midnight' flag at 0040:0070, to 1, indicating that a midnight has passed. Note - some BIOSes may indicate change of day by _incrementing_ the midnight flag byte, so that if two midnights pass without DOS reading the time, the date could still be updated correctly. See section ¯¯ 10.15 for details. ## 4.3 READING AND SETTING THE TICK COUNT You can read the tick count directly, or request it from the BIOS via int 1Ah. Get Tick Count : int 1Ah Call with: AH = 00 hex Returns: CX = High word of tick count DX = Low word of tick count AL = Midnight-passed flag Notes: This call clears the midnight flag byte. Notes: Do not use this call in an application - see below Set Tick Count : int 1Ah Call with: AH = 01 hex CX = High word of tick count DX = Low word of tick count Notes: This call clears the midnight flag byte. The DOS CLOCK$ device driver uses the Get Tick Count function, int 1Ah, function 0, and relies on the midnight flag returned by this function to detect a change of day. User programs should not use these two BIOS functions, because if the program calls the function just after midnight, it will see the midnight flag, and the midnight flag will be cleared, so DOS will miss out on seeing the change of day, and will not increment the date. See sections ¯¯ 10.15 and ¯¯ 10.16. This problem would be solved if DOS used the real-time clock for timekeeping (see section ¯¯ 3.3 for a DOS device driver that uses the real-time clock). It is safer and more efficient to read (and write) the count directly at its location in low memory. The tick count is 'volatile', and must be accessed with an indivisible operation (using a 32-bit register such as EAX), or with interrupts disabled. If you access the loword and hiword separately without disabling interrupts around the two accesses, a tick interrupt could come along and modify the tick count variable between the two reads or writes. See section ¯¯ 4.5 for details. ## 4.4 SPECIAL REQUIREMENTS - NONE The great advantage of timing using the BIOS tick count, is that it makes no changes to the system, i.e. it doesn't change the hardware setup, or modify any interrupt vectors. This simplifies the code, and means that if the program is terminated (by Ctrl-Break, or a Divide Overflow, or by the user replying 'A' to the Abort, Retry, Ignore prompt), no special clean-up is required. ## 4.5 SAMPLE PROGRAM: READING THE TICK COUNT The function read_bios_tick_count() reads and returns the BIOS tick count. The function has_tick_occurred() detects whether the tick count has changed since the last time that function was called. It returns TRUE on the initial call. It does not report _how_many_ timer ticks occurred between calls. Notice that read_bios_tick_count() explicitly disables interrupts around the read of the 32-bit tick count value. Even though the tick count variable is declared as volatile, the compiler (Borland C++ 2.0) generates two 16-bit MOV instructions without disabling interrupts. If an interrupt occurred between the two MOV instructions, an incorrect value will be read. Apparently this is not a bug, it is because the compiler doesn't know how to safely read 'volatile' variables. Hmm. I'd say if it's not a bug, it's definitely a mis-feature. If the compiler can use the 32-bit registers (compiling for protected mode, or compiling with 32-bit code under DOS, this problem does not (or should not!) occur. Michael Mauch (mauch@uni-duisburg.de) found that Borland C++ 4.0 does use a 32-bit MOV instruction if 32-bit code generation is enabled via #pragma option -3 or #pragma option -4. Dr. John Stockton (see section ¯¯ 1.7) reports that this problem also exists in Borland Pascal 7 when a signed long variable (BP7 doesn't have _unsigned_ longs) is loaded from the tick count variable, as the tick count is read non-atomically with two 16-bit accesses. Disabling interrupts around the load prevents the problem described above. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #2 Demonstrates reading the BIOS tick count Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE2.C and compile with: bcc -I -L -ms sample2.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Pass go, add printf(), program is 8K already :-) */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL) unsigned long read_bios_tick_count(void) { unsigned long ct; asm pushf; /* Preserve interrupt flag */ asm cli; /* Needed even though tick count is volatile */ ct = * BIOS_TICK_COUNT_P; asm popf; /* Restore interrupt flag */ return ct; } int has_tick_occurred(void) { static unsigned long old_tick_count = 0xFFFFFFFFL; /* Invalid */ if (read_bios_tick_count() != old_tick_count) { /* Changed? */ old_tick_count = read_bios_tick_count(); return TRUE; } return FALSE; /* No change */ } void main(void) { unsigned int n = 0; printf("Sample program #2 - Demonstrates reading the BIOS tick count variable\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); while (n < 18) /* Stop after one second */ if (has_tick_occurred()) printf("Tick %d: BIOS tick count variable = %ld\n", ++n, read_bios_tick_count()); exit(0); } -------------------------------- snip snip snip -------------------------------- ## 4.6 SAMPLE CODE: OPTIMISED FUNCTION TO READ THE TICK COUNT This is a more optimal coding of read_bios_tick_count() in assembler. I chose to disable interrupts and read the loword and hiword separately, rather than using LES or LDS (indivisible operations) because it is not good practice to load a segment register with a value which is not a real segment-paragraph. Of course if your code requires a 386 or higher, you can just load an extended (32-bit) register (e.g. EAX) in one single indivisible operation. -------------------------------- snip snip snip -------------------------------- ; Function to read the BIOS tick count (C-callable) ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; _read_bios_tick_count PROC near ; or FAR for far code model ; unsigned long read_bios_tick_count(void); push ds ; Preserve data segment pushf ; Keep interrupt flag xor ax,ax ; Zero mov ds,ax ; Address BIOS data area cli ; Don't want a tick to interrupt us mov ax,ds:[46Ch] ; Get loword of count mov dx,ds:[46Eh] ; Get hiword of count popf ; Restore interrupt flag as provided pop ds ; Restore data segment ret ; Return tick count in DX|AX _read_bios_tick_count ENDP -------------------------------- snip snip snip -------------------------------- ## 4.7 SAMPLE PROGRAM: USING THE TICK COUNT FOR TIMEOUT CHECKING This example demonstrates two independent timeout counters using the BIOS tick count variable. The timeout counter record consists of the starting tick count, the number of ticks in the timeout period, and a flag which can be used to report the transition to the timed-out state. set_timeout() sets up a timeout counter. The state of the timeout can then be requested using is_timedout() and just_timedout(). is_timedout() returns TRUE if the current time is outside the timeout period specified by the counter. just_timedout() returns TRUE the first time it is called after the timeout expires, and from then on, returns FALSE until a new timeout is configured. The timeout may occur up to one tick earlier than expected, depending on the synchronisation between setting the timeout, and the actual timer tick. A one tick timeout will time out on the next tick that occurs after the timeout was set up, so if the timeout is set just after a tick has occurred, the timeout will occur nearly 54.9254 ms later, but if the timeout is set just before a tick, the timeout will occur almost immediately. See section ¯¯ 10.10 for more details. Because the tick count restarts at midnight, leaving a timeout active for a whole day will cause the timeout state to change. For example a ten minute timeout will expire after ten minutes, but every day thereafter, from the time that the timeout started, the timeout function will report not timed out for ten minutes. This demo program uses two timeout counters, and waits for ten keypresses. One timeout counter is used as a global timeout for the whole program, set to 20 seconds. The other timeout is used as a timeout for each individual keypress. To avoid both timeouts, you must press any key ten times within a total of 20 seconds, with no more than four seconds elapsing between the keys. So, it's a demo! I didn't say it would be useful. Call it a game of skill :-) -------------------------------- snip snip snip -------------------------------- /* Sample program #3 Demonstrates multiple timeouts using the BIOS tick count Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE3.C and compile with: bcc -I -L -ms sample3.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define NTIMEOUTS 2 /* Set this to however many timeouts you need */ #define GLOBAL_TIMEOUT 0 /* Counter number to use for global timeout */ #define CHAR_TIMEOUT 1 /* Counter to use for per-character timeout */ #define FALSE 0 #define TRUE 1 unsigned long timeoutstart[NTIMEOUTS]; /* Starting tick value per timeout */ unsigned int timeoutlength[NTIMEOUTS]; /* Timeout period (ticks) per timeout */ unsigned int timeoutflag[NTIMEOUTS]; /* Flags for timeout state */ #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL) #define TICK_WRAP 0x001800B0L /* Past last value of tick count */ unsigned long read_bios_tick_count(void) { unsigned long ct; asm pushf; asm cli; ct = * BIOS_TICK_COUNT_P; asm popf; return ct; } /* tick_diff(), returns the difference between two timer tick counts. This is just new value minus old value, except if the period crosses midnight. */ unsigned long tick_diff(unsigned long start_tick, unsigned long now_tick) { signed long diff; if (start_tick >= TICK_WRAP || now_tick >= TICK_WRAP) return 0xFFFFFFFFL; /* Invalid */ diff = now_tick - start_tick; if (diff < 0) diff += TICK_WRAP; return (unsigned long) diff; } /* Set a timeout counter for timeout after a specific number of ticks */ void set_timeout(unsigned int timeoutnum, unsigned int timeoutticks) { if (timeoutnum >= NTIMEOUTS) return; timeoutstart[timeoutnum] = read_bios_tick_count(); /* Start time */ timeoutlength[timeoutnum] = timeoutticks; /* Duration */ timeoutflag[timeoutnum] = FALSE; return; } /* Returns whether the nominated counter is in the timed-out state. After the timeout has expired, this function will return TRUE, until a new timeout period is set. Do not leave timeouts active for periods approaching one day, as this will cause the timeout state to be incorrectly reported as FALSE for the same period of each day. */ int has_timedout(unsigned int timeoutnum) { if (timeoutflag[timeoutnum]) return TRUE; /* Latch the timed-out state */ return (tick_diff(timeoutstart[timeoutnum], read_bios_tick_count()) >= timeoutlength[timeoutnum]); } /* Test whether a counter has just timed out. Returns TRUE only the first time it is called after the timeout occurs. */ int just_timedout(unsigned int timeoutnum) { if (timeoutflag[timeoutnum] == TRUE) /* Already reported timeout */ return FALSE; if (has_timedout(timeoutnum)) { /* Timeout has expired */ timeoutflag[timeoutnum] = TRUE; return TRUE; } return FALSE; /* Timeout has not expired yet */ } void main(void) { unsigned int n, key; printf("Sample program #3 - Demonstrates multiple timeouts using the BIOS tick count\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); printf("Press any key ten times\n"); printf("The timeout on each character is four seconds\n"); printf("The overall timeout on all ten characters is twenty seconds\n\n"); set_timeout(GLOBAL_TIMEOUT, 364); /* Global timeout 20 sec */ for (n = 0; n < 10; ++n) { /* Read ten characters */ set_timeout(CHAR_TIMEOUT, 73); /* Char timeout 4 sec */ while (TRUE) { if (just_timedout(CHAR_TIMEOUT)) { printf("Timed out on single character\n"); exit(1); } if (just_timedout(GLOBAL_TIMEOUT)) { printf("Global timeout expired\n"); exit(2); } if (bioskey(1)) { key = bioskey(0); break; } } printf("Key pressed: %c\n", key); } printf("Neither timeout expired; normal program termination"); exit(0); } -------------------------------- snip snip snip -------------------------------- ## 4.8 SIMPLE DELAYS USING THE BIOS TICK COUNT A simple way to implement delays of about 0.1 seconds or longer with one tick resolution, or perform timeout checking, is to provide a function that waits for a tick to occur, such as the following function: -------------------------------- snip snip snip -------------------------------- void wait_next_tick(void) { static unsigned int last_tick_loword; unsigned int now_tick_loword; do { now_tick_loword = * ((volatile unsigned int far *) 0x0040006CL); } while (now_tick_loword == last_tick_loword); last_tick_loword = now_tick_loword; return; } -------------------------------- snip snip snip -------------------------------- This function can then be called in a loop, e.g. for (n = 0; n < 10; ++n) wait_next_tick(); to implement a delay of the desired number of ticks. A modified method can be used to implement timeout checking with regular polling of some input device such as a serial port buffer, or the keyboard. There is no need to use the hiword of the BIOS tick count variable; just checking for a change in the loword is enough to detect that a tick has occurred. ## 5 SPECIAL SOFTWARE PRECAUTIONS If your program intercepts any interrupt vectors (e.g. int 8 or int 1Ch), or reprograms the RTC or other hardware into a strange mode, it must restore hardware states and interrupt vectors (i.e. clean up) if terminated by DOS, or risk having its interrupt handlers overwritten by another program and causing a system crash or causing incorrect operation due to the hardware being in the wrong state. You should handle the following interrupts: þ DOS Ctrl-C interrupt þ DOS Critical Error interrupts þ Divide Overflow interrupt (optional) Here are the gory details. For more details try DOS technical books such as the MS-DOS Encyclopedia or Ralf Brown's venerated Interrupt List (see section ¯¯ 12 for details of both of these references). ## 5.1 THE CTRL-C AND CTRL-BREAK INTERRUPTS Int 23h is the DOS Ctrl-C interrupt. It is invoked by DOS whenever a Ctrl-C character (ASCII code 3) is detected in the keyboard input stream. When the Ctrl-Break combination is pressed, the BIOS issues int 1Bh (the Ctrl-Break interrupt), and DOS's int 1Bh handler sets an internal flag in DOS that causes a faked Ctrl-C to appear in the input. Thus, by trapping Ctrl-C, you are trapping Ctrl-Break too, except that Ctrl-C will only be registered when DOS input is read, while Ctrl-Break is generated as soon as the keystroke is accepted. Also, if input redirection is used, the Ctrl-C interrupt may not be registered properly, depending on the 'BREAK=' setting in CONFIG.SYS. ## 5.2 HANDLING THE CTRL-C INTERRUPT You can just replace the default int 23h handler using setvect() (DOS function 25h), there's no need to save the previous vector contents because DOS will restore the vector for you when the program exits or is terminated. However, if you intercept int 1Bh as well as int 23h, DOS will not restore int 1Bh when your program terminates, so your program will have to do this itself. Typical actions for a Ctrl-C interrupt handler include: þ Do nothing and rely on the Ctrl-C appearing in the keyboard input stream to the program (this will only happen if the program reads its keyboard input via DOS, not via the BIOS), þ Set a flag which will be checked by the program's mainline and will cause the mainline to take some appropriate action (e.g. clean up and terminate the program), þ Call a general 'user interruption' function inside the main portion of the program, which registers the Ctrl-C request, and/or takes an appropriate action, þ Restore interrupt vectors, restore normal hardware states, clean up, and terminate the program immediately, by itself. All DOS functions can be called from within a Ctrl-C interrupt handler. Some C library functions may not be safe to call - for instance, the function which was reading the DOS keyboard input when the Ctrl-C was detected, will be in progress and might not be re-entrant - see your compiler's library reference for details. On entry to the Ctrl-C interrupt handler, interrupts will be disabled, and it would normally be appropriate to enable them, using enable() or STI, unless the handler will always return quickly. If the handler returns control to DOS, an IRET instruction should be used. There is no return value. General registers may be modified by the Ctrl-C handler. Alternatively, the handler may call DOS to terminate the program (e.g. via DOS function 4Ch, terminate with return code). ## 5.3 THE CRITICAL ERROR INTERRUPT Int 24h is the DOS Critical Error interrupt, and is issued by DOS when a device driver indicates a critical failure. The default critical error handler issues the familiar "Abort, Retry, Ignore?" prompt. You can replace the default handler using setvect() (DOS function 25h). DOS will restore the vector for you when the program exits or is terminated. On entry to the int 24h handler, registers AX, SI, DI, BP contain information about the nature of the critical error, and the stack contains the values in all registers as provided to the int 21h call which caused the critical error to occur, as well as the return address for the int 21h call. For these reasons, int 24h handlers are usually written in assembler. ## 5.4 CRITICAL ERROR HANDLER PARAMETERS On entry to the int 24h handler, the stack is arranged thus: [SS:SP+0] IP (PC) of return address for int 24h handler [SS:SP+2] CS of return address for int 24h handler [SS:SP+4] Flags for return of int 24h handler [SS:SP+6] AX as provided to int 21h invocation [SS:SP+8] BX as provided to int 21h invocation [SS:SP+0Ah] CX as provided to int 21h invocation [SS:SP+0Ch] DX as provided to int 21h invocation [SS:SP+0Eh] SI as provided to int 21h invocation [SS:SP+10h] DI as provided to int 21h invocation [SS:SP+12h] BP as provided to int 21h invocation [SS:SP+14h] DS as provided to int 21h invocation [SS:SP+16h] ES as provided to int 21h invocation [SS:SP+18h] IP (PC) of return address for int 21h invocation [SS:SP+1Ah] CS of return address for int 21h invocation [SS:SP+1Ch] Flags for return from int 21h invocation On entry to the int 24h handler, BP:SI contain the segment:offset address of the device driver header of the device which flagged the critical error. The high eight bits of the DI register are undefined. The lower eight bits of DI contain the error description, as follows: 0 = Write-protected disk, 1 = Unknown unit, 2 = Drive not ready, 3 = Invalid command, 4 = Data error, 5 = Invalid request structure length, 6 = Seek error, 7 = Non-DOS disk, 8 = Sector not found, 9 = Out of paper (printer), 10 = Write fault, 11 = Read fault, 12 = General failure, 15 = Invalid disk change (DOS 3.0 and later). Many of these error codes are not applicable to character devices. If bit 7 of AH is set, the critical error occurred on a character device (e.g. PRN or AUX), and all other bits of AX are undefined. If bit 7 of AH is clear, the critical error occurred on a block device (i.e. a disk drive), and the error location is described by the remaining bits in AX. AL contains the drive designator minus 41 hex (i.e. 0 means drive A, 1 means drive B, 2 means drive C, etc). AH describes the error location, as follows: 7 6 5 4 3 2 1 0 * . . . . . . . 0 (Error occurred on block device) . * . . . . . . Not used . . * . . . . . "Ignore" allowed? (0 = no, 1 = yes) (3.1+) . . . * . . . . "Retry" allowed? (0 = no, 1 = yes) (3.1+) . . . . * . . . "Fail" allowed? (0 = no, 1 = yes) (3.1+) . . . . . * * . Location: 00=DOS, 01=FAT, 10=Root, 11=Files . . . . . . . * Read or Write operation (0 = read, 1 = write) Bits 3, 4, and 5 are only meaningful if the DOS version is 3.1 or later. The DOS version may be checked from inside the critical error handler, using DOS function 30 hex, or it may be determined by startup code and stored in a global variable accessible by the critical error handler. ## 5.5 CRITICAL ERROR HANDLER OPERATION The critical error handler may use DOS functions 01 through 0Ch (the old CP/M character I/O functions). It may also use DOS functions 30h and 59h (request DOS version, and request extended error information). Other DOS functions may NOT be called, as DOS is mostly non-reentrant. The critical error handler must preserve all register values, except the flags (presumably) and AL, which is used to specify the action for DOS to take upon return from the handler, as follows: 0 = Ignore 1 = Retry 2 = Abort 3 = Fail current function Ideally, a critical error handler built in to a program should also deallocate any other resources that that program might have allocated, such as EMS and/or XMS memory. Temporary files cannot be safely deleted because the DOS file functions must not be called. Possibly if the handler is going to abort the program anyway, it may be safe to call these functions. If anyone has detailed info, please let me know. (*) ## 5.6 THE DIVIDE OVERFLOW INTERRUPT The divide overflow interrupt is int 0. It is generated by the processor when the quotient of a signed or unsigned integer division (IDIV or DIV instruction) would exceed the size of the register into which it would be placed. DOS's default divide overflow handler issues the message "Divide overflow" and terminates the current application, giving the program no chance to restore interrupt vectors, hardware states, or allocated resources, or close files, etc. Generally if a divide overflow occurs, the user should reboot their system. As a result (or perhaps the cause) of this, most programs do not provide their own divide overflow interrupt handlers. If you wish to handle divide overflows, I would suggest using direct writes to the interrupt vector table in low memory to restore all intercepted vectors (except int 23h and 24h, which will be restored by DOS), restoring the hardware state directly, and perhaps deallocating any resources such as EMS and/or XMS, then calling DOS function 4Ch to terminate the program. It might be possible to write a divide overflow handler which resumes execution after loading an appropriate value into the result register. This requires scanning the offending instruction, and a detailed knowledge of the operation of the various x86 processors, and is left as an exercise for the reader :-) ## 5.7 ERROR HANDLING SYSTEM The error handling system I have used in the sample programs uses a function prototyped as follows: void abort_cleanup(int dos_is_safe); This function is responsible for performing as much cleanup as possible at program exit time. The dos_is_safe parameter specifies whether DOS functions may be safely used by the cleanup function. This parameter will be FALSE if the function is called from within the critical error handler or a divide overflow handler, and TRUE if the function is called from the Ctrl-C handler or by the program itself during cleanup for an orderly exit. abort_cleanup should not exit to DOS itself. This will be done by the caller. If dos_is_safe is FALSE, your abort_cleanup() function should not call any DOS functions. Interrupt vectors should be restored using direct accesses into the interrupt table in low memory (though this technique is frowned upon). Depending on the types of cleanups required, DOS may _have_ to be called. In certain circumstances (e.g. after a divide overflow), abort_cleanup() may crash the machine in its attempt to clean up properly. On the other hand, if it did not attempt to clean up properly, the machine might be left in an unstable state anyway. You will have to weigh up the pros and cons when deciding how much to try to clean up if dos_is_safe is FALSE. Perhaps you should do the most critical and/or most likely to succeed cleanups first. ## 5.8 SAMPLE CODE MODULE: CRITICAL ERROR HANDLER MODULE -------------------------------- snip snip snip -------------------------------- NAME CRIT_ERR ; Rudimentary critical error handler module ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; ; This module provides rudimentary critical error (int 24h) handling for DOS ; application programs. It is callable from Borland C. This file is written ; for small model (near code, near data). You can change the FAR_CODE equate ; to hopefully make it compatible with other memory models. ; ; This is a minimal implementation for demonstration purposes. ; ; Upon startup, the application should call crit_err_intercept() to install the ; new critical error handler. No corresponding uninstallation is required. ; ; If the user selects the Abort option to the "Abort, Retry, Ignore" prompt, ; the abort_cleanup() function (which is provided externally) is called, with ; its dos_is_safe parameter set to FALSE. This function performs as much ; cleanup as possible (restoring interrupt vectors, restoring hardware states, ; setting normal text video mode, and deallocating resources such as EMS and ; XMS memory. It would also delete temporary files and close any open files if ; dos_is_safe were TRUE. After abort_cleanup() returns, the critical error ; handler returns the Abort code to DOS, which will then abort the program. ; ; Save this file to CRIT_ERR.ASM and assemble with: ; masm /Mx crit_err; ; or ; tasm /mx crit_err; ; to produce CRIT_ERR.OBJ which can be linked into the user program. FALSE EQU 0 TRUE EQU 1 FAR_CODE EQU FALSE ; TRUE for far code models ; void crit_err_intercept(void); PUBLIC _crit_err_intercept ; unsigned int is_at_crit_prompt(void); PUBLIC _is_at_crit_prompt ; void abort_cleanup(int dos_is_safe); IF FAR_CODE EXTRN _abort_cleanup : FAR ELSE EXTRN _abort_cleanup : NEAR ENDIF _DATA SEGMENT _DATA ENDS DGROUP GROUP _DATA _TEXT SEGMENT PARA PUBLIC 'CODE' ASSUME cs:_TEXT ; Data - in code segment (naughty naughty) I24_IP DW 0 ; IP for return from int 24h intercept I24_CS DW 0 ; CS for same I24_FL DW 0 ; Flags for same In_Crit DB 0 ; Flag whether currently at int 24h prompt IF FAR_CODE _crit_err_intercept PROC far ELSE _crit_err_intercept PROC near ENDIF ; This function intercepts interrupt 24h, and replaces the DOS default int 24h ; handler with the new handler, crit_err_handler. This function should be ; called ONCE and ONLY ONCE at program startup. No corresponding restore- ; interrupt function is required. mov ax,3524h ; Request int 24h int 21h mov cs:[O24_IP],bx ; Self-modifying code? mov cs:[O24_CS],es ; Where? I didn't see it :-) push ds push cs pop ds mov dx,OFFSET _TEXT:crit_err_handler mov ax,2524h ; Set int 24h int 21h pop ds ret _crit_err_intercept ENDP IF FAR_CODE _is_at_crit_prompt PROC far ELSE _is_at_crit_prompt PROC near ENDIF ; This function returns the status of the In_Crit flag, and should be called ; by the Ctrl-C interrupt handler (if any) to check that the Ctrl-C was not ; pressed while at the Abort, Retry, Ignore prompt. The function returns ; FALSE if not at the prompt, or TRUE if at the prompt. If it returns TRUE, ; the Ctrl-C handler is not safe to call general DOS functions. mov al,cs:[In_Crit] xor ah,ah ret _is_at_crit_prompt ENDP crit_err_handler PROC far ; This function handles interrupt 24 hex, the DOS Critical Error interrupt. ; It calls the original DOS interrupt 24h handler, and checks the returned ; action code. ; If the action code is 2 (abort), it calls abort_cleanup() (provided by the ; application), passing a FALSE value for the dos_is_safe parameter. In ; either case, it then returns the user-specified action code to DOS. ; ; See documentation above for details of the abort_cleanup() function. pop cs:[I24_IP] ; IP of return address of int 24h pop cs:[I24_CS] ; CS of return address of int 24h pop cs:[I24_FL] ; Flags of int 24h invocation mov cs:[In_Crit],1 ; Set flag pushf ; Simulate an INT DB 9Ah ; CALL xxxx:xxxx O24_IP DW 0 ; Offset of call (modified) O24_CS DW 0FFFFh ; Segment of call (modified) mov cs:[In_Crit],0 ; Clear flag cmp al,2 ; Did user choose Abort? jne NotAbort ; If not push es ; If so, call abort_cleanup() push ds push di push si push bp push dx push cx push bx push ax mov ax,SEG DGROUP mov ds,ax ; Set up DS for call to C function xor ax,ax ; dos_is_safe is FALSE! push ax call _abort_cleanup pop ax ; Discard parameter ; mov ax,0E07h ; Enable these lines during debugging ; xor bx,bx ; to generate a beep after your ; int 10h ; abort_cleanup() function completes pop ax mov al,2 ; Restore the Abort code pop bx pop cx pop dx pop bp pop si pop di pop ds pop es NotAbort: push cs:[I24_FL] ; Flags of int 24h invocation push cs:[I24_CS] ; CS of return address of int 24h push cs:[I24_IP] ; IP of return address of int 24h iret crit_err_handler ENDP _TEXT ENDS END -------------------------------- snip snip snip -------------------------------- Gian Uberto Lauri (saint@dei.unipd.it) sent me a modified version of this module with the names changed to support the Borland C++ 3.1 compiler when compiling a C++ program. Borland C++ encodes the parameter types in the function name ("mangling"). Gian had to change the names of the functions as follows: _crit_err_intercept --> @crit_err_intercept$qv _is_at_crit_prompt --> @is_at_crit_prompt$qv _abort_cleanup --> @abort_cleanup$qi These names must be changed both at the PUBLIC or EXTRN declarations, and at the actual PROC and ENDP lines. These changes are specific to Borland C++. Other C++ compilers will handle this differently. ## 6 INTERRUPTS An interrupt is an interruption to the processor, that causes it to stop what it is doing and jump to a specially written subroutine, known as an interrupt handler, interrupt routine, or interrupt service routine (ISR). There are three types of interrupts - processor-generated interrupts, external hardware interrupts, and software interrupts. Processor-generated interrupts are generated internally by the processor (Intel 80x86) in certain conditions, such as a division overflow (see section ¯¯ 5.6). External hardware interrupts are generated by IRQs, and are described shortly. Software interrupts are invoked by software, and are generally used for calling system functions, e.g. BIOS functions, DOS functions, mouse functions, EMS functions, etc. Interrupts are identified by an interrupt number, in the range 0 to 0FFh. ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ Interrupt numbers ³ Interrupt type ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ ³ 0,1,2,3,4 ³ Processor ³ ³ 5,6,7 ³ Software and processor ³ ³ 8-0Fh ³ Hardware (IRQ0-7) and processor ³ ³ 10h-6Fh ³ Software (some are also processor interrupts) ³ ³ 70h-77h ³ Hardware (IRQ8-15) ³ ³ 78h-0FFh ³ Software ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ Some low-numbered interrupts have a split personality, because IBM ignored Intel's "reserved for processor" comment on the first 32 interrupts. The original 8086/8088 only used ints 0, 1, 2, 3, and 4 for processor interrupts, so IBM used ints 5 and upwards for hardware and software interrupts. With later x86 processors, Intel reclaimed their reserved interrupts, requiring special support in the EMM386 driver to handle these interrupts properly. See section ¯¯ 6.7 for details. Tor Sjowall {TOR} points out that these conflicts only occur in real mode and virtual 86 mode. In protected mode, there is no such conflict - the processor interrupts have their Intel defined functions, the hardware interrupts are vectored through different interrupts, and the software interrupts are not relevant (since they relate to DOS and BIOS, which are not protected mode programs). Software interrupts 23h and 24h (Ctrl-C and Critical Error) are described in section ¯¯ 5 and subsections. Software interrupt 1Ch and hardware int 8 are the timer tick interrupts, and are described in section ¯¯ 6.1. ## 6.1 THE TIMER TICK INTERRUPTS Interrupt 8 and interrupt 1C hex are the timer tick interrupts. Int 8 is a hardware interrupt, invoked directly by IRQ0, from CTC channel zero, and is the highest priority IRQ (unless interrupt priorities have been changed from the BIOS defaults). The BIOS POST sets int 8 to point to the BIOS's int 8 interrupt service routine, traditionally located at F000:FEA5, which performs the delayed floppy disk motor turn-off and updates the system time-of-day. Device drivers and TSRs often intercept this interrupt, so often the vector won't point directly to the BIOS. Int 1C hex is issued (i.e. generated) by the BIOS's int 8 service routine, and normally points to an IRET instruction in the BIOS. Int 1Ch is intended to be used by application programs which require a regular interrupt source. Some TSRs also hook this interrupt - see section ¯¯ 6.35 for details. ## 6.2 INTERRUPT VECTOR TABLE The interrupt vector table, or IVT, is a reserved area of RAM occupying the bottom kilobyte of main memory, i.e. from 0000:0000 to 0000:03FF. This is in real mode or virtual 8086 mode, under DOS. In protected mode this is probably completely different. (*) Each interrupt has a corresponding four-byte far code pointer, located at interrupt number x 4 bytes into the IVT, which points to the interrupt service routine that will be invoked when that interrupt is registered by the processor. For example, here is a dump of the first 128 bytes of the IVT on my machine: 0000:0000 1A 00 70 00 05 00 70 00 1B 2C 5D 57 05 00 70 00 ..p...p..,]W..p. 0000:0010 05 00 70 00 54 FF 00 F0 4C E1 00 F0 6F EF 00 F0 ..p.T..pLa.poo.p 0000:0020 57 01 80 E6 AD 2B 5D 57 6F EF 00 F0 45 10 1F CF W..f-+]Woo.pE..O 0000:0030 6F EF 00 F0 6F EF 00 F0 57 EF 00 F0 6F EF 00 F0 oo.poo.pWo.poo.p 0000:0040 C6 01 80 E6 4D F8 00 F0 41 F8 00 F0 C0 05 A2 D1 F..fMx.pAx.p@."Q 0000:0050 39 E7 00 F0 18 00 55 02 20 01 B3 E5 D2 EF 00 F0 9g.p..U. .3eRo.p 0000:0060 D4 E3 00 F0 65 0F A2 D1 6E FE 00 F0 64 06 70 00 Tc.pe."Qn~.pd.p. 0000:0070 1B 91 A1 03 A4 F0 00 F0 22 05 00 00 6E 42 00 C0 ..!.$p.p"...nB.@ The vector for interrupt 1C hex starts at 1Ch x 4, which is 70 hex. In the above vector table contents, the vector at 0000:0070 points to 03A1:911B, so every time int 1Ch is issued, the processor will jump to 03A1:911B and execute the ISR that starts at that address. ## 6.3 INTERCEPTING AN INTERRUPT To take control of an interrupt, use the getvect() and setvect() functions or DOS functions 35 hex and 25 hex. If necessary, you can directly access the interrupt vector table in low memory (see section ¯¯ 6.2). This may be required if DOS cannot safely be called - for example, in a critical error handler (see section ¯¯ 5.3). Interrupts MUST be locked out while any direct manipulation of this type is performed. Start by requesting the contents of the interrupt vector, using getvect() or DOS function 35 hex. This gives a far code pointer, which must be stored to be reinstated when your program terminates. The stored 'old interrupt' vector is also used for interrupt chaining (section ¯¯ 6.31). Then, set the interrupt vector to point to your new handler, using setvect() or DOS function 25 hex, and away you go. See section ¯¯ 5 and subsections for details of intercepting the DOS Ctrl-C and critical error interrupts and the divide overflow interrupt, which must be done to ensure that your program reinstates the original interrupt owner upon exit. ## 6.4 INTERRUPT HARDWARE Hardware interrupts are known as IRQs (interrupt requests). They interrupt the processor from its current task and cause it to jump to an interrupt handler, aka interrupt service routine (ISR). The processor has only one IRQ input, which is expanded by an 8259 PIC (programmable interrupt controller). The PC and XT have one PIC, which provides IRQ0-7. IRQ0 and 1 are the timer tick and keyboard interrupts, respectively. IRQ2 through IRQ7 are available on the slot bus, for use by peripheral cards. The AT has two PICs - the primary PIC, which is equivalent to the single PIC on the PC and XT, and the secondary PIC (also sometimes called the slave PIC). The third input (IRQ2) on the primary PIC is known as the 'chain' or 'cascade' or 'slave' interrupt on the AT, because it is the method by which the secondary PIC issues an interrupt request. The slot bus connection that was IRQ2 on the PC is replaced by IRQ9 on the AT and later machines (ISA bus). The two PICs and their interconnection are shown in Figure 2 in the FIGURES archive. Each PIC is responsible for prioritising its incoming interrupt requests, and issuing an interrupt request signal to the processor - either directly (in the case of the primary PIC) or via the primary PIC (in the case of the secondary PIC). Hardware interrupts are registered on the rising edge of the PIC input, which corresponds to a rising edge of the IRQ line on the slot bus of ISA machines. This is known as rising edge triggered interrupts. Level triggered interrupts, particularly active low level triggered interrupts, are more sensible for most applications, and EISA machines are apparently configurable for either edge- triggered or level-triggered operation. The MicroChannel Architecture (MCA) bus, used in IBM PS/2 machines, uses level triggered interrupts. The PICs are accessed via two I/O locations. The primary PIC appears at I/O addresses 20h and 21h, the secondary PIC (not present on PC and XT) appears at I/O addresses 0A0h and 0A1h. The lower address is the command/status register, the upper address is the interrupt mask register (IMR). ## 6.5 IRQ TO INTERRUPT MAPPING The default mapping between hardware interrupt requests (IRQs) and interrupts is set up by the BIOS POST, and is as follows. ÚÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄ¿ ³ IRQ Int ³ IRQ Int ³ IRQ Int ³ IRQ Int ³ ÃÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄÅÄÄÄÄÄÄÄ´ ³ 0 8 ³ 4 0Ch ³ 8 70h ³ 12 74h ³ ³ 1 9 ³ 5 0Dh ³ 9 71h ³ 13 75h ³ ³ 2* 0Ah ³ 6 0Eh ³ 10 72h ³ 14 76h ³ ³ 3 0Bh ³ 7 0Fh ³ 11 73h ³ 15 77h ³ ÀÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÙ * Note IRQ2 is not usable directly except on the original PC and XT, which do not have IRQ8-15. The slot bus connection that was IRQ2 on the PC and XT is connected to IRQ9 on AT-class ISA machines. The BIOS default handler for IRQ9 (int 71h) invokes the IRQ2 (int 0Ah) handler for backwards compatibility. I don't know the details of this. (*) ## 6.6 INTERRUPT FLAG, INTERRUPT ACCEPTANCE, INTERRUPT NESTING When a hardware device requests an interrupt, the PIC tells the processor that an interrupt is pending. The processor has an 'interrupt enable' flag in the Flags (F) register, which determines whether the processor will respond to the interrupt request from the PIC. This flag is cleared and set by the CLI and STI instructions (respectively) or the disable() and enable() functions or pseudofunctions, which execute CLI and STI instructions (respectively). If the interrupt enable flag is clear, the processor will not action the interrupt request. In this state, the PIC will continue to examine its inputs, and keep evaluating which interrupt is the highest priority active interrupt request, leaving its interrupt request line to the processor in the active state. Interrupts are prioritised, with IRQ0 (the timer tick) being highest priority and IRQ7 being lowest priority. IRQ8-15 fit in the gap between IRQ1 (the keyboard scancode interrupt) and IRQ3 (normally used for COM2). This priority is determined by control bytes sent to the PICs by the BIOS initialisation code. It can be changed by reinitialising the PICs but I know of no program that does this. When the processor is able to accept the interrupt request, it pushes the flags and the CS and IP registers onto the stack, and clears the interrupt flag in the flags register, before allowing the PICs to decide which is the highest priority interrupt and provide the address of the interrupt vector. The processor then executes the interrupt handler for the highest priority pending IRQ. The interrupt routine ends with an IRET, which is like a RETF but also pops the flags, i.e. 'undoes' the automatic stacking done by the processor when the interrupt was registered. During execution of the handler, the PICs continue to evaluate the highest priority interrupt being requested, and if an interrupt with a higher priority than the one in progress comes along, they will issue another interrupt request to the processor. The processor will ignore this request unless the interrupt handler in progress has explicitly enabled interrupts, by executing an STI or enable(). In this case, if a higher priority interrupt is pending, the handler in progress will itself be interrupted, so that the higher priority interrupt can be serviced. On return from the higher priority interrupt handler, the lower priority handler will resume. If during servicing of an interrupt, a lower priority interrupt source comes along, or the same interrupt is retriggered, the PIC will not interrupt the processor. Once the handler in progress has terminated, the lower or same priority interrupt will be actioned. The PIC knows which interrupt level is in progress, because it triggered the interrupt itself. But it cannot tell when that interrupt level has been processed. The interrupt handler has to tell it, via the EOI command, see section ¯¯ 6.28. As the timer tick has the highest priority, care should be taken to ensure that it is as short and efficient as possible, because it cannot be interrupted, even if it enables interrupts. See section ¯¯ 6.9 for an exception to this rule. ## 6.7 EMM386 INTERRUPT INTERCEPTION EMM386 places the 80x86 into virtual 8086 mode and intercepts interrupts at a hardware level, i.e. through specific features of the 386 and later processors. This is different from intercepting interrupts at the vector level. The reason for this behaviour is that several interrupts serve dual purposes - they are IBM-allocated hardware or software interrupts, but are also Intel-allocated processor interrupts known as processor exceptions (section ¯¯ 6 introductory). In real mode, the 80x86 will not generate these new internal interrupts, and behaves like an 8086/8088, 80186, or 80286, but EMM386 must put the 80x86 into virtual 8086 mode, so that it can use the paging facilities of the 386/486/586 to remap memory, etc. In virtual 8086 mode, these exceptions may occur. However, DOS and BIOS functions are designed assuming real mode, and do not expect to be called when these exceptions occur, therefore EMM386 must intercept these interrupts and when they occur it must determine whether the interrupt is a real-mode interrupt (in which case it invokes the appropriate interrupt handler via its vector) or a processor exception (in which case it displays a friendly message asking whether you want to terminate the program, then usually locks up the machine regardless :-) The extra time required for EMM386 to determine the interrupt type adds a significant amount of overhead to each interrupt, as demonstrated by the example in section ¯¯ 6.8 (software interrupts) and the sample program in section ¯¯ 10.16 (hardware interrupt). If anyone has more insight into EMM386 and its effects on interrupts, please let me know. (*) ## 6.8 AVOIDING EMM386 OVERHEAD Because EMM386 intercepts the interrupt at the hardware level, it can be bypassed by calling the interrupt handler directly via its interrupt vector, avoiding the actual INT instruction that will be intercepted by EMM386. This applies to software interrupts (i.e. function interrupts for BIOS, DOS, EMS, mouse, etc functions) only. The EMM386 overhead on hardware interrupts (IRQs) cannot be bypassed. When doing this manually, care must be taken to ensure that the processor is in the correct state as expected by the interrupt handler. This involves ensuring that the interrupt flag is clear. Quite a lot of messing around is required for a generic solution that preserves all ingoing registers including the flags (apart from the interrupt flag, of course), as shown in the following code section, which demonstrates how to call a software interrupt directly thus bypassing the EMM386 overhead: -------------------------------- snip snip snip -------------------------------- IntNum EQU 10h ; Interrupt to be invoked pushf sub sp,8 push bp push ax push ds mov bp,sp push WORD PTR [bp+14] ; Take a copy of the flags mov [bp+12],cs ; Segment of return address mov WORD PTR [bp+10],OFFSET ReturnPoint ; Offset of same xor ax,ax mov ds,ax ; Address interrupt vector table mov ax,ds:[(IntNum SHL 2) + 2] ; Segment of handler mov [bp+8],ax mov ax,ds:[IntNum SHL 2] ; Offset of handler mov [bp+6],ax popf pop ds pop ax pop bp cli retf ; 'RETF' to handler ReturnPoint: ; Continue -------------------------------- snip snip snip -------------------------------- This code section is rather convoluted. It sets up a stack frame as follows: BP+... Contains 14 Flags at subroutine entry 12 Segment of ReturnPoint 10 Offset of ReturnPoint 8 Segment of handler 6 Offset of handler 4 BP 2 AX 0 DS -2 Flags (copy of BP+16) A program to compare the speed of 50000 loops calling video BIOS functions 0Eh (teletype output) twice, 3 (request cursor position and size), and 8 (read character and attribute at cursor), first using an INT 10h to call the BIOS function and then using the above code, gave the following results on my 486DX2-66: EMM386 Method Time Speed (relative) Not present Int 10h 838730 100% (normalised) Not present Above code 991420 84.6% Present Int 10h 2084600 40.2% Present Above code 1440275 58.2% These results show that installing EMM386 slows the system significantly, but by calling the interrupt directly, some of the overhead is removed. The speed improvement gained by calling the interrupt instead of issuing an INT instruction, when EMM386 is installed, is 44.7%. Without EMM386 installed, calling the interrupt is slower than using INT, because of the messy stack manipulation. If anyone has any comments on these findings, please let me know. (*) ## 6.9 LONG TIMER TICK INTERRUPT HANDLERS A special method may be used if the timer tick interrupt handler must take a long time. Interrupts may be enabled and an EOI sent to the PIC, making the PIC think that the timer tick interrupt has been fully serviced. This allows lower priority interrupts to be serviced and handled in the normal way (but see section ¯¯ 6.9.1), thus preventing problems with the keyboard, mouse, and serial I/O, but of course this means that another timer tick interrupt could come along while the current handler is in progress. This will cause the int 8 handler to be re-entered unless the condition is detected using a flag or 'semaphore'. If on entry to the interrupt handler the semaphore is set, the interrupt handler must send an EOI and exit, or exit by chaining to the original handler. Here is an example timer tick interrupt intercepter that hooks into int 8 and implements this technique. Note that the Int8Sem and TriggerFlag variables appear in the code segment, to allow them to be accessed via CS (this avoids wasting time manipulating DS). -------------------------------- snip snip snip -------------------------------- ASSUME cs:_TEXT ; Current code segment name ASSUME ds:nothing,es:nothing,ss:nothing Int8Sem DB 0 ; Int 8 in progress semaphore TriggerFlag DB 0 ; Flag to trigger long function NewInt08 PROC far ; Int 8 intercepter pushf ; Preserve flags cli ; Make sure interrupts are off cmp Int8Sem,0 ; Check whether we're already busy jnz GoOld08 ; If so, don't do anything ; Decide here whether the long function should be performed. If so, ; branch to DoLongFunc, otherwise continue. TriggerFlag must be set ; by some external routine in order to trigger the background function. ; TriggerFlag will be reset to zero by the function when it completes. cmp TriggerFlag,0 ; Time to perform the function? jnz DoLongFunc ; If so ; Idle - just jump to old handler GoOld08: popf ; Fix stack DB 0EAh ; JMP xxxx:yyyy Old08Ofs DW 0 ; Vector to original handler - Offset Old08Seg DW 0 ; Segment ; Time to perform the long function DoLongFunc: inc Int8Sem ; Set busy flag pushf ; Simulate stack for an INT call DWORD PTR Old08Ofs ; Chain to old handler (sends EOI) sti ; Enable interrupts push dx ; Preserve push cx ; Preserve push bx ; Preserve push ax ; Preserve ; -- Insert code here to perform your long function. Preserve any other ; registers that you will use, using PUSH and POP. Note the asynchronous ; interrupt handler restrictions still apply (this routine cannot call DOS, ; etc), but this code may take as long as necessary. ; -- Your code goes here pop ax ; Restore pop bx ; Restore pop cx ; Restore pop dx ; Restore mov Int8Sem,0 ; No longer busy mov TriggerFlag,0 ; We have triggered popf ; Restore flags pushed at start of int 8 iret ; Finally, return to application NewInt08 ENDP -------------------------------- snip snip snip -------------------------------- This approach can be thought of as an interrupt-triggered 'branch' to another section of code. Once this interrupt intercepter calls the old handler to send the EOI and enables interrupts, it effectively becomes the 'mainline' and runs 'in the foreground', itself being interrupted by other interrupts of any priority. After completing its function, it may return control to the point where the interrupt interrupted execution, or it may choose not to do so (though this requires careful programming). Generally the interrupt handler restrictions still apply, because you do not know what the machine was doing at the time that the interrupt occurred. If used in a TSR, this technique can cause another problem. If another program hooks int 8, and chains to our interrupt handler using the CALL method so it can regain control after chaining, that program's interrupt handler may be called recursively. There are no formal guidelines for writing TSRs (at least, not at this level of depth) so I don't know how this should be handled. Anyone? (*) There _are_ TSRs that use this technique - I believe DOS's PRINT program does this - and it may have implications on int 8 handlers which chain to the original handler using the CALL method (see section ¯¯ 6.31). (*) Manipulation of semaphores is usually done using indivisible instructions, such as the XCHG instruction, so that it's not possible for the code to be re-entered between the semaphore test, and the semaphore set. In this case, that section of code is uninterruptible, because interrupts are explicitly locked out, so an indivisible instruction is not required. The explicit CLI instruction should not be needed, as the interrupt flag is turned off when the processor branches to the interrupt service routine, but it is good practice to explicitly disable interrupts here, as there are some programs which intercept int 8 but call the original handler with interrupts enabled. ## 6.9.1 DANGER OF LONG TIMER TICK INTERRUPT HANDLERS There is one further problem with this technique. If a lower priority interrupt was being handled while the int 8 occurred, and is still in progress, it will not complete and send its EOI until the int 8 handler completes and returns. Therefore, that interrupt and all lower priority interrupts, are locked out for the duration of the extended int 8 code. In some cases, it may be useful to poll the interrupt controller's In Service Register at the start of the int 8 handler, and if any other interrupts are already being serviced, do not do the extended code. If the interrupt handler _must_ execute the extended code regardless of whether any lower priority interrupts are being serviced, this may have an impact on other system functions. Ideas anyone? (*) ## 6.10 INTERRUPT MASK REGISTER Hardware interrupts may be masked individually via the Interrupt Mask Registers (IMRs) in the 8259 PIC chips. Each PIC has an 8-bit IMR, in which each bit corresponds to an IRQ line. Bits 0-7 in the primary PIC's IMR correspond to IRQ0-7 respectively, and bits 0-7 in the secondary PIC's IMR correspond to IRQ8-15 respectively. If IRQ2 is masked off in the primary PIC, this masks off IRQ8-15, as they are signalled by the secondary PIC via this cascade input on the primary PIC. Therefore, when enabling any IRQ on the secondary PIC (i.e. IRQ8 through 15), you should also explicitly enable IRQ2 on the primary PIC. If the bit in the IMR is set, the interrupt is _masked_ (i.e. disabled). This is the opposite of the interrupt enable flag in the processor, which is set to _enable_ interrupts. Setting a bit in the IMR _masks_ (prevents) the interrupt. The IMR is a read/write register and can be accessed at I/O address 21h (primary PIC) or 0A1h (secondary PIC). See section ¯¯ 6.11 for code examples. The PIC also contains an interrupt request register (IRR) and an in-service register. These can be read by issuing the appropriate read-back command to the PIC and then reading the command/status register at I/O address 20h (primary PIC) or 0A0h (secondary PIC), see sections ¯¯ 6.12 and ¯¯ 6.13. ## 6.11 ENABLING AND DISABLING THE TIMER TICK INTERRUPT Interrupt 8 can be enabled or disabled via bit zero of the interrupt mask register (IMR) in the primary 8259 PIC, at I/O address 21h. Each bit in this register controls the correspondingly numbered IRQ, and int 8 is IRQ0. Setting the bit _masks_ or _disables_ the interrupt, thus the name 'interrupt _mask_ register'. Disable interrupts using disable() or CLI around accesses to the IMR. Here are two sample subroutines to control int 8. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- DisableInt8: ; Destroys AL only pushf cli in al,21h or al,1 out 21h,al popf ret EnableInt8: ; Destroys AL only pushf cli in al,21h and al,0FEh out 21h,al popf ret -------------------------------- snip snip snip -------------------------------- ## 6.12 READING THE INTERRUPT REQUEST REGISTER The IRR in the primary 8259 PIC can be read with the following code fragment. It returns the IRR value in AL. -------------------------------- snip snip snip -------------------------------- ReadPIC0IRR: ; Returns IRR in AL pushf cli mov al,0Ah ; Read IRR command out 20h,al jmp SHORT $+2 in al,20h ; Read the IRR value popf ret -------------------------------- snip snip snip -------------------------------- If you know that no other software is going to be accessing the PIC, for example if you are reading the IRR in a loop with interrupts locked out, you can skip sending the 0Ah to port 20h before every read of the IRR. The PIC remembers that the IRR is selected to appear on a read of port 20h. So you would send the 0Ah to port 20h at the start of the loop, then just read port 20h every time through the loop. The same routine can be adapted to read the secondary PIC, just access port 0A0h instead of port 20h. ## 6.13 READING THE INTERRUPT IN SERVICE REGISTER The ISR (In Service Register, not Interrupt Service Routine) in the primary 8259 PIC can be read with the following code fragment. It returns the In Service Register value in AL. -------------------------------- snip snip snip -------------------------------- ReadPIC0ISR: ; Returns ISR in AL pushf cli mov al,0Bh ; Read ISR command out 20h,al jmp SHORT $+2 in al,20h ; Read the ISR value popf ret -------------------------------- snip snip snip -------------------------------- The In Service Register tells you what other interrupts are 'in service'. For example, if a serial port interrupt occurred on IRQ4, and during processing of that interrupt (before the EOI was sent to the PIC), a keyboard interrupt on IRQ1 occurred, and again during processing of that interrupt, the timer tick interrupt came along, then the In Service Register would contain 00010011 binary if read inside the timer tick interrupt, indicating that IRQ4, IRQ1, and IRQ0 are currently 'in service', i.e. their handlers are in progress and are nested. If in the above example, the IRQ4 handler completed and sent its EOI to the PIC before the IRQ1 occurred, its bit would not be set in the In Service Register. Because only higher priority (lower-numbered) interrupts can interrupt an interrupt handler (unless it sends an EOI early, in which case it is no longer "in service"), any interrupts flagged in the In Service Register must have occurred one after the other in order, from highest IRQ number to lowest IRQ number. The same routine can be adapted to read the secondary PIC, just access port 0A0h instead of port 20h. ## 6.14 WHEN YOU SHOULD DISABLE INTERRUPTS Generally, your program should disable interrupts around a sequence of accesses to an I/O device (such as the PIC, CTC, VGA chips, etc) to ensure that an interrupt service routine does not come along during your access sequence and access the chip, disrupting your access sequence. Interrupts should also be locked out when reading or writing variables that may be being accessed by an interrupt routine, unless you carefully design your communication with the interrupt routine so that this is not necessary. In particular, accesses to peripherals such as the RTC and CRTC (CRT controller) which have an address register and a data register, must be made with interrupts disabled, as if interrupts are enabled during such accesses, an interrupt handler could access the device and change the address register after your code had set the address but before your code had accessed the register, causing your code to access the wrong register, with possibly disastrous results - e.g. an ex monitor :-) If this results in interrupts being locked out for less than ten microseconds at a time, this will be acceptable for all normal applications. It might not be acceptable if the timer tick is running very much faster than usual and low jitter is needed - see section ¯¯ 6.15. Even when no address register is involved, interrupts should still be locked out over access sequences. For example, with interrupts enabled, the sequence in al,21h or al,1 out 21h,al looks innocent enough, but what happens if an interrupt is triggered between the IN and the OUT, and the interrupt routine also modifies the IMR, to turn on, or turn off, an unrelated interrupt? The interrupt handler will do its thing, but as soon as it returns, the IMR will be clobbered by an old copy of the IMR with bit 0 set, breaking the changes made by the interrupt handler. ## 6.15 WHEN YOU SHOULDN'T DISABLE INTERRUPTS These guidelines apply to DOS and any similar single-tasking operating system. The maximum length of time that interrupts can safely be locked out for depends on the operating environment. Although there are no formal guidelines, I would suggest 100 microseconds as a reasonable limit for good performance, and a few milliseconds as a sensible upper limit. If high speed timer tick interrupts or high speed serial communication are being used, the limit will typically be much lower, depending on the required interrupt rate, to avoid missing interrupts altogether. If you require accurately timed interrupt delivery, beware that disabling interrupts for even a short length of time will cause 'interrupt delivery jitter' (thanks {JAM} for the term :-) - i.e. occasionally the interrupt will be delayed slightly. See section ¯¯ 6.16 for details. Locking interrupts out for more than 50 ms continuously may cause missed timer ticks and problems with the keyboard and network (if present), at least. If a fast timer tick interrupt (see section ¯¯ 8 and subsections) is being used, or another demanding high speed interrupt such as high speed serial reception, it is easier to miss an interrupt (or lose data). If a hardware interrupt is missed because interrupts are locked out, the PIC does not generate an extra interrupt. ## 6.16 CAUSES OF INTERRUPT DELIVERY JITTER AND FAST TICK LOSS Interrupt delivery jitter ({JAM}'s term) occurs when interrupt acceptance is delayed, i.e. there is an unusual or inconsistent delay between the interrupt being signalled at the hardware level, and the processor starting execution of the interrupt handler, and the interrupt is serviced late. This happens for three reasons: þ Interrupts are locked out (processor's interrupt flag is clear) þ Equal or higher priority interrupt in progress (see section ¯¯ 6.6) (not normally applicable to the timer tick interrupt) þ Instruction or DRAM refresh in progress (contributes a very small amount of jitter, and are unavoidable) The first reason is the usual reason for interrupt delivery jitter on the timer tick interrupt (int 8 and int 1Ch). The first and second reasons are the usual cause of interrupt delivery jitter on other interrupt sources. For the normal 18.2065 Hz timer tick interrupt, this causes the delivery of interrupts to be uneven (i.e. to jitter slightly), in either a random or a partly random, partly cyclic manner. This is not usually a problem, as the low resolution timer tick is not used when timing requirements are critical. Interrupt jitter can also affect cases where the timer interrupt itself is not delayed; for example if an absolute timestamp (see section ¯¯ 9) is being used to timestamp serial data received under interrupt or some other occurrence that is signalled via an interrupt, if that interrupt is delayed, the timestamp will reflect the time that the interrupt was serviced, rather than when it was signalled by the serial chip. If an interrupt must be actioned within a short length of time, for example a serial received character interrupt or a fast tick interrupt (used when the tick rate is increased), delayed interrupt acceptance may result in a missed interrupt. If this occurs, the PIC does not generate an extra interrupt, in other words, the whole interrupt is lost, and this results in a cumulative timekeeping error unless the condition is detected and handled specially (see section ¯¯ 6.17). There are three main causes of interrupt delivery jitter due to interrupts being locked out: þ Real (hardware) interrupts þ Software interrupts þ Interrupts disabled while accessing hardware or volatile variables These causes are now described individually. ## 6.16.1 INTERRUPT DELIVERY JITTER DUE TO REAL INTERRUPTS Real interrupts include the timer tick (int 8), keyboard scancode (int 9), serial communication (if enabled) (including serial and bus mouse), RTC (int 70h) (if enabled), and network card (if present) interrupts. The handlers for all of these interrupts (except the timer tick) should re-enable interrupts quickly so that higher priority interrupts including the timer tick are not delayed for long, but some handlers do not enable interrupts because of bad design or deliberately, due to other considerations. Also EMM386 imposes extra overhead during interrupt acceptance; during this time another interrupt cannot be accepted, I think. (*) For example, if a network card interrupt handler on IRQ3 (for example) does not enable interrupts, and this interrupt is invoked on every network data block received by the machine, then every time a block of data is received, interrupts are locked out for, perhaps, 100 us. If a timer tick interrupt is signalled during this time, its acceptance will be delayed for up to 100 us, and interrupt delivery jitter occurs. Also, {JAM} points out that screen savers, which typically hook int 8, often clear the whole screen with interrupts disabled, resulting in a very long int 8 every once in a while when the screen saver 'kicks in'. Many other programs also intercept int 8 and will increase the amount of time occupied by each int 8 and/or by occasional int 8 invocations - network software uses int 8 as a timebase for timeout detection {JAM}, mouse drivers use it, and lots of pop-up and non-pop-up TSRs also use int 8. {TOR} points out that some BIOSes can also be the culprit: > The usual BIOS implementation of the keyboard interrupt and the floppy drive > interface are among the worst for blocking interrupts. I have actually > seen a keyboard driver [int 9 handler] issue its buffer full 'beep' with > interrupts locked out... ## 6.16.2 INTERRUPT DELIVERY JITTER DUE TO SOFTWARE INTERRUPTS Every time a software interrupt is issued, the processor disables interrupts before executing the interrupt handler. Well-behaved software interrupt handlers re-enable interrupts immediately on entry, but not all software interrupt handlers are well-behaved. In any case, there is a short length of time during which interrupts are disabled, and this time is lengthened if EMM386 is installed, because EMM386 intercepts the interrupt at a hardware level, and has to work out whether the interrupt is software-generated or is processor-generated, because several low-numbered interrupts are both processor exceptions and software interrupts. Therefore, every time your program or any code called by your program issues a software interrupt, interrupts are locked out for a short time, and possibly a long time if the interrupt handler is badly written or if several programs (e.g. TSRs) have intercepted that interrupt and many interrupt chains are performed before the request reaches its actual handler. Other software interrupts which may spend a significant length of time with interrupts locked out are: þ Screen scrolling via the BIOS þ Hard drive read, write, and seek accesses (possibly) þ Network accesses þ Mouse driver function calls þ EMS function calls þ XMS function calls þ Potentially, any code you did not write yourself! ## 6.16.3 INTERRUPT DELIVERY JITTER DUE TO HARDWARE ACCESSES Often, interrupts must be disabled manually, using CLI, around access sequences to hardware devices (see section ¯¯ 6.14) or accesses to volatile variables that may be modified by a hardware interrupt handler. If an interrupt is flagged during the short time that interrupts are locked out, it will be delayed until interrupts are re-enabled, causing interrupt delivery jitter. There are also other reasons why software might disable interrupts, usually (but not always) for short periods only. If your program requires very low jitter, it will probably have to do everything itself, because it cannot call any normal BIOS or DOS functions! ## 6.16.4 AVOIDING INTERRUPT DELIVERY JITTER If your application must run with a very fast timer tick interrupt, or must have very low interrupt jitter for whatever reason, it must avoid all of the causes of interrupt jitter described in sections ¯¯ 6.16 through ¯¯ 6.16.3. Interrupt jitter due to instruction execution (i.e. the interrupt cannot be accepted until the instruction in progress is completed) is unavoidable, but could probably be reduced by using short instructions and avoiding prefixes. Other causes of interrupt delivery jitter must be avoided for good results. This comes down to the following restrictions: þ Disable all hardware interrupt sources via the PIC(s) except the interrupt source you are using þ Do not issue software interrupts þ Do not call code over which you do not have control þ Do not chain to the original interrupt handler þ Do not disable interrupts using CLI at all þ Run the program without EMM386 if possible Following these guidelines should ensure that interrupts are never locked out due to a hardware interrupt, software interrupt, or deliberate execution of a CLI instruction. Disabling IRQ1 (keyboard scancode) will disable the keyboard, of course, and disabling serial interrupts may disable the mouse. Most other interrupts are not active in the background (e.g. the floppy disk interrupt is only active when a disk access is in progress) and should be unaffected. If you are using int 8 and the interrupt rate is fairly slow, you may choose to chain to the original int 8 handler, because this will not cause jitter as it only executes _after_ the interrupt has been registered. However, this could cause problems if TSRs and/or drivers are using int 8 and occasionally do something nasty such as using the long tick interrupt handler technique described in section ¯¯ 6.9, where they gain full control of the machine for a while. In short, if you chain to the original handler, you are giving execution to code over which you have no control, so if jitter is critical, you do so at your own risk! If you need very fast interrupts or very low interrupt jitter, be very careful about what you do and who you call - you may need to do everything yourself to avoid interrupt latencies! I don't know the details of EMM386 and its effects on interrupt jitter. For example, it may internally trap some privileged instructions, and delay interrupts while processing these instructions. If anyone knows the details, please let me know! (*) ## 6.17 DETECTING INTERRUPT DELIVERY JITTER AND MISSED FAST TICK INTERRUPTS Interrupt delivery jitter on int 8 can be detected by reading CTC channel zero on entry to your interrupt handler and looking at the amount of variation from the highest raw value read, or the expected raw value (assuming that the reload value is known). See the sample program in section ¯¯ 10.16.2 for an example of this technique. If your application will be sensitive to interrupt jitter, you should incorporate this type of check, and if jitter is excessive, perhaps advise the user that there is a problem and he/she should ascertain which driver or TSR is causing the problem and get technical help to fix it if possible. If a fast timer tick rate is being used, a missed interrupt can be detected by using another CTC channel as a reference, providing that the CTC channel is not required (and will not be touched) for any other purpose. I would suggest using channel two, which is normally used for speaker audio generation. You would set channel two to a large divisor (e.g. 65536) and mode two, and make sure that nothing else touched it - i.e. disable, or at least don't use, the BIOS video function that emits a beep (int 10h with AX = 0E07h), and possibly hook into the keyboard subsystem to prevent the beep when the type-ahead buffer gets full. Your fast tick interrupt routine would read a timestamp from CTC channel two to determine how many fast ticks have been missed and adjust its behaviour accordingly. This approach would prevent the cumulative error, but would not fix the 'jumpiness' or 'jitter' of the timekeeping. ## 6.18 DISABLING INTERRUPTS FOR LONGER THAN ONE TIMER TICK In some applications, you may choose to disable interrupts for longer than the recommended maximums in section ¯¯ 6.15. You can also selectively disable the timer tick interrupt and any other hardware interrupt source, via the PIC IMR (section ¯¯ 6.11). You will have to deal with the implications of doing this, however. While interrupts are disabled via the processor's interrupt flag, interrupts accumulate, so as soon as the interrupt flag is set (via a POPF or STI), {JAM} says: "the program does not regain control until ALL outstanding interrupts are processed, including interrupts that happen while the outstanding ones are being handled. On networked machines, that time may be in milliseconds!" ## 6.19 DISABLING INTERRUPTS FOR LONG PERIODS OF TIME If it is necessary to disable interrupts for a long period of time, causing timer ticks to be missed, be aware that doing this is likely to sabotage any network software on the machine, and will also break the mouse driver while interrupts are locked out. You should take the following precautions. Don't start the section of code where interrupts are locked out, until the floppy disk drive motors have all turned off. Check the byte at low memory location 0040:003F. If it's nonzero, one or more floppy disk drive motors are active. Wait until it is zero. Assuming you don't want the machine to lose time, you can either read CTC channel zero regularly and watch for a borrow and increment the BIOS timer tick count when that occurs (remember the wrap-around and the midnight flag), or upon completion of the no-interrupt section of code, read the RTC and calculate and store the appropriate timer tick value. This also requires setting the midnight flag if appropriate. Generally if you want to disable interrupts for _that_ long, you will be running the program on a dedicated machine, and you may not be too concerned about loss of time. In this case, since you have control of the machine that the software will be running on, you could install the ATRTC driver, see section ¯¯ 3.3, which removes the dependency on the BIOS timer tick for timekeeping. The other problems still remain, however. ## 6.20 OVERHEAD OF AN INTERRUPT When an interrupt is accepted, the processor branches to the interrupt handler. On modern processors, this causes the prefetch queue to be flushed, wasting a small amount of time. Of course the prefetch queue is being flushed all the time, by branches and jumps and calls, etc, so this is not a major problem. The prefetch queue will be flushed when the interrupt is accepted, and again when the interrupt handler returns with an IRET. A bigger problem is code and data caching. {JAM} Because this caching is done in blocks, the interrupt may cause wanted code to be flushed from the cache, to make room in the cache for the interrupt handler code, wasting considerable time in reloading the cache when the interrupt completes. There is nothing that can be done about either of these problems. {JAM} In protected mode, interrupt overhead is very much higher, because of the privilege changes, mode switches, etc that are involved. Interrupt overhead on a 386SX-25 is in the order of a few hundred microseconds. I assume this refers to a real-mode interrupt handler being used with protected mode code. If the interrupt handler operated in protected mode, or was a dual- mode interrupt handler (could operate in either mode), this overhead would not exist, presumably. If anyone has more detailed information on this subject, please let me know. Also any detailed information on what EMM386 does to interrupts and how much overhead it imposes, and if there is any way to bypass it, would be great. (*) Tor Sjowall {TOR} also mentions an additional source of interrupt overhead - the stack switch that DOS does on hardware interrupts if you have a line 'STACKS=X,Y' in CONFIG.SYS. I don't know how this stack switching works, or at what level it operates. Please let me know if you can help. (*) ## 6.21 EFFECT OF BACKGROUND INTERRUPTS The timer tick interrupt is normally permanently enabled, and from the point of view of the code being interrupted, it introduces a 'gap' in time, at regular intervals (assuming interrupts are enabled). You could imagine that the processor gets abducted by aliens in a UFO (if you had a vivid imagination :-) One moment it's executing your main routine, minding its own business, then suddenly it is taken away and made to do something else, then when it returns to where it was, it continues normally, without even knowing that it had been doing something else, except that some time has elapsed. Excuse the analogy. Your foreground code is constantly being interrupted without its knowledge. {JAM} explains this quite nicely as follows: "The IBM PC has a constant active background process that results in a small gap in any loop. This becomes magnified when programs are compiled for protected mode. Moreover, the standard hardware can add additional gaps. Most often these gaps are under our control. Finally, when connected to a network, many types of background activities can happen, most of which we cannot predict and are beyond our control. Whenever we design a program to function on networked machines, we must remember that these background processes are in effect and we must take them into account. For example, when we poll a device, we must be aware that there will be missing time slices from that polling". Unless specifically enabled by your program, the only interrupt sources likely to be operating regularly while the machine is idle, are int 8 (timer tick), int 9 (keyboard scancode), and interrupts for the serial mouse or bus mouse, and network card interrupts. ## 6.22 SAFE CONTROL OF INTERRUPTS When you access hardware devices (reading or writing the CTC registers, for example), you could disable interrupts around the access, like this: -------------------------------- snip snip snip -------------------------------- void write_some_registers(void) { /* Unsafe method! */ disable(); /* Or asm cli */ outportb(port1, value1); /* whatever you need to do */ outportb(port2, value2); /* whatever you need to do */ enable(); /* Or asm sti */ return; } -------------------------------- snip snip snip -------------------------------- This assumes that the function that called this function was operating with interrupts enabled, and wants them re-enabled when this function has finished talking to the hardware. This may not be the case! For example, the function that called this function may already be doing something critical which requires interrupts to be locked out, and remain locked out continuously during the call to our write_some_registers() function. The safe way to handle this is as follows: -------------------------------- snip snip snip -------------------------------- void write_some_registers_safely(void) { asm pushf; asm cli; /* or use disable() */ outportb(port1, value1); /* whatever you need to do */ outportb(port2, value2); /* whatever you need to do */ asm popf; return; } -------------------------------- snip snip snip -------------------------------- Here, we push the flags register onto the stack before disabling interrupts, then pop the flags register back once we have finished. This ensures that interrupts are locked out during our hardware manipulation, and also that the correct state of the interrupt flag is restored once we have finished. If interrupts were enabled on entry, the popf sets the interrupt flag ON, and the function effectively only disables interrupts for the minimum time, i.e. between the disable() (CLI) and the popf. If interrupts were disabled on entry, the popf sets the interrupt flag OFF, which it already was from the disable() (CLI instruction). Thus the routine _never_ enables interrupts. This simple technique ensures that the routine can be safely used in either situation - either interrupts allowed, or interrupts not allowed. According to an article by James Ralph (jim@grc.com) in PC Magazine, September 13 1994, page 340, there is a bug in some 286 processors which causes the popf instruction to briefly enable interrupts. The workaround proposed by James is to use an IRET (which presumably does not suffer from this bug) instead of a POPF (an IRET pops IP, CS, and the flags). This approach requires that you push CS and IP onto the stack first. The example given by James is similar to this: -------------------------------- snip snip snip -------------------------------- pushf ; Keep flags including interrupt flag cli ; Disable interrupts ; Do critical stuff in here - interrupts are locked out push cs ; Have flags on stack, now push CS call NEAR AnIRET ; CALL pushes IP, IRET pops IP, CS, flags ; Continue with the main function - interrupt flag is now restored to its ; original value on entry to the function ret ; End of the function ; Put the IRET somewhere in the code segment - it can be used by multiple ; instances of the above code. AnIRET: iret -------------------------------- snip snip snip -------------------------------- Another way to handle this would be: -------------------------------- snip snip snip -------------------------------- pushf ; Keep flags including interrupt flag cli ; Disable interrupts ; Do critical stuff in here - interrupts are locked out push cs ; Have flags on stack, now push CS push WORD PTR cs:RetAdr ; Push a value for IP iret ; Pops IP, CS, and flags RetAdr DW RetPoint ; Offset to 'return' to RetPoint: ; Continue with the main function - interrupt flag is now restored to its ; original value on entry to the function -------------------------------- snip snip snip -------------------------------- I have not taken this precaution in the sample code, because I'm lazy, but you probably should use this method unless your program will never be run on 286 machines or is 386/486/586-specific. ## 6.23 TIMER TICK INTERRUPT HANDLER GUIDELINES Note that these comments also apply to other asynchronous interrupts, such as the keyboard interrupt (int 9) and the serial and parallel port interrupts. For full details, find a DOS reference that discusses ISR programming and TSR techniques. Both int 8 and int 1Ch are asynchronous hardware-triggered interrupts (although int 1Ch is actually software-generated). See section ¯¯ 6.35 for a discussion of the differences in usage between int 8 and int 1Ch. There are major restrictions on what can safely be done inside an asynchronous interrupt handler, because when it is invoked, the hardware and software state of the machine is not known. For example, DOS may be in the middle of writing to a printer port, waiting for user input, or processing a disk I/O request, or the BIOS may be busy scrolling the text screen, plotting a pixel, beeping the speaker, or programming the DMA controller ready to transfer a sector of data from a floppy disk. Also, a C library function that uses static variables may be in progress. In fact, more than one of these 'levels' may be busy. For example, an fopen() call could be in progress, which called DOS, which called the BIOS to read a sector from a floppy disk, which is busy programming the DMA controller. Therefore, all of these software and hardware blocks are busy. In general it is best to limit the functions performed by an interrupt handler to minimal hardware manipulation, and use a shared variable interface with the main program, whenever possible. Be careful to make your main program aware of the interrupt routine - programs do not normally expect certain variables to change magically of their own accord. Use the 'volatile' keyword when declaring these variables and use disable() and enable() (CLI and STI) around any critical code sections. Also, if your timer tick interrupt handler may use a lot of stack space you should consider switching to another stack. This is much easier if the interrupt handler is written in assembler. Keep int 8 and int 1Ch routines as short and fast as possible to reduce delays imposed on other interrupt sources. ## 6.24 ACCESSING HARDWARE DEVICES IN AN INTERRUPT HANDLER Asynchronous interrupts are 'background' processes. It is not always safe for them to access hardware devices, because the 'foreground' processes - the main body of your program, or a function (e.g. DOS, BIOS, EMS, XMS, mouse, network, etc) called by your program - may be in the process of accessing the device, or may be expecting the device to remain in a certain state. Often, foreground accesses consist of reading and/or writing a few I/O locations in sequence, as with the CTC. To make things safe for your interrupt routines and for TSRs, when you access devices in this way in your foreground code, you must _ALWAYS_ disable interrupts around the sequence. This applies to devices such as the RTC, CTC, PICs, DMA controller, VGA ASICs, etc. Many hardware devices can be accessed (carefully) by an interrupt routine. This may be because they are not normally accessed in the foreground, or because the interrupt routine uses a part of the device that is not used by the foreground processes, or because the interrupt routine's accesses do not conflict with the foreground process's accesses. If you know enough to access the hardware directly, you will know when and how the foreground processes will access the device, so you can figure out what your interrupt routine can and cannot do safely with that device. Reading the CTC in an interrupt handler is always safe, providing that your foreground program is well-behaved (always disables interrupts around access sequences) and always reads the appropriate number of bytes from the data register, so the lobyte/ hibyte flag remains in sync (see section ¯¯ 7.17). ## 6.25 CALLING DOS AND BIOS IN AN INTERRUPT HANDLER Much of the BIOS, and all of DOS, is not re-entrant, and therefore cannot safely be called from an asynchronous (hardware) interrupt handler, because it might have been busy (i.e. in progress) when the interrupt occurred. None of the applications in this document require DOS or BIOS to be called from an asynchronous interrupt handler. If you need to do this, get a reference on TSR programming, as it is non-trivial! ## 6.26 CALLING C LIBRARY FUNCTIONS IN AN INTERRUPT HANDLER Many C library functions call DOS or BIOS functions, and are subject to the same restrictions. Also, some C library functions may not be re-entrant for other reasons - for instance, they may use global or static variables, or allocate memory which is in the process of being allocated to the foreground program. Check your compiler's library reference or programmers' guide for information about TSR considerations and re-entrancy of library functions. ## 6.27 RE-ENTRY OF INTERRUPT HANDLERS Generally hardware interrupt handlers are not re-entered, i.e. are not restarted during their execution, because they do not send an EOI (see section ¯¯ 6.28) until they have completed, and then interrupts are locked out (see section ¯¯ 6.29). There is one exception to this rule, which applies when a TSR uses the long timer interrupt technique described in section ¯¯ 6.9. This technique can also be used with the keyboard scancode interrupt, when a TSR pops up using that interrupt, but see section ¯¯ 6.9.1 for a potential problem. In these cases, the interrupt handler of a foreground program may actually be re-entered during processing, if it chains to the original handler using the CALL method (see section ¯¯ 6.31), because the original handler (which is the TSR's handler) can issue an EOI, allowing the entry part of the interrupt handler to be re-entered. The TSR's own interrupt handler will be aware of re-entry considerations, because the TSR will be causing them, but an interrupt handler in a foreground program may not have been designed with this in mind. They probably should be designed to support this technique. See section ¯¯ 6.9 for more details. If the code in the interrupt handler is inherently non-reentrant, this can be handled using a semaphore to detect re-entrance, as described in section ¯¯ 6.9. If the semaphore is set at the start of your handler, it should probably chain to the original handler using the JMP method without performing its normal function. In some cases it is possible that the semaphore would become set and would never clear. Hopefully nobody is even reading this stuff, as it is excessively boring. Yibble yibble yobble yoo, I am a fence post. It is time for my pill - I have to take one every 54.9254 milliseconds. ## 6.28 THE 'END OF INTERRUPT' SIGNAL Interrupts 8 to 15 (corresponding to IRQ0 to 7) and interrupts 70 hex to 77 hex (corresponding to IRQ8 to 15) are generated by hardware devices. An interrupt service routine for these interrupts must inform the 8259 PIC(s) when the device which generated the interrupt has been serviced, so that the PIC can reset its priority structure. This is done using a non-specific EOI (end of interrupt) command to the PIC. For int 8 to 15 (IRQ0 to IRQ7), a single EOI is used: outportb(0x20, 0x20); in C, or mov al,20h out 20h,al in assembler. For int 70 hex to 77 hex (IRQ8 to IRQ15), two EOIs are used: outportb(0xA0, 0x20); outportb(0x20, 0x20); in C, or mov al,20h out 0A0h,al out 20h,al in assembler. For IRQ8 through IRQ15, the EOI is typically sent to the secondary PIC first, as in these examples, though I don't believe there is any significance to the order in which they are sent. Normally the EOI is sent at the end of the interrupt routine just before the IRET instruction. See section ¯¯ 6.29 for interrupt control details. You can use the specific EOI command if you prefer - the value is 60 hex plus the IRQ number within the PIC, for example to send a specific EOI for IRQ4: outportb(0x20, 0x64); and to send a specific EOI for IRQ11: outportb(0xA0, 0x63); /* IRQ11 is input 3 on the second PIC */ outportb(0x20, 0x62); /* The chain IRQ is IRQ2 */ ## 6.28.1 LEVEL TRIGGERED INTERRUPT RESET IBM PS/2 machines that use MCA (Microchannel Architecture) buses have level triggered interrupts. This poses a problem for the timer interrupt - how to clear the timer interrupt request. I have no formal documentation on this, but I saw the following note in an article by Bob Smith (bobs@access.digex.net) in mid November 1995: > On an IBM Micro Channel Architecture system, the timer tick handler in the > BIOS sets the Clear IRQ0 bit (bit 7 in I/O port 61h). Without this, the > hard disk won't work. This might, in fact, apply to all level-triggered > interrupts in general, but I found out about setting that bit before having > to experiment any further. So it appears that the timer interrupt must be explicitly acknowledged and cleared on an MCA system, in addition to sending the EOI. This makes sense as there is no other way for the level to be reset to deassert the interrupt request in a level triggered interrupt system. There may be some similar requirement for an EISA system running in level triggered interrupt mode. Any more information would be welcomed. (*) This also has implications when int 8 is operated at a higher rate, because the int 8 intercepter would have to manually acknowledge the interrupt, in addition to sending the EOI, every time it didn't chain to the original int 8 handler. This may mean that a standard int 8 handler for a fast timer tick interrupt (see section ¯¯ 8) will not work on a PS/2. ## 6.29 ENABLING AND DISABLING INTERRUPTS IN AN INTERRUPT HANDLER On entry to an interrupt handler, processor interrupts are disabled (as if a disable() or CLI had been issued). Normal practice is to enable interrupts as soon as possible, perform processing, disable interrupts again, issue an EOI if applicable (see section ¯¯ 6.28), and return from interrupt. However, int 8 is the highest priority interrupt source, and until the EOI is sent, no other interrupts will get through (except NMI of course) so there's no need to enable interrupts during int 8 or int 1Ch processing, unless you hare re-ordered the interrupt priorities. The EOI command is sent to the PIC(s) at the end of the interrupt handler. For interrupt handlers which enable interrupts during processing, it is normally wise to disable interrupts using disable() or CLI just before issuing the EOI, so that another equal or lower priority interrupt does not occur after the EOI but before the IRET. Typical coding would be: IntHandler: sti push ax push other registers ; ... interrupt processing here pop other registers IntFinished: mov al,20h cli out 20h,al pop ax iret This particular consideration does not normally apply to int 8 handlers as they are normally the highest priority interrupt and do not need to enable interrupts during their operation. If an int 8 handler does enable interrupts, however, the above precaution should be taken. ## 6.30 STACK USAGE AND STACK CHECKING IN AN INTERRUPT HANDLER Stack usage (function nesting depth) must be kept to an absolute minimum unless your interrupt handler performs a stack switch to a local stack. Normally, you will be using the stack of whichever program was active at the moment that the timer tick occurred, and you don't know how much spare room there is in that stack. For interrupt handlers written in C, don't go allocating automatic strings or arrays! Declare any local variables static if possible. If your compiler has stack checking ON by default, and isn't too bright, you may need to turn stack checking OFF for all interrupt handlers, and for any functions that may be called by them, using the appropriate compiler directive. The directives for Borland C++ 4.0 (and probably 3.1 as well) are: #pragma option -N- turn OFF stack checking #pragma option -N turn ON stack checking if perviously enabled These can be placed around the whole function (or group of functions) that are to be compiled without stack checking, or just around the first line of the function (that gives the return type, function name, and parameters). That information was kindly sent by Michael Mauch (mauch@uni-duisburg.de) who mentions another problem he found with BC++ 4.0. He had an _inlined_ function that was called by an interrupt handler. Both the interrupt handler and the inlined function were declared with stack checking off. When he temporarily disabled inlining, during debugging, the compiler generated a stack check in the called function! The moral of the story is you can't always trust your compiler :-) If anyone can provide details of stack checking directives for other compilers, please let me know. (*) ## 6.31 CHAINING TO THE OLD INTERRUPT HANDLER Most interrupts have a default handler. Before your program takes over control of an interrupt, it must store the contents of the interrupt vector, which will be a far (i.e. segment and offset) pointer to the original handler, and which must be restored when your program terminates (see section ¯¯ 6.3). Often your replacement interrupt handler will need to use the original handler. This is called _chaining_ to the original handler of the interrupt, and is done through the pointer that your program stored when it intercepted the interrupt. Sometimes your replacement interrupt handler will always chain to the original handler, and sometimes chaining is done conditionally, i.e. when required or when appropriate. When chaining to an original interrupt handler, remember that the original interrupt handler was written to assume that it _was_ the handler for this interrupt source. Sometimes this requires a little care to make sure that it will operate properly if called by your replacement handler. For example, the BIOS int 8 handler issues an EOI command (see section ¯¯ 6.28) every time it is called, so if your interrupt handler chains to the BIOS's interrupt handler, it should not issue the EOI itself. It must issue the EOI if it does _not_ chain to the BIOS's interrupt handler, however. Also, the original interrupt handler will probably assume that interrupts will be disabled when it is invoked, as this is the case when it is invoked directly, so you must ensure that interrupts are disabled before chaining. There are two ways of chaining to the old handler - you can bury her, burn her or dump her. I mean, you can call it, or you can jump to it. Call it when your interrupt handler needs to regain control after the old handler has been invoked. Jump to it when you do not need to get control back, as this uses less stack space and is tidier. In the Thames. Remember that the processor pushes the flags, CS, and IP (in that order) when it accepts an interrupt, and an IRET (which is the way most handlers will exit) pops these registers back again. Therefore if you chain to a handler with a CALL, you must push some flags first, then use the far form of CALL, so that the IRET will return correctly. Chain_Call: ; ... Initial interrupt processing here pushf ; Simulate stack for an INT cli call FAR OldIntPtr ; Call old handler ; ... More interrupt processing here iret Chain_Jump: ; ... Initial interrupt processing here cli jmp FAR OldIntPtr ; Call old handler Note that some interrupts, specifically int 1Ch, do not require chaining, as the default handler is just an IRET. But see sections ¯¯ 6.33 and ¯¯ 6.35. See the interrupt handlers in the sample programs for more details. If you use the CALL method to chain to the old interrupt, in an int 8 handler, beware that there may be a TSR using the Long Tick Interrupt technique described in section ¯¯ 6.9, which will send an EOI but not return, thus causing your interrupt handler to be re-entered while it was part-way through execution. You probably should design the handler to support this possibility. If you use the JMP chaining method, this consideration does not apply. ## 6.32 WRITING INTERRUPT HANDLERS IN ASSEMBLY LANGUAGE Here are some guidelines and warnings that you should heed if you are coding an interrupt handler in assembly language. On entry to the interrupt handler, the only registers that will be known are CS and IP. DS is undefined. You must preserve any other registers that you modify, except the flags (which will be restored by the IRET). Also see sections ¯¯ 6.23 to ¯¯ 6.26 for restrictions on what may be called from, and done inside, your interrupt handler. ## 6.32.1 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: ACCESSING VARIABLES If your interrupt handler must have access to memory variables, such as flags, structures or buffers used to communicate with other parts of your program, there are three main ways to do this: þ Common code and data segment (COM files; tiny model), access with CS þ Put variables in code segment and access them using CS þ Put variables in data segment and set DS so you can access them. The first approach is used in single-segment COM-files (also known as tiny model) in which the code and data segment-paragraphs are the same. In these programs, CS will already address the segment (because the interrupt handler is in the same segment as the data), so you can access variables using CS. This is done via the ASSUME directive, which tells the assembler what segment each of the segment registers is supposed to contain. The directive: ASSUME cs:_TEXT,ds:nothing,es:nothing,ss:nothing tells the assembler that only the CS register is known at the moment, and that CS addresses the _TEXT segment. You would change the name to whatever segment you use for your single segment. This directive should appear before the interrupt handler. The ASSUMEd registers remain in effect until modified by another ASSUME directive. The above ASSUME directive tells the assembler that only the _TEXT segment is addressable, and that every access to a variable in that segment will require a CS segment override prefix. You need not explicitly code the CS override on every instruction - the assembler takes care of this automatically (unless you're using A86 :-) But be very careful with string instructions, because they don't make references to data objects, and an explicit segment override may be required. For example, if only CS is ASSUMEd: mov ax,SomeVariable ; This will generate a CS ; override and will work, mov si,OFFSET SomeTable ; Point to start of table lodsw ; Uh-oh! No override will ; be generated on this! lodsw cs: ; This will work The second method is used with multiple segment programs. The variables are placed in the code segment, typically near to the interrupt handler, and are accessed in a similar way. An ASSUME directive should be used to tell the assembler that CS is the only known segment register, and that it addresses the code segment (_TEXT or whatever). The trouble with this method is that the main program has to access those variables in the code segment, which is messy. This second method is most often used in large assembly language programs. The third method is the method used in C programs and most high level programs, where placing variables in the code segment is frowned upon and/or impossible. The variables are placed in the data segment, as normal. This requires that somehow, a segment register (typically DS) must be loaded with the appropriate segment at some point in the interrupt routine, before those variables are addressable. This also requires that the segment register (e.g. DS) is pushed at the start of the interrupt handler and popped again before it terminates. Again, an ASSUME directive should be used to tell the assembler what segment registers point to what. Here is a sample code fragment: -------------------------------- snip snip snip -------------------------------- DATA SEGMENT SomeVariable DW 0 ; Some variable, used by int. handler AnotherVar DW 0 ; Another variable, ditto DATA ENDS CODE SEGMENT ASSUME cs:CODE,ds:nothing,es:nothing,ss:nothing MyIntHandler PROC far pushf ; Preserve flags push ax ; Preserve register push ds ; Preserve DS mov ax,SEG DATA ; Get data segment to AX mov ds,ax ; Move it to DS ASSUME ds:DATA ; (CS, ES and SS are unchanged) mov ax,SomeVariable ; Get some variable add ax,AnotherVar ; etc, you get the idea... ; -- More code here pop ds ; Restore DS ASSUME ds:nothing ; Cannot address anything useful with DS pop ax ; Restore AX popf ; Restore flags DB 0EAh ; JMP xxxx:yyyy OldIntOfs DW 0 ; Vector to original handler - Offset OldIntSeg DW 0 ; Segment MyIntHandler ENDP -------------------------------- snip snip snip -------------------------------- I suggest using tiny model for assembly language programs (avoids segment register setting in the interrupt handler), or for assembly language programs in other models, place the variables in the code segment, and for C programs, place them in the data segment. This is a matter of personal preference, though. ## 6.32.2 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: STARTING CONDITION The interrupt flag in the flags register will be clear (i.e. interrupts locked out) unless a badly behaved interrupt handler has chained to your interrupt handler but left interrupts turned on. If you are doing critical hardware access in your handler, you may want to issue a CLI just in case. This should not apply to int 8, as it is the highest priority interrupt and should never be interrupted (except by an NMI!) You may have noticed that I pushed and popped the flags in the sample code in section ¯¯ 6.32.1. This is probably not necessary in such a case as the original flags are popped by the IRET at the end of the old interrupt handler that is being chained to via the JMP instruction (see section ¯¯ 6.31), but I think it's wise to make sure that the chained interrupt handler starts with the same flags that it would have had if our interrupt handler was not present. The direction flag will probably be clear, but DON'T COUNT ON IT! If you do any string manipulation in your interrupt handler, be sure to include a CLD instruction to ensure that the direction flag is known. Forgetting this precaution is an open-armed invitation to subtle intermittent bugs. ## 6.32.3 ASSEMBLY LANGUAGE INTERRUPT HANDLERS: PRESERVE THE REGISTERS Of course, your interrupt handler must not modify any registers - use PUSH and POP to preserve the old values in registers if you need to use the registers for something else. Watch out for instructions that modify unexpected registers - for example the 16-16-32 MUL instruction modifies DX; even if you don't _use_ the high word of the result, DX will still be modified. ## 6.33 USING INTERRUPT EIGHT IN A TSR You must intercept int 8 when speeding up the timer tick (see section ¯¯ 8). Int 8 can also be used by TSRs which want a regular interrupt source. TSRs should not use int 1Ch, though some do - see section ¯¯ 6.35. On installation, your TSR should obtain the contents of the int 8 vector using getvect() or DOS function 35 hex, and store it, then replace the interrupt handler with its own handler. Every time the TSR's int 8 handler is called, it must chain to the old interrupt handler, usually by jumping to it, as described in section ¯¯ 6.31. Your TSR then has a regular 54.9254 millisecond interrupt source. If a foreground program reprograms the timer tick for a faster rate (see section ¯¯ 8), calls to your int 8 handler may be unevenly spaced. In the worst case, it is possible for int 8 invocations to be spaced as closely as 27.4627 ms, half the normal spacing, and as far apart as 82.3881 ms, 1.5 times the normal spacing. Over a period of time the interval between int 8 calls should average out to 54.9254 ms, though. See sections ¯¯ 6.23 to ¯¯ 6.26 for details of restrictions and techniques for interrupt handlers. If your TSR can be uninstalled from the command line (a useful feature), the original int 8 vector contents must be restored, but before restoring vectors when uninstalling, ensure that the int 8 vector, and the vectors for any other interrupts your TSR intercepts, are currently pointing to the handler in the installed copy of the TSR. If they do not, one or more TSRs have been loaded after your TSR, and it is not safe to uninstall your TSR because restoring the interrupt vectors will unhook the other TSRs and sabotage their operation. In this case, you must advise the user that the TSR cannot be uninstalled as other TSRs have been installed above it. I believe there is a package called Tesseract, or maybe AMIS, written by Ralf Brown of the Interrupt List fame, which provides a general TSR template and also permits compatible TSRs (i.e. ones written to be compliant with the system) to be unloaded in any order. This sounds like a good idea, though I have not used it. I found ftp://oak.oakland.edu/SimTel/msdos/info/altmpx35.zip which is dated 13 Sept 1992, but I don't know if this is the latest version. If someone knows the latest version and its home site, please advise me so I can include a reference here. (*) ## 6.34 USING INT 8 WITHOUT CHAINING In some cases, for minimal interrupt overhead when int 8 is being operated at a high rate, it may be necessary to use int 8 without chaining. Doing this will cause the DOS time to freeze (unless an RTC-based CLOCK$ driver such as ATRTC, see section ¯¯ 3.3 is installed), will prevent floppy disk drives from turning off after two seconds of inactivity, will probably prevent timeout- based 'green' functions (slow-mode, hard drive spindown on laptops, etc) from kicking in, and will probably break the mouse driver and any network software, as well as most screen savers and some pop-up TSRs, so this is not something that should be done by a well-behaved program that is intended for general use. You can see the effect of disabling the timer interrupt by using the sample program in section ¯¯ 7.12 to set CTC channel zero to an inappropriate mode, such as mode zero, thus stopping the timer tick. ## 6.35 USING INT 1C HEX INSTEAD OF INT 8 Int 1Ch is intended for use by user programs for timing. It is invoked 18.2065 times per second by the BIOS int 8 handler. On entry to the handler, interrupts will be disabled. Do not issue an EOI command to the interrupt controller - the BIOS int 8 handler takes care of this after the int 1Ch handler returns. TSRs should not use int 1Ch - see below for a discussion of this. In theory, you should not need to chain to the original int 1Ch handler, as the default handler is a dummy handler, simply an IRET. However, some existing TSRs hook int 1Ch. For compatibility with those TSRs you should make your non-TSR programs chain to the old int 1Ch handler if they use int 1Ch. See sections ¯¯ 6.23 to ¯¯ 6.26 for details of what can, and cannot, be safely done inside an int 1Ch handler. To use int 1Ch, during initialisation the program should store the address of the original int 1Ch handler and replace the old handler with a new one, and on termination, the program should restore the old handler address. Chain to the old handler in the normal way on every int 1Ch call. It does not matter whether you chain before you perform your own processing, or after. A program which intercepts int 8 or int 1Ch should trap critical errors and the DOS Ctrl-C vector, and optionally the Divide Overflow vector, so that if the program is terminated due to a critical error or a user Ctrl-Break or Ctrl-C, the interrupt vector can be restored to its original address as part of the clean-up. See section ¯¯ 5 and subsections for details on trapping the Ctrl-C and critical error vectors. In my view, it is inappropriate for a TSR to hook int 1Ch. Some people have disagreed with this opinion, so for their benefit I will present the evidence that I have found, and explain the logic by which I arrived at my conclusion. 1 The MS-DOS Encyclopedia has two articles that relate to interrupts and TSRs. This book is published by Microsoft Press and edited by Ray Duncan. The two relevant articles are Article 11 on TSRs by Richard Wilton, and Article 13 on Hardware Interrupt Handlers by Jim Kyle and Chip Rabinowitz. Article 11 has a TSR example which uses int 8. The article makes no mention of int 1Ch at all. Article 13's example code also uses int 8 and the article only mentions int 1Ch in a table of low-numbered interrupts as "Timer tick (user defined)". 2 The PC and XT technical references have the following to say about int 1Ch: "This vector points to the code to be executed on every system-clock tick. This vector is invoked while responding to the timer interrupt, and control should be returned through an IRET instruction. The power- on routines initialise this vector to point to an IRET instruction, so that nothing will occur unless the application modifies the pointer. It is the responsibility of the application to save and restore all registers that will be modified." 3 From the book "DOS Programmer's Reference", 3rd edition, published by Que Corporation, written by Dettmann, Kyle and Johnson (see section ¯¯ 12), in the section on int 8: "Int 08h, which is called 18.2 times per second to advance the time-of- day counter, is tied directly to channel 0 of the system timer chip. People who write TSRs with utilities such as SideKick, for example, find Int 08h particularly useful for time-related triggering (as with a clock or alarm). This interrupt calls Int 1Ch (Timer Tick). Most TSRs should connect to Int 1Ch rather than to Int 08h. In the section on int 1Ch: "Vector 1Ch, the timer tick interrupt called by int 8 (system metronome), is initialised to point to an IRET instruction. A TSR that needs to be triggered at each clock tick can reset the vector for this interrupt to point to a custom interrupt handler. "Because this function is called from inside the int 08h code, before handling of that top-priority action is completed, it shares top priority and will prevent the system from responding to any other hardware interrupt requests, including those from serial devices or disk units, while it executes. Therefore it is necessary to keep to an absolute minimum the time spent in any handler for this function, or you will risk the loss of data when time-sensitive applications are running. "The best practice for a TSR is merely to set a flag from this function, then inspect the flag from another handler hooked into the int 28h (DosOK) chain, which gives ample time to take care of any needed processing without blocking hardware interrupts." In a section on TSR programming: "If DOS is not waiting for input, you can use the timer interrupt. The timer interrupt (1Ch) ticks 18.2 times per second. You can attach to this interrupt in the following service routine that checks the hot-key flag as well: "Timer Interrupt activates "Call next timer interrupt service ... The TSRs in the MS-DOS Encyclopedia use int 8 but do not say that int 8 should be used, and do not give reasons. The DOS Programmer's Reference states clearly that int 1Ch should be used by TSRs but do not give reasons, and its section on int 1Ch is worded so as to imply that int 1Ch should not be chained if used in a TSR, though it is obvious (and clearly shown in the TSR programming section of the same book) that it should be chained. Since the MS-DOS Encyclopedia is sanctioned by Microsoft and edited by Ray Duncan, I feel it has more weight (particularly the hardback edition :-) than the Que book, even though Jim Kyle, one of the authors of the Que book, co-designed the AMIS TSR interface! The technical reference also makes the point that the default handler for int 1Ch is an IRET, and clearly states that "nothing will occur unless the application modifies the pointer", though this text was written before TSRs were commonplace and is probably not written with TSR considerations taken into account. In my view, TSRs should not use int 1Ch, they should use int 8. Applications may use either (though they must use int 8 if they are speeding up the timer tick; this is a separate issue). If an application hooks int 8, it must chain to the original handler. If an application hooks int 1Ch, it should also chain to the original handler, to support existing TSRs which use int 1Ch. My logic in coming to this conclusion is: Int 1Ch is (or was originally) defined for _user_ program use, The default handler is an IRET, and was provided simply to keep the machine from crashing when int 1Ch is issued by the BIOS int 8 handler and no user program is using int 1Ch, Therefore an application grabbing int 1Ch does not need to chain, Therefore a TSR writer should not assume that int 1Ch will be chained, Therefore a TSR writer should use int 8, not int 1Ch. Some TSRs do use int 1Ch, Therefore an app using int 1Ch should chain, to support these TSRs. I believe that a TSR should operate as transparently as possible, i.e. the environment presented to a user program should be the same with or without the TSR. The default handler for int 1Ch is an IRET, so an application does not need to chain when it hooks int 1Ch. If a TSR hooks int 1Ch, the default int 1Ch handler (from an application program's point of view) is no longer an IRET, and the new 'default' handler must be chained. This has changed the environment from one where the default handler was just provided so that the machine didn't crash and there was no reason to chain, to one where chaining is required. Therefore I regard this as bad programming practice for a TSR writer. As to the question of whether an application program should chain int 1Ch, there are clearly some TSRs in existence that do use int 1Ch, so application programs should now chain int 1Ch. In my opinion this is unfortunate, but due to the number of programmers who write DOS software, and the lack of thorough documentation on TSR writing from IBM and Micro$oft, such misunderstandings and design misfeatures are a sad fact of life. There are other cases where programs must go to lengths to do things they shouldn't have to do, in order to work around problems due to bad design in other programs - for example, the old DOS VDISK program, which grabbed extended memory uncooperatively because it was written before the XMS standard evolved, is a good example - memory managers must check for its existence explicitly and refuse to install if VDISK is found. If you think that this lack of coordination is surprising, consider that an organised software company developing an operating system would provide its programming staff with a thorough design document, and allow only experienced system-level programmers to work on the interrupt routines, whereas Micro$oft has provided precious little documentation on writing reliable low-level code, and (because of DOS's lack of support for anything more esoteric than file and memory management), forced large numbers of programmers with varying amounts of low-level programming experience to 'go to the hardware' when they want a fast tick interrupt or a serial port that can operate faster than 300 baud :-) With this sorry state of affairs, it's a miracle that so many TSRs can live together at all! ## 6.36 SAMPLE PROGRAM: USING INT 1CH WITH CRITICAL ERROR AND CTRL-C HANDLING The following program demonstrates using int 1Ch and handling critical errors and Ctrl-C using the critical error handling module from section ¯¯ 5.8. The program traps Ctrl-C (it has its own Ctrl-C handler) and critical errors (via the crit_err_intercept() function in CRIT_ERR.ASM), and takes over the int 1Ch interrupt. It does not chain to the original int 1Ch handler, as this is supposed to be a dummy IRET instruction. If a badly written TSR is using this interrupt, then it will just have to miss out while my program is running (see section ¯¯ 6.35 for more details). Every timer tick, it toggles the speaker state, causing a ticking noise. The speaker toggle is done inside new_int_1Ch(). First, the user has the opportunity to press Ctrl-C while DOS function 1 is in progress. This triggers the Ctrl-C handler, which terminates the program with the message "Program terminated by Ctrl-Break or Ctrl-C". The abort_cleanup() function is called with dos_is_safe set to TRUE. If the user presses Enter instead of Ctrl-C, the program continues, and tries to open the file "A:NOSUCH.FIL". Leave the disk drive empty for this test. This invokes the critical error handler and issues the Abort, Retry, Ignore (or Fail) prompt. If the user selects Abort, the critical error intercepter (in CRIT_ERR.ASM) calls abort_cleanup() with dos_is_safe FALSE, then returns the Abort error code to DOS, which terminates the program. If the user selects Fail, the program continues, and calls abort_cleanup() with dos_is_safe TRUE, then terminates normally via exit(0). abort_cleanup() resets the control signals for the speaker and cleans up the int 1Ch vector. If DOS is not safe, it restores the vector directly by patching the interrupt vector table directly. It only attempts to restore the int 1Ch vector if the old_int_1Ch variable is not equal to 0xFFFFFFFF, i.e. the vector has actually been intercepted! In any case, the ticking sound should stop when the program terminates for any reason, indicating that the interrupt vectors were correctly restored and the machine is in a stable state. -------------------------------- snip snip snip -------------------------------- /* Sample program #4 Demonstrates using int 1Ch and handling Ctrl-C and critical errors Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save and assemble the critical error module CRIT_ERR (above) Save this sample code to SAMPLE4.C Compile this module with: bcc -c -I -ms sample4.c Link the modules with: tlink /c /x \c0s.obj sample4.obj crit_err.obj, sample4, nul, \cs Where inc_path is the path to your C header files, c0_path is the path to your startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB. */ #include /* Needed for enable(), disable(), MK_FP() */ #include /* Needed for O_RDONLY */ #include /* Needed for _open() and _write() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define STDERR 2 /* DOS handle for standard error */ void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */ unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */ typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */ intfuncp old_int_1Ch = (intfuncp)0xFFFFFFFFL; void abort_cleanup(int dos_is_safe) { if (dos_is_safe) { if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) { setvect(0x1C, old_int_1Ch); old_int_1Ch = (void far *)0xFFFFFFFFL; } /* Insert other cleanups here - DOS can be safely called */ } else { disable(); /* Probably superfluous */ if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) { *((intfuncp far *)MK_FP(0, 0x1C << 2)) = old_int_1Ch; old_int_1Ch = (void far *)0xFFFFFFFFL; } /* Insert other cleanups here - DOS can NOT safely be called */ } outportb(0x61, inportb(0x61) & 0xFC); /* Clean up speaker control */ return; } void interrupt ctrl_c_handler(void) { static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n"; if (is_at_crit_prompt()) abort_cleanup(FALSE); else { abort_cleanup(TRUE); _write(STDERR, &message, sizeof(message)); } exit(255); } void interrupt new_int_1Ch(void) { outportb(0x61, (inportb(0x61) & 0xFE) ^ 0x02); return; /* From interrupt */ } void intercept_int_1Ch(void) { old_int_1Ch = getvect(0x1C); setvect(0x1C, new_int_1Ch); return; } unsigned int dos_func_1(void) { _AX = 0x100; geninterrupt(0x21); /* DOS keyboard input with echo and break */ return _AL; } void main(void) { int n; printf("Sample program #4 - Demonstrates using int 1Ch and handling Ctrl-C and critical errors\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); crit_err_intercept(); /* Trap critical errors */ setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */ intercept_int_1Ch(); /* Intercept int 1Ch */ printf("Type characters, press Ctrl-C to test the Ctrl-C handler\n"); printf("Press Enter to continue\n"); do { n = dos_func_1(); } while (n != '\r'); /* Wait for C/R */ printf("Now testing critical error handler, opening 'A:NOSUCH.FIL'\n"); printf("Please remove any disk (if any) from drive A\n"); printf("Select the Abort option to test the critical error handler\n"); n = _open("a:nosuch.fil", O_RDONLY); if (n != 0 && n != -1) _close(n); abort_cleanup(TRUE); printf("Normal program termination\n"); exit(0); } -------------------------------- snip snip snip -------------------------------- ## 6.37 DEBUGGING INTERRUPT HANDLERS Saul Cozens (s.cozens@sheffield.ac.uk) wrote: > I have noticed that many people attempt to debug interrupt handlers by > adding a printf statement so they know that the ISR has been called. Yes, of course printf() is right out. printf() calls DOS (usually), therefore it cannot safely be used from within an interrupt handler such as an int 8 handler. Saul suggests that the BIOS 'write string' function is a safer bet. The BIOS video functions are listed as non-reentrant, but often you can get away with calling them from an interrupt handler. A common technique when trying to figure out just _what_ an interrupt handler is doing, is to issue a bell at appropriate points, using int 0x10, function 0x0E with 0x07 in AL. For a cleaner approach, on every interrupt just clear the Timer 2 Gate bit on Port B and toggle (flip) the Speaker Enable bit. This will produce a click on each interrupt. Alternatively, increment a character in screen memory. The character at offset 0F00h into the regen buffer is the bottom left corner character in 80x25 text modes. > I found a bug with Borland C 3.1. When a function is declared as an > interrupt function, the compiler quite rightly perserves all the registers > automatically (I used Turbo Debugger to look at the compiled code). > Unfortunately it does not save the high words of the 32-bit registers, even > when the options are set to 'use 32-bit registers'. I have also noticed this problem - in Borland Pascal 7. A library arithmetic function (long div) uses 32-bit registers, if the appropriate compiler option is set. If this function is called from within an interrupt routine, the hiword of EAX is destroyed. The 'interrupt' keyword on the function definition causes the 16-bit registers to be preserved on the stack, but not the 32-bit registers. This bug is particularly nasty because this function is called invisibly to the programmer, as part of an innocent-looking calculation. There may also be implications when running with a DOS extender. John Stockton (jrs@dclf.npl.co.uk) sent me the following information which contains a fix for this problem and also mentions another related problem: > Duncan Murdoch (dmurdoch@mast.queensu.ca) has provided inline TP/BP code > to save and restore EAX..EDX in an ISR: > > procedure PushEAXtoEDX ; {from DM} > Inline( > $66/ {db $66} $50/ {push ax} > $66/ {db $66} $53/ {push bx} > $66/ {db $66} $51/ {push cx} > $66/ {db $66} $52 {push dx} ) ; > > procedure PopEDXtoEAX ; {from DM} > Inline( > $66/ {db $66} $5A/ {pop dx} > $66/ {db $66} $59/ {pop cx} > $66/ {db $66} $5B/ {pop bx} > $66/ {db $66} $58 {pop ax} ) ; > > and he has pointed out that something like: > > var X, Y : longint ; > {...} > X := 10 ; Y := 10 ; > { repeatedly : } if X * Y <> 100 then BEEP ; > > in the main program can detect this problem and its cure. > > Matters are worse if the main program and the ISR both use the hardware > FPU programmed in Pascal. One can save and restore the FPU state, and > that does help but does not cure the problem. It seems that TP/BP FPU > code uses non-reentrant 80x86 routines around 80x87 instructions. > > Inspiration dawned during an E-mail exchange with Norbert Juffa > (norbert@itt.com, whose files should be read by anyone interested > in Pascal and floating point). > > I (with a '486) now compile the ISR in the {$N-,E-} state which forces > 6-byte software real arithmetic, and the main code in the {$N+,E-} state > using extended variables and hardware arithmetic. With a little care to > disable interrupts while transferring values between types real and > extended, all seems well. Thank you John for that information. > Another thing that used to catch me out was that single stepping (using > Turbo Debugger) through bits of code that re-program the PIT causes a > system crash. This is presumably because the writes to certain registers > must be consecutive and the Turbo Debugger writes to the PIT itself every > time it does a 'step'. When programming the CTC, the entire access sequence must be completed without interference, so interrupts must be locked out, and the sequence of accesses must be executed from start to finish without being interrupted by anything, including a debug single step interrupt or breakpoint. This section may be improved later (*) ## 7 HARDWARE INFORMATION AND PROGRAMMING ## 7.1 THE 14.31818 MHZ CLOCK A crystal oscillator or oscillator module generates a 14.31818 MHz clock which is divided by 12 to give the 1.1931816666666... MHz clock frequency (period is 12/14318180, or 0.83809534452 us), which is fed to all three channels of the counter/timer chip. This is the basic timing resolution of the counter/timer. ## 7.2 CLOCK FREQUENCY ACCURACY The 14.31818 MHz clock's absolute accuracy depends mainly on the quality of the 14.31818 MHz crystal or crystal oscillator module, and is typically in the region of +/- 5 ppm (0.0005%; 0.4 seconds per day) to +/- 20 ppm (0.002%; 1.73 seconds per day). Errors consist of initial frequency error, and variations due to temperature and long-term drift. Because of these inaccuracies, there is little point in specifying times or frequencies to more than five or six digits as I have done above. If required, frequency accuracy can be improved by installing a high quality, close-tolerance crystal, or a high quality crystal oscillator module, which will reduce all of the above error sources. If accuracy is still inadequate, with a crystal it may be possible to add a small variable capacitor to the oscillator circuit, to 'pull' the crystal onto the correct frequency. If anyone has specific advice on this, please let me know. (*) Alternatively, your software could incorporate an adjustment so that once the amount of error has been measured, manually by the user over a long period of time, it could be corrected by the software. Of course this must be configured individually for every machine the software will run on, and temperature and long term drift will still have an effect. Historical note: If you were wondering "Wouldn't 1 MHz have been easier?", yes it would, but that would have required an extra crystal. IBM were... er, 'clever' - they used a master clock of 14.31818 MHz, and used logic chips to derive the 4.77 MHz CPU clock, the timer clock, and the NTSC colour subcarrier frequency for the CGA card, so they could save a few dollars. Although the 14.31818 MHz signal is not required by modern CPUs and video cards (in fact, it is now only used for the CTC clock!), the strange frequency still hangs around like a stale fart - we are stuck with it forever. :-( ## 7.3 THE COUNTER/TIMER CHIP (CTC) The counter/timer chip (CTC) in the IBM PC family is an Intel 8253 in the PC and XT, or an Intel 8254 in the AT and later machines (except the PS/2 {JAM}) or a functional equivalent, and is part of the processor support chipset on the motherboard. On modern motherboards, it is part of one ASIC in a chipset. The CTC has three fully independent channels, numbered zero, one, and two. Each has a clock input, a gate input, and an output, and in the PC, family, these are wired as follows: Chan Clock input Gate input Output Channel is used for ---- ----------- ---------- ------ ------------------- 0 1.193182 MHz Tied high To IRQ0 Timer tick 1 1.193182 MHz Tied high DRAM refresh DRAM refresh 2 1.193182 MHz Timer 2 Gate Speaker gating Audio generation Software access to the CTC is via four adjacent addresses in the directly addressable I/O page. Programming information starts at section ¯¯ 7.9. In most respects the 8253 and 8254 are identical. The following description applies to both types of CTC unless specifically stated. ## 7.4 CTC CHANNELS Each channel operates independently, and can be programmed for one of six modes of operation. Normally, modes 2 or 3 are used. In these modes, the CTC channel takes the CTC clock (1.193182 MHz) and 'divides' this frequency down to produce a lower frequency at the output pin. Other modes operate differently. The frequency division is controlled by the 'divisor' value, a 16-bit unsigned number between 1 and 65536 (65536 is represented as zero), which is individually programmable for each channel in the CTC. Setting a very small divisor value gives a very high output frequency. A divisor of 65536 gives the lowest output frequency, 18.206507364909 Hz (cycle period is 54.92541649846559 ms). ## 7.4.1 CTC CHANNEL ZERO CTC channel zero normally operates in mode two or three with a divisor of 65536, giving an output frequency of 18.2065 Hz (period is 54.9254 ms). Its gate input is tied high. Its output drives the IRQ0 input of the primary PIC (8259 interrupt controller chip). On every rising edge of the channel zero output pin (i.e. transition from low to high), IRQ0 is triggered, invoking interrupt 8, the timer tick interrupt (see section ¯¯ 6.1). ## 7.4.2 CTC CHANNEL ZERO DEFAULT OPERATING MODE Traditionally, CTC channel zero has been set to operate in mode three by the BIOS POST, but recent 486 BIOSes that I have seen appear to be using mode two by default. The only significant differences are the width of the pulse from the CTC pin that triggers the timer tick interrupt, which is narrow in mode two but is still plenty wide enough for the Intel 8259 PIC chip, and the value read from the CTC channel zero counter (which decrements twice as quickly in mode 3). From a hardware point of view, either mode should work on all motherboards, but if some code in the BIOS assumes that CTC channel 0 is in the mode that the BIOS originally programmed, it may not work correctly if CTC channel 0 has been reprogrammed for the other. Of course, reprogramming the CTC divisor for a higher sample rate will also cause this problem. The only example of this that I know of, is the joystick read function (int 15h called with AH = 84h and DX = 1) (see section ¯¯ 10.4.2). Please tell me if you find any other problems related to changing the mode. (*) ## 7.4.3 CTC CHANNEL ONE CTC channel one triggers DRAM refresh cycles. DRAM (Dynamic Random Access Memory) is the main system memory in your computer (typical machines have four to eight megabytes of RAM, or 32 to 64 megabytes if you want to run Win 95 :-) DRAM stores data as electrical charges on tiny capacitors inside the chip, and this type of memory must be refreshed regularly to prevent the capacitors from discharging. On the PC and XT, refresh cycles are implemented via the DMA controller. On the AT and later machines, refresh cycles are performed by dedicated hardware. It appears that the AT does use CTC channel one to initiate DRAM refreshes, but I have heard that you cannot change the refresh rate on ATs and later machines. Can anyone shed any light on this? (*) The normal divisor for CTC channel one is 18, which gives a DRAM refresh cycle every 15.0857162013608 microseconds. Every refresh cycle forces the processor to wait briefly, and a popular trick used to be to slow down the DRAM refresh rate on PCs and XTs by increasing the divisor, to reduce the refresh overhead, giving a few percent performance improvement, so your flash 8MHz Turbo XT would actually seem to run at 8.05 MHz! Seems pretty pathetic now, doesn't it :-) CTC channel one is not even accessible on the PS/2's ASIC {JAM}. CTC channel one has no interrupt connection, but can be used for timing via the Refresh Detect signal on bit 4 of Port B. See section ¯¯ 7.37. ## 7.4.4 CTC CHANNEL TWO CTC channel two generates audio for the speaker. It is the most versatile CTC channel, because its gate input can be controlled by software, and its output can be read by software via Port B (see section ¯¯ 5.5). CTC channel two can be used for timing, but it cannot generate an interrupt. See section ¯¯ 7.29 and section ¯¯ 7.31 for examples of programming CTC channel two. See the section ¯¯ 5.5 for details of the speaker interface. ## 7.5 SPEAKER INTERFACE The speaker interface on the PC and XT is implemented via the 8255 PPI chip, which occupies I/O addresses 60h to 62h inclusive, and also provides the interface to the keyboard. Port B (read/write, at I/O address 61h) and Port C (read-only, at I/O address 62h) are used by the speaker interface. On the AT and later machines, which do not have a PPI chip, these functions are implemented in an ASIC in the chipset, or with discrete logic, as a partly read-only, partly read/write register at I/O address 61h, known as Port B. In most respects, the PC/XT and AT interfaces are similar. CTC channel two gate input can be controlled by software via a read/write bit in an I/O register; this signal is known as Timer 2 Gate. The CTC channel two output pin can be read back directly, via a read-only bit in an I/O register, and is AND-gated with a signal called Speaker Data (software controlled, via a read/write I/O register bit), the speaker being driven from the output of the AND gate, sometimes via a simple resistor-capacitor lowpass filter to remove high frequency components. On the PC and XT only, the speaker control signal (after the AND gate, and inverted) can also be read back by software, though this seems to be an undocumented feature and may not work on all machines. Figure 1 (FIGURES archive) shows the speaker interface signals and circuitry. PC and XT : I/O address 61h, "PPI Port B", read/write 7 6 5 4 3 2 1 0 * * * * * * . . Not relevant to speaker - do not modify! . . . . . . * . Speaker Data . . . . . . . * Timer 2 Gate PC and XT : I/O address 62h, "PPI Port C", read only 7 6 5 4 3 2 1 0 * * . . * * * * Not relevant to speaker, read-only . . * . . . . . Timer 2 output read-back . . . * . . . . Speaker signal (after AND gate, inverted), undocumented AT and later : I/O address 61h, "Port B", partly read/write, partly read-only 7 6 5 4 3 2 1 0 * * . . . . . . Not relevant to speaker, read-only . . * . . . . . Timer 2 output read-back, read-only . . . * . . . . Refresh Detect (read-only), see section ¯¯ 7.37 . . . . * * . . Not relevant to speaker - do not modify! (read/write) . . . . . . * . Speaker Data (read/write) . . . . . . . * Timer 2 Gate (read/write) I have a nasty suspicion that the PS/2 may not implement Port B properly. Can anyone confirm or deny this? (*) Audio generation can be done via CTC channel two, by setting Timer 2 Gate high and Speaker Data also high. This enables channel two, and enables its output to control the speaker directly. Alternatively, if Timer 2 Gate is set low, CTC channel two output goes high (assuming an appropriate mode is programmed for channel two), and Speaker Data can be manipulated to drive the speaker directly. The former technique is used in the sample program in section ¯¯ 7.30. Here is a code fragment that determines whether the speaker hardware is the PC/XT type or the AT type. It uses bit 7 of the I/O port at 61h. On an XT, PPI Port B is fully read/write, and bit 7 is the keycode acknowledge signal to the keyboard interface on the motherboard. On an AT, bits 4-7 of Port B are read-only, and bit 7 is the motherboard RAM parity error signal. By toggling bit 7 six times and testing whether the port reads the expected value, this code fragment determines what type of Port B hardware and keyboard interface is present. This code destroys AX and CX. -------------------------------- snip snip snip -------------------------------- pushf ; Keep interrupt flag mov cx,400h ; Six attempts (top bits of CH) cli ; Lock out interrupts during this stuff in al,61h ; Get Port B contents jmp SHORT $+2 ; Short delay mov ah,al ; Original value to AH Flip61Loop: xor ah,10000000b ; Flip top bit mov al,ah ; Get value to AL out 61h,al ; Write value to port jmp SHORT $+2 ; Short delay jmp SHORT $+2 ; Short delay in al,61h ; Read it back xor al,ah ; Set bit 7 if value didn't stay shl al,1 ; Shift bit into carry rcl cx,1 ; Shift bit into bottom of CX jnc Flip61Loop ; Loop if more flips (six in total). popf ; Restore interrupt flag test cl,cl ; Was port read/write? Zero if so. -------------------------------- snip snip snip -------------------------------- This code fragment will leave the zero flag true if the machine is a PC or XT (i.e. Port B bit 7 is read/write), or zero flag false if the machine is an AT or later machine (i.e. Port B bit 7 is read-only). You could follow it with the instruction: jnz Not_PCXT ; If not, it's an AT ## 7.6 CTC INTERNAL REGISTERS Each CTC channel operates independently. Each channel contains: þ A 6-bit Mode register þ A 16-bit Reload register (the 'divisor register' in modes 2 and 3) þ A 16-bit Counting register (the 'Counting Element' in Intel docs) þ A 16-bit Latch register þ An 8-bit Status Latch register þ A lobyte/hibyte flag þ A 'T' (toggle) flip-flop, used in mode three The major functional blocks are shown in Figure 3 (in the FIGURES archive). The Mode register controls the operating mode (section ¯¯ 7.8) and the access mode (see section ¯¯ 7.7) of the channel. It is written at the start of the programming sequence. When it is written, the channel output pin usually goes into a defined state - see the individual mode descriptions, section ¯¯ 7.8. The Reload register can be programmed by software. The Counting register is reloaded from this register at certain times (depending on the operating mode). In modes 2 and 3, which operate as frequency dividers, this register is also called the divisor register. The Counting register is a down-counting 16-bit counter. Its exact behaviour depends on the operating mode, but generally it counts down on every CTC clock pulse (0.8381 us). It cannot be read directly - it is always read via the Latch register. The Latch register is a 16-bit software-readable transparent latch which follows the Counting register unless the Latch command is issued. This command makes the Latch register freeze, so that a stable count value can be read. The 8-bit Status Latch register is used with the read-back function when the channel status is latched, see section ¯¯ 7.18. The lobyte/hibyte flag is an internal flag which determines which half of the 16-bit Reload and Latch registers will be accessed through the 8-bit access port. ## 7.7 ACCESS MODES Because the I/O interface to the CTC is only eight bits wide, the CTC implements three Access modes which control how values are written to the Reload registers and read from the Latch registers. þ Lobyte only þ Hibyte only þ Lobyte then hibyte (using the lobyte/hibyte flag) If lobyte only, or hibyte only, are selected, the registers are read or written with a single access. If lobyte/hibyte access is selected, a read or write to the data port will access the lobyte or hibyte of the registers, according to the lobyte/hibyte flag, which toggles automatically after each register access. In the lobyte/hibyte access mode, two 8-bit accesses are required to fully read the Latch register and to fully write the Reload register. Regardless of the access mode, the Counting register always operates as a 16-bit counter. If a channel is set for lobyte-only or hibyte-only access, when the data port is written, the other byte is taken to be zero. For example, for a channel set for lobyte-only access, writing 50 to the data port will set the reload register to 50, and a write of zero to the data port will set the reload register to 0, i.e. a divisor of 65536 in modes 2 and 3. For a channel set for hibyte-only access, a write of 50 to the data port will load the reload register with 12800. ## 7.8 CTC OPERATING MODES Each channel in the CTC can be independently set to one of six operating modes: þ Mode 0: Interrupt on terminal count þ Mode 1: Hardware-retriggerable one-shot þ Mode 2: Rate generator þ Mode 3: Square wave generator þ Mode 4: Software-triggered strobe þ Mode 5: Hardware-triggered strobe While reading the mode descriptions below, you may want to refer to section ¯¯ 7.3 and ¯¯ 7.4 for the gate and output connections for each channel. ## 7.8.1 OPERATING MODES: BEHAVIOUR COMMON TO ALL MODES When the mode word is written, all internal logic in the channel, including the lobyte/hibyte flag, is reset, and the output immediately goes to the initial state (which depends on the mode). A new value can be written into the Reload register at any time. The operating mode determines the exact effect that this will have, see the individual mode descriptions below. Loading and decrementing of the Counting register occurs on the _falling_ edge of the CTC clock input. The CTC samples the gate input on the _rising_ edge of the CTC clock input. In modes one, two, three, and five, a rising edge on the gate input sets an internal flip-flop, whose output is sampled on rising edges of CTC clock. This flip-flop is reset after its output has been sampled. Therefore the timing of the rising edge on gate need not be synchronised with CTC clock. In modes where falling edge on CTC clock loads the Counting register and also decrements it, the Counting register is not decremented on the CTC clock pulse which loads it. It starts decrementing on the _next_ CTC clock. The BCD/Binary flag allows BCD operation to be selected. In BCD mode, the Counting register operates in 4-digit binary-coded-decimal format. If the Counting register is zero and is decremented, it wraps around to 9999 hex. The Intel documentation does not describe how the chip will behave if the Reload register contains any digits outside the range 0-9 and I have not tested to find this out, as it may be implementation dependent. Also this feature is not normally used in the PC, and may well be non-functional on some workalikes (chipsets). In other words, don't use BCD mode! ## 7.8.2 OPERATING MODE ZERO: INTERRUPT ON TERMINAL COUNT When the mode word is written, the output pin goes low and the CTC waits for the Reload register to be loaded by software, whereupon it transfers the value in the Reload register into the Count register on the next falling edge of the CTC clock. Subsequent falling edges of CTC clock will decrement the Counting register _if the gate input is high_. If the gate is low, the Counting register will not decrement. The gate input is sampled on the rising edge of CTC clock. When the Counting register decrements from one to zero, the output goes high, and remains high until another Mode word is written, or another value is written into the Reload register. The Counting register continues to count even after it has decremented to zero - it wraps around to FFFF hex (9999 in BCD mode) - but this doesn't affect the output pin state. The Reload register may be written at any time. In two-byte access mode, when the first byte of the Reload register is written, counting stops and the output goes low. Once the Reload register is loaded, the next clock pulse will load the Counting register from the Reload register, and counting will resume, starting from the new value. See section ¯¯ 7.31 for an example of this mode being used with channel two and section ¯¯ 10.7 for this mode being used in PWM audio generation. ## 7.8.3 OPERATING MODE ONE: HARDWARE-RETRIGGERABLE ONE-SHOT This mode uses the gate input as a trigger. Gate is sampled on the rising edge of CTC clock. The trigger occurs on the rising edge of the gate input. When the mode word is written, the output pin goes high and the CTC waits for the Reload register to be loaded by software. It is then armed, and waits for a rising edge on the Gate input. Once this is detected, the next falling edge of CTC clock sets the output low and transfers the Reload register into the Counting register, and counting is enabled. On every subsequent falling edge of CTC clock, the Counting register decrements. When the Counting register decrements from one to zero, the output returns high and remains high, though the Counting register continues to decrement (it wraps around). During the counting period, the gate input may go low, and this will be ignored. A rising edge on gate during counting (a re-trigger) will cause the Reload register to be transferred into the Counting register on the next falling edge of CTC clock, as above, thus restarting the timer and extending the low-pulse at the output. The Reload register may be written at any time, but this will not affect the count in progress. This will affect the value reloaded into the Counting register when re-triggered. This mode is not used with channel 0 or 1, as their gate inputs are tied high. ## 7.8.4 OPERATING MODE TWO: RATE GENERATOR In mode two, the channel operates as a frequency divider. The reload register becomes the divisor, by which the CTC clock frequency is divided, to produce the output frequency. A low gate input stops the counter. When gate returns high, the counting register is reloaded and the count sequence begins again. When the mode word is written, the output goes high. When the Reload register has been written, the Reload register is transferred to the Counting register on the next falling edge of the CTC clock. The Counting register decrements by one on every falling edge of CTC clock. When the Counting register is decremented to one, the channel's output goes low. On the next falling edge of CTC clock the Counting register is reloaded from the Reload register, the output returns high, and the cycle continues. If the gate input goes low, counting stops and the output goes high immediately. Once the gate input has returned high, the next falling edge on CTC clock reloads the Counting register from the Reload register and operation continues. Programming a new value into the Reload register does not affect the count in progress. The next reload (due to the Counting register reaching 1 or due to the gate input going low then high) starts from the newly programmed value. A divisor (Reload register) value of one must _not_ be used with this mode. To summarise, the Counting register starts at the Reload register value and decrements down to one, then reloads. The output is low while the Counting register is equal to one. Thus output pulses are generated at 1.193182 MHz divided by the Reload register (divisor) value. The period between output pulses is the CTC clock period (0.8381 us) multiplied by the Reload register (divisor) value, and they are one CTC clock period wide. This makes mode two unsuitable for use with timer two for generating audio for the speaker, because the speaker cannot respond to such short pulses. For this reason, the 8254/8253 has operating mode three. ## 7.8.5 OPERATING MODE THREE: SQUARE WAVE GENERATOR Like mode two, mode three operates as a frequency divider. The difference is in the output signal. Whereas mode two produces a short pulse for every timer reload, mode three produces a square wave output. In this mode, the reload pulse is fed into an internal 'T' (toggle) flip-flop, which toggles (reverses state) on each pulse, and the output of this flip-flop becomes the output signal. Every time the Counting register reloads, the output pin toggles to the opposite state. This gives a square wave output, with equal high and low times (i.e. a 50% duty cycle, or 1:1 mark to space ratio). If an odd divisor is used, the duty cycle is not exactly 50% (as explained below). However, two reloads are needed to produce one output cycle, so the reload rate must be doubled to compensate for the halving action of the 'T' flip-flop. This is accomplished by making the Counting register decrement by two instead of by one for every CTC clock. So in mode three, the Counting register decrements in steps of two and reloads twice as fast as it would in mode two, and the twice- speed reload frequency is halved by the 'T' flip-flop to produce an even square wave output at the correct frequency. Odd divisor values are handled strangely. On every reload, the Reload register minus one (which will be an even value) is loaded into the Counting register. If the output pin is high, the chip waits until the Counting register has decremented to zero (not one, as would be normal), and reloads the Counting register on the next CTC clock after that. If the output pin is low, it reloads the Counting register after the Counting register reaches one, as normal. This makes the high pulse one CTC clock cycle wider than the low pulse, and shifts the output square wave's duty cycle slightly above 50%. The duty cycle error is only significant if the divisor value is small. The output pin goes high immediately when the mode word is written. Once the Reload register has been written, counting begins. If the gate input drops low, counting stops and the output pin goes high immediately. When the gate input has returned high, the next falling edge on CTC clock reloads the Counting register from the Reload register, leaving the output pin high, and counting resumes. If the Reload register is written while counting is in progress, the new value has no effect until a reload occurs, either due to the gate input going low then high, or due to a normal reload, which happens twice for every output cycle. A divisor (Reload register) value of one must _not_ be used with this mode. As well as the different output generated by the timer in modes two and three, there is a difference when the timer is read on-the-fly - see section ¯¯ 9. ## 7.8.6 OPERATING MODE FOUR: SOFTWARE-TRIGGERED STROBE Mode four operates as a retriggerable delay, generating a pulse when the delay expires. When the mode word is written, the output pin goes high. Once the Reload register has been written, the next falling edge of CTC clock loads the Counting register from the Reload register, and counting begins. When the Counting register decrements to zero, the output goes low for one CTC clock pulse then returns high. The Counting register continues to decrement, wrapping round to FFFF hex (or 9999 hex in BCD mode), but no more output pulses will occur. If the Reload register is written during counting, after the Reload register is fully written (both bytes, if programmed for lobyte/hibyte access), the next falling edge of CTC clock reloads the Counting register, retriggering the delay period or starting a new delay if the previous delay had expired. A low gate input disables counting but the gate input has no other effect. ## 7.8.7 OPERATING MODE FIVE: HARDWARE-TRIGGERED STROBE Mode five is a cross between mode one and mode four, using a rising edge on the gate input to trigger or retrigger the delay period. When the mode word is written, the output pin goes high and the CTC waits for the Reload register to be loaded by software. It is then armed, and waits for a rising edge on the Gate input. Once this is detected, the next falling edge of CTC clock transfers the Reload register into the Counting register, and counting is enabled. On every subsequent falling edge of CTC clock, the Counting register decrements. When the Counting register decrements to zero, the output goes low for one CTC clock pulse width then returns high. The Counting register continues to decrement, wrapping round to FFFF hex (or 9999 hex in BCD mode), but no more output pulses will occur until the channel is re-triggered by another rising edge on the gate input. During the counting period, the gate input may go low, and this will be ignored. A rising edge on gate during counting (a re-trigger) will cause the Reload register to be transferred into the Counting register on the next falling edge of CTC clock, as above, thus restarting the timer and re-triggering the delay. The Reload register may be written at any time, but this will not affect the count in progress. This will affect the value reloaded into the Counting register when re-triggered. This mode is not used with channel 0 or 1, as their gate inputs are tied high. ## 7.9 THE 8254/8253 REGISTERS On the PC family, the 8254/8253 timer occupies four I/O addresses in the directly addressable I/O page, as follows: 40h Channel 0 data port (read/write) 41h Channel 1 data port (read/write) 42h Channel 2 data port (read/write) 43h Mode/Command register (write only - read is ignored) ## 7.9.1 THE MODE/COMMAND REGISTER The Mode/Command register at I/O address 43h is defined as follows: 7 6 5 4 3 2 1 0 * * . . . . . . Select channel: 0 0 = Channel 0 0 1 = Channel 1 1 0 = Channel 2 1 1 = Read-back command (8254 only) (Illegal on 8253) (Illegal on PS/2 {JAM}) . . * * . . . . Command/Access mode: 0 0 = Latch count value command 0 1 = Access mode: lobyte only 1 0 = Access mode: hibyte only 1 1 = Access mode: lobyte/hibyte . . . . * * * . Operating mode: 0 0 0 = Mode 0, 0 0 1 = Mode 1, 0 1 0 = Mode 2, 0 1 1 = Mode 3, 1 0 0 = Mode 4, 1 0 1 = Mode 5, 1 1 0 = Mode 2, 1 1 1 = Mode 3 . . . . . . . * BCD/Binary mode: 0 = 16-bit binary, 1 = four-digit BCD You might prefer the following diagram and explanation. 7 6 5 4 3 2 1 0 ÚÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄ¿ ³ SC1 ³ SC0 ³ RL1 ³ RL0 ³ M2 ³ M1 ³ M0 ³ BCD ³ ÀÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÙ ³ ³ ³ ³ ³ ³ ³ ³ COMMAND SELECT BITS MODE SPECIFIER BITS ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ÃÄ Binary/BCD mode ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ 0 = Binary ³ ³ ³ ³ ³ ³ ³ 1 = BCD ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ÃÄÄÄÄÄÅÄÄÄÄÄÅÄÄ Mode number ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ 0 0 0 = Mode 0 ³ ³ ³ ³ 0 0 1 = Mode 1 ³ ³ ³ ³ 0 1 0 = Mode 2 ³ ³ ³ ³ 0 1 1 = Mode 3 ³ ³ ³ ³ 1 0 0 = Mode 4 ³ ³ ³ ³ 1 0 1 = Mode 5 ³ ³ ³ ³ 1 1 0 = Mode 2 ³ ³ ³ ³ 1 1 1 = Mode 3 ³ ³ ³ ³ ³ ³ ÃÄÄÄÄÄÅÄÄ Latch/Read/Write operation ³ ³ ³ ³ ³ ³ 0 0 = Latch count value command (for read) ³ ³ 0 1 = Read/Write lobyte only ³ ³ 1 0 = Read/Write hibyte only ³ ³ 1 1 = Read/Write lobyte then hibyte ³ ³ ÃÄÄÄÄÄÅÄÄ Timer/counter number ³ ³ 0 0 = Select channel 0 0 1 = Select channel 1 1 0 = Select channel 2 1 1 = Read-back command on 8254 (not allowed on 8253 and PS/2) The SC1 and SC0 (Select Channel) bits form a two-bit binary code which tells the CTC which of the three channels (channels 0, 1, and 2) you are talking to, or specifies the read-back command. As there are no 'overall' or 'master' operations or configurations, every write access to the mode/command register, except for the read-back command (see section ¯¯ 7.18), applies to one of the channels. These bits must always be valid on every write of the mode/command register, regardless of the other bits or the type of operation being performed. The RL1 and RL0 bits (Read/write/Latch) form a two-bit code which tells the CTC what access mode you wish to use for the selected channel, and also specify the Counter Latch command to the CTC. For the Read-back command, these bits have a special meaning (section ¯¯ 7.18). These bits also must be valid on every write access to the mode/command register. The M2, M1, and M0 (Mode) bits are a three-bit code which tells the selected channel what mode to operate in (except when the command is a Counter Latch command, i.e. RL1,0 = 0,0, where they are ignored, or when the command is a Read-back command, where they have special meanings, see section ¯¯ 7.18). The modes are described in section ¯¯ 7.8 and subsections. These bits must be valid on all mode selection commands (all writes to the mode/command register except when RL1,RL0 = 0,0 or when SC1,0 = 1,1). Like the Mode specification, the BCD bit must be valid on all mode selection commands. This bit simply specifies whether the channel will count in binary (the usual mode) or BCD, when it will behave as four separate cascaded 4-bit BCD counters. The counters always count DOWNWARDS, which can make BCD mode awkward to use. Also see section ¯¯ 7.8.1. ## 7.9.2 THE DATA PORTS Writing to the data ports sets the Reload register (one or two writes are used, according to the access mode - see section ¯¯ 7.7). Reading the ports returns the Latch register (lobyte, hibyte, or alternating lobyte and hibyte, depending on the access mode, see section ¯¯ 7.7) or the status register if a status read-back command has just been issued (see section ¯¯ 7.18). ## 7.9.3 ACCESSING THE REGISTERS Accessing a CTC channel involves writing one byte to the mode/command register at I/O address 43h, to tell the chip what you want to do, followed by reading or writing one, two or sometimes three bytes in succession, to or from the data port for the appropriate channel. This should always be done with interrupts disabled, because the CTC "remembers where it's up to", and will get confused if the normal sequence of register accesses is interrupted. Always use byte-sized I/O instructions to access these ports. In assembly, use OUT nn,AL or IN AL,nn (not AX). In C, use inportb() and outportb() or the equivalent 8-bit I/O functions or pseudofunctions for your compiler. ## 7.9.4 I/O RECOVERY DELAYS Modern CPUs operate internally and externally at very high speeds. Modern fast machines must be compatible with old ISA bus cards, which have slow peripheral devices such as serial ports, parallel ports, video and disk controllers, etc, accessed via the CPU's I/O space. I/O-addressed peripherals on the motherboard (the 8254/8253 CTC, the 8237 DMA controllers, the 8259 interrupt controllers, the real time clock, etc) are also slow by the standards of a modern CPU. On these fast machines, whenever the CPU makes an access to an I/O device (via the IN and OUT instructions and variants), hardware on the motherboard must slow down the access, in order to guarantee that the timing requirements of the slow peripheral are not violated (i.e. to give the peripheral enough time to provide or accept the data correctly and prepare for the next data transfer). There are two parameters of interest - the access time, and the recovery time. These times are in the order of several hundred nanoseconds, but depend on the motherboard and peripheral device in question. These times do not apply to memory accesses, which are cached and are much faster and use few wait states. The following diagram is a _simplified_ representation of what happens when the CPU executes two I/O read instructions (for example, "in al,42h / in al,40h"). Ú Valid ÚÄ42hÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄ40hÄÄÄÄÄÄÄÄÄÄÄÄ¿ ADDRESS ³ ³ ³ ³ ³ À Not valid ÄÄÄÙ ÀÄÄÄÄÄÄÄÄÙ ÀÄÄ... : : : : Ú Valid ÚÄIORÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄIORÄÄÄÄÄÄÄÄÄÄÄÄ¿ CONTROL ³ ³ ³ ³ ³ À Not valid ÄÄÄÙ ÀÄÄÄÄÄÄÄÄÙ ÀÄÄÄ... ÃÄÄÄTaccÄÄÄ´ ÃÄÄTrecÄÄÅÄÄÄTaccÄÄÄ´ : Ú Valid : ÚÄÄÄÄÄ¿ : ÚÄÄÄÄÄ¿ DATA ³ : ³ ³ : ³ ³ À Not valid ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄ... : : : : : : : : NOTES time--> a b c d e f g h At point 'a' the CPU makes an I/O read request. The address and control buses become valid. The address decoding logic sees that the address is in the range 40h-43h and selects the CTC. From this point, the CTC takes Tacc (the access time) to get itself ready and present the data on the data bus. At point 'b' the CTC has made the data available on the data bus. At 'c' the CPU reads the data from the data bus. At point 'd' the cycle is complete and the address and control buses go inactive. The CPU transfers the data into the AL register. At point 'e' the CPU generates a second access cycle just like the first. The peripheral also requires a certain amount of time to elapse between points 'd' and 'e' - this is called the peripheral's recovery time. The access time is the time required by the peripheral to accept data correctly (for an I/O write) or provide data correctly (for an I/O read). It is required on every access to an I/O device. The recovery time is the time required by the peripheral _after_ an I/O access, before the peripheral is ready to receive another I/O request. An analogy would be a little old lady in a car at an intersection. When the light changes, she fumbles around looking for the handbrake, then she tries to remember which pedal to push to go faster. Then she finally takes off. This is like the access time requirement. Then at the next intersection, she has to slow down and stop, and get ready for the lights to change again. This takes time. If the lights change before she has stopped and finished getting ready, she will do something stupid like crunching the gearbox or driving into a tree. This requirement is the recovery time. On slow motherboards (the old PC and XT, and probably most 286-based boards), the access time and recovery time are both guaranteed to be met because the CPU's bus interface is fairly slow, and comparatively fast peripheral devices are used (the 8254's recovery time is 165 or 200 ns, compared to the old 8253's recovery time of 1000 ns!). On fast motherboards, the access time is assured because the chipset on the motherboard inserts I/O wait states, but on some fast motherboards, notably some 286 and early 386 motherboards, the _recovery_ time is not guaranteed. The motherboard says "I have to wait until this peripheral is ready, but after the access is complete, I don't care". With these boards, two back-to-back I/O accesses to the _same_ peripheral (such as the sequence shown in the diagram above) will cause the second access to be ignored or misinterpreted by the peripheral. This design misfeature of some 286 and 386 motherboards is probably the result of a design compromise. From the point of view of a chipset designer there are several ways to deal with I/O recovery time requirements - 1. Enforce a recovery time after every I/O access, or 2. Enforce a recovery time between any back-to-back I/O accesses, or 3. Enforce a recovery time between back-to-back I/O accesses to the _same_ peripheral, or 4. Never enforce a recovery time after an I/O access. The first alternative would slow the machine unduly, because the recovery time would be enforced even if the next accesses were memory accesses (which would not affect the I/O-addressed peripheral). The second option is complicated to implement (though modern motherboards use this method, I believe). The third option is even more complicated. The fourth is the simplest approach, but it means that back-to-back accesses to the same peripheral will violate that peripheral's recovery time requirements. To support these motherboards, programmers would insert the famous "jmp short $+2" sequence into their code between back-to-back I/O accesses to the same device. The instruction is effectively a NOP (no-operation) instruction but it has an extra delaying effect because it clears the processor's instruction prefetch queue on the 286 and 386, requiring an external bus access, which must wait for the I/O access cycle to complete. Modern motherboards detect back-to-back I/O accesses, and insert wait states to ensure that recovery times are not violated, so there is no need to use this trick with them, but to support older systems, you may wish to do so. The sample code and programs in this document do use the "jmp short $+2" trick, because I am in the habit of using it. In C, you could use the inline assembler feature of most compilers, but Michael Mauch (mauch@uni-duisburg.de) advises that the optimiser may optimise out this instruction, so he suggests (for Borland C++ 3.1 and 4.0), __emit__(0xEB,0x00); which is not optimised out. You could set up a macro, e.g. #define breather __emit__(0xEB,0x00). In Turbo Pascal, you could use the appropriate directive to emit the two-byte instruction. The object code is $EB/$00. If anyone knows a better or more generic way to implement this in C and/or Pascal, please tell me. (*) An alternative method to the "jmp short $+2" method is to insert an access to another I/O location. This enforces another access time delay, which should cover the recovery time requirements of the first device. You could then interleave accesses to the device you are interested in, with accesses to the 'dummy' device. Apparently it is common practice to use "in al,61h" as the dummy instruction for this purpose. Port 61h is Port B (see section ¯¯ 7.5) and it can be read at any time with no unusual side-effects, so is ideal for this purpose, except that the IN instruction destroys AL, which is often inconvenient. An OUT instruction is more covenient but there is no port that can safely have any value OUTed to it. In the quoted message below, Bob Smith (bobs@access.digex.net) mentions that some IBM BIOSes use an OUT to port 4Fh (an unused I/O address) to insert delays. In an article in Usenet newsgroup comp.lang.asm.x86 in December 1995, Bob Smith (bobs@access.digex.net) posted the following interesting information: > The reason there is a short jump to the next instruction is certainly, as > [most people would say], that some I/O devices need more recovery time. > Moreover, the 386 processor treats a flush of the prefetch queue specially > with respect to I/O operations. The problem is that that trick doesn't work > any more! Quoting from the (now out-of-print) "i486 Microprocessor Data > Sheet" (Intel order #240440-001): > > "6.3.1 WRITE BUFFERS AND I/O CYCLES > > "Input/Output (I/O) cycles must be handled in a different manner by the write > buffers. I/O reads are never reordered in front of buffered memory writes. > This ensures that the 486 microprocessor will update all memory locations > before reading status from an I/O device. > > "The 486 microprocessor never buffers single I/O writes. When processing an > OUT instruction, internal execution stops until the I/O write actually > completes on the external bus. This allows time for the external system to > drive an invalidate into the 486 microprocessor or to mask interrupts before > the processor progresses to the instruction following OUT. Repeated OUT > instructions will be buffered. > > "I/O device recovery time must be handled slightly differently by the 486 > microprocessor than with the 386 microprocessor. I/O device back-to-back > write recovery times could be guaranteed by the 386 microprocessor by > inserting a jump to the next instruction in the code that writes to the > device. The jump forces the 386 microprocessor to generate a prefetch bus > cycle which can't begin until the I/O write completes. > > "Inserting a jump to the next write will not work with the 486 microprocessor > because the prefetch could be satisfied by the on-chip cache. A read cycle > must be explicitly generated to a non-cacheable location in memory to > guarantee that a read bus cycle is performed. This read will not be allowed > to proceed to the bus until after the I/O write has completed because I/O > writes are not buffered. The I/O device will have time to recover to accept > another write during the read cycle." > > FWIW, I have seen some BIOSes (in IBM systems) use an OUT (of any value) to > I/O port 4Fh (an otherwise unused port) in order to provide the needed > synchronization. Thanks Bob for that information. I believe Glen Blankenship (obother@netcom. com) also quoted the same information in a separate message. ## 7.10 PROGRAMMING THE MODE AND RELOAD REGISTER Until initialised, all channels are in an undefined state. The BIOS POST sets the operating modes for all channels. Channels can be programmed in any order. For any particular channel, the Mode register must be programmed first. Once the mode is set, one or two bytes (depending on the access mode - see section ¯¯ 7.7) are written into the data port for that channel; these are loaded into the Reload register. The channel is then initialised and begins operating according to the programmed mode. To program the mode and reload value for a CTC channel, issue a command byte of ccaammmb binary, where 'cc' = channel number, 'aa' = access mode, 'mmm' = mode, 'b' = BCD/binary selection (see section ¯¯ 7.9.1 for the bit definitions) then write the lobyte, or the hibyte, or lobyte then hibyte (depending on the access mode) of the reload value to the data port of the selected channel. Note - a reload value of 1 should NOT be used in modes two and three. Also, in these modes, low reload values will give very high output frequencies, and are not normally used with channel zero because the tick rate would be too high. The Reload register may be reprogrammed at any time, just by writing the lobyte, hibyte, or lobyte then hibyte (depending on the access mode), to the data port. See section ¯¯ 7.7 and subsections for details. ## 7.11 EFFECT OF REPROGRAMMING CHANNEL ZERO ON THE TIMER TICK INTERRUPT The system time is maintained using the timer tick interrupt, and reprogramming the mode of channel zero will reset the channel. When the mode word is written, the channel zero output pin of the CTC goes high immediately. If it was already high, no interrupt is generated. If it was low, an interrupt _is_ generated, causing the BIOS timer tick count variable to be incremented incorrectly. If CTC channel zero was previously programmed for mode 2, its output would already be high, and no extra interrupt would be generated. This should not be done continuously in an application unless you restore the correct DOS time at termination. Normally it is sufficient to reprogram channel zero at the start of your program, and leave it in that mode until finished. This does cause a slight jump in the time, but as it only happens once on every run of the program, it is not really worth worrying about. If you want to be more careful, you can wait until the timer tick interrupt occurs, and reprogram channel zero immediately after the interrupt has occurred. You can detect the interrupt by watching the BIOS tick count variable until it changes (only the loword need be monitored, as it always changes on each tick interrupt). When the interrupt has just occurred, the channel zero output pin will be high, so reprogramming the channel will not generate an interrupt, and the Counting register will be near the start of its 54.9254 ms cycle. A similar approach should be used when terminating the program, after channel zero has been reprogrammed with a smaller divisor to give a faster tick rate. Assuming your int 8 handler chains to the BIOS int 8 handler every 54.9254 ms, wait until the tick count changes (i.e. your int 8 handler has called the BIOS int 8 handler), then reprogram channel zero with the default parameters (mode 3 or 2, divisor of 65536). These considerations do not apply when the Reload register is loaded without a mode initialisation command written to the Mode/Command register, as done with the dynamic interrupt rate technique (see section ¯¯ 8.6). ## 7.12 SAMPLE PROGRAM: PROGRAMMING THE MODE AND RELOAD VALUE This function programs the operating mode and the reload value (the divisor in modes two and three) for a specified channel. If you use channel zero in a non-standard setup, you should restore it to its normal mode and divisor (mode two or three, with a divisor of 65536) when you've finished using it. See section ¯¯ 5 for details of how to intercept the Ctrl-C and Critical Error vectors, so that you can restore the normal mode at program termination even if the program is terminated by Ctrl-Break or Ctrl-C being pressed by the user, or due to a critical error. The init_channel() function accepts a channel number, a reload value which will be in the range 0 to 65535 (in modes two and three, a zero divisor gives division by 65536, and a divisor of one should not be used), and an operating mode number. The access mode is not provided as a parameter - the function always programs the channel for lobyte/hibyte access. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #5 Program the operating mode and reload value for a CTC channel Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE5.C and compile with: bcc -I -L -ms sample5.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Needed for toupper() */ #include #include /* Pass go, add printf(), program is 8K already :-) */ #include /* Needed for atoi() */ char *accessmodes[] = { "", "lobyte-only", "hibyte-only", "lobyte/hibyte" }; void init_channel(unsigned int channum, unsigned int accessmode, unsigned int mode, unsigned int reload) { if (channum > 2 || accessmode < 1 || accessmode > 3) return; asm pushf; /* Preserve interrupt flag */ asm cli; outportb(0x43, (channum << 6) + (accessmode << 4) + ((mode & 0x07) << 1)); /* Mode */ if (accessmode & 1) outportb(0x40 + channum, reload & 0xFF); /* Reload reg lobyte */ if (accessmode & 2) outportb(0x40 + channum, (reload >> 8) & 0xFF); /* Reload reg hibyte */ asm popf; /* Restore interrupt flag */ return; } void usage(void) { printf("Usage: SAMPLE5 \n\n"); printf("\tchannel is 0, 1, or 2\n"); printf("\taccessmode may be:\n"); printf("\t\tL = Lobyte only\n"); printf("\t\tH = Hibyte only\n"); printf("\t\tW = Lobyte/hibyte (16-bit)\n"); printf("\toperatingmode may be:\n"); printf("\t\t0 = Interrupt on terminal count\n"); printf("\t\t1 = Hardware-retriggerable one-shot\n"); printf("\t\t2 = Rate generator\n"); printf("\t\t3 = Square wave generator\n"); printf("\t\t4 = Software-triggered strobe\n"); printf("\t\t5 = Hardware-triggered strobe\n"); printf("\treload is an unsigned 16-bit value, use zero for divide-by-65536\n"); return; } void main(unsigned int argc, char * argv[]) { unsigned int channum, accessmode, mode, reload; printf("Sample program #5 - Set the mode and reload value for a CTC channel\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); if (argc < 5) { usage(); exit(1); } channum = argv[1][0] - '0'; switch (toupper(argv[2][0])) { case 'L': accessmode = 1; break; case 'H': accessmode = 2; break; case 'W': accessmode = 3; break; default: usage(); exit(1); } mode = argv[3][0] - '0'; if (channum > 2 || mode > 5) { usage(); exit(1); } reload = atoi(argv[4]); printf("Setting CTC channel %d for %s access, mode %d, with " \ "reload value %ld\n", channum, accessmodes[accessmode], mode, (long)(reload ? reload : 65536L)); init_channel(channum, accessmode, mode, reload); exit(0); } -------------------------------- snip snip snip -------------------------------- ## 7.13 READING THE RELOAD REGISTER It is not possible to read the Reload register contents. In modes two and three, it may be possible to infer the reload register value using clever techniques, but I don't believe there is any good reason to pursue this. ## 7.14 READING THE COUNTING REGISTER Reading the Counting register on-the-fly gives you a fairly accurate time value with a resolution of 0.8381 us for calculating elapsed time or timestamping internal or external events. You do not actually read the Counting register directly, it is read via the Latch register, which follows the Counting register value unless it is latched via the latch command. You can read the Counting register by making one or two (depending on the access mode) reads from the data port of the appropriate channel, however this value is not latched, and is not stable. In lobyte/hibyte access mode, there is a delay between reading the lobyte and hibyte, so the lobyte and hibyte don't correspond to the same instant in time, and you may read an incorrect value. This problem does not occur if the access mode is lobyte-only, or hibyte-only. {JAM} Some CTC hardware implementations do not buffer the counter properly, so if the Counting register is read at the instant it is changing value, you may read the counter part-way through the 'ripple-through', i.e. some low-order bits may have decremented but high-order bits may not have decremented yet. Therefore, even in lobyte-only or hibyte-only mode, the Counting register cannot be read reliably in this way. The CTC provides a latch command to avoid these problems. When the latch command is issued, the Latch register freezes, and the Counting register continues to count. Thus the Latch register contains a stable count which can be read via the data ports in the normal way. Once the appropriate number of bytes (one or two, depending on the access mode) have been read, the Latch register unlatches and resumes following the Counting register. ## 7.15 THE LATCH COMMAND To latch a channel, write a latch command byte to the Mode/Command register. The latch command byte is cc000000 binary, where 'cc' is the channel number. Then you can read the latched count from the data register for that channel. The Latch register remains latched until it has been fully read, or until the counter is reprogrammed with a new mode word. The latched value must be read before any other operation is performed on the channel, except initialising the channel with a new mode word. {JAM} Latching the count in progress should not affect the Counting register but when several machines were tested, they tended to occasionally miss a CTC clock, i.e. fail to decrement, if latch commands were being issued. This was much more pronounced on an Epson 386SX/20 PLUS, which would miss roughly one clock for every two latch commands issued! This seems to be an isolated example of bad hardware design, but is still disturbing. The channel can also be latched via the read-back command (section ¯¯ 7.18). The meaning of the value you read depends on the mode of the channel. The meaning of the count in modes two and three are described in sections ¯¯ 7.15.1 and ¯¯ 7.15.2. ## 7.15.1 MEANING OF COUNT VALUE IN MODE TWO In mode two, the value will be in the range of 1 to the divisor register value. It will start at the divisor register value, and decrement down to 1. When it would decrement to zero, it instead reloads to the divisor register value. For example if the divisor was 5, the count sequence would be 5, 4, 3, 2, 1, 5, 4... If the divisor is 0 (i.e. 65536), the sequence is 0, 65535, 65534, ... 2, 1, 0, 65535... For channel zero, a rising edge on the output pin triggers the timer tick interrupt at the instant that the channel reloads its Counting register from the Reload register. {JAM} On PS/2 machines, if the latch command is issued at the instant when the Counting register changes from 1 to the reload value, occasionally the read will yield a zero, even if the Reload register does not contain zero. In other words, if the Reload register is 20, the count sequence would be 5, 4, 3, 2, 1, 20, 19, 18... At the instant between the 1 and the 20, the timer does actually decrement to zero, and sometimes a zero will be read, even though zero is not in the valid counting sequence. If the divisor is 65536, the above problem mentioned by {JAM} does not occur. In other cases, you could work around the problem by specifically checking for a value of zero and substituting the reload value. In mode two, if you are using a divisor of 65536 (the normal value for channel zero), you can convert the down-counting value into an up-counting value by performing a 16-bit negation, i.e. up_count = 0 - read_count0(); or neg ax (or whichever register contains the count). This will give a 16-bit value which increases from 0 to 65535 then back to 0 again. If the divisor is not 65536, just subtract the count value from the divisor value to get an up-counting value which will increase from 0 to divisor minus one, then back to 0 again. See the above problem noted by {JAM}. ## 7.15.2 MEANING OF COUNT VALUE IN MODE THREE Refer to section ¯¯ 7.8.5 for a description of the operation of mode three. The raw count will always be an even value, because the Counting register decrements in steps of two instead of steps of one. The behaviour with an even divisor is easiest to describe, so I will assume that the divisor value is even. In this case, the count register counts down from the divisor value, in steps of two, until it reaches two, then reloads to the divisor value on the next CTC clock. The output latch toggles state at this moment. For example, if the divisor is 6, the count sequence would be 6, 4, 2, 6, 4, 2, 6, 4, 2... with the output latch toggling at the transition between each '2' and '6'. In this mode, to generate a full timestamp, you need to latch and read the Counting register _and_ the output pin state, so you know whether the channel is on its first or second countdown. The timer tick interrupt only occurs on the rising edge of the output of the T flip-flop, at the end of every second countdown (if we define the first countdown as when the output of the T flip- flop is high, and the second countdown as when its output is low). The read- back function is useful for this (it allows the count register and the output state to be latched and read by software), and is described in section ¯¯ 7.18. Mode two is more suitable than mode three for timestamping or timing functions, because the Counting register behaves sensibly and there is no need to know whether it is on the first or second countdown. On machines that support readback (all AT-class machines except the PS/2, see section ¯¯ 7.24.2), the count can therefore be read on-the-fly in mode three. See section ¯¯ 7.20 for details. See also section ¯¯ 7.15 for {JAM}'s comments on loss of CTC clocks when the channel is latched or read-back. ## 7.16 SAMPLE CODE: READING THE COUNT IN MODE TWO This function latches, reads, and returns the current Counting register contents of CTC channel zero. Remember that the Counting register counts downwards. This function assumes that CTC channel zero is operating in mode two with a divisor of 65536. See section ¯¯ 7.10 and ¯¯ 7.12 for sample code to set the mode and divisor. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Function to latch and read the Counting register of CTC channel zero, assuming that the channel is set to operate in mode two with a divisor of 65536. Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) */ unsigned int read_channel0_mode2(void) { unsigned int cv; asm pushf; /* Preserve interrupt flag */ asm cli; outportb(0x43, 0); /* Latch the count register */ cv = inportb(0x40); /* Lobyte of count */ cv += inportb(0x40) << 8; /* Hibyte of count */ asm popf; /* Restore interrupt flag */ return cv; /* Return down-counter */ } -------------------------------- snip snip snip -------------------------------- ## 7.17 THE LOBYTE/HIBYTE FLAG Each timer channel has an internal flag which keeps track of whether the lobyte or the hibyte of the count should be provided when the data port is read. Each time the data port is read, this flag toggles state (unless the channel was programmed for hibyte-only or lobyte-only access, i.e. bits 5 and 4 were 0,1 or 1,0 when it was initialised). After programming a timer channel, the flag is clear, and reading the data port will yield the lobyte, then the hibyte, then the lobyte, then the hibyte, etc. But if some other badly-behaved software reads the data port only once (or any odd number of times), the flag would be set, and you would read the hibyte first, then the lobyte, so you would be out of sync with the counter. There is no processor-accessible flag to tell you whether you are reading the lobyte or the hibyte. Issuing a latch command doesn't affect the lobyte/hibyte flag, either, unfortunately. This is why it's essential to disable interrupts while accessing the CTC, and always read or write BOTH bytes (unless the channel is programmed for lobyte- only or hibyte-only access). My experience has been that if you initialise the counter in your program (initialising it clears the lobyte/hibyte flag), it will stay synchronised, and there is no need to worry about the flag at all. If anyone has found otherwise, please tell me about it. (*) See section ¯¯ 7.27 for a program which attempts to determine the lobyte/hibyte flag state (among other things). ## 7.18 THE READ-BACK COMMAND The read-back command word is written to the mode/command register. Bits 7 and 6 of the command word (normally the counter select bits) are both '1'. Read-back is not supported on the 8253 (PCs and XTs); it was added with the 8254 (AT and later). However, {JAM} says all AT documentation states that this bit combination is reserved and, alas, the PS/2 LSI integration of the CTC does not implement the read-back command - on a PS/2 the read-back command is ignored. {JAM} has tested IBM ValuePoints and they are alright. It is just the PS/2 that does not support read-back (see section ¯¯ 7.24.2). A read-back command is specified by writing a value to the mode/command register as follows: 7 6 5 4 3 2 1 0 1 1 . . . . . . (Specify read-back command) . . * . . . . . Latch count flag: 0 = Yes, 1 = No . . . * . . . . Latch status flag: 0 = Yes, 1 = No . . . . * . . . Read-back timer channel 2: 1 = Yes, 0 = No . . . . . * . . Read-back timer channel 1: 1 = Yes, 0 = No . . . . . . * . Read-back timer channel 0: 1 = Yes, 0 = No . . . . . . . 0 (Reserved for future expansion) Command word bits 3, 2, and 1 enable read-back for timer channels 2, 1, and 0 respectively, thus any combination of the three channels can be selected for read-back with one command word. Bits 5 and 4 enable the two types of read-back. Important - Setting these bits to _zero_ enables the function. Bit 5 specifies latching the count value. This is the same as issuing a counter latch command (cc000000 binary), but several counters can be latched at the same time, depending on which counters are enabled by bits 3, 2, and 1 of the read-back command word. Bit 4 specifies latching the channel status. If this function is enabled (by setting the bit to 0), the next read of the data register for that channel will yield a status read-back byte, which is defined as follows: 7 6 5 4 3 2 1 0 * . . . . . . . Output pin state . * . . . . . . Null Count flag . . * * . . . . Access mode as specified at initialisation . . . . * * * . Operating mode as specified at initialisation . . . . . . . * BCD flag as specified at initialisation The bottom six bits return the values programmed into the channel when it was last initialised by a write of a mode word. Bits 7 and 6 relate to real-time events. Bit 7 indicates the actual state of the output pin of the timer chip at the moment that the read-back command was issued, and bit 6 indicates whether a newly-programmed divisor value has been loaded into the Counting register yet (if clear) or the channel is still waiting for a trigger signal or for the Counting register to count down to zero before a newly programmed Reload value is loaded into the Counting register (if set). The bit is set upon a mode or Reload value write to the channel, and cleared when the Reload value is loaded into the Counting register. ## 7.19 SAMPLE CODE: READ-BACK This function performs a full read-back on a specified CTC channel and fills in a readback_data structure with the count and the read-back status byte. -------------------------------- snip snip snip -------------------------------- /* Function to read-back a counter/timer channel count and status Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) */ #pragma inline; /* Required for asm pushf, popf, and cli */ typedef struct { unsigned int count; unsigned char status; } readback_data; void readback_channel(unsigned int channum, readback_data * rbdp) { if (channum < 3) { asm pushf; /* Preserve interrupt flag */ asm cli; /* Disable interrupts */ outportb(0x43, 0xC0 + (2 << channum)); /* Latch count, status */ rbdp->status = inportb(0x40 + channum); /* Get status */ rbdp->count = inportb(0x40 + channum); /* Get count lobyte */ rbdp->count += inportb(0x40 + channum) << 8; /* Get count hibyte */ asm popf; /* Restore interrupt flag */ return; } } -------------------------------- snip snip snip -------------------------------- ## 7.20 READING THE COUNT IN MODE THREE (8254 ONLY) Reading the count on-the-fly to get an absolute timestamp in mode three is more awkward than reading the count in mode two, and has a higher overhead. As far as I know, there is no reason why your program should not program CTC channel 0 to operate in mode 2 and leave mode 2 in effect when your program exits or is terminated (modern BIOSes set mode 2 as the default mode anyway, see section ¯¯ 7.4.2), so there should be no requirement to be able to read the count in mode 3. However, if the CTC is an 8254 (not an 8253 or a PS/2 CTC) it is possible to read the count in mode 3, so I will describe how this is done. The function presented in the section ¯¯ 7.21 is the result of some testing and experimentation. I have found it to be reliable on all of the machines I was able to test with, but if you have trouble with it, let me know. (*) The basis of reading the count in mode three is to read the count value, and also read the output pin state, then combine them. The count register counts down in sequence 0, 65534, 65532, 65530 ... 8, 6, 2, 0, 65534, 65532... with the output pin state toggling on each transition from 2 to 0. The rising edge of the output pin will initiate a timer tick interrupt, therefore I regard this as starting the count sequence, so when the output pin is low, the counter is on its second pass. See sections ¯¯ 7.8.5 and ¯¯ 7.15.2 for more details. We could use a read-back command to read the count and the output pin status, and derive an up-count combined value as: up_count = ((0 - actual_count) / 2) + (output_state ? 0 : 0x8000); This will work, and is reliable on some machines, but on other machines, the output pin state is occasionally read incorrectly, probably due to delays in the logic of the timer chip. So, I had to modify the routine to read the output pin state, read the count, read the output pin state again, then determine the true count in progress. The logic here is as follows: If the second output state is the same as the first (this is nearly always the case), then the output state and the count are both valid. If the output states are different, then a counter reload has occurred during the reading process, so use the count value to determine whether the count was latched just before, or just after, the output changed state. If the count value (after converting to an up-count) is small, then it was read just after the output changed state, so use the second output state. If the count is large, then it was read just before the output changed state, so the first output state is applicable. Now that I have the correct output state, the equivalent up-count value can be calculated using the above formula. This yields a 16-bit up-counting value which corresponds to the negative of the equivalent raw count in mode two. Needless to say, the part of the routine that talks directly to the timer chip operates with interrupts locked out. ## 7.21 SAMPLE CODE: READING THE COUNT IN MODE THREE This function latches, reads, and returns the current effective count value for timer channel zero, converted to a 16-bit up-counting value. It works with 8254 CTCs and fully compatible ASICs, but does not work with 8253s or on PS/2 machines. This function assumes that CTC channel zero is operating in mode three with a divisor of 65536. This USED TO BE the default mode set up by the BIOS, but mode 2 is the default used by modern 486 BIOSes that I have seen. See section ¯¯ 7.4.2 for details. The function also assumes that channel zero is set for lobyte/hibyte access (bits b5,4 = 1,1 in control register at initialisation) and that the lobyte/hibyte flag is correctly synchronised (see sections ¯¯ 7.7 and ¯¯ 7.17. See section ¯¯ 7.12 for sample code to set the mode and divisor. -------------------------------- snip snip snip -------------------------------- /* Function to read the count register (down-counter) of timer channel zero, assuming that the timer is in mode three, with a divisor of 65536. Returns the count in up-counter format. Requires an 8254 timer chip. Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) */ unsigned int read_timer0_mode3(void) { unsigned char st1, st2; /* Status read-back values */ unsigned int cv; /* Count value */ disable(); /* No ints please - can use asm cli */ outportb(0x43, 0xE2); /* Latch and read back status byte */ st1 = inportb(0x40); /* Read status byte */ outportb(0x43, 0x00); /* Latch count for timer 0 */ cv = inportb(0x40); /* Lobyte of count */ cv += inportb(0x40) << 8; /* Hibyte of count */ cv = (0 - cv) >> 1; /* Convert to up-count, 0-32767 */ outportb(0x43, 0xE2); /* Latch and read back status byte */ st2 = inportb(0x40); /* Read status byte */ enable(); /* Ints back on - can use asm sti */ if ((st1 ^ st2) & 0x80) /* If output pin changed state... */ if (cv < 0x4000) /* If reload just occurred... */ st1 ^= 0x80; /* Use newer output pin status */ if ((st1 & 0x80) == 0) /* If on second countdown... */ cv |= 0x8000; /* Set b15 */ return cv; /* Return as up-counter */ } -------------------------------- snip snip snip -------------------------------- ## 7.22 SAMPLE CODE: OPTIMISED MODE THREE COUNT READING FUNCTION The following function reads the count register of CTC channel zero assuming that CTC channel zero is operating in mode three with a divisor of 65536 and is set for lobyte/hibyte access, and the lobyte/hibyte flag is correctly synchronised. The value is returned in up-counting format, in the range 0-65535, and is the effective value that would be read from the counter in mode two using a raw read, except that the counting direction is reversed (the value returned by this function is an up-counter, the raw value is a down-counter). -------------------------------- snip snip snip -------------------------------- ; Function to read the count register (down-counter) of CTC channel zero, ; assuming that the channel is in mode three, with a divisor of 65536. ; Returns the count in up-counter format. Requires an 8254 timer chip. ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; _read_timer0_mode3 PROC near ; or FAR for far code model ; unsigned int read_timer0_mode3(void); pushf ; Keep interrupt flag mov al,11100010b ; Latch and read back status byte only cli ; Lock out interrupts out 43h,al ; Send it jmp SHORT $+2 ; Delay in al,40h ; Get status byte mov ah,al ; To AH jmp SHORT $+2 ; Delay mov al,00000000b ; Latch count for timer 0 out 43h,al ; Send it jmp SHORT $+2 ; Delay in al,40h ; Get lobyte of count mov dl,al ; Save in DL jmp SHORT $+2 ; Delay in al,40h ; Get hibyte of count mov dh,al ; Save in DH jmp SHORT $+2 ; Delay mov al,11100010b ; Latch and read back status byte again out 43h,al ; Send it jmp SHORT $+2 ; Delay in al,40h ; Get status byte popf ; Restore interrupt flag neg dx ; Convert to ascending count xor al,ah ; Did the output change? jns GotCount ; If not, no problemo test dh,dh ; Was count high or low? js GotCount ; If count was about to carry, keep old not ah ; If count just carried, change output GotCount: shl ah,1 ; Get output pin status to CF cmc ; Pin high = count 0-32767 rcr dx,1 ; Pin low = count 32768-65535 ret ; Return 16-bit ascending count in DX _read_timer0_mode3 ENDP -------------------------------- snip snip snip -------------------------------- ## 7.23 SAMPLE PROGRAM: MANIPULATE THE CTC AND PORT B The following program is a command driven utility that manipulates the CTC and the Port B hardware. It lets you send commands to the mode/command register, read and write the data registers in single-byte or lobyte/hibyte modes, set and display the Timer 2 Gate and Speaker Gate signals, and read the Timer 2 output on port B or C (see section ¯¯ 7.5). It has a simple help summary which is displayed when '?' is entered at the prompt. The program performs minimal error checking and is not intended to be bulletproof. You may find it useful for testing some subtle details of the CTC's operation. Parameters to a command must be separated from the command name by one or more spaces or tabs. Commands on the same line may be separated by semicolons (;) and the whole command line will be executed with interrupts locked out. Result text is stored in an internal buffer and displayed once the command line has been fully processed. Numeric parameters are assumed to be binary by default. To specify a hex value, prefix the hex digits with 'x' (e.g. 'xFEDC'). To specify a decimal value, prefix the digits with 'd' (e.g. 'd12345'). -------------------------------- snip snip snip -------------------------------- /* Sample program #6 Utility to manipulate the CTC Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE6.C and compile with: bcc -I -L -ms sample6.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* For tolower() */ #include /* For inportb() and outportb() */ #include /* For read() and write() */ #include /* For printf() */ #include /* For exit() */ #include /* For strlen() */ #define FALSE 0 #define TRUE 1 #define STDIN 0 #define LINELEN 120 /* Line length limit */ static unsigned int eval_ok; static char resulttext[10240]; /* Buffer for result text */ static char * resulttextp; unsigned int eval_value(char * s) { unsigned int p, v; char c; p = v = 0; eval_ok = TRUE; if (s[0] == 'd') { /* Decimal value */ ++p; while ((c = s[p++]) > ' ') { v *= 10; if ((c >= '0') && (c <= '9')) v += (c - '0'); else return (eval_ok = FALSE); } return v; } if (s[0] == 'x') { /* Hex value */ ++p; while ((c = s[p++]) > ' ') { v <<= 4; if ((c >= '0') && (c <= '9')) { v += (c - '0'); continue; } if ((c >= 'a') && (c <= 'f')) { v += (c - 'a' + 10); continue; } return (eval_ok = FALSE); } return v; } while ((c = s[p++]) > ' ') { /* Binary value - default */ v <<= 1; if ((c == '0') || (c == '1')) v += (c - '0'); else return (eval_ok = FALSE); } return v; } void rw_reg(unsigned int is16, unsigned int chan, char * parms) { unsigned int ioadr, v; ioadr = 0x40 + chan; if (parms[0]) { v = eval_value(parms); if (eval_ok == FALSE) { sprintf(resulttextp, "Bad parameter value: '%s'\n", parms); resulttextp = resulttext + strlen(resulttext); return; } outportb(ioadr, v & 0xFF); if (is16) outportb(ioadr, v >> 8); return; } v = inportb(ioadr); if (is16) { v += (inportb(ioadr) << 8); sprintf(resulttextp, "Channel %d read lobyte/hibyte: 0x%04X\n", chan, v); } else sprintf(resulttextp, "Channel %d read byte: 0x%02X\n", chan, v); resulttextp = resulttext + strlen(resulttext); return; } void do_command(char * cmd, char * parms) { unsigned int v; switch (cmd[0]) { case '?' : sprintf(resulttextp, "Command format: cmd [parms] [; cmd [parms]] [...]\n\n" "Commands on the same line are executed with interrupts locked out\n" "Values may be hex ('x' prefix), decimal ('d' prefix) or binary (default)\n" "\nCommands are:\n\n" "0 [value] - read [write] channel 0 data register\n" "1 [value] - read [write] channel 1 data register\n" "2 [value] - read [write] channel 2 data register\n" "00 [value] - read [write] channel 0 data register as lobyte/hibyte\n" "11 [value] - read [write] channel 1 data register as lobyte/hibyte\n" "22 [value] - read [write] channel 2 data register as lobyte/hibyte\n" "C value - write value to mode/command register\n" "R - read back timer 2 output via port B or C\n" "G [on|off] - read [set] timer 2 gate on port B\n" "S [on|off] - read [set] speaker gate on port B\n" "Q - quit\n" "\nExample command: g on; c 10110110; 22 x1234; s on\n" ); resulttextp = resulttext + strlen(resulttext); break; case '0' : case '1' : case '2' : if (cmd[1] == cmd[0]) rw_reg(TRUE, cmd[0] - '0', parms); else rw_reg(FALSE, cmd[0] - '0', parms); break; case 'c' : if (!parms[0]) { sprintf(resulttextp, "Must give parameter for 'c' command\n"); resulttextp = resulttext + strlen(resulttext); return; } v = eval_value(parms); if (eval_ok == FALSE) { sprintf(resulttextp, "Bad parameter value: '%s'\n", parms); resulttextp = resulttext + strlen(resulttext); return; } outportb(0x43, v & 0xFF); break; case 'r' : sprintf(resulttextp, "Timer 2 readback on port B (AT) is %s; on port C (PC/XT) is %s\n", (inportb(0x61) & 0x20) ? "high" : "low", (inportb(0x62) & 0x20) ? "high" : "low"); resulttextp = resulttext + strlen(resulttext); break; case 'g' : if (parms[0]) outportb(0x61, (inportb(0x61) & 0xFE) | (parms[1] == 'n')); else { sprintf(resulttextp, "Timer 2 gate is currently %s\n", (inportb(0x61) & 0x01) ? "on" : "off"); resulttextp = resulttext + strlen(resulttext); } break; case 's' : if (parms[0]) outportb(0x61, (inportb(0x61) & 0xFD) | ((parms[1] == 'n') << 1)); else { sprintf(resulttextp, "Speaker gate is currently %s\n", (inportb(0x61) & 0x02) ? "on" : "off"); resulttextp = resulttext + strlen(resulttext); } break; case 'q' : asm sti; exit(0); default : if (parms[0]) sprintf(resulttextp, "Bad command: '%s %s'\n", cmd, parms); else sprintf(resulttextp, "Bad command: '%s'\n", cmd); resulttextp = resulttext + strlen(resulttext); } return; } void do_commandline(char * s) { static char cmdbuf[LINELEN]; static char parmbuf[LINELEN]; unsigned int sp, dp1, dp2, endflags; char c; resulttextp = resulttext; asm cli; sp = 0; do { dp1 = 0; dp2 = 0; while ((s[sp] <= ' ') && (s[sp] != '\0')) ++sp; /* Skip leading whitespace */ while ((s[sp] > ' ') && (s[sp] != ';')) { c = s[sp++]; cmdbuf[dp1++] = tolower(c); } cmdbuf[dp1] = '\0'; if ((s[sp] != '\0') && (s[sp] != ';')) { while ((s[sp] <= ' ') && (s[sp] != '\0')) ++sp; /* Skip whitespace */ while ((s[sp] != '\0') && (s[sp] != ';')) { c = s[sp++]; parmbuf[dp2++] = tolower(c); } } while (dp2) { if (parmbuf[dp2 - 1] <= ' ') --dp2; else break; } parmbuf[dp2] = '\0'; if (dp1) do_command(cmdbuf, parmbuf); if (s[sp] == ';') ++sp; } while (s[sp]); asm pushf; asm pop endflags; asm sti; if (resulttextp != resulttext) write(1, resulttext, strlen(resulttext)); if (endflags & 0x200) printf("\nWarning! Interrupts were inadvertently enabled during the command!\n"); } void main(void) { static char inpbuf[LINELEN]; unsigned int p; printf("Sample program #6 - Manipulates the CTC directly\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); printf("Type '?' for help, 'Q' to quit\n"); while (1) { printf("\n>"); if ((p = read(STDIN, inpbuf, LINELEN - 2)) > 0) --p; inpbuf[p] = '\0'; do_commandline(inpbuf); } } -------------------------------- snip snip snip -------------------------------- ## 7.24 HARDWARE PROBLEMS AND DIFFERENCES ## 7.24.1 DIFFERENCES BETWEEN THE INTEL 8253 AND 8254 Though the 8254 was a "completely new design" from the 8253, the differences to the user or programmer are that the 8254 has the read-back command (see section ¯¯ 7.18), and the 8254 fixes a problem on the 8253 when used in mode 3 with a reload value of 3 (which does not concern us). ## 7.24.2 CHIPSET IMPLEMENTATIONS Differences in timer implementations in chipset ASICs are likely to be vague and unpredictable. Prof. John Mertus {JAM} (see section ¯¯ 1.7) has done some research on this, and found some machine-specific hardware differences. These are described in the applicable sections here, indicated with the marker {JAM}. One thing John discovered is that the PS/2 ASIC does not implement the read-back function (see section ¯¯ 7.18). Personally, I am p*ssed off at IBM for making such a cretinous and inconsiderate mistake. Because of them, we cannot just look at the machine type byte in the ROM and be sure that, if the machine is an AT-class machine, read-back will work - we must specifically test whether the machine supports read-back, and our programs may have to behave differently depending on the result of the test. Normally, clones are criticised for not being fully IBM compatible - this time, it is IBM! Rant mode off, dismount :-) BTW, {JAM} also reports that the 8254 CTC is implemented properly on the IBM ValuePoints. Any information on other machines would be welcomed. (*) ## 7.24.3 INTEL 8253/8254/82C54 CLOCK SYNCHRONISATION PROBLEMS This information is from Intel Q&A and application notes, and was sent to me by Louis Warshaw (louis@gate.net). Thanks Louis! Unfortunately I found the Intel documentation very vague, so I will quote the relevant parts and hope that Intel don't sue me :-) The problems concern synchronisation between the CTC clock input (the 1.193182 MHz clock) and the write access pulses when the data registers are written or when a counter latch command is issued. -WR ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÀÄÄÄÄÄÄÄÙ ^a CTC Clk ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ^b The timing diagram shows a write access to a data register (I/O address 40h, 41h, or 42h) and a rising edge on the CTC clock. The chip's specification for the time between point 'a' and point 'b' is called Twc, and is specified as 55 nanoseconds maximum for the Intel 8254. Here is what the Intel documentation says. My comments are in square brackets. "Question: Why is Twc specified to the rising edge of [CTC] Clock, but yet Clocks are loaded [sic] on the falling edge? "Answer: This is used for software synchronisation of loading a new count [reload value]. The new value must be in the Twc window to guarantee that the new count [reload value] is loaded on the next falling edge [of CTC clock]." I think this is just saying that the reload register must be fully loaded before the rising edge of CTC Clock, in order to be decremented on the following falling edge of CTC Clock. I assume that if the reload register is not loaded at least Twc nanoseconds before the rising edge, the chip will just wait for the next rising edge, thus there is an uncertainty of one CTC Clock width as to exactly _which_ CTC Clock will start decrementing the counting register, and this depends on the reload register becoming fully loaded at least shortly before the rising edge before the falling edge that will decrement it. "Question: Why should Gate be pulsed immediately following a write of a new count [reload] value, when using an asynchronous clock source [CTC Clock not synchronous with the Write pulse] in modes two and three? "Answer: If an asynchronous clock input is used for a counter [channel], you need to use Gate to synchronise the loading of the new count [reload value]." As for the second point, Intel's question and answer are so vague that I can not come to any conclusion about the implications for the programmer. "Question: What does the comment on page 3-74, figure 17, Note, Peripheral Components, 1993 mean? "NOTE: A Gate transition should not occur one [CTC] clock prior to terminal count". "Answer: Modes 2 and 3 use the [CTC] clock frequency for the Rate Generator and Square Wave Mode respectively. In modes 2 and 3, the 8254 (and 82C54) uses "look ahead" logic to precondition OUT to go low on the falling edge of the CLK input upon terminal count. Without this look ahead feature, the 8254 would not have time to resolve its internal logic at the same time OUT is to go low upon reaching terminal count. Monitoring the count value in software, before disabling counting via the Gate, is usually sufficient to prevent this combination of events. This has always been the operation of the 8254 (and 8253, and 82C54) and no problems resulting from this [sic]." Again nice and vague. I think this is saying that terminal count is anticipated by the look-ahead logic one CTC clock before it actually occurs, i.e. in mode 2 when the Counting Register reaches two, and if Gate goes low while the Counting Register is two, the output may actually go low as normal on the next CTC clock even though the Gate input is low. I wonder how this relates to mode 3. Two more problems are described. These apply only to the 82C54, the CMOS version of the 8254. I do not know whether any PCs actually use the 82C54. There are two 'failure modes' documented - the Twc count write failure mode and the Tcl counter latch command failure mode. "The Twc [counter write] failure mode occurs in a very narrow window between the Twc min and Twc max timing when writing the last [or only] byte of a count [Reload register] value. The Twc specification defines the relationship between the writing of a count [Reload] value and the Clk [CTC clock] pulse and whether the Clk pulse will or will not be reflected in the subsequent counting operation. The Clk pulse is a low to high transition on one of the 82C54's Clk input pins. [The 82C54 documentation states Twc min = 0, Twc max = 55 ns]. -WR ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÀÄÄÄÄÄÄÄÙ ³<ÄTwcÄ>³ CTC Clk ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ "If the rising edge of a Clk pulse happens before the Twc min specification then it is too early and will not be reflected in the count. If the Clk pulse happens after the Twc max specification then the Clk pulse will be reflected in the count. If the Clk happens between Twc min and Twc max it may or may not be reflected in the count value. Twc min is 0 ns and Twc max is 55 ns or a 55 ns window [sic]. "There is a worst case 8-20 ns [floating] window between Twc min and Twc max where the 82C54 counter control logic is corrupted and the counter enters an undefined state. The counter must be re-initialised by rewriting the counter Mode word. The problem is worse at cold temperatures (0 degrees C) and low VCC (4.5V). Only the counter being written to is affected. The other counters continue to count properly. "The Twc failure mode actually varies across the normal skew of the fabrication process. The 82C54's typical wafer fabrication process failure mode window is between 300 picoseconds to 1 nanosecond. The actual window may typically be less but this represents the +/- 100 picosecond resolution of the Teradyne test computer used to characterise the Twc failure mode. When the process shifts within the normal skew to the slow implant corner the failure mode window increased [sic] to a worst case of 8-20 nanoseconds. "The failure mode is a function of an asynchronous Clk and -WR input signals. When -WR and Clk are asynchronous the -WR may occur at any time in relation to the Clk. If -WR and Clk are synchronous -WR will always occur in the same relation ship [sic :-)] to Clk. The 82C54 Clk and -WR inputs are synchronous when the Clk input is the system microprocessor clock, or a derivative of it. If the 82C54 Clk source is independent of the system clock then the -WR and Clk are asynchronous unless hardware synchronised external [sic] to the 82C54. "There are three modifications which compensate for the failure mode: "1. Use a Clk input signal which is a derivative of the system microprocessor clock source. This makes the interaction of the -WR and Clk totally predictable. The -WR and Clk will not happen coincidentally and the synchronisation prohibits occurrence of a -WR within the failure mode window time of Clk. "2. Through the use of the 82C54 Read Back Command the software detects the state of the Counter Status byte Null Count flag which indicates whether the count has been moved from the Count Register [Reload register] to the Counting Element (CE) [Counting register] or "loaded". See Figure 1 Internal Block Diagram of a Counter ( Figure 5, 82C54 Data Sheet). Unless the Null Count flag is cleared the count has not been successfully loaded. If the Null Count flag is not cleared then the software rewrites the Mode word and count value [Reload value]. "3. Externally synchronise the -WR and Clk input signals. This is done by gating -WR with Clk. The -WR and Clk inputs then appear synchronous to the 82C54 which prohibits the occurrence of a -WR within the failure mode window time of Clk." As far as I can tell from discussion with Louis Warshaw, the problem affects writing a reload value on-the-fly to a CTC channel. The -WR signal on the timing diagram represents the pulse issued by the processor to write the last or only byte of the new reload value to the channel. The problem occurs if the rising edge of the Clk to that channel occurs within a certain time of the trailing (rising) edge of the write. There is a timing window which is between 8 and 20 ns wide, and may dynamically shift within the 0 to 55 ns specification Twc window, relative to the rising edge of -WR. If a rising edge of Clk appears within this 8 to 20 ns wide window, the internal logic of the counter will be corrupted and the counter will go into never-never land until reinitialised by a Mode write to the Mode/Command register. "Counter Latch Command failure mode, Tcl "The failure mode occurs during a very narrow window between -WR and Clk when latching a count [Counting register] value. The approximately 10 nanosecond window between -WR rising edge and -Clk falling edge, when asynchronously writing a Counter Latch or Read Back command, the count value read may be in error. The byte value read is not in sequence in relation to the previous or following byte read. The Counting Element [Counting register] and counter control logic are unaffected by the failure mode and continues [sic] to decrement properly. -WR ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÀÄÄÄÄÄÄÄÙ ³<ÄÄÄTclÄÄÄ>³ CTC Clk ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ "The error window has been verified on a Teradyne test computer to be a 200-300 picoseconds [sic] window between -WR rising edge and Clk falling edge when writing a Counter Latch or Read Back command. "The failure mode is not a violation of the Tcl specification. The Tcl specification tells the user a Clk pulse falling edge which happens close to the -WR rising edge of a Counter Latch or Read Back command will (Tcl min) or will not (Tcl max) be reflected in the count value subsequently read from the Counter Output Latch [Latch register]. The Tcl specification provides for a +/- one Clk pulse, or one bit error, in the count value latched. The failure mode results in a multiple bit error in the count value read [from the Latch register]. "There are three modifications which compensate for the failure mode: "1. Use a Clk signal which is a derivative of the system microprocessor clock source. This makes the interaction of the -WR and Clk [sic] totally predictable. The -WR and Clk never happen coincidentally and the synchronisation prohibits occurrence of a WRX [sic] within the failure mode window time of Clk. "2. Latch and read the count twice if an error greater than one bit error occurs. "3. Externally synchronise the -WR and Clk input signals. This is done by gating -WR with Clk. -WR and Clk then appear synchronous to the 82C54 which prohibits the occurrence of a -WR within the failure mode window time of Clk. This is saying that if the -WR access on a counter latch or read-back command falls within a narrow window a certain length of time after the falling edge of the Clk to that channel, an incorrect count value is latched in the Latch register. Presumably this occurs because the value provided by the Counting register becomes briefly invalid a short time after the Counting register decrements, and if the latch command happens to occur during that short time, the invalid value will be latched into the Latch register. Thus, with an 82C54 where the Clk is not synchronous with -WR, you cannot trust the value latched by a Counter Latch or read-back command. ## 7.25 IS THE CTC AN 8253 OR AN 8254? Well, you can check the BIOS Machine Type Byte at location F000:FFFE. Values 0xFD, 0xFE, and 0xFF indicate PCjr, XT/Portable, and original PC respectively, all of which have 8253 CTCs. A Type Byte value of 0xFC indicates an AT or later machine, which should have an 8254. In other words, -------------------------------- snip snip snip -------------------------------- unsigned int is_machine_an_AT(void) { return (*(unsigned char far *)MK_FP(0xF000, 0xFFFE) == 0xFC); } -------------------------------- snip snip snip -------------------------------- However, this method is not foolproof, because some clones may not have a valid Machine Type byte, and because of IBM's brilliance in not implementing a proper 8254 in the PS/2's ASIC. With this test, some clones, and PS/2 machines, would report an 8254 when they may only have an 8253. Also, when the CTC is emulated, as it is for DOS applications running under OS/2, and presumably under Linux, the full functionality of the CTC may not be available. The sample program in section ¯¯ 7.26 contains some code that determines whether the CTC is an 8253 or an 8254 or something else (i.e. a faulty chip or a partially emulated chip) which can be extracted and used to determine the CTC type, but note that it leaves CTC channel two in a non-standard mode (mode 0). This should not be a problem, as channel two is used for audio generation and is fully initialised by any code that will subsequently use that channel. ## 7.26 DETERMINING THE EXACT STATE OF THE CTC You might need to determine the state of a particular channel in the timer chip. The only things you can really find out are the state of the lobyte/hibyte flag (this cannot be read directly, but its state can be inferred by reading the count several times, assuming the channel is being clocked), and the programmed operating mode and BCD/binary flag, which can be determined by the read-back command (assuming that the timer chip is an 8254). The logic of the function infer_lobyte_hibyte_flag() in the program in section ¯¯ 7.27 is: read the count, then repeatedly re-read the count until a different value is obtained. Then infer the state from whether the lobyte or the hibyte of the value has changed. If the lobyte changed, then the flag is in sync (normal). If the hibyte changed, then the flag is out of sync. In the latter case, the flag can be brought into sync by reading the data register once. For each counter read operation, the count is latched prior to being read. The routine disables interrupts, to ensure that the number of CTC clocks between reads is minimal. ## 7.27 SAMPLE PROGRAM: REPORT CHANNEL STATES -------------------------------- snip snip snip -------------------------------- /* Sample program #7 Reports CTC type (8253, 8254, or faulty/emulated) and the operating states (lobyte/hibyte flag, mode, binary/BCD, output state) of all channels. Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE7.C and compile with: bcc -I -L -ms sample7.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include #include #include #include #include #define TESTVALUE 0x55AA /* Value to use as reload test value */ #define BACKWARDS ((unsigned int)((TESTVALUE >> 8) + ((TESTVALUE & 0xFF) << 8))) /* Backwards TESTVALUE */ #define EXP_CSTAT 0x30 /* Expected counter status */ #define CTC_EMUL 0 /* CTC is faulty or emulated by OS */ #define CTC_8253 1 /* CTC is an 8253 */ #define CTC_8254 2 /* CTC is an 8254 */ #define LHF_INSYNC 0 /* Lobyte/hibyte flag is in sync */ #define LHF_OUTSYNC 1 /* Lobyte/hibyte flag is out of sync */ #define LHF_UNKNOWN 2 /* Lobyte/hibyte flag cannot be determined */ typedef struct { unsigned int count; unsigned char status; } readback_data; /* Code */ unsigned int read_channel_raw(unsigned int channum) { unsigned int cv; if (channum < 3) { asm pushf; asm cli; outportb(0x43, channum << 6); cv = inportb(0x40 + channum); cv += inportb(0x40 + channum) << 8; asm popf; } return cv; } /* Simple short delay - just wait for at least one CTC clock to occur */ void wait_ctc_clock(void) { unsigned int ch0count; ch0count = read_channel_raw(0); while (read_channel_raw(0) == ch0count) ; return; } /* The following function is described in section ¯¯ 7.18 */ void readback_channel(unsigned int channum, readback_data * rbdp) { if (channum < 3) { asm pushf; /* Preserve interrupt flag */ asm cli; /* Disable interrupts */ outportb(0x43, 0xC0 + (2 << channum)); /* Latch count, status */ rbdp->status = inportb(0x40 + channum); /* Get status */ rbdp->count = inportb(0x40 + channum); /* Get count lobyte */ rbdp->count += inportb(0x40 + channum) << 8; /* Get count hibyte */ asm popf; /* Restore interrupt flag */ return; } } /* This function determines the CTC type. It stores the current contents of Port B (speaker and timer 2 gate control port), then turns off speaker enable and sets timer 2 gate low. It then attempts to read-back the status of CTC channel 2, and stores this in ch2rbd.status; this will be used to restore channel 2 to its original operating state if it turns out that the CTC is an 8254. The function then programs CTC channel 2 for mode zero with a reload value specified by TESTVALUE. In this mode, channel 2 will reload on the next CTC clock, and will not decrement, as its gate input is low. We then wait for at least one CTC clock to occur (detect this by reading CTC channel 0 and waiting for a change in the latched value). CTC channel 2 then contains a known value, and is in a stable state. We know the expected latched count value and the expected status value to be returned on a read-back. It is then possible to determine the CTC type by reading a few things and looking at the CTC's responses. Specifically, the routine checks that it can latch and read a stable value equal to the reload register value - if this fails, the CTC is assumed to be faulty or emulated. Then it issues a read-back command and keeps the read-back status byte, then latches and reads the count. If the CTC is an 8253, this will yield a 'backwards' count - i.e. TESTVALUE with hibyte and lobyte interchanged, because the lobyte/hibyte flag was reversed by the read-back (which reads the data register three times). On an 8254, this latch and read will yield TESTVALUE. Next, it performs another readback (to reinstate the original lobyte/hibyte flag if the CTC is an 8253) and keeps the read-back status again, then it latches and reads the count again. This should _always_ yield TESTVALUE, for either an 8253 or 8254. It then checks for the expected behaviour of an 8253 and an 8254 separately. If the CTC does not give the correct response, it will be reported as faulty or emulated. Note that this function spends most of its time with interrupts locked out. */ unsigned int detect_ctc_type(void) { unsigned int ctctype; /* Value to be returned */ unsigned int port61; /* Port 61h value */ readback_data ch2rbd, rbd1, rbd2; /* Read-back storage */ unsigned int backwards, forwards; /* Latched count values */ ctctype = CTC_EMUL; /* Assume faulty or unknown CTC type */ asm pushf; asm cli; port61 = inportb(0x61); /* Get Port B value */ outportb(0x61, port61 & 0xFC); /* Turn off timer 2 gate and speaker */ /* Try read-back on channel two, only useful if CTC turns out to be an 8254 */ readback_channel(2, &ch2rbd); readback_channel(2, &ch2rbd); /* Attempt to read-back channel two */ outportb(0x43, 0xB0); /* Channel 2, two bytes, mode 0, binary */ outportb(0x42, TESTVALUE & 0xFF); /* Lobyte of reload value */ outportb(0x42, TESTVALUE >> 8); /* Hibyte of reload value */ wait_ctc_clock(); wait_ctc_clock(); /* Wait for a couple of CTC clock pulses */ /* Just read the raw value a couple of times, make sure it's stable */ if ((read_channel_raw(2) != TESTVALUE) || (read_channel_raw(2) != TESTVALUE)) goto got_type; /* Structured programming? Never heard of it */ /* Try a read-back - on an 8253, this will reverse the lobyte/hibyte flag */ readback_channel(2, &rbd1); /* Read the count - on an 8253 this will be TESTVALUE backwards, on an 8254 it will be TESTVALUE */ backwards = read_channel_raw(2); /* Try another read-back, into rbd2 this time */ readback_channel(2, &rbd2); /* Now latch and read the count again */ forwards = read_channel_raw(2); /* Now, try to figure out what it is! */ if ((rbd1.status != EXP_CSTAT) && (rbd2.status != EXP_CSTAT) && (backwards == BACKWARDS) && (forwards == TESTVALUE)) ctctype = CTC_8253; if ((rbd1.status == EXP_CSTAT) && (rbd2.status == EXP_CSTAT) && (backwards == TESTVALUE) && (forwards == TESTVALUE)) ctctype = CTC_8254; got_type: /* Now we know what it is. If it's an 8254, we can restore channel 2 to its previous mode, although we cannot restore the original divisor, because we can't tell what it was. If it's not an 8254, we can't fix anything */ if (ctctype == CTC_8254) { outportb(0x43, 0x80 + (ch2rbd.status & 0x3F)); outportb(0x42, 0); outportb(0x42, 0); } outportb(0x61, port61); /* Restore speaker and timer 2 control bits */ asm popf; return ctctype; } unsigned int test_delta(unsigned int latchv, unsigned int cport) { unsigned int nreads, startcount, count, diff, hbdiff, lbdiff; asm pushf; asm cli; outportb(0x43, latchv); /* Read count to startcnt */ startcount = inportb(cport); startcount += inportb(cport) << 8; for (nreads = 0; nreads < 20; ++nreads) { outportb(0x43, latchv); /* Latch count again */ count = inportb(cport); count += inportb(cport) << 8; diff = startcount ^ count; /* Get difference */ hbdiff = ((diff & 0xFF00) != 0); lbdiff = ((diff & 0x00FF) != 0); if (lbdiff == hbdiff) /* Both or neither changed */ continue; /* Wait for difference */ if (lbdiff) { /* Lobyte changed */ asm popf; return LHF_INSYNC; /* Flag is in sync */ } if (hbdiff) { /* Hibyte changed */ asm popf; return LHF_OUTSYNC; /* Flag is out of sync */ } } /* for nreads */ asm popf; return LHF_UNKNOWN; /* Couldn't determine */ } /* The following function infer_lobyte_hibyte_flag() attempts to determine the state of the lobyte/hibyte flag for a specified CTC channel, assuming that that channel has been programmed for lobyte/hibyte access (i.e. bits 5 and 4 of the control register were 1,1 at initialisation). It should work for both the 8253 and 8254. The lobyte/hibyte flag toggles every time the latched (or unlatched) count is read from the counter data register. The function returns LHF_INSYNC, LHF_OUTSYNC, or LHF_UNKNOWN. This function seems to be reliable on fast machines but does not seem to work well on slow machines or XTs (I don't know why), so don't rely on its accuracy! */ unsigned int infer_lobyte_hibyte_flag(int channum) { unsigned int latchv, cport, result; unsigned int progress[3]; if (channum > 2) return LHF_UNKNOWN; latchv = channum << 6; cport = 0x40 + channum; progress[LHF_INSYNC] = 0; progress[LHF_OUTSYNC] = 0; progress[LHF_UNKNOWN] = 0; do result = test_delta(latchv, cport); while (++progress[result] < 10); return result; } void main(void) { unsigned char machtype; /* Machine type byte */ readback_data rbd[3]; /* Readback data structures */ unsigned int lhf[3]; /* Lobyte/hibyte flag values */ unsigned int ch; /* Channel number */ unsigned int port61; /* Port 61h value */ static char machname[4][31] = { "an AT class machine (8254 CTC)", /* 0xFC */ "a PCjr (8253 CTC)", /* 0xFD */ "a PC/XT (8253 CTC)", "an IBM-PC (8253 CTC)" }; printf("Sample program #7 - Reports CTC type, modes, and output states\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); machtype = *(unsigned char far *)MK_FP(0xF000, 0xFFFE); if (machtype < 0xFC) printf("The BIOS Machine Type byte has a non-standard value\n\n"); else printf("The BIOS Machine Type byte says this machine is %s\n\n", machname[machtype - 0xFC]); switch (detect_ctc_type()) { case CTC_EMUL: printf("CTC appears to be faulty, non-standard, or emulated by operating system\n\n"); printf("Cannot determine operating parameters\n"); break; case CTC_8253: printf("CTC is an 8253\n\nCannot determine operating modes; attempting to determine\n"); printf("lobyte/hibyte flag state assuming lobyte/hibyte access and mode 2 or 3\n\n"); for (ch = 0; ch < 3; ++ch) { switch (infer_lobyte_hibyte_flag(ch)) { case LHF_INSYNC: printf("Channel %d lobyte/hibyte flag sync:\tCorrect\n", ch); break; case LHF_OUTSYNC: printf("Channel %d lobyte/hibyte flag sync:\tReversed\n", ch); break; default: printf("Channel %d lobyte/hibyte flag sync:\tCannot be determined\n", ch); } } /* for ch */ break; case CTC_8254: printf("CTC is an 8254; all information is available\n\n"); port61 = inportb(0x61); outportb(0x61, (port61 & 0xFC) | 0x01); /* Enable timer 2 gate */ for (ch = 0; ch < 3; ++ch) { readback_channel(ch, &rbd[ch]); lhf[ch] = infer_lobyte_hibyte_flag(ch); } printf("Parameter\t\tChannel 0\tChannel 1\tChannel 2\n\n"); printf("Access sequence:"); for (ch = 0; ch < 3; ++ch) { switch (rbd[ch].status & 0x30) { case 0x00: printf("\tUninitialised"); rbd[ch].count = 0; break; case 0x10: printf("\tLobyte only"); rbd[ch].count &= 0xFF; break; case 0x20: printf("\tHibyte only"); rbd[ch].count &= 0xFF00; break; case 0x30: printf("\tLobyte/hibyte"); } /* switch */ } /* for ch */ printf("\n"); printf("Operating mode:\t\t%d\t\t%d\t\t%d\n", (rbd[0].status >> 1) & 0x07, (rbd[1].status >> 1) & 0x07, (rbd[2].status >> 1) & 0x07); printf("BCD/binary mode:"); for (ch = 0; ch < 3; ++ch) printf(rbd[ch].status & 1 ? "\tBCD\t" : "\tBinary\t"); printf("\n"); printf("Output pin state:"); for (ch = 0; ch < 3; ++ch) printf(rbd[ch].status & 0x80 ? "\tHigh\t" : "\tLow\t"); printf("\n"); printf("Null Count flag:"); for (ch = 0; ch < 3; ++ch) printf(rbd[ch].status & 0x40 ? "\tSet\t" : "\tClear\t"); printf("\n"); printf("Current raw count:\t0x%04X\t\t0x%04X\t\t0x%04X\n", rbd[0].count, rbd[1].count, rbd[2].count); printf("Lobyte/hibyte flag:"); for (ch = 0; ch < 3; ++ch) { if ((rbd[ch].status & 0x30) == 0x30) { switch (lhf[ch]) { case LHF_INSYNC: printf("\tCorrect\t"); break; case LHF_OUTSYNC: printf("\tReversed"); break; default: printf("\tUnknown\t"); } } else printf("\tN/A\t"); } /* for */ printf("\n"); asm cli; outportb(0x61, (inportb(0x61) & 0xFC) | (port61 & 0x03)); /* Restore timer 2 gate and speaker control bits */ asm sti; break; } /* switch ctctype */ exit(0); } -------------------------------- snip snip snip -------------------------------- ## 7.28 CTC ACCESS UNDER OS/2 Native OS/2 applications do not need to access the CTC directly. This section is concerned with DOS applications that can be run under OS/2 in a VDM (Virtual DOS Machine). The HW_TIMER option for the DOS session determines whether the DOS application is given access to the real CTC, or whether it uses the virtual CTC driver, VTIMER.SYS. Manipulating the CTC with the HW_TIMER option set ON may cause interference with other DOS tasks, though I think it does not affect OS/2 because OS/2 uses the Real Time Clock for its timekeeping. I believe that OS/2 does not use CTC channel zero itself; it is only required for DOS tasks. OS/2's VTIMER.SYS (virtual CTC emulator, used if HW_TIMER is OFF) is rather interesting. I have paraphrased some information from the OS/2 red book (OS/2 Version 2.0 Volume 2: DOS and Windows Environment), if anyone has more detailed or newer information I'd like to see it. (*) ## 7.28.1 OS/2 VTIMER.SYS: CTC CHANNEL ZERO VTIMER.SYS is able to generate virtual (emulated) interrupts at 54.9254 ms intervals, or 13.7314 ms intervals (four times faster). If the DOS session reprograms the divisor of channel zero, it gets its ticks at 13.7314 ms intervals, regardless of the actual divisor value it programmed (presumably unless it programmed a divisor of 65536). VTIMER.SYS runs the real CTC channel zero at 54.9254 ms, or 13.7314 ms if one or more DOS sessions are running at this rate. The 13.7314 ms interrupt capability is required for GW-BASIC which uses a four times faster interrupt for its PLAY command (music). Latching channel zero causes a "random value derived from the system time" to be loaded into the emulated Latch register, which can then be read. This design decision was based on the fact that the count register is often used to provide a random number seed, and this approach supposedly gives the DOS application a "sense of elapsed time". The documentation does not say whether read-back is supported, but I suspect it is not. In other words, it is not possible to use channel zero for timing in a DOS session under OS/2, unless HW_TIMER is set to ON. Stick to the tick count variable and/or timer interrupts, at the normal rate, if you want your program to run properly under OS/2. ## 7.28.2 OS/2 VTIMER.SYS: CTC CHANNEL ONE Apparently this channel only supports read accesses, which presumably return a random number or a number derived from the system time. All other accesses are ignored. At a guess, I would say that each read of the data register will yield a random or time-derived value. ## 7.28.3 OS/2 VTIMER.SYS: CTC CHANNEL TWO This is interesting. Channel two is linked up with the emulated Port B (see section ¯¯ 7.5) and OS/2 "serialises" speaker access from different tasks. When we generate a speaker tone, we program the divisor into channel two (which will determine the tone frequency), and then enable the speaker by means of the bottom two bits in Port B. VTIMER.SYS remembers the divisor value, and when the bits are set in Port B, it calls the OS/2 "kernel beep", which may block if it is already beeping on behalf of a different process. After completion of any beep in progress, the kernel beep function programs the correct value into the real channel two, and programs the real Port B to start the beep. When the DOS session turns off the bits in Port B, the beep is stopped and the kernel beep becomes available for use by other sessions. The "serialisation" can be pre-empted by an "interrupt time beep service" which is somehow used if the beep is issued by the keyboard scancode interrupt handler, to support the "keyboard buffer full" beep issued by the BIOS in a DOS session. Interesting! ## 7.29 GENERATING AUDIO TONES ON THE SPEAKER Although this is unrelated to timing... The PC speaker interface circuitry is thoroughly documented in section ¯¯ 7.5. CTC channel two is used for generating audio. It is normally operated in mode three, to produce a square wave signal. It can be used in different ways, though - see section ¯¯ 10.7.1 for the PWM audio generation technique. The speaker interface is controlled by two bits in the read/write register at I/O address 61 hex: 7 6 5 4 3 2 1 0 * * * * * * . . Not applicable to speaker control - do not modify! . . . . . . * . Speaker data . . . . . . . * Timer 2 Gate The Timer 2 Gate signal is directly connected to the 'gate' input of timer channel two. This signal must be high in order for the counter to decrement. When the gate signal goes low, the timer output goes high immediately, and counting ceases. The count register is reloaded from the divisor register on the next 1.193182 MHz clock pulse, and when the gate input goes high again, counting resumes starting at the divisor value, thus synchronising the counter. The Speaker data output is logical-ANDed with the output from timer two, to drive the speaker. Thus, to generate a tone using timer 2, Speaker data should be set to '1' and Timer 2 gate should also be set to '1'. The frequency of the tone will be 1193181.6666... divided by the divisor value programmed into CTC channel two. To generate audio by bit manipulation, Timer 2 gate should be set to zero. This disables timer two and forces its output high. The speaker can then be directly controlled via Speaker data. Setting this bit high allows current to flow in the speaker coil, causing the cone to move outwards (or inwards, depending on which way the speaker is wired - it doesn't matter really). Setting the bit low causes the cone to return to its normal position. Toggling the bit at rate of n toggles per second gives a frequency of n/2 Hz. ## 7.30 SAMPLE PROGRAM: GENERATING A TONE USING CTC CHANNEL TWO The following program generates a tone at approximately 1KHz for approximately one second, using CTC channel two. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #8 Demonstrates generating a tone using timer channel two Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE8.C and compile with: bcc -I -L -ms sample8.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL) #define DIVISOR(frequency) ((unsigned int) ((1193181.6666 / frequency) + 0.5)) #define FREQUENCY 1000 /* Tone frequency in Hz */ unsigned long read_bios_tick_count(void) { unsigned long ct; asm pushf; asm cli; ct = * BIOS_TICK_COUNT_P; asm popf; return ct; } int has_tick_occurred(void) { static unsigned long old_tick_count = 0xFFFFFFFFL; if (read_bios_tick_count() != old_tick_count) { old_tick_count = read_bios_tick_count(); return TRUE; } return FALSE; } void init_channel(unsigned int channum, unsigned int accessmode, unsigned int mode, unsigned int reload) { if (channum > 2 || accessmode < 1 || accessmode > 3) return; asm pushf; asm cli; outportb(0x43, (channum << 6) + (accessmode << 4) + ((mode & 0x07) << 1)); /* Mode */ outportb(0x40 + channum, reload & 0xFF); /* Reload reg lobyte */ outportb(0x40 + channum, (reload >> 8) & 0xFF); /* Reload reg hibyte */ asm popf; return; } void turn_tone_on(unsigned int divisor) { init_channel(2, 3, 3, divisor); /* Channel 2, 16-bit, mode 3 */ asm pushf; /* Preserve interrupt flag */ asm cli; outportb(0x61, inportb(0x61) | 0x03); /* Enable timer and speaker */ asm popf; /* Restore interrupt flag */ return; } void turn_tone_off(void) { asm pushf; /* Preserve interrupt flag */ asm cli; outportb(0x61, inportb(0x61) & 0xFC); /* Disable speaker */ asm popf; /* Restore interrupt flag */ return; } void main(void) { unsigned int n = 0; printf("Sample program #8 - Demonstrates generating a tone using CTC channel two\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); printf("Tone frequency is %d Hz\n", FREQUENCY); has_tick_occurred(); /* Init has_tick_occurred() */ while (has_tick_occurred() == FALSE) ; /* Wait for a tick to occur */ turn_tone_on(DIVISOR(FREQUENCY)); while (n < 18) /* Stop after one second */ if (has_tick_occurred()) ++n; turn_tone_off(); exit(0); } -------------------------------- snip snip snip -------------------------------- ## 7.31 TIMING SHORT PERIODS USING CTC CHANNEL TWO {JAM} The ideas and code example in this section are largely from Prof. John Mertus's document. Reading a CTC channel requires three I/O accesses, and I/O accesses are notoriously slow by comparison to memory accesses, particularly on fast machines. Referring to section ¯¯ 7.5, CTC channel two's output is readable on bit 5 of Port C at I/O address 62h (PC and XT) or bit 5 of Port B at I/O address 61h (AT and later), and this port can be read in a single I/O access. This can be useful when a short (microsecond-level) delay must be timed especially accurately. With this approach, CTC channel two is used in mode zero (see section ¯¯ 7.8.2), the 'interrupt on terminal count' mode. In this mode, we can write a count value to the CTC, then watch the Timer 2 Output signal and wait for it to go high, signalling that the time period has expired. When using this technique, you must ensure that the Timer 2 Gate output on bit 0 of Port B at I/O address 61h is high (CTC channel two will not count if this signal is low) and that the Speaker Data signal on bit 1 of the same register is low, to avoid sending horrible noises to the speaker! The following code fragment shows how to produce a short (five microseconds plus overhead) pulse on the strobe output of a parallel port, using this approach. It will not work on the old PC and XT - see the comment by the WaitCTC: label. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. You could also time longer periods, up to 54.9 ms, but in this case you would remove the PUSHF/CLI and STI around the delay code and set and clear the bit in the parallel port register in a different way - to clear it, use PUSHF/CLI, read the port, AND the value, write it back, and POPF, and same to set the bit (but use OR instead of AND, of course). This leaves the body of the delay loop operating with interrupts enabled, which is desirable to avoid problems with interrupt latency, etc, but could cause problems for example if the keyboard buffer filled up and a beep was issued, because this would result in Port B and CTC channel 2 being reprogrammed part-way through the delay loop. A pop-up TSR could also issue a beep, causing the same problem. This would require intercepting int 10h (BIOS video output, generates a beep) and int 9 (keystroke interrupt) or int 15h keystroke intercept, and even then, this would not prevent some interrupt-triggered code from reprogramming CTC channel 2. In other words, I can't see any safe way to implement longer delays with interrupts enabled using this technique (except in a controlled environment). -------------------------------- snip snip snip -------------------------------- NCTCClocks EQU 6 ; Six CTC clocks (5us) for the delay LPTPortBase DW 3BCh ; Set to your LPT port base address ; Somewhere in the initialisation code, set up Timer 2 Gate and Speaker Data: pushf cli ; No interrupts in al,61h ; Get Port B and al,11111101b ; Turn off Speaker Data or al,00000001b ; Turn on Timer 2 Gate out 61h,al ; Write it back popf ; Restore interrupt flag ; ... ; Then produce the short pulse: mov dx,LPTPortBase ; Get parallel port base I/O address inc dx inc dx ; Point to control register mov al,090h ; Timer 2, lobyte only, mode 0, binary pushf cli ; No interrupts out 43h,al ; Send command byte - prepare the CTC in al,dx ; Get parallel port value and al,11111110b ; Clear bit 0 (set pin 1, -STROBE, high) mov ah,al ; To AH for later inc ax ; Set bit 0 (set pin 1, -STROBE, low) out dx,al ; Set the I/O register ; At this point the -STROBE pin goes low mov al,NCTCClocks ; Number of CTC clocks to wait out 42h,al ; Start the timer in al,61h ;!! Use 62h for PC and XT!! WaitCTC: in al,61h ;!! Use 62h for PC and XT!! test al,20h ; Test bit 5 - has the time expired? jz WaitCTC ; If not, loop mov al,ah ; Get value with bit 0 off out dx,al ; Write it ; At this point the -STROBE pin returns high popf ; Restore interrupt flag -------------------------------- snip snip snip -------------------------------- Note that CTC channel two is being used in lobyte-only mode for maximum access speed; if you need to delay more than 255 CTC clocks, use the timer in the lobyte-hibyte mode and write a two-byte reload register value. You would want to avoid using CTC channel 2 for audio generation, including the standard BIOS beep, if you were using this technique inside an interrupt handler, because any beep in progress will be cut off when this code executes. {JAM} says: "On reasonably fast machines, timer 2 can be used to create delays from 5 to 54,000 microseconds with 1 to 2 microsecond accuracy". ## 7.32 TIMING SHORT PERIODS USING MODE THREE For timing short periods, where an absolute timestamp is not required, a simplified technique can be used, using CTC channel zero in mode three. Traditionally the BIOS programmed CTC channel zero to operate in mode three with a Reload value of zero. Modern BIOSes seem to prefer to use mode two. See section ¯¯ 7.4.2 for details. Referring to sections ¯¯ 7.8.5, ¯¯ 7.15.2 and ¯¯ 7.20, in mode three, the raw value read from the count register decrements in steps of two, each step corresponding to one 0.8381 us CTC clock period. Therefore, periods of time comfortably less than about 27 ms can be measured by reading the counter, storing the value read, then repeatedly reading the counter, calculating the difference, and waiting for this difference to exceed the desired number, which will be twice the number of 0.8381 us periods (because the timer decrements in steps of two). This technique is demonstrated in the sample program in section ¯¯ 7.34. ## 7.33 VERTICAL RETRACE A video monitor display is created by the electron beam in the monitor (colour monitors have three) which scans the screen in a 'raster' fashion similar to the way you read a book (though a lot quicker :-) The beam starts at the top left corner and draws one line from left to right, then returns to the left and scans the line below, and so on until the entire screen has been scanned. Each of these horizontal scans is called a line, and at least 15000 lines are scanned each second (depending on the screen resolution and timing parameters). Each time the electron beam reaches the right side of the screen, it returns very quickly to the left side of the screen, ready to scan the next line. This short 'return' time is called the horizontal retrace. The horizontal retrace interval is very short (a few microseconds) and is significant on some old CGA cards because a 'snow' effect was produced unless the video buffer was only accessed during the horizontal retrace interval. After the full screen has been scanned, the beam turns off and returns to the top of the screen. This is the vertical retrace interval, which occupies a length of time in the order of one or two milliseconds (depends on video mode timing parameters), and occurs about 50 to 70 times per second (equal to the field rate, or vertical scan rate, sometimes called the 'refresh' rate). LCD displays are not physically scanned in the same way, but they usually get their display information from a signal which is raster oriented. In any case, vertical retrace is emulated on LCD machines, for compatibility. Vertical retrace is indicated by a status bit in the video status register at I/O location 3BA hex (MDA, Hercules, and EGA and VGA in monochrome modes) or 3DA hex (CGA, MCGA, and EGA and VGA in colour modes). For CGA, MCGA, EGA, and VGA cards, bit 3 indicates vertical retrace, and is set during the retrace interval (i.e. clear during the display period) except for the MCGA card in 640x480 monochrome mode, when the bit has the opposite polarity (although the status register appears at 3DA, the colour address!). The MDA card does not have a vertical retrace indication, though the Hercules card does indicate vertical sync on bit 7 of the register at 3BA, with opposite polarity, i.e. the bit is clear during retrace). Some video cards are also able to generate IRQ2/9 on vertical retrace but standard VGA cards do not have this facility, so I will not describe it here. This interrupt can be simulated fairly successfully using CTC channel 0. This technique is described in section ¯¯ 10.16. The word at low memory address 0040:0063 (or 0000:0463) contains the I/O address of the CRTC which can be used to determine whether the video system is colour or monochrome. A value of 3B4 hex indicates monochrome. In this case vertical retrace detection is unreliable, as the MDA does not have any vertical retrace indication. A value of 3D4 hex indicates colour, in which case vertical retrace is indicated by bit 3 in the register at I/O address 3DA hex. ## 7.34 SAMPLE PROGRAM: TIMING SHORT PERIODS USING MODE THREE The following program uses CTC channel 0 in mode three to measure short durations to provide a striped background colour on a VGA adapter. It uses the VGA vertical retrace signal to synchronise the time periods with the start of each screen update. The effect of turning the computer's turbo switch on and off is minimal, and is not cumulative; this demonstrates that the program is correctly using the CTC to measure the time delay. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #9 Demonstrates timing short periods using mode three Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE9.C and compile with: bcc -I -L -ms sample9.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Needed for bioskey() */ #include /* Needed for MK_FP() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define DELAY 1790 /* Delay 1790 x 0.8381 us = 1.5 ms */ #define DAC_ADDR 0x3C8 /* VGA DAC address register */ #define DAC_DATA 0x3C9 /* VGA DAC data register (write) */ #define BIOSSHIFT (*((unsigned char far *)MK_FP(0x40, 0x17))) unsigned int read_timer0_mode3_raw(void) { asm pushf; asm xor al,al; asm cli; asm out 43h,al; asm in al,40h; asm mov ah,al asm in al,40h asm popf; asm xchg al,ah; return _AX; /* Return raw value */ } void main(void) { unsigned int video_status; /* I/O address of video status reg */ unsigned int colour[3]; /* Background colour - R, G, and B */ unsigned int rgbsel; /* Selects which colour to change */ unsigned int ctcval, newctc; /* Raw mode three values */ printf("Sample program #9 - Demonstrates timing short periods using mode three\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); printf("Press the Ctrl key to exit\n"); video_status = *((unsigned int far *)MK_FP(0x40, 0x63)) + 6; /* First, make sure it's in mode three! */ asm cli; outportb(0x43, 0x36); outportb(0x40, 0); outportb(0x40, 0); asm sti; /* Wait for vertical retrace to start */ while ((inportb(video_status) & 0x08) == 0) ; /* Start of retrace - reset colours */ newscr: colour[0] = colour[1] = colour[2] = 0; rgbsel = 0; asm cli; outportb(DAC_ADDR, 0); outportb(DAC_DATA, 0); outportb(DAC_DATA, 0); outportb(DAC_DATA, 0); asm sti; /* Check for CTRL pressed and terminate if so */ if (BIOSSHIFT & 0x04) { while (bioskey(1)) bioskey(0); /* Flush buffer */ exit(0); } /* Wait for start of display */ while ((inportb(video_status) & 0x08) != 0) ; /* Get the time now */ ctcval = read_timer0_mode3_raw(); /* Loop waiting for nominated time period to elapse, check for end of display */ while (1) { do { if ((inportb(video_status) & 0x08) != 0) goto newscr; /* If retrace has started */ newctc = read_timer0_mode3_raw(); /* Sample the time */ newctc = ctcval - newctc; /* Get CTC clocks elapsed x2 */ } while (newctc < (DELAY * 2)); /* Loop until desired time */ /* Time has elapsed - bump time reference and change the background colour */ ctcval -= (DELAY * 2); /* Use * 2 because of mode 3 */ colour[rgbsel] += 22; /* Increase R/G/B component */ if (++rgbsel > 2) /* Change R/G/B selector */ rgbsel = 0; asm cli; outportb(DAC_ADDR, 0); outportb(DAC_DATA, colour[0]); outportb(DAC_DATA, colour[1]); outportb(DAC_DATA, colour[2]); asm sti; } /* for */ } /* main() */ -------------------------------- snip snip snip -------------------------------- ## 7.35 THE REAL TIME CLOCK (RTC) The RTC/RAM chip is a Motorola MC146818A chip or workalike. It is not present in the original PC and XT and may not be present in non-hardware-compatible machines. It is often implemented as part of an ASIC, or in a hybrid module such as the DS1287, which contains the RTC/RAM chip, crystal, and backup battery. It is a CMOS device, containing a crystal oscillator and divider with interrupt and alarm logic, a non-volatile CMOS RAM array which stores the BIOS parameter settings, and a processor interface based on the CMOS RAM register file (which contains 64 or sometimes 128 registers). The crystal oscillator normally operates at 32768 Hz, using a small watch type crystal. The RTC has an interrupt output, which is wired to IRQ8 (normally mapped to int 70h). The RTC is accessed at I/O addresses 70 hex and 71 hex. Both ports are 8-bit and should only be accessed using 8-bit I/O instructions. The port at 70h is the address select port, which selects which of the 64 or 128 internal registers will be addressed by an access to I/O address 71h. The original MC146818 chip is bus-addressable, and this address/data system may be implemented in logic on the motherboard, not on the RTC/RAM chip itself. After the register has been specified by writing a register number to port 70h, the selected register's contents can then be read or written via the port at I/O address 71 hex. This address register and data register technique reduces the amount of I/O space required by the RTC, and is not actually part of the MC146818, but is implemented on the motherboard or in the ASIC. The same technique is used in the CRT Controller chip and other chips on video cards. Always disable interrupts around the access sequence, otherwise an interrupt routine could select a different RTC register, causing your code to read or write the wrong register. Also, note that the address select register at I/O address 70h is write-only. Reading the register will yield an undefined value. ## 7.35.1 READING AND WRITING RTC REGISTERS Here are functions to read and write RTC registers. Inline assembler is required for pushf and popf. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. The cli could be replaced by the disable() pseudofunction. Change inportb() and outportb() to inp() and outp() for Microsoft C, I think. -------------------------------- snip snip snip -------------------------------- unsigned char read_rtc_register(unsigned char reg_num) { unsigned char rv; asm pushf; asm cli; outportb(0x70, reg_num); asm jmp SHORT $+2 asm jmp SHORT $+2 asm jmp SHORT $+2 rv = inportb(0x71); asm popf; return rv; } void write_rtc_register(unsigned char reg_num, unsigned char value) { asm pushf; asm cli; outportb(0x70, reg_num); asm jmp SHORT $+2 asm jmp SHORT $+2 asm jmp SHORT $+2 outportb(0x71, value); asm popf; return; } -------------------------------- snip snip snip -------------------------------- ## 7.35.2 ALLOCATION OF THE RTC REGISTERS The first 10 registers (registers 0 to 9) are the date and time registers (including the alarm settings). These registers cannot be accessed during the update period, which is approximately two milliseconds long and occurs every second (details are given later). Registers 10 to 13 are control registers. The remaining registers (14-63 on a standard MC146818 which has 64 registers, or 14-127 on an enhanced version) are general purpose CMOS RAM locations, which are used by the BIOS to store setup information, and do not relate to timing. The time and date values are configurable for either packed BCD or binary data format, but the BIOS uses the packed BCD format, and some workalike chips do not support binary format, so for practical purposes, packed BCD format is mandatory. See the glossary for a description of packed BCD. Important! The date and time registers (registers 0 to 9) will yield correct values only if no update is in progress. See notes on Register A for details. These registers should not be written unless the 'Set' bit in Register B is set. See notes on Register B for details. The registers are as follows: Reg Function Format Range --- -------- ------ ----- 0 Seconds Two digit packed BCD 0 to 59 1 Seconds alarm Two digit packed BCD 0 to 59 2 Minutes Two digit packed BCD 0 to 59 3 Minutes alarm Two digit packed BCD 0 to 59 4 Hours See below 5 Hours alarm See below 6 Day of week BCD 1 to 7 (see below) 7 Date of month Two digit packed BCD 1 to 31 8 Month Two digit packed BCD 1 to 12 9 Year Two digit packed BCD 0 to 99 10 Register A See below 11 Register B See below 12 Register C See below 13 Register D See below The hours and hours alarm registers (registers 4 and 5) are formatted in 12-hour or 24-hour mode, depending on the setting of bit 1 of Register B (see the description for this bit). In 12-hour mode, bits 6-0 of the hours registers are the hours value, in the range 1 to 12, and bit 7 is the PM indicator (set indicates PM). In 24-hour mode, bits 7-0 of the hours registers are in 24-hour format (range 0 to 23). The seconds alarm, minutes alarm, and hours alarm registers may be set to a value from 0C0 hex to 0FF hex to indicate 'don't care'. For example if the seconds alarm value is zero, the minutes alarm value is 30 (stored in packed BCD form, of course), and the hours alarm value is 0FF hex, the alarm will be signalled at half past every hour. Note that this 'don't care' function may not be implemented in all ASIC workalikes. The day of week register (register 6) simply counts 1, 2, 3, 4, 5, 6, 7, 1, 2... where 1 means Sunday, 2 means Monday, etc. The RTC does not calculate the day of the week from the date. This register must be set by software. It is not used by the BIOS RTC functions or by DOS and will not necessarily be set correctly. Software normally calculates the day of week from the other date information rather than using this register. The RTC uses this register to switch between standard time and daylight saving time if daylight saving is enabled, but the daylight saving function is not used in PCs so there is no need to make sure that this register is set correctly. ## 7.35.3 RTC REGISTER A Register A is register number 10. It is read/write except bit 7, which is read-only: 7 6 5 4 3 2 1 0 * . . . . . . . Update In Progress (UIP) flag . * * * . . . . Prescaler control bits . . . . * * * * Periodic Interrupt rate control The UIP flag, if set, indicates that an update is in progress or is imminent. An update occurs once every second and takes approximately two milliseconds. During the update period, the values read from the date and time registers (though not the alarm registers) are changing and are not valid, because the RTC chip operates quite slowly internally (being low power CMOS) and it takes a while for an update to 'ripple through' from the seconds register all the way up to the year register. If the UIP flag is set, the date and time registers (registers 0-9) should not be accessed. Software must wait until the UIP flag becomes clear before reading any time or date related registers. The UIP flag becomes active approximately 244 us prior to the start of the update cycle, therefore the read or write operation must take less than 244 us to ensure that it completes before the update cycle begins. The Prescaler control bits determine what crystal frequency the RTC expects, and allow the prescaler and divider to be held reset. The values are: bit 6 5 4 0 0 0 Operation with 4.194304 MHz crystal 0 0 1 Operation with 1.048576 MHz crystal 0 1 0 Operation with 32768 Hz crystal (default) 0 1 1 Undefined 1 0 x Undefined 1 1 x Hold prescaler and divider reset (stops counting) (x means don't-care) While the prescaler and divider are held reset, counting and updating ceases. The first update will occur half a second after this condition is removed. The Periodic Interrupt rate control bits determine the periodic interrupt rate (d'oh :-) Here are the values: bit 3 2 1 0 Period Ints per second 0 0 0 0 No periodic interrupt 0 0 0 1 3.90625 ms 256 (see note below) 0 0 1 0 7.8125 ms 128 (see note below) 0 0 1 1 122.0703125 us 8192 0 1 0 0 244.140625 us 4096 0 1 0 1 488.28125 us 2048 0 1 1 0 976.5625 us 1024 (BIOS default) 0 1 1 1 1.1953125 ms 512 1 0 0 0 3.90625 ms 256 1 0 0 1 7.8125 ms 128 1 0 1 0 15.625 ms 64 1 0 1 1 31.25 ms 32 1 1 0 0 62.5 ms 16 1 1 0 1 125 ms 8 1 1 1 0 250 ms 4 1 1 1 1 500 ms 2 Note: Combinations 0001 and 0010 duplicate 1000 and 1001 respectively. If the RTC is operating from a 1MHz or 4MHz crystal (prescaler control bits are 00x), combinations 0001 and 0010 give interrupt rates of 30.517578125 us (32768 interrupts per second) and 61.03515625 us (16384 interrupts per second) respectively. 1MHz and 4MHz crystals are not used with RTCs in PCs because of the increased power consumption. ## 7.35.4 RTC REGISTER B Register B is register number 11. It is fully read/write: 7 6 5 4 3 2 1 0 * . . . . . . . Set flag (1 = set mode) . * . . . . . . PIE, Periodic Interrupt Enable (1 = enable) . . * . . . . . AIE, Alarm Interrupt Enable (1 = enable) . . . * . . . . UIE, Update Interrupt Enable (1 = enable) . . . . * . . . SQWE, Square wave enable, not used in PCs . . . . . * . . DM, BCD/binary mode (1 = binary) . . . . . . * . 12/24-hour mode (0 = 12-hour, 1 = 24-hour) . . . . . . . * DSE, Daylight Saving Enable (1 = enable) The Set flag must be set by software before any real-time registers (current date and time) are modified. When the bit is set, any real-time register update in progress is aborted, and while the bit is set, updates are prevented and the UIP bit in Register A remains clear. After setting the real-time registers, the SET bit must be cleared to resume normal operation. The PIE, AIE, and UIE enable the periodic, alarm, and update interrupts respectively, if set. The periodic interrupt occurs regularly as defined by bits 0-3 of Register A. The update interrupt, if enabled, occurs every second, immediately following an update. The alarm interrupt occurs whenever the hours, minutes and seconds registers match the time programmed into the alarm registers. See the note after the register list for alarm register details. The SQWE bit enables the square wave output at the frequency set by bits 0-3 of Register A. This pin is not used in PC applications. If the 12/24-hour mode is changed, the hours register should be reprogrammed. Daylight Saving mode, if enabled, causes the time to jump forward from 01:59:59 to 03:00:00 on the morning of the last Sunday in April, and backward from 01:59:59 to 01:00:00 on the last Sunday in October. The day of week register must be set correctly for this to work properly. The PC does not use this function. ## 7.35.5 RTC REGISTER C Register C is register number 12. It is read-only; writes are ignored. It contains three interrupt source flags and the combined interrupt flag. 7 6 5 4 3 2 1 0 * . . . . . . . IRQF (combined interrupt flag) . * . . . . . . PF (periodic flag) . . * . . . . . AF (alarm flag) . . . * . . . . UF (update flag) . . . . * * * * Unused; zero, read-only The three interrupt source flags are set if the condition that would generate the interrupt has occurred, regardless of whether the interrupt source is enabled (via Register B). These can be used to permit software polling of these conditions, if generating an actual interrupt is not justified. Any active interrupt source flags are cleared immediately after reading this register; thus if several interrupt sources are active, the software must be careful to check for each possible interrupt flag after every read of this register, otherwise a signal may be missed. The IRQF flag (combined interrupt flag) is set if the interrupt output from the RTC chip is active. This will be true if any of the interrupt source flags in this register are set in conjunction with that interrupt source being enabled via Register B. ## 7.35.6 RTC REGISTER D Register D is register number 13. Only bit 7 is meaningful, and this bit is read-only. 7 6 5 4 3 2 1 0 * . . . . . . . VRT, Valid Ram and Time flag . * * * * * * * Unused; zero, read-only The VRT flag indicates whether a power-up has occurred. It is cleared during loss of supply voltage, and is set immediately after a read of Register D. ## 7.35.7 READING THE RTC When reading the RTC's real-time registers it is necessary to avoid reading them during the update period, during which time they cannot be accessed by the processor (reading registers will yield undefined values, and writes will be ignored). Registers A, B, C, and D can be accessed at any time. Your software can use the UIP flag bit in Register A to determine whether an update is in progress or imminent. If this flag is clear, your software then has up to 244 us in which to perform the desired register access(es), and may then re-check the UIP flag and make more accesses if appropriate. If the UIP flag is set, the software may have to wait up to approximately 2.25 ms before the UIP flag is clear. If such a long delay in a read-RTC function is undesirable, a possible solution in some cases could be to store the time each time the RTC is read, and if the RTC is not available due to an update cycle being in progress, return the most recently read RTC value instead. Alternatively, the Update Interrupt and/or the Update Flag in Register C can be used to schedule reads of the RTC so they occur immediately after an update, either under interrupt (if the RTC interrupt is not required for any other purpose), or by polling the Update Flag and reading the real-time registers as soon as the flag reads as '1' (assuming no long background processes are active, this gives the code almost a whole second to make its RTC accesses before the next update cycle will begin. ## 7.35.8 SAMPLE PROGRAM: A TSR CLOCK USING INT 8 AND THE RTC This program is a TSR which hooks interrupt 8 and uses the RTC to display a persistent HH:MM:SS format time in the top right corner of the screen. -------------------------------- snip snip snip -------------------------------- NAME SAMPLE10 ; Sample program #10 ; Demonstrates a TSR clock using int 8 and reading the RTC directly ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; ; This program assembles into SAMPLE10.COM, a TSR program which displays the ; current time in the top right corner of the screen in text modes. It uses ; int 8 to get execution 18.2065 times per second, reads the RTC time directly ; from the RTC chip, and updates the screen every second. This program does ; not attempt to ascertain that an RTC is present. Also, it has no uninstall ; facility. ; ; If a non-standard video mode (i.e. mode 14 hex or higher) is in use, this ; program will assume that it is a text mode. This will probably result in ; disturbance to the display in high resolution graphics modes. This program ; is intended to be instructional only. ; ; Save this file to SAMPLE10.ASM and assemble with: ; masm SAMPLE10; ; link SAMPLE10; ; exe2bin SAMPLE10.exe SAMPLE10.com ; or ; tasm SAMPLE10; ; tlink /t SAMPLE10; ; ComFile SEGMENT ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing ORG 100h ; COM-type file Main PROC near jmp Main2 Main ENDP Hours DB 0FFh ; Hours (BCD) of last update Minutes DB 0FFh ; Minutes (BCD) of last update Seconds DB 0FFh ; Seconds (BCD) of last update ASSUME ds:nothing ; The following function handles int 8, the timer tick interrupt. It reads ; Register A and checks for an update in progress, and skips if so. It ; then reads the seconds register and checks to see whether he seconds have ; changed. If they have, it reads the hours and minutes registers also, and ; then displays the current time in HH:MM:SS format in the top right hand ; corner of the screen if the screen is currently in text mode. ; ; This procedure calls no DOS or BIOS functions. ; ; Note that the whole routine, and also the DisplayTime subroutine which is ; called by this routine, and its subroutines, run with interrupts disabled. NewInt08 PROC far ; Int 8 intercepter pushf ; Preserve flags push ax ; Keep register cli ; Just in case mov al,0Ah ; Register number for register A out 70h,al ; Set it jmp SHORT $+2 ; Delay in al,71h ; Read register and al,80h ; Test update-in-progress flag jnz Chain08 ; If busy, do it next time jmp SHORT $+2 ; Delay out 70h,al ; Select register zero - seconds jmp SHORT $+2 ; Delay in al,71h ; Read seconds cmp al,Seconds ; Did seconds change? je Chain08 ; If not, do nothing on this interrupt mov Seconds,al ; Store new seconds mov al,2 ; Minutes register jmp SHORT $+2 ; Delay out 70h,al ; Set it jmp SHORT $+2 ; Delay in al,71h ; Get minutes mov Minutes,al ; Store mov al,4 ; Hours register jmp SHORT $+2 ; Delay out 70h,al ; Set it jmp SHORT $+2 ; Delay in al,71h ; Get hours mov Hours,al ; Store call DisplayTime ; Display the time Chain08: pop ax ; Restore popf ; Restore DB 0EAh ; JMP xxxx:yyyy Old08Ofs DW 0 ; Vector to original handler - Offset Old08Seg DW 0 ; Segment NewInt08 ENDP ; This procedure uses the time values stored in Hours, Minutes, and Seconds to ; create a time value in the form HH:MM:SS and stores this to the top right ; corner of the screen. It checks the current video mode to avoid overwriting ; video memory incorrectly when a graphics mode is active, and also supports ; non-standard screen resolutions. It does not support Hercules graphics mode, ; because there is no standard video mode number for this mode as it is not ; officially recognised by IBM. DisplayTime PROC near ; Display the current time push ds ; Need DS xor ax,ax ; Zero mov ds,ax ; Address BIOS vars using DS mov al,ds:[449h] ; Get video mode cmp al,4 ; Check for modes 0-3 (text) jb TextMode ; If so cmp al,7 ; Check for MDA text mode je TextMode ; If so cmp al,14h ; Check for last standard graphics mode jb GraphMode ; If graphics, otherwise assume text TextMode: push bx ; Keep BX mov al,ds:[484h] ; Get number of lines minus one inc ax ; Get number of lines (not minus one) mov ah,ds:[462h] ; Get active page mul ah ; Get starting line number add ax,ds:[44Ah] ; Add number of columns shl ax,1 ; Get offset of end of line sub ax,18 ; Back up to 9 chars back from end xchg ax,bx ; To BX cmp BYTE PTR ds:[463h],0B4h ; Check for monochrome mov ax,0B000h ; Prepare for monochrome je HaveRegen ; If so mov ah,0B8h ; If not, use CGA regen buffer HaveRegen: mov ds,ax ; Address regen buffer with DS mov WORD PTR ds:[bx-2],720h ; Space before time mov al,Hours ; Get hours and al,7Fh ; Mask off AM/PM bit call StoreBCD ; Convert BCD to ASCII and store mov al,Minutes ; Get minutes call StoreColonBCD ; Convert BCD to ASCII and store mov al,Seconds ; Get seconds call StoreColonBCD ; Convert BCD to ASCII and store mov WORD PTR ds:[bx],720h ; Space after time pop bx ; Restore BX GraphMode: pop ds ; Fix up ret ; Done DisplayTime ENDP StoreColonBCD PROC near mov WORD PTR ds:[bx],":"+700h ; Colon (with attribute) inc bx inc bx ; Bump pointer StoreBCD PROC near push ax ; Keep shr al,1 shr al,1 shr al,1 shr al,1 call StoreBCDChar pop ax and al,0Fh StoreBCDChar PROC near add al,"0" mov ah,7 mov ds:[bx],ax inc bx inc bx ; Bump pointer ret StoreBCDChar ENDP StoreBCD ENDP StoreColonBCD ENDP Discard EQU $ ; Discard point TSRParas = (OFFSET (Discard-@curseg+15) SHR 4) ASSUME ds:ComFile SignOnMsg DB "Sample program #10 - TSR clock using int 8 and direct RTC access",13,10 DB "Part of the PC Timing FAQ / Application notes",13,10 DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10 DB "Installed",13,10,13,10,"$" ; Check DOS version Main2 PROC near mov ah,30h int 21h cmp al,2 ; Expect DOS 2.0 or later jae DOS_Ok int 20h ; Intercept int 8 DOS_Ok: mov ax,3508h int 21h ; Get vector for int 8 mov [Old08Ofs],bx mov [Old08Seg],es ; Store it mov dx,OFFSET NewInt08 mov ax,2508h int 21h ; Set new vector mov es,ds:[2Ch] ; Get segment of environment block mov ah,49h int 21h ; Deallocate our copy of environment mov dx,OFFSET SignOnMsg mov ah,9 int 21h ; Display message mov dx,TSRParas ; Number of paragraphs to leave resident mov ax,3100h int 21h ; Go resident Main2 ENDP ComFile ENDS END Main -------------------------------- snip snip snip -------------------------------- This program demonstrates why interrupts must be locked out during manipulation of hardware devices such as the RTC, because this TSR's int 8 handler explicitly changes the address register in the RTC on each timer tick. If some foreground code set the address register then made an access to the data register, without disabling interrupts around the sequence, very occasionally an int 8 could be signalled between the address register access and the data register access, causing an incorrect value to be read or causing the time to change to a random or meaningless value. This type of bug could be almost impossible to track down. This is why it is important to follow these guidelines, though programs that do not follow these guidelines often appear to work correctly. This is also a good example of a TSR which lengthens processing of int 8 and also lengthens this processing unevenly; most int 8 calls will be lengthened only slightly, but every time the seconds change, the int 8 invocation will be lengthened by a much greater amount. See section ¯¯ 6.16.1. ## 7.36 THE RTC INTERRUPT AND RELATED BIOS FUNCTIONS The RTC interrupt is IRQ8, which maps to int 70 hex. It does not exist on the PC and XT. This interrupt is invoked when any enabled interrupt source in the RTC issues an interrupt, providing that IRQ8 is enabled in the secondary PIC's Interrupt Mask Register (section ¯¯ 6.10) and IRQ2, the cascade interrupt, is enabled in the primary PIC's IMR. See section ¯¯ 7.35.4 for info on how to enable and disable the four interrupt sources in the RTC. Usually, only the Alarm and the Periodic Interrupt triggers on the RTC are ever used. The RTC interrupt is used in three ways: þ The 24-hour Alarm signal from the RTC (see section ¯¯ 3.4), þ The Event Wait and Delay functions of the BIOS (section ¯¯ 7.36.1), þ User programs (e.g. slow-down programs or programs that measure the execution time of other programs). The Alarm signal uses the Alarm function of the RTC (obviously), and this interrupt source is only enabled if the appropriate BIOS function has been called to enable the alarm function. The other two uses of int 70h involve the periodic interrupt from the RTC, which is operated at 1024 interrupts per second (see section ¯¯ 7.35.3), giving an interrupt every 976.5625 microseconds. This interrupt source on the RTC is only enabled when the BIOS Event Wait or Delay functions are requested, unless explicitly enabled by a foreground program or TSR directly accessing the RTC registers. IRQ8 (int 70h) must also be enabled in the PIC IMR. Often it will be left enabled, and the various interrupt sources will be controlled at the RTC, but some BIOSes may also disable the interrupt level when it is no longer required. ## 7.36.1 THE BIOS EVENT WAIT AND DELAY FUNCTIONS On AT and later machines, the BIOS provides an 'event wait' function and a delay function, that use the RTC interrupt for timing. The 'event wait' and delay functions use nine bytes of RAM in the BIOS data area, which are defined as follows: Address Type Description 0040:0098 Far ptr Pointer to byte to be set to 80h when event wait completes 0040:009C DWord Counter (down-counter, microseconds) 0040:00A0 Byte Status: 00h = Idle 01h = Event Wait or Delay in progress 80h = Delay time elapsed (transitional) The functions are as follows. Set Event Wait : int 15h Call with: AX = 8300 hex CX = Time to wait (microseconds) hiword DX = Time to wait (microseconds) loword ES:BX = Pointer to flag to be set when complete Returns: CF = Error indication (see below) This function sets up control information in the BIOS data area, and starts an 'event wait' timeout by enabling IRQ8 (int 70h) in the PIC and enabling the periodic interrupt via the RTC. It returns to the caller while the event wait is in progress. The event wait is counted down in the background, by the BIOS's IRQ8 (int 70h) handler. When the specified time elapses, the interrupt handler sets the byte that was pointed to by ES:BX when the function was invoked, to 80h, and the wait is complete. If this function is called when an event wait or delay function (described later) is in progress, it will return with carry set and ignore the request. If the function is not supported by the BIOS, it will return with carry set and AH set to 80h or 86h. Cancel Event Wait : int 15h Call with: AX = 8301 hex Returns: CF = Error indication This function cancels the event wait currently in progress. It disables the periodic interrupt in the RTC and resets the event wait status byte at 0040:00A0 to zero. Delay : int 15h Call with: AH = 86 hex CX = Time to delay (microseconds) hiword DX = Time to delay (microseconds) loword Returns: CF = Error indication This function delays for the number of microseconds specified in CX and DX, and returns with carry clear when the delay is complete. It returns with carry set and AH set to 80h or 86h if the function is unsupported. It returns with carry set if the delay function was called while an Event Wait or another Delay was in progress. This function uses the same data structure as the Event Wait function, but sets the pointer that determines the byte to be set to 80h when the wait completes, to point to the status byte. The time for these functions is specified in microseconds, but the resolution is only 977 us, since timing is done using the RTC interrupt. The periodic interrupt in the RTC is not resynchronised by the function, so there is also a 977 us uncertainty at the start of the time period, which limits accuracy of short delays. Also, IRQ8 (int 70h) occurs every 976.5625 us but the handler subtracts 977 from the count each time. This is an error of 0.0448% (448 ppm, 38.71 seconds per day). This error is cumulative and could become significant on long delays (it will make the delay shorter than expected). If any software locks out interrupts for more than 977 us at a time, interrupts will be missed and the time period will be extended. The BIOS joystick reading function (section ¯¯ 10.4.2) and the joystick position reading function given in section ¯¯ 10.4.4 may cause this problem. I have heard that the Event Wait function is used by the hard disk and floppy disk BIOS code, but I don't know the details. Info is welcomed. (*) ## 7.36.2 THE BIOS RTC INTERRUPT HANDLER The BIOS has its own IRQ8 (int 70h) handler, which counts down the Event Wait or Delay time value and sets the flag byte to 80h when the time expires (see section ¯¯ 7.36.1 for the gory details). The handler makes use of the three variables in the BIOS data area which are described in section ¯¯ 7.36.1. The exact behaviour may vary from one BIOS to another but is something like: First, check whether an alarm interrupt occurred (using Register C of the RTC, see section ¯¯ 7.35.5). If so, invoke int 4Ah (see section ¯¯ 3.4). Then check whether a periodic interrupt occurred. If so, subtract 977 from the unsigned long microsecond counter (see section ¯¯ 7.36.1). If this resulted in the long variable borrowing (i.e. wrapping around from a small positive number to a negative number), zero the status flag (see section ¯¯ 7.36.1), disable the regular interrupt source in the RTC, then set to 80 hex the byte pointed to by the far pointer in the BIOS data area (see section ¯¯ 7.36.1 again). The RTC interrupt handler in some BIOSes may unconditionally turn off the periodic interrupt enable in the RTC if the status flag (see section ¯¯ 7.36.1 again) is zero, to avoid unnecessary processor overhead (1024 interrupts per second can be significant). ## 7.36.3 USING THE RTC INTERRUPT The RTC interrupt is int 70h (IRQ8). When a program uses the RTC interrupt, it should chain to the original handler, because the BIOS may be in the middle of a Delay or Event Wait. The BIOS's int 70h handler may interfere with your program, by turning off the periodic interrupt enable in the RTC, so your int 70h handler must re-enable it after calling the BIOS's handler. The BIOS's handler will also count down the microseconds counter (see sections ¯¯ 7.36.1 and ¯¯ 7.36.2) and when it borrows, will set a memory variable at the address pointed to by the pointer in the BIOS data area, to 80 hex. This may not be desirable, as this pointer may be uninitialised, or may point to a variable in a program that is no longer running, etc. Therefore your program should be careful to prevent the BIOS's handler from doing this. This is demonstrated in the sample program in section ¯¯ 7.36.4. ## 7.36.4 SAMPLE PROGRAM: USING THE RTC INTERRUPT -------------------------------- snip snip snip -------------------------------- /* Sample program #11 Demonstrates using the RTC periodic interrupt Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save and assemble the critical error module CRIT_ERR Save this sample code to SAMPLE11.C Compile this module with: bcc -c -I -ms sample11.c Link the modules with: tlink /c /x \c0s.obj sample11.obj crit_err.obj, sample11, nul, \cs Where inc_path is the path to your C header files, c0_path is the path to your startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Needed for bioskey() */ #include /* Needed for inportb(), outportb(), etc */ #include /* Needed for _write() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define STDERR 2 /* DOS handle for standard error */ static unsigned long rtcticks; /* Counter for RTC interrupts */ void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */ unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */ typedef void interrupt (far *intfuncp)(); /* Pointer to an int handler */ intfuncp old_int70 = (intfuncp)0xFFFFFFFFL; unsigned char read_rtc_register(unsigned char reg_num) { unsigned char rv; asm pushf; asm cli; outportb(0x70, reg_num); asm jmp SHORT $+2 asm jmp SHORT $+2 asm jmp SHORT $+2 rv = inportb(0x71); asm popf; return rv; } void write_rtc_register(unsigned char reg_num, unsigned char value) { asm pushf; asm cli; outportb(0x70, reg_num); asm jmp SHORT $+2 asm jmp SHORT $+2 asm jmp SHORT $+2 outportb(0x71, value); asm popf; return; } void enable_rtc_int(void) { asm pushf; asm cli; write_rtc_register(0x0B, read_rtc_register(0x0B) | 0x40); outportb(0xA1, inportb(0xA1) & 0xFE); outportb(0x21, inportb(0x21) & 0xFB); asm popf; return; } void interrupt int70_handler(void) { if (read_rtc_register(0x0C) & 0x40) ++rtcticks; /* Increment RTC tick counter */ (old_int70)(); /* Chain to BIOS int 70h handler */ enable_rtc_int(); /* Make sure RTC int is still enabled */ if (* ((unsigned int far *)MK_FP(0x40, 0x9E)) > 0xFFFD) * (unsigned int far *)MK_FP(0x40, 0x9E) = 0xFFFF; return; /* From interrupt */ } void abort_cleanup(int dos_is_safe) { if (dos_is_safe) { if (old_int70 != (intfuncp)0xFFFFFFFFL) { setvect(0x70, old_int70); old_int70 = (void far *)0xFFFFFFFFL; } } else { if (old_int70 != (intfuncp)0xFFFFFFFFL) { *((intfuncp far *)MK_FP(0, 0x70 << 2)) = old_int70; old_int70 = (void far *)0xFFFFFFFFL; } } return; } void interrupt ctrl_c_handler(void) { static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n"; if (is_at_crit_prompt()) abort_cleanup(FALSE); else { abort_cleanup(TRUE); _write(STDERR, &message, sizeof(message)); } exit(255); } void main(void) { unsigned long msecs, secs; unsigned int partial; printf("Sample program #11 - Demonstrates using the RTC interrupt\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); printf("Press to exit\n\n"); crit_err_intercept(); /* Trap critical errors */ setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */ * (unsigned int far *)MK_FP(0x40, 0x9E) = 0xFFFF; old_int70 = getvect(0x70); setvect(0x70, int70_handler); asm cli; /* 1024 interrupts per second */ write_rtc_register(0x0A, (read_rtc_register(0x0A) & 0xF0) | 0x06); asm sti; enable_rtc_int(); while (1) { asm cli; msecs = rtcticks; asm sti; msecs *= 125; /* Calculate * 125 / 128 */ msecs >>= 6; if (msecs & 1) ++msecs; /* Round up */ msecs >>= 1; secs = msecs / 1000; partial = msecs % 1000; printf("%ld.%03d seconds\r", secs, partial); if (bioskey(1)) if ((bioskey(0) & 0xFF) == 27) break; } setvect(0x70, old_int70); old_int70 = (void far *)0xFFFFFFFFL; exit(0); } -------------------------------- snip snip snip -------------------------------- The int70_handler() function first checks that this int 70h is caused by the periodic interrupt, and if so, it increments its counter. It then calls the BIOS int 70h handler unconditionally. The BIOS handler will send the EOI to both PICs and will probably turn off the periodic interrupt enable flag in the RTC, so this handler turns the periodic interrupt back on. It also tries to ensure that no problems will occur with the timeout detection of the BIOS's handler. When the long microseconds counter at 0040:009C is decremented below zero by the BIOS handler, the BIOS handler writes to a memory variable pointed to by the pointer at 0040:0098, and this pointer may not have been initialised. By keeping the microsecond count at 0xFFFFxxxx, this routine prevents this problem. The mainline also sets the microsecond counter to 0xFFFFxxxx. This should allow the Delay and Event Wait functions to be used without interference from this program. ## 7.37 USING CTC CHANNEL ONE AND REFRESH DETECT My thanks to William Luitje (luitje@m-net.arbornet.org) for introducing me to this technique. William reports that it is used by the AMI BIOS during floppy disk operations. As shown in section ¯¯ 7.5, bit 4 of Port B at I/O address 61 hex on an AT and later machine is a read-only bit carrying a signal called Refresh Detect. This signal comes from a 'T' (toggle) flip-flop which is clocked by the refresh trigger signal, which comes from CTC channel one. I assume it is used by the BIOS POST (Power-on self-test) to check that CTC channel one is functioning correctly (IBM's paranoid self-test code has to test every single logic gate on the entire motherboard - this from the people who created the error message "Keyboard error or no keyboard present - press F1 to continue" :-) Assuming that the RAM refresh rate has not been changed (see section ¯¯ 7.4.3), this bit will toggle (change from 0 to 1 or from 1 to 0) once every 15.0857 microseconds (the exact value is 216/14.31818), and Port B can be polled in a loop to implement a delay of any length. For short delays, with interrupts locked out, this gives an accurate and very convenient relative delay mechanism. However, for long delays, it would be naughty to leave interrupts locked out for the entire delay period, and interrupts will cause gaps in the polling process, slightly lengthening the delay (it will wait longer than expected). There are several caveats. This method will not work on PCs and XTs. Also it will not work in an environment where Port B is emulated (for example, under OS/2 and probably any other virtual DOS machine). Finally, if the DRAM refresh period has been changed, the timing will be changed proportionately. ## 7.37.1 SAMPLE PROGRAM: TIMING THE REFRESH DETECT SIGNAL This program uses CTC channel 2 to measure a sampling period of half a second with interrupts locked out, and counts the number of transitions on the Refresh Detect signal during this period. It displays the value after each half-second sample, and repeats the sample continuously until Ctrl-C is pressed. Warnings about using this program: It will not work on an emulated machine, i.e. under OS/2 or any other multi-tasking operating system that gives it a virtual DOS machine. The program will not run on an old PC or XT; an AT or later machine is required. The program will disrupt the DOS time of day, so the machine should be rebooted after running this program if that is a problem. Also, it does not check the absolute accuracy of the Refresh Detect signal; the signal being measured and the sample timer are both derived from the same clock source. The joystick reading sample program in section ¯¯ 10.4.4 also demonstrates the Refresh Detect signal used as a timing reference. -------------------------------- snip snip snip -------------------------------- NAME SAMPLE12 ; Sample program #12 ; Demonstrates timing the Refresh Detect signal ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; ; This program assembles into SAMPLE12.COM, a small program which measures the ; number of DRAM refreshes in a half-second interval. It uses CTC channel 2 ; in mode 3 to measure half-second sample periods, and counts the number of ; transitions on the Refresh Detect signal on Port B bit 4. After each ; half-second sample, the value is displayed, and the program repeats itself. ; To terminate the program, press any key and wait. ; ; Save this file to SAMPLE12.ASM and assemble with: ; masm sample12; ; link sample12; ; exe2bin sample12.exe sample12.com ; or ; tasm sample12; ; tlink /t sample12; ; CTC2Divisor = 47727 ; 40ms CTC2Toggles = 25 ; Number of CTC channel 2 toggles ; expected in half a second ComFile SEGMENT ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing ORG 100h ; Com-type file Main PROC near jmp Main2 ; Skip Main ENDP InitialMsg DB "Sample program #12 - Demonstrates timing the Refresh Detect signal",13,10 DB "Part of the PC Timing FAQ / Application notes",13,10 DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10,"$" NotATMsg DB "Refresh Detect is supported on ATs and later machines; this machine appears",13,10 DB "to be a PC or XT. The PC and XT do not support Refresh Detect.",13,10,"$" ExplanationMsg DB "The numbers displayed are the counts of DRAM refreshes in a 1/2-second sample",13,10 DB "period. For the standard DRAM refresh rate of 15.0857us, this number should",13,10 DB "be about 33144. If you have run a program to slow down the DRAM refresh, the",13,10 DB "numbers will be lower.",13,10,13,10 DB "To terminate this program, press any key and wait",13,10 NewlineMsg DB 13,10,"$" Main2 PROC near mov dx,OFFSET InitialMsg ; Opening message mov ah,9 int 21h ; Display it ; Determine machine type (code from section ¯¯ 7.5) pushf ; Keep interrupt flag mov cx,400h ; Six attempts (top bits of CH) cli ; Lock out interrupts during this stuff in al,61h ; Get Port B contents jmp SHORT $+2 ; Short delay mov ah,al ; Original value to AH Flip61Loop: xor ah,10000000b ; Flip top bit mov al,ah ; Get value to AL out 61h,al ; Write value to port jmp SHORT $+2 ; Short delay jmp SHORT $+2 ; Short delay in al,61h ; Read it back xor al,ah ; Set bit 7 if value didn't stay shl al,1 ; Shift bit into carry rcl cx,1 ; Shift bit into bottom of CX jnc Flip61Loop ; Loop if more flips (six in total). popf ; Restore interrupt flag test cl,cl ; Was port read/write? Zero if so. jnz MachineAT ; If it's an AT, continue mov dx,OFFSET NotATMsg mov ah,9 int 21h mov al,1 ; Errorlevel jmp SHORT Terminate MachineAT: mov dx,OFFSET ExplanationMsg mov ah,9 int 21h in al,61h ; Get Port B and al,11111101b ; Turn off speaker enable or al,00000001b ; Turn on Timer 2 Gate out 61h,al mov al,0B6h ; Set CTC channel 2 for mode 3, out 43h,al ; divisor of 47727, giving 20ms jmp short $+2 ; high, 20ms low mov al,LOW CTC2Divisor out 42h,al jmp short $+2 mov al,HIGH CTC2Divisor out 42h,al jmp short $+2 MainLoop: mov cl,3 ; Set up shift count for later mov bx,CTC2Toggles ; Number of channel 2 transitions xor dx,dx ; Counter for refreshes cli ; Lock out interrupts during sample Loop1: in al,61h ; Read Port B test al,00100000b ; Test CTC channel 2 readback jz Loop1 ; Wait until high Loop2: in al,61h ; Read Port B test al,00100000b ; Test CTC channel 2 readback jnz Loop2 ; Wait until low mov ah,al ; Keep old value in AH Loop3: in al,61h ; Read Port B xor al,ah ; Find different bits test al,00110000b ; Either bit changed? jz Loop3 ; If not, loop xor ah,al ; Keep new value shl al,cl ; Bit 5 into carry sbb bx,0 ; Decrement BX if T2 output changed jz Done ; If waited the full sample time shl al,1 ; Bit 4 into carry adc dx,0 ; Increment DX if refresh occurred jmp SHORT Loop3 ; Loop Done: sti ; Interrupts back on mov ax,dx ; Get refresh counter call Mach16_DecASC ; Convert to decimal and display mov dx,OFFSET NewlineMsg ; CR/LF message mov ah,9 int 21h ; Display it mov ah,1 ; Test for keypress pending int 16h jz MainLoop ; If no key pending xor ah,ah ; Zero int 16h ; Clear out the key xor al,al ; Errorlevel 0 Terminate: mov ah,4Ch ; Terminate with errorlevel int 21h ; Call DOS int 20h ; In case DOS-1 (!) Main2 ENDP Mach16_DecASC PROC near ; Func: Convert machine 16-bit unsigned value ; to ASCII decimal representation and ; output via DOS function 2 ; In: AX = Value to output ; Out: None ; Lost: AX BX CX DX xor cx,cx ; Zero digit counter Mach16_DecASC1: xor dx,dx ; Clear high word of value in DX|AX mov bx,10 ; Base div bx ; Divide by 10 add dl,"0" ; DL is remainder, convert to ASCII push dx ; Store on stack inc cx ; Increment char counter test ax,ax ; Any more digits left? jnz Mach16_DecASC1 ; If so, loop Mach16_DecASC2: pop dx ; Get char back mov ah,2 ; Print char int 21h ; Call DOS loop Mach16_DecASC2 ; Loop for all chars ret ; Done Mach16_DecASC ENDP ComFile ENDS END Main -------------------------------- snip snip snip -------------------------------- ## 7.37.2 SAMPLE CODE: DELAY(MILLISECONDS) FUNCTION USING REFRESH DETECT This function uses the Refresh Detect signal to provide a delay(milliseconds) function. This function does not check that the refresh channel is operating with the correct divisor. It also does not check that it is running on an AT or later machine with the required Port B hardware. If required, these checks should be done at the start of the program that will use this function. -------------------------------- snip snip snip -------------------------------- Params = 4 ; USE 6 FOR FAR CODE MODELS! _delay PROC near push bp ; Preserve BP mov bp,sp ; Address stacked parameters mov cx,[bp+Params] ; Get loword of number of milliseconds mov dx,[bp+Params+2] ; Get hiword mov bx,61714 ; Initialise negative count register in al,61h ; Read Port B initially mov ah,al ; To AH jmp SHORT DelayDecr ; Decrement count and loop if nonzero DelayLoop: in al,61h ; Read Port B xor al,ah ; Get different bits test al,00100000b ; Did Refresh Detect toggle? jz DelayLoop ; If not, keep waiting xor ah,00100000b ; Toggle last known state flag sub bx,931 ; Approximating the number of Refresh jnb DelayLoop ; of Refresh Detect toggles per add bx,61714 ; millisecond as 61714 / 931 DelayDecr: sub cx,1 ; One millisecond has elapsed sbb dx,0 ; Borrow into hiword jnb DelayLoop ; If more milliseconds remaining pop bp ; Restore BP from caller ret _delay ENDP -------------------------------- snip snip snip -------------------------------- The declaration for the above function is: void delay(unsigned long milliseconds) The actual number of Refresh Detect toggles per millisecond is 14318.18/216, or about 66.3. The above function approximates this ratio to be 61714/931, which contributes an error of 0.085767 ppm, less than 1/100th typical crystal error. The longest delay that can be generated (milliseconds = 0xFFFFFFFF) is 49 days, 17 hours, 2 minutes, and 47.295 seconds. For this duration, the error contributed by the approximation is about 0.368 seconds. The delay(milliseconds) function may be called with interrupts enabled or with interrupts disabled. It does not modify the state of the interrupt flag during its execution. If it runs with interrupts enabled, the actual length of the delay will normally be longer than specified, due to gaps in processing caused by the timer tick interrupt and any other active interrupts (keyboard interrupt, serial port interrupt, network card interrupt, etc). If it runs with interrupts locked out, it will give an accurate delay, but it may disrupt the normal operation of the machine by preventing interrupts from being processed for an excessive length of time - see sections ¯¯ 6.15 to ¯¯ 6.19 for more information on this problem. The uncertainty is one refresh period, or about 15.0857 microseconds. The overhead is a few microseconds on a fast machine, longer on a slow machine. ## 8 SPEEDING UP THE TIMER TICK Note: This section makes many references to earlier sections. I recommend that if you are not familiar with the normal operation of the CTC, the timer tick interrupt, general interrupt considerations, and interrupt chaining, you should first read the related sections and any other sections that seem relevant. Increasing the timer tick rate involves the following steps: þ Intercept int 8, redirecting it to your new int 8 handler þ Intercept and handle the Ctrl-C and Critical Error interrupts so that the int 8 vector can be restored if the program is terminated due to a Ctrl-C or a critical error þ Reprogram the CTC channel zero divisor for the new interrupt rate þ Maintain a counter within your int 8 handler to schedule chaining to the original int 8 handler þ Restore int 8 and restore the normal divisor upon termination See section ¯¯ 6.3 for details of how to intercept an interrupt. See section ¯¯ 6.31 for information on chaining to the old interrupt handler, and section ¯¯ 5 and subsections for information about the Ctrl-C and Critical Error interrupts and how to handle them. See section ¯¯ 7.10 for how to program the divisor. See section ¯¯ 6.31 for details on how to chain to the original int 8 handler, and sections ¯¯ 6.28 and ¯¯ 6.28.1 for information on ending interrupt routines when they are not chained. The comments in section ¯¯ 6.15 and section ¯¯ 6.16 and subsections regarding interrupt jitter also apply when the timer tick is operated at a faster rate, because the maximum period for which interrupts can be locked out without loss of a timer interrupt becomes shorter as the timer interrupt rate is increased. Changing the divisor and/or operating mode of CTC channel 0 may also break the BIOS's joystick reading functions (see section ¯¯ 10.4.2). Also see section ¯¯ 8.4. The technique of speeding up the timer tick should not be used in TSRs because foreground programs are at liberty to use and reprogram the CTC chip for their own purposes. ## 8.1 THE FAST TICK INT 8 HANDLER Having reprogrammed the timer tick interrupt to operate at a higher speed, you must ensure that other software that uses int 8 (see section ¯¯ 6.1) is called at the correct rate, i.e. 18.2065 times per second. This is achieved with a counter variable, which duplicates the behaviour of CTC channel zero when it is operating with the normal divisor of 65536 (see section ¯¯ 7.4 and subsections). This operates by maintaining a 16-bit variable which accumulates CTC clock periods and will overflow after 65536 CTC clocks, indicating that another 54.9254 ms have elapsed. This variable is maintained by the new int 8 handler. Every time int 8 is signalled, the new channel zero divisor value (which represents the number of CTC clocks since the last int 8) is added into this variable, and if the variable carries (i.e. exceeds 65535 and wraps around), the old int 8 handler is called (i.e. is scheduled). If the variable does not wrap around, then the old int 8 handler is not called. For example, assume CTC channel zero is operating with a divisor of 1234 (decimal). This will give a fast tick rate of 1193181.66666... / 1234, or about 967 ticks per second. Each time the new int 8 handler is triggered, 1234 CTC clocks have elapsed (since the last time it was triggered), so we add 1234 into the scheduler variable, representing the number of CTC clocks that have just elapsed. When the variable wraps around (i.e. the processor's carry flag is set after the addition), another 65536 CTC clocks have elapsed, so it is time to chain to the original int 8 handler, which expects to be called every 65536 CTC clocks (the "slow tick" rate, if you like). Thus the variable mimics the CTC channel zero counting register when programmed for a divisor of 65536. Of course the slow ticks (calls to the old int 8 handler) will not be perfectly evenly spaced, but in almost all applications, variations are acceptable as long as they are not cumulative, and they will not be cumulative (unless fast ticks are missed, see section ¯¯ 6.16 and subsections), and if the new tick rate is high (like the example above) they will be fairly even. The worst slow tick jitter will occur with divisors near to 32768, where calls to the slow tick handler could be up to almost 32768 CTC clocks early or late. Even this will not be a problem under DOS, in most circumstances. ## 8.2 THE INTERFACE WITH THE MAINLINE Generally the fast tick handler will have some sort of interface with the mainline (the foreground process) of your program. Typically this will be implemented via shared variables. These variables transfer control information from the mainline to the interrupt routine (as in the Morse code player example program described later), or may transfer status or time information from the interrupt routine to the mainline (as in the one millisecond timer program also described later), or a combination of both. The shared variables can be put in either the code segment or the data segment. For a COM file (tiny model) the segments are the same, and this makes things quite convenient. See section ¯¯ 6.32.1 for more information. ## 8.3 WRITING A FAST TICK HANDLER Fast tick handlers are often written in assembly language as it is more convenient and more efficient, though the latter advantage is largely mooted by the speed of modern processors. Refer to section ¯¯ 6.32 and subsections for a discussion of guidelines that must be applied when writing an interrupt handler in assembly language. The fast tick handler must follow these guidelines. After the housekeeping instructions, the fast tick interrupt handler should perform the function that it is required for, then before it exits, it should handle chaining to the slow tick interrupt handler. This involves adding the divisor value into the scheduler variable and deciding whether to chain to the slow tick handler or not. If it decides to chain, it can use the JMP chaining method (see section ¯¯ 6.31 for details). If it does not chain, it must send an EOI signal to the PIC (see section ¯¯ 6.28) and return with an IRET. To support Microchannel machines, it may be necessary to acknowledge the int 8 - see section ¯¯ 6.28.1. Here is an example fast tick interrupt handler written in assembler for tiny model (i.e. a COM file). It increments the FastTickCount variable on each fast tick. This variable is for use by the mainline. -------------------------------- snip snip snip -------------------------------- FastTickRate EQU 1234 ; This is the new fast tick rate FastTickCount DW 0 ; Counter variable for use by mainline SlowTickSched DW 0 ; Schedule control var for slow tick ASSUME cs:_TEXT,ds:nothing,es:nothing,ss:nothing NewInt8Handler PROC far pushf ; Keep flags push ax ; Keep AX ; Push any other registers you will modify inc FastTickCount ; This is the 'action' in this example ; Pop any other registers you pushed, in ; reverse order, but do not pop AX or Flags add SlowTickSched,FastTickRate ; Add ticks into variable jnc NoSchedule ; If it didn't carry ; Another 54.9254 ms has elapsed - chain to the slow tick handler pop ax ; Restore AX popf ; Restore flags DB 0EAh ; JMP xxxx:yyyy Old08Ofs DW 0 ; Vector to original handler - Offset Old08Seg DW 0 ; Segment ; Not time to chain yet - send EOI and return. Note - may not support ; Microchannel machines which may require a hardware int 8 acknowledge ; signal to be issued. NoSchedule: mov al,20h ; EOI code for PIC out 20h,al ; Send pop ax ; Restore AX popf ; Restore flags iret NewInt8Handler ENDP -------------------------------- snip snip snip -------------------------------- Of course to use this interrupt handler, you must have first intercepted int 8 and reprogrammed CTC channel zero with a divisor of 1234. Also for safety you must have intercepted the DOS Ctrl-C and Critical Error interrupts so that the original channel zero divisor, and original int 8 handler address, can be restored if the program terminates for either of these reasons. ## 8.4 COMMENTS ON FAST TIMER TICK INTERRUPTS {JAM} makes some good comments about this (slightly paraphrased): "Speeding up the timer tick interrupt presents two problems. The first is the increased load on the CPU, and the second is that any routine that disables interrupts for over twice the fast tick interrupt period will cause a missed interrupt. Masking for less then the interrupt period will cause interrupt delivery jitter and maybe the loss of a fast tick interrupt. "In the days of 8 MHz ATs, the former problem was the dominant one; now with faster computers and more complicated operating systems and TSR programs, the more subtle second problem dominates." Klaus Hartnegg (klaus@mailserv.brain.uni-freiburg.de) tried using a fast timer interrupt at 4, 6, and 8 kHz, and reports "serious problems with interrupts generated by network and keyboard (especially bad with DOS's KEYB.COM driver, a lot better with a freeware replacement). I have come to the conclusion that it's probably not possible to rely on such a high frequency timer interrupt. There are too many periods of time with disabled interrupts that cause lost interrupts." ## 8.5 SAMPLE PROGRAM: MORSE PLAYER USING FAST TIMER TICK The following program demonstrates the techniques involved in operating the timer tick at a higher speed. The tickdiv variable contains the new divisor that is programmed into CTC channel zero. The int8sched variable controls chaining to the original handler. It duplicates the normal action of CTC channel zero when programmed with a divisor of 65536, as described in section ¯¯ 8.3. A single queue interface is used to transfer control information from the mainline to the timer tick handler. The int 8 routine has full control of the speaker hardware, and generates beeps using CTC channel two in response to control words sent from the mainline via the queue. Most of the code is fairly self-explanatory. The fast tick interrupt is chosen according to the speed selected via the command line parameter, which may be any number from 1 to 99. The speed doubles for each decade. There are ten divisors, contained in the tickdivs[] array. Within each decade of speed numbers, the tickdivs[] values give a smoothly increasing speed, then from one decade to the next, the number of interrupts per 'dit' or 'dah' goes up in powers of two, resulting in a smooth speed scale, with each decade giving a 2:1 increase in playback speed. For testing, a speed of 50 is reasonable. By the way, when speaking Morse code, speak a '.' as 'dit' and '-' as 'dah', and join a dit to any following dit or dah in the same letter code. So, for example, the Morse code for the letter C ("-.-.") is spoken "dah-di-dah-dit". A proper Morse code player would be much more powerful, but this program is coming dangerously close to being useful. I will be more careful in future :-) See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #13 Demonstrates fast timer tick Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save and assemble the critical error module CRIT_ERR Save this sample code to SAMPLE13.C Compile this module with: bcc -c -I -ms sample13.c Link the modules with: tlink /c /x \c0s.obj sample13.obj crit_err.obj, sample13, nul, \cs Where inc_path is the path to your C header files, c0_path is the path to your startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Needed for bioskey() */ #include /* Needed for MK_FP() */ #include /* Needed for _write() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define STDERR 2 /* DOS handle for standard error */ #define MORSEBUFSIZE 128 /* Number of entries in morse data buffer */ #define BEEP_DIVISOR 2000 /* Freq = 1193182 / BEEP_DIVISOR */ #define DIT_LENGTH 1 /* Length of a dit */ #define DAH_LENGTH 3 /* Length of a dah */ #define DIT_SPACING 1 /* Spacing between dits/dahs within a letter */ #define LETTER_SPACING 3 /* Spacing between letter codes */ #define WORD_SPACING 6 /* Spacing between words */ #define STOP_SPACING 10 /* Spacing after a full stop (period) '.' */ #define ONOFF 0x8000 /* Top bit controls tone on or off */ static unsigned int tickdivs[10] = { 16384, 15287, 14263, 13308, 12417, 11585, 10809, 10086, 9410, 8780 }; static unsigned int morsebuf[MORSEBUFSIZE]; /* Communication between mainline and int 8 stuff */ /* Characters for morsecode array: "" Ignore this code completely "w" Word space - enforce a word spacing at this point "s" Stop space - enforce a full stop spacing at this point ".--." Actual code to send, using letter spacing at end */ static unsigned char morsecode[128][7] = { "", "", "", "", "", "", "", "w", /* 0 to 7 */ "", "w", "w", "", "w", "w", "", "", /* 8 to 15 */ "", "", "", "", "", "", "", "", /* 16 to 23 */ "", "", "", "", "", "", "", "", /* 24 to 31 */ "w", "", "", "", "", "", "", "", /* ' ' to ''' */ "", "", "", "", "", "", "s", "", /* '(' to '/' */ "-----", ".----", "..---", "...--", "....-", /* 0 to 4 */ ".....", "-....", "--...", "---..", "----.", /* 5 to 9 */ "w", "", "", "", "", "", "", /* ':' to '@' */ ".-", "-...", "-.-.", "-..", ".", /* 'A' to 'E' */ "..-.", "--.", "....", "..", ".---", /* 'F' to 'J' */ "-.-", ".-..", "--", "-.", "---", /* 'K' to 'O' */ ".--.", "--.-", ".-.", "...", "-", /* 'P' to 'T' */ "..-", "...-", ".--", "-..-", "-.--", "--..", /* 'U' to 'Z' */ "", "", "", "", "", "", /* '[' to '`' */ ".-", "-...", "-.-.", "-..", ".", /* 'a' to 'e' */ "..-.", "--.", "....", "..", ".---", /* 'f' to 'j' */ "-.-", ".-..", "--", "-.", "---", /* 'k' to 'o' */ ".--.", "--.-", ".-.", "...", "-", /* 'p' to 't' */ "..-", "...-", ".--", "-..-", "-.--", "--..", /* 'u' to 'z' */ "", "", "", "", "" /* '{' to Del */ }; static unsigned int timescaler; /* Time range scaler */ static unsigned int inptr; static volatile unsigned int outptr; /* Offsets into morsebuf */ static unsigned int tickdiv; /* Actual chosen tick divisor */ void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */ unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */ typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */ intfuncp old_int8 = (intfuncp)0xFFFFFFFFL; /* Communication between the mainline and the int 8 handler is via the morsebuf array, which is used as a queue. Each entry in morsebuf is a 16-bit unsigned int. The top bit determines whether the beeping sound should be turned on (if set) or off (if clear), and the remaining bits determine how many fast ticks the int 8 routine will wait after actioning the top bit, before it fetches the next word from morsebuf. Access to morsebuf is controlled by the in and out pointers, which are actually offsets, not pointers. When these are equal, the queue is empty. */ void interrupt int8_handler(void) { static unsigned int int8counter = 0; static unsigned int int8sched = 0; if (int8counter) --int8counter; if ((int8counter == 0) && (outptr != inptr)) { /* Data there */ int8counter = morsebuf[outptr]; ++outptr; if (outptr >= MORSEBUFSIZE) /* Bump out ptr */ outptr = 0; if (int8counter & ONOFF) { /* Turn sound on */ outportb(0x43, 0xB6); /* Ch 2, mode 3 */ outportb(0x42, (BEEP_DIVISOR & 0xFF)); /* Lobyte */ outportb(0x42, (BEEP_DIVISOR >> 8)); /* Hibyte */ outportb(0x61, inportb(0x61) | 0x03); /* Speaker on */ } else { /* Turn sound off */ outportb(0x61, inportb(0x61) & 0xFC); /* Speaker off */ } int8counter &= (~ ONOFF); /* Remove on/off bit */ } int8sched += tickdiv; if (int8sched < tickdiv) { /* If carried */ (old_int8)(); /* Chain to BIOS */ } else /** note - may not support Microchannel machines */ outportb(0x20, 0x20); /* Send EOI if not chaining */ return; /* From interrupt */ } void restore_normal(void) { asm pushf; asm cli; outportb(0x43, 0x36); outportb(0x40, 0); outportb(0x40, 0); /* Restore normal divisor */ outportb(0x61, inportb(0x61) & 0xFC); /* Speaker off */ asm popf; return; } void abort_cleanup(int dos_is_safe) { if (dos_is_safe) { if (old_int8 != (intfuncp)0xFFFFFFFFL) { setvect(0x08, old_int8); old_int8 = (intfuncp)0xFFFFFFFFL; } } else { if (old_int8 != (intfuncp)0xFFFFFFFFL) { *((intfuncp far *)MK_FP(0, 0x08 << 2)) = old_int8; old_int8 = (intfuncp)0xFFFFFFFFL; } } restore_normal(); return; } void interrupt ctrl_c_handler(void) { static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n"; if (is_at_crit_prompt()) abort_cleanup(FALSE); else { abort_cleanup(TRUE); _write(STDERR, &message, sizeof(message)); } exit(255); } void poll_exit(void) { if (bioskey(1)) { if ((bioskey(0) & 0xFF) == 27) { setvect(0x08, old_int8); old_int8 = (intfuncp)0xFFFFFFFFL; restore_normal(); exit(0); } } return; } void putmorse(unsigned int codeval) { unsigned int tempptr; poll_exit(); tempptr = inptr + 1; if (tempptr >= MORSEBUFSIZE) tempptr = 0; while (outptr == tempptr) poll_exit(); /* Wait for space in the queue */ codeval = (((codeval & (~ONOFF)) << timescaler) | (codeval & ONOFF)); morsebuf[inptr] = codeval; inptr = tempptr; return; } void playmorse(char * str) { char ch; char * cp; unsigned int was_word; was_word = FALSE; cp = str; while ((ch = *cp++) != '\0') { switch (ch) { case 'w' : putmorse(WORD_SPACING); break; case 's' : putmorse(STOP_SPACING); break; case '.' : putmorse(DIT_LENGTH | ONOFF); putmorse(DIT_SPACING); was_word = TRUE; break; case '-' : case '_' : putmorse(DAH_LENGTH | ONOFF); putmorse(DIT_SPACING); was_word = TRUE; break; } } if (was_word) putmorse(LETTER_SPACING); return; } void main(int argc, char * argv[]) { unsigned int speedrange; int ch; FILE * infile; printf("Sample program #13 - Morse code player demonstrating fast timer tick\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); if ((argc < 3) || (strlen(argv[1]) != 2)) { printf("Usage: SAMPLE13 speed filename\n\n"); printf("\tspeed = 10 to 99, speed doubles each decade\n"); printf("\tfilename = name of file to be played\n"); exit(1); } timescaler = 8 - (argv[1][0] - '1'); /* Shift count for timings */ speedrange = argv[1][1] - '0'; /* Fine speed, 0-9 */ if ((timescaler > 8) || (speedrange > 9)) { printf("Speed out of range\n"); exit(2); } tickdiv = tickdivs[9 - speedrange]; infile = fopen(argv[2], "r"); if (infile == NULL) { printf("Could not open input file '%s'\n", argv[2]); exit(4); } printf("Press to exit\n"); crit_err_intercept(); /* Trap critical errors */ setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */ old_int8 = (intfuncp)getvect(0x08); setvect(0x08, int8_handler); asm cli; outportb(0x43, 0x36); outportb(0x40, tickdiv & 0xFF); outportb(0x40, tickdiv >> 8); asm sti; while ((ch = fgetc(infile)) != EOF) if (ch < 0x80) playmorse(morsecode[ch]); putmorse(1); /* Make sure the speaker is off */ while (inptr != outptr) ; /* Wait for buffer to empty */ setvect(0x08, old_int8); old_int8 = (intfuncp)0xFFFFFFFFL; restore_normal(); exit(0); } -------------------------------- snip snip snip -------------------------------- ## 8.6 DYNAMIC FAST TICK PERIODS In the Morse code player sample program, once the new fast tick rate has been chosen and programmed, it is not modified until the program terminates. The interrupt keeps occurring regularly at the fast tick rate. However, it is possible to dynamically change the fast tick rate on a per-interrupt basis. There are several reasons why you might want to do this - I can think of four applications, there may be more: þ You might want to create a signal with an uneven or completely arbitrary duty cycle, such as 5 ms high, 40 ms low (this example could also be done using a constant fast tick at 5 ms intervals and counting eight interrupts to get the 40 ms delay), þ You might be using the timer interrupt to schedule things which happen at irregular intervals, with some long gaps, some short, þ You might want your background interrupt routine to be able to adjust its speed according to user actions, such as keypresses which control the program 'speed', þ You might want an exact number of interrupts per second, which is not possible with a fixed divisor - see section ¯¯ 8.7 for a sample program that does this. All of these requirements can be handled in the same way. The technique involves the interrupt routine adjusting the value in the Reload register according to its requirements, to adjust the period between interrupts in a dynamic fashion. When the fast timer tick interrupt handler reprograms the Reload register, the new Reload register value does not affect the current countdown in progress, i.e. the length of time until the next interrupt, it affects the length of time between the next interrupt and the interrupt after that. In other words, you could say there is a one interrupt delay before the new value takes effect. ## 8.7 SAMPLE PROGRAM: DYNAMIC FAST TICK INTERRUPT HANDLER This sample program gives a fast tick rate of exactly 1000 fast ticks per second, using an effective divisor of 1193.18166666.... This cannot be achieved with a static divisor - the closest static divisors of 1193 and 1194 produce 1000.152277 and 999.3146287 interrupts per second respectively. To get exactly 1000 fast ticks per second, the divisor must be changed dynamically to give an effective divisor of 1193.181666... by cycling through the appropriate sequence of 1193 and 1194 divisors. Over a short period of time, the tick rate will rapidly approach exactly 1000 ticks per second (ignoring the error due to crystal inaccuracies, etc). The sequence of divisors is determined as follows. For 1000 ticks per second the divisor is 1193.18166666... which is 1193 plus 9/50 (0.18) plus 1/600 (0.0016666...), which is also 1193 plus 1/5 minus 1/50, plus 1/600. Count every fifth interrupt. On four out of every five interrupts, use a divisor of 1193, but on every fifth interrupt, when the divide-by-five counter carries, prepare to use 1194, and count a divide by 10 counter (which is really dividing by 50). If the counter _doesn't_ carry, use 1194. These two counters in combination add the 9/50. If the divide by 10 counter carries, prepare to use 1193, and count down a divide by 12 counter, which is actually counting 1/600ths; if it carries, use 1194. A similar approach could be used to get 200 fast tick interrupts per second (i.e. a 5ms fast tick interval). The divisor is 5965.90833333333, which is 5965 plus 9/10 plus 1/120, so you would use 5966 for 9 of every 10 cycles and use 5965 on the tenth, except if it is the twelfth tenth cycle in which case use 5966. Mode two must be used for this technique. See the description of behaviour with odd divisors in section ¯¯ 7.8.5 for the reasons. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #14 Demonstrates dynamic timer tick rates Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save and assemble the critical error module CRIT_ERR Save this sample code to SAMPLE14.C Compile this module with: bcc -c -I -ms sample14.c Link the modules with: tlink /c /x \c0s.obj sample14.obj crit_err.obj, sample14, nul, \cs Where inc_path is the path to your C header files, c0_path is the path to your startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Needed for bioskey() */ #include /* Needed for MK_FP() */ #include /* Needed for _write() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define STDERR 2 /* DOS handle for standard error */ #define BASETICK 1193 void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */ unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */ typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */ intfuncp old_int8 = (intfuncp)0xFFFFFFFFL; static volatile unsigned long milliseconds = 0; /* Milliseconds counter */ /* The interrupt handler is responsible for updating the tick divisor to give exactly 1000 ticks per second. It also increments a 32-bit counter which is used by the mainline. */ void interrupt int8_handler(void) { static unsigned int div_5 = 2; static unsigned int div_5_10 = 5; static unsigned int div_5_10_12 = 6; static unsigned int int8sched = 0; static unsigned int fastdiv = 0; asm { mov ax,1193 /* Prepare to use 1193 */ dec [div_5] /* Count down divide by 5 */ jns GotNewDiv /* If not reached one fifth yet */ mov [div_5],4 /* Reset dividing register */ inc ax /* Prepare to use 1194 */ dec [div_5_10] /* Count down nested divide by 10 */ jns GotNewDiv /* If not reached 1/10 of 1/5 yet */ mov [div_5_10],9 /* Reset dividing register */ dec ax /* Prepare to use 1193 */ dec [div_5_10_12] /* Count down nested divide by 12 */ jns GotNewDiv /* If not reached 1/12 of 1/10 of 1/5 */ inc ax /* The 1/600th! */ mov [div_5_10_12],11 /* Reset dividing register */ } GotNewDiv: asm { cmp ax,[fastdiv] /* Got divisor in AX - did it change? */ je SameDiv /* If not, don't reprogram CTC 0 */ mov [fastdiv],ax /* Store new value */ out 40h,al /* Write new lobyte */ mov al,ah /* Get hibyte */ out 40h,al /* Write new hibyte */ } SameDiv: /* End of inline assembly */ ++milliseconds; /* Increment millisecond count */ int8sched += fastdiv; if (int8sched < fastdiv) { /* If carried */ (old_int8)(); /* Chain to BIOS */ } else /** note - may not support Microchannel machines */ outportb(0x20, 0x20); /* Send EOI if not chaining */ return; /* From interrupt */ } void restore_normal(void) { asm pushf; asm cli; outportb(0x43, 0x36); outportb(0x40, 0); outportb(0x40, 0); /* Restore normal divisor */ asm popf; return; } void abort_cleanup(int dos_is_safe) { if (dos_is_safe) { if (old_int8 != (intfuncp)0xFFFFFFFFL) { setvect(0x08, old_int8); old_int8 = (intfuncp)0xFFFFFFFFL; } } else { if (old_int8 != (intfuncp)0xFFFFFFFFL) { *((intfuncp far *)MK_FP(0, 0x08 << 2)) = old_int8; old_int8 = (intfuncp)0xFFFFFFFFL; } } restore_normal(); return; } void interrupt ctrl_c_handler(void) { static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n"; if (is_at_crit_prompt()) abort_cleanup(FALSE); else { abort_cleanup(TRUE); _write(STDERR, &message, sizeof(message)); } exit(255); } void poll_exit(void) { if (bioskey(1)) { if ((bioskey(0) & 0xFF) == 27) { setvect(0x08, old_int8); old_int8 = (intfuncp)0xFFFFFFFFL; restore_normal(); exit(0); } } return; } void main(void) { unsigned long ms; printf("Sample program #14 - Millisecond timer demonstrating dynamic timer tick\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); printf("Press to exit\n\n"); crit_err_intercept(); /* Trap critical errors */ setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */ old_int8 = (intfuncp)getvect(0x08); setvect(0x08, int8_handler); asm cli; outportb(0x43, 0x34); /* Must use mode two! */ outportb(0x40, BASETICK & 0xFF); outportb(0x40, BASETICK >> 8); asm sti; while (1) { asm cli; ms = milliseconds; asm sti; printf("%010ld ms\r", ms); poll_exit(); } } -------------------------------- snip snip snip -------------------------------- Note the order in which things are restored to normal in poll_exit(). The call to restore_normal() to set the tick rate back to normal, must appear after the fast tick handler has been disconnected. If the fast tick handler was still connected after the tick rate was set to normal, it could reprogram the CTC again, and the program would terminate with the tick running with a divisor of 1193 or 1194 (at roughly 1ms intervals)! This program could be used to measure the execution time of another program with 1ms resolution, provided that the other program did not use CTC channel zero itself, and provided that the other program did not lock interrupts out for more than 1ms at a time. ## 9 READING AN ABSOLUTE TIMESTAMP It is possible to read an absolute timestamp at any moment in time. This timestamp is comprised of the value read from the Counter Latch register in channel 0 of the CTC (see sections ¯¯ 7.14 and ¯¯ 7.15), which is 16 bits wide, and the BIOS Tick Count variable (see section ¯¯ 4) which is 32 bits wide, though only the bottom 21 bits are used. Mode two is easiest to use for this purpose. BIOSes traditionally set up CTC channel zero to run in mode three, but recent BIOSes seem to be using mode two. See section ¯¯ 7.4.2 for details. In order to be able to read an absolute timestamp, your program must first ensure that CTC channel zero is operating in mode two with a divisor of 65536 and that the lobyte/hibyte flag is in sync. This is most easily ensured by simply setting the mode and divisor in the initialisation section of your program. See section ¯¯ 7.10 and section ¯¯ 7.12 for details. Reading the count in progress is described in sections ¯¯ 7.15, ¯¯ 7.15.1, and ¯¯ 7.16. Reading the BIOS tick count variable is described in sections ¯¯ 4.5 and ¯¯ 4.6. To ensure that a correct value is read, it is necessary to read the BIOS tick count first, then read the count in progress in the CTC, then enable interrupts, then re-read the BIOS tick count, then work out whether the first or second BIOS tick count value is appropriate (if they are different). This is demonstrated in the sample program in section ¯¯ 9.1. ## 9.1 SAMPLE PROGRAM: ABSOLUTE TIME REFERENCE (TIMESTAMP) IN MODE TWO This program demonstrates the initialisation required to set the timer to run in mode two, and a function that will return an absolute timestamp, in units of 0.8381 microseconds since midnight on the current day. Initially it will display the timestamp every time a key is pressed. Once the key is pressed, it goes into continuous timestamp checking mode, where it continuously requests and displays the absolute timestamp, and also checks that the timestamp never goes backwards. If the timestamp goes backwards, it displays the two timestamp values before the error, and the first timestamp after the negative increment. This will normally occur only at midnight. Pressing again will terminate the program. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #15 Demonstrates absolute timestamping in mode two Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE15.C and compile with: bcc -I -L -ms sample15.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, cli, and sti */ #include /* Needed for bioskey() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 typedef struct { unsigned int part; unsigned long ticks; } timestamp; #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL) void set_mode2(void) { auto unsigned int tick_loword; tick_loword = * BIOS_TICK_COUNT_P; while ((unsigned int) * BIOS_TICK_COUNT_P == tick_loword) ; asm pushf; asm cli; outportb(0x43, 0x34); /* Channel 0, mode 2 */ outportb(0x40, 0x00); /* Loword of divisor */ outportb(0x40, 0x00); /* Hiword of divisor */ asm popf; return; } void get_timestamp(timestamp * tsp) { auto unsigned long tickcount1, tickcount2; auto unsigned int ctcvalue; auto unsigned char ctclow, ctchigh; asm pushf; asm cli; tickcount1 = * BIOS_TICK_COUNT_P; outportb(0x43, 0); /* Latch value */ ctclow = inportb(0x40); ctchigh = inportb(0x40); /* Read count in progress */ asm sti; /* Force interrupt ENABLE */ ctcvalue = - ((ctchigh << 8) + ctclow); tickcount2 = * BIOS_TICK_COUNT_P; asm popf; if ((tickcount2 != tickcount1) && (ctcvalue & 0x8000)) tsp->ticks = tickcount1; else tsp->ticks = tickcount2; tsp->part = ctcvalue; return; } void main(void) { auto timestamp ts, ts1, ts2; auto unsigned int sched; auto unsigned int ch; printf("Sample program #15 - Demonstrates absolute timestamping\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); printf("Press any key to get timestamp; press for continuous test\n\n"); set_mode2(); do { ch = bioskey(0); /* Get a keypress */ get_timestamp(&ts); /* Get timestamp */ printf("Absolute timestamp: 0x%04X%04X%04X units of 0.8381 us\n", (unsigned int) (ts.ticks >> 16), (unsigned int) (ts.ticks & 0xFFFF), ts.part); } while ((ch & 0xFF) != 27); printf("\nProgram is now performing continuous timestamp test\n\n"); printf("Press to exit\n\n"); while (1) { ts2.ticks = ts1.ticks; ts2.part = ts1.part; ts1.ticks = ts.ticks; ts1.part = ts.part; get_timestamp(&ts); printf("0x%04X%04X%04X\r", (unsigned int) (ts.ticks >> 16), (unsigned int) (ts.ticks & 0xFFFF), ts.part); if ((ts.ticks < ts1.ticks) || ((ts.ticks == ts1.ticks) && (ts.part < ts1.part))) { /* Went backwards? */ printf("Timestamp went backwards: 0x%04X%04X%04X, 0x%04X%04X%04X, then 0x%04X%04X%04X\n", (unsigned int) (ts2.ticks >> 16), (unsigned int) (ts2.ticks & 0xFFFF), ts2.part, (unsigned int) (ts1.ticks >> 16), (unsigned int) (ts1.ticks & 0xFFFF), ts1.part, (unsigned int) (ts.ticks >> 16), (unsigned int) (ts.ticks & 0xFFFF), ts.part); } ++sched; if (!(sched & 0xFF)) if (bioskey(1)) if ((bioskey(0) & 0xFF) == 27) break; } exit(0); } -------------------------------- snip snip snip -------------------------------- The interrupt flag is carefully controlled inside the get_timestamp() function. Interrupts must remain enabled during normal execution of the program, so that the tick interrupt can maintain the BIOS tick count variable which forms part of the timestamp value. The state of the interrupt flag on entry to get_timestamp() is not important, but the function will enable interrupts during its operation. This program can be modified to support mode 3 operation of CTC channel zero but this is not necessary as there are no disadvantages to operating the CTC in mode two. {JAM} says that this technique gives an accurate timestamp with a resolution of few microseconds. On the computer he used, an Epson 20MH 386/SX, "Reasonable clock code is accurate to about 4 microseconds with a minimum read time of about 20 microseconds. The clock accuracy does not change much between machines and is never under 1 microseconds or over 4". {JAM} also points out that timer reads take in the region of three to eight CTC clock periods and therefore you cannot just wait for a particular time value to occur, because you probably will not sample the count at exactly the right time. You have to check for _at least_ that length of time elapsed. Finally, because the absolute timestamp value ranges from 0x000000000000 to 0x001800AFFFFF then wraps around to midnight, subtracting two timestsamp values will not give a correct indication of elapsed time if the period measured crossed a midnight boundary. See section ¯¯ 9.3 for details. ## 9.2 SAMPLE PROGRAM: ABSOLUTE TIMESTAMP IN MODE TWO - ASSEMBLER This program implements the second section of the sample program from section ¯¯ 9.1 but is written in assembler and performs direct screen writes. The get_timestamp() function is much more carefully optimised. To get an idea of how often this program can read the timer, update the number in screen memory, and perform several other tasks, set your system time to 23:59:50 and run the program, and note how far apart the three reported numbers are. On my 486DX2-66, they are mostly between 16 and 32 CTC clocks, and the GetTimestamp function takes between 7 and 9 CTC clocks to read its timestamp. For maximum speed, this program uses the BIOS Ctrl-Break flag at 0000:0471 to allow the program to be terminated, so you must press Ctrl-Break to terminate the program. -------------------------------- snip snip snip -------------------------------- NAME SAMPLE16 ; Sample program #16 ; Demonstrates absolute timestamping using mode 2, in assembler ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; ; This program assembles into SAMPLE16.COM, a small program which sets CTC ; channel 0 to mode 2 and repeatedly reads an absolute timestamp using the ; BIOS tick count variable and the count in progress in CTC channel 2, and ; displays the 48-bit timestamp (37 bits of which are actually used) as a ; 12-digit hex number in the bottom left hand corner of the screen. It also ; checks for the timestamp going backwards, and if this occurs, displays a ; message giving the two timestamps prior to the timestamp going backwards, ; and the timestamp on which the error was detected. If midnight passes, ; this message should be displayed, as the timestamp is only an offset into ; the current day. ; ; This program assumes it is running in text mode. It supports colour and ; monochrome systems and 43-line and 50-line modes. ; ; Save this file to SAMPLE16.ASM and assemble with: ; masm sample16; ; link sample16; ; exe2bin sample16.exe sample16.com ; or ; tasm sample16; ; tlink /t sample16; ; ComFile SEGMENT ASSUME cs:ComFile,ds:ComFile,es:nothing,ss:nothing ORG 100h ; Com-type file Main PROC near jmp Main2 ; Skip Main ENDP InitialMsg DB 13,44 DUP(10) DB "Sample program #16 - Demonstrates absolute timestamping in mode 2",13,10 DB "Part of the PC Timing FAQ / Application notes",13,10 DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10 DB "Press Ctrl-Break to terminate program",13,10,13,10,"$" BackwardsMsg DB 13,"Timestamp went backwards: 0x" Backwards1 DB "xxxxxxxxxxxx, 0x" Backwards2 DB "xxxxxxxxxxxx, then 0x" Backwards3 DB "xxxxxxxxxxxx",13,10,13,10,"$" HexBuffer DB "xxxxxxxxxxxx" TimeL DW 0 ; Time loword (CTC count) TimeM DW 0 ; Time midword (loword of tick count) TimeH DW 0 ; Time hiword (hiword of tick count) Time1L DW 0 ; Old time loword Time1M DW 0 ; Old time midword Time1H DW 0 ; Old time rock 'n' roll Time2L DW 0 ; Old old time loword Time2M DW 0 ; Old old time midword Time2H DW 0 ; Old old time hiword RegenSeg DW 0B800h ; Regen buffer segment BotLine DW 0 ; Regen offset of bottom line Main2 PROC near cld xor ax,ax ; Zero mov es,ax cmp WORD PTR es:[463h],3D4h ; Check for colour mode je GotRegenSeg mov RegenSeg,0B000h GotRegenSeg: xchg ax,bx ; BX = 0 (page number) mov ah,3 int 10h ; Get cursor position - DH = line mov ah,0Fh int 10h ; Get video mode mov al,ah ; Get screen width mul dh ; Calculate offset of bottom line shl ax,1 ; Shift for char/attrib mov BotLine,ax ; Store mov dx,OFFSET InitialMsg mov ah,9 int 21h ; Display initial message call InitCTC0Mode2 ; Init CTC chan 0 to mode 2, reload 0 MainLoop: mov ax,Time1L ; Copy Time1 to Time2 mov Time2L,ax mov ax,Time1M mov Time2M,ax mov ax,Time1H mov Time2H,ax mov ax,TimeL ; Copy Time to Time1 mov Time1L,ax mov ax,TimeM mov Time1M,ax mov ax,TimeH mov Time1H,ax call GetTimestamp ; Get timestamp mov TimeL,ax ; Store it mov TimeM,dx mov TimeH,bx sub ax,Time1L ; Subtract lowords sbb dx,Time1M ; Subtract midwords with borrow sbb bx,Time1H ; Subtract hiwords with borrow jnb Alright ; If no borrow, time didn't go backwards mov si,OFFSET Time2L ; Oldest time mov di,OFFSET Backwards1 ; First string position call ToASCII ; Convert to ASCII mov si,OFFSET Time1L ; Second-oldest time mov di,OFFSET Backwards2 ; Second string position call ToASCII ; Convert to ASCII mov si,OFFSET TimeL ; New time mov di,OFFSET Backwards3 ; Third string position call ToASCII ; Convert to ASCII mov dx,OFFSET BackwardsMsg mov ah,9 int 21h ; Display went-backwards message Alright: mov si,OFFSET TimeL ; New time mov di,OFFSET HexBuffer ; Hex text buffer call ToASCII ; Convert to ASCII mov si,OFFSET HexBuffer ; ASCII hex text mov di,BotLine ; Offset of bottom line of screen mov es,RegenSeg ; Regen buffer segment mov cx,12 ; Characters to copy ScrLoop: movsb ; Copy character inc di ; Skip attribute loop ScrLoop ; Loop xor ax,ax mov es,ax xchg al,BYTE PTR es:[471h] test al,al js Finish jmp MainLoop Finish: mov ax,4C00h int 21h ; Terminate with errorlevel 0 int 20h ; In case DOS-1 (!) Main2 ENDP InitCTC0Mode2 PROC near ; Func: Initialise CTC channel 0 to operate in ; mode 2 with reload value of 0 (divisor ; of 65536, 18.2065 interrupts/second). ; Wait for a tick to occur before setting ; mode (should minimise disturbance to ; system time). ; In: None ; Out: None ; Lost: AX (preserves interrupt flag) pushf push ds sti ; Ensure interrupts are enabled xor ax,ax mov ds,ax ; Address low memory with DS mov ax,ds:[46Ch] ; Get loword of tick count WaitTick: cmp ax,ds:[46Ch] ; Changed? je WaitTick ; If not, loop pop ds mov al,00110100b ; Channel 0, mode 2 cli out 43h,al ; Set mode xor ax,ax ; Zero jmp SHORT $+2 ; Delay out 40h,al ; Loword of divisor jmp SHORT $+2 ; Delay out 40h,al ; Hiword of divisor popf ; Restore interrupt flag ret InitCTC0Mode2 ENDP PROC GetTimestamp near ; Func: Return absolute timestamp (48-bit) in ; units of 0.83809534452us since midnight ; in the current day (range 000000000000h ; to 001800AFFFFFh) using BIOS tick count ; variable and CTC channel zero count in ; progress, assuming CTC channel 0 is ; operating in mode 2 with a reload value ; of 0 (65536 divisor). ; In: None ; Out: AX = Count loword (b0..15) (0000-FFFF) ; DX = Count midword (b16..31) (0000-FFFF) ; BX = Count hiword (b32..47) (0000-0018) ; Lost: AX BX DX ; Note: This routine briefly disables then ; enables then disables interrupts ; regardless of the state of the ; interrupt flag on entry. ; It restores the original interrupt ; flag state on exit. push ds ; Preserve register push di push si pushf ; Preserve interrupt flag xor ax,ax ; Zero mov ds,ax ; Address low memory with DS ASSUME ds:nothing ; Not addressing ComFile any more cli mov si,ds:[46Ch] ; Loword of tick count mov di,ds:[46Eh] ; Hiword of tick count mov al,00000000b ; Latch count for CTC channel 0 out 43h,al ; Send it jmp SHORT $+2 ; Delay in al,40h ; Get lobyte of count mov ah,al ; Save in AH jmp SHORT $+2 ; Delay in al,40h ; Get hibyte of count sti ; Make sure interrupts are enabled now xchg al,ah ; Get bytes the right way round nop ; Sniff for interrupt neg ax ; Convert to ascending count cli ; No interrupts again for reading count mov dx,ds:[46Ch] ; Loword of tick count again mov bx,ds:[46Eh] ; Hiword of tick count again popf ; Restore original interrupt flag cmp dx,si ; Did tick count change? je GotTimestamp ; If not, just return second tick count test ax,ax ; Is tick count low or high? jns GotTimestamp ; If low, read was just past interrupt mov dx,si ; If high, previous tick count is right mov bx,di ; Get hiword of tick count too GotTimestamp: pop si ; Restore working registers pop di pop ds ; Restore DS ASSUME ds:ComFile ; Back to ComFile ret GetTimestamp ENDP ToASCII PROC near ; Func: Convert a three-word time structure to ; 12-digit printable hex representation ; In: SI -> Structure ; DI -> ASCII buffer in this segment ; Out: DI -> Past characters stored ; Lost: AX DI ES push cs pop es ; ES to ComFile mov ax,ds:[si+4] ; Get hiword call Mach16ToHexAsc ; Convert to hex ASCII representation mov ax,ds:[si+2] ; Get hiword call Mach16ToHexAsc ; Convert to hex ASCII representation mov ax,ds:[si+0] ; Get hiword Mach16ToHexAsc PROC near push ax mov al,ah call Mach8ToHexAsc pop ax Mach8ToHexAsc PROC near push ax shr al,1 shr al,1 shr al,1 shr al,1 call Mach4ToHexAsc pop ax and al,0Fh Mach4ToHexAsc PROC near add al,90h daa adc al,40h daa stosb ret Mach4ToHexAsc ENDP Mach8ToHexAsc ENDP Mach16ToHexAsc ENDP ToASCII ENDP ComFile ENDS END Main -------------------------------- snip snip snip -------------------------------- See all the comments in section ¯¯ 9.1 relating to the C program; these comments also apply to this program. ## 9.3 HANDLING THE MIDNIGHT BOUNDARY The absolute timestamp value returned by the functions in the above programs will be in the range 0x000000000000 to 0x001800AFFFFF inclusive. Calculating the time difference between two of these timestamps by subtracting the first from the second will only give a correct result if the time period did not span a midnight boundary. To handle this case, you must check that the second timestamp is greater than the first, and if not, add 0x001800B00000 to the second timestamp before subtracting them. This will give a correct result, provided that no more than about 24 hours has elapsed between the two timestamps being taken! (The timestamp value does not include a date). -------------------------------- snip snip snip -------------------------------- typedef struct { /* As defined in the sample program */ unsigned int part; unsigned long ticks; } timestamp; /* The following function takes two timestamps in startts and stopts, and calculates the time difference and stores them in diffts. The difference is in units of 0.8381 us, the same units as the timestamp values. */ void calc_elapsed(timestamp * startts, timestamp * stopts, timestamp * diffts) { if (startts->ticks <= stopts->ticks) /* No change of day */ diffts->ticks = stopts->ticks - startts->ticks; else /* Change of day */ diffts->ticks = stopts->ticks + 0x001800B0L - startts->ticks; diffts->part = stopts->part - startts->part; if (stopts->part < startts->part) --(diffts->ticks); return; } -------------------------------- snip snip snip -------------------------------- ## 10 OTHER TOPICS ## 10.1 THE 586 TIME STAMP COUNTER In a message in comp.sys.intel and comp.lang.asm.x86 in mid-December 1994, Gordon Burditt (gordon@sneaky.lonestar.org) describes a partly undocumented instruction available on the Intel 586 (but not guaranteed to be available on future Intel processors). The instruction is RDTSC - Read Time Stamp Counter. Opcode encoding is 0F 31. It is "unprivileged if bit 2 of CR4 is clear, Ring 0 or real mode only if it is set" (whatever that means :-). This instruction loads the 64-bit Time Stamp Counter register contents into EDX:EAX. The Time Stamp Counter is zeroed on power-up and is incremented on each CPU clock cycle (e.g. 90 times per microsecond for a 90 MHz CPU - for clock doubled or clock tripled processors, does this mean the external clock or the internal clock? (*)). This level of resolution is useful for performance measurement and CPU usage billing. The unit of time is system-dependent, and also depends on the accuracy of the processor clock, which may not be very good. Use the CPUID instruction to determine if RDTSC exists on this CPU. EDX "feature bits" bit 4 is set if it does. The Time Stamp Counter register can be written via the documented 586 instruction WRMSR - Write Model-Specific Register, coding 0F 30. The privilege level for this instruction is ring 0 or real mode only. Set ECX to the register number (10 hex for the TSC register) and EDX:EAX to the new value and execute the instruction. Use CPUID to determine if WRMSR exists on this CPU. EDX "feature bits" bit 5 is set if it does. Also, if you are running DOS with EMM386 (i.e. V86 mode), you cannot use the privileged instructions. Thank you Gordon for this information. Quoting from an article dated Apr 27 1995 in comp.lang.asm.x86 by Philip O'Carroll (poc@maths.tcd.ie) with his permission: >> I can't execute the RDTSC instruction... Is there someone who knows why? > > 1) The RDTSC instruction cannot be executed from V86 mode. It gives a > GPF. I do not know why this is and I have only tested RDTSC from > within protected mode. > > 2) If you are executing it from 16-bit code you will need to use the ADRSIZE > prefix to access the upper 16 bits of the EAX and EDX registers. > > 3) It is possible that the instruction has been disabled by setting the > TSD (timestamp disable) bit in CR4. This is unlikely because the Pentium > powers up with it clear and I cannot see why an OS would disable it. Terje Mathisen (Terje.Mathisen@hda.hydro.com) adds, in an article in April 1995 in comp.lang.asm.x86: > RDTSC is by default available for all rings/modes, except V86. > > The V86 fault was an Intel internal error, i.e. it wasn't supposed to > be like that. The RDTSC instruction causes a GPF if executed in V86 mode. Terje says that though this is the documented behaviour, according to an Intel technician the RDTSC instruction should have worked in V86 mode too. Terje says that the Intel technician also said at the time that RDTSC would work in V86 mode on the P6. > The Time Stamp Disable (TSD) bit in CR4 must be changed (set) to restrict > RDTSC to ring 0, so (almost?) all operating systems will let you use the > time stamps from ring 3 code. Philip also sent me the following macro for VC++ 1.5 16-bit (protected mode Ring 3 code): > #define TIMESTAMP(var) __asm \ > { > _asm emit 0x0F \ > _asm emit 0x31 \ > _asm emit 0x66 \ > _asm mov word ptr var, ax \ > _asm emit 0x66 \ > _asm mov word ptr var[+4], dx \ > } > > Usage: > > DWORD timest[2]; > > TIMESTAMP(timestamp); Philip also told me he has written a Windows VxD for accessing the profiling counters from Ring 3 code, but I don't know where, or when, it will be available. > My VxD allows Windows apps to access the Pentium profiling registers > detailed in Byte July 1994. Specifically there are two counters which > can count various different processor events such as instructions > executed, data cache hits/misses etc. > > The TSC _can_ be used by Windows apps without recourse to a VxD. Thanks guys. ## 10.2 SERIAL PORT REGULAR INTERRUPT If your application will have a spare serial port to play with, it can generate a regular interrupt using the Transmit interrupt facility on the serial chip (known as a UART, for Universal Asynchronous Receiver/Transmitter). There are other ways to make the UART generate interrupts, but the Transmit interrupt is easiest to use. UARTs usually drive IRQ4 and IRQ3. These interrupts are reserved for COM1 and COM2 respectively. When COM3 and COM4 are present, they sometimes 'share' IRQ4 and IRQ3 respectively, with COM1 and COM2, but this 'sharing' only works if the ports are not used simultaneously (except on MicroChannel machines and possibly on EISA machines, where proper interrupt sharing is possible with the right software support). In some cases, the otherwise spare interrupt lines, such as IRQ5 and IRQ2/9, are used for COM3 and COM4. ## 10.2.1 SERIAL PORT (UART) DOCUMENTATION This information is brief and incomplete. There are many books and electronic documents which describe the UART much more thoroughly, such as Chris Blum's "The Serial Port" FAQ which is posted periodically in the Internet newsgroup comp.os.msdos.programmer. There are several types of UARTs. The basic device is the INS8250 which was originally developed by National Semiconductor. It is not an Intel device, despite the number. Descendants such as the 8250A, 16C450, and 16C550 add features, improve performance, and/or correct design errors in previous versions of the chip. The UART occupies eight consecutive I/O addresses starting at the I/O Base address. The I/O Base address of a nominated UART (e.g. COM1) can be found in the table in the BIOS data area in low memory, starting at 0040:0000 (aka 0000:0400). The table has four entries, at 0, 2, 4, and 6, which correspond to COM1, COM2, COM3, and COM4 respectively. If the value is zero, there is no such port. If nonzero, it specifies the I/O Base address of that port. The registers in the UART are as follows. I/O address Access Name Description ----------- ------ ---- ----------- IOBase+0 Read RDR Received data (DLAB=0) Write TDR Transmit data (write) (DLAB=0) Read/write BRDL Divisor register lobyte (DLAB=1) IOBase+1 Read/write IER Interrupt Enable Register (DLAB=0) Read/write BRDH Divisor register hibyte (DLAB=1) IOBase+2 Read-only IIR Interrupt Identification Register Write-only FCR FIFO control register (FIFO UARTs only) IOBase+3 Read/write LCR Line Control Register IOBase+4 Read/write MCR Modem Control Register IOBase+5 Read-only LSR Line Status Register IOBase+6 Read-only MSR Modem Status Register IOBase+7 Read/write Scratch register (on some UARTs only) The 'DLAB' above is the Divisor Latch Access Bit, which is bit 7 of the Line Control Register (LCR) at IOBase+3. This bit controls access to the divisor register (hence the name). The divisor register is a 16-bit register which acts as a divisor to determine the baud rate. It is accessible at IOBase+0 (lobyte) and IOBase+1 (hibyte) when the DLAB is set. When the DLAB is clear, the transmit and receive data register and the IIR appear at these I/O locations. The relevant registers are now described briefly. IER 7 6 5 4 3 2 1 0 IOBase+1, read/write * * . * . . . . Not used; zero . . * . . . . . Special function enable (some UARTs) . . . . * . . . Modem Status Change Interrupt Enable (1=enable) . . . . . * . . Line Status Change Interrupt Enable (1=enable) . . . . . . * . Transmit Ready Interrupt Enable (1=enable) . . . . . . . * Received Data Interrupt Enable (1=enable) IIR 7 6 5 4 3 2 1 0 IOBase+2, read-only * * . . . . . . FIFOs Enabled flags (FIFO UARTs only) . . * * . . . . Special function status (some UARTs) . . . . * * * . Interrupt Identification bits 2, 1, and 0 . . . . . . . * Interrupt output active (0=active, 1=inactive) LCR 7 6 5 4 3 2 1 0 IOBase+3, read/write * . . . . . . . Divisor Latch Access Bit (DLAB) . * . . . . . . Set Break (1=break, 0=normal) . . * . . . . . Stick Parity (1=stick, 0=normal parity, if enabled) . . . * . . . . Even Parity (1=even, 0=odd, if enabled) . . . . * . . . Parity Enable (1=enable, 0=disable) . . . . . * . . Stop bits (1=1.5/2, 0=1 stop bits) . . . . . . * * Word length (00=5, 01=6, 10=7, 11=8 data bits) LSR 7 6 5 4 3 2 1 0 IOBase+5, read-only * . . . . . . . Not used; 0 . * . . . . . . TSRE - Transmit Shift Register Empty . . * . . . . . THRE - Transmit Holding Register Empty . . . * . . . . BI - Break interrupt (break received) . . . . * . . . FE - Framing Error . . . . . * . . PE - Parity Error . . . . . . * . OR - Overrun error . . . . . . . * DR - Data Ready (received a data byte) MCR 7 6 5 4 3 2 1 0 IOBase+4, read/write * . . . . . . . Special function enable (some UARTs) . * * . . . . . Not used; zero . . . * . . . . Loopback enable (1=enable) . . . . * . . . OUT2 (interrupt buffer control) (1=active) . . . . . * . . OUT1 (1=active) . . . . . . * . RTS - Request To Send (1=active) . . . . . . . * DTR - Data Terminal Ready (1=active) MSR 7 6 5 4 3 2 1 0 IOBase+6, read-only * . . . . . . . DCD - Data Carrier Detect (0=inactive, 1=active) . * . . . . . . RI - Ring Indicator (0=inactive, 1=active) . . * . . . . . DSR - Data Set Ready (0=inactive, 1=active) . . . * . . . . CTS - Clear To Send (0=inactive, 1=active) . . . . * . . . DDCD - Delta DCD (0=no change, 1=changed) . . . . . * . . TERI - Trailing Edge Ring Indicator (1=edge) . . . . . . * . DDSR - Delta DSR (0=no change, 1=changed) . . . . . . . * DCTS - Delta CTS (0=no change, 1=changed) Bit 2 of the LCR controls the number of stop bits. If this bit is 0, one stop bit is used. If this bit is 1, two stop bits are used, except when the word length bits are both zero (i.e. 5-bit word length), in which case 1.5 stop bits are used. Bit 3 of the MCR (OUT2) controls the tristate buffer that drives the interrupt line. When the port is in use, and the interrupt facility is required, this bit must be set, to enable the buffer to drive the IRQ line on the slot bus. The baud rate divisor is chosen as 115200 divided by the baud rate. For example if a baud rate of 9600 bits per second is required, the divisor value is 115200/9600, which is 12. Both lobyte and hibyte must be programmed. The DLAB must be set prior to writing the divisor, and turned off afterwards. For a ten-bit character length (e.g. 8-bit data with no parity, or 7-bit data with parity), the transmitter will generate a transmit ready interrupt ten times slower than the bit rate, e.g. 960 times per second in the above example. The serial port interrupt must be enabled on the PIC for interrupt driven operation (see section ¯¯ 6.10 for details). There are four independently controllable interrupt sources in the UART. They correspond to bits 3-0 of the IER. When handling interrupts from the UART when more than one interrupt source is enabled in the IER, particularly if the modem status change interrupt is enabled, your software must take care to ensure that all interrupt sources are acknowledged before sending the EOI command to the PIC. This condition can be detected by checking bit 0 of the Interrupt Identification Register (IIR) - if this bit is zero, then an unacknowledged interrupt source is still pending. This will be one of the interrupt sources that are enabled via the IER. This condition must be cleared before the EOI is sent. The Received Data interrupt is cleared when the character is read from the Received Data register. The Transmit Ready interrupt is cleared when any character is written to the Transmit Data register. The Line Status Change and Modem Status Change interrupts are cleared by a read of the LSR and the MSR, respectively. The program in section ¯¯ 10.2.2 demonstrates how to use a serial port as a regular interrupt source. ## 10.2.2 SAMPLE PROGRAM: REGULAR INTERRUPT USING THE SERIAL PORT This program uses a serial port (COM1 in this case) to generate a regular (periodic) interrupt. The UART divisor is set to 96, giving a baud rate of 1200 bps. At ten bits per character, the UART will transmit a character 120 times per second, and generate the Transmit Ready interrupt at the same rate. This program has the IRQ and interrupt numbers, and the serial port's I/O Base address, hard-coded via #defines. These could be set by command line options and/or determined via the table of addresses at 0000:0400 described earlier. Note that while this program is running, it will be transmitting characters out the serial port at 1200 baud. If a serial printer, or any other device, is connected to the serial port, you might want to remove it before running this program! See section ¯¯ 10.2.3 for a method of incorporating this timing technique into a program that is already using the serial port, to implement delays in a transmitted data stream. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #17 Demonstrates regular interrupts using the serial port Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save and assemble the critical error module CRIT_ERR Save this sample code to SAMPLE17.C Compile this module with: bcc -c -I -ms sample17.c Link the modules with: tlink /c /x \c0s.obj sample17.obj crit_err.obj, sample17, nul, \cs Where inc_path is the path to your C header files, c0_path is the path to your startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, and cli */ #include /* Needed for bioskey() */ #include /* Needed for MK_FP */ #include /* Needed for _write() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define STDERR 2 /* DOS handle for standard error */ #define BAUDDIV 96 /* Interrupt rate = 11520 / BAUDDIV */ #define IOBASE 0x3F8 /* COM1 standard I/O base address */ #define COMIRQ 4 /* IRQ number for COM1 (standard) */ #define COMINT 0x0C /* Corresponding interrupt number */ #define PICMASK (1 << COMIRQ) /* Bitmask for interrupt in PIC IMR */ void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */ unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */ typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */ intfuncp old_com_int = (intfuncp)0xFFFFFFFFL; static unsigned int onetwentieths = 0; /* 120ths of seconds */ static unsigned int seconds = 0; /* Seconds */ static unsigned char old_brdl, old_brdh; /* Old baud rate divisor */ static unsigned char old_lcr, old_mcr, old_ier; /* Old LCR, MCR, IER contents */ /* The interrupt handler is invoked when the UART is transmit ready. It must feed the UART to shut it up. When the UART is hungry again, it will issue another interrupt. This handler increments a counter variable. */ void interrupt com_int_handler(void) { outportb(IOBASE, 0x00); /* "Feeeed me Seymour" */ if (++onetwentieths >= 120) { /* Increment 120ths count */ onetwentieths = 0; ++seconds; } outportb(0x20, 0x20); /* Send EOI */ return; /* From interrupt */ } void restore_normal(void) { asm pushf; asm cli; outportb(0x21, inportb(0x21) | PICMASK); /* Disable int in PIC */ outportb(IOBASE + 3, old_lcr & 0x7F); /* Clear DLAB */ outportb(IOBASE + 1, old_ier); /* Restore IER */ outportb(IOBASE + 3, old_lcr | 0x80); /* Set DLAB */ outportb(IOBASE + 0, old_brdl); /* Lobyte of divisor */ outportb(IOBASE + 1, old_brdh); /* Hibyte of divisor */ outportb(IOBASE + 3, old_lcr); /* Restore LCR */ outportb(IOBASE + 4, old_mcr); /* Restore MCR */ asm popf; return; } void abort_cleanup(int dos_is_safe) { if (dos_is_safe) { if (old_com_int != (intfuncp)0xFFFFFFFFL) { setvect(COMINT, old_com_int); old_com_int = (void far *)0xFFFFFFFFL; } } else { if (old_com_int != (intfuncp)0xFFFFFFFFL) { *((intfuncp far *)MK_FP(0, COMINT << 2)) = old_com_int; old_com_int = (void far *)0xFFFFFFFFL; } } restore_normal(); return; } void interrupt ctrl_c_handler(void) { static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n"; if (is_at_crit_prompt()) abort_cleanup(FALSE); else { abort_cleanup(TRUE); _write(STDERR, &message, sizeof(message)); } exit(255); } void poll_exit(void) { if (bioskey(1)) { if ((bioskey(0) & 0xFF) == 27) { setvect(COMINT, old_com_int); old_com_int = (void far *)0xFFFFFFFFL; restore_normal(); exit(0); } } return; } void main(void) { unsigned int main_onetwentieths, main_seconds, old_onetwentieths; printf("Sample program #17 - Demonstrates regular interrupts using the serial port\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); printf("Press to exit\n\n"); crit_err_intercept(); /* Trap critical errors */ setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */ old_com_int = getvect(COMINT); setvect(COMINT, com_int_handler); asm cli; old_lcr = inportb(IOBASE + 3); /* Get old LCR value */ old_mcr = inportb(IOBASE + 4); /* Get old MCR value */ outportb(IOBASE + 3, 0x83); /* Set DLAB */ old_brdl = inportb(IOBASE + 0); /* Get divisor lobyte */ old_brdh = inportb(IOBASE + 1); /* Get divisor hibyte */ outportb(IOBASE + 0, BAUDDIV & 0xFF); /* Set up divisor lobyte */ outportb(IOBASE + 1, BAUDDIV >> 8); /* Set up divisor hibyte */ outportb(IOBASE + 3, 0x03); /* Clear DLAB */ old_ier = inportb(IOBASE + 1); /* Get old IER value */ outportb(IOBASE + 4, 0x08); /* Enable interrupt buffer */ /* Use 0x18 instead of 0x08 above to set loopback mode so data is not transmitted out the serial port connector */ outportb(IOBASE + 1, 0x00); /* No interrupts yet */ outportb(0x21, inportb(0x21) & (~PICMASK)); /* Enable int in PIC */ outportb(IOBASE + 1, 0x02); /* Enable Tx interrupt */ asm sti; printf("Seconds 120ths\n"); while (1) { asm cli; main_onetwentieths = onetwentieths; main_seconds = seconds; asm sti; if (main_onetwentieths != old_onetwentieths) { printf("%5d %3d\r", main_seconds, main_onetwentieths); old_onetwentieths = main_onetwentieths; } poll_exit(); } } -------------------------------- snip snip snip -------------------------------- ## 10.2.3 INSERTING DELAYS INTO SERIAL PORT TRANSMITTED DATA The information and code given in this section is untested. In controller applications, it is sometimes necessary to insert delays into a serial transmission. This may be required as part of a communication protocol, or for other reasons. For example, certain types of modems used in medium speed data communication cannot accept data to be transmitted immediately when the transmit enable flow control line is driven active by the computer, so the computer must raise flow control and delay for a certain time (usually in the order of 5-20 milliseconds) before starting to transmit data. These delays can be created using the transmit interrupt method, using the same serial port which transmits the data, via the loopback enable bit, bit 4 of the MCR (see section ¯¯ 10.2.1). Setting this bit forces the UART's data output in the idle (marking) state, and loops its transmitted data back to its receiver, internally to the UART chip. In this state, the transmit ready interrupt can be used to time the delay period as per the sample program in section ¯¯ 10.2.2. When the required number of interrupts have occurred, i.e. the required delay time has elapsed, wait for the last byte to be serialised (by waiting for TSRE in the LSR to go true) and then turn off loopback mode. Your program can then begin transmitting. This method cannot be used if your program must be able to receive characters during the delay period, because in loopback mode, the UART ignores the receive data signal, but this method can be used in half duplex applications. Also, the granularity of the delay is one character length. Reprogramming the baud rate during the delay period might allow finer delay timing, but this would be very technical, if not impossible, to implement correctly. If your program transmits under interrupt, I would suggest using some flags to communicate between the mainline and the interrupt handler. For example, the mainline could signal the start of a transmission by enabling outgoing flow control, selecting loopback mode, sending one or two characters to the UART, setting an 'idle-leader' flag to be used by the interrupt routine, and enabling transmit interrupts. The interrupt routine would check the interrupt source (if more than one source is enabled on the UART) and if the interrupt is due to transmit ready, first check whether the idle-leader flag is set, and if so, send any character to the port (e.g. 0FF hex), decrement the idle leader counter, and return. If the idle leader timer counts down to zero, either the mainline or the interrupt routine would have to wait for TSRE to go active, turn off loopback mode, and start transmitting data. If your program is doing nothing while it waits during the transmit idle period, and does not otherwise transmit under interrupt, you can use the transmit ready interrupt signal without actual interrupts, in a polled fashion. Fast response to a transmit ready signal is not necessary, as there is a window of about two character lengths between when the THRE (Transmit Holding Register Empty) signal goes true, and when the transmit data register must be filled, due to the double buffering provided by the transmit holding register and transmit shift register. Here is a crude, untested function to transmit a string of data bytes without using interrupts. It asserts outgoing flow control (DTR and RTS), and waits for a number of character-periods determined by the leader_len parameter, then transmits the message pointed to by the msg parameter for the number of bytes specified by the msg_length parameter, waits for the last character to be fully serialised plus nearly one character length, and drops the RTS line. A similar method can be used with an interrupt handler, with quite a lot of extra mucking around. void wait_tx_string(unsigned leader_len, char * msg, unsigned msg_length) { while ((inportb(IOBASE+5) & 0x60) != 0x60) ; /* Wait for last char to be serialised */ asm pushf; asm cli; outportb(IOBASE+4, inportb(IOBASE+4) | 0x13); /* DTR, RTS, loopback */ asm popf; while (leader_len--) { outportb(IOBASE, 0xFF); /* Dummy byte */ while ((inportb(IOBASE+5) & 0x20) == 0) ; /* Wait for THRE again */ } while ((inportb(IOBASE+5) & 0x40) == 0) ; /* Wait for TSRE */ asm pushf; asm cli; outportb(IOBASE+4, inportb(IOBASE+4) & 0xEF); /* Loopback off */ asm popf; while (msg_length--) { outportb(IOBASE, *(msg++)); while ((inportb(IOBASE+5) & 0x20) == 0) ; /* Wait for THRE between chars */ } while ((inportb(IOBASE+5) & 0x40) == 0) ; /* Wait for last char sent */ asm pushf; asm cli; outportb(IOBASE+4, inportb(IOBASE+4) | 0x10); /* Loopback back on */ asm popf; outportb(IOBASE, 0xFF); /* Dummy byte */ while ((inportb(IOBASE+5) & 0x60) != 0x60) ; /* Wait for dummy char to be serialised */ asm pushf; asm cli; outportb(IOBASE+4, inportb(IOBASE+4) & 0xED); /* RTS, loopback off */ asm popf; return; } This is only an outline of this technique. If you are implementing this system, I would strongly recommend a thorough read of a technical document on the serial port, such as Chris Blum's article (see section ¯¯ 10.2.1) or manufacturers' data sheets for the serial chips, so you can determine all the implications of your code's actions. This is particularly important if timing is very critical, as there are timing subtleties and interactions between the transmit holding register and the transmit shift register that must be taken into account. There is also a problem caused by the fact that a transmit ready interrupt is acknowledged by a read of the LSR. This has serious implications relating to when the LSR may be interrogated. If the mainline accesses the LSR, it may clear a pending interrupt condition, causing transmit interrupts to cease. I have not investigated this properly, but be warned! (*) ## 10.3 EXTERNAL INTERRUPT SOURCES An external interrupt source can be used for many things, including timekeeping. External hardware of some sort will normally be required to drive the interrupt in the desired way. Usually the external interrupt source will use the parallel port or the serial port to get access to an interrupt level (IRQ) on the slot bus. The parallel or serial port input can be driven by an external source at the desired rate. If only a slow interrupt rate is required, you can clock the input at 300 Hz, which can be derived using a PLL (Phase Locked Loop) from the mains frequency. 300 Hz is a good choice because it can be generated from both 50Hz (Europe) and 60Hz (America) mains frequencies. Thanks to John Stockton for suggesting this technique (though he points out that he has not tested it). You may have noticed how you never have to adjust clocks that are mains powered (except after power loss, of course). This is because the mains frequency is usually regulated very carefully by power supply authorities and, though it may vary slightly in the short term, its long term accuracy should be very high. A frequency derived from the mains in this way could be a good clock source for timing applications which require high long-term accuracy. ## 10.3.1 EXTERNAL INTERRUPT THROUGH PARALLEL PORT The parallel port interrupt is normally connected to IRQ7 although some cards are jumper-selectable to IRQ5 and maybe other IRQs. The parallel port interrupt was intended to be used in the normal course of sending data to a parallel printer, but DOS and BIOS do not use the interrupt facility. Versions of OS/2 prior to Warp (3.0) did require the interrupt for printing, but from Warp onwards the interrupt is not required (though it can be used if the /IRQ switch is provided on the line in CONFIG.SYS, i.e. BASEDEV=PRINT0x.SYS /IRQ). The basic parallel port consists of three registers at consecutive I/O locations starting at the I/O Base address. The I/O Base address of a nominated LPT port (e.g. LPT1) can be found in the table in the BIOS data area in low memory, starting at 0040:0008 (aka 0000:0408). The table has three entries, at 8, 0A, and 0C, which correspond to LPT1, LPT2, and LPT3. If the value is zero, there is no such port. Some BIOSes may support a fourth port base entry at 0E, but other BIOSes use this location for an unrelated function. The register at IOBase+2 is the Control register. Bit 4 of this register controls the tristate buffer that drives the IRQ line, and the buffer is enabled if the bit is set. In this state, a falling edge (high to low transition) on the Ack signal (pin 10 of the 25-pin connector) will cause an interrupt (providing that the interrupt is enabled in the PIC's IMR; see section ¯¯ 6.10). ## 10.3.2 EXTERNAL INTERRUPT THROUGH SERIAL PORT In addition to the Transmit Ready interrupt (which can provide a regular interrupt source, see section ¯¯ 10.2 and subsections), the serial port can issue an interrupt when received data is available, when the receiver line status changes, and/or when the receiver 'modem status' changes. The 'modem status' refers to the four incoming flow control lines on the serial connector which indicate the modem status when the port is connected to a modem. These inputs are as follows. Name Full name Pin (9-pin) Pin (25-pin) CTS Clear To Send 8 5 DSR Data Set Ready 6 6 RI Ring Indicator 9 22 DCD Data Carrier Detect 1 8 The modem status change interrupt is enabled by bit 3 of the Interrupt Enable Register (IER) (see section ¯¯ 10.2.1 for details). When this interrupt is enabled, and the interrupt buffer is enabled via the OUT2 line in the Modem Control Register (MCR) (also see section ¯¯ 10.2.1) and the appropriate IRQ is enabled via the IMR in the PIC (see section ¯¯ 6.10), every transition on any of these four incoming lines will cause an interrupt request. The current states of the four incoming lines can be read on the Modem Status Register (see section ¯¯ 10.2.1) which also contains the 'delta' signals, which indicate whether the corresponding line has changed state since the last time the MSR was read. When using these signals, remember that they clear when your program reads the MSR, so read the MSR once only, and test the delta bits in this value - don't re-read the MSR to check for any other delta bits, as they will all have cleared just after the MSR was read the first time. See the notes in section ¯¯ 10.2.1 about ensuring that all interrupt sources are acknowledged before leaving the interrupt routine. ## 10.3.3 EXTERNAL INTERRUPT THROUGH SOUND CARD Sound cards such as the Sound Blaster can most probably generate periodic interrupts, though these are usually used for some purpose related to sound generation, not for timing in the general sense. I haven't investigated this one. Get a technical reference such as the Sound Blaster Freedom project if you want to try this. ## 10.3.4 EXTERNAL INTERRUPT THROUGH CUSTOM I/O CARD There are many third party I/O cards that are able to generate periodic interrupts for various purposes, and for one-off dedicated applications or for experimenting, you may wish to use these. I have no references, but you could try looking through advertisements in computer experimenters' magazines for sources. Alternatively, if you have the time, money, experience, and inclination, you can make your own I/O card. Interrupt lines on the ISA bus are all rising edge triggered. Just generate the rising edge, and if there is no other card driving that line, and the interrupt is enabled in the mask register of the appropriate PIC, the appropriate interrupt will be invoked. On ISA cards it seems to be standard practice to drive IRQ lines with a buffer that can be put into high impedance mode (tri-stated) or driving mode, under software control. While this is doesn't allow for interrupt sharing, or have any other great purpose, it is in general not a bad idea. The EIDE, MCA, and PCI busses will be different. Get a good technical book if you intend to try this. ## 10.4 THE JOYSTICK PORT The joystick port, or game port, is accessed via a single I/O location, which is normally at I/O address 201h (may be jumper-settable to 301h on some cards). The joystick standard joystick hardware interface circuit is given in Figure 4 in the FIGURES archive. It supports four pushbutton-type inputs without hardware debouncing, and four variable resistors (potentiometers, abbreviated 'pot') for position sensing, to support two joysticks, each with two buttons and two pots (for the X and Y axes). Some cards support only the first joystick (see later). ## 10.4.1 JOYSTICK PORT HARDWARE The joystick hardware cannot generate an interrupt, and has no outputs, though it does provide a +5V supply which can be used externally, which the parallel port does not have. It is really only useful as a general purpose input port. The pots are read using four independent monostable or 'one-shot' circuits. The monostable circuits are triggered by a signal from the processor, and each one charges or discharges a capacitor at a rate determined by the resistance of the associated pot. When triggered, the monostable's output goes high. When the capacitor reaches a certain voltage, the output returns low, and remains low until the monostable is next triggered by the processor. Thus the name, 'one-shot'. The processor triggers the monostable, then measures the length of time taken for the monostable's output to go low, to determine the resistance, and thus the position, of the pot. The formula relating resistance to time is supposedly: T = 24.2 + (0.011 x R) where T is the time in microseconds and R is the resistance of the pot in ohms, but the capacitors are usually inaccurate (+/- 20% or worse) ceramic components, and are influenced by temperature, so the above formula is 'nominal' only. In practice the relationship will vary from one input to the next, and depend on temperature. The nominal pot end-to-end resistance is 100 kilohms (100000 ohms), giving a nominal maximum timeout of about 1125 us. Times in this range can be measured accurately using CTC channel zero or two in either mode 3 or mode 2, or using Refresh Detect. A sample program to read the joystick position is given in section ¯¯ 10.4.2. The joystick connector is a 15-pin female D-sub connector. The pinout is: Pin Dir Type Stick Button Axis Return to 1 Out +5V 2 In Btn A 1 Gnd 3 In Pot A X +5V 4 - Gnd 5 - Gnd 6 In Pot A Y +5V 7 In Btn A 2 Gnd 8 Out +5V 9 Out +5V 10 In Btn B 1 Gnd 11 In Pot B X +5V 12 - Gnd 13 In Pot B Y +5V 14 In Btn B 2 Gnd 15 Out +5V Writing any value to the I/O port (201h or 301h) causes all four monostables to start timing. Their outputs go high immediately, and go low a certain length of time later, depending on the resistance of the associated potentiometer. Reading the I/O port yields the following: 7 6 5 4 3 2 1 0 * . . . . . . . Button B2 (pin 14), 0=closed, 1=open (default) . * . . . . . . Button B1 (pin 10), 0=closed, 1=open (default) . . * . . . . . Button A2 (pin 7), 0=closed, 1=open (default) . . . * . . . . Button A1 (pin 2), 0=closed, 1=open (default) . . . . * . . . Monostable BY (from pin 13), 1=timing, 0=timed-out . . . . . * . . Monostable BX (from pin 11), 1=timing, 0=timed-out . . . . . . * . Monostable AY (from pin 6), 1=timing, 0=timed-out . . . . . . . * Monostable AX (from pin 3), 1=timing, 0=timed-out Some cards only support one joystick. You may be able to tell by looking for a 14-pin chip with '556' in its part number (single joystick), or a 16-pin chip with '558' in its part number (two joysticks), usually located near the 15-pin connector. Some cards implement the joystick interface in an ASIC, in which case you may be able to follow tracks to find how many joysticks are supported. ## 10.4.2 READING THE JOYSTICK BUTTONS AND POSITION Most BIOSes apart from very early ones provide functions to read the buttons and positions of the joystick, accessed via int 15h. If the function is not supported, carry is set on return and AH may be set to 80h or 86h. Steve McGowan and Mark Feldman in their PC-GPE article say that many machines do not support the BIOS functions properly, and that the first function (read buttons) may be supported, while the second function (read positions) may not. Read Joystick Buttons : int 15h Call with: AH = 84 hex DX = 0000 hex Returns: AL = Button states in bits 7-4, as read from input port Bits 7-4 are valid in the returned value, and they default to '1' and are '0' if the corresponding button is currently depressed. This function does not perform any debouncing on the joystick button inputs. This means that the bit may 'bounce' (i.e. alternate randomly, one or more times) at the instant that it makes or breaks contact, because of the mechanical nature of the switch. Read Joystick Positions : int 15h Call with: AH = 84 hex DX = 0001 hex Returns: AX = Joystick A, axis X (0-511, 0 if timed-out) BX = Joystick A, axis Y (0-511, 0 if timed-out) CX = Joystick B, axis X (0-511, 0 if timed-out) DX = Joystick B, axis Y (0-511, 0 if timed-out) This function reads each of the four inputs separately, disabling interrupts for a few milliseconds each time. It may use CTC channel 0 for timing, and if so, its calculations will be affected if CTC channel 0 is operating in a different mode from the mode that the BIOS is expecting (e.g. if the BIOS POST set CTC channel 0 to mode 3, and a program has subsequently reprogrammed it for mode 2, or vice versa) or if CTC channel 0 is operating with a non-standard divisor. Inputs which have no joystick connected will time out and be reported as zero. ## 10.4.3 NOTES FROM THE PC-GPE ARTICLE In the joystick article in the PC Games Programmer's Encyclopedia (PC-GPE), Steve McGowan and Mark Feldman give some useful information. All joysticks they tested returned non-linear values, i.e. the value returned at centre-position is not half way between the values returned at corner positions, so most joystick setup programs require the user to set up the centre position as well as the corner positions. (This is not surprising, as joysticks apparently use logarithmic, not linear, potentiometers!) They suggest a 10% 'dead zone' around the centre, as joysticks do not always centre repeatably. Joysticks are not high quality devices, and some smoothing (e.g. 1/4 new plus 3/4 old, or 1/8 new + 7/8 old) on the position values may help. ## 10.4.4 SAMPLE PROGRAM: READING THE JOYSTICK POSITION The following program demonstrates three methods of reading the joystick pot positions. The first method, ctc2_read_joystick(), uses CTC channel 2 in mode 0 for timing the pulse produced by the joystick hardware, and also detecting timeout. Timeout occurs when more than MAXCTCCLOCKS CTC clocks pass and the monostable output is still active. This method reads one joystick input at a time. The second method, refd_read_joystick(), uses the Refresh Detect signal (see section ¯¯ 7.37) as the timing source, and reads all four inputs simultaneously. The third method uses the BIOS function call. The first method has two caveats: (1) the ctc2_read_joystick() function will cut off any audio being generated using CTC channel 2, and (2) T2PORT is 0x62 on PCs and XTs with the old 8255 chip (see section ¯¯ 7.30) so if you wish to support these machines, you will have to detect the machine type (code for this is included in section ¯¯ 7.37.1 but is in assembler) and select the port address accordingly. It may work under OS/2. HW_TIMER should be set ON. The ctc2_read_joystick() function uses a similar technique to the equivalent BIOS function, though I wrote it before I disassembled the BIOS version. It has several advantages over the BIOS function - it doesn't rely on the mode and divisor of CTC channel zero, so it will work if CTC channel zero has been programmed with a different mode and/or a different divisor, it has much more consistent and more accurate timeout detection, it reads one input at a time (so only the relevant inputs need be read), it will often be quicker than the BIOS function, and it has higher resolution. Its major disadvantage is that because it uses CTC channel 2, it will stop any speaker sound that may be in progress when the function is called (sound cards are not affected, of course). In a practical application, the values returned from ctc2_read_joystick() could be averaged (using a 3/4-old-averaged-value plus 1/4-new, or 7/8-old-averaged- value plus 1/8-new, or similar algorithm, or average of last n samples) to reduce jitter, though this will slow the response. The second method, using Refresh Detect, will not work on a PC or XT, as they do not have a Refresh Detect signal. Also, it assumes that the refresh rate is as configured by the BIOS, i.e. a divisor of 18 in CTC channel 1, giving one refresh every 15.0857 microseconds. It has a much lower resolution than the first method, but has the advantage that it reads all four inputs at once, so in most cases will be the quickest method, and it does not make any use of CTC channels 0 and 2, so it does not rely on the programmed mode and divisor in CTC channel 0, and does not disrupt speaker sound being generated via CTC channel 2. This sample program also reads the joystick using the BIOS function described in the previous section, and displays the values read directly and the values read via the BIOS. See section ¯¯ 6.22 for the explanation of the pushf/cli/popf technique. -------------------------------- snip snip snip -------------------------------- /* Sample program #18 Demonstrates three ways of reading the joystick Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save this file to SAMPLE18.C and compile with: bcc -I -L -ms sample18.c Where inc_path is the path to your C header files and your startup modules C0x.OBJ, and lib_path is the path to your C libraries Cx.LIB. */ #pragma inline; /* Required for asm pushf, popf, cli, and sti */ #include /* Needed for bioskey() */ #include /* Needed for inportb() and outportb() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define BIOS_TICK_COUNT_P ((volatile unsigned long far *) 0x0040006CL) #define JOYPORT 0x201 /* Joystick port I/O address */ #define MONOS 0x0F /* Bottom four bits are monostable outputs */ #define T2PORT 0x61 /* Use 0x62 for PC and XT! */ #define T2OUT 0x20 /* Bit 5 is timer 2 output readback */ #define PORTB 0x61 /* For Refresh Detect - AT only! */ #define REFDET 0x10 /* Bit 4 is Refresh Detect */ #define MAXCTCCLOCKS 1800 /* Max CTC clocks for timeout */ #define WAITCTCCLOCKS 10 /* CTC clocks for monostable recovery (<255) */ #define MAXREFRESH 100 /* Maximum refresh detect counts for timeout */ typedef struct { int ax; int ay; int bx; int by; } joyvals; /* The following function should preferably be called with interrupts enabled. It preserves the state of the interrupt flag, and explicitly disables interrupts at several places, including disabling interrupts for up to 1.5 ms during the read operation. It returns -1 if an error occurred (i.e. bad input number specified, or timeout), otherwise it returns the number of CTC clocks measured. It reads a single joystick pot input. */ unsigned int ctc2_read_joystick(unsigned int inputnum) { unsigned char joymask; /* Bitmask for input */ unsigned int endtime; /* Count in timer 2 at end of pulse */ if (inputnum > 3) return -1; /* Invalid input number */ joymask = 1 << inputnum; asm pushf; asm cli; outportb(PORTB, (inportb(PORTB) & 0xFC) | 0x01); /* Enable Timer 2 */ asm popf; if (inportb(JOYPORT) & joymask) { /* Check for still timing out */ asm pushf; asm cli; outportb(0x43, 0xB0); /* Chan. 2, two-byte, mode 0 */ outportb(0x42, MAXCTCCLOCKS & 0xFF); outportb(0x42, MAXCTCCLOCKS >> 8); while (inportb(JOYPORT) & joymask) { if (inportb(T2PORT) & T2OUT) { asm popf; return -1; } } asm popf; } asm jmp SHORT $+2 asm jmp SHORT $+2 /* Sniff for pending interrupts */ asm pushf; asm cli; outportb(0x43, 0x90); /* Channel 2, lobyte-only, mode 0 */ outportb(0x42, WAITCTCCLOCKS); while ((inportb(T2PORT) & T2OUT) == 0) ; /* Wait for a short time */ asm popf; asm jmp SHORT $+2 asm jmp SHORT $+2 /* Sniff for pending interrupts */ asm pushf; asm cli; outportb(0x43, 0xB0); /* Chan. 2, two-byte, mode 0 */ outportb(0x42, MAXCTCCLOCKS & 0xFF); outportb(0x42, MAXCTCCLOCKS >> 8); /* Start channel 2 */ outportb(JOYPORT, 0); /* Start monostables */ while (inportb(JOYPORT) & joymask) { if (inportb(T2PORT) & T2OUT) { /* Timed out */ asm popf; return -1; } } outportb(0x43, 0x80); /* Latch timer 2 */ endtime = inportb(0x42); endtime += inportb(0x42) << 8; asm popf; return MAXCTCCLOCKS - endtime; } /* The following function should be called with interrupts enabled. It will lock out interrupts for up to about 1.5 ms during the main timing cycle. It reads all four joystick positions. */ void refd_read_joystick(joyvals * jv) { unsigned char counts[16]; /* Counts per input combination */ unsigned char refcount; /* Counter for refreshes */ register unsigned char portbval; /* Value from port B, and counter */ register unsigned char inlast, inthis; /* Joystick port input values */ unsigned char timedout; /* Inputs that timed out either phase */ unsigned char changed; /* Inputs that changed */ /* Check for any monostables still timing out */ portbval = inportb(PORTB); for (refcount = 1; refcount < MAXREFRESH; ++refcount) { inthis = inportb(JOYPORT) & MONOS; if (!inthis) break; /* All monostables finished */ while (((inportb(PORTB) ^ portbval) & REFDET) == 0) ; portbval ^= 0xFF; } timedout = inthis; /* Set bits for inputs that timed out */ /* Initialise counts and wait sixteen refreshes for monostables to stabilise */ for (inthis = 0; inthis < 16; ++inthis) { counts[inthis] = 0; while (((inportb(PORTB) ^ portbval) & REFDET) == 0) ; portbval ^= 0xFF; } inlast = MONOS; /* Initialise most recent input value */ /* Timing critical stuff - could be optimised to assembly language */ asm pushf; asm cli; /* Lock ints for timing critical stuff */ portbval = inportb(PORTB); while (((inportb(PORTB) ^ portbval) & REFDET) == 0) ; /* Wait for refresh detect to change */ portbval ^= 0xFF; outportb(JOYPORT, 0); /* Start the monostables */ for (refcount = 1; refcount < MAXREFRESH; ++refcount) { inthis = inportb(JOYPORT) & MONOS; if (inthis < inlast) counts[inlast = inthis] = refcount; if (!inthis) break; /* All monostables finished */ while (((inportb(PORTB) ^ portbval) & REFDET) == 0) ; /* Wait for it to change */ portbval ^= 0xFF; } asm popf; timedout |= inthis; /* Any that timed out this time */ /* Now figure out what happened */ jv->ax = jv->ay = jv->bx = jv->by = -1; inlast = 0; for (inthis = 0; inthis <= MONOS; ++inthis) { if ((refcount = counts[MONOS - inthis]) != 0) { changed = (inthis - inlast) & (timedout ^ 0xFF); inlast = inthis; if (changed & 1) jv->ax = refcount; if (changed & 2) jv->ay = refcount; if (changed & 4) jv->bx = refcount; if (changed & 8) jv->by = refcount; } } return; } void bios_read_joystick(joyvals * jv) { unsigned int jax, jay, jbx, jby; _AX = 0x8400; _DX = 0x0001; geninterrupt(0x15); jax = _AX; jay = _BX; jbx = _CX; jby = _DX; jv->ax = jax; jv->ay = jay; jv->bx = jbx; jv->by = jby; return; } void main(void) { joyvals refdvals, biosvals; printf("Sample program #18 - Demonstrates reading joystick positions\n" "Part of the PC Timing FAQ / Application notes\n" "By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n" "Timeout (input not connected) is indicated by 65535 for the CTC2\n" "\tand RefDet methods, and 00000 for the BIOS function method\n\n" "Press to exit\n\n" "----- CTC2 method ----- ---- RefDet method ----" " ----- BIOS method -----\n\n"); while (1) { refd_read_joystick(&refdvals); bios_read_joystick(&biosvals); printf("%05u,%05u,%05u,%05u %05u,%05u,%05u,%05u %05u,%05u,%05u,%05u\r", ctc2_read_joystick(0), ctc2_read_joystick(1), ctc2_read_joystick(2), ctc2_read_joystick(3), refdvals.ax, refdvals.ay, refdvals.bx, refdvals.by, biosvals.ax, biosvals.ay, biosvals.bx, biosvals.by); if (bioskey(1)) if ((bioskey(0) & 0xFF) == 27) break; } exit(0); } -------------------------------- snip snip snip -------------------------------- The logic of ctc2_read_joystick() is not obvious so I will explain. The function only measures one joystick input, and it may have been called recently, so the input it is about to measure may still be timing out from an earlier call to ctc2_read_joystick(). The function tests explicitly for this, and if this is the case, it performs a timeout detection in the first while() loop, waiting for the monostable output to go low. If the monostable output does not go low within the timeout period, the function returns -1. If the monostable output is, or already was, low, then a short delay of about 16 CTC clocks plus overhead is inserted, to give a minimum recovery time for the monostable circuitry which must discharge or recharge the capacitor fully. If the monostable is triggered too quickly after it has timed out, the capacitor might not be fully discharged or recharged, resulting in an unusually short pulse, because the capacitor doesn't have to charge or discharge so far to reach the monostable threshold. Then, CTC channel 2 is programmed with a count of MAXCTCCLOCKS and the joystick monostables are triggered. This section of code operates with interrupts locked out. It continually checks the joystick status, and checks whether a timeout has occurred. A timeout is indicated by the Timer 2 Output signal on the I/O port at I/O address 61h (62h on the PC and XT). If a timeout occurs, the function returns -1. If the monostable times out and its status line goes low within the timeout period, the count in CTC channel 2 is latched, and the number of elapsed CTC clocks is calculated and returned. The function will always return within about 2 x MAXCTCCLOCKS CTC clocks (units of 0.838 us) plus interrupt overhead, unless the CTC is faulty. See section ¯¯ 7.30 for a detailed explanation of the timing method using CTC channel 2 in this way. The logic of refd_read_joystick is similar, but it watches for transitions on the Refresh Detect signal to measure elapsed time. Whenever the monostable bits in the joystick port value change, the counts[] array is updated with the Refresh Detect count for the appropriate input pattern. This means that if more than one monostable times out within the same sample period, the code does not have to potentially update up to four variables, possibly missing a Refresh Detect transition. The four returned values are calculated after the timing critical section has completed. The code also keeps flags for inputs which have timed out, either in the initial checking phase, or the main timing phase, and always returns -1 for these inputs. ## 10.4.5 USING THE JOYSTICK PORT FOR GENERAL PURPOSE INPUT The joystick button inputs can be used as general purpose button or switch inputs, and can also be driven by logic level signals or by open collector or open drain logic outputs. If used with a signal direct from a mechanical contact (e.g. a switch, microswitch, contact, or pushbutton), remember that the joystick port does not perform hardware debouncing, so this must be provided by external hardware or provided by software. Provided that you can tolerate poor accuracy, poor repeatability, poor matching between channels, and poor temperature stability, you can use the joystick position inputs as general purpose analogue inputs, but don't fart too close to them. The inputs should not be voltage-driven, they should be driven from a variable resistor from a positive supply rail such as the 5V rail (the way the joystick itself works), or from a positive variable current source. This gives a roughly linear relationship between resistance and time measured, which means an inverse (reciprocal) relationship between current and time measured. A voltage signal can be converted into a variable current signal, and a circuit to do this is given in Figure 5 in the FIGURES archive. This circuit converts a positive, ground-referenced voltage into a positive current source that can be fed into one joystick position input. The relationship between input voltage and output current is linear. 1V on the input produces an output current of 1mA. The circuit requires a 9-12V supply, which is unfortunately not available on the joystick port, though you could use a switched capacitor voltage booster (e.g. the Linear Technology LT1054) or a switching supply (e.g. the Motorola MC34063 or the National Semiconductor LM2574 series) to produce a higher voltage rail from the 5V output on the joystick port, but be aware that switching power supplies can create a lot of electrical noise. Because the relationship between input voltage and time measured is reciprocal, a zero input voltage will give an infinite timeout. Obviously this should be avoided, as it will prevent software from reading the inputs within a reasonable period of time. This can be prevented by ensuring that the input voltage never falls below a certain threshold, or it could be prevented by incorporating an offset in the voltage to current converter. In the very unlikely event that you are interested in pursuing this, I may be able to help so please drop me an email message. ## 10.4.6 JOYSTICK LEFT/RIGHT AND UP/DOWN DETECTION If you simply want to detect whether the joystick is left or right of centre, or above or below centre, and don't want the overhead of locking interrupts for several milliseconds at regular intervals, you could use a fast tick interrupt to poll the joystick port. I would suggest using an interrupt at about 500 us and working cyclically through three states. On one interrupt, trigger the joysticks. On the next interrupt, read the monostable states. On the next interrupt, do nothing. On the next interrupt, you're back to the first interrupt again, so trigger the monostables again. This will give a left/right and up/down indication every 1.5 ms, with a fairly low overhead. ## 10.5 THE MOUSE AND MOUSE DRIVER [NOT WRITTEN] I haven't investigated the mouse or the mouse driver. The format of the serial data is documented (see {JAM}'s documents for the basic information) but I have nothing on its use of the timer tick interrupt or the CTC hardware. This section may (or may not :-) be completed at a later date. Any information is welcomed. (*) ## 10.6 NETWORKS I have no experience with networks, so I will quote (paraphrased) from {JAM}'s documents (see section ¯¯ 1.7). The int 8 overhead is increased when network software is installed, because the network software uses the interrupt to check whether the network is still functioning properly. This increase is not really significant. Details are documented in the Netware book (see the references section). However, the network card interrupts the processor via the network card's own interrupt, whenever the processor must process and respond to a data packet. This occurs even if the computer is not using the network at the time, because the network still checks regularly that the computer is present. {JAM} continues: "Other machines were checked with just Pathworks or just Novell and the errors are similar to this. In fact, for machines using Novell over broadband networks, delays in the order of 1.5 to 2 milliseconds were not uncommon. The actual numbers presented here should be taken with a grain of salt; they are going to differ widely with different networks, loads, CPU speeds, and network cards". ## 10.7 SOUND GENERATION Though the PWM method of sound generation is widely used, the specific method of generating it on a PC, described in this section and subsections, was (to my knowledge) first described Mark Feldman the PC-GPE (PC Games Programmer's Encyclopedia) guru (see section ¯¯ 1.7), and subsequently developed by Peter Moylan and Tim Channon (see section ¯¯ 1.7 and ¯¯ 10.7.4). It has probably been independently developed by others. The documentation and the coding of the sample program are my own. The PC's basic beep sound makes the speaker cone move between two positions - in and out. This is shown by the following 'waveform' which graphs speaker position (on the vertical axis) against time (horizontal axis). IN ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ CONE ³ ³ ³ ³ OUT ÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄ 1 2 3 4 5 6 7 8 ms TIME... This is digital (on or off) control, and this level of control severely limits the type and subtlety of the sounds that can be generated. Better sound requires the speaker to be put in more than two positions. For example, an 8-bit sound card such as a Sound Blaster gives 256 discrete output voltages or speaker positions, using an analogue signal which can assume any of 256 discrete values, and CDs and good quality sound cards use a 16-bit converter that gives 65536 discrete values. A digital control can approximate this to a limited degree using a technique called Pulse Width Modulation (PWM), where a digital signal made up of pulses at a high frequency is _averaged_ by the hardware. The width of the pulses is adjusted ('modulated') and this varies the _average_ voltage of the signal when it is averaged over a short period of time. If the pulse rate is high enough, the speaker will not be able to follow the pulses themselves, but will follow the average value. If the pulse widths, and therefore the average value, are varied at audio frequency, the average value, and therefore the speaker cone position, varies at audio frequency, and audible sound is generated. ## 10.7.1 PULSE WIDTH MODULATION (PWM) PRINCIPLE 5V ÚÄ¿ ÚÄ¿ ÚÄ¿ ³ ³ ³ ³ ³ ³ 25% duty cycle 0V ÄÙ ÀÄÄÄÄÄÙ ÀÄÄÄÄÄÙ ÀÄÄÄÄ Average voltage = 1.25V 5V ÚÄÄÄ¿ ÚÄÄÄ¿ ÚÄÄÄ¿ ³ ³ ³ ³ ³ ³ 50% duty cycle 0V ÄÙ ÀÄÄÄÙ ÀÄÄÄÙ ÀÄÄ Average voltage = 2.5V 5V ÚÄÄÄÄÄ¿ ÚÄÄÄÄÄ¿ ÚÄÄÄÄÄ¿ ³ ³ ³ ³ ³ ³ 75% duty cycle 0V ÄÙ ÀÄÙ ÀÄÙ À Average voltage = 3.75V Simple PWM (shown above) uses a fixed pulse rate, and varies the pulse width. Notice that the rising edges on the above waveforms are all in sync and regular. The diagram below shows a PWM pulse stream, with pulse start points marked, and the corresponding approximate average value, showing the audio content in the signal. If the pulse rate is high enough, only the audio component is audible. ÚÄÄÄ¿ ÚÄÄÄÄÄ¿ ÚÄÄÄÄÄ¿ ÚÄÄÄ¿ ÚÄ¿ ÚÄ¿ ÚÄÄÄ¿ ÚÄÄÄÄÄ¿ ÚÄ PWM ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ÄÙ ÀÄÄÄÙ ÀÄÙ ÀÄÙ ÀÄÄÄÙ ÀÄÄÄÄÄÙ ÀÄÄÄÄÄÙ ÀÄÄÄÙ ÀÄÙ ^ ^ ^ ^ ^ ^ ^ ^ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄ AVERAGE ÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄ ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ Figure 6 in the FIGURES archive shows this a bit more clearly. ## 10.7.2 PWM AUDIO GENERATION IMPLEMENTATION PWM audio generation can be done directly by the microprocessor, but this is unreliable due to memory caching and other factors that may affect the speed of the processor's operation. The generic method uses CTC channels zero and two, and gives more consistent operation. Channel zero is used to generate interrupts at the pulse rate, typically 11kHz or higher, and the int 8 handler uses channel two to generate the pulses. ## 10.7.3 SAMPLE PROGRAM: DTMF GENERATION USING PWM The following sample program uses PWM to generate DTMF (dual tone multiple frequency) tones, also known as touch tones, which are used for signalling numbers being dialled on a touch tone telephone. The audio output from the program is very quiet, so I have not been able to confirm that it will actually dial a telephone, but its main purpose is to present the techniques and sample code. This program takes over the timer tick interrupt, operating it at about 18000 interrupts (PWM pulses) per second. It does not chain to the BIOS handler, and it does not restore the correct DOS time from the RTC on termination. This program will cause loss of time when run. The time can be corrected by rebooting the machine. -------------------------------- snip snip snip -------------------------------- NAME SAMPLE19 ; Sample program #19 ; Demonstrates DTMF (touch tone) generation using PWM sound techniques ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; ; This program assembles into SAMPLE19.COM, a small command-line driven program ; which generates DTMF (dual tone multiple frequency) tones, also known as ; touch tones, using PWM sound techniques through the PC speaker, according to ; the command line parameters. ; ; Save this file to SAMPLE19.ASM and assemble with: ; masm SAMPLE19; ; link SAMPLE19; ; exe2bin SAMPLE19.exe SAMPLE19.com ; or ; tasm SAMPLE19; ; tlink /t SAMPLE19; ; ; ; Note - this program will _not_ run properly under OS/2, Linux, Windows, or ; anything other than plain DOS. If possible, it should be run without EMM386 ; or QEMM or any other memory manager, particularly on slower machines such as ; 386SX or slow 386 machines. PulseDivisor = 66 ; Interrupt rate is 1.1931816666... MHz ; divided by this value Fifty = PulseDivisor/2 ; Pulse width for roughly 50% duty ; The chosen PulseDivisor of 66 gives an interrupt rate of about 18,079 ; interrupts per second. ; The following GW-BASIC program generates the 256-entry sinewave table with ; maximum spans of +/- 16, centre zero, using signed values. The span must ; be chosen so that when two sinewaves are added together and added to the ; 'Fifty' value (which represents half the number of CTC clocks between PWM ; pulses), the range of possible pulse widths is within the tolerance of the ; PWM interrupt rate. In this case, the maximum excursion for two summed ; sinewaves is taken to be +/- 32 (two sinewaves, each at +/- 16). When added ; to the 'Fifty' value (33), the pulse width range is 1 to 65. ; If you change the pulse rate, you must change the span (the 16# in line 20) ; appropriately. I chose a span of roughly (PulseDivisor - 2) / 4. ; ; 10 OPEN "SINE.DMP" FOR OUTPUT AS#1 : A# = 0 : I# = 3.141592653589793#/128# ; 20 FOR P = 0 TO 255 : S# = SIN(A#) : V = INT((S# * 16#) + .5#) : PRINT #1,V ; 30 A# = A# + I# : NEXT : CLOSE #1 : SYSTEM ; ; I used the following GW-BASIC program to generate lists of delta sequences ; for indexing into the 256-entry sinewave table. The A#=... value in line 20 ; specifies the sample rate; the last number in the equation is PulseDivisor. ; If you change PulseDivisor, modify this and rerun the program. ; To use the program, input the desired frequency, and it will calculate ; possible delta sequences and prompt you. Initially, just press Enter at ; the prompt, until you have chosen the delta sequence you will use. Then ; break and rerun the program, and at the prompt for the chosen sequence, ; type 'y '. The program will append to a file '$CALC.DMP' and list ; the delta sequence. ; ; 10 REM $CALC - Calculation for DTMF generator program ; 20 A#=14318180#/12#/66# : INPUT F : PRINT "There are" A#/F "samples/cycle" ; 30 I# = 256 * F / A# : PRINT "Samples are spaced at intervals of" I# ; 40 X# = 0 : D = 1 : R = 0 ; 50 R = R + 1 : X# = X# + I# : Z = ABS(X# - INT(X# + .5#)) : IF Z >= D THEN 50 ; 60 PRINT R "samples, error is" Z;: D = Z : INPUT Q$ : IF Q$ <> "y" GOTO 50 ; 70 OPEN "$CALC.DMP" FOR APPEND AS#1 : PRINT#1, F ":" R "values" : S# = 0 ; 80 I = 0 : FOR P = 1 TO R : SD = -I : S# = S# + I# : I = INT(S# + .5#) ; 90 SD = SD + I : PRINT#1, SD;: NEXT : PRINT#1,"" : CLOSE #1 : END Code SEGMENT ASSUME cs:Code,ds:Code,es:nothing,ss:nothing ORG 100h Begin: jmp Begin2 ; Skip data SignOnMsg DB "Sample program #19 - DTMF generator demonstrating PWM sound generation",13,10 DB "Part of the PC Timing FAQ / Application notes",13,10 DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10 DB "Characters 0-9, *, #, and A-D generate DTMF pairs",13,10 DB "Characters a-h generate single tones",13,10 DB "A comma generates a 1/2-second pause",13,10,"$" ALIGN 2 ParmPointer DW 81h ; Pointer into command tail OldInt8Ofs DW 0 ; Old int 8 handler offset OldInt8Seg DW 0 ; Old int 8 handler segment PWMBufferGet DW 0 ; 'Get' offset for PWMBuffer (volatile) PWMBufferPut DW 0 ; 'Put' offset for PWMBuffer PIC0IMR DB 0 ; PIC IMR before we stopped all but IRQ0 ALIGN 2 Tone0Ctrl DW 0,0 ; Delta and sinewave table pointers Tone1Ctrl DW 0,0 ; Same, for other tone of the pair CharScanTable: DW "0",Row4,Col2 DW "1",Row1,Col1 DW "2",Row1,Col2 DW "3",Row1,Col3 DW "4",Row2,Col1 DW "5",Row2,Col2 DW "6",Row2,Col3 DW "7",Row3,Col1 DW "8",Row3,Col2 DW "9",Row3,Col3 DW "*",Row4,Col1 DW "#",Row4,Col3 DW "A",Row1,Col4 DW "B",Row2,Col4 DW "C",Row3,Col4 DW "D",Row4,Col4 DW "a",Row1,0 DW "b",Row2,0 DW "c",Row3,0 DW "d",Row4,0 DW "e",Col1,0 DW "f",Col2,0 DW "g",Col3,0 DW "h",Col4,0 PastCharTable = $ Row1 DW 10,10,10,9,10,10,10,10,Row1 ; 697 Hz Row2 DW 11,11,11,11,11,10,11,11,11,11,Row2 ; 770 Hz Row3 DW 12,12,12,12,12,12,12,13,12,12,12,12,12,12,12,Row3 ; 852 Hz Row4 DW 13,14,13,Row4 ; 941 Hz Col1 DW 17,17,17,17,18,17,17,17,Col1 ; 1209 Hz Col2 DW 19,19,19,19,19,19,18,19,19,19,19,19,Col2 ; 1336 Hz Col3 DW 21,21,21,21,21,20,21,21,21,21,21,21,Col3 ; 1477 Hz Col4 DW 23,23,23,23,24,23,23,23,Col4 ; 1633 Hz SineTable DB 0,0,1,1,2,2,2,3,3,4,4,4,5,5,5,6,6,6,7,7,8,8,8,9,9,9,10 DB 10,10,10,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14 DB 14,14,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16 DB 16,16,16,16,16,16,16,16,16,16,16,15,15,15,15,15,15,15,14 DB 14,14,14,14,14,13,13,13,13,12,12,12,12,11,11,11,10,10,10 DB 10,9,9,9,8,8,8,7,7,6,6,6,5,5,5,4,4,4,3,3,2,2,2,1,1,0,0,0 DB -1,-1,-2,-2,-2,-3,-3,-4,-4,-4,-5,-5,-5,-6,-6,-6,-7,-7,-8 DB -8,-8,-9,-9,-9,-10,-10,-10,-10,-11,-11,-11,-12,-12,-12 DB -12,-13,-13,-13,-13,-14,-14,-14,-14,-14,-14,-15,-15,-15 DB -15,-15,-15,-15,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16 DB -16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-15,-15,-15 DB -15,-15,-15,-15,-14,-14,-14,-14,-14,-14,-13,-13,-13,-13 DB -12,-12,-12,-12,-11,-11,-11,-10,-10,-10,-10,-9,-9,-9,-8 DB -8,-8,-7,-7,-6,-6,-6,-5,-5,-5,-4,-4,-4,-3,-3,-2,-2,-2,-1 DB -1,0 ALIGN 4 ; No need to do this, really. PWMBuffer DB 256 DUP(?) ; PWM width-value data buffer (circular) Begin2: mov dx,OFFSET SignOnMsg ; Point to sign-on message mov ah,9 ; Function number int 21h ; Output the message cld ; Upwards direction call InitialisePWM ; Initialise and start PWM stuff DigitLoop: mov bx,ParmPointer ; Get pointer into command tail inc ParmPointer ; Bump it mov al,[bx] ; Get character from command tail cmp al,13 ; End of command tail? je DigitsDone ; If so cmp al," " ; Whitespace? jbe DigitLoop ; If so, skip it cmp al,"," ; Comma? jne NotComma ; If not mov cx,7850 ; If so, pause for half a second call MakeDelay jmp SHORT DigitLoop ; Loop NotComma: mov bx,OFFSET CharScanTable-6 ; Point to before first entry NextCharScan: add bx,6 ; Point to next cmp bx,OFFSET PastCharTable ; Scanned whole table? jae DigitLoop ; If not found, skip it cmp al,[bx] ; Check for match jne NextCharScan ; If not, loop mov ax,[bx+2] ; Get first tone pointer mov dx,[bx+4] ; Get second tone pointer mov cx,3140 ; 200 ms duration call MakeDTMF mov cx,785 ; 50m ms pause call MakeDelay jmp SHORT DigitLoop ; Loop DigitsDone: call UninstallPWM mov ax,4C00h int 21h int 20h MakeDTMF PROC near mov Tone0Ctrl,ax mov Tone0Ctrl+2,0 mov Tone1Ctrl,dx mov Tone1Ctrl+2,0 DTMFLoop: mov bx,OFFSET Tone0Ctrl call GetPulseWidth mov bx,OFFSET Tone1Ctrl cmp WORD PTR [bx],0 jz SingleTone xchg ax,dx call GetPulseWidth add al,dl SingleTone: add al,Fifty call PutPWM loop DTMFLoop ret MakeDTMF ENDP MakeDelay PROC near DelayLoop: mov al,Fifty call PutPWM loop DelayLoop ret MakeDelay ENDP GetPulseWidth PROC near ; Call with BX pointing to first or cld ; second tone control structure mov si,[bx] ; Get delta table pointer lodsw ; Get a delta or pointer test ah,ah ; Was it a pointer? jz GotDelta ; If not mov si,ax ; If pointer, reset pointer lodsw ; Get it, and increment pointer GotDelta: mov [bx],si ; Return the delta table pointer mov si,[bx+2] ; Get sine table pointer add si,ax ; Add delta and si,0FFh ; Wrap around mov [bx+2],si ; Restore sine table pointer mov al,SineTable[si] ; Get sine table entry ret GetPulseWidth ENDP InitialisePWM PROC near ; Get the current int 8 handler address, to be restored later mov ax,3508h ; Get interrupt vector for int 8 int 21h ; Call DOS mov OldInt8Ofs,bx ; Store offset mov OldInt8Seg,es ; Store segment ; Initialise PWM array with 50% duty cycle entries xor bx,bx ; Zero offset mov al,Fifty ; 50% duty cycle FillPWMBuf: mov PWMBuffer[bx],al ; Set entry inc bl ; Bump offset jnz FillPWMBuf ; Do all entries ; Wait for all floppy drives to turn off xor ax,ax ; Zero mov es,ax ; Address BIOS data area with ES WaitMotors: test BYTE PTR es:[43Fh],0Fh ; Any floppy drive motors active? jnz WaitMotors ; If so, wait ; Disable all interrupt sources on the primary (or only) PIC. Keep the ; original IMR contents, to be restored later. cli in al,21h ; Read primary PIC IMR jmp SHORT $+2 ; Short delay mov PIC0IMR,al ; Store it for later mov al,0FFh ; Mask off _everything_ out 21h,al ; Set up Port B and CTC channel 2 in al,61h ; Read Port B jmp SHORT $+2 ; Short delay and al,11111101b ; Speaker enable OFF or al,00000001b ; Timer 2 gate ON out 61h,al ; Write it back jmp SHORT $+2 ; Short delay mov al,10010000b ; Channel 2, lobyte access, mode 0 out 43h,al ; Set mode of channel 2 (no values yet) sti ; Allow interrupts ; Now grab int 8, the timer tick interrupt. DOS should leave the PIC IMR alone. mov dx,OFFSET NewInt8 ; Offset of new int 8 routine mov ax,2508h ; Set interrupt vector for int 8 int 21h ; Call DOS to do it ; Reprogram CTC channel 0 with the new interrupt rate cli ; Lock out interrupts again mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3 out 43h,al ; Write mode/command register jmp SHORT $+2 ; Short delay mov al,LOW PulseDivisor ; Lobyte of new divisor out 40h,al ; Send it jmp SHORT $+2 ; Short delay mov al,HIGH PulseDivisor ; Hibyte of new divisor out 40h,al ; Send it jmp SHORT $+2 ; Short delay ; Enable the speaker in al,61h jmp SHORT $+2 ; Short delay or al,00000011b ; Timer 2 gate and speaker enable ON. out 61h,al jmp SHORT $+2 ; Short delay ; Enable int 8 (IRQ0) in the PIC mov al,11111110b ; All masked except IRQ0 out 21h,al ; Set primary PIC IMR jmp SHORT $+2 ; Short delay ; Start interrupts and return to caller sti ; Tag! You're it :-) nop mov al,BYTE PTR PWMBufferGet ; Get 'get' pointer dec ax ; Back up one mov BYTE PTR PWMBufferPut,al ; Output one bufferful ret InitialisePWM ENDP UninstallPWM PROC near ; Disable the speaker pushf ; Preserve interrupt flag cli ; Lock out interrupts around this in al,61h ; Read Port B jmp SHORT $+2 ; Short delay and al,11111100b ; Disable Timer 2 and speaker out 61h,al ; Write it back jmp SHORT $+2 ; Short delay ; Disable all interrupt sources in the primary PIC mov al,0FFh ; Mask all IRQ0-7 out 21h,al ; Set PIC0 IMR ; Restore normal operation of CTC channel 0 mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3 out 43h,al ; Write mode/command word jmp SHORT $+2 ; Short delay xor al,al ; Zero out 40h,al ; Write loword of reload value jmp SHORT $+2 ; Short delay out 40h,al ; Write hiword of reload value popf ; Interrupts are safe (PIC is blocked) ; Restore original int 8 handler address push ds ; Will need to destroy DS for this mov dx,OldInt8Ofs ; Get offset mov ds,OldInt8Seg ; Get segment ASSUME ds:nothing ; DS no longer points to this segment mov ax,2508h ; Set int 8 vector int 21h ; Call DOS pop ds ; Restore DS ; Restore original IMR mov al,PIC0IMR ; Get old IMR contents out 21h,al ; Restore IMR ret UninstallPWM ENDP ; The following function stuffs a pulse width value into the circular buffer, ; first waiting for the interrupt routine's outgoing data pointer to catch up, ; if necessary. This prevents the foreground code from generating data more ; quickly than the interrupt routine is taking it, and maintains synchronisation ; between the two processes, unless the foreground code generates the data too ; slowly. PutPWM PROC near ; Put width in AL into buffer mov bx,PWMBufferPut ; Get the 'put' offset mov PWMBuffer[bx],al ; Store the width value in buffer inc bl ; Bump 'put' pointer mov PWMBufferPut,bx ; Store it back WaitBufFull: cmp bl,BYTE PTR PWMBufferGet ; If buffer is full... je WaitBufFull ; ... wait until there's a gap ret PutPWM ENDP ASSUME ds:nothing ; This program uses CTC channel 0 as a timebase, generating int 8 at regular ; intervals, and CTC channel 2 producing variable width pulses. The interrupt ; routine programs an 8-bit count value into channel 2 on every invocation, and ; channel 2 produces a pulse of the corresponding length on the speaker output ; signal. Because the interrupt rate is constant and the pulse width varies, ; pulse width modulation (PWM) sound is generated. ; This is the int 8 handler. It gets data from PWMBuffer, using PWMBufferGet ; as an offset into PWMBuffer indicating where it's currently up to. It bumps ; this variable on each timer interrupt. The bump increments the lobyte only, ; so that the offset wraps around from 255 back to 0 again (the buffer is 256 ; bytes in size). This code does not check to see whether PWMBufferGet has ; been bumped past PWMBufferPut. The foreground code must be fast enough to ; keep the buffer full - if not, the int 8 processing will repeat-play the ; buffer. ; Each entry in the buffer is one byte, and corresponds to the pulse width of ; one pulse. The data in this buffer is generated by the foreground code. ; The following code could be optimised somewhat - by page-aligning the ; PWM buffer, the MOV AL,PWMBuffer[BX] could be replaced with a direct load ; using a self-modified pointer, also removing the need to preserve BX. NewInt8 PROC far push bx ; Preserve push ax ; Preserve mov bx,PWMBufferGet ; Get pointer to data coming from buffer mov al,PWMBuffer[bx] ; Get one pulse-width byte from buffer out 42h,al ; Tell CTC channel 2 to make a pulse inc BYTE PTR PWMBufferGet ; Bump pointer (256-byte buffer) mov al,20h ; EOI command out 20h,al ; Send to primary PIC pop ax ; Restore pop bx ; Restore iret ; Return from interrupt NewInt8 ENDP Code ENDS END Begin -------------------------------- snip snip snip -------------------------------- ## 10.7.3.1 SAMPLE PROGRAM EXPLANATION Channel 0 is operated in mode 2 or 3, and generates interrupts (int 8) at regular intervals. Each int 8 will trigger the start of one pulse. The int 8 handler, NewInt8, will output a pulse-width value to the CTC channel 2 data register, and CTC channel 2 will produce a pulse of the corresponding length. Channel 2 is operated in mode 0, known as 'interrupt on terminal count' mode (see section ¯¯ 7.8.2). When CTC channel 2 has been initialised for this mode, and the Timer 2 Gate output in the Port B register (see section ¯¯ 7.5) is set to enable clocking of CTC channel 2, writing a count value to the channel 2 reload register will cause the CTC channel 2 output to go low for a period of time determined by the value written to the channel 2 register. By controlling the values written to the channel 2 register, the pulse width can be varied. The pulse width will be the CTC clock period (0.838 us) multiplied by the value written to the channel 2 register. To improve efficiency, because pulses are typically much less than 256 CTC clocks wide, CTC channel 2 is configured in lobyte-only access mode. Only one I/O access, to write a byte of data to CTC channel 2, is required to trigger a pulse on CTC channel 2. If the Speaker Data bit in Port B is set, the pulse will be sent to the PC's speaker. The above description covers the PWM output code, which consists of an interrupt handler, triggered at regular intervals via int 8 from CTC channel 0. The handler writes an 8-bit pulse-width value to the CTC channel 2 data register. The data that it writes is taken from a circular buffer. The interrupt handler maintains a pointer, the 'get' pointer, called PWMBufferGet, which lets it keep track of where it is up to in the buffer. On every interrupt, it loads the BX register from the 'get' pointer, reads a pulse-width value from the circular buffer at the appropriate position, and 'bumps' the 'get' pointer. The term 'bump' means to increment, but in this case, also wrap around from the end of the buffer to the start, as the buffer is circular. The actual data in the buffer is generated by the foreground code, and inserted into the buffer by the PutPWM function. This function maintains synchronisation between the foreground code and the interrupt handler, by checking that it will not overfill the buffer, before putting a byte into the buffer. This slows down the mainline code. As long as the mainline runs quickly enough, synchronisation between the mainline and the interrupt routine is maintained. The initialisation steps are fairly involved. All initialisation is done by the InitialisePWM function. First, the current int 8 handler address is stored so it can be restored later. Then the circular PWM buffer is filled with the 'Fifty' value, so that when the interrupt is started later, before the mainline has filled the buffer, it will play silence instead of garbage. The code then waits for all floppy drives to turn off. Because the replacement int 8 handler does not chain to the original handler, any actions normally done by the int 8 handler, such as updating the BIOS timer tick count variable and turning off floppy drives after two seconds of inactivity, will not be performed during the execution of this program, so we must wait for the disk drives to turn off before replacing the int 8 handler, otherwise they will remain on during the program's execution. The initialisation code then disables all interrupt sources on the primary interrupt controller. IRQ0 (int 8), the timer tick interrupt, will be enabled shortly. The code then initialises Port B, initially with Speaker Enable off, and programs the operating mode (mode 0, interrupt on terminal count) and the access mod (lobyte-only) in CTC channel 2. It then redirects int 8 to its own int 8 handler, reprograms CTC channel 0 with the new interrupt rate (18,079 interrupts per second), enables the Speaker Enable, and enables IRQ0 in the interrupt controller. Other interrupt sources are not enabled in the interrupt mask register of the primary PIC. This prevents interrupts due to a keypress from disturbing the sound generated. The system will not respond to keypresses while the program is running. It then enables interrupts, resets the 'put' pointer for the PWM buffer, and returns. Once this initialisation has been done, the interrupt routine will run quite happily in the background, outputting pulse widths from the circular buffer of pulse-width values. It will loop repeatedly through the buffer. Foreground code is required to set up the data in the buffer, and keep track of the 'get' pointer used by the interrupt routine, so it can control the flow of data into the circular buffer. The actual DTMF waveform generation is done via a 256-entry sinewave table with a span of +/- 16. The table contains one cycle of sinewave, and is indexed via a delta sequence table. Each of the eight possible frequencies has its own delta sequence table. The delta sequence table tells the program how many entries in the sinewave table to skip between PWM pulses (samples). For a high frequency, the deltas are large, so the program steps through the 256 entries of the sinewave table fairly quickly, and for lower frequencies, the delta is smaller. A table of deltas is required, to give the effect of a non-integral delta value so that reasonable frequency accuracy can be achieved. Running int 8 at these high rates causes a significant load on the machine, especially with slower machines. Using EMM386 adds interrupt overhead, and on slower machines, programs using this technique may not run properly with EMM386 installed. I have done limited testing with the sample program, and found that it works properly on a 10MHz 286, but I can't guarantee its performance on, say, a 386SX-16 running EMM386, or on an XT. ## 10.7.3.2 OTHER METHODS OF SOUND GENERATION The same fast int 8 handler can be modified to output an 8-bit unsigned sample value to a parallel port, which is connected to a DAC (digital to analogue converter). This gives much better sound quality than the PWM technique. The digital to analogue converter can be a chip, such as the Ferranti ZN429 or various devices from other manufacturers such as Analog Devices / PMI, Maxim, Burr-Brown, etc, or the el cheapo R-2R ladder DAC made from a chain of resistors. Commercial parallel port DAC units are available - the Speech Thing device is just a DAC on the parallel port. Sound cards have an 8-bit or 16-bit DAC, but are usually operated in DMA mode, where the sound card periodically requests an 8-bit or 16-bit data transfer from a buffer area in system memory and sends the value to the DAC. The DMA method gives much lower overhead, because the processor does not get involved in the transfer, and also removes the problem of sample timing jitter. ## 10.7.4 PETER MOYLAN'S MUSIC PACKAGE Peter Moylan's music package was written by Peter Moylan (see section ¯¯ 1.7) and Tim Channon. It uses CTC channel 0 with a divisor of 64, and CTC channel 2 in interrupt on terminal count mode (mode 0). It does not chain to the original int 8 handler, and does not fix up the DOS time on termination. This package produces 3-part polyphonic (i.e. three simultaneous pitches) music and supports several timbral qualities. It is noticeably out of tune, particularly at higher pitches, but this is due to limitations in the waveform generation algorithm, not the hardware technique used. Seven demonstration programs and source code in Modula-2 and assembler are included in the package, which is available on the Internet as: ftp://ee.newcastle.edu.au/pub/PMOS/music302.zip. The version number (3.02) may have changed. ## 10.8 RELATED SOFTWARE PACKAGES Here are my comments on some timing-related software packages available on the Internet. Many of these packages are several years old, so contact details may be well out of date. I have not checked any of the contact details. ## 10.8.1 THE ATIM PACKAGE ftp://oak.oakland.edu/SimTel/msdos/at/atim.zip Date: 19881125 Size: 4783 This package contains a small program called ATIM which will run another program and time its execution, using the RTC periodic interrupt for timing (approximately one millisecond resolution). The program is written in assembler, and commented source and brief documentation is included. The package was written by Howard Vigorita, NYACC (whatever that means :-), December 27, 1986. I presume it is public domain, though he doesn't say so. The program seems to work quite nicely. I'm not sure about the algorithm he uses to convert 1/1024ths to 1/1000ths of seconds, though. The only problem I noticed was that "COMMAND.COM" was hardcoded as the command interpreter name if the program is assembled to use the DOS EXEC function instead of the back door execute function - after all, this program is nearly nine years old! ## 10.8.2 THE MSCHRT AND TCHRT PACKAGES ftp://oak.oakland.edu/SimTel/msdos/c/mschrt3.zip Date: 19910604 Size: 53708 ftp://oak.oakland.edu/SimTel/msdos/turbo_c/tchrt3.zip Date: 19910605 Size: 53436 MSCHRT and TCHRT version 3 are Microsoft C and Turbo C compatible versions of a "high resolution timer toolbox" distributed as a library, from Ryle Design, P.O. Box 22, Mt. Pleasant, Michigan 48804, (517) 773-0587, CI$ 73047,1765. They also have an equivalent package for Turbo Pascal, called TPHRT. The package is shareware, $20 per copy. This company also sells a fully-functional timing toolbox called PCHRT, version 4, which also supports running the timer tick interrupt at a user-specified rate, and can be ordered from Ryle Design (order form included with MSCHRT V3 and TCHRT V3) for $49.95. This, and registered versions of MSCHRT, TCHRT, and presumably TPHRT, include library source and support. I have not checked that they are still contactable. The is clearly a very professionally designed package, which includes thorough documentation and has obviously been designed to make the user's task as easy and successful as possible. It provides 42 functions including the ability to produce formatted reports! It includes a self-calibration function, presumably to take into account the different amount of time required to read a timestamp on different machines. The timing functions can operate with interrupts enabled, or disabled (in this case, periods longer than 54.925 ms will not be measured correctly). It presumably sets CTC channel zero to mode 2, though the manual doesn't describe this correctly. Suggested applications are: timer or profiler to determine code performance, benchmarking programs, precise delays for hardware or process control, subject testing (e.g. reaction timing, race timing and scoring systems). The package also supports profiling and reporting on BIOS function interrupts (e.g. int 10h video, int 13h disk) - the vector is hooked and logging logic is installed, then complete information can be generated for that interrupt. Functions specifically to delay a specified length of time are also available. The package includes the library file, explanatory material, function reference, and five demo programs. There is no date in the manual, but the newest file in the archive is dated 19900723. I did not test this package - it's probably safe to assume that it works well. ## 10.8.3 THE TCTIMER PACKAGE ftp://oak.oakland.edu/SimTel/msdos/turbo_c/tctimer.zip Date: 19891029 Size: 15609 This is a public domain absolute timestamping package for Turbo C. It contains functions to enable mode 2 on CTC channel zero, to restore normal operation in mode 3 at exit, to read an absolute timestamp, and to calculate elapsed time in units of one microsecond using floating point arithmetic. The timestamp value is comprised of the count in progress and the bottom 16 bits of the BIOS timer tick variable, returned as a long (dword), therefore periods longer than one hour cannot be measured (this is mentioned in the documentation file). The documentation file says it was "written by Richard S. Sadowsky, 8/10/88, Version 1.0, released to the public domain, based on TPTIME.ARC which was written by Brian Foley and Kim Kokkonen of TurboPower Software and released to the public domain". Source code is included. This package appears to have the following problems. 1. Registers SI and DI are not preserved by the readtimer() function, usually causing the calling function to crash if it uses register variables. 2. The readtimer() function has many unnecessary I/O accesses and is fairly slow as a result. 3. Timing will be incorrect if the timed period spans a change of day, because just before midnight the loword of the BIOS tick count counts to 0AF hex then resets. This is not handled by this package. I tested the code briefly, after fixing the SI and DI problem, and it appeared to work correctly (apart from the midnight problem). ## 10.8.4 THE MILLISEC PACKAGE ftp://oak.oakland.edu/SimTel/msdos/c/millisec.zip Date: 19911204 Size: 37734 This package was released by Fred C. Smith (uunet!samsung!wizvax!fcshome!fredex) and is a modified version of a release by Dean Pentcheff (dean@violet.berkeley .edu) which is a modified version of the TCTIMER package (see previous section). Source is included. At some stage in the evolution of this package, the resolution seems to have been reduced to one millisecond. Dean Pentcheff's package (the 'missing link' :-) apparently returned elapsed time as a floating point number in units of one second, with three decimal places. This package returns elapsed time in units of one millisecond, to avoid floating point calculations. Also the CTC clock has been approximated to 1193000 Hz, resulting in a proportional error of 152.254 ppm (0.0152254%; 13.155 seconds per day). These routines use CTC channel zero in mode 2, as per the TCTIMER package, and the timer-reading function is identical to TCTIMER's one. The problems that I noted for TCTIMER still apply to this package. ## 10.8.5 THE MSEC_12 PACKAGE ftp://oak.oakland.edu/SimTel/msdos/c/msec_12.zip Date: 19920319 Size: 8484 This package was released by David Kirschbaum (kirsch@usasoc.soc.mil) and is a further modification of the MILLISEC package (see the previous section). David has moved the inline assembly stuff into a separate file, and fixed the problem with destroying SI and DI, though the rest of the read-timer function is the same as that of the TCTIMER package, so the remaining two problems are still present. The package uses one millisecond resolution, and approximates the CTC clock to 1193000 Hz, resulting in a proportional error of 152.254 ppm (0.0152254%; 13.155 seconds per day). Source and makefiles for TCC, BCC, and QC are included. ## 10.8.6 THE ERTIMER PACKAGE ftp://x2ftp.oulu.fi/pub/msdos/programming/docs/ertimer.zip Date: 19950506 Size: 9092 This ZIP file contains a message, a header file, and a C source file for an includable timing module that provides a user-selectable number of independent timers, each with 0.8381 us resolution, implemented via CTC channel 0 operating in mode 2 and using the loword of the BIOS timer tick count variable. Written by Ethan Rohrer, comments dated 19941204. Nicely written and fairly well commented, but cannot measure times longer than about an hour, and does not handle the problem of the CTC count synchronisation with the BIOS tick count, nor the midnight wraparound where the loword of the tick count counts to 0AF hex then wraps back to zero. Also does not lock out interrupts around hardware access sequences. Not reliable. ## 10.8.7 THE FASTCLOK PACKAGE ftp://x2ftp.oulu.fi/pub/msdos/programming/docs/fastclok.zip Date: 19950506 Size: 2588 This package consists of a C source file and a header file. The package runs the timer tick interrupt at 64 times its normal speed, using its own interrupt handler which chains to the BIOS handler correctly. Does not lock interrupts properly when installing and uninstalling. It installs an atexit() function to uninstall the fast timer and restore normal operation. The author does not identify him/herself. A comment in the source file says: "The gettimeofday() routine acts like the Unix version, with the exception that time zone does not matter. The time will be returned in timeval structures that match thier Unix counterparts". The program doesn't seem to include a gettimeofday() function, though. :-\ ## 10.9 BENCHMARKING CONSIDERATIONS When using absolute timestamping to benchmark a section of code, remember that because interrupts are enabled during execution of the code being timed, they will contribute to the time measured. During otherwise idle time, the timer tick interrupt will be active (every 54.9254 ms), the keyboard keystroke interrupt will occur every time a key is pressed or released, or repeatedly while the key is held down, and if a mouse driver is installed and enabled, the mouse's interrupt will occur several times every time the mouse is moved or the buttons change state. If the code being timed takes a short time, e.g. less than 100 milliseconds, the effect of the timer tick interrupt may be detectable. If the period is shorter than 54.9 ms, it can be measured with interrupts locked out, because interrupts are only required to ensure that the BIOS tick count variable is updated correctly on every cycle of CTC channel zero. The other factors can be avoided by not touching the keyboard or mouse during the test. Other factors have an effect on benchmarks, such as the processor cache state and, for file processing programs, the disk cache state. The latter problem can be avoided by disabling the disk cache, or ensuring that the input file is already in the cache (providing that the cache is big enough to hold it) by entering 'copy /b filename nul:' to force the entire file to be read from disk. Finally, adding the code which reads the timer adds to the execution time. For example, if you call a function to read an absolute timestamp twice in succession, the times read will differ by the amount of time taken to read the timestamp. For example, the assembly language get-timestamp function given in the sample program in section ¯¯ 9.2 takes between 7 and 9 CTC clocks (about 6.5 us) to execute on my 486DX2-66. I have no experience or information on ways to determine processor clock speed. If anyone can help, please let me know. (*) ## 10.10 GRANULARITY AND UNCERTAINTY This may seem obvious, but the accuracy of any time measurement is limited by the granularity of the timing source, and its uncertainty. Granularity, or resolution, refers to the fineness of the unit in which the time or duration can be measured. For example, using 54.9254 ms timer ticks to measure the time taken by a short section of code is going to be of limited use. On most of the test runs, no time will appear to have elapsed, but occasionally, one tick, or 54.9254 ms, will appear to have elapsed. The resolution is not high enough, and a different approach is required - for example, running the section of code repeatedly in a loop, and measuring the total time taken. If 1000 iterations of the code are timed using the timer tick, by sampling the BIOS Tick Count variable, running the code 1000 times, then re-reading the BIOS Tick Count and using the difference in tick counts to calculate the amount of time elapsed, we might find that five ticks, or about 275 ms, have elapsed, but how accurate is this figure? Code execution ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÄÄÄÄÄÄÄ Timer ticks ÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄ 1 2 3 4 5 6 7 8 (You will need a monospaced display to see the above diagram properly). In the above example, when the code started, the tick count was 2. When it finished, the tick count was 7. The execution time was 5 ticks. Code execution ÄÄÄÄÄÄÄÄÄÄÄÄÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÄÄÄÄ Timer ticks ÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄ 1 2 3 4 5 6 7 8 Above, when the code started, the tick count had just changed to 2, and when it finished, the tick count was 7, just about to change to 8. The measured time was 5 ticks, as before, but the actual execution time was nearly 6 ticks. Code execution ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÄÄÄÄÄÄÄÄÄÄ Timer ticks ÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄ 1 2 3 4 5 6 7 8 Above is the opposite case. The measured time is again from 2 to 7, 5 ticks, but the execution time was actually only slightly longer than 4 ticks. These examples demonstrate uncertainty of up to one tick at both the start and the end of the sampling time. The uncertainty at the start of the sample is due to the granularity, or resolution, of the timing source, and the fact that it is free-running or asynchronous (not synchronised) to the event being timed. The uncertainty at the end of the sampling time is the unavoidable effect of the resolution of the timing source. The total uncertainty of the sample is two ticks. If we wait for the tick count to change, then start the code, we can eliminate (or greatly reduce) the uncertainty at the start of the sampling time. The worst cases would then be: Code execution ÄÄÄÄÄÄÄÄÄÄÄÄÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÄÄÄÄÄÄÄÄÄÄ Timer ticks ÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄ 1 2 3 4 5 6 7 8 and Code execution ÄÄÄÄÄÄÄÄÄÄÄÄÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÄÄÄÄ Timer ticks ÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄÄÄÄÁÄÄÄ 1 2 3 4 5 6 7 8 Sometimes it is possible to synchronise the time reference and the event to be timed, either by delaying the start of the event (as in the above example) or by starting the time reference from a known part of its cycle when the start of the event is detected. Sometimes it is not possible to do this. For example, the Refresh Detect signal described in section ¯¯ 7.37 has a period of about 15 us, but cannot safely be stopped and restarted at a particular point so that it is synchronised to the start of some event. When using such a time base, you must either synchronise the event to the time base (as in the third method of reading the joystick position in section ¯¯ 10.4.4) or live with the fact that there is a 30 us uncertainty in any event that is timed using this method. Also see the sample program section ¯¯ 4.7 (timeouts implemented using the timer tick) where the uncertainty is actually at the _start_ of the timing period, not at the end. ## 10.11 CONVERTING BETWEEN MICROSECONDS AND CTC CLOCKS Conversion between microseconds and CTC clocks requires fairly accurate arithmetic, namely multiplication by 1.193181666... or 0.838095... This can be done using floating point, however this is slow on machines without a math coprocessor, and is inefficient, and does not necessarily give very good accuracy, even if you are not using a Pentium :-) And as floating point is not usually required in the remainder of the program, it seems silly to require it for this purpose only. For comparatively painless implementation on all x86 processors under DOS, the method described here uses a function that multiplies two long values (32 bits each) together, giving a 64-bit result, and returns the top 32 bits of the result as a 32-bit long. If bit 31 of the 64-bit result is set, then the return value is rounded up. Using longs to represent microseconds or CTC clocks limits the maximum period that can be expressed to about 59 minutes and 59.592 seconds (0xFFFFFFFFL CTC clocks), i.e. slightly less than one hour. The function definition follows. I have used Borland's register pseudovariables (_AX and _DX), so this must be changed for other compilers. -------------------------------- snip snip snip -------------------------------- unsigned long mul64shift32(unsigned long value, unsigned long mult) { asm { push si mov ax,WORD PTR value mul WORD PTR mult mov si,dx mov ax,WORD PTR value+2 mul WORD PTR mult+2 mov bx,ax mov cx,dx mov ax,WORD PTR value mul WORD PTR mult+2 add si,ax adc bx,dx adc cx,0 mov ax,WORD PTR value+2 mul WORD PTR mult add si,ax adc bx,dx adc cx,0 shl si,1 adc bx,0 adc cx,0 mov ax,bx mov dx,cx pop si } return (_DX << 16) + _AX; /* Should optimise out to nothing */ } -------------------------------- snip snip snip -------------------------------- The arithmetic expression for this function is: return_value = int ((value * mult / (2^32)) + 0.5) Note there is no way for overflow to occur in this function, because even with value and mult of 0xFFFFFFFFL, the 64-bit result is only 0xFFFFFFFE00000001. This function can be used in the conversion of microseconds to CTC clocks and vice versa, by the appropriate choice of the 'mult' value. The 'mult' value is defined as the desired multiplication factor (e.g. 0.838...) multiplied by 2^32. For conversion from CTC clocks to microseconds (multiplication by 0.838095...), the 'mult' value is 3599592096L (0xD68D6AA0L). For conversion from microseconds to CTC clocks (multiplication by 1.193181666...), 'mult' would be 5124676237, which is too large to express as a long (because the factor of 1.193181666... is greater than 1), so this conversion is done by multiplying by the fractional part of the conversion factor, 0.193181666..., then adding the original value. The fractional part of the conversion factor equates to a 'mult' value of 829708941L (0x31745A8DL). Here are the two conversion functions, which use mul64shift32() internally. -------------------------------- snip snip snip -------------------------------- unsigned long clocks_to_usec(unsigned long clocks) { return mul64shift32(clocks, 3599592096L); } unsigned long usec_to_clocks(unsigned long usecs) { if (usecs > 3599592094L) return 0xFFFFFFFFL; return usecs + mul64shift32(usecs, 829708941L); } -------------------------------- snip snip snip -------------------------------- Note the check in usec_to_clocks(). The maximum number of microseconds that can be represented by a 32-bit number of CTC clocks is 3599592095, which equates to 0xFFFFFFFFL CTC clocks. This represents a time of about 59 minutes and 59.592 seconds, just under one hour. Because of the unrelated units of the two quantities, conversion between clocks and microseconds using integer values inevitably introduces rounding errors, so conversions should not be done cumulatively. For example, if you are summing several durations, your measurements should be kept in clocks and converted to microseconds after the summation. Other than integer rounding error, the above functions contribute a proportional error of less than 0.00000001% (0.0001 ppm, 9 us per day, about five orders of magnitude better than typical crystal accuracy :-). ## 10.12 MAINTAINING A MILLISECOND OR MICROSECOND COUNT The sample program in section ¯¯ 4.7 uses the BIOS Tick Count variable as a time indication. This variable is in units of one tick, i.e. 54.9254 ms. There may be cases where you want to maintain a time value which is in units of some more sensible value, for example, milliseconds, or maybe microseconds. Converting between absolute tick count and absolute milliseconds is messy, but it is easy to maintain a variable, in units of one millisecond or microsecond, which is updated cumulatively using the timer tick. For example, you could define a 32-bit variable that will contain the number of milliseconds since the program started, and call this the milliseconds variable. When and where the milliseconds variable is updated depends on your program design. The variable needs to be updated every time a timer tick occurs. You can achieve this by hooking int 1C hex (see section ¯¯ 6.35 and ¯¯ 6.36) or by hooking int 8 (see section ¯¯ 6.33), or if there is a convenient 'idle' point in your program where it can read the BIOS tick count variable, the update can be done there, by checking whether the tick count has changed from the previous tick count, and if so, updating the millisecond variable and updating the previous tick count, but with this last method, the logic is a bit untidy because the update must behave correctly if more than one tick has elapsed since the update routine was last called. Updating the milliseconds variable involves adding the number of milliseconds that have elapsed, into the variable. If CTC channel zero is running with its normal divisor of 65536, every timer tick interrupt represents 54.9254 ms of elapsed time. But since the milliseconds variable is a 32-bit integer (no fractional part), you can't add 54.9254 to it. You have to keep another variable that keeps track of the remainder. On most interrupts, you will add 55 to the milliseconds variable, but on some interrupts, you will add only 54. This can be done using a scheduling variable to control whether the 'add' value will be 54, or 55. On every interrupt, we will add either 54 or 55 to the milliseconds variable. But the elapsed time is 54.9254 ms. A remainder variable keeps track of the fractional part of the real time, and allows us to decide whether to add 54 or to add 55. The fractional part of the tick period (in milliseconds) is 0.9254, or more accurately, 0.9254164984656, which is roughly 12/13. 65536 multiplied by this value is about 60648. If we add 60648 to a 16-bit count which represents a number of 1/65536ths-of-a-millisecond, every time the addition carries (which will be about 12 out of every 13 times), another millisecond has accumulated, so we would add 55 to the millisecond variable. If the remainder variable did not carry after adding the 60648, we would add 54 to the milliseconds instead. Over a reasonable period of time, and (most importantly) over a long period of time, the milliseconds variable will be accurate. The error contributed by this technique (due to approximating 65536 x 0.9254... to 60648) is only 0.02657 ppm, or less than one second per year. The error contributed by crystal inaccuracy will be about three orders of magnitude higher. The code to do the update comes out very nicely in assembler: add Remainder,60648 ; Add 65536 x 0.9254 adc MillisecL,54 ; Add 54 or 55 to loword adc MillisecH,0 ; Carry into hiword The three variables Remainder, MillisecL, and MillisecH, are all 16-bit. MillisecL and MillisecH are loword and hiword of the milliseconds variable. For a microsecond counter, the same technique applies, but instead of adding 54 or 55 on each tick, you are adding 54925, and the remainder is 65536 x 0.4164984656, or 27295. add Remainder,27295 ; Add 65536 x 0.4164984656 adc MicrosecL,54925 ; Add 54925 or 54926 to loword adc MicrosecH,0 ; Carry into hiword These techniques don't magically give you millisecond or microsecond timing resolution from a 54.9254 ms clock tick, of course. The resolution is still only 54.9254 ms. But they do provide a way to get a time value with a sensible unit. The same technique can be used when the timer tick is operated at a faster rate (see section ¯¯ 8 and subsections), though the constants change. For example, to get an actual timing resolution of about 500 us, you could use a channel 0 divisor of 596, giving an interrupt rate of one tick every 499.504825334 us. Using a microsecond variable, the update would add 499 plus the carry from adding 33084 to the remainder variable, and 499 plus carry to the microseconds variable: add Remainder,33084 ; Add 65536 x 0.504825334 adc MicrosecL,499 ; Add 499 or 500 to loword adc MicrosecH,0 ; Carry into hiword With these values, cumulative error due to approximation of 65536 x 0.5048... to 33084, is 0.00712 ppm. Choosing to use a millisecond timing variable may make your program easier to port to (or from) an environment where the system time is kept in units of one millisecond. For example, OS/2's system time is kept in units of 1ms, though it does not have a 1ms resolution - is actually only updated every 31.25 ms. ## 10.12.1 SAMPLE PROGRAM: MILLISECOND COUNT USING INT 1CH The following program uses int 1Ch with the critical error handling module from section ¯¯ 5.8, and demonstrates maintaining a milliseconds count. The timing resolution of the program is only 54.9254 ms, as it does not modify the timer tick rate, but the time is reported in units of one millisecond, rather than units of 54.9254 ms. Int 1Ch should not be used in TSRs - see section ¯¯ 6.35 for details. Every time the user presses a key, the current millisecond count is displayed. Pressing the Escape key terminates the program. -------------------------------- snip snip snip -------------------------------- /* Sample program #20 Demonstrates a milliseconds count using int 1Ch Part of the PC Timing FAQ / Application notes By K. Heidenstrom (kheidens@actrix.gen.nz) Save and assemble the critical error module CRIT_ERR Save this sample code to SAMPLE20.C Compile this module with: bcc -c -I -ms sample20.c Link the modules with: tlink /c /x \c0s.obj sample20.obj crit_err.obj, sample20, nul, \cs Where inc_path is the path to your C header files, c0_path is the path to your startup modules C0x.OBJ and lib_path is the path to your C libraries Cx.LIB. */ #include /* Needed for enable(), disable(), MK_FP() */ #include /* Needed for _open() and _write() */ #include /* Needed for printf() */ #include /* Needed for exit() */ #define FALSE 0 #define TRUE 1 #define STDERR 2 /* DOS handle for standard error */ void crit_err_intercept(void); /* Provided in CRIT_ERR.OBJ */ unsigned int is_at_crit_prompt(void); /* Provided in CRIT_ERR.OBJ */ typedef void interrupt (far *intfuncp)(); /* Pointer to interrupt handler */ intfuncp old_int_1Ch = (intfuncp)0xFFFFFFFFL; static unsigned int remainder; static volatile unsigned long milliseconds; void abort_cleanup(int dos_is_safe) { if (dos_is_safe) { if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) { setvect(0x1C, old_int_1Ch); old_int_1Ch = (void far *)0xFFFFFFFFL; } } else { disable(); /* Probably superfluous */ if (old_int_1Ch != (intfuncp)0xFFFFFFFFL) { *((intfuncp far *)MK_FP(0, 0x1C << 2)) = old_int_1Ch; old_int_1Ch = (void far *)0xFFFFFFFFL; } } return; } void interrupt ctrl_c_handler(void) { static char message[] = "\r\nProgram terminated by Ctrl-Break or Ctrl-C\r\n"; if (is_at_crit_prompt()) abort_cleanup(FALSE); else { abort_cleanup(TRUE); _write(STDERR, &message, sizeof(message)); } exit(255); } void interrupt new_int_1Ch(void) { asm { add remainder,60648 adc WORD PTR milliseconds+0,54 adc WORD PTR milliseconds+2,0 } return; /* From interrupt */ } void intercept_int_1Ch(void) { old_int_1Ch = getvect(0x1C); setvect(0x1C, new_int_1Ch); return; } unsigned long get_milliseconds(void) { static unsigned long rv; asm pushf; asm cli; rv = milliseconds; asm popf; return rv; } void main(void) { int n; milliseconds = 0; printf("Sample program #20 - Demonstrates millisecond count using int 1Ch\n"); printf("Part of the PC Timing FAQ / Application notes\n"); printf("By K. Heidenstrom (kheidens@actrix.gen.nz)\n\n"); crit_err_intercept(); /* Trap critical errors */ setvect(0x23, ctrl_c_handler); /* Trap Ctrl-C interrupt */ intercept_int_1Ch(); /* Intercept int 1Ch */ printf("Press any key to display current millisecond count\n"); printf("Press to exit\n\n"); do { while (bioskey(1) == 0) ; n = bioskey(0); printf("Millisecond count is: %ld\n", get_milliseconds()); } while ((n & 0xFF) != 27); abort_cleanup(TRUE); exit(0); } -------------------------------- snip snip snip -------------------------------- ## 10.13 NOTES ON MICROSOFT WINDOWS I have no interest in Windoze, but I have received a few comments regarding timing under Windoze which you may find useful. {TOR} Tor Sjowall (tor@oslonett.no) said (slightly paraphrased): > A regular 18Hz clock interrupt routine (int 8 or int 1Ch) in a DOS box under > Windows works as usual as long as the DOS box has the focus, but when another > window has the focus, the DOS box's timer tick interrupt rate slows down to > one tick every 800 ms. The tick counter however is incremented as usual. > This has driven me crazy... > > As far as I can gather from the documentation, this is the correct behaviour. > The reason being that Windows wants to use as much of the CPU capacity as > possible. Also I have not found any 'back door' into the 386 mode timer > interrupt routine that will allow my program to catch the ticks. > > The RTC periodic interrupt sort-of works under Windows. The problem is that > each DOS box has its own Virtual Machine, plus one for Windows itself. So > all these VMs each get a simulated hardware interrupt from the real 386 mode > interrupt handler. This works well enough, but the overhead is large. > Actually, Windows has a real ugly wart here: If the hardware interrupt was > enabled in the PIC before Windows was started, all the VMs under Windows will > get a simulated hardware interrupt with I/O ports trapped, etc etc. If the > interrupt was disabled on the PIC, only the VM that enabled the interrupt on > the PIC gets the interrupt. There is a text on the Developer CD, 'The Tao of > Interupts', that describes this in all its gory detail. > > The overhead is enormous: the V86 DOS interrupt has only 7% of the throughput > of a native DOS interrupt routine. A 90Mhz Pentium is a good idea... Thanks Tor for that information. ## 10.14 DOS FILE DATE AND TIME STAMPS {TOR} also suggested this topic, as it is related to timing. DOS's FAT (File Allocation Table) file system stores the date and time of last modification of every file. Date and time values are each 16 bits (two bytes) wide. In the directory structure, the date value is at offset 24 into the directory entry, and the time value is at offset 22. In the findfirst/find- next structure in the DTA, returned by DOS when findfirst or findnext are requested, the date is also at offset 24 and the time at offset 22. The file date word is constructed as follows. F E D C B A 9 8 7 6 5 4 3 2 1 0 * * * * * * * . . . . . . . . . Year minus 1980 (range 0-119) . . . . . . . * * * * . . . . . Month (range 1-12) . . . . . . . . . . . * * * * * Day of month (range 1-31) The file time word is constructed as follows. F E D C B A 9 8 7 6 5 4 3 2 1 0 * * * * * . . . . . . . . . . . Hours (range 0-23, 24-hour format) . . . . . * * * * * * . . . . . Minutes (range 0-59) . . . . . . . . . . . * * * * * Seconds / 2 (range 0-29) Note that the time is only stored with a resolution of two seconds, so the time stamp on a file modified at 12:34:56 is the same as the time stamp on a file modified at 12:34:57. The date and time fields can be combined into an unsigned long value (date in the hiword, time in the loword) and compared with other date/time fields in the same format to see which file is newer or whether the date and time are the same. ## 10.15 DOS AND THE DATE AND TIME Under DOS, the current date and time is not stored in the DOS kernel, but is provided as required, by the CLOCK$ driver. Whenever DOS wants to know the date and time, it issues a 6-byte read request to the clock driver, which it identifies via the CLOCK bit in the device attribute word. Traditionally the driver name is "CLOCK$" but this is not required. See section ¯¯ 3.3 for a replacement CLOCK$ driver that uses the AT's RTC. The CLOCK$ driver supports reading and writing. Six bytes are always read and written. These bytes encode the full date and time. The six bytes of data are all in binary form. The structure is: 0 WORD Number of days since 1st January 1980 (0-up) 2 BYTE Minutes (0-59) 3 BYTE Hours (0-23) 4 BYTE Hundredths of seconds (0-99) 5 BYTE Seconds (0-59) DOS will write to the CLOCK$ driver when int 21h, functions 2Bh or 2Dh (the DOS set date and set time functions) are issued. It will read the CLOCK$ driver when int 21h, functions 2Ah or 2Ch (DOS get date and get time functions) are issued, or when it wants to know the date and time for timestamping a disk file. The standard CLOCK$ driver supplied with DOS reads the date from the RTC on initialisation (i.e. at reboot time), and converts this date to a count of days elapsed since 1st January, 1980. It maintains the date internally in this form, as this is the form used by DOS in the CLOCK$ read and write function calls. The standard CLOCK$ driver uses the BIOS timer tick count variable to keep track of the time of day. This variable is set up by the computer's BIOS from the RTC time of day, as part of the power-on self-test (POST) procedure, so it is correct when DOS boots. The CLOCK$ driver issues BIOS interrupt 1Ah, function 0, Get Tick Count, every time it is asked to supply the current date and time. This function returns a flag in AL called the midnight flag. The flag is set by the BIOS int 8 handler when the tick count variable wraps around from 1800AFh to 0, and is true the _first_ time BIOS int 1Ah function 0 is called following a change of day. After the BIOS has reported the flag true, it clears the flag, and it will only be true again on the next change of day. This is also described in section ¯¯ 4.2. When the date and time are requested from the CLOCK$ driver, it calls the BIOS function, checks the midnight flag and if set, increments its count of days since 1st January 1980. It then converts the tick count into hours, minutes, seconds, and hundredths of seconds. Of course the tick count has a resolution of only 54.9254 ms, much more coarse than the 10ms resolution provided by the hundredths of seconds value. I do not know what algorithm the CLOCK$ driver uses to calculate the hours, minutes, seconds, and hundredths from the tick count. When the date and time are set, the CLOCK$ driver's days since 1980 count is set, and the CLOCK$ driver presumably calculates an appropriate tick count value and uses int 1Ah function 1 to set the tick count. I believe that the CLOCK$ driver also updates the RTC date and time, presumably through int 1Ah functions 3 and 5. The DOS kernel contains the code to convert between days, months, and years, and number of days since 1st January 1980. It can convert both ways, as the date is both requested (int 21h function 2Ah) and set (int 21h function 2Bh) in days, months, and years format. So to summarise, at reboot, the BIOS sets up the tick count using the RTC time, DOS boots and the CLOCK$ initialisation code calculates the number of days since 1 January 1980 from the RTC date and stores this internally. When the date and time is requested from the CLOCK$ driver, it calls int 1Ah function 0, checks the returned midnight flag and increments its day count if set, calculates the hours, minutes, seconds, and hundredths values from the tick count, and returns these values. When the date and time is set by writing to the CLOCK$ driver, the driver updates its day count to the specified value, and presumably calculates an appropriate tick count, sets it via int 1Ah function 1, sets the RTC time via int 1Ah function 3, and calculates days, months, and years from the day count and sets the RTC date via int 1Ah function 5. If anyone has disassembled any DOS CLOCK$ drivers, please let me know what you found out. (*) I will eventually do this anyway. ## 10.15.1 DOS DATE ROLLOVER BUGS There are two problems related to the change of day under DOS's CLOCK$ driver. The first is that int 1Ah, function 0, only returns the midnight flag set the first time it is called following a change of day. If an application program or TSR calls this function, and happens to call it after a change of day but before the CLOCK$ driver calls it, the application or TSR will see the change of day but the CLOCK$ driver will miss it. Therefore, no program should use int 1Ah function 0. Also see section ¯¯ 4.2. The second problem is that there is no way to tell how many midnights have passed, since the midnight flag is just a flag, not a count. This problem usually affects computers that run constantly but are unattended, where the date and time may not be requested for a long time - more than 24 hours. Two or more midnights may pass, but when the date and time is requested, the date in the CLOCK$ driver is only incremented by one. I have heard that some BIOSes actually implement the midnight flag as a count, and the CLOCK$ driver may possibly respond to values other than 0 or 1 and update the date correctly, but I don't know for sure. (*) ## 10.16 SIMULATING A VERTICAL RETRACE INTERRUPT The aim of this technique is to provide an interrupt which is synchronised to the screen refresh (i.e. vertical scan) so that certain functions that must be performed during the vertical retrace period can be done via this interrupt, in the background. For more details on this, see section ¯¯ 7.33. Some video cards (notably EGA cards) are able to generate a vertical retrace interrupt themselves, usually on IRQ2/9, but this facility is not standard on VGA cards. The vertical retrace interrupt can be simulated using CTC channel 0. Vertical retrace emulation is sometimes a hot topic on comp.os.msdos.programmer and comp.lang.asm.x86, with many people interested in how it can be done, but I (and my correspondent Anders Roar Nielsen, aroni@night.ping.dk) don't believe that it is necessary in most applications. Retrace can be detected by polling, the field time can be measured, and CTC channel 0 can be used to estimate where the video circuitry is 'up to' (at the default divisor, there are at least three field scans per CTC channel 0 wraparound). These techniques of maintaining code synchronisation to the screen refresh, using the CTC, will generally have much lower overhead and less impact on other aspects of the machine's performance. The triple buffering technique, described in section ¯¯ 10.16.3, does rely on vertical retrace interrupt simulation, however. ## 10.16.1 VERTICAL RETRACE INTERRUPT SIMULATION DESCRIPTION The technique described in this section is based on an apparently well-known algorithm. I saw it suggested by Tommy Marshall (tommym@oneworld.owt.com), in his message <3vv8n1$j8g@paperboy.owt.com> of Sat 05 Aug 1995. I have enhanced the algorithm to improve its performance under adverse conditions, and added thorough documentation. In his posting, Tommy mentions that some demo source code is available on his web site: http://www.owt.com/users/tommym/index.html. Thanks to Anders Roar Nielsen (aroni@night.ping.dk) for his help with this subject. The following diagram will only make sense if viewed on a monospaced screen. ÃÄÄÄÄÄ 17088 ÄÄÄÄÄÅÄÄÄÄÄ 17088 ÄÄÄÄÄÅÄÄÄÄÄ 17088 ÄÄÄÄÄÅÄÄÄÄÄ 17088 ÄÄÄÄÄ´ ÃÄÄÄÄ 16968 ÄÄÄÄ´ ÃÄÄÄÄ 16968 ÄÄÄÄ´ ÃÄÄÄÄ 16968 ÄÄÄÄ´ ÃÄÄÄÄ 16968 ÄÄÄÄ´ ÃÄÄ Ú¿ . Ú¿ . Ú¿ . Ú¿ . Ú¿ ³³ . ³³ . ³³ . ³³ . ³³ ³³ . ³³ . ³³ . ³³ . ³³ ÄÄÄÙÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙÀÄ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ a b * c d e f g h i j k l m The above diagram shows the retrace signal graphed against time (time is on the horizontal axis). The numbers on the diagram are in units of one CTC clock (0.8381 us). The values are as measured on my Tseng ET4000 W32i card operating in standard 25-line, 80-column colour VGA text mode (720x400 pixels). The pulses are the retrace indication from the video card, readable on the video status input port. At point A, when the retrace indication becomes active, the entire screen has been scanned and the electron beam is beginning its vertical retrace - retracing its steps back to the top of the screen. The retrace pulses are fairly short - in this case, only 76 CTC clocks, or about 64 us long. The actual vertical retrace time (the time taken for the electron beam to return to the top of the screen and start scanning the displayable part of the picture) is much longer than the pulse indicates; Klaus Hartnegg (klaus@mailserv.brain.uni-freiburg.de) reports that a typical VGA vertical blanking period is about 2 ms. The visible part of the next vertical scan starts a little later - say at point B. At point D, the scan ends and the next retrace begins, ready for the next scan which starts at E. During the retrace period, i.e. between point A and point B, it is safe to modify certain parameters in the video subsystem, for example to perform page flipping or some types of screen updates, without causing flicker or other visible interference. ## 10.16.1.1 MEASURING THE FIELD TIME The field time, i.e. the time span between the same edge of two adjacent retrace pulses, can be measured by initialising CTC channel 0 in mode 2, waiting for a rising (or falling) edge on the retrace indication, reading the count in CTC channel 0, waiting for the next edge of the same type, and re-reading the count in CTC channel 0, and calculating the number of CTC clocks elapsed between the two samples. This is done with interrupts disabled. In this case, a time of 17088 CTC clocks was measured, with a fluctuation of +/- 1 CTC clock period. The field period is this number multiplied by 0.8381 us (the CTC clock period). In this case the field period is 14.321 ms. The field rate (number of fields per second) is the reciprocal of the field time - in this case, about 69.8 fields per second, i.e. a vertical scan rate of about 69.8 Hz. ## 10.16.1.2 CONTROLLING THE CTC INTERRUPT Having determined the field period, 17088 CTC clock periods, we can program CTC channel 0 to give an interrupt a short time before the rising edge of the next retrace pulse will be due. We wait for a start of retrace, i.e. point A, and immediately reset and program CTC channel 0 for mode 2 with a count of slightly less than 17088, so that it will generate an interrupt shortly before the start of the next retrace, say at point C. During normal operation, CTC channel 0 will issue an interrupt at point C. The int 8 handler will loop, waiting for the start of the retrace (point D). When this occurs, the interrupt handler resets and reprograms CTC channel 0, so that it will interrupt again at point F. The important screen updates that must be done during retrace, can now be performed. The interrupt handler then exits, and the mainline gets execution until point F, at which point the CTC triggers another interrupt and the cycle repeats as if from point C. This technique assumes that the video mode does not change during execution and is not reset. A video mode reset may cause the scanning to restart out of sync. The interrupt will resynchronise in the latter case, but may lock interrupts for an unusually long amount of time when doing so, as it may potentially remain in the retrace wait loop for a long time. ## 10.16.1.3 SIGNIFICANCE OF THE SAFEMARGIN VALUE The number of CTC clocks which are subtracted from the field period to give the CTC count value is important. I will call it the SafeMargin value. The sample program uses a default SafeMargin of 120 CTC clocks, or about 100 us. The significance of the SafeMargin value is that it determines the maximum interrupt latency that can be tolerated. This latency is made up of interrupt acceptance delay (due to interrupts being locked out) and interrupt overhead (e.g. overhead caused by EMM386). If, for example, interrupts were locked out at point '*', by a CLI instruction issued by the mainline or some code that was called by the mainline, then the interrupt would be signalled (by the CTC) at point C, but would not be accepted immediately. If the acceptance was delayed past point D, the start of the retrace period, then the int 8 handler is going to see that the retrace has already started. If this occurs, the int 8 handler cannot guarantee that there is enough time for the screen manipulation, before the visible part of the scan begins and the manipulation will cause visible interference. Therefore, if interrupts are being locked out periodically by the mainline or code called by the mainline, the SafeMargin value must be long enough to cover the longest period for which interrupts will be locked out, plus any delays in interrupt acceptance (EMM386 overhead), so that in the worst case, if interrupts were locked out just before the CTC channel 0 interrupt was signalled, they will be enabled in time for the interrupt to be accepted and the int 8 handler to be entered and to check the retrace flag _before_ the retrace actually starts, so that there is almost the entire retrace period available for the screen update. The sample program allows the SafeMargin value to be set from the command line via a decimal number which represents the number of CTC clocks (units of 0.8381 us) for the SafeMargin value. The default SafeMargin value is 120, giving a safety margin of about 100 us including interrupt overhead. If you use a short SafeMargin value, it is essential that no foreground code locks out interrupts for any reasonable length of time. On the other hand, a large SafeMargin value reduces the amount of time available for other processes (i.e. the mainline), as a larger amount of time is spent in the loop in the int 8 handler, waiting for the start of retrace, between points C and D. If SafeMargin is increased to more than about half of the vertical scan time, the system falls apart, giving widely varying loop counts and a jumpy display. I haven't bothered to try to figure out why this occurs, because half a field period is normally at least 6 ms, so SafeMargin should never be anywhere near this long, but I noticed that it can be fixed by moving the instructions that prepare the CTC to accept its new count, back to just after the CTC count in progress is read, so that the CTC is frozen during the wait-for-retrace loop. ## 10.16.1.4 OVERHEAD DUE TO LARGE SAFEMARGIN AND SCREEN UPDATE Depending on the SafeMargin value you choose, you may also need to take into account the time spent between points C and D, as it will take a chunk of processing time, and operates with interrupts locked out. Remember that the operations performed by the retrace function (screen updates, etc) are also performed with interrupts locked out, so if they are extensive, this may have a significant impact on latency for other interrupts. For example, don't try to use this technique in a game that communicates via a serial link or a modem (multi-player multi-computer games) unless you're using a very low data rate, or carefully controlling the outgoing flow control lines to prevent loss of incoming characters! ## 10.16.1.5 ENHANCED HANDLING OF MISSED RETRACE START The above algorithm can be improved in cases where the start of retrace is missed due to interrupt latency. First, if we keep track of the last reload value that was programmed into the CTC, we can read the count in progress in the CTC and subtract it from that value, to determine the number of CTC clocks by which the interrupt was delayed. By subtracting our SafeMargin value, we can determine how many CTC clocks into the retrace period we are. We may be able to make use of a retrace interrupt even if it was delayed past the start of retrace, if we know that there is still enough time to execute screen update code before the start of the next visible scan. Also, we can correct for the error by reprogramming CTC channel 0 even if we cannot get a timing reference from the video subsystem. I will now explain these enhancements in detail, using an example. ÃÄÄÄÄ 16968 ÄÄÄÄ´ ³ ÚÄÄ¿ . ÚÄÄ¿ . ÚÄÄ¿ ³ ³ . ³ .³ . ³ ³ ³ ³ . ³ .³ . ³ ³ ÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ .ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄ ³ ³³³ ³ ³ ³ a bcd e f g Using the above diagram as an example, assume that the field period is 17088 clocks, SafeMargin is 120, and the visible scan starts at point E. The CTC was programmed with a count of 16968. An interrupt is signalled at point A but interrupts are locked out by the mainline or some code called by the mainline. At point B, retrace has already started, but our interrupt routine is still prevented from executing. Then interrupts are enabled at point C. The interrupt handler starts immediately, but discovers that retrace has already started. It reads the CTC count in progress at point D, and gets a value of, say, 16728. At point A, the CTC reloaded with a count of 16968, so by subtracting the count in progress from the last CTC count, i.e. 16968-16708, which is 260 CTC clocks, we can calculate the number of CTC clocks between point A and point D (point D being _now_). This value is the amount of time by which the interrupt was delayed, and includes delay caused by interrupts being locked out, and delay in the actual interrupt acceptance process, e.g. delay caused by EMM386. I will call this value the Latency value. We can then subtract SafeMargin (which is a fairly accurate estimate of the time between points A and B) from the Latency value, to calculate the time between point B (start of retrace) and point D (now). This gives a result of 140 clock cycles between B and D. If the start of visible scan at point E is known to be, say, 1300 CTC clocks after point B (the start of retrace), we can calculate how much time remains before the visible scan begins, by subtracting the number we just calculated (140, the time between B and D) from the 1300 (the time between B and E), to get 1160 CTC clocks, the amount of time left before the visible scan starts. If the screen update code is known to take comfortably less than this amount of time, then it can still be executed. If that was tricky, it gets trickier! If interrupt acceptance was delayed so long that the interrupt routine executed after the _end_ of the retrace pulse, it would not know that it had missed the pulse altogether, and would sit in its wait loop, waiting for the start of retrace, for the entire displayed field, until the _next_ retrace started! During this time, the mainline could not execute, and interrupts would be locked out! We can detect this, again by reading the CTC count in progress on entry to the int 8 handler and determining how long the interrupt acceptance was delayed (i.e. the Latency value). If the Latency is significantly longer than SafeMargin, we can assume that we have at least missed the _start_ of the retrace, and possibly the end as well. When the interrupt is accepted within the SafeMargin period, we can wait for the start of retrace, then resynchronise the CTC by resetting it and setting the count to the field period minus SafeMargin (16968) again. But when we miss the start of retrace, because interrupt acceptance was delayed for longer than SafeMargin, we no longer have a video timing reference from which we can resynchronise CTC channel 0. But since we know how long interrupt acceptance was delayed (the measured Latency value), we can estimate a new count to program into CTC channel 0, that will cause an interrupt at roughly the correct point in the next cycle. For example, in the above example, at point D we know that 260 clocks have elapsed since point A when the interrupt was signalled by the CTC. We want the next interrupt to be signalled at point F, which is SafeMargin clocks before the start of the next retrace, at point G. The count to be programmed into CTC channel 0 is therefore the field time minus the measured Latency. CTC channel 0 is reset and programmed with a count of 17088 - 240, or 16848. 16848 CTC clocks later, it will generate the interrupt at point F. This method is not 100% accurate, as there will be some delay in reading and setting the CTC count, as well as some delay between the two. This would result in the interrupts getting progressively later, if retraces were missed repeatedly. As soon as an interrupt is accepted within SafeMargin, the CTC is resynchronised from the retrace signal. I thought of a better method that would have kept better synchronisation in these circumstances, and it should have worked, but it didn't. Oh well. ## 10.16.1.6 OTHER NOTES There may be special considerations for interlaced video modes. If you are using these modes, you will probably already know enough to figure out whether there will be any problems :-) Also, because this technique intercepts int 8, the standard precautions as described in section ¯¯ 5 should be taken, to ensure that the program is not terminated without being able to clean up and restore the original int 8 handler and standard divisor on CTC channel 0. I found it very interesting to run various programs from the DOS shell and see the effect they have on interrupt latency. For example, on my 486DX2-66, the background interrupt latency is typically 15 to 20 CTC clock cycles, and no retraces are missed at SafeMargin = 120. My COMSPEC points to a file on a RAM drive - if my command processor was on the hard disk, and was uncached, the interrupt latency due to the DOS EXEC call that invokes the command processor would probably make it impossible to determine the background latency. The DOS EXEC call could be replaced with a delay loop, to determine the background latency in this case (if you wanted to). Listing a directory increases the longest int 8 latency slightly, but few if any retraces are missed. But, running CHKDSK gives a longest latency of about 7000 to 8000 CTC clocks, and many missed retraces. With the SMARTDRV.SYS disk cache installed, after CHKDSK has run once, it does not need to physically access the hard drive again, and the maximum interrupt latency drops to about 80 CTC clocks, with no missed retraces (SafeMargin = 120). ## 10.16.2 SAMPLE PROGRAM: SIMULATING A VERTICAL RETRACE INTERRUPT -------------------------------- snip snip snip -------------------------------- NAME SAMPLE21 ; Sample program #21 ; Demonstrates a simulated vertical retrace interrupt ; Part of the PC Timing FAQ / Application notes ; By K. Heidenstrom (kheidens@actrix.gen.nz) ; ; This program assembles into SAMPLE21.COM, a program which implements an ; simulated vertical retrace interrupt using CTC channel 0. It installs its ; interrupt handler, and shells to DOS, allowing other programs to be run ; while its interrupt handler is installed. The interrupt handler causes the ; screen text to move up and down, in a seasickness-inducing fashion. ; ; This program requires a VGA card able to operate in video mode 3 (80x25, ; colour mode) but does not check that this is present. ; ; Save this file to SAMPLE21.ASM and assemble with: ; masm SAMPLE21; ; link SAMPLE21; ; exe2bin SAMPLE21.exe SAMPLE21.com ; or ; tasm SAMPLE21; ; tlink /t SAMPLE21; ; ; The techniques used in this program cannot safely be used in a TSR or a ; program that shells to DOS in a general way to run any DOS program. This ; technique is intended to be used as part of an application, where the ; behaviour of the 'foreground' code is known and controlled as much as ; possible. Though this program does shell to DOS, it is not intended to be ; used to run all types of programs. The shell to DOS feature is just to ; demonstrate that the screen updates are in fact being done under interrupt. ; ; This program can be assembled with or without the performance monitoring and ; reporting capability. Set the REPORT conditional to 0 for no performance ; monitoring and reporting, or to 1 for performance monitoring and reporting. ; Additional code in the int 8 handler is enabled if REPORT is enabled. ; The performance and behaviour monitoring functions will often be useless in ; production code. REPORT = 1 ; Enable for report stuff Code SEGMENT ASSUME cs:Code,ds:Code ORG 100h Main: jmp Main2 SignOnMsg DB 13,10,"SAMPLE21 -- Demonstrates simulated vertical retrace interrupt",13,10 DB "Part of the PC Timing FAQ / Application notes",13,10 DB "By K. Heidenstrom (kheidens@actrix.gen.nz)",13,10,13,10 DB "Usage: SAMPLE21 [Safety-margin]",13,10,13,10 DB "This program assumes, but does not check for, a VGA card in 80x25 mode",13,10,13,10 DB "Type EXIT at the DOS prompt to quit this program",13,10,13,10,"$" ComspecMsg DB "SAMPLE21: Can't locate COMSPEC in environment",13,10,"$" IF REPORT Msg0 DB 13,10," Chosen safety margin: $" Msg1 DB " CTC clocks",13,10," Measured field time: $" Msg2 DB " CTC clocks",13,10," Total retraces: $" Msg3 DB 13,10,"Missed retrace starts: $" Msg4 DB 13,10,"Longest int 8 latency: $" Msg5 DB " CTC clocks",13,10," Longest retrace wait: $" Msg6 DB " loops",13,10,"Shortest retrace wait: $" Msg7 DB " loops",13,10,"$" ENDIF ComSpecTxt0 DB "COMSPEC=" ; Text to find COMSPEC in environment ComSpecTxtL = $ - ComSpecTxt0 ; Length of same ComspecPtr DW 0 ; Pointer to COMSPEC in environment ALIGN 2 ExecParmBlock: ; EXEC parameter block EnvirSeg DW 0 ; Segment-paragraph of environment DW ShellCommand ; Pointer to command line SetToCS1 DW 0 ; Segment part for above DW 5Ch ; Let it use our FCBs SetToCS2 DW 0 ; Segment part again DW 6Ch ; Ditto SetToCS3 DW 0 ; Ditto ShellCommand DB 0,13 ; Command tail length and contents IF REPORT ReportTbl DW MsgPrint,Msg0 DW PrintDec,SafeMargin DW MsgPrint,Msg1 DW PrintDec,FieldPeriod DW MsgPrint,Msg2 DW PrintDec,Retraces DW MsgPrint,Msg3 DW PrintDec,MissedStarts DW MsgPrint,Msg4 DW PrintDec,MaxLatency DW MsgPrint,Msg5 DW PrintDec,Longest DW MsgPrint,Msg6 DW PrintDec,Shortest DW MsgPrint,Msg7 DW Continue,0 ENDIF SafeMargin DW 120 ; Interrupt 100us early (default) FieldPeriod DW 0 ; Number of CTC clocks in each field ; (frames per second = 1,193,181.66666... / FieldPeriod) LastCTC DW 0 ; Last CTC count programmed Latency DW 0 ; Actual latency for this interrupt Int8Sched DW 0 ; Scheduler for calling BIOS int 8 IF REPORT First DW 1 ; Flag whether first retrace Retraces DW 0 ; Count of retraces MissedStarts DW 0 ; Count of missed retrace starts MaxLatency DW 0 ; Worst int 8 delay (CTC clocks) Longest DW 0 ; Longest retrace wait (loops) Shortest DW 0FFFFh ; Shortest retrace wait (loops) ENDIF Cycler DW 0 ; Cycle control variable ; The sinewave table was created using the following GW-BASIC program using a ; number of entries of 64, range of values of 16, and centre offset of 7.5. ; The program generated one '16' in the middle of the '15' values, but I just ; manually fixed this to 15. A value of 16 causes the screen to jump. ; ;10 PRINT"This program will generate a sinewave table. The table is written to" ;20 PRINT"a disk file called SINE.DMP. The file is in text form, and contains" ;30 PRINT"one line per entry, in ASCII decimal representation. All entries are" ;40 PRINT"integers. Parameters required are: number of entries in table, range" ;50 PRINT"of values (peak to peak), and centre offset. One cycle of sine wave" ;60 PRINT"is written.":PRINT:INPUT"Number of entries in table :",NE# ;70 INPUT"Peak to peak value range :",PP#:INPUT"Zero offset :",ZO# ;80 OPEN "SINE.DMP" FOR OUTPUT AS#1 : A# = 0 : I# = 6.283185307179586#/NE# ;90 FOR P = 1 TO NE# : S# = SIN(A#) : V = INT((S# * PP# / 2) + ZO# + .5#) ;100 PRINT #1,V : A# = A# + I# : NEXT : CLOSE #1 : SYSTEM CycleTbl DB 8,8,9,10,11,11,12,13,13,14,14,15,15,15,15,15 DB 15,15,15,15,15,15,14,14,13,13,12,11,11,10,9,8 DB 7,7,6,5,4,4,3,2,2,1,1,0,0,0,0,0 DB 0,0,0,0,0,0,1,1,2,2,3,4,4,5,6,7 Main2 PROC near cld ; Upwards string direction mov si,81h ; Command tail Loop1: lodsb ; Get character cmp al,13 ; C/R yet? je NoParam ; If so cmp al," " ; Whitespace? jbe Loop1 ; Loop if so ; Parse decimal number parameter to replace default SafeMargin value xor bx,bx ; Clear calculated value ReadNumLp: sub al,"0" ; Convert "0"-"9" to 0-9 cmp al,9 ; Check for valid char ja ReadNumFin ; If not, terminator cbw ; Zero AH xchg ax,bx ; New digit to BL, old total to AX mov dx,10 ; Ten to unused register mul dx ; Multiply old value by ten add bx,ax ; Add to new digit lodsb ; Read char from command tail jmp SHORT ReadNumLp ; Loop for more ReadNumFin: mov [SafeMargin],bx ; Store adjustment value NoParam: mov es,[ds:2Ch] ; Get segment of environment mov [EnvirSeg],es ; Set up for command processor xor di,di ; Start at start of environment ScanEnvLoop: mov si,OFFSET ComSpecTxt0 ; Point at 'COMSPEC=' mov cx,ComSpecTxtL ; Get length to compare push di ; Keep pointer to start repe cmpsb ; Compare to 'COMSPEC=' pop cx ; Restore pointer to start je GotComspec ; If found it mov di,cx ; Go to start of entry again mov cx,8000h ; Maximum length to scan xor al,al ; Null terminator to scan for repne scasb ; Scan for null terminator jne EnvirError ; If error in environment cmp BYTE PTR es:[di],0 ; Final entry in environment? jne ScanEnvLoop ; If not, keep looking EnvirError: mov dx,OFFSET ComspecMsg ; Point to message mov ah,9 int 21h ; Display it mov ax,4C01h ; Errorlevel 1 int 21h int 20h ; In case DOS-1 (!) GotComspec: mov [ComspecPtr],di ; Store offset into environment mov [SetToCS1],cs ; Set up segment-paragraphs in EXEC mov [SetToCS2],cs ; parameter block for command mov [SetToCS3],cs ; interpreter ; Relocate stack and shrink memory allocation push cs pop es ; ES to Code mov sp,OFFSET StackTop ; Relocate stack mov bx,OFFSET FreeSpace+15 ; Account for partial paragraph mov cl,4 ; Shift count shr bx,cl ; Shift to paragraph count mov ah,4Ah int 21h ; Shrink memory to minimum necessary ; First, set the VGA card to the required mode. It must be a colour mode, ; otherwise the retrace flag appears in a different I/O port and the code ; will fail. Any required mode tweaking would be done here, too. mov ax,3 ; Screen mode int 10h ; Set screen mode mov dx,OFFSET SignOnMsg ; Point to sign-on message mov ah,9 int 21h ; Display it ; Set CTC channel 0 for a known mode and reload value - mode 2, 65536. cli ; No interrupts here please mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin out 43h,al ; Prepare channel 0 for new divisor jmp SHORT $+2 ; Short delay xor al,al ; Divisor is 0 (65536) out 40h,al ; Write lobyte of divisor jmp SHORT $+2 ; Short delay out 40h,al ; Write hibyte of divisor ; Time the number of CTC clocks between two retraces call StampRetrace ; Load the processor cache call StampRetrace ; Wait for start of retrace, read CTC mov bx,ax ; Keep it call StampRetrace ; Do the same again sub ax,bx ; Calculate difference mov [FieldPeriod],ax ; Store retrace period (in CTC clocks) ; Calculate the value to be programmed into CTC channel 0 from now on sub ax,[SafeMargin] ; Subtract the desired safety margin mov [LastCTC],ax ; Store as last programmed value ; Program the timer to interrupt just before the next retrace starts xchg ax,dx ; To DX mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin out 43h,al ; Prepare channel 0 for new divisor jmp SHORT $+2 ; Short delay mov al,dl ; Lobyte of divisor out 40h,al ; Write lobyte of divisor jmp SHORT $+2 ; Short delay mov al,dh ; Hibyte of divisor out 40h,al ; Write hibyte of divisor sti mov ax,3508h int 21h ; Get int 8 vector mov [Old8Ofs],bx ; Store offset mov [Old8Seg],es ; Store segment mov dx,OFFSET New8 ; Point to new handler mov ax,2508h int 21h ; Set vector push cs pop es ; ES back to Code ; Now execute the command processor mov bx,OFFSET ExecParmBlock ; Point to EXEC parameter block mov dx,[ComspecPtr] ; Get offset to command specification mov ds,[EnvirSeg] ; Get segment of environment ASSUME ds:nothing mov ax,4B00h int 21h ; Execute command interpreter push cs pop ss ; Restore SS mov sp,OFFSET StackTop ; Reset stack push cs pop ds ; Restore DS ASSUME ds:Code ; Restore VGA CRTC register 8 to its default value mov dx,3D4h ; Address VGA CRTC mov ax,8 ; Register number and value (0) out dx,ax ; Restore it ; Restore normal mode and divisor in CTC channel 0 cli ; No interrupts around this bit mov al,00110110b ; Channel 0, lobyte/hibyte, mode 3 out 43h,al ; Prepare channel 0 for new divisor jmp SHORT $+2 ; Short delay xor al,al ; Divisor is 0 (65536) out 40h,al ; Write lobyte of divisor jmp SHORT $+2 ; Short delay out 40h,al ; Write hibyte of divisor sti ; Interrupts are OK now lds dx, [DWORD PTR Old8Ofs] ; Get old int 8 handler ASSUME ds:nothing mov ax,2508h int 21h ; Restore int 8 vector push cs pop ds ; DS back to Code ASSUME ds:Code ; Generate report if REPORT conditional enabled IF REPORT cld ; Just make sure mov si,OFFSET ReportTbl ReportLp: lodsw ; Handler address xchg ax,cx ; To CX lodsw ; Parameter xchg ax,bx ; to BX mov ax,[bx] ; Get value (if applicable) mov dx,bx ; Pointer to DX call cx ; Call handler jmp SHORT ReportLp ; Loop Continue: pop ax ; Fix up stack ENDIF mov ax,4C00h int 21h Main2 ENDP ; This function waits for the start of a vertical retrace then reads the count ; in progress in CTC channel 0. It assumes a VGA card, running in a colour ; mode. It also assumes CTC channel 0 is operating in lobyte-hibyte access ; mode and operating mode 2, and returns the count in AX, converted to an ; up-count. It first waits for any retrace currently in progress to end, then ; waits for the next retrace to start and immediately reads the CTC count. ; This function must be called with interrupts disabled. Destroys AX and DX. StampRetrace PROC near mov dx,3DAh ; VGA status port in colour modes WaitRetr1: in al,dx ; Read status test al,00001000b ; Check retrace flag jnz WaitRetr1 ; If set, we are already in a retrace WaitRetr2: in al,dx ; Read status test al,00001000b ; Check retrace flag jz WaitRetr2 ; If clear, keep waiting for retrace xor al,al ; Command to latch channel 0 out 43h,al jmp SHORT $+2 ; Short delay in al,40h ; Read lobyte of count in progress jmp SHORT $+2 ; Short delay mov ah,al ; Keep it in AH in al,40h ; Read hibyte of count in progress xchg al,ah ; To correct registers neg ax ; Convert to up-count ret ; Return in AX StampRetrace ENDP ; This function prints AX in ASCII decimal representation. Output is via DOS ; function 2. AX, BX, CX, and DX are all destroyed. PrintDec PROC near xor cx,cx ; Zero digit counter PrintDec1: xor dx,dx ; Clear high word of value in DX|AX mov bx,10 ; Base div bx ; Divide by 10 add dl,"0" ; DL is remainder, convert to ASCII push dx ; Store on stack inc cx ; Increment char counter test ax,ax ; Any more digits left? jnz PrintDec1 ; If so, loop PrintDec2: pop dx ; Get char back mov ah,2 ; Print char int 21h ; Call DOS loop PrintDec2 ; Loop for all chars ret ; Done PrintDec ENDP MsgPrint PROC near ; Print message pointed to by DX mov ah,9 int 21h ; Print message ('$' terminated) ret MsgPrint ENDP ; The following function is the replacement int 8 handler. There is a lot of ; conditional code that is enabled by the REPORT conditional. The version ; with reporting is very instructive, and useful during development, but you ; may prefer to base production code on the version without the performance ; monitoring code. ASSUME ds:nothing New8 PROC far ; New int 8 handler cli ; Make sure push ds push cs pop ds ; Address this segment with DS ASSUME ds:Code push dx push cx push bx push ax pushf cld ; Ensure DF is known ; Read count in progress in the CTC to CX xor al,al ; Command to latch channel 0 out 43h,al jmp SHORT $+2 ; Short delay in al,40h ; Read lobyte of count in progress jmp SHORT $+2 ; Short delay xchg ax,cx ; To CL in al,40h ; Read hibyte of count in progress mov ch,al ; To CH - now CX = count in progress ; Now have count in progress, in CX. Calculate the latency on this interrupt ; invocation. This can be determined from the reload value last programmed ; into the CTC (which is stored in LastCTC). The difference between LastCTC ; and the count in progress, is the latency. This value is left in CX. ; If reporting, update the MaxLatency variable if appropriate. neg cx ; Convert count in progress to negative add cx,[LastCTC] ; Now have latency for this interrupt mov [Latency],cx ; Store as measured latency IF REPORT cmp cx,[MaxLatency] ; Update MaxLatency jbe NotWorse ; If not exceeded current value mov [MaxLatency],cx ; If exceeded, update ENDIF ; Check for this interrupt handler being entered too late. This occurs if a ; retrace was already in progress when the interrupt routine was entered, or ; if the measured latency is significantly greater than the SafeMargin value ; (at least, say, 10 or 20 CTC clocks later, to allow for timing alignment ; errors - I have chosen 20 CTC clocks; anything less than this will always ; be picked up by the retrace being already active). ; ; If this occurs, the interrupt was delayed longer than the SafeMargin value, ; and the start of the retrace interval (and possibly the whole retrace pulse) ; has been missed. ; ; The logic is that either the interrupt was accepted in time, in which case ; we will wait for the start of retrace and reset the CTC with the correct ; delay again, or the interrupt was delayed past the start of retrace (and ; possibly even past the end of retrace!) In this case, we must find out ; how 'late' we are, not wait for the start of retrace (as it has already ; started and may even have finished), and program the CTC with an adjusted ; delay (adjusted downwards), so that the next interrupt will be signalled ; on schedule. ; This technique does not resynchronise the CTC interrupt to the video system, ; and does not include compensation for the delays in the code, so if retraces ; are missed repeatedly, the timing of the interrupts is likely to drift. ; After the first successful interrupt entry, however, the CTC will be ; resynchronised to the video retrace. NotWorse: xor bx,bx ; Zero loop counter / flag for later mov dx,3DAh ; VGA status port in al,dx ; Get status test al,00001000b ; Check for retrace jnz MissedRetrace ; If active, we missed the start mov ax,[SafeMargin] ; Get ideal interrupt acceptance delay add ax,20 ; Get maximum expected safety window cmp cx,ax ; Measured interrupt acceptance delay jae MissedRetrace ; Oh dear, we missed it completely! ; The latency (interrupt acceptance delay) was comfortably smaller than ; SafeMargin, and retrace is not active, so presumably the interrupt was ; accepted within the safe period. We can now wait for the retrace to ; start, then reprogram the CTC with the standard delay (from FieldPeriod ; minus SafeMargin). In report mode, count the loops while waiting. Retrace1: IF REPORT cmp bx,0FFFFh ; Check for overflow adc bx,0 ; Increment if not ENDIF in al,dx ; Read status test al,00001000b ; Check retrace flag jz Retrace1 ; If clear, keep waiting for retrace ; We have just successfully completed the wait-for-retrace loop. If in report ; mode, update longest and shortest wait times but only if this is not the ; first retrace. IF REPORT cmp [First],0 ; Is it the first retrace? jnz IsFirst ; If so cmp bx,[Longest] ; Longer than longest? jbe NotLonger ; If not mov [Longest],bx ; If so NotLonger: cmp bx,[Shortest] ; Shorter than shortest? jae NotShorter ; If not mov [Shortest],bx ; If so IsFirst: mov [First],0 ; Reset First flag if set NotShorter: ELSE inc bx ; Flag that retrace was safe ENDIF mov cx,[FieldPeriod] ; Total field time minus safe margin sub cx,[SafeMargin] ; Prepare value to load into CTC jmp SHORT ResetCTC ; Go to set up CTC ; We missed the start of retrace because the interrupt was delayed, probably ; by some foreground code locking interrupts out for a long time. This can't ; be helped now, but we must adjust the value programmed into the CTC to ; trigger the next interrupt, so that it will interrupt proportionally sooner, ; otherwise we will miss the next retrace, etc. ; Calculate the new value to be programmed into the CTC. This is simply the ; retrace period (FieldPeriod) minus the measured latency, which is already in ; CX from earlier calculations. This gives an adjusted value to load into the ; CTC for the next delay, so that it will interrupt at roughly the correct ; point next time. MissedRetrace: IF REPORT inc [MissedStarts] ; Flag we missed a retrace start ENDIF neg cx ; Get minus interrupt acceptance delay add cx,[FieldPeriod] ; Get adjusted CTC load value ; At this point, we have either missed the start of retrace and calculated a ; reduced value to load into the CTC for the next delay, or we have just had ; the start of retrace and have the standard value (FieldPeriod - SafeMargin) ; to load into CTC channel 0. CX contains the value to be loaded into the CTC ; to determine the delay from now until the next interrupt is signalled. ; Reset and restart the CTC using this value. ResetCTC: mov [LastCTC],cx ; Store as last programmed value mov al,00110100b ; Channel 0, lobyte/hibyte, mode 2, bin out 43h,al ; Prepare channel 0 for new divisor jmp SHORT $+2 ; Short delay xchg ax,cx ; Get CTC count value out 40h,al ; Write lobyte of divisor jmp SHORT $+2 ; Short delay mov al,ah ; Hibyte of divisor out 40h,al ; Write hibyte of divisor ; Set carry flag according to whether retrace missed, and call RetraceFunc. ; BX was cleared before the test for retrace already started, so if retrace ; had already started (i.e. retrace start missed), BX will still be zero. ; If not, BX will be at least 1, as it is incremented in the wait loop (if ; reporting is enabled) or explicitly (if reporting is not enabled). cmp bx,1 ; Set carry if retrace had started call RetraceFunc ; Do retrace stuff ; Increment retrace count (but not above 0FFFFh) IF REPORT cmp [Retraces],0FFFFh ; Check for overflow adc [Retraces],0 ; Increment if not ENDIF ; Either chain to BIOS int 8 handler, or send EOI to PIC and return from ; interrupt. The decision is made via the Int8Sched variable, which is ; incremented by the number of CTC clocks in each field (FieldPeriod). ; If it carries, the BIOS int 8 handler is called. Otherwise, we just send ; an EOI and return from interrupt. ; In production code, this logic could be modified to remove the Int8Sched and ; the conditional chain to the BIOS int 8 handler, and always send the EOI and ; return. If this is done, the system time will stop updating while the handler ; is installed. There is little to be gained by doing this, as the interrupt ; rate is not very high, so I suggest leaving the chaining code intact. mov ax,[FieldPeriod] ; Get number of CTC clocks elapsed add [Int8Sched],ax ; Add into BIOS int 8 scheduler variable cli ; Don't allow stack growth jc CallOld8 ; If it carried, chain to the BIOS mov al,20h ; EOI command out 20h,al ; Send to primary PIC popf ; Restore registers pop ax pop bx pop cx pop dx pop ds ASSUME ds:nothing iret CallOld8: popf ; Restore registers pop ax pop bx pop cx pop dx pop ds DB 0EAh ; JMP xxxx:xxxx Old8Ofs DW 0 ; Offset of BIOS int 8 handler Old8Seg DW 0 ; Segment of BIOS int 8 handler New8 ENDP ; RetraceFunc is called by the replacement int 8 handler on every retrace, just ; shortly after the start of retrace (unless the start of retrace was missed, ; see shortly). On entry, the main segment is addressable via DS, the direction ; flag is clear, and interrupts are disabled - they may be enabled within ; RetraceFunc, but since IRQ0 (the highest priority interrupt) is in progress, ; no other interrupt sources will get through anyway, so there is no point in ; issuing an STI. The flags and the four scratchpad registers (AX, BX, CX, and ; DX) may be destroyed, but any other registers must be preserved - specifically ; BP, SI, DI, and ES must be preserved. The int 8 handler does not perform a ; stack switch, so stack usage must be kept to a minimum. On entry, the carry ; flag indicates whether a full retrace period is available, and the Latency ; variable can be used to determine how much time remains if the full period ; is not available. ; The timer interrupt normally triggers a certain time (set by SafeMargin) ; prior to the start of a retrace, giving a safety margin in case interrupts ; are locked out and the timer interrupt is not actioned immediately when it ; is signalled by CTC channel 0. If interrupts are locked out for more than ; the safety margin period, the timer interrupt may be delayed until after ; the start of retrace, possibly even until after the end of the retrace pulse. ; The int 8 handler detects this condition, and sets the carry flag on entry to ; RetraceFunc if this occurred. Normally, carry will be clear on entry to ; RetraceFunc. ; This function may make use of the Latency variable, which contains the ; number of CTC clocks by which the current interrupt was delayed. Typically ; this will be in the order of 15 to 20, but it will be much higher if this ; interrupt entry was delayed by interrupts being disabled by foreground ; code. If the time between the start of retrace and the start of the next ; visible scan is known, it is possible to use the Latency variable to find ; the amount of time remaining before the visible scan starts. ; See the explanatory text for this program for more details. ; ; This function would be changed to a user-specific function. ; ; This function must obey the normal guidelines for hardware interrupt handlers, ; for example it must not try to call any DOS functions. Some BIOS functions ; are generally safe to call from hardware interrupt handlers, but in general, ; special operations such as page flipping, palette changing, font programming, ; etc should be done at a hardware level, and the mainline code should be aware ; that these operations are being done 'in the background' if there may be some ; interaction. RetraceFunc PROC near pushf ; Preserve carry flag mov bx,[Cycler] ; Get current point inc bx ; Bump offset and bx,3Fh ; Mask mov [Cycler],bx ; Store back popf ; Restore carry flag jc DontUpdate ; If missed start of retrace mov ah,[CycleTbl+bx] ; Get position for this cycle step mov dx,3D4h ; Address VGA CRTC mov al,8 ; Register number out dx,ax ; Set vertical start position DontUpdate: ret RetraceFunc ENDP DB 256 DUP(?) ; Stack space StackTop = $ ; Top of stack point FreeSpace = $ ; End of memory required Code ENDS END Main -------------------------------- snip snip snip -------------------------------- ## 10.16.3 DOUBLE AND TRIPLE BUFFERING Thanks to Paul Ross (pa-ross@uwe.ac.uk) for his help with this subject. Double buffering uses two screen buffers. While one buffer is being displayed, the other buffer is being updated with data for the next frame. The video card is told to change to the other buffer only during a vertical retrace, so the animation is smooth and flicker-free. The general flow is as follows: while (1) { Generate next frame using currently non-displayed buffer; Wait for vertical retrace to begin; Tell video card to swap to other buffer; } There is no requirement for a vertical retrace interrupt, because the software simply creates a buffer of data then waits until the retrace starts, then flips pages and starts creating the next buffer, and so on. If one frame of picture data can be generated in less than one vertical scan, the buffer alternates every retrace, and the frame update rate is equal to the vertical scan rate, i.e. 70 frames per second (or whatever). The software wastes time waiting for the vertical retrace, but if the software is still able to keep up with the maximum frame display rate of the video hardware, this is not a problem. But if it takes slightly longer than one frame to generate the next frame, a lot of time is wasted in the loop waiting for retrace. These diagrams may make more sense (if viewed on a monospaced display). The first diagram shows the software able to generate a new frame more quickly than the video card's frame rate: Retraces: ! ! ! ! Software: 1111111111wwwwwf2222222222wwwwwf1111111111wwwwwf22222... Display: 222222222222222 111111111111111 222222222222222 11111... Key: 1 = Generating data for, or displaying, buffer 1 2 = Generating data for, or displaying, buffer 2 w = Waiting for vertical retrace f = Flipping pages on video card The above diagram showed the buffers alternating every retrace, giving the maximum displayable frame update rate. The next diagram shows what happens with double buffering when it takes longer than one displayed frame for the software to create data for the next frame: Retraces: ! ! ! ! ! ! ! Software: 1111111111wwwwwf2222222222wwwwwf1111111111wwwwwf22222... Display: 2222222 2222222 1111111 1111111 2222222 2222222 11111... As you can see, if the software is too slow to keep up with the frame rate of the video card (as is often the case), the same frame will be displayed twice or three times (or whatever), while the software is creating the next frame. Once the software has a new frame ready, it then starts _waiting for the start of the next frame_, wasting up to nearly a whole frame time doing nothing. If the code takes, say, 1.3 frames to generate a picture, it will always flip pages every two frames, because it can't do anything while waiting for the retrace. So the screen updates are always evenly spaced (assuming that each frame takes the same amount of time to generate), but if you could use that waiting time, you could actually flip pages on two out of every three frames, like this: Retraces: ! ! ! ! ! ! ! Software: 111111111122222f2222233f3333333f111111111222222222333... Display: 3333333 3333333 1111111 2222222 3333333 3333333 11111... Comments: ^1ready ^2ready ^3ready ^1ready So you get the page flips spaced unevenly, but the frame rate goes up. This is called triple buffering, and it helps by allowing you to get a higher frame rate but with uneven frame timing. For example if it takes 1.3 frames of time to generate a new frame of data, with double buffering you would spend 0.7 frames every two frames (i.e. 35% of the processor time) waiting for the next retrace, so you would get one new frame of data every two scans, i.e. 35 fps. But with triple buffering, you could start creating a new frame during that 0.7 of a frame, so that it could be ready sooner. So you would get unevenly spaced frames, but a higher frame rate. To implement triple buffering, first of all you must use a video mode where three (or more) pages are available. Then to detect retrace, you can either poll the card (but remember the retrace pulse may be only about 64 us wide!), or use some method based on polling the CTC, or use the vertical retrace interrupt or an emulated vertical retrace interrupt. Using the vertical retrace interrupt or emulated vertical retrace interrupt method, your mainline (frame data generation) must keep some variable to show which frame contains the most recent valid data, and a flag to say that a new frame is available, which could be combined with the other variable. The interrupt routine, which is triggered every retrace, would then check to see if a new frame is available, and if so, flip pages to enable the most recently updated page to be displayed. So the mainline code flow would be something like: Set newframe variable to -1; Set workframe variable to 0; while (1) { Generate frame in buffer specified by workframe variable; Set newframe variable to workframe variable; Increment workframe variable modulo 3 (0,1,2,0,1,2,0...); } And the vertical retrace interrupt handler flow would be: If newframe variable is not -1 { /* Only flip if new data available */ Flip displayed page to be equal to newframe variable; set newframe variable to -1; } ## 11 QUESTIONS AND ANSWERS Well, since this is supposed to be a FAQ, I suppose I should include some frequently asked questions and my answers to them :-) Most of these questions are from Usenet newsgroups alt.msdos.programmer, comp.os.msdos.programmer, comp.lang.asm.x86, and related newsgroups. I have paraphrased most of them. ## 11.1 TIMING ACCURACY ---------- > What is the inherent inaccuracy in DOS's timekeeping and how can it be > avoided in an application where long term time accuracy is important? There are 1,573,042.24 ticks in a day, but when the BIOS was written, the 1.19318166666... MHz frequency was approximated to 1.193180 MHz, so the BIOS writers used 1,573,040 (001800B0 hex) ticks per day. This contributes a 'by-design' error of 1.42166 parts per million, but this is swamped by the error due to initial accuracy, temperature stability, and long term drift in the 14.31818 MHz system clock crystal, which is 5 ppm for a good quality crystal, and maybe 50 ppm for the crap ones that are often found in cheap PCs. One solution would be to write a DOS device driver for the CLOCK$ device, which accesses the RTC (either directly, or through the BIOS functions), so that the DOS time no longer relates to the time maintained by the BIOS in the timer tick count variable. However, the errors (initial, temperature, and long term) in the clock frequency of the RTC are probably going to be unacceptable also, unless your motherboard has a trimmer capacitor to fine-tune the oscillator frequency, and you have _lots_ of time to spend adjusting it :-) Also the RTC only has a resolution of one second. If you really need high accuracy, there are several approaches. þ Measure the accuracy over a one day or one week period and install an adjustment factor in the software to compensate for the initial frequency error (has to be done individually for each machine that will run the software). This method doesn't help against temperature and long-term drift. þ Install a more accurate crystal or a high quality crystal oscillator module. þ Use an external frequency source - either a clock controlled from a high quality crystal in a temperature controlled environment (crystal oven) or something derived from an external clock source (such as the mains frequency, or perhaps radio time signals?), into an input such as the parallel port ACK line which can generate an interrupt. ---------- > I want to implement a 10 millisecond clock, i.e. an interrupt every 10 ms. > The PIT clock is 1.19318 Mhz, so a count of 11931 will give an interrupt > at a rate of 11931/1193180 = 9.99933 ms. Using a divisor of 11931, I counted > interrupts over a long period and got 9.99849 ms per tick. The PIT clock is 14.31818/12, or 1.19318166666.... MHz. The absolute accuracy is normally better than +/- 100 ppm, often under +/- 10 ppm, depending on the accuracy of the crystal. Modern motherboards may not use accurate crystals, because there is not normally any reason to - the RTC determines the long-term accuracy and this is is clocked separately and read on every reboot. Try again using the correct value for the timer clock frequency - this should give a closer result, but you may not be able to get the accuracy you need. > I tried a count of 11932. This should give a tick interval of greater > than 10 ms, but instead, I get the 9.99933 ms tick interval I expected > with a count of 11931. Even worse, all of this happens only on some > machines; others work as expected. Clock frequencies vary from machine to machine, also with age and temperature, again depending on the quality of the crystal used. If the program just has to run on one machine, and its clock frequency is slightly off but at least stable, you may be able to calculate the actual clock frequency and modify the program to accommodate it. Also, you can get non-integer division by alternating or cycling the reload value between two different numbers, e.g. using 11930 on one cycle then 11931 on the next to get 11930.5 (long term, that is :-) One more thing - are you maintaining the BIOS timer tick interrupt? It is supposed to be called every 65536 clocks. You can use a 16-bit scheduler variable, and on every 10ms interrupt, add 11930 (or whatever you used) to the scheduler variable, and when the add causes a carry, 65536 clocks have elapsed so you should chain to the old int 8 handler rather than sending an EOI and returning. ## 11.2 TIMER INTERRUPTS (INT 8, INT 1CH, RTC INTERRUPT) ---------- > If there are no TSRs hooking into it, what does the timer-tick interrupt do > other than being used for counting the number of ticks since midnight? The traditional functions of int 8 (the hardware timer tick interrupt) are (a) updating the BIOS tick count variable which is used by DOS to determine the time of day, and setting the midnight flag if a midnight has passed, and (b) turning off the floppy drives after about two seconds since the last access. BIOSes _may_ use int 8 for anything else that they like. For example they _could_ use int 8 for green functions (e.g. spinning down the hard drive if it has not been accessed for a while on a laptop or killing the video drive if no video accesses have been made). I am not saying that BIOSes _do_ this, just that it is their perogative to do this, so it's not safe to assume that int 8 is only used to update the tick count and turn off the floppy drives. Also, any number of TSRs and device drivers, such as screen savers and disk caches, could be using int 8 and/or int 1Ch. ---------- > I have seen TSRs that hook int 1Ch rather than int 8, this implies that an > application program should chain to the previous handler if it uses int > 1Ch unless it has a good reason not to do so. My understanding is that int 1Ch is intended for use by user programs only, and that it should be neither necessary, nor desirable, to chain to the original handler, as the original handler is just an IRET. The user program's only obligation should be to restore the vector when it terminates. However, some TSR writers obviously didn't think this way (or maybe just didn't _think_ :-) so there are TSRs that hook int 1Ch. For their benefit your application can and should chain int 1Ch. But I do not believe TSRs should use int 1Ch. ---------- > What are the advantages of using int 8 versus int 1Ch? Documents I've read > recommend using int 1Ch. Why would you use int 8 instead? It depends what you want to do with the interrupt. If you just want a 54.9254 ms regular interrupt in an application program (i.e. not a TSR), you can use either. If you are writing a TSR, you should use int 8, not int 1Ch, because int 1Ch is intended for use by user programs, and a TSR is not a user program, it is more like an operating system extension, and a user program is within its rights to come along and hook int 1Ch without chaining to your handler. In this case (using int 8 in a TSR), you must chain to the original handler. The simplest way is just to JMP to it at the end of your intercepting code. If you are modifying the timer tick rate, or doing vertical retrace emulation, or anything clever with the timer, you must use int 8, and ensure that the old int 8 handler is chained at appropriate intervals. This technique cannot safely be used inside a TSR because an application is at liberty to pull the same tricks and break the TSR. In all cases, keep the amount of time spent in the interrupt handler to a minimum. ---------- > How can I increment a variable once every second, under interrupt? Timed interrupts on the PC can be generated via channel 0 of the timer chip (8253 or 8254) and via the real time clock (RTC). The timer cannot generate interrupts at one second intervals. It is normally operating at 18.2065 interrupts per second (this is called the 'timer tick'). You can hook into this timer tick interrupt (int 8 or int 1Ch if you're writing an application, int 8 only if you're writing a TSR). You can then count off interrupts and increment your seconds counter every 18.2065 interrupts. This is done by incrementing it after 18 or 19 interrupts, and alternating between 18 interrupts between increments, and 19 interrupts between increments, to give over the medium term or long term, one increment every 18.2065 interrupts. This requires some simple arithmetic. Of course this will cause the seconds variable to be incremented slightly unevenly. If that's acceptable, this is probably the best way to go. This technique can be used in an application or a TSR. If a slight unevenness in timing is not acceptable, you can reprogram timer channel 0 to operate at a different rate, such as, say, 50 ticks per second, and hook int 8, and call the old int 8 handler ('chain') 18.2065 times per second. The timer cannot generate exactly 50 interrupts per second with a single divisor value, but this can be achieved by dynamically reloading the timer divisor on each interrupt. Of course this method makes the calls to the old int 8 handler uneven, but this is not a problem for the software that uses this interrupt. You then can count off 50 fast interrupts and increment your seconds variable. However, this technique cannot safely be used in a TSR. The above techniques use the timer (8253/8254). If you know your program will always run on an AT or later, you can use the RTC. It is able to generate an interrupt every second, but this mode is not normally used. I've never tried using the update interrupt (once per second) but it should work, provided that you use the normal tricks to make sure the BIOS doesn't turn off the interrupt source. Alternatively, you could use the RTC at 1024 interrupts per second and count off the interrupts yourself. This technique will definitely work, though you are more likely to miss interrupts because they are happening at a faster rate. ---------- > What is the difference, and interaction, between the timer tick interrupt > and the real time clock's periodic interrupt? The timer interrupt is triggered by channel 0 of the timer chip, an Intel 8253 or 8254 or workalike. It is normally operated at 18.2065 interrupts per second (this is called the timer tick rate). The default handler is responsible for maintaining the system time (which is done through the BIOS Tick Count Variable in the BIOS Data Area) and turning off the floppy drive motors after two seconds of inactivity, and it is likely that some machines use it for other purposes too. The timer tick interrupt is IRQ0, which is int 8. This is the highest priority hardware interrupt request. As well as updating the time and turning off the floppy drive motors, the default handler issues int 1Ch on every tick. This interrupt is intended for use by user programs (not TSRs) as a regular interrupt source. TSRs and network software often intercept int 8 and use it for timing, timeout detection, regular updating, etc etc. The timer interrupt can be operated at a higher rate if desired (this technique cannot be used in a TSR). It can be programmed to occur at 1.19318166666...MHz divided by any integer from 2 to 65536 (very small divisors cause major overhead problems!). With trickery (the dynamic timer tick technique) it can be made to occur at a convenient rate, e.g. 1000 interrupts per second, etc. The program operating the timer at a higher rate must chain to the original handler at the correct rate, i.e. 18.2065 times per second. The timer and int 8 are present in all PC-compatible machines. The RTC is only present in the AT and later machines, which these days is at least 99% of the market. It is connected to IRQ8, which is int 70h. This is the highest priority interrupt on the slave interrupt controller, so it is third highest priority on the machine (highest and second highest are int 8, the timer tick, and int 9, the keyboard scancode interrupt). IRQ8 interrupt is generated by the RTC (Real Time Clock) chip, which also holds the machine's CMOS memory for storing BIOS settings. The interrupt can be programmed to occur at a particular time (through the Alarm function of the RTC), every second (the 'update' interrupt), or at one of the following rates (the periodic interrupt) - 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, or 8192 interrupts per second. The RTC interrupt is used by several BIOS functions, and the BIOS interrupt handler will sometimes turn off the interrupt, so when you hook this interrupt, you have to chain to the BIOS's handler then turn the interrupt source back on, just in case. ## 11.3 INTERRUPT PRIORITIES AND NESTING ---------- > While an interrupt handler is in progress, if the interrupt flag is cleared, > can the handler be interrupted by another hardware interrupt? Also, if an > interrupt handler takes a long time to run, can it be interrupted by itself > (for example, a keyboard interrupt handler)? No hardware interrupt will be accepted if the interrupt flag is clear, as it is on entry to an interrupt handler. But, it is normal for most interrupt handlers to enable interrupts via an STI instruction fairly early on, so that higher priority interrupts will be able to interrupt them. Hardware interrupts are prioritised by the 8259 interrupt controller(s). Lower IRQ numbers are higher priority (unless software has reprogrammed the interrupt controller modes). IRQ8-15 (not present on original PC and XT) fit in between IRQ1 and IRQ3. In other words, the priority order is: IRQ0 INT 8 Timer tick interrupt (HIGHEST PRIORITY) IRQ1 INT 9 Keyboard scancode interrupt IRQ2 INT 0Ah Uncommitted (see IRQ9) (ONLY PRESENT ON ORIGINAL PC AND XT) IRQ8 INT 70h RTC interrupt IRQ9 INT 71h Redirected IRQ2, uncommitted (COM ports, vertical retrace) IRQ10 INT 72h Unallocated IRQ11 INT 73h Unallocated IRQ12 INT 74h Bus mouse hardware interrupt IRQ13 INT 75h Math coprocessor IRQ14 INT 76h Hard disk (AT and later) IRQ15 INT 77h Unallocated IRQ3 INT 0Bh COM2, usually, or uncommitted IRQ4 INT 0Ch COM1, usually IRQ5 INT 0Dh Uncommitted (COM ports, sound cards, XT hard disk) IRQ6 INT 0Eh Floppy disk hardware interrupt IRQ7 INT 0Fh Parallel port, sound cards (LOWEST PRIORITY) If an interrupt handler is in progress, say on IRQ4 for example, and the handler enables interrupts using STI, then a higher priority interrupt, such as the keyboard scancode interrupt or timer tick interrupt, if signalled, _will_ interrupt the interrupt handler in progress. Once that higher priority interrupt has been processed, the lower priority interrupt handler will be resumed. But, a lower or equal priority interrupt will _not_ interrupt the handler in progress, until that handler has sent an EOI (end of interrupt) command to the interrupt controller(s) (first interrupt controller for IRQ0-7, both interrupt controllers for IRQ8-15). The EOI command is value 20h, and is sent to I/O port 20h for the first interrupt controller, and port 0A0h for the second interrupt controller. The EOI command tells the interrupt controller that the interrupt that was signalled by the interrupt controller (which provides the interrupt vector to tell the processor where the interrupt handler begins) is now finished with, and it resets the interrupt controller's logic. The interrupt controller will then signal any other interrupts that were pending. For example if an IRQ7 came along while IRQ4 was being processed, the interrupt controller would ignore it until the IRQ4 handler issued an EOI, then the interrupt controller would re-evaluate its pending interrupts and issue the highest priority pending interrupt, which would be IRQ7. To avoid the lower priority interrupt 'nesting' on top of the higher priority interrupt and causing stack growth, the EOI command is normally issued right at the end of the interrupt handler, and is issued with interrupts locked out, so that the interrupt handler will return to the main code before the lower priority interrupt is accepted. If you specifically want a particular interrupt priority to be able to interrupt itself, this is normally possible - just send the EOI at an early stage in the interrupt handler, and make sure interrupts are enabled. The interrupt controller thinks the interrupt handler has finished, so it will signal the interrupt again if the interrupt is triggered again during processing of the same priority interrupt. Of course this also lets lower priority interrupts through as well. -------- > The timer triggers IRQ0 (int 8) which has highest priority. Does this mean > that it really interrupts another lower priority interrupt, or does it only > mean that if there are several interrupts pending, IRQ0 will be chosen? First, no IRQ will interrupt _anything_ if the interrupt flag in the processor is clear (via CLI). This flag is also cleared automatically on entry to any interrupt handler, and must be explicitly set by the interrupt handler. Most software and hardware interrupt handlers will do this, unless they have some special reason for not doing so. If a lower priority interrupt is in progress, and the interrupt flag is set (interrupts are enabled), then a higher priority interrupt _will_ interrupt that interrupt handler. When the higher priority interrupt exits, the lower priority interrupt handler is resumed, in the normal way. If an interrupt of the same or lower priority occurs, it will not be serviced until the current interrupt handler has finished and sent an end of interrupt signal (more below). If interrupts are locked out via the interrupt flag in the processor, the interrupt controller chip will continually evaluate its inputs, keeping track of the highest priority pending input, and when the processor is able to accept the interrupt, the interrupt controller will first issue the highest priority interrupt. If a hardware interrupt request disappears while the interrupt controller is waiting for the processor to acknowledge its interrupt request (INTR), the interrupt controller is in the embarrassing position of having interrupted the processor but not having a valid interrupt request to issue. In this case, the interrupt controller issues an interrupt level 7. If the fleeting interrupt was on the primary interrupt controller (IRQ0, IRQ1, or IRQ3-7), this will cause IRQ7 (int 0Fh) to be executed. If the fleeting interrupt was on the secondary interrupt controller (IRQ8-15), this will cause IRQ15 (int 77h) to be executed. Any program handling IRQ7 and/or IRQ15 should be prepared for this possibility. The interrupt controller keeps track of the current interrupt priority. It knows when the interrupt priority changes to a higher priority, because it issued the interrupt request itself. It also knows when the higher priority interrupt ends, and a lower priority interrupt resumes, via the end of interrrupt command. > What is an EOI (end of interrupt) and what type should I use? Any hardware interrupt handler must notify the interrupt controller (or controllers, if it's IRQ8 or higher) when it has completed, so that the interrupt controller can keep track of interrupt levels in progress, etc. It does this partly through the EOI (end of interrupt) command. There are two types of EOI - the non-specific EOI and the specific EOI. Specific EOI is not often used, though any 8259 compatible interrupt controller should support it. It simply tells the interrupt controller that a specific interrupt handler has finished. The non-specific EOI tells the interrupt controller that the currently executing, highest priority interrupt handler has finished. The command is sent like this. The interrupt controller knows the highest priority executing interrupt level, because it generated the interrupt request and provided the vector. Assembler mov al,20h out 20h,al Micro$oft C outp(0x20, 0x20); Borland C outportb(0x20, 0x20); Pascal port[$20]=$20; GW-BASIC Just kidding :-) If the interrupt handler is for IRQ8 or higher, it must send an EOI command (0x20) to the secondary interrupt controller, at I/O address 0xA0, as well. I don't believe it really matters in what order these EOIs are sent in this case. If you are hooking int 8, then you should chain to the original int 8 handler, unless you have a special reason for not doing so. The original int 8 handler is part of the BIOS. It will send the EOI for you. ---------- > How do I tell if my timer tick interrupt handler is taking too much time, and > what would happen if the interrupt was to get called again while the handler > was still running from the first time? Until you send the EOI or chain to the original handler (in the case of int 8) or until you return (in the case of int 1Ch), the interrupt will not be called again while it is still running. > How are IRQ2 and IRQ9 related? I have run out of free IRQs except for IRQ2, > which I have kept free, since I have IRQ9 in use, and wanted to avoid any > problem. Can I use IRQ2? If you have IRQ9 in use, you are going to have trouble 'using' IRQ2 because the slot bus pin that was IRQ2 on the PC and XT is IRQ9 on later machines, so IRQ2 doesn't 'exist' any more :-) IRQ2 was just a standard interrupt on the PC and XT, with no assigned purpose (often used for extra COM ports or special hardware boards). The AT added a second interrupt controller (Intel 8259) which provides IRQ8 through IRQ15 inputs, but required a 'cascade' interrupt input into the main interrupt controller, and IRQ2 was chosen as the cascade interrupt. The slot bus pin that used to carry IRQ2 was fed into IRQ9, on the second interrupt controller, and BIOS and DOS were modified to software-redirect IRQ9 to IRQ2 so that many programs that were able to use IRQ2 would still work properly and be none the wiser on ATs when they would really be using IRQ9. The default IRQ9 handler sends the EOI to the secondary interrupt controller, then invokes the IRQ2 handler through the IRQ2 vector. When the IRQ2 handler sends its EOI to the primary interrupt controller, the IRQ9 is fully acknowledged. So that is the relationship between the two interrupts. IRQ2 is not accessible on the slot bus on ATs and later machines. This may not apply to MicroChannel motherboards, BTW, which were designed after the AT. ## 11.4 INTERRUPT HANDLER RESTRICTIONS ---------- > I'm writing a TSR that will make my computer beep several times when "RING" > is received from my modem. I want to make it a hook int 1Ch and make it > watch for "RING" using the BIOS serial functions interrupt 14h function 3, > then activate the beep. Is it safe to call int 14h from within an int 1Ch > handler? First, TSRs shouldn't use int 1Ch, use int 8 instead, and make sure you chain to the original handler. BIOS functions are nominally non-reentrant, but the int 14h services are so simple that provided the foreground program isn't using them to access the same serial port (very unlikely, as any decent comms software goes direct to hardware and doesn't use int 14h at all), you should be safe. But if you're using int 14h and calling it from within your int 8 handler, you won't call it quickly enough to catch the 'RING' string from the modem - you will get a receive overrun, unless you have an internal modem with an emulated serial port, which will hold the data until it is read. IOW, you should hook the serial interrupt for the serial port you're monitoring, and enable the serial interrupt, etc, so you get an interrupt when the modem sends something. Finally, how are you planning to program the beep? I suggest going direct to hardware, rather than using int 10h, which can often be non-reentrant. That means you must turn the sound on and off using the timer interrupt. ---------- > When I add a call to puts() in my timer interrupt handler, the machine > locks up or crashes with an EMM386 or QEMM exception. Why? Because puts() calls DOS and DOS is non-reentrant. When the timer tick interrupt is signalled, various parts of your computer's software and hardware may be 'busy', and calling most DOS functions and some BIOS functions will usually cause problems with reentrancy. If you want to output to the screen from within an interrupt handler, you either need to use TSR techniques to ensure that DOS or the BIOS is not busy, or write directly to screen memory. I find the latter technique more useful. -------- > Can I save to disk some data which I collect in my interrupt handler? Yes, absolutely. Especially if it's not a TSR. You can't write to disk from your int 8 handler, though - the BIOS might be in the middle of writing something else. There are lots of reasons why this would be very dangerous. Normally this would be done using a circular buffer, or 'queue', to pass data from your interrupt handler to your mainline. You have an area of memory (any size from a few bytes up to 32K or so) to be used circularly to store data, and have two pointers or offsets into the buffer, one being driven by the interrupt routine showing where the data is going in, and one controlled by the mainline which runs 'in the foreground' keeping track of data coming out of the circular buffer and being written to disk. Every time your interrupt routine puts data in the buffer, it 'bumps' the 'ingoing' pointer (increment pointer, check whether it has gone off the end of the buffer, and if so, reset it to the start of the buffer). Every time your mainline gets data out of the buffer, it bumps its outgoing pointer in the same way. If the two pointers are equal, there is no new data in the buffer. The interrupt handler should also handle a buffer overrun tidily, by checking for the 'ingoing' pointer crossing the 'outgoing' pointer and behaving accordingly (e.g. don't update the 'ingoing' pointer, and set a global variable somewhere that the mainline can detect, that indicates a buffer overflow). The mainline would put the data from the circular buffer in to a linear buffer and write that buffer to disk using the standard file I/O routines or DOS services when it gets full. ## 11.5 HIGH SPEED TIMER TICK ---------- > I need to trigger an analog to digital converter (which does not have its own > clock) 4000 times per second. This can be done by speeding up the timer tick, see section ¯¯ 8 and subsections. To get exactly 4000 interrupts per second, you need to use the dynamic tick period technique described in section ¯¯ 8.6. ---------- > I have a 8kbps data stream that I want to capture. I need the computer to > synchronise to the data stream. Could I do this in software? Timer channel 0 can be made to run at 8000 samples per second, but the internal timing sources are difficult to synchronise to an external signal. It might be possible, but I'd first suggest an external PLL synchronised with the signal, triggering interrupts via the ACK pin on a parallel port or through a flow control line on a serial port. ## 11.6 DOS DATE AND TIME ---------- > Where and how does DOS store the date? DOS stores the date as a number of days since 1/1/1980 internally in the CLOCK$ device driver, which is part of IO.SYS (MS-DOS) or IBMBIO.COM (other DOSes). There does not seem to be any way to locate the variable except manually, using a debugger. ---------- > I am using the RTC to keep the DOS clock in line. Just after midnight, the > date counts back a day. If I don't set the DOS clock there is no problem. > There is a byte in the BIOS Data Area at 0040:0070 which tells the system > that the date rolled over. How does this work? The midnight flag at 0040:0070 is set to 1 (or just incremented by some BIOSes) by the BIOS's int 8 (timer tick) handler when the tick count rolls over from 0x001800AF to 0x00000000 (i.e. at midnight). Every time the BIOS request-tick-count function (int 1Ah with AH=00) is called, this flag is returned in AL, and the flag byte in memory is cleared. The flag byte is also cleared if the set-tick-count function (int 1Ah with AH=01) is called. DOS relies on this flag when it calls the BIOS function. If your program is using the BIOS request-tick-count function, your program will be notified of the change of day, but DOS will not, because the flag is cleared as soon as it is reported - the BIOS doesn't care whether your program, or DOS, called the function, so DOS misses out on seeing the flag, and doesn't increment the date. In other words, don't use int 1Ah functions 00 and 01, and the DOS date will update properly. If you want to read or write the tick count, access it directly at 0040:006C. ## 11.7 ACCESSING HARDWARE ---------- > How can I read current time without using any BIOS and DOS function calls? You can access the RTC chip directly. The RTC is not present in the original PC and XT and may not be present in non-hardware-compatible machines. The RTC also implements the CMOS which stores your BIOS parameter settings, so be careful when accessing it! See section ¯¯ 7.35. This gives a resolution of one second. Also, you can read the BIOS Tick Count variable, but this is not in convenient units. See section ¯¯ 4. ---------- > I have an acquisition card which measures voltage and frequency of > electrical signals. This board can be configured to use IRQ2 through > IRQ7 (jumper-selectable) and I/O address 300h. How can I access the > devices via the I/O space? The I/O space is accessed via the IN and OUT instructions of the CPU (if you're writing in assembly). In C, use inportb() and outportb() (Borland) or inp() and outp() (Micro$oft). In Turbo Pascal, use port[]. The x86 processor in the PC can address up to 64K of I/O but the PC's I/O space is usually limited to the range 0000h - 03FFh because ISA bus I/O cards only decode the bottom 10 bits of the address on I/O accesses. Your card will probably have a CPU-addressable device such as an 8255 (parallel I/O chip) or similar, that will provide the interface between the hardware on the board, and the software that you write. If you can identify this chip (look for the biggest one, usually :-) and get the data sheet on it, you can find out how to talk to it. If it's a proprietary ASIC, though, you might be on your own. You could try asking the manufacturer nicely. If that fails, you could try disassembling any software that came with the card and working out what the I/O accesses are doing. Typically cards like that will occupy 4, 8, 16, or sometimes 32 adjacent I/O locations, and AFAIK the I/O space from 0300h to 031Fh is not normally used by any standard PC peripheral, so you should be safe putting it there. > And can I write an interrupt service routine that will perform I/O through > port 300h? Is this a normal procedure? Yes, and yes. The XT bus supports IRQ2-7. IRQ4 and IRQ3 are usually used for COM1 and COM2 respectively, IRQ6 is usually used for the floppy disk, and IRQ7 is sometimes used for the first parallel port, and for sound cards. IRQ5 is also sometimes used by sound cards, and is also often used by the hard drive on XTs. Assuming you want to put your XT bus card into an AT, you should be able to use IRQ2 or IRQ5 with it, without causing any conflicts. IRQ2 is actually remapped to IRQ9 on ATs (long story). There are several parts involved in setting up an interrupt handler, and I'd suggest you first try just talking to the chips on the board, and do the interrupt stuff later, as it can get a bit messy. ---------- > I need to delay program execution for 1ms. I found some old assembly code > that used timer 2 on the PIT. It sets the timer to square wave mode, then > counts state changes. This code doesn't work on a Pentium, 486, or 386 PC. > How can I make this work on newer PCs? It appears that timer 2 output isn't > tied to bit 5 of port 0x62 on these machines. Is there something different > about how timer 2 and/or the speaker is implemented on newer PCs? Yes. The ye olde machines used an 8255 at 60h-63h and the Timer 2 read-back signal was on port C at 62h. The AT and later machines use a micro as the keyboard interface, and don't implement any port at 62h at all (AFAIK). On these machines, Timer 2 readback is on bit 5 of port 61h and operates in the same way. For your purposes, the Refresh Detect signal might be more appropriate. This is a read-only signal on bit 4 of the port at 61h, on all machines except the old PC and XT, though I wouldn't guarantee it's present on all IBM machines (they seem to be the least compatible, for some stupid reason). Anyway, this bit toggles state once every 15.0857 microseconds (or 216/14.31818, to be exact). This can easily be read in a loop, and you can get a fairly accurate delay using this method. It won't work properly if the DRAM refresh rate has been changed, but people don't do that much any more :-) This method has advantages over using Timer 2 because you can use it with interrupts enabled and not have to worry about a keyboard buffer full beep clobbering your timer, though of course any interrupts that are serviced during the delay will lengthen the delay. ## 11.8 MISCELLANEOUS ---------- > How do I check for a keypress with a timeout? You need to check for a keypress in a loop, and also incorporate a timeout check in the loop. See the sample program in section ¯¯ 4.7 and the function in section ¯¯ 4.8. You can't use getch() or any stream I/O functions, because they will wait indefinitely for keys to be pressed. If your compiler supports bioskey(1), you can use that, otherwise you can write a function that uses int 16h function 1, 11h, or 21h, or int 21h function 6, to poll for a keypress. ---------- > How do you create a clock that will run in the top right hand corner of > the screen and let the user regain control of the computer in DOS? > I think you have to 'hook' an interrupt to accomplish this. Hook int 8, the timer interrupt (int 1Ch can also be used but is intended for use by applications, which may not chain to the old handler, so your TSR would stop updating while some apps are active). On every interrupt, check the time, either from the BIOS tick count or from the real time clock. You can redraw your time on the screen on every int 8 (i.e. 18.2065 times per second), or just when the second changes, whichever you prefer. There is a sample program in assembler that does this, in section ¯¯ 7.35.8. ---------- > In my Borland C++ program I need a delay of exactly 100 ms. How can I do it? There are many ways to implement processor-independent delays on PCs. There may be a problem depending on how exact you need your delay to be. There are two approaches - wait in a loop for the appropriate length of time, or trigger an interrupt at the correct time. With the first approach, you should leave interrupts enabled during the loop (otherwise the machine will lose time, as the timer tick interrupt comes along every 54.9254 ms), so any interrupts that come in during the loop, or near the end of it, may cause your delay to be longer than you expected. If you use the other method, acceptance of the interrupt can be delayed by foreground code disabling interrupts, or by other interrupts occuring during the delay. So there is no ideal solution if you need a very accurate delay. If you can tolerate an error of, say, 1% to 5%, and your program just wants to wait without doing anything else, and the program will not need to run on old PC and XT machines, you can use the Refresh Detect delay method, which is pretty tidy. If you can tolerate a resolution of 1ms, you can use the BIOS delay functions (not supported on old PC and XT machines either). Otherwise you can use the interrupt method, which is fairly tricky to program, or a method based on timer 2 (normally used for making beeps) which has disadvantages too. ## 12 REFERENCES These references are mostly from {JAM} John Mertus's article. He has given me permission to include them. I have included comments on the subject matter of the books where appropriate. They are in author order. There is no guarantee that the books are still available, or that these are the latest editions. Title: Assembly Language Programming for the IBM Personal Computer Author: David J. Bradley Published: Prentice-Hall, 1984 Comments: Possibly the first book to describe timing by reading the CTC May be no longer available Title: Interrupt List Author: Ralf Brown Comments: Electronic document available on Internet SimTel mirrors, e.g. Oak as ftp://oak.oakland.edu/SimTel/msdos/info/inter*.zip. Contains an exhaustive list of interrupt usage (mainly software interrupts), DOS data structures, etc, essential! Title: DOS Programmer's Reference, 3rd Edition Author: Terry Dettmann, Jim Kyle, Marcus Johnson Published: Que Corporation, 1992 ISBN: 0-88022-790-7; Library of Congress Catalog No. 91-66203 Title: EGA/VGA - A Programmer's Reference Guide Author: Bradley Dyck Kliewer Published: Intertext Publications / Multiscience Press, Inc, McGraw-Hill Book Company, 11 West 19th Street, New York, NY 10011, 1988 ISBN: 0-07-035089-2 Comments: Good as a reference but not recommended for beginners Title: IBM Personal System/2 Hardware Interface Technical Reference Published: IBM Corporation, Boca Raton, Florida, 1990 Title: Technical Reference, Personal Computer XT Published: IBM Corporation, Boca Raton, Florida, April 1983 Title: Peripheral Components Data Book Published: Intel Corporation, Mt. Prospect, Illinois, 1994 Comments: Includes full data sheet on 8254, recommended. According to information in the data book, it can be ordered within USA from Literature Sales, P.O. Box 7641, Mt. Prospect, IL 60056-7641 or by phone from USA and Canada on (800) 548-4725 voice or (708) 296-3699 fax, Intel order number 296467, ISBN 1-55512-207-8. They accept credit cards, but you may need to complete an order form. Title: PC Programmer's Guide to Low-Level Functions and Interrupts Author: Marcus Johnson Published: Sams Publishing, 201 West 103rd Street, Indianapolis, Indiana, 46290, 1994. Comments: Lots of useful low-level technical info, plus documentation on BIOS, DOS, EMS, XMS, DPMI, and other APIs. Disk included. Title: Accurate Timing under Microsoft Windows without reprogramming the System Timer Author: Jerry Jongerius Published: Microsoft System Journal, 1991 Comments: Reading the CTC Title: The MS-DOS Encyclopedia Published: Microsoft Press, 16011 NE 36th Way, Box 97017, Redmond, Washington 98073-9717, 1988 ISBN: 1-55615-174-8 Comments: Includes sections on interrupt-driven communications, TSR programming, exception handlers, hardware interrupt handlers, and debugging, DOS and BIOS function reference, and usage for DOS utilities, highly recommended. [KH] Title: The Peter Norton Programmer's Guide to the IBM PC Author: Peter Norton Published: Microsoft Press, 16011 NE 36th Way, Box 97017, Redmond, Washington 98073-9717, 1985 ISBN: 0-914845-46-2, Penguin ISBN 0-14-087-144-6 Comments: There is a newer edition Title: The Winn Rosch Hardware Bible Author: Winn L. Rosch Published: Brady Books, New York, 1989 Title: The IBM Personal Computer from the Inside Out Author: Murry Sargent and Richard L. Shoemaker Published: Addison-Wesley Publishing Co, Reading, Massachusetts, 1986 Title: Netware, the Professional Reference (second edition) Author: Karanjit Siyan Published: New Rider Publishers, Carmel, Indiana, 1993 Title: The Waite Group's MS-DOS Developer's Guide (second edition) Published: Howard W. Sams & Company, 4300 West 62nd Street, Indianapolis, Indiana 46268, 1989 ISBN: 0-672-22630-8 (Library of Congress Catalog Card 88-62227) Comments: Includes info on TSRs, serial port, EGA and VGA, and real-time programming. Title: Programmer's Guide to PC & PS/2 Video Systems Author: Richard Wilton Published: Microsoft Press, One Microsoft Way, Redmond, Washington 98052-6399, 1987 ISBN: 1-55615-103-9 Comments: Very readable book, recommended [KH] End of the PC Timing FAQ / Application notes Please drop me a line if you find this document useful, or if you have anything to add. ----//----