如何修复 Arduino 串行的错误输出?

问题描述 投票:0回答:1

我已经使用 termios 库编写了一段代码,用于从 Arduino Mega 读取串行数据。这是我第一次使用这个库,所以我仍在尝试了解它以及一般串行通信的工作原理。

我的代码的最终目标是发送包含各种信息集的 JSON-eqsue 数据包。但是,截至目前,我只是从示例消息开始。一切看起来都很好,但输出不是简单地说:

Example Text.
Example Text.
Example Text.

我反而得到这个:

Et.Example Text.Example Text.
Example Text.xt.Example Text.

请注意,每秒都会收到此信息即使它只应该每秒发送一次文本。我认为这是缓冲区的问题,但是,我再次对串行通信协议如何深入工作感到困惑。

下面分别是计算机和Arduino代码。 (如果代码有点过头,抱歉;这只是为了练习和原型设计。)

// serial.cpp

#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <iostream>
#include <string>
#include <cstring>

int serial_port;
char machine_data[1024];
int max_fd;

int serial_setup(char conn[]) {

    serial_port = open(conn, O_RDWR); // or "/dev/ttyS0" for a physical serial port

    // Boilerplate code for the default serial settings
    if (serial_port == -1) {
        std::cerr << "Error opening serial port." << std::endl;
        return 1;
    }

    std::cout << "Serial port is open." << std::endl;

    struct termios tty;
    memset(&tty, 0, sizeof(tty));

    if (tcgetattr(serial_port, &tty) != 0) {
        std::cerr << "Error getting serial port attributes." << std::endl;
        return 1;
    }

    cfsetospeed(&tty, B38400); // Set baud rate
    cfsetispeed(&tty, B38400);

    tty.c_cflag &= ~PARENB; // Disable parity
    tty.c_cflag &= ~CSTOPB; // One stop bit
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8; // 8 data bits

    tty.c_cflag &= ~CRTSCTS; // Disable hardware flow control
    tty.c_cflag |= CREAD | CLOCAL; // Enable receiver, ignore modem control lines

    tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Disable software flow control

    tty.c_lflag = 0; // No canonical mode

    tty.c_oflag = 0; // No output processing

    tty.c_cc[VMIN] = 0; // Read doesn't block
    tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout

    if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
        std::cerr << "Error setting serial port attributes." << std::endl;
        return 1;
    }

    std::cout << "Finished the setup for the serial port." << std::endl;


    return 0;

}

std::string read_serial() {

    std::string return_string;
    // This section is for the setup for the select function.
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(serial_port, &rfds);
    max_fd = serial_port + 1;
    struct timeval timeout;
    timeout.tv_sec = 5;
    timeout.tv_usec = 10000;

    int ready = select(max_fd, &rfds, nullptr, nullptr, &timeout);
    if (ready > 0) {
        std::cout << "Data available." << std::endl;    
        int num_data = read(serial_port, machine_data, sizeof(machine_data));
        return_string = machine_data;
        std::cout << "Completed a read function." << std::endl;
    }

    else { 
        std::cout << "Did not complete." << std::endl;

    }

    return return_string;

}

int main() {
    char serial_port[] = "/dev/ttyACM0";

    int status = serial_setup(serial_port);

    while (true) {
        std::string text = read_serial(); 

        std::cout << text << std::endl;
    }
    return 0;

}
// exp_serial.ino

void setup() {
    Serial.begin(38400);
}

void loop() {
    Serial.write("Example Text.");
    delay(1000);
}

编辑1:所以,在luantkow的建议之后,我得到了不同的输出,但它没有被破坏,而是被剪切了:

e Text.
Exam
ple Text.

我决定使用额外的打印函数来打印实际读取的字节数,并每次转储

machine_data
的内容。这是输出的示例:

Serial port is open.
Finished the setup for the serial port.
Data available.
Completed a read function.
Number of bytes availaible: 5
---> Examp
Dumping machine_data array.
Examp
Data available.
Completed a read function.
Number of bytes availaible: 8
---> le Text.
Dumping machine_data array.
le Text.
Data available.
Completed a read function.
Number of bytes availaible: 13
---> Example Text.
Dumping machine_data array.
Example Text.
Data available.
Completed a read function.
Number of bytes availaible: 10
---> Example Te
Dumping machine_data array.
Example Text.
Data available.
Completed a read function.
Number of bytes availaible: 3
---> xt.
Dumping machine_data array.
xt.mple Text.
Data available.
Completed a read function.
Number of bytes availaible: 4
---> Exam
Dumping machine_data array.
Example Text.
Data available.
Completed a read function.
Number of bytes availaible: 9
---> ple Text.
Dumping machine_data array.
ple Text.ext.
Data available.
Completed a read function.
Number of bytes availaible: 13
---> Example Text.
Dumping machine_data array.
Example Text.
Data available.
Completed a read function.
Number of bytes availaible: 8
---> Example 
Dumping machine_data array.
Example Text.
Data available.
Completed a read function.
Number of bytes availaible: 5
---> Text.
Dumping machine_data array.
Text.le Text.
Data available.
Completed a read function.
Number of bytes availaible: 3
---> Exa
Dumping machine_data array.
Exat.le Text.

