serial communication
Serial ports are one of the most common communication methods in robotics: host computer ROS2 / Linux programs communicate with STM32, ESP32, and lower-level control boards via USB-TTL, USB-CAN to serial adapters, CH340, CP2102, FT232, and other devices.
This section still follows the order:
先写普通 main() 里的同步示例
再写普通 main() 里的异步示例
最后写类封装版本
All asynchronous callbacks uniformly use std::bind.
Setting Up a Virtual Serial Port Test Environment on Linux
If you don't have a real STM32 in hand, you can use socat to create a pair of virtual serial ports.
Install socat
Ubuntu / Debian:
sudo apt install socat
Fedora:
sudo dnf install socat
Create a pair of virtual serial ports.
Open a new terminal and run:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
You will see output similar to:
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]
This means:
/dev/pts/3 和 /dev/pts/4 是一对互通的虚拟串口
程序写 /dev/pts/3,另一个终端读 /dev/pts/4 就能看到
程序读 /dev/pts/3,另一个终端写 /dev/pts/4 就能发给程序
The following examples assume the program uses /dev/pts/3, and the other terminal uses /dev/pts/4. You should modify according to the actual output of your own terminal.
Example 1: Synchronously open the serial port and send a line in ordinary main()
program objective
The program opens serial port /dev/pts/3, sets the baud rate to 115200, then sends:
hello serial
Another terminal reads this line of data from /dev/pts/4.
Full code
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
Compile and run
Terminal 1: Create virtual serial port.
socat -d -d pty,raw,echo=0 pty,raw,echo=0
Terminal 2: Listen on the other end.
cat /dev/pts/4
Terminal 3: Compile and run the program.
g++ demo1_serial_write.cpp -o demo1_serial_write -std=c++17 -lboost_system -pthread
./demo1_serial_write /dev/pts/3
Execution output and chronological order
Program terminal output:
main:准备打开串口 /dev/pts/3
main:发送成功,字节数 = 13
main:程序结束
Monitor terminal output:
hello serial
This example has virtually no noticeable delay, and the program ends immediately after sending is complete.
Points to note for this example
This is a synchronous serial port write. boost::asio::write() will block until the data is written or an error occurs.
Key Function Explanation
serial.open(port_name, ec)
Purpose: Open the serial port device.
Parameter:
port_name
For example, /dev/ttyUSB0, /dev/ttyACM0, /dev/pts/3.
ec
Used to receive error messages. This approach does not throw exceptions, making it more suitable for tutorials and robotics engineering.
serial.set_option(...)
Function: Set serial port parameters, such as baud rate, data bits, parity bit, stop bits, and flow control.
boost::asio::write(serial, boost::asio::buffer(msg), ec)
Purpose: Synchronously write data.
Return value: the number of bytes actually written.
Example 2: Synchronously read a line of serial data in a normal main()
program objective
The program opens the serial port and blocks waiting until it receives \n.
Full code
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
Compile and run
Terminal 1: Create virtual serial port.
socat -d -d pty,raw,echo=0 pty,raw,echo=0
Terminal 2: Run the program.
g++ demo2_serial_read_line.cpp -o demo2_serial_read_line -std=c++17 -lboost_system -pthread
./demo2_serial_read_line /dev/pts/3
Terminal 3: Write data to the other end.
echo "stm32 ok" > /dev/pts/4
Execution output and chronological order
Output immediately after program startup:
main:等待串口收到一行数据,以换行符结束
Then the program blocks. In another terminal, execute:
echo "stm32 ok" > /dev/pts/4
The program continues to output:
main:读取到字节数 = 9
main:读取到一行 = stm32 ok
The waiting time here depends on when you send the data.
Points to note for this example
read_until() is a synchronous blocking function. It will block until a newline character is received.
If you read the serial port directly like this within a ROS2 callback, it may cause the entire node's response to slow down.
Key Function Explanation
boost::asio::read_until(serial, buffer, '\n', ec)
Function: Read data from the serial port until a newline character \n is encountered.
Return value: the number of bytes read at least up to the delimiter.
Note: There may still be extra data in streambuf, so be careful to handle it appropriately during subsequent reads.
Example 3: Asynchronously reading a line of serial data in a regular main()
program objective
Change Example 2 to an asynchronous version:
async_read_until()register read task;io.run()start waiting;- The other end sends a line of data;
- Callback function execution;
io.run()return.
Full code
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
Compile and run
Terminal 1: Create virtual serial port.
socat -d -d pty,raw,echo=0 pty,raw,echo=0
Terminal 2: Run the program.
g++ demo3_async_serial_read.cpp -o demo3_async_serial_read -std=c++17 -lboost_system -pthread
./demo3_async_serial_read /dev/pts/3
Terminal 3: Send data.
echo "encoder 123" > /dev/pts/4
Execution output and chronological order
The program immediately outputs:
main:注册 async_read_until
main:async_read_until 已返回,准备 io.run()
Program stuck at io.run(). After you send data, the program outputs:
on_read_line:bytes_transferred = 12
on_read_line:收到一行 = encoder 123
main:io.run() 返回
请提供您要翻译的简体中文 Markdown 片段。
main:注册 async_read_until
main:async_read_until 已返回,准备 io.run()
on_read_line:bytes_transferred = 12
on_read_line:收到一行 = encoder 123
main:io.run() 返回
Points to note for this example
async_read_until() does not block. It only registers a read task.
If you do not call:
io.run();
The callback function will never execute.
Callback parameter description
async_read_until()'s callback usually receives two parameters:
const boost::system::error_code& ec
std::size_t bytes_transferred
So when binding, use:
std::placeholders::_1
std::placeholders::_2
We also passed:
&buffer
Used to retrieve the specific string in the callback.
Example 4: Encapsulating Asynchronous Serial Line Reading in a Class
program objective
Encapsulate asynchronous serial port reading into a class. This structure is closer to a real robot project.
The program continuously reads multiple lines of serial port data, prints each received line, and then registers for the next read.
Full code
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
Compile and run
Terminal 1: Create virtual serial port.
socat -d -d pty,raw,echo=0 pty,raw,echo=0
Terminal 2: Run the program.
g++ demo4_serial_class_reader.cpp -o demo4_serial_class_reader -std=c++17 -lboost_system -pthread
./demo4_serial_class_reader /dev/pts/3
Terminal 3: Continuous data transmission.
echo "imu 1 2 3" > /dev/pts/4
echo "odom 0.1 0.2" > /dev/pts/4
echo "battery 24.1" > /dev/pts/4
Execution output and chronological order
After the program starts, it outputs:
SerialLineReader:串口打开成功,开始异步读取
main:调用 io.run(),按 Ctrl+C 退出
Each time a line is sent, the program prints a line:
收到串口行,bytes = 10,内容 = imu 1 2 3
收到串口行,bytes = 13,内容 = odom 0.1 0.2
收到串口行,bytes = 13,内容 = battery 24.1
The program will not exit automatically, because each time a line is received, it calls start_read() again.
Points to note for this example
This sentence is the key to continuous reading:
start_read();
At the end of on_read(), it again registers the next asynchronous read.
If this line is not present, the program will only read one line, and then io.run() will return.
Class Member Variable Lifecycle Explanation
boost::asio::serial_port serial_;
boost::asio::streambuf buffer_;
They are class member variables with a lifetime as long as SerialLineReader reader.
This is safer than creating a temporary buffer in a local function.
Member function binding description
std::bind(&SerialLineReader::on_read,
this,
std::placeholders::_1,
std::placeholders::_2)
Called after the asynchronous read is complete:
this->on_read(ec, bytes_transferred);
Example 5: Encapsulating Asynchronous Serial Port Write Queue in a Class
program objective
A real robot's host computer often needs to continuously send commands to the lower computer, for example:
cmd_vel 0.2 0.0
cmd_vel 0.2 0.1
cmd_vel 0.0 0.0
When writing asynchronously, you cannot casually have multiple async_write() writing to the same serial port simultaneously. A more stable approach is to use a write queue:
如果当前没有写操作,就开始写
如果正在写,就先放入队列
当前写完后,再写下一条
Full code
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
Compile and run
Terminal 1: Create virtual serial port.
socat -d -d pty,raw,echo=0 pty,raw,echo=0
Terminal 2: Listen on the other end.
cat /dev/pts/4
Terminal 3: Run the program.
g++ demo5_serial_write_queue.cpp -o demo5_serial_write_queue -std=c++17 -lboost_system -pthread
./demo5_serial_write_queue /dev/pts/3
Execution output and chronological order
The program terminal outputs something like:
SerialWriter:串口打开成功
加入发送队列:cmd_vel 0.2 0.0
加入发送队列:cmd_vel 0.2 0.1
加入发送队列:cmd_vel 0.0 0.0
发送完成,字节数 = 16
发送完成,字节数 = 16
发送完成,字节数 = 16
main:发送队列清空,程序结束
Monitor terminal output:
cmd_vel 0.2 0.0
cmd_vel 0.2 0.1
cmd_vel 0.0 0.0
Points to note for this example
You cannot make msg a temporary local variable and then immediately perform an asynchronous write, because the asynchronous write may not have completed before the data is destroyed.
This example uses:
std::deque<std::string> write_queue_;
Save the pending data, ensuring that the string pointed to by buffer persists until the write is complete.
async_write() Description
boost::asio::async_write(serial_, buffer, handler);
Function: asynchronously write the entire buffer.
Callback parameter:
const boost::system::error_code& ec
std::size_t bytes_transferred
Return value: void.
Section Summary
- Synchronous serial port read/write is simple, but it will block.
- Asynchronous serial port read/write requires
io.run()driver. async_read_until()is suitable for reading text protocols that end with a newline.- Binary protocol is more suitable for
async_read_some()with its own frame parser. - Asynchronous writes must be mindful of the buffer lifecycle.
- In engineering, it is recommended to encapsulate serial port read/write into a class and manage sending data with a queue.