Foreword
In the previous post we introduced the CIA as a separate class. Previously mimicked the CIA's operation, by just forcing a hard interrupt every 1/60th of a second, just to get our emulator to work, avoiding the complexities of implementing and scheduling timers.
Thus, in the previous post we delved deeper and implement the CIA. This was actually needed as a precursor to this post where we will be implementing Tape loading functionality, which require more granular operation of the CIA.
Adding Front end Interaction
The logical place to start, is to add functionality to our front end for attaching a tape image. So within main.dart, which is basically our front end code, we add two buttons for the RunningState front end:
...
} else if (state is RunningState) {
return KeyboardListener(
focusNode: context.read<C64Bloc>().focusNode,
autofocus: true,
onKeyEvent: (event) => {
if (event is KeyDownEvent) {
context.read<C64Bloc>().add(KeyC64Event(keyDown: true, key: event.logicalKey))
} else if (event is KeyUpEvent) {
context.read<C64Bloc>().add(KeyC64Event(keyDown: false, key: event.logicalKey))
}
},
child: Column(
children: [
Row(
children: [
IconButton(
icon: Icon(Icons.folder),
onPressed: () async {
context.read<C64Bloc>().add(LoadTapeRequested());
}),
IconButton(
icon: Icon(Icons.play_arrow),
onPressed: !state.tapeLoaded ? null : () async {
context.read<C64Bloc>().add(PlayTapeRequested());
})
],
),
RawImage(
image: state.image, scale: 0.5),
],
));
}
...
As usual, we will add LoadTapeRequested and PlayTapeRequested in c64_event.dart. on<PlayTapeRequested>((event, emit) {
_tape.playTape();
});
on<LoadTapeRequested>((event, emit) async {
final result = await FilePicker.platform.pickFiles(
withData: true,
type: FileType.custom,
allowedExtensions: ['tap', 't64'],
);
if (result == null) return;
tapeLoaded = true;
_tape.setTapeImage(result.files.single.bytes!);
});
For PlayTapeRequested we simulate the press of a play button. _tape is an instance of a class Tape, which we will define later.The Tape Class
class Tape implements TapeMemoryInterface {
late Iterator _tapeImage;
bool _playSelected = false;
Alarms alarms;
TapeInterrupt interrupt;
Alarm? _tapeAlarm;
Tape({required this.alarms, required this.interrupt});
}
Before we go into detail on how to implement this class, let us take a step back and think about how Tape loading works on a C64. setTapeImage(type_data.Uint8List tapeData) {
_tapeImage = tapeData.iterator;
for (var i = 0; i < 21; i++) {
_tapeImage.moveNext();
}
populateRemainingPulses();
}
Uint8List variables provides you with an Iterator. In a tape image actual pulse width data actually starts after 21 bytes. populateRemainingPulses() {
var val = _tapeImage.current;
if (val != 0) {
_remainingPulseTicks = val << 3;
_tapeImage.moveNext();
} else {
var byte0 = _tapeImage.current;
_tapeImage.moveNext();
var byte1 = _tapeImage.current;
_tapeImage.moveNext();
var byte2 = _tapeImage.current;
_tapeImage.moveNext();
_remainingPulseTicks = (byte2 << 16) | (byte1 << 8) | byte0;
}
}
Here we need to understand the TAP format a bit better. Usually every byte indicates one pulse width. We then need to multiply this value by 8 to get to the width in CPU clock cycles.- Bit 4 - Cassette Switch Sense; 1 = Switch Closed
- Bit 5 - Cassette Motor Control; 0 = On, 1 = Off
@override
setMotor(bool on) {
if (on == _currentMotorOn) {
return;
}
_currentMotorOn = on;
if (on) {
setupAlarms();
} else {
_tapeAlarm!.unlink();
_remainingPulseTicks = _tapeAlarm?.getRemainingTicks();
}
}
This method will be invoke when we write to memory via our Memory class. We will deal with this plumbing later. setupAlarms() {
_tapeAlarm ??= alarms.addAlarm( (remaining) => processTapeAlarm(remaining));
if (_tapeAlarm!.list == null) {
alarms.reAddAlarm(_tapeAlarm!);
}
_tapeAlarm!.setTicks(_remainingPulseTicks);
}
Here we see the actual use of _remainingPulseTicks, when the motor is resumed. processTapeAlarm(int remaining) {
interrupt.triggerInterrupt();
populateRemainingPulses();
_tapeAlarm!.setTicks(_remainingPulseTicks + remaining);
}
This method is called when the pulse has expired. During this we trigger an interrupt and reschedule the next alarm. @override
int getCassetteSense() {
return _playSelected ? 0 : 0x10;
}
Changes to the CIA class
hasInterrupts() {
if (timerAintOccurred && timerAinterruptEnabled) {
return true;
} else if (timerBintOccurred && timerBinterruptEnabled) {
return true;
} else if (tapeInterruptOccurred && tapeInterruptEnabled) {
return true;
} else {
return false;
}
}
You will notice that I have included timerB interrupts and tape interrupts in the check as well. setMem(int address, int value) {
...
case 0xD:
if ((value & 0x80) != 0) {
timerAinterruptEnabled = ((value & 1) == 1) ? true : timerAinterruptEnabled;
} else {
timerAinterruptEnabled = ((value & 1) == 1) ? false : timerAinterruptEnabled;
}
if ((value & 0x80) != 0) {
timerBinterruptEnabled = ((value & 2) == 2) ? true : timerBinterruptEnabled;
} else {
timerBinterruptEnabled = ((value & 2) == 2) ? false : timerBinterruptEnabled;
}
if ((value & 0x80) != 0) {
tapeInterruptEnabled = ((value & 16) == 16) ? true : tapeInterruptEnabled;
} else {
tapeInterruptEnabled = ((value & 16) == 16) ? false : tapeInterruptEnabled;
}
...
}
As you might remember previously, register D in the CIA is the interrupt mask register. Here we have added timerB and the Tape Inteerupt as some interrupts we can enable or mask out. int getMem(int address) {
...
case 0xD:
var value = 0;
if (timerAintOccurred) {
timerAintOccurred = false;
value = value | 0x81;
}
if (timerBintOccurred) {
timerBintOccurred = false;
value = value | 0x82;
}
if (tapeInterruptOccurred) {
tapeInterruptOccurred = false;
value = value | 0x84;
}
return value;
}
...
}
Here we are reading the same register from earlier, but reading doesn't return the masks, but the actual interrupts that occurred. Once again, we have added timerB and TapeInterrupt. Once this registere has been read, we also clear all occurred interrupts.Changes to the Memory Class
setMem(int value, int address ) {
if ((address >> 8) == 0xDC) {
cia1.setMem(address, value);
} else if (address == 1) {
_ram.setInt8(address, value);
_tape.setMotor((value & 0x20) == 0 );
} else {
_ram.setInt8(address, value);
}
}
So, as mentioned earlier, bit 5 of memory location 1 controls the tape motor. Here we implement it, so that during a memory write to this location, we call setMotor appropriately. 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 == 1) {
var value = _ram.getUint8(address) & 0xef;
return value | _tape.getCassetteSense();
} else {
return _ram.getUint8(address);
}
}
Here we add the Cassette sense bit when reading the byte from memory location one. As mentioned previously, the cassette sense bit indicates if we pressed the play button.The results
Notice we have two new icons at the top, a folder icon and a play button. We use the folder icon to locate the tape image file from our local file system. Once we have selected a tape image, the play button becomes enabled.
This is the Hooray moment. When seeing FOUND DAN DARE, or what the file name of the tape image you used, you know you have implemented the tape loading correctly.
flutter build web --releaseAfter the build is finished, you will find the result in build/web with the project. cd into this folder. We now need a web server to serve this, and the easiest one to use is Python. So, within the build folder, run the following command:
python -m http.server 8000Now access the emulator in the browser with http://localhost:8000/












