Monday 18 February 2019

Creating Sound on the Zybo board

Foreword

In the previous post we start going down the alley of Tape Emulation and ended off writing some Python code for converting a .TAP file to sound.

The sound that we generate is basically a set of pulses of varying widths. Outputting this pulse widths as sound is a quick sanity check if we implemented tape emulation more or less correctly.

Our next goal is to see if we can implement this sound generation from a .TAP file in real time within the Zybo boards FPGA.

Playing the generated sound on the Zybo is perhaps the most complex part of the exercise, so I have decided to dedicate this post to Zybo sound generation.

Sound on the Zybo board

One of the nice features of the Zybo board is that it supports onboard sound. The Zybo board simply cannot hide away this feature because of the familiar color coded Line In/Out/Mic ports:


These ports are all hooked up to a Audio Codec chip from Analogue devices: The SSM2603.

This Audio Codec have two ports that hooks up to the ZYNQ SoC: an I2C port and an I2S port. The I2C port is used to configure the Audio Codec like sample rate and volume control.

The I2S port is used to transmit digital audio data between the ZYNQ and Audio Codec.

Both the I2S port and I2C port is linked to pins on the ZYNQ of which only the FPGA has access to.

Configuring the Audio Codec

As mentioned in the previous section, configuation of the audio codec is done via a I2C port.

Implementing a I2C port in an FPGA can be a daunting task, and one will be pleased to learn that the Zynq have two I2C onchip peripherials.

Shortly after discovering this, one might feel someone burst your bubble by discovering that the I2C port of the Audio codec is hooked up to pins that onchip peripherals don't have direct access to.

But fear not! The Zynq allows you to configure the ports of onchip peripherals to be redirected via EMIO. This basically means that you can make these ports available to the FPGA. Within the FPGA you can then either decide to hook up these ports directly to the output pins or you can join hook the ports to custom logic blocks.

The following block diagram within Vivado shows how this is achieved:



I have marked in red the ports of an onchip peripheral that I have exposed to the FPGA.

These ports I have hooked up to two instances of a custom logic block iobuf. This logic block is basically an implementation of a tristate buffer.

We can now proceed and write some code to initialise the Audio Codec. Firstly we need to initialise the onchip i2c peripheral we are going to use:

int main()
{
...
    Xil_Out32(0xe000501c, 0x1f);
//Set divider + addressing mode
    Xil_Out32(0xE0005000, 0x9004);
//master -> ACK -> CLR FIFO -> hold bus
    Xil_Out32(0xE0005000, 0x9004 + 2 + 8);
...

}

I have added a bit of comments on what is going on during initialisation, but I am not going to go into too much detail here. More details is provided in the Zynq Technical reference manual in Chapter 20: I2C Controller as well as Appendix B, in the register details for the I2C controller.

Let us now write some methods to read and write to the registers of Audio Codec:

...
int readReg(int addr) {
 //master -> ACK -> CLR FIFO -> hold bus
     u32 in2 = Xil_In32(0xE0005000) | 64 | 16;
     in2 = in2 & ~1;
     Xil_Out32(0xE0005000, in2);
     //write data to register
         Xil_Out32(0xE000500c, addr << 1);
     //write address
         Xil_Out32(0xE0005008, 26);
     // Wait for completion
         u32 status = Xil_In32(0xe0005010) & 1;
         do {
          status = Xil_In32(0xe0005010) & 1;
         } while (!status);

         //clear interrupts
         Xil_Out32(0xe0005010, 1);

         //set hold bus -> read -> clear fifo
         in2 = Xil_In32(0xe0005000) | 16 | 1 | 64;
         Xil_Out32(0xe0005000, in2);
         //set transfer size
         Xil_Out32(0xe0005014, 2);
         //set address
         Xil_Out32(0xe0005008, 26);
         //clear hold
         in2 = Xil_In32(0xe0005000) & (~16);
         Xil_Out32(0xe0005000, in2);
         //wait for completion
         do {
          status = Xil_In32(0xe0005010) & 1;
         } while (!status);
         Xil_Out32(0xe0005010, 1);
         u32 byte0 = Xil_In32(0xe000500c);
         u32 byte1 = Xil_In32(0xe000500c);
         return byte0 | (byte1 << 8);

}
...
void writeReg(int addr, int data) {
 //master -> ACK -> CLR FIFO -> hold bus
     u32 in2 = Xil_In32(0xE0005000) | 64 | 16;
     in2 = in2 & ~1;
     Xil_Out32(0xE0005000, in2);
     //write data to register
         Xil_Out32(0xE000500c, (addr << 1) | ((data & 256) ? 1 : 0));
         Xil_Out32(0xE000500c, data & 255);
     //write address
         Xil_Out32(0xE0005008, 26);
     // Wait for completion
         u32 status = Xil_In32(0xe0005010) & 1;
         do {
          status = Xil_In32(0xe0005010) & 1;
         } while (!status);

         //clear interrupts
         Xil_Out32(0xe0005010, 1);

         in2 = Xil_In32(0xe0005000) & (~16);
         Xil_Out32(0xe0005000, in2);
       return;
}
...

