ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于STM32与Keil5的心电图监测蓝牙App开发实战

基于STM32与Keil5的心电图监测蓝牙App开发实战 简介毕业设计级STM32心电监测系统资源包面向嵌入式方向的学生或开发者演示如何从传感器采集心电信号到手机App展示的完整链路。硬件端基于STM32平台利用ADC与DMA实时捕获心电数据经HC-05蓝牙无线透传软件端为Android Studio开发的App支持波形显示、历史记录与保存图片。压缩包共99个文件以41个H头文件与40个C源文件为主覆盖系统初始化、底层驱动、应用逻辑同时提供Keil5工程文件、启动文件、标准库、USMART调试组件、原理图PDF及README说明目录按CORE、SYSTEM、HARDWARE、USER等组织便于快速定位。资源仅408KB已有291人学习其完整工程框架与电路原理图可帮助读者快速搭建心电监测原型也可作为蓝牙透传、ADC采样等模块的参考范例。对于正在准备毕业设计或嵌入式竞赛的读者这份资料能节省大量查阅时间。1. 基于STM32Keil5做心电图监测蓝牙App技术选型与整体方案很多人把“心电图监测”想得很复杂但拿到一颗STM32F103和一个模拟前端硬件成本不到50元。难点其实在三点Keil5工程能不能一次跑通、50Hz工频干扰能不能压下去、蓝牙链路的数据能不能完整送到App。这块毕业设计的关键不是在App端堆动画而是把STM32端的采集模块、滤波算法和串口协议做成一个稳定数据源。本文按常见方案拆解AD8232模拟前端采集心电信号STM32内置ADC以250Hz采样率转换经过数字滤波后通过UART接HC-05蓝牙透传模块手机或PC串口App实时显示波形。配套源码和原理文档PDF可以直接对应下文实现。适合刚接触STM32的毕设党也适合想快速搭建心率采集原型的工程师。2. 从Keil5安装到STM32工程芯片包配置、编译报错与连接异常排查对于毕业设计Keil5一般指MDK5Microcontroller Development Kit。这一章先把开发环境从零到能烧录跑通再处理最常见的“no STM32 target found”和编译慢问题代码和工具链都可直接照搬。2.1 keil5安装教程兼容C51与STM32的芯片包安装顺序安装时最容易出现的坑是先装了C51版又装MDK导致Pack Installer里看不到STM32设备包。常见做法是分别安装但要选对路径C51版本和MDK版本不能共用同一个安装目录否则启动后芯片列表混乱。我在毕设里更习惯直接下载stm32芯片包离线安装包解压后双击导入这样能避开在线更新慢或失败的问题。需要注意选择与MDK5大版本匹配的DFP版本太老的DFP可能不支持新ARM编译器。如果电脑配置不差但keil5编译很慢先别怀疑机器常见原因有两个一是整个工程放在了网盘同步或杀毒软件监控目录下文件扫描拖慢编译二是编译器版本选择不对。我一般把工程放到磁盘根目录的workspace文件夹并在Options for Target的Output和Listing页里把生成中间文件的路径清空为默认编译速度会明显提升。下面这段测试代码用于验证环境它把PA1配置为推挽输出并快速翻转。连最基本的GPIO都跑不起来时先不要往下做ADC和蓝牙。#include stm32f10x.h int main(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); while (1) { GPIO_SetBits(GPIOA, GPIO_Pin_1); GPIO_ResetBits(GPIOA, GPIO_Pin_1); } }逻辑说明这段代码先把PA1对应的GPIO时钟打开再配置为50MHz推挽通用输出最后在while循环里不断拉高拉低产生方波。参数里GPIO_Pin_1指定通道GPIO_Mode_Out_PP适合驱动LED或示波器观测GPIO_Speed_50MHz用于翻转频率较高的场景如果PA1外接HC-05复位脚之类的慢速信号降到10MHz可以减少EMI。2.2 新建STM32工程选芯片、配置Flash下载算法MDK5新建工程时第一件事是在Device框里选择具体型号而不是靠自动检测。对于毕设我通常选STM32F103C8T6Flash为64KRAM为20K。然后要把启动文件、系统时钟初始化文件和标准外设库源文件加进工程。一个最小工程的文件分组可以参考下表。分组文件作用CMSISstartup_stm32f10x_md.s、system_stm32f10x.c中断向量表、时钟初始化StdPeriphstm32f10x_gpio.c、stm32f10x_adc.c、stm32f10x_tim.c外设库函数Usermain.c、ecg.c、ble.c应用逻辑在Options for Target里Debug页选择ST-Link Debugger然后进入Settings确认SW Device能识别到Cortex-M3内核。Utilities页的Flash Download里要添加对应Flash算法F103C8的起始地址是0x08000000容量128K如果没有这一步烧录时会出现“Error: Flash Download failed”。很多人把算法加错成F103ZE的512K也会导致校验失败。2.3 编译报错与no STM32 target found的排查步骤编译报错比硬件问题好解决因为MDK5会指出文件和行号。真正卡死人是在烧录阶段出现连接错误。下面表格列出三个高频异常按优先级排查。错误信息可能原因处理方法No STM32 target found!ST-Link驱动未装、SWDIO/SWCLK接反重装驱动交换SWDIO和SWCLK试试Flash Download failed - Cortex-M3Flash算法缺失或芯片型号不符在Flash Download里重新添加STM32F10x Med-densityCannot access targetSWD引脚被复用、芯片读保护开启按住复位键下载或进ST-Link Utility解除保护关于“error: no stm32 target found!”这个报错很多情况下并不是ST-Link坏了而是目标板供电只有USB的5V没有经过稳压到3.3V给芯片。另一个隐藏原因是之前烧录的程序把SWDIO引脚配置成了输出调试口失效。处理办法按住目标板复位键在MDK5点击下载的瞬间松开复位让芯片停留在复位状态完成握手然后立即烧录正确程序。2.4 用STM32 ST-Link Utility验证连接与恢复锁死芯片ST-Link Utility在毕设里是救命工具。打开软件后点击Target-Connect如果能读到芯片ID说明ST-Link驱动、接线和芯片供电都正常。如果读到“Cannot connect”先把SWD模式下速度从1.8MHz降到500kHz很多抗干扰差的杜邦线就是被高速模式下丢数据搞死的。芯片被锁死时执行Option Bytes操作将Read Protection改为Disabled并Apply然后执行Full Chip Erase整体擦除Flash。擦完后再回到MDK5重新烧录程序基本都能救回来。这套操作对于后续HC-05蓝牙模块调试也一样适用确认串口回环正常之前不要怀疑App。3. 心电信号采集与处理传感器选型、ADC采样、滤波算法与参数设置这一章解决“数据从哪来”的问题。核心是把毫伏级心电信号调理到ADC可测量的范围再配置好采样时序最后用代码把50Hz工频压下去。3.1 传感器选型AD8232单导联心率前端的接线常见做法是选AD8232微功耗心电前端。它内部集成仪表放大器和右腿驱动外围只需少量电阻电容就能输出经过放大的模拟信号。AD8232的输出是相对地的电压可直接接到STM32的ADC输入不需要再额外加电平抬升电路。引脚接线见下表。AD8232引脚STM32引脚说明OUTPA0ADC1通道0模拟输入3.3V3.3V给模拟前端供电GNDGND共地SDNPB0或3.3V高电平使能输出拉低关断SDN引脚容易被忽略。如果悬空AD8232的输出可能是高阻态ADC读到的值会乱跳。我一般直接用单个杜邦线把SDN接到3.3V省掉一个GPIO。电极采用三导联RA、LA、RL分别接右臂、左臂、右腿RL驱动能有效抑制共模干扰。3.2 STM32的ADC采样参数配置采样率250Hz与DMA搬运心电信号的频率范围是0.05Hz到100Hz根据奈奎斯特定理采样率不能低于200Hz。我把系统采样率定为250Hz每个采样点间隔4ms。这个速率对波形重建足够也保证单个蓝牙包的吞吐量不会太高。如果需要更清晰的P波和T波可以改成500Hz但滤波系数要同步重算。配置要点如下使用TIM2的更新事件作为ADC外部触发源TIM2预分频71自动重载3999这样定时器频率刚好是72MHz/(72×4000)250Hz。ADC时钟取PCLK2的6分频也就是12MHz。采样时间选239.5个周期内部采样电容充电更充分电压采集更稳。ADC通道配置为PA0对应ADC1的通道0。void ECG_ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟 72MHz / 6 12MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 3999; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }逻辑说明ADC1不采用连续转换而是等待TIM2的更新事件到来后才触发一次转换因此ADC_ContinuousConvMode设为DISABLE。ADC_ExternalTrigConv_T2_TRGO指定了触发源只要TIM2启动并且定时器计数溢出ADC就会自动启动转换。采样时间设为239.5周期是为了让内部保持电容稳定并不影响4ms的采样间隔。ADC转换结果还需要DMA自动搬运否则每250个采样点进入中断会消耗大量CPU时间。下面这段DMA初始化配置了循环模式缓冲区大小250适配250Hz采样率对应一秒的数据量。void ECG_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(ADC1-DR); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)ecg_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 250; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); ADC_DMACmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }注意ADC1和DMA1通道1是固定映射关系不需要手动配置重映射。DMA_BufferSize填写250不是字节数而是半字数。如果填错会出现缓冲区提前回绕或写越过界的现象。3.3 数字滤波实现50Hz工频陷波与0.5-40Hz带通工频干扰是心电信号最大的敌人。虽然AD8232硬件上已经做了高共模抑制但导线移动或电极接触不良时50Hz成分仍然会提前饱和ADC。我比较推荐在STM32里做一个双二阶IIR陷波器而不是只用简单的移动平均因为移动平均对50Hz的衰减深度不够。以下参数按采样率250Hz计算中心频率50HzQ值为10。双二阶陷波器的直接I型结构差分方程为y[n] b0·x[n] b1·x[n-1] b2·x[n-2] - a1·y[n-1] - a2·y[n-2]系数由RBJ音频EQ公式计算归一化后的值如下。代码实现为一个普通函数每个采样点调用一次。float ecg_x[3], ecg_y[3]; float ecg_notch(float input) { ecg_x[2] ecg_x[1]; ecg_x[1] ecg_x[0]; ecg_x[0] input; ecg_y[2] ecg_y[1]; ecg_y[1] ecg_y[0]; ecg_y[0] 0.9546f * ecg_x[0] - 0.5899f * ecg_x[1] 0.9546f * ecg_x[2] - (-0.5899f) * ecg_y[1] - 0.9091f * ecg_y[2]; return ecg_y[0]; }逻辑说明数组ecg_x保存输入历史ecg_y保存输出历史。每进入新采样点时把历史数据依次向后移然后按差分方程计算当前输出。系数中b0和b2相同a1与b1符号相反这是陷波滤波器固有的对称特性。如果发现50Hz抑制不够将Q值调大如果发现波形本身被扭曲把Q值调小通常Q8到12之间效果较好。滤波参数可以按下面的表格做参考不同采样率要重新计算陷波系数。参数数值说明采样率250HzADC定时器触发频率工频陷波50HzQ10抑制电源干扰高通0.5Hz一阶消除呼吸引起的基线漂移低通40Hz一阶抑制肌电噪声和射频干扰4. 蓝牙协议与传输设计HC-05配置、串口透传、数据帧打包与App接收逻辑采集端有了连续的心电数据下一步就是通过蓝牙交给App。这一章讲透HC-05的配置命令、串口透传时的数据帧设计和App侧接收的实现逻辑重点避免“蓝牙显示已连接但App收到乱码”的问题。4.1 蓝牙模块选型HC-05经典蓝牙还是BLE模块常见做法是选HC-05。经典蓝牙SPP协议在Android Studio里有原生BluetoothSocket支持单片机端只需要把UART接到蓝牙模块不需要处理复杂的GATT服务。BLE模块虽然省电但App端要写服务发现、特征值读写、通知回调工作量翻倍。毕设主要是验证心电传输用HC-05更稳。HC-05的本质是串口转无线STM32通过USART2发送数据蓝牙模块把串口数据以无线包发给手机。手机App用BluetoothSocket读流即可拿到原始字节流。唯一要注意的是HC-05默认波特率通常是9600而STM32这边如果配置成115200必须先改AT参数。4.2 HC-05的AT指令配置波特率与主从模式HC-05进入AT模式的方法比较特殊上电前按住模块上的按键再上电此时指示灯慢闪表示进入AT模式。AT模式下的串口波特率固定为38400和正常工作波特率无关。配置列表如下。AT指令含义典型返回值ATNAMEECG设置广播名OKATROLE0设置为从机OKATUART115200,0,0设置波特率115200停止位1不加校验OKATPSWD1234设置配对密码OKATRESET重启模块使配置生效OK发送ATUART115200,0,0时第三个参数0表示不使用校验位。如果误配成奇偶校验STM32端会收到错误数据。配置完成后务必发送ATRESET模块重启后才按新参数工作。调试时先用USB转TTL模块直接连接HC-05的TX和RX在PC端串口助手里验证AT指令返回再接到STM32上避免两边问题叠加。4.3 数据帧打包帧头、16位ADC值和校验和心电数据是连续字节流直接把ADC原始值发出去App端很难分辨一个字节是一个数据还是一个噪声。我采用固定帧格式每帧6个字节结构见下表。字节偏移内容说明00xAA帧头10x55帧头确认20x02数据长度表示后面数据占两个字节3ADC高8位心电数据高字节在前4ADC低8位心电数据5校验和前五个字节累加后取低八位发送函数如下。关键点在每次发送前等待USART_FLAG_TC置位避免前一个数据还没发送完就覆盖寄存器。uint8_t frame[6]; void ECG_SendFrame(uint16_t adc_value) { uint8_t i; uint8_t checksum 0; frame[0] 0xAA; frame[1] 0x55; frame[2] 0x02; frame[3] (uint8_t)(adc_value 8); frame[4] (uint8_t)(adc_value 0xFF); checksum frame[0] frame[1] frame[2] frame[3] frame[4]; frame[5] checksum 0xFF; for (i 0; i 6; i) { USART_SendData(USART2, frame[i]); while (USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET); } }逻辑说明发送端并不关心App有没有收到只负责把串口字节按顺序发出。校验和为帧头到数据部分的累加和低八位可以在接收端快速判断是否有丢字节CRC错误。这里用TC标志而不是TXE标志是因为TXE只表示数据已进入发送移位寄存器而TC表示整个字节已经发送完成连续发送时不容易出现字节间粘连。4.4 App接收逻辑与蓝牙连接失败排查Android端接收数据通常放在子线程的InputStream.read循环里。因为蓝牙串口是流式协议App端要自己维护一个帧解析器把收到的字节凑成完整帧再提取心电值。核心逻辑如下。private void readLoop() { byte[] buffer new byte[512]; int len; while ((len inputStream.read(buffer)) 0) { frameParser.push(buffer, len); while (frameParser.hasFrame()) { int[] data frameParser.nextFrame(); int sum (data[0] data[1] data[2] data[3] data[4]) 0xFF; if (sum data[5]) { int adc (data[3] 8) | data[4]; waveformView.addPoint(adc); } } } }逻辑说明inputStream.read阻塞等待蓝牙数据收到后先推入帧解析器。hasFrame负责扫描帧头0xAA 0x55并检查当前缓冲区是否够一帧长度。nextFrame取出完整一帧然后做校验和过滤。如果校验失败说明蓝牙传输中出现丢包或错位直接丢弃这一帧避免在波形图上出现毛刺。关于HC05蓝牙模块连接不上常见排查顺序是第一看模块指示灯是否慢闪快闪表示处于AT模式第二用USB转TTL直接连模块排除STM32干扰第三查配对密码是否一致HC-05默认密码1234如果改过手机端要用新密码。连接成功但收不到数据时优先查STM32的UART波特率和HC-05内部配置是否一致而不是怀疑App。5. App端显示优化与现场调试技巧波形渲染、心率计算、滤波调参验证方法5.1 用数据回放验证滤波参数是否合适现场设备不好带最直接的方法是先用录制好的心电数据在STM32里循环播放。做法是把一段正常ADC采样点定义成const数组放在main.c里通过蓝牙发送给App。我一般会在代码里加一个“旁路开关”用STM32上的一个按键在“原始ADC值”和“滤波后ADC值”之间切换。这样App端能同时看到滤波前后的差异答辩演示时非常有说服力。如果波形基线漂移说明0.5Hz高通截止频率还是偏高把时间常数拉长到0.3Hz如果波形上有大量50Hz毛刺说明陷波Q值太小或AD8232的RL驱动没接好。5.2 三个可复现的现场验证步骤验证一给AD8232的输入端接一个1Hz正弦波信号源检查App显示的波形周期是否为1秒如果周期偏差超过20%说明采样率配置有问题优先检查TIM2的预分频和自动重载值。验证二三个电极贴在左、右前臂和右脚踝静坐10秒后观察波形正常应看到清晰的P-QRS-T形态。手指捏住导联线中间时波形应该出现明显50Hz杂波松手后杂波消失这能定位屏蔽层和接地是否正常。验证三把HC-05拔掉用杜邦线短接STM32的USART2_TX和RX观察数据回环是否正常这能排除蓝牙模块本身的问题。5.3 心率计算时避免重复计数App端显示心率时简单阈值法容易把T波算成一次心跳。我的做法是设一个“不应期”检测到R波后的200ms内不再接受新的峰值。同时阈值不是固定值而是取最近1秒波形最大值的60%这样当信号幅值因电极接触变化时仍能稳定检测。上述逻辑在波形渲染线程的相邻采样点间判断即可不需要引入实时操作系统。蓝牙丢包时优先查看STM32的ORE标志这个标志代表串口寄存器溢出一旦出现说明发送频率超过串口处理能力此时降低采样率到200Hz或者把HC-05波特率调到满档115200。本文还有配套的精品资源点击获取
返回列表