基于STM32的心电图监测与蓝牙传输App设计全解析
2026/9/16 16:35:08 网站建设 项目流程

简介:这是一份基于STM32与Android的心电图监测蓝牙传输毕业设计完整源码,适合计算机、自动化、电子信息等专业学生用于课程设计、毕业设计或项目实战。资源共包含98个文件,以C语言和头文件源码为主体,附带Keil5工程配置、hex固件、原理图PDF、项目说明及启动文件等,压缩包仅414KB,目录模块划分清晰,便于快速定位到采集、显示、蓝牙等各部分代码。硬件部分以STM32单片机为核心,通过ADC实时采集心电电压信号,再由DMA高效搬运数据,并借助HC-05蓝牙模块无线发送至Android端;App使用Android Studio开发,支持蓝牙连接、心电图实时绘制、历史数据回看和图片保存。整套方案从底层寄存器配置到上层界面交互覆盖完整,能为读者提供嵌入式与移动端联合开发的系统化思路,也可在此基础上扩展多类生理信号监测功能。目前已有505人学习下载,具有较高参考与借鉴价值。

1. 心电图监测蓝牙传输这套毕设方案,先想清楚这几件事

拿到「基于STM32心电图监测蓝牙传输app设计」这类毕设源码包时,第一反应不该是打开工程文件看代码,而是先把系统边界划清楚:信号从哪来、经过几级处理、以什么格式发给 app、app 拿到之后又能做什么。这个链路里最容易翻车的地方,往往不在 STM32 本身,而在模拟前端采样和蓝牙传输的握手细节上。常见做法是 ADS1292 或 AD8232 采集心电信号,STM32 做滤波与特征提取,蓝牙模块(HC-05/06 或低功耗 BLE 模块)把数据推给 Android/iOS 客户端。这套方案的定位很明确:面向毕设答辩和功能演示,重点是把「可复现」放在「临床准确」前面——课程设计和毕业设计不需要过医疗认证,但必须让每个中间环节都能被提问、被验证。适合的人群是电子信息、生物医学工程、嵌入式方向的本科生,以及想快速上手完整 IoT 医疗链路的开发者。下文从硬件选型到 app 端收数,把一条能跑通的最小通路拆开讲。

2. 从模数转换到蓝牙协议:心电图数据链路的设计与选型

2.1 模拟前端选型:为什么毕设几乎都绕不开 ADS1292 或 AD8232

心电信号的本质是毫伏级的差分电压信号,频带集中在 0.05Hz~100Hz 之间,幅度约 0.5mV~4mV。如果直接把电极输出接到 STM32 的 ADC 引脚上,会得到一幅充满工频干扰和基线漂移的「毛刺图」。常见的做法有两种:一是用 AD8232 这类模拟前端芯片,它内部集成了仪表放大器和二阶高通/低通滤波器,输出的是经过初步调理的模拟信号,再接 STM32 内置 ADC 采样;二是用 ADS1292R 这类集成 24 位 Δ-Σ ADC 的专用芯片,它能把右腿驱动、导联脱落检测、数字滤波全部打包,直接通过 SPI 把数字量传给 MCU。

从参数角度看,ADS1292R 的优势是分辨率高、采样率可从 125SPS 到 8kSPS 配置,而且内置的呼吸测量功能在答辩时能多一个展示点;AD8232 则胜在外围简单、成本低、参考资料多。毕设场景里,如果题目明确写了「设计」而不是「实现」,选 ADS1292R 会更容易讲出工作量,因为它涉及 SPI 时序、寄存器配置、数据就绪中断等更深一层的嵌入式内容。AD8232 方案则更贴近「快速出一个实物」的需求,代码量可以少一半以上。

2.2 蓝牙模块选型:BLE 优先于经典蓝牙的四个理由

蓝牙传输部分的选型,决定了 app 端开发的难度。经典蓝牙(HC-05、HC-06)的好处是透传简单,STM32 往串口写数据,手机端就能通过 SPP 服务收到,但它有两个硬伤:功耗高、Android 端 SPP 支持不稳定;而 iOS 系统原生不支持 SPP,如果老师要求同时兼容两端,就会被迫绕道 MFi 或外置串口工具。BLE 方案(如 HC-42、JDY-23、nRF52832 模组)则天然避开了这些问题,手机端通过 GATT 协议读写 Characteristic,采样率不高时完全够用。

