From fd942b88fd3e8c9fa2bd6f3f5605a01e240d8532 Mon Sep 17 00:00:00 2001 From: shangjinlong Date: Wed, 28 Oct 2026 18:58:57 +0800 Subject: [PATCH] feat(platform): add three ESP32-S3 board profiles --- .github/workflows/ci.yml | 15 +- CHANGELOG.md | 6 + README.md | 11 +- README.zh-CN.md | 9 +- VENDORED_SOURCES.md | 2 +- components/mybot_platform/boards/README.md | 5 +- .../boards/atk-dnesp32s3-box/README.md | 14 + .../atk-dnesp32s3-box/atk_box_hardware.c | 65 ++++ .../boards/atk-dnesp32s3-box/board.c | 6 + .../boards/atk-dnesp32s3-box/board.cmake | 18 + .../boards/atk-dnesp32s3-box/board_config.h | 73 ++++ .../atk-dnesp32s3-box/sdkconfig.defaults | 10 + components/mybot_platform/boards/boards.cmake | 9 + .../boards/common/generic_profile.cmake | 2 + .../README.md | 16 + .../lilygo-t-display-s3-pro-mvsrlora/board.c | 6 + .../board.cmake | 19 + .../board_config.h | 72 ++++ .../lilygo_tdisplay_hardware.c | 46 +++ .../sdkconfig.defaults | 8 + .../mybot_platform/boards/tudouzi/README.md | 14 + .../mybot_platform/boards/tudouzi/board.c | 6 + .../mybot_platform/boards/tudouzi/board.cmake | 17 + .../boards/tudouzi/board_config.h | 38 ++ .../boards/tudouzi/sdkconfig.defaults | 11 + .../boards/tudouzi/tudouzi_hardware.c | 66 ++++ .../display/panels/generic/i80_st7789_panel.c | 196 ++++++++++ .../display/panels/generic/rgb565_spi_panel.c | 32 +- .../src/drivers/input/cst816s_boot_input.c | 97 ++++- docs/BOARD_PORTING.md | 14 +- docs/BOARD_PORTING.zh-CN.md | 10 +- docs/PLATFORM_UI.md | 12 +- docs/PLATFORM_UI.zh-CN.md | 10 +- docs/VALIDATION.md | 3 + tests/board_power/mocks/driver/i2c_master.h | 33 ++ tests/board_power/mocks/esp_err.h | 2 + tests/board_power/mocks/esp_log.h | 7 + tests/board_power/run.sh | 30 ++ tests/board_power/test_board_power.c | 198 ++++++++++ tests/cst226_input/run.sh | 21 ++ tests/cst226_input/test_cst226_input.c | 354 ++++++++++++++++++ tests/cst816s_input/mocks/mock_platform.h | 35 ++ tests/custom_panel_profiles/run.sh | 1 + .../test_custom_panel.cc | 5 +- tests/i80_st7789_lifecycle/run.sh | 27 ++ .../test_i80_st7789_lifecycle.cc | 311 +++++++++++++++ tests/st7789_lifecycle/mocks/host_platform.h | 47 +++ 47 files changed, 1975 insertions(+), 34 deletions(-) create mode 100644 components/mybot_platform/boards/atk-dnesp32s3-box/README.md create mode 100644 components/mybot_platform/boards/atk-dnesp32s3-box/atk_box_hardware.c create mode 100644 components/mybot_platform/boards/atk-dnesp32s3-box/board.c create mode 100644 components/mybot_platform/boards/atk-dnesp32s3-box/board.cmake create mode 100644 components/mybot_platform/boards/atk-dnesp32s3-box/board_config.h create mode 100644 components/mybot_platform/boards/atk-dnesp32s3-box/sdkconfig.defaults create mode 100644 components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/README.md create mode 100644 components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board.c create mode 100644 components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board.cmake create mode 100644 components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board_config.h create mode 100644 components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/lilygo_tdisplay_hardware.c create mode 100644 components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/sdkconfig.defaults create mode 100644 components/mybot_platform/boards/tudouzi/README.md create mode 100644 components/mybot_platform/boards/tudouzi/board.c create mode 100644 components/mybot_platform/boards/tudouzi/board.cmake create mode 100644 components/mybot_platform/boards/tudouzi/board_config.h create mode 100644 components/mybot_platform/boards/tudouzi/sdkconfig.defaults create mode 100644 components/mybot_platform/boards/tudouzi/tudouzi_hardware.c create mode 100644 components/mybot_platform/src/drivers/display/panels/generic/i80_st7789_panel.c create mode 100644 tests/board_power/mocks/driver/i2c_master.h create mode 100644 tests/board_power/mocks/esp_err.h create mode 100644 tests/board_power/mocks/esp_log.h create mode 100755 tests/board_power/run.sh create mode 100644 tests/board_power/test_board_power.c create mode 100755 tests/cst226_input/run.sh create mode 100644 tests/cst226_input/test_cst226_input.c create mode 100755 tests/i80_st7789_lifecycle/run.sh create mode 100644 tests/i80_st7789_lifecycle/test_i80_st7789_lifecycle.cc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dcf001..870fdf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,8 @@ jobs: tests/st7789_lifecycle tests/co5300_lifecycle tests/bread_compact_wifi_lcd \ tests/cst816s_input tests/tca9554 tests/aw9523b tests/compact_panel \ tests/cores3_pixel_ui tests/labplus_stm8_reset tests/atk_tca9555_power \ - tests/custom_panel_profiles -type f \ + tests/custom_panel_profiles tests/board_power tests/cst226_input \ + tests/i80_st7789_lifecycle -type f \ \( -name '*.c' -o -name '*.h' -o -name '*.cc' -o -name '*.cpp' \) \ -exec clang-format --dry-run --Werror {} + - name: Whitespace check @@ -44,7 +45,8 @@ jobs: tests/st7789_lifecycle tests/co5300_lifecycle tests/bread_compact_wifi_lcd \ tests/cst816s_input tests/tca9554 tests/aw9523b tests/compact_panel \ tests/cores3_pixel_ui tests/labplus_stm8_reset tests/atk_tca9555_power \ - tests/custom_panel_profiles -type f \ + tests/custom_panel_profiles tests/board_power tests/cst226_input \ + tests/i80_st7789_lifecycle -type f \ \( -name '*.c' -o -name '*.h' -o -name '*.cc' -o -name '*.cpp' \) \ -print0 | while IFS= read -r -d '' file; do head -n 5 "$file" | grep -q 'SPDX-License-Identifier:' || { @@ -60,6 +62,8 @@ jobs: run: sh tests/co5300_lifecycle/run.sh - name: CST816S and Boot input host lifecycle tests run: sh tests/cst816s_input/run.sh + - name: CST226 direct-I2C input host lifecycle tests + run: sh tests/cst226_input/run.sh - name: TCA9554 masked update host tests run: sh tests/tca9554/run.sh - name: AW9523B masked update host tests @@ -68,6 +72,10 @@ jobs: run: sh tests/labplus_stm8_reset/run.sh - name: ATK TCA9555 display-power host tests run: sh tests/atk_tca9555_power/run.sh + - name: Board power and expander sequencing host tests + run: sh tests/board_power/run.sh + - name: ST7789 i80 panel lifecycle host tests + run: sh tests/i80_st7789_lifecycle/run.sh - name: GC9D01N and GC9309NA panel profiles host tests run: sh tests/custom_panel_profiles/run.sh - name: Compact GC9107/ST7735 panel lifecycle tests @@ -351,6 +359,9 @@ jobs: - lilygo-t-cameraplus-s3-v1.2 - lilygo-t-circle-s3 - jiuchuan-s3 + - lilygo-t-display-s3-pro-mvsrlora + - atk-dnesp32s3-box + - tudouzi language: [zh-CN, en-US] steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f47242..72b9d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ Versioning and Conventional Commits. ### Added +- Three additional ESP32-S3 profiles: LilyGO T-Display S3 Pro with a 222×480 ST7796 panel, + CST226SE touch and PDM capture; ATK DNESP32S3 Box with an 8-bit i80 ST7789 panel and ES8311 + audio; and Tudouzi as a headless Wi-Fi subset with ES8311/ES7210 audio. Added host coverage for + the SY6970, XL9555/TCA9555 and AXP2101 startup sequences, CST226 direct-I2C input, i80 panel + lifecycle and ST7796 command profile. All three passed local ESP-IDF v5.5.2 Chinese and English + reconfigure/build/size/merge-bin validation; real-device validation remains pending. - Five additional ESP32-S3 hardware families across six compile-time profiles: ATK DNESP32S3 with TCA9555 display power, Yunliao S3 as a Wi-Fi subset, LilyGO T-CameraPlus S3 V1.0/V1.1 and V1.2 with separate standard-I2S/PDM audio paths, LilyGO T-Circle S3 with a 160×160 GC9D01N round diff --git a/README.md b/README.md index 821ab81..223af1a 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ ESP32-S3 boards. The project owns board initialization, Wi-Fi provisioning, persistent storage, HTTPS transport, audio, input, display, and the firmware lifecycle around the SDK. -The development baseline is **ESP-IDF v5.5.2**. Sixty-nine compile-time board profiles are -supported (62 display and 7 headless); the board, Flash, PSRAM, partition table, and hardware +The development baseline is **ESP-IDF v5.5.2**. Seventy-two compile-time board profiles are +supported (64 display and 8 headless); the board, Flash, PSRAM, partition table, and hardware drivers are selected with `MYBOT_BOARD`. See the [board documentation index](components/mybot_platform/boards/README.md) for hardware details, controls, build commands, and per-board limitations. @@ -59,13 +59,14 @@ Display profiles: `genjutech-s3-1.54tft`, `zhengchen-cam`, `labplus-ledong-v2`, `labplus-mpython-v3`, `atk-dnesp32s3`, `yunliao-s3`, `lilygo-t-cameraplus-s3`, `lilygo-t-cameraplus-s3-v1.2`, `lilygo-t-circle-s3`, -`jiuchuan-s3`, `m5stack-core-s3`, `m5stack-stick-s3`, and `sensecap-watcher`. +`jiuchuan-s3`, `lilygo-t-display-s3-pro-mvsrlora`, `atk-dnesp32s3-box`, +`m5stack-core-s3`, `m5stack-stick-s3`, and `sensecap-watcher`. Headless profiles: `atom-echos3r`, `xingzhi-cube-0.96oled-wifi`, `doit-s3-aibox`, -`bread-compact-wifi`, `hu-087`, `kevin-box-2`, and -`respeaker-flex-xvf3800-circular4-xiao`. +`bread-compact-wifi`, `hu-087`, `kevin-box-2`, +`respeaker-flex-xvf3800-circular4-xiao`, and `tudouzi`. All profiles currently use Wi-Fi. The ML307 profile reserves modem UART pins but does not implement modem or 4G networking. Only `m5stack-core-s3` currently exposes camera video. diff --git a/README.zh-CN.md b/README.zh-CN.md index d8c41f1..9a9cb38 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -9,7 +9,7 @@ 工程负责板级初始化、Wi-Fi 配网、持久化存储、HTTPS、音频、输入、显示,以及 SDK 外围的 固件生命周期。 -当前开发基线为 **ESP-IDF v5.5.2**,支持 69 个编译期 Board profile(62 个带屏、7 个无屏)。 +当前开发基线为 **ESP-IDF v5.5.2**,支持 72 个编译期 Board profile(64 个带屏、8 个无屏)。 通过 `MYBOT_BOARD` 选择板型、Flash、PSRAM、分区表和硬件驱动。具体硬件配置、输入操作、 构建命令和限制见[板卡说明索引](components/mybot_platform/boards/README.md)。 @@ -54,13 +54,14 @@ `genjutech-s3-1.54tft`、`zhengchen-cam`、`labplus-ledong-v2`、 `labplus-mpython-v3`、`atk-dnesp32s3`、`yunliao-s3`、 `lilygo-t-cameraplus-s3`、`lilygo-t-cameraplus-s3-v1.2`、`lilygo-t-circle-s3`、 -`jiuchuan-s3`、`m5stack-core-s3`、`m5stack-stick-s3` 和 `sensecap-watcher`。 +`jiuchuan-s3`、`lilygo-t-display-s3-pro-mvsrlora`、`atk-dnesp32s3-box`、 +`m5stack-core-s3`、`m5stack-stick-s3` 和 `sensecap-watcher`。 无屏 profile: `atom-echos3r`、`xingzhi-cube-0.96oled-wifi`、`doit-s3-aibox`、 -`bread-compact-wifi`、`hu-087`、`kevin-box-2` 和 -`respeaker-flex-xvf3800-circular4-xiao`。 +`bread-compact-wifi`、`hu-087`、`kevin-box-2`、 +`respeaker-flex-xvf3800-circular4-xiao` 和 `tudouzi`。 所有 profile 当前均使用 Wi-Fi。ML307 profile 仅预留 Modem UART 引脚,未实现 Modem 或 4G 联网。目前仅 `m5stack-core-s3` 提供摄像头视频能力。 diff --git a/VENDORED_SOURCES.md b/VENDORED_SOURCES.md index 9f7a8d3..0669107 100644 --- a/VENDORED_SOURCES.md +++ b/VENDORED_SOURCES.md @@ -59,7 +59,7 @@ are not vendored into this repository. | CoreS3 `components/mybot_platform/src/drivers/display/renderers/lvgl/pixel_glasses_view.cc`, expression geometry/masks/generators, `pixel_expressions.c`, and private expression headers/policy | `github.com/junlon2006/bk_solution_ai`, Solution checkout in `mybot-bk7259` | commit `dc4c85492bcf00f780c92d56c6cba2a362f54f68`; adapted to the existing ESP32 display API and CoreS3 320x240 geometry | | CoreS3 terminal subsets in `components/mybot_platform/src/drivers/display/renderers/lvgl/pixel_fonts.c` and `TERMINAL_FONT_LICENSE.txt` | DejaVu Sans Mono Bold, renamed MyBot Terminal | Source TTF SHA-256 `3a3c502eeff669a231549e80df9f7c49de109bafe303170409e905d0b31a38fe`; 9 px terminal and 10 px signature subsets, with retained Bitstream Vera terms | | `components/mybot_platform/src/drivers/display/adapters/lvgl/shared_lvgl_adapter.cc`, `components/mybot_platform/src/internal/display/display_panel.h` | Project-maintained shared adapter for ST7789, CO5300, SPD2010, and ST77916 panels | Uses the board panel drivers; no new upstream source | -| `components/mybot_platform/src/drivers/display/panels/generic/rgb565_spi_panel.c` GC9D01N and GC9309NA initialization profiles | Board-vendor hardware initialization tables | Only controller register values are adapted; the ESP LCD command driver, platform lifecycle and cleanup remain project-maintained or use the pinned Apache-2.0 `esp_lcd_gc9a01` component | +| `components/mybot_platform/src/drivers/display/panels/generic/rgb565_spi_panel.c` GC9D01N, GC9309NA and ST7796 initialization profiles; `generic/i80_st7789_panel.c` | Board hardware initialization values and ESP-IDF i80 API | Only controller register values are adapted; the ESP LCD command driver, platform lifecycle and cleanup remain project-maintained or use the pinned Apache-2.0 `esp_lcd_gc9a01` component | | `components/mybot_platform/boards/respeaker-flex-xvf3800-circular4-xiao`, `components/mybot_platform/src/drivers/audio/xvf3800_audio.c`, `partitions/v2/8m.csv` | `github.com/qiuyanli1990/respeaker-flex-circle-Agora-mybot` | commit `b06024382eb104c998aead4841e1df647193065b` | | `components/mybot_platform/boards/bread-compact-wifi-lcd/bread_input.c` | Existing local `components/mybot_platform/boards/atom-echos3r/atom_echos3r_input.c` input implementation | Project-local adaptation for Boot GPIO0 | | `components/mybot_platform/boards/esp-vocat/board_config.h` (hardware mapping verification only) | `github.com/espressif/esp-brookesia` | commit `b22c488f50bafe53342c8e171081bd736396ef58` | diff --git a/components/mybot_platform/boards/README.md b/components/mybot_platform/boards/README.md index ce44167..dccf578 100644 --- a/components/mybot_platform/boards/README.md +++ b/components/mybot_platform/boards/README.md @@ -5,7 +5,7 @@ drivers and build constraints are defined by the checked-in `board.cmake`, `boar `sdkconfig.defaults` files. Current validation status is centralized in [`docs/VALIDATION.md`](../../../docs/VALIDATION.md). -以下说明对应本工程已实现的 69 个板型,其中 62 个带屏、7 个无屏。硬件配置、输入行为和 +以下说明对应本工程已实现的 72 个板型,其中 64 个带屏、8 个无屏。硬件配置、输入行为和 构建约束以当前板级代码为准;验证状态统一见 [验证矩阵](../../../docs/VALIDATION.md)。不同尺寸、芯片或硬件版本的板卡不能仅凭外观 共用固件。 @@ -93,6 +93,9 @@ real-device validation. | [lilygo-t-cameraplus-s3-v1.2](lilygo-t-cameraplus-s3-v1.2/README.md) | 240×240 ST7789 | PDM microphone + MAX98357A | CST816D + Boot | V1.2 only; camera, PMIC, KEY1, SD, IR, sleep and shutdown are not implemented. | | [lilygo-t-circle-s3](lilygo-t-circle-s3/README.md) | 160×160 round GC9D01N | MSM261 + MAX98357A | CST816D + Boot | Rotary encoder, APA102, battery, sleep and shutdown are not implemented. | | [jiuchuan-s3](jiuchuan-s3/README.md) | 240×296 GC9309NA | ES8311 | Boot + volume buttons | Power-key actions, battery, charging, sleep and shutdown are not implemented. | +| [lilygo-t-display-s3-pro-mvsrlora](lilygo-t-display-s3-pro-mvsrlora/README.md) | 222×480 ST7796 | PDM capture + MAX98357A playback | CST226SE + Boot | SY6970 setup is implemented; SD, LoRa, vibration, RTC, IMU, battery and sleep are not. | +| [atk-dnesp32s3-box](atk-dnesp32s3-box/README.md) | 320×240 i80 ST7789 | ES8311 | Boot | XL9555/TCA9555 power setup is implemented; expander keys, camera, battery, dimming, sleep and shutdown are not. | +| [tudouzi](tudouzi/README.md) | Headless | ES8311 + ES7210 | Boot + two volume keys | Wi-Fi-only subset; SSD1306 UI, ML307, LED, battery, sleep and shutdown are not implemented. | ## Build, flash and validation / 构建、烧录与验证 diff --git a/components/mybot_platform/boards/atk-dnesp32s3-box/README.md b/components/mybot_platform/boards/atk-dnesp32s3-box/README.md new file mode 100644 index 0000000..2ec1e13 --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box/README.md @@ -0,0 +1,14 @@ +# ATK DNESP32S3 Box + +板型 ID 为 `atk-dnesp32s3-box`。 + +- 显示:320×240 ST7789,i80 8-bit 总线,10 MHz,共享 LVGL UI。 +- 音频:ES8311,16 kHz 单声道 signed-16 PCM。 +- 扩展器:TCA9555/XL9555 `0x20` 控制系统和功放供电。 +- 输入:GPIO0 Boot。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +仅支持检测到 ES8311 的硬件版本;NS4168 无 codec 音频变体未实现。扩展器按键、摄像头、电池、 +调光、休眠和关机未实现。i80 时序、ES8311、扩展器供电和面板方向仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/atk-dnesp32s3-box/atk_box_hardware.c b/components/mybot_platform/boards/atk-dnesp32s3-box/atk_box_hardware.c new file mode 100644 index 0000000..131d7d1 --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box/atk_box_hardware.c @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "driver/i2c_master.h" +#include "esp_err.h" +#include "esp_log.h" + +#include + +#define TAG "atk_box_hardware" +#define ATK_BOX_EXPANDER_ADDRESS 0x20 +#define ATK_BOX_I2C_TIMEOUT_MS 100 + +static esp_err_t write_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t value) { + const uint8_t command[] = {reg, value}; + return i2c_master_transmit(device, command, sizeof(command), ATK_BOX_I2C_TIMEOUT_MS); +} + +static esp_err_t read_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t *value) { + esp_err_t result = i2c_master_transmit(device, ®, sizeof(reg), ATK_BOX_I2C_TIMEOUT_MS); + if (result == ESP_OK) { + result = i2c_master_receive(device, value, 1, ATK_BOX_I2C_TIMEOUT_MS); + } + return result; +} + +int mybot_generic_board_extra_init(i2c_master_bus_handle_t bus) { + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = ATK_BOX_EXPANDER_ADDRESS, + .scl_speed_hz = 400000, + }; + i2c_master_dev_handle_t device = NULL; + esp_err_t result = i2c_master_bus_add_device(bus, &config, &device); + uint8_t output_low = 0; + + if (result == ESP_OK) { + result = write_register(device, 0x06, 0x3B); + } + if (result == ESP_OK) { + result = write_register(device, 0x07, 0xFE); + } + if (result == ESP_OK) { + result = write_register(device, 0x06, 0x1B); + } + if (result == ESP_OK) { + result = write_register(device, 0x07, 0xFE); + } + if (result == ESP_OK) { + result = read_register(device, 0x02, &output_low); + } + if (result == ESP_OK) { + result = write_register(device, 0x02, output_low | (uint8_t)((1U << 5) | (1U << 7))); + } + if (device) { + const esp_err_t remove_result = i2c_master_bus_rm_device(device); + if (result == ESP_OK) { + result = remove_result; + } + } + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=hardware action=initialize expander=0x20 result=error code=%s", + esp_err_to_name(result)); + return -1; + } + return 0; +} diff --git a/components/mybot_platform/boards/atk-dnesp32s3-box/board.c b/components/mybot_platform/boards/atk-dnesp32s3-box/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box/board.c @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "generic_board.h" + +const mybot_board_t *mybot_board_get(void) { + return mybot_generic_board_get(); +} diff --git a/components/mybot_platform/boards/atk-dnesp32s3-box/board.cmake b/components/mybot_platform/boards/atk-dnesp32s3-box/board.cmake new file mode 100644 index 0000000..9942bfe --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box/board.cmake @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "generic/i80_st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_EXTRA_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/atk_box_hardware.c") +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_16MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_OCT + CONFIG_SPIRAM_SPEED_80M + CONFIG_CODEC_ES8311_SUPPORT +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/atk-dnesp32s3-box/board_config.h b/components/mybot_platform/boards/atk-dnesp32s3-box/board_config.h new file mode 100644 index 0000000..bcc3b3d --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box/board_config.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_BOARD_CONFIG_H_ +#define MYBOT_BOARD_CONFIG_H_ + +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "driver/spi_common.h" + +#define MYBOT_BOARD_NAME "atk-dnesp32s3-box" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 1 +#define MYBOT_GENERIC_TOUCH_KIND 0 +#define MYBOT_GENERIC_EXTRA_INIT 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_NC +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_21 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_13 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_47 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_14 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_48 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_45 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#define MYBOT_ES8311_I2C_PORT MYBOT_GENERIC_I2C_PORT +#define MYBOT_ES8311_I2C_ADDRESS 0x30 +#define MYBOT_ES8311_INPUT_GAIN_DB 30.0f +#define MYBOT_ES8311_HARDWARE_VOLUME_SCALE_PERCENT 60 + +#define MYBOT_BOOT_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_NC +#define MYBOT_DISPLAY_SCLK GPIO_NUM_42 +#define MYBOT_DISPLAY_DC GPIO_NUM_2 +#define MYBOT_DISPLAY_CS GPIO_NUM_1 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 1 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 320 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY true +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#define MYBOT_DISPLAY_I80_RD GPIO_NUM_41 +#define MYBOT_DISPLAY_I80_D0 GPIO_NUM_40 +#define MYBOT_DISPLAY_I80_D1 GPIO_NUM_39 +#define MYBOT_DISPLAY_I80_D2 GPIO_NUM_38 +#define MYBOT_DISPLAY_I80_D3 GPIO_NUM_12 +#define MYBOT_DISPLAY_I80_D4 GPIO_NUM_11 +#define MYBOT_DISPLAY_I80_D5 GPIO_NUM_10 +#define MYBOT_DISPLAY_I80_D6 GPIO_NUM_9 +#define MYBOT_DISPLAY_I80_D7 GPIO_NUM_46 +#define MYBOT_DISPLAY_I80_CLOCK_HZ (10 * 1000 * 1000) +#define MYBOT_DISPLAY_ST7789_POST_INIT 1 +#define MYBOT_DISPLAY_ST7789_MADCTL 0x00 +#define MYBOT_DISPLAY_ST7789_COLMOD 0x65 + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/atk-dnesp32s3-box/sdkconfig.defaults b/components/mybot_platform/boards/atk-dnesp32s3-box/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box/sdkconfig.defaults @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/16m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_80M=y +CONFIG_CODEC_ES8311_SUPPORT=y +CONFIG_CODEC_I2C_BACKWARD_COMPATIBLE=n diff --git a/components/mybot_platform/boards/boards.cmake b/components/mybot_platform/boards/boards.cmake index 5946441..5641560 100644 --- a/components/mybot_platform/boards/boards.cmake +++ b/components/mybot_platform/boards/boards.cmake @@ -68,6 +68,9 @@ set(MYBOT_SUPPORTED_BOARDS "lilygo-t-cameraplus-s3-v1.2" "lilygo-t-circle-s3" "jiuchuan-s3" + "lilygo-t-display-s3-pro-mvsrlora" + "atk-dnesp32s3-box" + "tudouzi" ) set(_MYBOT_BOARDS_ROOT "${CMAKE_CURRENT_LIST_DIR}") @@ -211,6 +214,12 @@ function(mybot_resolve_board board) set(profile "${_MYBOT_BOARDS_ROOT}/lilygo-t-circle-s3/board.cmake") elseif(board STREQUAL "jiuchuan-s3") set(profile "${_MYBOT_BOARDS_ROOT}/jiuchuan-s3/board.cmake") + elseif(board STREQUAL "lilygo-t-display-s3-pro-mvsrlora") + set(profile "${_MYBOT_BOARDS_ROOT}/lilygo-t-display-s3-pro-mvsrlora/board.cmake") + elseif(board STREQUAL "atk-dnesp32s3-box") + set(profile "${_MYBOT_BOARDS_ROOT}/atk-dnesp32s3-box/board.cmake") + elseif(board STREQUAL "tudouzi") + set(profile "${_MYBOT_BOARDS_ROOT}/tudouzi/board.cmake") else() message(FATAL_ERROR "Unsupported MYBOT_BOARD: ${board}") endif() diff --git a/components/mybot_platform/boards/common/generic_profile.cmake b/components/mybot_platform/boards/common/generic_profile.cmake index b604ef2..d2e06e4 100644 --- a/components/mybot_platform/boards/common/generic_profile.cmake +++ b/components/mybot_platform/boards/common/generic_profile.cmake @@ -61,6 +61,8 @@ if(MYBOT_GENERIC_TOUCH) list(APPEND MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_touch esp_lcd_touch_gt911) + elseif(MYBOT_GENERIC_TOUCH_DRIVER STREQUAL "cst226") + # CST226 uses the project-owned direct I2C protocol in cst816s_boot_input.c. else() message(FATAL_ERROR "Unknown MYBOT_GENERIC_TOUCH_DRIVER") endif() diff --git a/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/README.md b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/README.md new file mode 100644 index 0000000..4550e8d --- /dev/null +++ b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/README.md @@ -0,0 +1,16 @@ +# LilyGO T-Display-S3-Pro MVSRLoRa + +板型 ID 为 `lilygo-t-display-s3-pro-mvsrlora`。 + +- 显示:222×480 ST7796,SPI3 mode 0,40 MHz,X 偏移 49,共享 LVGL UI。 +- 音频:MP34DT05TR PDM 采集和 MAX98357A 播放,16 kHz 单声道 signed-16 PCM。 +- 触摸:CST226SE,I2C0 GPIO5/GPIO6,地址 `0x5A`,复位 GPIO13。 +- 输入:GPIO0 Boot;GPIO41 控制扬声器使能。 +- 电源:SY6970 `0x6A` 关闭 ILIM、启用 ADC 并关闭看门狗。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO48,默认 60%。 + +SD 卡、LoRa、振动、RTC、IMU、电池和休眠未实现。ST7796、CST226SE、SY6970、PDM 麦克风、 +扬声器使能和面板方向仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board.c b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board.c @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "generic_board.h" + +const mybot_board_t *mybot_board_get(void) { + return mybot_generic_board_get(); +} diff --git a/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board.cmake b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board.cmake new file mode 100644 index 0000000..4af4a64 --- /dev/null +++ b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board.cmake @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "raw") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst226") +set(MYBOT_GENERIC_EXTRA_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/lilygo_tdisplay_hardware.c") +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_16MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_OCT + CONFIG_SPIRAM_SPEED_80M +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_gc9a01) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board_config.h b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board_config.h new file mode 100644 index 0000000..3dd606b --- /dev/null +++ b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/board_config.h @@ -0,0 +1,72 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_BOARD_CONFIG_H_ +#define MYBOT_BOARD_CONFIG_H_ + +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "driver/spi_common.h" + +#define MYBOT_BOARD_NAME "lilygo-t-display-s3-pro-mvsrlora" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 2 +#define MYBOT_GENERIC_TOUCH_KIND 1 +#define MYBOT_GENERIC_EXTRA_INIT 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_1 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_15 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_11 +#define MYBOT_AUDIO_I2S_CAPTURE_PDM 1 +#define MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS 1 +#define MYBOT_AUDIO_I2S_CONTROL_SPEAKER_POWER 1 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_5 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_6 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_41 + +#define MYBOT_BOOT_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_TOUCH_DRIVER_KIND 4 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_13 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_21 +#define MYBOT_TOUCH_RESET_LEVEL 0 +#define MYBOT_TOUCH_INTERRUPT_LEVEL 0 +#define MYBOT_TOUCH_SWAP_XY 0 +#define MYBOT_TOUCH_MIRROR_X 0 +#define MYBOT_TOUCH_MIRROR_Y 0 +#define MYBOT_TOUCH_LONG_PRESS_MS 3000 +#define MYBOT_TOUCH_SHORT_PRESS_MIN_MS 50 +#define MYBOT_TOUCH_POLL_INTERVAL_MS 20 + +#define MYBOT_PANEL_CONTROLLER 6 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_17 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_18 +#define MYBOT_DISPLAY_DC GPIO_NUM_9 +#define MYBOT_DISPLAY_CS GPIO_NUM_39 +#define MYBOT_DISPLAY_RESET GPIO_NUM_47 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_48 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 222 +#define MYBOT_DISPLAY_HEIGHT 480 +#define MYBOT_DISPLAY_OFFSET_X 49 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/lilygo_tdisplay_hardware.c b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/lilygo_tdisplay_hardware.c new file mode 100644 index 0000000..17aac26 --- /dev/null +++ b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/lilygo_tdisplay_hardware.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "driver/i2c_master.h" +#include "esp_err.h" +#include "esp_log.h" + +#include + +#define TAG "lilygo_tdisplay_hardware" +#define SY6970_ADDRESS 0x6A +#define SY6970_TIMEOUT_MS 100 + +static esp_err_t write_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t value) { + const uint8_t command[] = {reg, value}; + return i2c_master_transmit(device, command, sizeof(command), SY6970_TIMEOUT_MS); +} + +int mybot_generic_board_extra_init(i2c_master_bus_handle_t bus) { + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = SY6970_ADDRESS, + .scl_speed_hz = 400000, + }; + i2c_master_dev_handle_t device = NULL; + esp_err_t result = i2c_master_bus_add_device(bus, &config, &device); + if (result == ESP_OK) { + result = write_register(device, 0x00, 0x08); + } + if (result == ESP_OK) { + result = write_register(device, 0x02, 0xDD); + } + if (result == ESP_OK) { + result = write_register(device, 0x07, 0x8D); + } + if (device) { + const esp_err_t remove_result = i2c_master_bus_rm_device(device); + if (result == ESP_OK) { + result = remove_result; + } + } + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=power action=initialize pmic=sy6970 result=error code=%s", + esp_err_to_name(result)); + return -1; + } + return 0; +} diff --git a/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/sdkconfig.defaults b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/sdkconfig.defaults new file mode 100644 index 0000000..214c8f7 --- /dev/null +++ b/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/sdkconfig.defaults @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/16m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_80M=y diff --git a/components/mybot_platform/boards/tudouzi/README.md b/components/mybot_platform/boards/tudouzi/README.md new file mode 100644 index 0000000..139d5ab --- /dev/null +++ b/components/mybot_platform/boards/tudouzi/README.md @@ -0,0 +1,14 @@ +# Tudouzi + +板型 ID 为 `tudouzi`。 + +- 显示:当前为无屏 profile;128×64 SSD1306 单色 UI 未实现。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM,GPIO9 控制 PA。 +- 电源:AXP2101 `0x34` 配置 ALDO2、充电参数和系统欠压阈值。 +- 输入:GPIO0 主按键,GPIO1/GPIO2 音量加减。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +仅启用 Wi-Fi;ML307/4G、SSD1306、LED、电池状态、休眠和关机未实现。AXP2101、双 codec、 +PA 和按键仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/tudouzi/board.c b/components/mybot_platform/boards/tudouzi/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/tudouzi/board.c @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "generic_board.h" + +const mybot_board_t *mybot_board_get(void) { + return mybot_generic_board_get(); +} diff --git a/components/mybot_platform/boards/tudouzi/board.cmake b/components/mybot_platform/boards/tudouzi/board.cmake new file mode 100644 index 0000000..634671d --- /dev/null +++ b/components/mybot_platform/boards/tudouzi/board.cmake @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "dual") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_EXTRA_SOURCES "${CMAKE_CURRENT_LIST_DIR}/tudouzi_hardware.c") +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_16MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_OCT + CONFIG_SPIRAM_SPEED_80M + CONFIG_CODEC_ES8311_SUPPORT + CONFIG_CODEC_ES7210_SUPPORT +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/tudouzi/board_config.h b/components/mybot_platform/boards/tudouzi/board_config.h new file mode 100644 index 0000000..38e2f9f --- /dev/null +++ b/components/mybot_platform/boards/tudouzi/board_config.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_BOARD_CONFIG_H_ +#define MYBOT_BOARD_CONFIG_H_ + +#include "driver/gpio.h" +#include "driver/i2c_master.h" + +#define MYBOT_BOARD_NAME "tudouzi" +#define MYBOT_BOARD_HAS_DISPLAY 0 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 +#define MYBOT_GENERIC_EXTRA_INIT 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_40 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_47 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_48 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_42 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_41 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_9 + +#define MYBOT_AMOLED175_I2C_PORT MYBOT_GENERIC_I2C_PORT +#define MYBOT_AMOLED175_ES8311_ADDRESS 0x30 +#define MYBOT_AMOLED175_ES7210_ADDRESS 0x80 + +#define MYBOT_BOOT_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_1 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_2 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/tudouzi/sdkconfig.defaults b/components/mybot_platform/boards/tudouzi/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/tudouzi/sdkconfig.defaults @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/16m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_80M=y +CONFIG_CODEC_ES8311_SUPPORT=y +CONFIG_CODEC_ES7210_SUPPORT=y +CONFIG_CODEC_I2C_BACKWARD_COMPATIBLE=n diff --git a/components/mybot_platform/boards/tudouzi/tudouzi_hardware.c b/components/mybot_platform/boards/tudouzi/tudouzi_hardware.c new file mode 100644 index 0000000..e229b59 --- /dev/null +++ b/components/mybot_platform/boards/tudouzi/tudouzi_hardware.c @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "driver/i2c_master.h" +#include "esp_err.h" +#include "esp_log.h" + +#include +#include + +#define TAG "tudouzi_hardware" +#define AXP2101_ADDRESS 0x34 +#define AXP2101_TIMEOUT_MS 100 + +typedef struct { + uint8_t reg; + uint8_t value; +} register_value_t; + +static esp_err_t write_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t value) { + const uint8_t command[] = {reg, value}; + return i2c_master_transmit(device, command, sizeof(command), AXP2101_TIMEOUT_MS); +} + +static esp_err_t read_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t *value) { + esp_err_t result = i2c_master_transmit(device, ®, sizeof(reg), AXP2101_TIMEOUT_MS); + if (result == ESP_OK) { + result = i2c_master_receive(device, value, 1, AXP2101_TIMEOUT_MS); + } + return result; +} + +int mybot_generic_board_extra_init(i2c_master_bus_handle_t bus) { + static const register_value_t registers[] = { + {0x22, 0x06}, {0x27, 0x10}, {0x93, 0x1C}, {0x64, 0x03}, {0x61, 0x05}, {0x62, 0x0A}, + {0x63, 0x15}, {0x14, 0x00}, {0x15, 0x00}, {0x16, 0x05}, {0x24, 0x01}, {0x50, 0x14}, + }; + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = AXP2101_ADDRESS, + .scl_speed_hz = 400000, + }; + i2c_master_dev_handle_t device = NULL; + esp_err_t result = i2c_master_bus_add_device(bus, &config, &device); + for (size_t index = 0; result == ESP_OK && index < sizeof(registers) / sizeof(registers[0]); + ++index) { + result = write_register(device, registers[index].reg, registers[index].value); + } + uint8_t power_state = 0; + if (result == ESP_OK) { + result = read_register(device, 0x90, &power_state); + } + if (result == ESP_OK) { + result = write_register(device, 0x90, power_state | 0x02); + } + if (device) { + const esp_err_t remove_result = i2c_master_bus_rm_device(device); + if (result == ESP_OK) { + result = remove_result; + } + } + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=power action=initialize pmic=axp2101 result=error code=%s", + esp_err_to_name(result)); + return -1; + } + return 0; +} diff --git a/components/mybot_platform/src/drivers/display/panels/generic/i80_st7789_panel.c b/components/mybot_platform/src/drivers/display/panels/generic/i80_st7789_panel.c new file mode 100644 index 0000000..1f1e1f4 --- /dev/null +++ b/components/mybot_platform/src/drivers/display/panels/generic/i80_st7789_panel.c @@ -0,0 +1,196 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "board_config.h" +#include "display/board_backlight.h" +#include "display/display_panel.h" + +#include "driver/gpio.h" +#include "esp_err.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_log.h" + +#include +#include + +#define TAG "i80_st7789_panel" + +#ifndef MYBOT_DISPLAY_I80_RD +#define MYBOT_DISPLAY_I80_RD GPIO_NUM_NC +#endif + +#ifndef MYBOT_DISPLAY_ST7789_POST_INIT +#define MYBOT_DISPLAY_ST7789_POST_INIT 0 +#endif + +static esp_lcd_i80_bus_handle_t s_i80_bus; + +int mybot_display_panel_close(mybot_display_panel_t *panel) { + if (!panel) { + return -1; + } + if (panel->backlight_ready && mybot_board_set_display_backlight(0) < 0) { + return -1; + } + panel->backlight_ready = false; + if (panel->panel) { + if (panel->ready && esp_lcd_panel_disp_on_off(panel->panel, false) != ESP_OK) { + ESP_LOGW(TAG, "event=panel action=display_off result=error"); + } + if (esp_lcd_panel_del(panel->panel) != ESP_OK) { + return -1; + } + panel->panel = NULL; + } + if (panel->io) { + if (esp_lcd_panel_io_del(panel->io) != ESP_OK) { + return -1; + } + panel->io = NULL; + } + if (s_i80_bus) { + if (esp_lcd_del_i80_bus(s_i80_bus) != ESP_OK) { + return -1; + } + s_i80_bus = NULL; + } + panel->ready = false; + panel->x_alignment = 0; + panel->y_alignment = 0; + return 0; +} + +static esp_err_t prepare_rd_pin(void) { + if (MYBOT_DISPLAY_I80_RD == GPIO_NUM_NC) { + return ESP_OK; + } + const gpio_config_t config = { + .pin_bit_mask = 1ULL << MYBOT_DISPLAY_I80_RD, + .mode = GPIO_MODE_INPUT_OUTPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + const esp_err_t result = gpio_config(&config); + return result == ESP_OK ? gpio_set_level(MYBOT_DISPLAY_I80_RD, 1) : result; +} + +static esp_err_t send_post_init_commands(mybot_display_panel_t *panel) { +#if MYBOT_DISPLAY_ST7789_POST_INIT + const uint8_t madctl = MYBOT_DISPLAY_ST7789_MADCTL; + const uint8_t colmod = MYBOT_DISPLAY_ST7789_COLMOD; + esp_err_t result = esp_lcd_panel_io_tx_param(panel->io, 0x36, &madctl, 1); + if (result == ESP_OK) { + result = esp_lcd_panel_io_tx_param(panel->io, 0x3A, &colmod, 1); + } + return result; +#else + (void)panel; + return ESP_OK; +#endif +} + +int mybot_display_panel_open(mybot_display_panel_t *panel) { + if (!panel || panel->ready || mybot_display_panel_close(panel) < 0 || + mybot_board_set_display_backlight(0) < 0) { + return -1; + } + panel->backlight_ready = true; + + esp_err_t result = prepare_rd_pin(); + if (result != ESP_OK) { + goto fail; + } + const esp_lcd_i80_bus_config_t bus_config = { + .dc_gpio_num = MYBOT_DISPLAY_DC, + .wr_gpio_num = MYBOT_DISPLAY_SCLK, + .clk_src = LCD_CLK_SRC_DEFAULT, + .data_gpio_nums = + { + MYBOT_DISPLAY_I80_D0, + MYBOT_DISPLAY_I80_D1, + MYBOT_DISPLAY_I80_D2, + MYBOT_DISPLAY_I80_D3, + MYBOT_DISPLAY_I80_D4, + MYBOT_DISPLAY_I80_D5, + MYBOT_DISPLAY_I80_D6, + MYBOT_DISPLAY_I80_D7, + }, + .bus_width = 8, + .max_transfer_bytes = MYBOT_DISPLAY_WIDTH * MYBOT_DISPLAY_HEIGHT * sizeof(uint16_t), + .dma_burst_size = 64, + }; + result = esp_lcd_new_i80_bus(&bus_config, &s_i80_bus); + if (result != ESP_OK) { + goto fail; + } + + const esp_lcd_panel_io_i80_config_t io_config = { + .cs_gpio_num = MYBOT_DISPLAY_CS, + .pclk_hz = MYBOT_DISPLAY_I80_CLOCK_HZ, + .trans_queue_depth = 2, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .dc_levels = + { + .dc_idle_level = 0, + .dc_cmd_level = 0, + .dc_dummy_level = 0, + .dc_data_level = 1, + }, + }; + result = esp_lcd_new_panel_io_i80(s_i80_bus, &io_config, &panel->io); + if (result != ESP_OK) { + goto fail; + } + + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = MYBOT_DISPLAY_RESET, + .rgb_ele_order = MYBOT_DISPLAY_RGB_ORDER, + .bits_per_pixel = 16, + }; + result = esp_lcd_new_panel_st7789(panel->io, &panel_config, &panel->panel); + if (result == ESP_OK) { + result = esp_lcd_panel_reset(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_init(panel->panel); + } + if (result == ESP_OK) { + result = + esp_lcd_panel_set_gap(panel->panel, MYBOT_DISPLAY_OFFSET_X, MYBOT_DISPLAY_OFFSET_Y); + } + if (result == ESP_OK) { + result = esp_lcd_panel_invert_color(panel->panel, MYBOT_DISPLAY_INVERT_COLOR); + } + if (result == ESP_OK) { + result = send_post_init_commands(panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_swap_xy(panel->panel, MYBOT_DISPLAY_SWAP_XY); + } + if (result == ESP_OK) { + result = esp_lcd_panel_mirror(panel->panel, MYBOT_DISPLAY_MIRROR_X, MYBOT_DISPLAY_MIRROR_Y); + } + if (result == ESP_OK) { + result = esp_lcd_panel_disp_on_off(panel->panel, true); + } + if (result == ESP_OK) { + result = mybot_board_set_display_backlight(MYBOT_DISPLAY_BRIGHTNESS_PERCENT) == 0 + ? ESP_OK + : ESP_FAIL; + } + if (result != ESP_OK) { + goto fail; + } + panel->ready = true; + panel->x_alignment = 1; + panel->y_alignment = 1; + return 0; + +fail: + ESP_LOGE(TAG, "event=panel action=initialize bus=i80 result=error code=%s", + esp_err_to_name(result)); + (void)mybot_display_panel_close(panel); + return -1; +} diff --git a/components/mybot_platform/src/drivers/display/panels/generic/rgb565_spi_panel.c b/components/mybot_platform/src/drivers/display/panels/generic/rgb565_spi_panel.c index 81aa99c..fde2225 100644 --- a/components/mybot_platform/src/drivers/display/panels/generic/rgb565_spi_panel.c +++ b/components/mybot_platform/src/drivers/display/panels/generic/rgb565_spi_panel.c @@ -8,7 +8,8 @@ #include "esp_lcd_io_spi.h" #include "esp_log.h" -#if MYBOT_PANEL_CONTROLLER == 1 || MYBOT_PANEL_CONTROLLER == 4 || MYBOT_PANEL_CONTROLLER == 5 +#if MYBOT_PANEL_CONTROLLER == 1 || MYBOT_PANEL_CONTROLLER == 4 || MYBOT_PANEL_CONTROLLER == 5 || \ + MYBOT_PANEL_CONTROLLER == 6 #include "esp_lcd_gc9a01.h" #elif MYBOT_PANEL_CONTROLLER == 2 #include "esp_lcd_nv3023.h" @@ -192,6 +193,28 @@ static const gc9a01_lcd_init_cmd_t s_gc9309na_init[] = { {0x29, NULL, 0, 0}, {0x2C, NULL, 0, 10}, }; +#elif MYBOT_PANEL_CONTROLLER == 6 +static const gc9a01_lcd_init_cmd_t s_st7796_init[] = { + {0x3A, (uint8_t[]){0x05}, 1, 0}, + {0xF0, (uint8_t[]){0xC3}, 1, 0}, + {0xF0, (uint8_t[]){0x96}, 1, 0}, + {0xB4, (uint8_t[]){0x01}, 1, 0}, + {0xB7, (uint8_t[]){0xC6}, 1, 0}, + {0xE8, (uint8_t[]){0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8, 0}, + {0xC1, (uint8_t[]){0x06}, 1, 0}, + {0xC2, (uint8_t[]){0xA7}, 1, 0}, + {0xC5, (uint8_t[]){0x18}, 1, 0}, + {0xE0, + (uint8_t[]){0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, + 0x1B}, + 14, 0}, + {0xE1, + (uint8_t[]){0xF0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2D, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, + 0x1B}, + 14, 0}, + {0xF0, (uint8_t[]){0x3C}, 1, 0}, + {0xF0, (uint8_t[]){0x69}, 1, 0}, +}; #endif int mybot_display_panel_close(mybot_display_panel_t *panel) { @@ -303,6 +326,13 @@ int mybot_display_panel_open(mybot_display_panel_t *panel) { }; panel_config.vendor_config = (void *)&vendor_config; result = esp_lcd_new_panel_gc9a01(panel->io, &panel_config, &panel->panel); +#elif MYBOT_PANEL_CONTROLLER == 6 + const gc9a01_vendor_config_t vendor_config = { + .init_cmds = s_st7796_init, + .init_cmds_size = sizeof(s_st7796_init) / sizeof(s_st7796_init[0]), + }; + panel_config.vendor_config = (void *)&vendor_config; + result = esp_lcd_new_panel_gc9a01(panel->io, &panel_config, &panel->panel); #endif if (result == ESP_OK) { result = esp_lcd_panel_reset(panel->panel); diff --git a/components/mybot_platform/src/drivers/input/cst816s_boot_input.c b/components/mybot_platform/src/drivers/input/cst816s_boot_input.c index 81e6ad9..32ffdfd 100644 --- a/components/mybot_platform/src/drivers/input/cst816s_boot_input.c +++ b/components/mybot_platform/src/drivers/input/cst816s_boot_input.c @@ -5,19 +5,24 @@ #include #include "button_gpio.h" -#include "esp_lcd_touch.h" #ifndef MYBOT_TOUCH_DRIVER_KIND #define MYBOT_TOUCH_DRIVER_KIND 1 #endif +#if MYBOT_TOUCH_DRIVER_KIND == 4 +#include "driver/gpio.h" +#else +#include "esp_lcd_touch.h" +#endif + #if MYBOT_TOUCH_DRIVER_KIND == 1 #include "esp_lcd_touch_cst816s.h" #elif MYBOT_TOUCH_DRIVER_KIND == 2 #include "esp_lcd_touch_ft5x06.h" #elif MYBOT_TOUCH_DRIVER_KIND == 3 #include "esp_lcd_touch_gt911.h" -#else +#elif MYBOT_TOUCH_DRIVER_KIND != 4 #error "Unsupported MYBOT_TOUCH_DRIVER_KIND" #endif @@ -53,8 +58,12 @@ typedef struct { mybot_key_event_handler_t emit; void *user_data; button_handle_t boot; +#if MYBOT_TOUCH_DRIVER_KIND == 4 + i2c_master_dev_handle_t raw_touch; +#else esp_lcd_touch_handle_t touch; esp_lcd_panel_io_handle_t touch_io; +#endif TaskHandle_t touch_task; } input_context_t; @@ -66,6 +75,29 @@ static SemaphoreHandle_t s_task_stopped; i2c_master_bus_handle_t mybot_board_touch_i2c_bus_handle(void); +#if MYBOT_TOUCH_DRIVER_KIND == 4 +#define CST226_ADDRESS 0x5A +#define CST226_I2C_TIMEOUT_MS 100 + +static int read_cst226_touch(bool *down) { + const uint8_t command = 0x00; + uint8_t data[6] = {0}; + esp_err_t result = + i2c_master_transmit(s_context.raw_touch, &command, sizeof(command), CST226_I2C_TIMEOUT_MS); + if (result == ESP_OK) { + result = i2c_master_receive(s_context.raw_touch, data, sizeof(data), CST226_I2C_TIMEOUT_MS); + } + if (result != ESP_OK) { + return -1; + } + const int x = ((int)data[1] << 4) | (data[3] >> 4); + const int y = ((int)data[2] << 4) | (data[3] & 0x0F); + *down = (data[5] & 0x0F) != 0 && x >= 0 && x < MYBOT_DISPLAY_WIDTH && y >= 0 && + y < MYBOT_DISPLAY_HEIGHT; + return 0; +} +#endif + static bool adapter_is_active(void) { portENTER_CRITICAL(&s_context.lock); const bool active = s_context.active; @@ -141,12 +173,17 @@ static void touch_task(void *argument) { TickType_t pressed_at = 0; while (!task_should_stop()) { +#if MYBOT_TOUCH_DRIVER_KIND == 4 + bool down = false; + (void)read_cst226_touch(&down); +#else esp_lcd_touch_point_data_t point = {0}; uint8_t count = 0; const bool valid = esp_lcd_touch_read_data(s_context.touch) == ESP_OK && esp_lcd_touch_get_data(s_context.touch, &point, &count, 1) == ESP_OK; const bool down = valid && count > 0 && point.x <= MYBOT_DISPLAY_WIDTH && point.y <= MYBOT_DISPLAY_HEIGHT; +#endif const TickType_t now = xTaskGetTickCount(); if (down && !pressed) { @@ -183,6 +220,35 @@ static int create_touch(void) { return -1; } +#if MYBOT_TOUCH_DRIVER_KIND == 4 + if (MYBOT_TOUCH_RESET != GPIO_NUM_NC) { + const gpio_config_t reset_config = { + .pin_bit_mask = 1ULL << MYBOT_TOUCH_RESET, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t result = gpio_config(&reset_config); + if (result == ESP_OK) { + result = gpio_set_level(MYBOT_TOUCH_RESET, MYBOT_TOUCH_RESET_LEVEL); + } + if (result != ESP_OK) { + return -1; + } + vTaskDelay(pdMS_TO_TICKS(10)); + if (gpio_set_level(MYBOT_TOUCH_RESET, !MYBOT_TOUCH_RESET_LEVEL) != ESP_OK) { + return -1; + } + vTaskDelay(pdMS_TO_TICKS(50)); + } + const i2c_device_config_t device_config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = CST226_ADDRESS, + .scl_speed_hz = MYBOT_TOUCH_I2C_SPEED_HZ, + }; + return i2c_master_bus_add_device(bus, &device_config, &s_context.raw_touch) == ESP_OK ? 0 : -1; +#else #if MYBOT_TOUCH_DRIVER_KIND == 1 esp_lcd_panel_io_i2c_config_t io_config = { .dev_addr = ESP_LCD_TOUCH_IO_I2C_CST816S_ADDRESS, @@ -235,6 +301,7 @@ static int create_touch(void) { return -1; } return 0; +#endif } static int release_input_devices(void) { @@ -244,6 +311,13 @@ static int release_input_devices(void) { } else if (s_context.boot) { result = -1; } +#if MYBOT_TOUCH_DRIVER_KIND == 4 + if (s_context.raw_touch && i2c_master_bus_rm_device(s_context.raw_touch) == ESP_OK) { + s_context.raw_touch = NULL; + } else if (s_context.raw_touch) { + result = -1; + } +#else if (s_context.touch && esp_lcd_touch_del(s_context.touch) == ESP_OK) { s_context.touch = NULL; } else if (s_context.touch) { @@ -255,6 +329,7 @@ static int release_input_devices(void) { } else if (!s_context.touch && s_context.touch_io) { result = -1; } +#endif return result; } @@ -284,8 +359,13 @@ static int release_input(void) { portENTER_CRITICAL(&s_context.lock); s_context.emit = NULL; s_context.user_data = NULL; - s_context.started = - s_context.boot || s_context.touch || s_context.touch_io || s_context.touch_task; + s_context.started = s_context.boot || +#if MYBOT_TOUCH_DRIVER_KIND == 4 + s_context.raw_touch || +#else + s_context.touch || s_context.touch_io || +#endif + s_context.touch_task; if (!s_context.started) { s_context.task_stop_requested = false; } @@ -295,8 +375,13 @@ static int release_input(void) { int mybot_cst816s_input_start(void) { portENTER_CRITICAL(&s_context.lock); - const bool already_started = s_context.started || s_context.boot || s_context.touch || - s_context.touch_io || s_context.touch_task; + const bool already_started = s_context.started || s_context.boot || +#if MYBOT_TOUCH_DRIVER_KIND == 4 + s_context.raw_touch || +#else + s_context.touch || s_context.touch_io || +#endif + s_context.touch_task; portEXIT_CRITICAL(&s_context.lock); if (already_started) { return -1; diff --git a/docs/BOARD_PORTING.md b/docs/BOARD_PORTING.md index 6645a9e..7e70016 100644 --- a/docs/BOARD_PORTING.md +++ b/docs/BOARD_PORTING.md @@ -107,9 +107,8 @@ The bundled RTSA package supports only 60 ms audio frames. Although menuconfig l 6. Preserve the SDK audio boundary: 16 kHz, mono, signed 16-bit PCM, with frame counts rather than byte counts. 7. Select the appropriate display capability in `components/mybot_platform/CMakeLists.txt`: - `Kconfig.display` for LVGL or `Kconfig.headless` for a board without a panel. The current - headless selection covers AtomEchoS3R, Xingzhi Cube 0.96 OLED and ReSpeaker Flex; update it - when adding another headless board. + `Kconfig.display` for LVGL or `Kconfig.headless` for a board without a panel. The board index + lists the current headless profiles. 8. Add isolated CI builds for both languages and size reports, then record real-device provisioning, HTTPS, RTC, bidirectional audio, input, display, hangup, and repeated start/stop validation. @@ -197,7 +196,14 @@ display/audio family without that external reset. The shared SPI panel source can select a vendor-specific initialization table for controllers compatible with the pinned command interface. GC9D01N and GC9309NA profiles use custom command -tables while retaining the common SPI bus, flush, teardown and LVGL paths. +tables while retaining the common SPI bus, flush, teardown and LVGL paths. The ST7796 profile uses +the same command interface with a separate initialization table. The ATK Box uses the dedicated +`generic/i80_st7789_panel.c` lifecycle for an 8-bit Intel 8080 bus. + +CST226 profiles use the project-owned direct-I2C mode in `cst816s_boot_input.c`; they do not compile +the `esp_lcd_touch` wrapper. Board power callbacks such as SY6970, AXP2101 and the ATK Box +XL9555/TCA9555 sequence use bounded transfers and remove their temporary I2C device on every +success or failure path. `M5Stack StopWatch` uses M5PM1 and M5IOE1 before display and codec startup. The bridge raises LCD and codec power, resets LCD/touch, controls the speaker PA and leaves the motor off. The shared diff --git a/docs/BOARD_PORTING.zh-CN.md b/docs/BOARD_PORTING.zh-CN.md index 318bf6c..af8d6b3 100644 --- a/docs/BOARD_PORTING.zh-CN.md +++ b/docs/BOARD_PORTING.zh-CN.md @@ -105,8 +105,7 @@ RTSA 包前,SDK 构建封装会拒绝这些配置。当前构建与真机状 `components/mybot_stack/mybot_sdk/mybot` 添加 ESP-IDF 细节。 6. 保持 SDK 音频边界:16 kHz、单声道、signed 16-bit PCM,接口传帧数而不是字节数。 7. 在 `components/mybot_platform/CMakeLists.txt` 选择显示能力:带屏板型使用 - `Kconfig.display` 启用 LVGL,无屏板型使用 `Kconfig.headless`。当前无屏选择覆盖 - AtomEchoS3R、行者 Cube 0.96 OLED 与 ReSpeaker Flex,新增无屏板型时需同步此选择。 + `Kconfig.display` 启用 LVGL,无屏板型使用 `Kconfig.headless`。当前无屏板型统一列在板卡索引中。 8. 增加两种语言的隔离 CI 构建与尺寸报告,并记录真实设备的配网、HTTPS、RTC、双向音频、 输入、显示、挂断与重复启停验证。 @@ -124,7 +123,12 @@ ES8311+ES7210、ES8388、ES8374 和原始 I2S;原始采集可使用标准 I2S 音频 I2C 总线执行一次板级回调,回调必须使用有界超时并清理设备句柄。乐动掌控 V2 用它发送 STM8 面板复位命令;掌控板 V3 使用同系列显示与音频,但没有该外部复位步骤。 共享 SPI 面板源可为兼容的命令接口选择厂商初始化表。GC9D01N 与 GC9309NA profile 使用 -各自命令表,但仍复用公共 SPI、刷新、销毁和 LVGL 路径。 +各自命令表,但仍复用公共 SPI、刷新、销毁和 LVGL 路径。ST7796 profile 使用同一命令接口和 +独立初始化表;ATK Box 使用 `generic/i80_st7789_panel.c` 管理 8-bit Intel 8080 生命周期。 + +CST226 profile 使用 `cst816s_boot_input.c` 内项目自有的直连 I2C 模式,不编译 +`esp_lcd_touch` 封装。SY6970、AXP2101 与 ATK Box XL9555/TCA9555 等板级电源回调均使用有界 +传输,并在成功和失败路径移除临时 I2C 设备。 `M5Stack StopWatch` 在显示和 codec 启动前初始化 M5PM1 与 M5IOE1:打开 LCD/codec 电源, 复位 LCD/触摸,控制扬声器 PA,并默认关闭振动电机。共享圆屏布局把控件收窄到中央安全 diff --git a/docs/PLATFORM_UI.md b/docs/PLATFORM_UI.md index 4dc6f16..fe9a2f4 100644 --- a/docs/PLATFORM_UI.md +++ b/docs/PLATFORM_UI.md @@ -60,10 +60,12 @@ retain their existing board behavior. | `minsi-k08-dual` | ST7789, 240 × 240 | shared ST7789 adapter | Boot and volume buttons | | `magiclick-2p5` | GC9107 or ST7735, 128 × 128 | shared compact panel adapter | three buttons | | `atk-dnesp32s3-box3` | ST7789, 320 × 240 | shared ST7789 adapter | Boot | +| `lilygo-t-display-s3-pro-mvsrlora` | ST7796, 222 × 480 | shared panel adapter | CST226SE and Boot | +| `atk-dnesp32s3-box` | i80 ST7789, 320 × 240 | shared ST7789 adapter | Boot | +| `tudouzi` | Headless | none | Boot and two volume buttons | -The AtomEchoS3R, DOIT S3 AI Box, Bread Compact Wi-Fi, HU-087 and ReSpeaker Flex profiles have no -LCD and do not include LVGL. The ML307 and -Wi-Fi variants, along with the audio/video protocols, are independent of the display backend. +Headless profiles select `Kconfig.headless` and do not include LVGL. The ML307 and Wi-Fi variants, +along with the audio/video protocols, are independent of the display backend. The Xingzhi Cube uses SPI3 mode 3 at 40 MHz, zero offsets, and GPIO13 LEDC backlight with 5 kHz, 13-bit PWM at 60% duty. Bread Compact Wi-Fi LCD uses SPI3 mode 0 at 40 MHz, zero offsets, RGB order with color inversion, @@ -90,6 +92,10 @@ Minsi K08 uses ST7789 SPI3 mode 3 at 40 MHz, an 80-pixel X offset, swapped axes backlight. Magiclick 2.5 selects GC9107 or ST7735 from GPIO3 ADC and uses a GPIO13 LEDC backlight. ATK Box 3 uses ST7789 on SPI2 at 60 MHz with big-endian pixel data and AW9523B bit 8 as a binary backlight enable. +T-Display S3 Pro uses a 222 × 480 ST7796 panel on SPI3 at 40 MHz with a 49-pixel X gap and +CST226SE direct-I2C input. ATK DNESP32S3 Box uses an 8-bit i80 ST7789 bus at 10 MHz and leaves +the expander-owned power and backlight paths outside the panel driver. Both layouts are +build-tested only until panel orientation, color and touch alignment are checked on hardware. ## CoreS3 pixel-glasses view diff --git a/docs/PLATFORM_UI.zh-CN.md b/docs/PLATFORM_UI.zh-CN.md index a414f6c..3a27f02 100644 --- a/docs/PLATFORM_UI.zh-CN.md +++ b/docs/PLATFORM_UI.zh-CN.md @@ -58,10 +58,11 @@ | `minsi-k08-dual` | ST7789,240 × 240 | 通用 ST7789 | Boot 与音量键 | | `magiclick-2p5` | GC9107 或 ST7735,128 × 128 | 通用紧凑面板适配器 | 三个按键 | | `atk-dnesp32s3-box3` | ST7789,320 × 240 | 通用 ST7789 | Boot | +| `lilygo-t-display-s3-pro-mvsrlora` | ST7796,222 × 480 | 通用面板适配器 | CST226SE 与 Boot | +| `atk-dnesp32s3-box` | i80 ST7789,320 × 240 | 通用 ST7789 | Boot | +| `tudouzi` | 无屏 | 无 | Boot 与两个音量键 | -AtomEchoS3R、DOIT S3 AI Box、Bread Compact Wi-Fi、HU-087 与 ReSpeaker Flex 没有 LCD, -不编入 LVGL。ML307、Wi-Fi 及音视频协议与显示后端 -相互独立。 +无屏板型选择 `Kconfig.headless`,不编入 LVGL。ML307、Wi-Fi 及音视频协议与显示后端相互独立。 行者 Cube 使用 SPI3 mode 3 / 40 MHz、零偏移与 GPIO13 LEDC 背光,PWM 为 5 kHz、13-bit、 60% 占空比。 Bread Compact Wi-Fi LCD 使用 SPI3 mode 0 / 40 MHz、零偏移、RGB 顺序和颜色反转,以及 @@ -86,6 +87,9 @@ KEVIN SP V4 Dev 使用 ST7789 SPI3 mode 3 / 80 MHz、Y 偏移 20、颜色反转 Minsi K08 使用 ST7789 SPI3 mode 3 / 40 MHz、X 偏移 80、交换 X/Y 与 GPIO7 LEDC 背光。 Magiclick 2.5 通过 GPIO3 ADC 选择 GC9107 或 ST7735,并使用 GPIO13 LEDC 背光。 ATK Box 3 使用 SPI2 / 60 MHz 的 ST7789 与大端像素数据,并由 AW9523B bit 8 二值开关背光。 +T-Display S3 Pro 使用 SPI3 / 40 MHz 的 222 × 480 ST7796、49 像素 X 偏移和 CST226SE 直连 +I2C 输入。ATK DNESP32S3 Box 使用 10 MHz 的 8-bit i80 ST7789 总线,扩展器负责的电源与背光 +路径不在面板驱动内。两款布局目前只完成构建验证;面板方向、颜色和触摸坐标仍需真机确认。 ## CoreS3像素眼镜视图 diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 0624f91..2efe043 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -90,6 +90,9 @@ unsupported areas. Board READMEs describe hardware and operation only; the | `lilygo-t-cameraplus-s3-v1.2` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | V1.2 pin map, CST816D, ST7789, PDM microphone, speaker enable and backlight require device validation; camera and PMIC features are not implemented. | | `lilygo-t-circle-s3` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | GC9D01N initialization, 160×160 round orientation, CST816D, dual I2S audio and speaker enable require device validation; rotary encoder and APA102 are not implemented. | | `jiuchuan-s3` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | GC9309NA initialization, 240×296 orientation, ES8311, PA, GPIO5 power hold and three keys require device validation; power-key actions and battery reporting are not implemented. | +| `lilygo-t-display-s3-pro-mvsrlora` | CI: zh-CN, en-US; recorded merged build | Not completed / 未完成 | ST7796 timing and orientation, CST226SE mapping, SY6970 sequencing, PDM capture, speaker enable and backlight require device validation; SD, LoRa, vibration, RTC, IMU, battery and sleep are not implemented. | +| `atk-dnesp32s3-box` | CI: zh-CN, en-US; recorded merged build | Not completed / 未完成 | i80 ST7789 timing and orientation, ES8311 routing and XL9555/TCA9555 power sequencing require device validation; expander keys, camera, battery, dimming, sleep and shutdown are not implemented. The NS4168-only hardware variant is not supported. | +| `tudouzi` | CI: zh-CN, en-US; recorded merged build | Not completed / 未完成 | Headless Wi-Fi subset; AXP2101 sequencing, ES8311/ES7210 routing, PA and keys require device validation. SSD1306 UI, ML307/4G, LED, battery reporting, sleep and shutdown are not implemented. | ## Global boundaries / 全局边界 diff --git a/tests/board_power/mocks/driver/i2c_master.h b/tests/board_power/mocks/driver/i2c_master.h new file mode 100644 index 0000000..45b088a --- /dev/null +++ b/tests/board_power/mocks/driver/i2c_master.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef BOARD_POWER_I2C_MASTER_H_ +#define BOARD_POWER_I2C_MASTER_H_ + +#include +#include + +typedef int esp_err_t; +#define ESP_OK 0 +#define ESP_FAIL -1 + +typedef struct mock_i2c_bus *i2c_master_bus_handle_t; +typedef struct mock_i2c_device *i2c_master_dev_handle_t; + +typedef enum { + I2C_ADDR_BIT_LEN_7 = 0, +} i2c_addr_bit_len_t; + +typedef struct { + i2c_addr_bit_len_t dev_addr_length; + uint16_t device_address; + uint32_t scl_speed_hz; +} i2c_device_config_t; + +esp_err_t i2c_master_bus_add_device(i2c_master_bus_handle_t bus, const i2c_device_config_t *config, + i2c_master_dev_handle_t *out_device); +esp_err_t i2c_master_bus_rm_device(i2c_master_dev_handle_t device); +esp_err_t i2c_master_transmit(i2c_master_dev_handle_t device, const uint8_t *data, size_t size, + int timeout_ms); +esp_err_t i2c_master_receive(i2c_master_dev_handle_t device, uint8_t *data, size_t size, + int timeout_ms); + +#endif /* BOARD_POWER_I2C_MASTER_H_ */ diff --git a/tests/board_power/mocks/esp_err.h b/tests/board_power/mocks/esp_err.h new file mode 100644 index 0000000..bc3b792 --- /dev/null +++ b/tests/board_power/mocks/esp_err.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "driver/i2c_master.h" diff --git a/tests/board_power/mocks/esp_log.h b/tests/board_power/mocks/esp_log.h new file mode 100644 index 0000000..58f8e8e --- /dev/null +++ b/tests/board_power/mocks/esp_log.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef BOARD_POWER_ESP_LOG_H_ +#define BOARD_POWER_ESP_LOG_H_ + +#define ESP_LOGE(...) ((void)0) + +#endif /* BOARD_POWER_ESP_LOG_H_ */ diff --git a/tests/board_power/run.sh b/tests/board_power/run.sh new file mode 100755 index 0000000..b823cb1 --- /dev/null +++ b/tests/board_power/run.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +set -eu + +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +test_dir="$repo_root/tests/board_power" +test_build_dir=$(mktemp -d) +trap 'rm -r -- "$test_build_dir"' EXIT HUP INT TERM + +run_variant() { + name=$1 + define=$2 + source_file=$3 + "${CC:-cc}" -std=c11 -Wall -Wextra -Werror \ + -I"$test_dir/mocks" -D"$define=1" \ + "$test_dir/test_board_power.c" "$source_file" \ + -o "$test_build_dir/test-$name" + if command -v timeout >/dev/null 2>&1; then + timeout 20 "$test_build_dir/test-$name" + else + "$test_build_dir/test-$name" + fi +} + +run_variant sy6970 TEST_SY6970 \ + "$repo_root/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora/lilygo_tdisplay_hardware.c" +run_variant atk-box TEST_ATK_BOX \ + "$repo_root/components/mybot_platform/boards/atk-dnesp32s3-box/atk_box_hardware.c" +run_variant tudouzi TEST_TUDOUZI \ + "$repo_root/components/mybot_platform/boards/tudouzi/tudouzi_hardware.c" diff --git a/tests/board_power/test_board_power.c b/tests/board_power/test_board_power.c new file mode 100644 index 0000000..053110d --- /dev/null +++ b/tests/board_power/test_board_power.c @@ -0,0 +1,198 @@ +/* SPDX-License-Identifier: MIT */ +#include "driver/i2c_master.h" + +#include +#include +#include + +#if defined(TEST_SY6970) + defined(TEST_ATK_BOX) + defined(TEST_TUDOUZI) != 1 +#error "Select exactly one board power profile" +#endif + +int mybot_generic_board_extra_init(i2c_master_bus_handle_t bus); + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + exit(1); \ + } \ + } while (0) + +typedef struct { + uint8_t reg; + uint8_t value; +} write_record_t; + +struct mock_i2c_bus { + int unused; +}; + +struct mock_i2c_device { + bool open; +}; + +static struct mock_i2c_bus g_bus; +static struct mock_i2c_device g_device; +static write_record_t g_writes[16]; +static uint8_t g_read_registers[4]; +static unsigned int g_write_count; +static unsigned int g_read_count; +static unsigned int g_add_count; +static unsigned int g_remove_count; +static unsigned int g_write_attempts; +static unsigned int g_fail_write_at; +static bool g_fail_add; +static bool g_fail_read; +static bool g_fail_remove; + +#ifdef TEST_SY6970 +#define EXPECTED_ADDRESS 0x6A +#define EXPECTED_WRITES 3 +#define EXPECTED_READS 0 +#define EXPECTED_OUTPUT_READ 0 +static const write_record_t s_expected_writes[] = { + {0x00, 0x08}, + {0x02, 0xDD}, + {0x07, 0x8D}, +}; +#elif defined(TEST_ATK_BOX) +#define EXPECTED_ADDRESS 0x20 +#define EXPECTED_WRITES 5 +#define EXPECTED_READS 1 +#define EXPECTED_OUTPUT_READ 0x02 +static const write_record_t s_expected_writes[] = { + {0x06, 0x3B}, {0x07, 0xFE}, {0x06, 0x1B}, {0x07, 0xFE}, {0x02, 0xA4}, +}; +#else +#define EXPECTED_ADDRESS 0x34 +#define EXPECTED_WRITES 13 +#define EXPECTED_READS 1 +#define EXPECTED_OUTPUT_READ 0x90 +static const write_record_t s_expected_writes[] = { + {0x22, 0x06}, {0x27, 0x10}, {0x93, 0x1C}, {0x64, 0x03}, {0x61, 0x05}, + {0x62, 0x0A}, {0x63, 0x15}, {0x14, 0x00}, {0x15, 0x00}, {0x16, 0x05}, + {0x24, 0x01}, {0x50, 0x14}, {0x90, 0x16}, +}; +#endif + +static void reset_mock(void) { + g_device = (struct mock_i2c_device){0}; + g_write_count = 0; + g_read_count = 0; + g_add_count = 0; + g_remove_count = 0; + g_write_attempts = 0; + g_fail_write_at = 0; + g_fail_add = false; + g_fail_read = false; + g_fail_remove = false; +} + +const char *esp_err_to_name(esp_err_t error) { + return error == ESP_OK ? "ESP_OK" : "ESP_FAIL"; +} + +esp_err_t i2c_master_bus_add_device(i2c_master_bus_handle_t bus, const i2c_device_config_t *config, + i2c_master_dev_handle_t *out_device) { + CHECK(bus == &g_bus && config && out_device && !g_device.open); + CHECK(config->dev_addr_length == I2C_ADDR_BIT_LEN_7); + CHECK(config->device_address == EXPECTED_ADDRESS && config->scl_speed_hz == 400000); + ++g_add_count; + if (g_fail_add) { + return ESP_FAIL; + } + g_device.open = true; + *out_device = &g_device; + return ESP_OK; +} + +esp_err_t i2c_master_bus_rm_device(i2c_master_dev_handle_t device) { + CHECK(device == &g_device && device->open); + ++g_remove_count; + if (g_fail_remove) { + return ESP_FAIL; + } + g_device.open = false; + return ESP_OK; +} + +esp_err_t i2c_master_transmit(i2c_master_dev_handle_t device, const uint8_t *data, size_t size, + int timeout_ms) { + CHECK(device == &g_device && device->open && data && timeout_ms == 100); + if (size == 1) { + CHECK(g_read_count < sizeof(g_read_registers)); + g_read_registers[g_read_count++] = data[0]; + return ESP_OK; + } + CHECK(size == 2 && g_write_count < sizeof(g_writes) / sizeof(g_writes[0])); + ++g_write_attempts; + if (g_fail_write_at && g_write_attempts == g_fail_write_at) { + return ESP_FAIL; + } + g_writes[g_write_count++] = (write_record_t){.reg = data[0], .value = data[1]}; + return ESP_OK; +} + +esp_err_t i2c_master_receive(i2c_master_dev_handle_t device, uint8_t *data, size_t size, + int timeout_ms) { + CHECK(device == &g_device && device->open && data && size == 1 && timeout_ms == 100); + CHECK(g_read_count > 0 && g_read_registers[g_read_count - 1] == EXPECTED_OUTPUT_READ); + if (g_fail_read) { + return ESP_FAIL; + } +#ifdef TEST_ATK_BOX + *data = 0x04; +#elif defined(TEST_TUDOUZI) + *data = 0x14; +#else + *data = 0; +#endif + return ESP_OK; +} + +static void expect_success(void) { + CHECK(g_add_count == 1 && g_remove_count == 1 && !g_device.open); + CHECK(g_read_count == EXPECTED_READS); + CHECK(g_write_count == EXPECTED_WRITES); + for (unsigned int index = 0; index < EXPECTED_WRITES; ++index) { + CHECK(g_writes[index].reg == s_expected_writes[index].reg); + CHECK(g_writes[index].value == s_expected_writes[index].value); + } +} + +int main(void) { + reset_mock(); + CHECK(mybot_generic_board_extra_init(&g_bus) == 0); + expect_success(); + + reset_mock(); + g_fail_add = true; + CHECK(mybot_generic_board_extra_init(&g_bus) < 0); + CHECK(g_add_count == 1 && g_remove_count == 0 && g_write_count == 0 && !g_device.open); + + for (unsigned int failure = 1; failure <= EXPECTED_WRITES; ++failure) { + reset_mock(); + g_fail_write_at = failure; + CHECK(mybot_generic_board_extra_init(&g_bus) < 0); + CHECK(g_add_count == 1 && g_remove_count == 1 && !g_device.open); + } + + if (EXPECTED_READS) { + reset_mock(); + g_fail_read = true; + CHECK(mybot_generic_board_extra_init(&g_bus) < 0); + CHECK(g_add_count == 1 && g_remove_count == 1 && !g_device.open); + } + + reset_mock(); + g_fail_remove = true; + CHECK(mybot_generic_board_extra_init(&g_bus) < 0); + CHECK(g_add_count == 1 && g_remove_count == 1 && g_device.open); + + reset_mock(); + CHECK(mybot_generic_board_extra_init(&g_bus) == 0); + expect_success(); + puts("Board power and expander sequencing host tests passed"); + return 0; +} diff --git a/tests/cst226_input/run.sh b/tests/cst226_input/run.sh new file mode 100755 index 0000000..52a41f2 --- /dev/null +++ b/tests/cst226_input/run.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +set -eu + +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +test_dir="$repo_root/tests/cst226_input" +board_dir="$repo_root/components/mybot_platform/boards/lilygo-t-display-s3-pro-mvsrlora" +test_build_dir=$(mktemp -d) +trap 'rm -r -- "$test_build_dir"' EXIT HUP INT TERM + +"${CC:-cc}" -std=c11 -D_POSIX_C_SOURCE=200809L -Wall -Wextra -Werror -pthread \ + -I"$repo_root/tests/cst816s_input/mocks" -I"$board_dir" \ + "$test_dir/test_cst226_input.c" \ + "$repo_root/components/mybot_platform/src/drivers/input/cst816s_boot_input.c" \ + -o "$test_build_dir/test_cst226_input" + +if command -v timeout >/dev/null 2>&1; then + timeout 20 "$test_build_dir/test_cst226_input" +else + "$test_build_dir/test_cst226_input" +fi diff --git a/tests/cst226_input/test_cst226_input.c b/tests/cst226_input/test_cst226_input.c new file mode 100644 index 0000000..afa1a7a --- /dev/null +++ b/tests/cst226_input/test_cst226_input.c @@ -0,0 +1,354 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" + +#include "board_config.h" + +#include +#include +#include +#include +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + exit(1); \ + } \ + } while (0) + +struct mock_i2c_bus { + int unused; +}; + +struct mock_i2c_device { + bool open; +}; + +struct mock_button { + bool open; + button_callback_t click; + button_callback_t long_press; +}; + +struct mock_task { + pthread_t thread; + task_function_t function; + void *argument; +}; + +static struct mock_i2c_bus g_bus; +static struct mock_i2c_device g_device; +static struct mock_button g_button; +static bool g_gpio_configured; +static unsigned int g_gpio_levels[2]; +static unsigned int g_gpio_level_count; +static unsigned int g_add_count; +static unsigned int g_remove_count; +static unsigned int g_transmit_count; +static _Atomic unsigned int g_receive_count; +static _Atomic unsigned int g_down_reads; +static bool g_fail_add; +static bool g_fail_remove; +static bool g_fail_task_create; +static mybot_state_t g_state = MYBOT_STATE_STARTING; +static _Thread_local struct mock_task *g_current_task; + +static pthread_mutex_t g_event_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t g_event_condition = PTHREAD_COND_INITIALIZER; +static bool g_event_seen; + +static void reset_mock(void) { + g_device = (struct mock_i2c_device){0}; + g_button = (struct mock_button){0}; + g_gpio_configured = false; + g_gpio_level_count = 0; + g_add_count = 0; + g_remove_count = 0; + g_transmit_count = 0; + atomic_store(&g_receive_count, 0); + atomic_store(&g_down_reads, 0); + g_fail_add = false; + g_fail_remove = false; + g_fail_task_create = false; + g_state = MYBOT_STATE_STARTING; + pthread_mutex_lock(&g_event_lock); + g_event_seen = false; + pthread_mutex_unlock(&g_event_lock); +} + +const char *esp_err_to_name(esp_err_t error) { + return error == ESP_OK ? "ESP_OK" : "ESP_FAIL"; +} + +esp_err_t gpio_config(const gpio_config_t *config) { + CHECK(config && config->pin_bit_mask == 1ULL << MYBOT_TOUCH_RESET); + CHECK(config->mode == GPIO_MODE_OUTPUT); + CHECK(config->pull_up_en == GPIO_PULLUP_DISABLE); + CHECK(config->pull_down_en == GPIO_PULLDOWN_DISABLE); + CHECK(config->intr_type == GPIO_INTR_DISABLE); + CHECK(!g_gpio_configured); + g_gpio_configured = true; + return ESP_OK; +} + +esp_err_t gpio_set_level(gpio_num_t gpio, uint32_t level) { + CHECK(g_gpio_configured && gpio == MYBOT_TOUCH_RESET && level <= 1); + CHECK(g_gpio_level_count < sizeof(g_gpio_levels) / sizeof(g_gpio_levels[0])); + g_gpio_levels[g_gpio_level_count++] = level; + return ESP_OK; +} + +i2c_master_bus_handle_t mybot_board_touch_i2c_bus_handle(void) { + return &g_bus; +} + +esp_err_t i2c_master_bus_add_device(i2c_master_bus_handle_t bus, const i2c_device_config_t *config, + i2c_master_dev_handle_t *out_device) { + CHECK(bus == &g_bus && config && out_device && !g_device.open); + CHECK(config->dev_addr_length == I2C_ADDR_BIT_LEN_7); + CHECK(config->device_address == 0x5A && config->scl_speed_hz == MYBOT_TOUCH_I2C_SPEED_HZ); + ++g_add_count; + if (g_fail_add) { + return ESP_FAIL; + } + g_device.open = true; + *out_device = &g_device; + return ESP_OK; +} + +esp_err_t i2c_master_bus_rm_device(i2c_master_dev_handle_t device) { + CHECK(device == &g_device && device->open); + ++g_remove_count; + if (g_fail_remove) { + g_fail_remove = false; + return ESP_FAIL; + } + g_device.open = false; + return ESP_OK; +} + +esp_err_t i2c_master_transmit(i2c_master_dev_handle_t device, const uint8_t *data, size_t size, + int timeout_ms) { + CHECK(device == &g_device && device->open && data && size == 1 && data[0] == 0x00); + CHECK(timeout_ms == 100); + ++g_transmit_count; + return ESP_OK; +} + +esp_err_t i2c_master_receive(i2c_master_dev_handle_t device, uint8_t *data, size_t size, + int timeout_ms) { + CHECK(device == &g_device && device->open && data && size == 6 && timeout_ms == 100); + atomic_fetch_add(&g_receive_count, 1); + unsigned int down_reads = atomic_load(&g_down_reads); + if (down_reads) { + atomic_store(&g_down_reads, down_reads - 1); + data[1] = 0x01; + data[2] = 0x01; + data[3] = 0xF4; + data[5] = 0x01; + } else { + for (size_t index = 0; index < size; ++index) { + data[index] = 0; + } + } + return ESP_OK; +} + +esp_err_t iot_button_new_gpio_device(const button_config_t *config, + const button_gpio_config_t *gpio_config, + button_handle_t *out_button) { + CHECK(config && gpio_config && out_button && !g_button.open); + CHECK(config->long_press_time == MYBOT_BOOT_BUTTON_LONG_PRESS_MS); + CHECK(gpio_config->gpio_num == MYBOT_BOOT_BUTTON_GPIO); + g_button.open = true; + *out_button = &g_button; + return ESP_OK; +} + +esp_err_t iot_button_register_cb(button_handle_t button, button_event_t event, void *event_data, + button_callback_t callback, void *user_data) { + CHECK(button == &g_button && button->open && !event_data && callback && !user_data); + if (event == BUTTON_SINGLE_CLICK) { + button->click = callback; + } else { + CHECK(event == BUTTON_LONG_PRESS_START); + button->long_press = callback; + } + return ESP_OK; +} + +esp_err_t iot_button_delete(button_handle_t button) { + CHECK(button == &g_button && button->open); + button->open = false; + return ESP_OK; +} + +mybot_state_t mybot_get_state(void) { + return g_state; +} + +int mybot_board_handle_boot_long_press(void) { + return 0; +} + +static void *task_trampoline(void *argument) { + struct mock_task *task = argument; + g_current_task = task; + task->function(task->argument); + return NULL; +} + +int xTaskCreate(task_function_t task, const char *name, uint32_t stack_size, void *argument, + unsigned int priority, TaskHandle_t *out_task) { + (void)name; + CHECK(stack_size == 4096 && priority == 5); + if (g_fail_task_create) { + *out_task = NULL; + return 0; + } + struct mock_task *created = calloc(1, sizeof(*created)); + CHECK(created); + created->function = task; + created->argument = argument; + CHECK(pthread_create(&created->thread, NULL, task_trampoline, created) == 0); + CHECK(pthread_detach(created->thread) == 0); + *out_task = created; + return pdPASS; +} + +TaskHandle_t xTaskGetCurrentTaskHandle(void) { + return g_current_task; +} + +void vTaskDelete(TaskHandle_t task) { + CHECK(task == NULL || task == g_current_task); + if (g_current_task) { + free(g_current_task); + g_current_task = NULL; + } + pthread_exit(NULL); +} + +void vTaskDelay(TickType_t ticks) { + struct timespec delay = { + .tv_sec = ticks / 1000, + .tv_nsec = (long)(ticks % 1000) * 1000000L, + }; + nanosleep(&delay, NULL); +} + +TickType_t xTaskGetTickCount(void) { + struct timespec now; + CHECK(clock_gettime(CLOCK_MONOTONIC, &now) == 0); + return (TickType_t)((uint64_t)now.tv_sec * 1000U + (uint64_t)now.tv_nsec / 1000000U); +} + +SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *storage) { + CHECK(pthread_mutex_init(&storage->lock, NULL) == 0); + CHECK(pthread_cond_init(&storage->condition, NULL) == 0); + storage->given = false; + return storage; +} + +int xSemaphoreTake(SemaphoreHandle_t semaphore, TickType_t ticks) { + CHECK(semaphore); + pthread_mutex_lock(&semaphore->lock); + if (!ticks) { + const bool taken = semaphore->given; + semaphore->given = false; + pthread_mutex_unlock(&semaphore->lock); + return taken ? pdTRUE : 0; + } + CHECK(ticks == portMAX_DELAY); + while (!semaphore->given) { + CHECK(pthread_cond_wait(&semaphore->condition, &semaphore->lock) == 0); + } + semaphore->given = false; + pthread_mutex_unlock(&semaphore->lock); + return pdTRUE; +} + +int xSemaphoreGive(SemaphoreHandle_t semaphore) { + CHECK(semaphore); + pthread_mutex_lock(&semaphore->lock); + semaphore->given = true; + CHECK(pthread_cond_signal(&semaphore->condition) == 0); + pthread_mutex_unlock(&semaphore->lock); + return pdTRUE; +} + +int mybot_cst816s_input_start(void); +int mybot_cst816s_input_stop(void); +const mybot_key_ops_t *mybot_cst816s_input_ops(void); + +static void record_event(mybot_key_event_t event, void *user_data) { + CHECK(event == MYBOT_KEY_EVENT_CONVERSATION_START); + CHECK(user_data == &g_event_seen); + pthread_mutex_lock(&g_event_lock); + g_event_seen = true; + pthread_cond_broadcast(&g_event_condition); + pthread_mutex_unlock(&g_event_lock); +} + +static void wait_for_event(void) { + struct timespec deadline; + CHECK(clock_gettime(CLOCK_REALTIME, &deadline) == 0); + deadline.tv_sec += 2; + pthread_mutex_lock(&g_event_lock); + while (!g_event_seen) { + const int result = pthread_cond_timedwait(&g_event_condition, &g_event_lock, &deadline); + CHECK(result != ETIMEDOUT && result == 0); + } + pthread_mutex_unlock(&g_event_lock); +} + +static void test_start_failures(void) { + reset_mock(); + g_fail_add = true; + CHECK(mybot_cst816s_input_start() < 0); + CHECK(!g_button.open && !g_device.open && g_add_count == 1 && g_remove_count == 0); + + reset_mock(); + g_fail_task_create = true; + CHECK(mybot_cst816s_input_start() < 0); + CHECK(!g_button.open && !g_device.open && g_add_count == 1 && g_remove_count == 1); +} + +static void test_cleanup_retry(void) { + reset_mock(); + CHECK(mybot_cst816s_input_start() == 0); + CHECK(g_button.open && g_device.open); + CHECK(g_gpio_level_count == 2); + CHECK(g_gpio_levels[0] == MYBOT_TOUCH_RESET_LEVEL); + CHECK(g_gpio_levels[1] == !MYBOT_TOUCH_RESET_LEVEL); + g_fail_remove = true; + CHECK(mybot_cst816s_input_stop() < 0); + CHECK(!g_button.open && g_device.open); + CHECK(mybot_cst816s_input_stop() == 0); + CHECK(!g_button.open && !g_device.open); + CHECK(mybot_cst816s_input_stop() == 0); +} + +static void test_touch_short_press(void) { + reset_mock(); + g_state = MYBOT_STATE_READY; + atomic_store(&g_down_reads, 3); + CHECK(mybot_cst816s_input_start() == 0); + const mybot_key_ops_t *ops = mybot_cst816s_input_ops(); + void *context = NULL; + CHECK(ops && ops->init(&context, record_event, &g_event_seen) == 0 && context); + wait_for_event(); + CHECK(atomic_load(&g_receive_count) >= 4 && g_transmit_count >= 4); + ops->destroy(context); + CHECK(mybot_cst816s_input_stop() == 0); + CHECK(!g_button.open && !g_device.open); +} + +int main(void) { + test_start_failures(); + test_cleanup_retry(); + test_touch_short_press(); + puts("CST226 direct-I2C input host tests passed"); + return 0; +} diff --git a/tests/cst816s_input/mocks/mock_platform.h b/tests/cst816s_input/mocks/mock_platform.h index 2847300..f9bd89c 100644 --- a/tests/cst816s_input/mocks/mock_platform.h +++ b/tests/cst816s_input/mocks/mock_platform.h @@ -4,6 +4,7 @@ #include #include +#include #include typedef int esp_err_t; @@ -13,6 +14,10 @@ typedef int esp_err_t; const char *esp_err_to_name(esp_err_t error); typedef int gpio_num_t; +#define GPIO_MODE_OUTPUT 1 +#define GPIO_PULLUP_DISABLE 0 +#define GPIO_PULLDOWN_DISABLE 0 +#define GPIO_INTR_DISABLE 0 #define GPIO_NUM_NC -1 #define GPIO_NUM_0 0 #define GPIO_NUM_1 1 @@ -23,6 +28,7 @@ typedef int gpio_num_t; #define GPIO_NUM_7 7 #define GPIO_NUM_10 10 #define GPIO_NUM_11 11 +#define GPIO_NUM_13 13 #define GPIO_NUM_15 15 #define GPIO_NUM_21 21 #define GPIO_NUM_38 38 @@ -39,6 +45,16 @@ typedef int gpio_num_t; #define I2C_NUM_1 1 #define SPI2_HOST 2 +typedef struct { + uint64_t pin_bit_mask; + int mode; + int pull_up_en; + int pull_down_en; + int intr_type; +} gpio_config_t; +esp_err_t gpio_config(const gpio_config_t *config); +esp_err_t gpio_set_level(gpio_num_t gpio, uint32_t level); + #define ESP_LOGI(...) ((void)0) #define ESP_LOGW(...) ((void)0) #define ESP_LOGE(...) ((void)0) @@ -74,9 +90,28 @@ int xSemaphoreTake(SemaphoreHandle_t semaphore, TickType_t ticks); int xSemaphoreGive(SemaphoreHandle_t semaphore); typedef struct mock_i2c_bus *i2c_master_bus_handle_t; +typedef struct mock_i2c_device *i2c_master_dev_handle_t; typedef struct mock_panel_io *esp_lcd_panel_io_handle_t; typedef struct mock_touch *esp_lcd_touch_handle_t; +typedef enum { + I2C_ADDR_BIT_LEN_7 = 0, +} i2c_addr_bit_len_t; + +typedef struct { + i2c_addr_bit_len_t dev_addr_length; + uint16_t device_address; + uint32_t scl_speed_hz; +} i2c_device_config_t; + +esp_err_t i2c_master_bus_add_device(i2c_master_bus_handle_t bus, const i2c_device_config_t *config, + i2c_master_dev_handle_t *out_device); +esp_err_t i2c_master_bus_rm_device(i2c_master_dev_handle_t device); +esp_err_t i2c_master_transmit(i2c_master_dev_handle_t device, const uint8_t *data, size_t size, + int timeout_ms); +esp_err_t i2c_master_receive(i2c_master_dev_handle_t device, uint8_t *data, size_t size, + int timeout_ms); + typedef struct { uint16_t dev_addr; uint32_t scl_speed_hz; diff --git a/tests/custom_panel_profiles/run.sh b/tests/custom_panel_profiles/run.sh index d9e27a5..51047fb 100644 --- a/tests/custom_panel_profiles/run.sh +++ b/tests/custom_panel_profiles/run.sh @@ -32,3 +32,4 @@ run_board() { run_board lilygo-t-circle-s3 4 run_board jiuchuan-s3 5 +run_board lilygo-t-display-s3-pro-mvsrlora 6 diff --git a/tests/custom_panel_profiles/test_custom_panel.cc b/tests/custom_panel_profiles/test_custom_panel.cc index ad74e25..f7d12da 100644 --- a/tests/custom_panel_profiles/test_custom_panel.cc +++ b/tests/custom_panel_profiles/test_custom_panel.cc @@ -84,7 +84,7 @@ esp_err_t esp_lcd_new_panel_gc9a01(esp_lcd_panel_io_handle_t handle, CHECK(config->rgb_ele_order == MYBOT_DISPLAY_RGB_ORDER); CHECK(config->vendor_config); const auto *vendor = static_cast(config->vendor_config); - CHECK(vendor->init_cmds && vendor->init_cmds_size >= 25); + CHECK(vendor->init_cmds && vendor->init_cmds_size >= (EXPECTED_CONTROLLER == 6 ? 13 : 25)); bool found_controller_command = false; for (size_t index = 0; index < vendor->init_cmds_size; ++index) { if (EXPECTED_CONTROLLER == 4 && vendor->init_cmds[index].cmd == 0x74) { @@ -93,6 +93,9 @@ esp_err_t esp_lcd_new_panel_gc9a01(esp_lcd_panel_io_handle_t handle, if (EXPECTED_CONTROLLER == 5 && vendor->init_cmds[index].cmd == 0x7D) { found_controller_command = true; } + if (EXPECTED_CONTROLLER == 6 && vendor->init_cmds[index].cmd == 0xB4) { + found_controller_command = true; + } } CHECK(found_controller_command); panel = {}; diff --git a/tests/i80_st7789_lifecycle/run.sh b/tests/i80_st7789_lifecycle/run.sh new file mode 100755 index 0000000..ee74773 --- /dev/null +++ b/tests/i80_st7789_lifecycle/run.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +set -eu + +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +test_dir="$repo_root/tests/i80_st7789_lifecycle" +mock_dir="$repo_root/tests/st7789_lifecycle/mocks" +board_dir="$repo_root/components/mybot_platform/boards/atk-dnesp32s3-box" +test_build_dir=$(mktemp -d) +trap 'rm -r -- "$test_build_dir"' EXIT HUP INT TERM + +"${CC:-cc}" -std=c11 -Wall -Wextra -Werror \ + -I"$mock_dir" -I"$board_dir" \ + -I"$repo_root/components/mybot_platform/src/internal" \ + -c "$repo_root/components/mybot_platform/src/drivers/display/panels/generic/i80_st7789_panel.c" \ + -o "$test_build_dir/i80_st7789_panel.o" +"${CXX:-c++}" -std=c++17 -Wall -Wextra -Werror \ + -I"$mock_dir" -I"$board_dir" \ + -I"$repo_root/components/mybot_platform/src/internal" \ + "$test_dir/test_i80_st7789_lifecycle.cc" "$test_build_dir/i80_st7789_panel.o" \ + -o "$test_build_dir/test_i80_st7789_lifecycle" + +if command -v timeout >/dev/null 2>&1; then + timeout 20 "$test_build_dir/test_i80_st7789_lifecycle" +else + "$test_build_dir/test_i80_st7789_lifecycle" +fi diff --git a/tests/i80_st7789_lifecycle/test_i80_st7789_lifecycle.cc b/tests/i80_st7789_lifecycle/test_i80_st7789_lifecycle.cc new file mode 100644 index 0000000..12f0cea --- /dev/null +++ b/tests/i80_st7789_lifecycle/test_i80_st7789_lifecycle.cc @@ -0,0 +1,311 @@ +/* SPDX-License-Identifier: MIT */ +#include "host_platform.h" + +#include "board_config.h" +#include "display/display_panel.h" + +#include +#include +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + std::exit(1); \ + } \ + } while (0) + +struct host_i80_bus { + bool open = false; +}; + +struct host_panel_io { + bool open = false; +}; + +struct host_panel { + bool open = false; + bool initialized = false; + bool on = false; + int gap_x = -1; + int gap_y = -1; +}; + +static host_i80_bus bus; +static host_panel_io io; +static host_panel panel; +static bool rd_configured; +static int rd_level = -1; +static unsigned int backlight; +static unsigned int bus_initializations; +static unsigned int io_deletes; +static unsigned int panel_deletes; +static unsigned int bus_deletes; +static unsigned int post_command_count; +static const char *failed_step; + +static esp_err_t step(const char *name) { + if (failed_step == name) { + failed_step = nullptr; + return ESP_FAIL; + } + return ESP_OK; +} + +extern "C" { +const char *esp_err_to_name(esp_err_t error) { + return error == ESP_OK ? "ESP_OK" : "ESP_FAIL"; +} + +int mybot_board_set_display_backlight(unsigned int percent) { + CHECK(percent <= 100); + const esp_err_t result = step(percent ? "backlight_on" : "backlight_off"); + if (result != ESP_OK) { + return -1; + } + backlight = percent; + return 0; +} + +esp_err_t gpio_config(const gpio_config_t *config) { + CHECK(config && config->pin_bit_mask == 1ULL << MYBOT_DISPLAY_I80_RD); + CHECK(config->mode == GPIO_MODE_INPUT_OUTPUT); + CHECK(config->pull_up_en == GPIO_PULLUP_ENABLE); + CHECK(config->pull_down_en == GPIO_PULLDOWN_DISABLE); + CHECK(config->intr_type == GPIO_INTR_DISABLE); + const esp_err_t result = step("rd_config"); + if (result == ESP_OK) { + rd_configured = true; + } + return result; +} + +esp_err_t gpio_set_level(gpio_num_t gpio, uint32_t level) { + CHECK(gpio == MYBOT_DISPLAY_I80_RD && level <= 1); + const esp_err_t result = step("rd_high"); + if (result == ESP_OK) { + rd_level = static_cast(level); + } + return result; +} + +esp_err_t esp_lcd_new_i80_bus(const esp_lcd_i80_bus_config_t *config, + esp_lcd_i80_bus_handle_t *out) { + CHECK(config && out && !bus.open); + CHECK(config->dc_gpio_num == MYBOT_DISPLAY_DC); + CHECK(config->wr_gpio_num == MYBOT_DISPLAY_SCLK); + CHECK(config->clk_src == LCD_CLK_SRC_DEFAULT && config->bus_width == 8); + CHECK(config->max_transfer_bytes == + MYBOT_DISPLAY_WIDTH * MYBOT_DISPLAY_HEIGHT * sizeof(uint16_t)); + CHECK(config->dma_burst_size == 64); + CHECK(config->data_gpio_nums[0] == MYBOT_DISPLAY_I80_D0); + CHECK(config->data_gpio_nums[1] == MYBOT_DISPLAY_I80_D1); + CHECK(config->data_gpio_nums[2] == MYBOT_DISPLAY_I80_D2); + CHECK(config->data_gpio_nums[3] == MYBOT_DISPLAY_I80_D3); + CHECK(config->data_gpio_nums[4] == MYBOT_DISPLAY_I80_D4); + CHECK(config->data_gpio_nums[5] == MYBOT_DISPLAY_I80_D5); + CHECK(config->data_gpio_nums[6] == MYBOT_DISPLAY_I80_D6); + CHECK(config->data_gpio_nums[7] == MYBOT_DISPLAY_I80_D7); + ++bus_initializations; + const esp_err_t result = step("bus_new"); + if (result == ESP_OK) { + bus.open = true; + *out = &bus; + } + return result; +} + +esp_err_t esp_lcd_new_panel_io_i80(esp_lcd_i80_bus_handle_t handle, + const esp_lcd_panel_io_i80_config_t *config, + esp_lcd_panel_io_handle_t *out) { + CHECK(handle == &bus && bus.open && config && out && !io.open); + CHECK(config->cs_gpio_num == MYBOT_DISPLAY_CS); + CHECK(config->pclk_hz == MYBOT_DISPLAY_I80_CLOCK_HZ); + CHECK(config->trans_queue_depth == 2); + CHECK(config->lcd_cmd_bits == 8 && config->lcd_param_bits == 8); + CHECK(!config->dc_levels.dc_idle_level && !config->dc_levels.dc_cmd_level); + CHECK(!config->dc_levels.dc_dummy_level && config->dc_levels.dc_data_level); + const esp_err_t result = step("io_new"); + if (result == ESP_OK) { + io.open = true; + *out = &io; + } + return result; +} + +esp_err_t esp_lcd_new_panel_st7789(esp_lcd_panel_io_handle_t handle, + const esp_lcd_panel_dev_config_t *config, + esp_lcd_panel_handle_t *out) { + CHECK(handle == &io && io.open && config && out && !panel.open); + CHECK(config->reset_gpio_num == MYBOT_DISPLAY_RESET); + CHECK(config->rgb_ele_order == MYBOT_DISPLAY_RGB_ORDER); + CHECK(config->bits_per_pixel == 16 && !config->vendor_config); + const esp_err_t result = step("panel_new"); + if (result == ESP_OK) { + panel.open = true; + *out = &panel; + } + return result; +} + +esp_err_t esp_lcd_panel_io_tx_param(esp_lcd_panel_io_handle_t handle, int command, + const void *param, size_t param_size) { + CHECK(handle == &io && io.open && param && param_size == 1); + const auto value = *static_cast(param); + if (post_command_count % 2 == 0) { + CHECK(command == 0x36 && value == MYBOT_DISPLAY_ST7789_MADCTL); + } else { + CHECK(command == 0x3A && value == MYBOT_DISPLAY_ST7789_COLMOD); + } + ++post_command_count; + return step("post_cmd"); +} + +esp_err_t esp_lcd_panel_reset(esp_lcd_panel_handle_t handle) { + CHECK(handle == &panel && panel.open); + return step("panel_reset"); +} +esp_err_t esp_lcd_panel_init(esp_lcd_panel_handle_t handle) { + CHECK(handle == &panel && panel.open); + const esp_err_t result = step("panel_init"); + if (result == ESP_OK) { + panel.initialized = true; + } + return result; +} +esp_err_t esp_lcd_panel_set_gap(esp_lcd_panel_handle_t handle, int x, int y) { + CHECK(handle == &panel && panel.open); + CHECK(x == MYBOT_DISPLAY_OFFSET_X && y == MYBOT_DISPLAY_OFFSET_Y); + const esp_err_t result = step("panel_gap"); + if (result == ESP_OK) { + panel.gap_x = x; + panel.gap_y = y; + } + return result; +} +esp_err_t esp_lcd_panel_invert_color(esp_lcd_panel_handle_t handle, bool invert) { + CHECK(handle == &panel && panel.open && invert == MYBOT_DISPLAY_INVERT_COLOR); + return step("panel_invert"); +} +esp_err_t esp_lcd_panel_swap_xy(esp_lcd_panel_handle_t handle, bool swap) { + CHECK(handle == &panel && panel.open && swap == MYBOT_DISPLAY_SWAP_XY); + return step("panel_swap"); +} +esp_err_t esp_lcd_panel_mirror(esp_lcd_panel_handle_t handle, bool x, bool y) { + CHECK(handle == &panel && panel.open); + CHECK(x == MYBOT_DISPLAY_MIRROR_X && y == MYBOT_DISPLAY_MIRROR_Y); + return step("panel_mirror"); +} +esp_err_t esp_lcd_panel_disp_on_off(esp_lcd_panel_handle_t handle, bool on) { + CHECK(handle == &panel && panel.open); + const esp_err_t result = step(on ? "display_on" : "display_off"); + if (result == ESP_OK) { + panel.on = on; + } + return result; +} +esp_err_t esp_lcd_panel_del(esp_lcd_panel_handle_t handle) { + CHECK(handle == &panel && panel.open && backlight == 0); + const esp_err_t result = step("panel_delete"); + if (result == ESP_OK) { + panel = {}; + ++panel_deletes; + } + return result; +} +esp_err_t esp_lcd_panel_io_del(esp_lcd_panel_io_handle_t handle) { + CHECK(handle == &io && io.open && !panel.open); + const esp_err_t result = step("io_delete"); + if (result == ESP_OK) { + io = {}; + ++io_deletes; + } + return result; +} +esp_err_t esp_lcd_del_i80_bus(esp_lcd_i80_bus_handle_t handle) { + CHECK(handle == &bus && bus.open && !io.open && !panel.open); + const esp_err_t result = step("bus_delete"); + if (result == ESP_OK) { + bus = {}; + ++bus_deletes; + } + return result; +} +} + +static void reset_state() { + bus = {}; + io = {}; + panel = {}; + rd_configured = false; + rd_level = -1; + backlight = 0; + post_command_count = 0; + failed_step = nullptr; +} + +static void assert_released(const mybot_display_panel_t &display) { + CHECK(!display.ready && !display.backlight_ready && !display.io && !display.panel); + CHECK(!bus.open && !io.open && !panel.open && !panel.on && backlight == 0); +} + +static void test_open_and_close() { + reset_state(); + mybot_display_panel_t display{}; + CHECK(mybot_display_panel_open(&display) == 0); + CHECK(display.ready && display.backlight_ready); + CHECK(rd_configured && rd_level == 1); + CHECK(panel.open && panel.initialized && panel.on); + CHECK(panel.gap_x == MYBOT_DISPLAY_OFFSET_X && panel.gap_y == MYBOT_DISPLAY_OFFSET_Y); + CHECK(post_command_count == 2 && backlight == MYBOT_DISPLAY_BRIGHTNESS_PERCENT); + CHECK(mybot_display_panel_open(&display) < 0 && bus_initializations == 1); + CHECK(mybot_display_panel_close(&display) == 0); + assert_released(display); + CHECK(panel_deletes == 1 && io_deletes == 1 && bus_deletes == 1); + CHECK(mybot_display_panel_close(&display) == 0); +} + +static void test_open_failures_cleanup() { + const char *failures[] = { + "backlight_off", "rd_config", "rd_high", "bus_new", "io_new", + "panel_new", "panel_reset", "panel_init", "panel_gap", "panel_invert", + "post_cmd", "panel_swap", "panel_mirror", "display_on", "backlight_on", + }; + for (const char *failure : failures) { + reset_state(); + mybot_display_panel_t display{}; + failed_step = failure; + CHECK(mybot_display_panel_open(&display) < 0); + CHECK(!failed_step); + CHECK(mybot_display_panel_close(&display) == 0); + assert_released(display); + } +} + +static void test_cleanup_retry() { + const char *failures[] = {"panel_delete", "io_delete", "bus_delete"}; + for (const char *failure : failures) { + reset_state(); + mybot_display_panel_t display{}; + CHECK(mybot_display_panel_open(&display) == 0); + failed_step = failure; + CHECK(mybot_display_panel_close(&display) < 0); + CHECK(!failed_step); + CHECK(mybot_display_panel_close(&display) == 0); + assert_released(display); + const unsigned int inits = bus_initializations; + CHECK(mybot_display_panel_open(&display) == 0); + CHECK(bus_initializations == inits + 1); + CHECK(mybot_display_panel_close(&display) == 0); + assert_released(display); + } +} + +int main() { + test_open_and_close(); + test_open_failures_cleanup(); + test_cleanup_retry(); + std::puts("ST7789 i80 panel lifecycle host tests passed"); + return 0; +} diff --git a/tests/st7789_lifecycle/mocks/host_platform.h b/tests/st7789_lifecycle/mocks/host_platform.h index aeee930..8cb9c37 100644 --- a/tests/st7789_lifecycle/mocks/host_platform.h +++ b/tests/st7789_lifecycle/mocks/host_platform.h @@ -23,6 +23,12 @@ const char *esp_err_to_name(esp_err_t error); typedef int gpio_num_t; #define GPIO_MODE_OUTPUT 1 +#define GPIO_MODE_INPUT_OUTPUT 3 +#define GPIO_PULLUP_ENABLE 1 +#define GPIO_PULLUP_DISABLE 0 +#define GPIO_PULLDOWN_ENABLE 1 +#define GPIO_PULLDOWN_DISABLE 0 +#define GPIO_INTR_DISABLE 0 typedef struct { uint64_t pin_bit_mask; int mode; @@ -151,6 +157,7 @@ esp_err_t spi_bus_free(int host); typedef struct host_panel_io *esp_lcd_panel_io_handle_t; typedef struct host_panel *esp_lcd_panel_handle_t; +typedef struct host_i80_bus *esp_lcd_i80_bus_handle_t; typedef struct { int unused; } esp_lcd_panel_io_event_data_t; @@ -173,6 +180,38 @@ typedef struct { esp_lcd_panel_io_color_trans_done_cb_t on_color_trans_done; void *user_ctx; } esp_lcd_panel_io_spi_config_t; +#define LCD_CLK_SRC_DEFAULT 0 +typedef struct { + int dc_gpio_num; + int wr_gpio_num; + int clk_src; + int data_gpio_nums[16]; + size_t bus_width; + size_t max_transfer_bytes; + size_t dma_burst_size; +} esp_lcd_i80_bus_config_t; +typedef struct { + int cs_gpio_num; + uint32_t pclk_hz; + size_t trans_queue_depth; + esp_lcd_panel_io_color_trans_done_cb_t on_color_trans_done; + void *user_ctx; + int lcd_cmd_bits; + int lcd_param_bits; + struct { + unsigned int dc_idle_level : 1; + unsigned int dc_cmd_level : 1; + unsigned int dc_dummy_level : 1; + unsigned int dc_data_level : 1; + } dc_levels; + struct { + unsigned int cs_active_high : 1; + unsigned int reverse_color_bits : 1; + unsigned int swap_color_bytes : 1; + unsigned int pclk_active_neg : 1; + unsigned int pclk_idle_low : 1; + } flags; +} esp_lcd_panel_io_i80_config_t; typedef struct { int reset_gpio_num; int rgb_ele_order; @@ -185,6 +224,14 @@ typedef struct { esp_err_t esp_lcd_new_panel_io_spi(esp_lcd_spi_bus_handle_t host, const esp_lcd_panel_io_spi_config_t *config, esp_lcd_panel_io_handle_t *out); +esp_err_t esp_lcd_new_i80_bus(const esp_lcd_i80_bus_config_t *bus_config, + esp_lcd_i80_bus_handle_t *ret_bus); +esp_err_t esp_lcd_del_i80_bus(esp_lcd_i80_bus_handle_t bus); +esp_err_t esp_lcd_new_panel_io_i80(esp_lcd_i80_bus_handle_t bus, + const esp_lcd_panel_io_i80_config_t *io_config, + esp_lcd_panel_io_handle_t *ret_io); +esp_err_t esp_lcd_panel_io_tx_param(esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, + size_t param_size); esp_err_t esp_lcd_new_panel_st7789(esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *config, esp_lcd_panel_handle_t *out);