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