Skip to content →

CIAs – Timers, Keyboard and more

So, the C64 has two CIAs (Complex Interface Adapters), which cover multiple functions. Both have 2 timers, a Real-Time-Clock (RTC), and two data ports, which can read or written to.

CIA1

Adress area: $DC00-$DCFF Tasks: Keyboard, Joystick, Paddles, Datasette, IRQ control
Adress Function Remark
$DC00 Data Port A Monitoring/control of the 8 data lines of Port A. The lines are used for multiple purposes:

Read/Write: Bit 0..7 keyboard matrix columns
Read: Joystick Port 2: Bit 0..3 Direction (Left/Right/Up/Down), Bit 4 Fire button. 0 = activated.
Read: Lightpen: Bit 4 (as fire button), connected also with “/LP” (Pin 9) of the VIC
Read: Paddles: Bit 2..3 Fire buttons, Bit 6..7 Switch control port 1 (%01=Paddles A) or 2 (%10=Paddles B)

$DC01 Data Port B Monitoring/control of the 8 data lines of Port B. The lines are used for multiple purposes:

Read/Write: Bit 0..7 keyboard matrix rows
Read: Joystick Port 1: Bit 0..3 Direction (Left/Right/Up/Down), Bit 4 Fire button. 0 = activated.
Read: Bit 6: Timer A: Toggle/Impulse output (see register 14 bit 2)
Read: Bit 7: Timer B: Toggle/Impulse output (see register 15 bit 2)

$DC02 Data Direction
Port A
Bit X: 0=Input (read only), 1=Output (read and write)
$DC03 Data Direction
Port B
Bit X: 0=Input (read only), 1=Output (read and write)
$DC04 Timer A
Low Byte
Read: actual value Timer A (Low Byte)

Writing: Set latch of Timer A (Low Byte)

$DC05 Timer A
High Byte
Read: actual value Timer A (High Byte)

Writing: Set latch of timer A (High Byte) – if the timer is stopped, the high-byte will automatically be re-set as well

$DC06 Timer B
Low Byte
Read: actual value Timer B (Low Byte)

Writing: Set latch of Timer B (Low Byte)

$DC07 Timer B
High Byte
Read: actual value Timer B (High Byte)

Writing: Set latch of timer B (High Byte) – if the timer is stopped, the high-byte will automatically be re-set as well

$DC08 Real Time Clock
1/10s
Read:

Bit 0..3: Tenth seconds in BCD-format ($0-$9)
Bit 4..7: always 0
Writing:
Bit 0..3: if CRB-Bit7=0: Set the tenth seconds in BCD-format
Bit 0..3: if CRB-Bit7=1: Set the tenth seconds of the alarm time in BCD-format

$DC09 Real Time Clock
Seconds
Bit 0..3: Single seconds in BCD-format ($0-$9)

Bit 4..6: Ten seconds in BCD-format ($0-$5)
Bit 7: always 0

$DC0A Real Time Clock
Minutes
Bit 0..3: Single minutes in BCD-format( $0-$9)

Bit 4..6: Ten minutes in BCD-format ($0-$5)
Bit 7: always 0

$DC0B Real Time Clock
Hours
Bit 0..3: Single hours in BCD-format ($0-$9)

Bit 4..6: Ten hours in BCD-format ($0-$5)
Bit 7: Differentiation AM/PM, 0=AM, 1=PM
Writing into this register stops TOD, until register 8 (TOD 10THS) will be read.

$DC0C Serial
shift register
The byte within this register will be shifted bitwise to or from the SP-pin with every positive slope at the CNT-pin.
$DC0D Interrupt Control
and status
CIA1 is connected to the IRQ-Line.

Read: (Bit0..4 = INT DATA, Origin of the interrupt)
Bit 0: 1 = Underflow Timer A
Bit 1: 1 = Underflow Timer B
Bit 2: 1 = Time of day and alarm time is equal
Bit 3: 1 = SDR full or empty, so full byte was transferred, depending of operating mode serial bus
Bit 4: 1 = IRQ Signal occured at FLAG-pin (cassette port Data input, serial bus SRQ IN)
Bit 5..6: always 0
Bit 7: 1 = IRQ An interrupt occured, so at least one bit of INT MASK and INT DATA is set in both registers.
Flags will be cleared after reading the register!
Write: (Bit 0..4 = INT MASK, Interrupt mask)
Bit 0: 1 = Interrupt release through timer A underflow
Bit 1: 1 = Interrupt release through timer B underflow
Bit 2: 1 = Interrupt release if clock=alarmtime
Bit 3: 1 = Interrupt release if a complete byte has been received/sent.
Bit 4: 1 = Interrupt release if a positive slope occurs at the FLAG-Pin.
Bit 5..6: unused
Bit 7: Source bit. 0 = set bits 0..4 are clearing the according mask bit. 1 = set bits 0..4 are setting the according mask bit. If all bits 0..4 are cleared, there will be no change to the mask.

