EMS 知识库
首页 / knowledge/代码解析/common

crc.c

源文件路径:common/crc.c 所属模块:common(通用工具库) 代码行数:224

1. 文件职责

本文件实现 EMS 固件里全部的通用校验算法:1 个 8 位 CRC、3 个 16 位 CRC 入口和 1 个 16 位 CRC 的单字节增量迭代接口。它处于数据流的「出入两端」:发送侧在组好帧后补上校验字段,接收侧在解析前用它验证帧/记录的完整性;同时被数据层用于落盘结构(配置、故障日志、网络事件日志、断电续传数据)的完整性保护。全部算法都是查表实现,4 张表(wCRC8_Table、wCRCTable、auchCRCHi、auchCRCLo)为 static const 只读数据,函数本身无状态、不访问任何全局变量,因此没有自己的任务,是被 meter/inverter/bms/afci/evc/parallel 等通信与升级任务在收发包路径上直接同步调用的工具函数。

2. 关键数据结构与宏

名称类型说明
wCRC8_Table[256]static const char 数组CRC-8/MAXIM 查找表,[1]=0x5E,与 Dallas/Maxim 标准表一致(crc.c:18~:36)
wCRCTable[256]static const short 数组反射多项式 0xA001 的 16 位 CRC 表,wCRCTable[1]=0xC0C1(crc.c:38~:69)
auchCRCHi[256]static const unsigned char 数组经典 Modbus 附录 B 的 auchCRCHi 表,实际存放的是 CRC 结果的低字节(crc.c:71~:91,见备注 2)
auchCRCLo[256]static const unsigned char 数组经典 Modbus 附录 B 的 auchCRCLo 表,实际存放的是 CRC 结果的高字节(crc.c:93~:113)
wCRCWord局部变量 u1616 位 CRC 中间寄存器
nTemp / uindex局部变量 u8查表下标 = (CRC 低字节 ^ 输入字节) & 0xFF
crc(8 位)局部变量 unsigned charCRC-8 寄存器,初值 0x00
uchCRCHi / uchCRCLo局部变量 u8GetCrcCode_CRC16 的两个状态字节

4 张表都已用脚本核对为恰好 256 个元素,无缺项。本文件不定义结构体、枚举、联合体、位域,也没有 #define 常量。文件头注释里的 @file :convert.c 是复制粘贴遗留的错误标注(见备注 1)。

3. 函数清单

函数名作用域说明
GetCrcCode_8_MAXIM全局按 wCRC8_Table 逐字节计算 CRC-8/MAXIM,初值 0x00,返回 8 位结果
GetCrcCode_Modbus全局按 wCRCTable 计算反射 CRC-16,初值 0xFFFF,返回 16 位结果
GetCrcCode_IBM全局与 GetCrcCode_Modbus 同表同步,仅初值改为 0x0000
get_data_crc_check全局用外部传入的中间值 crc 再迭代一个字节,返回新的 16 位中间值
GetCrcCode_CRC16全局用 auchCRCHi/auchCRCLo 查表计算反射 CRC-16,初值由参数给定,返回 16 位结果

共 5 个函数,全部为全局作用域,文件中没有 static 函数。crc.h 中的 5 个原型与上表一一对应。

4. 核心逻辑解析

4.1 GetCrcCode_8_MAXIM(crc.c:126~:135)

crc = 0x00;
while (len--) { crc = wCRC8_Table[crc ^ (*ptr++)]; }

标准查表式 CRC-8/MAXIM(Dallas 1-Wire 用的那位):反射多项式 0x31(正向记法)、初值 0x00、无终值异或。核对结果:对标准测试向量 "123456789" 得 0xA1,与 CRC-8/MAXIM 的已知值一致;wCRC8_Table[1]=0x5E 也与该算法标准表一致。调用方 parallel_can.c 把它放在 CAN 帧的第 8 字节(frame_data[7]),对前 7 字节求校验:

即帧格式固定为「7 字节载荷 + 1 字节 CRC-8」。该算法不可增量调用(表里没有 crc 初值参数),因此接收端只能整帧重算。

4.2 GetCrcCode_Modbus(crc.c:144~:156)与 GetCrcCode_IBM(crc.c:165~:177)

两者代码结构完全相同,唯一差别是初值:

u16 wCRCWord = 0xFFFF;          // GetCrcCode_IBM 处为 0x0000
while (wLen--) {
    nTemp = *pData++ ^ wCRCWord;   // 只取低 8 位参与下标
    wCRCWord >>= 8;
    wCRCWord ^= wCRCTable[nTemp];
}

这是标准的「右移 + 查表」反射 CRC-16 迭代步,多项式 0xA001。

