计算Modbus RTU CRC 16

14

我正在实现一款软件,通过串口使用Modbus RTU协议读写数据。为此,我需要计算字节字符串末尾的两个CRC校验码,但是我无法完成这个任务。

在网上搜索时,我找到了似乎能够正确计算CRC校验码的两个函数:

WORD CRC16 (const BYTE *nData, WORD wLength)
{
    static const WORD wCRCTable[] = {
       0X0000, 0XC0C1, 0XC181, 0X0140, 0XC301, 0X03C0, 0X0280, 0XC241,
       0XC601, 0X06C0, 0X0780, 0XC741, 0X0500, 0XC5C1, 0XC481, 0X0440,
       0XCC01, 0X0CC0, 0X0D80, 0XCD41, 0X0F00, 0XCFC1, 0XCE81, 0X0E40,
       0X0A00, 0XCAC1, 0XCB81, 0X0B40, 0XC901, 0X09C0, 0X0880, 0XC841,
       0XD801, 0X18C0, 0X1980, 0XD941, 0X1B00, 0XDBC1, 0XDA81, 0X1A40,
       0X1E00, 0XDEC1, 0XDF81, 0X1F40, 0XDD01, 0X1DC0, 0X1C80, 0XDC41,
       0X1400, 0XD4C1, 0XD581, 0X1540, 0XD701, 0X17C0, 0X1680, 0XD641,
       0XD201, 0X12C0, 0X1380, 0XD341, 0X1100, 0XD1C1, 0XD081, 0X1040,
       0XF001, 0X30C0, 0X3180, 0XF141, 0X3300, 0XF3C1, 0XF281, 0X3240,
       0X3600, 0XF6C1, 0XF781, 0X3740, 0XF501, 0X35C0, 0X3480, 0XF441,
       0X3C00, 0XFCC1, 0XFD81, 0X3D40, 0XFF01, 0X3FC0, 0X3E80, 0XFE41,
       0XFA01, 0X3AC0, 0X3B80, 0XFB41, 0X3900, 0XF9C1, 0XF881, 0X3840,
       0X2800, 0XE8C1, 0XE981, 0X2940, 0XEB01, 0X2BC0, 0X2A80, 0XEA41,
       0XEE01, 0X2EC0, 0X2F80, 0XEF41, 0X2D00, 0XEDC1, 0XEC81, 0X2C40,
       0XE401, 0X24C0, 0X2580, 0XE541, 0X2700, 0XE7C1, 0XE681, 0X2640,
       0X2200, 0XE2C1, 0XE381, 0X2340, 0XE101, 0X21C0, 0X2080, 0XE041,
       0XA001, 0X60C0, 0X6180, 0XA141, 0X6300, 0XA3C1, 0XA281, 0X6240,
       0X6600, 0XA6C1, 0XA781, 0X6740, 0XA501, 0X65C0, 0X6480, 0XA441,
       0X6C00, 0XACC1, 0XAD81, 0X6D40, 0XAF01, 0X6FC0, 0X6E80, 0XAE41,
       0XAA01, 0X6AC0, 0X6B80, 0XAB41, 0X6900, 0XA9C1, 0XA881, 0X6840,
       0X7800, 0XB8C1, 0XB981, 0X7940, 0XBB01, 0X7BC0, 0X7A80, 0XBA41,
       0XBE01, 0X7EC0, 0X7F80, 0XBF41, 0X7D00, 0XBDC1, 0XBC81, 0X7C40,
       0XB401, 0X74C0, 0X7580, 0XB541, 0X7700, 0XB7C1, 0XB681, 0X7640,
       0X7200, 0XB2C1, 0XB381, 0X7340, 0XB101, 0X71C0, 0X7080, 0XB041,
       0X5000, 0X90C1, 0X9181, 0X5140, 0X9301, 0X53C0, 0X5280, 0X9241,
       0X9601, 0X56C0, 0X5780, 0X9741, 0X5500, 0X95C1, 0X9481, 0X5440,
       0X9C01, 0X5CC0, 0X5D80, 0X9D41, 0X5F00, 0X9FC1, 0X9E81, 0X5E40,
       0X5A00, 0X9AC1, 0X9B81, 0X5B40, 0X9901, 0X59C0, 0X5880, 0X9841,
       0X8801, 0X48C0, 0X4980, 0X8941, 0X4B00, 0X8BC1, 0X8A81, 0X4A40,
       0X4E00, 0X8EC1, 0X8F81, 0X4F40, 0X8D01, 0X4DC0, 0X4C80, 0X8C41,
       0X4400, 0X84C1, 0X8581, 0X4540, 0X8701, 0X47C0, 0X4680, 0X8641,
       0X8201, 0X42C0, 0X4380, 0X8341, 0X4100, 0X81C1, 0X8081, 0X4040 };

    BYTE nTemp;
    WORD wCRCWord = 0xFFFF;

    while (wLength--)
    {
        nTemp = *nData++ ^ wCRCWord;
        wCRCWord >>= 8;
        wCRCWord  ^= wCRCTable[nTemp];
    }
    return wCRCWord;
} // End: CRC16

