diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b17ce48..37810ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: run: | find main components/mybot_platform tests/atom_echos3r \ tests/st7789_lifecycle tests/co5300_lifecycle tests/bread_compact_wifi_lcd \ - tests/cores3_pixel_ui -type f \ + tests/cst816s_input tests/tca9554 tests/cores3_pixel_ui -type f \ \( -name '*.c' -o -name '*.h' -o -name '*.cc' -o -name '*.cpp' \) \ -exec clang-format --dry-run --Werror {} + - name: Whitespace check @@ -40,7 +40,7 @@ jobs: run: | find main components/mybot_platform tests/atom_echos3r \ tests/st7789_lifecycle tests/co5300_lifecycle tests/bread_compact_wifi_lcd \ - tests/cores3_pixel_ui -type f \ + tests/cst816s_input tests/tca9554 tests/cores3_pixel_ui -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:' || { @@ -54,6 +54,10 @@ jobs: run: sh tests/st7789_lifecycle/run.sh - name: CO5300 host lifecycle tests run: sh tests/co5300_lifecycle/run.sh + - name: CST816S and Boot input host lifecycle tests + run: sh tests/cst816s_input/run.sh + - name: TCA9554 masked update host tests + run: sh tests/tca9554/run.sh - name: Bread Compact Wi-Fi LCD host lifecycle tests run: sh tests/bread_compact_wifi_lcd/run.sh - name: CoreS3 pixel UI host lifecycle tests @@ -153,6 +157,30 @@ jobs: language: en-US ptime: 60 defaults: ci/ptime60.defaults;ci/en-us.defaults + - board: esp32-s3-touch-amoled-1.8-v2 + language: zh-CN + ptime: 60 + defaults: ci/ptime60.defaults + - board: esp32-s3-touch-amoled-1.8-v2 + language: en-US + ptime: 60 + defaults: ci/ptime60.defaults;ci/en-us.defaults + - board: esp32-s3-touch-lcd-1.85 + language: zh-CN + ptime: 60 + defaults: ci/ptime60.defaults + - board: esp32-s3-touch-lcd-1.85 + language: en-US + ptime: 60 + defaults: ci/ptime60.defaults;ci/en-us.defaults + - board: genjutech-s3-1.54tft + language: zh-CN + ptime: 60 + defaults: ci/ptime60.defaults + - board: genjutech-s3-1.54tft + language: en-US + ptime: 60 + defaults: ci/ptime60.defaults;ci/en-us.defaults - board: m5stack-core-s3 language: zh-CN ptime: 60 @@ -241,3 +269,86 @@ jobs: build-${{ matrix.board }}-ptime-${{ matrix.ptime }}-ui-${{ matrix.ui || 'default' }}-lang-${{ matrix.language }}-video-${{ matrix.video || 'off' }}/bootloader/bootloader.bin build-${{ matrix.board }}-ptime-${{ matrix.ptime }}-ui-${{ matrix.ui || 'default' }}-lang-${{ matrix.language }}-video-${{ matrix.video || 'off' }}/partition_table/partition-table.bin build-${{ matrix.board }}-ptime-${{ matrix.ptime }}-ui-${{ matrix.ui || 'default' }}-lang-${{ matrix.language }}-video-${{ matrix.video || 'off' }}/ota_data_initial.bin + + extended-idf-build: + name: extended-${{ matrix.board }}-${{ matrix.language }} + runs-on: ubuntu-24.04 + container: + image: espressif/idf:v5.5.2 + defaults: + run: + shell: bash + env: + BUILD_DIR: build-extended-${{ matrix.board }}-${{ matrix.language }} + strategy: + fail-fast: false + matrix: + board: + - xingzhi-metal-1.54-wifi + - xingzhi-cube-0.85tft-wifi + - xingzhi-cube-0.96oled-wifi + - xingzhi-abs-2.0 + - m5stack-stopwatch + - esp32-s3-touch-amoled-1.8 + - esp32-s3-touch-amoled-1.32 + - esp32-s3-touch-amoled-1.43c + - esp32-s3-touch-amoled-2.06 + - atoms3r-echo-base + - atoms3r-echo-pyramid + - sp-esp32-s3-1.28-box + - esp-box + - esp-box-3 + - esp-box-lite + - esp32s3-korvo2-v3 + - lichuang-dev + - quandong-s3-dev + - rymcu-bigsmart + - df-k10 + - mixgo-nova + - taiji-pi-s3 + - freenove-esp32s3-display-2.8-lcd + - esp32-s3-touch-lcd-1.54 + - esp32-s3-touch-lcd-1.83 + - esp32-s3-lcd-0.85 + - esp32-s3-touch-lcd-1.85b + - esp32-s3-touch-lcd-1.85c + - sp-esp32-s3-1.54-muma + - atk-dnesp32s3-box0 + - nulllab-ai-vox-v3 + - aipi-lite + - doit-s3-aibox + - magiclick-2p4 + - movecall-cuican-esp32s3 + - movecall-moji-esp32s3 + - bread-compact-wifi + - hu-087 + - kevin-sp-v4-dev + - du-chatx + language: [zh-CN, en-US] + steps: + - uses: actions/checkout@v4 + - name: Build ESP32-S3 firmware + run: | + source /opt/esp/idf/export.sh >/dev/null + defaults="ci/ptime60.defaults" + if [ "${{ matrix.language }}" = "en-US" ]; then + defaults="$defaults;ci/en-us.defaults" + fi + idf.py -B "$BUILD_DIR" \ + -DMYBOT_BOARD=${{ matrix.board }} \ + -DSDKCONFIG="$BUILD_DIR/sdkconfig" \ + -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;$defaults" \ + build + - name: Report size + run: | + source /opt/esp/idf/export.sh >/dev/null + idf.py -B "$BUILD_DIR" size + - name: Upload firmware + uses: actions/upload-artifact@v4 + with: + name: mybot-esp32-${{ matrix.board }}-${{ matrix.language }}-${{ github.sha }} + path: | + ${{ env.BUILD_DIR }}/mybot_esp32.bin + ${{ env.BUILD_DIR }}/bootloader/bootloader.bin + ${{ env.BUILD_DIR }}/partition_table/partition-table.bin + ${{ env.BUILD_DIR }}/ota_data_initial.bin diff --git a/CHANGELOG.md b/CHANGELOG.md index bedc809..ff5fc69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,32 @@ Versioning and Conventional Commits. ### Added -- Per-board hardware, input and operation documentation for all thirteen profiles, with shared +- KEVIN SP V4 Dev with a 240×280 ST7789 panel and ES8311 audio, plus DuChat X with a 128×160 + ST7789 panel, separate raw I2S audio and GPIO1 power hold. Both passed local ESP-IDF v5.5.2 + Chinese and English reconfigure/build/size/merge-bin validation; real-device validation + remains pending. +- Six additional ESP32-S3 profiles: DOIT S3 AI Box with PDM capture, Magiclick 2.4 with + NV3023, Movecall CuiCan/Moji with GC9A01, and Bread Compact Wi-Fi/HU-087 as headless raw-I2S + subsets. Chinese and English local merged builds passed; real-device validation remains pending. +- Ten additional ESP32-S3 profiles: eight ST7789/ILI9341 boards using ES8311 or + ES8311+ES7210 audio, followed by ST77916 LCD 1.85B/1.85C and SP 1.54 Muma. The batch adds + compact 128×128 layout coverage, AXP2101/TCA9554 reset paths, a secondary board power rail, + CST816S touch, and board-specific READMEs. Chinese and English local merged builds passed; + real-device validation remains pending. +- Twenty-two additional ESP32-S3 board profiles covering NV3023, GC9A01, ILI9341, SH8601 and + ST77916 panels; PCA9557/TCA9554, M5PM1/M5IOE1 and AXP2101 reset/power paths; FT5x06, GT911, + CST816S-compatible touch and GPIO input; ES8311, ES7210, ES8374 and raw/PDM audio paths. + `xingzhi-cube-0.96oled-wifi` is intentionally headless. All new profiles passed local + ESP-IDF v5.5.2 Chinese and English reconfigure/build/size/merge-bin validation; real-device + validation remains pending. Each new profile has its own concise board README linked from the + board index. +- Shared compile-time board/hardware skeleton for board families whose lifecycle contracts match. +- Per-board hardware, input and operation documentation for the original profiles, with shared build/flash instructions and one validation matrix. - CoreS3-only process-gray pixel-glasses UI, with 23 expressions, four 9 px terminal rows, a 10 px signature and fixed 32 px six-digit pairing codes on its 320x240 display. Automatic faces follow READY/conversation phases. Expressions affect only the LCD. - `MYBOT_BOARD` selects the view automatically; other twelve profiles retain their UI/hardware. + `MYBOT_BOARD` selects the view automatically; other profiles retain their UI/hardware. - Shared LVGL status UI for the other display-board profiles, with Chinese/English text, pairing codes, voiceprint status, and listening/thinking/speaking indicators. - LVGL light/dark themes, local state emoji, rounded status cards, persistent voiceprint @@ -42,6 +62,15 @@ Versioning and Conventional Commits. - Waveshare ESP32-S3 Touch AMOLED 2.16 Board profile with a 480x480 CO5300 panel, zero display gap, CST9217/Boot input, and shared AXP2101 and ES7210/ES8311 integration. Chinese and English variants passed ESP-IDF v5.5.2 reconfigure/build/size/merge-bin validation. +- Waveshare ESP32-S3 Touch AMOLED 1.8 V2 Board profile with a 368x448 CO5300 panel, TCA9554 + reset, AXP2101 power sequencing, CST816S/Boot input, and ES8311 audio. +- Waveshare ESP32-S3 Touch LCD 1.85 Board profile with a 360x360 ST77916 panel, panel-version + selection, dual I2C buses, TCA9554 reset, separated I2S, right-slot microphone capture, and + both-slot playback. +- GenjuTech S3 1.54 TFT Board profile with an inverting ST7789 display, ES8311 audio, Boot and + volume buttons, and board-specific PA/display power coupling. +- Chinese and English variants for all three new profiles passed ESP-IDF v5.5.2 + reconfigure/build/size/merge-bin validation. - Espressif ESP-VoCat Board profile with PCB V1.0/V1.2 runtime detection, ST77916 QSPI display, CST816S touch input, and ES7210/ES8311 full-duplex audio. - Pinned ST77916 2.0.2 and CST816S 1.1.1~1 production components. @@ -74,6 +103,11 @@ Versioning and Conventional Commits. ### Changed +- Extend the shared board skeleton with an optional secondary power rail for profiles that keep + codec and system power on separate GPIOs. +- Centralize generic hardware sequencing, round-screen safe-area layout, dual-I2C selection, + speaker PA control and display/audio capability derivation so additional profiles do not copy + board lifecycle code. - Centralize build and flash commands in the board index and current validation status in `docs/VALIDATION.md`; remove repeated status paragraphs, volatile build timings and per-board command copies from board and topic documentation. diff --git a/README.md b/README.md index 0de49e5..d676deb 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ voice-chat runtime on 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**. Thirteen compile-time board profiles are +The development baseline is **ESP-IDF v5.5.2**. Fifty-six compile-time board profiles are supported; 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. @@ -41,12 +41,29 @@ Display profiles: `zhengchen-1.54tft-ml307`, `zhengchen-1.54tft-wifi`, `xingzhi-cube-1.54tft-wifi`, `bread-compact-wifi-lcd`, `esp-vocat`, `esp32-s3-touch-amoled-1.75`, `esp32-s3-touch-amoled-1.75c`, -`esp32-s3-touch-amoled-2.16`, `m5stack-core-s3`, `m5stack-stick-s3`, and +`esp32-s3-touch-amoled-2.16`, `esp32-s3-touch-amoled-1.8-v2`, +`esp32-s3-touch-lcd-1.85`, `esp32-s3-touch-amoled-1.8`, +`esp32-s3-touch-amoled-1.32`, `esp32-s3-touch-amoled-1.43c`, +`esp32-s3-touch-amoled-2.06`, `atoms3r-echo-base`, `atoms3r-echo-pyramid`, +`sp-esp32-s3-1.28-box`, `esp-box`, `esp-box-3`, `esp-box-lite`, +`esp32s3-korvo2-v3`, `lichuang-dev`, `quandong-s3-dev`, `rymcu-bigsmart`, +`df-k10`, `mixgo-nova`, `taiji-pi-s3`, `m5stack-stopwatch`, +`xingzhi-metal-1.54-wifi`, `xingzhi-cube-0.85tft-wifi`, `xingzhi-abs-2.0`, +`freenove-esp32s3-display-2.8-lcd`, `esp32-s3-touch-lcd-1.54`, +`esp32-s3-touch-lcd-1.83`, `esp32-s3-lcd-0.85`, +`esp32-s3-touch-lcd-1.85b`, `esp32-s3-touch-lcd-1.85c`, +`sp-esp32-s3-1.54-muma`, `atk-dnesp32s3-box0`, `nulllab-ai-vox-v3`, +`aipi-lite`, `magiclick-2p4`, `movecall-cuican-esp32s3`, +`movecall-moji-esp32s3`, +`kevin-sp-v4-dev`, `du-chatx`, +`genjutech-s3-1.54tft`, `m5stack-core-s3`, `m5stack-stick-s3`, and `sensecap-watcher`. Headless profiles: -`atom-echos3r` and `respeaker-flex-xvf3800-circular4-xiao`. +`atom-echos3r`, `xingzhi-cube-0.96oled-wifi`, `doit-s3-aibox`, +`bread-compact-wifi`, `hu-087`, and +`respeaker-flex-xvf3800-circular4-xiao`. 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 aea4bc9..c8c8feb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,7 +11,7 @@ 工程负责板级初始化、Wi-Fi 配网、持久化存储、HTTPS、音频、输入、显示,以及 SDK 外围的 固件生命周期。 -当前开发基线为 **ESP-IDF v5.5.2**,支持 13 个编译期 Board profile。通过 `MYBOT_BOARD` +当前开发基线为 **ESP-IDF v5.5.2**,支持 56 个编译期 Board profile。通过 `MYBOT_BOARD` 选择板型、Flash、PSRAM、分区表和硬件驱动。具体硬件配置、输入操作、构建命令和限制见 [板卡说明索引](components/mybot_platform/boards/README.md)。 @@ -37,12 +37,29 @@ `zhengchen-1.54tft-ml307`、`zhengchen-1.54tft-wifi`、 `xingzhi-cube-1.54tft-wifi`、`bread-compact-wifi-lcd`、`esp-vocat`、 `esp32-s3-touch-amoled-1.75`、`esp32-s3-touch-amoled-1.75c`、 -`esp32-s3-touch-amoled-2.16`、`m5stack-core-s3`、`m5stack-stick-s3` 和 +`esp32-s3-touch-amoled-2.16`、`esp32-s3-touch-amoled-1.8-v2`、 +`esp32-s3-touch-lcd-1.85`、`esp32-s3-touch-amoled-1.8`、 +`esp32-s3-touch-amoled-1.32`、`esp32-s3-touch-amoled-1.43c`、 +`esp32-s3-touch-amoled-2.06`、`atoms3r-echo-base`、`atoms3r-echo-pyramid`、 +`sp-esp32-s3-1.28-box`、`esp-box`、`esp-box-3`、`esp-box-lite`、 +`esp32s3-korvo2-v3`、`lichuang-dev`、`quandong-s3-dev`、`rymcu-bigsmart`、 +`df-k10`、`mixgo-nova`、`taiji-pi-s3`、`m5stack-stopwatch`、 +`xingzhi-metal-1.54-wifi`、`xingzhi-cube-0.85tft-wifi`、`xingzhi-abs-2.0`、 +`freenove-esp32s3-display-2.8-lcd`、`esp32-s3-touch-lcd-1.54`、 +`esp32-s3-touch-lcd-1.83`、`esp32-s3-lcd-0.85`、 +`esp32-s3-touch-lcd-1.85b`、`esp32-s3-touch-lcd-1.85c`、 +`sp-esp32-s3-1.54-muma`、`atk-dnesp32s3-box0`、`nulllab-ai-vox-v3`、 +`aipi-lite`、`magiclick-2p4`、`movecall-cuican-esp32s3`、 +`movecall-moji-esp32s3`、 +`kevin-sp-v4-dev`、`du-chatx`、 +`genjutech-s3-1.54tft`、`m5stack-core-s3`、`m5stack-stick-s3` 和 `sensecap-watcher`。 无屏 profile: -`atom-echos3r` 和 `respeaker-flex-xvf3800-circular4-xiao`。 +`atom-echos3r`、`xingzhi-cube-0.96oled-wifi`、`doit-s3-aibox`、 +`bread-compact-wifi`、`hu-087` 和 +`respeaker-flex-xvf3800-circular4-xiao`。 所有 profile 当前均使用 Wi-Fi。ML307 profile 仅预留 Modem UART 引脚,未实现 Modem 或 4G 联网。目前仅 `m5stack-core-s3` 提供摄像头视频能力。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 7a0725a..0e76d64 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -16,6 +16,9 @@ It does not replace third-party terms. This file is informational and is not leg | Espressif SCCB 0.0.8 (I2C subset) | Apache-2.0 | `components/esp_sccb_intf` | | Espressif JPEG 0.6.1 (ESP32-S3 binary) | Espressif MIT, use on Espressif products | `components/esp_new_jpeg` | | Espressif ILI9341 LCD driver 2.0.2 | Apache-2.0 | `components/esp_lcd_ili9341` | +| Espressif GC9A01 LCD driver 2.0.4 | Apache-2.0 | `components/esp_lcd_gc9a01` | +| Espressif NV3023 LCD driver 1.0.1 | Apache-2.0 | `components/esp_lcd_nv3023` | +| Espressif SH8601 LCD driver 1.0.2 | Apache-2.0 | `components/esp_lcd_sh8601` | | LVGL 9.5.0 | MIT plus retained font/library-specific terms | `components/lvgl` | | Espressif LVGL port 2.8.0~1 | Apache-2.0 | `components/esp_lvgl_port` | | Espressif SPD2010 LCD driver 2.0.0~1 | Apache-2.0 | `components/esp_lcd_spd2010` | @@ -24,12 +27,16 @@ It does not replace third-party terms. This file is informational and is not leg | Espressif LCD touch 1.2.1 | Apache-2.0 | `components/esp_lcd_touch` | | Espressif CST816S touch driver 1.1.1~1 | Apache-2.0 | `components/esp_lcd_touch_cst816s` | | Waveshare CST9217 touch driver 1.0.4 | Apache-2.0 | `components/esp_lcd_touch_cst9217` | +| Espressif FT5x06 touch driver 1.1.0~1 | Apache-2.0 | `components/esp_lcd_touch_ft5x06` | +| Espressif GT911 touch driver 1.2.0~2 | Apache-2.0 | `components/esp_lcd_touch_gt911` | | Espressif IO expander 1.2.1 | Apache-2.0 | `components/esp_io_expander` | | Espressif TCA95xx 16-bit IO expander 2.0.2 | Apache-2.0 | `components/esp_io_expander_tca95xx_16bit` | | Espressif button 4.2.0 | Apache-2.0 | `components/button` | | Espressif knob 1.1.0 | Apache-2.0 | `components/knob` | | Espressif CMake utilities 0.5.0 | Apache-2.0 | `components/cmake_utilities` | | M5Stack M5PM1 1.0.7 | MIT | `components/m5pm1` | +| M5Stack M5IOE1 1.0.9 | MIT | `components/m5ioe1` | +| Espressif ES8374 codec source | Apache-2.0 | `components/mybot_platform/src/drivers/audio/es8374_codec.c` and its private headers | | Source Han Sans UI glyph subset | SIL Open Font License 1.1 | `components/mybot_platform/src/drivers/display/renderers/lvgl/lvgl_fonts.c`, `components/mybot_platform/src/drivers/display/assets/licenses/LVGL_VIEW_FONT_LICENSE.txt` | | Noto Color Emoji rasterized glyphs | SIL Open Font License 1.1 | `components/mybot_platform/assets/ui/noto_emoji`, `components/mybot_platform/src/drivers/display/renderers/lvgl/lvgl_assets.c` | | MyBot Terminal bitmap subsets derived from DejaVu Sans Mono Bold | Bitstream Vera font terms, with public-domain DejaVu changes | `components/mybot_platform/src/drivers/display/renderers/lvgl/pixel_fonts.c`, `TERMINAL_FONT_LICENSE.txt` | diff --git a/VENDORED_SOURCES.md b/VENDORED_SOURCES.md index 1c8c34b..f07e8ed 100644 --- a/VENDORED_SOURCES.md +++ b/VENDORED_SOURCES.md @@ -16,6 +16,9 @@ redistribution terms are documented separately in [THIRD_PARTY_NOTICES.md](THIRD | `components/esp_sccb_intf` | `github.com/espressif/esp-video-components`, `esp_sccb_intf` | 0.0.8, commit `3384d3510c5880edeb0f4c14d9e0196760280637` | | `components/esp_new_jpeg` | `github.com/espressif/esp-adf-libs`, `esp_new_jpeg` | 0.6.1, commit `35c37e1656db36dbf13b9aee8064d1b59f482f6c`; ESP32-S3 library SHA-256 `4205b1258ce0ef9fd9946abfc7cd5f105b08316ea567e1136cec60b3e6330896` | | `components/esp_lcd_ili9341` | `github.com/espressif/esp-bsp` | 2.0.2, commit `fc8bd325efcdef6d5802554659debba303058af6` | +| `components/esp_lcd_gc9a01` | ESP LCD GC9A01 production driver | 2.0.4; production source only, local CMake metadata | +| `components/esp_lcd_nv3023` | ESP LCD NV3023 production driver | 1.0.1; production source only, local CMake metadata | +| `components/esp_lcd_sh8601` | ESP LCD SH8601 production driver | 1.0.2; production source only, local CMake metadata | | `components/lvgl` | `github.com/lvgl/lvgl` | 9.5.0, commit `85aa60d18b3d5e5588d7b247abf90198f07c8a63` | | `components/esp_lvgl_port` | `github.com/espressif/esp-bsp`, `components/esp_lvgl_port` | 2.8.0~1, commit `d14ff131266bf1392efff88db72cb4638897507c` | | `components/esp_lcd_spd2010` | ESP Component Registry `espressif/esp_lcd_spd2010` | 2.0.0~1, commit `12f6ca1182ec48889b17ec570fadaaf267cb336e` | @@ -24,12 +27,16 @@ redistribution terms are documented separately in [THIRD_PARTY_NOTICES.md](THIRD | `components/esp_lcd_touch` | ESP Component Registry `espressif/esp_lcd_touch` | 1.2.1, commit `c927778a85eed239dd403c1719d4f543ad56e693`, content hash `3f85a7d95af876f1a6ecca8eb90a81614890d0f03a038390804e5a77e2caf862` | | `components/esp_lcd_touch_cst816s` | ESP Component Registry `espressif/esp_lcd_touch_cst816s` | 1.1.1~1, commit `ab5696c61e761b272a163740f94b0e5720c3dde4` | | `components/esp_lcd_touch_cst9217` | ESP Component Registry `waveshare/esp_lcd_touch_cst9217` | 1.0.4, commit `43e6e696bbf0cf8671a97fcfe02c4c720e4e4d27`, content hash `27a00845832b7987cacf4c4125cbed5415d940987583065b01870ac080930856` | +| `components/esp_lcd_touch_ft5x06` | ESP Component Registry `espressif/esp_lcd_touch_ft5x06` | 1.1.0~1; production source and Apache-2.0 license retained | +| `components/esp_lcd_touch_gt911` | ESP Component Registry `espressif/esp_lcd_touch_gt911` | 1.2.0~2; production source and Apache-2.0 license retained | | `components/esp_io_expander` | ESP Component Registry `espressif/esp_io_expander` | 1.2.1, commit `eb76dc6ecf21ccc4ee7ee58bfea3d3d31fa090cf` | | `components/esp_io_expander_tca95xx_16bit` | ESP Component Registry `espressif/esp_io_expander_tca95xx_16bit` | 2.0.2, commit `53f6127ba3a1dd80fbdf9a76b759ccd1a8dc0101` | | `components/button` | ESP Component Registry `espressif/button` | 4.2.0, commit `5f9cb98ae4d0e8153c4b4d1accf471214e5b6fe8` | | `components/knob` | ESP Component Registry `espressif/knob` | 1.1.0, commit `5f9cb98ae4d0e8153c4b4d1accf471214e5b6fe8` | | `components/cmake_utilities` | ESP Component Registry `espressif/cmake_utilities` | 0.5.0 | | `components/m5pm1` | ESP Component Registry `m5stack/m5pm1` | 1.0.7, content hash `731f79d0629e245787440f5419aac5d7a82befeb25f97689d6a2d0331a24a72d` | +| `components/m5ioe1` | ESP Component Registry `m5stack/m5ioe1` | 1.0.9; upstream MIT license retained | +| `components/mybot_platform/src/drivers/audio/es8374_codec.c`, `components/mybot_platform/src/internal/audio/es8374_codec.h`, `components/mybot_platform/src/internal/audio/es_common.h` | Espressif `esp_codec_dev` ES8374 device source | Same 1.5.11 source baseline as `components/esp_codec_dev`; vendored inside the platform component because the local codec-dev source set omits ES8374 | | `components/mybot_platform/src/drivers/display/renderers/lvgl/lvgl_fonts.c`, `components/mybot_platform/src/drivers/display/assets/licenses/LVGL_VIEW_FONT_LICENSE.txt` | `github.com/lvgl/lvgl`, `scripts/built_in_font/SourceHanSansSC-Normal.otf` | LVGL 9.5.0 source font SHA-256 `1ee89e1669362dee13851129c0a8a791a87521eb4148e5efbf5d26596738e25b`; generated 20 px, 4 bpp static UI subset | | `components/mybot_platform/assets/ui/noto_emoji`, `components/mybot_platform/src/drivers/display/renderers/lvgl/lvgl_assets.c` | Noto Color Emoji font glyphs rasterized by `github.com/78/noto-fonts`, `png/noto-color-emoji_64` | 2.0.0, commit `d45dbc64052d57048f20ab1770074172ce9eb53b`; per-image SHA-256 values and glyph provenance in `components/mybot_platform/assets/ui/noto_emoji/SOURCES.json` | @@ -88,7 +95,7 @@ are not included by these references. Firmware integration differences are limited to the active ESP32-S3 build: - Only `m5stack-core-s3` selects the process-gray pixel-glasses view. CMake chooses it from - `MYBOT_BOARD`, with no manual renderer flag; the other twelve profiles retain their current + `MYBOT_BOARD`, with no manual renderer flag; the other profiles retain their current UI/hardware. The original Source Han Sans 20 px UI subset is reused, while the CoreS3 path uses fixed widgets/curves, five immutable A8 masks (13,631 alpha bytes), and the terminal subsets above instead of the shared view's four emoji. Audio, camera/video policy, input, @@ -109,6 +116,12 @@ Firmware integration differences are limited to the active ESP32-S3 build: with independent identity and 480x480 zero-gap geometry. Its conservative 16 MB Flash/Octal PSRAM defaults require physical capacity confirmation; no new dependency or upstream application service is added. +- The GenjuTech 1.54, AMOLED 1.8 V2 and LCD 1.85 profiles add independent Board identities, + TCA9554 state handling, CST816S/Boot input, CO5300 368x448 and ST77916 360x360 panel variants, + and no new package dependency. The LCD 1.85 raw-I2S path exposes 32-bit right-slot capture and + duplicates mono playback into both slots while the SDK boundary remains 16 kHz mono signed-16 + PCM. Build success and any recorded image do not establish panel, audio, touch, power or radio + behavior on hardware. - LVGL and its ESP-IDF port are local, pinned dependencies included automatically for display boards and omitted for headless boards. The build omits examples, tests, and registry download metadata. The UI uses a static bilingual font subset and existing public LCD state; no upstream application services, diff --git a/components/esp_lcd_gc9a01/CMakeLists.txt b/components/esp_lcd_gc9a01/CMakeLists.txt new file mode 100644 index 0000000..a7a6d7a --- /dev/null +++ b/components/esp_lcd_gc9a01/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +idf_component_register( + SRCS "esp_lcd_gc9a01.c" + INCLUDE_DIRS "include" + REQUIRES esp_lcd + PRIV_REQUIRES driver +) +target_compile_definitions(${COMPONENT_LIB} PRIVATE + ESP_LCD_GC9A01_VER_MAJOR=2 + ESP_LCD_GC9A01_VER_MINOR=0 + ESP_LCD_GC9A01_VER_PATCH=4 +) diff --git a/components/esp_lcd_gc9a01/LICENSE b/components/esp_lcd_gc9a01/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/components/esp_lcd_gc9a01/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/esp_lcd_gc9a01/esp_lcd_gc9a01.c b/components/esp_lcd_gc9a01/esp_lcd_gc9a01.c new file mode 100644 index 0000000..34c4211 --- /dev/null +++ b/components/esp_lcd_gc9a01/esp_lcd_gc9a01.c @@ -0,0 +1,393 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_commands.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_check.h" + +#include "esp_lcd_gc9a01.h" + +static const char *TAG = "gc9a01"; + +static esp_err_t panel_gc9a01_del(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9a01_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9a01_init(esp_lcd_panel_t *panel); +static esp_err_t panel_gc9a01_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); +static esp_err_t panel_gc9a01_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_gc9a01_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_gc9a01_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); +static esp_err_t panel_gc9a01_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); +static esp_err_t panel_gc9a01_disp_on_off(esp_lcd_panel_t *panel, bool off); + +typedef struct { + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + bool reset_level; + int x_gap; + int y_gap; + uint8_t fb_bits_per_pixel; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // save current value of LCD_CMD_COLMOD register + const gc9a01_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; +} gc9a01_panel_t; + +esp_err_t esp_lcd_new_panel_gc9a01(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) +{ + esp_err_t ret = ESP_OK; + gc9a01_panel_t *gc9a01 = NULL; + gpio_config_t io_conf = { 0 }; + + ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + gc9a01 = (gc9a01_panel_t *)calloc(1, sizeof(gc9a01_panel_t)); + ESP_GOTO_ON_FALSE(gc9a01, ESP_ERR_NO_MEM, err, TAG, "no mem for gc9a01 panel"); + + if (panel_dev_config->reset_gpio_num >= 0) { + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + switch (panel_dev_config->color_space) { + case ESP_LCD_COLOR_SPACE_RGB: + gc9a01->madctl_val = 0; + break; + case ESP_LCD_COLOR_SPACE_BGR: + gc9a01->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space"); + break; + } +#elif ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + switch (panel_dev_config->rgb_endian) { + case LCD_RGB_ENDIAN_RGB: + gc9a01->madctl_val = 0; + break; + case LCD_RGB_ENDIAN_BGR: + gc9a01->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb endian"); + break; + } +#else + switch (panel_dev_config->rgb_ele_order) { + case LCD_RGB_ELEMENT_ORDER_RGB: + gc9a01->madctl_val = 0; + break; + case LCD_RGB_ELEMENT_ORDER_BGR: + gc9a01->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb element order"); + break; + } +#endif + + switch (panel_dev_config->bits_per_pixel) { + case 16: // RGB565 + gc9a01->colmod_val = 0x55; + gc9a01->fb_bits_per_pixel = 16; + break; + case 18: // RGB666 + gc9a01->colmod_val = 0x66; + // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel + gc9a01->fb_bits_per_pixel = 24; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + gc9a01->io = io; + gc9a01->reset_gpio_num = panel_dev_config->reset_gpio_num; + gc9a01->reset_level = panel_dev_config->flags.reset_active_high; + if (panel_dev_config->vendor_config) { + gc9a01->init_cmds = ((gc9a01_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds; + gc9a01->init_cmds_size = ((gc9a01_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size; + } + gc9a01->base.del = panel_gc9a01_del; + gc9a01->base.reset = panel_gc9a01_reset; + gc9a01->base.init = panel_gc9a01_init; + gc9a01->base.draw_bitmap = panel_gc9a01_draw_bitmap; + gc9a01->base.invert_color = panel_gc9a01_invert_color; + gc9a01->base.set_gap = panel_gc9a01_set_gap; + gc9a01->base.mirror = panel_gc9a01_mirror; + gc9a01->base.swap_xy = panel_gc9a01_swap_xy; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + gc9a01->base.disp_off = panel_gc9a01_disp_on_off; +#else + gc9a01->base.disp_on_off = panel_gc9a01_disp_on_off; +#endif + *ret_panel = &(gc9a01->base); + ESP_LOGD(TAG, "new gc9a01 panel @%p", gc9a01); + + ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_GC9A01_VER_MAJOR, ESP_LCD_GC9A01_VER_MINOR, + ESP_LCD_GC9A01_VER_PATCH); + + return ESP_OK; + +err: + if (gc9a01) { + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(gc9a01); + } + return ret; +} + +static esp_err_t panel_gc9a01_del(esp_lcd_panel_t *panel) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + + if (gc9a01->reset_gpio_num >= 0) { + gpio_reset_pin(gc9a01->reset_gpio_num); + } + ESP_LOGD(TAG, "del gc9a01 panel @%p", gc9a01); + free(gc9a01); + return ESP_OK; +} + +static esp_err_t panel_gc9a01_reset(esp_lcd_panel_t *panel) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9a01->io; + + // perform hardware reset + if (gc9a01->reset_gpio_num >= 0) { + gpio_set_level(gc9a01->reset_gpio_num, gc9a01->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(gc9a01->reset_gpio_num, !gc9a01->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + } else { // perform software reset + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(20)); // spec, wait at least 5ms before sending new command + } + + return ESP_OK; +} + +static const gc9a01_lcd_init_cmd_t vendor_specific_init_default[] = { +// {cmd, { data }, data_size, delay_ms} + // Enable Inter Register + {0xfe, (uint8_t []){0x00}, 0, 0}, + {0xef, (uint8_t []){0x00}, 0, 0}, + {0xeb, (uint8_t []){0x14}, 1, 0}, + {0x84, (uint8_t []){0x60}, 1, 0}, + {0x85, (uint8_t []){0xff}, 1, 0}, + {0x86, (uint8_t []){0xff}, 1, 0}, + {0x87, (uint8_t []){0xff}, 1, 0}, + {0x8e, (uint8_t []){0xff}, 1, 0}, + {0x8f, (uint8_t []){0xff}, 1, 0}, + {0x88, (uint8_t []){0x0a}, 1, 0}, + {0x89, (uint8_t []){0x23}, 1, 0}, + {0x8a, (uint8_t []){0x00}, 1, 0}, + {0x8b, (uint8_t []){0x80}, 1, 0}, + {0x8c, (uint8_t []){0x01}, 1, 0}, + {0x8d, (uint8_t []){0x03}, 1, 0}, + {0x90, (uint8_t []){0x08, 0x08, 0x08, 0x08}, 4, 0}, + {0xff, (uint8_t []){0x60, 0x01, 0x04}, 3, 0}, + {0xC3, (uint8_t []){0x13}, 1, 0}, + {0xC4, (uint8_t []){0x13}, 1, 0}, + {0xC9, (uint8_t []){0x30}, 1, 0}, + {0xbe, (uint8_t []){0x11}, 1, 0}, + {0xe1, (uint8_t []){0x10, 0x0e}, 2, 0}, + {0xdf, (uint8_t []){0x21, 0x0c, 0x02}, 3, 0}, + // Set gamma + {0xF0, (uint8_t []){0x45, 0x09, 0x08, 0x08, 0x26, 0x2a}, 6, 0}, + {0xF1, (uint8_t []){0x43, 0x70, 0x72, 0x36, 0x37, 0x6f}, 6, 0}, + {0xF2, (uint8_t []){0x45, 0x09, 0x08, 0x08, 0x26, 0x2a}, 6, 0}, + {0xF3, (uint8_t []){0x43, 0x70, 0x72, 0x36, 0x37, 0x6f}, 6, 0}, + {0xed, (uint8_t []){0x1b, 0x0b}, 2, 0}, + {0xae, (uint8_t []){0x77}, 1, 0}, + {0xcd, (uint8_t []){0x63}, 1, 0}, + {0x70, (uint8_t []){0x07, 0x07, 0x04, 0x0e, 0x0f, 0x09, 0x07, 0x08, 0x03}, 9, 0}, + {0xE8, (uint8_t []){0x34}, 1, 0}, // 4 dot inversion + {0x60, (uint8_t []){0x38, 0x0b, 0x6D, 0x6D, 0x39, 0xf0, 0x6D, 0x6D}, 8, 0}, + {0x61, (uint8_t []){0x38, 0xf4, 0x6D, 0x6D, 0x38, 0xf7, 0x6D, 0x6D}, 8, 0}, + {0x62, (uint8_t []){0x38, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x38, 0x0F, 0x71, 0xEF, 0x70, 0x70}, 12, 0}, + {0x63, (uint8_t []){0x38, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x38, 0x13, 0x71, 0xF3, 0x70, 0x70}, 12, 0}, + {0x64, (uint8_t []){0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07}, 7, 0}, + {0x66, (uint8_t []){0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00}, 10, 0}, + {0x67, (uint8_t []){0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98}, 10, 0}, + {0x74, (uint8_t []){0x10, 0x45, 0x80, 0x00, 0x00, 0x4E, 0x00}, 7, 0}, + {0x98, (uint8_t []){0x3e, 0x07}, 2, 0}, + {0x99, (uint8_t []){0x3e, 0x07}, 2, 0}, +}; + +static esp_err_t panel_gc9a01_init(esp_lcd_panel_t *panel) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9a01->io; + + // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + gc9a01->madctl_val, + }, 1), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]) { + gc9a01->colmod_val, + }, 1), TAG, "send command failed"); + + const gc9a01_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + if (gc9a01->init_cmds) { + init_cmds = gc9a01->init_cmds; + init_cmds_size = gc9a01->init_cmds_size; + } else { + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(gc9a01_lcd_init_cmd_t); + } + + bool is_cmd_overwritten = false; + for (int i = 0; i < init_cmds_size; i++) { + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd) { + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + gc9a01->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + gc9a01->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + default: + is_cmd_overwritten = false; + break; + } + + if (is_cmd_overwritten) { + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); + } + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGD(TAG, "send init commands success"); + + return ESP_OK; +} + +static esp_err_t panel_gc9a01_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); + esp_lcd_panel_io_handle_t io = gc9a01->io; + + x_start += gc9a01->x_gap; + x_end += gc9a01->x_gap; + y_start += gc9a01->y_gap; + y_end += gc9a01->y_gap; + + // define an area of frame memory where MCU can access + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]) { + (x_start >> 8) & 0xFF, + x_start & 0xFF, + ((x_end - 1) >> 8) & 0xFF, + (x_end - 1) & 0xFF, + }, 4), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]) { + (y_start >> 8) & 0xFF, + y_start & 0xFF, + ((y_end - 1) >> 8) & 0xFF, + (y_end - 1) & 0xFF, + }, 4), TAG, "send command failed"); + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * gc9a01->fb_bits_per_pixel / 8; + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len), TAG, "send color failed"); + + return ESP_OK; +} + +static esp_err_t panel_gc9a01_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9a01->io; + int command = 0; + if (invert_color_data) { + command = LCD_CMD_INVON; + } else { + command = LCD_CMD_INVOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9a01_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9a01->io; + if (mirror_x) { + gc9a01->madctl_val |= LCD_CMD_MX_BIT; + } else { + gc9a01->madctl_val &= ~LCD_CMD_MX_BIT; + } + if (mirror_y) { + gc9a01->madctl_val |= LCD_CMD_MY_BIT; + } else { + gc9a01->madctl_val &= ~LCD_CMD_MY_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + gc9a01->madctl_val + }, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9a01_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9a01->io; + if (swap_axes) { + gc9a01->madctl_val |= LCD_CMD_MV_BIT; + } else { + gc9a01->madctl_val &= ~LCD_CMD_MV_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + gc9a01->madctl_val + }, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_gc9a01_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + gc9a01->x_gap = x_gap; + gc9a01->y_gap = y_gap; + return ESP_OK; +} + +static esp_err_t panel_gc9a01_disp_on_off(esp_lcd_panel_t *panel, bool on_off) +{ + gc9a01_panel_t *gc9a01 = __containerof(panel, gc9a01_panel_t, base); + esp_lcd_panel_io_handle_t io = gc9a01->io; + int command = 0; + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + on_off = !on_off; +#endif + + if (on_off) { + command = LCD_CMD_DISPON; + } else { + command = LCD_CMD_DISPOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} diff --git a/components/esp_lcd_gc9a01/include/esp_lcd_gc9a01.h b/components/esp_lcd_gc9a01/include/esp_lcd_gc9a01.h new file mode 100644 index 0000000..a2c3531 --- /dev/null +++ b/components/esp_lcd_gc9a01/include/esp_lcd_gc9a01.h @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * @file + * @brief ESP LCD: GC9A01 + */ + +#pragma once + +#include "esp_lcd_panel_vendor.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LCD panel initialization commands. + * + */ +typedef struct { + int cmd; /* +#include +#include "esp_idf_version.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_commands.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_check.h" + +#include "esp_lcd_nv3023.h" + +static const char *TAG = "lcd_panel.nv3023"; + +static esp_err_t panel_nv3023_del(esp_lcd_panel_t *panel); +static esp_err_t panel_nv3023_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_nv3023_init(esp_lcd_panel_t *panel); +static esp_err_t panel_nv3023_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); +static esp_err_t panel_nv3023_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_nv3023_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_nv3023_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); +static esp_err_t panel_nv3023_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); +static esp_err_t panel_nv3023_disp_on_off(esp_lcd_panel_t *panel, bool off); + +typedef struct { + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + gpio_num_t reset_gpio_num; + bool reset_level; + int x_gap; + int y_gap; + uint8_t fb_bits_per_pixel; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // save current value of LCD_CMD_COLMOD register + const nv3023_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; +} nv3023_panel_t; + +esp_err_t esp_lcd_new_panel_nv3023(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) +{ + esp_err_t ret = ESP_OK; + nv3023_panel_t *nv3023 = NULL; + gpio_config_t io_conf = { 0 }; + + ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + nv3023 = (nv3023_panel_t *)calloc(1, sizeof(nv3023_panel_t)); + ESP_GOTO_ON_FALSE(nv3023, ESP_ERR_NO_MEM, err, TAG, "no mem for nv3023 panel"); + + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (panel_dev_config->reset_gpio_num != GPIO_NUM_NC) { +#else + if (panel_dev_config->reset_gpio_num >= 0) { +#endif + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + switch (panel_dev_config->color_space) { + case ESP_LCD_COLOR_SPACE_RGB: + nv3023->madctl_val = 0; + break; + case ESP_LCD_COLOR_SPACE_BGR: + nv3023->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space"); + break; + } +#elif ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + switch (panel_dev_config->rgb_endian) { + case LCD_RGB_ENDIAN_RGB: + nv3023->madctl_val = 0; + break; + case LCD_RGB_ENDIAN_BGR: + nv3023->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb endian"); + break; + } +#else + switch (panel_dev_config->rgb_ele_order) { + case LCD_RGB_ELEMENT_ORDER_RGB: + nv3023->madctl_val = 0; + break; + case LCD_RGB_ELEMENT_ORDER_BGR: + nv3023->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb element order"); + break; + } +#endif + + switch (panel_dev_config->bits_per_pixel) { + case 12: // RGB444 + nv3023->colmod_val = 0x33; + nv3023->fb_bits_per_pixel = 16; + break; + case 16: // RGB565 + nv3023->colmod_val = 0x55; + nv3023->fb_bits_per_pixel = 16; + break; + case 18: // RGB666 + nv3023->colmod_val = 0x66; + // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel + nv3023->fb_bits_per_pixel = 24; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + nv3023->io = io; + nv3023->reset_gpio_num = panel_dev_config->reset_gpio_num; + nv3023->reset_level = panel_dev_config->flags.reset_active_high; + if (panel_dev_config->vendor_config) { + nv3023->init_cmds = ((nv3023_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds; + nv3023->init_cmds_size = ((nv3023_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size; + } + nv3023->base.del = panel_nv3023_del; + nv3023->base.reset = panel_nv3023_reset; + nv3023->base.init = panel_nv3023_init; + nv3023->base.draw_bitmap = panel_nv3023_draw_bitmap; + nv3023->base.invert_color = panel_nv3023_invert_color; + nv3023->base.set_gap = panel_nv3023_set_gap; + nv3023->base.mirror = panel_nv3023_mirror; + nv3023->base.swap_xy = panel_nv3023_swap_xy; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + nv3023->base.disp_off = panel_nv3023_disp_on_off; +#else + nv3023->base.disp_on_off = panel_nv3023_disp_on_off; +#endif + *ret_panel = &(nv3023->base); + ESP_LOGD(TAG, "new nv3023 panel @%p", nv3023); + + ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_NV3023_VER_MAJOR, ESP_LCD_NV3023_VER_MINOR, + ESP_LCD_NV3023_VER_PATCH); + + return ESP_OK; + +err: + if (nv3023) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (panel_dev_config->reset_gpio_num != GPIO_NUM_NC) { +#else + if (panel_dev_config->reset_gpio_num >= 0) { +#endif + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(nv3023); + } + return ret; +} + +static esp_err_t panel_nv3023_del(esp_lcd_panel_t *panel) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (nv3023->reset_gpio_num != GPIO_NUM_NC) { +#else + if (nv3023->reset_gpio_num >= 0) { +#endif + gpio_reset_pin(nv3023->reset_gpio_num); + } + ESP_LOGD(TAG, "del nv3023 panel @%p", nv3023); + free(nv3023); + return ESP_OK; +} + +static esp_err_t panel_nv3023_reset(esp_lcd_panel_t *panel) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + esp_lcd_panel_io_handle_t io = nv3023->io; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (nv3023->reset_gpio_num != GPIO_NUM_NC) { +#else + if (nv3023->reset_gpio_num >= 0) { +#endif + gpio_set_level(nv3023->reset_gpio_num, nv3023->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(nv3023->reset_gpio_num, !nv3023->reset_level); + vTaskDelay(pdMS_TO_TICKS(120)); + } else { + esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0); + vTaskDelay(pdMS_TO_TICKS(120)); + } + + return ESP_OK; +} + +// Modified by MakerM0 +// Driver: nv3023, 0.85'TFT +static const nv3023_lcd_init_cmd_t vendor_specific_init_default[] = { +// {cmd, { data }, data_size, delay_ms} + {0xff, (uint8_t []){0xa5}, 1, 0}, + {0x3e, (uint8_t []){0x09}, 1, 0}, + {0x3a, (uint8_t []){0x05}, 1, 0}, + {0x82, (uint8_t []){0x00}, 1, 0}, + {0x98, (uint8_t []){0x00}, 1, 0}, + {0x63, (uint8_t []){0x0f}, 1, 0}, + {0x64, (uint8_t []){0x0f}, 1, 0}, + {0xb4, (uint8_t []){0x34}, 1, 0}, + {0xb5, (uint8_t []){0x30}, 1, 0}, + {0x83, (uint8_t []){0x03}, 1, 0}, + {0x86, (uint8_t []){0x04}, 1, 0}, + {0x87, (uint8_t []){0x16}, 1, 0}, + {0x88, (uint8_t []){0x0a}, 1, 0}, + {0x89, (uint8_t []){0x27}, 1, 0}, + {0x93, (uint8_t []){0x63}, 1, 0}, + {0x96, (uint8_t []){0x81}, 1, 0}, + {0xc3, (uint8_t []){0x10}, 1, 0}, + {0xe6, (uint8_t []){0x00}, 1, 0}, + {0x99, (uint8_t []){0x01}, 1, 0}, + {0x70, (uint8_t []){0x09, 0x1d, 0x14, 0x0a, 0x11, 0x16, 0x38, 0x0b, 0x08, 0x3e, 0x07, 0x0d, 0x16, 0x0F,0x14,0x05}, 16, 0}, + {0xa0, (uint8_t []){0x04, 0x28, 0x0c, 0x11, 0x0b, 0x23, 0x45, 0x07, 0x0a, 0x3b, 0x0d, 0x18, 0x14, 0x0F,0x19,0x08}, 16, 0}, + {0xff, (uint8_t []){0x00}, 1, 0}, + {0x11, (uint8_t []){0x00}, 0, 200}, + // {0x36, (uint8_t []){0x88}, 1, 0}, + // {0x2a, (uint8_t []){0x00, 0x00, 0x00, 0xef}, 4, 0}, + // {0x2b, (uint8_t []){0x00, 0x00, 0x01, 0x1b}, 4, 0}, + {0x29, (uint8_t []){0}, 0, 10}, + {0x2c, (uint8_t []){0}, 0, 0}, +}; + +static esp_err_t panel_nv3023_init(esp_lcd_panel_t *panel) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + esp_lcd_panel_io_handle_t io = nv3023->io; + + // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + nv3023->madctl_val, + }, 1), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]) { + nv3023->colmod_val, + }, 1), TAG, "send command failed"); + + const nv3023_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + if (nv3023->init_cmds) { + init_cmds = nv3023->init_cmds; + init_cmds_size = nv3023->init_cmds_size; + } else { + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(nv3023_lcd_init_cmd_t); + } + + bool is_cmd_overwritten = false; + for (int i = 0; i < init_cmds_size; i++) { + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd) { + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + nv3023->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + nv3023->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + default: + is_cmd_overwritten = false; + break; + } + + if (is_cmd_overwritten) { + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); + } + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGD(TAG, "send init commands success"); + + return ESP_OK; +} + +static esp_err_t panel_nv3023_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); + esp_lcd_panel_io_handle_t io = nv3023->io; + + x_start += nv3023->x_gap; + x_end += nv3023->x_gap; + y_start += nv3023->y_gap; + y_end += nv3023->y_gap; + + // define an area of frame memory where MCU can access + esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]) { + (x_start >> 8) & 0xFF, + x_start & 0xFF, + ((x_end - 1) >> 8) & 0xFF, + (x_end - 1) & 0xFF, + }, 4); + esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]) { + (y_start >> 8) & 0xFF, + y_start & 0xFF, + ((y_end - 1) >> 8) & 0xFF, + (y_end - 1) & 0xFF, + }, 4); + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * nv3023->fb_bits_per_pixel / 8; + esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len); + + return ESP_OK; +} + +static esp_err_t panel_nv3023_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + esp_lcd_panel_io_handle_t io = nv3023->io; + int command = 0; + if (invert_color_data) { + command = LCD_CMD_INVON; + } else { + command = LCD_CMD_INVOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_nv3023_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + esp_lcd_panel_io_handle_t io = nv3023->io; + if (mirror_x) { + nv3023->madctl_val |= LCD_CMD_MX_BIT; + } else { + nv3023->madctl_val &= ~LCD_CMD_MX_BIT; + } + if (mirror_y) { + nv3023->madctl_val |= LCD_CMD_MY_BIT; + } else { + nv3023->madctl_val &= ~LCD_CMD_MY_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + nv3023->madctl_val + }, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_nv3023_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + esp_lcd_panel_io_handle_t io = nv3023->io; + if (swap_axes) { + nv3023->madctl_val |= LCD_CMD_MV_BIT; + } else { + nv3023->madctl_val &= ~LCD_CMD_MV_BIT; + } + esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + nv3023->madctl_val + }, 1); + return ESP_OK; +} + +static esp_err_t panel_nv3023_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + nv3023->x_gap = x_gap; + nv3023->y_gap = y_gap; + return ESP_OK; +} + +static esp_err_t panel_nv3023_disp_on_off(esp_lcd_panel_t *panel, bool on_off) +{ + nv3023_panel_t *nv3023 = __containerof(panel, nv3023_panel_t, base); + esp_lcd_panel_io_handle_t io = nv3023->io; + int command = 0; + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + on_off = !on_off; +#endif + + if (on_off) { + command = LCD_CMD_DISPON; + } else { + command = LCD_CMD_DISPOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} diff --git a/components/esp_lcd_nv3023/include/esp_lcd_nv3023.h b/components/esp_lcd_nv3023/include/esp_lcd_nv3023.h new file mode 100644 index 0000000..cc1acac --- /dev/null +++ b/components/esp_lcd_nv3023/include/esp_lcd_nv3023.h @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "esp_lcd_panel_vendor.h" +#include "driver/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LCD panel initialization commands. + * + */ +typedef struct { + int cmd; /* +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "esp_check.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_commands.h" +#include "esp_log.h" + +#include "esp_lcd_sh8601.h" + +#define LCD_OPCODE_WRITE_CMD (0x02ULL) +#define LCD_OPCODE_READ_CMD (0x03ULL) +#define LCD_OPCODE_WRITE_COLOR (0x32ULL) + +static const char *TAG = "sh8601"; + +static esp_err_t panel_sh8601_del(esp_lcd_panel_t *panel); +static esp_err_t panel_sh8601_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_sh8601_init(esp_lcd_panel_t *panel); +static esp_err_t panel_sh8601_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); +static esp_err_t panel_sh8601_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_sh8601_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_sh8601_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); +static esp_err_t panel_sh8601_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); +static esp_err_t panel_sh8601_disp_on_off(esp_lcd_panel_t *panel, bool off); + +typedef struct { + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + int x_gap; + int y_gap; + uint8_t fb_bits_per_pixel; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // save surrent value of LCD_CMD_COLMOD register + const sh8601_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; + struct { + unsigned int use_qspi_interface: 1; + unsigned int reset_level: 1; + } flags; +} sh8601_panel_t; + +esp_err_t esp_lcd_new_panel_sh8601(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) +{ + ESP_RETURN_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + + esp_err_t ret = ESP_OK; + sh8601_panel_t *sh8601 = NULL; + sh8601 = calloc(1, sizeof(sh8601_panel_t)); + ESP_GOTO_ON_FALSE(sh8601, ESP_ERR_NO_MEM, err, TAG, "no mem for sh8601 panel"); + + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_config_t io_conf = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num, + }; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + + switch (panel_dev_config->rgb_ele_order) { + case LCD_RGB_ELEMENT_ORDER_RGB: + sh8601->madctl_val = 0; + break; + case LCD_RGB_ELEMENT_ORDER_BGR: + sh8601->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color element order"); + break; + } + + uint8_t fb_bits_per_pixel = 0; + switch (panel_dev_config->bits_per_pixel) { + case 16: // RGB565 + sh8601->colmod_val = 0x55; + fb_bits_per_pixel = 16; + break; + case 18: // RGB666 + sh8601->colmod_val = 0x66; + // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel + fb_bits_per_pixel = 24; + break; + case 24: // RGB888 + sh8601->colmod_val = 0x77; + fb_bits_per_pixel = 24; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + sh8601->io = io; + sh8601->reset_gpio_num = panel_dev_config->reset_gpio_num; + sh8601->fb_bits_per_pixel = fb_bits_per_pixel; + sh8601_vendor_config_t *vendor_config = (sh8601_vendor_config_t *)panel_dev_config->vendor_config; + if (vendor_config) { + sh8601->init_cmds = vendor_config->init_cmds; + sh8601->init_cmds_size = vendor_config->init_cmds_size; + sh8601->flags.use_qspi_interface = vendor_config->flags.use_qspi_interface; + } + sh8601->flags.reset_level = panel_dev_config->flags.reset_active_high; + sh8601->base.del = panel_sh8601_del; + sh8601->base.reset = panel_sh8601_reset; + sh8601->base.init = panel_sh8601_init; + sh8601->base.draw_bitmap = panel_sh8601_draw_bitmap; + sh8601->base.invert_color = panel_sh8601_invert_color; + sh8601->base.set_gap = panel_sh8601_set_gap; + sh8601->base.mirror = panel_sh8601_mirror; + sh8601->base.swap_xy = panel_sh8601_swap_xy; + sh8601->base.disp_on_off = panel_sh8601_disp_on_off; + *ret_panel = &(sh8601->base); + ESP_LOGD(TAG, "new sh8601 panel @%p", sh8601); + + ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_SH8601_VER_MAJOR, ESP_LCD_SH8601_VER_MINOR, + ESP_LCD_SH8601_VER_PATCH); + + return ESP_OK; + +err: + if (sh8601) { + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(sh8601); + } + return ret; +} + +static esp_err_t tx_param(sh8601_panel_t *sh8601, esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size) +{ + if (sh8601->flags.use_qspi_interface) { + lcd_cmd &= 0xff; + lcd_cmd <<= 8; + lcd_cmd |= LCD_OPCODE_WRITE_CMD << 24; + } + return esp_lcd_panel_io_tx_param(io, lcd_cmd, param, param_size); +} + +static esp_err_t tx_color(sh8601_panel_t *sh8601, esp_lcd_panel_io_handle_t io, int lcd_cmd, const void *param, size_t param_size) +{ + if (sh8601->flags.use_qspi_interface) { + lcd_cmd &= 0xff; + lcd_cmd <<= 8; + lcd_cmd |= LCD_OPCODE_WRITE_COLOR << 24; + } + return esp_lcd_panel_io_tx_color(io, lcd_cmd, param, param_size); +} + +static esp_err_t panel_sh8601_del(esp_lcd_panel_t *panel) +{ + sh8601_panel_t *sh8601 = __containerof(panel, sh8601_panel_t, base); + + if (sh8601->reset_gpio_num >= 0) { + gpio_reset_pin(sh8601->reset_gpio_num); + } + ESP_LOGD(TAG, "del sh8601 panel @%p", sh8601); + free(sh8601); + return ESP_OK; +} + +static esp_err_t panel_sh8601_reset(esp_lcd_panel_t *panel) +{ + sh8601_panel_t *sh8601 = __containerof(panel, sh8601_panel_t, base); + esp_lcd_panel_io_handle_t io = sh8601->io; + + // Perform hardware reset + if (sh8601->reset_gpio_num >= 0) { + gpio_set_level(sh8601->reset_gpio_num, sh8601->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(sh8601->reset_gpio_num, !sh8601->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(150)); + } else { // Perform software reset + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(80)); + } + + return ESP_OK; +} + +static const sh8601_lcd_init_cmd_t vendor_specific_init_default[] = { +// {cmd, { data }, data_size, delay_ms} + {0x44, (uint8_t []){0x01, 0xD1}, 2, 0}, + {0x35, (uint8_t []){0x00}, 0, 0}, + {0x53, (uint8_t []){0x20}, 1, 25}, +}; + +static esp_err_t panel_sh8601_init(esp_lcd_panel_t *panel) +{ + sh8601_panel_t *sh8601 = __containerof(panel, sh8601_panel_t, base); + esp_lcd_panel_io_handle_t io = sh8601->io; + const sh8601_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + bool is_cmd_overwritten = false; + + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, LCD_CMD_MADCTL, (uint8_t[]) { + sh8601->madctl_val, + }, 1), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, LCD_CMD_COLMOD, (uint8_t[]) { + sh8601->colmod_val, + }, 1), TAG, "send command failed"); + + // vendor specific initialization, it can be different between manufacturers + // should consult the LCD supplier for initialization sequence code + if (sh8601->init_cmds) { + init_cmds = sh8601->init_cmds; + init_cmds_size = sh8601->init_cmds_size; + } else { + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(sh8601_lcd_init_cmd_t); + } + + for (int i = 0; i < init_cmds_size; i++) { + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd) { + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + sh8601->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + sh8601->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + default: + is_cmd_overwritten = false; + break; + } + + if (is_cmd_overwritten) { + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); + } + + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, + "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGD(TAG, "send init commands success"); + + return ESP_OK; +} + +static esp_err_t panel_sh8601_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) +{ + sh8601_panel_t *sh8601 = __containerof(panel, sh8601_panel_t, base); + assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); + esp_lcd_panel_io_handle_t io = sh8601->io; + + x_start += sh8601->x_gap; + x_end += sh8601->x_gap; + y_start += sh8601->y_gap; + y_end += sh8601->y_gap; + + // define an area of frame memory where MCU can access + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, LCD_CMD_CASET, (uint8_t[]) { + (x_start >> 8) & 0xFF, + x_start & 0xFF, + ((x_end - 1) >> 8) & 0xFF, + (x_end - 1) & 0xFF, + }, 4), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, LCD_CMD_RASET, (uint8_t[]) { + (y_start >> 8) & 0xFF, + y_start & 0xFF, + ((y_end - 1) >> 8) & 0xFF, + (y_end - 1) & 0xFF, + }, 4), TAG, "send command failed"); + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * sh8601->fb_bits_per_pixel / 8; + tx_color(sh8601, io, LCD_CMD_RAMWR, color_data, len); + + return ESP_OK; +} + +static esp_err_t panel_sh8601_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) +{ + sh8601_panel_t *sh8601 = __containerof(panel, sh8601_panel_t, base); + esp_lcd_panel_io_handle_t io = sh8601->io; + int command = 0; + if (invert_color_data) { + command = LCD_CMD_INVON; + } else { + command = LCD_CMD_INVOFF; + } + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_sh8601_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) +{ + sh8601_panel_t *sh8601 = __containerof(panel, sh8601_panel_t, base); + esp_lcd_panel_io_handle_t io = sh8601->io; + esp_err_t ret = ESP_OK; + + if (mirror_x) { + sh8601->madctl_val |= BIT(6); + } else { + sh8601->madctl_val &= ~BIT(6); + } + if (mirror_y) { + ESP_LOGE(TAG, "mirror_y is not supported by this panel"); + ret = ESP_ERR_NOT_SUPPORTED; + } + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, LCD_CMD_MADCTL, (uint8_t[]) { + sh8601->madctl_val + }, 1), TAG, "send command failed"); + return ret; +} + +static esp_err_t panel_sh8601_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) +{ + ESP_LOGE(TAG, "swap_xy is not supported by this panel"); + return ESP_ERR_NOT_SUPPORTED; +} + +static esp_err_t panel_sh8601_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) +{ + sh8601_panel_t *sh8601 = __containerof(panel, sh8601_panel_t, base); + sh8601->x_gap = x_gap; + sh8601->y_gap = y_gap; + return ESP_OK; +} + +static esp_err_t panel_sh8601_disp_on_off(esp_lcd_panel_t *panel, bool on_off) +{ + sh8601_panel_t *sh8601 = __containerof(panel, sh8601_panel_t, base); + esp_lcd_panel_io_handle_t io = sh8601->io; + int command = 0; + + if (on_off) { + command = LCD_CMD_DISPON; + } else { + command = LCD_CMD_DISPOFF; + } + ESP_RETURN_ON_ERROR(tx_param(sh8601, io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} diff --git a/components/esp_lcd_sh8601/include/esp_lcd_sh8601.h b/components/esp_lcd_sh8601/include/esp_lcd_sh8601.h new file mode 100644 index 0000000..4073ccd --- /dev/null +++ b/components/esp_lcd_sh8601/include/esp_lcd_sh8601.h @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "esp_lcd_panel_vendor.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LCD panel initialization commands. + * + */ +typedef struct { + int cmd; /* +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_check.h" +#include "driver/gpio.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_touch.h" + +static const char *TAG = "FT5x06"; + +/* Registers */ +#define FT5x06_DEVICE_MODE (0x00) +#define FT5x06_GESTURE_ID (0x01) +#define FT5x06_TOUCH_POINTS (0x02) + +#define FT5x06_TOUCH1_EV_FLAG (0x03) +#define FT5x06_TOUCH1_XH (0x03) +#define FT5x06_TOUCH1_XL (0x04) +#define FT5x06_TOUCH1_YH (0x05) +#define FT5x06_TOUCH1_YL (0x06) + +#define FT5x06_TOUCH2_EV_FLAG (0x09) +#define FT5x06_TOUCH2_XH (0x09) +#define FT5x06_TOUCH2_XL (0x0A) +#define FT5x06_TOUCH2_YH (0x0B) +#define FT5x06_TOUCH2_YL (0x0C) + +#define FT5x06_TOUCH3_EV_FLAG (0x0F) +#define FT5x06_TOUCH3_XH (0x0F) +#define FT5x06_TOUCH3_XL (0x10) +#define FT5x06_TOUCH3_YH (0x11) +#define FT5x06_TOUCH3_YL (0x12) + +#define FT5x06_TOUCH4_EV_FLAG (0x15) +#define FT5x06_TOUCH4_XH (0x15) +#define FT5x06_TOUCH4_XL (0x16) +#define FT5x06_TOUCH4_YH (0x17) +#define FT5x06_TOUCH4_YL (0x18) + +#define FT5x06_TOUCH5_EV_FLAG (0x1B) +#define FT5x06_TOUCH5_XH (0x1B) +#define FT5x06_TOUCH5_XL (0x1C) +#define FT5x06_TOUCH5_YH (0x1D) +#define FT5x06_TOUCH5_YL (0x1E) + +#define FT5x06_ID_G_THGROUP (0x80) +#define FT5x06_ID_G_THPEAK (0x81) +#define FT5x06_ID_G_THCAL (0x82) +#define FT5x06_ID_G_THWATER (0x83) +#define FT5x06_ID_G_THTEMP (0x84) +#define FT5x06_ID_G_THDIFF (0x85) +#define FT5x06_ID_G_CTRL (0x86) +#define FT5x06_ID_G_TIME_ENTER_MONITOR (0x87) +#define FT5x06_ID_G_PERIODACTIVE (0x88) +#define FT5x06_ID_G_PERIODMONITOR (0x89) +#define FT5x06_ID_G_AUTO_CLB_MODE (0xA0) +#define FT5x06_ID_G_LIB_VERSION_H (0xA1) +#define FT5x06_ID_G_LIB_VERSION_L (0xA2) +#define FT5x06_ID_G_CIPHER (0xA3) +#define FT5x06_ID_G_MODE (0xA4) +#define FT5x06_ID_G_PMODE (0xA5) +#define FT5x06_ID_G_FIRMID (0xA6) +#define FT5x06_ID_G_STATE (0xA7) +#define FT5x06_ID_G_FT5201ID (0xA8) +#define FT5x06_ID_G_ERR (0xA9) + +/******************************************************************************* +* Function definitions +*******************************************************************************/ +static esp_err_t esp_lcd_touch_ft5x06_read_data(esp_lcd_touch_handle_t tp); +static bool esp_lcd_touch_ft5x06_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, + uint8_t *point_num, uint8_t max_point_num); +static esp_err_t esp_lcd_touch_ft5x06_get_track_id(esp_lcd_touch_handle_t tp, uint8_t *track_id, uint8_t max_point_num); +static esp_err_t esp_lcd_touch_ft5x06_del(esp_lcd_touch_handle_t tp); + +/* I2C read */ +static esp_err_t touch_ft5x06_i2c_write(esp_lcd_touch_handle_t tp, uint8_t reg, uint8_t data); +static esp_err_t touch_ft5x06_i2c_read(esp_lcd_touch_handle_t tp, uint8_t reg, uint8_t *data, uint8_t len); + +/* FT5x06 init */ +static esp_err_t touch_ft5x06_init(esp_lcd_touch_handle_t tp); +/* FT5x06 reset */ +static esp_err_t touch_ft5x06_reset(esp_lcd_touch_handle_t tp); + +/******************************************************************************* +* Public API functions +*******************************************************************************/ + +esp_err_t esp_lcd_touch_new_i2c_ft5x06(const esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *config, + esp_lcd_touch_handle_t *out_touch) +{ + esp_err_t ret = ESP_OK; + + ESP_RETURN_ON_FALSE(io != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller io handle can't be NULL"); + ESP_RETURN_ON_FALSE(config != NULL, ESP_ERR_INVALID_ARG, TAG, + "Pointer to the touch controller configuration can't be NULL"); + ESP_RETURN_ON_FALSE(out_touch != NULL, ESP_ERR_INVALID_ARG, TAG, + "Pointer to the touch controller handle can't be NULL"); + + /* Prepare main structure */ + esp_lcd_touch_handle_t esp_lcd_touch_ft5x06 = heap_caps_calloc(1, sizeof(esp_lcd_touch_t), MALLOC_CAP_DEFAULT); + ESP_GOTO_ON_FALSE(esp_lcd_touch_ft5x06, ESP_ERR_NO_MEM, err, TAG, "no mem for FT5x06 controller"); + + /* Communication interface */ + esp_lcd_touch_ft5x06->io = io; + + /* Only supported callbacks are set */ + esp_lcd_touch_ft5x06->read_data = esp_lcd_touch_ft5x06_read_data; + esp_lcd_touch_ft5x06->get_xy = esp_lcd_touch_ft5x06_get_xy; + esp_lcd_touch_ft5x06->get_track_id = esp_lcd_touch_ft5x06_get_track_id; + esp_lcd_touch_ft5x06->del = esp_lcd_touch_ft5x06_del; + + /* Mutex */ + esp_lcd_touch_ft5x06->data.lock.owner = portMUX_FREE_VAL; + + /* Save config */ + memcpy(&esp_lcd_touch_ft5x06->config, config, sizeof(esp_lcd_touch_config_t)); + + /* Prepare pin for touch interrupt */ + if (esp_lcd_touch_ft5x06->config.int_gpio_num != GPIO_NUM_NC) { + const gpio_config_t int_gpio_config = { + .mode = GPIO_MODE_INPUT, + .intr_type = (esp_lcd_touch_ft5x06->config.levels.interrupt ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE), + .pin_bit_mask = BIT64(esp_lcd_touch_ft5x06->config.int_gpio_num) + }; + ret = gpio_config(&int_gpio_config); + ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed"); + + /* Register interrupt callback */ + if (esp_lcd_touch_ft5x06->config.interrupt_callback) { + esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_ft5x06, esp_lcd_touch_ft5x06->config.interrupt_callback); + } + } + + /* Prepare pin for touch controller reset */ + if (esp_lcd_touch_ft5x06->config.rst_gpio_num != GPIO_NUM_NC) { + const gpio_config_t rst_gpio_config = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = BIT64(esp_lcd_touch_ft5x06->config.rst_gpio_num) + }; + ret = gpio_config(&rst_gpio_config); + ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed"); + } + + /* Reset controller */ + ret = touch_ft5x06_reset(esp_lcd_touch_ft5x06); + ESP_GOTO_ON_ERROR(ret, err, TAG, "FT5x06 reset failed"); + + /* Init controller */ + ret = touch_ft5x06_init(esp_lcd_touch_ft5x06); + ESP_GOTO_ON_ERROR(ret, err, TAG, "FT5x06 init failed"); + + *out_touch = esp_lcd_touch_ft5x06; + +err: + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Error (0x%x)! Touch controller FT5x06 initialization failed!", ret); + if (esp_lcd_touch_ft5x06) { + esp_lcd_touch_ft5x06_del(esp_lcd_touch_ft5x06); + } + } + + return ret; +} + +static esp_err_t esp_lcd_touch_ft5x06_read_data(esp_lcd_touch_handle_t tp) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + esp_err_t err; + uint8_t data[30]; + uint8_t points; + size_t i = 0; + + err = touch_ft5x06_i2c_read(tp, FT5x06_TOUCH_POINTS, &points, 1); + ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!"); + + if (points > 5 || points == 0) { + return ESP_OK; + } + + /* Number of touched points */ + points = (points > CONFIG_ESP_LCD_TOUCH_MAX_POINTS ? CONFIG_ESP_LCD_TOUCH_MAX_POINTS : points); + + err = touch_ft5x06_i2c_read(tp, FT5x06_TOUCH1_XH, data, 6 * points); + ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!"); + + portENTER_CRITICAL(&tp->data.lock); + + /* Number of touched points */ + tp->data.points = points; + + /* Fill all coordinates */ + for (i = 0; i < points; i++) { + tp->data.coords[i].track_id = ((data[(i * 6) + 2] & 0xf0) >> 4); + tp->data.coords[i].x = (((uint16_t)data[(i * 6) + 0] & 0x0f) << 8) + data[(i * 6) + 1]; + tp->data.coords[i].y = (((uint16_t)data[(i * 6) + 2] & 0x0f) << 8) + data[(i * 6) + 3]; + } + + portEXIT_CRITICAL(&tp->data.lock); + + return ESP_OK; +} + +static bool esp_lcd_touch_ft5x06_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, + uint8_t *point_num, uint8_t max_point_num) +{ + ESP_RETURN_ON_FALSE(tp != NULL, false, TAG, "Touch controller handle can't be NULL"); + ESP_RETURN_ON_FALSE(x != NULL, false, TAG, "Pointer to the x coordinates array can't be NULL"); + ESP_RETURN_ON_FALSE(y != NULL, false, TAG, "Pointer to the y coordinates array can't be NULL"); + ESP_RETURN_ON_FALSE(point_num != NULL, false, TAG, "Pointer to number of touch points can't be NULL"); + ESP_RETURN_ON_FALSE(max_point_num > 0, false, TAG, "Array size must be equal or larger than 1"); + + portENTER_CRITICAL(&tp->data.lock); + + /* Count of points */ + *point_num = (tp->data.points > max_point_num ? max_point_num : tp->data.points); + + for (size_t i = 0; i < *point_num; i++) { + x[i] = tp->data.coords[i].x; + y[i] = tp->data.coords[i].y; + + if (strength) { + strength[i] = tp->data.coords[i].strength; + } + } + + /* Invalidate */ + tp->data.points = 0; + + portEXIT_CRITICAL(&tp->data.lock); + + return (*point_num > 0); +} + +static esp_err_t esp_lcd_touch_ft5x06_get_track_id(esp_lcd_touch_handle_t tp, uint8_t *track_id, uint8_t max_point_num) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + ESP_RETURN_ON_FALSE(track_id != NULL, ESP_ERR_INVALID_ARG, TAG, "Pointer to the track ID array can't be NULL"); + ESP_RETURN_ON_FALSE(max_point_num > 0, ESP_ERR_INVALID_ARG, TAG, "Array size must be equal or larger than 1"); + + portENTER_CRITICAL(&tp->data.lock); + + for (int i = 0; i < max_point_num; i++) { + track_id[i] = tp->data.coords[i].track_id; + } + + portEXIT_CRITICAL(&tp->data.lock); + + return ESP_OK; +} + +static esp_err_t esp_lcd_touch_ft5x06_del(esp_lcd_touch_handle_t tp) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + /* Reset GPIO pin settings */ + if (tp->config.int_gpio_num != GPIO_NUM_NC) { + gpio_reset_pin(tp->config.int_gpio_num); + if (tp->config.interrupt_callback) { + gpio_isr_handler_remove(tp->config.int_gpio_num); + } + } + + /* Reset GPIO pin settings */ + if (tp->config.rst_gpio_num != GPIO_NUM_NC) { + gpio_reset_pin(tp->config.rst_gpio_num); + } + + free(tp); + + return ESP_OK; +} + +/******************************************************************************* +* Private API function +*******************************************************************************/ + +static esp_err_t touch_ft5x06_init(esp_lcd_touch_handle_t tp) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + esp_err_t ret = ESP_OK; + + // Valid touching detect threshold + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_THGROUP, 70); + + // valid touching peak detect threshold + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_THPEAK, 60); + + // Touch focus threshold + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_THCAL, 16); + + // threshold when there is surface water + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_THWATER, 60); + + // threshold of temperature compensation + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_THTEMP, 10); + + // Touch difference threshold + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_THDIFF, 20); + + // Delay to enter 'Monitor' status (s) + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_TIME_ENTER_MONITOR, 2); + + // Period of 'Active' status (ms) + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_PERIODACTIVE, 12); + + // Timer to enter 'idle' when in 'Monitor' (ms) + ret |= touch_ft5x06_i2c_write(tp, FT5x06_ID_G_PERIODMONITOR, 40); + + return ret; +} + +/* Reset controller */ +static esp_err_t touch_ft5x06_reset(esp_lcd_touch_handle_t tp) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + if (tp->config.rst_gpio_num != GPIO_NUM_NC) { + ESP_RETURN_ON_ERROR(gpio_set_level(tp->config.rst_gpio_num, tp->config.levels.reset), TAG, "GPIO set level error!"); + vTaskDelay(pdMS_TO_TICKS(10)); + ESP_RETURN_ON_ERROR(gpio_set_level(tp->config.rst_gpio_num, !tp->config.levels.reset), TAG, "GPIO set level error!"); + vTaskDelay(pdMS_TO_TICKS(10)); + } + + return ESP_OK; +} + +static esp_err_t touch_ft5x06_i2c_write(esp_lcd_touch_handle_t tp, uint8_t reg, uint8_t data) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + // *INDENT-OFF* + /* Write data */ + return esp_lcd_panel_io_tx_param(tp->io, reg, (uint8_t[]){data}, 1); + // *INDENT-ON* +} + +static esp_err_t touch_ft5x06_i2c_read(esp_lcd_touch_handle_t tp, uint8_t reg, uint8_t *data, uint8_t len) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + ESP_RETURN_ON_FALSE(data != NULL, ESP_ERR_INVALID_ARG, TAG, "Pointer to the data array can't be NULL"); + + /* Read data */ + return esp_lcd_panel_io_rx_param(tp->io, reg, data, len); +} diff --git a/components/esp_lcd_touch_ft5x06/include/esp_lcd_touch_ft5x06.h b/components/esp_lcd_touch_ft5x06/include/esp_lcd_touch_ft5x06.h new file mode 100644 index 0000000..75488fc --- /dev/null +++ b/components/esp_lcd_touch_ft5x06/include/esp_lcd_touch_ft5x06.h @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief ESP LCD touch: FT5x06 + */ + +#pragma once + +#include "esp_lcd_touch.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Create a new FT5x06 touch driver + * + * @note The I2C communication should be initialized before use this function. + * + * @param io LCD/Touch panel IO handle + * @param config: Touch configuration + * @param out_touch: Touch instance handle + * @return + * - ESP_OK on success + * - ESP_ERR_NO_MEM if there is no memory for allocating main structure + */ +esp_err_t esp_lcd_touch_new_i2c_ft5x06(const esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *config, + esp_lcd_touch_handle_t *out_touch); + +/** + * @brief I2C address of the FT5x06 controller + * + */ +#define ESP_LCD_TOUCH_IO_I2C_FT5x06_ADDRESS (0x38) + +/** + * @brief Touch IO configuration structure + * + */ +#define ESP_LCD_TOUCH_IO_I2C_FT5x06_CONFIG() \ + { \ + .scl_speed_hz = 100000, \ + .dev_addr = ESP_LCD_TOUCH_IO_I2C_FT5x06_ADDRESS, \ + .control_phase_bytes = 1, \ + .dc_bit_offset = 0, \ + .lcd_cmd_bits = 8, \ + .flags = \ + { \ + .disable_control_phase = 1, \ + } \ + } + + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_lcd_touch_gt911/CMakeLists.txt b/components/esp_lcd_touch_gt911/CMakeLists.txt new file mode 100644 index 0000000..7cf0dee --- /dev/null +++ b/components/esp_lcd_touch_gt911/CMakeLists.txt @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +idf_component_register( + SRCS "esp_lcd_touch_gt911.c" + INCLUDE_DIRS "include" + REQUIRES esp_lcd esp_lcd_touch +) diff --git a/components/esp_lcd_touch_gt911/LICENSE b/components/esp_lcd_touch_gt911/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/components/esp_lcd_touch_gt911/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/esp_lcd_touch_gt911/esp_lcd_touch_gt911.c b/components/esp_lcd_touch_gt911/esp_lcd_touch_gt911.c new file mode 100644 index 0000000..28faa3f --- /dev/null +++ b/components/esp_lcd_touch_gt911/esp_lcd_touch_gt911.c @@ -0,0 +1,447 @@ +/* + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_check.h" +#include "driver/gpio.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_touch.h" +#include "esp_lcd_touch_gt911.h" + +static const char *TAG = "GT911"; + +/* GT911 registers */ +#define ESP_LCD_TOUCH_GT911_READ_KEY_REG (0x8093) +#define ESP_LCD_TOUCH_GT911_READ_XY_REG (0x814E) +#define ESP_LCD_TOUCH_GT911_CONFIG_REG (0x8047) +#define ESP_LCD_TOUCH_GT911_PRODUCT_ID_REG (0x8140) +#define ESP_LCD_TOUCH_GT911_ENTER_SLEEP (0x8040) + +/* GT911 support key num */ +#define ESP_GT911_TOUCH_MAX_BUTTONS (4) + +/******************************************************************************* +* Function definitions +*******************************************************************************/ +static esp_err_t esp_lcd_touch_gt911_read_data(esp_lcd_touch_handle_t tp); +static bool esp_lcd_touch_gt911_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, + uint8_t *point_num, uint8_t max_point_num); +static esp_err_t esp_lcd_touch_gt911_get_track_id(esp_lcd_touch_handle_t tp, uint8_t *track_id, uint8_t point_num); +#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) +static esp_err_t esp_lcd_touch_gt911_get_button_state(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state); +#endif +static esp_err_t esp_lcd_touch_gt911_del(esp_lcd_touch_handle_t tp); + +/* I2C read/write */ +static esp_err_t touch_gt911_i2c_read(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t *data, uint8_t len); +static esp_err_t touch_gt911_i2c_write(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t data); + +/* GT911 reset */ +static esp_err_t touch_gt911_reset(esp_lcd_touch_handle_t tp); +/* Read status and config register */ +static esp_err_t touch_gt911_read_cfg(esp_lcd_touch_handle_t tp); + +/* GT911 enter/exit sleep mode */ +static esp_err_t esp_lcd_touch_gt911_enter_sleep(esp_lcd_touch_handle_t tp); +static esp_err_t esp_lcd_touch_gt911_exit_sleep(esp_lcd_touch_handle_t tp); + +/******************************************************************************* +* Public API functions +*******************************************************************************/ + +esp_err_t esp_lcd_touch_new_i2c_gt911(const esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *config, + esp_lcd_touch_handle_t *out_touch) +{ + esp_err_t ret = ESP_OK; + + ESP_RETURN_ON_FALSE(io != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller io handle can't be NULL"); + ESP_RETURN_ON_FALSE(config != NULL, ESP_ERR_INVALID_ARG, TAG, + "Pointer to the touch controller configuration can't be NULL"); + ESP_RETURN_ON_FALSE(out_touch != NULL, ESP_ERR_INVALID_ARG, TAG, + "Pointer to the touch controller handle can't be NULL"); + + /* Prepare main structure */ + esp_lcd_touch_handle_t esp_lcd_touch_gt911 = heap_caps_calloc(1, sizeof(esp_lcd_touch_t), MALLOC_CAP_DEFAULT); + ESP_GOTO_ON_FALSE(esp_lcd_touch_gt911, ESP_ERR_NO_MEM, err, TAG, "no mem for GT911 controller"); + + /* Communication interface */ + esp_lcd_touch_gt911->io = io; + + /* Only supported callbacks are set */ + esp_lcd_touch_gt911->read_data = esp_lcd_touch_gt911_read_data; + esp_lcd_touch_gt911->get_xy = esp_lcd_touch_gt911_get_xy; + esp_lcd_touch_gt911->get_track_id = esp_lcd_touch_gt911_get_track_id; +#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) + esp_lcd_touch_gt911->get_button_state = esp_lcd_touch_gt911_get_button_state; +#endif + esp_lcd_touch_gt911->del = esp_lcd_touch_gt911_del; + esp_lcd_touch_gt911->enter_sleep = esp_lcd_touch_gt911_enter_sleep; + esp_lcd_touch_gt911->exit_sleep = esp_lcd_touch_gt911_exit_sleep; + + /* Mutex */ + esp_lcd_touch_gt911->data.lock.owner = portMUX_FREE_VAL; + + /* Save config */ + memcpy(&esp_lcd_touch_gt911->config, config, sizeof(esp_lcd_touch_config_t)); + esp_lcd_touch_io_gt911_config_t *gt911_config = (esp_lcd_touch_io_gt911_config_t *) + esp_lcd_touch_gt911->config.driver_data; + + /* Prepare pin for touch controller reset */ + if (esp_lcd_touch_gt911->config.rst_gpio_num != GPIO_NUM_NC) { + const gpio_config_t rst_gpio_config = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.rst_gpio_num) + }; + ret = gpio_config(&rst_gpio_config); + ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed"); + } + + if (gt911_config && esp_lcd_touch_gt911->config.rst_gpio_num != GPIO_NUM_NC + && esp_lcd_touch_gt911->config.int_gpio_num != GPIO_NUM_NC) { + /* Prepare pin for touch controller int */ + const gpio_config_t int_gpio_config = { + .mode = GPIO_MODE_OUTPUT, + .intr_type = GPIO_INTR_DISABLE, + .pull_down_en = 0, + .pull_up_en = 1, + .pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.int_gpio_num), + }; + ret = gpio_config(&int_gpio_config); + ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed"); + + ESP_RETURN_ON_ERROR(gpio_set_level(esp_lcd_touch_gt911->config.rst_gpio_num, esp_lcd_touch_gt911->config.levels.reset), + TAG, "GPIO set level error!"); + ESP_RETURN_ON_ERROR(gpio_set_level(esp_lcd_touch_gt911->config.int_gpio_num, 0), TAG, "GPIO set level error!"); + vTaskDelay(pdMS_TO_TICKS(10)); + + /* Select I2C addr, set output high or low */ + uint32_t gpio_level; + if (ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP == gt911_config->dev_addr) { + gpio_level = 1; + } else if (ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS == gt911_config->dev_addr) { + gpio_level = 0; + } else { + gpio_level = 0; + ESP_LOGE(TAG, "Addr (0x%X) is invalid", gt911_config->dev_addr); + } + ESP_RETURN_ON_ERROR(gpio_set_level(esp_lcd_touch_gt911->config.int_gpio_num, gpio_level), TAG, "GPIO set level error!"); + vTaskDelay(pdMS_TO_TICKS(1)); + + ESP_RETURN_ON_ERROR(gpio_set_level(esp_lcd_touch_gt911->config.rst_gpio_num, !esp_lcd_touch_gt911->config.levels.reset), + TAG, "GPIO set level error!"); + vTaskDelay(pdMS_TO_TICKS(10)); + + vTaskDelay(pdMS_TO_TICKS(50)); + } else { + ESP_LOGI(TAG, "I2C address initialization procedure skipped - using default GT9xx setup"); + /* Reset controller */ + ret = touch_gt911_reset(esp_lcd_touch_gt911); + ESP_GOTO_ON_ERROR(ret, err, TAG, "GT911 reset failed"); + } + + /* Prepare pin for touch interrupt */ + if (esp_lcd_touch_gt911->config.int_gpio_num != GPIO_NUM_NC) { + const gpio_config_t int_gpio_config = { + .mode = GPIO_MODE_INPUT, + .intr_type = (esp_lcd_touch_gt911->config.levels.interrupt ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE), + .pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.int_gpio_num) + }; + ret = gpio_config(&int_gpio_config); + ESP_GOTO_ON_ERROR(ret, err, TAG, "GPIO config failed"); + + /* Register interrupt callback */ + if (esp_lcd_touch_gt911->config.interrupt_callback) { + esp_lcd_touch_register_interrupt_callback(esp_lcd_touch_gt911, esp_lcd_touch_gt911->config.interrupt_callback); + } + } + + /* Read status and config info */ + ret = touch_gt911_read_cfg(esp_lcd_touch_gt911); + ESP_GOTO_ON_ERROR(ret, err, TAG, "GT911 init failed"); + + *out_touch = esp_lcd_touch_gt911; + +err: + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Error (0x%x)! Touch controller GT911 initialization failed!", ret); + if (esp_lcd_touch_gt911) { + esp_lcd_touch_gt911_del(esp_lcd_touch_gt911); + } + } + + return ret; +} + +static esp_err_t esp_lcd_touch_gt911_enter_sleep(esp_lcd_touch_handle_t tp) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + esp_err_t err = touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_ENTER_SLEEP, 0x05); + ESP_RETURN_ON_ERROR(err, TAG, "Enter Sleep failed!"); + + return ESP_OK; +} + +static esp_err_t esp_lcd_touch_gt911_exit_sleep(esp_lcd_touch_handle_t tp) +{ + esp_err_t ret; + esp_lcd_touch_handle_t esp_lcd_touch_gt911 = tp; + + if (esp_lcd_touch_gt911->config.int_gpio_num != GPIO_NUM_NC) { + const gpio_config_t int_gpio_config_high = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.int_gpio_num) + }; + ret = gpio_config(&int_gpio_config_high); + ESP_RETURN_ON_ERROR(ret, TAG, "High GPIO config failed"); + gpio_set_level(esp_lcd_touch_gt911->config.int_gpio_num, 1); + + vTaskDelay(pdMS_TO_TICKS(5)); + + const gpio_config_t int_gpio_config_float = { + .mode = GPIO_MODE_OUTPUT_OD, + .pin_bit_mask = BIT64(esp_lcd_touch_gt911->config.int_gpio_num) + }; + ret = gpio_config(&int_gpio_config_float); + ESP_RETURN_ON_ERROR(ret, TAG, "Float GPIO config failed"); + } + + return ESP_OK; +} + +static esp_err_t esp_lcd_touch_gt911_read_data(esp_lcd_touch_handle_t tp) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + esp_err_t err; + uint8_t buf[41]; + uint8_t touch_cnt = 0; + uint8_t clear = 0; + size_t i = 0; + + err = touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, buf, 1); + ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!"); + + /* Any touch data? */ + if ((buf[0] & 0x80) == 0x00) { + touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear); +#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) + } else if ((buf[0] & 0x10) == 0x10) { + /* Read all keys */ + uint8_t key_max = ((ESP_GT911_TOUCH_MAX_BUTTONS < CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS) ? \ + (ESP_GT911_TOUCH_MAX_BUTTONS) : (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS)); + err = touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_READ_KEY_REG, &buf[0], key_max); + ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!"); + + /* Clear all */ + touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear); + ESP_RETURN_ON_ERROR(err, TAG, "I2C write error!"); + + portENTER_CRITICAL(&tp->data.lock); + + /* Buttons count */ + tp->data.buttons = key_max; + for (i = 0; i < key_max; i++) { + tp->data.button[i].status = buf[i] ? 1 : 0; + } + + portEXIT_CRITICAL(&tp->data.lock); +#endif + } else if ((buf[0] & 0x80) == 0x80) { + portENTER_CRITICAL(&tp->data.lock); + /* Invalidate */ + tp->data.points = 0; +#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) + for (i = 0; i < CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS; i++) { + tp->data.button[i].status = 0; + } +#endif + portEXIT_CRITICAL(&tp->data.lock); + /* Count of touched points */ + touch_cnt = buf[0] & 0x0f; + if (touch_cnt > 5 || touch_cnt == 0) { + touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear); + return ESP_OK; + } + + /* Read all points */ + err = touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG + 1, &buf[1], touch_cnt * 8); + ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!"); + + /* Clear all */ + err = touch_gt911_i2c_write(tp, ESP_LCD_TOUCH_GT911_READ_XY_REG, clear); + ESP_RETURN_ON_ERROR(err, TAG, "I2C read error!"); + + portENTER_CRITICAL(&tp->data.lock); + + /* Number of touched points */ + touch_cnt = (touch_cnt > CONFIG_ESP_LCD_TOUCH_MAX_POINTS ? CONFIG_ESP_LCD_TOUCH_MAX_POINTS : touch_cnt); + tp->data.points = touch_cnt; + + /* Fill all coordinates */ + for (i = 0; i < touch_cnt; i++) { + tp->data.coords[i].track_id = buf[(i * 8) + 1]; + tp->data.coords[i].x = ((uint16_t)buf[(i * 8) + 3] << 8) + buf[(i * 8) + 2]; + tp->data.coords[i].y = (((uint16_t)buf[(i * 8) + 5] << 8) + buf[(i * 8) + 4]); + tp->data.coords[i].strength = (((uint16_t)buf[(i * 8) + 7] << 8) + buf[(i * 8) + 6]); + } + + portEXIT_CRITICAL(&tp->data.lock); + } + + return ESP_OK; +} + +static bool esp_lcd_touch_gt911_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, + uint8_t *point_num, uint8_t max_point_num) +{ + ESP_RETURN_ON_FALSE(tp != NULL, false, TAG, "Touch controller handle can't be NULL"); + ESP_RETURN_ON_FALSE(x != NULL, false, TAG, "Pointer to the x coordinates array can't be NULL"); + ESP_RETURN_ON_FALSE(y != NULL, false, TAG, "Pointer to the y coordinates array can't be NULL"); + ESP_RETURN_ON_FALSE(point_num != NULL, false, TAG, "Pointer to number of touch points can't be NULL"); + ESP_RETURN_ON_FALSE(max_point_num > 0, false, TAG, "Array size must be equal or larger than 1"); + + portENTER_CRITICAL(&tp->data.lock); + + /* Count of points */ + *point_num = (tp->data.points > max_point_num ? max_point_num : tp->data.points); + + for (size_t i = 0; i < *point_num; i++) { + x[i] = tp->data.coords[i].x; + y[i] = tp->data.coords[i].y; + + if (strength) { + strength[i] = tp->data.coords[i].strength; + } + } + + portEXIT_CRITICAL(&tp->data.lock); + + return (*point_num > 0); +} + +static esp_err_t esp_lcd_touch_gt911_get_track_id(esp_lcd_touch_handle_t tp, uint8_t *track_id, uint8_t max_point_num) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + ESP_RETURN_ON_FALSE(track_id != NULL, ESP_ERR_INVALID_ARG, TAG, "Pointer to the track ID array can't be NULL"); + ESP_RETURN_ON_FALSE(max_point_num > 0, ESP_ERR_INVALID_ARG, TAG, "Array size must be equal or larger than 1"); + + portENTER_CRITICAL(&tp->data.lock); + + for (int i = 0; i < max_point_num; i++) { + track_id[i] = tp->data.coords[i].track_id; + } + + portEXIT_CRITICAL(&tp->data.lock); + + return ESP_OK; +} + +#if (CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS > 0) +static esp_err_t esp_lcd_touch_gt911_get_button_state(esp_lcd_touch_handle_t tp, uint8_t n, uint8_t *state) +{ + esp_err_t err = ESP_OK; + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + ESP_RETURN_ON_FALSE(state != NULL, ESP_ERR_INVALID_ARG, TAG, "Pointer to the state array can't be NULL"); + + *state = 0; + + portENTER_CRITICAL(&tp->data.lock); + + if (n > tp->data.buttons) { + err = ESP_ERR_INVALID_ARG; + } else { + *state = tp->data.button[n].status; + } + + portEXIT_CRITICAL(&tp->data.lock); + + return err; +} +#endif + +static esp_err_t esp_lcd_touch_gt911_del(esp_lcd_touch_handle_t tp) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + /* Reset GPIO pin settings */ + if (tp->config.int_gpio_num != GPIO_NUM_NC) { + gpio_reset_pin(tp->config.int_gpio_num); + if (tp->config.interrupt_callback) { + gpio_isr_handler_remove(tp->config.int_gpio_num); + } + } + + /* Reset GPIO pin settings */ + if (tp->config.rst_gpio_num != GPIO_NUM_NC) { + gpio_reset_pin(tp->config.rst_gpio_num); + } + + free(tp); + + return ESP_OK; +} + +/******************************************************************************* +* Private API function +*******************************************************************************/ + +/* Reset controller */ +static esp_err_t touch_gt911_reset(esp_lcd_touch_handle_t tp) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + if (tp->config.rst_gpio_num != GPIO_NUM_NC) { + ESP_RETURN_ON_ERROR(gpio_set_level(tp->config.rst_gpio_num, tp->config.levels.reset), TAG, "GPIO set level error!"); + vTaskDelay(pdMS_TO_TICKS(10)); + ESP_RETURN_ON_ERROR(gpio_set_level(tp->config.rst_gpio_num, !tp->config.levels.reset), TAG, "GPIO set level error!"); + vTaskDelay(pdMS_TO_TICKS(10)); + } + + return ESP_OK; +} + +static esp_err_t touch_gt911_read_cfg(esp_lcd_touch_handle_t tp) +{ + uint8_t buf[4]; + + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + ESP_RETURN_ON_ERROR(touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_PRODUCT_ID_REG, (uint8_t *)&buf[0], 3), TAG, + "GT911 read error!"); + ESP_RETURN_ON_ERROR(touch_gt911_i2c_read(tp, ESP_LCD_TOUCH_GT911_CONFIG_REG, (uint8_t *)&buf[3], 1), TAG, + "GT911 read error!"); + + ESP_LOGI(TAG, "TouchPad_ID:0x%02x,0x%02x,0x%02x", buf[0], buf[1], buf[2]); + ESP_LOGI(TAG, "TouchPad_Config_Version:%d", buf[3]); + + return ESP_OK; +} + +static esp_err_t touch_gt911_i2c_read(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t *data, uint8_t len) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + ESP_RETURN_ON_FALSE(data != NULL, ESP_ERR_INVALID_ARG, TAG, "Pointer to the data array can't be NULL"); + + /* Read data */ + return esp_lcd_panel_io_rx_param(tp->io, reg, data, len); +} + +static esp_err_t touch_gt911_i2c_write(esp_lcd_touch_handle_t tp, uint16_t reg, uint8_t data) +{ + ESP_RETURN_ON_FALSE(tp != NULL, ESP_ERR_INVALID_ARG, TAG, "Touch controller handle can't be NULL"); + + // *INDENT-OFF* + /* Write data */ + return esp_lcd_panel_io_tx_param(tp->io, reg, (uint8_t[]){data}, 1); + // *INDENT-ON* +} diff --git a/components/esp_lcd_touch_gt911/include/esp_lcd_touch_gt911.h b/components/esp_lcd_touch_gt911/include/esp_lcd_touch_gt911.h new file mode 100644 index 0000000..468eda3 --- /dev/null +++ b/components/esp_lcd_touch_gt911/include/esp_lcd_touch_gt911.h @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief ESP LCD touch: GT911 + */ + +#pragma once + +#include "esp_lcd_touch.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Create a new GT911 touch driver + * + * @note The I2C communication should be initialized before use this function. + * + * @param io LCD/Touch panel IO handle + * @param config: Touch configuration + * @param out_touch: Touch instance handle + * @return + * - ESP_OK on success + * - ESP_ERR_NO_MEM if there is no memory for allocating main structure + */ +esp_err_t esp_lcd_touch_new_i2c_gt911(const esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *config, + esp_lcd_touch_handle_t *out_touch); + +/** + * @brief I2C address of the GT911 controller + * + * @note When power-on detects low level of the interrupt gpio, address is 0x5D. + * @note Interrupt gpio is high level, address is 0x14. + * + */ +#define ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS (0x5D) +#define ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP (0x14) + +/** + * @brief GT911 Configuration Type + * + */ +typedef struct { + uint8_t dev_addr; /*!< I2C device address */ +} esp_lcd_touch_io_gt911_config_t; + +/** + * @brief Touch IO configuration structure + * + */ +#define ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG() \ + { \ + .scl_speed_hz = 100000, \ + .dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, \ + .control_phase_bytes = 1, \ + .dc_bit_offset = 0, \ + .lcd_cmd_bits = 16, \ + .flags = \ + { \ + .disable_control_phase = 1, \ + } \ + } + +#ifdef __cplusplus +} +#endif diff --git a/components/m5ioe1/CMakeLists.txt b/components/m5ioe1/CMakeLists.txt new file mode 100644 index 0000000..727dec2 --- /dev/null +++ b/components/m5ioe1/CMakeLists.txt @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: MIT +idf_component_register( + SRCS "src/M5IOE1.cpp" + INCLUDE_DIRS "src" + REQUIRES driver esp_driver_gpio esp_driver_i2c esp_rom +) diff --git a/components/m5ioe1/LICENSE b/components/m5ioe1/LICENSE new file mode 100644 index 0000000..98d36b6 --- /dev/null +++ b/components/m5ioe1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 M5Stack Technology CO LTD + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/components/m5ioe1/src/M5IOE1.cpp b/components/m5ioe1/src/M5IOE1.cpp new file mode 100644 index 0000000..af98ac0 --- /dev/null +++ b/components/m5ioe1/src/M5IOE1.cpp @@ -0,0 +1,5285 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#include "M5IOE1.h" +#include +#include + +static const char* TAG = "M5IOE1"; +static const char* TAG_I2C = "M5IOE1_I2C"; +static const char* TAG_GPIO = "M5IOE1_GPIO"; +static const char* TAG_ADC = "M5IOE1_ADC"; +static const char* TAG_PWM = "M5IOE1_PWM"; +static const char* TAG_LED = "M5IOE1_LED"; +static const char* TAG_AMP = "M5IOE1_AMP"; +static const char* TAG_IRQ = "M5IOE1_IRQ"; +static const char* TAG_SYS = "M5IOE1_SYS"; + +#ifdef ARDUINO +#include +#define M5IOE1_DELAY_MS(ms) delay(ms) +#define M5IOE1_GET_TIME_MS() millis() + +// Arduino 日志级别控制 +// Arduino log level control +static m5ioe1_log_level_t _m5ioe1_log_level = M5IOE1_LOG_LEVEL_INFO; + +#define M5IOE1_LOG_I(tag, fmt, ...) \ + do { \ + if (_m5ioe1_log_level >= M5IOE1_LOG_LEVEL_INFO) { \ + log_printf("[I][%s] " fmt "\r\n", tag, ##__VA_ARGS__); \ + } \ + } while (0) + +#define M5IOE1_LOG_W(tag, fmt, ...) \ + do { \ + if (_m5ioe1_log_level >= M5IOE1_LOG_LEVEL_WARN) { \ + log_printf("[W][%s] " fmt "\r\n", tag, ##__VA_ARGS__); \ + } \ + } while (0) + +#define M5IOE1_LOG_E(tag, fmt, ...) \ + do { \ + if (_m5ioe1_log_level >= M5IOE1_LOG_LEVEL_ERROR) { \ + log_printf("[E][%s] " fmt "\r\n", tag, ##__VA_ARGS__); \ + } \ + } while (0) + +#define M5IOE1_LOG_D(tag, fmt, ...) \ + do { \ + if (_m5ioe1_log_level >= M5IOE1_LOG_LEVEL_DEBUG) { \ + log_printf("[D][%s] " fmt "\r\n", tag, ##__VA_ARGS__); \ + } \ + } while (0) + +#define M5IOE1_LOG_V(tag, fmt, ...) \ + do { \ + if (_m5ioe1_log_level >= M5IOE1_LOG_LEVEL_VERBOSE) { \ + log_printf("[V][%s] " fmt "\r\n", tag, ##__VA_ARGS__); \ + } \ + } while (0) + +// (Arduino 轮询/中断任务句柄已移至实例成员 _pollTask,支持多实例) +// (Arduino polling/interrupt task handles moved to instance member _pollTask for multi-instance support) +#else +// 强制本组件编译所有级别的日志,实际输出由运行时的 esp_log_level_set 控制 +// Force compile all log levels for this component; actual output controlled by esp_log_level_set at runtime +#ifndef LOG_LOCAL_LEVEL +#define LOG_LOCAL_LEVEL ESP_LOG_VERBOSE +#endif +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "driver/gpio.h" +#define M5IOE1_DELAY_MS(ms) vTaskDelay(pdMS_TO_TICKS(ms)) +#define M5IOE1_GET_TIME_MS() (xTaskGetTickCount() * portTICK_PERIOD_MS) +#define M5IOE1_LOG_I(tag, fmt, ...) ESP_LOGI(tag, fmt, ##__VA_ARGS__) +#define M5IOE1_LOG_W(tag, fmt, ...) ESP_LOGW(tag, fmt, ##__VA_ARGS__) +#define M5IOE1_LOG_E(tag, fmt, ...) ESP_LOGE(tag, fmt, ##__VA_ARGS__) +#define M5IOE1_LOG_D(tag, fmt, ...) ESP_LOGD(tag, fmt, ##__VA_ARGS__) +#define M5IOE1_LOG_V(tag, fmt, ...) ESP_LOGV(tag, fmt, ##__VA_ARGS__) + +// ESP-IDF 平台日志级别控制 +// ESP-IDF platform log level control +static m5ioe1_log_level_t _m5ioe1_current_log_level = M5IOE1_LOG_LEVEL_INFO; +#endif + +// ============================ +// 全局日志级别控制 +// Global Log Level Control +// ============================ + +void M5IOE1::setLogLevel(m5ioe1_log_level_t level) +{ +#ifdef ARDUINO + _m5ioe1_log_level = level; +#else + _m5ioe1_current_log_level = level; + + // 将 M5IOE1 日志级别映射到 ESP-IDF 日志级别 + // Map M5IOE1 log level to ESP-IDF log level + esp_log_level_t esp_level; + switch (level) { + case M5IOE1_LOG_LEVEL_NONE: + esp_level = ESP_LOG_NONE; + break; + case M5IOE1_LOG_LEVEL_ERROR: + esp_level = ESP_LOG_ERROR; + break; + case M5IOE1_LOG_LEVEL_WARN: + esp_level = ESP_LOG_WARN; + break; + case M5IOE1_LOG_LEVEL_INFO: + esp_level = ESP_LOG_INFO; + break; + case M5IOE1_LOG_LEVEL_DEBUG: + esp_level = ESP_LOG_DEBUG; + break; + case M5IOE1_LOG_LEVEL_VERBOSE: + esp_level = ESP_LOG_VERBOSE; + break; + default: + esp_level = ESP_LOG_INFO; + break; + } + + esp_log_level_set(TAG, esp_level); +#endif +} + +m5ioe1_log_level_t M5IOE1::getLogLevel() +{ +#ifdef ARDUINO + return _m5ioe1_log_level; +#else + return _m5ioe1_current_log_level; +#endif +} + +void M5IOE1::enableDefaultInterruptLog(bool enable) +{ + _enableDefaultIsrLog = enable; +} + +// ============================ +// 构造函数 +// 析构函数 +// ============================ + +M5IOE1::M5IOE1() +{ + _addr = M5IOE1_DEFAULT_ADDR; + _initialized = false; + _autoSnapshot = true; + _enableDefaultIsrLog = false; + _requestedSpeed = M5IOE1_I2C_FREQ_DEFAULT; + _autoWakeEnabled = false; + _lastCommTime = 0; + _intMode = M5IOE1_INT_MODE_DISABLED; + _intPin = -1; + _pollingInterval = 5000; + _pinStatesValid = false; + _pwmStatesValid = false; + _pwmFrequency = 0; + _adcStateValid = false; + _i2cConfigValid = false; + +#ifdef ARDUINO + _wire = nullptr; + _sda = 0; + _scl = 0; +#if M5IOE1_HAS_M5UNIFIED_I2C + _m5_i2c = nullptr; + _commFreq = 0; +#endif +#else + _i2cDriverType = M5IOE1_I2C_DRIVER_NONE; +#if M5IOE1_HAS_I2C_MASTER + _i2c_master_bus = nullptr; + _i2c_master_dev = nullptr; +#endif // M5IOE1_HAS_I2C_MASTER +#if M5IOE1_HAS_I2C_BUS + _i2c_bus = nullptr; + _i2c_device = nullptr; +#endif + _busExternal = false; + _sda = -1; + _scl = -1; + _port = I2C_NUM_0; + _intrQueue = nullptr; +#if M5IOE1_HAS_M5UNIFIED_I2C + _m5_i2c = nullptr; + _commFreq = M5IOE1_I2C_FREQ_100K; +#endif +#endif + + _pollTask = nullptr; + + memset(_callbacks, 0, sizeof(_callbacks)); + _clearPinStates(); + _clearPwmStates(); + _clearAdcState(); + _clearI2cConfig(); + _clearAw8737a(); +} + +M5IOE1::~M5IOE1() +{ +#ifdef ARDUINO + _cleanupPollingArduino(); + _cleanupHardwareInterruptArduino(); +#else + _cleanupPolling(); + _cleanupHardwareInterrupt(); + + // 根据驱动类型进行清理 + // Cleanup based on driver type + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + // 自创建:先删除设备,再删除总线 + // Self-created: delete device first, then bus + if (_i2c_master_dev) { + i2c_master_bus_rm_device(_i2c_master_dev); + _i2c_master_dev = nullptr; + } + if (_i2c_master_bus) { + i2c_del_master_bus(_i2c_master_bus); + _i2c_master_bus = nullptr; + } + break; + + case M5IOE1_I2C_DRIVER_MASTER: + // 外部 i2c_master:总是删除我们创建的设备句柄,但不删除总线 + // External i2c_master: always delete the device handle we created, but not the bus + if (_i2c_master_dev) { + i2c_master_bus_rm_device(_i2c_master_dev); + _i2c_master_dev = nullptr; + } + break; +#endif // M5IOE1_HAS_I2C_MASTER + +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + // 外部 i2c_bus:总是删除我们创建的设备句柄,但不删除总线 + // External i2c_bus: always delete the device handle we created, but not the bus + if (_i2c_device) { + i2c_bus_device_delete(&_i2c_device); + _i2c_device = nullptr; + } + break; +#endif // M5IOE1_HAS_I2C_BUS + +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + // Legacy I2C:如果是自创建则卸载驱动 + // Legacy I2C: uninstall driver if self-created + if (!_busExternal) { + i2c_driver_delete(_port); + } + break; +#endif // !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + + default: + break; + } +#endif +} + +// ============================ +// 初始化函数 +// Initialization Functions +// ============================ + +#ifdef ARDUINO + +m5ioe1_err_t M5IOE1::begin(TwoWire* wire, uint8_t addr, uint8_t sda, uint8_t scl, uint32_t speed, int8_t intPin, + m5ioe1_int_mode_t intMode) +{ + _wire = wire; + _addr = addr; + _sda = sda; // 保存 SDA 引脚用于 I2C 重新初始化 + _scl = scl; // 保存 SCL 引脚用于 I2C 重新初始化 + _intPin = intPin; + + if (intMode == M5IOE1_INT_MODE_HARDWARE && _intPin < 0) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + + // 验证 I2C 频率 - M5IOE1 仅支持 100KHz 或 400KHz + // Validate I2C frequency - M5IOE1 only supports 100KHz or 400KHz + if (!_isValidI2cFrequency(speed)) { + M5IOE1_LOG_W(TAG_I2C, + "Invalid I2C frequency: %" PRIu32 + " Hz. M5IOE1 only supports 100KHz or 400KHz. Falling back to 100KHz.", + speed); + _requestedSpeed = M5IOE1_I2C_FREQ_100K; + } else { + _requestedSpeed = speed; + } + + // 始终以 100KHz 开始 - M5IOE1 在上电/复位后默认为 100KHz + // Always start with 100KHz - M5IOE1 defaults to 100KHz after power-on/reset + M5IOE1_LOG_I(TAG_I2C, "Initializing M5IOE1 with 100KHz (device default)"); + + // 在开始新的 I2C 会话之前结束之前的会话(修复 ESP_ERR_INVALID_STATE) + // End any previous I2C session before starting new one (fixes ESP_ERR_INVALID_STATE) + // 注意:ESP32 Arduino Wire 库需要足够的延时 + // Note: ESP32 Arduino Wire library needs sufficient delay + _wire->end(); + M5IOE1_DELAY_MS(50); + + // 初始化 I2C 总线并检查返回值 + // Initialize I2C bus and check return value + if (!_wire->begin(sda, scl, M5IOE1_I2C_FREQ_100K)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to initialize I2C bus (SDA=%d, SCL=%d)", sda, scl); + return M5IOE1_ERR_I2C_CONFIG; + } + + // 给 I2C 总线时间在初始化后稳定 + // Give the I2C bus time to stabilize after initialization + M5IOE1_DELAY_MS(100); + + // 使用地址解析流程:探测候选地址、REV 校验、完整初始化 + // Use address resolution: probe candidates, REV check, full init + return _resolveAndInit(addr, intMode); +} + +#if M5IOE1_HAS_M5UNIFIED_I2C +// ===================================================== +// Type A (Arduino): M5Unified I2C_Class, no hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(m5::I2C_Class* i2c, uint8_t addr, uint32_t speed, m5ioe1_int_mode_t intMode) +{ + if (!i2c || !i2c->isEnabled()) { + M5IOE1_LOG_E(TAG_I2C, "M5Unified I2C_Class not initialized"); + return M5IOE1_ERR_INVALID_ARG; + } + + _wire = nullptr; + _m5_i2c = i2c; + _addr = addr; + _sda = 0; + _scl = 0; + _intPin = -1; + + if (intMode == M5IOE1_INT_MODE_HARDWARE) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + + // 验证 I2C 频率 + // Validate I2C frequency + if (!_isValidI2cFrequency(speed)) { + M5IOE1_LOG_W(TAG_I2C, "Invalid I2C frequency: %" PRIu32 " Hz. Falling back to 100KHz.", speed); + _requestedSpeed = M5IOE1_I2C_FREQ_100K; + } else { + _requestedSpeed = speed; + } + + M5IOE1_LOG_I(TAG_I2C, "Initializing M5IOE1 with 100KHz (device default)"); + + // 步骤 1: 尝试唤醒设备(初始 100K) + // Step 1: Wake device at initial 100K + _commFreq = M5IOE1_I2C_FREQ_100K; + + // 使用地址解析流程:探测候选地址、REV 校验、完整初始化 + // Use address resolution: probe candidates, REV check, full init + return _resolveAndInit(addr, intMode); +} + +// ===================================================== +// Type B (Arduino): M5Unified I2C_Class, with hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(m5::I2C_Class* i2c, uint8_t addr, uint32_t speed, int intPin, m5ioe1_int_mode_t intMode) +{ + m5ioe1_err_t err = begin(i2c, addr, speed, M5IOE1_INT_MODE_DISABLED); + if (err != M5IOE1_OK) { + return err; + } + + _intPin = intPin; + if (intMode == M5IOE1_INT_MODE_HARDWARE && _intPin < 0) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + if (intMode != M5IOE1_INT_MODE_DISABLED) { + err = setInterruptMode(intMode); + if (err != M5IOE1_OK) { + _initialized = false; + return err; + } + } + return M5IOE1_OK; +} +#endif // M5IOE1_HAS_M5UNIFIED_I2C + +#else // ESP-IDF + +// ===================================================== +// Type 1A: Self-created I2C bus, no hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(i2c_port_t port, uint8_t addr, int sda, int scl, uint32_t speed, m5ioe1_int_mode_t intMode) +{ + _addr = addr; + _busExternal = false; +#if M5IOE1_HAS_I2C_MASTER + _i2cDriverType = M5IOE1_I2C_DRIVER_SELF_CREATED; +#else + _i2cDriverType = M5IOE1_I2C_DRIVER_LEGACY; +#endif + _intPin = -1; + _port = port; + _sda = sda; + _scl = scl; + + if (intMode == M5IOE1_INT_MODE_HARDWARE) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + + // 验证 I2C 频率 - M5IOE1 仅支持 100KHz 或 400KHz + // Validate I2C frequency - M5IOE1 only supports 100KHz or 400KHz + if (!_isValidI2cFrequency(speed)) { + M5IOE1_LOG_W(TAG_I2C, + "Invalid I2C frequency: %" PRIu32 + " Hz. M5IOE1 only supports 100KHz or 400KHz. Falling back to 100KHz.", + speed); + _requestedSpeed = M5IOE1_I2C_FREQ_100K; + } else { + _requestedSpeed = speed; + } + +#if M5IOE1_HAS_I2C_MASTER + // 始终以 100KHz 开始 - M5IOE1 在上电/复位后默认为 100KHz + // Always start with 100KHz - M5IOE1 defaults to 100KHz after power-on/reset + M5IOE1_LOG_I(TAG_I2C, "Initializing M5IOE1 with 100KHz (device default)"); + + // 使用 ESP-IDF 原生驱动创建 I2C 主总线 + // Create I2C master bus using ESP-IDF native driver + i2c_master_bus_config_t bus_config = { + .i2c_port = port, + .sda_io_num = (gpio_num_t)sda, + .scl_io_num = (gpio_num_t)scl, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .intr_priority = 0, + .trans_queue_depth = 0, + .flags = + { + .enable_internal_pullup = true, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) + .allow_pd = false, +#endif + }, + }; + + esp_err_t ret = i2c_new_master_bus(&bus_config, &_i2c_master_bus); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Failed to create I2C master bus: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + + // 使用地址解析流程:探测候选地址、REV 校验、完整初始化 + // Use address resolution: probe candidates, REV check, full init + m5ioe1_err_t res = _resolveAndInit(addr, intMode); + if (res != M5IOE1_OK) { + // 清理自创建的总线 + // Cleanup self-created bus on failure + if (_i2c_master_dev) { + i2c_master_bus_rm_device(_i2c_master_dev); + _i2c_master_dev = nullptr; + } + i2c_del_master_bus(_i2c_master_bus); + _i2c_master_bus = nullptr; + } + return res; + +#else // !M5IOE1_HAS_I2C_MASTER + // Legacy API (IDF < 5.3.0) + M5IOE1_LOG_I(TAG_I2C, "Initializing M5IOE1 with 100KHz (Legacy API, IDF < 5.3.0)"); + + i2c_config_t i2c_conf = {}; + i2c_conf.mode = I2C_MODE_MASTER; + i2c_conf.sda_io_num = sda; + i2c_conf.scl_io_num = scl; + i2c_conf.sda_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.scl_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.master.clk_speed = M5IOE1_I2C_FREQ_100K; + + esp_err_t ret = i2c_param_config(port, &i2c_conf); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "i2c_param_config failed: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + + ret = i2c_driver_install(port, I2C_MODE_MASTER, 0, 0, 0); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + M5IOE1_LOG_E(TAG_I2C, "i2c_driver_install failed: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + + // 使用地址解析流程 + // Use address resolution flow + m5ioe1_err_t res = _resolveAndInit(addr, intMode); + if (res != M5IOE1_OK && !_busExternal) { + i2c_driver_delete(port); + } + return res; + +#endif // M5IOE1_HAS_I2C_MASTER +} + +// ===================================================== +// Type 1B: Self-created I2C bus, with hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(i2c_port_t port, uint8_t addr, int sda, int scl, uint32_t speed, int intPin, + m5ioe1_int_mode_t intMode) +{ + m5ioe1_err_t err = begin(port, addr, sda, scl, speed, M5IOE1_INT_MODE_DISABLED); + if (err != M5IOE1_OK) { + return err; + } + + _intPin = intPin; + if (intMode == M5IOE1_INT_MODE_HARDWARE && _intPin < 0) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + if (intMode != M5IOE1_INT_MODE_DISABLED) { + err = setInterruptMode(intMode); + if (err != M5IOE1_OK) { + _initialized = false; + return err; + } + } + return M5IOE1_OK; +} + +#if M5IOE1_HAS_I2C_MASTER +// ===================================================== +// Type 2A: Existing i2c_master_bus_handle_t, no hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(i2c_master_bus_handle_t bus, uint8_t addr, uint32_t speed, m5ioe1_int_mode_t intMode) +{ + _addr = addr; + _busExternal = true; + _i2cDriverType = M5IOE1_I2C_DRIVER_MASTER; + _intPin = -1; + _i2c_master_bus = bus; + _sda = -1; // 外部总线未知 + // Unknown for external bus + _scl = -1; + + if (intMode == M5IOE1_INT_MODE_HARDWARE) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + + // 验证 I2C 频率 + // Validate I2C frequency + if (!_isValidI2cFrequency(speed)) { + M5IOE1_LOG_W(TAG_I2C, "Invalid I2C frequency: %" PRIu32 " Hz. Falling back to 100KHz.", speed); + _requestedSpeed = M5IOE1_I2C_FREQ_100K; + } else { + _requestedSpeed = speed; + } + + M5IOE1_LOG_I(TAG_I2C, "Initializing M5IOE1 with 100KHz (device default)"); + + // 使用地址解析流程 + // Use address resolution flow + m5ioe1_err_t res = _resolveAndInit(addr, intMode); + if (res != M5IOE1_OK && _i2c_master_dev) { + i2c_master_bus_rm_device(_i2c_master_dev); + _i2c_master_dev = nullptr; + } + return res; +} + +// ===================================================== +// Type 2B: Existing i2c_master_bus_handle_t, with hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(i2c_master_bus_handle_t bus, uint8_t addr, uint32_t speed, int intPin, + m5ioe1_int_mode_t intMode) +{ + m5ioe1_err_t err = begin(bus, addr, speed, M5IOE1_INT_MODE_DISABLED); + if (err != M5IOE1_OK) { + return err; + } + + _intPin = intPin; + if (intMode == M5IOE1_INT_MODE_HARDWARE && _intPin < 0) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + if (intMode != M5IOE1_INT_MODE_DISABLED) { + err = setInterruptMode(intMode); + if (err != M5IOE1_OK) { + _initialized = false; + return err; + } + } + return M5IOE1_OK; +} +#endif // M5IOE1_HAS_I2C_MASTER + +// ===================================================== +// Type 3A: Existing i2c_bus_handle_t, no hardware interrupt +// ===================================================== +#if M5IOE1_HAS_I2C_BUS +m5ioe1_err_t M5IOE1::begin(i2c_bus_handle_t bus, uint8_t addr, uint32_t speed, m5ioe1_int_mode_t intMode) +{ + _addr = addr; + _busExternal = true; + _i2cDriverType = M5IOE1_I2C_DRIVER_BUS; + _intPin = -1; + _i2c_bus = bus; + _sda = -1; // 外部总线未知 + // Unknown for external bus + _scl = -1; + + if (intMode == M5IOE1_INT_MODE_HARDWARE) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + + // 验证 I2C 频率 + // Validate I2C frequency + if (!_isValidI2cFrequency(speed)) { + M5IOE1_LOG_W(TAG_I2C, "Invalid I2C frequency: %" PRIu32 " Hz. Falling back to 100KHz.", speed); + _requestedSpeed = M5IOE1_I2C_FREQ_100K; + } else { + _requestedSpeed = speed; + } + + M5IOE1_LOG_I(TAG_I2C, "Initializing M5IOE1 with 100KHz (device default)"); + + // 使用地址解析流程 + // Use address resolution flow + m5ioe1_err_t res = _resolveAndInit(addr, intMode); + if (res != M5IOE1_OK && _i2c_device) { + i2c_bus_device_delete(&_i2c_device); + _i2c_device = nullptr; + } + return res; +} + +// ===================================================== +// Type 3B: Existing i2c_bus_handle_t, with hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(i2c_bus_handle_t bus, uint8_t addr, uint32_t speed, int intPin, m5ioe1_int_mode_t intMode) +{ + m5ioe1_err_t err = begin(bus, addr, speed, M5IOE1_INT_MODE_DISABLED); + if (err != M5IOE1_OK) { + return err; + } + + _intPin = intPin; + if (intMode == M5IOE1_INT_MODE_HARDWARE && _intPin < 0) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + if (intMode != M5IOE1_INT_MODE_DISABLED) { + err = setInterruptMode(intMode); + if (err != M5IOE1_OK) { + _initialized = false; + return err; + } + } + return M5IOE1_OK; +} + +#endif // M5IOE1_HAS_I2C_BUS + +#if M5IOE1_HAS_M5UNIFIED_I2C +// ===================================================== +// Type 4A: M5Unified I2C_Class, no hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(m5::I2C_Class* i2c, uint8_t addr, uint32_t speed, m5ioe1_int_mode_t intMode) +{ + if (!i2c || !i2c->isEnabled()) { + M5IOE1_LOG_E(TAG_I2C, "M5Unified I2C_Class not initialized"); + return M5IOE1_ERR_INVALID_ARG; + } + + _addr = addr; + _busExternal = true; + _i2cDriverType = M5IOE1_I2C_DRIVER_M5UNIFIED; + _intPin = -1; + _m5_i2c = i2c; + _sda = -1; + _scl = -1; + + if (intMode == M5IOE1_INT_MODE_HARDWARE) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + + // 验证 I2C 频率 + // Validate I2C frequency + if (!_isValidI2cFrequency(speed)) { + M5IOE1_LOG_W(TAG_I2C, "Invalid I2C frequency: %" PRIu32 " Hz. Falling back to 100KHz.", speed); + _requestedSpeed = M5IOE1_I2C_FREQ_100K; + } else { + _requestedSpeed = speed; + } + + M5IOE1_LOG_I(TAG_I2C, "Initializing M5IOE1 with 100KHz (device default)"); + + // 步骤 1: 设置初始通信频率 + // Step 1: Set initial communication frequency + _commFreq = M5IOE1_I2C_FREQ_100K; + + // 使用地址解析流程 + // Use address resolution flow + return _resolveAndInit(addr, intMode); +} + +// ===================================================== +// Type 4B: M5Unified I2C_Class, with hardware interrupt +// ===================================================== +m5ioe1_err_t M5IOE1::begin(m5::I2C_Class* i2c, uint8_t addr, uint32_t speed, int intPin, m5ioe1_int_mode_t intMode) +{ + m5ioe1_err_t err = begin(i2c, addr, speed, M5IOE1_INT_MODE_DISABLED); + if (err != M5IOE1_OK) { + return err; + } + + _intPin = intPin; + if (intMode == M5IOE1_INT_MODE_HARDWARE && _intPin < 0) { + M5IOE1_LOG_E(TAG_I2C, "Hardware interrupt mode requires interrupt pin"); + return M5IOE1_ERR_INVALID_ARG; + } + if (intMode != M5IOE1_INT_MODE_DISABLED) { + err = setInterruptMode(intMode); + if (err != M5IOE1_OK) { + _initialized = false; + return err; + } + } + return M5IOE1_OK; +} +#endif // M5IOE1_HAS_M5UNIFIED_I2C + +#endif // !ARDUINO + +m5ioe1_err_t M5IOE1::setInterruptMode(m5ioe1_int_mode_t intMode, uint32_t pollingIntervalMs) +{ + const char* modeName = "UNKNOWN"; + if (intMode == M5IOE1_INT_MODE_DISABLED) { + modeName = "DISABLED"; + } else if (intMode == M5IOE1_INT_MODE_POLLING) { + modeName = "POLLING"; + } else if (intMode == M5IOE1_INT_MODE_HARDWARE) { + modeName = "HARDWARE"; + } + + if (intMode == M5IOE1_INT_MODE_POLLING) { + M5IOE1_LOG_I(TAG_IRQ, "Interrupt mode -> %s (pause if I2C sleep on; resume when off)", modeName); + } else { + M5IOE1_LOG_I(TAG_IRQ, "Interrupt mode -> %s", modeName); + } + + _intMode = intMode; + _pollingInterval = pollingIntervalMs; + +#ifdef ARDUINO + _cleanupPollingArduino(); + _cleanupHardwareInterruptArduino(); + + switch (intMode) { + case M5IOE1_INT_MODE_POLLING: + if (_i2cConfigValid && _i2cConfig.sleepTime > 0) { + return M5IOE1_OK; + } + return _setupPollingArduino() ? M5IOE1_OK : M5IOE1_ERR_INTERNAL; + case M5IOE1_INT_MODE_HARDWARE: + return _setupHardwareInterruptArduino() ? M5IOE1_OK : M5IOE1_ERR_INTERNAL; + default: + break; + } +#else + _cleanupPolling(); + _cleanupHardwareInterrupt(); + + switch (intMode) { + case M5IOE1_INT_MODE_POLLING: + if (_i2cConfigValid && _i2cConfig.sleepTime > 0) { + return M5IOE1_OK; + } + return _setupPolling() ? M5IOE1_OK : M5IOE1_ERR_INTERNAL; + case M5IOE1_INT_MODE_HARDWARE: + return _setupHardwareInterrupt() ? M5IOE1_OK : M5IOE1_ERR_INTERNAL; + default: + break; + } +#endif + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setPollingInterval(float seconds) +{ + if (seconds < 0.001f || seconds > 3600.0f) { + M5IOE1_LOG_E(TAG_IRQ, "Invalid polling interval: %.3f seconds (valid range: 0.001-3600)", seconds); + return M5IOE1_ERR_INVALID_ARG; + } + + uint32_t intervalMs = (uint32_t)(seconds * 1000.0f); + _pollingInterval = intervalMs; + + // 如果当前处于轮询模式,使用新间隔重新启动 + // If currently in polling mode, restart with new interval + if (_intMode == M5IOE1_INT_MODE_POLLING) { +#ifdef ARDUINO + _cleanupPollingArduino(); + _cleanupHardwareInterruptArduino(); + if (_i2cConfigValid && _i2cConfig.sleepTime > 0) { + return M5IOE1_OK; + } + return _setupPollingArduino() ? M5IOE1_OK : M5IOE1_ERR_INTERNAL; +#else + _cleanupPolling(); + _cleanupHardwareInterrupt(); + if (_i2cConfigValid && _i2cConfig.sleepTime > 0) { + return M5IOE1_OK; + } + return _setupPolling() ? M5IOE1_OK : M5IOE1_ERR_INTERNAL; +#endif + } + + M5IOE1_LOG_I(TAG_IRQ, "Polling interval set to %.3f seconds (%u ms)", seconds, static_cast(intervalMs)); + return M5IOE1_OK; +} + +// ============================ +// 设备信息 +// Device Information +// ============================ + +m5ioe1_err_t M5IOE1::getUID(uint16_t* uid) +{ + if (uid == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg16(M5IOE1_REG_UID_L, uid) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +m5ioe1_err_t M5IOE1::getVersion(uint8_t* version) +{ + if (version == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg(M5IOE1_REG_REV, version) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +m5ioe1_err_t M5IOE1::getRefVoltage(uint16_t* voltage_mv) +{ + if (voltage_mv == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg16(M5IOE1_REG_REF_VOLTAGE_L, voltage_mv) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +// ============================ +// GPIO 功能 +// GPIO Functions +// ============================ + +m5ioe1_err_t M5IOE1::_pinModeWithErr(uint8_t pin, uint8_t mode) +{ + if (!_isValidPin(pin)) { + M5IOE1_LOG_E(TAG_GPIO, "Invalid pin"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_GPIO, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint16_t modeReg = 0, puReg = 0, pdReg = 0, drvReg = 0; + + M5IOE1_LOG_D(TAG_GPIO, "_pinModeWithErr: pin=%d mode=0x%02X", pin, mode); + + if (!_readReg16(M5IOE1_REG_GPIO_MODE_L, &modeReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_MODE register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_readReg16(M5IOE1_REG_GPIO_PU_L, &puReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_PU register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_readReg16(M5IOE1_REG_GPIO_PD_L, &pdReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_PD register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_readReg16(M5IOE1_REG_GPIO_DRV_L, &drvReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_DRV register"); + return M5IOE1_ERR_I2C_COMM; + } + + switch (mode) { + case INPUT: // 0x01 + modeReg &= ~(1 << pin); + puReg &= ~(1 << pin); + pdReg &= ~(1 << pin); + _pinStates[pin].isOutput = false; + _pinStates[pin].pull = 0; + break; + case PULLUP: // 0x04 - 仅上拉 + case INPUT_PULLUP: // 0x05 + modeReg &= ~(1 << pin); + puReg |= (1 << pin); + pdReg &= ~(1 << pin); + _pinStates[pin].isOutput = false; + _pinStates[pin].pull = 1; + break; + case PULLDOWN: // 0x08 - 仅下拉 + case INPUT_PULLDOWN: // 0x09 + modeReg &= ~(1 << pin); + puReg &= ~(1 << pin); + pdReg |= (1 << pin); + _pinStates[pin].isOutput = false; + _pinStates[pin].pull = 2; + break; + case OUTPUT: // 0x03 + // 如果此引脚上启用了 PWM 则禁用 + // Disable PWM if enabled on this pin + if (_isPwmPin(pin)) { + uint8_t ch = _getPwmChannel(pin); + uint8_t regL = M5IOE1_REG_PWM1_DUTY_L + ch * 2; + uint16_t pwmData = 0; + if (_readReg16(regL, &pwmData)) { + if (pwmData & ((uint16_t)M5IOE1_PWM_ENABLE << 8)) { + pwmData &= ~((uint16_t)M5IOE1_PWM_ENABLE << 8); + _writeReg16(regL, pwmData); + } + } + } + modeReg |= (1 << pin); + puReg &= ~(1 << pin); + pdReg &= ~(1 << pin); + drvReg &= ~(1 << pin); // 推挽 + // Push-pull + _pinStates[pin].isOutput = true; + _pinStates[pin].drive = 0; + break; + case OPEN_DRAIN: // 0x10 + case OUTPUT_OPEN_DRAIN: // 0x13 + // 如果此引脚上启用了 PWM 则禁用 + // Disable PWM if enabled on this pin + if (_isPwmPin(pin)) { + uint8_t ch = _getPwmChannel(pin); + uint8_t regL = M5IOE1_REG_PWM1_DUTY_L + ch * 2; + uint16_t pwmData = 0; + if (_readReg16(regL, &pwmData)) { + if (pwmData & ((uint16_t)M5IOE1_PWM_ENABLE << 8)) { + pwmData &= ~((uint16_t)M5IOE1_PWM_ENABLE << 8); + _writeReg16(regL, pwmData); + } + } + } + modeReg |= (1 << pin); + puReg &= ~(1 << pin); + pdReg &= ~(1 << pin); + drvReg |= (1 << pin); // 开漏 + // Open-drain + _pinStates[pin].isOutput = true; + _pinStates[pin].drive = 1; + break; + case ANALOG: // 0xC0 + // 模拟模式 - 设置为输入,无上拉下拉 + // Analog mode - set as input, no pull-up/down + modeReg &= ~(1 << pin); + puReg &= ~(1 << pin); + pdReg &= ~(1 << pin); + _pinStates[pin].isOutput = false; + _pinStates[pin].pull = 0; + break; + default: + M5IOE1_LOG_E(TAG_GPIO, "Invalid mode: %d", mode); + return M5IOE1_ERR_INVALID_ARG; + } + + // 步骤 1: 写入寄存器 + // Step 1: Write registers + if (!_writeReg16(M5IOE1_REG_GPIO_PU_L, puReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to write GPIO_PU register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_writeReg16(M5IOE1_REG_GPIO_PD_L, pdReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to write GPIO_PD register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_writeReg16(M5IOE1_REG_GPIO_DRV_L, drvReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to write GPIO_DRV register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_writeReg16(M5IOE1_REG_GPIO_MODE_L, modeReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to write GPIO_MODE register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualPu = 0, actualPd = 0, actualDrv = 0, actualMode = 0; + if (!_readReg16(M5IOE1_REG_GPIO_PU_L, &actualPu)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read back GPIO_PU register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_readReg16(M5IOE1_REG_GPIO_PD_L, &actualPd)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read back GPIO_PD register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_readReg16(M5IOE1_REG_GPIO_DRV_L, &actualDrv)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read back GPIO_DRV register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_readReg16(M5IOE1_REG_GPIO_MODE_L, &actualMode)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read back GPIO_MODE register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + bool puMatch = ((actualPu & (1 << pin)) == (puReg & (1 << pin))); + bool pdMatch = ((actualPd & (1 << pin)) == (pdReg & (1 << pin))); + bool drvMatch = ((actualDrv & (1 << pin)) == (drvReg & (1 << pin))); + bool modeMatch = ((actualMode & (1 << pin)) == (modeReg & (1 << pin))); + + if (!puMatch || !pdMatch || !drvMatch || !modeMatch) { + M5IOE1_LOG_E(TAG_GPIO, "Pin %d mode verification failed: PU=%c, PD=%c, DRV=%c, MODE=%c", pin, + puMatch ? 'Y' : 'N', pdMatch ? 'Y' : 'N', drvMatch ? 'Y' : 'N', modeMatch ? 'Y' : 'N'); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_GPIO); + + const char* modeName = (mode == INPUT) ? "INPUT" + : (mode == OUTPUT) ? "OUTPUT" + : (mode == INPUT_PULLUP) ? "INPUT_PULLUP" + : (mode == INPUT_PULLDOWN) ? "INPUT_PULLDOWN" + : (mode == OPEN_DRAIN) ? "OPEN_DRAIN" + : (mode == OUTPUT_OPEN_DRAIN) ? "OUTPUT_OPEN_DRAIN" + : "UNKNOWN"; + M5IOE1_LOG_I(TAG_GPIO, "Pin %d mode set and verified: %s (0x%02X)", pin, modeName, mode); + return M5IOE1_OK; +} + +void M5IOE1::pinMode(uint8_t pin, uint8_t mode) +{ + (void)_pinModeWithErr(pin, mode); +} + +void M5IOE1::pinModeWithRes(uint8_t pin, uint8_t mode, m5ioe1_err_t* err) +{ + if (err == nullptr) { + M5IOE1_LOG_E(TAG_GPIO, "pinModeWithRes err is null"); + return; + } + *err = _pinModeWithErr(pin, mode); +} + +m5ioe1_err_t M5IOE1::_digitalWriteWithErr(uint8_t pin, uint8_t value) +{ + if (!_isValidPin(pin)) { + M5IOE1_LOG_E(TAG_GPIO, "Invalid pin"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_GPIO, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint16_t outReg = 0; + M5IOE1_LOG_D(TAG_GPIO, "_digitalWriteWithErr: pin=%d val=%d", pin, value); + if (!_readReg16(M5IOE1_REG_GPIO_OUT_L, &outReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_OUT register"); + return M5IOE1_ERR_I2C_COMM; + } + + if (value) { + outReg |= (1 << pin); + } else { + outReg &= ~(1 << pin); + } + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg16(M5IOE1_REG_GPIO_OUT_L, outReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to write GPIO_OUT register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualOut = 0; + if (!_readReg16(M5IOE1_REG_GPIO_OUT_L, &actualOut)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read back GPIO_OUT register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + bool outMatch = ((actualOut & (1 << pin)) == (outReg & (1 << pin))); + + if (!outMatch) { + M5IOE1_LOG_E(TAG_GPIO, "Pin %d write verification failed: expected %d, actual %d", pin, value ? HIGH : LOW, + (actualOut & (1 << pin)) ? HIGH : LOW); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _pinStates[pin].outputLevel = value ? 1 : 0; + + // 步骤 5: 打印日志 + // Step 5: Log + M5IOE1_LOG_I(TAG_GPIO, "Pin %d digital write and verified: %d", pin, value ? HIGH : LOW); + + return M5IOE1_OK; +} + +void M5IOE1::digitalWrite(uint8_t pin, uint8_t value) +{ + (void)_digitalWriteWithErr(pin, value); +} + +void M5IOE1::digitalWriteWithRes(uint8_t pin, uint8_t value, m5ioe1_err_t* err) +{ + if (err == nullptr) { + M5IOE1_LOG_E(TAG_GPIO, "digitalWriteWithRes err is null"); + return; + } + *err = _digitalWriteWithErr(pin, value); +} + +m5ioe1_err_t M5IOE1::_digitalReadWithErr(uint8_t pin, int* value) +{ + if (value == nullptr) { + return M5IOE1_ERR_INVALID_ARG; + } + if (!_isValidPin(pin)) { + M5IOE1_LOG_E(TAG_GPIO, "Invalid pin"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_GPIO, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint16_t inReg = 0; + if (!_readReg16(M5IOE1_REG_GPIO_IN_L, &inReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_IN register"); + return M5IOE1_ERR_I2C_COMM; + } + + *value = (inReg & (1 << pin)) ? HIGH : LOW; + M5IOE1_LOG_D(TAG_GPIO, "_digitalReadWithErr: pin=%d -> %d", pin, *value); + return M5IOE1_OK; +} + +int M5IOE1::digitalRead(uint8_t pin) +{ + int value = -1; + if (_digitalReadWithErr(pin, &value) != M5IOE1_OK) { + return -1; + } + return value; +} + +int M5IOE1::digitalReadWithRes(uint8_t pin, m5ioe1_err_t* err) +{ + if (err == nullptr) { + M5IOE1_LOG_E(TAG_GPIO, "digitalReadWithRes err is null"); + return -1; + } + + int value = -1; + *err = _digitalReadWithErr(pin, &value); + return value; +} + +// ============================ +// 高级 GPIO 功能 +// Advanced GPIO Functions +// ============================ + +m5ioe1_err_t M5IOE1::setPullMode(uint8_t pin, uint8_t pullMode) +{ + if (!_isValidPin(pin)) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_FAIL; + + M5IOE1_LOG_D(TAG_GPIO, "setPullMode: pin=%d pull=%d", pin, pullMode); + + uint16_t puReg = 0, pdReg = 0; + if (!_readReg16(M5IOE1_REG_GPIO_PU_L, &puReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_PU register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_readReg16(M5IOE1_REG_GPIO_PD_L, &pdReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_PD register"); + return M5IOE1_ERR_I2C_COMM; + } + + puReg &= ~(1 << pin); + pdReg &= ~(1 << pin); + + if (pullMode == M5IOE1_PULL_UP) { + puReg |= (1 << pin); + _pinStates[pin].pull = 1; + } else if (pullMode == M5IOE1_PULL_DOWN) { + pdReg |= (1 << pin); + _pinStates[pin].pull = 2; + } else { + _pinStates[pin].pull = 0; + } + + // 步骤 1: 写入寄存器 + // Step 1: Write registers + if (!_writeReg16(M5IOE1_REG_GPIO_PU_L, puReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to write GPIO_PU register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_writeReg16(M5IOE1_REG_GPIO_PD_L, pdReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to write GPIO_PD register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualPu = 0, actualPd = 0; + if (!_readReg16(M5IOE1_REG_GPIO_PU_L, &actualPu)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read back GPIO_PU register"); + return M5IOE1_ERR_I2C_COMM; + } + if (!_readReg16(M5IOE1_REG_GPIO_PD_L, &actualPd)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read back GPIO_PD register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + bool puMatch = ((actualPu & (1 << pin)) == (puReg & (1 << pin))); + bool pdMatch = ((actualPd & (1 << pin)) == (pdReg & (1 << pin))); + + if (!puMatch || !pdMatch) { + M5IOE1_LOG_E(TAG_GPIO, "Pin %d pull mode verification failed: PU=%c, PD=%c", pin, puMatch ? 'Y' : 'N', + pdMatch ? 'Y' : 'N'); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_GPIO); + + const char* pullName = (pullMode == M5IOE1_PULL_NONE) ? "NONE" + : (pullMode == M5IOE1_PULL_UP) ? "PULL_UP" + : (pullMode == M5IOE1_PULL_DOWN) ? "PULL_DOWN" + : "UNKNOWN"; + M5IOE1_LOG_I(TAG_GPIO, "Pin %d pull mode set and verified: %s", pin, pullName); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setDriveMode(uint8_t pin, uint8_t driveMode) +{ + if (!_isValidPin(pin)) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_FAIL; + + M5IOE1_LOG_D(TAG_GPIO, "setDriveMode: pin=%d drive=%d", pin, driveMode); + + uint16_t drvReg = 0; + if (!_readReg16(M5IOE1_REG_GPIO_DRV_L, &drvReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read GPIO_DRV register"); + return M5IOE1_ERR_I2C_COMM; + } + + if (driveMode == M5IOE1_DRIVE_OPENDRAIN) { + drvReg |= (1 << pin); + _pinStates[pin].drive = 1; + } else { + drvReg &= ~(1 << pin); + _pinStates[pin].drive = 0; + } + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg16(M5IOE1_REG_GPIO_DRV_L, drvReg)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to write GPIO_DRV register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualDrv = 0; + if (!_readReg16(M5IOE1_REG_GPIO_DRV_L, &actualDrv)) { + M5IOE1_LOG_E(TAG_GPIO, "Failed to read back GPIO_DRV register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + bool drvMatch = ((actualDrv & (1 << pin)) == (drvReg & (1 << pin))); + + if (!drvMatch) { + M5IOE1_LOG_E(TAG_GPIO, "Pin %d drive mode verification failed: DRV=%c", pin, drvMatch ? 'Y' : 'N'); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_GPIO); + + M5IOE1_LOG_I(TAG_GPIO, "Pin %d drive mode set and verified: %s", pin, + (driveMode == M5IOE1_DRIVE_OPENDRAIN) ? "OpenDrain" : "PushPull"); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::getInputState(uint8_t pin, uint8_t* state) +{ + if (!_isValidPin(pin)) return M5IOE1_ERR_INVALID_ARG; + if (state == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_FAIL; + + int val = digitalRead(pin); + if (val < 0) return M5IOE1_ERR_I2C_COMM; + + *state = (uint8_t)val; + return M5IOE1_OK; +} + +// ============================ +// 中断功能 +// Interrupt Functions +// ============================ + +void M5IOE1::attachInterrupt(uint8_t pin, m5ioe1_callback_t callback, uint8_t mode) +{ + if (!_isValidPin(pin) || callback == nullptr || !_initialized) return; + + // 检查冲突的中断 + // Check for conflicting interrupts + if (_hasConflictingInterrupt(pin)) { + M5IOE1_LOG_E(TAG_IRQ, "Interrupt conflict on pin %d", pin); + return; + } + + _callbacks[pin].callback = callback; + _callbacks[pin].callbackArg = nullptr; + _callbacks[pin].arg = nullptr; + _callbacks[pin].enabled = true; + _callbacks[pin].rising = (mode == RISING); + + // 将引脚配置为输入 + // Configure pin as input + uint16_t modeReg = 0; + _readReg16(M5IOE1_REG_GPIO_MODE_L, &modeReg); + modeReg &= ~(1 << pin); + + // 步骤 1: 写入 GPIO_MODE 寄存器 + // Step 1: Write GPIO_MODE register + if (!_writeReg16(M5IOE1_REG_GPIO_MODE_L, modeReg)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_MODE register for interrupt"); + return; + } + + // 配置中断 + // Configure interrupt + uint16_t ieReg = 0, itReg = 0; + _readReg16(M5IOE1_REG_GPIO_IE_L, &ieReg); + _readReg16(M5IOE1_REG_GPIO_IP_L, &itReg); + + ieReg |= (1 << pin); + if (mode == RISING) { + itReg |= (1 << pin); + } else { + itReg &= ~(1 << pin); + } + + // 步骤 2: 写入中断寄存器 + // Step 2: Write interrupt registers + if (!_writeReg16(M5IOE1_REG_GPIO_IE_L, ieReg)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_IE register"); + return; + } + if (!_writeReg16(M5IOE1_REG_GPIO_IP_L, itReg)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_IP register"); + return; + } + + // 步骤 3: 回读验证 + // Step 3: Read-back verification + uint16_t actualMode = 0, actualIe = 0, actualIt = 0; + if (!_readReg16(M5IOE1_REG_GPIO_MODE_L, &actualMode)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to read back GPIO_MODE register"); + return; + } + if (!_readReg16(M5IOE1_REG_GPIO_IE_L, &actualIe)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to read back GPIO_IE register"); + return; + } + if (!_readReg16(M5IOE1_REG_GPIO_IP_L, &actualIt)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to read back GPIO_IP register"); + return; + } + + // 步骤 4: 验证关键位是否匹配 + // Step 4: Verify critical bits match + bool modeMatch = ((actualMode & (1 << pin)) == 0); // 输入模式 + bool ieMatch = ((actualIe & (1 << pin)) != 0); // 中断使能 + bool itMatch = ((actualIt & (1 << pin)) == (itReg & (1 << pin))); // 触发方式 + + if (!modeMatch || !ieMatch || !itMatch) { + M5IOE1_LOG_E(TAG_IRQ, "Pin %d interrupt verification failed: MODE=%c, IE=%c, IT=%c", pin, modeMatch ? 'Y' : 'N', + ieMatch ? 'Y' : 'N', itMatch ? 'Y' : 'N'); + return; + } + + // 步骤 5: 验证成功,更新缓存 + // Step 5: Verification passed, update cache + _pinStates[pin].intrEnabled = true; + _pinStates[pin].intrRising = (mode == RISING); + + M5IOE1_LOG_I(TAG_IRQ, "Pin %d interrupt attached and verified: mode=%s", pin, + mode == RISING ? "RISING" : "FALLING"); +} + +void M5IOE1::attachInterruptArg(uint8_t pin, m5ioe1_callback_arg_t callback, void* arg, uint8_t mode) +{ + if (!_isValidPin(pin) || callback == nullptr || !_initialized) return; + + if (_hasConflictingInterrupt(pin)) { + M5IOE1_LOG_E(TAG_IRQ, "Interrupt conflict on pin %d", pin); + return; + } + + _callbacks[pin].callback = nullptr; + _callbacks[pin].callbackArg = callback; + _callbacks[pin].arg = arg; + _callbacks[pin].enabled = true; + _callbacks[pin].rising = (mode == RISING); + + // 将引脚配置为输入 + // Configure pin as input + uint16_t modeReg = 0; + _readReg16(M5IOE1_REG_GPIO_MODE_L, &modeReg); + modeReg &= ~(1 << pin); + + // 配置中断 + // Configure interrupt + uint16_t ieReg = 0, itReg = 0; + _readReg16(M5IOE1_REG_GPIO_IE_L, &ieReg); + _readReg16(M5IOE1_REG_GPIO_IP_L, &itReg); + + ieReg |= (1 << pin); + if (mode == RISING) { + itReg |= (1 << pin); + } else { + itReg &= ~(1 << pin); + } + + // 步骤 1: 写入寄存器 + // Step 1: Write registers + if (!_writeReg16(M5IOE1_REG_GPIO_MODE_L, modeReg)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_MODE register"); + return; + } + if (!_writeReg16(M5IOE1_REG_GPIO_IE_L, ieReg)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_IE register"); + return; + } + if (!_writeReg16(M5IOE1_REG_GPIO_IP_L, itReg)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_IP register"); + return; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualMode = 0, actualIe = 0, actualIt = 0; + if (!_readReg16(M5IOE1_REG_GPIO_MODE_L, &actualMode)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to read back GPIO_MODE register"); + return; + } + if (!_readReg16(M5IOE1_REG_GPIO_IE_L, &actualIe)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to read back GPIO_IE register"); + return; + } + if (!_readReg16(M5IOE1_REG_GPIO_IP_L, &actualIt)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to read back GPIO_IP register"); + return; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + bool modeMatch = ((actualMode & (1 << pin)) == 0); // 输入模式 + bool ieMatch = ((actualIe & (1 << pin)) != 0); // 中断使能 + bool itMatch = ((actualIt & (1 << pin)) == (itReg & (1 << pin))); // 触发方式 + + if (!modeMatch || !ieMatch || !itMatch) { + M5IOE1_LOG_E(TAG_IRQ, "Pin %d interrupt verification failed: MODE=%c, IE=%c, IT=%c", pin, modeMatch ? 'Y' : 'N', + ieMatch ? 'Y' : 'N', itMatch ? 'Y' : 'N'); + return; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _pinStates[pin].intrEnabled = true; + _pinStates[pin].intrRising = (mode == RISING); + + M5IOE1_LOG_I(TAG_IRQ, "Pin %d interrupt attached and verified: mode=%s", pin, + mode == RISING ? "RISING" : "FALLING"); +} + +void M5IOE1::detachInterrupt(uint8_t pin) +{ + if (!_isValidPin(pin) || !_initialized) return; + + uint16_t ieReg = 0; + _readReg16(M5IOE1_REG_GPIO_IE_L, &ieReg); + ieReg &= ~(1 << pin); + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg16(M5IOE1_REG_GPIO_IE_L, ieReg)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_IE register for detach"); + return; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualIe = 0; + if (!_readReg16(M5IOE1_REG_GPIO_IE_L, &actualIe)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to read back GPIO_IE register for detach"); + return; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + bool ieMatch = ((actualIe & (1 << pin)) == 0); // 中断已禁用 + + if (!ieMatch) { + M5IOE1_LOG_E(TAG_IRQ, "Pin %d interrupt detach verification failed: IE=%c", pin, ieMatch ? 'Y' : 'N'); + return; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _callbacks[pin].callback = nullptr; + _callbacks[pin].callbackArg = nullptr; + _callbacks[pin].arg = nullptr; + _callbacks[pin].enabled = false; + _pinStates[pin].intrEnabled = false; + + M5IOE1_LOG_I(TAG_IRQ, "Pin %d interrupt detached and verified", pin); +} + +void M5IOE1::enableInterrupt(uint8_t pin) +{ + if (!_isValidPin(pin)) return; + _callbacks[pin].enabled = true; +} + +void M5IOE1::disableInterrupt(uint8_t pin) +{ + if (!_isValidPin(pin)) return; + _callbacks[pin].enabled = false; +} + +m5ioe1_err_t M5IOE1::getInterruptStatus(uint16_t* status) +{ + if (status == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_FAIL; + + if (!_readReg16(M5IOE1_REG_GPIO_IS_L, status)) { + return M5IOE1_ERR_I2C_COMM; + } + M5IOE1_LOG_D(TAG_IRQ, "IRQ status: 0x%04X", *status); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::clearInterrupt() +{ + if (!_initialized) return M5IOE1_FAIL; + + // GPIO_IS 寄存器是"写0清除"语义,必须写入0来清除所有中断 + // GPIO_IS register uses "write 0 to clear" semantics, must write 0 to clear all interrupts + if (!_writeReg16(M5IOE1_REG_GPIO_IS_L, 0)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_IS register for clear"); + return M5IOE1_ERR_I2C_COMM; + } + + M5IOE1_LOG_I(TAG_IRQ, "Interrupt cleared (all pins)"); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::clearInterrupt(uint8_t pin) +{ + if (!_initialized) return M5IOE1_FAIL; + if (pin >= M5IOE1_MAX_GPIO_PINS) { + M5IOE1_LOG_E(TAG_IRQ, "Invalid pin: %d", pin); + return M5IOE1_ERR_INVALID_ARG; + } + + // GPIO_IS 寄存器为"写 0 清除"语义:写 0 清除对应位,写 1 不影响 + // GPIO_IS register uses "write 0 to clear" semantics: write 0 clears the bit, write 1 has no effect + uint16_t mask = ~(1U << pin) & 0xFFFF; + if (!_writeReg16(M5IOE1_REG_GPIO_IS_L, mask)) { + M5IOE1_LOG_E(TAG_IRQ, "Failed to write GPIO_IS register for pin %d", pin); + return M5IOE1_ERR_I2C_COMM; + } + + M5IOE1_LOG_I(TAG_IRQ, "Interrupt cleared (pin %d)", pin); + return M5IOE1_OK; +} + +// ============================ +// ADC 功能 +// ADC Functions +// ============================ + +m5ioe1_err_t M5IOE1::analogRead(uint8_t channel, uint16_t* result) +{ + if (result == nullptr) { + M5IOE1_LOG_E(TAG_ADC, "analogRead result is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (channel < 1 || channel > 4) { + M5IOE1_LOG_E(TAG_ADC, "Invalid ADC channel: %d", channel); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_ADC, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + // 开始转换 + // Start conversion + uint8_t ctrl = (channel & M5IOE1_ADC_CH_MASK) | M5IOE1_ADC_START; + if (!_writeReg(M5IOE1_REG_ADC_CTRL, ctrl)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to write ADC_CTRL register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 等待完成 + // Wait for completion + uint8_t reg = 0; + int tries = 0; + do { + M5IOE1_DELAY_MS(1); + if (!_readReg(M5IOE1_REG_ADC_CTRL, ®)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to read ADC_CTRL register"); + return M5IOE1_ERR_I2C_COMM; + } + M5IOE1_LOG_V(TAG_ADC, "ADC ch%d waiting... tries=%d status=0x%02X", channel, tries, reg); + tries++; + } while ((reg & M5IOE1_ADC_BUSY) && tries < 20); + + if (reg & M5IOE1_ADC_BUSY) { + M5IOE1_LOG_E(TAG_ADC, "ADC conversion timeout on channel %d", channel); + return M5IOE1_ERR_TIMEOUT; + } + + if (!_readReg16(M5IOE1_REG_ADC_DATA_L, result)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to read ADC_DATA register"); + return M5IOE1_ERR_I2C_COMM; + } + M5IOE1_LOG_D(TAG_ADC, "ADC ch%d: raw=%u (tries=%d)", channel, *result, tries); + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_ADC); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::isAdcBusy(bool* busy) +{ + if (busy == nullptr) { + M5IOE1_LOG_E(TAG_ADC, "isAdcBusy busy is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_ADC, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t ctrl = 0; + if (!_readReg(M5IOE1_REG_ADC_CTRL, &ctrl)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to read ADC_CTRL register"); + return M5IOE1_ERR_I2C_COMM; + } + + *busy = (ctrl & M5IOE1_ADC_BUSY) != 0; + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::disableAdc() +{ + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg(M5IOE1_REG_ADC_CTRL, 0)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to write ADC_CTRL register for disable"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint8_t actualCtrl = 0; + if (!_readReg(M5IOE1_REG_ADC_CTRL, &actualCtrl)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to read back ADC_CTRL register for disable"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + // START/BUSY 位应该清零,通道位也应该为 0 + // START/BUSY bits should be cleared, channel bits should also be 0 + if (actualCtrl != 0) { + M5IOE1_LOG_E(TAG_ADC, "ADC disable verification failed: expected=0, actual=0x%02X", actualCtrl); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_ADC); + + M5IOE1_LOG_I(TAG_ADC, "ADC disabled and verified"); + return M5IOE1_OK; +} + +// ============================ +// 温度传感器 +// Temperature Sensor +// ============================ + +m5ioe1_err_t M5IOE1::readTemperature(uint16_t* temperature) +{ + if (temperature == nullptr) { + M5IOE1_LOG_E(TAG_ADC, "readTemperature temperature is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_ADC, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + if (!_writeReg(M5IOE1_REG_TEMP_CTRL, M5IOE1_TEMP_START)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to write TEMP_CTRL register"); + return M5IOE1_ERR_I2C_COMM; + } + + uint8_t ctrl = 0; + int tries = 0; + do { + M5IOE1_DELAY_MS(1); + if (!_readReg(M5IOE1_REG_TEMP_CTRL, &ctrl)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to read TEMP_CTRL register"); + return M5IOE1_ERR_I2C_COMM; + } + tries++; + } while ((ctrl & M5IOE1_TEMP_BUSY) && tries < 20); + + if (ctrl & M5IOE1_TEMP_BUSY) { + M5IOE1_LOG_E(TAG_ADC, "Temperature conversion timeout"); + return M5IOE1_ERR_TIMEOUT; + } + + if (!_readReg16(M5IOE1_REG_TEMP_DATA_L, temperature)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to read TEMP_DATA register"); + return M5IOE1_ERR_I2C_COMM; + } + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::isTemperatureBusy(bool* busy) +{ + if (busy == nullptr) { + M5IOE1_LOG_E(TAG_ADC, "isTemperatureBusy busy is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_ADC, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t ctrl = 0; + if (!_readReg(M5IOE1_REG_TEMP_CTRL, &ctrl)) { + M5IOE1_LOG_E(TAG_ADC, "Failed to read TEMP_CTRL register"); + return M5IOE1_ERR_I2C_COMM; + } + + *busy = (ctrl & M5IOE1_TEMP_BUSY) != 0; + return M5IOE1_OK; +} + +// ============================ +// PWM 功能 +// PWM Functions +// ============================ + +m5ioe1_err_t M5IOE1::setPwmFrequency(uint16_t frequency) +{ + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + M5IOE1_LOG_D(TAG_PWM, "setPwmFrequency: %d Hz", frequency); + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg16(M5IOE1_REG_PWM_FREQ_L, frequency)) { + M5IOE1_LOG_E(TAG_PWM, "Failed to write PWM_FREQ register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualFreq = 0; + if (!_readReg16(M5IOE1_REG_PWM_FREQ_L, &actualFreq)) { + M5IOE1_LOG_E(TAG_PWM, "Failed to read back PWM_FREQ register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + if (actualFreq != frequency) { + M5IOE1_LOG_E(TAG_PWM, "PWM frequency verification failed: expected=%d, actual=%d", frequency, actualFreq); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _pwmFrequency = frequency; + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_PWM); + + M5IOE1_LOG_I(TAG_PWM, "PWM frequency set and verified: %d Hz", frequency); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::getPwmFrequency(uint16_t* frequency) +{ + if (frequency == nullptr) { + M5IOE1_LOG_E(TAG_PWM, "getPwmFrequency frequency is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_PWM, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + if (!_readReg16(M5IOE1_REG_PWM_FREQ_L, frequency)) { + M5IOE1_LOG_E(TAG_PWM, "Failed to read PWM_FREQ register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 更新缓存 + // Update cache + _pwmFrequency = *frequency; + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setPwmDuty(uint8_t channel, uint8_t duty, bool polarity, bool enable) +{ + if (channel > 3 || duty > 100) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + // 将百分比转换为 12 位 (0-4095) + // Convert percentage to 12-bit (0-4095) + uint16_t duty12 = (uint16_t)((duty * 0x0FFF) / 100); + return setPwmDuty12bit(channel, duty12, polarity, enable); +} + +m5ioe1_err_t M5IOE1::getPwmDuty(uint8_t channel, uint8_t* duty, bool* polarity, bool* enable) +{ + if (channel > 3) { + M5IOE1_LOG_E(TAG_PWM, "Invalid PWM channel: %d", channel); + return M5IOE1_ERR_INVALID_ARG; + } + if (duty == nullptr || polarity == nullptr || enable == nullptr) { + M5IOE1_LOG_E(TAG_PWM, "getPwmDuty output pointer is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_PWM, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t regL = M5IOE1_REG_PWM1_DUTY_L + (channel * 2); + uint16_t data = 0; + if (!_readReg16(regL, &data)) { + M5IOE1_LOG_E(TAG_PWM, "Failed to read PWM channel %d duty register", channel); + return M5IOE1_ERR_I2C_COMM; + } + + uint16_t duty12 = data & 0x0FFF; + *duty = (uint8_t)((duty12 * 100) / 0x0FFF); + *polarity = (data & ((uint16_t)M5IOE1_PWM_POLARITY << 8)) != 0; + *enable = (data & ((uint16_t)M5IOE1_PWM_ENABLE << 8)) != 0; + + // 更新缓存 + // Update cache + _pwmStates[channel].duty12 = duty12; + _pwmStates[channel].polarity = *polarity; + _pwmStates[channel].enabled = *enable; + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setPwmDuty12bit(uint8_t channel, uint16_t duty12, bool polarity, bool enable) +{ + if (channel > 3 || duty12 > 0x0FFF) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + M5IOE1_LOG_D(TAG_PWM, "setPwmDuty12bit: ch=%d duty=%d pol=%s en=%s", channel, duty12, polarity ? "Inv" : "Norm", + enable ? "On" : "Off"); + + // 获取对应的引脚 + // Get corresponding pin + uint8_t pin = (channel == 0) ? 8 : (channel == 1) ? 7 : (channel == 2) ? 10 : 9; + + uint8_t regL = M5IOE1_REG_PWM1_DUTY_L + (channel * 2); + + uint8_t dataL = (uint8_t)(duty12 & 0xFF); + uint8_t dataH = (uint8_t)((duty12 >> 8) & 0x0F); + if (polarity) dataH |= M5IOE1_PWM_POLARITY; + if (enable) dataH |= M5IOE1_PWM_ENABLE; + + uint8_t buf[2] = {dataL, dataH}; + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeBytes(regL, buf, 2)) { + M5IOE1_LOG_E(TAG_PWM, "Failed to write PWM channel %d duty register", channel); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualData = 0; + if (!_readReg16(regL, &actualData)) { + M5IOE1_LOG_E(TAG_PWM, "Failed to read back PWM channel %d duty register", channel); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + uint16_t actualDuty12 = actualData & 0x0FFF; + bool actualPolarity = (actualData & ((uint16_t)M5IOE1_PWM_POLARITY << 8)) != 0; + bool actualEnable = (actualData & ((uint16_t)M5IOE1_PWM_ENABLE << 8)) != 0; + + if (actualDuty12 != duty12 || actualPolarity != polarity || actualEnable != enable) { + M5IOE1_LOG_E(TAG_PWM, "PWM channel %d verification failed: duty=%d/%d, pol=%d/%d, en=%d/%d", channel, duty12, + actualDuty12, polarity, actualPolarity, enable, actualEnable); + return M5IOE1_FAIL; + } + + // 步骤 4: 如果启用,将引脚设置为输出模式 + // Step 4: If enabled, set pin to output mode + if (enable) { + uint16_t modeReg = 0; + if (_readReg16(M5IOE1_REG_GPIO_MODE_L, &modeReg)) { + modeReg |= (1 << pin); + if (!_writeReg16(M5IOE1_REG_GPIO_MODE_L, modeReg)) { + M5IOE1_LOG_W(TAG_PWM, "Failed to set pin %d to output mode for PWM", pin); + } + } + } + + // 步骤 5: 验证成功,更新缓存 + // Step 5: Verification passed, update cache + _pwmStates[channel].duty12 = duty12; + _pwmStates[channel].polarity = polarity; + _pwmStates[channel].enabled = enable; + + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_PWM | M5IOE1_SNAPSHOT_DOMAIN_GPIO); + + M5IOE1_LOG_I(TAG_PWM, "PWM ch%d verified: duty=%d pol=%s en=%s", channel, duty12, polarity ? "Inv" : "Norm", + enable ? "On" : "Off"); + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setPwmConfig(uint8_t channel, bool enable, bool polarity, uint16_t frequency, uint16_t duty12) +{ + if (channel > 3 || duty12 > 0x0FFF) { + M5IOE1_LOG_E(TAG_PWM, "Invalid PWM config: ch=%d duty12=%d", channel, duty12); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_PWM, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + static const uint8_t kPwmPinMap[4] = {8, 7, 10, 9}; + uint8_t pin = kPwmPinMap[channel]; + + m5ioe1_validation_t validation = validateConfig(pin, M5IOE1_CONFIG_PWM, enable); + if (!validation.valid) { + M5IOE1_LOG_W(TAG_PWM, "PWM config warning on IO%d: %s", pin + 1, validation.error_msg); + } + + if (_pinStatesValid && _pinStates[pin].intrEnabled) { + M5IOE1_LOG_W(TAG_PWM, "PWM pin IO%d has interrupt enabled", pin + 1); + } + + if (_pwmStatesValid && _pwmFrequency != frequency) { + for (uint8_t other = 0; other < M5IOE1_MAX_PWM_CHANNELS; other++) { + if (other == channel) continue; + if (_pwmStates[other].enabled) { + M5IOE1_LOG_W(TAG_PWM, "PWM frequency change affects other channels: %d -> %d", _pwmFrequency, + frequency); + break; + } + } + } + + m5ioe1_err_t err = setPwmDuty12bit(channel, duty12, polarity, enable); + if (err != M5IOE1_OK) return err; + return setPwmFrequency(frequency); +} + +m5ioe1_err_t M5IOE1::getPwmDuty12bit(uint8_t channel, uint16_t* duty12, bool* polarity, bool* enable) +{ + if (channel > 3 || duty12 == nullptr || polarity == nullptr || enable == nullptr) { + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + uint8_t regL = M5IOE1_REG_PWM1_DUTY_L + (channel * 2); + uint16_t data = 0; + if (!_readReg16(regL, &data)) { + return M5IOE1_ERR_I2C_COMM; + } + + *duty12 = data & 0x0FFF; + *polarity = (data & ((uint16_t)M5IOE1_PWM_POLARITY << 8)) != 0; + *enable = (data & ((uint16_t)M5IOE1_PWM_ENABLE << 8)) != 0; + + // 更新缓存 + // Update cache + _pwmStates[channel].duty12 = *duty12; + _pwmStates[channel].polarity = *polarity; + _pwmStates[channel].enabled = *enable; + + return M5IOE1_OK; +} + +// ============================ +// Arduino 兼容 analogWrite +// Arduino-compatible analogWrite +// ============================ + +m5ioe1_err_t M5IOE1::analogWrite(uint8_t channel, uint8_t value) +{ + if (channel > 3) { + M5IOE1_LOG_E(TAG_PWM, "Invalid channel: ch=%d", channel); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_PWM, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + // 值为 0 时关闭 PWM 输出 + // Turn off PWM when value is 0 + if (value == 0) { + return setPwmDuty12bit(channel, 0, false, false); + } + + // 将 8-bit 值 (0-255) 缩放到 12-bit (0-4095) + // Scale 8-bit value (0-255) to 12-bit (0-4095) + // 公式:duty12 = (value * 4095) / 255 = value * 16 + value / 16 + // Formula: duty12 = (value * 4095) / 255 = value * 16 + value / 16 + uint16_t duty12 = (uint16_t)value * 16 + (uint16_t)value / 16; + + // 设置 PWM,默认启用输出,正常极性 + // Set PWM with output enabled, normal polarity + return setPwmDuty12bit(channel, duty12, false, true); +} + +// ============================ +// NeoPixel LED 功能 +// NeoPixel LED Functions +// ============================ + +m5ioe1_err_t M5IOE1::setLedCount(uint8_t count) +{ + if (count > M5IOE1_MAX_LED_COUNT) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + uint8_t cfg = count & M5IOE1_LED_NUM_MASK; + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg(M5IOE1_REG_LED_CFG, cfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to write LED_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint8_t actualCfg = 0; + if (!_readReg(M5IOE1_REG_LED_CFG, &actualCfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to read back LED_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + // 只比对 LED 数量位 [5:0],忽略 REFRESH 位 [6] + // Only compare LED count bits [5:0], ignore REFRESH bit [6] + bool countMatch = ((actualCfg & M5IOE1_LED_NUM_MASK) == (cfg & M5IOE1_LED_NUM_MASK)); + + if (!countMatch) { + M5IOE1_LOG_E(TAG_LED, "LED count verification failed: expected=%d, actual=%d", count, + actualCfg & M5IOE1_LED_NUM_MASK); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + + M5IOE1_LOG_I(TAG_LED, "LED count set and verified: %d", count); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setLedColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b) +{ + if (index >= M5IOE1_MAX_LED_COUNT) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + M5IOE1_LOG_D(TAG_LED, "setLedColor: idx=%d RGB(%d,%d,%d)", index, r, g, b); + + // 转换为 RGB565 + // Convert to RGB565 + uint16_t r5 = (r >> 3) & 0x1F; + uint16_t g6 = (g >> 2) & 0x3F; + uint16_t b5 = (b >> 3) & 0x1F; + uint16_t rgb565 = (r5 << 11) | (g6 << 5) | b5; + + uint8_t regAddr = M5IOE1_REG_LED_RAM_START + (index * 2); + uint8_t data[2] = {(uint8_t)(rgb565 & 0xFF), (uint8_t)((rgb565 >> 8) & 0xFF)}; + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeBytes(regAddr, data, 2)) { + M5IOE1_LOG_E(TAG_LED, "Failed to write LED_RAM register for index %d", index); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint16_t actualRgb565 = 0; + uint8_t actualData[2]; + if (!_readBytes(regAddr, actualData, 2)) { + M5IOE1_LOG_E(TAG_LED, "Failed to read back LED_RAM register for index %d", index); + return M5IOE1_ERR_I2C_COMM; + } + actualRgb565 = ((uint16_t)actualData[1] << 8) | actualData[0]; + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + if (actualRgb565 != rgb565) { + M5IOE1_LOG_E(TAG_LED, "LED color verification failed for index %d: expected=0x%04X, actual=0x%04X", index, + rgb565, actualRgb565); + return M5IOE1_FAIL; + } + + M5IOE1_LOG_I(TAG_LED, "LED color set and verified for index %d: RGB(%d,%d,%d)", index, r, g, b); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setLedColor(uint8_t index, m5ioe1_rgb_t color) +{ + return setLedColor(index, color.r, color.g, color.b); +} + +m5ioe1_err_t M5IOE1::refreshLeds() +{ + if (!_initialized) { + M5IOE1_LOG_E(TAG_LED, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_LED_CFG, &cfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to read LED_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + cfg |= M5IOE1_LED_REFRESH; + if (!_writeReg(M5IOE1_REG_LED_CFG, cfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to write LED_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::disableLeds() +{ + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg(M5IOE1_REG_LED_CFG, 0)) { + M5IOE1_LOG_E(TAG_LED, "Failed to write LED_CFG register for disable"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint8_t actualCfg = 0; + if (!_readReg(M5IOE1_REG_LED_CFG, &actualCfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to read back LED_CFG register for disable"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + // LED 数量为 0 且 REFRESH 位为 0 + // LED count is 0 and REFRESH bit is 0 + if (actualCfg != 0) { + M5IOE1_LOG_E(TAG_LED, "LED disable verification failed: expected=0, actual=0x%02X", actualCfg); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + + M5IOE1_LOG_I(TAG_LED, "LEDs disabled and verified"); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::clearLedRam() +{ + M5IOE1_LOG_D(TAG_LED, "clearLedRam"); + if (!_initialized) { + M5IOE1_LOG_E(TAG_LED, "Device not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + // 清除所有 LED RAM (0x30 - 0x6F, 共 64 字节,支持 32 个 LED) + // Clear all LED RAM (0x30 - 0x6F, 64 bytes total, supports 32 LEDs) + uint8_t zeroData[64] = {0}; + + M5IOE1_LOG_I(TAG_LED, "Clearing LED RAM..."); + + // 分块写入,每次写入 8 字节,避免 I2C 缓冲区溢出 + // Write in chunks of 8 bytes to avoid I2C buffer overflow + for (uint8_t offset = 0; offset < 64; offset += 8) { + uint8_t regAddr = M5IOE1_REG_LED_RAM_START + offset; + if (!_writeBytes(regAddr, zeroData, 8)) { + M5IOE1_LOG_E(TAG_LED, "Failed to clear LED RAM at offset %d", offset); + return M5IOE1_ERR_I2C_COMM; + } + // 每次写入后增加延迟,确保设备有时间处理 + // Add delay after each write to ensure device has time to process + M5IOE1_DELAY_MS(2); + } + + M5IOE1_LOG_I(TAG_LED, "LED RAM cleared successfully"); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setLeds(const m5ioe1_rgb_t* colors, uint8_t count, uint8_t arraySize, bool autoRefresh) +{ + // 步骤 1: 参数验证 + // Step 1: Parameter validation + if (!_initialized) { + M5IOE1_LOG_E(TAG_LED, "Device not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + if (colors == nullptr) { + M5IOE1_LOG_E(TAG_LED, "Colors array is null"); + return M5IOE1_ERR_INVALID_ARG; + } + + if (count == 0) { + M5IOE1_LOG_E(TAG_LED, "LED count cannot be 0"); + return M5IOE1_ERR_INVALID_ARG; + } + + if (count > M5IOE1_MAX_LED_COUNT) { + M5IOE1_LOG_E(TAG_LED, "LED count %d exceeds maximum %d", count, M5IOE1_MAX_LED_COUNT); + return M5IOE1_ERR_INVALID_ARG; + } + + // 边界检查:确保不会越界访问数组 + // Bounds check: ensure no out-of-bounds array access + if (count > arraySize) { + M5IOE1_LOG_E(TAG_LED, "LED count %d exceeds array size %d (would cause out-of-bounds access)", count, + arraySize); + return M5IOE1_ERR_INVALID_ARG; + } + + // 步骤 2: 设置 LED 数量 + // Step 2: Set LED count + uint8_t cfg = count & M5IOE1_LED_NUM_MASK; + if (!_writeReg(M5IOE1_REG_LED_CFG, cfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to write LED_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 验证 LED 数量设置 + // Verify LED count setting + uint8_t actualCfg = 0; + if (!_readReg(M5IOE1_REG_LED_CFG, &actualCfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to read back LED_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + if ((actualCfg & M5IOE1_LED_NUM_MASK) != cfg) { + M5IOE1_LOG_E(TAG_LED, "LED count verification failed: expected=%d, actual=%d", cfg, + actualCfg & M5IOE1_LED_NUM_MASK); + return M5IOE1_FAIL; + } + + // 步骤 3: 写入所有 LED 颜色数据并回读验证 + // Step 3: Write all LED color data and read-back verification + for (uint8_t i = 0; i < count; i++) { + // 转换为 RGB565 + // Convert to RGB565 + uint16_t r5 = (colors[i].r >> 3) & 0x1F; + uint16_t g6 = (colors[i].g >> 2) & 0x3F; + uint16_t b5 = (colors[i].b >> 3) & 0x1F; + uint16_t rgb565 = (r5 << 11) | (g6 << 5) | b5; + + uint8_t regAddr = M5IOE1_REG_LED_RAM_START + (i * 2); + uint8_t data[2] = {(uint8_t)(rgb565 & 0xFF), (uint8_t)((rgb565 >> 8) & 0xFF)}; + + if (!_writeBytes(regAddr, data, 2)) { + M5IOE1_LOG_E(TAG_LED, "Failed to write LED_RAM for index %d", i); + return M5IOE1_ERR_I2C_COMM; + } + + // 回读验证颜色数据 + // Read-back verification for color data + uint8_t actualData[2]; + if (!_readBytes(regAddr, actualData, 2)) { + M5IOE1_LOG_E(TAG_LED, "Failed to read back LED_RAM for index %d", i); + return M5IOE1_ERR_I2C_COMM; + } + uint16_t actualRgb565 = ((uint16_t)actualData[1] << 8) | actualData[0]; + if (actualRgb565 != rgb565) { + M5IOE1_LOG_E(TAG_LED, "LED color verification failed for index %d: expected=0x%04X, actual=0x%04X", i, + rgb565, actualRgb565); + return M5IOE1_FAIL; + } + } + + // 步骤 4: 更新缓存 + // Step 4: Update cache + + // 步骤 5: 可选刷新 + // Step 5: Optional refresh + if (autoRefresh) { + if (!_readReg(M5IOE1_REG_LED_CFG, &actualCfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to read LED_CFG for refresh"); + return M5IOE1_ERR_I2C_COMM; + } + actualCfg |= M5IOE1_LED_REFRESH; + if (!_writeReg(M5IOE1_REG_LED_CFG, actualCfg)) { + M5IOE1_LOG_E(TAG_LED, "Failed to trigger LED refresh"); + return M5IOE1_ERR_I2C_COMM; + } + } + + M5IOE1_LOG_I(TAG_LED, "Set %d LEDs successfully%s", count, autoRefresh ? " (refreshed)" : ""); + return M5IOE1_OK; +} + +// ============================ +// AW8737A 脉冲功能 +// AW8737A Pulse Functions +// ============================ + +m5ioe1_err_t M5IOE1::setAw8737aPulse(uint8_t pin, m5ioe1_aw8737a_pulse_t pulseNum, m5ioe1_aw8737a_refresh_t refresh) +{ + if (!_initialized) { + M5IOE1_LOG_E(TAG_AMP, "Device not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + // 验证引脚范围 (0-13) + // Validate pin range (0-13) + if (pin >= M5IOE1_MAX_GPIO_PINS) { + M5IOE1_LOG_E(TAG_AMP, "Invalid pin number: %d (valid range: 0-%d)", pin, M5IOE1_MAX_GPIO_PINS - 1); + return M5IOE1_ERR_INVALID_ARG; + } + + // 验证脉冲编号 (0-3) + // Validate pulse number (0-3) + if (pulseNum > M5IOE1_AW8737A_PULSE_3) { + M5IOE1_LOG_E(TAG_AMP, "Invalid pulse number: %d (valid range: 0-3)", pulseNum); + return M5IOE1_ERR_INVALID_ARG; + } + + // 检查引脚是否为输出模式,如果不是则自动配置为推挽输出 + // Check if pin is output mode, if not auto-configure as push-pull output + if (!_pinStates[pin].isOutput) { + M5IOE1_LOG_I(TAG_AMP, "AW8737A: Pin %d not output mode, auto-configuring as push-pull output", pin); + pinMode(pin, OUTPUT); + } + + // 保存配置到成员变量 + // Save configuration to member variables + _aw8737aConfigured = true; + _aw8737aPin = pin; + _aw8737aPulseNum = pulseNum; + + // 构建寄存器值(不含 REFRESH 位) + // Build register value (without REFRESH bit) + // [7] REFRESH | [6:5] NUM[1:0] | [4:0] GPIO[4:0] + uint8_t regValue = 0; + regValue |= (pin & M5IOE1_AW8737A_GPIO_MASK); // 位[4:0]: GPIO 选择 + // Bits[4:0]: GPIO selection + regValue |= ((pulseNum & M5IOE1_AW8737A_NUM_MASK) << M5IOE1_AW8737A_NUM_SHIFT); // 位[6:5]: 脉冲编号 + // Bits[6:5]: Pulse number + + // 写入寄存器(不含 REFRESH 位) + // Write register (without REFRESH bit) + if (!_writeReg(M5IOE1_REG_AW8737A_PULSE, regValue)) { + M5IOE1_LOG_E(TAG_AMP, "Failed to set AW8737A pulse config"); + return M5IOE1_ERR_I2C_COMM; + } + + // 更新缓存 + // Update cache + _aw8737aRegValue = regValue; + _aw8737aStateValid = true; + + // 回读验证 + // Read-back verification + uint8_t actualReg = 0; + if (!_readReg(M5IOE1_REG_AW8737A_PULSE, &actualReg)) { + M5IOE1_LOG_E(TAG_AMP, "Failed to read back AW8737A pulse register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 验证关键位是否匹配 + // Verify critical bits match + uint8_t actualValue = actualReg & 0x7F; + if (actualValue != regValue) { + M5IOE1_LOG_E(TAG_AMP, "AW8737A pulse verification failed: expected=0x%02X, actual=0x%02X", regValue, + actualValue); + return M5IOE1_FAIL; + } + + M5IOE1_LOG_I(TAG_AMP, "AW8737A pulse configured: pin=%d, num=%d (reg=0x%02X)", pin, pulseNum, regValue); + + // 如果需要立即刷新,调用 refreshAw8737aPulse + // If immediate refresh needed, call refreshAw8737aPulse + if (refresh == M5IOE1_AW8737A_REFRESH_NOW) { + return refreshAw8737aPulse(); + } + + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_AW8737A); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::refreshAw8737aPulse() +{ + if (!_initialized) { + M5IOE1_LOG_E(TAG_AMP, "Device not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + // 检查是否已配置 + // Check if configured + if (!_aw8737aConfigured) { + M5IOE1_LOG_W(TAG_AMP, "AW8737A pulse not configured, executing anyway"); + } + + // 检查引脚是否为输出模式,如果不是则自动配置为推挽输出 + // Check if pin is output mode, if not auto-configure as push-pull output + if (!_pinStates[_aw8737aPin].isOutput) { + M5IOE1_LOG_I(TAG_AMP, "AW8737A: Pin %d not output mode, auto-configuring as push-pull output", _aw8737aPin); + pinMode(_aw8737aPin, OUTPUT); + } + + // 读取当前寄存器值 + // Read current register value + uint8_t regValue = 0; + if (!_readReg(M5IOE1_REG_AW8737A_PULSE, ®Value)) { + M5IOE1_LOG_E(TAG_AMP, "Failed to read AW8737A pulse register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 设置 REFRESH 位 + // Set REFRESH bit + regValue |= M5IOE1_AW8737A_REFRESH; + + if (!_writeReg(M5IOE1_REG_AW8737A_PULSE, regValue)) { + M5IOE1_LOG_E(TAG_AMP, "Failed to refresh AW8737A pulse"); + return M5IOE1_ERR_I2C_COMM; + } + + // 输出详细日志 + // Output detailed log + const char* driveStr = (_pinStates[_aw8737aPin].drive == M5IOE1_DRIVE_PUSHPULL) ? "PUSH-PULL" : "OPEN-DRAIN"; + M5IOE1_LOG_I(TAG_AMP, "AW8737A pulse refresh: pin=%d, pulseNum=%d, mode=OUTPUT, drive=%s", _aw8737aPin, + _aw8737aPulseNum, driveStr); + + // 写入位 7 后等待 20ms,因为它会影响 I2C 通信 + // Wait 20ms after writing bit 7, as it affects I2C communication + M5IOE1_DELAY_MS(20); + + _autoSnapshotUpdate(M5IOE1_SNAPSHOT_DOMAIN_AW8737A); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setAw8737aMode(uint8_t pin, m5ioe1_aw8737a_mode_t mode, m5ioe1_aw8737a_refresh_t refresh) +{ + // Mode 直接映射到 Pulse (MODE_1=0pulse, MODE_2=1pulse, MODE_3=2pulses, MODE_4=3pulses) + // Mode directly maps to Pulse + if (mode > M5IOE1_AW8737A_MODE_4) { + M5IOE1_LOG_E(TAG_AMP, "Invalid AW8737A mode: %d (valid range: 0-3)", mode); + return M5IOE1_ERR_INVALID_ARG; + } + + return setAw8737aPulse(pin, (m5ioe1_aw8737a_pulse_t)mode, refresh); +} + +m5ioe1_err_t M5IOE1::refreshAw8737aMode() +{ + return refreshAw8737aPulse(); +} + +// ============================ +// RTC RAM 功能 +// RTC RAM Functions +// ============================ + +m5ioe1_err_t M5IOE1::writeRtcRAM(uint8_t offset, const uint8_t* data, uint8_t length) +{ + if (data == nullptr || offset >= M5IOE1_RTC_RAM_SIZE || length == 0 || (offset + length) > M5IOE1_RTC_RAM_SIZE) { + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t regAddr = M5IOE1_REG_RTC_RAM_START + offset; + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeBytes(regAddr, data, length)) { + M5IOE1_LOG_E(TAG_SYS, "Failed to write RTC_RAM register at offset %d", offset); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint8_t actualData[M5IOE1_RTC_RAM_SIZE]; + if (!_readBytes(regAddr, actualData, length)) { + M5IOE1_LOG_E(TAG_SYS, "Failed to read back RTC_RAM register at offset %d", offset); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + // 逐字节比较写入的数据 + // Compare written data byte by byte + bool allMatch = true; + for (uint8_t i = 0; i < length; i++) { + if (actualData[i] != data[i]) { + allMatch = false; + M5IOE1_LOG_E(TAG_SYS, "RTC_RAM verification failed at offset %d: expected=0x%02X, actual=0x%02X", + offset + i, data[i], actualData[i]); + break; + } + } + + if (!allMatch) { + return M5IOE1_FAIL; + } + + M5IOE1_LOG_I(TAG_SYS, "RTC_RAM write and verified: offset=%d, length=%d", offset, length); + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::readRtcRAM(uint8_t offset, uint8_t* data, uint8_t length) +{ + if (data == nullptr || offset >= M5IOE1_RTC_RAM_SIZE || length == 0 || (offset + length) > M5IOE1_RTC_RAM_SIZE) { + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t regAddr = M5IOE1_REG_RTC_RAM_START + offset; + return _readBytes(regAddr, data, length) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +// ============================ +// 系统配置 +// System Configuration +// ============================ + +m5ioe1_err_t M5IOE1::setI2cConfig(uint8_t sleepTime, m5ioe1_i2c_speed_t speed, m5ioe1_wake_edge_t wakeEdge, + m5ioe1_pull_config_t pullConfig) +{ + if (sleepTime > 15) { + M5IOE1_LOG_E(TAG_I2C, "Invalid I2C sleep time: %d", sleepTime); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_I2C, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint32_t targetFreq = (speed == M5IOE1_I2C_SPEED_400K) ? M5IOE1_I2C_FREQ_400K : M5IOE1_I2C_FREQ_100K; + bool speedChanged = (targetFreq != _requestedSpeed); + + uint8_t cfg = (sleepTime & M5IOE1_I2C_SLEEP_MASK); + if (speed == M5IOE1_I2C_SPEED_400K) cfg |= M5IOE1_I2C_SPEED_400K_BIT; + if (wakeEdge == M5IOE1_WAKE_EDGE_RISING) cfg |= M5IOE1_I2C_WAKE_RISING; + if (pullConfig == M5IOE1_PULL_DISABLED) cfg |= M5IOE1_I2C_PULL_OFF; + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg(M5IOE1_REG_I2C_CFG, cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to write I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint8_t actualCfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &actualCfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read back I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证关键位是否匹配 + // Step 3: Verify critical bits match + uint8_t expectedSleep = sleepTime & M5IOE1_I2C_SLEEP_MASK; + uint8_t actualSleep = actualCfg & M5IOE1_I2C_SLEEP_MASK; + bool actualSpeed400k = (actualCfg & M5IOE1_I2C_SPEED_400K_BIT) != 0; + bool actualWakeRising = (actualCfg & M5IOE1_I2C_WAKE_RISING) != 0; + bool actualPullOff = (actualCfg & M5IOE1_I2C_PULL_OFF) != 0; + + if (actualSleep != expectedSleep || actualSpeed400k != (speed == M5IOE1_I2C_SPEED_400K) || + actualWakeRising != (wakeEdge == M5IOE1_WAKE_EDGE_RISING) || + actualPullOff != (pullConfig == M5IOE1_PULL_DISABLED)) { + M5IOE1_LOG_E(TAG_I2C, "I2C_CFG verification failed: expected=0x%02X, actual=0x%02X", cfg, actualCfg); + return M5IOE1_FAIL; + } + + // 步骤 4: 如果速度改变,切换主机 I2C 总线 + // Step 4: If speed changed, switch host I2C bus + if (speedChanged) { + M5IOE1_LOG_D(TAG_I2C, "Switching host I2C bus from %" PRIu32 " Hz to %" PRIu32 " Hz", _requestedSpeed, + targetFreq); + +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + _commFreq = targetFreq; + M5IOE1_LOG_D(TAG_I2C, "M5Unified I2C frequency updated to %" PRIu32 " Hz", targetFreq); + } else +#endif + if (_wire != nullptr) { + _wire->end(); + M5IOE1_DELAY_MS(10); + if (!_wire->begin(_sda, _scl, targetFreq)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to reinitialize I2C bus at %" PRIu32 " Hz", targetFreq); + // 回滚设备配置 + // Rollback device config + uint8_t rollbackCfg = actualCfg; + if (speed == M5IOE1_I2C_SPEED_400K) { + rollbackCfg &= ~M5IOE1_I2C_SPEED_400K_BIT; + } else { + rollbackCfg |= M5IOE1_I2C_SPEED_400K_BIT; + } + _writeReg(M5IOE1_REG_I2C_CFG, rollbackCfg); + return M5IOE1_ERR_I2C_CONFIG; + } + M5IOE1_DELAY_MS(10); + } +#else + esp_err_t ret; + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + if (_i2c_master_dev != nullptr) { + ret = i2c_master_bus_rm_device(_i2c_master_dev); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Failed to remove I2C device: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + _i2c_master_dev = nullptr; + + i2c_device_config_t dev_config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = _addr, + .scl_speed_hz = targetFreq, + .scl_wait_us = 0, + .flags = {.disable_ack_check = false}, + }; + + ret = i2c_master_bus_add_device(_i2c_master_bus, &dev_config, &_i2c_master_dev); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Failed to add I2C device at %" PRIu32 " Hz: %s", targetFreq, + esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + } + break; +#endif // M5IOE1_HAS_I2C_MASTER + +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + if (_i2c_device != nullptr) { + ret = i2c_bus_device_delete(&_i2c_device); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Failed to delete I2C device: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + + _i2c_device = i2c_bus_device_create(_i2c_bus, _addr, targetFreq); + if (_i2c_device == nullptr) { + M5IOE1_LOG_E(TAG_I2C, "Failed to create I2C device at %" PRIu32 " Hz", targetFreq); + return M5IOE1_ERR_I2C_CONFIG; + } + } + break; +#endif // M5IOE1_HAS_I2C_BUS + +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: { + i2c_config_t i2c_conf = {}; + i2c_conf.mode = I2C_MODE_MASTER; + i2c_conf.sda_io_num = _sda; + i2c_conf.scl_io_num = _scl; + i2c_conf.sda_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.scl_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.master.clk_speed = targetFreq; + ret = i2c_param_config(_port, &i2c_conf); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "i2c_param_config failed: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + break; + } +#endif // !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + _commFreq = targetFreq; + M5IOE1_LOG_D(TAG_I2C, "M5Unified I2C frequency updated to %" PRIu32 " Hz", targetFreq); + break; +#endif + default: + break; + } +#endif + + // 步骤 5: 验证通信仍然有效 + // Step 5: Verify communication still works + uint16_t uid = 0; + if (!_readReg16(M5IOE1_REG_UID_L, &uid)) { + M5IOE1_LOG_E(TAG_I2C, "Communication failed after switching to %" PRIu32 " Hz", targetFreq); + // 回滚 + // Rollback + uint32_t originalFreq = _requestedSpeed; +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + _commFreq = originalFreq; + } else +#endif + if (_wire != nullptr) { + _wire->end(); + M5IOE1_DELAY_MS(10); + _wire->begin(_sda, _scl, originalFreq); + M5IOE1_DELAY_MS(10); + } +#else + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + if (_i2c_master_dev != nullptr) { + i2c_master_bus_rm_device(_i2c_master_dev); + i2c_device_config_t dev_config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = _addr, + .scl_speed_hz = originalFreq, + .scl_wait_us = 0, + .flags = {.disable_ack_check = false}, + }; + i2c_master_bus_add_device(_i2c_master_bus, &dev_config, &_i2c_master_dev); + } + break; +#endif // M5IOE1_HAS_I2C_MASTER +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + if (_i2c_device != nullptr) { + i2c_bus_device_delete(&_i2c_device); + _i2c_device = i2c_bus_device_create(_i2c_bus, _addr, originalFreq); + } + break; +#endif // M5IOE1_HAS_I2C_BUS +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: { + i2c_config_t i2c_conf = {}; + i2c_conf.mode = I2C_MODE_MASTER; + i2c_conf.sda_io_num = _sda; + i2c_conf.scl_io_num = _scl; + i2c_conf.sda_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.scl_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.master.clk_speed = originalFreq; + i2c_param_config(_port, &i2c_conf); // best effort rollback + break; + } +#endif // !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + _commFreq = originalFreq; + break; +#endif + default: + break; + } +#endif + // 回滚设备配置 + // Rollback device config + uint8_t rollbackCfg = actualCfg; + if (speed == M5IOE1_I2C_SPEED_400K) { + rollbackCfg &= ~M5IOE1_I2C_SPEED_400K_BIT; + } else { + rollbackCfg |= M5IOE1_I2C_SPEED_400K_BIT; + } + _writeReg(M5IOE1_REG_I2C_CFG, rollbackCfg); + return M5IOE1_ERR_I2C_COMM; + } + + // 更新请求的速度 + // Update requested speed + _requestedSpeed = targetFreq; + M5IOE1_LOG_D(TAG_I2C, "Host I2C bus switched to %" PRIu32 " Hz", targetFreq); + } + + // 步骤 6: 验证成功,更新缓存 + // Step 6: Verification passed, update cache + _i2cConfig.sleepTime = sleepTime; + _i2cConfig.speed400k = (speed == M5IOE1_I2C_SPEED_400K); + _i2cConfig.wakeRising = (wakeEdge == M5IOE1_WAKE_EDGE_RISING); + _i2cConfig.pullOff = (pullConfig == M5IOE1_PULL_DISABLED); + _i2cConfigValid = true; + + // 若配置了休眠时间,自动启用 autoWake 防止数据丢失 + // If sleep time is configured, auto-enable autoWake to prevent data loss + if (sleepTime > 0 && !_autoWakeEnabled) { + setAutoWakeEnable(true); + M5IOE1_LOG_W(TAG_I2C, "I2C sleep enabled (sleepTime=%d), auto-wake automatically enabled", sleepTime); + } + + _updatePollingForI2cSleep(sleepTime); + + M5IOE1_LOG_I(TAG_I2C, "I2C config set and verified: sleep=%d, speed=%s, wake=%s, pull=%s", sleepTime, + speed == M5IOE1_I2C_SPEED_400K ? "400K" : "100K", + wakeEdge == M5IOE1_WAKE_EDGE_RISING ? "rising" : "falling", + pullConfig == M5IOE1_PULL_DISABLED ? "off" : "on"); + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setI2cSleepTime(uint8_t sleepTime) +{ + if (sleepTime > 15) { + M5IOE1_LOG_E(TAG_I2C, "Invalid I2C sleep time: %d", sleepTime); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_I2C, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 清除 SLEEP 位 [3:0],设置新值 + // Clear SLEEP bits [3:0], set new value + cfg = (cfg & ~M5IOE1_I2C_SLEEP_MASK) | (sleepTime & M5IOE1_I2C_SLEEP_MASK); + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg(M5IOE1_REG_I2C_CFG, cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to write I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint8_t actualCfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &actualCfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read back I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证 SLEEP 位是否匹配 + // Step 3: Verify SLEEP bits match + uint8_t actualSleep = actualCfg & M5IOE1_I2C_SLEEP_MASK; + if (actualSleep != sleepTime) { + M5IOE1_LOG_E(TAG_I2C, "I2C_CFG sleep time verification failed: expected=%d, actual=%d", sleepTime, actualSleep); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _i2cConfig.sleepTime = sleepTime; + _i2cConfigValid = true; + + M5IOE1_LOG_I(TAG_I2C, "I2C sleep time set and verified to %d", sleepTime); + + // 若配置了休眠时间,自动启用 autoWake 防止数据丢失 + // If sleep time is configured, auto-enable autoWake to prevent data loss + if (sleepTime > 0 && !_autoWakeEnabled) { + setAutoWakeEnable(true); + M5IOE1_LOG_W(TAG_I2C, "I2C sleep enabled, auto-wake automatically enabled"); + } + + _updatePollingForI2cSleep(sleepTime); + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::getI2cSleepTime(uint8_t* sleepTime) +{ + if (sleepTime == nullptr) { + M5IOE1_LOG_E(TAG_I2C, "getI2cSleepTime sleepTime is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_I2C, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 更新缓存 + // Update cache + _i2cConfig.sleepTime = cfg & M5IOE1_I2C_SLEEP_MASK; + _i2cConfigValid = true; + + *sleepTime = _i2cConfig.sleepTime; + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setI2cWakeEdge(m5ioe1_wake_edge_t edge) +{ + if (!_initialized) { + M5IOE1_LOG_E(TAG_I2C, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 设置或清除 WAKE 位 [5] + // Set or clear WAKE bit [5] + if (edge == M5IOE1_WAKE_EDGE_RISING) { + cfg |= M5IOE1_I2C_WAKE_RISING; + } else { + cfg &= ~M5IOE1_I2C_WAKE_RISING; + } + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg(M5IOE1_REG_I2C_CFG, cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to write I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint8_t actualCfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &actualCfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read back I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证 WAKE 位是否匹配 + // Step 3: Verify WAKE bit matches + bool actualWakeRising = (actualCfg & M5IOE1_I2C_WAKE_RISING) != 0; + bool expectedWakeRising = (edge == M5IOE1_WAKE_EDGE_RISING); + if (actualWakeRising != expectedWakeRising) { + M5IOE1_LOG_E(TAG_I2C, "I2C_CFG wake edge verification failed: expected=%s, actual=%s", + expectedWakeRising ? "rising" : "falling", actualWakeRising ? "rising" : "falling"); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _i2cConfig.wakeRising = expectedWakeRising; + _i2cConfigValid = true; + + M5IOE1_LOG_I(TAG_I2C, "I2C wake edge set and verified to %s", expectedWakeRising ? "rising" : "falling"); + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::getI2cWakeEdge(m5ioe1_wake_edge_t* edge) +{ + if (edge == nullptr) { + M5IOE1_LOG_E(TAG_I2C, "getI2cWakeEdge edge is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_I2C, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 更新缓存 + // Update cache + _i2cConfig.wakeRising = (cfg & M5IOE1_I2C_WAKE_RISING) != 0; + _i2cConfigValid = true; + + *edge = _i2cConfig.wakeRising ? M5IOE1_WAKE_EDGE_RISING : M5IOE1_WAKE_EDGE_FALLING; + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::setI2cPullConfig(m5ioe1_pull_config_t config) +{ + if (!_initialized) { + M5IOE1_LOG_E(TAG_I2C, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 设置或清除 INT_PU/PD 位 [6] + // Set or clear INT_PU/PD bit [6] + if (config == M5IOE1_PULL_DISABLED) { + cfg |= M5IOE1_I2C_PULL_OFF; + } else { + cfg &= ~M5IOE1_I2C_PULL_OFF; + } + + // 步骤 1: 写入寄存器 + // Step 1: Write register + if (!_writeReg(M5IOE1_REG_I2C_CFG, cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to write I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 回读验证 + // Step 2: Read-back verification + uint8_t actualCfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &actualCfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read back I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 3: 验证 INT_PU/PD 位是否匹配 + // Step 3: Verify INT_PU/PD bit matches + bool actualPullOff = (actualCfg & M5IOE1_I2C_PULL_OFF) != 0; + bool expectedPullOff = (config == M5IOE1_PULL_DISABLED); + if (actualPullOff != expectedPullOff) { + M5IOE1_LOG_E(TAG_I2C, "I2C_CFG pull config verification failed: expected=%s, actual=%s", + expectedPullOff ? "disabled" : "enabled", actualPullOff ? "disabled" : "enabled"); + return M5IOE1_FAIL; + } + + // 步骤 4: 验证成功,更新缓存 + // Step 4: Verification passed, update cache + _i2cConfig.pullOff = expectedPullOff; + _i2cConfigValid = true; + + M5IOE1_LOG_I(TAG_I2C, "I2C internal pull-up set and verified to %s", expectedPullOff ? "disabled" : "enabled"); + + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::getI2cPullConfig(m5ioe1_pull_config_t* config) +{ + if (config == nullptr) { + M5IOE1_LOG_E(TAG_I2C, "getI2cPullConfig config is null"); + return M5IOE1_ERR_INVALID_ARG; + } + if (!_initialized) { + M5IOE1_LOG_E(TAG_I2C, "Not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &cfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read I2C_CFG register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 更新缓存 + // Update cache + _i2cConfig.pullOff = (cfg & M5IOE1_I2C_PULL_OFF) != 0; + _i2cConfigValid = true; + + *config = _i2cConfig.pullOff ? M5IOE1_PULL_DISABLED : M5IOE1_PULL_ENABLED; + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::factoryReset() +{ + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + if (!_writeReg(M5IOE1_REG_FACTORY_RESET, M5IOE1_FACTORY_RESET_KEY)) { + return M5IOE1_ERR_I2C_COMM; + } + + M5IOE1_DELAY_MS(100); + _initialized = false; + _pinStatesValid = false; + _pwmStatesValid = false; + _adcStateValid = false; + _clearPinStates(); + _clearPwmStates(); + _clearAdcState(); + M5IOE1_LOG_W(TAG_SYS, "Factory reset complete. Call begin() to reinitialize."); + + return M5IOE1_OK; +} + +// ============================ +// 自动唤醒功能 +// Auto Wake Feature +// ============================ + +void M5IOE1::setAutoWakeEnable(bool enable) +{ + _autoWakeEnabled = enable; + if (enable) { + _lastCommTime = M5IOE1_GET_TIME_MS(); + } + M5IOE1_LOG_I(TAG_I2C, "Auto wake %s", enable ? "enabled" : "disabled"); +} + +bool M5IOE1::isAutoWakeEnabled() const +{ + return _autoWakeEnabled; +} + +m5ioe1_err_t M5IOE1::sendWakeSignal() +{ +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + M5IOE1_M5UNIFIED_SEND_WAKE(_m5_i2c, _addr, _commFreq); + return M5IOE1_OK; + } +#endif + M5IOE1_I2C_ARDUINO_SEND_WAKE(_wire, _addr); + return M5IOE1_OK; +#else + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + return M5IOE1_I2C_MASTER_SEND_WAKE(_i2c_master_bus, _addr) == ESP_OK ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + return M5IOE1_I2C_BUS_SEND_WAKE(_i2c_device, M5IOE1_REG_REV) == ESP_OK ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + return M5IOE1_I2C_LEGACY_SEND_WAKE(_port, _addr) == ESP_OK ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + return M5IOE1_M5UNIFIED_SEND_WAKE(_m5_i2c, _addr, _commFreq) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +#endif + default: + return M5IOE1_ERR_INTERNAL; + } +#endif +} + +void M5IOE1::_checkAutoWake() +{ + if (!_autoWakeEnabled || !_i2cConfigValid || _i2cConfig.sleepTime == 0) return; + + uint32_t now = M5IOE1_GET_TIME_MS(); + uint32_t elapsed = now - _lastCommTime; + + // 如果距离上次通信超过1秒,发送唤醒信号 + // If more than 1 second since last communication, send wake signal + if (elapsed >= 1000) { + sendWakeSignal(); + M5IOE1_DELAY_MS(10); + } + _lastCommTime = now; +} + +void M5IOE1::_updatePollingForI2cSleep(uint8_t sleepTime) +{ + if (_intMode != M5IOE1_INT_MODE_POLLING) return; + +#ifdef ARDUINO + if (sleepTime > 0) { + if (_pollTask != nullptr) { + _cleanupPollingArduino(); + } + } else { + if (_pollTask == nullptr) { + _setupPollingArduino(); + } + } +#else + if (sleepTime > 0) { + if (_pollTask != nullptr) { + _cleanupPolling(); + } + } else { + if (_pollTask == nullptr) { + _setupPolling(); + } + } +#endif +} + +// ============================ +// 状态快照功能 +// State Snapshot Functions +// ============================ + +void M5IOE1::setAutoSnapshot(bool enable) +{ + _autoSnapshot = enable; +} + +bool M5IOE1::isAutoSnapshotEnabled() const +{ + return _autoSnapshot; +} + +m5ioe1_err_t M5IOE1::updateSnapshot() +{ + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + bool gpio = _snapshotPinStates(); + bool pwm = _snapshotPwmStates(); + bool adc = _snapshotAdcState(); + bool aw8737a = _snapshotAw8737a(); + + return (gpio && pwm && adc && aw8737a) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +// ============================ +// 调试功能 +// Debug Functions +// ============================ + +m5ioe1_err_t M5IOE1::getModeReg(uint16_t* reg) +{ + if (reg == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg16(M5IOE1_REG_GPIO_MODE_L, reg) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +m5ioe1_err_t M5IOE1::getOutputReg(uint16_t* reg) +{ + if (reg == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg16(M5IOE1_REG_GPIO_OUT_L, reg) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +m5ioe1_err_t M5IOE1::getInputReg(uint16_t* reg) +{ + if (reg == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg16(M5IOE1_REG_GPIO_IN_L, reg) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +m5ioe1_err_t M5IOE1::getPullUpReg(uint16_t* reg) +{ + if (reg == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg16(M5IOE1_REG_GPIO_PU_L, reg) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +m5ioe1_err_t M5IOE1::getPullDownReg(uint16_t* reg) +{ + if (reg == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg16(M5IOE1_REG_GPIO_PD_L, reg) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +m5ioe1_err_t M5IOE1::getDriveReg(uint16_t* reg) +{ + if (reg == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + return _readReg16(M5IOE1_REG_GPIO_DRV_L, reg) ? M5IOE1_OK : M5IOE1_ERR_I2C_COMM; +} + +// ============================ +// 配置验证 +// Configuration Validation +// ============================ + +m5ioe1_validation_t M5IOE1::validateConfig(uint8_t pin, m5ioe1_config_type_t configType, bool enable) +{ + m5ioe1_validation_t result = {false, {0}, 0xFF}; + + // 基本验证 + // Basic validation + if (!_isValidPin(pin)) { + snprintf(result.error_msg, sizeof(result.error_msg), "Invalid pin %d", pin); + return result; + } + + if (!_initialized) { + snprintf(result.error_msg, sizeof(result.error_msg), "Not initialized"); + return result; + } + + // 如果禁用,无需冲突检查 + // If disabling, no conflict check needed + if (!enable) { + result.valid = true; + return result; + } + + uint8_t mutexPin = 0xFF; + + switch (configType) { + case M5IOE1_CONFIG_GPIO_INPUT: + case M5IOE1_CONFIG_GPIO_OUTPUT: + // 检查引脚是否用于特殊功能 + // Check if pin is being used for special functions + if (_hasActivePwm(pin)) { + snprintf(result.error_msg, sizeof(result.error_msg), + "Pin %d is used for PWM. Disable first (e.g. setPwmDuty)", pin); + return result; + } + if (_hasActiveAdc(pin)) { + snprintf(result.error_msg, sizeof(result.error_msg), + "Pin %d is used for ADC. Disable first (disableAdc())", pin); + return result; + } + if (_isNeopixelPin(pin) && _isLedEnabled()) { + snprintf(result.error_msg, sizeof(result.error_msg), + "Pin %d (IO14) used for NeoPixel; call disableLeds()", pin); + return result; + } + break; + + case M5IOE1_CONFIG_GPIO_INTERRUPT: + // 检查中断互斥约束 + // Check interrupt mutex constraint + if (_getInterruptMutexPin(pin, &mutexPin)) { + if (_hasActiveInterrupt(mutexPin)) { + snprintf(result.error_msg, sizeof(result.error_msg), "Interrupt conflict: IO%d and IO%d are mutex", + pin + 1, mutexPin + 1); + result.conflicting_pin = mutexPin; + return result; + } + } + // 检查 IO5 的 I2C 睡眠模式冲突 + // Check I2C sleep mode conflict for IO5 + if (pin == 4 && _hasI2cSleepEnabled()) { // IO5 is pin index 4 + snprintf(result.error_msg, sizeof(result.error_msg), "IO5 interrupt disabled when I2C sleep enabled"); + return result; + } + break; + + case M5IOE1_CONFIG_ADC: + // 检查引脚是否支持 ADC + // Check if pin supports ADC + if (!_isAdcPin(pin)) { + snprintf(result.error_msg, sizeof(result.error_msg), "Pin %d does not support ADC", pin); + return result; + } + // 检查引脚是否配置为输出 + // Check if pin is configured as output + if (_pinStatesValid && _pinStates[pin].isOutput) { + snprintf(result.error_msg, sizeof(result.error_msg), "Pin %d is configured as output", pin); + return result; + } + break; + + case M5IOE1_CONFIG_PWM: + // 检查引脚是否支持 PWM + // Check if pin supports PWM + if (!_isPwmPin(pin)) { + snprintf(result.error_msg, sizeof(result.error_msg), "Pin %d does not support PWM", pin); + return result; + } + break; + + case M5IOE1_CONFIG_NEOPIXEL: + // NeoPixel 仅在 IO14 上工作(引脚索引 13) + // NeoPixel only works on IO14 (pin index 13) + if (!_isNeopixelPin(pin)) { + snprintf(result.error_msg, sizeof(result.error_msg), "NeoPixel only supported on IO14 (pin 13)"); + return result; + } + // 检查引脚是否有活动中断 + // Check if pin has active interrupt + if (_hasActiveInterrupt(pin)) { + snprintf(result.error_msg, sizeof(result.error_msg), + "IO14 has active interrupt, conflicts with NeoPixel"); + return result; + } + // 检查中断互斥(IO10 和 IO14 是互斥的) + // Check interrupt mutex (IO10 and IO14 are mutex) + if (_getInterruptMutexPin(pin, &mutexPin)) { + if (_hasActiveInterrupt(mutexPin)) { + snprintf(result.error_msg, sizeof(result.error_msg), + "IO10 has interrupt enabled, mutex with IO14 NeoPixel"); + result.conflicting_pin = mutexPin; + return result; + } + } + break; + + case M5IOE1_CONFIG_I2C_SLEEP: + // I2C 睡眠模式禁用 IO5 中断 + // I2C sleep mode disables IO5 interrupt + if (_hasActiveInterrupt(4)) { // IO5 is pin index 4 + snprintf(result.error_msg, sizeof(result.error_msg), "I2C sleep mode will disable IO5 interrupt"); + result.conflicting_pin = 4; + return result; + } + break; + + default: + snprintf(result.error_msg, sizeof(result.error_msg), "Unknown config type"); + return result; + } + + result.valid = true; + return result; +} + +// ============================ +// 内部辅助函数 +// Internal Helper Functions +// ============================ + +bool M5IOE1::_writeReg(uint8_t reg, uint8_t value) +{ + _checkAutoWake(); + for (int attempt = 0; attempt < M5IOE1_I2C_RETRY_COUNT; ++attempt) { +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + if (M5IOE1_M5UNIFIED_WRITE_BYTE(_m5_i2c, _addr, reg, value, _commFreq)) { + M5IOE1_LOG_D(TAG_I2C, "Write Reg[0x%02X] <- 0x%02X", reg, value); + return true; + } + } else +#endif + if (M5IOE1_I2C_ARDUINO_WRITE_BYTE(_wire, _addr, reg, value)) { + M5IOE1_LOG_D(TAG_I2C, "Write Reg[0x%02X] <- 0x%02X", reg, value); + return true; + } +#else + bool ok = false; + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + ok = (M5IOE1_I2C_MASTER_WRITE_BYTE(_i2c_master_dev, reg, value) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + ok = (M5IOE1_I2C_BUS_WRITE_BYTE(_i2c_device, reg, value) == ESP_OK); + break; +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + ok = (M5IOE1_I2C_LEGACY_WRITE_BYTE(_port, _addr, reg, value) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + ok = M5IOE1_M5UNIFIED_WRITE_BYTE(_m5_i2c, _addr, reg, value, _commFreq); + break; +#endif + default: + ok = false; + break; + } + if (ok) { + M5IOE1_LOG_D(TAG_I2C, "Write Reg[0x%02X] <- 0x%02X", reg, value); + return true; + } +#endif + if (attempt + 1 < M5IOE1_I2C_RETRY_COUNT) { + M5IOE1_DELAY_MS(M5IOE1_I2C_RETRY_DELAY_MS); + } + } + M5IOE1_LOG_E(TAG_I2C, "Write Reg[0x%02X] failed", reg); + return false; +} + +bool M5IOE1::_writeReg16(uint8_t reg, uint16_t value) +{ + _checkAutoWake(); + for (int attempt = 0; attempt < M5IOE1_I2C_RETRY_COUNT; ++attempt) { +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + if (M5IOE1_M5UNIFIED_WRITE_REG16(_m5_i2c, _addr, reg, value, _commFreq)) { + M5IOE1_LOG_D(TAG_I2C, "Write16 Reg[0x%02X] <- 0x%04X", reg, value); + return true; + } + } else +#endif + if (M5IOE1_I2C_ARDUINO_WRITE_REG16(_wire, _addr, reg, value)) { + M5IOE1_LOG_D(TAG_I2C, "Write16 Reg[0x%02X] <- 0x%04X", reg, value); + return true; + } +#else + bool ok = false; + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + ok = (M5IOE1_I2C_MASTER_WRITE_REG16(_i2c_master_dev, reg, value) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + ok = (M5IOE1_I2C_BUS_WRITE_REG16(_i2c_device, reg, value) == ESP_OK); + break; +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + ok = (M5IOE1_I2C_LEGACY_WRITE_REG16(_port, _addr, reg, value) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + ok = M5IOE1_M5UNIFIED_WRITE_REG16(_m5_i2c, _addr, reg, value, _commFreq); + break; +#endif + default: + ok = false; + break; + } + if (ok) { + M5IOE1_LOG_D(TAG_I2C, "Write16 Reg[0x%02X] <- 0x%04X", reg, value); + return true; + } +#endif + if (attempt + 1 < M5IOE1_I2C_RETRY_COUNT) { + M5IOE1_DELAY_MS(M5IOE1_I2C_RETRY_DELAY_MS); + } + } + M5IOE1_LOG_E(TAG_I2C, "Write16 Reg[0x%02X] failed", reg); + return false; +} + +bool M5IOE1::_readReg(uint8_t reg, uint8_t* value) +{ + if (value == nullptr) { + M5IOE1_LOG_E(TAG_I2C, "Read Reg[0x%02X] failed: null value", reg); + return false; + } + + _checkAutoWake(); + for (int attempt = 0; attempt < M5IOE1_I2C_RETRY_COUNT; ++attempt) { +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + if (M5IOE1_M5UNIFIED_READ_BYTE(_m5_i2c, _addr, reg, value, _commFreq)) { + M5IOE1_LOG_D(TAG_I2C, "Read Reg[0x%02X] -> 0x%02X", reg, *value); + return true; + } + } else +#endif + if (M5IOE1_I2C_ARDUINO_READ_BYTE(_wire, _addr, reg, value)) { + M5IOE1_LOG_D(TAG_I2C, "Read Reg[0x%02X] -> 0x%02X", reg, *value); + return true; + } +#else + bool ok = false; + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + ok = (M5IOE1_I2C_MASTER_READ_BYTE(_i2c_master_dev, reg, value) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + ok = (M5IOE1_I2C_BUS_READ_BYTE(_i2c_device, reg, value) == ESP_OK); + break; +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + ok = (M5IOE1_I2C_LEGACY_READ_BYTE(_port, _addr, reg, value) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + ok = M5IOE1_M5UNIFIED_READ_BYTE(_m5_i2c, _addr, reg, value, _commFreq); + break; +#endif + default: + ok = false; + break; + } + if (ok) { + M5IOE1_LOG_D(TAG_I2C, "Read Reg[0x%02X] -> 0x%02X", reg, *value); + return true; + } +#endif + if (attempt + 1 < M5IOE1_I2C_RETRY_COUNT) { + M5IOE1_DELAY_MS(M5IOE1_I2C_RETRY_DELAY_MS); + } + } + M5IOE1_LOG_E(TAG_I2C, "Read Reg[0x%02X] failed", reg); + return false; +} + +bool M5IOE1::_readReg16(uint8_t reg, uint16_t* value) +{ + if (value == nullptr) { + M5IOE1_LOG_E(TAG_I2C, "Read16 Reg[0x%02X] failed: null value", reg); + return false; + } + + _checkAutoWake(); + for (int attempt = 0; attempt < M5IOE1_I2C_RETRY_COUNT; ++attempt) { +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + if (M5IOE1_M5UNIFIED_READ_REG16(_m5_i2c, _addr, reg, value, _commFreq)) { + M5IOE1_LOG_D(TAG_I2C, "Read16 Reg[0x%02X] -> 0x%04X", reg, *value); + return true; + } + } else +#endif + if (M5IOE1_I2C_ARDUINO_READ_REG16(_wire, _addr, reg, value)) { + M5IOE1_LOG_D(TAG_I2C, "Read16 Reg[0x%02X] -> 0x%04X", reg, *value); + return true; + } +#else + bool ok = false; + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + ok = (M5IOE1_I2C_MASTER_READ_REG16(_i2c_master_dev, reg, value) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + ok = (M5IOE1_I2C_BUS_READ_REG16(_i2c_device, reg, value) == ESP_OK); + break; +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + ok = (M5IOE1_I2C_LEGACY_READ_REG16(_port, _addr, reg, value) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + ok = M5IOE1_M5UNIFIED_READ_REG16(_m5_i2c, _addr, reg, value, _commFreq); + break; +#endif + default: + ok = false; + break; + } + if (ok) { + M5IOE1_LOG_D(TAG_I2C, "Read16 Reg[0x%02X] -> 0x%04X", reg, *value); + return true; + } +#endif + if (attempt + 1 < M5IOE1_I2C_RETRY_COUNT) { + M5IOE1_DELAY_MS(M5IOE1_I2C_RETRY_DELAY_MS); + } + } + M5IOE1_LOG_E(TAG_I2C, "Read16 Reg[0x%02X] failed", reg); + return false; +} + +bool M5IOE1::_writeBytes(uint8_t reg, const uint8_t* data, uint8_t len) +{ + if (data == nullptr && len > 0) { + M5IOE1_LOG_E(TAG_I2C, "WriteBytes Reg[0x%02X] len=%d failed: null data", reg, len); + return false; + } + + _checkAutoWake(); + for (int attempt = 0; attempt < M5IOE1_I2C_RETRY_COUNT; ++attempt) { +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + if (M5IOE1_M5UNIFIED_WRITE_BYTES(_m5_i2c, _addr, reg, len, data, _commFreq)) { + M5IOE1_LOG_V(TAG_I2C, "WriteBytes Reg[0x%02X] len=%d: %02X %02X %02X...", reg, len, + len > 0 ? data[0] : 0, len > 1 ? data[1] : 0, len > 2 ? data[2] : 0); + return true; + } + } else +#endif + if (M5IOE1_I2C_ARDUINO_WRITE_BYTES(_wire, _addr, reg, len, data)) { + M5IOE1_LOG_V(TAG_I2C, "WriteBytes Reg[0x%02X] len=%d: %02X %02X %02X...", reg, len, len > 0 ? data[0] : 0, + len > 1 ? data[1] : 0, len > 2 ? data[2] : 0); + return true; + } +#else + bool ok = false; + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + ok = (M5IOE1_I2C_MASTER_WRITE_BYTES(_i2c_master_dev, reg, len, data) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + ok = (M5IOE1_I2C_BUS_WRITE_BYTES(_i2c_device, reg, len, data) == ESP_OK); + break; +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + ok = (M5IOE1_I2C_LEGACY_WRITE_BYTES(_port, _addr, reg, len, data) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + ok = M5IOE1_M5UNIFIED_WRITE_BYTES(_m5_i2c, _addr, reg, len, data, _commFreq); + break; +#endif + default: + ok = false; + break; + } + if (ok) { + M5IOE1_LOG_V(TAG_I2C, "WriteBytes Reg[0x%02X] len=%d: %02X %02X %02X...", reg, len, len > 0 ? data[0] : 0, + len > 1 ? data[1] : 0, len > 2 ? data[2] : 0); + return true; + } +#endif + if (attempt + 1 < M5IOE1_I2C_RETRY_COUNT) { + M5IOE1_DELAY_MS(M5IOE1_I2C_RETRY_DELAY_MS); + } + } + M5IOE1_LOG_E(TAG_I2C, "WriteBytes Reg[0x%02X] len=%d failed", reg, len); + return false; +} + +bool M5IOE1::_readBytes(uint8_t reg, uint8_t* data, uint8_t len) +{ + if (data == nullptr && len > 0) { + M5IOE1_LOG_E(TAG_I2C, "ReadBytes Reg[0x%02X] len=%d failed: null data", reg, len); + return false; + } + + _checkAutoWake(); + for (int attempt = 0; attempt < M5IOE1_I2C_RETRY_COUNT; ++attempt) { +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + if (M5IOE1_M5UNIFIED_READ_BYTES(_m5_i2c, _addr, reg, len, data, _commFreq)) { + M5IOE1_LOG_V(TAG_I2C, "ReadBytes Reg[0x%02X] len=%d: %02X %02X %02X...", reg, len, + len > 0 ? data[0] : 0, len > 1 ? data[1] : 0, len > 2 ? data[2] : 0); + return true; + } + } else +#endif + if (M5IOE1_I2C_ARDUINO_READ_BYTES(_wire, _addr, reg, len, data)) { + M5IOE1_LOG_V(TAG_I2C, "ReadBytes Reg[0x%02X] len=%d: %02X %02X %02X...", reg, len, len > 0 ? data[0] : 0, + len > 1 ? data[1] : 0, len > 2 ? data[2] : 0); + return true; + } +#else + bool ok = false; + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + ok = (M5IOE1_I2C_MASTER_READ_BYTES(_i2c_master_dev, reg, len, data) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + ok = (M5IOE1_I2C_BUS_READ_BYTES(_i2c_device, reg, len, data) == ESP_OK); + break; +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + ok = (M5IOE1_I2C_LEGACY_READ_BYTES(_port, _addr, reg, len, data) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + ok = M5IOE1_M5UNIFIED_READ_BYTES(_m5_i2c, _addr, reg, len, data, _commFreq); + break; +#endif + default: + ok = false; + break; + } + if (ok) { + M5IOE1_LOG_V(TAG_I2C, "ReadBytes Reg[0x%02X] len=%d: %02X %02X %02X...", reg, len, len > 0 ? data[0] : 0, + len > 1 ? data[1] : 0, len > 2 ? data[2] : 0); + return true; + } +#endif + if (attempt + 1 < M5IOE1_I2C_RETRY_COUNT) { + M5IOE1_DELAY_MS(M5IOE1_I2C_RETRY_DELAY_MS); + } + } + M5IOE1_LOG_E(TAG_I2C, "ReadBytes Reg[0x%02X] len=%d failed", reg, len); + return false; +} + +bool M5IOE1::_isValidPin(uint8_t pin) +{ + return pin < M5IOE1_MAX_GPIO_PINS; +} + +bool M5IOE1::_isAdcPin(uint8_t pin) +{ + // ADC 引脚:IO2(1), IO4(3), IO5(4), IO7(6) + // ADC pins: IO2(1), IO4(3), IO5(4), IO7(6) + return (pin == 1 || pin == 3 || pin == 4 || pin == 6); +} + +bool M5IOE1::_isPwmPin(uint8_t pin) +{ + // PWM 引脚:IO8(7), IO9(8), IO10(9), IO11(10) + // PWM pins: IO8(7), IO9(8), IO10(9), IO11(10) + return (pin == 7 || pin == 8 || pin == 9 || pin == 10); +} + +uint8_t M5IOE1::_getAdcChannel(uint8_t pin) +{ + switch (pin) { + case 1: + return 1; // IO2 + case 3: + return 2; // IO4 + case 4: + return 3; // IO5 + case 6: + return 4; // IO7 + default: + return 0; + } +} + +uint8_t M5IOE1::_getPwmChannel(uint8_t pin) +{ + switch (pin) { + case 8: + return 0; // IO9 -> PWM1 + case 7: + return 1; // IO8 -> PWM2 + case 10: + return 2; // IO11 -> PWM3 + case 9: + return 3; // IO10 -> PWM4 + default: + return 0; + } +} + +void M5IOE1::_clearPinStates() +{ + memset(_pinStates, 0, sizeof(_pinStates)); + _pinStatesValid = false; +} + +void M5IOE1::_clearPwmStates() +{ + memset(_pwmStates, 0, sizeof(_pwmStates)); + _pwmFrequency = 0; + _pwmStatesValid = false; +} + +void M5IOE1::_clearAdcState() +{ + memset(&_adcState, 0, sizeof(_adcState)); + _adcStateValid = false; +} + +bool M5IOE1::_snapshotPinStates() +{ + if (!_initialized) return false; + + uint16_t modeReg = 0, outReg = 0, inReg = 0, puReg = 0, pdReg = 0, drvReg = 0, ieReg = 0, itReg = 0; + + if (!_readReg16(M5IOE1_REG_GPIO_MODE_L, &modeReg)) return false; + if (!_readReg16(M5IOE1_REG_GPIO_OUT_L, &outReg)) return false; + if (!_readReg16(M5IOE1_REG_GPIO_IN_L, &inReg)) return false; + if (!_readReg16(M5IOE1_REG_GPIO_PU_L, &puReg)) return false; + if (!_readReg16(M5IOE1_REG_GPIO_PD_L, &pdReg)) return false; + if (!_readReg16(M5IOE1_REG_GPIO_DRV_L, &drvReg)) return false; + if (!_readReg16(M5IOE1_REG_GPIO_IE_L, &ieReg)) return false; + if (!_readReg16(M5IOE1_REG_GPIO_IP_L, &itReg)) return false; + + for (uint8_t pin = 0; pin < M5IOE1_MAX_GPIO_PINS; pin++) { + _pinStates[pin].isOutput = (modeReg & (1 << pin)) != 0; + _pinStates[pin].outputLevel = (outReg & (1 << pin)) ? 1 : 0; + _pinStates[pin].inputLevel = (inReg & (1 << pin)) ? 1 : 0; + _pinStates[pin].pull = (puReg & (1 << pin)) ? 1 : ((pdReg & (1 << pin)) ? 2 : 0); + _pinStates[pin].drive = (drvReg & (1 << pin)) ? 1 : 0; + _pinStates[pin].intrEnabled = (ieReg & (1 << pin)) != 0; + _pinStates[pin].intrRising = (itReg & (1 << pin)) != 0; + } + + _pinStatesValid = true; + return true; +} + +bool M5IOE1::_snapshotPwmStates() +{ + if (!_initialized) return false; + + if (!_readReg16(M5IOE1_REG_PWM_FREQ_L, &_pwmFrequency)) return false; + + for (uint8_t ch = 0; ch < M5IOE1_MAX_PWM_CHANNELS; ch++) { + uint8_t regL = M5IOE1_REG_PWM1_DUTY_L + (ch * 2); + uint16_t data = 0; + if (!_readReg16(regL, &data)) return false; + + _pwmStates[ch].duty12 = data & 0x0FFF; + _pwmStates[ch].enabled = (data & ((uint16_t)M5IOE1_PWM_ENABLE << 8)) != 0; + _pwmStates[ch].polarity = (data & ((uint16_t)M5IOE1_PWM_POLARITY << 8)) != 0; + } + + _pwmStatesValid = true; + return true; +} + +bool M5IOE1::_snapshotAdcState() +{ + if (!_initialized) return false; + + uint8_t ctrl = 0; + if (!_readReg(M5IOE1_REG_ADC_CTRL, &ctrl)) return false; + + _adcState.activeChannel = ctrl & M5IOE1_ADC_CH_MASK; + _adcState.busy = (ctrl & M5IOE1_ADC_BUSY) != 0; + + if (!_adcState.busy) { + if (!_readReg16(M5IOE1_REG_ADC_DATA_L, &_adcState.lastValue)) { + _adcState.lastValue = 0; + } + } + + _adcStateValid = true; + return true; +} + +void M5IOE1::_autoSnapshotUpdate(uint8_t domains) +{ + if (!_autoSnapshot || !_initialized) return; + + if (domains & M5IOE1_SNAPSHOT_DOMAIN_GPIO) { + _snapshotPinStates(); + } + if (domains & M5IOE1_SNAPSHOT_DOMAIN_PWM) { + _snapshotPwmStates(); + } + if (domains & M5IOE1_SNAPSHOT_DOMAIN_ADC) { + _snapshotAdcState(); + } + if (domains & M5IOE1_SNAPSHOT_DOMAIN_AW8737A) { + _snapshotAw8737a(); + } +} + +bool M5IOE1::_isValidI2cFrequency(uint32_t speed) +{ + return (speed == M5IOE1_I2C_FREQ_100K || speed == M5IOE1_I2C_FREQ_400K); +} + +m5ioe1_err_t M5IOE1::getI2cSpeed(m5ioe1_i2c_speed_t* speed) +{ + if (speed == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_initialized) return M5IOE1_ERR_NOT_INIT; + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &cfg)) return M5IOE1_ERR_I2C_COMM; + + // 更新缓存 + // Update cache + _i2cConfig.speed400k = (cfg & M5IOE1_I2C_SPEED_400K_BIT) != 0; + _i2cConfigValid = true; + + *speed = _i2cConfig.speed400k ? M5IOE1_I2C_SPEED_400K : M5IOE1_I2C_SPEED_100K; + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::switchI2cSpeed(m5ioe1_i2c_speed_t speed) +{ + if (!_initialized) { + M5IOE1_LOG_E(TAG_I2C, "Cannot switch I2C speed: device not initialized"); + return M5IOE1_ERR_NOT_INIT; + } + + uint32_t targetFreq = (speed == M5IOE1_I2C_SPEED_400K) ? M5IOE1_I2C_FREQ_400K : M5IOE1_I2C_FREQ_100K; + + // 如果目标速度与当前速度相同,直接返回成功 + // If target speed is same as current, return success directly + if (targetFreq == _requestedSpeed) { + M5IOE1_LOG_I(TAG_I2C, "I2C speed already at %" PRIu32 " Hz, no change needed", targetFreq); + return M5IOE1_OK; + } + + // 步骤 1: 读取当前 I2C 配置 + // Step 1: Read current I2C config + uint8_t i2cCfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &i2cCfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to read I2C config register"); + return M5IOE1_ERR_I2C_COMM; + } + + // 步骤 2: 根据目标速度设置或清除 400KHz 模式位 + // Step 2: Set or clear 400KHz mode bit based on target speed + if (speed == M5IOE1_I2C_SPEED_400K) { + i2cCfg |= M5IOE1_I2C_SPEED_400K_BIT; + } else { + i2cCfg &= ~M5IOE1_I2C_SPEED_400K_BIT; + } + + if (!_writeReg(M5IOE1_REG_I2C_CFG, i2cCfg)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to write I2C config register"); + return M5IOE1_ERR_I2C_COMM; + } + + M5IOE1_LOG_D(TAG_I2C, "M5IOE1 I2C config set to %" PRIu32 " Hz mode", targetFreq); + + // 步骤 3: 短暂延迟以允许设备处理配置更改 + // Step 3: Small delay to allow device to process the configuration change + M5IOE1_DELAY_MS(5); + + // 步骤 4: 将主机 I2C 总线切换到目标速度 + // Step 4: Switch host I2C bus to target speed +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + _commFreq = targetFreq; + M5IOE1_LOG_D(TAG_I2C, "M5Unified I2C frequency updated to %" PRIu32 " Hz", targetFreq); + } else +#endif + if (_wire != nullptr) { + // 必须使用 Wire.end() + Wire.begin() 在 ESP32 上正确切换 I2C 频率 + // Must use Wire.end() + Wire.begin() to properly switch I2C frequency on ESP32 + _wire->end(); + M5IOE1_DELAY_MS(10); + if (!_wire->begin(_sda, _scl, targetFreq)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to re-initialize I2C bus at %" PRIu32 " Hz", targetFreq); + // 尝试恢复到原来的速度 + // Try to recover with original speed + uint32_t originalFreq = (speed == M5IOE1_I2C_SPEED_400K) ? M5IOE1_I2C_FREQ_100K : M5IOE1_I2C_FREQ_400K; + _wire->begin(_sda, _scl, originalFreq); + // 恢复设备配置 + // Revert device config + if (speed == M5IOE1_I2C_SPEED_400K) { + i2cCfg &= ~M5IOE1_I2C_SPEED_400K_BIT; + } else { + i2cCfg |= M5IOE1_I2C_SPEED_400K_BIT; + } + _writeReg(M5IOE1_REG_I2C_CFG, i2cCfg); + return M5IOE1_ERR_I2C_CONFIG; + } + M5IOE1_DELAY_MS(10); + M5IOE1_LOG_D(TAG_I2C, "Host I2C bus switched to %" PRIu32 " Hz", targetFreq); + } +#else + // ESP-IDF:处理不同的驱动类型 + // ESP-IDF: Handle different driver types + esp_err_t ret; + + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + // 对于 i2c_master 驱动:删除设备并以新速度重新添加 + // For i2c_master driver: remove device and add with new speed + if (_i2c_master_dev != nullptr) { + ret = i2c_master_bus_rm_device(_i2c_master_dev); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Failed to remove I2C device: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + _i2c_master_dev = nullptr; + + // 以目标速度重新创建设备句柄 + // Recreate device handle with target speed + i2c_device_config_t dev_config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = _addr, + .scl_speed_hz = targetFreq, + .scl_wait_us = 0, + .flags = + { + .disable_ack_check = false, + }, + }; + + ret = i2c_master_bus_add_device(_i2c_master_bus, &dev_config, &_i2c_master_dev); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Failed to add I2C device at %" PRIu32 " Hz: %s", targetFreq, + esp_err_to_name(ret)); + // 尝试恢复到原来的速度 + // Try to recover with original speed + uint32_t originalFreq = + (speed == M5IOE1_I2C_SPEED_400K) ? M5IOE1_I2C_FREQ_100K : M5IOE1_I2C_FREQ_400K; + dev_config.scl_speed_hz = originalFreq; + i2c_master_bus_add_device(_i2c_master_bus, &dev_config, &_i2c_master_dev); + return M5IOE1_ERR_I2C_CONFIG; + } + M5IOE1_LOG_D(TAG_I2C, "I2C master device recreated at %" PRIu32 " Hz", targetFreq); + } + break; +#endif // M5IOE1_HAS_I2C_MASTER + +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + // 对于 i2c_bus 驱动:删除设备并以新速度创建 + // For i2c_bus driver: delete device and create with new speed + if (_i2c_device != nullptr) { + ret = i2c_bus_device_delete(&_i2c_device); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Failed to delete I2C device: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + + // 以目标速度重新创建设备句柄 + // Recreate device handle with target speed + _i2c_device = i2c_bus_device_create(_i2c_bus, _addr, targetFreq); + if (_i2c_device == nullptr) { + M5IOE1_LOG_E(TAG_I2C, "Failed to create I2C device at %" PRIu32 " Hz", targetFreq); + // 尝试恢复到原来的速度 + // Try to recover with original speed + uint32_t originalFreq = + (speed == M5IOE1_I2C_SPEED_400K) ? M5IOE1_I2C_FREQ_100K : M5IOE1_I2C_FREQ_400K; + _i2c_device = i2c_bus_device_create(_i2c_bus, _addr, originalFreq); + return M5IOE1_ERR_I2C_CONFIG; + } + M5IOE1_LOG_D(TAG_I2C, "I2C bus device recreated at %" PRIu32 " Hz", targetFreq); + } + break; +#endif // M5IOE1_HAS_I2C_BUS + +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: { + i2c_config_t i2c_conf = {}; + i2c_conf.mode = I2C_MODE_MASTER; + i2c_conf.sda_io_num = _sda; + i2c_conf.scl_io_num = _scl; + i2c_conf.sda_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.scl_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.master.clk_speed = targetFreq; + ret = i2c_param_config(_port, &i2c_conf); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "i2c_param_config failed: %s", esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + M5IOE1_LOG_D(TAG_I2C, "Legacy I2C reconfigured to %" PRIu32 " Hz", targetFreq); + break; + } +#endif // !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + _commFreq = targetFreq; + M5IOE1_LOG_D(TAG_I2C, "M5Unified I2C frequency updated to %" PRIu32 " Hz", targetFreq); + break; +#endif + default: + M5IOE1_LOG_E(TAG_I2C, "Unknown I2C driver type"); + return M5IOE1_ERR_INTERNAL; + } +#endif + + // 步骤 5: 验证通信仍然有效 + // Step 5: Verify communication still works + uint16_t uid = 0; + if (!_readReg16(M5IOE1_REG_UID_L, &uid)) { + M5IOE1_LOG_E(TAG_I2C, "Communication failed after switching to %" PRIu32 " Hz, reverting", targetFreq); + + // 恢复设备配置 + // Revert device config + if (speed == M5IOE1_I2C_SPEED_400K) { + i2cCfg &= ~M5IOE1_I2C_SPEED_400K_BIT; + } else { + i2cCfg |= M5IOE1_I2C_SPEED_400K_BIT; + } + uint32_t originalFreq = (speed == M5IOE1_I2C_SPEED_400K) ? M5IOE1_I2C_FREQ_100K : M5IOE1_I2C_FREQ_400K; + +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + _commFreq = originalFreq; + } else +#endif + if (_wire != nullptr) { + _wire->end(); + M5IOE1_DELAY_MS(10); + _wire->begin(_sda, _scl, originalFreq); + M5IOE1_DELAY_MS(10); + } +#else + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + if (_i2c_master_dev != nullptr) { + i2c_master_bus_rm_device(_i2c_master_dev); + i2c_device_config_t dev_config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = _addr, + .scl_speed_hz = originalFreq, + .scl_wait_us = 0, + .flags = {.disable_ack_check = false}, + }; + i2c_master_bus_add_device(_i2c_master_bus, &dev_config, &_i2c_master_dev); + } + break; +#endif // M5IOE1_HAS_I2C_MASTER +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + if (_i2c_device != nullptr) { + i2c_bus_device_delete(&_i2c_device); + _i2c_device = i2c_bus_device_create(_i2c_bus, _addr, originalFreq); + } + break; +#endif // M5IOE1_HAS_I2C_BUS +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: { + i2c_config_t i2c_conf = {}; + i2c_conf.mode = I2C_MODE_MASTER; + i2c_conf.sda_io_num = _sda; + i2c_conf.scl_io_num = _scl; + i2c_conf.sda_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.scl_pullup_en = GPIO_PULLUP_ENABLE; + i2c_conf.master.clk_speed = originalFreq; + i2c_param_config(_port, &i2c_conf); // best effort rollback + break; + } +#endif // !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + _commFreq = originalFreq; + break; +#endif + default: + break; + } +#endif + _writeReg(M5IOE1_REG_I2C_CFG, i2cCfg); + return M5IOE1_ERR_I2C_COMM; + } + + // 更新 I2C 配置缓存和请求速度 + // Update I2C config cache and requested speed + _i2cConfig.speed400k = (speed == M5IOE1_I2C_SPEED_400K); + _requestedSpeed = targetFreq; + + M5IOE1_LOG_I(TAG_I2C, "Successfully switched to %" PRIu32 " Hz I2C mode", targetFreq); + return M5IOE1_OK; +} + +// ============================ +// 地址候选与 REV 规则辅助实现 +// Address candidate & REV rule helpers implementation +// ============================ + +// 地址段 <-> 允许 REV 字符列表 +// Address range -> allowed REV characters (each char = one allowed revision) +const M5IOE1::m5ioe1_rev_rule_t M5IOE1::kRevRules[] = { + {0x6F, 0x76, "A"}, + {0x4F, 0x50, "W"}, +}; + +const M5IOE1::m5ioe1_rev_rule_t* M5IOE1::_findRevRule(uint8_t addr) const +{ + for (size_t i = 0; i < sizeof(kRevRules) / sizeof(kRevRules[0]); ++i) { + if (addr >= kRevRules[i].start && addr <= kRevRules[i].end) { + return &kRevRules[i]; + } + } + return nullptr; +} + +bool M5IOE1::_isRevisionAllowed(uint8_t addr, uint8_t rev) const +{ + const m5ioe1_rev_rule_t* rule = _findRevRule(addr); + if (!rule) return true; // 无规则则放行 + const char* p = rule->valid_revs; + while (*p) { + if ((uint8_t)(*p) == rev) return true; + ++p; + } + return false; +} + +// 对当前已应用的 _addr 探测 REV 寄存器,不打 ERROR 日志(直接调底层读取一次) +// Probe REV register for currently applied _addr, no ERROR log (single low-level read) +M5IOE1::RevCheckResult M5IOE1::_checkAddressRevision(uint8_t addr) const +{ + // 读取一次,不走 retry + uint8_t rev = 0; + bool ok = false; + +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + ok = M5IOE1_M5UNIFIED_READ_BYTE(_m5_i2c, addr, M5IOE1_REG_REV, &rev, _commFreq); + } else +#endif + { + ok = M5IOE1_I2C_ARDUINO_READ_BYTE(_wire, addr, M5IOE1_REG_REV, &rev); + } +#else + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: + ok = (M5IOE1_I2C_MASTER_READ_BYTE(_i2c_master_dev, M5IOE1_REG_REV, &rev) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: + ok = (M5IOE1_I2C_BUS_READ_BYTE(_i2c_device, M5IOE1_REG_REV, &rev) == ESP_OK); + break; +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: + ok = (M5IOE1_I2C_LEGACY_READ_BYTE(_port, addr, M5IOE1_REG_REV, &rev) == ESP_OK); + break; +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + ok = M5IOE1_M5UNIFIED_READ_BYTE(_m5_i2c, addr, M5IOE1_REG_REV, &rev, _commFreq); + break; +#endif + default: + ok = false; + break; + } +#endif + + if (!ok) { + return RevCheckResult::NO_RESPONSE; + } + + const m5ioe1_rev_rule_t* rule = _findRevRule(addr); + if (!rule) { + M5IOE1_LOG_D(TAG_I2C, "Addr 0x%02X REV=0x%02X(%c), no rule -> allow", addr, rev, rev); + return RevCheckResult::NO_RULE; + } + if (_isRevisionAllowed(addr, rev)) { + M5IOE1_LOG_D(TAG_I2C, "Addr 0x%02X REV=0x%02X(%c) matched", addr, rev, rev); + return RevCheckResult::MATCHED; + } + M5IOE1_LOG_W(TAG_I2C, "Addr 0x%02X REV=0x%02X(%c) not in allowed list '%s', abort", addr, rev, rev, + rule->valid_revs); + return RevCheckResult::MISMATCH; +} + +// 构建候选地址列表 +// Build candidate address list +// 规则: +// 0xFF (AUTODETECT): 按固定序列 0x4F, 0x6F, 0x70..0x76, 0x50 +// 0x6F: 如失败,兜底尝试 0x4F +// 0x4F: 如失败,兜底尝试 0x6F +// 其他: 如失败,兜底尝试 0x4F, 0x6F +uint8_t M5IOE1::_buildAddressCandidates(uint8_t requested, uint8_t* candidates) +{ + uint8_t n = 0; + if (requested == M5IOE1_AUTODETECT_ADDR) { + // 自动探测序列:0x4F, 0x6F, 0x70..0x76, 0x50 + // Auto-detect sequence: 0x4F, 0x6F, 0x70..0x76, 0x50 + candidates[n++] = 0x4F; + candidates[n++] = 0x6F; + for (uint8_t a = 0x70; a <= 0x76; ++a) candidates[n++] = a; + candidates[n++] = 0x50; + } else { + // 显式地址:先放用户地址,再放兜底地址 + // Explicit address: user address first, then fallback addresses + candidates[n++] = requested; + if (requested == 0x6F) { + candidates[n++] = 0x4F; + } else if (requested == 0x4F) { + candidates[n++] = 0x6F; + } else { + candidates[n++] = 0x4F; + candidates[n++] = 0x6F; + } + } + return n; +} + +// 将候选地址(及速度)应用到当前驱动上下文(重建 device handle 如有必要) +// Apply candidate address and speed to current driver context (rebuild device handle if needed) +m5ioe1_err_t M5IOE1::_applyAddressToDriver(uint8_t candidate, uint32_t freqHz) +{ + _addr = candidate; +#ifdef ARDUINO +#if M5IOE1_HAS_M5UNIFIED_I2C + if (_m5_i2c) { + _commFreq = freqHz; + return M5IOE1_OK; + } +#endif + // TwoWire: 需要重新 begin 以切换频率 + // TwoWire: need to re-begin to switch frequency + if (_wire != nullptr) { + _wire->end(); + M5IOE1_DELAY_MS(50); + if (!_wire->begin(_sda, _scl, freqHz)) { + M5IOE1_LOG_E(TAG_I2C, "Failed to re-init I2C @%" PRIu32 "Hz for addr 0x%02X", freqHz, candidate); + return M5IOE1_ERR_I2C_CONFIG; + } + M5IOE1_DELAY_MS(50); + } + return M5IOE1_OK; +#else + switch (_i2cDriverType) { +#if M5IOE1_HAS_I2C_MASTER + case M5IOE1_I2C_DRIVER_SELF_CREATED: + case M5IOE1_I2C_DRIVER_MASTER: { + // 删除旧 device handle,以新地址和速度重新创建 + // Delete old device handle and recreate with new address and speed + if (_i2c_master_dev) { + i2c_master_bus_rm_device(_i2c_master_dev); + _i2c_master_dev = nullptr; + } + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = candidate, + .scl_speed_hz = freqHz, + .scl_wait_us = 0, + .flags = {.disable_ack_check = false}, + }; + esp_err_t ret = i2c_master_bus_add_device(_i2c_master_bus, &dev_cfg, &_i2c_master_dev); + if (ret != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Failed to add device 0x%02X @%" PRIu32 "Hz: %s", candidate, freqHz, + esp_err_to_name(ret)); + return M5IOE1_ERR_I2C_CONFIG; + } + return M5IOE1_OK; + } +#endif +#if M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_BUS: { + if (_i2c_device) { + i2c_bus_device_delete(&_i2c_device); + _i2c_device = nullptr; + } + _i2c_device = i2c_bus_device_create(_i2c_bus, candidate, freqHz); + if (!_i2c_device) { + M5IOE1_LOG_E(TAG_I2C, "Failed to create bus device 0x%02X @%" PRIu32 "Hz", candidate, freqHz); + return M5IOE1_ERR_I2C_CONFIG; + } + return M5IOE1_OK; + } +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + case M5IOE1_I2C_DRIVER_LEGACY: { + // Legacy: 重新配置 I2C 参数以更新速度 + // Legacy: reconfigure I2C params to update speed + i2c_config_t cfg = {}; + cfg.mode = I2C_MODE_MASTER; + cfg.sda_io_num = _sda; + cfg.scl_io_num = _scl; + cfg.sda_pullup_en = GPIO_PULLUP_ENABLE; + cfg.scl_pullup_en = GPIO_PULLUP_ENABLE; + cfg.master.clk_speed = freqHz; + esp_err_t r = i2c_param_config(_port, &cfg); + if (r != ESP_OK) { + M5IOE1_LOG_E(TAG_I2C, "Legacy i2c_param_config failed @%" PRIu32 "Hz: %s", freqHz, esp_err_to_name(r)); + return M5IOE1_ERR_I2C_CONFIG; + } + return M5IOE1_OK; + } +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + case M5IOE1_I2C_DRIVER_M5UNIFIED: + _commFreq = freqHz; + return M5IOE1_OK; +#endif + default: + return M5IOE1_ERR_I2C_CONFIG; + } +#endif +} + +// 对单个候选地址执行完整初始化尝试(100K → 800ms → 100K → 400K) +// Full init attempt for one candidate (100K→800ms→100K→400K) +m5ioe1_err_t M5IOE1::_tryInitAtAddress(uint8_t candidate) +{ + // 以 100KHz 切换候选地址 + m5ioe1_err_t err = _applyAddressToDriver(candidate, M5IOE1_I2C_FREQ_100K); + if (err != M5IOE1_OK) return err; + + // 发送唤醒 + sendWakeSignal(); + M5IOE1_DELAY_MS(10); + + // 尝试1:100K + if (_initDevice()) return M5IOE1_OK; + + // 尝试2:等待 800ms 后再尝试 100K + M5IOE1_LOG_W(TAG_I2C, "Addr 0x%02X: 100K failed, waiting 800ms and retrying...", candidate); + M5IOE1_DELAY_MS(800); + sendWakeSignal(); + M5IOE1_DELAY_MS(10); + if (_initDevice()) return M5IOE1_OK; + + // 尝试3:切换到 400K + M5IOE1_LOG_W(TAG_I2C, "Addr 0x%02X: 100K retry failed, trying 400K...", candidate); + err = _applyAddressToDriver(candidate, M5IOE1_I2C_FREQ_400K); + if (err != M5IOE1_OK) return err; + + sendWakeSignal(); + M5IOE1_DELAY_MS(10); + if (_initDevice()) return M5IOE1_OK; + + M5IOE1_LOG_E(TAG_I2C, "Addr 0x%02X: all attempts failed", candidate); + return M5IOE1_ERR_I2C_COMM; +} + +// 初始化成功后的收尾流程 +// Post-init finalization +m5ioe1_err_t M5IOE1::_finishInit(m5ioe1_int_mode_t intMode) +{ + _initialized = true; + + // 强制配置 I2C(用户请求的频率 + 关闭休眠) + // Force configure I2C (user requested speed + disable sleep) + m5ioe1_i2c_speed_t targetSpeed = + (_requestedSpeed == M5IOE1_I2C_FREQ_400K) ? M5IOE1_I2C_SPEED_400K : M5IOE1_I2C_SPEED_100K; + + m5ioe1_i2c_speed_t currentSpeed; + if (getI2cSpeed(¤tSpeed) == M5IOE1_OK) { + M5IOE1_LOG_I(TAG_I2C, "Current I2C speed: %s, Requested: %s", + (currentSpeed == M5IOE1_I2C_SPEED_400K) ? "400K" : "100K", + (targetSpeed == M5IOE1_I2C_SPEED_400K) ? "400K" : "100K"); + } + + if (setI2cConfig(0, targetSpeed, M5IOE1_WAKE_EDGE_FALLING, M5IOE1_PULL_ENABLED) != M5IOE1_OK) { + M5IOE1_LOG_W(TAG_I2C, "Failed to set I2C config"); + } + +#if defined(M5IOE1_HAS_M5UNIFIED_I2C) && M5IOE1_HAS_M5UNIFIED_I2C +#ifdef ARDUINO + if (_m5_i2c) _commFreq = _requestedSpeed; +#else + if (_i2cDriverType == M5IOE1_I2C_DRIVER_M5UNIFIED) _commFreq = _requestedSpeed; +#endif +#endif + + _snapshotPinStates(); + _snapshotPwmStates(); + _snapshotAdcState(); + _snapshotAw8737a(); + _snapshotI2cConfig(); + M5IOE1_LOG_D(TAG_I2C, "Snapshot completed (pins/pwm/adc/aw8737a/i2c)"); + + M5IOE1_LOG_I(TAG_I2C, "M5IOE1 initialized at address 0x%02X (I2C: %" PRIu32 " Hz)", _addr, _requestedSpeed); + + if (intMode != M5IOE1_INT_MODE_DISABLED) { + m5ioe1_err_t err = setInterruptMode(intMode); + if (err != M5IOE1_OK) { + _initialized = false; + return err; + } + } + return M5IOE1_OK; +} + +// ============================ +// 地址解析与初始化核心流程 +// Address resolution and init core flow +// ============================ +// 调用此函数前,调用方必须已完成 bus 初始化和 _requestedSpeed 设置。 +// Caller must have completed bus init and set _requestedSpeed before calling this. +// 此函数负责地址探测/兜底、REV 校验、完整通信初始化、以及调用 _finishInit。 +// This function handles address probe/fallback, REV validation, full comms init, and calls _finishInit. +m5ioe1_err_t M5IOE1::_resolveAndInit(uint8_t requestedAddr, m5ioe1_int_mode_t intMode) +{ + uint8_t candidates[kMaxCandidates]; + uint8_t nCandidates = _buildAddressCandidates(requestedAddr, candidates); + + bool firstCandidate = true; + + for (uint8_t i = 0; i < nCandidates; ++i) { + uint8_t candidate = candidates[i]; + + // 兜底候选(非首个候选)以及自动探测的所有候选都需要 REV 校验 + // Fallback candidates (non-first) and all auto-detect candidates need REV check + bool needRevCheck = (!firstCandidate) || (requestedAddr == M5IOE1_AUTODETECT_ADDR); + firstCandidate = false; + + if (needRevCheck) { + // 先以 100K 读取 REV。若无响应,再仅切到 400K 读取 REV。 + // Read REV first at 100K. If there is no response, try REV-only probe at 400K. + if (_applyAddressToDriver(candidate, M5IOE1_I2C_FREQ_100K) != M5IOE1_OK) { + continue; + } + + RevCheckResult revResult = _checkAddressRevision(candidate); + if (revResult == RevCheckResult::NO_RESPONSE) { + if (_applyAddressToDriver(candidate, M5IOE1_I2C_FREQ_400K) != M5IOE1_OK) { + continue; + } + revResult = _checkAddressRevision(candidate); + } + + if (revResult == RevCheckResult::MISMATCH) { + // REV 不匹配:立即中止,不继续探测后续地址 + // REV mismatch: abort immediately, do not try further addresses + M5IOE1_LOG_E(TAG_I2C, "Addr 0x%02X: REV mismatch, aborting address resolution", candidate); + return M5IOE1_ERR_I2C_COMM; + } + if (revResult == RevCheckResult::NO_RESPONSE) { + M5IOE1_LOG_D(TAG_I2C, "Addr 0x%02X: no response during REV probe, trying next", candidate); + continue; + } + // MATCHED 或 NO_RULE:继续完整初始化 + M5IOE1_LOG_D(TAG_I2C, "Addr 0x%02X: REV probe passed (%s), proceeding with full init", candidate, + (revResult == RevCheckResult::MATCHED) ? "MATCHED" : "NO_RULE"); + } else { + M5IOE1_LOG_D(TAG_I2C, "Addr 0x%02X: primary address, skipping REV pre-check", candidate); + } + + // 执行完整初始化尝试(内部会以 100K 开始) + // Perform full init attempt (starts at 100K internally) + m5ioe1_err_t err = _tryInitAtAddress(candidate); + if (err == M5IOE1_OK) { + M5IOE1_LOG_I(TAG_I2C, "Device found at address 0x%02X", candidate); + + // 兜底成功:用户显式指定了地址,但通过备用地址才完成初始化 + // 明确告知用户硬件实际地址与传入参数不一致,建议修正调用方代码以避免后续维护混淆 + // Fallback success: user explicitly specified an address but init succeeded via a fallback + // candidate. Notify the user that the hardware's actual address differs from the argument + // and recommend updating the caller to prevent future maintenance confusion. + if (requestedAddr != M5IOE1_AUTODETECT_ADDR && candidate != requestedAddr) { + M5IOE1_LOG_W(TAG_I2C, + "[Address Fallback Notice] M5IOE1 initialization completed using a fallback address. " + "Requested address: 0x%02X, actual device address: 0x%02X. " + "Please update your initialization parameters to 0x%02X to ensure deterministic behavior " + "and avoid relying on the fallback mechanism.", + requestedAddr, candidate, candidate); + } + + return _finishInit(intMode); + } + + M5IOE1_LOG_W(TAG_I2C, "Addr 0x%02X: full init failed, trying next candidate", candidate); + } + + M5IOE1_LOG_E(TAG_I2C, "No M5IOE1 device found (requested=0x%02X)", requestedAddr); + return M5IOE1_ERR_I2C_COMM; +} + +bool M5IOE1::_initDevice() +{ + // 读取设备信息以验证通信 + // Read device info to verify communication + uint16_t uid = 0; + uint8_t version = 0; + + if (!_readReg16(M5IOE1_REG_UID_L, &uid)) { + M5IOE1_LOG_E(TAG_SYS, "Failed to read device UID"); + return false; + } + + if (!_readReg(M5IOE1_REG_REV, &version)) { + M5IOE1_LOG_E(TAG_SYS, "Failed to read device version"); + return false; + } + + M5IOE1_LOG_I(TAG_SYS, "Device UID: 0x%04X, FW Version: %02X(%c)", uid, version, version); + return true; +} + +void M5IOE1::_handleInterrupt() +{ + // 步骤 1: 读取一次中断状态 + // Step 1: Read interrupt status once + uint16_t status = 0; + if (getInterruptStatus(&status) != M5IOE1_OK) return; + + if (status == 0) return; + + // 步骤 2: 立即写入0清除所有中断(避免在处理回调期间丢失新中断) + // Step 2: Immediately write 0 to clear all interrupts (avoid losing new interrupts during callback processing) + _writeReg16(M5IOE1_REG_GPIO_IS_L, 0); + + // 步骤 3: 处理所有触发的引脚回调 + // Step 3: Process all triggered pin callbacks + for (uint8_t pin = 0; pin < M5IOE1_MAX_GPIO_PINS; pin++) { + if ((status & (1 << pin)) && _callbacks[pin].enabled) { + if (_enableDefaultIsrLog) { + M5IOE1_LOG_I(TAG_IRQ, "Pin %d triggered by %s edge", pin, + _callbacks[pin].rising ? "RISING" : "FALLING"); + } + + if (_callbacks[pin].callbackArg != nullptr) { + _callbacks[pin].callbackArg(_callbacks[pin].arg); + } else if (_callbacks[pin].callback != nullptr) { + _callbacks[pin].callback(); + } + } + } +} + +bool M5IOE1::_pinsConflict(uint8_t a, uint8_t b) +{ + // 中断冲突对(1-based IO 编号): + // Interrupt conflict pairs (1-based IO numbers): + // (1,6), (2,3), (7,12), (8,9), (10,14), (11,13) + uint8_t A = a + 1, B = b + 1; + + auto eq = [](uint8_t x, uint8_t y, uint8_t m, uint8_t n) { return (x == m && y == n) || (x == n && y == m); }; + + return eq(A, B, 1, 6) || eq(A, B, 2, 3) || eq(A, B, 7, 12) || eq(A, B, 8, 9) || eq(A, B, 10, 14) || + eq(A, B, 11, 13); +} + +bool M5IOE1::_hasConflictingInterrupt(uint8_t pin) +{ + for (uint8_t i = 0; i < M5IOE1_MAX_GPIO_PINS; i++) { + if (i != pin && _pinStates[i].intrEnabled && _pinsConflict(pin, i)) { + return true; + } + } + return false; +} + +// ============================ +// 配置验证辅助函数 +// Configuration Validation Helpers +// ============================ + +bool M5IOE1::_getInterruptMutexPin(uint8_t pin, uint8_t* mutexPin) +{ + if (mutexPin == nullptr) return false; + + // 中断互斥对(0-based 引脚索引): + // Interrupt mutex pairs (0-based pin indices): + // IO1(0) <-> IO6(5) + // IO2(1) <-> IO3(2) + // IO7(6) <-> IO12(11) + // IO8(7) <-> IO9(8) + // IO10(9) <-> IO14(13) + // IO11(10) <-> IO13(12) + + static const uint8_t mutexPairs[][2] = { + {0, 5}, // IO1 <-> IO6 + {1, 2}, // IO2 <-> IO3 + {6, 11}, // IO7 <-> IO12 + {7, 8}, // IO8 <-> IO9 + {9, 13}, // IO10 <-> IO14 + {10, 12} // IO11 <-> IO13 + }; + + for (size_t i = 0; i < sizeof(mutexPairs) / sizeof(mutexPairs[0]); i++) { + if (pin == mutexPairs[i][0]) { + *mutexPin = mutexPairs[i][1]; + return true; + } + if (pin == mutexPairs[i][1]) { + *mutexPin = mutexPairs[i][0]; + return true; + } + } + + return false; +} + +bool M5IOE1::_isNeopixelPin(uint8_t pin) +{ + // NeoPixel LED 功能仅在 IO14 上可用(引脚索引 13) + // NeoPixel LED function only available on IO14 (pin index 13) + return (pin == 13); +} + +bool M5IOE1::_hasActiveInterrupt(uint8_t pin) +{ + if (!_isValidPin(pin)) return false; + if (!_pinStatesValid) return false; + return _pinStates[pin].intrEnabled; +} + +bool M5IOE1::_hasActiveAdc(uint8_t pin) +{ + if (!_isAdcPin(pin)) return false; + if (!_adcStateValid) return false; + + // 检查 ADC 当前是否正在使用此引脚的通道 + // Check if the ADC is currently using this pin's channel + uint8_t channel = _getAdcChannel(pin); + return (_adcState.activeChannel == channel); +} + +bool M5IOE1::_hasActivePwm(uint8_t pin) +{ + if (!_isPwmPin(pin)) return false; + if (!_pwmStatesValid) return false; + + uint8_t channel = _getPwmChannel(pin); + return _pwmStates[channel].enabled; +} + +bool M5IOE1::_hasI2cSleepEnabled() +{ + if (!_i2cConfigValid) return false; + return (_i2cConfig.sleepTime > 0); +} + +bool M5IOE1::_isLedEnabled() +{ + // 直接读取 LED_CFG 寄存器 + // Read LED_CFG register directly + uint8_t ledCfg = 0; + if (!_readReg(M5IOE1_REG_LED_CFG, &ledCfg)) { + return false; + } + uint8_t ledCount = ledCfg & M5IOE1_LED_NUM_MASK; + return (ledCount > 0); +} + +// ============================ +// I2C 配置快照 +// I2C Config Snapshot +// ============================ + +void M5IOE1::_clearI2cConfig() +{ + memset(&_i2cConfig, 0, sizeof(_i2cConfig)); + _i2cConfigValid = false; +} + +bool M5IOE1::_snapshotI2cConfig() +{ + if (!_initialized) return false; + + uint8_t cfg = 0; + if (!_readReg(M5IOE1_REG_I2C_CFG, &cfg)) return false; + + _i2cConfig.sleepTime = cfg & M5IOE1_I2C_SLEEP_MASK; + _i2cConfig.speed400k = (cfg & M5IOE1_I2C_SPEED_400K_BIT) != 0; + _i2cConfig.wakeRising = (cfg & M5IOE1_I2C_WAKE_RISING) != 0; + _i2cConfig.pullOff = (cfg & M5IOE1_I2C_PULL_OFF) != 0; + _i2cConfigValid = true; + + return true; +} + +bool M5IOE1::_snapshotAw8737a() +{ + if (!_initialized) return false; + + uint8_t regValue = 0; + if (!_readReg(M5IOE1_REG_AW8737A_PULSE, ®Value)) { + M5IOE1_LOG_E(TAG_AMP, "Failed to read AW8737A pulse register"); + _aw8737aStateValid = false; + return false; + } + + // 解析寄存器值(不含 REFRESH 位) + // Parse register value (without REFRESH bit) + _aw8737aRegValue = regValue & 0x7F; + _aw8737aPin = regValue & M5IOE1_AW8737A_GPIO_MASK; + _aw8737aPulseNum = (m5ioe1_aw8737a_pulse_t)((regValue >> M5IOE1_AW8737A_NUM_SHIFT) & M5IOE1_AW8737A_NUM_MASK); + _aw8737aStateValid = true; + + return true; +} + +void M5IOE1::_clearAw8737a() +{ + _aw8737aConfigured = false; + _aw8737aPin = 0; + _aw8737aPulseNum = M5IOE1_AW8737A_PULSE_0; + _aw8737aRegValue = 0; + _aw8737aStateValid = false; +} + +// ============================ +// 快照验证 +// Snapshot Verification +// ============================ + +m5ioe1_snapshot_verify_t M5IOE1::verifySnapshot() +{ + m5ioe1_snapshot_verify_t result = {}; + result.consistent = true; + + if (!_initialized) { + result.consistent = false; + return result; + } + + // 验证 GPIO 寄存器 + // Verify GPIO registers + if (_pinStatesValid) { + uint16_t actualMode = 0, actualOutput = 0; + + if (_readReg16(M5IOE1_REG_GPIO_MODE_L, &actualMode) && _readReg16(M5IOE1_REG_GPIO_OUT_L, &actualOutput)) { + // 从缓存构建期望值 + // Build expected values from cache + uint16_t expectedMode = 0, expectedOutput = 0; + for (uint8_t i = 0; i < M5IOE1_MAX_GPIO_PINS; i++) { + if (_pinStates[i].isOutput) { + expectedMode |= (1 << i); + } + if (_pinStates[i].outputLevel) { + expectedOutput |= (1 << i); + } + } + + result.expected_mode = expectedMode; + result.actual_mode = actualMode; + result.expected_output = expectedOutput; + result.actual_output = actualOutput; + + if (expectedMode != actualMode || expectedOutput != actualOutput) { + result.gpio_mismatch = true; + result.consistent = false; + } + } else { + result.consistent = false; + } + } + + // 验证 PWM 寄存器 + // Verify PWM registers + if (_pwmStatesValid) { + uint16_t actualFreq = 0; + if (_readReg16(M5IOE1_REG_PWM_FREQ_L, &actualFreq)) { + if (actualFreq != _pwmFrequency) { + result.pwm_mismatch = true; + result.consistent = false; + } + } + + for (uint8_t ch = 0; ch < M5IOE1_MAX_PWM_CHANNELS; ch++) { + uint8_t regL = M5IOE1_REG_PWM1_DUTY_L + (ch * 2); + uint16_t data = 0; + if (_readReg16(regL, &data)) { + uint16_t actualDuty = data & 0x0FFF; + bool actualEnabled = (data & ((uint16_t)M5IOE1_PWM_ENABLE << 8)) != 0; + bool actualPolarity = (data & ((uint16_t)M5IOE1_PWM_POLARITY << 8)) != 0; + + if (actualDuty != _pwmStates[ch].duty12 || actualEnabled != _pwmStates[ch].enabled || + actualPolarity != _pwmStates[ch].polarity) { + result.pwm_mismatch = true; + result.consistent = false; + } + } + } + } + + // 验证 ADC 寄存器 + // Verify ADC registers + if (_adcStateValid) { + uint8_t ctrl = 0; + if (_readReg(M5IOE1_REG_ADC_CTRL, &ctrl)) { + uint8_t actualChannel = ctrl & M5IOE1_ADC_CH_MASK; + if (actualChannel != _adcState.activeChannel) { + result.adc_mismatch = true; + result.consistent = false; + } + } + } + + // 验证 AW8737A 寄存器 + // Verify AW8737A registers + if (_aw8737aStateValid) { + uint8_t actualReg = 0; + if (_readReg(M5IOE1_REG_AW8737A_PULSE, &actualReg)) { + result.expected_aw8737a = _aw8737aRegValue; + result.actual_aw8737a = actualReg & 0x7F; + + if (result.expected_aw8737a != result.actual_aw8737a) { + result.aw8737a_mismatch = true; + result.consistent = false; + } + } else { + result.consistent = false; + } + } + + return result; +} + +// ============================ +// 缓存状态查询函数 +// Cached State Query Functions +// ============================ + +m5ioe1_err_t M5IOE1::getCachedPwmFrequency(uint16_t* frequency) +{ + if (frequency == nullptr) return M5IOE1_ERR_INVALID_ARG; + if (!_pwmStatesValid) return M5IOE1_FAIL; + *frequency = _pwmFrequency; + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::getCachedPwmState(uint8_t channel, uint16_t* duty12, bool* polarity, bool* enabled) +{ + if (channel > 3 || duty12 == nullptr || polarity == nullptr || enabled == nullptr) { + return M5IOE1_ERR_INVALID_ARG; + } + if (!_pwmStatesValid) return M5IOE1_FAIL; + + *duty12 = _pwmStates[channel].duty12; + *polarity = _pwmStates[channel].polarity; + *enabled = _pwmStates[channel].enabled; + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::getCachedAdcState(uint8_t* activeChannel, bool* busy, uint16_t* lastValue) +{ + if (activeChannel == nullptr || busy == nullptr || lastValue == nullptr) { + return M5IOE1_ERR_INVALID_ARG; + } + if (!_adcStateValid) return M5IOE1_FAIL; + + *activeChannel = _adcState.activeChannel; + *busy = _adcState.busy; + *lastValue = _adcState.lastValue; + return M5IOE1_OK; +} + +m5ioe1_err_t M5IOE1::getCachedPinState(uint8_t pin, bool* isOutput, uint8_t* level, uint8_t* pull) +{ + if (!_isValidPin(pin) || isOutput == nullptr || level == nullptr || pull == nullptr) { + return M5IOE1_ERR_INVALID_ARG; + } + if (!_pinStatesValid) return M5IOE1_FAIL; + + *isOutput = _pinStates[pin].isOutput; + *level = _pinStates[pin].isOutput ? _pinStates[pin].outputLevel : _pinStates[pin].inputLevel; + *pull = _pinStates[pin].pull; + return M5IOE1_OK; +} + +// ============================ +// 平台特定函数 +// Platform-Specific Functions +// ============================ + +#ifdef ARDUINO + +void IRAM_ATTR M5IOE1::_arduinoIsrHandler(void* arg) +{ + M5IOE1* self = static_cast(arg); + if (self == nullptr || self->_pollTask == nullptr) { + return; + } + BaseType_t hpw = pdFALSE; + vTaskNotifyGiveFromISR(self->_pollTask, &hpw); + if (hpw == pdTRUE) { + portYIELD_FROM_ISR(); + } +} + +void M5IOE1::_pollTaskArduino(void* arg) +{ + M5IOE1* self = static_cast(arg); + + while (true) { + uint16_t status = 0; + if (self->getInterruptStatus(&status) == M5IOE1_OK && status != 0) { + self->_handleInterrupt(); + } + vTaskDelay(pdMS_TO_TICKS(self->_pollingInterval)); + } +} + +bool M5IOE1::_setupPollingArduino() +{ + BaseType_t ok = xTaskCreatePinnedToCore(_pollTaskArduino, "m5ioe1_poll", 4096, this, 5, &_pollTask, tskNO_AFFINITY); + return ok == pdPASS; +} + +void M5IOE1::_cleanupPollingArduino() +{ + if (_pollTask) { + vTaskDelete(_pollTask); + _pollTask = nullptr; + } +} + +void M5IOE1::_intrTaskArduino(void* arg) +{ + M5IOE1* self = static_cast(arg); + while (true) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + if (self) { + self->_handleInterrupt(); + } + } +} + +bool M5IOE1::_setupHardwareInterruptArduino() +{ + if (_intPin < 0) { + M5IOE1_LOG_E(TAG_IRQ, "Hardware interrupt mode requires interrupt pin"); + return false; + } + + pinMode(_intPin, INPUT_PULLUP); + + BaseType_t ok = xTaskCreatePinnedToCore(_intrTaskArduino, "m5ioe1_intr", 4096, this, 5, &_pollTask, tskNO_AFFINITY); + if (ok != pdPASS) { + _pollTask = nullptr; + return false; + } + + attachInterruptArg(digitalPinToInterrupt(_intPin), _arduinoIsrHandler, this, FALLING); + return true; +} + +void M5IOE1::_cleanupHardwareInterruptArduino() +{ + if (_intPin >= 0) { + detachInterrupt(_intPin); + } + if (_pollTask) { + vTaskDelete(_pollTask); + _pollTask = nullptr; + } +} + +#else // ESP-IDF + +void M5IOE1::_pollTaskFunc(void* arg) +{ + M5IOE1* self = static_cast(arg); + + while (true) { + uint16_t status = 0; + if (self->getInterruptStatus(&status) == M5IOE1_OK && status != 0) { + self->_handleInterrupt(); + } + vTaskDelay(pdMS_TO_TICKS(self->_pollingInterval)); + } +} + +void IRAM_ATTR M5IOE1::_isrHandler(void* arg) +{ + M5IOE1* self = static_cast(arg); + if (self && self->_intrQueue) { + // 立即禁用中断,防止电平触发造成 ISR 风暴 + // Disable interrupt immediately to prevent level-triggered ISR storm + gpio_intr_disable((gpio_num_t)self->_intPin); + uint32_t evt = 1; + BaseType_t hpw = pdFALSE; + xQueueSendFromISR(self->_intrQueue, &evt, &hpw); + if (hpw == pdTRUE) portYIELD_FROM_ISR(); + } +} + +bool M5IOE1::_setupHardwareInterrupt() +{ + if (_intPin < 0) { + M5IOE1_LOG_E(TAG_IRQ, "Hardware interrupt mode requires interrupt pin"); + return false; + } + + _intrQueue = xQueueCreate(8, sizeof(uint32_t)); + if (_intrQueue == nullptr) return false; + + gpio_config_t io_conf = {}; + io_conf.pin_bit_mask = (1ULL << _intPin); + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.intr_type = GPIO_INTR_LOW_LEVEL; + + if (gpio_config(&io_conf) != ESP_OK) { + vQueueDelete(_intrQueue); + _intrQueue = nullptr; + return false; + } + + esp_err_t isr_err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM); + if (isr_err != ESP_OK && isr_err != ESP_ERR_INVALID_STATE) { + vQueueDelete(_intrQueue); + _intrQueue = nullptr; + return false; + } + + if (gpio_isr_handler_add((gpio_num_t)_intPin, _isrHandler, this) != ESP_OK) { + vQueueDelete(_intrQueue); + _intrQueue = nullptr; + return false; + } + + // 创建任务以处理中断 + // Create task to handle interrupts + BaseType_t ok = xTaskCreatePinnedToCore( + [](void* arg) { + M5IOE1* self = static_cast(arg); + uint32_t evt; + while (true) { + if (xQueueReceive(self->_intrQueue, &evt, portMAX_DELAY) == pdTRUE) { + gpio_intr_disable((gpio_num_t)self->_intPin); + self->_handleInterrupt(); + gpio_intr_enable((gpio_num_t)self->_intPin); + } + } + }, + "m5ioe1_intr", 4096, this, configMAX_PRIORITIES - 2, &_pollTask, tskNO_AFFINITY); + + if (ok != pdPASS) { + gpio_isr_handler_remove((gpio_num_t)_intPin); + vQueueDelete(_intrQueue); + _intrQueue = nullptr; + return false; + } + + return true; +} + +bool M5IOE1::_setupPolling() +{ + BaseType_t ok = xTaskCreatePinnedToCore(_pollTaskFunc, "m5ioe1_poll", 4096, this, 5, &_pollTask, tskNO_AFFINITY); + return ok == pdPASS; +} + +void M5IOE1::_cleanupPolling() +{ + if (_pollTask) { + vTaskDelete(_pollTask); + _pollTask = nullptr; + } +} + +void M5IOE1::_cleanupHardwareInterrupt() +{ + if (_intrQueue) { + vQueueDelete(_intrQueue); + _intrQueue = nullptr; + } + if (_intPin >= 0) { + // 移除 GPIO ISR handler + // Remove GPIO ISR handler + // 如果返回错误(如"GPIO isr service is not installed")可忽略 + // The returned error (e.g., "GPIO isr service is not installed") can be ignored + // This error indicates that the ISR service is not currently installed and does not need to be removed, which + // is expected behavior + esp_err_t err = gpio_isr_handler_remove((gpio_num_t)_intPin); + if (err != ESP_OK) { + M5IOE1_LOG_W(TAG_IRQ, + "gpio_isr_handler_remove failed (can be ignored if ISR service was not installed): %s", + esp_err_to_name(err)); + } + } +} + +#endif diff --git a/components/m5ioe1/src/M5IOE1.h b/components/m5ioe1/src/M5IOE1.h new file mode 100644 index 0000000..bed8056 --- /dev/null +++ b/components/m5ioe1/src/M5IOE1.h @@ -0,0 +1,1913 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#ifndef _M5IOE1_H_ +#define _M5IOE1_H_ + +#include "M5IOE1_i2c_compat.h" +#include +#include + +#ifdef ARDUINO +// Arduino:FreeRTOS 头文件通过 Arduino 框架包含 +// Arduino: FreeRTOS headers included via Arduino framework +#else +// ESP-IDF 专用包含文件 +// ESP-IDF specific includes +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "esp_attr.h" +#endif + +// ============================ +// IO 引脚定义 +// IO Pin Definitions +// ============================ +typedef enum { + M5IOE1_PIN_NC = -1, + M5IOE1_PIN_1 = 0, + M5IOE1_PIN_2 = 1, + M5IOE1_PIN_3 = 2, + M5IOE1_PIN_4 = 3, + M5IOE1_PIN_5 = 4, + M5IOE1_PIN_6 = 5, + M5IOE1_PIN_7 = 6, + M5IOE1_PIN_8 = 7, + M5IOE1_PIN_9 = 8, + M5IOE1_PIN_10 = 9, + M5IOE1_PIN_11 = 10, + M5IOE1_PIN_12 = 11, + M5IOE1_PIN_13 = 12, + M5IOE1_PIN_14 = 13 +} m5ioe1_pin_t; + +// ============================ +// 错误码 +// Error Codes +// ============================ +typedef enum { + M5IOE1_OK = 0, // 成功 + // Success + M5IOE1_FAIL = -1, // 一般失败 + // General failure + M5IOE1_ERR_I2C_CONFIG = -2, // I2C 配置错误 (如频率不支持) + // I2C configuration error + M5IOE1_ERR_RULE_VIOLATION = -3, // 条件规则错误 (如引脚冲突,互斥功能) + // Condition rule violation + M5IOE1_ERR_INVALID_ARG = -4, // 无效参数 + // Invalid argument + M5IOE1_ERR_TIMEOUT = -5, // 超时 + // Timeout + M5IOE1_ERR_NOT_SUPPORTED = -6, // 不支持的功能 + // Function not supported + M5IOE1_ERR_I2C_COMM = -7, // I2C 通信错误 + // I2C communication error + M5IOE1_ERR_NOT_INIT = -8, // 设备未初始化 + // Device not initialized + M5IOE1_ERR_INTERNAL = -9, // 内部错误 + // Internal error +} m5ioe1_err_t; + +// ============================ +// 设备常量 +// Device Constants +// ============================ +#define M5IOE1_DEFAULT_ADDR 0x6F // 默认 I2C 地址 / Default I2C address +#define M5IOE1_DEFAULT_ADDR_2 0x4F // 第二地址(部分作为从机的产品) / Second address (for some slave products) +#define M5IOE1_AUTODETECT_ADDR 0xFF // 自动检测地址(0x6F~0x76:A 0x4F~0x50:W) / Auto-detect address +#define M5IOE1_MAX_GPIO_PINS 14 +#define M5IOE1_MAX_ADC_CHANNELS 4 +#define M5IOE1_MAX_PWM_CHANNELS 4 +#define M5IOE1_MAX_LED_COUNT 32 +#define M5IOE1_RTC_RAM_SIZE 32 + +// I2C retry settings +#define M5IOE1_I2C_RETRY_COUNT 2 +#define M5IOE1_I2C_RETRY_DELAY_MS 50 + +// ============================ +// I2C 频率常量 +// I2C Frequency Constants +// ============================ +#define M5IOE1_I2C_FREQ_100K 100000 +#define M5IOE1_I2C_FREQ_400K 400000 +#define M5IOE1_I2C_FREQ_DEFAULT M5IOE1_I2C_FREQ_100K + +// ============================ +// 寄存器地址 +// Register Addresses +// ============================ +// System +#define M5IOE1_REG_UID_L 0x00 // R [7:0] UID Low Byte +#define M5IOE1_REG_UID_H 0x01 // R [15:8] UID High Byte +#define M5IOE1_REG_REV 0x02 // R [7:0] SW7-SW0 Version +// GPIO +#define M5IOE1_REG_GPIO_MODE_L 0x03 // R/W [7:0] Mode P8-P1 +#define M5IOE1_REG_GPIO_MODE_H 0x04 // R/W [7:6] Res | [5:0] Mode P14-P9 +#define M5IOE1_REG_GPIO_OUT_L 0x05 // R/W [7:0] Out P8-P1 +#define M5IOE1_REG_GPIO_OUT_H 0x06 // R/W [7:6] Res | [5:0] Out P14-P9 +#define M5IOE1_REG_GPIO_IN_L 0x07 // R [7:0] In P8-P1 +#define M5IOE1_REG_GPIO_IN_H 0x08 // R [7:6] Res | [5:0] In P14-P9 +#define M5IOE1_REG_GPIO_PU_L 0x09 // R/W [7:0] PU P8-P1 +#define M5IOE1_REG_GPIO_PU_H 0x0A // R/W [7:6] Res | [5:0] PU P14-P9 +#define M5IOE1_REG_GPIO_PD_L 0x0B // R/W [7:0] PD P8-P1 +#define M5IOE1_REG_GPIO_PD_H 0x0C // R/W [7:6] Res | [5:0] PD P14-P9 +#define M5IOE1_REG_GPIO_IE_L 0x0D // R/W [7:0] IE P8-P1 +#define M5IOE1_REG_GPIO_IE_H 0x0E // R/W [7:6] Res | [5:0] IE P14-P9 +#define M5IOE1_REG_GPIO_IP_L 0x0F // R/W [7:0] IP P8-P1 +#define M5IOE1_REG_GPIO_IP_H 0x10 // R/W [7:6] Res | [5:0] IP P14-P9 +#define M5IOE1_REG_GPIO_IS_L 0x11 // R [7:0] IS P8-P1 +#define M5IOE1_REG_GPIO_IS_H 0x12 // R [7:6] Res | [5:0] IS P14-P9 +#define M5IOE1_REG_GPIO_DRV_L 0x13 // R/W [7:0] Drive P8-P1 +#define M5IOE1_REG_GPIO_DRV_H 0x14 // R/W [7:6] Res | [5:0] Drive P14-P9 +// ADC +#define M5IOE1_REG_ADC_CTRL 0x15 // R/W [7] BUSY | [6] START | [2:0] Channel +#define M5IOE1_REG_ADC_DATA_L 0x16 // R [7:0] ADC Data Low +#define M5IOE1_REG_ADC_DATA_H 0x17 // R [3:0] ADC Data High +// Temperature +#define M5IOE1_REG_TEMP_CTRL 0x18 // R/W [7] TBUSY | [6] TSTART +#define M5IOE1_REG_TEMP_DATA_L 0x19 // R [7:0] Temp Data Low +#define M5IOE1_REG_TEMP_DATA_H 0x1A // R [3:0] Temp Data High +// PWM +#define M5IOE1_REG_PWM1_DUTY_L 0x1B // R/W [7:0] PWM1 Duty Low +#define M5IOE1_REG_PWM1_DUTY_H 0x1C // R/W [7] EN | [6] POL | [3:0] PWM1 Duty High +#define M5IOE1_REG_PWM2_DUTY_L 0x1D // R/W [7:0] PWM2 Duty Low +#define M5IOE1_REG_PWM2_DUTY_H 0x1E // R/W [7] EN | [6] POL | [3:0] PWM2 Duty High +#define M5IOE1_REG_PWM3_DUTY_L 0x1F // R/W [7:0] PWM3 Duty Low +#define M5IOE1_REG_PWM3_DUTY_H 0x20 // R/W [7] EN | [6] POL | [3:0] PWM3 Duty High +#define M5IOE1_REG_PWM4_DUTY_L 0x21 // R/W [7:0] PWM4 Duty Low +#define M5IOE1_REG_PWM4_DUTY_H 0x22 // R/W [7] EN | [6] POL | [3:0] PWM4 Duty High +// System Config +#define M5IOE1_REG_I2C_CFG 0x23 // R/W [6] INT_PU/PD | [5] WAKE | [4] SPD | [3:0] SLEEP +#define M5IOE1_REG_LED_CFG 0x24 // R/W [6] REFRESH | [5:0] LED Num +#define M5IOE1_REG_PWM_FREQ_L 0x25 // R/W [7:0] PWM Freq Low +#define M5IOE1_REG_PWM_FREQ_H 0x26 // R/W [7:0] PWM Freq High +#define M5IOE1_REG_REF_VOLTAGE_L 0x27 // R [7:0] Ref Voltage Low +#define M5IOE1_REG_REF_VOLTAGE_H 0x28 // R [15:8] Ref Voltage High +#define M5IOE1_REG_FACTORY_RESET 0x29 // W [7:0] Reset Key +// Data areas +#define M5IOE1_REG_LED_RAM_START 0x30 // R/W NeoPixel RGB565 Data (32 LEDs x 2B) +#define M5IOE1_REG_LED_RAM_END 0x6F +#define M5IOE1_REG_RTC_RAM_START 0x70 // R/W RTC Retention RAM (32B) +#define M5IOE1_REG_RTC_RAM_END 0x8F +// Extended +#define M5IOE1_REG_AW8737A_PULSE 0x90 // R/W [7] REFRESH | [6:5] NUM | [4:0] GPIO + +// ============================ +// 位定义 +// Bit Definitions +// ============================ +// ADC Control +#define M5IOE1_ADC_CH_MASK 0x07 +#define M5IOE1_ADC_START (1 << 6) +#define M5IOE1_ADC_BUSY (1 << 7) +// Temperature Control +#define M5IOE1_TEMP_START (1 << 6) +#define M5IOE1_TEMP_BUSY (1 << 7) +// PWM Control +#define M5IOE1_PWM_POLARITY (1 << 6) +#define M5IOE1_PWM_ENABLE (1 << 7) +// I2C Config +#define M5IOE1_I2C_SLEEP_MASK 0x0F +#define M5IOE1_I2C_SPEED_400K_BIT (1 << 4) +#define M5IOE1_I2C_WAKE_RISING (1 << 5) +#define M5IOE1_I2C_PULL_OFF (1 << 6) +// LED Config +#define M5IOE1_LED_NUM_MASK 0x3F +#define M5IOE1_LED_REFRESH (1 << 6) +// Factory Reset +#define M5IOE1_FACTORY_RESET_KEY 0x3A +// AW8737A Pulse +#define M5IOE1_AW8737A_GPIO_MASK 0x1F +#define M5IOE1_AW8737A_NUM_SHIFT 5 +#define M5IOE1_AW8737A_NUM_MASK 0x03 +#define M5IOE1_AW8737A_REFRESH (1 << 7) + +// ============================ +// ADC 通道定义(支持 ADC 的 IO 引脚) +// ADC Channel Definitions (IO pins that support ADC) +// ============================ +#define M5IOE1_ADC_CH1 1 // IO2 (pin index 1) +#define M5IOE1_ADC_CH2 2 // IO4 (pin index 3) +#define M5IOE1_ADC_CH3 3 // IO5 (pin index 4) +#define M5IOE1_ADC_CH4 4 // IO7 (pin index 6) + +// ============================ +// PWM 通道定义(支持 PWM 的 IO 引脚) +// PWM Channel Definitions (IO pins that support PWM) +// ============================ +#define M5IOE1_PWM_CH1 0 // IO9 (pin index 8) +#define M5IOE1_PWM_CH2 1 // IO8 (pin index 7) +#define M5IOE1_PWM_CH3 2 // IO11 (pin index 10) +#define M5IOE1_PWM_CH4 3 // IO10 (pin index 9) + +// ============================ +// GPIO 电平定义 +// GPIO Level Definitions +// ============================ +#ifndef LOW +#define LOW 0x0 +#endif +#ifndef HIGH +#define HIGH 0x1 +#endif + +// ============================ +// GPIO 模式定义(Arduino 兼容) +// GPIO Mode Definitions (Arduino-compatible) +// ============================ +#ifndef INPUT +#define INPUT 0x01 +#endif +#ifndef OUTPUT +#define OUTPUT 0x03 +#endif +#ifndef PULLUP +#define PULLUP 0x04 +#endif +#ifndef INPUT_PULLUP +#define INPUT_PULLUP 0x05 +#endif +#ifndef PULLDOWN +#define PULLDOWN 0x08 +#endif +#ifndef INPUT_PULLDOWN +#define INPUT_PULLDOWN 0x09 +#endif +#ifndef OPEN_DRAIN +#define OPEN_DRAIN 0x10 +#endif +#ifndef OUTPUT_OPEN_DRAIN +#define OUTPUT_OPEN_DRAIN 0x13 +#endif +#ifndef ANALOG +#define ANALOG 0xC0 +#endif + +// ============================ +// 中断模式定义 +// Interrupt Mode Definitions +// ============================ +#ifndef DISABLED +#define DISABLED 0x00 +#endif +#ifndef RISING +#define RISING 0x01 +#endif +#ifndef FALLING +#define FALLING 0x02 +#endif +#ifndef CHANGE +#define CHANGE 0x03 +#endif +#ifndef ONLOW +#define ONLOW 0x04 +#endif +#ifndef ONHIGH +#define ONHIGH 0x05 +#endif +#ifndef ONLOW_WE +#define ONLOW_WE 0x0C +#endif +#ifndef ONHIGH_WE +#define ONHIGH_WE 0x0D +#endif + +// ============================ +// 上拉/下拉模式定义 +// Pull Mode Definitions +// ============================ +#define M5IOE1_PULL_NONE 0x00 +#define M5IOE1_PULL_UP 0x01 +#define M5IOE1_PULL_DOWN 0x02 + +// ============================ +// 驱动模式定义 +// Drive Mode Definitions +// ============================ +#define M5IOE1_DRIVE_PUSHPULL 0x00 +#define M5IOE1_DRIVE_OPENDRAIN 0x01 + +// ============================ +// AW8737A 脉冲刷新类型 +// AW8737A PULSE Refresh Types +// ============================ +typedef enum { + M5IOE1_AW8737A_REFRESH_WAIT = 0, // 不刷新,等待下一次触发 + // No refresh, wait for next trigger + M5IOE1_AW8737A_REFRESH_NOW = 1 // 刷新并立即执行 + // Refresh and execute immediately +} m5ioe1_aw8737a_refresh_t; + +// ============================ +// AW8737A 脉冲数量类型 +// AW8737A PULSE NUM Types +// ============================ +typedef enum { + M5IOE1_AW8737A_PULSE_0 = 0, // 0 个脉冲 + // 0 pulse + M5IOE1_AW8737A_PULSE_1 = 1, // 1 个脉冲 + // 1 pulse + M5IOE1_AW8737A_PULSE_2 = 2, // 2 个脉冲 + // 2 pulses + M5IOE1_AW8737A_PULSE_3 = 3 // 3 个脉冲 + // 3 pulses +} m5ioe1_aw8737a_pulse_t; + +// ============================ +// AW8737A 模式类型 +// AW8737A Mode Types +// ============================ +typedef enum { + M5IOE1_AW8737A_MODE_1 = 0, // 模式1: 0脉冲 (关闭/静音) + // Mode 1: 0 pulse (off/mute) + M5IOE1_AW8737A_MODE_2 = 1, // 模式2: 1脉冲 (低增益) + // Mode 2: 1 pulse (low gain) + M5IOE1_AW8737A_MODE_3 = 2, // 模式3: 2脉冲 (中增益) + // Mode 3: 2 pulses (medium gain) + M5IOE1_AW8737A_MODE_4 = 3 // 模式4: 3脉冲 (高增益) + // Mode 4: 3 pulses (high gain) +} m5ioe1_aw8737a_mode_t; + +// ============================ +// 中断处理模式 +// Interrupt Handling Mode +// ============================ +typedef enum { + M5IOE1_INT_MODE_DISABLED = 0, // 中断处理已禁用 + // Interrupt handling disabled + M5IOE1_INT_MODE_POLLING, // 轮询模式(默认) + // Polling mode (default) + M5IOE1_INT_MODE_HARDWARE // 硬件中断模式 + // Hardware interrupt mode +} m5ioe1_int_mode_t; + +// ============================ +// I2C 速度定义 +// I2C Speed Definitions +// ============================ +typedef enum { + M5IOE1_I2C_SPEED_100K = 0, // 100KHz 标准模式 + // 100KHz standard mode + M5IOE1_I2C_SPEED_400K = 1 // 400KHz 快速模式 + // 400KHz fast mode +} m5ioe1_i2c_speed_t; + +// ============================ +// I2C 唤醒边沿定义 +// I2C Wake Edge Definitions +// ============================ +typedef enum { + M5IOE1_WAKE_EDGE_FALLING = 0, // 下降沿唤醒(默认) + // Falling edge wake (default) + M5IOE1_WAKE_EDGE_RISING = 1 // 上升沿唤醒 + // Rising edge wake +} m5ioe1_wake_edge_t; + +// ============================ +// I2C 内部上拉定义 +// I2C Internal Pull-up Definitions +// ============================ +typedef enum { + M5IOE1_PULL_ENABLED = 0, // 内部上拉启用(默认) + // Internal pull-up enabled (default) + M5IOE1_PULL_DISABLED = 1 // 内部上拉禁用 + // Internal pull-up disabled +} m5ioe1_pull_config_t; + +// ============================ +// 快照域定义(位掩码) +// Snapshot Domain Definitions (bitmask) +// ============================ +typedef enum { + M5IOE1_SNAPSHOT_DOMAIN_GPIO = 1 << 0, // GPIO 引脚状态 + // GPIO pin states + M5IOE1_SNAPSHOT_DOMAIN_PWM = 1 << 1, // PWM 配置 + // PWM configuration + M5IOE1_SNAPSHOT_DOMAIN_ADC = 1 << 2, // ADC 状态 + // ADC state + M5IOE1_SNAPSHOT_DOMAIN_AW8737A = 1 << 3, // AW8737A 音频放大器 + // AW8737A audio amplifier + M5IOE1_SNAPSHOT_DOMAIN_ALL = 0x0F // 所有域 + // All domains +} m5ioe1_snapshot_domain_t; + +// ============================ +// 日志级别定义 +// Log Level Definitions +// ============================ +typedef enum { + M5IOE1_LOG_LEVEL_NONE = 0, // 无日志输出 + // No log output + M5IOE1_LOG_LEVEL_ERROR, // 仅错误消息 + // Error messages only + M5IOE1_LOG_LEVEL_WARN, // 警告和错误消息 + // Warning and error messages + M5IOE1_LOG_LEVEL_INFO, // 信息、警告和错误消息(默认) + // Info, warning and error messages (default) + M5IOE1_LOG_LEVEL_DEBUG, // 调试、信息、警告和错误消息 + // Debug, info, warning and error messages + M5IOE1_LOG_LEVEL_VERBOSE // 所有消息包括详细输出 + // All messages including verbose +} m5ioe1_log_level_t; + +// ============================ +// 用于验证的配置类型 +// Configuration Type for Validation +// ============================ +typedef enum { + M5IOE1_CONFIG_GPIO_INPUT = 0, // GPIO 输入模式 + // GPIO input mode + M5IOE1_CONFIG_GPIO_OUTPUT, // GPIO 输出模式 + // GPIO output mode + M5IOE1_CONFIG_GPIO_INTERRUPT, // GPIO 中断模式 + // GPIO interrupt mode + M5IOE1_CONFIG_ADC, // ADC 功能 + // ADC function + M5IOE1_CONFIG_PWM, // PWM 功能 + // PWM function + M5IOE1_CONFIG_NEOPIXEL, // NeoPixel LED 功能(仅 IO14) + // NeoPixel LED function (IO14 only) + M5IOE1_CONFIG_I2C_SLEEP // I2C 睡眠模式配置 + // I2C sleep mode configuration +} m5ioe1_config_type_t; + +// ============================ +// RGB 颜色结构 +// RGB Color Structure +// ============================ +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; +} m5ioe1_rgb_t; + +// ============================ +// 配置验证结果 +// Configuration Validation Result +// ============================ +typedef struct { + bool valid; + char error_msg[64]; + uint8_t conflicting_pin; +} m5ioe1_validation_t; + +// ============================ +// 快照验证结果 +// Snapshot Verification Result +// ============================ +typedef struct { + bool consistent; // 如果所有缓存值与硬件寄存器匹配则为 true + // true if all cached values match hardware registers + bool gpio_mismatch; // 如果 GPIO 寄存器与缓存不匹配则为 true + // true if GPIO registers don't match cache + bool pwm_mismatch; // 如果 PWM 寄存器与缓存不匹配则为 true + // true if PWM registers don't match cache + bool adc_mismatch; // 如果 ADC 寄存器与缓存不匹配则为 true + // true if ADC registers don't match cache + bool aw8737a_mismatch; // 如果 AW8737A 寄存器与缓存不匹配则为 true + // true if AW8737A registers don't match cache + uint16_t expected_mode; // 缓存的 GPIO 模式寄存器值 + // cached GPIO mode register value + uint16_t actual_mode; // 实际的 GPIO 模式寄存器值 + // actual GPIO mode register value + uint16_t expected_output; // 缓存的 GPIO 输出寄存器值 + // cached GPIO output register value + uint16_t actual_output; // 实际的 GPIO 输出寄存器值 + // actual GPIO output register value + uint8_t expected_aw8737a; // 缓存的 AW8737A 寄存器值 + // cached AW8737A register value + uint8_t actual_aw8737a; // 实际的 AW8737A 寄存器值 + // actual AW8737A register value +} m5ioe1_snapshot_verify_t; + +// ============================ +// 回调类型 +// Callback Types +// ============================ +typedef void (*m5ioe1_callback_t)(void); +typedef void (*m5ioe1_callback_arg_t)(void*); + +// ============================ +// M5IOE1 类 +// M5IOE1 Class +// ============================ +class M5IOE1 { +public: + /** + * @brief 构造 M5IOE1 对象 + * Construct M5IOE1 object + */ + M5IOE1(); + /** + * @brief 析构 M5IOE1 对象并释放资源 + * Destroy M5IOE1 object and release resources + */ + ~M5IOE1(); + + // ======================== + // 初始化 + // Initialization + // ======================== +#ifdef ARDUINO + /** + * @brief Initialize the M5IOE1 device (Arduino) + * @note Without intPin (or intPin=-1): Only POLLING and DISABLED modes are supported + * @note With intPin >= 0: Supports HARDWARE, POLLING, and DISABLED modes + * @note HARDWARE mode: Configures intPin as input with internal pull-up. Uses FALLING edge, then reads GPIO_IS + * registers (0x11-0x12) + * @note POLLING mode: Periodically reads GPIO_IS registers (0x11-0x12). Non-zero indicates one or more pins + * triggered + * @note DISABLED mode: No interrupt handling + * @param wire Pointer to TwoWire instance + * @param addr I2C address (default 0x6F) + * @param sda SDA pin (default -1, uses default I2C pins) + * @param scl SCL pin (default -1, uses default I2C pins) + * @param speed I2C speed in Hz (default 100000) + * @param intPin Hardware interrupt pin (-1 = no hardware interrupt, default -1) + * @param intMode Interrupt mode (default M5IOE1_INT_MODE_POLLING) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(TwoWire* wire, uint8_t addr = M5IOE1_DEFAULT_ADDR, uint8_t sda = -1, uint8_t scl = -1, + uint32_t speed = 100000, int8_t intPin = -1, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_POLLING); +#if M5IOE1_HAS_M5UNIFIED_I2C + /** + * @brief Initialize with M5Unified I2C_Class instance, no hardware interrupt (Arduino) + * @note The I2C bus must already be initialized (i2c->begin() called). + * M5IOE1 borrows the I2C_Class; caller retains ownership and lifecycle. + * 不负责驱动安装或释放,I2C 由调用方全程管理。 + * @param i2c 已 begin() 的 m5::I2C_Class 指针 / Already begin()-ed m5::I2C_Class + * @param addr I2C 地址 / I2C address (default 0x6F) + * @param speed I2C 速率(Hz)/ I2C speed in Hz (default 100000) + * @param intMode 中断模式 / Interrupt mode (default POLLING) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(m5::I2C_Class* i2c, uint8_t addr = M5IOE1_DEFAULT_ADDR, uint32_t speed = M5IOE1_I2C_FREQ_100K, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_POLLING); + + /** + * @brief Initialize with M5Unified I2C_Class instance, with hardware interrupt (Arduino) + * @param i2c 已 begin() 的 m5::I2C_Class 指针 / Already begin()-ed m5::I2C_Class + * @param addr I2C 地址 / I2C address + * @param speed I2C 速率(Hz)/ I2C speed in Hz + * @param intPin 硬件中断引脚 / Hardware interrupt pin + * @param intMode 中断模式 / Interrupt mode (default HARDWARE) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(m5::I2C_Class* i2c, uint8_t addr, uint32_t speed, int intPin, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_HARDWARE); +#endif // M5IOE1_HAS_M5UNIFIED_I2C +#else // ESP-IDF + // ===================================================== + // Type 1A: Self-created I2C bus, no hardware interrupt + // ===================================================== + /** + * @brief Initialize with self-created I2C bus, no hardware interrupt (ESP-IDF) + * @param port I2C port number (I2C_NUM_0 or I2C_NUM_1) + * @param addr I2C address (default 0x6F) + * @param sda SDA pin (default 21) + * @param scl SCL pin (default 22) + * @param speed I2C speed in Hz (only 100000 or 400000 supported) + * @param intMode Interrupt mode: M5IOE1_INT_MODE_POLLING or M5IOE1_INT_MODE_DISABLED + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(i2c_port_t port = I2C_NUM_0, uint8_t addr = M5IOE1_DEFAULT_ADDR, int sda = 21, int scl = 22, + uint32_t speed = M5IOE1_I2C_FREQ_100K, m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_POLLING); + + // ===================================================== + // Type 1B: Self-created I2C bus, with hardware interrupt + // ===================================================== + /** + * @brief Initialize with self-created I2C bus, with hardware interrupt (ESP-IDF) + * @param port I2C port number + * @param addr I2C address + * @param sda SDA pin + * @param scl SCL pin + * @param speed I2C speed in Hz + * @param intPin Hardware interrupt GPIO pin + * @param intMode Interrupt mode: M5IOE1_INT_MODE_HARDWARE, POLLING, or DISABLED + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(i2c_port_t port, uint8_t addr, int sda, int scl, uint32_t speed, int intPin, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_HARDWARE); + + // ===================================================== + // Type 2A/2B: Existing i2c_master_bus_handle_t (ESP-IDF >= 5.3.0 only) + // ===================================================== +#if M5IOE1_HAS_I2C_MASTER + /** + * @brief Initialize with existing i2c_master_bus handle (ESP-IDF native driver, IDF >= 5.3.0) + * @param bus Existing i2c_master_bus_handle_t + * @param addr I2C address (default 0x6F) + * @param speed I2C speed in Hz (for device handle creation) + * @param intMode Interrupt mode: M5IOE1_INT_MODE_POLLING or M5IOE1_INT_MODE_DISABLED + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(i2c_master_bus_handle_t bus, uint8_t addr = M5IOE1_DEFAULT_ADDR, + uint32_t speed = M5IOE1_I2C_FREQ_100K, m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_POLLING); + + /** + * @brief Initialize with existing i2c_master_bus handle, with hardware interrupt (IDF >= 5.3.0) + * @param bus Existing i2c_master_bus_handle_t + * @param addr I2C address + * @param speed I2C speed in Hz + * @param intPin Hardware interrupt GPIO pin + * @param intMode Interrupt mode + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(i2c_master_bus_handle_t bus, uint8_t addr, uint32_t speed, int intPin, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_HARDWARE); +#endif // M5IOE1_HAS_I2C_MASTER + + // ===================================================== + // Type 3A: Existing i2c_bus_handle_t, no hardware interrupt + // ===================================================== +#if M5IOE1_HAS_I2C_BUS + /** + * @brief Initialize with existing i2c_bus handle (esp-idf-lib component) + * @param bus Existing i2c_bus_handle_t + * @param addr I2C address (default 0x6F) + * @param speed I2C speed in Hz (for device handle creation) + * @param intMode Interrupt mode: M5IOE1_INT_MODE_POLLING or M5IOE1_INT_MODE_DISABLED + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(i2c_bus_handle_t bus, uint8_t addr = M5IOE1_DEFAULT_ADDR, uint32_t speed = M5IOE1_I2C_FREQ_100K, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_POLLING); + + // ===================================================== + // Type 3B: Existing i2c_bus_handle_t, with hardware interrupt + // ===================================================== + /** + * @brief Initialize with existing i2c_bus handle, with hardware interrupt + * @param bus Existing i2c_bus_handle_t + * @param addr I2C address + * @param speed I2C speed in Hz + * @param intPin Hardware interrupt GPIO pin + * @param intMode Interrupt mode + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(i2c_bus_handle_t bus, uint8_t addr, uint32_t speed, int intPin, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_HARDWARE); +#else + /** + * @brief i2c_bus overload is intentionally kept for diagnostics when unavailable + * @note This overload exists only to provide a clear compile-time message when called. + */ + inline m5ioe1_err_t begin(i2c_bus_handle_t bus, uint8_t addr = M5IOE1_DEFAULT_ADDR, + uint32_t speed = M5IOE1_I2C_FREQ_100K, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_POLLING) + { + (void)bus; + (void)addr; + (void)speed; + (void)intMode; + return M5IOE1_ERR_NOT_SUPPORTED; + } + inline m5ioe1_err_t begin(i2c_bus_handle_t bus, uint8_t addr, uint32_t speed, int intPin, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_HARDWARE) + { + (void)bus; + (void)addr; + (void)speed; + (void)intPin; + (void)intMode; + return M5IOE1_ERR_NOT_SUPPORTED; + } +#endif // M5IOE1_HAS_I2C_BUS + +#if M5IOE1_HAS_M5UNIFIED_I2C + // ===================================================== + // Type 4A: M5Unified I2C_Class, no hardware interrupt + // ===================================================== + /** + * @brief Initialize with M5Unified I2C_Class instance, no hardware interrupt (ESP-IDF) + * @note The I2C bus must already be initialized (i2c->begin() called). + * M5IOE1 borrows the I2C_Class; caller retains ownership and lifecycle. + * 不负责驱动安装或释放,I2C 由调用方全程管理。 + * @param i2c 已 begin() 的 m5::I2C_Class 指针 / Already begin()-ed m5::I2C_Class + * @param addr I2C 地址 / I2C address (default 0x6F) + * @param speed I2C 速率(Hz)/ I2C speed in Hz (default 100000) + * @param intMode 中断模式 / Interrupt mode (POLLING or DISABLED) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(m5::I2C_Class* i2c, uint8_t addr = M5IOE1_DEFAULT_ADDR, uint32_t speed = M5IOE1_I2C_FREQ_100K, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_POLLING); + + // ===================================================== + // Type 4B: M5Unified I2C_Class, with hardware interrupt + // ===================================================== + /** + * @brief Initialize with M5Unified I2C_Class instance, with hardware interrupt (ESP-IDF) + * @param i2c 已 begin() 的 m5::I2C_Class 指针 / Already begin()-ed m5::I2C_Class + * @param addr I2C 地址 / I2C address + * @param speed I2C 速率(Hz)/ I2C speed in Hz + * @param intPin 硬件中断 GPIO引脚 / Hardware interrupt GPIO pin + * @param intMode 中断模式 / Interrupt mode + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t begin(m5::I2C_Class* i2c, uint8_t addr, uint32_t speed, int intPin, + m5ioe1_int_mode_t intMode = M5IOE1_INT_MODE_HARDWARE); +#endif // M5IOE1_HAS_M5UNIFIED_I2C + +#endif // !ARDUINO + + /** + * @brief Set interrupt handling mode + * @param intMode Interrupt mode (DISABLED, POLLING, HARDWARE) + * @param pollingIntervalMs Polling interval in ms (for polling mode, default 5000ms) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setInterruptMode(m5ioe1_int_mode_t intMode, uint32_t pollingIntervalMs = 5000); + + /** + * @brief Set polling interval for POLLING mode + * @param seconds Polling interval in seconds (range: 0.001 to 3600, supports float) + * @return M5IOE1_OK if successful, M5IOE1_ERR_INVALID_ARG if out of range + * @note If already in POLLING mode, the task will be restarted with new interval + */ + m5ioe1_err_t setPollingInterval(float seconds); + + /** + * @brief Set global log level for M5IOE1 library + * @param level Log level (ERROR, WARN, INFO, DEBUG, VERBOSE) + * @note For ESP-IDF: Uses esp_log_level_set() to control ESP_LOGx macros + * @note For Arduino: Controls Serial.printf output filtering + * @note Default level is M5IOE1_LOG_LEVEL_INFO + */ + static void setLogLevel(m5ioe1_log_level_t level); + + /** + * @brief Get current global log level + * @return Current log level + */ + static m5ioe1_log_level_t getLogLevel(); + + // ======================== + // 设备信息 + // Device Information + // ======================== + /** + * @brief 读取设备 UID + * Read device UID + * @param uid 输出:UID(16-bit) + * Output: 16-bit UID + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getUID(uint16_t* uid); + /** + * @brief 读取设备版本号 + * Read device version + * @param version 输出:版本号 + * Output: version value + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getVersion(uint8_t* version); + /** + * @brief 读取参考电压(mV) + * Read reference voltage (mV) + * @param voltage_mv 输出:参考电压(mV) + * Output: reference voltage (mV) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getRefVoltage(uint16_t* voltage_mv); + + // ======================== + // GPIO 功能(Arduino 风格) + // GPIO Functions (Arduino-style) + // ======================== + /** + * @brief 设置 GPIO 模式(无返回值) + * Set GPIO mode (no return value) + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param mode 模式:INPUT/OUTPUT/INPUT_PULLUP/INPUT_PULLDOWN/OUTPUT_OPEN_DRAIN 等 + * Mode: INPUT/OUTPUT/INPUT_PULLUP/INPUT_PULLDOWN/OUTPUT_OPEN_DRAIN, etc. + * @note 如需错误码请使用 pinModeWithRes + * Use pinModeWithRes for error codes + */ + void pinMode(uint8_t pin, uint8_t mode); + /** + * @brief 写入数字电平(无返回值) + * Write digital level (no return value) + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param value 电平值:LOW 或 HIGH + * Level: LOW or HIGH + * @note 如需错误码请使用 digitalWriteWithRes + * Use digitalWriteWithRes for error codes + */ + void digitalWrite(uint8_t pin, uint8_t value); + /** + * @brief 读取数字电平(错误时返回 -1) + * Read digital level (returns -1 on error) + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @return 成功返回 0/1,失败返回 -1 + * Returns 0/1 on success, -1 on failure + */ + int digitalRead(uint8_t pin); + + // ======================== + // GPIO 功能(Arduino 风格 - 带错误码) + // GPIO Functions (Arduino-style - with error code) + // ======================== + /** + * @brief 设置 GPIO 模式并返回错误码 + * Set GPIO mode and return error code + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param mode 模式:INPUT/OUTPUT/INPUT_PULLUP/INPUT_PULLDOWN/OUTPUT_OPEN_DRAIN 等 + * Mode: INPUT/OUTPUT/INPUT_PULLUP/INPUT_PULLDOWN/OUTPUT_OPEN_DRAIN, etc. + * @param err 输出:错误码指针(不能为空) + * Output: error code pointer (must not be NULL) + * @note err 为空时仅记录日志并返回 + * If err is NULL, the function only logs and returns + */ + void pinModeWithRes(uint8_t pin, uint8_t mode, m5ioe1_err_t* err); + /** + * @brief 写入数字电平并返回错误码 + * Write digital level and return error code + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param value 电平值:LOW 或 HIGH + * Level: LOW or HIGH + * @param err 输出:错误码指针(不能为空) + * Output: error code pointer (must not be NULL) + * @note err 为空时仅记录日志并返回 + * If err is NULL, the function only logs and returns + */ + void digitalWriteWithRes(uint8_t pin, uint8_t value, m5ioe1_err_t* err); + /** + * @brief 读取数字电平并返回错误码 + * Read digital level and return error code + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param err 输出:错误码指针(不能为空) + * Output: error code pointer (must not be NULL) + * @return 成功返回 0/1,失败返回 -1 + * Returns 0/1 on success, -1 on failure + * @note err 为空时仅记录日志并返回 -1 + * If err is NULL, the function only logs and returns -1 + */ + int digitalReadWithRes(uint8_t pin, m5ioe1_err_t* err); + + // ======================== + // 中断功能 + // Interrupt Functions + // ======================== + /** + * @brief 绑定 GPIO 中断回调(无参数) + * Attach GPIO interrupt callback (no argument) + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param callback 回调函数 + * Callback function + * @param mode 中断模式:RISING 或 FALLING(非 RISING 值均视为 FALLING) + * Interrupt mode: RISING or FALLING (non-RISING values treated as FALLING) + * @note 需要先调用 begin 完成初始化 + * begin must be called before use + */ + void attachInterrupt(uint8_t pin, m5ioe1_callback_t callback, uint8_t mode); + /** + * @brief 绑定 GPIO 中断回调(带参数) + * Attach GPIO interrupt callback (with argument) + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param callback 回调函数(带参数) + * Callback function with argument + * @param arg 回调参数 + * Callback argument + * @param mode 中断模式:RISING 或 FALLING(非 RISING 值均视为 FALLING) + * Interrupt mode: RISING or FALLING (non-RISING values treated as FALLING) + * @note 需要先调用 begin 完成初始化 + * begin must be called before use + */ + void attachInterruptArg(uint8_t pin, m5ioe1_callback_arg_t callback, void* arg, uint8_t mode); + /** + * @brief 解绑并禁用 GPIO 中断 + * Detach and disable GPIO interrupt + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + */ + void detachInterrupt(uint8_t pin); + /** + * @brief 启用回调触发(软件开关) + * Enable callback trigger (software switch) + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @note 不修改硬件中断寄存器 + * Does not change hardware interrupt registers + */ + void enableInterrupt(uint8_t pin); + /** + * @brief 禁用回调触发(软件开关) + * Disable callback trigger (software switch) + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @note 不修改硬件中断寄存器 + * Does not change hardware interrupt registers + */ + void disableInterrupt(uint8_t pin); + /** + * @brief 获取中断状态位(GPIO_IS) + * Get interrupt status bits (GPIO_IS) + * @param status 输出:中断状态位掩码 + * Output: interrupt status bitmask + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getInterruptStatus(uint16_t* status); + /** + * @brief 清除所有中断状态 + * Clear all interrupt status + * @return M5IOE1_OK if successful, error code otherwise + * @note GPIO_IS 寄存器为"写 0 清除"语义,会清除所有引脚的中断状态 + * GPIO_IS register uses "write 0 to clear" semantics, clears all pins' interrupt status + */ + m5ioe1_err_t clearInterrupt(); + + /** + * @brief 清除指定引脚的中断状态 + * Clear interrupt status for a specific pin + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @return M5IOE1_OK if successful, error code otherwise + * @note GPIO_IS 寄存器为"写 0 清除"语义,仅清除指定引脚的中断位 + * GPIO_IS register uses "write 0 to clear" semantics, clears only the specified pin's interrupt bit + */ + m5ioe1_err_t clearInterrupt(uint8_t pin); + + // ======================== + // 高级 GPIO 功能 + // Advanced GPIO Functions + // ======================== + /** + * @brief 设置上拉/下拉模式 + * Set pull-up/pull-down mode + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param pullMode 上拉/下拉模式:M5IOE1_PULL_NONE/UP/DOWN + * Pull mode: M5IOE1_PULL_NONE/UP/DOWN + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setPullMode(uint8_t pin, uint8_t pullMode); + /** + * @brief 设置输出驱动模式 + * Set output drive mode + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param driveMode 驱动模式:M5IOE1_DRIVE_PUSHPULL 或 M5IOE1_DRIVE_OPENDRAIN + * Drive mode: M5IOE1_DRIVE_PUSHPULL or M5IOE1_DRIVE_OPENDRAIN + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setDriveMode(uint8_t pin, uint8_t driveMode); + /** + * @brief 读取 GPIO 输入电平 + * Read GPIO input level + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param state 输出:电平状态(0/1) + * Output: level state (0/1) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getInputState(uint8_t pin, uint8_t* state); + + // ======================== + // ADC 功能 + // ADC Functions + // ======================== + /** + * @brief Read ADC value + * @param channel ADC channel (1-4) + * @param result Pointer to store 12-bit result + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t analogRead(uint8_t channel, uint16_t* result); + /** + * @brief 查询 ADC 是否忙 + * Check if ADC is busy + * @param busy 输出:busy 状态 + * Output: busy status + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t isAdcBusy(bool* busy); + /** + * @brief 禁用 ADC 并清零控制寄存器 + * Disable ADC and clear control register + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t disableAdc(); + + // ======================== + // 温度传感器 + // Temperature Sensor + // ======================== + /** + * @brief 读取温度传感器原始值 + * Read temperature sensor raw value + * @param temperature 输出:温度原始值(12-bit) + * Output: raw temperature value (12-bit) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t readTemperature(uint16_t* temperature); + /** + * @brief 查询温度转换是否忙 + * Check if temperature conversion is busy + * @param busy 输出:busy 状态 + * Output: busy status + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t isTemperatureBusy(bool* busy); + + // ======================== + // PWM 功能 + // PWM Functions + // ======================== + /** + * @brief Set PWM frequency (shared by all channels) + * @param frequency Frequency in Hz + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setPwmFrequency(uint16_t frequency); + /** + * @brief 读取 PWM 频率 + * Read PWM frequency + * @param frequency 输出:频率(Hz) + * Output: frequency in Hz + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getPwmFrequency(uint16_t* frequency); + + /** + * @brief Set PWM duty cycle (percentage) + * @param channel PWM channel (0-3) + * @param duty Duty cycle percentage (0-100) + * @param polarity PWM polarity (false=normal, true=inverted) + * @param enable Enable PWM output + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setPwmDuty(uint8_t channel, uint8_t duty, bool polarity = false, bool enable = true); + /** + * @brief 读取 PWM 占空比(百分比) + * Read PWM duty cycle (percentage) + * @param channel PWM 通道(0-3) + * PWM channel (0-3) + * @param duty 输出:占空比百分比(0-100) + * Output: duty percentage (0-100) + * @param polarity 输出:极性状态 + * Output: polarity state + * @param enable 输出:使能状态 + * Output: enable state + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getPwmDuty(uint8_t channel, uint8_t* duty, bool* polarity, bool* enable); + + /** + * @brief Set PWM duty cycle (12-bit value) + * @param channel PWM channel (0-3) + * @param duty12 12-bit duty value (0-4095) + * @param polarity PWM polarity + * @param enable Enable PWM output + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setPwmDuty12bit(uint8_t channel, uint16_t duty12, bool polarity = false, bool enable = true); + /** + * @brief 读取 PWM 占空比(12-bit) + * Read PWM duty cycle (12-bit) + * @param channel PWM 通道(0-3) + * PWM channel (0-3) + * @param duty12 输出:12-bit 占空比(0-4095) + * Output: 12-bit duty (0-4095) + * @param polarity 输出:极性状态 + * Output: polarity state + * @param enable 输出:使能状态 + * Output: enable state + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getPwmDuty12bit(uint8_t channel, uint16_t* duty12, bool* polarity, bool* enable); + + /** + * @brief 一次性配置 PWM 参数 + * Configure PWM in one call + * @param channel PWM 通道(0-3) + * PWM channel (0-3) + * @param enable 输出使能(true=启用,false=禁用) + * Enable output (true=enable, false=disable) + * @param polarity 极性(false=正常,true=反相) + * Polarity (false=normal, true=inverted) + * @param frequency PWM 频率(0-65535,全通道共享) + * PWM frequency in Hz (0-65535, shared by all channels) + * @param duty12 12 位占空比(0-4095) + * 12-bit duty (0-4095) + * @note 此 API 对冲突仅告警,仍会继续配置 + * This API warns on conflicts but still applies settings + * @note 变更频率会影响所有通道 + * Changing frequency affects all channels + */ + m5ioe1_err_t setPwmConfig(uint8_t channel, bool enable, bool polarity, uint16_t frequency, uint16_t duty12); + + /** + * @brief Arduino 兼容的 analogWrite 函数(PWM 输出) + * Arduino-compatible analogWrite function (PWM output) + * @param channel PWM 通道(0-3):M5IOE1_PWM_CH1/CH2/CH3/CH4 + * PWM channel (0-3): M5IOE1_PWM_CH1/CH2/CH3/CH4 + * CH1=IO9, CH2=IO8, CH3=IO11, CH4=IO10 + * @param value PWM 占空比(0-255,8-bit Arduino 标准) + * PWM duty cycle (0-255, 8-bit Arduino standard) + * 0 = 0% duty, 127 = 50% duty, 255 = 100% duty + * @return M5IOE1_OK if successful, error code otherwise + * @note 此函数内部将 8-bit 值缩放到 12-bit + * This function scales 8-bit value to 12-bit internally + * @note 值为 0 时关闭 PWM 输出 + * Value 0 turns off PWM output + */ + m5ioe1_err_t analogWrite(uint8_t channel, uint8_t value); + + // ======================== + // NeoPixel LED 功能 + // NeoPixel LED Functions + // ======================== + /** + * @brief 一次性配置并设置所有 NeoPixel LED / Configure and set all NeoPixel LEDs at once + * @param colors RGB 颜色数组 / Array of RGB colors + * @param arraySize 颜色数组的大小(用于边界检查) / Size of the colors array (for bounds checking) + * @param count 要设置的 LED 数量(1-32) / Number of LEDs to set (1-32) + * @param autoRefresh 如果为 true,设置后自动刷新 LED(默认:true) + * If true, automatically refresh LEDs after setting (default: true) + * @return M5IOE1_OK if successful, error code otherwise + * @note 此函数将: + * 1. 验证参数(count <= arraySize,count <= 32) + * 2. 设置 LED 数量寄存器 + * 3. 将所有 RGB 数据写入 LED RAM + * 4. 可选触发刷新 + * @note This function will: + * 1. Validate parameters (count <= arraySize, count <= 32) + * 2. Set LED count register + * 3. Write all RGB data to LED RAM + * 4. Optionally trigger refresh + * @example + * m5ioe1_rgb_t leds[8] = { + * {255, 0, 0}, // Red + * {0, 255, 0}, // Green + * {0, 0, 255}, // Blue + * {255, 255, 0}, // Yellow + * {255, 0, 255}, // Magenta + * {0, 255, 255}, // Cyan + * {255, 255, 255},// White + * {128, 128, 128} // Gray + * }; + * ioe1.setLeds(leds, sizeof(leds)/sizeof(leds[0]), 8, true); + */ + m5ioe1_err_t setLeds(const m5ioe1_rgb_t* colors, uint8_t arraySize, uint8_t count, bool autoRefresh = true); + /** + * @brief 设置 NeoPixel LED 数量 + * Set NeoPixel LED count + * @param count LED 数量(0-32) + * LED count (0-32) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setLedCount(uint8_t count); + /** + * @brief 设置单个 LED 颜色(RGB888) + * Set a single LED color (RGB888) + * @param index LED 索引(0-31) + * LED index (0-31) + * @param r 红色分量(0-255) + * Red component (0-255) + * @param g 绿色分量(0-255) + * Green component (0-255) + * @param b 蓝色分量(0-255) + * Blue component (0-255) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setLedColor(uint8_t index, uint8_t r, uint8_t g, uint8_t b); + /** + * @brief 设置单个 LED 颜色(结构体) + * Set a single LED color (struct) + * @param index LED 索引(0-31) + * LED index (0-31) + * @param color RGB 颜色结构体 + * RGB color struct + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setLedColor(uint8_t index, m5ioe1_rgb_t color); + /** + * @brief 刷新 LED 显示 + * Refresh LED display + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t refreshLeds(); + /** + * @brief 禁用所有 LED 并清零配置 + * Disable all LEDs and clear configuration + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t disableLeds(); + + /** + * @brief 清除 LED RAM 寄存器(将所有 LED 数据设置为 0) + * Clear LED RAM registers (set all LED data to 0) + * @return M5IOE1_OK if successful, error code otherwise + * @note 此函数会清除所有 32 个 LED 的颜色数据,建议在 begin() 后调用 + * This function clears color data for all 32 LEDs, recommended to call after begin() + */ + m5ioe1_err_t clearLedRam(); + + // ======================== + // AW8737A 脉冲功能 + // AW8737A Pulse Functions + // ======================== + /** + * @brief 设置 AW8737A 脉冲输出配置 + * Set AW8737A pulse output configuration + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param pulseNum 脉冲数量(0-3) + * Number of pulses (0-3) + * @param refresh 刷新控制:WAIT 或 NOW + * Refresh control: WAIT or NOW + * @return M5IOE1_OK if successful, error code otherwise + * @note 如果引脚不是输出模式,会自动配置为推挽输出;如果已是输出模式则保持原有配置 + * If pin is not output mode, it will be auto-configured as push-pull output; if already output, keeps current + * config + * @note 如果使用开漏输出,需要外部上拉电阻 + * If using open-drain output, external pull-up is required + * @note 当 refresh=NOW 时,执行后会有 20ms 延迟 + * When refresh=NOW, there will be a 20ms delay after execution + */ + m5ioe1_err_t setAw8737aPulse(uint8_t pin, m5ioe1_aw8737a_pulse_t pulseNum, + m5ioe1_aw8737a_refresh_t refresh = M5IOE1_AW8737A_REFRESH_NOW); + + /** + * @brief 触发 AW8737A 脉冲刷新 + * Trigger AW8737A pulse refresh + * @return M5IOE1_OK if successful, error code otherwise + * @note 需要先调用 setAw8737aPulse 并设置 refresh=WAIT,然后调用此函数触发 + * Call setAw8737aPulse with refresh=WAIT first, then call this to trigger + * @note 如果引脚不是输出模式,会自动配置为推挽输出;如果已是输出模式则保持原有配置 + * If pin is not output mode, it will be auto-configured as push-pull output; if already output, keeps current + * config + * @note 如果使用开漏输出,需要外部上拉电阻 + * If using open-drain output, external pull-up is required + * @note 执行后会有 20ms 延迟 + * There will be a 20ms delay after execution + */ + m5ioe1_err_t refreshAw8737aPulse(); + + /** + * @brief 设置 AW8737A 增益模式并可选刷新 + * Set AW8737A gain mode with optional refresh + * @param pin GPIO 引脚号(0-13) + * GPIO pin number (0-13) + * @param mode 增益模式 (MODE_1 到 MODE_4) + * Gain mode (MODE_1 to MODE_4) + * @param refresh 刷新控制:WAIT 或 NOW + * Refresh control: WAIT or NOW + * @return M5IOE1_OK if successful, error code otherwise + * @note 如果引脚不是输出模式,会自动配置为推挽输出;如果已是输出模式则保持原有配置 + * If pin is not output mode, it will be auto-configured as push-pull output; if already output, keeps current + * config + * @note 如果使用开漏输出,需要外部上拉电阻 + * If using open-drain output, external pull-up is required + * @note 当 refresh=NOW 时,执行后会有 20ms 延迟 + * When refresh=NOW, there will be a 20ms delay after execution + */ + m5ioe1_err_t setAw8737aMode(uint8_t pin, m5ioe1_aw8737a_mode_t mode, + m5ioe1_aw8737a_refresh_t refresh = M5IOE1_AW8737A_REFRESH_NOW); + + /** + * @brief 刷新 AW8737A 模式配置 + * Refresh AW8737A mode configuration + * @return M5IOE1_OK if successful, error code otherwise + * @note 需要先调用 setAw8737aMode 并设置 refresh=WAIT,然后调用此函数触发 + * Call setAw8737aMode with refresh=WAIT first, then call this to trigger + * @note 如果引脚不是输出模式,会自动配置为推挽输出;如果已是输出模式则保持原有配置 + * If pin is not output mode, it will be auto-configured as push-pull output; if already output, keeps current + * config + * @note 如果使用开漏输出,需要外部上拉电阻 + * If using open-drain output, external pull-up is required + * @note 执行后会有 20ms 延迟 + * There will be a 20ms delay after execution + */ + m5ioe1_err_t refreshAw8737aMode(); + + // ======================== + // RTC RAM 功能 + // RTC RAM Functions + // ======================== + /** + * @brief 写入 RTC RAM 并回读验证 + * Write RTC RAM with read-back verification + * @param offset 起始偏移(0-31) + * Start offset (0-31) + * @param data 输入数据指针 + * Input data pointer + * @param length 数据长度(1-32,且 offset+length <= 32) + * Data length (1-32, offset+length <= 32) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t writeRtcRAM(uint8_t offset, const uint8_t* data, uint8_t length); + /** + * @brief 读取 RTC RAM + * Read RTC RAM + * @param offset 起始偏移(0-31) + * Start offset (0-31) + * @param data 输出数据缓冲区 + * Output data buffer + * @param length 数据长度(1-32,且 offset+length <= 32) + * Data length (1-32, offset+length <= 32) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t readRtcRAM(uint8_t offset, uint8_t* data, uint8_t length); + + // ======================== + // 系统配置 + // System Configuration + // ======================== + + /** + * @brief 一次性设置所有 I2C 配置 / Set all I2C configuration at once + * @param sleepTime 休眠超时(0=禁用,1-15=超时值) + * @param sleepTime Sleep timeout (0=disabled, 1-15=timeout value) + * @param speed I2C 速度(M5IOE1_I2C_SPEED_100K 或 M5IOE1_I2C_SPEED_400K) + * @param speed I2C speed (M5IOE1_I2C_SPEED_100K or M5IOE1_I2C_SPEED_400K) + * @param wakeEdge 唤醒边沿(M5IOE1_WAKE_EDGE_FALLING 或 M5IOE1_WAKE_EDGE_RISING) + * @param wakeEdge Wake edge (M5IOE1_WAKE_EDGE_FALLING or M5IOE1_WAKE_EDGE_RISING) + * @param pullConfig 上拉配置(M5IOE1_PULL_ENABLED 或 M5IOE1_PULL_DISABLED) + * @param pullConfig Pull-up config (M5IOE1_PULL_ENABLED or M5IOE1_PULL_DISABLED) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setI2cConfig(uint8_t sleepTime, m5ioe1_i2c_speed_t speed = M5IOE1_I2C_SPEED_100K, + m5ioe1_wake_edge_t wakeEdge = M5IOE1_WAKE_EDGE_FALLING, + m5ioe1_pull_config_t pullConfig = M5IOE1_PULL_ENABLED); + + /** + * @brief 切换 I2C 通讯速度 / Switch I2C communication speed + * @param speed 目标速度 (M5IOE1_I2C_SPEED_100K 或 M5IOE1_I2C_SPEED_400K) + * @param speed Target speed (M5IOE1_I2C_SPEED_100K or M5IOE1_I2C_SPEED_400K) + * @return M5IOE1_OK if successful, error code otherwise + * @note 此函数会同时配置设备和主机 I2C 总线 + * @note This function will configure both the device and host I2C bus + */ + m5ioe1_err_t switchI2cSpeed(m5ioe1_i2c_speed_t speed); + + /** + * @brief 获取当前 I2C 速度设置 / Get current I2C speed setting + * @param speed 存储速度值的指针 / Pointer to store the speed value + * @return M5IOE1_OK if successful, error code otherwise + * @note 从设备寄存器读取并更新内部缓存 + * @note Reads from device register and updates internal cache + */ + m5ioe1_err_t getI2cSpeed(m5ioe1_i2c_speed_t* speed); + + /** + * @brief 设置 I2C 休眠超时 / Set I2C sleep timeout + * @param sleepTime 休眠超时值(0=禁用,1-15=超时) + * @param sleepTime Sleep timeout value (0=disabled, 1-15=timeout) + * @return M5IOE1_OK if successful, error code otherwise + * @note 休眠时间公式:T = sleepTime * 基础时间 + * @note Sleep time formula: T = sleepTime * base_time + */ + m5ioe1_err_t setI2cSleepTime(uint8_t sleepTime); + + /** + * @brief 获取当前 I2C 休眠超时 / Get current I2C sleep timeout + * @param sleepTime 存储休眠超时值的指针 / Pointer to store the sleep timeout value + * @return M5IOE1_OK if successful, error code otherwise + * @note 从设备寄存器读取并更新内部缓存 + * @note Reads from device register and updates internal cache + */ + m5ioe1_err_t getI2cSleepTime(uint8_t* sleepTime); + + /** + * @brief 设置 I2C 唤醒边沿 / Set I2C wake edge + * @param edge 唤醒边沿(M5IOE1_WAKE_EDGE_FALLING 或 M5IOE1_WAKE_EDGE_RISING) + * @param edge Wake edge (M5IOE1_WAKE_EDGE_FALLING or M5IOE1_WAKE_EDGE_RISING) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setI2cWakeEdge(m5ioe1_wake_edge_t edge); + + /** + * @brief 获取当前 I2C 唤醒边沿 / Get current I2C wake edge + * @param edge 存储唤醒边沿设置的指针 / Pointer to store the wake edge setting + * @return M5IOE1_OK if successful, error code otherwise + * @note 从设备寄存器读取并更新内部缓存 + * @note Reads from device register and updates internal cache + */ + m5ioe1_err_t getI2cWakeEdge(m5ioe1_wake_edge_t* edge); + + /** + * @brief 设置 I2C 内部上拉配置 / Set I2C internal pull-up configuration + * @param config 上拉配置(M5IOE1_PULL_ENABLED 或 M5IOE1_PULL_DISABLED) + * @param config Pull-up config (M5IOE1_PULL_ENABLED or M5IOE1_PULL_DISABLED) + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t setI2cPullConfig(m5ioe1_pull_config_t config); + + /** + * @brief 获取当前 I2C 内部上拉配置 / Get current I2C internal pull-up configuration + * @param config 存储上拉配置的指针 / Pointer to store the pull-up configuration + * @return M5IOE1_OK if successful, error code otherwise + * @note 从设备寄存器读取并更新内部缓存 + * @note Reads from device register and updates internal cache + */ + m5ioe1_err_t getI2cPullConfig(m5ioe1_pull_config_t* config); + + /** + * @brief 恢复出厂设置 / Factory reset the device + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t factoryReset(); + + // ======================== + // 自动唤醒功能 + // Auto Wake Feature + // ======================== + /** + * @brief 启用/禁用 I2C 操作前的自动唤醒信号 + * Enable/disable automatic wake signal before I2C operations + * @param enable true to enable auto-wake, false to disable + * @note 当 IOE1 进入睡眠模式(I2C 睡眠超时)后,需要在 SDA 上发送 + * START 信号来唤醒。此功能会在需要时自动发送唤醒信号。 + * When IOE1 enters sleep mode (I2C sleep timeout), it needs a + * START signal on SDA to wake up. This feature automatically + * sends the wake signal when needed. + * @note 即使不启用此选项,通讯在大多数情况下也能成功,因为第一次 + * I2C 传输本身就能唤醒设备。启用此选项可确保可靠性。 + * Even without enabling this option, communication will likely + * succeed in most cases, as the first I2C transaction itself + * can wake the device. Enable this for guaranteed reliability. + */ + void setAutoWakeEnable(bool enable); + + /** + * @brief 检查自动唤醒是否启用 / Check if auto wake is enabled + * @return true if enabled + */ + bool isAutoWakeEnabled() const; + + /** + * @brief 手动发送唤醒信号到 IOE1 / Manually send wake signal to IOE1 + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t sendWakeSignal(); + + // ======================== + // 状态快照功能 + // State Snapshot Functions + // ======================== + /** + * @brief 启用/禁用自动快照更新 + * Enable/disable automatic snapshot updates + * @param enable true 启用,false 禁用 + * true to enable, false to disable + */ + void setAutoSnapshot(bool enable); + /** + * @brief 查询自动快照是否启用 + * Check if auto snapshot is enabled + * @return true if enabled + */ + bool isAutoSnapshotEnabled() const; + /** + * @brief 立即更新快照缓存 + * Update snapshot cache immediately + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t updateSnapshot(); + + // ======================== + // 调试功能 + // Debug Functions + // ======================== + /** + * @brief 读取 GPIO 模式寄存器 + * Read GPIO mode register + * @param reg 输出:寄存器值 + * Output: register value + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getModeReg(uint16_t* reg); + /** + * @brief 读取 GPIO 输出寄存器 + * Read GPIO output register + * @param reg 输出:寄存器值 + * Output: register value + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getOutputReg(uint16_t* reg); + /** + * @brief 读取 GPIO 输入寄存器 + * Read GPIO input register + * @param reg 输出:寄存器值 + * Output: register value + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getInputReg(uint16_t* reg); + /** + * @brief 读取 GPIO 上拉寄存器 + * Read GPIO pull-up register + * @param reg 输出:寄存器值 + * Output: register value + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getPullUpReg(uint16_t* reg); + /** + * @brief 读取 GPIO 下拉寄存器 + * Read GPIO pull-down register + * @param reg 输出:寄存器值 + * Output: register value + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getPullDownReg(uint16_t* reg); + /** + * @brief 读取 GPIO 驱动寄存器 + * Read GPIO drive register + * @param reg 输出:寄存器值 + * Output: register value + * @return M5IOE1_OK if successful, error code otherwise + */ + m5ioe1_err_t getDriveReg(uint16_t* reg); + + // ======================== + // 配置验证 + // Configuration Validation + // ======================== + /** + * @brief Validate pin configuration before applying + * @param pin Pin number (0-13) + * @param configType Configuration type (m5ioe1_config_type_t) + * @param enable true to enable config, false to disable + * @return Validation result with error details if invalid + */ + m5ioe1_validation_t validateConfig(uint8_t pin, m5ioe1_config_type_t configType, bool enable = true); + + // ======================== + // 快照验证 + // Snapshot Verification + // ======================== + /** + * @brief Verify that cached state matches actual hardware registers + * @return Verification result with mismatch details + */ + m5ioe1_snapshot_verify_t verifySnapshot(); + + // ======================== + // 缓存状态查询函数 + // Cached State Query Functions + // ======================== + /** + * @brief Get cached PWM frequency + * @param frequency Output: PWM frequency in Hz + * @return M5IOE1_OK if cache is valid, M5IOE1_FAIL if cache invalid + */ + m5ioe1_err_t getCachedPwmFrequency(uint16_t* frequency); + + /** + * @brief Get cached PWM channel state + * @param channel PWM channel (0-3) + * @param duty12 Output: 12-bit duty value + * @param polarity Output: polarity setting + * @param enabled Output: enable state + * @return M5IOE1_OK if cache is valid, M5IOE1_FAIL if cache invalid + */ + m5ioe1_err_t getCachedPwmState(uint8_t channel, uint16_t* duty12, bool* polarity, bool* enabled); + + /** + * @brief Get cached ADC state + * @param activeChannel Output: current ADC channel (0=disabled, 1-4=channel) + * @param busy Output: conversion status + * @param lastValue Output: last conversion result + * @return M5IOE1_OK if cache is valid, M5IOE1_FAIL if cache invalid + */ + m5ioe1_err_t getCachedAdcState(uint8_t* activeChannel, bool* busy, uint16_t* lastValue); + + /** + * @brief Get cached GPIO pin state + * @param pin Pin number (0-13) + * @param isOutput Output: true if pin is output + * @param level Output: current level (output or input) + * @param pull Output: pull mode (0=none, 1=up, 2=down) + * @return M5IOE1_OK if cache is valid, M5IOE1_FAIL if cache invalid + */ + m5ioe1_err_t getCachedPinState(uint8_t pin, bool* isOutput, uint8_t* level, uint8_t* pull); + + /** + * @brief Enable or disable default interrupt logging when no callback is registered + * @param enable true to enable logging, false to disable (default) + */ + void enableDefaultInterruptLog(bool enable); + +private: + // 设备状态 + // Device state + uint8_t _addr; + bool _initialized; + bool _autoSnapshot; + bool _enableDefaultIsrLog; + uint32_t _requestedSpeed; // 用户请求的 I2C 速度(用于 400K 切换) + // User requested I2C speed (for 400K switch) + + // 自动唤醒状态 + // Auto wake state + bool _autoWakeEnabled; // 自动唤醒是否启用 + // Whether auto wake is enabled + uint32_t _lastCommTime; // 上次通信时间(毫秒) + // Last communication time (milliseconds) + + // 中断模式 + // Interrupt mode + m5ioe1_int_mode_t _intMode; + int8_t _intPin; + uint32_t _pollingInterval; + +#ifdef ARDUINO + TwoWire* _wire; + uint8_t _sda; // SDA 引脚编号,用于 I2C 重新初始化 + // SDA pin number for I2C re-initialization + uint8_t _scl; // SCL 引脚编号,用于 I2C 重新初始化 + // SCL pin number for I2C re-initialization +#if M5IOE1_HAS_M5UNIFIED_I2C + m5::I2C_Class* _m5_i2c; + uint32_t _commFreq; // M5UNIFIED 路径专用,init 时从 100K 起,完成后切换至 _requestedSpeed + // M5UNIFIED path: starts at 100K during init, switches to _requestedSpeed after +#endif +#else + // I2C 驱动类型选择 + // I2C driver type selection + m5ioe1_i2c_driver_t _i2cDriverType; + + // I2C 句柄(根据驱动类型仅使用一对) + // I2C handles (only one pair is used based on driver type) + // M5IOE1_I2C_DRIVER_SELF_CREATED (IDF >= 5.3.0): 使用 _i2c_master_bus + _i2c_master_dev + // uses _i2c_master_bus + _i2c_master_dev + // M5IOE1_I2C_DRIVER_MASTER: 使用 _i2c_master_bus + _i2c_master_dev + // uses _i2c_master_bus + _i2c_master_dev + // M5IOE1_I2C_DRIVER_BUS: 使用 _i2c_bus + _i2c_device + // uses _i2c_bus + _i2c_device + // M5IOE1_I2C_DRIVER_LEGACY (IDF < 5.3.0): 使用 _port + _addr,无额外句柄 + // uses _port + _addr, no additional handle +#if M5IOE1_HAS_I2C_MASTER + i2c_master_bus_handle_t _i2c_master_bus; // ESP-IDF 原生驱动/自创建 (IDF >= 5.3.0) + // ESP-IDF native driver / self-created (IDF >= 5.3.0) + i2c_master_dev_handle_t _i2c_master_dev; // ESP-IDF 原生驱动/自创建 (IDF >= 5.3.0) + // ESP-IDF native driver / self-created (IDF >= 5.3.0) +#endif // M5IOE1_HAS_I2C_MASTER +#if M5IOE1_HAS_I2C_BUS + i2c_bus_handle_t _i2c_bus; // esp-idf-lib 组件 + // esp-idf-lib component + i2c_bus_device_handle_t _i2c_device; // esp-idf-lib 组件 + // esp-idf-lib component +#endif + + // I2C 管理标志 + // I2C management flags + bool _busExternal; // 如果总线句柄由外部提供则为 true + // true if bus handle is provided externally + + // 自创建总线的 I2C 引脚(用于频率切换) + // I2C pins for self-created bus (for frequency switching) + int _sda; + int _scl; + i2c_port_t _port; + + // 中断处理 + // Interrupt handling + QueueHandle_t _intrQueue; + + // M5Unified I2C_Class 借用句柄及当前通信频率 + // Borrowed M5Unified I2C_Class handle and current communication frequency +#if M5IOE1_HAS_M5UNIFIED_I2C + m5::I2C_Class* _m5_i2c; + uint32_t _commFreq; // M5UNIFIED 路径专用,init 时从 100K 起,完成后切换至 _requestedSpeed + // M5UNIFIED path: starts at 100K during init, switches to _requestedSpeed after +#endif +#endif + + // 中断轮询任务句柄(Arduino / ESP-IDF 共用) + // Interrupt poll task handle (shared by Arduino and ESP-IDF) + TaskHandle_t _pollTask; + + // 中断回调 + // Interrupt callbacks + struct { + m5ioe1_callback_t callback; + m5ioe1_callback_arg_t callbackArg; + void* arg; + bool enabled; + bool rising; + } _callbacks[M5IOE1_MAX_GPIO_PINS]; + + // 缓存的引脚状态 + // Cached pin states + struct { + bool isOutput; + uint8_t outputLevel; + uint8_t inputLevel; + uint8_t pull; // 0:无, 1:上拉, 2:下拉 + // 0:none, 1:up, 2:down + uint8_t drive; // 0:推挽, 1:开漏 + // 0:push-pull, 1:open-drain + bool intrEnabled; + bool intrRising; + } _pinStates[M5IOE1_MAX_GPIO_PINS]; + bool _pinStatesValid; + + // 缓存的 PWM 状态 + // Cached PWM states + struct { + uint16_t duty12; + bool enabled; + bool polarity; + } _pwmStates[M5IOE1_MAX_PWM_CHANNELS]; + uint16_t _pwmFrequency; + bool _pwmStatesValid; + + // 缓存的 ADC 状态 + // Cached ADC state + struct { + uint8_t activeChannel; + bool busy; + uint16_t lastValue; + } _adcState; + bool _adcStateValid; + + // 缓存的 I2C 配置状态(用于睡眠模式检测) + // Cached I2C config state (for sleep mode detection) + struct { + uint8_t sleepTime; // 0=禁用, 1-15=睡眠时间 + // 0=disabled, 1-15=sleep time + bool speed400k; // I2C 速度模式 + // I2C speed mode + bool wakeRising; // 唤醒边沿模式 + // Wake edge mode + bool pullOff; // 内部上拉关闭 + // Internal pull-up off + } _i2cConfig; + bool _i2cConfigValid; + + // NeoPixel 状态 + // NeoPixel state + + // AW8737A 配置状态缓存 + // AW8737A configuration state cache + bool _aw8737aConfigured; // 是否已调用 setAw8737aPulse 配置 + // Whether setAw8737aPulse has been called + uint8_t _aw8737aPin; // 配置的引脚号 + // Configured pin number + m5ioe1_aw8737a_pulse_t _aw8737aPulseNum; // 配置的脉冲数 + // Configured pulse count + uint8_t _aw8737aRegValue; // 缓存的寄存器值(不含 REFRESH 位) + // Cached register value (without REFRESH bit) + bool _aw8737aStateValid; // 缓存有效性标志 + // Cache validity flag + + // ======================== + // 内部辅助函数 + // Internal Helper Functions + // ======================== + m5ioe1_err_t _pinModeWithErr(uint8_t pin, uint8_t mode); + m5ioe1_err_t _digitalWriteWithErr(uint8_t pin, uint8_t value); + m5ioe1_err_t _digitalReadWithErr(uint8_t pin, int* value); + + bool _writeReg(uint8_t reg, uint8_t value); + bool _writeReg16(uint8_t reg, uint16_t value); + bool _readReg(uint8_t reg, uint8_t* value); + bool _readReg16(uint8_t reg, uint16_t* value); + bool _writeBytes(uint8_t reg, const uint8_t* data, uint8_t len); + bool _readBytes(uint8_t reg, uint8_t* data, uint8_t len); + + bool _isValidPin(uint8_t pin); + bool _isAdcPin(uint8_t pin); + bool _isPwmPin(uint8_t pin); + uint8_t _getAdcChannel(uint8_t pin); + uint8_t _getPwmChannel(uint8_t pin); + + void _clearPinStates(); + void _clearPwmStates(); + void _clearAdcState(); + bool _snapshotPinStates(); + bool _snapshotPwmStates(); + bool _snapshotAdcState(); + bool _snapshotAw8737a(); + void _clearAw8737a(); + void _autoSnapshotUpdate(uint8_t domains = M5IOE1_SNAPSHOT_DOMAIN_ALL); + + // ======================== + // 地址候选与 REV 规则辅助 + // Address candidate & REV rule helpers + // ======================== + + // 地址段 <-> 允许 REV 字符列表 + // Address range to allowed REV character list + struct m5ioe1_rev_rule_t { + uint8_t start; + uint8_t end; + const char* valid_revs; // 允许的 REV 字符(字符串,每个字符为一个允许值) + // Allowed REV characters (string, each char is one allowed value) + }; + static const m5ioe1_rev_rule_t kRevRules[]; + + // REV 校验三态结果 + // REV check tri-state result + enum class RevCheckResult { + NO_RESPONSE, // 地址无响应 + // No response at address + MATCHED, // REV 在允许列表中 + // REV is in allowed list + MISMATCH, // 地址有响应但 REV 不在允许列表中 + // Address responded but REV not in allowed list + NO_RULE, // 该地址段无规则(允许通过) + // No rule for this address range (allow through) + }; + + const m5ioe1_rev_rule_t* _findRevRule(uint8_t addr) const; + bool _isRevisionAllowed(uint8_t addr, uint8_t rev) const; + + // 低噪声探测 REV 寄存器(不打 error 日志) + // Low-noise probe for REV register (no error log) + RevCheckResult _checkAddressRevision(uint8_t addr) const; + + // 构建候选地址列表,返回实际候选数 + // Build candidate address list, returns actual candidate count + static constexpr uint8_t kMaxCandidates = 12; + static uint8_t _buildAddressCandidates(uint8_t requested, uint8_t* candidates); + + // 针对当前驱动上下文,将候选地址应用到 I2C 句柄(必要时重建 device handle) + // Apply candidate address to I2C handle for current driver context (rebuild device handle if needed) + m5ioe1_err_t _applyAddressToDriver(uint8_t candidate, uint32_t freqHz); + + // 对单个候选地址执行完整初始化尝试(100K → 800ms → 100K → 400K),不做 REV 检查 + // Perform full init attempt for one candidate (100K→800ms→100K→400K), no REV check + m5ioe1_err_t _tryInitAtAddress(uint8_t candidate); + + // 初始化成功后的收尾流程(快照、setI2cConfig、中断模式) + // Post-init finalization (snapshot, setI2cConfig, interrupt mode) + m5ioe1_err_t _finishInit(m5ioe1_int_mode_t intMode); + + // 地址解析与初始化核心流程(必须在 bus 初始化完成后调用) + // Address resolution and init core flow (must be called after bus init is complete) + m5ioe1_err_t _resolveAndInit(uint8_t requestedAddr, m5ioe1_int_mode_t intMode); + + bool _initDevice(); + void _handleInterrupt(); + + // 自动唤醒检查 + // Auto wake check + void _checkAutoWake(); + + // I2C 休眠与轮询任务联动 + // I2C sleep and polling task linkage + void _updatePollingForI2cSleep(uint8_t sleepTime); + + // I2C 频率验证 + // I2C frequency validation + bool _isValidI2cFrequency(uint32_t speed); + + // 中断互斥对检查 + // Interrupt mutex pairs check + static bool _pinsConflict(uint8_t a, uint8_t b); + bool _hasConflictingInterrupt(uint8_t pin); + + // 配置验证辅助函数 + // Configuration validation helpers + bool _getInterruptMutexPin(uint8_t pin, uint8_t* mutexPin); + bool _isNeopixelPin(uint8_t pin); + bool _hasActiveInterrupt(uint8_t pin); + bool _hasActiveAdc(uint8_t pin); + bool _hasActivePwm(uint8_t pin); + bool _hasI2cSleepEnabled(); + bool _isLedEnabled(); + + // I2C 配置快照 + // I2C config snapshot + void _clearI2cConfig(); + bool _snapshotI2cConfig(); + +#ifdef ARDUINO + // Arduino 专用 + // Arduino specific + bool _setupPollingArduino(); + void _cleanupPollingArduino(); + static void _pollTaskArduino(void* arg); + bool _setupHardwareInterruptArduino(); + void _cleanupHardwareInterruptArduino(); + static void _intrTaskArduino(void* arg); + static void IRAM_ATTR _arduinoIsrHandler(void* arg); +#else + // ESP-IDF 专用 + // ESP-IDF specific + static void _pollTaskFunc(void* arg); + static void IRAM_ATTR _isrHandler(void* arg); + bool _setupHardwareInterrupt(); + bool _setupPolling(); + void _cleanupPolling(); + void _cleanupHardwareInterrupt(); +#endif +}; + +#endif // _M5IOE1_H_ diff --git a/components/m5ioe1/src/M5IOE1_i2c_compat.h b/components/m5ioe1/src/M5IOE1_i2c_compat.h new file mode 100644 index 0000000..420d262 --- /dev/null +++ b/components/m5ioe1/src/M5IOE1_i2c_compat.h @@ -0,0 +1,788 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#ifndef __M5IOE1_I2C_COMPAT_H__ +#define __M5IOE1_I2C_COMPAT_H__ + +#include +#include +#include + +// ============================ +// I2C 延迟配置 (可调整以解决总线冲突) +// I2C Delay Configuration (adjustable to resolve bus conflicts) +// 适用于所有平台和驱动 (Arduino Wire / M5Unified / i2c_master / i2c_bus / Legacy) +// Applies to all platforms and drivers (Arduino Wire / M5Unified / i2c_master / i2c_bus / Legacy) +// ============================ +#ifndef M5IOE1_I2C_WRITE_DELAY_US +#define M5IOE1_I2C_WRITE_DELAY_US 500 // 写操作后延迟(微秒) / Delay after write (microseconds) +#endif + +#ifndef M5IOE1_I2C_READ_DELAY_US +#define M5IOE1_I2C_READ_DELAY_US 500 // 读操作前/后延迟(微秒) / Delay around read (microseconds) +#endif + +// 微秒级延迟宏 (平台无关) / Microsecond delay macro (platform-agnostic) +#ifdef ARDUINO +#define M5IOE1_I2C_DELAY_US(us) delayMicroseconds(us) +#else +#define M5IOE1_I2C_DELAY_US(us) esp_rom_delay_us(us) +#endif + +#ifdef ARDUINO + +#include "Wire.h" + +// ============================ +// Arduino I2C 功能 +// Arduino I2C Functions +// ============================ + +#ifndef M5IOE1_I2C_ARDUINO_READ_BYTE +static inline bool M5IOE1_I2C_ARDUINO_READ_BYTE(TwoWire *wire, uint8_t addr, uint8_t reg, uint8_t *data) +{ + wire->beginTransmission(addr); + wire->write(reg); + if (wire->endTransmission(false) != 0) { + return false; + } + delayMicroseconds(M5IOE1_I2C_READ_DELAY_US); // 增加延迟避免 I2C 总线冲突 / extra delay to avoid bus conflicts + if (wire->requestFrom(addr, (uint8_t)1) != 1) { + return false; + } + *data = wire->read(); + return true; +} +#endif + +#ifndef M5IOE1_I2C_ARDUINO_READ_BYTES +static inline bool M5IOE1_I2C_ARDUINO_READ_BYTES(TwoWire *wire, uint8_t addr, uint8_t start_reg, size_t len, + uint8_t *data) +{ + wire->beginTransmission(addr); + wire->write(start_reg); + if (wire->endTransmission(false) != 0) { + return false; + } + delayMicroseconds(M5IOE1_I2C_READ_DELAY_US); // 增加延迟避免 I2C 总线冲突 + if (wire->requestFrom(addr, (uint8_t)len) != len) { + return false; + } + for (size_t i = 0; i < len; i++) { + data[i] = wire->read(); + } + return true; +} +#endif + +#ifndef M5IOE1_I2C_ARDUINO_READ_REG16 +static inline bool M5IOE1_I2C_ARDUINO_READ_REG16(TwoWire *wire, uint8_t addr, uint8_t reg, uint16_t *data) +{ + uint8_t buf[2]; + if (!M5IOE1_I2C_ARDUINO_READ_BYTES(wire, addr, reg, 2, buf)) { + return false; + } + // 小端模式:低字节在前 + // Little-endian: low byte first + *data = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8); + return true; +} +#endif + +#ifndef M5IOE1_I2C_ARDUINO_WRITE_BYTE +static inline bool M5IOE1_I2C_ARDUINO_WRITE_BYTE(TwoWire *wire, uint8_t addr, uint8_t reg, uint8_t data) +{ + wire->beginTransmission(addr); + wire->write(reg); + wire->write(data); + if (wire->endTransmission() != 0) { + return false; + } + delayMicroseconds(M5IOE1_I2C_WRITE_DELAY_US); // 增加延迟给设备处理时间 + return true; +} +#endif + +#ifndef M5IOE1_I2C_ARDUINO_WRITE_BYTES +static inline bool M5IOE1_I2C_ARDUINO_WRITE_BYTES(TwoWire *wire, uint8_t addr, uint8_t start_reg, size_t len, + const uint8_t *data) +{ + wire->beginTransmission(addr); + wire->write(start_reg); + for (size_t i = 0; i < len; i++) { + wire->write(data[i]); + } + if (wire->endTransmission() != 0) { + return false; + } + delayMicroseconds(M5IOE1_I2C_WRITE_DELAY_US); // 增加延迟给设备处理时间 + return true; +} +#endif + +#ifndef M5IOE1_I2C_ARDUINO_WRITE_REG16 +static inline bool M5IOE1_I2C_ARDUINO_WRITE_REG16(TwoWire *wire, uint8_t addr, uint8_t reg, uint16_t data) +{ + uint8_t buf[2]; + // 小端模式:低字节在前 + // Little-endian: low byte first + buf[0] = (uint8_t)(data & 0xFF); + buf[1] = (uint8_t)((data >> 8) & 0xFF); + return M5IOE1_I2C_ARDUINO_WRITE_BYTES(wire, addr, reg, 2, buf); +} +#endif + +// 唤醒信号发送 / Wake signal send +// IOE1 使用 SDA 引脚下降沿触发唤醒中断 +// IOE1 uses SDA pin falling edge to trigger wake interrupt +#ifndef M5IOE1_I2C_ARDUINO_SEND_WAKE +static inline void M5IOE1_I2C_ARDUINO_SEND_WAKE(TwoWire *wire, uint8_t addr) +{ + // Send START then STOP to avoid leaving the bus in a busy state + wire->beginTransmission(addr); + wire->endTransmission(); // Send START with STOP +} +#endif + +// ============================ +// M5Unified I2C_Class 检测 (Arduino) +// M5Unified I2C_Class Detection (Arduino) +// ============================ +#if defined(__cplusplus) && __has_include() +#define M5IOE1_HAS_M5UNIFIED_I2C 1 +#include +#else +#define M5IOE1_HAS_M5UNIFIED_I2C 0 +#endif + +// ============================ +// M5Unified I2C_Class 通信封装 (Arduino) +// M5Unified I2C_Class Communication Wrappers (Arduino) +// (C++ only — m5::I2C_Class is a C++ class) +// ============================ +#if M5IOE1_HAS_M5UNIFIED_I2C + +#ifndef M5IOE1_M5UNIFIED_READ_BYTE +static inline bool M5IOE1_M5UNIFIED_READ_BYTE(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, uint8_t *data, + uint32_t freq) +{ + bool ok = i2c->readRegister(addr, reg, data, 1, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_READ_BYTES +static inline bool M5IOE1_M5UNIFIED_READ_BYTES(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, size_t len, uint8_t *data, + uint32_t freq) +{ + bool ok = i2c->readRegister(addr, reg, data, len, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_READ_REG16 +static inline bool M5IOE1_M5UNIFIED_READ_REG16(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, uint16_t *data, + uint32_t freq) +{ + uint8_t buf[2]; + if (!i2c->readRegister(addr, reg, buf, 2, freq)) return false; + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + // 小端模式:低字节在前 / Little-endian: low byte first + *data = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8); + return true; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_WRITE_BYTE +static inline bool M5IOE1_M5UNIFIED_WRITE_BYTE(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, uint8_t data, + uint32_t freq) +{ + bool ok = i2c->writeRegister8(addr, reg, data, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_WRITE_BYTES +static inline bool M5IOE1_M5UNIFIED_WRITE_BYTES(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, size_t len, + const uint8_t *data, uint32_t freq) +{ + bool ok = i2c->writeRegister(addr, reg, data, len, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_WRITE_REG16 +static inline bool M5IOE1_M5UNIFIED_WRITE_REG16(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, uint16_t data, + uint32_t freq) +{ + uint8_t buf[2]; + // 小端模式:低字节在前 / Little-endian: low byte first + buf[0] = (uint8_t)(data & 0xFF); + buf[1] = (uint8_t)((data >> 8) & 0xFF); + bool ok = i2c->writeRegister(addr, reg, buf, 2, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_SEND_WAKE +// 产生 I2C START 信号以唤醒处于睡眠的 IOE1 +// Generate I2C START signal to wake IOE1 from sleep +static inline bool M5IOE1_M5UNIFIED_SEND_WAKE(m5::I2C_Class *i2c, uint8_t addr, uint32_t freq) +{ + i2c->start(addr, false, freq); + i2c->stop(); + return true; +} +#endif + +#endif // M5IOE1_HAS_M5UNIFIED_I2C + +#else // ESP-IDF + +#include +#include +#include // esp_rom_delay_us + +// ============================ +// I2C 驱动检测 +// I2C Driver Detection +// ============================ + +// 检测 i2c_bus 是否可用 +// Detect if i2c_bus is available +// +// ESP-IDF < 5.3.0 +// Without BACKWARD_CONFIG: i2c_bus not supported; use legacy driver/i2c.h API. +// With BACKWARD_CONFIG: i2c_bus.h falls back to driver/i2c.h internally, safe to use. +// +// ESP-IDF >= 5.3.0 +// [Highest priority] M5Unified is present: +// -> i2c_bus mode is not safe because M5Unified owns its I2C_Class bus. +// Fix: use ioe1.begin(&M5.In_I2C, addr, freq) instead. +// M5GFX alone: +// -> header presence is not enough to prove an I2C runtime conflict. Projects that use +// M5GFX only for display/SPI can still use espressif/i2c_bus for external devices. +// Do not call lgfx::i2c::init() on the same port while i2c_bus owns it. +// With BACKWARD_CONFIG (no M5GFX/M5Unified): i2c_bus.h uses driver/i2c.h internally, safe. +// Without BACKWARD_CONFIG (no M5GFX/M5Unified): +// _DRIVER_I2C_H_ defined: i2c_config_t typedef conflict risk, disabled. +// _DRIVER_I2C_H_ not defined: no conflict risk, enable i2c_bus. +#if __has_include() +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) +#if defined(CONFIG_I2C_BUS_BACKWARD_CONFIG) +#define M5IOE1_HAS_I2C_BUS 1 // IDF < 5.3.0 + BACKWARD_CONFIG:可用 / available +#else +#define M5IOE1_HAS_I2C_BUS 0 // IDF < 5.3.0:默认 Legacy API / legacy API by default +#endif +#else +// IDF >= 5.3.0 +// +// Runtime conflict (highest priority): +// M5Unified owns an I2C_Class instance. espressif__i2c_bus conflicts with it on both code paths: +// BACKWARD_CONFIG=y -> i2c_bus.c calls i2c_driver_install() (legacy) -> runtime abort() +// BACKWARD_CONFIG=n -> i2c_bus_v2.c calls i2c_new_master_bus() on the same port -> undefined behaviour +// Therefore, i2c_bus mode MUST be disabled whenever M5Unified is in the project. +// +// M5GFX alone is intentionally allowed here. The presence of M5GFX.h does not mean lgfx::i2c::init() +// is used, and SPI/display-only M5GFX projects can share the project with espressif__i2c_bus safely. +#if __has_include() +#if defined(_I2C_BUS_H_) +#error \ + "[M5IOE1] i2c_bus cannot be used together with M5Unified. " \ + "M5Unified owns its I2C_Class bus. " \ + "BACKWARD_CONFIG=y: i2c_bus.c calls i2c_driver_install() (legacy driver) -> runtime abort() in check_i2c_driver_conflict(). " \ + "BACKWARD_CONFIG=n: i2c_bus_v2.c calls i2c_new_master_bus() on the same port -> undefined behaviour. " \ + "Fix: use ioe1.begin(&M5.In_I2C, addr, freq) and set I2C_USE_MODE=0." +#endif +#define M5IOE1_HAS_I2C_BUS 0 +#elif defined(CONFIG_I2C_BUS_BACKWARD_CONFIG) +#define M5IOE1_HAS_I2C_BUS 1 // BACKWARD_CONFIG, no M5Unified: compatible +#elif defined(_DRIVER_I2C_H_) +#define M5IOE1_HAS_I2C_BUS 0 // driver/i2c.h already included -> i2c_config_t typedef conflict risk, disabled +#else +#define M5IOE1_HAS_I2C_BUS 1 // no conflict risk +#endif +#endif +#else +#define M5IOE1_HAS_I2C_BUS 0 +#endif + +// 选择 I2C 驱动头文件 +// Select I2C driver header +// +// ESP-IDF < 5.3.0:使用传统 Legacy I2C API(driver/i2c.h),与 i2c_bus 无关 +// ESP-IDF < 5.3.0: use legacy I2C API (driver/i2c.h), independent of i2c_bus. +// +// ESP-IDF >= 5.3.0:优先使用新版 i2c_master 驱动 +// ESP-IDF >= 5.3.0: prefer the new i2c_master driver. +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) +#include +#elif __has_include() +#include +#else +#include +#endif + +// i2c_master 原生驱动可用性检测 +// i2c_master native driver availability detection +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) && __has_include() +#define M5IOE1_HAS_I2C_MASTER 1 +#else +#define M5IOE1_HAS_I2C_MASTER 0 +#endif + +// ============================ +// M5Unified I2C_Class 检测 +// M5Unified I2C_Class Detection +// ============================ +// +#if M5IOE1_HAS_I2C_BUS +#include +#else +// i2c_bus 桩类型 +// i2c_bus stub types +typedef void *i2c_bus_handle_t; +typedef void *i2c_bus_device_handle_t; +#endif + +// 仅 C++ 环境下检测 M5Unified 的 I2C_Class 是否可用。 +// Detects whether M5Unified's I2C_Class is available (C++ only). +// 注意:i2c_bus.h 必须先于 I2C_Class.hpp 包含,避免 i2c_config_t typedef 冲突。 +// Note: i2c_bus.h must be included before I2C_Class.hpp to avoid i2c_config_t typedef conflict. +#if defined(__cplusplus) && __has_include() +#define M5IOE1_HAS_M5UNIFIED_I2C 1 +#include +#else +#define M5IOE1_HAS_M5UNIFIED_I2C 0 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================ +// I2C 驱动类型选择 +// I2C Driver Type Selection +// ============================ +typedef enum { + M5IOE1_I2C_DRIVER_NONE = 0, // 未初始化 + // Not initialized + M5IOE1_I2C_DRIVER_SELF_CREATED, // 使用 i2c_port_t 自创建(IDF >= 5.3.0 用 i2c_master,否则用 Legacy) + // Self-created using i2c_port_t (i2c_master on IDF >= 5.3.0, Legacy otherwise) + M5IOE1_I2C_DRIVER_MASTER, // ESP-IDF 原生 i2c_master 驱动(仅 IDF >= 5.3.0) + // ESP-IDF native i2c_master driver (IDF >= 5.3.0 only) +#if M5IOE1_HAS_I2C_BUS + M5IOE1_I2C_DRIVER_BUS, // esp-idf-lib i2c_bus 组件 + // esp-idf-lib i2c_bus component +#endif +#if !M5IOE1_HAS_I2C_MASTER && !M5IOE1_HAS_I2C_BUS + M5IOE1_I2C_DRIVER_LEGACY, // 传统 driver/i2c.h API(IDF < 5.3.0 且无 i2c_bus) + // Legacy driver/i2c.h API (IDF < 5.3.0 without i2c_bus) +#endif +#if M5IOE1_HAS_M5UNIFIED_I2C + M5IOE1_I2C_DRIVER_M5UNIFIED, // 借用 M5Unified I2C_Class 通信(不负责驱动安装/释放) + // Borrow M5Unified I2C_Class for communication (no driver lifecycle) +#endif +} m5ioe1_i2c_driver_t; + +// ============================ +// ESP-IDF I2C 函数 (i2c_bus) +// ESP-IDF I2C Functions (i2c_bus) +// ============================ + +#if M5IOE1_HAS_I2C_BUS + +#ifndef M5IOE1_I2C_BUS_READ_BYTE +static inline esp_err_t M5IOE1_I2C_BUS_READ_BYTE(i2c_bus_device_handle_t dev, uint8_t reg, uint8_t *data) +{ + esp_err_t ret = i2c_bus_read_byte(dev, reg, data); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_BUS_READ_BYTES +static inline esp_err_t M5IOE1_I2C_BUS_READ_BYTES(i2c_bus_device_handle_t dev, uint8_t start_reg, size_t len, + uint8_t *data) +{ + esp_err_t ret = i2c_bus_read_bytes(dev, start_reg, len, data); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_BUS_READ_REG16 +static inline esp_err_t M5IOE1_I2C_BUS_READ_REG16(i2c_bus_device_handle_t dev, uint8_t reg, uint16_t *data) +{ + uint8_t buf[2]; + esp_err_t ret = i2c_bus_read_bytes(dev, reg, 2, buf); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + if (ret == ESP_OK) { + // 小端模式:低字节在前 + // Little-endian: low byte first + *data = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8); + } + return ret; +} +#endif + +#ifndef M5IOE1_I2C_BUS_WRITE_BYTE +static inline esp_err_t M5IOE1_I2C_BUS_WRITE_BYTE(i2c_bus_device_handle_t dev, uint8_t reg, uint8_t data) +{ + esp_err_t ret = i2c_bus_write_byte(dev, reg, data); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_BUS_WRITE_BYTES +static inline esp_err_t M5IOE1_I2C_BUS_WRITE_BYTES(i2c_bus_device_handle_t dev, uint8_t start_reg, size_t len, + const uint8_t *data) +{ + esp_err_t ret = i2c_bus_write_bytes(dev, start_reg, len, (uint8_t *)data); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_BUS_WRITE_REG16 +static inline esp_err_t M5IOE1_I2C_BUS_WRITE_REG16(i2c_bus_device_handle_t dev, uint8_t reg, uint16_t data) +{ + uint8_t buf[2]; + // 小端模式:低字节在前 + // Little-endian: low byte first + buf[0] = (uint8_t)(data & 0xFF); + buf[1] = (uint8_t)((data >> 8) & 0xFF); + esp_err_t ret = i2c_bus_write_bytes(dev, reg, 2, buf); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +// 唤醒信号发送 (i2c_bus) / Wake signal send (i2c_bus) +#ifndef M5IOE1_I2C_BUS_SEND_WAKE +static inline esp_err_t M5IOE1_I2C_BUS_SEND_WAKE(i2c_bus_device_handle_t dev, uint8_t reg) +{ + // Read any register to generate I2C start signal for wake + uint8_t dummy; + return i2c_bus_read_byte(dev, reg, &dummy); +} +#endif + +#else // !M5IOE1_HAS_I2C_BUS + +// ============================ +// ESP-IDF I2C 函数 (Legacy - driver/i2c.h,仅 IDF < 5.3.0 且无 i2c_bus) +// ESP-IDF I2C Functions (Legacy - driver/i2c.h, IDF < 5.3.0 without i2c_bus only) +// 使用 IDF 4.3+ 引入的简单 API:i2c_master_write_to_device / i2c_master_write_read_device +// Uses simple API introduced in IDF 4.3+: i2c_master_write_to_device / i2c_master_write_read_device +// ============================ + +#if !M5IOE1_HAS_I2C_MASTER + +#include +#include + +// 传统 I2C 超时,单位毫秒 / Legacy I2C timeout in milliseconds +#ifndef M5IOE1_I2C_LEGACY_TIMEOUT_MS +#define M5IOE1_I2C_LEGACY_TIMEOUT_MS 100 +#endif + +#ifndef M5IOE1_I2C_LEGACY_READ_BYTE +static inline esp_err_t M5IOE1_I2C_LEGACY_READ_BYTE(i2c_port_t port, uint8_t addr, uint8_t reg, uint8_t *data) +{ + esp_err_t ret = + i2c_master_write_read_device(port, addr, ®, 1, data, 1, pdMS_TO_TICKS(M5IOE1_I2C_LEGACY_TIMEOUT_MS)); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_LEGACY_READ_BYTES +static inline esp_err_t M5IOE1_I2C_LEGACY_READ_BYTES(i2c_port_t port, uint8_t addr, uint8_t start_reg, size_t len, + uint8_t *data) +{ + esp_err_t ret = + i2c_master_write_read_device(port, addr, &start_reg, 1, data, len, pdMS_TO_TICKS(M5IOE1_I2C_LEGACY_TIMEOUT_MS)); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_LEGACY_READ_REG16 +static inline esp_err_t M5IOE1_I2C_LEGACY_READ_REG16(i2c_port_t port, uint8_t addr, uint8_t reg, uint16_t *data) +{ + uint8_t buf[2]; + esp_err_t ret = + i2c_master_write_read_device(port, addr, ®, 1, buf, 2, pdMS_TO_TICKS(M5IOE1_I2C_LEGACY_TIMEOUT_MS)); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + if (ret == ESP_OK) { + // 小端模式:低字节在前 + // Little-endian: low byte first + *data = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8); + } + return ret; +} +#endif + +#ifndef M5IOE1_I2C_LEGACY_WRITE_BYTE +static inline esp_err_t M5IOE1_I2C_LEGACY_WRITE_BYTE(i2c_port_t port, uint8_t addr, uint8_t reg, uint8_t data) +{ + uint8_t buf[2] = {reg, data}; + esp_err_t ret = i2c_master_write_to_device(port, addr, buf, 2, pdMS_TO_TICKS(M5IOE1_I2C_LEGACY_TIMEOUT_MS)); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_LEGACY_WRITE_BYTES +static inline esp_err_t M5IOE1_I2C_LEGACY_WRITE_BYTES(i2c_port_t port, uint8_t addr, uint8_t start_reg, size_t len, + const uint8_t *data) +{ + // 需要在数据前添加寄存器地址 + // Need to prepend register address before data + uint8_t *buf = (uint8_t *)malloc(len + 1); + if (buf == NULL) return ESP_ERR_NO_MEM; + buf[0] = start_reg; + memcpy(buf + 1, data, len); + esp_err_t ret = i2c_master_write_to_device(port, addr, buf, len + 1, pdMS_TO_TICKS(M5IOE1_I2C_LEGACY_TIMEOUT_MS)); + free(buf); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_LEGACY_WRITE_REG16 +static inline esp_err_t M5IOE1_I2C_LEGACY_WRITE_REG16(i2c_port_t port, uint8_t addr, uint8_t reg, uint16_t data) +{ + uint8_t buf[3]; + buf[0] = reg; + // 小端模式:低字节在前 + // Little-endian: low byte first + buf[1] = (uint8_t)(data & 0xFF); + buf[2] = (uint8_t)((data >> 8) & 0xFF); + esp_err_t ret = i2c_master_write_to_device(port, addr, buf, 3, pdMS_TO_TICKS(M5IOE1_I2C_LEGACY_TIMEOUT_MS)); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +// 唤醒信号发送 (Legacy) / Wake signal send (Legacy) +// 通过读取一个字节来产生 I2C START 信号 +// Generates I2C START signal by reading one byte +#ifndef M5IOE1_I2C_LEGACY_SEND_WAKE +static inline esp_err_t M5IOE1_I2C_LEGACY_SEND_WAKE(i2c_port_t port, uint8_t addr) +{ + uint8_t dummy = 0; + uint8_t reg = 0x00; + // 忽略错误:唤醒时设备可能处于睡眠状态,ACK 可能超时 + // Ignore error: device may be asleep during wake, ACK may timeout + i2c_master_write_read_device(port, addr, ®, 1, &dummy, 1, pdMS_TO_TICKS(10)); + return ESP_OK; +} +#endif + +#endif // !M5IOE1_HAS_I2C_MASTER + +#endif // M5IOE1_HAS_I2C_BUS + +// ============================ +// ESP-IDF I2C 函数 (i2c_master - 原生驱动) +// ESP-IDF I2C Functions (i2c_master - native driver) +// 仅 ESP-IDF >= 5.3.0 支持(driver/i2c_master.h 引入于 5.1,API 稳定于 5.3) +// Available on ESP-IDF >= 5.3.0 only (driver/i2c_master.h introduced in 5.1, stable in 5.3) +// ============================ + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + +#ifndef M5IOE1_I2C_MASTER_READ_BYTE +static inline esp_err_t M5IOE1_I2C_MASTER_READ_BYTE(i2c_master_dev_handle_t dev, uint8_t reg, uint8_t *data) +{ + esp_err_t ret = i2c_master_transmit_receive(dev, ®, 1, data, 1, -1); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_MASTER_READ_BYTES +static inline esp_err_t M5IOE1_I2C_MASTER_READ_BYTES(i2c_master_dev_handle_t dev, uint8_t start_reg, size_t len, + uint8_t *data) +{ + esp_err_t ret = i2c_master_transmit_receive(dev, &start_reg, 1, data, len, -1); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_MASTER_READ_REG16 +static inline esp_err_t M5IOE1_I2C_MASTER_READ_REG16(i2c_master_dev_handle_t dev, uint8_t reg, uint16_t *data) +{ + uint8_t buf[2]; + esp_err_t ret = i2c_master_transmit_receive(dev, ®, 1, buf, 2, -1); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + if (ret == ESP_OK) { + // 小端模式:低字节在前 + // Little-endian: low byte first + *data = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8); + } + return ret; +} +#endif + +#ifndef M5IOE1_I2C_MASTER_WRITE_BYTE +static inline esp_err_t M5IOE1_I2C_MASTER_WRITE_BYTE(i2c_master_dev_handle_t dev, uint8_t reg, uint8_t data) +{ + uint8_t buf[2] = {reg, data}; + esp_err_t ret = i2c_master_transmit(dev, buf, 2, -1); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_MASTER_WRITE_BYTES +static inline esp_err_t M5IOE1_I2C_MASTER_WRITE_BYTES(i2c_master_dev_handle_t dev, uint8_t start_reg, size_t len, + const uint8_t *data) +{ + // 需要在数据前添加寄存器地址 + // Need to prepend register address + uint8_t *buf = (uint8_t *)malloc(len + 1); + if (buf == NULL) return ESP_ERR_NO_MEM; + buf[0] = start_reg; + memcpy(buf + 1, data, len); + esp_err_t ret = i2c_master_transmit(dev, buf, len + 1, -1); + free(buf); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +#ifndef M5IOE1_I2C_MASTER_WRITE_REG16 +static inline esp_err_t M5IOE1_I2C_MASTER_WRITE_REG16(i2c_master_dev_handle_t dev, uint8_t reg, uint16_t data) +{ + uint8_t buf[3]; + buf[0] = reg; + // 小端模式:低字节在前 + // Little-endian: low byte first + buf[1] = (uint8_t)(data & 0xFF); + buf[2] = (uint8_t)((data >> 8) & 0xFF); + esp_err_t ret = i2c_master_transmit(dev, buf, 3, -1); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ret; +} +#endif + +// 唤醒信号发送 (i2c_master) / Wake signal send (i2c_master) +#ifndef M5IOE1_I2C_MASTER_SEND_WAKE +static inline esp_err_t M5IOE1_I2C_MASTER_SEND_WAKE(i2c_master_bus_handle_t bus, uint8_t addr) +{ + // Use i2c_master_probe to generate START signal for wake + return i2c_master_probe(bus, addr, 10); +} +#endif + +#endif // ESP_IDF_VERSION >= 5.3.0 + +#ifdef __cplusplus +} +#endif + +// ============================ +// M5Unified I2C_Class 通信封装 +// M5Unified I2C_Class Communication Wrappers +// (C++ only — m5::I2C_Class is a C++ class) +// ============================ +#if M5IOE1_HAS_M5UNIFIED_I2C +#ifdef __cplusplus + +#ifndef M5IOE1_M5UNIFIED_READ_BYTE +static inline bool M5IOE1_M5UNIFIED_READ_BYTE(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, uint8_t *data, + uint32_t freq) +{ + bool ok = i2c->readRegister(addr, reg, data, 1, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_READ_BYTES +static inline bool M5IOE1_M5UNIFIED_READ_BYTES(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, size_t len, uint8_t *data, + uint32_t freq) +{ + bool ok = i2c->readRegister(addr, reg, data, len, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_READ_REG16 +static inline bool M5IOE1_M5UNIFIED_READ_REG16(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, uint16_t *data, + uint32_t freq) +{ + uint8_t buf[2]; + if (!i2c->readRegister(addr, reg, buf, 2, freq)) return false; + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_READ_DELAY_US); + // 小端模式:低字节在前 / Little-endian: low byte first + *data = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8); + return true; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_WRITE_BYTE +static inline bool M5IOE1_M5UNIFIED_WRITE_BYTE(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, uint8_t data, + uint32_t freq) +{ + bool ok = i2c->writeRegister8(addr, reg, data, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_WRITE_BYTES +static inline bool M5IOE1_M5UNIFIED_WRITE_BYTES(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, size_t len, + const uint8_t *data, uint32_t freq) +{ + bool ok = i2c->writeRegister(addr, reg, data, len, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_WRITE_REG16 +static inline bool M5IOE1_M5UNIFIED_WRITE_REG16(m5::I2C_Class *i2c, uint8_t addr, uint8_t reg, uint16_t data, + uint32_t freq) +{ + uint8_t buf[2]; + // 小端模式:低字节在前 / Little-endian: low byte first + buf[0] = (uint8_t)(data & 0xFF); + buf[1] = (uint8_t)((data >> 8) & 0xFF); + bool ok = i2c->writeRegister(addr, reg, buf, 2, freq); + M5IOE1_I2C_DELAY_US(M5IOE1_I2C_WRITE_DELAY_US); + return ok; +} +#endif + +#ifndef M5IOE1_M5UNIFIED_SEND_WAKE +// 产生 I2C START 信号以唤醒处于睡眠的 IOE1 +// Generate I2C START signal to wake IOE1 from sleep +static inline bool M5IOE1_M5UNIFIED_SEND_WAKE(m5::I2C_Class *i2c, uint8_t addr, uint32_t freq) +{ + i2c->start(addr, false, freq); + i2c->stop(); + return true; +} +#endif + +#endif // __cplusplus +#endif // M5IOE1_HAS_M5UNIFIED_I2C + +#endif // ARDUINO + +#endif // __M5IOE1_I2C_COMPAT_H__ diff --git a/components/mybot_platform/boards/README.md b/components/mybot_platform/boards/README.md index c74f59d..d728221 100644 --- a/components/mybot_platform/boards/README.md +++ b/components/mybot_platform/boards/README.md @@ -4,8 +4,9 @@ These documents describe the board profiles implemented by `agora-esp32-devkit`. drivers and build constraints are defined by the checked-in `board.cmake`, `board_config.h` and `sdkconfig.defaults` files. Use ESP-IDF v5.5.2 and select a board with `MYBOT_BOARD`. -以下说明对应本工程已实现的 13 个板型。硬件配置、输入行为、构建约束和验证状态以当前 -板级代码及本工程文档为准。不同尺寸、芯片或硬件版本的板卡不能仅凭外观共用固件。 +以下说明对应本工程已实现的 56 个板型。硬件配置、输入行为和构建约束以当前板级代码为准; +验证状态统一见[验证矩阵](../../../docs/VALIDATION.md)。不同尺寸、芯片或硬件版本的板卡 +不能仅凭外观共用固件。 | Board profile | 板卡说明 | 显示 | 视频 | | --- | --- | --- | --- | @@ -15,6 +16,9 @@ drivers and build constraints are defined by the checked-in `board.cmake`, `boar | `esp32-s3-touch-amoled-1.75` | [README](esp32-s3-touch-amoled-1.75/README.md) | CO5300 AMOLED 触摸屏 | 无 | | `esp32-s3-touch-amoled-1.75c` | [README](esp32-s3-touch-amoled-1.75c/README.md) | CO5300 AMOLED 触摸屏 | 无 | | `esp32-s3-touch-amoled-2.16` | [README](esp32-s3-touch-amoled-2.16/README.md) | 480×480 CO5300 AMOLED 触摸屏 | 无 | +| `esp32-s3-touch-amoled-1.8-v2` | [README](esp32-s3-touch-amoled-1.8-v2/README.md) | 368×448 CO5300 AMOLED 触摸屏 | 无 | +| `esp32-s3-touch-lcd-1.85` | [README](esp32-s3-touch-lcd-1.85/README.md) | 360×360 ST77916 触摸屏 | 无 | +| `genjutech-s3-1.54tft` | [README](genjutech-s3-1.54tft/README.md) | 240×240 ST7789 | 无 | | `m5stack-core-s3` | [README](m5stack-core-s3/README.md) | ILI9342 触摸屏 | GC0308,上传 1 fps | | `m5stack-stick-s3` | [README](m5stack-stick-s3/README.md) | 135×240 ST7789P3 | 无 | | `respeaker-flex-xvf3800-circular4-xiao` | [README](respeaker-flex-xvf3800-circular4-xiao/README.md) | 无屏幕 | 无 | @@ -23,7 +27,59 @@ drivers and build constraints are defined by the checked-in `board.cmake`, `boar | `zhengchen-1.54tft-ml307` | [README](zhengchen-1.54tft-ml307/README.md) | ST7789 | 无 | | `zhengchen-1.54tft-wifi` | [README](zhengchen-1.54tft-wifi/README.md) | ST7789 | 无 | -## 构建 +### Additional ESP32-S3 profiles / 新增板型 + +These profiles reuse the shared board lifecycle in `boards/common/`. Their `board_config.h` and +`board.cmake` files are the authoritative pin and capability records. None has completed +real-device validation. + +这些板型复用 `boards/common/` 的共享生命周期;引脚与能力以各自的 `board_config.h` 和 +`board.cmake` 为准。以下板型均未完成真机验证。 + +| Board profile | Display / 显示 | Audio / 音频 | Input / 输入 | Reduced scope / 当前子集 | +| --- | --- | --- | --- | --- | +| [xingzhi-metal-1.54-wifi](xingzhi-metal-1.54-wifi/README.md) | 240×240 ST7789 | ES8311 | PCA9557 touch + Boot | Touch, PCA9557 power/reset and PA require device validation. | +| [xingzhi-cube-0.85tft-wifi](xingzhi-cube-0.85tft-wifi/README.md) | 128×128 NV3023 | Raw I2S | Boot | No touch or codec control. | +| [xingzhi-cube-0.96oled-wifi](xingzhi-cube-0.96oled-wifi/README.md) | Headless | Raw I2S | Boot + two volume keys | SSD1306 monochrome UI is not implemented. | +| [xingzhi-abs-2.0](xingzhi-abs-2.0/README.md) | 240×240 ST7789 | ES8311 | Boot + two volume keys | Wi-Fi only; SD, vibration and cellular are not implemented. | +| [m5stack-stopwatch](m5stack-stopwatch/README.md) | 466×466 round CO5300 | ES8311 | CST820B-compatible CST816S path + two buttons | M5PM1/M5IOE1 power sequencing and round safe area are implemented; battery reporting, motor, sleep and shutdown are not. | +| [esp32-s3-touch-amoled-1.8](esp32-s3-touch-amoled-1.8/README.md) | 368×448 SH8601 QSPI | ES8311 | FT5x06 + Boot | Device timing and touch mapping require validation. | +| [esp32-s3-touch-amoled-1.32](esp32-s3-touch-amoled-1.32/README.md) | 466×466 SH8601 QSPI | ES8311 | Boot | Touchless profile. | +| [esp32-s3-touch-amoled-1.43c](esp32-s3-touch-amoled-1.43c/README.md) | 466×466 SH8601 QSPI | ES8311 + ES7210 | Boot | Touchless profile. | +| [esp32-s3-touch-amoled-2.06](esp32-s3-touch-amoled-2.06/README.md) | 410×502 SH8601 QSPI | ES8311 + ES7210 | FT5x06 + Boot | 22-pixel X gap; device timing requires validation. | +| [atoms3r-echo-base](atoms3r-echo-base/README.md) | 128×128 GC9A01-class SPI panel | ES8311 | Boot | Echo-base IO/power and LP5562 backlight integration are not implemented. | +| [atoms3r-echo-pyramid](atoms3r-echo-pyramid/README.md) | 128×128 GC9A01-class SPI panel | ES8311 + ES7210 | Boot | Echo-pyramid IO/power and LP5562 backlight integration are not implemented. | +| [sp-esp32-s3-1.28-box](sp-esp32-s3-1.28-box/README.md) | 240×240 round GC9A01 | ES8311 | CST816S-compatible touch + Boot | Battery/charging reporting and panel vendor tuning require validation. | +| [esp-box](esp-box/README.md) | 320×240 ILI9341 | ES8311 + ES7210 | Boot | Touch, board PMIC options and power sequencing require validation. | +| [esp-box-3](esp-box-3/README.md) | 320×240 ILI9341 | ES8311 + ES7210 | Boot | Touch, board PMIC options and power sequencing require validation. | +| [esp-box-lite](esp-box-lite/README.md) | 320×240 ILI9341 | ES8311 + ES7210 profile | Boot | ADC buttons, codec assignment and power sequencing require validation. | +| [esp32s3-korvo2-v3](esp32s3-korvo2-v3/README.md) | 320×240 ILI9341 | ES8311 + ES7210 | Boot | TCA9554 reset/CS is implemented; ADC keys, camera, battery and sleep are not. | +| [lichuang-dev](lichuang-dev/README.md) | 320×240 ST7789 | ES8311 + ES7210 | FT5x06 + Boot | PCA9557 display/PA control is implemented; camera and battery are not. | +| [quandong-s3-dev](quandong-s3-dev/README.md) | 320×240 ILI9341 | ES8311 | Boot | PCA9557 setup and active-low PA are implemented; hardware mapping requires validation. | +| [rymcu-bigsmart](rymcu-bigsmart/README.md) | 320×240 ST7789 | ES8311 + ES7210 | GT911 + Boot | PCA9557 display/PA control is implemented; camera and battery are not. | +| [df-k10](df-k10/README.md) | 240×320 ILI9341 | ES8311 + ES7210 | Boot | TCA9555 keys, camera, LED ring and power paths are not implemented. | +| [mixgo-nova](mixgo-nova/README.md) | 128×160 ST7789 | ES8374 | Boot | RGB LED and hardware validation are pending. | +| [taiji-pi-s3](taiji-pi-s3/README.md) | 360×360 round ST77916 QSPI | ES8311 playback + PDM capture | CST816S + Boot | PDM capture and mute/PA lifecycle are implemented; board power sequencing requires validation. | +| [freenove-esp32s3-display-2.8-lcd](freenove-esp32s3-display-2.8-lcd/README.md) | 320×240 ILI9341 | ES8311 | Boot | Camera, LED and battery paths are not implemented. | +| [esp32-s3-touch-lcd-1.54](esp32-s3-touch-lcd-1.54/README.md) | 240×240 ST7789 | ES8311 + ES7210 | CST816S + three buttons | Battery management, sleep and shutdown are not implemented. | +| [esp32-s3-touch-lcd-1.83](esp32-s3-touch-lcd-1.83/README.md) | 240×284 ST7789 | ES8311 + ES7210 | CST816S + Boot | AXP2101, PWR41, battery reporting and sleep require device validation. | +| [esp32-s3-lcd-0.85](esp32-s3-lcd-0.85/README.md) | 128×128 ST7789 | ES8311 + ES7210 | three buttons | Touchless profile; battery timing and 8 MB layout require validation. | +| [esp32-s3-touch-lcd-1.85b](esp32-s3-touch-lcd-1.85b/README.md) | 360×360 ST77916 QSPI | ES8311 + ES7210 | CST816S-compatible touch + Boot | Vendor panel revisions and touch mapping require validation. | +| [esp32-s3-touch-lcd-1.85c](esp32-s3-touch-lcd-1.85c/README.md) | 360×360 ST77916 QSPI | ES8311 + ES7210 V2 path | CST816S-compatible touch + Boot; TCA9554 reset | V1 raw-I2S path and touch mapping require validation. | +| [sp-esp32-s3-1.54-muma](sp-esp32-s3-1.54-muma/README.md) | 240×240 ST7789 | ES8311 | CST816S + Boot | Vendor panel tuning, battery and power hold require validation. | +| [atk-dnesp32s3-box0](atk-dnesp32s3-box0/README.md) | 240×240 ST7789 | ES8311 | three buttons | Sleep, shutdown, charging and battery reporting are not implemented. | +| [nulllab-ai-vox-v3](nulllab-ai-vox-v3/README.md) | 240×240 ST7789 | ES8311 | Boot + two volume keys | Wi-Fi subset only; ML307 networking is not implemented. | +| [aipi-lite](aipi-lite/README.md) | 128×128 ST7789 | ES8311 | Boot | Power key, sleep and battery reporting are not implemented. | +| [magiclick-2p4](magiclick-2p4/README.md) | 128×128 NV3023 | ES8311 | three buttons | Battery, LED, sleep and shutdown are not implemented. | +| [movecall-cuican-esp32s3](movecall-cuican-esp32s3/README.md) | 240×240 GC9A01 | ES8311 | Boot | 8 MB layout requires validation; LED, battery, sleep and shutdown are not implemented. | +| [movecall-moji-esp32s3](movecall-moji-esp32s3/README.md) | 240×240 GC9A01 | ES8311 | Boot | LED, battery, sleep and shutdown are not implemented. | +| [doit-s3-aibox](doit-s3-aibox/README.md) | Headless | PDM capture + I2S playback | three buttons | PDM timing, LED, sleep and shutdown require validation. | +| [bread-compact-wifi](bread-compact-wifi/README.md) | Headless | separate raw I2S | Boot + two volume keys | OLED UI, GPIO47 key, lamp and LED are not implemented. | +| [hu-087](hu-087/README.md) | Headless | separate raw I2S | one button | OLED UI is not implemented; long press is provision instead of volume. | +| [kevin-sp-v4-dev](kevin-sp-v4-dev/README.md) | 240×280 ST7789 | ES8311 | Boot | Camera, LED, battery, sleep and shutdown are not implemented. | +| [du-chatx](du-chatx/README.md) | 128×160 ST7789 | separate raw I2S | Boot | GPIO1 power hold is implemented; battery, charging, LED, sleep and shutdown are not. | + +## Build, flash and validation / 构建、烧录与验证 在工程根目录激活 ESP-IDF v5.5.2,并指定板型: diff --git a/components/mybot_platform/boards/aipi-lite/README.md b/components/mybot_platform/boards/aipi-lite/README.md new file mode 100644 index 0000000..abee946 --- /dev/null +++ b/components/mybot_platform/boards/aipi-lite/README.md @@ -0,0 +1,14 @@ +# AIPI Lite + +板型 ID 为 `aipi-lite`。 + +- 显示:128×128 ST7789,SPI3 mode 0,40 MHz,共享 LVGL 紧凑布局。 +- 音频:ES8311,I2C1 GPIO5/4,GPIO9 speaker PA。 +- 输入:GPIO42 Boot;GPIO1 电源键未接入。 +- 电源:GPIO10 电源保持。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO3 LEDC,默认 60%。 + +电源键动作、电池检测、休眠、关机、面板和音频仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/aipi-lite/board.c b/components/mybot_platform/boards/aipi-lite/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/aipi-lite/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/aipi-lite/board.cmake b/components/mybot_platform/boards/aipi-lite/board.cmake new file mode 100644 index 0000000..6f155e4 --- /dev/null +++ b/components/mybot_platform/boards/aipi-lite/board.cmake @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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/aipi-lite/board_config.h b/components/mybot_platform/boards/aipi-lite/board_config.h new file mode 100644 index 0000000..c18e8f9 --- /dev/null +++ b/components/mybot_platform/boards/aipi-lite/board_config.h @@ -0,0 +1,61 @@ +/* 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 "aipi-lite" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_14 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_12 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_13 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_11 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_5 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_4 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_POWER_GPIO GPIO_NUM_10 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_9 + +#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_42 +#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 SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_17 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_16 +#define MYBOT_DISPLAY_DC GPIO_NUM_7 +#define MYBOT_DISPLAY_CS GPIO_NUM_15 +#define MYBOT_DISPLAY_RESET GPIO_NUM_18 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_3 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 128 +#define MYBOT_DISPLAY_HEIGHT 128 +#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 false +#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/aipi-lite/sdkconfig.defaults b/components/mybot_platform/boards/aipi-lite/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/aipi-lite/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/atk-dnesp32s3-box0/README.md b/components/mybot_platform/boards/atk-dnesp32s3-box0/README.md new file mode 100644 index 0000000..8e355af --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box0/README.md @@ -0,0 +1,13 @@ +# ATK DNESP32S3 Box0 + +板型 ID 为 `atk-dnesp32s3-box0`。 + +- 显示:240×240 ST7789,SPI2,80 MHz,共享 LVGL UI。 +- 音频:ES8311,I2C0 GPIO11/12,GPIO21 speaker PA。 +- 输入:GPIO4 主按键、GPIO0 音量加、GPIO3 音量减。 +- 电源:GPIO2 系统保持、GPIO14 codec 电源、GPIO42 背光。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +电源时序、面板、音频、按键和电池检测仍需真机验证;休眠、深度睡眠和关机未实现。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/atk-dnesp32s3-box0/board.c b/components/mybot_platform/boards/atk-dnesp32s3-box0/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box0/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-box0/board.cmake b/components/mybot_platform/boards/atk-dnesp32s3-box0/board.cmake new file mode 100644 index 0000000..6f155e4 --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box0/board.cmake @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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-box0/board_config.h b/components/mybot_platform/boards/atk-dnesp32s3-box0/board_config.h new file mode 100644 index 0000000..2e3e5ad --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box0/board_config.h @@ -0,0 +1,62 @@ +/* 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-box0" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_13 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_9 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_11 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_12 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_POWER_GPIO GPIO_NUM_2 +#define MYBOT_GENERIC_AUX_POWER_GPIO GPIO_NUM_14 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_21 + +#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_4 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_3 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_40 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_39 +#define MYBOT_DISPLAY_DC GPIO_NUM_38 +#define MYBOT_DISPLAY_CS GPIO_NUM_41 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_42 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (80 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/atk-dnesp32s3-box0/sdkconfig.defaults b/components/mybot_platform/boards/atk-dnesp32s3-box0/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/atk-dnesp32s3-box0/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/atoms3r-echo-base/README.md b/components/mybot_platform/boards/atoms3r-echo-base/README.md new file mode 100644 index 0000000..7bb9460 --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-base/README.md @@ -0,0 +1,14 @@ +# AtomS3R Echo Base + +板型 ID 为 `atoms3r-echo-base`。 + +- 显示:128×128 GC9A01 类 SPI 面板,Y 偏移 32,共享 LVGL 紧凑布局。 +- 音频:ES8311,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO41 Boot;无触摸和音量键。 +- 存储:8 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +当前只接入显示和 ES8311 主路径。Echo Base 的外围 IO/电源管理、LP5562 背光和底座连接检测 +未实现;显示、音频、按键极性和电源仍需真机验证。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/atoms3r-echo-base/board.c b/components/mybot_platform/boards/atoms3r-echo-base/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-base/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/atoms3r-echo-base/board.cmake b/components/mybot_platform/boards/atoms3r-echo-base/board.cmake new file mode 100644 index 0000000..70d4a4d --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-base/board.cmake @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/8m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_8MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_OCT + CONFIG_SPIRAM_SPEED_80M + CONFIG_CODEC_ES8311_SUPPORT +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_gc9a01) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/atoms3r-echo-base/board_config.h b/components/mybot_platform/boards/atoms3r-echo-base/board_config.h new file mode 100644 index 0000000..1bbbfe1 --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-base/board_config.h @@ -0,0 +1,61 @@ +/* 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 "atoms3r-echo-base" +#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_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_8 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_7 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_5 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_38 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_39 +#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_41 +#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_PANEL_CONTROLLER 1 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_21 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_15 +#define MYBOT_DISPLAY_DC GPIO_NUM_42 +#define MYBOT_DISPLAY_CS GPIO_NUM_14 +#define MYBOT_DISPLAY_RESET GPIO_NUM_48 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 1 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 128 +#define MYBOT_DISPLAY_HEIGHT 128 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 32 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR false +#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/atoms3r-echo-base/sdkconfig.defaults b/components/mybot_platform/boards/atoms3r-echo-base/sdkconfig.defaults new file mode 100644 index 0000000..63ebd39 --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-base/sdkconfig.defaults @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=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/atoms3r-echo-pyramid/README.md b/components/mybot_platform/boards/atoms3r-echo-pyramid/README.md new file mode 100644 index 0000000..68ef69d --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-pyramid/README.md @@ -0,0 +1,14 @@ +# AtomS3R Echo Pyramid + +板型 ID 为 `atoms3r-echo-pyramid`。 + +- 显示:128×128 GC9A01 类 SPI 面板,共享 LVGL 紧凑布局。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO41 Boot;无触摸和音量键。 +- 存储:8 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +当前只接入显示和双 codec 主路径。Echo Pyramid 的外围 IO/电源管理、LP5562 背光和底座连接 +检测未实现;显示、双 codec 路由、按键极性和电源仍需真机验证。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/atoms3r-echo-pyramid/board.c b/components/mybot_platform/boards/atoms3r-echo-pyramid/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-pyramid/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/atoms3r-echo-pyramid/board.cmake b/components/mybot_platform/boards/atoms3r-echo-pyramid/board.cmake new file mode 100644 index 0000000..5a5af0c --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-pyramid/board.cmake @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/8m.csv") +set(MYBOT_GENERIC_AUDIO "dual") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_8MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_OCT + CONFIG_SPIRAM_SPEED_80M + CONFIG_CODEC_ES8311_SUPPORT + CONFIG_CODEC_ES7210_SUPPORT +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_gc9a01) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/atoms3r-echo-pyramid/board_config.h b/components/mybot_platform/boards/atoms3r-echo-pyramid/board_config.h new file mode 100644 index 0000000..d110d0d --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-pyramid/board_config.h @@ -0,0 +1,60 @@ +/* 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 "atoms3r-echo-pyramid" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#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_6 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_8 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_7 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_38 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_39 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#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_41 +#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_PANEL_CONTROLLER 1 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_21 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_15 +#define MYBOT_DISPLAY_DC GPIO_NUM_42 +#define MYBOT_DISPLAY_CS GPIO_NUM_14 +#define MYBOT_DISPLAY_RESET GPIO_NUM_48 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 1 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 128 +#define MYBOT_DISPLAY_HEIGHT 128 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#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/atoms3r-echo-pyramid/sdkconfig.defaults b/components/mybot_platform/boards/atoms3r-echo-pyramid/sdkconfig.defaults new file mode 100644 index 0000000..ece839c --- /dev/null +++ b/components/mybot_platform/boards/atoms3r-echo-pyramid/sdkconfig.defaults @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=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/boards.cmake b/components/mybot_platform/boards/boards.cmake index 75d7b46..9c84764 100644 --- a/components/mybot_platform/boards/boards.cmake +++ b/components/mybot_platform/boards/boards.cmake @@ -1,17 +1,60 @@ set(MYBOT_SUPPORTED_BOARDS "atom-echos3r" "bread-compact-wifi-lcd" + "bread-compact-wifi" "esp-vocat" "esp32-s3-touch-amoled-1.75" "esp32-s3-touch-amoled-1.75c" "esp32-s3-touch-amoled-2.16" + "esp32-s3-touch-amoled-1.8-v2" + "esp32-s3-touch-amoled-1.8" + "esp32-s3-touch-amoled-1.32" + "esp32-s3-touch-amoled-1.43c" + "esp32-s3-touch-amoled-2.06" + "esp32-s3-touch-lcd-1.85" + "esp32-s3-touch-lcd-1.85b" + "esp32-s3-touch-lcd-1.85c" + "esp32-s3-touch-lcd-1.54" + "esp32-s3-touch-lcd-1.83" + "esp32-s3-lcd-0.85" + "esp-box" + "esp-box-3" + "esp-box-lite" + "esp32s3-korvo2-v3" + "genjutech-s3-1.54tft" + "atoms3r-echo-base" + "atoms3r-echo-pyramid" + "aipi-lite" + "atk-dnesp32s3-box0" + "df-k10" + "doit-s3-aibox" + "freenove-esp32s3-display-2.8-lcd" + "hu-087" + "lichuang-dev" + "magiclick-2p4" "m5stack-core-s3" "m5stack-stick-s3" + "m5stack-stopwatch" + "mixgo-nova" + "movecall-cuican-esp32s3" + "movecall-moji-esp32s3" + "nulllab-ai-vox-v3" + "quandong-s3-dev" "respeaker-flex-xvf3800-circular4-xiao" + "rymcu-bigsmart" "sensecap-watcher" + "sp-esp32-s3-1.28-box" + "sp-esp32-s3-1.54-muma" + "taiji-pi-s3" + "xingzhi-abs-2.0" + "xingzhi-cube-0.85tft-wifi" + "xingzhi-cube-0.96oled-wifi" "xingzhi-cube-1.54tft-wifi" + "xingzhi-metal-1.54-wifi" "zhengchen-1.54tft-ml307" "zhengchen-1.54tft-wifi" + "kevin-sp-v4-dev" + "du-chatx" ) set(_MYBOT_BOARDS_ROOT "${CMAKE_CURRENT_LIST_DIR}") @@ -20,6 +63,8 @@ function(mybot_resolve_board board) set(profile "${_MYBOT_BOARDS_ROOT}/atom-echos3r/board.cmake") elseif(board STREQUAL "bread-compact-wifi-lcd") set(profile "${_MYBOT_BOARDS_ROOT}/bread-compact-wifi-lcd/board.cmake") + elseif(board STREQUAL "bread-compact-wifi") + set(profile "${_MYBOT_BOARDS_ROOT}/bread-compact-wifi/board.cmake") elseif(board STREQUAL "esp-vocat") set(profile "${_MYBOT_BOARDS_ROOT}/esp-vocat/board.cmake") elseif(board STREQUAL "esp32-s3-touch-amoled-1.75") @@ -28,21 +73,105 @@ function(mybot_resolve_board board) set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-amoled-1.75c/board.cmake") elseif(board STREQUAL "esp32-s3-touch-amoled-2.16") set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-amoled-2.16/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-amoled-1.8-v2") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-amoled-1.8-v2/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-amoled-1.8") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-amoled-1.8/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-amoled-1.32") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-amoled-1.32/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-amoled-1.43c") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-amoled-1.43c/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-amoled-2.06") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-amoled-2.06/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-lcd-1.85") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-lcd-1.85/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-lcd-1.85b") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-lcd-1.85b/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-lcd-1.85c") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-lcd-1.85c/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-lcd-1.54") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-lcd-1.54/board.cmake") + elseif(board STREQUAL "esp32-s3-touch-lcd-1.83") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-touch-lcd-1.83/board.cmake") + elseif(board STREQUAL "esp32-s3-lcd-0.85") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32-s3-lcd-0.85/board.cmake") + elseif(board STREQUAL "esp-box") + set(profile "${_MYBOT_BOARDS_ROOT}/esp-box/board.cmake") + elseif(board STREQUAL "esp-box-3") + set(profile "${_MYBOT_BOARDS_ROOT}/esp-box-3/board.cmake") + elseif(board STREQUAL "esp-box-lite") + set(profile "${_MYBOT_BOARDS_ROOT}/esp-box-lite/board.cmake") + elseif(board STREQUAL "esp32s3-korvo2-v3") + set(profile "${_MYBOT_BOARDS_ROOT}/esp32s3-korvo2-v3/board.cmake") + elseif(board STREQUAL "genjutech-s3-1.54tft") + set(profile "${_MYBOT_BOARDS_ROOT}/genjutech-s3-1.54tft/board.cmake") + elseif(board STREQUAL "atoms3r-echo-base") + set(profile "${_MYBOT_BOARDS_ROOT}/atoms3r-echo-base/board.cmake") + elseif(board STREQUAL "atoms3r-echo-pyramid") + set(profile "${_MYBOT_BOARDS_ROOT}/atoms3r-echo-pyramid/board.cmake") + elseif(board STREQUAL "aipi-lite") + set(profile "${_MYBOT_BOARDS_ROOT}/aipi-lite/board.cmake") + elseif(board STREQUAL "atk-dnesp32s3-box0") + set(profile "${_MYBOT_BOARDS_ROOT}/atk-dnesp32s3-box0/board.cmake") + elseif(board STREQUAL "df-k10") + set(profile "${_MYBOT_BOARDS_ROOT}/df-k10/board.cmake") + elseif(board STREQUAL "doit-s3-aibox") + set(profile "${_MYBOT_BOARDS_ROOT}/doit-s3-aibox/board.cmake") + elseif(board STREQUAL "hu-087") + set(profile "${_MYBOT_BOARDS_ROOT}/hu-087/board.cmake") + elseif(board STREQUAL "lichuang-dev") + set(profile "${_MYBOT_BOARDS_ROOT}/lichuang-dev/board.cmake") + elseif(board STREQUAL "magiclick-2p4") + set(profile "${_MYBOT_BOARDS_ROOT}/magiclick-2p4/board.cmake") elseif(board STREQUAL "m5stack-core-s3") set(profile "${_MYBOT_BOARDS_ROOT}/m5stack-core-s3/board.cmake") elseif(board STREQUAL "m5stack-stick-s3") set(profile "${_MYBOT_BOARDS_ROOT}/m5stack-stick-s3/board.cmake") + elseif(board STREQUAL "m5stack-stopwatch") + set(profile "${_MYBOT_BOARDS_ROOT}/m5stack-stopwatch/board.cmake") + elseif(board STREQUAL "mixgo-nova") + set(profile "${_MYBOT_BOARDS_ROOT}/mixgo-nova/board.cmake") + elseif(board STREQUAL "movecall-cuican-esp32s3") + set(profile "${_MYBOT_BOARDS_ROOT}/movecall-cuican-esp32s3/board.cmake") + elseif(board STREQUAL "movecall-moji-esp32s3") + set(profile "${_MYBOT_BOARDS_ROOT}/movecall-moji-esp32s3/board.cmake") + elseif(board STREQUAL "nulllab-ai-vox-v3") + set(profile "${_MYBOT_BOARDS_ROOT}/nulllab-ai-vox-v3/board.cmake") + elseif(board STREQUAL "quandong-s3-dev") + set(profile "${_MYBOT_BOARDS_ROOT}/quandong-s3-dev/board.cmake") elseif(board STREQUAL "respeaker-flex-xvf3800-circular4-xiao") set(profile "${_MYBOT_BOARDS_ROOT}/respeaker-flex-xvf3800-circular4-xiao/board.cmake") + elseif(board STREQUAL "rymcu-bigsmart") + set(profile "${_MYBOT_BOARDS_ROOT}/rymcu-bigsmart/board.cmake") elseif(board STREQUAL "sensecap-watcher") set(profile "${_MYBOT_BOARDS_ROOT}/sensecap-watcher/board.cmake") + elseif(board STREQUAL "sp-esp32-s3-1.28-box") + set(profile "${_MYBOT_BOARDS_ROOT}/sp-esp32-s3-1.28-box/board.cmake") + elseif(board STREQUAL "sp-esp32-s3-1.54-muma") + set(profile "${_MYBOT_BOARDS_ROOT}/sp-esp32-s3-1.54-muma/board.cmake") + elseif(board STREQUAL "taiji-pi-s3") + set(profile "${_MYBOT_BOARDS_ROOT}/taiji-pi-s3/board.cmake") + elseif(board STREQUAL "xingzhi-abs-2.0") + set(profile "${_MYBOT_BOARDS_ROOT}/xingzhi-abs-2.0/board.cmake") + elseif(board STREQUAL "xingzhi-cube-0.85tft-wifi") + set(profile "${_MYBOT_BOARDS_ROOT}/xingzhi-cube-0.85tft-wifi/board.cmake") + elseif(board STREQUAL "xingzhi-cube-0.96oled-wifi") + set(profile "${_MYBOT_BOARDS_ROOT}/xingzhi-cube-0.96oled-wifi/board.cmake") elseif(board STREQUAL "xingzhi-cube-1.54tft-wifi") set(profile "${_MYBOT_BOARDS_ROOT}/xingzhi-cube-1.54tft-wifi/board.cmake") + elseif(board STREQUAL "xingzhi-metal-1.54-wifi") + set(profile "${_MYBOT_BOARDS_ROOT}/xingzhi-metal-1.54-wifi/board.cmake") elseif(board STREQUAL "zhengchen-1.54tft-ml307") set(profile "${_MYBOT_BOARDS_ROOT}/zhengchen-1.54tft-ml307/board.cmake") elseif(board STREQUAL "zhengchen-1.54tft-wifi") set(profile "${_MYBOT_BOARDS_ROOT}/zhengchen-1.54tft-wifi/board.cmake") + elseif(board STREQUAL "freenove-esp32s3-display-2.8-lcd") + set(profile "${_MYBOT_BOARDS_ROOT}/freenove-esp32s3-display-2.8-lcd/board.cmake") + elseif(board STREQUAL "kevin-sp-v4-dev") + set(profile "${_MYBOT_BOARDS_ROOT}/kevin-sp-v4-dev/board.cmake") + elseif(board STREQUAL "du-chatx") + set(profile "${_MYBOT_BOARDS_ROOT}/du-chatx/board.cmake") else() message(FATAL_ERROR "Unsupported MYBOT_BOARD: ${board}") endif() diff --git a/components/mybot_platform/boards/bread-compact-wifi/README.md b/components/mybot_platform/boards/bread-compact-wifi/README.md new file mode 100644 index 0000000..7fce42b --- /dev/null +++ b/components/mybot_platform/boards/bread-compact-wifi/README.md @@ -0,0 +1,12 @@ +# Bread Compact Wi-Fi + +板型 ID 为 `bread-compact-wifi`。当前为 headless profile。 + +- 音频:独立麦克风/扬声器 I2S,16 kHz mono signed-16 PCM。 +- 输入:GPIO0 Boot、GPIO40 音量加、GPIO39 音量减。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +SSD1306/SH1106 单色屏 UI、GPIO47 触摸按键、GPIO18 灯和 GPIO48 LED 未接入。音频、 +按键和实际 Flash/PSRAM 仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/bread-compact-wifi/board.c b/components/mybot_platform/boards/bread-compact-wifi/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/bread-compact-wifi/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/bread-compact-wifi/board.cmake b/components/mybot_platform/boards/bread-compact-wifi/board.cmake new file mode 100644 index 0000000..14b6647 --- /dev/null +++ b/components/mybot_platform/boards/bread-compact-wifi/board.cmake @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "raw") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/bread-compact-wifi/board_config.h b/components/mybot_platform/boards/bread-compact-wifi/board_config.h new file mode 100644 index 0000000..c95ff45 --- /dev/null +++ b/components/mybot_platform/boards/bread-compact-wifi/board_config.h @@ -0,0 +1,34 @@ +/* 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 "bread-compact-wifi" +#define MYBOT_BOARD_HAS_DISPLAY 0 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 2 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_MIC_WS GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_15 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_7 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#define MYBOT_BOOT_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/bread-compact-wifi/sdkconfig.defaults b/components/mybot_platform/boards/bread-compact-wifi/sdkconfig.defaults new file mode 100644 index 0000000..214c8f7 --- /dev/null +++ b/components/mybot_platform/boards/bread-compact-wifi/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/common/generic_board.c b/components/mybot_platform/boards/common/generic_board.c new file mode 100644 index 0000000..e742463 --- /dev/null +++ b/components/mybot_platform/boards/common/generic_board.c @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "generic_board.h" +#include "generic_hardware.h" + +#include + +#include "announcement/embedded_ogg_prompt.h" +#include "board_config.h" +#include "esp_log.h" +#include "network/wifi_control.h" +#include "platform/board_actions.h" + +#include + +#define TAG "generic_board" + +#if MYBOT_GENERIC_AUDIO_KIND == 1 || MYBOT_GENERIC_AUDIO_KIND == 4 +const mybot_audio_capture_ops_t *mybot_es8311_audio_capture_ops(void); +const mybot_audio_playback_ops_t *mybot_es8311_audio_playback_ops(void); +const mybot_audio_volume_ops_t *mybot_es8311_audio_volume_ops(void); +#define CAPTURE_OPS mybot_es8311_audio_capture_ops +#define PLAYBACK_OPS mybot_es8311_audio_playback_ops +#define VOLUME_OPS mybot_es8311_audio_volume_ops +#elif MYBOT_GENERIC_AUDIO_KIND == 2 +const mybot_audio_capture_ops_t *mybot_esp32s3_audio_capture_ops(void); +const mybot_audio_playback_ops_t *mybot_esp32s3_audio_playback_ops(void); +const mybot_audio_volume_ops_t *mybot_esp32s3_audio_volume_ops(void); +#define CAPTURE_OPS mybot_esp32s3_audio_capture_ops +#define PLAYBACK_OPS mybot_esp32s3_audio_playback_ops +#define VOLUME_OPS mybot_esp32s3_audio_volume_ops +#elif MYBOT_GENERIC_AUDIO_KIND == 3 +const mybot_audio_capture_ops_t *mybot_amoled175_audio_capture_ops(void); +const mybot_audio_playback_ops_t *mybot_amoled175_audio_playback_ops(void); +const mybot_audio_volume_ops_t *mybot_amoled175_audio_volume_ops(void); +#define CAPTURE_OPS mybot_amoled175_audio_capture_ops +#define PLAYBACK_OPS mybot_amoled175_audio_playback_ops +#define VOLUME_OPS mybot_amoled175_audio_volume_ops +#else +#error "MYBOT_GENERIC_AUDIO_KIND must select an audio path" +#endif + +const mybot_announce_ops_t *mybot_esp32s3_announce_ops(void); +const mybot_https_ops_t *mybot_esp32s3_https_ops(void); +const mybot_kv_store_ops_t *mybot_esp32s3_kv_store_ops(void); +const mybot_wifi_ops_t *mybot_esp32s3_wifi_ops(void); + +#if MYBOT_GENERIC_TOUCH_KIND +const mybot_key_ops_t *mybot_cst816s_input_ops(void); +int mybot_cst816s_input_start(void); +int mybot_cst816s_input_stop(void); +#define KEY_OPS mybot_cst816s_input_ops +#else +const mybot_key_ops_t *mybot_esp32s3_button_ops(void); +int mybot_esp32s3_buttons_start(void); +#define KEY_OPS mybot_esp32s3_button_ops +#endif + +#if MYBOT_BOARD_HAS_DISPLAY +const mybot_lcd_ops_t *mybot_shared_lvgl_ops(void); +static const mybot_lcd_ops_t *s_lcd_ops; +static void *s_lcd_context; +#endif +static bool s_automatic_provisioning_started; +static bool s_prepared; +static bool s_cleanup_pending; + +static int board_show_screen(mybot_lcd_screen_t screen) { +#if MYBOT_BOARD_HAS_DISPLAY + if (s_lcd_ops && s_lcd_context) { + const mybot_lcd_content_t content = {.screen = screen}; + return s_lcd_ops->render(s_lcd_context, &content); + } +#else + (void)screen; +#endif + return 0; +} + +static void board_on_provisioning(const char *trigger) { + (void)board_show_screen(MYBOT_LCD_SCREEN_WIFI_PROVISIONING); + (void)mybot_embedded_ogg_play_wifi_provisioning(PLAYBACK_OPS(), VOLUME_OPS(), trigger); +} + +static void board_on_automatic_provisioning(void) { + s_automatic_provisioning_started = true; + board_on_provisioning("automatic"); +} + +static void board_on_input_provisioning(void) { + board_on_provisioning("input"); +} + +static int board_rollback_prepare(void) { + s_cleanup_pending = true; +#if MYBOT_GENERIC_TOUCH_KIND + if (mybot_cst816s_input_stop() < 0) { + return -1; + } +#endif +#if MYBOT_BOARD_HAS_DISPLAY + if (s_lcd_ops && s_lcd_context) { + s_lcd_ops->destroy(s_lcd_context); + s_lcd_context = NULL; + } + s_lcd_ops = NULL; +#endif + if (mybot_generic_hardware_deinit() < 0) { + return -1; + } + s_cleanup_pending = false; + return 0; +} + +static int board_prepare(void) { + if (s_prepared) { + return 0; + } + if (s_cleanup_pending && board_rollback_prepare() < 0) { + return -1; + } + if (mybot_generic_hardware_init() < 0) { + return -1; + } +#if MYBOT_BOARD_HAS_DISPLAY + s_lcd_ops = mybot_shared_lvgl_ops(); + if (!s_lcd_ops || s_lcd_ops->init(&s_lcd_context) < 0 || + board_show_screen(MYBOT_LCD_SCREEN_STARTING) < 0) { + (void)board_rollback_prepare(); + return -1; + } +#endif +#if MYBOT_GENERIC_TOUCH_KIND + if (mybot_cst816s_input_start() < 0) { + (void)board_rollback_prepare(); + return -1; + } +#else + if (mybot_esp32s3_buttons_start() < 0) { + (void)board_rollback_prepare(); + return -1; + } +#endif + s_prepared = true; + return 0; +} + +int mybot_board_handle_boot_long_press(void) { + mybot_board_request_wifi_provisioning(); + return 0; +} + +static int board_provision_wifi(void) { + return mybot_wifi_run_provisioning(board_on_input_provisioning); +} + +static int board_ensure_network(const char *device_id) { + s_automatic_provisioning_started = false; + const int result = mybot_wifi_ensure_network(device_id, board_on_automatic_provisioning); + if (s_automatic_provisioning_started) { + (void)mybot_board_wait_wifi_provisioning_request(0); + } + return result; +} + +static void board_shutdown_network(void) { + mybot_wifi_shutdown_network(); +} + +static int board_register_platform(void) { + const mybot_platform_descriptor_t descriptor = { + .wifi = mybot_esp32s3_wifi_ops(), + .kv_store = mybot_esp32s3_kv_store_ops(), + .key = KEY_OPS(), + .audio_capture = CAPTURE_OPS(), + .audio_playback = PLAYBACK_OPS(), + .audio_volume = VOLUME_OPS(), + .announce = mybot_esp32s3_announce_ops(), + .https = mybot_esp32s3_https_ops(), +#if MYBOT_BOARD_HAS_DISPLAY + .lcd = mybot_shared_lvgl_ops(), +#else + .lcd = NULL, +#endif + }; + return mybot_platform_register(&descriptor); +} + +static const mybot_board_t s_board = { + .id = MYBOT_BOARD_NAME, + .hw_model = MYBOT_BOARD_NAME, + .prepare = board_prepare, + .register_platform = board_register_platform, + .ensure_network = board_ensure_network, + .provision_wifi = board_provision_wifi, + .shutdown_network = board_shutdown_network, +}; + +const mybot_board_t *mybot_generic_board_get(void) { + return &s_board; +} diff --git a/components/mybot_platform/boards/common/generic_board.h b/components/mybot_platform/boards/common/generic_board.h new file mode 100644 index 0000000..8266de0 --- /dev/null +++ b/components/mybot_platform/boards/common/generic_board.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_GENERIC_BOARD_H_ +#define MYBOT_GENERIC_BOARD_H_ + +#include "mybot_platform/board.h" + +#ifdef __cplusplus +extern "C" { +#endif + +const mybot_board_t *mybot_generic_board_get(void); + +#ifdef __cplusplus +} +#endif + +#endif /* MYBOT_GENERIC_BOARD_H_ */ diff --git a/components/mybot_platform/boards/common/generic_hardware.c b/components/mybot_platform/boards/common/generic_hardware.c new file mode 100644 index 0000000..614cb3c --- /dev/null +++ b/components/mybot_platform/boards/common/generic_hardware.c @@ -0,0 +1,475 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "generic_hardware.h" + +#include "board_config.h" +#include "io_expander/pca9557.h" +#include "io_expander/tca9554.h" + +#include "driver/gpio.h" +#include "driver/ledc.h" +#include "esp_err.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define TAG "generic_board" +#define BACKLIGHT_LEDC_MODE LEDC_LOW_SPEED_MODE +#define BACKLIGHT_LEDC_TIMER LEDC_TIMER_0 +#define BACKLIGHT_LEDC_CHANNEL LEDC_CHANNEL_0 +#define BACKLIGHT_LEDC_RESOLUTION LEDC_TIMER_13_BIT +#define BACKLIGHT_LEDC_FREQUENCY_HZ 5000 +#define BACKLIGHT_MAX_DUTY ((1U << 13) - 1U) + +#if MYBOT_GENERIC_M5STACK_STOPWATCH +#include "m5stack_stopwatch_hardware.h" +#endif + +#ifndef MYBOT_GENERIC_AUDIO_KIND +#define MYBOT_GENERIC_AUDIO_KIND 0 +#endif + +#ifndef MYBOT_GENERIC_TOUCH_KIND +#define MYBOT_GENERIC_TOUCH_KIND 0 +#endif + +#ifndef MYBOT_GENERIC_PA_ACTIVE_LEVEL +#define MYBOT_GENERIC_PA_ACTIVE_LEVEL 1 +#endif + +#ifndef MYBOT_GENERIC_PA_GPIO +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC +#endif + +#ifndef MYBOT_GENERIC_POWER_ACTIVE_LEVEL +#define MYBOT_GENERIC_POWER_ACTIVE_LEVEL 1 +#endif + +#ifndef MYBOT_GENERIC_AUX_POWER_GPIO +#define MYBOT_GENERIC_AUX_POWER_GPIO GPIO_NUM_NC +#endif + +#ifndef MYBOT_GENERIC_AUX_POWER_ACTIVE_LEVEL +#define MYBOT_GENERIC_AUX_POWER_ACTIVE_LEVEL 1 +#endif + +typedef struct { + i2c_master_bus_handle_t audio_bus; + i2c_master_bus_handle_t touch_bus; + mybot_pca9557_t expander; + mybot_tca9554_t tca9554; + i2c_master_dev_handle_t axp2101; + bool expander_ready; + bool pa_ready; + bool pa_enabled; + bool power_ready; + bool aux_power_ready; + bool backlight_ready; + bool stopwatch_hardware_ready; + bool initialized; +} hardware_state_t; + +static hardware_state_t s_hardware; + +#ifdef MYBOT_GENERIC_AXP2101_ADDRESS +static esp_err_t initialize_axp2101(void) { + static const uint8_t registers[][2] = { + {0x22, 0x06}, {0x27, 0x10}, {0x80, 0x01}, {0x90, 0x00}, {0x91, 0x00}, + {0x82, 0x12}, {0x92, 0x1c}, {0x93, 0x1c}, {0x90, 0x03}, {0x64, 0x02}, + {0x61, 0x02}, {0x62, 0x0a}, {0x63, 0x01}, + }; + for (size_t i = 0; i < sizeof(registers) / sizeof(registers[0]); ++i) { + const uint8_t command[] = {registers[i][0], registers[i][1]}; + const esp_err_t result = + i2c_master_transmit(s_hardware.axp2101, command, sizeof(command), 100); + if (result != ESP_OK) { + return result; + } + } + return ESP_OK; +} +#endif + +static bool codec_needs_i2c(void) { + return MYBOT_GENERIC_AUDIO_KIND == 1 || MYBOT_GENERIC_AUDIO_KIND == 3 || + MYBOT_GENERIC_AUDIO_KIND == 4; +} + +static esp_err_t create_i2c_bus(int port, gpio_num_t sda, gpio_num_t scl, + i2c_master_bus_handle_t *out_bus) { + const i2c_master_bus_config_t config = { + .i2c_port = port, + .sda_io_num = sda, + .scl_io_num = scl, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, + }; + return i2c_new_master_bus(&config, out_bus); +} + +static esp_err_t configure_output_gpio(gpio_num_t gpio, int initial_level) { + if (gpio == GPIO_NUM_NC) { + return ESP_OK; + } + const gpio_config_t config = { + .pin_bit_mask = 1ULL << gpio, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .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(gpio, initial_level) : result; +} + +static esp_err_t initialize_backlight(void) { +#ifdef MYBOT_DISPLAY_BACKLIGHT_GPIO + if (MYBOT_DISPLAY_BACKLIGHT_GPIO == GPIO_NUM_NC) { + return ESP_OK; + } + const ledc_timer_config_t timer_config = { + .speed_mode = BACKLIGHT_LEDC_MODE, + .duty_resolution = BACKLIGHT_LEDC_RESOLUTION, + .timer_num = BACKLIGHT_LEDC_TIMER, + .freq_hz = BACKLIGHT_LEDC_FREQUENCY_HZ, + .clk_cfg = LEDC_AUTO_CLK, + }; + esp_err_t result = ledc_timer_config(&timer_config); + if (result != ESP_OK) { + return result; + } + const ledc_channel_config_t channel_config = { + .gpio_num = MYBOT_DISPLAY_BACKLIGHT_GPIO, + .speed_mode = BACKLIGHT_LEDC_MODE, + .channel = BACKLIGHT_LEDC_CHANNEL, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = BACKLIGHT_LEDC_TIMER, + .duty = MYBOT_DISPLAY_BACKLIGHT_INVERT ? BACKLIGHT_MAX_DUTY : 0, + .hpoint = 0, + .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD, + }; + result = ledc_channel_config(&channel_config); + if (result == ESP_OK) { + s_hardware.backlight_ready = true; + } + return result; +#else + return ESP_OK; +#endif +} + +static int release_hardware(void) { + int result = 0; + if (s_hardware.backlight_ready && + ledc_stop(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, 0) != ESP_OK) { + result = -1; + } + s_hardware.backlight_ready = false; + if (s_hardware.pa_ready) { + (void)mybot_generic_set_speaker_power(false); + } +#ifdef MYBOT_GENERIC_POWER_GPIO + if (s_hardware.power_ready && + gpio_set_level(MYBOT_GENERIC_POWER_GPIO, !MYBOT_GENERIC_POWER_ACTIVE_LEVEL) != ESP_OK) { + result = -1; + } +#endif + s_hardware.power_ready = false; + if (s_hardware.aux_power_ready && + gpio_set_level(MYBOT_GENERIC_AUX_POWER_GPIO, !MYBOT_GENERIC_AUX_POWER_ACTIVE_LEVEL) != + ESP_OK) { + result = -1; + } + s_hardware.aux_power_ready = false; +#if MYBOT_GENERIC_M5STACK_STOPWATCH + if (s_hardware.stopwatch_hardware_ready && mybot_m5stack_stopwatch_hardware_deinit() < 0) { + result = -1; + } + s_hardware.stopwatch_hardware_ready = false; +#endif +#ifdef MYBOT_GENERIC_AXP2101_ADDRESS + if (s_hardware.axp2101) { + if (i2c_master_bus_rm_device(s_hardware.axp2101) != ESP_OK) { + result = -1; + } else { + s_hardware.axp2101 = NULL; + } + } +#endif +#ifdef MYBOT_GENERIC_TCA9554_ADDRESS + if (s_hardware.tca9554.device && mybot_tca9554_deinit(&s_hardware.tca9554) < 0) { + result = -1; + } +#endif +#ifdef MYBOT_GENERIC_PCA9557_ADDRESS + if (s_hardware.expander_ready && mybot_pca9557_deinit(&s_hardware.expander) < 0) { + result = -1; + } +#endif + s_hardware.expander_ready = false; + if (s_hardware.touch_bus && i2c_del_master_bus(s_hardware.touch_bus) != ESP_OK) { + result = -1; + } else { + s_hardware.touch_bus = NULL; + } + if (s_hardware.audio_bus && i2c_del_master_bus(s_hardware.audio_bus) != ESP_OK) { + result = -1; + } else { + s_hardware.audio_bus = NULL; + } + s_hardware.initialized = false; + s_hardware.pa_ready = false; + s_hardware.pa_enabled = false; + return result; +} + +int mybot_generic_hardware_init(void) { + if (s_hardware.initialized) { + return 0; + } + if ((s_hardware.audio_bus || s_hardware.touch_bus || s_hardware.expander_ready || + s_hardware.tca9554.device || s_hardware.axp2101 || s_hardware.pa_ready || + s_hardware.power_ready || s_hardware.aux_power_ready || s_hardware.backlight_ready) && + release_hardware() < 0) { + return -1; + } + +#ifdef MYBOT_GENERIC_POWER_GPIO + esp_err_t result = + configure_output_gpio(MYBOT_GENERIC_POWER_GPIO, MYBOT_GENERIC_POWER_ACTIVE_LEVEL); + if (result != ESP_OK) { + return -1; + } + s_hardware.power_ready = MYBOT_GENERIC_POWER_GPIO != GPIO_NUM_NC; +#else + esp_err_t result = ESP_OK; +#endif + result = + configure_output_gpio(MYBOT_GENERIC_AUX_POWER_GPIO, MYBOT_GENERIC_AUX_POWER_ACTIVE_LEVEL); + if (result != ESP_OK) { + return -1; + } + s_hardware.aux_power_ready = MYBOT_GENERIC_AUX_POWER_GPIO != GPIO_NUM_NC; + + const bool touch_separate_bus = +#ifdef MYBOT_GENERIC_TOUCH_I2C_PORT + MYBOT_GENERIC_TOUCH_I2C_PORT != MYBOT_GENERIC_I2C_PORT; +#else + false; +#endif + if ((codec_needs_i2c() || (MYBOT_GENERIC_TOUCH_KIND != 0 && !touch_separate_bus)) && + !s_hardware.audio_bus) { + result = create_i2c_bus(MYBOT_GENERIC_I2C_PORT, MYBOT_GENERIC_I2C_SDA, + MYBOT_GENERIC_I2C_SCL, &s_hardware.audio_bus); + } +#if MYBOT_GENERIC_M5STACK_STOPWATCH + if (result == ESP_OK && s_hardware.audio_bus && + mybot_m5stack_stopwatch_hardware_init(s_hardware.audio_bus) == 0) { + s_hardware.stopwatch_hardware_ready = true; + } else if (result == ESP_OK) { + result = ESP_FAIL; + } +#endif +#ifdef MYBOT_GENERIC_TOUCH_I2C_PORT + if (MYBOT_GENERIC_TOUCH_KIND != 0 && touch_separate_bus && result == ESP_OK) { + result = create_i2c_bus(MYBOT_GENERIC_TOUCH_I2C_PORT, MYBOT_GENERIC_TOUCH_I2C_SDA, + MYBOT_GENERIC_TOUCH_I2C_SCL, &s_hardware.touch_bus); + } +#endif + +#ifdef MYBOT_GENERIC_PCA9557_ADDRESS + if (result == ESP_OK && s_hardware.audio_bus) { + result = mybot_pca9557_init(&s_hardware.expander, s_hardware.audio_bus, + MYBOT_GENERIC_PCA9557_ADDRESS, MYBOT_GENERIC_I2C_SPEED_HZ) == 0 + ? ESP_OK + : ESP_FAIL; + s_hardware.expander_ready = result == ESP_OK; + } + if (result == ESP_OK && s_hardware.expander_ready) { + result = + mybot_pca9557_configure_outputs(&s_hardware.expander, MYBOT_GENERIC_PCA9557_OUTPUT_MASK, + MYBOT_GENERIC_PCA9557_HIGH_MASK) == 0 + ? ESP_OK + : ESP_FAIL; + } +#ifdef MYBOT_GENERIC_PCA9557_LOW_MASK + if (result == ESP_OK && s_hardware.expander_ready) { + result = + mybot_pca9557_set_levels(&s_hardware.expander, MYBOT_GENERIC_PCA9557_LOW_MASK, 0) == 0 + ? ESP_OK + : ESP_FAIL; + } +#endif +#endif + +#ifdef MYBOT_GENERIC_TCA9554_ADDRESS + if (result == ESP_OK && s_hardware.audio_bus) { + result = mybot_tca9554_init(&s_hardware.tca9554, s_hardware.audio_bus, + MYBOT_GENERIC_TCA9554_ADDRESS, MYBOT_GENERIC_I2C_SPEED_HZ) == 0 + ? ESP_OK + : ESP_FAIL; + } + if (result == ESP_OK) { + result = mybot_tca9554_configure_outputs(&s_hardware.tca9554, + MYBOT_GENERIC_TCA9554_OUTPUT_MASK, 0) == 0 + ? ESP_OK + : ESP_FAIL; + } + if (result == ESP_OK) { + result = mybot_tca9554_set_levels(&s_hardware.tca9554, MYBOT_GENERIC_TCA9554_RESET_MASK, + MYBOT_GENERIC_TCA9554_RESET_MASK) == 0 + ? ESP_OK + : ESP_FAIL; + vTaskDelay(pdMS_TO_TICKS(120)); + } + if (result == ESP_OK) { + result = + mybot_tca9554_set_levels(&s_hardware.tca9554, MYBOT_GENERIC_TCA9554_RESET_MASK, 0) == 0 + ? ESP_OK + : ESP_FAIL; + vTaskDelay(pdMS_TO_TICKS(300)); + } + if (result == ESP_OK) { + result = mybot_tca9554_set_levels(&s_hardware.tca9554, MYBOT_GENERIC_TCA9554_RESET_MASK, + MYBOT_GENERIC_TCA9554_RESET_MASK) == 0 + ? ESP_OK + : ESP_FAIL; + } +#ifdef MYBOT_GENERIC_TCA9554_LOW_MASK + if (result == ESP_OK) { + result = + mybot_tca9554_set_levels(&s_hardware.tca9554, MYBOT_GENERIC_TCA9554_LOW_MASK, 0) == 0 + ? ESP_OK + : ESP_FAIL; + } +#endif +#endif + +#ifdef MYBOT_GENERIC_AXP2101_ADDRESS + if (result == ESP_OK && s_hardware.audio_bus) { + const i2c_device_config_t axp_config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = MYBOT_GENERIC_AXP2101_ADDRESS, + .scl_speed_hz = MYBOT_GENERIC_I2C_SPEED_HZ, + }; + result = i2c_master_bus_add_device(s_hardware.audio_bus, &axp_config, &s_hardware.axp2101); + } + if (result == ESP_OK) { + result = initialize_axp2101(); + } +#endif + + if (result == ESP_OK) { + result = configure_output_gpio(MYBOT_GENERIC_PA_GPIO, !MYBOT_GENERIC_PA_ACTIVE_LEVEL); + s_hardware.pa_ready = MYBOT_GENERIC_PA_GPIO != GPIO_NUM_NC; + } + if (result == ESP_OK) { + result = initialize_backlight(); + } + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=hardware action=initialize result=error code=%s", + esp_err_to_name(result)); + (void)release_hardware(); + return -1; + } + + s_hardware.initialized = true; + ESP_LOGI(TAG, "event=hardware action=initialize board=%s result=ok", MYBOT_BOARD_NAME); + return 0; +} + +int mybot_generic_hardware_deinit(void) { + return release_hardware(); +} + +i2c_master_bus_handle_t mybot_generic_audio_i2c_bus(void) { + return s_hardware.initialized ? s_hardware.audio_bus : NULL; +} + +i2c_master_bus_handle_t mybot_generic_touch_i2c_bus(void) { + if (!s_hardware.initialized) { + return NULL; + } + return s_hardware.touch_bus ? s_hardware.touch_bus : s_hardware.audio_bus; +} + +int mybot_generic_set_speaker_power(bool enabled) { + if (!s_hardware.initialized) { + return -1; + } + if (s_hardware.pa_enabled == enabled) { + return 0; + } +#ifdef MYBOT_GENERIC_PCA9557_PA_MASK + if (s_hardware.expander_ready) { + const uint8_t mask = MYBOT_GENERIC_PCA9557_PA_MASK; + if (mybot_pca9557_set_levels(&s_hardware.expander, mask, enabled ? mask : 0) < 0) { + return -1; + } + s_hardware.pa_enabled = enabled; + return 0; + } +#endif +#if MYBOT_GENERIC_M5STACK_STOPWATCH + if (s_hardware.stopwatch_hardware_ready) { + if (mybot_m5stack_stopwatch_set_speaker_pa(enabled) < 0) { + return -1; + } + s_hardware.pa_enabled = enabled; + return 0; + } +#endif + if (!s_hardware.pa_ready) { + s_hardware.pa_enabled = enabled; + return 0; + } + const int level = enabled ? MYBOT_GENERIC_PA_ACTIVE_LEVEL : !MYBOT_GENERIC_PA_ACTIVE_LEVEL; + if (gpio_set_level(MYBOT_GENERIC_PA_GPIO, level) != ESP_OK) { + return -1; + } + s_hardware.pa_enabled = enabled; + return 0; +} + +int mybot_board_set_display_backlight(unsigned int percent) { +#ifdef MYBOT_DISPLAY_BACKLIGHT_GPIO + if (MYBOT_DISPLAY_BACKLIGHT_GPIO == GPIO_NUM_NC) { + return 0; + } + if (!s_hardware.initialized || !s_hardware.backlight_ready || percent > 100) { + return -1; + } + uint32_t duty = (BACKLIGHT_MAX_DUTY * percent + 50U) / 100U; + if (MYBOT_DISPLAY_BACKLIGHT_INVERT) { + duty = BACKLIGHT_MAX_DUTY - duty; + } + if (ledc_set_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, duty) != ESP_OK || + ledc_update_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL) != ESP_OK) { + return -1; + } + return 0; +#else + (void)percent; + return 0; +#endif +} + +i2c_master_bus_handle_t mybot_es8311_board_i2c_bus_handle(void) { + return mybot_generic_audio_i2c_bus(); +} + +int mybot_es8311_board_set_speaker_power(bool enabled) { + return mybot_generic_set_speaker_power(enabled); +} + +i2c_master_bus_handle_t mybot_amoled175_i2c_bus_handle(void) { + return mybot_generic_audio_i2c_bus(); +} + +int mybot_amoled175_set_speaker_pa(bool enabled) { + return mybot_generic_set_speaker_power(enabled); +} + +i2c_master_bus_handle_t mybot_board_touch_i2c_bus_handle(void) { + return mybot_generic_touch_i2c_bus(); +} diff --git a/components/mybot_platform/boards/common/generic_hardware.h b/components/mybot_platform/boards/common/generic_hardware.h new file mode 100644 index 0000000..3f0d206 --- /dev/null +++ b/components/mybot_platform/boards/common/generic_hardware.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_GENERIC_HARDWARE_H_ +#define MYBOT_GENERIC_HARDWARE_H_ + +#include "driver/i2c_master.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int mybot_generic_hardware_init(void); +int mybot_generic_hardware_deinit(void); +i2c_master_bus_handle_t mybot_generic_audio_i2c_bus(void); +i2c_master_bus_handle_t mybot_generic_touch_i2c_bus(void); +int mybot_generic_set_speaker_power(bool enabled); +int mybot_board_set_display_backlight(unsigned int percent); + +#ifdef __cplusplus +} +#endif + +#endif /* MYBOT_GENERIC_HARDWARE_H_ */ diff --git a/components/mybot_platform/boards/common/generic_profile.cmake b/components/mybot_platform/boards/common/generic_profile.cmake new file mode 100644 index 0000000..47072dc --- /dev/null +++ b/components/mybot_platform/boards/common/generic_profile.cmake @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_BOARD_TARGET "esp32s3") +get_filename_component(MYBOT_GENERIC_BOARD_DIR "${MYBOT_BOARD_PROFILE}" DIRECTORY) +get_filename_component(MYBOT_GENERIC_COMMON_DIR + "${CMAKE_CURRENT_LIST_DIR}" ABSOLUTE) +set(MYBOT_BOARD_SDKCONFIG_DEFAULTS "${MYBOT_GENERIC_BOARD_DIR}/sdkconfig.defaults") +if(NOT MYBOT_GENERIC_DISPLAY) + set(MYBOT_BOARD_HEADLESS TRUE) +endif() +set(MYBOT_BOARD_INCLUDE_DIR + "${MYBOT_GENERIC_BOARD_DIR}" + "${MYBOT_GENERIC_COMMON_DIR}" + "${MYBOT_GENERIC_COMMON_DIR}/../esp32-s3-touch-amoled-1.75-common" +) +set(MYBOT_BOARD_PARTITION_TABLE "${MYBOT_GENERIC_PARTITION_TABLE}") +set(MYBOT_BOARD_REQUIRED_CONFIGS ${MYBOT_GENERIC_REQUIRED_CONFIGS}) +set(MYBOT_BOARD_FORBIDDEN_CONFIGS ${MYBOT_GENERIC_FORBIDDEN_CONFIGS}) + +get_filename_component(MYBOT_PLATFORM_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +set(MYBOT_BOARD_SOURCES + "${MYBOT_GENERIC_BOARD_DIR}/board.c" + "${MYBOT_GENERIC_COMMON_DIR}/generic_board.c" + "${MYBOT_GENERIC_COMMON_DIR}/generic_hardware.c" + ${MYBOT_GENERIC_EXTRA_SOURCES} +) + +if(MYBOT_GENERIC_DISPLAY) + include("${MYBOT_PLATFORM_ROOT}/src/drivers/display/display.cmake") + mybot_display_add_panel(MYBOT_BOARD_SOURCES "${MYBOT_GENERIC_DISPLAY}") + mybot_display_add_lvgl_sources(MYBOT_BOARD_SOURCES "shared_lvgl_adapter.cc") +endif() + +if(MYBOT_GENERIC_AUDIO STREQUAL "es8311") + list(APPEND MYBOT_BOARD_SOURCES "${MYBOT_PLATFORM_ROOT}/src/drivers/audio/es8311_codec_audio.c") +elseif(MYBOT_GENERIC_AUDIO STREQUAL "raw") + list(APPEND MYBOT_BOARD_SOURCES "${MYBOT_PLATFORM_ROOT}/src/drivers/audio/raw_i2s_audio.c") +elseif(MYBOT_GENERIC_AUDIO STREQUAL "dual") + list(APPEND MYBOT_BOARD_SOURCES + "${MYBOT_PLATFORM_ROOT}/src/drivers/audio/amoled175_codec_audio.c") +elseif(MYBOT_GENERIC_AUDIO STREQUAL "es8374") + list(APPEND MYBOT_BOARD_SOURCES + "${MYBOT_PLATFORM_ROOT}/src/drivers/audio/es8311_codec_audio.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/audio/es8374_codec.c") +endif() + +if(MYBOT_GENERIC_TOUCH) + list(APPEND MYBOT_BOARD_SOURCES + "${MYBOT_PLATFORM_ROOT}/src/drivers/input/cst816s_boot_input.c") + if(NOT MYBOT_GENERIC_TOUCH_DRIVER OR MYBOT_GENERIC_TOUCH_DRIVER STREQUAL "cst816s") + list(APPEND MYBOT_GENERIC_EXTRA_REQUIRES + esp_lcd_touch + esp_lcd_touch_cst816s) + elseif(MYBOT_GENERIC_TOUCH_DRIVER STREQUAL "ft5x06") + list(APPEND MYBOT_GENERIC_EXTRA_REQUIRES + esp_lcd_touch + esp_lcd_touch_ft5x06) + elseif(MYBOT_GENERIC_TOUCH_DRIVER STREQUAL "gt911") + list(APPEND MYBOT_GENERIC_EXTRA_REQUIRES + esp_lcd_touch + esp_lcd_touch_gt911) + else() + message(FATAL_ERROR "Unknown MYBOT_GENERIC_TOUCH_DRIVER") + endif() +else() + list(APPEND MYBOT_BOARD_SOURCES + "${MYBOT_PLATFORM_ROOT}/src/drivers/input/gpio_buttons.c") +endif() + +if(MYBOT_GENERIC_PCA9557) + list(APPEND MYBOT_BOARD_SOURCES + "${MYBOT_PLATFORM_ROOT}/src/drivers/io_expander/pca9557.c") +endif() + +if(MYBOT_GENERIC_TCA9554) + list(APPEND MYBOT_BOARD_SOURCES + "${MYBOT_PLATFORM_ROOT}/src/drivers/io_expander/tca9554.c") +endif() + +set(MYBOT_BOARD_REQUIRES + button + esp_codec_dev + esp_driver_gpio + esp_driver_i2c + esp_driver_i2s + esp_driver_ledc + esp_driver_spi + esp_lcd + ${MYBOT_GENERIC_EXTRA_REQUIRES} +) diff --git a/components/mybot_platform/boards/df-k10/README.md b/components/mybot_platform/boards/df-k10/README.md new file mode 100644 index 0000000..a43574a --- /dev/null +++ b/components/mybot_platform/boards/df-k10/README.md @@ -0,0 +1,14 @@ +# DFRobot K10 + +板型 ID 为 `df-k10`。 + +- 显示:240×320 ILI9341,SPI3,共享 LVGL 紧凑布局。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;TCA9555 扩展按键尚未实现。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +扩展按键、摄像头、LED 环和板级电源路径未实现。ILI9341 初始化、双 codec 路由和音频仍需 +真机验证。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/df-k10/board.c b/components/mybot_platform/boards/df-k10/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/df-k10/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/df-k10/board.cmake b/components/mybot_platform/boards/df-k10/board.cmake new file mode 100644 index 0000000..f3cb647 --- /dev/null +++ b/components/mybot_platform/boards/df-k10/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 "dual") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_ili9341) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/df-k10/board_config.h b/components/mybot_platform/boards/df-k10/board_config.h new file mode 100644 index 0000000..6dc01d6 --- /dev/null +++ b/components/mybot_platform/boards/df-k10/board_config.h @@ -0,0 +1,61 @@ +/* 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 "df-k10" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_3 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_0 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_45 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_47 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_48 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#define MYBOT_AMOLED175_I2C_PORT MYBOT_GENERIC_I2C_PORT +#define MYBOT_AMOLED175_ES8311_ADDRESS 0x30 +#define MYBOT_AMOLED175_ES7210_ADDRESS 0x23 + +#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_PANEL_CONTROLLER 3 +#define MYBOT_ILI9341_PROFILE 0 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_21 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_12 +#define MYBOT_DISPLAY_DC GPIO_NUM_13 +#define MYBOT_DISPLAY_CS GPIO_NUM_14 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 320 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#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/df-k10/sdkconfig.defaults b/components/mybot_platform/boards/df-k10/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/df-k10/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/doit-s3-aibox/README.md b/components/mybot_platform/boards/doit-s3-aibox/README.md new file mode 100644 index 0000000..23c9c63 --- /dev/null +++ b/components/mybot_platform/boards/doit-s3-aibox/README.md @@ -0,0 +1,12 @@ +# DOIT S3 AI Box + +板型 ID 为 `doit-s3-aibox`。当前为 headless profile,不编入 LVGL。 + +- 音频:PDM 采集 GPIO40/42,标准 I2S 播放 GPIO17/16/18。 +- 输入:GPIO10 主按键、GPIO15 音量加、GPIO9 音量减。 +- 电源:GPIO48 进程级保持。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +PDM 时钟、录音增益、播放、按键极性和电源仍需真机验证;LED、休眠和关机未实现。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/doit-s3-aibox/board.c b/components/mybot_platform/boards/doit-s3-aibox/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/doit-s3-aibox/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/doit-s3-aibox/board.cmake b/components/mybot_platform/boards/doit-s3-aibox/board.cmake new file mode 100644 index 0000000..14b6647 --- /dev/null +++ b/components/mybot_platform/boards/doit-s3-aibox/board.cmake @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "raw") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/doit-s3-aibox/board_config.h b/components/mybot_platform/boards/doit-s3-aibox/board_config.h new file mode 100644 index 0000000..46a9bf0 --- /dev/null +++ b/components/mybot_platform/boards/doit-s3-aibox/board_config.h @@ -0,0 +1,36 @@ +/* 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 "doit-s3-aibox" +#define MYBOT_BOARD_HAS_DISPLAY 0 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 2 +#define MYBOT_GENERIC_TOUCH_KIND 0 +#define MYBOT_AUDIO_I2S_CAPTURE_PDM 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_40 +#define MYBOT_AUDIO_I2S_MIC_WS GPIO_NUM_41 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_42 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_17 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_18 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_POWER_GPIO GPIO_NUM_48 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#define MYBOT_BOOT_BUTTON_GPIO GPIO_NUM_10 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_15 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_9 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/doit-s3-aibox/sdkconfig.defaults b/components/mybot_platform/boards/doit-s3-aibox/sdkconfig.defaults new file mode 100644 index 0000000..214c8f7 --- /dev/null +++ b/components/mybot_platform/boards/doit-s3-aibox/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/du-chatx/README.md b/components/mybot_platform/boards/du-chatx/README.md new file mode 100644 index 0000000..446e9b4 --- /dev/null +++ b/components/mybot_platform/boards/du-chatx/README.md @@ -0,0 +1,15 @@ +# DuChat X + +板型 ID 为 `du-chatx`。 + +- 显示:128×160 ST7789,SPI3 mode 0,40 MHz,X/Y 偏移 2/1,共享紧凑 LVGL UI。 +- 音频:独立麦克风与扬声器 raw I2S,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;无触摸和音量键。 +- 电源:GPIO1 高电平锁存,初始化时保持供电。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO9 LEDC,默认 60%。 + +电池、充电状态、LED、休眠和关机未实现。物理 Flash/PSRAM 容量、GPIO1 上电时序、面板方向、 +电源状态读取和音频仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/du-chatx/board.c b/components/mybot_platform/boards/du-chatx/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/du-chatx/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/du-chatx/board.cmake b/components/mybot_platform/boards/du-chatx/board.cmake new file mode 100644 index 0000000..ee77d8a --- /dev/null +++ b/components/mybot_platform/boards/du-chatx/board.cmake @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "raw") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/du-chatx/board_config.h b/components/mybot_platform/boards/du-chatx/board_config.h new file mode 100644 index 0000000..772361f --- /dev/null +++ b/components/mybot_platform/boards/du-chatx/board_config.h @@ -0,0 +1,59 @@ +/* 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 "du-chatx" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 2 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_MIC_WS GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_7 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_42 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_40 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#define MYBOT_GENERIC_POWER_GPIO GPIO_NUM_1 +#define MYBOT_GENERIC_POWER_ACTIVE_LEVEL 1 + +#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 SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_18 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_17 +#define MYBOT_DISPLAY_DC GPIO_NUM_8 +#define MYBOT_DISPLAY_CS GPIO_NUM_16 +#define MYBOT_DISPLAY_RESET GPIO_NUM_20 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_9 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 128 +#define MYBOT_DISPLAY_HEIGHT 160 +#define MYBOT_DISPLAY_OFFSET_X 2 +#define MYBOT_DISPLAY_OFFSET_Y 1 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/du-chatx/sdkconfig.defaults b/components/mybot_platform/boards/du-chatx/sdkconfig.defaults new file mode 100644 index 0000000..214c8f7 --- /dev/null +++ b/components/mybot_platform/boards/du-chatx/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/esp-box-3/README.md b/components/mybot_platform/boards/esp-box-3/README.md new file mode 100644 index 0000000..ed60ecb --- /dev/null +++ b/components/mybot_platform/boards/esp-box-3/README.md @@ -0,0 +1,14 @@ +# ESP-Box 3 + +板型 ID 为 `esp-box-3`。 + +- 显示:320×240 ILI9341,SPI3,共享 LVGL UI。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;当前未实现触摸。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 +- 背光:GPIO47 LEDC;LCD reset 为高有效。 + +ILI9341 厂商初始化、双 codec 路由、触摸、板级 PMIC/电源时序和音频仍需真机验证。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp-box-3/board.c b/components/mybot_platform/boards/esp-box-3/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp-box-3/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/esp-box-3/board.cmake b/components/mybot_platform/boards/esp-box-3/board.cmake new file mode 100644 index 0000000..f3cb647 --- /dev/null +++ b/components/mybot_platform/boards/esp-box-3/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 "dual") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_ili9341) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp-box-3/board_config.h b/components/mybot_platform/boards/esp-box-3/board_config.h new file mode 100644 index 0000000..4ba6e67 --- /dev/null +++ b/components/mybot_platform/boards/esp-box-3/board_config.h @@ -0,0 +1,62 @@ +/* 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 "esp-box-3" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_17 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_15 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_8 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_18 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_PANEL_CONTROLLER 3 +#define MYBOT_ILI9341_PROFILE 1 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_6 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_7 +#define MYBOT_DISPLAY_DC GPIO_NUM_4 +#define MYBOT_DISPLAY_CS GPIO_NUM_5 +#define MYBOT_DISPLAY_RESET GPIO_NUM_48 +#define MYBOT_DISPLAY_RESET_ACTIVE_HIGH 1 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_47 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#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 false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp-box-3/sdkconfig.defaults b/components/mybot_platform/boards/esp-box-3/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/esp-box-3/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/esp-box-lite/README.md b/components/mybot_platform/boards/esp-box-lite/README.md new file mode 100644 index 0000000..2aac23c --- /dev/null +++ b/components/mybot_platform/boards/esp-box-lite/README.md @@ -0,0 +1,15 @@ +# ESP-Box Lite + +板型 ID 为 `esp-box-lite`。 + +- 显示:320×240 ILI9341,SPI3,共享 LVGL UI。 +- 音频:当前按 ES8311 输出与 ES7210 采集 profile 接入,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;ADC 按键尚未实现。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 +- 背光:GPIO45 LEDC;LCD reset 为低有效。 + +实际 codec 型号、ADC 按键、ILI9341 初始化、电源时序和音频仍需真机核对。若实物 codec 与 +当前 profile 不同,应新增独立驱动,不得沿用错误配置。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp-box-lite/board.c b/components/mybot_platform/boards/esp-box-lite/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp-box-lite/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/esp-box-lite/board.cmake b/components/mybot_platform/boards/esp-box-lite/board.cmake new file mode 100644 index 0000000..f3cb647 --- /dev/null +++ b/components/mybot_platform/boards/esp-box-lite/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 "dual") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_ili9341) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp-box-lite/board_config.h b/components/mybot_platform/boards/esp-box-lite/board_config.h new file mode 100644 index 0000000..c4326df --- /dev/null +++ b/components/mybot_platform/boards/esp-box-lite/board_config.h @@ -0,0 +1,61 @@ +/* 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 "esp-box-lite" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_17 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_47 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_15 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_8 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_18 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_PANEL_CONTROLLER 3 +#define MYBOT_ILI9341_PROFILE 1 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_6 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_7 +#define MYBOT_DISPLAY_DC GPIO_NUM_4 +#define MYBOT_DISPLAY_CS GPIO_NUM_5 +#define MYBOT_DISPLAY_RESET GPIO_NUM_48 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_45 +#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 false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y true +#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_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp-box-lite/sdkconfig.defaults b/components/mybot_platform/boards/esp-box-lite/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/esp-box-lite/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/esp-box/README.md b/components/mybot_platform/boards/esp-box/README.md new file mode 100644 index 0000000..d88ac7c --- /dev/null +++ b/components/mybot_platform/boards/esp-box/README.md @@ -0,0 +1,14 @@ +# ESP-Box + +板型 ID 为 `esp-box`。 + +- 显示:320×240 ILI9341,SPI3,共享 LVGL UI。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;当前未实现触摸。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 +- 背光:GPIO45 LEDC,默认 60%。 + +ILI9341 厂商初始化、双 codec 路由、触摸、板级 PMIC/电源时序和音频仍需真机验证。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp-box/board.c b/components/mybot_platform/boards/esp-box/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp-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/esp-box/board.cmake b/components/mybot_platform/boards/esp-box/board.cmake new file mode 100644 index 0000000..f3cb647 --- /dev/null +++ b/components/mybot_platform/boards/esp-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 "dual") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_ili9341) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp-box/board_config.h b/components/mybot_platform/boards/esp-box/board_config.h new file mode 100644 index 0000000..7389872 --- /dev/null +++ b/components/mybot_platform/boards/esp-box/board_config.h @@ -0,0 +1,61 @@ +/* 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 "esp-box" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_17 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_47 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_15 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_8 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_18 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_PANEL_CONTROLLER 3 +#define MYBOT_ILI9341_PROFILE 1 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_6 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_7 +#define MYBOT_DISPLAY_DC GPIO_NUM_4 +#define MYBOT_DISPLAY_CS GPIO_NUM_5 +#define MYBOT_DISPLAY_RESET GPIO_NUM_48 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_45 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#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 false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp-box/sdkconfig.defaults b/components/mybot_platform/boards/esp-box/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/esp-box/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/esp32-s3-lcd-0.85/README.md b/components/mybot_platform/boards/esp32-s3-lcd-0.85/README.md new file mode 100644 index 0000000..89fc668 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-lcd-0.85/README.md @@ -0,0 +1,13 @@ +# ESP32-S3 LCD 0.85 + +板型 ID 为 `esp32-s3-lcd-0.85`。 + +- 显示:128×128 ST7789,SPI3 mode 3,40 MHz,偏移 `(2,1)`。 +- 音频:ES8311 与 ES7210,16 kHz mono signed-16 PCM。 +- 输入:GPIO5 主按键、GPIO4 音量加、GPIO0 音量减。 +- 电源:GPIO2 电池使能;GPIO46 背光;GPIO7 speaker PA。 +- 存储:8 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +当前无触摸。小屏 UI、面板方向、音频、电池和按键仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-lcd-0.85/board.c b/components/mybot_platform/boards/esp32-s3-lcd-0.85/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-lcd-0.85/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/esp32-s3-lcd-0.85/board.cmake b/components/mybot_platform/boards/esp32-s3-lcd-0.85/board.cmake new file mode 100644 index 0000000..3ffeed0 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-lcd-0.85/board.cmake @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/8m.csv") +set(MYBOT_GENERIC_AUDIO "dual") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_8MB + 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/esp32-s3-lcd-0.85/board_config.h b/components/mybot_platform/boards/esp32-s3-lcd-0.85/board_config.h new file mode 100644 index 0000000..1a91517 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-lcd-0.85/board_config.h @@ -0,0 +1,60 @@ +/* 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 "esp32-s3-lcd-0.85" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_8 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_11 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_12 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#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_7 +#define MYBOT_GENERIC_AUX_POWER_GPIO GPIO_NUM_2 + +#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_5 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_4 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_39 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_38 +#define MYBOT_DISPLAY_DC GPIO_NUM_45 +#define MYBOT_DISPLAY_CS GPIO_NUM_21 +#define MYBOT_DISPLAY_RESET GPIO_NUM_40 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_46 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 128 +#define MYBOT_DISPLAY_HEIGHT 128 +#define MYBOT_DISPLAY_OFFSET_X 2 +#define MYBOT_DISPLAY_OFFSET_Y 1 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 3 +#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/esp32-s3-lcd-0.85/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-lcd-0.85/sdkconfig.defaults new file mode 100644 index 0000000..ece839c --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-lcd-0.85/sdkconfig.defaults @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=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/esp32-s3-touch-amoled-1.32/README.md b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/README.md new file mode 100644 index 0000000..729515a --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/README.md @@ -0,0 +1,14 @@ +# ESP32-S3 Touch AMOLED 1.32 + +板型 ID 为 `esp32-s3-touch-amoled-1.32`。 + +- 显示:466×466 SH8601 QSPI,共享 LVGL UI。 +- 音频:ES8311,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;当前未实现触摸。 +- 电源:GPIO18 电源使能。 +- 存储:8 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +面板偏移、颜色、方向、电源轨、音频和按键仍需真机验证;睡眠、关机、电池上报和视频未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/board.c b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/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/esp32-s3-touch-amoled-1.32/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/board.cmake new file mode 100644 index 0000000..56dda31 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/board.cmake @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/8m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "sh8601/sh8601_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_PCA9557 OFF) +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_8MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_OCT + CONFIG_SPIRAM_SPEED_80M + CONFIG_CODEC_ES8311_SUPPORT +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_sh8601) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/board_config.h new file mode 100644 index 0000000..cd9c85e --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/board_config.h @@ -0,0 +1,60 @@ +/* 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 "esp32-s3-touch-amoled-1.32" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_41 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_40 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_42 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_47 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_48 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 +#define MYBOT_GENERIC_POWER_GPIO GPIO_NUM_18 +#define MYBOT_GENERIC_POWER_ACTIVE_LEVEL 1 + +#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_SH8601_PROFILE 2 +#define MYBOT_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_11 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_12 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_13 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_14 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_15 +#define MYBOT_DISPLAY_CS GPIO_NUM_10 +#define MYBOT_DISPLAY_RESET GPIO_NUM_8 +#define MYBOT_DISPLAY_RESET_ACTIVE_HIGH 0 +#define MYBOT_DISPLAY_WIDTH 466 +#define MYBOT_DISPLAY_HEIGHT 466 +#define MYBOT_DISPLAY_OFFSET_X 6 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/sdkconfig.defaults new file mode 100644 index 0000000..63ebd39 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.32/sdkconfig.defaults @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=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/esp32-s3-touch-amoled-1.43c/README.md b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/README.md new file mode 100644 index 0000000..013bc99 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/README.md @@ -0,0 +1,14 @@ +# ESP32-S3 Touch AMOLED 1.43C + +板型 ID 为 `esp32-s3-touch-amoled-1.43c`。 + +- 显示:466×466 SH8601 QSPI,共享 LVGL UI。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;当前未实现触摸。 +- 存储:8 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +SH8601 时序、双 codec 路由、电源轨、音频和按键仍需真机验证;睡眠、关机、电池上报和视频 +未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/board.c b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/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/esp32-s3-touch-amoled-1.43c/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/board.cmake new file mode 100644 index 0000000..3d32c4c --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/board.cmake @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/8m.csv") +set(MYBOT_GENERIC_AUDIO "dual") +set(MYBOT_GENERIC_DISPLAY "sh8601/sh8601_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_PCA9557 OFF) +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_8MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_OCT + CONFIG_SPIRAM_SPEED_80M + CONFIG_CODEC_ES8311_SUPPORT + CONFIG_CODEC_ES7210_SUPPORT +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_sh8601) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/board_config.h new file mode 100644 index 0000000..be6c2c8 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/board_config.h @@ -0,0 +1,57 @@ +/* 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 "esp32-s3-touch-amoled-1.43c" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_40 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_42 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_41 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_47 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_48 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define MYBOT_SH8601_PROFILE 3 +#define MYBOT_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_14 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_9 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_10 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_11 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_12 +#define MYBOT_DISPLAY_CS GPIO_NUM_15 +#define MYBOT_DISPLAY_RESET GPIO_NUM_13 +#define MYBOT_DISPLAY_RESET_ACTIVE_HIGH 0 +#define MYBOT_DISPLAY_WIDTH 466 +#define MYBOT_DISPLAY_HEIGHT 466 +#define MYBOT_DISPLAY_OFFSET_X 8 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/sdkconfig.defaults new file mode 100644 index 0000000..ece839c --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.43c/sdkconfig.defaults @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=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/esp32-s3-touch-amoled-1.8-v2/README.md b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/README.md new file mode 100644 index 0000000..33e0ca0 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/README.md @@ -0,0 +1,28 @@ +# ESP32-S3 Touch AMOLED 1.8 V2 + +板型 ID 为 `esp32-s3-touch-amoled-1.8-v2`。该 profile 使用独立的 368×448 CO5300 面板配置, +不能与 1.75、1.75C 或 2.16 AMOLED profile 交叉烧录。 + +| 项目 | 当前实现 | +| --- | --- | +| 存储 | 16 MB QIO Flash profile;80 MHz Octal PSRAM;实际容量待真机确认 | +| 分区 | [`partitions/v2/16m.csv`](../../../../partitions/v2/16m.csv),双 OTA | +| 电源 | TCA9554 执行面板复位;AXP2101 配置电源轨和充电参数 | +| 音频 | ES8311,16 kHz 单声道 PCM;GPIO46 扬声器功放 | +| 显示 | 368×448 CO5300 AMOLED,40 MHz QSPI,X 偏移 16,共享 LVGL | +| 输入 | CST816S 触摸与 GPIO0 Boot;短按切换对话,长按 3 秒配网 | +| 网络 / 视频 | Wi-Fi;未接入摄像头,不支持视频 | + +| 信号 | GPIO | +| --- | --- | +| I2S MCLK / BCLK / WS / DIN / DOUT | 16 / 9 / 45 / 10 / 8 | +| 公共 I2C SDA / SCL | 15 / 14 | +| QSPI CLK / CS / D0-D3 | 11 / 12 / 4、5、6、7 | +| CST816S RESET / INT | 39 / 13 | +| TCA9554 / AXP2101 地址 | `0x20` / `0x34` | + +逻辑界面保持 SDK 的 16 kHz 单声道 PCM 契约,I2S 与 ES8311 均运行在 16 kHz。 +CO5300 使用 RGB565 局部 DMA 刷新,传输区域保持偶数像素边界。 + +当前未实现 AXP2101 电源键快捷行为、电量/充电上报、休眠、关机、摄像头或低功耗行为。 +构建与真机状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/amoled18_hardware.c b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/amoled18_hardware.c new file mode 100644 index 0000000..99d0133 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/amoled18_hardware.c @@ -0,0 +1,179 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "amoled18_hardware.h" + +#include "board_config.h" +#include "io_expander/tca9554.h" + +#include "driver/gpio.h" +#include "esp_err.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define TAG "amoled18_hw" +#define I2C_TIMEOUT_MS 100 + +typedef struct { + i2c_master_bus_handle_t bus; + i2c_master_dev_handle_t axp2101; + mybot_tca9554_t expander; + bool pa_ready; + bool pa_enabled; + bool initialized; +} amoled18_hardware_t; + +static amoled18_hardware_t s_hardware; + +static esp_err_t write_axp_register(uint8_t reg, uint8_t value) { + const uint8_t command[] = {reg, value}; + return i2c_master_transmit(s_hardware.axp2101, command, sizeof(command), I2C_TIMEOUT_MS); +} + +static esp_err_t initialize_axp2101(void) { + static const uint8_t registers[][2] = { + {0x22, 0x06}, {0x27, 0x10}, {0x80, 0x01}, {0x90, 0x00}, {0x91, 0x00}, {0x82, 0x12}, + {0x92, 0x1c}, {0x90, 0x01}, {0x64, 0x02}, {0x61, 0x02}, {0x62, 0x08}, {0x63, 0x01}, + }; + for (size_t index = 0; index < sizeof(registers) / sizeof(registers[0]); ++index) { + esp_err_t result = write_axp_register(registers[index][0], registers[index][1]); + if (result != ESP_OK) { + return result; + } + } + return ESP_OK; +} + +static esp_err_t initialize_pa(void) { + const gpio_config_t config = { + .pin_bit_mask = 1ULL << MYBOT_AMOLED18_SPEAKER_PA, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_ENABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t result = gpio_config(&config); + if (result == ESP_OK) { + result = gpio_set_level(MYBOT_AMOLED18_SPEAKER_PA, 0); + } + if (result == ESP_OK) { + s_hardware.pa_ready = true; + } + return result; +} + +static int release_hardware(void) { + int result = 0; + if (s_hardware.pa_ready && gpio_set_level(MYBOT_AMOLED18_SPEAKER_PA, 0) != ESP_OK) { + result = -1; + } + if (s_hardware.axp2101 && i2c_master_bus_rm_device(s_hardware.axp2101) != ESP_OK) { + result = -1; + } + if (mybot_tca9554_deinit(&s_hardware.expander) < 0) { + result = -1; + } + if (s_hardware.bus && i2c_del_master_bus(s_hardware.bus) != ESP_OK) { + result = -1; + } + if (result == 0) { + s_hardware = (amoled18_hardware_t){0}; + } else { + s_hardware.initialized = false; + s_hardware.pa_enabled = false; + } + return result; +} + +int mybot_amoled18_hardware_init(void) { + if (s_hardware.initialized) { + return 0; + } + if ((s_hardware.bus || s_hardware.axp2101 || s_hardware.expander.device || + s_hardware.pa_ready) && + release_hardware() < 0) { + return -1; + } + + esp_err_t result = initialize_pa(); + const i2c_master_bus_config_t bus_config = { + .i2c_port = MYBOT_AMOLED18_I2C_PORT, + .sda_io_num = MYBOT_AMOLED18_I2C_SDA, + .scl_io_num = MYBOT_AMOLED18_I2C_SCL, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, + }; + if (result == ESP_OK) { + result = i2c_new_master_bus(&bus_config, &s_hardware.bus); + } + if (result == ESP_OK) { + result = + mybot_tca9554_init(&s_hardware.expander, s_hardware.bus, MYBOT_AMOLED18_TCA9554_ADDRESS, + MYBOT_AMOLED18_I2C_SPEED_HZ) == 0 + ? ESP_OK + : ESP_FAIL; + } + if (result == ESP_OK) { + result = mybot_tca9554_configure_outputs(&s_hardware.expander, 0x07, 0x00) == 0 ? ESP_OK + : ESP_FAIL; + } + if (result == ESP_OK) { + result = + mybot_tca9554_set_levels(&s_hardware.expander, 0x07, 0x07) == 0 ? ESP_OK : ESP_FAIL; + vTaskDelay(pdMS_TO_TICKS(100)); + } + if (result == ESP_OK) { + result = + mybot_tca9554_set_levels(&s_hardware.expander, 0x07, 0x00) == 0 ? ESP_OK : ESP_FAIL; + vTaskDelay(pdMS_TO_TICKS(300)); + } + if (result == ESP_OK) { + result = + mybot_tca9554_set_levels(&s_hardware.expander, 0x07, 0x07) == 0 ? ESP_OK : ESP_FAIL; + } + + const i2c_device_config_t axp_config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = MYBOT_AMOLED18_AXP2101_ADDRESS, + .scl_speed_hz = MYBOT_AMOLED18_I2C_SPEED_HZ, + }; + if (result == ESP_OK) { + result = i2c_master_bus_add_device(s_hardware.bus, &axp_config, &s_hardware.axp2101); + } + if (result == ESP_OK) { + result = initialize_axp2101(); + } + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=hardware action=initialize result=error code=%s", + esp_err_to_name(result)); + (void)release_hardware(); + return -1; + } + + s_hardware.initialized = true; + ESP_LOGI(TAG, "event=hardware action=initialize result=ok expander=0x%02x axp2101=0x%02x", + MYBOT_AMOLED18_TCA9554_ADDRESS, MYBOT_AMOLED18_AXP2101_ADDRESS); + return 0; +} + +int mybot_amoled18_hardware_deinit(void) { + return release_hardware(); +} + +i2c_master_bus_handle_t mybot_amoled18_i2c_bus_handle(void) { + return s_hardware.initialized ? s_hardware.bus : NULL; +} + +int mybot_amoled18_set_speaker_power(bool enabled) { + if (!s_hardware.initialized || !s_hardware.pa_ready) { + return -1; + } + if (s_hardware.pa_enabled == enabled) { + return 0; + } + if (gpio_set_level(MYBOT_AMOLED18_SPEAKER_PA, enabled ? 1 : 0) != ESP_OK) { + return -1; + } + s_hardware.pa_enabled = enabled; + return 0; +} diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/amoled18_hardware.h b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/amoled18_hardware.h new file mode 100644 index 0000000..2284f0e --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/amoled18_hardware.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_AMOLED18_HARDWARE_H_ +#define MYBOT_AMOLED18_HARDWARE_H_ + +#include "driver/i2c_master.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int mybot_amoled18_hardware_init(void); +int mybot_amoled18_hardware_deinit(void); +i2c_master_bus_handle_t mybot_amoled18_i2c_bus_handle(void); +int mybot_amoled18_set_speaker_power(bool enabled); + +#ifdef __cplusplus +} +#endif + +#endif /* MYBOT_AMOLED18_HARDWARE_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board.c b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board.c new file mode 100644 index 0000000..847c7bc --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board.c @@ -0,0 +1,156 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "mybot_platform/board.h" + +#include + +#include "amoled18_hardware.h" +#include "announcement/embedded_ogg_prompt.h" +#include "board_config.h" +#include "esp_log.h" +#include "network/wifi_control.h" +#include "platform/board_actions.h" + +#include + +#define TAG "amoled18_board" + +const mybot_audio_capture_ops_t *mybot_es8311_audio_capture_ops(void); +const mybot_audio_playback_ops_t *mybot_es8311_audio_playback_ops(void); +const mybot_audio_volume_ops_t *mybot_es8311_audio_volume_ops(void); +const mybot_announce_ops_t *mybot_esp32s3_announce_ops(void); +const mybot_https_ops_t *mybot_esp32s3_https_ops(void); +const mybot_key_ops_t *mybot_cst816s_input_ops(void); +const mybot_kv_store_ops_t *mybot_esp32s3_kv_store_ops(void); +const mybot_lcd_ops_t *mybot_shared_lvgl_ops(void); +const mybot_wifi_ops_t *mybot_esp32s3_wifi_ops(void); +int mybot_cst816s_input_start(void); +int mybot_cst816s_input_stop(void); + +static const mybot_lcd_ops_t *s_lcd_ops; +static void *s_lcd_context; +static bool s_automatic_provisioning_started; +static bool s_prepared; +static bool s_cleanup_pending; + +static int board_show_screen(mybot_lcd_screen_t screen) { + if (!s_lcd_ops || !s_lcd_context) { + return -1; + } + const mybot_lcd_content_t content = {.screen = screen}; + return s_lcd_ops->render(s_lcd_context, &content); +} + +static void board_on_provisioning(const char *trigger) { + (void)board_show_screen(MYBOT_LCD_SCREEN_WIFI_PROVISIONING); + (void)mybot_embedded_ogg_play_wifi_provisioning(mybot_es8311_audio_playback_ops(), + mybot_es8311_audio_volume_ops(), trigger); +} + +static void board_on_automatic_provisioning(void) { + s_automatic_provisioning_started = true; + board_on_provisioning("automatic"); +} + +static void board_on_input_provisioning(void) { + board_on_provisioning("input"); +} + +static int board_rollback_prepare(void) { + s_cleanup_pending = true; + if (mybot_cst816s_input_stop() < 0) { + return -1; + } + if (s_lcd_ops && s_lcd_context) { + s_lcd_ops->destroy(s_lcd_context); + s_lcd_context = NULL; + } + s_lcd_ops = NULL; + if (mybot_amoled18_hardware_deinit() < 0) { + return -1; + } + s_cleanup_pending = false; + return 0; +} + +static int board_prepare(void) { + if (s_prepared) { + return 0; + } + if (s_cleanup_pending && board_rollback_prepare() < 0) { + return -1; + } + if (mybot_amoled18_hardware_init() < 0) { + return -1; + } + s_lcd_ops = mybot_shared_lvgl_ops(); + if (!s_lcd_ops || s_lcd_ops->init(&s_lcd_context) < 0 || + board_show_screen(MYBOT_LCD_SCREEN_STARTING) < 0 || mybot_cst816s_input_start() < 0) { + (void)board_rollback_prepare(); + return -1; + } + s_prepared = true; + return 0; +} + +int mybot_board_handle_boot_long_press(void) { + mybot_board_request_wifi_provisioning(); + return 0; +} + +static int board_provision_wifi(void) { + return mybot_wifi_run_provisioning(board_on_input_provisioning); +} + +static int board_ensure_network(const char *device_id) { + s_automatic_provisioning_started = false; + int result = mybot_wifi_ensure_network(device_id, board_on_automatic_provisioning); + if (s_automatic_provisioning_started) { + (void)mybot_board_wait_wifi_provisioning_request(0); + } + return result; +} + +static void board_shutdown_network(void) { + mybot_wifi_shutdown_network(); +} + +static int board_register_platform(void) { + const mybot_platform_descriptor_t descriptor = { + .wifi = mybot_esp32s3_wifi_ops(), + .kv_store = mybot_esp32s3_kv_store_ops(), + .key = mybot_cst816s_input_ops(), + .audio_capture = mybot_es8311_audio_capture_ops(), + .audio_playback = mybot_es8311_audio_playback_ops(), + .audio_volume = mybot_es8311_audio_volume_ops(), + .announce = mybot_esp32s3_announce_ops(), + .https = mybot_esp32s3_https_ops(), + .lcd = mybot_shared_lvgl_ops(), + }; + return mybot_platform_register(&descriptor); +} + +void *mybot_es8311_board_i2c_bus_handle(void) { + return mybot_amoled18_i2c_bus_handle(); +} + +int mybot_es8311_board_set_speaker_power(bool enabled) { + return mybot_amoled18_set_speaker_power(enabled); +} + +i2c_master_bus_handle_t mybot_board_touch_i2c_bus_handle(void) { + return mybot_amoled18_i2c_bus_handle(); +} + +static const mybot_board_t s_board = { + .id = MYBOT_BOARD_NAME, + .hw_model = MYBOT_BOARD_NAME, + .prepare = board_prepare, + .register_platform = board_register_platform, + .ensure_network = board_ensure_network, + .provision_wifi = board_provision_wifi, + .shutdown_network = board_shutdown_network, +}; + +const mybot_board_t *mybot_board_get(void) { + return &s_board; +} diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board.cmake new file mode 100644 index 0000000..5347444 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board.cmake @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_BOARD_TARGET "esp32s3") +set(MYBOT_BOARD_SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults") +set(MYBOT_BOARD_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}") +set(MYBOT_BOARD_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 +) +set(MYBOT_BOARD_FORBIDDEN_CONFIGS + CONFIG_CODEC_I2C_BACKWARD_COMPATIBLE +) +set(MYBOT_BOARD_PARTITION_TABLE "partitions/v2/16m.csv") + +get_filename_component(MYBOT_PLATFORM_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +include("${MYBOT_PLATFORM_ROOT}/src/drivers/display/display.cmake") +set(MYBOT_BOARD_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/board.c" + "${CMAKE_CURRENT_LIST_DIR}/amoled18_hardware.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/io_expander/tca9554.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/input/cst816s_boot_input.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/audio/es8311_codec_audio.c" +) +mybot_display_add_panel(MYBOT_BOARD_SOURCES "co5300/co5300_amoled18_panel.c") +mybot_display_add_lvgl_sources(MYBOT_BOARD_SOURCES "shared_lvgl_adapter.cc") +set(MYBOT_BOARD_REQUIRES + button + esp_codec_dev + esp_driver_gpio + esp_driver_i2c + esp_driver_i2s + esp_driver_spi + esp_lcd + esp_lcd_co5300 + esp_lcd_touch + esp_lcd_touch_cst816s + esp_lvgl_port + lvgl + nvs_flash +) diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board_config.h new file mode 100644 index 0000000..82f18dc --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/board_config.h @@ -0,0 +1,66 @@ +/* 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 "esp32-s3-touch-amoled-1.8-v2" +#define MYBOT_BOARD_HAS_TOUCH 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_8 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_10 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 + +#define MYBOT_AMOLED18_I2C_PORT I2C_NUM_0 +#define MYBOT_AMOLED18_I2C_SDA GPIO_NUM_15 +#define MYBOT_AMOLED18_I2C_SCL GPIO_NUM_14 +#define MYBOT_AMOLED18_I2C_SPEED_HZ 400000 +#define MYBOT_AMOLED18_TCA9554_ADDRESS 0x20 +#define MYBOT_AMOLED18_AXP2101_ADDRESS 0x34 +#define MYBOT_AMOLED18_SPEAKER_PA GPIO_NUM_46 + +#define MYBOT_ES8311_I2C_PORT MYBOT_AMOLED18_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_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_11 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_4 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_5 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_6 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_7 +#define MYBOT_DISPLAY_CS GPIO_NUM_12 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_WIDTH 368 +#define MYBOT_DISPLAY_HEIGHT 448 +#define MYBOT_DISPLAY_OFFSET_X 16 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) + +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_39 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_13 +#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_BOOT_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/sdkconfig.defaults new file mode 100644 index 0000000..0be593f --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8-v2/sdkconfig.defaults @@ -0,0 +1,14 @@ +# 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/esp32-s3-touch-amoled-1.8/README.md b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/README.md new file mode 100644 index 0000000..fb339a5 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/README.md @@ -0,0 +1,15 @@ +# ESP32-S3 Touch AMOLED 1.8 + +板型 ID 为 `esp32-s3-touch-amoled-1.8`。 + +- 显示:368×448 SH8601 QSPI,共享 LVGL UI。 +- 音频:ES8311,16 kHz 单声道 signed-16 PCM。 +- 输入:FT5x06 触摸和 GPIO0 Boot。 +- 电源/扩展:AXP2101 `0x34`、TCA9554 `0x20`,用于电源轨和 LCD/触摸复位。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +AXP2101/TCA9554 上电顺序、SH8601 时序、触摸映射、音频和 PA 仍需真机验证。睡眠、关机、 +电池上报和视频未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/board.c b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/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/esp32-s3-touch-amoled-1.8/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/board.cmake new file mode 100644 index 0000000..27f5b1a --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/board.cmake @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "sh8601/sh8601_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "ft5x06") +set(MYBOT_GENERIC_TCA9554 ON) +set(MYBOT_GENERIC_PCA9557 OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_sh8601) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/board_config.h new file mode 100644 index 0000000..ac945bc --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/board_config.h @@ -0,0 +1,75 @@ +/* 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 "esp32-s3-touch-amoled-1.8" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 1 +#define MYBOT_GENERIC_TOUCH_KIND 2 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_8 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_15 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_14 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 +#define MYBOT_GENERIC_TCA9554_ADDRESS 0x20 +#define MYBOT_GENERIC_TCA9554_OUTPUT_MASK 0x07 +#define MYBOT_GENERIC_TCA9554_RESET_MASK 0x07 +#define MYBOT_GENERIC_AXP2101_ADDRESS 0x34 + +#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_TOUCH_DRIVER_KIND 2 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_39 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_13 +#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_SH8601_PROFILE 1 +#define MYBOT_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_11 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_4 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_5 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_6 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_7 +#define MYBOT_DISPLAY_CS GPIO_NUM_12 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_RESET_ACTIVE_HIGH 1 +#define MYBOT_DISPLAY_WIDTH 368 +#define MYBOT_DISPLAY_HEIGHT 448 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-1.8/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/esp32-s3-touch-amoled-2.06/README.md b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/README.md new file mode 100644 index 0000000..9cdcf07 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/README.md @@ -0,0 +1,15 @@ +# ESP32-S3 Touch AMOLED 2.06 + +板型 ID 为 `esp32-s3-touch-amoled-2.06`。 + +- 显示:410×502 SH8601 QSPI,X 方向 panel gap 为 22 像素。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:FT5x06 触摸和 GPIO0 Boot。 +- 电源:AXP2101 `0x34`。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +面板时序、偏移、颜色、触摸映射、AXP2101 电源和双 codec 音频仍需真机验证。睡眠、关机、 +电池上报和视频未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/board.c b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/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/esp32-s3-touch-amoled-2.06/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/board.cmake new file mode 100644 index 0000000..0fd3c46 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/board.cmake @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "dual") +set(MYBOT_GENERIC_DISPLAY "sh8601/sh8601_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "ft5x06") +set(MYBOT_GENERIC_PCA9557 OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_sh8601) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/board_config.h new file mode 100644 index 0000000..f99c001 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/board_config.h @@ -0,0 +1,71 @@ +/* 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 "esp32-s3-touch-amoled-2.06" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 2 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_41 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_42 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_40 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_15 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_14 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 +#define MYBOT_GENERIC_AXP2101_ADDRESS 0x34 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC + +#define MYBOT_TOUCH_DRIVER_KIND 2 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_9 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_38 +#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_SH8601_PROFILE 4 +#define MYBOT_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_11 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_4 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_5 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_6 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_7 +#define MYBOT_DISPLAY_CS GPIO_NUM_12 +#define MYBOT_DISPLAY_RESET GPIO_NUM_8 +#define MYBOT_DISPLAY_RESET_ACTIVE_HIGH 0 +#define MYBOT_DISPLAY_WIDTH 410 +#define MYBOT_DISPLAY_HEIGHT 502 +#define MYBOT_DISPLAY_OFFSET_X 22 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-amoled-2.06/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/esp32-s3-touch-lcd-1.54/README.md b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/README.md new file mode 100644 index 0000000..3fea534 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/README.md @@ -0,0 +1,13 @@ +# ESP32-S3 Touch LCD 1.54 + +板型 ID 为 `esp32-s3-touch-lcd-1.54`。 + +- 显示:240×240 ST7789,SPI3 mode 3,40 MHz,共享 LVGL UI。 +- 音频:ES8311 与 ES7210,16 kHz mono signed-16 PCM。 +- 输入:CST816S 触摸、GPIO5 主按键、GPIO4 音量加、GPIO0 音量减。 +- 电源:GPIO2 电池/外设使能。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +AXP/电池管理未完整接入;触摸、音频、面板、电源和按键仍需真机验证。睡眠与关机未实现。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/board.c b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/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/esp32-s3-touch-lcd-1.54/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/board.cmake new file mode 100644 index 0000000..978c07e --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/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 "dual") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst816s") +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 + CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/board_config.h new file mode 100644 index 0000000..07cabe4 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/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 "esp32-s3-touch-lcd-1.54" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_8 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_11 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_12 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#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_7 +#define MYBOT_GENERIC_AUX_POWER_GPIO GPIO_NUM_2 + +#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_5 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_4 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_TOUCH_DRIVER_KIND 1 +#define MYBOT_TOUCH_I2C_SPEED_HZ 100000 +#define MYBOT_TOUCH_RESET GPIO_NUM_47 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_48 +#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_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_39 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_38 +#define MYBOT_DISPLAY_DC GPIO_NUM_45 +#define MYBOT_DISPLAY_CS GPIO_NUM_21 +#define MYBOT_DISPLAY_RESET GPIO_NUM_40 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_46 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 3 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/sdkconfig.defaults new file mode 100644 index 0000000..58f71c7 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.54/sdkconfig.defaults @@ -0,0 +1,12 @@ +# 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 +CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/README.md b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/README.md new file mode 100644 index 0000000..7819bdf --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/README.md @@ -0,0 +1,13 @@ +# ESP32-S3 Touch LCD 1.83 + +板型 ID 为 `esp32-s3-touch-lcd-1.83`。 + +- 显示:240×284 ST7789,SPI3 mode 0,24 MHz,共享 LVGL UI。 +- 音频:ES8311 与 ES7210,16 kHz mono signed-16 PCM。 +- 输入:CST816S 触摸,GPIO0 Boot。 +- 电源:AXP2101 `0x34`;GPIO46 speaker PA。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +AXP2101 电源、触摸、面板和双 codec 音频仍需真机验证。PWR41、电池上报、睡眠和关机未接入。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/board.c b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/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/esp32-s3-touch-lcd-1.83/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/board.cmake new file mode 100644 index 0000000..978c07e --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/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 "dual") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst816s") +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 + CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/board_config.h new file mode 100644 index 0000000..8a8eadf --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/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 "esp32-s3-touch-lcd-1.83" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_8 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_15 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_14 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 +#define MYBOT_GENERIC_AXP2101_ADDRESS 0x34 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_TOUCH_DRIVER_KIND 1 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_39 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_13 +#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_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_7 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_6 +#define MYBOT_DISPLAY_DC GPIO_NUM_4 +#define MYBOT_DISPLAY_CS GPIO_NUM_5 +#define MYBOT_DISPLAY_RESET GPIO_NUM_38 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_40 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 284 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (24 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/sdkconfig.defaults new file mode 100644 index 0000000..58f71c7 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.83/sdkconfig.defaults @@ -0,0 +1,12 @@ +# 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 +CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/README.md b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/README.md new file mode 100644 index 0000000..f40cf5c --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/README.md @@ -0,0 +1,27 @@ +# ESP32-S3 Touch LCD 1.85 + +板型 ID 为 `esp32-s3-touch-lcd-1.85`。该 profile 使用 ST77916 QSPI 面板、双 I2C 总线、 +TCA9554 复位控制和独立麦克风/扬声器 I2S。 + +| 项目 | 当前实现 | +| --- | --- | +| 存储 | 16 MB QIO Flash profile;80 MHz Octal PSRAM;实际容量待真机确认 | +| 分区 | [`partitions/v2/16m.csv`](../../../../partitions/v2/16m.csv),双 OTA | +| 电源保持 | GPIO7 在进程运行期间保持高电平 | +| 音频 | 原始 I2S;16 kHz 单声道 PCM;麦克风读取右 slot,播放复制到两个 slot | +| 显示 | 360×360 ST77916,80 MHz QSPI,共享 LVGL | +| 输入 | CST816S 触摸与 GPIO0 Boot;短按切换对话,长按 3 秒配网 | +| 网络 / 视频 | Wi-Fi;未接入摄像头,不支持视频 | + +| 信号 | GPIO | +| --- | --- | +| 麦克风 I2S RX | WS 2、BCLK 15、DIN 39 | +| 扬声器 I2S TX | DOUT 47、BCLK 48、WS 38 | +| 控制 I2C SDA / SCL | 11 / 10,TCA9554 地址 `0x20` | +| 触摸 I2C SDA / SCL | 1 / 3,中断 GPIO4 | +| QSPI CLK / CS / D0-D3 | 40 / 21 / 46、45、42、41 | +| LCD 背光 | 5,LEDC 5 kHz、13-bit,默认 60% | + +TCA9554 的 bit0/bit1 用于释放 LCD 与触摸复位。当前 profile 不映射硬件参考中的 GPIO6 +电源按键,也未实现电量、充电上报、休眠、关机或低功耗行为。构建与真机状态见 +[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board.c b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board.c new file mode 100644 index 0000000..5e6872b --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board.c @@ -0,0 +1,148 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "mybot_platform/board.h" + +#include + +#include "announcement/embedded_ogg_prompt.h" +#include "board_config.h" +#include "esp_log.h" +#include "lcd185_hardware.h" +#include "network/wifi_control.h" +#include "platform/board_actions.h" + +#include + +#define TAG "lcd185_board" + +const mybot_audio_capture_ops_t *mybot_esp32s3_audio_capture_ops(void); +const mybot_audio_playback_ops_t *mybot_esp32s3_audio_playback_ops(void); +const mybot_audio_volume_ops_t *mybot_esp32s3_audio_volume_ops(void); +const mybot_announce_ops_t *mybot_esp32s3_announce_ops(void); +const mybot_https_ops_t *mybot_esp32s3_https_ops(void); +const mybot_key_ops_t *mybot_cst816s_input_ops(void); +const mybot_kv_store_ops_t *mybot_esp32s3_kv_store_ops(void); +const mybot_lcd_ops_t *mybot_shared_lvgl_ops(void); +const mybot_wifi_ops_t *mybot_esp32s3_wifi_ops(void); +int mybot_cst816s_input_start(void); +int mybot_cst816s_input_stop(void); + +static const mybot_lcd_ops_t *s_lcd_ops; +static void *s_lcd_context; +static bool s_automatic_provisioning_started; +static bool s_prepared; +static bool s_cleanup_pending; + +static int board_show_screen(mybot_lcd_screen_t screen) { + if (!s_lcd_ops || !s_lcd_context) { + return -1; + } + const mybot_lcd_content_t content = {.screen = screen}; + return s_lcd_ops->render(s_lcd_context, &content); +} + +static void board_on_provisioning(const char *trigger) { + (void)board_show_screen(MYBOT_LCD_SCREEN_WIFI_PROVISIONING); + (void)mybot_embedded_ogg_play_wifi_provisioning(mybot_esp32s3_audio_playback_ops(), + mybot_esp32s3_audio_volume_ops(), trigger); +} + +static void board_on_automatic_provisioning(void) { + s_automatic_provisioning_started = true; + board_on_provisioning("automatic"); +} + +static void board_on_input_provisioning(void) { + board_on_provisioning("input"); +} + +static int board_rollback_prepare(void) { + s_cleanup_pending = true; + if (mybot_cst816s_input_stop() < 0) { + return -1; + } + if (s_lcd_ops && s_lcd_context) { + s_lcd_ops->destroy(s_lcd_context); + s_lcd_context = NULL; + } + s_lcd_ops = NULL; + if (mybot_lcd185_hardware_deinit() < 0) { + return -1; + } + s_cleanup_pending = false; + return 0; +} + +static int board_prepare(void) { + if (s_prepared) { + return 0; + } + if (s_cleanup_pending && board_rollback_prepare() < 0) { + return -1; + } + if (mybot_lcd185_hardware_init() < 0) { + return -1; + } + s_lcd_ops = mybot_shared_lvgl_ops(); + if (!s_lcd_ops || s_lcd_ops->init(&s_lcd_context) < 0 || + board_show_screen(MYBOT_LCD_SCREEN_STARTING) < 0 || mybot_cst816s_input_start() < 0) { + (void)board_rollback_prepare(); + return -1; + } + s_prepared = true; + return 0; +} + +int mybot_board_handle_boot_long_press(void) { + mybot_board_request_wifi_provisioning(); + return 0; +} + +static int board_provision_wifi(void) { + return mybot_wifi_run_provisioning(board_on_input_provisioning); +} + +static int board_ensure_network(const char *device_id) { + s_automatic_provisioning_started = false; + int result = mybot_wifi_ensure_network(device_id, board_on_automatic_provisioning); + if (s_automatic_provisioning_started) { + (void)mybot_board_wait_wifi_provisioning_request(0); + } + return result; +} + +static void board_shutdown_network(void) { + mybot_wifi_shutdown_network(); +} + +static int board_register_platform(void) { + const mybot_platform_descriptor_t descriptor = { + .wifi = mybot_esp32s3_wifi_ops(), + .kv_store = mybot_esp32s3_kv_store_ops(), + .key = mybot_cst816s_input_ops(), + .audio_capture = mybot_esp32s3_audio_capture_ops(), + .audio_playback = mybot_esp32s3_audio_playback_ops(), + .audio_volume = mybot_esp32s3_audio_volume_ops(), + .announce = mybot_esp32s3_announce_ops(), + .https = mybot_esp32s3_https_ops(), + .lcd = mybot_shared_lvgl_ops(), + }; + return mybot_platform_register(&descriptor); +} + +i2c_master_bus_handle_t mybot_board_touch_i2c_bus_handle(void) { + return mybot_lcd185_touch_i2c_bus_handle(); +} + +static const mybot_board_t s_board = { + .id = MYBOT_BOARD_NAME, + .hw_model = MYBOT_BOARD_NAME, + .prepare = board_prepare, + .register_platform = board_register_platform, + .ensure_network = board_ensure_network, + .provision_wifi = board_provision_wifi, + .shutdown_network = board_shutdown_network, +}; + +const mybot_board_t *mybot_board_get(void) { + return &s_board; +} diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board.cmake new file mode 100644 index 0000000..49202db --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board.cmake @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_BOARD_TARGET "esp32s3") +set(MYBOT_BOARD_SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults") +set(MYBOT_BOARD_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}") +set(MYBOT_BOARD_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_BOARD_PARTITION_TABLE "partitions/v2/16m.csv") + +get_filename_component(MYBOT_PLATFORM_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +include("${MYBOT_PLATFORM_ROOT}/src/drivers/display/display.cmake") +set(MYBOT_BOARD_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/board.c" + "${CMAKE_CURRENT_LIST_DIR}/lcd185_hardware.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/io_expander/tca9554.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/input/cst816s_boot_input.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/audio/raw_i2s_audio.c" +) +mybot_display_add_panel(MYBOT_BOARD_SOURCES "st77916/st77916_lcd185_panel.c") +mybot_display_add_lvgl_sources(MYBOT_BOARD_SOURCES "shared_lvgl_adapter.cc") +set(MYBOT_BOARD_REQUIRES + button + esp_driver_gpio + esp_driver_i2c + esp_driver_i2s + esp_driver_ledc + esp_driver_spi + esp_lcd + esp_lcd_st77916 + esp_lcd_touch + esp_lcd_touch_cst816s + esp_lvgl_port + lvgl +) diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board_config.h new file mode 100644 index 0000000..d6a2734 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/board_config.h @@ -0,0 +1,66 @@ +/* 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 "esp32-s3-touch-lcd-1.85" +#define MYBOT_BOARD_HAS_TOUCH 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_I2S_MIC_WS GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_15 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_CAPTURE_RIGHT_SLOT 1 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_47 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_48 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS 1 + +#define MYBOT_LCD185_I2C_PORT I2C_NUM_0 +#define MYBOT_LCD185_I2C_SDA GPIO_NUM_11 +#define MYBOT_LCD185_I2C_SCL GPIO_NUM_10 +#define MYBOT_LCD185_TCA9554_ADDRESS 0x20 +#define MYBOT_LCD185_POWER_CONTROL GPIO_NUM_7 + +#define MYBOT_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_40 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_46 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_45 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_42 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_41 +#define MYBOT_DISPLAY_CS GPIO_NUM_21 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT GPIO_NUM_5 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 360 +#define MYBOT_DISPLAY_HEIGHT 360 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (80 * 1000 * 1000) + +#define MYBOT_TOUCH_I2C_PORT I2C_NUM_1 +#define MYBOT_TOUCH_I2C_SDA GPIO_NUM_1 +#define MYBOT_TOUCH_I2C_SCL GPIO_NUM_3 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_NC +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_4 +#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_BOOT_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/lcd185_hardware.c b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/lcd185_hardware.c new file mode 100644 index 0000000..7057e2c --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/lcd185_hardware.c @@ -0,0 +1,209 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "lcd185_hardware.h" + +#include "board_config.h" +#include "io_expander/tca9554.h" + +#include "driver/gpio.h" +#include "driver/ledc.h" +#include "esp_err.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define TAG "lcd185_hw" +#define BACKLIGHT_LEDC_MODE LEDC_LOW_SPEED_MODE +#define BACKLIGHT_LEDC_TIMER LEDC_TIMER_0 +#define BACKLIGHT_LEDC_CHANNEL LEDC_CHANNEL_0 +#define BACKLIGHT_LEDC_RESOLUTION LEDC_TIMER_13_BIT +#define BACKLIGHT_LEDC_FREQUENCY_HZ 5000 +#define BACKLIGHT_MAX_DUTY ((1U << 13) - 1U) + +typedef struct { + i2c_master_bus_handle_t control_bus; + i2c_master_bus_handle_t touch_bus; + mybot_tca9554_t expander; + bool power_hold_ready; + bool backlight_ready; + bool initialized; +} lcd185_hardware_t; + +static lcd185_hardware_t s_hardware; + +static esp_err_t initialize_power_hold(void) { + const gpio_config_t config = { + .pin_bit_mask = 1ULL << MYBOT_LCD185_POWER_CONTROL, + .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(&config); + if (result == ESP_OK) { + result = gpio_set_level(MYBOT_LCD185_POWER_CONTROL, 1); + } + if (result == ESP_OK) { + s_hardware.power_hold_ready = true; + } + return result; +} + +static esp_err_t initialize_backlight(void) { + const ledc_timer_config_t timer_config = { + .speed_mode = BACKLIGHT_LEDC_MODE, + .duty_resolution = BACKLIGHT_LEDC_RESOLUTION, + .timer_num = BACKLIGHT_LEDC_TIMER, + .freq_hz = BACKLIGHT_LEDC_FREQUENCY_HZ, + .clk_cfg = LEDC_AUTO_CLK, + }; + esp_err_t result = ledc_timer_config(&timer_config); + if (result != ESP_OK) { + return result; + } + const ledc_channel_config_t channel_config = { + .gpio_num = MYBOT_DISPLAY_BACKLIGHT, + .speed_mode = BACKLIGHT_LEDC_MODE, + .channel = BACKLIGHT_LEDC_CHANNEL, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = BACKLIGHT_LEDC_TIMER, + .duty = 0, + .hpoint = 0, + .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD, + }; + result = ledc_channel_config(&channel_config); + if (result == ESP_OK) { + s_hardware.backlight_ready = true; + } + return result; +} + +static esp_err_t initialize_i2c(void) { + const i2c_master_bus_config_t control_config = { + .i2c_port = MYBOT_LCD185_I2C_PORT, + .sda_io_num = MYBOT_LCD185_I2C_SDA, + .scl_io_num = MYBOT_LCD185_I2C_SCL, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, + }; + esp_err_t result = i2c_new_master_bus(&control_config, &s_hardware.control_bus); + const i2c_master_bus_config_t touch_config = { + .i2c_port = MYBOT_TOUCH_I2C_PORT, + .sda_io_num = MYBOT_TOUCH_I2C_SDA, + .scl_io_num = MYBOT_TOUCH_I2C_SCL, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, + }; + if (result == ESP_OK) { + result = i2c_new_master_bus(&touch_config, &s_hardware.touch_bus); + } + return result; +} + +static esp_err_t pulse_panel_resets(void) { + esp_err_t result = + mybot_tca9554_configure_outputs(&s_hardware.expander, 0x03, 0x00) == 0 ? ESP_OK : ESP_FAIL; + if (result == ESP_OK) { + result = + mybot_tca9554_set_levels(&s_hardware.expander, 0x03, 0x03) == 0 ? ESP_OK : ESP_FAIL; + vTaskDelay(pdMS_TO_TICKS(300)); + } + if (result == ESP_OK) { + result = + mybot_tca9554_set_levels(&s_hardware.expander, 0x03, 0x00) == 0 ? ESP_OK : ESP_FAIL; + vTaskDelay(pdMS_TO_TICKS(300)); + } + if (result == ESP_OK) { + result = + mybot_tca9554_set_levels(&s_hardware.expander, 0x03, 0x03) == 0 ? ESP_OK : ESP_FAIL; + } + return result; +} + +static int release_hardware(void) { + int result = 0; + if (s_hardware.backlight_ready && + ledc_stop(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, 0) != ESP_OK) { + result = -1; + } + if (s_hardware.backlight_ready && gpio_reset_pin(MYBOT_DISPLAY_BACKLIGHT) != ESP_OK) { + result = -1; + } + if (s_hardware.power_hold_ready && gpio_set_level(MYBOT_LCD185_POWER_CONTROL, 0) != ESP_OK) { + result = -1; + } + if (mybot_tca9554_deinit(&s_hardware.expander) < 0) { + result = -1; + } + if (s_hardware.touch_bus && i2c_del_master_bus(s_hardware.touch_bus) != ESP_OK) { + result = -1; + } + if (s_hardware.control_bus && i2c_del_master_bus(s_hardware.control_bus) != ESP_OK) { + result = -1; + } + if (result == 0) { + s_hardware = (lcd185_hardware_t){0}; + } else { + s_hardware.initialized = false; + } + return result; +} + +int mybot_lcd185_hardware_init(void) { + if (s_hardware.initialized) { + return 0; + } + if ((s_hardware.control_bus || s_hardware.touch_bus || s_hardware.expander.device || + s_hardware.power_hold_ready || s_hardware.backlight_ready) && + release_hardware() < 0) { + return -1; + } + + esp_err_t result = initialize_power_hold(); + if (result == ESP_OK) { + result = initialize_backlight(); + } + if (result == ESP_OK) { + result = initialize_i2c(); + } + if (result == ESP_OK) { + result = mybot_tca9554_init(&s_hardware.expander, s_hardware.control_bus, + MYBOT_LCD185_TCA9554_ADDRESS, 400000) == 0 + ? ESP_OK + : ESP_FAIL; + } + if (result == ESP_OK) { + result = pulse_panel_resets(); + } + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=hardware action=initialize result=error code=%s", + esp_err_to_name(result)); + (void)release_hardware(); + return -1; + } + + s_hardware.initialized = true; + ESP_LOGI(TAG, "event=hardware action=initialize result=ok"); + return 0; +} + +int mybot_lcd185_hardware_deinit(void) { + return release_hardware(); +} + +i2c_master_bus_handle_t mybot_lcd185_touch_i2c_bus_handle(void) { + return s_hardware.initialized ? s_hardware.touch_bus : NULL; +} + +int mybot_board_set_display_backlight(unsigned int percent) { + if (!s_hardware.initialized || !s_hardware.backlight_ready || percent > 100) { + return -1; + } + uint32_t duty = (BACKLIGHT_MAX_DUTY * percent + 50U) / 100U; + esp_err_t result = ledc_set_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, duty); + if (result == ESP_OK) { + result = ledc_update_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL); + } + return result == ESP_OK ? 0 : -1; +} diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/lcd185_hardware.h b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/lcd185_hardware.h new file mode 100644 index 0000000..db2f4ca --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/lcd185_hardware.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_LCD185_HARDWARE_H_ +#define MYBOT_LCD185_HARDWARE_H_ + +#include "driver/i2c_master.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int mybot_lcd185_hardware_init(void); +int mybot_lcd185_hardware_deinit(void); +i2c_master_bus_handle_t mybot_lcd185_touch_i2c_bus_handle(void); + +#ifdef __cplusplus +} +#endif + +#endif /* MYBOT_LCD185_HARDWARE_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/sdkconfig.defaults new file mode 100644 index 0000000..dda7423 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85/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 diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/README.md b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/README.md new file mode 100644 index 0000000..dbe4ad5 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/README.md @@ -0,0 +1,14 @@ +# ESP32-S3 Touch LCD 1.85B + +板型 ID 为 `esp32-s3-touch-lcd-1.85b`。 + +- 显示:360×360 ST77916 QSPI,SPI2,共享 LVGL UI。 +- 音频:ES8311 与 ES7210,16 kHz mono signed-16 PCM。 +- 输入:CST816S 兼容触摸和 GPIO0 Boot。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 面板复位:GPIO3;背光:GPIO5。 + +该板存在面板版本差异,当前使用 ST77916 默认初始化,厂商版本探测和专用初始化尚未接入。 +触摸型号/坐标、面板时序/颜色、音频和电源仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/board.c b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/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/esp32-s3-touch-lcd-1.85b/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/board.cmake new file mode 100644 index 0000000..5b264b6 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/board.cmake @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "dual") +set(MYBOT_GENERIC_DISPLAY "generic/qspi_st77916_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst816s") +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 + CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_st77916) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/board_config.h new file mode 100644 index 0000000..a75f1ae --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/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 "esp32-s3-touch-lcd-1.85b" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_48 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_47 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_11 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_10 +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_TOUCH_DRIVER_KIND 1 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_1 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_4 +#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_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_40 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_46 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_45 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_42 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_41 +#define MYBOT_DISPLAY_CS GPIO_NUM_21 +#define MYBOT_DISPLAY_RESET GPIO_NUM_3 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_5 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 360 +#define MYBOT_DISPLAY_HEIGHT 360 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/sdkconfig.defaults new file mode 100644 index 0000000..58f71c7 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85b/sdkconfig.defaults @@ -0,0 +1,12 @@ +# 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 +CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/README.md b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/README.md new file mode 100644 index 0000000..5e4f1e5 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/README.md @@ -0,0 +1,14 @@ +# ESP32-S3 Touch LCD 1.85C + +板型 ID 为 `esp32-s3-touch-lcd-1.85c`。 + +- 显示:360×360 ST77916 QSPI,SPI2,共享 LVGL UI。 +- 音频:当前实现 V2 的 ES8311 + ES7210 路径。 +- 输入:CST816S 兼容触摸和 GPIO0 Boot。 +- 扩展:TCA9554 `0x20`,bit0/bit1 用于 LCD/触摸复位。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO5;speaker PA:GPIO15。 + +V1 的 raw I2S 路径、触摸型号/坐标、面板版本初始化、电源和双 codec 音频仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/board.c b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/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/esp32-s3-touch-lcd-1.85c/board.cmake b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/board.cmake new file mode 100644 index 0000000..8bc01f9 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/board.cmake @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "dual") +set(MYBOT_GENERIC_DISPLAY "generic/qspi_st77916_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst816s") +set(MYBOT_GENERIC_TCA9554 ON) +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 + CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_st77916) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/board_config.h b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/board_config.h new file mode 100644 index 0000000..0fa35be --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/board_config.h @@ -0,0 +1,76 @@ +/* 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 "esp32-s3-touch-lcd-1.85c" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_48 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_47 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_11 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_10 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_15 +#define MYBOT_GENERIC_TCA9554_ADDRESS 0x20 +#define MYBOT_GENERIC_TCA9554_OUTPUT_MASK 0x03 +#define MYBOT_GENERIC_TCA9554_RESET_MASK 0x03 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_TOUCH_DRIVER_KIND 1 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_NC +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_4 +#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_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_40 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_46 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_45 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_42 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_41 +#define MYBOT_DISPLAY_CS GPIO_NUM_21 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_5 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 360 +#define MYBOT_DISPLAY_HEIGHT 360 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/sdkconfig.defaults b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/sdkconfig.defaults new file mode 100644 index 0000000..58f71c7 --- /dev/null +++ b/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85c/sdkconfig.defaults @@ -0,0 +1,12 @@ +# 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 +CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/components/mybot_platform/boards/esp32s3-korvo2-v3/README.md b/components/mybot_platform/boards/esp32s3-korvo2-v3/README.md new file mode 100644 index 0000000..49753fa --- /dev/null +++ b/components/mybot_platform/boards/esp32s3-korvo2-v3/README.md @@ -0,0 +1,15 @@ +# ESP32-S3 Korvo2 V3 + +板型 ID 为 `esp32s3-korvo2-v3`。 + +- 显示:320×240 ILI9341,SPI3,TCA9554 控制 LCD CS 与复位。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO5 Boot;ADC 按键尚未实现。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 +- 扩展:TCA9554 `0x20`;GPIO48 扬声器 PA。 + +ADC 按键、摄像头、电池上报、休眠和板级电源管理未实现。TCA9554 复位/CS 时序、ILI9341、 +双 codec 路由和音频仍需真机验证。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/esp32s3-korvo2-v3/board.c b/components/mybot_platform/boards/esp32s3-korvo2-v3/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/esp32s3-korvo2-v3/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/esp32s3-korvo2-v3/board.cmake b/components/mybot_platform/boards/esp32s3-korvo2-v3/board.cmake new file mode 100644 index 0000000..41392ad --- /dev/null +++ b/components/mybot_platform/boards/esp32s3-korvo2-v3/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 "dual") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_TCA9554 ON) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_ili9341) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/esp32s3-korvo2-v3/board_config.h b/components/mybot_platform/boards/esp32s3-korvo2-v3/board_config.h new file mode 100644 index 0000000..bb054cf --- /dev/null +++ b/components/mybot_platform/boards/esp32s3-korvo2-v3/board_config.h @@ -0,0 +1,65 @@ +/* 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 "esp32s3-korvo2-v3" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_8 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_17 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_18 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_48 +#define MYBOT_GENERIC_TCA9554_ADDRESS 0x20 +#define MYBOT_GENERIC_TCA9554_OUTPUT_MASK 0x0F +#define MYBOT_GENERIC_TCA9554_RESET_MASK 0x07 +#define MYBOT_GENERIC_TCA9554_LOW_MASK 0x08 + +#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_5 +#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_PANEL_CONTROLLER 3 +#define MYBOT_ILI9341_PROFILE 0 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_0 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_1 +#define MYBOT_DISPLAY_DC GPIO_NUM_2 +#define MYBOT_DISPLAY_CS GPIO_NUM_NC +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#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 false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/esp32s3-korvo2-v3/sdkconfig.defaults b/components/mybot_platform/boards/esp32s3-korvo2-v3/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/esp32s3-korvo2-v3/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/freenove-esp32s3-display-2.8-lcd/README.md b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/README.md new file mode 100644 index 0000000..197a18d --- /dev/null +++ b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/README.md @@ -0,0 +1,13 @@ +# Freenove ESP32-S3 Display 2.8 LCD + +板型 ID 为 `freenove-esp32s3-display-2.8-lcd`。 + +- 显示:320×240 ILI9341,SPI3,20 MHz,共享 LVGL UI。 +- 音频:ES8311,I2C0 GPIO16/15,GPIO1 speaker PA。 +- 输入:GPIO0 Boot;无触摸和音量键。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO45 LEDC,默认 60%。 + +面板、音频、PA 和按键仍需真机验证。摄像头、LED、电池、休眠和关机未实现。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/board.c b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/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/freenove-esp32s3-display-2.8-lcd/board.cmake b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/board.cmake new file mode 100644 index 0000000..64b1c97 --- /dev/null +++ b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/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 "es8311") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_ili9341) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/board_config.h b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/board_config.h new file mode 100644 index 0000000..ebd2dec --- /dev/null +++ b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/board_config.h @@ -0,0 +1,62 @@ +/* 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 "freenove-esp32s3-display-2.8-lcd" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_7 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_8 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_16 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_15 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_1 + +#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_PANEL_CONTROLLER 3 +#define MYBOT_ILI9341_PROFILE 0 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_11 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_12 +#define MYBOT_DISPLAY_DC GPIO_NUM_46 +#define MYBOT_DISPLAY_CS GPIO_NUM_10 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_45 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#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 false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (20 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_BGR + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/sdkconfig.defaults b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/freenove-esp32s3-display-2.8-lcd/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/genjutech-s3-1.54tft/README.md b/components/mybot_platform/boards/genjutech-s3-1.54tft/README.md new file mode 100644 index 0000000..b873184 --- /dev/null +++ b/components/mybot_platform/boards/genjutech-s3-1.54tft/README.md @@ -0,0 +1,33 @@ +# GenJuTech S3 1.54 TFT + +板型 ID 为 `genjutech-s3-1.54tft`。硬件参数、引脚和能力由本目录的 +[`board_config.h`](board_config.h)、[`board.cmake`](board.cmake)、[`board.c`](board.c) +与 [`sdkconfig.defaults`](sdkconfig.defaults) 定义。 + +| 项目 | 当前实现 | +| --- | --- | +| 存储 | 16 MB QIO Flash profile;80 MHz Octal PSRAM;实际容量待真机确认 | +| 分区 | [`partitions/v2/16m.csv`](../../../../partitions/v2/16m.csv),双 OTA | +| 音频 | ES8311,16 kHz 单声道 PCM;GPIO21 扬声器功放 | +| 显示 | 240×240 ST7789,SPI3 mode 3,80 MHz,颜色反转,共享 LVGL | +| 输入 | GPIO0 Boot、GPIO42 音量加、GPIO41 音量减,均低电平有效 | +| 网络 / 视频 | Wi-Fi;未接入摄像头,不支持视频 | + +| 信号 | GPIO | +| --- | --- | +| I2S MCLK / BCLK / WS / DIN / DOUT | 14 / 13 / 11 / 12 / 10 | +| ES8311 I2C SDA / SCL | 9 / 8 | +| LCD MOSI / SCLK / DC / CS / RESET | 3 / 4 / 5 / 6 / 7 | +| LCD 背光 | 2,LEDC 5 kHz、13-bit,默认 60% | + +屏幕、音频和输入在板级初始化后由进程持有。该板的扬声器使能与显示 I/O 存在板级耦合, +因此首次播放开启 GPIO21 后保持有效,直到板级硬件拆除;停止播放仅让 I2S 流空闲。 + +## 操作 + +- 短按 Boot:开始或结束对话。 +- 短按音量加/减:调节并持久化软件音量。 +- 长按 Boot 3 秒:停止 MyBot 后进入 Wi-Fi 配网。 + +当前未实现电量/充电、LED、触摸、摄像头、休眠、关机或低功耗行为。构建与真机状态见 +[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/genjutech-s3-1.54tft/board.c b/components/mybot_platform/boards/genjutech-s3-1.54tft/board.c new file mode 100644 index 0000000..2797728 --- /dev/null +++ b/components/mybot_platform/boards/genjutech-s3-1.54tft/board.c @@ -0,0 +1,134 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "mybot_platform/board.h" + +#include + +#include "announcement/embedded_ogg_prompt.h" +#include "board_config.h" +#include "esp_log.h" +#include "genjutech_hardware.h" +#include "network/wifi_control.h" +#include "platform/board_actions.h" + +#include + +#define TAG "genjutech_board" + +const mybot_audio_capture_ops_t *mybot_es8311_audio_capture_ops(void); +const mybot_audio_playback_ops_t *mybot_es8311_audio_playback_ops(void); +const mybot_audio_volume_ops_t *mybot_es8311_audio_volume_ops(void); +const mybot_announce_ops_t *mybot_esp32s3_announce_ops(void); +const mybot_https_ops_t *mybot_esp32s3_https_ops(void); +const mybot_key_ops_t *mybot_esp32s3_button_ops(void); +const mybot_kv_store_ops_t *mybot_esp32s3_kv_store_ops(void); +const mybot_lcd_ops_t *mybot_shared_lvgl_ops(void); +const mybot_wifi_ops_t *mybot_esp32s3_wifi_ops(void); +int mybot_esp32s3_buttons_start(void); + +static const mybot_lcd_ops_t *s_lcd_ops; +static void *s_lcd_context; +static bool s_automatic_provisioning_started; +static bool s_prepared; + +static int board_show_screen(mybot_lcd_screen_t screen) { + if (!s_lcd_ops || !s_lcd_context) { + return -1; + } + const mybot_lcd_content_t content = {.screen = screen}; + return s_lcd_ops->render(s_lcd_context, &content); +} + +static void board_on_provisioning(const char *trigger) { + (void)board_show_screen(MYBOT_LCD_SCREEN_WIFI_PROVISIONING); + (void)mybot_embedded_ogg_play_wifi_provisioning(mybot_es8311_audio_playback_ops(), + mybot_es8311_audio_volume_ops(), trigger); +} + +static void board_on_automatic_provisioning(void) { + s_automatic_provisioning_started = true; + board_on_provisioning("automatic"); +} + +static void board_on_button_provisioning(void) { + board_on_provisioning("button"); +} + +static int board_prepare(void) { + if (s_prepared) { + return 0; + } + if (mybot_genjutech_hardware_init() < 0) { + return -1; + } + s_lcd_ops = mybot_shared_lvgl_ops(); + if (!s_lcd_ops || s_lcd_ops->init(&s_lcd_context) < 0 || + board_show_screen(MYBOT_LCD_SCREEN_STARTING) < 0 || mybot_esp32s3_buttons_start() < 0) { + if (s_lcd_context) { + s_lcd_ops->destroy(s_lcd_context); + s_lcd_context = NULL; + } + (void)mybot_genjutech_hardware_deinit(); + return -1; + } + s_prepared = true; + return 0; +} + +int mybot_board_handle_boot_long_press(void) { + mybot_board_request_wifi_provisioning(); + return 0; +} + +static int board_provision_wifi(void) { + return mybot_wifi_run_provisioning(board_on_button_provisioning); +} + +static int board_ensure_network(const char *device_id) { + s_automatic_provisioning_started = false; + int result = mybot_wifi_ensure_network(device_id, board_on_automatic_provisioning); + if (s_automatic_provisioning_started) { + (void)mybot_board_wait_wifi_provisioning_request(0); + } + return result; +} + +static void board_shutdown_network(void) { + mybot_wifi_shutdown_network(); +} + +static int board_register_platform(void) { + const mybot_platform_descriptor_t descriptor = { + .wifi = mybot_esp32s3_wifi_ops(), + .kv_store = mybot_esp32s3_kv_store_ops(), + .key = mybot_esp32s3_button_ops(), + .audio_capture = mybot_es8311_audio_capture_ops(), + .audio_playback = mybot_es8311_audio_playback_ops(), + .audio_volume = mybot_es8311_audio_volume_ops(), + .announce = mybot_esp32s3_announce_ops(), + .https = mybot_esp32s3_https_ops(), + .lcd = mybot_shared_lvgl_ops(), + }; + return mybot_platform_register(&descriptor); +} + +void *mybot_es8311_board_i2c_bus_handle(void) { + return mybot_genjutech_i2c_bus_handle(); +} + +int mybot_es8311_board_set_speaker_power(bool enabled) { + return mybot_genjutech_set_speaker_power(enabled); +} + +static const mybot_board_t s_board = { + .id = MYBOT_BOARD_NAME, + .hw_model = MYBOT_BOARD_NAME, + .prepare = board_prepare, + .register_platform = board_register_platform, + .ensure_network = board_ensure_network, + .provision_wifi = board_provision_wifi, + .shutdown_network = board_shutdown_network, +}; + +const mybot_board_t *mybot_board_get(void) { + return &s_board; +} diff --git a/components/mybot_platform/boards/genjutech-s3-1.54tft/board.cmake b/components/mybot_platform/boards/genjutech-s3-1.54tft/board.cmake new file mode 100644 index 0000000..c38c96d --- /dev/null +++ b/components/mybot_platform/boards/genjutech-s3-1.54tft/board.cmake @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_BOARD_TARGET "esp32s3") +set(MYBOT_BOARD_SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults") +set(MYBOT_BOARD_INCLUDE_DIR "${CMAKE_CURRENT_LIST_DIR}") +set(MYBOT_BOARD_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 +) +set(MYBOT_BOARD_FORBIDDEN_CONFIGS + CONFIG_CODEC_I2C_BACKWARD_COMPATIBLE +) +set(MYBOT_BOARD_PARTITION_TABLE "partitions/v2/16m.csv") + +get_filename_component(MYBOT_PLATFORM_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +include("${MYBOT_PLATFORM_ROOT}/src/drivers/display/display.cmake") +set(MYBOT_BOARD_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/board.c" + "${CMAKE_CURRENT_LIST_DIR}/genjutech_hardware.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/audio/es8311_codec_audio.c" + "${MYBOT_PLATFORM_ROOT}/src/drivers/input/gpio_buttons.c" +) +mybot_display_add_panel(MYBOT_BOARD_SOURCES "st7789/st7789_panel.c") +mybot_display_add_lvgl_sources(MYBOT_BOARD_SOURCES "shared_lvgl_adapter.cc") +set(MYBOT_BOARD_REQUIRES + button + esp_codec_dev + esp_driver_gpio + esp_driver_i2c + esp_driver_i2s + esp_driver_ledc + esp_driver_spi + esp_lcd + esp_lvgl_port + lvgl + nvs_flash +) diff --git a/components/mybot_platform/boards/genjutech-s3-1.54tft/board_config.h b/components/mybot_platform/boards/genjutech-s3-1.54tft/board_config.h new file mode 100644 index 0000000..37310fe --- /dev/null +++ b/components/mybot_platform/boards/genjutech-s3-1.54tft/board_config.h @@ -0,0 +1,54 @@ +/* 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 "genjutech-s3-1.54tft" + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_14 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_13 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_11 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_12 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_10 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 + +#define MYBOT_GENJUTECH_I2C_PORT I2C_NUM_0 +#define MYBOT_GENJUTECH_I2C_SDA GPIO_NUM_9 +#define MYBOT_GENJUTECH_I2C_SCL GPIO_NUM_8 +#define MYBOT_GENJUTECH_I2C_SPEED_HZ 400000 +#define MYBOT_GENJUTECH_SPEAKER_PA GPIO_NUM_21 + +#define MYBOT_ES8311_I2C_PORT MYBOT_GENJUTECH_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_42 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_41 + +#define MYBOT_DISPLAY_MOSI GPIO_NUM_3 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_4 +#define MYBOT_DISPLAY_DC GPIO_NUM_5 +#define MYBOT_DISPLAY_CS GPIO_NUM_6 +#define MYBOT_DISPLAY_RESET GPIO_NUM_7 +#define MYBOT_DISPLAY_BACKLIGHT GPIO_NUM_2 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_SPI_MODE 3 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (80 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/genjutech-s3-1.54tft/genjutech_hardware.c b/components/mybot_platform/boards/genjutech-s3-1.54tft/genjutech_hardware.c new file mode 100644 index 0000000..1b3e2ac --- /dev/null +++ b/components/mybot_platform/boards/genjutech-s3-1.54tft/genjutech_hardware.c @@ -0,0 +1,179 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "genjutech_hardware.h" + +#include "board_config.h" + +#include "driver/gpio.h" +#include "driver/ledc.h" +#include "esp_err.h" +#include "esp_log.h" + +#define TAG "genjutech_hw" +#define BACKLIGHT_LEDC_MODE LEDC_LOW_SPEED_MODE +#define BACKLIGHT_LEDC_TIMER LEDC_TIMER_0 +#define BACKLIGHT_LEDC_CHANNEL LEDC_CHANNEL_0 +#define BACKLIGHT_LEDC_RESOLUTION LEDC_TIMER_13_BIT +#define BACKLIGHT_LEDC_FREQUENCY_HZ 5000 +#define BACKLIGHT_MAX_DUTY ((1U << 13) - 1U) + +typedef struct { + i2c_master_bus_handle_t bus; + bool pa_ready; + bool pa_enabled; + bool backlight_ready; + bool initialized; +} genjutech_hardware_t; + +static genjutech_hardware_t s_hardware; + +static esp_err_t initialize_pa(void) { + const gpio_config_t config = { + .pin_bit_mask = 1ULL << MYBOT_GENJUTECH_SPEAKER_PA, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_ENABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + esp_err_t result = gpio_config(&config); + if (result == ESP_OK) { + result = gpio_set_level(MYBOT_GENJUTECH_SPEAKER_PA, 0); + } + if (result == ESP_OK) { + s_hardware.pa_ready = true; + s_hardware.pa_enabled = false; + } + return result; +} + +static esp_err_t initialize_backlight(void) { + const ledc_timer_config_t timer_config = { + .speed_mode = BACKLIGHT_LEDC_MODE, + .duty_resolution = BACKLIGHT_LEDC_RESOLUTION, + .timer_num = BACKLIGHT_LEDC_TIMER, + .freq_hz = BACKLIGHT_LEDC_FREQUENCY_HZ, + .clk_cfg = LEDC_AUTO_CLK, + }; + esp_err_t result = ledc_timer_config(&timer_config); + if (result != ESP_OK) { + return result; + } + const ledc_channel_config_t channel_config = { + .gpio_num = MYBOT_DISPLAY_BACKLIGHT, + .speed_mode = BACKLIGHT_LEDC_MODE, + .channel = BACKLIGHT_LEDC_CHANNEL, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = BACKLIGHT_LEDC_TIMER, + .duty = 0, + .hpoint = 0, + .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD, + }; + result = ledc_channel_config(&channel_config); + if (result == ESP_OK) { + s_hardware.backlight_ready = true; + } + return result; +} + +static int release_hardware(void) { + int result = 0; + if (s_hardware.backlight_ready) { + if (ledc_stop(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, 0) == ESP_OK) { + esp_err_t reset_result = gpio_reset_pin(MYBOT_DISPLAY_BACKLIGHT); + if (reset_result != ESP_OK) { + result = -1; + } + s_hardware.backlight_ready = false; + } else { + result = -1; + } + } + if (s_hardware.pa_ready && gpio_set_level(MYBOT_GENJUTECH_SPEAKER_PA, 0) != ESP_OK) { + result = -1; + } + if (s_hardware.bus) { + if (i2c_del_master_bus(s_hardware.bus) == ESP_OK) { + s_hardware.bus = NULL; + } else { + result = -1; + } + } + s_hardware.initialized = false; + s_hardware.pa_enabled = false; + return result; +} + +int mybot_genjutech_hardware_init(void) { + if (s_hardware.initialized) { + return 0; + } + if ((s_hardware.bus || s_hardware.pa_ready || s_hardware.backlight_ready) && + release_hardware() < 0) { + return -1; + } + + esp_err_t result = initialize_pa(); + if (result == ESP_OK) { + result = initialize_backlight(); + } + const i2c_master_bus_config_t bus_config = { + .i2c_port = MYBOT_GENJUTECH_I2C_PORT, + .sda_io_num = MYBOT_GENJUTECH_I2C_SDA, + .scl_io_num = MYBOT_GENJUTECH_I2C_SCL, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, + }; + if (result == ESP_OK) { + result = i2c_new_master_bus(&bus_config, &s_hardware.bus); + } + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=hardware action=initialize result=error code=%s", + esp_err_to_name(result)); + (void)release_hardware(); + return -1; + } + + s_hardware.initialized = true; + ESP_LOGI(TAG, "event=hardware action=initialize result=ok speaker=off backlight=off"); + return 0; +} + +int mybot_genjutech_hardware_deinit(void) { + return release_hardware(); +} + +i2c_master_bus_handle_t mybot_genjutech_i2c_bus_handle(void) { + return s_hardware.initialized ? s_hardware.bus : NULL; +} + +int mybot_genjutech_set_speaker_power(bool enabled) { + if (!s_hardware.initialized || !s_hardware.pa_ready) { + return -1; + } + if (s_hardware.pa_enabled == enabled) { + return 0; + } + if (!enabled) { + /* This board couples PA shutdown to display I/O. Keep the amplifier + * enabled until hardware teardown and let the I2S stream go idle. */ + return 0; + } + esp_err_t result = gpio_set_level(MYBOT_GENJUTECH_SPEAKER_PA, enabled ? 1 : 0); + if (result != ESP_OK) { + return -1; + } + s_hardware.pa_enabled = enabled; + return 0; +} + +int mybot_board_set_display_backlight(unsigned int percent) { + if (!s_hardware.initialized || !s_hardware.backlight_ready || percent > 100) { + return -1; + } + uint32_t duty = (BACKLIGHT_MAX_DUTY * percent + 50U) / 100U; + esp_err_t result = ledc_set_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, duty); + if (result == ESP_OK) { + result = ledc_update_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL); + } + return result == ESP_OK ? 0 : -1; +} diff --git a/components/mybot_platform/boards/genjutech-s3-1.54tft/genjutech_hardware.h b/components/mybot_platform/boards/genjutech-s3-1.54tft/genjutech_hardware.h new file mode 100644 index 0000000..e629808 --- /dev/null +++ b/components/mybot_platform/boards/genjutech-s3-1.54tft/genjutech_hardware.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_GENJUTECH_HARDWARE_H_ +#define MYBOT_GENJUTECH_HARDWARE_H_ + +#include "driver/i2c_master.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int mybot_genjutech_hardware_init(void); +int mybot_genjutech_hardware_deinit(void); +i2c_master_bus_handle_t mybot_genjutech_i2c_bus_handle(void); +int mybot_genjutech_set_speaker_power(bool enabled); + +#ifdef __cplusplus +} +#endif + +#endif /* MYBOT_GENJUTECH_HARDWARE_H_ */ diff --git a/components/mybot_platform/boards/genjutech-s3-1.54tft/sdkconfig.defaults b/components/mybot_platform/boards/genjutech-s3-1.54tft/sdkconfig.defaults new file mode 100644 index 0000000..0be593f --- /dev/null +++ b/components/mybot_platform/boards/genjutech-s3-1.54tft/sdkconfig.defaults @@ -0,0 +1,14 @@ +# 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/hu-087/README.md b/components/mybot_platform/boards/hu-087/README.md new file mode 100644 index 0000000..3920e52 --- /dev/null +++ b/components/mybot_platform/boards/hu-087/README.md @@ -0,0 +1,13 @@ +# HU-087 + +板型 ID 为 `hu-087`。当前为 headless profile。 + +- 音频:独立麦克风/扬声器 I2S,16 kHz mono signed-16 PCM。 +- 功放控制:GPIO17,随播放启停。 +- 输入:GPIO18 触摸/按键。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 + +SSD1306 单色屏 UI 未接入;长按被映射为配网请求,原固件的长按音量行为不再保留。 +音频、功放、输入和实际 Flash/PSRAM 仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/hu-087/board.c b/components/mybot_platform/boards/hu-087/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/hu-087/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/hu-087/board.cmake b/components/mybot_platform/boards/hu-087/board.cmake new file mode 100644 index 0000000..14b6647 --- /dev/null +++ b/components/mybot_platform/boards/hu-087/board.cmake @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "raw") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/hu-087/board_config.h b/components/mybot_platform/boards/hu-087/board_config.h new file mode 100644 index 0000000..b91b672 --- /dev/null +++ b/components/mybot_platform/boards/hu-087/board_config.h @@ -0,0 +1,35 @@ +/* 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 "hu-087" +#define MYBOT_BOARD_HAS_DISPLAY 0 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 2 +#define MYBOT_GENERIC_TOUCH_KIND 0 +#define MYBOT_AUDIO_I2S_CONTROL_SPEAKER_POWER 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_MIC_WS GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_15 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_7 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_17 + +#define MYBOT_BOOT_BUTTON_GPIO GPIO_NUM_18 +#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 + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/hu-087/sdkconfig.defaults b/components/mybot_platform/boards/hu-087/sdkconfig.defaults new file mode 100644 index 0000000..214c8f7 --- /dev/null +++ b/components/mybot_platform/boards/hu-087/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/kevin-sp-v4-dev/README.md b/components/mybot_platform/boards/kevin-sp-v4-dev/README.md new file mode 100644 index 0000000..bd5f438 --- /dev/null +++ b/components/mybot_platform/boards/kevin-sp-v4-dev/README.md @@ -0,0 +1,14 @@ +# KEVIN SP V4 Dev + +板型 ID 为 `kevin-sp-v4-dev`。 + +- 显示:240×280 ST7789,SPI3 mode 3,80 MHz,Y 偏移 20,颜色反转,共享 LVGL UI。 +- 音频:ES8311,I2C1 GPIO4/5,GPIO41 speaker PA,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;无触摸和音量键。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO48 LEDC,默认 60%。 + +摄像头、LED、电池、休眠和关机未实现。物理 Flash/PSRAM 容量、I2C1 上的 ES8311、面板方向、 +音频和按键仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/kevin-sp-v4-dev/board.c b/components/mybot_platform/boards/kevin-sp-v4-dev/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/kevin-sp-v4-dev/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/kevin-sp-v4-dev/board.cmake b/components/mybot_platform/boards/kevin-sp-v4-dev/board.cmake new file mode 100644 index 0000000..6f155e4 --- /dev/null +++ b/components/mybot_platform/boards/kevin-sp-v4-dev/board.cmake @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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/kevin-sp-v4-dev/board_config.h b/components/mybot_platform/boards/kevin-sp-v4-dev/board_config.h new file mode 100644 index 0000000..197893a --- /dev/null +++ b/components/mybot_platform/boards/kevin-sp-v4-dev/board_config.h @@ -0,0 +1,60 @@ +/* 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 "kevin-sp-v4-dev" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_42 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_46 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_1 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_2 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_3 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_4 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_5 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_41 + +#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 SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_47 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_21 +#define MYBOT_DISPLAY_DC GPIO_NUM_45 +#define MYBOT_DISPLAY_CS GPIO_NUM_14 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_48 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 280 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 20 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 3 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (80 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/kevin-sp-v4-dev/sdkconfig.defaults b/components/mybot_platform/boards/kevin-sp-v4-dev/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/kevin-sp-v4-dev/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/lichuang-dev/README.md b/components/mybot_platform/boards/lichuang-dev/README.md new file mode 100644 index 0000000..924b09c --- /dev/null +++ b/components/mybot_platform/boards/lichuang-dev/README.md @@ -0,0 +1,15 @@ +# Lichuang Dev + +板型 ID 为 `lichuang-dev`。 + +- 显示:320×240 ST7789,SPI3 mode 2,共享 LVGL UI。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:FT5x06 触摸和 GPIO0 Boot。 +- 扩展:PCA9557 `0x19`,控制显示复位和 speaker PA。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +PCA9557 输出映射、FT5x06 坐标、ST7789 初始化、双 codec 路由、音频和背光仍需真机验证。 +摄像头和电池上报未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/lichuang-dev/board.c b/components/mybot_platform/boards/lichuang-dev/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/lichuang-dev/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/lichuang-dev/board.cmake b/components/mybot_platform/boards/lichuang-dev/board.cmake new file mode 100644 index 0000000..1811316 --- /dev/null +++ b/components/mybot_platform/boards/lichuang-dev/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 "dual") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "ft5x06") +set(MYBOT_GENERIC_PCA9557 ON) +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/lichuang-dev/board_config.h b/components/mybot_platform/boards/lichuang-dev/board_config.h new file mode 100644 index 0000000..5456073 --- /dev/null +++ b/components/mybot_platform/boards/lichuang-dev/board_config.h @@ -0,0 +1,79 @@ +/* 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 "lichuang-dev" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 2 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_14 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_13 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_12 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_45 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_1 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_2 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC +#define MYBOT_GENERIC_PCA9557_ADDRESS 0x19 +#define MYBOT_GENERIC_PCA9557_OUTPUT_MASK 0x03 +#define MYBOT_GENERIC_PCA9557_HIGH_MASK 0x00 +#define MYBOT_GENERIC_PCA9557_LOW_MASK 0x01 +#define MYBOT_GENERIC_PCA9557_PA_MASK 0x02 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_TOUCH_DRIVER_KIND 2 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_NC +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_NC +#define MYBOT_TOUCH_RESET_LEVEL 0 +#define MYBOT_TOUCH_INTERRUPT_LEVEL 0 +#define MYBOT_TOUCH_SWAP_XY 1 +#define MYBOT_TOUCH_MIRROR_X 1 +#define MYBOT_TOUCH_MIRROR_Y 0 +#define MYBOT_TOUCH_X_MAX 240 +#define MYBOT_TOUCH_Y_MAX 320 +#define MYBOT_TOUCH_LONG_PRESS_MS 3000 +#define MYBOT_TOUCH_SHORT_PRESS_MIN_MS 50 +#define MYBOT_TOUCH_POLL_INTERVAL_MS 20 + +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_40 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_41 +#define MYBOT_DISPLAY_DC GPIO_NUM_39 +#define MYBOT_DISPLAY_CS GPIO_NUM_NC +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_42 +#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_SPI_MODE 2 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (80 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/lichuang-dev/sdkconfig.defaults b/components/mybot_platform/boards/lichuang-dev/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/lichuang-dev/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/m5stack-stopwatch/README.md b/components/mybot_platform/boards/m5stack-stopwatch/README.md new file mode 100644 index 0000000..c3f9976 --- /dev/null +++ b/components/mybot_platform/boards/m5stack-stopwatch/README.md @@ -0,0 +1,16 @@ +# M5Stack StopWatch + +板型 ID 为 `m5stack-stopwatch`。这是 466×466 圆屏 profile,使用共享 LVGL 中央安全区布局。 + +- 显示:CO5300 QSPI,466×466,内部亮度控制。 +- 音频:ES8311 `0x18`,16 kHz 单声道 signed-16 PCM。 +- 输入:CST820B 兼容触摸路径、GPIO1 主按键、GPIO2 音量按键。 +- 电源/扩展:M5PM1 `0x6E`、M5IOE1 `0x4F`,负责充电、LCD/codec 电源、复位、 + speaker PA 和电机默认关闭。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +M5PM1/M5IOE1 时序、触摸协议、圆屏裁切、音频和按键行为尚未真机验证。电池上报、振动输出、 +休眠和关机尚未实现。 + +详细引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/m5stack-stopwatch/board.c b/components/mybot_platform/boards/m5stack-stopwatch/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/m5stack-stopwatch/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/m5stack-stopwatch/board.cmake b/components/mybot_platform/boards/m5stack-stopwatch/board.cmake new file mode 100644 index 0000000..380f6fa --- /dev/null +++ b/components/mybot_platform/boards/m5stack-stopwatch/board.cmake @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "co5300/co5300_466_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst816s") +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_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID +) +set(MYBOT_GENERIC_EXTRA_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/m5stack_stopwatch_hardware.cc") +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_co5300 m5ioe1 m5pm1) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/m5stack-stopwatch/board_config.h b/components/mybot_platform/boards/m5stack-stopwatch/board_config.h new file mode 100644 index 0000000..df116c3 --- /dev/null +++ b/components/mybot_platform/boards/m5stack-stopwatch/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 "m5stack-stopwatch" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_DISPLAY_ROUND 1 +#define MYBOT_GENERIC_AUDIO_KIND 1 +#define MYBOT_GENERIC_TOUCH_KIND 1 +#define MYBOT_GENERIC_M5STACK_STOPWATCH 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_18 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_17 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_15 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_21 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_16 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_47 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_48 +#define MYBOT_GENERIC_I2C_SPEED_HZ 100000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#define MYBOT_ES8311_I2C_PORT MYBOT_GENERIC_I2C_PORT +#define MYBOT_ES8311_I2C_ADDRESS 0x18 +#define MYBOT_ES8311_INPUT_GAIN_DB 30.0f +#define MYBOT_ES8311_HARDWARE_VOLUME_SCALE_PERCENT 60 + +#define MYBOT_BOOT_BUTTON_GPIO GPIO_NUM_1 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_2 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_TOUCH_DRIVER_KIND 1 +#define MYBOT_TOUCH_I2C_SPEED_HZ 100000 +#define MYBOT_TOUCH_RESET GPIO_NUM_NC +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_13 +#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_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_40 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_41 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_42 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_46 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_45 +#define MYBOT_DISPLAY_CS GPIO_NUM_39 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_WIDTH 466 +#define MYBOT_DISPLAY_HEIGHT 466 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/m5stack-stopwatch/m5stack_stopwatch_hardware.cc b/components/mybot_platform/boards/m5stack-stopwatch/m5stack_stopwatch_hardware.cc new file mode 100644 index 0000000..f8193a5 --- /dev/null +++ b/components/mybot_platform/boards/m5stack-stopwatch/m5stack_stopwatch_hardware.cc @@ -0,0 +1,127 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "m5stack_stopwatch_hardware.h" + +#include "M5IOE1.h" +#include "M5PM1.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include + +#define TAG "m5stack_stopwatch_hw" + +namespace { +constexpr uint8_t kIoExpanderAddress = 0x4F; +constexpr uint8_t kPowerManagerAddress = 0x6E; +constexpr uint8_t kChargeStatePin = 2; +constexpr uint8_t kChargeCurrentPin = 3; +constexpr uint8_t kLcdPowerPin = 7; +constexpr uint8_t kLcdResetPin = 4; +constexpr uint8_t kTouchResetPin = 3; +constexpr uint8_t kCodecPowerPin = 2; +constexpr uint8_t kSpeakerPaPin = 9; +constexpr uint8_t kMotorPin = 8; + +M5IOE1 *s_io_expander; +M5PM1 *s_power_manager; +i2c_master_bus_handle_t s_bus; +bool s_initialized; + +int set_ioe_output(M5IOE1 &ioe, uint8_t pin, uint8_t level) { + ioe.pinMode(pin, OUTPUT); + if (ioe.setDriveMode(pin, M5IOE1_DRIVE_PUSHPULL) != M5IOE1_OK) { + return -1; + } + m5ioe1_err_t result = M5IOE1_FAIL; + ioe.digitalWriteWithRes(pin, level, &result); + return result == M5IOE1_OK ? 0 : -1; +} + +int initialize_expander() { + s_io_expander = new M5IOE1(); + if (!s_io_expander || s_io_expander->begin(s_bus, kIoExpanderAddress, M5IOE1_I2C_FREQ_100K, + M5IOE1_INT_MODE_POLLING) != M5IOE1_OK) { + return -1; + } + if (set_ioe_output(*s_io_expander, kLcdPowerPin, HIGH) < 0 || + set_ioe_output(*s_io_expander, kCodecPowerPin, HIGH) < 0 || + set_ioe_output(*s_io_expander, kSpeakerPaPin, LOW) < 0 || + set_ioe_output(*s_io_expander, kMotorPin, LOW) < 0 || + set_ioe_output(*s_io_expander, kTouchResetPin, HIGH) < 0 || + set_ioe_output(*s_io_expander, kLcdResetPin, HIGH) < 0) { + return -1; + } + vTaskDelay(pdMS_TO_TICKS(20)); + if (set_ioe_output(*s_io_expander, kLcdResetPin, LOW) < 0) { + return -1; + } + vTaskDelay(pdMS_TO_TICKS(10)); + if (set_ioe_output(*s_io_expander, kLcdResetPin, HIGH) < 0) { + return -1; + } + vTaskDelay(pdMS_TO_TICKS(120)); + return 0; +} + +int initialize_power_manager() { + s_power_manager = new M5PM1(); + if (!s_power_manager || + s_power_manager->begin(s_bus, kPowerManagerAddress, M5PM1_I2C_FREQ_100K) != M5PM1_OK) { + return -1; + } + if (s_power_manager->setChargeEnable(true) != M5PM1_OK || + s_power_manager->setBoostEnable(true) != M5PM1_OK) { + return -1; + } + s_power_manager->pinMode(kChargeStatePin, INPUT); + s_power_manager->pinMode(kChargeCurrentPin, OUTPUT); + s_power_manager->digitalWrite(kChargeCurrentPin, LOW); + return 0; +} +} // namespace + +int mybot_m5stack_stopwatch_hardware_init(i2c_master_bus_handle_t bus) { + if (s_initialized || !bus) { + return -1; + } + s_bus = bus; + if (initialize_expander() < 0 || initialize_power_manager() < 0) { + ESP_LOGE(TAG, "event=hardware action=initialize result=error"); + (void)mybot_m5stack_stopwatch_hardware_deinit(); + return -1; + } + s_initialized = true; + return 0; +} + +int mybot_m5stack_stopwatch_set_speaker_pa(bool enabled) { + if (!s_initialized || !s_io_expander) { + return -1; + } + m5ioe1_err_t result = M5IOE1_FAIL; + s_io_expander->digitalWriteWithRes(kSpeakerPaPin, enabled ? HIGH : LOW, &result); + return result == M5IOE1_OK ? 0 : -1; +} + +int mybot_m5stack_stopwatch_hardware_deinit(void) { + int result = 0; + if (s_io_expander) { + m5ioe1_err_t io_result = M5IOE1_FAIL; + s_io_expander->digitalWriteWithRes(kSpeakerPaPin, LOW, &io_result); + if (io_result != M5IOE1_OK) { + result = -1; + } + s_io_expander->digitalWriteWithRes(kMotorPin, LOW, &io_result); + if (io_result != M5IOE1_OK) { + result = -1; + } + } + delete s_io_expander; + s_io_expander = nullptr; + delete s_power_manager; + s_power_manager = nullptr; + s_bus = nullptr; + s_initialized = false; + return result; +} diff --git a/components/mybot_platform/boards/m5stack-stopwatch/m5stack_stopwatch_hardware.h b/components/mybot_platform/boards/m5stack-stopwatch/m5stack_stopwatch_hardware.h new file mode 100644 index 0000000..463152a --- /dev/null +++ b/components/mybot_platform/boards/m5stack-stopwatch/m5stack_stopwatch_hardware.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_M5STACK_STOPWATCH_HARDWARE_H_ +#define MYBOT_M5STACK_STOPWATCH_HARDWARE_H_ + +#include "driver/i2c_master.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int mybot_m5stack_stopwatch_hardware_init(i2c_master_bus_handle_t bus); +int mybot_m5stack_stopwatch_set_speaker_pa(bool enabled); +int mybot_m5stack_stopwatch_hardware_deinit(void); + +#ifdef __cplusplus +} +#endif + +#endif /* MYBOT_M5STACK_STOPWATCH_HARDWARE_H_ */ diff --git a/components/mybot_platform/boards/m5stack-stopwatch/sdkconfig.defaults b/components/mybot_platform/boards/m5stack-stopwatch/sdkconfig.defaults new file mode 100644 index 0000000..68a9fd4 --- /dev/null +++ b/components/mybot_platform/boards/m5stack-stopwatch/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_I2C_BACKWARD_COMPATIBLE=n +CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/components/mybot_platform/boards/magiclick-2p4/README.md b/components/mybot_platform/boards/magiclick-2p4/README.md new file mode 100644 index 0000000..b52e1d9 --- /dev/null +++ b/components/mybot_platform/boards/magiclick-2p4/README.md @@ -0,0 +1,13 @@ +# Magiclick 2.4 + +板型 ID 为 `magiclick-2p4`。 + +- 显示:128×128 NV3023,SPI3 mode 0,40 MHz,共享紧凑 LVGL 布局。 +- 音频:ES8311,I2C0 GPIO5/6,GPIO4 speaker PA。 +- 输入:GPIO21 主按键、GPIO47 音量加、GPIO0 音量减。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO13 LEDC,默认 60%。 + +电池管理、LED、休眠和关机未实现;面板、PA、音频和按键仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/magiclick-2p4/board.c b/components/mybot_platform/boards/magiclick-2p4/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/magiclick-2p4/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/magiclick-2p4/board.cmake b/components/mybot_platform/boards/magiclick-2p4/board.cmake new file mode 100644 index 0000000..9919751 --- /dev/null +++ b/components/mybot_platform/boards/magiclick-2p4/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 "es8311") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_nv3023) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/magiclick-2p4/board_config.h b/components/mybot_platform/boards/magiclick-2p4/board_config.h new file mode 100644 index 0000000..804ab98 --- /dev/null +++ b/components/mybot_platform/boards/magiclick-2p4/board_config.h @@ -0,0 +1,61 @@ +/* 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 "magiclick-2p4" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_8 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_11 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_12 + +#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_4 + +#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_21 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_47 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_PANEL_CONTROLLER 2 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_15 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_16 +#define MYBOT_DISPLAY_DC GPIO_NUM_18 +#define MYBOT_DISPLAY_CS GPIO_NUM_17 +#define MYBOT_DISPLAY_RESET GPIO_NUM_14 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_13 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 128 +#define MYBOT_DISPLAY_HEIGHT 128 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#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/magiclick-2p4/sdkconfig.defaults b/components/mybot_platform/boards/magiclick-2p4/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/magiclick-2p4/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/mixgo-nova/README.md b/components/mybot_platform/boards/mixgo-nova/README.md new file mode 100644 index 0000000..9e3494b --- /dev/null +++ b/components/mybot_platform/boards/mixgo-nova/README.md @@ -0,0 +1,14 @@ +# MIXGO Nova + +板型 ID 为 `mixgo-nova`。 + +- 显示:128×160 ST7789,SPI3,共享 LVGL 紧凑布局。 +- 音频:ES8374 `0x20`,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;无触摸和音量键。 +- 存储:8 MB QIO Flash profile、Quad PSRAM、双 OTA 分区。 +- 背光:GPIO14 LEDC,默认 60%。 + +ES8374、ST7789 偏移/方向、背光、音频和电源仍需真机验证;RGB LED 未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/mixgo-nova/board.c b/components/mybot_platform/boards/mixgo-nova/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/mixgo-nova/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/mixgo-nova/board.cmake b/components/mybot_platform/boards/mixgo-nova/board.cmake new file mode 100644 index 0000000..933e8cd --- /dev/null +++ b/components/mybot_platform/boards/mixgo-nova/board.cmake @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/8m.csv") +set(MYBOT_GENERIC_AUDIO "es8374") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_8MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_QUAD + CONFIG_SPIRAM_SPEED_80M +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/mixgo-nova/board_config.h b/components/mybot_platform/boards/mixgo-nova/board_config.h new file mode 100644 index 0000000..fcd45e6 --- /dev/null +++ b/components/mybot_platform/boards/mixgo-nova/board_config.h @@ -0,0 +1,60 @@ +/* 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 "mixgo-nova" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 4 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_35 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_34 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_47 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_33 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_48 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_37 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_36 +#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 0x20 +#define MYBOT_ES8311_INPUT_GAIN_DB 18.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 SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_40 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_41 +#define MYBOT_DISPLAY_DC GPIO_NUM_18 +#define MYBOT_DISPLAY_CS GPIO_NUM_45 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_14 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 128 +#define MYBOT_DISPLAY_HEIGHT 160 +#define MYBOT_DISPLAY_OFFSET_X 2 +#define MYBOT_DISPLAY_OFFSET_Y 1 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_SPI_MODE 0 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/mixgo-nova/sdkconfig.defaults b/components/mybot_platform/boards/mixgo-nova/sdkconfig.defaults new file mode 100644 index 0000000..b2445ea --- /dev/null +++ b/components/mybot_platform/boards/mixgo-nova/sdkconfig.defaults @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_QUAD=y +CONFIG_SPIRAM_SPEED_80M=y diff --git a/components/mybot_platform/boards/movecall-cuican-esp32s3/README.md b/components/mybot_platform/boards/movecall-cuican-esp32s3/README.md new file mode 100644 index 0000000..5a36101 --- /dev/null +++ b/components/mybot_platform/boards/movecall-cuican-esp32s3/README.md @@ -0,0 +1,13 @@ +# Movecall CuiCan ESP32-S3 + +板型 ID 为 `movecall-cuican-esp32s3`。 + +- 显示:240×240 GC9A01,SPI3,40 MHz,共享 LVGL UI。 +- 音频:ES8311,I2C0 GPIO6/7,GPIO17 speaker PA。 +- 输入:GPIO0 Boot;无触摸和音量键。 +- 存储:8 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO16 LEDC,默认 60%。 + +面板、音频、PA、按键和 8 MB 配置仍需真机验证;LED、电池、休眠和关机未实现。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/movecall-cuican-esp32s3/board.c b/components/mybot_platform/boards/movecall-cuican-esp32s3/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/movecall-cuican-esp32s3/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/movecall-cuican-esp32s3/board.cmake b/components/mybot_platform/boards/movecall-cuican-esp32s3/board.cmake new file mode 100644 index 0000000..70d4a4d --- /dev/null +++ b/components/mybot_platform/boards/movecall-cuican-esp32s3/board.cmake @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/8m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_REQUIRED_CONFIGS + CONFIG_PARTITION_TABLE_CUSTOM + CONFIG_ESPTOOLPY_FLASHSIZE_8MB + CONFIG_ESPTOOLPY_FLASHMODE_QIO + CONFIG_SPIRAM + CONFIG_SPIRAM_MODE_OCT + CONFIG_SPIRAM_SPEED_80M + CONFIG_CODEC_ES8311_SUPPORT +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_gc9a01) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/movecall-cuican-esp32s3/board_config.h b/components/mybot_platform/boards/movecall-cuican-esp32s3/board_config.h new file mode 100644 index 0000000..bccc3f7 --- /dev/null +++ b/components/mybot_platform/boards/movecall-cuican-esp32s3/board_config.h @@ -0,0 +1,61 @@ +/* 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 "movecall-cuican-esp32s3" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_39 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_41 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_40 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_42 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_6 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_7 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_17 + +#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_PANEL_CONTROLLER 1 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_10 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_12 +#define MYBOT_DISPLAY_DC GPIO_NUM_14 +#define MYBOT_DISPLAY_CS GPIO_NUM_13 +#define MYBOT_DISPLAY_RESET GPIO_NUM_11 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_16 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#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/movecall-cuican-esp32s3/sdkconfig.defaults b/components/mybot_platform/boards/movecall-cuican-esp32s3/sdkconfig.defaults new file mode 100644 index 0000000..63ebd39 --- /dev/null +++ b/components/mybot_platform/boards/movecall-cuican-esp32s3/sdkconfig.defaults @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/8m.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=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/movecall-moji-esp32s3/README.md b/components/mybot_platform/boards/movecall-moji-esp32s3/README.md new file mode 100644 index 0000000..e7d5ed5 --- /dev/null +++ b/components/mybot_platform/boards/movecall-moji-esp32s3/README.md @@ -0,0 +1,13 @@ +# Movecall Moji ESP32-S3 + +板型 ID 为 `movecall-moji-esp32s3`。 + +- 显示:240×240 GC9A01,SPI3,40 MHz,共享 LVGL UI。 +- 音频:ES8311,I2C0 GPIO5/4,GPIO9 speaker PA。 +- 输入:GPIO0 Boot;无触摸和音量键。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO3 LEDC,默认 60%。 + +面板、音频、PA、按键和电源仍需真机验证;LED、电池、休眠和关机未实现。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/movecall-moji-esp32s3/board.c b/components/mybot_platform/boards/movecall-moji-esp32s3/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/movecall-moji-esp32s3/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/movecall-moji-esp32s3/board.cmake b/components/mybot_platform/boards/movecall-moji-esp32s3/board.cmake new file mode 100644 index 0000000..87663cf --- /dev/null +++ b/components/mybot_platform/boards/movecall-moji-esp32s3/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 "es8311") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_gc9a01) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/movecall-moji-esp32s3/board_config.h b/components/mybot_platform/boards/movecall-moji-esp32s3/board_config.h new file mode 100644 index 0000000..8063f3d --- /dev/null +++ b/components/mybot_platform/boards/movecall-moji-esp32s3/board_config.h @@ -0,0 +1,61 @@ +/* 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 "movecall-moji-esp32s3" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_14 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_12 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_13 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_11 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_5 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_4 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_9 + +#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_PANEL_CONTROLLER 1 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_17 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_16 +#define MYBOT_DISPLAY_DC GPIO_NUM_7 +#define MYBOT_DISPLAY_CS GPIO_NUM_15 +#define MYBOT_DISPLAY_RESET GPIO_NUM_18 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_3 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#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/movecall-moji-esp32s3/sdkconfig.defaults b/components/mybot_platform/boards/movecall-moji-esp32s3/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/movecall-moji-esp32s3/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/nulllab-ai-vox-v3/README.md b/components/mybot_platform/boards/nulllab-ai-vox-v3/README.md new file mode 100644 index 0000000..8c2345e --- /dev/null +++ b/components/mybot_platform/boards/nulllab-ai-vox-v3/README.md @@ -0,0 +1,13 @@ +# NullLab AI Vox V3 + +板型 ID 为 `nulllab-ai-vox-v3`。 + +- 显示:240×240 ST7789,SPI3 mode 0,40 MHz,共享 LVGL UI。 +- 音频:ES8311,MCLK GPIO11、BCLK GPIO10、WS GPIO8、DIN GPIO9、DOUT GPIO7。 +- 输入:GPIO0 Boot、GPIO46 音量加、GPIO45 音量减。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO16 LEDC,默认 60%。 + +当前只实现 Wi-Fi 子集;ML307 蜂窝网络未接入。面板、音频、PA、电池和按键仍需真机验证。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/nulllab-ai-vox-v3/board.c b/components/mybot_platform/boards/nulllab-ai-vox-v3/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/nulllab-ai-vox-v3/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/nulllab-ai-vox-v3/board.cmake b/components/mybot_platform/boards/nulllab-ai-vox-v3/board.cmake new file mode 100644 index 0000000..6f155e4 --- /dev/null +++ b/components/mybot_platform/boards/nulllab-ai-vox-v3/board.cmake @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "es8311") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +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/nulllab-ai-vox-v3/board_config.h b/components/mybot_platform/boards/nulllab-ai-vox-v3/board_config.h new file mode 100644 index 0000000..425cd41 --- /dev/null +++ b/components/mybot_platform/boards/nulllab-ai-vox-v3/board_config.h @@ -0,0 +1,60 @@ +/* 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 "nulllab-ai-vox-v3" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_11 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_8 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_7 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_13 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_12 +#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_46 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_45 +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_21 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_17 +#define MYBOT_DISPLAY_DC GPIO_NUM_14 +#define MYBOT_DISPLAY_CS GPIO_NUM_15 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_16 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#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_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/nulllab-ai-vox-v3/sdkconfig.defaults b/components/mybot_platform/boards/nulllab-ai-vox-v3/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/nulllab-ai-vox-v3/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/quandong-s3-dev/README.md b/components/mybot_platform/boards/quandong-s3-dev/README.md new file mode 100644 index 0000000..254ece8 --- /dev/null +++ b/components/mybot_platform/boards/quandong-s3-dev/README.md @@ -0,0 +1,14 @@ +# Quandong S3 Dev + +板型 ID 为 `quandong-s3-dev`。 + +- 显示:320×240 ILI9341,SPI2,共享 LVGL UI。 +- 音频:ES8311,16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;无触摸和音量键。 +- 扩展:PCA9557 `0x19`;GPIO1 为低有效 PA 使能。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +PCA9557 输出映射、ILI9341 厂商初始化、PA 极性和时序、音频及背光仍需真机验证。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/quandong-s3-dev/board.c b/components/mybot_platform/boards/quandong-s3-dev/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/quandong-s3-dev/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/quandong-s3-dev/board.cmake b/components/mybot_platform/boards/quandong-s3-dev/board.cmake new file mode 100644 index 0000000..538257c --- /dev/null +++ b/components/mybot_platform/boards/quandong-s3-dev/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/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_PCA9557 ON) +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 +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_ili9341) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/quandong-s3-dev/board_config.h b/components/mybot_platform/boards/quandong-s3-dev/board_config.h new file mode 100644 index 0000000..8855c4e --- /dev/null +++ b/components/mybot_platform/boards/quandong-s3-dev/board_config.h @@ -0,0 +1,67 @@ +/* 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 "quandong-s3-dev" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_7 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_8 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_16 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_15 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_1 +#define MYBOT_GENERIC_PA_ACTIVE_LEVEL 0 +#define MYBOT_GENERIC_PCA9557_ADDRESS 0x19 +#define MYBOT_GENERIC_PCA9557_OUTPUT_MASK 0xF8 +#define MYBOT_GENERIC_PCA9557_HIGH_MASK 0x00 + +#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_PANEL_CONTROLLER 3 +#define MYBOT_ILI9341_PROFILE 2 +#define MYBOT_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_11 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_12 +#define MYBOT_DISPLAY_DC GPIO_NUM_46 +#define MYBOT_DISPLAY_CS GPIO_NUM_10 +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_RESET_ACTIVE_HIGH 1 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_45 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#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 false +#define MYBOT_DISPLAY_MIRROR_Y true +#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_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/quandong-s3-dev/sdkconfig.defaults b/components/mybot_platform/boards/quandong-s3-dev/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/quandong-s3-dev/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/rymcu-bigsmart/README.md b/components/mybot_platform/boards/rymcu-bigsmart/README.md new file mode 100644 index 0000000..e48fbec --- /dev/null +++ b/components/mybot_platform/boards/rymcu-bigsmart/README.md @@ -0,0 +1,15 @@ +# RYMCU BigSmart + +板型 ID 为 `rymcu-bigsmart`。 + +- 显示:320×240 ST7789,SPI3 mode 2,共享 LVGL UI。 +- 音频:ES8311 输出与 ES7210 采集,16 kHz 单声道 signed-16 PCM。 +- 输入:GT911 触摸和 GPIO0 Boot。 +- 扩展:PCA9557 `0x19`,控制显示复位和 speaker PA。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +PCA9557 输出映射、GT911 坐标、ST7789 初始化、双 codec 路由、音频和电源仍需真机验证。 +摄像头和电池上报未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/rymcu-bigsmart/board.c b/components/mybot_platform/boards/rymcu-bigsmart/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/rymcu-bigsmart/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/rymcu-bigsmart/board.cmake b/components/mybot_platform/boards/rymcu-bigsmart/board.cmake new file mode 100644 index 0000000..73f50ff --- /dev/null +++ b/components/mybot_platform/boards/rymcu-bigsmart/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 "dual") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "gt911") +set(MYBOT_GENERIC_PCA9557 ON) +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/rymcu-bigsmart/board_config.h b/components/mybot_platform/boards/rymcu-bigsmart/board_config.h new file mode 100644 index 0000000..2df20ea --- /dev/null +++ b/components/mybot_platform/boards/rymcu-bigsmart/board_config.h @@ -0,0 +1,79 @@ +/* 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 "rymcu-bigsmart" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 3 +#define MYBOT_GENERIC_TOUCH_KIND 3 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_38 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_14 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_13 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_12 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_45 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_1 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_2 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC +#define MYBOT_GENERIC_PCA9557_ADDRESS 0x19 +#define MYBOT_GENERIC_PCA9557_OUTPUT_MASK 0x03 +#define MYBOT_GENERIC_PCA9557_HIGH_MASK 0x00 +#define MYBOT_GENERIC_PCA9557_LOW_MASK 0x01 +#define MYBOT_GENERIC_PCA9557_PA_MASK 0x02 + +#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_NC +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_NC +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 + +#define MYBOT_TOUCH_DRIVER_KIND 3 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_NC +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_NC +#define MYBOT_TOUCH_RESET_LEVEL 0 +#define MYBOT_TOUCH_INTERRUPT_LEVEL 0 +#define MYBOT_TOUCH_SWAP_XY 1 +#define MYBOT_TOUCH_MIRROR_X 1 +#define MYBOT_TOUCH_MIRROR_Y 0 +#define MYBOT_TOUCH_X_MAX 240 +#define MYBOT_TOUCH_Y_MAX 320 +#define MYBOT_TOUCH_LONG_PRESS_MS 3000 +#define MYBOT_TOUCH_SHORT_PRESS_MIN_MS 50 +#define MYBOT_TOUCH_POLL_INTERVAL_MS 20 + +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_40 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_41 +#define MYBOT_DISPLAY_DC GPIO_NUM_39 +#define MYBOT_DISPLAY_CS GPIO_NUM_NC +#define MYBOT_DISPLAY_RESET GPIO_NUM_NC +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_42 +#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_SPI_MODE 2 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (80 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/rymcu-bigsmart/sdkconfig.defaults b/components/mybot_platform/boards/rymcu-bigsmart/sdkconfig.defaults new file mode 100644 index 0000000..597a395 --- /dev/null +++ b/components/mybot_platform/boards/rymcu-bigsmart/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/sp-esp32-s3-1.28-box/README.md b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/README.md new file mode 100644 index 0000000..a91d746 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/README.md @@ -0,0 +1,15 @@ +# SP ESP32-S3 1.28 Box + +板型 ID 为 `sp-esp32-s3-1.28-box`。 + +- 显示:240×240 圆屏 GC9A01,SPI3,共享 LVGL 圆屏安全区布局。 +- 音频:ES8311,16 kHz 单声道 signed-16 PCM。 +- 输入:CST816S 兼容触摸、GPIO0 Boot;触摸使用独立 I2C1 GPIO11/7。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 +- 背光:GPIO42 LEDC,默认 60%。 + +触摸复位/中断、面板厂商初始化、颜色、方向、背光、音频和电源仍需真机验证。电池/充电上报 +未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/sp-esp32-s3-1.28-box/board.c b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.28-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/sp-esp32-s3-1.28-box/board.cmake b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/board.cmake new file mode 100644 index 0000000..3987162 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/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 "es8311") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst816s") +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_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_gc9a01) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/sp-esp32-s3-1.28-box/board_config.h b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/board_config.h new file mode 100644 index 0000000..5768f36 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/board_config.h @@ -0,0 +1,78 @@ +/* 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 "sp-esp32-s3-1.28-box" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_DISPLAY_ROUND 1 +#define MYBOT_GENERIC_AUDIO_KIND 1 +#define MYBOT_GENERIC_TOUCH_KIND 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_8 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_15 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_14 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 + +#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_GENERIC_TOUCH_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_TOUCH_I2C_SDA GPIO_NUM_11 +#define MYBOT_GENERIC_TOUCH_I2C_SCL GPIO_NUM_7 +#define MYBOT_TOUCH_DRIVER_KIND 1 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_6 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_12 +#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 1 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_2 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_4 +#define MYBOT_DISPLAY_DC GPIO_NUM_47 +#define MYBOT_DISPLAY_CS GPIO_NUM_5 +#define MYBOT_DISPLAY_RESET GPIO_NUM_38 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_42 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 1 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#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/sp-esp32-s3-1.28-box/sdkconfig.defaults b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/sdkconfig.defaults new file mode 100644 index 0000000..68a9fd4 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.28-box/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_I2C_BACKWARD_COMPATIBLE=n +CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/README.md b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/README.md new file mode 100644 index 0000000..b83dc28 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/README.md @@ -0,0 +1,13 @@ +# SP ESP32-S3 1.54 Muma + +板型 ID 为 `sp-esp32-s3-1.54-muma`。 + +- 显示:240×240 ST7789,SPI3 mode 0,40 MHz,共享 LVGL UI。 +- 音频:ES8311,I2C0 GPIO15/14,GPIO46 speaker PA。 +- 输入:CST816S 兼容触摸和 GPIO0 Boot;触摸使用独立 I2C1 GPIO11/7。 +- 存储:16 MB QIO Flash、80 MHz Octal PSRAM、双 OTA。 +- 背光:GPIO42,低有效 PWM。 + +面板 vendor 调优、触摸、音频、电池检测、电源保持和按键仍需真机验证;休眠和关机未实现。 + +公共构建见[板卡索引](../README.md),验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/board.c b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/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/sp-esp32-s3-1.54-muma/board.cmake b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/board.cmake new file mode 100644 index 0000000..e47b122 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/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 "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst816s") +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_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/board_config.h b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/board_config.h new file mode 100644 index 0000000..eae40eb --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/board_config.h @@ -0,0 +1,76 @@ +/* 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 "sp-esp32-s3-1.54-muma" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 1 +#define MYBOT_GENERIC_TOUCH_KIND 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_9 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_10 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_8 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_15 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_14 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_46 + +#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_GENERIC_TOUCH_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_TOUCH_I2C_SDA GPIO_NUM_11 +#define MYBOT_GENERIC_TOUCH_I2C_SCL GPIO_NUM_7 +#define MYBOT_TOUCH_DRIVER_KIND 1 +#define MYBOT_TOUCH_I2C_SPEED_HZ 100000 +#define MYBOT_TOUCH_RESET GPIO_NUM_6 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_12 +#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_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_2 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_4 +#define MYBOT_DISPLAY_DC GPIO_NUM_47 +#define MYBOT_DISPLAY_CS GPIO_NUM_5 +#define MYBOT_DISPLAY_RESET GPIO_NUM_38 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_42 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 1 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#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_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/sdkconfig.defaults b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/sdkconfig.defaults new file mode 100644 index 0000000..68a9fd4 --- /dev/null +++ b/components/mybot_platform/boards/sp-esp32-s3-1.54-muma/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_I2C_BACKWARD_COMPATIBLE=n +CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/components/mybot_platform/boards/taiji-pi-s3/README.md b/components/mybot_platform/boards/taiji-pi-s3/README.md new file mode 100644 index 0000000..cb913bf --- /dev/null +++ b/components/mybot_platform/boards/taiji-pi-s3/README.md @@ -0,0 +1,15 @@ +# Taiji Pi S3 + +板型 ID 为 `taiji-pi-s3`。 + +- 显示:360×360 圆屏 ST77916 QSPI,共享 LVGL 圆屏安全区布局。 +- 音频:I2S 播放和 PDM 采集;PDM 解调后直接提供 16 kHz 单声道 signed-16 PCM。 +- 输入:CST816S 触摸和 GPIO0 Boot;触摸使用独立 I2C1 GPIO7/8。 +- 静音/PA:GPIO48 低有效,随播放启停。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +ST77916 默认初始化、PDM 时钟/增益、I2S 播放、mute/PA、触摸、背光和板级电源仍需真机验证。 +完整板级电源时序尚未实现。 + +引脚见 [board_config.h](board_config.h);公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/taiji-pi-s3/board.c b/components/mybot_platform/boards/taiji-pi-s3/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/taiji-pi-s3/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/taiji-pi-s3/board.cmake b/components/mybot_platform/boards/taiji-pi-s3/board.cmake new file mode 100644 index 0000000..db9a1b1 --- /dev/null +++ b/components/mybot_platform/boards/taiji-pi-s3/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 "raw") +set(MYBOT_GENERIC_DISPLAY "generic/qspi_st77916_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_TOUCH_DRIVER "cst816s") +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_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID +) +set(MYBOT_GENERIC_EXTRA_REQUIRES esp_lcd_st77916) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/taiji-pi-s3/board_config.h b/components/mybot_platform/boards/taiji-pi-s3/board_config.h new file mode 100644 index 0000000..41b2d00 --- /dev/null +++ b/components/mybot_platform/boards/taiji-pi-s3/board_config.h @@ -0,0 +1,77 @@ +/* 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 "taiji-pi-s3" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_DISPLAY_ROUND 1 +#define MYBOT_GENERIC_AUDIO_KIND 2 +#define MYBOT_GENERIC_TOUCH_KIND 1 +#define MYBOT_AUDIO_I2S_CAPTURE_PDM 1 +#define MYBOT_AUDIO_I2S_CONTROL_SPEAKER_POWER 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 80 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_42 +#define MYBOT_AUDIO_I2S_MIC_WS GPIO_NUM_45 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_46 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_18 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_16 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_17 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_48 +#define MYBOT_GENERIC_PA_ACTIVE_LEVEL 0 + +#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_GENERIC_TOUCH_I2C_PORT I2C_NUM_1 +#define MYBOT_GENERIC_TOUCH_I2C_SDA GPIO_NUM_7 +#define MYBOT_GENERIC_TOUCH_I2C_SCL GPIO_NUM_8 +#define MYBOT_TOUCH_DRIVER_KIND 1 +#define MYBOT_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_40 +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_41 +#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_DISPLAY_SPI_HOST SPI2_HOST +#define MYBOT_DISPLAY_PCLK GPIO_NUM_9 +#define MYBOT_DISPLAY_DATA0 GPIO_NUM_11 +#define MYBOT_DISPLAY_DATA1 GPIO_NUM_12 +#define MYBOT_DISPLAY_DATA2 GPIO_NUM_13 +#define MYBOT_DISPLAY_DATA3 GPIO_NUM_14 +#define MYBOT_DISPLAY_CS GPIO_NUM_10 +#define MYBOT_DISPLAY_RESET GPIO_NUM_47 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_15 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 360 +#define MYBOT_DISPLAY_HEIGHT 360 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X true +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (40 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/taiji-pi-s3/sdkconfig.defaults b/components/mybot_platform/boards/taiji-pi-s3/sdkconfig.defaults new file mode 100644 index 0000000..24f50e9 --- /dev/null +++ b/components/mybot_platform/boards/taiji-pi-s3/sdkconfig.defaults @@ -0,0 +1,9 @@ +# 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_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/components/mybot_platform/boards/xingzhi-abs-2.0/README.md b/components/mybot_platform/boards/xingzhi-abs-2.0/README.md new file mode 100644 index 0000000..5c0e4ab --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-abs-2.0/README.md @@ -0,0 +1,15 @@ +# 行者 ABS 2.0 + +板型 ID 为 `xingzhi-abs-2.0`。当前只实现 Wi-Fi 子集,不包含蜂窝网络。 + +- 显示:240×240 ST7789,SPI3 mode 3,共享 LVGL UI。 +- 音频:ES8311,16 kHz 单声道 signed-16 PCM;I2C0 GPIO41/42。 +- 输入:GPIO0 Boot、GPIO45 音量加、GPIO2 音量减。 +- 扩展:PCA9557 `0x19`;GPIO47 进程级电源保持。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +SD 卡、振动、蜂窝、电池/充电、扩展电源路径、休眠和关机未实现。PCA9557、PA、面板、触摸 +能力与上电顺序仍需真机验证。 + +引脚以 [board_config.h](board_config.h) 为准;公共构建见[板卡索引](../README.md), +验证状态见[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/xingzhi-abs-2.0/board.c b/components/mybot_platform/boards/xingzhi-abs-2.0/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-abs-2.0/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/xingzhi-abs-2.0/board.cmake b/components/mybot_platform/boards/xingzhi-abs-2.0/board.cmake new file mode 100644 index 0000000..e4214da --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-abs-2.0/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 "es8311") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_PCA9557 ON) +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/xingzhi-abs-2.0/board_config.h b/components/mybot_platform/boards/xingzhi-abs-2.0/board_config.h new file mode 100644 index 0000000..98ad26b --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-abs-2.0/board_config.h @@ -0,0 +1,63 @@ +/* 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 "xingzhi-abs-2.0" +#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_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_17 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_15 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_41 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_42 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_21 +#define MYBOT_GENERIC_POWER_GPIO GPIO_NUM_47 +#define MYBOT_GENERIC_PCA9557_ADDRESS 0x19 +#define MYBOT_GENERIC_PCA9557_OUTPUT_MASK 0x00 +#define MYBOT_GENERIC_PCA9557_HIGH_MASK 0x00 + +#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_45 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_2 + +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_10 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_9 +#define MYBOT_DISPLAY_DC GPIO_NUM_8 +#define MYBOT_DISPLAY_CS GPIO_NUM_14 +#define MYBOT_DISPLAY_RESET GPIO_NUM_18 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_13 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 3 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (80 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/xingzhi-abs-2.0/sdkconfig.defaults b/components/mybot_platform/boards/xingzhi-abs-2.0/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-abs-2.0/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/xingzhi-cube-0.85tft-wifi/README.md b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/README.md new file mode 100644 index 0000000..1ce9258 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/README.md @@ -0,0 +1,17 @@ +# 行者 Cube 0.85 TFT Wi-Fi + +板型 ID 为 `xingzhi-cube-0.85tft-wifi`。硬件参数定义在本目录的 +[board_config.h](board_config.h)、[board.cmake](board.cmake) 和 +[sdkconfig.defaults](sdkconfig.defaults)。 + +- 显示:128×128 NV3023,SPI3,共享 LVGL 紧凑布局。 +- 音频:独立麦克风/扬声器 raw I2S,平台转换为 16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot;无触摸和音量键。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 +- 背光:GPIO13 LEDC,默认 60%。 + +当前没有 codec 控制、触摸、电池、休眠或关机实现。面板初始化、方向、颜色、背光、音频和 +电源仍需真机验证。 + +公共构建和烧录步骤见[板卡索引](../README.md),验证状态见 +[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/board.c b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/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/xingzhi-cube-0.85tft-wifi/board.cmake b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/board.cmake new file mode 100644 index 0000000..da6e5ec --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/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 "raw") +set(MYBOT_GENERIC_DISPLAY "generic/rgb565_spi_panel.c") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_PCA9557 OFF) +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_nv3023) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/board_config.h b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/board_config.h new file mode 100644 index 0000000..858c661 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/board_config.h @@ -0,0 +1,56 @@ +/* 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 "xingzhi-cube-0.85tft-wifi" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 2 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MIC_WS GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_7 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_15 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_16 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#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_PANEL_CONTROLLER 2 +#define MYBOT_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_10 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_9 +#define MYBOT_DISPLAY_DC GPIO_NUM_8 +#define MYBOT_DISPLAY_CS GPIO_NUM_14 +#define MYBOT_DISPLAY_RESET GPIO_NUM_18 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_13 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 128 +#define MYBOT_DISPLAY_HEIGHT 128 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y true +#define MYBOT_DISPLAY_INVERT_COLOR false +#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/xingzhi-cube-0.85tft-wifi/sdkconfig.defaults b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/sdkconfig.defaults new file mode 100644 index 0000000..214c8f7 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.85tft-wifi/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/xingzhi-cube-0.96oled-wifi/README.md b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/README.md new file mode 100644 index 0000000..88362e9 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/README.md @@ -0,0 +1,15 @@ +# 行者 Cube 0.96 OLED Wi-Fi + +板型 ID 为 `xingzhi-cube-0.96oled-wifi`。该 profile 当前是 headless 子集,不编入 LVGL。 + +- 显示:未启用;SSD1306 单色 UI 尚未实现。 +- 音频:独立麦克风/扬声器 raw I2S,SDK 边界为 16 kHz 单声道 signed-16 PCM。 +- 输入:GPIO0 Boot、GPIO40 音量加、GPIO39 音量减。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 + +初始化、按键极性和音频仍需真机验证。引脚与构建能力以 +[board_config.h](board_config.h)、[board.cmake](board.cmake) 和 +[sdkconfig.defaults](sdkconfig.defaults) 为准。 + +公共构建和烧录步骤见[板卡索引](../README.md),验证状态见 +[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/board.c b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/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/xingzhi-cube-0.96oled-wifi/board.cmake b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/board.cmake new file mode 100644 index 0000000..541e956 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/board.cmake @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 + +set(MYBOT_GENERIC_PARTITION_TABLE "partitions/v2/16m.csv") +set(MYBOT_GENERIC_AUDIO "raw") +set(MYBOT_GENERIC_TOUCH OFF) +set(MYBOT_GENERIC_PCA9557 OFF) +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 +) +include("${CMAKE_CURRENT_LIST_DIR}/../common/generic_profile.cmake") diff --git a/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/board_config.h b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/board_config.h new file mode 100644 index 0000000..648e1a8 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/board_config.h @@ -0,0 +1,33 @@ +/* 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 "xingzhi-cube-0.96oled-wifi" +#define MYBOT_BOARD_HAS_DISPLAY 0 +#define MYBOT_BOARD_HAS_TOUCH 0 +#define MYBOT_GENERIC_AUDIO_KIND 2 +#define MYBOT_GENERIC_TOUCH_KIND 0 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MIC_WS GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_MIC_BCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_MIC_DIN GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_SPK_DOUT GPIO_NUM_7 +#define MYBOT_AUDIO_I2S_SPK_BCLK GPIO_NUM_15 +#define MYBOT_AUDIO_I2S_SPK_WS GPIO_NUM_16 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_NC +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_NC + +#define MYBOT_BOOT_BUTTON_GPIO GPIO_NUM_0 +#define MYBOT_VOLUME_UP_BUTTON_GPIO GPIO_NUM_40 +#define MYBOT_VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39 + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/sdkconfig.defaults b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/sdkconfig.defaults new file mode 100644 index 0000000..214c8f7 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-cube-0.96oled-wifi/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/xingzhi-metal-1.54-wifi/README.md b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/README.md new file mode 100644 index 0000000..0e8a4a1 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/README.md @@ -0,0 +1,15 @@ +# 行者 Metal 1.54 Wi-Fi + +板型 ID 为 `xingzhi-metal-1.54-wifi`。引脚和能力以 [board_config.h](board_config.h)、 +[board.cmake](board.cmake) 与 [sdkconfig.defaults](sdkconfig.defaults) 为准。 + +- 显示:240×240 ST7789,SPI3 mode 3,共享 LVGL UI。 +- 音频:ES8311,16 kHz 单声道 signed-16 PCM;I2C0 GPIO41/42。 +- 输入:PCA9557 `0x19`、CST816S 触摸和 GPIO0 Boot。 +- 存储:16 MB QIO Flash profile、80 MHz Octal PSRAM、双 OTA 分区。 +- 电源:GPIO48 进程级电源保持;实际极性和上电时序待真机确认。 + +当前未实现电池/充电上报、休眠和关机;PCA9557 电源/复位、触摸、PA 和面板仍需真机验证。 + +公共构建、烧录和验证入口见[板卡索引](../README.md)与 +[验证矩阵](../../../../docs/VALIDATION.md)。 diff --git a/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/board.c b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/board.c new file mode 100644 index 0000000..dca1106 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/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/xingzhi-metal-1.54-wifi/board.cmake b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/board.cmake new file mode 100644 index 0000000..f46d7ec --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/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 "es8311") +set(MYBOT_GENERIC_DISPLAY "st7789/st7789_panel.c") +set(MYBOT_GENERIC_TOUCH ON) +set(MYBOT_GENERIC_PCA9557 ON) +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/xingzhi-metal-1.54-wifi/board_config.h b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/board_config.h new file mode 100644 index 0000000..fd4af66 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/board_config.h @@ -0,0 +1,76 @@ +/* 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 "xingzhi-metal-1.54-wifi" +#define MYBOT_BOARD_HAS_DISPLAY 1 +#define MYBOT_BOARD_HAS_TOUCH 1 +#define MYBOT_GENERIC_AUDIO_KIND 1 +#define MYBOT_GENERIC_TOUCH_KIND 1 + +#define MYBOT_AUDIO_SAMPLE_RATE 16000 +#define MYBOT_AUDIO_DEFAULT_VOLUME 70 +#define MYBOT_AUDIO_I2S_MCLK GPIO_NUM_5 +#define MYBOT_AUDIO_I2S_BCLK GPIO_NUM_6 +#define MYBOT_AUDIO_I2S_WS GPIO_NUM_4 +#define MYBOT_AUDIO_I2S_DIN GPIO_NUM_17 +#define MYBOT_AUDIO_I2S_DOUT GPIO_NUM_15 + +#define MYBOT_GENERIC_I2C_PORT I2C_NUM_0 +#define MYBOT_GENERIC_I2C_SDA GPIO_NUM_41 +#define MYBOT_GENERIC_I2C_SCL GPIO_NUM_42 +#define MYBOT_GENERIC_I2C_SPEED_HZ 400000 +#define MYBOT_GENERIC_PA_GPIO GPIO_NUM_21 +#define MYBOT_GENERIC_POWER_GPIO GPIO_NUM_48 +#define MYBOT_GENERIC_PCA9557_ADDRESS 0x19 +#define MYBOT_GENERIC_PCA9557_OUTPUT_MASK 0x00 +#define MYBOT_GENERIC_PCA9557_HIGH_MASK 0x00 + +#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_TOUCH_I2C_SPEED_HZ 400000 +#define MYBOT_TOUCH_RESET GPIO_NUM_NC +#define MYBOT_TOUCH_INTERRUPT GPIO_NUM_NC +#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_DISPLAY_SPI_HOST SPI3_HOST +#define MYBOT_DISPLAY_MOSI GPIO_NUM_10 +#define MYBOT_DISPLAY_SCLK GPIO_NUM_9 +#define MYBOT_DISPLAY_DC GPIO_NUM_8 +#define MYBOT_DISPLAY_CS GPIO_NUM_14 +#define MYBOT_DISPLAY_RESET GPIO_NUM_18 +#define MYBOT_DISPLAY_BACKLIGHT_GPIO GPIO_NUM_13 +#define MYBOT_DISPLAY_BACKLIGHT_INVERT 0 +#define MYBOT_DISPLAY_BRIGHTNESS_PERCENT 60 +#define MYBOT_DISPLAY_WIDTH 240 +#define MYBOT_DISPLAY_HEIGHT 240 +#define MYBOT_DISPLAY_OFFSET_X 0 +#define MYBOT_DISPLAY_OFFSET_Y 0 +#define MYBOT_DISPLAY_SWAP_XY false +#define MYBOT_DISPLAY_MIRROR_X false +#define MYBOT_DISPLAY_MIRROR_Y false +#define MYBOT_DISPLAY_INVERT_COLOR true +#define MYBOT_DISPLAY_SPI_MODE 3 +#define MYBOT_DISPLAY_PIXEL_CLOCK_HZ (80 * 1000 * 1000) +#define MYBOT_DISPLAY_RGB_ORDER LCD_RGB_ELEMENT_ORDER_RGB + +#endif /* MYBOT_BOARD_CONFIG_H_ */ diff --git a/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/sdkconfig.defaults b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/sdkconfig.defaults new file mode 100644 index 0000000..bc4a897 --- /dev/null +++ b/components/mybot_platform/boards/xingzhi-metal-1.54-wifi/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/src/drivers/audio/es8311_codec_audio.c b/components/mybot_platform/src/drivers/audio/es8311_codec_audio.c index ff9ee40..6b0b352 100644 --- a/components/mybot_platform/src/drivers/audio/es8311_codec_audio.c +++ b/components/mybot_platform/src/drivers/audio/es8311_codec_audio.c @@ -8,7 +8,11 @@ #include #include "driver/i2s_std.h" +#if MYBOT_GENERIC_AUDIO_KIND == 4 +#include "audio/es8374_codec.h" +#else #include "es8311_codec.h" +#endif #include "esp_codec_dev.h" #include "esp_codec_dev_defaults.h" #include "esp_err.h" @@ -33,7 +37,7 @@ #define TAG "es8311_audio" #if MYBOT_AUDIO_SAMPLE_RATE != 16000 -#error "ES8311 audio must expose 16 kHz PCM to mybot" +#error "Onboard codec audio must expose 16 kHz PCM to mybot" #endif #if MYBOT_AUDIO_DEFAULT_VOLUME < MYBOT_AUDIO_VOLUME_MIN || \ @@ -372,6 +376,16 @@ static int initialize_codec_locked(void) { goto codec_error; } +#if MYBOT_GENERIC_AUDIO_KIND == 4 + es8374_codec_cfg_t codec_config = { + .ctrl_if = s_audio.ctrl_if, + .gpio_if = s_audio.gpio_if, + .codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH, + .master_mode = false, + .pa_pin = GPIO_NUM_NC, + .pa_reverted = false, + }; +#else es8311_codec_cfg_t codec_config = { .ctrl_if = s_audio.ctrl_if, .gpio_if = s_audio.gpio_if, @@ -388,7 +402,12 @@ static int initialize_codec_locked(void) { .no_dac_ref = true, .mclk_div = 256, }; +#endif +#if MYBOT_GENERIC_AUDIO_KIND == 4 + s_audio.codec_if = es8374_codec_new(&codec_config); +#else s_audio.codec_if = es8311_codec_new(&codec_config); +#endif if (!s_audio.codec_if) { goto codec_error; } diff --git a/components/mybot_platform/src/drivers/audio/es8374_codec.c b/components/mybot_platform/src/drivers/audio/es8374_codec.c new file mode 100644 index 0000000..3cc12c7 --- /dev/null +++ b/components/mybot_platform/src/drivers/audio/es8374_codec.c @@ -0,0 +1,742 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "audio/es8374_codec.h" +#include "esp_log.h" +#include "esp_codec_dev_vol.h" +#include "audio/es_common.h" + +#define TAG "ES8374" + +typedef struct { + audio_codec_if_t base; + es8374_codec_cfg_t cfg; + bool is_open; + bool enabled; +} audio_codec_es8374_t; + +static const esp_codec_dev_vol_range_t vol_range = { + .min_vol = + { + .vol = 0xC0, + .db_value = -96.0, + }, + .max_vol = + { + .vol = 0x0, + .db_value = 0.0, + }, +}; + +static int es8374_write_reg(audio_codec_es8374_t *codec, int reg, int value) { + return codec->cfg.ctrl_if->write_reg(codec->cfg.ctrl_if, reg, 1, &value, 1); +} + +static int es8374_read_reg(audio_codec_es8374_t *codec, int reg, int *value) { + *value = 0; + return codec->cfg.ctrl_if->read_reg(codec->cfg.ctrl_if, reg, 1, value, 1); +} + +static int es8374_set_voice_mute(audio_codec_es8374_t *codec, bool enable) { + int ret = 0; + int reg = 0; + ret |= es8374_read_reg(codec, 0x36, ®); + if (ret == 0) { + reg = reg & 0xdf; + ret |= es8374_write_reg(codec, 0x36, reg | (((int)enable) << 5)); + } + return ret; +} + +static es_bits_length_t get_bits(uint8_t bits) { + switch (bits) { + default: + case 16: + return BIT_LENGTH_16BITS; + case 18: + return BIT_LENGTH_18BITS; + case 20: + return BIT_LENGTH_20BITS; + case 24: + return BIT_LENGTH_24BITS; + case 32: + return BIT_LENGTH_32BITS; + } +} + +static int es8374_set_bits_per_sample(audio_codec_es8374_t *codec, uint8_t bits) { + int ret = ESP_CODEC_DEV_OK; + int reg = 0; + es_bits_length_t bit_per_sample = get_bits(bits); + bits = (int)bit_per_sample & 0x0f; + + if (codec->cfg.codec_mode & ESP_CODEC_DEV_WORK_MODE_ADC) { + ret |= es8374_read_reg(codec, 0x10, ®); + if (ret == 0) { + reg = reg & 0xe3; + ret |= es8374_write_reg(codec, 0x10, reg | (bits << 2)); + } + } + if (codec->cfg.codec_mode & ESP_CODEC_DEV_WORK_MODE_DAC) { + ret |= es8374_read_reg(codec, 0x11, ®); + if (ret == 0) { + reg = reg & 0xe3; + ret |= es8374_write_reg(codec, 0x11, reg | (bits << 2)); + } + } + return ret; +} + +static int _set_mic_gain(audio_codec_es8374_t *codec, float gain) { + int ret = 0; + if (gain >= 0 && gain < 24) { + int gain_n = 0; + gain_n = (int)gain / 3; + ret = es8374_write_reg(codec, 0x22, gain_n | (gain_n << 4)); // MIC PGA + } else { + ret = -1; + } + return ret; +} + +static int es8374_i2s_config_clock(audio_codec_es8374_t *codec, es_i2s_clock_t cfg) { + int ret = 0; + int reg = 0; + ret |= es8374_read_reg(codec, 0x0f, ®); // power up adc and input + reg &= 0xe0; + int divratio = 0; + switch (cfg.sclk_div) { + case MCLK_DIV_1: + divratio = 1; + break; + case MCLK_DIV_2: // = 2, + divratio = 2; + break; + case MCLK_DIV_3: // = 3, + divratio = 3; + break; + case MCLK_DIV_4: // = 4, + divratio = 4; + break; + case MCLK_DIV_5: // = 20, + divratio = 5; + break; + case MCLK_DIV_6: // = 5, + divratio = 6; + break; + case MCLK_DIV_7: // = 29, + divratio = 7; + break; + case MCLK_DIV_8: // = 6, + divratio = 8; + break; + case MCLK_DIV_9: // = 7, + divratio = 9; + break; + case MCLK_DIV_10: // = 21, + divratio = 10; + break; + case MCLK_DIV_11: // = 8, + divratio = 11; + break; + case MCLK_DIV_12: // = 9, + divratio = 12; + break; + case MCLK_DIV_13: // = 30, + divratio = 13; + break; + case MCLK_DIV_14: // = 31 + divratio = 14; + break; + case MCLK_DIV_15: // = 22, + divratio = 15; + break; + case MCLK_DIV_16: // = 10, + divratio = 16; + break; + case MCLK_DIV_17: // = 23, + divratio = 17; + break; + case MCLK_DIV_18: // = 11, + divratio = 18; + break; + case MCLK_DIV_20: // = 24, + divratio = 19; + break; + case MCLK_DIV_22: // = 12, + divratio = 20; + break; + case MCLK_DIV_24: // = 13, + divratio = 21; + break; + case MCLK_DIV_25: // = 25, + divratio = 22; + break; + case MCLK_DIV_30: // = 26, + divratio = 23; + break; + case MCLK_DIV_32: // = 27, + divratio = 24; + break; + case MCLK_DIV_33: // = 14, + divratio = 25; + break; + case MCLK_DIV_34: // = 28, + divratio = 26; + break; + case MCLK_DIV_36: // = 15, + divratio = 27; + break; + case MCLK_DIV_44: // = 16, + divratio = 28; + break; + case MCLK_DIV_48: // = 17, + divratio = 29; + break; + case MCLK_DIV_66: // = 18, + divratio = 30; + break; + case MCLK_DIV_72: // = 19, + divratio = 31; + break; + default: + break; + } + reg |= divratio; + ret |= es8374_write_reg(codec, 0x0f, reg); + + int dacratio_l = 0; + int dacratio_h = 0; + + switch (cfg.lclk_div) { + case LCLK_DIV_128: + dacratio_l = 128 % 256; + dacratio_h = 128 / 256; + break; + case LCLK_DIV_192: + dacratio_l = 192 % 256; + dacratio_h = 192 / 256; + break; + case LCLK_DIV_256: + dacratio_l = 256 % 256; + dacratio_h = 256 / 256; + break; + case LCLK_DIV_384: + dacratio_l = 384 % 256; + dacratio_h = 384 / 256; + break; + case LCLK_DIV_512: + dacratio_l = 512 % 256; + dacratio_h = 512 / 256; + break; + case LCLK_DIV_576: + dacratio_l = 576 % 256; + dacratio_h = 576 / 256; + break; + case LCLK_DIV_768: + dacratio_l = 768 % 256; + dacratio_h = 768 / 256; + break; + case LCLK_DIV_1024: + dacratio_l = 1024 % 256; + dacratio_h = 1024 / 256; + break; + case LCLK_DIV_1152: + dacratio_l = 1152 % 256; + dacratio_h = 1152 / 256; + break; + case LCLK_DIV_1408: + dacratio_l = 1408 % 256; + dacratio_h = 1408 / 256; + break; + case LCLK_DIV_1536: + dacratio_l = 1536 % 256; + dacratio_h = 1536 / 256; + break; + case LCLK_DIV_2112: + dacratio_l = 2112 % 256; + dacratio_h = 2112 / 256; + break; + case LCLK_DIV_2304: + dacratio_l = 2304 % 256; + dacratio_h = 2304 / 256; + break; + case LCLK_DIV_125: + dacratio_l = 125 % 256; + dacratio_h = 125 / 256; + break; + case LCLK_DIV_136: + dacratio_l = 136 % 256; + dacratio_h = 136 / 256; + break; + case LCLK_DIV_250: + dacratio_l = 250 % 256; + dacratio_h = 250 / 256; + break; + case LCLK_DIV_272: + dacratio_l = 272 % 256; + dacratio_h = 272 / 256; + break; + case LCLK_DIV_375: + dacratio_l = 375 % 256; + dacratio_h = 375 / 256; + break; + case LCLK_DIV_500: + dacratio_l = 500 % 256; + dacratio_h = 500 / 256; + break; + case LCLK_DIV_544: + dacratio_l = 544 % 256; + dacratio_h = 544 / 256; + break; + case LCLK_DIV_750: + dacratio_l = 750 % 256; + dacratio_h = 750 / 256; + break; + case LCLK_DIV_1000: + dacratio_l = 1000 % 256; + dacratio_h = 1000 / 256; + break; + case LCLK_DIV_1088: + dacratio_l = 1088 % 256; + dacratio_h = 1088 / 256; + break; + case LCLK_DIV_1496: + dacratio_l = 1496 % 256; + dacratio_h = 1496 / 256; + break; + case LCLK_DIV_1500: + dacratio_l = 1500 % 256; + dacratio_h = 1500 / 256; + break; + default: + break; + } + ret |= es8374_write_reg(codec, 0x06, dacratio_h); // ADCFsMode, single SPEED, RATIO=256 + ret |= es8374_write_reg(codec, 0x07, dacratio_l); // ADCFsMode, single SPEED, RATIO=256 + + return ret; +} + +static int es8374_set_d2se_pga(audio_codec_es8374_t *codec, es_d2se_pga_t gain) { + int ret = 0; + int reg = 0; + if (gain > D2SE_PGA_GAIN_MIN && gain < D2SE_PGA_GAIN_MAX) { + ret = es8374_read_reg(codec, 0x21, ®); + reg &= 0xfb; + reg |= gain << 2; + ret = es8374_write_reg(codec, 0x21, reg); // MIC PGA + } + return ret; +} + +static int es8374_config_fmt(audio_codec_es8374_t *codec, es_i2s_fmt_t fmt) { + int ret = 0; + int reg = 0; + int fmt_i2s = fmt & 0x0f; + if (codec->cfg.codec_mode & ESP_CODEC_DEV_WORK_MODE_ADC) { + ret |= es8374_read_reg(codec, 0x10, ®); + if (ret == 0) { + reg = reg & 0xfc; + ret |= es8374_write_reg(codec, 0x10, reg | fmt_i2s); + } + } + if (codec->cfg.codec_mode & ESP_CODEC_DEV_WORK_MODE_DAC) { + ret |= es8374_read_reg(codec, 0x11, ®); + if (ret == 0) { + reg = reg & 0xfc; + ret |= es8374_write_reg(codec, 0x11, reg | (fmt_i2s)); + } + } + return ret; +} + +static int es8374_config_dac_output(audio_codec_es8374_t *codec, es_dac_output_t output) { + int ret = 0; + int reg = 0; + ret = es8374_write_reg(codec, 0x1d, 0x02); + ret |= es8374_read_reg(codec, 0x1c, ®); // set spk mixer + reg |= 0x80; + ret |= es8374_write_reg(codec, 0x1c, reg); + ret |= es8374_write_reg(codec, 0x1D, 0x02); // spk set + ret |= es8374_write_reg(codec, 0x1F, 0x00); // spk set + ret |= es8374_write_reg(codec, 0x1E, 0xA0); // spk on + return ret; +} + +static int es8374_config_adc_input(audio_codec_es8374_t *codec, es_adc_input_t input) { + int ret = 0; + int reg = 0; + ret |= es8374_read_reg(codec, 0x21, ®); + if (ret == 0) { + reg = (reg & 0xcf) | 0x14; + ret |= es8374_write_reg(codec, 0x21, reg); + } + return ret; +} + +static int es8374_set_adc_dac_volume(audio_codec_es8374_t *codec, esp_codec_dec_work_mode_t mode, + float db_value) { + int reg = esp_codec_dev_vol_calc_reg(&vol_range, db_value); + int ret = ESP_CODEC_DEV_OK; + if (mode & ESP_CODEC_DEV_WORK_MODE_ADC) { + ret = es8374_write_reg(codec, 0x25, (uint8_t)reg); + } + if (mode & ESP_CODEC_DEV_WORK_MODE_DAC) { + ret = es8374_write_reg(codec, 0x38, (uint8_t)reg); + } + return ret; +} + +static int es8374_init_reg(audio_codec_es8374_t *codec, es_i2s_fmt_t fmt, es_i2s_clock_t cfg, + es_dac_output_t out_channel, es_adc_input_t in_channel) { + int ret = 0; + int reg = 0; + + ret |= es8374_write_reg(codec, 0x00, 0x3F); // IC Rst start + ret |= es8374_write_reg(codec, 0x00, 0x03); // IC Rst stop + ret |= es8374_write_reg(codec, 0x01, 0x7F); // IC clk on + + ret |= es8374_read_reg(codec, 0x0F, ®); + reg &= 0x7f; + + reg |= (codec->cfg.master_mode << 7); + ret |= es8374_write_reg(codec, 0x0f, reg); // CODEC IN I2S SLAVE MODE + + ret |= es8374_write_reg(codec, 0x6F, 0xA0); // pll set:mode enable + ret |= es8374_write_reg(codec, 0x72, 0x41); // pll set:mode set + ret |= es8374_write_reg(codec, 0x09, 0x01); // pll set:reset on ,set start + ret |= es8374_write_reg(codec, 0x0C, 0x22); // pll set:k + ret |= es8374_write_reg(codec, 0x0D, 0x2E); // pll set:k + ret |= es8374_write_reg(codec, 0x0E, 0xC6); // pll set:k + ret |= es8374_write_reg(codec, 0x0A, 0x3A); // pll set: + ret |= es8374_write_reg(codec, 0x0B, 0x07); // pll set:n + ret |= es8374_write_reg(codec, 0x09, 0x41); // pll set:reset off ,set stop + + ret |= es8374_i2s_config_clock(codec, cfg); + + ret |= es8374_write_reg(codec, 0x24, 0x08); // adc set + ret |= es8374_write_reg(codec, 0x36, 0x00); // dac set + ret |= es8374_write_reg(codec, 0x12, 0x30); // timing set + ret |= es8374_write_reg(codec, 0x13, 0x20); // timing set + + ret |= es8374_config_fmt(codec, fmt); + + ret |= es8374_write_reg(codec, 0x21, 0x50); // adc set: SEL LIN1 CH+PGAGAIN=0DB + ret |= es8374_write_reg(codec, 0x22, 0xFF); // adc set: PGA GAIN=0DB + ret |= es8374_write_reg(codec, 0x21, 0x14); // adc set: SEL LIN1 CH+PGAGAIN=18DB + ret |= es8374_write_reg(codec, 0x22, 0x55); // pga = +15db + ret |= es8374_write_reg( + codec, 0x08, + 0x21); // set class d divider = 33, to avoid the high frequency tone on laudspeaker + ret |= es8374_write_reg(codec, 0x00, 0x80); // IC START + + ret |= es8374_set_adc_dac_volume(codec, ESP_CODEC_DEV_WORK_MODE_ADC, 0.0); // 0db + + ret |= es8374_write_reg(codec, 0x14, 0x8A); // IC START + ret |= es8374_write_reg(codec, 0x15, 0x40); // IC START + ret |= es8374_write_reg(codec, 0x1A, 0xA0); // monoout set + ret |= es8374_write_reg(codec, 0x1B, 0x19); // monoout set + ret |= es8374_write_reg(codec, 0x1C, 0x90); // spk set + ret |= es8374_write_reg(codec, 0x1D, 0x01); // spk set + ret |= es8374_write_reg(codec, 0x1F, 0x00); // spk set + ret |= es8374_write_reg(codec, 0x1E, 0x20); // spk on + ret |= es8374_write_reg(codec, 0x28, 0x00); // alc set + ret |= es8374_write_reg(codec, 0x25, 0x00); // ADCVOLUME on + ret |= es8374_write_reg(codec, 0x38, 0x00); // DACVOLUME on + ret |= es8374_write_reg(codec, 0x37, 0x30); // dac set + ret |= es8374_write_reg(codec, 0x6D, 0x60); // SEL:GPIO1=DMIC CLK OUT+SEL:GPIO2=PLL CLK OUT + ret |= es8374_write_reg(codec, 0x71, 0x05); // for automute setting + ret |= es8374_write_reg(codec, 0x73, 0x70); + + ret |= es8374_config_dac_output(codec, out_channel); // 0x3c Enable DAC and Enable Lout/Rout/1/2 + ret |= es8374_config_adc_input(codec, in_channel); // 0x00 LINSEL & RINSEL + ret |= es8374_set_adc_dac_volume(codec, ESP_CODEC_DEV_WORK_MODE_DAC, -96.0); + ret |= es8374_write_reg(codec, 0x37, 0x00); // dac set + return ret; +} + +static int es8374_stop(audio_codec_es8374_t *codec) { + int ret = 0; + int reg = 0; + if (codec->cfg.codec_mode == ESP_CODEC_DEV_WORK_MODE_LINE) { + ret |= es8374_read_reg(codec, 0x1a, ®); // disable lout + reg |= 0x08; + ret |= es8374_write_reg(codec, 0x1a, reg); + reg &= 0x9f; + ret |= es8374_write_reg(codec, 0x1a, reg); + ret |= es8374_write_reg(codec, 0x1D, 0x12); // mute speaker + ret |= es8374_write_reg(codec, 0x1E, 0x20); // disable class d + ret |= es8374_read_reg(codec, 0x1c, ®); // disable spkmixer + reg &= 0xbf; + ret |= es8374_write_reg(codec, 0x1c, reg); + ret |= es8374_write_reg(codec, 0x1F, 0x00); // spk set + } + if (codec->cfg.codec_mode & ESP_CODEC_DEV_WORK_MODE_DAC) { + ret |= es8374_set_voice_mute(codec, true); + + ret |= es8374_read_reg(codec, 0x1a, ®); // disable lout + reg |= 0x08; + ret |= es8374_write_reg(codec, 0x1a, reg); + reg &= 0xdf; + ret |= es8374_write_reg(codec, 0x1a, reg); + ret |= es8374_write_reg(codec, 0x1D, 0x12); // mute speaker + ret |= es8374_write_reg(codec, 0x1E, 0x20); // disable class d + ret |= es8374_read_reg(codec, 0x15, ®); // power up dac + reg |= 0x20; + ret |= es8374_write_reg(codec, 0x15, reg); + } + if (codec->cfg.codec_mode & ESP_CODEC_DEV_WORK_MODE_ADC) { + ret |= es8374_read_reg(codec, 0x10, ®); // power up adc and input + reg |= 0xc0; + ret |= es8374_write_reg(codec, 0x10, reg); + ret |= es8374_read_reg(codec, 0x21, ®); // power up adc and input + reg |= 0xc0; + ret |= es8374_write_reg(codec, 0x21, reg); + } + return ret; +} + +static int es8374_start(audio_codec_es8374_t *codec) { + int ret = ESP_CODEC_DEV_OK; + int reg = 0; + bool mode_line = (codec->cfg.codec_mode == ESP_CODEC_DEV_WORK_MODE_LINE); + if (mode_line) { + ret |= es8374_read_reg(codec, 0x1a, ®); // set monomixer + reg |= 0x60; + reg |= 0x20; + reg &= 0xf7; + ret |= es8374_write_reg(codec, 0x1a, reg); + ret |= es8374_read_reg(codec, 0x1c, ®); // set spk mixer + reg |= 0x40; + ret |= es8374_write_reg(codec, 0x1c, reg); + ret |= es8374_write_reg(codec, 0x1D, 0x02); // spk set + ret |= es8374_write_reg(codec, 0x1F, 0x00); // spk set + ret |= es8374_write_reg(codec, 0x1E, 0xA0); // spk on + } + if (mode_line || (codec->cfg.codec_mode & ESP_CODEC_DEV_WORK_MODE_ADC)) { + ret |= es8374_read_reg(codec, 0x21, ®); // power up adc and input + reg &= 0x3f; + ret |= es8374_write_reg(codec, 0x21, reg); + ret |= es8374_read_reg(codec, 0x10, ®); // power up adc and input + reg &= 0x3f; + ret |= es8374_write_reg(codec, 0x10, reg); + } + + if (mode_line || (codec->cfg.codec_mode & ESP_CODEC_DEV_WORK_MODE_DAC)) { + ret |= es8374_read_reg(codec, 0x1a, ®); // disable lout + reg |= 0x08; + ret |= es8374_write_reg(codec, 0x1a, reg); + reg &= 0xdf; + ret |= es8374_write_reg(codec, 0x1a, reg); + ret |= es8374_write_reg(codec, 0x1D, 0x12); // mute speaker + ret |= es8374_write_reg(codec, 0x1E, 0x20); // disable class d + ret |= es8374_read_reg(codec, 0x15, ®); // power up dac + reg &= 0xdf; + ret |= es8374_write_reg(codec, 0x15, reg); + ret |= es8374_read_reg(codec, 0x1a, ®); // disable lout + reg |= 0x20; + ret |= es8374_write_reg(codec, 0x1a, reg); + reg &= 0xf7; + ret |= es8374_write_reg(codec, 0x1a, reg); + ret |= es8374_write_reg(codec, 0x1D, 0x02); // mute speaker + ret |= es8374_write_reg(codec, 0x1E, 0xa0); // disable class d + ret |= es8374_set_voice_mute(codec, false); + } + return ret; +} + +static int es8374_set_mute(const audio_codec_if_t *h, bool mute) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL || codec->is_open == false) { + return ESP_CODEC_DEV_INVALID_ARG; + } + return es8374_set_voice_mute(codec, mute); +} + +static int es8374_set_vol(const audio_codec_if_t *h, float db_value) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL) { + return ESP_CODEC_DEV_INVALID_ARG; + } + if (codec->is_open == false) { + return ESP_CODEC_DEV_WRONG_STATE; + } + return es8374_set_adc_dac_volume(codec, ESP_CODEC_DEV_WORK_MODE_DAC, db_value); +} + +static int es8374_set_mic_gain(const audio_codec_if_t *h, float db) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL) { + return ESP_CODEC_DEV_INVALID_ARG; + } + if (codec->is_open == false) { + return ESP_CODEC_DEV_WRONG_STATE; + } + int ret = _set_mic_gain(codec, db); + return ret == 0 ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_WRITE_FAIL; +} + +static void es8374_pa_power(audio_codec_es8374_t *codec, bool enable) { + int16_t pa_pin = codec->cfg.pa_pin; + if (pa_pin == -1 || codec->cfg.gpio_if == NULL) { + return; + } + codec->cfg.gpio_if->setup(pa_pin, AUDIO_GPIO_DIR_OUT, AUDIO_GPIO_MODE_FLOAT); + codec->cfg.gpio_if->set(pa_pin, codec->cfg.pa_reverted ? !enable : enable); +} + +static int es8374_open(const audio_codec_if_t *h, void *cfg, int cfg_size) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + es8374_codec_cfg_t *codec_cfg = (es8374_codec_cfg_t *)cfg; + if (codec == NULL || codec_cfg == NULL || codec_cfg->ctrl_if == NULL || + cfg_size != sizeof(es8374_codec_cfg_t)) { + return ESP_CODEC_DEV_INVALID_ARG; + } + int ret = ESP_CODEC_DEV_OK; + memcpy(&codec->cfg, codec_cfg, sizeof(es8374_codec_cfg_t)); + es8374_pa_power(codec, false); + es_i2s_clock_t clkdiv; + clkdiv.lclk_div = LCLK_DIV_256; + clkdiv.sclk_div = MCLK_DIV_4; + ret |= es8374_stop(codec); + ret |= es8374_init_reg(codec, (BIT_LENGTH_16BITS << 4) | ES_I2S_NORMAL, clkdiv, DAC_OUTPUT_ALL, + ADC_INPUT_LINPUT1_RINPUT1); + ret |= _set_mic_gain(codec, 15); + ret |= es8374_set_d2se_pga(codec, D2SE_PGA_GAIN_EN); + ret |= es8374_config_fmt(codec, ES_I2S_NORMAL); + if (ret != 0) { + return ESP_CODEC_DEV_WRITE_FAIL; + } + codec->is_open = true; + return ESP_CODEC_DEV_OK; +} + +static int es8374_close(const audio_codec_if_t *h) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL) { + return ESP_CODEC_DEV_INVALID_ARG; + } + if (codec->is_open) { + es8374_pa_power(codec, false); + es8374_stop(codec); + es8374_write_reg(codec, 0x00, 0x7F); // IC Reset and STOP + codec->is_open = false; + } + return ESP_CODEC_DEV_OK; +} + +static int es8374_set_fs(const audio_codec_if_t *h, esp_codec_dev_sample_info_t *fs) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL || codec->is_open == false) { + return ESP_CODEC_DEV_INVALID_ARG; + } + int ret = 0; + ret |= es8374_config_fmt(codec, ES_I2S_NORMAL); + ret |= es8374_set_bits_per_sample(codec, fs->bits_per_sample); + return ESP_CODEC_DEV_OK; +} + +static int es8374_enable(const audio_codec_if_t *h, bool enable) { + int ret = ESP_CODEC_DEV_OK; + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL) { + return ESP_CODEC_DEV_INVALID_ARG; + } + if (codec->is_open == false) { + return ESP_CODEC_DEV_WRONG_STATE; + } + if (codec->enabled == enable) { + return ESP_CODEC_DEV_OK; + } + if (enable) { + ret = es8374_start(codec); + es8374_pa_power(codec, true); + } else { + es8374_pa_power(codec, false); + ret = es8374_stop(codec); + es8374_write_reg(codec, 0x00, 0x7F); // IC Reset and STOP + } + if (ret == ESP_CODEC_DEV_OK) { + codec->enabled = enable; + ESP_LOGD(TAG, "Codec is %s", enable ? "enable" : "disable"); + } + return ret; +} + +static int es8374_set_reg(const audio_codec_if_t *h, int reg, int value) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL) { + return ESP_CODEC_DEV_INVALID_ARG; + } + if (codec->is_open == false) { + return ESP_CODEC_DEV_WRONG_STATE; + } + return es8374_write_reg(codec, reg, value); +} + +static int es8374_get_reg(const audio_codec_if_t *h, int reg, int *value) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL) { + return ESP_CODEC_DEV_INVALID_ARG; + } + if (codec->is_open == false) { + return ESP_CODEC_DEV_WRONG_STATE; + } + return es8374_read_reg(codec, reg, value); +} + +static void es8374_dump(const audio_codec_if_t *h) { + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)h; + if (codec == NULL || codec->is_open == false) { + return; + } + for (int i = 0; i < 50; i++) { + int value = 0; + int ret = es8374_read_reg(codec, i, &value); + if (ret != ESP_CODEC_DEV_OK) { + break; + } + ESP_LOGI(TAG, "%02x: %02x", i, value); + } +} + +const audio_codec_if_t *es8374_codec_new(es8374_codec_cfg_t *codec_cfg) { + if (codec_cfg == NULL || codec_cfg->ctrl_if == NULL) { + ESP_LOGE(TAG, "Wrong codec config"); + return NULL; + } + if (codec_cfg->ctrl_if->is_open(codec_cfg->ctrl_if) == false) { + ESP_LOGE(TAG, "Control interface not open yet"); + return NULL; + } + audio_codec_es8374_t *codec = (audio_codec_es8374_t *)calloc(1, sizeof(audio_codec_es8374_t)); + if (codec == NULL) { + CODEC_MEM_CHECK(codec); + return NULL; + } + codec->base.open = es8374_open; + codec->base.enable = es8374_enable; + codec->base.set_fs = es8374_set_fs; + codec->base.mute = es8374_set_mute; + codec->base.set_vol = es8374_set_vol; + codec->base.set_mic_gain = es8374_set_mic_gain; + codec->base.set_reg = es8374_set_reg; + codec->base.get_reg = es8374_get_reg; + codec->base.dump_reg = es8374_dump, codec->base.close = es8374_close; + do { + int ret = codec->base.open(&codec->base, codec_cfg, sizeof(es8374_codec_cfg_t)); + if (ret != 0) { + ESP_LOGE(TAG, "Open fail"); + break; + } + return &codec->base; + } while (0); + if (codec) { + free(codec); + } + return NULL; +} diff --git a/components/mybot_platform/src/drivers/audio/raw_i2s_audio.c b/components/mybot_platform/src/drivers/audio/raw_i2s_audio.c index ad1f162..47157e3 100644 --- a/components/mybot_platform/src/drivers/audio/raw_i2s_audio.c +++ b/components/mybot_platform/src/drivers/audio/raw_i2s_audio.c @@ -5,6 +5,7 @@ #include #include +#include "driver/i2s_pdm.h" #include "driver/i2s_std.h" #include "esp_err.h" #include "esp_log.h" @@ -26,6 +27,32 @@ #define AUDIO_VOLUME_NVS_NAMESPACE "audio" #define TAG "mybot_audio" +#ifndef MYBOT_AUDIO_I2S_CAPTURE_RIGHT_SLOT +#define MYBOT_AUDIO_I2S_CAPTURE_RIGHT_SLOT 0 +#endif + +#ifndef MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS +#define MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS 0 +#endif + +#ifndef MYBOT_AUDIO_I2S_CAPTURE_PDM +#define MYBOT_AUDIO_I2S_CAPTURE_PDM 0 +#endif + +#ifndef MYBOT_AUDIO_I2S_CONTROL_SPEAKER_POWER +#define MYBOT_AUDIO_I2S_CONTROL_SPEAKER_POWER 0 +#endif + +#if MYBOT_AUDIO_I2S_CONTROL_SPEAKER_POWER +int mybot_generic_set_speaker_power(bool enabled); +#endif + +#if MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS +#define AUDIO_SCRATCH_FRAMES (AUDIO_MAX_FRAMES * 2) +#else +#define AUDIO_SCRATCH_FRAMES AUDIO_MAX_FRAMES +#endif + typedef struct { i2s_chan_handle_t channel; aosl_atomic_t started; @@ -35,7 +62,7 @@ typedef struct { bool playback; bool enabled; bool stop_pending; - int32_t scratch[AUDIO_MAX_FRAMES]; + int32_t scratch[AUDIO_SCRATCH_FRAMES]; } audio_context_t; typedef struct { @@ -206,10 +233,11 @@ static int validate_format(int rate, int channels, int bits) { } static i2s_std_config_t make_i2s_config(int rate, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, - gpio_num_t din) { + gpio_num_t din, bool both_slots, bool right_slot) { i2s_std_config_t config = { .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate), - .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), + .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG( + I2S_DATA_BIT_WIDTH_32BIT, both_slots ? I2S_SLOT_MODE_STEREO : I2S_SLOT_MODE_MONO), .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, @@ -225,7 +253,8 @@ static i2s_std_config_t make_i2s_config(int rate, gpio_num_t bclk, gpio_num_t ws }, }, }; - config.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT; + config.slot_cfg.slot_mask = + both_slots ? I2S_STD_SLOT_BOTH : (right_slot ? I2S_STD_SLOT_RIGHT : I2S_STD_SLOT_LEFT); config.slot_cfg.ws_width = I2S_DATA_BIT_WIDTH_32BIT; config.slot_cfg.bit_shift = true; config.slot_cfg.left_align = true; @@ -252,7 +281,8 @@ static int capture_init(void **out_ctx, int rate, int channels, int bits) { return -1; } - i2s_chan_config_t channel_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER); + i2s_chan_config_t channel_config = I2S_CHANNEL_DEFAULT_CONFIG( + MYBOT_AUDIO_I2S_CAPTURE_PDM ? I2S_NUM_0 : I2S_NUM_1, I2S_ROLE_MASTER); channel_config.dma_desc_num = 6; channel_config.dma_frame_num = 240; if (i2s_new_channel(&channel_config, NULL, &ctx->channel) != ESP_OK) { @@ -260,10 +290,33 @@ static int capture_init(void **out_ctx, int rate, int channels, int bits) { return -1; } - i2s_std_config_t std_config = +#if MYBOT_AUDIO_I2S_CAPTURE_PDM + if (!SOC_I2S_SUPPORTS_PDM_RX) { + i2s_del_channel(ctx->channel); + free(ctx); + return -1; + } + const i2s_pdm_rx_config_t pdm_config = { + .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG((uint32_t)rate), + .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = + { + .clk = MYBOT_AUDIO_I2S_MIC_BCLK, + .din = MYBOT_AUDIO_I2S_MIC_DIN, + .invert_flags = + { + .clk_inv = false, + }, + }, + }; + const esp_err_t result = i2s_channel_init_pdm_rx_mode(ctx->channel, &pdm_config); +#else + const i2s_std_config_t std_config = make_i2s_config(rate, MYBOT_AUDIO_I2S_MIC_BCLK, MYBOT_AUDIO_I2S_MIC_WS, I2S_GPIO_UNUSED, - MYBOT_AUDIO_I2S_MIC_DIN); - if (i2s_channel_init_std_mode(ctx->channel, &std_config) != ESP_OK) { + MYBOT_AUDIO_I2S_MIC_DIN, false, MYBOT_AUDIO_I2S_CAPTURE_RIGHT_SLOT != 0); + const esp_err_t result = i2s_channel_init_std_mode(ctx->channel, &std_config); +#endif + if (result != ESP_OK) { i2s_del_channel(ctx->channel); free(ctx); return -1; @@ -298,6 +351,14 @@ static int capture_read(void *opaque, void *buffer, int frames) { } size_t bytes_read = 0; +#if MYBOT_AUDIO_I2S_CAPTURE_PDM + esp_err_t err = i2s_channel_read(ctx->channel, buffer, (size_t)frames * sizeof(int16_t), + &bytes_read, AUDIO_IO_TIMEOUT_MS); + if (err == ESP_ERR_TIMEOUT) { + return 0; + } + return err == ESP_OK ? (int)(bytes_read / sizeof(int16_t)) : -1; +#else esp_err_t err = i2s_channel_read(ctx->channel, ctx->scratch, (size_t)frames * sizeof(ctx->scratch[0]), &bytes_read, AUDIO_IO_TIMEOUT_MS); @@ -320,9 +381,12 @@ static int capture_read(void *opaque, void *buffer, int frames) { output[i] = (int16_t)value; } return samples; +#endif } static int clear_playback_dma_locked(audio_context_t *ctx) { + const size_t bytes_per_frame = + sizeof(ctx->scratch[0]) * (MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS ? 2 : 1); memset(ctx->scratch, 0, sizeof(ctx->scratch)); while (ctx->dma_clear_frames < AUDIO_DMA_FRAMES) { size_t frames = AUDIO_DMA_FRAMES - ctx->dma_clear_frames; @@ -330,13 +394,13 @@ static int clear_playback_dma_locked(audio_context_t *ctx) { frames = AUDIO_MAX_FRAMES; } size_t loaded = 0; - esp_err_t err = i2s_channel_preload_data(ctx->channel, ctx->scratch, - frames * sizeof(ctx->scratch[0]), &loaded); - if (loaded > frames * sizeof(ctx->scratch[0]) || loaded % sizeof(ctx->scratch[0])) { + esp_err_t err = + i2s_channel_preload_data(ctx->channel, ctx->scratch, frames * bytes_per_frame, &loaded); + if (loaded > frames * bytes_per_frame || loaded % bytes_per_frame) { return -1; } - ctx->dma_clear_frames += loaded / sizeof(ctx->scratch[0]); - if (err != ESP_OK || loaded != frames * sizeof(ctx->scratch[0])) { + ctx->dma_clear_frames += loaded / bytes_per_frame; + if (err != ESP_OK || loaded != frames * bytes_per_frame) { ESP_LOGE(TAG, "event=audio_device direction=playback action=clear_dma result=error " "error=%s loaded=%u", @@ -364,6 +428,11 @@ static int audio_stop_locked(audio_context_t *ctx) { ctx->enabled = false; ctx->dma_clear_frames = 0; } +#if MYBOT_AUDIO_I2S_CONTROL_SPEAKER_POWER + if (ctx->playback && mybot_generic_set_speaker_power(false) < 0) { + return -1; + } +#endif if (ctx->playback && clear_playback_dma_locked(ctx) < 0) { return -1; } @@ -419,16 +488,25 @@ static esp_err_t playback_sink_write(void *opaque, const int16_t *pcm, size_t fr } int32_t gain_q16 = (int32_t)aosl_atomic_read(&s_output_gain_q16); for (size_t i = 0; i < frames; ++i) { - ctx->scratch[i] = (int32_t)((int64_t)pcm[i] * gain_q16); + const int32_t sample = (int32_t)((int64_t)pcm[i] * gain_q16); + if (MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS) { + ctx->scratch[i * 2] = sample; + ctx->scratch[i * 2 + 1] = sample; + } else { + ctx->scratch[i] = sample; + } } + const size_t sample_count = frames * (MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS ? 2U : 1U); + const size_t bytes_per_frame = + sizeof(ctx->scratch[0]) * (MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS ? 2U : 1U); size_t bytes_written = 0; - esp_err_t err = i2s_channel_write(ctx->channel, ctx->scratch, frames * sizeof(ctx->scratch[0]), - &bytes_written, timeout_ms); - if (bytes_written > frames * sizeof(ctx->scratch[0]) || - bytes_written % sizeof(ctx->scratch[0])) { + esp_err_t err = + i2s_channel_write(ctx->channel, ctx->scratch, sample_count * sizeof(ctx->scratch[0]), + &bytes_written, timeout_ms); + if (bytes_written > sample_count * sizeof(ctx->scratch[0]) || bytes_written % bytes_per_frame) { return ESP_FAIL; } - *written_frames = bytes_written / sizeof(ctx->scratch[0]); + *written_frames = bytes_written / bytes_per_frame; return err; } @@ -445,7 +523,8 @@ static int playback_init(void **out_ctx, int rate, int channels, int bits) { } ctx->playback = true; - i2s_chan_config_t channel_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + i2s_chan_config_t channel_config = I2S_CHANNEL_DEFAULT_CONFIG( + MYBOT_AUDIO_I2S_CAPTURE_PDM ? I2S_NUM_1 : I2S_NUM_0, I2S_ROLE_MASTER); channel_config.dma_desc_num = 6; channel_config.dma_frame_num = 240; channel_config.auto_clear_after_cb = true; @@ -454,9 +533,9 @@ static int playback_init(void **out_ctx, int rate, int channels, int bits) { return -1; } - i2s_std_config_t std_config = - make_i2s_config(rate, MYBOT_AUDIO_I2S_SPK_BCLK, MYBOT_AUDIO_I2S_SPK_WS, - MYBOT_AUDIO_I2S_SPK_DOUT, I2S_GPIO_UNUSED); + i2s_std_config_t std_config = make_i2s_config( + rate, MYBOT_AUDIO_I2S_SPK_BCLK, MYBOT_AUDIO_I2S_SPK_WS, MYBOT_AUDIO_I2S_SPK_DOUT, + I2S_GPIO_UNUSED, MYBOT_AUDIO_I2S_PLAYBACK_BOTH_SLOTS != 0, false); if (i2s_channel_init_std_mode(ctx->channel, &std_config) != ESP_OK) { i2s_del_channel(ctx->channel); free(ctx); @@ -485,6 +564,14 @@ static int playback_start(void *opaque) { return -1; } ctx->enabled = true; +#if MYBOT_AUDIO_I2S_CONTROL_SPEAKER_POWER + if (mybot_generic_set_speaker_power(true) < 0) { + (void)i2s_channel_disable(ctx->channel); + ctx->enabled = false; + audio_unlock(); + return -1; + } +#endif if (mybot_pcm_playback_buffer_start(ctx->playback_buffer) < 0) { ctx->stop_pending = true; (void)audio_stop_locked(ctx); diff --git a/components/mybot_platform/src/drivers/display/panels/co5300/co5300_466_panel.c b/components/mybot_platform/src/drivers/display/panels/co5300/co5300_466_panel.c new file mode 100644 index 0000000..881b0a1 --- /dev/null +++ b/components/mybot_platform/src/drivers/display/panels/co5300/co5300_466_panel.c @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "board_config.h" +#include "display/display_panel.h" + +#include "driver/gpio.h" +#include "driver/spi_master.h" +#include "esp_err.h" +#include "esp_lcd_co5300.h" +#include "esp_lcd_io_spi.h" +#include "esp_log.h" + +#include +#include + +#define TAG "co5300_466_panel" +#define BRIGHTNESS_PERCENT 60 + +_Static_assert(MYBOT_DISPLAY_WIDTH == 466 && MYBOT_DISPLAY_HEIGHT == 466, + "This CO5300 variant requires a 466x466 panel"); +_Static_assert(MYBOT_DISPLAY_OFFSET_X == 0 && MYBOT_DISPLAY_OFFSET_Y == 0, + "This CO5300 variant requires zero panel gaps"); +_Static_assert((MYBOT_DISPLAY_TRANSFER_ROWS % 2) == 0, "CO5300 transfer rows must be even"); + +static const co5300_lcd_init_cmd_t s_vendor_init[] = { + {0xFE, (uint8_t[]){0x00}, 1, 0}, + {0xC4, (uint8_t[]){0x80}, 1, 0}, + {0x3A, (uint8_t[]){0x55}, 1, 10}, + {0x35, (uint8_t[]){0x00}, 1, 10}, + {0x53, (uint8_t[]){0x20}, 1, 10}, + {0x51, (uint8_t[]){0xFF}, 1, 10}, + {0x63, (uint8_t[]){0xFF}, 1, 10}, + {0x2A, (uint8_t[]){0x00, 0x00, 0x01, 0xD1}, 4, 0}, + {0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xD1}, 4, 0}, + {0x11, NULL, 0, 120}, + {0x29, NULL, 0, 20}, +}; + +int mybot_display_panel_close(mybot_display_panel_t *panel) { + if (!panel) { + return -1; + } + if (panel->panel && esp_lcd_panel_co5300_set_brightness(panel->panel, 0) != ESP_OK) { + ESP_LOGW(TAG, "event=panel action=brightness_off result=error"); + } + 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 (panel->spi_ready) { + if (spi_bus_free(MYBOT_DISPLAY_SPI_HOST) != ESP_OK) { + return -1; + } + panel->spi_ready = false; + } + panel->ready = false; + panel->x_alignment = 0; + panel->y_alignment = 0; + return 0; +} + +int mybot_display_panel_open(mybot_display_panel_t *panel) { + if (!panel || panel->ready || mybot_display_panel_close(panel) < 0) { + return -1; + } + + const spi_bus_config_t bus_config = { + .sclk_io_num = MYBOT_DISPLAY_PCLK, + .data0_io_num = MYBOT_DISPLAY_DATA0, + .data1_io_num = MYBOT_DISPLAY_DATA1, + .data2_io_num = MYBOT_DISPLAY_DATA2, + .data3_io_num = MYBOT_DISPLAY_DATA3, + .max_transfer_sz = MYBOT_DISPLAY_WIDTH * MYBOT_DISPLAY_TRANSFER_ROWS * sizeof(uint16_t), + .flags = SPICOMMON_BUSFLAG_QUAD, + }; + esp_err_t result = spi_bus_initialize(MYBOT_DISPLAY_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO); + if (result != ESP_OK) { + goto fail; + } + panel->spi_ready = true; + + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = MYBOT_DISPLAY_CS, + .dc_gpio_num = GPIO_NUM_NC, + .spi_mode = 0, + .pclk_hz = MYBOT_DISPLAY_PIXEL_CLOCK_HZ, + .trans_queue_depth = 2, + .lcd_cmd_bits = 32, + .lcd_param_bits = 8, + .flags.quad_mode = true, + }; + result = esp_lcd_new_panel_io_spi(MYBOT_DISPLAY_SPI_HOST, &io_config, &panel->io); + if (result != ESP_OK) { + goto fail; + } + + const co5300_vendor_config_t vendor_config = { + .init_cmds = s_vendor_init, + .init_cmds_size = sizeof(s_vendor_init) / sizeof(s_vendor_init[0]), + .flags.use_qspi_interface = true, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = MYBOT_DISPLAY_RESET, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + .vendor_config = (void *)&vendor_config, + }; + result = esp_lcd_new_panel_co5300(panel->io, &panel_config, &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_reset(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_init(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_invert_color(panel->panel, MYBOT_DISPLAY_INVERT_COLOR); + } + 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 = esp_lcd_panel_co5300_set_brightness(panel->panel, BRIGHTNESS_PERCENT); + } + if (result != ESP_OK) { + goto fail; + } + + panel->ready = true; + panel->x_alignment = 2; + panel->y_alignment = 2; + return 0; + +fail: + ESP_LOGE(TAG, "event=panel action=initialize 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/co5300/co5300_amoled18_panel.c b/components/mybot_platform/src/drivers/display/panels/co5300/co5300_amoled18_panel.c new file mode 100644 index 0000000..a880d82 --- /dev/null +++ b/components/mybot_platform/src/drivers/display/panels/co5300/co5300_amoled18_panel.c @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "board_config.h" +#include "display/display_panel.h" + +#include "driver/spi_master.h" +#include "esp_err.h" +#include "esp_lcd_co5300.h" +#include "esp_lcd_io_spi.h" +#include "esp_log.h" + +#include +#include + +#define TAG "co5300_amoled18" +#define BRIGHTNESS_PERCENT 60 + +_Static_assert((MYBOT_DISPLAY_WIDTH % 2) == 0, "CO5300 transfers require an even width"); +_Static_assert((MYBOT_DISPLAY_HEIGHT % 2) == 0, "CO5300 transfers require an even height"); +_Static_assert((MYBOT_DISPLAY_TRANSFER_ROWS % 2) == 0, "CO5300 transfer rows must be even"); + +static const co5300_lcd_init_cmd_t s_vendor_init[] = { + {0x11, NULL, 0, 600}, + {0xFE, (uint8_t[]){0x20}, 1, 0}, + {0x19, (uint8_t[]){0x10}, 1, 0}, + {0x1C, (uint8_t[]){0xA0}, 1, 0}, + {0xFE, (uint8_t[]){0x00}, 1, 0}, + {0xC4, (uint8_t[]){0x80}, 1, 0}, + {0x3A, (uint8_t[]){0x55}, 1, 0}, + {0x35, (uint8_t[]){0x00}, 1, 0}, + {0x53, (uint8_t[]){0x20}, 1, 0}, + {0x51, (uint8_t[]){0xFF}, 1, 0}, + {0x63, (uint8_t[]){0xFF}, 1, 0}, + {0x2A, (uint8_t[]){0x00, 0x00, 0x01, 0xDF}, 4, 0}, + {0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xDF}, 4, 0}, + {0x36, (uint8_t[]){0x00}, 1, 0}, + {0x29, NULL, 0, 600}, +}; + +int mybot_display_panel_close(mybot_display_panel_t *panel) { + if (!panel) { + return -1; + } + if (panel->panel && esp_lcd_panel_co5300_set_brightness(panel->panel, 0) != ESP_OK) { + ESP_LOGW(TAG, "event=panel action=brightness_off result=error"); + } + 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 (panel->spi_ready) { + if (spi_bus_free(MYBOT_DISPLAY_SPI_HOST) != ESP_OK) { + return -1; + } + panel->spi_ready = false; + } + panel->ready = false; + panel->x_alignment = 0; + panel->y_alignment = 0; + return 0; +} + +int mybot_display_panel_open(mybot_display_panel_t *panel) { + if (!panel || panel->ready || mybot_display_panel_close(panel) < 0) { + return -1; + } + + const spi_bus_config_t bus_config = { + .sclk_io_num = MYBOT_DISPLAY_PCLK, + .data0_io_num = MYBOT_DISPLAY_DATA0, + .data1_io_num = MYBOT_DISPLAY_DATA1, + .data2_io_num = MYBOT_DISPLAY_DATA2, + .data3_io_num = MYBOT_DISPLAY_DATA3, + .max_transfer_sz = MYBOT_DISPLAY_WIDTH * MYBOT_DISPLAY_TRANSFER_ROWS * sizeof(uint16_t), + .flags = SPICOMMON_BUSFLAG_QUAD, + }; + esp_err_t result = spi_bus_initialize(MYBOT_DISPLAY_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO); + if (result != ESP_OK) { + goto fail; + } + panel->spi_ready = true; + + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = MYBOT_DISPLAY_CS, + .dc_gpio_num = GPIO_NUM_NC, + .spi_mode = 0, + .pclk_hz = MYBOT_DISPLAY_PIXEL_CLOCK_HZ, + .trans_queue_depth = 2, + .lcd_cmd_bits = 32, + .lcd_param_bits = 8, + .flags.quad_mode = true, + }; + result = esp_lcd_new_panel_io_spi(MYBOT_DISPLAY_SPI_HOST, &io_config, &panel->io); + if (result != ESP_OK) { + goto fail; + } + + const co5300_vendor_config_t vendor_config = { + .init_cmds = s_vendor_init, + .init_cmds_size = sizeof(s_vendor_init) / sizeof(s_vendor_init[0]), + .flags.use_qspi_interface = true, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = MYBOT_DISPLAY_RESET, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + .vendor_config = (void *)&vendor_config, + }; + result = esp_lcd_new_panel_co5300(panel->io, &panel_config, &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_reset(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_init(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_invert_color(panel->panel, MYBOT_DISPLAY_INVERT_COLOR); + } + 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 = esp_lcd_panel_co5300_set_brightness(panel->panel, BRIGHTNESS_PERCENT); + } + if (result != ESP_OK) { + goto fail; + } + + panel->ready = true; + panel->x_alignment = 2; + panel->y_alignment = 2; + return 0; + +fail: + ESP_LOGE(TAG, "event=panel action=initialize 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/qspi_st77916_panel.c b/components/mybot_platform/src/drivers/display/panels/generic/qspi_st77916_panel.c new file mode 100644 index 0000000..cbaec00 --- /dev/null +++ b/components/mybot_platform/src/drivers/display/panels/generic/qspi_st77916_panel.c @@ -0,0 +1,144 @@ +/* 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 "driver/spi_master.h" +#include "esp_err.h" +#include "esp_lcd_io_spi.h" +#include "esp_lcd_st77916.h" +#include "esp_log.h" + +#include +#include + +#define TAG "st77916_qspi_panel" + +#ifndef MYBOT_DISPLAY_RESET_ACTIVE_HIGH +#define MYBOT_DISPLAY_RESET_ACTIVE_HIGH 0 +#endif + +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 (panel->spi_ready) { + if (spi_bus_free(MYBOT_DISPLAY_SPI_HOST) != ESP_OK) { + return -1; + } + panel->spi_ready = false; + } + panel->ready = false; + panel->x_alignment = 0; + panel->y_alignment = 0; + return 0; +} + +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; + + const spi_bus_config_t bus_config = { + .sclk_io_num = MYBOT_DISPLAY_PCLK, + .data0_io_num = MYBOT_DISPLAY_DATA0, + .data1_io_num = MYBOT_DISPLAY_DATA1, + .data2_io_num = MYBOT_DISPLAY_DATA2, + .data3_io_num = MYBOT_DISPLAY_DATA3, + .max_transfer_sz = MYBOT_DISPLAY_WIDTH * MYBOT_DISPLAY_TRANSFER_ROWS * sizeof(uint16_t), + .flags = SPICOMMON_BUSFLAG_QUAD, + }; + esp_err_t result = spi_bus_initialize(MYBOT_DISPLAY_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO); + if (result != ESP_OK) { + goto fail; + } + panel->spi_ready = true; + + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = MYBOT_DISPLAY_CS, + .dc_gpio_num = GPIO_NUM_NC, + .spi_mode = 0, + .pclk_hz = MYBOT_DISPLAY_PIXEL_CLOCK_HZ, + .trans_queue_depth = 2, + .lcd_cmd_bits = 32, + .lcd_param_bits = 8, + .flags.quad_mode = true, + }; + result = esp_lcd_new_panel_io_spi(MYBOT_DISPLAY_SPI_HOST, &io_config, &panel->io); + if (result != ESP_OK) { + goto fail; + } + + const st77916_vendor_config_t vendor_config = { + .flags.use_qspi_interface = true, + }; + const 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, + .flags.reset_active_high = MYBOT_DISPLAY_RESET_ACTIVE_HIGH, + .vendor_config = (void *)&vendor_config, + }; + result = esp_lcd_new_panel_st77916(panel->io, &panel_config, &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_reset(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_init(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_invert_color(panel->panel, MYBOT_DISPLAY_INVERT_COLOR); + } + 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 = 2; + panel->y_alignment = 2; + return 0; + +fail: + ESP_LOGE(TAG, "event=panel action=initialize 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 new file mode 100644 index 0000000..0910b32 --- /dev/null +++ b/components/mybot_platform/src/drivers/display/panels/generic/rgb565_spi_panel.c @@ -0,0 +1,225 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "board_config.h" +#include "display/board_backlight.h" +#include "display/display_panel.h" + +#include "driver/spi_master.h" +#include "esp_err.h" +#include "esp_lcd_io_spi.h" +#include "esp_log.h" + +#if MYBOT_PANEL_CONTROLLER == 1 +#include "esp_lcd_gc9a01.h" +#elif MYBOT_PANEL_CONTROLLER == 2 +#include "esp_lcd_nv3023.h" +#elif MYBOT_PANEL_CONTROLLER == 3 +#include "esp_lcd_ili9341.h" +#else +#error "Unsupported MYBOT_PANEL_CONTROLLER" +#endif + +#include +#include + +#define TAG "generic_spi_panel" + +#ifndef MYBOT_DISPLAY_RESET_ACTIVE_HIGH +#define MYBOT_DISPLAY_RESET_ACTIVE_HIGH 0 +#endif + +#if MYBOT_PANEL_CONTROLLER == 3 +#ifndef MYBOT_ILI9341_PROFILE +#define MYBOT_ILI9341_PROFILE 0 +#endif + +#if MYBOT_ILI9341_PROFILE == 1 +static const ili9341_lcd_init_cmd_t s_ili9341_profile_1[] = { + {0xC8, (uint8_t[]){0xFF, 0x93, 0x42}, 3, 0}, + {0xC0, (uint8_t[]){0x0E, 0x0E}, 2, 0}, + {0xC5, (uint8_t[]){0xD0}, 1, 0}, + {0xC1, (uint8_t[]){0x02}, 1, 0}, + {0xB4, (uint8_t[]){0x02}, 1, 0}, + {0xE0, + (uint8_t[]){0x00, 0x03, 0x08, 0x06, 0x13, 0x09, 0x39, 0x39, 0x48, 0x02, 0x0A, 0x08, 0x17, 0x17, + 0x0F}, + 15, 0}, + {0xE1, + (uint8_t[]){0x00, 0x28, 0x29, 0x01, 0x0D, 0x03, 0x3F, 0x33, 0x52, 0x04, 0x0F, 0x0E, 0x37, 0x38, + 0x0F}, + 15, 0}, + {0xB1, (uint8_t[]){0x00, 0x1B}, 2, 0}, + {0x36, (uint8_t[]){0x08}, 1, 0}, + {0x3A, (uint8_t[]){0x55}, 1, 0}, + {0xB7, (uint8_t[]){0x06}, 1, 0}, + {0x11, NULL, 0, 128}, + {0x29, NULL, 0, 128}, +}; +#elif MYBOT_ILI9341_PROFILE == 2 +static const ili9341_lcd_init_cmd_t s_ili9341_profile_2[] = { + {0xCF, (uint8_t[]){0x00, 0xC1, 0x30}, 3, 0}, + {0xED, (uint8_t[]){0x64, 0x03, 0x12, 0x81}, 4, 0}, + {0xE8, (uint8_t[]){0x85, 0x00, 0x78}, 3, 0}, + {0xCB, (uint8_t[]){0x39, 0x2C, 0x00, 0x34, 0x02}, 5, 0}, + {0xF7, (uint8_t[]){0x20}, 1, 0}, + {0xEA, (uint8_t[]){0x00, 0x00}, 2, 0}, + {0xC0, (uint8_t[]){0x13}, 1, 0}, + {0xC1, (uint8_t[]){0x13}, 1, 0}, + {0xC5, (uint8_t[]){0x22, 0x35}, 2, 0}, + {0xC7, (uint8_t[]){0xBD}, 1, 0}, + {0x36, (uint8_t[]){0x08}, 1, 0}, + {0xB6, (uint8_t[]){0x0A, 0xA2}, 2, 0}, + {0x3A, (uint8_t[]){0x55}, 1, 0}, + {0xF6, (uint8_t[]){0x01, 0x30}, 2, 0}, + {0xB1, (uint8_t[]){0x00, 0x1B}, 2, 0}, + {0xF2, (uint8_t[]){0x00}, 1, 0}, + {0x26, (uint8_t[]){0x01}, 1, 0}, + {0xE0, + (uint8_t[]){0x0F, 0x35, 0x31, 0x0B, 0x0E, 0x06, 0x49, 0xA7, 0x33, 0x07, 0x0F, 0x03, 0x0C, 0x0A, + 0x00}, + 15, 0}, + {0xE1, + (uint8_t[]){0x00, 0x0A, 0x0F, 0x04, 0x11, 0x08, 0x36, 0x58, 0x4D, 0x07, 0x10, 0x0C, 0x32, 0x34, + 0x0F}, + 15, 0}, + {0x11, NULL, 0, 0}, + {0x29, NULL, 0, 0}, +}; +#endif +#endif + +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 (panel->spi_ready) { + if (spi_bus_free(MYBOT_DISPLAY_SPI_HOST) != ESP_OK) { + return -1; + } + panel->spi_ready = false; + } + panel->ready = false; + panel->x_alignment = 0; + panel->y_alignment = 0; + return 0; +} + +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; + + const spi_bus_config_t bus_config = { + .mosi_io_num = MYBOT_DISPLAY_MOSI, + .miso_io_num = GPIO_NUM_NC, + .sclk_io_num = MYBOT_DISPLAY_SCLK, + .quadwp_io_num = GPIO_NUM_NC, + .quadhd_io_num = GPIO_NUM_NC, + .max_transfer_sz = MYBOT_DISPLAY_WIDTH * MYBOT_DISPLAY_TRANSFER_ROWS * sizeof(uint16_t), + }; + esp_err_t result = spi_bus_initialize(MYBOT_DISPLAY_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO); + if (result != ESP_OK) { + goto fail; + } + panel->spi_ready = true; + + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = MYBOT_DISPLAY_CS, + .dc_gpio_num = MYBOT_DISPLAY_DC, + .spi_mode = MYBOT_DISPLAY_SPI_MODE, + .pclk_hz = MYBOT_DISPLAY_PIXEL_CLOCK_HZ, + .trans_queue_depth = 2, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + }; + result = esp_lcd_new_panel_io_spi(MYBOT_DISPLAY_SPI_HOST, &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, + .flags.reset_active_high = MYBOT_DISPLAY_RESET_ACTIVE_HIGH, + }; +#if MYBOT_PANEL_CONTROLLER == 1 + result = esp_lcd_new_panel_gc9a01(panel->io, &panel_config, &panel->panel); +#elif MYBOT_PANEL_CONTROLLER == 2 + result = esp_lcd_new_panel_nv3023(panel->io, &panel_config, &panel->panel); +#else +#if MYBOT_ILI9341_PROFILE == 1 + const ili9341_vendor_config_t vendor_config = { + .init_cmds = s_ili9341_profile_1, + .init_cmds_size = sizeof(s_ili9341_profile_1) / sizeof(s_ili9341_profile_1[0]), + }; + panel_config.vendor_config = (void *)&vendor_config; +#elif MYBOT_ILI9341_PROFILE == 2 + const ili9341_vendor_config_t vendor_config = { + .init_cmds = s_ili9341_profile_2, + .init_cmds_size = sizeof(s_ili9341_profile_2) / sizeof(s_ili9341_profile_2[0]), + }; + panel_config.vendor_config = (void *)&vendor_config; +#endif + result = esp_lcd_new_panel_ili9341(panel->io, &panel_config, &panel->panel); +#endif + 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 = 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 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/sh8601/sh8601_panel.c b/components/mybot_platform/src/drivers/display/panels/sh8601/sh8601_panel.c new file mode 100644 index 0000000..f36babd --- /dev/null +++ b/components/mybot_platform/src/drivers/display/panels/sh8601/sh8601_panel.c @@ -0,0 +1,210 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "board_config.h" +#include "display/display_panel.h" + +#include "driver/spi_master.h" +#include "esp_err.h" +#include "esp_lcd_io_spi.h" +#include "esp_lcd_sh8601.h" +#include "esp_log.h" + +#include +#include + +#define TAG "sh8601_panel" + +#if MYBOT_SH8601_PROFILE == 1 +static const sh8601_lcd_init_cmd_t s_profile_18[] = { + {0x11, (uint8_t[]){0x00}, 0, 120}, + {0x44, (uint8_t[]){0x01, 0xD1}, 2, 0}, + {0x35, (uint8_t[]){0x00}, 1, 0}, + {0x53, (uint8_t[]){0x20}, 1, 10}, + {0x2A, (uint8_t[]){0x00, 0x00, 0x01, 0x6F}, 4, 0}, + {0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xBF}, 4, 0}, + {0x51, (uint8_t[]){0x00}, 1, 10}, + {0x29, (uint8_t[]){0x00}, 0, 10}, +}; +#endif + +#if MYBOT_SH8601_PROFILE == 2 +static const sh8601_lcd_init_cmd_t s_profile_132[] = { + {0xFE, (uint8_t[]){0x00}, 1, 0}, + {0xC4, (uint8_t[]){0x80}, 1, 0}, + {0x3A, (uint8_t[]){0x55}, 1, 0}, + {0x35, (uint8_t[]){0x00}, 1, 0}, + {0x53, (uint8_t[]){0x20}, 1, 0}, + {0x51, (uint8_t[]){0xFF}, 1, 0}, + {0x63, (uint8_t[]){0xFF}, 1, 0}, + {0x2A, (uint8_t[]){0x00, 0x06, 0x01, 0xD7}, 4, 0}, + {0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xD1}, 4, 0}, + {0x11, NULL, 0, 100}, + {0x29, NULL, 0, 0}, +}; +#endif + +#if MYBOT_SH8601_PROFILE == 3 +static const sh8601_lcd_init_cmd_t s_profile_143c[] = { + {0xFE, (uint8_t[]){0x00}, 1, 0}, + {0xC4, (uint8_t[]){0x80}, 1, 0}, + {0x3A, (uint8_t[]){0x55}, 1, 0}, + {0x35, (uint8_t[]){0x00}, 1, 0}, + {0x53, (uint8_t[]){0x20}, 1, 0}, + {0x51, (uint8_t[]){0xFF}, 1, 0}, + {0x36, (uint8_t[]){0xC0}, 1, 0}, + {0x63, (uint8_t[]){0xFF}, 1, 0}, + {0x2A, (uint8_t[]){0x00, 0x06, 0x01, 0xD7}, 4, 0}, + {0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xD1}, 4, 0}, + {0x11, NULL, 0, 100}, + {0x29, NULL, 0, 0}, +}; +#endif + +#if MYBOT_SH8601_PROFILE == 4 +static const sh8601_lcd_init_cmd_t s_profile_206[] = { + {0x11, (uint8_t[]){0x00}, 0, 120}, + {0xC4, (uint8_t[]){0x80}, 1, 0}, + {0x44, (uint8_t[]){0x01, 0xD1}, 2, 0}, + {0x35, (uint8_t[]){0x00}, 1, 0}, + {0x53, (uint8_t[]){0x20}, 1, 10}, + {0x63, (uint8_t[]){0xFF}, 1, 10}, + {0x51, (uint8_t[]){0x00}, 1, 10}, + {0x2A, (uint8_t[]){0x00, 0x16, 0x01, 0xAF}, 4, 0}, + {0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xF5}, 4, 0}, + {0x29, (uint8_t[]){0x00}, 0, 10}, + {0x51, (uint8_t[]){0xFF}, 1, 0}, +}; +#endif + +static const sh8601_lcd_init_cmd_t *selected_init(size_t *size) { +#if MYBOT_SH8601_PROFILE == 1 + *size = sizeof(s_profile_18) / sizeof(s_profile_18[0]); + return s_profile_18; +#elif MYBOT_SH8601_PROFILE == 2 + *size = sizeof(s_profile_132) / sizeof(s_profile_132[0]); + return s_profile_132; +#elif MYBOT_SH8601_PROFILE == 3 + *size = sizeof(s_profile_143c) / sizeof(s_profile_143c[0]); + return s_profile_143c; +#elif MYBOT_SH8601_PROFILE == 4 + *size = sizeof(s_profile_206) / sizeof(s_profile_206[0]); + return s_profile_206; +#else +#error "Unsupported MYBOT_SH8601_PROFILE" +#endif +} + +int mybot_display_panel_close(mybot_display_panel_t *panel) { + if (!panel) { + return -1; + } + 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 (panel->spi_ready) { + if (spi_bus_free(MYBOT_DISPLAY_SPI_HOST) != ESP_OK) { + return -1; + } + panel->spi_ready = false; + } + panel->ready = false; + panel->x_alignment = 0; + panel->y_alignment = 0; + return 0; +} + +int mybot_display_panel_open(mybot_display_panel_t *panel) { + if (!panel || panel->ready || mybot_display_panel_close(panel) < 0) { + return -1; + } + const spi_bus_config_t bus_config = { + .sclk_io_num = MYBOT_DISPLAY_PCLK, + .data0_io_num = MYBOT_DISPLAY_DATA0, + .data1_io_num = MYBOT_DISPLAY_DATA1, + .data2_io_num = MYBOT_DISPLAY_DATA2, + .data3_io_num = MYBOT_DISPLAY_DATA3, + .max_transfer_sz = MYBOT_DISPLAY_WIDTH * MYBOT_DISPLAY_TRANSFER_ROWS * sizeof(uint16_t), + .flags = SPICOMMON_BUSFLAG_QUAD, + }; + esp_err_t result = spi_bus_initialize(MYBOT_DISPLAY_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO); + if (result != ESP_OK) { + goto fail; + } + panel->spi_ready = true; + + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = MYBOT_DISPLAY_CS, + .dc_gpio_num = GPIO_NUM_NC, + .spi_mode = 0, + .pclk_hz = MYBOT_DISPLAY_PIXEL_CLOCK_HZ, + .trans_queue_depth = 2, + .lcd_cmd_bits = 32, + .lcd_param_bits = 8, + .flags.quad_mode = true, + }; + result = esp_lcd_new_panel_io_spi(MYBOT_DISPLAY_SPI_HOST, &io_config, &panel->io); + if (result != ESP_OK) { + goto fail; + } + + size_t command_count = 0; + const sh8601_lcd_init_cmd_t *commands = selected_init(&command_count); + const sh8601_vendor_config_t vendor_config = { + .init_cmds = commands, + .init_cmds_size = command_count, + .flags.use_qspi_interface = true, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = MYBOT_DISPLAY_RESET, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + .flags.reset_active_high = MYBOT_DISPLAY_RESET_ACTIVE_HIGH, + .vendor_config = (void *)&vendor_config, + }; + result = esp_lcd_new_panel_sh8601(panel->io, &panel_config, &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_reset(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_init(panel->panel); + } + if (result == ESP_OK) { + result = esp_lcd_panel_invert_color(panel->panel, MYBOT_DISPLAY_INVERT_COLOR); + } + 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) { + goto fail; + } + panel->ready = true; + panel->x_alignment = 2; + panel->y_alignment = 2; + return 0; + +fail: + ESP_LOGE(TAG, "event=panel action=initialize 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/st77916/st77916_lcd185_panel.c b/components/mybot_platform/src/drivers/display/panels/st77916/st77916_lcd185_panel.c new file mode 100644 index 0000000..bdfb7de --- /dev/null +++ b/components/mybot_platform/src/drivers/display/panels/st77916/st77916_lcd185_panel.c @@ -0,0 +1,345 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "board_config.h" +#include "display/board_backlight.h" +#include "display/display_panel.h" + +#include "driver/spi_master.h" +#include "esp_err.h" +#include "esp_lcd_io_spi.h" +#include "esp_lcd_st77916.h" +#include "esp_log.h" + +#include +#include + +#define TAG "st77916_lcd185" +#define LCD_OPCODE_READ_CMD 0x0B + +static const st77916_lcd_init_cmd_t s_vendor_init[] = { + {0xF0, (uint8_t[]){0x28}, 1, 0}, + {0xF2, (uint8_t[]){0x28}, 1, 0}, + {0x73, (uint8_t[]){0xF0}, 1, 0}, + {0x7C, (uint8_t[]){0xD1}, 1, 0}, + {0x83, (uint8_t[]){0xE0}, 1, 0}, + {0x84, (uint8_t[]){0x61}, 1, 0}, + {0xF2, (uint8_t[]){0x82}, 1, 0}, + {0xF0, (uint8_t[]){0x00}, 1, 0}, + {0xF0, (uint8_t[]){0x01}, 1, 0}, + {0xF1, (uint8_t[]){0x01}, 1, 0}, + {0xB0, (uint8_t[]){0x56}, 1, 0}, + {0xB1, (uint8_t[]){0x4D}, 1, 0}, + {0xB2, (uint8_t[]){0x24}, 1, 0}, + {0xB4, (uint8_t[]){0x87}, 1, 0}, + {0xB5, (uint8_t[]){0x44}, 1, 0}, + {0xB6, (uint8_t[]){0x8B}, 1, 0}, + {0xB7, (uint8_t[]){0x40}, 1, 0}, + {0xB8, (uint8_t[]){0x86}, 1, 0}, + {0xBA, (uint8_t[]){0x00}, 1, 0}, + {0xBB, (uint8_t[]){0x08}, 1, 0}, + {0xBC, (uint8_t[]){0x08}, 1, 0}, + {0xBD, (uint8_t[]){0x00}, 1, 0}, + {0xC0, (uint8_t[]){0x80}, 1, 0}, + {0xC1, (uint8_t[]){0x10}, 1, 0}, + {0xC2, (uint8_t[]){0x37}, 1, 0}, + {0xC3, (uint8_t[]){0x80}, 1, 0}, + {0xC4, (uint8_t[]){0x10}, 1, 0}, + {0xC5, (uint8_t[]){0x37}, 1, 0}, + {0xC6, (uint8_t[]){0xA9}, 1, 0}, + {0xC7, (uint8_t[]){0x41}, 1, 0}, + {0xC8, (uint8_t[]){0x01}, 1, 0}, + {0xC9, (uint8_t[]){0xA9}, 1, 0}, + {0xCA, (uint8_t[]){0x41}, 1, 0}, + {0xCB, (uint8_t[]){0x01}, 1, 0}, + {0xD0, (uint8_t[]){0x91}, 1, 0}, + {0xD1, (uint8_t[]){0x68}, 1, 0}, + {0xD2, (uint8_t[]){0x68}, 1, 0}, + {0xF5, (uint8_t[]){0x00, 0xA5}, 2, 0}, + {0xDD, (uint8_t[]){0x4F}, 1, 0}, + {0xDE, (uint8_t[]){0x4F}, 1, 0}, + {0xF1, (uint8_t[]){0x10}, 1, 0}, + {0xF0, (uint8_t[]){0x00}, 1, 0}, + {0xF0, (uint8_t[]){0x02}, 1, 0}, + {0xE0, + (uint8_t[]){0xF0, 0x0A, 0x10, 0x09, 0x09, 0x36, 0x35, 0x33, 0x4A, 0x29, 0x15, 0x15, 0x2E, + 0x34}, + 14, 0}, + {0xE1, + (uint8_t[]){0xF0, 0x0A, 0x0F, 0x08, 0x08, 0x05, 0x34, 0x33, 0x4A, 0x39, 0x15, 0x15, 0x2D, + 0x33}, + 14, 0}, + {0xF0, (uint8_t[]){0x10}, 1, 0}, + {0xF3, (uint8_t[]){0x10}, 1, 0}, + {0xE0, (uint8_t[]){0x07}, 1, 0}, + {0xE1, (uint8_t[]){0x00}, 1, 0}, + {0xE2, (uint8_t[]){0x00}, 1, 0}, + {0xE3, (uint8_t[]){0x00}, 1, 0}, + {0xE4, (uint8_t[]){0xE0}, 1, 0}, + {0xE5, (uint8_t[]){0x06}, 1, 0}, + {0xE6, (uint8_t[]){0x21}, 1, 0}, + {0xE7, (uint8_t[]){0x01}, 1, 0}, + {0xE8, (uint8_t[]){0x05}, 1, 0}, + {0xE9, (uint8_t[]){0x02}, 1, 0}, + {0xEA, (uint8_t[]){0xDA}, 1, 0}, + {0xEB, (uint8_t[]){0x00}, 1, 0}, + {0xEC, (uint8_t[]){0x00}, 1, 0}, + {0xED, (uint8_t[]){0x0F}, 1, 0}, + {0xEE, (uint8_t[]){0x00}, 1, 0}, + {0xEF, (uint8_t[]){0x00}, 1, 0}, + {0xF8, (uint8_t[]){0x00}, 1, 0}, + {0xF9, (uint8_t[]){0x00}, 1, 0}, + {0xFA, (uint8_t[]){0x00}, 1, 0}, + {0xFB, (uint8_t[]){0x00}, 1, 0}, + {0xFC, (uint8_t[]){0x00}, 1, 0}, + {0xFD, (uint8_t[]){0x00}, 1, 0}, + {0xFE, (uint8_t[]){0x00}, 1, 0}, + {0xFF, (uint8_t[]){0x00}, 1, 0}, + {0x60, (uint8_t[]){0x40}, 1, 0}, + {0x61, (uint8_t[]){0x04}, 1, 0}, + {0x62, (uint8_t[]){0x00}, 1, 0}, + {0x63, (uint8_t[]){0x42}, 1, 0}, + {0x64, (uint8_t[]){0xD9}, 1, 0}, + {0x65, (uint8_t[]){0x00}, 1, 0}, + {0x66, (uint8_t[]){0x00}, 1, 0}, + {0x67, (uint8_t[]){0x00}, 1, 0}, + {0x68, (uint8_t[]){0x00}, 1, 0}, + {0x69, (uint8_t[]){0x00}, 1, 0}, + {0x6A, (uint8_t[]){0x00}, 1, 0}, + {0x6B, (uint8_t[]){0x00}, 1, 0}, + {0x70, (uint8_t[]){0x40}, 1, 0}, + {0x71, (uint8_t[]){0x03}, 1, 0}, + {0x72, (uint8_t[]){0x00}, 1, 0}, + {0x73, (uint8_t[]){0x42}, 1, 0}, + {0x74, (uint8_t[]){0xD8}, 1, 0}, + {0x75, (uint8_t[]){0x00}, 1, 0}, + {0x76, (uint8_t[]){0x00}, 1, 0}, + {0x77, (uint8_t[]){0x00}, 1, 0}, + {0x78, (uint8_t[]){0x00}, 1, 0}, + {0x79, (uint8_t[]){0x00}, 1, 0}, + {0x7A, (uint8_t[]){0x00}, 1, 0}, + {0x7B, (uint8_t[]){0x00}, 1, 0}, + {0x80, (uint8_t[]){0x48}, 1, 0}, + {0x81, (uint8_t[]){0x00}, 1, 0}, + {0x82, (uint8_t[]){0x06}, 1, 0}, + {0x83, (uint8_t[]){0x02}, 1, 0}, + {0x84, (uint8_t[]){0xD6}, 1, 0}, + {0x85, (uint8_t[]){0x04}, 1, 0}, + {0x86, (uint8_t[]){0x00}, 1, 0}, + {0x87, (uint8_t[]){0x00}, 1, 0}, + {0x88, (uint8_t[]){0x48}, 1, 0}, + {0x89, (uint8_t[]){0x00}, 1, 0}, + {0x8A, (uint8_t[]){0x08}, 1, 0}, + {0x8B, (uint8_t[]){0x02}, 1, 0}, + {0x8C, (uint8_t[]){0xD8}, 1, 0}, + {0x8D, (uint8_t[]){0x04}, 1, 0}, + {0x8E, (uint8_t[]){0x00}, 1, 0}, + {0x8F, (uint8_t[]){0x00}, 1, 0}, + {0x90, (uint8_t[]){0x48}, 1, 0}, + {0x91, (uint8_t[]){0x00}, 1, 0}, + {0x92, (uint8_t[]){0x0A}, 1, 0}, + {0x93, (uint8_t[]){0x02}, 1, 0}, + {0x94, (uint8_t[]){0xDA}, 1, 0}, + {0x95, (uint8_t[]){0x04}, 1, 0}, + {0x96, (uint8_t[]){0x00}, 1, 0}, + {0x97, (uint8_t[]){0x00}, 1, 0}, + {0x98, (uint8_t[]){0x48}, 1, 0}, + {0x99, (uint8_t[]){0x00}, 1, 0}, + {0x9A, (uint8_t[]){0x0C}, 1, 0}, + {0x9B, (uint8_t[]){0x02}, 1, 0}, + {0x9C, (uint8_t[]){0xDC}, 1, 0}, + {0x9D, (uint8_t[]){0x04}, 1, 0}, + {0x9E, (uint8_t[]){0x00}, 1, 0}, + {0x9F, (uint8_t[]){0x00}, 1, 0}, + {0xA0, (uint8_t[]){0x48}, 1, 0}, + {0xA1, (uint8_t[]){0x00}, 1, 0}, + {0xA2, (uint8_t[]){0x05}, 1, 0}, + {0xA3, (uint8_t[]){0x02}, 1, 0}, + {0xA4, (uint8_t[]){0xD5}, 1, 0}, + {0xA5, (uint8_t[]){0x04}, 1, 0}, + {0xA6, (uint8_t[]){0x00}, 1, 0}, + {0xA7, (uint8_t[]){0x00}, 1, 0}, + {0xA8, (uint8_t[]){0x48}, 1, 0}, + {0xA9, (uint8_t[]){0x00}, 1, 0}, + {0xAA, (uint8_t[]){0x07}, 1, 0}, + {0xAB, (uint8_t[]){0x02}, 1, 0}, + {0xAC, (uint8_t[]){0xD7}, 1, 0}, + {0xAD, (uint8_t[]){0x04}, 1, 0}, + {0xAE, (uint8_t[]){0x00}, 1, 0}, + {0xAF, (uint8_t[]){0x00}, 1, 0}, + {0xB0, (uint8_t[]){0x48}, 1, 0}, + {0xB1, (uint8_t[]){0x00}, 1, 0}, + {0xB2, (uint8_t[]){0x09}, 1, 0}, + {0xB3, (uint8_t[]){0x02}, 1, 0}, + {0xB4, (uint8_t[]){0xD9}, 1, 0}, + {0xB5, (uint8_t[]){0x04}, 1, 0}, + {0xB6, (uint8_t[]){0x00}, 1, 0}, + {0xB7, (uint8_t[]){0x00}, 1, 0}, + {0xB8, (uint8_t[]){0x48}, 1, 0}, + {0xB9, (uint8_t[]){0x00}, 1, 0}, + {0xBA, (uint8_t[]){0x0B}, 1, 0}, + {0xBB, (uint8_t[]){0x02}, 1, 0}, + {0xBC, (uint8_t[]){0xDB}, 1, 0}, + {0xBD, (uint8_t[]){0x04}, 1, 0}, + {0xBE, (uint8_t[]){0x00}, 1, 0}, + {0xBF, (uint8_t[]){0x00}, 1, 0}, + {0xC0, (uint8_t[]){0x10}, 1, 0}, + {0xC1, (uint8_t[]){0x47}, 1, 0}, + {0xC2, (uint8_t[]){0x56}, 1, 0}, + {0xC3, (uint8_t[]){0x65}, 1, 0}, + {0xC4, (uint8_t[]){0x74}, 1, 0}, + {0xC5, (uint8_t[]){0x88}, 1, 0}, + {0xC6, (uint8_t[]){0x99}, 1, 0}, + {0xC7, (uint8_t[]){0x01}, 1, 0}, + {0xC8, (uint8_t[]){0xBB}, 1, 0}, + {0xC9, (uint8_t[]){0xAA}, 1, 0}, + {0xD0, (uint8_t[]){0x10}, 1, 0}, + {0xD1, (uint8_t[]){0x47}, 1, 0}, + {0xD2, (uint8_t[]){0x56}, 1, 0}, + {0xD3, (uint8_t[]){0x65}, 1, 0}, + {0xD4, (uint8_t[]){0x74}, 1, 0}, + {0xD5, (uint8_t[]){0x88}, 1, 0}, + {0xD6, (uint8_t[]){0x99}, 1, 0}, + {0xD7, (uint8_t[]){0x01}, 1, 0}, + {0xD8, (uint8_t[]){0xBB}, 1, 0}, + {0xD9, (uint8_t[]){0xAA}, 1, 0}, + {0xF3, (uint8_t[]){0x01}, 1, 0}, + {0xF0, (uint8_t[]){0x00}, 1, 0}, + {0x21, NULL, 0, 0}, + {0x11, NULL, 0, 120}, + {0x29, NULL, 0, 0}, +}; + +static esp_err_t create_panel_io(uint32_t clock_hz, esp_lcd_panel_io_handle_t *io) { + const esp_lcd_panel_io_spi_config_t config = { + .cs_gpio_num = MYBOT_DISPLAY_CS, + .dc_gpio_num = GPIO_NUM_NC, + .spi_mode = 0, + .pclk_hz = clock_hz, + .trans_queue_depth = 2, + .lcd_cmd_bits = 32, + .lcd_param_bits = 8, + .flags.quad_mode = true, + }; + return esp_lcd_new_panel_io_spi(MYBOT_DISPLAY_SPI_HOST, &config, io); +} + +int mybot_display_panel_close(mybot_display_panel_t *panel) { + if (!panel) { + return -1; + } + if (panel->backlight_ready) { + if (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 (panel->spi_ready) { + if (spi_bus_free(MYBOT_DISPLAY_SPI_HOST) != ESP_OK) { + return -1; + } + panel->spi_ready = false; + } + panel->ready = false; + return 0; +} + +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; + + const spi_bus_config_t bus_config = { + .sclk_io_num = MYBOT_DISPLAY_PCLK, + .data0_io_num = MYBOT_DISPLAY_DATA0, + .data1_io_num = MYBOT_DISPLAY_DATA1, + .data2_io_num = MYBOT_DISPLAY_DATA2, + .data3_io_num = MYBOT_DISPLAY_DATA3, + .max_transfer_sz = MYBOT_DISPLAY_WIDTH * MYBOT_DISPLAY_TRANSFER_ROWS * sizeof(uint16_t), + .flags = SPICOMMON_BUSFLAG_QUAD, + }; + esp_err_t result = spi_bus_initialize(MYBOT_DISPLAY_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO); + if (result != ESP_OK) { + goto fail; + } + panel->spi_ready = true; + + esp_lcd_panel_io_handle_t probe_io = NULL; + uint8_t panel_version[4] = {0}; + result = create_panel_io(3 * 1000 * 1000, &probe_io); + if (result == ESP_OK) { + uint32_t command = ((uint32_t)LCD_OPCODE_READ_CMD << 24) | 0x04; + result = esp_lcd_panel_io_rx_param(probe_io, command, panel_version, sizeof(panel_version)); + (void)esp_lcd_panel_io_del(probe_io); + } + if (result != ESP_OK) { + ESP_LOGW(TAG, "event=panel action=read_version result=error code=%s", + esp_err_to_name(result)); + result = ESP_OK; + } + + result = create_panel_io(MYBOT_DISPLAY_PIXEL_CLOCK_HZ, &panel->io); + if (result != ESP_OK) { + goto fail; + } + st77916_vendor_config_t vendor_config = { + .flags.use_qspi_interface = true, + }; + if (panel_version[0] == 0x00 && panel_version[1] == 0x02 && panel_version[2] == 0x7F && + panel_version[3] == 0x7F) { + vendor_config.init_cmds = s_vendor_init; + vendor_config.init_cmds_size = sizeof(s_vendor_init) / sizeof(s_vendor_init[0]); + } + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = MYBOT_DISPLAY_RESET, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + .vendor_config = &vendor_config, + }; + result = esp_lcd_new_panel_st77916(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_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; + return 0; + +fail: + ESP_LOGE(TAG, "event=panel action=initialize 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/renderers/lvgl/lvgl_view.cc b/components/mybot_platform/src/drivers/display/renderers/lvgl/lvgl_view.cc index 852057f..14ad920 100644 --- a/components/mybot_platform/src/drivers/display/renderers/lvgl/lvgl_view.cc +++ b/components/mybot_platform/src/drivers/display/renderers/lvgl/lvgl_view.cc @@ -335,6 +335,13 @@ void update_notifications(const mybot_lcd_content_t &content) { int view_width() { return s_view.width; } + +int title_y(bool pairing_code) { + if (s_view.height < 200) { + return pairing_code ? 8 : 36; + } + return pairing_code ? 20 : 81; +} } // namespace int mybot_lvgl_view_create_sized(lv_display_t *display, int width, int height) { @@ -345,20 +352,41 @@ int mybot_lvgl_view_create_sized(lv_display_t *display, int width, int height) { } s_view.width = width; s_view.height = height; + const bool compact = height < 200; +#ifndef MYBOT_DISPLAY_ROUND +#define MYBOT_DISPLAY_ROUND 0 +#endif +#if MYBOT_DISPLAY_ROUND + const int margin = width / 5; + const int header_x = margin; + const int header_y = height >= 400 ? 44 : 36; + const int header_width = width - margin * 2; + const int card_y = height >= 400 ? 92 : 82; + const int card_height = height >= 400 ? 220 : height - 150; + const int footer_y = height - 66; +#else const int margin = width >= 240 ? 12 : 4; + const int header_x = 0; + const int header_y = 0; + const int header_width = width; + const int card_y = compact ? 22 : 46; + const int card_height = compact ? height - 54 : (height >= 220 ? 138 : (height - 102)); + const int footer_y = compact ? height - 22 : height - 44; +#endif const int card_width = width - margin * 2; const int card_x = margin; - const int card_y = 46; - const int card_height = height >= 220 ? 138 : (height - 102); - const int center_x = (card_width - 64) / 2; - const int footer_y = height - 44; + const int header_height = compact ? 20 : 38; + const int badge_size = compact ? 32 : 64; + const int center_x = (card_width - badge_size) / 2; s_view.root = make_panel(lv_display_get_screen_active(display), 0, 0, width, height, kTheme.background); if (!s_view.root) { return -1; } - lv_obj_t *header = make_panel(s_view.root, 0, 0, width, 38, kTheme.background); - lv_obj_t *footer = make_panel(s_view.root, margin, footer_y, card_width, 34, kTheme.surface); + lv_obj_t *header = + make_panel(s_view.root, header_x, header_y, header_width, header_height, kTheme.background); + lv_obj_t *footer = + make_panel(s_view.root, margin, footer_y, card_width, compact ? 18 : 34, kTheme.surface); s_view.card = make_panel(s_view.root, card_x, card_y, card_width, card_height, kTheme.surface); if (!header || !footer || !s_view.card) { mybot_lvgl_view_destroy(); @@ -368,8 +396,11 @@ int mybot_lvgl_view_create_sized(lv_display_t *display, int width, int height) { lv_obj_set_style_border_color(s_view.card, lv_color_hex(kTheme.border), 0); lv_obj_set_style_border_width(s_view.card, 1, 0); lv_obj_set_style_radius(footer, 12, 0); - s_view.badge = make_panel(s_view.card, center_x, 8, 64, 64, kTheme.surface); - s_view.activity = make_panel(s_view.card, center_x, card_height - 26, 64, 16, kTheme.surface); + s_view.badge = + make_panel(s_view.card, center_x, compact ? 2 : 8, badge_size, badge_size, kTheme.surface); + s_view.activity = + make_panel(s_view.card, center_x, compact ? card_height - 16 : card_height - 26, + compact ? 40 : 64, compact ? 10 : 16, kTheme.surface); if (!s_view.badge || !s_view.activity) { mybot_lvgl_view_destroy(); return -1; @@ -378,25 +409,30 @@ int mybot_lvgl_view_create_sized(lv_display_t *display, int width, int height) { lv_obj_set_style_border_width(s_view.badge, 2, 0); lv_obj_set_style_bg_opa(s_view.activity, LV_OPA_TRANSP, 0); - const int brand_width = width < 240 ? width / 2 : width / 3; - s_view.brand = make_label(header, 8, 4, brand_width, 30); - s_view.status = make_label(header, brand_width, 4, width - brand_width - 8, 30); - s_view.notification = make_label(header, 4, 4, width - 8, 30); - s_view.icon = make_label(s_view.badge, 0, 0, 60, 40); + const int brand_width = header_width < 240 ? header_width / 2 : header_width / 3; + const int header_label_height = compact ? 18 : 30; + s_view.brand = + make_label(header, compact ? 2 : 8, compact ? 1 : 4, brand_width, header_label_height); + s_view.status = make_label(header, brand_width, compact ? 1 : 4, header_width - brand_width - 8, + header_label_height); + s_view.notification = + make_label(header, compact ? 2 : 4, compact ? 1 : 4, header_width - 4, header_label_height); + s_view.icon = make_label(s_view.badge, 0, 0, badge_size - 4, compact ? 22 : 40); s_view.emoji = lv_image_create(s_view.card); - s_view.title = make_label(s_view.card, 4, 81, card_width - 8, 30); - s_view.code = make_label(s_view.card, 2, 63, card_width - 4, 45); - s_view.notice = make_label(footer, 2, 3, card_width - 4, 28); + s_view.title = make_label(s_view.card, 4, title_y(false), card_width - 8, compact ? 22 : 30); + s_view.code = make_label(s_view.card, 2, compact ? 28 : 63, card_width - 4, compact ? 32 : 45); + s_view.notice = make_label(footer, 2, compact ? 0 : 3, card_width - 4, compact ? 18 : 28); if (!s_view.brand || !s_view.status || !s_view.notification || !s_view.icon || !s_view.emoji || !s_view.title || !s_view.code || !s_view.notice) { mybot_lvgl_view_destroy(); return -1; } - lv_obj_set_pos(s_view.emoji, center_x, 8); - lv_obj_set_size(s_view.emoji, 64, 64); + lv_obj_set_pos(s_view.emoji, center_x, compact ? 2 : 8); + lv_obj_set_size(s_view.emoji, badge_size, badge_size); lv_obj_set_style_text_align(s_view.brand, LV_TEXT_ALIGN_LEFT, 0); if (width < 240) { - lv_obj_set_style_text_font(s_view.brand, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_font(s_view.brand, + compact ? &lv_font_montserrat_10 : &lv_font_montserrat_14, 0); #if CONFIG_MYBOT_LANGUAGE_ZH_CN lv_obj_set_style_text_font(s_view.status, &mybot_lvgl_font_20, 0); lv_obj_set_style_text_font(s_view.notification, &mybot_lvgl_font_20, 0); @@ -418,8 +454,9 @@ int mybot_lvgl_view_create_sized(lv_display_t *display, int width, int height) { lv_obj_center(s_view.icon); lv_obj_set_style_text_letter_space(s_view.code, 0, 0); for (unsigned i = 0; i < 5; ++i) { - s_view.bars[i] = - make_panel(s_view.activity, 7 + static_cast(i) * 11, 4, 6, 8, kTheme.blue); + s_view.bars[i] = make_panel( + s_view.activity, compact ? 2 + static_cast(i) * 8 : 7 + static_cast(i) * 11, + compact ? 2 : 4, compact ? 5 : 6, compact ? 6 : 8, kTheme.blue); if (!s_view.bars[i]) { mybot_lvgl_view_destroy(); return -1; @@ -554,7 +591,7 @@ void mybot_lvgl_view_update(const mybot_lcd_content_t *content) { } if (!s_view.has_content || pairing_code != (s_view.previous.screen == MYBOT_LCD_SCREEN_PAIR_CODE)) { - lv_obj_set_y(s_view.title, pairing_code ? 20 : 81); + lv_obj_set_y(s_view.title, title_y(pairing_code)); } if (pairing_code && (!s_view.has_content || normalized.screen != s_view.previous.screen || std::strcmp(normalized.pair_code, s_view.previous.pair_code) != 0)) { diff --git a/components/mybot_platform/src/drivers/input/cst816s_boot_input.c b/components/mybot_platform/src/drivers/input/cst816s_boot_input.c new file mode 100644 index 0000000..81e6ad9 --- /dev/null +++ b/components/mybot_platform/src/drivers/input/cst816s_boot_input.c @@ -0,0 +1,412 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "board_config.h" + +#include +#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 == 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 +#error "Unsupported MYBOT_TOUCH_DRIVER_KIND" +#endif + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "iot_button.h" +#include "platform/board_actions.h" + +#include + +#define TAG "cst816s_input" + +#ifndef MYBOT_BOOT_BUTTON_LONG_PRESS_MS +#define MYBOT_BOOT_BUTTON_LONG_PRESS_MS 3000 +#endif + +#ifndef MYBOT_TOUCH_X_MAX +#define MYBOT_TOUCH_X_MAX (MYBOT_DISPLAY_WIDTH - 1) +#endif + +#ifndef MYBOT_TOUCH_Y_MAX +#define MYBOT_TOUCH_Y_MAX (MYBOT_DISPLAY_HEIGHT - 1) +#endif + +typedef struct { + portMUX_TYPE lock; + bool active; + bool started; + bool task_stop_requested; + unsigned callbacks_in_flight; + mybot_key_event_handler_t emit; + void *user_data; + button_handle_t boot; + esp_lcd_touch_handle_t touch; + esp_lcd_panel_io_handle_t touch_io; + TaskHandle_t touch_task; +} input_context_t; + +static input_context_t s_context = { + .lock = portMUX_INITIALIZER_UNLOCKED, +}; +static StaticSemaphore_t s_task_stopped_storage; +static SemaphoreHandle_t s_task_stopped; + +i2c_master_bus_handle_t mybot_board_touch_i2c_bus_handle(void); + +static bool adapter_is_active(void) { + portENTER_CRITICAL(&s_context.lock); + const bool active = s_context.active; + portEXIT_CRITICAL(&s_context.lock); + return active; +} + +static bool task_should_stop(void) { + portENTER_CRITICAL(&s_context.lock); + const bool stop = s_context.task_stop_requested; + portEXIT_CRITICAL(&s_context.lock); + return stop; +} + +static void emit_event(mybot_key_event_t event) { + mybot_key_event_handler_t emit = NULL; + void *user_data = NULL; + + portENTER_CRITICAL(&s_context.lock); + if (s_context.active && s_context.emit) { + ++s_context.callbacks_in_flight; + emit = s_context.emit; + user_data = s_context.user_data; + } + portEXIT_CRITICAL(&s_context.lock); + + if (!emit) { + return; + } + emit(event, user_data); + + portENTER_CRITICAL(&s_context.lock); + --s_context.callbacks_in_flight; + portEXIT_CRITICAL(&s_context.lock); +} + +static void handle_short_press(void) { + if (!adapter_is_active()) { + return; + } + const mybot_state_t state = mybot_get_state(); + if (state == MYBOT_STATE_READY) { + emit_event(MYBOT_KEY_EVENT_CONVERSATION_START); + } else if (state == MYBOT_STATE_IN_CONVERSATION) { + emit_event(MYBOT_KEY_EVENT_CONVERSATION_STOP); + } +} + +static void handle_long_press(const char *source) { + (void)source; + ESP_LOGI(TAG, "event=input source=%s action=wifi_provisioning_request", source); + if (mybot_board_handle_boot_long_press() < 0) { + ESP_LOGE(TAG, "event=provision_request source=%s result=error", source); + } +} + +static void boot_click(void *button, void *user_data) { + (void)button; + (void)user_data; + handle_short_press(); +} + +static void boot_long_press(void *button, void *user_data) { + (void)button; + (void)user_data; + handle_long_press("boot_button"); +} + +static void touch_task(void *argument) { + (void)argument; + bool pressed = false; + bool long_fired = false; + TickType_t pressed_at = 0; + + while (!task_should_stop()) { + 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; + const TickType_t now = xTaskGetTickCount(); + + if (down && !pressed) { + pressed = true; + long_fired = false; + pressed_at = now; + } else if (down && pressed && !long_fired && + now - pressed_at >= pdMS_TO_TICKS(MYBOT_TOUCH_LONG_PRESS_MS)) { + long_fired = true; + handle_long_press("touch"); + } else if (!down && pressed) { + pressed = false; + const TickType_t duration = now - pressed_at; + if (!long_fired && duration >= pdMS_TO_TICKS(MYBOT_TOUCH_SHORT_PRESS_MIN_MS)) { + handle_short_press(); + } + } + vTaskDelay(pdMS_TO_TICKS(MYBOT_TOUCH_POLL_INTERVAL_MS)); + } + + TaskHandle_t current = xTaskGetCurrentTaskHandle(); + portENTER_CRITICAL(&s_context.lock); + if (s_context.touch_task == current) { + s_context.touch_task = NULL; + } + portEXIT_CRITICAL(&s_context.lock); + xSemaphoreGive(s_task_stopped); + vTaskDelete(NULL); +} + +static int create_touch(void) { + i2c_master_bus_handle_t bus = mybot_board_touch_i2c_bus_handle(); + if (!bus) { + return -1; + } + +#if MYBOT_TOUCH_DRIVER_KIND == 1 + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = ESP_LCD_TOUCH_IO_I2C_CST816S_ADDRESS, + .scl_speed_hz = MYBOT_TOUCH_I2C_SPEED_HZ, + .control_phase_bytes = 1, + .dc_bit_offset = 0, + .lcd_cmd_bits = 8, + .lcd_param_bits = 0, + .flags.disable_control_phase = 1, + }; +#elif MYBOT_TOUCH_DRIVER_KIND == 2 + esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_FT5x06_CONFIG(); + io_config.scl_speed_hz = MYBOT_TOUCH_I2C_SPEED_HZ; +#elif MYBOT_TOUCH_DRIVER_KIND == 3 + esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG(); + io_config.scl_speed_hz = MYBOT_TOUCH_I2C_SPEED_HZ; +#endif + esp_err_t result = esp_lcd_new_panel_io_i2c(bus, &io_config, &s_context.touch_io); + if (result != ESP_OK) { + return -1; + } + + const esp_lcd_touch_config_t touch_config = { + .x_max = MYBOT_TOUCH_X_MAX, + .y_max = MYBOT_TOUCH_Y_MAX, + .rst_gpio_num = MYBOT_TOUCH_RESET, + .int_gpio_num = MYBOT_TOUCH_INTERRUPT, + .levels = + { + .reset = MYBOT_TOUCH_RESET_LEVEL, + .interrupt = MYBOT_TOUCH_INTERRUPT_LEVEL, + }, + .flags = + { + .swap_xy = MYBOT_TOUCH_SWAP_XY, + .mirror_x = MYBOT_TOUCH_MIRROR_X, + .mirror_y = MYBOT_TOUCH_MIRROR_Y, + }, + }; +#if MYBOT_TOUCH_DRIVER_KIND == 1 + result = esp_lcd_touch_new_i2c_cst816s(s_context.touch_io, &touch_config, &s_context.touch); +#elif MYBOT_TOUCH_DRIVER_KIND == 2 + result = esp_lcd_touch_new_i2c_ft5x06(s_context.touch_io, &touch_config, &s_context.touch); +#elif MYBOT_TOUCH_DRIVER_KIND == 3 + result = esp_lcd_touch_new_i2c_gt911(s_context.touch_io, &touch_config, &s_context.touch); +#endif + if (result != ESP_OK) { + (void)esp_lcd_panel_io_del(s_context.touch_io); + s_context.touch_io = NULL; + return -1; + } + return 0; +} + +static int release_input_devices(void) { + int result = 0; + if (s_context.boot && iot_button_delete(s_context.boot) == ESP_OK) { + s_context.boot = NULL; + } else if (s_context.boot) { + result = -1; + } + if (s_context.touch && esp_lcd_touch_del(s_context.touch) == ESP_OK) { + s_context.touch = NULL; + } else if (s_context.touch) { + result = -1; + } + if (!s_context.touch && s_context.touch_io && + esp_lcd_panel_io_del(s_context.touch_io) == ESP_OK) { + s_context.touch_io = NULL; + } else if (!s_context.touch && s_context.touch_io) { + result = -1; + } + return result; +} + +static int release_input(void) { + portENTER_CRITICAL(&s_context.lock); + s_context.active = false; + s_context.task_stop_requested = true; + TaskHandle_t task = s_context.touch_task; + portEXIT_CRITICAL(&s_context.lock); + + if (task && (!s_task_stopped || xSemaphoreTake(s_task_stopped, portMAX_DELAY) != pdTRUE)) { + ESP_LOGE(TAG, "event=input action=deinitialize result=error reason=task_join"); + return -1; + } + + unsigned in_flight = 0; + do { + portENTER_CRITICAL(&s_context.lock); + in_flight = s_context.callbacks_in_flight; + portEXIT_CRITICAL(&s_context.lock); + if (in_flight) { + vTaskDelay(pdMS_TO_TICKS(1)); + } + } while (in_flight); + + const int result = release_input_devices(); + 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; + if (!s_context.started) { + s_context.task_stop_requested = false; + } + portEXIT_CRITICAL(&s_context.lock); + return result; +} + +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; + portEXIT_CRITICAL(&s_context.lock); + if (already_started) { + return -1; + } + + if (!s_task_stopped) { + s_task_stopped = xSemaphoreCreateBinaryStatic(&s_task_stopped_storage); + } + if (!s_task_stopped) { + return -1; + } + (void)xSemaphoreTake(s_task_stopped, 0); + + const button_config_t button_config = { + .long_press_time = MYBOT_BOOT_BUTTON_LONG_PRESS_MS, + .short_press_time = 50, + }; + const button_gpio_config_t gpio_config = { + .gpio_num = MYBOT_BOOT_BUTTON_GPIO, + .active_level = 0, + .enable_power_save = false, + .disable_pull = false, + }; + esp_err_t result = iot_button_new_gpio_device(&button_config, &gpio_config, &s_context.boot); + if (result == ESP_OK) { + result = + iot_button_register_cb(s_context.boot, BUTTON_SINGLE_CLICK, NULL, boot_click, NULL); + } + if (result == ESP_OK) { + result = iot_button_register_cb(s_context.boot, BUTTON_LONG_PRESS_START, NULL, + boot_long_press, NULL); + } + if (result == ESP_OK && create_touch() < 0) { + result = ESP_FAIL; + } + if (result == ESP_OK) { + portENTER_CRITICAL(&s_context.lock); + s_context.task_stop_requested = false; + portEXIT_CRITICAL(&s_context.lock); + if (xTaskCreate(touch_task, "mybot_touch", 4096, NULL, 5, &s_context.touch_task) != + pdPASS) { + result = ESP_ERR_NO_MEM; + } + } + if (result != ESP_OK) { + portENTER_CRITICAL(&s_context.lock); + s_context.task_stop_requested = true; + portEXIT_CRITICAL(&s_context.lock); + if (release_input() < 0) { + ESP_LOGE(TAG, "event=input action=initialize cleanup=error"); + } + return -1; + } + + portENTER_CRITICAL(&s_context.lock); + s_context.started = true; + portEXIT_CRITICAL(&s_context.lock); + return 0; +} + +int mybot_cst816s_input_stop(void) { + return release_input(); +} + +static int input_init(void **out_context, mybot_key_event_handler_t emit, void *user_data) { + if (!out_context || !emit) { + return -1; + } + *out_context = NULL; + + portENTER_CRITICAL(&s_context.lock); + if (s_context.active || !s_context.started) { + portEXIT_CRITICAL(&s_context.lock); + return -1; + } + s_context.emit = emit; + s_context.user_data = user_data; + s_context.active = true; + portEXIT_CRITICAL(&s_context.lock); + *out_context = &s_context; + return 0; +} + +static void input_destroy(void *opaque) { + if (opaque != &s_context) { + return; + } + + portENTER_CRITICAL(&s_context.lock); + s_context.active = false; + unsigned in_flight = s_context.callbacks_in_flight; + portEXIT_CRITICAL(&s_context.lock); + while (in_flight) { + vTaskDelay(pdMS_TO_TICKS(1)); + portENTER_CRITICAL(&s_context.lock); + in_flight = s_context.callbacks_in_flight; + portEXIT_CRITICAL(&s_context.lock); + } + + portENTER_CRITICAL(&s_context.lock); + s_context.emit = NULL; + s_context.user_data = NULL; + portEXIT_CRITICAL(&s_context.lock); +} + +static const mybot_key_ops_t s_ops = { + .init = input_init, + .destroy = input_destroy, +}; + +const mybot_key_ops_t *mybot_cst816s_input_ops(void) { + return &s_ops; +} diff --git a/components/mybot_platform/src/drivers/input/gpio_buttons.c b/components/mybot_platform/src/drivers/input/gpio_buttons.c index 850bbd0..49daca6 100644 --- a/components/mybot_platform/src/drivers/input/gpio_buttons.c +++ b/components/mybot_platform/src/drivers/input/gpio_buttons.c @@ -117,16 +117,18 @@ int mybot_esp32s3_buttons_start(void) { button_handle_t boot = NULL; button_handle_t volume_up = NULL; button_handle_t volume_down = NULL; + const bool has_volume_up = MYBOT_VOLUME_UP_BUTTON_GPIO != GPIO_NUM_NC; + const bool has_volume_down = MYBOT_VOLUME_DOWN_BUTTON_GPIO != GPIO_NUM_NC; if (create_button(MYBOT_BOOT_BUTTON_GPIO, &boot) < 0 || - create_button(MYBOT_VOLUME_UP_BUTTON_GPIO, &volume_up) < 0 || - create_button(MYBOT_VOLUME_DOWN_BUTTON_GPIO, &volume_down) < 0 || + (has_volume_up && create_button(MYBOT_VOLUME_UP_BUTTON_GPIO, &volume_up) < 0) || + (has_volume_down && create_button(MYBOT_VOLUME_DOWN_BUTTON_GPIO, &volume_down) < 0) || iot_button_register_cb(boot, BUTTON_SINGLE_CLICK, NULL, boot_click, NULL) != ESP_OK || iot_button_register_cb(boot, BUTTON_LONG_PRESS_START, NULL, boot_long_press, NULL) != ESP_OK || - iot_button_register_cb(volume_up, BUTTON_SINGLE_CLICK, NULL, volume_up_click, NULL) != - ESP_OK || - iot_button_register_cb(volume_down, BUTTON_SINGLE_CLICK, NULL, volume_down_click, NULL) != - ESP_OK) { + (volume_up && iot_button_register_cb(volume_up, BUTTON_SINGLE_CLICK, NULL, volume_up_click, + NULL) != ESP_OK) || + (volume_down && iot_button_register_cb(volume_down, BUTTON_SINGLE_CLICK, NULL, + volume_down_click, NULL) != ESP_OK)) { if (volume_down) { (void)iot_button_delete(volume_down); } @@ -155,7 +157,7 @@ static int buttons_init(void **out_ctx, mybot_key_event_handler_t emit, void *us *out_ctx = NULL; portENTER_CRITICAL(&s_context.lock); - if (s_context.active || !s_context.boot || !s_context.volume_up || !s_context.volume_down) { + if (s_context.active || !s_context.boot) { portEXIT_CRITICAL(&s_context.lock); return -1; } diff --git a/components/mybot_platform/src/drivers/io_expander/pca9557.c b/components/mybot_platform/src/drivers/io_expander/pca9557.c new file mode 100644 index 0000000..88cc06b --- /dev/null +++ b/components/mybot_platform/src/drivers/io_expander/pca9557.c @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "io_expander/pca9557.h" + +#include "esp_err.h" +#include "esp_log.h" + +#define TAG "pca9557" +#define PCA9557_I2C_TIMEOUT_MS 100 +#define PCA9557_REG_OUTPUT 0x01 +#define PCA9557_REG_CONFIG 0x03 + +static int write_register(mybot_pca9557_t *expander, uint8_t reg, uint8_t value) { + const uint8_t command[] = {reg, value}; + const esp_err_t result = + i2c_master_transmit(expander->device, command, sizeof(command), PCA9557_I2C_TIMEOUT_MS); + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=io_expander action=write register=0x%02x result=error code=%s", reg, + esp_err_to_name(result)); + return -1; + } + return 0; +} + +int mybot_pca9557_init(mybot_pca9557_t *expander, i2c_master_bus_handle_t bus, uint8_t address, + uint32_t speed_hz) { + if (!expander || !bus || expander->device) { + return -1; + } + *expander = (mybot_pca9557_t){.bus = bus}; + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = address, + .scl_speed_hz = speed_hz, + }; + const esp_err_t result = i2c_master_bus_add_device(bus, &config, &expander->device); + if (result != ESP_OK) { + expander->device = NULL; + ESP_LOGE(TAG, "event=io_expander action=initialize address=0x%02x result=error code=%s", + address, esp_err_to_name(result)); + return -1; + } + return 0; +} + +int mybot_pca9557_deinit(mybot_pca9557_t *expander) { + if (!expander) { + return -1; + } + if (expander->device && i2c_master_bus_rm_device(expander->device) != ESP_OK) { + return -1; + } + expander->device = NULL; + expander->bus = NULL; + expander->output_state = 0; + return 0; +} + +int mybot_pca9557_configure_outputs(mybot_pca9557_t *expander, uint8_t output_mask, + uint8_t high_mask) { + if (!expander || !expander->device) { + return -1; + } + const uint8_t state = high_mask & output_mask; + if (write_register(expander, PCA9557_REG_OUTPUT, state) < 0 || + write_register(expander, PCA9557_REG_CONFIG, (uint8_t)~output_mask) < 0) { + return -1; + } + expander->output_state = state; + return 0; +} + +int mybot_pca9557_set_levels(mybot_pca9557_t *expander, uint8_t mask, uint8_t high_mask) { + if (!expander || !expander->device) { + return -1; + } + const uint8_t state = (uint8_t)((expander->output_state & ~mask) | (high_mask & mask)); + if (write_register(expander, PCA9557_REG_OUTPUT, state) < 0) { + return -1; + } + expander->output_state = state; + return 0; +} diff --git a/components/mybot_platform/src/drivers/io_expander/tca9554.c b/components/mybot_platform/src/drivers/io_expander/tca9554.c new file mode 100644 index 0000000..4593666 --- /dev/null +++ b/components/mybot_platform/src/drivers/io_expander/tca9554.c @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include "io_expander/tca9554.h" + +#include "esp_err.h" +#include "esp_log.h" + +#define TAG "tca9554" +#define TCA9554_I2C_TIMEOUT_MS 100 +#define TCA9554_REG_OUTPUT 0x01 +#define TCA9554_REG_CONFIG 0x03 + +static int write_register(mybot_tca9554_t *expander, uint8_t reg, uint8_t value) { + const uint8_t command[] = {reg, value}; + esp_err_t result = + i2c_master_transmit(expander->device, command, sizeof(command), TCA9554_I2C_TIMEOUT_MS); + if (result != ESP_OK) { + ESP_LOGE(TAG, "event=io_expander action=write register=0x%02x result=error code=%s", reg, + esp_err_to_name(result)); + return -1; + } + return 0; +} + +int mybot_tca9554_init(mybot_tca9554_t *expander, i2c_master_bus_handle_t bus, uint8_t address, + uint32_t speed_hz) { + if (!expander || !bus || expander->device) { + return -1; + } + *expander = (mybot_tca9554_t){.bus = bus}; + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = address, + .scl_speed_hz = speed_hz, + }; + esp_err_t result = i2c_master_bus_add_device(bus, &config, &expander->device); + if (result != ESP_OK) { + expander->device = NULL; + ESP_LOGE(TAG, "event=io_expander action=initialize address=0x%02x result=error code=%s", + address, esp_err_to_name(result)); + return -1; + } + return 0; +} + +int mybot_tca9554_deinit(mybot_tca9554_t *expander) { + if (!expander) { + return -1; + } + int result = 0; + if (expander->device && i2c_master_bus_rm_device(expander->device) != ESP_OK) { + result = -1; + } + if (result == 0) { + expander->device = NULL; + expander->bus = NULL; + expander->output_state = 0; + } + return result; +} + +int mybot_tca9554_configure_outputs(mybot_tca9554_t *expander, uint8_t output_mask, + uint8_t high_mask) { + if (!expander || !expander->device) { + return -1; + } + const uint8_t output_state = high_mask & output_mask; + if (write_register(expander, TCA9554_REG_OUTPUT, output_state) < 0 || + write_register(expander, TCA9554_REG_CONFIG, (uint8_t)~output_mask) < 0) { + return -1; + } + expander->output_state = output_state; + return 0; +} + +int mybot_tca9554_set_levels(mybot_tca9554_t *expander, uint8_t mask, uint8_t high_mask) { + if (!expander || !expander->device) { + return -1; + } + const uint8_t output_state = (uint8_t)((expander->output_state & ~mask) | (high_mask & mask)); + if (write_register(expander, TCA9554_REG_OUTPUT, output_state) < 0) { + return -1; + } + expander->output_state = output_state; + return 0; +} diff --git a/components/mybot_platform/src/internal/audio/es8374_codec.h b/components/mybot_platform/src/internal/audio/es8374_codec.h new file mode 100644 index 0000000..8bcb46e --- /dev/null +++ b/components/mybot_platform/src/internal/audio/es8374_codec.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _ES8374_CODEC_H_ +#define _ES8374_CODEC_H_ + +#include "audio_codec_if.h" +#include "audio_codec_ctrl_if.h" +#include "audio_codec_gpio_if.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ES8374_CODEC_DEFAULT_ADDR (0x20) +#define ES8374_CODEC_DEFAULT_ADDR_1 (0x21) + +/** + * @brief ES8374 codec configuration + */ +typedef struct { + const audio_codec_ctrl_if_t *ctrl_if; /*!< Codec Control interface */ + const audio_codec_gpio_if_t *gpio_if; /*!< Codec GPIO interface */ + esp_codec_dec_work_mode_t codec_mode; /*!< Codec work mode: ADC or DAC */ + bool master_mode; /*!< Whether codec works as I2S master or not */ + int16_t pa_pin; /*!< PA chip power pin */ + bool pa_reverted; /*!< false: enable PA when pin set to 1, true: enable PA when pin set to 0 */ +} es8374_codec_cfg_t; + +/** + * @brief New ES8374 codec interface + * @param codec_cfg: ES8374 codec configuration + * @return NULL: Fail to new ES8374 codec interface + * -Others: ES8374 codec interface + */ +const audio_codec_if_t *es8374_codec_new(es8374_codec_cfg_t *codec_cfg); + +#ifdef __cplusplus +} +#endif + +#endif //__ES8374_H__ diff --git a/components/mybot_platform/src/internal/audio/es_common.h b/components/mybot_platform/src/internal/audio/es_common.h new file mode 100644 index 0000000..232ddad --- /dev/null +++ b/components/mybot_platform/src/internal/audio/es_common.h @@ -0,0 +1,183 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _ES_COMMON_H_ +#define _ES_COMMON_H_ + +#include "esp_log.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CODEC_MEM_CHECK(ptr) \ + if (ptr == NULL) { \ + ESP_LOGE(TAG, "Fail to alloc memory at %s:%d", __FUNCTION__, __LINE__); \ + } + +#define BITS(n) (1 << n) + +#define MCLK_DEFAULT_DIV (256) + +typedef enum { + BIT_LENGTH_MIN = -1, + BIT_LENGTH_16BITS = 0x03, + BIT_LENGTH_18BITS = 0x02, + BIT_LENGTH_20BITS = 0x01, + BIT_LENGTH_24BITS = 0x00, + BIT_LENGTH_32BITS = 0x04, + BIT_LENGTH_MAX, +} es_bits_length_t; + +typedef enum { + MCLK_DIV_MIN = -1, + MCLK_DIV_1 = 1, + MCLK_DIV_2 = 2, + MCLK_DIV_3 = 3, + MCLK_DIV_4 = 4, + MCLK_DIV_6 = 5, + MCLK_DIV_8 = 6, + MCLK_DIV_9 = 7, + MCLK_DIV_11 = 8, + MCLK_DIV_12 = 9, + MCLK_DIV_16 = 10, + MCLK_DIV_18 = 11, + MCLK_DIV_22 = 12, + MCLK_DIV_24 = 13, + MCLK_DIV_33 = 14, + MCLK_DIV_36 = 15, + MCLK_DIV_44 = 16, + MCLK_DIV_48 = 17, + MCLK_DIV_66 = 18, + MCLK_DIV_72 = 19, + MCLK_DIV_5 = 20, + MCLK_DIV_10 = 21, + MCLK_DIV_15 = 22, + MCLK_DIV_17 = 23, + MCLK_DIV_20 = 24, + MCLK_DIV_25 = 25, + MCLK_DIV_30 = 26, + MCLK_DIV_32 = 27, + MCLK_DIV_34 = 28, + MCLK_DIV_7 = 29, + MCLK_DIV_13 = 30, + MCLK_DIV_14 = 31, + MCLK_DIV_MAX, +} es_sclk_div_t; + +typedef enum { + LCLK_DIV_MIN = -1, + LCLK_DIV_128 = 0, + LCLK_DIV_192 = 1, + LCLK_DIV_256 = 2, + LCLK_DIV_384 = 3, + LCLK_DIV_512 = 4, + LCLK_DIV_576 = 5, + LCLK_DIV_768 = 6, + LCLK_DIV_1024 = 7, + LCLK_DIV_1152 = 8, + LCLK_DIV_1408 = 9, + LCLK_DIV_1536 = 10, + LCLK_DIV_2112 = 11, + LCLK_DIV_2304 = 12, + + LCLK_DIV_125 = 16, + LCLK_DIV_136 = 17, + LCLK_DIV_250 = 18, + LCLK_DIV_272 = 19, + LCLK_DIV_375 = 20, + LCLK_DIV_500 = 21, + LCLK_DIV_544 = 22, + LCLK_DIV_750 = 23, + LCLK_DIV_1000 = 24, + LCLK_DIV_1088 = 25, + LCLK_DIV_1496 = 26, + LCLK_DIV_1500 = 27, + LCLK_DIV_MAX, +} es_lclk_div_t; + +typedef enum { + D2SE_PGA_GAIN_MIN = -1, + D2SE_PGA_GAIN_DIS = 0, + D2SE_PGA_GAIN_EN = 1, + D2SE_PGA_GAIN_MAX = 2, +} es_d2se_pga_t; + +typedef enum { + ADC_INPUT_MIN = -1, + ADC_INPUT_LINPUT1_RINPUT1 = 0x00, + ADC_INPUT_MIC1 = 0x05, + ADC_INPUT_MIC2 = 0x06, + ADC_INPUT_LINPUT2_RINPUT2 = 0x50, + ADC_INPUT_DIFFERENCE = 0xf0, + ADC_INPUT_MAX, +} es_adc_input_t; + +typedef enum { + DAC_OUTPUT_MIN = -1, + DAC_OUTPUT_LOUT1 = 0x04, + DAC_OUTPUT_LOUT2 = 0x08, + DAC_OUTPUT_SPK = 0x09, + DAC_OUTPUT_ROUT1 = 0x10, + DAC_OUTPUT_ROUT2 = 0x20, + DAC_OUTPUT_ALL = 0x3c, + DAC_OUTPUT_MAX, +} es_dac_output_t; + +typedef enum { + MIC_GAIN_MIN = -1, + MIC_GAIN_0DB = 0, + MIC_GAIN_3DB = 3, + MIC_GAIN_6DB = 6, + MIC_GAIN_9DB = 9, + MIC_GAIN_12DB = 12, + MIC_GAIN_15DB = 15, + MIC_GAIN_18DB = 18, + MIC_GAIN_21DB = 21, + MIC_GAIN_24DB = 24, + MIC_GAIN_MAX, +} es_mic_gain_t; + +typedef enum { + ES_MODULE_MIN = -1, + ES_MODULE_ADC = 0x01, + ES_MODULE_DAC = 0x02, + ES_MODULE_ADC_DAC = 0x03, + ES_MODULE_LINE = 0x04, + ES_MODULE_MAX +} es_module_t; + +typedef enum { + ES_MODE_MIN = -1, + ES_MODE_SLAVE = 0x00, + ES_MODE_MASTER = 0x01, + ES_MODE_MAX, +} es_mode_t; + +typedef enum { + ES_I2S_MIN = -1, + ES_I2S_NORMAL = 0, + ES_I2S_LEFT = 1, + ES_I2S_RIGHT = 2, + ES_I2S_DSP = 3, + ES_I2S_MAX +} es_i2s_fmt_t; + +typedef struct { + es_sclk_div_t sclk_div; /*!< bits clock divide */ + es_lclk_div_t lclk_div; /*!< WS clock divide */ +} es_i2s_clock_t; + +typedef enum { + ES_PA_SETUP = 1, + ES_PA_ENABLE = (1 << 1), + ES_PA_DISABLE = (1 << 2), +} es_pa_setting_t; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/components/mybot_platform/src/internal/io_expander/pca9557.h b/components/mybot_platform/src/internal/io_expander/pca9557.h new file mode 100644 index 0000000..5cbe2e5 --- /dev/null +++ b/components/mybot_platform/src/internal/io_expander/pca9557.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_PCA9557_H_ +#define MYBOT_PCA9557_H_ + +#include "driver/i2c_master.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + i2c_master_bus_handle_t bus; + i2c_master_dev_handle_t device; + uint8_t output_state; +} mybot_pca9557_t; + +int mybot_pca9557_init(mybot_pca9557_t *expander, i2c_master_bus_handle_t bus, uint8_t address, + uint32_t speed_hz); +int mybot_pca9557_deinit(mybot_pca9557_t *expander); +int mybot_pca9557_configure_outputs(mybot_pca9557_t *expander, uint8_t output_mask, + uint8_t high_mask); +int mybot_pca9557_set_levels(mybot_pca9557_t *expander, uint8_t mask, uint8_t high_mask); + +#ifdef __cplusplus +} +#endif + +#endif /* MYBOT_PCA9557_H_ */ diff --git a/components/mybot_platform/src/internal/io_expander/tca9554.h b/components/mybot_platform/src/internal/io_expander/tca9554.h new file mode 100644 index 0000000..c487437 --- /dev/null +++ b/components/mybot_platform/src/internal/io_expander/tca9554.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#ifndef MYBOT_TCA9554_H_ +#define MYBOT_TCA9554_H_ + +#include "driver/i2c_master.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + i2c_master_bus_handle_t bus; + i2c_master_dev_handle_t device; + uint8_t output_state; +} mybot_tca9554_t; + +int mybot_tca9554_init(mybot_tca9554_t *expander, i2c_master_bus_handle_t bus, uint8_t address, + uint32_t speed_hz); +int mybot_tca9554_deinit(mybot_tca9554_t *expander); +int mybot_tca9554_configure_outputs(mybot_tca9554_t *expander, uint8_t output_mask, + uint8_t high_mask); +int mybot_tca9554_set_levels(mybot_tca9554_t *expander, uint8_t mask, uint8_t high_mask); + +#ifdef __cplusplus +} +#endif + +#endif /* MYBOT_TCA9554_H_ */ diff --git a/docs/AUDIO_PLAYBACK.md b/docs/AUDIO_PLAYBACK.md index 28dac40..1253994 100644 --- a/docs/AUDIO_PLAYBACK.md +++ b/docs/AUDIO_PLAYBACK.md @@ -2,7 +2,7 @@ [English](AUDIO_PLAYBACK.md) | [简体中文](AUDIO_PLAYBACK.zh-CN.md) -All thirteen board profiles use one PCM playback buffer through seven board audio drivers. This +All supported board profiles use one PCM playback buffer through the board audio adapters. This keeps the SDK boundary at 16 kHz, mono signed-16 PCM and moves continuous I2S feeding into an independent task. The shared implementation is [`pcm_playback_buffer.c`](../components/mybot_platform/src/services/audio/pcm_playback_buffer.c); @@ -50,6 +50,9 @@ the number or width of I2S slots. Native conversion occurs only in the worker's | `esp32-s3-touch-amoled-1.75` | `amoled175_codec_audio.c` | 16-bit stereo; duplicate mono into both slots | | `esp32-s3-touch-amoled-1.75c` | `amoled175_codec_audio.c` | 16-bit stereo; duplicate mono into both slots | | `esp32-s3-touch-amoled-2.16` | `amoled175_codec_audio.c` | 16-bit stereo; duplicate mono into both slots | +| `esp32-s3-touch-amoled-1.8-v2` | `es8311_codec_audio.c` | 16-bit stereo; duplicate mono into both slots | +| `esp32-s3-touch-lcd-1.85` | `raw_i2s_audio.c` | 32-bit right-slot capture; duplicate mono into both playback slots | +| `genjutech-s3-1.54tft` | `es8311_codec_audio.c` | 16-bit stereo; duplicate mono into both slots | | `respeaker-flex-xvf3800-circular4-xiao` | `xvf3800_audio.c` | 32-bit stereo; existing volume scaling and slot alignment | Writes retry unwritten frames after a short write or timeout, preserving order. Invalid byte @@ -64,9 +67,9 @@ If that wait fails, resources remain allocated for cleanup retry. Restart resets prebuffer state, so audio from a previous conversation cannot remain in the software queue. Codec-based boards mute or close the output before allowing old DMA data to clear. TX clocks -remain active when capture still needs them. Zhengchen, Xingzhi Cube, Bread, and XVF3800 disable the -playback channel and preload silence into DMA before reusing it. The board-specific power, clock, and volume -behavior remains in each driver. +remain active when capture still needs them. Raw-I2S boards (Zhengchen, Xingzhi Cube, Bread, and +LCD 1.85) and XVF3800 disable the playback channel and preload silence into DMA before reusing it. +The board-specific power, clock, and volume behavior remains in each driver. Wi-Fi provisioning prompts and the offline test use a separate drain operation before stopping, allowing the FIFO and hardware tail to finish. Provisioning prompts feed this same FIFO in diff --git a/docs/AUDIO_PLAYBACK.zh-CN.md b/docs/AUDIO_PLAYBACK.zh-CN.md index d9ab567..44cec98 100644 --- a/docs/AUDIO_PLAYBACK.zh-CN.md +++ b/docs/AUDIO_PLAYBACK.zh-CN.md @@ -2,7 +2,7 @@ [English](AUDIO_PLAYBACK.md) | [简体中文](AUDIO_PLAYBACK.zh-CN.md) -当前 13 个 Board profile 通过 7 个板级音频驱动共用同一套 PCM 播放缓冲。SDK 边界保持 +当前全部 Board profile 通过板级音频适配器共用同一套 PCM 播放缓冲。SDK 边界保持 16 kHz、单声道 signed-16 PCM,独立任务持续向 I2S 供数。公共实现位于 [`pcm_playback_buffer.c`](../components/mybot_platform/src/services/audio/pcm_playback_buffer.c), 各板级驱动提供原生 I2S sink。 @@ -46,6 +46,9 @@ SDK 和缓冲接口中的帧数始终表示**单声道 PCM 帧数**,不随 I2S | `esp32-s3-touch-amoled-1.75` | `amoled175_codec_audio.c` | 16 位双声道,将单声道复制到两个槽 | | `esp32-s3-touch-amoled-1.75c` | `amoled175_codec_audio.c` | 16 位双声道,将单声道复制到两个槽 | | `esp32-s3-touch-amoled-2.16` | `amoled175_codec_audio.c` | 16 位双声道,将单声道复制到两个槽 | +| `esp32-s3-touch-amoled-1.8-v2` | `es8311_codec_audio.c` | 16 位双声道,将单声道复制到两个槽 | +| `esp32-s3-touch-lcd-1.85` | `raw_i2s_audio.c` | 32 位右 slot 采集;播放将单声道复制到两个 slot | +| `genjutech-s3-1.54tft` | `es8311_codec_audio.c` | 16 位双声道,将单声道复制到两个槽 | | `respeaker-flex-xvf3800-circular4-xiao` | `xvf3800_audio.c` | 32 位双声道,保留音量缩放及槽内对齐 | 短写或超时后继续重试剩余帧,保持 PCM 顺序。返回非法字节数、非超时驱动错误,或连续 @@ -59,8 +62,8 @@ SDK 和缓冲接口中的帧数始终表示**单声道 PCM 帧数**,不随 I2S 对话音频残留在软件队列中。 使用 codec 的板型先静音或关闭输出,再等待旧 DMA 数据清除;如果采集仍需要 TX 时钟, -则保留该时钟。征辰、行者 Cube、Bread 和 XVF3800 停用播放通道并向 DMA 预装静音后再复用。电源、时钟、 -音量行为仍由板级驱动维护。 +则保留该时钟。原始 I2S 板型(征辰、行者 Cube、Bread 与 LCD 1.85)和 XVF3800 停用播放 +通道并向 DMA 预装静音后再复用。电源、时钟、音量行为仍由板级驱动维护。 配网提示音及离线测试停止前使用单独的排空操作,等待 FIFO 和硬件尾音完成。 配网提示音以 320 帧为一块写入同一 FIFO,不经过 SDK 的 960 帧供数节奏。最后一次 diff --git a/docs/BOARD_PORTING.md b/docs/BOARD_PORTING.md index abf0fb6..366f41f 100644 --- a/docs/BOARD_PORTING.md +++ b/docs/BOARD_PORTING.md @@ -76,6 +76,8 @@ and Octal PSRAM. its physical Flash/PSRAM capacities require confirmation on the release device. `esp32-s3-touch-amoled-2.16` also uses a conservative 16 MB QIO Flash profile and 80 MHz Octal PSRAM; confirm its physical capacities before release. +The GenjuTech 1.54, AMOLED 1.8 V2 and LCD 1.85 profiles use the same 16 MB QIO Flash and 80 MHz +Octal PSRAM profile; confirm each device before release. `bread-compact-wifi-lcd` is a fixed ESP32-S3 N16R8 profile with 16 MB QIO Flash and 8 MB Octal PSRAM at 80 MHz; validate the module and detected capacities on the wired device. @@ -108,8 +110,9 @@ build with conversation animations disabled. This is build coverage, not hardwar 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 and ReSpeaker Flex; update it when adding another headless board. + `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. 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. @@ -183,6 +186,28 @@ after hardware tests establish it is required. Other panel sizes and shared-cloc require separate profiles rather than runtime selection. Chinese and English build support for this fixed N16R8 profile has been validated with ESP-IDF v5.5.2; real-device validation remains pending. +## Shared profile skeleton / 共享板型骨架 + +`boards/common/generic_profile.cmake` composes the shared board lifecycle, audio adapter, panel, +touch/key input, optional PCA9557/TCA9554/AXP2101 setup, display backlight and SDK registration. +Each board still owns its `board.cmake`, `board_config.h`, `sdkconfig.defaults` and `board.c`. +The generic path supports ES8311, ES8311+ES7210, ES8374 and raw I2S; raw capture can use standard +I2S or PDM. Keep board-specific power or expander code outside the common path when it changes +the lifecycle order. + +`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 +round layout narrows widgets to a center safe rectangle; this is not a substitute for panel and +touch validation. + +Profiles with separate system and codec rails may set `MYBOT_GENERIC_AUX_POWER_GPIO` in addition +to `MYBOT_GENERIC_POWER_GPIO`. Both rails are asserted before I2C, display and codec clients are +created and are released during board teardown. + +`Taiji Pi S3` uses PDM RX on I2S0 and standard-I2S playback on I2S1. PDM samples enter the +platform already as signed 16-bit mono values, so the SDK receives 16 kHz mono signed-16 without +another slot conversion. The active-low GPIO48 mute path follows playback start/stop. + The `esp-vocat` profile supports PCB V1.0 and V1.2 under one compile-time Board because they share the same target, storage, partition, and component configuration. This is runtime hardware-revision detection, not runtime Board selection. Board preparation creates native I2C0 on GPIO2/GPIO1, drives @@ -256,6 +281,22 @@ Camera, RTC, IMU, TF card, battery reporting, shutdown, and low-power behavior r Chinese and English firmware build support for 2.16 has been validated with ESP-IDF v5.5.2; real-device validation remains pending. +The `genjutech-s3-1.54tft` profile reuses the ES8311, raw button, ST7789, and shared LVGL paths. +Its PA pin is coupled to display I/O by the board design, so the platform keeps the PA enabled +after its first playback start and only disables it during board teardown. Touch, LED, battery, +charging, sleep, shutdown, and video are not implemented. + +The `esp32-s3-touch-amoled-1.8-v2` profile owns a 368 x 448 CO5300 variant with a 16-pixel X gap. +TCA9554 resets the panel before AXP2101 rail configuration; CST816S and Boot share the process-lifetime +input path. The SDK and ES8311 I2S path remain at 16 kHz mono signed-16 PCM. AXP2101 power-key +actions, battery reporting, sleep, shutdown, and video are not implemented. + +The `esp32-s3-touch-lcd-1.85` profile uses separate control and touch I2C buses plus separate +microphone and speaker I2S peripherals. The microphone is read from the right slot and mono playback +is copied into both TX slots while the SDK boundary remains mono 16 kHz signed-16 PCM. TCA9554 +releases LCD/touch reset, GPIO7 holds power, and GPIO5 controls backlight. The GPIO6 power button, +battery reporting, sleep, shutdown, and video are not implemented. + The ReSpeaker Flex profile is an example of a hardware audio front end rather than a raw microphone codec. Its XVF3800 must run separately flashed Circular-4 16 kHz, two-channel I2S firmware and owns AEC, beamforming, AGC, and noise suppression. The ESP32-S3 uses I2S slave mode and converts the diff --git a/docs/BOARD_PORTING.zh-CN.md b/docs/BOARD_PORTING.zh-CN.md index 9737559..cfc0599 100644 --- a/docs/BOARD_PORTING.zh-CN.md +++ b/docs/BOARD_PORTING.zh-CN.md @@ -74,6 +74,8 @@ Octal PSRAM。 实际 Flash/PSRAM 容量必须在发布设备上核对。 `esp32-s3-touch-amoled-2.16` 同样采用保守的 16 MB QIO Flash profile 与 80 MHz Octal PSRAM; 发布前确认实际容量。 +GenjuTech 1.54、AMOLED 1.8 V2 与 LCD 1.85 profile 同样使用 16 MB QIO Flash 与 80 MHz +Octal PSRAM;发布前应逐台确认实际容量。 `bread-compact-wifi-lcd` 固定为 ESP32-S3 N16R8 profile,16 MB QIO Flash、80 MHz 8 MB Octal PSRAM;需在实际接线设备验证模组和检测到的容量。 @@ -105,13 +107,33 @@ RTSA 包前,SDK 构建封装会拒绝这些配置。[CI 工作流](../.github/ 6. 保持 SDK 音频边界:16 kHz、单声道、signed 16-bit PCM,接口传帧数而不是字节数。 7. 在 `components/mybot_platform/CMakeLists.txt` 选择显示能力:带屏板型使用 `Kconfig.display` 启用 LVGL,无屏板型使用 `Kconfig.headless`。当前无屏选择覆盖 - AtomEchoS3R 与 ReSpeaker Flex,新增无屏板型时需同步此选择。 + AtomEchoS3R、行者 Cube 0.96 OLED 与 ReSpeaker Flex,新增无屏板型时需同步此选择。 8. 增加两种语言的隔离 CI 构建与尺寸报告,并记录真实设备的配网、HTTPS、RTC、双向音频、 输入、显示、挂断与重复启停验证。 所有带屏板型使用共享 LVGL 界面,不保留旧渲染器或后端选择开关。控制器初始化和销毁放在 `src/drivers/display/panels/`,通过 `display.cmake` 和可复用的 LVGL 适配器接入。 `CONFIG_MYBOT_LVGL_UI` 是根据板型确定的隐藏配置,无屏板型不启用;主题与动画仍可配置。 + +## 共享板型骨架 + +`boards/common/generic_profile.cmake` 组合共享板级生命周期、音频适配器、面板、触摸/按键、 +可选的 PCA9557/TCA9554/AXP2101 初始化、显示背光和 SDK 注册。每个板型仍独立维护 +`board.cmake`、`board_config.h`、`sdkconfig.defaults` 与 `board.c`。通用路径支持 ES8311、 +ES8311+ES7210、ES8374 和原始 I2S;原始采集可使用标准 I2S 或 PDM。会改变启动顺序的板级 +电源或扩展器逻辑不应塞入公共路径。 + +`M5Stack StopWatch` 在显示和 codec 启动前初始化 M5PM1 与 M5IOE1:打开 LCD/codec 电源, +复位 LCD/触摸,控制扬声器 PA,并默认关闭振动电机。共享圆屏布局把控件收窄到中央安全 +矩形;这不替代面板和触摸真机验证。 + +系统电源与 codec 电源分开的板型可在 `MYBOT_GENERIC_POWER_GPIO` 之外设置 +`MYBOT_GENERIC_AUX_POWER_GPIO`。两路电源都会在创建 I2C、显示和 codec 客户端之前打开, +并在板级销毁时释放。 + +`Taiji Pi S3` 在 I2S0 上使用 PDM 采集、I2S1 上使用标准 I2S 播放。PDM 解调后直接提供 +16 kHz 单声道 signed-16 数据,SDK 边界不再做 slot 转换。低有效 GPIO48 静音脚随播放 +启停。 共享面板契约位于 `src/internal/display/display_panel.h`,内部头文件不属于 SDK API; 板级电源和背光控制仍归 Board 层所有。 @@ -226,6 +248,20 @@ SDK 停止期间任一输入仍能请求配网。在各自匹配的发布硬件 应留在平台驱动内,并保持 Cloud AEC。摄像头、RTC、IMU、TF 卡、电量、关机与低功耗尚未支持。 2.16 中英文固件构建支持已在 ESP-IDF v5.5.2 验证,真机验证仍未完成。 +`genjutech-s3-1.54tft` 复用 ES8311、raw 按键、ST7789 与共享 LVGL 路径。该板功放引脚与显示 +I/O 存在设计耦合,因此首次播放开启 PA 后保持有效,仅在板级拆除时关闭;停止播放只让 I2S +流空闲。触摸、LED、电量/充电、休眠、关机与视频尚未实现。 + +`esp32-s3-touch-amoled-1.8-v2` 使用独立的 368 x 448 CO5300 变体和 16 像素 X 偏移。 +TCA9554 先复位面板,AXP2101 再配置电源轨;CST816S 与 Boot 共用进程级输入路径。SDK 与 +ES8311 I2S 均保持 16 kHz 单声道 signed-16 PCM。AXP2101 电源键行为、电量、休眠、关机与 +视频尚未实现。 + +`esp32-s3-touch-lcd-1.85` 使用独立的控制 I2C、触摸 I2C、麦克风 I2S 与扬声器 I2S。麦克风读取 +右 slot,播放将单声道复制到两个 TX slot,SDK 边界仍为单声道 16 kHz signed-16 PCM。 +TCA9554 释放 LCD/触摸复位,GPIO7 保持电源,GPIO5 控制背光。GPIO6 电源键、电量、休眠、 +关机与视频尚未实现。 + ReSpeaker Flex profile 是硬件音频前端而非原始麦克风 codec 的示例。XVF3800 必须预先单独 烧录 Circular-4 16 kHz 双通道 I2S 固件,并负责 AEC、波束成形、AGC 与降噪。ESP32-S3 工作在 I2S 从机模式,将选定的 32-bit 采集 slot 转换到 SDK 的单声道 signed-16 边界。该 diff --git a/docs/CORES3_UI.md b/docs/CORES3_UI.md index f9fc83a..607d5a6 100644 --- a/docs/CORES3_UI.md +++ b/docs/CORES3_UI.md @@ -4,7 +4,7 @@ `MYBOT_BOARD=m5stack-core-s3` automatically selects the process-gray pixel-glasses view on the 320 × 240 ILI9342 display. It uses the existing CoreS3 LVGL adapter and common LCD-view API. -There is no manual renderer switch or `CONFIG_MYBOT_LVGL_UI` override. The other twelve board +There is no manual renderer switch or `CONFIG_MYBOT_LVGL_UI` override. The other board profiles retain their existing UI and hardware behavior; see [Platform UI](PLATFORM_UI.md). This replaces CoreS3's earlier status-card view. Audio, camera/video policy, FT6336 touch gestures, @@ -31,29 +31,23 @@ The pairing page directs users to the web console. A six-digit numeric pairing c fixed 32 px font on CoreS3; it does not shrink to fit. Chinese guidance reuses the existing 20 px Source Han Sans subset. That subset covers product UI text, not arbitrary Chinese chat. -## Automatic faces and optional LCD indicators +## Automatic faces -The public LCD interface retains four optional one-shot emotion indicators. The current SDK MCP -path accepts only `audio.set_volume` and `audio.adjust_volume` inside a `message.user` envelope; -it does not emit these emotion indicators. The table describes renderer behavior when indicators -are submitted, as exercised by the LCD adapter host tests. +The renderer selects faces from the SDK workflow screen and server-state indicators. The current +public LCD interface does not expose one-shot emotion indicators; its MCP path accepts only +`audio.set_volume` and `audio.adjust_volume` inside a `message.user` envelope. -| State or submitted LCD indicator | Face and hold | +| SDK state | Face and hold | | --- | --- | | READY / return from a conversation | NEUTRAL | | Enter a conversation | SURPRISED for 300 ms, plus entering transition | | LISTENING | LISTENING | | THINKING | THINKING; PROCESSING after three continuous seconds | | SPEAKING / no active phase | NEUTRAL | -| `MYBOT_LCD_INDICATOR_HAPPY` | HAPPY for 2000 ms | -| `MYBOT_LCD_INDICATOR_SAD` | SAD for 3000 ms | -| `MYBOT_LCD_INDICATOR_ANGRY` | ANNOYED for 1200 ms | -| `MYBOT_LCD_INDICATOR_SURPRISED` | SURPRISED for 800 ms | With animations enabled, each entering transition adds 250 ms before its hold, including the -conversation opener. With animations disabled, only the hold remains. Each submitted emotion -indicator replaces the unfinished foreground and replays even the same expression name. -Ordinary phase/voiceprint updates do not restart the hold. +conversation opener. With animations disabled, only the hold remains. Phase and voiceprint updates +do not restart the opener. Expiry restores the latest automatic face, including PROCESSING if thinking has continued for three seconds in the background. Leaving a conversation or detaching the SDK cancels its expression timers. Operational guidance takes precedence. All expressions are LCD-only and send @@ -67,7 +61,7 @@ SURPRISED, SHY, PROUD, LOVE, SLEEPY, BORED, SAD, PLEADING, APOLOGY, WORRIED, ANNOYED, REFUSE, PROCESSING, ERROR, SLEEP ``` -GREETING and the other prepared artwork remain available without automatic demo cycling. +The prepared artwork remains available without automatic demo cycling. ERROR and SLEEP artwork do not enter an SDK error state or put the board to sleep. The CoreS3 adapter owns expression timing alongside its existing copied LCD-state submissions; rendering stays in the existing LVGL task, without an extra UI thread or UTC dependency. @@ -153,9 +147,8 @@ input paths. They do not validate this new pixel renderer. Real-device testing r - Check all ten guidance screens and 23 expressions in both languages on the 320 × 240 panel. - Verify four separate terminal rows, the signature and full-size six-digit pairing codes. -- Verify the SURPRISED opener, phase changes and three-second thinking timeout. Inject optional - LCD emotion indicators to test replacement, same-indicator replay and expiry; phase/voiceprint - updates must preserve hold timing. The SDK MCP path cannot trigger these expression tests. +- Verify the SURPRISED opener, phase changes, three-second thinking timeout and opener expiry. + Phase/voiceprint updates must preserve the opener hold timing. - Test animation enabled/disabled, overflowing SSID/instructions, and web-console pairing. - Regress short touch, three-second provisioning, Wi-Fi recovery and repeated SDK stop/start. - Run full-duplex audio with video off and with the existing 1 fps camera uplink enabled. diff --git a/docs/CORES3_UI.zh-CN.md b/docs/CORES3_UI.zh-CN.md index 48575ca..c11da54 100644 --- a/docs/CORES3_UI.zh-CN.md +++ b/docs/CORES3_UI.zh-CN.md @@ -4,7 +4,7 @@ `MYBOT_BOARD=m5stack-core-s3` 自动为320 × 240 ILI9342屏选择工艺灰像素眼镜界面,复用原 CoreS3 LVGL适配器及公共LCD视图接口。不增加手工渲染器开关,无需覆盖隐藏的 -`CONFIG_MYBOT_LVGL_UI`。其他十二个Board profile保留原UI与硬件行为,见 +`CONFIG_MYBOT_LVGL_UI`。其他Board profile保留原UI与硬件行为,见 [平台UI](PLATFORM_UI.zh-CN.md)。 本视图替换CoreS3旧状态卡片。音频、摄像头/视频策略、FT6336触摸手势、电源、引脚、PSRAM @@ -25,27 +25,21 @@ RTSA快照不变。 配对页提示在网页控制台输入配对码。CoreS3六位数字配对码固定使用32 px字体,不缩小适配。 中文引导复用原20 px思源黑体子集,字形覆盖产品文案,不覆盖任意中文聊天正文。 -## 自动表情与可选LCD指示 +## 自动表情 -公共LCD接口保留四种可选的一次性情绪指示。当前SDK MCP通路仅接受`message.user`封装内的 -`audio.set_volume`与`audio.adjust_volume`,不产生这些情绪指示。下表描述提交指示时的 -渲染器行为,LCD适配器宿主测试通过直接提交指示验证。 +渲染器根据SDK流程页面和服务端状态指示选择表情。当前公共LCD接口不提供一次性情绪指示; +MCP通路仅接受`message.user`封装内的`audio.set_volume`与`audio.adjust_volume`。 -| 状态或提交的LCD指示 | 表情与保持时间 | +| SDK状态 | 表情与保持时间 | | --- | --- | | READY / 挂断返回 | NEUTRAL | | 进入会话 | SURPRISED保持300 ms,另计进入过渡 | | LISTENING | LISTENING | | THINKING | THINKING;连续三秒后PROCESSING | | SPEAKING / 无阶段 | NEUTRAL | -| `MYBOT_LCD_INDICATOR_HAPPY` | HAPPY,2000 ms | -| `MYBOT_LCD_INDICATOR_SAD` | SAD,3000 ms | -| `MYBOT_LCD_INDICATOR_ANGRY` | ANNOYED,1200 ms | -| `MYBOT_LCD_INDICATOR_SURPRISED` | SURPRISED,800 ms | 开启动画时,每次保持另加250 ms进入过渡,会话开场同样另计;关闭动画时只有保持时间。 -每次提交情绪指示都会替换未结束的前景,重复提交同一指示也会重播。 -普通阶段/声纹更新不重置保持计时,到期恢复最新自动表情;后台思考已满三秒时恢复 +普通阶段/声纹更新不重置开场保持计时,到期恢复最新自动表情;后台思考已满三秒时恢复 PROCESSING。离开会话或SDK解挂会清除表情计时,必要引导优先。全部表情仅控制LCD, 不发送电机命令。 @@ -57,7 +51,7 @@ SURPRISED, SHY, PROUD, LOVE, SLEEPY, BORED, SAD, PLEADING, APOLOGY, WORRIED, ANNOYED, REFUSE, PROCESSING, ERROR, SLEEP ``` -GREETING及其余预置图案保留,不自动演示轮播。ERROR和SLEEP图案不改变SDK运行状态,也不 +预置图案保留,不自动演示轮播。ERROR和SLEEP图案不改变SDK运行状态,也不 使设备休眠。CoreS3适配器在原有复制投递LCD状态流程中维护表情计时,现有LVGL任务渲染, 不新增UI线程或UTC时钟依赖。 @@ -133,8 +127,8 @@ zh-CN耗时38秒、en-US耗时37秒,总计1分16秒。这是构建验证,不 - 在320 × 240屏上检查两种语言、十种引导页面及23个表情。 - 检查四行独立终端、签名及固定大号六位配对码。 -- 验证SURPRISED开场、阶段变化、三秒思考超时;注入可选LCD情绪指示,检查替换、同一指示 - 重播及保持到期,阶段/声纹更新不得重置保持计时。SDK MCP通路不能触发这些表情测试。 +- 验证SURPRISED开场、阶段变化、三秒思考超时及开场保持到期;阶段/声纹更新不得重置 + 开场保持计时。 - 开关动画,检查超宽SSID/提示滚动及网页控制台配对。 - 回归短触、三秒配网、断网恢复和反复SDK启停。 - 视频关闭及原1 fps摄像头上行开启时分别验证全双工音频。 diff --git a/docs/PLATFORM_UI.md b/docs/PLATFORM_UI.md index ab38bc0..e285613 100644 --- a/docs/PLATFORM_UI.md +++ b/docs/PLATFORM_UI.md @@ -22,9 +22,37 @@ paths and touch/key ownership retain their existing board behavior. | `esp32-s3-touch-amoled-1.75` | CO5300, 466 × 466 | shared panel adapter | existing touch/key path | | `esp32-s3-touch-amoled-1.75c` | CO5300, 466 × 466 | shared panel adapter | existing touch/key path | | `esp32-s3-touch-amoled-2.16` | CO5300 480 variant, 480 × 480 | shared panel adapter | CST9217 and Boot | +| `esp32-s3-touch-amoled-1.8-v2` | CO5300 368×448 variant, 368 × 448 | shared panel adapter | CST816S and Boot | +| `esp32-s3-touch-lcd-1.85` | ST77916, 360 × 360 | shared panel adapter | CST816S and Boot | +| `genjutech-s3-1.54tft` | ST7789, 240 × 240 | shared ST7789 adapter | Boot and volume buttons | | `sensecap-watcher` | SPD2010, 412 × 412 | shared panel adapter | existing encoder path | - -The AtomEchoS3R and ReSpeaker Flex profiles have no LCD and do not include LVGL. The ML307 and +| `xingzhi-metal-1.54-wifi`, `xingzhi-abs-2.0` | ST7789, 240 × 240 | shared panel adapter | Boot/touch or Boot/volume | +| `xingzhi-cube-0.85tft-wifi` | NV3023, 128 × 128 | shared panel adapter | Boot | +| `xingzhi-cube-0.96oled-wifi` | Headless | none | Boot and volume buttons | +| `m5stack-stopwatch` | CO5300, 466 × 466 round | shared panel adapter | CST820B-compatible touch and buttons | +| `esp32-s3-touch-amoled-1.8` | SH8601, 368 × 448 | shared panel adapter | FT5x06 and Boot | +| AMOLED 1.32/1.43c/2.06 | SH8601, 466 × 466 or 410 × 502 | shared panel adapter | Boot; FT5x06 on 2.06 | +| `atoms3r-echo-base`, `atoms3r-echo-pyramid` | GC9A01-class, 128 × 128 | shared panel adapter | Boot | +| `sp-esp32-s3-1.28-box` | GC9A01, 240 × 240 round | shared panel adapter | CST816S-compatible touch and Boot | +| `esp-box`, `esp-box-3`, `esp-box-lite` | ILI9341, 320 × 240 | shared panel adapter | Boot; touch not implemented | +| `esp32s3-korvo2-v3`, `df-k10` | ILI9341, 320 × 240 | shared panel adapter | Boot | +| `lichuang-dev`, `rymcu-bigsmart` | ST7789, 320 × 240 | shared panel adapter | FT5x06 or GT911 plus Boot | +| `quandong-s3-dev` | ILI9341, 320 × 240 | shared panel adapter | Boot | +| `mixgo-nova` | ST7789, 128 × 160 | shared panel adapter | Boot | +| `taiji-pi-s3` | ST77916, 360 × 360 round | shared panel adapter | CST816S and Boot | +| `freenove-esp32s3-display-2.8-lcd` | ILI9341, 320 × 240 | shared panel adapter | Boot | +| `esp32-s3-touch-lcd-1.54`, `esp32-s3-touch-lcd-1.83` | ST7789, 240 × 240 or 240 × 284 | shared ST7789 adapter | CST816S and buttons | +| `esp32-s3-lcd-0.85`, `aipi-lite` | ST7789, 128 × 128 | shared ST7789 compact layout | buttons | +| `esp32-s3-touch-lcd-1.85b`, `esp32-s3-touch-lcd-1.85c` | ST77916, 360 × 360 | shared panel adapter | CST816S-compatible touch and Boot | +| `sp-esp32-s3-1.54-muma` | ST7789, 240 × 240 | shared ST7789 adapter | CST816S and Boot | +| `atk-dnesp32s3-box0`, `nulllab-ai-vox-v3` | ST7789, 240 × 240 | shared ST7789 adapter | buttons | +| `magiclick-2p4` | NV3023, 128 × 128 | shared panel adapter | three buttons | +| `movecall-cuican-esp32s3`, `movecall-moji-esp32s3` | GC9A01, 240 × 240 | shared panel adapter | Boot | +| `kevin-sp-v4-dev` | ST7789, 240 × 280 | shared ST7789 adapter | Boot | +| `du-chatx` | ST7789, 128 × 160 | shared ST7789 compact layout | Boot | + +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. 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. @@ -36,6 +64,12 @@ real-device validation remains pending. AMOLED 2.16 uses a zero panel gap and 40 MHz QSPI. Its CO5300 transfer regions retain even pixel boundaries on both axes, and touch/panel orientation must be verified on the physical board. Chinese and English 2.16 build support has been validated; real-device testing remains pending. +AMOLED 1.8 V2 uses a 16-pixel X gap and 40 MHz QSPI with the same even-pixel CO5300 transfer +constraint. LCD 1.85 uses GPIO5 LEDC backlight at 60% and a 360 × 360 ST77916 panel. GenjuTech +1.54 uses ST7789 SPI3 mode 3 at 80 MHz, color inversion, and GPIO2 LEDC backlight at 60%. +KEVIN SP V4 Dev uses ST7789 SPI3 mode 3 at 80 MHz, a 20-pixel Y offset, color inversion, and a +GPIO48 LEDC backlight. DuChat X uses ST7789 SPI3 mode 0 at 40 MHz, a 2×1-pixel offset, mirrored +axes, and a GPIO9 LEDC backlight. ## CoreS3 pixel-glasses view @@ -43,15 +77,14 @@ CoreS3 alone uses the fixed process-gray skin, 23 expressions and four terminal text and a 10 px signature. Six-digit numeric pairing codes remain 32 px. READY shows NEUTRAL; entering a conversation shows SURPRISED for 300 ms plus its entering transition. LISTENING and THINKING select their faces, continuous THINKING becomes PROCESSING after three seconds, and -SPEAKING/no phase uses NEUTRAL. Four optional one-shot LCD emotion indicators temporarily override -the face and replay on each submission, with 2000/3000/1200/800 ms holds plus a 250 ms enabled -entering transition. The current SDK MCP path accepts only `audio.set_volume` and -`audio.adjust_volume` inside a `message.user` envelope; it does not emit emotion indicators. +SPEAKING/no phase uses NEUTRAL. The current SDK does not expose one-shot emotion indicators; its +MCP path accepts only `audio.set_volume` and `audio.adjust_volume` inside a `message.user` +envelope. These are LCD-only changes. See [CoreS3 UI](CORES3_UI.md) for timing, resources and validation. CoreS3's legacy light-theme setting is retained but does not change the pixel skin. Its animation -option controls expression motion and transitions; disabling it retains state/hold progression -and necessary guidance scrolling. The other twelve profiles retain their existing UI/hardware; +option controls expression motion and transitions; disabling it retains the opener hold and +necessary guidance scrolling. The other profiles retain their existing UI/hardware; SDK, AOSL, RTSA, camera/video policy, input, power and memory configuration are unchanged. ## Shared workflow view on other display boards @@ -69,8 +102,8 @@ available when `CONFIG_MYBOT_LVGL_UI_ANIMATIONS` disables state activity animati The pairing-code screen directs users to enter the code in the web console. Its instruction scrolls only when wider than the footer label and stops scrolling on exit. -On screens narrower than 240 pixels (currently StickS3), Chinese labels retain the 20 px bilingual -font; English status, notification, and ordinary labels use 10 px text. The provisioning SSID +On screens narrower than 240 pixels, Chinese labels retain the 20 px bilingual font; English +status, notification, and ordinary labels use 10 px text. The provisioning SSID uses 20 px in both languages and scrolls if needed. Pairing codes select a 32, 20, or 10 px font to fit, with tighter spacing for long codes on narrow screens. The built-in Chinese subset covers the UI labels, not arbitrary conversation text. @@ -83,6 +116,11 @@ CoreS3's stripe is 10,240 bytes, StickS3's is 4,320 bytes, and AMOLED 2.16's is separate from task stacks and driver allocations. The hardware integration owns reset, offsets, color order, backlight, and teardown. +Round profiles set `MYBOT_DISPLAY_ROUND=1`. The shared view then narrows the header, card and +footer to a central safe rectangle instead of placing widgets in the panel corners. This layout +logic is build-verified only; color, orientation, clipping and touch alignment still require +real-device checks for each panel. + ## Build examples Use a separate build directory for every board and language. No extra UI defaults are required: diff --git a/docs/PLATFORM_UI.zh-CN.md b/docs/PLATFORM_UI.zh-CN.md index 587acc6..122feb8 100644 --- a/docs/PLATFORM_UI.zh-CN.md +++ b/docs/PLATFORM_UI.zh-CN.md @@ -21,9 +21,37 @@ | `esp32-s3-touch-amoled-1.75` | CO5300,466 × 466 | 通用面板适配器 | 原有触摸/按键路径 | | `esp32-s3-touch-amoled-1.75c` | CO5300,466 × 466 | 通用面板适配器 | 原有触摸/按键路径 | | `esp32-s3-touch-amoled-2.16` | CO5300 480 变体,480 × 480 | 通用面板适配器 | CST9217 与 Boot | +| `esp32-s3-touch-amoled-1.8-v2` | CO5300 368×448 变体,368 × 448 | 通用面板适配器 | CST816S 与 Boot | +| `esp32-s3-touch-lcd-1.85` | ST77916,360 × 360 | 通用面板适配器 | CST816S 与 Boot | +| `genjutech-s3-1.54tft` | ST7789,240 × 240 | 通用 ST7789 | Boot 与音量按键 | | `sensecap-watcher` | SPD2010,412 × 412 | 通用面板适配器 | 原有旋钮路径 | - -AtomEchoS3R 与 ReSpeaker Flex 没有 LCD,不编入 LVGL。ML307、Wi-Fi 及音视频协议与显示后端 +| `xingzhi-metal-1.54-wifi`、`xingzhi-abs-2.0` | ST7789,240 × 240 | 通用面板适配器 | Boot/触摸或 Boot/音量键 | +| `xingzhi-cube-0.85tft-wifi` | NV3023,128 × 128 | 通用面板适配器 | Boot | +| `xingzhi-cube-0.96oled-wifi` | 无屏 | 无 | Boot 与音量键 | +| `m5stack-stopwatch` | CO5300,466 × 466 圆屏 | 通用面板适配器 | CST820B 兼容触摸与按键 | +| `esp32-s3-touch-amoled-1.8` | SH8601,368 × 448 | 通用面板适配器 | FT5x06 与 Boot | +| AMOLED 1.32/1.43c/2.06 | SH8601,466 × 466 或 410 × 502 | 通用面板适配器 | Boot;2.06 使用 FT5x06 | +| `atoms3r-echo-base`、`atoms3r-echo-pyramid` | GC9A01 类,128 × 128 | 通用面板适配器 | Boot | +| `sp-esp32-s3-1.28-box` | GC9A01,240 × 240 圆屏 | 通用面板适配器 | CST816S 兼容触摸与 Boot | +| `esp-box`、`esp-box-3`、`esp-box-lite` | ILI9341,320 × 240 | 通用面板适配器 | Boot;未实现触摸 | +| `esp32s3-korvo2-v3`、`df-k10` | ILI9341,320 × 240 | 通用面板适配器 | Boot | +| `lichuang-dev`、`rymcu-bigsmart` | ST7789,320 × 240 | 通用面板适配器 | FT5x06 或 GT911,以及 Boot | +| `quandong-s3-dev` | ILI9341,320 × 240 | 通用面板适配器 | Boot | +| `mixgo-nova` | ST7789,128 × 160 | 通用面板适配器 | Boot | +| `taiji-pi-s3` | ST77916,360 × 360 圆屏 | 通用面板适配器 | CST816S 与 Boot | +| `freenove-esp32s3-display-2.8-lcd` | ILI9341,320 × 240 | 通用面板适配器 | Boot | +| `esp32-s3-touch-lcd-1.54`、`esp32-s3-touch-lcd-1.83` | ST7789,240 × 240 或 240 × 284 | 通用 ST7789 | CST816S 与按键 | +| `esp32-s3-lcd-0.85`、`aipi-lite` | ST7789,128 × 128 | 通用 ST7789 紧凑布局 | 按键 | +| `esp32-s3-touch-lcd-1.85b`、`esp32-s3-touch-lcd-1.85c` | ST77916,360 × 360 | 通用面板适配器 | CST816S 兼容触摸与 Boot | +| `sp-esp32-s3-1.54-muma` | ST7789,240 × 240 | 通用 ST7789 | CST816S 与 Boot | +| `atk-dnesp32s3-box0`、`nulllab-ai-vox-v3` | ST7789,240 × 240 | 通用 ST7789 | 按键 | +| `magiclick-2p4` | NV3023,128 × 128 | 通用面板适配器 | 三个按键 | +| `movecall-cuican-esp32s3`、`movecall-moji-esp32s3` | GC9A01,240 × 240 | 通用面板适配器 | Boot | +| `kevin-sp-v4-dev` | ST7789,240 × 280 | 通用 ST7789 | Boot | +| `du-chatx` | ST7789,128 × 160 | 通用 ST7789 紧凑布局 | Boot | + +AtomEchoS3R、DOIT S3 AI Box、Bread Compact Wi-Fi、HU-087 与 ReSpeaker Flex 没有 LCD, +不编入 LVGL。ML307、Wi-Fi 及音视频协议与显示后端 相互独立。 行者 Cube 使用 SPI3 mode 3 / 40 MHz、零偏移与 GPIO13 LEDC 背光,PWM 为 5 kHz、13-bit、 60% 占空比;显示和电源行为仍需 @@ -34,20 +62,25 @@ GPIO42 LEDC 背光(5 kHz、13-bit、60% 占空比)。仅有一个 Boot 输 AMOLED 2.16 使用零 panel gap 与 40 MHz QSPI;CO5300 传输区域两个方向均保持偶数像素 边界,触摸和面板方向必须在实际板卡上验证。 2.16 中英文构建支持已验证,真机测试仍未完成。 +AMOLED 1.8 V2 使用 16 像素 X 偏移与 40 MHz QSPI,并保持相同的 CO5300 偶数像素约束。 +LCD 1.85 使用 GPIO5 LEDC 背光(60%)与 360 × 360 ST77916 面板。GenjuTech 1.54 使用 +ST7789 SPI3 mode 3 / 80 MHz、颜色反转与 GPIO2 LEDC 背光(60%)。 +KEVIN SP V4 Dev 使用 ST7789 SPI3 mode 3 / 80 MHz、Y 偏移 20、颜色反转与 GPIO48 LEDC +背光。DuChat X 使用 ST7789 SPI3 mode 0 / 40 MHz、2×1 像素偏移、X/Y 镜像与 GPIO9 LEDC +背光。 ## CoreS3像素眼镜视图 仅CoreS3使用固定工艺灰皮肤、23表情及四行终端,正文9 px、签名10 px,六位数字配对码 固定32 px。READY为NEUTRAL;进入会话SURPRISED保持300 ms并另计进入过渡。LISTENING、 THINKING显示对应表情,连续思考三秒后转PROCESSING,SPEAKING/无阶段时NEUTRAL。 -四种可选的一次性LCD情绪指示临时覆盖当前图案,每次提交都会重播,分别保持 -2000/3000/1200/800 ms;开启动画时另加250 ms进入过渡。当前SDK MCP通路仅接受 -`message.user`封装内的`audio.set_volume`与`audio.adjust_volume`,不产生情绪指示。 +当前SDK不提供一次性情绪指示;MCP通路仅接受`message.user`封装内的 +`audio.set_volume`与`audio.adjust_volume`。 仅控制LCD,具体规则、资源与验证见[CoreS3 UI](CORES3_UI.zh-CN.md)。 CoreS3保留浅色主题字段兼容但不改变像素皮肤;动画开关控制表情运动/切换,关闭后仍推进 -状态与保持到期,并保留必要的引导文字滚动。其他十二个profile保留原UI/硬件;SDK、AOSL、 -RTSA、摄像头/视频策略、输入、电源及内存配置不变。 +开场保持并保留必要的引导文字滚动。其他profile保留原UI/硬件;摄像头/视频策略、 +输入、电源及内存配置不变;SDK、AOSL 与 RTSA 输入保持不变。 ## 其他带屏板型的共享流程视图 @@ -62,7 +95,7 @@ Flash,不加入运行时 PNG/GIF 解码器或全屏缓存。 配对码页面提示用户在网页控制台输入配对码;提示超出底部标签宽度时才循环滚动,离开 配对码页面后停止滚动。 -宽度小于 240 像素的屏幕(当前为 StickS3)保留 20 px 中文字库;英文状态、通知及普通 +宽度小于 240 像素的屏幕保留 20 px 中文字库;英文状态、通知及普通 标签使用 10 px 字体。配网 SSID 在两种语言下均使用 20 px,超宽时滚动。配对码根据宽度 选择 32、20 或 10 px 字体,窄屏长配对码同时收紧字距。内置中文字库覆盖 UI 文案, 不覆盖任意中文聊天正文。 @@ -74,6 +107,10 @@ Flash,不加入运行时 PNG/GIF 解码器或全屏缓存。 硬件集成负责复位、偏移、颜色顺序、 背光和销毁。 +圆屏 profile 设置 `MYBOT_DISPLAY_ROUND=1`,共享视图会把标题栏、状态卡和页脚收窄到中央 +安全矩形,避免控件落入圆角区域。当前只完成构建验证;颜色、方向、裁切和触摸坐标仍需逐板 +真机确认。 + ## 编译示例 不同板型和语言请使用独立构建目录,无需额外启用 UI: diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 87c4314..f350b11 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -85,9 +85,19 @@ git diff --check 480x480 CO5300 output with zero gap, even pixel boundaries, panel colors/orientation/brightness, CST9217 press/release/hold, 16 kHz primary-mic and full-duplex audio against the reference 24 kHz setting, PA noise, volume persistence, and repeated provisioning/start/stop. +- [ ] For Waveshare AMOLED 1.8 V2, verify physical Flash/PSRAM, TCA9554/AXP2101 initialization, + the 368x448 CO5300 gap/colors/orientation/brightness, CST816S press/release/hold, ES8311 + bi-directional audio, PA noise, volume persistence, and provisioning while mybot is stopped. +- [ ] For Waveshare LCD 1.85, verify physical Flash/PSRAM, GPIO7 power hold, TCA9554 LCD/touch reset, + panel-version selection, 360x360 colors/orientation, GPIO5 backlight, CST816S, right-slot + microphone capture, both-slot playback, full-duplex audio, volume persistence, and repeated + provisioning/start/stop. +- [ ] For GenjuTech 1.54, verify physical Flash/PSRAM, ST7789 colors/orientation/inversion and GPIO2 + backlight, ES8311 audio and GPIO21 PA/display coupling, Boot/volume buttons, volume + persistence, prompt drain, and repeated provisioning/start/stop. - [ ] Verify an existing build directory rejects a change of `MYBOT_BOARD`, and keep separate - artifacts for all three Waveshare AMOLED profiles. A clean build accepts any profile; it - cannot detect the connected board's revision. Match the physical revision before flashing. + artifacts for every Waveshare touch profile. A clean build accepts any profile; it cannot + detect the connected board's revision. Match the physical device before flashing. - [ ] For SenseCAP Watcher, back up and checksum the 200 KiB `nvsfactory` region before first flash; verify normal flashing leaves it unchanged and never publish an `erase-flash` procedure. - [ ] For M5Stack StickS3, test USB and battery boot, M5PM1 G2/G3 sequencing, speaker pop/noise, @@ -97,6 +107,10 @@ git diff --check polarity and short/long presses, first-boot Wi-Fi AP and localized prompt, 16 kHz capture and playback pitch/noise and full-duplex quality, volume persistence, and repeated provisioning after SDK stop/start. Confirm the headless build omits LVGL. +- [ ] For every additional profile marked "Not completed" in the validation matrix, verify the + applicable panel/controller, touch or keys, codec/I2S path, reset and power ordering, round or + narrow UI clipping, provisioning, volume persistence, and repeated SDK start/stop. Do not + promote a profile to hardware-validated from build or host-test evidence alone. ## Publish diff --git a/docs/RELEASING.zh-CN.md b/docs/RELEASING.zh-CN.md index 1c156f5..b218bcb 100644 --- a/docs/RELEASING.zh-CN.md +++ b/docs/RELEASING.zh-CN.md @@ -74,9 +74,17 @@ git diff --check USB/电池上电时序、可选 TCA9554 搭载情况、MCLK42/LCD-reset39/touch-reset40、480x480 CO5300 零偏移和偶数像素边界、颜色/方向/亮度、CST9217 按下/释放/长按。结合参考 24 kHz 配置验证 16 kHz 主麦和全双工音频、PA 噪声、音量持久化及反复配网/启停。 -- [ ] 确认已有构建目录拒绝切换 `MYBOT_BOARD`,并为三个 Waveshare AMOLED profile 保留独立 +- [ ] 对 Waveshare AMOLED 1.8 V2,验证实际 Flash/PSRAM、TCA9554/AXP2101 初始化、 + 368x448 CO5300 偏移/颜色/方向/亮度、CST816S 按下/释放/长按、ES8311 双向音频、PA + 噪声、音量持久化,以及 mybot 停止期间配网。 +- [ ] 对 Waveshare LCD 1.85,验证实际 Flash/PSRAM、GPIO7 电源保持、TCA9554 LCD/触摸复位、 + 面板版本选择、360x360 颜色/方向、GPIO5 背光、CST816S、右 slot 麦克风采集、双 slot + 播放、全双工音频、音量持久化及反复配网/启停。 +- [ ] 对 GenjuTech 1.54,验证实际 Flash/PSRAM、ST7789 颜色/方向/反转与 GPIO2 背光、 + ES8311 音频与 GPIO21 PA/显示耦合、Boot/音量按键、音量持久化、提示音排空及反复配网/启停。 +- [ ] 确认已有构建目录拒绝切换 `MYBOT_BOARD`,并为每个 Waveshare 触屏 profile 保留独立 制品。全新构建目录可以使用任一 profile,无法识别连接设备的硬件版本;烧录前须核对 - 实际硬件版本与固件是否匹配。 + 实际硬件与固件是否匹配。 - [ ] 对 SenseCAP Watcher,首次烧录前备份并校验 200 KiB `nvsfactory` 区域;确认正常烧录不 改变该区域,且发布流程不得包含 `erase-flash`。 - [ ] 对 M5Stack StickS3,验证 USB 与电池启动、M5PM1 G2/G3 时序、扬声器爆音/噪声、 @@ -85,6 +93,9 @@ git diff --check - [ ] 对 M5Stack AtomEchoS3R,确认检测到 8 MB Flash 与 8 MB Octal PSRAM,验证 GPIO41 按键极性和短/长按、首次配网热点及对应语言提示音、16 kHz 采集/播放的音调与噪声和全双工 质量、音量持久化及 SDK 停止/重启后的重复配网。确认无屏固件不编入 LVGL。 +- [ ] 对验证矩阵中所有标记“未完成”的新增板型,按实际硬件验证面板/控制器、触摸或按键、 + codec/I2S 通路、复位与上电顺序、圆屏或窄屏裁切、配网、音量持久化及反复启停。不得仅凭 + 构建或宿主测试将板型升级为真机验证通过。 ## 发布 diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index a024e42..4d6546e 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -27,6 +27,9 @@ unsupported areas. Board READMEs describe hardware and operation only; the | `esp32-s3-touch-amoled-1.75` | CI: zh-CN, en-US | Not completed / 未完成 | Power, audio slots, display and touch; no video, playback reference, local AEC, RTC, IMU, TF card, battery reporting, sleep or shutdown. | | `esp32-s3-touch-amoled-1.75c` | CI: zh-CN, en-US | Not completed / 未完成 | Same as 1.75, with no TCA9554 support; verify detected Flash capacity before expanding the partition layout. | | `esp32-s3-touch-amoled-2.16` | CI: zh-CN, en-US; recorded merged build / 已记录合并构建 | Not completed / 未完成 | Physical Flash/PSRAM and TCA9554 presence, power, 480x480 display, touch and audio; no video, playback reference, local AEC, RTC, IMU, TF card, battery reporting, sleep or shutdown. | +| `esp32-s3-touch-amoled-1.8-v2` | CI: zh-CN, en-US; recorded merged build / 已记录合并构建 | Not completed / 未完成 | Physical Flash/PSRAM, AXP2101/TCA9554 sequencing, 368x448 QSPI display, CST816S and 16 kHz ES8311 audio require device validation; no AXP2101 power-key action, battery reporting, sleep, shutdown or video. | +| `esp32-s3-touch-lcd-1.85` | CI: zh-CN, en-US; recorded merged build / 已记录合并构建 | Not completed / 未完成 | Physical Flash/PSRAM, GPIO7 power hold, panel-version selection, backlight, CST816S and right-slot microphone/both-slot playback require device validation; GPIO6 power button, battery reporting, sleep, shutdown and video are not implemented. | +| `genjutech-s3-1.54tft` | CI: zh-CN, en-US; recorded merged build / 已记录合并构建 | Not completed / 未完成 | Physical Flash/PSRAM, ST7789, ES8311, PA/display coupling, Boot and volume inputs require device validation; no touch, LED, battery/charging, sleep, shutdown or video. | | `m5stack-core-s3` | CI: zh-CN, en-US, video, light-theme and static-animation variants; recorded video build / 已记录视频构建 | Partial / 部分通过: earlier provisioning, bidirectional audio, camera uplink and status-card UI; current pixel UI and shared UI changes pending / 旧通路通过,当前像素 UI 与共享 UI 待回归 | Current pixel UI, capture scheduling and DMA regression; no video downlink, camera preview, battery reporting or automatic sleep. Video is opt-in for direct builds. | | `m5stack-stick-s3` | CI: zh-CN, en-US | Not completed / 未完成 | Power, audio, input and panel; no GPIO12 support, IMU, infrared, battery reporting, shutdown or low power. | | `respeaker-flex-xvf3800-circular4-xiao` | CI: zh-CN, en-US | Not completed / 未完成 | Requires separately flashed XVF3800 Circular-4 16 kHz two-channel I2S firmware; XVF version, clock direction, routing and audio pending. No Linear-4, XVF firmware update, LED ring, LCD or video. | @@ -34,6 +37,46 @@ unsupported areas. Board READMEs describe hardware and operation only; the | `xingzhi-cube-1.54tft-wifi` | CI: zh-CN, en-US; recorded merged build / 已记录合并构建 | Not completed / 未完成 | Physical Flash/PSRAM, GPIO21 power hold, backlight, display, buttons and 16 kHz audio; no touch, video, battery/charging, shutdown, sleep or low power. | | `zhengchen-1.54tft-ml307` | CI: zh-CN, en-US | Not completed / 未完成 | ML307/4G is not implemented; Wi-Fi remains the network path. Audio, panel and buttons still need device validation; no battery status, temperature monitoring, sleep or low power. | | `zhengchen-1.54tft-wifi` | CI: zh-CN, en-US | Not completed / 未完成 | Physical PSRAM capacity, audio, panel and buttons still need device validation; no modem/4G, battery/charging, temperature monitoring, sleep or low power. | +| `xingzhi-metal-1.54-wifi` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | PCA9557, ES8311, ST7789, touch and PA mapping require device validation; no battery, sleep or shutdown. | +| `xingzhi-cube-0.85tft-wifi` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | NV3023 panel, backlight, raw I2S and power require device validation; no touch or codec control. | +| `xingzhi-cube-0.96oled-wifi` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Headless subset; SSD1306 monochrome UI is not implemented. Raw I2S and volume keys require device validation. | +| `xingzhi-abs-2.0` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Wi-Fi subset only; SD, vibration, cellular, battery, PCA9557 power paths, sleep and shutdown are not implemented. | +| `m5stack-stopwatch` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | M5PM1/M5IOE1 sequencing, CO5300 466×466 panel, CST820B touch, buttons, audio and round safe area require device validation; battery reporting and motor are not implemented. | +| `esp32-s3-touch-amoled-1.8` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | TCA9554/AXP2101 sequencing, SH8601 368×448 timing and FT5x06 mapping require device validation. | +| `esp32-s3-touch-amoled-1.32` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | SH8601 466×466 timing, power rail, audio and panel offset require device validation; touch is not implemented. | +| `esp32-s3-touch-amoled-1.43c` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | SH8601 466×466 timing, ES8311/ES7210 routing and power require device validation; touch is not implemented. | +| `esp32-s3-touch-amoled-2.06` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | SH8601 410×502 timing, 22-pixel X gap, AXP2101, FT5x06 and dual-codec routing require device validation. | +| `atoms3r-echo-base` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Display/audio mapping requires validation; companion IO/power and LP5562 backlight integration are not implemented. | +| `atoms3r-echo-pyramid` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Display/dual-codec mapping requires validation; companion IO/power and LP5562 backlight integration are not implemented. | +| `sp-esp32-s3-1.28-box` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | GC9A01 color/orientation, touch, backlight and power require device validation; battery/charging reporting is not implemented. | +| `esp-box` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ILI9341 initialization, audio routing, touch and board PMIC/power sequencing require device validation. | +| `esp-box-3` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ILI9341 initialization, audio routing, touch and board PMIC/power sequencing require device validation. | +| `esp-box-lite` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Codec assignment, ILI9341, power sequencing and ADC buttons require validation; ADC-button support is not implemented. | +| `esp32s3-korvo2-v3` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | TCA9554 reset/CS, ILI9341, dual-codec routing and board power require device validation; ADC keys, camera and battery are not implemented. | +| `lichuang-dev` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | PCA9557 display/PA control, FT5x06, ST7789 and dual-codec routing require device validation; camera and battery are not implemented. | +| `quandong-s3-dev` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | PCA9557 output mapping, active-low PA, ILI9341 and audio routing require device validation. | +| `rymcu-bigsmart` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | PCA9557 display/PA control, GT911, ST7789 and dual-codec routing require device validation; camera and battery are not implemented. | +| `df-k10` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ILI9341 and dual-codec mapping require validation; TCA9555 keys, camera, LED ring and board power paths are not implemented. | +| `mixgo-nova` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ES8374/ST7789 mapping, power, audio and 128×160 UI require device validation; RGB LED is not implemented. | +| `taiji-pi-s3` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST77916, CST816S, PDM capture, I2S playback, mute/PA and round UI require device validation; board power sequencing is not complete. | +| `freenove-esp32s3-display-2.8-lcd` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ILI9341, ES8311, PA and backlight require device validation; camera, LED and battery paths are not implemented. | +| `esp32-s3-touch-lcd-1.54` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST7789, CST816S, dual codec, auxiliary power and three keys require device validation; battery management, sleep and shutdown are not implemented. | +| `esp32-s3-touch-lcd-1.83` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST7789, CST816S, AXP2101 and dual codec require device validation; PWR41, battery reporting, sleep and shutdown are not implemented. | +| `esp32-s3-lcd-0.85` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST7789 128×128 layout, dual codec, battery enable and keys require device validation; touch is not implemented. | +| `esp32-s3-touch-lcd-1.85b` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST77916 panel revisions, CST816S-compatible touch, dual codec, reset/backlight and power require device validation. | +| `esp32-s3-touch-lcd-1.85c` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST77916, TCA9554 reset, CST816S-compatible touch, dual codec V2 path and power require device validation; V1 raw-I2S is not implemented. | +| `sp-esp32-s3-1.54-muma` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST7789 vendor tuning, CST816S, ES8311, battery and power hold require device validation; sleep and shutdown are not implemented. | +| `atk-dnesp32s3-box0` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST7789, ES8311, GPIO2/GPIO14 power sequencing, three keys and battery require device validation; sleep, shutdown and charging are not implemented. | +| `nulllab-ai-vox-v3` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Wi-Fi subset; ST7789, ES8311, battery and keys require device validation. ML307 networking is not implemented. | +| `aipi-lite` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | ST7789 128×128, ES8311, power hold and battery require device validation; power key, sleep and shutdown are not implemented. | +| `magiclick-2p4` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | NV3023, ES8311, PA, keys and power require device validation; battery, LED, sleep and shutdown are not implemented. | +| `movecall-cuican-esp32s3` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | GC9A01, ES8311, PA and 8 MB layout require device validation; LED, battery, sleep and shutdown are not implemented. | +| `movecall-moji-esp32s3` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | GC9A01, ES8311, PA, keys and power require device validation; LED, battery, sleep and shutdown are not implemented. | +| `doit-s3-aibox` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Headless PDM capture, I2S playback, GPIO48 power and keys require device validation; LED, sleep and shutdown are not implemented. | +| `bread-compact-wifi` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Headless raw I2S, Boot/volume keys and storage require validation; OLED UI, GPIO47 key, lamp and LED are not implemented. | +| `hu-087` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Headless raw I2S, GPIO17 amplifier enable and GPIO18 input require validation; SSD1306 UI is not implemented and long press is used for provisioning. | +| `kevin-sp-v4-dev` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Physical Flash/PSRAM, ST7789 mode 3 timing, ES8311 on I2C1, PA and Boot input require device validation; camera, LED, battery, sleep and shutdown are not implemented. | +| `du-chatx` | CI: zh-CN, en-US; local merged build | Not completed / 未完成 | Physical Flash/PSRAM, GPIO1 power hold, ST7789 orientation, raw audio slots and Boot input require device validation; battery, charging, LED, sleep and shutdown are not implemented. | ## Global boundaries / 全局边界 diff --git a/scripts/build_all.py b/scripts/build_all.py index f9c2dcc..d7cd2ce 100644 --- a/scripts/build_all.py +++ b/scripts/build_all.py @@ -57,7 +57,10 @@ def supported_boards() -> list[BoardProfile]: profiles.append( BoardProfile( name=name, - has_display="mybot_display_add_" in profile, + has_display=( + "mybot_display_add_" in profile + or "set(MYBOT_GENERIC_DISPLAY " in profile + ), video_capable="cores3_camera_video.c" in profile, ) ) diff --git a/tests/co5300_lifecycle/README.md b/tests/co5300_lifecycle/README.md index c77a718..4fd8a68 100644 --- a/tests/co5300_lifecycle/README.md +++ b/tests/co5300_lifecycle/README.md @@ -5,10 +5,10 @@ sh tests/co5300_lifecycle/run.sh ``` This compiles the original production panel against the AMOLED 1.75 and 1.75C -profiles and the separate 480-pixel panel against the 2.16 profile. Constructor -mocks inspect the initialization commands, +profiles, the separate 480-pixel panel against the 2.16 profile, and the +368x448 panel against the 1.8 V2 profile. Constructor mocks inspect the initialization commands, address windows, QSPI configuration, panel dimensions, reset pin, and brightness. -The original 1.75/1.75C command bytes and delays are checked for compatibility. +Each panel variant's command bytes and delays are checked for compatibility. Fault injection covers initialization errors, partially returned handles, cleanup timeouts, retries, and repeated open/close. The existing diff --git a/tests/co5300_lifecycle/run.sh b/tests/co5300_lifecycle/run.sh index 3000013..ca919c4 100644 --- a/tests/co5300_lifecycle/run.sh +++ b/tests/co5300_lifecycle/run.sh @@ -8,12 +8,15 @@ test_build_dir=$(mktemp -d) trap 'rm -r -- "$test_build_dir"' EXIT HUP INT TERM if [ "$#" -eq 0 ]; then - set -- esp32-s3-touch-amoled-1.75 esp32-s3-touch-amoled-1.75c esp32-s3-touch-amoled-2.16 + set -- esp32-s3-touch-amoled-1.75 esp32-s3-touch-amoled-1.75c esp32-s3-touch-amoled-2.16 \ + esp32-s3-touch-amoled-1.8-v2 fi for board_id do panel_source="$repo_root/components/mybot_platform/src/drivers/display/panels/co5300/co5300_panel.c" if [ "$board_id" = esp32-s3-touch-amoled-2.16 ]; then panel_source="$repo_root/components/mybot_platform/src/drivers/display/panels/co5300/co5300_480_panel.c" + elif [ "$board_id" = esp32-s3-touch-amoled-1.8-v2 ]; then + panel_source="$repo_root/components/mybot_platform/src/drivers/display/panels/co5300/co5300_amoled18_panel.c" fi "${CC:-cc}" -std=c11 -Wall -Wextra -Werror \ -I"$test_dir/mocks" -I"$repo_root/tests/st7789_lifecycle/mocks" \ diff --git a/tests/co5300_lifecycle/test_co5300_lifecycle.c b/tests/co5300_lifecycle/test_co5300_lifecycle.c index 8f54a20..c6796bc 100644 --- a/tests/co5300_lifecycle/test_co5300_lifecycle.c +++ b/tests/co5300_lifecycle/test_co5300_lifecycle.c @@ -9,6 +9,14 @@ #include #include +#ifdef MYBOT_DISPLAY_SPI_HOST +#define TEST_SPI_HOST MYBOT_DISPLAY_SPI_HOST +#define TEST_PIXEL_CLOCK_HZ MYBOT_DISPLAY_PIXEL_CLOCK_HZ +#else +#define TEST_SPI_HOST MYBOT_AMOLED175_LCD_SPI_HOST +#define TEST_PIXEL_CLOCK_HZ MYBOT_AMOLED175_LCD_PIXEL_CLOCK_HZ +#endif + #define CHECK(condition) \ do { \ if (!(condition)) { \ @@ -116,6 +124,24 @@ static const co5300_lcd_init_cmd_t s_216_commands[] = { {0x29, NULL, 0, 600}, }; +static const co5300_lcd_init_cmd_t s_18_v2_commands[] = { + {0x11, NULL, 0, 600}, + {0xfe, (uint8_t[]){0x20}, 1, 0}, + {0x19, (uint8_t[]){0x10}, 1, 0}, + {0x1c, (uint8_t[]){0xa0}, 1, 0}, + {0xfe, (uint8_t[]){0x00}, 1, 0}, + {0xc4, (uint8_t[]){0x80}, 1, 0}, + {0x3a, (uint8_t[]){0x55}, 1, 0}, + {0x35, (uint8_t[]){0x00}, 1, 0}, + {0x53, (uint8_t[]){0x20}, 1, 0}, + {0x51, (uint8_t[]){0xff}, 1, 0}, + {0x63, (uint8_t[]){0xff}, 1, 0}, + {0x2a, (uint8_t[]){0x00, 0x00, 0x01, 0xdf}, 4, 0}, + {0x2b, (uint8_t[]){0x00, 0x00, 0x01, 0xdf}, 4, 0}, + {0x36, (uint8_t[]){0x00}, 1, 0}, + {0x29, NULL, 0, 600}, +}; + static void check_command(const co5300_lcd_init_cmd_t *actual, const co5300_lcd_init_cmd_t *expected) { CHECK(actual->cmd == expected->cmd && actual->data_bytes == expected->data_bytes); @@ -136,6 +162,13 @@ static void check_vendor_commands(const co5300_vendor_config_t *vendor) { for (size_t i = 0; i < vendor->init_cmds_size; ++i) { check_command(&vendor->init_cmds[i], &s_216_commands[i]); } + } else if (!strcmp(MYBOT_BOARD_NAME, "esp32-s3-touch-amoled-1.8-v2")) { + CHECK(MYBOT_DISPLAY_WIDTH == 368 && MYBOT_DISPLAY_HEIGHT == 448); + CHECK(MYBOT_DISPLAY_OFFSET_X == 16 && MYBOT_DISPLAY_OFFSET_Y == 0); + CHECK(vendor->init_cmds_size == sizeof(s_18_v2_commands) / sizeof(s_18_v2_commands[0])); + for (size_t i = 0; i < vendor->init_cmds_size; ++i) { + check_command(&vendor->init_cmds[i], &s_18_v2_commands[i]); + } } else { CHECK(!strcmp(MYBOT_BOARD_NAME, "esp32-s3-touch-amoled-1.75") || !strcmp(MYBOT_BOARD_NAME, "esp32-s3-touch-amoled-1.75c")); @@ -154,7 +187,7 @@ const char *esp_err_to_name(esp_err_t error) { } esp_err_t spi_bus_initialize(int host, const spi_bus_config_t *config, int dma) { - CHECK(host == MYBOT_AMOLED175_LCD_SPI_HOST && dma == SPI_DMA_CH_AUTO && !s_spi_open); + CHECK(host == TEST_SPI_HOST && dma == SPI_DMA_CH_AUTO && !s_spi_open); CHECK(config->sclk_io_num == MYBOT_DISPLAY_PCLK); CHECK(config->data0_io_num == MYBOT_DISPLAY_DATA0 && config->data1_io_num == MYBOT_DISPLAY_DATA1); @@ -176,7 +209,7 @@ esp_err_t spi_bus_initialize(int host, const spi_bus_config_t *config, int dma) } esp_err_t spi_bus_free(int host) { - CHECK(host == MYBOT_AMOLED175_LCD_SPI_HOST && s_spi_open && !s_io.open && !s_panel.open); + CHECK(host == TEST_SPI_HOST && s_spi_open && !s_io.open && !s_panel.open); const esp_err_t result = step("spi_free", EVENT_SPI_FREE); if (result == ESP_OK) { s_spi_open = false; @@ -187,9 +220,9 @@ esp_err_t spi_bus_free(int host) { 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) { - CHECK(host == MYBOT_AMOLED175_LCD_SPI_HOST && s_spi_open && !s_io.open); + CHECK(host == TEST_SPI_HOST && s_spi_open && !s_io.open); CHECK(config->cs_gpio_num == MYBOT_DISPLAY_CS && config->dc_gpio_num == GPIO_NUM_NC); - CHECK(config->spi_mode == 0 && config->pclk_hz == MYBOT_AMOLED175_LCD_PIXEL_CLOCK_HZ); + CHECK(config->spi_mode == 0 && config->pclk_hz == TEST_PIXEL_CLOCK_HZ); CHECK(config->lcd_cmd_bits == 32 && config->lcd_param_bits == 8 && config->flags.quad_mode); CHECK(config->trans_queue_depth == 2); const esp_err_t result = step("io_new", EVENT_IO_NEW); diff --git a/tests/cst816s_input/mocks/button_gpio.h b/tests/cst816s_input/mocks/button_gpio.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/button_gpio.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/driver/gpio.h b/tests/cst816s_input/mocks/driver/gpio.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/driver/gpio.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/driver/i2c_master.h b/tests/cst816s_input/mocks/driver/i2c_master.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/driver/i2c_master.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/driver/spi_common.h b/tests/cst816s_input/mocks/driver/spi_common.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/driver/spi_common.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/esp_lcd_touch.h b/tests/cst816s_input/mocks/esp_lcd_touch.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/esp_lcd_touch.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/esp_lcd_touch_cst816s.h b/tests/cst816s_input/mocks/esp_lcd_touch_cst816s.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/esp_lcd_touch_cst816s.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/esp_log.h b/tests/cst816s_input/mocks/esp_log.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/esp_log.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/freertos/FreeRTOS.h b/tests/cst816s_input/mocks/freertos/FreeRTOS.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/freertos/FreeRTOS.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/freertos/semphr.h b/tests/cst816s_input/mocks/freertos/semphr.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/freertos/semphr.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/freertos/task.h b/tests/cst816s_input/mocks/freertos/task.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/freertos/task.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/iot_button.h b/tests/cst816s_input/mocks/iot_button.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/iot_button.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/mock_platform.h b/tests/cst816s_input/mocks/mock_platform.h new file mode 100644 index 0000000..2847300 --- /dev/null +++ b/tests/cst816s_input/mocks/mock_platform.h @@ -0,0 +1,166 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef CST816S_INPUT_MOCK_PLATFORM_H_ +#define CST816S_INPUT_MOCK_PLATFORM_H_ + +#include +#include +#include + +typedef int esp_err_t; +#define ESP_OK 0 +#define ESP_FAIL -1 +#define ESP_ERR_NO_MEM 0x101 +const char *esp_err_to_name(esp_err_t error); + +typedef int gpio_num_t; +#define GPIO_NUM_NC -1 +#define GPIO_NUM_0 0 +#define GPIO_NUM_1 1 +#define GPIO_NUM_2 2 +#define GPIO_NUM_3 3 +#define GPIO_NUM_4 4 +#define GPIO_NUM_5 5 +#define GPIO_NUM_7 7 +#define GPIO_NUM_10 10 +#define GPIO_NUM_11 11 +#define GPIO_NUM_15 15 +#define GPIO_NUM_21 21 +#define GPIO_NUM_38 38 +#define GPIO_NUM_39 39 +#define GPIO_NUM_40 40 +#define GPIO_NUM_41 41 +#define GPIO_NUM_42 42 +#define GPIO_NUM_45 45 +#define GPIO_NUM_46 46 +#define GPIO_NUM_47 47 +#define GPIO_NUM_48 48 + +#define I2C_NUM_0 0 +#define I2C_NUM_1 1 +#define SPI2_HOST 2 + +#define ESP_LOGI(...) ((void)0) +#define ESP_LOGW(...) ((void)0) +#define ESP_LOGE(...) ((void)0) + +typedef pthread_mutex_t portMUX_TYPE; +#define portMUX_INITIALIZER_UNLOCKED PTHREAD_MUTEX_INITIALIZER +#define portENTER_CRITICAL(lock) ((void)pthread_mutex_lock(lock)) +#define portEXIT_CRITICAL(lock) ((void)pthread_mutex_unlock(lock)) + +#define pdMS_TO_TICKS(ms) (ms) +#define pdPASS 1 +#define pdTRUE 1 +#define portMAX_DELAY UINT32_MAX +typedef uint32_t TickType_t; + +typedef struct mock_task *TaskHandle_t; +typedef void (*task_function_t)(void *); +int xTaskCreate(task_function_t task, const char *name, uint32_t stack_size, void *argument, + unsigned int priority, TaskHandle_t *out_task); +TaskHandle_t xTaskGetCurrentTaskHandle(void); +void vTaskDelete(TaskHandle_t task); +void vTaskDelay(TickType_t ticks); +TickType_t xTaskGetTickCount(void); + +typedef struct mock_semaphore { + pthread_mutex_t lock; + pthread_cond_t condition; + bool given; +} StaticSemaphore_t; +typedef StaticSemaphore_t *SemaphoreHandle_t; +SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *storage); +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_panel_io *esp_lcd_panel_io_handle_t; +typedef struct mock_touch *esp_lcd_touch_handle_t; + +typedef struct { + uint16_t dev_addr; + uint32_t scl_speed_hz; + unsigned int control_phase_bytes; + unsigned int dc_bit_offset; + unsigned int lcd_cmd_bits; + unsigned int lcd_param_bits; + struct { + unsigned int disable_control_phase : 1; + } flags; +} esp_lcd_panel_io_i2c_config_t; +esp_err_t esp_lcd_new_panel_io_i2c(i2c_master_bus_handle_t bus, + const esp_lcd_panel_io_i2c_config_t *config, + esp_lcd_panel_io_handle_t *out_io); +esp_err_t esp_lcd_panel_io_del(esp_lcd_panel_io_handle_t io); + +typedef struct { + uint16_t x; + uint16_t y; +} esp_lcd_touch_point_data_t; +typedef struct { + uint16_t x_max; + uint16_t y_max; + gpio_num_t rst_gpio_num; + gpio_num_t int_gpio_num; + struct { + uint8_t reset; + uint8_t interrupt; + } levels; + struct { + uint8_t swap_xy; + uint8_t mirror_x; + uint8_t mirror_y; + } flags; +} esp_lcd_touch_config_t; +esp_err_t esp_lcd_touch_new_i2c_cst816s(esp_lcd_panel_io_handle_t io, + const esp_lcd_touch_config_t *config, + esp_lcd_touch_handle_t *out_touch); +esp_err_t esp_lcd_touch_del(esp_lcd_touch_handle_t touch); +esp_err_t esp_lcd_touch_read_data(esp_lcd_touch_handle_t touch); +esp_err_t esp_lcd_touch_get_data(esp_lcd_touch_handle_t touch, esp_lcd_touch_point_data_t *data, + uint8_t *count, uint8_t max_count); +#define ESP_LCD_TOUCH_IO_I2C_CST816S_ADDRESS 0x15 + +typedef struct mock_button *button_handle_t; +typedef struct { + uint32_t long_press_time; + uint32_t short_press_time; +} button_config_t; +typedef struct { + gpio_num_t gpio_num; + int active_level; + bool enable_power_save; + bool disable_pull; +} button_gpio_config_t; +typedef enum { + BUTTON_SINGLE_CLICK = 1, + BUTTON_LONG_PRESS_START = 2, +} button_event_t; +typedef void (*button_callback_t)(void *button, void *user_data); +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); +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); +esp_err_t iot_button_delete(button_handle_t button); + +typedef enum { + MYBOT_STATE_STARTING, + MYBOT_STATE_READY, + MYBOT_STATE_IN_CONVERSATION, +} mybot_state_t; +mybot_state_t mybot_get_state(void); + +typedef enum { + MYBOT_KEY_EVENT_CONVERSATION_START, + MYBOT_KEY_EVENT_CONVERSATION_STOP, +} mybot_key_event_t; +typedef void (*mybot_key_event_handler_t)(mybot_key_event_t event, void *user_data); +typedef struct { + int (*init)(void **out_context, mybot_key_event_handler_t emit, void *user_data); + void (*destroy)(void *context); +} mybot_key_ops_t; + +int mybot_board_handle_boot_long_press(void); + +#endif /* CST816S_INPUT_MOCK_PLATFORM_H_ */ diff --git a/tests/cst816s_input/mocks/mybot/mybot.h b/tests/cst816s_input/mocks/mybot/mybot.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/mybot/mybot.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/mybot/platform/mybot_key.h b/tests/cst816s_input/mocks/mybot/platform/mybot_key.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/mybot/platform/mybot_key.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/mocks/platform/board_actions.h b/tests/cst816s_input/mocks/platform/board_actions.h new file mode 100644 index 0000000..b65df2b --- /dev/null +++ b/tests/cst816s_input/mocks/platform/board_actions.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "mock_platform.h" diff --git a/tests/cst816s_input/run.sh b/tests/cst816s_input/run.sh new file mode 100644 index 0000000..11e0396 --- /dev/null +++ b/tests/cst816s_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/cst816s_input" +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"$test_dir/mocks" \ + -I"$repo_root/components/mybot_platform/boards/esp32-s3-touch-lcd-1.85" \ + "$test_dir/test_cst816s_input.c" \ + "$repo_root/components/mybot_platform/src/drivers/input/cst816s_boot_input.c" \ + -o "$test_build_dir/test_cst816s_input" + +if command -v timeout >/dev/null 2>&1; then + timeout 20 "$test_build_dir/test_cst816s_input" +else + "$test_build_dir/test_cst816s_input" +fi diff --git a/tests/cst816s_input/test_cst816s_input.c b/tests/cst816s_input/test_cst816s_input.c new file mode 100644 index 0000000..f84a54d --- /dev/null +++ b/tests/cst816s_input/test_cst816s_input.c @@ -0,0 +1,488 @@ +/* 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_panel_io { + bool open; +}; +struct mock_touch { + bool open; +}; +struct mock_button { + bool open; + button_callback_t click; + button_callback_t long_press; + void *click_user_data; + void *long_press_user_data; +}; +struct mock_task { + pthread_t thread; + task_function_t function; + void *argument; +}; + +static struct mock_i2c_bus g_bus; +static struct mock_panel_io g_io; +static struct mock_touch g_touch; +static struct mock_button g_button; +static bool g_fail_button_new; +static button_event_t g_fail_register_event; +static bool g_fail_button_delete; +static bool g_fail_io_new; +static bool g_fail_io_delete; +static bool g_fail_touch_new; +static bool g_fail_touch_delete; +static bool g_fail_task_create; +static unsigned int g_button_delete_calls; +static unsigned int g_provision_calls; +static unsigned int g_state_calls; +static mybot_state_t g_state = MYBOT_STATE_STARTING; +static _Thread_local struct mock_task *g_current_task; + +const char *esp_err_to_name(esp_err_t error) { + return error == ESP_OK ? "ESP_OK" : "ESP_FAIL"; +} + +i2c_master_bus_handle_t mybot_board_touch_i2c_bus_handle(void) { + return &g_bus; +} + +esp_err_t esp_lcd_new_panel_io_i2c(i2c_master_bus_handle_t bus, + const esp_lcd_panel_io_i2c_config_t *config, + esp_lcd_panel_io_handle_t *out_io) { + CHECK(bus == &g_bus && !g_io.open); + CHECK(config->dev_addr == ESP_LCD_TOUCH_IO_I2C_CST816S_ADDRESS); + CHECK(config->scl_speed_hz == MYBOT_TOUCH_I2C_SPEED_HZ); + CHECK(config->control_phase_bytes == 1 && config->dc_bit_offset == 0); + CHECK(config->lcd_cmd_bits == 8 && config->lcd_param_bits == 0); + CHECK(config->flags.disable_control_phase == 1); + if (g_fail_io_new) { + g_fail_io_new = false; + return ESP_FAIL; + } + g_io.open = true; + *out_io = &g_io; + return ESP_OK; +} + +esp_err_t esp_lcd_panel_io_del(esp_lcd_panel_io_handle_t io) { + CHECK(io == &g_io && g_io.open); + if (g_fail_io_delete) { + g_fail_io_delete = false; + return ESP_FAIL; + } + g_io.open = false; + return ESP_OK; +} + +esp_err_t esp_lcd_touch_new_i2c_cst816s(esp_lcd_panel_io_handle_t io, + const esp_lcd_touch_config_t *config, + esp_lcd_touch_handle_t *out_touch) { + CHECK(io == &g_io && g_io.open && !g_touch.open); + CHECK(config->x_max == MYBOT_DISPLAY_WIDTH - 1); + CHECK(config->y_max == MYBOT_DISPLAY_HEIGHT - 1); + CHECK(config->rst_gpio_num == MYBOT_TOUCH_RESET); + CHECK(config->int_gpio_num == MYBOT_TOUCH_INTERRUPT); + CHECK(config->levels.reset == MYBOT_TOUCH_RESET_LEVEL); + CHECK(config->levels.interrupt == MYBOT_TOUCH_INTERRUPT_LEVEL); + CHECK(config->flags.swap_xy == MYBOT_TOUCH_SWAP_XY); + CHECK(config->flags.mirror_x == MYBOT_TOUCH_MIRROR_X); + CHECK(config->flags.mirror_y == MYBOT_TOUCH_MIRROR_Y); + if (g_fail_touch_new) { + g_fail_touch_new = false; + return ESP_FAIL; + } + g_touch.open = true; + *out_touch = &g_touch; + return ESP_OK; +} + +esp_err_t esp_lcd_touch_del(esp_lcd_touch_handle_t touch) { + CHECK(touch == &g_touch && g_touch.open); + if (g_fail_touch_delete) { + g_fail_touch_delete = false; + return ESP_FAIL; + } + g_touch.open = false; + return ESP_OK; +} + +esp_err_t esp_lcd_touch_read_data(esp_lcd_touch_handle_t touch) { + CHECK(touch == &g_touch && g_touch.open); + return ESP_FAIL; +} + +esp_err_t esp_lcd_touch_get_data(esp_lcd_touch_handle_t touch, esp_lcd_touch_point_data_t *data, + uint8_t *count, uint8_t max_count) { + (void)touch; + (void)data; + (void)max_count; + *count = 0; + return ESP_FAIL; +} + +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->long_press_time == MYBOT_BOOT_BUTTON_LONG_PRESS_MS); + CHECK(config->short_press_time == 50); + CHECK(gpio_config->gpio_num == MYBOT_BOOT_BUTTON_GPIO); + CHECK(gpio_config->active_level == 0); + CHECK(!g_button.open); + if (g_fail_button_new) { + g_fail_button_new = false; + return ESP_FAIL; + } + 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); + CHECK(event_data == NULL && callback != NULL); + if (g_fail_register_event == event) { + g_fail_register_event = 0; + return ESP_FAIL; + } + if (event == BUTTON_SINGLE_CLICK) { + button->click = callback; + button->click_user_data = user_data; + } else { + CHECK(event == BUTTON_LONG_PRESS_START); + button->long_press = callback; + button->long_press_user_data = user_data; + } + return ESP_OK; +} + +esp_err_t iot_button_delete(button_handle_t button) { + CHECK(button == &g_button && button->open); + ++g_button_delete_calls; + if (g_fail_button_delete) { + g_fail_button_delete = false; + return ESP_FAIL; + } + button->open = false; + button->click = NULL; + button->long_press = NULL; + return ESP_OK; +} + +mybot_state_t mybot_get_state(void) { + ++g_state_calls; + return g_state; +} + +int mybot_board_handle_boot_long_press(void) { + ++g_provision_calls; + 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) { + g_fail_task_create = false; + *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 fire_button(button_event_t event) { + CHECK(g_button.open); + button_callback_t callback = + event == BUTTON_SINGLE_CLICK ? g_button.click : g_button.long_press; + void *user_data = + event == BUTTON_SINGLE_CLICK ? g_button.click_user_data : g_button.long_press_user_data; + CHECK(callback); + callback(&g_button, user_data); +} + +static void assert_released(void) { + CHECK(!g_button.open && !g_touch.open && !g_io.open); +} + +static unsigned int g_event_count; +static mybot_key_event_t g_last_event; + +static void record_event(mybot_key_event_t event, void *user_data) { + CHECK(user_data == &g_event_count); + ++g_event_count; + g_last_event = event; +} + +static void test_start_failures_and_retry(void) { + const mybot_key_ops_t *ops = mybot_cst816s_input_ops(); + void *context = NULL; + CHECK(ops && ops->init && ops->destroy); + CHECK(ops->init(&context, record_event, &g_event_count) < 0 && !context); + CHECK(mybot_cst816s_input_stop() == 0); + + g_fail_button_new = true; + CHECK(mybot_cst816s_input_start() < 0); + assert_released(); + g_fail_register_event = BUTTON_LONG_PRESS_START; + CHECK(mybot_cst816s_input_start() < 0); + assert_released(); + g_fail_io_new = true; + CHECK(mybot_cst816s_input_start() < 0); + assert_released(); + g_fail_touch_new = true; + CHECK(mybot_cst816s_input_start() < 0); + assert_released(); + g_fail_task_create = true; + CHECK(mybot_cst816s_input_start() < 0); + assert_released(); + + CHECK(mybot_cst816s_input_start() == 0); + CHECK(g_button.open && g_touch.open && g_io.open); + CHECK(mybot_cst816s_input_start() < 0); + CHECK(mybot_cst816s_input_stop() == 0); + CHECK(mybot_cst816s_input_stop() == 0); + assert_released(); + CHECK(mybot_cst816s_input_start() == 0); + CHECK(mybot_cst816s_input_stop() == 0); + assert_released(); +} + +static void test_cleanup_retry(void) { + CHECK(mybot_cst816s_input_start() == 0); + g_fail_touch_delete = true; + CHECK(mybot_cst816s_input_stop() < 0); + CHECK(!g_button.open && g_touch.open && g_io.open); + CHECK(mybot_cst816s_input_stop() == 0); + assert_released(); + + CHECK(mybot_cst816s_input_start() == 0); + g_fail_io_delete = true; + CHECK(mybot_cst816s_input_stop() < 0); + CHECK(!g_button.open && !g_touch.open && g_io.open); + CHECK(mybot_cst816s_input_stop() == 0); + assert_released(); + + CHECK(mybot_cst816s_input_start() == 0); + g_fail_button_delete = true; + CHECK(mybot_cst816s_input_stop() < 0); + CHECK(g_button.open && !g_touch.open && !g_io.open); + CHECK(mybot_cst816s_input_stop() == 0); + assert_released(); +} + +static void test_callback_lifecycle(void) { + const mybot_key_ops_t *ops = mybot_cst816s_input_ops(); + CHECK(mybot_cst816s_input_start() == 0); + for (unsigned int cycle = 0; cycle < 3; ++cycle) { + void *context = NULL; + CHECK(ops->init(&context, record_event, &g_event_count) == 0 && context); + void *duplicate = NULL; + CHECK(ops->init(&duplicate, record_event, &g_event_count) < 0 && !duplicate); + + g_state = MYBOT_STATE_READY; + fire_button(BUTTON_SINGLE_CLICK); + CHECK(g_event_count == cycle * 2 + 1); + CHECK(g_last_event == MYBOT_KEY_EVENT_CONVERSATION_START); + g_state = MYBOT_STATE_IN_CONVERSATION; + fire_button(BUTTON_SINGLE_CLICK); + CHECK(g_event_count == cycle * 2 + 2); + CHECK(g_last_event == MYBOT_KEY_EVENT_CONVERSATION_STOP); + g_state = MYBOT_STATE_STARTING; + fire_button(BUTTON_SINGLE_CLICK); + CHECK(g_event_count == cycle * 2 + 2); + + unsigned int provisions = g_provision_calls; + fire_button(BUTTON_LONG_PRESS_START); + CHECK(g_provision_calls == provisions + 1); + ops->destroy(context); + + const unsigned int states = g_state_calls; + g_state = MYBOT_STATE_READY; + fire_button(BUTTON_SINGLE_CLICK); + CHECK(g_state_calls == states && g_event_count == cycle * 2 + 2); + fire_button(BUTTON_LONG_PRESS_START); + CHECK(g_provision_calls == provisions + 2); + } + CHECK(mybot_cst816s_input_stop() == 0); +} + +static pthread_mutex_t g_callback_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t g_callback_condition = PTHREAD_COND_INITIALIZER; +static bool g_callback_entered; +static bool g_callback_release; +static atomic_bool g_destroy_done; + +static void blocking_event(mybot_key_event_t event, void *user_data) { + CHECK(event == MYBOT_KEY_EVENT_CONVERSATION_START); + CHECK(user_data == &g_callback_entered); + pthread_mutex_lock(&g_callback_lock); + g_callback_entered = true; + pthread_cond_broadcast(&g_callback_condition); + while (!g_callback_release) { + CHECK(pthread_cond_wait(&g_callback_condition, &g_callback_lock) == 0); + } + pthread_mutex_unlock(&g_callback_lock); +} + +static void wait_for_callback(void) { + struct timespec deadline; + CHECK(clock_gettime(CLOCK_REALTIME, &deadline) == 0); + deadline.tv_sec += 2; + pthread_mutex_lock(&g_callback_lock); + while (!g_callback_entered) { + const int result = + pthread_cond_timedwait(&g_callback_condition, &g_callback_lock, &deadline); + CHECK(result != ETIMEDOUT && result == 0); + } + pthread_mutex_unlock(&g_callback_lock); +} + +static void *click_thread(void *argument) { + (void)argument; + fire_button(BUTTON_SINGLE_CLICK); + return NULL; +} + +static void *destroy_thread(void *argument) { + mybot_cst816s_input_ops()->destroy(argument); + atomic_store(&g_destroy_done, true); + return NULL; +} + +static void test_destroy_waits_for_callback(void) { + const mybot_key_ops_t *ops = mybot_cst816s_input_ops(); + CHECK(mybot_cst816s_input_start() == 0); + void *context = NULL; + g_state = MYBOT_STATE_READY; + CHECK(ops->init(&context, blocking_event, &g_callback_entered) == 0); + + pthread_t click; + CHECK(pthread_create(&click, NULL, click_thread, NULL) == 0); + wait_for_callback(); + atomic_store(&g_destroy_done, false); + pthread_t destroy; + CHECK(pthread_create(&destroy, NULL, destroy_thread, context) == 0); + + struct timespec pause = {.tv_nsec = 50000000L}; + nanosleep(&pause, NULL); + CHECK(!atomic_load(&g_destroy_done)); + + pthread_mutex_lock(&g_callback_lock); + g_callback_release = true; + pthread_cond_broadcast(&g_callback_condition); + pthread_mutex_unlock(&g_callback_lock); + CHECK(pthread_join(click, NULL) == 0); + CHECK(pthread_join(destroy, NULL) == 0); + CHECK(atomic_load(&g_destroy_done)); + CHECK(mybot_cst816s_input_stop() == 0); + assert_released(); +} + +int main(void) { + test_start_failures_and_retry(); + test_cleanup_retry(); + test_callback_lifecycle(); + test_destroy_waits_for_callback(); + printf("CST816S/Boot input host fault-injection tests passed (%u button deletes)\n", + g_button_delete_calls); + return 0; +} diff --git a/tests/st7789_lifecycle/README.md b/tests/st7789_lifecycle/README.md index 6a8938c..c51f5db 100644 --- a/tests/st7789_lifecycle/README.md +++ b/tests/st7789_lifecycle/README.md @@ -7,8 +7,8 @@ sh tests/st7789_lifecycle/run.sh ``` The harness compiles the production ST7789 panel and shared LVGL adapter with -host implementations of the ESP-IDF and LVGL APIs. It uses the -`xingzhi-cube-1.54tft-wifi` board configuration. +host implementations of the ESP-IDF and LVGL APIs. It runs against the +`xingzhi-cube-1.54tft-wifi` and `genjutech-s3-1.54tft` board configurations. Coverage includes initialization failures, partially returned constructor handles, retained resources after cleanup failures, repeated attach/detach and diff --git a/tests/st7789_lifecycle/mocks/host_platform.h b/tests/st7789_lifecycle/mocks/host_platform.h index 0be1bcb..27f990e 100644 --- a/tests/st7789_lifecycle/mocks/host_platform.h +++ b/tests/st7789_lifecycle/mocks/host_platform.h @@ -37,6 +37,7 @@ esp_err_t rtc_gpio_deinit(gpio_num_t gpio); #define GPIO_NUM_0 0 #define GPIO_NUM_1 1 #define GPIO_NUM_2 2 +#define GPIO_NUM_3 3 #define GPIO_NUM_4 4 #define GPIO_NUM_5 5 #define GPIO_NUM_6 6 @@ -44,6 +45,7 @@ esp_err_t rtc_gpio_deinit(gpio_num_t gpio); #define GPIO_NUM_8 8 #define GPIO_NUM_9 9 #define GPIO_NUM_10 10 +#define GPIO_NUM_11 11 #define GPIO_NUM_12 12 #define GPIO_NUM_13 13 #define GPIO_NUM_14 14 diff --git a/tests/st7789_lifecycle/run.sh b/tests/st7789_lifecycle/run.sh index 27ab9ea..d4041f9 100644 --- a/tests/st7789_lifecycle/run.sh +++ b/tests/st7789_lifecycle/run.sh @@ -4,23 +4,31 @@ set -eu repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) test_dir="$repo_root/tests/st7789_lifecycle" -board_dir="$repo_root/components/mybot_platform/boards/xingzhi-cube-1.54tft-wifi" test_build_dir=$(mktemp -d) trap 'rm -r -- "$test_build_dir"' EXIT HUP INT TERM -"${CC:-cc}" -std=c11 -Wall -Wextra -Werror \ - -I"$test_dir/mocks" -I"$board_dir" \ - -I"$repo_root/components/mybot_platform/src/internal" \ - -c "$repo_root/components/mybot_platform/src/drivers/display/panels/st7789/st7789_panel.c" \ - -o "$test_build_dir/panel.o" -"${CXX:-c++}" -std=c++17 -Wall -Wextra -Werror -pthread \ - -I"$test_dir/mocks" -I"$board_dir" \ - -I"$repo_root/components/mybot_platform/src/internal" \ - -I"$repo_root/components/mybot_stack/mybot_sdk/mybot/include" \ - "$test_dir/test_st7789_lifecycle.cc" \ - "$repo_root/components/mybot_platform/src/drivers/display/adapters/lvgl/shared_lvgl_adapter.cc" \ - "$test_build_dir/panel.o" -o "$test_build_dir/test_st7789_lifecycle" +for board_id in xingzhi-cube-1.54tft-wifi genjutech-s3-1.54tft; do + board_dir="$repo_root/components/mybot_platform/boards/$board_id" + "${CC:-cc}" -std=c11 -Wall -Wextra -Werror \ + -I"$test_dir/mocks" -I"$board_dir" \ + -I"$repo_root/components/mybot_platform/src/internal" \ + -c "$repo_root/components/mybot_platform/src/drivers/display/panels/st7789/st7789_panel.c" \ + -o "$test_build_dir/panel-$board_id.o" + "${CXX:-c++}" -std=c++17 -Wall -Wextra -Werror -pthread \ + -I"$test_dir/mocks" -I"$board_dir" \ + -I"$repo_root/components/mybot_platform/src/internal" \ + -I"$repo_root/components/mybot_stack/mybot_sdk/mybot/include" \ + "$test_dir/test_st7789_lifecycle.cc" \ + "$repo_root/components/mybot_platform/src/drivers/display/adapters/lvgl/shared_lvgl_adapter.cc" \ + "$test_build_dir/panel-$board_id.o" -o "$test_build_dir/test_st7789_lifecycle-$board_id" + if command -v timeout >/dev/null 2>&1; then + timeout 20 "$test_build_dir/test_st7789_lifecycle-$board_id" + else + "$test_build_dir/test_st7789_lifecycle-$board_id" + fi +done +board_dir="$repo_root/components/mybot_platform/boards/xingzhi-cube-1.54tft-wifi" "${CC:-cc}" -std=c11 -Wall -Wextra -Werror \ -I"$test_dir/mocks" -I"$board_dir" \ -I"$repo_root/components/mybot_platform/include" \ @@ -35,12 +43,6 @@ trap 'rm -r -- "$test_build_dir"' EXIT HUP INT TERM "$test_dir/test_xingzhi_board.cc" "$test_build_dir/board.o" \ -o "$test_build_dir/test_xingzhi_board" -if command -v timeout >/dev/null 2>&1; then - timeout 20 "$test_build_dir/test_st7789_lifecycle" -else - "$test_build_dir/test_st7789_lifecycle" -fi - for scenario in rtc_hold_dis rtc_deinit gpio_config gpio_high ledc_timer ledc_channel \ ledc_duty ledc_update lcd_init lcd_render buttons_start ok; do "$test_build_dir/test_xingzhi_board" "$scenario" diff --git a/tests/tca9554/mocks/driver/i2c_master.h b/tests/tca9554/mocks/driver/i2c_master.h new file mode 100644 index 0000000..1b6e7c8 --- /dev/null +++ b/tests/tca9554/mocks/driver/i2c_master.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: MIT */ +#ifndef TCA9554_MOCK_I2C_H_ +#define TCA9554_MOCK_I2C_H_ + +#include +#include + +typedef int esp_err_t; +#define ESP_OK 0 +#define ESP_FAIL -1 +#define I2C_ADDR_BIT_LEN_7 0 + +typedef struct mock_i2c_bus *i2c_master_bus_handle_t; +typedef struct mock_i2c_device *i2c_master_dev_handle_t; +typedef struct { + int 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); + +#endif /* TCA9554_MOCK_I2C_H_ */ diff --git a/tests/tca9554/mocks/esp_err.h b/tests/tca9554/mocks/esp_err.h new file mode 100644 index 0000000..bc3b792 --- /dev/null +++ b/tests/tca9554/mocks/esp_err.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#include "driver/i2c_master.h" diff --git a/tests/tca9554/mocks/esp_log.h b/tests/tca9554/mocks/esp_log.h new file mode 100644 index 0000000..e8a566b --- /dev/null +++ b/tests/tca9554/mocks/esp_log.h @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +#define ESP_LOGE(...) ((void)0) diff --git a/tests/tca9554/run.sh b/tests/tca9554/run.sh new file mode 100644 index 0000000..a4a63bc --- /dev/null +++ b/tests/tca9554/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/tca9554" +test_build_dir=$(mktemp -d) +trap 'rm -r -- "$test_build_dir"' EXIT HUP INT TERM + +"${CC:-cc}" -std=c11 -Wall -Wextra -Werror \ + -I"$test_dir/mocks" \ + -I"$repo_root/components/mybot_platform/src/internal" \ + "$test_dir/test_tca9554.c" \ + "$repo_root/components/mybot_platform/src/drivers/io_expander/tca9554.c" \ + -o "$test_build_dir/test_tca9554" + +if command -v timeout >/dev/null 2>&1; then + timeout 20 "$test_build_dir/test_tca9554" +else + "$test_build_dir/test_tca9554" +fi diff --git a/tests/tca9554/test_tca9554.c b/tests/tca9554/test_tca9554.c new file mode 100644 index 0000000..52c8d9c --- /dev/null +++ b/tests/tca9554/test_tca9554.c @@ -0,0 +1,118 @@ +/* SPDX-License-Identifier: MIT */ +#include "io_expander/tca9554.h" + +#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 { + i2c_master_bus_handle_t bus; + uint16_t address; + uint32_t speed_hz; + bool open; +}; + +static struct mock_i2c_bus g_bus; +static struct mock_i2c_device g_device; +static uint8_t g_register; +static uint8_t g_value; +static bool g_fail_add; +static bool g_fail_remove; +static bool g_fail_write; +static unsigned int g_writes; + +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 && !g_device.open); + CHECK(config->dev_addr_length == I2C_ADDR_BIT_LEN_7); + CHECK(config->device_address == 0x20 && config->scl_speed_hz == 400000); + if (g_fail_add) { + g_fail_add = false; + return ESP_FAIL; + } + g_device = (struct mock_i2c_device){ + .bus = bus, + .address = config->device_address, + .speed_hz = config->scl_speed_hz, + .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 && g_device.open); + 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); + CHECK(data && size == 2 && timeout_ms == 100); + if (g_fail_write) { + g_fail_write = false; + return ESP_FAIL; + } + g_register = data[0]; + g_value = data[1]; + ++g_writes; + return ESP_OK; +} + +int main(void) { + mybot_tca9554_t expander = {0}; + CHECK(mybot_tca9554_init(NULL, &g_bus, 0x20, 400000) < 0); + CHECK(mybot_tca9554_init(&expander, NULL, 0x20, 400000) < 0); + g_fail_add = true; + CHECK(mybot_tca9554_init(&expander, &g_bus, 0x20, 400000) < 0); + CHECK(!expander.device); + + CHECK(mybot_tca9554_init(&expander, &g_bus, 0x20, 400000) == 0); + CHECK(expander.device == &g_device && expander.output_state == 0); + CHECK(mybot_tca9554_init(&expander, &g_bus, 0x20, 400000) < 0); + + CHECK(mybot_tca9554_configure_outputs(&expander, 0x07, 0x02) == 0); + CHECK(g_register == 0x03 && g_value == 0xf8); + CHECK(g_writes == 2 && expander.output_state == 0x02); + CHECK(mybot_tca9554_set_levels(&expander, 0x01, 0x01) == 0); + CHECK(g_register == 0x01 && g_value == 0x03); + CHECK(mybot_tca9554_set_levels(&expander, 0x02, 0x02) == 0); + CHECK(g_value == 0x03); + CHECK(mybot_tca9554_set_levels(&expander, 0x03, 0x00) == 0); + CHECK(g_value == 0x00 && expander.output_state == 0); + + g_fail_write = true; + CHECK(mybot_tca9554_set_levels(&expander, 0x01, 0x01) < 0); + CHECK(expander.output_state == 0); + CHECK(mybot_tca9554_set_levels(&expander, 0x01, 0x01) == 0); + CHECK(expander.output_state == 0x01); + + g_fail_remove = true; + CHECK(mybot_tca9554_deinit(&expander) < 0); + CHECK(expander.device == &g_device); + CHECK(mybot_tca9554_deinit(&expander) == 0); + CHECK(!expander.device && !expander.bus && expander.output_state == 0); + CHECK(mybot_tca9554_deinit(&expander) == 0); + puts("TCA9554 masked update host tests passed"); + return 0; +}