Foreword
In the previous post we managed to load the tape header from a .TAP file and display the file found from it and display the file found on the screen, with all these actions performed by the KERNEL ROM.This basically proofs that we have implemented out Tape module correctly. If one really want to be nostalgic, one could actually hook up a 1530 datasette to our design, provided you get the level shifting right, and it should work.
Our C64 FPGA module in its current state doesn't really support the full graphical capabilities of the VIC-II. In fact, currently it can only render characters stored in the default memory location (e.g. addresses 1024 to 2023), with hard coded border and background colors, which are light and dark blue.
This means that if we load a classic game within our C64 module, it will probably load, but we not be able to see the fancy colourful graphic effects.
The way to approach this problem is to pick a classic C64 game you like and gradually implement the graphic capabilities the game require, till you can perfectly play the game.
The game I have picked was Dan Dare, Pilot of the Future.
We will start by implementing the graphical capabilities that the loading of the game Dan Dare requires.
When you load the game Dan Dare from tape, as with many other games of the era, you will be presented with flashing borders as well as a splash screen in multi color high resolution mode.
In this post we will look into implementing the flashing borders.
The display of the splash screen we will implement in the next post or two.
VIC-II register access
When the game Dan Dare loads, the effect of flashing borders is achieved by writing alternating colors in a rapid fashion to memory location 53280.Memory location 53280 is indeed a register within the VIC-II. At this point I need to stop the discussion in its tracks: Our VIC-II module doesn't even provide access register access at the moment!
So, let us start by implementing functionality within our C64 module for accessing registers from the VIC-II.
Firstly, we need to add some extra ports to our VIC-II module:
module vic_ii ( input clk_1_mhz, ... input [5:0] addr_in, input [7:0] reg_data_in, input we, output [7:0] data_out, ... );
For addr_in we take the lower 6 bits of of the address output from our Arlet 6502 core.
For reg_data_in we are taking the Data Out, also from our Arlet 6502 core.
The data_out we need to add as an extra input to our data multiplexer, which we will discuss a bit later.
As mentioned, we are only using the lower six bits of the address bus. For reading this is not a problem at all. The multiplexer will simply ignore data from our VIC-II module if we didn't requested data from the VIC-II module.
Writes are more of a problem. We only want to write to a VIC-II register if it was really the intent, of course. For this we need the need the we port, which we connect as follows:
vic_ii vic_inst ( ... .we(we & (addr == 16'hd020 | addr == 16'hd021 | addr == 16'hd011)), ... );
For now we are only doing a write for three of the VIC-II registers: d020, d021 and d011. For the remaining registers, we are just going to write to main RAM. This just makes our life easier for now.
You will also realise that we are sending the 1Mhz clock signal to our VIC-II module. This is because the read/writes will be performed by our 6502 module, which operates at 1Mhz.
You might recall that big chunks of our VIC-II module operates at 8 MHZ, which in turn might let you think for a moment: Two different clock speeds again, so do we need again cater for cross clock domains?
Luckily not! Remember that in our design our 1MHZ clock is not a pure 1MHZ with a 50% duty cycle. We derive our 1MHz by taking our 8MHz, and for each 8 cycles we are masking out 7 cycles and enabling one.
This means that our 1MHz signal pulse will always be in sync with a 8Mhz pulse, although our 1Mhz will have a weird duty cycle of 12.5%
Finally, let us change our multiplexer logic to cater for reads from our VIC-II registers:
always @* casex (addr_delayed) ... 16'hd020, 16'hd021, 16'hd011: combined_d_out = vic_reg_data_out; ... default: combined_d_out = ram_out; endcase
Recall that we connect combined_d_out to the Data In on our Arlet 6502 core.
We have now given our CPU the capability to access VIC-II registers. We now need to implement the mentioned registers within the VIC-II.
Equipping our VIC-II with registers
Let us now implement the three registers within our VIC-II module:... reg [7:0] data_out_reg; reg [7:0] screen_control_1 = 0; reg [3:0] border_color = 0; reg [3:0] background_color = 0; ... assign screen_enabled = screen_control_1[4]; assign data_out = data_out_reg; always @(posedge clk_1_mhz) case (addr_in) 6'h20: data_out_reg <= {4'b0,border_color}; 6'h21: data_out_reg <= {4'b0,background_color}; 6'h11: data_out_reg <= screen_control_1; endcase always @(posedge clk_1_mhz) begin if (we & addr_in == 6'h20) border_color <= reg_data_in[3:0]; else if (we & addr_in == 6'h21) background_color <= reg_data_in[3:0]; else if (we & addr_in == 6'h11) screen_control_1 <= reg_data_in[7:0]; end ...
We implement the register data_out_reg for reading purposes. In our C64 module reads are always delayed by one clock cycle, so the data_out_reg performs this task for us.
You might wonder why we have implement the screen control register if, in this post, we only care about border and background color. This is just to cater for the scenario where the screen gets blanked, in which case the full screen gets covered with flashing borders.
Having each VIC-II register declared individually seems quite like a cumbersome process. One might be tempted to think that it would be easier to define all registers together as an array which will resolve to a Block RAM element during synthesis.
This is a very valid point. However, one needs to keep in mind that a Block RAM element can only allow up to two simultaneous memory access operations. The VIC-II needs more than two simultaneous accesses, just to mention a few:
- Border colour/Background color
- Screen control
- X raster pos
- Y raster pos
It is therefor better to specify register separately.
Finally we need to use these registers for color generation:
assign color_for_bit = pixel_shift_reg[7] == 1 ? current_front_color : background_color; assign final_color = (visible_vert & visible_horiz & screen_enabled) ? color_for_bit : border_color;
Let us quickly refresh ourselves with the above snippet of code.
pixel_shift_reg is a byte shift register where we shift out a bit at a time. Where the current bit is zero, we output the background_color as the color to display.
We use final_color to alternate between the border color and color_for_bit where applicable. I have also added screen_enabled to the mix, where the screen gets blanked totally with the border color if display is disabled.
pixel_shift_reg is a byte shift register where we shift out a bit at a time. Where the current bit is zero, we output the background_color as the color to display.
We use final_color to alternate between the border color and color_for_bit where applicable. I have also added screen_enabled to the mix, where the screen gets blanked totally with the border color if display is disabled.
The need to disable KERNEL ROM
With the registers implemented, I still couldn't see the flashing borders.I encountered a similar issue when I developed a C64 emulator in JavaScript.
When I read the above mentioned blog post, I remembered that the issue was that the loader Dan Dare override the IRQ vector at address FFFF/FFFE.
The problem comes in that one needs to disable the KERNEL ROM to expose this new vector from RAM, which I haven't implemented yet.
The switching in/out of ROMS from address space is controlled by register 1. We already worked with register 1 in the previous post where we had to implement motor control and tape button status.
I changed the logic a bit for reading and writing to register 1:
... reg [7:0] reg_1_6510 = 8'h37; assign motor_control = reg_1_6510[5]; always @(posedge clk_1_mhz) if (we & (addr == 1)) reg_1_6510 <= ram_in; ... always @* casex (addr_delayed) 16'b1: combined_d_out = {reg_1_6510[7:5], tape_button, reg_1_6510[3:0]}; ... 16'b111x_xxxx_xxxx_xxxx : if (reg_1_6510[1]) combined_d_out = kernel_out; else combined_d_out = ram_out; ... endcase ...
I have created an 8-bit register for memory location 1 called reg_1_6510. For starters, I am now feeding the motor control bit from bit 5 of this register.
We are keeping our eyes on bit one of reg_1_6510. If this bit is 1, we return the contents of kernel ROM and the contents of the underlying RAM if this bit is 0.
The End Result
The following video show the end result when we load the game Dan Dare from a .TAP file. Flashing borders appear:In Summary
In this post we gave the capability for our C64 module to change the border and background color.This enabled us to emulate the flashing borders when we load the game Dan Dare.
In the next post we will do some development that will enable us to see the Splash screen during the loading process.
Till next time!
No comments:
Post a Comment