还有

uint CRC16_2(QByteArray buf, int len)
{
  uint crc = 0xFFFF;

  for (int pos = 0; pos < len; pos++)
  {
    crc ^= (uint)buf[pos];          // XOR byte into least sig. byte of crc

    for (int i = 8; i != 0; i--) {    // Loop over each bit
      if ((crc & 0x0001) != 0) {      // If the LSB is set
        crc >>= 1;                    // Shift right and XOR 0xA001
        crc ^= 0xA001;
      }
      else                            // Else LSB is not set
        crc >>= 1;                    // Just shift right
    }
  }
  // Note, this number has low and high bytes swapped, so use it accordingly (or swap bytes)
  return crc;
}

问题是我应该得到两个十六进制字节作为CRC校验码,而这个函数返回一个整数值。例如,对于“01”(1个字节),我应该得到“7E80”,而我得到的是“21695”,我无法从这个整数值进行任何类型的转换来获得所需的十六进制数据。

因此,我的问题是:如何从整数结果转换为所需的双十六进制结果?我尝试了几个选项,但都没有成功。

注意:我正在使用Qt,因此如果有人能找到实现QByteArray或其他Qt友好代码的解决方案,我将很高兴。无论如何,不使用Qt、C或C++的解决方案都是无用的:P


格式化,你正在将它打印为十进制而不是十六进制。尝试使用例如std::cout << std::hex << value << '\n';。虽然十进制值21695与十六进制0x7e80不同。 - Some programmer dude
这个问题类似于这个问题,但是针对的是另一种编程语言。然而,如果您希望在不使用表格的情况下计算CRC,这仍然可能对您有用。 - avra
4个回答

10
unsigned int CRC16_2(unsigned char *buf, int len)
{  
  unsigned int crc = 0xFFFF;
  for (int pos = 0; pos < len; pos++)
  {
  crc ^= (unsigned int)buf[pos];    // XOR byte into least sig. byte of crc

  for (int i = 8; i != 0; i--) {    // Loop over each bit
    if ((crc & 0x0001) != 0) {      // If the LSB is set
      crc >>= 1;                    // Shift right and XOR 0xA001
      crc ^= 0xA001;
    }
    else                            // Else LSB is not set
      crc >>= 1;                    // Just shift right
    }
  }

  return crc;
}

我自己也是个新手,但是-

我使用了您提供的代码并测试了一下,如您所说它没有正常工作,但后来我发现它传递的是十六进制字符,所以我只需将"uint"更改为"char",至少对我来说可以正常运行。

我甚至手动计算了一个样本进行了双重检查。


@Roney 感谢你的回答,尽管我最终会使用 Kuba 的版本,因为我已经验证过了 =] - Momergil
1
@Momergil:这是Adam的答案,Roney只是稍微编辑了一下。 - László Papp
请勿在生产环境中使用此代码。除了调试之外,永远不需要使用这种CRC计算方法。 - Daniel Kamil Kozar
@DanielKamilKozar,有趣...但为什么? - Mohammad Kanan
1
@MohammadKanan:因为它由于一次处理一个比特的输入数据而变慢。当然,如果你正在编写自己的实现并进行调试,这是非常有价值的,因为你可以在过程的每个步骤中查看CRC输出寄存器。然而,这个简单的基准测试显示,按字节版本在我的机器上快5到6倍。即使你处于非常受内存限制的环境中,在这种特定情况下牺牲512字节的只读内存也可以给你带来很大的加速。 - Daniel Kamil Kozar
@DanielKamilKozar,感谢您提供的宝贵信息。实际上,我的问题主要动机是,我在使用的成熟源代码中看到了同样的代码段...好吧,我没有研究它对速度和错误率的影响...但我会去研究的。 - Mohammad Kanan

