Seeed-Studio / seeed-linux-dtoverlays

Device Tree Overlays for Seeed boards
Other
82 stars 54 forks source link

R1000 RS485 DE/nRE pin change is too slow #108

Closed fjuliofontes closed 1 month ago

fjuliofontes commented 1 month ago

I've been using the reComputer R1000 for a while and I do see some use cases for it, but unfortunately my application requires modbus RUT over RS485 and I can't make it work with software DE/nRE pin driving. The tcdrain() function takes a lot of time and when the driver switches to receiving mode the response was gone already.

I think it is a bit shame having the designs not using the proper RTS pins for the UARTS of the CM4 module. The chips are there but they are simple not usable.

I'm writing this to make the developers aware of the issue and with the hope for a version 2 re-design that would make possible to enable rtscts directly and having the DE/nRE pin being directly driven on the hardware layer.

is-qian commented 1 month ago

In this commit, RS485_DE configures the serial port to RS485 mode and removes tcdrain().

fjuliofontes commented 1 month ago

In this commit, RS485_DE configures the serial port to RS485 mode and removes tcdrain().

That is true, however the DE/nDE control is still done by software. What essentially does the configuration to RS485? Because from what I understand the configuration is usually used to enable the RTS control and since the RTS pin is not the same as the one actually connected to the DE/nRE pin I'm not sure what will this improve.

is-qian commented 1 month ago

At first, it was because the IO was insufficient, which led to the RTS being occupied. Now we are considering whether to correct it by modifying the hardware.

is-qian commented 1 month ago

In this commit, RS485_DE configures the serial port to RS485 mode and removes tcdrain().

That is true, however the DE/nDE control is still done by software. What essentially does the configuration to RS485? Because from what I understand the configuration is usually used to enable the RTS control and since the RTS pin is not the same as the one actually connected to the DE/nRE pin I'm not sure what will this improve.

After configuring the serial port to RS485 mode, the serial port will send data in blocking mode instead of DMA, so tcdrain() is no longer needed.

fjuliofontes commented 1 month ago

ing the hardware

that would be perfect...

fjuliofontes commented 1 month ago

In this commit, RS485_DE configures the serial port to RS485 mode and removes tcdrain().

That is true, however the DE/nDE control is still done by software. What essentially does the configuration to RS485? Because from what I understand the configuration is usually used to enable the RTS control and since the RTS pin is not the same as the one actually connected to the DE/nRE pin I'm not sure what will this improve.

After configuring the serial port to RS485 mode, the serial port will send data in blocking mode instead of DMA, so tcdrain() is no longer needed.

Not sure if that works. From the tests that I did it never worked. Could it be because of OS versions? I'm running on Balena OS and the only way that I got it to work (with 10% error rate) was by doing nanosleep() after the write() of the number of bytes transmitted multiplied by the byte time. I'm running with 19200 baud.

This is how I'm configuring the UART port. I'm running everything within C code to try to reduce to the minimum the latency.

    struct termios tty;

    // setup serial port now
    int serial_port = open(tty_name, O_RDWR | O_NOCTTY | O_NDELAY);
    if (serial_port < 0)
    {
        printf("Error while configuring serial port: %s\n", tty_name);
        fflush(stdout);
        return 0;
    }

    // get serial port configuration
    if (tcgetattr(serial_port, &tty) != 0)
    {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
        fflush(stdout);
        return 0;
    }

    // update serial port settings
    tty.c_cflag &= ~PARENB;        // Clear parity bit, disabling parity
    tty.c_cflag |= PARENB;         // Enable parity
    tty.c_cflag &= ~CSTOPB;        // Clear stop field, only one stop bit used in communication
    tty.c_cflag &= ~CSIZE;         // Clear all the size bits
    tty.c_cflag |= CS8;            // 8 bits per byte
    tty.c_cflag &= ~CRTSCTS;       // Disable RTS/CTS hardware flow control
    tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)
    // In canonical mode, input is processed when a new line character is received.
    tty.c_lflag &= ~ICANON;                                                      // Canonical mode is disabled
    tty.c_lflag &= ~ECHO;                                                        // Disable echo
    tty.c_lflag &= ~ECHOE;                                                       // Disable erasure
    tty.c_lflag &= ~ECHONL;                                                      // Disable new-line echo
    tty.c_lflag &= ~ISIG;                                                        // Disable interpretation of INTR, QUIT and SUSP
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);                                      // Turn off s/w flow ctrl
    tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); // Disable any special handling of received bytes
    tty.c_oflag &= ~OPOST;                                                       // Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR;                                                       // Prevent conversion of newline to carriage return/line feed
    // tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT IN LINUX)
    // tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT IN LINUX)
    tty.c_cc[VTIME] = 0; // wait up to 100ms
    tty.c_cc[VMIN] = 0;

    // set rs485 mode
    struct serial_rs485 rs485conf = {0};
    rs485conf.flags |= SER_RS485_ENABLED;
    if (ioctl (serial_port, TIOCSRS485, &rs485conf) < 0) {
        perror("ioctl");
        return 1;
    }

    // Set in/out baud rate to be LINE_BAUDRATE bps
    cfsetispeed(&tty, LINE_BAUDRATE);
    cfsetospeed(&tty, LINE_BAUDRATE);

    // Save tty settings, also checking for error
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0)
    {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
        fflush(stdout);
        return 0;
    }

