cpp
线激光 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
说明:
HostClient和DeviceServer都不绑定具体串口库,方便 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,用于正确编译中文注释。
Description
Languages
Python
100%