Add board: ESP-Spot (#460)

* feat: add ESP-Spot S3

Co-authored-by: Tang Yumei <tangyumei@espressif.com>

* feat: add button and power control

long press KEY to shut down.

* feat: change for s3 adc

* fix: adc cali fail

* add ESP-Spot S3 Readme

* fix ES8311 config

---------

Co-authored-by: Liu Ruichao <liuruichao@espressif.com>
This commit is contained in:
tangyumei3535
2025-04-13 06:45:05 +08:00
committed by GitHub
parent 4787eb41ab
commit f141a6bf32
5 changed files with 347 additions and 0 deletions

View File

@@ -88,6 +88,8 @@ elseif(CONFIG_BOARD_TYPE_ESP32S3_KORVO2_V3)
set(BOARD_TYPE "esp32s3-korvo2-v3") set(BOARD_TYPE "esp32s3-korvo2-v3")
elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT) elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT)
set(BOARD_TYPE "esp-sparkbot") set(BOARD_TYPE "esp-sparkbot")
elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3)
set(BOARD_TYPE "esp-spot-s3")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8) elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8)
set(BOARD_TYPE "esp32-s3-touch-amoled-1.8") set(BOARD_TYPE "esp32-s3-touch-amoled-1.8")
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85C) elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85C)

View File

@@ -114,6 +114,8 @@ choice BOARD_TYPE
bool "ESP32S3_KORVO2_V3开发板" bool "ESP32S3_KORVO2_V3开发板"
config BOARD_TYPE_ESP_SPARKBOT config BOARD_TYPE_ESP_SPARKBOT
bool "ESP-SparkBot开发板" bool "ESP-SparkBot开发板"
config BOARD_TYPE_ESP_SPOT_S3
bool "ESP-Spot-S3"
config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8 config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8
bool "Waveshare ESP32-S3-Touch-AMOLED-1.8" bool "Waveshare ESP32-S3-Touch-AMOLED-1.8"
config BOARD_TYPE_ESP32S3_Touch_LCD_1_85C config BOARD_TYPE_ESP32S3_Touch_LCD_1_85C

View File

