Foreword
Welcome back! It has been some time since my post.We ended off the previous post having our C64 Android emulator in a state where we actually could enter and run very basic BASIC programs.
Looking back, it is quite interesting to note with how little hardware emulation we could get away with to get our emulator in such a state. I mean, CIA adaptor #1 we only had to worry about the first two registers (e.g. 0 and 1) out of 16 registers for the purpose of the keyboard.
Strictly speaking we needed to also implement the timer emulation on CIA#1 since on the C64 a timer interrupt gets generated every 1/60 of a second under normal working conditions. But, even here we could get away with a hack of just blindly triggering an interrupt every 20000 emulated cycles, of course respecting the interrupt flag of the 6502. It was surprising that this hack even brought to live the BASIC variable TI$, which shows you the time of day, provided you have set it.
When implementing Tape emulation, we would unfortunately not be so lucky to get away with the minimum CIA emulation. The tape code in the Kernel ROM makes use a lot of the timer functionality in the CIA chip as well as setting and masking off interrupts.
My original goal for this post was to implement Tape emulation. However, this exercise proved to be quite a learning curve in the Android world, and I ended up only getting halfway with this goal, that is implementing full CIA timer functionality with associated interrupts.
So, what did my learning curve equation consists of? Well, first of all I had learn a new way of thinking regarding Object orientation in C. Admitted, I am quite spoiled with Object orientated languages like Java and JavaScript. So, for instance, if you need to implement timer A and timer B of CIA#1 having identical functionality, you can just think of class called Timer and create separate object instances for timer A and timer B.
To implement such kind of object orientation in C gets very tricky and one needs to pass struct-variables around containing associated state to methods. You also needs to give special attention to pointer referencing and dereferencing. More on this in coming sections in this post.
The final learning curve I had to face, was how to set up the environment to actually step through native code in Android. In this post I will also be talking a bit about this.
OK, lets get started!
Implementing Timers
In the long run, timers will play a critical roll within our emulator. Apart from implementing the physical CIA timers within our emulator, we will also need to implement timer like functionality for events that occur after a set number of clock cycles has passed, like emulating an interrupt on the flag1 line with tape emulation, or forcing the rendering of a raster line.It therefore makes sense to abstract the timer functionality to a level as high as possible. On a high level a timers should just be viewed as a list of counting down values. Ideally each time a cpu gets executed, the following should happen with each value in the count down timer list:
- Decrements timer value with the number of cycles of the cpu instruction just executed
- If the timer value is zero, call a call back event associated with the timer. It is also up to this event to decide whether a next count down cycle should be scheduled.
struct timer_struct { int remainingCycles; int started; void (*expiredevent) (struct timer_struct*); void (*interrupt) (); int stateParam1; int stateParam2; int stateParam3; }; void add_timer_to_list(struct timer_struct * timer);
First off all remainingCycles is the number of cpu cycles remaining before reaching zero. The member started indicates whether the timer is active and keep updating remainingCycles. expiredevent is a pointer to a function that should be called when remainingCycles have reached 0.
interrupt is also a pointer to a function and is called when it is required to invoke an interrupt associated with timer.
You will also see that in this structure there are members defined, stateParam1, stateParam2 and stateParam3. These members are for properties specific to the applicable timer and is not general amongst other timer types. This will become clear in a moment.
You will also see that I have defined a function prototype add_timer_to_list. This is just for convenience, so that each place where a timer instance is defined, can add itself to the list of timers to be processed.
add_timer_to_list is defined within cpu.c. Let us have a look at its implementation:
... struct timer_node { struct timer_struct * timer; struct timer_node * next; }; struct timer_node * timer_list_head; ... void add_timer_to_list(struct timer_struct * timer) { if (timer_list_head == NULL) { timer_list_head = malloc(sizeof(struct timer_node)); timer_list_head->timer = timer; timer_list_head->next = NULL; return; } else { struct timer_node * current = timer_list_head; struct timer_node * previous = NULL; while (current != NULL) { previous = current; current = current->next; } previous->next = malloc(sizeof(struct timer_node)); previous->next->timer = timer; previous->next->next = NULL; } } ...
This is plain standard c link list manipulation, so I will not go into detail on how the link list list is implemented. I can maybe just mention that within the link list we always work with a pointer to the timer_struct instance. This is just to ensure that all changes to timer properties happens in one place.
Let us now move on to the code where we process the time list:
... int currentCycles; ... int step() { int result = 0; opcode = memory_read(pc); currentCycles = instructionCycles[opcode]; remainingCycles -= currentCycles; ... } ... void processAlarms() { struct timer_node * current = timer_list_head; while (current != NULL) { if (current->timer->started == 1) { current->timer->remainingCycles = current->timer->remainingCycles - currentCycles; if (current->timer->remainingCycles < 0) { current->timer->remainingCycles = 0; current->timer->expiredevent(current->timer); } } current = current->next; } } int runBatch(int address) { remainingCycles = 20000; int lastResult = 0; while ((remainingCycles > 0) && (lastResult == 0)) { lastResult = step(); if (lastResult != 0) break; if ((address > 0) && (pc == address)) { lastResult = -1; break; } processAlarms(); memory_write(0xd012, (remainingCycles < 50) ? 0 : 1); } return lastResult; }
First of all you will notice that have created a global variable for the cycle count of the current instruction called currentCycles because it is used in a number of places.
Within the runBatch while loop we call process alarms for each executed CPU instruction. All the processing happens within processalarms.
In processAlarms we process every timer in the list. For every timer we basically do the following:
- Check if it is started
- If started, we decrease remainingCycles by the number of cycles of the current instruction.
- As soon as RemainingCycles reaches zero, we invoke the method pointed to by expiredevent.
As mentioned previously, it is up to the expiredevent method to reset the remainingCycle variable to a pre-defined value and/or to stop the timer if desired.
In processAlarms() you will notice the abundant use of the -> operator. For the uninformed, this is the deference operator. Take special note of current->timer being passed to the expired event. Although we invoke the deference operator, we are not passing a copy of the applicable timer struct to this method, but also a reference to it. This is because the current variable is of type timer_node, within which timer is defined as a pointer.
Let us now proceed to the physical implementation of a CIA timer. We do this within a new file called timer.c:
Let us now proceed to the physical implementation of a CIA timer. We do this within a new file called timer.c:
#include <alarm.h> void expired(struct timer_struct *tdev) { tdev->remainingCycles = tdev->stateParam2; if (tdev->stateParam1 == 0) //if not continuios tdev->started=0; } void set_timer_low(struct timer_struct *tdev , int lowValue) { tdev->stateParam2 = tdev->stateParam2 & (0xff << 8); tdev->stateParam2 = tdev->stateParam2 | lowValue; } void set_timer_high(struct timer_struct *tdev , int highValue) { tdev->stateParam2 = tdev->stateParam2 & 0xff; tdev->stateParam2 = tdev->stateParam2 | (highValue << 8); } int get_time_low(struct timer_struct *tdev) { int low = tdev->remainingCycles & 0xff; return low; } int get_control_reg(struct timer_struct *tdev) { int value = 0; if(tdev->started==1) value = value | 1; if(tdev->stateParam1==0) value = value | 8; return value; } void set_control_reg(struct timer_struct *tdev, int value) { tdev->started = value & 1; tdev->stateParam1 = ((value & 8) == 8) ? 0 : 1; if ((value & 16) != 0) tdev->remainingCycles = tdev->stateParam2; } int get_time_high(struct timer_struct *tdev) { int high = tdev->remainingCycles & (0xff<<8); high = high >> 8; return high; } struct timer_struct getTimerInstanceA() { struct timer_struct mytimer; mytimer.expiredevent = &expired; mytimer.remainingCycles = 0xffff; mytimer.started = 0; return mytimer; } struct timer_struct getTimerInstanceB() { struct timer_struct mytimer; mytimer.expiredevent = &expired; mytimer.remainingCycles = 0xffff; mytimer.started = 0; return mytimer; }
First of all, you will notice that in this file we have special use for the stateParam1 and stateParam2 member of timer_struct. stateParam2 we for storing the latch value that we use to reload when the timer has expired. stateParam1 we use to indicate whether the timer should automaitcally restart when expired.
To get a populated timer_struct you either use getTimerInstanceA or getTimerInstanceB. The default set up the timer as stopped and remainingCycle with a value of 0xffff. In the event of the timer expiring, expired() will be called. The rest of the methods is just helper methods that memory.c can call with to read/write with the expected CIA register format.
One other thing you will also realise is that we pass through a pointer to a timer_struct. This is just because C doesn't natively support objects.
Implementing Interrupts
Let us now implement interrupts. We implement this in a file called interrupts.c:int interrupt_mask; int interrupts_occured; void set_mask(int new_mask) { if ((new_mask & 0x80) == 0x80) { //set the following masks interrupt_mask = (interrupt_mask | new_mask) & 0x7f; } else { //clear the following masks new_mask = ~new_mask; interrupt_mask = interrupt_mask & new_mask; interrupt_mask = interrupt_mask & 0x7f; } } int read_interrupts_register() { int read_result = interrupts_occured; interrupts_occured = 0; int ir_msb = ((read_result & interrupt_mask) != 0) ? 0x80 : 0; return ir_msb | read_result; } int trigger_irq() { return (interrupts_occured & interrupt_mask); } void interrupt_timer_A() { interrupts_occured = interrupts_occured | 1; } void interrupt_timer_B() { interrupts_occured = interrupts_occured | 2; }
Two global variables of importance: interrupt_mask and interrupts_occured. Interrupt_mask masks indicate which interrupt should physically interrupt the 6502. interrupts_occured shows which interrupt has occured.
interrupt_timer_A and interrupt_timer_B is called respectively by timer A and timer B when a timer underflow has occured. The side effect of calling one of these methods is that the applicable bit in the interrupts_occured variable is set.
The set_mask method is set for the purpose for setting the interrupt mask as explained in the same way as in the CIA datasheet. The most significant bit of the new_mask parameter plays a very important role in setting the mask. If this bit is set, all the other bits that are set will enable the applicable interrupts. Likewise when the msb is 0, all bits that are set will disable the applicable interrupts.
The read_interrupts_register will be used by memory.c when receiving a request to read the status of the interrupts register.
Finally, trigger_irq will be called will be invoked by cpu.c to determine if an interrupt should be simulated.
There is couple of changes required within timer.c to accommodate interrupts:
void expired(struct timer_struct *tdev) { //__android_log_print(ANDROID_LOG_DEBUG, "expired", "expired"); tdev->remainingCycles = tdev->stateParam2; if (tdev->stateParam1 == 0) //if not continuios tdev->started=0; tdev->interrupt(); } ... struct timer_struct getTimerInstanceA() { struct timer_struct mytimer; mytimer.expiredevent = &expired; mytimer.remainingCycles = 0xffff; mytimer.started = 0; mytimer.interrupt = &interrupt_timer_A; return mytimer; } ... struct timer_struct getTimerInstanceB() { struct timer_struct mytimer; mytimer.expiredevent = &expired; mytimer.remainingCycles = 0xffff; mytimer.started = 0; mytimer.interrupt = &interrupt_timer_B; return mytimer; }
The following changes are required within cpu.c:
void process_interrupts() { if (interruptFlag == 1) return; if (trigger_irq() == 0) return; pushWord(pc); breakFlag = 0; Push(getStatusFlagsAsByte()); breakFlag = 1; interruptFlag = 1; int tempVal = memory_read(0xffff) * 256; tempVal = tempVal + memory_read(0xfffe); pc = tempVal; } int step() { ... process_interrupts(); ... }
Mapping to Memory Space
We now need to map the interrupt and timer functionality to memory. Here is the highlight of changes applied to memory.c:... struct timer_struct timerA; struct timer_struct timerB; ... jchar cia1_read(int address) { jchar result = 0; switch (address) { case 0xdc00: break; case 0xdc01: result = getKeyPortByte(mainMem[0xdc00]); break; case 0xdc02: break; case 0xdc03: break; case 0xdc04: //timer A low result = get_time_low(&timerA); break; case 0xdc05: //timer A high result = get_time_high(&timerA); break; case 0xdc06: //timer B low result = get_time_low(&timerB); break; case 0xdc07: //timer B high result = get_time_high(&timerB); break; case 0xdc0d: // interrupt control result = read_interrupts_register(); break; case 0xdc0e: // control reg a result = get_control_reg(&timerA); break; case 0xdc0f: // control reg b result = get_control_reg(&timerB); break; } return result; } void cia1_write(int address, int value) { switch (address) { case 0xdc00: mainMem[address] = value; break; case 0xdc01: mainMem[address] = value; break; case 0xdc02: break; case 0xdc03: break; case 0xdc04: //timer A low set_timer_low(&timerA, value); break; case 0xdc05: //timer A high set_timer_high(&timerA, value); break; case 0xdc06: //timer B low set_timer_low(&timerB, value); break; case 0xdc07: //timer B high set_timer_high(&timerB, value); break; case 0xdc0d: // interrupt control set_mask(value); break; case 0xdc0e: // control reg a set_control_reg(&timerA, value); break; case 0xdc0f: // control reg b set_control_reg(&timerB,value); break; } } jchar memory_read(int address) { if ((address >=0xdc00) & (address < 0xdc10)) return cia1_read(address); else return mainMem[address]; } void memory_write(int address, jchar value) { if (((address >= 0xa000) && (address < 0xc000)) | ((address >= 0xe000) && (address < 0x10000))) return; if ((address >=0xdc00) & (address < 0xdc10)) cia1_write(address, value); else mainMem[address] = value; } void Java_com_johan_emulator_engine_Emu6502_memoryInit(JNIEnv* pEnv, jobject pObj) { timerA = getTimerInstanceA(); add_timer_to_list(&timerA); timerB = getTimerInstanceB(); add_timer_to_list(&timerB); } ...
I have created two global timer_structs, timerA and timerB. These variables gets initialised in memoryInit, by calling getTimerInstanceA() and getTimerInstanceB() from timer.c. The same timer_struct instances gets send to cpu.c via add_timer_to_list.
Again note that we are passing a reference of the timer variables to add_timer_to_list with the help of the ampersand(&) operator. In this way we ensure that memory.c, timer.c and cpu.c works on the same set of timers.
The rest of the code is fairly self explanatory. When memory reads/write falls within the address range dc00-dc10, it gets diverted to cia1read/cia1write. For each of these methods there is a 16 selector switch statement to do the applicable thing.
Debugging Native Source in Android
When i took the changes I did in this post for a test run, the emulated screen showed up blank without the usual C64 welcome message.I didn't had the faintest clue into where to start looking for the bug, especially with so many new lines of code added that relates to pointers and structs, it sounded virtually impossible to track down the issue by just reading through code.
I needed a way to step through the native code. I new that the Android development environment provides you with a couple of ways of stepping through code, but just how to set it up is completely different ball game.
With a lot of of fiddling I eventually figured out how to step through Android native code. In this section I will try to explain how to do it.
Let us start by having a look at the Android native debug architecture.
For some time I thought there was something magical about the Android native debug architecture. However, after some suffering I discovered it was identical to the debug architecture for debugging an ordinary Linux application!
We all know that in order to single step through a native Linux application you will use a utility called GDB.
With GDB you can even debug an application that is running on another machine. When debugging remotely, however, you would need to first ensure that an instance of GDBSERVER is installed on the machine running the application you want to debug.
The instance of GDBSERVER will then attach to the running process. On the other machine you will use GDB to connect remotely to GDBSERVER.
So, how do you setup the GDBSERVER/GDB environment do you can debug a native Android application? Let us go through this process step by step.
When compiling the native code, it is important that you supply the debug option to ndk-build:
ndk-build NDK_DEBUG=1
It wouldn't harm if we do a quick refresher on how the process works where we build an apk file that includes a compiled library of our native code.
Under app/src/main, there lives the following folders:
assets
java
jni
jniLibs
res
All your native source code, that is .c files and header files, lives under jni.
When you want to compile your native code, you need to be inside the jni folder within a terminal. Within this folder you would then issue the following command:
ndk-build
This command would then create a folder libs on the same folder level as jni. Each time you rebuild, however, you should rename the libs folder to jniLibs.
By default, ndk-build generates binaries for a couple of platforms. You can see this when looking at the folders inside jniLibs:
arm64-v8a armeabi armeabi-v7a mips mips64 x86 x86_64
Under each of these folders you will find a binary or two for the relevant platform.
Now, when you run ndk-build with the option NDK_DEBUG=1, you will see an extra binary been within each of these folders, called gdbserver. There we go, the first similarity with Linux debugging!
What we basically will need to do, is to get this gdbserver binary to start up on the mobile device and attach to your running Android application. You can then use GDB that comes out with your version of Linux to remotely attach to gdbserver.
We will now dedicate a section for discussing GDBSERVER setup and a section for setting up GDB.
GDBSERVER setup
One thing you must do, before deploying your app to the mobile device for debugging, is to rename all gdbserver instances to gdbserver.so. If you don't do this, you might not see gdbserver on the mobile device, and therefore you would not be able to start it.With the renaming done, you can now deploy the app to your mobile device, which probably the easiest would be to start the app from Android Studio, and then stopping it.
Now, start the application on your mobile device. It is now important to get the process id of the running process. To get get this, open up a terminal on your PC and type the following:
adb shell
adb should automatically pick up the currently attached Android device that has a debug bridge enabled and start a remote shell connection to it. This shell behaves almost like a Linux console.
At the remote console specify ps. You will now get a list of processes running. You will spot your application by its name. The second column will is the process id of the process. Take a note of this number.
Our goal now is to start up gdbserver and attach to our running application.
Locating the gdbserver binary on the mobile device can be quite a challenge. What makes it even more of a challenge is that pre Android 4.2 devices saves gdbserver in a different location than Android 4.2 and above.
For this post, I am only going to cover Android 4.2 and above. On the remote console issue the command cd /data/app. Now do a ls. You will see one of the folders displayed will look similar to your application name as shown in the screenshot above. Change into this directory.
Now, within this folder change to the directory lib/{platform}, where {platform} is the applicable platform like arm or x86_64.
If you now do an ls, you should see the gdbserver binary. We are now ready for the next step. Issue the following command:
/gdbserver.so :5039 --attach 2997
Replace 2851 with the process id you previously noted.
If all goes well, your system should respond as follows:
Attached; pid = 2997 Listening on port 5039
Before we end off this section, we should just enable port forwarding between the mobile device to a port mapped locally on the host:
adb forward tcp:5039 tcp:5039ffff
GDB setup
Before we start GDB, we need a binary from the mobile device. So, on your pc opens up a new terminal and issue the following commands:
mkdir android
cd android
adb pull /system/bin/app_process
Now, within the same terminal window, issue the following command:
gdb ~/android/app_process
With GDB started, we connect to the remote gdb server:
(gdb) target remote:5039
Next, we should specify the path on our local drive conatining a compiled library of our source code with debug systems. When we ran ndk-build NDK_DEBUG=1, such a library was actually generated automatically. This lib gets generated in a folder called obj, which lives in the same directory level as jniLibs. In my case the absolute path I specified to GDB is as follows:
set solib-search-path /home/johan/cln/androidemuC64/app/src/main/obj/local/x86_64
Next up, you should specify a breakpoint like the following:
break cpu.c:357
Now press c and then <enter>. Your app shall now resume execution until a breakpoint is hit.
Normal GDB commands apply.
In Summary
In this post we have implemented variable timers and interrupts.We also had a sneak preview into how to debug native code in Android.
In the next post we will see if we can finish off with tape emelation.
Till next time!
No comments:
Post a Comment