ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA实战:用VHDL实现I2S音频接口的完整指南

FPGA实战:用VHDL实现I2S音频接口的完整指南 简介本资源是一套基于VHDL实现I2S数字音频接口的完整工程代码与配套文档面向FPGA开发工程师、嵌入式音频系统设计者及数字电路课程学习者解决数字音频设备间高保真数据传输的硬件接口设计问题。压缩包共79个文件含42个VHDL源文件覆盖时钟生成、帧同步控制、数据移位寄存器、复位逻辑等核心模块、22个说明类TXT文档含接口时序分析、配置参数注释及第三方IP引用说明、4个GIF时序图直观展示输入/输出信号波形关系以及PDF与DOC格式的设计规范与接口协议摘要整体大小1.51MB。资源已获190人次学习下载提供可综合、可仿真、可直接集成至Xilinx或Intel FPGA项目的模块化代码结构并包含OpenCores开源I2S IP的适配版本与多版本归档如i2s_rel1_1.zip、i2s_interface.tar.gz显著降低音频子系统开发门槛。 如果你是在FPGA项目里接过音频芯片大概率见过这类目录名一个叫“I2S.rar”的压缩包解压出来是一套VHDL源码里面有发送器、接收器、时钟分频注释还得靠猜。标题里那一串“I2S / VHDL / 音频 / 接口”本质上就是一整套用VHDL实现I2S音频总线接口的工程。这类工程很适合用来吃透两个东西一个是I2S协议本身的时序逻辑另一个是FPGA里做音频数据搬运的常见套路。这篇文章就以这类工程为线索从协议层一路拆到RTL代码、仿真验证、上板调试最后再说说TDM、异步FIFO这些进阶话题。适合正在用FPGA接DAC/ADC/编解码器、或者刚接触音频接口设计的开发者参考。1. I2S协议到底传了什么三根线、帧结构与时序边沿1.1 三根线的分工与主从关系I2SInter-IC Sound是飞利浦定义的一种数字音频总线用途很简单在芯片之间传音频采样数据比如FPGA往DAC送音乐、从ADC收麦克风数据、或者和编解码器互通。它最核心的优势就是线少标准I2S就三根线BCLKBit Clock位时钟每个时钟周期对应一个数据位频率也就是采样率乘以位深乘以声道数。WSWord Select字选择标志当前正在传左声道还是右声道数据低电平通常是左声道、高电平是右声道。有的芯片定义相反要留意。SDSerial Data串行数据逐位传输的音频采样数据一般按MSB先发。还有一根可选的MCLK主时钟很多音频芯片用它做内部PLL参考源频率通常是采样率的256倍或384倍比如48kHz采样率对应12.288MHz或18.432MHz。主从关系上总线上总有一个master负责生成BCLK和WS另一个是slave根据主设备给出的时钟边沿收发数据。多数场景下FPGA做主设备外部音频编码器、DAC做从设备但也可以反过来由音频芯片提供BCLK和WSFPGA做从设备收发数据。设计时首先要确定这个后面很多边沿对齐的细节都依赖于谁在产生时钟。1.2 一个完整采样周期的数据排列I2S的数据是按帧组织的一帧包含一个左声道采样值和一个右声道采样值。每个采样值由若干位组成常见的是16bit、24bit、32bit。位深越大动态范围越高24bit是音频领域的主流选择。标准I2S有几个关键约定MSB先行最高有效位最先出现在SD线上。左声道在前先传左声道再传右声道。数据比WS延迟一个BCLK标准I2S格式中SD上的第一个有效位MSB在WS翻转后的第二个BCLK边沿才出现也就是数据相对WS延迟了一个BCLK周期。这是为了给接收端留出判断左右声道的时间。举个例子24bit双声道、48kHz采样率的情况下一个采样周期内的排列大致是WS: _______________________________ _______________________________ | 左声道 L | 右声道 R | BCLK: _||_||_||_...每个BCLK对应一个bit SD: X [b23][b22][b21]...[b0] X [b23][b22]...[b0] XWS变低期间传左声道变高期间传右声道SD上每个bit对应一个BCLK周期而且bits按从高位到低位的顺序排列。1.3 上升沿还是下降沿把发送与接收边沿关系说清楚我经常看到有人问“主设备读取数据和从设备准备好数据都是在BCLK的上升沿吗”老实说这个问题没有一刀切的答案要看总线角色和数据手册。标准I2S的典型要求是发送方无论是谁在BCLK的下降沿更新SD线上的数据。接收方在BCLK的上升沿采样SD线上的数据。WS通常在BCLK的上升沿切换不同实现有差异但数据总是比WS晚一个BCLK。为什么这样设计关键在于建立时间setup time。如果发送方在下降沿更新数据数据经过一段传播延迟后稳定下来接收方在接下来的上升沿采样半个BCLK周期足够数据稳定。如果发送方和接收方都在上升沿动作那数据更新的瞬间就被采样采样结果不确定可能采到旧值也可能采到新值这就麻烦了。但实际工程里很多IP核的做法是也在下降沿更新WS因为接收端在上升沿采样时WS也同样是稳定的。所以不要只记住“必须上升沿还是下降沿”要抓住本质保证在接收方采样沿到来前数据线已经稳定。具体到某颗芯片一定以它的datasheet时序图为准。2. VHDL工程怎么搭主时钟分频、信号定义与模块划分2.1 为什么用VHDL写I2S接口选择VHDL而不是Verilog有不少历史原因但单就I2S接口而言VHDL的强类型系统反而能减少低级错误。比如std_logic_vector的位宽不匹配在VHDL里编译就直接报错而Verilog的隐式截断可能到了仿真后期才暴露问题。另外VHDL的record类型可以把左右声道数据、控制状态打包代码可读性好很多。我见过很多老工程都是用VHDL写的I2S模块可能是因为音频DSP领域欧洲厂商如NXP、Cirrus Logic的传统代码多是VHDL。不管怎样工程里核心代码结构是通用的一个顶层模块、一个时钟生成模块、一个发送器、一个接收器、若干数据缓冲。2.2 顶层接口信号如何定义顶层模块强烈建议把位深定义成类属参数而不是写死。原因很简单今天你接一颗24bit的DAC明天可能要接32bit的HDMI音频改参数比改代码省事得多。一个比较典型的顶层信号列表如下信号名方向宽度说明clkin1FPGA主时钟比如50MHz或100MHzreset_nin1低有效复位mclkout1主时钟输出可选bclkout/in1位时钟主模式输出从模式输入wsout/in1字选择/声道选择sdoout1串行数据输出接DAC/编解码器输入sdiin1串行数据输入接ADC/编解码器输出left_in / right_ininDATA_WIDTH待发送的左/右声道数据left_out / right_outoutDATA_WIDTH接收到的左/右声道数据load_validin1数据装载有效信号通常一个帧周期拉高一次从模式时BCLK和WS由外部芯片生成FPGA只需要在对应的边沿采样和发送不需要自己分频。主模式时FPGA要自己生成BCLK和WS下面重点讲这个。2.3 位时钟和字选择时钟的生成频率计算与分频位时钟BCLK的频率计算公式很简单BCLK fs × 单通道slot位数 × 通道数这里的slot位数不一定等于实际数据位深。比如有些DAC要求每个声道固定占32个BCLK周期哪怕你只传24bit有效数据也要把slot补到32bit。这个很关键否则左右声道会错位。以48kHz采样率、24bit数据、32bit slot、双声道为例BCLK 48000 × 32 × 2 3.072MHz WS BCLK / 64 48kHz MCLK 256 × fs 12.288MHz如果你的FPGA主时钟是12.288MHz那MCLK直接输出去就行。但如果主时钟是50MHz就需要用PLL生成12.288MHz再在这个时钟域下做分频生成BCLK和WS。一个简单的分频计数器写法如下-- Generate BCLK 3.072MHz from a 12.288MHz master clock -- 12.288MHz / 3.072MHz 4, so divide by 4 (2:1 duty cycle) signal cnt : unsigned(1 downto 0); signal bclk_int : std_logic; process(clk_12m, reset_n) begin if reset_n 0 then cnt (others 0); bclk_int 0; elsif rising_edge(clk_12m) then if cnt 2 then cnt (others 0); bclk_int not bclk_int; else cnt cnt 1; end if; end if; end process; bclk bclk_int;WS生成更简单每64个BCLK翻转一次就行因为一个帧周期是64个BCLK左右各32。注意WS的翻转要和BCLK对齐最简单的方式是用一个BCLK边沿使能的计数器signal bclk_cnt : integer range 0 to 63; signal ws_int : std_logic; process(reset_n, bclk) begin if reset_n 0 then bclk_cnt 0; ws_int 0; elsif rising_edge(bclk) then if bclk_cnt 63 then bclk_cnt 0; ws_int not ws_int; else bclk_cnt bclk_cnt 1; end if; end if; end process; ws ws_int;这样生成的WS频率就是BCLK / 64 48kHz正好等于采样率。2.4 模块划分与数据通路一个完整可用的I2S接口工程我通常会划分成这几个模块clk_gen负责从FPGA主时钟生成MCLK、BCLK、WS以及内部的load_valid脉冲。i2s_tx把并行左右声道数据转成串行数据按照I2S时序从sdo发出去。i2s_rx从sdi接收串行数据按照WS边沿将数据还原成左右声道并行数据。async_fifo可选当音频时钟域和FPGA逻辑时钟域不同源时用来做跨时钟域缓冲。reg_interface可选如果工程里有MCU通过SPI/I2C配置音频芯片这一层负责翻译寄存器配置。数据通路大致是上层逻辑比如音频处理算法把左/右声道采样值写入发送器发送器在load_valid有效时锁存数据然后按BCLK节奏逐位移出同时I2S总线上进来的数据由接收器逐位移入在WS边沿处锁存成完整的采样值交给后续处理。注意发送器的数据装载必须与帧边界对齐否则会出现左右声道数据错位。3. 发送器和接收器代码逐段拆解边沿对齐与位计数的实现细节3.1 发送器在正确的边沿把比特送出去发送器的核心任务就是在当前声道的帧内从MSB到LSB逐个输出数据位。为了让接收端在BCLK上升沿采样稳定数据更新放在下降沿。下面的代码是一个24bit发送器的核心思路architecture rtl of i2s_tx is signal bit_cnt : integer range 0 to DATA_WIDTH-1 : 0; signal sreg_l : std_logic_vector(DATA_WIDTH-1 downto 0); signal sreg_r : std_logic_vector(DATA_WIDTH-1 downto 0); signal ws_int : std_logic : 0; begin ws ws_int; -- WS toggles every DATA_WIDTH bits process(reset_n, bclk) begin if reset_n 0 then ws_int 0; elsif falling_edge(bclk) then if bit_cnt DATA_WIDTH-1 then ws_int not ws_int; end if; end if; end process; -- Serialize data on falling edge process(reset_n, bclk) begin if reset_n 0 then sdo 0; bit_cnt 0; sreg_l (others 0); sreg_r (others 0); elsif falling_edge(bclk) then if bit_cnt 0 then -- New frame, load new data if load_valid 1 then sreg_l left_in; sreg_r right_in; end if; if ws_int 0 then sdo sreg_l(DATA_WIDTH-1); else sdo sreg_r(DATA_WIDTH-1); end if; bit_cnt 1; else if ws_int 0 then sdo sreg_l(DATA_WIDTH-1-bit_cnt); else sdo sreg_r(DATA_WIDTH-1-bit_cnt); end if; if bit_cnt DATA_WIDTH-1 then bit_cnt 0; else bit_cnt bit_cnt 1; end if; end if; end if; end process; end architecture;代码里要注意几个点load_valid用来同步外部采样数据的到达节奏。如果每次帧边界都重新锁存数据那外部数据产生频率必须严格等于采样率否则要么数据重复要么丢数据。sdo在bit_cnt0时输出的是上一帧锁存数据的最高位也就是标准I2S里“数据比WS延迟一个BCLK”的来源。如果你把更新时刻统一到下降沿接收端在下一个上升沿采样时数据已经稳定了近半个BCLK周期。实际项目中如果每通道slot是32bit而数据只有24bit可以有两种处理把数据放到32bit slot的最高位、低位补0或者发送时在slot尾部填充0。通常采用前者因为接收端按slot首位对齐。3.2 接收器移位寄存与左右通道分离接收器正好是发送器的逆过程在BCLK上升沿采样SDI数据把串行数据逐位移入移位寄存器然后在WS边沿把移位寄存器的内容锁存成完整的左右声道采样值。architecture rtl of i2s_rx is signal shift_reg : std_logic_vector(DATA_WIDTH-1 downto 0); signal ws_d : std_logic; begin process(reset_n, bclk) begin if reset_n 0 then shift_reg (others 0); ws_d 0; left_out (others 0); right_out (others 0); elsif rising_edge(bclk) then ws_d ws; shift_reg shift_reg(DATA_WIDTH-2 downto 0) sdi; if ws_d 0 and ws 1 then -- WS rising edge: left channel complete left_out shift_reg; elsif ws_d 1 and ws 0 then -- WS falling edge: right channel complete right_out shift_reg; end if; end if; end process; end architecture;这个代码的关键是WS边沿检测用ws_d打一拍保存上一拍的WS值比较当前ws和上一拍ws_d就能判断上升沿和下降沿。为什么在上升沿采样SDI因为按照标准I2S时序发送方在BCLK下降沿更新数据到上升沿时数据已经稳定这个时候采样最可靠。如果你的从设备是在上升沿更新数据那就要把采样沿改成下降沿否则会采到毛刺。3.3 代码里最容易翻车的几个细节这几个问题我都实际踩过复位状态下WS和SD的初值不一致。有的模块复位时把WS拉高有的拉低如果总线上多个设备上电顺序不一致可能第一个帧周期就出现左右声道互换。建议统一约定复位后WS0左声道、sdo0并且外部设备也要等复位完成再使能。位计数清零时机不对导致数据移错位。比如24bit数据计数范围应该是0到23第24个BCLK时归零。如果写成了0到31按32bit slot计数那数据后面会跟8个无效bit很多DAC会把这8个bit当成有效数据产生噪声或者音量异常。数据锁存和帧边界没有对齐。我看到不少实现是在load_valid拉高时不管当前位计到哪直接装载新数据这会导致当前帧后面的数据全是乱的。正确做法是load_valid只能作为“数据准备好”的标志发送器在检测到帧边界bit_cnt0时才真正锁存数据。也就是说数据要提前至少一个帧周期准备好等着边界来取。接收器左右声道锁存数据搞反。如果你定义WS低电平是左声道那么左声道数据应该在WS由低变高那一个边沿锁存因为此时左声道数据已经全部接收完。很多初学者在WS刚变为低电平时就去锁存左声道结果存进去的是上一个右声道残留数据。4. 验证和上板调试从仿真波形到接上真实的音频功放芯片4.1 用testbench和仿真波形确认时序关系VHDL/Verilog仿真工具ModelSim、Questa、或者Vivado自带的XSim在I2S调试里几乎是必须的。不仿真直接上板出问题的时候根本分不清是逻辑错了还是外部芯片配置错了。testbench里主要做几件事生成主时钟、复位信号。例化I2S发送器往left_in和right_in灌入已知的测试数据比如左声道0xAAAAAA、右声道0x555555。跑一段时间后在仿真波形里直接检查sdo线上的数据是否正确。实际检查的步骤我一般这么来看BCLK频率对不对一个完整BCLK周期是否等于参考时钟的4个周期按分频比4算。看WS周期从WS上升沿到下一个上升沿是不是64个BCLK。看SD数据找到WS低电平期间的第一个数据位是否等于0xAAAAAA的最高位再检查下一帧左声道数据是否完整。看数据相对WS的延迟数据第一位是否比WS翻转晚一个BCLK。这一步如果能通过说明时序逻辑基本没问题。下面给一个最简testbench骨架方便你直接改来用entity tb_i2s_tx is end entity; architecture sim of tb_i2s_tx is signal clk : std_logic : 0; signal reset_n : std_logic : 0; signal bclk : std_logic; signal ws : std_logic; signal sdo : std_logic; signal left_in : std_logic_vector(23 downto 0); signal right_in : std_logic_vector(23 downto 0); signal load_valid : std_logic; begin clk not clk after 20 ns; -- 25MHz, adjust as needed process begin reset_n 0; wait for 100 ns; reset_n 1; left_in xAAAAAA; right_in x555555; load_valid 1; wait for 100 ns; load_valid 0; wait; end process; uut : entity work.i2s_tx generic map (DATA_WIDTH 24) port map ( clk clk, reset_n reset_n, bclk bclk, ws ws, sdo sdo, left_in left_in, right_in right_in, load_valid load_valid ); end architecture;4.2 上板调试的几个关键测量点仿真过了不代表上板一定出声因为真实芯片有时序要求还有信号完整性、电源噪声等问题。上板调试我习惯用示波器或逻辑分析仪看这几点MCLK频率如果音频芯片需要MCLK做PLL参考源MCLK必须干净稳定。用示波器看频率和幅度频率偏差超过1%就有可能导致芯片失锁或输出噪声。BCLK频率和占空比BCLK频率不对采样率就不对声音会变调。占空比一般要求40%到60%如果太偏说明分频逻辑或FPGA的IO配置比如slew rate有问题。WS波形WS应该是一个对称的方波频率等于采样率。如果WS上出现毛刺或者频率不对八成是分频计数器有bug。SD上的数据跳变正常播放音乐时SD线上应该有丰富的跳变如果SD一直是恒定电平说明发送器没在工作或数据全零。如果手头有逻辑分析仪建议把BCLK、WS、SD一起抓下来按协议解码能直接看到一帧数据里每一个bit的值还能对照I2S协议分析左右声道是否错位。4.3 接一块MAX98357A验证整条链路验证I2S接口最简单的办法就是在发送端接一块MAX98357A这类I2S输入的D类功放芯片直接驱动喇叭。MAX98357A的典型接法只需要5根线电源、GND、BCLK、WS、DIN不需要MCLK非常适合调试。接线示意MAX98357A引脚FPGA引脚说明BCLKbclk位时钟输入LRCK/WSws声道时钟/字选择DINsdo串行音频数据VIN3.3V电源注意要和FPGA共地GNDGND共地上电以后如果喇叭没有声音按这个顺序排查用示波器看有没有BCLK、WS波形。确认BCLK频率如果设成48kHz采样率、64个BCLK每帧BCLK应该是3.072MHz左右如果你设置的是32bit slot但代码写的是24bit实际BCLK频率就会出现偏差。确认WS频率是48kHz如果WS是24kHz说明每帧BCLK数量翻倍了可能是左右声道数据长度搞成了64bit。如果波形都对但没声音把发送器输出的数据换成固定值比如左声道输出正弦波表中的一个恒定值0x800000看喇叭有没有“咔嗒”声。有声音说明通路通了没声音则是数据位不对或电源问题。MAX98357A内部还带增益配置通过GAIN引脚设置如果声音很小但波形正常检查一下GAIN引脚有没有拉到正确的电平。5. 进阶玩法TDM多声道、异步FIFO和采样率适配5.1 TDM模式的工作原理和各设备边沿关系I2S本身只是双声道但多声道音频系统比如5.1、7.1、车载功放需要在一个总线上传多个声道的数据所以就出现了TDMTime Division Multiplexing模式。TDM的基本思路是把一个帧分成多个slot每个slot对应一个声道。比如TDM8就是一帧8个slot每个slot32bitBCLK频率就变成BCLK fs × 32 × 8TDM模式下WS不再只是高低电平区分左右声道而是变成了帧同步信号通常每个帧周期拉高一个BCLK周期表示一帧开始。数据则依次排在各个slot里。有人问“TDM里主设备读取数据和从设备准备好数据都是在BCLK的上升沿吗”——还是那句话取决于具体芯片。但大多数TDM音频接口继承了I2S的时序习惯发送方在BCLK下降沿更新数据接收方在上升沿采样。这保证了采样建立时间。有些DSP芯片例外它们要求主设备在上升沿准备数据如果照搬I2S习惯去设计数据就会错半个BCLK导致采样到不稳定的电平。所以接任何新芯片之前第一件事就是查它的datasheet时序图。TDM模式在VHDL里的实现和I2S很接近区别在于WS信号的位宽和slot计数I2S是每帧切换一次WS电平TDM是每帧发一个脉冲数据位计数也要扩展成“当前slot索引 当前slot内bit索引”发送器代码才算完整。5.2 异步FIFO当音频主时钟和FPGA逻辑时钟不同源时很多工程里FPGA内部逻辑时钟和I2S音频时钟来自不同的PLL或者外部晶振频率其实并不完全同步。比如FPGA主时钟是100MHz的晶振音频芯片的MCLK是12.288MHz两个时钟源没有固定相位关系。如果直接把音频数据在100MHz时钟域写入、在BCLK时钟域读出一定会偶尔出现读写指针冲突导致声音断续或爆音。解决这个问题的方法就是异步FIFO。原理很简单发送端把音频采样值按帧写入FIFOI2S发送器按BCLK时钟从FIFO读出数据。只要FIFO深度足够大能吸收短期的速率抖动两边就能正常工作。FIFO深度怎么算一般要大于“最大突发写入量 - 最大突发读出量”的差值。实际经验是48kHz采样率、双声道、24bit数据FIFO深度做到512个采样值就足够应对绝大多数时钟抖动场景。异步FIFO在FPGA里实现要么调用厂商IP核要么自己写。自己写时要用格雷码做读写指针跨时钟域同步避免二进制计数器的多位同时变化导致同步出错。如果不想自己造轮子直接用Xilinx的FIFO Generator或者Intel的ALTSYNCFIFO生成一个异步FIFO IP核位宽设成2 × DATA_WIDTH左右声道各一半深度设512或1024两边时钟分别接逻辑时钟和BCLK。5.3 采样率适配与重采样思路如果I2S接口固定工作在48kHz但FPGA里要播放的音频源是44.1kHz的MP3或WAV文件直接送进去声音会变调因为采样率不匹配。这时就需要采样率转换SRCSample Rate Conversion。采样率转换最简单的实现是线性插值把44.1kHz的采样点映射到48kHz的时间轴上用两个相邻采样点按比例插值出新的采样点。这个算法在FPGA里开销极小但对音频质量要求高的场景效果不够好。更好的方案是使用CIC滤波器或专门的多相滤波器结构。很多音频芯片如一些USB音频控制器会把重采样算法做成硬件模块效果比CPU软插值更稳定。如果在FPGA里做重采样我的建议是不要从头写高阶滤波器先把需求定义清楚——是44.1k转48k还是48k转96k、需要多少dB的阻带衰减、采样点精度要求多少bit然后再选择合适的多相滤波器阶数。这部分如果展开篇幅很大简单提一个检查点重采样输出数据的左右声道对齐、位宽对齐是I2S接口最容易出错的地方。我没有在这个工程里用特别深奥的技巧但上升沿还是下降沿这个问题真的是每个做音频接口的人都会反复遇到的坎。最简单的判断方法就是画时序图发送器在哪个沿更新数据接收器就在它之后的半个周期采样。只要这条规则成立你的I2S接口基本就是稳的。希望这篇基于VHDL I2S工程的拆解能让你从“照抄代码”变成“看得懂时序、调得了问题”。本文还有配套的精品资源点击获取
返回列表