You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 00:07:06