Foreword
In the previous post we managed to interface the keyboard to our C64 Flutter emulator. With that implemented, we were able to enter a simple Basic program into our emulator and running it.
Now, my ultimate goal for writing this emulator, is to be able to run the game Dan Dare in our emulator, loading it from a tape image.
So, to achieve this end goal, the next goal would be for our emulator to be able to load a tape image. On a C64, loading from the tape rely heavily on the features of a CIA (Complex Interface Chip). The features tape loading rely on is connecting access the read head from the tape, timers and interrupts.
Up to now we have been mimicking some of the features of a CIA chip. The address range of the CIA chip is within DC00-DCFF. It immediately comes to mind that in the previous post we implemented two of the registers of the CIA, DC00 and DC01 for keyboard access.
We also implicitly implemented a timer and interrupts in our emulator, interrupting the CPU every 1/60 of a second, so that the cursor can flash and keyboard entry could work. However, we blindly forced these interrupts just as a quick hack just to get the cursor and keyboard to work. We didn't even consider the values set in the CIA for setting the timer.
However, to implement tape loading we would not be able to get away with a quick hack 😀 We will need to emulate the CIA properly for this purpose.
So, in this post we will implement CIA emulation bit by bit. This will include revisiting our current keyboard and timer interrupt implementation (e.g. doing the 1/60 second interrupt), and implementing it properly with CIA implementation.
We will probably only get to tape emulation in the next post.
Enjoy!
Creating the CIA skeleton
Lets begin our journey by creating a CIA class just as a skeleton. This class will evolve over time to contain all the functionality that a CIA will contain:
class Cia1 {
setMem(int address, int value) {
print("setMem ${address.toRadixString(16)} ${value.toRadixString(16)}");
}
int getMem(int address) {
print("getMem ${address.toRadixString(16)}");
return 0;
}
}
Here we do something interesting. Every write or read from the CIA address range we log. With this we can see which functionality is used and we can just implement the bare minimum functionality of the CIA chip. step() {
/*
if ((_cycles > 1000000) &&((_cycles % 16666) < 30) && (_i == 0)) {
push(pc >> 8);
push(pc & 0xff);
push((_n << 7) | (_v << 6) | (2 << 4) | (_d << 3) | (_i << 2) | (_z << 1) | _c);
_i = 1;
pc = memory.getMem(0xfffe) | (memory.getMem(0xffff) << 8);
}
*/
var opCode = memory.getMem(pc);
pc++;
var insLen = CpuTables.instructionLen[opCode];
...
}
Next we need to make an instance of this class and inject into our Memory class: C64Bloc() : super(InitialState()) {
memory.setKeyInfo(this);
on<InitEmulatorEvent>((event, emit) async {
final basicData = await rootBundle.load("assets/basic.bin");
final characterData = await rootBundle.load("assets/characters.bin");
final kernalData = await rootBundle.load("assets/kernal.bin");
Cia1 cia1 = Cia1();
memory.setCia1(cia1);
...
}
...
}
We modify the actual Memory class like this:class Memory {
...
late final Cia1 cia1;
...
setCia1(Cia1 cia1) {
this.cia1 = cia1;
}
...
setMem(int value, int address ) {
if ((address >> 8) == 0xDC) {
cia1.setMem(address, value);
} else {
_ram.setInt8(address, value);
}
}
int getMem(int address) {
_readCount++;
if (address >= 0xA000 && address <= 0xBFFF) {
return _basic.getUint8(address & 0x1fff);
} else if (address >= 0xE000 && address <= 0xFFFF) {
return _kernal.getUint8(address & 0x1fff);
} else if (address == 0xD012) {
return (_readCount & 1024) == 0 ? 1 : 0;
} else if ((address >> 8) == 0xDC ) {
return cia1.getMem(address);
/*else if (address == 0xDC01) {
return keyInfo.getKeyInfo(_ram.getUint8(0xDC00));*/
} else {
return _ram.getUint8(address);
}
}
}
So, every time an address starts with DC we send this access to the CIA instance. You will also see that I have commented out the explicit access to the DC01 register, which we added in the previous post for keyboard access. We will implement this functionality at a later stage into our CIA class.setMem dc0d 7f setMem dc00 7f setMem dc0e 8 setMem dc0f 8 setMem dc03 0 setMem dc02 ff setMem dc04 95 setMem dc05 42 setMem dc0d 81 getMem dc0e setMem dc0e 11 setMem dc04 25 setMem dc05 40 setMem dc0d 81 getMem dc0e setMem dc0e 11So, let us quickly see what is going on here. With the write to DC0D, we disable all interupts going to the CPU.
Implementing the Alarm System
However, as I added more of these operations dependant on CPU cycles executed, I saw performance gradually worsening, especially when I added more of the VIC-II operations.
class Alarms {
final LinkedList<Alarm> _alarmList = LinkedList<Alarm>();
Alarms();
Alarm addAlarm(Function(int remainder) callback) {
var alarm = Alarm._(this, callback);
_alarmList.add(alarm);
return alarm;
}
}
So, here we have a class containing all our alarms. Internally all the alarms is store in a linked list, which is a data structure in Dart. We will visit this in a while.final class Alarm extends LinkedListEntry<Alarm> {
late final Alarms _alarms;
late final Function(int remainder) _callback;
Alarm._(Alarms alarms, Function(int remainder) callback ) {
_alarms = alarms;
_callback = callback;
}
}
Let us now add some more meat to our alarm class:final class Alarm extends LinkedListEntry<Alarm> {
var _targetClock = 0;
...
setTicks(int ticks) {
_targetClock = _alarms.getCurrentCpuCount() + ticks;
}
getRemainingTicks() {
return _targetClock - _alarms.getCurrentCpuCount();
}
getTargetClock() {
return _targetClock;
}
processAlarm(int remainder) {
_callback(remainder);
}
}
Basically I have added some methods for keeping track of how far we are from triggering a alarm. The processAlarm will be invoked when the alarm is triggered.class Alarms {
final LinkedList<Alarm> _alarmList = LinkedList<Alarm>();
int _cpuCount = 0;
Alarms();
Alarm addAlarm(Function(int remainder) callback) {
var alarm = Alarm._(this, callback);
_alarmList.add(alarm);
return alarm;
}
reAddAlarm(Alarm alarm) {
_alarmList.add(alarm);
}
int getCurrentCpuCount() {
return _cpuCount;
}
processAlarms(int cpuCycles) {
_cpuCount = cpuCycles;
for (Alarm item in _alarmList) {
if (item.getRemainingTicks() <= 0) {
item.processAlarm(item.getRemainingTicks());
}
}
}
}
The key method added here is processAlarms(). This method loops through the alarms, checking which expired and then calling its callback.Wiring everything together
class C64Bloc extends Bloc<C64Event, C64State> implements KeyInfo {
final Memory memory = Memory();
final List<int> matrix = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
final FocusNode focusNode = FocusNode();
late final Cpu _cpu = Cpu(memory: memory);
late final Alarms alarms = Alarms();
type_data.ByteData image = type_data.ByteData(200*200*4);
int dumpNo = 0;
int frameNo = 0;
Timer? timer;
...
C64Bloc() : super(InitialState()) {
on<InitEmulatorEvent>((event, emit) async {
final basicData = await rootBundle.load("assets/basic.bin");
final characterData = await rootBundle.load("assets/characters.bin");
final kernalData = await rootBundle.load("assets/kernal.bin");
Cia1 cia1 = Cia1(alarms: alarms);
cia1.setKeyInfo(this);
memory.setCia1(cia1);
memory.populateMem(basicData, characterData, kernalData);
_cpu.setInterruptCallback(() => cia1.hasInterrupts());
...
}
...
}
I have added a field for our alarms. I am also now injecting an Cia1 instance into our memory. on<RunEvent>((event, emit) {
timer = Timer.periodic(const Duration(milliseconds: 17), (timer) {
int start = DateTime.now().millisecondsSinceEpoch;
int targetCycles = _cpu.getCycles() + 16666;
do {
_cpu.step();
alarms.processAlarms(_cpu.getCycles());
} while (_cpu.getCycles() < targetCycles);
...
});
});
After every CPU we process the alarms with the current cpu cycles.Expanding the CIA1 class
class Cia1 {
int timerAlatchLow = 0xff;
int timerAlatchHigh = 0xff;
int timerAvalue = 0xffff;
Alarms alarms;
Alarm? timerAalarm;
bool timerAstarted = false;
bool timerAoneshot = false;
int registerE = 0;
int register0 = 0;
bool timerAinterruptEnabled = false;
bool timerAintOccurred = false;
late final KeyInfo keyInfo;
Cia1({required this.alarms});
setKeyInfo(KeyInfo keyInfo) {
this.keyInfo = keyInfo;
}
...
}
The meaning of these private variables will became clear in a bit. updateTimerA() {
if (!timerAstarted) {
return;
}
if (timerAalarm != null) {
timerAvalue = timerAalarm!.getRemainingTicks();
}
}
timerAValue is the value of count down timerA in the CIA. To increase locality, we dont update this value with the execution of every CPU instruction. Instead, we wrote this method that updates the value when the CPU reads the value of this register. hasInterrupts() {
if (timerAintOccurred && timerAinterruptEnabled) {
return true;
} else {
return false;
}
}
processTimerAalarm(int remaining) {
// Do interrupt
timerAintOccurred = true;
if (timerAoneshot) {
timerAalarm?.unlink();
timerAstarted = false;
return;
}
timerAalarm!.setTicks((timerAlatchLow | (timerAlatchHigh << 8)) + remaining);
}
Here we deal with when the timer expire and we set interrupts. We remove the timer from the alarm list if it is oneshot. Otherwise we schedule the running of the timer again. setMem(int address, int value) {
print("setMem ${address.toRadixString(16)} ${value.toRadixString(16)}");
value = value & 0xff;
address = address & 0xf;
switch (address) {
case 0x0:
register0 = value;
case 0x4:
timerAlatchLow = value;
case 0x5:
timerAlatchHigh = value;
case 0xD:
if ((value & 0x80) != 0) {
timerAinterruptEnabled = ((value & 1) == 1) ? true : timerAinterruptEnabled;
} else {
timerAinterruptEnabled = ((value & 1) == 1) ? false : timerAinterruptEnabled;
}
case 0xE:
var startTimerA = ((value & 1) == 1) ? true : false;
var forceTimerA = ((value & 16) != 0) ? true : false;
updateTimerA();
if (forceTimerA) {
timerAvalue = timerAlatchLow | (timerAlatchHigh << 8);
}
var startingTimerA = startTimerA & !timerAstarted;
var stoppingTimerA = !startTimerA & timerAstarted;
var alreadyRunningTimerA = startTimerA && timerAstarted;
if (startingTimerA || (alreadyRunningTimerA && forceTimerA)) {
// schedule timer on alarm
timerAalarm ??= alarms.addAlarm( (remaining) => processTimerAalarm(remaining));
if (timerAalarm!.list == null) {
alarms.reAddAlarm(timerAalarm!);
}
timerAalarm!.setTicks(timerAvalue);
// set timer as started
} else if (stoppingTimerA) {
//unschedule timer A
timerAalarm!.unlink();
}
timerAoneshot = (value & 8) != 0;
timerAstarted = startTimerA;
registerE = value;
default:
// throw "Not implemented";
}
}
int getMem(int address) {
print("getMem ${address.toRadixString(16)}");
updateTimerA();
address = address & 0xf;
switch (address) {
case 0x0:
return register0;
case 0x1:
return keyInfo.getKeyInfo(register0);
case 0x4:
return timerAvalue & 0xff;
case 0x5:
return timerAvalue >> 8;
case 0xD:
if (timerAintOccurred) {
timerAintOccurred = false;
return 0x81;
} else {
return 0;
}
case 0xE:
var result = registerE & 0x06;
result = result | (timerAstarted ? 1 : 0);
result = result | (timerAoneshot ? 8 : 0);
return result;
}
return 255;
}
You will see each time we read from the CIA1 we update the timer. In the write function we also adjust the alarms accordingly if we chane the state of the times.Changes to the CPU class
class Cpu {
...
late final Function() _interruptCallback;
...
setInterruptCallback(Function() callback) {
_interruptCallback = callback;
}
...
step() {
if (_interruptCallback() & (_i == 0)) {
push(pc >> 8);
push(pc & 0xff);
push((_n <<< 7) | (_v << 6) | (2 << 4) | (_d << 3) | (_i << 2) | (_z << 1) | _c);
_i = 1;
pc = memory.getMem(0xfffe) | (memory.getMem(0xffff) << 8);
}
...
}
...
}
Now, we call the interruptCallBack, which basically tie back to the CIA1 class we created. Also, we only invoke an interrupt only when the Inteerupt disable flag is not set.