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