STM32+ADS1292+蓝牙App心电监测系统设计与实现
2026/9/16 13:07:36 网站建设 项目流程

简介:基于STM32的心电图监测蓝牙传输app设计毕设源码包,适合计算机、自动化、电子信息等相关专业学生用于课程设计或毕业设计。项目评审得分95分,经严格调试可正常运行。整体包含98个文件,压缩包仅414KB,涵盖41个C头文件、40个C源代码文件,以及Keil工程文件、hex固件、原理图PDF和项目说明文档等,结构完整。硬件部分基于STM32单片机,利用ADC采集心电信号并经DMA传输,通过HC-05蓝牙发送;软件部分基于Android Studio开发,可实现蓝牙连接、实时心电图展示、历史记录查看与图片保存。目前已有505人学习下载,源码注释清晰,便于在此基础上进行二次开发与功能扩展。

1. 为什么心电监测要从 STM32 和蓝牙 App 开始

绝大多数人的第一印象里,心电图监测是中大型医疗设备才能做的事,十二导联、热敏纸打印、心内科医生判读,每一样看起来都离个人开发者很远。但换成工程视角,单导联心电采集的链路并不复杂:模拟前端把毫伏级心电信号放大并滤除噪声,微控制器完成 ADC 采样与数字滤波,再通过蓝牙把数据帧推给手机 App 实时绘制波形。这是把信号链、嵌入式、无线传输和移动端开发串在一起的完整毕业设计,也是少数几个能从零到一整套跑通的软硬结合项目。

这个标题下的 .zip 里,核心资产是 STM32 固件源码、Android App 源码和原理图 PCB 工程。拿到它之后,你要做的不只是烧录跑通,还要能说清楚 ADS1292 或 AD8232 的采样配置、HC-05 蓝牙模块的透传机制、App 端的 BluetoothSocket 通信和实时波形绘制逻辑。下面按信号流的方向,从硬件选型一路讲到 App 曲线刷新,中间会穿插可直接抄用的代码和调试经验,适合准备答辩、找嵌入式相关工作、或者想快速落地一个可穿戴心电原型的开发者。

2. 硬件链路搭建:ADS1292 采集、STM32 主控与蓝牙模块选型

2.1 为什么毕设方案普遍选 ADS1292 而不是 AD8232

心电信号本身幅值只有 0.5mV 到 4mV,频带集中在 0.05Hz 到 100Hz,直接送进 STM32 的 ADC 几乎没有可用的分辨率。市面上常见的两种处理方式:一是用 AD8232 这种模拟前端芯片,自带仪表放大器和二阶高通低通滤波,输出模拟信号再接 STM32 内部 ADC;二是用 ADS1292 这种集成 24 位 Delta-Sigma ADC 的专用模拟前端,通过 SPI 直接输出数字心电数据。

毕设项目里我一般会优先推荐 ADS1292。原因有三点:第一,内置 PGA 可编程增益放大,不需要额外搭运放电路,原理图更简单;第二,24 位分辨率配合高采样率,能直接体现数字信号处理的优势,滤波算法有发挥空间;第三,ADS1292 是 TI 为 ECG 应用设计的,自带右腿驱动、导联脱落检测和内部测试信号,这些功能在答辩演示时非常加分。AD8232 的优势在模拟链路直观可见,适合偏硬件方向的同学,但如果你后续要做心率变异性分析、QRS 波检测这类算法,数字输出显然更友好。

2.2 STM32 最小系统与晶振电容的计算要点

主控芯片从 F103C8T6 到 F446RE 都能跑,关键指标是有一个硬件 SPI 和一个 USART 串口。F103C8T6 是毕设最常见的选型,72MHz 主频处理 250Hz 到 500Hz 采样率的心电数据绰绰有余,而且绝大多数网上教程都以它为例,遇到问题好搜。

