NEWS, EDITORIALS, REFERENCE

Subscribe to C64OS.com with your favorite RSS Reader
January 30, 2017#14 Programming Theory

The Event Model

Post Archive Icon

When we think about how a computer operates and how a human interacts with it there are a number of different models. Games, for example, have something called the main loop. The computer repeats the cycle of the main loop over and over while the main gameplay is occurring. On each pass through the loop the game updates game state, calculates the new positions of the bad guys, checks to see what key's or joystick movements the user has made, updates the position of the player, and finally redraws the scene as efficiently as possible. And loops to do that all over again.

Compare this to a typical BASIC program. The flow starts on a line number and progresses to the next line number. For-loops allow the program flow to loop back to an earlier line or even an earlier part of the same line and run the same code again. Goto and Gosub allow the program flow to jump to a different line and carry on from there. However, when you get to an Input statement, or a Get statement, the program flow pauses at that point until the input is complete. It is fundamentally impossible for the basic program to do anything while the user is entering data at an Input statement. This is so very unlike most arcade-style games. Where in the game, the world is constantly changing and being updated, and the user's input is only momentarily polled between updates to the rest of the world.

Now let's think about how a typical, modern, graphical user interface works. The state of the environment is not changing nearly so much as an arcade game's would. However, it is also not fully locked into one input until the input is complete. Rather, there is something akin to the game's loop that is responsible for redrawing changes in the "world" state, and also tracking user input. Definitely by keyboard, and usually also by mouse or joystick. Part of the world's changing state may consist of the position of the mouse cursor or the current time, for example.

In some sense an operating system with a graphical user interface is built a lot like a game. One difference is that in a game the actors and the world are highly constrained. The main loop can therefore be tigntly coupled to the very specific gameplay it was designed for. Whereas in an operating system the main loop has to be quite decoupled from the "world", because the world consists of many different programs and applications that can be run that have been written to work with the services provided by the operating system. The way an operating system accomplishes this decoupling is by using two (or more) independent loops. One of those loops, on the C64 it will be the interrupt service routine, is used to track user input. The continuous flow of user input: key presses, mouse movements, clicks, drags and more, are broken down into the smallest possible chunks and encapsulated as "events."

Events are small memory structures that contain all the relevant information about a user input action. The events are buffered—queued in chronological order—by the service routine. Because the service routine is driven by the processor's interrupt, it is always capable of polling for user input no matter what else the main program is doing. If the program is busy for more than a few hundred milliseconds as the user is typing or clicking, the service routine continues to capture, encapsulate and buffer new events at the same time.

The job of the operating system's main loop, therefore, is to check for the presence of new events and distribute them such that the right part of an application can begin executing and do whatever ought to be done in response to the kind of event and the contents it encapsulates. This model is known as "Event Driven." Rather than the code flowing from one line to the next and moving about as the result of Gotos/Gosubs or fixed menu options that are selected from the results of Input or Get statements, the entire program is divided into many small units and most of the time none of the program's code is running. The operating system is instead updating the screen, and capturing the actions of the user. When an event is created it causes the right part of the program the user is running to start up and process the event. After some code that handles the event completes, the program goes back to doing nothing until the presence of an event drives it into action again. Hence, the flow of the program is literally driven by events.

How does the C64's built in OS work?

Let's go back now to how the C64's built in operating system works. As it turns out, the idea of events and of a program being event driven is not entirely foreign even to how Basic programs work. The KERNAL implements the C64's interrupt service routine, and one of its jobs is indeed to poll the keyboard for user input. This happens even when the Basic program is otherwise occupied. Sounds exactly like what I was describing above, except that it only polls the keyboard and not a mouse. What about buffering events? Well, this takes us back to an early article I wrote, Why PETSCII Anyway? Every PETSCII character is just a single byte, but it includes control characters that are interpreted by the KERNAL's screen editor. The KERNAL does use an event buffer that buffers PETSCII characters. The buffer is 10 bytes long. This is enough for 10 one-byte keyboard events.

They really are like extremely trimmed down events. Unlike in modern web and OS event models, there is no way to know about a key going up or going down, or being held in combination with other modifiers. What you get is a PETSCII character code in the buffer. So, what about the main loop, and event delivery? Well, as it happens the built in OS has that too. This is the KERNAL's main event loop:

