Luy62

在qt中使用ZH-44043d采集器

在 Qt 中实现 RS485 通讯(与 ZH-44043D-14NI 采集器交互),核心是利用 Qt 的QSerialPort模块实现串口数据收发,并整合之前的 Modbus CRC16 算法完成指令封装与响应解析。下面是完整的实现步骤、代码示例和注意事项。

一、前期准备

  1. Qt 环境配置(关键)
    Qt 中操作串口需要依赖SerialPort模块,必须在项目的.pro文件中添加以下配置,否则无法编译通过:
    QT += core gui serialport # 若为控制台项目,可去掉gui
    其他原有配置(如SOURCES、HEADERS等)保持不变

  2. 硬件准备
    RS485 硬件:USB 转 485 模块
    接线:模块D+对应采集器D+,模块D-对应采集器D-,确保供电与接地稳定
    串口参数:与采集器一致(115200 波特率、8 数据位、1 停止位、无校验、无流控)

二、核心实现步骤(Qt Widgets 示例)
步骤 1:包含必要头文件

点击查看代码

#include <QWidget>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QByteArray>
#include <QDebug>
#include <cstdint>

// 提前声明CRC相关辅助函数(复用之前的C++算法)
uint8_t reverseByte(uint8_t byte);
uint16_t reverseWord(uint16_t word);
uint16_t calculateModbusCRC16(const uint8_t* data, uint16_t length);
void splitCRCToBytes(uint16_t crc, uint8_t& lowByte, uint8_t& highByte);

步骤 2:定义类成员变量(串口对象 + CRC 辅助函数)
在窗口类(如MainWindow)中定义私有成员变量,用于管理串口和数据:

点击查看代码
class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    // 串口接收数据槽函数(响应readyRead()信号)
    void onSerialPortReadyRead();
    // 发送Modbus指令的槽函数(可绑定按钮点击)
    void sendRead3rdCurrentCmd();

private:
    QSerialPort *m_serialPort;  // 串口对象指针
};

// 实现CRC辅助函数(与之前的C++算法完全一致,适配Qt)
uint8_t reverseByte(uint8_t byte) {
    uint8_t reversed = 0;
    for (int i = 0; i < 8; ++i) {
        reversed |= ((byte >> i) & 0x01) << (7 - i);
    }
    return reversed;
}

uint16_t reverseWord(uint16_t word) {
    uint16_t reversed = 0;
    for (int i = 0; i < 16; ++i) {
        reversed |= ((word >> i) & 0x0001) << (15 - i);
    }
    return reversed;
}

uint16_t calculateModbusCRC16(const uint8_t* data, uint16_t length) {
    const uint16_t POLY = 0x8005;
    uint16_t crc = 0xFFFF;

    if (length == 0) return crc;

    for (uint16_t i = 0; i < length; ++i) {
        uint8_t reversedByte = reverseByte(data[i]);
        crc ^= (static_cast<uint16_t>(reversedByte) << 8);

        for (int bit = 0; bit < 8; ++bit) {
            if (crc & 0x8000) {
                crc = (crc << 1) ^ POLY;
            } else {
                crc <<= 1;
            }
        }
    }

    crc = reverseWord(crc);
    crc ^= 0x0000;
    return crc;
}

void splitCRCToBytes(uint16_t crc, uint8_t& lowByte, uint8_t& highByte) {
    lowByte = crc & 0xFF;
    highByte = (crc >> 8) & 0xFF;
}

步骤 3:初始化串口(构造函数中)
配置串口参数并打开串口,关联readyRead()信号(有数据接收时触发槽函数):

