STM32实战:基于STM32F103的智能鹌鹑孵化箱(温湿度+翻蛋控制)

文章目录

    • 一、项目概述与系统设计
    • 二、硬件清单与接线说明
      • [2.1 所需元器件清单](#2.1 所需元器件清单)
      • [2.2 引脚接线对照表](#2.2 引脚接线对照表)
      • [2.3 完整接线说明](#2.3 完整接线说明)
    • 三、软件开发环境搭建
      • [3.1 安装Keil MDK-ARM](#3.1 安装Keil MDK-ARM)
      • [3.2 创建工程步骤](#3.2 创建工程步骤)
      • [3.3 添加标准外设库文件](#3.3 添加标准外设库文件)
    • 四、核心驱动代码编写
      • [4.1 创建文件:`main.h` --- 主头文件](#4.1 创建文件:main.h — 主头文件)
      • [4.2 创建文件:`delay.c` --- 延时函数实现](#4.2 创建文件:delay.c — 延时函数实现)
      • [4.3 创建文件:`delay.h` --- 延时函数头文件](#4.3 创建文件:delay.h — 延时函数头文件)
      • [4.4 创建文件:`dht22.c` --- DHT22温湿度传感器驱动](#4.4 创建文件:dht22.c — DHT22温湿度传感器驱动)
      • [4.5 创建文件:`dht22.h` --- DHT22头文件](#4.5 创建文件:dht22.h — DHT22头文件)
      • [4.6 创建文件:`ds18b20.c` --- DS18B20温度传感器驱动](#4.6 创建文件:ds18b20.c — DS18B20温度传感器驱动)
      • [4.7 创建文件:`ds18b20.h` --- DS18B20头文件](#4.7 创建文件:ds18b20.h — DS18B20头文件)
      • [4.8 创建文件:`oled.c` --- OLED显示屏驱动](#4.8 创建文件:oled.c — OLED显示屏驱动)
      • [4.9 创建文件:`oled.h` --- OLED头文件](#4.9 创建文件:oled.h — OLED头文件)
      • [4.10 创建文件:`oled_font.h` --- OLED字库](#4.10 创建文件:oled_font.h — OLED字库)
      • [4.11 创建文件:`stepper.c` --- 步进电机驱动](#4.11 创建文件:stepper.c — 步进电机驱动)
      • [4.12 创建文件:`stepper.h` --- 步进电机头文件](#4.12 创建文件:stepper.h — 步进电机头文件)
      • [4.13 创建文件:`key.c` --- 按键扫描驱动](#4.13 创建文件:key.c — 按键扫描驱动)
      • [4.14 创建文件:`key.h` --- 按键头文件](#4.14 创建文件:key.h — 按键头文件)
      • [4.15 创建文件:`control.c` --- 核心控制逻辑](#4.15 创建文件:control.c — 核心控制逻辑)
      • [4.16 创建文件:`control.h` --- 控制逻辑头文件](#4.16 创建文件:control.h — 控制逻辑头文件)
      • [4.17 创建文件:`main.c` --- 主程序入口](#4.17 创建文件:main.c — 主程序入口)
    • 五、程序控制流程
    • 六、温度回差控制算法详解
    • 七、编译与烧录
      • [7.1 工程编译设置](#7.1 工程编译设置)
      • [7.2 烧录程序](#7.2 烧录程序)
    • 八、实际安装与调试
      • [8.1 孵化箱体制作建议](#8.1 孵化箱体制作建议)
      • [8.2 翻蛋机构机械结构](#8.2 翻蛋机构机械结构)
      • [8.3 传感器安装位置](#8.3 传感器安装位置)
      • [8.4 上电调试步骤](#8.4 上电调试步骤)
    • 九、常见问题与解决方案

一、项目概述与系统设计

本项目将手把手教你制作一个智能鹌鹑孵化箱控制系统,基于STM32F103C8T6微控制器,集成DHT22温湿度传感器、DS18B20备用温度传感器、步进电机翻蛋机构、0.96寸OLED显示屏、继电器加热加湿控制以及按键设置功能。整个系统能够自动维持孵化箱内温度在37.8°C左右、湿度在55%-65%之间,并每隔2小时自动翻蛋一次,模拟母鹌鹑的自然孵化过程。
STM32F103C8T6 主控芯片
DHT22 温湿度传感器
DS18B20 温度传感器
0.96寸 OLED显示屏
步进电机驱动模块
继电器模块1 - 加热
继电器模块2 - 加湿
4个独立按键
蜂鸣器报警
28BYJ-48 步进电机
加热片/加热灯
加湿器/雾化片

二、硬件清单与接线说明

2.1 所需元器件清单

序号 元器件名称 规格型号 数量
1 主控板 STM32F103C8T6最小系统板 1
2 温湿度传感器 DHT22 1
3 温度传感器 DS18B20防水探头 1
4 OLED显示屏 0.96寸 IIC接口 SSD1306 1
5 步进电机 28BYJ-48 5V 1
6 步进电机驱动 ULN2003驱动板 1
7 继电器模块 2路5V继电器模块 1
8 按键 6x6x5mm 微动开关 4
9 蜂鸣器 有源蜂鸣器模块 5V 1
10 加热元件 12V 50W PTC加热片 1
11 加湿器 5V超声波雾化片模块 1
12 电源模块 12V/5V双路输出开关电源 1
13 杜邦线 公母/母母杜邦线 若干
14 上拉电阻 4.7KΩ 1/4W 2

2.2 引脚接线对照表

STM32F103C8T6 引脚分配:

复制代码
DHT22 数据引脚    ------ PA0
DS18B20 数据引脚  ------ PA1
OLED SCL         ------ PB6 (I2C1_SCL)
OLED SDA         ------ PB7 (I2C1_SDA)
步进电机 IN1     ------ PB0
步进电机 IN2     ------ PB1
步进电机 IN3     ------ PB3
步进电机 IN4     ------ PB4
继电器1(加热)    ------ PB12
继电器2(加湿)    ------ PB13
按键 K1(菜单)    ------ PA4
按键 K2(增加)    ------ PA5
按键 K3(减少)    ------ PA6
按键 K4(确认)    ------ PA7
蜂鸣器           ------ PB14

2.3 完整接线说明

DHT22接线:

  • VCC → 3.3V
  • GND → GND
  • DATA → PA0(需外接4.7KΩ上拉电阻到3.3V)

DS18B20接线:

  • 红线(VCC) → 3.3V
  • 黑线(GND) → GND
  • 黄线(DATA) → PA1(需外接4.7KΩ上拉电阻到3.3V)

OLED显示屏接线(IIC接口):

  • VCC → 3.3V
  • GND → GND
  • SCL → PB6
  • SDA → PB7

ULN2003步进电机驱动板接线:

  • IN1 → PB0
  • IN2 → PB1
  • IN3 → PB3
  • IN4 → PB4
  • VCC → 5V
  • GND → GND
  • 电机插头插入驱动板白色插座

继电器模块接线:

  • VCC → 5V
  • GND → GND
  • IN1 → PB12(加热控制)
  • IN2 → PB13(加湿控制)
  • COM端接12V电源正极
  • NO端接加热片正极
  • 加热片负极接12V电源负极

三、软件开发环境搭建

3.1 安装Keil MDK-ARM

前往Keil官网下载MDK-ARM开发工具,版本推荐5.36及以上。安装完成后,需要安装STM32F1系列的设备支持包。

3.2 创建工程步骤

  1. 打开Keil,点击 Project → New μVision Project
  2. 选择保存路径,输入工程名称 Incubator_Controller
  3. 在芯片选择界面,选择 STMicroelectronics → STM32F103C8
  4. 在弹出的运行时环境管理界面,勾选以下组件:
    • CMSIS → CORE
    • Device → Startup
    • Device → StdPeriph Drivers → Framework
    • Device → StdPeriph Drivers → GPIO
    • Device → StdPeriph Drivers → RCC
    • Device → StdPeriph Drivers → TIM
    • Device → StdPeriph Drivers → USART(调试用)

3.3 添加标准外设库文件

将以下标准外设库源文件添加到工程中:

  • stm32f10x_gpio.c
  • stm32f10x_rcc.c
  • stm32f10x_tim.c
  • stm32f10x_usart.c
  • stm32f10x_i2c.c
  • misc.c

四、核心驱动代码编写

4.1 创建文件:main.h --- 主头文件

c 复制代码
/**
 * @file    main.h
 * @brief   智能鹌鹑孵化箱主头文件
 * @author  Incubator_Project
 * @date    2026
 */

#ifndef __MAIN_H
#define __MAIN_H

#include "stm32f10x.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* ==================== 系统参数宏定义 ==================== */
/* 孵化温度设定范围 */
#define TEMP_MIN             36.0f       /* 最低允许温度 */
#define TEMP_MAX             39.0f       /* 最高允许温度 */
#define TEMP_DEFAULT          37.8f      /* 默认孵化温度 */
#define TEMP_HYSTERESIS      0.3f       /* 温度回差控制 */

/* 孵化湿度设定范围 */
#define HUMI_MIN             45.0f       /* 最低允许湿度 */
#define HUMI_MAX             70.0f       /* 最高允许湿度 */
#define HUMI_DEFAULT          60.0f      /* 默认孵化湿度 */
#define HUMI_HYSTERESIS      3.0f       /* 湿度回差控制 */

/* 翻蛋参数 */
#define EGG_TURN_INTERVAL    7200       /* 翻蛋间隔时间(秒) 2小时 */
#define EGG_TURN_ANGLE       180        /* 翻蛋角度(度) */
#define EGG_TURN_STEPS       2048       /* 28BYJ-48 步进电机2048步=360度 */

/* 报警参数 */
#define ALARM_TEMP_HIGH      39.5f       /* 高温报警阈值 */
#define ALARM_TEMP_LOW       36.0f       /* 低温报警阈值 */
#define ALARM_HUMI_HIGH      75.0f       /* 高湿报警阈值 */
#define ALARM_HUMI_LOW       40.0f       /* 低湿报警阈值 */

/* ==================== 引脚定义 ==================== */
/* DHT22 温湿度传感器 */
#define DHT22_PORT           GPIOA
#define DHT22_PIN            GPIO_Pin_0
#define DHT22_RCC            RCC_APB2Periph_GPIOA

/* DS18B20 温度传感器 */
#define DS18B20_PORT         GPIOA
#define DS18B20_PIN          GPIO_Pin_1
#define DS18B20_RCC          RCC_APB2Periph_GPIOA

/* I2C 接口 (OLED) */
#define I2C_SCL_PORT         GPIOB
#define I2C_SCL_PIN          GPIO_Pin_6
#define I2C_SDA_PORT         GPIOB
#define I2C_SDA_PIN          GPIO_Pin_7
#define I2C_RCC              RCC_APB2Periph_GPIOB

/* 步进电机 (ULN2003) */
#define STEPPER_PORT         GPIOB
#define STEPPER_IN1_PIN      GPIO_Pin_0
#define STEPPER_IN2_PIN      GPIO_Pin_1
#define STEPPER_IN3_PIN      GPIO_Pin_3
#define STEPPER_IN4_PIN      GPIO_Pin_4
#define STEPPER_RCC          RCC_APB2Periph_GPIOB
#define STEPPER_ALL_PINS     (STEPPER_IN1_PIN | STEPPER_IN2_PIN | \
                              STEPPER_IN3_PIN | STEPPER_IN4_PIN)

/* 继电器控制 */
#define RELAY_PORT           GPIOB
#define RELAY_HEAT_PIN       GPIO_Pin_12   /* 加热继电器 */
#define RELAY_HUMI_PIN       GPIO_Pin_13   /* 加湿继电器 */
#define RELAY_RCC            RCC_APB2Periph_GPIOB

/* 按键 */
#define KEY_PORT             GPIOA
#define KEY_MENU_PIN         GPIO_Pin_4    /* 菜单键 */
#define KEY_UP_PIN           GPIO_Pin_5    /* 增加键 */
#define KEY_DOWN_PIN         GPIO_Pin_6    /* 减少键 */
#define KEY_OK_PIN           GPIO_Pin_7    /* 确认键 */
#define KEY_RCC              RCC_APB2Periph_GPIOA
#define KEY_ALL_PINS         (KEY_MENU_PIN | KEY_UP_PIN | \
                              KEY_DOWN_PIN | KEY_OK_PIN)

/* 蜂鸣器 */
#define BUZZER_PORT          GPIOB
#define BUZZER_PIN           GPIO_Pin_14
#define BUZZER_RCC           RCC_APB2Periph_GPIOB

/* ==================== 系统状态枚举 ==================== */
typedef enum {
    SYSTEM_INIT = 0,         /* 系统初始化状态 */
    SYSTEM_RUNNING,          /* 正常运行状态 */
    SYSTEM_ALARM,            /* 报警状态 */
    SYSTEM_SETTING           /* 设置状态 */
} SystemState_t;

typedef enum {
    MENU_MAIN = 0,           /* 主显示界面 */
    MENU_SET_TEMP,           /* 设置温度 */
    MENU_SET_HUMI,           /* 设置湿度 */
    MENU_SET_INTERVAL,       /* 设置翻蛋间隔 */
    MENU_MANUAL_TURN,        /* 手动翻蛋 */
    MENU_CALIBRATE           /* 校准传感器 */
} MenuState_t;

/* ==================== 全局结构体 ==================== */
typedef struct {
    float temperature;       /* 当前温度 (°C) */
    float humidity;          /* 当前湿度 (%) */
    float targetTemp;        /* 目标温度 */
    float targetHumi;        /* 目标湿度 */
    uint8_t heatStatus;      /* 加热状态 0=关闭 1=开启 */
    uint8_t humiStatus;      /* 加湿状态 0=关闭 1=开启 */
    uint32_t turnInterval;   /* 翻蛋间隔(秒) */
    uint32_t lastTurnTime;   /* 上次翻蛋时间戳 */
    uint16_t turnCount;      /* 翻蛋计数 */
    uint8_t eggPosition;     /* 当前蛋位置 0=原位 1=翻转后 */
    SystemState_t sysState;  /* 系统状态 */
    MenuState_t menuState;   /* 菜单状态 */
    uint8_t alarmFlag;       /* 报警标志 */
    uint8_t displayUpdate;   /* 显示更新标志 */
} SystemData_t;

/* ==================== 函数声明 ==================== */
/* 系统初始化 */
void SystemClock_Config(void);
void GPIO_Configuration(void);
void Timer_Configuration(void);
void NVIC_Configuration(void);

/* 传感器驱动 */
uint8_t DHT22_Read(float *temperature, float *humidity);
uint8_t DS18B20_Read(float *temperature);

/* 输出控制 */
void Relay_Heat(uint8_t status);
void Relay_Humi(uint8_t status);
void Stepper_Rotate(uint16_t steps, uint8_t direction);
void Buzzer_Beep(uint16_t duration_ms);

/* 显示驱动 */
void OLED_Init(void);
void OLED_Display(void);

/* 按键处理 */
uint8_t Key_Scan(void);
void Key_Process(uint8_t keyValue);

/* 控制逻辑 */
void TempControl_Process(void);
void HumiControl_Process(void);
void EggTurn_Process(void);
void Alarm_Check(void);

/* 延时函数 */
void Delay_ms(uint32_t ms);
void Delay_us(uint32_t us);

/* 全局变量 */
extern SystemData_t g_sysData;
extern volatile uint32_t g_sysTick;

#endif /* __MAIN_H */

4.2 创建文件:delay.c --- 延时函数实现

c 复制代码
/**
 * @file    delay.c
 * @brief   精确延时函数实现
 */

#include "delay.h"

static uint8_t  fac_us = 0;    /* us延时倍乘数 */
static uint16_t fac_ms = 0;    /* ms延时倍乘数 */

/**
 * @brief  初始化延时函数
 * @note   使用SysTick定时器,时钟72MHz
 */
void Delay_Init(void)
{
    /* SystemCoreClock / 8000000 = 72MHz / 8M = 9 */
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
    fac_us = SystemCoreClock / 8000000;     /* 9 */
    fac_ms = (uint16_t)fac_us * 1000;        /* 9000 */
}

/**
 * @brief  微秒级延时
 * @param  nus: 延时的微秒数(0~233015)
 */
void Delay_us(uint32_t nus)
{
    uint32_t temp;
    SysTick->LOAD = nus * fac_us;              /* 加载计数值 */
    SysTick->VAL = 0x00;                        /* 清空计数器 */
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;  /* 使能SysTick */
    do
    {
        temp = SysTick->CTRL;
    } while ((temp & 0x01) && !(temp & (1 << 16))); /* 等待计数完成 */
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;  /* 关闭SysTick */
    SysTick->VAL = 0x00;                        /* 清空计数器 */
}

/**
 * @brief  毫秒级延时
 * @param  nms: 延时的毫秒数
 */
void Delay_ms(uint32_t nms)
{
    uint32_t temp;
    SysTick->LOAD = (uint32_t)nms * fac_ms;    /* 加载计数值 */
    SysTick->VAL = 0x00;                        /* 清空计数器 */
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;  /* 使能SysTick */
    do
    {
        temp = SysTick->CTRL;
    } while ((temp & 0x01) && !(temp & (1 << 16))); /* 等待计数完成 */
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;  /* 关闭SysTick */
    SysTick->VAL = 0x00;                        /* 清空计数器 */
}

4.3 创建文件:delay.h --- 延时函数头文件

c 复制代码
/**
 * @file    delay.h
 * @brief   延时函数头文件
 */

#ifndef __DELAY_H
#define __DELAY_H

#include "stm32f10x.h"

void Delay_Init(void);
void Delay_us(uint32_t nus);
void Delay_ms(uint32_t nms);

#endif /* __DELAY_H */

4.4 创建文件:dht22.c --- DHT22温湿度传感器驱动

c 复制代码
/**
 * @file    dht22.c
 * @brief   DHT22 数字温湿度传感器驱动
 * @note    单总线通信协议,数据格式:40位(湿度高8+湿度低8+温度高8+温度低8+校验8)
 */

#include "dht22.h"
#include "delay.h"

/**
 * @brief  设置DHT22数据引脚为输出模式
 */
static void DHT22_Pin_Output(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    GPIO_InitStructure.GPIO_Pin = DHT22_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;   /* 推挽输出 */
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DHT22_PORT, &GPIO_InitStructure);
}

/**
 * @brief  设置DHT22数据引脚为输入模式
 */
static void DHT22_Pin_Input(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    GPIO_InitStructure.GPIO_Pin = DHT22_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;      /* 上拉输入 */
    GPIO_Init(DHT22_PORT, &GPIO_InitStructure);
}

/**
 * @brief  DHT22初始化
 */
void DHT22_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* 使能GPIO时钟 */
    RCC_APB2PeriphClockCmd(DHT22_RCC, ENABLE);

    /* 默认配置为输出模式 */
    GPIO_InitStructure.GPIO_Pin = DHT22_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DHT22_PORT, &GPIO_InitStructure);

    /* 初始拉高总线 */
    GPIO_SetBits(DHT22_PORT, DHT22_PIN);
}

/**
 * @brief  读取DHT22的一位数据
 * @return uint8_t: 读取到的位值(0或1)
 */
static uint8_t DHT22_Read_Bit(void)
{
    uint8_t retry = 0;

    /* 等待低电平结束(传感器响应信号) */
    while (GPIO_ReadInputDataBit(DHT22_PORT, DHT22_PIN) == 0)
    {
        retry++;
        Delay_us(1);
        if (retry > 100) return 0;
    }

    /* 延时40us后读取电平状态 */
    Delay_us(40);

    if (GPIO_ReadInputDataBit(DHT22_PORT, DHT22_PIN) == 1)
    {
        /* 等待高电平结束 */
        retry = 0;
        while (GPIO_ReadInputDataBit(DHT22_PORT, DHT22_PIN) == 1)
        {
            retry++;
            Delay_us(1);
            if (retry > 100) return 0;
        }
        return 1;   /* 高电平持续时间长表示1 */
    }
    else
    {
        return 0;   /* 低电平表示0 */
    }
}

/**
 * @brief  读取DHT22一个字节数据
 * @return uint8_t: 读取到的8位数据
 */
static uint8_t DHT22_Read_Byte(void)
{
    uint8_t i;
    uint8_t data = 0;

    for (i = 0; i < 8; i++)
    {
        data <<= 1;                    /* 左移一位 */
        if (DHT22_Read_Bit() == 1)     /* 读取1位 */
        {
            data |= 0x01;
        }
    }

    return data;
}

/**
 * @brief  读取DHT22温湿度数据
 * @param  temperature: 温度值输出指针(°C)
 * @param  humidity: 湿度值输出指针(%)
 * @return uint8_t: 0=成功, 1=失败
 */
uint8_t DHT22_Read(float *temperature, float *humidity)
{
    uint8_t buf[5] = {0};    /* 40位数据缓冲区 */
    uint8_t i;
    uint8_t checkSum;

    /* ===== 步骤1: 主机发送起始信号 ===== */
    DHT22_Pin_Output();                    /* 设置为输出模式 */
    GPIO_ResetBits(DHT22_PORT, DHT22_PIN); /* 拉低总线 */
    Delay_ms(2);                           /* 保持低电平至少1ms(这里用2ms) */
    GPIO_SetBits(DHT22_PORT, DHT22_PIN);   /* 拉高总线 */
    Delay_us(30);                          /* 保持高电平20~40us */

    /* ===== 步骤2: 切换到输入模式等待传感器响应 ===== */
    DHT22_Pin_Input();

    /* 等待传感器拉低总线(响应信号,约80us低电平) */
    uint16_t retry = 0;
    while (GPIO_ReadInputDataBit(DHT22_PORT, DHT22_PIN) == 1)
    {
        retry++;
        Delay_us(1);
        if (retry > 200) return 1;  /* 超时,传感器无响应 */
    }

    /* 等待传感器释放总线(约80us高电平) */
    retry = 0;
    while (GPIO_ReadInputDataBit(DHT22_PORT, DHT22_PIN) == 0)
    {
        retry++;
        Delay_us(1);
        if (retry > 200) return 1;  /* 超时 */
    }

    retry = 0;
    while (GPIO_ReadInputDataBit(DHT22_PORT, DHT22_PIN) == 1)
    {
        retry++;
        Delay_us(1);
        if (retry > 200) return 1;  /* 超时 */
    }

    /* ===== 步骤3: 读取40位数据 ===== */
    for (i = 0; i < 5; i++)
    {
        buf[i] = DHT22_Read_Byte();
    }

    /* 恢复总线为输出模式并拉高 */
    DHT22_Pin_Output();
    GPIO_SetBits(DHT22_PORT, DHT22_PIN);

    /* ===== 步骤4: 校验数据 ===== */
    checkSum = buf[0] + buf[1] + buf[2] + buf[3];
    if (checkSum != buf[4])
    {
        return 2;  /* 校验失败 */
    }

    /* ===== 步骤5: 解析温湿度数据 ===== */
    /* DHT22 湿度 = 前两个字节组成16位值 / 10 */
    uint16_t humiRaw = ((uint16_t)buf[0] << 8) | buf[1];
    *humidity = (float)humiRaw / 10.0f;

    /* DHT22 温度 = 后两个字节组成16位值 / 10 */
    uint16_t tempRaw = ((uint16_t)buf[2] << 8) | buf[3];

    /* 检查温度符号位(最高位为1表示负温度) */
    if (tempRaw & 0x8000)
    {
        tempRaw &= 0x7FFF;  /* 清除符号位 */
        *temperature = -(float)tempRaw / 10.0f;
    }
    else
    {
        *temperature = (float)tempRaw / 10.0f;
    }

    return 0;  /* 读取成功 */
}

4.5 创建文件:dht22.h --- DHT22头文件

c 复制代码
/**
 * @file    dht22.h
 * @brief   DHT22 传感器驱动头文件
 */

#ifndef __DHT22_H
#define __DHT22_H

#include "stm32f10x.h"
#include "main.h"

void DHT22_Init(void);
uint8_t DHT22_Read(float *temperature, float *humidity);

#endif /* __DHT22_H */

4.6 创建文件:ds18b20.c --- DS18B20温度传感器驱动

c 复制代码
/**
 * @file    ds18b20.c
 * @brief   DS18B20 数字温度传感器驱动
 * @note    单总线协议,12位分辨率,精度±0.5°C
 */

#include "ds18b20.h"
#include "delay.h"

/**
 * @brief  复位DS18B20并检测存在脉冲
 * @return uint8_t: 0=成功检测到设备, 1=未检测到设备
 */
static uint8_t DS18B20_Reset(void)
{
    uint8_t presence;

    /* 配置为输出模式 */
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = DS18B20_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DS18B20_PORT, &GPIO_InitStructure);

    /* 主机拉低总线至少480us */
    GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN);
    Delay_us(500);

    /* 主机释放总线,拉高 */
    GPIO_SetBits(DS18B20_PORT, DS18B20_PIN);
    Delay_us(60);

    /* 切换到输入模式检测存在脉冲 */
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(DS18B20_PORT, &GPIO_InitStructure);

    /* 读取存在脉冲,DS18B20会拉低总线60~240us */
    presence = GPIO_ReadInputDataBit(DS18B20_PORT, DS18B20_PIN);

    /* 等待存在脉冲结束 */
    Delay_us(420);

    /* 恢复为输出模式 */
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(DS18B20_PORT, &GPIO_InitStructure);
    GPIO_SetBits(DS18B20_PORT, DS18B20_PIN);

    return presence;  /* 0表示检测到设备 */
}

/**
 * @brief  向DS18B20写入一个位
 * @param  bit: 要写入的位值(0或1)
 */
static void DS18B20_Write_Bit(uint8_t bit)
{
    if (bit)
    {
        /* 写1: 拉低1~15us后释放 */
        GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN);
        Delay_us(2);       /* 保持低电平2us */
        GPIO_SetBits(DS18B20_PORT, DS18B20_PIN);
        Delay_us(60);      /* 等待60us让DS18B20采样 */
    }
    else
    {
        /* 写0: 拉低60~120us */
        GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN);
        Delay_us(65);      /* 保持低电平65us */
        GPIO_SetBits(DS18B20_PORT, DS18B20_PIN);
        Delay_us(5);       /* 释放总线5us */
    }
}

/**
 * @brief  从DS18B20读取一个位
 * @return uint8_t: 读取到的位值
 */
static uint8_t DS18B20_Read_Bit(void)
{
    uint8_t bit;
    GPIO_InitTypeDef GPIO_InitStructure;

    /* 主机拉低总线1~15us */
    GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN);
    Delay_us(2);

    /* 释放总线 */
    GPIO_SetBits(DS18B20_PORT, DS18B20_PIN);
    Delay_us(12);

    /* 切换输入模式读取 */
    GPIO_InitStructure.GPIO_Pin = DS18B20_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DS18B20_PORT, &GPIO_InitStructure);

    bit = GPIO_ReadInputDataBit(DS18B20_PORT, DS18B20_PIN);

    /* 恢复输出模式 */
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Init(DS18B20_PORT, &GPIO_InitStructure);

    Delay_us(50);  /* 等待读时序完成 */
    return bit;
}

/**
 * @brief  向DS18B20写入一个字节
 * @param  data: 要写入的数据
 */
static void DS18B20_Write_Byte(uint8_t data)
{
    uint8_t i;
    for (i = 0; i < 8; i++)
    {
        DS18B20_Write_Bit(data & 0x01);  /* 先写最低位 */
        data >>= 1;
    }
}

/**
 * @brief  从DS18B20读取一个字节
 * @return uint8_t: 读取到的数据
 */
static uint8_t DS18B20_Read_Byte(void)
{
    uint8_t i;
    uint8_t data = 0;

    for (i = 0; i < 8; i++)
    {
        if (DS18B20_Read_Bit())
        {
            data |= (0x01 << i);  /* 先读最低位 */
        }
    }
    return data;
}

/**
 * @brief  DS18B20初始化
 */
void DS18B20_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(DS18B20_RCC, ENABLE);

    GPIO_InitStructure.GPIO_Pin = DS18B20_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DS18B20_PORT, &GPIO_InitStructure);

    GPIO_SetBits(DS18B20_PORT, DS18B20_PIN);
    Delay_ms(10);
}

/**
 * @brief  读取DS18B20温度值
 * @param  temperature: 温度值输出指针(°C)
 * @return uint8_t: 0=成功, 1=失败
 */
uint8_t DS18B20_Read(float *temperature)
{
    uint8_t tempLSB, tempMSB;
    int16_t tempRaw;

    /* 复位并检测设备 */
    if (DS18B20_Reset())
    {
        return 1;  /* 未检测到设备 */
    }

    /* 跳过ROM匹配(单设备使用时) */
    DS18B20_Write_Byte(0xCC);  /* SKIP ROM命令 */
    /* 启动温度转换 */
    DS18B20_Write_Byte(0x44);  /* CONVERT T命令 */

    /* 等待转换完成(12位分辨率需要最大750ms) */
    Delay_ms(800);

    /* 重新复位 */
    if (DS18B20_Reset())
    {
        return 1;
    }

    /* 跳过ROM匹配 */
    DS18B20_Write_Byte(0xCC);
    /* 读取暂存器数据 */
    DS18B20_Write_Byte(0xBE);  /* READ SCRATCHPAD命令 */

    /* 读取9个字节(我们只需要前2个温度字节) */
    tempLSB = DS18B20_Read_Byte();  /* 温度低字节 */
    tempMSB = DS18B20_Read_Byte();  /* 温度高字节 */

    /* 跳过其余7个字节 */
    for (uint8_t i = 0; i < 7; i++)
    {
        DS18B20_Read_Byte();
    }

    /* 合成16位原始温度值 */
    tempRaw = ((int16_t)tempMSB << 8) | tempLSB;

    /* 转换为实际温度(12位分辨率,每LSB=0.0625°C) */
    *temperature = (float)tempRaw * 0.0625f;

    return 0;
}

4.7 创建文件:ds18b20.h --- DS18B20头文件

c 复制代码
/**
 * @file    ds18b20.h
 * @brief   DS18B20 温度传感器驱动头文件
 */

#ifndef __DS18B20_H
#define __DS18B20_H

#include "stm32f10x.h"
#include "main.h"

void DS18B20_Init(void);
uint8_t DS18B20_Read(float *temperature);

#endif /* __DS18B20_H */

4.8 创建文件:oled.c --- OLED显示屏驱动

c 复制代码
/**
 * @file    oled.c
 * @brief   0.96寸OLED显示屏驱动 (SSD1306, IIC接口, 128x64像素)
 * @note    支持中英文混合显示,使用软件IIC通信
 */

#include "oled.h"
#include "delay.h"
#include "oled_font.h"  /* 字库文件 */
#include <string.h>
#include <stdio.h>

/* 全局显示缓冲区 (128x64像素 = 1024字节) */
static uint8_t OLED_Buffer[1024];

/**
 * @brief  软件IIC起始信号
 */
static void I2C_Start(void)
{
    /* SDA高,SCL高 */
    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    Delay_us(5);

    /* SDA拉低(在SCL高时SDA下降沿表示起始) */
    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);
    Delay_us(5);

    /* SCL拉低 */
    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
}

/**
 * @brief  软件IIC停止信号
 */
static void I2C_Stop(void)
{
    /* SDA低 */
    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);
    Delay_us(5);

    /* SCL拉高 */
    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    Delay_us(5);

    /* SDA拉高(在SCL高时SDA上升沿表示停止) */
    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
    Delay_us(5);
}

/**
 * @brief  IIC发送一个字节
 * @param  byte: 要发送的数据
 */
static void I2C_SendByte(uint8_t byte)
{
    uint8_t i;

    for (i = 0; i < 8; i++)
    {
        /* 准备数据(先发送最高位) */
        if (byte & 0x80)
        {
            GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
        }
        else
        {
            GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);
        }
        byte <<= 1;
        Delay_us(2);

        /* SCL上升沿,从机读取数据 */
        GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
        Delay_us(5);
        GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
        Delay_us(2);
    }
}

/**
 * @brief  等待从机应答
 * @return uint8_t: 0=有应答, 1=无应答
 */
static uint8_t I2C_WaitAck(void)
{
    uint8_t ack;
    uint16_t timeout = 0;

    /* 释放SDA */
    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
    Delay_us(2);

    /* SCL高电平期间读取SDA状态 */
    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);

    /* 等待从机拉低SDA或超时 */
    while (GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN) == 1)
    {
        timeout++;
        if (timeout > 1000)
        {
            GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
            return 1;  /* 无应答 */
        }
    }

    ack = 0;  /* 有应答 */
    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    return ack;
}

/**
 * @brief  写入OLED命令字节
 * @param  cmd: 命令字节
 */
static void OLED_WriteCmd(uint8_t cmd)
{
    I2C_Start();
    I2C_SendByte(0x78);   /* OLED地址: 0x3C << 1 = 0x78 */
    I2C_WaitAck();
    I2C_SendByte(0x00);   /* 控制字节: 0x00表示命令 */
    I2C_WaitAck();
    I2C_SendByte(cmd);    /* 命令数据 */
    I2C_WaitAck();
    I2C_Stop();
}

/**
 * @brief  写入OLED数据字节
 * @param  dat: 数据字节
 */
static void OLED_WriteData(uint8_t dat)
{
    I2C_Start();
    I2C_SendByte(0x78);
    I2C_WaitAck();
    I2C_SendByte(0x40);   /* 控制字节: 0x40表示数据 */
    I2C_WaitAck();
    I2C_SendByte(dat);
    I2C_WaitAck();
    I2C_Stop();
}

/**
 * @brief  设置OLED显示区域
 * @param  x: 列地址(0~127)
 * @param  y: 页地址(0~7,每页8行)
 */
static void OLED_SetPos(uint8_t x, uint8_t y)
{
    OLED_WriteCmd(0xB0 + y);                    /* 设置页地址 */
    OLED_WriteCmd(((x & 0xF0) >> 4) | 0x10);    /* 设置列地址高4位 */
    OLED_WriteCmd(x & 0x0F);                     /* 设置列地址低4位 */
}

/**
 * @brief  I2C接口初始化
 */
static void I2C_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(I2C_RCC, ENABLE);

    /* SCL引脚配置 */
    GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(I2C_SCL_PORT, &GPIO_InitStructure);

    /* SDA引脚配置 */
    GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;
    GPIO_Init(I2C_SDA_PORT, &GPIO_InitStructure);

    /* 初始拉高总线 */
    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
}

/**
 * @brief  OLED全屏填充
 * @param  color: 0x00全黑, 0xFF全亮
 */
void OLED_Fill(uint8_t color)
{
    uint8_t page, col;
    for (page = 0; page < 8; page++)
    {
        OLED_WriteCmd(0xB0 + page);
        OLED_WriteCmd(0x00);
        OLED_WriteCmd(0x10);
        for (col = 0; col < 128; col++)
        {
            OLED_WriteData(color);
        }
    }
}

/**
 * @brief  更新OLED显示缓冲
 */
void OLED_Refresh(void)
{
    uint8_t page, col;
    for (page = 0; page < 8; page++)
    {
        OLED_SetPos(0, page);
        for (col = 0; col < 128; col++)
        {
            OLED_WriteData(OLED_Buffer[page * 128 + col]);
        }
    }
}

/**
 * @brief  清空显示缓冲区
 */
void OLED_Clear(void)
{
    memset(OLED_Buffer, 0x00, sizeof(OLED_Buffer));
}

/**
 * @brief  在缓冲区指定位置画点
 * @param  x: X坐标(0~127)
 * @param  y: Y坐标(0~63)
 * @param  mode: 1=点亮, 0=熄灭
 */
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t mode)
{
    if (x > 127 || y > 63) return;

    if (mode)
    {
        OLED_Buffer[x + (y / 8) * 128] |= (0x01 << (y % 8));
    }
    else
    {
        OLED_Buffer[x + (y / 8) * 128] &= ~(0x01 << (y % 8));
    }
}

/**
 * @brief  显示6x8点阵字符
 * @param  x: 列起始位置
 * @param  y: 页起始位置(0~7)
 * @param  ch: 要显示的字符
 */
void OLED_ShowChar(uint8_t x, uint8_t y, char ch)
{
    uint8_t i;
    uint8_t *pFont;

    if (x > 121 || y > 7) return;

    pFont = (uint8_t *)&Font6x8[ch - 32];  /* 字符从空格(32)开始 */

    for (i = 0; i < 6; i++)
    {
        OLED_Buffer[x + i + y * 128] = pFont[i];
    }
}

/**
 * @brief  显示字符串
 * @param  x: 列起始位置
 * @param  y: 页起始位置
 * @param  str: 字符串指针
 */
void OLED_ShowString(uint8_t x, uint8_t y, const char *str)
{
    while (*str != '\0')
    {
        OLED_ShowChar(x, y, *str);
        x += 6;
        if (x > 121)
        {
            x = 0;
            y++;
        }
        if (y > 7) break;
        str++;
    }
}

/**
 * @brief  显示数字(整数)
 * @param  x: X坐标
 * @param  y: 页坐标
 * @param  num: 要显示的数字
 * @param  len: 数字位数
 */
void OLED_ShowNum(uint8_t x, uint8_t y, int32_t num, uint8_t len)
{
    char str[12];
    snprintf(str, sizeof(str), "%*ld", len, (long)num);
    OLED_ShowString(x, y, str);
}

/**
 * @brief  显示浮点数
 * @param  x: X坐标
 * @param  y: 页坐标
 * @param  num: 浮点数
 * @param  intLen: 整数位数
 * @param  decLen: 小数位数
 */
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t intLen, uint8_t decLen)
{
    char str[12];
    snprintf(str, sizeof(str), "%*.*f", intLen + decLen + 1, decLen, num);
    OLED_ShowString(x, y, str);
}

/**
 * @brief  OLED初始化序列
 */
void OLED_Init(void)
{
    /* 先初始化I2C接口 */
    I2C_Init();

    Delay_ms(200);  /* 等待OLED上电稳定 */

    /* SSD1306 初始化命令序列 */
    OLED_WriteCmd(0xAE);  /* 关闭显示 */

    OLED_WriteCmd(0x00);  /* 设置低列地址 */
    OLED_WriteCmd(0x10);  /* 设置高列地址 */

    OLED_WriteCmd(0x40);  /* 设置显示起始行 */

    OLED_WriteCmd(0xB0);  /* 设置页地址 */

    OLED_WriteCmd(0x81);  /* 对比度设置 */
    OLED_WriteCmd(0xFF);  /* 对比度值 0xFF最大 */

    OLED_WriteCmd(0xA1);  /* 列重映射:从左到右 */

    OLED_WriteCmd(0xA6);  /* 正常显示模式(非反色) */

    OLED_WriteCmd(0xA8);  /* 多路复用比 */
    OLED_WriteCmd(0x3F);  /* 64路 */

    OLED_WriteCmd(0xC8);  /* COM扫描方向:从下到上 */

    OLED_WriteCmd(0xD3);  /* 显示偏移 */
    OLED_WriteCmd(0x00);

    OLED_WriteCmd(0xD5);  /* 显示时钟分频 */
    OLED_WriteCmd(0x80);

    OLED_WriteCmd(0xD9);  /* 预充电周期 */
    OLED_WriteCmd(0xF1);

    OLED_WriteCmd(0xDA);  /* COM引脚配置 */
    OLED_WriteCmd(0x12);

    OLED_WriteCmd(0xDB);  /* VCOMH电压 */
    OLED_WriteCmd(0x40);

    OLED_WriteCmd(0x8D);  /* 充电泵设置 */
    OLED_WriteCmd(0x14);  /* 使能充电泵 */

    OLED_WriteCmd(0xAF);  /* 开启显示 */

    OLED_Clear();          /* 清空缓冲区 */
    OLED_Refresh();        /* 刷新显示 */
}

4.9 创建文件:oled.h --- OLED头文件

c 复制代码
/**
 * @file    oled.h
 * @brief   OLED显示屏驱动头文件
 */

#ifndef __OLED_H
#define __OLED_H

#include "stm32f10x.h"

/* OLED函数声明 */
void OLED_Init(void);
void OLED_Fill(uint8_t color);
void OLED_Refresh(void);
void OLED_Clear(void);
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t mode);
void OLED_ShowChar(uint8_t x, uint8_t y, char ch);
void OLED_ShowString(uint8_t x, uint8_t y, const char *str);
void OLED_ShowNum(uint8_t x, uint8_t y, int32_t num, uint8_t len);
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t intLen, uint8_t decLen);

#endif /* __OLED_H */

4.10 创建文件:oled_font.h --- OLED字库

c 复制代码
/**
 * @file    oled_font.h
 * @brief   OLED 6x8 ASCII字库
 * @note    包含ASCII 32~126号字符的点阵数据
 */

#ifndef __OLED_FONT_H
#define __OLED_FONT_H

#include "stm32f10x.h"

/* 6x8点阵ASCII字库 (每个字符6字节) */
static const uint8_t Font6x8[][6] = {
    /* 空格 ' ' (32) */
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    /* ! (33) */
    {0x00, 0x00, 0x5F, 0x00, 0x00, 0x00},
    /* " (34) */
    {0x00, 0x07, 0x00, 0x07, 0x00, 0x00},
    /* # (35) */
    {0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00},
    /* $ (36) */
    {0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00},
    /* % (37) */
    {0x23, 0x13, 0x08, 0x64, 0x62, 0x00},
    /* & (38) */
    {0x36, 0x49, 0x55, 0x22, 0x50, 0x00},
    /* ' (39) */
    {0x00, 0x05, 0x03, 0x00, 0x00, 0x00},
    /* ( (40) */
    {0x00, 0x1C, 0x22, 0x41, 0x00, 0x00},
    /* ) (41) */
    {0x00, 0x41, 0x22, 0x1C, 0x00, 0x00},
    /* * (42) */
    {0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00},
    /* + (43) */
    {0x08, 0x08, 0x3E, 0x08, 0x08, 0x00},
    /* , (44) */
    {0x00, 0x50, 0x30, 0x00, 0x00, 0x00},
    /* - (45) */
    {0x08, 0x08, 0x08, 0x08, 0x08, 0x00},
    /* . (46) */
    {0x00, 0x60, 0x60, 0x00, 0x00, 0x00},
    /* / (47) */
    {0x20, 0x10, 0x08, 0x04, 0x02, 0x00},
    /* 0 (48) */
    {0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00},
    /* 1 (49) */
    {0x00, 0x42, 0x7F, 0x40, 0x00, 0x00},
    /* 2 (50) */
    {0x42, 0x61, 0x51, 0x49, 0x46, 0x00},
    /* 3 (51) */
    {0x21, 0x41, 0x45, 0x4B, 0x31, 0x00},
    /* 4 (52) */
    {0x18, 0x14, 0x12, 0x7F, 0x10, 0x00},
    /* 5 (53) */
    {0x27, 0x45, 0x45, 0x45, 0x39, 0x00},
    /* 6 (54) */
    {0x3C, 0x4A, 0x49, 0x49, 0x30, 0x00},
    /* 7 (55) */
    {0x01, 0x71, 0x09, 0x05, 0x03, 0x00},
    /* 8 (56) */
    {0x36, 0x49, 0x49, 0x49, 0x36, 0x00},
    /* 9 (57) */
    {0x06, 0x49, 0x49, 0x29, 0x1E, 0x00},
    /* : (58) */
    {0x00, 0x36, 0x36, 0x00, 0x00, 0x00},
    /* ; (59) */
    {0x00, 0x56, 0x36, 0x00, 0x00, 0x00},
    /* < (60) */
    {0x00, 0x08, 0x14, 0x22, 0x41, 0x00},
    /* = (61) */
    {0x14, 0x14, 0x14, 0x14, 0x14, 0x00},
    /* > (62) */
    {0x41, 0x22, 0x14, 0x08, 0x00, 0x00},
    /* ? (63) */
    {0x02, 0x01, 0x51, 0x09, 0x06, 0x00},
    /* @ (64) */
    {0x32, 0x49, 0x79, 0x41, 0x3E, 0x00},
    /* A (65) */
    {0x7E, 0x11, 0x11, 0x11, 0x7E, 0x00},
    /* B (66) */
    {0x7F, 0x49, 0x49, 0x49, 0x36, 0x00},
    /* C (67) */
    {0x3E, 0x41, 0x41, 0x41, 0x22, 0x00},
    /* D (68) */
    {0x7F, 0x41, 0x41, 0x22, 0x1C, 0x00},
    /* E (69) */
    {0x7F, 0x49, 0x49, 0x49, 0x41, 0x00},
    /* F (70) */
    {0x7F, 0x09, 0x09, 0x01, 0x01, 0x00},
    /* G (71) */
    {0x3E, 0x41, 0x41, 0x51, 0x32, 0x00},
    /* H (72) */
    {0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00},
    /* I (73) */
    {0x00, 0x41, 0x7F, 0x41, 0x00, 0x00},
    /* J (74) */
    {0x20, 0x40, 0x41, 0x3F, 0x01, 0x00},
    /* K (75) */
    {0x7F, 0x08, 0x14, 0x22, 0x41, 0x00},
    /* L (76) */
    {0x7F, 0x40, 0x40, 0x40, 0x40, 0x00},
    /* M (77) */
    {0x7F, 0x02, 0x04, 0x02, 0x7F, 0x00},
    /* N (78) */
    {0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00},
    /* O (79) */
    {0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00},
    /* P (80) */
    {0x7F, 0x09, 0x09, 0x09, 0x06, 0x00},
    /* Q (81) */
    {0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00},
    /* R (82) */
    {0x7F, 0x09, 0x19, 0x29, 0x46, 0x00},
    /* S (83) */
    {0x46, 0x49, 0x49, 0x49, 0x31, 0x00},
    /* T (84) */
    {0x01, 0x01, 0x7F, 0x01, 0x01, 0x00},
    /* U (85) */
    {0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00},
    /* V (86) */
    {0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00},
    /* W (87) */
    {0x7F, 0x20, 0x18, 0x20, 0x7F, 0x00},
    /* X (88) */
    {0x63, 0x14, 0x08, 0x14, 0x63, 0x00},
    /* Y (89) */
    {0x03, 0x04, 0x78, 0x04, 0x03, 0x00},
    /* Z (90) */
    {0x61, 0x51, 0x49, 0x45, 0x43, 0x00},
    /* [ (91) */
    {0x00, 0x00, 0x7F, 0x41, 0x41, 0x00},
    /* \ (92) */
    {0x02, 0x04, 0x08, 0x10, 0x20, 0x00},
    /* ] (93) */
    {0x41, 0x41, 0x7F, 0x00, 0x00, 0x00},
    /* ^ (94) */
    {0x04, 0x02, 0x01, 0x02, 0x04, 0x00},
    /* _ (95) */
    {0x40, 0x40, 0x40, 0x40, 0x40, 0x00},
    /* ` (96) */
    {0x00, 0x01, 0x02, 0x04, 0x00, 0x00},
    /* a (97) */
    {0x20, 0x54, 0x54, 0x54, 0x78, 0x00},
    /* b (98) */
    {0x7F, 0x48, 0x44, 0x44, 0x38, 0x00},
    /* c (99) */
    {0x38, 0x44, 0x44, 0x44, 0x20, 0x00},
    /* d (100) */
    {0x38, 0x44, 0x44, 0x48, 0x7F, 0x00},
    /* e (101) */
    {0x38, 0x54, 0x54, 0x54, 0x18, 0x00},
    /* f (102) */
    {0x08, 0x7E, 0x09, 0x01, 0x02, 0x00},
    /* g (103) */
    {0x08, 0x14, 0x54, 0x54, 0x3C, 0x00},
    /* h (104) */
    {0x7F, 0x08, 0x04, 0x04, 0x78, 0x00},
    /* i (105) */
    {0x00, 0x44, 0x7D, 0x40, 0x00, 0x00},
    /* j (106) */
    {0x20, 0x40, 0x44, 0x3D, 0x00, 0x00},
    /* k (107) */
    {0x00, 0x7F, 0x10, 0x28, 0x44, 0x00},
    /* l (108) */
    {0x00, 0x41, 0x7F, 0x40, 0x00, 0x00},
    /* m (109) */
    {0x7C, 0x04, 0x18, 0x04, 0x78, 0x00},
    /* n (110) */
    {0x7C, 0x08, 0x04, 0x04, 0x78, 0x00},
    /* o (111) */
    {0x38, 0x44, 0x44, 0x44, 0x38, 0x00},
    /* p (112) */
    {0x7C, 0x14, 0x14, 0x14, 0x08, 0x00},
    /* q (113) */
    {0x08, 0x14, 0x14, 0x18, 0x7C, 0x00},
    /* r (114) */
    {0x7C, 0x08, 0x04, 0x04, 0x08, 0x00},
    /* s (115) */
    {0x48, 0x54, 0x54, 0x54, 0x20, 0x00},
    /* t (116) */
    {0x04, 0x3F, 0x44, 0x40, 0x20, 0x00},
    /* u (117) */
    {0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00},
    /* v (118) */
    {0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00},
    /* w (119) */
    {0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00},
    /* x (120) */
    {0x44, 0x28, 0x10, 0x28, 0x44, 0x00},
    /* y (121) */
    {0x0C, 0x50, 0x50, 0x50, 0x3C, 0x00},
    /* z (122) */
    {0x44, 0x64, 0x54, 0x4C, 0x44, 0x00},
    /* { (123) */
    {0x00, 0x08, 0x36, 0x41, 0x00, 0x00},
    /* | (124) */
    {0x00, 0x00, 0x7F, 0x00, 0x00, 0x00},
    /* } (125) */
    {0x00, 0x41, 0x36, 0x08, 0x00, 0x00},
    /* ~ (126) */
    {0x08, 0x04, 0x08, 0x10, 0x08, 0x00},
};

#endif /* __OLED_FONT_H */

4.11 创建文件:stepper.c --- 步进电机驱动

c 复制代码
/**
 * @file    stepper.c
 * @brief   28BYJ-48步进电机驱动 (ULN2003)
 * @note    使用4相8拍驱动方式,步距角5.625°/64减速比
 *          一圈需要 360° / (5.625°/64) * 8拍 = 4096步
 *          实际测量约2048步一圈(因制造公差)
 */

#include "stepper.h"
#include "delay.h"

/* 8拍驱动相序表(4相步进电机) */
static const uint8_t StepTable[8] = {
    0x08,  /* 0b1000 - A相通电 */
    0x0C,  /* 0b1100 - AB相通电 */
    0x04,  /* 0b0100 - B相通电 */
    0x06,  /* 0b0110 - BC相通电 */
    0x02,  /* 0b0010 - C相通电 */
    0x03,  /* 0b0011 - CD相通电 */
    0x01,  /* 0b0001 - D相通电 */
    0x09,  /* 0b1001 - DA相通电 */
};

/**
 * @brief  步进电机GPIO初始化
 */
void Stepper_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(STEPPER_RCC, ENABLE);

    GPIO_InitStructure.GPIO_Pin = STEPPER_ALL_PINS;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(STEPPER_PORT, &GPIO_InitStructure);

    /* 初始全部拉低,电机断电 */
    GPIO_ResetBits(STEPPER_PORT, STEPPER_ALL_PINS);
}

/**
 * @brief  步进电机转动指定步数
 * @param  steps: 要转动的步数
 * @param  direction: 0=逆时针(反向), 1=顺时针(正向)
 * @note   电机速度由延时控制,调节Delay_us参数可改变转速
 */
void Stepper_Rotate(uint16_t steps, uint8_t direction)
{
    uint16_t i;
    uint8_t stepIndex;

    for (i = 0; i < steps; i++)
    {
        if (direction)  /* 顺时针 */
        {
            stepIndex = i % 8;
        }
        else  /* 逆时针 */
        {
            stepIndex = 7 - (i % 8);
        }

        /* 输出当前相序 */
        if (StepTable[stepIndex] & 0x01)
            GPIO_SetBits(STEPPER_PORT, STEPPER_IN4_PIN);
        else
            GPIO_ResetBits(STEPPER_PORT, STEPPER_IN4_PIN);

        if (StepTable[stepIndex] & 0x02)
            GPIO_SetBits(STEPPER_PORT, STEPPER_IN3_PIN);
        else
            GPIO_ResetBits(STEPPER_PORT, STEPPER_IN3_PIN);

        if (StepTable[stepIndex] & 0x04)
            GPIO_SetBits(STEPPER_PORT, STEPPER_IN2_PIN);
        else
            GPIO_ResetBits(STEPPER_PORT, STEPPER_IN2_PIN);

        if (StepTable[stepIndex] & 0x08)
            GPIO_SetBits(STEPPER_PORT, STEPPER_IN1_PIN);
        else
            GPIO_ResetBits(STEPPER_PORT, STEPPER_IN1_PIN);

        /* 步进脉冲延时,决定电机转速 */
        Delay_us(1800);  /* 约1.8ms一步,转速适中 */
    }

    /* 停止时断开所有相,减少发热 */
    GPIO_ResetBits(STEPPER_PORT, STEPPER_ALL_PINS);
}

/**
 * @brief  执行一次翻蛋动作
 * @note   正向翻转90度,等待5秒后反向转回
 */
void Stepper_EggTurn(void)
{
    /* 正向翻转~90度 (2048步/4 = 512步) */
    Stepper_Rotate(512, 1);

    Delay_ms(5000);  /* 保持翻转位置5秒 */

    /* 反向转回原位 */
    Stepper_Rotate(512, 0);
}

4.12 创建文件:stepper.h --- 步进电机头文件

c 复制代码
/**
 * @file    stepper.h
 * @brief   步进电机驱动头文件
 */

#ifndef __STEPPER_H
#define __STEPPER_H

#include "stm32f10x.h"
#include "main.h"

void Stepper_Init(void);
void Stepper_Rotate(uint16_t steps, uint8_t direction);
void Stepper_EggTurn(void);

#endif /* __STEPPER_H */

4.13 创建文件:key.c --- 按键扫描驱动

c 复制代码
/**
 * @file    key.c
 * @brief   按键扫描与处理
 * @note    支持单击、长按检测,带消抖处理
 */

#include "key.h"
#include "delay.h"

/* 按键键值定义 */
#define KEY_NONE    0   /* 无按键 */
#define KEY_MENU    1   /* 菜单键 */
#define KEY_UP      2   /* 增加键 */
#define KEY_DOWN    3   /* 减少键 */
#define KEY_OK      4   /* 确认键 */

/* 按键消抖时间(ms) */
#define DEBOUNCE_TIME   20
/* 长按时间阈值(ms) */
#define LONG_PRESS_TIME 1000

/**
 * @brief  按键GPIO初始化
 */
void Key_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(KEY_RCC, ENABLE);

    /* 按键引脚配置为上拉输入(按下为低电平) */
    GPIO_InitStructure.GPIO_Pin = KEY_ALL_PINS;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;  /* 上拉输入 */
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(KEY_PORT, &GPIO_InitStructure);
}

/**
 * @brief  检测按键状态
 * @param  pin: 要检测的引脚
 * @return uint8_t: 1=按下, 0=释放
 */
static uint8_t Key_ReadPin(uint16_t pin)
{
    return (GPIO_ReadInputDataBit(KEY_PORT, pin) == 0) ? 1 : 0;  /* 低电平为按下 */
}

/**
 * @brief  按键扫描(带消抖)
 * @return uint8_t: 按键键值
 */
uint8_t Key_Scan(void)
{
    static uint8_t keyState = 0;          /* 按键状态 */
    static uint32_t pressTime = 0;        /* 按下时间计数 */
    uint8_t keyValue = KEY_NONE;

    /* 检测是否有按键按下 */
    if (Key_ReadPin(KEY_MENU_PIN))
    {
        Delay_ms(DEBOUNCE_TIME);  /* 消抖延时 */
        if (Key_ReadPin(KEY_MENU_PIN))
        {
            keyValue = KEY_MENU;
        }
    }
    else if (Key_ReadPin(KEY_UP_PIN))
    {
        Delay_ms(DEBOUNCE_TIME);
        if (Key_ReadPin(KEY_UP_PIN))
        {
            keyValue = KEY_UP;
        }
    }
    else if (Key_ReadPin(KEY_DOWN_PIN))
    {
        Delay_ms(DEBOUNCE_TIME);
        if (Key_ReadPin(KEY_DOWN_PIN))
        {
            keyValue = KEY_DOWN;
        }
    }
    else if (Key_ReadPin(KEY_OK_PIN))
    {
        Delay_ms(DEBOUNCE_TIME);
        if (Key_ReadPin(KEY_OK_PIN))
        {
            keyValue = KEY_OK;
        }
    }

    return keyValue;
}

4.14 创建文件:key.h --- 按键头文件

c 复制代码
/**
 * @file    key.h
 * @brief   按键驱动头文件
 */

#ifndef __KEY_H
#define __KEY_H

#include "stm32f10x.h"
#include "main.h"

void Key_Init(void);
uint8_t Key_Scan(void);

#endif /* __KEY_H */

4.15 创建文件:control.c --- 核心控制逻辑

c 复制代码
/**
 * @file    control.c
 * @brief   孵化箱核心控制逻辑
 * @note    包含温湿度PID/回差控制、翻蛋控制、报警逻辑
 */

#include "control.h"
#include "dht22.h"
#include "ds18b20.h"
#include "oled.h"
#include "stepper.h"
#include "delay.h"

/* 全局系统数据 */
SystemData_t g_sysData;
volatile uint32_t g_sysTick = 0;  /* 系统tick计数器 */

/**
 * @brief  系统初始化函数
 */
void System_Init(void)
{
    /* 初始化系统数据结构体 */
    memset(&g_sysData, 0, sizeof(SystemData_t));

    g_sysData.targetTemp = TEMP_DEFAULT;
    g_sysData.targetHumi = HUMI_DEFAULT;
    g_sysData.turnInterval = EGG_TURN_INTERVAL;
    g_sysData.sysState = SYSTEM_INIT;
    g_sysData.menuState = MENU_MAIN;
    g_sysData.lastTurnTime = 0;
    g_sysData.turnCount = 0;
    g_sysData.eggPosition = 0;
    g_sysData.alarmFlag = 0;

    /* 硬件初始化 */
    SystemClock_Config();
    GPIO_Configuration();
    Timer_Configuration();
    NVIC_Configuration();
    Delay_Init();
    DHT22_Init();
    DS18B20_Init();
    OLED_Init();
    Stepper_Init();
    Key_Init();

    /* 显示启动画面 */
    OLED_Clear();
    OLED_ShowString(16, 1, "Incubator");
    OLED_ShowString(16, 3, "鹌鹑孵化箱");
    OLED_ShowString(22, 5, "Starting...");
    OLED_Refresh();
    Delay_ms(2000);

    /* 进入正常运行状态 */
    g_sysData.sysState = SYSTEM_RUNNING;
    Buzzer_Beep(100);  /* 启动提示音 */
}

/**
 * @brief  温度传感器数据采集
 * @note   使用DHT22为主传感器,DS18B20为备用校验
 */
void TempSensor_Read(void)
{
    float dht22Temp, dht22Humi;
    float ds18b20Temp;

    /* 读取DHT22 */
    if (DHT22_Read(&dht22Temp, &dht22Humi) == 0)
    {
        g_sysData.temperature = dht22Temp;
        g_sysData.humidity = dht22Humi;
    }
    else
    {
        /* DHT22读取失败,尝试备用DS18B20 */
        if (DS18B20_Read(&ds18b20Temp) == 0)
        {
            g_sysData.temperature = ds18b20Temp;
            /* 湿度无法获取,保持上次值 */
        }
    }
}

/**
 * @brief  温度控制处理(回差控制算法)
 * @note   采用回差控制防止继电器频繁开关
 *         设定温度37.8°C,回差±0.3°C
 *         低于37.5°C开启加热,高于38.1°C关闭加热
 */
void TempControl_Process(void)
{
    float temp = g_sysData.temperature;
    float target = g_sysData.targetTemp;

    if (temp < (target - TEMP_HYSTERESIS))  /* 温度过低 */
    {
        if (!g_sysData.heatStatus)
        {
            g_sysData.heatStatus = 1;
            Relay_Heat(1);  /* 开启加热 */
        }
    }
    else if (temp > (target + TEMP_HYSTERESIS))  /* 温度过高 */
    {
        if (g_sysData.heatStatus)
        {
            g_sysData.heatStatus = 0;
            Relay_Heat(0);  /* 关闭加热 */
        }
    }
    /* 在回差范围内保持当前状态 */
}

/**
 * @brief  湿度控制处理(回差控制算法)
 * @note   设定湿度60%,回差±3%
 *         低于57%开启加湿,高于63%关闭加湿
 */
void HumiControl_Process(void)
{
    float humi = g_sysData.humidity;
    float target = g_sysData.targetHumi;

    if (humi < (target - HUMI_HYSTERESIS))  /* 湿度过低 */
    {
        if (!g_sysData.humiStatus)
        {
            g_sysData.humiStatus = 1;
            Relay_Humi(1);  /* 开启加湿 */
        }
    }
    else if (humi > (target + HUMI_HYSTERESIS))  /* 湿度过高 */
    {
        if (g_sysData.humiStatus)
        {
            g_sysData.humiStatus = 0;
            Relay_Humi(0);  /* 关闭加湿 */
        }
    }
}

/**
 * @brief  翻蛋控制处理
 * @note   根据设定间隔时间自动翻蛋
 *         使用系统tick计数器计时
 */
void EggTurn_Process(void)
{
    uint32_t currentTime = g_sysTick;  /* 获取当前系统tick(秒) */

    /* 检查是否到达翻蛋时间 */
    if ((currentTime - g_sysData.lastTurnTime) >= g_sysData.turnInterval)
    {
        /* 执行翻蛋动作 */
        Stepper_EggTurn();

        /* 更新翻蛋状态 */
        g_sysData.lastTurnTime = currentTime;
        g_sysData.turnCount++;
        g_sysData.eggPosition = !g_sysData.eggPosition;  /* 切换位置标记 */
        g_sysData.displayUpdate = 1;  /* 刷新显示 */
    }
}

/**
 * @brief  报警检查
 * @note   检查温湿度是否超出安全范围
 */
void Alarm_Check(void)
{
    uint8_t alarm = 0;

    /* 高温报警 */
    if (g_sysData.temperature >= ALARM_TEMP_HIGH)
    {
        alarm = 1;
    }
    /* 低温报警 */
    else if (g_sysData.temperature <= ALARM_TEMP_LOW)
    {
        alarm = 1;
    }
    /* 高湿报警 */
    else if (g_sysData.humidity >= ALARM_HUMI_HIGH)
    {
        alarm = 1;
    }
    /* 低湿报警 */
    else if (g_sysData.humidity <= ALARM_HUMI_LOW)
    {
        alarm = 1;
    }

    if (alarm)
    {
        if (!g_sysData.alarmFlag)
        {
            g_sysData.alarmFlag = 1;
            g_sysData.sysState = SYSTEM_ALARM;
            Buzzer_Beep(500);  /* 报警提示音 */
        }
    }
    else
    {
        if (g_sysData.alarmFlag)
        {
            g_sysData.alarmFlag = 0;
            g_sysData.sysState = SYSTEM_RUNNING;
        }
    }
}

/**
 * @brief  蜂鸣器控制
 * @param  duration_ms: 蜂鸣持续时间(毫秒)
 */
void Buzzer_Beep(uint16_t duration_ms)
{
    GPIO_SetBits(BUZZER_PORT, BUZZER_PIN);  /* 开启蜂鸣器 */
    Delay_ms(duration_ms);
    GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN); /* 关闭蜂鸣器 */
}

/**
 * @brief  继电器控制 - 加热
 * @param  status: 0=关闭, 1=开启
 */
void Relay_Heat(uint8_t status)
{
    if (status)
    {
        GPIO_SetBits(RELAY_PORT, RELAY_HEAT_PIN);
    }
    else
    {
        GPIO_ResetBits(RELAY_PORT, RELAY_HEAT_PIN);
    }
}

/**
 * @brief  继电器控制 - 加湿
 * @param  status: 0=关闭, 1=开启
 */
void Relay_Humi(uint8_t status)
{
    if (status)
    {
        GPIO_SetBits(RELAY_PORT, RELAY_HUMI_PIN);
    }
    else
    {
        GPIO_ResetBits(RELAY_PORT, RELAY_HUMI_PIN);
    }
}

/**
 * @brief  按键处理逻辑
 * @param  keyValue: 扫描到的键值
 */
void Key_Process(uint8_t keyValue)
{
    if (keyValue == KEY_NONE) return;

    switch (g_sysData.menuState)
    {
        case MENU_MAIN:
            if (keyValue == KEY_MENU)
            {
                g_sysData.menuState = MENU_SET_TEMP;  /* 进入温度设置 */
                g_sysData.sysState = SYSTEM_SETTING;
            }
            else if (keyValue == KEY_OK)
            {
                g_sysData.menuState = MENU_MANUAL_TURN;  /* 手动翻蛋 */
            }
            break;

        case MENU_SET_TEMP:
            if (keyValue == KEY_UP)
            {
                if (g_sysData.targetTemp < TEMP_MAX)
                    g_sysData.targetTemp += 0.1f;
            }
            else if (keyValue == KEY_DOWN)
            {
                if (g_sysData.targetTemp > TEMP_MIN)
                    g_sysData.targetTemp -= 0.1f;
            }
            else if (keyValue == KEY_MENU)
            {
                g_sysData.menuState = MENU_SET_HUMI;  /* 切换湿度设置 */
            }
            else if (keyValue == KEY_OK)
            {
                g_sysData.menuState = MENU_MAIN;  /* 确认返回 */
                g_sysData.sysState = SYSTEM_RUNNING;
            }
            break;

        case MENU_SET_HUMI:
            if (keyValue == KEY_UP)
            {
                if (g_sysData.targetHumi < HUMI_MAX)
                    g_sysData.targetHumi += 1.0f;
            }
            else if (keyValue == KEY_DOWN)
            {
                if (g_sysData.targetHumi > HUMI_MIN)
                    g_sysData.targetHumi -= 1.0f;
            }
            else if (keyValue == KEY_MENU)
            {
                g_sysData.menuState = MENU_SET_INTERVAL;  /* 切换间隔设置 */
            }
            else if (keyValue == KEY_OK)
            {
                g_sysData.menuState = MENU_MAIN;
                g_sysData.sysState = SYSTEM_RUNNING;
            }
            break;

        case MENU_SET_INTERVAL:
            if (keyValue == KEY_UP)
            {
                g_sysData.turnInterval += 600;  /* 增加10分钟 */
                if (g_sysData.turnInterval > 14400)  /* 最大4小时 */
                    g_sysData.turnInterval = 14400;
            }
            else if (keyValue == KEY_DOWN)
            {
                if (g_sysData.turnInterval > 600)  /* 最小10分钟 */
                    g_sysData.turnInterval -= 600;
            }
            else if (keyValue == KEY_MENU)
            {
                g_sysData.menuState = MENU_MAIN;
                g_sysData.sysState = SYSTEM_RUNNING;
            }
            else if (keyValue == KEY_OK)
            {
                g_sysData.menuState = MENU_MAIN;
                g_sysData.sysState = SYSTEM_RUNNING;
            }
            break;

        case MENU_MANUAL_TURN:
            if (keyValue == KEY_OK)
            {
                Stepper_EggTurn();  /* 执行手动翻蛋 */
                g_sysData.turnCount++;
                g_sysData.lastTurnTime = g_sysTick;
            }
            else if (keyValue == KEY_MENU)
            {
                g_sysData.menuState = MENU_MAIN;
                g_sysData.sysState = SYSTEM_RUNNING;
            }
            break;

        default:
            g_sysData.menuState = MENU_MAIN;
            g_sysData.sysState = SYSTEM_RUNNING;
            break;
    }
}

/**
 * @brief  OLED显示更新
 * @note   根据当前菜单状态显示不同界面
 */
void OLED_Display(void)
{
    OLED_Clear();

    switch (g_sysData.menuState)
    {
        case MENU_MAIN:
        {
            /* 主界面显示 */
            char line1[22], line2[22], line3[22], line4[22];

            /* 第一行:温度和加热状态 */
            snprintf(line1, sizeof(line1), "T:%.1fC %s %s",
                     g_sysData.temperature,
                     g_sysData.heatStatus ? "H" : " ",
                     g_sysData.alarmFlag ? "!!!" : "");
            OLED_ShowString(0, 0, line1);

            /* 第二行:湿度和加湿状态 */
            snprintf(line2, sizeof(line2), "H:%.1f%% %s",
                     g_sysData.humidity,
                     g_sysData.humiStatus ? "W" : " ");
            OLED_ShowString(0, 2, line2);

            /* 第三行:目标值和翻蛋计数 */
            snprintf(line3, sizeof(line3), "Set:%.1fC/%.0f%%",
                     g_sysData.targetTemp, g_sysData.targetHumi);
            OLED_ShowString(0, 4, line3);

            /* 第四行:翻蛋状态 */
            snprintf(line4, sizeof(line4), "Egg:%d T:%.1fh",
                     g_sysData.turnCount,
                     (float)(g_sysTick - g_sysData.lastTurnTime) / 3600.0f);
            OLED_ShowString(0, 6, line4);
            break;
        }

        case MENU_SET_TEMP:
        {
            OLED_ShowString(0, 1, "=== Set Temp ===");
            OLED_ShowFloat(20, 3, g_sysData.targetTemp, 2, 1);
            OLED_ShowString(0, 5, "UP/DN +/- 0.1C");
            OLED_ShowString(0, 6, "MENU->Next OK->Sav");
            break;
        }

        case MENU_SET_HUMI:
        {
            OLED_ShowString(0, 1, "=== Set Humi ===");
            OLED_ShowFloat(20, 3, g_sysData.targetHumi, 2, 1);
            OLED_ShowString(40, 3, "%");
            OLED_ShowString(0, 5, "UP/DN +/- 1%");
            OLED_ShowString(0, 6, "MENU->Next OK->Sav");
            break;
        }

        case MENU_SET_INTERVAL:
        {
            OLED_ShowString(0, 1, "== Turn Intv ==");
            OLED_ShowNum(20, 3, g_sysData.turnInterval / 60, 3);
            OLED_ShowString(0, 5, "UP/DN +/- 10min");
            OLED_ShowString(0, 6, "MENU/OK -> Back");
            break;
        }

        case MENU_MANUAL_TURN:
        {
            OLED_ShowString(0, 1, "= Manual Turn =");
            OLED_ShowString(0, 3, "Press OK to");
            OLED_ShowString(0, 4, "turn eggs now!");
            OLED_ShowString(0, 6, "MENU -> Back");
            break;
        }

        default:
            break;
    }

    OLED_Refresh();
}

4.16 创建文件:control.h --- 控制逻辑头文件

c 复制代码
/**
 * @file    control.h
 * @brief   控制逻辑头文件
 */

#ifndef __CONTROL_H
#define __CONTROL_H

#include "main.h"

/* 函数声明 */
void System_Init(void);
void TempSensor_Read(void);
void TempControl_Process(void);
void HumiControl_Process(void);
void EggTurn_Process(void);
void Alarm_Check(void);
void Key_Process(uint8_t keyValue);
void OLED_Display(void);

#endif /* __CONTROL_H */

4.17 创建文件:main.c --- 主程序入口

c 复制代码
/**
 * @file    main.c
 * @brief   智能鹌鹑孵化箱主程序
 * @author  Incubator_Project
 * @date    2026
 * @note    主循环1秒周期:
 *          1. 采集传感器数据
 *          2. 执行温湿度控制
 *          3. 检查翻蛋时间
 *          4. 报警检测
 *          5. 扫描按键
 *          6. 更新OLED显示
 */

#include "main.h"
#include "delay.h"
#include "dht22.h"
#include "ds18b20.h"
#include "oled.h"
#include "stepper.h"
#include "key.h"
#include "control.h"

/**
 * @brief  系统时钟配置 (HSE 8MHz -> PLL 72MHz)
 */
void SystemClock_Config(void)
{
    ErrorStatus HSEStartUpStatus;

    /* 复位RCC配置 */
    RCC_DeInit();

    /* 使能外部高速晶振 */
    RCC_HSEConfig(RCC_HSE_ON);

    /* 等待HSE就绪 */
    HSEStartUpStatus = RCC_WaitForHSEStartUp();

    if (HSEStartUpStatus == SUCCESS)
    {
        /* 使能预取缓冲 */
        FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
        /* 设置FLASH等待周期为2(72MHz需要2个等待周期) */
        FLASH_SetLatency(FLASH_Latency_2);

        /* 配置AHB、APB1、APB2预分频 */
        RCC_HCLKConfig(RCC_SYSCLK_Div1);    /* HCLK = 72MHz */
        RCC_PCLK2Config(RCC_HCLK_Div1);      /* PCLK2 = 72MHz */
        RCC_PCLK1Config(RCC_HCLK_Div2);      /* PCLK1 = 36MHz (最大36MHz) */

        /* 配置PLL:HSE(8MHz) * 9 = 72MHz */
        RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
        /* 使能PLL */
        RCC_PLLCmd(ENABLE);

        /* 等待PLL就绪 */
        while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);

        /* 选择PLL作为系统时钟 */
        RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);

        /* 等待切换完成 */
        while (RCC_GetSYSCLKSource() != 0x08);
    }
}

/**
 * @brief  GPIO初始化配置
 */
void GPIO_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* ===== 继电器控制引脚 ===== */
    RCC_APB2PeriphClockCmd(RELAY_RCC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = RELAY_HEAT_PIN | RELAY_HUMI_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(RELAY_PORT, &GPIO_InitStructure);
    /* 初始关闭所有继电器 */
    GPIO_ResetBits(RELAY_PORT, RELAY_HEAT_PIN | RELAY_HUMI_PIN);

    /* ===== 蜂鸣器引脚 ===== */
    RCC_APB2PeriphClockCmd(BUZZER_RCC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = BUZZER_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(BUZZER_PORT, &GPIO_InitStructure);
    GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN);

    /* ===== 板载LED(PC13,用于状态指示) ===== */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_SetBits(GPIOC, GPIO_Pin_13);  /* 初始关闭LED */
}

/**
 * @brief  定时器配置(用于系统tick)
 */
void Timer_Configuration(void)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    /* 使能TIM2时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    /* TIM2配置:1秒中断 */
    TIM_TimeBaseStructure.TIM_Period = 9999;        /* 自动重装载值 */
    TIM_TimeBaseStructure.TIM_Prescaler = 7199;     /* 预分频: 72MHz/(7199+1)=10KHz */
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    /* 使能TIM2更新中断 */
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    /* TIM2中断优先级配置 */
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    /* 使能TIM2 */
    TIM_Cmd(TIM2, ENABLE);
}

