MS-DOS(R) Q&A

Jeff Prosise

Jeff Prosise writes extensively about programming in MS-DOS and is a contributing editor to several computer magazines. He is the author of two books on MS-DOS published by Ziff-Davis Press.

QI wrote a simple program that uses INT 14H, Function 01H, Transmit Character, to transmit commands to a modem connected to COM1 or COM2. It works fine on the system I developed it on, but it failed on others. The INT 14H API is so simple I wonder how I could be doing anything wrong. Can you help?

AThe problem probably isn’t your program; it’s INT 14H. Serial data transfers accomplished with INT 14H, Function 01H can fail for a variety of reasons. The most common cause of failure is Function 01H’s reliance on the Data Set Ready (DSR) and Clear to Send (CTS) pins on the RS-232 port. In most BIOSes, Function 01H prepares to transmit a character by asserting Data Terminal Ready (DTR) and Request to Send (RTS), and then waits for CTS and DSR to be asserted in response. Some modems never assert CTS and DSR, causing Function 01H to timeout and return to the caller without transmitting a character. To ensure modem communications are carried out properly, you should supply your own code to transmit characters to a serial port. Only then can you be sure of what is going on at the level of the serial port’s Universal Asynchronous Receiver/Transmitter (UART) chip—and certain that characters are being output as you intended.

Figures 1 and 2 contain the two routines you’ll need. The InitPort procedure in Figure 1 initializes the serial port whose base I/O address (3F8H for COM1, 2F8H for COM2, and so on) is passed in DX by writing values to the UART’s Divisor, Line Control, Interrupt Enable, and Modem Control registers. The two Divisor registers (one for the least significant byte of the 16-bit divisor word, the other for the most significant byte) determine the transmission rate—1200 baud, 2400 baud, and so on. The Line Control register controls the parity setting, number of data bits, and number of stop bits. The Interrupt Enable register controls when and under what circumstances the serial port’s UART chip generates a hardware interrupt to notify the CPU of an event or condition. InitPort initializes all the bits in this register to zero to disable serial port interrupts. The Modem Control register controls other aspects of the UART’s operation, including the state of DTR and RTS. Your program should call InitPort with the I/O address of the COM port in DX, the divisor word in BX, and a data format code in AL. Valid values for AL and BX are documented in Figure 1. The following lines initialize COM1 to 1200 bps, no parity, 8 data bits, and 1 stop bit.

mov al,3

mov bx,96

mov dx,3F8h

call InitPort

When InitPort returns, the serial port is ready to output characters.

After the serial port is initialized, you can use the OutChar procedure listed in Figure 2 to output a character. You pass the address of the COM port in DX and the character being transmitted in AL. No checking is performed to ensure that there is a valid COM port at the address specified; it’s up to your program. The following lines transmit a carriage return (ASCII 13) out serial port COM1 (I/O address 3F8H).

mov al,13

mov dx,3F8h

call OutChar

OutChar works by asserting DTR and RTS (because some modems will not accept characters unless these lines are asserted). It then loops until the UART’s transmit buffer—the register where output data is stored prior to being transmitted—becomes empty. Next, the character is output to the UART’s Transmit Buffer register. OutChar loops again until the transmit buffer empties, and then inhibits DTR and RTS. When OutChar returns, the character transmission is a success. Because OutChar does not wait for a signal from the modem via the DSR or CTS lines before transmitting the character, it will succeed where INT 14H fails.

It’s easy to construct a string output routine that uses OutChar to transmit a string delimited by a zero:

cld;Clear direction

;flag

mov si,offset string;Initialize SI

NextChar: lodsb;Get a character

;from the string

or al,al;Exit if zero

jz done

mov dx,uart_addr;Move UART address

;into DX

call OutChar;Transmit the

;character

jmp NextChar;Loop back for

;another character

done:

o

o

o

When the terminating zero is reached, execution branches to the line labeled done. All the characters preceding the zero have been transmitted.

Programmers should be aware that certain internal modems like the one in some Toshiba T3100SX computers may require you to set bit 2 of the General Purpose Output (GPO2) before transmitting the first character to wake up the modem. When the T3100SX’s modem is powered up in auto mode, the modem remains deactivated until the GPO2 bit (bit 3 in the UART’s Modem Control register) is set to 1. Therefore, a communications program should assert GPO2 before transmitting the first character to “wake up” the modem. Since GPO2 is also used to enable and disable COM port interrupts on IBM PCs and compatibles, it’s prudent to zero the bits in the UART’s Interrupt Enable register before asserting GPO2. Otherwise, unwanted interrupts could occur.