Wait for a key from the keyboard

.,E5CD A5 C6    LDA $C6         get the keyboard buffer index
.,E5CF 85 CC    STA $CC         cursor enable, $00 = flash cursor, $xx = no flash
.,E5D1 8D 92 02 STA $0292       screen scrolling flag, $00 = scroll, $xx = no scroll
                                this disables both the cursor flash and the screen scroll
                                while there are characters in the keyboard buffer
.,E5D4 F0 F7    BEQ $E5CD       loop if the buffer is empty

That code above is not part of your Basic program. It is a loop built into the KERNAL that is independent of the interrupt service routine, that continually checks for the presence of a PETSCII character event in the event queue. Your Basic program essentially executes as quickly as possible, racing towards the first Input or Get statement. As soon as it hits one of those statements it stops dead, no further Basic code is executed. Instead, control returns to the KERNAL's main event loop, above. This will loop well over a hundred thousand times a second as it idly waits for an event to appear in the buffer.

Well, what about being event driven?. If the statement was an Input statement, the KERNAL continues to accept events from the buffer and processes them through the code of the screen editor up until the return key is pressed. You can change text colors, clear the screen, move the cursor around the screen with the cursor keys, shift from uppercase graphics to shifted character set modes, etc. When the return key is pressed, control is handed back to the Basic program which continues executing the code that follows the Input statement. If the statement was a Get statement, then the Basic program has a lot more control. The moment a single PETSCII character event appears in the buffer, the KERNAL immediately returns control to the Basic program with the contents of that event.

How is the event delivered? Delivery is exceedingly simple. The event is only ever delivered to the very next line of the Basic program. Many Basic programs are designed around a simple menu. The menu is drawn on the screen and lists several options specifying a letter or number for each option. Then the program hits a Get statement, and Basic execution stops, only the KERNAL's main event loop and interrupt service routine are then running. When the event is returned to the Basic program it checks to see if the character corresponds to a menu option, and branches off to a different part of the program depending. Usually a program is complex enough that other steps are required after that, but eventually, the program will either end, or it will return to redraw the main menu and stop at the Get statement again. And there you have it, the program is event driven. And the Get and Input statements are the machinery of event delivery.

How does the event model in C64 OS work?

Unlike GEOS and some other C64 operating systems, C64 OS does not replace the KERNAL rom. It does however patch out the BASIC rom in order to reclaim 8K of RAM, and because the execution model of Basic is completely incompatible with a more modern event-driven model. After patching out BASIC, C64 OS redirects the interrupt to a custom routine. This routine is similar to the KERNAL's routine, but more advanced. It updates the JiffyClock, it flashes the cursor (if enabled), it draws the time of day to the screen and flashes the colon between the minutes and seconds. Next it reads changes to the mouse, and updates the mouse cursor's position on the screen. The service routine will also be responsible for some other things, such as polling for ethernet data, and firing off scheduled timers. But this is not an article about those other things. After updating the mouse cursor it begins the process of scanning and producing events.

C64 OS's events are moderately sophisticated, but still trim enough to fit in a very small amount of memory. Modern OSes allow the mouse buttons to be modified by the keyboard, and also allow keyboard commands to be the result of combinations of modifier keys with a character key. This level of event generation on a C64 is nearly unheard of. One problem is that the mouse buttons are read via the same CIA1 port as part of the keyboard matrix. On a 1351 mouse, the left button is equivalent to the joystick fire button, and the right mouse button is like pushing the joystick up. In order to see what we are dealing with it is necessary to understand how the CIA's ports are wired to the keyboard matrix and joystick/mouse ports.

                                CIA 1 Port B ($DC01)           Joy 2

        Bits      PB7   PB6   PB5   PB4   PB3   PB2   PB1   PB0
        Wires     3     6     5     4     7     2     1     0


        PA7 A     STOP  Q     C=    SPACE 2     CTRL  <-    1

        PA6 G     /     ^     =     RSHFT HOME  ;     *     £

CIA1    PA5 F     ,     @     :     .     -     L     P     +
Port A
($DC00) PA4 E     N     O     K     M     0     J     I     9    Fire

        PA3 D     V     U     H     B     8     G     Y     7    Right

        PA2 C     X     T     F     C     6     D     R     5    Left

        PA1 B     LSHFT E     S     Z     4     A     W     3    Down

        PA0 H     CSRDN F5    F3    F1    F7    CSRRT RETRN DEL  Up

