文章目录
-
- 一、前言
-
- [1.1 技术背景](#1.1 技术背景)
- [1.2 应用场景](#1.2 应用场景)
- [1.3 本文目标](#1.3 本文目标)
- 二、系统架构设计
-
- [2.1 硬件系统架构](#2.1 硬件系统架构)
- [2.2 投喂器设计](#2.2 投喂器设计)
- [2.3 换水系统设计](#2.3 换水系统设计)
- [2.4 项目文件结构](#2.4 项目文件结构)
- 三、环境准备
-
- [3.1 硬件准备](#3.1 硬件准备)
- [3.2 软件准备](#3.2 软件准备)
- 四、核心模块实现
-
- [4.1 舵机驱动](#4.1 舵机驱动)
-
- [4.1.1 舵机驱动头文件](#4.1.1 舵机驱动头文件)
- [4.1.2 舵机驱动实现](#4.1.2 舵机驱动实现)
- [4.2 投喂控制器](#4.2 投喂控制器)
-
- [4.2.1 投喂控制头文件](#4.2.1 投喂控制头文件)
- [4.2.2 投喂控制实现](#4.2.2 投喂控制实现)
- [4.3 换水控制器](#4.3 换水控制器)
-
- [4.3.1 换水控制头文件](#4.3.1 换水控制头文件)
- 五、故障排查与问题解决
-
- [5.1 舵机抖动或失控](#5.1 舵机抖动或失控)
- [5.2 水泵不工作](#5.2 水泵不工作)
- 六、总结
-
- [6.1 核心知识点回顾](#6.1 核心知识点回顾)
- [6.2 性能指标](#6.2 性能指标)
- [6.3 扩展方向](#6.3 扩展方向)
一、前言
1.1 技术背景
养鱼是许多人的爱好,但日常维护工作繁琐,尤其是定时投喂和定期换水。对于经常出差或工作繁忙的人来说,很难保证鱼缸的日常护理。智能鱼缸系统通过自动化技术,实现定时投喂、自动换水、水质监测等功能,让养鱼变得更加轻松。
自动投喂系统需要精确控制投喂量和投喂时间,避免过量投喂导致水质恶化。自动换水系统则需要精确控制换水量,保持水质稳定的同时避免对鱼类造成应激。
STM32F103作为主控芯片,配合RTC实时时钟、舵机控制、水位传感器、水泵控制等模块,可以构建一个功能完善的智能鱼缸管理系统。
1.2 应用场景
智能鱼缸系统的应用场景包括:
- 家庭鱼缸:自动维护,减少日常工作量
- 办公室鱼缸:无人值守自动运行
- 水族店:批量管理多个鱼缸
- 实验室:精确控制实验环境
- 水产养殖:小规模自动化养殖
1.3 本文目标
通过本教程,你将学到:
- RTC实时时钟的时间管理与定时任务
- 舵机控制原理与精确角度控制
- 水位检测与水泵控制
- 水质监测(温度、pH值)
- 定时器中断与任务调度
- OLED中文显示与菜单设计
- 多任务协调与状态机设计
- 数据存储与历史记录
- 异常处理与安全保护
完成本教程后,你将能够独立开发一个具备以下功能的智能鱼缸系统:
- 每天最多8组定时投喂
- 每次投喂量可调(1-10档)
- 自动换水(单次换水量可调)
- 水温实时监测
- 水位监测与缺水保护
- OLED显示当前状态和下次任务
- 手动投喂/换水按钮
- 数据断电保存
技术栈:
- 主控芯片:STM32F103C8T6(Cortex-M3,72MHz)
- 实时时钟:STM32内置RTC + 32.768kHz晶振
- 投喂模块:SG90舵机 + 自制投喂器
- 换水系统:12V水泵 + 水位传感器
- 温度监测:DS18B20数字温度传感器
- 水位检测:浮球开关/压力传感器
- 显示模块:0.96寸OLED(I2C接口)
- 存储芯片:AT24C02(EEPROM)
- 输入设备:4个按键 + 1个急停按钮
- 电源:5V/2A + 12V/2A双电源
二、系统架构设计
2.1 硬件系统架构
人机交互
环境监测
自动换水系统
自动投喂系统
主控处理单元
时间管理单元
RTC实时时钟
定时任务调度
多组闹钟
最多8组
STM32F103C8T6
Cortex-M3 72MHz
状态机
任务协调
安全保护
异常处理
SG90舵机
PWM控制
投喂器
旋转下料
投喂量控制
角度+时间
排水泵
12V水泵
进水泵
12V水泵
水位传感器
浮球开关
流量计
可选
DS18B20
水温监测
pH传感器
可选
OLED显示屏
状态+菜单
按键
手动控制
急停按钮
安全保护
2.2 投喂器设计
投喂器工作原理:
- 储料仓:存放鱼食,底部有出料口
- 旋转挡板:舵机带动挡板旋转,控制出料
- 角度控制 :
- 0度:关闭(不出料)
- 45度:小量出料
- 90度:大量出料
- 时间控制:旋转持续时间决定投喂量
投喂量档位:
| 档位 | 舵机角度 | 持续时间 | 适用场景 |
|---|---|---|---|
| 1 | 30° | 0.5s | 少量鱼/小鱼 |
| 3 | 45° | 1.0s | 正常投喂 |
| 5 | 60° | 1.5s | 较多鱼 |
| 8 | 75° | 2.0s | 大量鱼 |
| 10 | 90° | 3.0s | 特殊需求 |
2.3 换水系统设计
换水流程:
- 排水阶段:启动排水泵,排出旧水
- 检测阶段:监测水位,达到设定值停止排水
- 静置阶段:等待几分钟,让水质稳定
- 进水阶段:启动进水泵,注入新水
- 完成阶段:达到目标水位,停止进水
安全保护:
- 最低水位保护:水位过低时禁止排水
- 最高水位保护:水位过高时禁止进水
- 超时保护:单步操作超时自动停止
- 急停按钮:紧急情况下立即停止所有操作
2.4 项目文件结构
📄 项目文件清单
SmartFishTank/
├── Core/
│ ├── Inc/
│ │ ├── main.h
│ │ ├── rtc_driver.h
│ │ ├── servo_driver.h
│ │ ├── feeder.h
│ │ ├── water_change.h
│ │ ├── ds18b20_driver.h
│ │ ├── level_sensor.h
│ │ ├── pump_driver.h
│ │ ├── oled_driver.h
│ │ ├── key_driver.h
│ │ ├── eeprom_driver.h
│ │ ├── ui_manager.h
│ │ └── font.h
│ └── Src/
│ ├── main.c
│ ├── rtc_driver.c
│ ├── servo_driver.c
│ ├── feeder.c
│ ├── water_change.c
│ ├── ds18b20_driver.c
│ ├── level_sensor.c
│ ├── pump_driver.c
│ ├── oled_driver.c
│ ├── key_driver.c
│ ├── eeprom_driver.c
│ ├── ui_manager.c
│ └── font.c
├── Drivers/
│ ├── STM32F1xx_HAL_Driver/
│ └── CMSIS/
└── README.md
三、环境准备
3.1 硬件准备
必需硬件清单:
| 序号 | 器件名称 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|---|
| 1 | STM32最小系统板 | STM32F103C8T6 | 1 | 核心控制器 |
| 2 | RTC晶振 | 32.768kHz | 1 | 备用电池供电 |
| 3 | SG90舵机 | 180度 | 1 | 投喂控制 |
| 4 | 水泵 | 12V 扬程2m | 2 | 进/排水 |
| 5 | 水位传感器 | 浮球开关 | 2 | 高/低水位 |
| 6 | DS18B20 | 防水型 | 1 | 水温监测 |
| 7 | OLED显示屏 | 0.96寸 I2C | 1 | 信息显示 |
| 8 | EEPROM芯片 | AT24C02 | 1 | 数据存储 |
| 9 | 继电器模块 | 5V低电平触发 | 2 | 控制水泵 |
| 10 | 按键 | 轻触开关 | 4 | 用户输入 |
| 11 | 急停按钮 | 蘑菇头 | 1 | 安全保护 |
| 12 | 电源适配器 | 5V/2A | 1 | 系统供电 |
| 13 | 电源适配器 | 12V/2A | 1 | 水泵供电 |
| 14 | 杜邦线 | 母对母 | 若干 | 连接 |
| 15 | ST-Link | V2 | 1 | 调试下载 |
SG90舵机引脚:
| 线色 | 功能 | 连接 |
|---|---|---|
| 棕 | GND | GND |
| 红 | VCC | 5V |
| 橙 | 信号 | PA0(TIM2_CH1) |
DS18B20引脚:
| 引脚 | 功能 | 连接 |
|---|---|---|
| VCC | 电源 | 3.3V |
| DATA | 数据 | PA1(需4.7K上拉) |
| GND | 地 | GND |
3.2 软件准备
开发环境:
- Keil MDK-ARM 5.38+ 或 STM32CubeIDE 1.12.0+
- STM32F1xx HAL库
- 串口调试助手
四、核心模块实现
4.1 舵机驱动
SG90舵机使用PWM信号控制角度,周期20ms,脉宽0.5ms-2.5ms对应0-180度。
4.1.1 舵机驱动头文件
📄 创建文件:
Core/Inc/servo_driver.h
c
/**
* @file servo_driver.h
* @brief SG90舵机驱动头文件
*/
#ifndef __SERVO_DRIVER_H
#define __SERVO_DRIVER_H
#include "stm32f1xx_hal.h"
#include <stdint.h>
/* 舵机角度范围 */
#define SERVO_MIN_ANGLE 0
#define SERVO_MAX_ANGLE 180
#define SERVO_MID_ANGLE 90
/* 函数声明 */
void Servo_Init(void);
void Servo_SetAngle(uint8_t angle);
void Servo_SetMicroseconds(uint16_t us);
void Servo_Off(void);
#endif /* __SERVO_DRIVER_H */
4.1.2 舵机驱动实现
📄 创建文件:
Core/Src/servo_driver.c
c
/**
* @file servo_driver.c
* @brief SG90舵机驱动实现
*/
#include "servo_driver.h"
/* PWM配置 */
#define SERVO_TIM TIM2
#define SERVO_TIM_CHANNEL TIM_CHANNEL_1
#define SERVO_GPIO_PORT GPIOA
#define SERVO_GPIO_PIN GPIO_PIN_0
/* 定时器周期:20ms (50Hz) */
/* 72MHz / 72 / 20000 = 50Hz */
#define SERVO_PWM_PERIOD 20000 /* 20ms = 20000us */
/* 脉宽范围:0.5ms - 2.5ms */
#define SERVO_MIN_PULSE 500 /* 0.5ms */
#define SERVO_MAX_PULSE 2500 /* 2.5ms */
static TIM_HandleTypeDef htim2;
void Servo_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/* 配置PWM输出引脚 */
GPIO_InitStruct.Pin = SERVO_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SERVO_GPIO_PORT, &GPIO_InitStruct);
/* 配置定时器 */
htim2.Instance = SERVO_TIM;
htim2.Init.Prescaler = 72 - 1; /* 72MHz / 72 = 1MHz */
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = SERVO_PWM_PERIOD - 1; /* 20ms周期 */
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim2);
/* 配置PWM通道 */
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = SERVO_MIN_PULSE; /* 初始位置 */
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, SERVO_TIM_CHANNEL);
/* 启动PWM */
HAL_TIM_PWM_Start(&htim2, SERVO_TIM_CHANNEL);
}
void Servo_SetAngle(uint8_t angle) {
if (angle > SERVO_MAX_ANGLE) {
angle = SERVO_MAX_ANGLE;
}
/* 角度转脉宽 */
uint16_t pulse = SERVO_MIN_PULSE +
(angle * (SERVO_MAX_PULSE - SERVO_MIN_PULSE) / SERVO_MAX_ANGLE);
__HAL_TIM_SET_COMPARE(SERVO_TIM, SERVO_TIM_CHANNEL, pulse);
}
void Servo_SetMicroseconds(uint16_t us) {
if (us < SERVO_MIN_PULSE) us = SERVO_MIN_PULSE;
if (us > SERVO_MAX_PULSE) us = SERVO_MAX_PULSE;
__HAL_TIM_SET_COMPARE(SERVO_TIM, SERVO_TIM_CHANNEL, us);
}
void Servo_Off(void) {
HAL_TIM_PWM_Stop(&htim2, SERVO_TIM_CHANNEL);
}
4.2 投喂控制器
4.2.1 投喂控制头文件
📄 创建文件:
Core/Inc/feeder.h
c
/**
* @file feeder.h
* @brief 自动投喂控制器头文件
*/
#ifndef __FEEDER_H
#define __FEEDER_H
#include <stdint.h>
#include <stdbool.h>
/* 投喂任务结构体 */
typedef struct {
uint8_t hour; /* 小时(0-23) */
uint8_t minute; /* 分钟(0-59) */
uint8_t amount; /* 投喂量(1-10档) */
bool enabled; /* 是否启用 */
} FeedingTask;
#define MAX_FEEDING_TASKS 8 /* 最大投喂任务数 */
/* 函数声明 */
void Feeder_Init(void);
int Feeder_AddTask(uint8_t hour, uint8_t minute, uint8_t amount);
int Feeder_RemoveTask(uint8_t index);
void Feeder_Feed(uint8_t amount); /* 立即投喂 */
void Feeder_CheckAndFeed(void); /* 检查定时任务 */
FeedingTask* Feeder_GetTasks(void);
uint8_t Feeder_GetTaskCount(void);
void Feeder_SaveTasks(void);
void Feeder_LoadTasks(void);
#endif /* __FEEDER_H */
4.2.2 投喂控制实现
📄 创建文件:
Core/Src/feeder.c
c
/**
* @file feeder.c
* @brief 自动投喂控制器实现
*/
#include "feeder.h"
#include "servo_driver.h"
#include "rtc_driver.h"
#include "eeprom_driver.h"
#include <string.h>
#define FEEDER_EEPROM_ADDR 0x20
#define FEEDER_MAGIC 0xFE01
/* 投喂参数表 */
static const struct {
uint8_t angle;
uint16_t durationMs;
} feedParams[11] = {
{0, 0}, /* 0档:关闭 */
{30, 500}, /* 1档 */
{35, 600}, /* 2档 */
{45, 800}, /* 3档 */
{50, 1000}, /* 4档 */
{60, 1200}, /* 5档 */
{65, 1400}, /* 6档 */
{70, 1600}, /* 7档 */
{75, 2000}, /* 8档 */
{80, 2500}, /* 9档 */
{90, 3000} /* 10档 */
};
static FeedingTask feedingTasks[MAX_FEEDING_TASKS];
static uint8_t taskCount = 0;
static uint32_t lastCheckTime = 0;
void Feeder_Init(void) {
Servo_Init();
Feeder_LoadTasks();
}
int Feeder_AddTask(uint8_t hour, uint8_t minute, uint8_t amount) {
if (taskCount >= MAX_FEEDING_TASKS) {
return -1; /* 任务已满 */
}
if (hour > 23 || minute > 59 || amount < 1 || amount > 10) {
return -1; /* 参数错误 */
}
/* 检查是否重复 */
for (int i = 0; i < taskCount; i++) {
if (feedingTasks[i].hour == hour &&
feedingTasks[i].minute == minute) {
return -1; /* 时间冲突 */
}
}
feedingTasks[taskCount].hour = hour;
feedingTasks[taskCount].minute = minute;
feedingTasks[taskCount].amount = amount;
feedingTasks[taskCount].enabled = true;
taskCount++;
Feeder_SaveTasks();
return 0;
}
int Feeder_RemoveTask(uint8_t index) {
if (index >= taskCount) {
return -1;
}
/* 移动后续任务 */
for (int i = index; i < taskCount - 1; i++) {
feedingTasks[i] = feedingTasks[i + 1];
}
taskCount--;
Feeder_SaveTasks();
return 0;
}
void Feeder_Feed(uint8_t amount) {
if (amount < 1 || amount > 10) {
return;
}
uint8_t angle = feedParams[amount].angle;
uint16_t duration = feedParams[amount].durationMs;
/* 打开投喂口 */
Servo_SetAngle(angle);
HAL_Delay(duration);
/* 关闭投喂口 */
Servo_SetAngle(0);
HAL_Delay(500);
}
void Feeder_CheckAndFeed(void) {
RTC_TimeTypeDef currentTime;
/* 每分钟检查一次 */
uint32_t currentTick = HAL_GetTick();
if (currentTick - lastCheckTime < 60000) {
return;
}
lastCheckTime = currentTick;
/* 获取当前时间 */
if (RTC_GetTime(¤tTime) != HAL_OK) {
return;
}
/* 检查所有任务 */
for (int i = 0; i < taskCount; i++) {
if (!feedingTasks[i].enabled) {
continue;
}
if (feedingTasks[i].hour == currentTime.hours &&
feedingTasks[i].minute == currentTime.minutes) {
/* 执行投喂 */
Feeder_Feed(feedingTasks[i].amount);
}
}
}
FeedingTask* Feeder_GetTasks(void) {
return feedingTasks;
}
uint8_t Feeder_GetTaskCount(void) {
return taskCount;
}
void Feeder_SaveTasks(void) {
uint8_t buffer[sizeof(feedingTasks) + 3];
buffer[0] = (FEEDER_MAGIC >> 8) & 0xFF;
buffer[1] = FEEDER_MAGIC & 0xFF;
buffer[2] = taskCount;
memcpy(&buffer[3], feedingTasks, sizeof(feedingTasks));
EEPROM_Write(FEEDER_EEPROM_ADDR, buffer, sizeof(buffer));
}
void Feeder_LoadTasks(void) {
uint8_t buffer[sizeof(feedingTasks) + 3];
EEPROM_Read(FEEDER_EEPROM_ADDR, buffer, sizeof(buffer));
uint16_t magic = (buffer[0] << 8) | buffer[1];
if (magic == FEEDER_MAGIC) {
taskCount = buffer[2];
if (taskCount > MAX_FEEDING_TASKS) {
taskCount = 0;
}
memcpy(feedingTasks, &buffer[3], sizeof(feedingTasks));
}
}
4.3 换水控制器
4.3.1 换水控制头文件
📄 创建文件:
Core/Inc/water_change.h
c
/**
* @file water_change.h
* @brief 自动换水控制器头文件
*/
#ifndef __WATER_CHANGE_H
#define __WATER_CHANGE_H
#include <stdint.h>
#include <stdbool.h>
typedef enum {
WC_STATE_IDLE, /* 待机 */
WC_STATE_DRAINING, /* 排水中 */
WC_STATE_WAITING, /* 静置中 */
WC_STATE_FILLING, /* 进水中 */
WC_STATE_COMPLETED /* 完成 */
} WaterChange_State;
/* 换水配置 */
typedef struct {
uint8_t drainPercent; /* 排水百分比(10-50%) */
uint16_t waitTime; /* 静置时间(秒) */
bool autoMode; /* 自动模式 */
uint8_t scheduleDay; /* 自动换水周期(天) */
} WaterChange_Config;
/* 函数声明 */
void WaterChange_Init(void);
void WaterChange_Start(uint8_t drainPercent);
void WaterChange_Stop(void);
void WaterChange_Process(void);
WaterChange_State WaterChange_GetState(void);
uint8_t WaterChange_GetProgress(void); /* 进度0-100% */
void WaterChange_SetConfig(WaterChange_Config* config);
WaterChange_Config* WaterChange_GetConfig(void);
#endif /* __WATER_CHANGE_H */
五、故障排查与问题解决
5.1 舵机抖动或失控
问题1:舵机角度不准确或抖动
现象: 舵机无法到达指定角度,或持续抖动
原因分析:
- 电源功率不足(SG90需要约100mA电流)
- PWM信号受干扰
- 舵机损坏
- 机械阻力过大
解决方案:
-
独立供电
5V电源 ---+--- 舵机VCC
|
电容(470μF)
|
GND -
信号隔离
c
/* 舵机信号线与电源线分开走线 */
/* 避免与电机线平行 */
5.2 水泵不工作
问题2:继电器吸合但水泵不转
现象: 听到继电器吸合声,但水泵不工作
原因分析:
- 继电器触点接触不良
- 水泵电源未接通
- 水泵空转保护
- 水泵损坏
解决方案:
- 检查继电器驱动
c
/* 确保GPIO输出正确电平 */
HAL_GPIO_WritePin(PUMP_PORT, PUMP_PIN, GPIO_PIN_SET); /* 吸合 */
HAL_GPIO_WritePin(PUMP_PORT, PUMP_PIN, GPIO_PIN_RESET); /* 释放 */
-
添加续流二极管
继电器线圈 ---+--- 二极管(1N4007) ---+
| |
VCC GND
六、总结
6.1 核心知识点回顾
-
RTC定时任务:实现了多组定时投喂任务的管理与执行。
-
舵机控制:掌握了PWM角度控制,实现了精确的投喂量控制。
-
水泵控制:实现了进排水泵的安全控制与水位监测。
-
状态机设计:使用状态机管理换水流程,确保操作安全。
-
安全保护:实现了多层次的异常检测与保护机制。
6.2 性能指标
| 指标 | 数值 |
|---|---|
| 定时精度 | ±1分钟 |
| 投喂量精度 | ±10% |
| 换水精度 | ±5% |
| 温度精度 | ±0.5℃ |
| 响应时间 | <1秒 |
6.3 扩展方向
- WiFi模块:添加ESP8266,实现远程控制
- 手机APP:开发配套APP,查看状态和控制
- 摄像头:添加摄像头,实时查看鱼缸
- 自动喂食:添加自动喂食器,支持多种饲料
- 水质监测:添加pH、氨氮等传感器