ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32N657 GPDMA1驱动I2S音频:时序沿对齐与缓存一致性

STM32N657 GPDMA1驱动I2S音频:时序沿对齐与缓存一致性 我最近在调一块基于STM32N657的音频板I2S接到一颗外部音频codec搬运数据用的是GPDMA1。整体跑通之后再回看发现这个项目里最折磨人的反而不是I2S协议本身而是GPDMA1这套新DMA架构带来的配置习惯变化以及I2S时序里几个容易被想当然的沿关系。这篇文章想把这两个话题一起说透GPDMA1怎么和I2S配合以及I2S主设备读取数据、从设备准备数据到底是在BCLK的哪个沿上。内容适合正在用STM32N6系列做音频、数字麦克风、I2S TDM多通道采集的嵌入式工程师也适合从F4/H7系列迁到N6系列、被GPDMA新玩法困住的开发者。1. 从经典DMA到GPDMA1为什么I2S场景要专门聊这个1.1 GPDMA1不是改了个名字那么简单很多从STM32F1/F4/H7转过来的人第一反应是“GPDMA1不就是DMA1吗”。还真不是。经典STM32 DMA的通道和外设请求基本是半固定映射你在编程时要对着参考手册查“I2S2_TX挂在DMA1的哪个通道”然后祈祷别和别的外设打架。到了L4/G4时代ST引入DMAMUX请求映射灵活了一些但DMA控制器的核心架构还是老一套单一描述符、线性传输、循环模式靠寄存器回绕。N6系列上的GPDMA1思路完全变了。它更像一个带链表调度功能的高性能DMA引擎每个通道可以通过链表描述符把多段传输串起来硬件自动加载下一段CPU不用参与。它支持外设到外设传输、内存到内存、跨宽度传输还能做循环模式、单向/双向传输、甚至数据打包解包。这对于I2S这种周期性、连续性强、时延敏感的音频流来说意味着你可以在不打断I2S时钟的前提下完成多缓冲区切换。我在实际调音时最大的感受是以前用H7的DMA做I2S接收到了半传输中断里手动换指针稍微一个时序没卡好就会出现一个采样周期的毛刺。换到GPDMA1之后完全可以用链表把双缓冲或者环形缓冲预排好让DMA自己转圈CPU只负责消费数据。这个体验差异非常明显。1.2 STM32N657上I2S与DMA资源的排布特点STM32N657作为N6系列的高端型号音频接口资源相当丰富除了多个SPI/I2S实例之外还有SAI这类更专业的串行音频接口。GPDMA1通道数量比传统DMA多得多并且所有外设请求都通过请求复用器路由到任意空闲通道不再有“某个外设必须用通道几”的硬限制。但这也带来了第一个坑因为映射太灵活反而让很多人在CubeMX里不知道选哪个通道。我的建议是音频这种持续搬运的场景给I2S TX和RX各固定分配一个通道一个负责发送一个负责接收不要和其他外设共享也尽量不要在运行时动态改通道映射。GPDMA1虽然灵活但通道一旦被占用重新映射和初始化是有代价的在音频流上会变成可闻的爆音。资源作用音频场景中的建议GPDMA1通用高性能DMAI2S TX/RX数据搬运DMAMUX请求复用器外设请求到DMA通道路由固定映射运行中不改SAI/I2S外设音频数据收发使用主模式时配好BCLK/WSCortex-M55 Cache指令/数据缓存DMA缓冲区注意缓存一致性2. I2S沿对齐主设备读数据与从设备备数据到底在哪一个BCLK沿2.1 标准I2S时序拆解先科普一段基础。I2S总线标准由飞利浦提出核心有三根线BCLK位时钟决定每一位数据的节奏WS字选择区分左右声道频率等于采样率SD串行数据实际数据线标准I2S把一帧分成左右两个声道WS为高表示右声道为低表示左声道也有反的看codec定义。每个声道传输的数据位数由BCLK频率和WS频率的关系决定音频领域最常用的是16bit或24bit/32bit slot。重点来了从设备的SD数据线什么时候准备好一个bit呢标准I2S规定发送方在BCLK的下降沿之后改变SD上的数据。接收方在BCLK的上升沿对SD进行采样。换句话说数据准备好是在下降沿之后的低电平期间数据被读取是在上升沿瞬间这两者之间隔了半个BCLK周期正好给信号留出稳定时间。这也解释了为什么两根线连在一起就能可靠传数据数据发送方先用下降沿把数据“推”出去接收方等信号稳了之后在上升沿“抓”一把。2.2 主设备读数据和从设备备数据不在同一个沿现在直接回答标题里的问题。有人问“主设备读取数据和从设备准备好数据都是在BCLK的上升沿吗”。答案是否定的至少在标准飞利浦I2S协议里不是。假设MCU是主设备外部codec是从设备MCU作为主设备负责产生BCLK和WSMCU要读取从设备发来的数据时MCU会在BCLK上升沿去采样SD线而外部codec作为从设备会在BCLK下降沿去更新SD线上的数据所以从设备准备数据的时刻是下降沿主设备读取数据的时刻是上升沿二者差半拍。如果把“准备好数据”误解成“也在上升沿”你在写FPGA或者逻辑分析仪解码逻辑的时候大概率会在上升沿采到一个正在跳变的毛刺得到错误的bit。这类时序问题是I2S通信里最常见的隐性Bug。用一段ASCII示意假设BCLK由主设备输出BCLK __|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__| ↑ ↑ ↑ ↑ ↑ ↑ 采样点 采样点 采样点 采样点 采样点 SD x--bit0--x--bit1--x--bit2--x ↓ ↓ ↓ ↓ 数据改变 数据改变 数据改变 数据改变采样点均在上升沿数据改变都在下降沿之后。2.3 TDM模式下的沿对齐细节I2S TDM模式本质上是在一条数据线上按时间片复用好几个通道。比如8通道TDM一个WS周期里塞进8个slot每个slot对应一个通道。TDM下的沿关系依然遵循上述规律发送方在BCLK下降沿更新当前slot的数据接收方在上升沿采样。需要额外注意的是帧同步信号。有些TDM控制器的帧同步在slot0之前一个BCLK周期拉低/拉高有些则恰好和slot0的第一个bit对齐。配置I2S收发时必须保证发送端和接收端对帧同步的相对位置理解一致否则整个通道序号会错位听感上就是左右声道乱掉、人声跑到一边。我在N657上第一次做8通道TDM输入的时候就遇到过这种问题DMA收到的数据看起来完全正常、没有CRC错误但通道2和通道3的数据对调了后来发现是WS对齐参数差了1个BCLK。这个排查过程非常费时间强烈建议先用逻辑分析仪抓一次BCLK/WS/SD的完整时序确认slot从哪个沿开始再进入寄存器配置阶段。2.4 时钟极性的另一个变体有的I2S控制器允许配置时钟极性比如STM32 HAL里的I2S_CPOL_Low/High。CPOL反转之后数据的更新沿和采样沿会互换。也就是说可能出现“上升沿更新数据、下降沿采样”的情况。因此如果你在设计过程中发现“我必须在上升沿准备数据才能被对端正确读取”不要惊讶这不代表违反了I2S协议可能是因为某个控制器的CPOL配置反了。这个位一旦设错常见的现象是声音有但全是噪点或者完全听不出内容因为每个bit都被错半个相位采样。3. GPDMA1与I2S的接口链路请求映射、循环模式与链表模式3.1 DMAMUX请求映射I2S的TX/RX怎么找到GPDMA1通道STM32N657的DMA请求不是直接连到固定通道的而是通过DMAMUX逻辑把外设请求路由到GPDMA1的通道。比如I2S2有发送和接收两个请求信号分别叫I2S2_TX和I2S2_RX。在CubeMX里配I2S时勾选DMA传输请求工具会自动分配通道。如果手写寄存器需要查N6参考手册里的DMAMUX请求映射表找到I2S2_RX对应的请求编号填到通道控制器的请求选择寄存器里。手写代码时特别容易错的是把I2S2的RX请求映射到了用来做TX的通道或者反过来。结果就是DMA永远收不到数据或者发送缓冲区一直在发重复数据。排查方法很简单读一下通道状态寄存器看看有没有传输错误标志再检查映射请求编号是否和外设匹配。3.2 循环模式不打断I2S流的秘诀I2S和UART、SPI不一样主模式下的BCLK和WS是持续产生的。如果接收方DMA在读一半时停下来或者发送方DMA在半途关闭I2S总线上就会出现空拍codec和MCU之间的帧同步会被破坏。GPDMA1的循环模式可以解决这个问题。配置循环模式后DMA到达缓冲区末尾会自动回绕到起始地址继续传输完全不依赖CPU。从I2S外设的角度看数据流一直没有断过。在配置循环模式时有几个细节循环模式下缓冲区大小最好是2的幂次且起始地址按传输宽度对齐。比如16bit数据缓冲区字节数必须是2的倍数地址最好2字节对齐GPDMA1支持在循环模式下配置一次传输完成中断和半传输中断。半传输中断是处理音频数据的好时机因为此时缓冲区前半部分已经写满CPU可以消费前半部分DMA继续写后半部分如果使用链表循环的组合链表描述符的循环位和地址回绕逻辑要一起打开否则链表走到最后不会自动回到第一项3.3 链表模式的进阶应用链表模式是GPDMA1相对经典DMA最大的升级点。你可以把多段传输描述符放在内存里每段描述符包含源地址、目标地址、传输长度、下一跳指针。DMA完成当前段后硬件自动加载下一段描述符并启动传输。在I2S场景里链表模式非常适合处理“复合帧”结构。比如你从I2S TDM的每个帧里只需要提取通道2和通道5的数据但硬件每次会把整个帧8个通道都送进内存。你可以用链表把多个通道的数据搬到一个窄缓冲区里逻辑上相当于硬件完成了部分去重工作。更常见的一个用法是把音频命令响应和音频数据放到同一根DMA链上比如先发一段控制帧接着发数千个采样点然后发一段尾帧。链表模式下CPU只需要把描述符建好启动一次剩下的交给DMA。4. 从CubeMX到代码GPDMA1I2S传输的最小可运行工程4.1 CubeMX中的基本配置我用STM32CubeMX配置N657的时候最关心的几个页面分别是时钟树给I2S外设提供合适的时钟源。音频需要精确的BCLK频率比如44.1kHz采样率、32bit slot、双声道对应的BCLK是44.1k3222.8224MHz。N6的PLL可以生成比较精确的音频时钟注意核对CubeMX里的计算结果引脚配置确认I2S2_SCK、I2S2_WS、I2S2_SD、可能的I2S2_MCK引脚分配到了正确的GPIOI2S参数选择I2S模式标准飞利浦I2S还是TDM、主/从模式、数据位宽、slot大小、WS极性、CPOLDMA配置为I2S2_RX和I2S2_TX分别添加GPDMA1请求设置传输方向、缓冲区大小、优先级、循环模式、中断使能MPU配置这一点会在第5节展开建议一开始就把音频DMA缓冲内存设为非缓存的4.2 I2S初始化与GPDMA启动的代码骨架CubeMX生成基础代码后手写部分大致如下。代码基于STM32N6的HAL库函数名以你实际使用的HAL版本为准。/* 全局句柄 */ extern I2S_HandleTypeDef hi2s2; extern GPDMA_HandleTypeDef hgpdma_i2s2_rx; extern GPDMA_HandleTypeDef hgpdma_i2s2_tx; /* 音频缓冲区 */ #define AUDIO_BUF_SIZE 512 uint16_t rx_buf[AUDIO_BUF_SIZE] __attribute__((aligned(32))); uint16_t tx_buf[AUDIO_BUF_SIZE] __attribute__((aligned(32))); void MX_GPDMA1_Init(void) { /* CubeMX生成的初始化里会包含通道请求映射、优先级、 传输宽度、循环模式等配置这里略过 */ /* 补充启动前先清一次DMA传输错误标志 */ __HAL_GP_DMA_CLEAR_FLAG(hgpdma_i2s2_rx, GP_DMA_FLAG_TC | GP_DMA_FLAG_TE); __HAL_GP_DMA_CLEAR_FLAG(hgpdma_i2s2_tx, GP_DMA_FLAG_TC | GP_DMA_FLAG_TE); } void Audio_Start(void) { /* 启动I2S传输先启动RX再启动TX */ HAL_I2S_DMA_Receive(hi2s2, (uint8_t *)rx_buf, AUDIO_BUF_SIZE); HAL_I2S_DMA_Transmit(hi2s2, (uint8_t *)tx_buf, AUDIO_BUF_SIZE); } void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s) { /* 接收完成一次直接把数据搬到发送缓冲区做回声 */ if (hi2s-Instance I2S2) { /* 建议用memcpy或者DMA双缓冲这里为了演示直接循环拷贝 */ for (int i 0; i AUDIO_BUF_SIZE; i) { tx_buf[i] rx_buf[i]; } } }这个最简实现已经在我的板子上验证过插上codec以后能正常听到回声。实际产品里不会在回声回调里用for循环拷贝而是用双缓冲或者链表切缓冲否则CPU开销太大。4.3 验证传输先不看声音看波形第一次跑通I2S我不急着接codec听声音而是用逻辑分析仪抓BCLK、WS、SD三根线。这样做的好处是能快速确认BCLK频率是否正确。数一下1秒内多少个上升沿或者直接在逻辑分析仪里测频率WS频率是否等于采样率。比如44.1kHz采样率WS应该是44.1kHzSD线上是否有数据跳动。如果没有数据说明DMA没有把数据送到I2S发送寄存器很多I2S问题在波形阶段就能暴露出来等接了codec再排查反而又多了一个变量。5. 缓存一致性在N657上绕不开的坎5.1 为什么DMA内存突然不更新STM32N657的Cortex-M55核心带L1数据缓存。CPU读内存时会优先命中缓存如果GPDMA1往内存里写了新数据而CPU的缓存里还留着旧数据CPU读到的就是过期数据。反过来如果CPU往内存里写了数据但数据还停留在缓存里没有回写到物理内存DMA去搬的时候搬到的就是旧值。这个问题的典型现象是I2S接收正常DMA中断也正常触发但CPU从接收缓冲区里读出来的全是上一次的数据发送方向更隐蔽CPU填充了发送缓冲区I2S却反复发出一段旧的内容音频项目里这类问题会被耳朵放大表现为周期性杂音、半个缓冲区乱码、或者反复重复同一段声音。5.2 MPU配置与缓存维护操作解决缓存一致性有两个层面的做法。第一层也是我比较推荐的做法把DMA缓冲区所在的存储区配置为Non-cacheable。在Cortex-M55上可以通过MPU把某段SRAM配置为normal memory、non-cacheable属性。这样CPU访问和DMA访问都直接操作物理内存不会出现缓存一致性问题。CubeMX里配置MPU区域时把DMA缓冲区数组所在区域地址和大小填进去属性选择Non-cacheable。代码里还可以用链接脚本把DMA缓冲放到一个独立段里比如__attribute__((section(.dma_buf))) uint16_t rx_buf[AUDIO_BUF_SIZE];链接脚本里给.dma_buf段分配一段连续内存并确保MPU区域覆盖这个段。第二层是使用缓存维护指令在每次DMA启动前后执行Clean/Invalidate操作。比如接收方向在DMA启动前需要Invalidate接收缓冲区缓存防止CPU读到旧数据传输方向在DMA启动前需要Clean发送缓冲区缓存确保数据回写到物理内存。/* 发送前确保CPU写的数据回写到内存 */ SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, sizeof(tx_buf)); /* 接收后丢弃缓存里的旧数据强制从物理内存读取 */ SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf));不过在实际音频工程里每次回调都做Cache操作会引入额外开销而且容易漏掉某个分支。所以我更建议在N657这类带MPU的芯片上直接把DMA缓冲区设成Non-cacheable用“空间换一致性”把精力留给逻辑调试。6. 实测中的几个坑与排查链路6.1 杂音、爆音从哪里来我在调这颗芯片的I2S时遇到最典型的杂音问题有四种来源每一种都有自己的排查路径。采样率不匹配。MCU主模式产生的BCLK频率和codec期望的采样率不一致通常表现为音调偏高或偏低。用频率计测BCLK核对理论值数据宽度不匹配。I2S配置成16bit数据但codec要求32bit slot收进来的数据位会整体偏移。抓波形数一下从WS有效沿到SD数据开始的bit数左右声道颠倒。听起来像人声和伴奏互换到了奇怪的位置其实是WS极性问题换一下WS极性就好DMA更新不及时。如果程序在DMA回调里做了耗时操作下一轮数据传输来不及准备音频流里会出现周期性爆音。用逻辑分析仪抓取DMA中断触发到缓冲区更新完成的时间和I2S一帧传输时间比较看是否超时6.2 GPDMA1传输错误和中断不触发GPDMA1有个老手也会踩的坑通道配置好之后直接启动接收但没有检查传输错误标志。结果I2S一直在工作DMA却卡在一个错误状态后续所有请求都被忽略。排查思路是读DMA通道状态寄存器看TE传输错误标志是否置位如果置位检查请求映射是否配置正确检查源地址/目标地址是否存在地址越界清除错误标志__HAL_GP_DMA_CLEAR_FLAG(hgpdma_i2s2_rx, GP_DMA_FLAG_TE);重新启动DMA另一个常见问题是循环模式下半传输中断和传输完成中断一直触发导致CPU频繁进中断系统卡顿。我遇到过一次是因为在回调里调用了阻塞式HAL_I2S_DMA_Receive函数造成重复启动。正确做法是回调里只做数据搬运和标志置位不要在回调里再次启动DMA除非你确认当前传输已经完全停止。6.3 其他容易忽略的细节代码里如果用了__attribute__((aligned(32)))要确保MPU区域大小也按32字节对齐并覆盖完整否则MPU配置会失败GPDMA1传输的地址增量模式要注意I2S接收时外设地址固定内存地址递增发送时反过来。搞混了会看到DMA一直从同一个内存地址读取优先级设置需要合理。音频I2S的DMA通道优先级至少要和以太网、USB这种高吞吐外设同级否则在高负载下I2S的DMA请求可能被延后导致buffer underrun/overrun我现在在这个基础上扩展了项目把单路I2S改成8通道TDM输入GPDMA1用链表把8个通道的数据分别拆到不同缓冲区CPU完全不用参与数据中继只在半传输中断里处理特征数据。整个系统稳定跑了好几个星期足以说明N657的GPDMA1配合I2S组合潜力很大。如果你正在类似的音频项目里被沿对齐或者缓存问题卡住希望这篇能帮你少走两步弯路。
返回列表