Joy 1                               Fire  Right Left  Down  Up
(Note: Joystick Port 1 is connected to CIA1's Port B, and Joystick Port 2 is connected to CIA1's Port A)
https://www.c64-wiki.com/index.php/Keyboard

Ignoring Joystick Port 2, a mouse in Joystick Port 1 has its left button wired to the same pin of the CIA's Port B as all the keyboard keys in the Fire column (PB4). And its right mouse button is in the same column as all the keys in the Up column (PB0). What this means is that when, say, the left mouse button is held down, it is electrically impossible to determine the up or down status any of the following keys: Space, Right Shift, ., M, B, C, Z or F1.

Why is that exactly? It's because of how the keyboard matrix works. The keyboard matrix enables a depressed key to electrically connect a pin on CIA1's Port A to a pin on CIA1's Port B. The CIA ports are internally pulled up, which means if a pin is left floating it will be read by the computer as a binary 1. When a pin on Port A is set low, and a key in that row is depressed, it electrically connects the pin in that key's column such that it is pulled low. The computer then reads this pin as a binary 0, indicating the key is depressed.

However, when you press the left mouse button down, the PB4 pin of CIA1 is connected, through the mouse, directly to ground. Thus, that bit when read will always read as 0. Since the mouse drags the pin on Port B low regardless of what is set on CIA Port A, it is fundamentally impossible to read the status of a key in that column at the same time. A similar situation exists for the right mouse button. If the right mouse button is held down, it becomes electrically impossible to discern the up or down status of any of the following keys: 1, £, +, 9, 7, 5, 3 or DELETE.

In makes a lot of sense therefore to ignore the keyboard when we know that mouse buttons are being pressed or the joystick is being fiddled with. And this is exactly the approach taken by Craig Bruce in his 3-Key Rollover keyscanning routine discussed at length in a 1993 C= Hacking magazine article. I'll return to this routine in a bit, because C64 OS uses a modified version of it. The problem with ignoring the keyboard is that it then becomes impossible to combine keyboard modifier keys with mouse clicks. This would be unfortunate, because having such information dramatically increases the power and flexibility of potential user interfaces. A left click while the control key is held down, for example, could make the difference between following an HTML link on a web page and opening a dialog that gives you some information about the link. This would be a power user feature that would make using such a program feel much more flexible and modern.

When you use a C64 the behavior of both Left and Right Shift keys is identical. You would be forgiven if you believed that they were internally electrically together. However, they are not, their identical behavior is entirely produced by the keyscanning routine in the KERNAL. In fact, besides the RESTORE key which is not part of the matrix, but is wired directly to the CPU's NMI pin, and the Shift Lock key which is a mechanical latch on the Left Shift key, all the other 64 keys on the keyboard are entirely independent and have absolutely nothing physically special about them. Their behaviors are entirely determined by software. That's great because it means we are free to read the Left and Right Shift keys independent of one another.

The keys available on a C64 that are traditionally considered modifier keys are Control, C= Key, Left Shift and Right Shift. Of these, if you check the table above, only the Right Shift key collides with the mouse buttons. The first thing C64 OS does is to check if any of the mouse buttons are down. It does this by setting all pins on CIA1's Port A high, thus disabling all keyboard rows, and checking to see if any of the pins on CIA1's Port B are low. If they are, a mouse button is depressed. If mouse buttons are being pressed general keyboard scanning is skipped, just as in Craig Bruce's routine, however the Control key, Commodore Key and Left Shift key are explicitly and individually scanned. Next the mouse buttons are scanned. And some surprisingly complicated logic is employed to detect their transitions so they can be encoded into mouse events.

Now is a good time to say that C64 OS uses 3 separate event queues. Mouse Events, Keyboard Command Events, and Printable Text Events. I will describe each of these in further detail below.

Mouse Events