The solution presented here should work for any modem connected to COM1 or COM2. However it pays to test serial communications support on a wide array of configurations to make sure you don’t get tripped up by subtle incompatibilities.

Figure 1 Initializing a Serial Port

; InitPort initializes a serial port.

;

; Call with: DX = Serial port's base I/O address

; BX = Data rate divisor

; 384 = 300 baud

; 96 = 1200

; 48 = 2400

; 24 = 4800

; 12 = 9600

; AL = Data format code

; Bits 6-7: Reserved (should be 0)

; Bits 3-5: Parity setting

; 000 = No parity

; 001 = Even parity

; 011 = Odd parity

; Bit 2: Number of stop bits

; 0 = 1 stop bit

; 1 = 2 stop bits

; Bit 1: Reserved (should be 1)

; Bit 0: Number of data bits

; 0 = 7 data bits

; 1 = 8 data bits

InitPort proc near

push ax ;Save format code

mov al,80h ;Set bit 7 of AL

add dx,3 ;DX -> Line Control register

out dx,al ;Set DLAB (bit 7)

jmp short $+2 ;I/O delay

mov al,bl ;Transfer divisor LSB to AL

sub dx,3 ;DX -> Divisor LSB register

out dx,al ;Output divisor LSB

jmp short $+2 ;I/O delay

mov al,bh ;Transfer divisor MSB to AL

inc dx ;DX -> Divisor MSB register

out dx,al ;Output divisor MSB

jmp short $+2 ;I/O delay

pop ax ;Retrieve format code

and al,3Fh ;Mask bits 6 and 7

or al,2 ;Set bit 1

add dx,2 ;DX -> Line Control register

out dx,al ;Output data format code

jmp short $+2 ;I/O delay

xor al,al ;Zero AL

sub dx,2 ;DX -> Interrupt Enable register

out dx,al ;Disable UART interrupts

jmp short $+2 ;I/O delay

add dx,3 ;DX -> Modem Control register

out dx,al ;Clear modem control bits

ret ;Return to caller

InitPort endp

Figure 2 Transmitting a Character Through a Serial Port

; OutChar transmits a byte of data to a serial port.

;

; Call with: AL = Character to transmit

; DX = Serial port's base I/O address

;

; Note: The serial port must be initialized before it can be used.

; See InitPort for details.

OutChar proc near

push ax ;Save the character

mov al,3

add dx,4 ;DX -> Modem Control register

out dx,al ;Assert DTR and RTS for output

jmp short $+2 ;I/O delay

inc dx ;DX -> Line Status register

OCLoop1: in al,dx ;Read line status

test al,20h ;Loop until transmit buffer

jz OCLoop1 ;becomes empty

pop ax ;Retrieve the character

sub dx,5 ;DX -> Transmit Buffer register

out dx,al ;Output the character

jmp short $+2 ;I/O delay

add dx,5 ;DX -> Line Status register

OCLoop2: in al,dx ;Read line status

test al,20h ;Loop until transmit buffer

jz OCLoop2 ;becomes empty

xor al,al ;Zero AL

dec dx ;DX -> Modem Control register

out dx,al ;Inhibit DTR and RTS

ret ;Return to caller

OutChar endp

QWhat is the safest and most reliable way to write a TSR that can detect when it has been loaded, so it can prevent itself from being loaded a second time?

AProgrammers have devised many methods over the years for detecting an installed TSR when the same TSR is run a second time. The safest method is used by the MS-DOS TSRs Print, Assign, and Share. This method uses the MS-DOS Multiplex Interrupt INT 2FH. The TSR assigns itself an ID code called a multiplex ID number between C0H and FFH (values from 00H to BFH are reserved for MS-DOS) and then hooks itself to INT 2FH. When it receives an INT 2FH, the installed TSR inspects the values in AH and AL. AL holds the multiplex function code; a value of 00H means the TSR is requesting an installation check. AH holds the multiplex ID number. If AL is equal to 00H and AH is equal to the TSR’s ID code, the TSR sets AL to FFH before returning to signal the caller that it is installed.