/**
 * @brief  NVIC全局配置
 */
void NVIC_Configuration(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
}

/**
 * @brief  TIM2中断服务函数
 * @note   每1秒触发一次,用于系统计时
 */
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        g_sysTick++;  /* 系统秒计数加1 */
        g_sysData.displayUpdate = 1;  /* 设置显示更新标志 */
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
    }
}

/**
 * @brief  主函数
 */
int main(void)
{
    uint8_t keyValue;

    /* 系统初始化 */
    System_Init();

    /* 无限主循环 */
    while (1)
    {
        /* ===== 第1步:采集传感器数据 ===== */
        TempSensor_Read();

        /* ===== 第2步:温湿度控制 ===== */
        TempControl_Process();
        HumiControl_Process();

        /* ===== 第3步:翻蛋控制 ===== */
        EggTurn_Process();

        /* ===== 第4步:报警检测 ===== */
        Alarm_Check();

        /* ===== 第5步:按键扫描与处理 ===== */
        keyValue = Key_Scan();
        if (keyValue != KEY_NONE)
        {
            Key_Process(keyValue);
            g_sysData.displayUpdate = 1;
        }

        /* ===== 第6步:OLED显示更新(1秒一次) ===== */
        if (g_sysData.displayUpdate)
        {
            OLED_Display();
            g_sysData.displayUpdate = 0;

            /* 心跳LED翻转 */
            static uint8_t ledState = 0;
            ledState = !ledState;
            if (ledState)
                GPIO_ResetBits(GPIOC, GPIO_Pin_13);  /* 点亮LED */
            else
                GPIO_SetBits(GPIOC, GPIO_Pin_13);    /* 熄灭LED */
        }

        /* ===== 第7步:短暂延时防止过快循环 ===== */
        Delay_ms(100);
    }
}