It is an obvious goal of C64 OS to keep the code as tight and fast as possible. For this reason, Mouse Events are just 3 bytes each. And the buffer can hold up to 3 events. Not exceeding 3 bytes is very convenient because the routine that reads an event off the queue can have its results returned entirely in registers. The three bytes are X position, Y position and Flags. These are returned in the .X, .Y and .A registers respectively. X and Y should be self-explanatory. The flags are more complicated. The lower 4 bits are used to define 16 possible event types. And the upper 4 bits are used as flags for the three modifier keys, and as a 9th bit of X position, because 320 pixels overflows an 8-bit value.

A Mouse Event is structured as follows:


X position - 1 Byte
Y position - 1 Byte
Flags      - 1 Byte

Flags Bits:

0 - | These 4 bits are used 
1 - | to hold a number
2 - | 0-F that represents
3 - | the event type

4 - Left Shift
5 - Control
6 - Commodore
7 - X Pos MSB

The Mouse Event types are as follows:

0000 - 0 - move
0001 - 1 - left down
0010 - 2 - left tracking
0011 - 3 - left up
0100 - 4 - left click
0101 - 5 - left dblclick
0110 - 6 - right down
0111 - 7 - right up
1000 - 8 - right click
1001 - 9 - reserved
1010 - A - reserved
1011 - B - reserved
1100 - C - reserved
1101 - D - reserved
1110 - E - reserved
1111 - F - reserved

The button tracking is surprisingly complicated. It is not a surprise to me why most mouse routines in other C64 projects have limited support and what one would consider odd behavior by modern standards. It's really hard to get it right. When no buttons are pressed, and the left button goes down, a left down event is generated. At the time that any event is generated the event captures the state of the modifier keys and the position of the cursor. While the left button is held down pressing the right button is completely ignored. While the left button is down, if the mouse moves, left tracking events are generated. When the left button is finally released a left up event is generated. If a sufficiently short period of time has elapsed between when the left button went down and when it went up, a left click event will also be generated.

A left double click event will be generated if a sufficiently short length of time elapses between two click events, however I have not implemented double clicking yet. Managing the buttons is quite tricky. For example, if the left button goes down, then the right button goes down and it is ignored, then the left button goes up leaving the right button still down. The right button going up will still be ignored. If no buttons are down and the right button goes down, a right down event is generated. While the right button is down the left button is then ignored. There is no such event as right tracking, nor is there such a thing as a right double click. There is room in the 4 bits available to support these, but they aren't implemented in modern OSes, so it doesn't seem necessary to implement them in C64 OS.

Mouse cursor and events planning notes
Notes on very handy graph paper about mouse events and sprites.

The only other thing I should say about C64 OS's support of the mouse is that it uses two sprites to get a hires two color (inner color with an outline) cursor. The cursor is bound to the edges of the screen, and the cursor uses an acceleration curve, although getting this "right" is something I'm still working on. I have been experimenting with other C64 OSes and programs that implement support for a mouse. And most are very primitive. The Geos/Wheels mouse driver is one of the best. It supports acceleration and the cursor is bound the edges of the screen. But it only uses one sprite which means the cursor has only one color in hires. Sometimes the cursor can get lost on the screen depending on what it is positioned above. GoDot, on the other hand, uses a two sprite cursor, but has no acceleration and is not bound to the edges of the screen. Menuette, a CMD utility written in 1993, is similar to GoDot.

Let me assure you, not being bound to the edges of the screen produces a terrible experience. Macintosh UX research discovered a long long time ago that it is far more efficient to hit targets that sit along screen edges if you can simply drive the mouse hard into a screen edge and click. This advantage is obliterated if the mouse goes off one side and onto the other side of the screen. But, I know why some routines do this. Because trapping the mouse to the bounds of the screen is remarkably challenging. An acceleration curve is also one of those things that you don't realize is important until you experience the difference. A 1351 is indeed a "proportional" input device. Which is great. It means if you move the mouse an inch slowly or quickly the computer still knows you've moved the mouse an inch. This allows you to move the cursor faster by moving your hand faster. This is a huge step up from using a Joystick to control the cursor. But what an acceleration curve does is even better. Try this on your PC or Mac. If you move the mouse one inch slowly, the cursor moves only an inch or so across the screen. But if you move the mouse still only one inch but you move it quickly, the on screen cursor will shoot across the screen. This added acceleration allows you to move the cursor to anywhere on the screen quickly and with minimal hand movement, but you don't give up fine pixel-precise control of the cursor when you need it.

