这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » STM32H7S78-DK本地MP3播放

共1条 1/1 1 跳转至页

STM32H7S78-DK本地MP3播放

高工
2026-11-12 12:40:16     打赏
STM32H7S78-DK 本地 MP3 播放

本文记录在「悦聆」多媒体中枢上新增本地 MP3 播放的完整过程:SDMMC1 读 SD 卡(FATFS)→ minimp3 软解码 → 与网络收音机共用同一条 I2S6/WM8904 音频管道出声。功能本身不复杂,价值在三个 BSP 层的典型坑、一个 GPIO 极性反转的坑,以及最后"低音破声"的定量排查方法——这套方法在任何嵌入式音频项目里都通用。

3ff184e1c05e7d280df34fd1f7125cdd.jpg

1. 架构

SD 卡(FAT32,根目录 *.mp3)
      │ SDMMC1 4bit(PLL2_S 135.5MHz 内核时钟, IDMA)
      ▼
mp3_player(解码线程, prio 6): FATFS 读文件组帧 → minimp3
      │ pcm_util: 线性插值重采样到 48117Hz + Goertzel 频谱
      ▼ (pcm_q 阻塞反压,与收音机共用)
audio_sai(发送线程, prio 5) → GPDMA 循环 → I2S6 → WM8904 → 耳机口

设计要点是复用而不是新造:网络收音机里的重采样和频谱分析这次抽成了公共模块 pcm_util.c/h,两个解码线程(收音机/MP3)共用。因为重采样器内部有跨帧相位状态(全局单例),同一时刻只允许一路音源,由上层 UI 互斥保证:

/* 点电台:先停本地播放 */
static void station_btn_cb(lv_event_t *e)
{
    mp3_stop();
    radio_play((int)(intptr_t)lv_event_get_user_data(e));
}

/* 点 MP3 曲目:先停收音机 */
static void mp3_track_btn_cb(lv_event_t *e)
{
    radio_stop();
    mp3_play((int)(intptr_t)lv_event_get_user_data(e));
}

切换音源前还要调 pcm_resample_reset() 清掉上一路的插值相位和跨帧尾样本,否则换源瞬间会有一个样本级的跳变杂音。

2. SD 卡接入的三个坑

坑一:ext_flash_app 变体没有 sdmmc1 节点

板子基础 dts(stm32h7s78_dk.dts)里有完整的 sdmmc1 配置,但外部 Flash 启动变体 stm32h7s78_dk_stm32h7s7xx_ext_flash_app.dts 只 #include 了 common.dtsi,SD 节点不在里面。直接开 FATFS 会在编译期报 Wrong FF_VOLUMES setting——FATFS 的卷号是从设备树里 status=okay 的 disk 节点数出来的,一个都没有就是 0。

在应用 overlay 里把基础 dts 的配置补齐:

&sdmmc1 {
    pinctrl-0 = <&sdmmc1_d0_pc8 &sdmmc1_d1_pc9
             &sdmmc1_d2_pc10 &sdmmc1_d3_pc11
             &sdmmc1_ck_pc12 &sdmmc1_cmd_pd2>;
    pinctrl-names = "default";
    clocks = <&rcc STM32_CLOCK(AHB5, 8)>,
         <&rcc STM32_SRC_PLL2_S SDMMC_SEL(0)>;
    bus-width = <4>;
    hw-flow-control;
    cd-gpios = <&gpiom 14 GPIO_ACTIVE_LOW>;
    status = "okay";

    sdmmc {
        compatible = "zephyr,sdmmc-disk";
        disk-name = "SD";
        status = "okay";
    };
};

两个 clocks 条目含义不同:第一个是 AHB5 总线时钟(寄存器访问),第二个是内核时钟源选择——SDMMC 外设的实际工作时钟,这里选了 PLL2_S。

坑二:PLL2 缺 div-s,内核时钟选源被拒

配完节点后启动报 Failed to enable SDHC domain clock。根因:common.dtsi 里 PLL2 只配了 div-q(那是给 I2S6 出 271MHz 用的),没配 div-s。Zephyr 的 STM32 时钟驱动用这个宏判断 PLL 某路输出是否可用:

/* include/zephyr/drivers/clock_control/stm32_clock_control.h */
#define STM32_PLL2_S_ENABLED    DT_NODE_HAS_PROP(DT_NODELABEL(pll2), div_s)

没有 div-s 属性就视为未使能,选源直接返回 -ENOTSUP。补一行即可——PLL2 VCO 为 24MHz/12×271=542MHz,除 4 得 135.5MHz 给 SDMMC:

&pll2 {
    div-s = <4>;
};

验证不靠猜,直接 GDB 读寄存器:RCC_PLL2DIVR2(0x580244C4)低 3 位为 3,即 DIVS=3+1=4,和预期一致。

坑三:新旧两个 SDMMC 驱动互斥

