	page	60,132
	title	SERIAL	 C interrupt driven serial I/O routines for IBM PC COM ports

;These routines allow c programs to use serial port 0 in an interrupt
;driven environment. This environment is locally generated and controlled
;
;This program is released to the public domain for non-commercial
;uses. Please do not charge "copying fees" when redistrubiting
;this program. Feedback is desired.
;
;Skip Hansen, WB6YMH 6/22/86
;RCP/M (213) 541-2503
;or via RLI linked systems at WB6KAJ or KD6SQ
;
;	1/84	Greg Lindberg	Original routines written for Pronto Computer
;
;	2/86	Skip,WB6YMH	Modified for use on IBM PC COM1
;
;	4/26/86 Lee, WB6KAJ	Modified transmit byte routine to check
;				CTS before sending a byte for use with
;				hardware flow control.
;
;	01/30/87 Mike, WA6FXT	Modified to conform to Microsoft 'C' 4.0
;				Also added DTR control to _serinit, _serldone
;
;	01/31/87 Mike, WA6FXT	Modified to allow COM port selection to be
;				done dynamic (command option).
;
;	6/1/87	Skip, WB6YMH	Corrected bug in serldone. Fixed bug in
;				serlbaud caused by fallthru logic after
;				port option was added to serlinit.
;
;

;
;macro for output from al to 16 bit port adr
;
outbyte		macro	name
		mov	dx,name 	;point dx to port adr
		out	dx,al		;output 8 bit value
		endm
;
;macro for byte input to al from 16 bit port
;
inbyte		macro	name
		mov	dx,name 	;point dx to port adr
		in	al,dx		;read byte
		endm

;
;macro for output from al to 16 bit port base adr with offset 'name'
;
outport		macro	name
		mov	dx,name		;get the offset value
		call	out_byte	;output the byte
		endm
;
;macro for byte input to al from 16 bit port base adr with offset 'name'
;
inport		macro	name
		mov	ax,port_base	;get the base address
		add	ax,name		;add the OFFSET
		mov	dx,ax		;set DX to point at port address
		in	al,dx		;read byte
		endm

false		equ	0
true		equ	not false

i8259_mask	equ	21h		;interrupt mask register
i8259_ctrl	equ	20h		;interrupt controller control port
rdaien		equ	1		;enable bit for RDA interrupts
linmod		equ	03h		;line mode, 8 bits, no parity
mdmmod		equ	0bh		;modem mode= DTR and RTS high &

;	COM port definitions
COM1		equ	3f8h		;base address of COM1 8250
com_vec1 	equ	0ch		;interrupt vector for COM1
int_level1	equ	4		;interrupt level on 8259
int_mask1	equ	00010000b	;interrupt mask bit in 8259

COM2		equ	2f8h		;base address of COM2 8250
com_vec2 	equ	0bh		;interrupt vector for COM2
int_level2	equ	3		;interrupt level on 8259
int_mask2	equ	00001000b	;interrupt mask bit in 8259

;	OFFSETS from base address of COM port
mdmdat		equ	0		;offset to data port
mdmint		equ	1		;interrupt enable register
mdmlin		equ	3		;line control register
mdmmdm		equ	4		;modem control register
mdmsta		equ	5		;line status register
mdmmsr		equ	6		;modem status register

DTR_BIT 	equ	1		;DTR control bit in modem control reg
RTS_BIT 	equ	2		;RTS control bit

check_cts	equ	true

_TEXT	SEGMENT	BYTE	PUBLIC	'CODE'
_TEXT	ENDS
_DATA	SEGMENT	WORD	PUBLIC	'DATA'
_DATA	ENDS
CONST	SEGMENT	WORD	PUBLIC	'CONST'
CONST	ENDS
_BBS	SEGMENT	WORD	PUBLIC	'BBS'
_BBS	ENDS

DGROUP	GROUP	CONST,	_BBS,	_DATA

	ASSUME	CS:_TEXT, DS:DGROUP, SS:DGROUP

_DATA	SEGMENT

	public	trnbuffin,trnbuffout,trn_buff,trn_buff_end
	public	rcvbuffin,rcvbuffout,rcv_buff,rcv_buff_end
	public	com_port,port_base

