diff --git a/README.md b/README.md index 447f262a..447ed72b 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ - 神奇按钮 2.4 - 虾哥 Mini C3 - 微雪电子 ESP32-S3-Touch-AMOLED-1.8 +- LILYGO T-Circle-S3
@@ -73,6 +74,9 @@ + + +
## 固件部分 diff --git a/README_en.md b/README_en.md index aee74095..d3964727 100644 --- a/README_en.md +++ b/README_en.md @@ -53,6 +53,7 @@ Breadboard setup shown below: - MagiClick 2.4 - Xmini C3 - Waveshare ESP32-S3-Touch-AMOLED-1.8 +- LILYGO T-Circle-S3
@@ -73,6 +74,9 @@ Breadboard setup shown below: + + +
## Firmware Section diff --git a/README_ja.md b/README_ja.md index 93a0a8b0..a659df89 100644 --- a/README_ja.md +++ b/README_ja.md @@ -53,6 +53,7 @@ - MagiClick 2.4 - Xmini C3 - Waveshare ESP32-S3-Touch-AMOLED-1.8 +- LILYGO T-Circle-S3
@@ -73,6 +74,9 @@ + + +
## ファームウェアセクション diff --git a/docs/lilygo-t-circle-s3.jpg b/docs/lilygo-t-circle-s3.jpg new file mode 100644 index 00000000..45985d87 Binary files /dev/null and b/docs/lilygo-t-circle-s3.jpg differ diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9cc39635..e194ee7e 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -3,12 +3,14 @@ set(SOURCES "audio_codecs/audio_codec.cc" "audio_codecs/box_audio_codec.cc" "audio_codecs/es8311_audio_codec.cc" "audio_codecs/cores3_audio_codec.cc" + "audio_codecs/tcircles3_audio_codec.cc" "led/single_led.cc" "led/circular_strip.cc" "display/display.cc" "display/no_display.cc" "display/lcd_display.cc" "display/ssd1306_display.cc" + "boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c" "protocols/protocol.cc" "protocols/mqtt_protocol.cc" "protocols/websocket_protocol.cc" @@ -68,6 +70,8 @@ elseif(CONFIG_BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8) set(BOARD_TYPE "esp32-s3-touch-amoled-1.8") elseif(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI_LCD) set(BOARD_TYPE "bread-compact-wifi-lcd") +elseif(CONFIG_BOARD_TYPE_LILYGO_T_CIRCLE_S3) + set(BOARD_TYPE "lilygo-t-circle-s3") endif() file(GLOB BOARD_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_TYPE}/*.cc) list(APPEND SOURCES ${BOARD_SOURCES}) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 3b5bdfc7..71b84679 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -70,6 +70,8 @@ choice BOARD_TYPE bool "ESP-SparkBot开发板" config BOARD_TYPE_ESP32S3_Touch_AMOLED_1_8 bool "Waveshare ESP32-S3-Touch-AMOLED-1.8" + config BOARD_TYPE_LILYGO_T_CIRCLE_S3 + bool "LILYGO T-Circle-S3" endchoice choice DISPLAY_LCD_TYPE diff --git a/main/audio_codecs/tcircles3_audio_codec.cc b/main/audio_codecs/tcircles3_audio_codec.cc new file mode 100644 index 00000000..32ce43f4 --- /dev/null +++ b/main/audio_codecs/tcircles3_audio_codec.cc @@ -0,0 +1,140 @@ +#include "tcircles3_audio_codec.h" + +#include +#include +#include +#include +#include + +static const char TAG[] = "Tcircles3AudioCodec"; + +Tcircles3AudioCodec::Tcircles3AudioCodec(int input_sample_rate, int output_sample_rate, + gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data, + bool input_reference){ + duplex_ = true; // 是否双工 + input_reference_ = input_reference; // 是否使用参考输入,实现回声消除 + input_channels_ = input_reference_ ? 2 : 1; // 输入通道数 + input_sample_rate_ = input_sample_rate; + output_sample_rate_ = output_sample_rate; + + CreateVoiceHardware(mic_bclk, mic_ws, mic_data, spkr_bclk, spkr_lrclk, spkr_data); + + gpio_config_t config; + config.pin_bit_mask = BIT64(GPIO_NUM_45); + config.mode = GPIO_MODE_OUTPUT; + config.pull_up_en = GPIO_PULLUP_DISABLE; + config.pull_down_en = GPIO_PULLDOWN_ENABLE; + config.intr_type = GPIO_INTR_DISABLE; +#if SOC_GPIO_SUPPORT_PIN_HYS_FILTER + config.hys_ctrl_mode = GPIO_HYS_SOFT_ENABLE; +#endif + gpio_config(&config); + gpio_set_level(GPIO_NUM_45, 0); + ESP_LOGI(TAG, "Tcircles3AudioCodec initialized"); +} + +Tcircles3AudioCodec::~Tcircles3AudioCodec(){ + 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 Tcircles3AudioCodec::CreateVoiceHardware(gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data){ + + i2s_chan_config_t mic_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + mic_chan_config.auto_clear = true; // Auto clear the legacy data in the DMA buffer + i2s_chan_config_t spkr_chan_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER); + spkr_chan_config.auto_clear = true; // Auto clear the legacy data in the DMA buffer + + ESP_ERROR_CHECK(i2s_new_channel(&mic_chan_config, NULL, &rx_handle_)); + ESP_ERROR_CHECK(i2s_new_channel(&spkr_chan_config, &tx_handle_, NULL)); + + i2s_std_config_t mic_config ={ + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(static_cast(input_sample_rate_)), + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), + .gpio_cfg ={ + .mclk = I2S_GPIO_UNUSED, + .bclk = mic_bclk, + .ws = mic_ws, + .dout = I2S_GPIO_UNUSED, + .din = mic_data, + .invert_flags ={ + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false, + } + } + }; + + i2s_std_config_t spkr_config ={ + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(static_cast(11025)), + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), + .gpio_cfg ={ + .mclk = I2S_GPIO_UNUSED, + .bclk = spkr_bclk, + .ws = spkr_lrclk, + .dout = spkr_data, + .din = I2S_GPIO_UNUSED, + .invert_flags ={ + .mclk_inv = false, + .bclk_inv = false, + .ws_inv = false + } + } + }; + + ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle_, &mic_config)); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &spkr_config)); + ESP_LOGI(TAG, "Voice hardware created"); +} + +void Tcircles3AudioCodec::SetOutputVolume(int volume){ + volume_ = volume; + AudioCodec::SetOutputVolume(volume); +} + +void Tcircles3AudioCodec::EnableInput(bool enable){ + if (enable){ + }else{ + } + AudioCodec::EnableInput(enable); +} + +void Tcircles3AudioCodec::EnableOutput(bool enable){ + if (enable){ + gpio_set_level(GPIO_NUM_45, 1); + }else{ + gpio_set_level(GPIO_NUM_45, 0); + } + AudioCodec::EnableOutput(enable); +} + +int Tcircles3AudioCodec::Read(int16_t *dest, int samples){ + if (input_enabled_){ + size_t bytes_read; + i2s_channel_read(rx_handle_, dest, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY); + } + return samples; +} + +void AdjustVolume(const int16_t *input_data, int16_t *output_data, size_t samples, float volume){ + for (size_t i = 0; i < samples; i++){ + output_data[i] = (float)input_data[i] * volume; + } +} + +int Tcircles3AudioCodec::Write(const int16_t *data, int samples){ + if (output_enabled_){ + size_t bytes_read; + auto output_data = (int16_t *)malloc(samples * sizeof(int16_t)); + AdjustVolume(data, output_data, samples, (float)(volume_ / 100.0)); + i2s_channel_write(tx_handle_, output_data, samples * sizeof(int16_t), &bytes_read, portMAX_DELAY); + free(output_data); + } + return samples; +} diff --git a/main/audio_codecs/tcircles3_audio_codec.h b/main/audio_codecs/tcircles3_audio_codec.h new file mode 100644 index 00000000..3db28e71 --- /dev/null +++ b/main/audio_codecs/tcircles3_audio_codec.h @@ -0,0 +1,38 @@ +#ifndef _TCIRCLES3_AUDIO_CODEC_H +#define _TCIRCLES3_AUDIO_CODEC_H + +#include "audio_codec.h" + +#include +#include + +class Tcircles3AudioCodec : public AudioCodec +{ +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; + + uint32_t volume_ = 70; + + void CreateVoiceHardware(gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data,gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data); + + virtual int Read(int16_t *dest, int samples) override; + virtual int Write(const int16_t *data, int samples) override; + +public: + Tcircles3AudioCodec(int input_sample_rate, int output_sample_rate, + gpio_num_t mic_bclk, gpio_num_t mic_ws, gpio_num_t mic_data, + gpio_num_t spkr_bclk, gpio_num_t spkr_lrclk, gpio_num_t spkr_data, + bool input_reference); + virtual ~Tcircles3AudioCodec(); + + virtual void SetOutputVolume(int volume) override; + virtual void EnableInput(bool enable) override; + virtual void EnableOutput(bool enable) override; +}; + +#endif // _BOX_AUDIO_CODEC_H diff --git a/main/boards/lilygo-t-circle-s3/config.h b/main/boards/lilygo-t-circle-s3/config.h new file mode 100644 index 00000000..e115c77a --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/config.h @@ -0,0 +1,48 @@ +#ifndef _BOARD_CONFIG_H_ +#define _BOARD_CONFIG_H_ + +// M5Stack CoreS3 Board configuration + +#include +#include "pin_config.h" + +#define AUDIO_INPUT_REFERENCE true +#define AUDIO_INPUT_SAMPLE_RATE 24000 +#define AUDIO_OUTPUT_SAMPLE_RATE 24000 + +#define AUDIO_MIC_I2S_GPIO_BCLK static_cast(MSM261_BCLK) +#define AUDIO_MIC_I2S_GPIO_WS static_cast(MSM261_WS) +#define AUDIO_MIC_I2S_GPIO_DATA static_cast(MSM261_DATA) + +#define AUDIO_SPKR_I2S_GPIO_BCLK static_cast(MAX98357A_BCLK) +#define AUDIO_SPKR_I2S_GPIO_LRCLK static_cast(MAX98357A_LRCLK) +#define AUDIO_SPKR_I2S_GPIO_DATA static_cast(MAX98357A_DATA) +#define AUDIO_SPKR_ENABLE static_cast(MAX98357A_SD_MODE) + +#define TOUCH_I2C_SDA_PIN static_cast(TP_SDA) +#define TOUCH_I2C_SCL_PIN static_cast(TP_SCL) + +#define BUILTIN_LED_GPIO GPIO_NUM_NC +#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 LCD_WIDTH +#define DISPLAY_HEIGHT LCD_HEIGHT +#define DISPLAY_MOSI LCD_MOSI +#define DISPLAY_SCLK LCD_SCLK +#define DISPLAY_DC LCD_DC +#define DISPLAY_RST LCD_RST +#define DISPLAY_CS LCD_CS +#define DISPLAY_BL static_cast(LCD_BL) +#define DISPLAY_MIRROR_X false +#define DISPLAY_MIRROR_Y false +#define DISPLAY_SWAP_XY false + +#define DISPLAY_OFFSET_X 0 +#define DISPLAY_OFFSET_Y 0 + +#define DISPLAY_BACKLIGHT_PIN DISPLAY_BL +#define DISPLAY_BACKLIGHT_OUTPUT_INVERT false + +#endif // _BOARD_CONFIG_H_ diff --git a/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c b/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c new file mode 100644 index 00000000..25a78674 --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.c @@ -0,0 +1,353 @@ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_commands.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_check.h" + +#include "esp_lcd_gc9d01n.h" + +static const char *TAG = "gc9d01n"; + +static esp_err_t panel_gc9d01n_del(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9d01n_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9d01n_init(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9d01n_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); +static esp_err_t panel_gc9d01n_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_gc9d01n_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_gc9d01n_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); +static esp_err_t panel_gc9d01n_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); +static esp_err_t panel_gc9d01n_disp_on_off(esp_lcd_panel_t *panel, bool off); + +typedef struct{ + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + bool reset_level; + int x_gap; + int y_gap; + uint8_t fb_bits_per_pixel; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // save current value of LCD_CMD_COLMOD register + const gc9d01n_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; +} gc9d01n_panel_t; + +esp_err_t esp_lcd_new_panel_gc9d01n(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel){ + esp_err_t ret = ESP_OK; + gc9d01n_panel_t *gc9d01n = NULL; + gpio_config_t io_conf = {0}; + + ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + gc9d01n = (gc9d01n_panel_t *)calloc(1, sizeof(gc9d01n_panel_t)); + ESP_GOTO_ON_FALSE(gc9d01n, ESP_ERR_NO_MEM, err, TAG, "no mem for gc9d01n panel"); + + if (panel_dev_config->reset_gpio_num >= 0){ + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + switch (panel_dev_config->color_space){ + case ESP_LCD_COLOR_SPACE_RGB: + gc9d01n->madctl_val = 0; + break; + case ESP_LCD_COLOR_SPACE_BGR: + gc9d01n->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space"); + break; + } +#else + switch (panel_dev_config->rgb_endian){ + case LCD_RGB_ENDIAN_RGB: + gc9d01n->madctl_val = 0; + break; + case LCD_RGB_ENDIAN_BGR: + gc9d01n->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb endian"); + break; + } +#endif + + switch (panel_dev_config->bits_per_pixel){ + case 16: // RGB565 + gc9d01n->colmod_val = 0x55; + gc9d01n->fb_bits_per_pixel = 16; + break; + case 18: // RGB666 + gc9d01n->colmod_val = 0x66; + // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel + gc9d01n->fb_bits_per_pixel = 24; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + gc9d01n->io = io; + gc9d01n->reset_gpio_num = panel_dev_config->reset_gpio_num; + gc9d01n->reset_level = panel_dev_config->flags.reset_active_high; + if (panel_dev_config->vendor_config){ + gc9d01n->init_cmds = ((gc9d01n_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds; + gc9d01n->init_cmds_size = ((gc9d01n_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size; + } + gc9d01n->base.del = panel_gc9d01n_del; + gc9d01n->base.reset = panel_gc9d01n_reset; + gc9d01n->base.init = panel_gc9d01n_init; + gc9d01n->base.draw_bitmap = panel_gc9d01n_draw_bitmap; + gc9d01n->base.invert_color = panel_gc9d01n_invert_color; + gc9d01n->base.set_gap = panel_gc9d01n_set_gap; + gc9d01n->base.mirror = panel_gc9d01n_mirror; + gc9d01n->base.swap_xy = panel_gc9d01n_swap_xy; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + gc9d01n->base.disp_off = panel_gc9d01n_disp_on_off; +#else + gc9d01n->base.disp_on_off = panel_gc9d01n_disp_on_off; +#endif + *ret_panel = &(gc9d01n->base); + ESP_LOGD(TAG, "new gc9d01n panel @%p", gc9d01n); + + // ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_GC9D01N_VER_MAJOR, ESP_LCD_GC9D01N_VER_MINOR, + // ESP_LCD_GC9D01N_VER_PATCH); + + return ESP_OK; + +err: + if (gc9d01n){ + if (panel_dev_config->reset_gpio_num >= 0){ + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(gc9d01n); + } + return ret; +} + +static esp_err_t panel_gc9d01n_del(esp_lcd_panel_t *panel){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + + if (gc9d01n->reset_gpio_num >= 0){ + gpio_reset_pin(gc9d01n->reset_gpio_num); + } + ESP_LOGD(TAG, "del gc9d01n panel @%p", gc9d01n); + free(gc9d01n); + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_reset(esp_lcd_panel_t *panel){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + + // perform hardware reset + if (gc9d01n->reset_gpio_num >= 0){ + gpio_set_level(gc9d01n->reset_gpio_num, gc9d01n->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(gc9d01n->reset_gpio_num, !gc9d01n->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + } + else{ // perform software reset + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(20)); // spec, wait at least 5ms before sending new command + } + + return ESP_OK; +} + +static const gc9d01n_lcd_init_cmd_t vendor_specific_init_default[] = { + // {cmd, { data }, data_size, delay_ms} + // Enable Inter Register + {0xFE, (uint8_t[]){0x00}, 0, 0}, + {0xEF, (uint8_t[]){0x00}, 0, 0}, + {0x80, (uint8_t[]){0xFF}, 1, 0}, + {0x81, (uint8_t[]){0xFF}, 1, 0}, + {0x82, (uint8_t[]){0xFF}, 1, 0}, + {0x84, (uint8_t[]){0xFF}, 1, 0}, + {0x85, (uint8_t[]){0xFF}, 1, 0}, + {0x86, (uint8_t[]){0xFF}, 1, 0}, + {0x87, (uint8_t[]){0xFF}, 1, 0}, + {0x88, (uint8_t[]){0xFF}, 1, 0}, + {0x89, (uint8_t[]){0xFF}, 1, 0}, + {0x8A, (uint8_t[]){0xFF}, 1, 0}, + {0x8B, (uint8_t[]){0xFF}, 1, 0}, + {0x8C, (uint8_t[]){0xFF}, 1, 0}, + {0x8D, (uint8_t[]){0xFF}, 1, 0}, + {0x8E, (uint8_t[]){0xFF}, 1, 0}, + {0x8F, (uint8_t[]){0xFF}, 1, 0}, + {0x3A, (uint8_t[]){0x05}, 1, 0}, + {0xEC, (uint8_t[]){0x01}, 1, 0}, + {0x74, (uint8_t[]){0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00}, 7, 0}, + {0x98, (uint8_t[]){0x3E}, 1, 0}, + {0x99, (uint8_t[]){0x3E}, 1, 0}, + {0xB5, (uint8_t[]){0x0D, 0x0D}, 2, 0}, + {0x60, (uint8_t[]){0x38, 0x0F, 0x79, 0x67}, 4, 0}, + {0x61, (uint8_t[]){0x38, 0x11, 0x79, 0x67}, 4, 0}, + {0x64, (uint8_t[]){0x38, 0x17, 0x71, 0x5F, 0x79, 0x67}, 6, 0}, + {0x65, (uint8_t[]){0x38, 0x13, 0x71, 0x5B, 0x79, 0x67}, 6, 0}, + {0x6A, (uint8_t[]){0x00, 0x00}, 2, 0}, + {0x6C, (uint8_t[]){0x22, 0x02, 0x22, 0x02, 0x22, 0x22, 0x50}, 7, 0}, + {0x6E, (uint8_t[]){0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0x0F, 0x0F, 0x0D, 0x0D, 0x0B, 0x0B, 0x09, 0x09, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x0A, 0x0C, 0x0C, 0x0E, 0x0E, 0x10, 0x10, 0x00, 0x00, 0x02, 0x02, 0x04, 0x04}, 32, 0}, + {0xBF, (uint8_t[]){0x01}, 1, 0}, + {0xF9, (uint8_t[]){0x40}, 1, 0}, + {0x9B, (uint8_t[]){0x3B, 0x93, 0x33, 0x7F, 0x00}, 5, 0}, + {0x7E, (uint8_t[]){0x30}, 1, 0}, + {0x70, (uint8_t[]){0x0D, 0x02, 0x08, 0x0D, 0x02, 0x08}, 6, 0}, + {0x71, (uint8_t[]){0x0D, 0x02, 0x08}, 3, 0}, + {0x91, (uint8_t[]){0x0E, 0x09}, 2, 0}, + {0xC3, (uint8_t[]){0x19, 0xC4, 0x19, 0xC9, 0x3C}, 5, 0}, + {0xF0, (uint8_t[]){0x53, 0x15, 0x0A, 0x04, 0x00, 0x3E}, 6, 0}, + {0xF1, (uint8_t[]){0x56, 0xA8, 0x7F, 0x33, 0x34, 0x5F}, 6, 0}, + {0xF2, (uint8_t[]){0x53, 0x15, 0x0A, 0x04, 0x00, 0x3A}, 6, 0}, + {0xF3, (uint8_t[]){0x52, 0xA4, 0x7F, 0x33, 0x34, 0xDF}, 6, 0}, + + // {0x20, (uint8_t[]){0x00}, 0, 0}, + {0x36, (uint8_t[]){0x00}, 1, 0}, + {0x11, (uint8_t[]){0x00}, 0, 200}, + {0x29, (uint8_t[]){0x00}, 0, 0}, + {0x2C, (uint8_t[]){0x00}, 0, 20}, +}; + +static esp_err_t panel_gc9d01n_init(esp_lcd_panel_t *panel){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + + // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]){gc9d01n->madctl_val,},1),TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]){gc9d01n->colmod_val,},1),TAG, "send command failed"); + + const gc9d01n_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + if (gc9d01n->init_cmds){ + init_cmds = gc9d01n->init_cmds; + init_cmds_size = gc9d01n->init_cmds_size; + }else{ + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(gc9d01n_lcd_init_cmd_t); + } + + bool is_cmd_overwritten = false; + for (int i = 0; i < init_cmds_size; i++){ + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd){ + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + gc9d01n->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + gc9d01n->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + default: + is_cmd_overwritten = false; + break; + } + + if (is_cmd_overwritten){ + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); + } + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGD(TAG, "send init commands success"); + + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + + x_start += gc9d01n->x_gap; + x_end += gc9d01n->x_gap; + y_start += gc9d01n->y_gap; + y_end += gc9d01n->y_gap; + + // define an area of frame memory where MCU can access + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]){(x_start >> 8) & 0xFF,x_start & 0xFF,((x_end - 1) >> 8) & 0xFF,(x_end - 1) & 0xFF,},4),TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]){(y_start >> 8) & 0xFF,y_start & 0xFF,((y_end - 1) >> 8) & 0xFF,(y_end - 1) & 0xFF,},4),TAG, "send command failed"); + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * gc9d01n->fb_bits_per_pixel / 8; + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len), TAG, "send color failed"); + + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_invert_color(esp_lcd_panel_t *panel, bool invert_color_data){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + int command = 0; + if (invert_color_data){ + command = LCD_CMD_INVON; + }else{ + command = LCD_CMD_INVOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + if (mirror_x){ + gc9d01n->madctl_val |= LCD_CMD_MX_BIT; + }else{ + gc9d01n->madctl_val &= ~LCD_CMD_MX_BIT; + } + if (mirror_y){ + gc9d01n->madctl_val |= LCD_CMD_MY_BIT; + }else{ + gc9d01n->madctl_val &= ~LCD_CMD_MY_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]){gc9d01n->madctl_val}, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_swap_xy(esp_lcd_panel_t *panel, bool swap_axes){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + if (swap_axes){ + gc9d01n->madctl_val |= LCD_CMD_MV_BIT; + }else{ + gc9d01n->madctl_val &= ~LCD_CMD_MV_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]){gc9d01n->madctl_val}, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + gc9d01n->x_gap = x_gap; + gc9d01n->y_gap = y_gap; + return ESP_OK; +} + +static esp_err_t panel_gc9d01n_disp_on_off(esp_lcd_panel_t *panel, bool on_off){ + gc9d01n_panel_t *gc9d01n = __containerof(panel, gc9d01n_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9d01n->io; + int command = 0; + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + on_off = !on_off; +#endif + + if (on_off){ + command = LCD_CMD_DISPON; + }else{ + command = LCD_CMD_DISPOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} diff --git a/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.h b/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.h new file mode 100644 index 00000000..ec057cc2 --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/esp_lcd_gc9d01n.h @@ -0,0 +1,99 @@ +#pragma once + +#include "esp_lcd_panel_vendor.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LCD panel initialization commands. + * + */ +typedef struct { + int cmd; /* +#include +#include +#include +#include +#include "esp_lcd_gc9d01n.h" + +#define TAG "LilygoTCircleS3Board" + +class Cst816x : public I2cDevice{ +public: + struct TouchPoint_t{ + int num = 0; + int x = -1; + int y = -1; + }; + + Cst816x(i2c_master_bus_handle_t i2c_bus, uint8_t addr) : I2cDevice(i2c_bus, addr){ + uint8_t chip_id = ReadReg(0xA7); + ESP_LOGI(TAG, "Get chip ID: 0x%02X", chip_id); + read_buffer_ = new uint8_t[6]; + } + + ~Cst816x(){ + delete[] read_buffer_; + } + + void UpdateTouchPoint(){ + ReadRegs(0x02, read_buffer_, 6); + tp_.num = read_buffer_[0] & 0x0F; + tp_.x = ((read_buffer_[1] & 0x0F) << 8) | read_buffer_[2]; + tp_.y = ((read_buffer_[3] & 0x0F) << 8) | read_buffer_[4]; + } + + const TouchPoint_t &GetTouchPoint(){ + return tp_; + } + +private: + uint8_t *read_buffer_ = nullptr; + TouchPoint_t tp_; +}; + +class LilygoTCircleS3Board : public WifiBoard{ +private: + i2c_master_bus_handle_t i2c_bus_; + Cst816x *cst816d_; + LcdDisplay *display_; + Button boot_button_; + + void InitI2c(){ + // Initialize I2C peripheral + i2c_master_bus_config_t i2c_bus_config = { + .i2c_port = I2C_NUM_0, + .sda_io_num = TOUCH_I2C_SDA_PIN, + .scl_io_num = TOUCH_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_config, &i2c_bus_)); + } + + void I2cDetect(){ + 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); + }else if (ret == ESP_ERR_TIMEOUT){ + printf("UU "); + }else{ + printf("-- "); + } + } + printf("\r\n"); + } + } + + static void touchpad_daemon(void *param){ + vTaskDelay(pdMS_TO_TICKS(2000)); + auto &board = (LilygoTCircleS3Board&)Board::GetInstance(); + auto touchpad = board.GetTouchpad(); + bool was_touched = false; + while (1){ + touchpad->UpdateTouchPoint(); + if (touchpad->GetTouchPoint().num > 0){ + // On press + if (!was_touched){ + was_touched = true; + Application::GetInstance().ToggleChatState(); + } + } + // On release + else if (was_touched){ + was_touched = false; + } + vTaskDelay(pdMS_TO_TICKS(50)); + } + vTaskDelete(NULL); + } + + void InitCst816d(){ + ESP_LOGI(TAG, "Init CST816x"); + cst816d_ = new Cst816x(i2c_bus_, 0x15); + xTaskCreate(touchpad_daemon, "tp", 2048, NULL, 5, NULL); + } + + void InitSpi(){ + spi_bus_config_t buscfg = {}; + buscfg.mosi_io_num = DISPLAY_MOSI; + buscfg.miso_io_num = GPIO_NUM_NC; + buscfg.sclk_io_num = DISPLAY_SCLK; + 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 InitGc9d01nDisplay(){ + ESP_LOGI(TAG, "Init GC9D01N"); + + esp_lcd_panel_io_handle_t panel_io = nullptr; + esp_lcd_panel_handle_t panel = nullptr; + + ESP_LOGD(TAG, "Install panel IO"); + esp_lcd_panel_io_spi_config_t io_config = {}; + io_config.cs_gpio_num = DISPLAY_CS; + io_config.dc_gpio_num = DISPLAY_DC; + 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, &panel_io)); + + ESP_LOGD(TAG, "Install LCD driver"); + esp_lcd_panel_dev_config_t panel_config = {}; + panel_config.reset_gpio_num = DISPLAY_RST; + panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + panel_config.bits_per_pixel = 16; + ESP_ERROR_CHECK(esp_lcd_new_panel_gc9d01n(panel_io, &panel_config, &panel)); + + esp_lcd_panel_reset(panel); + + esp_lcd_panel_init(panel); + esp_lcd_panel_invert_color(panel, false); + esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY); + esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y); + + display_ = new LcdDisplay(panel_io, panel, DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y, DISPLAY_MIRROR_X, + DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY); + + gpio_config_t config; + config.pin_bit_mask = BIT64(DISPLAY_BL); + config.mode = GPIO_MODE_OUTPUT; + config.pull_up_en = GPIO_PULLUP_DISABLE; + config.pull_down_en = GPIO_PULLDOWN_ENABLE; + config.intr_type = GPIO_INTR_DISABLE; +#if SOC_GPIO_SUPPORT_PIN_HYS_FILTER + config.hys_ctrl_mode = GPIO_HYS_SOFT_ENABLE; +#endif + gpio_config(&config); + gpio_set_level(DISPLAY_BL, 0); + } + + void InitializeButtons(){ + boot_button_.OnClick([this]() + { + auto& app = Application::GetInstance(); + if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) { + ResetWifiConfiguration(); + } + app.ToggleChatState(); }); + } + + // 物联网初始化,添加对 AI 可见设备 + void InitializeIot(){ + auto &thing_manager = iot::ThingManager::GetInstance(); + thing_manager.AddThing(iot::CreateThing("Speaker")); + } + +public: + LilygoTCircleS3Board() : boot_button_(BOOT_BUTTON_GPIO){ + InitI2c(); + InitCst816d(); + I2cDetect(); + InitSpi(); + InitGc9d01nDisplay(); + InitializeButtons(); + InitializeIot(); + } + + virtual AudioCodec *GetAudioCodec() override{ + static Tcircles3AudioCodec *audio_codec = nullptr; + if (audio_codec == nullptr){ + audio_codec = new Tcircles3AudioCodec(AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE, + AUDIO_MIC_I2S_GPIO_BCLK, AUDIO_MIC_I2S_GPIO_WS, AUDIO_MIC_I2S_GPIO_DATA, + AUDIO_SPKR_I2S_GPIO_BCLK, AUDIO_SPKR_I2S_GPIO_LRCLK, AUDIO_SPKR_I2S_GPIO_DATA, + AUDIO_INPUT_REFERENCE); + } + return audio_codec; + } + + virtual Display *GetDisplay() override{ + return display_; + } + + Cst816x *GetTouchpad(){ + return cst816d_; + } +}; + +DECLARE_BOARD(LilygoTCircleS3Board); diff --git a/main/boards/lilygo-t-circle-s3/pin_config.h b/main/boards/lilygo-t-circle-s3/pin_config.h new file mode 100644 index 00000000..db428cac --- /dev/null +++ b/main/boards/lilygo-t-circle-s3/pin_config.h @@ -0,0 +1,47 @@ +/* + * @Description: None + * @Author: LILYGO_L + * @Date: 2023-08-16 14:24:03 + * @LastEditTime: 2025-01-20 10:11:16 + * @License: GPL 3.0 + */ +#pragma once + +// MAX98357A +#define MAX98357A_BCLK 5 +#define MAX98357A_LRCLK 4 +#define MAX98357A_DATA 6 +#define MAX98357A_SD_MODE 45 + +// MSM261 +#define MSM261_BCLK 7 +#define MSM261_WS 9 +#define MSM261_DATA 8 + +// APA102 +#define APA102_DATA 38 +#define APA102_CLOCK 39 + +// H0075Y002-V0 +#define LCD_WIDTH 160 +#define LCD_HEIGHT 160 +#define LCD_MOSI 17 +#define LCD_SCLK 15 +#define LCD_DC 16 +#define LCD_RST -1 +#define LCD_CS 13 +#define LCD_BL 18 + +// IIC +#define IIC_SDA 11 +#define IIC_SCL 14 + +// CST816D +#define TP_SDA 11 +#define TP_SCL 14 +#define TP_RST -1 +#define TP_INT 12 + +//Rotary Encoder +#define KNOB_DATA_A 47 +#define KNOB_DATA_B 48