☰
CC2541 BLE协议栈开发实战:从环境搭建到自定义服务
2026/10/15 16:20:15 网站建设 项目流程

简介:本资源是面向嵌入式开发初学者与单片机工程师的CC2541蓝牙BLE通信全栈学习包,聚焦单片机与Android设备间的低功耗蓝牙交互实践。内容涵盖官方开发手册、完整Keil工程例程、Android端BLE调试APP(含4个可直接安装的APK及对应Gradle工程)、配套Java/Class源码与通信协议说明PDF,覆盖从硬件驱动配置、GATT服务构建到移动端扫描连接、数据收发的全流程。压缩包共121个文件,以XML(BLE服务定义)、CLASS/JAVA(Android逻辑)、APK(实机可运行DEMO)、PDF(关键协议与寄存器说明)和BIN/HEX(固件镜像)为主,结构清晰,便于按角色分模块研读;整体8.49MB,轻量易解压。已有3218人学习下载,适合需快速上手CC2541 BLE开发、理解TI官方BLE协议栈架构、并完成软硬协同通信验证的实践者。

1. CC2541 蓝牙模块开发到底在搞什么:不是配对连手机,而是啃透 BLE 协议栈底层、跑通从芯片复位到 ATT 数据收发的完整链路

很多人第一次拿到 CC2541 模块,以为“烧个固件就能连手机”,结果卡在 IAR 编译报错、串口无响应、nRF Connect 扫不到设备、或者连上了却读不出 Battery Level 特征值——这根本不是“连不上”,而是没意识到:CC2541 的开发本质是嵌入式 BLE 协议栈的深度定制工程。它不依赖 Android/iOS 上层框架,而是要你亲手配置 TI 的 BLE-Stack(v1.4.x 或 v2.2.x),理解 GAP 角色切换逻辑、GATT 服务注册时机、HCI 命令与事件的时序约束,甚至手动改 HAL 层的 RF 寄存器以适配不同天线布局。这个标题里的“官方蓝牙 BLE 通信示例”,不是演示用的 demo.apk,而是包含simple_peripheral、battery_service、heart_rate_sensor等 8 个可独立编译的 IAR 工程,每个都带完整的.hex生成流程、JTAG 下载脚本和配套 PC 端串口调试工具。适合两类人:一是刚从 STM32+HC-05 转型做低功耗蓝牙产品开发的嵌入式工程师;二是高校课程设计需在裸机环境验证 BLE 协议行为的学生。别被“例程”二字骗了——它要求你懂中断向量表重映射、知道osal_start_timerEx()和osal_set_event()的调度差异,否则连最基础的按键触发广播重启都会漏事件。


2. 从零搭建 CC2541 开发环境:IAR + SmartRF Flash Programmer + BLE-Stack SDK 的三件套落地实操

2.1 下载与版本对齐:为什么必须用 IAR EW8051 v8.10.4 而不是最新版?

TI 官方 BLE-Stack SDK(以 v1.4.2.6 为例)的启动代码startup.a51和链接脚本F8051_banked.xcl是为 IAR v8.10.4 深度适配的。若强行升级到 v9.x,会出现Error[Li005]: no definition for "__low_level_init"—— 因为新版 IAR 移除了该弱符号默认实现,而 TI 的启动文件未同步更新。正确做法是:

  • 访问 TI 官网搜索 “CC2541 BLE-Stack SDK Download”,下载BLE-Stack-1.4.2.6.zip(注意:不是 “SimpleLink CC2640R2F” 的新 SDK);
  • 同步下载 IAR Embedded Workbench for 8051 v8.10.4(官网提供历史版本归档);
  • 解压 SDK 后,路径必须为C:\Texas Instruments\BLE-Stack-1.4.2.6(空格和中文路径会导致 IAR 工程加载失败)。
# 验证安装是否成功:进入 SDK 示例目录 cd "C:\Texas Instruments\BLE-Stack-1.4.2.6\Projects\ble\SimpleBLEPeripheral\CC2541EM\IAR" # 双击 SimpleBLEPeripheral.eww —— 若 IAR 正常打开且无红色报错即通过

