Add M5Stack StickS3 board support. (#2060)

Co-authored-by: luoweiyuan <luoweiyuan@m5stack.com>
This commit is contained in:
Create123
2026-06-16 01:05:21 +08:00
committed by GitHub
parent 3f4a275aec
commit b392c630aa
7 changed files with 469 additions and 0 deletions

View File

@@ -206,6 +206,11 @@ elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_S3)
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_M5STACK_STICK_S3)
set(BOARD_TYPE "m5stack-stick-s3")
set(BUILTIN_TEXT_FONT font_puhui_basic_16_4)
set(BUILTIN_ICON_FONT font_awesome_16_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_32)
elseif(CONFIG_BOARD_TYPE_M5STACK_CORE_TAB5)
set(BOARD_TYPE "m5stack-tab5")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
@@ -1006,6 +1011,11 @@ if(CONFIG_IDF_TARGET_ESP32S3)
endif()
set(MAIN_PRIV_REQUIRES_EXTRA "")
if(BOARD_TYPE STREQUAL "m5stack-stick-s3")
list(APPEND MAIN_PRIV_REQUIRES_EXTRA
m5stack__m5pm1
)
endif()
if(CONFIG_BOARD_TYPE_ESP_VOCAT)
list(APPEND MAIN_PRIV_REQUIRES_EXTRA
espressif__touch_slider_sensor

View File

@@ -251,6 +251,9 @@ choice BOARD_TYPE
config BOARD_TYPE_M5STACK_CORE_S3
bool "M5Stack CoreS3"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_M5STACK_STICK_S3
bool "M5Stack StickS3"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_M5STACK_CORE_TAB5
bool "M5Stack Tab5"
depends on IDF_TARGET_ESP32P4

View File

@@ -0,0 +1,133 @@
# M5Stack Sticks3
* MCU: ESP32-S3
* PSRAM: 8MB
* Flash: 8MB
* Display: 1.14Inch, 135x240 LCD.
-----------
## Hardware
* SYS_I2C/I2C0
* SCL -- G48
* SDA -- G47
* PMIC: M5PM1
* Interface: I2C0@0x6E
* LCD
* Power: M5PM1_G2 高电平使能
* Resolution: 135x240
* Driver: CO5300
* Interface: SPI
* MOSI -- G39
* SCLK -- G40
* CS -- G41
* RS -- G45
* BL -- G38
* Audio
* Power: M5PM1_G2 高电平使能
* ES8311@0x18
* Control Interface: SYS_I2C
* Data Interface: I2S0
* MCLK -- G18
* BCLK -- G17
* ASDOUT -- G16
* LRCK -- G15
* DSDIN -- G14
* PA -- PM1_G3
* Key
* KEY1 -- G11
* IMU
* BMI270@0x68
* Interface: SYS_I2C
* IR
* TX -- G46
* RX -- G42
## 快速构建
推荐使用 release 脚本生成完整固件包:
```bash
python scripts/release.py m5stack-stick-s3 --name m5stack-stick-s3
```
生成的固件压缩包位于:
```text
releases/v2.2.6_m5stack-stick-s3.zip
```
`config.json` 中的关键构建配置:
```text
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv"
CONFIG_SPIRAM=y
```
## 手动配置
配置编译目标:
```bash
idf.py set-target esp32s3
```
打开配置菜单:
```bash
idf.py menuconfig
```
选择板卡:
```text
Xiaozhi Assistant -> Board Type -> M5Stack StickS3
```
配置 Flash 大小:
```text
Serial flasher config -> Flash size -> 8 MB
```
配置分区表:
```text
Partition Table -> Custom partition CSV file -> partitions/v2/8m.csv
```
配置 PSRAM
```text
Component config -> ESP PSRAM -> Support for external, SPI-connected RAM -> Select
```
编译:
```bash
idf.py build
```
## 合并固件
手动构建后,可使用以下命令合并烧录固件:
```bash
esptool.py --chip esp32s3 merge_bin \
--flash_mode dio \
--flash_freq 80m \
--flash_size 8MB \
0x0 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
0xd000 build/ota_data_initial.bin \
0x20000 build/xiaozhi.bin \
0x600000 build/generated_assets.bin \
-o M5Stack-StickS3-XiaoZhi-v2.2.6_0x00.bin
```
烧录合并后的固件:
```bash
esptool.py -b 1500000 write_flash -z 0 M5Stack-StickS3-XiaoZhi-v2.2.6_0x00.bin
```

View File

@@ -0,0 +1,52 @@
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
#define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
// I2S0 pins
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_18
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_17
#define AUDIO_I2S_GPIO_WS GPIO_NUM_15
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_14
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_16
// Audio Codec I2C (SYS_I2C/I2C0)
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_47
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_48
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
// PA control via PM1_G3 (not a GPIO pin)
#define AUDIO_CODEC_GPIO_PA GPIO_NUM_NC
// Button
#define BOOT_BUTTON_GPIO GPIO_NUM_11
#define USER_BUTTON_GPIO GPIO_NUM_12
// Display pins
#define DISPLAY_MOSI_PIN GPIO_NUM_39
#define DISPLAY_CLK_PIN GPIO_NUM_40
#define DISPLAY_CS_PIN GPIO_NUM_41
#define DISPLAY_DC_PIN GPIO_NUM_45
#define DISPLAY_RST_PIN GPIO_NUM_21
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_38
// Display configuration
#define DISPLAY_WIDTH 135
#define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X false
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY false
#define DISPLAY_INVERT_COLOR true
#define DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB
#define DISPLAY_OFFSET_X 52
#define DISPLAY_OFFSET_Y 40
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false
#define DISPLAY_SPI_MODE 0
// PMIC PM1 address
#define PM1_I2C_ADDR 0x6E
#endif // _BOARD_CONFIG_H_

View File

@@ -0,0 +1,14 @@
{
"target": "esp32s3",
"builds": [
{
"name": "m5stack-stick-s3",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"",
"CONFIG_USE_ESP_WAKE_WORD=y",
"CONFIG_SPIRAM=y"
]
}
]
}

