Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
cardputer-adv
Board support for the M5Stack Cardputer ADV (ESP32-S3) on esp-hal 1.x.
Hardware
What the Cardputer ADV has, and what this crate covers so far.
| Part | Status | API |
|---|---|---|
| LCD, 1.14" ST7789V2, 240×135 | ✅ drawing directly, or a framebuffer sent by DMA; the controller's sleep | display, frame_display, Frame, FrameDisplay::flush, FrameDisplay::set_asleep |
| Backlight (GPIO38) | ✅ dimmable, 0–100% | Backlight::set_percent |
| Keyboard, 56 keys (TCA8418) | ✅ Shift and Fn layers, interrupt-driven, wakes the chip from light sleep | keyboard, keyboard_on, Keyboard::wait_for_inputs, Keyboard::listen_for_wake |
| Motion sensor (BMI270) | ✅ accelerometer, ±8 g at 100 Hz; gyroscope, ±2000 °/s at 100 Hz; ❌ motion interrupts | imu, Imu::acceleration, Imu::set_gyroscope, Imu::calibrate_gyroscope, Imu::rotation |
| Speaker (ES8311 codec, NS4150B amplifier) | ✅ playing samples at 44.1 kHz, volume, output off for sleep | speaker, Speaker::run, codec, Codec |
| Microphone (through the ES8311, I2S in on GPIO46) | ✅ recording at 44.1 kHz, alongside the speaker; preamp gain | audio, Audio::run, Codec::set_microphone, Codec::set_microphone_gain |
| Battery (GPIO10) | ✅ voltage and estimated charge; charging can't be detected (no line for it) | battery, Battery::millivolts, Battery::percent |
| Shared I2C bus (keyboard, codec, motion sensor) | ✅ | i2c |
| microSD slot (SPI: CS 12, MOSI 14, CLK 40, MISO 39) | ⚠️ FAT files through embedded-sdmmc, with the sd-card feature; not yet tested on hardware |
sd_card, SdCard |
| IR transmitter (GPIO44) | ✅ NEC codes on a 38 kHz carrier; ❌ other protocols | rmt, ir_transmitter, IrTransmitter::send_nec |
| RGB LED (WS2812 on GPIO21) | ✅ any colour; it's powered through GPIO38, so it's lit only while the backlight is | rmt, rgb_led, RgbLed::set_color |
| G0 button, after boot | ✅ debounced presses, wakes the chip from light sleep | button, Button::wait_for_press |
| Grove port (GPIO1, 2) and EXT header | not board-specific: use esp-hal directly |
API
display(DisplayPins)sets up the 1.14" ST7789V2 LCD and turns the backlight on. You get it back as a 240×135 RGB565 embedded-graphicsDrawTarget, in landscape with the keyboard below, and aBacklight. It draws straight to the panel, so each shape shows as it's drawn.Backlight::set_percentdims it with 256 Hz PWM on LEDC (as M5GFX does; the LED driver doesn't follow fast PWM); the RGB LED, on the same pin, dims too.frame_display(DisplayPins, dma)sets up the same LCD to be fed from aFrame: a 240×135 RGB565 framebuffer (64.8 kB, kept in a static) that you draw into with embedded-graphics.FrameDisplay::flush(&mut frame).awaitthen sends it by DMA, so other tasks run meanwhile.- Only pixels whose colour changed are sent: each run of changed rows goes as one rectangle, as wide as its changes. Redrawing the whole frame every time is fine.
- A whole frame takes 13 ms at 40 MHz. Drawing straight to the panel through
displaysends a frame in about 28 ms, converting pixel by pixel. FrameDisplay::set_asleep(true).awaitputs the panel's controller to sleep (a few µA instead of a few mA) with the picture kept in its memory;set_asleep(false)wakes it. Turn the backlight off first.
keyboard(KeyboardPins)sets up the 56-key TCA8418 keyboard on I2C, with Shift and Fn layers from cardputer-adv-keyboard. Fn +;.,/gives the arrow keys.Keyboard::wait_for_inputs().awaitsleeps until the TCA8418 pulls its interrupt line (GPIO11) low, then returns the keys pressed.- It waits for the low level, not an edge, so events that arrive while you're busy aren't lost.
- A 1 s fallback poll covers a missed interrupt;
missed_interrupts()counts how often that poll was needed. Keyboard::listen_for_wake(), just before esp-hal'sLowPower::sleep_light, makes a key press end the light sleep;wait_for_inputsthen reads the key.
imu(&I2cBus)finds the BMI270 motion sensor (at 0x68 or 0x69), uploads its configuration through bmi2 and turns on its accelerometer at ±8 g and 100 Hz.Imu::acceleration()reads it in milli-g.speaker(SpeakerPins)starts a 16-bit, 44.1 kHz I2S stream over DMA to the ES8311 codec, playing silence.codec(&I2cBus)then turns on the codec's DAC path to the NS4150B amplifier and returns aCodec, whoseset_volume(percent)sets its level.Speaker::run(fill).awaitkeeps the stream fed with the mono samples your closure writes.- The buffer holds 23 ms. If
fillfalls further behind, the stream restarts on silence, so the sound skips rather than stops. Codec::set_output(false)powers the codec's DAC and its path to the amplifier down, andset_output(true)brings them back. Turn it off before the I2S clock stops (in a light sleep): the codec clocks itself from the stream, and the clock stopping and starting clicks in the speaker.- To avoid skips while anything else holds the CPU for longer (drawing a frame straight to the panel takes about 30 ms), run the speaker on a higher-priority executor, such as esp-rtos's
InterruptExecutor.
- The buffer holds 23 ms. If
i2c(I2cPins)sets up the I2C bus that the keyboard, codec and motion sensor share. Pass it tokeyboard_on,imuandcodec.keyboard(KeyboardPins)sets the bus up itself, for when the keyboard is all you need.audio(AudioPins)starts the speaker's stream and the microphone's together, on the same I2S clocks, and returns anAudio. Oncecodechas set the codec up,Codec::set_microphone(true)turns its ADC on, andCodec::set_microphone_gain(db)sets its preamp (0–30 dB, in 3 dB steps).Audio::run(fill, take).awaitplays whatfillwrites and hands what's heard totake, up to 256 mono samples at a time; the two run together because the microphone runs on the speaker's clocks. A stream that falls more than 23 ms behind restarts, so its sound has a gap.Imu::set_gyroscope(true)turns the BMI270's gyroscope on (±2000 °/s at 100 Hz; it draws about 0.9 mA, soimuleaves it off), andImu::rotation()reads it in milli-degrees a second. Uncalibrated it can read several degrees a second at rest (from 0.5 to 9 °/s in different runs on the ADV tested);Imu::calibrate_gyroscope().await, with the Cardputer lying still for about 0.4 s, measures that offset and takes it off from then on.rmt(RMT)sets up the RMT peripheral for the RGB LED and the IR transmitter; give each one of its transmit channels.rgb_led(channel, GPIO21)returns anRgbLed, andRgbLed::set_color(Rgb888).awaitsets it (black is off). It's powered through the backlight's switch, so it's lit only while the backlight is, and dims with it.ir_transmitter(channel, GPIO44)returns anIrTransmitter, which sends NEC codes on a 38 kHz carrier:send_nec(address, command),send_nec_extendedfor a 16-bit address, andsend_nec_repeatfor a held button.button(GPIO0)returns the G0Button:is_pressed(),wait_for_press().await(debounced), andlisten_for_wake()to wake the chip from light sleep, asKeyboard::listen_for_wakedoes.sd_card(SdCardPins), with thesd-cardfeature, sets up the microSD card's own SPI bus (SPI3) and returns anSdCard, an embedded-sdmmcBlockDevice. Read and write FAT files throughembedded_sdmmc::VolumeManager; the crate re-exportsembedded_sdmmc, so the versions match. The card is started on first use (at 400 kHz, then run at 20 MHz), and again after a failed access, so it can be inserted or swapped while the app runs. It's blocking, so a long read holds up its executor.- Each part's pins come out of esp-hal's
Peripheralswith a macro named after it:display_pins!(p),i2c_pins!(p),keyboard_pins!(p),speaker_pins!(p),audio_pins!(p),battery_pins!(p)andsd_card_pins!(p). Only those fields move, so the rest ofpstays usable. The…Pinsstructs can still be written out by hand. battery(BatteryPins)measures the battery voltage (Battery::millivolts()) on GPIO10 through the board's 2:1 divider, as M5Stack's M5Unified does, andBattery::percent()the charge left, by M5Unified's estimate (3.30 V empty, 4.10 V full). The board can't tell whether it's charging: on USB with the power switch on, it reads the charging voltage.
use ;
use ;
let = display?;
let mut keyboard = keyboard?;
display.clear.expect; // mipidsi's own error type
loop
With the motion sensor and the speaker, set the shared bus up first:
use ;
let bus = i2c?;
let mut keyboard = keyboard_on?;
let mut imu = imu?;
let = imu.acceleration?; // milli-g
// In a task (ideally on a higher-priority executor):
let mut speaker = speaker?;
// …and once it's running (the codec clocks itself from the stream):
let mut codec = codec?;
codec.set_volume?;
let Err = speaker.run.await; // runs until it fails
The microphone, the RGB LED and the IR transmitter:
use audio_pins;
use ;
let mut audio = audio?;
let mut codec = codec?;
codec.set_microphone_gain?;
codec.set_microphone?;
// In a task: silence out, 44.1 kHz mono in.
let Err = audio.run.await;
let rmt = rmt?;
let mut led = rgb_led?;
let mut ir = ir_transmitter?;
led.set_color.await?;
ir.send_nec.await?;
With a framebuffer, sent by DMA:
use Frame;
use ConstStaticCell;
static FRAME: = new;
let = frame_display?;
let frame = FRAME.take;
loop
Requirements
- esp-hal 1.2, with its
unstablefeature (LEDC, I2S, RMT and light sleep are behind it), on an ESP32-S3. - The
sd-cardfeature, for the microSD card. It's off by default, so apps without a card don't build embedded-sdmmc. - The
defmtfeature, fordefmt::FormatonError, if your app logs with defmt. - An embassy-time driver, for the async parts (
Keyboard::wait_for_inputs,FrameDisplay::flush,Speaker::run): esp-rtos's, say. Displayis a mipidsi type; the crate re-exportsmipidsi, so you can name its types without depending on it yourself.
Sleeping while the screen is dark
The ESP32-S3 can light-sleep (about 1 mA) and carry on where it left off, woken by a key. In this order, with esp-hal's LowPower:
use ;
let mut low_power = new;
// Going dark:
backlight.set_percent?;
display.set_asleep.await?; // the panel controller sleeps, keeping its picture
codec.set_output?; // before the I2S clock stops, or the speaker clicks
// Each time round while dark:
keyboard.listen_for_wake; // a key press ends the sleep
low_power.set_wakeup_deadline;
low_power.sleep_light; // returns at the key, or the deadline
// …look at the motion sensor, say, then read keys with wait_for_inputs as usual.
// Lighting up again:
display.set_asleep.await?;
codec.set_output?;
backlight.set_percent?;
While the chip sleeps its USB is off (no log, no flashing until it wakes), and the embassy clock stops. The speaker's stream runs dry and restarts on silence by itself.
Pins
| Function | GPIO |
|---|---|
| LCD SCK / MOSI / CS / DC / RST | 36 / 35 / 37 / 34 / 33 |
| LCD backlight, PWM (also powers the RGB LED) | 38 |
| Battery voltage (ADC1, 2:1 divider) | 10 |
| I2C SDA / SCL (keyboard, ES8311 codec, BMI270 IMU) | 8 / 9 |
| Keyboard interrupt | 11 |
| I2S BCLK / WS / DOUT / DIN, the ES8311 (no MCLK: it clocks from BCLK) | 41 / 43 / 42 / 46 |
| G0 button | 0 |
| RGB LED (WS2812) | 21 |
| IR LED | 44 |
| microSD SCK / MOSI / MISO / CS (SPI3) | 40 / 14 / 39 / 12 |
The I2C bus is a critical_section::Mutex, shared by embedded-hal-bus's CriticalSectionDevices, so the keyboard, the codec and the motion sensor can be used from any executor or core. Each transfer holds the critical section, which delays interrupts by about 0.3 ms for a key or motion read. The motion sensor's start-up holds it for 3 ms per 128-byte burst.
Status
The display settings (panel offset 52/40, 90° rotation, colour inversion) come from working ADV firmware (iwan-inc/cardputer-rust). The display and the keyboard, including its interrupt line, work on a Cardputer ADV (tested October 2026). So do the motion sensor and the speaker. imu tries the BMI270's two addresses, 0x68 then 0x69, because sources disagree on which one the ADV uses (espp says 0x68, oxstack 0x69). The ES8311 sequence is M5Unified's Cardputer ADV speaker enable, as espp's driver uses it. The battery reading is new and not yet checked against a multimeter. The pins for the microphone, the microSD card, the IR LED, the RGB LED and G0 are M5Unified's for the ADV, and the microphone's codec sequence is its microphone enable. The microphone, the gyroscope, G0, the RGB LED and IR transmission work on the ADV too (tested October 2026; the IR LED's light itself not yet seen). The microSD card hasn't been tested on hardware yet. examples/hardware.rs (just hardware) tries each of these parts, showing its steps and readings on the screen.
Documentation
docs.rs can't build for Xtensa, so the API docs are on GitHub Pages instead, built by CI from main. To build them locally, run just doc.
Licence
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT licence (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 licence, shall be dual licensed as above, without any additional terms or conditions.