Thursday 29 August 2019

IO Area Bank Switching

Foreword

In the previous two posts we did some development that will enable us the display of the associated splash screen while our C64 FPGA implementation loads the game Dan Dare from tape.

This development included implementing Multicolor bitmap mode as well as implementing VIC-II memory banking.

You might have noticed that throughout this Blog Series I am trying to follow more or less the same approach as I did in my Blog series where I created a C64 emulator in JavaScript. In this old Blog Series, I also mentioned at one point, that in order to completely load the game and get to the intro screen, we need to implement IO area banking.

The reason for this is because, during Game loading, we also write to the RAM area below the IO peripheral area (e.g. addresses D000-E000). If you do not implement the Banking of the IO area properly, you might end up with some weird side effects that is painful to debug.

IO Banking for reading

Let us start by implementing IO Banking for reading. That is either we enable reading of IO register in the region D000-DFFF, or we enable reading from the RAM region underneath.

As with the Kernel ROM and BASIC ROM, the banking of the IO region is controlled by memory address 1.

To familiarise ourselves with when the IO region is enabled, we consult the C64 memory map at http://sta.c64.org/cbm64mem.html.

The following extract shows us wehn th IO region is enabled:

Bits #0-#2: %1xx: I/O area visible at $D000-$DFFF. (Except for the value %100, see above.) 
We can convert this required into Verilog:

assign io_enabled = reg_1_6510[2] && !(reg_1_6510[1:0] == 0);

To either read from the IO region or the RAM below it, we write the following code:

    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 = io_enabled ? vic_reg_data_out : ram_out;
          16'hd012: combined_d_out = io_enabled ? line_counter : ram_out;
          16'b1101_10xx_xxxx_xxxx: combined_d_out = io_enabled ? color_ram_out : ram_out;
          16'hdcxx: combined_d_out = io_enabled ? ((addr_delayed == 16'hdc00) ? 255 : cia_1_data_out) : ram_out;
          16'hddxx: combined_d_out = io_enabled ? cia_2_data_out : ram_out;
          default: combined_d_out = ram_out;
        endcase

So, if io_enabled is false we just return ram_out for the io register read request.

Writing to IO Registers

Next, let us handle writing to IO register banking:

assign color_ram_write_enable = we & io_enabled & (addr >= 16'hd800 & addr < 16'hdbe8);
    
    cia cia_1(
          .port_a_out(keyboard_control),
          .port_b_in(keyboard_result),
          .addr(addr[3:0]),
          .we(we & io_enabled & (addr[15:8] == 8'hdc)),
          .clk(clk_1_mhz),
          .chip_select(addr[15:8] == 8'hdc & io_enabled),
          .data_in(ram_in),
          .data_out(cia_1_data_out),
          .flag1(flag1 & !flag1_delayed),
          .irq(irq)
            );

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

vic_test_3 vic_inst
    (
        .clk_in(clk),
        .clk_counter(clk_div_counter),
        .clk_2_mhz(clk_2_mhz),
        .blank_signal(blank_signal),
        .frame_sync(frame_sync),
        .data_in({color_ram_out2,vic_combined_d}),
        .c64_reset(c64_reset),
        .addr(vic_addr),
        .out_rgb(out_rgb),
        .clk_1_mhz(clk_1_mhz),
        .addr_in(addr[5:0]),
        .reg_data_in(ram_in),
        .we(we & io_enabled & (addr == 16'hd020 | addr == 16'hd021 | addr == 16'hd011
             | addr == 16'hd016 | addr == 16'hd018)),
        .data_out(vic_reg_data_out)

        );


These code changes will block writes to IO registers if io_enabled is false.

You might have noticed that we unconditionally write to the RAM region underneath the IO region. This causes unpredictable behaviour when we load the game Dan Dare from the Tape image.

We disable writes to this RAM region when io_enabled == false with the following code:

...
assign do_io_write = io_enabled & ((addr >= 16'hd000) && (addr < 16'he000));
...

     always @ (posedge clk_1_mhz)
       begin
        if (we & !do_io_write) 
        begin
         ram[addr] <= ram_in;
         ram_out <= ram_in;
        end
        else 
        begin
         ram_out <= ram[addr];
        end 
       end 
...


You will notice that when io_enabled is false, that we exclude RAM writes for the full IO region, even though we currently have IO register gaps in our current C64 implementation. This have the effect that writing to these gaps will effectively be discarded.

This behaviour will luckily not cause any issues in our scenario.

The End Result

The following video shows the end result of our development


We managed to get to the Intro screen after the loading of the game.

The credits is moving very fast, but we will fix this in a future post.

In Summary

In this post we have implemented banking functionality for the IO region. This enabled our emulator to load the Game Dan Dare completely from a tape image and display the Intro screen.

In the next post we will implement Joystick functionality.

Till next time!

No comments:

Post a Comment