Saturday, 15 June 2019

Integrating Tape Interface with C64 Module: Part 4

Foreword

In the previous post we developed and integrated timers to our CIA module.

We also ran a simulation to verify that our timers works more or less as expected within the CIA module.

In this post we will be implementing interrupts within our CIA module.

With interrupts fully implemented within our CIA module, we are one step closer to integrate our tape module to our C64 module.

An overview of Interrupts on the CIA

The easiest way to understand how interrupts work on the CIA, is just to look at the datasheet of a CIA 6526.

A copy can easily obtained from the archives of 6502.orghttp://archive.6502.org/datasheets/mos_6526_cia_recreated.pdf

Within the datasheet, we can see that there is one register taking care of all interrupt functionality:
So, all the interrupt functionality is provided by register number 14 of the CIA. Behind the scenes, however, register 14 is in actual fact two separate registers: When writing to location 14, the value will be saved in an Interrupt Mask register. When reading from this location, the value of the Interrupt Status Register will be returned.

At point a typical question will pop up: If you cannot write to the Interrupt status register, how can you clear this register from interrupts that occurred?

The answer: By reading this register.

You can see that the CIA supports five interrupts. In our whole C64 project, however, we will only be using three interrupts:

  • Timer A
  • Timer B, and
  • FLG
The FLG is the interrupt we will be using to connect our Tape interface in a later post.

Defining Interrupts on the Timers


We will be using timers to test our interrupt functionality.

Our Timer module, as it stands currently, doesn't have an output defined to signal a timer interupt.

So, let us start off by first defining an output port on out timer module for signalling an interrupt.:

module timer(
  input [15:0] reload_val,
  input force_reload,
  input new_started,
  input new_runmode,
  input write_control,
  input clk,
  output [15:0] counter_out,
  output started_status,
  output runmode_status,
  output overflow
    );


This pin will obviously set to a one when our counter reaches zero:

assign overflow = (counter == 0) ? 1 : 0;

We need to wire these pins up for Timer A and Timer B in our CIA module:

...
  wire timer_a_overflow;
  wire timer_b_overflow;
...
  timer timer_a(
    .reload_val({slave_reg_5,slave_reg_4}),
    .force_reload(write_cra & data_in[4]),
    .new_started(data_in[0]),
    .new_runmode(data_in[3]),
    .write_control(write_cra),
    .clk(clk),
    .counter_out(counter_a_val),
    .started_status(started_status_a),
    .runmode_status(runmode_status_a), 
    .overflow(timer_a_overflow)
      );

  timer timer_b(
    .reload_val({slave_reg_7,slave_reg_6}),
    .force_reload(write_crb & data_in[4]),
    .new_started(data_in[0]),
    .new_runmode(data_in[3]),
    .write_control(write_crb),
    .clk(clk),
    .counter_out(counter_b_val),
    .started_status(started_status_b),
    .runmode_status(runmode_status_b), 
    .overflow(timer_b_overflow)
      );
...

We have now defined two interrupts that we can to develop the Interrupts functionality for our CIA.

Developing the Interrupt functionality

Let us now develop the rest of the interrupt functionality.

As mentioned earlier, the ICR is in effect two registers. So, let us start by creating them:

  reg [4:0] int_mask = 0;
  reg [4:0] int_stat = 0;


Since the CIA only supports 5 interrupts, I am only making this register 5 bits wide, instead of the usual 8.

Let us create the logic for setting the contents of the interrupt mask register:

  always @(posedge clk)
  if (we)
  case (addr)
    0: slave_reg_0 <= data_in;
    1: slave_reg_1 <= data_in;
    2: slave_reg_2 <= data_in;
    3: slave_reg_3 <= data_in;
    4: slave_reg_4 <= data_in;
    5: slave_reg_5 <= data_in;
    6: slave_reg_6 <= data_in;
    7: slave_reg_7 <= data_in;
    13: if (data_in[7]) 
          int_mask <= int_mask | data_in[4:0];
        else
          int_mask <= int_mask & ~data_in[4:0];
    14: slave_reg_14 <= data_in;
    15: slave_reg_15 <= data_in;
  endcase