;	DATA AREA
trnbuffin	dw	offset DGROUP:trn_buff	;pointer to input point in transmit buffer
trnbuffout	dw	offset DGROUP:trn_buff	;pointer to output point in transmit buffer
trn_buff	db	160 dup (?)	;transmit buffer
trn_buff_end	equ	$
rcvbuffin	dw	offset DGROUP:rcv_buff	;pointer to input point in receive buffer
rcvbuffout	dw	offset DGROUP:rcv_buff	;pointer to output point in receive buffer
rcv_buff	dw	2048 dup (?)	;receive buffer
rcv_buff_end	equ	$
		dw	100 dup (?)	;local interrupt stack
intstk		equ	$
ssseg		dw	?		;storage for interrupted stack seg
spptr		dw	?		;storage for interrupted stack pointer
com_port	dw	?		;storage for COM port selection
port_base	dw	?		;storage for COM port base address

_DATA	ENDS


_TEXT	SEGMENT

	PUBLIC	_SERLINIT,_TRNBYTE,_RCVBYTE,_SERLDONE
	PUBLIC	_TIMERCV,_PURGERCV,_SERLBAUD,_rcvint
	PUBLIC	_dtr_on,_dtr_off,rts_on,rts_off

dsseg		dw	$		;storage for C data segment

;
; name		serlbaud -- baud rate initialazation for serial port 0
;
; synopsis	serlbaud(,port_num,baud);
;		int port_num;	port number (IE: 0=COM1)
;		int baud;	baud rate for serial port
;
; description	This routine initializes serial port 0 at the specified
;		baud rate.
;
_serlbaud	proc	near
		push	bp		;save BP
		mov	bp,sp		;point to passed parms
		mov	dx,[bp+4]	;get port number
		mov	al,[bp+6]	;get requested baud rate
		ror	al,1		;put in right place
		ror	al,1
		ror	al,1
		and	al,0e0h 	;save only baud rate
		or	al,3		;get rest of int 14h command
		xor	ah,ah		;make it init command
		int	14h		;init port 0 for 8 data bits, 1 stop, no parity
		mov	ax,[bp+4]	;get port number
		push	ax		;
		call	_serlinit	;
		pop	ax		;fix stack
		pop	bp		;restore callers BP
		ret			;all done so fall thru to serlinit()
					;since ibm clears interrupt setup with
					;int 14 baudrate calls
_serlbaud	endp

;
; name		serlinit -- interrupt driven serial initialization
;
; synopsis	serlinit(port_num);
;		int port_num;	port number (IE: 0=COM1)
;
; description	This routine initializes interrupts for serial port 0.
;
_serlinit	proc	near
		push	bp		;save BP
		mov	bp,sp		;point to passed parms
		push	ds		;save data segment
		mov	dsseg,ds	;save C data segment for interrupt routine

		mov	ax,[bp+4]	;get the port number
		mov	com_port,ax	;set the number for later use
		mov	dx,COM1		;assume COM1
		cmp	ax,0		;see if COM1
		jz	ser_setcom
		mov	dx,COM2		;set for COM2
ser_setcom:	mov	port_base,dx	;set the port base address

		mov	ax,com_port	;get the port number
		mov	bx,2500h+com_vec1	;assume COM1
		cmp	ax,0		;COM1?
		jz	ser_setvec ;yes
		mov	bx,2500h+com_vec2	;must be COM2

ser_setvec:	mov	ax,cs		;set up for init of interrupt vector
		mov	ds,ax
		mov	dx,offset cs:_rcvint	;point to isr
		mov	ax,bx		;move the vector to AX

		cli			;no interrupts now
		int	21h		;initialize receive interrupt
		pop	ds		;restore data pointer
		mov	ax,offset ds:trn_buff ;clear transmit buffer
		mov	trnbuffin,ax
		mov	trnbuffout,ax
		mov	ax,offset ds:rcv_buff ;clear receive buffer
		mov	rcvbuffin,ax
		mov	rcvbuffout,ax
		inport	mdmdat		;clear any garbarge characters
		in	al,dx		;from COM port
		in	al,dx		;
		mov	al,linmod	;clear DLAB bit and set line mode
		outport	mdmlin		;
		mov	al,mdmmod	;get mode to use
		outport	mdmmdm		;to modem control register
		mov	al,rdaien	;enable interrupts from RDA
		outport	mdmint		;
		mov	dx,i8259_mask	;unmask interrupts from serial port 0
		in	al,dx		;get present mask

		push	ax		;save it
		mov	ax,com_port	; check the port
		cmp	ax,0		;see if COM1
		jnz	 ser_mask2	;must be COM2

		pop	ax		;restore AX
		and	al,not int_mask1 ;enable interrupts for COM1
		jmp	short ser_setmask

