ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于STM32的水下水质监测终端:集成4G与腾讯云IoT的全栈实现

基于STM32的水下水质监测终端:集成4G与腾讯云IoT的全栈实现 简介基于STM32的水下水质检测装置设计文档面向嵌入式开发者、物联网爱好者及高校项目实践者适用于水环境监测与智能硬件开发场景。方案以STM32F103RCT6为主控集成谐振式水深传感器、模拟量水质传感器、DS18B20防水温度传感器与合宙Air724UG-4G模块实现水深、温度、浊度、TDS实时采集并经由腾讯云IoT平台对接微信小程序远程控制文档同时给出0-1m水深、±5cm深度误差、±1℃温度精度等关键性能指标。包体为1个PDF文件容量约43.93MB内容涵盖需求分析、硬件选型、结构封装、嵌入式软件与微信小程序设计并附具体参数和误差指标。目前已有127人浏览学习。适合用于课程设计、毕业设计或水质监测项目参考可帮助读者快速掌握从传感器数据采集、4G上传到云端可视化的完整链路以及基于DIY外壳的低成本实现思路。1. 为什么要自己动手做一台能下潜的水质监测终端传统水质监测大多停留在人工采样、实验室分析的阶段采样周期长、点位覆盖稀疏遇到突发污染很难第一时间拿到数据。而市售的多参数水质浮标价格高、部署重不适合小水体、浅水区的快速巡检。这台基于 STM32 的水下水质检测装置思路很直接把深度、温度、浊度、TDS 四个参数集成到一个小型水下终端里通过 4G 网络把数据实时推到腾讯云物联网平台再配合微信小程序做远程查看与控制。整套系统从传感器采集、MQTT 上报到小程序指令下发链路完整适合做毕业设计、环境监测原型验证也适合想深入理解「嵌入式设备 物联网云平台 小程序端」全栈开发的工程师。项目最具启发性的地方在于它用两个 5V 小水泵改变储水舱水量来调节浮力实现了低成本的水下深度控制这比传统螺旋桨推进方案更简单也更容易复现。2. 硬件选型与系统架构设计2.1 主控选型为什么是 STM32F103RCT6 而不是其他芯片主控选择 STM32F103RCT6核心考量是外设资源与开发成本的平衡。这颗芯片属于大容量型产品内置 256 KB Flash 和 48 KB SRAM对于本项目的代码量绰绰有余。更重要的是它提供了 5 个 USART、3 个 12 位 ADC、2 个 I2C、3 个 SPI、51 个通用 GPIO以及 2 个 DMA 控制器。项目中 DS18B20 使用单总线协议、TDS 和浊度传感器输出模拟信号、4G 模块走串口通信这些外设需求刚好被覆盖不需要额外的扩展芯片。从开发效率角度看STM32F103 系列是当前资料最丰富、生态最成熟的 Cortex-M3 芯片之一。无论是标准外设库还是 HAL 库网上都有大量可验证的参考代码。如果换用国产替代芯片如 GD32F103虽然硬件兼容但在 4G 模块的串口驱动和 ADC 采样的细节上仍需要重新验证时序没有必要增加这个风险。开发工具选择 Keil MDK配合 ST-Link 下载调试上手门槛最低。2.2 传感器选型与接口分析各传感器与主控的接口关系如下表所示模块型号/类型主控接口输出信号关键参数深度测量谐振式水位水深传感器通用 GPIO ADC模拟/频率信号0~1m误差 ±5cm温度感知DS18B20 防水型单总线 GPIO数字信号0~40℃误差 ±1℃浊度检测模拟量水质传感器ADC 通道0~4.5V 模拟量0~1000NTUTDS 检测TDS 传感器模块ADC 通道0~2.3V 模拟量0~1000ppm4G 通信合宙 Air724UGUSART AT 指令UART TTLMQTT over TCP动力控制5V 潜水小水泵 x2GPIO 继电器开关量排水下潜 / 吸水浮起DS18B20 的防水封装是必须的普通封装的传感器在潮湿环境下管脚容易氧化导致读数漂移。TDS 传感器的激励源是交流信号这能有效防止探头极化但这也意味着 ADC 采样时不能直接读取单次值而是需要在软件层做多次采样求平均或者配合硬件 RC 滤波后再进 ADC。谐振式水深传感器的输出形式在不同厂家之间差异很大有的输出模拟电压有的输出 PWM 频率。设计时需要在原理图上预留跳线或排针让软件可以通过配置选择采样方式避免硬件改版。2.3 动力系统设计思路设备的上下浮动控制是项目中最有工程味道的部分。两个 5V 潜水小水泵分别负责「排水」和「吸水」通过继电器控制通断。排水时储水舱水量减少、浮力增加设备上浮吸水时储水舱水量增加、浮力减小设备下潜。继电器选用低电平触发的光耦隔离模块目的是避免水泵启停时产生的反向电动势通过电源线干扰 STM32 的 ADC 采样。水泵工作时电流约 300~500mA两节 18650 锂电池串联可以提供约 7.4V 电压但 STM32 和传感器需要 3.3V 和 5V 两路稳定电源因此供电链路设计为18650 电池组7.4V→ AMS1117-5.0 → 5V 供电轨水泵、继电器、传感器 ↓ AMS1117-3.3 → 3.3V 供电轨STM32、OLED、4G 模块提示水泵和 4G 模块是系统中最大的两个功耗源。4G 模块在建立网络连接和 MQTT 心跳时会有瞬间大电流建议在 4G 模块的 VCC 引脚旁并联一个 470μF 的电解电容防止电压跌落导致模块重启。2.4 结构分层与防水处理设备外壳采用普通塑料饭盒分为上下两层。上层放置 STM32 开发板、继电器模块和 4G 模块传感器探头穿过隔板伸入水中下层是储水舱两个水泵固定在舱体底部。上层必须做密封处理常用的做法是在饭盒盖边缘打一圈中性硅酮密封胶传感器过孔处用热熔胶封堵。需要注意的一点是DS18B20 和 TDS 探头长期接触水体连接线进入上层舱体的位置是漏水高发区建议在过线孔处加装 M8 防水航空接头虽然成本增加十几元但能显著提升设备在野外环境下的可靠性。3. STM32 驱动开发与传感器数据采集3.1 工程框架与代码结构基于标准外设库建立工程代码结构按模块划分USER/ ├── main.c // 主循环与状态机 ├── stm32f10x_it.c // 中断服务函数 HARDWARE/ ├── ds18b20.c // 单总线驱动 ├── adc_tds.c // TDS/浊度 ADC 采样 ├── depth_sensor.c // 水深传感器驱动 ├── relay_ctrl.c // 继电器控制 └── usart_air724.c // 4G 模块串口驱动 BSP/ ├── delay.c // 延时函数 ├── i2c_oled.c // OLED 显示驱动 └── mqtt_data.c // MQTT 报文组包3.2 DS18B20 驱动实现DS18B20 使用的是单总线协议时序要求严格代码需要在关中断状态下执行时序操作避免被其他中断打断导致时序错误。以下是标准驱动中的关键读取函数uint8_t DS18B20_Read_Byte(void) { uint8_t i, data 0; for (i 0; i 8; i) { data 1; DS18B20_DATA_OUT(); // 切换为输出模式 GPIO_WriteBit(DS18B20_PORT, DS18B20_PIN, Bit_RESET); delay_us(2); // 主机拉低总线 2us GPIO_WriteBit(DS18B20_PORT, DS18B20_PIN, Bit_SET); delay_us(6); // 释放总线 6us DS18B20_DATA_IN(); // 切换为输入模式 delay_us(4); // 在 4us 后采样读到 0 或 1 if (GPIO_ReadInputDataBit(DS18B20_PORT, DS18B20_PIN)) data | 0x80; delay_us(50); // 等待时隙结束 } return data; }这段代码的核心在于时序窗口的精确控制。DS18B20 规定主机发出读时序后必须在 15μs 内完成采样所以代码在第 12 行先拉低总线 2μs 再释放等待 6μs 后切换输入模式再过 4μs 采样。这里使用delay_us()函数是纯软件延时实际延时会受编译器优化等级影响建议在调试时用逻辑分析仪抓一下波形确认每个时隙的长度符合 DS18B20 数据手册要求。如果发现读出的温度值偶尔出现 85℃上电复位值说明时序还不够稳定需要微调延时参数。温度值读取完成后原始数据是一个 16 位有符号整数0x07D0 代表 25℃。转换公式为float temp (float)((raw_temp 4) | ((raw_temp 0x0F) 12)) * 0.0625;用右移 4 位取得整数部分低 12 位符号扩展后乘以 0.0625即 1/16得到小数部分这是 DS18B20 的固定分辨率不需要额外配置。3.3 TDS 与浊度的 ADC 多通道采样TDS 和浊度传感器输出都是模拟电压信号分别接入 STM32 的 ADC1_IN0 和 ADC1_IN1 通道。使用 ADC 的扫描模式配合 DMA可以自动完成多通道循环采样避免 CPU 频繁介入。配置如下ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; uint16_t adc_buffer[2]; // 分别存放 TDS 和浊度通道的值 void ADC_DMA_Init(void) { DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 2; 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_Init(DMA1_Channel1, DMA_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; // 扫描模式 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; // 两个通道 ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); }DMA 工作在循环模式下硬件会自动把两次转换结果依次写入adc_buffer[0]和adc_buffer[1]。主循环中直接读取这个数组即可得到两个通道的最新采样值。设置采样时间为 55.5 个周期目的是降低传感器输出阻抗对采样电容充电时间的影响如果采样时间太短读到的电压值会偏低。TDS 电压到 ppm 的换算不是线性的需要根据传感器的标定曲线查表计算。通用做法是使用分段线性插值传感器手册通常会提供 25℃ 下的电压-ppm 对照表。温度对 TDS 测量结果影响较大水温每升高 1℃电导率大约增加 2%所以正式代码里要引入温度补偿float tds_compensate(float voltage, float temperature) { float raw_tds tds_voltage_to_ppm(voltage); // 查表获得未补偿值 float temp_coeff 1.0f 0.02f * (temperature - 25.0f); return raw_tds / temp_coeff; // 折算回 25℃ 基准浓度 }浊度的换算逻辑相反电压值越高代表水体越浑浊具体公式因传感器型号而异但整体思路都是在 ADC 原始值、电压值、物理量之间做线性映射。3.4 水深数据的稳定化处理谐振式水深传感器输出的模拟信号在水中会受到水流扰动影响数值跳动比较大。单纯在硬件上加一个 100nF 电容滤波效果有限软件上需要对采样值做滑动平均或者一阶低通滤波。一阶低通滤波实现最简单float depth_filtered 0.0f; #define DEPTH_ALPHA 0.15f // 滤波系数越小越平滑 float depth_lowpass(float depth_raw) { depth_filtered DEPTH_ALPHA * depth_raw (1.0f - DEPTH_ALPHA) * depth_filtered; return depth_filtered; }系数 0.15 意味着新的采样值只贡献 15% 的权重对高频波动抑制明显。但如果设备实际上下移动时这个系数会导致深度响应滞后约 1~2 秒。本项目深度控制精度要求是 ±5cm且水的浮力变化本来就是缓慢过程1~2 秒的滞后可以接受。如果后续想提高响应速度可以改成动态滤波系数当原始值与滤波值偏差超过 3cm 时认为设备在主动运动此时把DEPTH_ALPHA临时提高到 0.6加快跟踪速度。4. Air724UG 4G 模块与腾讯云 IoT 平台接入4.1 MQTT 上报链路搭建合宙 Air724UG 模块出厂默认是 AT 固件可以通过串口发送 AT 指令完成网络注册、TCP 连接和 MQTT 报文收发。整体链路是STM32 (USART) ←AT指令→ Air724UG (4G) ←→ 腾讯云 IoT MQTT Broker腾讯云 IoT 平台的 MQTT 接入地址格式为${ProductID}.iotcloud.tencentdevices.com端口 1883。设备认证使用 HMAC-SHA1 签名用户名格式为${ProductID}${DeviceName}密码是签名后的字符串。签名计算写在 PC 端工具或小程序云端函数里设备端不需要自行实现加密算法。4.2 AT 指令序列与 MQTT 参数配置设备上电后的初始化指令序列如下// 1. 模块开机 AT\r\n // 测试串口通信返回 OK ATE0\r\n // 关闭回显减少串口数据量 // 2. 网络注册 ATCREG?\r\n // 查询网络注册状态期望返回 0,1 ATCGREG?\r\n // 查询 GPRS 网络注册状态 // 3. 激活数据业务 ATCGACT1,1\r\n // 激活 PDP 上下文 // 4. 连接腾讯云 MQTT 服务器 ATMQTTCFG\dev_product_id\,\dev_device_name\,\dev_device_secret\\r\n ATMQTTOPEN\PRODUCTID.iotcloud.tencentdevices.com\,1883\r\n // 5. 等待 MQTT 连接成功后订阅与发布 ATMQTTSUB\$topic/device/control\,0\r\n ATMQTTPUB\$topic/device/data\,\{\\\depth\\\:0.35,\\\temp\\\:22.5,\\\tds\\\:128,\\\ntu\\\:45}\,0\r\n关键参数说明ATMQTTCFG中的三个参数是腾讯云 IoT 平台的设备三元组分别对应产品 ID、设备名称和设备密钥。设备密钥在上报前先与当前时间戳拼接再做 HMAC-SHA1 签名实际发送时需要把签名结果作为 MQTT 密码字段。ATMQTTOPEN的地址不能填 IP必须填平台分配的域名否则 TLS 握手或 MQTT 连接会失败。ATMQTTPUB的 QoS 选择 0 即可水质数据上报频率是每 30 秒一次数据量小、允许偶发丢失QoS 0 可以最大程度降低网络开销。提示Air724UG 模块出厂固件版本不同AT 指令集有细微差异。使用前务必执行ATCVERSION查看固件版本然后对照合宙官方 AT 指令手册确认MQTTCFG指令的传入参数格式。部分新固件把参数分隔符从逗号改成了分号直接套老代码会报错。4.3 数据上报格式设计腾讯云 IoT 平台支持标准 JSON 数据模板在平台控制台创建数据流时定义字段名设备端上报的 JSON 必须与模板字段一一对应。推荐上报格式如下{ method: report, clientToken: 123456, params: { depth: 0.35, temp: 22.5, tds: 128, ntu: 45, battery: 7.2 } }method固定为reportclientToken是本次上报的唯一标识建议使用自增计数方便在平台控制台排查丢包。params里除了四个水质参数外把电池电压也一并上报用于小程序端显示低电量告警。STM32 端使用snprintf拼装 JSON 字符串时要注意浮点格式化。默认的%f在部分嵌入式 C 库中不被支持编译时会链接失败。更稳妥的做法是把浮点数放大 10 倍或 100 倍转成整数再输出字符串char json_buf[128]; uint16_t depth_x100 (uint16_t)(depth_filtered * 100); // 0.35m → 35 uint16_t temp_x10 (uint16_t)(temp_value * 10); // 22.5℃ → 225 snprintf(json_buf, sizeof(json_buf), {\method\:\report\,\clientToken\:\%d\,\params\:{\depth\:%d.%02d,\temp\:%d.%d}}, msg_count, depth_x100 / 100, depth_x100 % 100, temp_x10 / 10, temp_x10 % 10);手动拆分整数和小数部分避开了浮点格式化问题生成的 JSON 也是合法的数值格式。这种方法在嵌入式 IOT 项目中非常实用避免了引入printf浮点库带来的代码体积膨胀。5. 微信小程序端实时数据展示与远程控制5.1 小程序与腾讯云 IoT 的对接方式微信小程序不能直接通过 MQTT 连接腾讯云 IoT Broker因为小程序网络请求只支持 HTTPS且不允许任意 TCP 长连接。这里需要借助腾讯云 IoT 平台提供的 HTTP API 来完成数据下发和设备状态获取。实际流程是小程序端调用云开发的云函数云函数持有腾讯云 API 的 SecretId 和 SecretKey通过云函数转发请求到腾讯云 IoT 平台。这样设备密钥和 API 密钥都保存在云端不会暴露在用户手机上。5.2 获取设备最新数据小程序云函数代码示例如下// cloudfunctions/getDeviceData/index.js const cloud require(wx-server-sdk) const tencentcloud require(tencentcloud-sdk-nodejs-iotexplorer) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event, context) { const IotExplorerClient tencentcloud.iotexplorer.v20190423.Client const client new IotExplorerClient({ credential: { secretId: process.env.TENCENT_SECRET_ID, secretKey: process.env.TENCENT_SECRET_KEY, }, region: ap-guangzhou, profile: { httpProfile: { endpoint: iotexplorer.tencentcloudapi.com } } }) const params { ProductId: event.productId, DeviceName: event.deviceName, } try { const result await client.DescribeDeviceData(params) return { code: 0, data: JSON.parse(result.Data) } } catch (err) { return { code: -1, msg: err.message } } }这个云函数做的事情只有一件调用腾讯云 IoT Explorer 的DescribeDeviceData接口获取设备上报的最新数据。process.env.TENCENT_SECRET_ID是在云开发控制台配置的环境变量不要硬编码在代码里。小程序前端拿到返回数据后用setData渲染到页面上即可。5.3 远程控制指令下发控制设备上浮和下潜的指令通过腾讯云 IoT 平台的PublishMessage接口下发// cloudfunctions/controlDevice/index.js const params { ProductId: event.productId, DeviceName: event.deviceName, Topic: $topic/device/control, Payload: JSON.stringify({ action: event.action, // up 或 down duration: event.duration // 水泵运行时间单位秒 }) } const result await client.PublishMessage(params)设备端在 MQTT 订阅了$topic/device/control这个主题收到消息后解析 JSON根据action字段控制继电器的通断。控制逻辑中必须加一个duration参数避免用户在小程序上误触后水泵长时间空转。例如下潜指令可以限制为duration: 3即水泵最多运行 3 秒每次下潜深度约 10cm用户可以通过多次点击来精确调整深度。5.4 深度控制闭环设备端收到控制指令后需要结合当前深度做闭环判断。比如用户目标深度是 0.5m当前深度是 0.2m设备应该自动进入下潜状态直到深度达到 0.5m ±3cm 才停止。这部分逻辑放在 STM32 端实现伪代码如下void depth_control_task(void) { if (target_depth 0.01f) // 收到有效目标深度 { float current depth_filtered; float diff target_depth - current; if (diff 0.03f) // 当前深度太浅需要下潜吸水 { RELAY_PUMP_IN_ON(); // 打开吸水水泵 RELAY_PUMP_OUT_OFF(); } else if (diff -0.03f) // 当前深度太深需要上浮排水 { RELAY_PUMP_OUT_ON(); // 打开排水水泵 RELAY_PUMP_IN_OFF(); } else { RELAY_PUMP_IN_OFF(); // 在目标范围内关闭所有水泵 RELAY_PUMP_OUT_OFF(); } } }3cm 的滞回区间是必须的。如果没有滞回控制水泵会在目标深度附近频繁启停继电器触点寿命会急剧缩短且水流的惯性会导致深度来回振荡。6. 实测校准与排错技巧6.1 深度传感器零点和满量程标定谐振式水深传感器的输出在空气中并非 0V而是存在一个固定的零点偏置。连接好硬件后先把设备放在空气中静置 10 秒记录此时 ADC 采样值作为zero_offset然后在 1m 深的水桶中记录满量程值full_scale。实际深度计算公式为float depth_cm (adc_value - zero_offset) * 100.0f / (full_scale - zero_offset);这个标定过程每次开机后自动执行一次即可不必每次测量都做。注意水深传感器的工作电压必须稳定在 5V电压波动会直接影响输出幅值导致换算后的深度值偏移。建议用万用表实测传感器供电引脚电压如果低于 4.8V需要在电源路径上加大滤波电容。6.2 TDS 探头极化的判断与处理TDS 探头如果长时间浸泡在水中探头表面会附着杂质导致测量值缓慢漂移。判断是否极化可以把探头从水中拿出用纯净水冲洗后放入标准溶液中测试。如果误差超过 10%需要用软毛刷轻轻刷洗探头表面。传感器采用交流激励源能减缓极化但不能完全避免建议每个月的维护周期中增加一次探头清洗。6.3 4G 模块常见连接故障排查遇到小程序端长时间刷不出数据的情况优先按以下顺序排查现象可能原因检查方式串口发送 AT 无响应模块未开机或 TX/RX 接反用 USB 转 TTL 直接连模块单独验证ATCREG?返回 0,0SIM 卡未识别或天线未接检查 SIM 卡卡扣和天线接头MQTT 连接超时签名错误或设备未激活在腾讯云控制台查看设备在线状态数据上报但小程序不显示数据模板字段不匹配在控制台设备日志里查看原始上报 JSON6.4 微信小程序页面下拉刷新与深度控制体验优化小程序端的数据展示页建议开启enablePullDownRefresh同时使用 30 秒的定时器自动调用云函数拉取最新数据。深度控制按钮需要增加二次确认弹窗防止误触导致设备突然下潜。更实用的设计是把目标深度的设定改为滑块形式用户滑动选择目标深度后点击「执行」设备端自动完成闭环控制这比单纯的点按「上浮」「下潜」按钮要直观得多。实际联调中最容易忽略的是 4G 模块的功耗控制。设备长时间待机时可以让 Air724UG 进入飞行模式定时唤醒上报数据后再休眠整机平均功耗能从 300mA 降到 80mA 左右两节 18650 电池的续航可以从 6 小时延长到 20 小时以上。这个低功耗策略在设计电池容量时就要提前规划好否则锂电池组在野外环境下很难支撑一整天的连续监测任务。本文还有配套的精品资源点击获取
返回列表