STM32 DAC如何实现外部Flash WAV音频的DMA循环播放且低RAM占用?
STM32F1 DAC播放外部Flash WAV:兼顾RAM占用与任务响应的方案
问题描述
我已经在STM32F1的DAC上实现了WAV音频播放功能。由于音频数据量较大(比如44.1K采样率、8位采样深度),内部Flash存储空间不足,因此将音频文件存储在SPI外部Flash中,采用逐缓冲区(每次通常512字节)读取并以DMA正常模式喂入DAC的方案。该方案虽能正常工作,但MCU会陷入缓冲区填充循环无法处理其他任务(终止循环则音频停止);若将所有音频数据读入数组并采用DMA循环模式播放,又会占用过多RAM,对于大文件并不实用。请问是否存在兼顾两者的解决方案?
当前实现代码
while(0 < bytes_last) { int blksize = (bitsPerSample == 8)? MIN(bytes_last, BUFSIZE / 2) : MIN(bytes_last, BUFSIZE); uint32_t bytes_read = 0; if (HAL_SPI_Receive(&SPI_FLASH_HANDLE, fileBuffer, blksize, 1000) != HAL_OK) { /* Transfer error in transmission process */ Error_Handler(); } int i = 0; while(i < BUFSIZE){ if(fileBuffer[i] != 0 && fileBuffer[i] != 0xFF){ bytes_read++; i++; }else{ break; } } uint16_t numSamples = bytes_read / bytesPerSample / channels; int16_t *pInput = (int16_t *)fileBuffer; uint16_t *pOutput = (uint16_t *)dmaBuffer[dmaBank]; prepareData(channels, numSamples, pInput, pOutput); // wait for DMA complete while(flg_dma_done == 0) { __NOP(); } HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1); flg_dma_done = 0; HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)dmaBuffer[dmaBank], numSamples, DAC_ALIGN_12B_R); dmaBank = !dmaBank; bytes_last -= blksize; }; FLASH_CS_HIGH(); while(flg_dma_done == 0) { __NOP(); }
解决方案:双缓冲区+异步DMA+中断回调
核心是用双缓冲区交替工作,配合SPI和DAC的DMA异步操作+中断回调,彻底消除阻塞循环,让MCU能同时处理音频播放和其他任务,同时RAM占用仅为两个缓冲区的大小。
具体实现步骤
- 双缓冲区架构:保留原有的
dmaBuffer[2]作为DAC输出缓冲,新增spiBuffer[2](或复用现有fileBuffer)作为外部Flash读取缓冲,两个缓冲区分别负责"正在播放"和"预加载数据"。 - SPI异步读取:替换阻塞式
HAL_SPI_Receive为HAL_SPI_Receive_DMA,让SPI在后台读取数据,读完触发中断回调处理数据。 - DAC中断联动:在DAC的DMA完成回调中,自动切换到预加载好的缓冲区继续播放,同时触发下一段SPI数据读取。
- 移除阻塞等待:删除所有
while(flg_dma_done == 0)这类循环,所有流程通过中断回调驱动。
关键代码示例
// 全局变量:标记当前播放/待加载的缓冲区 uint8_t dma_playing_bank = 0; uint8_t spi_loading_bank = 1; uint32_t bytes_remaining = 0; uint8_t cmd_buffer[4]; // 存储外部Flash读命令+地址 // 初始化播放:加载第一块数据并启动DAC void Audio_Play_Start(uint32_t total_audio_bytes) { bytes_remaining = total_audio_bytes; // 配置外部Flash读地址命令(根据你的Flash型号调整) cmd_buffer[0] = 0x03; // 普通读命令 cmd_buffer[1] = (audio_start_addr >> 16) & 0xFF; cmd_buffer[2] = (audio_start_addr >> 8) & 0xFF; cmd_buffer[3] = audio_start_addr & 0xFF; // 先发送读命令,再启动DMA接收第一块数据 FLASH_CS_LOW(); HAL_SPI_Transmit_DMA(&SPI_FLASH_HANDLE, cmd_buffer, sizeof(cmd_buffer)); } // SPI DMA发送完成回调:开始接收数据 void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI_FLASH_HANDLE.Instance) { int blksize = (bitsPerSample == 8) ? MIN(bytes_remaining, BUFSIZE/2) : MIN(bytes_remaining, BUFSIZE); HAL_SPI_Receive_DMA(&SPI_FLASH_HANDLE, spiBuffer[spi_loading_bank], blksize); } } // SPI DMA接收完成回调:处理数据并准备DAC缓冲 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI_FLASH_HANDLE.Instance) { FLASH_CS_HIGH(); // 过滤有效数据(和原代码逻辑一致) uint32_t bytes_read = 0; while(bytes_read < BUFSIZE && spiBuffer[spi_loading_bank][bytes_read] != 0 && spiBuffer[spi_loading_bank][bytes_read] != 0xFF) { bytes_read++; } // 转换为DAC可用格式 uint16_t numSamples = bytes_read / bytesPerSample / channels; prepareData(channels, numSamples, (int16_t*)spiBuffer[spi_loading_bank], dmaBuffer[spi_loading_bank]); // 首次播放:直接启动DAC DMA if (!HAL_DAC_GetState(&hdac, DAC_CHANNEL_1)) { HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)dmaBuffer[spi_loading_bank], numSamples, DAC_ALIGN_12B_R); dma_playing_bank = spi_loading_bank; spi_loading_bank = !spi_loading_bank; } // 加载下一块数据(如果还有剩余) if (bytes_remaining > 0) { bytes_remaining -= blksize; audio_start_addr += blksize; // 更新读地址命令 cmd_buffer[1] = (audio_start_addr >> 16) & 0xFF; cmd_buffer[2] = (audio_start_addr >> 8) & 0xFF; cmd_buffer[3] = audio_start_addr & 0xFF; FLASH_CS_LOW(); HAL_SPI_Transmit_DMA(&SPI_FLASH_HANDLE, cmd_buffer, sizeof(cmd_buffer)); } } } // DAC DMA转换完成回调:切换缓冲区继续播放 void HAL_DAC_ConvCpltCallback(DAC_HandleTypeDef *hdac) { if (hdac->Instance == DAC_CHANNEL_1.Instance) { HAL_DAC_Stop_DMA(hdac, DAC_CHANNEL_1); // 切换到预加载好的缓冲区 dma_playing_bank = !dma_playing_bank; HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t*)dmaBuffer[dma_playing_bank], get_current_sample_count(), DAC_ALIGN_12B_R); } }
额外优化建议
- 外部Flash地址自动递增:若你的SPI Flash支持连续读模式(如部分型号的0x0B命令),可省去每次更新地址的步骤,减少MCU操作。
- 缓冲区大小对齐:将SPI读取块大小与DAC DMA缓冲区大小设为一致,简化数据处理逻辑。
- 中断优先级配置:给DAC和SPI的DMA中断设置较高优先级,确保音频播放不被低优先级任务打断。
内容的提问来源于stack exchange,提问作者Travis Su
相关产品推荐
相关产品推荐