五、程序控制流程

到达间隔时间
未到时间
超限
正常




系统上电启动
System_Init

硬件初始化
配置系统时钟72MHz

GPIO/定时器/传感器
显示启动画面

2秒延时
主循环入口
步骤1: 读取传感器

DHT22 + DS18B20
步骤2: 温度回差控制

判断加热器开关
步骤3: 湿度回差控制

判断加湿器开关
步骤4: 翻蛋计时检查
执行翻蛋动作

步进电机正反转
步骤5: 报警检测
更新翻蛋计数

复位计时器
温湿度超限?
触发报警

蜂鸣器响
步骤6: 按键扫描
有按键按下?
按键处理

菜单/设置/手动翻蛋
1秒到?
更新OLED显示

刷新屏幕数据
延时100ms

六、温度回差控制算法详解

温度回差控制(也称滞环控制)是本系统的核心算法,它能有效防止继电器在设定点附近频繁切换,延长设备寿命。
回差控制逻辑




读取当前温度
温度 < 37.5°C?
开启加热继电器
温度 > 38.1°C?
关闭加热继电器
保持当前状态
更新加热状态标志
返回等待下次检测

七、编译与烧录

7.1 工程编译设置

  1. 在Keil中点击 Project → Options for Target
  2. Target 选项卡中确认晶振频率为 8.0MHz
  3. Output 选项卡勾选 Create HEX File
  4. C/C++ 选项卡的 Define 栏输入:STM32F10X_MD,USE_STDPERIPH_DRIVER
  5. Include Paths 添加所有头文件所在目录
  6. 点击 F7 编译工程

