
简介面向嵌入式开发者的完整音乐播放实验工程主控使用 STM32F103ZET6 微控制器音频输出由 PCM5102 数模转换芯片承担整体实现从 SD 卡读取文件、解析数据并实时播放的流程。项目覆盖 Cortex-M3 外设初始化、FatFS 文件系统移植、I2S 音频接口配置和 DMA 传输等典型环节适合正在学习单片机音频应用、或希望参考现成播放框架的开发者阅读。压缩包约 7.95MB共 564 个文件以 342 个 C 源文件、103 个头文件和 43 个汇编文件为主体另含 Keil 工程配置、链接脚本及少量说明文档便于按模块理解工程组织方式。目前已有 2082 人学习下载。通过代码可以了解 SD 卡挂载、音乐数据分块读取、缓冲管理和 PCM5102 输出控制的完整链路调试信息与工程文件也能辅助定位初始化失败、声道设置异常、DMA 中断异常等问题。整体方案适合在此基础上继续扩展音乐播放器、语音播报或更复杂的音频处理应用。 做这个小实验之前我其实犹豫了一阵子——PCM5102 这个 DAC 在 STM32F103 上播放 SD 卡里的 WAV 文件是不是有点大材小用毕竟 F103ZET6 的 I2S 外设用起来没有 F4 那么痛快SD 卡还得走 SPI 模式速度上限摆在那。但真正调通之后我觉得这套组合非常适合作为音频播放的基础实验硬件成本低、原理链路清晰、可复现性强而且把 STM32 的 SPI、I2S、DMA、FATFS 文件系统全串起来了。这篇文章就把我在 STM32F103ZET6 上完整跑通 PCM5102 播放 SD 卡音乐的过程整理出来包括选型思路、接线表、关键代码、以及我实际踩过的几个坑给正在做音频类嵌入式实验的朋友一个可以直接参考的完整方案。1. 整体方案与设计思路1.1 为什么是“F103ZET6 PCM5102 SD 卡”这套组合先说结论这套组合是“低成本学习音频播放链路”的一个经典配法。主控选了 STM32F103ZET6原因很直接它自带 I2S 外设虽然只有 SPI2/I2S2 和 SPI3/I2S3 这两个可以做音频接口同时有足够的 DMA 通道来保证音频数据流的连续搬运。对于讲解“SD 卡 → 文件系统 → 内存缓冲 → DMA → I2S → DAC”这条完整链路F103 的复杂度刚刚好。如果你第一次做音频播放直接上 F4 或 H7很多问题会被高性能掩盖反而不利于理解底层机制。DAC 芯片选了 PCM5102这是德州仪器的一款立体声数模转换芯片支持最高 384kHz 采样率、32bit 位深关键是它对主控非常友好支持 I2S/LJ/RJ 三种标准格式自带时钟恢复电路内部还有 PLL因此不需要强制外接主时钟 MCLK。这一点对 F103 来说特别重要因为 F103 的 I2S 外设不像 F4 那样方便生成 MCLK 输出而 PCM5102 允许把 SCK 脚直接接地让芯片自己从 BCK 里恢复时钟大幅降低了硬件接线和软件配置的复杂度。SD 卡这块F103ZET6 虽然带了 SDIO 控制器但为了简化接线和降低新手门槛这个实验选择 SPI 模式。SPI 模式理论上速度上限在 18Mbps约 2.25MB/s对播放 44.1kHz/16bit/立体声的 WAV 文件来说数据率约为 176.4KB/s带宽绰绰有余。当然实际用下来 FATFS 的 overhead 会让实际吞吐打折这个问题后面第 4 节我会重点讲。1.2 硬件接线与引脚分配先给出我实际使用的完整接线表方便你直接对照接线。PCM5102 模块常见的引脚名称是 SCK、BCK、DIN、LRCK、FMT、FLT、XMS、DEMP 等不同卖家模块丝印会略有差异但核心引脚都一致。功能STM32F103ZET6 引脚PCM5102 / SD 卡模块引脚I2S2 位时钟 BCKPB13PCM5102 BCKI2S2 数据 DINPB15PCM5102 DINI2S2 声道时钟 LRCKPB12PCM5102 LRCK系统时钟 SCK不接或接 GNDPCM5102 SCK格式选择 FMT-接 GNDI2S 标准格式滤波配置 FLT-接 GND软静音 XMS-接 GND取消软静音去加重 DEMP-接 GNDPCM5102 电源3.3V / GNDVIN / GNDSD 卡 CSPA4SD 模块 CSSD 卡 SCKPA5SD 模块 SCKSD 卡 MISOPA6SD 模块 MISOSD 卡 MOSIPA7SD 模块 MOSISD 卡电源3.3V / GNDVCC / GND注意两个易错点。第一PCM5102 的 SCK 通常建议直接接地而不是悬空。接地后芯片会启用内部的 PLL 时钟恢复电路从 BCK 中提取主时钟这样我们就不需要额外提供 MCLK 了。第二模块的 VIN 供电要看具体模块有没有板载稳压器有的模块标注 VIN 3.3V-5V 都能用有的模块必须 3.3V。我用的模块实测 3.3V 供电输出音量略低于 5V但稳定性更好建议统一用 3.3V。另外PB13/PB12/PB15 这三个引脚复用为 I2S2 功能后SD 卡的 SPI1PA4-PA7与它互不冲突。如果你的开发板上有板载 TFT 屏幕占用了一部分 SPI 引脚需要先确认引脚冲突我一开始没注意查了很久才发现是屏幕和 SD 卡抢了同一个 SPI 片选线。2. 音频文件与文件系统准备2.1 为什么用 WAV 而不是 MP3/FLAC这个问题每届学生都会问我的回答很简单F103 解码 MP3 太吃力。MP3 是有损压缩格式播放前需要先解码。F103ZET6 主频仅 72MHz没有硬件解码器如果软解 MP3 得移植一个解码库如 Helix MP3 decoder不仅代码体积大、CPU 占用高而且解码后的数据还需要和 I2S 播放进度做同步调试难度直接上升一个量级。相比之下WAVPCM 编码就是裸的采样数据只需要解析一个 44 字节的文件头后面的 data 块就是一个一个采样点直接送进 I2S 的 TX 寄存器就能发声。所以这个实验里音乐源文件的最佳选择是 WAV推荐使用 16bit、采样率 22.05kHz 或 44.1kHz 的立体声 PCM 编码。如果你只有 MP3可以用格式工厂、Audacity 或者 ffmpeg 转成 WAV命令行转换示例ffmpeg -i input.mp3 -ar 44100 -ac 2 -sample_fmt s16 output.wav这里-ar 44100表示采样率 44100Hz-ac 2表示双声道-sample_fmt s16表示有符号 16bit 整数。越低的采样率数据传输压力越小但音质也会相应下降。我测试下来 22.05kHz/16bit/单声道在 SPI 模式的 SD 卡下非常流畅44.1kHz 立体声在优化读取逻辑后也能稳定运行。2.2 SD 卡 SPI 模式初始化与 FATFS 移植SD 卡 SPI 模式的初始化流程比 SDIO 模式简单但有一个关键点初始化期间必须使用低速 SPI 时钟通常在 400kHz 以下初始化成功后再切到高速。标准流程是发送至少 74 个时钟脉冲让 SD 卡完成上电复位。CMD0GO_IDLE_STATE让卡进入 SPI 模式。CMD8SEND_IF_COND确认卡支持 SDHC 2.0 协议同时获取电压范围信息。CMD55 ACMD41SD_SEND_OP_COND循环发送直到卡返回 ready。CMD58READ_OCR读取 OCR 寄存器判断卡类型SDHC/SDXC。读取 CSD 寄存器计算扇区容量。我用标准库 FatFs 移植时底层只实现了几个接口函数disk_initialize、disk_read、disk_write本实验只读不写、disk_ioctl和get_fattime。其中disk_ioctl里必须实现GET_SECTOR_COUNT和GET_SECTOR_SIZE这两个命令否则f_mount会返回错误。FatFs 的配置在ffconf.h中本实验的关键配置项如下配置项取值说明_MAX_SS512标准 SD 卡扇区大小_USE_LFN1开启长文件名方便放中文歌名_FS_RPATH0不需要相对路径关闭节省 RAM_FS_READONLY1本实验只读文件不写文件_VOLUMES1只挂载一个卷有了 FatFs 之后挂载和打开文件就非常方便了FATFS fs; FIL fil; FRESULT fr; fr f_mount(fs, 0:, 1); if (fr ! FR_OK) { // 挂载失败一般是 SD 卡初始化失败或格式不对 return ERROR_SD_MOUNT_FAILED; } fr f_open(fil, 0:/music.wav, FA_READ); if (fr ! FR_OK) { return ERROR_FILE_NOT_FOUND; }注意 SD 卡格式必须为 FAT32实验前先在电脑上把卡格式化一遍别用 exFAT 和 NTFSFatFs 默认不支持。另外卡里建议只放 WAV 文件不要放太多零碎小文件文件碎片多了读取时会频繁跳转扇区对播放流畅度有影响。3. PCM5102 驱动与 I2SDMA 播放核心流程3.1 PCM5102 工作模式配置FMT 和 SCK 别乱接PCM5102 的工作模式主要由 FMT、SCK 两个引脚决定不同组合对应不同数据格式和时钟模式。在这个实验里只需按如下接法FMT 接 GNDI2S 标准格式也就是 Phillips I2S 协议BCK 的下降沿锁存数据LRCK 低电平表示左声道高电平表示右声道。这种格式和 STM32 的 I2S 外设“标准模式”完全对应。SCK 接 GND芯片内部 PLL 从 BCK 恢复系统时钟。此时 BCK 会自动倍频PCM5102 内部能正常工作。实测 SCK 悬空偶尔也会出声但容易出现启动时无声或爆音建议还是老实接地。FLT 和 DEMP 这两个引脚在绝大多数场景下都接 GND分别表示“正常滤波模式”和“不使用去加重滤波器”。XMS 是软静音控制接低电平解除静音我见过有人把 XMS 悬空导致偶尔不响的这个引脚千万别悬空。PCM5102 的输出是模拟差分信号通常模块上已经带了 LOUT/ROUT 接口直接接 3.5mm 耳机座或有源音箱的 LINE IN 就可以。如果你想接到后级功放建议在输出端串一个 10uF 电解电容隔直这个实验里直接用模块输出即可。3.2 I2S2 外设初始化与时钟源选择STM32F103 的 I2S2 和 SPI2 共用一套外设引脚和寄存器。也就是说当你想用 I2S2 时就不能再用 SPI2。初始化代码里要先把 SPI2 复位然后配置为 I2S 模式GPIO_InitTypeDef GPIO_InitStructure; I2S_InitTypeDef I2S_InitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // 2. 复用功能开漏/推挽输出注意是复用推挽 GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); // 3. 复位 SPI2再配置为 I2S SPI_I2S_DeInit(SPI2); I2S_InitStructure.I2S_AudioFreq I2S_AudioFreq_44k; // 根据 WAV 头实际采样率动态调整 I2S_InitStructure.I2S_Standard I2S_Standard_Phillips; // I2S 标准格式 I2S_InitStructure.I2S_DataFormat I2S_DataFormat_16b; // 16bit 采样 I2S_InitStructure.I2S_CPOL I2S_CPOL_Low; I2S_InitStructure.I2S_Mode I2S_Mode_MasterTx; // 主机发送模式 I2S_InitStructure.I2S_MCLKOutput I2S_MCLKOutput_Disable; // F103 不需要输出 MCLKPCM5102 自恢复 I2S_Init(SPI2, I2S_InitStructure); // 4. 使能 I2S I2S_Cmd(SPI2, ENABLE);这里有一个新手很容易忽略的点I2S 的时钟源选择。F103 的 I2S2 时钟源默认来自 PLLCLK但也可以通过RCC_I2S2CLKConfig设置为 SYSCLK。在我的工程里系统时钟跑在 72MHzI2S2 的时钟源直接设为 SYSCLK这样分频器可以比较精确地生成 44.1kHz 所需的位时钟RCC_I2S2CLKConfig(RCC_I2S2CLKSource_SYSCLK);为什么强调这一步因为如果时钟源配置不对I2S 外设计算出来的分频系数会偏差很大播放出来的声音不是变调就是刺啦刺啦的噪声。你可以在调试模式下读取 SPI2 的 I2SPR 寄存器确认分频值是否符合预期。另外I2S_AudioFreq支持 8k/11.025k/16k/22.05k/32k/44.1k/48k/96k 等常见值。如果你播放的 WAV 采样率不在标准表里标准库的I2S_Init会尝试按接近值计算但生成的频率可能与实际采样率有偏差听感上表现为音调轻微偏移。所以还是建议大家做实验时固定使用 44.1kHz 或 22.05kHz 的 WAV 文件。3.3 WAV 文件头解析与读取播放 WAV 之前必须先解析文件头。WAV 文件头结构固定为 44 字节在未使用扩展块的情况下我用如下结构体去读取typedef struct { uint32_t chunkID; // RIFF uint32_t chunkSize; uint32_t format; // WAVE uint32_t subchunk1ID; // fmt uint32_t subchunk1Size; uint16_t audioFormat; // 1 表示 PCM uint16_t numChannels; // 声道数1 或 2 uint32_t sampleRate; // 采样率 uint32_t byteRate; // 每秒字节数 uint16_t blockAlign; uint16_t bitsPerSample; // 16 uint32_t subchunk2ID; // data uint32_t subchunk2Size; // 数据块大小 } WAV_HEADER;在代码里从文件开头读 44 字节强转成这个结构体然后校验 chunkID 是否为 0x46464952ASCII RIFF、format 是否为 0x45564157WAVE、audioFormat 是否为 1。然后根据sampleRate和numChannels配置 I2S 的采样率和通道数。注意这个结构体字节对齐一定要按 1 对齐否则字段读取全乱我用的是__packedtypedef __packed struct { // 字段同上 } WAV_HEADER;文件头解析完成后文件指针就位于 data 数据块的起始位置直接开始循环读取播放即可。3.4 DMA 双缓冲播放避免卡顿的关键如果直接在主循环里轮询SPI_I2S_SendData发送数据播放会非常卡因为每次读写寄存器都要等信号量中间夹杂着 SD 卡读取的延迟会让 I2S 的 TX FIFO 长时间为空声音自然就断断续续。正确做法是使用 DMA让数据从内存自动搬运到 I2S 的数据寄存器同时 CPU 去读 SD 卡填充下一块缓冲区。DMA 通道选择I2S2 的 TX 对应 DMA1 通道 5。配置如下DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)SPI2-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)audioBuffer; // 内存缓冲区 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize AUDIO_BUF_SIZE / 2; // 单位半字 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure);这里有一个容易踩的坑DMA_BufferSize的单位是“数据传输次数”因为外设数据大小是半字16bit所以缓冲区字节数要除以 2。比如缓冲区是 2048 字节那 BufferSize 就填 1024。DMA 配置好之后播放循环用双缓冲交替uint16_t audioBuffer[2][AUDIO_BUF_SIZE / 2]; uint8_t bufIndex 0; UINT bytesRead; // 先读满第一块 f_read(fil, audioBuffer[0], AUDIO_BUF_SIZE, bytesRead); DMA_SetCurrDataCounter(DMA1_Channel5, bytesRead / 2); DMA_Cmd(DMA1_Channel5, ENABLE); while (1) { // 等待当前 DMA 传输完成 while (DMA_GetFlagStatus(DMA1_FLAG_TC5) RESET); DMA_ClearFlag(DMA1_FLAG_TC5); // 切换缓冲区预读下一块 bufIndex ^ 1; f_read(fil, audioBuffer[bufIndex], AUDIO_BUF_SIZE, bytesRead); if (bytesRead 0) { break; // 文件读完了 } // 重新配置 DMA 内存地址和长度 DMA_Cmd(DMA1_Channel5, DISABLE); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)audioBuffer[bufIndex]; DMA_InitStructure.DMA_BufferSize bytesRead / 2; DMA_Init(DMA1_Channel5, DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); }双缓冲的思路是DMA 正在发送 buffer 0 时CPU 去读 SD 卡填充 buffer 1反过来 DMA 发送 buffer 1 时CPU 填充 buffer 0。这样 SD 卡读取的延迟就被完全隐藏了。缓冲区大小我推荐AUDIO_BUF_SIZE取 2048 字节每块 1K 采样点44.1kHz 下约 23ms 音频太小容易下溢太大占用 RAM。F103ZET6 有 64KB SRAM两个 2KB 缓冲区完全无压力。4. 实测问题与排查技巧实录4.1 播放流程概览整个实验上电后的运行流程是这样系统初始化时钟、GPIO、DMA→ SD 卡 SPI 初始化 → FATFS 挂载 → 打开 WAV 文件 → 解析 WAV 头 → 根据采样率重配 I2S → DMA 启动 → 循环读取播放。我加了一个按键控制按一下开始播放再按一下暂停再按继续。加了 LED 指示当前状态方便调试时判断程序卡在哪个环节。我对这一段发布一个通用流程图逻辑只描述逻辑不依赖具体工具初始化硬件 → 挂载文件系统 → 解析音频文件 → 启动 DMA 播放 → 持续读取文件数据 → 播放完成回初始状态。4.2 问题 1SD 卡初始化失败f_mount 返回 FR_NOT_READY现象程序卡在挂载文件系统返回错误码 3FR_NOT_READY。排查过程先用逻辑分析仪查看 SD 卡 CS、SCK、MOSI 的时序发现 CMD0 发出后卡没有返回 0x01。进一步检查发现SD 卡模块的 CS 引脚虽然接在 PA4但初始化代码里没有把 PB12 的 SPI2_NSS 拉高导致卡处于不活跃状态。SD 卡 SPI 模式下CS 必须由 GPIO 软件控制不能用 SPI 硬件的 NSS。解决把 CS 对应的 GPIO 配置为推挽输出在发送任何 SPI 命令之前拉高 CS只有在发送命令期间才拉低。另外SPI 初始化时先设置 256 分频低速等 SD 卡识别成功后再切换到 2 分频高速。这是 SD 卡 SPI 模式的标准操作顺序不能反。还有一个常见原因是供电不稳。SD 卡在初始化瞬间电流较大如果用开发板的 3.3V LDO 供电可能因为电压跌落导致卡无响应。解决方法是尽量用独立稳压模块给 SD 卡单独供电或者在 VCC 与 GND 之间加一个 100uF 电解电容和 0.1uF 陶瓷电容。4.3 问题 2有声音但是严重破音/杂音现象能出声但声音里有明显的“呲呲”噪声低频部分还伴有周期性爆音。排查过程这种问题通常有三类原因。第一类是数据格式不匹配PCM5102 的 FMT 引脚如果接成右对齐格式而代码里配的是 I2S 标准格式就会出现明显噪声因为左右声道采样数据错位了。第二类是 I2S 位宽配置错误如果 WAV 是 24bit但 I2S_DataFormat 配成 16bitDMA 每传输一个半字后 I2S 会丢掉 8 位表现为音调升高加噪声。第三类就是电源噪声PCM5102 是模拟器件如果 VIN 和 GND 走线太长或和数字信号共地模拟输出容易耦合高频噪声。解决步骤先确认 FMT 接地确保 PCM5102 工作在 I2S 标准格式。然后用调试器读取 WAV 头的 bitsPerSample 字段确认与 I2S 配置一致。最后在 PCM5102 的 VIN 引脚附近加一个 10uF 电解电容和 100nF 陶瓷电容去耦实测对噪声改善非常明显。4.4 问题 344.1kHz 立体声播放卡顿声音断续现象22.05kHz 单声道正常但换成 44.1kHz 立体声后每隔 1-2 秒就卡一下。排查过程SPI 模式读 SD 卡的最高速度在 18MHz 时钟下可以跑到 2MB/s 左右但实际受 FATFS 文件簇大小、文件碎片、SPI 传输效率影响有效吞吐可能只有 800KB/s 左右。而 44.1kHz/16bit/立体声的实时数据率是 44100 * 2 * 2 176.4KB/s理论上是够的。卡顿的原因是f_read在读取过程中有时会被文件系统内部操作阻塞比如 FAT 表更新、扇区跨边界读取等造成缓冲区下溢。解决把双缓冲区从 2KB 增大到 4KB并开启 FatFs 的_FS_EXFAT不行FAT32 卡不需要这个。更有效的做法是每次f_read一次性读取 4KB 或 8KB减少文件系统调用的次数。我自己调通后用 4096 字节的双缓冲44.1kHz 立体声可以稳定播放不卡顿。如果换了多张 SD 卡仍然卡顿可以考虑用 DMA 方式配合 SPI 读取 SD 卡这样f_read的耗时会更短。F103ZET6 上 SPI1 的 RX DMA 通道是 DMA1_Channel2把disk_read的底层改为 SPI1 DMA 收发CPU 只在 DMA 中断里做状态切换能进一步降低读取延迟。4.5 常见问题速查表现象可能原因解决方式f_mount 返回 FR_NOT_READYCS 引脚没有软件控制用 GPIO 拉高拉低 CS不能依赖硬件 NSSf_open 返回 FR_NO_FILE文件名不对或格式不对确认卡为 FAT32文件名为 8.3 格式或开启 LFN有声音但破音I2S 数据格式/位宽不匹配检查 FMT 引脚、I2S_DataFormat、WAV 头位深完全无声XMS 悬空/SCK 接法错误XMS 接 GNDSCK 接 GND音调偏高或偏低I2S 采样率与 WAV 不一致用 WAV 头里的 sampleRate 动态配置 I2S44.1kHz 卡顿SD 卡读取速度不足增大缓冲区、单次多读数据、改用 SPIDMA5. 一点个人经验和后续扩展方向这个实验做完之后我对 F103 的 I2S 外设和 DMA 配合方式有了更深的理解。以前用开发板例程直接跑 MP3 播放很多细节被封装掉了总觉得音频播放是个“很简单的事”。这次用 WAV PCM5102 手动把链路搭起来才发现真正的难点不在代码能跑通而在于理解数据是怎么流动的文件系统按扇区读数据DMA 按半字搬数据I2S 按位时钟发数据PCM5102 按帧格式还原模拟信号。任何一层的节奏不同步最后听到的都是噪声。如果你想在这个实验基础上继续深入我有三个推荐方向。第一加一个按键切歌和 OLED 显示当前歌名把 FatFs 的文件遍历功能用起来这样就是一个完整的音乐播放器。第二把读取端从 SPI 模式改为 SDIO 模式F103ZET6 自带 SDIO 控制器读写速度能提升数倍可以流畅播放 96kHz/24bit 的高规格音频。第三加一个录音功能用 I2S 的 RX 方向接一个麦克风如 INMP441把音频数据写入 SD 卡这样就形成了完整的录音回放系统。这些扩展都不需要更换主控只要在现有代码基础上加模块算是性价比很高的进阶路径。最后再说一个容易被忽略的细节SD 卡和 PCM5102 的 GND 一定要与 STM32 共地。我之前有一次用 USB 供电调试PCM5102 的地和板子地之间接触不良导致输出一直有 50Hz 交流声检查了半天才发现是杜邦线虚接。做音频实验地线的可靠性往往比代码更重要。本文还有配套的精品资源点击获取