Friday 23 August 2019

Implementing VIC-II bank switching

Foreword

In the previous post we have implemented Multicolor Bitmap mode within our VIC-II module.

As a test we got the VIC-II to render the Loading Splash Screen of the game Dan Dare. In this test we hardwired the upper two bits of the VIC-II address to 1's because the splash screen data was located in VIC bank 3 (e.g. RAM address 49152-65535).

Obviously if you want to simulate the whole loading sequence from the time the C64 boot up, this hardcoding will not do the trick, and we will need to implement the full VIC-II bank switching with the aid of the DD00 register.

Therefore, in this post we will spend some time to implement this VIC-II bank switching. We will then check when we load the game, that the splash screen will eventually be displayed.

Implementing VIC-II bank switching

VIC-II bank switching is driven by peripheral register DD00, by the lower two bits. This website gives us more information about these bits:

Bits #0-#1: VIC bank. Values:
  • , 0: Bank #3, $C000-$FFFF, 49152-65535.
  • %01, 1: Bank #2, $8000-$BFFF, 32768-49151.
  • %10, 2: Bank #1, $4000-$7FFF, 16384-32767.
  • %11, 3: Bank #0, $0000-$3FFF, 0-16383.
So, these bits actually makes out bits 15 and 14 of the VIC-II address, inverted though.

Address DD00 forms part of CIA#2. So, let us start by instantiating an extra CIA instance and mapping it into the address space of our 6502:

    cia cia_2(
          .port_a_out(cia_2_port_a),
          .addr(addr[3:0]),
          .we(we & (addr[15:8] == 8'hdd)),
          .clk(clk_1_mhz),
          .chip_select(addr[15:8] == 8'hdd),
          .data_in(ram_in),
          .data_out(cia_2_data_out)
            );

    always @*
        casex (addr_delayed)
          16'b1: combined_d_out = {reg_1_6510[7:5], tape_button, reg_1_6510[3:0]};
          16'b101x_xxxx_xxxx_xxxx : combined_d_out = basic_out;
          16'b111x_xxxx_xxxx_xxxx : if (reg_1_6510[1])
                                      combined_d_out = kernel_out;
                                    else
                                      combined_d_out = ram_out;
          16'hd020, 16'hd021, 16'hd011,
          16'hd016, 16'hd018 : combined_d_out = vic_reg_data_out;
          16'hd012: combined_d_out = line_counter;
          6'h26: combined_d_out = color_ram_out;
          16'hdcxx: combined_d_out = cia_1_data_out;
          16'hddxx: combined_d_out = cia_2_data_out;
          default: combined_d_out = ram_out;
        endcase


We can now append bits 1 and 0 of DD00 (e.g. cia_2_port_a) to the top of the address generated by the VIC-II. In our current design, the final VIC-II address is called portb_add, and is defined as follows:

assign portb_add = (portb_reset_counter < 3900000) ? 
    portb_reset_counter[15:0] : {2'b0,vic_addr};

As you can see, previously we just padded bits 14 and 15 of the VIC-II address with zeros.

You might remember that the portb_reset_counter maneuver was an ugly hack to get the second port of our Block RAM to work properly. More on this later.

To finish off our memory mapping for our VIC-II, we still need to properly map the character ROM.

From various documentation on the Internet, we see that the Character ROM is visible in Bank 0 and 2 within the address range $1000-$2000 and $9000-$a000 respectively.

The following code takes care of mapping the Character ROM into the address space of the VIC-II:

if ((vic_addr[13:12] == 2'b1) & cia_2_port_a[0])
  vic_combined_d = chargen_out;
else
  vic_combined_d = ram_out_2;

Battle of the Block RAMS

With all the changes done as described in the previous section, I waited with much anticipation while the Bitstream was generated.

When I tested the resulting Bitstream, I was was greeted with a blank screen. What a disappointment!

My first instinct was that something went wrong again with the AXI modules, causing that pixel information couldn't flow between SDRAM and our VIC-II module.

However, when I inspected the framebuffer area in SDRAM (via the XSCT console), I found that the whole framebuffer area was filled with zeroes. This indicates that all AXI modules and our VGA module is functioning properly.

First of all, we know that writing to SDRAM is working ok since the framebuffer area is filled zeroes. If writing to SDRAM wasn't functioning properly, we would have seen random values within the RAM area.

Secondly, we know that reading pixel data from SDRAM is also functioning correctly. A framebuffer filled with zeroes will yield black pixels, which we are getting (e.g. a blank screen).

There is however a very faint clue on what might be the issue. The frame is also colored in black instead of the usual light blue. The rendering of the border pixels should cause the least amount problems and potentially indicates that our 6502 failed to set the Border color register of our VIC-II module.

Has the 6502 perhaps crashed because the contents of the Block RAM has become corrupted? I did after all experienced previously an issue with a dual port Block RAM configuration where the VIC-II simply couldn't get the correct data omn the one port. It was for this  issue I had to do the portb_reset_counter maneuver as mentioned in the previous section.

Going though the Xilinx community forums I found a clue on this AR#: https://www.xilinx.com/support/answers/34699.html

This post start with the heading:

Block Memory Generator v3.3 - Spartan-6 Block RAM Read First mode address space overlap ("collision") issue with simultaneous Read and Write may cause memory corruption
This sounds like something that might be of interest to us. The following paragraph is of particular interest:

A read/write on one port and a write operation from the other port at the same address is not allowed. This restriction on the operation of the block RAM should not be ignored.
In our our case it can easily happen that the 6502 writes to an address while the VIC-II is reading from the same address.

To understand when this can happen, let us look a timing diagram for a typical write operation to block RAM in our C64 module:


The top signal is the 1MHz clock signal and the 2Mhz signal at the bottom. The write enable signal is in the centre.

The write signal is active during pulses 1,2 and 3. During these periods the possibility also exist that the address on both ports can be the same, which can be conditions for memory corruption as described earlier.

To avoid these address collisions, we need to do the following:

  • Ensure the Write Enable signal is low during the 2MHz clock pulses
  • During the 1Mhz clock pulses we should ensure that the addresses on both ports of the Block RAM are different.
We implement these requirements with the following code:

assign we = ((clk_div_counter != 6) && (clk_div_counter != 2)) ? we_raw : 0;

assign portb_raw = {~cia_2_port_a[1:0], vic_addr};
assign portb_add = (clk_div_counter == 7) ? {portb_raw[15:1],~addr[0]} : portb_raw;

We disable the Write Enable signal before every 2MHz clock pulse. The we_raw should be connected to our 6502 module.

The portb_raw and portb_add assignments ensure the addresses of the two ports are different during 1MHz clock pulses. We accomplish this by just substituting the least significant bit of port bit with the inverse of the LSB of port A.

The End Result

With all the code changes, our C64 module now shows the Splash screen correctly during loading.

The following video shows the loading process up to and until the Splash screen appears:


In Summary

In this Blog we created all the glue logic, so that our VIC-II could work with Bank switching.

We also managed to display the Splash screen during the loading of the Game Dan Dare.

In the next post we will check how much further the loading process can get, and fixing issues that might arise.

Till next time!


No comments:

Post a Comment