All these plus the addition of modifier key combinations, I believe that C64 OS's mouse routines and mouse events are the closest thing to "modern" that any C64 OS or application has ever provided. This will be a great building block to stand on for the applications I want to write in the future for my C64. Most programs that roll their own mouse rouines end up with crappy half-baked mouse behavior, because going the extra mile to get the details right is time consuming.

Keyboard Command Events

Many programs on the C64 are capable of using key combinations as commands. However, I have yet to encounter a program or OS that supports them in a "modern" way. This is partially the result of the KERNAL's key scanning routine which abstracts away which physical keys were actually pressed and only buffers the resultant PETSCII character values. If your program does not support changing text colors on the fly, then using keyboard commands of Control + 1 through 9 is as easy as checking the regular keyboard buffer for the PETSCII color and Reverse codes that are silkscreened onto those keys. Similarly, if your program doesn't support arbitrary graphics symbols, then C= plus any of the regular letter keys will produce a PETSCII graphic character that you can notice in the buffer and interpret as a key command instead of an inputable/printable character. The best part of this for most programs is that it's really easy to support.

The downsides, however, quickly become evident if you notice at all how a modern OS is able to use key commands. The first problem is that it is impossible to have any key command that combines more than one modifier. You cannot use Control and Commodore key together with a letter on the keyboard to produce a unique command. The KERNAL simply doesn't map such combinations to anything uniquely identifiable. The control key automatically takes precedence. Another problem is that you cannot use Control with the regular letters of the keyboard, because the KERNAL maps these to various other PETSCII values. Control-S, for example, produces the PETSCII value, HOME. Control-Q is CRSR DOWN. Control-M is the same as RETURN. Using the KERNAL's built in key scanning, it is fundamentally impossible to tell the difference between Control-M and Return, therefore, you cannot ever use Control-M as a key command.

So, how are things being implemented in C64 OS to make this situation better? When no mouse buttons are held down, C64 OS scans the entire keyboard. It does this using the routines outlined by Craig Bruce, referred to above. This routine scans each row, one at a time and fills an 8-byte buffer which is equal to 64 bits. Each bit represents the up or down state of every key on the keyboard. (Exceptions noted above for Restore and Shift-Lock). Next, it explicitly checks to see if the Control or Commodore keys are held down. If they are, C64 OS deviates from Craig Bruce's routines somewhat. Bruce's routines are designed to be entirely transparent to the underlying program. It modifies the behavior of the KERNAL's keyscanning and buffering routines, but does not change the programmatic interface. Most programs will have no idea that Bruce's routines are even in use. Programs that make use of C64 OS's features, on the other hand, have to be written specifically to work with C64 OS.

