基于STM32的智能鱼缸控制系统:传感器采集、PID控温与ESP8266上报
简介《基于STM32的智能鱼缸设计》是一份面向嵌入式初学者、电子类专业学生及DIY爱好者的技术方案PDF围绕STM32F103RCT6主控将传统养鱼升级为可自动检测与控制的智能系统。文档从硬件选型切入说明DS18B20水温传感器、TDS水质传感器、步进电机、抽水马达、RGB氛围灯及1.4寸TFT-LCD的用途与连接方式并给出硬件连线、原理图、汉字取模、程序下载、主程序代码与执行流程等软件设计内容还讨论自动/手动模式和一键控制逻辑便于理解从电路搭建到程序落地的完整链路。资源包为1个PDF文件约21.76MB按前言、硬件选型、STM32程序设计等模块组织。目前已有348人学习下载适合课程设计、毕业设计或DIY项目参考也可作为物联网与微控制器应用的实践案例。1. 智能鱼缸为什么落在 STM32 上场景、边界与系统分工养鱼最麻烦的不是喂食是半夜加热棒失控、水泵干转、水位探头结垢之后系统还按旧数据继续跑。智能鱼缸的“智能”如果只停在手机 App 上断网那一刻本地执行器就成了盲盒。把 STM32 放在鱼缸边上的价值是让定时器、ADC、GPIO、PWM 和独立看门狗组成一个断网也能自保的闭环水温用 DS18B20 读水位和 TDS 走 ADC灯光用 PWM 调加热棒和水泵用继电器切断ESP8266 只负责把数据送出去。常见做法是 STM32F103C8T6 加 OLED 做本地显示目标温度、回差、喂食时间存在内部 Flash。适合做 stm32 项目、基于 stm32 的毕业设计也适合已经会点灯、想跑通一个完整 stm32鱼缸 的人。先定安全边界再谈 PID 和远程控制。2. 硬件选型与最小系统从 STM32 主控到传感器与执行器的引脚规划2.1 STM32 型号与最小系统选型F103C8T6 够不够用单缸智能鱼缸的 IO 数量并不夸张通常 6 路数字输出、4 路模拟输入、1 路单总线、1 路 I2C、2 路串口就够。真正吃资源的是定时器和 FlashPWM 调光要占用一个高级或通用定时器微秒延时最好独立占用一个基本定时器舵机还要一路 50Hz PWM。F103C8T6 有 4 个定时器、2 个 12 位 ADC、64KB Flash 和 20KB RAM跑裸机状态机绰绰有余。它的短板是没有硬件浮点PID 里用 float 会慢一些但鱼缸温度变化以秒计完全能接受。型号Flash/RAM定时器ADC适合场景注意STM32F103C8T664KB/20KB42×12bit单缸控制传感器少于 10 路无硬件浮点PID 用 float 需实测STM32F103RCT6256KB/48KB83多缸、多路 PWM引脚多板子大STM32G03032KB/8KB51成本敏感功能精简生态资料少于 F103STM32F4071MB/192KB143要跑网络或复杂算法功耗和成本偏高开发环境我一般用 Keil5 加 STM32CubeMX。装完 Keil5 后要先装 STM32F1 芯片包否则新建工程时器件列表是空的ST-Link 驱动也要装好不然下载时提示找不到设备。如果不想用 HAL就用 stm32标准库新建工程把启动文件、system_stm32f10x.c、stm32f10x_gpio.c、stm32f10x_rcc.c这些按需加进工程再在main.c里自己配时钟。串口下载可以靠 stm32 bootloader配合 flash loader demonstrator 把 BOOT0 拉高进入下载模式现场升级时比拆壳接 ST-Link 方便。还有一个容易忽略的引脚问题F103 的 PA15、PB3、PB4 默认是 JTAG 功能直接当普通 GPIO 会不出电平。常见做法是在 GPIO 初始化前打开 AFIO 时钟执行__HAL_AFIO_REMAP_SWJ_NOJTAG()也就是 stm32禁用jtag、保留 SWD。这样 PA15、PB3、PB4 能释放出来做继电器或按键同时还能用 SWD 下载调试。电源部分AMS1117-3.3 从 5V 降到 3.3V 给 MCU 和传感器输入输出都要有电容。有人问 ams1117把钽电容换成陶瓷电容对stm32有影响吗关键看 LDO 的 ESR 要求AMS1117 这类老 LDO 输出如果只用小容量陶瓷可能振荡表现为 3.3V 上有几十毫伏高频纹波ADC 读数乱跳。稳妥做法是输出保留 10μF 以上钽电容或者用 22μF 陶瓷再加 1Ω 串联电阻补 ESR同时并 100nF 去高频。2.2 传感器与执行器接口清单与引脚分配表引脚分配先定“谁必须独占”再填“谁可以复用”。USART2 的 PA2、PA3 一旦给 ESP8266就不能再拿来做 ADCTIM3_CH1 和 TIM3_CH2 如果给 LED 和舵机那 PA6、PA7 就不能做普通 IODS18B20 要一个带外部上拉的 GPIO最好避开继电器控制脚防止继电器动作时干扰单总线。功能器件接口引脚备注水温DS18B20 防水探头单总线PB124.7kΩ 上拉到 3.3V水位HC-SR04 或压力传感器输入捕获/ADCPB8/PB9 或 PA0超声波非接触避免探针腐蚀TDSTDS 模块ADC1_IN1PA1输出 0~2.3VpHpH 模块ADC1_IN2PA2输出 0~3V需两点校准本地显示SSD1306 OLEDI2C1PB6/PB7地址 0x3C无线上报ESP8266USART2PA2/PA33.3V 电平供电要足加热棒继电器/SSRGPIO 输出PB0默认断开低有效按模块改水泵继电器GPIO 输出PB1加续流二极管灯光LED 灯带TIM3_CH1 PWMPA61kHz 以上无频闪喂食舵机TIM3_CH2 PWMPA750Hz0.5~2.5ms按键轻触开关GPIO 输入PC13内部上拉按下为低蜂鸣器无源蜂鸣器TIM4_CH1 PWMPB52kHz 报警传感器选型上DS18B20 的优势是数字输出、防水封装成熟、单总线省引脚缺点是 12 位转换要 750ms不能高频读。水位如果用水位压力传感器要选 0.5~4.5V 输出并做分压否则超过 3.3V 会打坏 ADC。TDS 和 pH 模块输出阻抗不低直接进 ADC 会拉低精度常见做法是加一级电压跟随器或者至少保证采样时间足够长。执行器侧加热棒如果直接走机械继电器吸合瞬间线圈反电动势会拉低 5VMCU 可能复位所以继电器模块要光耦隔离5V 和 3.3V 之间不要随便共电源。2.3 用 STM32CubeMX 与 Keil5 建工程时钟树、GPIO、定时器配置建工程按这个顺序走CubeMX 选 STM32F103C8T6RCC 选外部晶振 HSE时钟树把 HCLK 拉到 72MHzAPB1 设 36MHzAPB2 设 72MHz。ADC1 开多通道扫描加 DMA顺序设为水位、TDS、pH。TIM1 设成 1μs 计数给 DS18B20 和超声波做延时。TIM3 设 PWM通道 1 给 LED通道 2 给舵机但两个通道频率要求不同所以实际工程里我会把 LED 放到 TIM3舵机放到 TIM4或者用软件改 ARR 分时复用。USART2 设 1152008N1开 DMA 发送。生成 MDK-ARM 工程后在main.c里先校准 ADC再启动 PWM 和 DMA。// main.c 初始化片段先校准再启动否则前几次 ADC 值不准 HAL_ADCEx_Calibration_Start(hadc1); HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buf, ADC_CH_NUM); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 300); // ARR999 时约 30% 占空比PWM 频率按72MHz / (PSC1) / (ARR1)算。LED 调光要 1kHz 以上取 PSC71、ARR999得到 1kHz舵机要 50Hz取 PSC71、ARR19999比较值 500~2500 对应 0.5~2.5ms蜂鸣器 2kHz 取 PSC71、ARR499。改频率时先算 ARR再改比较值别把舵机接到 1kHz 的 PWM 上否则舵机内部会发出高频啸叫并发热。2.4 供电与隔离继电器、加热棒、水泵共地问题整个鱼缸控制盒建议用 12V/3A 电源进12V 直接给水泵和降压模块5V 给继电器3.3V 给 MCU 和传感器。所有地最终要共地但强电部分必须隔离220V 加热棒火线串继电器或固态继电器零线直连强弱电走线分开继电器输出侧和 MCU 侧不要共用走线。水泵是感性负载关断时会产生反峰继电器触点并 RC 吸收或者用固态继电器。水位探针如果用直流长期加电几周就会电解发黑读数漂移常见做法是只在采样前给探针供电或者用交流方波激励。模拟部分同样要小心。ADC 的参考电压就是 3.3V如果 3.3V 被继电器线圈拉出纹波TDS 和 pH 会跟着跳。电源入口加 1000μF 电解继电器线圈两端加续流二极管MCU 的 3.3V 再经一级 LC 或磁珠隔离。模拟地和数字地单点连接ADC 输入脚并 100nF 到地。上电顺序也要注意先让继电器默认断开MCU 初始化完成后再根据传感器状态决定是否允许加热否则一上电就加热是最危险的失败模式。3. 用 STM32 采集水温、水位与水质ADC、DS18B20 与滤波实现3.1 DS18B20 单总线时序与 STM32 微秒延时DS18B20 的难点不在协议在微秒级延时。HAL_Delay 最小 1ms不够用如果用空循环延时编译器优化等级一改就偏。我一般拿 TIM1 做 1μs 计数器PSC71ARR0xFFFF需要延时就清零计数器再等计数到达。这样不会因为中断打断而出现 stm32延时函数delay卡死 的情况因为 TIM 计数是硬件行为但要保证 TIM1 已经启动。// TIM1 做 1us 基准72MHz 下 PSC71 void delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(htim1, 0); HAL_TIM_Base_Start(htim1); while (__HAL_TIM_GET_COUNTER(htim1) us); HAL_TIM_Base_Stop(htim1); } uint8_t ds18b20_reset(void) { set_pin_output(); // 主机拉低总线 HAL_GPIO_WritePin(DS_GPIO, DS_PIN, GPIO_PIN_RESET); delay_us(480); set_pin_input(); // 释放4.7k 上拉拉高 delay_us(70); uint8_t ack !HAL_GPIO_ReadPin(DS_GPIO, DS_PIN); delay_us(410); return ack; // 1 表示器件应答 }读温度流程是复位、跳过 ROM0xCC、启动转换0x44、等 750ms、复位、跳过 ROM、读暂存器0xBE、读 9 字节、校验 CRC。原始值低 11 位有效温度等于原始值乘 0.0625。如果连续三次 CRC 错或复位无应答就把传感器标记为掉线加热棒立即断开。线长超过 5 米时上拉电阻要减小到 3.3k 左右或者改用屏蔽线并降低采样频率。3.2 水位与 TDS/pH 模拟量的 ADC 采样与滑动平均ADC 是 12 位参考 3.3V读数 0~4095 对应 0~3.3V。水位压力传感器输出 0.5~4.5V不能直接进 ADC先用两个电阻分压到 0.25~2.25V再按分压比反推。TDS 模块输出 0~2.3VpH 模块输出 0~3V 并带偏置三点里最容易漂的是 pH必须用标准液校准。采样时不要单次读开 DMA 连续采 16 次去掉最大最小再平均能压掉水泵启停带来的尖峰。#define ADC_N 16 uint16_t adc_buf[ADC_N]; uint16_t adc_avg(void) { uint32_t sum 0; for (int i 0; i ADC_N; i) sum adc_buf[i]; return sum / ADC_N; } float adc_to_volt(uint16_t adc) { return adc * 3.3f / 4095.0f; // F103 参考电压取 3.3V }传感器输出范围换算关系注意水位压力0.5~4.5V分压后按量程线性换算必须分压到 3.3V 以内TDS0~2.3Vppm 与电压近似线性需温度补偿每 1℃ 约补偿 2%pH0~3V两点校准后线性插值4.00 和 6.86 标准液校准 pH 时把探头洗净擦干先泡 4.00 标准液记录 ADC 电压再泡 6.86记录第二点。用这两点算斜率和截距不要用理论值硬套。TDS 的换算和温度有关水温 25℃ 是基准实测温度偏离时要补偿。水位如果换成 HC-SR04就不存在电解问题但超声波在鱼缸里会遇到水面波动和泡沫读数会跳所以也要做滑动平均并且安装时让探头离水面至少 5cm避免盲区。3.3 定时器触发 ADC 与超声波水位输入捕获ADC 连续转换会占 CPU更好的做法是 TIM2 每 1ms 触发一次 ADCDMA 搬 3 个通道主循环只读结果。这样采样节拍稳定串口打印也不会打乱采样。超声波 HC-SR04 的 Echo 高电平时间对应距离距离 高电平时间 × 340 / 2。用 TIM4_CH4 做输入捕获上升沿记下 CCR下降沿再记一次差值乘 0.017 就是厘米级距离。这个思路和 stm32定时器捕获测频率 一样都是测两个边沿之间的计数差。// TIM4 输入捕获回调记录回波高电平 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static uint32_t rise 0; if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_4) { if (HAL_GPIO_ReadPin(ECHO_GPIO, ECHO_PIN) GPIO_PIN_SET) { rise HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_4); } else { uint32_t fall HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_4); uint32_t ticks (fall rise) ? (fall - rise) : (0xFFFF - rise fall); water_cm ticks * 0.017f; // 72MHz 下 1 tick 约 13.9ns声速 340m/s } } }串口调试时把温度、水位、TDS、pH、当前状态按一行 CSV 打出来波特率 115200。注意不要在高频中断里调用 printfHAL 的阻塞发送会拖慢中断严重时看门狗复位。可以放到主循环 100ms 发一次或者用 DMA 加环形缓冲。stm32串口调试pid 的时候把目标值、反馈值、PID 输出、加热状态四列打出来用上位机画曲线比盯着 OLED 调参快得多。4. 智能鱼缸的自动控制PWM 调光、继电器水泵与 PID 控温4.1 加热棒与温度回差控制先滞环后 PID加热棒是大惯性负载继电器一开一关水温不会立刻变化。如果直接用 PID 输出开关量继电器会频繁吸合触点很快烧蚀。更稳的起点是回差控制目标 26℃低于 25.5℃ 开加热高于 26.5℃ 关加热同时叠加 30℃ 硬上限。回差 0.5℃ 是常见起点水体越大回差可以越小但不要小于传感器分辨率否则会在阈值附近抖动。#define TEMP_SET 26.0f #define TEMP_HYST 0.5f void heater_control(float temp) { static uint8_t on 0; if (temp 30.0f) { // 超温硬断电优先级最高 HAL_GPIO_WritePin(HEAT_GPIO, HEAT_PIN, GPIO_PIN_RESET); fault_flag | FAULT_OVER_TEMP; return; } if (!on temp TEMP_SET - TEMP_HYST) { on 1; HAL_GPIO_WritePin(HEAT_GPIO, HEAT_PIN, GPIO_PIN_SET); } else if (on temp TEMP_SET TEMP_HYST) { on 0; HAL_GPIO_WritePin(HEAT_GPIO, HEAT_PIN, GPIO_PIN_RESET); } }如果要更平滑可以用 PWM 驱动固态继电器做过零触发再用增量式 PID。增量式比位置式好恢复积分饱和也不容易炸。typedef struct { float kp, ki, kd; float e1, e2; } PID; float pid_calc(PID *p, float set, float fb) { float e set - fb; float du p-kp * (e - p-e1) p-ki * e p-kd * (e - 2*p-e1 p-e2); p-e2 p-e1; p-e1 e; return du; // 累加后限幅到 0~100 }参数可以先从 Kp20、Ki0.5、Kd100 试但鱼缸加热的积分项一定要小否则停加热后水温还会继续升出现超调。串口曲线里看阶跃响应超调大就减 Ki振荡就减 Kp升温太慢再加一点 Kp。4.2 用 STM32 定时器输出 PWM 调 LED 与喂食舵机PWM 在鱼缸里有三个典型用途LED 调光、舵机喂食、蜂鸣器报警。LED 调光频率要高于 1kHz否则摄像头拍出来有横纹舵机必须 50Hz脉宽 0.5~2.5ms无源蜂鸣器用 2kHz 方波。定时器不够时可以把 LED 和蜂鸣器放同一个定时器的不同通道舵机单独占一个。用途频率PSCARR说明LED 调光1kHz71999占空比 0~1000舵机50Hz7119999比较值 500~2500蜂鸣器2kHz71499无源蜂鸣器// 舵机角度 0~180 映射到 500~2500us void servo_set(uint8_t deg) { uint16_t ccr 500 (uint16_t)deg * 2000 / 180; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, ccr); }喂食舵机不要一直给 PWM 堵转转到 90° 后延时 500ms再让定时器停止输出或回到 0°否则舵机内部齿轮长期受力会发热。LED 调光可以用正弦或线性渐变避免突然全亮吓到鱼但渐变速度要慢常见做法是 30 分钟内从 0 升到设定亮度。4.3 换水、过滤、喂食的状态机与安全互锁鱼缸控制最怕两个动作撞在一起加热时排水导致加热棒干烧喂食时开过滤饲料直接被吸走。用状态机把动作串起来互锁优先级高于定时。状态迁移表可以先画清楚再写 switch。状态动作进入条件退出条件互锁IDLE执行器默认关上电复位定时到无FEEDING舵机转 90°喂食时间到5s 后先停水泵DRAINING排水泵开水位高水位低加热必须关FILLING进水阀开水位低水位到排水必须关FILTER过滤泵开常开故障无FAULT全部关超温/掉线手动复位无void fsm_run(void) { switch (state) { case IDLE: if (feed_timer_expired()) state FEEDING; break; case FEEDING: HAL_GPIO_WritePin(PUMP_GPIO, PUMP_PIN, GPIO_PIN_RESET); // 先停水泵 servo_set(90); if (elapsed(5000)) { servo_set(0); state IDLE; } break; case DRAINING: if (heater_on) { state FAULT; break; } // 加热中不排水 HAL_GPIO_WritePin(DRAIN_GPIO, DRAIN_PIN, GPIO_PIN_SET); if (water_low()) state FILLING; break; default: all_off(); break; } }掉电恢复后不要直接进 FILTER 或加热先读 Flash 里的目标参数再根据当前水位和温度决定状态。换水如果靠水泵抽水排水泵不能比进水泵快太多否则水位波动大超声波会误判。喂食后延迟 10 分钟再开过滤让饲料有时间下沉。4.4 异常处理看门狗、超温断电、传感器掉线独立看门狗 IWDG 用内部 32kHz RC 做时钟不用外部晶振也能跑。初始化成 1s 溢出主循环每 200ms 喂一次。如果程序卡在某个 while 等待看门狗会复位 MCU所有 GPIO 回到默认输入态继电器线圈断电加热棒和水泵自然断开。但要注意复位后如果立即恢复加热可能反复超温所以故障标志要存在备份寄存器或 Flash 里重启后先进入 FAULT。异常检测方式动作恢复超温温度 30℃断加热蜂鸣器响温度降回且手动复位传感器掉线连续 3 次无应答断加热进 FAULT重新插拔并复位水位低超声波距离超阈值断加热开进水水位到正常程序跑飞IWDG 1s 未喂狗系统复位全部关自动但记录复位计数禁用 JTAG 后 SWD 还能用现场调试别把 SWD 引脚也关了。stm32 bootloader 可以用于升级但升级前必须先把加热棒和水泵继电器置为断开且断电重启期间保持断开。最后继电器输出建议默认低电平断开避免上电瞬间 MCU 引脚浮空导致误动作。5. 把智能鱼缸接上管理端ESP8266 串口上报、OLED 本地显示与调参实战5.1 ESP8266 与 STM32 的串口 JSON 上报ESP8266 和 STM32 之间最常见的连接是交叉串口ESP8266 TX 接 STM32 PA3USART2_RXESP8266 RX 接 PA2USART2_TXCH_PD 用 10kΩ 上拉到 3.3VVCC 要能提供 500mA 峰值GND 共地。不要用 5V 串口直连ESP8266 是 3.3V 电平。初始化时用 AT 指令配成 STA 模式连接后开 TCP 透传每 5 秒发一次 JSON。ATCWMODE1 ATCWJAPyour_ssid,your_password ATCIPSTARTTCP,192.168.1.100,8080 ATCIPSEND72 {t:26.3,w:18.2,tds:320,ph:7.1,st:OK}STM32 侧拼 JSON 不要用浮点格式化太大的缓冲区F103 的 snprintf 对 float 支持要开启。下面这段每 5 秒调用一次发送用 DMA避免阻塞主循环。char json[96]; int n snprintf(json, sizeof(json), {\t\:%.1f,\w\:%.1f,\tds\:%u,\ph\:%.2f,\st\:\%s\}, temp, water_cm, tds_ppm, ph_val, state_name()); HAL_UART_Transmit_DMA(huart2, (uint8_t*)json, n);掉线重连不用太复杂连续 3 次发送失败就重新发ATCIPSTART再失败就重启 ESP8266。stm32 http 库不是必需的ESP8266 做 TCP 透传上位机写个 Python socket 服务端就能收数据。5.2 OLED 本地显示与按键调参OLED 用 SSD1306I2C 地址 0x3C接 PB6/PB7。第一行显示水温和目标温度第二行显示水位和 TDS第三行显示 pH 和当前状态。按键 PC13 短按切换页面长按 2 秒进入设置再用两个按键加减目标温度。参数不要每次改都写 Flash退出设置时统一存一次。F103 的 Flash 页是 1KB擦写寿命约 1 万次频繁写会提前损坏。#define PARAM_ADDR 0x0801FC00 void save_param(void) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef e {FLASH_TYPEERASE_PAGES, PARAM_ADDR, 1}; uint32_t err; HAL_FLASHEx_Erase(e, err); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, PARAM_ADDR, *(uint32_t*)param); HAL_FLASH_Lock(); }5.3 验证与排错逻辑分析仪、串口波形、长时间运行上电前先测 3.3V 和 5V 对地阻值确认没有短路。逻辑分析仪抓 DS18B20 的复位脉冲正常应该是 480μs 低电平后器件拉低约 60~240μs抓 PWM 看占空比是否随设置变化。ADC 噪声用万用表看 3.3V 参考如果纹波超过 20mV先查 AMS1117 输出电容和继电器续流。常见坑还有 stm32延时函数delay卡死多半是 TIM 没启动或中断优先级配置导致计数不跑串口 printf 放在中断里阻塞会把主循环拖到看门狗复位。老化测试至少跑 72 小时记录 IWDG 复位次数和最近一次故障码。把 IWDG 复位计数和故障码留在 OLED 第二行再去查水泵和加热棒的接线比事后猜死机原因省一晚上。本文还有配套的精品资源点击获取