STM32实战:基于STM32F103的智能鱼缸监控投喂系统(水质监测+自动换水)

文章目录

    • 一、项目概述
      • [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 硬件接线总表)
    • 三、开发环境搭建
    • 四、核心模块代码实现
      • [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 软件准备

  1. Keil MDK-ARM 5.38 - 主要开发环境
  2. STM32CubeMX 6.8.1 - 图形化配置工具
  3. CH340驱动 - USB转串口驱动
  4. 串口助手 - 串口调试工具

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 性能优化建议

  1. 电源优化

    • 使用线性稳压器减少电源纹波
    • 模拟地和数字地单点连接
    • 关键模块增加滤波电容
  2. 软件优化

    • 传感器数据采用滑动平均滤波
    • 增加异常数据检测和剔除
    • 控制逻辑加入滞回防止频繁开关
  3. 安全增强

    • 加入水泵超时保护
    • 加热棒干烧检测
    • 水位异常紧急停机

八、项目扩展建议

8.1 功能扩展

  1. WiFi远程监控 - 加入ESP8266模块, 数据上传云平台
  2. 手机APP控制 - 开发Android/iOS APP, 远程查看和控制
  3. 数据记录 - 增加SD卡或Flash存储历史数据
  4. 摄像头监控 - 加入摄像头模块, 远程查看鱼缸状态
  5. 智能投喂 - 根据水质和鱼的生长情况自动调整投喂量

8.2 硬件升级

  1. 更换主控 - 升级到STM32F4/F7系列, 性能更强
  2. 专业传感器 - 使用工业级水质传感器, 精度更高
  3. 触摸屏 - 更换为彩色触摸屏, 操作更直观
  4. 独立RTC - 使用DS3231等高精度RTC芯片

总结

本项目基于STM32F103微控制器, 实现了一个功能完善的智能鱼缸监控投喂系统, 涵盖:

  • 水质监测: 水温、pH值、TDS、溶解氧多参数监测
  • 自动投喂: 支持两组定时投喂, 投喂量可调
  • 自动换水: 根据水质参数和定时自动换水
  • 智能控制: 自动恒温、定时增氧、水位保护
  • 本地交互: OLED显示、按键操作、声光报警

通过模块化的软件设计和详细的硬件指导, 零基础的学习者也可以按照教程一步步完成项目, 同时项目预留了充足的扩展空间, 适合进一步学习和开发。

相关推荐
深圳市晨芯阳科技有限公司1 小时前
晨芯阳科技HC358-N双通道运算放大IC
科技·单片机·嵌入式硬件
一路往蓝-Anbo1 小时前
第四章:手撕协议栈 —— 缓冲区与结构体数据的 Mock 技巧
网络·stm32·单片机·嵌入式硬件·软件工程·tdd
jghhh012 小时前
STM32指纹密码锁的程序
stm32·单片机·嵌入式硬件
Achou.Wang2 小时前
从 Atomic 到 Futex:深入解析并发同步的三重境界
单片机·嵌入式硬件
不怕犯错,就怕不做2 小时前
linux的notifier_block内核通知链
linux·驱动开发·嵌入式硬件
时空自由民.2 小时前
Arm Coretex-M核MCU做IAP/OTA升级时候为什么要做中断向量表地址偏移?
arm开发·单片机·嵌入式硬件
不脱发的程序猿2 小时前
MCU升级固件合并和转换工具
单片机·嵌入式硬件
qq_370773093 小时前
OpenOCD 嵌入式调试完全指南:从零开始调试 GD32/STM32 单片机
stm32·单片机·嵌入式硬件·openocd
LCG元3 小时前
STM32实战:基于STM32F103的迷迭香智慧种植系统(自动补光+滴灌)
stm32·单片机·嵌入式硬件