计算 Modbus RTU CRC 16

Posted

技术标签:

【中文标题】计算 Modbus RTU CRC 16【英文标题】:Calculating Modbus RTU CRC 16 【发布时间】:2013-10-21 06:40:01 【问题描述】:

我正在实现一个软件,我在其中通过串行读取和写入 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 &lt;&lt; std::hex &lt;&lt; value &lt;&lt; '\n';。虽然十进制值21695与十六进制0x7e80不同。 问题与 this one 类似,但针对不同的编程语言。但是,如果您希望在不使用表格的情况下计算 CRC,那您仍然可能会感兴趣。 【参考方案1】:
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;

我自己就是个菜鸟,butttt-

我使用了你提供的代码并自己测试了它,正如你所说的那样它不能正常工作,但后来我意识到它正在传递十六进制字符,所以我只是将 uint 更改为 char 并且它至少检查了我。

我什至手工计算了一个样本来仔细检查。

【讨论】:

@Roney Thansk Roney 的答案,虽然我最终会使用 Kuba 的版本,因为我已经验证了它 =] @Momergil:这是亚当的回答。 Roney 只是稍微编辑了一下。 请不要在生产中使用此代码。除了调试之外,没有必要使用这种 CRC 计算方法。 @DanielKamilKozar,很有趣……但为什么呢? @MohammadKanan :因为一次处理一位输入数据,它很慢。如果您正在编写自己的实现和调试,这当然是非常宝贵的,因为您基本上可以在过程的每个步骤中查看 CRC 输出寄存器。但是,this simple benchmark 表明,在我的机器上,字节版本的速度要快 5 到 6 倍。即使您处于内存非常受限的环境中,牺牲 512 字节(在这种特殊情况下)的只读内存也可以为您带来很大的加速。【参考方案2】:

根据MODBUS over serial line specification and implementation guide V1.02,CRC 以 little-endian(低字节优先)发送。

不过,我不知道您是如何想出需要任何十六进制字节用于 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:必须在整个帧上计算 CRC。我已经编辑了代码来解决这个问题。这表明您正在尝试在工业自动化设置中使用您不理解的代码。我希望没有人被杀。 哈哈,不用担心 :) 我只是想知道:在 Modbus 协议中,需要安装包:从地址(ok),命令/功能(ok),然后是寄存器号和然后是数据 - 而您的函数只有数据。在这种情况下,我应该如何在你的函数中插入它?只添加一个 @Momergil:您提供了要使用的功能,为您检查它不是我的工作。你可以自己做。同样,data 参数是具有寄存器号等的 MODBUS 有效负载。您必须能够理解此代码并按照您认为合适的方式对其进行修改。这是一个起点,而不是一个最终的解决方案。 SO 不是免费外包。【参考方案3】:

我尝试使用您在此处发布的第一个代码示例(使用 table 的代码示例),我发现使用 index.xml 有错误。为了使代码正确运行,您必须访问受其大小限制的区域中的表格。

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&lt;&lt;8) | (wCRCWord&gt;&gt;8)。例如,考虑一个简单的十六进制消息:11 03 00 6B 00 03。此代码为所考虑的帧计算的 CRC 值为87 76,完整的消息为11 03 00 6B 00 03 76 87(示例来自此处:simplymodbus.ca/ASCII.htm)【参考方案4】:

这是我的两分钱。 首先你不能将两个值返回给一个函数,所以

    将两个值添加到数组中(记得删除 const) 返回一个包含这两个值的结构。

    过程返回值如下

    WORD n = CRC16(nData,wLength);
    WORD x = (0xFF00&n)>>8,y=0x00FF&n;
    printf("0x%04x\n", n); // to check original value
    printf("0x%02x\t0x%02x\n",x,y); // to check separated values
    

试试这个,然后告诉我。

【讨论】:

以上是关于计算 Modbus RTU CRC 16的主要内容,如果未能解决你的问题,请参考以下文章

Modbus RTU CRC校验码计算方法

modbus rtu校验方法

51单片机MODBUS通讯,RTU中的CRC校验

modbus+rtu功能码是啥进制数

C51 modbus rtu crc下面这段代码校验结果总是不对,请高手指教,不胜感激!

VB Modbus RTU CRC 校验