ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FreeRTOS 下跑通 BLE 心电采集:任务划分、队列通信与低功耗实战

2026/8/31 8:01:02 拓冰建站 浏览量
FreeRTOS 下跑通 BLE 心电采集:任务划分、队列通信与低功耗实战 简介本资源是一个面向嵌入式开发者的FreeRTOS与蓝牙低功耗BLE融合实践项目聚焦于医疗健康场景下的实时心电图ECG数据采集与无线传输适用于具备C语言基础和STM32/nRF等MCU开发经验的中高级工程师及高校高年级学生。项目完整实现FreeRTOS多任务调度含传感器采集、滤波处理、BLE服务封装与数据队列通信并深度集成BLE协议栈含RTX_CM4等CMSIS-RTOS兼容内核库解决资源受限环境下实时性、功耗与通信可靠性的协同难题。压缩包共1773个文件以592个C源码、571个头文件为核心辅以43个Makefile构建脚本、41套Keil/IAR工程配置uvprojx/ewp等、58个链接脚本.ld/.icf及33个可执行固件.hex整体9.46MB结构清晰、模块解耦度高便于分层学习与移植验证。已有314人下载学习读者可直接复现完整ECG蓝牙终端系统获取真实信号滤波算法实现、BLE自定义服务配置范例及FreeRTOS任务间同步与通信的典型工程实践。 搞过几轮 BLE 实验之后我越来越觉得纯裸机写蓝牙心电这种应用基本属于给自己挖坑。中断里要处理广播事件、连接事件、收发数据主循环还要管 ADC 采样、滤波、心率算法状态一多就乱成一锅粥。这期实验我直接把 FreeRTOS 拉进来把采集处理发送拆成独立任务整个工程的思路一下就清晰了。这篇就和你聊聊在 FreeRTOS 下把 BLE 心电跑起来从任务划分到栈配置、队列通信、断连重连把关键的取舍和踩过的坑一次说清楚。先说这个项目是干什么的一个低功耗蓝牙心电采集设备MCU 通过模拟前端采集心电信号滤波和心率计算之后通过 BLE GATT 把数据推给手机 App 显示波形。要实现这个效果除了硬件电路之外固件侧的核心问题有三个——怎么保证采集的实时性、怎么让 BLE 协议栈和业务任务互不干扰、以及怎么控制功耗。FreeRTOS 恰好能在这些问题上帮上大忙。适合正在学 RTOS 的嵌入式开发者、以及想把 BLE 项目做出稳定性的工程师参考。1. 先理清一点为什么心电监测要上实时系统1.1 裸机方案到底卡在哪很多朋友第一版心电代码都是裸机写的我第一版也是。主循环里面大概长这样轮询 ADC 转换完成标志、处理滤波、更新心率、处理按键、处理 BLE 事件。刚烧进去的时候一切正常蓝牙一连接上波形很漂亮。但是只要开始持续传输偶尔就会出问题——波形抗干扰能力下降、偶发掉线、手机端波形动画一卡一卡的。问题不出在算法而是出在 CPU 时间片分配上。BLE 协议栈本身是事件驱动的。以常见方案为例协议栈在主循环里被反复调用每调用一次处理一个待处理事件。这个调用要尽量频繁否则连接事件错过 deadline硬件就认为链路超时直接断连。但心电的 ADC 采样又要求严格的定时比如 250Hz 的采样率就必须每 4ms 采一次。要是主循环恰好去做了一个比较重的滤波运算直接把协议栈调用给挡住了BLE 就断。裸机下为了缓解这个问题常见做法是疯狂加中断优先级、缩短滤波时间但本质上是拆东墙补西墙。再一个痛点是状态管理。裸机把连接中已连接正在传输这些状态全部压在全局变量里主循环和中断轮流改这些变量越改越乱。我当时调一个断线重连的 bug 花了两天最后发现是一个全局标志在中断里被清了主循环却还在等它。1.2 FreeRTOS 在 BLE 项目里能解决什么FreeRTOS 进入这个项目之后核心改变不是代码能跑而是每个功能的实时性有了保障。ADC 采集是一个独立任务BLE 协议栈调度是另一个独立任务算法处理和数据封装又是一个任务。三个任务通过队列解耦不再互相踩。更重要的是优先级机制。FreeRTOS 里可以明确告诉调度器BLE 协议栈任务必须是最高优先级因为链路超时是硬件级的硬限制采样任务次之算法任务可以稍微低一点晚几十毫秒算完没关系。调度器按照这个设定去分配 CPU保证了每个任务都在自己的截止时间之前完成。任务划分还有一个隐性好处代码变得好测了。采集任务可以单独模拟输入算法任务可以拉出来在 PC 上跑同款测试集BLE 任务只关注封包格式。我这次实验最大的体会就是把工程拆成任务之后出 bug 的定位速度比裸机快了一个量级。2. 整体方案与平台选型2.1 硬件平台怎么选几种常见组合的取舍做 BLE 心电平台选择直接决定你后续的工作量。这一轮我验证了几种搭配各有各的脾气。最经典的组合是 MCU 加外部 BLE 芯片比如 STM32 某款 BLE SoC两边通过 UART 或 SPI 通信。优点是 MCU 资源充足跑算法不心疼缺点是需要自己处理两个芯片之间的握手协议数据传输吞吐受限调试也麻烦。另一类是带 BLE 的 SoC比如 nRF52 系列协议栈和用户代码跑在同一颗芯片上内存虽然小一点但省去了双芯片互通的痛苦而且 Nordic 的 SDK 自带 FreeRTOS 支持我最后选的这条路。这里要特别说明一下网上关于STM32 FreeRTOS 蓝牙模块的教程很多那个方案里 MCU 压根不跑 BLE 协议栈只是跑一个串口透传驱动跟本实验说的FreeRTOS 下运行 BLE不是一回事。我这次的目标是把 BLE 协议栈也纳入实时系统调度体系这样才能真正控制链路时序也才能解释清楚后续遇到的优先级问题。2.2 协议栈与 RTOS 怎么共存行业内主流的做法是BLE 协议栈作为整体运行RTOS 为其分配一个高优先级控制线程协议栈的底层中断如 RADIO 中断、定时器中断负责抓取射频事件然后把事件上报给控制线程处理。这样一来协议栈不会阻塞你的业务代码业务代码密集计算时协议栈也有机会抢占执行。但现实是不同厂商的协议栈跟 FreeRTOS 的集成方式差异很大。比如 Nordic 的 SoftDevice 在旧版 SDK 里是直接接管了全部中断给用户预留了几个特定优先级的中断号FreeRTOS 配置要避开这些新版 Zephyr 方案则是把 BLE 控制器跑在一个独立的协作式调度上下文里。很多人在照搬别人 FreeRTOS 模板时发现蓝牙一开就死机多半是优先级或者中断向量没配对。一个通用建议是先不创建任何业务任务跑通协议栈自带的示例工程确认协议栈在 FreeRTOS 下能正常广播和连接再逐步往里面加任务。加一个任务测一次不要一上来就把所有的功能缝合在一起。2.3 任务划分与优先级分配这个项目我最终分了 6 个任务BLE_StackTask优先级最高负责运行协议栈调度处理。ECG_SampleTask第二优先级通过定时器触发 ADC 采样。ECG_ProcessTask第三优先级做滤波和心率计算。DataSendTask第四优先级把处理后的数据打包并通过协议栈发送。DisplayTask第五优先级驱动屏幕或 LED 指示状态可选项。IdleTask系统自带做低功耗处理。优先级之间要留出余量不要把所有任务排成紧挨着的数字。我见过不少工程把 5 个任务分别设为 1、2、3、4、5 优先级看似合理但实际运行中一旦某个任务因为异常执行时间变长就会引发优先级翻转连带的问题。建议关键任务之间至少隔一个档位。任务栈大小也值得认真算我自己踩过不少坑。BLE 协议栈任务需要至少 1~2KB如果协议栈内部有事件处理缓冲还要更多心电处理任务跑滤波算法尤其是 32 位浮点运算时建议给到 512 字到 1KB简单的采样任务 256 字起步。这只是起步值实际要结合编译器报的栈溢出慢慢调。3. 工程搭建与基础配置实操3.1 开发工具链与工程骨架我这次用的是 nRF52832 作为主控SDK 为 17.1.0 版本IDE 选 Keil 和 SES 都测试过最终留在 SES因为其链接脚本对 FreeRTOS 的堆栈划分更透明。如果你用 STM32 加外部模块的方案也可以用 STM32CubeIDE原理一致。工程骨架建议从官方 BLE 示例比如ble_app_hrs开始因为它已经配置好了服务、广播、连接参数。在这个基础上手工添加 FreeRTOS 相关文件。注意 SDK 里的 FreeRTOS 配置头文件FreeRTOSConfig.h是跟官方例程绑定的直接拿来用不一定适合你的项目需要按实际需求改。关键配置项配置项建议值说明configUSE_PREEMPTION1使用抢占式调度保证高优先级任务及时响应configUSE_TIME_SLICING1同优先级任务时间片轮转可选项configTOTAL_HEAP_SIZE4096~8192堆大小视任务数量和数据缓冲而定configMAX_PRIORITIES8任务的优先级个数够用即可configMINIMAL_STACK_SIZE128空闲任务栈默认即可configUSE_TICKLESS_IDLE1开启低功耗 tickless 模式延长待机时间configTOTAL_HEAP_SIZE这个参数我反复调过。太小任务创建失败太大系统内存不够用。我的计算思路是把每个任务的栈大小加起来例如 10245125125122560 字约 5KB加上队列缓冲几 KB再加上协议栈预留内存最后乘以 1.2 的安全系数。4~8KB 在 nRF52832 上是比较稳的区间。3.2 FreeRTOS 移植到 BLE SDK 的细节点一个容易坑人的细节是中断优先级设置。Cortex-M 内核从 0 开始编号数值越小优先级越高。FreeRTOS 要求中断优先级数值必须大于等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY否则不能安全调用FromISR结尾的 API。BLE 协议栈的 RADIO 中断优先级往往非常高这时候如果你在协议栈的上下文里使用了 FreeRTOS 的 API就可能触发断言。解决办法是协议栈相关事件不要直接在 ISR 里处理而是通过信号量或事件标志组通知协议栈任务去处理。这样既满足实时性又避开了 FreeRTOS 的中断安全限制。再有一个是SysTick 与协议栈定时器冲突。FreeRTOS 默认使用 SysTick 作为系统时钟但某些 BLE 协议栈也会抢占 RTC0/定时器。nRF52 SDK 的 FreeRTOS 适配层已经做了处理把系统 tick 改到 RTC1 上。如果你是自己移植务必确认协议栈占用的定时器和 FreeRTOS 的系统 tick 没有冲突否则现象非常随机一会儿蓝牙起不来一会儿运行十几分钟死机。3.3 心电采样任务的设计采样任务用 FreeRTOS 的软件定时器触发而不是单纯靠vTaskDelay。因为vTaskDelay的相对延时存在累计误差任务执行时间长时下一次延时的起始点会后移采样间隔就不稳定。软件定时器虽然在 FreeRTOS 里也是基于 tick 计数但它的触发相对固定可以做到漂移很小。ADC 采样流程分为三个环节配置 ADC 通道使能内部放大器和偏置电路。启动一次转换使用xSemaphoreTake等待转换完成信号量。信号量在 ADC 完成中断中释放。拿到原始值后写入ECG_DataQueue同时触发处理任务。等待转换的这段时间采样任务会阻塞让出 CPU这是 FreeRTOS 的高效之处——裸机里只能空转等待白白浪费 CPU 周期。如果采样率是 250Hz周期 4ms一个采样周期里大部分时间是空闲的这些空闲时间完全可以被调度给滤波算法或协议栈。信号量与队列的配合还有个妙用ADC 中断里只释放二值信号量不做其他事中断服务时间极短。这符合中断里只做最少事的原则也避免 ADC 中断与 BLE 高优先级中断打架。3.4 BLE 广播、连接与 GATT 服务配置心电数据走 GATT 的 Notify 方式。心率服务Heart Rate Service是标准服务有心率测量特征值手机端不用写复杂解析代码。心电原始波形数据如果也想传就得自定义一个特征用自定义的 UUID属性设置为 NotifyNotify 权限设置为加密读更好防止别人随便连接读取你身体数据。广播参数上我建议广播间隔设 40ms 到 100ms 之间。太密比如 20ms会让扫描端看到设备快但功耗增加太疏比如 1000ms是省电但连接体验很差手机 App 半天扫不到。这个实验里我用了 50ms 广播间隔兼顾发现速度和功耗。连接参数也很关键。连接间隔建议设为 15ms 到 30ms从机 latency 设为 0超时时间设 2000ms 以上。如果连接间隔太大心电数据吞吐不够手机端波形一卡一卡如果超时太短瞬时遮挡就可能断连。这两组参数必须在广播或连接请求中明确协商否则协议栈按默认值走体验很差。一个常见坑如果你从官方心率示例改过来官方例程的 MTU 可能只有 23 字节。单次通知包最大 20 字节而一个心电数据包往往需要 4 通道 x 2 字节 8 字节够用但如果你想传更详细的波形数据或者多通道建议把 MTU 请求调到 247 字节需要手机端配合否则每次只能发很少的数据。4. 核心实现细节与调试方法4.1 任务间通信队列、信号量与事件标志组的选择FreeRTOS 提供了多种 IPC 机制用错地方就是麻烦源头。队列Queue适合数据流场景比如 ADC 原始数据从采样到处理。队列有缓冲生产者和消费者速度不匹配时不会丢数据只会积压。二值信号量Binary Semaphore适合事件通知场景比如数据已经准备好转换已经完成。它不携带数据只起到唤醒作用。事件标志组Event Group适合多个条件同时满足的场景比如蓝牙已连上且电量为高才启动持续发送。我在这个项目里用了两条队列加两个信号量。ADC 原始数据队列长度为 128心电处理完的数据队列长度为 32。为什么原始数据队列长度要大因为算法任务可能偶尔被 BLE 高优先级任务抢占到延迟队列不够长时采样数据就会丢失心电波形上出现毛刺。128 的长度在 250Hz 采样率下能缓冲约 0.5 秒的数据足够扛过偶发的调度抖动。这里有个经验之谈队列长度宁可给大不要给小但也不能盲目给大。给大了浪费内存给小了丢数据难排查。可以先从理论值两倍起步跑一晚上数据看实际水位可以用uxQueueSpacesAvailable打印再逐步调低到一个安全值。4.2 心电数据封包与发送逻辑心电数据从处理任务出来之后要组装成 BLE 通知包。我的包格式是这样的字节偏移含义0数据包序号8bit溢出回卷1通道数2~5通道1 ~ 通道4 的高字节如果只有单通道可省6~9通道1 ~ 通道4 的低字节包序号很重要手机端通过它判断是否丢包。BLE 的 Notify 本身有链路层重传机制但重传失败或缓存溢出时上层照样会丢如果 App 不检查序号波形图上就会出现莫名其妙的断点。加上序号后App 可以在断点处做插值或者提示用户信号质量变差。发送节奏也有讲究。心电采样 250Hz每次采一个点如果每个点都单独发一个通知包那一秒钟要有 250 个通知包BLE 4.0 的吞吐很难稳定支撑取决于连接间隔和包大小。我的方案是攒 10 个点成一批发布即 4ms x 10 40ms 发一次批量数据。这样每秒钟只需 25 个通知包数据量反而小链路压力低手机端波形重采样后依然流畅。这背后是数据聚合的思想BLE 链路是低功耗窄带设计特别不适合高频小包传输。一次多传几个点整体功耗更优、链路更稳。如果你有实际抓包工具比如 Ellisys 或 Nordic 的 nRF Sniffer可以对比一下逐点发和批量发时的空中包数量差距很明显。4.3 低功耗设计FreeRTOS Tickless 与 BLE 的配合做心电贴片设备电池续航是刚需。如果 CPU 全程满频运行用不了几个小时就报废。FreeRTOS 的 Tickless 模式可以在系统空闲时挂起内核时钟进入较深的睡眠状态配合 BLE 协议栈的事件调度能显著降低平均电流。具体操作上nRF52 的 SDK 里有一个sd_app_evt_wait()函数用于协议栈等待下一个 BLE 事件。在 FreeRTOS 的IDLE钩子或低功耗任务中调用它可以让 CPU 在无事可做时停下来。注意如果开启了 Tickless自己写代码时千万不要用vTaskDelay(1)这种短延时的忙等逻辑它会不断唤醒 CPU导致功耗不降反升。实测数据裸机轮询时持续发送心电数据的平均电流约 4.5mA迁移到 FreeRTOS Tickless 之后同样的持续发送场景平均电流降到约 1.8mA。这不是协议栈的功劳而是没事就睡的收益。如果只是待机不传输平均电流能到几十微安级别。这里有个使用 Tickless 前的注意事项调试时最好先关掉 Tickless因为断点停下时内核 tick 已经不在跑有些调试器会表现异常。程序功能全部调通了再开启 Tickless 做功耗优化不然会调试到怀疑人生。5. 常见问题与排查技巧实录5.1 硬故障和栈溢出怎么快速定位最常见的问题就是跑着跑着死机了。这时候第一反应应该是查硬故障HardFault。在 Keil 或 SES 的调试器里暂停后查看PC寄存器和LR寄存器再查 Call Stack 是哪个函数触发的。如果发现是某个任务的栈溢出有两个办法一是开启 FreeRTOS 的栈溢出检测功能configCHECK_FOR_STACK_OVERFLOW设为 1 或 2。检测方式 1 是任务切换时检查栈顶标记检测方式 2 是任务切换时完整检查栈空间更可靠但开销更大。我建议开发阶段用方式 2。二是打印每个任务的高水位线。在任务里定时调用uxTaskGetStackHighWaterMark看最小剩余栈空间。如果某个任务的水位线长期在 20 字以内说明栈偏小了需要调大。这个方法比等系统崩了再去查要省心得多。5.2 任务卡死与优先级翻转FreeRTOS 下最常见的问题是某个任务一直不执行或者系统整体卡死。排查顺序是确认系统 tick 有没有在跑xTaskGetTickCount是不是一直在增长。确认最高优先级任务是否因为等待某个信号量而把自己阻塞了如果是那这个信号量是谁释放的有没有可能永远没人释放。确认中断优先级配置是否满足 FreeRTOS 的约束如果中断里调用了xSemaphoreGiveFromISR而这个中断的优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY就会触发断言。优先级翻转的问题我实际遇到过。某个低优先级的算法任务持有队列写权限高优先级的 BLE 发送任务想要写同一队列结果被阻塞中优先级的显示任务趁机抢占了 CPU导致 BLE 发送延迟变大。解决办法是把对同一数据结构访问的任务合并成一个或者用一个互斥量保护并开启优先级继承机制configUSE_MUTEXES和configUSE_PRIORITY_INHERITANCE都为 1。5.3 BLE 为什么连不上或反复断开排查清单如果蓝牙连不上先不要怀疑代码按这个顺序排除广播参数对不对广播间隔是否太密、信道是否全开扫描端能不能看到设备连接参数是否在协议栈支持范围内有的手机对连接间隔非常敏感你配置的 7.5ms 间隔可能在 Android 上直接拒绝。是否达到了还能再次连接的设备数上限很多 SDK 默认只支持 1 个连接之前那个没断开的话新的就连不上。有没有开白名单或绑定白名单模式下不匹配的设备连不上。协议栈任务是否被饿死了如果 FreeRTOS 里协议栈任务优先级太低被其他任务抢占就会错过连接事件导致断连。把打印任务、显示任务放低一点或者用事件标志组延迟处理。连接之后反复断开最可能是连接事件超时。特别是开着 Tickless 低功耗时系统进入睡眠太深醒不过来处理射频事件。解决办法是在连接状态下不要进入比协议栈要求的睡眠还深的模式或者使用sd_app_evt_wait()替代自定义的睡眠逻辑。5.4 数据丢包与波形毛刺心电波形在手机端出现毛刺首先要区分是信号源问题还是传输问题。可以把板子上的原始数据通过串口打印出来对比串口数据和手机收的数据。如果串口出来就有毛刺说明是模拟前端或采样时序问题如果串口数据正常但手机端有毛刺那就是 BLE 传输问题。传输丢包的几个常见原因队列溢出处理任务被高优先级任务抢占太久队列写满后新数据被丢弃。解决方法是调大数据队列或优化处理任务、减少单次处理耗时。BLE 通知发送速度超过协议栈缓冲能力连续调用多个发送 API 而中间没有延时或流控协议栈缓冲溢出直接丢弃。建议发送任务里加一个简单信号量每次发送完在发送完成回调里给信号量等待信号量后再发下一包。连接间隔太大或 MTU 太小单次传输能力不足导致数据积压延迟增大最终表现为丢包。调整连接参数或增大 MTU。6. 这套方案还能往哪些方向扩展这个实验做完整个框架是可复用的。不仅仅是心电任何高保真数据 无线传输 实时可视化的需求比如血氧、脑电、肌电、甚至工业振动监测都可以套用同样的任务骨架采集任务、处理任务、BLE 任务、低功耗管理。如果后续想升级可以考虑加一个记录模式把心电原始数据先写入 Flash 或 SD 卡等蓝牙连上后再整段历史数据回传。这种应用对 BLE 吞吐的要求更高你可以尝试把 MTU 调到 247 甚至 512并考虑用连接事件扩展DLE技术。FreeRTOS 侧的任务调度逻辑不用大改只需要增加一个回传任务。另外一件事值得做把心率算法跑得更准。FreeRTOS 的调度优势在于你可以很容易地离线保存原始数据然后在 PC 上重新跑算法对比不同算法参数的效果。把算法模块做成和硬件解耦的独立任务想换算法时只改任务内部实现接口不动整个工程稳如老狗。最后说点实在的。这次实验让我最大改观的不是 FreeRTOS 本身而是它的诊断能力。裸机时代出问题只能盯着调试器发愣FreeRTOS 下可以通过任务状态列表、栈水位、队列使用率这些指标把系统运行状态量化出来。如果你正在裸机 BLE 项目里挣扎我强烈建议迈出 RTOS 这一步先用本文这套最小系统把任务框架搭起来再迭代加功能。刚开始会有点不习惯跑通一个完整任务切换之后你会回来感谢那个敢于重构的自己。本文还有配套的精品资源点击获取