ARTICLE DETAIL

资讯详情

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

STM32F103老人护理监测仪:多传感器数据采集与嵌入式开发实战

STM32F103老人护理监测仪:多传感器数据采集与嵌入式开发实战 简介基于STM32F103的多传感器护理监测项目面向嵌入式开发者和物联网爱好者集成DHT11温湿度、微波生命雷达、红外体温、一氧化碳、液晶屏及WiFi等模块可实时监测老人或病人的环境与生理状态适用于智慧养老、病房监护等场景。压缩包共184个文件、约3.8MB以15个C源文件和15个头文件为核心完整实现各传感器驱动与主控逻辑uvprojx/uvoptx为Keil工程配置文件hex/axf为编译输出程序lst/map辅助编译调试另有bat脚本等工具文件整体结构清晰便于按模块对照学习。当前已有46人学习下载。代码基于KEIL标准库编写注释详细各模块接线均在代码中通过宏定义给出配合J-Link或ST-Link即可下载调试若更换STM32F103其它型号只需调整芯片型号与Flash容量。对于需要快速完成多传感器融合项目或开发健康监测原型的开发者可显著节省驱动编写与联调时间。1. 老人护理监测仪五路传感器在 STM32F103 上的一次完整拼装深夜值班室屏幕上弹出这样一条记录室内一氧化碳浓度 982ppm微波雷达判定目标处于连续微动状态红外体温读到 36.8℃。这说明老人已经醒来、燃气泄漏正在逼近危险阈值而传感器给出的这一组数据足以让护理人员提前介入。这台基于 STM32F103 的护理监测仪把 DHT11 温湿度、微波生命雷达、MLX90614 红外体温、MQ-7 一氧化碳、TFT 液晶屏和 WiFi 模块拼成了一条完整的信号采集链路代码全部跑在 KEIL 标准库上不依赖 RTOS也不需要 HAL。对正在做医疗电子原型、养老设备 DEMO 或毕设进阶的开发者来说这套工程的价值在于把最容易出错的传感器时序和通信协议先替你走通了一遍剩下的事情就是对照接线改参数。2. DHT11 与 MQ-7从单总线时序到 ADC 浓度换算2.1 DHT11 为什么坚持用 GPIO 模拟单总线而不走 IICDHT11 的通信协议是单总线一根数据线既做输入又做输出。市面上有带 IIC 接口的温湿度传感器但 DHT11 的出货量决定了它便宜、替换容易而且它要求的时序窗口相对宽松只要主机拉低时间、释放后的采样点落在合理区间读数就稳定。真正的坑在于很多人用系统滴答做延时中断一多时序就被打断所以我在这类项目里一般直接用 GPIO 模拟时序代码里也是这么处理的。uint8_t DHT11_Read_Data(uint8_t *hum, uint8_t *hum_dec, uint8_t *temp, uint8_t *temp_dec) { uint8_t buf[5] {0}; uint8_t i; DHT11_Output_Mode(); // 切换为推挽输出 DHT11_DQ_0(); delay_us(20); // 主机拉低至少 18ms这里取 20ms 余量 DHT11_DQ_1(); delay_us(30); // 拉高 20~40us 后释放总线 DHT11_Input_Mode(); // 转为上拉输入等待从机响应 if (!DHT11_DQ_READ()) // 从机响应先拉低 80us 再拉高 80us { while (!DHT11_DQ_READ()); // 等待响应低电平结束 while (DHT11_DQ_READ()); // 等待响应高电平结束 for (i 0; i 40; i) // 40 位数据湿度整数/小数、温度整数/小数、校验 { while (!DHT11_DQ_READ()); // 每个 bit 前的低电平 delay_us(40); // 在 40us 处采样判断 0 或 1 buf[i / 8] 1; if (DHT11_DQ_READ()) buf[i / 8] | 0x01; while (DHT11_DQ_READ()); // 等待该 bit 高电平结束 } if (buf[0] buf[1] buf[2] buf[3] buf[4]) { *hum buf[0]; *hum_dec buf[1]; *temp buf[2]; *temp_dec buf[3]; return 0; } } return 1; // 超时或校验失败 }这段代码的关键点在两个while循环和delay_us(40)的采样点。DHT11 数据位用高电平宽度区分 0 和 135us 以下算 070us 左右算 1所以在 40us 处读一次电平就能把两类位区分开。注意delay_us(30)必须在切换到输入模式前完成否则上拉还没稳定读到的第一个响应电平会抖动。这里的buf[0] buf[1] buf[2] buf[3] buf[4]是低八位校验加出来的值超过 255 时只取低 8 位参与比较所以直接用单字节变量相加即可不用人为做 0xFF。提示DHT11 的读取周期建议不小于 1 秒连续读会把从机拉入忙状态返回的数据全是 0xFF。2.2 MQ-7 的 ADC 采样与预热校准MQ-7 是一氧化碳传感器输出是模拟电压接 STM32F103 的 ADC 引脚即可。它内部有一个加热电阻上电初期需要预热冷启动头几分钟读数会漂这是正常现象项目里一般会做 60 秒的启动抑制前 60 秒只显示---不参与报警判断。uint16_t MQ7_Read_ADC(void) { uint16_t sum 0; uint8_t i; for (i 0; i 8; i) // 连续采样 8 次 { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成 sum ADC_GetConversionValue(ADC1); } return sum / 8; // 滑动平均MQ-7 输出本身有波动 }换算浓度时不要直接拿 ADC 值当 ppm常见做法是采集洁净空气中的基准电压V0再用Rs/R0曲线反查。STM32F103 的 ADC 是 12 位参考电压 3.3V所以Vout adc * 3.3 / 4096。我一般会在代码里留两个宏MQ7_BASE_VOLTAGE和MQ7_ALARM_PPM校准环境不同改宏比改逻辑快得多。如果手头没有标准气源至少做两级阈值300ppm 预警、1000ppm 报警避免单阈值在预热期误触发。MQ-7 的加热电压建议 5V 供电而 STM32F103 的 ADC 引脚耐压是 3.3V。很多新手直接把 MQ-7 的 AOUT 接到 PA1结果分压不对读数一直满量程。正确接法是加一个 10k 对地分压电阻或者用运放做电平搬移。代码里的接线定义其实已经把这个分压电阻当成默认配置了硬件不一致时先量 AOUT 引脚电压确认在 0~3.3V 范围内再接。2.3 项目接线约束与代码对照这套工程的接线定义全部写在代码头部按模块分块注释。下表是这套案例中较常规的一组对应关系实际以代码中#define的引脚为准模块信号线STM32F103 引脚说明DHT11DATPA0单总线数据需 4.7k 上拉MQ-7AOUTPA1ADC1_IN1带分压电阻微波雷达RXPA2串口 2 接收微波雷达TXPA3串口 2 发送MLX90614SCLPB6IIC 时钟MLX90614SDAPB7IIC 数据TFT 液晶屏FSMCPD0~PD1516 位并口数据线WiFi 模块TX/RXPB10/PB11串口 3从上表能看出一个设计原则把低速、时序敏感的传感器放在 GPIO 或 IIC 引脚上把需要连续数据流的雷达和 WiFi 放到独立串口上。这样既不会让 DHT11 的延时被雷达中断干扰也不会让 WiFi 的 AT 响应和传感器数据在同一个串口里互相污染。DHT11 的上拉电阻是必须的GPIO 内部上拉阻值在 30~50k对单总线来说偏弱数据线长一点就会出现偶发读错。4.7k 外置上拉可以保证总线释放时电平爬升够快这是排查“数据每隔几个周期跳一次”时最先要检查的地方。3. 生命体征的感知雷达存在检测与红外体温补偿3.1 微波雷达选型为什么不做简单人体感应而是存在检测传统 PIR 人体感应只能输出“有没有动”老人静卧不动就判断为无人这是护理场景不能接受的。微波生命雷达工作在 24GHz 频段能区分“目标存在”“目标运动”“目标静止”三种状态静止的呼吸起伏也会被微多普勒效应捕获。实际项目中常用带串口输出的 24GHz 雷达模块接线少数据帧里直接给出目标状态、距离和能量值STM32F103 只需要在串口中断里收帧。这类雷达模块的典型帧格式如下不同固件字节偏移可能略有差异但帧头、校验机制基本一致字段字节数含义帧头20xF4 0xF3数据字段1固定数据标识目标状态10 无人 1 有人 2 有人且运动移动距离1单位 cm移动能量10~100静止距离1单位 cm静止能量10~100校验1前面所有字节求和取低 8 位3.2 雷达帧解析与目标状态机接收端用串口 2 的中断把字节攒进环形缓冲区然后逐帧校验。这里的关键是不要让校验逻辑在中断里执行中断只做存字节主循环里再解析否则一帧数据里任何一个字节的间隔抖动都会导致丢帧。// frame: 完整一帧, len: 帧长, 通过指针回传目标状态和两个距离值 uint8_t Radar_Parse_Frame(uint8_t *frame, uint8_t len, uint8_t *state, uint8_t *move_d, uint8_t *static_d) { uint8_t sum 0, i; if (len 12 || frame[0] ! 0xF4 || frame[1] ! 0xF3) return 0; for (i 2; i len - 1; i) // 从数据字段到静止能量全部参与校验 sum frame[i]; if ((sum 0xFF) ! frame[len - 1]) // 校验错误直接丢弃整帧 return 0; *state frame[6]; // 官方数据手册中目标状态所在字节 *move_d frame[7]; // 移动目标距离 *static_d frame[9]; // 静止目标距离 return 1; }解析完成后不要直接拿单帧状态去报警雷达在目标由动转静时会有一段时间输出不稳定。我一般会维护一个 5 帧的状态窗口连续 3 帧为无人才把人员状态置为无人连续 2 帧为有人就立刻置为有人。这样既滤掉了瞬时误判又不会在老人跌倒时延迟太久。目标状态机和报警阈值放在一起处理state2且持续 10 秒以上是“活动异常”static_d小于雷达死区通常是 20cm且能量持续走低则可能是跌倒后无动作。这比单一阈值判断可靠得多。3.3 MLX90614 红外体温的读取与距离补偿MLX90614 是非接触红外测温模块SMBus 接口地址固定为 0x5A读取 RAM 地址 0x07 得到物体温度0x06 得到环境温度。标准库的硬件 IIC 在 STM32F103 上使用时要处理总线错误和仲裁项目里用模拟 IIC 反而稳定因为 MLX90614 的时钟频率要求并不高。// 读 MLX90614 的 RAM 寄存器addr 传 0x07 表示物体温度 // 返回值需要乘以 0.02 再减去 273.15 才是摄氏度 uint16_t MLX90614_Read_RAM(uint8_t addr) { uint16_t data; I2C_Start(); I2C_WriteByte(0x5A 1); // 设备地址 写位 I2C_ReadAck(); I2C_WriteByte(addr); // 写入 RAM 地址 0x06 / 0x07 I2C_ReadAck(); I2C_Start(); // 重复起始切换为读方向 I2C_WriteByte((0x5A 1) | 1); I2C_ReadAck(); data I2C_ReadByte(1); // 读低字节主机回 NACK data | (I2C_ReadByte(0) 8); // 读高字节回 ACK 以便继续读 PEC I2C_ReadByte(0); // PEC 校验字节本实现中不验证 I2C_Stop(); return data; } // 调用示例 // float tobj MLX90614_Read_RAM(0x07) * 0.02f - 273.15f;data是 14 位数据最高两位是标志位所以读完要立即转成温度。0x07 * 0.02 - 273.15的结果单位是摄氏度这在数据手册的公式表里是固定换算系数。代码里最后那个I2C_ReadByte(0)是在读 PEC 校验字节这里没有做 CRC8 验证工业环境里可以把 PEC 也解析出来计算方式是对地址、命令和数据做 CRC8初值为 0。3.4 体温数据修正的现场方法MLX90614 测的是额头或手腕表面温度和腋下体温有 1~2℃ 的偏差。更麻烦的是环境温度和测量距离对这个偏差影响很大距离越远红外能量衰减越多读数越低。常见的补偿公式是T_est T_obj (T_amb - T_obj) * k offset其中k取 0.05~0.1offset是校准偏差可以在设备固定安装后用体温计实测一次标定。这个工程里雷达的静止距离正好可以喂给补偿逻辑雷达给出距离值体温补偿系数按距离分段小于 30cm 用近距离系数大于 80cm 直接显示--因为超过 1 米后 MLX90614 的读数已经失去参考意义。这就是前面为什么要坚持用串口雷达而不是简单人体感应模块的原因距离信息本身就参与了传感器融合。4. TFT 液晶屏与 WiFi 上报现场交互与链路调度4.1 显示通道FSMC 刷屏与局部刷新策略这块工程的 TFT 液晶屏走的是 FSMC 接口16 位数据线一次写一个像素点。很多人在 F103 上刷屏慢原因是把整屏当画布一次重绘刷一帧要几百毫秒。正确做法是开启视窗只更新变化区域比如温湿度数值只占屏幕右上角一个 120x40 的区域那就在这个区域内重绘数字其余部分不动。// 设置可视区域只允许写入 x0~x1, y0~y1 范围内的像素 void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteReg(0x2A, x0 8); // 列地址高字节 LCD_WriteReg(0x2A 1, x0 0xFF); // 列地址低字节 LCD_WriteReg(0x2A 2, x1 8); LCD_WriteReg(0x2A 3, x1 0xFF); LCD_WriteReg(0x2B, y0 8); // 行地址 LCD_WriteReg(0x2B 1, y0 0xFF); LCD_WriteReg(0x2B 2, y1 8); LCD_WriteReg(0x2B 3, y1 0xFF); LCD_WriteReg(0x2C, 0); // 开始写入显存 }0x2A是列地址寄存器0x2B是行地址寄存器0x2C是显存写入命令。设置完窗口后连续写入像素点数据会自动换行不需要再发地址命令。配合一个“脏矩形”结构体把数据变化的区域记录下来每次循环只刷脏矩形刷新率可以从 5fps 提到 30fps 以上。刷屏时如果发现花屏先检查 FSMC 时序里的地址建立时间和数据建立时间典型值是0x00003034这一档时序过快会让 TFT 控制器来不及采样。对于用 0.96 寸 OLED 的方案IIC 屏则简单得多SSD1306 驱动只需要初始化序列和显存整包上传但它的刷新率瓶颈在 IIC 时钟上所以这类屏适合只显示小字量的场景大数字动态变化还是 TFT 更合适。4.2 无线通道ESP8266 的 AT 指令任务划分WiFi 模块用 ESP8266 的 AT 固件是常见做法STM32F103 通过串口发指令模块返回OK、ERROR或者IPD数据。AT 指令最坑的是等待时间连接路由器可能要 3 秒以上TCP 建链也要 1 秒左右如果在主循环里阻塞等这些响应DHT11 的读时序会被拖垮雷达的帧也会丢。所以任务划分方式应该是WiFi 的连接和上报放在一个独立的状态机中每 10ms 检查一次串口接收不为任何一条 AT 指令死等。// 上报一帧 JSON 数据到 TCP 服务器 // 注意ATCIPSEND 的字节数必须和实际发送长度一致否则数据会被截断 void ESP8266_Report(float temp, uint8_t hum, uint8_t gas, uint8_t person) { char msg[72]; char cmd[24]; uint16_t len; len sprintf(msg, {\t\:%.1f,\h\:%d,\co\:%d,\p\:%d}\r\n, temp, hum, gas, person); sprintf(cmd, ATCIPSEND%d\r\n, len); // 先告之发送长度 UART3_Send_String(ATCWJAP\mynet\,\12345678\\r\n); // 接入 AP UART3_Send_String(cmd); // 再告知数据长度 delay_ms(20); UART3_Send_String(msg); // 最后发送数据体 }ATCIPSENDlen是告诉模块“接下来要发多少字节”模块收到后回提示符此时才能发数据。很多排错案例都是长度算错strlen直接算中文会多算字节或者漏掉\r\n导致服务器端收到半个 JSON。数据体里用\r\n结尾是为了配合 TCP 服务器按行解析的惯例如果用 TCP 调试工具测试注意不要被工具的显示格式干扰。4.3 串口 1 与串口 3 的分工依据这套工程里串口资源分配很有代表性串口 1 留给调试下载和雷达以外的日志输出串口 3 单独给 WiFi 模块。理由是 STM32F103 的串口 1 挂在 APB2 总线上串口 3 挂在 APB1 总线上两者的时钟源不同。用标准库时串口 1 的时钟使能是RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE)串口 3 则是RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE)写错总线就会导致波特率配置失效串口完全不出数据。115200 波特率下两个总线的分频系数不同如果从串口 1 往串口 3 搬家直接改USART_InitStructure.USART_BaudRate是不够的还要检查RCC_PCLK1Config的时钟树配置。另外调试信息如果和 AT 指令响应用同一个串口主循环里printf一多WiFi 模块的响应就会被冲掉。所以串口 1 接调试器、串口 3 接模块的分工不能混。4.4 完整数据流的调度顺序系统初始化完成后主循环按一个固定节奏跑任务每 2 秒读一次 DHT11更新温湿度缓存每 1 秒读一次 MQ-7 ADC做平均滤波串口 2 中断持续收雷达帧主循环每 100ms 解析一次每 500ms 刷新 TFT 的脏矩形区域每 5 秒触发一次 WiFi 上报状态机。这个调度顺序的核心是耗时操作全部放主循环中断只做“收字节”和“置标志”。雷达数据每 100ms 解析一次刚好和模块输出频率匹配DHT11 读一次要 30ms放在 2 秒周期里不影响其他任务。如果以后要在这套代码上移植 FreeRTOS可以直接把上面 5 个周期任务拆成 5 个任务信号量用xSemaphoreGiveFromISR从串口中断里释放任务优先级按“雷达 显示 WiFi 传感器”排。5. 长期运行的稳定性处理电源、看门狗与死机恢复护理设备最怕的就是“悄无声息地不动了”。传感器偶尔读错可以容忍但主控死机后没人知道系统的可信度就归零。这里有几个实际验证过的问题点。第一个是供电。DHT11、雷达、ESP8266 同时工作时的峰值电流远超 STM32F103 的 LDO 承受能力常见做法是雷达和 WiFi 模块单独用 3.3V 稳压芯片供电传感器侧再加 10uF 和 0.1uF 的去耦电容。ESP8266 在 Wi-Fi 发射瞬间电流会到 300mA如果和主控共用一颗 AMS1117电压跌落会让 ADC 参考电压漂移MQ-7 的浓度读数会周期性跳高。把模块供电和模拟采样供电分开是这类项目里最值得先做的改动。第二个是独立看门狗。STM32F103 的 IWDG 使用内部 40kHz LSI 时钟不依赖主时钟主时钟挂了它照样能复位。配置代码如下void IWDG_Init(void) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); // 取消写保护 IWDG_SetPrescaler(IWDG_Prescaler_64); // 40kHz / 64 625Hz IWDG_SetReload(625); // 625 次计数约 1 秒溢出 IWDG_ReloadCounter(); IWDG_Enable(); // 使能后不可关闭 }喂狗点放在主循环末尾每轮循环喂一次。注意不要在 DHT11 读取前喂狗因为单总线时序最长可能阻塞 30ms如果喂狗点选在阻塞之前一旦传感器对地短路主循环就死在while等待里看门狗失去意义。正确位置是主循环所有阻塞操作完成之后。第三个问题是 SWD 下载失败后的恢复手段。项目里 DHT11 和雷达的 GPIO 初始化如果和 SWD 引脚冲突会导致第二次下载连不上这是很常见的事故。处理办法是把 BOOT0 拉高、BOOT1 拉低让芯片进入系统存储器模式用串口 ISP 擦除 Flash再复位回正常模式。所以硬件设计时 BOOT0 要留跳线帽或 0 欧电阻的位置不要直接接地。最后一个小细节是 PA11 引脚。PA11 同时映射到 USB_DM 和 TIM1_CH4如果代码里初始化了 GPIO 和定时器又没有关闭复用功能引脚会出现电平异常和传感器通信时会偶发数据错乱。排查顺序一般是先用万用表量引脚电压再查引脚复用表最后看时钟树。这套工程里 TFT 的数据线占了 PD0~PD15PA11 虽然空闲但如果有扩展板要新增外设优先避让 PA11能省掉不少排查时间。本文还有配套的精品资源点击获取
返回列表