sdmmc1 使能后,legacy disk 驱动 SDMMC_STM32 和新 SDHC 驱动 SDHC_STM32_SDMMC 同时满足 default y 条件。legacy 驱动会让 FATFS 直接在 st,stm32-sdmmc 节点上找 disk-name 属性(我们没有,属性在 zephyr,sdmmc-disk 子节点上),编译报 'DT_N_S_soc_S_sdmmc_52007000_P_disk_name' undeclared。我们走的是新驱动路线(SDHC 子系统 + IDMA + cache 维护都更完善),显式关掉旧的:

CONFIG_DISK_ACCESS=y
CONFIG_FILE_SYSTEM=y
CONFIG_FAT_FILESYSTEM_ELM=y
CONFIG_FS_FATFS_LFN=y          # 不开这个文件名显示成 CANON_~1.MP3 短名
CONFIG_SDMMC_STM32=n           # 用新 SDHC 驱动
CONFIG_HEAP_MEM_POOL_SIZE=8192 # SDHC 驱动的 DMA 对齐缓冲走系统堆

FS_FATFS_LFN 值得单独说:不开长文件名支持,FATFS 只认 8.3 短名,canon_in_d.mp3 在曲目列表里显示为 CANON_~1.MP3——功能是通的,但体验上一个时代的差距。

3. 解码线程:和流媒体的同与不同

解码主循环和收音机几乎同构,但有一个本质区别:文件可以断点续播,广播流不能。收音机暂停时必须继续消费网络数据并丢弃(否则服务器端断开),MP3 暂停则是完全不读文件,位置天然保持:

while (req_track == cur_track && !stop_req) {
    if (pause_flag) {
        /* 暂停:不消费文件(断点续播),音频层自动填静音 */
        mp3_state = MP3_ST_PAUSED;
        spectrum_decay();
        k_msleep(50);
        continue;
    }
    mp3_state = MP3_ST_PLAYING;

    if (!ibuf_fill(&file)) {
        break;  /* EOF,播完自动切下一首 */
    }

    int samples = mp3dec_decode_frame(&mp3dec, ibuf, (int)ibuf_len,
                      pcm_frame, &info);
    if (info.frame_bytes > 0) {
        ibuf_len -= info.frame_bytes;   /* 滑窗前移 */
        memmove(ibuf, ibuf + info.frame_bytes, ibuf_len);
    } else if (ibuf_len >= IBUF_SIZE - 1) {
        /* 缓冲满还找不到帧头(常见于大 ID3 标签):丢一半重新同步 */
        ibuf_len /= 2;
        memmove(ibuf, ibuf + ibuf_len, ibuf_len);
    }

    if (samples > 0) {
        size_t out_n = pcm_resample_to_playback(pcm_frame, samples,
                            info.hz, info.channels, rs_buf);
        spectrum_feed(rs_buf, out_n);
        (void)audio_write_pcm(rs_buf, out_n);   /* 阻塞反压,天然限速 */
    }
}

几处值得展开:

  • minimp3 的容错设计:mp3dec_decode_frame 的返回值分两部分——frame_bytes 是这次消耗掉的输入字节数(含跳过的垃圾数据),samples 是解出的 PCM 样本数。哪怕遇到 ID3v2 标签、垃圾填充,只要按 frame_bytes 滑动窗口就能自动重新同步。输入滑窗 5KB,远大于最大 MP3 帧约 1.4KB。

  • 不需要文件缓冲池:网络流需要 24KB pipe 吸收抖动,文件读取是确定性的,audio_write_pcm 的阻塞反压(队列满就等)天然把解码速率对齐到播放速率,一个 5KB 滑窗就够。

  • 播完自动切歌:play_one() 返回后线程检查退出原因——stop_req 是用户停止,req_track 变了是切歌,否则就是自然播完,索引加一回卷继续播。

4. 热插拔:一个 ACTIVE_LOW 极性反转的坑

挂载/扫描只在开机做一次显然不够——用户换卡后列表不会更新。实现是主循环每秒轮询卡检测脚(PM14,dts 里声明为低有效):

void mp3_card_poll(void)
{
    bool present = gpio_pin_get_dt(&cd_pin) != 0;

    if (present && !card_present) {
        k_msleep(300);  /* 触点防抖/卡上电稳定 */
        present = gpio_pin_get_dt(&cd_pin) != 0;
    }

    if (!present) {
        if (card_present) {
            mp3_stop();
            k_msleep(100);  /* 等解码线程退出当前曲目 */
            if (card_mounted) {
                fs_unmount(&mountpoint);
                card_mounted = false;
            }
            track_count = 0;
            mp3_state = MP3_ST_NO_CARD;
        }
        card_present = false;
        return;
    }

    card_present = true;
    if (!card_mounted) {
        if (fs_mount(&mountpoint) == 0) {   /* 失败则下个周期(2s 节流)重试 */
            card_mounted = true;
            mp3_state = scan_tracks() > 0 ? MP3_ST_IDLE : MP3_ST_EMPTY;
        }
    }
}

