[toc]
Linux 内核 BCD 转换工具解析
[lib/bcd.c] [BCD 转换(bcd2bin/bin2bcd)] [提供 1 字节 BCD 与二进制数之间的高效互转实现,并导出为内核通用符号]
介绍
lib/bcd.c 只有两件事:实现并导出两个函数 _bcd2bin() 与 _bin2bcd()。它们通常通过 include/linux/bcd.h 里的宏 bcd2bin() / bin2bcd() 被驱动代码调用:
- 常量参数:走
const_*宏,编译期直接折叠; - 非编译期常量:走
_bcd2bin/_bin2bcd,运行期完成转换,同时尽量避免代价更高的除法/取模路径。
BCD(Binary-Coded Decimal,二进制编码十进制)就是:用 4 个二进制位表示 1 个十进制数字(0~9)。常见是把一个字节拆成两个“半字节”(nibble):
- 高 4 位:十位数字
- 低 4 位:个位数字
例子
十进制 45
- 十位是 4 → 二进制 0100
- 个位是 5 → 二进制 0101
- 合在一起:
0x45(也就是0100 0101b)
0x59的 BCD 含义:高 4 位 5、低 4 位 9 → 十进制 59
和普通二进制数的区别
同样的字节:
0x45作为二进制数是十进制 690x45作为BCD表示十进制 45
快速判断 BCD 是否“合法”
一个字节是合法 BCD 当且仅当:
- 低 4 位 < 10
- 高 4 位 < 10
比如0x1A不合法(A=10 不是十进制数字)。
你来做个小判断题:0x3C 作为 BCD 合法吗?(只答“合法/不合法”即可)
历史与背景
这项技术为了解决什么问题而诞生?
很多硬件寄存器(尤其是时间/日期类字段)用 BCD(Binary-Coded Decimal) 存储:一个字节的高 4 位表示十位、低 4 位表示个位。驱动读寄存器后需要把 BCD 转成普通整数;写寄存器前需要把整数转回 BCD。lib/bcd.c 提供统一、轻量的实现,避免各驱动重复实现并保证行为一致。
重要里程碑或迭代特征(从代码形态可见)
- 接口稳定、极小实现:函数很短,且
EXPORT_SYMBOL说明它设计为通用能力供大量模块复用。 - 编译期常量折叠 + 运行期优化并存:头文件宏选择路径,使常量场景零运行期开销,非常量场景尽量减少指令成本。
社区活跃度和主流应用情况
这类工具属于“基础库”,改动频率通常不高,但使用面很广(大量驱动会用到)。你可以把它视为内核长期稳定的通用小工具之一。
主要优势体现在哪些方面?
- 体积小、可复用:两函数非常短,适合作为基础设施。
- 性能友好:非编译期常量路径尽量避免除法/取模(对某些架构除法代价更高)。
- 可被编译器进一步优化:函数声明通常带“纯函数/const”属性语义(不读写全局状态),利于优化与公共子表达式消除。
已知劣势、局限性、不适用性
只覆盖 1 字节 BCD 的典型语义(两位十进制):不直接处理多字节 BCD(例如 4 位十进制或更长)。
输入有效性要由调用者保证:
bcd2bin若输入 nibble 超过 9,会产生“非十进制意义”的数值;bin2bcd若输入超过两位十进制范围,组合出的 nibble 可能不符合 BCD 语义。
一般应在读硬件寄存器后先做bcd_is_valid()之类的校验,或保证写入前数值在合法范围。
使用场景
首选场景举例
- RTC/时间日期类寄存器:秒/分/时/日/月/年常以 BCD 存放,驱动读出后用
bcd2bin()得到整数,写回前用bin2bcd()编码。 - 某些 PMIC/传感器/控制器的配置寄存器:如果寄存器字段以 BCD 表达十进制阈值或计数,同样适用。
不推荐使用的场景?为什么?
- 把它当“通用十进制编码”:它只针对“每 4 bit 表示一个十进制数字”的 BCD 模式,不负责字符串格式化或多位十进制处理。
- 输入可能超出 0~99 的场景:如果你不明确约束范围,
bin2bcd的输出可能不是合法 BCD。
对比分析
这里把它与三种常见替代方案对比(以“实现方式、性能开销、资源占用、隔离级别、启动速度”的口径类比到算法层面):
1) vs 朴素实现(t = val/10; ones = val%10;)
- 实现方式:朴素实现最直观;
lib/bcd.c用乘法/移位近似除法。 - 性能开销:在除法昂贵的架构上,乘法+移位通常更便宜;在除法已经很快的架构上差异可能不大。
- 资源占用:两者都几乎为零;
lib/bcd.c不需要表。 - 隔离级别:都是纯计算;差异主要在“对输入范围的隐含要求”上。
- 启动速度:无差异(无需初始化)。
2) vs 查表(0~99 预生成 BCD 表)
- 实现方式:查表 O(1) 且简单;但需要静态表数据。
- 性能开销:查表需要一次内存访问;在某些场景下算术可能更稳定。
- 资源占用:查表占用只读数据段空间;当前实现几乎不占数据空间。
- 隔离/启动:同样无需初始化。
3) vs 字符串/格式化途径(sprintf/解析)
- 实现方式:不适配寄存器语义,路径更重。
- 性能开销:明显更高。
- 资源占用:代码路径和栈使用都更大。
- 隔离/启动:不适合作为底层驱动寄存器编码方式。
总结
关键特性
- 提供 BCD↔整数的基础转换;
- 常量场景可编译期折叠,运行期场景尽量避免除法;
- 假设目标是 1 字节、两位十进制的典型硬件寄存器用法;
- 输入合法性通常由调用者或上层逻辑保证(需要时用校验宏)。
_bcd2bin / _bin2bcd:BCD 与二进制转换中的位域拆分与“乘法近似除法”技巧
在很多 Cortex-M(尤其是未开启硬件除法或除法代价较高的配置)上,用乘法+移位替代除法常见于内核这类基础库;这里 _bin2bcd() 的实现正是这种技巧的典型例子。
_bcd2bin:把一个 BCD 字节转为二进制
1 | /** |
_bin2bcd:把二进制转为 BCD(关键点在 t 的计算)
1 | /** |
为什么 (val * 103) >> 10 能当作 val / 10 用(在常用范围内)
核心是把除法转成“乘以 1/10 的近似倒数”:
- 选择
k=10,因为2^10 = 1024 1024 / 10 = 102.4,用整数常数103近似102.4- 于是
val / 10近似为val * 103 / 1024,再右移 10 位就是除以 1024
更关键的是:对 RTC 场景(秒/分/时/日/月/年)常见 val<=99,这个近似可以证明“不会跨过下一个整数”,因此 floor(val*103/1024) 恰好等于 floor(val/10):
令 q = floor(val/10),则 val 落在 [10q, 10q+9],且 q<=9(因为 val<=99)。
- 下界:
(10q)*103/1024 = q + (6q)/1024,显然 ≥ q - 上界:
(10q+9)*103/1024 = q + (6q+927)/1024
对q<=9,有6q+927 <= 981 < 1024,因此< q+1
所以结果落在 [q, q+1),取 floor 必为 q,也就是正确的十位数。
bcd.h 里的宏:为什么要区分 const_* 与 _* 版本
1 |
__builtin_constant_p(...):如果编译器能确定参数是编译期常量,就直接用const_bin2bcd/const_bcd2bin,这样/、%会在编译期折叠,不产生运行时代价。- 否则走
_bin2bcd():用乘法+移位,避免运行时除法/取模的开销。