这里要算一笔带宽账。心电图单导联 250SPS、每个样本用 2 字节表示,那么秒级数据量只有 500 字节,即使加上包头、时间戳和循环冗余校验,每秒钟不超过 600 字节,BLE 的 20 字节单包载荷每次写操作就能承载约 10 个样本。换句话说,BLE 的理论吞吐瓶颈根本不会卡在心电数据上。如果图省事必须用 HC-05,那要注意它默认的波特率通常是 9600,而 ADS1292R 用 SPI 读到的数据往往一包就有 9 字节(状态位加双通道各 24 位),解析逻辑要严格按帧格式来,否则手机上看到的就是乱码。

2.3 数据帧协议:不加帧头帧尾的蓝牙传输都是在给答辩埋雷

从 STM32 到手机 app 的这一段,不能直接裸发原始 ADC 值。蓝牙传输是流式的,没有 TCP 那种可靠的字节流边界概念,接收端如果不做分包和粘包处理,任何一次丢包都会导致后续所有数据错位。一个在毕设里足够用的帧格式设计如下:

typedef struct { uint8_t header; // 固定为 0xAA uint8_t length; // 数据区长度+3 uint8_t channel; // 0x01 表示第一导联数据 uint16_t adc_value; // 原始采样值,大端模式 uint8_t rssi; // 可选的蓝牙信号强度占位 uint8_t crc; // 对前 5 字节做异或校验 } ecg_frame_t;

逻辑说明:接收端不断读取字节,遇到 0xAA 就认为可能是一个帧的开始,再读 length 字段,如果 length 在预置区间内,就继续读 channel、数据位和校验位;最后对已收到的字节做一次 CRC 比对,吻合则进入解析流程,不吻合则把当前字节丢弃并回到等待帧头的状态。这样做的好处是,即使丢失了帧中某个字节,下一帧的 0xAA 仍然能重新同步,不会造成持续错乱。

参数说明:header 选 0xAA 而不是 0x55,是因为 0xAA 在二进制下是 10101010,与常见的连续 0x00/0xFF 噪声模式区分度更高;length 字段建议用实际数据长度加固定偏移,这样收到非法长度时可以直接判错;CRC 不必用 MultiBus 或 CRC16,单字节异或校验在课堂演示场景下足够抵抗偶发干扰,又能减少 STM32 的开销。

3. STM32 端工程搭建:芯片选型、时钟配置与心电数据处理最小实现

3.1 芯片选型建议与 CubeMX 初始化要点

毕设源码里最常见的芯片是 STM32F103C8T6 和 STM32F407VET6,两者都能胜任本方案,但工作量和答辩讲解的侧重点完全不同。F103C8T6 的优势是资料极多、开发板价格低、Keil 工程模板到处都能找到;缺点是处理 250SPS 心电数据时虽然完全够用,但如果你想在设备端做 QRS 波检测或者心率变异性分析,内存会显得紧张。F407 的 FPU 和更高主频对后续加算法更友好,但小板上布线稍微复杂,对新手而言外围电路反而容易出错。

初始化时有一个容易被忽略的关键点:使用 ADS1292R 方案时,SPI 和外设中断的优先级必须匹配。建议在 CubeMX 中把 SPI 时钟设为分频后约 2MHz,并开启 SPI 的 DMA 接收,否则读寄存器时一个 GPIO 模拟片选加一次 SPI 阻塞传输虽能工作,但 CPU 占用率会接近六成,留给滤波算法的时间就少了。ADC 内置方案则要特别注意参考电压,STM32 的 VDDA 引脚若是直接接 3.3V,采样值噪声会比用独立基准源大很多。

3.2 ADS1292 寄存器配置顺序:先配置后采集才能出稳定波形

ADS1292R 的寄存器配置是这类毕设最容易卡壳的地方。常见做法是上电后先延时 10ms 以上等待内部晶振稳定,然后依次写 CONFIG1、CONFIG2、CONFIG3、CH1SET、CH2SET、RLD_SENS、LOFF_SENS 等寄存器,最后才设置 START 位。如果顺序颠倒,很可能出现数据持续为 0xFFFFFF 或者导联脱落标志位一直被置位的情况。

void ads1292_init(void) { uint8_t cfg[] = { 0x00, 0x01, 0x10, 0x20, 0x60, 0x00, 0x00, 0x00 }; // 配置命令格式:SDATAC 退出连续采集 + WREG 写寄存器 ads1292_send_cmd(0x11); // SDATAC,停止连续读取模式 ads1292_write_reg(0x01, 0x01); // CONFIG1:260Hz 采样率,内部测试信号关 ads1292_write_reg(0x02, 0x10); // CONFIG2:输出 0.5V 参考,测试信号接入 ads1292_write_reg(0x03, 0x20); // CONFIG3:开启内部参考、右腿驱动使能 ads1292_write_reg(0x05, 0x60); // CH1SET:增益 6,正常输入 ads1292_write_reg(0x07, 0x00); // RLD_SENS:第一导联正输入接右腿驱动 ads1292_send_cmd(0x10); // RDATAC,回到连续采集模式 }

逻辑说明:SDATAC 命令的作用是让芯片退出默认的连续输出状态,从而允许通过 SPI 写入配置寄存器,如果省略这一步,后续所有写寄存器操作都会被芯片忽略。CONFIG1 的采样率字段决定每秒输出的数据组数,这里设为 260Hz 是因为心电的有效能量集中在几十赫兹以内,260SPS 既能减少数据量,也能给 app 端画图留下足够的刷新率。CONFIG3 里开启右腿驱动后,人体共模干扰会被反馈回路抑制,实测波形中的 50Hz 工频分量会明显下降。

参数说明:如果你是接 STM32F103C8T6,SPI 的 CPOL 和 CPHA 要与 ADS1292 的数据手册匹配,通常设置为模式 1(CPOL=0, CPHA=1),不匹配时读回的数据会整体移位成看似随机的值。调试时可以用逻辑分析仪抓 SPI 时序,比对芯片手册上的时序图,这比盲调寄存器有效得多。

3.3 心电数据的滑动平均滤波与基线漂移抑制

ADC 读回来的原始数据不能直接送到蓝牙,至少要做两级处理:一是去除直流偏置和低频基线漂移,二是抑制随机噪声。最常见的做法是滑动窗口平均加一阶高通滤波。窗口长度要兼顾延迟和滤波效果,260SPS 下取 10 个点平均,对应的时间窗口约 38.5ms,不会让 R 波被明显削平。

#define SAMPLE_RATE 260 #define WINDOW_SIZE 10 static int16_t ring_buf[WINDOW_SIZE]; static uint8_t buf_index = 0; static int32_t sum = 0; int16_t ecg_filter(int16_t raw) { sum -= ring_buf[buf_index]; ring_buf[buf_index] = raw; sum += raw; buf_index = (buf_index + 1) % WINDOW_SIZE; return (int16_t)(sum / WINDOW_SIZE); }

逻辑说明:这里使用环形缓冲区而不是每来一个新样本就重算全部窗口和,是出于嵌入式资源消耗的考虑。每次更新只需要做一次减法、一次加法和一次除法,时间开销恒定。原始心电信号的基线会因呼吸和电极移动而上下漂移,滑动平均后相当于让高频噪声被压制,低频率基线仍然保留,这一级处理足够让手机端画出的波形看起来「没有毛刺且始终在屏幕中间」。

参数说明:窗口大小调大可以进一步增强平滑效果,但 R 波本身宽度只有约 80ms,窗口超过 15 个采样点就会把 R 波幅度压低,心率计算时可能出现漏检。窗口大小调小则噪声抑制不足。如果你在 app 端发现波形锯齿感明显,可以优先把这处的 WINDOW_SIZE 从 10 调到 6,然后观察波形变化,而不是急着改蓝牙协议。

3.4 心率计算:时域阈值法的实现与边界处理

心率计算在设备端做还是 app 端做,取决于你希望蓝牙传输的数据量。让 STM32 每秒钟只发一次心率值,比把全部波形发过去省电得多,但会牺牲 app 端绘制实时波形的能力。折中方案是设备端算心率、也发波形,这样一旦心率计算算法效果不佳,答辩时还能切换 app 端的备用窗口显示原始波形。

uint16_t ecg_get_heart_rate(int16_t filtered) { static int16_t last_value = 0; static uint8_t refractory = 0; int16_t threshold = 300; if (refractory > 0) { refractory--; return 0; } if (filtered > threshold && last_value <= threshold) { refractory = 15; return 1; } last_value = filtered; return 0; }

逻辑说明:阈值法把采样点与参考阈值进行比较,当信号从阈值以下跃迁到阈值以上时记为一个心拍。代码中的 refractory 相当于不应期,心跳事件产生后强制跳过 15 个采样点,这段间隔约 58ms,能有效防止 T 波或噪声被误判成一次心跳。阈值本身不能设死,否则不同人的信号幅度差异会让检测失效,建议在预处理阶段统计一段时间的最大值,取最大值的 60% 作为自适应阈值。

参数说明:这里的 300 只是一个占位值,实际需要根据增益和导联位置调整。更稳妥的做法是在系统启动后先采集 3 秒静息数据,计算峰值平均后得出动态阈值,再进入正常检测流程。需要注意的是,如果滤波后的数据是带符号整数,阈值判断要统一对绝对值或对正向峰值处理,否则倒置的 R 波会被直接忽略。

4. 手机 app 端数据接收与波形绘制:从蓝牙连接封装到实时画图

4.1 Android 蓝牙 BLE 连接的最小流程

app 端常见的开发方式是使用 Android 原生 BluetoothLeScanner 配合 Kotlin 协程,把蓝牙操作封装成一个单例对象。毕设源码里经常能看到直接在 Activity 里写一长串回调代码的写法,这样做在答辩时演示快速连接没有问题,但若要加入断线重连、数据缓存等功能,重构成本会很高。最省事且稳定的路径是:扫描 → 连接 → 发现服务 → 订阅通知。

private fun connectToDevice(device: BluetoothDevice) { bluetoothGatt = device.connectGatt(this, false, gattCallback) } private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices() } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { val service = gatt.getService(UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb")) val characteristic = service.getCharacteristic(UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb")) gatt.setCharacteristicNotification(characteristic, true) } override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val data = characteristic.value handleEcgFrame(data) } }

