Kampis Elektroecke

Design des I2S-Sender

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.

Signal Beschreibung
Clock Taktfrequenz der Audio Samples
nReset Reset Eingang (Active Low)
Ready Ready Signal um eine neue Übertragung zu signalisieren
Tx
Zu übertragende Daten
SCLK Serial Clock der I2S-Schnittstelle
LRCLK Links/Rechts Takt (WS) der I2S-Schnittstelle
SD Serial Data der I2S-Schnittstelle

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.

Signal Beschreibung
MCLK Haupttakt für das Audio Interface
nReset Reset Eingang (Active Low)
SCLK Serial Clock der I2S-Schnittstelle
LRCLK LRCLK (WS) der I2S-Schnittstelle
SD Serial Data der I2S-Schnittstelle

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:

Parameter Wert
MCLK 12,288 MHz
SCLK 1,536 MHz
LRCLK 48 kHz
RATIO 8
WIDTH 16

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

2 Kommentare

    1. 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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert