Foreword
In the previous post we have implemented interrupts within our CIA module.At last we are ready to integrate our Tape module to our C64 module, which we will cover in this post.
We will be using the tape image for the game Dan Dare.
The ultimate goal for this post doesn't sound so exciting: To be able to found the Header on the mentioned Tape image and showing Found Dan Dare.
We will continue using the Tape image of Dan Dare in coming posts, and we will work our way through first getting the flashing borders and splash loading screen to display, till actually to a point where we can play the game.
FLAG1 as an edge triggered interrupt
If we were to take the output of our Tape module and connect it directly to the FLAG1 input of our CIA module, our FLAG1 will basically act as a level interrupt.On a conventional CIA chip, however, all interrupts are edge triggered interrupts. We therefor need to make a small modification, so that our FLAG1 input behaves as an edge interrupt:
... reg flag1_delayed; ... cia cia_1( .port_a_out(keyboard_control), .port_b_in(keyboard_result), .addr(addr[3:0]), .we(we & (addr[15:8] == 8'hdc)), .clk(clk_1_mhz), .chip_select(addr[15:8] == 8'hdc), .data_in(ram_in), .data_out(cia_1_data_out), .flag1(flag1 & !flag1_delayed), .irq(irq) ); always @(posedge clk_1_mhz) flag1_delayed <= flag1; ...
Tape Button and motor control
If you ever loaded a game from a tape on a real C64, you will know that the C64 has control over the Cassette motor: The tape briefly pauses when a file header is found, as well as when the file has finished loading.How can the C64 control the tape motor in this way? The answer is via bits 4 and 5 from memory location 1. Quoted from this:
- Bit #4: Datasette button status; 0 = One or more of PLAY, RECORD, F.FWD or REW pressed; 1 = No button is pressed.
- Bit #5: Datasette motor control; 0 = On; 1 = Off.
In order for our C64 module to emulate tape loading, we also need to emulate the above mentioned bits.
We will use the ~/` button on the USB keyboard to represent the play button for Bit 4. We will hook up this key to our C64 in a coming section.
To serve above mentioned bots, we need to define an input port and an output port on our c64 module:
module c64( ... input wire tape_button, output reg motor_control = 1, ... );
The motor control bit gets set as follows:
always @(posedge clk_1_mhz) if (we & (addr == 1)) motor_control <= ram_in[5];
To cater for reads for location 1, we need to adjust our memory read case statement as follows:
always @* casex (addr_delayed) 16'b1: combined_d_out = {ram_out[7:6], motor_control, tape_button, ram_out[3:0]}; 16'b101x_xxxx_xxxx_xxxx : combined_d_out = basic_out; 16'b111x_xxxx_xxxx_xxxx : combined_d_out = kernel_out; 16'hd012: combined_d_out = line_counter; 16'hdcxx: combined_d_out = cia_1_data_out; default: combined_d_out = ram_out; endcase
We have now defined our motor control functioanlity within our C64 module, but we still need to route it to our Tape module:
module tape( 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, input motor_control, output pwm ); ... tape_pwm t_pwm( .time_val(timer_val), .load_timer(load_timer), .pwm(pwm), .motor_control(motor_control), .clk(clk_1_mhz) ); ..
In our pwm module we basically want to pause the generation of pwm pulses when the motor control bit is high:
... always @(posedge clk) if (timer > 0 & !motor_control) timer <= timer - 1; else if (timer == 0) timer <= load; ..
Assigning a keyboard button as play button
I mentioned earlier that I want to use the ~/` key on the USB keyboard as our play button.You will recall from a previous post that we interface with a USB keyboard with a minimalistic USB protocol stack, where we convert USB key scan codes to C64 scancodes and then setting the appropriate bit in a key array linked to our C64 module.
We start by adding a mapping for the ~ key to the big if-else statement:
u32 mapUsbToC64(int usbCode) { if (usbCode == 0x4) { //A return 0xa; } else if (usbCode == 0x5) { //B return 0x1c; } else if (usbCode == 0x6) { //C return 0x14; ... ... } else if (usbCode == 0x2c) { //space return 0x3c; } else if (usbCode == 53) { //play key `~ return 100; } }
The USB key scancode for the required key is 53 and we map this code 100.
But, C64 scan codes only goes up to 63, so what is with the 100?
The point is that the play key is not actually key in the key bit array that we want to set. We rather use it as a special value:
void getC64Words(u32 usbWord0, u32 usbWord1, u32 *c64Word0, u32 *c64Word1) { *c64Word0 = 0; *c64Word1 = 0; if (usbWord0 & 2) { *c64Word0 = 0x8000; } usbWord0 = usbWord0 >> 16; for (int i = 0; i < 2; i++) { int current = usbWord0 & 0xff; if (current != 0) { int scanCode = mapUsbToC64(current); if (scanCode == 100) { Xil_Out32(0x43C00008, 0); } else if (scanCode < 32) { *c64Word0 = *c64Word0 | (1 << scanCode); } else { *c64Word1 = *c64Word1 | (1 << (scanCode - 32)); } } usbWord0 = usbWord0 >> 8; } for (int i = 0; i < 4; i++) { int current = usbWord1 & 0xff; if (current != 0) { int scanCode = mapUsbToC64(current); if (scanCode == 100) { Xil_Out32(0x43C00008, 0); } else if (scanCode < 32) { *c64Word0 = *c64Word0 | (1 << scanCode); } else { *c64Word1 = *c64Word1 | (1 << (scanCode - 32)); } } usbWord1 = usbWord1 >> 8; } }
So if, during the conversion process from USB to C64 scan code we encounter a 100, we set the value zero to memory location 0x43c00008.
Remember that 0x43c00008 is a register mapped to our AXI slave interface. Just to recap from a previous post:
0x43c0_0000: key array 1 0x43c0_0004: key array 2 0x43c0_0008: tape control: bit 0: PWM bit (READ ONLY). For debug bit 1: Reset tape position to address 0x238270 in memory
For register 0x43c0_0008 we will introduce an extra function for bit 2: Tape button. Our Slave AXI block needs to be modified to accommodate this bit so it can be surfaced to the tape_button input of our C64 module.
Test Run
Time for a test run.As usual you will open Xilinx SDK, program the FPGA and start our C program to run on the ARM processor. In our case the C program will run a mini USB stack, capturing keystrokes and forwarding to our C64 module.
With the C program running, we need to open the XSCT console, and execute a couple of commands.
First we need to pause our Tape module and reset the read address:
mwr 0x43c00008 6
Next we should load the tape image into memory:
mwr -size b -bin -file "/home/johan/Downloads/Dan Dare.tap" 0x238250 600000
Next we release the reset bit:
mwr 0x43c00008 4
You can now move to the USB keyboard and follow the conventional process for kicking off the loading of a tape on a C64. That is typing LOAD and when prompted to press play on tape, just press ~.
Currently our VIC-II doesn't support screen blanking, as you would see when loading a program from tape. However, we can see the prompts as something is loading:
In Summary
In this post we finally integrated our Tape Module into our C64 Module.As a test we managed to load a file header from a .tap file and display the file name.
In the next post we will implement the necessary functionality for displaying the loading effects while loading the game Dan Dare.
Till next time!