逻辑说明:connectGatt 的第二个参数设为 false 表示不自动重连,这样可以避免设备端意外断电时 app 反复发起无效连接。发现服务后要对目标服务的 UUID 做一次判断,HC-42 这类模块默认服务 UUID 通常是 0xFFE0,特征是 0xFFE1,如果设备端用的是自定义 UUID,也要先在 STM32 端把广播名称和服务 UUID 打印出来核对,否则回调永远不会触发。

参数说明:setCharacteristicNotification 只是打开了通知开关,大多数 BLE 模块还要求向特征的 0x2902 描述符写入 0x01 才能真正启用通知。若你在真机上发现连接成功但收不到数据,九成是漏了这一步。写法是在 onServicesDiscovered 里再调用 writeDescriptor。

4.2 波形绘制的性能陷阱:用 SurfaceView 而不是频繁 invalidate

把心电数据画到屏幕上,最简单的方案是自定义 View 重写 onDraw,再加 invalidate 触发刷新。但这个方案在数据速率为 260SPS 时会出现掉帧,因为每次刷新都要重新绘制整条曲线,加上 Android 主线程还要处理蓝牙回调,掉帧几乎不可避免。更常见且可靠的做法是使用 SurfaceView 的独立绘制线程,或者直接在 Canvas 上做增量绘制。

