004 – ZedBoard Audio Codec SPI Controller


In this post we implement the SPI Controller logic that will configure the ADAU1761 audio codec in our ZedBoard Audio Processor design. The SPI Controller is the third of the four major components in our ZedBoard Audio Processor design’s top level, as mentioned in a previous post.

As shown in the figure below, the SPI Controller logic is partitioned into two submodules: the SPI Master and the SPI Driver.

SPI Controller
SPI Controller

SPI Master

The SPI Master is a generic module that implements the master device in a single-slave SPI bus. In addition to the standard SPI signals, the clock phase and polarity of the SPI Master can be changed at runtime using the corresponding input ports. The bit width of a transaction can be adjusted during the module instantiation.

Just as a heads up: even though configuration options for the clock phase, clock polarity and transaction bit width have been added to the RTL description, most of them have not been thoroughly tested. The only hardware validation has been done by communicating with the audio codec on the ZedBoard with its default settings and at fairly low speed. Because we only use this interface once after powering up the system, this isn’t a problem.

The RTL description of the SPI Master module is shown below.

module spi_master # (
    parameter SPI_CLOCK_DIVIDER_WIDTH   = 8,
    parameter SPI_DATA_WIDTH            = 8
) (
    // Clock, reset
    input   logic   i_clock,
    input   logic   i_reset,
    // Data, control and status interface
    input   logic                                   i_enable,
    input   logic                                   i_clock_polarity,
    input   logic                                   i_clock_phase,
    input   logic [SPI_CLOCK_DIVIDER_WIDTH-1 : 0]   i_spi_clock_divider,
    input   logic [SPI_DATA_WIDTH-1 : 0]            i_data_in,
    output  logic [SPI_DATA_WIDTH-1 : 0]            o_data_out,
    output  logic                                   o_done,
    output  logic                                   o_busy,
    // SPI interface
    output  logic   o_spi_cs_n,
    output  logic   o_spi_clock,
    output  logic   o_spi_mosi,
    input   logic   i_spi_miso
);

    timeunit 1ns;
    timeprecision 1ps;

    enum logic [2:0]    {IDLE,
                        PRE_DELAY,
                        SETUP,
                        TRANSMISSION,
                        POST_DELAY,
                        DONE} fsm_state;
    logic [SPI_DATA_WIDTH-1 : 0] spi_data_counter;

    // Clock generation
    logic [SPI_CLOCK_DIVIDER_WIDTH-1 : 0] clock_counter;
    logic spi_clock;

    always_ff @ (posedge i_clock) begin
        if (i_reset) begin
            spi_clock <= 1\'b0;
            clock_counter <= \'b0;
        end else begin
            if ((fsm_state == TRANSMISSION) || (fsm_state == PRE_DELAY) || (fsm_state == POST_DELAY)) begin
                clock_counter <= clock_counter + 1;
                if (clock_counter == (i_spi_clock_divider)) begin
                    if (fsm_state == TRANSMISSION) begin
                        spi_clock <= ~ spi_clock;
                    end
                    clock_counter <= \'b0;
                end
            end else begin
                if (i_clock_polarity) begin
                    spi_clock <= 1\'b1;
                end else begin
                    spi_clock <= 1\'b0;
                end
                clock_counter <= \'b0;
            end
        end
    end
    assign o_spi_clock = spi_clock;

    // SPI clock edge detection
    logic spi_clock_falling;
    logic spi_clock_rising;
    logic spi_clock_ff;

    always_ff @(posedge i_clock) begin
        spi_clock_ff <= spi_clock;
        spi_clock_falling <= 1\'b0;
        spi_clock_rising <= 1\'b0;
        if ((spi_clock == 1\'b1) && (spi_clock_ff == 1\'b0)) begin   // Rising edge
            spi_clock_rising <= 1\'b1;
        end
        if ((spi_clock == 1\'b0) && (spi_clock_ff == 1\'b1)) begin   // Falling edge
            spi_clock_falling <= 1\'b1;
        end
    end

    // Main FSM
    logic enable_delay1;
    logic enable_delay2;
    logic enable_rising;
    logic spi_cs_n;
    logic [SPI_DATA_WIDTH-1 : 0] data_out_shift;
    logic [SPI_DATA_WIDTH-1 : 0] data_in_shift;
    logic spi_mosi;
    logic done;

    always_ff @(posedge i_clock) begin
        if (i_reset) begin
            fsm_state <= IDLE;
            spi_data_counter <= \'b0;
            spi_cs_n <= 1\'b1;
            data_out_shift <= \'b0;
            done <= 1\'b0;
            spi_mosi <= 1\'b0;
            data_out_shift <= \'b0;
            o_data_out <= \'b0;
            o_busy <= \'b0;
        end else begin
            // Detecting the rising edge of the \'i_enable\' signal - BEGIN
            enable_delay1 <= i_enable;
            enable_delay2 <= enable_delay1;
            enable_rising <= 1\'b0;
            if ((enable_delay2 == 1\'b0) && (enable_delay1 == 1\'b1)) begin
                enable_rising <= 1\'b1;
            end
            // Detecting the rising edge of the \'i_enable\' signal - END
            case (fsm_state)
                IDLE : begin
                    spi_cs_n <= \'b1;
                    o_busy <= \'b0;
                    if (enable_rising) begin
                        fsm_state <= PRE_DELAY;
                        data_out_shift <= i_data_in;
                        data_in_shift <= \'b0;
                        o_busy <= \'b1;
                    end
                end

                PRE_DELAY : begin
                    spi_cs_n <= \'b0;
                    if (clock_counter == (i_spi_clock_divider - 1)) begin
                        fsm_state <= SETUP;
                    end
                end

                SETUP : begin
                    if (~i_clock_phase) begin
                        spi_data_counter <= spi_data_counter + 1;
                        spi_mosi <= data_out_shift[$size(data_out_shift)-1];
                        data_out_shift <= data_out_shift << 1;
                    end
                    fsm_state <= TRANSMISSION;
                end

                TRANSMISSION : begin
                    if (i_clock_phase && i_clock_polarity) begin
                        if (spi_clock_rising == \'b1) begin      // Capture MISO data
                            data_in_shift <= {data_in_shift[$size(data_in_shift)-2:0], i_spi_miso};
                        end
                        if (spi_clock_falling == \'b1) begin
                            spi_data_counter <= spi_data_counter + 1;
                            spi_mosi <= data_out_shift[$size(data_out_shift)-1];
                            data_out_shift <= data_out_shift << 1;
                        end
                        if ((spi_data_counter == SPI_DATA_WIDTH) && (clock_counter == (i_spi_clock_divider-1)) && (spi_clock)) begin
                            spi_data_counter <= \'b0;
                            fsm_state <= POST_DELAY;
                        end
                    end
                    if (i_clock_phase && (~i_clock_polarity)) begin
                        if (spi_clock_falling == \'b1) begin      // Capture MISO data
                            data_in_shift <= {data_in_shift[($size(data_in_shift)-1):0], i_spi_miso};
                        end
                        if (spi_clock_rising == \'b1) begin
                            spi_data_counter <= spi_data_counter + 1;
                            spi_mosi <= data_out_shift[$size(data_out_shift)-1];
                            data_out_shift <= data_out_shift << 1;
                        end
                        if ((spi_data_counter == SPI_DATA_WIDTH) && (clock_counter == (i_spi_clock_divider-1)) && (~spi_clock)) begin
                            spi_data_counter <= \'b0;
                            fsm_state <= POST_DELAY;
                        end
                    end
                    if ((~i_clock_phase) && i_clock_polarity) begin
                        if (spi_clock_falling == \'b1) begin      // Capture MISO data
                            data_in_shift <= {data_in_shift[($size(data_out_shift)-2):0], i_spi_miso};
                        end
                        if (spi_clock_rising == \'b1) begin
                            spi_data_counter <= spi_data_counter + 1;
                            spi_mosi <= data_out_shift[$size(data_out_shift)-1];
                            data_out_shift <= data_out_shift << 1;
                        end
                        if ((spi_data_counter == SPI_DATA_WIDTH) && (spi_clock_rising)) begin
                            spi_data_counter <= \'b0;
                            fsm_state <= POST_DELAY;
                        end
                    end
                    if ((~i_clock_phase) && (~i_clock_polarity)) begin
                        if (spi_clock_rising == \'b1) begin      // Capture MISO data
                            data_in_shift <= {data_in_shift[($size(data_in_shift)-1):0], i_spi_miso};
                        end
                        if (spi_clock_falling == \'b1) begin
                            spi_data_counter <= spi_data_counter + 1;
                            spi_mosi <= data_out_shift[$size(data_out_shift)-1];
                            data_out_shift <= data_out_shift << 1;
                        end
                        if ((spi_data_counter == SPI_DATA_WIDTH) && (spi_clock_falling)) begin
                            spi_data_counter <= \'b0;
                            fsm_state <= POST_DELAY;
                        end
                    end
                end

                POST_DELAY : begin
                    if (clock_counter == (i_spi_clock_divider - 1)) begin
                        spi_cs_n <= \'b1;
                        fsm_state <= DONE;
                    end
                end

                DONE : begin
                    done <= \'b1;
                    o_data_out <= data_in_shift;
                    if (done) begin
                        done <= \'b0;
                        fsm_state <= IDLE;
                    end
                end

                default : begin
                    fsm_state <= IDLE;
                end
            endcase
        end
    end
    assign o_done = done;
    assign o_spi_cs_n = spi_cs_n;
    assign o_spi_mosi = spi_mosi;

endmodule

My original goal when writing the SPI Master was to have a module that supported all modes of the SPI protocol. As I shifted my focus to the audio processing elements of the design, this universal support lost relevance. This is to say: if our only goal is to configure the ADAU1761, a single-mode SPI Master can be implemented with a much more compact RTL description. I just happened to have this version ready to go, and it works the way we need it to in this context, so we might as well use it.

SPI Driver

The SPI Master is a generic module that can, in theory, be used to interact with any device that supports SPI Slave communication. Thus, we need to feed it the data that we want to send to the SPI Slave device (in our case, the audio codec on the ZedBoard). This is the job of the SPI Driver module.

To understand how the SPI Driver feeds data to the SPI Master, we need to be aware of the following:

The ADAU1761 defaults to I2C communication after each power cycle. To switch to SPI mode, the CS signal must be pulled down three times with dummy transactions following power-up. After that, the codec will remain in SPI mode until the next power cycle.

Except for the PLL register, each read/write operation includes four bytes:

  • The ADAU1761 defaults to I2C communication after each power cycle. To switch to SPI mode, the CS signal must be pulled down three times with dummy transactions following power-up. After that, the codec will remain in SPI mode until the next power cycle.
  • Except for the PLL register, each read/write operation includes four bytes:The LSB of the first byte determines whether the SPI Master wants to do a read (LSB = ‘1‘) or a write (LSB = ‘0‘). All registers in the audio codec contain one byte, except for the PLL register, which contains six bytes.
  • The second and third bytes indicate the 16-bit address of the register that the SPI Master wants to access.
  • The fourth byte contains the data sent by the Master in a write operation, or returned by the ADAU1761 in a read operation

Audio Passthrough Configuration

The ADAU1761 is a highly configurable audio codec, with built in mixing and processing capabilities. Of course, since we would like to do all the processing ourselves in the FPGA fabric, we don’t want any of that; the only thing we need the coded to handle is the DA and AD conversion. So, our setup should look like his:

Audio Tx -> ZedBoard Line In -> ADAU1761 AD -> Zynq -> ADAU1761 DA -> ZedBoard Line Out -> Audio Rx

This simple configuration requires quite a few SPI transactions to set everything up. These are captured in the Main FSM within the SPI Driver, which is listed below. As you can see, there are a lot of register values to set. I used a reference project by Digilent where they perform the SPI configuration in software as a guide.

module spi_driver # (
    parameter SPI_DATA_WIDTH = 8 
) (
    // Clock, reset
    input   logic i_clock,
    input   logic i_reset,
    // Control
    input   logic i_enable,
    // SPI Master Control
    input   logic [SPI_DATA_WIDTH-1 : 0]    i_data,
    input   logic                           i_done,
    input   logic                           i_busy,
    output  logic                           o_enable,
    output  logic [SPI_DATA_WIDTH-1 : 0]    o_data
);

    timeunit 1ns;
    timeprecision 1ps;

    // Edge detection for the \'i_enable\' input
    logic enable_delay1; 
    logic enable_delay2; 
    logic enable_rising;

    always_ff @(posedge i_clock) begin : enable_edge_detection
        enable_delay1 <= i_enable;
        enable_delay2 <= enable_delay1;
        enable_rising <= 1\'b0;
        if ((enable_delay2 == 1\'b0) && (enable_delay1 == 1\'b1)) begin
            enable_rising <= 1\'b1;
        end
    end

    // Main FSM - Begin
    enum logic [4:0]    {IDLE,
                        DUMMY_WRITE_1,
                        DUMMY_WRITE_2,
                        DUMMY_WRITE_3,
                        WRITE_CLOCK_CONTROL_REG,
                        WRITE_I2S_MASTER_MODE,
                        LEFT_MIXER_ENABLE,
                        LEFT_0_DB,
                        RIGHT_MIXER_ENABLE,
                        RIGHT_0_DB,
                        PLAYBACK_LEFT_MIXER_UNMUTE_ENABLE,
                        PLAYBACK_RIGHT_MIXER_UNMUTE_ENABLE,
                        HEADPHONE_OUTPUT_LEFT_ENABLE,
                        HEADPHONE_OUTPUT_RIGHT_ENABLE,
                        PLAYBACK_RIGHT_MIXER_LINE_OUT_ENABLE,
                        PLAYBACK_LEFT_MIXER_LINE_OUT_ENABLE,
                        LINE_OUT_LEFT_ENABLE,
                        LINE_OUT_RIGHT_ENABLE,
                        ADCS_ENABLE,
                        CHANNELS_PLAYBACK_ENABLE,
                        DACS_ENABLE,
                        SERIAL_INPUT_L0_R0_TO_DAC_LR,
                        SERIAL_OUTPUT_ADC_LR_TO_SERIAL_OUTPUT_L0_R0,
                        CLOCK_ALL_ENGINES_ENABLE,
                        CLOCK_GENERATORS_ENABLE} fsm_state = IDLE;

    always_ff @(posedge i_clock) begin
        case (fsm_state)
            IDLE : begin
                o_enable <= 1\'b0;
                o_data <= \'b0;
                if (enable_rising == 1\'b1) begin
                    fsm_state <= DUMMY_WRITE_1;
                end
            end

            DUMMY_WRITE_1 : begin
                o_enable <= 1\'b1;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= DUMMY_WRITE_2;
                end
            end

            DUMMY_WRITE_2 : begin
                o_enable <= 1\'b1;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= DUMMY_WRITE_3;
                end
            end

            DUMMY_WRITE_3 : begin
                o_enable <= 1\'b1;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= WRITE_CLOCK_CONTROL_REG;
                end
            end

            WRITE_CLOCK_CONTROL_REG : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00400007;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= WRITE_I2S_MASTER_MODE;
                end
            end

            WRITE_I2S_MASTER_MODE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00401501;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= LEFT_MIXER_ENABLE;
                end
            end

            LEFT_MIXER_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00400A01;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= LEFT_0_DB;
                end
            end

            LEFT_0_DB : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00400B05;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= RIGHT_MIXER_ENABLE;
                end
            end

            RIGHT_MIXER_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00400C01;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= RIGHT_0_DB;
                end
            end

            RIGHT_0_DB : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00400D05;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= PLAYBACK_LEFT_MIXER_UNMUTE_ENABLE;
                end
            end

            PLAYBACK_LEFT_MIXER_UNMUTE_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00401C21;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= PLAYBACK_RIGHT_MIXER_UNMUTE_ENABLE;
                end
            end

            PLAYBACK_RIGHT_MIXER_UNMUTE_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00401E41;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= HEADPHONE_OUTPUT_LEFT_ENABLE;
                end
            end

            HEADPHONE_OUTPUT_LEFT_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h004023E6;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= HEADPHONE_OUTPUT_RIGHT_ENABLE;
                end
            end

            HEADPHONE_OUTPUT_RIGHT_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h004024E6;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= PLAYBACK_RIGHT_MIXER_LINE_OUT_ENABLE;
                end
            end

            PLAYBACK_RIGHT_MIXER_LINE_OUT_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00402109;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= PLAYBACK_LEFT_MIXER_LINE_OUT_ENABLE;
                end
            end

            PLAYBACK_LEFT_MIXER_LINE_OUT_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00402003;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= LINE_OUT_LEFT_ENABLE;
                end
            end

            LINE_OUT_LEFT_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h004025E6;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= LINE_OUT_RIGHT_ENABLE;
                end
            end

            LINE_OUT_RIGHT_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h004026E6;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= ADCS_ENABLE;
                end
            end

            ADCS_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00401903;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= CHANNELS_PLAYBACK_ENABLE;
                end
            end

            CHANNELS_PLAYBACK_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00402903;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= DACS_ENABLE;
                end
            end

            DACS_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h00402A03;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= SERIAL_INPUT_L0_R0_TO_DAC_LR;
                end
            end

            SERIAL_INPUT_L0_R0_TO_DAC_LR : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h0040F201;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= SERIAL_OUTPUT_ADC_LR_TO_SERIAL_OUTPUT_L0_R0;
                end
            end

            SERIAL_OUTPUT_ADC_LR_TO_SERIAL_OUTPUT_L0_R0 : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h0040F301;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= CLOCK_ALL_ENGINES_ENABLE;
                end
            end

            CLOCK_ALL_ENGINES_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h0040F97F;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= CLOCK_GENERATORS_ENABLE;
                end
            end

            CLOCK_GENERATORS_ENABLE : begin
                o_enable <= 1\'b1;
                o_data <= 32\'h0040FA03;
                if (i_done == 1\'b1) begin
                    o_enable <= 1\'b0;
                    fsm_state <= IDLE;
                end
            end

            default : begin
                fsm_state <= IDLE;
            end
        endcase
    end

endmodule

The SPI Controller includes an ‘enable’ input, which is used to trigger the codec configuration by pressing the ZedBoard’s ‘btnc’ button. This must only be done after power cycling the board, if the Zynq is reprogrammed with the power on after the codec has been configured, audio data will start flowing as soon as the bitstream is programmed. Keep this in mind, and make sure to only start outputting data when you are ready for it. An unexpected audio output might catch you off-guard, damage your equipment and, in the worst case, your ears!

We have now covered the three major supporting elements of our ZedBoard Audio Processor’s top level: the Debouncer, the Master Clock Generator and the SPI Controller. On the next post we will turn our attention to the Audio Processor Core, by far the most interesting element in our system and where we will spend most of our time.

Cheers,

Isaac


Leave a Reply

Your email address will not be published. Required fields are marked *