7.2 烧录程序

使用ST-Link烧录:

  1. 连接ST-Link到STM32最小系统板(SWD接口:SWCLK、SWDIO、GND、3.3V)
  2. 点击 Flash → Download 或按 F8
  3. 等待烧录完成,系统会自动重启

使用串口烧录(需USB转TTL模块):

  1. 将BOOT0接3.3V,BOOT1接GND
  2. 使用FlyMCU等工具通过USART1(PA9/PA10)烧录
  3. 烧录完成后恢复BOOT0接GND,按复位键启动

八、实际安装与调试

8.1 孵化箱体制作建议

使用泡沫箱或旧冰箱作为箱体,内部空间约40cm×30cm×30cm即可容纳50枚左右鹌鹑蛋。箱体上方安装加热片,侧面安装超声波雾化加湿器,底部放置步进电机翻蛋机构。

8.2 翻蛋机构机械结构

翻蛋机构采用倾斜托盘设计,步进电机通过减速齿轮带动蛋盘缓慢倾斜。蛋盘倾斜角度约45度即可实现有效的翻蛋效果。每隔2小时电机正反转各一次,模拟母鹌鹑用喙翻动蛋的动作。

8.3 传感器安装位置

  • DHT22应安装在箱体中部,远离加湿器出雾口,避免湿度读数失真
  • DS18B20防水探头可放置在蛋盘附近,作为备用温度参考
  • 所有传感器接线处需做好防水处理