Again, here is lot of things going on here and can be best understood with the Zynq Technical Reference Manual. Here it is also handy to have the Datasheet for the SSM2603 Audio Codec available to understand the format required for setting and reading registers.

We can now continue and write some code for initialising the Audio Codec:

int main()
{
...
    writeReg(15,0);
    usleep(1000);
    writeReg(6, 16 + 32 + 64);
    writeReg(2, 0b101111001);
    writeReg(3, 0b101111001);
    writeReg(4, 0);
    writeReg(5, 0);
    writeReg(7, 1);
    writeReg(8, 0);
    usleep(1000);
    writeReg(9, 1);
    usleep(1000);
    writeReg(6, 32);
    usleep(1000);
    writeReg(4,16+6);

...
}

Let me give a quick run down what is happening here.

The first write to register 15 forces the Audio Codec to write default values to all registers.

The write to register 6 powers up all blocks within the Audio Codec accept the Out Block. According to the datasheet we can only enable the out block later in the initialisation process.

The writes to registers 2 and 3 sets the volume of the left and right DAC.

Next, let us skip straight to the write to register 7. This write informs the format of the samples that will be presented to the I2S bus, which in this case is 16 bit samples that is left justified.

With the write to register 8 we are setting the actual sample rate, which is 48KHz.

With the write to register 9 we are enabling the digital core. Note that it is preceeded by a small delay. According to the datasheet a short delay should be allowed after all blocks are powered up.

With the write to register 6 we are finally powering up the Out block and with the write to register 4 we are enabling the DAC.

You will also see that between the write to register 6 and the write to register 4 I have also added as small delay. Nowhere in the datasheet it is specified that it is necessary to do this. However, with trail and error i have found that if you do not add this delay you can do whatever you want, you will not get any sound output to the speaker.

This concludes the configuration of the Audio Codec. In the next section we will discuss how to implement the I2S interface.

Implementing the I2S interface

To implement a I2S interface is much simpler than a I2C interface.

To start off let us have a look at a I2S timing diagram from the Audio Codec datasheet:

Within the datasheet you will see other timing diagrams for other Input modes, but we will only be focusing on Left-Justified mode.

A signal not present in the above diagram is MCLK (e.g. Master Clock) which is 256 times the sampling rate.

Back to the diagram. The first waveform (RECLRC/PBLRC), indicates for which channel the current sample is applicable for.

The BCLK generates a pulse for each bit of data. In our case where we have 16 bits per channel, the frequency will be 32 times the sample rate.

Lastly we have the signal RECDAT/PBDAT that is the actual sample data.

All three signals together with MCLK should all be in sync to avoid data corruption. We will see in a moment how this is done.

Now let us calculate the frequencies for the different clocks.

As mentioned earlier on MCLK is 256 times the sample rate. Thus MLCK should be 12.288MHz.

BCLK is 32 times the sample rate and therefore is the frequency 1.536MHz.

