NRG355MC蓝牙例程开发实战:编译、广播、GATT与低功耗避坑 简介ST公司BlueNRG-355MC蓝牙SoC的完整开发例程包面向低功耗蓝牙与物联网开发者解决BLE设备从固件移植到协议栈二次开发的核心难题。例程覆盖芯片初始化、射频参数配置、BLE协议栈分层调用、连接管理与GATT数据交换并针对休眠、深度睡眠等低功耗模式给出参考实现特别适合智能家居、健康监测及可穿戴终端等电池供电场景。压缩包共2000个文件以C源文件、头文件、工程配置.ewp/.uvprojx、编译固件.hex/.bin为主辅以HTML文档与PNG原理图说明整体约264.55MB结构清晰便于检索。已有343人学习使用。例程基于BlueNRG-LP DK 1.4.0附带库函数、示例代码和配置工具帮助开发者掌握GATT自定义服务、HCI命令交互、事件驱动编程及AES安全加密等要点可在STM32CubeIDE中直接编译调试大幅缩短低功耗蓝牙产品的评估与开发周期。1. 拿到 NRG355MC 例程包是「省事」还是「新的迷宫」很多开发者拿到某厂商这颗 NRG355MC 蓝牙芯片的评估板第一反应都是去翻配套的例程包想着「官方都写好了跑一个就是了」。真打开工程才发现目录层级叠了好几层芯片型号选项、链接脚本、协议栈库文件各说各话按 README 点一遍编译报错信息五花八门。这篇笔记就从解压例程包开始把「例程能跑」和「例程改得动」这两件事拆开讲——前一半帮你 30 分钟点亮一颗会广播的蓝牙后一半告诉你改 GATT、调功耗、挪到自家板子的时候到底哪些参数在起作用。我这里聊的「NRG355MC 例程」不是某个单一文件而是一整套软件套件外设寄存器例程、BLE 协议栈集成例程、低功耗例程、驱动库外加 IDE 工程模板。你真正要用的是它的 BLE 连接链路怎么被初始化、服务和特征值怎么挂上去、广播和连接参数怎么配。适合的读者是手头有这块芯片或者准备选它做低功耗产品的嵌入式开发者、硬件工程师以及被厂商例程工程结构搞到头疼的初学者。2. 例程包结构、工具链与第一个能跑起来的工程2.1 例程包的目录逻辑与工程模板选型一提到例程包很多刚接触 NRG355MC 的开发者就会沿用在单片机上攒代码的习惯找一个main.c从头看。这颗芯片的例程包结构完全不同它把驱动、协议栈、板级支持、应用代码四分第一次打开会很不适应。我一般解压后先不看代码先用文件管理器把根目录展开找到Projects或者Examples这一级。常见做法是按芯片型号分子目录型号目录里再按「板卡型号 是否有BLE协议栈」分成几组。每个具体例程目录下放的是 IDE 工程文件、源码、链接脚本和README。你要做的不是把所有工程都编译一遍而是找到和你的板子最接近的那个模板。NRG355MC_SDK/ Drivers/ Middlewares/ # BLE 协议栈库、射频驱动 Projects/ NRG355MC_DK/ BLE/ BLE_Beacon/ mdk/ # MDK 工程 ewarm/ # IAR 工程 inc/ src/ README.md打开README.md的「Hardware Setup」段落确认默认的串口引脚、按键和 LED 定义是否和你手上的评估板一致。这一步跳过的后果是板子不一样点灯例程编译烧录都成功就是没反应。工具链方面工程模板最常见的交付格式是 MDK 和 IAR 两套。我这边习惯直接用 MDK因为工程文件里预置了芯片型号、宏定义和烧录配置不用自己从头配启动文件。个别例程会带 GCC Makefile如果你是在 Linux 服务器上批量编译可以优先选 GCC 版本省得来回切换 IDE。2.2 编译烧录最小例程从点灯到串口打印选一个最不带蓝牙的例程作为第一跑比如 GPIO 控制 LED 的例程把编译链路验证通。这个例程通常包含系统时钟初始化、引脚复用配置、一个main死循环和一个延时函数。它的价值在于能验证三件事IDE 的芯片型号选对了、链接脚本能正确生成镜像、烧录器能正常擦写芯片。打开 MDK 工程后第一件事不是点编译而是打开Options for Target确认 Device 一栏选的是 NRG355MC 对应的型号或兼容型号。接着看Debug选项卡里的烧录器设置NRG355MC 烧录走 SWD 接口常见配置是用板载调试器或通用 SWD 调试器。如果Flash Download里没有正确的烧录算法需要手动添加该芯片的 Flash 算法文件。/* 最小例程的核心结构删掉了厂商库的细节逻辑大致如此 */ #include chip_system.h #include gpio_driver.h int main(void) { system_clock_config(); /* 配置 HSI/HSE 与总线时钟 */ gpio_led_init(); /* LED 引脚复用推挽输出 */ while (1) { gpio_led_toggle(); delay_ms(500); } }这段代码里system_clock_config是时钟初始化入口内部通常包含一个SystemCoreClock全局变量的更新后续配置串口、定时器都依赖它。gpio_led_init里面要核对两个东西引脚号是否与板卡丝印一致复用功能是 GPIO 还是外设功能。NRG355MC 的每个引脚都有多种复用可能选错复用功能是例程「编译过、不工作」的头号原因。delay_ms优先用 SysTick 实现不要用空循环否则后续关了中断会发现延时直接失效。编译干净后对着串口例程再跑一次。串口例程的引脚默认是固定的但很多评估板把串口接到了板载调试器的虚拟串口上直接插 USB 就能看到日志。确认main里调用了uart_log_init波特率常见 115200用任意串口工具打开对应 COM 口复位板子能看到起始横幅就说明工具链完全通了。这一步跑通之后你才真正拿到「可回退的干净基线」。后续无论怎么改 BLE 配置、怎么折腾协议栈只要回到这个例程重新编译烧录就能区分是硬件问题还是软件问题。这个习惯值得养着后面排错全靠它做对照。3. 让蓝牙先「响」广播、连接与 GAP 参数3.1 GAP 初始化与广播数据设备怎么「报名字」点灯例程跑通后第二步就该让手机扫描到这颗芯片。这里进入 BLE 最核心的 GAPGeneric Access Profile层。NRG355MC 例程里协议栈通常以库的形式提供应用代码通过一组协议栈 API 来操作。你要理解的关键点是协议栈需要你给一堆回调函数然后在后台事件循环里被调用代码结构不是传统的顺序执行。先看广播参数。手机能搜到设备前提是设备在广播。广播间隔、广播数据、广播类型这三个参数决定了设备的可见性和功耗。NRG355MC 例程里的默认广播间隔大多是 100ms 左右这是「功能演示友好」的设置不代表产品最终值。广播数据里最常见的是设备名称和厂商自定义数据它们的长度受到严格限制。/* 广播参数配置函数名以你拿到的 SDK 版本为准 */ static uint8_t adv_data[] { 0x02, 0x01, 0x06, /* FlagsLE General Discoverable */ 0x05, 0x09, N, R, G, 3 /* Complete Local NameNRG3 */ }; static void ble_app_init(void) { ble_gap_init(); /* 协议栈 GAP 层初始化 */ ble_gap_set_adv_data(sizeof(adv_data), adv_data); ble_gap_set_adv_params(160, 160, 0x00); /* 间隔 100ms不可连接广播 */ ble_gap_start_adv(); }广播间隔参数 160 的含义是「每单位 0.625ms」。协议栈里的广播间隔单位是标准 BLE 时隙绝大多数例程直接写成倍数。160 个时隙等于 100ms。这个值对功耗影响非常明显间隔越短手机发现越快射频平均电流越高。产品上常见 200ms 到 800ms 之间。0x00表示不可连接广播改成0x01才是可连接广播。很多第一次上手的人把广播类型写成不可连接导致手机能扫描到但连接超时这是第一个典型翻车点。广播数据里0x02, 0x01, 0x06是标准 AD 结构长度、类型、值。类型 0x01 表示 Flags值 0x06 含义是「支持 LE 通用发现模式不支持传统 BR/EDR」。设备名称 AD 结构中0x09 表示完整本地名称后面跟名称字节。这块最常见的坑是「AD 结构长度没算对」多一个字节或少一个字节扫描端会解析失败设备时隐时现。关于广播数据长度我要特意说一下边界。NRG355MC 的广播通道报文总长 37 字节其中广播数据最多 31 字节这 31 字节要包含 Flags、名称、服务 UUID 以及厂商数据。你往里面塞超过 31 字节协议栈 API 会返回错误。例程里通常不会触发这个错误因为你一改动就很容易触到上限。遇到广播失败先数数据长度这个经验我重复了无数次。3.2 连接参数与事件回调连上只是开始广播能扫到连接又是另一套参数。连接间隔、从设备延迟、监督超时这三个参数共同决定了连接后的功耗和实时性。NRG355MC 例程的连接参数默认值偏向低时延方便演示双向收发。但如果你做的是传感器节点这个默认值会让你待机电流高得离谱。连接参数通过「连接更新请求」或「参数更新请求」下发到协议栈。典型配置表如下/* 连接参数单位同样是 1.25ms 时隙 */ #define CONN_INTERVAL_MIN 6 /* 7.5ms最小值 */ #define CONN_INTERVAL_MAX 12 /* 15ms最大值 */ #define CONN_SLAVE_LATENCY 0 /* 从设备延迟 */ #define CONN_TIMEOUT 1000 /* 监督超时 1s */连接间隔 7.5ms 意味着从设备每 7.5ms 要醒来一次接收数据平均电流自然高。如果你做的是温湿度采集、电量上报这类低频率业务可以把连接间隔拉到 100ms 以上并开启从设备延迟。从设备延迟的作用是允许从设备连续跳过若干个连接事件一跳就是「连接间隔 × 延迟数」的时间不用起来收包省电效果直接乘出来。常规建议上报频率低就设 50~200ms 间隔加 4~8 个从设备延迟功耗能降一个数量级。事件回调在例程里体现为connection_event、disconnection_event这类函数。你需要在回调里自己承担责任常见做法是在断线后立即重新开启广播。例程里往往把重连写在同一个回调里但如果你同时连过多个主设备这里的处理要小心回调里要根据连接句柄判断是哪一个连接断开了。static void connection_event(uint8_t conn_handle, uint8_t status) { if (status CONNECTION_STATUS_DISCONNECTED) { /* 断线重广播注意延迟一下避免忙循环 */ ble_gap_start_adv(); } else if (status CONNECTION_STATUS_CONNECTED) { /* 连接成功后通常不需要额外动作 */ app_connection_established true; } }断线后立即重广播是简单直接的处理方法但要注意如果断线是因为信号干扰立即广播可能马上又被连接又断线形成抖动。更稳妥的做法是加入退避策略第一次断线等 1 秒第二次等 5 秒连续多次断线就进入长周期广播。这样能明显提升设备在复杂环境下的生存率也是我从产品现场学到的血泪经验。手机端验证连接时建议用手机自带的 BLE 调试工具扫描到设备后发起连接然后观察连接间隔是否与你配置的一致。很多调试工具能直接读出当前连接参数如果读出来的值和配置值不一样说明主设备主动发起了连接参数更新或者从设备在连接后没有正确更新参数。这个时候回到例程的connection_event里看一眼是否调用了参数更新请求。4. 按你的产品改 GATT例程服务怎么动刀4.1 从官方服务到自定义服务UUID 与数据模型BLE 连接建立之后手机和芯片之间的数据交换靠 GATTGeneric Attribute Profile。例程里通常会提供一两个服务电池服务、设备信息服务有的还带一个自定义的数据收发服务。开发产品时官方服务只留设备信息就够了核心业务要自己设计服务。先理解 GATT 的三层结构服务Service包含若干个特征值Characteristic特征值下面还有描述符Descriptor。手机读写的是特征值。设计你自己的服务时要先准备一个 128 位私有 UUID16 位 UUID 需要付费购买私有服务的标准做法是用一个 128 位 UUID格式例程里有定义好的宏可以直接借鉴。/* 自定义服务温湿度上报 */ static const uint8_t temp_humi_service_uuid[16] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0xFF }; static const uint8_t temp_char_uuid[16] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0xFE };GATT 服务注册有固定顺序先添加服务再添加特征值最后添加描述符。NRG355MC 例程的协议栈 API 通常是异步的注册请求发出后要等待事件回调确认成功再继续注册下一个特征值。很多人会把所有注册请求连续发出结果回调乱序服务注册一半就卡住。正确做法是在服务添加成功的回调里再添加第一个特征值特征值添加成功的回调里再添加第二个串行推进。特征值的属性字节也很关键常见组合是「读 通知」或者「写 写无响应」。属性设错手机端能看到特征值但读返回错误或写不进去。建议在服务设计阶段画一张特征值清单标清每个特征值的读写权限、是否需要通知、长度是定长还是变长再开始写代码能省掉大量反复烧录调试的时间。4.2 特征值读写与手机联调日志是最好的老师服务注册完成后应用代码要和协议栈事件对接。手机端写一个特征值时芯片端要能在写事件回调里取到数据并转发给业务逻辑芯片端要上报数据时调用通知/指示接口把数据压进发送缓冲区。static void gatt_write_event(uint8_t conn_handle, uint16_t handle, uint8_t *data, uint16_t len) { /* 先判断 handle 是哪个特征值再做业务分发 */ if (handle app_config_char_handle) { app_save_config(data, len); } } static void app_notify_temperature(int16_t temp) { uint8_t payload[2]; payload[0] (uint8_t)(temp 0xFF); payload[1] (uint8_t)((temp 8) 0xFF); ble_gatt_send_notification(app_conn_handle, temp_char_handle, sizeof(payload), payload); }通知接口调用时芯片端的协议栈不一定能立刻发出数据。如果上一次通知还没完成再次调用可能会失败。看门狗式的解决办法是在发送完成回调里清一个标志位业务层发送前检查这个标志位。NRG355MC 例程里如果没处理这个关联高频上报数据时会出现丢包手机端看到数据断断续续。手机联调用的 App 一般能显示服务列表、特征值列表还能直接读写。联调时打开串口日志日志里要带上每次读写的 handle 和数据。我习惯在gatt_write_event里打印收到的原始字节这样手机端写入了什么芯片端看日志就能对齐。遇到数据对不上的时候先确认字节序温湿度转成两个字节时高字节在前还是低字节在前和手机 App 的解析代码必须一致否则读出来就是负数或者几十倍的偏差。5. NRG355MC 例程开发避坑清单现象、原因、解法5.1 编译与烧录环节的坑现象一工程编译报错找不到某个头文件错误信息指向hal或platform目录。原因直接打开了Projects下的子工程但例程依赖 SDK 根目录的驱动库MDK 工程里引用的相对路径是从「工程文件所在位置」计算的一旦工程文件被移动或复制到其他目录路径全部失效。解决不要移动官方工程文件在原始位置打开编译如果想复制到自己的工作目录整个 SDK 根目录一起复制并且保持相对结构不被破坏。现象二烧录时报错提示无法连接目标设备或 Flash 下载失败。原因最常见于芯片的 SWD 引脚复用被改掉了。例程里如果初始化了 SWD 引脚作为 GPIO 使用烧录器就再也没法访问芯片。解决按住板子上的复位键在烧录器开始连接时松开让芯片在复位状态下进入烧录流程。如果不行检查 SWD 引脚是否被外部电阻拉到异常电平。另外某些例程代码会主动关闭调试接口这种芯片只能靠串口 ISP 方式恢复恢复前看看板卡跳线是否支持。现象三编译成功烧录成功但程序不跑上电后电流异常大。原因芯片型号选错导致启动文件或链接脚本不匹配。NRG355MC 不同封装的 Flash 和 RAM 大小不同链接脚本里指定的堆栈地址超出实际内存程序直接跑飞。解决回到工程选项里核对芯片型号再到链接脚本里确认 Flash/RAM 的起始地址和大小与实际芯片一致。这类问题用调试器读 PC 寄存器能快速定位地址跳到一个非法区域基本就是链接脚本问题。5.2 运行与功耗环节的坑现象四手机能扫到设备但连接时总是超时偶尔能连上但很快断开。原因广播类型配置成了不可连接广播或者广播数据里的 Flags 字段与广播类型矛盾。另一个隐蔽原因是板载晶振精度不够NRG355MC 对低频时钟的精度有要求如果板子用的是低成本陶瓷晶振且匹配电容不对射频中心频率偏移连接就会反复失败。解决先检查广播类型再检查广播数据长度和 Flags 字节最后用频谱分析仪看射频中心频点或者直接换上官方评估板的晶振电路做对照。现象五代码进了低功耗模式实测电流还是高比数据手册标称值大很多。原因例程默认大量外设没有关闭最常见的是串口、LED、内部上拉电阻、GPIO 浮空输入。浮空 GPIO 会通过漏电流路径拉高整体功耗这是低功耗调试里最麻烦的一类问题。解决进入低功耗前把所有不用的 GPIO 配成模拟输入或推挽输出低电平把调试串口关闭或不做任何输出操作。另外协议栈在连接状态下本身有周期性唤醒不要拿连接态的电流值和广播态、睡眠态比较对比前先看当前链路状态。现象六例程在调试器在线连接时正常一断电重启就死机。原因代码里依赖了调试器提供的时钟或调试接口状态。某些低功耗例程在进入停机模式前会判断调试器是否连接在线时跳过停机离线时直接停机就会表现成「烧录后没有反应」。解决检查低功耗例程的进入条件确认唤醒源是否配置正确。这类问题往往是唤醒引脚没有上拉或者唤醒后没有重新配置系统时钟。6. 例程做到能出货把日志留在编译期之外从「例程能跑」到「产品能出货」中间隔着一道大坎如何把调试代码彻底隔离让正式固件跑出真实功耗。NRG355MC 例程默认开着串口轮询、LED 闪烁这些东西让新手的测量数据虚高得吓人。我的做法是用编译宏控制整个日志模块而不是靠注释代码。/* app_log.h */ #ifndef APP_LOG_ENABLE #define APP_LOG_ENABLE 1 #endif #if APP_LOG_ENABLE #define APP_LOG(fmt, ...) uart_send_line(fmt, ##__VA_ARGS__) #else #define APP_LOG(fmt, ...) #endif工程里所有的调试打印都走APP_LOG宏。正式固件把APP_LOG_ENABLE置 0编译后日志代码彻底消失不产生任何串口输出也不会影响中断时序。比这个更彻底的做法是连串口初始化代码也用同一个宏包住这样外设时钟和引脚复用都不会被打开功耗测量才算干净。数据上报上把连接间隔拉到应用允许的最大值打开从设备延迟广播间隔放到 500ms 级别。你要验证的不是「功能通」而是「一年后电池还有电」。这一步要在真实发射功率下测别拿开发板的供电电路读数最好用万用表串联电池端测量平均电流跑一个完整的上报周期再算均值。还有一个小习惯我踩坑多次后养成了每次改协议栈参数都保存一份之前的可运行工程副本注释里写明改动点和测量电流值。NRG355MC 的例程参数之间是联动的你调整了广播间隔可能影响到连接成功率和手机端扫描体验没有对照基线很难判断是哪次改动引入了问题。这比任何调试技巧都管用。希望这篇笔记能帮你少走一段我走过的弯路祝你的板子一次点亮电流一次测过。本文还有配套的精品资源点击获取