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
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
Newsgroups: comp.sys.ibm.pc,comp.sys.ibm.pc.misc,comp.sys.ibm.pc.programmer