// SurfaceView 绘制线程核心逻辑 while (isRunning) { Canvas canvas = holder.lockCanvas(); if (canvas != null) { canvas.drawColor(Color.BLACK); Paint linePaint = new Paint(); linePaint.setColor(Color.GREEN); linePaint.setStrokeWidth(2f); Path path = new Path(); for (int i = 0; i < ecgBuffer.size(); i++) { float x = i * 2f; float y = 400f - ecgBuffer.get(i) / 10f; if (i == 0) path.moveTo(x, y); else path.lineTo(x, y); } canvas.drawPath(path, linePaint); holder.unlockCanvasAndPost(canvas); } Thread.sleep(30); }

逻辑说明:这里用一个数组作为环形缓冲区,每收到一个新样本就覆盖最旧的数据。绘制时每次把整条路径重建一次,路径节点数量等于缓冲区长度,典型值 512 个点。30ms 的刷新周期对应约 33fps,对心电波形展示来说足够流畅,而且不会消耗太多电量。锁画布和提交画布必须成对出现,如果在 lockCanvas 之后抛出异常导致没有 unlock,后续绘制会一直阻塞。

参数说明:x 坐标间距 2f 是假设屏幕宽度足够容纳 512 个点,换成平板或横屏时需要按屏幕宽度动态计算。y 坐标里的 400f 是基线位置,心电信号幅值波动较大时可以把画布高度的二分之一作为基线,再用一个放大系数把数据映射到画布高度范围内。显示范围偏小时,优先调放大系数而不是调数据源。

