Foreword
In the previous we managed to boot the C64 system with a screen showing the contents of screen memory in real time. It booted with the welcome message and a flashing cursor.
In this post we will provide some keyboard interfacing with our C64 emulator. We will approach this in a very experimental fashion, exploring how Flutter itself work with keyboard interfacing in a app. Then we will try to see if we can get keyboard interfacing to work in our app, and finally see if our emulator can work with the keyboard.
Enjoy!
KeyboardListener in Flutter
What we want for our emulator is basically to tell when a key is held down, and when it is released. Flutter provides this for us via a KeyboardListener. From the Flutter documentation it is not so straightforward on how to use this, so I looked around for a worked example on the Internet and found the following:
https://medium.com/@wartelski/how-to-flutter-keyboard-events-keyboard-listener-in-flutter-web-0c36ab9654a9
The following snippet is the core of the example:
With this example we can basically catch it when a key is down. Now all is well in this example, except for we have a final variable for _focusNode. This is, however, only a thing we can do with a StatefulWidget. In our case, however, we are within a StatelessWidget, where we cannot do such things.class C64Bloc extends Bloc<C64Event, C64State> { final Memory memory = Memory(); final FocusNode focusNode = FocusNode(); ... }And now we go further and wrap our RawImage in a KeyboardListener:
... } else if (state is RunningState) { return KeyboardListener( focusNode: context.read<C64Bloc>().focusNode, autofocus: true, onKeyEvent: (event) => { if (event is KeyDownEvent) { if (event.logicalKey == LogicalKeyboardKey.keyM) { print("The m key is pressed!!") } } else if (event is KeyUpEvent) { if (event.logicalKey == LogicalKeyboardKey.keyM) { print("The m key is released!!") } } }, child: RawImage( image: state.image, scale: 0.5), ); } else { ...So, here we listen for the "M" key and write out to the console when this key is pressed and released.
Simulating a key press in our emulator
Bit #x: 0 = Select keyboard matrix column #x.
Bit #x: 0 = A key is currently being pressed in keyboard matrix row #x, in the column selected at memory address $DC00.
class KeyC64Event extends C64Event { final bool keyDown; KeyC64Event({required this.keyDown}); }So, we will either trigger an event with keyDown = true, when a key is pressed, or an event with keyDown = false, when a key is released.
} else if (state is RunningState) { return KeyboardListener( focusNode: context.read<C64Bloc>().focusNode, autofocus: true, onKeyEvent: (event) => { if (event is KeyDownEvent) { if (event.logicalKey == LogicalKeyboardKey.keyM) { context.read<C64Bloc>().add(KeyC64Event(keyDown: true)) } } else if (event is KeyUpEvent) { if (event.logicalKey == LogicalKeyboardKey.keyM) { context.read<C64Bloc>().add(KeyC64Event(keyDown: false)) } } }, child: RawImage( image: state.image, scale: 0.5), ); } else {Next, let us listen for these events in our Bloc:
class C64Bloc extends Bloc<C64Event, C64State> { ... bool keyDown = false; ... C64Bloc() : super(InitialState()) { ... on<KeyC64Event>((event, emit) { keyDown = event.keyDown; }); ... } ... }So, within our Bloc, keyDown is a variable keeping track of whether the key is up or down, which in this case is the state of the M key on our keyboard. We will make use of this variable to simulate a key stroke in our emulator.
abstract class KeyInfo { int getKeyInfo(int column); }And now let us implement the interface in our Bloc:
class C64Bloc extends Bloc<C64Event, C64State> implements KeyInfo { ... @override int getKeyInfo(int column) { } ... }So, given the list of columns energised, we return the rows. Now, as an exercise, lets say if we press the M key on the keyboard, which we currently check for in our KeyBoardListener, we want our C64 emulator to also show an M.
@override int getKeyInfo(int column ) { if (!keyDown) { return 0xff; } if ((column & 0x10) == 0) { return 0xef; } else { return 0xff; } }One thing to remember here is that when working with the keyboard matrix, we don't work with the default assumption that one means active, but the other way around. So a zero means in the column byte that a certain column is energised, and a zero in the row byte means that the switch for that bit position is held down.
class Memory { ... late final KeyInfo keyInfo; ... setKeyInfo(KeyInfo keyInfo) { this.keyInfo = keyInfo; } ... }So, we can pass our keyInfo object to our Memory class. We assign the keyInfo when our Bloc class is instantiated:
class C64Bloc extends Bloc<C64Event, C64State> implements KeyInfo { ... C64Bloc() : super(InitialState()) { memory.setKeyInfo(this); ... } ... }Finally, let us use keyInfo our Memory class:
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 == 0xDC01) { return keyInfo.getKeyInfo(_ram.getUint8(0xDC00)); } else { return _ram.getUint8(address); } } ...So, when address DC01 is read from our Memory we invoke getKeyInfo and passing it the contents of memory location DC00. At the moment we will fetch location DC00 from RAM.
Implementing the full keyboard
final List<int> matrix = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
class KeyC64Event extends C64Event { final bool keyDown; final LogicalKeyboardKey key; KeyC64Event({required this.keyDown, required this.key}); }With this in place our Bloc class will receive indeed a key code, but what make only sense in the Flutter world. We need a kind of a lookup table or a map to convert a Flutter keyboard scan code to a C64 keyboard scan code. So for this purpose we create the following map, preferably in a separate file:
Map<LogicalKeyboardKey, int> keyMap = Map.unmodifiable({ LogicalKeyboardKey.keyA : 0x0A, LogicalKeyboardKey.keyB : 0x1C, LogicalKeyboardKey.keyC : 0x14, LogicalKeyboardKey.keyD : 0x12, ... LogicalKeyboardKey.digit0 : 0x23, LogicalKeyboardKey.digit1 : 0x38, LogicalKeyboardKey.digit2 : 0x3B, LogicalKeyboardKey.digit3 : 0x08, LogicalKeyboardKey.digit4 : 0x0B, LogicalKeyboardKey.digit5 : 0x10, LogicalKeyboardKey.digit6 : 0x13, LogicalKeyboardKey.digit7 : 0x18, LogicalKeyboardKey.digit8 : 0x1B, LogicalKeyboardKey.digit9 : 0x20, ... LogicalKeyboardKey.space : 0x3c, LogicalKeyboardKey.shiftLeft : 0x0F, LogicalKeyboardKey.enter : 0x01, ... });With this map cretaed, we can now modify our listener a bit for the event KeyC64Event:
on<KeyC64Event>((event, emit) { int c64KeyCode = keyMap[event.key] ?? 0; int col = c64KeyCode >> 3; int row = 1 << (c64KeyCode & 7); if (!event.keyDown) { matrix[col] |= row; } else { matrix[col] &= ~row; } });We start off by looking up the C64 scancode, given the Flutter key code. Now bit 5-3 of the scan code is the column and bits 2-0 is the row.
@override int getKeyInfo(int column ) { int result = 0xff; // Accumulator for the OR'ed numbers for (var row in matrix) { if ((column & 1) == 0) { result &= row; } column = column >> 1; } return result; }We are shifting the column right eveyrtime, looking everytime if the lowest bit is zero. If it is zero, we know the column is selected. We and all the selected columns together. If, for any row position in a selected column there is a zero, then the final value for that bit position would be zero. A zero means there was one or more keys selected in that bit position for the selected columns.