ser_mask2:	pop	ax		;restore AX
		and	al,not int_mask2 ;enable interrupts for COM2

ser_setmask:	out	dx,al		;restore new mask

		call	_dtr_on		;allow inputs

		sti			;interrupts back on
		pop	bp		;restore callers BP
		ret			;all done so exit
_serlinit	endp

;
; name		trnbyte -- transmit byte of data over serial port 0
;
; synopsis	trnbyte(trndata);
;		int trnbyte;	;data byte to send
;
; description	This routine sends a byte of data out serial port 0.
;
_trnbyte 	proc	near
		push	bp		;save BP
		mov	bp,sp		;point to passed parms
trn_byte_loop:	inport	mdmsta		;get status byte
		and	al,20h		;transmit buffer empty ?
		jz	trn_byte_loop	;loop if not
if check_cts
		inport	mdmmsr		;get modem status reg.
		and	al,10h		;look at cts.
		jz	trn_byte_loop	;loop until ready.
endif
		mov	al,[bp+4]	;get character to output
		outport	mdmdat		;send character

		pop	bp		;restore callers BP
		ret			;all done so exit
_trnbyte 	endp


;
; name		rcvbyte -- receive byte from serial port 0
;
; synopsis	rcvbyte();
;
; description	This routine attempts to return a byte of data from
;		serial channel 0. If no data is available it returns
;		0ffffh else it returns the data with error status in
;		the high byte
;
_rcvbyte 	proc	near
		mov	bx,rcvbuffout	;is buffer empty
		cmp	bx,rcvbuffin
		mov	ax,0ffffh	;load false return
		je	short rcvend	;if so go try leave
		mov	ax,[bx] 	;get return byte
		inc	bx		;update pointer
		inc	bx
		cmp	bx,offset ds:rcv_buff_end
		jl	rcvoutsav
		mov	bx,offset ds:rcv_buff
rcvoutsav:	mov	rcvbuffout,bx

rcvend: 	ret			;all done so exit
_rcvbyte 	endp

_rcvint		proc	far
		push	ds		;save present data seg
		mov	ds,dsseg	;set to C data segment
		mov	ssseg,ss	;save interrupted stack
		mov	spptr,sp
		mov	ss,dsseg	;point to local stack
		mov	sp,offset ds:intstk
		push	ax		;save everything
		push	bx		;
		push	dx		;
		inport	mdmdat		;get received char
		mov	bl,al		;save char
		inport	mdmsta		;get status
		and	al,0eh		;save only framing, and overrun error
		mov	ah,al		;put status into high byte
		mov	al,bl		;get char back
		mov	bx,rcvbuffin	;is buffer full
		inc	bx
		inc	bx
		cmp	bx,offset ds:rcv_buff_end
		jl	rcvint1
		mov	bx,offset ds:rcv_buff
rcvint1:	cmp	bx,rcvbuffout
		je	short rcvintdn	;if so leave
		mov	bx,rcvbuffin
		mov	[bx],ax 	;else save char and error status
		inc	bx		;update pointer
		inc	bx
		cmp	bx,offset ds:rcv_buff_end
		jl	rcvint2
		mov	bx,offset ds:rcv_buff
rcvint2:	mov	rcvbuffin,bx

rcvintdn:
		mov	ax,com_port	;check which COM port
		cmp	ax,0		;COM1?
		jnz	rcv_com2	;no

		MOV	AL,60h+int_level1 ;specific EOI, level 1
		jmp	short rcv_int_clr

rcv_com2:	MOV	AL,60h+int_level2 ;specific EOI, level 1

rcv_int_clr:	outbyte	i8259_ctrl	;send specific EOI to 8259
rcv_int_dn:	pop	dx		;restore everything
		pop	bx		;
		pop	ax		;
		mov	ss,ssseg	;restore interrupted stack
		mov	sp,spptr
		pop	ds		;restore data seg
		iret			;and leave
_rcvint		endp

