ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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 PWM50Hz0.5~2.5msWiFi 模组PA9/PA10USART11152008N1调试串口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 翻转带来的电磁干扰继电器这种秒级动作的外设完全没有理由用 HIGHPull保持 NOPULL因为光耦输入端有自己的下拉再开内部上下拉反而会拉偏逻辑电平。至于 Keil5 里写这套代码记得通过 Pack Installer 装好 STM32F1 系列的 Device Family Pack否则stm32f1xx_hal.h会报找不到定义——这类开发环境的问题在 stm32 项目里占掉的时间往往比写业务代码还多。3. STM32 端采集与控制的最小可用代码3.1 单总线水温的时序容错DS18B20 的时序对延时精度敏感用HAL_Delay那种毫秒级函数是写不出来的。常见做法是用 DWT 周期计数器做微秒延时它不受中断影响比空循环for(i0;in;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 上电后的默认寄存器值说明温度转换还没完成就被读走了。解决办法是发起转换后至少等 750ms12 位分辨率或者轮询总线直到读到非 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.0、temp_high 27.0小型鱼缸这个区间波动完全可接受而继电器动作频率能降到每小时两三次以内。投喂舵机用 TIM3 的 PWM50Hz 对应 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 之上的报文拼装。连接流程可以先用串口助手手动跑一遍确认每一步的回显再写进代码。ATCWMODE1 # 单 Station 模式 ATCWJAPyour_ssid,your_pass # 连路由回显 WIFI GOT IP 才算成功 ATCIPMUX0 # 单连接模式MQTT 必须 ATMQTTUSERCFG0,1,fish001,user,pass,0,0, ATMQTTCONN0,broker.example.com,1883,1 # 1 表示 clean session # 上行一帧遥测数据QoS 0不 retain ATMQTTPUB0,fish/001/telemetry,{t:25.4,lv:82,tds:310,heat:1},0,0每一条指令后面都要给足响应等待时间ATCWJAP在信号差的环境下可能要 8 秒以上固定延时 2 秒就发下一条必然失败。稳妥写法是发指令后循环读取串口匹配OK或者ERROR超时 15 秒判失败并重试三次。另外ATMQTTUSERCFG里的用户名密码如果 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}阈值下发后写入 Flashfish/{id}/status设备 → 云端{online:1,rssi:-62}配合 LWT 遗嘱判断掉线字段名用t/lv/tds这种短名不是偷懒。ESP8266 的 AT 固件对单条指令长度有上限一帧报文超过 200 字节就可能被截断而 5 秒一帧乘以一天 17280 帧报文长度直接影响流量。至于遗嘱消息在ATMQTTCONN之前用ATMQTTWILL设置把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 bindchangeonModeChange labelradio valueauto checked{{mode auto}} /自动/label labelradio valuemanual checked{{mode manual}} /手动/label /radio-groupclientId每次进页面重新生成这一点值得强调。如果固定写死一个 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 设 60sATMQTTCONN加自动重连逻辑小程序数据刷新但命令无效订阅主题与发布主题写反核对cmd是云到设备telemetry是设备到云投喂时间到了不动舵机供电不足被拉低舵机单独供电PWM 信号地与 MCU 共地两个不在表里但很实用的技巧。第一个是给 WiFi 连接加状态机而不是写成一串if把「未初始化 / 已连路由 / 已连 broker / 断开重连中」四个状态显式建模每个状态超时后只做一件事——回到上一个稳定状态。这样即使路由器重启设备也能在几十秒内自愈而不是卡在一个半连接状态里永远出不来。第二个是上行帧里塞一个递增的序号字段服务端发现序号倒退就说明设备重启过能立刻区分「设备死机重启」和「网络抖动丢包」这两种情况的处理方式完全不同。加热棒这类大功率负载还有个容易被忽视的细节每次上电时先读一次水位和温度确认水位正常、温度探头在线再允许加热继电器动作并且给一个 10 秒的启动延时避开刚通电时传感器读数不稳的窗口。这条逻辑写进task_heat_ctrl之前的前置检查里比事后加报警有用得多——鱼缸这种东西一次深夜的干烧就够重新买一缸鱼了。本文还有配套的精品资源点击获取
返回列表