点击查看代码
MainWindow::MainWindow(QWidget *parent)
    : QWidget(parent)
{
    // 1. 初始化串口对象
    m_serialPort = new QSerialPort(this);

    // 2. 选择串口端口(需根据你的USB转485模块修改,如"COM3"、"/dev/ttyUSB0")
    QString portName = "COM3";  // Windows示例,Linux请改为"/dev/ttyUSB0"
    m_serialPort->setPortName(portName);

    // 3. 配置串口参数(匹配ZH-44043D-14NI)
    m_serialPort->setBaudRate(QSerialPort::Baud115200);  // 115200波特率
    m_serialPort->setDataBits(QSerialPort::Data8);       // 8数据位
    m_serialPort->setStopBits(QSerialPort::OneStop);     // 1停止位
    m_serialPort->setParity(QSerialPort::NoParity);      // 无校验
    m_serialPort->setFlowControl(QSerialPort::NoFlowControl);  // 无流控

    // 4. 打开串口(只读/只写/读写,这里选读写)
    if (m_serialPort->open(QSerialPort::ReadWrite)) {
        qDebug() << "串口打开成功:" << portName;
        // 5. 关联串口接收信号与槽函数
        connect(m_serialPort, &QSerialPort::readyRead, this, &MainWindow::onSerialPortReadyRead);
    } else {
        qDebug() << "串口打开失败:" << m_serialPort->errorString();
    }

    // 测试:发送读取第3路电流指令(可绑定按钮点击,这里直接调用演示)
    sendRead3rdCurrentCmd();
}

MainWindow::~MainWindow()
{
    // 关闭并释放串口
    if (m_serialPort->isOpen()) {
        m_serialPort->close();
    }
    delete m_serialPort;
}

步骤 4:封装并发送 Modbus 指令 实现发送读取第 3 路电流的指令,自动计算 CRC 并拼接完整帧:
点击查看代码
void MainWindow::sendRead3rdCurrentCmd()
{
    if (!m_serialPort->isOpen()) {
        qDebug() << "串口未打开,无法发送指令";
        return;
    }

    // 1. 准备指令前6字节(不含CRC):01 03 00 06 00 01
    uint8_t cmdPrefix[] = {0x01, 0x03, 0x00, 0x06, 0x00, 0x01};
    int prefixLen = sizeof(cmdPrefix);

    // 2. 计算Modbus CRC16
    uint16_t crcValue = calculateModbusCRC16(cmdPrefix, prefixLen);
    uint8_t crcLow, crcHigh;
    splitCRCToBytes(crcValue, crcLow, crcHigh);

    // 3. 拼接完整指令(前6字节+CRC低位+CRC高位)
    QByteArray sendData;
    for (int i = 0; i < prefixLen; ++i) {
        sendData.append(static_cast<char>(cmdPrefix[i]));
    }
    sendData.append(static_cast<char>(crcLow));   // CRC低位:0x64
    sendData.append(static_cast<char>(crcHigh));  // CRC高位:0x0B

    // 4. 发送指令到串口
    qint64 sendLen = m_serialPort->write(sendData);
    if (sendLen == -1) {
        qDebug() << "指令发送失败:" << m_serialPort->errorString();
    } else {
        qDebug() << "指令发送成功,发送内容(十六进制):" << sendData.toHex(' ').toUpper();
    }
}

