GP2X Gp2x Demo Development


The only problem I'm stating with multiple functions is:

With a single function, you can easily find the start of that function.
With multiple functions, it's becomes more difficult as to what pieces of code to copy.

I don't know what rlyeh did in the end, but I did notice he had a lot of problems getting things to work correctly.
 
Squidge posted on Apr 21 2006 at 01:32 AM said:
The only problem I'm stating with multiple functions is:

With a single function, you can easily find the start of that function.
With multiple functions, it's becomes more difficult as to what pieces of code to copy.
Oh, I understand. You're right, that makes it a lot harder.

I compile the 940 executable as a separate file (but with the same normal gp2x compiler) which makes that problem much easier, you just have to get the entry point right.
 
Last edited by a moderator:
Episode 5, in which some noise gets made. Total size of demo.gpe without debugging: 4588 bytes. With debugging: 10028 bytes.

As a basic framework, the result from the the previous episode is reasonably complete -- that is, further progress depends on what artistic effect is being implemented. However, a decent demo has music and I haven't discussed audio at all yet, so it's time to look into it. Luckily, it's not too hard.

Following the spirit of the rest of this series, it would be nice to discard the operating system's interpretation of how audio should be done and just bash away at the hardware directly. Unfortunately that is not practical in this case. It turns out that the audio hardware has a VERY small buffer and it signals that it needs more data with an interrupt. Linux is handling that interrupt. There might be some way to extend the operating system to get it to call a custom interrupt handler instead of its own built in one, but that seems like a lot of work. So I'll just use the standard audio stuff.

The standard audio stuff involves opening a special device file, "/dev/dsp", configuring it with "ioctl" sytem calls, and then writing data to it. So the first thing to do is implement a new system call interface for "ioctl" (I already have open, close, and write from previous episodes). Then the code to get started looks like this:

Code:
int g_nFdDsp; // file handle to the audio device

int InitSound()
{
  int nRate, nBits, nStereo, nFrag;

  g_nFdDsp = OpenFile("/dev/dsp", 1); // 1 == O_WRONLY
  if(g_nFdDsp < 0)
	return 0;

  // set the audio parameters
  nRate = 44100;
  Ioctl3(g_nFdDsp, DSP_SPEED, (unsigned long) &nRate);

  nBits = 16;
  Ioctl3(g_nFdDsp, DSP_SETFMT, (unsigned long) &nBits);

  nStereo = 1;
  Ioctl3(g_nFdDsp, DSP_STEREO, (unsigned long) &nStereo);

  nFrag = 0x2000D; // double buffered, 4096 samples
  Ioctl3(g_nFdDsp, DSP_SETFRAGMENT, (unsigned long) &nFrag);
  bNB = 1;

  return 1;
}
Explanation: I'm not sure exactly what audio formats the dsp device on gp2x linux supports, but I know it supports this most common format: stereo 16-bit audio running at 44100 samples per second (CD quality). After opening the file for write access, I use the Ioctl3 call to feed it different parameters to get this set up. The DSP_SPEED, DSP_SETFMT, DSP_STEREO constants are taken from the appropriate include files that would be used if I wasn't trying to be so self-contained.

The DSP_SETFRAGMENT call needs more explanation. The lower 16 bits of the value represent the size of the buffer, in samples: 0x0D corresponds to 2^12, or 4096 samples for both buffers, making the total size of the buffers 16384 bytes. It's four bytes per sample because each individual sample is 16 bits and there are two of them because it's stereo. What buffer size to choose? It's a good idea (for reasons that will be clear later) to make sure that the buffer is at least twice as big as the frame rate of the demo. I find that 30 frames per second is a good frame rate to aim for on the gp2x -- faster than that doesn't add much because of the persistence of the LCD display, slower than that and it starts to look a little jerky (although 20 frames/sec is not horrible). 1/30 of a second is 1470 samples, so I pick the next highest available buffer size (times two). Two important thing to remember about the buffer format: the individual audio samples are signed shorts, and they are organized alternately for each side, that is Left, Right, Left, Right, ...

The upper 16 bits of the value represent the number of "buffers" that the audio device uses. By specifying a value of 2, that means "double buffered". In the double-buffered system, the dsp device will be writing data out from a single "current" buffer and will have a backup available to start writing from when the "current" buffer empties. If I try to write to /dev/dsp when the "current" buffer and the second buffer are both full or almost full, the call to write will BLOCK until the buffers drain enough to accept more data.

In a "normal" environment the blocking is not a problem, in fact it makes coding audio pretty easy, with a code snip that might look something like this:

Code:
short sBuf[4096];
while(1)
{
  FillAudioBuffer(sBuf);
  WriteFile(g_nFdDsp, sBuf, 8192);
}
The WriteFile function would just block until it actually needed data, and the double buffering makes sure that audio keeps playing while the next sound buffer is being computed. It's a terrible idea for the whole program to sit there waiting for the WriteFile call to return when the buffers have drained. So the normal idea is to create a thread that runs a loop which looks like that code above. Very simple, very tidy. Most or all of the libraries like SDL or rlyeh's minlib use a scheme just like that for their audio processing.

In this framework it's not so simple though, for two reasons: first (and most important), I don't have threads! The normal threading support is in libpthread.a, and since I'm not using any libraries, I'm out of luck. The second reason is more subtle -- even if I could use threads, I'm not sure I want to. With threads you can't really be sure when each thread will be run although they will run often enough to get the work done. The problem is that the FillAudioBuffer call might be doing its work right during the brief time that the VSYNC line goes low, and if this happens the main demo loop will miss it and wait for the next one. That will cause a tiny stutter in the video. If there is a significant amount of computation in FillAudioBuffer, it could happen frequently. There are ways around the problem -- the simplest of which is to have the loop wait not just for the VSync but also for a certain amount of time to pass.

But I don't have threads anyway, so I have to add the audio computations to the normal frame loop, so I can't have it blocking. There are a couple of ways to ensure that. First, I could try to make sure by reading the timer developed in episode 4 to make sure that an entire buffer has been drained so a WriteFile call will not block. Another approach is to make the /dev/dsp file nonblocking. That's the approach I'll use here. It just takes a couple more lines added to the above initialization code:
Code:
bNB = 1;
Ioctl3(g_nFdDsp, 0x5421, (unsigned long) &bNB); // 0x5421 == FIONBIO
Having this file be nonblocking changes the sound buffer concept by quite a bit. Now, suppose that I have a somewhat large buffer of sound (say 8192 samples, about 1/5 of a second). Every frame a function gets called that does this:

* Fill up whatever part of the buffer is currently empty
* Try to write the entire buffer to /dev/dsp
* Mark whatever amount actually got written (if anything) as being now empty.

I implement this as a circular buffer which is the perfect data structure to use for an application like this. You can refer to the code to see the details.

If I'm happy to just stuff zeros into the audio buffers in FillAudioBuffer(), I now have a complete audio system -- one that generates silence.

Silence isn't very fun though. Two approaches to audio for demos:

* Find, port, or write a music player. Most commonly, this would be a player for tracker music such as .mod, .s3m, or .xm. There is no such library currently available for the gp2x, so porting one would be a worthy task.

* Find, port, or write a "soft synth" which is a custom chunk of code that generates musical tones on the fly. This is the approach that interests me and since I can implement the very simple beginnings of such a thing without much code, I'll do that here.

In the file 'sound.c' I have the start of a little softsynth. The key things to keep track of are the events that form the "score" for the music (which note gets played when and what its parameters are, etc), and then a data structure that keeps track of each sound that is being played right now and how to generate the next sample for it. Then there has to be a mixer of some kind to get the sounds merged into one data stream. The audio produced by the executable for this demo is not the greatest music ever made obviously, but it illustrates the concepts. For the curious, the tones are generated using the "Karplus-Strong" algorithm which is described in great detail many places on the web.

As always, there are more details than can be explained easily in a few paragraphs so reading the code is recommended. The source code and gpe can be found at http://members.gamedev.net/dzz/demo5.zip Note that this is just sample code, intended to illustrate concepts. It's probably adaptable for use in your own projects, and feel free to do so, but there may be some work involved in making it live happily with the idiosyncracies of your personal codebase.

For further interesting reading about building the framework for a demo softsynth (for the PC instead of the gp2x but many of the concepts are the same), see:

http://kebby.org/articles.html

In the next episode, I will get the softsynth running on the second processor (the "940") to illustrate dual-processor use.

I'm not sure what topic should come after that, so if you have any suggestions (preferably specific to the gp2x), speak up.
 
Regarding copying a function's code: The method I've used (for a different purpose and on an x86 rather than ARM) was to place a dummy function call at the end of the function I wanted to copy. While copying it I'd watch out for the call and replace it with a ret when I found it. I haven't experimented with this on an ARM though so it may not be possible or as simple as it was for me.
 
Episode 6a. Exploring the second processor.

Originally I was planning to do "using the 940" as episode 6 in this series. However, since it's such an interesting area there's no way I can cover it in one article. So instead episode 6 will itself be a four part series:

Episode 6a: Getting code running on the second processor
Episode 6b: Practical issues: debugging and interprocessor communication
Episode 6c: Performance analysis
Episode 6d: Complete example

