/*---------------------------------------------------------------------------- Interrupt-driven serial port code Chris Giese http://SisAndHappy.com/ChrisGiese/ This code is public domain (no copyright). You can do whatever you want with it. Compile with: Turbo C, Borland C for DOS, or Watcom C for DOS 3 June 2017: - Stylistic changes - It can now be built with Turbo C, Watcom C (16- or 32-bit), or DJGPP 9 September 2009: - Initial release To do: - probe IRQ instead of assuming IRQ 3/4 - detect serial chip by probing instead of reading I/O address from BIOS - this code does not currently support more than one COM port per IRQ - allow user control of modem handshake lines (RTS, CTS, etc.) - do something when errors (framing, parity, etc.) are detected ----------------------------------------------------------------------------*/ #if 1 #define _DEBUG 1 #define DEBUG(X) X #else #define _DEBUG 0 #define DEBUG(X) /* nothing */ #endif /*---------------------------------------------------------------------------- DOS COMPILER PORTABILITY CODE EXPORTS: peekw() inportb() outportb() INTERRUPT IRQ2INT() HOOK_INT() UNHOOK_INT() CHAIN_INT() (not DJGPP) vector_t void control_irq(vector_t *v, int enable); ----------------------------------------------------------------------------*/ #if 0 #include #else typedef unsigned short uint16_t; #endif /* MK_FP(), peek(), inportb(), outportb() */ #include /* _dos_getvect(), _dos_setvect(), _chain_intr() */ /* The BIOS programs the 8259 interrupt controllers such that IRQs 0-7 -> INTs 8-15 and IRQs 8-15 -> INTs 0x70-0x77 */ #define IRQ2INT(N) ((N) < 8) ? ((N) + 8) : ((N) + 0x70) /***************************************************************************** Turbo C *****************************************************************************/ #if defined(__TURBOC__) #define peekw(S,O) peek(S,O) #define INTERRUPT interrupt #define HOOK_INT(V,N,H) \ if(!(V)->hooked) \ { \ (V)->hooked = 1; \ (V)->vect_num = (N); \ (V)->ov = _dos_getvect(N); \ _dos_setvect(N, H); \ } #define UNHOOK_INT(V) \ if((V)->hooked) \ { \ (V)->hooked = 0; \ _dos_setvect((V)->vect_num, (V)->ov);\ } #define CHAIN_INT(V) \ _chain_intr((V)->ov); typedef struct { unsigned hooked : 1; unsigned vect_num : 8; void interrupt (*ov)(); } vector_t; /***************************************************************************** 16-bit Watcom C *****************************************************************************/ #elif (defined(__WATCOMC__) && !defined(__386__)) #include #define inportb(P) inp(P) #define outportb(P,V) outp(P,V) #define peekw(S,O) *(uint16_t far *)MK_FP(S,O) #define INTERRUPT interrupt #define HOOK_INT(V,N,H) \ if(!(V)->hooked) \ { \ (V)->hooked = 1; \ (V)->vect_num = (N); \ (V)->ov = _dos_getvect(N); \ _dos_setvect(N, H); \ } #define UNHOOK_INT(V) \ if((V)->hooked) \ { \ (V)->hooked = 0; \ _dos_setvect((V)->vect_num, (V)->ov);\ } #define CHAIN_INT(V) \ _chain_intr((V)->ov); typedef struct { unsigned hooked : 1; unsigned vect_num : 8; void interrupt (*ov)(); } vector_t; /***************************************************************************** 32-bit Watcom C Some of the macros here work only if segment base address = 0, as it is with CauseWay DOS extender. *****************************************************************************/ #elif defined(__WATCOMC__)&&defined(__386__) #include #define inportb(P) inp(P) #define outportb(P,V) outp(P,V) #define peekw(S,O) *(uint16_t *)((S) * 16uL + (O)) #define INTERRUPT interrupt #define HOOK_INT(V,N,H) \ if(!(V)->hooked) \ { \ (V)->hooked = 1; \ (V)->vect_num = (N); \ (V)->ov = _dos_getvect(N); \ _dos_setvect(N, H); \ } #define UNHOOK_INT(V) \ if((V)->hooked) \ { \ (V)->hooked = 0; \ _dos_setvect((V)->vect_num, (V)->ov);\ } #define CHAIN_INT(V) \ _chain_intr((V)->ov); typedef struct { unsigned hooked : 1; unsigned vect_num : 8; void interrupt (*ov)(); } vector_t; /***************************************************************************** DJGPP *****************************************************************************/ #elif defined(__DJGPP__) #include /* _farpeekb(), _farpeekw(), _farpokeb() */ #include /* _go32_... */ #include /* _dos_ds */ #define peekw(S,O) _farpeekw(_dos_ds, (S) * 16uL + (O)) #define INTERRUPT /* nothing */ #define HOOK_INT(V,N,H) \ if(!(V)->hooked) \ { \ (V)->hooked = 1; \ (V)->vect_num = (N); \ (V)->eip = (H); \ _go32_dpmi_get_protected_mode_interrupt_vector(N, &(V)->ov);\ (V)->nv.pm_selector = _my_cs(); \ (V)->nv.pm_offset = (unsigned long)(H); \ _go32_dpmi_allocate_iret_wrapper(&(V)->nv); \ _go32_dpmi_set_protected_mode_interrupt_vector(N, &(V)->nv);\ } #define UNHOOK_INT(V) \ if((V)->hooked) \ { \ (V)->hooked = 0; \ _go32_dpmi_set_protected_mode_interrupt_vector( \ (V)->vect_num, &(V)->ov); \ _go32_dpmi_free_iret_wrapper(&(V)->nv); \ } typedef struct { unsigned hooked : 1; unsigned vect_num : 8; void (*eip)(void); _go32_dpmi_seginfo ov, nv; } vector_t; #else #error Sorry, unsupported compiler #endif /***************************************************************************** enable or disable the IRQ specified by the vector_t object *****************************************************************************/ void control_irq(vector_t *v, int enable) { unsigned j; j = v->vect_num; /* BIOS IRQ mapping: IRQ 0-7 = INT 8-15 */ if(j >= 8 && j <= 15) { j -= 8; j = 1 << j; if(enable) outportb(0x21, inportb(0x21) & ~j); else outportb(0x21, inportb(0x21) | j); } /* BIOS IRQ mapping: IRQ 8-15 = INT 0x70-0x77 */ else if(j >= 0x70 && j <= 0x77) { j -= 0x70; j = 1 << j; if(enable) {/* IRQ2 = cascade: */ outportb(0x21, inportb(0x21) & ~0x04); outportb(0xA1, inportb(0xA1) & ~j); } else outportb(0xA1, inportb(0xA1) | j); } } /*---------------------------------------------------------------------------- QUEUES ----------------------------------------------------------------------------*/ typedef struct { unsigned char *buf; unsigned size, in_ptr, out_ptr; } queue_t; /****************************************************************************/ static int inq(queue_t *q, unsigned char data) { unsigned temp; temp = q->in_ptr + 1; if(temp >= q->size) temp = 0; /* if in_ptr reaches out_ptr, the queue is full */ if(temp == q->out_ptr) return -1; q->buf[q->in_ptr] = data; q->in_ptr = temp; return 0; } /***************************************************************************** this function used to look like this: static int deq(queue_t *q, unsigned char *data) but Watcom C produces broken code for such a function. I don't know why. *****************************************************************************/ static int deq(queue_t *q) { int rv; /* if out_ptr reaches in_ptr, the queue is empty */ if(q->out_ptr == q->in_ptr) return -1; rv = q->buf[q->out_ptr]; q->out_ptr++; if(q->out_ptr >= q->size) q->out_ptr = 0; return rv; } /*---------------------------------------------------------------------------- SERIAL PORT I/O EXPORTS: serial_t (can be re-defined as an opaque type) void serial_destroy(serial_t *port, int do_stats); serial_t *serial_create(unsigned io_addr); int serial_setup(serial_t *port, unsigned long baud, unsigned bits, int enable_fifo); void serial_tx(serial_t *port, char *s); char *serial_rx(serial_t *port, unsigned timeout0); ----------------------------------------------------------------------------*/ #include /* free(), calloc(), malloc(), realloc() */ #include /* memset() */ #include /* printf() */ typedef struct { queue_t rx, tx; char tx_busy; /* number of interrupts: spurious, total, MSR events, transmit, receive... */ unsigned spur_count, int_count, msr_count, tx_count, rx_count; /* ...overrun/parity/framing error, other (the latter should =0) */ unsigned err_count, unknown_count; /* number of: framing errors, parity errors, overrun errors */ unsigned ferr_count, perr_count, oerr_count; /* hardware resources */ unsigned char fifo_size, irq; unsigned io_addr; vector_t vector; } serial_t; // xxx - any way to get rid of this global variable? static serial_t *g_port; /****************************************************************************/ static void INTERRUPT serial_irq(void) { unsigned iir, reason, lsr, j; serial_t *port; int i; port = g_port; /* verify interrupt is from serial chip */ iir = inportb(port->io_addr + 2); if(iir & 0x01) port->spur_count++; else port->int_count++; /* loop while chip signals interrupt */ for(; (iir & 0x01) == 0; ) { /* determine reason for interrupt */ reason = iir; reason >>= 1; reason &= 0x07; switch(reason) { /* MSR event */ case 0: port->msr_count++; /* ...clear MSR interrupt by reading MSR */ (void)inportb(port->io_addr + 6); break; /* transmitter ready for more data */ case 1: port->tx_count++; i = deq(&port->tx); /* nothing more to transmit. Reading from IIR cleared the transmit interrupt. */ if(i < 0) port->tx_busy = 0; /* fill the transmit FIFO to minimize interrupts */ else { for(j = port->fifo_size; j != 0; j--) { outportb(port->io_addr + 0, i); i = deq(&port->tx); if(i < 0) break; } } break; /* received data is available */ case 2: /* stale data in receive FIFO */ case 6: port->rx_count++; /* ...clear receive interrupt by reading RBR */ lsr = inportb(port->io_addr + 5); for(; lsr & 0x01; ) { i = inportb(port->io_addr + 0); // xxx - keep track of queue overflow? (void)inq(&port->rx, i); lsr = inportb(port->io_addr + 5); } break; /* overrun/parity/framing error or break received */ case 3: port->err_count++; /* ...clear error interrupt by reading LSR */ lsr = inportb(port->io_addr + 5); if(lsr & 0x02) port->oerr_count++; if(lsr & 0x04) port->perr_count++; if(lsr & 0x08) port->ferr_count++; break; /* WTF? */ default: port->unknown_count++; break; } iir = inportb(port->io_addr + 2); } if(port->irq >= 8) outportb(0xA0, 0x20); outportb(0x20, 0x20); } /***************************************************************************** Identifies serial chip type (8250, 16550, etc.) Returns FIFO size or 1 if no/defective FIFO. 16650+ detection is UNTESTED. *****************************************************************************/ static unsigned serial_id(unsigned io_addr) { unsigned j, k; /* set EFR = 0 (16650+ chips only) "The EFR can only be accessed after writing [0xBF] to the LCR..." For 16550/A, this code zeroes the FCR instead */ outportb(io_addr + 3, 0xBF); outportb(io_addr + 2, 0); /* set FCR = 1 to enable FIFOs (if any) */ outportb(io_addr + 3, 0); outportb(io_addr + 2, 0x01); /* enabling FIFOs should set bits b7 and b6 in Interrupt ID register */ j = inportb(io_addr + 2) & 0xC0; DEBUG( printf("Serial chip type: ");) /* no FIFO -- check if scratch register exists */ if(j == 0) { outportb(io_addr + 7, 0xA5); outportb(0x80, 0xFF); /* prevent bus float returning 0xA5 */ j = inportb(io_addr + 7); outportb(io_addr + 7, 0x5A); outportb(0x80, 0xFF); /* prevent bus float returning 0x5A */ k = inportb(io_addr + 7); /* scratch register 7 exists */ DEBUG( if(j == 0xA5 && k == 0x5A) printf("8250A/16450 (no FIFO)\n"); else /* all 8250s (including 8250A) are said to have serious problems... */ printf("ewww, 8250/8250B (no FIFO)\n"); ) } /* FIFO exists... */ DEBUG( else if(j == 0x40) printf("UNKNOWN; assuming no FIFO\n"); else if(j == 0x80) printf("16550; defective FIFO disabled\n"); ) else if(j == 0xC0) { /* for 16650+, should be able to read 0 from EFR else will read 1 from FCR */ outportb(io_addr + 3, 0xBF); if(inportb(io_addr + 2) == 0) { DEBUG( printf("16650+ (32-byte FIFO)\n");) return 32; } else { DEBUG( printf("16550A (16-byte FIFO)\n");) return 16; } } return 1; } /****************************************************************************/ void serial_destroy(serial_t *port, int do_stats) { if(port == NULL) return; /* disable all interrupts at the serial chip */ outportb(port->io_addr + 1, 0); /* disable interrupts at the 8259 interrupt controller chips control_irq(&port->vector, 0); */ /* restore old interrupt handler */ UNHOOK_INT(&port->vector); /* free buffer memory */ if(port->rx.buf != NULL) free(port->rx.buf); if(port->tx.buf != NULL) free(port->tx.buf); if(do_stats) { /* interrupts not caused by the serial chip: */ printf("Spurious interrupts: %5u\t", port->spur_count); /* interrupts caused by the serial chip: */ printf("Valid interrupts : %5u\n", port->int_count); /* the next five values should sum to the previous value, I think */ printf("MSR interrupts : %5u\t", port->msr_count); printf("Transmit interrupts: %5u\n", port->tx_count); printf("Receive interrupts : %5u\t", port->rx_count); printf("Error interrupts : %5u\n", port->err_count); /* this value should be zero: */ printf("Unknown interrupts : %5u\t", port->unknown_count); printf("Framing errors : %5u\n", port->ferr_count); printf("Parity errors : %5u\t", port->perr_count); printf("Overrun errors : %5u\n", port->oerr_count); } /* zero the serial_t object to cause a segfault if we attempt to use it after this function */ memset(port, 0, sizeof(serial_t)); free(port); } /****************************************************************************/ #define BUF_SIZE 256 serial_t *serial_create(unsigned io_addr) { serial_t *port; unsigned j; /* create serial port object and allocate buffers */ if((port = calloc(1, sizeof(serial_t))) == NULL || (port->rx.buf = malloc(BUF_SIZE)) == NULL || (port->tx.buf = malloc(BUF_SIZE)) == NULL) { serial_destroy(port, 0); printf("Error in serial_create(): out of memory\n"); return NULL; } port->rx.size = port->tx.size = BUF_SIZE; /* probe for serial chip at given io_addr */ for(j = 0; j < 4; j++) { // xxx - probe for chip; don't use the BIOS like this: if(peekw(0x40, 0 + 2 * j) != io_addr) continue; port->io_addr = io_addr; // xxx - probe IRQ; don't assume these values: // port->irq = (j & 0x01) ? 3 : 4; port->irq = (j & 0x01) ? 4 : 3; port->fifo_size = serial_id(io_addr); DEBUG( printf("I/O=0x%03X, IRQ=%u, BUF_SIZE=%u\n", port->io_addr, port->irq, BUF_SIZE);) /* install interrupt handler */ HOOK_INT(&port->vector, IRQ2INT(port->irq), serial_irq); /* enable serial IRQ at 8259 interrupt controller chip(s) */ control_irq(&port->vector, 1); return port; } printf("Error in serial_create(): no serial chip at I/O address 0x%03X\n", io_addr); serial_destroy(port, 0); return NULL; } /***************************************************************************** Sets bit rate and number of data bits and optionally enables FIFO. Also enables all interrupts except transmit and clears dangling interrupts. *****************************************************************************/ int serial_setup(serial_t *port, unsigned long baud, unsigned bits, int enable_fifo) { unsigned divisor, io_addr, j; if(baud > 115200L || baud < 2) { printf("Error in serial_setup(): bit rate (%lu) must be < 115200 and > 2\n", baud); return -1; } divisor = (unsigned)(115200L / baud); if(bits < 7 || bits > 8) { printf("Error in serial_setup(): number of data bits (%u) must be 7 or 8\n", bits); return -1; } io_addr = port->io_addr; /* set bit rate. b7 of register 3 (DLAB)=1: access bit-rate divisor at registers 0 and 1 */ outportb(io_addr + 3, 0x80); outportb(io_addr + 0, divisor); divisor >>= 8; outportb(io_addr + 1, divisor); /* set 7 or 8 data bits, no parity, 1 stop bit. b7 of register 3 (DLAB)=0: access tx/rx at register 0 and interrupt enable at register 1 */ outportb(io_addr + 3, (bits == 7) ? 2 : 3); /* enable all interrupts at the serial chip */ outportb(io_addr + 1, 0x0F); /* clear FIFO (if any), then enable */ outportb(io_addr + 2, 0x06); if(port->fifo_size > 1 && enable_fifo) outportb(io_addr + 2, 0xC7); else outportb(io_addr + 2, 0); /* loopback off, interrupt gate (Out2) on, handshaking lines (RTS, DTR) off */ outportb(io_addr + 4, 0x08); /* clear dangling interrupts */ while(1) { j = inportb(io_addr + 2); if(j & 0x01) break; (void)inportb(io_addr + 0); (void)inportb(io_addr + 5); (void)inportb(io_addr + 6); } return 0; } /***************************************************************************** xxx - should this function have a timeout; like serial_rx()? *****************************************************************************/ void serial_tx(serial_t *port, char *s) { unsigned j; /* if serial chip is not currently transmitting... */ if(!port->tx_busy) { /* ...fill the serial chip's hardware transmit FIFO */ for(j = port->fifo_size; j != 0; j--) { if(*s == '\0') break; outportb(port->io_addr + 0, *s); s++; } port->tx_busy = 1; } /* store remaining bytes to be transmitted in the software transmit queue */ for(; *s != '\0'; s++) { while(inq(&port->tx, *s)) /* queue full; wait */; } } /****************************************************************************/ char *serial_rx(serial_t *port, unsigned timeout0) { unsigned timeout, alloc, len; char *s; int c; timeout = timeout0 / 10; alloc = len = 0; s = NULL; while(1) { c = deq(&port->rx); /* no data from serial port: do timeout */ if(c < 0) { delay(10); timeout--; /* terminate string and return */ if(timeout == 0) { if(s != NULL && len < alloc) s[len] = '\0'; return s; } } /* a byte is available from the serial port: */ else { /* reset timeout */ timeout = timeout0 / 10; /* enlarge return value string, if necessary leave room for 0 byte at end of string */ if(len + 1 >= alloc) { unsigned new_alloc; char *new_s; /* start with 16 bytes, then double the allocated memory size every time the string overflows */ new_alloc = (alloc == 0) ? 16 : (alloc * 2); new_s = realloc(s, new_alloc); /* no memory left for more data? return what data we have */ if(new_s == NULL) { if(s != NULL && len < alloc) s[len] = '\0'; return s; } s = new_s; alloc = new_alloc; } /* store received byte in string */ s[len] = c; len++; } } } /*---------------------------------------------------------------------------- MAIN ROUTINE ----------------------------------------------------------------------------*/ #include /* free() */ #include /* strstr() */ #include /* printf() */ /* opaque type */ //typedef struct { char dummy; } serial_t; extern serial_t *g_port; void serial_destroy(serial_t *port, int do_stats); serial_t *serial_create(unsigned io_addr); int serial_setup(serial_t *port, unsigned long baud, unsigned bits, int enable_fifo); void serial_tx(serial_t *port, char *s); char *serial_rx(serial_t *port, unsigned timeout0); /****************************************************************************/ #define BPERL 16 /* byte/line for dump */ void dump(void *data_p, unsigned count) { unsigned char *data = (unsigned char *)data_p; unsigned byte1, byte2; while(count != 0) { for(byte1 = 0; byte1 < BPERL; byte1++) { if(count == 0) break; printf("%02X ", data[byte1]); count--; } printf("\t"); for(byte2 = 0; byte2 < byte1; byte2++) { if(data[byte2] < ' ') printf("."); else printf("%c", data[byte2]); } printf("\n"); data += BPERL; } } /****************************************************************************/ int main(void) { unsigned i, io_addr; serial_t *port; char *rx; /* use serial ports known to the BIOS (COM1-COM4) */ for(i = 0; i < 4; i++) { if((io_addr = peekw(0x40, 0 + 2 * i)) == 0) continue; /* open port */ printf("Serial port at I/O address 0x%03X...\n", io_addr); if((g_port = port = serial_create(io_addr)) == NULL) continue; DEBUG( printf("Calling serial_setup()...\n");) /* set 115200 baud, 8N1, FIFO (if any) on */ if(serial_setup(port, 115200L, 8, 1) != 0) // if(serial_setup(port, 115200L, 8, 0) != 0) { printf("Error setting serial port bit rate\n"); serial_destroy(port, 0); continue; } /* detect hardware modem by sending reset command... */ DEBUG( printf("Calling serial_tx()...\n");) serial_tx(port, "ATZ\r"); /* ...and waiting up to 1/2 second... */ DEBUG( printf("Calling serial_rx()...\n");) if((rx = serial_rx(port, 500)) != NULL) { printf("Received from serial port:\n"); dump(rx, strlen(rx)); } else printf("Received NOTHING from serial port\n"); /* ...for echoed command and "OK" reply */ if(rx != NULL && strstr(rx, "ATZ") && strstr(rx, "\r\nOK\r\n")) printf("Hardware modem detected\n"); if(rx != NULL) free(rx); /* close port (_DEBUG=1 to display serial port statistics) */ serial_destroy(port, _DEBUG); } return 0; }