最小系统部分,要注意热词里出现过的STM32 晶振电容计算。8MHz 主晶振的负载电容公式是 CL = (C1 × C2) / (C1 + C2) + Cstray,其中 Cstray 是 PCB 走线和引脚寄生电容,通常按 3pF 到 5pF 估算。如果晶振要求的 CL 是 18pF,那么 C1 和 C2 各取 30pF 左右比较合适。实际焊板时如果波形起振不稳定,优先排查这两个电容的容值是否对称,而不是先怀疑芯片坏了。

模块型号/方案接口说明
模拟前端ADS1292SPI(SCLK/MOSI/MISO/CS)24 位 Δ-Σ ADC,内置 PGA,双通道
主控STM32F103C8T6SPI1 + USART172MHz,64KB Flash,20KB SRAM
蓝牙HC-05(经典蓝牙 SPP)USART2 或 USART1 复用透传模式,默认波特率 9600
电源3.7V 锂电池 + AMS1117-3.3模拟和数字分开走线,避免地弹干扰

STM32 与 ADS1292 的接线就五根线——VCC、GND、SCLK、DIN、DOUT、DRDY(数据就绪引脚)。DRDY 是 ADS1292 拉低表示新数据已经准备好,STM32 用 GPIO 外部中断或者轮询检测这个下降沿,再发起 SPI 读取,这是整个采集时序的核心。

2.3 蓝牙模块选型:HC-05 透传与低功耗 BLE 的取舍

在这个方案里,蓝牙承担的是双向透传角色。HC-05 是经典蓝牙 2.0 模块,市面上 10 块钱上下,默认工作在从模式,支持 AT 指令配置,上电后自动进入透传状态。Android 端用 BluetoothAdapter 就能搜索到它,走的是 SPP 协议(UUID 为 00001101-0000-1000-8000-00805F9B34FB),不需要写 GATT 服务端客户端逻辑,开发难度低,非常适合毕设节奏。

但如果你后续想往产品化方向走,经典蓝牙的功耗和配对体验都跟不上。BLE 方案(如 CC2541、nRF52832)需要自己设计服务、特征值、通知使能等 GATT 层逻辑,STM32 端要么用串口透传固件,要么自己适配 AT 指令协议。毕设从可演示性和源码完整度考虑,HC-05 够用;如果指导老师对功耗有明确要求,可以做成双模方案——先用 HC-05 跑通验证,再替换为 BLE 模块并把串口数据映射到 Notification 特征值上。另外就是"HC05 蓝牙模块连接不上"这类问题几乎是每个做这个课题的人都会碰到,主要原因集中在波特率不匹配、AT 模式没退出、或者模块被配对列表占用,后文排错章节会展开讲。

3. STM32 固件实现:SPI 读取、滤波和蓝牙串口发送

3.1 ADS1292 寄存器配置与 SPI 读取时序

配置 ADS1292 的标准流程是:上电等待复位稳定 → 发送 SDATAC 停止连续读取 → 逐寄存器写入配置 → 发送 RDATAC 开始连续输出。核心寄存器就几个,下面这段代码展示了最基本的初始化逻辑。

// ADS1292 寄存器地址 #define CONFIG1 0x01 #define CONFIG2 0x02 #define LOFF 0x03 #define CH1SET 0x04 #define CH2SET 0x05 #define RLD_SENS 0x06 void ADS1292_Init(void) { // 停止连续读取模式,才能写寄存器 ADS1292_SendCmd(0x11); // SDATAC // CONFIG1: 设置 CLK 为内部 512kHz,采样率 250SPS uint8_t cfg1 = 0x02; // 0100 0010,HR=00 -> 125SPS 基准,DAISY 关闭 ADS1292_WriteReg(CONFIG1, cfg1); // CONFIG2: 开启内部测试信号(用于验证链路),默认关 ADS1292_WriteReg(CONFIG2, 0xA0); // 1000 0000,INT_TEST=0,测试信号接入 // CH1SET: PGA 增益 6,输入短接(测试模式) ADS1292_WriteReg(CH1SET, 0x60); // 0110 0000,GAIN=110 -> 6V/V // CH2SET: PGA 增益 6,正常电极输入 ADS1292_WriteReg(CH2SET, 0x50); // 0101 0000,GAIN=101 -> 6V/V // 启动连续读取 ADS1292_SendCmd(0x02); // WAKEUP ADS1292_SendCmd(0x10); // RDATAC }

