feat: 优化界面设计与传感器功能

主要更改包括:

1. 传感器功能改进:
   - 实现水位传感器状态变化检测,正确处理有水/无水状态
   - 优化温湿度数据显示,使用更清晰的格式

2. LCD界面优化:
   - 适配80*160屏幕尺寸,调整所有页面布局
   - 温湿度页面:使用"C"作为温度单位,优化显示位置
   - 食物重量页面:调整显示位置,简化状态文本
   - 水位页面:替换圆形图标为矩形背景,提升视觉效果
   - 系统状态页面:简化状态文本,优化布局
   - 统一标题栏设计,添加分隔线

3. 功能增强:
   - 实现ST7735_DrawLine函数,用于绘制分隔线
   - 实现ST7735_FillCircle函数,用于图形绘制
   - 添加步进电机驱动代码,支持电机控制

4. 其他优化:
   - 修复未使用变量的编译警告
   - 调整字体大小和显示位置,提高可读性
   - 优化颜色方案,提升视觉体验

这些更改使界面更加美观、清晰,同时增强了系统的功能和稳定性。
This commit is contained in:
2026-02-24 23:41:40 +08:00
parent addfb3faad
commit 97d6d10731
10 changed files with 823 additions and 43 deletions

View File

@@ -46,6 +46,7 @@ add_executable(${CMAKE_PROJECT_NAME}
Core/Bsp/MultiButton-master/Multi_Button.c
Core/Bsp/BSP_Device/bsp_aht30/bsp_aht30.c
Core/Bsp/BSP_Device/bsp_rtc/bsp_rtc.c
Core/Bsp/bsp_motor/stepper_motor.c
)
# Add STM32CubeMX generated sources
add_subdirectory(cmake/stm32cubemx)
@@ -74,6 +75,7 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
Core/Bsp/MultiButton-master
Core/Bsp/BSP_Device/bsp_aht30
Core/Bsp/BSP_Device/bsp_rtc
Core/Bsp/bsp_motor
)
# Add project symbols (macros)

View File

@@ -59,6 +59,9 @@ HAL_StatusTypeDef MP3_Init(void)
HAL_StatusTypeDef ret1 = MP3_SendCommand(init_cmd1, sizeof(init_cmd1));
elog_d("MP3", "开启声音命令返回值: %d", ret1);
// 设置为低音量-测试用
MP3_SetVolume(2);
// 延时等待模块响应
HAL_Delay(100);

View File

