In the first part, I would like to show how a simple I2S transmitter can be designed and used to output a constant sound via a loudspeaker using a CS4344 stereo D/A converter.
The sound to be output is to be stored in the block memory of the FPGA and read out by the transmitter which sends the data to the D/A converter.
The entire project is divided into three sections, which I will discuss gradually:
A Top design which integrates the system clock and an I2S module
The I2S module which integrates the ROM and the I2S transmitter
The I2S transmitter
The I2S transmitter
The bottom end of the design should be the I2S transmitter, which has the task of sending the individual data words over the I2S interface.
Signal
Description
Clock
Input clock for the audio interface
nReset
Reset input for the audio interface (active low)
Ready
Ready signal to indicate a new data transmission
Tx
Transmission data
SCLK
Serial clock for the I2S interface
LRCLK
Left/Right clock (WS) for the I2S interface
SD
Serial data for the I2S interface
The block diagram results in the following entity of the transmitter:
entity I2S_Transmitter is
Generic ( WIDTH : INTEGER := 16
);
Port ( Clock : in STD_LOGIC;
nReset : in STD_LOGIC;
Ready : out STD_LOGIC;
Tx : in STD_LOGIC_VECTOR(((2 * WIDTH) - 1) downto 0);
LRCLK : out STD_LOGIC;
SCLK : out STD_LOGIC;
SD : out STD_LOGIC
);
end I2S_Transmitter;
The size of a data word is defined via the parameter WIDTH.
A three-stage state machine controls the transmitter and is described as follows:
architecture I2S_Transmitter_Arch of I2S_Transmitter is
type State_t is (State_Reset, State_LoadWord, State_TransmitWord);
signal CurrentState : State_t := State_Reset;
signal Tx_Int : STD_LOGIC_VECTOR(((2 * WIDTH) - 1) downto 0) := (others => '0');
signal Ready_Int : STD_LOGIC := '0';
signal LRCLK_Int : STD_LOGIC := '1';
signal SD_Int : STD_LOGIC := '0';
signal Enable : STD_LOGIC := '0';
begin
process
variable BitCounter : INTEGER := 0;
begin
wait until falling_edge(Clock);
case CurrentState is
when State_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;
During a reset, the output signals are deleted and the clock for SCLK is deactivated. After the reset, the machine changes from the State_Reset state to the state State_TransmitWord. In this state, the machine transmits the content of the buffer Tx_Int via the I2S interface.
As soon as the transmission of the last data bit is started, Ready is set to signal the end of a transmission and the readiness to accept new data. The machine then changes to the state State_LoadWord state, where the send buffer is filled with a new data word and a new transfer is started.
The I2S module
The I2S transmitter is used by the higher-level I2S module to transfer data from a ROM to the D/A converter.
Signal
Description
MCLK
Master clock for the audio interface
nReset
Reset input for the audio interface (active low)
SCLK
Serial clock for the I2S interface
LRCLK
Left/Right clock (WS) for the I2S interface
SD
Serial data for the I2S interface
With the following entity:
entity I2S is
Generic ( RATIO : INTEGER := 8;
WIDTH : INTEGER := 16
);
Port ( MCLK : in STD_LOGIC;
nReset : in STD_LOGIC;
LRCLK : out STD_LOGIC;
SCLK : out STD_LOGIC;
SD : out STD_LOGIC
);
end I2S;
The parameters RATIO and WIDTH define the ratio of SCLK to MCLK and the width of a data word per channel.
In addition to the I2S transmitter, the module uses a ROM, which can be created via the block memory generator and filled with data. Both can be done using Vivado's IP integrator.
Finally, the ROM is initialized with a sine signal coe file via Other Options.
The I2S module uses a state machine to read data from the ROM and transfer it to the I2S transmitter.
architecture I2S_Arch of I2S is
type State_t is (State_Reset, State_WaitForReady, State_IncreaseAddress, State_WaitForStart);
signal CurrentState : State_t := State_Reset;
signal Tx : STD_LOGIC_VECTOR(((2 * WIDTH) - 1) downto 0) := (others => '0');
signal ROM_Data : STD_LOGIC_VECTOR((WIDTH - 1) downto 0) := (others => '0');
signal ROM_Address : STD_LOGIC_VECTOR(6 downto 0) := (others => '0');
signal Ready : STD_LOGIC;
signal Clock_Audio : STD_LOGIC := '0';
component I2S_Transmitter is
Generic ( WIDTH : INTEGER := 16
);
Port ( Clock : in STD_LOGIC;
nReset : in STD_LOGIC;
Ready : out STD_LOGIC;
Tx : in STD_LOGIC_VECTOR(((2 * WIDTH) - 1) downto 0);
LRCLK : out STD_LOGIC;
SCLK : out STD_LOGIC;
SD : out STD_LOGIC
);
end component;
component SineROM is
Port ( Address : in STD_LOGIC_VECTOR(6 downto 0);
Clock : in STD_LOGIC;
DataOut : out STD_LOGIC_VECTOR(15 downto 0)
);
end component SineROM;
begin
Transmitter : I2S_Transmitter generic map( WIDTH => WIDTH
)
port map( Clock => Clock_Audio,
nReset => nReset,
Ready => Ready,
Tx => Tx,
LRCLK => LRCLK,
SCLK => SCLK,
SD => SD
);
ROM : SineROM port map (Clock => MCLK,
Address => ROM_Address,
DataOut => ROM_Data
);
process
variable Counter : INTEGER := 0;
begin
wait until rising_edge(MCLK);
if(Counter < ((RATIO / 2) - 1)) then
Counter := Counter + 1;
else
Counter := 0;
Clock_Audio <= not Clock_Audio;
end if;
if(nReset = '0') then
Counter := 0;
Clock_Audio <= '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;
The first process is used to generate the clock signal SCLK required for the transmitter from MCLK.
process
variable Counter : INTEGER := 0;
begin
wait until rising_edge(MCLK);
if(Counter < ((RATIO / 2) - 1)) then
Counter := Counter + 1;
else
Counter := 0;
Clock_Audio <= not Clock_Audio;
end if;
if(nReset = '0') then
Counter := 0;
Clock_Audio <= '0';
end if;
end process;
The second process takes care of the processing of the state machine. After leaving the state State_Reset, the machine waits in the State_WaitForReady state until the transmitter signals readiness with the Ready signal.
As soon as the transmitter is ready, the machine changes to the State_WaitForStart state. In this state, the current data word is read from the ROM and transferred to the transmitter.
The ROM is shown here only contains the information from one channel. The data must be expanded accordingly for the second channel.
As soon as the transmitter clears the ready signal and begins transmitting the data, the state machine changes to the State_IncreaseAddress state. In this state, the ROM address is increased by one and then switched back to the State_WaitForReady state.
The Top module
The last component is the top design that includes the I2S module and a clocking wizard.
This example uses the following parameters to control the CS4344:
Parameter
Value
MCLK
12.288 MHz
SCLK
1.536 MHz
LRCLK
48 kHz
RATIO
8
WIDTH
16
The Clocking Wizard is used to generate the 12.288 MHz clock from the oscillator frequency of the programmable logic. For this purpose, the clocking wizard is inserted via the IP integrator and instantiated together with the finished I2S module in the VHDL code.
entity Top is
Generic ( RATIO : INTEGER := 8;
WIDTH : INTEGER := 16
);
Port ( Clock : in STD_LOGIC;
nReset : in STD_LOGIC;
MCLK : out STD_LOGIC;
LRCLK : out STD_LOGIC;
SCLK : out STD_LOGIC;
SD : out STD_LOGIC;
LED : out STD_LOGIC_VECTOR(3 downto 0)
);
end Top;
architecture Top_Arch of Top is
signal nSystemReset : STD_LOGIC := '0';
signal MCLK_DCM : STD_LOGIC := '0';
signal Locked : STD_LOGIC := '0';
component I2S is
Generic ( RATIO : INTEGER := 8;
WIDTH : INTEGER := 16
);
Port ( MCLK : in STD_LOGIC;
nReset : in STD_LOGIC;
LRCLK : out STD_LOGIC;
SCLK : out STD_LOGIC;
SD : out STD_LOGIC
);
end component;
component AudioClock is
Port ( ClockIn : in STD_LOGIC;
Locked : out STD_LOGIC;
MCLK : out STD_LOGIC;
nReset : in STD_LOGIC
);
end component;
begin
InputClock : AudioClock port map ( ClockIn => Clock,
nReset => nReset,
MCLK => MCLK_DCM,
Locked => Locked
);
I2S_Module : I2S generic map ( RATIO => RATIO,
WIDTH => WIDTH
)
port map ( 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;
The design can now be implemented, transferred to the FPGA and tested. Ideally, the D/A converter outputs a 480 Hz sine signal, since the signal pattern from the ROM has a length of 100 samples and the sampling frequency is 48 kHz. The communication and the signal can be checked with an oscilloscope:
Also, the audio signal can be checked. The FFT function of an oscilloscope is the ideal tool to do it.