@@ -0,0 +1,57 @@
# ESP-Spot S3
## 简介
<div align="center">
<a href="https://oshwhub.com/esp-college/esp-spot"><b> 立创开源平台 </b></a>
|
<a href="https://www.bilibili.com/video/BV1ekRAYVEZ1/"><b> Bilibili Demo </b></a>
</div>
ESP-Spot 是 ESP Friends 开源的一款智能语音交互盒子内置麦克风、扬声器、IMU 惯性传感器可使用电池供电。ESP-Spot 不带屏幕,带有一个 RGB 指示灯和两个按钮。硬件详情可查看[立创开源项目](https://oshwhub.com/esp-college/esp-spot)。
ESP-Spot 开源项目采用 ESP32-S3-WROOM-1-N16R8 模组。如在复刻时使用了其他大小的 Flash需修改对应的参数。
## 配置、编译命令
**配置编译目标为 ESP32S3**
```bash
idf.py set-target esp32s3
```
**打开 menuconfig 并配置**
```bash
idf.py menuconfig
```
分别配置如下选项:
- `Xiaozhi Assistant``Board Type` → 选择 `ESP-Spot-S3`
- `Partition Table``Custom partition CSV file` → 输入 `partitions.csv`
- `Serial flasher config``Flash size` → 选择 `16 MB`
`S` 保存,按 `Q` 退出。
**编译**
```bash
idf.py build
```
**烧录**
```bash
idf.py flash
```
> [!TIP]
>
> **若电脑始终无法找到 ESP-Spot 串口,可尝试如下步骤**
> 1. 打开前盖;
> 2. 拔出带有模组的 PCB 板;
> 3. 按住 <kbd>BOOT</kbd> 同时插回 PCB 版,注意不要颠倒;
>
> 此时, ESP-Spot 应当已进入下载模式。在烧录完成后,可能需要重新插拔 PCB 板。

View File

@@ -0,0 +1,35 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 16000
#define AUDIO_INPUT_REFERENCE false
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_8
#define AUDIO_I2S_GPIO_WS GPIO_NUM_17
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_16
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_15
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_18
#define AUDIO_CODEC_PA_PIN GPIO_NUM_40
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_2
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define KEY_BUTTON_GPIO GPIO_NUM_12
#define LED_PIN GPIO_NUM_11
#define VBAT_ADC_CHANNEL ADC_CHANNEL_9 // S3: IO10
#define MCU_VCC_CTL GPIO_NUM_4 // set 1 to power on MCU
#define PERP_VCC_CTL GPIO_NUM_6 // set 1 to power on peripherals
#define ADC_ATTEN ADC_ATTEN_DB_12
#define ADC_WIDTH ADC_BITWIDTH_DEFAULT
#define FULL_BATTERY_VOLTAGE 4100
#define EMPTY_BATTERY_VOLTAGE 3200
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,251 @@
#include "wifi_board.h"
#include "audio_codecs/es8311_audio_codec.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "iot/thing_manager.h"
#include "sdkconfig.h"
#include <wifi_station.h>
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include <driver/gpio.h>
#include "esp_timer.h"
#include "led/circular_strip.h"
#define TAG "esp_spot_s3"
bool button_released_ = false;
bool shutdown_ready_ = false;
esp_timer_handle_t shutdown_timer;
class EspSpotS3Bot : public WifiBoard {
private:
i2c_master_bus_handle_t i2c_bus_;
Button boot_button_;
Button key_button_;
adc_oneshot_unit_handle_t adc1_handle;
adc_cali_handle_t adc1_cali_handle;
bool do_calibration = false;
bool key_long_pressed = false;
int64_t last_key_press_time = 0;
static const int64_t LONG_PRESS_TIMEOUT_US = 5 * 1000000ULL;
void InitializeI2c() {
// Initialize I2C peripheral
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = I2C_NUM_0,
.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_));
}
void InitializeADC() {
adc_oneshot_unit_init_cfg_t init_config1 = {
.unit_id = ADC_UNIT_1
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));
adc_oneshot_chan_cfg_t chan_config = {
.atten = ADC_ATTEN,
.bitwidth = ADC_WIDTH,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, VBAT_ADC_CHANNEL, &chan_config));
adc_cali_handle_t handle = NULL;
esp_err_t ret = ESP_FAIL;
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN,
.bitwidth = ADC_WIDTH,
};
ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
if (ret == ESP_OK) {
do_calibration = true;
adc1_cali_handle = handle;
ESP_LOGI(TAG, "ADC Curve Fitting calibration succeeded");
}
#endif // ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
ResetWifiConfiguration();
});
key_button_.OnClick([this]() {
auto& app = Application::GetInstance();
app.ToggleChatState();
key_long_pressed = false;
});
key_button_.OnLongPress([this]() {
int64_t now = esp_timer_get_time();
auto* led = static_cast<CircularStrip*>(this->GetLed());
if (key_long_pressed) {
if ((now - last_key_press_time) < LONG_PRESS_TIMEOUT_US) {
ESP_LOGW(TAG, "Key button long pressed the second time within 5s, shutting down...");
led->SetSingleColor(0, {0, 0, 0});
gpio_hold_dis(MCU_VCC_CTL);
gpio_set_level(MCU_VCC_CTL, 0);
} else {
last_key_press_time = now;
BlinkGreenFor5s();
}
key_long_pressed = true;
} else {
ESP_LOGW(TAG, "Key button first long press! Waiting second within 5s to shutdown...");
last_key_press_time = now;
key_long_pressed = true;
BlinkGreenFor5s();
}
});
}
void InitializePowerCtl() {
InitializeGPIO();
gpio_set_level(MCU_VCC_CTL, 1);
gpio_hold_en(MCU_VCC_CTL);
gpio_set_level(PERP_VCC_CTL, 1);
gpio_hold_en(PERP_VCC_CTL);
}
void InitializeGPIO() {
gpio_config_t io_pa = {
.pin_bit_mask = (1ULL << AUDIO_CODEC_PA_PIN),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_pa);
gpio_set_level(AUDIO_CODEC_PA_PIN, 0);
gpio_config_t io_conf_1 = {
.pin_bit_mask = (1ULL << MCU_VCC_CTL),
.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_1);
gpio_config_t io_conf_2 = {
.pin_bit_mask = (1ULL << PERP_VCC_CTL),
.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_2);
}
void InitializeIot() {
auto& thing_manager = iot::ThingManager::GetInstance();
thing_manager.AddThing(iot::CreateThing("Speaker"));
thing_manager.AddThing(iot::CreateThing("Battery"));
}
void BlinkGreenFor5s() {
auto* led = static_cast<CircularStrip*>(GetLed());
if (!led) {
return;
}
led->Blink({50, 25, 0}, 100);
esp_timer_create_args_t timer_args = {
.callback = [](void* arg) {
auto* self = static_cast<EspSpotS3Bot*>(arg);
auto* led = static_cast<CircularStrip*>(self->GetLed());
if (led) {
led->SetSingleColor(0, {0, 0, 0});
}
},
.arg = this,
.dispatch_method = ESP_TIMER_TASK,
.name = "blinkGreenFor5s_timer"
};
esp_timer_handle_t blink_timer = nullptr;
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &blink_timer));
ESP_ERROR_CHECK(esp_timer_start_once(blink_timer, LONG_PRESS_TIMEOUT_US));
}
public:
EspSpotS3Bot() : boot_button_(BOOT_BUTTON_GPIO), key_button_(KEY_BUTTON_GPIO, true) {
InitializePowerCtl();
InitializeADC();
InitializeI2c();
InitializeButtons();
InitializeIot();
}
virtual Led* GetLed() override {
static CircularStrip led(LED_PIN, 1);
return &led;
}
virtual AudioCodec* GetAudioCodec() override {
static Es8311AudioCodec audio_codec(i2c_bus_, I2C_NUM_0,
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, AUDIO_CODEC_PA_PIN,
AUDIO_CODEC_ES8311_ADDR, false);
return &audio_codec;
}
virtual bool GetBatteryLevel(int &level, bool &charging, bool &discharging) {
if (!adc1_handle) {
InitializeADC();
}
int raw_value = 0;
int voltage = 0;
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, VBAT_ADC_CHANNEL, &raw_value));
if (do_calibration) {
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle, raw_value, &voltage));
voltage = voltage * 3 / 2; // compensate for voltage divider
ESP_LOGI(TAG, "Calibrated voltage: %d mV", voltage);
} else {
ESP_LOGI(TAG, "Raw ADC value: %d", raw_value);
voltage = raw_value;
}
voltage = voltage < EMPTY_BATTERY_VOLTAGE ? EMPTY_BATTERY_VOLTAGE : voltage;
voltage = voltage > FULL_BATTERY_VOLTAGE ? FULL_BATTERY_VOLTAGE : voltage;
// 计算电量百分比
level = (voltage - EMPTY_BATTERY_VOLTAGE) * 100 / (FULL_BATTERY_VOLTAGE - EMPTY_BATTERY_VOLTAGE);
charging = gpio_get_level(MCU_VCC_CTL);
ESP_LOGI(TAG, "Battery Level: %d%%, Charging: %s", level, charging ? "Yes" : "No");
return true;
}
};
DECLARE_BOARD(EspSpotS3Bot);