Im ersten Teil möchte ich zeigen, wie ein einfacher I2S-Transmitter entworfen und genutzt werden kann, um mit Hilfe eines CS4344 Stereo D/A Konverter einen konstanten Ton über einen Lautsprecher auszugeben. Der ausgegebene Ton soll im Block-Memory des FPGAs gespeichert und vom Sender, der die Daten an den D/A Konverter sendet, ausgelesen werden.
Das komplette Projekt teilt sich in drei Abschnitte auf, die ich nach und nach erörtern werde:
- Ein Top-Design, in dem der Systemtakt erzeugt und das I2S-Modul instanziiert wird
- Das I2S-Modul, welches den Speicher für den Ton und den I2S-Sender enthält
- Der I2S-Sender
Der I2S-Sender:
Am untersten Ende des Designs steht der I2S-Sender, der die Aufgabe hat die einzelnen Datenwörter über die I2S-Schnittstelle zu verschicken.
Aus dem Blockschaltbild des Senders ergibt sich die folgende Entität:
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;
Über den Parameter WIDTH
wird die Breite eines Datenwortes pro Kanal definiert.
Der Sender besteht aus einem dreistufigen Zustandsautomaten, der wie folgt beschrieben ist:
architecture I2S_Transmitter_Arch of I2S_Transmitter is type State_t is (STATE_LOAD, STATE_TRANSMIT); signal CurrentState : State_t := STATE_LOAD; 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_LOAD => BitCounter := 0; Tx_Int <= Tx; LRCLK_Int <= '0'; CurrentState <= STATE_TRANSMIT; when STATE_TRANSMIT => BitCounter := BitCounter + 1; if(BitCounter > (WIDTH - 1)) then LRCLK_Int <= '1'; end if; if(BitCounter < ((2 * WIDTH) - 1)) then Ready_Int <= '0'; CurrentState <= STATE_TRANSMIT; else Ready_Int <= '1'; CurrentState <= STATE_LOAD; 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 BitCounter := 0; Ready_Int <= '0'; LRCLK_Int <= '1'; Enable <= '1'; SD_Int <= '0'; Tx_Int <= (others => '0'); CurrentState <= STATE_TRANSMIT; end if; end process; Ready <= Ready_Int; SCLK <= Clock and Enable; LRCLK <= LRCLK_Int; SD <= SD_Int; end I2S_Transmitter_Arch;
Während eines Reset werden die Ausgangssignale gelöscht und der Takt für SCLK deaktiviert.
Nach einem Reset wechselt der Automat in den Zustand STATE_LOAD
. In diesem Zustand überträgt der Automat den Inhalt des Buffers Tx_Int
über die I2S-Schnittstelle. Sobald die Übertragung des letzten Datenbits gestartet wird, wird Ready
gesetzt um das Ende einer Übertragung und die Bereitschaft neue Daten annehmen zu können zu signalisieren. Anschließend wechselt der Automat in den Zustand STATE_LOAD
, wo der Sendebuffer mit einem neuen Datenwort gefüllt und eine neue Übertragung gestartet wird.
Das I2S-Modul:
Der fertige Sender wird vom übergeordneten I2S-Modul genutzt um Daten aus einem ROM an den D/A-Wandler zu übertragen.
Aus dieser Beschreibung ergibt sich die nachfolgende Entität:
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;
Die Parameter RATIO
und WIDTH
definieren das Verhältnis von SCLK zu MCLK und die Breite eines Datenwortes pro Kanal.
Das Modul verwendet, neben dem I2S-Sender, ein ROM, welches über den Block-Memory Generator erstellt und mit Daten gefüllt werden kann. Beides kann über den IP-Integrator von Vivado gemacht werden.
Zum Abschluss wird das ROM über Other Options über eine coe-Datei mit einem Sinussignal initialisiert.
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,
Das I2S-Modul verwendet einen Zustandsautomaten um Daten aus dem ROM auszulesen und an den I2S-Sender zu übergeben.
architecture I2S_Arch of I2S is type State_t is (STATE_WAIT_READY, STATE_INC_ADDRESS, STATE_WAIT_START); signal CurrentState : State_t := STATE_WAIT_READY; 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 SCLK_Int : 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 => SCLK_Int, 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; 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_WAIT_READY => if(Ready = '1') then CurrentState <= STATE_WAIT_START; else CurrentState <= STATE_WAIT_READY; end if; when STATE_WAIT_START => ROM_Address <= STD_LOGIC_VECTOR(to_unsigned(WordCounter, ROM_Address'length)); Tx <= x"0000" & ROM_Data; if(Ready = '0') then CurrentState <= STATE_INC_ADDRESS; else CurrentState <= STATE_WAIT_START; end if; when STATE_INC_ADDRESS => if(WordCounter < 99) then WordCounter := WordCounter + 1; else WordCounter := 0; end if; CurrentState <= STATE_WAIT_READY; end case; if(nReset = '0') then WordCounter := 0; CurrentState <= STATE_WAIT_START; end if; end process; end I2S_Arch;
Der erste Prozess wird genutzt um aus MCLK das für den Sender benötigte Taktsignal SCLK zu erzeugen.
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;
Der zweite Prozess kümmert sich um die Abarbeitung des Zustandsautomaten. Nach dem Verlassen des Reset-Zustandes wartet der Automat so lange in dem Zustand STATE_WAIT_READY
bis der Sender durch das Ready
-Signal Bereitschaft signalisiert.
Sobald der Sender bereit ist, wechselt der Automat in den Zustand STATE_WAIT_START
. In diesem Zustand wird das aktuelle Datenwort aus dem ROM ausgelesen und an den Sender übergeben.
Hinweis:
Das hier gezeigte ROM beinhaltet nur die Informationen aus einem Kanal. Für den zweiten Kanal müssen die Daten entsprechend erweitert werden.
Sobald der Sender das Ready
-Signal löscht und mit der Übertragung der Daten beginnt, wechselt der Zustandsautomat in den Zustand STATE_INC_ADDRESS
. In diesem Zustand wird die ROM-Adresse um eins erhöht und dann wieder zurück in den Zustand STATE_WAIT_READY
gewechselt.
Das Top-Design:
Die letzte Komponente ist das Top-Design, dass das I2S-Modul und einen Clocking Wizard beinhaltet. Dieses Beispiel verwendet die folgenden Parameter für die Ansteuerung des CS4344:
Der Clocking Wizard wird genutzt um den 12,288 MHz Takt aus der Oszillatorfrequenz der programmierbaren Logik zu erzeugen. Dazu wird der Clocking Wizard über den IP-Integrator eingefügt und zusammen mit dem fertigen I2S-Modul im VHDL-Code instantiiert.
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;
Das Design kann nun implementiert, auf das FPGA übertragen und getestet werden. Im Idealfall gibt der D/A-Wandler ein 480 Hz Sinussignal aus, da das Signalmuster aus dem ROM eine Länge von 100 Abtastwerten aufweist und die Abtastfrequenz 48 kHz beträgt. Mit einem Oszilloskop kann die Kommunikation und das Signal kontrolliert werden:
Und wenn man schon dabei ist, kann auch gleich das Audiosignal gecheckt werden. Die FFT-Funktion eines Oszilloskops eignet sich sehr gut dafür.
Wie man erkennt, wird ein (fast) perfekter Sinus mit einer Frequenz von 480 Hz ausgegeben. Die Oberwellen erscheinen bei den Vielfachen der Abtastfrequenz und stammen aus der D/A-Wandlung des Konverters.
Im nächsten Teil wird der I2S-Sender zusätzlich noch mit einem AXI-Stream Interface ausgestattet, um ihn mit dem Processing System des ZYNQ verbinden zu können.
→ Zurück
Müsste der S Clock nicht 3 MHz (64 x 48kHz) haben?
Hallo Rolf,
nein, da die Samples 16 Bit lang sind. Ich muss somit 32 Bit innerhalb einer Periode von LRCLK (48 kHz) senden, woraus sich ein SCLK von 1,536 MHz ergibt.
Gruß
Daniel