Friday, 26 April 2019

Integrating Tape Interface with C64 Module: Part 1

Foreword

In the previous post we created the building blocks for simulating the PWM output of a Commodore 1530 Datasette given a .TAP file.

What we need to next is to integrate our Tape simulator with our existing C64 module so we can start loading some programs stored in .TAP format.

With integration of our Tape simulator to our C64 module, the following comes to mind: Memory access complexity. Whereas in our previous post where we only needed to make use of one AXI port for memory access, we now need to make use of three AXI ports:


  • One for writing produced VIC-II frames to SDRAM.
  • A second AXI port used by the VGA block retrieving the produced VIC-II frames from SDRAM for display purposes
  • A third AXI port, to be implemented in this post, for reading .TAP file data for our Tape interface.
Adding a third AXI port is probably not such a big deal, but it is important that we establish that each component gets its fair share of memory bandwidth.

Thus, in this post we will focus on integrating the Tape interface with SDRAM, along with the VIC-II- and VGA-module. We will then verify that there is not any Memory bandwidth contention by checking that the frames are rendered properly on screen and that the Tape Interface produces pulses with widths that corresponds to the .TAP file stored in memory.

Encapsulating the Tape interface

In the previous post we have developed the various building blocks for our Tape Interface, but we actually haven't discuss how we glue these blocks together.

We need to create a module containing instances of these blocks:

module tape_interface(
  input clk,
  input clk_1_mhz,
  input restart,
  input reset,
  output [31:0] ip2bus_mst_addr,
  output [11:0] ip2bus_mst_length,
  input [31:0] ip2bus_mstrd_d,
  output [4:0] ip2bus_inputs,
  input [5:0] ip2bus_otputs,
  output pwm
    );
    
wire data_valid_read_word;
wire [7:0] byte_data;
wire [31:0] r_word_data_out;
wire ack_byte_slice;
wire data_valid_byte;
wire ack_sample_assem;
wire [23:0] timer_val;
wire load_timer;
    
read_word r_word(
      .clk(clk),
      .restart(restart),
      .reset(reset),
      .ack(ack_byte_slice),
      .ip2bus_mst_addr(ip2bus_mst_addr),
      .ip2bus_mst_length(ip2bus_mst_length),
      .ip2bus_mstrd_d(ip2bus_mstrd_d),
      .data_wire_out(r_word_data_out),
      .ip2bus_inputs(ip2bus_inputs),
      .ip2bus_otputs(ip2bus_otputs),
      .data_valid(data_valid_read_word)
        );
        
byteslicer byte_slice(
          .clk(clk_1_mhz),
          .data_valid(data_valid_read_word),
          .byte_out(byte_data),
          .ack(ack_byte_slice),
          .data_in(r_word_data_out),
          .restart(restart),
          .data_valid_out(data_valid_byte),
          .read(ack_sample_assem)
            );
        
sample_assembler samp_assem(
              .clk(clk_1_mhz),
              .data_valid(data_valid_byte),
              .data(byte_data),
              .ack(ack_sample_assem),
              .pwm(pwm),
              .timer_val(timer_val),
              .load_timer(load_timer),
              .restart(restart)
                );
        
tape_pwm t_pwm(
                  .time_val(timer_val),
                  .load_timer(load_timer),
                  .pwm(pwm),
                  .clk(clk_1_mhz)
                    );
    
endmodule


Let us now see how this block fits in with the rest of the design:


Our new AXI interface block is the one on the left bottom. This will be the AXI interface block we use for retrieving .TAP file data from SDRAM.

In the above screenshot I have also showing one of the existing AXI interface blocks. The reason for this is because this block also include a Slave Port, which we will extend so that our ARM core can access information about the tape_interface (e.g. PWM and Restart).

Later on in this post we will be writing a C program that will run on the ARM core, that will measure the pulse widths from the PWM pin. In this way we can determine if the correct pulses is produced.

We will again make use of Block diagram assistance to connect the new AXI Master port, which I described in an earlier post.

For interest sake I just would like to show how the resulting wiring looks like:


Our key component here is axi_smc. It has now three S_AXI inputs on the left, whereas previously there was only two.