步骤 5:接收并解析串口响应 实现onSerialPortReadyRead()槽函数,读取采集器返回的数据并解析:
点击查看代码
void MainWindow::onSerialPortReadyRead()
{
    // 1. 读取串口缓冲区所有数据
    QByteArray recvData = m_serialPort->readAll();
    if (recvData.isEmpty()) {
        return;
    }

    // 2. 打印接收结果(十六进制格式,方便调试)
    qDebug() << "收到响应(十六进制):" << recvData.toHex(' ').toUpper();

    // 3. 合法性校验(响应帧应为7字节:01 03 02 XX XX CRC1 CRC2)
    if (recvData.length() != 7) {
        qDebug() << "响应帧长度异常,预期7字节,实际" << recvData.length() << "字节";
        return;
    }

    // 4. 逐字节拆解响应(对应你的接收数据:01 03 02 00 00 B8 44)
    uint8_t devAddr = static_cast<uint8_t>(recvData.at(0));       // 第1字节:设备地址(01)
    uint8_t funcCode = static_cast<uint8_t>(recvData.at(1));      // 第2字节:功能码(03)
    uint8_t dataLen = static_cast<uint8_t>(recvData.at(2));       // 第3字节:数据区长度(02)
    uint8_t dataLow = static_cast<uint8_t>(recvData.at(3));       // 第4字节:数据低位(00)
    uint8_t dataHigh = static_cast<uint8_t>(recvData.at(4));      // 第5字节:数据高位(00)
    uint8_t recvCrcLow = static_cast<uint8_t>(recvData.at(5));    // 第6字节:响应CRC低位(B8)
    uint8_t recvCrcHigh = static_cast<uint8_t>(recvData.at(6));   // 第7字节:响应CRC高位(44)

    // 5. 验证功能码(无错误,错误功能码为0x83)
    if (funcCode != 0x03) {
        qDebug() << "采集器返回错误,功能码:" << QString::number(funcCode, 16).toUpper();
        return;
    }

    // 6. 验证数据区长度(预期2字节,对应1个16位寄存器)
    if (dataLen != 0x02) {
        qDebug() << "数据区长度异常,预期2字节,实际" << static_cast<int>(dataLen) << "字节";
        return;
    }

    // 7. 计算响应数据的CRC(验证传输是否无误)
    uint8_t crcCheckData[] = {devAddr, funcCode, dataLen, dataLow, dataHigh};
    uint16_t crcCheckValue = calculateModbusCRC16(crcCheckData, sizeof(crcCheckData));
    uint8_t checkCrcLow, checkCrcHigh;
    splitCRCToBytes(crcCheckValue, checkCrcLow, checkCrcHigh);

    if (checkCrcLow != recvCrcLow || checkCrcHigh != recvCrcHigh) {
        qDebug() << "响应CRC校验失败,预期:" << QString::number(checkCrcLow, 16).toUpper()
                 << " " << QString::number(checkCrcHigh, 16).toUpper();
        return;
    }

    // 8. 解析第3路电流值(核心计算)
    uint16_t currentData = (static_cast<uint16_t>(dataHigh) << 8) | dataLow;  // 拼接16位数据(00 00 → 0)
    float actualCurrent = static_cast<float>(currentData) / 10000.0f;         // 基础计算:DATA/10000

    // 补充:乘以你的采集器电流量程(如5A、10A,需根据实际配置修改)
    float currentRange = 5.0f;  // 示例:5A量程,根据你的设备修改
    actualCurrent *= currentRange;

    // 9. 输出最终结果
    qDebug() << "第3路电流解析结果:";
    qDebug() << "  寄存器原始值(十六进制):" << QString::number(currentData, 16).toUpper();
    qDebug() << "  实际电流值(A):" << actualCurrent;
}

三、关键注意事项

  1. 串口端口选择
    Windows:在设备管理器中查看 USB 转 485 模块对应的 COM 口(如 COM3、COM4)
    Linux:查看/dev/ttyUSB或/dev/ttyACM,需赋予串口权限(sudo chmod 777 /dev/ttyUSB0)
    可通过QSerialPortInfo::availablePorts()枚举所有可用串口,方便用户选择。
  2. RS485 硬件适配
    Qt 仅负责软件串口数据收发,RS485 的差分信号转换由硬件完成(USB 转 485 模块)
    多设备组网时,需在总线两端添加 120Ω 终端电阻,避免信号反射
    通讯线使用屏蔽双绞线,远离强电磁干扰源(如变频器、电机)。
  3. 数据粘包处理
    若串口接收数据不完整或粘包,可添加延时读取(QTimer::singleShot)或帧尾判断(基于 Modbus 帧长度规则)
    示例中假设单次接收完整 7 字节响应,实际项目中可缓存接收数据,待数据长度达标后再解析。
  4. CRC 算法兼容性
    示例中复用了之前的 C++ CRC 算法,完全适配 Qt 的QByteArray数据格式,无需修改核心逻辑
    若需优化性能,可将 CRC 算法改为 Qt 风格(使用QVector替代原生数组),但功能完全一致。

总结

在 Qt 中使用 RS485 与 ZH-44043D-14NI 通讯的核心要点:
配置.pro文件引入serialport模块,是串口操作的前提;
用QSerialPort配置与采集器一致的串口参数,关联readyRead()信号接收数据;
复用 Modbus CRC16 算法完成指令封装与响应校验,确保通讯可靠性;
按照 Modbus-RTU 帧结构解析响应数据,结合采集器的参数计算规则得到实际测量值。

posted on 2026-01-06 15:26  LUY62  阅读(50)  评论(0)    收藏  举报

导航