文章目录
-
- 一、前言
-
- [1.1 技术背景](#1.1 技术背景)
- [1.2 应用场景](#1.2 应用场景)
- [1.3 本文目标](#1.3 本文目标)
- 二、环境准备
-
- [2.1 硬件清单](#2.1 硬件清单)
- [2.2 硬件连接图](#2.2 硬件连接图)
- 三、核心实现
-
- [3.1 STM32CubeMX配置](#3.1 STM32CubeMX配置)
- [3.2 主程序实现](#3.2 主程序实现)
- [3.3 HX711称重驱动](#3.3 HX711称重驱动)
- [3.4 步进电机驱动](#3.4 步进电机驱动)
- 四、测试验证
-
- [4.1 硬件测试](#4.1 硬件测试)
- [4.2 功能测试](#4.2 功能测试)
- 五、故障排查
-
- [5.1 常见问题](#5.1 常见问题)
- 六、总结
-
- [6.1 核心知识点](#6.1 核心知识点)
- [6.2 扩展方向](#6.2 扩展方向)
- [6.3 学习资源](#6.3 学习资源)
一、前言
1.1 技术背景
现代都市生活节奏快,很多养宠物的上班族经常面临无法按时喂食的困扰。智能宠物喂食器应运而生,它能够按照预设的时间和份量自动投放食物,让宠物主人即使不在家也能确保爱宠按时进食。
STM32F103系列微控制器凭借其丰富的外设接口、低功耗特性和高性价比,成为物联网智能硬件开发的理想选择。本项目将展示如何使用STM32F103实现一个功能完善的智能宠物喂食系统。
1.2 应用场景
- 上班族家庭:工作日自动定时喂食,周末手动控制
- 短期出差:设置多日喂食计划,远程监控进食情况
- 多宠物家庭:精确控制每只宠物的食量
- 宠物店/寄养中心:批量管理多个喂食器
1.3 本文目标
通过本教程,你将学会:
- STM32F103的RTC实时时钟配置与使用
- 步进电机驱动与控制算法
- 重量传感器的校准与数据采集
- 红外传感器的物体检测
- OLED显示屏的图形界面开发
- 按键与旋转编码器的人机交互设计
- 低功耗待机模式实现
技术栈:
- 硬件平台:STM32F103C8T6
- 电机驱动:28BYJ-48步进电机 + ULN2003驱动板
- 称重模块:HX711 + 5kg压力传感器
- 显示模块:0.96寸I2C OLED (SSD1306)
- 通信模块:ESP8266 WiFi模块(可选)
- 输入设备:旋转编码器 + 按键
- 传感器:红外对射传感器(检测食物余量)
- 开发工具:STM32CubeIDE
二、环境准备
2.1 硬件清单
| 器件名称 | 型号/规格 | 数量 | 说明 |
|---|---|---|---|
| 主控芯片 | STM32F103C8T6 | 1 | Blue Pill开发板 |
| 步进电机 | 28BYJ-48 | 1 | 5V减速步进电机 |
| 电机驱动 | ULN2003 | 1 | 步进电机驱动板 |
| 称重传感器 | HX711模块 | 1 | 24位AD转换 |
| 压力传感器 | 5kg称重传感器 | 1 | 检测食物重量 |
| OLED显示屏 | 0.96寸 I2C | 1 | SSD1306驱动 |
| 旋转编码器 | EC11 | 1 | 带按键功能 |
| 红外传感器 | 对射式 | 1 | 检测食物余量 |
| WiFi模块 | ESP8266-01S | 1 | 可选,远程控制 |
| 电源 | 5V/2A | 1 | 系统供电 |
2.2 硬件连接图
PA0-PA3
PB12/PB13
PB6/PB7
PA8/PA9/PA10
PA11
PA2/PA3
STM32F103C8T6
28BYJ-48步进电机
HX711称重模块
OLED显示屏
旋转编码器
红外传感器
ESP8266 WiFi
引脚连接表:
| STM32引脚 | 连接设备 | 功能说明 |
|---|---|---|
| PA0-PA3 | ULN2003 | 步进电机驱动 |
| PB12 | HX711 DT | 称重数据 |
| PB13 | HX711 SCK | 称重时钟 |
| PB6 | OLED SCL | I2C时钟 |
| PB7 | OLED SDA | I2C数据 |
| PA8 | 编码器A相 | 旋转检测 |
| PA9 | 编码器B相 | 方向检测 |
| PA10 | 编码器按键 | 确认/菜单 |
| PA11 | 红外传感器 | 食物检测 |
| PA2/PA3 | ESP8266 | WiFi通信 |
三、核心实现
3.1 STM32CubeMX配置
时钟配置:
- HSE Crystal/Ceramic Resonator
- System Clock: 72MHz
- RTC时钟源:LSE Crystal (32.768kHz)
外设配置:
- RTC:使能日历功能
- TIM2:1ms定时器,用于步进电机控制
- TIM3:编码器模式,用于旋转编码器
- I2C1:PB6(SCL), PB7(SDA),OLED通信
- USART2:PA2(TX), PA3(RX),WiFi通信
- GPIO :
- PA0-PA3:输出,步进电机
- PB12:输入,HX711 DT
- PB13:输出,HX711 SCK
- PA11:输入,红外传感器
- PA10:输入,编码器按键(外部中断)
3.2 主程序实现
📄 创建文件:
Core/Src/main.c
c
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Smart Pet Feeder System
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "rtc.h"
#include "tim.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"
/* USER CODE BEGIN Includes */
#include "oled_driver.h"
#include "hx711_driver.h"
#include "stepper_motor.h"
#include "encoder.h"
#include "menu_system.h"
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */
/* Private defines -----------------------------------------------------------*/
#define FEED_INTERVAL_MS 60000 // 喂食间隔检查(1分钟)
#define MOTOR_SPEED_DELAY 2 // 电机步进延时(ms)
#define FOOD_THRESHOLD 50 // 食物不足阈值(克)
/* Private variables ---------------------------------------------------------*/
// 喂食计划结构体
typedef struct {
uint8_t hour;
uint8_t minute;
uint16_t amount; // 喂食量(克)
uint8_t enabled; // 是否启用
} FeedSchedule;
// 系统状态
volatile FeedSchedule schedule[4]; // 最多4个喂食时段
volatile uint8_t schedule_count = 0;
volatile uint8_t feeding_in_progress = 0;
volatile float current_weight = 0.0;
volatile uint8_t food_low_warning = 0;
// 显示缓冲区
char display_buffer[32];
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void Check_Feeding_Schedule(void);
static void Process_Feeding(uint16_t target_amount);
static void Update_Display(void);
static void Enter_LowPower_Mode(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/**
* @brief 主函数入口
*/
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_RTC_Init();
MX_TIM2_Init();
MX_TIM3_Init();
MX_I2C1_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
printf("\\r\\n===== Smart Pet Feeder =====\\r\\n");
// 初始化OLED
OLED_Init();
OLED_Clear();
OLED_ShowString(0, 0, "Pet Feeder", 16);
OLED_ShowString(0, 2, "Initializing...", 8);
HAL_Delay(1000);
// 初始化HX711
HX711_Init();
HX711_Calibrate(); // 校准零点
// 初始化步进电机
Stepper_Init();
// 初始化编码器
Encoder_Init();
// 初始化菜单系统
Menu_Init();
// 加载保存的喂食计划(从Flash或EEPROM)
Load_Schedule();
printf("System ready\\r\\n");
OLED_Clear();
/* USER CODE END 2 */
/* Infinite loop */
while (1)
{
/* USER CODE BEGIN 3 */
// 读取当前重量
current_weight = HX711_GetWeight();
// 检测食物余量
food_low_warning = HAL_GPIO_ReadPin(IR_SENSOR_GPIO_Port, IR_SENSOR_Pin);
// 检查喂食计划
if (!feeding_in_progress)
{
Check_Feeding_Schedule();
}
// 处理用户输入(编码器+按键)
Menu_ProcessInput();
// 更新显示
Update_Display();
// 低功耗处理
if (!feeding_in_progress && !Menu_IsActive())
{
Enter_LowPower_Mode();
}
HAL_Delay(100);
}
/* USER CODE END 3 */
}
/**
* @brief 检查喂食计划
*/
static void Check_Feeding_Schedule(void)
{
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
static uint8_t last_minute = 255;
// 获取当前时间
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
// 避免同一分钟重复喂食
if (sTime.Minutes == last_minute)
{
return;
}
last_minute = sTime.Minutes;
// 检查所有喂食计划
for (uint8_t i = 0; i < schedule_count; i++)
{
if (schedule[i].enabled &&
schedule[i].hour == sTime.Hours &&
schedule[i].minute == sTime.Minutes)
{
printf("Starting scheduled feeding #%d\\r\\n", i + 1);
Process_Feeding(schedule[i].amount);
break;
}
}
}
/**
* @brief 执行喂食过程
* @param target_amount: 目标喂食量(克)
*/
static void Process_Feeding(uint16_t target_amount)
{
feeding_in_progress = 1;
float start_weight = HX711_GetWeight();
float target_total = start_weight - target_amount;
printf("Feeding started: target %d grams\\r\\n", target_amount);
// 显示喂食状态
OLED_Clear();
OLED_ShowString(0, 0, "Feeding...", 16);
sprintf(display_buffer, "Target: %dg", target_amount);
OLED_ShowString(0, 2, display_buffer, 8);
// 启动步进电机
Stepper_SetDirection(CW); // 顺时针旋转
while (feeding_in_progress)
{
current_weight = HX711_GetWeight();
float dispensed = start_weight - current_weight;
// 更新显示
sprintf(display_buffer, "Dispensed: %dg", (int)dispensed);
OLED_ShowString(0, 4, display_buffer, 8);
// 检查是否达到目标量
if (current_weight <= target_total || dispensed >= target_amount)
{
break;
}
// 检查食物是否充足
if (food_low_warning)
{
printf("Warning: Food running low!\\r\\n");
OLED_ShowString(0, 6, "Low Food!", 8);
}
// 步进电机转动一步
Stepper_Step();
HAL_Delay(MOTOR_SPEED_DELAY);
}
// 停止电机
Stepper_Stop();
// 显示完成
OLED_Clear();
OLED_ShowString(0, 0, "Feeding Done!", 16);
sprintf(display_buffer, "Total: %dg", (int)(start_weight - current_weight));
OLED_ShowString(0, 2, display_buffer, 8);
HAL_Delay(3000);
feeding_in_progress = 0;
printf("Feeding completed\\r\\n");
}
/**
* @brief 更新OLED显示
*/
static void Update_Display(void)
{
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
// 如果菜单激活,由菜单系统控制显示
if (Menu_IsActive())
{
return;
}
// 获取当前时间
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
// 第一行:时间
sprintf(display_buffer, "%02d:%02d:%02d", sTime.Hours, sTime.Minutes, sTime.Seconds);
OLED_ShowString(0, 0, display_buffer, 16);
// 第二行:食物重量
sprintf(display_buffer, "Food: %.1fg", current_weight);
OLED_ShowString(0, 2, display_buffer, 8);
// 第三行:下次喂食时间
uint8_t next_feed = 255;
for (uint8_t i = 0; i < schedule_count; i++)
{
if (schedule[i].enabled)
{
if (schedule[i].hour > sTime.Hours ||
(schedule[i].hour == sTime.Hours && schedule[i].minute > sTime.Minutes))
{
next_feed = i;
break;
}
}
}
if (next_feed != 255)
{
sprintf(display_buffer, "Next: %02d:%02d", schedule[next_feed].hour, schedule[next_feed].minute);
}
else
{
sprintf(display_buffer, "Next: --:--");
}
OLED_ShowString(0, 4, display_buffer, 8);
// 第四行:状态指示
if (food_low_warning)
{
OLED_ShowString(0, 6, "LOW FOOD!", 8);
}
else
{
OLED_ShowString(0, 6, "Status: OK", 8);
}
}
/**
* @brief 进入低功耗模式
*/
static void Enter_LowPower_Mode(void)
{
// 关闭OLED显示以节省电量
OLED_DisplayOff();
// 进入睡眠模式,等待中断唤醒
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
// 唤醒后恢复显示
OLED_DisplayOn();
}
/* USER CODE BEGIN 4 */
/**
* @brief 编码器按键中断回调
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == ENCODER_BUTTON_Pin)
{
// 消抖
HAL_Delay(20);
if (HAL_GPIO_ReadPin(ENCODER_BUTTON_GPIO_Port, ENCODER_BUTTON_Pin) == GPIO_PIN_RESET)
{
Menu_ButtonPressed();
}
}
}
/* USER CODE END 4 */
/**
* @brief System Clock Configuration
*/
void SystemClock_Config(void)
{
// 系统时钟配置代码(由CubeMX生成)
// ...
}
void Error_Handler(void)
{
__disable_irq();
while (1)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
HAL_Delay(100);
}
}
3.3 HX711称重驱动
📄 创建文件:
Core/Inc/hx711_driver.h
c
#ifndef __HX711_DRIVER_H
#define __HX711_DRIVER_H
#include "main.h"
// HX711引脚定义
#define HX711_SCK_GPIO_Port GPIOB
#define HX711_SCK_Pin GPIO_PIN_13
#define HX711_DT_GPIO_Port GPIOB
#define HX711_DT_Pin GPIO_PIN_12
// 函数声明
void HX711_Init(void);
int32_t HX711_Read(void);
float HX711_GetWeight(void);
void HX711_Calibrate(void);
void HX711_SetScale(float scale);
void HX711_Tare(void);
#endif /* __HX711_DRIVER_H */
📄 创建文件:
Core/Src/hx711_driver.c
c
#include "hx711_driver.h"
#include "gpio.h"
// 校准参数
static float calibration_scale = 0.0035; // 默认校准系数
static int32_t offset = 0;
/**
* @brief 初始化HX711
*/
void HX711_Init(void)
{
// SCK初始化为输出低电平
HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET);
}
/**
* @brief 读取HX711原始数据
* @retval 24位ADC值
*/
int32_t HX711_Read(void)
{
int32_t data = 0;
// 等待DT变低(数据准备好)
while (HAL_GPIO_ReadPin(HX711_DT_GPIO_Port, HX711_DT_Pin) == GPIO_PIN_SET);
// 读取24位数据
for (int8_t i = 0; i < 24; i++)
{
HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET);
data = data << 1;
HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET);
if (HAL_GPIO_ReadPin(HX711_DT_GPIO_Port, HX711_DT_Pin) == GPIO_PIN_SET)
{
data++;
}
}
// 第25个脉冲设置增益(128)
HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET);
// 转换为有符号数
if (data & 0x800000)
{
data |= 0xFF000000;
}
return data;
}
/**
* @brief 获取重量值(克)
* @retval 重量值
*/
float HX711_GetWeight(void)
{
int32_t raw = HX711_Read();
return (float)(raw - offset) * calibration_scale;
}
/**
* @brief 校准零点(去皮)
*/
void HX711_Calibrate(void)
{
int32_t sum = 0;
// 取10次平均值作为零点
for (uint8_t i = 0; i < 10; i++)
{
sum += HX711_Read();
HAL_Delay(10);
}
offset = sum / 10;
}
/**
* @brief 设置校准系数
* @param scale: 校准系数(克/ADC值)
*/
void HX711_SetScale(float scale)
{
calibration_scale = scale;
}
/**
* @brief 去皮(当前重量设为0)
*/
void HX711_Tare(void)
{
HX711_Calibrate();
}
3.4 步进电机驱动
📄 创建文件:
Core/Inc/stepper_motor.h
c
#ifndef __STEPPER_MOTOR_H
#define __STEPPER_MOTOR_H
#include "main.h"
// 旋转方向
#define CW 0 // 顺时针
#define CCW 1 // 逆时针
// 步进电机引脚
#define MOTOR_IN1_GPIO_Port GPIOA
#define MOTOR_IN1_Pin GPIO_PIN_0
#define MOTOR_IN2_GPIO_Port GPIOA
#define MOTOR_IN2_Pin GPIO_PIN_1
#define MOTOR_IN3_GPIO_Port GPIOA
#define MOTOR_IN3_Pin GPIO_PIN_2
#define MOTOR_IN4_GPIO_Port GPIOA
#define MOTOR_IN4_Pin GPIO_PIN_3
// 函数声明
void Stepper_Init(void);
void Stepper_SetDirection(uint8_t dir);
void Stepper_Step(void);
void Stepper_Stop(void);
void Stepper_RotateSteps(uint16_t steps, uint8_t dir);
#endif /* __STEPPER_MOTOR_H */
📄 创建文件:
Core/Src/stepper_motor.c
c
#include "stepper_motor.h"
// 28BYJ-48步进序列(4相8拍)
static const uint8_t step_sequence[8][4] = {
{1, 0, 0, 0},
{1, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 1, 0},
{0, 0, 1, 0},
{0, 0, 1, 1},
{0, 0, 0, 1},
{1, 0, 0, 1}
};
static uint8_t current_step = 0;
static uint8_t direction = CW;
/**
* @brief 初始化步进电机
*/
void Stepper_Init(void)
{
// 所有引脚初始化为低电平
HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN3_GPIO_Port, MOTOR_IN3_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN4_GPIO_Port, MOTOR_IN4_Pin, GPIO_PIN_RESET);
}
/**
* @brief 设置旋转方向
* @param dir: CW(顺时针)或CCW(逆时针)
*/
void Stepper_SetDirection(uint8_t dir)
{
direction = dir;
}
/**
* @brief 执行一步
*/
void Stepper_Step(void)
{
// 根据方向更新步数
if (direction == CW)
{
current_step++;
if (current_step >= 8) current_step = 0;
}
else
{
if (current_step == 0) current_step = 7;
else current_step--;
}
// 设置引脚状态
HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin,
step_sequence[current_step][0] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin,
step_sequence[current_step][1] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN3_GPIO_Port, MOTOR_IN3_Pin,
step_sequence[current_step][2] ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN4_GPIO_Port, MOTOR_IN4_Pin,
step_sequence[current_step][3] ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
/**
* @brief 停止电机(所有线圈断电)
*/
void Stepper_Stop(void)
{
HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN3_GPIO_Port, MOTOR_IN3_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(MOTOR_IN4_GPIO_Port, MOTOR_IN4_Pin, GPIO_PIN_RESET);
}
/**
* @brief 旋转指定步数
* @param steps: 步数
* @param dir: 方向
*/
void Stepper_RotateSteps(uint16_t steps, uint8_t dir)
{
Stepper_SetDirection(dir);
for (uint16_t i = 0; i < steps; i++)
{
Stepper_Step();
HAL_Delay(2); // 2ms每步
}
Stepper_Stop();
}
四、测试验证
4.1 硬件测试
测试1:称重传感器校准
c
void Test_Weight_Sensor(void)
{
printf("Weight sensor test\\r\\n");
// 零点校准
HX711_Calibrate();
printf("Zero calibrated\\r\\n");
// 放置已知重量(如100g砝码)
HAL_Delay(2000);
// 读取并计算校准系数
float weight = HX711_GetWeight();
printf("Raw reading: %.2f\\r\\n", weight);
// 校准系数 = 实际重量 / 读数
float scale = 100.0 / weight;
printf("Calibration scale: %.6f\\r\\n", scale);
}
测试2:步进电机测试
c
void Test_Stepper_Motor(void)
{
printf("Stepper motor test\\r\\n");
// 顺时针旋转一圈(2048步)
printf("Rotating CW...\\r\\n");
Stepper_RotateSteps(2048, CW);
HAL_Delay(1000);
// 逆时针旋转一圈
printf("Rotating CCW...\\r\\n");
Stepper_RotateSteps(2048, CCW);
printf("Motor test complete\\r\\n");
}
4.2 功能测试
测试3:喂食功能测试
- 设置喂食量为50克
- 验证实际出食量是否准确
- 测试多次喂食的一致性
五、故障排查
5.1 常见问题
问题1:称重数据不稳定
原因分析:
- 电源噪声干扰
- 机械振动
- 传感器安装不牢固
解决方案:
- 使用独立稳压电源给HX711供电
- 添加软件滤波算法
- 加固传感器安装
c
// 软件滤波:滑动平均
float GetFilteredWeight(void)
{
static float buffer[10];
static uint8_t index = 0;
buffer[index] = HX711_GetWeight();
index = (index + 1) % 10;
float sum = 0;
for (uint8_t i = 0; i < 10; i++)
{
sum += buffer[i];
}
return sum / 10.0;
}
问题2:电机堵转或异响
原因分析:
- 食物卡住
- 电机驱动电流不足
- 步进序列错误
解决方案:
- 添加堵转检测功能
- 使用更大电流的驱动板
- 检查步进序列配置
六、总结
6.1 核心知识点
- RTC实时时钟:实现精准的定时功能
- 步进电机控制:精确的定量投放控制
- 称重传感器:高精度的重量检测
- OLED图形界面:友好的用户交互
- 低功耗设计:延长电池续航时间
6.2 扩展方向
- 摄像头监控:添加摄像头记录宠物进食情况
- 语音播报:添加语音模块播放录音吸引宠物
- 手机APP:开发配套APP远程控制
- 多宠物识别:使用RFID识别不同宠物