Although the situation is improving, there has been a lot of confusion about the second processor; I've seen it called a "video processor" for example. In reality, it is a complete normal Arm processor that is roughly equivalent to the "main" processor, with two exceptions: First, it has smaller caches (4k vs 16k). That's actually a huge difference and impacts the types of code we'd choose to run on that processor. Second, it does not have a virtual memory system -- it just uses physical memory addresses. It does have a "memory protection unit" but that isn't of much use to us.

In theory, the two processors share exactly the same memory. In practice, the first 32MB of memory is used by linux on the first processor and cannot be easily used by the second processor. That last sentence is not necessarily true depending on your definition of 'easily' and there a few ugly but fascinating tricks we might be able to use in this regard, but I will save that discussion for episode 6c.

Each processor has a special use for memory near address ZERO. Specifically, there is an interrupt table down there, and execution after a reset of the processor starts at location 0. The first processor has been set up during the machine's boot process (otherwise linux wouldn't be running at all). So if the physical addresses on the second processor had exactly the same meaning as the addresses on the first processor, the second processor would try to boot into linux as soon as you started it up! This is because it would start to execute from exactly the same code as the first processor. Although it may seem amusing to try it, it actually wouldn't get very far, primarily because of the differences in virtual memory. The point of this is that both processors should NOT have the same physical memory corresponding to "address 0".

Really what we would like to do for now is make the first 32MB of memory off-limits to the second processor and instead have address ZERO on the second processor be somewhere up in the upper 32MB of memory. Because of this obvious desire, there is a register field in the MMSP2 at register address 0x3B48 that we can set in order to do this. By setting the field to 2 we cause all addresses generated by the 940 to have the value 0x2000000 added to them, which basically moves the memory up to the upper 32MB. We could also just use the topmost 16MB by setting the field to 3 which causes 0x3000000 to be added to each address.

The following pictures illustrates the memory layout:
6a.png


So that's part of what we have to do to get the second processor going. Here's a list of EVERYTHING we have to do:

Pause the 940, to make sure it isn't running
Code:
// the low bit of register 0x0904 controls whether the 940 is running or not
g_pusRegs[0x0904>>1] &= (~(1<<0));
Put the 940 in a reset state. This is done using a bit in register 0x3B48, which is the same register used for the memory address shifting described above.
Code:
g_pusRegs[0x3B48>>1] = (1<<7) | 2; // (1<<7) for reset, 2 for the memory address shift
Copy the code that we want the 940 to use to the spot where it runs
More on that below
Make sure that interrupts connecting the two processors are disabled, since we won't use them
Code:
g_pusRegs[0x3B40>>1] = 0;
g_pusRegs[0x3B42>>1] = 0;
Take the second processor out of the 'reset' state
Code:
g_pusRegs[0x3B48>>1] = 2; // 2 for the memory address shift
Enable the second processor
Code:
g_pusRegs[0x0904>>1] |= (1<<0);

And that's all there is to it (heh)! Next, let's figure out what code we should copy to the second processor. As mentioned previously, the code near address ZERO has a special meaning. In particular, the first 8 32-bit memory locations are entry points for various interrupts, exceptions, etc, as follows:

Address 0x00: Reset vector
Address 0x04: Undefined Instruction vector
Address 0x08: Software Interrupt vector
Address 0x0C: Prefecth Abort vector
Address 0x10: Data Abort vector
Address 0x14: Reserved
Address 0x18: IRQ vector
Address 0x1C: FIQ vector

We don't really care much about these in particular, but the hardware does so we should should set it up properly.

There's no getting around it -- to do the first baby steps on the 940 we'll have to use assembly language. We won't have to use much assembly though. Here's the beginning of our assembly code to go at address zero on the 940:
Code:
  asm ("b .CodeEntryPoint"); // have all the entry points jump to the start of the code
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm (".CodeEntryPoint:");
We could have used other instructions there; like "ldr pc, ..." or just any old no-op for that matter. If we wanted to build a little mini-operating system for the 940 we might be interested in handling the different vectors in a more reasonable way. But we won't be doing anything THAT complicated so this will be fine.

Ok, now when the second processor is enabled and reset, we manage to get it running at our symbol .CodeEntryPoint. Before we go off doing astonishing audiovisual demo effects, there's still some more setup that needs to be done. In particular, we have to set up the memory ranges and initialize our program "stack".

Through a slightly complicated configuration process, we get to decide whether each part of the memory is cached or not, and we have to think carefully about that choice. If memory is cached, it is very fast to access memory that has been recently accessed because it gets loaded into a fast cache on the processor. However, if that memory gets changed it does not immediately get saved to the "main" memory. A simple and basically accurate rule is that any data we wish to communicate off the processor (video memory or variables shared with the other processor for example) should be non-cached and everything else should be cached.

