PAYATHON 2026

DS28EC20 的 32 字节 Extended Read CRC16 为何与设备结果不一致?

数据阿明

原脚本有两处错误:循环只处理了 34 个字节,漏掉末尾的 0x34;右移算法应使用反射多项式 0xA001。即使修正这两处,对题中的完整 35 个字节算出的原始 CRC 仍是 0xB55B,取反后为 0x4AA4,与记录的 0x1181 不一致。脚本错误可以确定,但设备返回值与所列数据为何不一致,还需要核对实际通信记录。

CRC16 的计算规则

DS28EC20 使用多项式 x^16 + x^15 + x^2 + 1,正常表示为 0x8005。采用最低有效位优先的右移实现时,使用其反射表示 0xA001,初始值为 0x0000,发送前将结果按位取反。DS28EC20 数据手册的 CRC GENERATION 章节

这里需要区分两个操作:

  • 反射:决定多项式表示与移位方向的搭配,例如右移实现使用 0xA001。
  • 取反:对最终的 16 位结果执行 crc ^ 0xFFFF。

0xA001 不是对 0x8005 按位取反得到的。使用下面的右移算法时,也不需要再手动反转每个输入字节。

原脚本为什么得到 0xE578

两个计算循环都写成了:

for(my $i = 0; $i < 34; $i++) {

这个循环只处理下标 0 到 33。输入共有 35 个字节,下标 34 对应的最后一个字节 0x34 被漏掉了。

使用初始值 0x0000 和右移多项式 0xA001,对题中数据计算可得:

参与计算的数据未取反 CRC取反后 CRC
前 34 字节,最后为 0x330x1A870xE578
完整 35 字节,最后为 0x340xB55B0x4AA4

表中第一行与原脚本的第二组输出一致,说明这组输出符合实际处理的 34 个字节。

第一组计算则把 0x8005 直接用于检查最低位并右移的循环,移位方向与多项式表示不匹配。原描述中的 0x8001 也与脚本不一致,应以脚本实际使用的 0x8005 为准。

Python 实现

下面将原始 CRC 计算与最终取反分开,便于逐项检查:

def crc16_raw(data, crc=0x0000):
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc & 0xFFFF


message = bytes.fromhex(
    "A5 00 00 "
    "03 04 05 06 07 08 09 "
    "10 11 12 13 14 15 16 17 18 19 "
    "20 21 22 23 24 25 26 27 28 29 "
    "30 31 32 33 34"
)

raw_crc = crc16_raw(message)
transmitted_crc = raw_crc ^ 0xFFFF
crc_bytes = transmitted_crc.to_bytes(2, byteorder="little")

print(f"Length: {len(message)}")
print(f"Raw CRC: 0x{raw_crc:04X}")
print(f"Inverted CRC: 0x{transmitted_crc:04X}")
print("CRC bytes:", crc_bytes.hex(" "))

对所列字节的计算结果为:

Length: 35
Raw CRC: 0xB55B
Inverted CRC: 0x4AA4
CRC bytes: a4 4a

这里的 0x10 是十进制 16,不是十进制 10。bytes.fromhex() 按十六进制解析,与原脚本的 hex($line) 一致。

Perl 脚本的修改方法

保留现有文件读取部分,将两组尝试性计算替换为以下循环。循环直接遍历数组,避免因手写长度而遗漏字节。

$crc = 0x0000;

for my $byte (@number_set_1) {
    $crc ^= $byte;

    for (1 .. 8) {
        if ($crc & 1) {
            $crc = ($crc >> 1) ^ 0xA001;
        } else {
            $crc >>= 1;
        }
    }
}

$crc &= 0xFFFF;
$crc_n = (~$crc) & 0xFFFF;

printf("Length: %d\n", scalar @number_set_1);
printf("Raw CRC: 0x%04X\n", $crc);
printf("Inverted CRC: 0x%04X\n", $crc_n);
printf("CRC bytes: %02X %02X\n",
       $crc_n & 0xFF, ($crc_n >> 8) & 0xFF);

为什么仍然不能确认 0x1181

首次读取时,CRC 覆盖 0xA5、TA1、TA2,以及从起始地址到当前页末尾的数据。从 0x0000 开始读取时,这些就是题中的 35 个字节。同一次命令继续读取后续页时,每页的 CRC 都从零开始计算,只覆盖该页数据。若起始地址不在页首,首次 CRC 前的数据会少于 32 字节。DS28EC20 数据手册的 Extended Read Memory 与 CRC GENERATION 章节

建议按以下顺序核对实际读写日志:

  1. 确认发送的是 A5 00 00,并记录紧随其后的 32 个接收数据字节。
  2. 将再后面的两个 CRC 字节单独记录,保留接收顺序。不要把用于产生读取时隙的占位字节当作返回数据。
  3. 确认取到的是本次命令第一页的 CRC,计算输入中没有混入 ROM 选择命令、ROM ID 或上一页的 CRC。
  4. 核对驱动是否已经对 CRC 做过字节交换或取反,避免重复转换。

两个 CRC 字节按低字节在前组合。例如,本例计算出的 0x4AA4 对应接收顺序 A4 4A。若 0x1181 是按这种顺序组合后的整数,其原始字节应为 81 11;若日志原始顺序其实是 11 81,组合后则为 0x8111。两种情况都与本例的 0x4AA4 不一致,单纯交换字节无法解释差异。

对于所列的完整输入,计算结果是 0x4AA4。仅凭这些字节和合并后的 0x1181,无法判断差异来自读取偏移、数据记录、驱动处理还是总线传输。还需要同一次事务的原始收发字节才能继续定位。

备注:内容仅供参考。