简介:这份基于单片机的智能鱼缸监控系统设计资料包,面向电子信息、自动化、物联网等专业学生与嵌入式开发者,适合作为课程设计、毕业设计或项目初期演示的完整参考方案。包内包含单片机控制源码、仿真电路、原理图截图与详细设计文档,覆盖水温检测、液晶显示、电机控制等功能模块,可帮助读者理解从硬件搭建到软件调试的完整开发流程。压缩包共六十八个文件,以C语言源文件、仿真工程、编译工程、电路图片和说明文档为主,另附烧录文件与辅助脚本,整体大小约五点零六兆字节,目录结构清晰,便于按模块检索学习。资源中额外提供作者整理的说明笔记,可对照代码掌握逻辑思路与排错方法;项目曾获导师指导认可,答辩评审分达九十五分,代码经过测试运行成功。目前已有157人学习下载,适合需要快速上手单片机系统设计或在此基础上进行二次扩展的读者。
1. 单片机智能鱼缸监控系统在做什么——先立框架
一个鱼缸里死鱼的原因,九成不是缺食,是水温波动和溶氧不足。水温忽高忽低、水泵停转半天没人发现,等看到浮头再去补救已经晚了。基于单片机的智能鱼缸监控系统,就是把温度、水位、浑浊度、pH 这几路信号接进单片机,按设定阈值自动控制加热棒、增氧泵、水泵和喂食器,同时把采集结果送到屏幕、上位机或云平台,让用户看到的是“状态”,而不是一次次去翻死鱼。这类系统是单片机课程设计和毕业设计里最常见也最完整的综合题,它同时覆盖了传感器接口、模拟量采集、定时器中断、驱动电路、串口协议和上位机可视化,做完这一套,等于把单片机应用开发的主干路径完整走了一遍。下面按我自己做这类项目时的顺序来讲,从硬件选型一路到联调提分。
2. 传感器选型与单片机接口设计:温度、水位、浑浊度、pH 核心电路
2.1 传感器选型:从“能测”到“测得准”的取舍
我见过不少项目先把传感器买全,结果调不通再换,浪费时间。选传感器不是越贵越好,而是看输出类型是不是单片机容易接的信号。
| 采集量 | 常用传感器 | 输出信号 | 单片机接口 | 注意点 |
|---|---|---|---|---|
| 水温 | DS18B20 | 单总线数字信号 | 任意 GPIO,加上拉电阻 | 防水探头封装,引线别太长 |
| 水位 | 光电液位开关 / 超声波 | 开关量 / 模拟电压 | GPIO 或 ADC | 液位开关要避开气泡干扰 |
| 浑浊度 | 红外对射式浊度传感器 | 0~4.5V 模拟电压 | ADC 引脚 | 探头要浸入水中并避光 |
| pH | pH 电极 + 变送板 | 0~5V 或 4~20mA | ADC 或运放电路 | 电极需要校准液标定 |
| 照明 | 高亮 LED 灯带 | 开关量 | GPIO + 驱动管 | 与主控电源隔离 |
DS18B20 的时序比较严格,初始化、读位、写位都要按手册延时,51 单片机用普通 IO 模拟单总线也可以用,但注意要关闭中断,避免时序被定时器打断。浑浊度传感器很多模块输出的是模拟电压,而且电压范围和鱼缸水体的洁净程度相关,不是所有模块都直接标定成 NTU 值,所以固件里要保留一个线性换算的参数。
pH 模块是这类项目里最容易出问题的传感器。电极输出的是微弱的毫伏级信号,变送板上通常自带运放,输出 0~5V 对应 pH 0 ~ 14。如果买到的模块是 4~20mA 电流环输出,后级要并一个 250Ω 电阻转成 1~5V 再接 ADC,别直接并到单片机上。
提示:水位传感器我不推荐那种浮球开关,因为浮球漂在水面上会随水流抖动,产生误触发。做鱼缸监控建议用光电式,安装在水位上限和下限两个位置。
2.2 单片机选型与最小系统电路
单片机的选择直接决定开发效率和代码复杂度。如果只追求教学演示,STC89C52 或者 STC15 系列就够;但要做 ADC 采集、PID 控温、串口同时跑多个任务,STM32F103C8T6 是最稳的,内部有 12 位 ADC、多个定时器、足够多的串口。
“单片机最小系统”这部分,项目文档里一定要把以下内容写清楚,评阅老师也喜欢看:
- 供电:5V 输入,AMS1117-3.3V 给 STM32 供电,滤波电容用 10μF + 100nF 并联
- 晶振:8MHz 无源晶振配两个 20pF 电容,或者直接用内部 HSI 但串口波特率会偏
- 复位:STM32 用 NRST 引脚,10kΩ 上拉到 3.3V,100nF 电容到地
- 去耦:每个电源引脚附近放一个 100nF 陶瓷电容
需要注意一点,STM32F103C8T6 的 ADC 是 12 位,参考电压默认是 VDDA,如果传感器输出 0~5V,就不能直接进 ADC 引脚,必须先用电阻分压把 5V 降到 3.3V,再进 PA4~PA7。分压用两个精密电阻,比如 10kΩ 和 20kΩ,计算出的分压比送入固件。
浑浊度传感器的输出阻抗并不低,直接接 ADC 会受走线干扰。我在两个应用里都并联了一个 0.1μF 电容来做低通滤波,这对应了“单片机低通滤波左移右移”里常说的 RC 低通思路——采样前先压掉高频噪声。
/* 温度采集:DS18B20 单总线读取,返回摄氏度*10 */ int16_t DS18B20_ReadTemp(void) { uint8_t low, high; int16_t temp; if (DS18B20_Reset() != 0) { return 0xFFF0; // 传感器不在线 } DS18B20_WriteByte(0xCC); // skip ROM,单设备直接跳过地址匹配 DS18B20_WriteByte(0x44); // 启动温度转换 while (!DS18B20_ReadBit()); // 等待转换完成,数据线被拉低,转换完释放 DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器 low = DS18B20_ReadByte(); high = DS18B20_ReadByte(); temp = (high << 8) | low; return temp * 10 / 16; // 12 位精度,LSB 代表 1/16 摄氏度 }这段代码是单总线读温度的标准流程。注意0xCC是跳过 ROM 匹配,如果总线上挂了多个 DS18B20 就要改用搜索 ROM 命令并处理 64 位序列号,这个细节在项目文档里可以作为扩展点写进去。
2.3 驱动电路:继电器、水泵、增氧泵、加热棒与喂食舵机
传感器是输入,执行器是输出。鱼缸里的执行器都是 220V 或 5V 的大电流设备,不能用单片机 GPIO 直接驱动。常见做法是用 GPIO 接一个 NPN 三极管驱动 5V 继电器,再用继电器触点控制 220V 水泵或加热棒。
- 加热棒:继电器控制电源通断,注意加热棒不能极速频繁启停
- 水泵:继电器控制,或 PWM 调速(需要 MOS 管,不能用继电器调速)
- 增氧泵:继电器控制即可
- 喂食器:用 5V 舵机,GPIO 输出 PWM 信号,控制舵机转动角度把饲料推出去
驱动电路的核心是隔离和续流。继电器线圈是感性负载,断电瞬间会产生反电动势,必须在线圈两端并联一个 1N4007 二极管(负极接电源正端),否则会把三极管击穿。这类细节在图纸评审时几乎必被问到,写进“硬件设计”文档里是加分项。
/* 舵机输出:50Hz PWM,0.5ms~2.5ms 对应 0~180 度 */ void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t angle) { uint16_t pulse = 500 + (uint16_t)angle * 1000 / 180; __HAL_TIM_SET_COMPARE(htim, channel, pulse); }对于 STM32 的定时器 PWM 输出,pulse的值不是直接填微秒,而是取决于定时器时钟和预分频设置。如果定时器时钟是 72MHz,预分频设置为 143 得到 500kHz,计数周期设为 9999 得到 50Hz,那 pulse 值 500 就是 1ms。后面的参数调节要按这个公式反推。
3. 单片机固件实现:ADC 采集滤波、控制策略与 Modbus-RTU 协议链路
3.1 ADC 多通道采集与滑动平均滤波
ADC 采集本身不难,难的是采集结果稳不稳。鱼缸里浊度传感器和 pH 传感器的输出本身就带有缓慢漂移和随机噪声,直接拿单次采样值去判断阈值,会导致继电器频繁吸合。我一般用滑动平均滤波:维护一个长度为 10 或 16 的环形缓冲区,每采一次新值就替换最老的值,然后取算术平均。
#define ADC_BUF_SIZE 16 uint16_t ADC_ReadFiltered(ADC_HandleTypeDef *hadc, uint32_t channel) { static uint16_t buf[ADC_BUF_SIZE]; static uint8_t index = 0; static uint8_t filled = 0; uint32_t sum = 0; /* 切换 ADC 通道并等待稳定 */ ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = channel; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc, &sConfig); HAL_ADC_Start(hadc); if (HAL_ADC_PollForConversion(hadc, 100) == HAL_OK) { buf[index] = HAL_ADC_GetValue(hadc); } HAL_ADC_Stop(hadc); index = (index + 1) % ADC_BUF_SIZE; if (index > filled) filled = index; uint8_t count = filled < ADC_BUF_SIZE ? filled : ADC_BUF_SIZE; for (uint8_t i = 0; i < count; i++) { sum += buf[i]; } return (uint16_t)(sum / count); }这里的改动正好回应了“单片机低通滤波左移右移”里的一个常见疑问——移动平均本质上是低通滤波,窗口长度决定截止频率。窗口越大越平滑但反应越慢,对鱼缸这种慢变系统,16 个点足够。
采样时间ADC_SAMPLETIME_55CYCLES也要提一句。STM32F103 的 ADC 输入阻抗要求信号源内阻足够低,浊度传感器的输出内阻偏高,如果采样时间太短,内部采样电容充不满,采集值会偏小。当我把它改成 55.5 周期后,读数明显稳定。
3.2 控制策略:加热棒启停、缺氧保护和自动喂食
很多作品把控制写成“温度低于 25 度就打开加热”,这在实际运行中会让加热棒寿命变短,因为温度到达 25 度后会惯性继续上升,随后又立刻降到目标值以下,反复启停。我采用的控温策略是滞回控制:
- 水温低于目标温度 1.5℃ 时开启加热棒
- 水温高于目标温度 0.5℃ 时关闭加热棒
目标温度 26℃,滞回区间 [24.5, 26.5]℃ - 温度 = 24.3 ℃ -> 开启加热 - 温度 = 26.1 ℃ -> 保持加热(未到上限) - 温度 = 26.7 ℃ -> 关闭加热这个策略既避免了继电器频繁吸合,又不需要 PID 那样调节复杂的参数。如果是毕业设计想上一点深度,可以在温度控制环里做增量式 PID,输出 PWM 控制加热棒功率,但这需要把加热棒换成可控硅调压模块。
增氧泵的控制不能只靠定时器。如果鱼缸里的溶氧量没有直接传感器(光学溶氧传感器很贵),系统可以用间接判断:水温越高,水体的饱和溶氧浓度越低。我一般这样设计规则:
- 默认每小时开启 15 分钟增氧
- 水温超过 30 度时,改为每 30 分钟开启 15 分钟
- 喂食后开启增氧 10 分钟,因为鱼进食时耗氧量上升
喂食动作要和定时任务配合。舵机转到 90 度把饲料推出,停留 1 秒后再转回 0 度,整个过程大约需要 3 秒。这期间主循环不能停下来等舵机转动,我用定时器标志位加一个 3 秒的状态机来控制,而不是用HAL_Delay(3000)死等。
3.3 串口链路与 Modbus-RTU 帧处理
“modbus单片机帧接收数据程序”是很多人在搜的内容。做鱼缸监控这类项目,下位机和上位机之间最常用的通信协议就是把 Modbus-RTU 改为简化版裸串口协议,或者直接采用标准 Modbus-RTU 协议。数据量不大,标准 Modbus-RTU 是更好的选择,因为上位机、触摸屏、组态软件全都支持,评阅时演示成本低。
Modbus-RTU 的一条读保持寄存器请求帧格式如下:
从站地址(1字节) + 功能码(1字节) + 寄存器起始地址(2字节) + 寄存器数量(2字节) + CRC16(2字节)典型请求帧:01 03 00 00 00 05 85 C9,含义是“读取 1 号从机、地址 0x0000 开始的 5 个寄存器(温度、水位、浑浊度、pH、设备状态)”。
单片机解析帧的关键是收完整再处理。我用一个串口空闲中断加环形缓冲区的做法,接收完一帧后校验 CRC,再进入命令处理函数。
/* 串口接收到一帧数据后的 CRC 校验与分发 */ void Modbus_OnFrameReceived(uint8_t *frame, uint16_t len) { uint16_t crc_calc = Modbus_CRC16(frame, len - 2); uint16_t crc_recv = (frame[len - 2] | (frame[len - 1] << 8)); if (crc_calc != crc_recv) { return; // CRC 错误,直接丢弃 } if (frame[0] != MODBUS_ADDR) { return; // 地址不匹配,不响应 } switch (frame[1]) { case 0x03: /* 读保持寄存器 */ Modbus_HandleReadRegisters(frame, len); break; case 0x06: /* 写单个寄存器 */ Modbus_HandleWriteSingle(frame, len); break; default: break; } }这里要注意一个常见的时序问题:CRC 校验要在收到完整帧之后做,不能边收边校验。因为 CRC 的 CRC16 是整帧的校验码,必须把整帧放进一个缓冲区才能计算。我用的是 DMA 接收加串口空闲中断,DMA 把数据写入环形缓冲区,空闲中断表示一帧结束,然后统一解析。
如果用的是 51 单片机,没有 DMA,就在串口中断里把每个收到的字节压进全局数组,再用一个字节计数器和接收超时定时器来判断一帧是否结束。接收超时时间一般设 3.5 个字符时间,9600 波特率大约是 4ms。
4. 上位机与云平台:协议解析、告警与可视化落地
4.1 通信链路选择:USB-TTL 直连还是 WiFi 上云
鱼缸监控系统的上位机链路有两条主流路线:
- USB-TTL 直连:单片机 UART 通过 CH340 转 USB 接到 PC 或树莓派,上位机读串口数据
- WiFi 上云:单片机外接 ESP8266 或 ESP32,通过 MQTT 协议把数据推到云平台
如果做毕业设计,两条链路建议都做,核心数据采集和控制逻辑放在单片机里,ESP8266 只做透传。不要把所有业务逻辑都放到云端去处理,否则一旦断网,鱼缸的自动加热和增氧就全停了,这在实践里是不可接受的。
我一般把 ESP8266 配置成 STA 模式,用 AT 指令连接 WiFi,再用 MQTT 协议连接云平台。ESP8266 的固件版本不同,AT 指令略有差异,但现在主流的 V1.7 以上版本都支持 MQTT,直接用AT+MQTTCONN开头的那一组指令即可。
4.2 上位机最小实现:Python 串口读取与实时绘图
上位机我用 Python 写。核心逻辑分成三块:串口参数配置、Modbus-RTU 请求发送、数据解析和可视化。Pyserial 负责串口,modbus_tk 库负责协议栈,matplotlib 负责画实时曲线。
import serial import modbus_tk.defines as cst import modbus_tk.modbus_rtu as modbus_rtu import matplotlib.pyplot as plt import matplotlib.animation as animation from collections import deque # 初始化串口,参数必须和单片机端一致 master = modbus_rtu.RtuMaster( serial.Serial(port="COM3", baudrate=9600, bytesize=8, parity="N", stopbits=1, timeout=0.5) ) master.set_timeout(1.0) temp_history = deque(maxlen=120) # 只保留最近 120 个采样点 def read_fish_tank_status(): """读取 1 号从机,起始地址 0,共 4 个寄存器,返回值单位是原始 ADC 值""" data = master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 4) temp_raw, water_level, turbidity_raw, ph_raw = data return temp_raw, water_level, turbidity_raw, ph_raw def update_plot(frame): temp_raw, water_level, turbidity_raw, ph_raw = read_fish_tank_status() temp = temp_raw / 10.0 # 温度,单位 0.1 摄氏度 turbidity_ntu = turbidity_raw * 4.5 / 4095 # 通过分压电阻后的 ADC 量程换算 temp_history.append(temp) plt.cla() plt.plot(temp_history) plt.xlabel("Sample index") plt.ylabel("Temperature (C)") plt.title("Fish Tank Temperature Trend") ani = animation.FuncAnimation(plt.gcf(), update_plot, interval=2000) plt.show()这段代码有几个参数要解释清楚:
baudrate=9600, timeout=1.0:单片机端的串口配置必须一致,很多联调失败就是波特率不匹配READ_HOLDING_REGISTERS, 0, 4:读 4 个寄存器,和单片机端定义的寄存器表一一对应turbidity_ntu = turbidity_raw * 4.5 / 4095:因为 5V 模拟量经过分压到 3.3V ADC,实际换算关系要根据具体的电阻分压比重新计算
4.3 告警链路:本地蜂鸣器与云端推送结合
单片机本地告警用蜂鸣器和 LED 实现,优先级从高到低排列:
- 水温超过 33℃:立即关闭加热,蜂鸣器连续鸣叫
- 水位低于下限:关闭水泵,蜂鸣器间歇鸣叫,防止水泵空转烧毁
- 浑浊度超过阈值:提示换水,不自动执行
- pH 超过 8.5 或低于 6.5:提示换水并加强增氧
云端告警用 MQTT 的遗嘱消息和主题订阅配合。单片机上电时发布当前状态到fish_tank/status,异常时发布告警消息到fish_tank/alarm,订阅fish_tank/control接收远程控制指令。
/* ESP8266 透传模式下的 MQTT 告警发布 */ void ESP8266_PublishAlarm(uint8_t alarm_code) { char buf[64]; memset(buf, 0, sizeof(buf)); /* AT+CIPSEND=54,发送一条 54 字节的 MQTT 发布消息 */ snprintf(buf, sizeof(buf), "AT+CIPSEND=54\r\n"); ESP8266_SendCommand(buf, "OK", 1000); ESP8266_SendString("fish_tank/alarm "); /* 主题 */ // 实际消息是 JSON 格式:{"temp":32.5,"alarm":3} ESP8266_SendString("{\"temp\":32.5,\"alarm\":3}"); }实际项目中不要把 MQTT 报文内容手拼,压缩域和长度字段很容易算错。调试时先用串口助手抓 AT 指令的回显,再把数据长度字段54与抓到的报文长度逐字节核对。
5. 联调、典型故障与提分设计细节
5.1 上电瞬间继电器误动作的根因与对策
鱼缸监控系统最常见的故障是“上电时继电器会咔哒响一下,然后才恢复正常”。这个现象的原因是单片机复位期间 GPIO 处于高阻态,圈继电器驱动三极管的基极被噪声电压拉高,导致继电器瞬间吸合。
解决办法不是改程序,而是改硬件:
- 在单片机和三极管基极之间串一个 10kΩ 电阻,并加一个 10kΩ 下拉电阻到地
- 把继电器驱动管放在单片机复位后才使能,比如用一个 GPIO 控制光耦的供电
代码层面还有一种做法:初始化阶段把控制继电器的引脚先设置为推挽输出低电平,整个初始化完成后再把这几个引脚配置到应用状态机。
void Relay_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); /* 上电默认为低电平,禁止继电器动作 */ HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15, GPIO_PIN_RESET); }5.2 用 printf 重定向串口做数据跟踪
单片机调试时,我最常用的手段就是把串口当调试输出口,每次状态机切换时打印一行状态。用 STM32 时要重定向fputc到串口。
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }然后外部定时触发打印:
printf("[sys] temp=%.1f level=%d turb=%d ph=%.2f heater=%d pump=%d\r\n", temp_c, water_level, turb_raw, ph_val, heater_state, pump_state);注意printf在嵌入式环境里若不开微库,会占用大量 Flash。我一般关闭浮点打印功能,或者改用整数显示——把温度乘以 10 传给上位机,上位机再除以 10 显示,这样在 51 单片机上也跑得动。
5.3 提分设计:模块化驱动、异常记录与看门狗
这个项目评为高分的关键,不是功能多少,而是系统的可靠性和可复现性。我见过太多作品“答辩时正常,回去就死机”。三个值得加的设计细节:
- 看门狗:监控主循环运行周期,一旦主循环卡死(比如 DSP 芯片的浮点异常或外部设备挂起),立即复位恢复运行。IWDG 超时时间我一般设 1 秒,喂狗时机放在控制状态机之后、人机交互之前。
- 传感器故障检测:DS18B20 返回
0xFFF0这类错误码时,系统要能继续运行。我处理为故障状态,温度显示--.-,加热逻辑强制关闭,避免误判低温导致加热棒持续加热。 - 数据日志存储:把最近 24 小时的温度、事件记录写入 AT24C02 外部存储,单片机断电重启后还能调出历史数据。这一条对答辩“系统异常分析”环节非常有说服力,可以直接汇总成表格或曲线。
最后提一个非常实用的验证技巧:先不接真实的传感器和执行器,用三个电位器模拟温度、水位、浑浊度的电压信号接进 ADC 引脚,再用 LED 代替继电器负载来做功能测试。把模拟环境调稳定了,再接真传感器,可把联调时间缩短一半。数码管显示小数时,记住只保留一位有效数字,超过一位会让用户误读为传感器噪声。
本文还有配套的精品资源,点击获取