This repository has been archived on 2025-05-24. You can view files and clone it, but cannot push or open issues or pull requests.
Files
VARCem/src/devices/ports/serial.c
2018-11-22 16:39:40 -05:00

866 lines
20 KiB
C

/*
* VARCem Virtual ARchaeological Computer EMulator.
* An emulator of (mostly) x86-based PC systems and devices,
* using the ISA,EISA,VLB,MCA and PCI system buses, roughly
* spanning the era between 1981 and 1995.
*
* This file is part of the VARCem Project.
*
* Implementation of NS8250-series UART devices.
*
* The original IBM-PC design did not have any serial ports of
* any kind. Rather, these were offered as add-on devices, most
* likely because a) most people did not need one at the time,
* and, b) this way, IBM could make more money off them.
*
* So, for the PC, the offerings were for an IBM Asynchronous
* Communications Adapter, and, later, a model for synchronous
* communications. The "Async Adapter" was based on the NS8250
* UART chip, and is what we now call the "com" port of the PC.
*
* Of course, many system builders came up with similar boards,
* and even more boards were designed where several I/O functions
* were combined into a single "multi-I/O" board, as that saved
* space and bus slots. Early boards had discrete chips for most
* functions, but later on, many of these were integrated into a
* single "super-I/O" chip.
*
* This file implements the standard NS8250, as well as the later
* 16450 and 16550 series, which fixed bugs and added features
* like FIFO buffers, higher line speeds and DMA transfers.
*
* The lower half of the driver can interface to the host system
* serial ports, or other channels, for real-world access.
*
* Version: @(#)serial.c 1.0.12 2018/11/16
*
* Author: Fred N. van Kempen, <decwiz@yahoo.com>
*
* Copyright 2017,2018 Fred N. van Kempen.
*
* Redistribution and use in source and binary forms, with
* or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the entire
* above notice, this list of conditions and the following
* disclaimer.
*
* 2. Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names
* of its contributors may be used to endorse or promote
* products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <wchar.h>
#define HAVE_STDARG_H
#define dbglog serial_log
#include "../../emu.h"
#include "../../io.h"
#include "../../mem.h"
#include "../../rom.h"
#include "../../timer.h"
#include "../../device.h"
#include "../../devices/system/pic.h"
#include "serial.h"
/* Interrupt reasons. */
#define SER_INT_LSR 0x01
#define SER_INT_RX 0x02
#define SER_INT_TX 0x04
#define SER_INT_MSR 0x08
/* IER register bits. */
#define IER_RDAIE 0x01
#define IER_THREIE 0x02
#define IER_RXLSIE 0x04
#define IER_MSIE 0x08
#define IER_SLEEP 0x10 /* NS16750 */
#define IER_LOWPOWER 0x20 /* NS16750 */
#define IER_MASK 0x0f /* not including SLEEP|LOWP */
/* IIR register bits. */
#define IIR_IP 0x01
#define IIR_IID 0x0e
# define IID_IDMDM 0x00
# define IID_IDTX 0x02
# define IID_IDRX 0x04
# define IID_IDERR 0x06
# define IID_IDTMO 0x0c /* 16550+ */
#define IIR_IIRFE 0xc0 /* 16550+ */
# define IIR_FIFO64 0x20
# define IIR_FIFOBAD 0x80
# define IIR_FIFOENB 0xc0
/* FCR register bits. */
#define FCR_FCRFE 0x01
#define FCR_RFR 0x02
#define FCR_TFR 0x04
#define FCR_SELDMA1 0x08
#define FCR_FENB64 0x20 /* 16750 */
#define FCR_RTLS 0xc0
# define FCR_RTLS1 0x00
# define FCR_RTLS4 0x40
# define FCR_RTLS8 0x80
# define FCR_RTLS14 0xc0
/* LCR register bits. */
#define LCR_WLS 0x03
# define WLS_BITS5 0x00
# define WLS_BITS6 0x01
# define WLS_BITS7 0x02
# define WLS_BITS8 0x03
#define LCR_SBS 0x04
#define LCR_PE 0x08
#define LCR_EP 0x10
#define LCR_PS 0x20
# define PAR_NONE 0x00
# define PAR_EVEN (LCR_PE | LCR_EP)
# define PAR_ODD LCR_PE
# define PAR_MARK (LCR_PE | LCR_PS)
# define PAR_SPACE (LCR_PE | LCR_PS | LCR_EP)
#define LCR_BC 0x40
#define LCR_DLAB 0x80
/* MCR register bits. */
#define MCR_DTR 0x01
#define MCR_RTS 0x02
#define MCR_OUT1 0x04 /* 8250 */
#define MCR_OUT2 0x08 /* 8250, INTEN on IBM-PC */
#define MCR_LMS 0x10
#define MCR_AUTOFLOW 0x20 /* 16750 */
/* LSR register bits. */
#define LSR_DR 0x01
#define LSR_OE 0x02
#define LSR_PE 0x04
#define LSR_FE 0x08
#define LSR_BI 0x10
#define LSR_THRE 0x20
#define LSR_TEMT 0x40
#define LSR_RXFE 0x80
#define LSR_MASK (LSR_BI|LSR_FE|LSR_PE|LSR_OE)
/* MSR register bits. */
#define MSR_DCTS 0x01
#define MSR_DDSR 0x02
#define MSR_TERI 0x04
#define MSR_DDCD 0x08
#define MSR_CTS 0x10
#define MSR_DSR 0x20
#define MSR_RI 0x40
#define MSR_DCD 0x80
#define MSR_MASK (MSR_DDCD|MSR_TERI|MSR_DDSR|MSR_DCTS)
typedef struct serial {
int8_t port; /* port number (0,1,..) */
int8_t irq; /* IRQ channel used */
uint16_t base; /* I/O address used */
int8_t is_pcjr; /* PCjr UART (fixed OUT2) */
int8_t type; /* UART type */
uint8_t int_status;
uint8_t lsr, thr, mcr, rcr, /* UART registers */
iir, ier, lcr, msr;
uint8_t dlab1, dlab2;
uint8_t dat,
hold;
uint8_t scratch;
uint8_t fcr;
/* Callback data. */
serial_ops_t *ops;
void *ops_arg;
int64_t delay;
void *bh; /* BottomHalf handler */
int fifo_read,
fifo_write;
uint8_t fifo[64];
} serial_t;
#ifdef ENABLE_SERIAL_LOG
int serial_do_log = ENABLE_SERIAL_LOG;
#endif
static serial_t ports[SERIAL_MAX]; /* the ports */
#ifdef _LOGGING
static void
serial_log(int level, const char *fmt, ...)
{
# ifdef ENABLE_SERIAL_LOG
va_list ap;
if (serial_do_log >= level) {
va_start(ap, fmt);
pclog_ex(fmt, ap);
va_end(ap);
}
# endif
}
#endif
static void
update_ints(serial_t *dev)
{
int stat = 0;
/* None yet. */
dev->iir = IIR_IP;
if ((dev->ier & IER_RXLSIE) && (dev->int_status & SER_INT_LSR)) {
/* Line Status interrupt. */
stat = 1;
dev->iir = IID_IDERR;
} else if ((dev->ier & IER_RDAIE) && (dev->int_status & SER_INT_RX)) {
/* Received Data available. */
stat = 1;
dev->iir = IID_IDRX;
} else if ((dev->ier & IER_THREIE) && (dev->int_status & SER_INT_TX)) {
/* Transmit Data empty. */
stat = 1;
dev->iir = IID_IDTX;
} else if ((dev->ier & IER_MSIE) && (dev->int_status & SER_INT_MSR)) {
/* Modem Status interrupt. */
stat = 1;
dev->iir = IID_IDMDM;
}
DEBUG("Serial%d: intr, IIR=%02X, type=%d, mcr=%02X\n",
dev->port, dev->iir, dev->type, dev->mcr);
if (stat && ((dev->mcr & MCR_OUT2) || dev->is_pcjr)) {
/* Raise an interrupt. */
if (dev->type < UART_TYPE_16450) {
/* Edge-triggered. */
picint(1 << dev->irq);
} else {
/* Level-triggered. */
picintlevel(1 << dev->irq);
}
} else {
/* Clear an interrupt. */
picintc(1 << dev->irq);
}
}
static void
clear_fifo(serial_t *dev)
{
memset(dev->fifo, 0x00, sizeof(dev->fifo));
dev->fifo_read = dev->fifo_write = 0;
}
static void
write_fifo(serial_t *dev, uint8_t *ptr, uint8_t len)
{
while (len-- > 0) {
dev->fifo[dev->fifo_write++] = *ptr++;
if (dev->fifo_write == sizeof(dev->fifo))
dev->fifo_write = 0;
/*OVERFLOW NOT DETECTED*/
}
if (! (dev->lsr & LSR_DR)) {
dev->lsr |= LSR_DR;
dev->int_status |= SER_INT_RX;
update_ints(dev);
}
}
static uint8_t
read_fifo(serial_t *dev)
{
if (dev->fifo_read != dev->fifo_write) {
dev->dat = dev->fifo[dev->fifo_read++];
if (dev->fifo_read == sizeof(dev->fifo))
dev->fifo_read = 0;
}
#if 0
/* If we have more, generate (new) int. */
if (sp->fifo_read != sp->fifo_write) {
#if 1
sp->delay = 1000*TIMER_USEC;
#else
if (sp->bh != NULL) {
sp->int_status |= SERINT_RECEIVE;
sp->lsr |= LSR_DR;
/* Update interrupt state. */
update_ints(sp);
} else {
sp->delay = 1000*TIMER_USEC;
}
#endif
}
}
#endif
return(dev->dat);
}
static void
receive_callback(void *priv)
{
serial_t *dev = (serial_t *)priv;
dev->delay = 0;
if (dev->fifo_read != dev->fifo_write) {
dev->lsr |= LSR_DR;
dev->int_status |= SER_INT_RX;
update_ints(dev);
}
}
static void
reset_port(serial_t *dev)
{
dev->iir = dev->ier = dev->lcr = 0;
dev->fifo_read = dev->fifo_write = 0;
dev->int_status = 0x00;
}
/* Fake interrupt generator, needed for Serial Mouse. */
static void
read_timer(void *priv)
{
serial_t *dev = (serial_t *)priv;
dev->delay = 0;
if (dev->fifo_read != dev->fifo_write) {
dev->lsr |= LSR_DR;
dev->int_status |= SER_INT_RX;
update_ints(dev);
}
}
#ifdef USE_HOST_SERIAL
/* BHTTY READ COMPLETE handler. */
static void
read_done(void *arg, int num)
{
serial_t *dev = (serial_t *)arg;
/* We can do at least 'num' bytes.. */
while (num-- > 0) {
/* Get a byte from them. */
if (bhtty_read(dev->bh, &dev->hold, 1) < 0) break;
/* Stuff it into the FIFO and set intr. */
write_fifo(dev, &dev->hold, 1);
}
/* We have data waiting for us.. delay a little, and then read it. */
timer_add(ser_timer, &dev->delay, &dev->delay, dev);
}
#endif
static void
ser_write(uint16_t addr, uint8_t val, void *priv)
{
serial_t *dev = (serial_t *)priv;
uint8_t wl, sb, pa, msr;
uint32_t baud, speed;
DEBUG("Serial%i: write(%i, %02x)\n", dev->port, (addr & 0x0007), val);
switch (addr & 0x0007) {
case 0: /* DLAB, DATA */
if (dev->lcr & LCR_DLAB) {
/* DLAB set, set DLAB low byte. */
dev->dlab1 = val;
return;
}
/* DLAB clear, regular data write. */
dev->thr = val;
#ifdef USE_HOST_SERIAL
if (dev->bh != NULL) {
/* We are linked, so send to BH layer. */
bhtty_write((BHTTY *)dev->bh, dev->thr);
}
#endif
if (dev->ops && dev->ops->write)
dev->ops->write(dev, dev->ops_arg, val);
/* WRITE completed, we are ready for more. */
dev->lsr |= LSR_THRE;
dev->int_status |= SER_INT_TX;
update_ints(dev);
/* Loopback echo data to RX needed? */
if (dev->mcr & MCR_LMS) {
write_fifo(dev, &val, 1);
dev->int_status |= SER_INT_TX;
update_ints(dev);
}
break;
case 1: /* DLAB, IER */
if (dev->lcr & LCR_DLAB) {
/* DLAB set, set DLAB high byte. */
dev->dlab2 = val;
return;
}
/* DLAB clear, set IER register bits. */
dev->ier = (val & IER_MASK);
update_ints(dev);
break;
case 2: /* FCR */
#if 0
if (dev->type >= UART_TYPE_16550) {
DEBUG("Serial%i: tried to enable FIFO (%02x), type %d!\n", dev->port, val, dev->type);
dev->fcr = val;
}
#else
dev->fcr = val;
#endif
break;
case 3: /* LCR */
if ((dev->lcr & LCR_DLAB) && !(val & LCR_DLAB)) {
/* We dropped DLAB, so handle baudrate. */
baud = ((dev->dlab2 << 8) | dev->dlab1);
if (baud > 0) {
speed = 115200UL / baud;
DEBUG("Serial%i: divisor %u, baudrate %i\n",
dev->port, baud, speed);
#ifdef USE_HOST_SERIAL
if (dev->bh != NULL)
bhtty_speed((BHTTY *)dev->bh, speed);
#endif
} else {
DEBUG("Serial%i: divisor %u invalid!\n",
dev->port, baud);
}
}
wl = (val & LCR_WLS) + 5; /* databits */
sb = (val & LCR_SBS) ? 2 : 1; /* stopbits */
pa = (val & (LCR_PE|LCR_EP|LCR_PS)) >> 3;
DEBUG("Serial%i: WL=%i SB=%i PA=%i\n", dev->port, wl, sb, pa);
#ifdef USE_HOST_SERIAL
if (dev->bh != NULL)
bhtty_params((BHTTY *)dev->bh, wl, pa, sb);
#endif
dev->lcr = val;
break;
case 4: /*MCR*/
#ifdef USE_HOST_SERIAL
if (dev->bh == NULL) {
/* Not linked, force LOOPBACK mode. */
val |= MCR_LMS;
}
#endif
if ((val & MCR_RTS) && !(dev->mcr & MCR_RTS)) {
/*
* This is old code for use by the Serial Mouse
* driver. If the user toggles RTS, serial mice
* are expected to send an ID, to inform any
* enumerator there 'is' something.
*/
if (dev->ops && dev->ops->mcr) {
dev->ops->mcr(dev, dev->ops_arg);
DEBUG("Serial%i: RTS raised\n", dev->port);
}
}
if ((val & MCR_OUT2) && !(dev->mcr & MCR_OUT2)) {
#ifdef USE_HOST_SERIAL
if (dev->bh != NULL) {
/* Linked, start host port. */
(void)bhtty_active(dev->bh, 1);
} else {
#endif
/* Not linked, start RX timer. */
timer_add(read_timer,
&dev->delay, &dev->delay, dev);
/* Fake CTS, DSR and DCD (for now.) */
dev->msr = (MSR_CTS | MSR_DCTS |
MSR_DSR | MSR_DDSR |
MSR_DCD | MSR_DDCD);
dev->int_status |= SER_INT_MSR;
update_ints(dev);
#ifdef USE_HOST_SERIAL
}
#endif
}
dev->mcr = val;
if (val & MCR_LMS) { /* loopback mode */
msr = (val & 0x0c) << 4;
msr |= (val & MCR_RTS) ? MCR_LMS : 0;
msr |= (val & MCR_DTR) ? MCR_AUTOFLOW : 0;
if ((dev->msr ^ msr) & MSR_CTS)
msr |= MSR_DCTS;
if ((dev->msr ^ msr) & MSR_DSR)
msr |= MSR_DDSR;
if ((dev->msr ^ msr) & MSR_DCD)
msr |= MSR_DDCD;
if ((dev->msr & MSR_TERI) && !(msr & MSR_RI))
msr |= MSR_TERI;
dev->msr = msr;
}
break;
case 5: /*LSR*/
if (val & LSR_MASK)
dev->int_status |= SER_INT_LSR;
if (val & LSR_DR)
dev->int_status |= SER_INT_RX;
if (val & LSR_THRE)
dev->int_status |= SER_INT_TX;
dev->lsr = val;
update_ints(dev);
break;
case 6: /*MSR*/
dev->msr = val;
if (dev->msr & MSR_MASK)
dev->int_status |= SER_INT_MSR;
update_ints(dev);
break;
case 7: /*SCRATCH*/
if (dev->type > UART_TYPE_8250) {
dev->scratch = val;
}
break;
}
}
static uint8_t
ser_read(uint16_t addr, void *priv)
{
serial_t *dev = (serial_t *)priv;
uint8_t ret = 0x00;
switch (addr & 0x0007) {
case 0: /* DATA / DLAB1 */
if (dev->lcr & LCR_DLAB) {
/* DLAB set, read DLAB low byte. */
ret = dev->dlab1;
break;
}
/*
* DLAB clear, regular data read.
* First, clear the RXDATA interrupt.
*/
dev->lsr &= ~LSR_DR;
dev->int_status &= ~SER_INT_RX;
update_ints(dev);
/* If there is data in the RX FIFO, grab it. */
ret = read_fifo(dev);
if (dev->fifo_read != dev->fifo_write) {
dev->delay = 1000LL * TIMER_USEC;
}
break;
case 1: /* LCR / DLAB2 */
if (dev->lcr & LCR_DLAB) {
/* DLAB set, read DLAB high byte. */
ret = dev->dlab2;
} else {
/* DLAB clear, read IER register bits. */
ret = dev->ier;
}
break;
case 2: /* IIR */
ret = dev->iir;
if ((ret & IIR_IID) == IID_IDTX) {
/* Transmit is done. */
dev->int_status &= ~SER_INT_TX;
update_ints(dev);
}
if (dev->type >= UART_TYPE_16550) {
/* If FIFO enabled.. */
if (dev->fcr & FCR_FCRFE)
/* Report FIFO active. */
ret |= FCR_RTLS14;
}
break;
case 3: /* LCR */
ret = dev->lcr;
break;
case 4: /* MCR */
ret = dev->mcr;
break;
case 5: /* LSR */
if (dev->lsr & LSR_THRE)
dev->lsr |= LSR_TEMT;
dev->lsr |= LSR_THRE;
ret = dev->lsr;
if (dev->lsr & 0x1f)
dev->lsr &= ~0x1e;
dev->int_status &= ~SER_INT_LSR;
update_ints(dev);
break;
case 6: /* MSR */
/* Grab current modem status and reset delta bits. */
ret = dev->msr;
dev->msr &= ~0x0f;
/* Clear MSR interrupt status. */
dev->int_status &= ~SER_INT_MSR;
update_ints(dev);
break;
case 7: /* SCRATCH */
if (dev->type > UART_TYPE_8250) {
ret = dev->scratch;
}
break;
}
return(ret);
}
static void *
ser_init(const device_t *info)
{
serial_t *dev;
/* Get the correct device. */
dev = &ports[(info->local & 127) - 1];
/* Set up local/weird stuff. */
if (info->local & 128)
dev->is_pcjr = 1;
/* Clear port. */
reset_port(dev);
/* Enable the I/O handler for this port. */
io_sethandler(dev->base, 8, ser_read,NULL,NULL, ser_write,NULL,NULL, dev);
timer_add(receive_callback, &dev->delay, &dev->delay, dev);
INFO("SERIAL: COM%i (I/O=%04X, IRQ=%i)\n",
info->local & 127, dev->base, dev->irq);
return(dev);
}
static void
ser_close(void *priv)
{
serial_t *dev = (serial_t *)priv;
#ifdef USE_HOST_SERIAL
/* Close the host device. */
if (dev->bh != NULL)
(void)serial_link(dev->port, NULL);
#endif
/* Remove the I/O handler. */
io_removehandler(dev->base, 8,
ser_read,NULL,NULL, ser_write,NULL,NULL, dev);
/* Clear port. */
reset_port(dev);
}
const device_t serial_1_device = {
"COM1:",
0,
1,
ser_init, ser_close, NULL,
NULL, NULL, NULL, NULL,
NULL
};
const device_t serial_2_device = {
"COM2:",
0,
2,
ser_init, ser_close, NULL,
NULL, NULL, NULL, NULL,
NULL,
};
const device_t serial_1_pcjr_device = {
"COM1:",
0,
1+128,
ser_init, ser_close, NULL,
NULL, NULL, NULL, NULL,
NULL
};
/* API: (re-)initialize all serial ports. */
void
serial_reset(void)
{
serial_t *dev;
int i;
DEBUG("SERIAL: reset ([%i] [%i])\n",
serial_enabled[0], serial_enabled[1]);
for (i = 0; i < SERIAL_MAX; i++) {
/* Get the correct device and clear it. */
dev = &ports[i];
memset(dev, 0x00, sizeof(serial_t));
dev->port = i;
dev->type = UART_TYPE_8250;
/* Set up default port and IRQ for the device. */
switch(i) {
case 0: /* standard first port, "COM1" */
dev->base = SERIAL1_ADDR;
dev->irq = 4;
break;
case 1: /* standard second port, "COM2" */
dev->base = SERIAL2_ADDR;
dev->irq = 3;
break;
case 2: /* "COM3" */
dev->base = 0x03e8;
dev->irq = 4;
break;
case 3: /* "COM4" */
dev->base = 0x02e8;
dev->irq = 3;
break;
default:
break;
}
/* Clear port. */
reset_port(dev);
}
}
/* API: set up (the address/IRQ of) one of the serial ports. */
void
serial_setup(int id, uint16_t port, int8_t irq)
{
serial_t *dev = &ports[id];
INFO("SERIAL: setting up COM%i as %04X [enabled=%i]\n",
id+1, port, serial_enabled[id]);
if (! serial_enabled[id]) return;
dev->base = port;
dev->irq = irq;
}
/* API: attach another device to a serial port. */
void *
serial_attach(int port, serial_ops_t *ops, void *arg)
{
serial_t *dev;
/* No can do if port not enabled. */
if (! serial_enabled[port]) return(NULL);
/* Grab the desired port block. */
dev = &ports[port];
/* Set up callback info. */
dev->ops = ops;
dev->ops_arg = arg;
return(dev);
}
/* API: clear the FIFO buffers of a serial port. */
void
serial_clear(void *arg)
{
serial_t *dev = (serial_t *)arg;
clear_fifo(dev);
}
/* API: write data to a serial port. */
void
serial_write(void *arg, uint8_t *ptr, uint8_t len)
{
serial_t *dev = (serial_t *)arg;
write_fifo(dev, ptr, len);
}