The next step is to map out exactly how we will use the memory, so we know how to configure it. For our purposes here let's just say that the first 16MB of the memory as viewed by the 940 (the physical memory range 32MB-48MB) will be cached. That will include all of the code and normal variables, as well as the system stack which we'll put at 1MB -- 1 meg for code, normal program data, and stack should be plenty for anything we'd do on the 940. The 15 megabytes between 1MB and 16MB are available then for cached data -- maybe we'd write a malloc() system to use it or maybe we'd just divide it up in a fixed way depending on what our program might do.

Then the upper 16MB of memory as viewed by the 940 (physical memory range 48MB-64MB) will be uncached and used for communication with the first processor, and maybe for further data storage if needed.

Finally, we need to do a little more configuration of the chip. The following code takes care of all this. You can go do more research to understand exactly what each line is doing or just take my word for it. the 'mcr' instruction is used to configure "coprocessor 15". The ARM documentation gives more information, though maybe my comments will be of some use.
Code:
  asm (".CodeEntryPoint:");
  asm ("mov sp, #0x100000");	// set the stack top (1M)
  asm ("sub sp, sp, #4");	   // minus 4, just to be tidy

  // set up memory region 0 -- the whole 4GB address space, uncached
  // bit 0 = enable
  // bits 1-5: 5-bit region size (N).  Size = 2^(N+1).  For this partition, set all bits for maximum size
  // bits 12:31: base address.  Both regions in this example have a base address of zero

  asm ("mov r0, #0x3F");  // region data
  asm ("mcr p15, 0, r0, c6, c0, 0");  // data region 0
  asm ("mcr p15, 0, r0, c6, c0, 1");  // code region 0
  // set up region 1 which is the first 16 megabytes.
  // bit 0 = enable
  // bits 1-5: as above.  16 MB is 2^24 so N should be 23.  In hex, 23 is 0x17.
  // (0x17<<1) | 1 = 0x2F
  asm ("mov r0, #0x2F");  // region data
  asm ("mcr p15, 0, r0, c6, c1, 0"); // data region 1
  asm ("mcr p15, 0, r0, c6, c1, 1"); // code region 1
  // set region 1 to be cacheable (so the first 16M will be cacheable)
  asm ("mov r0, #2"); // 2 means region 1 (1 would have meant region 0, 4 would mean region 2, etc)
  asm ("mcr p15, 0, r0, c2, c0, 0"); // turn on data cache
  asm ("mcr p15, 0, r0, c2, c0, 1"); // turn on instruction cache
  // set region 1 to be bufferable too (only data)
  asm ("mcr p15, 0, r0, c3, c0, 0"); // turn on write buffer
  // set protection on for all regions
  asm ("mov r0, #15"); // more than we actually need
  asm ("mcr p15, 0, r0, c5, c0, 0"); // set full data permission
  asm ("mcr p15, 0, r0, c5, c0, 1"); // set full code permission

  asm ("mrc p15, 0, r0, c1, c0, 0"); // fetch current control reg
  asm ("orr r0, r0, #1"); // 0x00000001: enable protection unit
  asm ("orr r0, r0, #4"); // 0x00000004: enable D cache
  asm ("orr r0, r0, #0x1000"); // 0x00001000: enable I cache
  asm ("orr r0, r0, #0xC0000000"); //  0xC0000000: async+fastbus
  asm ("mcr p15, 0, r0, c1, c0, 0"); // set control reg
  // ok, now we're ready to rock

The last thing we need to work out for this article is how to build the program that will run on the 940. There are many ways to do this. We could compile the relevant code as part of our 920 program then copy the relevant parts to the 940, for example. We could get a "generic" Arm compiler and use that to make the executable (I am told that the compiler for the GP32 can be used in this way). The approach I will use here uses the normal gcc for the gp2x from the standard devkit available on the wiki. Just like the rest of the demo code from this series. In fact there isn't a lot of difference in the way it's used -- for this series on demo development we're not using any of the libraries so most of the issues that could arise aren't important. There's really only two issues that we have to deal with.

