From 73ad50c7325ac406bd1f4c7d12d23cd6b951b53d Mon Sep 17 00:00:00 2001 From: Hao Guan Date: Tue, 28 Apr 2026 13:55:39 +0800 Subject: [PATCH] feat: add M5Stack AtomS3R Echo Pyramid board (#1959) Made-with: Cursor --- main/CMakeLists.txt | 5 + main/Kconfig.projbuild | 3 + main/boards/atoms3r-echo-pyramid/README.md | 51 ++ .../atoms3r_echo_pyramid.cc | 763 ++++++++++++++++++ main/boards/atoms3r-echo-pyramid/config.h | 43 + main/boards/atoms3r-echo-pyramid/config.json | 13 + 6 files changed, 878 insertions(+) create mode 100644 main/boards/atoms3r-echo-pyramid/README.md create mode 100644 main/boards/atoms3r-echo-pyramid/atoms3r_echo_pyramid.cc create mode 100644 main/boards/atoms3r-echo-pyramid/config.h create mode 100644 main/boards/atoms3r-echo-pyramid/config.json diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 310f38a..1f904fb 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -209,6 +209,11 @@ elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_ECHO_BASE) 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_ATOM_S3R_ECHO_PYRAMID) + set(BOARD_TYPE "atoms3r-echo-pyramid") + 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_ATOM_S3R_CAM_M12_ECHO_BASE) set(BOARD_TYPE "atoms3r-cam-m12-echo-base") elseif(CONFIG_BOARD_TYPE_M5STACK_ATOM_ECHOS3R) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 7b958a9..c3e20a8 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -254,6 +254,9 @@ choice BOARD_TYPE config BOARD_TYPE_M5STACK_ATOM_S3R_ECHO_BASE bool "M5Stack AtomS3R + Echo Base" depends on IDF_TARGET_ESP32S3 + config BOARD_TYPE_M5STACK_ATOM_S3R_ECHO_PYRAMID + bool "M5Stack AtomS3R + Echo Pyramid" + depends on IDF_TARGET_ESP32S3 config BOARD_TYPE_M5STACK_ATOM_S3R_CAM_M12_ECHO_BASE bool "M5Stack AtomS3R CAM/M12 + Echo Base" depends on IDF_TARGET_ESP32S3 diff --git a/main/boards/atoms3r-echo-pyramid/README.md b/main/boards/atoms3r-echo-pyramid/README.md new file mode 100644 index 0000000..38fbc89 --- /dev/null +++ b/main/boards/atoms3r-echo-pyramid/README.md @@ -0,0 +1,51 @@ +# 编译配置命令 + +**配置编译目标为 ESP32S3:** + +```bash +idf.py set-target esp32s3 +``` + +**打开 menuconfig:** + +```bash +idf.py menuconfig +``` + +**选择板子:** + +``` +Xiaozhi Assistant -> Board Type -> M5Stack AtomS3R + Echo Pyramid +``` + +**修改 flash 大小:** + +``` +Serial flasher config -> Flash size -> 8 MB +``` + +**修改分区表:** + +``` +Partition Table -> Custom partition CSV file -> partitions/v2/8m.csv +``` + +**修改 psram 配置:** + +``` +Component config -> ESP PSRAM -> SPI RAM config -> Mode (QUAD/OCT) -> Octal Mode PSRAM +``` + +**编译:** + +```bash +idf.py build +``` + +## 使用说明 + +Echo Pyramid 正常运行时请从 Pyramid 底座的 USB-C 口供电;AtomS3R 的 USB-C 口主要用于烧录。 + +# 参考资料 + +https://github.com/m5stack/M5Echo-Pyramid \ No newline at end of file diff --git a/main/boards/atoms3r-echo-pyramid/atoms3r_echo_pyramid.cc b/main/boards/atoms3r-echo-pyramid/atoms3r_echo_pyramid.cc new file mode 100644 index 0000000..5fe3fdf --- /dev/null +++ b/main/boards/atoms3r-echo-pyramid/atoms3r_echo_pyramid.cc @@ -0,0 +1,763 @@ +#include "wifi_board.h" +#include "display/lcd_display.h" +#include "application.h" +#include "button.h" +#include "config.h" +#include "i2c_device.h" +#include "audio_codec.h" +#include "led/led.h" +#include "assets/lang_config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define TAG "AtomS3R+EchoPyramid" + +#define PYRAMID_SI5351_ADDR 0x60 +#define PYRAMID_STM32_ADDR 0x1A +#define PYRAMID_AW87559_ADDR 0x5B +#define PYRAMID_POWER_ON_RETRY_COUNT 20 +#define PYRAMID_POWER_ON_RETRY_DELAY_MS 250 + +#define STM32_SPK_RESTART_REG_ADDR 0xA0 +#define STM32_RGB1_BRIGHTNESS_REG_ADDR 0x10 +#define STM32_RGB2_BRIGHTNESS_REG_ADDR 0x11 +#define STM32_RGB1_STATUS_REG_ADDR 0x20 +#define STM32_RGB2_STATUS_REG_ADDR 0x60 +#define STM32_RGB_NUM_MAX 13 + +#define AW87559_REG_ID 0x00 +#define AW87559_REG_SYSCTRL 0x01 +#define AW87559_REG_PAGR 0x06 +#define AW87559_ID 0x5A +#define AW87559_SYS_EN_SW_MASK (1 << 6) +#define AW87559_SYS_EN_BOOST_MASK (1 << 4) +#define AW87559_SYS_EN_PA_MASK (1 << 3) +#define AW87559_GAIN_16_5DB 11 + +class Si5351 : public I2cDevice { +public: + Si5351(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(3, 0xFF); // Disable all clock outputs. + WriteReg(16, 0x80); + WriteReg(17, 0x80); + WriteReg(18, 0x80); + WriteReg(183, 0xC0); // Crystal load capacitance: 10 pF. + } + + void SetMclk(uint32_t sample_rate) { + if (sample_rate == 24000) { + SetPll(884736000UL, 144); // 884.736 MHz / 144 = 6.144 MHz + } else if (sample_rate == 16000) { + SetPll(884736000UL, 216); // 4.096 MHz + } else if (sample_rate == 44100) { + SetPll(903168000UL, 80); // 11.2896 MHz + } else if (sample_rate == 48000) { + SetPll(884736000UL, 72); // 12.288 MHz + } else { + ESP_LOGW(TAG, "Unsupported Si5351 sample rate: %lu", static_cast(sample_rate)); + } + } + +private: + static constexpr uint32_t kXtalFreq = 27000000UL; + + void WriteRegs(uint8_t reg, const uint8_t* data, size_t length) { + uint8_t buffer[9] = {}; + buffer[0] = reg; + for (size_t i = 0; i < length; ++i) { + buffer[i + 1] = data[i]; + } + ESP_ERROR_CHECK(i2c_master_transmit(i2c_device_, buffer, length + 1, 100)); + } + + void SetPll(uint32_t pll_freq, uint32_t ms_div) { + uint32_t a = pll_freq / kXtalFreq; + uint32_t rest = pll_freq % kXtalFreq; + uint32_t c = 1000000UL; + uint32_t b = (rest * c) / kXtalFreq; + + uint32_t p1 = 128 * a + (128 * b) / c - 512; + uint32_t p2 = 128 * b - c * ((128 * b) / c); + uint32_t p3 = c; + + WriteReg(3, 0xFF); + + uint8_t pll_buf[8] = { + static_cast((p3 >> 8) & 0xFF), + static_cast(p3 & 0xFF), + static_cast((p1 >> 16) & 0x03), + static_cast((p1 >> 8) & 0xFF), + static_cast(p1 & 0xFF), + static_cast(((p3 >> 12) & 0xF0) | ((p2 >> 16) & 0x0F)), + static_cast((p2 >> 8) & 0xFF), + static_cast(p2 & 0xFF), + }; + WriteRegs(26, pll_buf, sizeof(pll_buf)); + + uint32_t ms_p1 = 128 * ms_div - 512; + uint8_t ms_buf[8] = { + 0x00, + 0x01, + static_cast((ms_p1 >> 16) & 0x03), + static_cast((ms_p1 >> 8) & 0xFF), + static_cast(ms_p1 & 0xFF), + 0x00, + 0x00, + 0x00, + }; + WriteRegs(50, ms_buf, sizeof(ms_buf)); // Multisynth1 -> CLK1 + + WriteReg(17, 0x4F); // CLK1 from PLLA, 8 mA drive. + WriteReg(16, 0x80); + WriteReg(18, 0x80); + WriteReg(177, 0xA0); // Reset PLLA. + vTaskDelay(pdMS_TO_TICKS(10)); + WriteReg(3, 0xFD); // Enable CLK1 only. + + ESP_LOGI(TAG, "Si5351 CLK1 set to %lu Hz", static_cast(pll_freq / ms_div)); + } +}; + +class Aw87559 : public I2cDevice { +public: + Aw87559(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + auto id = ReadReg(AW87559_REG_ID); + if (id != AW87559_ID) { + ESP_LOGW(TAG, "Unexpected AW87559 ID: 0x%02x", id); + } + + UpdateBits(AW87559_REG_SYSCTRL, AW87559_SYS_EN_SW_MASK, AW87559_SYS_EN_SW_MASK); + UpdateBits(AW87559_REG_SYSCTRL, AW87559_SYS_EN_BOOST_MASK, AW87559_SYS_EN_BOOST_MASK); + UpdateBits(AW87559_REG_SYSCTRL, AW87559_SYS_EN_PA_MASK, AW87559_SYS_EN_PA_MASK); + UpdateBits(AW87559_REG_PAGR, 0x1F, AW87559_GAIN_16_5DB); + } + +private: + void UpdateBits(uint8_t reg, uint8_t mask, uint8_t value) { + auto reg_value = ReadReg(reg); + reg_value &= ~mask; + reg_value |= value & mask; + WriteReg(reg, reg_value); + } +}; + +class Stm32PyramidCtrl : public I2cDevice { +public: + Stm32PyramidCtrl(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + ResetSpeaker(); + SetBrightness(1, 100); + SetBrightness(2, 100); + SetAllRgb(1, 0, 0, 64); + SetAllRgb(2, 0, 0, 64); + } + + void ResetSpeaker() { + WriteReg(STM32_SPK_RESTART_REG_ADDR, 1); + vTaskDelay(pdMS_TO_TICKS(100)); + } + + void SetBrightness(uint8_t channel, uint8_t brightness) { + if (brightness > 100) { + brightness = 100; + } + WriteReg(channel == 1 ? STM32_RGB1_BRIGHTNESS_REG_ADDR : STM32_RGB2_BRIGHTNESS_REG_ADDR, brightness); + } + + void SetAllRgb(uint8_t channel, uint8_t r, uint8_t g, uint8_t b) { + const uint8_t base = (channel == 1) ? STM32_RGB1_STATUS_REG_ADDR : STM32_RGB2_STATUS_REG_ADDR; + + // Echo Pyramid STM32 uses 4-byte stride per LED: (B,G,R,0x00). + // One page is 0x10 bytes and contains 4 LEDs. + for (int page = 0; page < 4; ++page) { + uint8_t reg = base + static_cast(page * 0x10); + uint8_t payload[1 + 16] = {0}; + payload[0] = reg; + for (int i = 0; i < 4; ++i) { + payload[1 + i * 4 + 0] = b; + payload[1 + i * 4 + 1] = g; + payload[1 + i * 4 + 2] = r; + payload[1 + i * 4 + 3] = 0x00; + } + ESP_ERROR_CHECK(i2c_master_transmit(i2c_device_, payload, sizeof(payload), 100)); + } + } + + void SetStatusColor(uint8_t r, uint8_t g, uint8_t b) { + SetAllRgb(1, r, g, b); + SetAllRgb(2, r, g, b); + } +}; + +class PyramidStatusLed : public Led { +public: + void SetController(Stm32PyramidCtrl* ctrl) { ctrl_ = ctrl; } + + void OnStateChanged() override { + if (ctrl_ == nullptr) { + return; + } + + auto& app = Application::GetInstance(); + switch (app.GetDeviceState()) { + case kDeviceStateListening: + ctrl_->SetStatusColor(0, 64, 0); // green + break; + case kDeviceStateSpeaking: + ctrl_->SetStatusColor(64, 0, 0); // red + break; + default: + ctrl_->SetStatusColor(0, 0, 64); // blue + break; + } + } + +private: + Stm32PyramidCtrl* ctrl_ = nullptr; +}; + +class Lp5562 : public I2cDevice { +public: + Lp5562(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr) { + WriteReg(0x00, 0B01000000); // Set chip_en to 1 + WriteReg(0x08, 0B00000001); // Enable internal clock + WriteReg(0x70, 0B00000000); // Configure all LED outputs to be controlled from I2C registers + + // PWM clock frequency 558 Hz + auto data = ReadReg(0x08); + data = data | 0B01000000; + WriteReg(0x08, data); + } + + void SetBrightness(uint8_t brightness) { + // Map 0~100 to 0~255 + brightness = brightness * 255 / 100; + WriteReg(0x0E, brightness); + } +}; + +class CustomBacklight : public Backlight { +public: + CustomBacklight(Lp5562* lp5562) : lp5562_(lp5562) {} + + void SetBrightnessImpl(uint8_t brightness) override { + if (lp5562_) { + lp5562_->SetBrightness(brightness); + } else { + ESP_LOGE(TAG, "LP5562 not available"); + } + } + +private: + Lp5562* lp5562_ = nullptr; +}; + +class PyramidAudioCodec : public AudioCodec { +public: + PyramidAudioCodec(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, uint8_t es7210_addr, bool input_reference) { + duplex_ = true; + input_reference_ = input_reference; + input_channels_ = input_reference_ ? 2 : 1; + output_channels_ = 1; + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + input_gain_ = 30; + pa_pin_ = pa_pin; + + CreateDuplexChannels(mclk, bclk, ws, dout, din); + + audio_codec_i2s_cfg_t i2s_cfg = { + .port = I2S_NUM_0, + .rx_handle = rx_handle_, + .tx_handle = tx_handle_, + }; + data_if_ = audio_codec_new_i2s_data(&i2s_cfg); + assert(data_if_ != NULL); + + audio_codec_i2c_cfg_t i2c_cfg = { + .port = i2c_port, + .addr = es8311_addr, + .bus_handle = i2c_master_handle, + }; + out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(out_ctrl_if_ != NULL); + + gpio_if_ = audio_codec_new_gpio(); + assert(gpio_if_ != NULL); + + es8311_codec_cfg_t es8311_cfg = {}; + es8311_cfg.ctrl_if = out_ctrl_if_; + es8311_cfg.gpio_if = gpio_if_; + es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC; + es8311_cfg.pa_pin = pa_pin_; + es8311_cfg.use_mclk = true; + es8311_cfg.hw_gain.pa_voltage = 5.0; + es8311_cfg.hw_gain.codec_dac_voltage = 3.3; + out_codec_if_ = es8311_codec_new(&es8311_cfg); + assert(out_codec_if_ != NULL); + + esp_codec_dev_cfg_t dev_cfg = { + .dev_type = ESP_CODEC_DEV_TYPE_OUT, + .codec_if = out_codec_if_, + .data_if = data_if_, + }; + output_dev_ = esp_codec_dev_new(&dev_cfg); + assert(output_dev_ != NULL); + + i2c_cfg.addr = es7210_addr; + in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg); + assert(in_ctrl_if_ != NULL); + + es7210_codec_cfg_t es7210_cfg = {}; + es7210_cfg.ctrl_if = in_ctrl_if_; + es7210_cfg.mic_selected = ES7210_SEL_MIC1 | ES7210_SEL_MIC3; + in_codec_if_ = es7210_codec_new(&es7210_cfg); + assert(in_codec_if_ != NULL); + + dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN; + dev_cfg.codec_if = in_codec_if_; + input_dev_ = esp_codec_dev_new(&dev_cfg); + assert(input_dev_ != NULL); + + ESP_LOGI(TAG, "Pyramid audio codec initialized"); + } + + virtual ~PyramidAudioCodec() { + if (output_dev_) { + esp_codec_dev_close(output_dev_); + esp_codec_dev_delete(output_dev_); + } + if (input_dev_) { + esp_codec_dev_close(input_dev_); + esp_codec_dev_delete(input_dev_); + } + + audio_codec_delete_codec_if(in_codec_if_); + audio_codec_delete_ctrl_if(in_ctrl_if_); + audio_codec_delete_codec_if(out_codec_if_); + audio_codec_delete_ctrl_if(out_ctrl_if_); + audio_codec_delete_gpio_if(gpio_if_); + audio_codec_delete_data_if(data_if_); + } + + void SetOutputVolume(int volume) override { + std::lock_guard lock(data_if_mutex_); + if (output_dev_ != nullptr) { + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, volume)); + } + AudioCodec::SetOutputVolume(volume); + } + + void EnableInput(bool enable) override { + std::lock_guard lock(data_if_mutex_); + if (enable == input_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 2, + .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), + .sample_rate = static_cast(input_sample_rate_), + .mclk_multiple = 0, + }; + if (input_reference_) { + fs.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1); + } + ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), input_gain_)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(input_dev_)); + } + AudioCodec::EnableInput(enable); + } + + void EnableOutput(bool enable) override { + std::lock_guard lock(data_if_mutex_); + if (enable == output_enabled_) { + return; + } + if (enable) { + esp_codec_dev_sample_info_t fs = { + .bits_per_sample = 16, + .channel = 1, + .channel_mask = 0, + .sample_rate = static_cast(output_sample_rate_), + .mclk_multiple = 0, + }; + ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs)); + ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, output_volume_)); + } else { + ESP_ERROR_CHECK(esp_codec_dev_close(output_dev_)); + } + AudioCodec::EnableOutput(enable); + } + +private: + const audio_codec_data_if_t* data_if_ = nullptr; + const audio_codec_ctrl_if_t* out_ctrl_if_ = nullptr; + const audio_codec_if_t* out_codec_if_ = nullptr; + const audio_codec_ctrl_if_t* in_ctrl_if_ = nullptr; + const audio_codec_if_t* in_codec_if_ = nullptr; + const audio_codec_gpio_if_t* gpio_if_ = nullptr; + esp_codec_dev_handle_t output_dev_ = nullptr; + esp_codec_dev_handle_t input_dev_ = nullptr; + gpio_num_t pa_pin_ = GPIO_NUM_NC; + std::mutex data_if_mutex_; + + void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din) { + assert(input_sample_rate_ == output_sample_rate_); + + i2s_chan_config_t chan_cfg = { + .id = I2S_NUM_0, + .role = I2S_ROLE_MASTER, + .dma_desc_num = AUDIO_CODEC_DMA_DESC_NUM, + .dma_frame_num = AUDIO_CODEC_DMA_FRAME_NUM, + .auto_clear_after_cb = true, + .auto_clear_before_cb = false, + .intr_priority = 0, + }; + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle_, &rx_handle_)); + + i2s_std_config_t std_cfg = { + .clk_cfg = { + .sample_rate_hz = static_cast(output_sample_rate_), + .clk_src = I2S_CLK_SRC_DEFAULT, + .mclk_multiple = I2S_MCLK_MULTIPLE_256, +#ifdef I2S_HW_VERSION_2 + .ext_clk_freq_hz = 0, +#endif + }, + .slot_cfg = { + .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, + .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO, + .slot_mode = I2S_SLOT_MODE_STEREO, + .slot_mask = I2S_STD_SLOT_BOTH, + .ws_width = I2S_DATA_BIT_WIDTH_16BIT, + .ws_pol = false, + .bit_shift = true, +#ifdef I2S_HW_VERSION_2 + .left_align = true, + .big_endian = false, + .bit_order_lsb = false, +#endif + }, + .gpio_cfg = { + .mclk = mclk, + .bclk = bclk, + .ws = ws, + .dout = dout, + .din = din, + .invert_flags = { + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + }, + }, + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &std_cfg)); + ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_)); + ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_)); + ESP_LOGI(TAG, "Pyramid duplex I2S channels created"); + } + + int Read(int16_t* dest, int samples) override { + if (input_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(input_dev_, reinterpret_cast(dest), samples * sizeof(int16_t))); + } + return samples; + } + + int Write(const int16_t* data, int samples) override { + if (output_enabled_) { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(output_dev_, const_cast(data), samples * sizeof(int16_t))); + } + return samples; + } +}; + +static const gc9a01_lcd_init_cmd_t gc9107_lcd_init_cmds[] = { + // {cmd, { data }, data_size, delay_ms} + {0xfe, (uint8_t[]){0x00}, 0, 0}, + {0xef, (uint8_t[]){0x00}, 0, 0}, + {0xb0, (uint8_t[]){0xc0}, 1, 0}, + {0xb2, (uint8_t[]){0x2f}, 1, 0}, + {0xb3, (uint8_t[]){0x03}, 1, 0}, + {0xb6, (uint8_t[]){0x19}, 1, 0}, + {0xb7, (uint8_t[]){0x01}, 1, 0}, + {0xac, (uint8_t[]){0xcb}, 1, 0}, + {0xab, (uint8_t[]){0x0e}, 1, 0}, + {0xb4, (uint8_t[]){0x04}, 1, 0}, + {0xa8, (uint8_t[]){0x19}, 1, 0}, + {0xb8, (uint8_t[]){0x08}, 1, 0}, + {0xe8, (uint8_t[]){0x24}, 1, 0}, + {0xe9, (uint8_t[]){0x48}, 1, 0}, + {0xea, (uint8_t[]){0x22}, 1, 0}, + {0xc6, (uint8_t[]){0x30}, 1, 0}, + {0xc7, (uint8_t[]){0x18}, 1, 0}, + {0xf0, + (uint8_t[]){0x1f, 0x28, 0x04, 0x3e, 0x2a, 0x2e, 0x20, 0x00, 0x0c, 0x06, + 0x00, 0x1c, 0x1f, 0x0f}, + 14, 0}, + {0xf1, + (uint8_t[]){0x00, 0x2d, 0x2f, 0x3c, 0x6f, 0x1c, 0x0b, 0x00, 0x00, 0x00, + 0x07, 0x0d, 0x11, 0x0f}, + 14, 0}, +}; + +class AtomS3rEchoPyramidBoard : public WifiBoard { +private: + i2c_master_bus_handle_t i2c_bus_; + i2c_master_bus_handle_t i2c_bus_internal_; + Si5351* si5351_ = nullptr; + Aw87559* aw87559_ = nullptr; + Stm32PyramidCtrl* stm32_ = nullptr; + PyramidStatusLed led_; + Lp5562* lp5562_ = nullptr; + Display* display_ = nullptr; + Button boot_button_; + bool is_pyramid_connected_ = false; + + void InitializeI2c() { + i2c_master_bus_config_t i2c_bus_cfg = { + .i2c_port = I2C_NUM_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_)); + + i2c_bus_cfg.i2c_port = I2C_NUM_0; + i2c_bus_cfg.sda_io_num = GPIO_NUM_45; + i2c_bus_cfg.scl_io_num = GPIO_NUM_0; + ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &i2c_bus_internal_)); + } + + void I2cDetect() { + is_pyramid_connected_ = false; + bool has_es8311 = false; + bool has_es7210 = false; + bool has_si5351 = false; + bool has_stm32 = false; + bool has_aw87559 = false; + uint8_t address; + + printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\r\n"); + for (int i = 0; i < 128; i += 16) { + printf("%02x: ", i); + for (int j = 0; j < 16; j++) { + fflush(stdout); + address = i + j; + esp_err_t ret = i2c_master_probe(i2c_bus_, address, pdMS_TO_TICKS(200)); + if (ret == ESP_OK) { + printf("%02x ", address); + if (address == (AUDIO_CODEC_ES8311_ADDR >> 1)) { + has_es8311 = true; + } else if (address == (AUDIO_CODEC_ES7210_ADDR >> 1)) { + has_es7210 = true; + } else if (address == PYRAMID_SI5351_ADDR) { + has_si5351 = true; + } else if (address == PYRAMID_STM32_ADDR) { + has_stm32 = true; + } else if (address == PYRAMID_AW87559_ADDR) { + has_aw87559 = true; + } + } else if (ret == ESP_ERR_TIMEOUT) { + printf("UU "); + } else { + printf("-- "); + } + } + printf("\r\n"); + } + + is_pyramid_connected_ = has_es8311 && has_es7210 && has_si5351 && has_stm32 && has_aw87559; + } + + void WaitForPyramidConnection() { + for (int attempt = 0; attempt < PYRAMID_POWER_ON_RETRY_COUNT; ++attempt) { + I2cDetect(); + if (is_pyramid_connected_) { + if (attempt > 0) { + ESP_LOGI(TAG, "Echo Pyramid detected after %d retries", attempt); + } + return; + } + + ESP_LOGW(TAG, "Echo Pyramid not ready, retrying (%d/%d)", + attempt + 1, PYRAMID_POWER_ON_RETRY_COUNT); + vTaskDelay(pdMS_TO_TICKS(PYRAMID_POWER_ON_RETRY_DELAY_MS)); + } + } + + void CheckPyramidConnection() { + if (is_pyramid_connected_) { + return; + } + + InitializeLp5562(); + InitializeSpi(); + InitializeGc9107Display(); + InitializeButtons(); + GetBacklight()->SetBrightness(100); + + display_->SetupUI(); + display_->SetStatus(Lang::Strings::ERROR); + display_->SetEmotion("triangle_exclamation"); + display_->SetChatMessage("system", "Echo Pyramid\nnot connected"); + + while (1) { + ESP_LOGE(TAG, "Echo Pyramid is disconnected"); + vTaskDelay(pdMS_TO_TICKS(1000)); + + I2cDetect(); + if (is_pyramid_connected_) { + vTaskDelay(pdMS_TO_TICKS(500)); + I2cDetect(); + if (is_pyramid_connected_) { + ESP_LOGI(TAG, "Echo Pyramid is reconnected"); + vTaskDelay(pdMS_TO_TICKS(200)); + esp_restart(); + } + } + } + } + + void InitializePyramidDevices() { + ESP_LOGI(TAG, "Init Echo Pyramid devices"); + si5351_ = new Si5351(i2c_bus_, PYRAMID_SI5351_ADDR); + si5351_->SetMclk(AUDIO_OUTPUT_SAMPLE_RATE); + stm32_ = new Stm32PyramidCtrl(i2c_bus_, PYRAMID_STM32_ADDR); + led_.SetController(stm32_); + led_.OnStateChanged(); + aw87559_ = new Aw87559(i2c_bus_, PYRAMID_AW87559_ADDR); + } + + void InitializeLp5562() { + ESP_LOGI(TAG, "Init LP5562"); + lp5562_ = new Lp5562(i2c_bus_internal_, 0x30); + } + + void InitializeSpi() { + ESP_LOGI(TAG, "Initialize SPI bus"); + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = GPIO_NUM_21; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = GPIO_NUM_15; + 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 InitializeGc9107Display() { + ESP_LOGI(TAG, "Init GC9107 display"); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_panel_io_handle_t io_handle = NULL; + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_14; + io_config.dc_gpio_num = GPIO_NUM_42; + io_config.spi_mode = 0; + 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, &io_handle)); + + ESP_LOGI(TAG, "Install GC9A01 panel driver"); + esp_lcd_panel_handle_t panel_handle = NULL; + gc9a01_vendor_config_t gc9107_vendor_config = { + .init_cmds = gc9107_lcd_init_cmds, + .init_cmds_size = sizeof(gc9107_lcd_init_cmds) / sizeof(gc9a01_lcd_init_cmd_t), + }; + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = GPIO_NUM_48; + panel_config.rgb_endian = LCD_RGB_ENDIAN_BGR; + panel_config.bits_per_pixel = 16; + panel_config.vendor_config = &gc9107_vendor_config; + + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9a01(io_handle, &panel_config, &panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); + + display_ = new SpiLcdDisplay(io_handle, panel_handle, + 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) { + EnterWifiConfigMode(); + return; + } + app.ToggleChatState(); + }); + } + +public: + AtomS3rEchoPyramidBoard() : boot_button_(BOOT_BUTTON_GPIO) { + InitializeI2c(); + WaitForPyramidConnection(); + CheckPyramidConnection(); + InitializePyramidDevices(); + InitializeLp5562(); + InitializeSpi(); + InitializeGc9107Display(); + InitializeButtons(); + GetBacklight()->RestoreBrightness(); + } + + virtual Led* GetLed() override { + return &led_; + } + + virtual AudioCodec* GetAudioCodec() override { + static PyramidAudioCodec audio_codec( + i2c_bus_, + I2C_NUM_1, + 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, + AUDIO_CODEC_ES8311_ADDR, + AUDIO_CODEC_ES7210_ADDR, + AUDIO_INPUT_REFERENCE); + return &audio_codec; + } + + virtual Display* GetDisplay() override { + return display_; + } + + virtual Backlight* GetBacklight() override { + static CustomBacklight backlight(lp5562_); + return &backlight; + } +}; + +DECLARE_BOARD(AtomS3rEchoPyramidBoard); diff --git a/main/boards/atoms3r-echo-pyramid/config.h b/main/boards/atoms3r-echo-pyramid/config.h new file mode 100644 index 0000000..62959a5 --- /dev/null +++ b/main/boards/atoms3r-echo-pyramid/config.h @@ -0,0 +1,43 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// AtomS3R + Echo Pyramid Board configuration + +#include + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_NC +#define AUDIO_I2S_GPIO_WS GPIO_NUM_8 +#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_6 +#define AUDIO_I2S_GPIO_DIN GPIO_NUM_5 +#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_7 + +#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_38 +#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_39 +#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_ES7210_ADDR ES7210_CODEC_DEFAULT_ADDR +#define AUDIO_CODEC_GPIO_PA GPIO_NUM_NC + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#define BOOT_BUTTON_GPIO GPIO_NUM_41 +#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define DISPLAY_SDA_PIN GPIO_NUM_NC +#define DISPLAY_SCL_PIN GPIO_NUM_NC +#define DISPLAY_WIDTH 128 +#define DISPLAY_HEIGHT 128 +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 32 + +#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_NC +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/atoms3r-echo-pyramid/config.json b/main/boards/atoms3r-echo-pyramid/config.json new file mode 100644 index 0000000..09a9a21 --- /dev/null +++ b/main/boards/atoms3r-echo-pyramid/config.json @@ -0,0 +1,13 @@ +{ + "target": "esp32s3", + "builds": [ + { + "name": "atoms3r-echo-pyramid", + "sdkconfig_append": [ + "CONFIG_BOARD_TYPE_M5STACK_ATOM_S3R_ECHO_PYRAMID=y", + "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y", + "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"" + ] + } + ] +}