# 线激光 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`:协议、上位机、下位机测试;每个测试文件顶部都写明测试内容。 ## 整体模块图 ```mermaid flowchart LR HostApp["fa:fa-desktop 上位机业务代码"] --> HostClient["HostClient
主站封装"] HostClient --> Protocol["protocol.hpp / protocol.cpp
CRC16 / 帧编解码 / 位姿转换"] Protocol --> Transport["fa:fa-exchange 传输层
串口 / RS-485 / TCP 桥接"] Transport --> DeviceServer["DeviceServer
从站帧处理器"] DeviceServer --> RegisterBank["RegisterBank
保持寄存器表"] RegisterBank --> Firmware["fa:fa-microchip 下位机固件
运动控制逻辑"] Tests["fa:fa-check 测试程序"] --> Protocol Tests --> HostClient Tests --> DeviceServer ``` 说明: - `HostClient` 和 `DeviceServer` 都不绑定具体串口库,方便 PC、驱动板、仿真测试共用同一套协议代码。 - `protocol` 是唯一的帧格式和数据格式入口,避免上下位机分别实现导致字节序或 CRC 不一致。 - `RegisterBank` 使用紧凑数组保存 `0xD000 ~ 0xD06B`,比 `map` 更适合资源较弱的板卡。 ## 主从通信时序 下面的时序图解释一次典型的“读取当前位姿”和“写入纠偏量”流程。 ```mermaid 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() 成功 ``` ## 下位机帧处理流程 ```mermaid 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[返回非法功能码异常] ``` ## 类关系 ```mermaid 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 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 个保持寄存器。 ```mermaid flowchart LR subgraph PoseBlock["TS + XYZABC 数据块,共 14 个寄存器"] TS["0~1
uint32 时间戳 ms"] X["2~3
float X mm"] Y["4~5
float Y mm"] Z["6~7
float Z mm"] A["8~9
float A deg"] B["10~11
float B deg"] C["12~13
float C deg"] end Current["当前位姿
0xD00A ~ 0xD017"] --> PoseBlock Target["目标示教位姿
0xD020 ~ 0xD02D"] --> PoseBlock Correction["6 轴纠偏量
0xD036 ~ 0xD043"] --> PoseBlock ``` 寄存器表核心区域: ```mermaid flowchart TD R0["0xD000
模式命令字"] --> R1["0xD001
设备状态字"] R1 --> R2["0xD002 ~ 0xD009
预留"] R2 --> R3["0xD00A ~ 0xD017
当前 6 轴位姿"] R3 --> R4["0xD018 ~ 0xD01F
预留"] R4 --> R5["0xD020 ~ 0xD02D
目标示教位姿"] R5 --> R6["0xD02E ~ 0xD035
预留"] R6 --> R7["0xD036 ~ 0xD043
6 轴纠偏量"] R7 --> R8["0xD044 ~ 0xD04B
预留"] R8 --> R9["0xD04C ~ 0xD06B
标定参数预留"] ``` ## 模式状态机 ```mermaid 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 待机`,不能直接进入运行模式。 ## 测试覆盖图 ```mermaid flowchart LR ProtocolTests["protocol_tests.cpp
CRC / 文档示例帧 / 位姿转换 / 解析器"] --> Protocol["协议层"] HostTests["host_tests.cpp
上位机请求构造 / 响应解析 / ACK 校验"] --> Host["HostClient"] DeviceTests["device_tests.cpp
寄存器读写 / 模式副作用 / 异常响应"] --> Device["DeviceServer + RegisterBank"] Protocol --> Shared["公共字节序和 CRC 规则"] Host --> Shared Device --> Shared ``` ## 为什么这样实现 ```mermaid flowchart TB Requirement["需求
高性能 / 低耗用 / 可迁移到驱动板"] --> 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`,降低协议错误风险。 ## 构建和测试 ```bash cmake -S cpp -B cpp/build cmake --build cpp/build ctest --test-dir cpp/build --output-on-failure ``` Windows/MSVC 下,`CMakeLists.txt` 已启用 `/utf-8`,用于正确编译中文注释。