news 2026/9/19 5:15:14

STM32智能鱼缸:本地闭环控制与微信小程序物联网设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32智能鱼缸:本地闭环控制与微信小程序物联网设计

简介:这是一份面向嵌入式物联网学习者及课程设计、毕业设计需求者的项目文档,围绕基于STM32的智能鱼缸系统与配套微信小程序展开。资源以单个PDF交付,压缩包约42.7MB,正文系统梳理了以STM32F103RCT6为主控的硬件方案,涵盖DS18B20防水温度传感器、MQ137氨气传感器、光敏电阻、水质浑浊度传感器、ESP8266 WiFi模块与继电器的作用及选型依据。内容覆盖水质异常红灯提示换水、水温超限自动加热、光强低于阈值自动补光、氨气检测保障硝化环境、增氧泵定时启停等功能需求,并说明ESP8266以STA模式经MQTT接入腾讯云IoT平台上传数据、由小程序远程设置阈值与切换控制模式的实现思路。读者可据此获得从需求分析、硬件连接调试到软件编写联调的完整参考,适合作为物联网入门与课设、毕设的技术蓝本。已有146人学习。

1. 一套智能鱼缸真正的分水岭,在于本地控制闭环放在哪一侧

很多人做智能鱼缸,第一反应是买几个带 WiFi 的智能插座,把加热棒、水泵、补光灯分别挂上去,再用定时任务排班。这套方案能跑,但只要你养过一缸七彩神仙或者水晶虾,就知道它靠不住:插座只认时间不认水温,加热棒在室温 30℃ 的夏天照样按冬天的时间表工作,一次疏忽就是团灭。

把控制权交给 STM32 才是分水岭。板子本地读水温、水位、TDS,自己判定该不该通电,网络只负责把状态送上微信小程序、把阈值和人手下发的指令收回来。断网时鱼缸照常运行,联网时你在公司也能看到 25.4℃ 这个数字在跳。这也是近几年物联网毕业设计里,基于 STM32 的鱼缸类题目经久不衰的原因——它逼着你把 GPIO 操作、单总线时序、ADC 采样、定时器调度和一条完整的上下行链路全部串一遍,而不是调个现成 SDK 就交差。

这篇内容面向想把「STM32 + 物联网 + 微信小程序」这条链路真正打通的人,从外设分配讲到温控回差,再讲到小程序页面退出时该不该断开连接。

2. 智能鱼缸的外设清单与 STM32 资源分配表

2.1 先确定哪些量必须采、哪些设备必须控

做硬件盘点时,最忌讳一上来就堆传感器。先问三个问题:这个量失控会不会死鱼、这个量用户愿不愿意每天看、这个量采回来有没有对应的执行动作。按这个标准筛,必采项其实只有四类。

水温是第一优先级,因为它同时决定加热和风扇两条执行路径。水位是第二优先级,低水位必须立刻停加热棒,否则干烧是火灾级别的风险,这条保护逻辑必须在 STM32 里硬编码,不能依赖云端下发。TDS(溶解性总固体)用来判断换水时机,属于「看一眼就够了」的量,采样频率可以压到几分钟一次。光照和 pH 视养的品种决定要不要加,草缸配 pH,普通观赏鱼用 TDS 加光照基本够用。

执行侧对应四路:加热棒(继电器)、循环水泵(继电器,常开)、补光灯(继电器或 PWM 调光)、自动投喂(SG90 舵机)。如果按智能生态鱼缸管理系统设计来规划,还要预留一路备用继电器给 UV 杀菌灯或者二氧化碳电磁阀。这个清单决定了后面 GPIO 要分几组、哪几路必须光耦隔离。

2.2 GPIO 与定时器的分配原则

最常见的坑是把所有继电器直接挂在 GPIO 上。STM32 单个 IO 的灌电流/拉电流上限通常在 20mA 量级,而一路 5V 继电器模块的线圈驱动电流往往在 60~80mA,直接驱动要么带不动,要么长期工作把 IO 烧掉。稳妥做法是继电器模块用独立 5V 供电,GPIO 只走光耦输入端,串联 1kΩ 限流电阻,逻辑电平取反也在这里处理好。

