From e1a65f2933324aa8615b4464d2d38c41760a1859 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 3 Nov 2026 12:05:09 -0600 Subject: [PATCH 1/4] feat(m5stack-tab5): Kconfig options to skip audio recording and DPI DMA2D Two opt-outs for applications that are short of internal DMA-capable RAM (the SD card driver needs some of it for unaligned transfers): - CONFIG_M5STACK_TAB5_AUDIO_RECORDING (default y): when disabled, initialize_audio() creates the I2S TX channel alone, with no RX channel (whose DMA ring matches the 12 x 240-frame playback ring), no ES7210 setup and no microphone task (whose stack is in internal RAM). start_audio_recording() then fails. - CONFIG_M5STACK_TAB5_DPI_DMA2D (default y): when disabled, the DPI panel keeps CPU draw_bitmap() copies instead of enabling DMA2D. Rotated LVGL flushes go through the PPA straight into the frame buffer, so an application that always rotates never uses it. Both defaults keep the current behavior. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- components/m5stack-tab5/Kconfig | 27 +++ components/m5stack-tab5/README.md | 17 ++ .../m5stack-tab5/include/m5stack-tab5.hpp | 6 +- components/m5stack-tab5/src/audio.cpp | 171 +++++++++++------- components/m5stack-tab5/src/video.cpp | 22 ++- 5 files changed, 169 insertions(+), 74 deletions(-) diff --git a/components/m5stack-tab5/Kconfig b/components/m5stack-tab5/Kconfig index 67f02663fa..7678de7673 100644 --- a/components/m5stack-tab5/Kconfig +++ b/components/m5stack-tab5/Kconfig @@ -27,6 +27,18 @@ menu "M5Stack Tab5 Configuration" help Size of the stack used for the audio processing task. + config M5STACK_TAB5_AUDIO_RECORDING + bool "Enable audio recording (ES7210 microphones)" + default y + help + Set up the I2S RX channel, the ES7210 microphone codec and the + microphone task in initialize_audio(), for start_audio_recording(). + The RX channel's DMA ring is the same size as the playback ring (about + 11.5 KB of internal DMA-capable RAM), and the microphone task's stack + is in internal RAM too. Disable it in an application that only plays + audio to get that memory back: initialize_audio() then creates the TX + channel alone, and start_audio_recording() fails. + config M5STACK_TAB5_ST7121_HW_ROTATION bool "ST7121: apply 0/180 rotation in the panel (EXPERIMENTAL)" default n @@ -56,6 +68,21 @@ menu "M5Stack Tab5 Configuration" PPA-only, and a rotated flush with no PPA available is dropped with an error instead of being rotated in software. + config M5STACK_TAB5_DPI_DMA2D + bool "Copy draw_bitmap() areas into the frame buffer with DMA2D" + default y + help + Let the DPI panel copy esp_lcd_panel_draw_bitmap() areas into its + frame buffer with the DMA2D engine (on ESP-IDF 6.0 and later only on + the ST7123 / ST7121 panels: the ILI9881 panel keeps CPU copies). It + takes the PSRAM traffic of those copies off the CPU, which can starve + the panel's scan-out DMA, but its descriptors cost about 5 KB of + internal DMA-capable RAM. Only unrotated LVGL flushes, + write_lcd_lines() and the software rotation fallback use draw_bitmap(): + rotated flushes go through the PPA straight into the frame buffer. An + application that always rotates (e.g. landscape) can disable this to + get that memory back. + config M5STACK_TAB5_DPI_NUM_FBS int "Number of DPI frame buffers" default 1 diff --git a/components/m5stack-tab5/README.md b/components/m5stack-tab5/README.md index 62cfa80e8c..b5c821a28b 100644 --- a/components/m5stack-tab5/README.md +++ b/components/m5stack-tab5/README.md @@ -149,6 +149,23 @@ the 1.8 MB rotation scratch buffer is not needed either). The frame buffers are cache-line aligned, so they also satisfy a raised `LV_DRAW_BUF_ALIGN` (e.g. 128 for the PPA draw unit), which a plain `heap_caps_malloc()` draw buffer may not. +## Saving internal DMA-capable RAM + +Two options in `menuconfig` (M5Stack Tab5 Configuration) give back internal +DMA-capable RAM that an application may not need (the SD card driver, for +one, needs some of it for unaligned transfers): + +- `CONFIG_M5STACK_TAB5_AUDIO_RECORDING` (default on): with it off, + `initialize_audio()` sets up playback only. There is no I2S RX channel + (whose DMA ring matches the playback ring, about 11.5 KB), no ES7210 + setup and no microphone task (whose stack is in internal RAM too), and + `start_audio_recording()` fails. +- `CONFIG_M5STACK_TAB5_DPI_DMA2D` (default on): with it off, + `esp_lcd_panel_draw_bitmap()` copies into the frame buffer with the CPU + instead of the DMA2D engine, which saves about 5 KB. Rotated LVGL flushes + do not use `draw_bitmap()` (the PPA rotates them straight into the frame + buffer), so an application that always rotates loses nothing. + ## Example The [example](./example) shows how to use the `espp::M5StackTab5` hardware abstraction component to initialize and use various subsystems of the Tab5. diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index 5d98880402..c519a83c91 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -449,6 +449,9 @@ class M5StackTab5 : public BaseComponent { ///////////////////////////////////////////////////////////////////////////// /// Initialize the dual audio system (ES8388 codec + ES7210 AEC) + /// \note With CONFIG_M5STACK_TAB5_AUDIO_RECORDING disabled only playback is + /// set up (no I2S RX channel, ES7210 or microphone task), to save + /// internal RAM in applications that never record. /// \param sample_rate The audio sample rate (default 48kHz) /// \param task_config The task configuration for the audio task /// \return true if the audio system was successfully initialized, false otherwise @@ -516,7 +519,8 @@ class M5StackTab5 : public BaseComponent { /// \param callback Function to call with recorded audio data: 16-bit /// signed interleaved stereo (ES7210 microphone 1 on the left slot, /// microphone 2 on the right) at audio_sample_rate() - /// \return True if recording started successfully + /// \return True if recording started successfully (false, among other + /// reasons, when CONFIG_M5STACK_TAB5_AUDIO_RECORDING is disabled) /// \note The callback runs in the dedicated microphone task's context (a /// separate task from audio playback), so keep it quick and non- /// blocking and size that task's stack accordingly diff --git a/components/m5stack-tab5/src/audio.cpp b/components/m5stack-tab5/src/audio.cpp index ce2b437fb1..4c0657fbbe 100644 --- a/components/m5stack-tab5/src/audio.cpp +++ b/components/m5stack-tab5/src/audio.cpp @@ -50,6 +50,14 @@ static audio_hal_iface_samples_t audio_hal_samples_from_rate(uint32_t rate) { // every audio BSP: see codec/i2s_tx_feed.hpp for the mechanism and why it // matters. This file keeps only what is specific to the Tab5. +// Whether initialize_audio() sets up recording (the I2S RX channel, the ES7210 +// and the microphone task): CONFIG_M5STACK_TAB5_AUDIO_RECORDING. +#if defined(CONFIG_M5STACK_TAB5_AUDIO_RECORDING) +static constexpr bool kAudioRecordingEnabled = true; +#else +static constexpr bool kAudioRecordingEnabled = false; +#endif + bool M5StackTab5::initialize_audio(uint32_t sample_rate, const espp::Task::BaseConfig &task_config) { logger_.info("Initializing dual audio system (ES8388 + ES7210) at {} Hz", sample_rate); @@ -72,7 +80,11 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, "the I2S TX DMA ring must outlast the whole-block wait plus one block"); chan_cfg.dma_desc_num = espp::i2s_tx_feed::DMA_DESC_NUM; chan_cfg.dma_frame_num = espp::i2s_tx_feed::DMA_FRAME_NUM; - ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &audio_tx_handle, &audio_rx_handle)); + // Both channels come from the one call so they run full duplex (RX shares the + // TX BCLK/WS), and each gets a DMA ring of this size. Without recording only + // the TX channel is created, which saves the RX ring's internal DMA memory. + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &audio_tx_handle, + kAudioRecordingEnabled ? &audio_rx_handle : nullptr)); // Configure I2S for stereo output (needed for proper ES8388 operation). The // playback data is interleaved 16-bit stereo, so the slot mode MUST be stereo @@ -97,8 +109,12 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, // the TX BCLK/WS, so the RX channel must use the same standard-mode // configuration - a different frame geometry (e.g. multi-slot TDM) would // not match the shared clock. - logger_.info("Configuring I2S channel for recording (RX)"); - ESP_ERROR_CHECK(i2s_channel_init_std_mode(audio_rx_handle, &audio_std_cfg)); + if (kAudioRecordingEnabled) { + logger_.info("Configuring I2S channel for recording (RX)"); + ESP_ERROR_CHECK(i2s_channel_init_std_mode(audio_rx_handle, &audio_std_cfg)); + } else { + logger_.info("Recording disabled (CONFIG_M5STACK_TAB5_AUDIO_RECORDING): no RX channel"); + } // Stream buffers and task auto tx_buf_size = calc_audio_buffer_size(sample_rate); @@ -115,11 +131,15 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, audio_tx_feed_.stream = audio_tx_stream; audio_tx_feed_.frame_bytes = NUM_CHANNELS * NUM_BYTES_PER_CHANNEL; // RX buffer for recording - audio_rx_buffer.resize(tx_buf_size); + if (kAudioRecordingEnabled) { + audio_rx_buffer.resize(tx_buf_size); + } // now enable both channels ESP_ERROR_CHECK(i2s_channel_enable(audio_tx_handle)); - ESP_ERROR_CHECK(i2s_channel_enable(audio_rx_handle)); + if (kAudioRecordingEnabled) { + ESP_ERROR_CHECK(i2s_channel_enable(audio_rx_handle)); + } std::error_code ec; es8388_i2c_device_ = internal_i2c_.add_device( @@ -134,24 +154,28 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, logger_.error("Could not initialize ES8388 I2C device: {}", ec.message()); return false; } - es7210_i2c_device_ = internal_i2c_.add_device( - { - .device_address = 0x40, - .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), - .scl_speed_hz = internal_i2c_.config().clk_speed, - .log_level = espp::Logger::Verbosity::WARN, - }, - ec); - if (!es7210_i2c_device_) { - logger_.error("Could not initialize ES7210 I2C device: {}", ec.message()); - return false; + if (kAudioRecordingEnabled) { + es7210_i2c_device_ = internal_i2c_.add_device( + { + .device_address = 0x40, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!es7210_i2c_device_) { + logger_.error("Could not initialize ES7210 I2C device: {}", ec.message()); + return false; + } } // Wire codec register access over internal I2C set_es8388_write(espp::make_i2c_addressed_write(es8388_i2c_device_)); set_es8388_read(espp::make_i2c_addressed_read_register(es8388_i2c_device_)); - set_es7210_write(espp::make_i2c_addressed_write(es7210_i2c_device_)); - set_es7210_read(espp::make_i2c_addressed_read_register(es7210_i2c_device_)); + if (kAudioRecordingEnabled) { + set_es7210_write(espp::make_i2c_addressed_write(es7210_i2c_device_)); + set_es7210_read(espp::make_i2c_addressed_read_register(es7210_i2c_device_)); + } // ES8388 DAC playback config audio_hal_codec_config_t es8388_cfg{}; @@ -184,49 +208,51 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, es8388_set_voice_mute(false); // Make sure it's not muted es8388_ctrl_state(AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START); - // ES7210 ADC recording config - audio_hal_codec_config_t es7210_cfg{}; - es7210_cfg.codec_mode = AUDIO_HAL_CODEC_MODE_ENCODE; - es7210_cfg.i2s_iface.bits = AUDIO_HAL_BIT_LENGTH_16BITS; - es7210_cfg.i2s_iface.fmt = AUDIO_HAL_I2S_NORMAL; - es7210_cfg.i2s_iface.mode = AUDIO_HAL_MODE_SLAVE; - // configure the ES7210 for the rate the I2S clocks are actually running at - // (rather than assuming 48 kHz) so its coefficient table matches - es7210_cfg.i2s_iface.samples = audio_hal_samples_from_rate(sample_rate); - logger_.info("Initializing ES7210 codec for recording (ADC) at {} Hz", sample_rate); - if (es7210_adc_init(&es7210_cfg) != ESP_OK) { - logger_.error("ES7210 init failed"); - return false; - } - if (es7210_adc_config_i2s(AUDIO_HAL_CODEC_MODE_ENCODE, &es7210_cfg.i2s_iface) != ESP_OK) { - logger_.error("ES7210 I2S cfg failed"); - } - // Enable the ES7210 digital high-pass filter on the ADC channels to strip - // the microphone DC offset. The espp es7210 driver leaves these registers - // at their reset value (HPF off), so the DC offset is amplified by the - // analog gain and rails the ADC to full scale - recordings come out as a - // near-constant DC level that the AC-coupled speaker plays as a click then - // silence. These are the driver's documented "quick setup" HPF values. - // write_register clears the error code on success, so a later successful - // write would mask an earlier failure; remember the first failure explicitly. - std::error_code hpf_ec, first_hpf_ec; - auto write_hpf = [&](uint8_t reg, uint8_t val) { - es7210_i2c_device_->write_register(reg, std::vector{val}, hpf_ec); - if (hpf_ec && !first_hpf_ec) { - first_hpf_ec = hpf_ec; + if (kAudioRecordingEnabled) { + // ES7210 ADC recording config + audio_hal_codec_config_t es7210_cfg{}; + es7210_cfg.codec_mode = AUDIO_HAL_CODEC_MODE_ENCODE; + es7210_cfg.i2s_iface.bits = AUDIO_HAL_BIT_LENGTH_16BITS; + es7210_cfg.i2s_iface.fmt = AUDIO_HAL_I2S_NORMAL; + es7210_cfg.i2s_iface.mode = AUDIO_HAL_MODE_SLAVE; + // configure the ES7210 for the rate the I2S clocks are actually running at + // (rather than assuming 48 kHz) so its coefficient table matches + es7210_cfg.i2s_iface.samples = audio_hal_samples_from_rate(sample_rate); + logger_.info("Initializing ES7210 codec for recording (ADC) at {} Hz", sample_rate); + if (es7210_adc_init(&es7210_cfg) != ESP_OK) { + logger_.error("ES7210 init failed"); + return false; } - }; - write_hpf(0x22, 0x0a); // ADC1/2 HPF1 - write_hpf(0x23, 0x2a); // ADC1/2 HPF2 - write_hpf(0x20, 0x0a); // ADC3/4 HPF2 - write_hpf(0x21, 0x2a); // ADC3/4 HPF1 - if (first_hpf_ec) { - logger_.warn("Could not enable the ES7210 high-pass filter: {}", first_hpf_ec.message()); + if (es7210_adc_config_i2s(AUDIO_HAL_CODEC_MODE_ENCODE, &es7210_cfg.i2s_iface) != ESP_OK) { + logger_.error("ES7210 I2S cfg failed"); + } + // Enable the ES7210 digital high-pass filter on the ADC channels to strip + // the microphone DC offset. The espp es7210 driver leaves these registers + // at their reset value (HPF off), so the DC offset is amplified by the + // analog gain and rails the ADC to full scale - recordings come out as a + // near-constant DC level that the AC-coupled speaker plays as a click then + // silence. These are the driver's documented "quick setup" HPF values. + // write_register clears the error code on success, so a later successful + // write would mask an earlier failure; remember the first failure explicitly. + std::error_code hpf_ec, first_hpf_ec; + auto write_hpf = [&](uint8_t reg, uint8_t val) { + es7210_i2c_device_->write_register(reg, std::vector{val}, hpf_ec); + if (hpf_ec && !first_hpf_ec) { + first_hpf_ec = hpf_ec; + } + }; + write_hpf(0x22, 0x0a); // ADC1/2 HPF1 + write_hpf(0x23, 0x2a); // ADC1/2 HPF2 + write_hpf(0x20, 0x0a); // ADC3/4 HPF2 + write_hpf(0x21, 0x2a); // ADC3/4 HPF1 + if (first_hpf_ec) { + logger_.warn("Could not enable the ES7210 high-pass filter: {}", first_hpf_ec.message()); + } + // apply the stored microphone volume (the driver's init leaves the analog + // gain at its 0 dB minimum, which records very quietly) + es7210_adc_set_gain_all(microphone_gain_from_volume(mic_volume_)); + es7210_adc_ctrl_state(AUDIO_HAL_CODEC_MODE_ENCODE, AUDIO_HAL_CTRL_START); } - // apply the stored microphone volume (the driver's init leaves the analog - // gain at its 0 dB minimum, which records very quietly) - es7210_adc_set_gain_all(microphone_gain_from_volume(mic_volume_)); - es7210_adc_ctrl_state(AUDIO_HAL_CODEC_MODE_ENCODE, AUDIO_HAL_CTRL_START); // Create two independent tasks, matching the (well-behaved) esp-box audio // path: one drains the playback stream buffer to the TX channel, and a @@ -235,8 +261,9 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, // exactly the rate samples arrive (rather than only between blocking TX // writes), which the shared-task approach did irregularly - on this // full-duplex controller that jitter degraded the recording and let the ring - // back up. - logger_.info("Creating audio playback and microphone tasks"); + // back up. Without recording there is no RX channel to read, so no + // microphone task either. + logger_.info("Creating audio playback{} task", kAudioRecordingEnabled ? " and microphone" : ""); using namespace std::placeholders; audio_task_ = espp::Task::make_unique( {.callback = std::bind(&M5StackTab5::audio_task_callback, this, _1, _2, _3), @@ -244,12 +271,14 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, // give the microphone task a distinct name from the playback task (they share // the rest of the caller's task config) so the two are easy to tell apart in // logs and while debugging - auto mic_task_config = task_config; - mic_task_config.name = - task_config.name.empty() ? std::string("microphone") : task_config.name + " microphone"; - microphone_task_ = espp::Task::make_unique( - {.callback = std::bind(&M5StackTab5::microphone_task_callback, this, _1, _2, _3), - .task_config = mic_task_config}); + if (kAudioRecordingEnabled) { + auto mic_task_config = task_config; + mic_task_config.name = + task_config.name.empty() ? std::string("microphone") : task_config.name + " microphone"; + microphone_task_ = espp::Task::make_unique( + {.callback = std::bind(&M5StackTab5::microphone_task_callback, this, _1, _2, _3), + .task_config = mic_task_config}); + } // Start the tasks stepwise so a failure does not leave a half-initialized // state: if the microphone task fails to start, stop the already-running @@ -259,7 +288,7 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, logger_.error("Failed to start the audio playback task"); return false; } - if (!microphone_task_->start()) { + if (microphone_task_ && !microphone_task_->start()) { logger_.error("Failed to start the microphone task"); // the playback task may already have registered its handle for // play_audio()'s notifications: forget it before the task goes away @@ -331,7 +360,7 @@ size_t M5StackTab5::play_audio(std::span data) { void M5StackTab5::microphone_volume(float volume) { mic_volume_ = std::clamp(volume, 0.0f, 100.0f); - if (audio_initialized_) { + if (kAudioRecordingEnabled && audio_initialized_) { es7210_adc_set_gain_all(microphone_gain_from_volume(mic_volume_)); } } @@ -340,6 +369,10 @@ float M5StackTab5::microphone_volume() const { return mic_volume_; } bool M5StackTab5::start_audio_recording( std::function callback) { + if (!kAudioRecordingEnabled) { + logger_.error("Audio recording is disabled (CONFIG_M5STACK_TAB5_AUDIO_RECORDING)"); + return false; + } if (!audio_initialized_) { logger_.error("Audio system not initialized"); return false; diff --git a/components/m5stack-tab5/src/video.cpp b/components/m5stack-tab5/src/video.cpp index fe97e069dc..d4bd45c85b 100644 --- a/components/m5stack-tab5/src/video.cpp +++ b/components/m5stack-tab5/src/video.cpp @@ -64,6 +64,14 @@ static constexpr uint8_t kNumDpiFramebuffers = CONFIG_M5STACK_TAB5_DPI_NUM_FBS; static_assert(kNumDpiFramebuffers >= 1 && kNumDpiFramebuffers <= M5StackTab5::max_frame_buffers, "CONFIG_M5STACK_TAB5_DPI_NUM_FBS must be 1..3 (the DPI panel driver's limit)"); +// Whether draw_bitmap() copies into the frame buffer may use the DMA2D engine +// (CONFIG_M5STACK_TAB5_DPI_DMA2D; per controller, see initialize_lcd()). +#if defined(CONFIG_M5STACK_TAB5_DPI_DMA2D) +static constexpr bool kDpiDma2dEnabled = true; +#else +static constexpr bool kDpiDma2dEnabled = false; +#endif + // Whether this ESP-IDF's DPI panel raises its frame-done event from the DMA // transfer-done interrupt after re-arming on the queued frame buffer // (on_frame_buf_complete: v5.5.5+, v6.0.3+). Earlier versions raise @@ -335,7 +343,7 @@ bool M5StackTab5::initialize_lcd() { dpi_cfg.video_timing.vsync_pulse_width = 4; dpi_cfg.video_timing.vsync_front_porch = 20; #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.flags.use_dma2d = true; + dpi_cfg.flags.use_dma2d = kDpiDma2dEnabled; #endif } else if (detected_controller == DisplayController::ST7123 && lcd_handles_.panel == nullptr) { @@ -357,7 +365,7 @@ bool M5StackTab5::initialize_lcd() { dpi_cfg.video_timing.vsync_pulse_width = 2; dpi_cfg.video_timing.vsync_front_porch = 220; #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.flags.use_dma2d = true; + dpi_cfg.flags.use_dma2d = kDpiDma2dEnabled; #endif } else if (detected_controller == DisplayController::ST7121 && lcd_handles_.panel == nullptr) { dpi_cfg.virtual_channel = 0; @@ -376,7 +384,7 @@ bool M5StackTab5::initialize_lcd() { dpi_cfg.video_timing.vsync_pulse_width = 20; dpi_cfg.video_timing.vsync_front_porch = 200; #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.flags.use_dma2d = true; + dpi_cfg.flags.use_dma2d = kDpiDma2dEnabled; #endif } @@ -571,6 +579,10 @@ bool M5StackTab5::initialize_lcd() { // M5Stack's own Tab5 demo firmware ships with DMA2D enabled on production // units that are predominantly ST71xx. // + // CONFIG_M5STACK_TAB5_DPI_DMA2D turns it off altogether, for applications + // that never draw_bitmap() (every flush rotated by the PPA) and would rather + // have back the internal DMA-capable RAM its descriptors take. + // // ESP-IDF v6.0.0 only: its DPI draw_bitmap() checks the DMA2D hook before the // "pointer is inside a frame buffer" test (fixed in v6.0.1), so with DMA2D on // and 2+ frame buffers a frame-buffer switch would become a full-frame copy @@ -580,7 +592,9 @@ bool M5StackTab5::initialize_lcd() { #else constexpr bool dma2d_breaks_fb_switch = false; #endif - if constexpr (dma2d_breaks_fb_switch) { + if constexpr (!kDpiDma2dEnabled) { + logger_.info("DMA2D disabled (CONFIG_M5STACK_TAB5_DPI_DMA2D): keeping CPU draw_bitmap copies"); + } else if constexpr (dma2d_breaks_fb_switch) { logger_.info("ESP-IDF v6.0.0 with {} frame buffers: keeping CPU draw_bitmap copies (DMA2D " "would turn a frame-buffer switch into a copy)", kNumDpiFramebuffers); From 192995734102675c848ddc87bdeb48f8b4db7fac Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 3 Nov 2026 12:52:35 -0600 Subject: [PATCH 2/4] refactor(m5stack-tab5): make recording, playback and DMA2D runtime options Replace the two Kconfig options with parameters, following the DisplayConfig / initialize_display(config) pattern: - initialize_audio(const AudioConfig &): sample_rate, playback_enabled, recording_enabled and task_config. Each direction is set up only when enabled (its I2S channel and DMA ring, its codec and its task); alone, either channel runs as a simplex master. initialize_audio(sample_rate, task_config) keeps its signature and enables both. - initialize_lcd(const LcdConfig &): dma2d_enabled. initialize_lcd() keeps the current behavior. play_audio(), volume() and mute() do nothing for the ES8388 without playback, start_audio_recording() fails without recording, and the sample rate setter reconfigures whichever channel drives the clocks. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- components/m5stack-tab5/Kconfig | 27 -- components/m5stack-tab5/README.md | 35 ++- .../m5stack-tab5/include/m5stack-tab5.hpp | 63 ++++- components/m5stack-tab5/src/audio.cpp | 242 ++++++++++-------- components/m5stack-tab5/src/video.cpp | 26 +- 5 files changed, 229 insertions(+), 164 deletions(-) diff --git a/components/m5stack-tab5/Kconfig b/components/m5stack-tab5/Kconfig index 7678de7673..67f02663fa 100644 --- a/components/m5stack-tab5/Kconfig +++ b/components/m5stack-tab5/Kconfig @@ -27,18 +27,6 @@ menu "M5Stack Tab5 Configuration" help Size of the stack used for the audio processing task. - config M5STACK_TAB5_AUDIO_RECORDING - bool "Enable audio recording (ES7210 microphones)" - default y - help - Set up the I2S RX channel, the ES7210 microphone codec and the - microphone task in initialize_audio(), for start_audio_recording(). - The RX channel's DMA ring is the same size as the playback ring (about - 11.5 KB of internal DMA-capable RAM), and the microphone task's stack - is in internal RAM too. Disable it in an application that only plays - audio to get that memory back: initialize_audio() then creates the TX - channel alone, and start_audio_recording() fails. - config M5STACK_TAB5_ST7121_HW_ROTATION bool "ST7121: apply 0/180 rotation in the panel (EXPERIMENTAL)" default n @@ -68,21 +56,6 @@ menu "M5Stack Tab5 Configuration" PPA-only, and a rotated flush with no PPA available is dropped with an error instead of being rotated in software. - config M5STACK_TAB5_DPI_DMA2D - bool "Copy draw_bitmap() areas into the frame buffer with DMA2D" - default y - help - Let the DPI panel copy esp_lcd_panel_draw_bitmap() areas into its - frame buffer with the DMA2D engine (on ESP-IDF 6.0 and later only on - the ST7123 / ST7121 panels: the ILI9881 panel keeps CPU copies). It - takes the PSRAM traffic of those copies off the CPU, which can starve - the panel's scan-out DMA, but its descriptors cost about 5 KB of - internal DMA-capable RAM. Only unrotated LVGL flushes, - write_lcd_lines() and the software rotation fallback use draw_bitmap(): - rotated flushes go through the PPA straight into the frame buffer. An - application that always rotates (e.g. landscape) can disable this to - get that memory back. - config M5STACK_TAB5_DPI_NUM_FBS int "Number of DPI frame buffers" default 1 diff --git a/components/m5stack-tab5/README.md b/components/m5stack-tab5/README.md index b5c821a28b..82bdcc0830 100644 --- a/components/m5stack-tab5/README.md +++ b/components/m5stack-tab5/README.md @@ -151,20 +151,27 @@ for the PPA draw unit), which a plain `heap_caps_malloc()` draw buffer may not. ## Saving internal DMA-capable RAM -Two options in `menuconfig` (M5Stack Tab5 Configuration) give back internal -DMA-capable RAM that an application may not need (the SD card driver, for -one, needs some of it for unaligned transfers): - -- `CONFIG_M5STACK_TAB5_AUDIO_RECORDING` (default on): with it off, - `initialize_audio()` sets up playback only. There is no I2S RX channel - (whose DMA ring matches the playback ring, about 11.5 KB), no ES7210 - setup and no microphone task (whose stack is in internal RAM too), and - `start_audio_recording()` fails. -- `CONFIG_M5STACK_TAB5_DPI_DMA2D` (default on): with it off, - `esp_lcd_panel_draw_bitmap()` copies into the frame buffer with the CPU - instead of the DMA2D engine, which saves about 5 KB. Rotated LVGL flushes - do not use `draw_bitmap()` (the PPA rotates them straight into the frame - buffer), so an application that always rotates loses nothing. +Applications that are short of internal DMA-capable RAM can skip the parts +they don't use. The SD card driver, for example, needs some of that RAM for +unaligned transfers. + +- `initialize_audio({.playback_enabled = ..., .recording_enabled = ...})` + sets up only the directions it is asked for. Each I2S channel has a DMA + ring of about 11.5 KB and its own task, whose stack is in internal RAM. + Without recording there is no ES7210 setup and `start_audio_recording()` + fails. Without playback there is no ES8388 setup and `play_audio()` queues + nothing. +- `initialize_lcd({.dma2d_enabled = false})` keeps CPU copies for + `esp_lcd_panel_draw_bitmap()` instead of the DMA2D engine, which saves a + few KB. Rotated LVGL flushes don't use `draw_bitmap()` (the PPA rotates + them straight into the frame buffer), so an application that always + rotates loses nothing. + +```cpp +auto &tab5 = espp::M5StackTab5::get(); +tab5.initialize_lcd({.dma2d_enabled = false}); // always rotated +tab5.initialize_audio({.recording_enabled = false}); // playback only +``` ## Example diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index c519a83c91..ece4f438f6 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -210,10 +210,30 @@ class M5StackTab5 : public BaseComponent { // Display & Touchpad ///////////////////////////////////////////////////////////////////////////// + /// How initialize_lcd() sets the panel up + struct LcdConfig { + /// Let the DPI panel copy esp_lcd_panel_draw_bitmap() areas into its + /// frame buffer with the DMA2D engine instead of the CPU (on ESP-IDF 6.0 + /// and later only on the ST7123 / ST7121 panels: the ILI9881 panel keeps + /// CPU copies). That takes the copies' PSRAM traffic off the CPU, which + /// otherwise can starve the panel's scan-out DMA, but DMA2D costs a few + /// KB of internal DMA-capable RAM. Only unrotated LVGL flushes, + /// write_lcd_lines() and the software rotation fallback use + /// draw_bitmap(): rotated flushes go through the PPA straight into the + /// frame buffer, so an application that always rotates (e.g. landscape) + /// can turn this off and lose nothing. + bool dma2d_enabled{true}; + }; + /// Initialize the LCD (low level display driver, MIPI-DSI + ST7703) /// \return true if the LCD was successfully initialized, false otherwise bool initialize_lcd(); + /// Initialize the LCD (low level display driver, MIPI-DSI + ST7703) + /// \param config How to set the panel up + /// \return true if the LCD was successfully initialized, false otherwise + bool initialize_lcd(const LcdConfig &config); + /// How initialize_display() sets LVGL up struct DisplayConfig { /// Size of each of LVGL's two draw buffers, in pixels. They are allocated @@ -448,10 +468,31 @@ class M5StackTab5 : public BaseComponent { // Audio System ///////////////////////////////////////////////////////////////////////////// - /// Initialize the dual audio system (ES8388 codec + ES7210 AEC) - /// \note With CONFIG_M5STACK_TAB5_AUDIO_RECORDING disabled only playback is - /// set up (no I2S RX channel, ES7210 or microphone task), to save - /// internal RAM in applications that never record. + /// How initialize_audio() sets the audio system up + struct AudioConfig { + /// The audio sample rate, in Hz + uint32_t sample_rate{48000}; + /// Set up playback: the I2S TX channel, the ES8388 DAC, the speaker and + /// the playback task. Without it play_audio() queues nothing. + bool playback_enabled{true}; + /// Set up recording: the I2S RX channel, the ES7210 microphones and the + /// microphone task. Without it start_audio_recording() fails. + /// \note Each I2S channel has a DMA ring of about 11.5 KB of internal + /// DMA-capable RAM, and each task's stack is in internal RAM too, + /// so an application that only plays (or only records) saves that + /// memory by turning the other direction off. + bool recording_enabled{true}; + /// The playback task's configuration; the microphone task uses it too, + /// with " microphone" appended to the name + espp::Task::BaseConfig task_config{.name = "tab5_audio", + .stack_size_bytes = + CONFIG_M5STACK_TAB5_AUDIO_TASK_STACK_SIZE, + .priority = 20, + .core_id = 1}; + }; + + /// Initialize the audio system (ES8388 codec + ES7210 AEC) for playback and + /// recording /// \param sample_rate The audio sample rate (default 48kHz) /// \param task_config The task configuration for the audio task /// \return true if the audio system was successfully initialized, false otherwise @@ -462,6 +503,13 @@ class M5StackTab5 : public BaseComponent { .priority = 20, .core_id = 1}); + /// Initialize the audio system (ES8388 codec + ES7210 AEC) + /// \param config The sample rate, which directions to set up, and the task + /// configuration + /// \return true if the audio system was successfully initialized, false + /// otherwise (also when neither playback nor recording is enabled) + bool initialize_audio(const AudioConfig &config); + /// Enable or disable the audio system /// \param enable True to enable, false to disable void enable_audio(bool enable); @@ -520,7 +568,7 @@ class M5StackTab5 : public BaseComponent { /// signed interleaved stereo (ES7210 microphone 1 on the left slot, /// microphone 2 on the right) at audio_sample_rate() /// \return True if recording started successfully (false, among other - /// reasons, when CONFIG_M5STACK_TAB5_AUDIO_RECORDING is disabled) + /// reasons, when initialize_audio() did not enable recording) /// \note The callback runs in the dedicated microphone task's context (a /// separate task from audio playback), so keep it quick and non- /// blocking and size that task's stack accordingly @@ -1050,6 +1098,11 @@ class M5StackTab5 : public BaseComponent { // Audio system std::atomic audio_initialized_{false}; + // the directions initialize_audio() set up (written before + // audio_initialized_ is set; true until then, so that volume() and mute() + // keep reaching the ES8388 before initialization as they always have) + bool audio_playback_enabled_{true}; + bool audio_recording_enabled_{true}; std::atomic volume_{50.0f}; // microphone volume (percent), mapped onto the ES7210 analog gain range std::atomic mic_volume_{70.0f}; diff --git a/components/m5stack-tab5/src/audio.cpp b/components/m5stack-tab5/src/audio.cpp index 4c0657fbbe..d00c81b9ce 100644 --- a/components/m5stack-tab5/src/audio.cpp +++ b/components/m5stack-tab5/src/audio.cpp @@ -50,25 +50,33 @@ static audio_hal_iface_samples_t audio_hal_samples_from_rate(uint32_t rate) { // every audio BSP: see codec/i2s_tx_feed.hpp for the mechanism and why it // matters. This file keeps only what is specific to the Tab5. -// Whether initialize_audio() sets up recording (the I2S RX channel, the ES7210 -// and the microphone task): CONFIG_M5STACK_TAB5_AUDIO_RECORDING. -#if defined(CONFIG_M5STACK_TAB5_AUDIO_RECORDING) -static constexpr bool kAudioRecordingEnabled = true; -#else -static constexpr bool kAudioRecordingEnabled = false; -#endif - bool M5StackTab5::initialize_audio(uint32_t sample_rate, const espp::Task::BaseConfig &task_config) { - logger_.info("Initializing dual audio system (ES8388 + ES7210) at {} Hz", sample_rate); + return initialize_audio(AudioConfig{.sample_rate = sample_rate, .task_config = task_config}); +} + +bool M5StackTab5::initialize_audio(const AudioConfig &config) { + const uint32_t sample_rate = config.sample_rate; + const auto &task_config = config.task_config; + const bool playback = config.playback_enabled; + const bool recording = config.recording_enabled; + logger_.info("Initializing audio system ({}{}{}) at {} Hz", playback ? "ES8388 playback" : "", + playback && recording ? " + " : "", recording ? "ES7210 recording" : "", + sample_rate); if (audio_initialized_) { logger_.warn("Audio already initialized"); return true; } + if (!playback && !recording) { + logger_.error("initialize_audio(): neither playback nor recording is enabled"); + return false; + } + audio_playback_enabled_ = playback; + audio_recording_enabled_ = recording; - // I2S standard channel for TX (playback) - logger_.info("Creating I2S channel for playback (TX)"); + // I2S standard channels: TX for playback, RX for recording + logger_.info("Creating I2S channel(s)"); i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer // The ring must outlast the audio task's whole-block wait plus one block at @@ -81,10 +89,11 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, chan_cfg.dma_desc_num = espp::i2s_tx_feed::DMA_DESC_NUM; chan_cfg.dma_frame_num = espp::i2s_tx_feed::DMA_FRAME_NUM; // Both channels come from the one call so they run full duplex (RX shares the - // TX BCLK/WS), and each gets a DMA ring of this size. Without recording only - // the TX channel is created, which saves the RX ring's internal DMA memory. - ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &audio_tx_handle, - kAudioRecordingEnabled ? &audio_rx_handle : nullptr)); + // TX BCLK/WS), and each gets a DMA ring of this size: a direction that is not + // enabled is not created, which saves its ring's internal DMA memory. Alone, + // either channel drives the clocks itself (simplex master). + ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, playback ? &audio_tx_handle : nullptr, + recording ? &audio_rx_handle : nullptr)); // Configure I2S for stereo output (needed for proper ES8388 operation). The // playback data is interleaved 16-bit stereo, so the slot mode MUST be stereo @@ -101,7 +110,9 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, .invert_flags = {.mclk_inv = false, .bclk_inv = false, .ws_inv = false}}, }; logger_.info("Configuring I2S standard mode with sample rate {} Hz", sample_rate); - ESP_ERROR_CHECK(i2s_channel_init_std_mode(audio_tx_handle, &audio_std_cfg)); + if (playback) { + ESP_ERROR_CHECK(i2s_channel_init_std_mode(audio_tx_handle, &audio_std_cfg)); + } // RX channel for recording (ES7210). The ES7210 is configured below for // standard I2S output (16-bit stereo: microphone 1 on the left slot, @@ -109,11 +120,9 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, // the TX BCLK/WS, so the RX channel must use the same standard-mode // configuration - a different frame geometry (e.g. multi-slot TDM) would // not match the shared clock. - if (kAudioRecordingEnabled) { + if (recording) { logger_.info("Configuring I2S channel for recording (RX)"); ESP_ERROR_CHECK(i2s_channel_init_std_mode(audio_rx_handle, &audio_std_cfg)); - } else { - logger_.info("Recording disabled (CONFIG_M5STACK_TAB5_AUDIO_RECORDING): no RX channel"); } // Stream buffers and task @@ -122,39 +131,45 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, // frames: the sample-rate formula can land between frames, and a block that // split one would desynchronize the framing of everything received after it tx_buf_size -= tx_buf_size % (NUM_CHANNELS * NUM_BYTES_PER_CHANNEL); - audio_tx_buffer.resize(tx_buf_size); - // The audio task never blocks on the stream (see audio_task_callback), so - // the trigger level is irrelevant; the minimum keeps a producer's send from - // ever holding a receiver. - audio_tx_stream = xStreamBufferCreate(tx_buf_size * 4, 1); - xStreamBufferReset(audio_tx_stream); - audio_tx_feed_.stream = audio_tx_stream; - audio_tx_feed_.frame_bytes = NUM_CHANNELS * NUM_BYTES_PER_CHANNEL; + if (playback) { + audio_tx_buffer.resize(tx_buf_size); + // The audio task never blocks on the stream (see audio_task_callback), so + // the trigger level is irrelevant; the minimum keeps a producer's send from + // ever holding a receiver. + audio_tx_stream = xStreamBufferCreate(tx_buf_size * 4, 1); + xStreamBufferReset(audio_tx_stream); + audio_tx_feed_.stream = audio_tx_stream; + audio_tx_feed_.frame_bytes = NUM_CHANNELS * NUM_BYTES_PER_CHANNEL; + } // RX buffer for recording - if (kAudioRecordingEnabled) { + if (recording) { audio_rx_buffer.resize(tx_buf_size); } - // now enable both channels - ESP_ERROR_CHECK(i2s_channel_enable(audio_tx_handle)); - if (kAudioRecordingEnabled) { + // now enable the channels + if (playback) { + ESP_ERROR_CHECK(i2s_channel_enable(audio_tx_handle)); + } + if (recording) { ESP_ERROR_CHECK(i2s_channel_enable(audio_rx_handle)); } std::error_code ec; - es8388_i2c_device_ = internal_i2c_.add_device( - { - .device_address = 0x10, - .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), - .scl_speed_hz = internal_i2c_.config().clk_speed, - .log_level = espp::Logger::Verbosity::WARN, - }, - ec); - if (!es8388_i2c_device_) { - logger_.error("Could not initialize ES8388 I2C device: {}", ec.message()); - return false; + if (playback) { + es8388_i2c_device_ = internal_i2c_.add_device( + { + .device_address = 0x10, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!es8388_i2c_device_) { + logger_.error("Could not initialize ES8388 I2C device: {}", ec.message()); + return false; + } } - if (kAudioRecordingEnabled) { + if (recording) { es7210_i2c_device_ = internal_i2c_.add_device( { .device_address = 0x40, @@ -170,45 +185,49 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, } // Wire codec register access over internal I2C - set_es8388_write(espp::make_i2c_addressed_write(es8388_i2c_device_)); - set_es8388_read(espp::make_i2c_addressed_read_register(es8388_i2c_device_)); - if (kAudioRecordingEnabled) { + if (playback) { + set_es8388_write(espp::make_i2c_addressed_write(es8388_i2c_device_)); + set_es8388_read(espp::make_i2c_addressed_read_register(es8388_i2c_device_)); + } + if (recording) { set_es7210_write(espp::make_i2c_addressed_write(es7210_i2c_device_)); set_es7210_read(espp::make_i2c_addressed_read_register(es7210_i2c_device_)); } - // ES8388 DAC playback config - audio_hal_codec_config_t es8388_cfg{}; - es8388_cfg.codec_mode = AUDIO_HAL_CODEC_MODE_DECODE; - es8388_cfg.dac_output = AUDIO_HAL_DAC_OUTPUT_ALL; // Enable both L and R outputs - // es8388_cfg.adc_input = AUDIO_HAL_ADC_INPUT_LINE1; // Not used for playback but set anyway - es8388_cfg.i2s_iface.bits = AUDIO_HAL_BIT_LENGTH_16BITS; - es8388_cfg.i2s_iface.fmt = AUDIO_HAL_I2S_NORMAL; - es8388_cfg.i2s_iface.mode = AUDIO_HAL_MODE_SLAVE; - es8388_cfg.i2s_iface.samples = AUDIO_HAL_48K_SAMPLES; - logger_.info("Initializing ES8388 codec for playback (DAC) at {} Hz", sample_rate); - if (es8388_init(&es8388_cfg) != ESP_OK) { - logger_.error("ES8388 init failed"); - return false; - } - if (es8388_config_fmt(ES_MODULE_DAC, ES_I2S_NORMAL) != ESP_OK) { - logger_.error("ES8388 format config failed"); - } - if (es8388_set_bits_per_sample(ES_MODULE_DAC, BIT_LENGTH_16BITS) != ESP_OK) { - logger_.error("ES8388 bps config failed"); - } + if (playback) { + // ES8388 DAC playback config + audio_hal_codec_config_t es8388_cfg{}; + es8388_cfg.codec_mode = AUDIO_HAL_CODEC_MODE_DECODE; + es8388_cfg.dac_output = AUDIO_HAL_DAC_OUTPUT_ALL; // Enable both L and R outputs + // es8388_cfg.adc_input = AUDIO_HAL_ADC_INPUT_LINE1; // Not used for playback but set anyway + es8388_cfg.i2s_iface.bits = AUDIO_HAL_BIT_LENGTH_16BITS; + es8388_cfg.i2s_iface.fmt = AUDIO_HAL_I2S_NORMAL; + es8388_cfg.i2s_iface.mode = AUDIO_HAL_MODE_SLAVE; + es8388_cfg.i2s_iface.samples = AUDIO_HAL_48K_SAMPLES; + logger_.info("Initializing ES8388 codec for playback (DAC) at {} Hz", sample_rate); + if (es8388_init(&es8388_cfg) != ESP_OK) { + logger_.error("ES8388 init failed"); + return false; + } + if (es8388_config_fmt(ES_MODULE_DAC, ES_I2S_NORMAL) != ESP_OK) { + logger_.error("ES8388 format config failed"); + } + if (es8388_set_bits_per_sample(ES_MODULE_DAC, BIT_LENGTH_16BITS) != ESP_OK) { + logger_.error("ES8388 bps config failed"); + } - // Configure DAC output routing - if (es8388_config_dac_output(DAC_OUTPUT_ALL) != ESP_OK) { - logger_.error("ES8388 DAC output config failed"); - } + // Configure DAC output routing + if (es8388_config_dac_output(DAC_OUTPUT_ALL) != ESP_OK) { + logger_.error("ES8388 DAC output config failed"); + } - // Set initial volume and unmute - es8388_set_voice_volume(static_cast(volume_)); - es8388_set_voice_mute(false); // Make sure it's not muted - es8388_ctrl_state(AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START); + // Set initial volume and unmute + es8388_set_voice_volume(static_cast(volume_)); + es8388_set_voice_mute(false); // Make sure it's not muted + es8388_ctrl_state(AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START); + } - if (kAudioRecordingEnabled) { + if (recording) { // ES7210 ADC recording config audio_hal_codec_config_t es7210_cfg{}; es7210_cfg.codec_mode = AUDIO_HAL_CODEC_MODE_ENCODE; @@ -261,17 +280,19 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, // exactly the rate samples arrive (rather than only between blocking TX // writes), which the shared-task approach did irregularly - on this // full-duplex controller that jitter degraded the recording and let the ring - // back up. Without recording there is no RX channel to read, so no - // microphone task either. - logger_.info("Creating audio playback{} task", kAudioRecordingEnabled ? " and microphone" : ""); + // back up. Each task exists only for a direction that is enabled. using namespace std::placeholders; - audio_task_ = espp::Task::make_unique( - {.callback = std::bind(&M5StackTab5::audio_task_callback, this, _1, _2, _3), - .task_config = task_config}); + if (playback) { + logger_.info("Creating audio playback task"); + audio_task_ = espp::Task::make_unique( + {.callback = std::bind(&M5StackTab5::audio_task_callback, this, _1, _2, _3), + .task_config = task_config}); + } // give the microphone task a distinct name from the playback task (they share // the rest of the caller's task config) so the two are easy to tell apart in // logs and while debugging - if (kAudioRecordingEnabled) { + if (recording) { + logger_.info("Creating microphone task"); auto mic_task_config = task_config; mic_task_config.name = task_config.name.empty() ? std::string("microphone") : task_config.name + " microphone"; @@ -284,23 +305,27 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, // state: if the microphone task fails to start, stop the already-running // playback task and report failure rather than leaving it running with // audio_initialized_ set. - if (!audio_task_->start()) { + if (audio_task_ && !audio_task_->start()) { logger_.error("Failed to start the audio playback task"); return false; } if (microphone_task_ && !microphone_task_->start()) { logger_.error("Failed to start the microphone task"); - // the playback task may already have registered its handle for - // play_audio()'s notifications: forget it before the task goes away - audio_tx_feed_.detach_task(); - audio_task_->stop(); - // the task may have paused charging on its first blocks - resume_charging_after_audio(); + if (audio_task_) { + // the playback task may already have registered its handle for + // play_audio()'s notifications: forget it before the task goes away + audio_tx_feed_.detach_task(); + audio_task_->stop(); + // the task may have paused charging on its first blocks + resume_charging_after_audio(); + } return false; } // Enable speaker output - enable_audio(true); + if (playback) { + enable_audio(true); + } audio_initialized_ = true; return true; @@ -314,7 +339,9 @@ void M5StackTab5::enable_audio(bool enable) { void M5StackTab5::volume(float volume) { volume = std::max(0.0f, std::min(100.0f, volume)); volume_ = volume; - es8388_set_voice_volume(static_cast(volume_)); + if (audio_playback_enabled_) { + es8388_set_voice_volume(static_cast(volume_)); + } logger_.debug("Volume set to {:.1f} %", volume); } @@ -322,14 +349,16 @@ float M5StackTab5::volume() const { return volume_; } void M5StackTab5::mute(bool mute) { mute_ = mute; - es8388_set_voice_mute(mute_); + if (audio_playback_enabled_) { + es8388_set_voice_mute(mute_); + } logger_.debug("Audio {}", mute ? "muted" : "unmuted"); } bool M5StackTab5::is_muted() const { return mute_; } size_t M5StackTab5::play_audio(const uint8_t *data, uint32_t num_bytes) { - if (!audio_initialized_ || !data || num_bytes == 0) { + if (!audio_initialized_ || !audio_playback_enabled_ || !data || num_bytes == 0) { return 0; } // Enqueue only whole 16-bit stereo frames (4 bytes) that actually fit: @@ -360,7 +389,7 @@ size_t M5StackTab5::play_audio(std::span data) { void M5StackTab5::microphone_volume(float volume) { mic_volume_ = std::clamp(volume, 0.0f, 100.0f); - if (kAudioRecordingEnabled && audio_initialized_) { + if (audio_initialized_ && audio_recording_enabled_) { es7210_adc_set_gain_all(microphone_gain_from_volume(mic_volume_)); } } @@ -369,14 +398,14 @@ float M5StackTab5::microphone_volume() const { return mic_volume_; } bool M5StackTab5::start_audio_recording( std::function callback) { - if (!kAudioRecordingEnabled) { - logger_.error("Audio recording is disabled (CONFIG_M5STACK_TAB5_AUDIO_RECORDING)"); - return false; - } if (!audio_initialized_) { logger_.error("Audio system not initialized"); return false; } + if (!audio_recording_enabled_) { + logger_.error("Audio recording was not enabled in initialize_audio()"); + return false; + } // Reject a start while a recording is already in progress: the microphone // task reads audio_rx_callback_ whenever recording_ is true, so reassigning // the callback here (without stopping first) would race that task. The caller @@ -460,15 +489,24 @@ size_t M5StackTab5::audio_buffer_size() const { return audio_tx_buffer.size(); } void M5StackTab5::audio_sample_rate(uint32_t sample_rate) { logger_.info("Setting audio sample rate to {} Hz", sample_rate); + // the channel that drives the clocks: TX (which RX shares in full duplex), + // or RX when only recording is set up + i2s_chan_handle_t clock_handle = audio_tx_handle ? audio_tx_handle : audio_rx_handle; + if (!clock_handle) { + logger_.warn("Audio not initialized: pass the sample rate to initialize_audio()"); + return; + } // stop the channel - i2s_channel_disable(audio_tx_handle); + i2s_channel_disable(clock_handle); // update the sample rate audio_std_cfg.clk_cfg.sample_rate_hz = sample_rate; - i2s_channel_reconfig_std_clock(audio_tx_handle, &audio_std_cfg.clk_cfg); + i2s_channel_reconfig_std_clock(clock_handle, &audio_std_cfg.clk_cfg); // clear the buffer - xStreamBufferReset(audio_tx_stream); + if (audio_tx_handle) { + xStreamBufferReset(audio_tx_stream); + } // restart the channel - i2s_channel_enable(audio_tx_handle); + i2s_channel_enable(clock_handle); } } // namespace espp diff --git a/components/m5stack-tab5/src/video.cpp b/components/m5stack-tab5/src/video.cpp index d4bd45c85b..6a8d048873 100644 --- a/components/m5stack-tab5/src/video.cpp +++ b/components/m5stack-tab5/src/video.cpp @@ -64,14 +64,6 @@ static constexpr uint8_t kNumDpiFramebuffers = CONFIG_M5STACK_TAB5_DPI_NUM_FBS; static_assert(kNumDpiFramebuffers >= 1 && kNumDpiFramebuffers <= M5StackTab5::max_frame_buffers, "CONFIG_M5STACK_TAB5_DPI_NUM_FBS must be 1..3 (the DPI panel driver's limit)"); -// Whether draw_bitmap() copies into the frame buffer may use the DMA2D engine -// (CONFIG_M5STACK_TAB5_DPI_DMA2D; per controller, see initialize_lcd()). -#if defined(CONFIG_M5STACK_TAB5_DPI_DMA2D) -static constexpr bool kDpiDma2dEnabled = true; -#else -static constexpr bool kDpiDma2dEnabled = false; -#endif - // Whether this ESP-IDF's DPI panel raises its frame-done event from the DMA // transfer-done interrupt after re-arming on the queued frame buffer // (on_frame_buf_complete: v5.5.5+, v6.0.3+). Earlier versions raise @@ -177,7 +169,9 @@ M5StackTab5::DisplayController M5StackTab5::detect_display_controller() { // Unknown display controller return M5StackTab5::DisplayController::UNKNOWN; } -bool M5StackTab5::initialize_lcd() { +bool M5StackTab5::initialize_lcd() { return initialize_lcd(LcdConfig{}); } + +bool M5StackTab5::initialize_lcd(const LcdConfig &config) { logger_.info("Initializing M5Stack Tab5 LCD (MIPI-DSI, {}x{})", display_width_, display_height_); // Re-initialization is NOT supported once the publication gate has opened: @@ -343,7 +337,7 @@ bool M5StackTab5::initialize_lcd() { dpi_cfg.video_timing.vsync_pulse_width = 4; dpi_cfg.video_timing.vsync_front_porch = 20; #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.flags.use_dma2d = kDpiDma2dEnabled; + dpi_cfg.flags.use_dma2d = config.dma2d_enabled; #endif } else if (detected_controller == DisplayController::ST7123 && lcd_handles_.panel == nullptr) { @@ -365,7 +359,7 @@ bool M5StackTab5::initialize_lcd() { dpi_cfg.video_timing.vsync_pulse_width = 2; dpi_cfg.video_timing.vsync_front_porch = 220; #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.flags.use_dma2d = kDpiDma2dEnabled; + dpi_cfg.flags.use_dma2d = config.dma2d_enabled; #endif } else if (detected_controller == DisplayController::ST7121 && lcd_handles_.panel == nullptr) { dpi_cfg.virtual_channel = 0; @@ -384,7 +378,7 @@ bool M5StackTab5::initialize_lcd() { dpi_cfg.video_timing.vsync_pulse_width = 20; dpi_cfg.video_timing.vsync_front_porch = 200; #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.flags.use_dma2d = kDpiDma2dEnabled; + dpi_cfg.flags.use_dma2d = config.dma2d_enabled; #endif } @@ -579,7 +573,7 @@ bool M5StackTab5::initialize_lcd() { // M5Stack's own Tab5 demo firmware ships with DMA2D enabled on production // units that are predominantly ST71xx. // - // CONFIG_M5STACK_TAB5_DPI_DMA2D turns it off altogether, for applications + // LcdConfig::dma2d_enabled turns it off altogether, for applications // that never draw_bitmap() (every flush rotated by the PPA) and would rather // have back the internal DMA-capable RAM its descriptors take. // @@ -592,9 +586,9 @@ bool M5StackTab5::initialize_lcd() { #else constexpr bool dma2d_breaks_fb_switch = false; #endif - if constexpr (!kDpiDma2dEnabled) { - logger_.info("DMA2D disabled (CONFIG_M5STACK_TAB5_DPI_DMA2D): keeping CPU draw_bitmap copies"); - } else if constexpr (dma2d_breaks_fb_switch) { + if (!config.dma2d_enabled) { + logger_.info("DMA2D disabled (LcdConfig::dma2d_enabled): keeping CPU draw_bitmap copies"); + } else if (dma2d_breaks_fb_switch) { logger_.info("ESP-IDF v6.0.0 with {} frame buffers: keeping CPU draw_bitmap copies (DMA2D " "would turn a frame-buffer switch into a copy)", kNumDpiFramebuffers); From abe3da8ab385f13adf32da4ad2bbd4dfa9be68e4 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 3 Nov 2026 14:41:53 -0600 Subject: [PATCH 3/4] fix(m5stack-tab5): stop RX during a sample rate change; review doc fixes - audio_sample_rate(rate) also disables the RX channel while it reconfigures the shared clock in full duplex - volume(), mute() and microphone_volume() note that they only store the value when their direction is not set up - quote the measured DMA2D saving (about 3 KB) everywhere Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- components/m5stack-tab5/README.md | 4 ++-- components/m5stack-tab5/include/m5stack-tab5.hpp | 10 ++++++++-- components/m5stack-tab5/src/audio.cpp | 16 ++++++++++++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/components/m5stack-tab5/README.md b/components/m5stack-tab5/README.md index 82bdcc0830..5424d662b2 100644 --- a/components/m5stack-tab5/README.md +++ b/components/m5stack-tab5/README.md @@ -162,8 +162,8 @@ unaligned transfers. fails. Without playback there is no ES8388 setup and `play_audio()` queues nothing. - `initialize_lcd({.dma2d_enabled = false})` keeps CPU copies for - `esp_lcd_panel_draw_bitmap()` instead of the DMA2D engine, which saves a - few KB. Rotated LVGL flushes don't use `draw_bitmap()` (the PPA rotates + `esp_lcd_panel_draw_bitmap()` instead of the DMA2D engine, which saves + about 3 KB. Rotated LVGL flushes don't use `draw_bitmap()` (the PPA rotates them straight into the frame buffer), so an application that always rotates loses nothing. diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index ece4f438f6..7451468020 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -216,8 +216,8 @@ class M5StackTab5 : public BaseComponent { /// frame buffer with the DMA2D engine instead of the CPU (on ESP-IDF 6.0 /// and later only on the ST7123 / ST7121 panels: the ILI9881 panel keeps /// CPU copies). That takes the copies' PSRAM traffic off the CPU, which - /// otherwise can starve the panel's scan-out DMA, but DMA2D costs a few - /// KB of internal DMA-capable RAM. Only unrotated LVGL flushes, + /// otherwise can starve the panel's scan-out DMA, but DMA2D costs about + /// 3 KB of internal DMA-capable RAM. Only unrotated LVGL flushes, /// write_lcd_lines() and the software rotation fallback use /// draw_bitmap(): rotated flushes go through the PPA straight into the /// frame buffer, so an application that always rotates (e.g. landscape) @@ -516,6 +516,8 @@ class M5StackTab5 : public BaseComponent { /// Set the audio volume /// \param volume The volume as a percentage (0-100) + /// \note Without playback (AudioConfig::playback_enabled) the value is only + /// stored. void volume(float volume); /// Get the audio volume @@ -524,6 +526,8 @@ class M5StackTab5 : public BaseComponent { /// Mute or unmute the audio /// \param mute True to mute, false to unmute + /// \note Without playback (AudioConfig::playback_enabled) the value is only + /// stored. void mute(bool mute); /// Check if audio is muted @@ -580,6 +584,8 @@ class M5StackTab5 : public BaseComponent { /// Set the microphone volume /// \param volume The volume as a percentage (0 - 100), mapped onto the /// ES7210 analog microphone gain range (0 dB - +37.5 dB) + /// \note Without recording (AudioConfig::recording_enabled) the value is + /// only stored. void microphone_volume(float volume); /// Get the microphone volume diff --git a/components/m5stack-tab5/src/audio.cpp b/components/m5stack-tab5/src/audio.cpp index d00c81b9ce..8855335375 100644 --- a/components/m5stack-tab5/src/audio.cpp +++ b/components/m5stack-tab5/src/audio.cpp @@ -489,15 +489,20 @@ size_t M5StackTab5::audio_buffer_size() const { return audio_tx_buffer.size(); } void M5StackTab5::audio_sample_rate(uint32_t sample_rate) { logger_.info("Setting audio sample rate to {} Hz", sample_rate); - // the channel that drives the clocks: TX (which RX shares in full duplex), - // or RX when only recording is set up + // The channel that drives the clocks: TX (whose BCLK/WS RX shares in full + // duplex), or RX when only recording is set up. i2s_chan_handle_t clock_handle = audio_tx_handle ? audio_tx_handle : audio_rx_handle; if (!clock_handle) { logger_.warn("Audio not initialized: pass the sample rate to initialize_audio()"); return; } - // stop the channel + // stop the channels: in full duplex RX runs on the clock being changed, so + // it is stopped too rather than left sampling through the switch (the + // microphone task's read just times out meanwhile) i2s_channel_disable(clock_handle); + if (audio_rx_handle && audio_rx_handle != clock_handle) { + i2s_channel_disable(audio_rx_handle); + } // update the sample rate audio_std_cfg.clk_cfg.sample_rate_hz = sample_rate; i2s_channel_reconfig_std_clock(clock_handle, &audio_std_cfg.clk_cfg); @@ -505,8 +510,11 @@ void M5StackTab5::audio_sample_rate(uint32_t sample_rate) { if (audio_tx_handle) { xStreamBufferReset(audio_tx_stream); } - // restart the channel + // restart the channels i2s_channel_enable(clock_handle); + if (audio_rx_handle && audio_rx_handle != clock_handle) { + i2s_channel_enable(audio_rx_handle); + } } } // namespace espp From 74df8e17bc35bc126638482b631440e651019d1d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 3 Nov 2026 15:02:47 -0600 Subject: [PATCH 4/4] fix(m5stack-tab5): log each I2S channel as it is configured Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- components/m5stack-tab5/src/audio.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/m5stack-tab5/src/audio.cpp b/components/m5stack-tab5/src/audio.cpp index 8855335375..0004bc2ae3 100644 --- a/components/m5stack-tab5/src/audio.cpp +++ b/components/m5stack-tab5/src/audio.cpp @@ -109,8 +109,8 @@ bool M5StackTab5::initialize_audio(const AudioConfig &config) { .din = audio_asdout_io, // ES7210 ASDOUT (record data output) .invert_flags = {.mclk_inv = false, .bclk_inv = false, .ws_inv = false}}, }; - logger_.info("Configuring I2S standard mode with sample rate {} Hz", sample_rate); if (playback) { + logger_.info("Configuring I2S channel for playback (TX) at {} Hz", sample_rate); ESP_ERROR_CHECK(i2s_channel_init_std_mode(audio_tx_handle, &audio_std_cfg)); } @@ -121,7 +121,7 @@ bool M5StackTab5::initialize_audio(const AudioConfig &config) { // configuration - a different frame geometry (e.g. multi-slot TDM) would // not match the shared clock. if (recording) { - logger_.info("Configuring I2S channel for recording (RX)"); + logger_.info("Configuring I2S channel for recording (RX) at {} Hz", sample_rate); ESP_ERROR_CHECK(i2s_channel_init_std_mode(audio_rx_handle, &audio_std_cfg)); }