Issue 1: making sure that the correct code gets put at memory location zero, so it gets run when the processor is reset. Let's make a complete source file for what we have so far, which is just a little stub (saved in code940.c):
Code:
void code940(void)
{
  asm ("b .CodeEntryPoint"); // have all the entry points jump to the start of the code
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm (".CodeEntryPoint:");
  asm ("mov sp, #0x100000");	// set the stack top (1M)
  asm ("sub sp, sp, #4");	   // minus 4, just to be tidy

  // set up memory region 0 -- the whole 4GB address space, uncached
  // bit 0 = enable
  // bits 1-5: 5-bit region size (N).  Size = 2^(N+1).  For this partition, set all bits for maximum size
  // bits 12:31: base address.  Both regions in this example have a base address of zero

  asm ("mov r0, #0x3F");  // region data
  asm ("mcr p15, 0, r0, c6, c0, 0");  // data region 0
  asm ("mcr p15, 0, r0, c6, c0, 1");  // code region 0
  // set up region 1 which is the first 16 megabytes.
  // bit 0 = enable
  // bits 1-5: as above.  16 MB is 2^24 so N should be 23.  In hex, 23 is 0x17.
  // (0x17<<1) | 1 = 0x2F
  asm ("mov r0, #0x2F");  // region data
  asm ("mcr p15, 0, r0, c6, c1, 0"); // data region 1
  asm ("mcr p15, 0, r0, c6, c1, 1"); // code region 1
  // set region 1 to be cacheable (so the first 16M will be cacheable)
  asm ("mov r0, #2"); // 2 means region 1 (1 would have meant region 0, 4 would mean region 2, etc)
  asm ("mcr p15, 0, r0, c2, c0, 0"); // turn on data cache
  asm ("mcr p15, 0, r0, c2, c0, 1"); // turn on instruction cache
  // set region 1 to be bufferable too (only data)
  asm ("mcr p15, 0, r0, c3, c0, 0"); // turn on write buffer
  // set protection on for all regions
  asm ("mov r0, #15"); // more than we actually need
  asm ("mcr p15, 0, r0, c5, c0, 0"); // set full data permission
  asm ("mcr p15, 0, r0, c5, c0, 1"); // set full code permission

  asm ("mrc p15, 0, r0, c1, c0, 0"); // fetch current control reg
  asm ("orr r0, r0, #1"); // 0x00000001: enable protection unit
  asm ("orr r0, r0, #4"); // 0x00000004: enable D cache
  asm ("orr r0, r0, #0x1000"); // 0x00001000: enable I cache
  asm ("orr r0, r0, #0xC0000000"); //  0xC0000000: async+fastbus
  asm ("mcr p15, 0, r0, c1, c0, 0"); // set control reg
  // ok, now we're ready to rock
}
I'll compile this with:
...gcc -O0 -c code940.c

I stick in the -O0 to turn off optimization, just to make sure that the assembly we so carefully constructed doesn't get mangled by some well-meaning optimizer. This makes our code940.o file.

Now we need to make that .o into an executable, so we try to do it with:

...ld code940.o -o code940.gpe

This produces a troublesome looking warning:
ld: warning: cannot find entry symbol _start; defaulting to 00008074

The loader wants to start the program at the symbol _start, but we don't have one! The answer is to tell it
to use our "code940" function as the entry point:

...ld -e code940 code940.o -o code940.gpe

It works!

But should we trust it? let's see what the assembly code actually generated by gcc is, by running
...gcc -O0 -S code940.c

Here's the beginning of the resulting code940.s:
Code:
  .file  "code940.c"
  .text
  .align  2
  .global  code940
  .type  code940, %function
code940:
  @ args = 0, pretend = 0, frame = 0
  @ frame_needed = 1, uses_anonymous_args = 0
  mov  ip, sp
  stmfd  sp!, {fp, ip, lr, pc}
  sub  fp, ip, #4
#APP
  b .CodeEntryPoint
  b .CodeEntryPoint
  b .CodeEntryPoint
  b .CodeEntryPoint
  b .CodeEntryPoint
  b .CodeEntryPoint
  b .CodeEntryPoint
  b .CodeEntryPoint
  .CodeEntryPoint:
Hey, wait a minute! The compiler stuck in three instructions before the code we so carefully wrote:
mov ip, sp
stmfd sp!, {fp, ip, lr, pc}
sub fp, ip, #4

