简介:这份PDF完整呈现了基于STM32的水下水质检测装置设计方案,适合电子/嵌入式方向的学生、竞赛团队及物联网开发者参考。内容围绕STM32F103RCT6主控,讲解水深、温度、浊度、TDS检测原理,并覆盖4G模块上云、腾讯云IoT平台接入以及微信小程序远程控制实现,从硬件选型、结构封装到嵌入式软件与云端联调均有系统梳理。资源为单个PDF文档,大小43.93MB,页数内容详实,便于按章节阅读。目前已有127人学习下载,适合用于毕业设计、课程项目或产品原型开发前的方案调研与功能拆解。借助这份资料,可快速理解谐振式水深传感器、模拟量水质传感器、DS18B20防水温度传感器、继电器控制水泵浮力调节等关键技术,并对照其DIY塑料饭盒外壳思路降低样机制作成本。
1. 不装 APP 也能看水质:STM32 与微信小程序的分工逻辑
水下水质检测这个题目,真正麻烦的从来不是“测”这个动作,而是“看”这个动作。传感器泡在水里,数据出来了,人却待在岸上、办公室里,甚至在外地。给每台设备配一台工控屏不现实,给每个用户装一个原生 APP 更没有人愿意接受——这就是微信小程序在这个项目里最值得被选中的原因:它天然跨平台、免安装、打开即用,而 STM32 负责的则是另外一个问题:如何在电池供电和水下密封的环境里,把一个又一个模拟信号变成可靠、可传输的数字帧。
两者合在一起,构成了这类毕业设计或产品原型最常见的形态:STM32 做边缘采集和协议打包,小程序做远端展示和触达。你在调试中要处理的,也不再是单片机的点灯问题,而是一条从传感器探头、运放调理电路、ADC 采样、滤波算法、通信模组、云平台,一直延伸到小程序页面的完整数据链路。这篇文章不打算照着 PDF 目录复述方案文档,而是按一线工程师做这套东西的常规路径,把硬件选型、代码实现、通信协议和小程序端对接的关键节点逐个讲清。适合正在做 STM32 嵌入式开发、又需要补小程序知识的开发者,也适合准备做毕设但不想只在原理图上打转的学生。
2. 硬件链路怎么搭:传感信号、主控资源与通信选型
2.1 从传感器信号类型反推电路拓扑
水质检测听起来是“测水质”,落到硬件上其实是“测电压”和“测数字”。不同的传感器输出类型完全不同,这直接决定了你的运放电路、接口方式和 STM32 内部资源分配。常见的水质参数无外乎五种:温度、pH、浊度、溶解氧、TDS。温度最简单,用 NTC 热敏电阻或者 DS18B20 数字温度传感器都行;pH 电极输出的是高内阻的微弱电压信号,不能直接进 ADC,必须先用高输入阻抗的运算放大器做缓冲和放大;浊度传感器通常是红外 LED 照射水体、光敏管接收散射光,输出的是随浊度变化的电压,但 LED 部分往往需要 PWM 驱动;溶解氧传感器如果选极谱式探头,输出的是微弱电流信号,复杂一些,实际项目里很多方案直接走 RS485 数字输出,省掉模拟调理;TDS 测量电导率,探头不能通直流电,需要交流激励源。
把这些信号整理一下,你在原理图阶段就应该画出第二层需求:需要至少 3 路 ADC 输入通道,1 路 PWM 输出,1 路 UART 给 RS485 通信,还要保留若干 GPIO 作为传感器供电开关。表格里的接口映射可以作为参考起点,实际引脚分配要结合你的芯片封装和 PCB 布局来定。
| 参数 | 探头输出 | 调理方式 | STM32 接口 |
|---|---|---|---|
| 温度 | NTC 分压电压 / 单总线数字 | 分压电路 | ADC / GPIO |
| pH | 高内阻电压(毫伏级) | 高输入阻抗运放 | ADC |
| 浊度 | 0~4.5V 电压 | 电压跟随器 | ADC + PWM |
| 溶解氧 | RS485 Modbus | 无,直接通信 | USART + 收发器 |
| TDS | 交流电流 | 交流激励 + 整流 | ADC + GPIO 控制 |
2.2 为什么选 L4 系列而不是 F1 系列做主控
很多 STM32 项目默认用 F103C8T6,但水下水质检测装置通常是电池供电,F1 的功耗在深度睡眠场景下并不理想。常见做法是换成 STM32L431 或者 STM32L476 这类 L4 系列,原因有两个:一是 STOP 模式下的电流能压到微安级别,配合定时唤醒可以做到长时间待机;二是 L4 的 ADC 是 12 位硬件过采样,支持多通道扫描加 DMA,省出来的 CPU 时间可以丢给通信协议处理。如果项目没有低功耗需求、纯做岸电供电的浮标站,F103 也完全够用。
GPIO 的配置在这个项目里比一般点灯实验讲究得多。传感器供电控制引脚要设置为推挽输出,并且拉低到地来关闭传感器,而不是仅仅断开 GPIO 输出——很多传感器模块在断电后还会通过信号引脚倒灌电流,这个细节在低功耗调试时会让人抓狂。ADC 引脚的 io_mode 要设置为模拟模式,别留着默认的推挽复用,否则采样值会偏离实际电压。
2.3 通信链路选型:WiFi / LoRa / 蓝牙怎么取舍
数据要从小水下的主控板到达手机小程序,中间隔着一层水体和一层空气。水体会严重衰减 2.4GHz 信号,所以通信模组通常放在水面以上的浮体或岸边机箱里。剩下这一段陆地无线链路有几种常见方案,选型要看部署场景:近岸鱼塘、实验室水族箱、城市河道这类百米以内的场景,首选 WiFi,用 ESP8266 或者 ESP32 做透传成本最低、调试最方便;如果设备分布在几百米甚至几公里的水库断面,LoRa 才是可靠做法,但你需要自建网关,网关再通过以太网或 4G 上云;蓝牙 BLE 只适合手机贴近设备调试用,不适合长期上传。简单对比可以看这个表。
| 方案 | 通信距离 | 功耗 | 组网复杂度 | 适用场景 |
|---|---|---|---|---|
| WiFi (ESP8266) | 50~100m | 高 | 低 | 鱼塘、实验室、近岸 |
| LoRa | 1~10km | 低 | 高,需网关 | 水库、河道断面 |
| 蓝牙 BLE | 10m | 低 | 低 | 现场调试、水族箱 |
| 4G 模组 | 不限 | 高 | 中 | 偏远站点、移动巡查 |
我一般建议毕设和快速原型优先走 WiFi 方案,先把整条数据链路跑通,再考虑 LoRa。因为 MQTT 协议、JSON 透传、小程序对接这些都和底层无线方式无关,换 LoRa 只是把 ESP8266 换成串口转 LoRa 模块,上层代码几乎不用动。
2.4 供电与采样间隔的估算方法
水下装置的供电大部分用锂电池组,容量按“采样间隔”来估算比按“连续工作”现实得多。一套 L431 + 多路传感器 + ESP8266 的典型功耗约 300mA@5V,如果每 15 分钟唤醒一次、每次工作 10 秒,平均电流大概是 300mA * 10/900 ≈ 3.3mA,加上 STOP 模式本身约 10uA,基本可忽略。那么一块 5000mAh 的电池理论上能撑 1500 小时,约两个月。这个估算里没有考虑电源转换效率和电池自放电,实际打个七折。
3. STM32 端的采集代码:ADC、定时器与数据帧设计
3.1 用 DMA 做多通道 ADC 采集,避免阻塞式轮询
水质采集不是一次性读完就完事,pH 和浊度信号本身有噪声,通常要连续采集多轮再滤波。常见做法是:TIM6 定时触发 ADC 启动转换,ADC 按扫描模式依次采样 pH、浊度、温度三路通道,DMA 把结果搬到内存数组。这套组合拳的好处是采样间隔由定时器决定、数据搬运由 DMA 完成,CPU 可以去处理通信协议,不阻塞。初始化代码分三段:GPIO、ADC、DMA。
// 使能时钟,配置引脚复用 __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4; gpio.Mode = GPIO_MODE_ANALOG; gpio.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &gpio); // ADC 配置:扫描模式 + 三通道 hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ENABLE; // 开启多通道扫描 hadc1.Init.ContinuousConvMode = DISABLE; // 由定时器触发,不连续 hadc1.Init.NbrOfConversion = 3; HAL_ADC_Init(&hadc1); ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; // pH 探头,PA0 sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_640CYCLES; // 高阻信号,采样时间尽量长 HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_1; // 浊度,PA1 sConfig.Rank = 2; sConfig.SamplingTime = ADC_SAMPLETIME_92CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_4; // 温度,PA4 sConfig.Rank = 3; sConfig.SamplingTime = ADC_SAMPLETIME_92CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig);这段代码里有三个参数值得注意。PCLK_DIV4决定 ADC 时钟频率,分频太小会导致转换结果不稳定;SamplingTime对 pH 这种高内阻源特别关键,采样时间短于信号源充电时间,读到的电压会偏低,而且误差随着温度变化漂移,这就是采集数据“看起来差不多、一对比又不准”的典型原因;ContinuousConvMode必须为 DISABLE,因为我们要用定时器来控制采样节奏。
3.2 定时器触发采样:把采样节奏交给硬件
用 TIM6 做触发源的好处是采样间隔极其均匀,不受中断优先级和主循环抖动影响。配置 TIM6 的更新周期为 1ms,即 1kHz 触发频率,ADC 每次转换三通道,DMA 搬运完成会产生传输完成中断,我们在中断里设置一个标志位,主循环检测到标志位就去数组里取数据。
htim6.Instance = TIM6; htim6.Init.Prescaler = 80 - 1; // 80MHz / 80 = 1MHz htim6.Init.Period = 1000 - 1; // 1MHz / 1000 = 1kHz 触发 HAL_TIM_Base_Init(&htim6); HAL_TIM_Base_Start(&htim6); // DMA 配置,循环模式搬运 3 个半字 hdma_adc1.Instance = DMA1_Channel1; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; HAL_DMA_Init(&hdma_adc1); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); uint16_t adc_buf[3]; HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 3);PeriphInc必须关闭,因为 ADC 外设的数据寄存器地址固定;MemInc必须开启,这样三次转换结果才能依次落到 adc_buf[0]、adc_buf[1]、adc_buf[2] 三个元素里。缓冲区类型是 uint16_t 数组,DMA 搬运宽度是半字,正好匹配 12 位 ADC 的结果宽度。切换到 DMA 循环模式以后,数组会被持续刷新,你在主循环里读到的永远是最新一组采样值,不用手动清除标志。如果你使用 GPIO 中断去触发采样也不是不行,但高优先级中断频繁进入会挤占通信协议处理时间,定时器方案在这类项目里更合适。
3.3 滤波:滑动平均加异常值剔除
传感器信号进入 ADC 以后,原始值往往带毛刺,直接上传会看到历史曲线上全是尖峰。我常用的组合是:先做窗口为 16 的滑动平均,再做一次简单的异常值剔除——如果某次采样值偏离当前均值超过 50%,就丢弃该次数据并连续计数,连续丢弃超过 5 次就认为传感器或信号链出了问题。这样既压掉了随机噪声,又不会因为个别干扰值拉偏整体数据。
#define FILTER_WIN 16 #define ABNORM_RATIO 0.5f typedef struct { uint16_t buf[FILTER_WIN]; uint8_t idx; uint32_t sum; } mov_avg_filter_t; uint16_t mov_avg_push(mov_avg_filter_t *f, uint16_t val) { f->sum -= f->buf[f->idx]; f->sum += val; f->buf[f->idx] = val; f->idx = (f->idx + 1) % FILTER_WIN; return (uint16_t)(f->sum / FILTER_WIN); } uint16_t water_filter_put(mov_avg_filter_t *f, uint16_t val) { uint16_t avg_val = f->sum / FILTER_WIN; if ((val > avg_val * (1 + ABNORM_RATIO)) || (val < avg_val * 0.5f)) return avg_val; // 丢弃异常点,返回上一次均值 return mov_avg_push(f, val); }滑动平均窗口大小的选择直接影响响应速度。窗口 16 在 1kHz 采样率下其实只覆盖 16ms,对水质这种慢变信号来说响应仍然很快;但如果你的采样频率很低、比如每秒一次,窗口 4~8 更合适,否则真实变化被过度平滑,pH 突变的响应时间会慢得可疑。异常值剔除阈值也不能拍脑袋,先把自己传感器的量程和噪声底摸清楚再定。
3.4 校准参数与数据帧设计
pH 探头存在个体差异,每次换探头都需要两点校准。校准得到的斜率 k 和偏移 b 不能存在代码常量里,常见做法是写到 STM32 内部 Flash 模拟 EEPROM 的扇区,或者外挂一块 W25Q16。上电时先读校准参数,没有就按默认值 1.0 和 0 初始化。实际计算时用ph_value = (raw_adc * 3.3f / 4095.0f) * k + b,这里的 k 是电压到 pH 值的折算系数。
通信数据帧是上下位机之间的契约,我建议一上来就固定版本号,避免后续各改各的。帧结构包含帧头、长度、设备 ID、消息类型、有效载荷和 CRC16 校验。有效载荷里按固定顺序放温度、pH、浊度、电池电压,各占 2 字节,用大端序。这样一帧数据正好 24 字节左右,在串口调试助手和抓包工具里一眼能看出来龙去脉。
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2B | 0xAA 0x55 |
| 长度 | 1B | 从设备 ID 到 CRC 的字节数 |
| 设备 ID | 1B | 单机固定,多机区分用 |
| 消息类型 | 1B | 0x01 实时数据,0x02 校准应答 |
| 温度 | 2B | 放大 10 倍传输 |
| pH | 2B | 放大 100 倍传输 |
| 浊度 | 2B | 整数,单位 NTU |
| 电池电压 | 2B | 单位 mV |
| CRC16 | 2B | Modbus 多项式 |
| 帧尾 | 2B | 0x0D 0x0A |
4. 数据到小程序:ESP8266 透传、MQTT 与微信登录态
4.1 用 ESP8266 做 TCP 透传的最小 AT 指令序列
STM32 直接连 WiFi 不现实,常规做法是 STM32 通过串口发 AT 指令给 ESP8266,ESP8266 负责网络连接。ESP8266 刷 AT 固件后,把工作模式设置成 Station 模式加透传模式,连接到家里的路由器和云端 MQTT broker 的 TCP 端口。这套配置指令在不同固件版本间略有差异,但下面这个序列是很多项目通用的:
AT+CWMODE=1 AT+CWJAP="your_ssid","your_password" AT+CIPSTART="TCP","broker.emqx.io",1883 AT+CIPMODE=1 AT+CIPSENDAT+CIPMODE=1开启透传模式后,ESP8266 会把串口收到的所有字节原样通过 TCP 连接发出去,不再用AT+CIPSEND一条一条发。STM32 端相当于多了一条“串口网线”,上层跑 MQTT 协议、HTTP 协议都由 STM32 自己构造报文。透传模式下退出需要用+++前缀,注意发送时前后不能有回车帧间隔,否则不生效。如果你发现连不上 broker,优先排查的是AT+CWJAP返回的错误码,比如 2 表示密码错误、3 表示找不到 AP。
4.2 MQTT 主题设计与心跳保活
设备长时间在线,TCP 连接可能被运营商或家用路由器的 NAT 超时切断,MQTT 的心跳保活机制就是应对这个的。STM32 侧每 20 秒发一个 PINGREQ 数据包,broker 如果在约定的 keepalive 时间内没有收到任何包,就会断开连接。keepalive 时间一般设 60 秒,心跳间隔设 20 秒,留足重传余量。主题设计上,发布和订阅分开,上行数据放一个主题,下行控制命令放另一个主题:
| 主题 | 权限 | 用途 |
|---|---|---|
| device/{id}/telemetry | 设备发布 | 上报水质数据 |
| device/{id}/command | 设备订阅 | 接收小程序下发的采样间隔、校准指令 |
| device/{id}/status | 设备发布 | 上报在线状态、电池电量 |
4.3 STM32 侧 MQTT 报文的轻量实现
如果不想在 STM32 里引入完整 MQTT 客户端库,可以直接构造一个最小的 PUBLISH 报文。MQTT 报文结构不算复杂,发布一条 QoS0 消息只需要固定头、可变头和有效载荷三段。这里给一个用串口透传直接发送的核心代码片段,完整报文需要把剩余长度字段按变长编码规则处理:
// 构建 CONNECT 报文,client_id 为 "dev_001" uint8_t packet[128]; uint16_t idx = 0; packet[idx++] = 0x10; // CONNECT 报文固定头 // 剩余长度:10 + 2 + len(client_id) + 2 + len(client_id) packet[idx++] = 10 + 2 + 7 + 2 + 7; packet[idx++] = 0x00; packet[idx++] = 0x04; memcpy(&packet[idx], "MQTT", 4); idx += 4; packet[idx++] = 0x04; // 协议级别 3.1.1 packet[idx++] = 0x02; // 连接标志:清理会话 packet[idx++] = 0x00; packet[idx++] = 0x3C; // keepalive 60s packet[idx++] = 0x00; packet[idx++] = 0x07; memcpy(&packet[idx], "dev_001", 7); idx += 7; HAL_UART_Transmit(&huart2, packet, idx, 100);如果你不想自己抠报文格式,移植 Eclipse Paho Embedded C 库到 STM32 也是常见做法,它只需要你提供网络读写的回调函数,正好可以套在 ESP8266 的透传串口上。注意 MQTT 3.1.1 的 CONNECT 报文在协议级别 0x04这个字段上不能写错,写错版本 broker 会直接断开连接,这种错误非常隐蔽。
4.4 微信小程序登录:用 code 换 token 的完整流程
微信小程序不能直接在手机端拿到用户身份,规范做法是wx.login()拿到临时凭证 code,发送到自己的后端,后端再用 code 去向微信接口换取 openid 和 session_key。这一步是真后端做的事,小程序本身拿不到 openid。拿到 openid 后,后端生成自己的登录态 token 返回给小程序,小程序存进 storage,后续请求头带 token 鉴权。代码上分三段:
// 小程序端:获取 code 并换取自定义登录态 wx.login({ success: async (res) => { if (res.code) { const loginRes = await new Promise((resolve, reject) => { wx.request({ url: 'https://api.example.com/v1/auth/login', method: 'POST', data: { code: res.code }, success: resolve, fail: reject }); }); wx.setStorageSync('token', loginRes.data.token); } } });这段逻辑里有三个关键点。res.code有效期只有 5 分钟,而且只能用一次,后端换完 session_key 后立即失效;session_key绝不能下发到小程序端,否则任何人都能拿着它伪造用户身份;后端需要自己维护 token 与 openid 的映射表,小程序端所有数据接口都只认 token。如果你不需要用户体系,只想让小程序能看数据,也可以不做 wx.login,直接把设备 ID 写死在页面请求参数里,代价是任何人都能换设备 ID 看数据,隐私要求高的场景不能这么干。
4.5 OneNET 平台与自建后端的取舍
如果你的 MQTT broker 不想自己搭,可以用 OneNET 这类物联网平台。OneNET 支持 MQTT 协议接入,平台侧自动解析设备上报的数据流,还自带数据可视化面板和小程序 SDK,省去自建后端。代价是设备和数据都绑在平台上,换平台要改协议,而且平台的数据流模板一旦建好,增删字段都要去后台操作。自建后端则灵活得多,可以直接在同一个进程里做用户鉴权、数据入库和设备指令下发,对毕设来说多写几百行代码,但可控性高一个档次。小程序端如果对接 OneNET,通常是用它的开放 API 拉数据,而不是直连 MQTT——小程序里跑 MQTT over WebSocket 也可以,但连接稳定性受小程序后台限制,冷启动重连逻辑很麻烦,不如请求 REST API 省心。
5. 小程序端展示:实时数据、历史曲线与点位切换
5.1 页面布局与数据绑定的最小结构
小程序端的职责是:把设备上报的数据变成一个能看、能操作的一屏界面。最典型的布局是顶部显示当前点位和连接状态,中间三个卡片分别显示温度、pH、浊度数值,底部是历史趋势曲线和功能按钮。WXML 里用数据绑定把界面和逻辑层变量串起来:
<view class="card"> <text class="label">pH 值</text> <text class="value">{{currentData.ph}}</text> </view> <view class="card" wx:for="{{sensorList}}" wx:key="id"> <text>{{item.name}}</text> <text>{{item.value}}</text> </view>5.2 实时数据拉取与下拉刷新逻辑
小程序没有常驻后台,最稳妥的方案是进入页面后开启定时器,每 5 秒调用一次后端接口拉取最新数据。5 秒这个间隔是经验值:小于 3 秒,手机电量掉得快,后端压力也大;大于 10 秒,水质突变看不出来。请求函数里带 token,失败时要区分是登录过期还是网络问题,登录过期就跳转重新登录,网络问题则保留旧数据并显示“连接中”状态。
fetchLatestData() { wx.request({ url: 'https://api.example.com/v1/device/001/latest', header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` }, success: (res) => { if (res.data.code === 0) { this.setData({ currentData: res.data.data }); this.drawCurve(res.data.history); } else if (res.data.code === 401) { this.reLogin(); } }, fail: () => { this.setData({ onlineStatus: 'offline' }); } }); }setData不能太频繁。小程序的 setData 是把数据从逻辑层传到渲染层的完整序列化过程,5 秒一次的实时更新没问题,但如果你同时更新一个很大的 history 数组,页面会明显卡顿。正确做法是历史曲线数据单独存,画图时直接用,不参与 setData 响应式绑定。
5.3 canvas 画一条能看的历史曲线
小程序原生的 canvas 组件画折线图并不复杂:先拿历史数据数组,找到最大值和最小值算出纵轴比例,再把每个点的横坐标按时间间隔均匀分布,最后描点连线。这样一个轻量自绘的方案比引入 echarts-for-weixin 轻得多,适合数据点少于 50 个的场景。
drawCurve(history) { const ctx = wx.createCanvasContext('trendCanvas'); const width = 300, height = 150; const padding = 20; const max = Math.max(...history.map(p => p.ph)) + 0.5; const min = Math.min(...history.map(p => p.ph)) - 0.5; ctx.clearRect(0, 0, width, height); ctx.setStrokeStyle('#2b7f6e'); ctx.setLineWidth(2); history.forEach((point, i) => { const x = padding + (i / (history.length - 1)) * (width - 2 * padding); const y = height - padding - ((point.ph - min) / (max - min)) * (height - 2 * padding); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.draw(); }坐标换算的核心是归一化:(value - min) / (max - min)得到 0 到 1 之间的比例,再乘以绘图区高度。注意 padding 要留足,否则最大值和最小值的点会画到画布边缘被裁掉。canvas 的draw()是异步渲染,多次调用时要在回调里做节流,否则连续刷新会出现闪烁。
5.4 多点位切换与阈值告警
如果装置有多个监测点,页面顶部用一个单选框组件切换点位。每个点位对应一组设备和一套数据接口。点位切换后要清空原有历史数据,避免新旧曲线连在一起。阈值告警做最简单的本地判断就行:小程序从后端拿到阈值配置,每次刷新比对当前值是否越界,越界就改变卡片背景色并触发wx.vibrateShort()震动提醒。阈值配置存在wx.setStorageSync的本地缓存里,避免每次进页面都请求一遍。
checkAlarm(value) { const alarms = wx.getStorageSync('alarm_threshold'); if (alarms.phMin && value.ph < alarms.phMin) { this.setData({ phAlarm: true }); wx.vibrateShort({ type: 'medium' }); } else { this.setData({ phAlarm: false }); } }6. 水下长期运行的三个细节:低功耗、ADC 校准和现场排查
6.1 STOP 模式与 RTC 定时唤醒
待机策略上,设备不能一直全速采数,通常每 15 分钟醒来工作一次。L4 系列进入 STOP 模式前要把内部 Flash 的延迟等级调低,唤醒后从 RTC 中断恢复再重新初始化时钟。这里最容易出问题的是唤醒后外设时钟没重新配置,ADC 读出来全是 0——常规做法是在进入 STOP 模式前把 DMA 和外设全部 DeInit,唤醒后重新走一遍初始化函数。
void enter_stop_mode(void) { HAL_ADC_Stop_DMA(&hadc1); HAL_TIM_Base_Stop(&htim6); HAL_UART_DeInit(&huart2); HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 900, RTC_WAKEUPCLOCK_CK_SPRE_16BITS); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); SystemClock_Config(); // 唤醒后时钟会切回 MSI,必须重新配置 MX_ADC1_Init(); MX_TIM6_Init(); HAL_TIM_Base_Start(&htim6); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 3); }RTC_WAKEUPCLOCK_CK_SPRE_16BITS的含义是唤醒定时器用 16 位计数器,时钟源 1Hz,最大延时 65535 秒,约 18 小时。900 就是 900 秒,即 15 分钟。进入 STOP 模式前务必确认串口发送已完成,否则最后一个 MQTT 包会丢在半路。
6.2 用 VREFINT 校准 ADC 参考电压
锂电池电压从 4.2V 一路降到 3.4V 的过程中,L4 的内部 ADC 参考电压 Vdda 是不稳定的,直接按 3.3V 固定值换算电压会引入几个百分点的误差。芯片出厂时在 Flash 里存放了一个 VREFINT 校准值,通过读取内部参考电压通道的 ADC 原始值,可以反推当前的 Vdda,进而修正实际电压。
const uint16_t vrefint_cal = *(uint16_t *)0x1FFF75AA; // 1.2V 对应的原始值 HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint16_t vref_sample = HAL_ADC_GetValue(&hadc1); float vdda = 1.2f * vrefint_cal / vref_sample; float real_voltage = (float)adc_buf[0] * vdda / 4095.0f;校准值地址0x1FFF75AA是 STM32L4 系列存放 VREFINT_CAL 的固定位置,F1 系列没有这个寄存器,地址也不通用。你项目的启动文件里如果已经包含了校准变量,可以直接引用,不必写死地址。实测下来,做不做这步校准,pH 计算值在满电和低电量时能差 0.3 以上,对于要求 0.1 精度的水质测量来说不可接受。
6.3 现场容易踩的坑
| 现象 | 根因 | 处理 |
|---|---|---|
| 小程序显示数据不更新 | ESP8266 断网后未重连 | 在透传模块上加看门狗,心跳失败自动重连 |
| pH 值漂移严重 | 传感器探头被气泡附着 | 安装时倾斜探头,确保水流冲刷 |
| 电池一两天就没电 | 传感器供电引脚未真正关闭 | 检查是否用 GPIO 低电平拉断供电,测量实际关闭电流 |
| 小程序请求 401 | 登录 token 过期 | 后端把 token 有效期设长,前端加静默续期逻辑 |
6.4 下水前的最后一道工序:信号源联调
整机密封前,先用信号发生器模拟传感器输出,把 0.5V、1.0V、1.5V、2.0V 四组电压分别输入到 ADC 对应引脚,检查小程序端显示的换算值和理论值是否一致。这一操作能把信号链路上所有问题都隔离在密封舱之外——如果模拟电压下数据通、接上真实传感器就不通,说明问题在探头或调理电路;如果模拟电压下数据都不通,直接查代码和通信链路,省去在岸边反复拆装的折腾。校正时多准备一组已知 pH 值的标准缓冲液,7.0 和 4.0 各配一瓶,先把传感器校准曲线拉平再下水,数据才经得起推敲。
本文还有配套的精品资源,点击获取