Skip to content

Commit f141a6b

Browse files
Add board: ESP-Spot (#460)
* feat: add ESP-Spot S3 Co-authored-by: Tang Yumei <[email protected]> * 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 <[email protected]>
1 parent 4787eb4 commit f141a6b

File tree

5 files changed

+347
-0
lines changed

5 files changed

+347
-0
lines changed

main/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ elseif(CONFIG_BOARD_TYPE_ESP32S3_KORVO2_V3)
8888
set(BOARD_TYPE "esp32s3-korvo2-v3")
8989
elseif(CONFIG_BOARD_TYPE_ESP_SPARKBOT)
9090
set(BOARD_TYPE "esp-sparkbot")
91+
elseif(CONFIG_BOARD_TYPE_ESP_SPOT_S3)
92+
set(BOARD_TYPE "esp-spot-s3")
9193
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8)
9294
set(BOARD_TYPE "esp32-s3-touch-amoled-1.8")
9395
elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_LCD_1_85C)

main/Kconfig.projbuild

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ choice BOARD_TYPE
114114
bool "ESP32S3_KORVO2_V3开发板"
115115
config BOARD_TYPE_ESP_SPARKBOT
116116
bool "ESP-SparkBot开发板"
117+
config BOARD_TYPE_ESP_SPOT_S3
118+
bool "ESP-Spot-S3"
117119
config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8
118120
bool "Waveshare ESP32-S3-Touch-AMOLED-1.8"
119121
config BOARD_TYPE_ESP32S3_Touch_LCD_1_85C

