跳到正文

Wiki

序列埠通訊

約 6 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

序列埠是機器人裡最常見的通訊方式之一:上位機 ROS2 / Linux 程式通過 USB-TTL、USB-CAN 轉序列埠、CH340、CP2102、FT232 等裝置和 STM32、ESP32、下位機控制板通訊。

本節依舊遵循順序:

先写普通 main() 里的同步示例
再写普通 main() 里的异步示例
最后写类封装版本

所有非同步回撥統一使用 std::bind


1.Linux 下準備虛擬序列埠測試環境

如果你手裡沒有真實 STM32,可以用 socat 建立一對虛擬序列埠。

1.1.安裝 socat

Ubuntu / Debian:

sudo apt install socat

Fedora:

sudo dnf install socat

1.2.建立一對虛擬序列埠

新開一個終端,執行:

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。你要按自己終端實際輸出修改。


2.示例 1:普通 main() 裡同步開啟序列埠併發送一行

2.1.程式目標

程式開啟序列埠 /dev/pts/3,設定波特率 115200,然後傳送:

hello serial

另一個終端從 /dev/pts/4 讀到這行資料。

2.2.完整程式碼

#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;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

2.3.編譯執行

終端 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

2.4.執行輸出與時間順序

程式終端輸出:

main:准备打开串口 /dev/pts/3
main:发送成功,字节数 = 13
main:程序结束

監聽終端輸出:

hello serial

這個示例基本沒有明顯延遲,傳送完成後程式立刻結束。

2.5.本示例需要注意的點

這是同步寫序列埠。boost::asio::write() 會阻塞,直到資料寫完或發生錯誤。

2.6.關鍵函式說明

2.6.1.serial.open(port_name, ec)

作用:開啟序列埠裝置。

引數:

port_name

例如 /dev/ttyUSB0/dev/ttyACM0/dev/pts/3

ec

用於接收錯誤資訊。這樣寫不會拋異常,更適合教程和機器人工程。

2.6.2.serial.set_option(...)

作用:設定序列埠引數,例如波特率、資料位、校驗位、停止位、流控。

2.6.3.boost::asio::write(serial, boost::asio::buffer(msg), ec)

作用:同步寫資料。

返回值:實際寫入的位元組數。


3.示例 2:普通 main() 裡同步讀取一行序列埠資料

3.1.程式目標

程式開啟序列埠並阻塞等待,直到收到 \n 為止。

3.2.完整程式碼

#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;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

3.3.編譯執行

終端 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

3.4.執行輸出與時間順序

程式啟動後立刻輸出:

main:等待串口收到一行数据,以换行符结束

然後程式阻塞。你在另一個終端執行:

echo "stm32 ok" > /dev/pts/4

程式繼續輸出:

main:读取到字节数 = 9
main:读取到一行 = stm32 ok

這裡的等待時間取決於你什麼時候傳送資料。

3.5.本示例需要注意的點

read_until() 是同步阻塞函式。沒有收到換行符之前,它會一直卡住。

如果你在 ROS2 回撥裡直接這樣讀序列埠,可能導致整個節點響應變慢。

3.6.關鍵函式說明

3.6.1.boost::asio::read_until(serial, buffer, '\n', ec)

作用:從序列埠讀取資料,直到遇到換行符 \n

返回值:至少讀取到分隔符為止的位元組數。

注意:streambuf 裡可能還留有額外資料,後續讀取時要注意處理。


4.示例 3:普通 main() 裡非同步讀取一行序列埠資料

4.1.程式目標

把示例 2 改成非同步版本:

  1. async_read_until() 註冊讀取任務;
  2. io.run() 開始等待;
  3. 另一端傳送一行資料;
  4. 回撥函式執行;
  5. io.run() 返回。

4.2.完整程式碼

#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;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

4.3.編譯執行

終端 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

4.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() 返回

4.5.本示例需要注意的點

async_read_until() 不阻塞。它只是註冊讀取任務。

如果你不呼叫:

io.run();

回撥函式永遠不會執行。

4.6.回撥引數說明

async_read_until() 的回撥一般接收兩個引數:

const boost::system::error_code& ec
std::size_t bytes_transferred

所以繫結時用:

std::placeholders::_1
std::placeholders::_2

我們額外傳了:

&buffer

用於在回撥裡取出具體字串。


5.示例 4:類裡封裝非同步序列埠行讀取

5.1.程式目標

把非同步序列埠讀取封裝成類。這個結構更接近真實機器人專案。

程式會連續讀取多行序列埠資料,每收到一行就列印,然後繼續註冊下一次讀取。

5.2.完整程式碼

#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;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

5.3.編譯執行

終端 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

5.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()

5.5.本示例需要注意的點

這一句是連續讀取的關鍵:

start_read();

它在 on_read() 的最後再次註冊下一次非同步讀取。

如果沒有這句,程式只會讀一行,然後 io.run() 就會返回。

5.6.類成員變數生命週期說明

boost::asio::serial_port serial_;
boost::asio::streambuf buffer_;

它們是類成員變數,生命週期和 SerialLineReader reader 一樣長。

這比在區域性函數里建立臨時 buffer 更安全。

5.7.成員函式繫結說明

std::bind(&SerialLineReader::on_read,
          this,
          std::placeholders::_1,
          std::placeholders::_2)

表示非同步讀取完成後呼叫:

this->on_read(ec, bytes_transferred);

6.示例 5:類裡封裝非同步序列埠寫佇列

6.1.程式目標

真實機器人上位機經常需要連續給下位機發命令,例如:

cmd_vel 0.2 0.0
cmd_vel 0.2 0.1
cmd_vel 0.0 0.0

非同步寫時不能隨便多個 async_write() 同時寫同一個序列埠。更穩的做法是寫佇列:

如果当前没有写操作,就开始写
如果正在写,就先放入队列
当前写完后,再写下一条

6.2.完整程式碼

#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;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

6.3.編譯執行

終端 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

6.4.執行輸出與時間順序

程式終端輸出類似:

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

6.5.本示例需要注意的點

這裡不能把 msg 做成臨時區域性變數後立刻非同步寫,因為非同步寫可能還沒完成,資料已經銷燬。

本例用:

std::deque<std::string> write_queue_;

儲存待發送資料,保證 buffer 指向的字串在寫完成之前一直存在。

6.6.async_write() 說明

boost::asio::async_write(serial_, buffer, handler);

作用:非同步寫完整個 buffer。

回撥引數:

const boost::system::error_code& ec
std::size_t bytes_transferred

返回值:void


7.本節總結

  1. 同步序列埠讀寫簡單,但會阻塞。
  2. 非同步序列埠讀寫需要 io.run() 驅動。
  3. async_read_until() 適合讀取以換行結尾的文本協議。
  4. 二進位制協議更適合 async_read_some() 加自己的幀解析器。
  5. 非同步寫必須注意 buffer 生命週期。
  6. 工程裡推薦把序列埠讀寫封裝成類,並用佇列管理傳送資料。