With the code block:

        // set rs485 mode
    struct serial_rs485 rs485conf = {0};
    rs485conf.flags |= SER_RS485_ENABLED;
    if (ioctl (serial_port, TIOCSRS485, &rs485conf) < 0) {
        perror("ioctl");
        return 1;
    }

Or without it the results are the same...

is-qian commented 1 month ago

We have also conducted tests on RS485 with Balena OS, and we currently have two ways to solve this issue. 1. Add a kernel patch. You can refer to this repository. 2. Continue using the rs485_de tool. You can refer to this reference.

is-qian commented 1 month ago

In this commit, RS485_DE configures the serial port to RS485 mode and removes tcdrain().

That is true, however the DE/nDE control is still done by software. What essentially does the configuration to RS485? Because from what I understand the configuration is usually used to enable the RTS control and since the RTS pin is not the same as the one actually connected to the DE/nRE pin I'm not sure what will this improve.

After configuring the serial port to RS485 mode, the serial port will send data in blocking mode instead of DMA, so tcdrain() is no longer needed.

Not sure if that works. From the tests that I did it never worked. Could it be because of OS versions? I'm running on Balena OS and the only way that I got it to work (with 10% error rate) was by doing nanosleep() after the write() of the number of bytes transmitted multiplied by the byte time. I'm running with 19200 baud.

This is how I'm configuring the UART port. I'm running everything within C code to try to reduce to the minimum the latency.

    struct termios tty;

    // setup serial port now
    int serial_port = open(tty_name, O_RDWR | O_NOCTTY | O_NDELAY);
    if (serial_port < 0)
    {
        printf("Error while configuring serial port: %s\n", tty_name);
        fflush(stdout);
        return 0;
    }

    // get serial port configuration
    if (tcgetattr(serial_port, &tty) != 0)
    {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
        fflush(stdout);
        return 0;
    }

    // update serial port settings
    tty.c_cflag &= ~PARENB;        // Clear parity bit, disabling parity
    tty.c_cflag |= PARENB;         // Enable parity
    tty.c_cflag &= ~CSTOPB;        // Clear stop field, only one stop bit used in communication
    tty.c_cflag &= ~CSIZE;         // Clear all the size bits
    tty.c_cflag |= CS8;            // 8 bits per byte
    tty.c_cflag &= ~CRTSCTS;       // Disable RTS/CTS hardware flow control
    tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)
    // In canonical mode, input is processed when a new line character is received.
    tty.c_lflag &= ~ICANON;                                                      // Canonical mode is disabled
    tty.c_lflag &= ~ECHO;                                                        // Disable echo
    tty.c_lflag &= ~ECHOE;                                                       // Disable erasure
    tty.c_lflag &= ~ECHONL;                                                      // Disable new-line echo
    tty.c_lflag &= ~ISIG;                                                        // Disable interpretation of INTR, QUIT and SUSP
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);                                      // Turn off s/w flow ctrl
    tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); // Disable any special handling of received bytes
    tty.c_oflag &= ~OPOST;                                                       // Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR;                                                       // Prevent conversion of newline to carriage return/line feed
    // tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT IN LINUX)
    // tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT IN LINUX)
    tty.c_cc[VTIME] = 0; // wait up to 100ms
    tty.c_cc[VMIN] = 0;

    // set rs485 mode
    struct serial_rs485 rs485conf = {0};
    rs485conf.flags |= SER_RS485_ENABLED;
    if (ioctl (serial_port, TIOCSRS485, &rs485conf) < 0) {
        perror("ioctl");
        return 1;
    }

    // Set in/out baud rate to be LINE_BAUDRATE bps
    cfsetispeed(&tty, LINE_BAUDRATE);
    cfsetospeed(&tty, LINE_BAUDRATE);

    // Save tty settings, also checking for error
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0)
    {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
        fflush(stdout);
        return 0;
    }

