feat:添加绿荫搭子小智盒子支持

This commit is contained in:
Gil Zhang 2025-12-29 21:54:24 +08:00
parent 213117ded2
commit 373783dc63
8 changed files with 543 additions and 2 deletions

View File

@ -121,6 +121,11 @@ elseif(CONFIG_BOARD_TYPE_KEVIN_YUYING_313LCD)
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_30_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_VERDURE_ASSISTANT)
set(BOARD_TYPE "verdure-assistant")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
elseif(CONFIG_BOARD_TYPE_LICHUANG_DEV_S3)
set(BOARD_TYPE "lichuang-dev")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)

View File

@ -195,6 +195,9 @@ choice BOARD_TYPE
config BOARD_TYPE_CGC_144
bool "CGC 144"
depends on IDF_TARGET_ESP32
config BOARD_TYPE_VERDURE_ASSISTANT
bool "绿荫搭子 Verdure Assistant"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_LICHUANG_DEV_S3
bool "立创·实战派 ESP32-S3"
depends on IDF_TARGET_ESP32S3
@ -582,7 +585,7 @@ choice DISPLAY_STYLE
config USE_EMOTE_MESSAGE_STYLE
bool "Emote animation style"
depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR || BOARD_TYPE_LICHUANG_DEV_S3
depends on BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ECHOEAR || BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_VERDURE_ASSISTANT
endchoice
choice WAKE_WORD_TYPE
@ -657,7 +660,7 @@ config USE_DEVICE_AEC
bool "Enable Device-Side AEC"
default n
depends on USE_AUDIO_PROCESSOR && (BOARD_TYPE_ESP_BOX_3 || BOARD_TYPE_ESP_BOX || BOARD_TYPE_ESP_BOX_LITE \
|| BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_75 || BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_1_83\
|| BOARD_TYPE_LICHUANG_DEV_S3 || BOARD_TYPE_VERDURE_ASSISTANT || BOARD_TYPE_ESP_KORVO2_V3 || BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_1_75 || BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_1_83\
|| BOARD_TYPE_WAVESHARE_S3_TOUCH_AMOLED_2_06 || BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_4B || BOARD_TYPE_WAVESHARE_P4_WIFI6_TOUCH_LCD_4B || BOARD_TYPE_WAVESHARE_P4_WIFI6_TOUCH_LCD_7B \
|| BOARD_TYPE_WAVESHARE_P4_WIFI6_TOUCH_LCD_XC || BOARD_TYPE_ESP_S3_LCD_EV_Board_2 || BOARD_TYPE_YUNLIAO_S3 \
|| BOARD_TYPE_ECHOEAR || BOARD_TYPE_WAVESHARE_S3_TOUCH_LCD_3_49)

View File

