尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于STM32与ESP8266的智能鱼缸环境监测与远程控制

基于STM32与ESP8266的智能鱼缸环境监测与远程控制 简介面向嵌入式与物联网方向的学习者与毕业设计开发者这份 PDF 资料围绕基于 STM32 的智能鱼缸系统展开重点解决鱼缸环境参数实时监测、自动调节与远程控制问题。系统以 STM32F103RCT6 为主控配合 DS18B20 防水温度传感器、MQ137 氨气传感器、光敏电阻与水质浑浊度传感器并通过 ESP8266 接入腾讯云 IoT 平台借助微信小程序完成水质、水温、光强等数据查看及增氧泵、加热棒、灯光等远程设置。资料内含项目功能、设计需求、硬件模块组成、整体设计思路及 ESP8266 工作模式配置等完整说明可帮助读者理解传感器采集、继电器控制、云端通信与小程序交互的协同逻辑适合课程设计、竞赛项目或方案复现时参考。文件共 1 个 PDF压缩包约 42.7MB已有 146 人学习下载。1. 鱼缸出事的那几分钟往往没人守在旁边凌晨两点加热棒接触不良水温从 26℃ 掉到 21℃早上起来鱼全趴缸出差三天回家水面一层油膜氨氮已经爆表或者只是阴天忘了开灯水草一周就黄了。这些场景是很多人动手做智能鱼缸的直接动机因为鱼缸的变量其实不多但需要的是持续在场。这套基于 STM32F103RCT6 的鱼缸控制器把这几个变量拆成了四路采集DS18B20 测水温、MQ137 测氨气、TDS 类水质传感器测浑浊度、光照传感器测环境亮度。本地完成阈值判断和继电器动作再通过 ESP8266 把数据推到腾讯云 IoT微信小程序负责远程看数和改参数。它适合两类人一类是电子或物联网方向的在校生想找一个把 ADC 采样、单总线时序、I2C、UART、MQTT 全串起来的完整练手项目另一类是养鱼又想折腾的工程师想要一套阈值能自己改、探头能自己加、数据不进别人服务器的私有方案。硬件成本不高真正的难点不在焊接而在传感器标定和控制逻辑的稳定性。2. 四路环境量采集DS18B20、MQ137、TDS 与光照的接口设计2.1 引脚分配与总线类型选择STM32F103RCT6 是 64 引脚封装51 个通用 IO外设够用但引脚一旦定下来再改就要重新排版。我的习惯是先把总线类型分清再分配引脚单总线的挂一个 IO模拟量的进 ADC 通道I2C 的走硬件 I2C串口的留给 ESP8266剩下的才给继电器和指示灯。模块接口类型推荐引脚供电输出形式DS18B20 防水探头单总线PA03.3V数字12 位温度MQ137 氨气传感器模拟PA1 / ADC1_IN15V模拟电压随浓度变化TDS / 浑浊度传感器模拟PA2 / ADC1_IN25V模拟电压02.3V 量级BH1750 或光敏电阻I2C / 模拟PB6、PB7 或 PA33.3V数字 lux 或电阻分压ESP8266UARTPA9 / PA10USART13.3VAT 或 MQTT 透传增氧泵 / 加热棒 / 补光灯GPIOPB0、PB1、PB25V 驱动高低电平控制继电器这里有两个容易踩的坑。第一MQ137 和 TDS 模块标称 5V 供电输出模拟电压可能超过 3.3V直接进 ADC 会打坏引脚我的做法是在输出和 ADC 之间串一个 10k 加 20k 的分压软件里再乘回 1.5 倍。第二MQ137 的加热丝耗电接近 150mA不能让 STM32 板上的 LDO 供最好单独一路 5V否则 Wi-Fi 一发射温度读数就飘。2.2 DS18B20 单总线时序与防水探头读取DS18B20 用在鱼缸里最大的好处是探头可以长期泡水输出的是数字量不需要外部 ADC。代价是它对时序敏感尤其是 1-Wire 的复位脉冲和读写时隙延时不准就读回 85.0℃ 这个经典错误值。/* ds18b20.c —— 单总线时序PA0微秒级延时由 DWT 或 TIM 提供 */ #define DQ_OUT() { GPIOA-CRL (GPIOA-CRL ~(0xF 0)) | (0x3 0); } /* PA0 推挽输出 */ #define DQ_IN() { GPIOA-CRL (GPIOA-CRL ~(0xF 0)) | (0x4 0); } /* PA0 浮空输入 */ #define DQ_H() GPIO_SetBits(GPIOA, GPIO_Pin_0) #define DQ_L() GPIO_ResetBits(GPIOA, GPIO_Pin_0) #define DQ_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) uint8_t ds18b20_reset(void) { uint8_t ack; DQ_OUT(); DQ_L(); delay_us(500); /* 拉低 480us 产生复位脉冲 */ DQ_H(); delay_us(30); /* 主机释放总线并等待 15~60us */ DQ_IN(); ack DQ_READ(); /* 从机在 60~240us 内拉低读到 0 表示存在 */ delay_us(420); return ack; /* 0 在线1 无器件 */ } int16_t ds18b20_read_temp(void) { uint8_t tl, th; if (ds18b20_reset()) return -1000; /* 探头掉了返回异常码 */ ds18b20_write_byte(0xCC); /* 跳过 ROM单探头场景够用 */ ds18b20_write_byte(0x44); /* 启动一次温度转换 */ delay_ms(750); /* 12 位分辨率最长 750ms */ ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); /* 读暂存器 */ tl ds18b20_read_byte(); th ds18b20_read_byte(); return (int16_t)((th 8) | tl); /* 原始值实际温度 值 / 16.0 */ }逻辑上分三步复位确认器件在线、启动转换、等待足够时间后读回两字节暂存器。参数上最关键的是 750ms 的转换等待如果为了省时间改成 9 位分辨率等待可以缩到 94ms但分辨率掉到 0.5℃对鱼缸来说完全够用我一般会把分辨率配置成 11 位0.125℃等 375ms兼顾速度和精度。返回-1000这种异常码比返回 0 更有用上层一眼就知道是探头掉了而不是水温真的是 0℃。2.3 MQ137 与 TDS 的 ADC 采样与滑动中值滤波这两路都是模拟量STM32F103 的 ADC 是 12 位、参考电压 3.3V理论分辨率 0.8mV。听起来很精细但实际用起来会发现原始值抖得厉害尤其 ESP8266 发射的瞬间电源纹波会让读数跳十几个 LSB。直接拿单次采样去判断继电器会疯狂哒哒响。/* adc_filter.c —— 15 次采样去极值后取中位兼顾抗脉冲和响应速度 */ #define ADC_N 15 uint16_t adc_read_filtered(uint8_t ch) { uint16_t buf[ADC_N], tmp; uint32_t sum 0; for (uint8_t i 0; i ADC_N; i) { ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); buf[i] ADC_GetConversionValue(ADC1); } /* 冒泡排序取中位数避开偶发尖峰 */ for (uint8_t i 0; i ADC_N - 1; i) for (uint8_t j i 1; j ADC_N; j) if (buf[i] buf[j]) { tmp buf[i]; buf[i] buf[j]; buf[j] tmp; } for (uint8_t i 5; i ADC_N - 5; i) sum buf[i]; /* 掐头去尾再平均 */ return (uint16_t)(sum / (ADC_N - 10)); }采样周期设成 239.5 个 ADC 时钟是有意为之MQ137 的输出阻抗比较高采样时间短了内部采样电容充不满读数会系统性偏低。滤波上我没有用纯均值而是排序后掐掉最大最小各 5 个再平均这样既压住了尖峰又不会像纯中值那样丢掉连续变化的趋势。标定环节要单独花时间。MQ137 的标定分两步先通电老化 24 到 48 小时让加热丝和敏感膜稳定再在洁净空气里记录 R0之后用 Rs/R0 的比值去查灵敏度曲线。TDS 传感器相对简单用一包标准溶液比如 707ppm泡一下记录当前 ADC 值做一次线性两点标定即可。传感器原始值参考换算关系建议报警阈值MQ137 氨气洁净空气 ADC 约 200400相对值需按 R0 归一化超过标定基线 30% 亮红灯TDS 浑浊度纯净水 ADC 约 100200线性映射到 ppm超过 400ppm 提示换水DS18B20直接读数字值 ÷ 16低于 24℃ 启动加热光照BH1750 直读 lux无需换算低于 50lux 开补光灯注意MQ137 对温湿度交叉敏感鱼缸边上的水汽会让读数整体偏高。如果条件允许把 MQ137 装在鱼缸上方 5cm 以上、避开溅水的位置读数会稳定很多。2.4 光敏电阻和 BH1750 该怎么选原始方案里写的是光敏电阻成本几毛钱接一个分压电阻就能进 ADC。但光敏电阻的阻值和照度是非线性关系而且不同批次一致性很差换个模块就要重新标定阈值。如果只是做「天黑开灯」这种二值判断光敏电阻完全够用如果小程序上要显示一个像样的 lux 数值建议换成 BH1750I2C 接口直读勒克斯省掉整条标定链路。我一般两种都留焊盘代码里用宏切换前期调试用光敏电阻省事成品换成 BH1750。3. 继电器执行层增氧泵间隔定时与加热棒回差控制3.1 三路继电器的驱动电路与上电安全态增氧泵、加热棒、补光灯都靠继电器通断STM32 的 IO 输出电流只有 20mA 量级不能直接驱动继电器线圈。常见做法是加一级三极管或者 ULN2003 达林顿阵列同时在线圈两端并一个续流二极管否则断电瞬间的反向电动势会顺着回路打回 MCU。比电路更容易出事的是上电瞬间的状态。STM32 复位期间 IO 是高阻态如果继电器是低电平触发此时可能被误触发加热棒在没人看的时候直接通电。稳妥的处理是在继电器驱动级加下拉电阻让默认状态锁定为断开软件初始化时再显式拉高。执行部件控制引脚触发电平上电默认态最大持续电流增氧泵PB0低电平吸合断开依据泵体一般 1A加热棒PB1低电平吸合断开50W 约 0.23A补光灯PB2低电平吸合断开0.5A3.2 增氧泵的间隔定时实现需求里写的是「定时打开增氧泵」并且间隔时间要能从小程序远程改。这个功能用软件定时器实现最灵活不必占用硬件定时器资源。核心是把「开多久」和「停多久」两个参数独立成变量再由一个 1 秒的时基去递减。/* aerator.c —— 增氧泵间歇控制参数可被云端下发覆盖 */ typedef struct { uint8_t state; /* 0 停1 开 */ uint32_t on_sec; /* 每次开启时长默认 300s */ uint32_t off_sec; /* 两次开启的间隔默认 1800s */ uint32_t counter; /* 当前阶段剩余秒数 */ } Aerator_t; Aerator_t g_air { .state 0, .on_sec 300, .off_sec 1800, .counter 1800 }; void aerator_tick_1s(void) /* 在 SysTick 或 1s 软定时里调用 */ { if (g_air.counter 0) { g_air.counter--; return; } if (g_air.state 0) { RELAY_AIR_ON(); g_air.state 1; g_air.counter g_air.on_sec; } else { RELAY_AIR_OFF(); g_air.state 0; g_air.counter g_air.off_sec; } }这段代码的取舍在于增氧和温度、氨气之间没有强耦合独立跑一个时间轮就够了不需要上完整的 RTOS 任务。参数下发时只改on_sec和off_sec不要直接改counter否则正在计时的阶段会被打乱出现「刚停三秒又开」的怪现象。更稳的做法是下一轮生效即新参数先存到一个 pending 变量里等当前阶段走完再替换。提示增氧泵的启动电流比额定电流大不少如果发现定时切换时系统复位先查 5V 电源是否被拉垮再查继电器触点是否已经有粘连。3.3 加热棒阈值回差与状态机温度控制最忌讳的就是拿一个阈值直接比较。设定 24℃ 开加热如果水温在 23.98 和 24.02 之间波动继电器会在一秒内来回吸合几十次触点寿命直接报废加热棒也受不了。解决办法是引入回差也就是开和停用两个不同的阈值。/* heater.c —— 带回差的温度控制回差 1.0℃单位 0.1℃ */ #define TEMP_RAW(x) ((x) / 16) /* DS18B20 原始值转整数摄氏度 ×10 */ static uint8_t heat_on 0; void heater_control(int16_t raw_temp, uint8_t mode) { if (mode ! AUTO_MODE) return; /* 手动模式下由小程序直接控制 */ int16_t t10 (raw_temp * 10) / 16; /* 转成 0.1℃ 单位如 245 24.5℃ */ if (!heat_on t10 (int16_t)(g_cfg.temp_low * 10)) { RELAY_HEAT_ON(); heat_on 1; /* 低于下限开加热 */ } else if (heat_on t10 (int16_t)(g_cfg.temp_low * 10 10)) { RELAY_HEAT_OFF(); heat_on 0; /* 高于下限 1℃停加热 */ } }这里的回差是 1.0℃也就是下限 24℃ 时低于 24.0 开高于 25.0 停。热带鱼可以把下限设到 25℃回差保持 1℃冷水鱼则整体下移。如果用的是大功率加热棒回差可以再放大到 1.5℃避免水温过冲。另外g_cfg.temp_low是从云端下发的配置所以这行代码天然支持小程序远程调阈值主循环里每 500ms 调一次就够了不必放进中断。4. ESP8266 STA 模式接入腾讯云 IoT 与微信小程序数据链路4.1 建链方式的选择ESP8266 接到 STM32 的 USART1 上工作模式配置成 STA也就是作为客户端连家里的路由器拿到 IP 后以 TCP 客户端身份连腾讯云 IoT 的接入点。这一步和 AP 模式的区别在于AP 模式是模块自己开热点手机得连它的 Wi-Fi 才能通信出了家门就废了STA 模式走的是家里路由器的上行链路才有远程的意义。真正的分歧点在协议栈放在哪。一种做法是 ESP8266 只做透传STM32 里移植一份 MQTT 客户端自己拼固定头和剩余长度另一种是给 ESP8266 刷带 MQTT 指令的固件用ATMQTTUSERCFG、ATMQTTCONN、ATMQTTPUB这类指令STM32 只负责发 AT 命令和解析响应。毕业设计类的项目我更推荐后者因为 MQTT 的报文编码、心跳保活、断线重连都由模块处理了STM32 端的代码量能少一半。# 上电建链的 AT 指令序列注意每条都要等 OK 或对应响应 ATRST # 软复位等待 ready ATCWMODE1 # 1 STA 模式 ATCWJAPyour_ssid,your_pass # 连接家用路由器返回 WIFI GOT IP ATCIPMUX0 # 单连接模式MQTT 只需要一条链路 ATMQTTUSERCFG0,1,${productId}${deviceName},${deviceName};${sdkappid},${secret},0,0, ATMQTTCONN0,${productId}.iotcloud.tencentdevices.com,1883,1参数上有几个地方必须对上clientId按腾讯云的约定拼成产品 ID 加设备名用户名里的sdkappid是固定后缀不要自己编密码是密钥加哈希算出来的不同接入方式算法不同这点最好直接照抄控制台的生成结果不要凭记忆手写。端口 1883 是明文 MQTT调试阶段用它最省事正式用再考虑 TLS但 STM32F103 的资源跑 TLS 会比较吃力。4.2 上报报文的字段设计数据上报的频率不用太高鱼缸是个慢变量系统5 秒一次已经足够频率高了只会白白增加功耗和流量。报文用 JSON 组织字段名尽量短因为 STM32 的内存有限拼字符串的开销比看起来大。字段含义单位示例值上报频率temp水温℃一位小数25.65snh3氨气相对值无量纲1210stds浑浊度ppm32010slux光照强度lux8530sheat加热棒状态0/10变化时air增氧泵状态0/11变化时mode控制模式0 手动 / 1 自动1变化时/* mqtt_report.c —— 拼 JSON 并发送注意缓冲区要留够余量 */ static char tx_buf[256]; void report_status(void) { int n snprintf(tx_buf, sizeof(tx_buf), ATMQTTPUB0,\$thing/up/property/%s/%s\,1,0,%d\r\n {\method\:\report\,\params\:{\temp\:%.1f,\nh3\:%d, \tds\:%d,\lux\:%d,\heat\:%d,\air\:%d,\mode\:%d}}, PRODUCT_ID, DEVICE_NAME, strlen(payload), /* payload 由第二行内容实测长度决定 */ temp_c, nh3_val, tds_val, lux_val, heat_on, g_air.state, g_cfg.mode); HAL_UART_Transmit(huart1, (uint8_t *)tx_buf, n, 1000); }这里的坑在于ATMQTTPUB要求先给出载荷长度而长度又依赖后面拼出来的字符串所以不能一次性snprintf解决。实际写法是先拼好 JSON 存到单独缓冲区测出长度再拼 AT 命令头和 JSON 一起发。另外状态类字段heat、air、mode采用「变化时上报」而不是周期上报可以显著减少报文量但要注意在设备刚上线时补发一次全量状态否则小程序上会一直显示默认值。4.3 小程序下发指令的解析小程序侧不需要什么复杂框架用原生的wx.request去调云端的 API或者直接在小程序里接 MQTT over WebSocket 都行。对于功能验证阶段我更推荐前者因为不用在手机端维护长连接调试时用接口测试工具也能模拟。下发的报文结构保持一致用method区分动作{ method: control, clientToken: req-20240101-001, params: { temp_low: 24.5, aeration_on: 300, aeration_off: 1200, mode: 1 } }STM32 解析时不要用完整的 JSON 库那会吃掉太多 Flash。常见做法是用字符串查找定位字段名再用atof/atoi取值。三个要点一是取下发数据要带长度串口中断里按行缓存遇到\n再交给解析函数二是解析完必须回一个带clientToken的应答不然小程序无法判断指令是否生效三是所有参数落进配置结构体后要立刻做范围裁剪比如温度下限只允许 18℃ 到 30℃间隔时间只允许 60 秒到 7200 秒防止一次错误下发把加热棒设成常开。5. 联调阶段的排错顺序与两个值得保留的习惯联调最怕的是同时怀疑所有环节一会儿觉得传感器坏了一会儿觉得 Wi-Fi 掉线。我一般按固定顺序推先看电源再看串口打印再看传感器原始值最后才看云端。电源排在第一位是因为大部分「随机复位」「读数乱跳」最后都追到 5V 被继电器或 Wi-Fi 发射拉垮用示波器看一次上电波形比改十遍代码管用。串口打印要打得有节制。主循环里把四路原始 ADC 值、换算值、继电器状态、模式一起打一行格式固定方便肉眼扫。一旦发现温度正常但继电器不动直接看模式是不是被设成了手动发现上报正常但小程序不更新先确认clientToken有没有回再看主题名拼错没有。现象优先排查点常见原因温度恒为 85.0单总线延时微秒延时不准或探头未接上拉电阻氨气读数整体偏高安装位置水汽进入敏感腔或未完成老化标定继电器频繁吸合阈值回差回差设成 0或采样未滤波Wi-Fi 连上但上报失败三元组参数clientId 拼接格式或密码算法错误增氧间隔改了不生效参数更新时机直接改了 counter应下一轮替换两个我一直在用的习惯。第一个是把所有上报数据都带上原始值比如同时上报tds_raw和tds_ppm云端存下来之后回头发现阈值不合适可以直接用历史原始值反推不用重新跑一遍现场。第二个是在小程序上保留一个手动模式开关自动控制逻辑出问题时切到手动用一次对照实验就能判断是算法问题还是硬件问题——比如手动开加热棒水温照样不涨那就别再改代码了去查加热棒和继电器。最后一个细节关于回差如果你发现水温曲线在阈值附近还是有小幅震荡把回差从 1.0℃ 调到 0.5℃ 再观察一次继电器动作日志一般就能看出是回差不够还是加热功率本身过大导致过冲。本文还有配套的精品资源点击获取
返回列表