With the code block:

        // set rs485 mode
    struct serial_rs485 rs485conf = {0};
    rs485conf.flags |= SER_RS485_ENABLED;
    if (ioctl (serial_port, TIOCSRS485, &rs485conf) < 0) {
        perror("ioctl");
        return 1;
    }

Or without it the results are the same...

I think it seems like the RS485 mode hasn't been successfully set. You can try updating the device using the new DTO because in the new DTO, RS485 mode is enabled by default.You can also configure the serial port tool (minicom) to RS485 mode first and conduct a transmission and reception test.

fjuliofontes commented 1 month ago

In this commit, RS485_DE configures the serial port to RS485 mode and removes tcdrain().

That is true, however the DE/nDE control is still done by software. What essentially does the configuration to RS485? Because from what I understand the configuration is usually used to enable the RTS control and since the RTS pin is not the same as the one actually connected to the DE/nRE pin I'm not sure what will this improve.

After configuring the serial port to RS485 mode, the serial port will send data in blocking mode instead of DMA, so tcdrain() is no longer needed.

Not sure if that works. From the tests that I did it never worked. Could it be because of OS versions? I'm running on Balena OS and the only way that I got it to work (with 10% error rate) was by doing nanosleep() after the write() of the number of bytes transmitted multiplied by the byte time. I'm running with 19200 baud. This is how I'm configuring the UART port. I'm running everything within C code to try to reduce to the minimum the latency.

    struct termios tty;

    // setup serial port now
    int serial_port = open(tty_name, O_RDWR | O_NOCTTY | O_NDELAY);
    if (serial_port < 0)
    {
        printf("Error while configuring serial port: %s\n", tty_name);
        fflush(stdout);
        return 0;
    }

    // get serial port configuration
    if (tcgetattr(serial_port, &tty) != 0)
    {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
        fflush(stdout);
        return 0;
    }

    // update serial port settings
    tty.c_cflag &= ~PARENB;        // Clear parity bit, disabling parity
    tty.c_cflag |= PARENB;         // Enable parity
    tty.c_cflag &= ~CSTOPB;        // Clear stop field, only one stop bit used in communication
    tty.c_cflag &= ~CSIZE;         // Clear all the size bits
    tty.c_cflag |= CS8;            // 8 bits per byte
    tty.c_cflag &= ~CRTSCTS;       // Disable RTS/CTS hardware flow control
    tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)
    // In canonical mode, input is processed when a new line character is received.
    tty.c_lflag &= ~ICANON;                                                      // Canonical mode is disabled
    tty.c_lflag &= ~ECHO;                                                        // Disable echo
    tty.c_lflag &= ~ECHOE;                                                       // Disable erasure
    tty.c_lflag &= ~ECHONL;                                                      // Disable new-line echo
    tty.c_lflag &= ~ISIG;                                                        // Disable interpretation of INTR, QUIT and SUSP
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);                                      // Turn off s/w flow ctrl
    tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); // Disable any special handling of received bytes
    tty.c_oflag &= ~OPOST;                                                       // Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR;                                                       // Prevent conversion of newline to carriage return/line feed
    // tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT IN LINUX)
    // tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT IN LINUX)
    tty.c_cc[VTIME] = 0; // wait up to 100ms
    tty.c_cc[VMIN] = 0;

    // set rs485 mode
    struct serial_rs485 rs485conf = {0};
    rs485conf.flags |= SER_RS485_ENABLED;
    if (ioctl (serial_port, TIOCSRS485, &rs485conf) < 0) {
        perror("ioctl");
        return 1;
    }

    // Set in/out baud rate to be LINE_BAUDRATE bps
    cfsetispeed(&tty, LINE_BAUDRATE);
    cfsetospeed(&tty, LINE_BAUDRATE);

    // Save tty settings, also checking for error
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0)
    {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
        fflush(stdout);
        return 0;
    }