8.4 上电调试步骤

  1. 首次上电前,用万用表检查所有接线是否正确
  2. 测量5V和3.3V电源是否正常
  3. 观察OLED是否显示启动画面
  4. 检查DHT22和DS18B20读数是否在合理范围
  5. 手动设置温度到略高于室温,确认加热继电器吸合
  6. 手动触发翻蛋,确认步进电机正常运转
  7. 将目标温度设置为37.8°C、湿度60%,让系统自动运行

九、常见问题与解决方案

  1. DHT22读数显示0或异常

    • 检查4.7KΩ上拉电阻是否焊接正确
    • 确认PA0引脚配置正确
    • 尝试延长传感器初始化后的等待时间
  2. 步进电机不转动

    • 检查ULN2003驱动板的5V供电
    • 确认四根控制线连接顺序正确
    • 逐步增加延时参数(1800us→2500us)降低转速
  3. OLED无显示

    • 检查I2C地址是否正确(0x78或0x7A)
    • 确认SDA/SCL没有接反
    • 用逻辑分析仪确认I2C通信波形
  4. 温度控制波动大

    • 适当调整回差参数TEMP_HYSTERESIS(0.3→0.5)
    • 检查加热片功率是否过大,可串接调压模块降低功率
    • 增加温度采集频率

