Tutorial 17 – Starting Audio (or a really complicated wire)

In this tutorial we will cover talking to the Analog Devices AU1761 audio processing chip in the ZedBoard. This chip is also present on the ZYBO board so most of this tutorial will work for that board too. You just need to change some of the pin placement constraints.

Overview

The grand goal of this tutorial is to take audio from an iPhone, iPod, or other device in on the audio line in input and pass that through to the headphone out output so you can hear the audio on some headphones. You heard that right. In this tutorial we will enable the ZedBoard to connect your headphones to your iPod. How cool is that?

Of course, the real purpose of the tutorial is to learn some things and get us set up to do more interesting things in the next tutorial. But for now, we need a way to configure the ADAU1761 chip on the ZedBoard. It is fairly complex and does not start up processing audio. It needs to be configured. To do that we will make a SPI interface that will let the ARM processor read and write registers in the audio chip. We will then write some code to configure the chip. We will also use the clocking wizard to generate a 24MHz clock to send to the ADAU1761 chip for its operation. And finally we will receive digital audio from the ADAU1761 chip using the I2S protocol and send that audio back out to the chip so it can send it to the headphones.

The SPI interface

SPI is a very simple four wire serial protocol. The ADAU1761 chip can talk I2C or SPI, and by default is configured for I2C. If you already have an I2C controller, and the ZYNC has a couple, then you can configure that to talk to the ADAU1761 chip. And that is just what most people would do. However, this tutorial is intended to be more generic and teach something about logic design, so we will write our own interface code. I2C is more complicated than SPI, so we will put the ADAU1761 chip into SPI mode and program it that way.

SPI interfaces use four wires for communication. One chip is always the master and decides when read or write operations will take place. There can be multiple slave devices which are controlled by the master. There is a clock generated by the master device, serial data from the master to the slave device, and serial data from the slave back to the master, and finally a chip select signal which tells the slave that the transaction is meant for it. You can read about the SPI interface in the ADAU1761 datasheet from the Analog Devices website. Look at page 41.

tut17fig1The figure to the right shows a timing diagram for a write cycle. The clock can be free running or run just during the transaction. CDATA and CLATCHn need to be setup before the rising edge of CCLK. A transaction needs at least four bytes, and the bytes are sent most significant bit first. The first byte contains seven zero bits followed by the read/write bit. The second byte contains the most significant byte of the register 16-bit address and the third byte contains the least significant byte of the address. The fourth byte is the data byte to write to the address. More than one data byte can be written and those just keep following. The address will increment by one for each subsequent byte written.

tut17fig2Here is an example of a read cycle. It is basically the same as the write cycle but the read/write bit is set high. The value of the CDATA input is ignored in the data part of the transaction and instead the COUT output is driven by the ADAU1761 and contains the data being read. This data is properly setup to the rising edge of the CCLK signal.

Getting Started

From Vivado, select Tools→Create and Package IP, like we have done in the other tutorials. Choose to Create a new AXI4 peripheral, and give it the name adau1761_controller. Make it an AXI4 Lite slave with six registers. Click Next and then select Edit IP and click Finish. I won’t walk through all the steps in detail as you should be familiar with them from previous tutorials, but here are the highlights.

The Big Picture

tut17fig4

We are aiming to have an IP block which looks like the figure on the right. When we create the IP block in the IP Packager it will create the top module adau1761_controller_v1_0 and the register block adau1761_controller_v1_0_S00_AXI. We will create the adau1761_spi module and connect its control and status signals to the register block.

Modify the Register Block

Modify the adau1761_controller_v1_0_S00_AXI.v file to have the following ports. These will connect to the control and status signals in our adau1761_spi module. These allow the ARM processor to control the SPI interface and to read status from the interface.

 output wire reset,
 output wire start,
 output wire read,
 output wire [2:0] nbytes,
 output wire [15:0] address,
 output wire [63:0] write_data,
 input wire busy,
 input wire [63:0] read_data,

The reset output will reset the SPI state machine. The start output will start a SPI transaction when it rises. The read output specifies whether a read or write transaction is to occur, and the nbytes output specifies the number of bytes in a transaction. Actually it specifies one less than the number of bytes. A zero value implies one byte and a value of seven implies eight bytes. The address value is just the 16-bit address for the transaction. The write_data output specifies what data to write. Since we can write up to eight bytes of data, it is an eight byte value. However, if you send fewer than eight bytes the value needs to be justified into the most significant bytes. So for example, if you send just one byte the data needs to be in write_data[63:56].

The busy signal tells the ARM that the unit is still sending or receiving data. The read_data signal is the data that was read back from the device. The read_data value is aligned like the write_data value.

At the end of the file add the following user logic.

