
简介本资源是一个面向嵌入式开发者的FreeRTOS与蓝牙低功耗BLE融合实践项目聚焦于实时心电图ECG数据采集与无线传输系统构建适用于具备C语言基础和STM32/nRF等MCU开发经验的中级以上工程师及高校高年级学生。项目完整实现FreeRTOS多任务调度含传感器采集、滤波处理、BLE服务封装与数据队列传递并深度集成BLE协议栈含RTX_CM4/CM3等内核适配库解决资源受限环境下实时性、低功耗与无线通信协同的关键问题。压缩包共1773个文件主体为592个C源码与571个头文件实现任务逻辑与外设驱动辅以43个Makefile、41套Keil/IAR工程配置uvprojx/ewp等、58个链接脚本.ld及7份PDF技术文档总大小9.46MB。已有314人学习下载提供从FreeRTOS任务划分、BLE服务定义如ECG特征值配置、ADC采样数字滤波Butterworth等到调试排错的全流程参考目录结构按模块分层清晰便于快速定位核心代码与移植适配。 做嵌入式实验做到第七个最怕的不是代码写不出来而是原本裸机跑得好好的功能一旦叠加起来就全乱套。前面几个实验里我先后点亮过LED、驱动过OLED、用ADC采过电位器电压、也单独试过蓝牙串口透传每个模块单独测试都好好的。到了BLE实验7FreeRTOS下运行蓝牙心电这个题目要把AD8232心电前端、FreeRTOS实时系统、蓝牙BLE透传三样东西放在一个工程里跑问题一下就冒出来了采样要连续不能断蓝牙发送要稳定不能卡OLED刷新还不能拖后腿裸机主循环那套顺序轮询的思路根本扛不住。这篇文章就是我把这个实验完整跑通的全记录包括任务怎么拆、队列怎么配、蓝牙数据帧怎么设计、以及联调时踩进去的三个大坑和完整排查过程。如果你正在做FreeRTOSBLE相关的项目或者准备把心电采集这类实时性要求高的传感器任务搬进RTOS环境这篇内容应该能帮你少走很多弯路。1. 为什么要上FreeRTOS心电采集这活儿裸机真扛不住1.1 裸机时代的假实时先说说我为什么在实验7之前就开始对裸机方案不耐烦。前面几个实验用的是典型的主循环中断结构while(1)里轮询标志位有数据就处理没数据就发呆。单模块时这种方式完全够用因为每个外设的节奏能互相错开。但心电采样不一样它要求持续、均匀、不能丢点。AD8232这个心电前端模块输出的是模拟电压信号STM32要用ADC以固定频率采样一般是250Hz到500Hz这个量级。如果主循环里某个操作耗时太长采样就会变成不均匀的锯齿状波形上表现出来就是毛刺和断裂。偏偏蓝牙串口发送又是那种不定时抽风的操作——缓冲区满了要等HAL_UART_Transmit阻塞起来能卡住整个循环好几十毫秒。OLED刷新更不用说了I2C在400kHz下刷一帧小屏幕也要几个毫秒。三个任务抢一个CPU裸机的调度方式只能是谁先占住谁干完全没有轻重缓急的区分。结果就是蓝牙发送数据时采样被延误心电波形出现周期性缺口OLED刷新时蓝牙数据在缓冲区里堆积手机端收到的是毫无规律延迟的数据流。1.2 FreeRTOS解决的是优先级和阻塞两个问题FreeRTOS进来之后思路整个变了。我不再需要在一个大循环里小心翼翼地排列每个操作的顺序而是把任务拆开让系统自己决定谁先跑。关键的概念有两个。第一是优先级抢占采样任务设为最高优先级只要定时器触发标志位一置位它就立刻抢到CPU执行权不管此时蓝牙任务是不是发送到一半。第二是阻塞机制蓝牙任务发送数据时如果串口忙它不会死等而是把自己挂起让出CPU给别的任务跑。这样采样任务永远能在下一个采样点到来之前执行不会被其他任务拖住。这个阻塞挂起的机制我打个比方裸机方案就像只有一个窗口的食堂打饭一个人在前面慢慢打饭后面所有人都只能等着FreeRTOS方案就像有几个独立窗口后面的人发现自己窗口暂时没人处理可以先去做别的准备工作等窗口喊号了再回来取餐。1.3 实验7的核心目标拆解拿到实验7题目的时候我先做了需求拆解把FreeRTOS下运行蓝牙心电这个总体目标分成四个可独立实现的子目标心电采集ADC以固定频率我选250Hz采样AD8232输出的模拟信号数据处理对原始采样值做去基线漂移和简单滤波提取有效波形特征蓝牙上报通过BLE透传模块把数据包发到手机端数据格式要固定可解析实时反馈OLED上实时显示心率数值或波形状态方便现场调试这四个子目标在裸机下相互干扰在FreeRTOS下就是四个独立任务或三个任务加一个中断服务各自运行通过队列交换数据。任务拆分清了后面的代码就顺了。2. 硬件连接与FreeRTOS工程准备先把地基建稳2.1 实验平台与材料清单这个实验的硬件组合很经典我用的是以下配置组件型号/规格作用主控板STM32F103C8T6蓝丸核心板运行FreeRTOS负责采样、处理、通信心电前端AD8232模块采集人体心电信号输出模拟电压蓝牙模块JDY-31或DX-BT05等串口透传模块通过BLE实现串口数据透传显示屏0.96寸OLEDI2C接口SSD1306显示心率和运行状态电源5V USB供电 AMS1117-3.3V稳压给各模块提供稳定电源如果手头用的是其他蓝牙模块比如HC-05这个是经典蓝牙不是BLE或者nRF52832系列连接思路也类似——都是串口接入MCU的USART差别主要在AT指令配置方式和波特率设置上。本实验的关键不在于蓝牙模块品牌而在于MCU通过串口往模块发数据模块以BLE广播/连接方式把数据送到手机这条链路是否通畅。2.2 引脚连接与电源噪声处理硬件接线看着简单实际踩坑的都在细节。AD8232模块的输出引脚OUT接STM32的PA1ADC1通道1模块的LO和LO-是导联脱落检测引脚可以接普通IO口读取状态本实验不涉及脱落检测直接悬空即可。蓝牙模块的RXD接STM32的PA9USART1_TXTXD接PA10USART1_RXVCC接3.3VGND共地。OLED的SDA接PB7、SCL接PB6使用I2C1。**电源是这里最容易出问题的环节。**AD8232对电源噪声非常敏感如果直接用STM32板载3.3V同时给蓝牙模块和OLED供电蓝牙发射时的电流波动会直接耦合到心电信号里波形上出现规律的射频干扰纹。我的处理办法是AD8232单独用一片AMS1117-3.3V从5V降压供电AD8232的电源引脚旁边并联一个10uF电解电容和100nF陶瓷电容模拟地GND和数字地在模块端单点汇合。实测下来这个改动对波形质量的改善非常明显。2.3 FreeRTOS工程搭建与内存规划工程模板我直接用STM32CubeMX生成在Middleware里勾选FreeRTOS选CMSIS_V1接口新版CubeMX默认是CMSIS_V2老工程习惯用V1两者API差异主要在系统节拍和延迟函数上我这次用V1更顺手。生成之后freertos.c里会自动创建一个defaultTask我的习惯是把它改成统计与调试任务其余功能任务全部自己手动创建。内存配置这块要特别注意STM32F103C8T6只有20KB RAM。我的分配如下configTOTAL_HEAP_SIZE8KB堆内存供任务栈和内核对象使用心电采集任务栈256字4KB并非单独分配实际256字即1KB滤波任务栈256字蓝牙发送任务栈512字因为串口发送的库函数调用链比较深OLED刷新任务栈256字剩余空间由FreeRTOS内核自动管理任务栈宁可多给不能少给栈溢出是个慢性病——有时候跑几十分钟才崩一次非常难查。后面第六节我会专门说怎么用FreeRTOS自带的栈溢出检测定位这种问题。3. 任务拆解与实现四个独立线程各司其职3.1 任务划分总览在FreeRTOS下设计架构最重要的是先画清楚谁干什么、干完活把结果交给谁。任务名优先级功能周期/触发方式ECG_Sampling_Task4最高ADC采样心电信号放入原始数据队列定时器中断置位ECG_Process_Task3从队列取原始数据做滤波处理结果放入发送队列队列有数据即唤醒BLE_Send_Task2取出滤波后的数据封包串口发送到蓝牙模块队列有数据即唤醒OLED_Display_Task1最低读取统计信息刷新OLED显示每200msdefaultTask0统计任务栈高水位调试用每2秒高优先级任务做的是实时性要求高但耗时短的操作低优先级任务做的是慢但不需要抢时间的操作。这个优先级顺序是经过考量的采样任务消耗CPU时间很短ADC读取队列写入几微秒完成但绝不能延迟蓝牙发送任务虽然有耗时但可以被打断后再恢复所以优先级放中间。3.2 心电采集任务定时器是采样节奏的主导者采样的核心是均匀性。ADC本身没有定时采样功能它只能在你调用它的时候取一次值。如果靠vTaskDelay来做采样节拍任务可能因为被其他任务抢占而延迟几十微秒甚至几毫秒导致采样间隔抖动。我的做法是用一个硬件定时器TIM2产生250Hz的更新中断中断里只做一件事给采集任务发送一个二值信号量。采集任务阻塞在信号量上每一次获得信号量就立刻执行一次ADC转换。// 定时器中断回调 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { BaseType_t xHigherPriorityTaskWoken pdFALSE; vTaskNotifyGiveFromISR(xECGSamplingTaskHandle, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } // 心电采集任务 void ECG_Sampling_Task(void *argument) { uint16_t adc_value 0; HAL_TIM_Base_Start_IT(htim2); // 启动定时器250Hz中断 for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 等定时器信号 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_value HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); xQueueSend(xRawDataQueue, adc_value, 0); } }这里使用vTaskNotifyGiveFromISR而不是xSemaphoreGiveFromISR是因为任务通知的效率比信号量更高——不需要为每个信号量单独创建内核对象释放和获取都更快。在250Hz这个采样频率下CPU占用率差异不大但如果以后想跑到1kHz这个选择就会明显影响性能。注意一个细节HAL_ADC_PollForConversion是阻塞式等待如果ADC转换失败会卡住10ms。后来我把超时时间改成了2ms——在正常情况下ADC转换只需要几十微秒如果2ms还没完成说明硬件配置有问题没有必要死等让系统直接暴露问题反而更好排查。3.3 滤波任务从一堆毛刺里捞出可用的PQRST波形原始ADC数据直接发到手机波形会是这个样子的基线飘飘忽忽 superimposed 着50Hz工频干扰和肌电噪声整个波形看起来像一条粗毛线R波根本找不准。裸机时代我写过各种滤波函数但放进RTOS后思路可以更清爽——滤波作为一个独立任务吃原始数据吐干净数据。我用的是最简单的滑动平均滤波Moving Average窗口大小16。心电波形的主频成分大约在1~30Hz250Hz采样率下窗口16对应的时间窗口是64ms这个窗口的低通截止频率大约2.5Hz左右会损失一部分波形细节但作为实验验证数据链路是否通畅完全够用。#define FILTER_WINDOW_SIZE 16 static int16_t filter_buffer[FILTER_WINDOW_SIZE]; static uint8_t filter_index 0; static int32_t filter_sum 0; uint16_t moving_average_filter(uint16_t new_sample) { filter_sum - filter_buffer[filter_index]; filter_buffer[filter_index] new_sample; filter_sum new_sample; filter_index (filter_index 1) % FILTER_WINDOW_SIZE; return (uint16_t)(filter_sum / FILTER_WINDOW_SIZE); } void ECG_Process_Task(void *argument) { uint16_t raw_value, filtered_value; for (;;) { xQueueReceive(xRawDataQueue, raw_value, portMAX_DELAY); filtered_value moving_average_filter(raw_value); xQueueSend(xSendDataQueue, filtered_value, 0); } }这个任务几乎没有阻塞点队列里只要有数据它就立刻处理然后继续等CPU开销很小。滑动平均的缺点是有相位滞后对实验来说无所谓如果你想做得更专业一点可以把数据扒出来离线对比几种滤波算法的效果——比如陷波50Hz的Notch滤波或者用一阶高通滤掉基线漂移。那些放到实验8的内容里更合适。3.4 蓝牙发送任务与OLED刷新任务蓝牙发送任务的核心是封包把原始数据包装成带帧头、帧尾和校验的协议格式。裸机时我只简单地发原始值手机端解析全靠猜数据稍微错位整条波形就乱了。这次我设计了固定格式的数据帧详细说明放在第5节。OLED刷新任务每200ms执行一次从全局结构体里读取最近一次心率计算值和当前队列使用率等状态信息拼成字符串后用I2C发送到屏幕。这个任务的耗时约20ms因为SSD1306的缓冲区有1KBI2C传输在400kHz下需要20ms左右如果裸机上干这个采样早就乱了但在RTOS里它优先级最低随时可以被抢占完全不影响其他任务。4. 任务间通信队列长度与阻塞策略怎么定4.1 为什么不用全局变量非要用队列刚开始玩RTOS的时候我也犯过偷懒的错既然是同一个单片机上的多个任务共享内存用全局变量不就行了加个volatile再关一下中断不也能保护临界区理论上是能跑的但实际排查问题的时候会头痛欲裂。全局变量的问题在于它没有任何生产者和消费者握手机制。采集任务往里写一个数据滤波任务不知道有新数据来了要么轮询变量状态浪费CPU要么用别的信号通知等于又实现了一遍队列。而且全局变量被多处修改之后一旦逻辑出错你根本不知道是哪一步把它改坏的。队列就干净得多。xQueueSend把数据拷进队列内部缓冲区xQueueReceive在没有数据时让任务阻塞等待。这种缓冲区同步的双重作用天然地完成了生产者和消费者的解耦。4.2 队列长度的计算队列长度给多少给太短采集任务xQueueSend时队列满了返回errQUEUE_FULL数据被丢弃给太长浪费RAM。我一开始给原始数据队列长度设成32跑起来后发现一个问题滤波任务执行速度非常快每次从队列取一个点就处理一个点队列几乎不可能满。但蓝牙发送任务那边如果串口发送被更高优先级任务打断发送速度会周期性变慢队列就有堆积风险。实测后我把两个队列都定为64个元素。以250Hz采样率来算64个采样点对应大约256ms的缓冲时间就算蓝牙发送卡顿半秒以内数据也不会丢。如果RAM宽裕设到128也行但在F103C8T6这种只有20KB RAM的片子上队列元素越多留给其他功能的堆空间就越少要平衡。4.3 队列使用中的两个注意事项第一队列拷贝的是数据本身不是指针。我在创建队列时元素大小是sizeof(uint16_t)也就是2字节。千万别在栈上定义一个局部变量然后往队列里发这个局部变量的地址等函数返回后地址失效接收方拿到的就是野指针数据。第二发送时的超时时间设为0。xQueueSend的第三个参数是等待时间如果队列满了任务可以选择挂起等待。但我这里采集任务优先级最高等不起——它必须把当前采样值尽快交给下层就算队列满了丢一个新样本比让整个采样链路卡住要好得多。所以发送超时设0发送失败就丢弃数据保证采样永不阻塞。5. 蓝牙数据帧设计手机端拿到的必须能解析5.1 透传模块与自组协议的选择STM32F103C8T6本身没有BLE射频硬件所以要实现蓝牙BLE通信路线有两条。路线一是换主控比如用nRF52832或ESP32-C3这类自带BLE的SoC直接在芯片上跑BLE协议栈路线二是在F103外面挂串口透传BLE模块JDY-31、DX-BT05这类MCU只管往串口扔数据模块负责建立BLE连接和数据透传。本实验用的是路线二。理由很实际实验主题是FreeRTOS重点在于多任务调度和通信架构BLE底层协议栈不是本实验的训练目标。串口透传模块把BLE通信封装成无线串口MCU侧只需要关心USART怎么发数据上手快调试也直观。如果你用的是ESP32-S3这类自带BLE的芯片那就要去接esp32的BLE APIGATT server、service、characteristic都得自己搭逻辑会复杂很多但那属于另一种实验范畴了。本实验用透传模块等价于把蓝牙当串口线用简单可靠。5.2 数据帧格式定义蓝牙发送出去的数据如果只是一串不带格式的二进制数值手机端收到的流根本没法切分。我用的是最经典的帧协议Byte0 Byte1 Byte2 Byte3 Byte4 Byte5 0xA5 0x09 0x01 dataH dataL checksumByte0帧头固定0xA5Byte1帧长度从Byte0到Byte5共6字节所以是0x06Byte2数据类型0x01代表心电原始值滤波后0x02代表心率值Byte3/Byte416位心电采样值大端模式高字节在前Byte5校验字节对Byte0到Byte4做异或和void BLE_Send_Task(void *argument) { uint8_t tx_buffer[6]; uint16_t ecg_value; for (;;) { if (xQueueReceive(xSendDataQueue, ecg_value, pdMS_TO_TICKS(100)) pdTRUE) { tx_buffer[0] 0xA5; tx_buffer[1] 0x06; tx_buffer[2] 0x01; tx_buffer[3] (ecg_value 8) 0xFF; tx_buffer[4] ecg_value 0xFF; tx_buffer[5] tx_buffer[0] ^ tx_buffer[1] ^ tx_buffer[2] ^ tx_buffer[3] ^ tx_buffer[4]; HAL_UART_Transmit(huart1, tx_buffer, 6, 20); } } }校验字节用异或和简单高效足够对付单比特错误。手机端BLE调试助手收到数据后按帧头0xA5定位帧起点取6字节校验通过后组合出16位心电值。数据格式固定后不管是用手机APP还是C#上位机解析逻辑都只需写一次。5.3 波特率与采样率的匹配计算这个计算很多人会忽略但恰恰是蓝牙显示波形断断续续的头号原因。250Hz采样率每个采样点16位也就是每秒产生500字节的原始负荷。套上6字节帧格式后每秒实际发送字节数 250帧 × 6字节/帧 1500字节/秒。如果蓝牙模块出厂默认波特率是96008位数据位1位停止位无校验每字节实际占用10个bit9600波特率能承载的极限速率是960字节/秒。1500字节/秒的负荷远超960字节/秒的能力串口发送必然来不及。数据在缓冲区堆积表现为越积越多最后溢出丢包波形断断续续。解决办法是提高波特率。把蓝牙模块和MCU的USART1都配置为115200能承载的速率变成11520字节/秒115200/10远大于1500字节/秒的需求余量充足。注意如果用HC-05这种模块配对后串口波特率要和AT指令里配置的一致两端必须同步改。5.4 手机端查看效果的方式调试阶段直接在手机装一个BLE串口调试工具比如BLE调试助手或nRF Connect连接蓝牙模块在RX界面就能看到源源不断的十六进制数据。每6字节一组开头是A5 06 01后面跟着数据字节和校验字节。把十六进制转成十进制就能得到心电波形数值。想看到波形可以用支持波形显示的BLE工具或者把数据转存到电脑上画图。我自己实际调试时偷了个懒——先在OLED上打一个数据帧正确的状态标志确定链路通畅后再去手机端观察波形细节这样能省掉很多来回切APP的时间。6. 联调实测三个典型问题的完整排查过程6.1 问题一蓝牙偶尔出现0xFF帧头现象手机端收到的数据流中偶尔会出现一串以FF FF FF开头的乱码大概几秒钟一次一出现就是三五帧连在一起。排查链路第一步先看是不是数据帧协议问题。用逻辑分析仪挂在USART1的TX引脚上抓到的波形显示MCU发送的每一帧都是正常的A5 06 01 ...没有错帧。第二步怀疑蓝牙模块的固件缓冲区溢出。因为蓝牙模块的串口波特率固定但BLE无线传输的速率会受到距离和干扰影响当无线信道拥挤时蓝牙模块内部缓冲区满SLAVE设备会向主机发送暂停信号导致模块的数据在内部堆积。第三步在MCU侧添加一个流控信号线把模块的STATE引脚接到MCU的GPIO发送前先查询模块是否允许接收数据。最终解决给JDY-31模块的STATE引脚接上拉电阻GPIO读高电平表示模块空闲可接收读低电平表示模块忙MCU等待。加上这个软件流控后乱码帧消失。// 伪代码示意 if (HAL_GPIO_ReadPin(BLE_STATE_GPIO_Port, BLE_STATE_Pin) GPIO_PIN_SET) { HAL_UART_Transmit(huart1, tx_buffer, 6, 20); }6.2 问题二任务栈溢出导致系统周期性复位现象系统跑2~3分钟后突然复位看门狗没有动作复位后又能跑几分钟周而复始。排查链路FreeRTOS的configCHECK_FOR_STACK_OVERFLOW宏有两个检查级别我设置成2最严格然后开启了栈溢出钩子函数vApplicationStackOverflowHook。在钩子里打印当前任务名定位到是BLE_Send_Task溢出。为什么这个任务会溢出因为HAL_UART_Transmit的调用链比较深而且在校验和计算时用局部数组变量比较多。我把BLE_Send_Task的栈从256字提到512字重新编译烧录跑了半小时没再复位。用FreeRTOS的uxTaskGetStackHighWaterMark可以实时看到任务栈剩余最小值我习惯在每个任务里定期把高水位值打印出来调试完再删掉。这里分享一个经验**malloc会用栈printf会用栈HAL库函数会用栈每个库函数的栈消耗都不可小觑。**裸机下根本没有这个概念不少人刚上RTOS时都栽在栈溢出这个坑上。6.3 问题三心电波形出现周期性挖坑现象手机端波形整体正常但每隔大约1秒出现一个明显的凹陷好像波形被某个规律动作挖掉了一块。排查链路一开始怀疑是滤波器的窗口效应但窗口大小16对应64ms周期不可能1秒才出现一次。怀疑是蓝牙模块的广播周期干扰——BLE协议本身有广播和连接事件周期如果无线链路偶尔丢包表现就是每秒掉一个点。于是把OLED刷新任务暂时停掉波形凹陷消失重新恢复OLED凹陷又出现。原因清楚了OLED刷新任务每隔200ms调用一次I2C写屏幕缓冲区的操作I2C在传输过程中会独占I2C时钟线但问题不在于I2C本身而在于OLED刷新任务可能和蓝牙发送任务同时抢一个I2C外设导致串口发送被延迟。进一步追踪发现我的OLED任务用了HAL_I2C_Mem_Write这个函数是阻塞式的在写1KB数据时会卡在while循环里等I2C空闲而I2C空闲的判断恰恰被我配置成等待传输完成一旦OLED刷新和蓝牙发送时间碰在一起总线仲裁延时就会导致蓝牙帧发送晚了几个字节。解决思路要么给I2C加超时不阻塞要么把OLED刷新改成非阻塞的DMA方式。我选择了后者——OLED数据要发送到SSD1306的GDDRAM用DMA方式刷新CPU在传输期间可以运行其他任务。改完之后周期性凹陷消失。6.4 排查小结问题根因解决方式蓝牙乱码帧蓝牙模块缓冲区溢出无线吞吐不足增加STATE引脚软件流控周期性复位BLE_Send_Task任务栈溢出栈从256字调到512字波形周期性凹坑OLED I2C阻塞式发送抢占总线改为DMA非阻塞刷新这三个问题有个共同点都不是某一行代码的语法错误而是多任务并发带来的时序问题。在裸机里你不会同时遇到线程调度、资源竞争、栈深浅、外设总线仲裁这几个维度的问题在RTOS里这些问题全部要在设计阶段考虑好否则排查起来特别费劲。7. 数据验证与优化空间从能跑到可靠7.1 波形验证与心率计算项目跑通后的验收标准包括三部分波形连续无断点、手机端解析数据正确、心率数值误差在允许范围内。波形验证我用100k电位器模拟心电信号心电模块自带的测试模式在手机端连续观察30秒确认波形无中断。心率数值的计算可以用最基础的R波检测设定一个动态阈值比如取最近1秒数据的最大值×0.6如果新采样值超过阈值且与前一个R波间隔超过200ms就认为检测到一个R波。统计两个R波之间的时间间隔60除以间隔秒就是瞬时心率。这个算法非常粗糙实际心电监护里有更复杂的QRS波检测算法Pan-Tompkins等但作为实验验证采集-传输-解析链路的完整性已经够用。7.2 性能余量检查系统跑稳之后用uxTaskGetStackHighWaterMark检查每个任务的栈余量结果如下任务分配栈字最大使用量字预留余量ECG_Sampling_Task25618076ECG_Process_Task256120136BLE_Send_Task512360152OLED_Display_Task25619066栈余量都还比较健康。CPU占用率用vTaskGetRunTimeStats统计250Hz采样滤波蓝牙OLED这套组合下来CPU占用大约35%余量充足。将来如果想加心率计算、导联脱落检测、或者把采样率提到500Hz都还有空间。7.3 可以继续深挖的方向如果这个实验做完还有余力我个人推荐的扩展方向有三个。一是把ADC改成DMA循环采集模式配合双缓冲可以进一步降低CPU占用二是把滤波算法升级成陷波50Hz带通滤波波形质量会明显提升三是把蓝牙数据帧加上序号和时间戳便于上位机做更精确的分析。每个方向都能单独写一篇实验笔记但核心的FreeRTOS任务调度队列通信稳定发送这套基本功在这个实验7里已经全部覆盖了。最后说一句我个人的体会这个实验真正的门槛不在硬件也不在蓝牙协议而在思维的转变。从一个while循环加几个中断到多个任务彼此独立、通过消息队列协作这种思维方式不亲手做一遍实验、不踩几个时序的坑很难真正建立起来。实验7做完之后我自己回头看之前写的裸机代码确实能明显感觉到有了RTOS的视角处理复杂嵌入式应用的思路清晰了很多。本文还有配套的精品资源点击获取