diff --git a/CSpectPlugins.sln b/CSpectPlugins.sln index 60b1e0c..a871ad2 100644 --- a/CSpectPlugins.sln +++ b/CSpectPlugins.sln @@ -11,7 +11,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RTC", "RTC\RTC.csproj", "{0 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "I2CTestHarness", "I2CTestHarness\I2CTestHarness.csproj", "{3708E183-3E15-4D20-8726-46C452B951C7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UARTForwarder", "UARTForwarder\UARTForwarder.csproj", "{B620589D-3613-40FB-ACFB-9C825D474D7F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UARTReplacement", "UARTReplacement\UARTReplacement.csproj", "{B620589D-3613-40FB-ACFB-9C825D474D7F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/UARTForwarder/SerialPort.cs b/UARTForwarder/SerialPort.cs deleted file mode 100644 index 761476f..0000000 --- a/UARTForwarder/SerialPort.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Plugins.UARTForwarder -{ - public class SerialPort : IDisposable - { - private System.IO.Ports.SerialPort port; - - public SerialPort(string PortName, int BaudRate) - { - try - { - port = new System.IO.Ports.SerialPort(); - port.PortName = PortName; - port.BaudRate = BaudRate; - port.Parity = System.IO.Ports.Parity.None; - port.DataBits = 8; - port.StopBits = System.IO.Ports.StopBits.One; - port.Handshake = System.IO.Ports.Handshake.None; - //port.ReadTimeout = 1; - //port.WriteTimeout = 500; - port.Open(); - } - catch - { - port = null; - } - } - - public void Write(byte[] buffer, int offset, int count) - { - //try - //{ - if (port != null) - port.Write(buffer, offset, count); - //} - //catch (TimeoutException) - //{ - //} - } - - public byte ReadByte(out bool Success) - { - try - { - if (port != null) - { - var b = Convert.ToByte(port.ReadByte()); - Success = true; - return b; - } - } - catch - { - } - Success = false; - return 0xff; - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // Dispose managed state (managed objects). - if (port != null) - { - if (port.IsOpen) - port.Close(); - port.Dispose(); - port = null; - } - } - - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. - - disposedValue = true; - } - } - - // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. - // ~SerialPort() { - // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - // Dispose(false); - // } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - // TODO: uncomment the following line if the finalizer is overridden above. - // GC.SuppressFinalize(this); - } - #endregion - } -} diff --git a/UARTForwarder/Properties/AssemblyInfo.cs b/UARTReplacement/Properties/AssemblyInfo.cs similarity index 88% rename from UARTForwarder/Properties/AssemblyInfo.cs rename to UARTReplacement/Properties/AssemblyInfo.cs index 0dbc6cf..4fcee3b 100644 --- a/UARTForwarder/Properties/AssemblyInfo.cs +++ b/UARTReplacement/Properties/AssemblyInfo.cs @@ -5,12 +5,12 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("UARTForwarder")] +[assembly: AssemblyTitle("UARTReplacement")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("UARTForwarder")] -[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyProduct("UARTReplacement")] +[assembly: AssemblyCopyright("Copyright © 2020 Robin Verhagen-Guest")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/UARTReplacement/SerialPort.cs b/UARTReplacement/SerialPort.cs new file mode 100644 index 0000000..55ee84d --- /dev/null +++ b/UARTReplacement/SerialPort.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Plugins.UARTReplacement +{ + public class SerialPort : IDisposable + { + private System.IO.Ports.SerialPort port; + private int clock = 27000000; // CSpect defaults to HDMI timings + private int prescaler = 234; // Next baud defaults to 115200 (more accurately 115384 with integer division) + + public SerialPort(string PortName, int BaudRate) + { + try + { + port = new System.IO.Ports.SerialPort(); + port.PortName = PortName; + int baud = Baud; + port.BaudRate = baud > 0 ? baud : 115200; + port.Parity = System.IO.Ports.Parity.None; + port.DataBits = 8; + port.StopBits = System.IO.Ports.StopBits.One; + port.Handshake = System.IO.Ports.Handshake.None; + LogClock(false); + LogPrescaler(""); + port.Open(); + } + catch (Exception /*ex*/) + { + port = null; + } + } + + public void Write(byte[] buffer, int offset, int count) + { + if (port != null) + port.Write(buffer, offset, count); + } + + /// + /// If there is a byte available in the UART buffer return it, otherwise a value representing no data. + /// + /// Output parameter indicating whether the result was a valid byte or no data. + /// A byte from the UART buffer, or a value representing no data. + public byte ReadByte(out bool Success) + { + try + { + if (port != null) + { + var b = Convert.ToByte(port.ReadByte()); + Success = true; + return b; + } + } + catch + { + } + Success = false; + return 0xff; + } + + /// + /// Given a raw byte read from REG_VIDEO_TIMING, and another raw byte written to PORT_UART_CONTROL, + /// updates the clock and potentially also bits 17:13 of the prescaler. + /// + /// Read from REG_VIDEO_TIMING, used to update the clock. + /// Written to PORT_UART_CONTROL, used to update bits 17:13 of the prescaler. + /// + public int SetPrescalerAndClock(byte BaudByte, byte VideoTimingByte) + { + if (port == null) + return 0; + int mode = VideoTimingByte & 7; + switch (mode) + { + case 0: + clock = 28000000; + break; + case 1: + clock = 28571429; + break; + case 2: + clock = 29464286; + break; + case 3: + clock = 30000000; + break; + case 4: + clock = 31000000; + break; + case 5: + clock = 32000000; + break; + case 6: + clock = 33000000; + break; + case 7: + clock = 27000000; + break; + default: + clock = 28000000; + break; + } + if ((BaudByte & 0x10) == 0x10) + { + // Get bits 14..17 of the new prescaler + int newBits = (BaudByte & 0x07) << 14; + // Mask out everything of the existing prescaler except bits 14..17 + int oldBits = prescaler & 0x3fff; + // Combine the two sets of bits + prescaler = oldBits | newBits; + port.BaudRate = Baud; + LogClock(false); + LogPrescaler("17:14"); + } + else + { + port.BaudRate = Baud; + LogClock(); + } + return clock; + } + + /// + /// Given a raw byte written to PORT_UART_RX, parses the high bit to decide whether it represents a change + /// to bits 6:0 or 13:7, and updates the prescaler accordingly. + /// + /// The raw byte written to I/O port PORT_UART_RX. + /// Returns the newly recalculated baud. + public int SetPrescaler(byte BaudByte) + { + if (port == null) + return 0; + if ((BaudByte & 0x80) == 0) + { + // Get bits 0..6 of the new prescaler + int newBits = BaudByte & 0x7f; + // Mask out everything of the existing prescaler except bits 0..6 + int oldBits = prescaler & 0x1ff80; + // Combine the two sets of bits + prescaler = oldBits | newBits; + port.BaudRate = Baud; + LogPrescaler("6:0"); + } + else + { + // Get bits 7..13 of the new prescaler + int newBits = (BaudByte & 0x7f) << 7; + // Mask out everything of the existing prescaler except bits 7..13 + int oldBits = prescaler & 0x1c07f; + // Combine the two sets of bits + prescaler = oldBits | newBits; + port.BaudRate = Baud; + LogPrescaler("13:7"); + } + return Baud; + } + + /// + /// Baud is always calculated dynamically from the clock and prescaler, using integer division. + /// + public int Baud + { + get + { + // + if (prescaler == 0) + return 0; + return Convert.ToInt32(Math.Truncate((Convert.ToDecimal(clock) / prescaler))); + } + } + + /// + /// Convenience method to log the clock and calculated baud to the debug console, every time the video timing clock changes. + /// + /// + /// Optionally choose not to log the calculated baud, if you know the prescaler is about to be changed + /// and logged straight afterwareds. + /// + private void LogClock(bool LogBaud = true) + { + if (port == null) + return; + Debug.WriteLine("Clock: " + clock); + if (LogBaud) + Debug.WriteLine("Baud: " + Baud); + } + + /// + /// Convenience method to log the prescaler and calculated baud to the debug console, every time the prescaler changes. + /// + /// + private void LogPrescaler(string BitsChanged) + { + if (port == null) + return; + string bstr = Convert.ToString(prescaler, 2).PadLeft(17, '0'); + Debug.WriteLine("Prescaler: " + bstr.Substring(0, 3) + " " + bstr.Substring(3, 7) + " " + bstr.Substring(10, 7) + + " (" + prescaler + (string.IsNullOrWhiteSpace(BitsChanged) ? "" : ", changed bits " + BitsChanged) + ")"); + Debug.WriteLine("Baud: " + Baud); + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // Dispose managed state (managed objects). + if (port != null) + { + if (port.IsOpen) + port.Close(); + port.Dispose(); + port = null; + } + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~SerialPort() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/UARTForwarder/UARTForwarder.csproj b/UARTReplacement/UARTReplacement.csproj similarity index 94% rename from UARTForwarder/UARTForwarder.csproj rename to UARTReplacement/UARTReplacement.csproj index 216a525..c7d8264 100644 --- a/UARTForwarder/UARTForwarder.csproj +++ b/UARTReplacement/UARTReplacement.csproj @@ -7,8 +7,8 @@ {B620589D-3613-40FB-ACFB-9C825D474D7F} Library Properties - Plugins.UARTForwarder - UARTForwarder + Plugins.UARTReplacement + UARTReplacement v4.5.2 512 @@ -48,7 +48,7 @@ - + diff --git a/UARTForwarder/UARTForwarder_Device.cs b/UARTReplacement/UARTReplacement_Device.cs similarity index 62% rename from UARTForwarder/UARTForwarder_Device.cs rename to UARTReplacement/UARTReplacement_Device.cs index 30750cc..4739528 100644 --- a/UARTForwarder/UARTForwarder_Device.cs +++ b/UARTReplacement/UARTReplacement_Device.cs @@ -6,13 +6,15 @@ using System.Threading.Tasks; using Plugin; -namespace Plugins.UARTForwarder +namespace Plugins.UARTReplacement { - public class UARTLogger_Device : iPlugin + public class UARTReplacement_Device : iPlugin { private const ushort PORT_UART_TX = 0x133b; private const ushort PORT_UART_RX = 0x143b; private const ushort PORT_UART_CONTROL = 0x153b; + private const byte REG_VIDEO_TIMING = 0x11; + private iCSpect CSpect; private UARTTargets Target; @@ -41,10 +43,11 @@ public List Init(iCSpect _CSpect) // create a list of the ports we're interested in, but only if we're logging List ports = new List(); - ports.Add(new sIO(PORT_UART_TX, eAccess.Port_Write)); - ports.Add(new sIO(PORT_UART_TX, eAccess.Port_Read)); - ports.Add(new sIO(PORT_UART_RX, eAccess.Port_Read)); - ports.Add(new sIO(PORT_UART_CONTROL, eAccess.Port_Write)); + ports.Add(new sIO(PORT_UART_TX, eAccess.Port_Write)); + ports.Add(new sIO(PORT_UART_TX, eAccess.Port_Read)); + ports.Add(new sIO(PORT_UART_RX, eAccess.Port_Write)); + ports.Add(new sIO(PORT_UART_RX, eAccess.Port_Read)); + ports.Add(new sIO(PORT_UART_CONTROL, eAccess.Port_Write)); return ports; } @@ -63,15 +66,31 @@ public bool Write(eAccess _type, int _port, byte _value) switch (_port) { case PORT_UART_CONTROL: + // Writes to this port contain siwtches between the ESP and Pi UARTs (we only handle the former), + // and also potential changes to the prescaler which cause the UART baud to change. + // We might not need to query REG_VIDEO_TIMING every single time the baud or UART selector changes, + // But just in case it can vary, let's do it. It doesn't seem to cause performance issues to do so. var target = (_value & 64) == 0 ? UARTTargets.ESP : UARTTargets.Pi; Target = target; - //Debug.WriteLine("Switched UART to " + target.ToString()); + if (Target == UARTTargets.ESP) + espPort.SetPrescalerAndClock(_value, CSpect.GetNextRegister(REG_VIDEO_TIMING)); // We are transparently logging without handling the write, so return false return false; case PORT_UART_TX: if (Target == UARTTargets.ESP) { + // This is an outgoing UART byte which should be sent to our buffered UART implementation espPort.Write(new byte[] { _value }, 0, 1); + // If we are actively handling the write, return true. + return true; + } + // If we are not handling the write, return false so CSpect's own Pi UART will handle it. + return false; + case PORT_UART_RX: + if (Target == UARTTargets.ESP) + { + // Writes to this port represent changes to the prescaler which cause the UART baud to change + espPort.SetPrescaler(_value); return true; } return false; @@ -97,6 +116,8 @@ public byte Read(eAccess _type, int _port, out bool _isvalid) // then read the actual value, then handle the callback with the read value. if (Target == UARTTargets.ESP) { + // This is an incoming UART byte which should be read from our buffered UART implementation + // Z80 code will only call this if the status flag indicates data is ready when reading PORT_UART_TX. _isvalid = true; if (readBuffer.Count > 0) { @@ -108,10 +129,12 @@ public byte Read(eAccess _type, int _port, out bool _isvalid) else return 0xff; } + // If not already handled, any remaining reads from this port represent data reads + // from the Pi UART. So pass this through to let CSpect handle it (which initiates + // another recursive call to this method, which we handle above). UART_RX_Internal = true; byte val = CSpect.InPort(PORT_UART_RX); UART_RX_Internal = false; - //Debug.WriteLine("RX: " + val.ToString("X2")); _isvalid = true; return val; case PORT_UART_TX: @@ -124,16 +147,20 @@ public byte Read(eAccess _type, int _port, out bool _isvalid) } if (Target == UARTTargets.ESP) { + // Reads from this port return a status flag in bit 0 indicating whether + // there is any data available to read from the UART buffer. _isvalid = true; if (readBuffer.Count > 0) return 1; else return 0; } + // If not already handled, any remaining reads from this port represent data ready status flag + // reads from the Pi UART. So pass this through to let CSpect handle it (which initiates + // another recursive call to this method, which we handle above). UART_RX_Internal = true; byte val2 = CSpect.InPort(PORT_UART_TX); UART_RX_Internal = false; - //Debug.WriteLine("RX: " + val.ToString("X2")); _isvalid = true; return val2; } @@ -142,6 +169,11 @@ public byte Read(eAccess _type, int _port, out bool _isvalid) return 0xff; } + + // This method is runs on a background thread to continuously read all available bytes + // proactively from the plugin's serial port, and fill the UART read buffer with them. + // Access to the buffer from both threads is synchronised with a mutex via the lock() construct. + // This method It may not offer the best performance, but so far I have not needed to optimise it. public void ESPWork() { while(true) diff --git a/UARTForwarder/UARTTargets.cs b/UARTReplacement/UARTTargets.cs similarity index 84% rename from UARTForwarder/UARTTargets.cs rename to UARTReplacement/UARTTargets.cs index 9f27ddd..e51a0c2 100644 --- a/UARTForwarder/UARTTargets.cs +++ b/UARTReplacement/UARTTargets.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Plugins.UARTForwarder +namespace Plugins.UARTReplacement { public enum UARTTargets {