$DC0E Control Timer A Bit 0: 0 = Stop timer; 1 = Start timer

Bit 1: 1 = Indicates a timer underflow at port B in bit 6.
Bit 2: 0 = Through a timer overflow, bit 6 of port B will get high for one cycle , 1 = Through a timer underflow, bit 6 of port B will be inverted
Bit 3: 0 = Timer-restart after underflow (latch will be reloaded), 1 = Timer stops after underflow.
Bit 4: 1 = Load latch into the timer once.
Bit 5: 0 = Timer counts system cycles, 1 = Timer counts positive slope at CNT-pin
Bit 6: Direction of the serial shift register, 0 = SP-pin is input (read), 1 = SP-pin is output (write)
Bit 7: Real Time Clock, 0 = 60 Hz, 1 = 50 Hz

$DC0F Control Timer B Bit 0: 0 = Stop timer; 1 = Start timer

Bit 1: 1 = Indicates a timer underflow at port B in bit 7.
Bit 2: 0 = Through a timer overflow, bit 7 of port B will get high for one cycle , 1 = Through a timer underflow, bit 7 of port B will be inverted
Bit 3: 0 = Timer-restart after underflow (latch will be reloaded), 1 = Timer stops after underflow.
Bit 4: 1 = Load latch into the timer once.
Bit 5..6:

  •  %00 = Timer counts System cycle
  •  %01 = Timer counts positive slope on CNT-pin
  •  %10 = Timer counts underflow of timer A
  •  %11 = Timer counts underflow of timer A if the CNT-pin is high

Bit 7: 0 = Writing into the TOD register sets the clock time, 1 = Writing into the TOD register sets the alarm time.

$DC10-$DCFF The CIA 1 register are mirrored each 16 Bytes

Timers

The timers are pretty much self-explaining. They are ticked on every CPU-cycle and can be configured to trigger an IRQ on certain occasions (write to 0xdc0d sets the possible triggers for the IRQ to happen).
This is used quite often in C64 code, and also by the OS itself. Remember the cursor from the last chapter, that wasn’t blinking at all? Well, if we configure the CIA1 correctly, so that it triggers an IRQ when it underflows (this is what the OS sets automatically), we will see a blinking cursor!
The IRQ is already implemented in our 6502 from the NES, so all we have to do is, flag for the IRQ, once a condition is met.
Underflowing is probably the most used feature of the Timers, but we should make sure to implement the other modes as well.