At the output of the axi_smc, however, we are still outputting only two M_AXI ports. This is because the Zynq block only supports up to two S_AXI_GP ports. So, two of our input AXI ports needs to share a GP port and this is where memory mapping becomes very important, which we will cover in the next section.

Memory Mapping

Let us have a look at the address mapping:


The first block, myip_burst_read_test_0 is an AXI read block for reading data for the VGA block. This block consumes the most of the memory bandwidth in our design. This block make use of GP0

You can see that the next two blocks, myip_burst_read_test_0 and myip_burst_test_0 share both S_AXI_GP1 of the Processing system. Together these two blocks consumes much less bandwidth than the VGA block. For this reason I have group them together to use the same GP port.

Verifying the design

With everything hooked up, it is time to verify the design. We will do this by checking that the VGA output is rendered correctly and that pulse widths produced by our Tape interface is correct.

The most difficult part is validating the pulse widths. For this purpose we are going to write a C program that will execute on one of the ARM cores, which will measure the time period of the pulses.

The program is as follows:

#include <stdio.h>
#include "xil_exception.h"
#include "xparameters.h"
#include "platform.h"
#include "xil_printf.h"
#include "xil_cache.h"
#include "xil_io.h"
#include "xscugic.h"
#include "xgpiops.h"
#include <unistd.h>

void scheduleTimer(int usec) {
 //set timer value
 Xil_Out32(0xE0002080, usec);
 //reload timer
 Xil_Out32(0xE0002084, 0x40000000);
 Xil_Out32(0xE0002084, 0x81000000);
}

int main()
{
    init_platform();
    Xil_Out32(0x43c00008, 2);
    usleep(1000);
    Xil_Out32(0x43c00008, 0);
    usleep(1000);
    scheduleTimer(15000000);
    //u32 in2 = Xil_In32(0x43c00008);
    while (!(Xil_In32(0x43c00008) & 1)) {

    }

    u32 time = Xil_In32(0xE0002084) & 0xffffff;
    int numbers[100];// = int[20];
    for (int i = 0; i < (0x6a00 -32); i++) {
        while ((Xil_In32(0x43c00008) & 1)) {

        }

        while (!(Xil_In32(0x43c00008) & 1)) {

        }
        u32 end = Xil_In32(0xE0002084) & 0xffffff;

        time = Xil_In32(0xE0002084) & 0xffffff;
    }

    for (int i = 0; i < 100; i++) {
        while ((Xil_In32(0x43c00008) & 1)) {

        }

        while (!(Xil_In32(0x43c00008) & 1)) {

        }
        u32 end = Xil_In32(0xE0002084) & 0xffffff;
        if (end > time)
         numbers[i] = time + (0xffffff - end);
 else
            numbers[i] = time - end;
        time = Xil_In32(0xE0002084) & 0xffffff;
    }


    cleanup_platform();
    return 0;
}


In this program we are making use of one of the timers in the USB block of the Zynq, for accurate time measuring. This timer is set to run continously.

One of the limitations of this USB timer is that it can only count durations of up to 16 seconds, which is a bit too short for our purposes since the header in a .TAP gets transmitted at about 18 seconds.

We can, however, overcome this limitation by just checking each time if the pulse end time is larger than the start time (Remember our counter is counting down). If this happen, we know an overflow condition has occurred in our timer and we can make the necessary adjustments to reflect the true duration.

In the code you will also saw frequent reference to register 0x43c00008. This register contains two bits of importance:


  • Bit 0: PWM output
  • Bit 1: Restart bit
With the code in action, we skip the first 0x69e0 pulses, bringing us closer to the start of the pulses for the file header. We can then gather a small set of sensible pulses, which we can use to do some spot checks on the pulses produced.

In Summary

In this post we have integrated our Tape Interface to the memory subsystem of our C64 design as a third AXI access port.

We also verified that the pulses produced by the Tape Interface are sensible values.

In the next post we will continue integrating our Tape module with the rest of the C64 system, gradullay working our way to an implementation where we load a program from a .TAP file into our C64 system.

Till next time!

No comments:

Post a Comment