dotnet/runtime

SerialPort on Linux fails to open for 250k baud

Open

#64,507 opened on Jan 30, 2022

View on GitHub
 (6 comments) (1 reaction) (0 assignees)C# (5,445 forks)batch import
area-System.IO.Portshelp wanted

Repository metrics

Stars
 (17,886 stars)
PR merge metrics
 (Avg merge 12d 11h) (661 merged PRs in 30d)

Description

Description

Trying to open a System.IO.Ports.SerialPort for 250000 baud under Linux gives an argument exception. Even if it didn't the underlying code would still fail to properly open that rate.

This is a common serial rate for many 3D printers, such as all Lulzbot printers.

Bot said to add an Area, but didn't say where. So here it is: @adamsitnik

Reproduction Steps

Running on any version of .NET (fails in 5 and 6) on a Raspberry Pi with the latest 32-bit Rasberry Pi OS, attempt to open a serial port:

var port port = new SerialPort("/dev/ttyACM0", 250000, Parity.None, 8, StopBits.One);

Expected behavior

I expect it to open and be usable.

Actual behavior

An exception is thrown.

Regression?

No response

Known Workarounds

I have a known-good, working workaround. It's ugly, but works. First, open the port with a supported rate, like 115200. then run this code:

[DllImport("libc", SetLastError = true)]
private static extern int ioctl(IntPtr fd, uint request, int[] data);

private void SetNonStandardBaudRateIfRequired()
{
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
        && _baudRate > 115200)
    {
        // due to limitations/behavior of Linux driver, we need to work around setting the baud rate above 115200
        var stream = m_port.GetType()
            .GetField("_internalSerialStream", BindingFlags.NonPublic | BindingFlags.Instance)
            .GetValue(m_port);
        var handle = (SafeHandle)stream.GetType()
            .GetField("_handle", BindingFlags.NonPublic | BindingFlags.Instance)
            .GetValue(stream);

        uint TCGETS2 = 0x802C542A;
        uint TCSETS2 = 0x402C542B;
        var BOTHER = 0x1000; // 0o010000;
        var CBAUD = 0x100F; // 0o010017;

        var buffer = new int[64];

        // get the current value
        var fd = handle.DangerousGetHandle();
        ioctl(fd, TCGETS2, buffer);
        buffer[2] &= ~CBAUD; // turn off the standard baud flag
        buffer[2] |= BOTHER; // turn on the "other baud rate" flag
        buffer[9] = buffer[10] = _baudRate; // set the same rate in both directions
        Console.WriteLine($"BAUD RATE WORKAROUND");
        // write the new value
        var result = ioctl(fd, TCSETS2, buffer);
        if (result != 0)
        {
            // failure
            Console.WriteLine($"IOCTL FAILED: {Marshal.GetLastWin32Error()}");
            return;
        }
        // verify
        ioctl(fd, TCGETS2, buffer);
        if (buffer[9] != _baudRate || buffer[10] != _baudRate)
        {
            // failure
            Console.WriteLine($"IOCTL FAILED: read != write");
            return;
        }
    }
}

I told you it was ugly.

NOTE that this code is unsetting the CBAUD flag and then setting the BOTHER flag, which the BCL does not do. Simply allowing 250000 baud through is not enough, the logic above must be applied.

NOTE 2 the above is not the right way to do it in the BCL. The proper fix should be done in C in the PAL, somewhere in this method:

https://github.com/dotnet/runtime/blob/c12bea880a2f1290d16adf97ec1000aa63631da2/src/native/libs/System.IO.Ports.Native/pal_termios.c#L350

I would have done a fork and PR, but honestly I don't have the native toolchain set up, and that feels like a huge headache. Anyone with a little C knowledge can port the above into that file.

Configuration

.NET 5.0 Raspberry Pi 4 ARM

pi@raspberrypi:~/marlin $ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 11 (bullseye)"
NAME="Raspbian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye

This is not specific to this configuration. I'm willing to bet it will fail on any Linux kernel.

Other information

Fix and where it belongs are detailed above

Contributor guide