core-s3
Rust board support package for the M5Stack CoreS3 K128 (ESP32-S3) with optional support for an M5Stack Gateway H2 Thread/Zigbee co-processor.
The crate is intentionally #![no_std] and keeps the reusable BSP layer modular:
- board metadata and pin/device maps for CoreS3 peripherals
- crate-owned ILI9342C-compatible display bring-up
- lightweight
embedded-graphicswidgets and dirty-region helpers - FT6336U touch parsing and rotation-aware coordinate mapping
- AXP2101 power/battery helpers and AW9523B expander support
- BMI270/BMM150 motion/orientation helpers
- BM8563 RTC helpers with small
no_stddate/time types - ES7210/AW88298 audio configuration helpers
- feature-gated CoreS3 GC0308 camera metadata, SCCB setup, ESP-HAL LCD_CAM bring-up, and bounded DMA capture behind
camera - feature-gated Gateway H2 UART/framing/OpenThread transport surfaces and Spinel HDLC-lite codec behind
gateway-h2 - optional TF-card SD parts compatible with
embedded-sdmmc
Hardware note: v0.4.4 keeps the default ESP32-S3 path on the current stable downstream stack around
esp-hal = "=1.1.2". Display, touch, battery, motion, compass, RTC, audio, and Gateway H2 examples were smoke-tested during v0.3 development. The shared LCD/TF-card SPI API configures GPIO35 as SD MISO, handles the CoreS3 LCD D/C vs TF-card MISO mode switch inside the BSP, keeps TF-card CS asserted across CMD0 response polling for reliable pre-inserted-card acquisition, and restores SD/MISO-safe idle state after LCD transactions for cooperative LCD + SD use.
Peripheral support
| Peripheral | Address / pins | Support |
|---|---|---|
| ILI9342C SPI LCD | MOSI GPIO37, SCLK GPIO36, CS GPIO3, D/C GPIO35, TF CS GPIO4 held high | init, RGB565 drawing, clipping, rotation/MADCTL, dirty-region blits, shared-SPI initializer |
| FT6336U touch | I2C 0x38 on SDA GPIO12/SCL GPIO11 |
touch report parsing, down/up/move, gestures, rotation mapping, hit testing |
| AXP2101 PMIC | I2C 0x34 |
CoreS3 defaults, backlight rail, M5Unified-compatible battery SOC/status helpers, shutdown/sleep prep |
| AW9523B expander | I2C 0x58 |
CoreS3 defaults and safe output helpers, LCD reset pin helper |
| BMI270 IMU | I2C 0x69 |
init/config, accel/gyro raw reads, offsets, basic motion detection |
| BMM150 magnetometer | 0x10 on BMI270 auxiliary sensor-hub I2C |
generic register helper, hard-iron offset, integer heading helper; CoreS3 access path needs BMI270 sensor-hub validation |
| BM8563 RTC | I2C 0x51 |
get/set date-time, alarms, timer metadata |
| ES7210 microphone ADC | I2C 0x40, I2S GPIO0/34/33/13/14 |
configuration helper; I2S DMA remains app/HAL-owned |
| AW88298 speaker amp | I2C 0x36, I2S GPIO0/34/33/13/14 |
configuration helper; I2S DMA remains app/HAL-owned |
| Gateway H2 | UART1, TX GPIO1, RX GPIO2, 115200 baud | UART bring-up, small request/response/event framing, OpenThread/Spinel transport traits, and Spinel HDLC-lite codec |
| TF-card slot | SCLK GPIO36, MOSI GPIO37, MISO GPIO35, CS GPIO4 | slot metadata, card-detect helper, shared-SPI SpiDevice parts, optional embedded-sdmmc::SdCard conversion |
| GC0308 camera | XCLK GPIO2, PCLK GPIO45, VSYNC GPIO46, HREF GPIO38, D0..D7 GPIO39/40/41/42/15/16/48/47, SCCB GPIO12/GPIO11 | feature-gated camera API, sensor probe/config, QQVGA RGB565/grayscale metadata, ESP-HAL LCD_CAM DMA capture wrapper |
Repository layout
crates/core-s3/ no_std BSP crate
examples/hello_world/ basic LCD validation
examples/downstream_esp_hal_112/ downstream compatibility build for esp-hal 1.1.2
examples/dirty_regions/ dirty-region animation
examples/dual_core/ PRO CPU + APP CPU example
examples/gateway_h2/ Gateway H2 UART scaffold
examples/display_widgets/ widget rendering smoke test
examples/touch_demo/ FT6336U smoke-test shell
examples/battery_status/ AXP2101/battery smoke-test shell
examples/imu/ BMI270 smoke-test shell
examples/compass/ BMM150 smoke-test shell
examples/rtc/ BM8563 smoke-test shell
examples/audio_init/ ES7210/AW88298 smoke-test shell
examples/sd_card/ AW9523B TF-card detect demo
examples/sd_block_probe/ shared-SPI embedded-sdmmc capacity probe
examples/display_sd_coexist/ alternating LCD + raw SD read/write coexistence test
examples/camera_capture/ GC0308 live LCD preview and bounded DMA capture demo
examples/gateway_h2_transport/ H2 framing smoke-test shell
examples/full_board_demo/ board overview smoke-test shell
.github/workflows/ PR validation and firmware release automation
Features
| Feature | Description |
|---|---|
defmt |
Enables defmt formatting for supported dependency-free public types. |
camera |
Enables core_s3::camera and ESP-HAL-backed CoreS3 GC0308/LCD_CAM bring-up helpers. Implies esp-hal. |
esp-hal |
Enables ESP-HAL-backed CoreS3 bring-up helpers on Xtensa ESP32-S3 targets. |
gateway-h2 |
Exposes core_s3::gateway_h2, Gateway H2 metadata, UART bring-up, Matter/Thread config types, H2 framing, OpenThread/Spinel transport traits, and Spinel HDLC-lite encode/decode helpers. |
sdmmc |
Enables conversion from BSP SD parts into embedded_sdmmc::SdCard<SPI, DELAY>. |
Minimal display example
use ;
use ;
let mut parts = init_display?;
parts.display.clear?;
Label
.draw?;
Widgets and dirty-region sprite
core_s3::display::DirtySprite stores an off-screen framebuffer and tracks only changed rectangles. Drawing through embedded-graphics marks dirty regions automatically; flush_dirty / flush_dirty_at then blit only final pixels for changed regions into the real display target. Dirty rectangles are clipped, touching/intersecting rectangles merge deterministically, and capacity overflow collapses pending regions into one conservative bounding rectangle so a changed pixel can never lose its dirty marker. invalidate(area) and invalidate_all() support explicit repaint requests. Dirty state clears only after every target write succeeds, so failed flushes remain retryable.
core_s3::ui provides small reusable widgets without a GUI framework dependency:
LabelButtonToggleSliderProgressBarBatteryIndicatorStatusBarMenu
Shared LCD + TF-card SPI
CoreS3 routes the LCD and TF-card socket through the same SPI signal group:
SPI2
SCLK GPIO36
MOSI GPIO37
MISO GPIO35
LCD CS GPIO3
TF CS GPIO4
For firmware that needs both devices, use CoreS3::init_shared_spi, store the returned CoreS3SharedSpiParts in a static_cell::StaticCell, then create the LCD and SD chip-select devices independently with CoreS3::init_display_on_shared_spi and CoreS3::init_sd_on_shared_spi.
M5Stack's official CoreS3 PinMap lists LCD D/C on GPIO35 and TF-card MISO on the same GPIO35 pad. M5GFX's CoreS3 panel switches GPIO35 on LCD CS boundaries: LCD CS active routes GPIO35 as D/C output, while LCD CS inactive releases GPIO35 back to SPI MISO. The BSP follows that model with CoreS3-specific LCD and SD SpiDevice wrappers: LCD transactions force TF-card CS high, route GPIO35 as D/C output only while LCD CS is active, then restore LCD CS high, TF-card CS high, GPIO35 SD MISO/input, and SD-safe SPI settings before returning. SD transactions release GPIO35 as a pulled-up MISO input before TF-card CS is active.
For bounded high-throughput updates on the shared bus, CoreS3SharedDisplay::with_lcd_transaction(...) keeps one logical update inside one safe LCD/SD handoff. Its scoped transaction exposes validated blit_rgb565_be(area, bytes) writes and transfer statistics. The convenience CoreS3SharedDisplay::blit_rgb565_be(...) performs one address window and one logical session. Input must be exactly two bytes per pixel in row-major, big-endian RGB565 order, the area must be fully in bounds, and v0.5.1 requires landscape orientation. Validation occurs before LCD CS is asserted. Existing iterator and embedded-graphics APIs remain available. The v0.5.1 path was hardware-validated with the dirty-region animation/color pattern and 250 alternating LCD/SD read/write/readback cycles, including repeated reset, cold boot, and card remove/reinsert/reset.
For robust acquisition when a card is already inserted at flash/cold-boot/reset time, initialize and probe SD before LCD SPI traffic: create shared SPI and SD parts, initialize internal I2C, call CoreS3::init_core_s3_power(...), CoreS3::power_cycle_tf_card_rail(...), sd_parts.spi_device.prepare_for_card_acquire(), then call CoreS3SdParts::into_sdmmc() and SdCard::num_bytes(). After the SD probe, initialize the LCD with CoreS3::init_display_on_powered_shared_spi(...). Use examples/display_sd_coexist to validate alternating LCD updates and raw SD read/write/readback on hardware.
Downstream firmware can keep using embedded_hal::spi::SpiDevice and, with feature sdmmc, CoreS3SdParts::into_sdmmc() returns an embedded_sdmmc::SdCard<SPI, DELAY> suitable for a real num_bytes() capacity probe.
The BSP intentionally does not provide credential/token/secret abstractions, fake filesystems, plaintext storage policy, or encryption; downstream firmware should encrypt sensitive bytes before writing them to SD.
Camera
Enable feature camera for the CoreS3 GC0308 camera API. The BSP owns the CoreS3 camera pin map from M5Stack's UserDemo (XCLK GPIO2, SCCB on internal I²C GPIO12/GPIO11, PCLK GPIO45, VSYNC GPIO46, HREF GPIO38, and D0..D7 GPIO39/40/41/42/15/16/48/47), probes the GC0308 product ID, applies the GC0308 default register table, and configures the advertised output mode.
v0.5.0 intentionally supports only bounded low-memory raw modes until broader frame sizes are hardware-validated:
CameraConfig::qqvga_rgb565()—160x120, RGB565,QQVGA_RGB565_FRAME_BUFFER_BYTESCameraConfig::qr_grayscale()—160x120, luminance bytes,QR_GRAYSCALE_FRAME_BUFFER_BYTESDigitalZoom::{X1,X2,X4}— centered GC0308 sensor crop; preview code can scale the cropped frame on the LCD
ESP-HAL 1.1.x LCD_CAM capture requires a descriptor-backed DMA buffer, so CoreS3Camera::capture_dma_frame(...) accepts and returns esp_hal::dma::DmaRxBuf rather than a plain &mut [u8]. Use examples/camera_capture as the no-std live-preview template; it starts in sensor-crop DigitalZoom::X2, scales the cropped frame to the left-side LCD preview area, and exposes large right-side touch buttons for interactive 1x / 2x / 4x zoom changes. Camera use consumes GPIO2, which conflicts with treating Grove Port A pin 2 as an application-owned GPIO/UART/I²C pin while the camera is active.
Hardware validation for the v0.5.0 camera path was run on a real CoreS3 for serial DMA capture, live LCD preview, and interactive touch-controlled sensor-crop zoom.
Power / battery status
core_s3::power::Axp2101::battery_level_percent() reads AXP2101 register 0xA4, matching M5Unified's CoreS3 getBatteryLevel() behavior. Axp2101::status() prefers that gauge SOC when it returns 0..=100; if unavailable, the existing BatteryStatus::percentage falls back to a coarse voltage estimate and sets percentage_estimated = true with state_of_charge = None.
Charging state comes from AXP2101 register 0x01 bits 5:6. External power uses register 0x00 bit 0x20, and battery presence uses register 0x00 bit 0x08. CoreS3/AXP2101 does not expose battery current through this BSP path, so current-based coulomb counting is not available from AXP2101 alone.
Matter / Gateway H2 scope
The BSP does not implement Matter, Thread, Zigbee, OpenThread CLI, or the OpenThread state machine. M5Stack's Gateway H2 Thread Border Router documentation builds ESP-IDF's examples/openthread/ot_rcp firmware for the ESP32-H2 module, so core_s3::gateway_h2::spinel provides the bounded Spinel HDLC-lite byte-stuffing/FCS codec needed by downstream OpenThread host integrations. The downstream application still owns OpenThread host integration, Matter commissioning/runtime, and protocol policy.
Consumer firmware should own:
- Wi-Fi/IP networking
- Matter server/runtime such as
rs-matter - endpoints and clusters
- commissioning and persistence
- Thread/OpenThread/Spinel/Zigbee protocol integration
- Home Assistant behavior
Building
The default ESP32-S3 build is kept compatible with:
= "=1.1.2"
= "=0.15.0"
= "=0.17.0"
= "=0.1.4"
= "=0.10.0"
= "=1.0.0-beta.0"
= "=0.3.0"
= "=0.7.0"
= "1.0"
Install the ESP Rust toolchain with espup, then:
Flash an example with cargo-embed through the workspace runner:
Embed.toml is configured for ESP32-S3 JTAG with GDB disabled so cargo +esp run ... flashes and starts examples directly.
v0.5 migration notes
- Update dependencies from
core-s3 = "0.4"tocore-s3 = "0.5". - Enable feature
camerato use the CoreS3 GC0308 camera module andCoreS3::init_camera(...). - Camera v0.5.0 consumes the internal I²C bus for GC0308 SCCB during initialization/configuration and returns it through
CoreS3Camera::release_i2c()when the application is done with camera ownership. CameraConfig::qqvga_rgb565()andCameraConfig::qr_grayscale()are the supported bounded capture modes in v0.5.0;DigitalZoom::{X1,X2,X4}provides centered sensor-crop zoom. Larger base sizes/JPEG/custom windows are rejected until hardware-validated.
v0.4 migration notes
- Update dependencies from
core-s3 = "0.3"tocore-s3 = "0.4". - Prefer
core-s3 = "0.4.4"or newer for shared LCD + TF-card SPI: v0.4.4 keeps the v0.4.2 pre-inserted-card acquisition fixes and adds cooperative LCD/SD sharing so LCD transactions restore the SD/MISO-safe GPIO35/SPI2 idle state. - ESP-HAL users should pin to the supported
esp-hal = "=1.1.2"family unless they explicitly opt into and validate a newer stack in their application. - Existing
CoreS3::init_displayremains available for display-only firmware. - Firmware that needs both LCD and TF-card access should migrate to
CoreS3::init_shared_spi,CoreS3::init_sd_on_shared_spi,CoreS3::init_internal_i2c,CoreS3::power_cycle_tf_card_rail,CoreS3SharedSdDevice::prepare_for_card_acquire, andCoreS3::init_display_on_powered_shared_spiwhen SD must be acquired before LCD traffic. The olderCoreS3::init_display_on_shared_spiremains available for display-first code. - Use feature
sdmmcif you want BSP SD parts to convert directly intoembedded_sdmmc::SdCard. - Gateway H2 Matter/Thread config types remain configuration-only. Use
gateway_h2::transportfor H2 framing and OpenThread transport traits, and usegateway_h2::spinelfor the Spinel HDLC-lite codec when the H2 is flashed with ESP-IDF OpenThread RCP firmware. - Full Matter server/runtime code still belongs in consumer applications, not this BSP.
v0.3 migration notes
- Update dependencies from
core-s3 = "0.2"tocore-s3 = "0.3". BatteryStatusnow includes percentage estimate, charge state, external-power state, and low-battery state. Code using the old{ millivolts, state }fields should migrate tocharge_stateand the richer status fields.
Unsupported / application-owned functionality
- Camera support is feature-gated and currently limited to the CoreS3 GC0308 QQVGA raw modes documented above.
- High-throughput I2S DMA capture/playback is application/HAL-owned.
- Matter, Thread, Zigbee, OpenThread, and Spinel protocol stacks are application-owned.
- Voltage-based battery percentage is approximate and should not be used as a precise fuel gauge.