摘要:本文深入讲解智能宠物喂食器的硬件设计、固件开发、云端控制、定时计划、食物识别等完整技术方案。
一、产品需求分析
1.1 核心功能
| 功能 | 需求描述 | 技术实现 |
|---|---|---|
| 定时投喂 | 支持10组定时计划 | RTC + Cron表达式 |
| 远程控制 | APP一键投喂 | MQTT指令 |
| 精准称重 | ±1g精度 | HX711 + 称重传感器 |
| 食量统计 | 日/周/月统计 | 时序数据库 |
| 缺粮提醒 | 低于阈值告警 | 红外/超声波检测 |
| 语音呼唤 | 主人录音播放 | I2S + 功放 |
| 食物识别 | 干粮/湿粮识别 | 摄像头 + AI |
| 防卡粮 | 卡粮检测与处理 | 电流检测 + 反转 |
二、硬件设计
2.1 系统框图
┌─────────────────────────────────────────────────┐
│ ESP32-S3 主控 │
│ ┌─────────────────────────────────────────┐ │
│ │ WiFi + BLE + 外设接口 │ │
│ └─────────────────────────────────────────┘ │
│ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ 电机驱动│ │ 称重模块│ │ 语音模块│ │
│ │ TB6612 │ │ HX711 │ │ MAX98357│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │
│ │ 直流电机│ │ 称重传感│ │ 喇叭 │ │
│ │ +螺旋杆 │ │ 器 │ │ +麦克风 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 红外检测│ │ 温湿度 │ │ 摄像头 │ │
│ │ 缺粮 │ │ 传感器 │ │ OV2640 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────┘
2.2 电机控制电路
c
// TB6612电机驱动
#define MOTOR_IN1 GPIO_NUM_1
#define MOTOR_IN2 GPIO_NUM_2
#define MOTOR_PWM GPIO_NUM_3
void motor_init(void) {
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << MOTOR_IN1) | (1ULL << MOTOR_IN2),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_conf);
// PWM配置
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = LEDC_TIMER_10_BIT,
.freq_hz = 1000,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
ledc_channel_config_t channel_conf = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.gpio_num = MOTOR_PWM,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&channel_conf);
}
void motor_set_speed(int speed) {
// speed: -100 ~ 100
if (speed > 0) {
gpio_set_level(MOTOR_IN1, 1);
gpio_set_level(MOTOR_IN2, 0);
} else if (speed < 0) {
gpio_set_level(MOTOR_IN1, 0);
gpio_set_level(MOTOR_IN2, 1);
speed = -speed;
} else {
gpio_set_level(MOTOR_IN1, 0);
gpio_set_level(MOTOR_IN2, 0);
}
uint32_t duty = speed * 1023 / 100;
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}
2.3 称重传感器
c
// HX711称重模块
#define HX711_SCK GPIO_NUM_4
#define HX711_DOUT GPIO_NUM_5
typedef struct {
float scale_factor; // 比例系数
long offset; // 零点偏移
float weight; // 当前重量(g)
float last_weight; // 上次重量
} hx711_t;
void hx711_init(hx711_t *hx) {
gpio_set_direction(HX711_SCK, GPIO_MODE_OUTPUT);
gpio_set_direction(HX711_DOUT, GPIO_MODE_INPUT);
hx->scale_factor = 1.0;
hx->offset = 0;
}
long hx711_read_raw(void) {
// 等待数据就绪
while (gpio_get_level(HX711_DOUT) == 1) {
vTaskDelay(1);
}
long data = 0;
for (int i = 0; i < 24; i++) {
gpio_set_level(HX711_SCK, 1);
ets_delay_us(1);
data = (data << 1) | gpio_get_level(HX711_DOUT);
gpio_set_level(HX711_SCK, 0);
ets_delay_us(1);
}
// 第25个脉冲设置增益
gpio_set_level(HX711_SCK, 1);
ets_delay_us(1);
gpio_set_level(HX711_SCK, 0);
// 转换为有符号数
if (data & 0x800000) {
data |= 0xFF000000;
}
return data;
}
float hx711_get_weight(hx711_t *hx, int times) {
long sum = 0;
for (int i = 0; i < times; i++) {
sum += hx711_read_raw();
}
long avg = sum / times;
hx->weight = (avg - hx->offset) / hx->scale_factor;
return hx->weight;
}
void hx711_calibrate(hx711_t *hx, float known_weight) {
// 1. 空载时记录零点
printf("请移除所有物品...\n");
vTaskDelay(3000 / portTICK_PERIOD_MS);
hx->offset = hx711_read_raw();
printf("零点偏移: %ld\n", hx->offset);
// 2. 放置已知重量物品
printf("请放置 %.1fg 物品...\n", known_weight);
vTaskDelay(3000 / portTICK_PERIOD_MS);
long reading = hx711_read_raw();
// 3. 计算比例系数
hx->scale_factor = (reading - hx->offset) / known_weight;
printf("比例系数: %.2f\n", hx->scale_factor);
}
三、定时投喂系统
3.1 定时任务管理
c
typedef struct {
uint8_t hour;
uint8_t minute;
uint8_t weekdays; // 位掩码:bit0=周日, bit1=周一...
float amount_g; // 投喂量(克)
bool enabled;
char voice_msg[64]; // 语音消息
} feeding_schedule_t;
typedef struct {
feeding_schedule_t schedules[MAX_SCHEDULES];
int count;
time_t last_feed_time;
float daily_total;
} feeding_manager_t;
// 检查是否需要投喂
bool check_schedule(feeding_manager_t *mgr) {
time_t now = time(NULL);
struct tm *t = localtime(&now);
for (int i = 0; i < mgr->count; i++) {
feeding_schedule_t *s = &mgr->schedules[i];
if (!s->enabled) continue;
// 检查时间
if (t->tm_hour != s->hour || t->tm_min != s->minute) continue;
// 检查星期
uint8_t weekday_bit = 1 << t->tm_wday;
if (!(s->weekdays & weekday_bit)) continue;
// 检查是否已投喂(防止重复)
if (now - mgr->last_feed_time < 60) continue;
// 执行投喂
execute_feeding(s->amount_g, s->voice_msg);
mgr->last_feed_time = now;
mgr->daily_total += s->amount_g;
return true;
}
return false;
}
// Cron表达式解析(高级版本)
typedef struct {
uint8_t minute[60]; // 0-59
uint8_t hour[24]; // 0-23
uint8_t day_of_month[32]; // 1-31
uint8_t month[12]; // 0-11
uint8_t day_of_week[7]; // 0-6 (0=周日)
} cron_expr_t;
bool cron_match(cron_expr_t *cron, struct tm *t) {
return cron->minute[t->tm_min] &&
cron->hour[t->tm_hour] &&
cron->day_of_month[t->tm_mday] &&
cron->month[t->tm_mon] &&
cron->day_of_week[t->tm_wday];
}
3.2 精准投喂控制
c
typedef struct {
float target_amount; // 目标投喂量
float current_amount; // 当前已投喂
float start_weight; // 开始时食盆重量
bool in_progress; // 投喂进行中
uint32_t start_time; // 开始时间
uint32_t timeout_ms; // 超时时间
} feeding_process_t;
void execute_feeding(float amount_g, const char *voice_msg) {
feeding_process_t process = {
.target_amount = amount_g,
.current_amount = 0,
.start_weight = hx711_get_weight(&hx, 10),
.in_progress = true,
.start_time = get_timestamp(),
.timeout_ms = 30000 // 30秒超时
};
// 播放语音
if (voice_msg[0] != '\0') {
play_voice(voice_msg);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
// 启动电机
motor_set_speed(80);
// 监控投喂量
while (process.in_progress) {
float current_weight = hx711_get_weight(&hx, 5);
process.current_amount = current_weight - process.start_weight;
// 达到目标量
if (process.current_amount >= process.target_amount) {
motor_set_speed(0);
process.in_progress = false;
break;
}
// 超时检测
if (get_timestamp() - process.start_time > process.timeout_ms) {
motor_set_speed(0);
report_error(ERROR_FEEDING_TIMEOUT);
process.in_progress = false;
break;
}
// 卡粮检测
if (detect_jam()) {
motor_set_speed(-30); // 反转
vTaskDelay(500 / portTICK_PERIOD_MS);
motor_set_speed(80); // 重新正转
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
// 记录投喂日志
log_feeding(process.current_amount, process.start_time);
}
四、食物识别
4.1 摄像头方案
c
// OV2640摄像头初始化
camera_config_t camera_config = {
.pin_pwdn = CAM_PIN_PWDN,
.pin_reset = CAM_PIN_RESET,
.pin_xclk = CAM_PIN_XCLK,
.pin_sccb_sda = CAM_PIN_SIOD,
.pin_sccb_scl = CAM_PIN_SIOC,
.pin_d7 = CAM_PIN_D7,
.pin_d6 = CAM_PIN_D6,
.pin_d5 = CAM_PIN_D5,
.pin_d4 = CAM_PIN_D4,
.pin_d3 = CAM_PIN_D3,
.pin_d2 = CAM_PIN_D2,
.pin_d1 = CAM_PIN_D1,
.pin_d0 = CAM_PIN_D0,
.pin_vsync = CAM_PIN_VSYNC,
.pin_href = CAM_PIN_HREF,
.pin_pclk = CAM_PIN_PCLK,
.xclk_freq_hz = 20000000,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_QVGA,
.jpeg_quality = 12,
.fb_count = 2,
.grab_mode = CAMERA_GRAB_LATEST
};
esp_err_t camera_init() {
return esp_camera_init(&camera_config);
}
4.2 食物类型识别
python
# 食物识别模型
class FoodTypeClassifier:
def __init__(self, model_path):
self.model = tflite.Interpreter(model_path)
self.model.allocate_tensors()
self.labels = ['dry_food', 'wet_food', 'treats', 'empty', 'unknown']
def classify(self, image):
# 预处理
input_details = self.model.get_input_details()
height, width = input_details[0]['shape'][1:3]
resized = cv2.resize(image, (width, height))
normalized = resized / 255.0
input_data = np.expand_dims(normalized, axis=0).astype(np.float32)
# 推理
self.model.set_tensor(input_details[0]['index'], input_data)
self.model.invoke()
output_details = self.model.get_output_details()
output = self.model.get_tensor(output_details[0]['index'])
# 解析结果
class_idx = np.argmax(output[0])