下表的分配方案基于 STM32F103C8T6 这类资源,够用且留了余量。

功能外设/引脚模式备注
DS18B20 水温PC13开漏输出/浮空输入切换需 4.7kΩ 上拉到 3.3V
水位(非接触式)PA0ADC1_IN0模拟量输出型传感器
TDS 探头PA1ADC1_IN1必须交流激励,见 3.2
加热棒继电器PB0推挽输出 + 光耦低电平有效
水泵继电器PB1推挽输出 + 光耦常开,掉电保持运行
补光灯PB5TIM3_CH2 PWM可做日出日落渐变
投喂舵机PA6TIM3_CH1 PWM50Hz,0.5~2.5ms
WiFi 模组PA9/PA10USART1115200,8N1
调试串口PA2/PA3USART2打印日志用

这张表里最容易忽略的是 USART2。调试串口看着多余,实际上它是排错时唯一能救命的东西——继电器一吸合就复位、DS18B20 偶尔读回 85℃,这些现象没有日志只能靠猜。给调试口留一路,用 USB-TTL 接电脑,比反复插拔烧录器省事得多。

2.3 GPIO 初始化里几个容易写错的地方

CubeMX 生成的初始化代码通常没问题,但手写的时候有两处高频错误。一是把继电器引脚设成开漏输出,结果上电后继电器一直半吸合、发热严重;二是忘了先把引脚电平写成「断开」状态再配置为输出,上电瞬间继电器「啪」地吸一下,加热棒在没水的情况下通电几百毫秒。

/* relay.c —— 上电先写安全电平,再配成推挽输出 */ void relay_init(void) { /* 1. 先把引脚拉到非有效电平,避免配置瞬间误动作 */ HAL_GPIO_WritePin(GPIOB, RELAY_HEAT_PIN | RELAY_PUMP_PIN, GPIO_PIN_SET); GPIO_InitTypeDef g = {0}; g.Pin = RELAY_HEAT_PIN | RELAY_PUMP_PIN | RELAY_LIGHT_PIN; g.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽,不要开漏 */ g.Pull = GPIO_NOPULL; g.Speed = GPIO_SPEED_FREQ_LOW; /* 继电器不需要翻转速度 */ HAL_GPIO_Init(GPIOB, &g); HAL_GPIO_WritePin(GPIOB, RELAY_HEAT_PIN, GPIO_PIN_SET); /* 加热默认关 */ }

参数上只有两个值得推敲:Speed设成 LOW 能明显降低 IO 翻转带来的电磁干扰,继电器这种秒级动作的外设完全没有理由用 HIGH;Pull保持 NOPULL,因为光耦输入端有自己的下拉,再开内部上下拉反而会拉偏逻辑电平。至于 Keil5 里写这套代码,记得通过 Pack Installer 装好 STM32F1 系列的 Device Family Pack,否则stm32f1xx_hal.h会报找不到定义——这类开发环境的问题在 stm32 项目里占掉的时间,往往比写业务代码还多。

3. STM32 端采集与控制的最小可用代码

3.1 单总线水温的时序容错

DS18B20 的时序对延时精度敏感,用HAL_Delay那种毫秒级函数是写不出来的。常见做法是用 DWT 周期计数器做微秒延时,它不受中断影响,比空循环for(i=0;i<n;i++)稳定得多。

