crc.c
GetCrcCode_8_MAXIM(crc.c:126~:135)4.2 GetCrcCode_Modbus(crc.c:144~:156)与 GetCrcCode_IBM(crc.c:165~:177)4.3 get_data_crc_check(crc.c:190~:199)4.4 GetCrcCode_CRC16(crc.c:212~:224)4.5 校验字段的位置惯例4.6 与 uCOS-II / 定时器的关系5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
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 | 局部变量 u16 | 16 位 CRC 中间寄存器 |
nTemp / uindex | 局部变量 u8 | 查表下标 = (CRC 低字节 ^ 输入字节) & 0xFF |
crc(8 位) | 局部变量 unsigned char | CRC-8 寄存器,初值 0x00 |
uchCRCHi / uchCRCLo | 局部变量 u8 | GetCrcCode_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 字节求校验:
midware/inverter/parallel_comm/parallel_can.c:92、:542:接收侧GetCrcCode_8_MAXIM(rxMsg->frame_data, 7)与frame_data[7]比对;midware/inverter/parallel_comm/parallel_can.c:152、:527:发送侧对前 7 字节算校验后写入frame_data[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。
GetCrcCode_Modbus等价于 CRC-16/MODBUS:标准向量"123456789"→0x4B37;01 03 00 00 00 0A→0xCDC5(即常见 Modbus 帧尾C5 CD)。工程内约定把返回值的低字节先发、高字节后发:buf[n] = crc & 0xFF; buf[n+1] = crc >> 8;(midware/afci/afci_comm.c:63~:65、midware/evc/evc.c:68~:69等)。GetCrcCode_IBM等价于 CRC-16/ARC(初值0x00),标准向量"123456789"→0xBB3D。工程内只被midware/evc/evc_upgrade.c的 EVC 升级帧使用。
两者都不做终值异或(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 校验字段的位置惯例
本文件只负责算值,不负责摆放。工程内形成的事实约定是:
- Modbus RTU(AFCI/EVC/电表/Modbus 从站):CRC 两字节紧跟在 PDU 之后,低字节在前;
- CAN 协议(并机):CRC-8 固定在
frame_data[7]; - 落盘结构(
data/system_config/system_config.c、data/resume_data/flash/flash_data_store.c、data/fault_warn_log/fault_warn_log.c、data/net_event_log/net_event_log.c):u16_crc_code放在结构体尾部,计算长度统一写为sizeof(X) - 2,校验时用同一表达式重算比对。
4.6 与 uCOS-II / 定时器的关系
无。本文件不调用任何 OS API,不使用信号量/邮箱/延时,也不存在内部状态或重试逻辑。它总是在调用它的任务上下文中同步执行,执行时间是纯 CPU 查表开销(每字节一次移位异或),是整个固件里最安全的工具函数之一。
5. 对外接口
导出(供外部使用):
| 名称 | 使用方(文件级) |
|---|---|
GetCrcCode_Modbus | data/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_MAXIM | midware/inverter/parallel_comm/parallel_can.c:92/:152/:527/:542(parallel_sci.c:78 为注释掉的旧调用) |
GetCrcCode_IBM | midware/evc/evc_upgrade.c:61/:68/:134/:143 |
get_data_crc_check | midware/bms/alpha/battery_upgrade.c:287~290、:395 |
GetCrcCode_CRC16 | midware/bms/alpha/battery_comm.c:1992 |
被调用(本文件引用的外部符号):
| 名称 | 来源 |
|---|---|
u8 / u16 / u32 | system/sys/chip.h,经 crc.h:13 引入 |
本文件不导出全局变量,也不定义任何可被外部改写的状态。
6. 依赖关系
依赖:
common/crc.h(crc.c:13)——进而依赖system/sys/chip.h的类型定义;- 仅依赖 C 标准语法,未使用
string.h/stdio.h(#if S19_DEBUG之类的调试打印在本文件中不存在)。
被依赖:见上一节「导出」列的 28+2+1+1+1 个调用文件;这些文件都是通过 #include "crc.h" 引入的(35 个包含点,完整清单见 crc.h.md 第 6 节)。
7. 备注与风险点
- 文件头注释张冠李戴:
crc.c:2写的是@file :convert.c,@author :Alpha EMS Team,与crc.h的@file :crc.h不一致,属复制粘贴遗留,无功能影响但会误导检索。 - 表名与表内容互换,极易被"修正"成 bug:
auchCRCHi里放的是 CRC 低字节([1]=0xC1,而wCRCTable[1]=0xC0C1),auchCRCLo里放的是高字节;GetCrcCode_CRC16的下标又用uchCRCLo。这三处「反着来」互相抵消成了正确的标准算法(已实测与 CRC-16/MODBUS 完全一致)。如果后续有人照 Modbus 附录 B 的样例代码去"修"其中任意一处,算法就会静默变成另一个 CRC,且只在电池日志校验失败时才暴露。 wCRCTable声明为short(有符号):表里0xC0C1这类值在 16 位平台上为负,wCRCWord ^= wCRCTable[nTemp]会走有符号→无符号的整数提升再截断。当前实现在 C 的模运算语义下结果正确(已实测),但属于实现细节上的隐患,改成u16更稳妥。wCRC8_Table声明为char同理(0xBC等值为负,参与^时先提升为 int,再截断回unsigned char,同样"碰巧"正确)。- 无参数校验:5 个函数都直接解引用指针,没有 NULL 判断,
len为 0 时返回初值。传入空指针会直接触发硬件异常;传入超长长度会越界读。工程内的调用点都是内部缓冲,暂未发现触发路径,但作为被 35 个文件复用的公共库,缺少这道防线值得记录。 get_data_crc_check缺少"完成"语义:接口只做单字节迭代,调用方必须自己保证「初值0xFFFF+ 不重不漏地覆盖每个字节」。battery_upgrade.c里初始化点(:607)与消费点(:287)相距很远,且中间穿插了状态机跳转与OS延时,属于典型的状态跨越多处维护的写法,一旦将来新增分支漏掉初始化就会产出错误的校验值且无任何报错。GetCrcCode_8_MAXIM无法增量计算:与上面相反,它没有传入中间值的入口,接收端只能整帧重算。若将来并机帧需要分片接收,需要新增接口而不是复用本函数。GetCrcCode_Modbus与GetCrcCode_CRC16功能重叠:GetCrcCode_CRC16(0xFFFF, ...)与GetCrcCode_Modbus(...)结果完全相同(已实测)。同一份算法在两处以不同代码形态存在,一旦将来只修其中一处(例如统一改成硬件 CRC 外设),两处会不一致。- 文件编码与同目录其它文件不一致(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 打开时是否正常取决于工程/编辑器的本地编码设置。