寄存器配置里最容易翻车的地方是CONFIG1的采样率位和CHxSET的增益位。上面代码用了 250SPS,这个值对心电来说是够用的(心电有效频率成分到 100Hz 为止,奈奎斯特频率要求至少 200SPS),而且数据量小,蓝牙串口 9600 波特率也能扛住。注意:如果改用 500SPS,三个通道的数据加协议头,9600 波特率就会出现串口丢字节的情况,要么把波特率升到 115200,要么降低采样率。

读取数据时,DRDY 引脚拉低后,需要连续读 3 个字节(通道 1 是 24 位 ADC 值),命令是发送一个 dummy 字节。

uint8_t rx_buf[27]; // 3 字节状态 + 3 通道 × 3?实际是 3 字节 + 2通道×3字节 void ADS1292_ReadData(int32_t *ch1, int32_t *ch2) { while (HAL_GPIO_ReadPin(DRDY_GPIO_Port, DRDY_Pin) != GPIO_PIN_RESET); uint8_t buf[9]; HAL_SPI_Receive(&hspi1, buf, 9, 10); // buf[0..2] 是状态字节,通常忽略 *ch1 = ((int32_t)(buf[3]) << 16) | ((int32_t)(buf[4]) << 8) | buf[5]; *ch2 = ((int32_t)(buf[6]) << 16) | ((int32_t)(buf[7]) << 8) | buf[8]; }

这里有个容易忽略的细节:ADS1292 的输出是 24 位二进制补码,直接把三个字节拼成 int32 后必须做符号扩展,否则负电压的波形会显示成一段水平的跳变。最简单的处理方式:如果最高位(bit23)为 1,就把值或上 0xFF000000。

3.2 数据帧格式设计:让 App 端解析更省心

STM32 把心电数据通过串口发给蓝牙模块,App 那边通过蓝牙读到的是无差别的字节流。为了防止串口噪声或者粘包导致波形错乱,需要自己定义一套简易帧协议。我常用的格式是:

字段长度说明
帧头2 字节0xAA 0x55固定握手
数据长度1 字节0x04有效载荷字节数
CH1 高1 字节AD[23:16]通道 1 高 8 位
CH1 中1 字节AD[15:8]
CH1 低1 字节AD[7:0]
校验和1 字节累加和低 8 位除帧头外所有字节累加

发送函数写成这样:

void SendECGFrame(int32_t ch1, int32_t ch2) { uint8_t frame[9]; frame[0] = 0xAA; frame[1] = 0x55; frame[2] = 0x04; frame[3] = (uint8_t)((ch1 >> 16) & 0xFF); frame[4] = (uint8_t)((ch1 >> 8) & 0xFF); frame[5] = (uint8_t)(ch1 & 0xFF); uint8_t sum = frame[2] + frame[3] + frame[4] + frame[5]; frame[6] = sum; HAL_UART_Transmit(&huart2, frame, 7, 10); }

帧头选0xAA 0x55是因为这两个字节的二进制互补性足够强,误判概率低。App 端解析时,先找帧头,校验和不对就丢弃到下一帧,这样即使偶尔丢一个字节也不会导致整个波形持续错位。加上第 2 个通道是为了后续做呼吸检测或者双导联扩展。

3.3 数字滤波:50Hz 工频陷波和 0.5Hz~40Hz 带通

ADS1292 硬件上可以设置高通滤波高通频率(CONFIG2 里的 HPF 位),但只靠硬件滤波的阻带衰减有限,尤其是 50Hz 工频干扰,电极接触不良或者环境里有日光灯时,基波和二次谐波非常明显。所以固件里要加数字滤波。

我一般用二阶 IIR 陷波器来去除 50Hz 干扰,系数可以用 Python 离线算好,再在 STM32 上以双二阶(biquad)结构运行。采样率 250Hz 下,一组经典型系数配置如下:

