ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

手写BCH(63,39)纠错码:从数学原理到NAND Flash 4bit ECC实现

手写BCH(63,39)纠错码:从数学原理到NAND Flash 4bit ECC实现 简介面向NAND闪存稳定性提升的4bits纠错BCH算法源代码包围绕三星K9LAG08U0M等MLC芯片的数据校验需求提供可实际运行的编解码实现。包内共10个文件以C语言源码为主体涵盖编码、解码、全局定义、错误处理及测试数据生成等模块同时附带两份PDF文档分别对应三星芯片规格书与BCH算法原理讲解并配套自动化测试脚本与错误数据模式便于对照验证。整个压缩包体量仅943KB层次清晰便于嵌入式存储开发者、驱动工程师及SSD学习人员快速上手。目前已有1211人学习下载被多次用于Flash ECC方案参考。通过这份源码读者可以深入理解BCH码的有限域运算、伴随式解码、错误位置定位等核心步骤并能借助自带测试工具快速评估算法效果为实际项目中的可靠存储设计提供有力支撑对提升NAND Flash数据完整性设计能力也大有裨益。 前段时间在调一块NAND Flash驱动的ECC逻辑原来用的是1bit纠错的汉明码颗粒老化后连续出现correctable error偶尔还有几页直接ECC fail。没办法只能升级到4bit纠错。搜了一圈现成库要么体积太大要么绑定特定平台最后决定把BCH算法的源代码完整吃透自己撸一套。这篇文章就把这次实战的完整思路写出来包含可直接运行的C代码以及调参、踩坑、工程落地的经验给同样在搞存储控制器、嵌入式可靠性的朋友做个参考。1. 为什么恰恰是4bit BCH而不是更“高级”的LDPC不少朋友第一反应是现在SSD主控不都上LDPC了吗怎么还有人折腾BCH这个问题问得对但要看场景。1.1 数据翻转与纠错能力的真实需求无论是NAND Flash还是DDR内存存储单元在读写过程中都会因为电荷泄漏、读干扰、写干扰等原因产生比特翻转。制程越先进、闪存层数越高错误率越明显。1bit纠错面对这种情况已经力不从心只要一个扇区出现两位错误整个扇区就报废了。4bit纠错是嵌入式领域一个很典型的“甜点值”。相比1bit纠错它能容忍四位随机错误覆盖绝大多数颗粒老化场景相比8bit、12bit纠错它的校验位更少、编解码延迟更低、硬件面积更小。在很多工业级产品中4bit已经是写入规格书的硬性指标。1.2 BCH相比汉明码、RS码、LDPC的取舍汉明码只能纠1bit检测2bit适合DDR ECC这类延迟极度敏感的场合。RS码本质是字节级纠错擅长处理突发错误但对独立随机比特错误的空间利用率不如BCH。LDPC纠错能力强接近香农限但需要软判决信息和迭代译码控制器的复杂度和延迟都上去了。BCH码属于循环码二进制BCH可以直接翻转错误比特不需要计算错误值代码可控性好中等纠错能力下性价比非常高。所以4bit这个档位BCH几乎是标准答案。源码实现起来也远没有LDPC那么劝退理解清楚数学原理后核心代码其实没多少行。2. BCH的数学骨架GF(2^6)有限域和生成多项式很多人在这一步被劝退。我得说不需要完整啃完《纠错码引论》才能写代码但有几个核心概念必须搞懂否则后面调试会一头雾水。2.1 伽罗华域表是怎么来的BCH运算全部落在GF(2^m)有限域上。选m6是因为本原BCH码的码长n2^m-1得n63足够演示4bit纠错。GF(2^6)里的每个元素都可以看成二进制多项式域上的加法就是异或乘法就是对指数做模63加法。工程上不用真正实现域乘法我通常会提前建两张表gf_exp[i]记录α^i对应的域元素值gf_log[val]记录某个域元素对应的指数。查表比每次做乘法和求逆快一个数量级。初始化时从α^01开始每次乘α如果寄存器溢出到第6位就异或上本原多项式的低位部分。这里用的是本原多项式x^6 x 1对应二进制1000011去掉最高位就是0x43。#define GF_M 6 #define GF_N ((1 GF_M) - 1) /* 63 */ #define PRIMITIVE_POLY 0x43 static uint8_t gf_exp[2 * GF_N]; static uint8_t gf_log[GF_N]; void gf_init(void) { int i; uint8_t x 1; for (i 0; i GF_N; i) { gf_exp[i] x; gf_log[x] i; x 1; if (x 0x40) x ^ PRIMITIVE_POLY; } for (i GF_N; i 2 * GF_N; i) gf_exp[i] gf_exp[i - GF_N]; }这张表是整个BCH算法的地基。后续生成多项式、伴随式计算、BM迭代全都跑在查表上。2.2 生成多项式为什么取α^1, α^3, α^5, α^7的共轭根这是最容易产生疑惑的地方。BCH码的纠错能力由生成多项式的根决定。要纠t位错生成多项式必须以α^1, α^2, …, α^(2t)为根。由于二进制域上偶次幂是奇次幂的平方所以实际只需要保证α^1, α^3, α^5, α^7是根它们的平方也自动是根。但生成多项式是GF(2)上的多项式系数只能是0或1。α^3的最小多项式不只是(xα^3)而是要把α^3的所有共轭元都乘进来。某个元素β的共轭元集合是β, β^2, β^4, β^8…直到回到β本身。所以实现时不能简单地套公式要先把α^1、α^3、α^5、α^7各自的共轭闭包全部找出来然后对所有根做乘法static int gen_poly[GF_M * 4 1]; /* 校验位最多24位多项式次数24系数25个 */ static int gen_poly_deg; static int get_conjugates(int root, int *out) { int cnt 0; int cur root; do { out[cnt] cur; cur (cur * 2) % GF_N; } while (cur ! root cnt GF_M); return cnt; } void compute_generator(void) { int visited[GF_N] {0}; int roots[GF_N]; int root_cnt 0; int roots4[4] {1, 3, 5, 7}; int i, j, tmp[GF_M]; for (i 0; i 4; i) { int cnt get_conjugates(roots4[i], tmp); for (j 0; j cnt; j) { if (!visited[tmp[j]]) { visited[tmp[j]] 1; roots[root_cnt] tmp[j]; } } } /* poly 1 */ gen_poly_deg 0; gen_poly[0] 1; for (i 0; i root_cnt; i) { /* poly poly * (x alpha^roots[i]) */ int exp_idx roots[i]; int new_poly[GF_M * 4 1] {0}; for (j 0; j gen_poly_deg; j) { new_poly[j] ^ gen_poly[j]; new_poly[j 1] ^ gf_mul(gen_poly[j], gf_exp[exp_idx]); } gen_poly_deg; for (j 0; j gen_poly_deg; j) gen_poly[j] new_poly[j]; } }注意这里gf_mul其实就是查指数表相加取模static uint8_t gf_mul(uint8_t a, uint8_t b) { if (!a || !b) return 0; return gf_exp[(gf_log[a] gf_log[b]) % GF_N]; }当所有系数都化为0或1后gen_poly就是生成多项式。对BCH(63,39)来说gen_poly_deg会恰好等于GF_M * 4 24也就是校验位数量。2.3 编码的本质异或除法求余BCH编码和CRC编码思路几乎一样把39位信息多项式左移24位再除以生成多项式余数就是24位校验位。整个过程是GF(2)上的多项式除法也就是只做异或不进位。void bch_encode(const uint8_t data[39], uint8_t codeword[63]) { int i, j; uint8_t remainder[24] {0}; for (i 0; i 39; i) codeword[i] data[i]; for (i 0; i 39; i) { uint8_t bit data[i]; uint8_t feedback bit ^ remainder[0]; memmove(remainder, remainder 1, 23); remainder[23] 0; if (feedback) { for (j 0; j 24; j) { if ((gen_poly[24 - 1 - j]) 1) remainder[j] ^ 1; } } } for (i 0; i 24; i) codeword[39 i] remainder[i]; }这段代码本质上和软件CRC没区别。对于熟悉CRC的人来说BCH编码没有任何新东西。3. 手写一套BCH(63,39,4)可运行源代码接下来是重头戏完整的译码流程包括伴随式计算、Berlekamp-Massey迭代求错误位置多项式、Chien搜索定位错误比特并翻转。这套流程是BCH的核心也是网上源码最容易藏bug的地方。3.1 伴随式计算检查接收码字是否有错把接收到的63位码字当作多项式R(x)分别计算R(α^1), R(α^2), …, R(α^8)。注意偶次幂可以直接用奇次幂的平方算但为了代码清晰我还是直接遍历。void compute_syndromes(const uint8_t codeword[63], uint8_t syndromes[8]) { int i, j; for (i 1; i 8; i) { uint8_t s 0; for (j 0; j 63; j) { if (codeword[j]) { int exp (i * j) % GF_N; s ^ gf_exp[exp]; } } syndromes[i - 1] s; } }如果8个伴随式全部为0说明接收码字是合法码字直接跳过纠错。注意这里“全部为0”的判断要用 0在GF上只有元素0才是0。3.2 BM迭代从伴随式反解错误位置多项式错误位置多项式σ(x)是译码的核心。BM算法本质上是寻找一个最短的线性反馈移位寄存器来复现伴随式序列理解不了也没关系直接背标准流程就行。关键注意两点偏差delta是σ(x)与伴随式的卷积更新时要把旧的σ保存下来这和线性反馈移位寄存器的“候选连接多项式”是对应的。int berlekamp_massey(const uint8_t syndromes[8], uint8_t lambda[5]) { int i, j; uint8_t B[5] {1, 0, 0, 0, 0}; uint8_t T[5]; int L 0, m 1; uint8_t delta; memset(lambda, 0, 5); lambda[0] 1; for (i 1; i 8; i) { delta syndromes[i - 1]; for (j 1; j L; j) delta ^ gf_mul(lambda[j], syndromes[i - 1 - j]); if (delta 0) { m; } else { memcpy(T, lambda, 5); for (j 0; j m 5; j) if (B[j]) lambda[j m] ^ gf_mul(delta, B[j]); if (2 * L i - 1) { L i - L; for (j 0; j 5; j) B[j] gf_mul(T[j], gf_exp[(GF_N - gf_log[delta]) % GF_N]); m 1; } else { m; } } } return L; }这里gf_exp[(GF_N - gf_log[delta]) % GF_N]是求delta的逆元。跑完BM后lambda[]就是错误位置多项式系数L是它的次数理论上不能超过4否则说明错误数超过纠错能力。3.3 Chien搜索暴力穷举的位置但可以高效迭代错误位置多项式有了接下来就是找哪些位置出错。理论上逐一代入σ(α^i)检查是否为0就行63个位置不算多。但硬件实现里通常用迭代方法每个时钟周期扫一个位置这就是Chien搜索。int chien_search(uint8_t lambda[5], int pos_out[4], int max_err) { int found 0; int i, j; for (i 0; i 63; i) { /* evaluate sigma(alpha^{-i}) sigma(alpha^{63-i}) */ uint8_t acc 0; for (j 0; j 5; j) { if (lambda[j]) { int exp (j * ((63 - i) % 63)) % GF_N; acc ^ gf_exp[exp]; } } if (acc 0) { if (found max_err) return -1; pos_out[found] i; } } return found; }对于二进制BCH找到错误位置之后直接翻转对应比特不需要计算错误值。这是二进制BCH和RS码最大的区别也是它的实现更简单的原因。3.4 完整纠错流程和误纠保护组合起来就是完整的bch_decodeint bch_decode(uint8_t codeword[63]) { uint8_t syndromes[8]; uint8_t lambda[5]; int pos[4], nerr, i; compute_syndromes(codeword, syndromes); int nonzero 0; for (i 0; i 8; i) if (syndromes[i]) nonzero 1; if (!nonzero) return 0; int L berlekamp_massey(syndromes, lambda); if (L 0 || L 4) return -1; nerr chien_search(lambda, pos, 4); if (nerr ! L) return -1; for (i 0; i nerr; i) codeword[pos[i]] ^ 1; /* 二次校验纠正后伴随式必须全为0 */ compute_syndromes(codeword, syndromes); for (i 0; i 8; i) if (syndromes[i]) return -2; return nerr; }第一次跑完纠错后我强烈建议再算一次伴随式做二次校验。原因很简单当错误数量超过4bit时BM算法可能收敛到一个错误的σ(x)Chien搜索也能找到对应的位置这时会把一个本来不能纠的码字“纠正”成另一个合法码字这就是误纠。二次校验能挡掉绝大多数误纠情况。4. 工程化落地缩短码、字节序和误纠那些坑手写Demo跑通很容易但真正放到NAND控制器或Flash驱动里立刻会撞上几个硬骨头。4.1 缩短码与高位填充的处理BCH(63,39)是理论上的本原码但实际产品很少有人直接用63位。Flash的扇区通常按512B、2KB、4KB组织需要把码长缩短到适合配页的尺寸。所谓缩短码就是固定后续的某几个高位置为0只在剩余位上放数据和校验。比如需要码长40位时可以取BCH(63,39)的前23位固定为0这就是一个缩短的BCH(40,16)。译码时这些高位不参与存储但在算法里要按0处理否则Chien搜索的位置索引会对不上。我踩过的坑编码器缩短后多项式除法虽然不用处理固定0的高位但Chien搜索必须从码字真实起点开始位置偏移一旦算错纠错结果全部错位。4.2 字节序与位序最容易翻车的两个方向这是所有纠错码应用里最阴间的坑。数据在内存里是byte数组但BCH算法处理的是bit流。到底bit0是字节的最低位还是最高位第一个字节是码字最高位还是最低位不同控制器厂商的约定完全不同。我的建议是在算法入口统一收敛到一个固定的位序约定。比如规定codeword[0]是最高位、codeword[62]是最低位字节写入时按MSB-first展开。否则今天在A平台调通移植到B平台立刻翻车。4.3 误纠判定宁可报错也不要改错前面提到二次校验这里再展开讲。工业场景里数据损坏了但被当成“已纠正”返回给上层比直接返回错误更可怕会造成静默数据损坏。所以我在产品代码里对返回状态做了严格区分返回0无错误返回正数纠正了n位错误返回-1错误数量过多或出错位次不合法返回-2纠正后伴随式仍不为0判定误纠。上层驱动看到-1和-2一律按不可纠正错误处理直接把坏块标记出来而不是把数据交出去。5. 从BCH(63,39)到实际存储控制器参数怎么选最后聊点参数规划的事。很多朋友拿到源码后第一个问题就是这个m到底选多大校验位多少够用5.1 不同场景下的码率、延时和面积权衡以GF(2^6)的BCH(63,39)为例纠4bit需要24位校验码率约为62%。如果是GF(2^10)的BCH码长可以到1023同样纠4bit时校验位需要40位信息位983位码率约96%。所以工程上更倾向于用大m因为校验位被摊薄码率更高。但m越大域表越大Chien搜索的位置范围也越大硬件查找电路更宽。对NAND控制器来说典型选择是GF(2^10)或GF(2^13)的BCH配合DMA和流水线把译码延迟压到几微秒以内。纯软件方案适合启动自检、离线校验这些非实时场景我实测在168MHz的Cortex-M4上跑一帧BCH(63,39)全流程大概零点几毫秒量级实时读写肯定不够。5.2 与DDR3 ECC内存条的对比顺便回应一下很多人问的“DDR3 ECC内存条和普通内存条区别”。DDR3 ECC内存条用的是汉明码或扩展汉明码属SEC-DED纠1bit错误、检测2bit错误。它的优势是延迟低能跟内存总线速度匹配但纠错能力远不如4bit BCH。所以两者不是替换关系内存条需要极致低延迟1bit纠错是性价比最优解NAND Flash读延迟本来就在几十微秒量级多花几微秒做4bit甚至更高强度的BCH完全划算。5.3 后续还能往哪个方向扩展如果这套BCH(63,39)源码跑通了扩展成更强的BCH并不难。把m改成8或10把生成多项式的根从1,3,5,7延长到1,3,5,7,9,11就能支持6bit、8bit纠错核心架构完全不用动。再往后走可以研究LDPC但那个起点就完全不一样了。我个人在实际使用中的体会是写BCH代码最花时间的不是算法本身而是构造一个能反复验证的测试环境。我习惯在PC上写一个随机错误注入的测试程序对每一帧随机翻转0到6个bit分别验证纠错成功、纠错失败、误纠三条路径的行为。这个测试跑过十万帧之后再往嵌入式平台移植就踏实很多。最后分享一个小技巧调试时不要一上来就调到4bit先把t改成1跑通最简单的BCH(63,57)汉明码场景再一步步往4bit调。每加一档纠错能力错误位置多项式的求解复杂度和边界条件都不一样逐级递进能帮你少走很多弯路。本文还有配套的精品资源点击获取
返回列表