/* bsp_ds18b20.c —— 微秒延时基于 DWT->CYCCNT */ static void ow_delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000U); while ((DWT->CYCCNT - start) < ticks) { } } /* 复位并检测存在脉冲:返回 0 表示器件在线 */ uint8_t ds_reset(void) { uint8_t ack; ow_set_output(); HAL_GPIO_WritePin(OW_PORT, OW_PIN, GPIO_PIN_RESET); ow_delay_us(480); /* 拉低 >480us */ ow_set_input(); /* 释放,靠 4.7k 上拉回高 */ ow_delay_us(70); /* 15~60us 内器件拉低应答 */ ack = HAL_GPIO_ReadPin(OW_PORT, OW_PIN); ow_delay_us(410); /* 补满整个 480us 时隙 */ return ack; }

关键点在于释放总线后必须切回输入模式,让外部上拉电阻把电平拉高。如果一直保持推挽输出高电平,器件拉低应答时会出现短暂的电源对地短路,读回来的永远是 0。读回ack == 0才算成功,一旦连续三次失败就该把该路传感器标记为离线,而不是继续拿旧数据去控温。

读回值等于 85.0℃ 是另一个经典现象:这是 DS18B20 上电后的默认寄存器值,说明温度转换还没完成就被读走了。解决办法是发起转换后至少等 750ms(12 位分辨率),或者轮询总线直到读到非 85 的值再采信。

3.2 水位与 TDS 的 ADC 采样和滑动滤波

水位用非接触式传感器比电极式省心得多。电极式方案靠水的导电性检测,长期通直流会在电极上发生电解,几天就长满绿色藻类附着物,读数一路漂移。如果非要用电极,至少用两个 GPIO 交替极性驱动,让电极上平均电压为零。

ADC 采样本身不难,难的是让读数不跳。开启 ADC1 的连续转换加 DMA 搬运是最省 CPU 的方式,回调里再叠一层滑动均值滤波。

/* adc_filter.c —— 16 点滑动均值,等效降低约 2 位噪声 */ #define WIN 16 static uint16_t win[WIN]; static uint8_t idx; static uint32_t sum; uint16_t adc_filter(uint16_t raw) { sum -= win[idx]; /* 减去最老的一个 */ win[idx] = raw; /* 写入最新的 */ sum += raw; idx = (idx + 1) & (WIN - 1); /* WIN 必须是 2 的幂,用位与代替取模 */ return (uint16_t)(sum / WIN); }

窗口长度取 16 是个折中:再长会拖慢对真实变化的响应,比如补水瞬间水位抬升,窗口太长会明显滞后;再短则对水泵启停引起的纹波压不住。采样周期上,水位 200ms 一次、TDS 几秒一次就够,TDS 的两个电极必须用交流方波激励,否则直流极化会让读数单调下降,最终稳定在一个永远不变的数字上——看起来像坏了,其实是电化学现象。

3.3 用定时器做无阻塞调度与投喂

stm32 定时器的价值在鱼缸项目里体现得很直接:把周期性任务挂在 1ms 节拍上,主循环永远不阻塞。用HAL_Delay串起来的代码在喂食时会卡住 800ms,这期间水温采样和超温保护全部停摆,属于典型的自找麻烦。

/* scheduler.c —— TIM4 配成 1kHz 中断,提供全局毫秒节拍 */ volatile uint32_t tick_ms; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM4) tick_ms++; } /* 宏式软调度:静态变量记录上次执行时间,天然支持多任务 */ #define TASK(fn, period) \ do { \ static uint32_t _last; \ if (tick_ms - _last >= (period)) { \ _last = tick_ms; \ fn(); \ } \ } while (0) void app_loop(void) { TASK(task_sample_temp, 1000); /* 水温 1s 一次 */ TASK(task_sample_level, 200); /* 水位要快,关系到干烧保护 */ TASK(task_heat_ctrl, 2000); /* 温控判定,2s 足够 */ TASK(task_feed_check, 10000); /* 10s 比对一次投喂日历 */ TASK(task_report_up, 5000); /* 5s 上行一帧 */ }

温控的核心是回差,也就是滞环控制。设单点阈值会导致加热棒在临界温度上来回吸合,一天几百次,继电器触点很快烧黑。正确写法是两个阈值加一个状态标志。

