【BlueZ 】util 模块:通用工具函数,源码中高频复用的基础组件

在 BlueZ 5.x 庞大的源码体系中,util 模块扮演着"幕后英雄"的角色。它不是直接处理蓝牙协议栈核心逻辑的主角,却是支撑所有模块运转的基础设施。从 HCI 命令封包的字节序处理、设备地址的格式转换,到 GATT 服务 UUID 的可读化显示、调试日志的十六进制输出,几乎每一个模块都在高频复用 util 模块提供的通用工具函数。


目录

[一、util 模块整体架构与定位](#一、util 模块整体架构与定位)

二、字符串处理工具:高效解析与分隔

[三、地址解析工具:BD 地址的生命周期管理](#三、地址解析工具:BD 地址的生命周期管理)

[四、位运算与 ID 管理:位图分配机制](#四、位运算与 ID 管理:位图分配机制)

[五、数据校验与错误映射:HCI 错误码的人性化](#五、数据校验与错误映射:HCI 错误码的人性化)

六、时间处理与定时器封装

七、内存管理:安全分配与零初始化

八、日志与调试工具

[九、UUID 与 Appearance 映射:协议数据的人类可读化](#九、UUID 与 Appearance 映射:协议数据的人类可读化)

十、字节序与非对齐访问:协议数据的基石

十一、跨模块支撑实战

十二、设计思想总结

十三、开发调试实战技巧

十四、总结


本文基于 BlueZ 5.x 完整源码,深入剖析 util 模块的设计哲学、核心实现与跨模块支撑价值。覆盖的源码文件包括:

  • lib/bluetooth.h--- 底层蓝牙数据类型定义与地址操作

  • lib/bluetooth.c--- BD 地址转换与错误码映射

  • src/shared/util.h --- 通用工具宏与函数声明

  • src/shared/util.c--- 工具函数实现与 UUID/Appearance 映射表

  • src/shared/timeout.h--- 定时器接口封装

  • src/shared/timeout-glib.c--- GLib 主循环适配实现


一、util 模块整体架构与定位

1.1 双层工具架构

BlueZ 的 util 模块并非单一文件,而是由两个层次的工具集组成:

|-----------|--------------------------------------------------|-----------------------------|-----------------------|
| 层次 | 源文件 | 核心职责 | 调用场景 |
| 协议底层层 | lib/bluetooth.h / lib/bluetooth.c | BD 地址操作、字节序转换、错误码映射 | HCI 收发、Socket 操作、地址校验 |
| 通用工具层 | src/shared/util.h / src/shared/util.c | 内存管理、位运算、UUID 映射、调试输出、字符串处理 | 几乎所有上层模块 |
| 时间管理层 | src/shared/timeout.h / src/shared/timeout-glib.c | 定时器封装、超时回调、GLib 主循环集成 | 连接超时、扫描定时、状态机转换 |

这种分层设计使得低层协议工具可以被 lib/ 下的 HCI、SDP 等底层库直接使用,而通用工具层则为 src/ 下的 BlueZ 守护进程(bluetoothd)提供服务。

1.2 跨模块复用全景图

通过 Grep 搜索 util_debugutil_hexdumpbt_uuid16_to_strstrdelimitstrsuffixutil_get_uidutil_clear_uid 等核心函数的调用,可见 util 模块被 94+ 个源文件引用,覆盖:

  • HCI 模块:命令封包时的字节序转换 (le16_to_cpu)、BD 地址字符串解析 (str2ba)

  • DBus 模块:路径字符串分隔 (strdelimit)、UUID 转换为可读字符串 (bt_uuid16_to_str)

  • 设备管理:BD 地址校验 (bachk)、Appearance 值解析 (bt_appear_to_str)

  • GATT/ATT 协议:128-bit UUID 处理 (bt_uuid128_to_str)、字节对齐访问 (get_unaligned)

  • 监控工具:HCI 数据包十六进制打印 (util_hexdump)、错误码转义 (bt_error)

  • 测试框架:内存安全分配 (util_malloc)、唯一 ID 分配 (util_get_uid)

二、字符串处理工具:高效解析与分隔

2.1 strdelimit:动态分隔符替换

cpp 复制代码
// src/shared/util.c:1314
char *strdelimit(char *str, char *del, char c)
{
    char *dup;

    if (!str)
        return NULL;

    dup = strdup(str);
    if (dup[0] == '\0')
        return dup;

    while (del[0] != '\0') {
        char *rep = dup;

        while ((rep = strchr(rep, del[0])))
            rep[0] = c;

        del++;
    }

    return dup;
}

设计亮点

  • 遍历 del 字符串中的每个分隔符,将其全部替换为统一字符 c

  • 自动处理空字符串边界情况

  • 返回新分配的字符串副本,调用方负责释放

典型应用场景 --- 在 shell.c中用于命令行参数解析:

cpp 复制代码
// src/shared/shell.c:330
str = strdelimit(arg, del, '"');

将多个不同的分隔符(如空格、Tab、逗号)统一替换为双引号,便于后续基于单一分隔符的 token 解析。

2.2 strsuffix:后缀判断的边界处理

cpp 复制代码
// src/shared/util.c:1337
int strsuffix(const char *str, const char *suffix)
{
    int len;
    int suffix_len;

    if (!str || !suffix)
        return -1;

    if (str[0] == '\0' && suffix[0] != '\0')
        return -1;

    if (suffix[0] == '\0' && str[0] != '\0')
        return -1;

    len = strlen(str);
    suffix_len = strlen(suffix);
    if (len < suffix_len)
        return -1;

    return strncmp(str + len - suffix_len, suffix, suffix_len);
}

健壮性设计

  • 四重边界检查(空指针、空字符串组合)

  • 长度预检查避免越界

  • 使用 strncmp 进行精确后缀匹配

调用场景 --- 命令行交互中判断续行符:

cpp 复制代码
// src/shared/shell.c:338
if (w->we_wordc && !strsuffix(w->we_wordv[w->we_wordc -1], "..."))

检查最后一个参数是否以 "..." 结尾,用于实现多行命令输入功能。

三、地址解析工具: BD 地址的生命周期管理

3.1 bdaddr_t 结构:6 字节的蓝牙地址

cpp 复制代码
// lib/bluetooth.h:336
typedef struct {
    uint8_t b[6];
} __attribute__((packed)) bdaddr_t;

__attribute__((packed)) 确保结构体无填充字节,可直接映射 HCI 数据包中的地址字段。

预定义特殊地址

cpp 复制代码
// lib/bluetooth.h:345-347
#define BDADDR_ANY   (&(bdaddr_t) {{0, 0, 0, 0, 0, 0}})
#define BDADDR_ALL   (&(bdaddr_t) {{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}})
#define BDADDR_LOCAL (&(bdaddr_t) {{0, 0, 0, 0xff, 0xff, 0xff}})

3.2 地址字节序转换:baswap

cpp 复制代码
// lib/bluetooth.c:29
void baswap(bdaddr_t *dst, const bdaddr_t *src)
{
    register unsigned char *d = (unsigned char *) dst;
    register const unsigned char *s = (const unsigned char *) src;
    register int i;

    for (i = 0; i < 6; i++)
        d[i] = s[5-i];
}

为什么需要字节交换? 蓝牙协议规范中 BD 地址的传输顺序与 Linux 内核存储顺序相反。HCI 数据包中地址以 LSB-first 传输,而 BlueZ 内部以 MSB-first 存储。baswap完成两个字节序的互相转换。

3.3 地址字符串双向转换

BD 地址转字符串

cpp 复制代码
// lib/bluetooth.c:39 --- 动态分配版本
char *batostr(const bdaddr_t *ba)
{
    char *str = bt_malloc(18);
    if (!str)
        return NULL;

    sprintf(str, "%2.2X:%2.2X:%2.2X:%2.2X:%2.2X:%2.2X",
        ba->b[0], ba->b[1], ba->b[2],
        ba->b[3], ba->b[4], ba->b[5]);

    return str;
}

// lib/bluetooth.c:65 --- 调用方提供缓冲区版本
int ba2str(const bdaddr_t *ba, char *str)
{
    return sprintf(str, "%2.2X:%2.2X:%2.2X:%2.2X:%2.2X:%2.2X",
        ba->b[5], ba->b[4], ba->b[3], ba->b[2], ba->b[1], ba->b[0]);
}

注意 batostrba2str 的字节顺序差异:batostr 直接按数组顺序输出,而 ba2str 反转了字节。这反映了 BlueZ 内部存在两种地址表示习惯,调用方需根据上下文选择正确的函数。

字符串转 BD 地址

cpp 复制代码
// lib/bluetooth.c:78
int str2ba(const char *str, bdaddr_t *ba)
{
    int i;

    if (bachk(str) < 0) {
        memset(ba, 0, sizeof(*ba));
        return -1;
    }

    for (i = 5; i >= 0; i--, str += 3)
        ba->b[i] = strtol(str, NULL, 16);

    return 0;
}

解析 "XX:XX:XX:XX:XX:XX" 格式字符串,包含严格的前置校验。

3.4 地址格式校验:bachk

cpp 复制代码
// lib/bluetooth.c:98
int bachk(const char *str)
{
    if (!str)
        return -1;

    if (strlen(str) != 17)
        return -1;

    while (*str) {
        if (!isxdigit(*str++))
            return -1;

        if (!isxdigit(*str++))
            return -1;

        if (*str == 0)
            break;

        if (*str++ != ':')
            return -1;
    }

    return 0;
}

严格校验 17 字符长度(6×2位 + 5个冒号)和 XX:XX:XX:XX:XX:XX 的正则格式,使用 isxdigit 确保十六进制合法性。

3.5 OUI 提取:ba2oui

cpp 复制代码
// lib/bluetooth.c:93
int ba2oui(const bdaddr_t *ba, char *str)
{
    return sprintf(str, "%2.2X-%2.2X-%2.2X", ba->b[5], ba->b[4], ba->b[3]);
}

提取 BD 地址的前 3 字节作为 OUI(Organizationally Unique Identifier),格式化为 ISO 9661 标准的连字符分隔格式(如 00-1A-7D),便于查询设备厂商信息。

四、位运算与 ID 管理:位图分配机制

4.1 BIT 宏与 ARRAY_SIZE

cpp 复制代码
// src/shared/util.h:19-20
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#define BIT(n)  (1 << (n))

这两个宏看似简单,却是 BlueZ 中高频使用的基础设施。BIT(n) 用于位标志定义,ARRAY_SIZE 用于数组大小计算,避免硬编码魔数。

4.2 唯一 ID 分配:util_get_uid

cpp 复制代码
// src/shared/util.c:169
uint8_t util_get_uid(uint64_t *bitmap, uint8_t max)
{
    uint8_t id;

    id = ffsll(~*bitmap);

    if (!id || id > max)
        return 0;

    *bitmap |= ((uint64_t)1) << (id - 1);

    return id;
}

核心算法解析

  1. ~*bitmap 取反位图,将"已分配"变为"未分配"

  2. ffsll() 找到最低位的 1(即第一个空闲位的位置)

  3. 检查 ID 是否在有效范围 [1, max]

  4. 置位该位表示"已分配"

这个算法时间复杂度为 O(1),利用 CPU 硬件指令 BSF(Bit Scan Forward)实现高效查找。适用于有限资源池的 ID 分配场景,如 L2CAP 通道 ID 管理、连接句柄分配等。

4.3 ID 释放:util_clear_uid

cpp 复制代码
// src/shared/util.c:184
void util_clear_uid(uint64_t *bitmap, uint8_t id)
{
    if (!id || id > 64)
        return;

    *bitmap &= ~(((uint64_t)1) << (id - 1));
}

对应的释放操作,通过位与运算清除指定位。边界检查确保 id 在有效范围内,防止位图损坏。

4.4 使用场景

在 GATT 数据库管理中,每个属性都需要唯一的句柄号。util_get_uid 确保句柄号不重复分配,util_clear_uid 在属性删除时回收句柄:

cpp 复制代码
// src/gatt-database.c 典型用法
uint8_t handle = util_get_uid(&bitmap, max_handles);
if (handle == 0) {
    // 资源耗尽
    return -ENOSPC;
}
// ... 使用 handle ...
util_clear_uid(&bitmap, handle);

五、数据校验与错误映射:HCI 错误码的人性化

5.1 bt_error:HCI 错误码转 Unix errno

cpp 复制代码
// lib/bluetooth.c:187
int bt_error(uint16_t code)
{
    switch (code) {
    case 0:
        return 0;
    case HCI_UNKNOWN_COMMAND:
        return EBADRQC;
    case HCI_NO_CONNECTION:
        return ENOTCONN;
    case HCI_HARDWARE_FAILURE:
        return EIO;
    case HCI_PAGE_TIMEOUT:
        return EHOSTDOWN;
    case HCI_AUTHENTICATION_FAILURE:
        return EACCES;
    case HCI_MEMORY_FULL:
        return ENOMEM;
    case HCI_CONNECTION_TIMEOUT:
        return ETIMEDOUT;
    // ... 更多错误码映射
    default:
        return ENOSYS;
    }
}

设计价值

  • 将蓝牙 HCI 规范定义的硬件错误码(如 HCI_PAGE_TIMEOUT = 0x04)转换为标准 POSIX errno

  • 使得上层应用可以用标准的 strerror(errno) 获取错误描述

  • 统一错误处理接口,屏蔽蓝牙协议栈特有的错误码体系

5.2 bt_compidtostr:厂商 ID 映射

cpp 复制代码
// lib/bluetooth.c:261
const char *bt_compidtostr(int compid)
{
    switch (compid) {
    case 0:
        return "Ericsson Technology Licensing";
    case 1:
        return "Nokia Mobile Phones";
    case 2:
        return "Intel Corp.";
    // ... 100+ 厂商映射
    }
}

映射 Bluetooth SIG 分配的 Company ID 到厂商名称字符串,用于 Read Remote Supported Features 等 HCI 命令返回信息的可读化。

六、时间处理与定时器封装

6.1 timeout 模块:统一的定时器接口

cpp 复制代码
// src/shared/timeout.h:16-17
unsigned int timeout_add(unsigned int timeout, timeout_func_t func,
            void *user_data, timeout_destroy_func_t destroy);
void timeout_remove(unsigned int id);

提供毫秒级和秒级两种精度的定时器注册接口:

cpp 复制代码
// src/shared/timeout.h:20
unsigned int timeout_add_seconds(unsigned int timeout, timeout_func_t func,
            void *user_data, timeout_destroy_func_t destroy);

6.2 GLib 适配实现

cpp 复制代码
// src/shared/timeout-glib.c:41
unsigned int timeout_add(unsigned int timeout, timeout_func_t func,
            void *user_data, timeout_destroy_func_t destroy)
{
    struct timeout_data *data;
    guint id;

    data = g_try_new0(struct timeout_data, 1);
    if (!data)
        return 0;

    data->func = func;
    data->destroy = destroy;
    data->user_data = user_data;

    id = g_timeout_add_full(G_PRIORITY_DEFAULT, timeout, timeout_callback,
                        data, timeout_destroy);
    if (!id)
        g_free(data);

    return id;
}

封装价值

  • 抽象层:BlueZ 上层代码无需直接依赖 GLib 的 g_timeout_add

  • 可移植性:可以替换为 timeout-ell.ctimeout-mainloop.c 实现

  • 资源管理:通过 timeout_destroy_func_t 回调机制,确保定时器销毁时正确释放关联数据

6.3 定时器在状态机中的应用

蓝牙连接建立过程涉及多个超时点:

  1. Page 扫描超时:timeout_add_seconds(10, page_timeout_cb, ...)

  2. 认证响应超时:timeout_add(3000, auth_timeout_cb, ...)

  3. 连接参数协商超时:timeout_add_seconds(5, params_timeout_cb, ...)

统一的 timeout 封装使得这些超时逻辑与主循环解耦,便于测试和移植。

七、内存管理:安全分配与零初始化

7.1 util_malloc:带断言的安全分配

cpp 复制代码
// src/shared/util.c:33
void *util_malloc(size_t size)
{
    if (__builtin_expect(!!size, 1)) {
        void *ptr;

        ptr = malloc(size);
        if (ptr)
            return ptr;

        fprintf(stderr, "failed to allocate %zu bytes\n", size);
        abort();
    }

    return NULL;
}

关键设计

  • __builtin_expect(!!size, 1):GCC 分支预测优化,提示编译器 size > 0 是大概率事件

  • 分配失败时直接 abort():避免了空指针泄漏到后续代码段引发更难调试的段错误

  • size == 0 时返回 NULL 而非 malloc(0) 的实现相关行为

7.2 util_memdup:内存复制安全封装

cpp 复制代码
// src/shared/util.c:49
void *util_memdup(const void *src, size_t size)
{
    void *cpy;

    if (!src || !size)
        return NULL;

    cpy = util_malloc(size);
    if (!cpy)
        return NULL;

    memcpy(cpy, src, size);

    return cpy;
}

先校验源指针和大小,再通过 util_malloc 分配并复制,形成安全的内存复制操作链。

7.3 new0 宏:零初始化的类型安全分配

cpp 复制代码
// src/shared/util.h:74
#define new0(type, count)           \
    (type *) (__extension__ ({      \
        size_t __n = (size_t) (count);  \
        size_t __s = sizeof(type);  \
        void *__p;          \
        __p = util_malloc(__n * __s);   \
        memset(__p, 0, __n * __s);  \
        __p;                \
    }))

这是一个 GNU 扩展语句表达式(({...}))宏,实现了类型安全的零初始化内存分配:

  • 类型安全:返回值强制转换为指定类型指针

  • 零初始化:memset 确保所有字节清零,避免未初始化内存引发的随机行为

  • 异常安全:util_malloc 分配失败时 abort,memset不会被执行

典型用法

cpp 复制代码
// 分配 10 个 device_info 结构,并全部清零
struct device_info *devices = new0(struct device_info, 10);

7.4 bt_malloc 与 bt_free:底层极简封装

cpp 复制代码
// lib/bluetooth.c:171-184
void *bt_malloc(size_t size)
{
    return malloc(size);
}

void *bt_malloc0(size_t size)
{
    return calloc(size, 1);
}

void bt_free(void *ptr)
{
    free(ptr);
}

底层蓝牙库使用的轻量级封装,直接映射到标准库函数。与 util_malloc 的区别在于:bt_malloc 不做断言检查,适合底层库在资源受限场景下使用(返回 NULL 由调用方处理)。

八、日志与调试工具

8.1 util_debug:格式化调试输出

cpp 复制代码
// src/shared/util.c:65
void util_debug_va(util_debug_func_t function, void *user_data,
                const char *format, va_list va)
{
    char str[78];

    if (!function || !format)
        return;

    vsnprintf(str, sizeof(str), format, va);

    function(str, user_data);
}

设计亮点

  • 函数指针回调机制:不直接打印到 stderr,而是通过 util_debug_func_t 回调将日志推送给上层

  • 固定缓冲区(78 字节):避免动态分配,适合嵌入式场景

  • va_list 版本支持自定义 printf 格式

调用方式示例:

cpp 复制代码
// 注册调试输出函数
util_debug(my_logger, NULL, "Connection established: %s", addr_str);

8.2 util_hexdump:数据包十六进制打印

cpp 复制代码
// src/shared/util.c:91
void util_hexdump(const char dir, const unsigned char *buf, size_t len,
                util_debug_func_t function, void *user_data)
{
    static const char hexdigits[] = "0123456789abcdef";
    char str[68];
    size_t i;

    if (!function || !len)
        return;

    str[0] = dir;  // 方向标记:'<' 接收,'>' 发送

    for (i = 0; i < len; i++) {
        str[((i % 16) * 3) + 1] = ' ';
        str[((i % 16) * 3) + 2] = hexdigits[buf[i] >> 4];
        str[((i % 16) * 3) + 3] = hexdigits[buf[i] & 0xf];
        str[(i % 16) + 51] = isprint(buf[i]) ? buf[i] : '.';

        if ((i + 1) % 16 == 0) {
            str[49] = ' ';
            str[50] = ' ';
            str[67] = '\0';
            function(str, user_data);
            str[0] = ' ';
        }
    }
    // ... 剩余不足 16 字节的处理
}

输出格式类似 hexdump -C

cpp 复制代码
> 04 0E 1D 00 11 22 33 44 55 66 77 88 99 AA BB CC  .............
> 02 01 06 03 0A 11 0B 0E 0C 0A 0A 0A 0A 0A 0A 0A  .............

其中 dir 参数标记数据方向(> 发送、< 接收),每行 16 字节,右侧显示 ASCII 可打印字符。这在 HCI 调试、GATT 数据抓取场景中不可或缺。

九、UUID 与 Appearance 映射:协议数据的人类可读化

9.1 UUID 映射体系

BlueZ 维护了三级 UUID 映射体系:

|-----------------------|----------------|-------------------------|------------------------------------|
| 函数 | 输入 | 映射表 | 用途 |
| bt_uuid16_to_str() | uint16_t | uuid16_table[] | 16-bit 标准 UUID(SDP, RFCOMM, HID 等) |
| bt_uuid32_to_str() | uint32_t | 间接调用 bt_uuid16_to_str | 32-bit UUID(高 16 位为 0 时退化为 16-bit) |
| bt_uuid128_to_str() | uint8_t[16] | uuid128_table[] | 128-bit 完整 UUID(GATT 服务/特征) |
| bt_uuidstr_to_str() | const char * | 综合判断 | 字符串格式 UUID 智能识别 |

uuid16_table:标准服务与 Profile 映射

cpp 复制代码
// src/shared/util.c:192
static const struct {
    uint16_t uuid;
    const char *str;
} uuid16_table[] = {
    { 0x0001, "SDP" },
    { 0x0003, "RFCOMM" },
    { 0x0007, "ATT" },
    { 0x0008, "OBEX" },
    { 0x001f, "MCAP Data Channel" },
    { 0x0100, "L2CAP" },
    { 0x1101, "Serial Port" },
    { 0x1105, "OBEX Object Push" },
    { 0x110d, "Advanced Audio Distribution" },
    { 0x110e, "A/V Remote Control" },
    { 0x111e, "Handsfree" },
    { 0x1124, "Human Interface Device Service" },
    { 0x1800, "Generic Access Profile" },
    { 0x1801, "Generic Attribute Profile" },
    { 0x180A, "Device Information" },
    { 0x180D, "Heart Rate" },
    { 0x180F, "Battery Service" },
    { 0x2800, "Primary Service" },
    { 0x2803, "Characteristic" },
    { 0x2A01, "Appearance" },
    // ... 300+ 条目
};

映射表覆盖了从基础协议(L2CAP, SDP)到高层 Profile(A2DP, HFP, HID)再到 GATT 服务的完整 UUID 体系。

bt_uuid128_to_str:128-bit UUID 转换

cpp 复制代码
// src/shared/util.c:1175
const char *bt_uuid128_to_str(const uint8_t uuid[16])
{
    char uuidstr[37];

    sprintf(uuidstr, "%8.8x-%4.4x-%4.4x-%4.4x-%8.8x%4.4x",
                get_le32(&uuid[12]), get_le16(&uuid[10]),
                get_le16(&uuid[8]), get_le16(&uuid[6]),
                get_le32(&uuid[2]), get_le16(&uuid[0]));

    return bt_uuidstr_to_str(uuidstr);
}

将 HCI/ATT 协议中的 128-bit UUID(按 little-endian 存储)转换为标准 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 格式,再通过 bt_uuidstr_to_str 查询映射表。

bt_uuidstr_to_str:智能字符串解析

cpp 复制代码
// src/shared/util.c:1187
const char *bt_uuidstr_to_str(const char *uuid)
{
    uint32_t val;
    size_t len;
    int i;

    if (!uuid)
        return NULL;

    len = strlen(uuid);

    // 短格式(16/32 位)
    if (len < 36) {
        char *endptr = NULL;

        val = strtol(uuid, &endptr, 0);
        if (!endptr || *endptr != '\0')
            return NULL;

        if (val > UINT16_MAX)
            return bt_uuid32_to_str(val);

        return bt_uuid16_to_str(val);
    }

    // 标准 36 位格式
    if (len != 36)
        return NULL;

    // 先查 128 位专用表
    for (i = 0; uuid128_table[i].str; i++) {
        if (strcasecmp(uuid128_table[i].uuid, uuid) == 0)
            return uuid128_table[i].str;
    }

    // 检查是否为标准 16/32 位 UUID 的 128 位展开形式
    if (strncasecmp(uuid + 8, "-0000-1000-8000-00805f9b34fb", 28))
        return "Vendor specific";

    if (sscanf(uuid, "%08x-0000-1000-8000-00805f9b34fb", &val) != 1)
        return NULL;

    return bt_uuid32_to_str(val);
}

这个函数实现了三级回退查找:

  1. 短格式直接按数值查找

  2. 128 位格式先查 uuid128_table(包含 Eddystone、MicroBit、Nordic UART 等专有 UUID)

  3. 最后检查是否为标准 16/32 位 UUID 的 128 位扩展形式(以 0000-1000-8000-00805f9b34fb 为后缀)

9.2 Appearance 映射:设备外观类型

cpp 复制代码
// src/shared/util.c:1228
static const struct {
    uint16_t val;
    bool generic;
    const char *str;
} appearance_table[] = {
    {    0, true,  "Unknown" },
    {   64, true,  "Phone" },
    {  128, true,  "Computer" },
    {  192, true,  "Watch" },
    {  256, true,  "Clock" },
    {  640, true,  "Media Player" },
    {  768, true,  "Thermometer" },
    {  832, true,  "Heart Rate Sensor" },
    {  960, true,  "Human Interface Device" },
    {  961, false, "Keyboard" },
    {  962, false, "Mouse" },
    {  963, false, "Joystick" },
    {  964, false, "Gamepad" },
    // ...
};

generic 字段标识是否为通用类别。查找逻辑先匹配具体 Appearance 值,若未匹配则回退到所属的通用类别:

cpp 复制代码
// src/shared/util.c:1290
const char *bt_appear_to_str(uint16_t appearance)
{
    const char *str = NULL;
    int i, type = 0;

    for (i = 0; appearance_table[i].str; i++) {
        if (appearance_table[i].generic) {
            if (appearance < appearance_table[i].val)
                break;
            type = i;
        }

        if (appearance_table[i].val == appearance) {
            str = appearance_table[i].str;
            break;
        }
    }

    if (!str)
        str = appearance_table[type].str;

    return str;
}

十、字节序与非对齐访问:协议数据的基石

10.1 字节序转换宏

cpp 复制代码
// src/shared/util.h:22-48
#if __BYTE_ORDER == __LITTLE_ENDIAN
#define le16_to_cpu(val) (val)
#define le32_to_cpu(val) (val)
#define le64_to_cpu(val) (val)
#define cpu_to_le16(val) (val)
#define cpu_to_le32(val) (val)
#define cpu_to_le64(val) (val)
#define be16_to_cpu(val) bswap_16(val)
#define be32_to_cpu(val) bswap_32(val)
#define be64_to_cpu(val) bswap_64(val)
#define cpu_to_be16(val) bswap_16(val)
#define cpu_to_be32(val) bswap_32(val)
#define cpu_to_be64(val) bswap_64(val)
#elif __BYTE_ORDER == __BIG_ENDIAN
// ... 相反的映射
#endif

BlueZ 同时维护两套字节序宏体系:

  • lib/bluetooth.hhtobs, btohs, bt_get_le16, bt_put_be32

  • src/shared/util.hle16_to_cpu, be32_to_cpu, get_le16, put_be32

前者用于底层蓝牙库,后者用于上层共享模块。命名风格的差异反映了代码演化历史。

10.2 非对齐访问宏

cpp 复制代码
// src/shared/util.h:52
#define get_unaligned(ptr)          \
__extension__ ({                \
    struct __attribute__((packed)) {    \
        __typeof__(*(ptr)) __v;     \
    } *__p = (__typeof__(__p)) (ptr);   \
    __p->__v;               \
})

#define put_unaligned(val, ptr)         \
do {                        \
    struct __attribute__((packed)) {    \
        __typeof__(*(ptr)) __v;     \
    } *__p = (__typeof__(__p)) (ptr);   \
    __p->__v = (val);           \
} while (0)

为什么需要非对齐访问? 蓝牙协议数据(HCI 包、ATT 帧)中的字段可能出现在任意字节偏移位置。在 ARM 等架构上,对齐访问会触发总线错误。attribute((packed)) 强制编译器生成逐字节访问代码。

10.3 内联 getter/setter 函数

cpp 复制代码
// src/shared/util.h:128
static inline uint16_t get_le16(const void *ptr)
{
    return le16_to_cpu(get_unaligned((const uint16_t *) ptr));
}

static inline uint32_t get_be32(const void *ptr)
{
    return be32_to_cpu(get_unaligned((const uint32_t *) ptr));
}

// 甚至支持 24 位字段
static inline uint32_t get_le24(const void *ptr)
{
    const uint8_t *src = ptr;
    return ((uint32_t)src[2] << 16) | get_le16(ptr);
}

这些内联函数将字节序转换、非对齐访问和类型转换封装为一体,在协议解析代码中实现零开销抽象:

cpp 复制代码
// ATT 响应解析中的典型用法
uint16_t handle = get_le16(&resp[1]);
uint8_t opcode = get_u8(&resp[0]);

10.4 bt_get_unaligned 体系

lib/bluetooth.h 提供了完整的 bt_get_*/bt_put_* 函数族,与 src/shared/util.h 中的 get_*/put_* 形成互补:

cpp 复制代码
// lib/bluetooth.h:211
static inline uint64_t bt_get_le64(const void *ptr)
{
    return bt_get_unaligned((const uint64_t *) ptr);
}

// 支持 128 位操作
static inline void bswap_128(const void *src, void *dst)
{
    const uint8_t *s = (const uint8_t *) src;
    uint8_t *d = (uint8_t *) dst;
    int i;
    for (i = 0; i < 16; i++)
        d[15 - i] = s[i];
}

十一、跨模块支撑实战

11.1 HCI 模块中的 util 调用链

cpp 复制代码
HCI 命令发送流程:
  bt_put_le16()    → 封装命令参数(字节序转换)
  bt_get_unaligned() → 读取事件响应(非对齐访问)
  ba2str()         → BD 地址转日志字符串
  bt_error()       → HCI 错误码转 errno
  util_hexdump()   → 原始数据包十六进制打印

lib/hci.c 中的 hci_send_cmd 为例:

cpp 复制代码
// lib/hci.c 典型模式
uint8_t cp[HCI_MAX_EVENT_SIZE];
// ... 填充命令参数 ...

// 发送 HCI 命令
if (hci_send_cmd(sock, OGF_LINK_CTL, OC_LINK_CTL_LE_CREATE_CONN, cp, sizeof(cp)) < 0) {
    bt_error(errno);  // 错误码转换
    return -1;
}

11.2 GATT 数据库中的 ID 管理

cpp 复制代码
// src/gatt-database.c
static uint64_t handle_bitmap = 0;

// 分配唯一属性句柄
uint8_t alloc_handle(void)
{
    uint8_t handle = util_get_uid(&handle_bitmap, 0xFFFF);
    if (handle == 0)
        return 0;
    return handle;
}

// 释放属性句柄
void free_handle(uint8_t handle)
{
    util_clear_uid(&handle_bitmap, handle);
}

11.3 设备扫描中的 Appearance 解析

cpp 复制代码
// src/device.c 典型用法
if (dev->appearance) {
    const char *app_name = bt_appear_to_str(dev->appearance);
    /* 用于 DBus 属性上报或日志 */
    g_print("Device %s appearance: %s\n", addr, app_name);
}

11.4 监控工具中的协议解析

monitor/ 模块中,util 工具被密集使用:

cpp 复制代码
// monitor/l2cap.c
static void l2cap_packet_handler(const unsigned char *data, uint16_t len)
{
    uint16_t psm = get_le16(&data[4]);
    uint16_t cid = get_le16(&data[6]);
    
    util_hexdump('>', data, len, my_debug_func, NULL);
    bt_uuid16_to_str(psm);  // PSM 映射到 Profile 名称
}

十二、设计思想总结

12.1 分层与抽象

BlueZ util 模块体现了清晰的分层原则:

  • 协议相关层(lib/bluetooth.*):紧贴蓝牙协议规范,处理地址、错误码等协议特有概念

  • 协议无关层(`src/shared/util.*`):提供通用编程工具,不依赖蓝牙协议知识

这种分层使得 lib/bluetooth.* 可被独立的蓝牙应用(如 hcitool、sdptool)复用,而 src/shared/util.* 服务于 BlueZ 守护进程及其插件体系。

12.2 防御式编程

几乎每个函数都包含严格的参数校验:

  • 空指针检查

  • 边界条件验证(空字符串、长度越界)

  • 枚举值范围检查

  • 格式合法性校验(bachk 的正则式校验)

这种防御式设计是嵌入式蓝牙协议栈稳定性的关键保障。

12.3 性能优先

虽然是通用工具层,但 util 模块在性能上做了多项优化:

  • ffsll() 硬件指令加速 ID 分配

  • __builtin_expect 分支预测提示

  • static inline 内联函数消除调用开销

  • 固定缓冲区避免堆分配(util_debug 的 78 字节栈缓冲)

  • 查表替代计算(UUID/Appearance 映射表)

12.4 可移植性

通过条件编译实现跨平台支持:

  • __BYTE_ORDER 大小端自适应

  • HAVE_GETRANDOM 运行时库版本适配

  • GLib/ell/mainloop 多种主循环后端

timeout-glib.ctimeout-ell.ctimeout-mainloop.c 三个实现文件完美展示了接口与实现分离的设计模式。

十三、开发调试实战技巧

13.1 自定义调试输出函数

cpp 复制代码
// 将 util_debug 输出重定向到自定义日志系统
void my_logger(const char *str, void *user_data)
{
    syslog(LOG_INFO, "%s", str);
}

// 注册调试回调
util_debug(my_logger, NULL, "Init complete, state=%d", state);

13.2 BD 地址安全校验

cpp 复制代码
// 在接受外部输入的地址前,务必使用 bachk 校验
int validate_address(const char *addr)
{
    if (bachk(addr) < 0) {
        fprintf(stderr, "Invalid BD_ADDR format: %s\n", addr);
        return -EINVAL;
    }
    return 0;
}

13.3 UUID 调试技巧

cpp 复制代码
// GATT 服务 UUID 调试
uint8_t gatt_uuid[] = {0x00, 0x18, 0x00, 0x00, ...}; // 128-bit
const char *name = bt_uuid128_to_str(gatt_uuid);
// 输出: "Generic Access Profile"

// 短 UUID 快速识别
const char *name16 = bt_uuid16_to_str(0x110e);
// 输出: "A/V Remote Control"

13.4 HCI 错误快速定位

cpp 复制代码
// 将 HCI 事件中的错误码转换为可读信息
uint8_t hci_error = event[2]; // 例如 0x05 (Authentication Failure)
int err = bt_error(hci_error);
// err = -13 (EACCES)
// 然后可以用 strerror(err) 获取标准错误描述

13.5 常见问题排查

问题 1:地址格式不匹配

  • 原因:混用了 batostr(不降序)和 ba2str(降序)

  • 解决:统一使用一种函数,或通过 baswap 明确转换

问题 2:UUID 查找返回 "Vendor specific"

  • 原因:128-bit UUID 的 Base UUID 后缀不匹配标准格式

  • 解决:检查 UUID 的后 28 字节是否为 0000-1000-8000-00805f9b34fb

问题 3:util_malloc 触发 abort

  • 原因:内存严重不足或 size 参数异常

  • 解决:检查调用链中是否存在 util_malloc(0) 的情况(返回 NULL),或系统内存状态

问题 4:get_unaligned 访问崩溃

  • 原因:传入了非协议数据的指针,且架构不支持非对齐访问

  • 解决:确保只对蓝牙协议数据包使用 get_unaligned,或在必要时手动对齐

十四、总结

BlueZ util 模块是一个看似简单却极为重要的基础组件体系。它由两层工具库加一个定时器框架构成,为整个蓝牙协议栈提供了:

  1. 地址生命周期管理:从字符串到字节流的双向转换与校验

  2. 协议数据安全访问:字节序转换与非对齐读写的统一封装

  3. 资源高效分配:基于位图的 O(1) ID 分配机制

  4. 人类可读映射:300+ UUID 和 Appearance 值的查表转换

  5. 调试基础设施:安全的日志输出与数据包十六进制打印

  6. 跨平台适配:大小端自适应、多主循环后端支持

理解 util 模块的设计思想和实现细节,对于深入掌握 BlueZ 协议栈的运作机制、开发自定义蓝牙 Profile、以及排查协议层问题都具有不可替代的价值。它不是"边角料",而是支撑整个蓝牙协议栈稳定运行的"隐式核心"。


核心源文件索引:

|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|
| 文件 | 关键函数/宏 | 功能分类 |
| lib/bluetooth.h | bdaddr_t, baswap, str2ba, bachk, bt_get_le16, bt_put_be32 | 地址操作、字节序转换 |
| lib/bluetooth.c | batostr, ba2str, bt_error, bt_compidtostr, bt_malloc, bt_free | 地址转换、错误映射、内存管理 |
| src/shared/util.h | ARRAY_SIZE, BIT, get_unaligned, new0, get_le16, put_be32 | 基础宏、非对齐访问、内联函数 |
| src/shared/util.c | util_malloc, util_memdup, util_debug, util_hexdump, util_get_uid, bt_uuid16_to_str, bt_appear_to_str, strdelimit, strsuffix | 内存管理、调试、ID 分配、映射表、字符串处理 |
| src/shared/timeout.h | timeout_add, timeout_remove, timeout_add_seconds | 定时器接口 |
| src/shared/timeout-glib.c | timeout_add (GLib 实现), timeout_remove | GLib 主循环定时器实现 |


相关推荐
Cicada12843 分钟前
分享我一直在用的一套 AI 记忆系统方案
大数据·数据库·人工智能
桃西西呀1 小时前
买房怕买贵?我把真实成交价丢进决策树,看它到底按什么给房子定价
人工智能·机器学习·llm
M78佐菲1 小时前
51单片机学习笔记(2)
linux·笔记·嵌入式硬件·学习·51单片机
jimmyleeee1 小时前
大模型安全之十:LLM无界消耗(Unbounded Consumption)
人工智能·安全
C++ 老炮儿的技术栈1 小时前
西门子 S7 常用存储区(I/Q/M/T/C/DB/PI/PQ)
linux·开发语言·c++·windows·qt·io
m0_462605221 小时前
大模型实战营week6
人工智能
广东帝工智能安防1 小时前
景区水域安全预警方案:BCAS架构在旅游场景的适配与落地
人工智能·安全·三维激光雷达·景区安全·雷视融合
2601_949950631 小时前
一套小程序,解决资料整理、在线刷题和错题复盘
人工智能·小程序·刷题·练习·小程序推荐
渡川见闻1 小时前
2026年工业生产运营软件推荐
人工智能