我已经使用 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.
所以,从串口本身读取和每次覆盖数组似乎都是一个问题。再次强调,只是更多调试信息。
正如评论中提到的,
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;
}