大家好,我是你们的嵌入式转 ROS2 战友!在前面的 Day1~Day14 中,我们搞定了 ROS2 的核心通信机制(话题、服务、参数、Launch 文件),但要让 ROS2 真正 “落地” 到硬件,串口通信是绕不开的第一道坎 —— 毕竟绝大多数单片机、传感器、工业模块还在靠 UART “说话”。
今天我们先补Linux 原生串口开发这门前置课:从 Ubuntu 串口权限踩坑,到底层 termios 结构体配置,再到用 C++ 写一套可复用的串口读写代码。学完这篇,你就能在 Ubuntu 下直接和单片机 “裸连”,下期我们再把它封装成 ROS2 节点,实现软硬无缝对接!
一、Ubuntu 串口权限配置(先解决 “打不开串口” 的坑)
在 Ubuntu 下,串口设备(比如 USB 转串口模块/dev/ttyUSB0、硬件串口/dev/ttyS0)默认属于dialout用户组,普通用户没有读写权限 —— 这就是为什么你直接运行代码会报Permission denied。
1.1 永久生效方案(推荐)
把当前用户加入dialout组,重启或重新登录后永久生效:
sudo usermod -aG dialout $USER1.2 临时验证方案(重启失效)
如果只是临时测试,可以用chmod直接修改设备权限(重启后会恢复,不推荐生产环境用):
sudo chmod 666 /dev/ttyUSB0 # 给所有用户读写权限1.3 如何确认串口设备?
插入 USB 转串口模块后,用以下命令查看设备列表:
dmesg | grep ttyUSB # 看内核日志里的 USB 串口设备 ls -l /dev/ttyUSB* # 直接列出设备文件二、串口核心参数详解(和单片机 “对暗号” 的基础)
串口通信要 “通”,两端必须参数完全一致:波特率、数据位、停止位、校验位,一个都不能错!
2.1 波特率(Baud Rate)
表示每秒传输的比特数,常用值:9600、19200、38400、115200(嵌入式最常用 115200)。
2.2 数据位(Data Bits)
每次传输的有效数据位数,通常是 8 位(因为一个字节 8 位)。
2.3 停止位(Stop Bits)
表示一帧数据结束的标志,通常是 1 位。
2.4 校验位(Parity)
用于简单的错误检测,嵌入式最常用None(无校验)。
三、Linux 串口编程核心:termios 结构体
Linux 下串口编程靠的是 POSIX 标准的termios接口,核心是配置struct termios结构体。嵌入式开发一般用原始模式(Raw Mode),即不做任何预处理,直接收发字节流。
关键配置思路:
- c_cflag:使能接收、忽略调制解调器、设置数据位;
- c_iflag:关闭软件流控、关闭回车换行转换;
- c_lflag:关闭规范模式、关闭回显;
- c_oflag:关闭输出处理。
四、C++ 原生串口读写代码实战(可直接复用)
下面写一套完整的 C++ 串口类,包含:打开串口、配置参数、发送数据、接收数据、关闭串口。代码全英文注释,方便后续移植到 ROS2 节点。
4.1 完整代码实现
#include <iostream> #include <fcntl.h> // File control definitions #include <termios.h> // POSIX terminal control definitions #include <unistd.h> // UNIX standard function definitions #include <cstring> // String operations class LinuxSerial { private: int serial_fd; // Serial port file descriptor public: LinuxSerial() : serial_fd(-1) {} // Open serial port // @param port: Serial port path (e.g., "/dev/ttyUSB0") // @return: true if success, false otherwise bool openPort(const char* port) { // Open port: Read/Write, No controlling terminal, Non-blocking mode first serial_fd = open(port, O_RDWR | O_NOCTTY | O_NDELAY); if (serial_fd == -1) { std::cerr << "Error: Failed to open port " << port << std::endl; return false; } // Restore blocking mode for read() fcntl(serial_fd, F_SETFL, 0); return true; } // Configure serial port parameters // @param baud_rate: Baud rate (e.g., B115200, B9600) // @param data_bits: Data bits (5, 6, 7, 8) // @param stop_bits: Stop bits (1, 2) // @param parity: Parity ('N' for None, 'O' for Odd, 'E' for Even) // @return: true if success, false otherwise bool configurePort(speed_t baud_rate, int data_bits = 8, int stop_bits = 1, char parity = 'N') { if (serial_fd == -1) { std::cerr << "Error: Port not opened yet!" << std::endl; return false; } struct termios tty; memset(&tty, 0, sizeof(tty)); // Clear struct // Get current terminal attributes if (tcgetattr(serial_fd, &tty) != 0) { std::cerr << "Error: Failed to get termios attributes!" << std::endl; return false; } // Set baud rate for input and output cfsetispeed(&tty, baud_rate); cfsetospeed(&tty, baud_rate); // Enable receiver, ignore modem control lines tty.c_cflag |= (CLOCAL | CREAD); // Set data bits tty.c_cflag &= ~CSIZE; // Clear data bits mask switch (data_bits) { case 5: tty.c_cflag |= CS5; break; case 6: tty.c_cflag |= CS6; break; case 7: tty.c_cflag |= CS7; break; case 8: tty.c_cflag |= CS8; break; default: std::cerr << "Error: Invalid data bits!" << std::endl; return false; } // Set stop bits switch (stop_bits) { case 1: tty.c_cflag &= ~CSTOPB; break; // 1 stop bit case 2: tty.c_cflag |= CSTOPB; break; // 2 stop bits default: std::cerr << "Error: Invalid stop bits!" << std::endl; return false; } // Set parity switch (parity) { case 'N': // No parity tty.c_cflag &= ~PARENB; tty.c_iflag &= ~INPCK; // Disable parity check break; case 'O': // Odd parity tty.c_cflag |= (PARENB | PARODD); tty.c_iflag |= INPCK; // Enable parity check break; case 'E': // Even parity tty.c_cflag |= PARENB; tty.c_cflag &= ~PARODD; tty.c_iflag |= INPCK; // Enable parity check break; default: std::cerr << "Error: Invalid parity!" << std::endl; return false; } // -------------------------- // Input flags configuration // -------------------------- // Disable software flow control (XON/XOFF) tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Disable carriage return to newline conversion tty.c_iflag &= ~(ICRNL | INLCR); // -------------------------- // Output flags configuration // -------------------------- // Disable output processing (raw output) tty.c_oflag &= ~OPOST; // -------------------------- // Local flags configuration // -------------------------- // Disable canonical mode (raw input) tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // -------------------------- // Control characters configuration // -------------------------- // Read() blocks until at least 1 character is received tty.c_cc[VMIN] = 1; // Read() timeout (in deciseconds, 0 = no timeout) tty.c_cc[VTIME] = 0; // Flush input and output buffers tcflush(serial_fd, TCIOFLUSH); // Set new terminal attributes if (tcsetattr(serial_fd, TCSANOW, &tty) != 0) { std::cerr << "Error: Failed to set termios attributes!" << std::endl; return false; } return true; } // Send data to serial port // @param data: Pointer to data buffer // @param len: Length of data to send // @return: Number of bytes sent, -1 if error int sendData(const uint8_t* data, size_t len) { if (serial_fd == -1) { std::cerr << "Error: Port not opened yet!" << std::endl; return -1; } return write(serial_fd, data, len); } // Receive data from serial port (blocking mode) // @param buffer: Pointer to buffer to store received data // @param len: Maximum length to receive // @return: Number of bytes received, -1 if error int receiveData(uint8_t* buffer, size_t len) { if (serial_fd == -1) { std::cerr << "Error: Port not opened yet!" << std::endl; return -1; } return read(serial_fd, buffer, len); } // Close serial port void closePort() { if (serial_fd != -1) { close(serial_fd); serial_fd = -1; } } ~LinuxSerial() { closePort(); // Auto close on destruction } }; // -------------------------- // Example usage // -------------------------- int main() { LinuxSerial serial; const char* port = "/dev/ttyUSB0"; speed_t baud_rate = B115200; // 1. Open port if (!serial.openPort(port)) { return -1; } std::cout << "Port " << port << " opened successfully!" << std::endl; // 2. Configure port (115200, 8N1) if (!serial.configurePort(baud_rate, 8, 1, 'N')) { serial.closePort(); return -1; } std::cout << "Port configured successfully! (115200 8N1)" << std::endl; // 3. Send test data uint8_t send_buf[] = "Hello STM32!"; int send_len = serial.sendData(send_buf, sizeof(send_buf) - 1); // Exclude '\0' std::cout << "Sent " << send_len << " bytes: " << send_buf << std::endl; // 4. Receive data (blocking) uint8_t recv_buf[256]; std::cout << "Waiting for data..." << std::endl; int recv_len = serial.receiveData(recv_buf, sizeof(recv_buf) - 1); if (recv_len > 0) { recv_buf[recv_len] = '\0'; // Add null terminator for C string std::cout << "Received " << recv_len << " bytes: " << recv_buf << std::endl; } // 5. Close port serial.closePort(); std::cout << "Port closed." << std::endl; return 0; }4.2 代码编译与测试
把代码保存为serial_test.cpp,用 g++ 编译:
g++ serial_test.cpp -o serial_test运行前确保串口权限配置好,然后执行:
./serial_test测试建议:用 USB 转串口模块连接电脑,短接 TX 和 RX(自发自收),运行代码应该能看到发送的数据又被接收回来。
五、总结与下期预告
今天我们啃下了 Linux 原生串口开发的硬骨头:
- 搞定了 Ubuntu 串口权限(加入 dialout 组);
- 理解了串口四大参数(波特率、数据位、停止位、校验位);
- 掌握了 termios 结构体的原始模式配置;
- 写了一套可复用的 C++ 串口类,支持收发数据。
这一切都是为了下期内容做铺垫 ——【ROS2 速成 - Day16】我们会把今天的 LinuxSerial 类封装成 ROS2 的串口通信节点,实现 “ROS2 话题 ↔ 串口数据” 的双向透传,真正让你的 STM32、ESP32 和 ROS2 世界连起来!
码字不易,欢迎点赞 + 收藏 + 关注!有问题的同学可以在评论区留言,我会逐一解答~我们 Day16 见!
📢 下期连载预告:【ROS2 速成 - Day16】ROS2 串口通信节点开发(软硬通信核心)