View File

@@ -0,0 +1,256 @@
#include "wifi_board.h"
#include "codecs/es8311_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "i2c_device.h"
#include "assets/lang_config.h"
#include "backlight.h"
#include "settings.h"
#include <driver/i2c_master.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <driver/spi_common.h>
#include <esp_log.h>
#include <wifi_manager.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "M5PM1.h"
#define TAG "M5StackStickS3"
// reduce the output volume to 60%
class Sticks3AudioCodec : public Es8311AudioCodec {
public:
Sticks3AudioCodec(void* i2c_master_handle, i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din,
gpio_num_t pa_pin, uint8_t es8311_addr, bool use_mclk = true, bool pa_inverted = false)
: Es8311AudioCodec(i2c_master_handle, i2c_port, input_sample_rate, output_sample_rate,
mclk, bclk, ws, dout, din, pa_pin, es8311_addr, use_mclk, pa_inverted) {}
virtual void SetOutputVolume(int volume) override {
if (volume > 100) {
volume = 100;
}
if (volume < 0) {
volume = 0;
}
// Scale volume to 60% (x 0.6)
int scaled_volume = (int)(volume * 0.6f + 0.5f); // Round to nearest integer
ESP_LOGI(TAG, "Requested output volume: %d%%, scaled to hardware: %d%%", volume, scaled_volume);
// Call parent's SetOutputVolume with scaled value for hardware
Es8311AudioCodec::SetOutputVolume(scaled_volume);
// Update output_volume_ to original value for display
output_volume_ = volume;
// Save original value to settings
Settings settings("audio", true);
settings.SetInt("output_volume", volume);
}
};
class M5StackStickS3Board : public WifiBoard {
private:
Button boot_button_;
Button user_button_;
LcdDisplay* display_;
i2c_master_bus_handle_t i2c_bus_;
M5PM1* pmic_;
void InitializeI2c() {
// Initialize I2C peripheral (SYS_I2C/I2C0)
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 InitializePm1() {
// Initialize PM1 device
ESP_LOGI(TAG, "M5Stack PMIC Init.");
pmic_ = new M5PM1();
pmic_->begin(i2c_bus_, M5PM1_DEFAULT_ADDR);
pmic_->setChargeEnable(true);
pmic_->setBoostEnable(false);
// Configure PM1 G0 as input for charging status detection
// PM1 G0: low = charging, high = not charging
pmic_->pinMode(0, INPUT);
// Configure PM1 G2 (LCD/Audio Power) as output high
pmic_->pinMode(2, OUTPUT);
pmic_->gpioSetDrive(M5PM1_GPIO_NUM_2, M5PM1_GPIO_DRIVE_PUSHPULL);
pmic_->digitalWrite(2, HIGH);
vTaskDelay(pdMS_TO_TICKS(100));
// Configure PM1 G3 (PA) as output high
pmic_->pinMode(3, OUTPUT);
pmic_->gpioSetDrive(M5PM1_GPIO_NUM_3, M5PM1_GPIO_DRIVE_PUSHPULL);
pmic_->digitalWrite(3, HIGH);
// Enable double click to shutdown
pmic_->setDoubleOffDisable(false);
vTaskDelay(pdMS_TO_TICKS(20));
}
void InitializeSpi() {
ESP_LOGI(TAG, "Initialize SPI bus");
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = DISPLAY_MOSI_PIN;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = DISPLAY_CLK_PIN;
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 InitializeLcdDisplay() {
ESP_LOGI(TAG, "Initialize LCD Display");
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
// Install panel IO
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = DISPLAY_CS_PIN;
io_config.dc_gpio_num = DISPLAY_DC_PIN;
io_config.spi_mode = DISPLAY_SPI_MODE;
io_config.pclk_hz = 40 * 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));
// Install LCD driver
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = DISPLAY_RST_PIN;
panel_config.rgb_ele_order = DISPLAY_RGB_ORDER;
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_set_gap(panel, 0, 0);
esp_lcd_panel_invert_color(panel, DISPLAY_INVERT_COLOR);
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);
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);
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting && !WifiManager::GetInstance().IsConnected()) {
EnterWifiConfigMode();
}
app.ToggleChatState();
});
// User button on GPIO12: single click sets stored volume to 70%
user_button_.OnClick([this]() {
AudioCodec* codec = GetAudioCodec();
if (codec) {
// Use base AudioCodec::SetOutputVolume to update stored value
// without touching hardware that may not be initialized yet.
codec->SetOutputVolume(60);
codec->AudioCodec::SetOutputVolume(60);
ESP_LOGI(TAG, "User button pressed: stored volume set to 80%%");
} else {
ESP_LOGW(TAG, "User button pressed but codec is not available yet");
}
});
}
public:
M5StackStickS3Board() :
boot_button_(BOOT_BUTTON_GPIO),
user_button_(USER_BUTTON_GPIO),
display_(nullptr),
i2c_bus_(nullptr),
pmic_(nullptr) {
InitializeI2c();
InitializePm1(); // Initialize PM1 after I2C, before LCD/Audio
InitializeSpi();
InitializeLcdDisplay();
InitializeButtons();
GetBacklight()->SetBrightness(60);
}
virtual AudioCodec* GetAudioCodec() override {
static Sticks3AudioCodec 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_GPIO_PA, // Not used, PA controlled by PM1_G3
AUDIO_CODEC_ES8311_ADDR,
false);
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 bool GetBatteryLevel(int &level, bool& charging, bool& discharging) override {
if (!pmic_) {
return false;
}
// Get battery voltage in mV
uint16_t voltage_mv = 0;
if (pmic_->readVbat(&voltage_mv) != M5PM1_OK) {
return false;
}
// Get charging status from PM1_G0 (low = charging, high = not charging)
int pm1_g0_level = pmic_->digitalRead(0);
if (pm1_g0_level < 0) {
return false;
}
charging = (pm1_g0_level == 0); // Low level means charging
discharging = !charging;
// Convert voltage to battery level percentage
// Typical Li-ion battery: 3.0V (0%) to 4.2V (100%)
const int BATTERY_MIN_VOLTAGE = 3000; // 3.0V
const int BATTERY_MAX_VOLTAGE = 4200; // 4.2V
if (voltage_mv < BATTERY_MIN_VOLTAGE) {
level = 0;
} else if (voltage_mv > BATTERY_MAX_VOLTAGE) {
level = 100;
} else {
level = ((voltage_mv - BATTERY_MIN_VOLTAGE) * 100) / (BATTERY_MAX_VOLTAGE - BATTERY_MIN_VOLTAGE);
}
ESP_LOGD(TAG, "Battery: %d%% (%dmV), Charging: %s", level, voltage_mv, charging ? "Yes" : "No");
return true;
}
};
DECLARE_BOARD(M5StackStickS3Board);

View File

@@ -138,3 +138,4 @@ dependencies:
## Required IDF version
idf:
version: '>=5.5.2'
m5stack/m5pm1: ^1.0.5