In C64 OS, if the Control key or Commodore key are down, (and we're already beyond worrying about Mouse Events) it is going to produce a Key Command Event. Control and Commodore are not used to change the key-decoding table in the KERNAL, but the Shift Keys are. If a Shift Key is held down the shifted decode table will be indexed instead of the standard decode table. These tables are found in the KERNAL, and are used to map a 64-key matrix index to a PETSCII value. I opted to use the KERNAL tables because not every C64 has the same key layout. Some European keyboards have slight variations. Here are the two decode tables used by C64 OS:

standard keyboard table

.:EB81 14 0D 1D 88 85 86 87 11
.:EB89 33 57 41 34 5A 53 45 01
.:EB91 35 52 44 36 43 46 54 58
.:EB99 37 59 47 38 42 48 55 56
.:EBA1 39 49 4A 30 4D 4B 4F 4E
.:EBA9 2B 50 4C 2D 2E 3A 40 2C
.:EBB1 5C 2A 3B 13 01 3D 5E 2F
.:EBB9 31 5F 04 32 20 02 51 03
.:EBC1 FF

shifted keyboard table

.:EBC2 94 8D 9D 8C 89 8A 8B 91
.:EBCA 23 D7 C1 24 DA D3 C5 01
.:EBD2 25 D2 C4 26 C3 C6 D4 D8
.:EBDA 27 D9 C7 28 C2 C8 D5 D6
.:EBE2 29 C9 CA 30 CD CB CF CE
.:EBEA DB D0 CC DD 3E 5B BA 3C
.:EBF2 A9 C0 5D 93 01 3D DE 3F
.:EBFA 21 5F 04 22 A0 02 D1 83
.:EC02 FF

Craig Bruce's 3-Key rollover routines are used again to figure out which physical key ought to be interpreted first, and which keys should be ignored and when. The way it works is similar to how the mouse buttons are tracked and ignored, but it is slightly different and more complex because there are so many keys and people have lots of fingers. This key is then mapped to a PETSCII value via the selected decode table. However, the value is not merely put into the standard keyboard buffer. Instead it is used to generate a Key Command Event.

Key command events planning notes
Notes about key command events and mapping and translation.

A Key Command Event is 2 bytes and the buffer can hold up to 3 events. The first byte is used to hold the PETSCII value (limited of course only to the values listed in the two tables above), and the second byte is used for the modifier key flags. The modifier flags are very similar to the KERNAL's modifier flags byte. In fact, I use the same address in system memory, $028D. However, the KERNAL only decodes a single "Shift" flag, which is set with either Left or Right Shift Keys. The Key Command Event flags byte has 4 bit flags: Right Shift, Control, Commodore key and Left Shift.

Printable Keyboard Events

If the Commodore Key and Control Key are not held down, behavior mostly the same as the regular KERNAL keyboard buffering. If you simply type the letters "Hello" (including that shifted H), those PETSCII values will be put into the standard keyboard buffer. As mentioned above, I describe these as single byte printable key events. Separating these out as two different types of keyboard events has relevance when it comes to how events are distributed. But I will save that for a discussion in another post.

You will notice in the two tables above, if you compare them carefully, there are several places where the same code appears at the same place in both tables. $01, $02 and $04 are the map values for the Shift keys, the Commodore key and the Control key.1 But these are pulled out a head of time as modifier flags. Additionally, you will notice other matching codes: $5F, $30, $3D.

The C64c keyboard with special keys indicated

$5F is the back arrow, left most key of the top row, $30 is the 0 key and $3D is the = key. Each of these keys has no extra Shifted value printed on the key itself, as you can see in the image of a C64 keyboard, indicated with the red arrows. (The RVS OFF on the 0 key is only activated via the Control Key, which in C64 OS will produce a Key Command Event.) And so holding shift while pressing these keys produces the same PETSCII value as when not holding shift.

You will also notice, in the image indicated with blue arrows, that there are six keys which when shifted produce a highly non-standard PETSCII graphic symbol.2 Some extra logic is applied to remap these 6 shifted values to the most common ASCII values that are missing from PETSCII. Shifted @ and * become a pair of braces ({ }). They're side-by-side and near to the square brackets key pair. Shifted Up Arrow becomes a tilde (~), Shift £ becomes a pipe (|), Shift - becomes an underscore (_). This is very common and mnemonic. Shift + becomes a backtick (`).

Strangely, PETSCII has two Space characters and two carriage return characters. $20, $A0 and $0D, $8D respectively. The second of each just has bit 7 set. C64 OS remaps these latter values to the former values. For a full discussion on how characters will be mapped and rendered, see my earlier post, A Modern Character Set

After being some printable values are remapped, they are added to the standard keyboard buffer used by the KERNAL. There are some things to mention here. There are several cursor control characters that will still end up being put into the keyboard buffer. These include, CRSR DN, CRSR UP, CRSR RIGHT, CRSR LEFT, RETURN, DEL, INSERT, CLEAR, and HOME. These are indeed buffered as ordinary printable characters, and they will therefore be distributed as "events" via the same mechanism and to the same targets as the other printable characters.

Special Key Commmand Events

You will notice that the F1, F3, F5, F7 keys and their shifted versions F2, F4, F6 and F8 are found in the printable key maps without needing to press the Control or Commodore keys. That is because PETSCII actually has character values for 8 user defined functions. However, in C64 OS, these F-keys are queued in the Key Commands buffer, along with whatever modifier keys were held down with them. So, F1 alone will produce a Key Command Event with $85 as the PETSCII value byte and a Flags byte of $00. If you hold down the Commodore key and press F1 however, a Key Command Event is produced with a value of $85 and a Flags byte indicating the Commodore Key is held down. If you combine the 4 F-keys with L-Shift, R-Shift, Control and Commodore it is possible to produce up to 64 unique functions!

The Run and Stop PETSCII values are handled just like the F-Keys. Pressing the Stop key produces a Key Command Event. Pressing the key in combination with Control or the Commodore key allows for modified versions of the concept of Run or Stop. I'm not sure how that will be used, but it feels good to have that flexibility available.

About Cursor Control

There is one more area of keyboard control where the C64 seems foreign and strange to modern eyes. Instead of four cursor keys, there are only two. There is a shift key beside the cursor keys which fits three fingers nicely. To go right or down one simply pushes the two cursor keys alone. But to go up or left, you have to hold shift will pressing those same two cursor keys. Surprisingly the brain can get used to just about anything. After programming on my C64 all weekend, when I get back to work I spend all Monday fighting myself to use the "regular" inverted-T cursor keys on a Mac. There are also sound technical reasons why there are only two keys. The C64 keyboard has 66 physical keys. The Restore key is specially and independently wired to the C64's NMI line. And the Shift Lock key is a mechanical latch that is electrically the same has holding down the left shift key. That leaves 64 keys that are part of the matrix. There are 64 keys because the matrix is based upon the two 8-bit ports of the CIA1. 8 times 8 is 64. Without using a whole other chip, like the C128 does, the CIA's two ports can only map 64 physical keys. In order to get 4 independent cursor keys, decisions would have had to be made about which 2 other keys to sacrifice.

Using shift with the two cursor keys in itself is not as horrible as it seems. You do get used to it. The problem is that modern computers universally use the shift key in combination with the cursor keys to control text selection. From wherever the text cursor is, hold shift and press the left cursor a few times and a selection begins growing. This is unimaginably useful and efficient when combined with Key Commands for cut, copy and paste. But on the C64, because Shift is required to change the direction of the cursor keys it would seem impossible to get this behavior.

But, if you recall, the Left and Right Shift keys are fully independent in the keyboard matrix. What C64 OS does is decodes the Left and Right Shift keys differently, if the key value is one of the cursor keys. So, if you hold the Left Shift or the Right Shift with a letter key, you just get the Upper Case version of that letter as you would expect. And if you hold the Right Shift key with the Cursor Down key, you get Cursor Up again as usual, which is put into the Printable Keyboard Events buffer. However, if you hold Left Shift and press Cursor Down, rather than getting a Cursor Down Printable Key Event, it produces a Key Command Event with the Cursor Down value. If you hold Left Shift AND Right Shift and press Cursor Down, the Right Shift maps Cursor Down to the Cursor Up PETSCII Value and the Left Shift key causes that PETSCII Value to be put in a Key Command Event.

In other words, the cursor keys operate as normal only with the Shift Key that is immediately beside them. While the Left Shift key is treated completely independently to produce Key Command Events out of the four cursor directions. Other parts of C64 OS, for example standard one line and multi-line string editors will be able to interpret these Key Command Events to modify the text selection range.

Conclusion

That's it for C64 OS's Event system, for both Mouse and Keyboard handling. It is too much to fit into this post how Events are dispatched. But I will be writing about that in the not too distant future. This event model is, I think, one of the most complete of any C64 project. But it is still surprisingly lightweight. It is currently the largest linked component of the core operating system, but calling it large is an overstatement. It assembles down to just a few hundred bytes. Packing all of the above described functionality into a few hundred bytes would be utterly laughable on a big modern computer. But it feels powerful and tight on a C64.

  1. Note that these are the bit flags for those keys found in $028D. Although C64 OS does not explicitly use these values. Bruce's routine extracts them manually. []
  2. The PI symbol is only selected when in the Uppercase/Graphics character set. But since C64 OS uses the Uppercase/Lowercase set the printed symbol is actually the equally non-standard full hash. []

Do you like what you see?

You've just read one of my high-quality, long-form, weblog posts, for free! First, thank you for your interest, it makes producing this content feel worthwhile. I love to hear your input and feedback in the forums below. And I do my best to answer every question.

I'm creating C64 OS and documenting my progress along the way, to give something to you and contribute to the Commodore community. Please consider purchasing one of the items I am currently offering or making a small donation, to help me continue to bring you updates, in-depth technical discussions and programming reference. Your generous support is greatly appreciated.

Greg Naçu — C64OS.com

Want to support my hard work? Here's how!