开篇第一步
Inter-IC Sound Interface(简称I2S)是由飞利浦公司开发,用于通过不同IC之间的串行接口(例如从处理器到DAC)传输数字音频数据。该接口使用以下信号进行数据传输:
SCK (串行时钟)——用于数据传输的时钟。
SD (串行数据)- 每个数据字的各个位通过该线传输。
WS (字选择)- 定义传输数据字的长度。它用于标记右或左音频通道。
仅音频数据通过 I2S 传输。附加数据(例如各个总线用户的配置)通过其他接口传输。数据传输总是在两个总线之间沿一个方向进行,其中一路总线必须充当主机并负责生成时钟信号。在由多个发送器和接收器组成的复杂系统中,时钟信号由外部总线主控器生成,并且相应的发送器生成数据。
所有数据均以二进制补码和 MSB 优先的方式传输。如果接收方和发送方的字宽存在正差(即一方的字宽小于另一方的字宽),则剩余位填充0。根据规范,数据可以同步于正时钟沿或负时钟沿,从而数据总是在负时钟沿读入。
WS信号选择活动通道,并将低或高相位内的所有数据分配给相应的通道:
WS = 0 – 通道 1(左)
WS = 1 – 通道 2(右)
WS信号必须在下一个数据字的 MSB 之前的一个时钟周期发生变化,以便接收器可以将数据读入正确的通道。WS信号的时钟频率通常对应于音频信号的采样频率。
在这篇文章中,展示如何设计一个简单的 I2S 发射器,并使用 CS4344 立体声 D/A 转换器通过扬声器输出恒定的声音。
要输出的声音将存储在 FPGA 的block memory中,并由发送器读出,并将数据发送到 D/A 转换器。整个项目分为三个部分,将逐步讨论:
集成系统时钟和I2S模块的Top设计
集成ROM和I2S发送器的I2S模块
I2S发送器
I2S发送器
设计的最底层应该是 I2S 发送器,其任务是通过 I2S 接口发送各个数据字。
该框图产生了以下发送器:
entityI2S_Transmitteris Generic(WIDTH:INTEGER:=16 ); Port(Clock:inSTD_LOGIC; nReset:inSTD_LOGIC; Ready:outSTD_LOGIC; Tx:inSTD_LOGIC_VECTOR(((2*WIDTH)-1)downto0); LRCLK:outSTD_LOGIC; SCLK:outSTD_LOGIC; SD:outSTD_LOGIC ); endI2S_Transmitter;
数据字的大小通过WIDTH参数定义。
三级状态机控制发送器,描述如下:
architectureI2S_Transmitter_ArchofI2S_Transmitteris typeState_tis(State_Reset,State_LoadWord,State_TransmitWord); signalCurrentState:State_t:=State_Reset; signalTx_Int:STD_LOGIC_VECTOR(((2*WIDTH)-1)downto0):=(others=>'0'); signalReady_Int:STD_LOGIC:='0'; signalLRCLK_Int:STD_LOGIC:='1'; signalSD_Int:STD_LOGIC:='0'; signalEnable:STD_LOGIC:='0'; begin process variableBitCounter:INTEGER:=0; begin waituntilfalling_edge(Clock); caseCurrentStateis whenState_Reset=> Ready_Int<= '0'; LRCLK_Int <= '1'; Enable <= '1'; SD_Int <= '0'; Tx_Int <= (others =>'0'); CurrentState<= State_LoadWord; when State_LoadWord => BitCounter:=0; Tx_Int<= Tx; LRCLK_Int <= '0'; CurrentState <= State_TransmitWord; when State_TransmitWord => BitCounter:=BitCounter+1; if(BitCounter>(WIDTH-1))then LRCLK_Int<= '1'; end if; if(BitCounter < ((2 * WIDTH) - 1)) then Ready_Int <= '0'; CurrentState <= State_TransmitWord; else Ready_Int <= '1'; CurrentState <= State_LoadWord; end if; Tx_Int <= Tx_Int(((2 * WIDTH) - 2) downto 0) & "0"; SD_Int <= Tx_Int((2 * WIDTH) - 1); end case; if(nReset = '0') then CurrentState <= State_Reset; end if; end process; Ready <= Ready_Int; SCLK <= Clock and Enable; LRCLK <= LRCLK_Int; SD <= SD_Int; end I2S_Transmitter_Arch;
复位期间,输出信号被置位,SCLK时钟被停用。复位后,机器从State_Reset状态变为State_TransmitWord状态。在此状态下,机器Tx_Int通过 I2S 接口传输缓冲区的内容。
一旦开始传输最后一个数据位,Ready就设置为表示传输结束并准备好接受新数据。然后机器更改为 stateState_LoadWord状态,其中发送缓冲区填充有新的数据字并开始新的传输。
I2S模块
I2S 模块使用 I2S 发送器将数据从 ROM 传输到 D/A 转换器。
具有以下代码:
entityI2Sis Generic(RATIO:INTEGER:=8; WIDTH:INTEGER:=16 ); Port(MCLK:inSTD_LOGIC; nReset:inSTD_LOGIC; LRCLK:outSTD_LOGIC; SCLK:outSTD_LOGIC; SD:outSTD_LOGIC ); endI2S;
参数RATIO 定义了SCLK与MCLK WIDTH的比率以及每个通道的数据字的宽度。
除了 I2S 发送器之外,该模块还使用 ROM,该 ROM 可以通过block memory生成器创建并填充数据。两者都可以使用 Vivado 的 IP 来完成。
最后,通过其他选项使用正弦信号 coe 文件(参见附件)对 ROM 进行初始化。
I2S 模块使用状态机从 ROM 读取数据并将其传输到 I2S 发送器。
architectureI2S_ArchofI2Sis typeState_tis(State_Reset,State_WaitForReady,State_IncreaseAddress,State_WaitForStart); signalCurrentState:State_t:=State_Reset; signalTx:STD_LOGIC_VECTOR(((2*WIDTH)-1)downto0):=(others=>'0'); signalROM_Data:STD_LOGIC_VECTOR((WIDTH-1)downto0):=(others=>'0'); signalROM_Address:STD_LOGIC_VECTOR(6downto0):=(others=>'0'); signalReady:STD_LOGIC; signalSCLK_Int:STD_LOGIC:='0'; componentI2S_Transmitteris Generic(WIDTH:INTEGER:=16 ); Port(Clock:inSTD_LOGIC; nReset:inSTD_LOGIC; Ready:outSTD_LOGIC; Tx:inSTD_LOGIC_VECTOR(((2*WIDTH)-1)downto0); LRCLK:outSTD_LOGIC; SCLK:outSTD_LOGIC; SD:outSTD_LOGIC ); endcomponent; componentSineROMis Port(Address:inSTD_LOGIC_VECTOR(6downto0); Clock:inSTD_LOGIC; DataOut:outSTD_LOGIC_VECTOR(15downto0) ); endcomponentSineROM; begin Transmitter:I2S_Transmittergenericmap(WIDTH=>WIDTH ) portmap(Clock=>SCLK_Int, nReset=>nReset, Ready=>Ready, Tx=>Tx, LRCLK=>LRCLK, SCLK=>SCLK, SD=>SD ); ROM:SineROMportmap(Clock=>MCLK, Address=>ROM_Address, DataOut=>ROM_Data ); process variableCounter:INTEGER:=0; begin waituntilrising_edge(MCLK); if(Counter< ((RATIO / 2) - 1)) then Counter := Counter + 1; else Counter := 0; SCLK_Int <= not SCLK_Int; end if; if(nReset = '0') then Counter := 0; SCLK_Int <= '0'; end if; end process; process variable WordCounter : INTEGER := 0; begin wait until rising_edge(MCLK); case CurrentState is when State_Reset => WordCounter:=0; CurrentState<= State_WaitForReady; when State_WaitForReady => if(Ready='1')then CurrentState<= State_WaitForStart; else CurrentState <= State_WaitForReady; end if; when State_WaitForStart => ROM_Address<= STD_LOGIC_VECTOR(to_unsigned(WordCounter, ROM_Address'length)); Tx <= x"0000" & ROM_Data; if(Ready = '0') then CurrentState <= State_IncreaseAddress; else CurrentState <= State_WaitForStart; end if; when State_IncreaseAddress => if(WordCounter< 99) then WordCounter := WordCounter + 1; else WordCounter := 0; end if; CurrentState <= State_WaitForReady; end case; if(nReset = '0') then CurrentState <= State_Reset; end if; end process; end I2S_Arch;
第一个过程用于从MCLK生成发送器所需的时钟信号SCLK 。
process variableCounter:INTEGER:=0; begin waituntilrising_edge(MCLK); if(Counter< ((RATIO / 2) - 1)) then Counter := Counter + 1; else Counter := 0; SCLK_Int <= not SCLK_Int; end if; if(nReset = '0') then Counter := 0; SCLK_Int <= '0'; end if; end process;
第二个进程负责状态机的处理。离开State_Reset状态后,机器在该State_WaitForReady状态下等待,直到发送器发出就绪信号Ready 。
一旦发送器准备就绪,机器就会更改State_WaitForStart状态。在此状态下,从 ROM 读取当前数据字并将其传输到发送器。
PS:此处显示的 ROM 仅包含一个通道的信息。第二个通道的数据需进行扩展。
一旦发送器清除就绪信号并开始发送数据,状态机就会更改为State_IncreaseAddress状态。该状态下ROM地址加1,然后切换回State_WaitForReady状态
top模块
最后一个组件是顶层设计,包括 I2S 模块和时钟PLL。
本示例使用以下参数来控制 CS4344:
MCLK:12.288MHz
SCLK:1.536 MHz
LRCLK:48kHz
比率:8
宽度:16
时钟PLL生成 12.288 MHz 时钟,并与之前代码中完成的 I2S 模块一起实例化。
entityTopis Generic(RATIO:INTEGER:=8; WIDTH:INTEGER:=16 ); Port(Clock:inSTD_LOGIC; nReset:inSTD_LOGIC; MCLK:outSTD_LOGIC; LRCLK:outSTD_LOGIC; SCLK:outSTD_LOGIC; SD:outSTD_LOGIC; LED:outSTD_LOGIC_VECTOR(3downto0) ); endTop; architectureTop_ArchofTopis signalnSystemReset:STD_LOGIC:='0'; signalMCLK_DCM:STD_LOGIC:='0'; signalLocked:STD_LOGIC:='0'; componentI2Sis Generic(RATIO:INTEGER:=8; WIDTH:INTEGER:=16 ); Port(MCLK:inSTD_LOGIC; nReset:inSTD_LOGIC; LRCLK:outSTD_LOGIC; SCLK:outSTD_LOGIC; SD:outSTD_LOGIC ); endcomponent; componentAudioClockis Port(ClockIn:inSTD_LOGIC; Locked:outSTD_LOGIC; MCLK:outSTD_LOGIC; nReset:inSTD_LOGIC ); endcomponent; begin InputClock:AudioClockportmap(ClockIn=>Clock, nReset=>nReset, MCLK=>MCLK_DCM, Locked=>Locked ); I2S_Module:I2Sgenericmap(RATIO=>RATIO, WIDTH=>WIDTH ) portmap(MCLK=>MCLK_DCM, nReset=>nSystemReset, LRCLK=>LRCLK, SCLK=>SCLK, SD=>SD ); nSystemReset<= nReset and Locked; LED(0) <= nReset; LED(1) <= Locked; LED(2) <= nSystemReset; MCLK <= MCLK_DCM; end Top_Arch;
最后就可以进行测试。理想情况下,D/A 转换器输出 480 Hz 正弦信号。因为来自 ROM 的信号模式的长度为 100 个样本,采样频率为 48 kHz。可以用示波器检查总线和信号:
此外,还可以检查音频信号(示波器的 FFT 功能是实现此目的的最佳工具)。
附件-Coe
memory_initialization_radix=16; memory_initialization_vector= 0000, 0809, 100A, 17FB, 1FD4, 278D, 2F1E, 367F, 3DA9, 4495, 4B3B, 5196, 579E, 5D4E, 629F, 678D, 6C12, 7029, 73D0, 7701, 79BB, 7BF9, 7DBA, 7EFC, 7FBE, 7FFF, 7FBE, 7EFC, 7DBA, 7BF9, 79BB, 7701, 73D0, 7029, 6C12, 678D, 629F, 5D4E, 579E, 5196, 4B3B, 4495, 3DA9, 367F, 2F1E, 278D, 1FD4, 17FB, 100A, 0809, 0000, F7F7, EFF6, E805, E02C, D873, D0E2, C981, C257, BB6B, B4C5, AE6A, A862, A2B2, 9D61, 9873, 93EE, 8FD7, 8C30, 88FF, 8645, 8407, 8246, 8104, 8042, 8001, 8042, 8104, 8246, 8407, 8645, 88FF, 8C30, 8FD7, 93EE, 9873, 9D61, A2B2, A862, AE6A, B4C5, BB6B, C257, C981, D0E2, D873, E02C, E805, EFF6, F7F7,
下一篇文章,将向 I2S 发送器添加 AXI-Stream 接口,并将其与 ZYNQ 的处理系统连接,播放 SD 卡中的音频文件。
审核编辑:汤梓红
-
处理器
+关注
关注
68文章
19155浏览量
229040 -
FPGA
+关注
关注
1625文章
21663浏览量
601666 -
接口
+关注
关注
33文章
8486浏览量
150805 -
音频
+关注
关注
29文章
2832浏览量
81330
原文标题:使用 FPGA 播放音频(一)
文章出处:【微信号:Open_FPGA,微信公众号:OpenFPGA】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论