串口通信
串口是機器人裏最常見的通信方式之一:上位機 ROS2 / Linux 程序通過 USB-TTL、USB-CAN 轉串口、CH340、CP2102、FT232 等設備和 STM32、ESP32、下位機控制板通信。
本節依舊遵循順序:
先写普通 main() 里的同步示例
再写普通 main() 里的异步示例
最后写类封装版本
所有異步回調統一使用 std::bind。
Linux 下準備虛擬串口測試環境
如果你手裏沒有真實 STM32,可以用 socat 創建一對虛擬串口。
安裝 socat
Ubuntu / Debian:
sudo apt install socat
Fedora:
sudo dnf install socat
創建一對虛擬串口
新開一個終端,運行:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
你會看到類似輸出:
2026/05/24 12:00:00 socat[12345] N PTY is /dev/pts/3
2026/05/24 12:00:00 socat[12345] N PTY is /dev/pts/4
2026/05/24 12:00:00 socat[12345] N starting data transfer loop with FDs [5,5] and [7,7]
這表示:
/dev/pts/3 和 /dev/pts/4 是一对互通的虚拟串口
程序写 /dev/pts/3,另一个终端读 /dev/pts/4 就能看到
程序读 /dev/pts/3,另一个终端写 /dev/pts/4 就能发给程序
下面示例都假設程序使用 /dev/pts/3,另一個終端使用 /dev/pts/4。你要按自己終端實際輸出修改。
示例 1:普通 main() 裏同步打開串口併發送一行
程序目標
程序打開串口 /dev/pts/3,設置波特率 115200,然後發送:
hello serial
另一個終端從 /dev/pts/4 讀到這行數據。
完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <iostream>
#include <string>
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// serial_port 表示串口设备,后续读写都通过它完成。
boost::asio::serial_port serial(io);
boost::system::error_code ec;
std::cout << "main:准备打开串口 " << port_name << std::endl;
serial.open(port_name, ec);
if (ec)
{
std::cout << "打开串口失败:" << ec.message() << std::endl;
return 1;
}
serial.set_option(boost::asio::serial_port_base::baud_rate(115200));
serial.set_option(boost::asio::serial_port_base::character_size(8));
serial.set_option(boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial.set_option(boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial.set_option(boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::string msg = "hello serial\n";
// 把缓冲区中的数据写入连接。
std::size_t n = boost::asio::write(serial, boost::asio::buffer(msg), ec);
if (ec)
{
std::cout << "发送失败:" << ec.message() << std::endl;
return 1;
}
std::cout << "main:发送成功,字节数 = " << n << std::endl;
std::cout << "main:程序结束" << std::endl;
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
終端 1:創建虛擬串口。
socat -d -d pty,raw,echo=0 pty,raw,echo=0
終端 2:監聽另一端。
cat /dev/pts/4
終端 3:編譯運行程序。
g++ demo1_serial_write.cpp -o demo1_serial_write -std=c++17 -lboost_system -pthread
./demo1_serial_write /dev/pts/3
運行輸出與時間順序
程序終端輸出:
main:准备打开串口 /dev/pts/3
main:发送成功,字节数 = 13
main:程序结束
監聽終端輸出:
hello serial
這個示例基本沒有明顯延遲,發送完成後程序立刻結束。
本示例需要注意的點
這是同步寫串口。boost::asio::write() 會阻塞,直到數據寫完或發生錯誤。
關鍵函數說明
serial.open(port_name, ec)
作用:打開串口設備。
參數:
port_name
例如 /dev/ttyUSB0、/dev/ttyACM0、/dev/pts/3。
ec
用於接收錯誤信息。這樣寫不會拋異常,更適合教程和機器人工程。
serial.set_option(...)
作用:設置串口參數,例如波特率、數據位、校驗位、停止位、流控。
boost::asio::write(serial, boost::asio::buffer(msg), ec)
作用:同步寫數據。
返回值:實際寫入的字節數。
示例 2:普通 main() 裏同步讀取一行串口數據
程序目標
程序打開串口並阻塞等待,直到收到 \n 爲止。
完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <iostream>
#include <istream>
#include <string>
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// serial_port 表示串口设备,后续读写都通过它完成。
boost::asio::serial_port serial(io);
boost::system::error_code ec;
serial.open(port_name, ec);
if (ec)
{
std::cout << "打开串口失败:" << ec.message() << std::endl;
return 1;
}
// 设置串口或 socket 参数,例如波特率、数据位或超时相关配置。
serial.set_option(boost::asio::serial_port_base::baud_rate(115200));
boost::asio::streambuf buffer;
std::cout << "main:等待串口收到一行数据,以换行符结束" << std::endl;
std::size_t n = boost::asio::read_until(serial, buffer, '\n', ec);
if (ec)
{
std::cout << "读取失败:" << ec.message() << std::endl;
return 1;
}
std::istream is(&buffer);
std::string line;
std::getline(is, line);
std::cout << "main:读取到字节数 = " << n << std::endl;
std::cout << "main:读取到一行 = " << line << std::endl;
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
終端 1:創建虛擬串口。
socat -d -d pty,raw,echo=0 pty,raw,echo=0
終端 2:運行程序。
g++ demo2_serial_read_line.cpp -o demo2_serial_read_line -std=c++17 -lboost_system -pthread
./demo2_serial_read_line /dev/pts/3
終端 3:向另一端寫數據。
echo "stm32 ok" > /dev/pts/4
運行輸出與時間順序
程序啓動後立刻輸出:
main:等待串口收到一行数据,以换行符结束
然後程序阻塞。你在另一個終端執行:
echo "stm32 ok" > /dev/pts/4
程序繼續輸出:
main:读取到字节数 = 9
main:读取到一行 = stm32 ok
這裏的等待時間取決於你什麼時候發送數據。
本示例需要注意的點
read_until() 是同步阻塞函數。沒有收到換行符之前,它會一直卡住。
如果你在 ROS2 回調裏直接這樣讀串口,可能導致整個節點響應變慢。
關鍵函數說明
boost::asio::read_until(serial, buffer, '\n', ec)
作用:從串口讀取數據,直到遇到換行符 \n。
返回值:至少讀取到分隔符爲止的字節數。
注意:streambuf 裏可能還留有額外數據,後續讀取時要注意處理。
示例 3:普通 main() 裏異步讀取一行串口數據
程序目標
把示例 2 改成異步版本:
async_read_until()註冊讀取任務;io.run()開始等待;- 另一端發送一行數據;
- 回調函數執行;
io.run()返回。
完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <functional>
#include <iostream>
#include <istream>
#include <string>
void on_read_line(const boost::system::error_code& ec,
std::size_t bytes_transferred,
boost::asio::streambuf* buffer)
{
if (ec)
{
std::cout << "on_read_line:读取失败:" << ec.message() << std::endl;
return;
}
std::istream is(buffer);
std::string line;
std::getline(is, line);
std::cout << "on_read_line:bytes_transferred = " << bytes_transferred << std::endl;
std::cout << "on_read_line:收到一行 = " << line << std::endl;
}
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// serial_port 表示串口设备,后续读写都通过它完成。
boost::asio::serial_port serial(io);
boost::system::error_code ec;
serial.open(port_name, ec);
if (ec)
{
std::cout << "打开串口失败:" << ec.message() << std::endl;
return 1;
}
serial.set_option(boost::asio::serial_port_base::baud_rate(115200));
boost::asio::streambuf buffer;
std::cout << "main:注册 async_read_until" << std::endl;
boost::asio::async_read_until(serial,
buffer,
'\n',
std::bind(on_read_line,
std::placeholders::_1,
std::placeholders::_2,
&buffer));
std::cout << "main:async_read_until 已返回,准备 io.run()" << std::endl;
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
io.run();
std::cout << "main:io.run() 返回" << std::endl;
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
終端 1:創建虛擬串口。
socat -d -d pty,raw,echo=0 pty,raw,echo=0
終端 2:運行程序。
g++ demo3_async_serial_read.cpp -o demo3_async_serial_read -std=c++17 -lboost_system -pthread
./demo3_async_serial_read /dev/pts/3
終端 3:發送數據。
echo "encoder 123" > /dev/pts/4
運行輸出與時間順序
程序立刻輸出:
main:注册 async_read_until
main:async_read_until 已返回,准备 io.run()
程序卡在 io.run()。你發送數據後,程序輸出:
on_read_line:bytes_transferred = 12
on_read_line:收到一行 = encoder 123
main:io.run() 返回
完整輸出類似:
main:注册 async_read_until
main:async_read_until 已返回,准备 io.run()
on_read_line:bytes_transferred = 12
on_read_line:收到一行 = encoder 123
main:io.run() 返回
本示例需要注意的點
async_read_until() 不阻塞。它只是註冊讀取任務。
如果你不調用:
io.run();
回調函數永遠不會執行。
回調參數說明
async_read_until() 的回調一般接收兩個參數:
const boost::system::error_code& ec
std::size_t bytes_transferred
所以綁定時用:
std::placeholders::_1
std::placeholders::_2
我們額外傳了:
&buffer
用於在回調裏取出具體字符串。
示例 4:類裏封裝異步串口行讀取
程序目標
把異步串口讀取封裝成類。這個結構更接近真實機器人項目。
程序會連續讀取多行串口數據,每收到一行就打印,然後繼續註冊下一次讀取。
完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <functional>
#include <iostream>
#include <istream>
#include <string>
class SerialLineReader
{
public:
SerialLineReader(boost::asio::io_context& io, const std::string& port_name)
: serial_(io)
{
boost::system::error_code ec;
serial_.open(port_name, ec);
if (ec)
{
std::cout << "打开串口失败:" << ec.message() << std::endl;
return;
}
serial_.set_option(boost::asio::serial_port_base::baud_rate(115200));
serial_.set_option(boost::asio::serial_port_base::character_size(8));
serial_.set_option(boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial_.set_option(boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial_.set_option(boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::cout << "SerialLineReader:串口打开成功,开始异步读取" << std::endl;
start_read();
}
private:
void start_read()
{
boost::asio::async_read_until(serial_,
buffer_,
'\n',
std::bind(&SerialLineReader::on_read,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_read(const boost::system::error_code& ec, std::size_t bytes_transferred)
{
if (ec)
{
std::cout << "SerialLineReader::on_read 错误:" << ec.message() << std::endl;
return;
}
std::istream is(&buffer_);
std::string line;
std::getline(is, line);
std::cout << "收到串口行,bytes = " << bytes_transferred
<< ",内容 = " << line << std::endl;
start_read();
}
private:
// serial_port 表示串口设备,后续读写都通过它完成。
boost::asio::serial_port serial_;
boost::asio::streambuf buffer_;
};
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
SerialLineReader reader(io, port_name);
std::cout << "main:调用 io.run(),按 Ctrl+C 退出" << std::endl;
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
io.run();
std::cout << "main:io.run() 返回" << std::endl;
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
終端 1:創建虛擬串口。
socat -d -d pty,raw,echo=0 pty,raw,echo=0
終端 2:運行程序。
g++ demo4_serial_class_reader.cpp -o demo4_serial_class_reader -std=c++17 -lboost_system -pthread
./demo4_serial_class_reader /dev/pts/3
終端 3:連續發送數據。
echo "imu 1 2 3" > /dev/pts/4
echo "odom 0.1 0.2" > /dev/pts/4
echo "battery 24.1" > /dev/pts/4
運行輸出與時間順序
程序啓動後輸出:
SerialLineReader:串口打开成功,开始异步读取
main:调用 io.run(),按 Ctrl+C 退出
每發送一行,程序打印一行:
收到串口行,bytes = 10,内容 = imu 1 2 3
收到串口行,bytes = 13,内容 = odom 0.1 0.2
收到串口行,bytes = 13,内容 = battery 24.1
程序不會自動退出,因爲每次收到一行後都會重新調用 start_read()。
本示例需要注意的點
這一句是連續讀取的關鍵:
start_read();
它在 on_read() 的最後再次註冊下一次異步讀取。
如果沒有這句,程序只會讀一行,然後 io.run() 就會返回。
類成員變量生命週期說明
boost::asio::serial_port serial_;
boost::asio::streambuf buffer_;
它們是類成員變量,生命週期和 SerialLineReader reader 一樣長。
這比在局部函數里創建臨時 buffer 更安全。
成員函數綁定說明
std::bind(&SerialLineReader::on_read,
this,
std::placeholders::_1,
std::placeholders::_2)
表示異步讀取完成後調用:
this->on_read(ec, bytes_transferred);
示例 5:類裏封裝異步串口寫隊列
程序目標
真實機器人上位機經常需要連續給下位機發命令,例如:
cmd_vel 0.2 0.0
cmd_vel 0.2 0.1
cmd_vel 0.0 0.0
異步寫時不能隨便多個 async_write() 同時寫同一個串口。更穩的做法是寫隊列:
如果当前没有写操作,就开始写
如果正在写,就先放入队列
当前写完后,再写下一条
完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <deque>
#include <functional>
#include <iostream>
#include <string>
class SerialWriter
{
public:
SerialWriter(boost::asio::io_context& io, const std::string& port_name)
: serial_(io)
{
boost::system::error_code ec;
serial_.open(port_name, ec);
if (ec)
{
std::cout << "打开串口失败:" << ec.message() << std::endl;
return;
}
serial_.set_option(boost::asio::serial_port_base::baud_rate(115200));
std::cout << "SerialWriter:串口打开成功" << std::endl;
}
void async_send(const std::string& msg)
{
bool writing = !write_queue_.empty();
write_queue_.push_back(msg);
std::cout << "加入发送队列:" << msg;
if (!writing)
{
do_write();
}
}
private:
void do_write()
{
// 注册异步写入,写完后由回调通知结果。
boost::asio::async_write(serial_,
boost::asio::buffer(write_queue_.front()),
std::bind(&SerialWriter::on_write,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_write(const boost::system::error_code& ec, std::size_t bytes_transferred)
{
if (ec)
{
std::cout << "发送失败:" << ec.message() << std::endl;
return;
}
std::cout << "发送完成,字节数 = " << bytes_transferred << std::endl;
write_queue_.pop_front();
if (!write_queue_.empty())
{
do_write();
}
}
private:
boost::asio::serial_port serial_;
std::deque<std::string> write_queue_;
};
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
SerialWriter writer(io, port_name);
writer.async_send("cmd_vel 0.2 0.0\n");
writer.async_send("cmd_vel 0.2 0.1\n");
writer.async_send("cmd_vel 0.0 0.0\n");
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
io.run();
std::cout << "main:发送队列清空,程序结束" << std::endl;
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
終端 1:創建虛擬串口。
socat -d -d pty,raw,echo=0 pty,raw,echo=0
終端 2:監聽另一端。
cat /dev/pts/4
終端 3:運行程序。
g++ demo5_serial_write_queue.cpp -o demo5_serial_write_queue -std=c++17 -lboost_system -pthread
./demo5_serial_write_queue /dev/pts/3
運行輸出與時間順序
程序終端輸出類似:
SerialWriter:串口打开成功
加入发送队列:cmd_vel 0.2 0.0
加入发送队列:cmd_vel 0.2 0.1
加入发送队列:cmd_vel 0.0 0.0
发送完成,字节数 = 16
发送完成,字节数 = 16
发送完成,字节数 = 16
main:发送队列清空,程序结束
監聽終端輸出:
cmd_vel 0.2 0.0
cmd_vel 0.2 0.1
cmd_vel 0.0 0.0
本示例需要注意的點
這裏不能把 msg 做成臨時局部變量後立刻異步寫,因爲異步寫可能還沒完成,數據已經銷燬。
本例用:
std::deque<std::string> write_queue_;
保存待發送數據,保證 buffer 指向的字符串在寫完成之前一直存在。
async_write() 說明
boost::asio::async_write(serial_, buffer, handler);
作用:異步寫完整個 buffer。
回調參數:
const boost::system::error_code& ec
std::size_t bytes_transferred
返回值:void。
本節總結
- 同步串口讀寫簡單,但會阻塞。
- 異步串口讀寫需要
io.run()驅動。 async_read_until()適合讀取以換行結尾的文本協議。- 二進制協議更適合
async_read_some()加自己的幀解析器。 - 異步寫必須注意 buffer 生命週期。
- 工程裏推薦把串口讀寫封裝成類,並用隊列管理發送數據。