把二进制数变成BCD码,进制听起来像是码计教科书里才有的冷知识。但你要是算器拆过数字电压表,或者调过老式电子秤的进制校准程序,就会明白这玩意儿有多实在——MCU读到的码计ADC原始值永远是二进制,而七段数码管只认BCD。算器中间那层转换逻辑,进制写不好就是码计满屏乱跳的字符。
我见过最野的算器写法是在中断里硬算。工程师图省事,进制直接调库函数做除法,码计结果每次转换吃掉几百微秒,算器刷新率一高,进制显示就开始闪烁。码计后来换成查表法,算器把0到9999的BCD结果预先存成数组,速度是快了,但4KB的Flash说没就没。真正靠谱的做法,其实是那套老掉牙的“加3移位法”(double-dabble),用移位和加法就能搞定,连乘法器都用不上。
原理不复杂。二进制数每左移一位,相当于乘以2。但BCD每一位只能表示0到9,一旦某位超过4,继续移位就会越过边界。这时候提前加3,等于把进位预支掉。举个例子:二进制1010(十进制10),左移一位变成10100(20),如果按BCD拆开,高位该是2,低位该是0。但直接移位后低四位是0100(4),高四位是0001(1),完全对不上。要是移位前先给低四位加3,0101变成1000,再左移,低四位变0000,高四位变0010,结果就成了0x20,正好对应BCD的“20”。
这套逻辑用C语言写出来也就二十行。核心循环处理每一位,每次先检查每个BCD位是否大于4,大于就加3,然后整体左移一位。关键点在于从最高位开始处理,因为左移会改变位权。有个朋友曾经把顺序写反,从最低位开始移,结果转换出来的数值总是差一截,调了整整一个下午。后来他在代码注释里画了个箭头,标明“从MSB开始”,再也没犯过同样的错。
实际工程里还有个坑——输入范围。如果你只处理8位二进制,最大值255,BCD结果顶多三位。但要是面对16位ADC,65535转成BCD得五位,循环次数得跟着变。我见过有人把循环次数写死成8,输入一大就溢出,显示直接归零。正确的做法是先算出输入位宽,再决定循环次数,或者干脆用while循环,直到所有原始位都处理完毕。
有人可能会问,现代单片机那么多硬件外设,不是有专门的BCD调整指令吗?像x86的DAA,或者ARM的某些DSP扩展。但嵌入式环境千奇百怪,低成本MCU往往没有这些玩意儿。这时候,纯软件的加3移位法反而是最通用的。而且它有个隐藏优势——不占额外RAM,原地操作寄存器就行。对那种只有128字节内存的8位机来说,这点很关键。
最后说个观察。很多教程爱把这段代码封装成函数,参数传指针,返回状态码,看起来高大上。但实际调用的地方,往往就在一个主循环里,每次转换固定调用一次。与其搞那么复杂,不如直接定义成宏,或者内联函数,省掉函数调用开销。编译器优化开-O2之后,效果差不多,但代码可读性反而更好。别为了“规范”牺牲掉直观性,毕竟调试的时候,谁都不想翻三层函数调用栈去找一个移位错误。
二进制转BCD这事儿,本质上就是跟进位较劲。理解了加3的意图,剩下的就是循环和移位。写多了你会发现,很多看似高深的外设驱动,底层逻辑也不过如此。