2、是选择已有的开发板,用来拓展功能。在这个思路下去寻找ESP32-S3的开发板,最贴合自己需求的是ESP-EYE板子,但是价格太贵,放弃了。
最终选择了Firebeetle 2 ESP32-S3这个板子,价格不错,主控是ESP32-S3-WROOM-1-N16R8模组拥有16MB Flash和8MB PSRAM。主控是32 位 LX7 双核处理器,主频高达 240 MHz;内置 512 KB SRAM、384 KB ROM 存储空间,并支持多个外部 SPI、Dual SPI、 Quad SPI、Octal SPI、QPI、OPI flash 和片外 RAM额外增加用于加速神经网络计算和信号处理等工作的向量指令 (vector instructions);45 个可编程 GPIO,支持常用外设接口如 SPI、I2S、I2C、PWM、RMT、ADC、DAC、UART、SD/MMC 主机控制器和 TWAITM 控制器等。板子上带着OV2640摄像头。板子也很小巧。在这个板子的基础上,再做一个扩展PCB,加上TFT显示屏,就可以来跑自己自己的项目啦!既然打样了PCB就顺便加上了麦克风、SD卡,和一颗SHT30。
二 设计框图及原理介绍
项目框图初步设计都是由Scheme-it网页绘制。这里是分享链接:
https://www.digikey.cn/schemeit/project/esp32-s3-catfeed-3cf574619fec4bfa85f548852a34f69c
如图所示,整个项目并不复杂。OV2640摄像头作为信号输入,摄像头将图像信息传送给主控。主控进行分析,是否是猫咪出现。这里判断猫咪出现使用了乐鑫提供的AI算法,使用了ESP-WHO这个项目中的cat_face_detection例程。主控将摄像头获得的图像在TFT屏幕上显示出来,当判断出当前图像中有猫咪存在,就驱动舵机去添加猫粮。这里舵机我使用了优必选的串口舵机,使用3个串口舵机做成一个机械臂,用来实现需要的动作。这个舵机是串口舵机,通过串口命令字来进行控制。
三 项目具体实现
整改项目是依赖cat_face_detection例程来实现的,首先从github上将esp-who项目项目clone回来。首先驱动摄像头,在配置文件中我选择了“ESP-S3-EYE DevKit”,然后修改components/modules/camera/who_camera.h文件,这个文件中指明了OV2640与ESP32S3连接对应的管脚。
#elif CONFIG_CAMERA_MODULE_ESP_S3_EYE
#define CAMERA_MODULE_NAME "ESP-S3-EYE"
#define CAMERA_PIN_PWDN -1
#define CAMERA_PIN_RESET -1
#define CAMERA_PIN_VSYNC 6
#define CAMERA_PIN_HREF 42
#define CAMERA_PIN_PCLK 5
#define CAMERA_PIN_XCLK 45
#define CAMERA_PIN_SIOD 1
#define CAMERA_PIN_SIOC 2
#define CAMERA_PIN_D0 39
#define CAMERA_PIN_D1 40
#define CAMERA_PIN_D2 41
#define CAMERA_PIN_D3 4
#define CAMERA_PIN_D4 7
#define CAMERA_PIN_D5 8
#define CAMERA_PIN_D6 46
#define CAMERA_PIN_D7 48
Firebeetle 2 ESP32-S3这个板子摄像头兼容OV2640和0V7725,板子上使用了一颗AXP313A的电源管理芯片,来控制摄像头的电源。所以在使用摄像头之前还需要使用IIC协议,控制电源芯片,先给摄像头供电。修改components/modules/camera/who_camera.c文件,这里我使用的是esp-idf4.4的写法。
#include "who_camera.h"
#include "esp_log.h"
#include "esp_system.h"
static const char *TAG = "who_camera";
// ---------------------------------------------
#include "driver/i2c.h"
#define AXP313A_ADDR 0x36
#define I2C_MASTER_SCL_IO 2
#define I2C_MASTER_SDA_IO 1
#define I2C_MASTER_NUM 0
#define I2C_MASTER_TX_BUF_DISABLE 0
#define I2C_MASTER_RX_BUF_DISABLE 0
#define I2C_MASTER_FREQ_HZ 400000
#define I2C_MASTER_TIMEOUT_MS 1000
static esp_err_t AXP313A_register_write_byte(uint8_t reg_addr, uint8_t data)
{
int ret;
uint8_t write_buf[2] = {reg_addr, data};
ret = i2c_master_write_to_device(I2C_MASTER_NUM, AXP313A_ADDR, write_buf, sizeof(write_buf), I2C_MASTER_TIMEOUT_MS / portTICK_RATE_MS);
return ret;
}
static esp_err_t i2c_master_init(void)
{
int i2c_master_port = I2C_MASTER_NUM;
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
i2c_param_config(i2c_master_port, &conf);
return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
}
// ---------------------------------------------
static QueueHandle_t xQueueFrameO = NULL;
static void task_process_handler(void *arg)
{
while (true)
{
camera_fb_t *frame = esp_camera_fb_get();
if (frame)
xQueueSend(xQueueFrameO, &frame, portMAX_DELAY);
}
}
void register_camera(const pixformat_t pixel_fromat,
const framesize_t frame_size,
const uint8_t fb_count,
const QueueHandle_t frame_o)
{
ESP_LOGI(TAG, "Camera module is %s", CAMERA_MODULE_NAME);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//开启摄像头前,先打开电源
ESP_ERROR_CHECK(i2c_master_init());
ESP_LOGI(TAG, "I2C initialized successfully");
ESP_ERROR_CHECK(AXP313A_register_write_byte(0x00,0x04));
vTaskDelay(100);
ESP_ERROR_CHECK(AXP313A_register_write_byte(0x10,0x19));
ESP_ERROR_CHECK(AXP313A_register_write_byte(0x16,0x07)); //1.2v
ESP_ERROR_CHECK(AXP313A_register_write_byte(0x17,23)); //2.8v
ESP_LOGI(TAG, "I2C unitialized successfully");
vTaskDelay(1000);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#if CONFIG_CAMERA_MODULE_ESP_EYE || CONFIG_CAMERA_MODULE_ESP32_CAM_BOARD
/* IO13, IO14 is designed for JTAG by default,
* to use it as generalized input,
* firstly declair it as pullup input */
gpio_config_t conf;
conf.mode = GPIO_MODE_INPUT;
conf.pull_up_en = GPIO_PULLUP_ENABLE;
conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
conf.intr_type = GPIO_INTR_DISABLE;
conf.pin_bit_mask = 1LL << 13;
gpio_config(&conf);
conf.pin_bit_mask = 1LL << 14;
gpio_config(&conf);
#endif
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = CAMERA_PIN_D0;
config.pin_d1 = CAMERA_PIN_D1;
config.pin_d2 = CAMERA_PIN_D2;
config.pin_d3 = CAMERA_PIN_D3;
config.pin_d4 = CAMERA_PIN_D4;
config.pin_d5 = CAMERA_PIN_D5;
config.pin_d6 = CAMERA_PIN_D6;
config.pin_d7 = CAMERA_PIN_D7;
config.pin_xclk = CAMERA_PIN_XCLK;
config.pin_pclk = CAMERA_PIN_PCLK;
config.pin_vsync = CAMERA_PIN_VSYNC;
config.pin_href = CAMERA_PIN_HREF;
config.pin_sscb_sda = CAMERA_PIN_SIOD;
config.pin_sscb_scl = CAMERA_PIN_SIOC;
config.pin_pwdn = CAMERA_PIN_PWDN;
config.pin_reset = CAMERA_PIN_RESET;
config.xclk_freq_hz = XCLK_FREQ_HZ;
config.pixel_format = pixel_fromat;
config.frame_size = frame_size;
config.jpeg_quality = 10;
config.fb_count = fb_count;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
return;
}
sensor_t *s = esp_camera_sensor_get();
s->set_vflip(s, 0); // flip it back
// initial sensors are flipped vertically and colors are a bit saturated
if (s->id.PID == OV3660_PID)
{
s->set_brightness(s, 1); // up the blightness just a bit
s->set_saturation(s, -2); // lower the saturation
}
xQueueFrameO = frame_o;
xTaskCreatePinnedToCore(task_process_handler, TAG, 2 * 1024, NULL, 5, NULL, 1);
}
然后驱动TFT屏幕,例程中屏幕使用的是240x240的屏幕,我使用的是172x32的屏幕,做好屏幕适配即可。阅读例程的主程序,主程序看上去特别简单,使用了两个消息队列:摄像头不停地获取数据,丢到消息队列里。然后有个猫脸识别的线程,不停地从消息队列中读取图片,然后进行识别。识别出猫脸后,在图片中框出猫脸的位置,然后放到另外一个队列中。最后有个屏幕显示的线程,从队列中获取图片展示出来。
我这边就直接修改猫脸识别的线程,整个流程如图(使用Scheme-it网页绘制)
代码如下,修改了components/modules/ai/who_cat_face_detection.cpp文件:
// #include "freertos/semphr.h"
// #include "freertos/task.h"
#include "driver/uart.h"
#include "string.h"
#include "driver/gpio.h"
#include "who_cat_face_detection.hpp"
#include "esp_log.h"
#include "esp_camera.h"
#include "dl_image.hpp"
#include "cat_face_detect_mn03.hpp"
#include "who_ai_utils.hpp"
static const char *TAG = "cat_face_detection";
static QueueHandle_t xQueueFrameI = NULL;
static QueueHandle_t xQueueEvent = NULL;
static QueueHandle_t xQueueFrameO = NULL;
static QueueHandle_t xQueueResult = NULL;
static bool gEvent = true;
static bool gReturnFB = true;
static QueueHandle_t xQueueAIFrame = NULL;
static QueueHandle_t xQueueLCDFrame = NULL;
// ---------------------------------------------------------------------
SemaphoreHandle_t semphrHandle; // 定义一个信号量
bool catfeed = false;
#define TXD_PIN (GPIO_NUM_43)
#define RXD_PIN (GPIO_NUM_44)
#define UART_NUM UART_NUM_0
static const int CMD_BUF_SIZE = 10; // 舵机每命令字10字节,多个舵机 就用10的倍数
#define SPIFFS_PATH "/spiffs" // 机械臂命令文件
// 串口初始化
void uart_init(void)
{
const uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
// We won't use a buffer for sending data.
uart_driver_install(UART_NUM, 256, 0, 0, NULL, 0);
uart_param_config(UART_NUM, &uart_config);
uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
// 向串口发送机械臂移动命令,命令字由 spiffs文件获取
void sendCmdData(void)
{
char buf[CMD_BUF_SIZE];
uint8_t sec = 1;
FILE *fp = fopen(SPIFFS_PATH "/action.hts", "rb");
if (fp == NULL)
{
ESP_LOGE(TAG, "Fail to open file: %s", SPIFFS_PATH "/action.txt");
return;
}
// 读取文件
while (!feof(fp))
{
memset(buf, 0, sizeof(buf));
if (fread(buf, sizeof(char), sizeof(buf), fp) >= CMD_BUF_SIZE)
{
// for (int i = 0; i < CMD_BUF_SIZE; i++)
// {
// printf("%02x , ", buf[i]);
// }
// printf("\r\n");
sec++;
uart_write_bytes(UART_NUM, buf, CMD_BUF_SIZE); // 写入串口命令字
vTaskDelay(pdMS_TO_TICKS(50));
if (sec % 3 == 0)
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
fclose(fp);
}
// 喂猫的任务
void catFeedTask(void *pvParam)
{
uart_init();
while (1)
{
xSemaphoreTake(semphrHandle, portMAX_DELAY);
if (catfeed )
{
printf("Feed cat process is running! \n");
catfeed = false;
sendCmdData(); // 操纵舵机工作
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreGive(semphrHandle);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
static void task_process_handler(void *arg)
{
time_t curltime,lasttime= time(NULL);
camera_fb_t *frame = NULL;
CatFaceDetectMN03 detector(0.4F, 0.3F, 10, 0.3F);
while (true)
{
if (gEvent)
{
bool is_detected = false;
if (xQueueReceive(xQueueFrameI, &frame, portMAX_DELAY))
{
std::list<dl::detect::result_t> &detect_results = detector.infer((uint16_t *)frame->buf, {(int)frame->height, (int)frame->width, 3});
if (detect_results.size() > 0)
{
draw_detection_result((uint16_t *)frame->buf, frame->height, frame->width, detect_results);
print_detection_result(detect_results);
is_detected = true;
curltime = time(NULL);
xSemaphoreTake(semphrHandle, portMAX_DELAY); // 探测到猫咪,允许喂猫
if(curltime-lasttime>30){
lasttime= time(NULL);
catfeed = true;
}
xSemaphoreGive(semphrHandle);
}
}
if (xQueueFrameO)
{
xQueueSend(xQueueFrameO, &frame, portMAX_DELAY);
}
else if (gReturnFB)
{
esp_camera_fb_return(frame);
}
else
{
free(frame);
}
if (xQueueResult)
{
xQueueSend(xQueueResult, &is_detected, portMAX_DELAY);
}
}
}
}
static void task_event_handler(void *arg)
{
while (true)
{
xQueueReceive(xQueueEvent, &(gEvent), portMAX_DELAY);
}
}
void register_cat_face_detection(const QueueHandle_t frame_i,
const QueueHandle_t event,
const QueueHandle_t result,
const QueueHandle_t frame_o,
const bool camera_fb_return)
{
xQueueFrameI = frame_i;
xQueueFrameO = frame_o;
xQueueEvent = event;
xQueueResult = result;
gReturnFB = camera_fb_return;
semphrHandle = xSemaphoreCreateBinary();
xSemaphoreGive(semphrHandle); // 释放信号量
xTaskCreate(catFeedTask, "catFeedTask", 1024 * 2, NULL, 1, NULL);
xTaskCreatePinnedToCore(task_process_handler, "cat_face_process", 3 * 1024, NULL, 5, NULL, 1);
if (xQueueEvent)
xTaskCreatePinnedToCore(task_event_handler, "cat_face_event", 1 * 1024, NULL, 5, NULL, 1);
}
这个文件做了这几个地方的修改:
1:添加了串口功能。我使用的舵机是串口舵机,即需要使用串口来控制的舵机,所以这里启动了串口0,波特率115200,使用GPIO44作为串口输出。在系统启动后就初始化好串口。
2:添加了一个布尔值“catfeed”,用来在猫脸识别线程和舵机驱动线程两个线程间通讯,决定是否驱动舵机动作。使用二进制互斥量保障两个线程修改该变量的安全。
3:使用spiffs文件系统,用来保存舵机的命令字。舵机不同的动作有对应的命令字,使用文件系统就可以解耦舵机动作驱动。只需要预先制作好舵机的动作,保存成文件,需要是读取并发送到串口即可。
4:设置了个时钟限制,如果上次识别和这次识别间隔没有超过30秒,就不动作;只有超过了30秒钟,才会驱动舵机动作。
五、总结感悟
经历了重重波折,总算是把项目完成了。ESP32S3功能是真的强大,esp-idf也是真的复杂,整个项目基于已有的例程,却也遇到了重重困难,总算是逐一解决了。AI做猫脸识别速度挺快,识别率也挺不错的,不过当放入狗脸时,还是有蛮大几率误识别的,不得不说是个蛮大的缺憾。感谢电子森林举办的这次活动,让我完完整整地体验了一把在ESP32S3上AI开发之路。
代码以及舵机说明文件:
链接:https://pan.baidu.com/s/1xgYQeKIWc2gN14NMt6mlPQ
提取码:8888