// Add user logic here
assign reset = slv_reg0[25];
assign start = slv_reg0[24];
assign address = slv_reg0[15:0];
assign nbytes = slv_reg0[18:16];
assign read = slv_reg0[19];
assign slv_reg1 = {{C_S_AXI_DATA_WIDTH-1{1'b0}},busy};
assign write_data[31:0] = slv_reg2;
assign write_data[63:32] = slv_reg3;
assign slv_reg4 = read_data[31:0];
assign slv_reg5 = read_data[63:32];
// User logic ends

This logic maps the register bits into the control and status wires that leave and enter the module. The control registers and readable and writeable by the ARM. These are slv_reg0, slv_reg2, and slv_reg3. We want the reset signal to come from bit 25 of slv_reg0. So the first assign makes that happen. Likewise for the other control signals. For the status signals, the assignment is the other way around. The ARM does not write to those registers but instead they are set by the status inputs to the register block. The assignment to slv_reg1 says that it’s LSB is the busy input and the other bits are always zero. Similarly the slv_reg4 and slv_reg5 and made from the read_data bits.

Register 0, 2, and 3 and readable and writeable. Registers 1, 4, and 5 are read only. You will need to change the declarations for slv_reg1, slv_reg4, and slv_reg5 to be wires and remove the logic which writes to those registers.

Controller top-level

Now let’s focus on the adau1761_controller_v1_0.v file. Add the following ports which will be used to communicate with the ADAU1761 chip.

 // Users to add ports here
 (* mark_debug = "true" *) output adau1761_cclk,
 (* mark_debug = "true" *) output adau1761_clatchn,
 (* mark_debug = "true" *) output adau1761_cdata,
 (* mark_debug = "true" *) input adau1761_cout,

Notice that we have them marked for debug. That will let us look at the interaction of the FPGA with the ADAU1761 chip later in the tutorial.

We will also need to declare the register signals which will go from our register block into our SPI controller. Here are those, also with the debug attribute.

 (* mark_debug = "true" *) wire reset;
 (* mark_debug = "true" *) wire start;
 (* mark_debug = "true" *) wire read;
 (* mark_debug = "true" *) wire [15:0] address;
 (* mark_debug = "true" *) wire [2:0] nbytes;
 (* mark_debug = "true" *) wire [63:0] write_data;
 (* mark_debug = "true" *) wire busy;
 (* mark_debug = "true" *) wire [63:0] read_data;

We added the control and status ports to the register block. Now we need to change the register block instance in the top level module to connect the wires we just defined to the new ports that we added in the previous section.

The SPI Controller

All that is left for this file is to instantiate the actual SPI controller that will do the work.  We will call the module adau1761_spi and it takes two parameters. The first is called clk_period and specifies how many of our clock cycles there are in a SPI clock cycle. The assumption here is that the clock this logic is running off will be significantly faster than the SPI clock period. The maximum SPI clock period for the ADAU1761 is 100ns and we will be running off the 100MHz AXI clock. We actually probably want to run the SPI clock slower than 10MHz too, since there is no hurry and going slower is less likely to run into problems that could be difficult to debug. The second parameter is called max_bytes and it specifies the maximum number of bytes that are allowed in a SPI transaction. Because of our register layout the maximum number of bytes in this case is eight.

Actually, while we are here, let’s create a parameter for this top-level module which specifies the SPI clock period. That way a user of this IP block can specify what they want since our clock frequency may not always be 100MHz. Make the parameter called SPI_CLOCK_PERIOD.

In addition to all the control signals from the registers, our adau1761_spi block also has a clock input which is connected to the AXI clock s00_axi_aclk, and ports which connect to the ADAU1761 SPI interface. Don’t make the mistake of connecting the clk input of the adau1761_spi module to a signal called clk. There is no signal called clk at the top module. The clock at the top module is called s00_axi_aclk.

Now, as we move lower into our hierarchy of modules, we want to try to make the design more generic. This lets us reuse the design in other places. The SPI signal names at this level are all prefixed with adau1761_ and are not very generic. We will change those names in the adau1761_spi interface to have the names spi_clk, spi_frame, spi_mosi, and spi_miso. Here are the connections for that part of the instantiation.

 .spi_clk(adau1761_cclk),
 .spi_frame(adau1761_clatchn),
 .spi_mosi(adau1761_cdata),
 .spi_miso(adau1761_cout)

The clock signal is obvious, but I prefer the term frame when dealing with what the ADAU1761 calls the chip select. This is really the signal which specifies that the data on the other signals is valid and should be paid attention to. This signal frames the transaction. Also, it starts to get very confusing when you use terms like data in and data out on a SPI interface. Those terms are relative to the master or the slave. It is better to use the acronym MISO and MOSI for master out slave in and master in slave out. Those are unambiguous.

Now we are ready to tackle the SPI engine itself.

The SPI Engine

We need to first create the adau1761_spi.v file and make sure it is in the correct location in the IP repository. See my blog post on creating files in the correct location for IP packaging. If you just use the default location then Vivado will not do the right thing.

OK, let’s first declare our module and its parameters and ports.

`timescale 1ns/1ns
module adau1761_spi #
 (
 parameter clk_period = 100,
 parameter max_bytes = 8
 )
 (
 input clk,
 input reset,
 input start,
 output reg busy,
 input read,
 input [$clog2(max_bytes)-1:0] nbytes,
 input [15:0] address,
 input [max_bytes*8-1:0] write_data,
 output [max_bytes*8-1:0] read_data,
 output reg spi_clk,
 output reg spi_frame,
 output reg spi_mosi,
 input spi_miso
 );

That should be pretty self-explanatory. Next I declare a local parameter called clk_half_period. A local parameter is just a parameter which cannot be overridden by instantiating modules. They are useful to hold computed values which will not change. In this module, clk_period may be changed by the calling module but what constitutes half of that value cannot be changed by the calling module. This sounds confusing but it is not. Here is the declaration.

 localparam clk_half_period = clk_period/2;

So, if the calling module says that clk_period is 100, then clk_half_period will be set to 50. If the calling module says that clk_period is 10 then clk_half_period will be 5. The calling module cannot say that clk_period is 100 and clk_half_period is 70. It can only change clk_half_period indirectly.

Next we need to watch the start signals to see if it rises. That will indicate that we need to start a SPI transaction. Doing this is a common idiom that we have seen before.

reg [1:0] start_shift = 0;
always @(posedge clk)
start_shift <= {start_shift,start};
wire start_rise = start_shift == 2'b01;

Next we keep a counter which helps us create the SPI clock. The counter starts if we are not busy and we see a rise on the start signal. It counts if we are busy, and wraps when the count is equal to the clock period minus one.

  reg [$clog2(clk_period)-1:0] clk_count;
  always @(posedge clk)
    if (reset)
      clk_count <= 0;
    else if (!busy && start_rise)
      clk_count <= 0;
    else if (busy)
      if (clk_count == clk_period-1)
	clk_count <= 0;
      else
	clk_count <= clk_count+1;

Now we look at the count values and make the SPI clock rise and fall at the correct times.

  wire clk_rise = clk_count == clk_half_period;
  wire clk_fall = clk_count == 0;
  always @(posedge clk)
    if (clk_fall) spi_clk <= 0;
    else if (clk_rise) spi_clk <= 1;

Next we need a shift register which will hold the data that we will be shifting out. This will also do double duty on read transactions and shift the read data back in.

  reg [8*max_bytes+24:0] shift_reg;

We will also need a counter to count the number of bits that we have shifted so that we know when to end the transaction.

  integer shift_count

Now we have an always block which will load the shift register and shift it on the falling edge of the SPI clock. Remember we need to keep the data stable around the rising edge of the clock. If you have a keen eye you will see that our shift register has one more bit than you would expect. This extra bit is used to hold the half cycle of data at the start of the transaction. Here is the always block in its entirety.

  always @(posedge clk)
    if (reset)
      busy <= 0;
    else if (!busy)
      begin
        if (start_rise)
	  begin
	    busy <= 1;
	    shift_count <= 0;
	    shift_reg <= {8'b0,read,address,write_data};
	  end
        end
    else if (clk_rise)
      shift_count <= shift_count+1;
    else if (clk_fall)
      begin
	shift_reg <= {shift_reg,spi_miso};
	if (shift_count == 32+nbytes*8)
	  busy <= 0;
      end

Basically the important parts are we reset busy when we get reset. If we are not busy when start rises, then we become busy, set our shift count to zero, and load the shift register. If we are busy and the SPI clock rises, then we increment our shift count. Otherwise if we are busy and the SPI clock falls then we shift the shift register and if we hit the total number of bytes to shift then we go back to the idle state.

One little detail. Technically we should capture the spi_miso data on the rising edge of the SPI clock. We capture it here on the falling edge right before it changes. Because the SPI clock actually falls the cycle after clk_fall is true this is safe to do.

We need to set the read_data output to the value that was shifted in to the shift register. This only needs to be stable when we are not shifting so we just pass the shift register bits back directly.

  assign read_data = shift_reg[max_bytes*8-1:0];

The spi_frame output is just the inverse of busy.

  always @(*) spi_frame = ~busy;

And of course the SPI output spi_mosi is just the most significant bit of the shift register.

  always @(*) spi_mosi = shift_reg[8*max_bytes+24];

And don’t forget to end your module!

Now merge all the file changes and add the SPI_CLOCK_PERIOD parameter to the GUI view and package your IP.

Building the Full Chip

Now back in your original project go to the IP Integrator and instantiate an adau1761_controller IP component and the ZYNQ processor. Run all the block automations, and all the connection automations. Make the adau1761_* ports from the controller external and create the HDL wrapper by right-clicking on the audio_controller instance in the Sources window, and then run Generate Block Design.

You then need to add the constraints for the SPI interface. For expediency, here they are. You will need to modify as appropriate for the ZYBO board.

set_property IOSTANDARD LVCMOS33 [get_ports adau1761_cclk]
set_property IOSTANDARD LVCMOS33 [get_ports adau1761_clatchn]
set_property IOSTANDARD LVCMOS33 [get_ports adau1761_cdata]
set_property IOSTANDARD LVCMOS33 [get_ports adau1761_cout]
set_property PACKAGE_PIN AB4 [get_ports adau1761_cclk]
set_property PACKAGE_PIN AB1 [get_ports adau1761_clatchn]
set_property PACKAGE_PIN Y5 [get_ports adau1761_cdata]
set_property PACKAGE_PIN AB5 [get_ports adau1761_cout]

Now go ahead and Run Synthesis on the design. With that complete you should be able to Set Up Debug, found under the Flow Navigator under Synthesized Design, to add your signals that were marked for debug. This will let us watch the SPI engine talk to the ADAU1761 chip.

Ideally we would set up a simulation of the IP block using an AXI4 Lite bus functional model (BFM) and a model of the SPI interface on the ADAU1761 chip, The BFM allows the simulator to take the place of the ZYNQ processor and lets your test perform register reads and writes to stimulate the IP block. However, the BFM is a separate license from Xilinx and it is not free. I am working on a model which can be used in the IP Integrator but it is not ready yet.

After the debug core is inserted, generate a bitstream file and export it to the SDK. Then start the SDK.

 The ARM Code

Once SDK starts up create a new empty application called audio_control. Add a source file main.c and start by including the xparameters and adau1761_controller header files.

#include "adau1761_controller.h"
#include "xparameters.h"

Then we define a shorter name for the base address of the controller, mnemonic names for the register offsets, and some position information for the register fields.

#define AUDIO_BASE XPAR_ADAU1761_CONTROLLER_0_S00_AXI_BASEADDR

#define CTRL_ADDR ADAU1761_CONTROLLER_S00_AXI_SLV_REG0_OFFSET
#define BUSY_ADDR ADAU1761_CONTROLLER_S00_AXI_SLV_REG1_OFFSET
#define WRITE_LOW_DATA_ADDR ADAU1761_CONTROLLER_S00_AXI_SLV_REG2_OFFSET
#define WRITE_HIGH_DATA_ADDR ADAU1761_CONTROLLER_S00_AXI_SLV_REG3_OFFSET
#define READ_LOW_DATA_ADDR ADAU1761_CONTROLLER_S00_AXI_SLV_REG4_OFFSET
#define READ_HIGH_DATA_ADDR ADAU1761_CONTROLLER_S00_AXI_SLV_REG5_OFFSET

#define RESET_POS 25
#define START_POS 24
#define READ_POS 19
#define NBYTES_POS 16
#define ADDR_POS 0

Next we define the function that will perform a write transaction. It takes as a parameter the address, the data, and the number of bytes to write. The control word is composed of one less than the number of bytes and the address value shifted into the correct positions. We don’t need to worry about the read bit because that is zero for writes. Then the low and high portions of the data value. These need to be shifted into the most significant positions. Then the registers need to be written. Note that a rising edge on the start bit is what actually kicks off the transaction and it is located in the control register. So we write the control word, and then we write the control word again with the start bit set. Then we wait until the busy register returns zero. Here is the code.

void write_adau1761(uint32_t addr, uint64_t data, uint32_t nbytes)
{
  uint32_t busy;
  uint32_t control_word = (nbytes-1)<<NBYTES_POS|
    addr<<ADDR_POS;
  uint32_t data_low = data<<(8*(8-nbytes));
  uint32_t data_high = (data<<(8*(8-nbytes)))>>32;
  ADAU1761_CONTROLLER_mWriteReg(AUDIO_BASE,WRITE_LOW_DATA_ADDR,
    data_low);
  ADAU1761_CONTROLLER_mWriteReg(AUDIO_BASE,WRITE_HIGH_DATA_ADDR,
    data_high);
  ADAU1761_CONTROLLER_mWriteReg(AUDIO_BASE,CTRL_ADDR,control_word);
  ADAU1761_CONTROLLER_mWriteReg(AUDIO_BASE,CTRL_ADDR,control_word|
    (1<<START_POS));
  while ((busy = ADAU1761_CONTROLLER_mReadReg(AUDIO_BASE,BUSY_ADDR)));
}

The read function is similar but does not take the data parameter and instead returns a 64-bit value. We again set the control word, this time with the read bit set. We write the control word and then write it again with the start bit set. We then wait for busy to be low, and then read the received data. The read data does not need to be aligned since it is shifter in to the shift register from the LSB. But it does need to be masked. Here is the read function.

uint64_t read_adau1761(uint32_t addr, uint32_t nbytes)
{
  uint32_t busy;
  uint32_t control_word = 1<<READ_POS|(nbytes-1)<<NBYTES_POS|
    addr<<ADDR_POS;
  uint64_t data_low;
  uint64_t data_high;
  ADAU1761_CONTROLLER_mWriteReg(AUDIO_BASE,CTRL_ADDR,control_word);
  ADAU1761_CONTROLLER_mWriteReg(AUDIO_BASE,CTRL_ADDR,control_word|
    (1<<START_POS));
  while ((busy = ADAU1761_CONTROLLER_mReadReg(AUDIO_BASE,BUSY_ADDR)));
  data_low = ADAU1761_CONTROLLER_mReadReg(AUDIO_BASE,
    READ_LOW_DATA_ADDR);
  data_high = ADAU1761_CONTROLLER_mReadReg(AUDIO_BASE,
    READ_HIGH_DATA_ADDR);
  return ((data_high<<32)|data_low)&((1LL<<8*nbytes)-1);
}

Next we need a function to reset the SPI controller and place the ADAU1761 chip into SPI mode. By default the chip is in I2C mode and it needs to see the CLATCH signal fall three times to switch into SPI mode. Here is the reset code.

void reset_adau1761(void)
{
  ADAU1761_CONTROLLER_mWriteReg(AUDIO_BASE,CTRL_ADDR,1<<RESET_POS);
  ADAU1761_CONTROLLER_mWriteReg(AUDIO_BASE,CTRL_ADDR,0);
  read_adau1761(0,1);
  read_adau1761(0,1);
  read_adau1761(0,1);
}

Finally we need to initialize the ADAU1761 part. This is not as easy as it sounds. There are over twenty different registers that need to be configured. There is a good tutorial on doing this written by Mike Field on his blog at ZedBoard Audio. I highly recommend giving that a read if you want to understand more about configuring the chip. You can also download the datasheet from Analog Devices and read more about the part. For the purposes of this tutorial however, we just need to write the registers with the magic values. Here’s the code.

void init_adau1761(void) {
  volatile int i;
  //  setup PLL
  write_adau1761(0x4000,0x0e,1);
  //  Configure PLL
  write_adau1761(0x4002,0x007d000c2301,6);
  //  Wait for PLL to lock
  while (!(read_adau1761(0x4002,6)&2));
  //  Enable clock to core
  write_adau1761(0x4000,0xf,1);
  //  delay
  for (i=0; i<1000000; i++);
  //  I2S master mode
  write_adau1761(0x4015,0x01,1);
  //  left mixer enable
  write_adau1761(0x400a,0x01,1);
  //  left 0db
  write_adau1761(0x400b,0x05,1);
  //  right mixer enable
  write_adau1761(0x400c,0x01,1);
  //  right 0db
  write_adau1761(0x400d,0x05,1);
  //  Playback left mixer unmute, enable
  write_adau1761(0x401c,0x21,1);
  //  Playback right mixer unmute, enable
  write_adau1761(0x401e,0x41,1);
  //  Enable headphone output left
  write_adau1761(0x4023,0xe7,1);
  //  Enable headphone output right
  write_adau1761(0x4024,0xe7,1);
  //  Enable line out left
  write_adau1761(0x4025,0xe7,1);
  //  Enable line out right
  write_adau1761(0x4026,0xe7,1);
  //  Enable both ADCs
  write_adau1761(0x4019,0x03,1);
  //  Enable playback both channels
  write_adau1761(0x4029,0x03,1);
  //  Enable both DACs
  write_adau1761(0x402a,0x03,1);
  //  Serial input L0,R0 to DAC L,R
  write_adau1761(0x40f2,0x01,1);
  //  Serial output ADC L,R to serial output L0,R0
  write_adau1761(0x40f3,0x01,1);
  //  Enable clocks to all engines
  write_adau1761(0x40f9,0x7f,1);
  //  Enable both clock generators
  write_adau1761(0x40fa,0x03,1);
}

Then you just need a main() routine to call the reset and init functions.

int main(void) {
  reset_adau1761();
  init_adau1761();
}

And there you have the C code to initialize the part. Now we will turn our attention to building the rest of the FPGA that can pass the audio through.

Generating the Audio Clock

The ADAU1761 chip needs to get a 24MHz clock from the FPGA. We will use the clocking wizard to create one for it to use. Go back to your FPGA project and open the IP Integrator. Click the Add IP icon and add a Clock Wizard component.

The Re-customize IP window should pop up. If it does not right-click on the IP component and select Re-customize IP. In the Clocking Options tab  make sure the Primary clock input frequency is set to 100MHz. In the Output Clocks tab make sure the output frequency is set to 24MHz. Make sure that the Clocking Feedback source is set to Automatic Control On-Chip. Also, lower on that page make select Active Low for the Reset Type. Then click OK.

Do not run the connection automation. Connect the FCLK_CLK0 clock output from the ZYNQ to the clk_in1 input of the Clocking Wizard. Connect the FCLK_RESET0_N output from the ZYNQ to the resetn input of the Clocking Wizard. Right-click in the IP Integrator Diagram window and create an output port called adau1761_mclk and another called led0. Connect the clk_out1 port of the Clocking Wizard to the adau1761_mclk port and the locked output to the led0 port. This will let us know that the PLL has locked by looking at the LED.

Loopback the Digital Audio

Now we will construct the logic to loopback the digital audio input to the digital audio output.

The audio data coming from and going to the ADAU1761 chip uses the I2S protocol. This protocol is similar to SPI in that it uses serial data and a clock. Unlike SPI however, the data never stops. There is no concept of a transaction, it just streams constantly. Rather than a frame signal, there is a channel select signal. When the channel select signal is low the data is for the left channel and when it is high, the data is for the right channel. The data is sent most significant bit first and the data is encoded using twos complement. One device is the master and generates the serial clock and word select signal. If it is transmitting the audio data from the master to the slave, then it also sends the serial data. If it is receiving then the slave sends the serial data.

In our case, there is a transmit and receive stream of data and the ADAU1761 is the master. For all practical purposes that means we just need to take the data we receive and send it back to the master. In the next tutorial we will actually process the I2S data.

For now, create a new IP component. Give it an AXI4 Lite slave interface and eight registers. We won’t be using the registers for now. Call the IP an adau1761_data component.

To the top level module add the following ports.

   // Users to add ports here
   (* mark_debug = "true" *) input wire adau1761_bclk,
   (* mark_debug = "true" *) input wire adau1761_lrclk,
   (* mark_debug = "true" *) input wire adau1761_adc_sdata,
   (* mark_debug = "true" *) output wire adau1761_dac_sdata,
   // User ports ends

Then at the end of the file loopback the ADC data to the DAC.

  // Add user logic here
  assign adau1761_dac_sdata = adau1761_adc_sdata;
  // User logic ends

Go ahead and merge the changes and repackage the IP. Don’t worry about the registers for now. We will hook those up in the next tutorial.

tut17fig3Back in your main project go ahead and run the connection automation. Then make the I2S ports external. When complete your design should look like the figure to the right. If everything looks good you should be able to generate the block design.

We need to add some pin constraints to the design as well. Here are the ones I have.

set_property IOSTANDARD LVCMOS33 [get_ports adau1761_mclk]
set_property PACKAGE_PIN AB2 [get_ports adau1761_mclk]
set_property IOSTANDARD LVCMOS33 [get_ports led0]
set_property PACKAGE_PIN T22 [get_ports led0]

set_property IOSTANDARD LVCMOS33 [get_ports adau1761_adc_sdata]
set_property IOSTANDARD LVCMOS33 [get_ports adau1761_dac_sdata]
set_property IOSTANDARD LVCMOS33 [get_ports adau1761_bclk]
set_property IOSTANDARD LVCMOS33 [get_ports adau1761_lrclk]
set_property PACKAGE_PIN AA7 [get_ports adau1761_adc_sdata]
set_property PACKAGE_PIN Y8 [get_ports adau1761_dac_sdata]
set_property PACKAGE_PIN AA6 [get_ports adau1761_bclk]
set_property PACKAGE_PIN Y6 [get_ports adau1761_lrclk]

Once the constraints are entered you should be able to synthesize the design.

Setting Up Debug

Run the Setup Debug tool to set up the embedded logic analyzer. We will use this to look at the SPI interface as well as the audio data. Since we have marked all the nets we are interested in just add all of them.Notice that some nets have an undefined clock domain. These correspond to the I2S interface signals. Also notice that there are only three signals and not four. That is because the ADC and DAC data signals are now the same. We need to define the clock domain to be used for the I2S signals. We could place them on the FCLK_CLK0 clock coming from the ZYNQ processor, but instead let’s put them on the mclk audio clock domain. This will have the effect of creating a second logic analyzer unit.

Right-clock on the red undefined label in the Clock Domain column and select Select Clock Domain… Then select the adau1761_mclk_OBUF clock. Do the same for the other undefined signals. You should then be able to clock Next. Select a sample data depth of 16384 and click Next. Then click Finish.

You should now be able to generate the bitstream.

Putting it All Together

You should be able to program the FPGA and run the program to configure the ADAU1761. Note that when you first program the FPGA you will get a message saying there are no debug cores. This is because the ARM has not yet been configured to there is no clock going to the embedded logic analyzer. Once you run the software the ARM will be configured and Vivado will be able to see the logic analyzers.

You should now be able to plug an iPod or other audio source into the line in input on the ZedBoard (the blue connector) and plug in some headphones (the black connector) and listen to your tunes. Use the embedded logic analyzer to see the digital audio as it passes through the FPGA. In the next tutorial we will actually receive the data and process it before sending it back out.

Files

Here are the files I have for this tutorial in case you get stuck or you just want to see how I implemented things.

77 thoughts on “Tutorial 17 – Starting Audio (or a really complicated wire)

  1. hi,Pete
    I just want to say,you are very kind of person!
    Thank you very much! I learn a lot from your tutorials.

  2. Hey Pete,

    Could you please tell how to set up the logic analyser? I followed all the steps. Then in SDK when I am programming the FPGA and launching on GDB, logic analyzer is not coming up! Could you please tell how to resolve this issue?

  3. hi Pete,

    Could you please tell how to set the logic analyzer?
    I followed all your steps then programmed the FGPA and launched it on GDB. The led0 glows red but there is no popping up of logic analyzer window, also I am unable to listen to anything on the line out?

  4. Pete,

    This tutorial has been really helpful. I have been working on a similar project. I followed the procedure and there is no output coming on Line out. I made sure frequency is correct and proper clock is given.Could it be because of Vivado version? I am using 2016.4. Which version did you use?

  5. You need to make sure to run the SetupDebug step after you have synthesized the design. This will let you specify which of the mark_debug signas you actually want to see. Then you need to run implementation to get a bit file. The software debug environment is not used to look at the logic analyzer signals. You need to run the hardware manager and connect that over JTAG like you do when you program the FPGA. The logic analyzer should come up there. For these tutorials I used 2015.1 but I recommend using 2019.1 now. Some screenshots are different but things work mostly the same.

  6. Pete,

    The Logic analyzer worked well. We are facing one more issue. The PLL is not getting locked. The red LED on the board is glowing, but when we debugged we found out that the SDK code(main.c) is getting stuck in the “while loop” where it has to check if the second register has value 2 (waiting for PLL to Lock).

    while (!(read_adau1761(0x4002,6)&2));

    If we remove that statement in SDK code, the whole C code is running but no song is being played.
    Is there any way you can tell to debug this and ensure that the PLL gets locked?

  7. It is hard for me to say what the problem could be. However, my guess is that you are actually not controlling the adau1761 correctly through the SPI interface. I would try writing a program to write a register and then read the result back. Choose a register in the device that has a wide field that you can write arbitrary values into. Confirm that you read back what you write. Use the ILA to confirm the values on the SPI interface into the chip. Let me know what you find out.

    -Pete

  8. Why are we using a 2 bit start_shift register? What do we gain from doing it that way (instead of using the start signal itself) besides the one clock cycle delay for the start_rise signal? Great Tutorials by the way!

  9. If you know the start signal is synchronized to clk then you can do exactly that. I just find the code is cleaner to use start_rise = start_shift == 2'b01; If the start signal is not synchronized to the clk signal then you need to put it through a flip-flop clocked off clk (and many would argue two) before you sample it. So the code would need to be written this way.

    I hope that helps.

    -Pete

  10. Pingback: FPGA music visualiser – part 1 – Cam Beaney's Blog

  11. Hi Pete,

    Really enjoying your tutorials. I have a Nexys Video board, would I be able to set this up using the Microblaze processor instead of the Zynq which the Nexys Video doesn’t have?

    Thanks!

  12. Yes, you can do similar things with the Microblaze. Just instantiate one of those cores. For bare metal programming things should work much the same. The board would need to support SDRAM if you wanted to boot Linux on the MicroBlaze.

    -Pete

  13. Hi Pete, I have a Zybo-Z7 board and I would like to rewrite the .xdc file.
    But I find it difficult to find which package pins are correct.
    Can you please show me how to find them?
    Thanks,
    Freddie

  14. Freddie,

    The first few tutorials go through how to use the schematic to find the package pins on the FPGA. Usually the pin on the FPGA is indicated where the signal on the board connects to the FPGA.

  15. I don’t have a Linux version of the program. But what you would do is use the Linux mmap kernel call to map the memory address of the adau1761 controller and then use the returned pointer to write to the adau1761. Most of the code would then be the same.

  16. Hey Guys,
    So if you have done everything correctly,and still if your audio is not working then just add some delay after each write_adau1761 function call in init function ,

    i.e, make that for loop delay as a function called delay and call it after each write_adau1761 function call

  17. Hey there Pete, I’m getting an error while trying to synthesize adau1761_controller IP. The error in Vivado 2018.2 is:

    [Synth 8-1852] concurrent assignment to a non-net slv_reg1 is not permitted [adau1761_controller_v1_0_S00_AXI.v”:436] (2 more like this)
    [Synth 8-1852] concurrent assignment to a non-net slv_reg4 is not permitted [adau1761_controller_v1_0_S00_AXI.v”:439]
    [Synth 8-1852] concurrent assignment to a non-net slv_reg5 is not permitted [adau1761_controller_v1_0_S00_AXI.v”:440]

    The code from adau1761_controller_v1_0_S00_AXI.v is:
    LINE 436: assign slv_reg1 = {{C_S_AXI_DATA_WIDTH-1{1’b0}},busy};
    LINE 439: assign slv_reg4 = read_data[31:0];
    LINE 440: assign slv_reg5 = read_data[63:32];

    Is there a remedy you’d recommend?

    Thanks!

  18. I think you must have modified the code in some way. What you say is on line 436, I see on line 403. sly_reg1, sly_reg4, and sly_reg5 all need to be declared as wire type. Earlier in the code you should see this.
    reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
    wire [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
    reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2;
    reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3;
    wire [C_S_AXI_DATA_WIDTH-1:0] slv_reg4;
    wire [C_S_AXI_DATA_WIDTH-1:0] slv_reg5;

    See how some are wire and some are reg?

  19. That was exactly right. I had them all set as registers for some reason. Thanks so much! My code now passes synthesis.

  20. Hi Pete, after generated bit stream I got two critical warning –
    TIMING #1 Critical Warning Invalid clock redefinition on a clock tree. The primary clock design_1_i/clk_wiz_0/inst/clk_in1 is defined downstream of clock clk_fpga_0 and overrides its insertion delay and/or waveform definition

    TIMING #1 Critical Warning A primary clock design_1_i/clk_wiz_0/inst/clk_in1 is created on an inappropriate internal pin design_1_i/clk_wiz_0/inst/clk_in1. It is not recommended to create a primary clock on a hierarchical pin when its driver pin has a fanout connected to multiple clock pins

  21. Hi Pete after creating adau1761_controller IP , after Building the Full Chip section I have generated bit stream without error and after that I created platform project in vitis and built it. I got following errors in driver.

    Description Resource Path Location Type
    make: *** [Makefile:30: zynq_fsbl_bsp/ps7_cortexa9_0/lib/libxil.a] Error 2 Platform C/C++ Problem

    Description Resource Path Location Type
    make[1]: *** [Makefile:46: ps7_cortexa9_0/libsrc/adau1761_controller_v1_0/src/make.libs] Error 2 Platform C/C++ Problem

  22. HI Pete, thank you so much for this great tutorial, while i am stocked by the adau1761_controller.h file. It would be great if you could provide me more information about this file? Thankssss!

  23. Maybe this is related to a newer release of Vivado. I certainly didn’t get that when I created the tutorial. But I was using 2015.2. So pretty far back. I’ll have to try with something newer.

  24. Dear Pete,

    This is a great project, thank you!
    To help others that are trying to get it working with a Pynq-Z2 board (https://www.xilinx.com/support/university/xup-boards/XUPPYNQ-Z2.html), it does work in Vivado 2020.1 (and later will also work).
    This header file (adau1761_controller.h) could help:

    #include
    #include
    #include
    #include

    #define ADAU1761_CONTROLLER_S00_AXI_SLV_REG0_OFFSET 0x00
    #define ADAU1761_CONTROLLER_S00_AXI_SLV_REG1_OFFSET 0x04
    #define ADAU1761_CONTROLLER_S00_AXI_SLV_REG2_OFFSET 0x08
    #define ADAU1761_CONTROLLER_S00_AXI_SLV_REG3_OFFSET 0x0C
    #define ADAU1761_CONTROLLER_S00_AXI_SLV_REG4_OFFSET 0x10
    #define ADAU1761_CONTROLLER_S00_AXI_SLV_REG5_OFFSET 0x14

    void write_adau1761(uint32_t addr, uint64_t data, uint32_t nbytes);
    uint64_t read_adau1761(uint32_t addr, uint32_t nbytes);
    void reset_adau1761(void);
    void init_adau1761(void);
    void ADAU1761_CONTROLLER_mWriteReg(uint32_t base, uint32_t offset, uint32_t data){
    Xil_Out32( base + offset, data);
    }

    uint32_t ADAU1761_CONTROLLER_mReadReg(uint32_t base, uint32_t offset){
    return Xil_In32(base + offset);
    }

    And these are the constraints for the Audio (xdc):
    ##Locked PLL
    set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33} [get_ports led0]

    ##Audio

    set_property -dict {PACKAGE_PIN M17 IOSTANDARD LVCMOS33} [get_ports adau1761_clatchn]
    set_property -dict {PACKAGE_PIN M18 IOSTANDARD LVCMOS33} [get_ports adau1761_cdata]

    set_property -dict {PACKAGE_PIN U5 IOSTANDARD LVCMOS33} [get_ports adau1761_mclk]
    set_property -dict {PACKAGE_PIN T9 IOSTANDARD LVCMOS33} [get_ports adau1761_cout]
    set_property -dict {PACKAGE_PIN U9 IOSTANDARD LVCMOS33} [get_ports adau1761_cclk]
    set_property -dict {PACKAGE_PIN F17 IOSTANDARD LVCMOS33} [get_ports adau1761_adc_sdata]
    set_property -dict {PACKAGE_PIN G18 IOSTANDARD LVCMOS33} [get_ports adau1761_dac_sdata]
    set_property -dict {PACKAGE_PIN T17 IOSTANDARD LVCMOS33} [get_ports adau1761_lrclk]
    set_property -dict {PACKAGE_PIN R18 IOSTANDARD LVCMOS33} [get_ports adau1761_bclk]

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.