所以,从串口本身读取每次覆盖数组似乎都是一个问题。再次强调,只是更多调试信息。

c++ arduino termios
1个回答
0
投票

正如评论中提到的,

read
成功时返回实际读取并可以使用的字节数。在您的代码中,您调用

char machine_data[1024];
[...]
std::string return_string
[...]
return_string = machine_data;

它有效地调用字符串上的赋值运算符,来自 https://en.cppreference.com/w/cpp/string/basic_string/operator%3D:

将内容替换为 s 指向的空终止字符串的内容,就像使用 allocate(s, Traits::length(s)) 一样。

因此 std::string 检查 machine_data 内容,查找设置为 0 的第一个字节。由于 machine_data 是一个全局变量,您可以假设它是用零预先初始化的。您的结果将有一个由读取结果组成的“尾巴”,该结果设法读取了稍微多的数据。要解决此问题,请使用字符串的分配方法,该方法获取指向第一个字符的指针以及要使用的字符数:

return_string.assign(machine_data, read_result);

正如您已经注意到的,这不足以逐行获取数据。您需要准备代码以应对以下可能性:

  • 仅读取部分行,需要再次尝试才能读取整行

  • 一次会读取多行

      #include <fcntl.h>
      #include <unistd.h>
      #include <termios.h>
      #include <iostream>
      #include <string>
      #include <cstring>
      #include <algorithm>
    
      int serial_setup(char conn[]) {
          int serial_port = open(conn, O_RDONLY);
    
          // Boilerplate code for the default serial settings
          if (serial_port == -1) {
              std::cerr << "Error opening serial port: %s" << strerror(errno) <<  std::endl;
              return serial_port;
          }
    
          std::clog << "Serial port is open." << std::endl;
    
          struct termios tty;
          memset(&tty, 0, sizeof(tty));
    
          if (tcgetattr(serial_port, &tty) != 0) {
              close(serial_port);
              std::cerr << "Error getting serial port attributes: " << strerror(errno) << std::endl;
              return -1;
          }
    
          cfsetospeed(&tty, B38400); // Set baud rate
          cfsetispeed(&tty, B38400);
    
          tty.c_cflag &= ~PARENB; // Disable parity
          tty.c_cflag &= ~CSTOPB; // One stop bit
          tty.c_cflag &= ~CSIZE;
          tty.c_cflag |= CS8; // 8 data bits
    
          tty.c_cflag &= ~CRTSCTS; // Disable hardware flow control
          tty.c_cflag |= CREAD | CLOCAL; // Enable receiver, ignore modem control lines
    
          tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Disable software flow control
    
          tty.c_lflag = 0; // No canonical mode
    
          tty.c_oflag = 0; // No output processing
    
          tty.c_cc[VMIN] = 0; // Read doesn't block
          tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout
    
          if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
              close(serial_port);
              std::cerr << "Error setting serial port attributes: " << strerror(errno) << std::endl;
              return -1;
          }
    
          std::clog << "Finished the setup for the serial port." << std::endl;
    
          return serial_port;
      }
    
      std::string read_line_serial(int serial_descriptor, char* buffer, size_t buffer_size, char** data_end)
      {
          char* data_start = buffer;
          do
          {
              char* end_of_line = std::find(data_start, *data_end, '\n');
    
              if(end_of_line != *data_end) // The new line character was found
              {
                  std::string result(data_start, end_of_line);
                  end_of_line++; // We don't need the '\n', lets jump over it
                  size_t data_left = *data_end - end_of_line;
                  memmove(buffer, end_of_line, data_left * sizeof(char));
                  *data_end = buffer + data_left;
                  return result;
              }
    
              fd_set rfds;
              FD_ZERO(&rfds);
              FD_SET(serial_descriptor, &rfds);
    
              int max_fd = serial_descriptor + 1;
              struct timeval timeout;
              timeout.tv_sec = 5;
              timeout.tv_usec = 10000;
    
              int ready = select(max_fd, &rfds, nullptr, nullptr, &timeout);
              if(ready == 0)
              {
                  return std::string();
              }
              size_t already_in_buffer = *data_end - buffer;
              if(already_in_buffer == buffer_size)
              {
                  std::cerr << "Buffer overflow, dropping line" << std::endl;
                  already_in_buffer = 0;
                  *data_end = buffer;
              }
              int num_data = read(serial_descriptor, *data_end, buffer_size - already_in_buffer);
              if(num_data == -1)
              {
                  std::cerr << "Read failed with: " << strerror(errno) << std::endl;
                  return std::string();
              }
              data_start = *data_end;
              *data_end += num_data;
          }
          while(true);
      }
    
      int main(int argc, char** argv) {
    
          if(argc < 2)
          {
              std::cerr << "Provide path to serial port" << std::endl;
              return 1;
          }
    
          int serial_handle = serial_setup(argv[1]);
    
          if(serial_handle == -1)
          {
              return 1;
          }
    
          constexpr size_t buffer_size = 1024;
          char buffer[buffer_size];
          char* data_position = buffer;
    
          while (true) {
              std::string text = read_line_serial(serial_handle, buffer, buffer_size, &data_position);
    
              std::cout << text << std::endl;
          }
    
          close(serial_handle);
    
          return 0;
      }
    

