home *** CD-ROM | disk | FTP | other *** search
- Xref: sparky comp.sys.ibm.pc:1109 comp.sys.ibm.pc.programmer:803 comp.sys.ibm.pc.hardware:37050
- Path: sparky!uunet!cs.utexas.edu!qt.cs.utexas.edu!yale.edu!ira.uka.de!sbusol.rz.uni-sb.de!coli.uni-sb.de!sbustd!chbl
- From: chbl@sbustd.rz.uni-sb.de (Christian Blum)
- Newsgroups: comp.sys.ibm.pc,comp.sys.ibm.pc.programmer,comp.sys.ibm.pc.hardware
- Subject: FAQ serial port (part two)
- Date: 25 Jan 1993 10:05:57 GMT
- Organization: Studenten-Mail, Rechenzentrum Universitaet des Saarlandes
- Lines: 567
- Message-ID: <1k0e25INNglt@coli-gate.coli.uni-sb.de>
- NNTP-Posting-Host: sbustd.stud.uni-sb.de
-
-
- This is a FAQ answer on serial communications using the TTY protocol. It
- contains information on the TTY protocol and hardware and software implemen-
- tations on IBM PCs which is derived from National Semiconductor data sheets
- and practical experience of the author and his supporters.
- If you want to contribute to this FAQ in any way, please email me (probably
- by replying to this posting). My email address is: chbl@stud.uni-sb.de
- See the end for details.
- It's the second publication of this file. Lots of errors have been
- removed, and lots of information has been added (which has surely brought
- other errors with it, see Murphy's Law).
-
-
- PART TWO - PROGRAMMING
-
-
- Programming
- -----------
-
- Now for the clickety-clickety thing. I hope you're a bit keen in
- assembler programming (if not, you've got a problem B-). Programming the UART
- in high level languages is, of course, possible, but not at very high
- rates or interrupt-driven. I give you several routines in assembler (and,
- wherever possible, in C) that do the dirty work for you.
-
- First thing to do is detect which chip is used. It shouldn't be difficult
- to convert this C function into assembler; I'll omit the assembly version.
-
- int detect_UART(unsigned baseaddr)
- {
- // this function returns 0 if no UART is installed.
- // 1: 8250, 2: 16450, 3: 16550, 4: 16550A
- int x;
- // first step: see if the LCR is there
- outp(baseaddr+3,0x1b);
- if (inp(baseaddr+3)!=0x1b) return 0;
- outp(baseaddr+3,0x3);
- if (inp(baseaddr+3)!=0x3) return 0;
- // next thing to do is look for the scratch register
- outp(baseaddr+7,0x55);
- if (inp(baseaddr+7)!=0x55) return 1;
- outp(baseaddr+7,0xAA);
- if (inp(baseaddr+7)!=0xAA) return 1;
- // then check if there's a FIFO
- outp(baseaddr+2,0x1);
- x=inp(baseaddr+2);
- if ((x&0x80)==0) return 2;
- if ((x&0x40)==0) return 3;
- return 4;
- }
-
- Remember: if it's not a 16550A, don't use the FIFO mode!
-
-
- Now the non-interrupt version of TX and RX.
-
- Let's assume the following constants are set correctly (either by
- 'CONSTANT EQU value' or by '#define CONSTANT value'). You can easily use
- variables instead, but I wanted to save the extra lines for the ADD
- commands necessary then...
-
- UART_BASEADDR the base address of the UART
- UART_BAUDRATE the divisor value (e.g. 12 for 9600 baud)
- UART_LCRVAL the value to be written to the LCR (e.g. 0x1b for 8N1)
- UART_FCRVAL the value to be written to the FCR. Bit 0, 1 and 2 set,
- bits 6 & 7 according to trigger level wished (see above).
- 0x87 is a good value.
-
- First thing to do is initializing the UART. This works as follows:
-
- init_UART proc near
- push ax ; we are 'clean guys'
- push dx
- mov dx,UART_BASEADDR+3 ; LCR
- mov al,80h ; set DLAB
- out dx,al
- mov dx,UART_BASEADDR ; divisor
- mov ax,UART_BAUDRATE
- out dx,ax
- mov dx,UART_BASEADDR+3 ; LCR
- mov al,UART_LCRVAL ; params
- out dx,al
- mov dx,UART_BASEADDR+4 ; MCR
- xor ax,ax ; clear loopback
- out dx,al
- ;***
- pop dx
- pop ax
- ret
- init_UART endp
-
- void init_UART()
- {
- outp(UART_BASEADDR+3,0x80);
- outpw(UART_BASEADDR,UART_BAUDRATE);
- outp(UART_BASEADDR+3,UART_LCRVAL);
- outp(UART_BASEADDR+4,0);
- //***
- }
-
- If we wanted to use the FIFO functions of the 16550A, we'd have to add
- some lines to the routines above (where the ***s are).
- In assembler:
- mov dx,UART_BASEADDR+2 ; FCR
- mov al,UART_FCRVAL
- out dx,al
- And in C:
- outp(UART_BASEADDR+2,UART_FCRVAL);
-
- Don't forget to disable the FIFO when your program exits! Some other
- software may rely on this!
-
- Not very complex so far, isn't it? Well, I told you so at the very
- beginning, and we wanted to start easy. Now let's send a character.
-
- UART_send proc near
- ; character to be sent in AL
- push dx
- push ax
- mov dx,UART_BASEADDR+5
- us_wait:
- in al,dx ; wait until we are allowed to write a byte to the THR
- test al,20h
- jz us_wait
- pop ax
- mov dx,UART_BASEADDR
- out dx,al ; then write the byte
- pop ax
- pop dx
- ret
- UART_send endp
-
- void UART_send(char character)
- {
- while ((inp(UART_BASEADDR+5)&0x20)!=0) {;}
- outp(UART_BASEADDR,(int)character);
- }
-
- This one sends a null-terminated string.
-
- UART_send_string proc near
- ; DS:SI contains a pointer to the string to be sent.
- push si
- push ax
- push dx
- cld ; we want to read the string in its correct order
- uss_loop:
- lodsb
- or al,al ; last character sent?
- jz uss_ende
- ;*1*
- mov dx,UART_BASEADDR+5
- push ax
- uss_wait:
- in al,dx
- test al,20h
- jz uss_wait
- mov dx,UART_BASEADDR
- pop ax
- out dx,al
- ;*2*
- jmp uss_loop
- uss_ende:
- pop dx
- pop ax
- pop si
- ret
- UART_send_string endp
-
- void UART_send_string(char *string)
- {
- int i;
- for (i=0; string[i]!=0; i++)
- {
- //*1*
- while ((inp(UART_BASEADDR+5)&0x20)!=0) {;}
- outp(UART_BASEADDR,(int)string[i]);
- //*2*
- }
- }
-
- Of course, we could have used our already programmed function/procedure
- UART_send instead of the piece of code limited by *1* and *2*, but we are
- interested in high-speed code.
-
- It shouldn't be a hard nut for you to modify the above function/procedure
- so that it sends a block of data rather than a null-terminated string. I'll
- omit that here.
-
- Now for reception. We want to program routines that do the following:
- - check if a character received or an error occured
- - read a character if there's one available
-
- Both the C and the assembler routines return 0 (in AX) if there is
- neither an error condition nor a character available. If a character is
- available, Bit 8 is set and AL or the lower byte of the return value
- contains the character. Bit 9 is set if we lost data (overrun), bit 10
- signals a parity error, bit 11 signals a framing error, bit 12 shows if
- there is a break in the data stream and bit 15 signals if there are any
- errors in the FIFO (if we turned it on). The procedure/function is much
- smaller than this paragraph:
-
- UART_get_char proc near
- push dx
- mov dx,UART_BASEADDR+5
- in al,dx
- mov ah,al
- and ah,9fh
- test al,1
- jz ugc_nochar
- mov dx,UART_BASEADDR
- in al,dx
- ugc_nochar:
- pop dx
- ret
- UART_get_char endp
-
- unsigned UART_get_char()
- {
- unsigned x;
- x=(inp(UART_BASEADDR+5)<<8)&0x9f;
- if (x&0x100) x|=((unsigned)inp(UART_BASEADDR))&0xff);
- return x;
- }
-
- This procedure/function lets us easily keep track of what's happening
- with the RxD pin. It does not provide any information on the modem status
- lines! We'll program that later on.
-
- If we wanted to show what's happening with the RxD pin, we'd just have to
- write a routine like the following (I use a macro in the assembler version
- to shorten the source code):
-
- DOS_print macro pointer
- ; prints a string in the code segment
- push ax
- push ds
- push cs
- pop ds
- mov dx,pointer
- mov ah,9
- int 21h
- pop ds
- pop ax
- endm
-
- UART_watch_rxd proc near
- uwr_loop:
- ; check if keyboard hit; we want a possibility to break the loop
- mov ah,1
- int 16h
- jnz uwr_exit
- call UART_get_char
- or ax,ax
- jz uwr_loop
- test ah,1 ; is there a character in AL?
- jz uwr_nodata
- push ax ; yes, print it
- mov dl,al
- mov ah,2
- int 21h
- pop ax
- uwr_nodata:
- test ah,0eh ; any error at all?
- jz uwr_loop ; this speeds up things
- test ah,2 ; overrun error?
- jz uwr_noover
- DOS_print overrun_text
- uwr_noover:
- test ah,4 ; parity error?
- jz uwr_nopar
- DOS_print parity_text
- uwr_nopar:
- test ah,8 ; framing error?
- jz uwr_loop
- DOS_print framing_text
- jmp uwr_loop
- overrun_text db "*** Overrun Error ***$"
- parity_text db "*** Parity Error ***$"
- framing_text db "*** Framing Error ***$"
- UART_watch_rxd endp
-
- void UART_watch_rxd()
- {
- union _useful_
- {
- unsigned val;
- char character;
- } x;
- while (!kbhit())
- {
- x.val=UART_get_char();
- if (!x.val) continue; // nothing? Continue
- if (x.val&0x100) putc(x.character); // character? Print it
- if (!(x.val&0x0e00)) continue; // any error condidion? No, continue
- if (x.val&0x200) printf("*** Overrun Error ***");
- if (x.val&0x400) printf("*** Parity Error ***");
- if (x.val&0x800) printf("*** Framing Error ***");
- }
- }
-
- If you call these routines from a function/procedure as shown below,
- you've got a small terminal program!
-
- terminal proc near
- ter_loop:
- call UART_watch_rxd ; watch line until a key is pressed
- xor ax,ax ; get that key from the buffer
- int 16h
- cmp al,27 ; is it ESC?
- jz ter_end ; yes, then end this function
- call UART_send ; send the character typed if it's not ESC
- jmp ter_loop ; don't forget to check if data comes in
- ter_end:
- ret
- terminal endp
-
- void terminal()
- {
- int key;
- while (1)
- {
- UART_watch_rxd();
- key=getche();
- if (key==27) break;
- UART_send((char)key);
- }
- }
-
- These, of course, should be called from an embedding routine like the
- following (the assembler routines concatenated will assemble as an .EXE
- file. Put the lines 'code segment' and 'assume cs:code,ss:stack' to the
- front).
-
- main proc near
- call UART_init
- call terminal
- mov ax,4c00h
- int 21h
- main endp
- code ends
- stack segment stack 'stack'
- dw 128 dup (?)
- stack ends
- end main
-
- void main()
- {
- UART_init();
- terminal();
- }
-
- Here we are. Now you've got everything you need to program null-modem
- polling UART software.
- You know the way. Now go and add functions to check if a data set is
- there, then establish a connection. Don't know how? Set DTR, wait for DSR.
- If you want to send, set RTS and wait for CTS before you actually transmit
- data. You don't need to store old values of the MCR: this register is
- readable. Just read in the data, AND/OR the bit required and write the
- byte back.
-
-
- Now for the interrupt-driven version of the program. This is going to be
- a bit voluminous, so I draw the scene and leave the painting to you. If you
- want to implement interrupt-driven routines in a C program use either the
- inline-assembler feature or link the objects together.
-
- First thing to do is initialize the UART the same way as shown above.
- But there is some more work to be done before you enable the UART
- interrupt: FIRST SET THE INTERRUPT VECTOR CORRECTLY! Use Function 0x25 of
- the DOS interrupt 0x21. See also the note on known bugs if you've got a
- 8250.
-
- UART_INT EQU 0Ch ; for COM2 / COM4 use 0bh
- UART_ONMASK EQU 11101111b ; for COM2 / COM4 use 11110111b
- UART_OFFMASK EQU 00010000b ; for COM2 / COM4 use 00001000b
- UART_IERVAL EQU ? ; replace ? by any value between 0h and 0fh
- ; (dependent on which ints you want)
- ; DON'T SET bit 1 yet!
-
- initialize_UART_interrupt proc near
- push ds
- push cs ; build a pointer in DS:DX
- pop ds
- lea dx,interrupt_service_routine
- mov ax,2500h+UART_INT
- int 21h
- pop ds
- mov dx,UART_BASEADDR+4 ; MCR
- in al,dx
- or al,8 ; set OUT2 bit to enable interrupts
- out dx,al
- mov dx,UART_BASEADDR+1 ; IER
- mov al,UART_IERVAL
- out dx,al
- in al,21h ; last thing to do is unmask the int in the ICU
- and al,UART_ONMASK
- out 21h,al
- sti ; and free interrupts if they have been disabled
- ret
- initialize_UART_interrupt endp
-
- Now the interrupt service routine. It has to follow several rules:
- first, it MUST NOT change the contents of any register of the CPU! Then it
- has to tell the ICU (did I tell you that this is the interrupt control
- unit?) that the interrupt is being serviced. Next thing is test which part
- of the UART needs service. Let's have a look at the following procedure:
-
- interupt_service_routine proc far ; define as near if you want to link .COM
- ;*1*
- push ax
- push cx
- push dx
- push bx
- push sp
- push bp
- push si
- push di
- ;*2* replace the part between *1* and *2* by pusha on an 80186+ system
- push ds
- push es
- mov al,20h ; remember: first thing to do in interrupt routines is tell
- out 20h,al ; the ICU about it. This avoids lock-up
- int_loop:
- mov dx,UART_BASEADDR+2 ; IIR
- xor ax,ax ; clear AH; this is the fastest and shortest possibility
- in al,dx ; check IIR info
- test al,1
- jnz int_end
- and al,6 ; we're interested in bit 1 & 2 (see data sheet info)
- mov si,ax ; this is already an index! Well-devised, huh?
- call word ptr cs:int_servicetab[si] ; ensure a near call is used...
- jmp int_loop
- int_end:
- pop es
- pop ds
- ;*3*
- pop di
- pop si
- pop bp
- pop sp
- pop bx
- pop dx
- pop cx
- pop ax
- ;*4* *3* - *4* can be replaced by popa on an 80186+ based system
- iret
- interupt_service_routine endp
-
- This is the part of the service routine that does the decisions. Now we
- need four different service routines to cover all four interrupt source
- possibilities (EVEN IF WE DIDN'T ENABLE THEM!! Since 'unexpected'
- interrupts can have higher priority than 'expected' ones, they can appear
- if an expected [not masked] interrupt situation shows up).
-
- int_servicetab DW int_modem, int_tx, int_rx, int_status
-
- int_modem proc near
- mov dx,UART_BASE+6 ; MSR
- in al,dx
- ; do with the info what you like; probably just ignore it...
- ; but YOU MUST READ THE MSR or you'll lock up the system!
- ret
- int_modem endp
-
- int_tx proc near
- ; get next byte of data from a buffer or something
- ; (remember to set the segment registers correctly!)
- ; and write it to the THR (offset 0)
- ; if no more data is to be sent, disable the THRE interrupt
- ; If the FIFO is used, you can write data as long as bit 5
- ; of the LSR is 0
-
- ; end of data to be sent?
- ; no, jump to end_int_tx
- mov dx,UART_BASEADDR+1
- in al,dx
- and al,00001101b
- out dx,al
- end_int_tx:
- ret
- int_tx endp
-
- int_rx proc near
- mov dx,UART_BASEADDR
- in al,dx
- ; do with the character what you like (best write it to a
- ; FIFO buffer)
- ; the following lines speed up FIFO mode operation
- mov dx,UART_BASEADDR+5
- in al,dx
- test al,1
- jnz int_rx
- ret
- int_rx endp
-
- int_status proc near
- mov dx,UART_BASEADDR+5
- in al,dx
- ; do what you like. It's just important to read the LSR
- ret
- int_status endp
-
- How is data sent now? Write it to a FIFO buffer that is read by the
- interrupt routine. Then set bit 1 of the IER and check if this has already
- started transmission. If not, you'll have to start it by yourself... THIS
- IS DUE TO THOSE NUTTY GUYS AT BIG BLUE WHO DECIDED TO USE EDGE TRIGGERED
- INTERRUPTS INSTEAD OF PROVIDING ONE SINGLE FLIP FLOP FOR THE 8253/8254!
- This procedure can be a C function, too. It is not time-critical at all.
-
- ; copy data to buffer
- mov dx,UART_BASEADDR+1 ; IER
- in al,dx
- or al,2 ; set bit 1
- out dx,al
- mov dx,UART_BASEADDR+5 ; LSR
- in al,dx
- test al,40h ; is there a transmission running?
- jz dont_crank ; yes, so don't mess it up
- call int_tx ; no, crank it up
- dont_crank:
-
- Well, that's it! Your main program has to take care about the buffers,
- nothing else!
-
- One more thing: always remember that at 115,200 baud there is service to
- be done at least every 8 microseconds! On an XT with 4.77 MHz this is
- about 5 assembler commands!! So forget about servicing the serial port at
- this rate in an interrupt-driven manner on such computers. An AT with 12
- MHz probably will manage it if you use 'macro commands' such as pusha and/or
- a 16550A in FIFO mode. An AT can perform about 20 instructions between two
- characters, a 386 with 25 MHz will do about 55, and a 486 with 33 MHz will
- manage about 150. Using a 16550A is strongly recommended at high rates.
- The interrupt service routines can be accelerated by not pushing that
- much registers, and pusha and popa are fast instructions.
-
- Another last thing: due to the poor construction of the PC interrupt
- system, one interrupt line can only be driven by one device. This means if
- you want to use COM3 and your mouse is connected to COM1, you can't use
- interrupt features without disabling the mouse (write 0x0 to the mouse's
- MCR).
-
-
-
- Well, that's the end of my short :-) summary. Don't hesitate to correct
- me if I'm wrong (preferably via email) in the details (I hope not, but it's
- not easy to find typographical and other errors in a text that you've
- written yourself). And please help to complete this list! If you've got
- anything to add, email it to me or post it as an additional FAQ answer.
- Maybe anybody can bring himself to write a summary on modem communication?
-
- Please tell me what you think about it! Is it worth while to do the
- work? Is anybody interested in it? Shall I carry on, complete and re-post
- the list? Please give me feedback!
-
-
- Yours
-
- Chris
-
-
- --
- ---------------------------------------------------------------------
- Christian Blum Universitaet des Saarlandes, Germany
- Friedrich-Ebert-Str. 50 chbl@stud.uni-sb.de
- W-6685 Heiligenwald chbl@sbustd.rz.uni-sb.de
- Germany Tel (+49) (0) 6821 67476
-