9
根据MODBUS串行线规范和实现指南V1.02,CRC以小端序(低字节在前)发送。
不过,我不知道您是如何得出需要任何十六进制字节用于CRC的结论。MODBUS RTU是一种二进制协议,CRC作为两个字节而非四个十六进制数字发送!
以下是使用您提供的CRC16函数的方法。
QByteArray makeRTUFrame(int slave, int function, const QByteArray & data) {
    Q_ASSERT(data.size() <= 252);
    QByteArray frame;
    QDataStream ds(&frame, QIODevice::WriteOnly);
    ds.setByteOrder(QDataStream::LittleEndian);
    ds << quint8(slave) << quint8(function);
    ds.writeRawData(data.constData(), data.size());
    int const crc = CRC16((BYTE*)frame.constData(), frame.size());
    ds << quint16(crc);
    return frame;
}

感谢快速回复。好的,在CRC16函数中不要忘记需要在data.constData()之前添加(BYTE*),你确定你的这个函数可以工作吗?:P 我试图将其与SimplyModbus.ca的Excel表进行测试,但我无法使CRC值匹配(注意:选择生成的QByteArray并在qdebug()中显示为toHex())。如果您自己尝试,我会很高兴。 - Momergil
@Momergil:CRC必须计算整个帧。我已经编辑了代码来修复这个问题。这表明您正在尝试在工业自动化设置中使用您不理解的代码。我希望没有人会因此丧命。 - Kuba hasn't forgotten Monica
哈哈,没问题 :) 我只是想知道:在Modbus协议中,需要使用从机地址(可以),命令/功能(可以),然后是寄存器号和数据,而您的函数只有数据。在这种情况下,我应该如何将其插入到您的函数中?只需添加<< quint8(register)即可吗?注意:我无法验证CRC是否正确完成。为了确保,您是否检查过您的CRC计算是否正常,例如Simply Modbus的CRC计算器? xD(希望你不会因为我问这个问题而生气 ^^) - Momergil
1
@Momergil:你提供了要使用的函数,检查它不是我的工作,你可以自己做。同样,data参数是MODBUS负载,其中包含寄存器号等内容。你必须能够理解这段代码并根据需要进行修改。这只是一个起点,而不是一个完成的解决方案。SO不是免费外包平台。 - Kuba hasn't forgotten Monica

2

我尝试使用您在此处发布的第一个代码示例(使用表格的代码),并发现在使用索引时存在错误。为了使代码正确运行,您必须访问由其大小限制的区域内的表格。

wCRCWord  ^= wCRCTable[(nTemp & 0xFF)];

所以,下面列出了返回适用于MODBUS的CRC16正确值的全部代码。返回的数字已经交换了Lo和Hi字节。

WORD CRC16 (const BYTE *nData, WORD wLength)
{
    static const WORD wCRCTable[] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
    0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
    0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
    0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
    0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
    0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
    0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
    0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
    0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
    0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
    0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
    0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
    0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
    0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
    0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
    0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
    0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
    0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
    0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
    0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
    0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
    0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
    0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
    0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
    0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
    0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
    0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
    0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
    0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
    0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
    0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 };

    BYTE nTemp;
    WORD wCRCWord = 0xFFFF;

    while (wLength--)
    {
        nTemp = *nData++ ^ wCRCWord;
        wCRCWord >>= 8;
        wCRCWord  ^= wCRCTable[(nTemp & 0xFF)];
    }
    return wCRCWord;
} // End: CRC16

你说返回的数已经交换了Lo和Hi字节。你确定这段代码是这样吗?我认为这段代码返回的是普通的CRC校验码,用户必须在将其连接到消息帧之前交换字节,如下所示:wCRCWord = (wCRCWord<<8) | (wCRCWord>>8)。例如,考虑一个简单的十六进制消息:11 03 00 6B 00 03。对于该考虑的帧,此代码计算出的CRC值为87 76,完整的消息为11 03 00 6B 00 03 76 87(例如来自此处:http://www.simplymodbus.ca/ASCII.htm)。 - Marko Gulin

0

这是我的两分意见。 首先,您不能将两个值返回给一个函数,因此

  1. 将两个值添加到数组中(记得删除const)
  2. 返回一个包含这两个值的结构体。
  3. 按以下方式处理返回值

    WORD n = CRC16(nData,wLength);
    WORD x = (0xFF00&n)>>8,y=0x00FF&n;
    printf("0x%04x\n", n); // 检查原始值
    printf("0x%02x\t0x%02x\n",x,y); // 检查分离的值
    

试一下,然后告诉我。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接