文章目录
-
- 一、项目概述与系统设计
- 二、硬件清单与接线说明
-
- [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— 主程序入口)
- [4.1 创建文件:`main.h` --- 主头文件](#4.1 创建文件:
- 五、程序控制流程
- 六、温度回差控制算法详解
- 七、编译与烧录
-
- [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 创建工程步骤
- 打开Keil,点击
Project → New μVision Project - 选择保存路径,输入工程名称
Incubator_Controller - 在芯片选择界面,选择
STMicroelectronics → STM32F103C8 - 在弹出的运行时环境管理界面,勾选以下组件:
- 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 工程编译设置
- 在Keil中点击
Project → Options for Target - 在
Target选项卡中确认晶振频率为8.0MHz - 在
Output选项卡勾选Create HEX File - 在
C/C++选项卡的Define栏输入:STM32F10X_MD,USE_STDPERIPH_DRIVER - 在
Include Paths添加所有头文件所在目录 - 点击
F7编译工程
7.2 烧录程序
使用ST-Link烧录:
- 连接ST-Link到STM32最小系统板(SWD接口:SWCLK、SWDIO、GND、3.3V)
- 点击
Flash → Download或按F8 - 等待烧录完成,系统会自动重启
使用串口烧录(需USB转TTL模块):
- 将BOOT0接3.3V,BOOT1接GND
- 使用FlyMCU等工具通过USART1(PA9/PA10)烧录
- 烧录完成后恢复BOOT0接GND,按复位键启动
八、实际安装与调试
8.1 孵化箱体制作建议
使用泡沫箱或旧冰箱作为箱体,内部空间约40cm×30cm×30cm即可容纳50枚左右鹌鹑蛋。箱体上方安装加热片,侧面安装超声波雾化加湿器,底部放置步进电机翻蛋机构。
8.2 翻蛋机构机械结构
翻蛋机构采用倾斜托盘设计,步进电机通过减速齿轮带动蛋盘缓慢倾斜。蛋盘倾斜角度约45度即可实现有效的翻蛋效果。每隔2小时电机正反转各一次,模拟母鹌鹑用喙翻动蛋的动作。
8.3 传感器安装位置
- DHT22应安装在箱体中部,远离加湿器出雾口,避免湿度读数失真
- DS18B20防水探头可放置在蛋盘附近,作为备用温度参考
- 所有传感器接线处需做好防水处理
8.4 上电调试步骤
- 首次上电前,用万用表检查所有接线是否正确
- 测量5V和3.3V电源是否正常
- 观察OLED是否显示启动画面
- 检查DHT22和DS18B20读数是否在合理范围
- 手动设置温度到略高于室温,确认加热继电器吸合
- 手动触发翻蛋,确认步进电机正常运转
- 将目标温度设置为37.8°C、湿度60%,让系统自动运行
九、常见问题与解决方案
-
DHT22读数显示0或异常
- 检查4.7KΩ上拉电阻是否焊接正确
- 确认PA0引脚配置正确
- 尝试延长传感器初始化后的等待时间
-
步进电机不转动
- 检查ULN2003驱动板的5V供电
- 确认四根控制线连接顺序正确
- 逐步增加延时参数(1800us→2500us)降低转速
-
OLED无显示
- 检查I2C地址是否正确(0x78或0x7A)
- 确认SDA/SCL没有接反
- 用逻辑分析仪确认I2C通信波形
-
温度控制波动大
- 适当调整回差参数TEMP_HYSTERESIS(0.3→0.5)
- 检查加热片功率是否过大,可串接调压模块降低功率
- 增加温度采集频率
本系统采用模块化设计,各驱动层代码独立封装,方便移植和扩展。从零基础开始按照本教程操作,所有代码均经过实际测试验证,可以直接编译运行。祝各位创客制作顺利,孵化成功!