The only problem with this method is selecting a unique multiplex ID number. For MS-DOS, this is not a problem. Each MS-DOS TSR is assigned a unique ID and application programmers are expected to honor these conventions. For an application program, there is no guarantee that a randomly selected ID number won’t be the same value used by another program. To avoid conflicts, two further steps are required. First, when the TSR returns FFH in AL to verify that it is installed, it should also return a pointer to a string that uniquely identifies the program. The caller can then inspect the contents of memory at the location referenced by the pointer and verify the program’s identity. Second, rather than select a fixed multiplex ID number that will be used every time, the TSR’s initialization code should call INT 2FH repeatedly with AL set to 00H and AH set to C0H, then C1H, and so on, until it finds an unused multiplex ID number (one that returns 00H in AL). Correspondingly, routines that use the multiplex ID number should be built to use the value that is returned by this procedure.

The assembly language procedures in Figures 3, 4, and 5 show how to do this. Figure 3 contains a prototype INT 2FH handler. Upon entry, the handler checks the ID code in AH and branches to Mplex1 if the ID code is its own. If execution falls through (if the ID code belongs to someone else), the interrupt is passed on down the interrupt chain with a JMP instruction that transfers control to the original INT 2FH handler. If the ID code in AH matches ProgID and if AL is 0, the handler sets AL to FFH and points ES:DI to the signature string “TSRName”. This string should contain your TSR’s unique name. It should be something that is unlikely to be duplicated by other TSR writers.

The CheckInstall procedure in Figure 4, which the TSR could call during the initialization phase, returns carry set if an instance of the program is already resident in memory and carry clear if there are no other instances presently in memory. If CheckInstall returns the carry flag set, AH contains the TSR’s multiplex ID number. This would be useful if the TSR’s INT 2FH handler implemented other functions besides 00H—a great way to let two instances of a TSR (one resident and one transient) exchange messages. For a disk caching program, for example, Function 01H could be defined so that the size of the disk cache is passed back in AX. Then an instance of the program run from the command line could obtain that information from the installed instance of the program and report it to the user.

If CheckInstall returns carry clear, indicating the TSR is not already installed, the TSR’s initialization code should take one additional step. It should call the GetMplexID routine listed in Figure 5 to obtain a unique multiplex ID number. GetMplexID calls INT 2FH repeatedly (with potential multiplex ID numbers in AH) until it locates one for which AL returns 00H, indicating the ID number is unused. Then it returns with carry clear and AH set to the ID number. If carry returns set, then all 64 multiplex ID numbers from C0H to FFH are in use. It’s highly unlikely that this will ever occur.

With these routines at its disposal, the portion of a TSR’s initialization code that checks for an installed instance of the same TSR might look like this:

call CheckInstall

jc AlreadyInstalled

call GetMplexID

mov ProgID,ah

o

o

o

If the TSR is already resident in memory, execution goes to the line labeled AlreadyInstalled. If this is the first instance of the TSR, then execution falls through and GetMplexID is called to obtain a unique multiplex ID number. The result is stored in ProgID. This is the same ProgID variable that the installed INT 2FH handler references when it checks the multiplex ID value passed to it in AH. After obtaining the ID number, the TSR would proceed as normal by calling INT 27H or INT 21H, Function 31H, to terminate but stay resident.

Figure 3 Prototype INT 2FH Handler

signature db "TSRName"

MplexInt proc far

pushf ;Save flags

cmp ah,cs:[ProgID] ;Branch if AH holds the

je Mplex1 ; multiplex ID

popf ;Restore flags

jmp cs:[int2Fh] ;Pass the interrupt on

Mplex1: popf ;Restore FLAGS

or al,al ;Branch if function code

jnz MplexExit ; is other than 00h

mov al,0FFh ;Set AL to FFh

push cs ;Point ES:DI to the

pop es ; program signature

mov di,offset signature

MplexExit: iret ;Return from interrupt

MplexInt endp

Figure 4 Determining if a TSR is Already Installed

; CheckInstall determines if a TSR is already installed.

;

; Call with: DS -> Data segment containing program signature

;