It did this because code940 is a C function. Luckily, we can tell the compiler to not do that, and here is the final source file after that:
Code:
void code940(void) __attribute__((naked));
void code940(void)
{
  asm ("b .CodeEntryPoint"); // have all the entry points jump to the start of the code
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm ("b .CodeEntryPoint");
  asm (".CodeEntryPoint:");
  asm ("mov sp, #0x100000");	// set the stack top (1M)
  asm ("sub sp, sp, #4");	   // minus 4, just to be tidy

  // set up memory region 0 -- the whole 4GB address space, uncached
  // bit 0 = enable
  // bits 1-5: 5-bit region size (N).  Size = 2^(N+1).  For this partition, set all bits for maximum size
  // bits 12:31: base address.  Both regions in this example have a base address of zero

  asm ("mov r0, #0x3F");  // region data
  asm ("mcr p15, 0, r0, c6, c0, 0");  // data region 0
  asm ("mcr p15, 0, r0, c6, c0, 1");  // code region 0
  // set up region 1 which is the first 16 megabytes.
  // bit 0 = enable
  // bits 1-5: as above.  16 MB is 2^24 so N should be 23.  In hex, 23 is 0x17.
  // (0x17<<1) | 1 = 0x2F
  asm ("mov r0, #0x2F");  // region data
  asm ("mcr p15, 0, r0, c6, c1, 0"); // data region 1
  asm ("mcr p15, 0, r0, c6, c1, 1"); // code region 1
  // set region 1 to be cacheable (so the first 16M will be cacheable)
  asm ("mov r0, #2"); // 2 means region 1 (1 would have meant region 0, 4 would mean region 2, etc)
  asm ("mcr p15, 0, r0, c2, c0, 0"); // turn on data cache
  asm ("mcr p15, 0, r0, c2, c0, 1"); // turn on instruction cache
  // set region 1 to be bufferable too (only data)
  asm ("mcr p15, 0, r0, c3, c0, 0"); // turn on write buffer
  // set protection on for all regions
  asm ("mov r0, #15"); // more than we actually need
  asm ("mcr p15, 0, r0, c5, c0, 0"); // set full data permission
  asm ("mcr p15, 0, r0, c5, c0, 1"); // set full code permission

  asm ("mrc p15, 0, r0, c1, c0, 0"); // fetch current control reg
  asm ("orr r0, r0, #1"); // 0x00000001: enable protection unit
  asm ("orr r0, r0, #4"); // 0x00000004: enable D cache
  asm ("orr r0, r0, #0x1000"); // 0x00001000: enable I cache
  asm ("orr r0, r0, #0xC0000000"); //  0xC0000000: async+fastbus
  asm ("mcr p15, 0, r0, c1, c0, 0"); // set control reg
  // ok, now we're ready to rock
}
Give it a try and look at the code940.s produced to verify that it is really what we want

Ok, after the 'ld' command with this new source file, we have a 1064 byte code940.gpe file. Let's peek inside:
...objdump -s code940.gpe

That gives us this:
Code:
code940.gpe:	 file format elf32-littlearm

Contents of section .text:
 8074 060000ea 050000ea 040000ea 030000ea  ................
 8084 020000ea 010000ea 000000ea ffffffea  ................
 8094 01d6a0e3 04d04de2 3f00a0e3 100f06ee  ......M.?.......
 80a4 300f06ee 2f00a0e3 110f06ee 310f06ee  0.../.......1...
 80b4 0200a0e3 100f02ee 300f02ee 100f03ee  ........0.......
 80c4 0f00a0e3 100f05ee 300f05ee 100f11ee  ........0.......
 80d4 010080e3 040080e3 010a80e3 030180e3  ................
 80e4 100f01ee							 ....
Contents of section .comment:
 0000 00474343 3a202847 4e552920 342e302e  .GCC: (GNU) 4.0.
 0010 3200								 2.
Looking at the .text section, you can see the pattern of branches at the beginning. BUT: oh no! The
text section starts at address 8074! That's not what we want! We want it at address 0! So once
again we have to modify the 'ld' command:

Code:
  ...ld -e code940 -Ttext 0x0 code940.o -o code940.gpe

Now the result of objdump is:

Code:
code940.gpe:	 file format elf32-littlearm

Contents of section .text:
 0000 060000ea 050000ea 040000ea 030000ea  ................
 0010 020000ea 010000ea 000000ea ffffffea  ................
 0020 01d6a0e3 04d04de2 3f00a0e3 100f06ee  ......M.?.......
 0030 300f06ee 2f00a0e3 110f06ee 310f06ee  0.../.......1...
 0040 0200a0e3 100f02ee 300f02ee 100f03ee  ........0.......
 0050 0f00a0e3 100f05ee 300f05ee 100f11ee  ........0.......
 0060 010080e3 040080e3 010a80e3 030180e3  ................
 0070 100f01ee							 ....
Contents of section .comment:
 0000 00474343 3a202847 4e552920 342e302e  .GCC: (GNU) 4.0.
 0010 3200								 2.

That looks good!

Issue 2: making sure that the executable file is in the correct format. By default, the gnu tools for the gp2x generate executables in the 'elf' format -- and a good thing too, because otherwise linux wouldn't know how to load them! But we don't want the elf format for our 940 program, we just want raw instructions.

Luckily, the tool suite contains a program called 'objcopy' which can easily convert the elf format to a raw binary. Just add the following to the makefile after the link line:
...objcopy -O binary code940.gpe code940.bin

Looking at the 116-byte result file, code940.bin, in a hex editor, we see:

Code:
060000ea 050000ea 040000ea 030000ea
020000ea 010000ea 000000ea ffffffea
01d6a0e3 04d04de2 3f00a0e3 100f06ee
300f06ee 2f00a0e3 110f06ee 310f06ee
0200a0e3 100f02ee 300f02ee 100f03ee
0f00a0e3 100f05ee 300f05ee 100f11ee
010080e3 040080e3 010a80e3 030180e3
100f01ee