/* heat_ctrl.c —— 回差控制,内置水位联动保护 */ void task_heat_ctrl(void) { float t = g_temp_c; if (g_level_low) { /* 最高优先级:低水位断电 */ relay_off(RELAY_HEAT); g_heat_on = 0; return; } if (isnan(t) || g_temp_offline) return; /* 探头失效时保持现状,不乱动 */ if (t < g_cfg.temp_low && !g_heat_on) { relay_on(RELAY_HEAT); g_heat_on = 1; } else if (t > g_cfg.temp_high && g_heat_on) { relay_off(RELAY_HEAT); g_heat_on = 0; } }

回差建议取 1.5~2℃,比如目标 26℃,设temp_low = 25.0temp_high = 27.0,小型鱼缸这个区间波动完全可接受,而继电器动作频率能降到每小时两三次以内。投喂舵机用 TIM3 的 PWM,50Hz 对应 20ms 周期,比较值 500 对应 0.5ms(挡板关闭)、1500 对应 1.5ms(转出饲料)。投喂是低频动作,用HAL_Delay阻塞几百毫秒可以接受,但要确认这期间不看门狗超时——如果开了 IWDG 且溢出时间设成 200ms,喂食动作会直接把板子复位,这个组合坑过不少人。

4. 从 ESP8266 到微信小程序的上下行协议

4.1 设备侧用 AT 指令接 MQTT 的最小流程

WiFi 模组常见做法是用 ESP8266 跑 AT 固件,串口接 USART1。需要确认固件版本在 v1.2.0 以上,否则不包含 MQTT 指令集,只能自己实现 TCP 之上的报文拼装。连接流程可以先用串口助手手动跑一遍,确认每一步的回显,再写进代码。

AT+CWMODE=1 # 单 Station 模式 AT+CWJAP="your_ssid","your_pass" # 连路由,回显 WIFI GOT IP 才算成功 AT+CIPMUX=0 # 单连接模式,MQTT 必须 AT+MQTTUSERCFG=0,1,"fish001","user","pass",0,0,"" AT+MQTTCONN=0,"broker.example.com",1883,1 # 1 表示 clean session # 上行一帧遥测数据,QoS 0,不 retain AT+MQTTPUB=0,"fish/001/telemetry",'{"t":25.4,"lv":82,"tds":310,"heat":1}',0,0

每一条指令后面都要给足响应等待时间,AT+CWJAP在信号差的环境下可能要 8 秒以上,固定延时 2 秒就发下一条必然失败。稳妥写法是发指令后循环读取串口,匹配OK或者ERROR,超时 15 秒判失败并重试三次。另外,AT+MQTTUSERCFG里的用户名密码如果 broker 允许匿名,留空字符串即可,但很多云平台强制要求设备三元组,这部分要在配置文件里单独留出来,别硬编码进源文件。

4.2 主题与 JSON 报文设计

主题结构建议一次定死,后面改起来涉及小程序和服务端两处同步。设备 ID 放在主题层级里,比塞在 payload 里更方便做权限隔离。

主题方向载荷示例说明
fish/{id}/telemetry设备 → 云端{"t":25.4,"lv":82,"tds":310,"heat":1}5s 一帧,字段名尽量短
fish/{id}/cmd云端 → 设备{"mode":"manual","heat":1}用户手动操作
fish/{id}/cfg云端 → 设备{"temp_low":25.0,"temp_high":27.0}阈值下发后写入 Flash
fish/{id}/status设备 → 云端{"online":1,"rssi":-62}配合 LWT 遗嘱判断掉线

字段名用t/lv/tds这种短名不是偷懒。ESP8266 的 AT 固件对单条指令长度有上限,一帧报文超过 200 字节就可能被截断,而 5 秒一帧乘以一天 17280 帧,报文长度直接影响流量。至于遗嘱消息,在AT+MQTTCONN之前用AT+MQTTWILL设置,把fish/{id}/status的 payload 设成{"online":0},设备异常断电时 broker 会替你发布,小程序端就能立刻显示离线,而不是傻等超时。

