循環冗餘檢查 (CRC) 是資料通訊與儲存領域最常用的錯誤偵測演算法。本文從數學原理出發,詳細推導 CRC-16 (含 CCITT 變體) 與 CRC-32 的查表法運算邏輯,提供可直接嵌入專案的標準 C/C++ 實作碼,並解析查表法生成過程,助你徹底理解「知其然更知其所以然」。
| 名稱 | 多項式 (Hex) | 多項式表示 | 應用領域 |
|---|---|---|---|
| CRC-16 (IBM) | 0x8005 | x¹⁶ + x¹⁵ + x² + 1 | Modbus, USB, 磁碟儲存 |
| CRC-CCITT (X.25) | 0x1021 | x¹⁶ + x¹² + x⁵ + 1 | 藍牙, X.25, PPP, IrDA |
| CRC-32 (IEEE 802.3) | 0x04C10DB7 | x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹¹ + x¹⁰ + x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1 | Ethernet, ZIP, PNG, GZIP, SATA |
注意:程式碼中多項式常省略最高位的 1 (如 CRC-16 實際用 0x8005 而非 0x18005)。
CRC 本質是模 2 多項式除法 (XOR 取代減法)。以 1 位元組資料為例:
逐位運算效率低。觀察可知:中間餘數的最高位決定是否 XOR 多項式。由於資料每次只影響最高位判斷,我們可預先計算所有 256 種可能的輸入位元組 (0x00~0xFF) 經 8 步運算後的餘數,建立查找表。運算時僅需:
CRC = (CRC << 8) ^ Table[(CRC >> (n-8)) ^ DataByte];
其中 n 為 CRC 位寬 (16 或 32)。此法將運算從「位元迴圈」降為「單次查表 + XOR + 位移」,速度提升數十倍。
#include <stdint.h>
#include <stddef.h>
// ===== 多項式常數 =====
static const uint16_t POLY_CRC16 = 0x8005; // CRC-16 (IBM)
static const uint16_t POLY_CCITT = 0x1021; // CRC-CCITT (X.25)
static const uint32_t POLY_CRC32 = 0x04C10DB7; // CRC-32 (IEEE 802.3)
// ===== 查找表 =====
static uint16_t crc16_table[256];
static uint32_t crc32_table[256];
static bool tables_init = false;
// ===== 建表函式 =====
static void build_crc16_table(uint16_t poly) {
for (int i = 0; i < 256; ++i) {
uint16_t crc = (uint16_t)(i << 8);
for (int j = 0; j < 8; ++j) {
if (crc & 0x8000) crc = (crc << 1) ^ poly;
else crc <<= 1;
}
crc16_table[i] = crc;
}
}
static void build_crc32_table(uint32_t poly) {
for (int i = 0; i < 256; ++i) {
uint32_t crc = (uint32_t)(i << 24);
for (int j = 0; j < 8; ++j) {
if (crc & 0x80000000) crc = (crc << 1) ^ poly;
else crc <<= 1;
}
crc32_table[i] = crc;
}
}
static void ensure_tables_init() {
if (!tables_init) {
build_crc16_table(POLY_CRC16); // 預設 CRC-16,若需 CCITT 請自行調用 build_crc16_table(POLY_CCITT)
build_crc32_table(POLY_CRC32);
tables_init = true;
}
}
// ===== 公開 API =====
// CRC-16 / CRC-CCITT (初始值 0x0000, 無最終 XOR, 無反轉)
uint16_t crc16(const uint8_t *data, size_t len, uint16_t poly) {
if (poly != POLY_CRC16) build_crc16_table(poly); // 支援動態切換多項式
ensure_tables_init();
uint16_t crc = 0;
for (size_t i = 0; i < len; ++i) {
crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF];
}
return crc;
}
// CRC-32 (初始值 0xFFFFFFFF, 最終 XOR 0xFFFFFFFF, 輸入/輸出位元反轉 - 標準 IEEE 802.3)
// 注意:此處為簡化示範「無反轉」版本,標準 CRC-32 需配合位元反轉表或執行期反轉
uint32_t crc32_simple(const uint8_t *data, size_t len) {
ensure_tables_init();
uint32_t crc = 0; // 標準初始值為 0xFFFFFFFF
for (size_t i = 0; i < len; ++i) {
crc = (crc << 8) ^ crc32_table[((crc >> 24) ^ data[i]) & 0xFF];
}
return crc; // 標準需返回 crc ^ 0xFFFFFFFF
}
/* === 標準 CRC-32 (IEEE 802.3 / ZIP / PNG) 參考實作 === */
static uint32_t crc32_std_table[256];
static void build_crc32_std_table() {
for (int i = 0; i < 256; ++i) {
uint32_t c = (uint32_t)i;
for (int j = 0; j < 8; ++j)
c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1); // 反轉多項式 0x04C10DB7 -> 0xEDB88320
crc32_std_table[i] = c;
}
}
uint32_t crc32_standard(const uint8_t *data, size_t len) {
static bool init = false; if (!init) { build_crc32_std_table(); init = true; }
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < len; ++i)
crc = (crc >> 8) ^ crc32_std_table[(crc ^ data[i]) & 0xFF];
return crc ^ 0xFFFFFFFF;
}
| 參數 | CRC-16/CCITT (簡化版) | CRC-32 標準 (IEEE 802.3) |
|---|---|---|
| 初始值 | 0x0000 | 0xFFFFFFFF |
| 輸入位元反轉 | 否 | 是 (LSB first) |
| 輸出位元反轉 | 否 | 是 |
| 最終 XOR | 0x0000 | 0xFFFFFFFF |
| 查表索引 | (CRC >> 8) ^ Data | (CRC ^ Data) & 0xFF (配合反轉表) |
| 查表運算 | CRC = (CRC << 8) ^ Table[...] | CRC = (CRC >> 8) ^ Table[...] |
// CRC-16 (0x8005) : "123456789" -> 0xBB3D (或 0x44C2 視反轉設定)
// CRC-CCITT (0x1021): "123456789" -> 0x29B1 (標準 X.25 初始 0xFFFF)
// CRC-32 (IEEE) : "123456789" -> 0xCBF43926 (標準檢驗值)
掌握 CRC 原理與正確實作,是確保資料完整性、實現通訊協定、檔案校驗的基石。建議專案中封裝為獨立模組,並附上單元測試驗證標準向量。