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: The serial port (part 2) 
Date: 28 Feb 1993 22:01:59 GMT 
Organization: Studenten-Mail, Rechenzentrum Universitaet des Saarlandes 
NNTP-Posting-Host: sbustd.stud.uni-sb.de 
 
 
Date of release: 25 Feb 1993 
Part two: Programming 
 
 
Programming 
=========== 
 
  Now for the clickety-clickety thing. I hope you're a bit keen in 
assembler programming. 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,1); 
   x=inp(baseaddr+2); 
   if ((x&0x80)==0) return 2; 
   if ((x&0x40)==0) return 3; 
   // some old-fashioned software relies on this! 
   outp(baseaddr+2,0x0); 
   return 4; 
} 
 
  If it's not a 16550A, FIFO mode operation won't work, but there's no 
problem in switching it on nevertheless... If your software doesn't use 
the FIFOs explicitly, write 0x7 to the FCR and mask bits 3, 6 & 7 of the 
IIR. This does not reduce interrupt overhead but makes transmission more 
reliable without changing anything for the software. But remember that the 
16550 has a bug with its FIFOs (see hardware section), so if the function 
above returns 3, switch the FIFOs off. 
 
  This useful function has been provided by Mike Surikov; it allows you to 
detect which interrupt is used by a certain UART. 
 
int detect_IRQ(unsigned baseaddr) 
{ 
  // This function returns -1 if UART has no IRQ 
  // else IRQ level (2-7) [INT 0xA - 0xF, CB] 
  int mask, irq, imr, ier, lcr, mcr; 
  disable();              // disable CPU interrupts 
  // [this is not a standard library function; emulate B-) it with 
  // _asm { cli }  CB] 
  lcr = inp(baseaddr+3);  // Read LCR 
  outp(baseaddr+3, ~0x80 & lcr); // Clear DLAB 
  ier = inp(baseaddr+1);  // Read IER 
  outp(baseaddr+1, 0x00); // Disable all UART interrupts 
  mcr = inp(baseaddr+4);  // Read MCR 
  outp(baseaddr+4, ~0x10 & mcr | 0x0C); // Enable UART interrupt generation 
  imr = inp(0x21);        // Read the interrupt mask register 
  outp(0x20, 0x0A);       // Prepare to read the IRR 
  // Here transmitter must be already empty 
  mask = 0xFC;            // The mask for IRQ2-7 
  outp(baseaddr+1, 0x02); // Enable 'Transmitter Empty' interrupt 
  mask &= inp(0x20);      // Select risen interrupt request 
  outp(baseaddr+1, 0x00); // Disable 'Transmitter Empty' interrupt 
  mask &= ~inp(0x20);     // Select fallen interrupt request 
  outp(baseaddr+1, 0x02); // Enable 'Transmitter Empty' interrupt 
  mask &= inp(0x20);      // Select risen interrupt request 
  outp(0x21, ~mask);      // Unmask only this interrupt(s) 
  outp(0x20, 0x0C);       // Enter the poll mode 
  irq = inp(0x20);        // Accept the high level interrupt 
  inp(baseaddr+5);        // Read LSR to reset line status interrupt 
  inp(baseaddr+0);        // Read RBR to reset data ready interrupt 
  inp(baseaddr+2);        // Read IIR to reset transmitter empty interrupt 
  inp(baseaddr+6);        // Read MSR to reset modem status interrupt 
  outp(baseaddr+1, ier);  // Restore Interrupt Enable Reg 
  outp(baseaddr+3, lcr);  // Restore Line Control Reg 
  outp(baseaddr+4, mcr);  // Restore Modem Control Reg 
  outp(0x20, 0x20);       // End of interrupt mode 
  outp(0x21, imr);        // Restore Interrupt Mask Reg 
  enable();               // Enable the CPU interrupts 
  // [ _asm { sti }   CB] 
  return (irq & 0x80) ? irq & 0x07 : -1; 
} 
 
 
  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 then necessary... 
 
  UART_BASEADDR   the base address of the UART 
  UART_BAUDRATE   the divisor value (eg. 12 for 9600 baud) 
  UART_LCRVAL     the value to be written to the LCR (eg. 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, 0x7 establishes compatibility 
                  (except that there are some bits to be masked in the IIR). 
 
  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  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_end 
  ;*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_end: 
  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 and thus save the call/ret. 
 
  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 has been 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 
  xchg al,ah 
  and  ax,9f00h 
  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) & 0x9f) << 8; 
   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 dx 
  push cs 
  pop  ds 
  mov  dx,pointer 
  mov  ah,9 
  int  21h 
  pop  dx 
  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  ; Beware! Don't call INT 16h with high transmission 
  int  16h   ; rates, it won't work! 
  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  ; better use this for high rates: mov ah,0eh 
  int  21h   ;/                                int 10h 
  pop  ax 
