ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32水质检测实战:从传感器采集到微信小程序显示的全链路设计

STM32水质检测实战:从传感器采集到微信小程序显示的全链路设计 简介这份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 / GPIOpH高内阻电压毫伏级高输入阻抗运放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高低鱼塘、实验室、近岸LoRa1~10km低高需网关水库、河道断面蓝牙 BLE10m低低现场调试、水族箱4G 模组不限高中偏远站点、移动巡查我一般建议毕设和快速原型优先走 WiFi 方案先把整条数据链路跑通再考虑 LoRa。因为 MQTT 协议、JSON 透传、小程序对接这些都和底层无线方式无关换 LoRa 只是把 ESP8266 换成串口转 LoRa 模块上层代码几乎不用动。2.4 供电与采样间隔的估算方法水下装置的供电大部分用锂电池组容量按“采样间隔”来估算比按“连续工作”现实得多。一套 L431 多路传感器 ESP8266 的典型功耗约 300mA5V如果每 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 字节左右在串口调试助手和抓包工具里一眼能看出来龙去脉。字段长度说明帧头2B0xAA 0x55长度1B从设备 ID 到 CRC 的字节数设备 ID1B单机固定多机区分用消息类型1B0x01 实时数据0x02 校准应答温度2B放大 10 倍传输pH2B放大 100 倍传输浊度2B整数单位 NTU电池电压2B单位 mVCRC162BModbus 多项式帧尾2B0x0D 0x0A4. 数据到小程序ESP8266 透传、MQTT 与微信登录态4.1 用 ESP8266 做 TCP 透传的最小 AT 指令序列STM32 直接连 WiFi 不现实常规做法是 STM32 通过串口发 AT 指令给 ESP8266ESP8266 负责网络连接。ESP8266 刷 AT 固件后把工作模式设置成 Station 模式加透传模式连接到家里的路由器和云端 MQTT broker 的 TCP 端口。这套配置指令在不同固件版本间略有差异但下面这个序列是很多项目通用的ATCWMODE1 ATCWJAPyour_ssid,your_password ATCIPSTARTTCP,broker.emqx.io,1883 ATCIPMODE1 ATCIPSENDATCIPMODE1开启透传模式后ESP8266 会把串口收到的所有字节原样通过 TCP 连接发出去不再用ATCIPSEND一条一条发。STM32 端相当于多了一条“串口网线”上层跑 MQTT 协议、HTTP 协议都由 STM32 自己构造报文。透传模式下退出需要用前缀注意发送时前后不能有回车帧间隔否则不生效。如果你发现连不上 broker优先排查的是ATCWJAP返回的错误码比如 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 classcard text classlabelpH 值/text text classvalue{{currentData.ph}}/text /view view classcard wx:for{{sensorList}} wx:keyid text{{item.name}}/text text{{item.value}}/text /view5.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 各配一瓶先把传感器校准曲线拉平再下水数据才经得起推敲。本文还有配套的精品资源点击获取
返回列表