概述:自动化检测物体体积
(一)前置知识
Modbus 协议详解:概念、使用方法与实例应用
一、Modbus 是什么?
Modbus 是一种 串行通信协议,由 Modicon 公司于 1979 年开发,用于工业自动化系统中设备间的通信。它采用 主从架构(Master-Slave),支持多种物理层接口(如 RS-232、RS-485、以太网)。
核心特点:
- 简单性:协议结构简单,易于实现。
- 开放性:公开协议规范,无专利限制。
- 广泛支持:超过 80% 的工业设备支持 Modbus。
- 灵活性:支持多种传输模式(RTU、ASCII、TCP)。
二、Modbus 的通信模式
Modbus 主要有三种传输模式:
-
Modbus RTU(常用):
- 二进制编码,数据紧凑,适用于串口通信(RS-485/RS-232)。
- 典型应用:工厂传感器、PLC 控制。
- 示例:用户代码中使用
COM5
连接的电机和光栅设备。
-
Modbus ASCII:
- 数据以 ASCII 字符传输,可读性高,效率低于 RTU。
- 适用于调试场景。
-
Modbus TCP:
- 基于以太网传输,协议扩展了 TCP/IP 封装。
- 适用于工业物联网和远程监控。
三、Modbus 协议的数据模型
Modbus 通过 寄存器 定义设备的数据存储单元,分为四种类型:
寄存器类型 | 地址范围 | 访问权限 | 功能码 |
---|---|---|---|
线圈(Coils) | 0x0000-0xFFFF | 读写 | 0x01, 0x05, 0x0F |
离散输入(Input Status) | 0x0000-0xFFFF | 只读 | 0x02 |
保持寄存器(Holding Registers) | 0x0000-0xFFFF | 读写 | 0x03, 0x06, 0x10 |
输入寄存器(Input Registers) | 0x0000-0xFFFF | 只读 | 0x04 |
示例:
- 用户代码中光栅传感器使用 保持寄存器(地址
0x009F
读取遮挡光束数)。 - 电机控制通过写 保持寄存器(地址
0x02
启动电机)。
四、Modbus 通信流程:主从架构
- 主站(Master):发起请求的设备(如用户的工控机)。
- 从站(Slave):响应请求的设备(如光栅传感器、电机控制器)。
请求-响应流程:
- 主站发送一个包含 从站地址、功能码、数据 的请求帧。
- 从站校验地址匹配后执行操作,返回响应帧。
- 主站解析响应帧,确认操作结果。
五、Modbus RTU 的数据帧格式
一个典型的 Modbus RTU 帧结构如下:
字段 | 长度 | 说明 |
---|---|---|
地址 | 1 字节 | 从站地址(1-247) |
功能码 | 1 字节 | 指定操作类型(如读/写寄存器) |
数据 | 可变长度 | 寄存器地址、数量或写入值 |
CRC 校验 | 2 字节 | 循环冗余校验码 |
示例:读取保持寄存器(功能码 0x03
)
- 主站请求帧:
[从站地址] [0x03] [起始地址高字节] [起始地址低字节] [寄存器数量高字节] [寄存器数量低字节] [CRC]
- 从站响应帧:
[从站地址] [0x03] [字节数] [数据1高字节] [数据1低字节] ... [CRC]
六、如何使用 Modbus?
以下步骤结合用户项目代码说明如何实现 Modbus 通信:
1. 硬件连接
- RS-485/RS-232 接线:确保设备与主站正确连接,使用终端电阻(RS-485)。
- 用户代码中的配置:
// 创建 Modbus RTU 上下文(COM5,9600 波特率) modbus_t* ctx = modbus_new_rtu("COM5", 9600, 'N', 8, 1);
2. 初始化通信参数
- 波特率:与从站设备一致(如 9600、19200)。
- 校验位:无校验(
'N'
)、奇校验('O'
)、偶校验('E'
)。 - 超时设置:避免主站阻塞。
modbus_set_response_timeout(ctx, 1, 0); // 1秒超时
3. 读写寄存器
- 读取光栅传感器数据:
// 设置从站地址为1(光栅X方向) modbus_set_slave(ctx, 1); uint16_t blocked_beam_count; modbus_read_registers(ctx, 0x009F, 1, &blocked_beam_count);
- 控制电机启停:
// 设置从站地址为3(电机控制器) modbus_set_slave(ctx, 3); modbus_write_register(ctx, 0x02, 1); // 启动电机
4. 错误处理
- 检查返回值:Modbus API 返回
-1
表示失败。 - 获取错误信息:
if (modbus_read_registers(ctx, addr, 1, &value) == -1) { qDebug() << "错误:" << modbus_strerror(errno); }
5. 多线程同步
- 互斥锁保护:避免多个线程同时操作同一 Modbus 上下文。
std::lock_guard<std::mutex> lock(mutex); modbus_read_registers(ctx, addr, 1, &value);
七、用户代码中的实际应用
1. 光栅传感器数据采集
- 从站地址切换:
SlaveDevice->SetSlave(2); _SpectralDataFecherY->GetBlockedBeamCount();// Y方向光栅
- 寄存器地址:
0x009F
:遮挡光束数。0x00A0
:起始端光束状态。
2. 电机控制
- 启动电机:向地址
0x02
写1
。 - 方向设置:地址
0x01
写0
(逆时针)。
八、调试工具与最佳实践
常用工具:
- Modbus Poll:模拟主站,测试寄存器读写。
- Wireshark:抓包分析 Modbus TCP 通信。
- 串口调试助手:查看 RTU 数据帧。
最佳实践:
- 统一从站地址规划:避免地址冲突(如光栅X:1,光栅Y:2,电机:3)。
- 超时与重试机制:防止主站因无响应阻塞。
- 数据校验:使用 CRC 确保传输完整性。
- 日志记录:记录关键操作和错误信息。
九、常见问题与解决
-
无响应:
- 检查物理连接(线缆、终端电阻)。
- 确认从站地址和波特率匹配。
-
CRC 校验错误:
- 检查数据帧格式是否符合设备要求。
- 在代码中启用
modbus_set_debug(ctx, TRUE)
打印原始数据。
-
寄存器写失败:
- 确认寄存器是否可写(例如输入寄存器只读)。
- 检查权限(某些设备需解锁才能写入)。
十、总结
Modbus 是实现工业设备通信的基石。通过理解其协议结构、寄存器模型和错误处理机制,开发者可以高效集成传感器、控制器等设备。用户项目中的代码展示了如何通过 libmodbus 库 实现多从站管理、数据采集与设备控制,典型应用包括:
- 光栅遮挡检测
- 电机启停与方向控制
通过结合调试工具和最佳实践,可大幅提升 Modbus 通信的稳定性和可靠性。
一、libmodbus 概述
libmodbus 是一个用 C 语言编写的开源库,支持 Modbus RTU(串行通信)和 Modbus TCP(以太网通信)协议。
📦 核心功能:
- 提供简洁的 API,实现 Modbus 主站(Master)和从站(Slave)通信。
- 跨平台支持(Linux、Windows、macOS)。
- 支持同步请求/响应模型。
二、核心概念与数据结构
1. modbus_t
结构体
- 作用:表示一个 Modbus 通信上下文,保存串口或网络参数、超时设置等。
- 用户代码中的封装:
// 用户代码中的 ModbusSlave 类封装了 modbus_t class ModbusSlave { private: modbus_t* ctx; // 通过 GetCtx() 访问 };
2. 从站地址(Slave Address)
- 作用:Modbus 网络中每个设备有唯一地址(1-247)。主站通过该地址选择通信目标。
- 用户代码示例:
// 切换至电机从站(地址3) SlaveDevice->SetSlave(3); Motor->Start();
三、常用 API 详解
1. 创建上下文
- RTU 模式(串口通信):
modbus_t* ctx = modbus_new_rtu("/dev/ttyS0", 9600, 'N', 8, 1);
- 参数:设备路径(如 COM5)、波特率、校验位、数据位、停止位。
- TCP 模式:
modbus_t* ctx = modbus_new_tcp("192.168.1.100", 502);
2. 连接管理
int modbus_connect(modbus_t* ctx); // 建立连接
void modbus_close(modbus_t* ctx); // 关闭连接
3. 设置从站地址
int modbus_set_slave(modbus_t* ctx, int slave);
- 用户代码示例:
// 在 SpectralDataFecher 中切换光栅设备地址 int SpectralDataFecher::SetSlave(int slave) { return modbus_set_slave(SlaveDevice->GetCtx(), slave); }
4. **读写寄存器
- 读取保持寄存器:
int modbus_read_registers(modbus_t* ctx, int addr, int nb, uint16_t* dest);
addr
:寄存器起始地址。nb
:读取数量。dest
:存储结果的数组。
- 写入单个寄存器:
int modbus_write_register(modbus_t* ctx, int addr, uint16_t value);
5. **错误处理
- 获取错误信息:
const char* modbus_strerror(int errnum);
- 用户代码中的错误处理示例:
if (SlaveDevice->ReadRegisters(addr, 1, &value) == -1) { qDebug() << "错误:" << modbus_strerror(errno); return -1; }
四、用户代码中的关键实现
1. Modbus 从站管理(ModbusSlave 类)
- 职责:封装
modbus_t
上下文,提供连接、读写接口。 - 代码亮点:
// modbus-slave.cpp bool ModbusSlave::Connect(const char* device, int baud, ...) { ctx = modbus_new_rtu(device, baud, parity, data_bit, stop_bit); if (modbus_connect(ctx) == -1) { modbus_free(ctx); return false; // 连接失败释放资源 } return true; }
2. 光栅数据读取(SpectralDataFecher 类)
- 功能:解析光栅传感器数据(遮挡光束数、两端状态)。
- 关键逻辑:
uint16_t SpectralDataFecher::GetBlockedBeamCount() { uint16_t value; if (GetRegisterValue(FUNCTION::BLOCKED_BEAM_COUNT, value) == -1) return 0; return value; }
3. 电机控制(MotorCtrl 类)
- 功能:通过写寄存器控制电机启停、方向、归位。
- 代码逻辑:
void MotorCtrl::Start() { while (true) { if (mbs->WriteRegister(0x02, 1) == 1) break; // 持续重试直至成功 std::this_thread::sleep_for(100ms); } }
五、线程安全与性能优化
1. 线程安全
- 问题:libmodbus 非线程安全,多线程操作同一上下文需同步。
- 用户解决方案:
// EdgeDetection 中使用 std::mutex 保护 Modbus 操作 std::lock_guard<std::mutex> lock(mutex); SlaveDevice->SetSlave(3); Motor->Start();
2. 通信延迟优化
- 代码中的延迟:
std::this_thread::sleep_for(50ms); // 防止频繁请求
- 原因:避免 Modbus 设备响应不过来或缓冲区溢出。
六、常见问题与调试
1. 连接失败
- 可能原因:错误的串口参数、物理连接问题。
- 用户代码处理:
CPP if (SlaveDevice->Connect() == -1) { qDebug() << "连接异常,请检查设备!"; exit(1); }
2. 寄存器读写错误
- 调试方法:使用
modbus_strerror(errno)
输出错误信息。if (modbus_read_registers(ctx, addr, 1, &value) == -1) { qDebug() << "读取失败:" << modbus_strerror(errno); }
七、总结
libmodbus 在用户项目中的角色是实现与 Modbus 从站设备(光栅传感器、电机控制器)的通信。通过封装 modbus_t
上下文、切换从站地址、读写寄存器数据,完成了以下功能:
- 光栅检测:实时读取光束遮挡状态,计算物体尺寸。
- 电机控制:根据检测结果启停电机,实现自动化测量。
- 错误恢复:通过重试机制提高通信可靠性。
最佳实践建议:
- 增加日志记录,便于追踪通信问题。
- 使用异步通信模型(如
modbus_set_response_timeout
)优化性能。 - 定期检测连接状态,实现自动重连。
(二)项目流程速览
光栅项目运行流程说明
-
用户启动检测
- 操作:点击界面上的 Start 按钮。
- 触发:
EdgeDetection
类的start()
方法设置isStart = true
,通知电机控制线程开始检测条件。
-
光栅传感器监测物体状态
- X/Y方向检测:
- 线程:
GratingSensorThread
每秒约执行100次检测(通过休眠10ms控制频率)。 - 步骤:
- 切换Modbus从站地址至光栅设备(X: 地址1,Y: 地址2)。
- 检查光栅两端是否未被遮挡(
properPlaceX/Y
),确保物体放置正确。 - 统计中间被遮挡的光束数量(
BlockedBeamX/Y
),计算实际尺寸(如2.4857mm/beam
)。
- 条件判断:若检测到物体(
hasLightBlockingX/Y = true
)且位置正确,标记为可启动电机。
- 线程:
- X/Y方向检测:
-
电机启动与旋转
- 条件满足:当
hasLightBlockingX
、hasLightBlockingY
、properPlaceX
、properPlaceY
均为true
时:- 锁保护:获取互斥锁,防止Modbus通信冲突。
- 发送指令:切换至电机从站(地址3),发送启动命令(写寄存器
0x02
为1)。 - 持续旋转:电机按预设方向(逆时针)转动,直到物体移出。
- 条件满足:当
-
停止电机与复位
- 触发条件:光栅检测到物体完全移出(
hasLightBlockingX/Y = false
)。 - 动作:
- 停止电机:发送停止命令(写寄存器
0x03
为1)。 - 返回原点:设置原点寄存器(
0x0A
),电机反向旋转至初始位置。 - 重置标志:
isStart = false
,结束本次检测周期。
- 停止电机:发送停止命令(写寄存器
- 触发条件:光栅检测到物体完全移出(
-
用户获取角度数据
- 操作:点击 Get 按钮。
- 执行:读取电机角度寄存器(
0x16
),返回原始计数值(后续可能需转换为实际角度)。
流程示意图
PLAINTEXT
用户操作
│
▼
[Start按钮] → EdgeDetection.start() → isStart = true
│
▼
光栅检测线程(GratingSensorThread)
├─ X方向检测(从站1)
│ ├─ 是否遮挡两端? → 更新 properPlaceX
│ └─ 统计遮挡光束数 → BlockedBeamX
│
└─ Y方向检测(从站2)
├─ 是否遮挡两端? → 更新 properPlaceY
└─ 统计遮挡光束数 → BlockedBeamY
│
▼
电机控制线程(MotorThread)
├─ 条件满足?
│ ├─ 是 → 启动电机(从站3)
│ └─ 否 → 等待...
│
▼
物体移出 → 停止电机 → 回原点 → isStart = false
│
▼
[Get按钮] → 读取电机角度 → 显示/日志
关键设计点
-
多线程协作
- 光栅检测线程:高频读取传感器数据,确保实时性。
- 电机控制线程:独立判断启动条件,避免阻塞界面响应。
- 互斥锁(
mutex
):确保Modbus通信的原子性,防止多线程同时操作串口。
-
Modbus多从站管理
- 地址切换:通过
SetSlave()
动态切换设备(光栅X:1,光栅Y:2,电机:3)。 - 错误处理:通信失败时重试(如电机启动命令需循环发送直至成功)。
- 地址切换:通过
-
状态机逻辑
- 启动条件:需同时满足物体存在(遮挡光束)且位置正确(两端无遮挡)。
- 停止条件:物体完全离开光栅区域,触发复位流程。
典型应用场景
-
物体尺寸测量
- 将物体放置在光栅区域,系统自动计算X/Y方向的遮挡长度(如
BlockedBeamX * 2.4857mm
)。
- 将物体放置在光栅区域,系统自动计算X/Y方向的遮挡长度(如
-
自动化控制
- 在生产线中,当物体到达检测位置时启动电机(如传送带),移出后停止并复位,实现循环作业。
(三)具体实现细节
一、主界面模块(MainWidget)
实现文件:main-widget.h
/main-widget.cpp
1. 核心功能
- 提供用户操作界面,包含两个按钮:
- Start按钮:触发边缘检测流程。
- Get按钮:获取电机旋转角度。
2. 关键代码分析
// main-widget.cpp 中按钮点击事件
void MainWidget::on_pushButton_clicked()
{
edgeDetect.start(); // 启动检测流程
}
void MainWidget::on_pushButton_2_clicked()
{
qDebug() << "角度:" << edgeDetect.GetMotorAngle(); // 获取电机角度
}
edgeDetect
对象:EdgeDetection
类实例,负责核心逻辑。start()
方法:设置标志位,通知电机线程开始检测。
二、边缘检测核心模块(EdgeDetection)
实现文件:edgedetection.h
/edgedetection.cpp
1. 初始化阶段
EdgeDetection::EdgeDetection()
{
// 初始化Modbus从站(COM5, 9600波特率)
SlaveDevice = new ModbusSlave("COM5", 9600, 'N', 8, 1);
if (SlaveDevice->Connect() == -1) exit(1); // 连接失败终止程序
// 初始化光栅数据获取器(X/Y方向)
_SpectralDataFecherX = new SpectralDataFecher(SlaveDevice);
_SpectralDataFecherY = new SpectralDataFecher(SlaveDevice);
// 初始化电机控制器(从站地址3)
SlaveDevice->SetSlave(3);
Motor = new MotorCtrl(SlaveDevice);
Motor->SetValue(MotorCtrl::ANGLE, 0); // 持续旋转
Motor->SetValue(MotorCtrl::DIRECTION, 0); // 逆时针
// 启动光栅检测线程和电机控制线程
GratingSensorThread();
MotorThread();
}
- Modbus多从站管理:通过切换从站地址(1/2/3)区分光栅X/Y和电机设备。
- 电机初始配置:设置为逆时针无限旋转,等待触发条件。
2. 光栅检测线程(GratingSensorThread)
void EdgeDetection::GratingSensorThread()
{
SpectralThread = std::thread([this] {
while (1) {
std::lock_guard<std::mutex> lock(mutex);
// 读取Y方向光栅(从站2)
SlaveDevice->SetSlave(2);
properPlaceY = !_SpectralDataFecherY->GetEndBeamState()
&& !_SpectralDataFecherY->GetBeginBeamState();
_DeviceData.BlockedBeamY = _SpectralDataFecherY->GetBlockedBeamCount();
// 读取X方向光栅(从站1)
_SpectralDataFecherX->SetSlave(1);
properPlaceX = !_SpectralDataFecherX->GetEndBeamState()
&& !_SpectralDataFecherX->GetBeginBeamState();
_DeviceData.BlockedBeamX = _SpectralDataFecherX->GetBlockedBeamCount();
std::this_thread::sleep_for(10ms); // 降低CPU占用
}
});
}
- 双光栅检测:
- properPlaceX/Y:检查物体是否未遮挡光栅两端(正确放置)。
- BlockedBeamX/Y:计算被遮挡的光束数,通过
SCALE_PER_CELL
转换为实际尺寸。
- 线程安全:使用
std::mutex
确保Modbus通信的独占访问。
3. 电机控制线程(MotorThread)
void EdgeDetection::MotorThread() {
_MotorThread = std::thread([this]() {
while (true) {
if (isStart) {
// 条件满足:物体存在且放置正确
if (hasLightBlockingX && hasLightBlockingY && properPlaceX && properPlaceY)
{
std::lock_guard<std::mutex> lock(mutex);
SlaveDevice->SetSlave(3); // 切换至电机从站
if (!Motor->IsRuning())
{
Motor->Start(); // 启动电机
}
}
// 物体移出:停止电机并返原点
if (!hasLightBlockingX && !hasLightBlockingY) {
SlaveDevice->SetSlave(3);
Motor->Stop();
Motor->SetValue(MotorCtrl::ORIGIN, 0); // 返回原点
isStart = false; // 重置启动标志
}
}
}
});
}
- 条件触发:
- 启动电机:当X/Y光栅均检测到物体且位置正确。
- 停止电机:物体离开后停止,并触发返回原点操作。
- 原点复位:通过设置
ORIGIN
寄存器,电机反向旋转至初始位置。
三、Modbus通信模块(ModbusSlave)
实现文件:modbus-slave.h
/modbus-slave.cpp
1. 核心功能
- 封装
libmodbus
库,提供简化API。 - 支持多从站切换(通过
SetSlave()
)。
2. 关键实现
// modbus-slave.cpp 中连接初始化
bool ModbusSlave::Connect(const char *device, int baud, ...)
{
d->ctx = modbus_new_rtu(device, baud, parity, data_bit, stop_bit);
if (modbus_connect(d->ctx) == -1)
{
modbus_free(d->ctx);
return false; // 连接失败处理
}
return true;
}
- 串口参数:配置波特率、校验位、数据位等,匹配硬件设备设置。
- 错误处理:连接失败时释放资源,避免内存泄漏。
四、光栅数据处理模块(SpectralDataFecher)
实现文件:spectral-data-fecher.h
/spectral-data-fecher.cpp
1. 数据读取逻辑
uint16_t SpectralDataFecher::GetBlockedBeamCount() { uint16_t value; if (GetRegisterValue(FUNCTION::BLOCKED_BEAM_COUNT, value) == -1) return 0; return value; // 直接返回遮挡光束数 } int SpectralDataFecher::GetEndBeamState() { uint16_t value; GetRegisterValue(FUNCTION::END_BEAM_STATE, value); return (value & 0x8000) ? 1 : 0; // 取最高位判断末尾光束状态 }
- 寄存器地址:
0x009F
:遮挡光束数量。0x00A3
:末端光束状态(二进制最高位)。
五、电机控制模块(MotorCtrl)
实现文件:motor-ctrl.h
/motor-ctrl.cpp
1. 电机命令发送
void MotorCtrl::Start()
{
while (true)
{
if (mbs->WriteRegister(0x02, 1) == 1)
{ // 写入启动寄存器
isRuning = true;
break;
}
std::this_thread::sleep_for(100ms); // 失败后重试
}
}
- 寄存器映射:
0x02
:启动/停止(1启动,0停止)。0x01
:方向(0逆时针,1顺时针)。0x0A
:返回原点命令。
2. 角度计算
uint16_t MotorCtrl::GetAngle()
{
uint16_t angle;
mbs->ReadRegisters(0x16, 1, &angle); // 读取角度寄存器
return angle;
}
- 值转换:代码中
angle / 440.740
可能是将原始计数值转换为实际角度(需根据电机参数验证)。
六、模块交互流程
七、潜在优化点
- 线程管理:使用
QThread
替代std::thread
,便于与Qt事件循环集成。 - 错误恢复:增加Modbus通信失败后的自动重连机制。
- 性能优化:调整光栅检测线程的休眠时间,平衡响应速度与CPU占用。