您可以通过使用规范模式(您在

serial_setup
中明确禁用该模式)来简化代码:请参阅https://linux.die.net/man/3/tcsetattr

在规范模式下:

  • 输入可逐行使用。当键入行分隔符之一(NL、EOL、EOL2;或行首的 EOF)时,输入行可用。除了 EOF 的情况外,行分隔符包含在 read(2) 返回的缓冲区中。
  • 启用行编辑(ERASE、KILL;如果设置了 IEXTEN 标志:WERASE、REPRINT、LNEXT)。 read(2)最多返回一行输入;如果 read(2) 请求的字节数少于当前输入行中可用的字节数,则仅读取请求的字节数,其余字符将可用于将来的 read(2)。 在非规范模式下,可以立即输入(用户无需键入行分隔符),不执行任何输入处理,并且禁用行编辑。 MIN (c_cc[VMIN]) 和 TIME (c_cc[VTIME]) 的设置决定了 read(2) 完成的情况;

规范模式似乎完全满足您的需要:

    #include <fcntl.h>
    #include <unistd.h>
    #include <termios.h>
    #include <iostream>
    #include <string>
    #include <cstring>
    
    int serial_setup(char conn[]) {
    
        int serial_port = open(conn, O_RDONLY | O_CLOEXEC);
    
        // Boilerplate code for the default serial settings
        if (serial_port == -1) {
            std::cerr << "Error opening serial port: %s" << strerror(errno) <<  std::endl;
            return serial_port;
        }
    
        std::clog << "Serial port is open." << std::endl;
    
        struct termios tty;
        memset(&tty, 0, sizeof(tty));
    
        if (tcgetattr(serial_port, &tty) != 0) {
            close(serial_port);
            std::cerr << "Error getting serial port attributes: " << strerror(errno) << std::endl;
            return -1;
        }
    
        cfsetospeed(&tty, B38400); // Set baud rate
        cfsetispeed(&tty, B38400);
    
        tty.c_cflag &= ~PARENB; // Disable parity
        tty.c_cflag &= ~CSTOPB; // One stop bit
        tty.c_cflag &= ~CSIZE;
        tty.c_cflag |= CS8; // 8 data bits
    
        tty.c_cflag &= ~CRTSCTS; // Disable hardware flow control
        tty.c_cflag |= CREAD | CLOCAL; // Enable receiver, ignore modem control lines
    
        tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Disable software flow control
    
        tty.c_lflag = ICANON; // Canonical mode
    
        tty.c_oflag = 0; // No output processing
    
        if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
            close(serial_port);
            std::cerr << "Error setting serial port attributes: " << strerror(errno) << std::endl;
            return -1;
        }
    
        std::clog << "Finished the setup for the serial port." << std::endl;
    
        return serial_port;
    }
    
    std::string read_line_serial(int serial_descriptor)
    {
        constexpr size_t buffer_size = 4096;
        char buffer[buffer_size];
    
        fd_set rfds;
    
        FD_ZERO(&rfds);
        FD_SET(serial_descriptor, &rfds);
    
        int max_fd = serial_descriptor + 1;
    
        struct timeval timeout;
        timeout.tv_sec = 5;
        timeout.tv_usec = 10000;
    
        int ready = select(max_fd, &rfds, nullptr, nullptr, &timeout);
    
        if(ready == 0)
        {
            return std::string();
        }
    
        int num_data = read(serial_descriptor, buffer, buffer_size);
    
        if(num_data == -1)
        {
            std::cerr << "Read failed with: " << strerror(errno) << std::endl;
            return std::string();
        }
    
        return std::string(buffer, num_data);
    }
    
    int main(int argc, char** argv) {
    
        if(argc < 2)
        {
            std::cerr << "Provide path to serial port" << std::endl;
            return 1;
        }
    
        int serial_handle = serial_setup(argv[1]);
    
        if(serial_handle == -1)
        {
            return 1;
        }
    
        while (true) {
            std::string text = read_line_serial(serial_handle);
    
            std::cout << text;
            // We don't use std::endl because text will contain '\n'
            // but endl also flushes the stream:
            std::cout.flush();
        }
    
        close(serial_handle);
    
        return 0;
    }
    
© www.soinside.com 2019 - 2024. All rights reserved.