文章目录
-
- 一、项目概述
-
- [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 系统架构图)
- [2.5 硬件接线总表](#2.5 硬件接线总表)
- 三、开发环境搭建
-
- [3.1 软件准备](#3.1 软件准备)
- [3.2 STM32CubeMX工程创建](#3.2 STM32CubeMX工程创建)
- 四、核心模块代码实现
-
- [4.1 延时函数模块](#4.1 延时函数模块)
- [4.2 DS18B20水温传感器模块](#4.2 DS18B20水温传感器模块)
- [4.3 ADC与水质传感器模块](#4.3 ADC与水质传感器模块)
- [4.4 28BYJ-48步进电机投喂模块](#4.4 28BYJ-48步进电机投喂模块)
- [4.5 继电器与水泵控制模块](#4.5 继电器与水泵控制模块)
- [4.6 OLED显示模块](#4.6 OLED显示模块)
- [4.7 按键与蜂鸣器模块](#4.7 按键与蜂鸣器模块)
- 五、系统主程序
-
- [5.1 系统参数配置](#5.1 系统参数配置)
- [5.2 主程序实现](#5.2 主程序实现)
- [5.3 系统功能实现](#5.3 系统功能实现)
- 六、系统工作流程图
- 七、调试步骤与常见问题
-
- [7.1 硬件调试步骤](#7.1 硬件调试步骤)
-
- [步骤1: 电源检查](#步骤1: 电源检查)
- [步骤2: 通信接口测试](#步骤2: 通信接口测试)
- [步骤3: 执行器测试](#步骤3: 执行器测试)
- [7.2 传感器校准](#7.2 传感器校准)
- [7.3 常见问题与解决方案](#7.3 常见问题与解决方案)
- [7.4 性能优化建议](#7.4 性能优化建议)
- 八、项目扩展建议
-
- [8.1 功能扩展](#8.1 功能扩展)
- [8.2 硬件升级](#8.2 硬件升级)
- 总结
一、项目概述
1.1 项目背景
观赏鱼养殖的核心是稳定的水体环境,水温、水位、水质的波动会直接影响鱼类存活。传统养鱼需要人工定期投喂、换水、监测水质,不仅耗时费力,而且容易因疏忽导致水质恶化。本项目基于STM32F103C8T6微控制器,打造一套集水质监测、自动投喂、自动换水于一体的智能鱼缸管理系统,让养鱼变得更加轻松、科学。
1.2 核心功能
| 功能模块 | 功能描述 | 技术指标 |
|---|---|---|
| 水质监测 | 实时监测水温、pH值、TDS值、溶解氧 | 水温精度±0.5℃,pH精度±0.1 |
| 自动投喂 | 定时定量自动投喂,支持多组定时 | 最多8组定时任务,投喂量可调 |
| 自动换水 | 根据水质参数自动触发换水 | 支持换水百分比设置,带超时保护 |
| 水位监测 | 高低水位检测,防干烧保护 | 浮球开关/超声波传感器可选 |
| 本地显示 | OLED屏实时显示所有参数和状态 | 0.96寸I2C接口,128×64分辨率 |
| 数据存储 | 参数掉电保存,重启无需重新设置 | EEPROM/Flash存储 |
| 报警功能 | 参数异常时声光报警 | 两级报警机制 |
1.3 系统总成本
硬件整体参考总成本约150-200元,极具性价比,是STM32进阶学习的经典综合项目。
二、硬件选型与系统架构
2.1 主控芯片选型
STM32F103C8T6
- 内核:ARM Cortex-M3,主频72MHz
- 存储:64KB Flash,20KB SRAM
- 接口:3个USART、2个SPI、2个I2C、1个12位ADC(16通道)
- 定时器:4个通用定时器,支持PWM输出
- 工作电压:2.0-3.6V
- 封装:LQFP48
选型理由:性能强劲,接口丰富,资料完善,价格亲民(约12元),非常适合中小型智能设备开发。
2.2 传感器模块选型
| 传感器名称 | 型号 | 功能 | 接口 | 价格 |
|---|---|---|---|---|
| 水温传感器 | DS18B20 | 测量鱼缸水温 | 单总线 | 4.8元 |
| pH传感器 | pH-016 | 测量水体酸碱度 | 模拟ADC | 18元 |
| TDS传感器 | TDS-001 | 测量总溶解固体 | 模拟ADC | 15元 |
| 溶解氧传感器 | DO-600 | 测量水体溶氧量 | 模拟ADC | 35元 |
| 水位传感器 | 浮球开关/HC-SR04 | 监测水位高度 | GPIO/超声波 | 3.8元 |
2.3 执行器模块选型
| 执行器名称 | 型号 | 功能 | 驱动方式 | 价格 |
|---|---|---|---|---|
| 步进电机 | 28BYJ-48 | 自动投喂机构驱动 | ULN2003驱动板 | 6.5元 |
| 微型潜水泵 | 12V静音泵 | 进排水控制 | 继电器模块 | 15元/个 |
| 继电器模块 | 4路光电隔离 | 控制水泵等设备 | GPIO | 14元 |
| OLED显示屏 | 0.96寸I2C | 参数显示 | I2C | 7.5元 |
| 蜂鸣器 | 有源蜂鸣器 | 报警提示 | GPIO | 2元 |
2.4 系统架构图
交互层
执行层
控制层
感知层
智能鱼缸系统架构
DS18B20水温传感器
pH值传感器
TDS浊度传感器
溶解氧传感器
水位传感器
STM32F103C8T6主控
电源管理模块
EEPROM存储
28BYJ-48步进电机
投喂机构
进水泵
排水泵
加热棒
增氧泵
OLED显示屏
按键模块
蜂鸣器报警
串口通信
2.5 硬件接线总表
| STM32引脚 | 连接模块 | 信号说明 |
|---|---|---|
| PA0 | pH传感器 | ADC输入,pH模拟信号 |
| PA1 | TDS传感器 | ADC输入,TDS模拟信号 |
| PA2 | 溶解氧传感器 | ADC输入,DO模拟信号 |
| PA3 | DS18B20 | 单总线数据 |
| PA4 | 继电器IN1 | 进水泵控制 |
| PA5 | 继电器IN2 | 排水泵控制 |
| PA6 | 继电器IN3 | 加热棒控制 |
| PA7 | 继电器IN4 | 增氧泵控制 |
| PB0 | ULN2003 IN1 | 步进电机控制1 |
| PB1 | ULN2003 IN2 | 步进电机控制2 |
| PB2 | ULN2003 IN3 | 步进电机控制3 |
| PB3 | ULN2003 IN4 | 步进电机控制4 |
| PB6 | OLED SCL | I2C时钟 |
| PB7 | OLED SDA | I2C数据 |
| PB8 | 蜂鸣器 | 报警输出 |
| PB9 | 按键1 | 设置键 |
| PB10 | 按键2 | 加键 |
| PB11 | 按键3 | 减键 |
| PB12 | 高水位检测 | 输入上拉 |
| PB13 | 低水位检测 | 输入上拉 |
| 5V | 所有模块VCC | 电源正极 |
| GND | 所有模块GND | 电源地 |
三、开发环境搭建
3.1 软件准备
- Keil MDK-ARM 5.38 - 主要开发环境
- STM32CubeMX 6.8.1 - 图形化配置工具
- CH340驱动 - USB转串口驱动
- 串口助手 - 串口调试工具
3.2 STM32CubeMX工程创建
步骤1:选择芯片
打开STM32CubeMX,选择STM32F103C8T6芯片,点击Start Project。
步骤2:配置系统时钟
SYSCLK: 72MHz
HSE: 8MHz外部晶振
PLL: 9倍频
AHB Prescaler: 1
APB1 Prescaler: 2
APB2 Prescaler: 1
步骤3:配置外设
GPIO配置:
- PA0-PA2: 模拟输入(ADC)
- PA3: 推挽输出(DS18B20)
- PA4-PA7: 推挽输出(继电器)
- PB0-PB3: 推挽输出(步进电机)
- PB6-PB7: I2C(OLED)
- PB8: 推挽输出(蜂鸣器)
- PB9-PB11: 输入上拉(按键)
- PB12-PB13: 输入上拉(水位检测)
ADC配置:
- 使能ADC1,通道0、1、2
- 采样时间:239.5周期
- 右对齐,单次转换模式
I2C配置:
- 使能I2C1
- 速度模式:标准模式(100kHz)
USART配置(可选,用于调试):
- 使能USART1
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验:无
步骤4:生成代码
点击GENERATE CODE,选择MDK-ARM V5作为工具链,生成工程。
四、核心模块代码实现
4.1 延时函数模块
📄 创建文件:Core/Inc/delay.h
c
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32f1xx_hal.h"
void delay_init(uint8_t SYSCLK);
void delay_ms(uint16_t nms);
void delay_us(uint32_t nus);
#endif /* __DELAY_H */
📄 创建文件:Core/Src/delay.c
c
#include "delay.h"
static uint8_t fac_us = 0;
static uint16_t fac_ms = 0;
/**
* @brief 延时函数初始化
* @param SYSCLK: 系统时钟频率(MHz)
* @retval None
*/
void delay_init(uint8_t SYSCLK)
{
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
fac_us = SYSCLK;
fac_ms = (uint16_t)SYSCLK * 1000;
}
/**
* @brief 微秒级延时
* @param nus: 延时微秒数
* @retval None
* @note 最大延时: 2^24 / 72 ≈ 233015us
*/
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD;
ticks = nus * fac_us;
told = SysTick->VAL;
while(1)
{
tnow = SysTick->VAL;
if(tnow != told)
{
if(tnow < told) tcnt += told - tnow;
else tcnt += reload - tnow + told;
told = tnow;
if(tcnt >= ticks) break;
}
}
}
/**
* @brief 毫秒级延时
* @param nms: 延时毫秒数
* @retval None
* @note 最大延时: 2^24 / 72000 ≈ 233ms
*/
void delay_ms(uint16_t nms)
{
uint32_t ticks;
uint32_t told, tnow, tcnt = 0;
uint32_t reload = SysTick->LOAD;
ticks = nms * fac_ms;
told = SysTick->VAL;
while(1)
{
tnow = SysTick->VAL;
if(tnow != told)
{
if(tnow < told) tcnt += told - tnow;
else tcnt += reload - tnow + told;
told = tnow;
if(tcnt >= ticks) break;
}
}
}
4.2 DS18B20水温传感器模块
📄 创建文件:Core/Inc/ds18b20.h
c
#ifndef __DS18B20_H
#define __DS18B20_H
#include "stm32f1xx_hal.h"
#include "delay.h"
/* DS18B20 引脚定义 */
#define DS18B20_GPIO_PORT GPIOA
#define DS18B20_GPIO_PIN GPIO_PIN_3
#define DS18B20_GPIO_CLK() __HAL_RCC_GPIOA_CLK_ENABLE()
/* DS18B20 操作宏 */
#define DS18B20_OUT() do{GPIOA->CRL &= 0xFFFF0FFF; GPIOA->CRL |= 0x00003000;}while(0)
#define DS18B20_IN() do{GPIOA->CRL &= 0xFFFF0FFF; GPIOA->CRL |= 0x00008000;}while(0)
#define DS18B20_DQ_LOW() HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_RESET)
#define DS18B20_DQ_HIGH() HAL_GPIO_WritePin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN, GPIO_PIN_SET)
#define DS18B20_DQ_READ() HAL_GPIO_ReadPin(DS18B20_GPIO_PORT, DS18B20_GPIO_PIN)
/* 函数声明 */
uint8_t DS18B20_Init(void);
float DS18B20_GetTemperature(void);
#endif /* __DS18B20_H */
📄 创建文件:Core/Src/ds18b20.c
c
#include "ds18b20.h"
/**
* @brief 复位DS18B20
* @retval 0: 存在, 1: 不存在
*/
static uint8_t DS18B20_Reset(void)
{
uint8_t status;
DS18B20_OUT(); /* 设置为输出模式 */
DS18B20_DQ_LOW(); /* 拉低DQ */
delay_us(480); /* 拉低至少480us */
DS18B20_DQ_HIGH(); /* 释放总线 */
delay_us(60); /* 等待15~60us */
DS18B20_IN(); /* 设置为输入模式 */
status = DS18B20_DQ_READ(); /* 读取存在脉冲 */
delay_us(420); /* 等待时序结束 */
return status;
}
/**
* @brief DS18B20写一个字节
* @param data: 要写入的数据
* @retval None
*/
static void DS18B20_WriteByte(uint8_t data)
{
uint8_t i;
DS18B20_OUT();
for(i = 0; i < 8; i++)
{
DS18B20_DQ_LOW();
delay_us(1);
if(data & 0x01)
DS18B20_DQ_HIGH();
else
DS18B20_DQ_LOW();
delay_us(60);
DS18B20_DQ_HIGH();
delay_us(2);
data >>= 1;
}
}
/**
* @brief DS18B20读一个字节
* @retval 读取到的数据
*/
static uint8_t DS18B20_ReadByte(void)
{
uint8_t i, data = 0;
for(i = 0; i < 8; i++)
{
DS18B20_OUT();
DS18B20_DQ_LOW();
delay_us(1);
DS18B20_DQ_HIGH();
DS18B20_IN();
delay_us(12);
data >>= 1;
if(DS18B20_DQ_READ())
data |= 0x80;
delay_us(50);
}
return data;
}
/**
* @brief DS18B20初始化
* @retval 0: 成功, 1: 失败
*/
uint8_t DS18B20_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
DS18B20_GPIO_CLK();
GPIO_InitStruct.Pin = DS18B20_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(DS18B20_GPIO_PORT, &GPIO_InitStruct);
DS18B20_DQ_HIGH();
return DS18B20_Reset();
}
/**
* @brief 获取温度值
* @retval 温度值(摄氏度)
*/
float DS18B20_GetTemperature(void)
{
uint8_t tempH, tempL;
int16_t temp;
float temperature;
if(DS18B20_Reset() == 1)
return 999.0f; /* 读取失败返回异常值 */
DS18B20_WriteByte(0xCC); /* 跳过ROM指令 */
DS18B20_WriteByte(0x44); /* 启动温度转换 */
delay_ms(750); /* 等待转换完成, 12位精度需要750ms */
DS18B20_Reset();
DS18B20_WriteByte(0xCC); /* 跳过ROM指令 */
DS18B20_WriteByte(0xBE); /* 读取暂存器 */
tempL = DS18B20_ReadByte();
tempH = DS18B20_ReadByte();
temp = (tempH << 8) | tempL;
if(temp < 0)
{
temp = ~temp + 1;
temperature = (float)temp * (-0.0625f);
}
else
{
temperature = (float)temp * 0.0625f;
}
return temperature;
}
4.3 ADC与水质传感器模块
📄 创建文件:Core/Inc/adc_sensor.h
c
#ifndef __ADC_SENSOR_H
#define __ADC_SENSOR_H
#include "stm32f1xx_hal.h"
#include "delay.h"
/* ADC通道定义 */
#define ADC_PH_CHANNEL ADC_CHANNEL_0 /* PA0 - pH传感器 */
#define ADC_TDS_CHANNEL ADC_CHANNEL_1 /* PA1 - TDS传感器 */
#define ADC_DO_CHANNEL ADC_CHANNEL_2 /* PA2 - 溶解氧传感器 */
/* 校准参数 (需要根据实际传感器校准) */
#define PH_4_VOLTAGE 1.80f /* pH=4.00时的电压(V) */
#define PH_7_VOLTAGE 1.50f /* pH=7.00时的电压(V) */
#define PH_9_VOLTAGE 1.30f /* pH=9.18时的电压(V) */
#define TDS_REF_VOLTAGE 3.3f /* TDS参考电压 */
#define TDS_TEMP_COEFF 0.02f /* TDS温度系数 */
/* 函数声明 */
void ADC_Sensor_Init(void);
uint16_t ADC_GetValue(uint32_t channel, uint8_t times);
float ADC_GetVoltage(uint32_t channel);
float PH_GetValue(float temperature);
float TDS_GetValue(float temperature);
float DO_GetValue(float temperature);
float MovingAverageFilter(float newValue, float* buffer, uint8_t size);
#endif /* __ADC_SENSOR_H */
📄 创建文件:Core/Src/adc_sensor.c
c
#include "adc_sensor.h"
extern ADC_HandleTypeDef hadc1;
/* 滤波缓冲区 */
static float ph_buffer[5] = {0};
static float tds_buffer[5] = {0};
static float do_buffer[5] = {0};
static uint8_t filter_index = 0;
/**
* @brief ADC传感器初始化
* @retval None
*/
void ADC_Sensor_Init(void)
{
/* ADC已在CubeMX中初始化, 此处仅初始化滤波缓冲区 */
for(uint8_t i = 0; i < 5; i++)
{
ph_buffer[i] = 7.0f;
tds_buffer[i] = 0.0f;
do_buffer[i] = 8.0f;
}
}
/**
* @brief 获取ADC平均值
* @param channel: ADC通道
* @param times: 采样次数
* @retval ADC平均值
*/
uint16_t ADC_GetValue(uint32_t channel, uint8_t times)
{
uint32_t sum = 0;
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = channel;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
for(uint8_t i = 0; i < times; i++)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 10);
sum += HAL_ADC_GetValue(&hadc1);
delay_us(100);
}
return sum / times;
}
/**
* @brief 获取ADC电压值
* @param channel: ADC通道
* @retval 电压值(V)
*/
float ADC_GetVoltage(uint32_t channel)
{
uint16_t adc_value = ADC_GetValue(channel, 10);
return (float)adc_value * 3.3f / 4096.0f;
}
/**
* @brief 移动平均滤波器
* @param newValue: 新的采样值
* @param buffer: 滤波缓冲区
* @param size: 缓冲区大小
* @retval 滤波后的值
*/
float MovingAverageFilter(float newValue, float* buffer, uint8_t size)
{
float sum = 0;
buffer[filter_index % size] = newValue;
for(uint8_t i = 0; i < size; i++)
{
sum += buffer[i];
}
return sum / size;
}
/**
* @brief 获取pH值
* @param temperature: 当前水温(用于温度补偿)
* @retval pH值
*/
float PH_GetValue(float temperature)
{
float voltage = ADC_GetVoltage(ADC_PH_CHANNEL);
float ph_value;
/* 两点校准法 (pH7和pH4) */
float slope = (4.0f - 7.0f) / (PH_4_VOLTAGE - PH_7_VOLTAGE);
ph_value = 7.0f + slope * (voltage - PH_7_VOLTAGE);
/* 温度补偿 (简化模型) */
ph_value = ph_value + (25.0f - temperature) * 0.003f;
/* 滤波 */
ph_value = MovingAverageFilter(ph_value, ph_buffer, 5);
/* 范围限制 */
if(ph_value > 14.0f) ph_value = 14.0f;
if(ph_value < 0.0f) ph_value = 0.0f;
return ph_value;
}
/**
* @brief 获取TDS值
* @param temperature: 当前水温(用于温度补偿)
* @retval TDS值(ppm)
*/
float TDS_GetValue(float temperature)
{
float voltage = ADC_GetVoltage(ADC_TDS_CHANNEL);
float tds_value;
/* TDS计算公式 */
float compensationCoefficient = 1.0f + 0.02f * (temperature - 25.0f);
float compensationVoltage = voltage / compensationCoefficient;
/* 转换为ppm (根据传感器特性调整系数) */
tds_value = (133.42f * compensationVoltage * compensationVoltage * compensationVoltage -
255.86f * compensationVoltage * compensationVoltage +
857.39f * compensationVoltage) * 0.5f;
/* 滤波 */
tds_value = MovingAverageFilter(tds_value, tds_buffer, 5);
/* 范围限制 */
if(tds_value < 0.0f) tds_value = 0.0f;
return tds_value;
}
/**
* @brief 获取溶解氧值
* @param temperature: 当前水温(用于温度补偿)
* @retval 溶解氧值(mg/L)
*/
float DO_GetValue(float temperature)
{
float voltage = ADC_GetVoltage(ADC_DO_CHANNEL);
float do_value;
/* 溶解氧计算公式 (根据传感器特性调整) */
/* 假设传感器输出0-3V对应0-20mg/L */
do_value = voltage * 6.6667f;
/* 温度补偿 (饱和溶氧量随温度升高而降低) */
float saturation = 14.64f - 0.398f * temperature +
0.008f * temperature * temperature -
0.00006f * temperature * temperature * temperature;
/* 简化的补偿模型 */
do_value = do_value * (saturation / 8.24f);
/* 滤波 */
do_value = MovingAverageFilter(do_value, do_buffer, 5);
/* 范围限制 */
if(do_value < 0.0f) do_value = 0.0f;
if(do_value > 20.0f) do_value = 20.0f;
return do_value;
}
4.4 28BYJ-48步进电机投喂模块
📄 创建文件:Core/Inc/stepper.h
c
#ifndef __STEPPER_H
#define __STEPPER_H
#include "stm32f1xx_hal.h"
#include "delay.h"
/* 步进电机引脚定义 - ULN2003驱动板 */
#define STEPPER_IN1_PORT GPIOB
#define STEPPER_IN1_PIN GPIO_PIN_0
#define STEPPER_IN2_PORT GPIOB
#define STEPPER_IN2_PIN GPIO_PIN_1
#define STEPPER_IN3_PORT GPIOB
#define STEPPER_IN3_PIN GPIO_PIN_2
#define STEPPER_IN4_PORT GPIOB
#define STEPPER_IN4_PIN GPIO_PIN_3
/* 步进电机参数 */
#define STEPPER_SPEED 2000 /* 步进速度(us), 越小越快 */
#define STEPPER_PER_REV 4096 /* 每转步数 (4相8拍, 64减速比) */
#define FEED_STEPS 512 /* 每次投喂步数 (约1/8圈) */
/* 方向定义 */
#define STEPPER_CW 0 /* 顺时针 */
#define STEPPER_CCW 1 /* 逆时针 */
/* 函数声明 */
void Stepper_Init(void);
void Stepper_Step(uint8_t direction, uint32_t steps);
void Stepper_Feed(uint8_t feed_times);
void Stepper_Stop(void);
#endif /* __STEPPER_H */
📄 创建文件:Core/Src/stepper.c
c
#include "stepper.h"
/* 4相8拍步进序列 */
static const uint8_t step_sequence[8][4] =
{
{1, 0, 0, 0}, /* A */
{1, 1, 0, 0}, /* AB */
{0, 1, 0, 0}, /* B */
{0, 1, 1, 0}, /* BC */
{0, 0, 1, 0}, /* C */
{0, 0, 1, 1}, /* CD */
{0, 0, 0, 1}, /* D */
{1, 0, 0, 1} /* DA */
};
static volatile uint8_t stepper_running = 0;
/**
* @brief 步进电机GPIO初始化
* @retval None
*/
void Stepper_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 配置IN1-IN4为推挽输出 */
GPIO_InitStruct.Pin = STEPPER_IN1_PIN | STEPPER_IN2_PIN |
STEPPER_IN3_PIN | STEPPER_IN4_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(STEPPER_IN1_PORT, &GPIO_InitStruct);
/* 初始状态: 所有引脚低电平 */
Stepper_Stop();
}
/**
* @brief 步进电机停止
* @retval None
*/
void Stepper_Stop(void)
{
HAL_GPIO_WritePin(STEPPER_IN1_PORT, STEPPER_IN1_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(STEPPER_IN2_PORT, STEPPER_IN2_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(STEPPER_IN3_PORT, STEPPER_IN3_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(STEPPER_IN4_PORT, STEPPER_IN4_PIN, GPIO_PIN_RESET);
stepper_running = 0;
}
/**
* @brief 设置单步状态
* @param step_index: 步数索引(0-7)
* @retval None
*/
static void Stepper_SetStep(uint8_t step_index)
{
HAL_GPIO_WritePin(STEPPER_IN1_PORT, STEPPER_IN1_PIN,
step_sequence[step_index][0] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(STEPPER_IN2_PORT, STEPPER_IN2_PIN,
step_sequence[step_index][1] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(STEPPER_IN3_PORT, STEPPER_IN3_PIN,
step_sequence[step_index][2] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(STEPPER_IN4_PORT, STEPPER_IN4_PIN,
step_sequence[step_index][3] ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
/**
* @brief 步进电机转动指定步数
* @param direction: 方向(STEPPER_CW/STEPPER_CCW)
* @param steps: 步数
* @retval None
*/
void Stepper_Step(uint8_t direction, uint32_t steps)
{
int8_t step_dir = (direction == STEPPER_CW) ? 1 : -1;
int16_t current_step = 0;
stepper_running = 1;
for(uint32_t i = 0; i < steps && stepper_running; i++)
{
Stepper_SetStep(current_step);
delay_us(STEPPER_SPEED);
current_step += step_dir;
/* 边界处理 */
if(current_step >= 8) current_step = 0;
if(current_step < 0) current_step = 7;
}
Stepper_Stop();
}
/**
* @brief 执行投喂操作
* @param feed_times: 投喂次数(每次约1/8圈)
* @retval None
*/
void Stepper_Feed(uint8_t feed_times)
{
for(uint8_t i = 0; i < feed_times; i++)
{
/* 顺时针转动投喂 */
Stepper_Step(STEPPER_CW, FEED_STEPS);
delay_ms(100);
/* 反转回位, 防止饲料卡滞 */
Stepper_Step(STEPPER_CCW, FEED_STEPS / 4);
delay_ms(500);
}
}
4.5 继电器与水泵控制模块
📄 创建文件:Core/Inc/relay_pump.h
c
#ifndef __RELAY_PUMP_H
#define __RELAY_PUMP_H
#include "stm32f1xx_hal.h"
/* 继电器引脚定义 */
#define RELAY_IN_PORT GPIOA
#define RELAY_IN1_PIN GPIO_PIN_4 /* 进水泵 */
#define RELAY_IN2_PIN GPIO_PIN_5 /* 排水泵 */
#define RELAY_IN3_PIN GPIO_PIN_6 /* 加热棒 */
#define RELAY_IN4_PIN GPIO_PIN_7 /* 增氧泵 */
/* 水位检测引脚定义 */
#define WATER_LEVEL_PORT GPIOB
#define HIGH_LEVEL_PIN GPIO_PIN_12 /* 高水位 */
#define LOW_LEVEL_PIN GPIO_PIN_13 /* 低水位 */
/* 继电器状态 */
#define RELAY_ON GPIO_PIN_RESET
#define RELAY_OFF GPIO_PIN_SET
/* 水位状态 */
#define LEVEL_HIGH 0 /* 浮球开关触发时为低电平 */
#define LEVEL_LOW 1
/* 换水参数 */
#define DRAIN_TIMEOUT_MS 120000 /* 排水超时: 2分钟 */
#define FILL_TIMEOUT_MS 120000 /* 进水超时: 2分钟 */
#define WAIT_AFTER_DRAIN 5000 /* 排水后等待: 5秒 */
/* 函数声明 */
void RelayPump_Init(void);
void Relay_SetState(uint16_t relay_pin, uint8_t state);
void WaterPump_In(uint8_t state);
void WaterPump_Out(uint8_t state);
void Heater_SetState(uint8_t state);
void AirPump_SetState(uint8_t state);
uint8_t WaterLevel_GetHigh(void);
uint8_t WaterLevel_GetLow(void);
uint8_t WaterChange_Auto(uint8_t drain_percent);
#endif /* __RELAY_PUMP_H */
📄 创建文件:Core/Src/relay_pump.c
c
#include "relay_pump.h"
#include "delay.h"
/**
* @brief 继电器和水泵初始化
* @retval None
*/
void RelayPump_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 继电器引脚配置 - 推挽输出 */
GPIO_InitStruct.Pin = RELAY_IN1_PIN | RELAY_IN2_PIN |
RELAY_IN3_PIN | RELAY_IN4_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(RELAY_IN_PORT, &GPIO_InitStruct);
/* 初始状态: 所有继电器关闭 */
HAL_GPIO_WritePin(RELAY_IN_PORT, RELAY_IN1_PIN, RELAY_OFF);
HAL_GPIO_WritePin(RELAY_IN_PORT, RELAY_IN2_PIN, RELAY_OFF);
HAL_GPIO_WritePin(RELAY_IN_PORT, RELAY_IN3_PIN, RELAY_OFF);
HAL_GPIO_WritePin(RELAY_IN_PORT, RELAY_IN4_PIN, RELAY_OFF);
/* 水位检测引脚配置 - 输入上拉 */
GPIO_InitStruct.Pin = HIGH_LEVEL_PIN | LOW_LEVEL_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(WATER_LEVEL_PORT, &GPIO_InitStruct);
}
/**
* @brief 设置继电器状态
* @param relay_pin: 继电器引脚
* @param state: 状态(RELAY_ON/RELAY_OFF)
* @retval None
*/
void Relay_SetState(uint16_t relay_pin, uint8_t state)
{
HAL_GPIO_WritePin(RELAY_IN_PORT, relay_pin, state);
}
/**
* @brief 进水泵控制
* @param state: 状态(1开启, 0关闭)
* @retval None
*/
void WaterPump_In(uint8_t state)
{
Relay_SetState(RELAY_IN1_PIN, state ? RELAY_ON : RELAY_OFF);
}
/**
* @brief 排水泵控制
* @param state: 状态(1开启, 0关闭)
* @retval None
*/
void WaterPump_Out(uint8_t state)
{
Relay_SetState(RELAY_IN2_PIN, state ? RELAY_ON : RELAY_OFF);
}
/**
* @brief 加热棒控制
* @param state: 状态(1开启, 0关闭)
* @retval None
*/
void Heater_SetState(uint8_t state)
{
Relay_SetState(RELAY_IN3_PIN, state ? RELAY_ON : RELAY_OFF);
}
/**
* @brief 增氧泵控制
* @param state: 状态(1开启, 0关闭)
* @retval None
*/
void AirPump_SetState(uint8_t state)
{
Relay_SetState(RELAY_IN4_PIN, state ? RELAY_ON : RELAY_OFF);
}
/**
* @brief 获取高水位状态
* @retval LEVEL_HIGH: 高水位, LEVEL_LOW: 未达高水位
*/
uint8_t WaterLevel_GetHigh(void)
{
return HAL_GPIO_ReadPin(WATER_LEVEL_PORT, HIGH_LEVEL_PIN);
}
/**
* @brief 获取低水位状态
* @retval LEVEL_HIGH: 不缺水, LEVEL_LOW: 缺水
*/
uint8_t WaterLevel_GetLow(void)
{
return HAL_GPIO_ReadPin(WATER_LEVEL_PORT, LOW_LEVEL_PIN);
}
/**
* @brief 自动换水流程
* @param drain_percent: 排水百分比(10-50)
* @retval 0: 成功, 1: 失败(超时)
*/
uint8_t WaterChange_Auto(uint8_t drain_percent)
{
uint32_t start_time;
uint8_t success = 0;
/* 参数范围限制 */
if(drain_percent < 10) drain_percent = 10;
if(drain_percent > 50) drain_percent = 50;
/**************************
* 第一步: 排水
**************************/
WaterPump_Out(1);
start_time = HAL_GetTick();
/* 根据百分比计算排水时间 (简化: 用延时代替精确流量控制) */
uint32_t drain_time = (DRAIN_TIMEOUT_MS * drain_percent) / 100;
while(HAL_GetTick() - start_time < drain_time)
{
/* 低水位保护 */
if(WaterLevel_GetLow() == LEVEL_LOW)
{
WaterPump_Out(0);
return 1;
}
delay_ms(100);
}
WaterPump_Out(0);
/**************************
* 第二步: 静置等待
**************************/
delay_ms(WAIT_AFTER_DRAIN);
/**************************
* 第三步: 进水
**************************/
WaterPump_In(1);
start_time = HAL_GetTick();
while(HAL_GetTick() - start_time < FILL_TIMEOUT_MS)
{
/* 高水位检测, 到达高水位停止进水 */
if(WaterLevel_GetHigh() == LEVEL_HIGH)
{
success = 1;
break;
}
delay_ms(100);
}
WaterPump_In(0);
return success ? 0 : 1;
}
4.6 OLED显示模块
📄 创建文件:Core/Inc/oled.h
c
#ifndef __OLED_H
#define __OLED_H
#include "stm32f1xx_hal.h"
#include <stdarg.h>
#include <stdio.h>
/* OLED I2C地址 */
#define OLED_ADDRESS 0x78
/* OLED尺寸 */
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_PAGES 8
/* 颜色定义 */
#define OLED_WHITE 1
#define OLED_BLACK 0
/* 函数声明 */
void OLED_Init(void);
void OLED_Clear(void);
void OLED_DisplayOn(void);
void OLED_DisplayOff(void);
void OLED_SetCursor(uint8_t x, uint8_t y);
void OLED_WriteByte(uint8_t data, uint8_t cmd);
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size);
void OLED_ShowString(uint8_t x, uint8_t y, const char* str, uint8_t size);
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size);
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t decimals, uint8_t size);
void OLED_Printf(uint8_t x, uint8_t y, uint8_t size, const char* fmt, ...);
void OLED_DrawBMP(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, const uint8_t* BMP);
void OLED_UpdateDisplay(void);
#endif /* __OLED_H */
📄 创建文件:Core/Src/oled.c
c
#include "oled.h"
#include "delay.h"
extern I2C_HandleTypeDef hi2c1;
/* 显示缓冲区 */
static uint8_t OLED_Buffer[OLED_PAGES][OLED_WIDTH];
/* 6x8 ASCII字模 */
static const uint8_t F6x8[][6] =
{
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x2f, 0x00, 0x00},
{0x00, 0x00, 0x07, 0x00, 0x07, 0x00},
{0x00, 0x14, 0x7f, 0x14, 0x7f, 0x14},
{0x00, 0x24, 0x2a, 0x7f, 0x2a, 0x12},
{0x00, 0x62, 0x64, 0x08, 0x13, 0x23},
{0x00, 0x36, 0x49, 0x55, 0x22, 0x50},
{0x00, 0x00, 0x05, 0x03, 0x00, 0x00},
{0x00, 0x00, 0x1c, 0x22, 0x41, 0x00},
{0x00, 0x00, 0x41, 0x22, 0x1c, 0x00},
{0x00, 0x14, 0x08, 0x3E, 0x08, 0x14},
{0x00, 0x08, 0x08, 0x3E, 0x08, 0x08},
{0x00, 0x00, 0x00, 0xA0, 0x60, 0x00},
{0x00, 0x08, 0x08, 0x08, 0x08, 0x08},
{0x00, 0x00, 0x60, 0x60, 0x00, 0x00},
{0x00, 0x20, 0x10, 0x08, 0x04, 0x02},
{0x00, 0x3E, 0x51, 0x49, 0x45, 0x3E},
{0x00, 0x00, 0x42, 0x7F, 0x40, 0x00},
{0x00, 0x42, 0x61, 0x51, 0x49, 0x46},
{0x00, 0x21, 0x41, 0x45, 0x4B, 0x31},
{0x00, 0x18, 0x14, 0x12, 0x7F, 0x10},
{0x00, 0x27, 0x45, 0x45, 0x45, 0x39},
{0x00, 0x3C, 0x4A, 0x49, 0x49, 0x30},
{0x00, 0x01, 0x71, 0x09, 0x05, 0x03},
{0x00, 0x36, 0x49, 0x49, 0x49, 0x36},
{0x00, 0x06, 0x49, 0x49, 0x29, 0x1E},
{0x00, 0x00, 0x36, 0x36, 0x00, 0x00},
{0x00, 0x00, 0xAC, 0x6C, 0x00, 0x00},
{0x00, 0x08, 0x14, 0x22, 0x41, 0x00},
{0x00, 0x14, 0x14, 0x14, 0x14, 0x14},
{0x00, 0x41, 0x22, 0x14, 0x08, 0x00},
{0x00, 0x02, 0x01, 0x51, 0x09, 0x06},
{0x00, 0x32, 0x49, 0x79, 0x41, 0x3E},
{0x00, 0x7E, 0x09, 0x09, 0x09, 0x7E},
{0x00, 0x7F, 0x49, 0x49, 0x49, 0x36},
{0x00, 0x3E, 0x41, 0x41, 0x41, 0x22},
{0x00, 0x7F, 0x41, 0x41, 0x22, 0x1C},
{0x00, 0x7F, 0x49, 0x49, 0x49, 0x41},
{0x00, 0x7F, 0x09, 0x09, 0x09, 0x01},
{0x00, 0x3E, 0x41, 0x41, 0x51, 0x72},
{0x00, 0x7F, 0x08, 0x08, 0x08, 0x7F},
{0x00, 0x00, 0x41, 0x7F, 0x41, 0x00},
{0x00, 0x20, 0x40, 0x41, 0x3F, 0x01},
{0x00, 0x7F, 0x08, 0x14, 0x22, 0x41},
{0x00, 0x7F, 0x40, 0x40, 0x40, 0x40},
{0x00, 0x7F, 0x02, 0x0C, 0x02, 0x7F},
{0x00, 0x7F, 0x04, 0x08, 0x10, 0x7F},
{0x00, 0x3E, 0x41, 0x41, 0x41, 0x3E},
{0x00, 0x7F, 0x09, 0x09, 0x09, 0x06},
{0x00, 0x3E, 0x41, 0x51, 0x21, 0x5E},
{0x00, 0x7F, 0x09, 0x19, 0x29, 0x46},
{0x00, 0x26, 0x49, 0x49, 0x49, 0x32},
{0x00, 0x01, 0x01, 0x7F, 0x01, 0x01},
{0x00, 0x3F, 0x40, 0x40, 0x40, 0x3F},
{0x00, 0x1F, 0x20, 0x40, 0x20, 0x1F},
{0x00, 0x3F, 0x40, 0x38, 0x40, 0x3F},
{0x00, 0x63, 0x14, 0x08, 0x14, 0x63},
{0x00, 0x07, 0x08, 0x70, 0x08, 0x07},
{0x00, 0x61, 0x51, 0x49, 0x45, 0x43},
{0x00, 0x00, 0x7F, 0x41, 0x41, 0x00},
{0x00, 0x02, 0x04, 0x08, 0x10, 0x20},
{0x00, 0x00, 0x41, 0x41, 0x7F, 0x00},
{0x00, 0x04, 0x02, 0x01, 0x02, 0x04},
{0x00, 0x40, 0x40, 0x40, 0x40, 0x40},
{0x00, 0x00, 0x01, 0x02, 0x04, 0x00},
{0x00, 0x20, 0x54, 0x54, 0x54, 0x78},
{0x00, 0x7F, 0x48, 0x44, 0x44, 0x38},
{0x00, 0x38, 0x44, 0x44, 0x44, 0x20},
{0x00, 0x38, 0x44, 0x44, 0x48, 0x7F},
{0x00, 0x38, 0x54, 0x54, 0x54, 0x18},
{0x00, 0x08, 0x7E, 0x09, 0x01, 0x02},
{0x00, 0x18, 0xA4, 0xA4, 0xA4, 0x7C},
{0x00, 0x7F, 0x08, 0x04, 0x04, 0x78},
{0x00, 0x00, 0x44, 0x7D, 0x40, 0x00},
{0x00, 0x40, 0x80, 0x84, 0x7D, 0x00},
{0x00, 0x7F, 0x10, 0x28, 0x44, 0x00},
{0x00, 0x00, 0x41, 0x7F, 0x40, 0x00},
{0x00, 0x7C, 0x04, 0x18, 0x04, 0x78},
{0x00, 0x7C, 0x08, 0x04, 0x7C, 0x04},
{0x00, 0x38, 0x44, 0x44, 0x44, 0x38},
{0x00, 0xFC, 0x18, 0x24, 0x24, 0x18},
{0x00, 0x18, 0x24, 0x24, 0x18, 0xFC},
{0x00, 0x7C, 0x08, 0x04, 0x04, 0x08},
{0x00, 0x48, 0x54, 0x54, 0x54, 0x20},
{0x00, 0x04, 0x3F, 0x44, 0x40, 0x20},
{0x00, 0x3C, 0x40, 0x40, 0x20, 0x7C},
{0x00, 0x1C, 0x20, 0x40, 0x20, 0x1C},
{0x00, 0x3C, 0x40, 0x30, 0x40, 0x3C},
{0x00, 0x44, 0x28, 0x10, 0x28, 0x44},
{0x00, 0x1C, 0xA0, 0xA0, 0xA0, 0x7C},
{0x00, 0x44, 0x64, 0x54, 0x4C, 0x44},
{0x00, 0x08, 0x08, 0x36, 0x49, 0x00},
{0x00, 0x00, 0x00, 0x77, 0x00, 0x00},
{0x00, 0x08, 0x14, 0x22, 0x41, 0x00},
{0x00, 0x14, 0x14, 0x14, 0x14, 0x14},
{0x00, 0x41, 0x22, 0x14, 0x08, 0x00},
{0x00, 0x02, 0x01, 0x51, 0x09, 0x06}
};
/* 8x16 ASCII字模 */
static const uint8_t F8X16[][16] =
{
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00},
{0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x70,0x88,0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0xF8,0x88,0x88,0x88,0x88,0x88,0x88,0x88,0xF8,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x88,0x88,0x88,0x98,0xA8,0xC8,0x88,0x88,0x88,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x70,0x88,0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0xBC,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x3E,0x41,0x41,0x49,0x41,0x41,0x41,0x41,0x3E,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0xF8,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x08,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x06,0x00,0x00},
{0x00,0x00,0x00,0x3F,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x3F,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x08,0x08,0x18,0x28,0x48,0x88,0x08,0x08,0x08,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x3F,0x20,0x20,0x20,0x3F,0x20,0x20,0x20,0x3F,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x3F,0x20,0x20,0x20,0x3F,0x20,0x20,0x20,0x20,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0xA8,0xA8,0xA8,0xA8,0xFF,0xA8,0xA8,0xA8,0xA8,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0xFF,0xA0,0xA0,0xA0,0xE0,0xA0,0xA0,0xA0,0xFF,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x3F,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x3F,0x20,0x20,0x20,0x20,0x20,0x20,0x22,0x3E,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x3F,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x3E,0x22,0x22,0x22,0x3E,0x22,0x22,0x22,0x3E,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}
};
/**
* @brief OLED写一个字节
* @param data: 要写入的数据
* @param cmd: 0=命令, 1=数据
* @retval None
*/
void OLED_WriteByte(uint8_t data, uint8_t cmd)
{
uint8_t buf[2];
buf[0] = cmd ? 0x40 : 0x00;
buf[1] = data;
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDRESS, buf, 2, 100);
}
/**
* @brief OLED初始化
* @retval None
*/
void OLED_Init(void)
{
delay_ms(100);
OLED_WriteByte(0xAE, 0); /* 关闭显示 */
OLED_WriteByte(0xD5, 0); /* 设置时钟分频因子,震荡频率 */
OLED_WriteByte(0x80, 0);
OLED_WriteByte(0xA8, 0); /* 设置驱动路数 */
OLED_WriteByte(0x3F, 0);
OLED_WriteByte(0xD3, 0); /* 设置显示偏移 */
OLED_WriteByte(0x00, 0);
OLED_WriteByte(0x40, 0); /* 设置显示开始行 */
OLED_WriteByte(0x8D, 0); /* 电荷泵设置 */
OLED_WriteByte(0x14, 0);
OLED_WriteByte(0x20, 0); /* 内存地址模式 */
OLED_WriteByte(0x02, 0);
OLED_WriteByte(0xA1, 0); /* 段重定义设置 */
OLED_WriteByte(0xC8, 0); /* 扫描方向设置 */
OLED_WriteByte(0xDA, 0); /* 硬件引脚配置 */
OLED_WriteByte(0x12, 0);
OLED_WriteByte(0x81, 0); /* 对比度设置 */
OLED_WriteByte(0xCF, 0);
OLED_WriteByte(0xD9, 0); /* 设置预充电周期 */
OLED_WriteByte(0xF1, 0);
OLED_WriteByte(0xDB, 0); /* 设置VCOMH电压倍率 */
OLED_WriteByte(0x30, 0);
OLED_WriteByte(0xA4, 0); /* 全局显示开启 */
OLED_WriteByte(0xA6, 0); /* 设置显示方式 */
OLED_Clear();
OLED_DisplayOn();
}
/**
* @brief 开启OLED显示
* @retval None
*/
void OLED_DisplayOn(void)
{
OLED_WriteByte(0x8D, 0);
OLED_WriteByte(0x14, 0);
OLED_WriteByte(0xAF, 0);
}
/**
* @brief 关闭OLED显示
* @retval None
*/
void OLED_DisplayOff(void)
{
OLED_WriteByte(0x8D, 0);
OLED_WriteByte(0x10, 0);
OLED_WriteByte(0xAE, 0);
}
/**
* @brief 设置光标位置
* @param x: X坐标(0-127)
* @param y: Y坐标/页(0-7)
* @retval None
*/
void OLED_SetCursor(uint8_t x, uint8_t y)
{
OLED_WriteByte(0xB0 + y, 0);
OLED_WriteByte(((x & 0xF0) >> 4) | 0x10, 0);
OLED_WriteByte((x & 0x0F) | 0x01, 0);
}
/**
* @brief 清屏
* @retval None
*/
void OLED_Clear(void)
{
for(uint8_t i = 0; i < OLED_PAGES; i++)
{
for(uint8_t j = 0; j < OLED_WIDTH; j++)
{
OLED_Buffer[i][j] = 0x00;
}
}
OLED_UpdateDisplay();
}
/**
* @brief 更新显示缓冲区到屏幕
* @retval None
*/
void OLED_UpdateDisplay(void)
{
for(uint8_t i = 0; i < OLED_PAGES; i++)
{
OLED_SetCursor(0, i);
for(uint8_t j = 0; j < OLED_WIDTH; j++)
{
OLED_WriteByte(OLED_Buffer[i][j], 1);
}
}
}
/**
* @brief 在指定位置显示一个字符
* @param x: X坐标
* @param y: Y坐标/页
* @param ch: 字符
* @param size: 字体大小(12=6x8, 16=8x16)
* @retval None
*/
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size)
{
uint8_t c = 0;
uint8_t i = 0;
c = ch - ' ';
if(size == 12)
{
for(i = 0; i < 6; i++)
{
OLED_Buffer[y][x + i] = F6x8[c][i];
}
}
else if(size == 16)
{
for(i = 0; i < 8; i++)
{
OLED_Buffer[y][x + i] = F8X16[c][i];
OLED_Buffer[y + 1][x + i] = F8X16[c][i + 8];
}
}
}
/**
* @brief 显示字符串
* @param x: X坐标
* @param y: Y坐标/页
* @param str: 字符串
* @param size: 字体大小
* @retval None
*/
void OLED_ShowString(uint8_t x, uint8_t y, const char* str, uint8_t size)
{
uint8_t x0 = x;
uint8_t char_width = (size == 12) ? 6 : 8;
while(*str)
{
if(x0 > OLED_WIDTH - char_width)
{
x0 = 0;
y += (size == 12) ? 1 : 2;
}
OLED_ShowChar(x0, y, *str, size);
x0 += char_width;
str++;
}
}
/**
* @brief 显示数字
* @param x: X坐标
* @param y: Y坐标/页
* @param num: 数字
* @param len: 位数
* @param size: 字体大小
* @retval None
*/
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
{
char buf[16];
sprintf(buf, "%*lu", len, num);
OLED_ShowString(x, y, buf, size);
}
/**
* @brief 显示浮点数
* @param x: X坐标
* @param y: Y坐标/页
* @param num: 浮点数
* @param decimals: 小数位数
* @param size: 字体大小
* @retval None
*/
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t decimals, uint8_t size)
{
char buf[32];
char fmt[16];
sprintf(fmt, "%%.%df", decimals);
sprintf(buf, fmt, num);
OLED_ShowString(x, y, buf, size);
}
/**
* @brief 格式化输出
* @param x: X坐标
* @param y: Y坐标/页
* @param size: 字体大小
* @param fmt: 格式化字符串
* @retval None
*/
void OLED_Printf(uint8_t x, uint8_t y, uint8_t size, const char* fmt, ...)
{
char buf[64];
va_list args;
va_start(args, fmt);
vsprintf(buf, fmt, args);
va_end(args);
OLED_ShowString(x, y, buf, size);
}
4.7 按键与蜂鸣器模块
📄 创建文件:Core/Inc/key_beep.h
c
#ifndef __KEY_BEEP_H
#define __KEY_BEEP_H
#include "stm32f1xx_hal.h"
/* 按键引脚定义 */
#define KEY_PORT GPIOB
#define KEY1_PIN GPIO_PIN_9 /* 设置键 */
#define KEY2_PIN GPIO_PIN_10 /* 加键 */
#define KEY3_PIN GPIO_PIN_11 /* 减键 */
/* 蜂鸣器引脚定义 */
#define BEEP_PORT GPIOB
#define BEEP_PIN GPIO_PIN_8
/* 按键状态 */
#define KEY_PRESSED 0
#define KEY_RELEASED 1
/* 按键值 */
#define KEY_NONE 0
#define KEY1_PRESSED 1
#define KEY2_PRESSED 2
#define KEY3_PRESSED 3
/* 长按时间(ms) */
#define KEY_LONG_PRESS_MS 1000
/* 函数声明 */
void KeyBeep_Init(void);
uint8_t Key_Scan(void);
void Beep_On(void);
void Beep_Off(void);
void Beep_Short(void);
void Beep_Long(void);
void Beep_Alarm(uint8_t times);
#endif /* __KEY_BEEP_H */
📄 创建文件:Core/Src/key_beep.c
c
#include "key_beep.h"
#include "delay.h"
/**
* @brief 按键和蜂鸣器初始化
* @retval None
*/
void KeyBeep_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 按键引脚配置 - 输入上拉 */
GPIO_InitStruct.Pin = KEY1_PIN | KEY2_PIN | KEY3_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(KEY_PORT, &GPIO_InitStruct);
/* 蜂鸣器引脚配置 - 推挽输出 */
GPIO_InitStruct.Pin = BEEP_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(BEEP_PORT, &GPIO_InitStruct);
Beep_Off();
}
/**
* @brief 按键扫描(带消抖)
* @retval 按键值(KEY_NONE/KEY1_PRESSED/KEY2_PRESSED/KEY3_PRESSED)
*/
uint8_t Key_Scan(void)
{
static uint8_t key_up = 1; /* 按键松开标志 */
if(key_up)
{
if(HAL_GPIO_ReadPin(KEY_PORT, KEY1_PIN) == KEY_PRESSED)
{
delay_ms(10); /* 消抖 */
if(HAL_GPIO_ReadPin(KEY_PORT, KEY1_PIN) == KEY_PRESSED)
{
key_up = 0;
return KEY1_PRESSED;
}
}
else if(HAL_GPIO_ReadPin(KEY_PORT, KEY2_PIN) == KEY_PRESSED)
{
delay_ms(10);
if(HAL_GPIO_ReadPin(KEY_PORT, KEY2_PIN) == KEY_PRESSED)
{
key_up = 0;
return KEY2_PRESSED;
}
}
else if(HAL_GPIO_ReadPin(KEY_PORT, KEY3_PIN) == KEY_PRESSED)
{
delay_ms(10);
if(HAL_GPIO_ReadPin(KEY_PORT, KEY3_PIN) == KEY_PRESSED)
{
key_up = 0;
return KEY3_PRESSED;
}
}
}
if(HAL_GPIO_ReadPin(KEY_PORT, KEY1_PIN) == KEY_RELEASED &&
HAL_GPIO_ReadPin(KEY_PORT, KEY2_PIN) == KEY_RELEASED &&
HAL_GPIO_ReadPin(KEY_PORT, KEY3_PIN) == KEY_RELEASED)
{
key_up = 1;
}
return KEY_NONE;
}
/**
* @brief 开启蜂鸣器
* @retval None
*/
void Beep_On(void)
{
HAL_GPIO_WritePin(BEEP_PORT, BEEP_PIN, GPIO_PIN_SET);
}
/**
* @brief 关闭蜂鸣器
* @retval None
*/
void Beep_Off(void)
{
HAL_GPIO_WritePin(BEEP_PORT, BEEP_PIN, GPIO_PIN_RESET);
}
/**
* @brief 短鸣一声
* @retval None
*/
void Beep_Short(void)
{
Beep_On();
delay_ms(50);
Beep_Off();
}
/**
* @brief 长鸣一声
* @retval None
*/
void Beep_Long(void)
{
Beep_On();
delay_ms(200);
Beep_Off();
}
/**
* @brief 报警声
* @param times: 鸣叫次数
* @retval None
*/
void Beep_Alarm(uint8_t times)
{
for(uint8_t i = 0; i < times; i++)
{
Beep_On();
delay_ms(100);
Beep_Off();
delay_ms(100);
}
}
五、系统主程序
5.1 系统参数配置
📄 创建文件:Core/Inc/system_config.h
c
#ifndef __SYSTEM_CONFIG_H
#define __SYSTEM_CONFIG_H
#include <stdint.h>
/* 系统配置结构体 */
typedef struct
{
/* 温度阈值 */
float temp_min; /* 最低温度(℃) */
float temp_max; /* 最高温度(℃) */
uint8_t heater_enable; /* 加热棒使能 */
/* pH阈值 */
float ph_min; /* 最低pH */
float ph_max; /* 最高pH */
/* TDS阈值 */
uint16_t tds_max; /* 最高TDS(ppm) */
/* 溶解氧阈值 */
float do_min; /* 最低溶氧(mg/L) */
/* 投喂设置 */
uint8_t feed_hour1; /* 投喂时间1-小时 */
uint8_t feed_minute1; /* 投喂时间1-分钟 */
uint8_t feed_times1; /* 投喂次数1 */
uint8_t feed_enable1; /* 投喂1使能 */
uint8_t feed_hour2; /* 投喂时间2-小时 */
uint8_t feed_minute2; /* 投喂时间2-分钟 */
uint8_t feed_times2; /* 投喂次数2 */
uint8_t feed_enable2; /* 投喂2使能 */
/* 换水设置 */
uint8_t water_change_hour; /* 换水时间-小时 */
uint8_t water_change_percent; /* 换水百分比 */
uint8_t water_change_enable; /* 换水使能 */
/* 增氧定时 */
uint8_t air_on_hour; /* 增氧开始时间 */
uint8_t air_off_hour; /* 增氧结束时间 */
uint8_t air_enable; /* 定时增氧使能 */
/* 系统时间 (RTC) */
uint8_t rtc_hour; /* 当前小时 */
uint8_t rtc_minute; /* 当前分钟 */
uint8_t rtc_second; /* 当前秒 */
} SystemConfig_t;
/* 系统状态结构体 */
typedef struct
{
float temperature; /* 当前水温 */
float ph_value; /* 当前pH */
float tds_value; /* 当前TDS */
float do_value; /* 当前溶解氧 */
uint8_t water_level_high; /* 高水位状态 */
uint8_t water_level_low; /* 低水位状态 */
uint8_t heater_state; /* 加热棒状态 */
uint8_t air_pump_state; /* 增氧泵状态 */
uint8_t alarm_flag; /* 报警标志 */
uint8_t alarm_type; /* 报警类型 */
uint8_t feed1_done; /* 今日投喂1已完成 */
uint8_t feed2_done; /* 今日投喂2已完成 */
uint8_t water_change_done; /* 今日换水已完成 */
} SystemStatus_t;
/* 报警类型 */
#define ALARM_NONE 0x00
#define ALARM_TEMP_LOW 0x01
#define ALARM_TEMP_HIGH 0x02
#define ALARM_PH_LOW 0x04
#define ALARM_PH_HIGH 0x08
#define ALARM_TDS_HIGH 0x10
#define ALARM_DO_LOW 0x20
#define ALARM_WATER_LOW 0x40
/* 菜单类型 */
typedef enum
{
MENU_MAIN,
MENU_TEMP,
MENU_PH,
MENU_TDS,
MENU_DO,
MENU_FEED1,
MENU_FEED2,
MENU_WATER_CHANGE,
MENU_AIR_PUMP,
MENU_TIME_SET,
MENU_MANUAL_CTRL
} MenuType_t;
/* 全局变量 */
extern SystemConfig_t sysConfig;
extern SystemStatus_t sysStatus;
extern MenuType_t currentMenu;
extern uint8_t menuIndex;
/* 函数声明 */
void System_LoadConfig(void);
void System_SaveConfig(void);
void System_Init(void);
void System_UpdateStatus(void);
void System_CheckAlarm(void);
void System_HandleAlarm(void);
void System_FeedCheck(void);
void System_WaterChangeCheck(void);
void System_AirPumpCtrl(void);
void System_HeaterCtrl(void);
void System_DisplayUpdate(void);
void System_MenuProcess(uint8_t key);
#endif /* __SYSTEM_CONFIG_H */
5.2 主程序实现
📄 修改文件:Core/Src/main.c
c
#include "main.h"
#include "delay.h"
#include "ds18b20.h"
#include "adc_sensor.h"
#include "stepper.h"
#include "relay_pump.h"
#include "oled.h"
#include "key_beep.h"
#include "system_config.h"
I2C_HandleTypeDef hi2c1;
ADC_HandleTypeDef hadc1;
/* 全局系统配置和状态 */
SystemConfig_t sysConfig;
SystemStatus_t sysStatus;
MenuType_t currentMenu = MENU_MAIN;
uint8_t menuIndex = 0;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_I2C1_Init(void);
static void MX_ADC1_Init(void);
/**
* @brief 应用程序入口点
* @retval int
*/
int main(void)
{
uint32_t sensor_timer = 0;
uint32_t display_timer = 0;
uint32_t rtc_timer = 0;
uint8_t key_value;
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_ADC1_Init();
/* 初始化延时 */
delay_init(72);
delay_ms(100);
/* 初始化所有模块 */
System_Init();
DS18B20_Init();
ADC_Sensor_Init();
Stepper_Init();
RelayPump_Init();
OLED_Init();
KeyBeep_Init();
/* 欢迎界面 */
OLED_Clear();
OLED_ShowString(32, 2, "智能鱼缸", 16);
OLED_ShowString(24, 5, "系统启动中...", 12);
OLED_UpdateDisplay();
delay_ms(2000);
Beep_Short();
/* 主循环 */
while (1)
{
/**************************
* 1. 按键处理
**************************/
key_value = Key_Scan();
if(key_value != KEY_NONE)
{
Beep_Short();
System_MenuProcess(key_value);
}
/**************************
* 2. 传感器数据更新 (500ms)
**************************/
if(HAL_GetTick() - sensor_timer > 500)
{
sensor_timer = HAL_GetTick();
System_UpdateStatus();
System_CheckAlarm();
}
/**************************
* 3. 显示更新 (200ms)
**************************/
if(HAL_GetTick() - display_timer > 200)
{
display_timer = HAL_GetTick();
System_DisplayUpdate();
}
/**************************
* 4. RTC模拟 (1000ms)
**************************/
if(HAL_GetTick() - rtc_timer > 1000)
{
rtc_timer = HAL_GetTick();
/* 更新模拟RTC时间 */
sysConfig.rtc_second++;
if(sysConfig.rtc_second >= 60)
{
sysConfig.rtc_second = 0;
sysConfig.rtc_minute++;
if(sysConfig.rtc_minute >= 60)
{
sysConfig.rtc_minute = 0;
sysConfig.rtc_hour++;
if(sysConfig.rtc_hour >= 24)
{
sysConfig.rtc_hour = 0;
/* 新的一天, 重置每日任务标志 */
sysStatus.feed1_done = 0;
sysStatus.feed2_done = 0;
sysStatus.water_change_done = 0;
}
}
}
/* 定时任务检查 */
System_FeedCheck();
System_WaterChangeCheck();
System_AirPumpCtrl();
System_HeaterCtrl();
}
/**************************
* 5. 报警处理
**************************/
if(sysStatus.alarm_flag)
{
System_HandleAlarm();
}
HAL_Delay(10);
}
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);
}
/**
* @brief ADC1 Initialization Function
* @param None
* @retval None
*/
static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
}
/**
* @brief I2C1 Initialization Function
* @param None
* @retval None
*/
static void MX_I2C1_Init(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
}
/**
* @brief 错误回调函数
* @retval None
*/
void Error_Handler(void)
{
__disable_irq();
while (1)
{
}
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
}
#endif /* USE_FULL_ASSERT */
5.3 系统功能实现
📄 创建文件:Core/Src/system_config.c
c
#include "system_config.h"
#include "oled.h"
#include "key_beep.h"
#include "ds18b20.h"
#include "adc_sensor.h"
#include "stepper.h"
#include "relay_pump.h"
#include "delay.h"
/**
* @brief 系统初始化(加载默认配置)
* @retval None
*/
void System_Init(void)
{
/* 加载默认配置 */
sysConfig.temp_min = 24.0f;
sysConfig.temp_max = 28.0f;
sysConfig.heater_enable = 1;
sysConfig.ph_min = 6.5f;
sysConfig.ph_max = 8.0f;
sysConfig.tds_max = 500;
sysConfig.do_min = 5.0f;
sysConfig.feed_hour1 = 9;
sysConfig.feed_minute1 = 0;
sysConfig.feed_times1 = 2;
sysConfig.feed_enable1 = 1;
sysConfig.feed_hour2 = 18;
sysConfig.feed_minute2 = 0;
sysConfig.feed_times2 = 2;
sysConfig.feed_enable2 = 1;
sysConfig.water_change_hour = 14;
sysConfig.water_change_percent = 30;
sysConfig.water_change_enable = 1;
sysConfig.air_on_hour = 8;
sysConfig.air_off_hour = 20;
sysConfig.air_enable = 1;
sysConfig.rtc_hour = 12;
sysConfig.rtc_minute = 0;
sysConfig.rtc_second = 0;
/* 初始化状态 */
sysStatus.alarm_flag = 0;
sysStatus.alarm_type = ALARM_NONE;
sysStatus.feed1_done = 0;
sysStatus.feed2_done = 0;
sysStatus.water_change_done = 0;
sysStatus.heater_state = 0;
sysStatus.air_pump_state = 0;
}
/**
* @brief 更新系统状态(传感器数据)
* @retval None
*/
void System_UpdateStatus(void)
{
/* 读取水温 */
sysStatus.temperature = DS18B20_GetTemperature();
/* 读取水质参数 */
sysStatus.ph_value = PH_GetValue(sysStatus.temperature);
sysStatus.tds_value = TDS_GetValue(sysStatus.temperature);
sysStatus.do_value = DO_GetValue(sysStatus.temperature);
/* 读取水位状态 */
sysStatus.water_level_high = WaterLevel_GetHigh();
sysStatus.water_level_low = WaterLevel_GetLow();
}
/**
* @brief 检查报警条件
* @retval None
*/
void System_CheckAlarm(void)
{
sysStatus.alarm_type = ALARM_NONE;
/* 温度报警 */
if(sysStatus.temperature < sysConfig.temp_min)
sysStatus.alarm_type |= ALARM_TEMP_LOW;
if(sysStatus.temperature > sysConfig.temp_max)
sysStatus.alarm_type |= ALARM_TEMP_HIGH;
/* pH报警 */
if(sysStatus.ph_value < sysConfig.ph_min)
sysStatus.alarm_type |= ALARM_PH_LOW;
if(sysStatus.ph_value > sysConfig.ph_max)
sysStatus.alarm_type |= ALARM_PH_HIGH;
/* TDS报警 */
if(sysStatus.tds_value > sysConfig.tds_max)
sysStatus.alarm_type |= ALARM_TDS_HIGH;
/* 溶解氧报警 */
if(sysStatus.do_value < sysConfig.do_min)
sysStatus.alarm_type |= ALARM_DO_LOW;
/* 低水位报警 */
if(sysStatus.water_level_low == LEVEL_LOW)
sysStatus.alarm_type |= ALARM_WATER_LOW;
sysStatus.alarm_flag = (sysStatus.alarm_type != ALARM_NONE) ? 1 : 0;
}
/**
* @brief 处理报警
* @retval None
*/
void System_HandleAlarm(void)
{
static uint32_t alarm_timer = 0;
/* 每3秒报警一次 */
if(HAL_GetTick() - alarm_timer > 3000)
{
alarm_timer = HAL_GetTick();
Beep_Alarm(3);
}
}
/**
* @brief 检查定时投喂
* @retval None
*/
void System_FeedCheck(void)
{
/* 投喂时间1 */
if(sysConfig.feed_enable1 && !sysStatus.feed1_done)
{
if(sysConfig.rtc_hour == sysConfig.feed_hour1 &&
sysConfig.rtc_minute == sysConfig.feed_minute1)
{
Stepper_Feed(sysConfig.feed_times1);
sysStatus.feed1_done = 1;
Beep_Short();
}
}
/* 投喂时间2 */
if(sysConfig.feed_enable2 && !sysStatus.feed2_done)
{
if(sysConfig.rtc_hour == sysConfig.feed_hour2 &&
sysConfig.rtc_minute == sysConfig.feed_minute2)
{
Stepper_Feed(sysConfig.feed_times2);
sysStatus.feed2_done = 1;
Beep_Short();
}
}
}
/**
* @brief 检查定时换水
* @retval None
*/
void System_WaterChangeCheck(void)
{
if(sysConfig.water_change_enable && !sysStatus.water_change_done)
{
if(sysConfig.rtc_hour == sysConfig.water_change_hour &&
sysConfig.rtc_minute == 0)
{
WaterChange_Auto(sysConfig.water_change_percent);
sysStatus.water_change_done = 1;
Beep_Long();
}
}
}
/**
* @brief 定时增氧控制
* @retval None
*/
void System_AirPumpCtrl(void)
{
if(!sysConfig.air_enable)
{
AirPump_SetState(0);
sysStatus.air_pump_state = 0;
return;
}
/* 时间段内开启增氧 */
if(sysConfig.rtc_hour >= sysConfig.air_on_hour &&
sysConfig.rtc_hour < sysConfig.air_off_hour)
{
AirPump_SetState(1);
sysStatus.air_pump_state = 1;
}
else
{
AirPump_SetState(0);
sysStatus.air_pump_state = 0;
}
/* 溶氧过低时强制开启 */
if(sysStatus.do_value < sysConfig.do_min)
{
AirPump_SetState(1);
sysStatus.air_pump_state = 1;
}
}
/**
* @brief 加热棒控制
* @retval None
*/
void System_HeaterCtrl(void)
{
if(!sysConfig.heater_enable)
{
Heater_SetState(0);
sysStatus.heater_state = 0;
return;
}
/* 低于最低温度开启 */
if(sysStatus.temperature < sysConfig.temp_min)
{
Heater_SetState(1);
sysStatus.heater_state = 1;
}
/* 高于最高温度关闭 */
else if(sysStatus.temperature > sysConfig.temp_max)
{
Heater_SetState(0);
sysStatus.heater_state = 0;
}
/* 滞回区间保持当前状态 */
}
/**
* @brief 更新显示
* @retval None
*/
void System_DisplayUpdate(void)
{
char status_buf[32];
if(currentMenu == MENU_MAIN)
{
OLED_Clear();
/* 第一行: 时间和温度 */
OLED_Printf(0, 0, 12, "%02d:%02d %.1f'C",
sysConfig.rtc_hour, sysConfig.rtc_minute,
sysStatus.temperature);
/* 第二行: pH和TDS */
OLED_Printf(0, 2, 12, "pH:%.2f TDS:%d",
sysStatus.ph_value, (uint16_t)sysStatus.tds_value);
/* 第三行: 溶解氧和水位 */
OLED_Printf(0, 4, 12, "DO:%.1fmg/L %s",
sysStatus.do_value,
sysStatus.water_level_low == LEVEL_LOW ? "LOW" : "OK");
/* 第四行: 设备状态 */
sprintf(status_buf, "%c%c%c%c",
sysStatus.heater_state ? 'H' : '-',
sysStatus.air_pump_state ? 'O' : '-',
sysStatus.feed1_done || sysStatus.feed2_done ? 'F' : '-',
sysStatus.alarm_flag ? '!' : ' ');
OLED_ShowString(0, 6, status_buf, 12);
/* 报警闪烁指示 */
if(sysStatus.alarm_flag && (HAL_GetTick() / 500) % 2)
{
OLED_ShowString(120, 0, "!", 12);
}
OLED_UpdateDisplay();
}
}
/**
* @brief 菜单处理
* @param key: 按键值
* @retval None
*/
void System_MenuProcess(uint8_t key)
{
/* 简化的菜单处理 - 长按Key1手动投喂 */
if(key == KEY1_PRESSED && currentMenu == MENU_MAIN)
{
Stepper_Feed(1);
}
/* Key2手动换水 */
else if(key == KEY2_PRESSED && currentMenu == MENU_MAIN)
{
WaterChange_Auto(20);
}
/* Key3开启/关闭报警声 */
else if(key == KEY3_PRESSED)
{
sysStatus.alarm_flag = 0;
}
}
六、系统工作流程图
主循环流程
是
否
是
否
是
否
是
否
是
否
系统启动
硬件初始化
加载系统配置
显示欢迎界面
主循环
按键扫描处理
有按键?
执行对应操作
传感器数据采集
数据滤波处理
更新系统状态
报警条件检查
有报警?
触发声光报警
RTC时间更新
整点/分钟?
定时任务检查
执行设备控制
到投喂时间?
执行自动投喂
到换水时间?
执行自动换水
温度控制
增氧控制
水位保护
更新OLED显示
七、调试步骤与常见问题
7.1 硬件调试步骤
步骤1: 电源检查
1. 检查5V和3.3V电源电压是否正常
2. 检查各模块电源指示灯是否点亮
3. 测量STM32核心板供电电压
步骤2: 通信接口测试
1. I2C接口测试 - OLED是否能正常显示
2. 单总线测试 - DS18B20是否能读取温度
3. ADC测试 - pH/TDS传感器是否有输出
步骤3: 执行器测试
1. 继电器测试 - 控制引脚输出高低电平, 听继电器吸合声
2. 步进电机测试 - 发送脉冲, 观察电机是否转动
3. 水泵测试 - 确认管路连接正确后测试抽水
7.2 传感器校准
DS18B20温度校准
c
// 温度修正公式(如有偏差)
float temp_calibrated = raw_temp + OFFSET;
pH传感器校准
c
// 两点校准法
// 1. 将探头放入pH=4.00标准溶液, 记录电压V4
// 2. 将探头放入pH=7.00标准溶液, 记录电压V7
// 3. 计算斜率: slope = (7.00 - 4.00) / (V7 - V4)
// 4. pH = 7.00 + slope * (voltage - V7)
TDS传感器校准
c
// 使用已知TDS值的溶液进行校准
// TDS = k * voltage + b
// 通过两点测量计算k和b的值
7.3 常见问题与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| OLED不显示 | I2C引脚接反 I2C地址错误 供电不足 | 检查SDA/SCL接线 确认OLED地址(0x78/0x7A) 使用独立5V电源 |
| DS18B20读取失败 | 数据引脚接错 缺少上拉电阻 传感器损坏 | 确认DQ引脚连接 增加4.7K上拉电阻 更换传感器测试 |
| pH读数不准 | 探头老化 未进行校准 温度影响 | 更换新探头 使用标准溶液校准 加入温度补偿算法 |
| 步进电机抖动不转 | 电源电压不足 相序接错 速度太快 | 使用12V电源供电 检查ULN2003接线 减小脉冲频率 |
| 水泵不工作 | 继电器未吸合 电源功率不足 水泵空转 | 检查继电器控制信号 使用足够功率的电源 检查水管是否有堵塞 |
| 频繁误报警 | 传感器数据波动大 阈值设置不合理 | 增加软件滤波 调整阈值范围, 增加滞回 |
7.4 性能优化建议
-
电源优化
- 使用线性稳压器减少电源纹波
- 模拟地和数字地单点连接
- 关键模块增加滤波电容
-
软件优化
- 传感器数据采用滑动平均滤波
- 增加异常数据检测和剔除
- 控制逻辑加入滞回防止频繁开关
-
安全增强
- 加入水泵超时保护
- 加热棒干烧检测
- 水位异常紧急停机
八、项目扩展建议
8.1 功能扩展
- WiFi远程监控 - 加入ESP8266模块, 数据上传云平台
- 手机APP控制 - 开发Android/iOS APP, 远程查看和控制
- 数据记录 - 增加SD卡或Flash存储历史数据
- 摄像头监控 - 加入摄像头模块, 远程查看鱼缸状态
- 智能投喂 - 根据水质和鱼的生长情况自动调整投喂量
8.2 硬件升级
- 更换主控 - 升级到STM32F4/F7系列, 性能更强
- 专业传感器 - 使用工业级水质传感器, 精度更高
- 触摸屏 - 更换为彩色触摸屏, 操作更直观
- 独立RTC - 使用DS3231等高精度RTC芯片
总结
本项目基于STM32F103微控制器, 实现了一个功能完善的智能鱼缸监控投喂系统, 涵盖:
- 水质监测: 水温、pH值、TDS、溶解氧多参数监测
- 自动投喂: 支持两组定时投喂, 投喂量可调
- 自动换水: 根据水质参数和定时自动换水
- 智能控制: 自动恒温、定时增氧、水位保护
- 本地交互: OLED显示、按键操作、声光报警
通过模块化的软件设计和详细的硬件指导, 零基础的学习者也可以按照教程一步步完成项目, 同时项目预留了充足的扩展空间, 适合进一步学习和开发。