4.3 数据帧在 app 端的解析与异常处理

蓝牙通知回调里拿到的是字节数组,app 端要做与 STM32 端对应的帧校验。这里要特别处理一个情况:一次通知不一定刚好包含一帧完整数据,可能是半帧,也可能包含两帧。解决方法是维护一个累计缓冲区,先把收到的字节全部追加进去,再循环尝试从缓冲区头部解析。

# Kotlin/Java 里这类逻辑比较啰嗦,用 Python 描述解析思路 def parse_ecg_stream(data_stream): frames = [] while len(data_stream) >= 7: if data_stream[0] != 0xAA: data_stream.pop(0) continue length = data_stream[1] if length != 6: data_stream.pop(0) continue crc = 0 for i in range(5): crc ^= data_stream[i] if crc != data_stream[5]: data_stream.pop(0) continue frames.append(data_stream[0:6]) del data_stream[0:6] return frames

逻辑说明:while 循环持续处理直到剩余字节不足一帧。帧头不对就弹出当前字节重新查找同步头;长度字段不对说明帧结构已损坏,同样前移一个字节;校验失败时丢弃当前帧头重新寻找同步。这套逻辑可以单独写成单元测试,用随机丢帧的模拟数据验证解析器不会卡死。

参数说明:帧长度字段建议在协议设计时固定为 7 字节(含帧头、长度、通道、数据高低位、信号强度和 CRC)。如果长度只占一个字节,最大只能表示 255 字节,心电数据不会超出,因此用定长帧能显著降低解析复杂度。

5. 快速验证法:没有真实心电信号时先跑通全链路

设备端采集和 app 端解析如果分头调试,出问题时责任不好界定。这里给一个在毕设验收前非常管用的全链路自检方法:在 STM32 端用定时器生成规则的模拟心电数据,即正弦波或合成 P-QRS-T 波序列,直接把这个序列按统一帧格式发往蓝牙,app 端只验证「收到的帧校验通过率」和「绘制出的波形是否与预期吻合」。

void generate_synthetic_ecg(uint8_t *buf, uint16_t sample_index) { int16_t value = (int16_t)(200 * sin(2 * PI * sample_index / 260.0)); buf[0] = 0xAA; buf[1] = 0x06; buf[2] = 0x01; buf[3] = (uint8_t)((value >> 8) & 0xFF); buf[4] = (uint8_t)(value & 0xFF); buf[5] = buf[0] ^ buf[1] ^ buf[2] ^ buf[3] ^ buf[4]; }

逻辑说明:这个伪数据生成器产生的是一组规律性极强的正弦序列,周期为 1 秒,对应 60bpm 心率。用它验证 app 端时,若波形图上能看到平滑的正弦曲线,说明蓝牙链路、帧解析和绘图代码都正常;如果波形乱跳或断裂,则问题大概率在帧格式或缓冲区处理上,与心电信号调理无关。验证通过后,再把定时器触发的生成函数替换成 ADC 或 SPI 读取逻辑,就能逐步排查硬件问题。

参数说明:采样索引 sample_index 需要在定时器中断里自增并取模 260,保证波形周期稳定。你可以把真实数据与模拟数据用一个按键切换,这样答辩时先演示模拟信号的规律波形,再切换到真实人体信号,对比效果非常直观,也能证明你的硬件链路具备采集能力。

最后一个值得专门验证的参数是波特率匹配。HC-05 默认 9600,HC-42 默认 115200,STM32 的 USART 分频寄存器配置错了会导致整数倍的乱码或偶尔漏字符。拿到新模块时先在串口助手里发 AT 指令确认当前波特率,再把它固定到 115200 并写到蓝牙模块的非易失配置里,这样可以避免每次上电都重新配对一次。整个工程的调试顺序建议按「串口打印 → 有线串口助手看帧 → 蓝牙透传 → app 端显示」四步走,每一步都验证通过后再进下一步,这套流程比直接在手机上看到乱码再回头改代码要省得多。

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

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

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

立即咨询