43ed5714d7 · 2026-06-09 12:42:15 +08:00
1 Commit
2026-06-09 12:42:15 +08:00
2026-06-09 12:42:15 +08:00
2026-06-09 12:42:15 +08:00
2026-06-09 12:42:15 +08:00
2026-06-09 12:42:15 +08:00
2026-06-09 12:42:15 +08:00
2026-06-09 12:42:15 +08:00
2026-06-09 12:42:15 +08:00

线激光 Modbus RTU C++ 实现

本目录是 ../py/docs/proto.md 协议的 C++17 实现,覆盖上位机主站、下位机从站、公共 Modbus RTU 协议层、示例程序和测试。

实现目标:

  • 上位机负责构造读写请求、解析从站响应。
  • 下位机负责维护保持寄存器表、校验请求、执行合法写入、返回响应。
  • 公共协议层统一处理 CRC16、大小端、帧编解码和 TS+XYZABC 位姿寄存器转换。
  • 不引入第三方库,减少资源占用,方便后续迁移到性能较弱的驱动板。

目录结构

  • include/line_laser_modbus/protocol.hpp:公共协议常量、CRC、帧构造/解析、TS+XYZABC 转换。
  • include/line_laser_modbus/host.hpp:上位机/主站侧封装。
  • include/line_laser_modbus/device.hpp:下位机/从站侧封装和内存保持寄存器表。
  • src/*.cpp:协议、上位机、下位机实现。
  • apps/host_demo.cpp:上位机构造 RTU 帧示例。
  • apps/device_demo.cpp:下位机处理主站请求示例。
  • tests/*.cpp:协议、上位机、下位机测试;每个测试文件顶部都写明测试内容。

整体模块图

flowchart LR
    HostApp["fa:fa-desktop 上位机业务代码"] --> HostClient["HostClient<br/>主站封装"]
    HostClient --> Protocol["protocol.hpp / protocol.cpp<br/>CRC16 / 帧编解码 / 位姿转换"]
    Protocol --> Transport["fa:fa-exchange 传输层<br/>串口 / RS-485 / TCP 桥接"]
    Transport --> DeviceServer["DeviceServer<br/>从站帧处理器"]
    DeviceServer --> RegisterBank["RegisterBank<br/>保持寄存器表"]
    RegisterBank --> Firmware["fa:fa-microchip 下位机固件<br/>运动控制逻辑"]

    Tests["fa:fa-check 测试程序"] --> Protocol
    Tests --> HostClient
    Tests --> DeviceServer

说明:

  • HostClientDeviceServer 都不绑定具体串口库,方便 PC、驱动板、仿真测试共用同一套协议代码。
  • protocol 是唯一的帧格式和数据格式入口,避免上下位机分别实现导致字节序或 CRC 不一致。
  • RegisterBank 使用紧凑数组保存 0xD000 ~ 0xD06B,比 map 更适合资源较弱的板卡。

主从通信时序

下面的时序图解释一次典型的“读取当前位姿”和“写入纠偏量”流程。

sequenceDiagram
    participant App as 上位机业务
    participant Host as HostClient
    participant Bus as 串口_RS485
    participant Dev as DeviceServer
    participant Bank as RegisterBank
    participant Ctrl as 运动控制逻辑

    App->>Host: make_read_current_pose_request()
    Host->>Host: 构造 0x03 请求帧并追加 CRC16
    Host->>Bus: 发送 RTU 请求
    Bus->>Dev: 收到完整帧
    Dev->>Dev: 校验从站地址 / 功能码 / CRC
    Dev->>Bank: read(0xD00A, 14)
    Bank-->>Dev: TS + X/Y/Z/A/B/C 寄存器
    Dev->>Dev: 构造 0x03 响应帧并追加 CRC16
    Dev-->>Bus: 返回 RTU 响应
    Bus-->>Host: 接收响应帧
    Host->>Host: parse_current_pose_response()
    Host-->>App: Pose6D

    App->>Host: make_write_correction_request(Pose6D)
    Host->>Host: TS+XYZABC 转 14 个寄存器并构造 0x10 请求帧
    Host->>Bus: 发送纠偏量写入帧
    Bus->>Dev: 收到完整帧
    Dev->>Dev: 校验帧并检查地址范围
    Dev->>Bank: write(0xD036, 14 registers)
    Bank-->>Ctrl: 固件可读取最新纠偏量
    Dev-->>Host: 0x10 写确认响应
    Host-->>App: parse_write_ack() 成功

下位机帧处理流程

flowchart TD
    Start([收到一帧 RTU 数据]) --> Size{长度是否足够?}
    Size -- 否 --> Ignore1([忽略])
    Size -- 是 --> Slave{从站地址是否匹配?}
    Slave -- 否 --> Ignore2([忽略])
    Slave -- 是 --> Crc{CRC 是否正确?}
    Crc -- 否 --> Ignore3([忽略])
    Crc -- 是 --> Func{功能码}

    Func -- "0x03 读保持寄存器" --> ParseRead[解析 ReadRequest]
    ParseRead --> AddrRead{地址范围有效?}
    AddrRead -- 否 --> ExRead[返回异常响应 0x83]
    AddrRead -- 是 --> ReadBank[从 RegisterBank 读取寄存器]
    ReadBank --> ReadResp[构造读响应帧]

    Func -- "0x10 写多个保持寄存器" --> ParseWrite[解析 WriteRequest]
    ParseWrite --> AddrWrite{地址范围有效?}
    AddrWrite -- 否 --> ExWriteAddr[返回异常响应 0x90]
    AddrWrite -- 是 --> ModeCheck{是否写模式字?}
    ModeCheck -- 否 --> WriteBank[写入 RegisterBank]
    ModeCheck -- 是 --> ValidMode{模式值和切换是否合法?}
    ValidMode -- 否 --> ExWriteValue[返回非法值异常]
    ValidMode -- 是 --> WriteBank
    WriteBank --> WriteResp[构造写确认帧]

    Func -- 其他 --> ExFunc[返回非法功能码异常]

类关系

classDiagram
    class HostClient {
        -uint8_t slave_id_
        +make_read_status_request()
        +make_read_current_pose_request()
        +make_write_mode_request(mode)
        +make_write_target_pose_request(pose)
        +make_write_correction_request(pose)
        +parse_status_response(frame)
        +parse_current_pose_response(frame)
        +parse_write_ack(frame, address, quantity)
    }

    class DeviceServer {
        -uint8_t slave_id_
        -RegisterBank bank_
        +process_request(request)
        +bank()
    }

    class RegisterBank {
        -array<uint16_t> registers_
        +read(address, quantity)
        +write(address, registers)
        +set_mode(mode)
        +set_state(state)
        +set_current_pose(pose)
        +target_pose()
        +correction()
    }

    class Protocol {
        +crc16(data)
        +append_crc(frame)
        +build_read_request(...)
        +build_write_request(...)
        +parse_read_response(...)
        +parse_write_request(...)
        +encode_pose_registers(pose)
        +decode_pose_registers(registers)
    }

    HostClient ..> Protocol : 构造/解析帧
    DeviceServer ..> Protocol : 校验/构造响应
    DeviceServer *-- RegisterBank : 持有
    RegisterBank ..> Protocol : 位姿寄存器转换

寄存器和位姿数据布局

三类 6 轴数据块都使用同一套布局:uint32 timestamp_ms + 6 个 float,共 14 个保持寄存器。

flowchart LR
    subgraph PoseBlock["TS + XYZABC 数据块,共 14 个寄存器"]
        TS["0~1<br/>uint32 时间戳 ms"]
        X["2~3<br/>float X mm"]
        Y["4~5<br/>float Y mm"]
        Z["6~7<br/>float Z mm"]
        A["8~9<br/>float A deg"]
        B["10~11<br/>float B deg"]
        C["12~13<br/>float C deg"]
    end

    Current["当前位姿<br/>0xD00A ~ 0xD017"] --> PoseBlock
    Target["目标示教位姿<br/>0xD020 ~ 0xD02D"] --> PoseBlock
    Correction["6 轴纠偏量<br/>0xD036 ~ 0xD043"] --> PoseBlock

寄存器表核心区域:

flowchart TD
    R0["0xD000<br/>模式命令字"] --> R1["0xD001<br/>设备状态字"]
    R1 --> R2["0xD002 ~ 0xD009<br/>预留"]
    R2 --> R3["0xD00A ~ 0xD017<br/>当前 6 轴位姿"]
    R3 --> R4["0xD018 ~ 0xD01F<br/>预留"]
    R4 --> R5["0xD020 ~ 0xD02D<br/>目标示教位姿"]
    R5 --> R6["0xD02E ~ 0xD035<br/>预留"]
    R6 --> R7["0xD036 ~ 0xD043<br/>6 轴纠偏量"]
    R7 --> R8["0xD044 ~ 0xD04B<br/>预留"]
    R8 --> R9["0xD04C ~ 0xD06B<br/>标定参数预留"]

模式状态机

stateDiagram-v2
    [*] --> Standby
    Standby: 0 待机复位
    Calibration: 1 系统标定
    Teaching: 2 焊前扫描示教
    Tracking: 3 在线全轴跟踪
    Replay: 4 轨迹批量复现
    Emergency: 5 紧急停止

    Standby --> Calibration
    Standby --> Teaching
    Standby --> Tracking
    Standby --> Replay

    Calibration --> Standby: 标定完成
    Teaching --> Standby
    Teaching --> Tracking
    Teaching --> Replay
    Tracking --> Replay
    Replay --> Tracking

    Standby --> Emergency
    Calibration --> Emergency
    Teaching --> Emergency
    Tracking --> Emergency
    Replay --> Emergency

    Emergency --> Standby: 只允许退回待机

DeviceServer 写模式命令字时会检查:

  • 模式值必须在 0 ~ 5
  • 任意模式都可以切到 0 待机5 急停
  • 急停后只能切回 0 待机,不能直接进入运行模式。

测试覆盖图

flowchart LR
    ProtocolTests["protocol_tests.cpp<br/>CRC / 文档示例帧 / 位姿转换 / 解析器"] --> Protocol["协议层"]
    HostTests["host_tests.cpp<br/>上位机请求构造 / 响应解析 / ACK 校验"] --> Host["HostClient"]
    DeviceTests["device_tests.cpp<br/>寄存器读写 / 模式副作用 / 异常响应"] --> Device["DeviceServer + RegisterBank"]

    Protocol --> Shared["公共字节序和 CRC 规则"]
    Host --> Shared
    Device --> Shared

为什么这样实现

flowchart TB
    Requirement["需求<br/>高性能 / 低耗用 / 可迁移到驱动板"] --> NoThirdParty["不引入第三方库"]
    Requirement --> ExplicitData["显式 uint8_t / uint16_t / uint32_t 类型"]
    Requirement --> CompactBank["紧凑寄存器数组"]
    Requirement --> NoTransportBinding["协议层不绑定串口库"]

    NoThirdParty --> Result["减少依赖和移植成本"]
    ExplicitData --> Result
    CompactBank --> Result
    NoTransportBinding --> Result

核心取舍:

  • 公共协议代码只处理字节和寄存器,不负责串口收发,便于在 PC 和固件之间复用。
  • 使用 std::array 存下位机寄存器表,避免动态映射结构的额外开销。
  • 使用 std::optional 返回解析结果,避免异常依赖,适合固件侧错误处理。
  • CRC、大小端、float 位模式转换都集中在 protocol.cpp,降低协议错误风险。

构建和测试

cmake -S cpp -B cpp/build
cmake --build cpp/build
ctest --test-dir cpp/build --output-on-failure

Windows/MSVC 下,CMakeLists.txt 已启用 /utf-8,用于正确编译中文注释。

S
Description
Modbus RTU protocol tools for line laser vision and motion controller communication
Readme 201 KiB
Languages
Python 100%