// 二阶 IIR 陷波器,fs=250Hz,f0=50Hz,Q=30 typedef struct { float b0, b1, b2; float a1, a2; float x1, x2, y1, y2; } Biquad; float Biquad_Process(Biquad *f, float x) { float y = f->b0 * x + f->b1 * f->x1 + f->b2 * f->x2 - f->a1 * f->y1 - f->a2 * f->y2; f->x2 = f->x1; f->x1 = x; f->y2 = f->y1; f->y1 = y; return y; }

Q 值选 30 是平衡点,Q 值越大陷波带宽越窄,对 50Hz 附近的有效心电成分损伤越小,但抗频偏能力变差;如果市电频率实际是 49.8Hz,Q 值太大反而滤不干净。如果是在实验室环境测量,Q 值可以放宽到 20 更保守。实际滤波效果可以用 STM32 内部定时器触发一次自检,把导联接一个 1Hz 方波,观察输出波形是否保留边沿、50Hz 毛刺是否明显衰减。

4. Android App 开发:BluetoothSocket 连接、数据解析与实时波形绘制

4.1 蓝牙连接:经典蓝牙 SPP 的权限适配

App 端如果采用经典蓝牙(对应 HC-05),Android 的蓝牙 API 相对直接。但不同 Android 版本对蓝牙权限的要求差异很大,这是"App 抓包失败、搜不到设备、连不上"这类问题的重要来源。Android 12 之前只需要BLUETOOTHBLUETOOTH_ADMIN;Android 12 及以上必须在运行时申请BLUETOOTH_SCANBLUETOOTH_CONNECT这两个危险权限。

// Android 12+ 动态权限申请(Kotlin) val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT ) } else { arrayOf( Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.ACCESS_FINE_LOCATION ) } requestPermissions(permissions, 100);

蓝牙扫描和连接是一个异步过程,需要注册BroadcastReceiver监听ACTION_FOUND找到 HC-05,然后通过远程设备地址创建BluetoothSocket

