No UART hardware flow control - how to do RS485 with the Omega?



  • @huntc @crispyoz The MT7688 silicon contains a "UART lite" peripheral, which internally does have hardware handshake. The datasheet describes all the related bits (see UART modem control and interrupt registers), and the linux kernel driver most likely serves those bits correctly when using ioctl().

    Only, on the same MT7688 silicon, Mediatek did not bother to wire those handshake signals to any pin multiplexer at all. šŸ˜¢ šŸ™„

    So it's not that the Omega does not expose the needed pins, it's that these signals are buried deep in the chip itself, with no possibility to ever connect them with the outside world. This is the same sort of oversight like forgetting to connect the PWM unit to DMA - the MT7688 is a router chip that was "IoTzed" apparently in a hurry. For a pure router, PWM or serial handshake is not important.

    Still, Omega2's UARTs can be used for RS485. Instead of using ioctl() to switch RTS, just use a regular GPIO. What RS485 actually needs is not handshake in the true sense anyway (there are no handshake signals in RS485), but just a way to enable/disable the output driver.

    I've done a few RS485 projects with the Omega2 this way.

    One of them needed modbus (which is a RS485 based protocol) and I found that libmodbus even has API for this case: modbus_rtu_set_custom_rts(), which allows to pass a pointer to a function which is called when the output drivers should be turned on or off. So I could just set/reset a GPIO in this function, and everything worked.



  • So, I'm not having much luck. I'm writing things in Rust, so I'm probably making these questions a little harder to answer. However, if I attempt to use ioctl on the file descriptor returned from having opened /dev/ttyS0, I receive "Not a tty (os error 25)". Any thoughts?

    I'm specifically enabling SER_RS485_ENABLED, which to be honest, I'm unsure is needed anyhow, as I'm not setting anything else. Here's the complete Rust code around this for fun:

    const SER_RS485_ENABLED: u32 = 1 << 0;
    
    #[repr(C)]
    #[derive(Default)]
    pub struct SerialRs485 {
        flags: u32,
        delay_rts_before_send: u32,
        delay_rts_after_send: u32,
        _padding: [u32; 5],
    }
    
    nix::ioctl_write_ptr!(config_rs485, b'T', 0x2F, SerialRs485); // TIOCSRS485
    
    async fn open_rs485(uart_path: impl AsRef<Path>) -> io::Result<File> {
        let fd = OpenOptions::new()
            .read(true)
            .write(true)
            .open(uart_path)
            .await?;
    
        let mut config = SerialRs485::default();
        config.flags |= SER_RS485_ENABLED;
    
        unsafe {
            config_rs485(fd.as_raw_fd(), &config)?;
        }
    
        Ok(fd)
    }
    


  • @crispyoz Sorry for not being entirely clear here. We're using an RS485 Transceiver, but we were preferring to hook up the UART's RTS to the "enable tx" pin (output driver).



  • @luz Thanks for the comprehensive reply. That's crazy re hardware flow control not being exposed outside the MT7688 itself...

    We're indeed using a regular GPIO on our microcontroller and the nRF kit being used has a nice little event/task system whereby I can get the hardware to clear the enable tx GPIO once the bits have been transmitted. I was looking to see if there was something equivalent for the Onion/MT7688 given that Linux itself does support this, but as per your comments, not possible. What I'm now doing is calling tcdrain on the file descriptor to block and wait for the bytes to be written having set the GPIO. On returning from that, I clear the GPIO. I suspect that modbus_rtu_set_custom_rts must do something similar, so I'll investigate.I shall report back.



  • @luz - seems as though modbus-rtu relies on a sleep before clearing the GPIO... I guess you can reliably work out the timing there and they default to the time it takes to send one byte, but it does "feel" a bit of a hack. Hopefully,tcdrain does the job.



  • @huntc said:

    So, I'm not having much luck. I'm writing things in Rust, so I'm probably making these questions a little harder to answer.

    Yes šŸ˜‰ But this opens a very interesting sideline: what rust toolset are you using for openwrt? rust-lang isn't available in openwrt itself at this time, is it?

    However, if I attempt to use ioctl on the file descriptor returned from having opened /dev/ttyS0, I receive "Not a tty (os error 25)". Any thoughts?

    This seems strange, because /dev/ttyS0 is definitely a tty. I could imagine specific ioctl() flags returning EINVAL for features unsupported in the UART lite, but not ENOTTY.

    I'm specifically enabling SER_RS485_ENABLED, which to be honest, I'm unsure is needed anyhow, as I'm not setting anything else.

    OpenWrt uses the really super standard ns16550a driver for the "UART lite", so I doubt the driver even knows about buried RTS/CTS on the MT7688 SoC. So I'd expect TIOCSRS485 to work software-wise (with no effect on the real world, of course). I might miss some detail, but getting ENOTTY feels like a different, unrelated problem.

    [Update: "ns16550a" is the compatibility string in the device tree - the actual driver is called '8250' which has dozens of variants for specific hardware in drivers/tty/serial/8250 - but ns16550a in the omega2 device tree selects the standard variant]

    Here's the complete Rust code around this for fun:

    Fun indeed - I read the book and some blogs describing elaborate fights with the borrow checker. And still, this snippet has the inevitable unsafe in it. I see the benefits, but haven't managed to wrap my head around all this yet, steaming along in C++ šŸ˜‰

    We're indeed using a regular GPIO on our microcontroller and the nRF kit being used has a nice little event/task system whereby I can get the hardware to clear the enable tx GPIO once the bits have been transmitted. I was looking to see if there was something equivalent for the Onion/MT7688 given that Linux itself does support this, but as per your comments, not possible.

    I was considering to patch GPIO support into ns16550a before I found libmodbus had a workaround for that already built-in. As you say, the tricky thing is to withdraw the driver enable signal not before the tx buffer is actually drained.

    seems as though modbus-rtu relies on a sleep before clearing the GPIO... I guess you can reliably work out the timing there and they default to the time it takes to send one byte,

    yes, it just calculates the expected tx time from the number of bytes sent and the baud rate.

    but it does "feel" a bit of a hack.

    It does to me, too. But it works šŸ˜‰

    Hopefully, tcdrain does the job.

    Let us know when you find out, please šŸ™‚

    If it does work, then the ns16550a driver must already have a non-blocking way to determine the point of time when the tx fifo gets empty, and if it has, patching some extra GPIO code right there should be possible...



  • I couln't resist digging into the kernel to see how it actually works...

    Turns out there is no really clean way to get that point in time when tx is drained with a 8250/16550: There's usleep and looping in the 8250_core's wait_for_xmitr().

    What's more, looking into how the RS485 mode is implemented, they don't even rely on wait_for_xmitr() alone but use an additional delay_rts_after_send timer.

    The entire thing is insanely complex, I'm far from really understanding it, but one interesting tidbit is that some serial drivers already have "rts-gpios" etc. properties in the device tree to use regular GPIOs for HW handshake lines. Apparently there's the intention for this to become generic, as it is documented in top level serial.yaml.

    But only the Freescale IMX driver, and a non-mainline sirfsoc_uart (contained in OpenWrt) actually implement it.

    The 8250 drivers do not. However, there is one GPIO based feature for RS485 implemented for all serial drivers: "rs485-term" for bus termination, implemented along with the other generic rs485 features in serial_core.c.



  • @luz Thanks again.

    Yes šŸ˜‰ But this opens a very interesting sideline: what rust toolset are you using for openwrt? rust-lang isn't available in openwrt itself at this time, is it?

    I'm using the mipsel-unknown-linux-musl target, which seems fine. I also built the OpenWrt SDK on macOS, which also seems to work.

    Fun indeed - I read the book and some blogs describing elaborate fights with the borrow checker.

    TBH I've never found the borrow checker to be an issue. Understanding the notion of lifetimes takes a bit longer though.

    And still, this snippet has the inevitable unsafe in it. I see the benefits, but haven't managed to wrap my head around all this yet, steaming along in C++ šŸ˜‰

    unsafe rarely creeps up - mostly when calling out to C. šŸ™‚

    Turns out there is no really clean way to get that point in time when tx is drained with a 8250/16550: There's usleep and looping in the 8250_core's wait_for_xmitr().

    Looks as though it'll be usleep for me then too.



  • An update here - I'm able to send data out on ttyS0 and my microcontroller receives it ok and then replies... however, the replies are a bit hit-and-miss when receiving on the Omega2S+. I'm wondering now how to go about diagnosing the issue further.

    My symptoms are scarily similar to a prior post.



  • Okay... all appears well with /dev/ttyS1 - I was using /dev/ttyS0 before... what could be the difference there? That other post mentioned extra capacitance on ttyS0, but I also tried with lower baud rates etc. Pretty weirdo, but at least things appear to be working for me now.



  • @huntc I'm interested to know what baud rate you are using on ttyS0 when you experience this issue. Have you examined the coms on a scope? I'm interested to know if this is a hardware or software issue.



  • @crispyoz Sorry for the slow reply. I tried both 9600 and 115200 baud with the same results. I'm assuming that we have the same voltages for both S0 and S1 (this is the Omega dev board). I don't have a scope, but my colleague and I will get together next week and scope it with his. It is strange that others haven't had the same experience.



  • Questions from my colleague on the h/w side: why do we need these pull-up resistors on the dev board? (contrary to the hardware guide, which requires floating or pull-down for S0 to avoid affecting boot):

    c35020cd-bb00-4f4f-9aa7-929f518b2543-image.png

    BTW the TX and RX labels are on the wrong lines too.



  • We finally managed to perform our RS485 testing yesterday, which involved an Omega2S+. It turns out that waiting on flush blocks for around 30ms. This was way too long for us, so we ended up calculating our time-on-wire and rounding up to the nearest ms. Given 115200 baud, start/stop bits, and a 41-byte fixed payload size, waiting 4ms worked out about right (it took about 3.7ms for the send to complete when observed by the scope). Given also that it takes about 1.5ms for our microcontroller to process the data and reply, things are working out quite well. Our Omega does RS485, although it'd be so much nicer if we didn't have to fudge the timing.

    Also, ttyS0 is used for the console, hence being conflicted when trying to use it for our purposes.

    All good.



Looks like your connection to Community was lost, please wait while we try to reconnect.