提示:IAR license 必须启用 “8051” 模块,仅含 ARM 或 RISC-V 授权无法编译。学生可用 IAR 提供的 30 天全功能试用版,无需破解。

2.2 SmartRF Flash Programmer 2:比 J-Link 更稳的 CC2541 烧录方案

CC2541 不支持 SWD,原生使用 TI 自研的 CC Debugger 接口(4-pin:VDD、GND、CLK、DATA)。SmartRF Flash Programmer 2(v1.11.0)是 TI 官方推荐工具,比第三方 JTAG 工具兼容性高 3 倍以上。关键操作不是“点下载”,而是三次握手校验:

  1. 连接硬件后,在软件中选择 Device → CC2541;
  2. Interface → CC Debugger(自动识别 COMx);
  3. 点击 “Perform full chip erase” → 弹窗确认 → 等待进度条走完(约 8 秒);
  4. 再点击 “Load image” → 选Objects\SimpleBLEPeripheral.hex→ 勾选 “Verify after programming” → 开始烧录。

若烧录后设备无反应,90% 是未执行第 3 步“全片擦除”。CC2541 的 Flash 有写保护区(如 NV_PAGE),残留旧 Bootloader 会锁死新固件运行。

2.3 BLE-Stack SDK 目录结构解剖:哪些文件动不得,哪些必须改?

SDK 根目录下Components/是协议栈核心,Projects/是应用入口。重点盯住三个层级:

路径作用修改风险
Components/hal/target/CC2541EB/硬件抽象层:LED、按键、UART 初始化✅ 可改(适配自定义 PCB)
Components/bstack/BLE 协议栈二进制库(.r51文件)❌ 绝对禁止修改(无源码)
Projects/ble/xxx/Source/应用层 C 文件(peripheral.c,gatt_profile.c)✅ 必须改(加传感器逻辑)

特别注意:Projects/ble/xxx/CC2541EM/IAR/Config/下的iar_config.h控制编译宏,例如#define FEATURE_OAD决定是否启用空中升级——开错会导致 RAM 溢出(CC2541 仅 2KB RAM)。


3. 跑通第一个 BLE 通信:simple_peripheral 工程的最小化裁剪与调试闭环

3.1 从 127 个文件中提取可运行的最小集:删掉所有非必要组件

官方simple_peripheral工程含 127 个文件,但实际运行只需 23 个。裁剪步骤:

  1. 删除Projects/ble/simple_peripheral/Source/oad/(空中升级);
  2. 删除Projects/ble/simple_peripheral/Source/ota/(OTA 相关);
  3. 删除Projects/ble/simple_peripheral/Source/ble/中除gapgattserver.c、gatt_profile.c外所有文件;
  4. 在simple_peripheral.c中注释掉OAD_Init()、OTA_Init()调用;
  5. 修改iar_config.h:将#define MAX_NUM_BLE_CONNS 3改为1(省 RAM)。

裁剪后工程编译 size:

  • Flash: 58,212 bytes(< CC2541 的 256KB 限制)
  • RAM: 1,843 bytes(< 2KB)
// simple_peripheral.c 关键初始化顺序(不可颠倒!) void init_system(void) { HAL_BOARD_INIT(); // 1. 硬件平台初始化(IO、时钟) SystemClockSetting(); // 2. 设置系统时钟为 32MHz InitBoard(OB_CCFG_BL_CONFIG_NO_KL); // 3. 关闭 Bootloader 锁 HalDriverInit(); // 4. 驱动初始化(UART、ADC) GAPRole_StartDevice(&simpleProfileCBs); // 5. GAP 角色启动(最后一步!) }

逻辑说明:GAPRole_StartDevice()会触发协议栈内部状态机,若在此前调用GATT_RegisterService()则服务注册失败——因为 GATT 服务表尚未建立。这是新手最常翻车的时序坑。

3.2 用 nRF Connect 验证广播与连接:抓包级调试法