;
; name		timercv -- receive with time out
;
; synopsis	timercv(timeout);
;		int timeout;	;time out in seconds
;
; description	This routine tries to receive a char within a given
;		time. If a char becomes available it is returned.
;		Else if passed time elapses it returns -1 as an error.
;
_timercv 	proc	near
		push	bp		;save BP
		mov	bp,sp		;point to passed parms
		sub	sp,4		;make local storage
		mov	ax,[bp+4]	;get time to wait
		mov	cx,182		;find time in ticks
		mul	cx
		mov	cx,10
		div	cx
		mov	[bp-4],ax	;save it
		xor	ax,ax		;find out current time in ticks
		int	1ah
		add	[bp-4],dx	;make it ending time
		adc	cx,0
		mov	[bp-2],cx	;and save it

timrlp: 	call	_rcvbyte 	;is there a character ready
		cmp	ax,0ffffh
		jne	timrend 	;leave if so

		xor	ax,ax		;else get new time
		int	1ah
		sub	dx,[bp-4]	;timed out yet
		sbb	cx,[bp-2]
		jl	timrlp		;if not go back and check again

		mov	ax,0ffffh	;else error out
timrend:	add	sp,4		;deallocate local storage

		pop	bp		;restore callers BP
		ret			;all done so exit
_timercv 	endp

;
; name		purgercv -- purge receiver
;
; synopsis	purgercv();
;
; description	This routine keeps requesting receive characters
;		until the timercv times out for one second.
;
_purgercv	proc	near
		mov	ax,1		;time out of one second
		push	ax		;
purgelp:	call	_timercv 	;see if there are any chars
		cmp	ax,0ffffh	;did it time out
		jne	purgelp 	;try again if not
		pop	bx		;clean stack
		ret			;done leave
_purgercv	endp

;
; name		serldone -- disables interrupts from serial port 0
;
; synopsis	serldone();
;
; description	This routine turns off interrupts from serial port 0
;		and restores the interrupt vectors to there default valuse
;
_serldone	proc	near
		push	bp		;save BP
		mov	bp,sp		;point to passed parms

		call	_dtr_off	;disable the port

		inbyte	i8259_mask	;mask out interrupts from serial port

		push	ax		;save it
		mov	ax,com_port	; check the port
		cmp	ax,0		;see if COM1
		jnz	serd_mask2	;must be COM2

		pop	ax		;restore AX
		or	al,int_mask1	;disable interrupts for COM1
		jmp	short serd_clrmask

serd_mask2:	pop	ax		;restore AX
		or	al,int_mask2	;disable interrupts for COM2

serd_clrmask:	out	dx,al		;restore new mask
		pop	bp		;restore callers BP
		ret			;all done so exit
_serldone	endp

;
; name		dtr_on -- turn on DTR handshaking line on RS-233 port
;
; synopsis	dtr_on();
;
_dtr_on		proc	near
	 	mov	al,linmod	;clear DLAB bit and set line mode
		outport	mdmlin		;
		inport	mdmmdm		;get current setup
		or	al,DTR_BIT	;turn DTR on
		outport	mdmmdm		;to modem control register
		ret
_dtr_on		endp

;
; name		dtr_off -- turn on DTR handshaking line off RS-233 port
;
; synopsis	dtr_off();
;
_dtr_off	proc	near
		mov	al,linmod	;clear DLAB bit and set line mode
		outport	mdmlin		;
		inport	mdmmdm		;get current setup
		and	al,not DTR_BIT	;DTR off
		outport	mdmmdm		;to modem control register
		ret
_dtr_off	endp

;
; name		rts_on -- turn on RTS handshaking line on RS-233 port
;
; synopsis	rts_on();
;

rts_on: 	mov	al,linmod	;clear DLAB bit and set line mode
		outport	mdmlin		;
		inport	mdmmdm		;get current setup
		or	al,RTS_BIT	;turn RTS on
		outport	mdmmdm		;to modem control register
		ret
;
; name		rts_off -- turn on RTS handshaking line off RS-233 port
;
; synopsis	rts_off();
;

rts_off:	mov	al,linmod	;clear DLAB bit and set line mode
		outport	mdmlin		;
		inport	mdmmdm		;get current setup
		and	al,not RTS_BIT	;RTS off
		outport	mdmmdm		;to modem control register
		ret

;
;	A routine to output a byte in al, to the port @ port_base, offset
;	by the value in dx
;
out_byte:	push	ax		;save the byte to send
		mov	ax,port_base	;get the base address
		add	ax,dx		;add the OFFSET
		mov	dx,ax		;set DX to point at port address
		pop	ax		;restore AX
		out	dx,al		;output 8 bit value
		ret

_TEXT	ENDS
	end