4.3 小程序端的连接、单选框与导航栏处理

小程序侧有两条路。一条是设备数据先进自建服务端,小程序用wx.request轮询或走 WebSocket,好处是能加鉴权和历史数据;另一条是小程序直接连 MQTT broker,开发快但要把 broker 的 wss 端口暴露出来,且必须用去掉了 Buffer 依赖的 mqtt.js 构建版本,否则小程序编译阶段就报错。

// pages/tank/tank.js const mqtt = require('../../libs/mqtt.min.js'); // 小程序专用构建,不含 Buffer Page({ data: { temp: '--', level: '--', mode: 'auto', navH: 0 }, onLoad() { const info = wx.getWindowInfo(); // 自定义顶部导航栏高度 = 状态栏高度 + 44px 标题栏 this.setData({ navH: info.statusBarHeight + 44 }); this.client = mqtt.connect('wxs://broker.example.com:8084/mqtt', { clientId: 'wx_' + Date.now(), // 每次进页面用新 clientId,避免会话抢占 username: 'mini', password: this.data.token, clean: true, reconnectPeriod: 3000 }); this.client.on('connect', () => this.client.subscribe('fish/001/telemetry')); this.client.on('message', (topic, payload) => { const d = JSON.parse(payload.toString()); this.setData({ temp: d.t.toFixed(1), level: d.lv }); }); }, // 自动 / 手动模式切换,用单选框承载 onModeChange(e) { const mode = e.detail.value; this.setData({ mode }); this.client.publish('fish/001/cmd', JSON.stringify({ mode, ts: Date.now() })); }, onUnload() { this.client && this.client.end(true); // 页面销毁必须断开,否则后台持续重连 } });

对应页面的单选框组,注意checked要绑定数据而不是写死,否则切换后回显不对:

<radio-group bindchange="onModeChange"> <label><radio value="auto" checked="{{mode === 'auto'}}" />自动</label> <label><radio value="manual" checked="{{mode === 'manual'}}" />手动</label> </radio-group>

clientId每次进页面重新生成这一点值得强调。如果固定写死一个 ID,用户从后台切回小程序时新旧会话会互相顶掉,表现为数据时有时无、命令发不出去。onUnload里的end(true)同理,缺少这一句,用户退出页面后连接仍在重连,手机上耗电和流量都很明显。如果项目是用 uniapp 写的,用 HBuilderX 的「发行 → 小程序-微信」把编译产物输出到dist/dev/mp-weixin,再用微信开发者工具打开那个目录即可,AppID 需要在 manifest 里先填好。

5. 联调阶段的排错顺序与几个硬技巧

联调最容易乱的地方是同时改三端。合理的顺序是先单机、再联网、最后小程序:STM32 脱离 WiFi 单独跑够 24 小时,确认串口日志里温控动作和实际温度对得上;再接上模组,用串口助手看上行帧有没有按时发出;最后才打开小程序。跳过第一步的代价是,出问题时你根本不知道是采样错了、网络断了还是小程序解析错了。

按现象排查的对照表如下。

现象高概率原因处理方式
继电器吸合瞬间 MCU 复位线圈反电动势干扰电源,共地走线过长线圈并联续流二极管,继电器独立 5V 供电,光耦隔离
水温恒读 85.0℃转换未完成就读取转换后延时 750ms,或读到 85 时丢弃重采
TDS 数值缓慢下降后不动电极直流极化改为交流方波激励,正负交替驱动
MQTT 每几分钟断一次心跳超时或路由踢连接keepalive 设 60s,AT+MQTTCONN加自动重连逻辑
小程序数据刷新但命令无效订阅主题与发布主题写反核对cmd是云到设备,telemetry是设备到云
投喂时间到了不动舵机供电不足被拉低舵机单独供电,PWM 信号地与 MCU 共地