uwr_nodata: 
  test ah,0eh ; any error at all? 
  jz   uwr_loop  ; this speeds up things since errors should be rare 
  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 
uwr_exit: 
  ret 
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_  // unions wanna have names 
      { 
      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&0xe00)) 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 keyboard 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 25h of 
the DOS interrupt 21h. 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 NOT UART ONMASK 
UART_IERVAL   EQU ?   ; replace ? by any value between 0h and 0fh 
                      ; (dependent on which ints you want) 
        ; DON'T SET bit 1 now! 
 
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*                             ; it doesn't matter anyway since IRET is 
  push ax                          ; always a FAR command 
  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 the service being done. 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 1 
 
  ; 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 [not the one of the 16550A, silly!]) 
  ; 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 
  nop 
  nop  ; give the UART some time... 
  nop 
  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 
(turn on FIFOs). 
  The interrupt service routines can be accelerated by not pushing that 
much registers, and pusha and popa are fast replacements for 8 other 
pushs/pops. 
 
  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). 
  There is a way around this (but only with your own software, NOT WITH 
MOUSE DRIVERS!): cut the wire between the card edge-connector and the 
interrupt line driver and solder in a diode (1N4148 will do), cathode to 
edge. Then add a resistor (say, 1k) between the interrupt line at the card 
edge and ground. Doing this on every serial card makes it possible to 
connect two or more serial ports to one interrupt line. If your iron is 
heated anyway, it's a good idea to AND the interrupt signal with BAUDOUT 
divided by 4 (this makes the serial interrupt 'level triggered'). You won't 
encounter interrupt hang-ups any more! It's like someone putting his/her 
finger on your doorbell... you definetely WILL OPEN B-). But the ICU can 
get confused if it is triggered more than about half a million times a 
second... 
 
  I forgot to mention the meaning of XON/XOFF. This is a software method 
of data flow control. If the data set (or the computer) has trouble with 
its buffers, it sends an XOFF character (Ctrl-S, ASCII 0x13) and restarts 
the transmission with an XON character (Ctrl-Q, ASCII 0x11). [Now you know 
why these keys are used to stop/start transmission with some terminals]. 
Since no hardware data flow control is needed, 3 wires are enough to connect 
the two devices. XON means 'transmission on' and XOFF ... well, guess. 
 
 
 
  Well, that's the end of my short B-) 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 a follow-up. Maybe anybody can bring 
him-/herself to write a summary on modem communication? Maybe it's me some 
day... 
 
  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! 
 
  Ah, one more thing. Some people told me that they have difficulties to 
obtain new releases of this file from the news. I decided to start a mailing 
list for new releases. If you want to be added to this list, please email; 
of course I'll remove you if you want me to (and also if mail bounces too 
often). Please specify if you wish to receive this release via email, too. 
 
  Yours 
 
        Chris 
 
 
-- 
------------- This compilation of characters brought to you by: ------------- 
Chris Blum  Fr.-Ebert-Str. 50  66578 Heiligenwald  Germany (+49)(0)6821 67476 
Internet: chbl@stud.uni-sb.de (preferred)   or   et11hks4@etcip1.ee.uni-sb.de 
The opinions expressed above are not necessarily my own, but if anybody feels 
embarrassed by them they most probably are. YOU =:-{ ME ;-) YOU :-) or <ZOT!>