@ -0,0 +1,96 @@
# 绿荫搭子 (Verdure Assistant)
## 简介
绿荫搭子是一款基于 ESP32-S3 的智能语音助手开发板,基于立创·实战派 ESP32-S3 开发板代码修改而来。
## 项目地址
- 开源硬件平台: [https://oshwhub.com/greenshade/verdure-assistant](https://oshwhub.com/greenshade/verdure-assistant)
## 硬件特性
- **主控芯片**: ESP32-S3
- **显示屏**: 2.4寸 ST7789 LCD (320x240)
- **触摸屏**: FT5x06 电容触摸
- **音频编解码**: ES8311 + ES7210
- **摄像头**: GC0308 (可选)
- **扩展芯片**: PCA9557 GPIO 扩展器
## 引脚配置
### 音频 I2S 引脚
| 功能 | GPIO |
|------|------|
| MCLK | GPIO38 |
| BCLK | GPIO14 |
| WS | GPIO13 |
| DIN | GPIO12 |
| DOUT | GPIO45 |
### I2C 引脚
| 功能 | GPIO |
|------|------|
| SDA | GPIO1 |
| SCL | GPIO2 |
### 显示屏 SPI 引脚
| 功能 | GPIO |
|------|------|
| MOSI | GPIO40 |
| SCLK | GPIO41 |
| DC | GPIO39 |
| 背光 | GPIO42 |
### 其他引脚
| 功能 | GPIO |
|------|------|
| BOOT 按钮 | GPIO0 |
| LED | GPIO48 |
## 编译方法
### 方法一:使用 release.py 脚本(推荐)
```bash
python scripts/release.py verdure-assistant
```
### 方法二:使用 idf.py 手动配置
1. 设置目标芯片:
```bash
idf.py set-target esp32s3
```
2. 配置开发板类型:
```bash
idf.py menuconfig
```
导航到: `Xiaozhi Assistant` -> `Board Type` -> 选择 `绿荫搭子 Verdure Assistant`
3. 编译和烧录:
```bash
idf.py build
idf.py flash monitor
```
## 功能支持
- ✅ Wi-Fi 连接
- ✅ 语音交互
- ✅ LCD 显示
- ✅ 触摸屏输入
- ✅ 摄像头 (可选)
- ✅ 设备端 AEC (回声消除)
- ✅ Emote 动画表情
## 特殊说明
- 本开发板使用 PCA9557 GPIO 扩展器控制功放使能和摄像头电源
- 支持设备端 AEC (Acoustic Echo Cancellation)
- 默认启用 GC0308 摄像头驱动
## 致谢
感谢立创开源硬件平台和实战派开发板提供的硬件基础。

View File

@ -0,0 +1,62 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_INPUT_REFERENCE true
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_38
#define AUDIO_I2S_GPIO_WS GPIO_NUM_13
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_14
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_12
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_45
#define AUDIO_CODEC_USE_PCA9557
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_1
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_2
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define AUDIO_CODEC_ES7210_ADDR 0x82
#define BUILTIN_LED_GPIO GPIO_NUM_48
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY true
#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_42
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
/* Camera pins */
#define CAMERA_PIN_PWDN GPIO_NUM_NC
#define CAMERA_PIN_RESET GPIO_NUM_NC
#define CAMERA_PIN_XCLK GPIO_NUM_5
#define CAMERA_PIN_SIOD GPIO_NUM_1
#define CAMERA_PIN_SIOC GPIO_NUM_2
#define CAMERA_PIN_D7 GPIO_NUM_9
#define CAMERA_PIN_D6 GPIO_NUM_4
#define CAMERA_PIN_D5 GPIO_NUM_6
#define CAMERA_PIN_D4 GPIO_NUM_15
#define CAMERA_PIN_D3 GPIO_NUM_17
#define CAMERA_PIN_D2 GPIO_NUM_8
#define CAMERA_PIN_D1 GPIO_NUM_18
#define CAMERA_PIN_D0 GPIO_NUM_16
#define CAMERA_PIN_VSYNC GPIO_NUM_3
#define CAMERA_PIN_HREF GPIO_NUM_46
#define CAMERA_PIN_PCLK GPIO_NUM_7
#define XCLK_FREQ_HZ 20000000
#endif // _BOARD_CONFIG_H_

View File

@ -0,0 +1,14 @@
{
"target": "esp32s3",
"builds": [
{
"name": "verdure-assistant",
"sdkconfig_append": [
"CONFIG_USE_DEVICE_AEC=y",
"CONFIG_CAMERA_GC0308=y",
"CONFIG_CAMERA_GC0308_AUTO_DETECT_DVP_INTERFACE_SENSOR=y",
"CONFIG_CAMERA_GC0308_DVP_YUV422_640X480_16FPS=y"
]
}
]
}

View File

@ -0,0 +1,22 @@
[
{"emote": "happy", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "laughing", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "funny", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "loving", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "embarrassed", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "confident", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "delicious", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "sad", "src": "Sad.eaf", "loop": true, "fps": 20},
{"emote": "crying", "src": "cry.eaf", "loop": true, "fps": 20},
{"emote": "sleepy", "src": "sleep.eaf", "loop": true, "fps": 20},
{"emote": "silly", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "angry", "src": "angry.eaf", "loop": true, "fps": 20},
{"emote": "surprised", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "shocked", "src": "shocked.eaf", "loop": true, "fps": 20},
{"emote": "thinking", "src": "confused.eaf", "loop": true, "fps": 20},
{"emote": "winking", "src": "neutral.eaf", "loop": true, "fps": 20},
{"emote": "relaxed", "src": "Happy.eaf", "loop": true, "fps": 20},
{"emote": "confused", "src": "confused.eaf", "loop": true, "fps": 20},
{"emote": "neutral", "src": "winking.eaf", "loop": false, "fps": 20},
{"emote": "idle", "src": "neutral.eaf", "loop": false, "fps": 20}
]

View File

@ -0,0 +1,36 @@
[
{
"name": "eye_anim",
"align": "GFX_ALIGN_LEFT_MID",
"x": 10,
"y": 30
},
{
"name": "status_icon",
"align": "GFX_ALIGN_TOP_MID",
"x": -120,
"y": 18
},
{
"name": "toast_label",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 20,
"width": 200,
"height": 40
},
{
"name": "clock_label",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 20,
"width": 200,
"height": 50
},
{
"name": "listen_anim",
"align": "GFX_ALIGN_TOP_MID",
"x": 0,
"y": 5
}
]

View File

@ -0,0 +1,303 @@
/**
* @file verdure_assistant_board.cc
* @brief 绿 (Verdure Assistant)
* @see https://oshwhub.com/greenshade/verdure-assistant
*
* · ESP32-S3
*/
#include "wifi_board.h"
#include "codecs/box_audio_codec.h"
#include "display/lcd_display.h"
#include "display/emote_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "i2c_device.h"
#include "esp32_camera.h"
#include "mcp_server.h"
#include <esp_log.h>
#include <esp_lcd_panel_vendor.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include <esp_lcd_touch_ft5x06.h>
#include <esp_lvgl_port.h>
#include <lvgl.h>
#define TAG "VerdureAssistantBoard"
class Pca9557 : public I2cDevice {
public:
Pca9557(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) {
WriteReg(0x01, 0x03);
WriteReg(0x03, 0xf8);
}
void SetOutputState(uint8_t bit, uint8_t level) {
uint8_t data = ReadReg(0x01);
data = (data & ~(1 << bit)) | (level << bit);
WriteReg(0x01, data);
}
};
class CustomAudioCodec : public BoxAudioCodec {
private:
Pca9557* pca9557_;
public:
CustomAudioCodec(i2c_master_bus_handle_t i2c_bus, Pca9557* pca9557)
: BoxAudioCodec(i2c_bus,
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN,
GPIO_NUM_NC,
AUDIO_CODEC_ES8311_ADDR,
AUDIO_CODEC_ES7210_ADDR,
AUDIO_INPUT_REFERENCE),
pca9557_(pca9557) {
}
virtual void EnableOutput(bool enable) override {
BoxAudioCodec::EnableOutput(enable);
if (enable) {
pca9557_->SetOutputState(1, 1);
} else {
pca9557_->SetOutputState(1, 0);
}
}
};
class VerdureAssistantBoard : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
i2c_master_dev_handle_t pca9557_handle_;
Button boot_button_;
Display* display_;
Pca9557* pca9557_;
Esp32Camera* camera_;
void InitializeI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = (i2c_port_t)1,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
// Initialize PCA9557
pca9557_ = new Pca9557(i2c_bus_, 0x19);
}
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = GPIO_NUM_40;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = GPIO_NUM_41;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
// During startup (before connected), pressing BOOT button enters Wi-Fi config mode without reboot
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
#if CONFIG_USE_DEVICE_AEC
boot_button_.OnDoubleClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateIdle) {
app.SetAecMode(app.GetAecMode() == kAecOff ? kAecOnDeviceSide : kAecOff);
}
});
#endif
}
void InitializeSt7789Display() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
// 液晶屏控制IO初始化
ESP_LOGD(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = GPIO_NUM_NC;
io_config.dc_gpio_num = GPIO_NUM_39;
io_config.spi_mode = 2;
io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI3_HOST, &io_config, &panel_io));
// 初始化液晶屏驱动芯片ST7789
ESP_LOGD(TAG, "Install LCD driver");
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
pca9557_->SetOutputState(0, 0);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
esp_lcd_panel_disp_on_off(panel, true);
#if CONFIG_USE_EMOTE_MESSAGE_STYLE
display_ = new emote::EmoteDisplay(panel, panel_io, DISPLAY_WIDTH, DISPLAY_HEIGHT);
#else
display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
#endif
}
void InitializeTouch()
{
esp_lcd_touch_handle_t tp;
esp_lcd_touch_config_t tp_cfg = {
.x_max = DISPLAY_HEIGHT,
.y_max = DISPLAY_WIDTH,
.rst_gpio_num = GPIO_NUM_NC, // Shared with LCD reset
.int_gpio_num = GPIO_NUM_NC,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = 1,
.mirror_x = 1,
.mirror_y = 0,
},
};
esp_lcd_panel_io_handle_t tp_io_handle = NULL;
esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_FT5x06_CONFIG();
tp_io_config.scl_speed_hz = 400000;
esp_lcd_new_panel_io_i2c(i2c_bus_, &tp_io_config, &tp_io_handle);
esp_lcd_touch_new_i2c_ft5x06(tp_io_handle, &tp_cfg, &tp);
assert(tp);
/* Add touch input (for selected screen) */
const lvgl_port_touch_cfg_t touch_cfg = {
.disp = lv_display_get_default(),
.handle = tp,
};
if(touch_cfg.disp) {
lvgl_port_add_touch(&touch_cfg);
} else {
ESP_LOGE(TAG, "Touch display is not initialized");
}
}
void InitializeCamera() {
// Open camera power
pca9557_->SetOutputState(2, 0);
static esp_cam_ctlr_dvp_pin_config_t dvp_pin_config = {
.data_width = CAM_CTLR_DATA_WIDTH_8,
.data_io = {
[0] = CAMERA_PIN_D0,
[1] = CAMERA_PIN_D1,
[2] = CAMERA_PIN_D2,
[3] = CAMERA_PIN_D3,
[4] = CAMERA_PIN_D4,
[5] = CAMERA_PIN_D5,
[6] = CAMERA_PIN_D6,
[7] = CAMERA_PIN_D7,
},
.vsync_io = CAMERA_PIN_VSYNC,
.de_io = CAMERA_PIN_HREF,
.pclk_io = CAMERA_PIN_PCLK,
.xclk_io = CAMERA_PIN_XCLK,
};
esp_video_init_sccb_config_t sccb_config = {
.init_sccb = false,
.i2c_handle = i2c_bus_,
.freq = 100000,
};
esp_video_init_dvp_config_t dvp_config = {
.sccb_config = sccb_config,
.reset_pin = CAMERA_PIN_RESET,
.pwdn_pin = CAMERA_PIN_PWDN,
.dvp_pin = dvp_pin_config,
.xclk_freq = XCLK_FREQ_HZ,
};
esp_video_init_config_t video_config = {
.dvp = &dvp_config,
};
camera_ = new Esp32Camera(video_config);
}
void InitializeTools() {
auto &mcp_server = McpServer::GetInstance();
mcp_server.AddTool("self.system.reconfigure_wifi",
"End this conversation and enter WiFi configuration mode.\n"
"**CAUTION** You must ask the user to confirm this action.",
PropertyList(), [this](const PropertyList& properties) {
EnterWifiConfigMode();
return true;
});
}
public:
VerdureAssistantBoard() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c();
InitializeSpi();
InitializeSt7789Display();
InitializeTouch();
InitializeButtons();
InitializeCamera();
InitializeTools();
GetBacklight()->RestoreBrightness();
}
virtual AudioCodec* GetAudioCodec() override {
static CustomAudioCodec audio_codec(
i2c_bus_,
pca9557_);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
virtual Camera* GetCamera() override {
return camera_;
}
};
DECLARE_BOARD(VerdureAssistantBoard);