FEATURED · 精选文章

基于STM32的生理健康监测系统:从ADC采样到OneNet云平台完整实现

发布时间 / 2026/9/12 19:06:33
来源 / 创域科博编辑部
栏目 / 资讯中心
基于STM32的生理健康监测系统:从ADC采样到OneNet云平台完整实现 简介面向嵌入式学习者和开发者基于STM32的生理健康监测系统完整源码工程实现体温、脉搏、心率等生理参数的实时采集与显示并能借助ESP8266将数据上传至OneNet平台。压缩包共138个文件约10.45MB以C语言为主包含44个h头文件、39个c源文件以及工程配置文件、链接脚本、编译输出文件等涵盖传感器驱动、ESP8266通信、OLED显示及系统初始化等模块另有PDF文档和图片素材方便对照工程理解硬件连接与运行效果。已有421人学习浏览。工程可直接导入Keil使用结合OLED屏可直观看到测量结果代码采用模块化组织将采集、通信与显示分离便于理解ADC采样、I2C时序与网络透传等关键知识点。配套OneNet云平台方案适合课程设计、毕业设计与物联网健康监测入门完整工程目录和辅助脚本可帮助快速上手。1. 基于STM32的生理健康监测系统从ADC采样到OneNet上云的完整链路很多人第一次接触STM32项目都会选“体温心率监测”这个方向但真正把数据从传感器搬到云端才明白难点不在“读寄存器”而在“三个模块怎么同步”。这套基于STM32的生理健康监测系统工程传感器采集、ESP8266上传、OLED显示三块并行核心数据链路是体温走DS18B20单总线脉搏和心率走定时器输入捕获IIC总线挂0.96寸OLED最后UART接ESP8266上报OneNet平台。对正在筹备基于stm32的毕业设计的人来说这套工程能看到完整的传感器驱动和协议栈写法对有嵌入式和物联网经验的人值得推敲的是它的时序设计、中断优先级分配和云平台数据帧格式处理。2. 传感器采集与ADC驱动体温、脉搏、心率的数据源头2.1 传感器选型与信号连接采集端涉及三个物理量体温用DS18B20脉搏用红外对管加比较器输出脉冲信号心率由脉搏周期换算得到片内ADC负责备用模拟量采集。之所以没有直接用MAX30100这类现成的血氧心率模块一是成本考量二是这个项目需要同时体现ADC、TIM、IIC、UART四种外设的综合使用这也是“stm32项目”答辩时的高频考点——评委看到的不只是功能而是外设调度能力。引脚分配是系统稳定的第一步我通常按照“数字信号和模拟信号分离、IIC和UART不共用中断优先级”的原则来布外设引脚总线类型说明DS18B20PA0单总线输出开漏需接4.7kΩ上拉脉搏比较器输出PA6TIM3_CH1输入捕获脉冲信号低电平有效OLED SCL / SDAPB8 / PB9IIC1复用开漏输出ESP8266 TXD / RXDPA2 / PA3USART2与ESP8266交叉连接备用模拟量PA5ADC1_IN5可接NTC或环境光传感器这样分配的思路是PA6的输入捕获通道与USART2的DMA请求不会互相抢占中断IIC的速率在标准模式下只要工作在100kHzCPU占用就很低。很多“stm32教程”喜欢把所有外设挤在同一个端口实际联调时会发现中断响应不及时脉搏信号一乱心率计算跟着错。2.2 DS18B20单总线时序与C实现DS18B20的时序要求比较苛刻初始化、写位、读位都有严格的15μs到60μs时间窗。这个源码包里没有把单总线协议拆成独立模块我一般会自己封装核心代码如下uint8_t DS18B20_Reset(void) { uint8_t presence 1; GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET); // 配置为输出模式拉低总线 gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET); // 拉低 Delay_us(480); // 保持480μs GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET); // 释放总线 // 切输入模式读存在脉冲 gpio.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, gpio); Delay_us(60); presence GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); Delay_us(420); return presence; // 返回0表示检测到存在脉冲 }逻辑上先拉低总线480μs释放后切换输入模式DS18B20会主动拉低60~240μs作为应答。这个函数返回0说明传感器在线返回1说明总线悬空或器件未上电。常见问题是忘记切换GPIO模式导致一直读到高电平另外Delay_us必须做成基于SysTick的阻塞延时不能用循环空转否则编译器优化后会改变时序窗口。单总线读取温度需要跳过ROM匹配0xCC再发转换命令0x44然后读暂存器0xBE实际项目里温度转换一般预留750ms。2.3 TIM输入捕获测量脉搏周期脉搏信号经过比较器后输出方波两个上升沿之间的时间就是一次心跳周期。使用TIM3的输入捕获通道1配置上升沿触发捕获值记录当前计数器的数值void TIM3_IC_Init(void) { TIM_ICInitTypeDef icConfig; TIM_TimeBaseInitTypeDef timeBase; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); timeBase.TIM_Period 0xFFFF; // 16位计数器最大值 timeBase.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz每微秒计数1次 timeBase.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, timeBase); icConfig.TIM_Channel TIM_Channel_1; icConfig.TIM_ICPolarity TIM_ICPolarity_Rising; // 上升沿捕获 icConfig.TIM_ICSelection TIM_ICSelection_DirectTI; icConfig.TIM_ICPrescaler TIM_ICPSC_DIV1; icConfig.TIM_ICFilter 0x04; // 数字滤波抑制毛刺 TIM_ICInit(TIM3, icConfig); TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE); NVIC_EnableIRQ(TIM3_IRQn); TIM_Cmd(TIM3, ENABLE); }预分频72-1后计数器频率是1MHz即每微秒加1。用16位计数器记录一个完整周期内的计数差值就得到了以微秒为单位的脉搏周期60除以周期秒数就是心率。中断服务函数里要处理一个边界问题计数器可能发生溢出回卷所以差值计算需要判断当前捕获值是否大于上一次捕获值。void TIM3_IRQHandler(void) { uint16_t currentCapture, period; if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { currentCapture TIM_GetCapture1(TIM3); if (currentCapture lastCapture) { period currentCapture - lastCapture; } else { period 0xFFFF - lastCapture currentCapture; } // period 即两次脉搏间隔的微秒数 lastCapture currentCapture; TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); } }注意TIM_ICFilter这个参数比较器输出在信号质量差时会有连续毛刺0x04对应4个时钟周期的数字滤波能过滤掉大部分抖动。缺点是会引入微小相位延迟对心率这种0.5~3Hz的信号完全无影响但如果是测高速PWM信号滤波系数要相应调小。2.4 ADC多通道采集的坑工程里预留了ADC1通道5作为备用模拟量输入实现时要注意启动方式。如果使用连续转换模式ADC会自动反复扫描通道导致数据覆盖所以正确做法是单次转换、软件触发void ADC_SingleRead_Init(void) { ADC_InitTypeDef adcConfig; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 12MHz ADC时钟 adcConfig.ADC_Mode ADC_Mode_Independent; adcConfig.ADC_ScanConvMode DISABLE; // 单通道模式 adcConfig.ADC_ContinuousConvMode DISABLE; // 单次转换 adcConfig.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 adcConfig.ADC_DataAlign ADC_DataAlign_Right; adcConfig.ADC_NbrOfChannel 1; ADC_Init(ADC1, adcConfig); ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); }采样时间选择239.5个周期目的是在前级信号源输出阻抗高的情况下保证采样电容充分充电。如果用28.5个周期转换结果会有末位跳变。获取值时要等EOC标志位置起再读不能用Delay硬等否则多通道切换时会出现上一次的残留数据。3. ESP8266通讯模块AT指令集与OneNet数据上报3.1 ESP8266在系统中的角色与固件准备ESP8266模块负责把采集到的体温、脉搏、心率数据送到OneNet平台。在STM32F103上跑ESP8266最简单可靠的方式是用AT固件而不是直接在ESP8266上跑SDK。原因有两点一是AT固件经过大量验证TCP连接和WiFi重连逻辑都已实现不占用MCU内存二是源码包里的协议栈可以专注处理业务数据不用管LWIP底层。这个项目的源码只写了UART收发逻辑完整AT指令流程需要自己串起来。务必先确认模块固件版本。发送ATGMR返回AT version:1.2.0之类的结果如果返回错误格式后续AT指令全部无效。市面上很多ESP8266模块默认是AT固件但也有刷了NodeMCU固件的两者AT指令完全不通用。3.2 AT指令交互与数据透传UART2以115200波特率连接ESP8266配置为8N1。AT指令以\r\n结尾每条指令后用等待应答的方式处理不要连续发送。关键交互序列如下AT // 返回OK确认模块存活 ATCWMODE1 // 设为Station模式 ATCWJAPWiFiSSID,passwd // 连接路由器返回WIFI GOT IP ATMQTTUSERCFG0,1,设备ID,产品ID,APIKey,0,0, // MQTT认证 ATMQTTCONN0,mqtts.heclouds.com,1883,1 // 连接OneNet ATMQTTSUB0,topic/cmd,1 // 订阅下行指令第一行的AT不能省它同时完成了波特率自检。实际开发中ESP8266上电后需要等待1~2秒让模块完成内部初始化然后在循环里发AT直到收到OK最多重试5次。这样即使模块处于异常状态也能恢复。数据上报使用MQTT发布ATMQTTPUB0,topic/dp,1,0,{\temp\:36.5,\pulse\:72,\heart\:72}其中主题名和OneNet平台的数据流名称要严格对应OneNet的MQTT设备接入协议要求payload必须是JSON格式。3.3 上报失败时的重连策略项目跑起来最头疼的是ESP8266掉线后自动重连逻辑。我建议做一个有限状态机正常状态循环发数据收到ERROR或CLOSED就切到重连状态先发ATCWJAP重新连WiFi再发ATMQTTCONN重新连接MQTT。这里有个细节MQTT重连不能太频繁否则会被OneNet服务器临时封IP加上10秒退避比较合理。下面是一段上报函数的核心实现uint8_t ESP8266_Report(float temp, uint8_t pulse, uint8_t heart_rate) { char payload[128]; char cmd[160]; snprintf(payload, sizeof(payload), {\temp\:%.1f,\pulse\:%d,\heart\:%d}, temp, pulse, heart_rate); snprintf(cmd, sizeof(cmd), ATMQTTPUB0,\topic/dp\,1,0,\%s\\r\n, payload); ESP8266_SendString(cmd); // 串口发送完整AT指令 return ESP8266_WaitResponse(OK, 2000); // 2秒内等待OK }参数里ATMQTTPUB的第三个1表示QoS级别为1即至少送达一次。本地测试时可以把QoS设成0减轻服务器压力上线后建议保持1避免数据丢失。ESP8266_WaitResponse内部用DMA接收串口数据并设置一个2秒超时这个超时值不能太长否则主循环里数据采集会被卡住。ESP8266收发有一个稳定性和传输速率的权衡串口波特率提升到460800之后AT指令响应变快了但飞线稍微长一点就容易丢字节。实际项目中115200已经足够因为每秒最多上报5帧数据每帧不超过150字节。4. OLED显示与本地人机交互IIC时序与UI刷新策略4.1 SSD1306驱动与显存管理0.96寸OLED用的是SSD1306控制器这块芯片内部有1KB显存也就是128x64像素每个像素占1bit。MCU通过IIC接口向显存写入数据SSD1306内部会持续扫描显存刷新屏幕MCU不需要参与逐像素刷新。这种方式下MCU只需要把需要修改的局部区域数据写入剩余的刷新工作全部由SSD1306完成。IIC初始化时要注意STM32的IIC外设用起来比较繁琐很多开发者转向软件模拟IIC。这个源码包里提供的是IIC模块我建议使用硬件IIC配合DMA关键初始化如下void I2C_Init_OLED(void) { I2C_InitTypeDef i2cConfig; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; gpio.GPIO_Mode GPIO_Mode_AF_OD; // 复用开漏需外接上拉 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); i2cConfig.I2C_Mode I2C_Mode_I2C; i2cConfig.I2C_ClockSpeed 400000; // 400kHz快速模式 i2cConfig.I2C_DutyCycle I2C_DutyCycle_16_9; i2cConfig.I2C_Ack I2C_Ack_Enable; i2cConfig.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, i2cConfig); I2C_Cmd(I2C1, ENABLE); }GPIO配置为复用开漏后必须外接4.7kΩ上拉电阻到3.3V否则SCL和SDA无法产生可靠的高电平。如果把I2C_ClockSpeed从400000改成100000显示刷新率会明显下降实测画一条完整波形曲线时间从15ms变成60ms影响比较大。4.2 中文显示与字库处理OLED要显示“体温”、“脉搏”这些中文字符SSD1306默认只有ASCII字库所以必须外挂中文字库数组。每个16x16中文汉字占32字节显示一个汉字需要通过取模软件生成点阵数据。通常的做法是做一个字符索引表typedef struct { uint8_t index[3]; // UTF-8编码比如体是0xE4 0xBD 0x93 uint8_t width; // 字宽16或12 const uint8_t *data; // 点阵数据指针 } FontIndex; static const FontIndex cn_font_table[] { {体, 16, font_ti}, {温, 16, font_wen}, {脉, 16, font_mai}, {搏, 16, font_bo}, {心, 16, font_xin}, {率, 16, font_lv}, };显示函数做UTF-8匹配字符串指针每次读一个汉字然后在表中查找点阵。注意UTF-8中文字符是3字节编码不能用strlen按字节遍历。工程内存有限放200个常用汉字就占6.4KB Flash所以只在Flash里存核心字库生僻字通过图片形式预先烧录。4.3 局部刷新避免整屏闪烁新手最容易犯的错误是每秒钟把整个1KB显存全量刷新导致屏幕不断闪烁。合理的做法是把显示区域拆成三块第一行显示当前模式第二行显示温度数值第三行显示脉搏波形。每次更新时只向对应区域的显存地址发送数据核心代码如下void OLED_Update_Numeric(uint8_t row, char *text) { uint8_t page row * 2; // 每个字符占2页每页8像素高 uint8_t col 0; OLED_Set_Pos(page, col); // 设置显存起始位置 while (*text) { OLED_ShowChar(*text); // 逐个显示ASCII字符 } }OLED_Set_Pos里包含了IIC的buffer写命令序列。局部刷新的收益不只是闪烁问题还有MCU负载全屏刷新一次需要发送1024字节数据在400kHz IIC下耗时约21ms如果每秒刷新10次就是210ms占CPU时间20%以上。局部刷新只发送变化的字节CPU负载能控制在5%以内对ESP8266的数据上报延时影响明显减小。波形绘制也有技巧。脉搏波形数据是连续的周期性信号绘制时只需把新采样点追加到曲线末尾然后整体左移1个像素这样IIC开销固定为1行显存数据而不是整个波形区域。5. 系统联调与串口日志从printf重定向到OneNet数据校验联调阶段最有效的工具是串口日志。把电平转换板连接PA9USART1到PC波特率115200然后重定向printfint fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }在Keil工程里勾选“Use MicroLIB”printf就能直接输出日志。这里有一个容易卡住的细节如果使用了JLINK调试串口输出和调试器共用PA9会导致日志混乱改用PA2、PA3USART2做调试口是更稳妥的选择这也是为什么这套工程把ESP8266也放在USART2上需要提前规划。日志格式建议统一前缀方便PC端脚本过滤例如[INFO] DS18B20 OK, temp36.5C [INFO] PULSE period833ms, heart72bpm [ERROR] ATMQTTCONN timeout [DEBUG] payload{temp:36.5,pulse:72,heart:72}这样通过PuTTY或串口助手就能实时确认哪个环节出问题。集成测试时先不接ESP8266用串口助手模拟AT应答把WiFi和MQTT交互拆出来独立验证确认通过之后再把ESP8266接入系统避免一个问题牵连四个模块。OneNet平台的数据校验也有技巧正常上报后在设备详情页能看到数据流更新但要注意OneNet的数据流名称和代码里的topic必须完全一致大小写敏感。另外OneNet对同一设备的MQTT连接频率有限制如果代码里出现重启后立刻重连平台可能返回-1表示禁止接入这时等待30秒再重连即可。源码包里还有keilkilll.bat这个清理脚本可以一键删除中间文件保证在更换电脑或跨工程拷贝时不会因为旧的axf或uvguix缓存文件导致编译异常。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