So the resulting file, code940.bin, is exactly the data that we need to copy over to the second processor. We could read this secondary file off of the SD card and put it there, or we could compile the file directly into our main .gpe executable.

In the next article, I'll take care of that last detail and discuss how the demo program on the main processor can coordinate with the program on the second processor to get some real work done. There's no sample program with part 6a because we're not quite ready to do anything yet, but by the end of 6b we'll have sample code that works. Until then, everybody get to work on your demos for the competition!

If I've made any errors or explained anything poorly, please let me know. I'll also be happy to answer any questions on related topics if I know the answers.
 
Nicely explained article. Just a couple of things:

If you don't want the compiler to optimize your carefully crafted assembler, use "asm volatile" instead of just "asm".

For the amount of assembler your doing, placing more than a single line of assembler may make it a little more tidy and readable.

Eg:

asm volatile (
"b .CodeEntryPoint @ have all the entry points jump to the start of the code \n"
"b .CodeEntryPoint \n"
"b .CodeEntryPoint \n"
"b .CodeEntryPoint \n"
"b .CodeEntryPoint \n"
"b .CodeEntryPoint \n"
"b .CodeEntryPoint \n"
"b .CodeEntryPoint \n"
".CodeEntryPoint: \n");

(with the lines under "asm volatile" carefully tabbed maybe...)

You can put comments into the assembler using the '@' symbol.
 
I knew some basics of using the second processor, and I have just being reading and trying to understand rlyeh's dualcore functions, but rlyeh makes some of the hardest to read code I have ever seen. This article explained a lot more to me in five minutes than studying rlyeh's code for a whole night. I have plans for the 940T, and this article will help those plans get realised.
 
Just a note for the reputation of our beloved 940T : the fact that it has fewer cache than the 920 is because it has no MMU.

The MMU of the 920 needs a big cache so that access to page tables can be fast enough. Without page tables to fetch from memory, the need for a big cache is reduced.

So this is not *that* bad.

Anyway, thank you again for all your work.
 
If the region from 32MB-48MB is set as cached on the 940T, what would be the effect of the 920T writing to that memory? Or must all memory shared between the two CPUs take place in non-cached memory?

What I'm planning is to set up two buffers in memory accessible by both CPUs. The 920T will be writing to one buffer while the 940T is reading from the other buffer. When the 920T has finished writing to its buffer and the 940T has finished reading from its buffer, they swap over so the 920T is now writing to the second buffer and the 940T is now reading from the first buffer. The 920T never reads from the buffers and the 940T never writes to the buffers. Would this be able to be achieved in the 940T's cached memory or would it still have to take place in the non-cached memory?

Is this memory layout originally posted by Squidge still accurate as far as we know? Should we restrict ourselves to the memory area between 0x033A6800 (after the secondary frame buffer) and 0x03D00000 (start of the MPEG H/W decoder internal buffers)? That is approximately 9MB of memory that is apparently unused that would be uncached to both CPUs in this setup.
 
slygamer posted on May 8 2006 at 06:03 AM said:
If the region from 32MB-48MB is set as cached on the 940T, what would be the effect of the 920T writing to that memory? Or must all memory shared between the two CPUs take place in non-cached memory?
It is uncached on the 920, so the writes would go directly to physical memory. If that memory is in the cache on the 940, the 940 would see the old value if it tries to read it and would see the old value until the memory address is no longer in the cache.
What I'm planning is to set up two buffers in memory accessible by both CPUs. The 920T will be writing to one buffer while the 940T is reading from the other buffer. When the 920T has finished writing to its buffer and the 940T has finished reading from its buffer, they swap over so the 920T is now writing to the second buffer and the 940T is now reading from the first buffer. The 920T never reads from the buffers and the 940T never writes to the buffers. Would this be able to be achieved in the 940T's cached memory or would it still have to take place in the non-cached memory?
It should be okay if you "flush the cache" any time the cache could contain incorrect data, which could conceivably happen when you swap buffers. If the buffers are large it won't happen beause the cache will be filled from the other buffer. You don't mention that the 940 ever writes anything but I assume it must. That activity could also require attention if the data written by the 940 is to a cached region.
Is this memory layout originally posted by Squidge still accurate as far as we know? Should we restrict ourselves to the memory area between 0x033A6800 (after the secondary frame buffer) and 0x03D00000 (start of the MPEG H/W decoder internal buffers)? That is approximately 9MB of memory that is apparently unused that would be uncached to both CPUs in this setup.
I'm not sure that we know every memory range that's used by the kernel, and which parts of it we "should" use is debatable. I would say that restricting yourself to those 9 MB is a pretty safe approach. If for some reason you need more than 9MB it could get a little trickier.



Squidge posted on May 7 2006 at 04:56 PM said:
Nicely explained article. Just a couple of things:
Thanks Squidge, excellent and helpful comments as always!


