diff --git a/CMakeLists.txt b/CMakeLists.txt index f0b41969..3bd8262c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,9 @@ set(X16_SOURCES src/ymglue.cpp src/midi.c src/extern/ymfm/src/ymfm_opm.cpp + src/extern/serialuart/serialib/serialib.cpp + src/extern/serialuart/serialuart_wrapper.cpp + src/extern/serialuart/serialuartTL16C2550.cpp ) if(WIN32) diff --git a/Makefile b/Makefile index bdffb3c1..c1b3e6d2 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ BUILD_DIR = build all: @cmake -E echo "Building with CMake" cmake -E make_directory $(BUILD_DIR) - cmake -S . -B $(BUILD_DIR) + cmake -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Release cmake --build $(BUILD_DIR) $(ARGS) @cmake -E echo "x16emu and makecart executables can be found in ./$(BUILD_DIR)" diff --git a/src/extern/serialuart/serialib/serialib.cpp b/src/extern/serialuart/serialib/serialib.cpp new file mode 100644 index 00000000..8b7e1764 --- /dev/null +++ b/src/extern/serialuart/serialib/serialib.cpp @@ -0,0 +1,1149 @@ +/*! + \file serialib.cpp + \brief Source file of the class serialib. This class is used for communication over a serial device. + \author Philippe Lucidarme (University of Angers) + \version 2.0 + \date december the 27th of 2019 + \modified September 18th of 2025, by Jason Hill + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +This is a licence-free software, it can be used by anyone who try to build a better world. + */ + +#include "serialib.h" + + + +//_____________________________________ +// ::: Constructors and destructors ::: + + +/*! + \brief Constructor of the class serialib. +*/ +serialib::serialib() +{ +#if defined (_WIN32) || defined( _WIN64) + // Set default value for RTS and DTR (Windows only) + currentStateRTS=true; + currentStateDTR=true; + hSerial = INVALID_HANDLE_VALUE; +#endif +#if defined (__linux__) || defined(__APPLE__) + fd = -1; +#endif +} + + +/*! + \brief Destructor of the class serialib. It close the connection +*/ +// Class desctructor +serialib::~serialib() +{ + closeDevice(); +} + + + +//_________________________________________ +// ::: Configuration and initialization ::: + + + +/*! + \brief Open the serial port + \param Device : Port name (COM1, COM2, ... for Windows ) or (/dev/ttyS0, /dev/ttyACM0, /dev/ttyUSB0 ... for linux) + \param Bauds : Baud rate of the serial port. + + \n Supported baud rate for Windows : + - 110 + - 300 + - 600 + - 1200 + - 2400 + - 4800 + - 9600 + - 14400 + - 19200 + - 38400 + - 56000 + - 57600 + - 115200 + - 128000 + - 256000 + + \n Supported baud rate for Linux :\n + - 110 + - 300 + - 600 + - 1200 + - 2400 + - 4800 + - 9600 + - 19200 + - 38400 + - 57600 + - 115200 + + \n Optionally supported baud rates, depending on Linux kernel:\n + - 230400 + - 460800 + - 500000 + - 576000 + - 921600 + - 1000000 + - 1152000 + - 1500000 + - 2000000 + - 2500000 + - 3000000 + - 3500000 + - 4000000 + + \param Databits : Number of data bits in one UART transmission. + + \n Supported values: \n + - SERIAL_DATABITS_5 (5) + - SERIAL_DATABITS_6 (6) + - SERIAL_DATABITS_7 (7) + - SERIAL_DATABITS_8 (8) + - SERIAL_DATABITS_16 (16) (not supported on Unix) + + \param Parity: Parity type + + \n Supported values: \n + - SERIAL_PARITY_NONE (N) + - SERIAL_PARITY_EVEN (E) + - SERIAL_PARITY_ODD (O) + - SERIAL_PARITY_MARK (MARK) (not supported on Unix) + - SERIAL_PARITY_SPACE (SPACE) (not supported on Unix) + \param Stopbit: Number of stop bits + + \n Supported values: + - SERIAL_STOPBITS_1 (1) + - SERIAL_STOPBITS_1_5 (1.5) (not supported on Unix) + - SERIAL_STOPBITS_2 (2) + + \return 1 success + \return -1 device not found + \return -2 error while opening the device + \return -3 error while getting port parameters + \return -4 Speed (Bauds) not recognized + \return -5 error while writing port parameters + \return -6 error while writing timeout parameters + \return -7 Databits not recognized + \return -8 Stopbits not recognized + \return -9 Parity not recognized + */ +int serialib::openDevice(const char *Device, const unsigned int Bauds, + SerialDataBits Databits, + SerialParity Parity, + SerialStopBits Stopbits) { +#if defined (_WIN32) || defined( _WIN64) + // Open serial port + hSerial = CreateFileA(Device,GENERIC_READ | GENERIC_WRITE,0,0,OPEN_EXISTING,/*FILE_ATTRIBUTE_NORMAL*/0,0); + if(hSerial==INVALID_HANDLE_VALUE) { + if(GetLastError()==ERROR_FILE_NOT_FOUND) + return -1; // Device not found + + // Error while opening the device + return -2; + } + + // Set parameters + + // Structure for the port parameters + DCB dcbSerialParams; + dcbSerialParams.DCBlength=sizeof(dcbSerialParams); + + // Get the port parameters + if (!GetCommState(hSerial, &dcbSerialParams)) return -3; + + // Set the speed (Bauds) + switch (Bauds) + { + case 110 : dcbSerialParams.BaudRate=CBR_110; break; + case 300 : dcbSerialParams.BaudRate=CBR_300; break; + case 600 : dcbSerialParams.BaudRate=CBR_600; break; + case 1200 : dcbSerialParams.BaudRate=CBR_1200; break; + case 2400 : dcbSerialParams.BaudRate=CBR_2400; break; + case 4800 : dcbSerialParams.BaudRate=CBR_4800; break; + case 9600 : dcbSerialParams.BaudRate=CBR_9600; break; + case 14400 : dcbSerialParams.BaudRate=CBR_14400; break; + case 19200 : dcbSerialParams.BaudRate=CBR_19200; break; + case 38400 : dcbSerialParams.BaudRate=CBR_38400; break; + case 56000 : dcbSerialParams.BaudRate=CBR_56000; break; + case 57600 : dcbSerialParams.BaudRate=CBR_57600; break; + case 115200 : dcbSerialParams.BaudRate=CBR_115200; break; + case 128000 : dcbSerialParams.BaudRate=CBR_128000; break; + case 256000 : dcbSerialParams.BaudRate=CBR_256000; break; + default : return -4; + } + //select data size + BYTE bytesize = 0; + switch(Databits) { + case SERIAL_DATABITS_5: bytesize = 5; break; + case SERIAL_DATABITS_6: bytesize = 6; break; + case SERIAL_DATABITS_7: bytesize = 7; break; + case SERIAL_DATABITS_8: bytesize = 8; break; + case SERIAL_DATABITS_16: bytesize = 16; break; + default: return -7; + } + BYTE stopBits = 0; + switch(Stopbits) { + case SERIAL_STOPBITS_1: stopBits = ONESTOPBIT; break; + case SERIAL_STOPBITS_1_5: stopBits = ONE5STOPBITS; break; + case SERIAL_STOPBITS_2: stopBits = TWOSTOPBITS; break; + default: return -8; + } + BYTE parity = 0; + switch(Parity) { + case SERIAL_PARITY_NONE: parity = NOPARITY; break; + case SERIAL_PARITY_EVEN: parity = EVENPARITY; break; + case SERIAL_PARITY_ODD: parity = ODDPARITY; break; + case SERIAL_PARITY_MARK: parity = MARKPARITY; break; + case SERIAL_PARITY_SPACE: parity = SPACEPARITY; break; + default: return -9; + } + // configure byte size + dcbSerialParams.ByteSize = bytesize; + // configure stop bits + dcbSerialParams.StopBits = stopBits; + // configure parity + dcbSerialParams.Parity = parity; + + // Write the parameters + if(!SetCommState(hSerial, &dcbSerialParams)) return -5; + + // Set TimeOut + + // Set the Timeout parameters + timeouts.ReadIntervalTimeout=0; + // No TimeOut + timeouts.ReadTotalTimeoutConstant=MAXDWORD; + timeouts.ReadTotalTimeoutMultiplier=0; + timeouts.WriteTotalTimeoutConstant=MAXDWORD; + timeouts.WriteTotalTimeoutMultiplier=0; + + // Write the parameters + if(!SetCommTimeouts(hSerial, &timeouts)) return -6; + + // Opening successfull + return 1; +#endif +#if defined (__linux__) || defined(__APPLE__) + // Structure with the device's options + struct termios options; + + + // Open device + fd = open(Device, O_RDWR | O_NOCTTY | O_NDELAY); + // If the device is not open, return -2 + if (fd == -1) return -2; + // Open the device in nonblocking mode + fcntl(fd, F_SETFL, FNDELAY); + + + // Get the current options of the port + tcgetattr(fd, &options); + // Clear all the options + bzero(&options, sizeof(options)); + + // Prepare speed (Bauds) + speed_t Speed; + switch (Bauds) + { + case 110 : Speed=B110; break; + case 300 : Speed=B300; break; + case 600 : Speed=B600; break; + case 1200 : Speed=B1200; break; + case 2400 : Speed=B2400; break; + case 4800 : Speed=B4800; break; + case 9600 : Speed=B9600; break; + case 19200 : Speed=B19200; break; + case 38400 : Speed=B38400; break; + case 57600 : Speed=B57600; break; + case 115200 : Speed=B115200; break; +#if defined (B230400) + case 230400 : Speed=B230400; break; +#endif +#if defined (B460800) + case 460800 : Speed=B460800; break; +#endif +#if defined (B500000) + case 500000 : Speed=B500000; break; +#endif +#if defined (B576000) + case 576000 : Speed=B576000; break; +#endif +#if defined (B921600) + case 921600 : Speed=B921600; break; +#endif +#if defined (B1000000) + case 1000000 : Speed=B1000000; break; +#endif +#if defined (B1152000) + case 1152000 : Speed=B1152000; break; +#endif +#if defined (B1500000) + case 1500000 : Speed=B1500000; break; +#endif +#if defined (B2000000) + case 2000000 : Speed=B2000000; break; +#endif +#if defined (B2500000) + case 2500000 : Speed=B2500000; break; +#endif +#if defined (B3000000) + case 3000000 : Speed=B3000000; break; +#endif +#if defined (B3500000) + case 3500000 : Speed=B3500000; break; +#endif +#if defined (B4000000) + case 4000000 : Speed=B4000000; break; +#endif + default : return -4; + } + int databits_flag = 0; + switch(Databits) { + case SERIAL_DATABITS_5: databits_flag = CS5; break; + case SERIAL_DATABITS_6: databits_flag = CS6; break; + case SERIAL_DATABITS_7: databits_flag = CS7; break; + case SERIAL_DATABITS_8: databits_flag = CS8; break; + //16 bits and everything else not supported + default: return -7; + } + int stopbits_flag = 0; + switch(Stopbits) { + case SERIAL_STOPBITS_1: stopbits_flag = 0; break; + case SERIAL_STOPBITS_2: stopbits_flag = CSTOPB; break; + //1.5 stopbits and everything else not supported + default: return -8; + } + int parity_flag = 0; + switch(Parity) { + case SERIAL_PARITY_NONE: parity_flag = 0; break; + case SERIAL_PARITY_EVEN: parity_flag = PARENB; break; + case SERIAL_PARITY_ODD: parity_flag = (PARENB | PARODD); break; + + /* + case SERIAL_PARITY_MARK: parity_flag = (PARENB | PARODD | CMSPAR); break; + case SERIAL_PARITY_SPACE: + parity_flag = (PARENB | CMSPAR); + parity_flag &= ~PARODD; + break; + */// CMSPAR Not supported on MacOS and some Linux distros + + default: return -9; + } + + // Set the baud rate + cfsetispeed(&options, Speed); + cfsetospeed(&options, Speed); + // Configure the device : data bits, stop bits, parity, no control flow + // Ignore modem control lines (CLOCAL) and Enable receiver (CREAD) + options.c_cflag |= ( CLOCAL | CREAD | databits_flag | parity_flag | stopbits_flag); + options.c_iflag |= ( IGNPAR | IGNBRK ); + // Timer unused + options.c_cc[VTIME]=0; + // At least on character before satisfy reading + options.c_cc[VMIN]=0; + // Activate the settings + tcsetattr(fd, TCSANOW, &options); + // Success + return (1); +#endif + +} + +bool serialib::isDeviceOpen() +{ +#if defined (_WIN32) || defined( _WIN64) + return hSerial != INVALID_HANDLE_VALUE; +#endif +#if defined (__linux__) || defined(__APPLE__) + return fd >= 0; +#endif +} + +/*! + \brief Close the connection with the current device +*/ +void serialib::closeDevice() +{ +#if defined (_WIN32) || defined( _WIN64) + CloseHandle(hSerial); + hSerial = INVALID_HANDLE_VALUE; +#endif +#if defined (__linux__) || defined(__APPLE__) + close (fd); + fd = -1; +#endif +} + + + + +//___________________________________________ +// ::: Read/Write operation on characters ::: + + + +/*! + \brief Write a char on the current serial port + \param Byte : char to send on the port (must be terminated by '\0') + \return 1 success + \return -1 error while writting data + */ +int serialib::writeChar(const char Byte) +{ +#if defined (_WIN32) || defined( _WIN64) + // Number of bytes written + DWORD dwBytesWritten; + // Write the char to the serial device + // Return -1 if an error occured + if(!WriteFile(hSerial,&Byte,1,&dwBytesWritten,NULL)) return -1; + // Write operation successfull + return 1; +#endif +#if defined (__linux__) || defined(__APPLE__) + // Write the char + if (write(fd,&Byte,1)!=1) return -1; + + // Write operation successfull + return 1; +#endif +} + + + +//________________________________________ +// ::: Read/Write operation on strings ::: + + +/*! + \brief Write a string on the current serial port + \param receivedString : string to send on the port (must be terminated by '\0') + \return 1 success + \return -1 error while writting data + */ +int serialib::writeString(const char *receivedString) +{ +#if defined (_WIN32) || defined( _WIN64) + // Number of bytes written + DWORD dwBytesWritten; + // Write the string + if(!WriteFile(hSerial,receivedString,strlen(receivedString),&dwBytesWritten,NULL)) + // Error while writing, return -1 + return -1; + // Write operation successfull + return 1; +#endif +#if defined (__linux__) || defined(__APPLE__) + // Lenght of the string + int Lenght=strlen(receivedString); + // Write the string + if (write(fd,receivedString,Lenght)!=Lenght) return -1; + // Write operation successfull + return 1; +#endif +} + +// _____________________________________ +// ::: Read/Write operation on bytes ::: + + + +/*! + \brief Write an array of data on the current serial port + \param Buffer : array of bytes to send on the port + \param NbBytes : number of byte to send + \return 1 success + \return -1 error while writting data + */ +int serialib::writeBytes(const void *Buffer, const unsigned int NbBytes, unsigned int *NbBytesWritten) +{ +#if defined (_WIN32) || defined( _WIN64) + // Write data + DWORD NumBytes; + //if(!WriteFile(hSerial, Buffer, NbBytes, NbBytesWritten, NULL)) + if(!WriteFile(hSerial, Buffer, NbBytes, &NumBytes, NULL)){ + // Error while writing, return -1 + return -1; + } + // Write operation successfull + return 1; +#endif +#if defined (__linux__) || defined(__APPLE__) + // Write data + *NbBytesWritten = write (fd,Buffer,NbBytes); + if (*NbBytesWritten !=(ssize_t)NbBytes) return -1; + // Write operation successfull + return 1; +#endif +} + +int serialib::writeBytes(const void *Buffer, const unsigned int NbBytes) +{ + unsigned int NbBytesWritten; + return writeBytes(Buffer, NbBytes, &NbBytesWritten); +} + +/*! + \brief Wait for a byte from the serial device and return the data read + \param pByte : data read on the serial device + \param timeOut_ms : delay of timeout before giving up the reading + If set to zero, timeout is disable (Optional) + \return 1 success + \return 0 Timeout reached + \return -1 error while setting the Timeout + \return -2 error while reading the byte + */ +int serialib::readChar(char *pByte,unsigned int timeOut_ms) +{ +#if defined (_WIN32) || defined(_WIN64) + // Number of bytes read + DWORD dwBytesRead = 0; + + // Set the TimeOut + timeouts.ReadTotalTimeoutConstant=timeOut_ms; + + // Write the parameters, return -1 if an error occured + if(!SetCommTimeouts(hSerial, &timeouts)) return -1; + + // Read the byte, return -2 if an error occured + if(!ReadFile(hSerial,pByte, 1, &dwBytesRead, NULL)) return -2; + + // Return 0 if the timeout is reached + if (dwBytesRead==0) return 0; + + // The byte is read + return 1; +#endif +#if defined (__linux__) || defined(__APPLE__) + // Timer used for timeout + timeOut timer; + // Initialise the timer + timer.initTimer(); + // While Timeout is not reached + while (timer.elapsedTime_ms()0 success, return the number of bytes read + \return -1 error while setting the Timeout + \return -2 error while reading the byte + \return -3 MaxNbBytes is reached + */ +int serialib::readStringNoTimeOut(char *receivedString,char finalChar,unsigned int maxNbBytes) +{ + // Number of characters read + unsigned int NbBytes=0; + // Returned value from Read + char charRead; + + // While the buffer is not full + while (NbBytes0 success, return the number of bytes read (including the null character) + \return 0 timeout is reached + \return -1 error while setting the Timeout + \return -2 error while reading the character + \return -3 MaxNbBytes is reached + */ +int serialib::readString(char *receivedString,char finalChar,unsigned int maxNbBytes,unsigned int timeOut_ms) +{ + // Check if timeout is requested + if (timeOut_ms==0) return readStringNoTimeOut(receivedString,finalChar,maxNbBytes); + + // Number of bytes read + unsigned int nbBytes=0; + // Character read on serial device + char charRead; + // Timer used for timeout + timeOut timer; + long int timeOutParam; + + // Initialize the timer (for timeout) + timer.initTimer(); + + // While the buffer is not full + while (nbBytes0) + { + // Wait for a byte on the serial link with the remaining time as timeout + charRead=readChar(&receivedString[nbBytes],timeOutParam); + + // If a byte has been received + if (charRead==1) + { + // Check if the character received is the final one + if (receivedString[nbBytes]==finalChar) + { + // Final character: add the end character 0 + receivedString [++nbBytes]=0; + // Return the number of bytes read + return nbBytes; + } + // This is not the final character, just increase the number of bytes read + nbBytes++; + } + // Check if an error occured during reading char + // If an error occurend, return the error number + if (charRead<0) return charRead; + } + // Check if timeout is reached + if (timer.elapsedTime_ms()>timeOut_ms) + { + // Add the end caracter + receivedString[nbBytes]=0; + // Return 0 (timeout reached) + return 0; + } + } + + // Buffer is full : return -3 + return -3; +} + + +/*! + \brief Read an array of bytes from the serial device (with timeout) + \param buffer : array of bytes read from the serial device + \param maxNbBytes : maximum allowed number of bytes read + \param timeOut_ms : delay of timeout before giving up the reading + \param sleepDuration_us : delay of CPU relaxing in microseconds (Linux only) + In the reading loop, a sleep can be performed after each reading + This allows CPU to perform other tasks + \return >=0 return the number of bytes read before timeout or + requested data is completed + \return -1 error while setting the Timeout + \return -2 error while reading the byte + */ +int serialib::readBytes (void *buffer,unsigned int maxNbBytes,unsigned int timeOut_ms, unsigned int sleepDuration_us) +{ +#if defined (_WIN32) || defined(_WIN64) + // Avoid warning while compiling + UNUSED(sleepDuration_us); + + // Number of bytes read + DWORD dwBytesRead = 0; + + // Set the TimeOut + timeouts.ReadTotalTimeoutConstant=(DWORD)timeOut_ms; + + // Write the parameters and return -1 if an error occrured + if(!SetCommTimeouts(hSerial, &timeouts)) return -1; + + + // Read the bytes from the serial device, return -2 if an error occured + if(!ReadFile(hSerial,buffer,(DWORD)maxNbBytes,&dwBytesRead, NULL)) return -2; + + // Return the byte read + return dwBytesRead; +#endif +#if defined (__linux__) || defined(__APPLE__) + // Timer used for timeout + timeOut timer; + // Initialise the timer + timer.initTimer(); + unsigned int NbByteRead=0; + // While Timeout is not reached + while (timer.elapsedTime_ms()0) + { + // Increase the number of read bytes + NbByteRead+=Ret; + // Success : bytes has been read + if (NbByteRead>=maxNbBytes) + return NbByteRead; + } + // Suspend the loop to avoid charging the CPU + usleep (sleepDuration_us); + } + // Timeout reached, return the number of bytes read + return NbByteRead; +#endif +} + + + + +// _________________________ +// ::: Special operation ::: + + + +/*! + \brief Empty receiver buffer + Note that when using serial over USB on Unix systems, a delay of 20ms may be necessary before calling the flushReceiver function + \return If the function succeeds, the return value is nonzero. + If the function fails, the return value is zero. +*/ +char serialib::flushReceiver() +{ +#if defined (_WIN32) || defined(_WIN64) + // Purge receiver + return PurgeComm (hSerial, PURGE_RXCLEAR); +#endif +#if defined (__linux__) || defined(__APPLE__) + // Purge receiver + tcflush(fd,TCIFLUSH); + return true; +#endif +} + + + +/*! + \brief Return the number of bytes in the received buffer (UNIX only) + \return The number of bytes received by the serial provider but not yet read. +*/ +int serialib::available() +{ +#if defined (_WIN32) || defined(_WIN64) + // Device errors + DWORD commErrors; + // Device status + COMSTAT commStatus; + // Read status + ClearCommError(hSerial, &commErrors, &commStatus); + // Return the number of pending bytes + return commStatus.cbInQue; +#endif +#if defined (__linux__) || defined(__APPLE__) + int nBytes=0; + // Return number of pending bytes in the receiver + ioctl(fd, FIONREAD, &nBytes); + return nBytes; +#endif + +} + + + +// __________________ +// ::: I/O Access ::: + +/*! + \brief Set or unset the bit DTR (pin 4) + DTR stands for Data Terminal Ready + Convenience method :This method calls setDTR and clearDTR + \param status = true set DTR + status = false unset DTR + \return If the function fails, the return value is false + If the function succeeds, the return value is true. +*/ +bool serialib::DTR(bool status) +{ + if (status) + // Set DTR + return this->setDTR(); + else + // Unset DTR + return this->clearDTR(); +} + + +/*! + \brief Set the bit DTR (pin 4) + DTR stands for Data Terminal Ready + \return If the function fails, the return value is false + If the function succeeds, the return value is true. +*/ +bool serialib::setDTR() +{ +#if defined (_WIN32) || defined(_WIN64) + // Set DTR + currentStateDTR=true; + return EscapeCommFunction(hSerial,SETDTR); +#endif +#if defined (__linux__) || defined(__APPLE__) + // Set DTR + int status_DTR=0; + ioctl(fd, TIOCMGET, &status_DTR); + status_DTR |= TIOCM_DTR; + ioctl(fd, TIOCMSET, &status_DTR); + return true; +#endif +} + +/*! + \brief Clear the bit DTR (pin 4) + DTR stands for Data Terminal Ready + \return If the function fails, the return value is false + If the function succeeds, the return value is true. +*/ +bool serialib::clearDTR() +{ +#if defined (_WIN32) || defined(_WIN64) + // Clear DTR + currentStateDTR=false; + return EscapeCommFunction(hSerial,CLRDTR); +#endif +#if defined (__linux__) || defined(__APPLE__) + // Clear DTR + int status_DTR=0; + ioctl(fd, TIOCMGET, &status_DTR); + status_DTR &= ~TIOCM_DTR; + ioctl(fd, TIOCMSET, &status_DTR); + return true; +#endif +} + + + +/*! + \brief Set or unset the bit RTS (pin 7) + RTS stands for Data Termina Ready + Convenience method :This method calls setDTR and clearDTR + \param status = true set DTR + status = false unset DTR + \return false if the function fails + \return true if the function succeeds +*/ +bool serialib::RTS(bool status) +{ + if (status) + // Set RTS + return this->setRTS(); + else + // Unset RTS + return this->clearRTS(); +} + + +/*! + \brief Set the bit RTS (pin 7) + RTS stands for Data Terminal Ready + \return If the function fails, the return value is false + If the function succeeds, the return value is true. +*/ +bool serialib::setRTS() +{ +#if defined (_WIN32) || defined(_WIN64) + // Set RTS + currentStateRTS=true; + return EscapeCommFunction(hSerial,SETRTS); +#endif +#if defined (__linux__) || defined(__APPLE__) + // Set RTS + int status_RTS=0; + ioctl(fd, TIOCMGET, &status_RTS); + status_RTS |= TIOCM_RTS; + ioctl(fd, TIOCMSET, &status_RTS); + return true; +#endif +} + + + +/*! + \brief Clear the bit RTS (pin 7) + RTS stands for Data Terminal Ready + \return If the function fails, the return value is false + If the function succeeds, the return value is true. +*/ +bool serialib::clearRTS() +{ +#if defined (_WIN32) || defined(_WIN64) + // Clear RTS + currentStateRTS=false; + return EscapeCommFunction(hSerial,CLRRTS); +#endif +#if defined (__linux__) || defined(__APPLE__) + // Clear RTS + int status_RTS=0; + ioctl(fd, TIOCMGET, &status_RTS); + status_RTS &= ~TIOCM_RTS; + ioctl(fd, TIOCMSET, &status_RTS); + return true; +#endif +} + + + + +/*! + \brief Get the CTS's status (pin 8) + CTS stands for Clear To Send + \return Return true if CTS is set otherwise false + */ +bool serialib::isCTS() +{ +#if defined (_WIN32) || defined(_WIN64) + DWORD modemStat; + GetCommModemStatus(hSerial, &modemStat); + return modemStat & MS_CTS_ON; +#endif +#if defined (__linux__) || defined(__APPLE__) + int status=0; + //Get the current status of the CTS bit + ioctl(fd, TIOCMGET, &status); + return status & TIOCM_CTS; +#endif +} + + + +/*! + \brief Get the DSR's status (pin 6) + DSR stands for Data Set Ready + \return Return true if DTR is set otherwise false + */ +bool serialib::isDSR() +{ +#if defined (_WIN32) || defined(_WIN64) + DWORD modemStat; + GetCommModemStatus(hSerial, &modemStat); + return modemStat & MS_DSR_ON; +#endif +#if defined (__linux__) || defined(__APPLE__) + int status=0; + //Get the current status of the DSR bit + ioctl(fd, TIOCMGET, &status); + return status & TIOCM_DSR; +#endif +} + + + + + + +/*! + \brief Get the DCD's status (pin 1) + CDC stands for Data Carrier Detect + \return true if DCD is set + \return false otherwise + */ +bool serialib::isDCD() +{ +#if defined (_WIN32) || defined(_WIN64) + DWORD modemStat; + GetCommModemStatus(hSerial, &modemStat); + return modemStat & MS_RLSD_ON; +#endif +#if defined (__linux__) || defined(__APPLE__) + int status=0; + //Get the current status of the DCD bit + ioctl(fd, TIOCMGET, &status); + return status & TIOCM_CAR; +#endif +} + + +/*! + \brief Get the RING's status (pin 9) + Ring Indicator + \return Return true if RING is set otherwise false + */ +bool serialib::isRI() +{ +#if defined (_WIN32) || defined(_WIN64) + DWORD modemStat; + GetCommModemStatus(hSerial, &modemStat); + return modemStat & MS_RING_ON; +#endif +#if defined (__linux__) || defined(__APPLE__) + int status=0; + //Get the current status of the RING bit + ioctl(fd, TIOCMGET, &status); + return status & TIOCM_RNG; +#endif +} + + +/*! + \brief Get the DTR's status (pin 4) + DTR stands for Data Terminal Ready + May behave abnormally on Windows + \return Return true if CTS is set otherwise false + */ +bool serialib::isDTR() +{ +#if defined (_WIN32) || defined( _WIN64) + return currentStateDTR; +#endif +#if defined (__linux__) || defined(__APPLE__) + int status=0; + //Get the current status of the DTR bit + ioctl(fd, TIOCMGET, &status); + return status & TIOCM_DTR ; +#endif +} + + + +/*! + \brief Get the RTS's status (pin 7) + RTS stands for Request To Send + May behave abnormally on Windows + \return Return true if RTS is set otherwise false + */ +bool serialib::isRTS() +{ +#if defined (_WIN32) || defined(_WIN64) + return currentStateRTS; +#endif +#if defined (__linux__) || defined(__APPLE__) + int status=0; + //Get the current status of the CTS bit + ioctl(fd, TIOCMGET, &status); + return status & TIOCM_RTS; +#endif +} + + + + + + +// ****************************************** +// Class timeOut +// ****************************************** + + +/*! + \brief Constructor of the class timeOut. +*/ +// Constructor +timeOut::timeOut() +{} + + +/*! + \brief Initialise the timer. It writes the current time of the day in the structure PreviousTime. +*/ +//Initialize the timer +void timeOut::initTimer() +{ +#if defined (NO_POSIX_TIME) + LARGE_INTEGER tmp; + QueryPerformanceFrequency(&tmp); + counterFrequency = tmp.QuadPart; + // Used to store the previous time (for computing timeout) + QueryPerformanceCounter(&tmp); + previousTime = tmp.QuadPart; +#else + gettimeofday(&previousTime, NULL); +#endif +} + +/*! + \brief Returns the time elapsed since initialization. It write the current time of the day in the structure CurrentTime. + Then it returns the difference between CurrentTime and PreviousTime. + \return The number of microseconds elapsed since the functions InitTimer was called. + */ +//Return the elapsed time since initialization +unsigned long int timeOut::elapsedTime_ms() +{ +#if defined (NO_POSIX_TIME) + // Current time + LARGE_INTEGER CurrentTime; + // Number of ticks since last call + int sec; + + // Get current time + QueryPerformanceCounter(&CurrentTime); + + // Compute the number of ticks elapsed since last call + sec=CurrentTime.QuadPart-previousTime; + + // Return the elapsed time in milliseconds + return sec/(counterFrequency/1000); +#else + // Current time + struct timeval CurrentTime; + // Number of seconds and microseconds since last call + int sec,usec; + + // Get current time + gettimeofday(&CurrentTime, NULL); + + // Compute the number of seconds and microseconds elapsed since last call + sec=CurrentTime.tv_sec-previousTime.tv_sec; + usec=CurrentTime.tv_usec-previousTime.tv_usec; + + // If the previous usec is higher than the current one + if (usec<0) + { + // Recompute the microseonds and substract one second + usec=1000000-previousTime.tv_usec+CurrentTime.tv_usec; + sec--; + } + + // Return the elapsed time in milliseconds + return sec*1000+usec/1000; +#endif +} diff --git a/src/extern/serialuart/serialib/serialib.h b/src/extern/serialuart/serialib/serialib.h new file mode 100644 index 00000000..99f78aff --- /dev/null +++ b/src/extern/serialuart/serialib/serialib.h @@ -0,0 +1,273 @@ +/*! +\file serialib.h +\brief Header file of the class serialib. This class is used for communication over a serial device. +\author Philippe Lucidarme (University of Angers) +\version 2.0 +\date December the 27th of 2019 +\modified September 18th of 2025, by Jason Hill + +This Serial library is used to communicate through serial port. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This is a licence-free software, it can be used by anyone who try to build a better world. +*/ + + +#ifndef SERIALIB_H +#define SERIALIB_H + +#if defined(__CYGWIN__) + // This is Cygwin special case + #include +#endif + +// Include for windows +#if defined (_WIN32) || defined (_WIN64) +#if defined(__GNUC__) + // This is MinGW special case + #include +#else + // sys/time.h does not exist on "actual" Windows + #define NO_POSIX_TIME +#endif + // Accessing to the serial port under Windows + #include +#endif + +// Include for Linux +#if defined (__linux__) || defined(__APPLE__) + #include + #include + #include + #include + #include + #include + #include + // File control definitions + #include + #include + #include +#endif + +/*! To avoid unused parameters */ +#define UNUSED(x) (void)(x) + +/** + * number of serial data bits + */ +enum SerialDataBits { + SERIAL_DATABITS_5, /**< 5 databits */ + SERIAL_DATABITS_6, /**< 6 databits */ + SERIAL_DATABITS_7, /**< 7 databits */ + SERIAL_DATABITS_8, /**< 8 databits */ + SERIAL_DATABITS_16, /**< 16 databits */ +}; + +/** + * number of serial stop bits + */ +enum SerialStopBits { + SERIAL_STOPBITS_1, /**< 1 stop bit */ + SERIAL_STOPBITS_1_5, /**< 1.5 stop bits */ + SERIAL_STOPBITS_2, /**< 2 stop bits */ +}; + +/** + * type of serial parity bits + */ +enum SerialParity { + SERIAL_PARITY_NONE, /**< no parity bit */ + SERIAL_PARITY_EVEN, /**< even parity bit */ + SERIAL_PARITY_ODD, /**< odd parity bit */ + SERIAL_PARITY_MARK, /**< mark parity */ + SERIAL_PARITY_SPACE /**< space bit */ +}; + +/*! \class serialib + \brief This class is used for communication over a serial device. +*/ +class serialib +{ +public: + + //_____________________________________ + // ::: Constructors and destructors ::: + + + + // Constructor of the class + serialib (); + + // Destructor + ~serialib (); + + + + //_________________________________________ + // ::: Configuration and initialization ::: + + + // Open a device + int openDevice(const char *Device, const unsigned int Bauds, + SerialDataBits Databits = SERIAL_DATABITS_8, + SerialParity Parity = SERIAL_PARITY_NONE, + SerialStopBits Stopbits = SERIAL_STOPBITS_1); + + // Check device opening state + bool isDeviceOpen(); + + // Close the current device + void closeDevice(); + + + + + //___________________________________________ + // ::: Read/Write operation on characters ::: + + + // Write a char + int writeChar (char); + + // Read a char (with timeout) + int readChar (char *pByte,const unsigned int timeOut_ms=0); + + + + + //________________________________________ + // ::: Read/Write operation on strings ::: + + + // Write a string + int writeString (const char *String); + + // Read a string (with timeout) + int readString ( char *receivedString, + char finalChar, + unsigned int maxNbBytes, + const unsigned int timeOut_ms=0); + + + + // _____________________________________ + // ::: Read/Write operation on bytes ::: + + + // Write an array of bytes + int writeBytes(const void *Buffer, const unsigned int NbBytes, unsigned int *NbBytesWritten); + int writeBytes (const void *Buffer, const unsigned int NbBytes); + + // Read an array of byte (with timeout) + int readBytes (void *buffer,unsigned int maxNbBytes,const unsigned int timeOut_ms=0, unsigned int sleepDuration_us=100); + + + + + // _________________________ + // ::: Special operation ::: + + + // Empty the received buffer + char flushReceiver(); + + // Return the number of bytes in the received buffer + int available(); + + + + + // _________________________ + // ::: Access to IO bits ::: + + + // Set CTR status (Data Terminal Ready, pin 4) + bool DTR(bool status); + bool setDTR(); + bool clearDTR(); + + // Set RTS status (Request To Send, pin 7) + bool RTS(bool status); + bool setRTS(); + bool clearRTS(); + + // Get RI status (Ring Indicator, pin 9) + bool isRI(); + + // Get DCD status (Data Carrier Detect, pin 1) + bool isDCD(); + + // Get CTS status (Clear To Send, pin 8) + bool isCTS(); + + // Get DSR status (Data Set Ready, pin 9) + bool isDSR(); + + // Get RTS status (Request To Send, pin 7) + bool isRTS(); + + // Get CTR status (Data Terminal Ready, pin 4) + bool isDTR(); + + +private: + // Read a string (no timeout) + int readStringNoTimeOut (char *String,char FinalChar,unsigned int MaxNbBytes); + +#if defined (_WIN32) || defined(_WIN64) + // Current DTR and RTS state (can't be read on WIndows) + bool currentStateRTS; + bool currentStateDTR; +#endif + + + + +#if defined (_WIN32) || defined( _WIN64) + // Handle on serial device + HANDLE hSerial; + // For setting serial port timeouts + COMMTIMEOUTS timeouts; +#endif +#if defined (__linux__) || defined(__APPLE__) + int fd; +#endif + +}; + + + +/*! \class timeOut + \brief This class can manage a timer which is used as a timeout. + */ +// Class timeOut +class timeOut +{ +public: + + // Constructor + timeOut(); + + // Init the timer + void initTimer(); + + // Return the elapsed time since initialization + unsigned long int elapsedTime_ms(); + +private: +#if defined (NO_POSIX_TIME) + // Used to store the previous time (for computing timeout) + LONGLONG counterFrequency; + LONGLONG previousTime; +#else + // Used to store the previous time (for computing timeout) + struct timeval previousTime; +#endif +}; + +#endif // serialib_H diff --git a/src/extern/serialuart/serialuartTL16C2550.cpp b/src/extern/serialuart/serialuartTL16C2550.cpp new file mode 100644 index 00000000..7023a67c --- /dev/null +++ b/src/extern/serialuart/serialuartTL16C2550.cpp @@ -0,0 +1,1021 @@ +/*! +\file serialuartTL16C2550.cpp +\brief Source file of the class serialuartTL16C2550. A serial UART for Commander X16 Emulator +\author Jason Hill +\version 0.3 +\date September 18th of 2025, by Jason Hill +\modified March 2026 – Added 16-byte RX/TX FIFOs, threading, and full TL16C2550 interrupt system + +This Serial library is used for communication of a physical serial device on the X16 emulator. Simulating +some aspects of the TL16C2550 for use on personal computers. + +Description: This file simulates the Texas Instruments TL16C2550 UART as faithfully as an OS-level +serial port allows. The implementation adds: + + - True 16-byte RX and TX FIFOs (std::deque, capacity-limited to UART_HW_FIFO_SIZE). + - Per-character error flags (OE/PE/FE/BI) stored with each RX FIFO entry and exposed + through LSR only when the flagged character is at the head of the FIFO. + - Full TL16C2550 interrupt priority chain: + 1. Receiver Line Status (OE/PE/FE/BI) – highest priority + 2. Received Data Available / FIFO trigger + 3. Character Timeout (FIFO idle 4 char-times) + 4. Transmitter Holding Register Empty + 5. Modem Status change – lowest priority + - IIR correctly encodes the highest-priority pending source; bits 7:6 reflect + FIFO-enabled state. + - IRQ output gated by MCR OUT2 (bit 3), exposed via irqPending(). + - MSR delta bits (DCTS/DDSR) set on modem signal transitions, cleared on MSR read. + - Thread-safe FIFO access: threadCycleUpdate() pushes RX / drains TX from a background + thread; addrread() / addrwrite() called/run on the emulator thread. + +On hardware, UART1 is connected to an ESP32 on the X16 Serial/Wifi Card. An ESP8266 or +ESP32 with appropriate firmware must be attached to a host COM port for the same function. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This is a licence-free software, it can be used by anyone who try to build a better world. +*/ + +//#define DEBUGUART +//#define VERBOSEUART + +#include "serialuartTL16C2550.hpp" + +// =========================================================================== +// Constructor / Destructor +// =========================================================================== + +serialuartTL16C2550::serialuartTL16C2550(): + IIR(IIR_NO_INT), IER(0), FCR(0), MCR(0), LCR(0), LSR(LSR_THRE | LSR_TEMT), + MSR(0), scratchReg(0), DLSB(0x60), DMSB(0), requestedDivisor(0x60), + loopvalue(0), + irqRxLineStatus(false), irqRxDataReady(false), irqCharTimeout(false), + irqThrEmpty(false), irqModemStatus(false), + rxTimeoutTicks(0), rxTimeoutThreshold(40), ticksPerChar(4), + prevCTS(false), prevDSR(false), + quit(false), hasCTS(false), hasDSR(false), availCount(0), + checksperbaud(5), //At least (2)twice as fast as baud rate for no delays, 5 is plenty fast + fifo_level(1), + baud_delay(std::chrono::nanoseconds((1000000000 / 9600) / 10)) +{ + //Nothing to see here - see initilization list. +} + +serialuartTL16C2550::~serialuartTL16C2550(){ + quit = true; + if (t.joinable()) { + t.join(); + } + this->serialPort.closeDevice(); +} + +// =========================================================================== +// irqPending +// +// The TL16C2550 drives its IRQ pin low when any enabled interrupt source is +// active AND MCR bit 3 (OUT2) is set. +// =========================================================================== +bool serialuartTL16C2550::irqPending() const { + if (!(MCR & MCR_OUT2)) + return false; // IRQ output tri-stated when OUT2 = 0 + return (IIR & 0x01) == 0; // IIR bit 0 = 0 means interrupt pending +} + +// =========================================================================== +// updateIIR +// +// Rebuilds IIR from the five interrupt-pending flags and the IER mask. +// Must be called after any change to the flags, IER, FCR, or RX FIFO depth. +// +// Priority (TL16C2550 datasheet Table 3): +// 1 = RLS – Receiver Line Status +// 2 = RDA – Received Data Available / FIFO trigger +// 3 = CTI – Character Timeout Indication (FIFO mode only) +// 4 = THRE – Transmitter Holding Register Empty +// 5 = MSR – Modem Status Register +// =========================================================================== +void serialuartTL16C2550::updateIIR(){ + unsigned char newIIR = IIR_NO_INT; // default: no interrupt pending + + // Priority 1 – Receiver Line Status + if ((IER & IER_ELSI) && irqRxLineStatus) { + newIIR = IIR_RX_LINE_STAT; + } + // Priority 2 – Received Data Available (FIFO at or above trigger level) + else if ((IER & IER_ERBFI) && irqRxDataReady) { + newIIR = IIR_RX_DATA; + } + // Priority 3 – Character Timeout (FIFO mode, stale data) + else if ((IER & IER_ERBFI) && irqCharTimeout) { + newIIR = IIR_CHAR_TIMEOUT; + } + // Priority 4 – Transmitter Holding Register Empty + else if ((IER & IER_ETBEI) && irqThrEmpty) { + newIIR = IIR_THR_EMPTY; + } + // Priority 5 – Modem Status + else if ((IER & IER_EDSSI) && irqModemStatus) { + newIIR = IIR_MODEM_STATUS; + } + + // Bits 7:6 indicate FIFO enabled (set to 11 when FCR bit 0 = 1) + if (FCR & FCR_FIFO_EN) { + newIIR |= IIR_FIFO_BITS; + } else { + newIIR &= ~IIR_FIFO_BITS; + } + + IIR = newIIR; +} + +// =========================================================================== +// computeLSR +// +// Builds the live LSR value from the current FIFO state plus the 'sticky' +// error bits that are held in the LSR member until LSR is read. +// +// The TL16C2550 reports per-character error flags (PE/FE/BI) only for the +// character currently at the head of the RX FIFO. OE is set as soon as an +// overrun occurs and is also 'sticky' until LSR is read. +// =========================================================================== +unsigned char serialuartTL16C2550::computeLSR() const { + // Start from the 'sticky' error bits (OE/PE/FE/BI survive until LSR read) + unsigned char lsr = LSR & (LSR_OE | LSR_PE | LSR_FE | LSR_BI); + + // DR – data ready: at least one byte in RX FIFO + if (!rxFifo.empty()) { + lsr |= LSR_DR; + // Per-character errors from the head entry + lsr |= rxFifo.front().err & (LSR_PE | LSR_FE | LSR_BI); + } + + // THRE – TX FIFO (or holding register) empty + if (txFifo.empty()) { + lsr |= LSR_THRE; + } + + // TEMT – transmitter completely empty (FIFO + shift register) + // We don't model the shift register separately; treat TEMT = THRE. + if (txFifo.empty()) { + lsr |= LSR_TEMT; + } + + // RXFE (bit 7) – FIFO mode only: at least one error in any FIFO entry + if (FCR & FCR_FIFO_EN) { + for (const auto &entry : rxFifo) { + if (entry.err) { + lsr |= LSR_RXFE; + break; + } + } + } + + return lsr; +} + +// =========================================================================== +// rxFIFOPush +// +// background threadCycleUpdate() +// Pushes one received byte into the RX FIFO. If the FIFO is full an overrun +// is declared: LSR OE is set, the line-status interrupt is armed, and the new +// byte is discarded (matching TL16C2550 behaviour). +// =========================================================================== +void serialuartTL16C2550::rxFifoPush(unsigned char data, unsigned char errFlags){ + std::lock_guard lock(rxMutex); + + int cap = (FCR & FCR_FIFO_EN) ? UART_HW_FIFO_SIZE : 1; + + if ((int)rxFifo.size() >= cap) { + // Overrun: set OE in 'sticky' LSR bits + LSR |= LSR_OE; + irqRxLineStatus = true; + updateIIR(); + return; // discard the new byte per TL16C2550 datasheet + } + + rxFifo.push_back({data, errFlags}); + + // Any per-character error arms the line-status interrupt + if (errFlags & (LSR_PE | LSR_FE | LSR_BI)) { + irqRxLineStatus = true; + } + + // Check RX trigger level for DataAvailable interrupt + int trigger = (int)fifo_level; // 1, 4, 8, or 14 + if ((int)rxFifo.size() >= trigger) { + irqRxDataReady = true; + irqCharTimeout = false; // DataAvailable takes priority; reset timeout + rxTimeoutTicks = 0; + } + + // Any new byte resets the character-timeout counter + rxTimeoutTicks = 0; + irqCharTimeout = false; + + updateIIR(); +} + +// =========================================================================== +// rxFIFOPop +// +// background threadCycleUpdate() +// +// Removes and returns the head entry. Updates DR, RXFE, DataAvailable, and timeout +// interrupt state after the pop. +// =========================================================================== +bool serialuartTL16C2550::rxFifoPop(UartRxEntry &out){ + std::lock_guard lock(rxMutex); + + if (rxFifo.empty()) + return false; + + out = rxFifo.front(); + rxFifo.pop_front(); + + // If FIFO fell below trigger level, clear DataAvailable interrupt + if ((int)rxFifo.size() < (int)fifo_level) { + irqRxDataReady = false; + } + + // If FIFO is now empty, clear timeout interrupt and reset counter + if (rxFifo.empty()) { + irqCharTimeout = false; + rxTimeoutTicks = 0; + } + + // Re-evaluate line-status interrupt: + // irqRxLineStatus stays set until LSR is read 'sticky', but if the + // head-character error was the only trigger and the head just changed, + // the new head's errors will be reported on the next LSR read. + + updateIIR(); + return true; +} + +// =========================================================================== +// rxFifoReset (FCR bit 1) +// =========================================================================== +void serialuartTL16C2550::rxFifoReset(){ + std::lock_guard lock(rxMutex); + rxFifo.clear(); + irqRxDataReady = false; + irqCharTimeout = false; + rxTimeoutTicks = 0; + // Clear DR and RXFE in the 'sticky' LSR; leave OE/PE/FE/BI until LSR read + LSR &= ~LSR_DR; + updateIIR(); +} + +// =========================================================================== +// txFifoReset (FCR bit 2) +// =========================================================================== +void serialuartTL16C2550::txFifoReset(){ + { + std::lock_guard lock(txMutex); + txFifo.clear(); + } + // Empty TX → arm THRE interrupt + irqThrEmpty = true; + LSR |= (LSR_THRE | LSR_TEMT); + updateIIR(); +} + +// =========================================================================== +// updateMSR +// +// Called by threadCycleUpdate() when CTS or DSR changes. Sets the corresponding +// delta bits in MSR (DCTS = bit 0, DDSR = bit 1) and arms the modem-status +// interrupt. The delta bits are 'sticky' until MSR is read. +// =========================================================================== +void serialuartTL16C2550::updateMSR(bool newCTS, bool newDSR){ + bool changed = false; + + if (newCTS != prevCTS) { + MSR |= 0x01; // DCTS + changed = true; + prevCTS = newCTS; + } + if (newDSR != prevDSR) { + MSR |= 0x02; // DDSR + changed = true; + prevDSR = newDSR; + } + + // Update the current-state bits (bits 5:4 = DSR/CTS on TL16C2550) + if (newCTS) MSR |= 0x20; else MSR &= ~0x20; + if (newDSR) MSR |= 0x10; else MSR &= ~0x10; + + if (changed) { + irqModemStatus = true; + updateIIR(); + } +} + +// =========================================================================== +// threadCycleUpdate (background thread) +// +// Runs at roughly x2 the baud rate. Tasks: +// 1. Drain the TX FIFO to the host serial port (respecting AFE/CTS). +// 2. Push available bytes from the host port into the RX FIFO. +// 3. Advance the character-timeout counter and fire the timeout interrupt +// if the FIFO has been idle for RX_TIMEOUT_CHARS character-times. +// 4. Manage RTS for automatic flow control (MCR AFE bit). +// 5. Detect CTS/DSR transitions and update MSR/interrupt. +// =========================================================================== +void serialuartTL16C2550::threadCycleUpdate(){ + while (!this->quit) { + + // ---------------------------------------------------------------- + // 1. Refresh modem signal states and update MSR / IRQ + // ---------------------------------------------------------------- + bool newCTS = this->serialPort.isCTS(); + bool newDSR = this->serialPort.isDSR(); + hasCTS = newCTS; + hasDSR = newDSR; + updateMSR(newCTS, newDSR); + + // ---------------------------------------------------------------- + // 2. TX: drain TX FIFO → host serial port + // In AFE mode, only send when CTS is asserted. + // ---------------------------------------------------------------- + { + std::lock_guard lock(txMutex); + if (!txFifo.empty()) { + bool allowSend = true; + if (MCR & MCR_AFE) { + allowSend = hasCTS; + } + if (allowSend) { + unsigned char txByte = txFifo.front(); + txFifo.pop_front(); + + // Write to physical port + this->serialPort.writeBytes(&txByte, 1); + + // If FIFO just became empty, arm THRE interrupt + if (txFifo.empty()) { + irqThrEmpty = true; + LSR |= (LSR_THRE | LSR_TEMT); + updateIIR(); + } + } + } + } + + // ---------------------------------------------------------------- + // 3. RX: pump host port → RX FIFO + // Read up to (FIFO capacity – current occupancy) bytes so we + // never overflow the FIFO in one tick. + // ---------------------------------------------------------------- + availCount = this->dataAvailable(); + if (availCount > 0) { + int cap = (FCR & FCR_FIFO_EN) ? UART_HW_FIFO_SIZE : 1; + int occupied; + { + std::lock_guard lock(rxMutex); + occupied = (int)rxFifo.size(); + } + int canReceive = cap - occupied; + + for (int i = 0; i < canReceive && i < availCount; ++i) { + unsigned char rxByte = 0; + int result = this->serialPort.readBytes(&rxByte, 1, 0, 0); + if (result <= 0) + break; + // Push with no error flags; physical parity/framing errors + // are surfaced by the host OS and could be added here if the + // OS API exposes them (platform-specific extension point). + rxFifoPush(rxByte, 0); + } + } + + // ---------------------------------------------------------------- + // 4. Character Timeout + // If the RX FIFO is not empty and no new byte has arrived for + // RX_TIMEOUT_CHARS character-times, assert the timeout interrupt. + // "One character-time" ≈ ticksPerChar background ticks. + // ---------------------------------------------------------------- + { + std::lock_guard lock(rxMutex); + if (!rxFifo.empty() && !irqRxDataReady) { + rxTimeoutTicks++; + if (rxTimeoutTicks >= rxTimeoutThreshold && !irqCharTimeout) { + irqCharTimeout = true; + updateIIR(); + } + } + // If FIFO drained to empty between ticks, reset the counter. + if (rxFifo.empty()) { + rxTimeoutTicks = 0; + irqCharTimeout = false; + } + } + + // ---------------------------------------------------------------- + // 5. AFE automatic RTS control + // Drop RTS when the RX FIFO reaches the trigger level so the + // sender pauses; raise it again once the FIFO drains below trigger. + // ---------------------------------------------------------------- + if (MCR & MCR_AFE) { + std::lock_guard lock(rxMutex); + if ((int)rxFifo.size() >= (int)fifo_level) { + if (serialPort.isRTS()) { + this->serialPort.clearRTS(); + } + } else { + if (!serialPort.isRTS()) { + this->serialPort.setRTS(); + } + } + } + + std::this_thread::sleep_for(std::chrono::nanoseconds(500)); + if (this->quit) + break; + } +} + +// =========================================================================== +// addrwrite - address write +// +// Called by the emulator when 6502 code writes to a UART register address. +// address bits 2:0 select the register; bit 3 (or higher) selects the channel +// on multi-channel devices – this class models one channel so we mask to 0x07. +// =========================================================================== +int serialuartTL16C2550::addrwrite(unsigned char *value, int address){ + int retVal = 0; + +#ifdef DEBUGUART + if (address != 0) { + printf("WriteAddress: $%02x\tValue: $%02x", address, (int)(*value)); + printf("\t-----\tIER: $%02x IIR: $%02x MCR: $%02x LSR: $%02x MSR: $%02x" + " Scratch: $%02x LCR: $%02x DLSB: $%02x DMSB: $%02x", + (int)IER, (int)IIR, (int)MCR, (int)LSR, (int)MSR, + (int)scratchReg, (int)LCR, (int)DLSB, (int)DMSB); + printf(" Divisor: %d\tBaud: %d\n", + requestedDivisor, baudCalculator(requestedDivisor)); + } +#endif + + bool LCRconfigDirty = false; + + switch (address & 0x07) { + + // ------------------------------------------------------------------- + // Offset 0 – Transmit Holding Register (DLAB=0) / DLL (DLAB=1) + // ------------------------------------------------------------------- + case 0: + if (LCR & 0x80 /* DLAB = 1 */) { + DLSB = *value; + retVal = DLSB; + } else { + if (MCR & MCR_LOOPBACK) { + // Loopback: route the byte directly into the RX FIFO. + loopvalue = *value; + rxFifoPush(loopvalue, 0); + retVal = 1; + } else { + // Push into TX FIFO (respect capacity) + { + std::lock_guard lock(txMutex); + int cap = (FCR & FCR_FIFO_EN) ? UART_HW_FIFO_SIZE : 1; + if ((int)txFifo.size() < cap) { + txFifo.push_back(*value); + retVal = 1; + } else { + retVal = 0; // TX FIFO full – byte dropped (caller should check THRE) + } + } + // Writing to THR clears the THRE interrupt + irqThrEmpty = false; + LSR &= ~(LSR_THRE | LSR_TEMT); + updateIIR(); + } + } + break; + + // ------------------------------------------------------------------- + // Offset 1 – Interrupt Enable Register (DLAB=0) / DLM (DLAB=1) + // ------------------------------------------------------------------- + case 1: + if (LCR & 0x80 /* DLAB = 1 */) { + DMSB = *value; + retVal = DMSB; + } else { + IER = 0x0F & (*value); + // If ETBEI was just enabled and TX FIFO is already empty, + // immediately arm the THRE interrupt. + if ((IER & IER_ETBEI) && txFifo.empty()) { + irqThrEmpty = true; + } + updateIIR(); + retVal = IER; + } + break; + + // ------------------------------------------------------------------- + // Offset 2 – FIFO Control Register (write-only) + // + // FCR is write-only on real hardware; the shadow copy is kept in FCR. + // Bit 0: FIFO enable. Bits 1/2: RX/TX FIFO reset. Bits 7:6: RX trigger. + // ------------------------------------------------------------------- + case 2: { + unsigned char oldFCR = FCR; + FCR = 0xCF & (*value); // bits 4:3 are not used on TL16C2550 + + // Disabling FIFOs also clears them (datasheet §9.3) + if ((oldFCR & FCR_FIFO_EN) && !(FCR & FCR_FIFO_EN)) { + rxFifoReset(); + txFifoReset(); + } + + // Explicit reset commands (can be issued even while enabling) + if (FCR & FCR_RX_RESET) { + rxFifoReset(); + FCR &= ~FCR_RX_RESET; // self-clearing + } + if (FCR & FCR_TX_RESET) { + txFifoReset(); + FCR &= ~FCR_TX_RESET; // self-clearing + } + + // Decode RX trigger level from bits 7:6 + switch (FCR & FCR_RX_TRIG) { + case 0x00: fifo_level = 1; break; + case 0x40: fifo_level = 4; break; + case 0x80: fifo_level = 8; break; + case 0xC0: fifo_level = 14; break; + default: fifo_level = 1; break; + } + + // Compute character-timeout threshold in threadCycleUpdate ticks. + // One character-time at checksperbaud ticks per baud event ≈ + // checksperbaud ticks. Four character-times: + ticksPerChar = checksperbaud; + rxTimeoutThreshold = RX_TIMEOUT_CHARS * ticksPerChar; + + updateIIR(); + retVal = FCR; + break; + } + + // ------------------------------------------------------------------- + // Offset 3 – Line Control Register + // ------------------------------------------------------------------- + case 3: + // Detect exit from Divisor Latch Access mode → baud rate change + if (LCR & 0x80) { // currently DLAB=1 + if (!((*value) & 0x80)) { // transitioning to DLAB=0 + requestedDivisor = DLSB; + requestedDivisor |= ((uint16_t)(DMSB) << 8); + LCRconfigDirty = true; + } + } + // Detect change in word/parity/stop bits + if ((LCR & 0x7F) != ((*value) & 0x7F)) { + LCRconfigDirty = true; + } + + LCR = *value; + + if (LCRconfigDirty && !(LCR & 0x80)) { + reconfigureSerial(); + } + retVal = LCR; + break; + + // ------------------------------------------------------------------- + // Offset 4 – Modem Control Register + // ------------------------------------------------------------------- + case 4: + if (*value != MCR) { + MCR = *value; + + // DTR pin + if (MCR & MCR_DTR) { + if (!serialPort.isDTR()) this->serialPort.DTR(true); + } else { + if ( serialPort.isDTR()) this->serialPort.DTR(false); + } + + // RTS pin (only when not in AFE mode; AFE manages it automatically) + if (!(MCR & MCR_AFE)) { + if (MCR & MCR_RTS) { + if (!serialPort.isRTS()) this->serialPort.RTS(true); + } else { + if ( serialPort.isRTS()) this->serialPort.RTS(false); + } + } + + // Loopback mode: when entering loopback the MSR input bits + // are driven by the MCR output bits (OUT2→DCD, OUT1→RI, + // RTS→CTS, DTR→DSR) – see TL16C2550 datasheet §7.6 + if (MCR & MCR_LOOPBACK) { + bool lbCTS = (MCR & MCR_RTS) != 0; + bool lbDSR = (MCR & MCR_DTR) != 0; + updateMSR(lbCTS, lbDSR); + } + + // If ETBEI is enabled and TX is now empty, arm THRE + if ((IER & IER_ETBEI) && txFifo.empty()) { + irqThrEmpty = true; + updateIIR(); + } + } + retVal = MCR; + break; + + // ------------------------------------------------------------------- + // Offset 5 – Line Status Register (read-only) + // ------------------------------------------------------------------- + case 5: + retVal = -5; // writes ignored + break; + + // ------------------------------------------------------------------- + // Offset 6 – Modem Status Register (read-only) + // ------------------------------------------------------------------- + case 6: + retVal = -6; // writes ignored + break; + + // ------------------------------------------------------------------- + // Offset 7 – Scratch Register + // ------------------------------------------------------------------- + case 7: + scratchReg = *value; + retVal = 0; + break; + + default: + break; + } + return retVal; +} + +// =========================================================================== +// addrread - Address Read +// +// Called by the emulator when 6502 code reads from a UART register address. +// =========================================================================== +int serialuartTL16C2550::addrread(unsigned char *value, int address){ + int retVal = -1; + +#ifdef DEBUGUART + if (address != 0) { + printf("ReadAddress: $%02x", address); + printf("\tFCR:$%02x\t\tIER: $%02x IIR: $%02x MCR: $%02x LSR: $%02x" + " MSR: $%02x Scratch: $%02x LCR: $%02x DLSB: $%02x DMSB: $%02x", + (int)FCR, (int)IER, (int)IIR, (int)MCR, (int)LSR, (int)MSR, + (int)scratchReg, (int)LCR, (int)DLSB, (int)DMSB); + printf("\n"); + } +#endif + + switch (address & 0x07) { + + // ------------------------------------------------------------------- + // Offset 0 – Receive Buffer Register (DLAB=0) / DLL (DLAB=1) + // ------------------------------------------------------------------- + case 0: + if (LCR & 0x80 /* DLAB = 1 */) { + *value = DLSB; + retVal = 1; + } else { + if (MCR & MCR_LOOPBACK) { + // In loopback the RX FIFO is used, so fall through to normal pop. + // (The loopvalue byte was already pushed into the FIFO on write.) + } + UartRxEntry entry{}; + if (rxFifoPop(entry)) { + *value = entry.data; + retVal = entry.data; + // Reading RBR clears the character-timeout interrupt + irqCharTimeout = false; + updateIIR(); + } else { + *value = 0; + retVal = -1; + } + } + break; + + // ------------------------------------------------------------------- + // Offset 1 – Interrupt Enable Register (DLAB=0) / DLM (DLAB=1) + // ------------------------------------------------------------------- + case 1: + if (LCR & 0x80 /* DLAB = 1 */) { + *value = DMSB; + retVal = 1; + } else { + *value = IER; + retVal = 1; + } + break; + + // ------------------------------------------------------------------- + // Offset 2 – Interrupt Identification Register (read-only) + // + // Reading IIR when THRE is the reported source clears the THRE interrupt + // (it will re-arm automatically the next time the TX FIFO drains to empty). + // ------------------------------------------------------------------- + case 2: + // Refresh IIR before returning it + updateIIR(); + *value = IIR; + retVal = 1; + + // If THRE was the reported interrupt, clear it; it re-arms on next drain + if ((IIR & 0x0E) == IIR_THR_EMPTY) { + irqThrEmpty = false; + updateIIR(); + } + break; + + // ------------------------------------------------------------------- + // Offset 3 – Line Control Register + // ------------------------------------------------------------------- + case 3: + *value = LCR; + retVal = 1; + break; + + // ------------------------------------------------------------------- + // Offset 4 – Modem Control Register + // ------------------------------------------------------------------- + case 4: + *value = MCR & 0x3F; // bits 7:5 are not implemented + retVal = 1; + break; + + // ------------------------------------------------------------------- + // Offset 5 – Line Status Register + // + // Reading LSR: + // • Reports live FIFO state (DR, THRE, TEMT, RXFE) + // • Clears the Receiver Line Status interrupt and 'sticky' OE/PE/FE/BI bits + // ------------------------------------------------------------------- + case 5: + *value = computeLSR(); + retVal = 1; + // Clear 'sticky' error bits and the line-status interrupt + LSR &= ~(LSR_OE | LSR_PE | LSR_FE | LSR_BI); + irqRxLineStatus = false; + updateIIR(); + break; + + // ------------------------------------------------------------------- + // Offset 6 – Modem Status Register + // + // Reading MSR: + // • Reports current CTS/DSR and delta bits + // • Clears delta bits (DCTS/DDSR/TERI/DDCD) and the modem-status interrupt + // ------------------------------------------------------------------- + case 6: + // Bits 7:4 are current modem input states (already maintained in MSR) + // Bits 3:0 are delta bits – cleared on read + *value = MSR; + retVal = MSR; + MSR &= 0xF0; // clear all delta bits + irqModemStatus = false; + updateIIR(); + break; + + // ------------------------------------------------------------------- + // Offset 7 – Scratch Register + // ------------------------------------------------------------------- + case 7: + *value = scratchReg; + retVal = 1; + break; + + default: + retVal = -2; + break; + } + return retVal; +} + +// =========================================================================== +// reconfigureSerial +// +// Closes and re-opens the host serial port with the settings currently +// encoded in LCR and requestedDivisor. Restarts the threadCycleUpdate thread. +// =========================================================================== +void serialuartTL16C2550::reconfigureSerial(){ + SerialDataBits Databits; + SerialStopBits Stopbits; + SerialParity Parity; + int wordLength = LCR & 0x03; + + if (this->serialPort.isDeviceOpen()) { + this->quit = true; + if (t.joinable()) t.join(); + this->quit = false; + this->serialPort.closeDevice(); + } + + // Data bits + switch (wordLength) { + case 0: Databits = SERIAL_DATABITS_5; break; + case 1: Databits = SERIAL_DATABITS_6; break; + case 2: Databits = SERIAL_DATABITS_7; break; + default: Databits = SERIAL_DATABITS_8; break; + } + + // Stop bits + if (LCR & 0x04) { + Stopbits = (wordLength == 0) ? SERIAL_STOPBITS_1_5 : SERIAL_STOPBITS_2; + } else { + Stopbits = SERIAL_STOPBITS_1; + } + + // Parity + if (LCR & 0x08) { + if (LCR & 0x20) { + Parity = (LCR & 0x10) ? SERIAL_PARITY_SPACE : SERIAL_PARITY_MARK; + } else { + Parity = (LCR & 0x10) ? SERIAL_PARITY_EVEN : SERIAL_PARITY_ODD; + } + } else { + Parity = SERIAL_PARITY_NONE; + } + + unsigned int baudRate = baudCalculator(requestedDivisor); + int errorOpening = this->serialPort.openDevice(path.c_str(), baudRate, Databits, Parity, Stopbits); + this->serialPort.DTR(MCR & MCR_DTR); + this->serialPort.RTS(MCR & MCR_AFE); // AFE controls RTS dynamically + + if (errorOpening != 1) { + std::cerr << errorOpening << ": Error opening: " << path << std::endl; + switch (errorOpening) { + case -1: std::cerr << "device not found\n"; break; + case -2: std::cerr << "error while opening the device\n"; break; + case -3: std::cerr << "error while getting port parameters\n"; break; + case -4: std::cerr << "Speed (Bauds) not recognized\n"; break; + case -5: std::cerr << "error while writing port parameters\n"; break; + case -6: std::cerr << "error while writing timeout parameters\n"; break; + case -7: std::cerr << "Databits not recognized\n"; break; + case -8: std::cerr << "Stopbits not recognized\n"; break; + case -9: std::cerr << "Parity not recognized\n"; break; + default: break; + } + } else { + std::cerr << "LCR: $" << std::hex << (int)LCR << std::dec + << "\t Baud: " << baudRate + << "\tData bits: " << 5 + (int)Databits + << "\tParity: " << descParity(static_cast(Parity)) + << "\tStopbits: " << descStopbits(static_cast(Stopbits)) + << "\tDivisor: " << requestedDivisor + << std::endl; + } + + // Update baud-rate timing for threadCycleUpdate + baud_delay = std::chrono::nanoseconds((1000000000 / baudRate) / checksperbaud); + + // ticksPerChar: at checksperbaud ticks-per-baud-event, + // one character ≈ (bits_per_char) ticks. Use 10 for 8N1. + ticksPerChar = checksperbaud; + rxTimeoutThreshold = RX_TIMEOUT_CHARS * ticksPerChar; + + // Restart the background thread + t = std::thread(&serialuartTL16C2550::threadCycleUpdate, this); +} + +// =========================================================================== +// initialization +// =========================================================================== +int serialuartTL16C2550::init(char *port){ + // Power-on defaults (for instance -> ROMTERM.PRG checks IER/LCR/MCR = 0, and scratch register write, to detect the card) + IIR = IIR_NO_INT; + IER = 0; + FCR = 0; + MCR = 0; + LCR = 0; + LSR = LSR_THRE | LSR_TEMT; + MSR = 0; + DLSB = 0x60; // default on emulator launch/reset: 9600 baud with 14.7456 MHz crystal + DMSB = 0; + requestedDivisor = 0x60; + loopvalue = 0; + + irqRxLineStatus = false; + irqRxDataReady = false; + irqCharTimeout = false; + irqThrEmpty = false; + irqModemStatus = false; + + rxTimeoutTicks = 0; + ticksPerChar = checksperbaud; + rxTimeoutThreshold = RX_TIMEOUT_CHARS * ticksPerChar; + prevCTS = false; + prevDSR = false; + hasCTS = false; + hasDSR = false; + availCount = 0; + fifo_level = 1; + + {//Block Scope + std::lock_guard lk(rxMutex); + rxFifo.clear(); + }//mutex released + {//Block Scope + std::lock_guard lk(txMutex); + txFifo.clear(); + }//mutex released + + if (this->serialPort.isDeviceOpen()) { + this->quit = true; + if (t.joinable()) t.join(); + this->quit = false; + this->serialPort.closeDevice(); + } + + // Build the path string (Windows COM >9 requires \\.\\ prefix) +#if defined(_WIN32) || defined(_WIN64) + path = "\\\\.\\"; +#else + path = ""; +#endif + path += (char *)port; + + std::cout << "Trying port: " << path << " - "; + int errorOpening = this->serialPort.openDevice(path.c_str(), 9600); + + if (errorOpening != 1) { + std::cout << (int)errorOpening << ": "; + switch (errorOpening) { + case -1: std::cerr << "device not found\n"; break; + case -2: std::cerr << "error while opening the device\n"; break; + case -3: std::cerr << "error while getting port parameters\n"; break; + case -4: std::cerr << "Speed (Bauds) not recognized\n"; break; + case -5: std::cerr << "error while writing port parameters\n"; break; + case -6: std::cerr << "error while writing timeout parameters\n"; break; + case -7: std::cerr << "Databits not recognized\n"; break; + case -8: std::cerr << "Stopbits not recognized\n"; break; + case -9: std::cerr << "Parity not recognized\n"; break; + default: std::cout << ": Error opening: " << path << std::endl; break; + } + return -1; + } + + printf("Successful connection to %s\n", path.c_str()); + this->serialPort.clearDTR(); + this->serialPort.clearRTS(); + + baud_delay = std::chrono::nanoseconds((1000000000 / 9600) / checksperbaud); + + t = std::thread(&serialuartTL16C2550::threadCycleUpdate, this); + return 0; +} + +// =========================================================================== +// Low-level port helpers +// =========================================================================== + +int serialuartTL16C2550::write(unsigned char *val){ + return this->serialPort.writeBytes(val, 1); +} + +int serialuartTL16C2550::read(unsigned char *readVal){ + return this->serialPort.readBytes(readVal, 1, 1000, 1000); +} + +int serialuartTL16C2550::dataAvailable(){ + return this->serialPort.available(); +} + +int serialuartTL16C2550::baudCalculator(uint16_t divisor){ + if (divisor > 0) + return (OSC / divisor) / 16; + return -1; +} + +// =========================================================================== +// Diagnostic helpers +// =========================================================================== + +std::string serialuartTL16C2550::descParity(int parity){ + switch (parity) { + case 0: return "No Parity"; + case 1: return "Even Parity"; + case 2: return "Odd Parity"; + case 3: return "Mark Parity"; + case 4: return "Space Parity"; + default: return "Error"; + } +} + +std::string serialuartTL16C2550::descStopbits(int stopbits){ + switch (stopbits) { + case 0: return "1 bit"; + case 1: return "1.5 bits"; + case 2: return "2 bits"; + default: return "Error"; + } +} diff --git a/src/extern/serialuart/serialuartTL16C2550.hpp b/src/extern/serialuart/serialuartTL16C2550.hpp new file mode 100644 index 00000000..69014102 --- /dev/null +++ b/src/extern/serialuart/serialuartTL16C2550.hpp @@ -0,0 +1,226 @@ +/*! +\file serialuartTL16C2550.hpp +\brief Header file of the class serialuartTL16C2550. A serial UART for Commander X16 Emulator +\author Jason Hill +\version 0.3 +\date September 18th of 2025, by Jason Hill +\modified March 2026 – Added 16-byte RX/TX FIFOs, threading, and full TL16C2550 interrupt system + +This Serial library is used for communication of a physical serial device on the X16 emulator. Simulating +some aspects of the TL16C2550 for use on personal computers. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This is a licence-free software, it can be used by anyone who try to build a better world. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "serialib/serialib.h" + +// --------------------------------------------------------------------------- +// Hardware FIFO capacity for the TL16C2550 +// --------------------------------------------------------------------------- +static constexpr int UART_HW_FIFO_SIZE = 16; + +// --------------------------------------------------------------------------- +// RX FIFO entry: each byte in the FIFO carries its own per-character error +// flags exactly as the TL16C2550 stores them. The flags occupy LSR bits +// 1-4 (OE/PE/FE/BI) and are reported in LSR only when that character is at +// the head of the FIFO. +// --------------------------------------------------------------------------- +struct UartRxEntry { + unsigned char data; + unsigned char err; // LSR error bits for this character: OE|PE|FE|BI +}; + +// --------------------------------------------------------------------------- +// IIR interrupt-type codes (bits 3:1). Bit 0 = 0 means interrupt pending. +// --------------------------------------------------------------------------- +static constexpr unsigned char IIR_NO_INT = 0x01; // no interrupt pending +static constexpr unsigned char IIR_MODEM_STATUS = 0x00; // priority 5 (lowest) +static constexpr unsigned char IIR_THR_EMPTY = 0x02; // priority 4 +static constexpr unsigned char IIR_RX_DATA = 0x04; // priority 2 +static constexpr unsigned char IIR_RX_LINE_STAT = 0x06; // priority 1 (highest) +static constexpr unsigned char IIR_CHAR_TIMEOUT = 0x0C; // priority 3 (FIFO mode only) +static constexpr unsigned char IIR_FIFO_BITS = 0xC0; // bits 7:6 when FIFOs enabled + +// --------------------------------------------------------------------------- +// LSR bit masks +// --------------------------------------------------------------------------- +static constexpr unsigned char LSR_DR = 0x01; // data ready +static constexpr unsigned char LSR_OE = 0x02; // overrun error +static constexpr unsigned char LSR_PE = 0x04; // parity error +static constexpr unsigned char LSR_FE = 0x08; // framing error +static constexpr unsigned char LSR_BI = 0x10; // break interrupt +static constexpr unsigned char LSR_THRE = 0x20; // transmitter holding register empty +static constexpr unsigned char LSR_TEMT = 0x40; // transmitter empty (shift reg + holding) +static constexpr unsigned char LSR_RXFE = 0x80; // error in RX FIFO (FIFO mode only) + +// --------------------------------------------------------------------------- +// IER bit masks +// --------------------------------------------------------------------------- +static constexpr unsigned char IER_ERBFI = 0x01; // enable RX data available interrupt +static constexpr unsigned char IER_ETBEI = 0x02; // enable TX holding register empty interrupt +static constexpr unsigned char IER_ELSI = 0x04; // enable receiver line status interrupt +static constexpr unsigned char IER_EDSSI = 0x08; // enable modem status interrupt + +// --------------------------------------------------------------------------- +// FCR bit masks +// --------------------------------------------------------------------------- +static constexpr unsigned char FCR_FIFO_EN = 0x01; +static constexpr unsigned char FCR_RX_RESET = 0x02; +static constexpr unsigned char FCR_TX_RESET = 0x04; +static constexpr unsigned char FCR_RX_TRIG = 0xC0; // bits 7:6 select trigger level + +// --------------------------------------------------------------------------- +// MCR bit masks +// --------------------------------------------------------------------------- +static constexpr unsigned char MCR_DTR = 0x01; +static constexpr unsigned char MCR_RTS = 0x02; +static constexpr unsigned char MCR_OUT1 = 0x04; +static constexpr unsigned char MCR_OUT2 = 0x08; // gates IRQ output to system +static constexpr unsigned char MCR_LOOPBACK = 0x10; +static constexpr unsigned char MCR_AFE = 0x20; // Auto Flow-control Enable + +// Number of character-times of RX FIFO inactivity before timeout interrupt +static constexpr int RX_TIMEOUT_CHARS = 4; + +class serialuartTL16C2550 { +public: + serialuartTL16C2550(); + ~serialuartTL16C2550(); + + int init(char*); + int addrwrite(unsigned char *value, int address); + int addrread (unsigned char *value, int address); + + // Returns true when the UART is asserting its IRQ output line. + // MCR bit 3 (OUT2) must be set for IRQ to be driven. + // The X16 emulator will poll this to know whether to assert the CPU's IRQ pin. + bool irqPending() const; + +private: + // ---- TL16C2550 Addressable Registers ---------------------------------------- + unsigned char IIR; // Interrupt Identification Register (read-only) + unsigned char IER; // Interrupt Enable Register + unsigned char FCR; // FIFO Control Register (write-only; shadow kept here) + unsigned char MCR; // Modem Control Register + unsigned char LCR; // Line Control Register + unsigned char LSR; // Line Status Register (partially computed on read) + unsigned char MSR; // Modem Status Register + unsigned char scratchReg; // Scratch Register (no side effects) + unsigned char DLSB; // Divisor Latch – Least Significant Byte (DLAB=1) + unsigned char DMSB; // Divisor Latch – Most Significant Byte (DLAB=1) + uint16_t requestedDivisor; + + // Loopback data register: THR writes -> here; RBR reads <- here in loopback mode + unsigned char loopvalue; + + // ---- 16-Byte RX FIFO -------------------------------------------------------- + // Stores received characters together with their per-character error flags. + // Protected by rxMutex because threadCycleUpdate() pushes from a background thread + // while addrread() pops from the emulator thread. + std::deque rxFifo; + std::mutex rxMutex; + + // ---- 16-Byte TX FIFO -------------------------------------------------------- + // The emulator thread pushes via addrwrite(); threadCycleUpdate() drains to the host + // serial port. + std::deque txFifo; + std::mutex txMutex; + + // ---- Interrupt Pending Flags ------------------------------------------------- + // Each flag is set by the event that causes the condition and cleared when the + // condition is resolved (see TL16C2550 datasheet Table 3 for clearing actions). + bool irqRxLineStatus; // OE/PE/FE/BI in head of FIFO; cleared by LSR read + bool irqRxDataReady; // FIFO at or above trigger level; cleared when FIFO falls below + bool irqCharTimeout; // FIFO non-empty, idle > 4 char-times; cleared by RBR read + bool irqThrEmpty; // TX FIFO empty; cleared by THR write or IIR read + bool irqModemStatus; // CTS/DSR/RI/DCD changed; cleared by MSR read + + // ---- Character-Timeout Tracking --------------------------------------------- + // threadCycleUpdate increments rxTimeoutTicks every tick when RX FIFO is non-empty + // and no new byte arrived. When it reaches rxTimeoutThreshold the timeout + // interrupt fires. Any new RX byte resets the counter. + int rxTimeoutTicks; + int rxTimeoutThreshold; // = RX_TIMEOUT_CHARS * ticks_per_char, updated on baud change + + // Ticks per character at the current baud/format (used to compute threshold) + int ticksPerChar; + + // ---- Modem Signal Tracking -------------------------------------------------- + // Previous states of CTS/DSR, so we can detect transitions for MSR delta bits. + bool prevCTS; + bool prevDSR; + + // ---- Physical Serial Port --------------------------------------------------- + serialib serialPort; + + // Crystal Oscillator Speed on Texelec Serial/Wifi Card + static const int OSC = 14745600; + std::string path; + + // ---- Private Helper Methods ------------------------------------------------- + void reconfigureSerial(); + int write(unsigned char*); + int read (unsigned char*); + int dataAvailable(); + int baudCalculator(uint16_t); + + // Rebuild IIR from the five interrupt-pending flags and IER. + // Call whenever any flag or IER changes. + void updateIIR(); + + // Push a received byte (plus error flags) into rxFifo. + // Handles overrun: if FIFO is full, sets OE and fires line-status interrupt. + void rxFifoPush(unsigned char data, unsigned char errFlags = 0); + + // Pop the head entry from rxFifo. Returns false if empty. + bool rxFifoPop(UartRxEntry &out); + + // Reset (flush) the RX FIFO and clear related interrupt state (FCR bit 1). + void rxFifoReset(); + + // Reset (flush) the TX FIFO and arm THRE interrupt (FCR bit 2). + void txFifoReset(); + + // Recompute the LSR byte from live FIFO state. + // Sticky error bits (OE/PE/FE/BI) are held in the LSR member until LSR is read. + unsigned char computeLSR() const; + + // Update MSR delta bits when CTS/DSR transitions are detected. + void updateMSR(bool newCTS, bool newDSR); + + // ---- Background Thread ------------------------------------------------------ + std::thread t; + bool quit; + void threadCycleUpdate(); // thread body + + // Cached signal states (set by threadCycleUpdate, read by addrwrite/addrread) + bool hasCTS; + bool hasDSR; + int availCount; // host OS bytes available in its own serial buffer + + //Baud-rate timing + //checks-per-baud: How often the thread will scan the UART, at least twice as fast as baud rate, for no delays + //see initilization list for class. + int checksperbaud; + unsigned char fifo_level; // RX trigger level decoded from FCR (1/4/8/14) + std::chrono::steady_clock::duration baud_delay; + + // Diagnostic helpers + std::string descParity(int); + std::string descStopbits(int); +}; diff --git a/src/extern/serialuart/serialuart_wrapper.cpp b/src/extern/serialuart/serialuart_wrapper.cpp new file mode 100644 index 00000000..cf69bfc0 --- /dev/null +++ b/src/extern/serialuart/serialuart_wrapper.cpp @@ -0,0 +1,74 @@ +/*! +\file serialuart_wrapper.cpp +\brief Sourc file to wrap the class serialuartTL16C2550 for use in C. +\author Jason Hill +\version 0.1 +\date September 18th of 2025, by Jason Hill +\modified none + +This Serial library is used for communication of a physical serial device on the X16 emulator. Simulating +some aspects of the TL16C2550 for use on personal computers. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This is a licence-free software, it can be used by anyone who try to build a better world. +*/ + + +#include "serialuart_wrapper.h" +#include "serialuartTL16C2550.hpp" + +serialuartTL16C2550Handle uartCreate() { + return static_cast(new serialuartTL16C2550()); +} + +bool uart_irqPending( serialuartTL16C2550Handle uart){ + serialuartTL16C2550* obj = static_cast(uart); + + if (obj != NULL) { + return obj->irqPending(); + } + + return false; +} + +int uart_init( serialuartTL16C2550Handle uart, char *value ) { + + serialuartTL16C2550* obj = static_cast(uart); + if (obj != NULL) { + return obj->init(value); + } + + return -1; +} + +int uart_addrwrite( serialuartTL16C2550Handle uart, unsigned char *value, int address ) { + + serialuartTL16C2550* obj = static_cast(uart); + if (obj !=NULL) { + return obj->addrwrite(value, address); + } + + return -1; +} + +int uart_addrread( serialuartTL16C2550Handle uart, unsigned char *value, int address ) { + + serialuartTL16C2550* obj = static_cast(uart); + if (obj!=NULL) { + return obj->addrread(value, address); + } + return -1; +} + +void uart_destroy(serialuartTL16C2550Handle uart) { + + serialuartTL16C2550* obj = static_cast(uart); + delete obj; + obj = NULL; + +} diff --git a/src/extern/serialuart/serialuart_wrapper.h b/src/extern/serialuart/serialuart_wrapper.h new file mode 100644 index 00000000..303d37b5 --- /dev/null +++ b/src/extern/serialuart/serialuart_wrapper.h @@ -0,0 +1,43 @@ +/*! +\file serialuart_wrapper.h +\brief Header file to wrap the class serialuartTL16C2550 for use in C. +\author Jason Hill +\version 0.1 +\date September 18th of 2025, by Jason Hill +\modified none + +This Serial library is used for communication of a physical serial device on the X16 emulator. Simulating +some aspects of the TL16C2550 for use on personal computers. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This is a licence-free software, it can be used by anyone who try to build a better world. +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +// Pointer for the C++ Class Instance +typedef void* serialuartTL16C2550Handle; + +// Constructor-like function +serialuartTL16C2550Handle uartCreate(); + +// Method-calling function +int uart_init( serialuartTL16C2550Handle, char*); +int uart_addrwrite( serialuartTL16C2550Handle , unsigned char*, int); +int uart_addrread( serialuartTL16C2550Handle , unsigned char*, int); +bool uart_irqPending( serialuartTL16C2550Handle uart); + + +// Destructor-like function +void uart_destroy( serialuartTL16C2550Handle ); + +#ifdef __cplusplus +} +#endif diff --git a/src/main.c b/src/main.c index efa94eb5..2433f56f 100644 --- a/src/main.c +++ b/src/main.c @@ -44,6 +44,7 @@ #include "cartridge.h" #include "midi.h" #include "git_rev.h" +#include "extern/serialuart/serialuart_wrapper.h" #ifdef __EMSCRIPTEN__ #include @@ -120,6 +121,8 @@ char *gif_path = NULL; char *wav_path = NULL; uint8_t *fsroot_path = NULL; uint8_t *startin_path = NULL; +uint8_t *uart1_path = NULL; +uint8_t *uart2_path = NULL; uint8_t keymap = 0; // KERNAL's default int window_scale = 1; float screen_x_scale = 1.0; @@ -136,6 +139,15 @@ char *cartridge_path = NULL; bool has_midi_card = false; uint16_t midi_card_addr; +//Serial UART Setup +bool has_uart1 = false; +uint16_t uart1_addr = 0x9fe0; +serialuartTL16C2550Handle uart1 = NULL; + +bool has_uart2 = false; +uint16_t uart2_addr = 0x9fe8; +serialuartTL16C2550Handle uart2 = NULL; + bool using_hostfs = true; uint8_t MHZ = 8; @@ -340,6 +352,41 @@ machine_reset() mouse_state_init(); reset6502(regs.is65c816); midi_serial_init(); + + //User requested Serial UART - need to check + if(has_uart1){ + if(uart1 != NULL){ + uart_destroy(uart1); + } + uart1 = uartCreate(); + if(uart1 != NULL){ + if (uart_init( uart1, (char*)uart1_path ) ){ + has_uart1=false; + } + else{ + printf("UART1 succesuflly mapped to address: $%04x\n", uart1_addr); + } + } else { + has_uart1=false; + } + } + if(has_uart2){ + if(uart2 != NULL){ + uart_destroy(uart2); + } + + uart2 = uartCreate(); + if(uart2 != NULL){ + if (uart_init( uart2, (char*)uart2_path ) ){ + has_uart2=false; + } + else{ + printf("UART2 succesuflly mapped to address: $%04x\n", uart2_addr); + } + } else { + has_uart2=false; + } + } } void @@ -546,6 +593,15 @@ usage() printf("\tConnect the system MIDI input devices to the input of the first UART\n"); printf("\tof the emulated MIDI card. The -midicard option is required for this\n"); printf("\toption to have any effect.\n"); + printf("-uart1 []\n\texample: [/dev/ttyUSB0] (Posix) COM1 (Windows)\n"); + printf("\tConnect the system COM port devices to the the first UART\n"); + printf("\tThis option is experimental.\n"); + printf("-uart2 []\n\texample: /dev/ttyACM0 (Posix) COM2 (Windows)\n"); + printf("\tConnect the system COM port devices to the the second UART\n"); + printf("\tThis option is experimental.\n"); + printf("-uartaddr [
]\n"); + printf("\tIf not specified, defaults to '$9FE0'\n\tUART2 is alwasy UART1 address + 8.\n"); + printf("\tThis option is experimental.\n"); #ifdef TRACE printf("-trace [
]\n"); printf("\tPrint instruction trace. Optionally, a trigger address\n"); @@ -1145,6 +1201,46 @@ main(int argc, char **argv) argc--; argv++; warn_rockwell = false; + } else if (!strcmp(argv[0], "-uart1")) { + argc--; + argv++; + if (!argc || argv[0][0] == '-') { + usage(); + } + uart1_path = (uint8_t *)argv[0]; + argc--; + argv++; + has_uart1=true; + + } else if (!strcmp(argv[0], "-uartaddr")) { + argc--; + argv++; + if (!argc || argv[0][0] == '-') { + usage(); + } + + if (argc && argv[0][0] != '-') { + //Force to memory mapped addresses only + uart1_addr = 0x9f00 | ((uint16_t)strtol(argv[0], NULL, 16) & 0xff); + argc--; + argv++; + } else { + uart1_addr = 0x9fe0; + } + + uart2_addr = uart1_addr+8; + + } else if (!strcmp(argv[0], "-uart2")) { + argc--; + argv++; + if (!argc || argv[0][0] == '-') { + usage(); + } + uart2_path = (uint8_t *)argv[0]; + argc--; + argv++; + has_uart2=true; + } else { usage(); } @@ -1198,6 +1294,20 @@ main(int argc, char **argv) fprintf(stderr, "Warning: -sf2 and -midicard must be specified together in order to enable the MIDI synth.\n"); has_midi_card = false; } + + if (has_uart2 || has_uart1) { + //Techincally cannot be less than 0x9f0 due to above code in argument parsing. + if (uart1_addr < 0x9f60 || uart1_addr>0x9ff8) { + fprintf(stderr, "Warning: Serial UART card address must be in the range of 9F60-9FF8, Unabled to connect\n"); + has_uart1 = has_uart2 = false; + } + + //Make sure to stay in memory mapped range. + if(has_uart2 && uart1_addr>0x9ff0){ + has_uart2=false; + fprintf(stderr, "Warning: Serial UART1 card address must be in the range of 9F60-9FF0 when using two UARTs, Unabled to connect\n"); + } + } if (cartridge_path) { if (!cartridge_load(cartridge_path, !zeroram)) { @@ -1768,7 +1878,7 @@ emulator_loop(void *param) audio_render(); } - if (video_get_irq_out() || via1_irq() || (has_via2 && via2_irq()) || (ym2151_irq_support && YM_irq()) || (has_midi_card && midi_serial_irq())) { + if (video_get_irq_out() || via1_irq() || (has_via2 && via2_irq()) || (ym2151_irq_support && YM_irq()) || (has_midi_card && midi_serial_irq()) || (has_uart1 && uart_irqPending(uart1)) || (has_uart2 && uart_irqPending(uart2)) ) { // printf("IRQ!\n"); irq6502(); } diff --git a/src/memory.c b/src/memory.c index 14f51900..28e79d49 100644 --- a/src/memory.c +++ b/src/memory.c @@ -20,6 +20,14 @@ #include "iso_8859_15.h" #include "midi.h" +#include "extern/serialuart/serialuart_wrapper.h" +extern bool has_uart1; +extern bool has_uart2; +extern uint16_t uart1_addr; +extern uint16_t uart2_addr; +extern serialuartTL16C2550Handle uart1; +extern serialuartTL16C2550Handle uart2; + uint8_t ram_bank; uint8_t rom_bank; @@ -242,7 +250,20 @@ real_read6502(uint16_t address, uint8_t bank, bool debugOn, int16_t x16Bank) } else if (has_midi_card && (address & 0xfff0) == midi_card_addr) { // midi card return midi_serial_read(address & 0xf, debugOn); - } else { + } + //Serial UART1 (serial wifi) + else if ( has_uart1 && (address >= uart1_addr && address <= (uart1_addr+7) ) ){ + uint8_t retVal=0; + uart_addrread(uart1, &retVal, address & 0x07 ); + return retVal; + }// END of Serial UART1*/ + //Serial UART2 (serial wifi) + else if ( has_uart2 && (address >= uart2_addr && address <= (uart2_addr+7)) ){ + uint8_t retVal=0; + uart_addrread(uart2, &retVal, address & 0x07 ); + return retVal; + }// END of Serial UART2*/ + else { // future expansion return 0x9f; // open bus read } @@ -332,7 +353,15 @@ write6502(uint16_t address, uint8_t bank, uint8_t value) emu_write(address & 0xf, value); } else if (has_midi_card && (address & 0xfff0) == midi_card_addr) { midi_serial_write(address & 0xf, value); - } else { + } + else if (has_uart1 && (address >= uart1_addr && address <= (uart1_addr+7) ) ) { + uart_addrwrite(uart1, &value, address & 0x07 ); + } + else if (has_uart2 && (address >= uart2_addr && address <= (uart2_addr+7) ) ) { + uart_addrwrite(uart2, &value, address & 0x07 ); + } + //END of UART2 + else { // future expansion } } else if (address < 0xc000) { // banked RAM