尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于STM32与MQTT的物联网健康监测系统设计与详解

基于STM32与MQTT的物联网健康监测系统设计与详解 简介基于MQTT协议的物联网健康监测系统是一份以STM32微控制器为硬件核心、使用C语言开发的完整课程设计项目主要面向物联网工程、嵌入式系统方向的高校学生可用于期末大作业或课程设计参考。项目已通过导师指导并获得97分从硬件底层驱动到MQTT消息上报链路完整清晰下载后无需修改即可编译运行。压缩包共296个文件以h/c源文件、d依赖文件、o/crf编译中间文件为主配合png/jpg界面截图、js/json前端配置以及wxml/wxss微信小程序页面文件完整覆盖嵌入式终端、通信协议、云端接入与移动端展示等环节整体体积约8.16MB结构紧凑便于排查和学习。目前已有168人学习使用特别适合需要快速完成高评分物联网项目的读者可从中直接复用MQTT通信模块、传感器数据采集与界面交互代码并借助配套文档理解整体设计思路。1. 为什么MQTT健康监测课设选了STM32F103标准外设库拿到这个97分的MQTT健康监测工程最重要的不是直接烧录看效果而是理解数据链路为什么这样走。心率传感器输出模拟信号STM32F103用ADC采样后把数据同时送进I2C接口的OLED和USART连接的ESP8266ESP8266负责与物联网MQTT Broker建立长连接把数据发布到订阅主题。整个过程涉及的STM32标准外设库、MQTT控制报文、AT指令链路正好是物联网课设和期末大作业里最有区分度的部分。这套代码适合嵌入式课程设计也适合想从裸机编程进入物联网协议层的读者。下面按硬件、协议、链路、答辩四个维度拆开讲每一部分都有可以直接复用的代码。2. STM32外设资源分配ADC采样、I2C传感器与USART透传的配合打开工程文件文件名列表里stm32f10x_tim.c、stm32f10x_flash.c、stm32f10x_rcc.c、stm32f10x_adc.c、stm32f10x_i2c.c、stm32f10x_can.c、stm32f10x_usart.c基本都是标准外设库的驱动。看起来像“全家桶”式导入实际工程里每个模块各司其职。下面按健康监测的数据流来看这些外设怎么分配以及初始化参数该怎么调整。2.1 健康监测系统需要哪些外设资源第一版硬件规划通常是一块STM32F103C8T6最小系统板、一个PulseSensor心率模块、一个OLED屏、一个ESP8266模块。外设资源分配如下外设对应库文件作用配置要点ADC1stm32f10x_adc.c采集心率模拟电压采样时间要足够长建议大于100usI2C1stm32f10x_i2c.cOLED显示、可选数字温湿度传感器速度100kHz或400kHzUSART1stm32f10x_usart.c与ESP8266通信波特率115200帧格式8N1RCCstm32f10x_rcc.c时钟树配置外部8MHz晶振SYSCLK72MHzTIMstm32f10x_tim.c定时触发ADC采样500ms触发一次即可FLASHstm32f10x_flash.c保存报警阈值和校准值扇区擦除后再写入PulseSensor本质是一个光电容积脉搏波传感器输出的是随脉搏起伏的模拟电压幅度一般在0.5V到1.2V之间所以ADC量程用3.3V参考电压就够。OLED用I2C比SPI少两根线且驱动代码稳定适合课设快速出效果。USART1接ESP8266USART2可以接调试串口这样业务数据和日志数据互相不干扰。2.2 Keil工程文件与标准外设库的组织方式BKM32.uvguix.Administrator是Keil的窗口布局文件记录代码折叠、断点等界面状态不属于业务代码。BKM32.axf是上一次编译产生的镜像烧录器可以直接加载它但如果你改了源码而忘记重新编译烧进去还是旧固件。所以工程里要养成烧录前重新Build的习惯。根目录的keilkilll.bat是个很好的加分项内容一般是del *.o *.d *.crf *.axf *.htm *.sct *.map *.dep执行后可以清除Keil生成的中间文件让工程压缩包从几十MB降到几百KB。提交期末作业前运行一次既方便老师解压也能避免把带路径的中间文件传给别人后编译出错。标准外设库的特点是函数名和寄存器一一对应比如要配一个ADC通道先开APB2时钟、再配置GPIO、然后调ADC_Init和ADC_RegularChannelConfig。这个工程把stm32f10x_can.c也加进了编译列表原因是标准外设库整体加入工程比逐个挑文件更方便代价是编译时间变长。功能上CAN没有参与健康监测数据链路可以忽略。2.3 外设初始化代码ADC、USART与printf重定向ADC是数据采集入口配置好坏直接影响心率数值。下面是一份可用的ADC1通道0配置代码void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入禁止内部上拉下拉 GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 只采集一个通道 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; 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_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }代码里ADC_SampleTime_239Cycles5是采样时间ADC时钟如果是12MHz对应约20us。心率信号频率低采样时间长一点能抑制高频噪声。连续转换模式下只需要在主循环里读取ADC1-DR寄存器就能拿到最新值不需要每次启动转换。如果接的是体温模拟模块可以把PA0的参考电压改为内部参考但那是另一个配置。USART1的初始化需要注意TX和RX两个引脚的GPIO模式不同代码里的AF_PP和IN_FLOATING不能互换void USART1_Config(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); }调试时尽量不要用HAL库的打印方式而是在同一源文件重定义fputcint fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }这样就能直接用printf输出日志前提是Keil中勾选Use MicroLIB。注意在串口中断处理中不要调用printf否则重入会导致莫名其妙的死机。2.4 外设配置的三个常见误用第一是ADC引脚没有配置成AIN。如果保持GPIO_Mode_AF_PP引脚内部有源极输出电路毫伏级信号会被拉乱读到的心率数据是一堆锯齿波。第二是I2C引脚没有配置为开漏并外接上拉电阻OLED在低温或线长时会白屏。第三是USART波特率不一致ESP8266模块默认有些是9600、有些是115200先使用串口助手确认模块返回的是“ready”而不是乱码再去调STM32代码。标准外设库里的stm32f10x_flash.c在这个工程中用于保存报警阈值写入前必须整页擦除。在系统运行时频繁写Flash会拉长中断响应我一般只在阈值发生变化时写一次并且把写入函数放在低优先级任务里。3. MQTT协议报文构造嵌入式端CONNECT/PUBLISH的最小实现MQTT AT指令可以省去协议栈但如果你选的WiFi模块不带MQTT固件或者老师要求讲清楚协议就得认识报文本身。MQTT 3.1.1报文由固定头、可变头、负载三部分组成固定头第二个字节是“剩余长度”用来告诉Broker后面还有多少字节这是最容易写错的字段。3.1 先记住5种控制报文的固定头健康监测系统最低限度只需要5种报文。报文类型固定头首字节用途触发时机CONNECT0x10请求建立连接上电后CONNACK0x20Broker返回连接结果收到CONNECT后PUBLISH0x30/0x32/0x34发布数据每次采集周期PINGREQ0xC0保活探测KeepAlive超时前SUBSCRIBE0x82订阅下行控制主题连接成功后注意SUBSCRIBE的首字节一定是0x82低四位是0010这是协议规定的标志位。如果按PUBLISH的习惯写成0x80Broker会直接断开连接或返回错误。这个坑在评审现场被问到概率很高。3.2 在STM32上用C构造CONNECT报文先写一个剩余长度编码函数它支持大于127字节的负载static uint8_t mqtt_encode_remaining(uint8_t *buf, uint32_t len) { uint8_t count 0; do { uint8_t d len % 128; len / 128; if (len 0) d | 0x80; buf[count] d; } while (len 0); return count; }编码规则是每个字节低7位放数据最高位表示还有后续字节。比如150字节的剩余长度分解为0x96 0x01Broker读到0x96后知道后面还有字节继续读0x01加起来得到150。CONNECT报文构造函数如下uint16_t mqtt_build_connect(uint8_t *buf, const char *client_id, uint16_t keep_alive) { uint16_t pos 0; uint16_t remaining; uint8_t client_len strlen(client_id); buf[pos] 0x10; // CONNECT remaining 10 2 client_len; pos mqtt_encode_remaining(buf[pos], remaining); buf[pos] 0x00; buf[pos] 0x04; // 协议名长度 buf[pos] M; buf[pos] Q; buf[pos] T; buf[pos] T; buf[pos] 0x04; // MQTT 3.1.1 协议级别 buf[pos] 0x02; // Clean Session1 buf[pos] (uint8_t)(keep_alive 8); buf[pos] (uint8_t)(keep_alive 0xFF); buf[pos] 0x00; buf[pos] client_len; memcpy(buf[pos], client_id, client_len); pos client_len; return pos; }逻辑说明剩余长度等于10字节可变头加2字节ClientID长度加client_id实际长度。connect flag里的0x02表示Clean Session开启设备掉线重连后不保留离线消息。Keep Alive放在两个字节里高字节在前。这里假设client_id长度不超过127字节所以剩余长度编码只需要一字节但为了通用性仍然调用编码函数。PUBLISH报文稍微复杂一点因为需要考虑QoSuint16_t mqtt_build_publish(uint8_t *buf, const char *topic, const uint8_t *payload, uint16_t payload_len, uint8_t qos) { uint16_t pos 0; uint16_t remaining; uint8_t topic_len strlen(topic); buf[pos] (qos 1) ? 0x32 : (qos 2) ? 0x34 : 0x30; remaining 2 topic_len payload_len; if (qos 0) remaining 2; // 报文标识符 pos mqtt_encode_remaining(buf[pos], remaining); buf[pos] 0x00; buf[pos] topic_len; memcpy(buf[pos], topic, topic_len); pos topic_len; if (qos 0) { buf[pos] 0x00; // 报文标识符高字节 buf[pos] 0x01; // 低字节 } memcpy(buf[pos], payload, payload_len); pos payload_len; return pos; }参数说明qos0时首字节0x30不需要报文标识符qos1时0x32并在主题后插入两字节报文标识符Broker回PUBACK后同标识符才能释放重传资源。课程设计里我一般建议用QoS0代码少一层状态机。3.3 QoS、Keep Alive、Clean Session怎么设参数建议值原因QoS0心率数据以曲线形式展示丢一两帧肉眼无感知Keep Alive30大多数NAT会话60秒无流量会断30秒足够提前PINGClean Session1课程设计不需要离线补发遗嘱开启拔电时Broker能知道设备离线演示效果好连接标志位设置为0x06就表示Clean Session和Will同时开启。开启遗嘱后CONNECT报文的Payload顺序会变成ClientID、WillTopic、WillMessage长度也要对应增加。有些同学在这个地方只改标志位没有追加遗嘱字段导致Broker解析失败连接一直不成功。收到CONNACK后还要检查第二个字节0x00表示接受连接0x02表示Broker不支持协议级别0x05表示用户名密码未授权。排查阶段不要只看“连不上”要看AT串口返回的十六进制数据。4. 从传感器到MQTT BrokerESP8266 AT指令链路与数据验证MCU构造的MQTT报文要真正发到云上中间还隔着WiFi模块。最常见的方案是ESP8266刷MQTT AT固件STM32通过串口发送AT指令完成连接和发布。下面这条链路按“接线、配网、连Broker、发数据、云端验证”的顺序走每一个环节都有可检查的返回值。4.1 硬件接线与供电坑STM32F103引脚ESP8266引脚说明PA9RXDUSART1发送PA10TXDUSART1接收3.3V外接稳压VCC峰值电流约300mAGNDGND必须共地ESP8266的WiFi发射瞬间电流很大由STM32板载稳压器直接供电时经常出现“初始化正常一入网就重启”的现象。我通常从USB的5V取电经过AMS1117-3.3V给ESP8266单独供电再用万用表确认ESP8266的VCC在传输数据时没有跌落超过0.1V。4.2 AT指令配网与MQTT连接以下指令序列适用于常见的ESP8266 MQTT AT固件每条都以回车换行结尾ATCWMODE1 ATCWJAPyour_ssid,your_password ATMQTTUSERCFG0,1,BKM32_Device,mqtt_user,mqtt_pass,0,0, ATMQTTCONN0,broker.emqx.io,1883,1参数说明CWMODE1表示Station模式只把ESP8266作为客户端接入路由器。CWJAP返回WIFI CONNECTED和WIFI GOT IP才算成功。MQTTUSERCFG中第二个参数1是开启MQTT功能最后三个参数依次是证书标志、证书内容、遗嘱主题保持初始状态即可。MQTTCONN中第一个0是linkID同一个linkID后续发布订阅都指向这个连接最后一个1表示开启自动重连。发布一条心率数据有两种写法。一种是先发命令等待“”提示符再发数据ATMQTTPUB0,health/data,1,0 {heart_rate:72,temperature:36.5,ts:1713000000}另一种是某些固件支持一行完成把数据直接放在命令尾部并用引号包起来。如果第一种方式返回ERROR就查看固件文档改成第二种。数据格式建议用JSON因为MQTTX或Node-RED订阅后可以直接可视化不需要单独写解析。提示如果使用ESP8266 MQTT AT固件务必先用串口助手确认固件支持MQTTCONN指令有些旧固件只支持透传模式不支持本项目的连接方式。4.3 本地Broker搭建与数据验证公网Broker适合演示但实验室网络偶尔会连不上公用服务器。负责任的做法是在本地用Docker起一个EMQX流程完全一致docker run -d --name emqx -p 1883:1883 -p 18083:18083 emqx/emqx浏览器打开http://localhost:18083进入控制台默认账号密码一般是admin/public。在控制台的客户端列表里能看到当前连接数还能看到收到的消息量。这样演示时即使现场没有外网也能保证数据链路完整。验证时用MQTTX创建一个客户端填入相同Broker地址和主题health/data点击订阅后就能看到STM32上报的JSON。订阅端最好再显示收到消息的时间戳方便判断设备是否在持续上报。如果发现收不到先检查EMQX控制台里有没有新客户端接入再看ESP8266的串口日志返回的是PUBLISH OK还是ERROR。4.4 串口日志与状态机的配合MCU向ESP8266发AT指令后不能简单加个延迟就继续因为WiFi模块响应时间在几十到几百毫秒之间波动。我会在STM32端维护一个状态机状态依次为UART_WAIT_OK、UART_WAIT_CONNECTED、MQTT_WAIT_CONNACK、MQTT_RUNNING。每次收到一行以\r\n结尾的字符串时用strstr判断是“OK”“WIFI GOT IP”还是“ERROR”然后切换状态。主循环里的数据发布只在MQTT_RUNNING状态下执行。调试阶段最容易被忽略的是ESP8266上电后的“ready”字符。它不代表WiFi已连接只是模块启动完成。有的同学在初始化串口后马上发送AT结果模块还没就绪第一帧命令被丢弃后续就乱了。正确做法是等待“ready”或循环发送AT直到收到OK再继续。5. 期末答辩高分区断线重连、看门狗与演示细节这个项目能跑起来只拿基础分拉开差距的是异常处理和演示节奏。以下内容不是文档里要求的但都是评审老师会注意到的点。5.1 演示顺序与异常场景建议按四步演示上电后OLED显示心率/温度数据在刷新。打开MQTTX订阅health/data点开图表看曲线。拔掉心率传感器的信号线再插回OLED恢复显示。关掉路由器电源15秒后恢复设备自动重新接入并继续上报。第四步是关键。评委看到设备能自动恢复才会认为你理解了长连接的含义而不是把代码写死。5.2 断线重连状态机与STM32看门狗在前面状态机的基础上增加RECONNECT_WAIT状态。进入该状态时记录当前时间只有超过10秒才重新发起ATCWJAP连接避免WiFi网络没恢复时频繁重连把模块卡死。代码结构大致是typedef enum { WIFI_CONNECTING, MQTT_CONNECTING, MQTT_RUNNING, RECONNECT_WAIT } LinkState; LinkState state WIFI_CONNECTING; uint32_t reconnect_start 0;主循环里根据状态执行对应步骤。需要注意不要用delay函数等10秒会让看门狗误判系统卡死。看门狗配置如下IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_64); IWDG_SetReload(625); IWDG_ReloadCounter(); IWDG_Enable();在40kHz LSI时钟下预分频64后计数频率625Hz重载值625对应1秒超时。喂狗位置放在数据成功发布之后不要放在主循环开头。这样如果Broker连接卡死导致串口阻塞看门狗才能复位设备。调试时如果觉得频繁复位先在Keil里禁用IWDG调通后再打开。5.3 容易被追问的三个问题老师常问“为什么用MQTT不用HTTP”用一句话回答HTTP是短连接每次请求都要重建TCP连接协议头大MQTT通过一条TCP长连接双向通信固定头只有2字节实时性和功耗都更优。问“Broker收不到数据怎么定位”时按三层回答第一看STM32串口发出的是不是正确AT指令第二看ESP8266是否返回PUBLISH OK第三看MQTTX是否收到数据。三层都没问题再看JSON格式是否有未转义的引号。问“你的系统有没有算法含量”时可以提滑动平均滤波对ADC采样值每次取最近10次平均值剔除明显毛刺。这是一道简单但说得清的技术点。5.4 把课设扩展成可跟踪的数据管道如果想把架构往上提在EMQX控制台里配置规则引擎用一条SQL选中health/data主题SELECT payload FROM health/data转发到MySQL或InfluxDB前端用Grafana画趋势图。这样健康监测就从“能看到数据”变成了“数据可查询、可报警、可追溯”。最后把采集周期从500ms改成10sJSON字段换成你的传感器字段例如把payload字段改为soil_humidity、light_level就是一套农田环境监测链路。本文还有配套的精品资源点击获取
返回列表