@@ -554,6 +554,108 @@ void ST7735_InvertColors(bool invert) {
ST7735_Unselect();
}
/**
* 在ST7735液晶显示屏上绘制一条直线
*
* @param x0 直线起始点的X坐标
* @param y0 直线起始点的Y坐标
* @param x1 直线结束点的X坐标
* @param y1 直线结束点的Y坐标
* @param color 直线的颜色
*/
void ST7735_DrawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) {
// 检查坐标是否超出显示屏边界
if ((x0 >= ST7735_WIDTH) || (y0 >= ST7735_HEIGHT) || (x1 >= ST7735_WIDTH) || (y1 >= ST7735_HEIGHT))
return;
// 选择显示屏以进行操作
ST7735_Select();
// 使用Bresenham算法绘制直线
int16_t dx = abs((int16_t)x1 - (int16_t)x0);
int16_t dy = abs((int16_t)y1 - (int16_t)y0);
int16_t sx = (x0 < x1) ? 1 : -1;
int16_t sy = (y0 < y1) ? 1 : -1;
int16_t err = dx - dy;
int16_t current_x = x0;
int16_t current_y = y0;
while (1) {
// 绘制当前点
ST7735_DrawPixel(current_x, current_y, color);
// 检查是否到达终点
if (current_x == x1 && current_y == y1)
break;
// 计算下一步
int16_t e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
current_x += sx;
}
if (e2 < dx) {
err += dx;
current_y += sy;
}
}
// 取消选择显示屏,结束操作
ST7735_Unselect();
}
/**
* 在ST7735液晶显示屏上绘制一个填充圆
*
* @param x 圆心的X坐标
* @param y 圆心的Y坐标
* @param radius 圆的半径
* @param color 填充颜色
*/
void ST7735_FillCircle(uint16_t x, uint16_t y, uint16_t radius, uint16_t color) {
// 检查坐标是否超出显示屏边界
if ((x >= ST7735_WIDTH) || (y >= ST7735_HEIGHT))
return;
// 选择显示屏以进行操作
ST7735_Select();
// 使用中点圆算法绘制填充圆
int16_t x_pos = radius;
int16_t y_pos = 0;
int16_t err = 1 - radius;
while (x_pos >= y_pos) {
// 绘制水平扫描线
if ((y + y_pos) < ST7735_HEIGHT) {
ST7735_FillRectangle(x - x_pos, y + y_pos, 2 * x_pos + 1, 1, color);
}
if ((y - y_pos) >= 0 && y_pos != 0) {
ST7735_FillRectangle(x - x_pos, y - y_pos, 2 * x_pos + 1, 1, color);
}
if ((y + x_pos) < ST7735_HEIGHT && x_pos != y_pos) {
ST7735_FillRectangle(x - y_pos, y + x_pos, 2 * y_pos + 1, 1, color);
}
if ((y - x_pos) >= 0 && x_pos != y_pos) {
ST7735_FillRectangle(x - y_pos, y - x_pos, 2 * y_pos + 1, 1, color);
}
// 计算下一步
if (err < 0) {
y_pos++;
err += 2 * y_pos + 1;
} else {
x_pos--;
err += 2 * (y_pos - x_pos) + 1;
y_pos++;
}
}
// 取消选择显示屏,结束操作
ST7735_Unselect();
}
/**
* @brief 设置ST7735显示器的伽马曲线
*

View File

@@ -315,6 +315,16 @@ extern "C"
// 解释:此函数用于在显示屏上的指定位置绘制图像,图像数据以数组形式提供
void ST7735_DrawImage(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint16_t *data);
// 在指定位置绘制一条直线
// 参数x0 - 直线起始X坐标y0 - 直线起始Y坐标x1 - 直线结束X坐标y1 - 直线结束Y坐标color - 直线颜色
// 解释此函数用于在显示屏上绘制一条直线使用Bresenham算法实现
void ST7735_DrawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color);
// 绘制一个填充圆
// 参数x - 圆心X坐标y - 圆心Y坐标radius - 圆的半径color - 填充颜色
// 解释:此函数用于在显示屏上绘制一个填充的圆形
void ST7735_FillCircle(uint16_t x, uint16_t y, uint16_t radius, uint16_t color);
// 反转显示屏颜色
// 参数invert - 是否反转颜色true/false
// 解释:此函数用于反转显示屏上的颜色,可用于显示反转效果

View File

@@ -0,0 +1,237 @@
#include "stepper_motor.h"
#include "cmsis_os.h"
/* USER CODE BEGIN 1 */
/**
* @brief 28BYJ-48步进电机驱动源文件
*
* 28BYJ-48是5线4相永磁步进电机采用ULN2003驱动器
* 控制序列如下:
*
* 全步模式4拍
* IN1 IN2 IN3 IN4
* 1 0 0 0 -> Step 1
* 0 1 0 0 -> Step 2
* 0 0 1 0 -> Step 3
* 0 0 0 1 -> Step 4
*
* 半步模式8拍
* IN1 IN2 IN3 IN4
* 1 0 0 0 -> Step 1
* 1 1 0 0 -> Step 2
* 0 1 0 0 -> Step 3
* 0 1 1 0 -> Step 4
* 0 0 1 0 -> Step 5
* 0 0 1 1 -> Step 6
* 0 0 0 1 -> Step 7
* 1 0 0 1 -> Step 8
*/
// 步进电机控制序列数组 - 全步模式
static const uint8_t full_step_sequence[4][4] = {
{1, 0, 0, 0}, // Step 1
{0, 1, 0, 0}, // Step 2
{0, 0, 1, 0}, // Step 3
{0, 0, 0, 1} // Step 4
};
// 步进电机控制序列数组 - 半步模式
static const uint8_t half_step_sequence[8][4] = {
{1, 0, 0, 0}, // Step 1
{1, 1, 0, 0}, // Step 2
{0, 1, 0, 0}, // Step 3
{0, 1, 1, 0}, // Step 4
{0, 0, 1, 0}, // Step 5
{0, 0, 1, 1}, // Step 6
{0, 0, 0, 1}, // Step 7
{1, 0, 0, 1} // Step 8
};
// 当前步序位置
static uint8_t current_step = 0;
static StepperMode_t current_mode = STEPPER_MODE_FULL_STEP;
/**
* @brief 设置步进电机各引脚状态
* @param in1~in4 各相的高低电平状态 (0或1)
*/
static void Set_Stepper_Pins(uint8_t in1, uint8_t in2, uint8_t in3, uint8_t in4)
{
// 设置IN1引脚
if(in1) {
HAL_GPIO_WritePin(STEPPER_IN1_PORT, STEPPER_IN1_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(STEPPER_IN1_PORT, STEPPER_IN1_PIN, GPIO_PIN_RESET);
}
// 设置IN2引脚
if(in2) {
HAL_GPIO_WritePin(STEPPER_IN2_PORT, STEPPER_IN2_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(STEPPER_IN2_PORT, STEPPER_IN2_PIN, GPIO_PIN_RESET);
}
// 设置IN3引脚
if(in3) {
HAL_GPIO_WritePin(STEPPER_IN3_PORT, STEPPER_IN3_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(STEPPER_IN3_PORT, STEPPER_IN3_PIN, GPIO_PIN_RESET);
}
// 设置IN4引脚
if(in4) {
HAL_GPIO_WritePin(STEPPER_IN4_PORT, STEPPER_IN4_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(STEPPER_IN4_PORT, STEPPER_IN4_PIN, GPIO_PIN_RESET);
}
}
/**
* @brief 初始化步进电机
*/
void Stepper_Motor_Init(void)
{
Stepper_Motor_Stop(); // 停止
// 重置步序位置
current_step = 0;
current_mode = STEPPER_MODE_FULL_STEP;
}
/**
* @brief 单步驱动步进电机 (全步模式)
* @param direction 转动方向 (STEPPER_DIR_CW 或 STEPPER_DIR_CCW)
*/
void Stepper_Motor_OneStep_Full(StepperDir_t direction)
{
uint8_t step_idx;
if(direction == STEPPER_DIR_CW) {
// 顺时针:步序递增
step_idx = current_step % 4;
current_step++;
} else {
// 逆时针:步序递减
if(current_step == 0) {
current_step = 4; // 准备递减到3
}
current_step--;
step_idx = current_step % 4;
}
// 根据步序设置引脚状态
Set_Stepper_Pins(
full_step_sequence[step_idx][0],
full_step_sequence[step_idx][1],
full_step_sequence[step_idx][2],
full_step_sequence[step_idx][3]
);
}
/**
* @brief 单步驱动步进电机 (半步模式)
* @param direction 转动方向 (STEPPER_DIR_CW 或 STEPPER_DIR_CCW)
*/
void Stepper_Motor_OneStep_Half(StepperDir_t direction)
{
uint8_t step_idx;
if(direction == STEPPER_DIR_CW) {
// 顺时针:步序递增
step_idx = current_step % 8;
current_step++;
} else {
// 逆时针:步序递减
if(current_step == 0) {
current_step = 8; // 准备递减到7
}
current_step--;
step_idx = current_step % 8;
}
// 根据步序设置引脚状态
Set_Stepper_Pins(
half_step_sequence[step_idx][0],
half_step_sequence[step_idx][1],
half_step_sequence[step_idx][2],
half_step_sequence[step_idx][3]
);
}
/**
* @brief 驱动步进电机转动指定步数
* @param steps 需要转动的步数
* @param direction 转动方向
* @param delay_ms 每步之间的延时 (毫秒)
* @param mode 驱动模式
*/
void Stepper_Motor_RotateSteps(uint16_t steps, StepperDir_t direction, uint16_t delay_ms, StepperMode_t mode)
{
for(uint16_t i = 0; i < steps; i++) {
if(mode == STEPPER_MODE_FULL_STEP) {
Stepper_Motor_OneStep_Full(direction);
} else {
Stepper_Motor_OneStep_Half(direction);
}
// 延时,控制转速
osDelay(delay_ms);
}
}
/**
* @brief 驱动步进电机转动指定角度
* @param angle 需要转动的角度 (0~360)
* @param direction 转动方向
* @param delay_ms 每步之间的延时 (毫秒)
* @param mode 驱动模式
*/
void Stepper_Motor_RotateAngle(float angle, StepperDir_t direction, uint16_t delay_ms, StepperMode_t mode)
{
uint16_t steps;
// 计算需要的步数
// 28BYJ-48步距角为5.625°即每步5.625度
// 在全步模式下每转一圈需要4096步才能达到实际输出轴一圈
// 所以每度需要 4096 / 360 ≈ 11.38 步
if(mode == STEPPER_MODE_FULL_STEP) {
steps = (uint16_t)(angle * (STEPPER_FULL_STEP_PER_REVOLUTION / 360.0));
} else {
steps = (uint16_t)(angle * (STEPPER_HALF_STEP_PER_REVOLUTION / 360.0));
}
Stepper_Motor_RotateSteps(steps, direction, delay_ms, mode);
}
/**
* @brief 驱动步进电机转动指定圈数
* @param rounds 需要转动的圈数
* @param direction 转动方向
* @param delay_ms 每步之间的延时 (毫秒)
* @param mode 驱动模式
*/
void Stepper_Motor_RotateRounds(uint8_t rounds, StepperDir_t direction, uint16_t delay_ms, StepperMode_t mode)
{
uint32_t total_steps;
if(mode == STEPPER_MODE_FULL_STEP) {
total_steps = rounds * STEPPER_FULL_STEP_PER_REVOLUTION;
} else {
total_steps = rounds * STEPPER_HALF_STEP_PER_REVOLUTION;
}
Stepper_Motor_RotateSteps((uint16_t)total_steps, direction, delay_ms, mode);
}
/**
* @brief 停止步进电机
*/
void Stepper_Motor_Stop(void)
{
// 将所有引脚设置为低电平,电机停止
Set_Stepper_Pins(0, 0, 0, 0);
}
/* USER CODE END 1 */

View File

@@ -0,0 +1,108 @@
#ifndef __STEPPER_MOTOR_H
#define __STEPPER_MOTOR_H
#include "main.h"
/* USER CODE BEGIN 0 */
/**
* @brief 28BYJ-48步进电机驱动头文件
*
* 该文件提供对28BYJ-48步进电机的基本控制功能
* 包括正转、反转、单步转动等操作
*
* 28BYJ-48电机参数
* - 步距角5.625° (64步/圈如果使用内部减速比为64:1)
* - 减速比64:1
* - 实际输出轴转一圈需要4096步 (64 * 64)
*/
// 步进电机驱动引脚定义 - 可根据实际硬件连接修改
#define STEPPER_IN1_PORT STEPPER_IN1_GPIO_Port
#define STEPPER_IN1_PIN STEPPER_IN1_Pin
#define STEPPER_IN2_PORT STEPPER_IN2_GPIO_Port
#define STEPPER_IN2_PIN STEPPER_IN2_Pin
#define STEPPER_IN3_PORT STEPPER_IN3_GPIO_Port
#define STEPPER_IN3_PIN STEPPER_IN3_Pin
#define STEPPER_IN4_PORT STEPPER_IN4_GPIO_Port
#define STEPPER_IN4_PIN STEPPER_IN4_Pin
// 步进电机相关常量定义
#define STEPPER_FULL_STEP_PER_REVOLUTION 4096 // 一圈需要的步数 (64*64)
#define STEPPER_HALF_STEP_PER_REVOLUTION 2048 // 半步模式下一圈需要的步数
#define STEPPER_WAVE_DRIVE_PER_REVOLUTION 4096 // 波形驱动模式下一圈需要的步数
// 电机状态定义
typedef enum {
STEPPER_STATUS_OK = 0,
STEPPER_STATUS_ERROR,
STEPPER_STATUS_BUSY
} StepperStatus_t;
// 电机转动方向定义
typedef enum {
STEPPER_DIR_CW = 0, // 顺时针
STEPPER_DIR_CCW // 逆时针
} StepperDir_t;
// 电机驱动模式定义
typedef enum {
STEPPER_MODE_FULL_STEP = 0, // 全步模式
STEPPER_MODE_HALF_STEP // 半步模式
} StepperMode_t;
/**
* @brief 初始化步进电机
*/
void Stepper_Motor_Init(void);
/**
* @brief 单步驱动步进电机 (全步模式)
* @param direction 转动方向 (STEPPER_DIR_CW 或 STEPPER_DIR_CCW)
*/
void Stepper_Motor_OneStep_Full(StepperDir_t direction);
/**
* @brief 单步驱动步进电机 (半步模式)
* @param direction 转动方向 (STEPPER_DIR_CW 或 STEPPER_DIR_CCW)
*/
void Stepper_Motor_OneStep_Half(StepperDir_t direction);
/**
* @brief 驱动步进电机转动指定步数
* @param steps 需要转动的步数
* @param direction 转动方向
* @param delay_ms 每步之间的延时 (毫秒)
* @param mode 驱动模式
*/
void Stepper_Motor_RotateSteps(uint16_t steps, StepperDir_t direction, uint16_t delay_ms, StepperMode_t mode);
/**
* @brief 驱动步进电机转动指定角度
* @param angle 需要转动的角度 (0~360)
* @param direction 转动方向
* @param delay_ms 每步之间的延时 (毫秒)
* @param mode 驱动模式
*/
void Stepper_Motor_RotateAngle(float angle, StepperDir_t direction, uint16_t delay_ms, StepperMode_t mode);
/**
* @brief 驱动步进电机转动指定圈数
* @param rounds 需要转动的圈数
* @param direction 转动方向
* @param delay_ms 每步之间的延时 (毫秒)
* @param mode 驱动模式
*/
void Stepper_Motor_RotateRounds(uint8_t rounds, StepperDir_t direction, uint16_t delay_ms, StepperMode_t mode);
/**
* @brief 停止步进电机
*/
void Stepper_Motor_Stop(void);
/* USER CODE END 0 */
#endif /* __STEPPER_MOTOR_H */

View File

@@ -87,6 +87,14 @@ void Error_Handler(void);
#define M3_IO_GPIO_Port GPIOB
#define MOTOR_Pin GPIO_PIN_6
#define MOTOR_GPIO_Port GPIOC
#define STEPPER_IN1_Pin GPIO_PIN_7
#define STEPPER_IN1_GPIO_Port GPIOC
#define STEPPER_IN2_Pin GPIO_PIN_8
#define STEPPER_IN2_GPIO_Port GPIOC
#define STEPPER_IN3_Pin GPIO_PIN_9
#define STEPPER_IN3_GPIO_Port GPIOC
#define STEPPER_IN4_Pin GPIO_PIN_8
#define STEPPER_IN4_GPIO_Port GPIOA
/* USER CODE BEGIN Private defines */

View File

@@ -36,6 +36,7 @@
#include "bsp_aht30.h"
#include "bsp_rtc.h"
#include "i2c.h"
#include "stepper_motor.h" // 添加步进电机驱动头文件
/* USER CODE END Includes */
@@ -52,6 +53,10 @@
#define KEY2_MANUAL_FEED // 按键2手动喂食仅手动模式有效
#define KEY3_DISPLAY_NEXT // 按键3切换显示界面
#define KEY4_TIME_SET // 按键4长按设置时间
/***********************人体存在和水位检测作为两个单独的按键GPIO输入******************* */
#define KEY_WATER_LEVEL // 水位检测
#define KEY_BODY_EXIST // 人体存在
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
@@ -80,10 +85,26 @@ typedef struct {
uint8_t system_mode; // 系统模式 (0=手动, 1=自动)
} Sensor_Data_t;
// 喂食控制命令枚举
typedef enum {
FEED_CMD_NONE = 0, // 无命令
FEED_CMD_MANUAL, // 手动喂食
FEED_CMD_AUTO, // 自动喂食
FEED_CMD_REMOTE, // 远程喂食
FEED_CMD_TEST // 测试喂食
} Feed_Cmd_t;
// 全局变量
static LCD_Page_t current_page = LCD_PAGE_TIME; // 当前显示页面
static Sensor_Data_t sensor_data = {0}; // 传感器数据
static volatile uint8_t lcd_force_refresh = 0; // 强制刷新标志RTC更新时设置
static volatile uint8_t system_mode = 1; // 系统模式1=自动模式0=手动模式(全局变量)
static volatile uint8_t feeding_in_progress = 0; // 喂食进行中标志
// 喂食控制相关全局变量
static volatile Feed_Cmd_t feed_command = FEED_CMD_NONE; // 喂食命令标志
static volatile uint16_t feed_angle = 90; // 喂食角度默认90度
static volatile uint8_t feed_amount = 1; // 喂食份数
/* USER CODE END Variables */
/* Definitions for defaultTask */
@@ -121,6 +142,13 @@ const osThreadAttr_t sensor_attributes = {
.stack_size = 1024 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for step_motor */
osThreadId_t step_motorHandle;
const osThreadAttr_t step_motor_attributes = {
.name = "step_motor",
.stack_size = 512 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
@@ -140,6 +168,7 @@ extern void wifi_task_mqtt(void *argument);
void LCD_Task(void *argument);
void button_task(void *argument);
void sensorTask(void *argument);
void step_motor_task(void *argument);
void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
@@ -151,6 +180,9 @@ void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN Init */
ST7735_Init(); // 初始化ST7735显示屏
/* USER CODE END Init */
/* USER CODE BEGIN RTOS_MUTEX */
@@ -185,6 +217,9 @@ void MX_FREERTOS_Init(void) {
/* creation of sensor */
sensorHandle = osThreadNew(sensorTask, NULL, &sensor_attributes);
/* creation of step_motor */
step_motorHandle = osThreadNew(step_motor_task, NULL, &step_motor_attributes);
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
/* USER CODE END RTOS_THREADS */
@@ -248,7 +283,6 @@ void LCD_Task(void *argument)
char display_str[32];
RTC_Time_t rtc_time;
uint16_t text_color = ST7735_WHITE;
uint16_t bg_color = ST7735_BLACK;
// 显示区域参数 - 适配160x80横屏
@@ -272,89 +306,137 @@ void LCD_Task(void *argument)
case LCD_PAGE_TIME:
// 时间显示页面
ST7735_FillScreen(bg_color);
ST7735_WriteString(2, 2, "Time", &Font_7x10, text_color, bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_BLUE);
ST7735_WriteString(5, 2, "Time", &Font_7x10, ST7735_WHITE, ST7735_BLUE);
// 使用RTC时间独立于SNTP即使网络断开也能显示
if (BSP_RTC_GetTime(&rtc_time) == 0) {
// 显示时间(大字体,只显示时:分,不显示秒)
snprintf(display_str, sizeof(display_str), "%02d:%02d", rtc_time.hour, rtc_time.minute);
ST7735_WriteString(25, 20, display_str, &Font_16x26, text_color, bg_color);
ST7735_WriteString(40, 30, display_str, &Font_16x26, ST7735_WHITE, bg_color);
// 显示年月日(小字体,向下移动并居中)
snprintf(display_str, sizeof(display_str), "%04d-%02d-%02d",
rtc_time.year, rtc_time.month, rtc_time.day);
ST7735_WriteString(20, 60, display_str, &Font_7x10, ST7735_CYAN, bg_color);
ST7735_WriteString(30, 65, display_str, &Font_7x10, ST7735_CYAN, bg_color);
} else {
ST7735_WriteString(25, 20, "--:--", &Font_16x26, text_color, bg_color);
ST7735_WriteString(20, 60, "----/--/--", &Font_7x10, ST7735_CYAN, bg_color);
ST7735_WriteString(40, 30, "--:--", &Font_16x26, ST7735_WHITE, bg_color);
ST7735_WriteString(30, 65, "----/--/--", &Font_7x10, ST7735_CYAN, bg_color);
}
break;
case LCD_PAGE_TEMP_HUMI:
// 温湿度页面
ST7735_FillScreen(bg_color);
ST7735_WriteString(2, 2, "Temp & Humi", &Font_7x10, text_color, bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_GREEN);
ST7735_WriteString(5, 2, "Temp & Humi", &Font_7x10, ST7735_WHITE, ST7735_GREEN);
// 绘制分隔线
ST7735_DrawLine(0, 15, ST7735_WIDTH, 15, ST7735_WHITE);
// 显示温度(橙色 - 国际标准温度色)
snprintf(display_str, sizeof(display_str), "T: %.2f C", sensor_data.temperature);
ST7735_WriteString(5, 20, display_str, &Font_11x18, ST7735_ORANGE, bg_color);
snprintf(display_str, sizeof(display_str), "Temp:");
ST7735_WriteString(5, 25, display_str, &Font_7x10, ST7735_WHITE, bg_color);
snprintf(display_str, sizeof(display_str), "%.3fC", sensor_data.temperature);
ST7735_WriteString(40, 25, display_str, &Font_16x26, ST7735_ORANGE, bg_color);
// 显示湿度(青色 - 国际标准湿度色)
snprintf(display_str, sizeof(display_str), "H: %.2f %%", sensor_data.humidity);
ST7735_WriteString(5, 45, display_str, &Font_11x18, ST7735_CYAN, bg_color);
snprintf(display_str, sizeof(display_str), "Humi:");
ST7735_WriteString(5, 55, display_str, &Font_7x10, ST7735_WHITE, bg_color);
snprintf(display_str, sizeof(display_str), "%.3f%%", sensor_data.humidity);
ST7735_WriteString(40, 55, display_str, &Font_16x26, ST7735_CYAN, bg_color);
break;
case LCD_PAGE_FOOD_WEIGHT:
// 食物重量页面
ST7735_FillScreen(bg_color);
ST7735_WriteString(2, 2, "Food Weight", &Font_7x10, text_color, bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_YELLOW);
ST7735_WriteString(5, 2, "Food Weight", &Font_7x10, ST7735_BLACK, ST7735_YELLOW);
// 绘制分隔线
ST7735_DrawLine(0, 15, ST7735_WIDTH, 15, ST7735_WHITE);
// 显示食物重量
snprintf(display_str, sizeof(display_str), "%.1f g", sensor_data.food_weight);
ST7735_WriteString(25, 20, display_str, &Font_16x26, text_color, bg_color);
snprintf(display_str, sizeof(display_str), "%.2f g", sensor_data.food_weight);
ST7735_WriteString(15, 25, display_str, &Font_16x26, ST7735_WHITE, bg_color);
// 显示状态
if (sensor_data.food_weight < 50.0f) {
ST7735_WriteString(20, 55, "Low", &Font_11x18, ST7735_RED, bg_color);
ST7735_WriteString(20, 65, "LOW", &Font_11x18, ST7735_RED, bg_color);
} else if (sensor_data.food_weight < 100.0f) {
ST7735_WriteString(15, 55, "Medium", &Font_11x18, ST7735_YELLOW, bg_color);
ST7735_WriteString(15, 65, "MEDIUM", &Font_11x18, ST7735_YELLOW, bg_color);
} else {
ST7735_WriteString(20, 55, "Good", &Font_11x18, ST7735_GREEN, bg_color);
ST7735_WriteString(20, 65, "GOOD", &Font_11x18, ST7735_GREEN, bg_color);
}
break;
case LCD_PAGE_WATER_LEVEL:
// 水位页面
ST7735_FillScreen(bg_color);
ST7735_WriteString(2, 2, "Water Level", &Font_7x10, text_color, bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_CYAN);
ST7735_WriteString(5, 2, "Water Level", &Font_7x10, ST7735_BLACK, ST7735_CYAN);
// 绘制分隔线
ST7735_DrawLine(0, 15, ST7735_WIDTH, 15, ST7735_WHITE);
// 根据水位传感器状态显示
if (sensor_data.water_level == 0) {
ST7735_WriteString(20, 20, "NO WATER", &Font_16x26, ST7735_RED, bg_color);
ST7735_WriteString(35, 55, "Add water", &Font_11x18, ST7735_YELLOW, bg_color);
// 绘制警告背景
ST7735_FillRectangle(0, 20, ST7735_WIDTH, 30, ST7735_RED);
ST7735_WriteString(20, 20, "NO WATER", &Font_16x26, ST7735_WHITE, ST7735_RED);
ST7735_WriteString(10, 60, "Add water", &Font_16x26, ST7735_YELLOW, bg_color);
} else {
ST7735_WriteString(50, 25, "OK", &Font_16x26, ST7735_GREEN, bg_color);
ST7735_WriteString(35, 55, "Water OK", &Font_11x18, text_color, bg_color);
// 绘制正常背景
ST7735_FillRectangle(0, 20, ST7735_WIDTH, 30, ST7735_GREEN);
ST7735_WriteString(5, 20, "WATER OK", &Font_16x26, ST7735_WHITE, ST7735_GREEN);
ST7735_WriteString(10, 60, "Not add", &Font_16x26, ST7735_WHITE, bg_color);
}
break;
case LCD_PAGE_SYSTEM_STATUS:
// 系统状态页面
ST7735_FillScreen(bg_color);
ST7735_WriteString(2, 2, "System Status", &Font_7x10, text_color, bg_color);
// 绘制标题栏
ST7735_FillRectangle(0, 0, ST7735_WIDTH, 15, ST7735_MAGENTA);
ST7735_WriteString(5, 2, "System", &Font_7x10, ST7735_WHITE, ST7735_MAGENTA);
// 绘制分隔线
ST7735_DrawLine(0, 15, ST7735_WIDTH, 15, ST7735_WHITE);
// 显示MQTT连接状态
snprintf(display_str, sizeof(display_str), "MQTT:");
ST7735_WriteString(5, 25, display_str, &Font_7x10, ST7735_WHITE, bg_color);
if (WIFI_Is_MQTT_Connected()) {
ST7735_WriteString(5, 20, "MQTT: OK", &Font_11x18, ST7735_GREEN, bg_color);
ST7735_WriteString(40, 25, "OK", &Font_7x10, ST7735_GREEN, bg_color);
} else {
ST7735_WriteString(5, 20, "MQTT: OFF", &Font_11x18, ST7735_RED, bg_color);
ST7735_WriteString(40, 25, "OFF", &Font_7x10, ST7735_RED, bg_color);
}
// 显示系统模式
snprintf(display_str, sizeof(display_str), "Mode:");
ST7735_WriteString(5, 45, display_str, &Font_7x10, ST7735_WHITE, bg_color);
if (sensor_data.system_mode) {
ST7735_WriteString(5, 45, "Mode: AUTO", &Font_11x18, text_color, bg_color);
ST7735_WriteString(40, 45, "AUTO", &Font_7x10, ST7735_GREEN, bg_color);
} else {
ST7735_WriteString(5, 45, "Mode: MANUAL", &Font_11x18, text_color, bg_color);
ST7735_WriteString(40, 45, "MANUAL", &Font_7x10, ST7735_YELLOW, bg_color);
}
// 显示喂食状态
snprintf(display_str, sizeof(display_str), "Feed:");
ST7735_WriteString(5, 65, display_str, &Font_7x10, ST7735_WHITE, bg_color);
if (feeding_in_progress) {
ST7735_WriteString(40, 65, "Busy", &Font_7x10, ST7735_ORANGE, bg_color);
} else {
ST7735_WriteString(40, 65, "Idle", &Font_7x10, ST7735_WHITE, bg_color);
}
break;
}
@@ -464,6 +546,131 @@ void sensorTask(void *argument)
/* USER CODE END sensorTask */
}
/**
* @brief 请求喂食操作
* @param cmd: 喂食命令类型
* @param angle: 转动角度
* @param amount: 喂食份数
* @retval 1=请求成功, 0=请求失败(正在喂食中)
*/
uint8_t Request_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount) {
if (feeding_in_progress) {
elog_w("FEED", "喂食进行中,无法接受新命令");
return 0;
}
feed_command = cmd;
feed_angle = angle;
feed_amount = amount;
elog_i("FEED", "喂食请求已提交: cmd=%d, angle=%d, amount=%d", cmd, angle, amount);
return 1;
}
/**
* @brief 获取当前喂食命令
* @retval 当前喂食命令
*/
Feed_Cmd_t Get_Feed_Command(void) {
return feed_command;
}
/**
* @brief 清除喂食命令
*/
void Clear_Feed_Command(void) {
feed_command = FEED_CMD_NONE;
}
/**
* @brief 执行喂食操作
* @param cmd: 喂食命令类型
* @param angle: 转动角度
* @param amount: 喂食份数
*/
static void Execute_Feed(Feed_Cmd_t cmd, uint16_t angle, uint8_t amount) {
if (feeding_in_progress) {
return; // 防止重复执行
}
feeding_in_progress = 1;
// 根据命令类型播放不同音频
switch (cmd) {
case FEED_CMD_MANUAL:
MP3_Play(FEED_MANUAL_TRIGGER);
elog_i("FEED", "执行手动喂食: 角度%d度, %d份", angle, amount);
break;
case FEED_CMD_AUTO:
MP3_Play(FEED_AUTO_START);
elog_i("FEED", "执行自动喂食: 角度%d度, %d份", angle, amount);
break;
case FEED_CMD_REMOTE:
MP3_Play(REMOTE_CMD_RECEIVED);
elog_i("FEED", "执行远程喂食: 角度%d度, %d份", angle, amount);
break;
case FEED_CMD_TEST:
elog_i("FEED", "执行测试喂食: 角度%d度", angle);
break;
default:
break;
}
// 执行实际的喂食动作
for (uint8_t i = 0; i < amount; i++) {
Stepper_Motor_RotateAngle(angle, STEPPER_DIR_CW, 2, STEPPER_MODE_FULL_STEP);
if (i < amount - 1) {
osDelay(1000); // 多份之间间隔1秒
}
}
// 等待食物落下
osDelay(3000);
// 停止电机
Stepper_Motor_Stop();
// 播放完成音效
MP3_Play(FEED_COMPLETE);
elog_i("FEED", "喂食完成");
feeding_in_progress = 0;
}
/* USER CODE BEGIN Header_step_motor_task */
/**
* @brief Function implementing the step_motor thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_step_motor_task */
void step_motor_task(void *argument)
{
/* USER CODE BEGIN step_motor_task */
// 初始化步进电机
Stepper_Motor_Init();
/* Infinite loop */
for(;;)
{
// 检查是否有喂食命令需要执行
if (feed_command != FEED_CMD_NONE && !feeding_in_progress) {
Feed_Cmd_t current_cmd = feed_command;
uint16_t current_angle = feed_angle;
uint8_t current_amount = feed_amount;
// 清除命令标志
Clear_Feed_Command();
// 执行喂食
Execute_Feed(current_cmd, current_angle, current_amount);
}
osDelay(100); // 短暂延时,保持任务响应性
}
/* USER CODE END step_motor_task */
}
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
@@ -475,6 +682,8 @@ static Button KEY1; // 按键1
static Button KEY2; // 按键2
static Button KEY3; // 按键3
static Button KEY4; // 按键4
static Button M3_IO; // M3 IO
static Button HC_SR505; //HC-SR505
uint8_t read_button_gpio(uint8_t button_id) {
switch (button_id) {
@@ -490,14 +699,58 @@ uint8_t read_button_gpio(uint8_t button_id) {
case 4:
return HAL_GPIO_ReadPin(KEY4_GPIO_Port, KEY4_Pin);
break;
case 5:
return HAL_GPIO_ReadPin(M3_IO_GPIO_Port, M3_IO_Pin);
break;
case 6:
return HAL_GPIO_ReadPin(HC_SR505_IO_GPIO_Port, HC_SR505_IO_Pin);
break;
default:
return 0;
}
}
void key1_single_click_handler(Button *btn) { elog_i("KEY", "按键1单击"); }
/**
* @brief 按键1处理函数模式切换自动/手动)
* @param btn: 按键句柄
*/
void key1_single_click_handler(Button *btn) {
// 切换系统模式
system_mode = !system_mode; // 0和1之间切换
void key2_single_click_handler(Button *btn) { elog_i("KEY", "按键2单击"); }
// 更新传感器数据中的模式状态
sensor_data.system_mode = system_mode;
// 记录日志
elog_i("KEY", "按键1单击 - 模式切换: %s", system_mode ? "自动模式" : "手动模式");
// 播放模式切换音效
MP3_Play(system_mode ? MODE_AUTO : MODE_MANUAL);
// 强制刷新LCD显示以更新模式状态
lcd_force_refresh = 1;
}
/**
* @brief 按键2处理函数手动喂食仅手动模式有效
* @param btn: 按键句柄
*/
void key2_single_click_handler(Button *btn) {
// 检查是否为手动模式
if (!system_mode) {
// 手动模式下才允许手动喂食
if (Request_Feed(FEED_CMD_MANUAL, 90, 1)) {
elog_i("KEY", "按键2单击 - 手动喂食请求已提交");
} else {
elog_w("KEY", "喂食进行中,请稍后再试");
MP3_Play(SYS_ERROR_ALARM);
}
} else {
// 自动模式下按键无效
elog_w("KEY", "当前为自动模式按键2无效");
MP3_Play(SYS_ERROR_ALARM);
}
}
void key3_single_click_handler(Button *btn) {
elog_i("KEY", "按键3单击");
@@ -506,13 +759,28 @@ void key3_single_click_handler(Button *btn) {
void key4_single_click_handler(Button *btn) { elog_i("KEY", "按键4单击"); }
void M3_long_press_start_handler(Button *btn) {
elog_i("KEY", "M3水位传感器检测到有水低电平");
sensor_data.water_level = 1;
}
void M3_press_up_handler(Button *btn) {
elog_i("KEY", "M3水位传感器检测到无水高电平");
sensor_data.water_level = 0;
}
void HC_SR055_long_click_handler(Button *btn) { elog_i("KEY", "HC-SR505触发"); }
void user_button_init(void) {
// 初始化按键 (active_level: 0=低电平有效, 1=高电平有效)
button_init(&KEY1, read_button_gpio, 0, 1);
button_init(&KEY2, read_button_gpio, 0, 2);
button_init(&KEY3, read_button_gpio, 0, 3);
button_init(&KEY4, read_button_gpio, 0, 4);
button_init(&M3_IO, read_button_gpio, 0, 5);
button_init(&HC_SR505, read_button_gpio, 1, 6);
elog_i("BUTTON", "按键初始化完成");
// 设置按键回调函数
@@ -520,6 +788,9 @@ void user_button_init(void) {
button_attach(&KEY2, BTN_SINGLE_CLICK, key2_single_click_handler);
button_attach(&KEY3, BTN_SINGLE_CLICK, key3_single_click_handler);
button_attach(&KEY4, BTN_SINGLE_CLICK, key4_single_click_handler);
button_attach(&M3_IO, BTN_LONG_PRESS_START, M3_long_press_start_handler);
button_attach(&M3_IO, BTN_PRESS_UP, M3_press_up_handler);
button_attach(&HC_SR505, BTN_LONG_PRESS_START, HC_SR055_long_click_handler); // 长按触发
elog_i("BUTTON", "按键回调函数设置完成");
@@ -528,9 +799,41 @@ void user_button_init(void) {
button_start(&KEY2);
button_start(&KEY3);
button_start(&KEY4);
button_start(&M3_IO);
button_start(&HC_SR505);
elog_i("BUTTON", "按键任务已启动");
}
/**
* @brief 获取当前系统模式
* @retval 系统模式 (0=手动, 1=自动)
*/
uint8_t Get_System_Mode(void) {
return system_mode;
}
/**
* @brief 设置系统模式
* @param mode: 目标模式 (0=手动, 1=自动)
*/
void Set_System_Mode(uint8_t mode) {
if (mode <= 1) {
system_mode = mode;
sensor_data.system_mode = mode;
lcd_force_refresh = 1; // 强制刷新显示
elog_i("SYSTEM", "系统模式设置为: %s", mode ? "自动" : "手动");
}
}
/**
* @brief 检查是否正在喂食
* @retval 1=正在喂食, 0=空闲
*/
uint8_t Is_Feeding_In_Progress(void) {
return feeding_in_progress;
}
/* USER CODE END KEY Prototypes */
/* USER CODE BEGIN LCD_Page_Functions */
@@ -620,4 +923,3 @@ void RTC_TimeUpdateCallback(void) {
/* USER CODE END LCD_Page_Functions */
/* USER CODE END Application */

View File

@@ -51,11 +51,11 @@ void MX_GPIO_Init(void)
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, HX711_SCK_Pin|LCD_DC_Pin|LCD_RESET_Pin|GPIO_PIN_8, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, HX711_SCK_Pin|LCD_DC_Pin|LCD_RESET_Pin|STEPPER_IN4_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOC, LCD_CS_Pin|LCD_BLK_Pin|MOTOR_Pin|GPIO_PIN_7
|GPIO_PIN_8|GPIO_PIN_9, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOC, LCD_CS_Pin|LCD_BLK_Pin|MOTOR_Pin|STEPPER_IN1_Pin
|STEPPER_IN2_Pin|STEPPER_IN3_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOB, LED2_Pin|LED1_Pin, GPIO_PIN_SET);
@@ -72,17 +72,17 @@ void MX_GPIO_Init(void)
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(HX711_DOUT_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pins : HX711_SCK_Pin LCD_DC_Pin LCD_RESET_Pin PA8 */
GPIO_InitStruct.Pin = HX711_SCK_Pin|LCD_DC_Pin|LCD_RESET_Pin|GPIO_PIN_8;
/*Configure GPIO pins : HX711_SCK_Pin LCD_DC_Pin LCD_RESET_Pin STEPPER_IN4_Pin */
GPIO_InitStruct.Pin = HX711_SCK_Pin|LCD_DC_Pin|LCD_RESET_Pin|STEPPER_IN4_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pins : LCD_CS_Pin LCD_BLK_Pin MOTOR_Pin PC7
PC8 PC9 */
GPIO_InitStruct.Pin = LCD_CS_Pin|LCD_BLK_Pin|MOTOR_Pin|GPIO_PIN_7
|GPIO_PIN_8|GPIO_PIN_9;
/*Configure GPIO pins : LCD_CS_Pin LCD_BLK_Pin MOTOR_Pin STEPPER_IN1_Pin
STEPPER_IN2_Pin STEPPER_IN3_Pin */
GPIO_InitStruct.Pin = LCD_CS_Pin|LCD_BLK_Pin|MOTOR_Pin|STEPPER_IN1_Pin
|STEPPER_IN2_Pin|STEPPER_IN3_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;

View File

@@ -35,7 +35,7 @@ Dma.USART1_TX.0.Priority=DMA_PRIORITY_VERY_HIGH
Dma.USART1_TX.0.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority
FREERTOS.FootprintOK=true
FREERTOS.IPParameters=Tasks01,FootprintOK,configTOTAL_HEAP_SIZE
FREERTOS.Tasks01=defaultTask,41,256,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;wifi_mqtt,40,3000,wifi_task_mqtt,As external,NULL,Dynamic,NULL,NULL;LCD_SHOW_Task,40,1024,LCD_Task,Default,NULL,Dynamic,NULL,NULL;button,50,512,button_task,Default,NULL,Dynamic,NULL,NULL;sensor,24,1024,sensorTask,Default,NULL,Dynamic,NULL,NULL
FREERTOS.Tasks01=defaultTask,41,256,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;wifi_mqtt,40,3000,wifi_task_mqtt,As external,NULL,Dynamic,NULL,NULL;LCD_SHOW_Task,40,1024,LCD_Task,Default,NULL,Dynamic,NULL,NULL;button,50,512,button_task,Default,NULL,Dynamic,NULL,NULL;sensor,24,1024,sensorTask,Default,NULL,Dynamic,NULL,NULL;step_motor,24,512,step_motor_task,Default,NULL,Dynamic,NULL,NULL
FREERTOS.configTOTAL_HEAP_SIZE=30000
File.Version=6
GPIO.groupedBy=Group By Peripherals
@@ -147,6 +147,8 @@ PA6.Locked=true
PA6.Signal=GPIO_Output
PA7.Mode=TX_Only_Simplex_Unidirect_Master
PA7.Signal=SPI1_MOSI
PA8.GPIOParameters=GPIO_Label
PA8.GPIO_Label=STEPPER_IN4
PA8.Locked=true
PA8.Signal=GPIO_Output
PA9.Mode=Asynchronous
@@ -215,10 +217,16 @@ PC6.GPIOParameters=GPIO_Label
PC6.GPIO_Label=MOTOR
PC6.Locked=true
PC6.Signal=GPIO_Output
PC7.GPIOParameters=GPIO_Label
PC7.GPIO_Label=STEPPER_IN1
PC7.Locked=true
PC7.Signal=GPIO_Output
PC8.GPIOParameters=GPIO_Label
PC8.GPIO_Label=STEPPER_IN2
PC8.Locked=true
PC8.Signal=GPIO_Output
PC9.GPIOParameters=GPIO_Label
PC9.GPIO_Label=STEPPER_IN3
PC9.Locked=true
PC9.Signal=GPIO_Output
PCC.Checker=false