With the code block:

        // set rs485 mode
    struct serial_rs485 rs485conf = {0};
    rs485conf.flags |= SER_RS485_ENABLED;
    if (ioctl (serial_port, TIOCSRS485, &rs485conf) < 0) {
        perror("ioctl");
        return 1;
    }

Or without it the results are the same...

I think it seems like the RS485 mode hasn't been successfully set. You can try updating the device using the new DTO because in the new DTO, RS485 mode is enabled by default.You can also configure the serial port tool (minicom) to RS485 mode first and conduct a transmission and reception test.

I can try the new DTO. Testing modbus RUT with minicom is not easy. You have to dump the frame in hex mode. But I will test the new DTO. Where can I get the compiled version "dtbo" for this new DTO? Since I'm running on Balena OS I need to add it compiled I think.

is-qian commented 1 month ago

Here is the newly compiled gtbo file. You can try it.

fjuliofontes commented 1 month ago

Here is the newly compiled gtbo file. You can try it.

I tested the new dtbo and without the nanosleep function after transmitting the communication won't work. I can connect the enable pin to an oscilloscope if needed but I guess that the enable pin is being toggled before the bytes are copied from the buffers to the UART.

Are you sure that the UART is not in DMA after applying the dtbo?

is-qian commented 1 month ago

Here is the newly compiled gtbo file. You can try it.

I tested the new dtbo and without the nanosleep function after transmitting the communication won't work. I can connect the enable pin to an oscilloscope if needed but I guess that the enable pin is being toggled before the bytes are copied from the buffers to the UART.

Are you sure that the UART is not in DMA after applying the dtbo?

In this dtbo, the serial port is configured as RS485 mode by default, and in this mode, the serial port sends data in a blocking manner unless your serial port tool modifies it. For example, if you're using minicom, it will turn off the RS485 mode by default when opening the serial port, so please ensure that your program or serial port tool has not reconfigured the serial port. You can directly use the echo and cat commands to test the serial port at different baud rates. Additionally, with the latest dtbo and rs485_de tools, we can utilize Modbus at 115200 baud rate or higher within node_red.

is-qian commented 1 month ago

This is a demo I modified based on your code. It sends the data received by RX out through TX.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <gpiod.h>
#include <pty.h>
#include <linux/serial.h>// setup serial port now
#define LINE_BAUDRATE B115200
#define BUFFER_SIZE 512
#define RS485_CONSUMER "RS485_program"

struct gpiod_chip *chip;
struct gpiod_line *line;

void setup_gpio(char *rs485_de_chip, int rs485_de_line)
{
    int ret;
    // open GPIO controller
    chip = gpiod_chip_open(rs485_de_chip);
    if (!chip) {
        fprintf(stderr, "gpiod_chip_open");
        exit(EXIT_FAILURE);
    }

    // request GPIO line
    line = gpiod_chip_get_line(chip, rs485_de_line);
    if (!line) {
        fprintf(stderr, "Failed to get GPIO line %d\n", rs485_de_line);
        gpiod_chip_close(chip);
        exit(EXIT_FAILURE);
    }

    // set GPIO as output
    ret = gpiod_line_request_output(line, RS485_CONSUMER, 0);
    if (ret < 0) {
        fprintf(stderr, "Failed to request GPIO line as output: %s\n", strerror(-ret));
        gpiod_chip_close(chip);
        exit(EXIT_FAILURE);
    }
}

void toggle_gpio_high() {
    gpiod_line_set_value(line, 1);
}

void toggle_gpio_low() {
    gpiod_line_set_value(line, 0);
}

