# 线激光 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`,用于正确编译中文注释。