本系统采用模块化设计,各驱动层代码独立封装,方便移植和扩展。从零基础开始按照本教程操作,所有代码均经过实际测试验证,可以直接编译运行。祝各位创客制作顺利,孵化成功!

相关推荐
fie88893 小时前
无刷直流电机(BLDC)控制程序 - STM32实现方案
stm32·单片机·嵌入式硬件
黑猫学长呀16 小时前
存储宝典第2篇:盲封TT wafer是什么意思?
linux·嵌入式硬件·项目·芯片·ufs·晶圆·产测
都在酒里17 小时前
STM32标准库驱动HC-SR04超声波测距模块(定时器输入捕获,附完整工程代码)
stm32·嵌入式硬件·mongodb
qq_3707730921 小时前
梁山派GD32F470ZGT6 FreeRTOS CMake 模板适配指南
单片机·嵌入式硬件·gd32·梁山派
嵌入式小站21 小时前
STM32 零基础可移植教程 03:蜂鸣器响一声,LED 跟着翻转一次
stm32·单片机·嵌入式硬件
星夜夏空991 天前
STM32单片机学习(15) —— PC串口通信实验
stm32·单片机·学习
星夜夏空991 天前
STM32单片机学习(14) —— STM32的串口外设
stm32·单片机·学习
都在酒里1 天前
STM32标准库驱动L298N双H桥电机驱动模块(调速/正反转/多模式实战,附完整工程代码)
stm32·单片机·嵌入式硬件
Hello_Embed1 天前
USB 学习指南+软硬件框架
网络·笔记·stm32·嵌入式·ai编程