282 lines
9.6 KiB
Markdown
282 lines
9.6 KiB
Markdown
# 线激光 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<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` 更适合资源较弱的板卡。
|
|
|
|
## 主从通信时序
|
|
|
|
下面的时序图解释一次典型的“读取当前位姿”和“写入纠偏量”流程。
|
|
|
|
```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<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 个保持寄存器。
|
|
|
|
```mermaid
|
|
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
|
|
```
|
|
|
|
寄存器表核心区域:
|
|
|
|
```mermaid
|
|
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/>标定参数预留"]
|
|
```
|
|
|
|
## 模式状态机
|
|
|
|
```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<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
|
|
```
|
|
|
|
## 为什么这样实现
|
|
|
|
```mermaid
|
|
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`,降低协议错误风险。
|
|
|
|
## 构建和测试
|
|
|
|
```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`,用于正确编译中文注释。
|