If this looks a bit strange, just remember that the MSB of the value written to the Interrupt Mask register have an import role. If this value is one, it means we are enabling interrupts. If the MSB is zero, it means that we want to mask off interrupts.

Next, let us tackle the Interrupts status register. Before we look into this register, it is important to realise that our CIA module in its current state have a design anomaly.

Our CIA is currently linked up to the lower 4 bits of the address bus, and for all address reads our CIA will return data for the register these four bits represents.

In general, this is not a problem for us, because we some multiplexing logic that will just ignore these values if we didn't targeted a CIA with a read.

This do, however, become a problem with reads from the Interrupt status register, since a read clears the contents of the Interrupt Status register.

We therefor need some way to indicate to the CIA when are targeting a read towards it. We do this by defining an extra port:

module cia(
...
  input chip_select,
...
    );

From the outside, we assign this port as follows:

    cia cia_1(
...
          .chip_select(addr[15:8] == 8'hdc),
...
            );


One might be tempted to think, shouldn't we rather check for addr[15:5]?

The answer is no. On a C64 within the memory range DC00-DCFF it will always assume the lower four bits is meaned for CIA#1.

We can now do the assignment to the Int Mask Register:

  always @(posedge clk)
  if (chip_select & !we & (addr == 13))
    int_stat <= 0;
  else
    int_stat <= int_stat | {flag1, 1'b0, 1'b0, timer_b_overflow, timer_a_overflow};

Flag1 is only shown for sake of completeness.

The case statement for assigning data_out looks as follows:

  always @(posedge clk)
  if(!we)
  case (addr)
    0: data_out <= slave_reg_0;
    1: data_out <= port_b_in;
    2: data_out <= slave_reg_2;
    3: data_out <= slave_reg_3;
    4: data_out <= counter_a_val[7:0];
    5: data_out <= counter_a_val[15:8];
    6: data_out <= counter_b_val[7:0];
    7: data_out <= counter_b_val[15:8];
    13: data_out <= {irq, 1'b0, int_stat};
    14: data_out <= {slave_reg_14[7:5],
                     1'b1,
                     runmode_status_a,
                     slave_reg_14[2],
                     slave_reg_14[1],
                     started_status_a
    };
    15: data_out <= {slave_reg_15[7:5],
                     1'b1,
                     runmode_status_b,
                     slave_reg_15[2],
                     slave_reg_15[1],
                     started_status_b
    };

  endcase


You will see that we make reference to an irq, which we haven't defined yet.

This is the output we will actually hook up to the irq pin of our 6502. We declare this wire within our CIA module as follows:

module cia(
...
  output irq
    );
...
  assign irq = (int_stat & int_mask) ? 1 : 0;
...

An interrupt will only resolve to an IRQ if enable by the Interrupt Mask Register.

The Test Program

To test the interrupt functionality we will again write a 6502 test program that we will run in simulation mode:

      SEI
      LDA #$1F
      STA $DC0D
      LDA $DC0D
      CLI
      LDA #$64
      STA $DC04
      LDA #$00
      STA $DC05
      LDA #$11
      STA $DC0E
      JSR DELAY
      LDA #$81
      STA $DC0D
      JSR DELAY
      LDA #$01
      STA $DC0D
      JSR DELAY 
      NOP
      NOP
      NOP      
DELAY LDX #$14
LOOP DEX
     BNE LOOP
     LDA $DC04
     LDX $DC0D
RTS


With this program we let Timer A expire twice, with interrupts disabled in the first expire and enabled in the second expire.

In this test program I haven't wrote an interrupt handler. It is sufficient for now to confirm in the simulation to verify that a jump to the interrupt vector is performed.

In Summary

In this post we have implemented interrupts within our CIA module.

In the next post we will verify if our FPGA can boot ok with the CIA module.

We will then wire up the Tape module to our C64 module.

Till next time!

No comments:

Post a Comment