资讯动态

CC2540/CC2541 BLE密码配对实战:OSAL调度、SMP状态机与NV绑定

发布时间:2026/10/27 17:34:42 来源:尧图企业网站定制
简介本资源是一套面向嵌入式物联网开发者的BLE蓝牙单片机实战例程聚焦CC2540/CC2541芯片在OSAL操作系统下的主机-从机配对与绑定核心功能实现适用于具备C语言基础和蓝牙通信概念的中级开发者快速验证安全连接流程。压缩包共357个文件含127个C源码与146个头文件支撑协议栈分层逻辑、19个预编译库加速OSAL任务调度与BLE协议处理、10个IAR工程配置文件.xcl/.cfg及调试支持文件.dbgdt/.wsdt等整体8.82MB结构完整、注释详实便于理解gapbondmgr.c、gapperiphbondmgr.c等关键绑定模块的调用链与参数配置。已有81人学习下载读者可直接复现密码配对交互、硬件引脚映射代码中已明确定义、烧录调试全流程并参考配套bat批处理脚本如cc254x_ubl_pp.bat完成一键构建与固件生成为自定义传感器接入或低功耗优化提供可靠起点。1. CC2540/CC2541 OSAL 的 BLE 配对绑定实战为什么“输密码”在低功耗蓝牙里不是默认选项而你非得亲手把它焊进协议栈里很多人第一次打开这个.zip包看到OSAL、SMP、IO_CAP_DISPLAY_YESNO这些词就懵了——BLE 设备明明能“一键配对”为什么还要写几十个函数去处理 PIN 码输入、加密协商、绑定存储真相是CC2540/CC2541 的 TI BLE 协议栈v1.4.2 及之前默认不启用带密码的 Just Works 或 Passkey Entry 模式更不自动保存绑定信息它把“安全”做成一个可裁剪的模块而绝大多数例程直接砍掉了它。这个项目不是教你怎么点亮 LED而是解决一个真实产线痛点医疗手环必须输入 6 位 PIN 才能连上护士站主机门禁卡读头要记住每台 PDA 的长期密钥防止冒用工业传感器在换电池后不能丢失已授权的网关身份。它用 OSAL 任务调度驱动 SMSecurity Manager层把SM_PasskeyInput、SM_BondMgr、NV非易失存储三者拧成一股绳——不是调 API 就完事而是让osal_start_timerEx()定时触发密钥生成、让osal_msg_send()在SM_MSG_EVENT里塞入用户输入、让osal_nv_write()把 LTK/IRK/CSRK 写进 Flash 第 0x2000 偏移。适合正在啃 TI BLE 协议栈源码、被sm_linkEstablished()返回SM_AUTHREQ_MITM卡住、或需要把旧 CC2541 模块升级到 HIPAA 合规级安全的嵌入式工程师。新手能照着改simple_peripheral.c老手会盯着sm.h里SM_PAIRING_METHOD_PASSKEY的宏定义边界。2. 从裸机到 OSAL为什么必须用 OSAL 而不是裸写中断OSAL 任务调度与 BLE 安全事件的耦合逻辑TI 的 BLE 协议栈BLE-Stack v1.4.2不是 Linux 那种抢占式内核它的 OSALOperating System Abstraction Layer本质是一个基于优先级的协作式调度器靠osal_run_system()循环轮询tasksEvents[]数组。当你在simple_peripheral.c里调用GAPRole_StartDevice()它内部会注册GAPROLE_STATE_CHANGE事件而安全配对流程中产生的SM_MSG_EVENT必须由一个独立的 OSAL 任务来接收和分发——否则sm_processMsg()就永远收不到SM_PAIRING_REQUEST消息。这就是为什么所有“带密码配对”的例程都必须新建一个SM_TaskID而不是在PeripheralTask里硬塞SM_Init()。2.1 OSAL 任务注册与 SM 消息路由的硬编码约定OSAL 不允许动态创建任务 ID所有任务 ID 必须在OSAL_Tasks.h中静态声明并与tasksArr[]数组严格对齐。查看本例程的OSAL_Tasks.h你会发现// OSAL_Tasks.h - 必须与 tasksArr[] 顺序一致 #define SM_TASK_ID 3 // 注意不是 0,1,2PeripheralTask 占了前3个 #define PERIPHERAL_TASK_ID 0 #define GAPROLE_TASK_ID 1 #define GATT_TASK_ID 2而tasksArr[]在OSAL_SimpleAV.c中定义为const pTaskEventHandlerFn tasksArr[] { peripheralTask, // ID 0 gapRoleTask, // ID 1 gattTask, // ID 2 smTask, // ID 3 ← 这里必须是 smTask()且位置固定 NULL // 结束符 };提示如果你把smTask放在tasksArr[0]OSAL会把SM_MSG_EVENT发给peripheralTask导致sm_processMsg()永远不被执行——现象是设备能广播、能连接但一发起配对就超时断开串口打印SM: Pairing failed: 0x05 (Authentication failure)。这是新手最常翻车的第一步。2.2 SM_Task 的核心循环如何把“用户输入密码”变成“协议栈可识别的 Passkey”smTask()不是简单地while(1)它必须遵循 OSAL 的消息泵模式uint16 smTask(uint8 task_id, uint16 events) { if (events SYS_EVENT_MSG) { uint8 *pMsg; while ((pMsg osal_msg_receive(smTaskID)) ! NULL) { sm_processMsg((smMsg_t*)pMsg); osal_msg_deallocate(pMsg); } } if (events SM_START_PASSKEY_EVT) { // 关键此处触发用户交互 HalKeyConfig(KEY_ALL, KEY_MODE_INTERRUPT, sm_keyPressCB); LCD_WRITE_STRING(INPUT PIN:, 0, 0); osal_start_timerEx(smTaskID, SM_INPUT_TIMEOUT_EVT, 30000); // 30秒超时 } return (events ^ SYS_EVENT_MSG); }这段代码揭示了三个硬性约束sm_keyPressCB()是 HAL 层按键回调必须映射到物理按键如 P0_0~P0_3不能用 UART 输入模拟LCD_WRITE_STRING()调用的是hal_lcd.c的底层驱动若你的板子没接 LCD必须重写为 UART 提示如UART_printf(Enter 6-digit PIN: )SM_INPUT_TIMEOUT_EVT是自定义事件需在sm.h中添加#define SM_INPUT_TIMEOUT_EVT 0x0004且必须用osal_start_timerEx()启动不能用osal_set_event()—— 因为后者不带时间参数无法实现“30秒未输入则取消配对”。2.3 OSAL 与 GAP 层的握手为什么GAPBondMgr_SetParameter()必须在GAPRole_StartDevice()之后调用很多开发者把安全参数设置写在osalInitTasks()最开头结果配对始终走Just Works模式。原因在于GAPBondMgr初始化依赖GAP层的gapState状态机。正确顺序是// simple_peripheral.c 中的初始化顺序不可颠倒 void Peripheral_Init(uint8 task_id) { // ... 其他初始化 GAPRole_StartDevice(PeripheralCBs); // ← 必须先启动 GAP 角色 // 此时 GAP 的 gapState GAP_INIT_STATEGAPBondMgr 才准备好 uint8 pairMode GAPBOND_PAIRING_MODE_INITIATE; // 主动发起配对 uint8 mitm TRUE; // 启用 MITMMan-in-the-Middle protection uint8 ioCap GAPBOND_IO_CAP_DISPLAY_YESNO; // 显示确认 GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, pairMode, sizeof(uint8)); GAPBondMgr_SetParameter(GAPBOND_MITM_PROTECTION, mitm, sizeof(uint8)); GAPBondMgr_SetParameter(GAPBOND_IO_CAPABILITIES, ioCap, sizeof(uint8)); }GAPBondMgr_SetParameter()实际上是向GAPRole任务发送GAPROLE_BOND_PARAM_UPDATE消息该消息只有在GAPRole处于GAPROLE_STARTED状态时才会被处理。如果提前调用参数会被丢弃——现象是sm_processMsg()收到SM_PAIRING_REQUEST后sm_state却卡在SM_STATE_IDLE日志显示SM: No pairing parameters set。3. 密码配对全流程拆解从 GAP 连接请求到 SMP 加密完成的 7 个关键状态跃迁BLE 配对不是原子操作而是由 GAP 层触发、SMP 层执行、LL 层承载的跨层状态机。CC2540/CC2541 的 TI 协议栈将整个流程压缩为 7 个smState每个状态对应一次 LL 数据包交换和一次 OSAL 事件分发。理解这 7 步才能精准定位配对卡在哪一环。3.1 状态机全景图smState如何在sm.c中被驱动smState 值名称触发条件关键动作常见失败点SM_STATE_IDLE空闲初始状态等待SM_PAIRING_REQUEST若未调用GAPBondMgr_SetParameter()永远停在此态SM_STATE_WAIT_PAIR_REQ等待配对请求对方发PAIRING_REQ解析IO Cap、Oob、AuthReq字段若对方AuthReq中MITM0本端mitmTRUE会拒绝SM_STATE_SEND_PAIR_RSP发送配对响应本端决定接受配对构造PAIRING_RSP并调用sm_sendPairingResponse()若ioCap设置为DISPLAY_ONLY但对方是KEYBOARD_ONLY会返回0x08 (Unsupported features)SM_STATE_WAIT_CONFIRM等待确认值收到PAIRING_CONFIRM计算Confirm Value并比对sm_CalculateConfirmValue()依赖sm_pCB-randNum和sm_pCB-prand若 RNG 未初始化会返回 0SM_STATE_SEND_RANDOM发送随机数用户确认后发送PAIRING_RANDOM若sm_pCB-randNum未通过osal_rand()生成sm_SendRandom()会发送全 0SM_STATE_WAIT_RANDOM等待对方随机数收到PAIRING_RANDOM计算LTK并调用sm_generateLTK()sm_generateLTK()需sm_pCB-tk临时密钥若Passkey未正确注入tk为 0SM_STATE_ENCRYPT_LINK加密链路LTK生成成功调用HCI_LE_EnableEncryptionCmd()若NV存储失败sm_bondMgrWriteBondRecord()返回FALSE加密失败注意smState变化全部发生在sm_processMsg()内部你不能在smTask()中手动修改sm_pCB-smState。所有状态跃迁都由sm_processMsg()根据收到的消息类型自动推进。3.2 Passkey Entry 模式的特殊路径为什么SM_STATE_WAIT_CONFIRM之后不是SM_STATE_SEND_RANDOM当ioCap GAPBOND_IO_CAP_DISPLAY_YESNO且mitm TRUE时协议栈会走 Passkey Entry 流程此时状态机多出两个中间态SM_STATE_WAIT_PASSKEY等待用户输入 6 位数字sm_keyPressCB()每按一次键触发此态SM_STATE_SEND_PASSKEY将 6 位数字转为tkTemporary Key并调用sm_sendPasskey()发送PAIRING_CONFIRM关键代码在sm_processMsg()的case SM_PAIRING_CONFIRM_EVT:分支case SM_PAIRING_CONFIRM_EVT: if (sm_pCB-ioCap GAPBOND_IO_CAP_DISPLAY_YESNO) { // 显示确认值等待用户按 YES/NO 键 LCD_WRITE_STRING(Confirm:, 0, 0); LCD_WRITE_UINT32(sm_pCB-confirmVal, 1, 0); // 显示 6 位 Confirm Value } else if (sm_pCB-ioCap GAPBOND_IO_CAP_KEYBOARD_ONLY) { // 等待用户输入 Passkey sm_pCB-smState SM_STATE_WAIT_PASSKEY; osal_start_timerEx(smTaskID, SM_INPUT_TIMEOUT_EVT, 30000); } break;这里暴露了一个隐藏坑GAPBOND_IO_CAP_DISPLAY_YESNO模式下设备只显示 Confirm Value不显示 Passkey而GAPBOND_IO_CAP_KEYBOARD_ONLY模式下设备才需要输入 Passkey。如果你误设ioCap DISPLAY_YESNO却指望对方输入密码配对必然失败。3.3 绑定Bonding与加密Encryption的分离为什么配对成功后仍可能无法读取加密特征值配对Pairing只协商出LTKLong Term Key而绑定Bonding是将LTK、IRKIdentity Resolving Key、CSRKConnection Signature Resolving Key持久化到 NV 存储。很多例程只做配对不做绑定导致设备重启后需重新配对。本例程的绑定逻辑在sm_processMsg()的case SM_PAIRING_SUCCESS_EVT:中case SM_PAIRING_SUCCESS_EVT: // 1. 生成 Bond Record 结构体 bondRec.devAddr sm_pCB-devAddr; bondRec.ltk sm_pCB-ltk; bondRec.irk sm_pCB-irk; bondRec.csrk sm_pCB-csrk; // 2. 写入 NV地址 0x2000 osal_nv_write(ZCD_NV_BOND_ID, 0, sizeof(bondRec), bondRec); // 3. 启动加密 HCI_LE_EnableEncryptionCmd(sm_pCB-connHandle, sm_pCB-randNum, sm_pCB-encLK); break;其中ZCD_NV_BOND_ID是 TI 协议栈预定义的 NV 项 ID通常为0x0020必须与hal_board_cfg.h中HAL_NV_PAGE_SIZE对齐。若osal_nv_write()返回FALSE说明 NV 写入失败——常见原因是 Flash 页未擦除CC2541 的 NV 存储需整页擦除或ZCD_NV_BOND_ID超出HAL_NV_PAGE_END边界。4. 绑定数据持久化NV 存储的 3 种写法与 CC2540/CC2541 Flash 页管理的血泪经验TI 的 OSAL NVNon-Volatile机制不是标准 SPI Flash 驱动而是将片内 Flash 的特定页Page划分为多个 16 字节槽Slot每个槽存一个 NV 项。CC2540/CC2541 的 Flash 页大小为 2KB2048 字节一页最多存 128 个 NV 项2048 / 16。但实际可用页数取决于 Bootloader 占用——标准 TI Bootloader 占用 Page 00x0000–0x07FF所以ZCD_NV_PAGE通常设为 Page 10x0800–0x0FFF。4.1 NV 项定义与地址映射为什么ZCD_NV_BOND_ID必须是偶数NV 项 ID 不是内存地址而是槽索引。osal_nv_write(id, offset, len, buf)的id参数会被转换为物理地址// hal_flash.c 中的地址计算逻辑 uint16 nvItemAddr (ZCD_NV_PAGE * HAL_FLASH_PAGE_SIZE) (id * NV_ITEM_SIZE);其中NV_ITEM_SIZE 16。因此ZCD_NV_BOND_ID 0x0020→ 物理地址 0x0800 (0x20 * 16) 0x0800 0x200 0x0A00ZCD_NV_BOND_ID 0x0021→ 地址 0x0A10但0x0A10不是 16 字节对齐的起始地址会导致写入越界血泪经验所有ZCD_NV_*_ID必须是 16 的倍数即id % 16 0否则osal_nv_write()会静默失败。本例程中ZCD_NV_BOND_ID定义为0x0020正是为了对齐。4.2 三种 NV 写法对比osal_nv_write()、osal_nv_item_init()与halFlashWrite()方法适用场景是否需手动擦除安全性本例程采用osal_nv_write(id, offset, len, buf)写入已存在的 NV 项否自动处理高带 CRC 校验✅ 绑定记录写入osal_nv_item_init(id, len, buf)首次初始化 NV 项是需先halFlashErasePage(ZCD_NV_PAGE)中无 CRC❌ 未使用halFlashWrite(addr, len, buf)直接写 Flash 任意地址是必须整页擦除低无校验易损坏 Bootloader❌ 禁止本例程在sm_bondMgrWriteBondRecord()中使用osal_nv_write()因为它会自动检查槽是否为空、写入后校验 CRC。而新手常犯的错误是在main()开头直接调用halFlashWrite(0x0A00, 16, bondRec)结果擦除了 Page 1 的前半页导致后续osal_nv_write()失败——现象是配对成功但NV无记录重启后需重配。4.3 NV 数据恢复如何从 Flash 中读取已绑定的设备列表绑定不是单设备而是支持多设备。TI 协议栈用ZCD_NV_BOND_ID作为基址后续设备依次用ZCD_NV_BOND_ID 1,ZCD_NV_BOND_ID 2... 但本例程只存单设备所以读取逻辑极简bondRec_t bondRec; if (osal_nv_read(ZCD_NV_BOND_ID, 0, sizeof(bondRec), bondRec) SUCCESS) { // 成功读取用 bondRec.ltk 启动加密 HCI_LE_EnableEncryptionCmd(connHandle, bondRec.randNum, bondRec.encLK); } else { // NV 为空走首次配对流程 GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, pairMode, sizeof(uint8)); }osal_nv_read()返回SUCCESS表示槽非空且 CRC 校验通过。若返回INVALIDPARAMETER说明ZCD_NV_BOND_ID无效若返回FAILURE说明槽为空或 CRC 错误——此时应清空该槽并重试osal_nv_delete(ZCD_NV_BOND_ID)。5. 避坑指南CC2540/CC2541 BLE 配对绑定的 5 个致命陷阱与现场排查口诀配对失败不是“不行”而是“卡在某个状态”。以下 5 条是我在 3 个医疗设备项目中踩过的坑每条都附带串口日志特征、根因和 1 行修复命令。5.1 现象串口打印SM: Pairing failed: 0x05 (Authentication failure)连接后 3 秒断开原因GAPBondMgr_SetParameter(GAPBOND_MITM_PROTECTION, mitm, sizeof(uint8))中mitm设为FALSE但对方设备如 Android 手机强制要求 MITM。解决将mitm改为TRUE并确保ioCap与对方匹配手机通常为KEYBOARD_ONLY你的设备需设为DISPLAY_YESNO或KEYBOARD_DISPLAY。验证命令在Peripheral_Init()中加UART_printf(MITM%d, IOCap%d\r\n, mitm, ioCap);5.2 现象配对流程走到SM_STATE_WAIT_CONFIRMLCD 显示 6 位 Confirm Value但按 YES 键无反应原因HalKeyConfig()注册的sm_keyPressCB()未正确映射到物理按键或按键消抖时间过长导致sm_keyPressCB()被多次调用sm_pCB-smState被意外覆盖。解决在sm_keyPressCB()开头加状态守卫void sm_keyPressCB(uint8 keys) { if (sm_pCB-smState ! SM_STATE_WAIT_CONFIRM sm_pCB-smState ! SM_STATE_WAIT_PASSKEY) return; // ... 后续逻辑 }验证命令用逻辑分析仪抓 P0_0 引脚确认按键按下时sm_keyPressCB()只触发 1 次。5.3 现象配对成功SM: Pairing success日志出现但NV中无绑定记录重启后需重配原因osal_nv_write()调用时ZCD_NV_BOND_ID超出 Page 1 范围0x0800–0x0FFF写入地址0x1000以上覆盖了 Bootloader 或其他 NV 项。解决检查hal_board_cfg.h中ZCD_NV_PAGE 1并确认ZCD_NV_BOND_ID计算后地址 0x1000// 计算0x0800 (0x0020 * 16) 0x0A00 0x1000 ✓ // 错误示例0x0020 * 16 0x200 → 0x0800 0x200 0x0A00 ✓若 id0x0100 → 0x0800 0x1000 0x1800 ✗验证命令用 SmartRF Flash Programmer 读取 Page 10x0800–0x0FFF搜索0x0A00附近是否有非 0xFF 数据。5.4 现象Android 手机配对时弹出“输入 6 位密码”但 CC2541 侧无任何提示直接超时失败原因ioCap设为GAPBOND_IO_CAP_DISPLAY_ONLY但手机期望KEYBOARD_ONLY双方IO Cap不匹配导致PAIRING_RSP返回0x08。解决将ioCap改为GAPBOND_IO_CAP_KEYBOARD_DISPLAY既可显示 Confirm Value又可输入 Passkey并在sm_keyPressCB()中支持数字键输入。验证命令用 nRF Connect App 连接进入 Pairing 页观察其显示的IO Capability是否与代码一致。5.5 现象绑定后能加密通信但读取某特征值时返回0x80 (Insufficient Authentication)原因该特征值的属性Attribute设置了GATT_PERMIT_AUTHEN_READ但GAPBondMgr未在加密后自动更新安全级别。解决在SM_PAIRING_SUCCESS_EVT处理完后手动调用GAPBondMgr_SetParameter(GAPBOND_AUTHENTICATED, auth, sizeof(uint8)); // auth TRUE验证命令用gatttool -b XX:XX:XX:XX:XX:XX --char-read -a 0x0025若返回handle: 0x0025 value: ...则成功若返回Characteristic value/descriptor read request failed: Insufficient Authentication则失败。6. 进阶技巧用 Wireshark Ubertooth One 实时抓取 BLE 配对包定位 SMP 层协议异常Wireshark 本身不支持 BLE 抓包必须配合 Ubertooth One或 nRF52840 Dongle做空中抓包。本例程的配对流程能否跑通最终要落到LL层的PAIRING_REQ/PAIRING_RSP/PAIRING_CONFIRM/PAIRING_RANDOM四个 PDU 上。光看串口日志是黑匣子抓包才是后悔药。6.1 Ubertooth One 抓包环境搭建3 行命令搞定Ubertooth One 需刷入ubertooth-one固件并用ubertooth-btbb工具转为 PCAP 格式# 1. 刷固件Linux 下 ubertooth-util -r # 重置设备 ubertooth-firmware -f ubertooth-one-2018-03-R1.dfu # 刷最新固件 # 2. 启动抓包监听 37/38/39 信道-c 37 指定信道-l 1000000 采样率 ubertooth-btbb -c 37 -l 1000000 -w capture.pcap # 3. Wireshark 打开 capture.pcap过滤 ble # 过滤表达式btle btle.advertising_header.pdu_type 0x05 # SCAN_REQ # 或btle btle.llid 1 btle.length 0 # 数据包注意Ubertooth One 默认只抓广告包要抓连接态数据包需在ubertooth-btbb后加-r参数启用连接跟踪需目标设备在范围内主动连接。6.2 配对包关键字段解读从 Wireshark 中一眼识别失败环节打开capture.pcap按btle过滤找到PAIRING_REQ包展开Bluetooth Low Energy Link Layer→Data→Security Manager Protocol字段正常值异常表现诊断意义Io capability0x04(DisplayYesNo)0x00(DisplayOnly)本端ioCap设置错误Authentication requirements0x01(Bonding MITM)0x00(No bonding)GAPBondMgr_SetParameter()未生效Max encryption key size0x10(16 bytes)0x08(8 bytes)对方设备不支持 AES-128需降级Initiator key distribution0x01(LTK only)0x00对方不希望分发密钥绑定失败若PAIRING_REQ后无PAIRING_RSP说明本端未响应——查smState是否卡在SM_STATE_WAIT_PAIR_REQ若PAIRING_CONFIRM的Confirm Value两端不一致说明sm_CalculateConfirmValue()输入的randNum/prand不同步——查 RNG 是否被复位。6.3 自定义日志增强在sm.c中插入 3 行调试桩替代 Wireshark若无 Ubertooth可在sm_processMsg()关键节点加 UART 日志case SM_PAIRING_REQUEST_EVT: UART_printf(SM: REQ from %02X%02X%02X%02X%02X%02X\r\n, pMsg-pPairingReq-devAddr[0], pMsg-pPairingReq-devAddr[1], pMsg-pPairingReq-devAddr[2], pMsg-pPairingReq-devAddr[3], pMsg-pPairingReq-devAddr[4], pMsg-pPairingReq-devAddr[5]); break; case SM_PAIRING_CONFIRM_EVT: UART_printf(SM: CONFIRM0x%08X\r\n, sm_pCB-confirmVal); break; case SM_PAIRING_RANDOM_EVT: UART_printf(SM: RAND0x%016llX\r\n, *(uint64_t*)sm_pCB-randNum); break;这样你就能在串口看到CONFIRM值是否与 Wireshark 抓到的一致——不一致说明sm_pCB-randNum被意外修改一致则问题在对方设备。我干这行八年每次配对失败第一反应不是改代码而是抓包看PAIRING_CONFIRM。因为sm_processMsg()的状态机再复杂也逃不过LL层那几个字节。Wireshark 不是高级玩具它是 BLE 开发者的 X 光机。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