两个不在表里但很实用的技巧。第一个是给 WiFi 连接加状态机而不是写成一串if:把「未初始化 / 已连路由 / 已连 broker / 断开重连中」四个状态显式建模,每个状态超时后只做一件事——回到上一个稳定状态。这样即使路由器重启,设备也能在几十秒内自愈,而不是卡在一个半连接状态里永远出不来。第二个是上行帧里塞一个递增的序号字段,服务端发现序号倒退就说明设备重启过,能立刻区分「设备死机重启」和「网络抖动丢包」,这两种情况的处理方式完全不同。

加热棒这类大功率负载还有个容易被忽视的细节:每次上电时先读一次水位和温度,确认水位正常、温度探头在线,再允许加热继电器动作,并且给一个 10 秒的启动延时,避开刚通电时传感器读数不稳的窗口。这条逻辑写进task_heat_ctrl之前的前置检查里,比事后加报警有用得多——鱼缸这种东西,一次深夜的干烧就够重新买一缸鱼了。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 5:15:57

PHP与Python:Web开发语言对比与技术选型指南

1. 语言背景与定位差异PHP和Python作为两种主流的服务器端编程语言&#xff0c;各自有着截然不同的发展轨迹和应用场景。PHP最初由Rasmus Lerdorf于1994年创建&#xff0c;设计初衷是为了管理个人主页&#xff08;Personal Home Page&#xff09;&#xff0c;后来逐渐演变成专业…

作者头像 李华
网站建设 2026/9/19 5:24:17

桌面端 Coding Agent:多模型、Subagent 与 MCP 配置实战

1. 从 Claude Code 说起&#xff1a;为什么桌面端 Coding Agent 是个真需求Claude Code 火了一整年&#xff0c;命令行里敲claude然后看着它读文件、改代码、跑测试&#xff0c;确实爽。但用久了你会发现一个问题&#xff1a;它是个 CLI 工具&#xff0c;本质上是"寄生&qu…

作者头像 李华
网站建设 2026/9/19 5:08:44

三菱FX3U与PC的RS485通信实战:FX3U-485-BD接线和D8120设置

简介&#xff1a;面向PLC控制与上位机通信开发人员的实战文档&#xff0c;围绕FX3U-128MT与主站PC间的无协议RS485通信展开&#xff0c;适用于需要远程采集PLC报警信息、且传输距离超过RS232限值的自动化项目。文档完整记录了硬件选型与接线过程&#xff0c;包括MOXA四通道PCI-…

作者头像 李华
网站建设 2026/9/19 5:23:59

自研浏览器插件实战:从需求梳理到上线维护的完整指南

“自研插件”这四个字&#xff0c;在圈子里的分量其实挺微妙的。很多人一听“自己写插件”&#xff0c;第一反应是“大佬又在造轮子”&#xff0c;但以我自己折腾这几年插件的实际感受来说&#xff0c;大部分自研插件既不是为了秀技术&#xff0c;也不是为了替代那些成熟的开源…

作者头像 李华
网站建设 2026/9/19 5:15:26

数据库三范式实战:从1NF到3NF的表结构设计权衡

做数据库相关工作&#xff0c;范式这个概念基本绕不开。面试被问、课程设计被考、设计表结构时被同事甩一句“你这表不符合3NF”&#xff0c;都是常有的事。如果你去搜“三范式”或者“3NF”&#xff0c;能搜出一堆教科书定义&#xff0c;什么“每一列不可再分”“非主属性完全…

作者头像 李华
网站建设 2026/9/19 5:16:47

2026企业选型指南:主流BI产品哪家好?全方位对比评测

2026年&#xff0c;企业数据环境正在经历深刻变化。IDC数据显示&#xff0c;2025年中国BI软件市场规模达13.2亿美元&#xff0c;连续6年保持25%以上同比增速。对于企业客服、市场、售后等部门而言&#xff0c;数据已不再是IT部门的专属领地——客服团队需要实时洞察客户投诉趋势…

作者头像 李华