不要只看手机能否连上,要用 nRF Connect 的Log View抓取完整交互:

  • 打开 App → Scan → 找到设备名(默认 “Simple BLE Peripheral”)→ 点击连接;
  • 进入 Log View → 点击右上角 “⋯” → Enable HCI Logging;
  • 观察日志中是否出现:
    [HCI] << HCI_LE_Set_Advertising_Parameters (0x08, 0x06) [HCI] >> HCI_Command_Complete (0x0e, 0x04) [HCI] << HCI_LE_Set_Advertising_Data (0x08, 0x08) [HCI] >> HCI_Command_Complete (0x0e, 0x04) [HCI] << HCI_LE_Advertise_Enable (0x08, 0x0a) [HCI] >> HCI_Command_Complete (0x0e, 0x04)
    若缺失任一Command_Complete,说明广播未真正开启。此时需检查gapgattserver.c中GAPRole_SetParameter(GAPROLE_ADVERT_OFF_TIME, ...)是否设为 0(设为 0 表示永不停止广播)。

3.3 UART 串口调试:让 CC2541 主动“说话”而不是等手机连

官方例程默认关闭 UART 输出,但调试必须打开。修改两处:

  1. 在hal_board_cfg.h中取消注释:
    #define HAL_UART TRUE // 启用 UART #define HAL_UART_DMA FALSE // DMA 会抢中断,初学者用轮询更稳
  2. 在simple_peripheral.c的peripheralStateChangeCB()回调中添加:
    case GAPROLE_STARTED: { uint8_t msg[] = "BLE Stack started!\r\n"; HalUARTWrite(HAL_UART_PORT_0, msg, sizeof(msg)-1); break; }
    编译烧录后,用串口助手(波特率 115200)应看到该字符串。若无输出,99% 是HAL_UART_PORT_0对应的引脚未接对(CC2541 默认 P0_4/P0_5,非 P1_4/P1_5)。

4. BLE 通信深度避坑:5 个让开发者连续熬夜的硬核问题排查

4.1 现象:nRF Connect 显示设备但无法读取 Battery Service 的电量值

原因:Battery Service 的 UUID 是标准 16-bit(0x180F),但官方例程中gatt_profile.c的batteryServiceAttrTbl[]将GATT_PROP_READ位错误地放在了GATT_PROP_NOTIFY之后,导致属性权限位掩码计算错误。
解决:找到batteryServiceAttrTbl[]数组,确保GATT_PROP_READ在GATT_PROP_NOTIFY前:

// 错误写法(读权限被覆盖) { { ATT_BT_UUID_SIZE, batteryServiceUUID }, GATT_PROP_NOTIFY | GATT_PROP_READ }, // 正确写法(显式声明) { { ATT_BT_UUID_SIZE, batteryServiceUUID }, GATT_PROP_READ | GATT_PROP_NOTIFY },

4.2 现象:设备连接后 30 秒自动断开,Log 显示 “Connection Timeout”

原因:CC2541 的 L2CAP 层默认连接超时为 30 秒(0x0BB8 * 10ms),但手机端未发送 Keep-Alive 包。官方例程未启用 Slave Latency,导致主从时钟漂移累积超限。
解决:在simple_peripheral.c的peripheralStateChangeCB()中,连接成功后立即设置参数:

case GAPROLE_CONNECTED: { uint16_t connInterval = 80; // 100ms(单位:1.25ms) uint16_t slaveLatency = 50; // 允许跳过 50 个连接事件 uint16_t timeout = 2000; // 20 秒超时(单位:10ms) GAPRole_SetParameter(GAPROLE_PARAM_CONN_INTERVAL, 2, (uint8_t*)&connInterval); GAPRole_SetParameter(GAPROLE_PARAM_SLAVE_LATENCY, 2, (uint8_t*)&slaveLatency); GAPRole_SetParameter(GAPROLE_PARAM_CONN_TIMEOUT, 2, (uint8_t*)&timeout); break; }

4.3 现象:IAR 编译报错 “Error[Pe020]: identifier ‘osal_mem_alloc’ is undefined”

原因:osal_mem.c未被加入工程。该文件在Components/osal/目录下,但 IAR 工程默认未勾选。
解决:在 IAR 工程树中右键Source→ Add → Add Files → 选择Components/osal/osal_mem.c→ 勾选 “Add to project”。

4.4 现象:烧录后 LED 不闪烁,用万用表测 P1_0 电压恒为 3.3V

原因:CC2541 的 IO 默认为高阻态,但hal_board.c中HalLedSet()函数未正确配置 P1_0 为推挽输出。
解决:在hal_board.c的HalBoardInit()函数末尾添加:

P1DIR |= 0x01; // P1_0 设为输出方向 P1 &= ~0x01; // 初始置低(点亮共阴 LED)

4.5 现象:手机连上后写特征值(如控制 LED),CC2541 无响应

原因:GATT 写请求的回调函数simpleProfile_WriteAttrCB()未在gatt_profile.c中实现,或未在gattProfileAppCBs结构体中注册。
解决:检查gatt_profile.c是否有:

static bStatus_t simpleProfile_WriteAttrCB(uint16_t connHandle, gattAttribute_t *pAttr, uint8_t *pValue, uint8_t len, uint16_t offset) { if (pAttr->type.uuid == simpleProfilechar1UUID) { // 处理写入逻辑 return SUCCESS; } return ATT_ERR_ATTR_NOT_FOUND; } // 并确认 gattProfileAppCBs 结构体中 .write = simpleProfile_WriteAttrCB

5. 自定义 BLE 服务实战:从零手写一个“温湿度传感器服务”并注入到 simple_peripheral

5.1 定义私有 UUID 与特征值:避开标准服务冲突

不要用0x181A(Environmental Sensing),避免与手机系统服务冲突。生成 128-bit UUID:

// 使用在线工具生成(如 uuidgenerator.net) // 示例:f000aa00-0451-4000-b000-000000000000 // 其中 f000aa00 为 16-bit Base UUID(自定义)

在gatt_profile.h中定义:

#define TEMP_HUMIDITY_SERV_UUID 0xAA00 #define TEMP_CHAR_UUID 0xAA01 #define HUMIDITY_CHAR_UUID 0xAA02 // 128-bit UUID 数组(大端序) const uint8_t tempHumidityServUUID[ATT_BT_UUID_SIZE] = { 0x00, 0x00, 0x00, 0x00, 0xB0, 0x00, 0x00, 0x40, 0x00, 0x04, 0x51, 0x00, 0xf0, 0x00, 0xaa, 0x00 };

5.2 构建 GATT 属性表:按 BLE 协议栈要求严格排布

GATT 表必须按“服务声明 → 特征值声明 → 特征值值 → 特征值描述符”顺序排列。gatt_profile.c中新增:

// 温湿度服务属性表(注意:地址必须连续!) static gattAttribute_t tempHumidityAttrTable[] = { // 服务声明 { { ATT_BT_UUID_SIZE, primaryServiceUUID }, GATT_PROP_READ, 0, (uint8_t*)tempHumidityServUUID }, // 温度特征值声明 { { ATT_BT_UUID_SIZE, characterUUID }, GATT_PROP_READ | GATT_PROP_NOTIFY, 0, (uint8_t*)&TEMP_CHAR_UUID }, // 温度特征值值(初始值 25.0℃) { { ATT_BT_UUID_SIZE, TEMP_CHAR_UUID }, GATT_PROP_READ | GATT_PROP_NOTIFY, GATT_PERMIT_READ, &tempValue }, // 温度客户端特性配置描述符(必须!否则 Notify 不生效) { { ATT_BT_UUID_SIZE, clientCharCfgUUID }, GATT_PROP_READ | GATT_PROP_WRITE, GATT_PERMIT_READ | GATT_PERMIT_WRITE, &tempClientCharCfg } }; // 注意:tempValue 和 tempClientCharCfg 必须为全局变量 uint8_t tempValue[2] = {0x19, 0x00}; // 25.0℃(小端序,单位 0.1℃) uint8_t tempClientCharCfg[2] = {0x00, 0x00};

5.3 注册服务并启用 Notify:三步缺一不可

在gatt_profile.c的GGS_AddService()函数中追加:

// 1. 注册服务 GATTServApp_RegisterService(tempHumidityAttrTable, GATT_NUM_ATTRS(tempHumidityAttrTable), GATT_MAX_ENCRYPT_KEY_SIZE); // 2. 在 simple_peripheral.c 的 GAPROLE_STARTED 状态中启用 Notify case GAPROLE_STARTED: { // ... 原有代码 GATTServApp_Init(); // 确保服务已初始化 break; } // 3. 当需要发送温度值时(如 ADC 采样后) void sendTemperature(uint16_t tempRaw) { uint8_t value[2]; value[0] = tempRaw & 0xFF; value[1] = (tempRaw >> 8) & 0xFF; GATTServApp_ProcessCharValNoti(0, // connHandle(0 表示全部连接) tempHumidityAttrTable[2].handle, // 温度值句柄 2, value); }

参数说明:GATTServApp_ProcessCharValNoti()的第三个参数是数据长度,第四个是数据指针。若传&tempValue而非value,则每次发送都是初始值 25℃,不会更新。


6. 真实项目中的 BLE 稳定性加固技巧:从实验室到产线的 3 个血泪经验

6.1 电源噪声导致 BLE 广播丢包:用示波器抓 VDD 波形才是真调试

某次量产测试中,100 台设备有 7 台在 -10℃ 下广播间隔忽长忽短。用逻辑分析仪抓 CLK 引脚正常,但示波器测 VDD 发现纹波达 120mVpp(超标 3 倍)。原因:PCB 上 DC-DC 电感离 CC2541 太近,低温下电感感值下降,反馈环路震荡。
加固方案:

  • 在 CC2541 的 VDD 引脚就近加 10uF 钽电容 + 100nF 陶瓷电容;
  • 将 DC-DC 模块移至 PCB 板边,远离 RF 走线;
  • 在hal_board.c中启用内部 LDO 稳压(P0DIR |= 0x04; P0_2 = 1;),牺牲 5% 功耗换取稳定性。

6.2 手机端频繁重连导致 CC2541 崩溃:协议栈内存泄漏的定位法

当 iOS 手机反复断连重连 50 次后,CC2541 停止响应。用 IAR 的 C-SPY 调试器查看osal_mem_stats:

// 在 main() 中周期打印 osal_mem_stats_t stats; osal_mem_getStats(&stats); // 若 stats.nFreeBlocks 持续减少,说明 osal_mem_alloc() 未配对 osal_mem_free()

定位到gapgattserver.c的gapRole_processGAPMsg()中,GAP_MSG_EVENT类型消息未释放pMsg。修复:

case GAP_MSG_EVENT: if (pMsg->hdr.status == SUCCESS) { // ... 处理逻辑 } osal_msg_deallocate((uint8_t*)pMsg); // 必须释放! break;

6.3 OTA 升级失败后的后悔药:保留双 Bank Bootloader 的物理开关

官方 OAD 方案依赖 Bootloader 检查 CRC,但一旦新固件 CRC 错误,设备将变砖。我们给产线加了一根跳线:

  • 跳线开路 → 正常启动用户程序;
  • 跳线短接 → 强制进入 Bootloader 模式,通过 UART 用 TI 的OAD_Downloader.exe重刷原始固件。
    硬件上只需在 P0_3(任意 GPIO)接 10K 上拉,跳线接地即可。软件在main()开头加:
if ((P0 & 0x08) == 0) { // P0_3 为低 while(1) { // 等待 UART 下载 if (HalUARTRead(HAL_UART_PORT_0, buf, sizeof(buf)) > 0) { oad_download_handler(buf); } } }

我带过的三个模拟项目 X/Y/Z,全部在第二轮 ESD 测试后暴露出 BLE 断连问题,最终都靠示波器抓 VDD 和 C-SPY 查内存解决了。CC2541 不是玩具芯片,它的 BLE 协议栈像一台精密老式机械表——齿轮咬合顺序错一齿,整机停摆。别迷信“例程能跑就行”,真正的稳定,藏在你亲手改过的每一行osal_mem_free()和焊在板子上的每一颗钽电容里。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询