两者都不做终值异或(xorout = 0),与各自的算法定义一致。

4.3 get_data_crc_check(crc.c:190~:199)

u16 get_data_crc_check(u16 crc, const u8 data)
{
    u8 nTemp = data ^ crc;
    crc >>= 8;
    crc ^= wCRCTable[nTemp];
    return crc;
}

这正是 GetCrcCode_Modbus 循环体的一次迭代,被抽出来供「数据分多帧到达、无法一次性拿到连续内存」的场景使用。midware/bms/alpha/battery_upgrade.c 的 BMS CAN 升级流程就是这种场景:先在某处把中间值初始化成 0xFFFF(battery_upgrade.c:607),随后在每一帧 CAN 报文的载荷字节上连续调用(battery_upgrade.c:287~:290 每次消费 frame_data[4..7] 四个字节,battery_upgrade.c:395 在循环中消费 frame_data[i]),最终结果等价于对拼接后的整段数据调用 GetCrcCode_Modbus。

中间值必须由调用方自己维护:本函数不保存任何状态,传入的 crc 就是「到目前为止的 CRC」,返回的是「加入本字节后的 CRC」。如果调用方忘记初始化或漏调一次,结果会静默错误(无任何报错机制)。

4.4 GetCrcCode_CRC16(crc.c:212~:224)

u8 uchCRCHi = (initCRC16 >> 8) & 0xFF;
u8 uchCRCLo = (initCRC16 & 0xFF);
while (len--) {
    uindex     = uchCRCLo ^ *updata++;
    uchCRCLo   = uchCRCHi ^ auchCRCHi[uindex];
    uchCRCHi   = auchCRCLo[uindex];
}
return (uchCRCHi << 8 | uchCRCLo);

这是 Modbus 附录 B 那段经典样例代码的形态,但两个状态字节的角色与样例代码相反:样例代码用高字节做查表下标(uindex = uchCRCHi ^ msg),本文件用低字节做下标(uindex = uchCRCLo ^ msg)。与之匹配地,本文件的两张表内容也和表名反着放(auchCRCHi[1]=0xC1 其实是 wCRCTable[1]=0xC0C1 的低字节)。

已用程序实测核对:本函数实现的就是标准反射 CRC-16(多项式 0xA001),initCRC16 = 0xFFFF 时结果与 GetCrcCode_Modbus 完全一致("123456789" → 0x4B37、01 03 00 00 00 0A → 0xCDC5),initCRC16 = 0x0000 时与 GetCrcCode_IBM 一致(→ 0xBB3D)。也就是说「表名与内容互换 + 下标用低字节」这一组合是自洽的,并非实现错误,但极易被后续维护者「按样例代码修正」而改坏。

工程内唯一调用点是 midware/bms/alpha/battery_comm.c:1992:GetCrcCode_CRC16(0xFFFF, g_t_bms_log_buff.u8_log_buff, g_t_bms_log_buff.u8_log_len),用于校验从电池读回的日志包,随后与 data[] 中高字节在前取出的 l_u16_rev_crc = (u16)(data[i] << 8 | data[i+1]) 比对(battery_comm.c:1991)。因为本函数返回值就是标准值本身(不是字节交换),所以电池协议那边是「高字节先发」的约定。

4.5 校验字段的位置惯例

本文件只负责算值,不负责摆放。工程内形成的事实约定是:

4.6 与 uCOS-II / 定时器的关系

无。本文件不调用任何 OS API,不使用信号量/邮箱/延时,也不存在内部状态或重试逻辑。它总是在调用它的任务上下文中同步执行,执行时间是纯 CPU 查表开销(每字节一次移位异或),是整个固件里最安全的工具函数之一。

5. 对外接口

导出(供外部使用):

名称使用方(文件级)
GetCrcCode_Modbusdata/fault_warn_log/fault_warn_log.c、data/net_event_log/net_event_log.c、data/resume_data/flash/flash_data_store.c、data/system_config/system_config.c、hmi/ui/ui_history_error_log.c、midware/afci/afci_comm.c、midware/afci/afci_upgrade.c、midware/ethernt/ethernet.c、midware/evc/evc.c、midware/evc/evc_ii.c、midware/interface/modbus/modbus.c、midware/inverter/alphaess/alphaess_comm_usart.c、midware/inverter/alphaess/alphaess_upgrade.c、midware/inverter/ginlong/ginlong_comm_usart.c、midware/inverter/goodwe/goodwe_comm.c、midware/inverter/inovance/inovance_comm_usart.c、midware/inverter/inovance/inovance_upgrade.c、midware/inverter/parallel.c、midware/inverter/parallel_comm/parallel_sci.c、midware/inverter/pvcharger.c、midware/inverter/set/set_comm_usart_s5.c、midware/meter/backupbox.c、midware/meter/diesel_generator.c、midware/meter/meter.c、midware/meter/meter_upgrade.c、net/ems.c、net/ems_download.c、net/server.c(共 28 个 .c)
GetCrcCode_8_MAXIMmidware/inverter/parallel_comm/parallel_can.c:92/:152/:527/:542(parallel_sci.c:78 为注释掉的旧调用)
GetCrcCode_IBMmidware/evc/evc_upgrade.c:61/:68/:134/:143
get_data_crc_checkmidware/bms/alpha/battery_upgrade.c:287~290、:395
GetCrcCode_CRC16midware/bms/alpha/battery_comm.c:1992