BluetoothDevice device = adapter.getRemoteDevice("00:15:83:00:XX:XX"); BluetoothSocket socket = device.createRfcommSocketToServiceRecord( UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") ); socket.connect(); InputStream input = socket.getInputStream();

createRfcommSocketToServiceRecord这个 UUID 是 SPP 的标准 UUID,HC-05 出厂默认就是这个,不要随意改。如果连接失败抛出IOException,可以尝试用反射方法device.createRfcommSocket(1)兜底,这在部分山寨 HC-05 兼容性差时有效。

4.2 数据解析与波形绘制

拿到InputStream后,需要开一个子线程循环读取字节。为了避免 UI 线程卡顿,读出的一帧数据先放到队列里,再由绘图线程取走刷新界面。波形绘制一般用自定义 View 的onDraw,核心逻辑是保存最近 N 个采样点,每来一个新点,把所有点的 X 坐标左移一个像素宽度然后重绘。

class ECGView extends View { private static final int MAX_POINTS = 500; private float[] ecgData = new float[MAX_POINTS]; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); void addPoint(float value) { System.arraycopy(ecgData, 1, ecgData, 0, MAX_POINTS - 1); ecgData[MAX_POINTS - 1] = value; invalidate(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Path path = new Path(); for (int i = 1; i < MAX_POINTS; i++) { float x = (i - 1) * (getWidth() / (float) MAX_POINTS); float y = getHeight() / 2 - ecgData[i] / 1000000f * getHeight(); if (i == 1) path.moveTo(x, y); else path.lineTo(x, y); } canvas.drawPath(path, paint); } }

这里把 24 位 ADC 原始值除以1000000f再做归一化,是因为 ADS1292 输出范围是 -8388608 到 8388607,除以 100 万之后幅值落在 ±8 之间,乘以 View 高度后在屏幕上能显示完整波形。实际项目中应根据电极接触后的信号幅值动态调整这个缩放系数,否则波形要么顶天立地要么是一条直线。

实时波形要顺滑,有两个细节:一是invalidate()的频率不要超过 30 次/秒,否则图形上下文压力大导致 CPU 飙高;二是串口读线程不要每次读 1 个字节,应该用 8 字节缓冲批量读取,再解析出完整帧。Android 经典蓝牙的InputStream一次read返回的字节数不确定,必须做粘包处理——把收到的字节追加到环形缓冲区,按之前定义的帧格式找出帧头 0xAA 0x55,再截取后 4 个字节解析。

4.3 心率计算:R 波检测的简单实现

除了显示波形,很多毕设还要求计算实时心率。最基础的做法是阈值检测 R 波峰值,然后在时间轴上计算 RR 间期。

private float threshold = 300000f; // 初始阈值,根据实际波形调整 private long lastRRTime = 0; float processSample(float value) { long now = System.currentTimeMillis(); if (value > threshold && now - lastRRTime > 300) { float rrInterval = now - lastRRTime; // 毫秒 int heartRate = (int)(60000f / rrInterval); lastRRTime = now; return heartRate; } return 0; }

300ms 的空白期是为了防止一次 QRS 波群出现多个峰值导致重复计数。阈值的大小决定了检测灵敏度,可以先用一个 1 秒的滑动窗口统计最大峰值的 70% 作为动态阈值,这样电极接触变化导致信号幅值漂移时依然能稳定触发。心率显示的容器可以加一个低通滤波,连续取最近 5 次 RR 间期的平均,避免单次计算跳变太大。

5. 调试技巧、排错清单和低功耗方向优化

5.1 HC-05 连接不上与串口乱码排查

做这个项目的过程中九成的人会卡在蓝牙环节,常见症状是 App 搜不到模块、能搜到但连接失败、连上了收不到数据、或者数据乱码。按优先级排查:

现象可能原因解决方式
搜不到 HC-05模块未进入可发现模式按住模块按键上电,指示灯慢闪(约 2Hz)表示可被发现
能搜到连不上模块已连接其他设备断开其他手机或 AT+RESET;检查模块工作在从模式 AT+ROLE=0
无数据波特率与固件不匹配先确认 AT+UART 返回,默认 9600;基板上电后用 USB-TTL 空测
乱码波特率不匹配两边统一为 9600/8/N/1;确认 STM32 串口与本处一致
连上就断电源供电不足HC-05 峰值电流 30mA+,必须从 STM32 板 LDO 后取电而不是直接挂电池

如果 STM32 串口用 USB-TTL 接电脑能收到正确的AA 55...,再接 HC-05 就乱码,优先怀疑蓝牙模块的波特率被改写成了 115200,用 AT+UART=9600,0,0 恢复。每次测试前把手机蓝牙缓存清一遍,避免旧的配对信息干扰。

5.2 抗人体干扰:右腿驱动和导联线处理

静态测试用信号发生器或者手指触摸电极,波形都能看,但人体实测时常见 50Hz 工频干扰巨大,波形完全被淹没。处理手段有三个:板子上把右腿驱动(RLD)电极接好,这是 ADS1292 消共模干扰的关键;导联线用屏蔽线,屏蔽层单端接地;最后一招是滤波顺序调整,先 50Hz 陷波再做 40Hz 低通。如果顺序反过来,工频的二次谐波还会残留在带通内,波形会显得粗糙。

5.3 进阶:用串口虚拟示波器快速验证固件链路

调试时候不用每次都打开 App,写一个极简上位机脚本(或者用 Python 的 matplotlib 串口读取)能快速确认信号链是否正常。核心思路是复用AA 55帧格式,脚本只负责读串口画波形,不涉及蓝牙。这个工具可以让你把"硬件问题"和"App 问题"快速分开。

5.4 低功耗优化方向

如果做完基础功能后想提升项目完整度,可以考虑三处改动:ADS1292 的心电空闲期发送 STANDBY 指令让芯片进入待机模式;HC-05 换成 BLE 模块,App 采用 GATT Notification 模式接收数据而不是维持一个常开的 SPP 连接;STM32 的时钟从 72MHz 降到 36MHz,采样和滤波的计算量足够应付,省下的功耗能明显延长续航。低功耗模式加进去之后,整体系统架构也不需要推翻重来,只在固件里加一个调度状态机即可。

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

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

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

立即咨询