rixed posted on May 8 2006 at 04:28 AM said:
The MMU of the 920 needs a big cache so that access to page tables can be fast enough. Without page tables to fetch from memory, the need for a big cache is reduced.
Thanks for the insight! I'm just starting to study the MMU in detail.

Do you (or anybody else) happen to know the page size on the gp2x?
 
Last edited by a moderator:
Dzz posted on May 8 2006 at 10:53 PM said:
It should be okay if you "flush the cache" any time the cache could contain incorrect data, which could conceivably happen when you swap buffers. If the buffers are large it won't happen beause the cache will be filled from the other buffer. You don't mention that the 940 ever writes anything but I assume it must. That activity could also require attention if the data written by the 940 is to a cached region.
I think I will stick to the non-cached memory for areas accessed by both CPUs. It sounds like trouble waiting to happen if the cached area is used.
 
Last edited by a moderator:
Dzz: The page size can be 1MB (section), 64KB, 4KB (coarse) or 1KB (tiny) depending on how the MMU is configured. I believe Linux itself uses 4KB normally.

Slygamer: That would be the easiest way to do it, yes. Otherwise you would have to flush to ensure correct read results on the 940, and drain (or flush) when finished to ensure the 920 gets the correct data. You can always get stuff working first, and then try to implement caching later on.
 
Squidge posted on May 8 2006 at 08:37 AM said:
I believe Linux itself uses 4KB normally.
Thanks! If any of you system gurus disagrees please let us know, otherwise I'm going to assume that 4k pages is the answer.
 
Last edited by a moderator:
In the latest linux source code release, if you goto \arch\arm\mm\proc-arm920.S you'll find this:

Code:
/*
 * and the page size
 */
#define PAGESIZE	4096

Which is where I got my answer.

Although I think you can also call getpagesize() which should give you the (same?) answer.
 
Can the 940T write to the hardware registers? For example, could the 940T use the hardware blitter? If so, would we have to offset the hardware registers by the 940T's base address like we do for memory access?
 
The code associated with episode 4 of this series is a reasonably complete framework that can serve as the basis for developing demos on the gp2x. However, there are a few issues with the code, so I took a little time to make a few changes to that framework. In particular:

* There was a report about USB not functioning properly after running the demo. Upon investigation, this turned out to be because of the way I was restarting the menu program -- I was not properly giving that program its environment variables so it could not find the executables it needed to run. This has been fixed.

* In version 2.0 of the firmware, the VSync signal changed polarity. The code now figures out which polarity to use.

* The division routine included in the library had truly horrible performance. That code has been replaced with a division routine with better performance. The division code is not heavily tested but seems to work properly.

To get this latest version of the demo development framework, go to:

http://gp2xgamer.com/framework.zip

Thanks! Expect episode 6b of the series in the next week or so.
 
Dzz posted on May 12 2006 at 05:39 AM said:
The code associated with episode 4 of this series is a reasonably complete framework that can serve as the basis for developing demos on the gp2x. However, there are a few issues with the code, so I took a little time to make a few changes to that framework. In particular:

* There was a report about USB not functioning properly after running the demo. Upon investigation, this turned out to be because of the way I was restarting the menu program -- I was not properly giving that program its environment variables so it could not find the executables it needed to run. This has been fixed.

* In version 2.0 of the firmware, the VSync signal changed polarity. The code now figures out which polarity to use.

* The division routine included in the library had truly horrible performance. That code has been replaced with a division routine with better performance. The division code is not heavily tested but seems to work properly.

To get this latest version of the demo development framework, go to:

http://gp2xgamer.com/framework.zip

Thanks! Expect episode 6b of the series in the next week or so.

okay, then I gonna use this for part 4. As I don't have FW 2.0 I don't now if part 1-3 has similar issues to fix. Can somebody test that?
 
Last edited by a moderator:
synkro posted on May 12 2006 at 01:39 AM said:
okay, then I gonna use this for part 4. As I don't have FW 2.0 I don't now if part 1-3 has similar issues to fix. Can somebody test that?
The menu restarting mechanism was broken in all episodes. The Vsync appeard in episode 2 and never until now handled firmware 2.0 correctly.
 
Last edited by a moderator:
Dzz posted on May 12 2006 at 02:44 PM said:
synkro posted on May 12 2006 at 01:39 AM said:
okay, then I gonna use this for part 4. As I don't have FW 2.0 I don't now if part 1-3 has similar issues to fix. Can somebody test that?
The menu restarting mechanism was broken in all episodes. The Vsync appeard in episode 2 and never until now handled firmware 2.0 correctly.

ah fuck! thanks for the quick answer, hopefully I can revise the last parts and start at part 4.
 
Last edited by a moderator:
Back
Top