写完烧进去,卡明明插着却报"未检测到SD卡"。GDB 直接读 GPIOM IDR(0x58023010):bit14=0,物理电平是低,卡在位——硬件没问题,是软件判断反了。

根因是个 API 语义陷阱:gpio_pin_get_dt() 返回的不是物理电平,而是应用了 GPIO_ACTIVE_LOW 反转后的逻辑值。引脚物理低 → 逻辑"激活"→ 返回 1。我第一版写的是 == 0 表示插卡,正好全反。这个函数名字起得像读电平,实际读的是逻辑状态,修完后在代码里留了注释:

/* 卡检测脚(低有效)。注意 gpio_pin_get_dt 已含 ACTIVE_LOW 反转:
 * 返回 1 = 卡插入 */
static const struct gpio_dt_spec cd_pin =
    GPIO_DT_SPEC_GET(DT_NODELABEL(sdmmc1), cd_gpios);

UI 侧配合:曲目列表从"建页面时一次性填充"改成可重建,主循环发现曲目数变化就在 lvgl_lock 下 lv_obj_clean + 重建按钮,插卡后列表一秒左右自动出现。

5. 低音破声:一次定量排查实录

现象:音量拉大后低音有破声,音箱已排除。软件音量最大 100% 是单位增益(sample * 100 / 100),数学上不会削波,那是哪里的问题?

第一步:加测量,不靠猜

在音频发送线程加两个调试计数器,统计实际发给 DMA 的 PCM 峰值:

volatile uint32_t dbg_peak;     /* 已发 PCM 峰值 */
volatile uint32_t dbg_clip_samples; /* 达到满幅(>=32760)的样本数 */

static void measure_peak(const int16_t *samples, size_t count)
{
    uint32_t peak = 0;

    for (size_t i = 0; i < count; i++) {
        uint32_t a = samples[i] < 0 ? (uint32_t)-samples[i]
                        : (uint32_t)samples[i];
        if (a > peak) {
            peak = a;
        }
        if (a >= 32760) {
            dbg_clip_samples++;
        }
    }
    if (peak > dbg_peak) {
        dbg_peak = peak;
    }
}

让破声复现,然后 openocd + GDB 在运行中的板子上直接读:

(gdb) print dbg_peak           → 29431
(gdb) print dbg_clip_samples   → 0

峰值 29431(约 90% 满幅),满幅样本 0——数字域完全干净,削波假设被排除。排查方向立刻收窄到 codec 的 DAC/输出放大级。

第二步:A/B 实验收敛

改一行代码烧一版,用听感做二分:

void audio_set_volume(uint8_t vol)
{
    cur_volume = MIN(vol, 100) / 2;     /* 实验1: -6dB → 破声消失 */
    cur_volume = MIN(vol, 100) * 7 / 10;    /* 实验2: -3dB → 还有破声 */
    cur_volume = MIN(vol, 100) * 55 / 100;  /* 实验3: -5.2dB → 干净,定版 */
}

结论:接近满幅的低音信号把 WM8904 输出级顶失真了(耳机驱动电流/电荷泵在大动态低音下供不上是典型的模拟域现象),修复就是软件音量留余量,最大响度损失约 5dB 由音箱端补偿。

两个调试计数器留在代码里不删——以后再遇到任何音质问题,第一步就是读 dbg_peak 判断数字域是否干净,比反复试听猜方向快得多。

6. 无仪器验证方法总结

这次开发全程没用示波器/逻辑分析仪,手段就是 GDB 读变量和寄存器(串口被终端占用时尤其好用):

验证点手段预期值
SD 时钟修复读 RCC_PLL2DIVR2 @0x580244C4低 3 位 = 3(div-s=4)
卡检测脚读 GPIOM IDR @0x58023010bit14=0 插卡 / 1 拔卡
挂载扫描结果读 mp3_state / track_countIDLE / 实际曲目数
播放是否满速读 dbg_tx_blocks 增速~21 块/秒(48ms/块)
读取速率匹配读 cur_pos 增速≈ 码率(256kbps ≈ 32KB/s)
数字域是否削波读 dbg_peak / dbg_clip_samples<32760 / 0

工程位于 app/net_radio(本次新增 mp3_player.c/h、pcm_util.c/h),构建烧录命令与前几篇一致:

cd /d/luglZephyrproject
export ZEPHYR_SDK_INSTALL_DIR="D:/luglZephyrproject/zephyr-sdk-1.0.1/zephyr-sdk-1.0.1"
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
west build -b stm32h7s78_dk/stm32h7s7xx/ext_flash_app --sysbuild \
    -s D:/STM32/stm32H7S78-dk/app/net_radio \
    -d D:/STM32/stm32H7S78-dk/app/net_radio/build -p
west flash -d D:/STM32/stm32H7S78-dk/app/net_radio/build --domain net_radio





关键词: STM32H7S78-DK     MP3     LVGL    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]