main/boards/esp-spot-s3/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# ESP-Spot S3
2+
3+
## 简介
4+
5+
<div align="center">
6+
<a href="https://oshwhub.com/esp-college/esp-spot"><b> 立创开源平台 </b></a>
7+
|
8+
<a href="https://www.bilibili.com/video/BV1ekRAYVEZ1/"><b> Bilibili Demo </b></a>
9+
</div>
10+
11+
ESP-Spot 是 ESP Friends 开源的一款智能语音交互盒子,内置麦克风、扬声器、IMU 惯性传感器,可使用电池供电。ESP-Spot 不带屏幕,带有一个 RGB 指示灯和两个按钮。硬件详情可查看[立创开源项目](https://oshwhub.com/esp-college/esp-spot)
12+
13+
ESP-Spot 开源项目采用 ESP32-S3-WROOM-1-N16R8 模组。如在复刻时使用了其他大小的 Flash,需修改对应的参数。
14+
15+
16+
## 配置、编译命令
17+
18+
**配置编译目标为 ESP32S3**
19+
20+
```bash
21+
idf.py set-target esp32s3
22+
```
23+
24+
**打开 menuconfig 并配置**
25+
26+
```bash
27+
idf.py menuconfig
28+
```
29+
30+
分别配置如下选项:
31+
32+
- `Xiaozhi Assistant``Board Type` → 选择 `ESP-Spot-S3`
33+
- `Partition Table``Custom partition CSV file` → 输入 `partitions.csv`
34+
- `Serial flasher config``Flash size` → 选择 `16 MB`
35+
36+
`S` 保存,按 `Q` 退出。
37+
38+
**编译**
39+
40+
```bash
41+
idf.py build
42+
```
43+
44+
**烧录**
45+
46+
```bash
47+
idf.py flash
48+
```
49+
50+
> [!TIP]
51+
>
52+
> **若电脑始终无法找到 ESP-Spot 串口,可尝试如下步骤**
53+
> 1. 打开前盖;
54+
> 2. 拔出带有模组的 PCB 板;
55+
> 3. 按住 <kbd>BOOT</kbd> 同时插回 PCB 版,注意不要颠倒;
56+
>
57+
> 此时, ESP-Spot 应当已进入下载模式。在烧录完成后,可能需要重新插拔 PCB 板。

main/boards/esp-spot-s3/config.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#ifndef _BOARD_CONFIG_H_
2+
#define _BOARD_CONFIG_H_
3+
4+
#include <driver/gpio.h>
5+
6+
#define AUDIO_INPUT_SAMPLE_RATE 16000
7+
#define AUDIO_OUTPUT_SAMPLE_RATE 16000
8+
9+
#define AUDIO_INPUT_REFERENCE false
10+
11+
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_8
12+
#define AUDIO_I2S_GPIO_WS GPIO_NUM_17
13+
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_16
14+
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_15
15+
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_18
16+
17+
#define AUDIO_CODEC_PA_PIN GPIO_NUM_40
18+
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_2
19+
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1
20+
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
21+
22+
#define BOOT_BUTTON_GPIO GPIO_NUM_0
23+
#define KEY_BUTTON_GPIO GPIO_NUM_12
24+
#define LED_PIN GPIO_NUM_11
25+
26+
#define VBAT_ADC_CHANNEL ADC_CHANNEL_9 // S3: IO10
27+
#define MCU_VCC_CTL GPIO_NUM_4 // set 1 to power on MCU
28+
#define PERP_VCC_CTL GPIO_NUM_6 // set 1 to power on peripherals
29+
30+
#define ADC_ATTEN ADC_ATTEN_DB_12
31+
#define ADC_WIDTH ADC_BITWIDTH_DEFAULT
32+
#define FULL_BATTERY_VOLTAGE 4100
33+
#define EMPTY_BATTERY_VOLTAGE 3200
34+
35+
#endif // _BOARD_CONFIG_H_
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
#include "wifi_board.h"
2+
#include "audio_codecs/es8311_audio_codec.h"
3+
#include "application.h"
4+
#include "button.h"
5+
#include "config.h"
6+
#include "iot/thing_manager.h"
7+
#include "sdkconfig.h"
8+
9+
#include <wifi_station.h>
10+
#include <esp_log.h>
11+
#include <driver/i2c_master.h>
12+
#include <driver/spi_common.h>
13+
#include "esp_adc/adc_oneshot.h"
14+
#include "esp_adc/adc_cali.h"
15+
#include "esp_adc/adc_cali_scheme.h"
16+
17+
#include <driver/gpio.h>
18+
#include "esp_timer.h"
19+
#include "led/circular_strip.h"
20+
21+
#define TAG "esp_spot_s3"
22+
23+
bool button_released_ = false;
24+
bool shutdown_ready_ = false;
25+
esp_timer_handle_t shutdown_timer;
26+
27+
class EspSpotS3Bot : public WifiBoard {
28+
private:
29+
i2c_master_bus_handle_t i2c_bus_;
30+
Button boot_button_;
31+
Button key_button_;
32+
adc_oneshot_unit_handle_t adc1_handle;
33+
adc_cali_handle_t adc1_cali_handle;
34+
bool do_calibration = false;
35+
bool key_long_pressed = false;
36+
int64_t last_key_press_time = 0;
37+
static const int64_t LONG_PRESS_TIMEOUT_US = 5 * 1000000ULL;
38+
39+
void InitializeI2c() {
40+
// Initialize I2C peripheral
41+
i2c_master_bus_config_t i2c_bus_cfg = {
42+
.i2c_port = I2C_NUM_0,
43+
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
44+
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
45+
.clk_source = I2C_CLK_SRC_DEFAULT,
46+
.glitch_ignore_cnt = 7,
47+
.intr_priority = 0,
48+
.trans_queue_depth = 0,
49+
.flags = {
50+
.enable_internal_pullup = 1,
51+
},
52+
};
53+
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_));
54+
}
55+
56+
void InitializeADC() {
57+
adc_oneshot_unit_init_cfg_t init_config1 = {
58+
.unit_id = ADC_UNIT_1
59+
};
60+
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &adc1_handle));
61+
62+
adc_oneshot_chan_cfg_t chan_config = {
63+
.atten = ADC_ATTEN,
64+
.bitwidth = ADC_WIDTH,
65+
};
66+
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, VBAT_ADC_CHANNEL, &chan_config));
67+
68+
adc_cali_handle_t handle = NULL;
69+
esp_err_t ret = ESP_FAIL;
70+
71+
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
72+
adc_cali_curve_fitting_config_t cali_config = {
73+
.unit_id = ADC_UNIT_1,
74+
.atten = ADC_ATTEN,
75+
.bitwidth = ADC_WIDTH,
76+
};
77+
ret = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
78+
if (ret == ESP_OK) {
79+
do_calibration = true;
80+
adc1_cali_handle = handle;
81+
ESP_LOGI(TAG, "ADC Curve Fitting calibration succeeded");
82+
}
83+
#endif // ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
84+
}
85+
86+
void InitializeButtons() {
87+
boot_button_.OnClick([this]() {
88+
auto& app = Application::GetInstance();
89+
ResetWifiConfiguration();
90+
});
91+
92+
key_button_.OnClick([this]() {
93+
auto& app = Application::GetInstance();
94+
app.ToggleChatState();
95+
key_long_pressed = false;
96+
});
97+
98+
key_button_.OnLongPress([this]() {
99+
int64_t now = esp_timer_get_time();
100+
auto* led = static_cast<CircularStrip*>(this->GetLed());
101+
102+
if (key_long_pressed) {
103+
if ((now - last_key_press_time) < LONG_PRESS_TIMEOUT_US) {
104+
ESP_LOGW(TAG, "Key button long pressed the second time within 5s, shutting down...");
105+
led->SetSingleColor(0, {0, 0, 0});
106+
107+
gpio_hold_dis(MCU_VCC_CTL);
108+
gpio_set_level(MCU_VCC_CTL, 0);
109+
110+
} else {
111+
last_key_press_time = now;
112+
BlinkGreenFor5s();
113+
}
114+
key_long_pressed = true;
115+
} else {
116+
ESP_LOGW(TAG, "Key button first long press! Waiting second within 5s to shutdown...");
117+
last_key_press_time = now;
118+
key_long_pressed = true;
119+
120+
BlinkGreenFor5s();
121+
}
122+
});
123+
}
124+
125+
void InitializePowerCtl() {
126+
InitializeGPIO();
127+
128+
gpio_set_level(MCU_VCC_CTL, 1);
129+
gpio_hold_en(MCU_VCC_CTL);
130+
131+
gpio_set_level(PERP_VCC_CTL, 1);
132+
gpio_hold_en(PERP_VCC_CTL);
133+
}
134+
135+
void InitializeGPIO() {
136+
gpio_config_t io_pa = {
137+
.pin_bit_mask = (1ULL << AUDIO_CODEC_PA_PIN),
138+
.mode = GPIO_MODE_OUTPUT,
139+
.pull_up_en = GPIO_PULLUP_DISABLE,
140+
.pull_down_en = GPIO_PULLDOWN_DISABLE,
141+
.intr_type = GPIO_INTR_DISABLE
142+
};
143+
gpio_config(&io_pa);
144+
gpio_set_level(AUDIO_CODEC_PA_PIN, 0);
145+
146+
gpio_config_t io_conf_1 = {
147+
.pin_bit_mask = (1ULL << MCU_VCC_CTL),
148+
.mode = GPIO_MODE_OUTPUT,
149+
.pull_up_en = GPIO_PULLUP_DISABLE,
150+
.pull_down_en = GPIO_PULLDOWN_DISABLE,
151+
.intr_type = GPIO_INTR_DISABLE
152+
};
153+
gpio_config(&io_conf_1);
154+
155+
gpio_config_t io_conf_2 = {
156+
.pin_bit_mask = (1ULL << PERP_VCC_CTL),
157+
.mode = GPIO_MODE_OUTPUT,
158+
.pull_up_en = GPIO_PULLUP_DISABLE,
159+
.pull_down_en = GPIO_PULLDOWN_DISABLE,
160+
.intr_type = GPIO_INTR_DISABLE
161+
};
162+
gpio_config(&io_conf_2);
163+
}
164+
165+
void InitializeIot() {
166+
auto& thing_manager = iot::ThingManager::GetInstance();
167+
thing_manager.AddThing(iot::CreateThing("Speaker"));
168+
thing_manager.AddThing(iot::CreateThing("Battery"));
169+
}
170+
171+
172+
void BlinkGreenFor5s() {
173+
auto* led = static_cast<CircularStrip*>(GetLed());
174+
if (!led) {
175+
return;
176+
}
177+
178+
led->Blink({50, 25, 0}, 100);
179+
180+
esp_timer_create_args_t timer_args = {
181+
.callback = [](void* arg) {
182+
auto* self = static_cast<EspSpotS3Bot*>(arg);
183+
auto* led = static_cast<CircularStrip*>(self->GetLed());
184+
if (led) {
185+
led->SetSingleColor(0, {0, 0, 0});
186+
}
187+
},
188+
.arg = this,
189+
.dispatch_method = ESP_TIMER_TASK,
190+
.name = "blinkGreenFor5s_timer"
191+
};
192+
193+
esp_timer_handle_t blink_timer = nullptr;
194+
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &blink_timer));
195+
ESP_ERROR_CHECK(esp_timer_start_once(blink_timer, LONG_PRESS_TIMEOUT_US));
196+
}
197+
198+
public:
199+
EspSpotS3Bot() : boot_button_(BOOT_BUTTON_GPIO), key_button_(KEY_BUTTON_GPIO, true) {
200+
InitializePowerCtl();
201+
InitializeADC();
202+
InitializeI2c();
203+
InitializeButtons();
204+
InitializeIot();
205+
}
206+
207+
virtual Led* GetLed() override {
208+
static CircularStrip led(LED_PIN, 1);
209+
return &led;
210+
}
211+
212+
virtual AudioCodec* GetAudioCodec() override {
213+
static Es8311AudioCodec audio_codec(i2c_bus_, I2C_NUM_0,
214+
AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK,
215+
AUDIO_I2S_GPIO_WS, AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN, AUDIO_CODEC_PA_PIN,
216+
AUDIO_CODEC_ES8311_ADDR, false);
217+
return &audio_codec;
218+
}
219+
220+
virtual bool GetBatteryLevel(int &level, bool &charging, bool &discharging) {
221+
if (!adc1_handle) {
222+
InitializeADC();
223+
}
224+
225+
int raw_value = 0;
226+
int voltage = 0;
227+
228+
ESP_ERROR_CHECK(adc_oneshot_read(adc1_handle, VBAT_ADC_CHANNEL, &raw_value));
229+
230+
if (do_calibration) {
231+
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc1_cali_handle, raw_value, &voltage));
232+
voltage = voltage * 3 / 2; // compensate for voltage divider
233+
ESP_LOGI(TAG, "Calibrated voltage: %d mV", voltage);
234+
} else {
235+
ESP_LOGI(TAG, "Raw ADC value: %d", raw_value);
236+
voltage = raw_value;
237+
}
238+
239+
voltage = voltage < EMPTY_BATTERY_VOLTAGE ? EMPTY_BATTERY_VOLTAGE : voltage;
240+
voltage = voltage > FULL_BATTERY_VOLTAGE ? FULL_BATTERY_VOLTAGE : voltage;
241+
242+
// 计算电量百分比
243+
level = (voltage - EMPTY_BATTERY_VOLTAGE) * 100 / (FULL_BATTERY_VOLTAGE - EMPTY_BATTERY_VOLTAGE);
244+
245+
charging = gpio_get_level(MCU_VCC_CTL);
246+
ESP_LOGI(TAG, "Battery Level: %d%%, Charging: %s", level, charging ? "Yes" : "No");
247+
return true;
248+
}
249+
};
250+
251+
DECLARE_BOARD(EspSpotS3Bot);

0 commit comments

Comments
 (0)