int main(int argc, char* argv[])
{
    int serial_port, ready, rs485_de_line;
    struct termios tty;
    fd_set readfds, writefds;
    char *rs485_de_chip, *tty_name;
    ssize_t n;
    char buffer[BUFFER_SIZE];
    struct timeval timeout;

    if(argc < 4) {
        printf("Usage: %s /dev/ttyAMA* DE_CHIP(/dev/gpiochip*)  DE_LINE \n", argv[0]);
        return 1;
    }

    tty_name = argv[1];
    rs485_de_chip = argv[2];
    rs485_de_line = atoi(argv[3]);

    setup_gpio(rs485_de_chip, rs485_de_line);

    serial_port = open(tty_name, O_RDWR | O_NOCTTY | O_NDELAY);
    if (serial_port < 0)
    {
        printf("Error while configuring serial port: %s\n", tty_name);
        fflush(stdout);
        return 0;
    }

    // get serial port configuration
    if (tcgetattr(serial_port, &tty) != 0)
    {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
        fflush(stdout);
        return 0;
    }
    tty.c_cflag &= ~PARENB;        // Clear parity bit, disabling parity
    tty.c_cflag |= PARENB;         // Enable parity
    tty.c_cflag &= ~CSTOPB;        // Clear stop field, only one stop bit used in communication
    tty.c_cflag &= ~CSIZE;         // Clear all the size bits
    tty.c_cflag |= CS8;            // 8 bits per byte
    tty.c_cflag &= ~CRTSCTS;       // Disable RTS/CTS hardware flow control
    tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)
    // In canonical mode, input is processed when a new line character is received.
    tty.c_lflag &= ~ICANON;                                                      // Canonical mode is disabled
    tty.c_lflag &= ~ECHO;                                                        // Disable echo
    tty.c_lflag &= ~ECHOE;                                                       // Disable erasure
    tty.c_lflag &= ~ECHONL;                                                      // Disable new-line echo
    tty.c_lflag &= ~ISIG;                                                        // Disable interpretation of INTR, QUIT and SUSP
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);                                      // Turn off s/w flow ctrl
    tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); // Disable any special handling of received bytes
    tty.c_oflag &= ~OPOST;                                                       // Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR;                                                       // Prevent conversion of newline to carriage return/line feed
    // tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT IN LINUX)
    // tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT IN LINUX)
    tty.c_cc[VTIME] = 0; // wait up to 100ms
    tty.c_cc[VMIN] = 0;

    // set rs485 mode
    struct serial_rs485 rs485conf = {0};
    rs485conf.flags |= SER_RS485_ENABLED;
    if (ioctl (serial_port, TIOCSRS485, &rs485conf) < 0) {
        perror("ioctl");
        return 1;
    }

    // Set in/out baud rate to be LINE_BAUDRATE bps
    // tcgetattr(serial_port, &tty);
    cfmakeraw(&tty);
    cfsetispeed(&tty, LINE_BAUDRATE);
    cfsetospeed(&tty, LINE_BAUDRATE);

    // Save tty settings, also checking for error
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0)
    {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
        fflush(stdout);
        return 0;
    }
    int num = 0;
    while (1) {
        FD_ZERO(&readfds);
        FD_SET(serial_port, &readfds);
        timeout.tv_sec = 0; // wait 0 second
        timeout.tv_usec = 500; // wait 200 micro second
        ready = select(serial_port + 1, &readfds, NULL, NULL, &timeout);
        if (ready == -1) {
            perror("select");
            break;
        }
         // 判断是否还有数据接收
        else if (ready == 0) {
            if (num > 0) {
                // send data to serial port
                toggle_gpio_high();
                write(serial_port, buffer, num);
                toggle_gpio_low();
                num = 0;
            }
            continue;
        }
        if (FD_ISSET(serial_port, &readfds)) {
            // data from serial port is ready to read

            n = read(serial_port, buffer + num, BUFFER_SIZE - num);
            num += n;
            buffer[num] = '\0';
        }

    }

    // close serial port
    close(serial_port);
    return 0;

}

Here are my test results 企业微信截图_17268250775076

is-qian commented 1 month ago

The speed of the DE/nRE pin is sufficient. The problem is that you should not send data when receiving data. The program should adopt a half-duplex working mode.

fjuliofontes commented 3 weeks ago

Thank you, I will test and comment. For now since I've a pilot this week I've switched for two external USB-RS485 dongles. I also heard that a new redesign is expected to switch the DE/nRE pin for the RTS pin of the given UART, so I'm not sure if I should waste much more time on this. The program that I'm doing requires a error rate less than 5% since it will read counters and if I fail to read them I will lose data points. With my previous version I was having a error rate of 10%.