在这里插入图片描述

[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 作为二进制数是十进制 69
  • 0x45 作为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
2
3
4
5
6
7
8
9
10
11
/**
* @brief 将 8-bit BCD(高 4 位十位,低 4 位个位)转换为二进制。
*
* @param val 输入 BCD:例如 0x59 表示十进制 59。
* @return 十进制数值:例如返回 59。
*/
unsigned _bcd2bin(unsigned char val)
{
/* 低 4 位为个位;高 4 位为十位(乘 10)。 */
return (val & 0x0f) + (val >> 4) * 10;
}

_bin2bcd:把二进制转为 BCD(关键点在 t 的计算)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @brief 将二进制值转换为 8-bit BCD(十位<<4 | 个位)。
*
* @param val 输入十进制数值(通常期望在 0..99 范围内)。
* @return BCD 编码结果。
*
* @note t 的计算通过“乘以倒数并右移”来近似 val/10,
* 在 val<=99 这类 RTC 常用取值范围内可保证得到正确的十位数。
*/
unsigned char _bin2bcd(unsigned val)
{
/* 近似:t ≈ floor(val / 10),用 (val * 103) >> 10 实现 */
const unsigned int t = (val * 103) >> 10;

/* t 为十位;val - t*10 为个位 */
return (t << 4) | (val - t * 10);
}

为什么 (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
2
#define bcd2bin(x) (__builtin_constant_p((u8)(x)) ? const_bcd2bin(x) : _bcd2bin(x))
#define bin2bcd(x) (__builtin_constant_p((u8)(x)) ? const_bin2bcd(x) : _bin2bcd(x))
  • __builtin_constant_p(...):如果编译器能确定参数是编译期常量,就直接用 const_bin2bcd/const_bcd2bin,这样 /% 会在编译期折叠,不产生运行时代价。
  • 否则走 _bin2bcd():用乘法+移位,避免运行时除法/取模的开销。