Foreword
In the previous post we managed to emulate a text mode C64 screen, together with a flashing cursor.In this post we will be adding keyboard emulation. Specifically, we will be sourcing keys from an Android on screen keyboard (aka soft keyboard) and not a physical keyboard attached to our Android device.
Trapping Key Events from an on Screen Keyboard
In order to emulate a keyboard within our C64 emulator, it is important that we know at any point in time the complete state of our keyboard, that is which keys are down and which keys up.In the Android world where it is more in common to use an On Screen keyboard than a psychical one, it is quite ironic that it is easier to trap up/down key events within an Android app from a physical keyboard than from an On screen keyboard.
Within Activities there exists the methods onKeyDown and onKeyUp that you can override to trap key events. These events, however, is only applicable for physical keyboards.
Another hiccup of an on screen keyboard is getting it to display. By default an on screen keyboard will only display if an editable control has focus. This is problematic in our situation, since we don't have editable controls on our screen.
Luckily the Android Framework provides us with a way out that allows us to create an explicit on screen keyboard by means of a KeyboardView. The details will become clear as we go along.
First, we need to add a KeyboardView to the layout of our FrontActivity:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="com.johan.emulator.activities.FrontActivity" tools:showIn="@layout/activity_front"> <com.johan.emulator.view.C64SurfaceView android:id="@+id/Video" android:layout_width="320px" android:layout_height="200px" /> <android.inputmethodservice.KeyboardView android:id="@+id/keyboardview" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:focusable="true" android:focusableInTouchMode="true" android:visibility="visible"/> </RelativeLayout>
This will add an explicit keyboard at the bottom of the FrontActivity screen, which will be visible when the FrontActivity launches.
A KeyboardView needs to be provided with a keyboard layout. This layout is specified within a xml file. The following is an example xml layout file for a single key keyboard:
<?xml version="1.0" encoding="utf-8"?> <Keyboard xmlns:android="http://schemas.android.com/apk/res/android" android:keyWidth="11%p" android:keyHeight="10%p" > <Row> <Key android:codes="49" android:keyLabel="1"/> </Row> </Keyboard>
Each layout consists out of a couple of rows and each row consists out of a couple of keys.
Each key element consists out of two main attributes. The first attribute is keyLabel which is the text to be displayed on the button. The second attribute is the codes property. This is the scan code to pass when that button is pressed or released. Usually the ASCII code of the label is passed through as the scan code.
We can actually make the scan code work for us. For the scan code we can pass the actual C64 scan code, which will simplify our live a lot. A list of C64 scan codes can found here.
Next, let us write some code to associate the layout with our KeyboardView. So, within onCreate of the FrontActivity, at the following code:
@Override protected void onCreate(Bundle savedInstanceState) { ... // Create the Keyboard Keyboard mKeyboard= new Keyboard(this,R.xml.kbd); // Lookup the KeyboardView KeyboardView mKeyboardView= (KeyboardView)findViewById(R.id.keyboardview); mKeyboardView.setKeyboard(mKeyboard); mKeyboardView.setPreviewEnabled(false); mKeyboardView.setOnKeyboardActionListener(new KeyboardView.OnKeyboardActionListener() { @Override public void onPress(int i) { } @Override public void onRelease(int i) { } @Override public void onKey(int i, int[] ints) { } @Override public void onText(CharSequence charSequence) { } @Override public void swipeLeft() { } @Override public void swipeRight() { } @Override public void swipeDown() { } @Override public void swipeUp() { } }); }
The mKeyBoard instance contains a parsed version of the xml layout file. We can then assosiate this layout with our KeyboardView by calling mKeyboardView.setKeyboard(mKeyboard).
We also need to add an event listener to our KeyboardView. Currently all events defined in the listener are currently empty. We will give these methods some meat in the next section. For now it is worthwhile to mention that the onPress and onRelease are our key methods that can trap events when a key is pressed or released.
This is how the finished keyboard will look like in our FrontActivity:
I used characters from the Unicode character set for the labels of Return, BackSpace and Shift. Alternatively, KeyboardView provides the option to specify an image as label for these buttons. I will however not be exploring this option.
You will also realise something interesting going on with the shift key. This is in effect a toggle key. If it is on the green light will lit. If not it will be off. So, our shift key is in actual fact a Shift Lock key.
This toggle functionality is built in with KeyBoardView. To make a button toggable you just need to add the isSticky attribute for the particular key in the xml layout file, as shown below:
<?xml version="1.0" encoding="utf-8"?> <Keyboard xmlns:android="http://schemas.android.com/apk/res/android" android:keyWidth="11%p" android:keyHeight="10%p" > ... <Row> <Key android:codes="15" android:keyLabel="↥" android:isSticky="true" android:keyEdgeFlags="left" /> <Key android:codes="12" android:keyLabel="Z" /> <Key android:codes="23" android:keyLabel="X" /> <Key android:codes="20" android:keyLabel="C" /> <Key android:codes="31" android:keyLabel="V" /> <Key android:codes="28" android:keyLabel="B" /> <Key android:codes="39" android:keyLabel="N" /> <Key android:codes="36" android:keyLabel="M" /> <Key android:codes="0" android:keyLabel="↤" /> </Row> <Row> <Key android:codes="60" android:keyLabel="SP" /> </Row> </Keyboard>
Integrating Keyboard with Emulator
Let us now integrate our On Screen Keyboard with our emulator.Firstly, within our FrontActivity, define a private variable for the C64 Keyboard Matrix. Here is the variable declaration, together with the initialisation:
... private ByteBuffer keyBoardMatrix; ... @Override protected void onCreate(Bundle savedInstanceState) { ... keyBoardMatrix = ByteBuffer.allocateDirect(8); ... }
So, the keyboard matrix is basically an 8 byte buffer. For more information on the ins and outs of the C64 keyboard, please have a look at the following post of JavaScript emulator series:
http://emufromscratch.blogspot.co.za/2016/06/part-13-implementing-workable-keyboard.html
We will also be using a ByteBuffer in the same way as we did in the previous post. In this way our native code can access the keyBoard matrix directly at any time without encountering JNI overhead when scanning the keyboard.
For completeness, let us cover the code for passing this ByteBuffer to the native code.
We start by defining the following native method stub within Emu6502:
public native void setKeyboardMatrix(ByteBuffer keyBoardMatrix);
And we implement this method within memory.c:
... jbyte* keyboardMatrix; ... void Java_com_johan_emulator_engine_Emu6502_setKeyboardMatrix(JNIEnv* pEnv, jobject pObj, jobject oBuf) { keyboardMatrix = (jbyte *) (*pEnv)->GetDirectBufferAddress(pEnv, oBuf); } ...
Obviously, this method needs to be invoked from our FrontActivity:
... @Override protected void onCreate(Bundle savedInstanceState) { ... keyBoardMatrix = ByteBuffer.allocateDirect(8); emuInstance.setKeyboardMatrix(keyBoardMatrix); ... }
Let us now implement the code for populating the Keyboard matrix:
@Override protected void onCreate(Bundle savedInstanceState) { ... List<Keyboard.Key> keys = mKeyboard.getKeys(); for (Keyboard.Key currentKey : keys) { if (currentKey.codes[0] == 15) shiftKey = currentKey; } ... mKeyboardView.setOnKeyboardActionListener(new KeyboardView.OnKeyboardActionListener() { @Override public void onPress(int i) { if (i == 15) return; System.out.println("Kode: "+i); byte col = (byte)(i & 7); col = (byte) (1 << col); byte row = (byte)(i & 0x38); row = (byte) (row >> 3); row = (byte)(7 - row); System.out.println("Row: "+row); System.out.println("Col: "+col); byte tempKey = keyBoardMatrix.get(row); tempKey = (byte)(tempKey | col); keyBoardMatrix.put(row, tempKey); } @Override public void onRelease(int i) { if (i == 15) return; byte col = (byte)(i & 7); col = (byte) (1 << col); byte row = (byte)(i & 0x38); row = (byte) (row >> 3); row = (byte)(7 - row); byte tempKey = keyBoardMatrix.get(row); tempKey = (byte)(tempKey & ~col); keyBoardMatrix.put(row, tempKey); } @Override public void onKey(int i, int[] ints) { if (i != 15) return; if (shiftKey.on) { byte tempKey = keyBoardMatrix.get(6); tempKey = (byte)(tempKey | (1 << 7)); keyBoardMatrix.put(6, tempKey); } else { byte tempKey = keyBoardMatrix.get(6); tempKey = (byte)(tempKey & ~ (1 << 7)); keyBoardMatrix.put(6, tempKey); } } ... });
The code is very similar as described in my JavaScript emulator post I mentioned earlier on. Basically what happens is that each bit within the 8-byte ByteBuffer represent a specific key. If a particular bit is set to one, it means that the associated key is down. Conversely, if a bit is zero, the associated key is up.
In the above code, the shift key gets special attention since it is a toggable key. On initialisation we first loop through the keys within the mKeyboard instance, looking for a key with scan code 15 (e.g. the C64 scan code for Shift). Once we have found this key, we assign it to the shiftKey private variable.
We don't do anything with this key within the OnPress and Onrelease methods. We do, however, handle this key within the OnKey method. Within the Onkey method method we check the on value of the shiftKey. If true, we set the shift key bit within the matrix to one. Zero otherwise.
Now it is time to move onto the keyboard related native code within memory.c. This is, luckily, just a handful of code:
jchar getKeyPortByte(int outPortBits) { int temp = 0; int i; for (i = 0; i < 8; i++) { if ((outPortBits & 0x80) == 0) temp = temp | keyboardMatrix[i]; outPortBits = outPortBits << 1; } return ~temp & 0xff; } jchar memory_read(int address) { if (address == 0xdc01) return getKeyPortByte(mainMem[0xdc00]); else return mainMem[address]; }
Let us have a look at the method getKeyPortByte. It accepts an parameter with name outPortBits. This is effectively the contents of memory location dc00 that falls within CIA#1. In its configuration within the C64, this location is responsible for energising selected rows on the keyboard matrix.
A row is energised by applying a logic zero to it. Each key in a row that is down, will pull the column in which it lives down.
Should you have a couple of rows pulled low and you have a couple of keys in these rows that is down for a specific column, the column will be pulled down. This sounds like a logical OR operation, but just with reversed polarity.
We make use of the logical OR property within the getKeyPortByte method. However, to make a logical OR work for us, we need to work with the assumption that active is a logic 1 instead of a logic 0. We do the inversion to active-when-low just before getKeyPortByte returns.
getKeyPortByte get used within memory_read when the requested address to read is dc01.
A quick Test Run
We end off this post with a quick test run.I have entered a quick Hello World program in C64 Basic and gave a run:
That's it for this post.
In Summary
In this post we implemented an emulated keyboard to our emulator using an On screen keyboard.In the next post we will be adding Tape emulation.
Till next time!
No comments:
Post a Comment