STM32 I2S DMA TxHalfCplt回调仅触发一次问题排查
STM32 I2S DMA双缓冲TxHalfCplt回调仅触发一次问题排查
问题现象
- 采用STM32 I2S DMA双缓冲方案播放固定格式WAV文件,启动传输后
HAL_I2S_TxHalfCpltCallback仅执行一次就停止触发,仅HAL_I2S_TxCpltCallback能持续正常工作 - 缓冲区前半部分数据无法被更新,残留旧内容通过DAC播放,后半部分可正常填充新数据
- UART调试仅能捕获第一次
TxHalfCpltCallback的输出日志,后续无相关触发记录 - 已在CMSIS V2 RTOS环境中将音频任务优先级设为最高
环境说明
- 硬件:STM32系列芯片(带I2S、DMA外设)
- 软件框架:HAL库 + CMSIS V2 RTOS
- 音频方案:I2S DMA双缓冲循环播放
代码片段
任务代码
uint32_t task_stat = STOP_TASK; uint32_t StartAddr = 0x00000; // 音频数据起始地址 uint32_t EndAddr = 0x4FFFF; // 音频数据结束地址 uint32_t currentAddr = 0x00000; uint32_t flashOffsetHalfBuffer = (AUDIO_BUFF_SIZE / 2) * sizeof(uint16_t); // 半缓冲字节数 uint32_t flashOffsetFullBuffer = AUDIO_BUFF_SIZE * sizeof(uint16_t); // 全缓冲字节数 /* 无限循环 */ for(;;) { if(osMessageQueueGet(test_TaskQueueHandle, &task_stat, NULL, osWaitForever) == osOK){ printf("TEST STARTED\r\n"); // 预填充全缓冲区 initAudioData(StartAddr, dacData); currentAddr = StartAddr + flashOffsetFullBuffer; FLAG_I2S_DATA_READY = 1; // 启动DMA传输 HAL_I2S_Transmit_DMA(&hi2s1, (uint16_t *)dacData, DMA_MAX(AUDIO_BUFF_SIZE / 2)); } else { printf("ERR: TASK HANDLE MSG QT\r\n"); } while (task_stat == RUN_TASK) { if( FLAG_I2S_DATA_READY == 0 ){ if((currentAddr + flashOffsetHalfBuffer) <= EndAddr){ if(processAudioData(currentAddr, dacPtr) == STAT_OK){ currentAddr += flashOffsetHalfBuffer; FLAG_I2S_DATA_READY = 1; } } else { // 循环播放,重置地址到起始位置 currentAddr = StartAddr; if(processAudioData(currentAddr, dacPtr) == STAT_OK){ currentAddr += flashOffsetHalfBuffer; FLAG_I2S_DATA_READY = 1; } } } if(osMessageQueueGet(test_TaskQueueHandle, &task_stat, NULL, 1) == osOK){ printf("TASK PAUSE\r\n"); HAL_I2S_DMAStop(&hi2s1); currentAddr = 0x00000; task_stat = STOP_TASK; } } osDelay(1); }
回调函数
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s){ dacPtr = &dacData[AUDIO_BUFF_SIZE/2]; FLAG_I2S_DATA_READY = 0; FLAG_FIRST_HALF_RELOAD = 0; FLAG_SECOND_HALF_RELOAD = 1; // printf("C\r\n"); } void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s){ dacPtr = &dacData[0]; FLAG_I2S_DATA_READY = 0; FLAG_FIRST_HALF_RELOAD = 1; FLAG_SECOND_HALF_RELOAD = 0; // printf("H\r\n"); }
全局定义
// DMA状态标志 volatile uint8_t FLAG_I2S_DATA_READY = 1; volatile uint8_t FLAG_FIRST_HALF_RELOAD = 0; volatile uint8_t FLAG_SECOND_HALF_RELOAD = 0; // 音频DMA缓冲区 uint16_t dacData[AUDIO_BUFF_SIZE]; // 缓冲区指针 static volatile uint16_t *dacPtr = &dacData[0];
问题排查与修复方案
1. DMA传输长度参数错误
当前启动DMA时传入的长度是AUDIO_BUFF_SIZE / 2,这仅为半缓冲区的数据项数。双缓冲方案要求DMA传输整个缓冲区的长度,才能在循环模式下交替触发半完成、完成回调。
修正代码:
// 传入全缓冲区的数据项数(uint16_t类型) HAL_I2S_Transmit_DMA(&hi2s1, (uint16_t *)dacData, AUDIO_BUFF_SIZE);
注:确保AUDIO_BUFF_SIZE是缓冲区中uint16_t类型数据的总个数,而非字节数。
2. 未开启DMA循环模式
若DMA配置为Normal模式,传输完成一次后会自动停止,无法持续触发回调。必须将DMA Tx通道设置为循环模式(Circular Mode)。
- CubeMX配置:找到I2S对应的DMA Tx通道,将
Mode设置为Circular - 代码层面确认:
hdma_spi1_tx.Init.Mode = DMA_CIRCULAR; // 确保此项为循环模式
3. 全局标志的竞态问题
RTOS环境下,中断回调(上下文)与任务上下文同时操作全局标志,容易出现竞态逻辑错误。建议用RTOS信号量替代全局标志,实现安全同步。
修正步骤:
- 定义信号量:
osSemaphoreId_t audioSemaphore;
- 初始化信号量(任务创建前执行):
audioSemaphore = osSemaphoreNew(1, 1, NULL);
- 修改回调函数:
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s){ dacPtr = &dacData[AUDIO_BUFF_SIZE/2]; osSemaphoreRelease(audioSemaphore); } void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s){ dacPtr = &dacData[0]; osSemaphoreRelease(audioSemaphore); }
- 修改任务循环逻辑:
while (task_stat == RUN_TASK) { // 等待回调触发的信号量 if(osSemaphoreAcquire(audioSemaphore, osWaitForever) == osOK){ if((currentAddr + flashOffsetHalfBuffer) <= EndAddr){ if(processAudioData(currentAddr, dacPtr) == STAT_OK){ currentAddr += flashOffsetHalfBuffer; } } else { currentAddr = StartAddr; if(processAudioData(currentAddr, dacPtr) == STAT_OK){ currentAddr += flashOffsetHalfBuffer; } } } // 处理暂停消息 if(osMessageQueueGet(test_TaskQueueHandle, &task_stat, NULL, 1) == osOK){ printf("TASK PAUSE\r\n"); HAL_I2S_DMAStop(&hi2s1); currentAddr = 0x00000; task_stat = STOP_TASK; } }
4. 数据宽度匹配检查
确认I2S的数据宽度配置(如16位)与DMA的数据宽度、缓冲区类型(uint16_t)完全匹配,避免因数据位宽不匹配导致DMA计数错误,进而引发回调触发异常。
内容的提问来源于stack exchange,提问作者Travis Su
相关产品推荐
相关产品推荐

