十六进制转BCD码,进制很多人第一次写代码处理这个问题时都会掉进同一个坑:把0x1F直接当成十进制31,进制然后拆成3和1。进制但BCD码不是进制这么玩的。BCD(Binary-Coded Decimal)的进制本质是用4个二进制位表示一个十进制数字,也就是进制说,一个字节里的进制高4位和低4位各自代表0到9之间的一个数。0x1F这种十六进制数,进制如果硬转成BCD,进制会得到非法结果,进制因为F不是进制一个合法的BCD数字。
我见过一个真实的进制案例。某电表采集程序需要把十六进制电表读数转成BCD码打包上传。进制开发人员写了一句bcd = ((hex / 10) << 4) | (hex % 10),进制看起来没问题。进制但电表返回的0x1A这个值,除法按十进制算得到2和6,组装成0x26。而实际上0x1A是26,转BCD应该是0x26——刚好对了。但换一个值0x64,十进制是100,除法得10和0,再移位组装,低4位变成0,高4位是10,也就是0xA0。这就出问题了,10不是合法的BCD高半字节。程序在后续解析时直接崩溃。
正确的做法是先把十六进制数转成十进制数值,再对十进制数值做BCD编码。以0x64为例,它的十进制值是100。拆成百位1、十位0、个位0,BCD表示为0x0100(两个字节)。如果只用一个字节,最多表示0到99,100就溢出了。所以十六进制转BCD,第一步永远是确认数值范围,第二步才是编码。
具体算法没那么玄乎。假设十六进制数存在变量hexVal里,先算十进制值:decVal = hexVal(如果hexVal本身就是数值类型,这一步是隐式的)。然后取百位、十位、个位:
int hundreds = decVal / 100; int tens = (decVal / 10) % 10; int ones = decVal % 10; uint16_t bcd = (hundreds << 8) | (tens << 4) | ones; 注意这里用16位来存,因为百位需要占据高字节的低4位。如果确定数值不超过99,用8位就够了:bcd = (tens << 4) | ones。
还有一种情况是十六进制字符串转BCD。比如“1F”这个字符串,它表示的数值是31。但很多人会直接取字符'1'和'F'的ASCII码,然后做位操作,结果全乱。正确路径是先用strtol把字符串转成整数31,再按上面的方法转BCD。或者更直接:把字符串里的每个字符当成十六进制数字解析,拼出完整数值,再转BCD。两种方式等价,但前者更不容易出错。
我个人的习惯是写一个辅助函数,输入是uint32_t类型的十进制数值,输出是uint8_t数组或uint16_t。这样无论上游给的是十六进制还是十进制,只要数值对,BCD就错不了。另外,BCD转十六进制是逆过程:把高4位乘以10加上低4位,得到十进制数值,再按需要转成十六进制显示。这两个方向都别偷懒,别想着用位运算一步到位——BCD的4位边界和十六进制的4位边界虽然看起来一样,但语义完全不同。
最后提一个容易忽略的点:BCD码里0x0A到0x0F是非法值。如果你在调试时看到某个字节低4位是0x0C,那说明上游数据或者转换逻辑已经出错了。这时候别急着往下算,先回头检查输入是不是被当成了十六进制数值直接拆位。这个坑我踩过不止一次。