Thursday, 22 May 2025

A Commodore 64 Emulator in Flutter: Part 13

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.

In our case we would place the focusNode in our Bloc. Probably not the best place if one think about separation of concerns, but for now it is the best place if we want to keep a single instance of FocusNode alive. So, we do the following changes:

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

Next, let us see we can simulate a key press in our emulator. To figure out how let us dig a bit into how the keyboard is implemente in hardware.

Firstly, a keyboard is arranged a matrix of rows and columns, and where a row and column meets, there is a key switch. If the switch is pushed, it will short the row to ground. To see if a switch is pressed is a two step process. You need to energised each column in turn and see which columns are shorted to ground.

Firstly, to get an idea how the matrix of a C64 is arranged, the following diagram is helpful:

Now, the big question is which memory locations do we need to manipulate and read to see which key was pressed.

The following web link provide us with a memory map which will aid in finding these memory locations:

Scrolling down, we eventually find the place where it is dealt with the keyboard:


As you can see, both these ports is used by the joystick ports and the keyboard. The first piece of info that is useful for us, is the following at memory location DC00:

  • Bit #x: 0 = Select keyboard matrix column #x.

So, this is actually where we energise one or more columns. In the matrix diagram, this is actually the parts labeled A - H. Each of these are assigned a bit number (0 - 7) in the byte we write to this port.

The next piece of useful info is at memory location DC01:

  • Bit #x: 0 = A key is currently being pressed in keyboard matrix row #x, in the column selected at memory address $DC00.

So, we select one or more columns in location DC00 and within the selected column, we can read via location DC01 which rows in that column is selected.

Let us now see how we can emulate a keypress in our emulator. At this point we are able to catch keys from the keyboard with a KeyboardListener. In our KeyboardListener we can basically trigger events for which we listen for in our Bloc.

First let us define a event class which we will trigger:

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.

With this in mind, let us modify our 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) {
                      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.

Now, the action simulation of a key press should happen in our Memory class when a read is done from address DC01, we should consider which column is enable via address DC00, and see if in the column enabled, that there is indeed one of the keys held down and send back a value that reflects this.

So, we have a situation here where Memory wants some info from our Bloc class in which it lives, but we dont want to provide Memory for with all the state of the Bloc class. To achieve this we need to create an interface with methods returning the info the Memory needs.

Here is the interface:

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.

So, let us look at the keyboard matrix diagram again to see where the M key is located. The M key is located at column E and row 4. So with the bit counting starting at column A, the bit number of column E is 4.  So we are interested in column bit 3 and row bit 4. 

With this in mind, Let us give getKeyInfo() some meat:

  @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.

With all this written, let us make our Memory class make use of it:

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.

Now, when we build and run, and press the M key a couple of times, the screen looks like as follows:

We managed to implement the implement a simple key press!

Implementing the full keyboard

Let us now look at implementing a full keyboard, or at least sufficient keys, like the alphabet, digits and some symbols, just to type a simple basic program within our emulator.

Up to now we kept track only of a single whether it is down via keyDown, but now we need to keep track of whether several keys are held down. So, we need like kind of a boolean matrix, or to put it more plainly, an array of eight bytes. Each column is a byte:

  final List<int> matrix = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
I mentioned earlier that in a real C64, a zero means the key is selected. So, this array filled with the value 0xff's, means no key is held down at the moment.

Previously in our Main class, we just looked for the M key being pressed and released, and then pass this event to our Bloc class. Obviously now we will need to remove this explicit check for the M key and pass all key events to our Bloc class. This necessitates us to modify our KeyC64Event class to say which key was pressed not if a key was pressed:

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.

In the if statement, if the key is released, we OR the bit position with. Please it is pressed, we mask off the bit position.

Now we need to modify the method getKeyInfo, which is the method our Memory class calls when reading Address DC01. When calling this method, we tell the method which columns needs to be considered. Potentially two ore more columns can be selected, in which case we need to do a kind of a OR operation, to reduce the selected columns to one.

We can express this reducing in a simple for loop:

  @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.

Now, let us see if we can write a simple program, with the keyboard input enabled:

Next, let us run the program:
We have a working program!

In Summary

In this post we implemented keyboard input and write a small test program.

In the next post we will start implementing tape loading, from a tape image.

Until next time!