; Returns: Carry set = Program is installed (AH contains the

; program's multiplex ID number)

; Carry clear = Program is not installed

;

; Note: If carry returns clear, call GetMplexID to obtain an

; unused multiplex ID number

CheckInstall proc near

cld ;Clear direction flag

mov ax,C000h ;Initialize AH and AL

mov cx,40h ;Initialize count

Check1: push ax ;Save AX and CX

push cx

xor di,di ;Zero ES and DI

mov es,di

int 2Fh ;Interrupt 2Fh

cmp al,0FFh ;Branch if AL!=FFh

jne Check2

mov si,offset signature ;Compare signatures

mov cx,7 ; (7 bytes, program-

repe cmpsb ; dependent)

jne Check2 ;Branch if compare fails

pop cx ;Clear stack and exit

pop ax ; with carry set

stc

ret

Check2: pop cx ;Retrieve AX and CX

pop ax

inc ah ;Next multiplex ID

loop Check1 ;Loop until done

clc ;Exit with carry clear

ret

CheckInstall endp

Figure 5 Obtaining an Unused Multiplex ID Number

; GetMplexID obtains an unused multiplex ID number.

;

; Call with: Nothing

;

; Returns: Carry set = No multiplex ID numbers available

; Carry clear = Multiplex ID number in AH

GetMplexID proc near

mov ax,0C000h ;Initialize AH and AL

mov cx,40h ;Initialize count

GMID1: push ax ;Save AX and CX

push cx

int 2Fh ;Interrupt 2Fh

or al,al ;Branch if AL= =0

jz GMID2

pop cx ;Retrieve AX and CX

pop ax

inc ah ;Increment ID number

loop GMID1 ;Loop until done

stc ;Exit with carry set

ret

GMID2: pop cx ;Clear stack and exit

pop ax ; with carry clear

clc

ret

GetMplexID endp

QEvery assembly-language programmer knows that it’s possible to convert a 16-bit unsigned value to ASCII decimal format by repeatedly dividing by 10 and saving the remainder on the stack. When the quotient reaches zero, the digits that make up the result are conveniently stored on the stack in the order that they appear when the number is read from left to right. This technique fails for most 32-bit values, however, because a divide overflow occurs if the quotient resulting from the first divide operation is greater than 65,535. Can you provide an assembly-language procedure that converts a 32-bit unsigned integer to ASCII decimal format?

AHex2Asc (see Figure 6) converts the 32-bit unsigned value in AX:BX to ASCII decimal format and writes the result to standard output using MS-DOS Function 02H. To perform the conversion, Hex2Asc uses a simple algorithm. First, it divides the high word of the 32-bit value (AX) by 10 and stores the result in SI. With the remainder from the previous operation still in DX, it divides the low word of the 32-bit value (BX) by 10. The procedure stores the result in BX and copies the value saved in SI back to AX. Next, it increments the digit counter (CX) and pushes the remainder from the earlier divide operation (stored in DX) onto the stack. The program then tests the value in AX:BX. If both AX and BX hold zero, it exits the loop. If either register holds a nonzero value, the procedure starts the whole process over.

Finally, using CX as a counter, Hex2Asc pops a digit off the stack, adds 30H to convert from binary to ASCII, and writes it to standard output. The program repeats until CX equals zero.

Hex2Asc will handle values as large as 4,294,967,295. If you need to convert larger numbers, the algorithm is easily extensible to 64-, 96-, and even 128-bit integers. Simply partition the number into 16-bit components and divide each component by 10, starting with the highest word and proceeding to the lowest. On each iteration, push the remainder from the final divide operation onto the stack. When all components of the number are equal to zero, the stack holds the number’s base-10 ASCII equivalent.

Figure 6 Hex2Asc

; Hex2Asc converts a 32-bit unsigned integer to ASCII decimal format

; and writes the result to standard output.

;

; Call with: AX:BX = 32-bit value

;

; Returns: Nothing

Hex2Asc proc near

xor cx,cx ;Initialize count in CX

mov di,10 ;Set DI to 10

h2a1: xor dx,dx ;Zero DX

div di ;Divide high word by 10

mov si,ax ;Save quotient in SI

mov ax,bx ;Transfer low word to AX

div di ;Divide low word by 10

mov bx,ax ;Save quotient in BX

mov ax,si ;Restore AX

inc cx ;Increment digit count

push dx ;Save remainder on stack

or ax,ax ;Loop back if AX and BX

jnz h2a1 ; are not equal to 0

or bx,ax

jnz h2a1

h2a2: mov ah,02h ;DOS function 02h

pop dx ;Retrieve one digit

add dl,30h ;Binary to ASCII

int 21h ;Output it

loop h2a2 ;Loop until done

ret

Hex2Asc endp