3. 特征值存储与访问机制:本地GATT数据库实现
特征值是 GATT 服务中最核心的数据单元。嵌入式 BLE 协议栈需要在本地维护一份“GATT 数据库”,用于存储每个特征值的句柄、UUID、权限、缓存值等属性。本章聚焦两种典型的本地存储结构——静态数组与动态链表,并讲解如何注册读写回调函数,使上层应用能够响应 ATT 读写请求。
3.1 本地 GATT 数据库的基本组成
一个特征值在数据库中至少需要记录以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| handle | uint16_t | ATT 句柄,唯一标识该特征值 |
| uuid | uint16_t 或 128bit | 特征值 UUID(如 0x2A19 表示电池电量) |
| permissions | bitmask | 读/写/通知/指示权限 |
| value | uint8_t 数组 | 当前缓存的特征值数据 |
| value_len | uint16_t | 有效数据长度 |
| value_max_len | uint16_t | 存储缓冲区最大容量 |
| user_cb | function pointer | 读写回调函数指针 |
设计要点: 权限字段需同时包含“安全权限”(如是否需要加密、MITM)和“操作类型”(读、写、通知、指示)。常用实现采用 8 位或 16 位位图。
3.2 基于静态数组的 GATT 数据库
对于资源受限的 MCU(Cortex-M0,<64KB Flash),推荐在编译期定义固定大小的特征值表。每个特征值在数组中占用一个槽位。
3.2.1 数组定义与初始化
typedef struct {
uint16_t handle;
uint16_t uuid;
uint8_t permissions;
uint8_t value[32]; // 最大32字节
uint16_t value_len;
uint16_t value_max_len;
int (*read_cb)(uint16_t conn_handle, uint8_t *buf, uint16_t *len);
int (*write_cb)(uint16_t conn_handle, const uint8_t *buf, uint16_t len);
} gatt_char_t;
#define MAX_CHARS 8
static gatt_char_t gatt_db[MAX_CHARS];
static uint8_t char_count = 0;
3.2.2 注册特征值
static int battery_level_read(uint16_t conn_handle, uint8_t *buf, uint16_t *len) {
// 读取硬件ADC,填充buf,设置*len
*len = 1;
buf[0] = get_battery_level(); // 0~100
return 0; // 成功
}
static int battery_level_write(uint16_t conn_handle, const uint8_t *buf, uint16_t len) {
// 一般电池特征值不允许写,返回错误
return -1; // 不支持写
}
void app_register_battery_characteristic(void) {
gatt_char_t *ch = &gatt_db[char_count++];
ch->handle = 0x0010;
ch->uuid = 0x2A19;
ch->permissions = GATT_PERM_READ;
ch->value_len = 0;
ch->value_max_len = 1;
ch->read_cb = battery_level_read;
ch->write_cb = battery_level_write;
}
3.3 基于动态链表的 GATT 数据库
当特征值数量不确定,或需要运行时动态添加/删除服务时,使用单链表或双向链表更灵活。代价是每增加一个特征值需要 malloc 一个节点。
typedef struct gatt_char_node {
gatt_char_t data;
struct gatt_char_node *next;
} gatt_char_node_t;
static gatt_char_node_t *gatt_db_head = NULL;
int gatt_db_add(gatt_char_t *char_data) {
gatt_char_node_t *node = malloc(sizeof(gatt_char_node_t));
if (!node) return -1;
memcpy(&node->data, char_data, sizeof(gatt_char_t));
node->next = gatt_db_head;
gatt_db_head = node;
return 0;
}
适用场景提示: 静态数组适合 OTA 升级后特征值不变的应用(如健康手环);动态链表适合支持多种配置文件的通用 BLE 模块(如传感器网关)。
3.4 特征值读写回调函数注册机制
BLE 协议栈在收到 ATT Read Request 或 Write Request 时,需要调用用户提供的回调函数来获取最新值或写入数据。注册回调通常有两种方式:
- 全局回调表:协议栈直接扫描数据库,根据句柄找到对应的回调并调用。
- 句柄映射表:额外维护一个句柄→回调指针的哈希表或二分查找表。
以下示例展示如何在协议栈的 ATT 处理层中调用注册的回调:
// 协议栈内部:收到ATT读请求时的分发函数
int att_read_handler(uint16_t conn_handle, uint16_t attr_handle) {
for (int i = 0; i < char_count; i++) {
if (gatt_db[i].handle == attr_handle) {
if (gatt_db[i].read_cb) {
uint8_t buf[32];
uint16_t len = 0;
int ret = gatt_db[i].read_cb(conn_handle, buf, &len);
if (ret == 0) {
// 将buf通过ATT Read Response发送回对端
att_send_read_rsp(conn_handle, attr_handle, buf, len);
} else {
att_send_error_rsp(conn_handle, attr_handle, ATT_ERR_READ_NOT_PERMITTED);
}
}
break;
}
}
return 0;
}
3.5 本地 GATT 数据库访问流程
3.6 调试与验证
使用串口日志或 BLE Sniffer 验证数据库行为:
[ATT] Read Request handle=0x0010 length=0
[GATT] lookup handle=0x0010 -> found idx=2
[GATT] permission check: READ allowed
[GATT] calling read_cb (battery_level_read)
[GATT] read_cb returns 0, data=[0x64] len=1
[ATT] Read Response handle=0x0010 data=[0x64]
3.7 常见错误与避坑指南
| 错误现象 | 可能原因 | 解决措施 |
|---|---|---|
| 对方读特征值返回错误码 0x0A(Attribute Not Found) | GATT 数据库中未注册该句柄,或句柄被覆盖 | 检查数组/链表初始化,确保句柄唯一且未越界 |
| 写特征值后数据不更新 | write_cb 未写回 value 缓冲区 | 回调中更新 ch->value 和 ch->value_len |
| 多次注册后内存溢出(动态链表) | 未释放旧节点,或忘记 free | 增加节点销毁函数,在服务删除时释放 |
| 权限检查始终通过 | permissions 位掩码定义错误 | 使用 #define 定义常量,与协议栈枚举对齐 |
3.8 性能与稳定性优化建议
- 数组优先: 若无动态增减需求,优先使用静态数组,避免堆碎片。
- 查找加速: 若特征值超过 20 个,将数组按句柄排序,使用二分查找代替线性扫描。
- 原子操作: 如果回调在中断上下文被调用,需要保护 gatt_db 的访问(如禁用全局中断或使用临界区)。
- 缓存一致性: 若特征值由传感器循环更新,务必在 write_cb 中同步更新缓存值,并触发通知(如果需要)。
特别注意: 回调函数中不应执行阻塞操作(如延时、打印大量日志),否则会阻塞 ATT 协议栈,导致连接断开或超时。若需要较长时间处理,应启动异步任务,立即返回 pending 状态码。