被调用(本文件引用的外部符号):

名称来源
u8 / u16 / u32system/sys/chip.h,经 crc.h:13 引入

本文件不导出全局变量,也不定义任何可被外部改写的状态。

6. 依赖关系

依赖:

被依赖:见上一节「导出」列的 28+2+1+1+1 个调用文件;这些文件都是通过 #include "crc.h" 引入的(35 个包含点,完整清单见 crc.h.md 第 6 节)。

7. 备注与风险点

  1. 文件头注释张冠李戴:crc.c:2 写的是 @file :convert.c,@author :Alpha EMS Team,与 crc.h 的 @file :crc.h 不一致,属复制粘贴遗留,无功能影响但会误导检索。
  2. 表名与表内容互换,极易被"修正"成 bug:auchCRCHi 里放的是 CRC 低字节([1]=0xC1,而 wCRCTable[1]=0xC0C1),auchCRCLo 里放的是高字节;GetCrcCode_CRC16 的下标又用 uchCRCLo。这三处「反着来」互相抵消成了正确的标准算法(已实测与 CRC-16/MODBUS 完全一致)。如果后续有人照 Modbus 附录 B 的样例代码去"修"其中任意一处,算法就会静默变成另一个 CRC,且只在电池日志校验失败时才暴露。
  3. wCRCTable 声明为 short(有符号):表里 0xC0C1 这类值在 16 位平台上为负,wCRCWord ^= wCRCTable[nTemp] 会走有符号→无符号的整数提升再截断。当前实现在 C 的模运算语义下结果正确(已实测),但属于实现细节上的隐患,改成 u16 更稳妥。wCRC8_Table 声明为 char 同理(0xBC 等值为负,参与 ^ 时先提升为 int,再截断回 unsigned char,同样"碰巧"正确)。
  4. 无参数校验:5 个函数都直接解引用指针,没有 NULL 判断,len 为 0 时返回初值。传入空指针会直接触发硬件异常;传入超长长度会越界读。工程内的调用点都是内部缓冲,暂未发现触发路径,但作为被 35 个文件复用的公共库,缺少这道防线值得记录。
  5. get_data_crc_check 缺少"完成"语义:接口只做单字节迭代,调用方必须自己保证「初值 0xFFFF + 不重不漏地覆盖每个字节」。battery_upgrade.c 里初始化点(:607)与消费点(:287)相距很远,且中间穿插了状态机跳转与 OS 延时,属于典型的状态跨越多处维护的写法,一旦将来新增分支漏掉初始化就会产出错误的校验值且无任何报错。
  6. GetCrcCode_8_MAXIM 无法增量计算:与上面相反,它没有传入中间值的入口,接收端只能整帧重算。若将来并机帧需要分片接收,需要新增接口而不是复用本函数。
  7. GetCrcCode_Modbus 与 GetCrcCode_CRC16 功能重叠:GetCrcCode_CRC16(0xFFFF, ...) 与 GetCrcCode_Modbus(...) 结果完全相同(已实测)。同一份算法在两处以不同代码形态存在,一旦将来只修其中一处(例如统一改成硬件 CRC 外设),两处会不一致。
  8. 文件编码与同目录其它文件不一致(GBK):本文件的唯一两处中文注释(:204 的 电池日志、校验 共 12 个字节)是 GBK 编码(字节 B5 E7 B3 D8 C8 D5 D6 BE / D0 A3 D1 E9),而 common/crc.h、convert.c、convert.h、hex_file_parse.c、hex_file_parse.h、s19.c、s19.h 均为纯 ASCII 或 UTF-8。同一文件用 GBK 写中文而在 UTF-8 环境(含多数 Git/CI 工具与代码审查界面)下会显示为乱码;用 Keil 打开时是否正常取决于工程/编辑器的本地编码设置。