Monday 10 June 2019

Integrating Tape Interface with C64 Module: Part 3

Foreword

In the previous post we started to developed a CIA module for our C64 FPGA design.

At the end of the previous post we have implemented the Port A and Port B of the CIA, which we used for keyboard interfacing.

In this post we are going to implemented the Timers (e.g. Timer A and Timer B) within our CIA module.

Developing the Timer Module

A timer is a crucial component of a CIA chip. So, let us begin by summarising a timer's operation:

  • A timer counts down from a predefined value till it reaches zero.
  • As soon as the timer reaches zero, an underflow condition occurs. With the underflow condition the timer gets reloaded with a predefined value.
  • With the timer reloaded, one of two things can happen:
    • Continuous mode: The timer with continue counting from the predefined value towards zero
    • One-shot mode: With the timer reloaded, the timer will stop. 
As seen from the above, a crucial part of timer operating is deciding when the timer should start, and when it should stop. A START register bit is specifically commissioned for this purpose.

If the START bit is one, it will count. As soon as the START bit is set to zero, the timer will stop counting.

There is two sources that can set the START bit:

  • You can set this bit yourself
  • Overflow condition in One-shot mode: With an overflow condition in one-shot mode, the START bit will be changed from a one to a zero.
With the theory discussed, let us start to create the timer module.

The core of the time module is the timer itself, so let us start by defining it as a register:

  reg [15:0] counter = 0;

As discussed, we need to also store the the state of START and RUNMODE. So, we will define a register for both:

  reg started = 0;
  reg runmode = 0;

At this point, the question is: How do we externally set the state of these two registers?

Obviously we will start by defining input ports for setting these values:

module timer(
...
  input new_started,
  input new_runmode,
  input clk,
...
    );


Also, it would be nice to indicate when the state inputs are valid. We will therefore create a new input port:

module timer(
...
  input write_control,
...
    );

The following snippet shows the setting of the RUNMODE state:

  always @(posedge clk)
  if (write_control)
    runmode <= new_runmode;


The setting of the START state is a bit more complex, since we can either set this state manually, or it can potentially get set during an underflow condition:

  always @(posedge clk)
  if (counter == 0 & runmode)
    started <= 0;
  else if (write_control)
    started <= new_started;


Let us now write some code for updating the counter itself:

module timer(
  input [15:0] reload_val,
  input force_reload,
...
    );

...
  always @(posedge clk)
  if (force_reload & write_control)
      counter <= reload_val;
  else if (counter > 0 & started)
    counter <= counter - 1;
  else if (counter == 0 & started)
    counter <= reload_val;
...

You can see that I have introduced two new ports which I haven't discussed before: reload_val and force_reload.

reload_val contains the predefined starting value for our timer. I have decided not to store this value internally, so with each reload I will fetch the value externally.

After some consideration, I thought it was best to store the reload values in registers on the CIA module itself, so it is not necessary to inspect the write_control value when reloading the counter.

force_reload is another feature of timers on a CIA. At any point, whether the timer is counting down or not, you can reload the timer by asserting the force_reload input.

We are just about done. What we still need to do, is to expose the internal state of our timer to the CIA:

module timer(
...
  output [15:0] counter_out,
  output started_status,
  output runmode_status
    );
...
  assign started_status = started;
  assign runmode_status = runmode;
   
  assign counter_out = counter;
...

Testing the Timer

Before integrating the timer with the CIA module, we need to test the timer module a bit.

The following top module will aid in testing the timer module:

module top(

    );

reg clk = 0;
reg force_reload = 0;
reg started = 0;
reg run_mode = 1;
reg write_control = 0;
wire [15:0] counter_val;

timer timer_a(
  .reload_val(20),
  .force_reload(force_reload),
  .new_started(started),
  .new_runmode(run_mode),
  .write_control(write_control),
  .clk(clk),
  .counter_out(counter_val) 
);

initial begin
  #50
  @(negedge clk)
  force_reload = 1;
  write_control = 1;
  @(negedge clk)
  force_reload = 0;
  write_control = 0;
  #50
  @(negedge clk)
  write_control = 1;
  started = 1;
  @(negedge clk)
  write_control = 0;
  #1000
  @(negedge clk)
  write_control = 1;
  started = 1;
  run_mode = 0;
  @(negedge clk)
  write_control = 0;
//  started = 1;
  
end

always #5 clk = ~clk;
endmodule


Here we test our timer with a force reload, and starting and stopping the timer.

Integration into the CIA module

With our Timer Module tested, it is now time to integrate this module into our CIA module.

We start by adding a couple of new slave registers:

  reg [7:0] slave_reg_0 = 0;
  reg [7:0] slave_reg_1 = 0;
  reg [7:0] slave_reg_2 = 0;
  reg [7:0] slave_reg_3 = 0;
  reg [7:0] slave_reg_4 = 255;
  reg [7:0] slave_reg_5 = 255;
  reg [7:0] slave_reg_6 = 255;
  reg [7:0] slave_reg_7 = 255;  
  reg [7:0] slave_reg_14 = 255;
  reg [7:0] slave_reg_15 = 255;


We then proceed and add some new wires:

  wire write_cra;
  wire write_crb;
  
  wire [15:0] counter_a_val;
  wire started_status_a;
  wire runmode_status_a; 

  wire [15:0] counter_b_val;
  wire started_status_b;
  wire runmode_status_b; 

  
  assign write_cra = we & (addr == 14) ? 1 : 0;
  assign write_crb = we & (addr == 15) ? 1 : 0;


I have defined write_cra and write_crb to signal our timer module when we are setting state, that is a write to register 14 or 15.

Here is how we define the two timer instances (e.g. Timer A and Timer B):

  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) 

      );

  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) 

      );


What remains to be done is to modify the functionality to write/read to the new CIA registers:

  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];
    
    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
  
  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;
    
    14: slave_reg_14 <= data_in;
    15: slave_reg_15 <= data_in;
  endcase


Testing the CIA

With the timers integrated we can now run a small test program 6502 assembly program to see if the CIA functions correctly as a whole:

LDA #$20
STA $DC04
LDA #$1
STA $DC05
LDA $DC0E
ORA #$8
STA $DC0E
ORA #$1
STA $DC0E
NOP
NOP
NOP
NOP
NOP
NOP
LDA $DC0E
ORA #$10
STA $DC0E
NOP
NOP
NOP
NOP
NOP
NOP

This code will test timer A in One-Shot mode. With little adjustment, the code can be modified to also test timer B.

In Summary

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

In the next post we will implement the Interrupt functionality of the CIA.

Once we have implemented interrupts on our CIA, we would be able to interface the Tape module to our C64 module.

Till next time!


No comments:

Post a Comment