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