void tickAllTimers(uint8_t cycles) {
	//	CIA 1 Timers
	if (cia1_timerA.tick(cycles)) {
		if (cia1_irq_status.IRQ_on_timerA_underflow) {
			cia1_irq_status.IRQ_occured_general_flag = true;
			cia1_irq_status.IRQ_occured_by_underflow_timerA = true;
			setIRQ(true);
		}
	}
	if (cia1_timerB.tick(cycles)) {
		if (cia1_irq_status.IRQ_on_timerB_underflow) {
			cia1_irq_status.IRQ_occured_general_flag = true;
			cia1_irq_status.IRQ_occured_by_underflow_timerB = true;
			setIRQ(true);
		}
	}
...
C64 – blinking cursor after CIA1 (Timer1) was implemented

Keyboard

The Keyboard in the C64 is connected to a 8×8 matrix of lines, that are connected to Port A and Port B of the CIA1.

Bildergebnis für cia keyboard matrix

By the way the ports are designed (I’m trying to not get too technical here – if you would like to have more info about why this is the way it is, the C64 Wiki can help you out), the way we have to read out the keys is: The OS sends a write to Port A, with a value of 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf and 0x7f. You will see, in any of these values there is one bit not set. This is the bit of the line, that the OS will listen to in the next read from Port B.

Example: If the OS writes 0xef to Port A, the binary representation is ‭1110 1111‬, so the bit on position 4 (zero-based) is the one missing, so we are looking at PA4 (which could have N, O, K, M, 0, J, I and 9 as possible button presses).

On the next read from Port B, we can set which keys of this “line” are actually pressed. This is, again, done inverted. So, we start with 0xff, and invert the bits of the PB0-7 accordingly.
So, for our example, if we had pressed the keys N and M at that very moment of the read, we will just return 0x6f.
(N = PB7, M = PB4, -> invert bit 7 and bit 4 from 0xff => 0x6f)

uint8_t readCIA1DataPortB() {
	cia1_data_port_B = 0xff;
	uint8_t pos = cia1_data_port_A ^ 0xff;
	uint8_t c = 0;
	while (pos) {
		pos >>= 1;
		c++;
	}
	if (c && cia1_data_port_A) {
		c--;
		for (uint8_t i = 0; i < 8; i++) {
			if (KEYS[KEYMAP[c][i]])
				cia1_data_port_B &= ~(1 << i);
		}
	}
	else if (cia1_data_port_A != 0xff) {
		for (uint8_t f = 0; f < 8; f++) {
			for (uint8_t i = 0; i < 8; i++) {
				if (KEYS[KEYMAP[f][i]])
					cia1_data_port_B &= ~(1 << i);
			}
		}
	}
	return cia1_data_port_B;
}
NOTE: KEYS[] is just an array of SDL_Scancode's I set up accordingly to the Keyboard matrix, so I can just pull the equivalent when reading the keys.

CIA2

The CIA2 is very identical to the CIA1, except a few points.

Adress range: $DD00-$DDFF Tasks: Serial bus, RS-232, VIC memory, NMI control
Adress Function Remark
$DD00 Data Port A Bit 0..1: Select the position of the VIC-memory
  •  %00, 0: Bank 3: $C000-$FFFF, 49152-65535
  •  %01, 1: Bank 2: $8000-$BFFF, 32768-49151
  •  %10, 2: Bank 1: $4000-$7FFF, 16384-32767
  •  %11, 3: Bank 0: $0000-$3FFF, 0-16383 (standard)

Bit 2: RS-232: TXD Output, userport: Data PA 2 (pin M)
Bit 3..5: serial bus Output (0=High/Inactive, 1=Low/Active)

  • Bit 3: ATN OUT
  • Bit 4: CLOCK OUT
  • Bit 5: DATA OUT

Bit 6..7: serial bus Input (0=Low/Active, 1=High/Inactive)

  • Bit 6: CLOCK IN
  • Bit 7: DATA IN
$DD01 Data Port B Bit 0..7: userport Data PB 0-7 (Pins C,D,E,F,H,J,K,L)

The KERNAL offers several RS232-Routines, which use the pins as followed:
Bit 0, 3..7: RS-232: reading

  • Bit 0: RXD
  • Bit 3: RI
  • Bit 4: DCD
  • Bit 5: User port pin J
  • Bit 6: CTS
  • Bit 7: DSR

Bit 1..5: RS-232: writing

  • Bit 1: RTS
  • Bit 2: DTR
  • Bit 3: RI
  • Bit 4: DCD
  • Bit 5: User port pin J
$DD02 Data direction
Port A
see CIA 1
$DD03 Data direction
Port B
see CIA 1
$DD04 Timer A
Low Byte
see CIA 1
$DD05 Timer A
High Byte
see CIA 1
$DD06 Timer B
Low Byte
see CIA 1
$DD07 Timer B
High Byte
see CIA 1
$DD08 Real Time Clock
1/10s
see CIA 1
$DD09 Real Time Clock
Seconds
see CIA 1
$DD0A Real Time Clock
Minutes
see CIA 1
$DD0B Real Time Clock
Hours
see CIA 1
$DD0C Serial
shift register
see CIA 1
$DD0D Interrupt control
and status
CIA2 is connected to the NMI-Line.

Bit 4: 1 = NMI Signal occured at FLAG-pin (RS-232 data received)
Bit 7: 1 = NMI An interrupt occured, so at least one bit of INT MASK and INT DATA is set in both registers.

$DD0E Control Timer A see CIA 1
$DD0F Control Timer B see CIA 1

As you can see, the Timers in the CIA2 don’t trigger IRQs, but NMIs (Non-Maskable Interrupts). From a programmers view this is very comfortable, because we can select either of the interrupts, without a difficult set-up first.

The other different part is, Port A and Port B don’t handle the keyboard and joystick, but rather the serial port (RS232), which does the communication with e.g. the Disk Drive 1541 (which we won’t emulate for the moment, but maybe in the later process, if there are enough benefits to it), and, very important, to what the window is set, that the VIC has when it looks at RAM.
Since the VIC only ever can see 16k of our 64k RAM, this setting is very important when it comes to reading graphics (bitmaps etc.).

Speaking about the 64K of RAM we have, well, we do have 64K addressable, but we can reach quite a bit more than just 64K of memory. This is, where the PLA comes into play, that sets which memory region is showing which component – this will be our next chapter.