We will generate the 12.288MHz clock with a clock wizard within Vivado. The resulting clock we will need to forward externally from the Zynq to the Audio Codec. Xilinx recommends not to forward a generated clock directly to an output pin, but rather to make use of an ODDR component. The following module definition will take care of this:

module oddr_buf(
  output Mlck_O,
  input clk_in
    );

   ODDR #(
      .DDR_CLK_EDGE("OPPOSITE_EDGE"), // "OPPOSITE_EDGE" or "SAME_EDGE" 
      .INIT(1'b0),    // Initial value of Q: 1'b0 or 1'b1
      .SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
   ) ODDR_inst (
      .Q(Mlck_O),   // 1-bit DDR output
      .C(clk_in),   // 1-bit clock input
      .CE(1), // 1-bit clock enable input
      .D1(1), // 1-bit data input (positive edge)
      .D2(0), // 1-bit data input (negative edge)
      .R(0),   // 1-bit reset
      .S(0)    // 1-bit set
   );

endmodule


We pass the generated clock to clk_in. The output port Mlck_O is the signal we should assign to an output pin.

Now onto the generation of the rest of the I2S signals. We start by creating an empty module with the required ports:

module i2s(
  input clk,
  output clk_1_5_mhz,
  output channel_enable,
  output out_data,
    );

endmodule

For the input port clk we pass the generated 12.288MHz signal. clk_1_5_mhz is our generated bclk signal.

channel_enable is the channel indicator and out_data the actual sample data.

Let us write some code to generate the bclk signal:

...
reg [1:0] clk_div_counter = 0;
reg bclk_int = 0;
...
    always @(posedge clk)
    if (clk_div_counter == 3)
      bclk_int <= ~bclk_int;

    always @(posedge clk)
        clk_div_counter <= clk_div_counter + 1; 
...

So, the bclk clock is generated from the MCLK by means of a clock divider.

Both the remaining signals transition on the negative edge of BCLK, so us quickly create a wire signalling this behaviour:

...
    wire neg_edge;
...
    assign neg_edge = (clk_div_counter == 3) & (bclk_int == 1) ? 1 : 0;
...

Next, let us write code for the channel indicator:

...
    assign channel_enable = prclk_int;
...
    reg [3:0] channel_enable_counter = 0;
...
    always @(posedge clk)
    if (neg_edge)
      channel_enable_counter <= channel_enable_counter + 1;

    always @(posedge clk)
    if (neg_edge & channel_enable_counter == 15)
      prclk_int <= ~prclk_int;
...

And now let us write some code for out data:

...
    reg [31:0] shift_reg;
...
    assign out_data = shift_reg[31];
...
    always @(posedge clk)
    if (channel_enable_counter == 15 & neg_edge)
    begin
      shift_reg <= {data_val, data_val};
    end
    else if (neg_edge)
      shift_reg <= {shift_reg[30:0] , 1'b0};
...

As you can see, we have implemented a shift register for shifting out the sample values, which we reload each time the channel indicator signal toggles.

data_val is the actual sample value, which we haven't defined yet. For this we are going to define something very simple, which will be a monotone with a frequency between 2000Hz and 3000Hz. For this we can just alternate the sample value between 30000 and 0 every 6th sample:

...
    reg [15:0] data_val = 0;
...
    always @(posedge clk)
    if (channel_enable_counter == 15 & neg_edge)
    begin
      if (sample_mod_counter == 0)
      begin
        sample_mod_counter <= 6;
        data_val <= (data_val == 0) ? 30000 : 0;        
      end
      else
        sample_mod_counter <= sample_mod_counter - 1;
    end
...

What only remains is to link up the external pins to our audio codec:


This is all there is for creating sound on the Zybo board, which in this case will be a monotone

In Summary

In this post we played around with sound on the Zybo board and managed to generate a monotone.

This exercise will aid us in the next post to create a cassette interface and verify the design by listening to the produced pulses.

This post will also come in handy in future posts where we implement SID emulation.

Till next time!