GP2X Gp2x Demo Development

Discussion in 'General Discussions' started by Dzz, Apr 11, 2006.

  1. nickspoon

    nickspoon vultum stultum habes

    Joined:
    Nov 4, 2005
    Messages:
    4,234
    Location:
    Essex, UK
    Well, I've got a bitmap reading function, but it uses fopen(). I can't seem to change that into a readable file pointer.
     
  2. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    I'll have code for "reading from a file" in the next installment. There is already a system call in asmlib.s for opening a file, I just need to add another one to read from the file. These functions will match the open() and read() system calls rather than fopen and fread, but it should be easy to convert the code.
     
    Last edited by a moderator: Feb 4, 2016
  3. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    Episode 6b, in which we get some code running on the 940, having adventures along the way.

    In the last episode we looked at the things that must be done to get code running on the gp2x coprocessor (henceforth called the '940' or '940T'). This time, let's start off by getting that code organized into a real project.

    The goal of this project is to put all the header and setup assembly junk into a "main.c" file, then have a "code.c" file that has the code that does the actual work in it. At first, code.c just has this code in it:
    Code:
    void Do940Stuff()
    {
      // do something interesting here
      while(1)
      ; // do nothing forever!
    }
    
    We'd like to call that function from the main startup function which is now stored in main.c and is basically just what was developed in episode 6a:
    Code:
    extern void Do940Stuff();
    
    void code940(void) __attribute__((naked));
    void code940(void)
    {
      asm volatile (
    	"b .CodeEntryPoint @ have all the entry points jump to 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"
    	"mov sp, #0x100000		   @ set the stack top (1M)\n"
    	"sub sp, sp, #4			  @ minus 4, just to be tidy\n"
    	"@ set up memory region 0 -- the whole 4GB address space, uncached\n"
    	"@ bit 0 = enable\n"
    	"@ bits 1-5: 5-bit region size (N).  Size = 2^(N+1).  For this\n"
    	"@   partition, set all bits for maximum size\n"
    	"@ bits 12:31: base address.  Both regions in this example have a\n"
    	"@   base address of zero\n"
    	"mov r0, #0x3F			   @ region data\n"
    	"mcr p15, 0, r0, c6, c0, 0   @ data region 0\n"
    	"mcr p15, 0, r0, c6, c0, 1   @ code region 0\n"
    	"@ set up region 1 which is the first 16 megabytes.\n"
    	"@ bit 0 = enable\n"
    	"@ bits 1-5: as above.  16 MB is 2^24 so N is 23.  In hex, 23==0x17.\n"
    	"@ (0x17<<1) | 1 = 0x2F\n"
    	"mov r0, #0x2F			   @ region data\n"
    	"mcr p15, 0, r0, c6, c1, 0   @ data region 1\n"
    	"mcr p15, 0, r0, c6, c1, 1   @ code region 1\n"
    	"@ set region 1 to be cacheable (so the first 16M will be cacheable)\n"
    	"mov r0, #2   @ 2 means region 1 (1 would have meant region 0, 4 would mean region 2, etc)\n"
    	"mcr p15, 0, r0, c2, c0, 0   @ turn on data cache\n"
    	"mcr p15, 0, r0, c2, c0, 1   @ turn on instruction cache\n"
    	"@ set region 1 to be bufferable too (only data)\n"
    	"mcr p15, 0, r0, c3, c0, 0   @ turn on write buffer\n"
    	"@ set protection on for all regions\n"
    	"mov r0, #15				 @ more than we actually need\n"
    	"mcr p15, 0, r0, c5, c0, 0   @ set full data permission\n"
    	"mcr p15, 0, r0, c5, c0, 1   @ set full code permission\n"
    	"mrc p15, 0, r0, c1, c0, 0   @ fetch current control reg\n"
    	"orr r0, r0, #1			  @ 0x00000001: enable protection unit\n"
    	"orr r0, r0, #4			  @ 0x00000004: enable D cache\n"
    	"orr r0, r0, #0x1000		 @ 0x00001000: enable I cache\n"
    	"orr r0, r0, #0xC0000000	 @ 0xC0000000: async+fastbus\n"
    	"mcr p15, 0, r0, c1, c0, 0   @ set control reg\n"
    	// ok, now we're ready to rock
      );
      Do940Stuff();
    }
    
    We're going to store these files in a subdirectory called "940" just to keep the main directory from getting too cluttered. So after putting these .c files into the "940" directory all we need is a makefile:
    Code:
    CROSS_COMPILE = C:/devkitGP2X/bin/arm-linux-
    LDFLAGS = -static
    
    CXX = $(CROSS_COMPILE)gcc
    LD = $(CROSS_COMPILE)ld
    
    CXXFLAGS = -IC:/devkitGP2X/include -Wall -Werror
    LIBS = -Lc:/devkitGP2X/lib
    
    CODE940_TARGET = code940
    CODE940_OBJS = main.o code.o
    
    all : $(CODE940_TARGET)
    
    $(CODE940_TARGET) : $(CODE940_OBJS)
      $(LD) -e code940 -Ttext 0x0 $(CODE940_OBJS)  -o $(CODE940_TARGET)
      $(CROSS_COMPILE)objcopy -O binary code940 code940.bin
      ls -l code940.bin
      cp code940.bin ..
    
    main.o: main.c
      $(CXX) $(CXXFLAGS) -O0 -c main.c
    
    code.o: code.c
      $(CXX) $(CXXFLAGS) -O2 -c code.c
    
    This is really just putting the code from episode 6a into action. Building this copies the "code940.bin" into the main directory. It's 124 bytes in size.

    Next, it's time to get that code loaded and put to work. We'll start with the normal framework. At startup time we call Startup940:
    Code:
    int Startup940()
    {
      int fd;
      unsigned char buffer[1024];
      int nRead, nOffset;
    
      g_pusRegs[0x3B48>>1] = (1<<7) | 2; // (1<<7)==reset, 2 == memory address shift
      g_pusRegs[0x0904>>1] &= (~(1<<0)); // pause the 940
      
    // copy the data
    
      fd = OpenFile("code940.bin", 0); // 0 == O_RDONLY
      if(fd < 0)
    	return 0; // could not open the file
      nOffset = 0;
      while((nRead = ReadFile(fd, buffer, 1024)) > 0)
      {
    	dzzmemcpy(g_pNoncachedMemory + nOffset, buffer, nRead);
    	nOffset += nRead;
      }
      CloseFile(fd);
    
      // Make sure that interrupts connecting the two processors are
      // disabled, since we won't use them
      
      g_pusRegs[0x3B40>>1] = 0;
      g_pusRegs[0x3B42>>1] = 0;
    
      // Take the second processor out of the 'reset' state
      g_pusRegs[0x3B48>>1] = 2; // 2 for the memory address shift
      // Enable the second processor
      g_pusRegs[0x0904>>1] |= (1<<0);
    
      return 1; // the program is going
    }
    
    That's all pretty straightforward, just read in the code940.bin file, stick it in the memory starting at 32MB, and let the 940T go at it.

    Similarly, when the program is done, we'll shut down the 940:
    Code:
    void Shutdown940()
    {
      g_pusRegs[0x3B48>>1] = (1<<7) | 2; // reset the 940
      g_pusRegs[0x0904>>1] &= (~(1<<0)); // pause the 940
    }
    
    And that's it, our first complete program using the 940T. Well, it doesn't use it for MUCH!

    To do some useful work, the two processors will need to talk to each other. A convenient way to organize this is to define a structure that we'll put in a shared include file and use a particular memory location for it. In our code, let's call the structure type SharedData and put it in shared.h. Here's a simple shared.h:
    Code:
    typedef struct
    {
      int nData; // just a dummy value, we'll do something more useful later
    } SharedData;
    
    Now, let's say that we will store the shared data structure at physical address 0x3E00000 (62MB). There's nothing magic about this address, I just figured we would put it up toward the top of memory out of the way. In each both the main program and in the 940 code we'll declare:

    SharedData *g_pSharedData;

    In the main program we set it to be:

    g_pSharedData = (SharedData *) (g_pMappedTop32Megs + 0x1E00000);

    where g_pMappedTop32Megs is the return value from the MMap call used to get access to the top 32 megs of memory.

    In the 940 we initialize this by just setting the physical address MINUS the "offset" since the 940 code thinks that physical address 0x2000000 is address 0 (see article 6a for more info on the way the memory gets mapped):

    g_pSharedData = (SharedData *) 0x1E00000;

    Now we can add members to this structure at will and refer to them easily from both sides. Simple! This is, of course, only one way to do this, feel free to invent your own mechanism. It's just memory... as long as you keep clear on which addresses on one processor match up to which addresses on the other processor, you can do pretty much whatever you like.

    Now that we have a way for the processors to communicate, let's use it for something useful. One thing we will very likely want to do as we develop code for the 940 is communicate debugging data back to the main program. Let's build a simple little debugging facility that we can use for this purpose. The idea is that we'll be able to communicate strings back, and have them appear in debugging lines. We'll use a circular buffers for this, so let's just define the SharedData structure like this:
    Code:
    typedef struct
    {
      char DBuf[1024];	 // shared data
      int nNextWriteDebug; // next character to write to
      int nNextReadDebug;  // next character to read from
    } SharedData;
    
    Now we add functions to the 940 code to write data to this circular buffer (updating DBuf and nNextWriteDebug). The 920 code polls every frame to retrieve the data, updating nNextReadDebug. I won't bother putting the code for this in the article, check the source file if you want to see the details.

    There we have it, a program on the 940 communicating data back to the main program on the 920.

    There's just one last issue to deal with -- what if the 940 is writing data to the buffer at exactly the same time as the 920 is reading it. It's possible that confusion could arise. Whenever developing applications for multi-processor systems that share memory, access to that memory must be regulated so both processors dont' step all over the work that the other one is doing. We can do such regulation by building synchronization primitives. This is a very interesting area and whole books are written about it. Curious people should google terms like mutual exclusion, mutex, semaphore, process synchronization, etc.

    For our purposes we'll use a simple construct that is enough for most normal tasks -- we'll just lock the data structure. So we add a "lock" variable to the SharedData structure and the rule is simple: if we want access to the data structure we have to wait until the lock variable is zero. When that happens, we set its value to 1 to mark that we have locked the data. then we do our work and reset the lock to 0 when we are done. Simple idea!

    There's only one complication: the natural code to implement this might look like this:
    Code:
    while(g_pSharedData->nLock != 0)
    ; // do nothing... wait for the lock to be released
    g_pSharedData->nLock = 1;
    // do our work here
    g_pSharedData->nLock = 0;
    
    The problem is that both processors could enter this chunk of code at exactly the same time, so they would both see the nLock field as 0 and both go on assuming they have the lock! This is called a "race condition" and it is somewhat unlikely to happen since it's such a brief period of time, but when building systems like this it's important to be correct.

    In a multiprocessor system like the gp2x the answer requires support from the hardware, and the ARM has an instruction designed to help out: SWP. The SWP instruction swaps a register with a memory location and guarantees that that swapping action cannot be overlapped with the swapping action on the other processor. It does this by setting a particular circuit board line high while the operation is in progress and if the other processor wants to access the memory it has to wait for that very brief time.

    We can use this to build a locking mechanism that uses an integer variable as the lock. We'll store the lock itself in the shared data structure:
    Code:
    typedef struct
    {
      unsigned int uLock;  // lock to regulate access to shared data
      char DBuf[1024];	 // shared data
      int nNextWriteDebug; // next character to write to
      int nNextReadDebug;  // next character to read from
    } SharedData;
    
    The functions to actually do the locking and unlocking:
    Code:
    Lock:
      mov r2, #1		 @ value to set the lock to
    LockLoop:
      swp r3, r2, [r0]   @ swap 1 into the lock
      cmp r3, #1		 @ if a 1 was already there,
      beq LockLoop	   @   go back and try again
      mov pc, lr
    
    Unlock:
      mov r2, #0		@ set the lock to 0
      str r2, [r0]
      mov pc, lr
    
    The unlock code could be written in C but the lock needs to be in assembly because of the special SWP instruction.

    Refer to the code for further details.

    That wraps up this episode. As always the code is available at http://gp2xgamer.com/demo6b.zip

    This code forms a framework for putting both processors to work in a demo or other application. The next article in the series will explore some performance issues of using the two processors and the memory caches. I don't have a very clear idea yet of exactly what I want to do so it will likely take a while to write. If I don't come up with anything good, the article series could end here, unless other ideas for unlrelated topics arise.

    If I have made any errors or if my explanations are bad, please let me and the rest of us know about it.
     
  4. rixed

    rixed Member

    Joined:
    Dec 31, 2005
    Messages:
    206
    Location:
    Paris (fr)
    Thank for for another article, Dzz.

    Anyway, this time there's a little mistake I guess :
    you do not need any locking to share a cyclic buffer between a reader and a writer.
     
  5. Squidge

    Squidge Certified Guru

    Joined:
    Nov 16, 2003
    Messages:
    8,495
    Location:
    UK
    I agree, since nNextWriteDebug is only r/w by the 940, and nNextReadDebug is only r/w by the 920, the lock isn't strictly necessary. However, it will still work, and is still useful as an example of how to access shared data. Should someone take the code and expand upon it (which I think is the general idea with these articles), they will not get any unfortunate side-effects should they add data which is r/w by both sides as it's already covered.

    So I wouldn't say it was a mistake as such, more just unnecessary in the current context.
     
  6. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    Thanks, you guys are right. Maybe I should have made the debug facility use a linked list for the debug commands. That would have also illustrated another important rule: do not use pointers in the shared data!
    (because the memory addresses are different on the two processors)

    Anyway, as Squidge points out, there's no harm in pretending it's necessary in this case.
     
  7. twetmore

    twetmore Still Fresh

    Joined:
    May 23, 2006
    Messages:
    2
    Dzz,

    I would first like to echo the sentiments expressed by many others here. This is a really great service you are providing. Thank you.

    I am just getting started with the GP2X myself and you have so far answered many questions that were rattling around in my head already!

    With regards to the locking discussion here, and its necessity, I assumed a lock would be required to be able to use the debug buffer reliably.

    Looking over the code, I see two things I would like to mention:
    • Buffer overrun
      First, there is no guard to prevent the 940 from overwriting data not yet read by the 920. That is not a huge problem, I guess, but could lead to some corrupted debug data.
    • Terminating the read (920)
      The 920 read code actually does reference (reads) the write pointer (nNextWriteDebug), so it is quite possible for the 920 and 940 to conflict in the buffer.
    Based on these two comments, I would suggest continuing to use the lock as you have, to prevent the 920 and 940 from having trouble with the buffer. For example, if the 940 is updating the write pointer while the 920 is reading the buffer, the 920 may not terminate its read properly, possibly causing it to read corrupted data or loop infinitely in the worst case (ie. if the 940 writes data faster than the 920 is reading it).

    Of course, the 940 could still write data that the 920 hasn't read (since the 940 doesn't check the nNextReadDebug pointer), but I am not sure if that problem is worth solving or not, since it would require blocking the 940 until the 920 catches up with the data. I suppose it depends on the goal, whether or not you want robust debugging code at the cost of slowing the 940's performance.

    Since this is for debugging, the performance overhead of the lock should really not be a concern. And if you cannot rely on having good debugging data, I'm not sure how useful it would be. After all, the debug data is there to help you solve problems, not give you more problems to investigate! :)

    Thanks to all the other posters here as well, especially Squidge and synkro. Before I came to this forum, I started reading what I believe are the "archived" versions of these tutorials put together by synkro, so thanks for those efforts.

    I have had my GP2X for several months, but just now finally cleared my schedule enough to begin playing with it. I look forward to my exploration of the GP2X and these tutorials are just the thing I need to get off the ground. Keep up the great work!

    -Tony
     
    Last edited by a moderator: Feb 4, 2016
  8. rixed

    rixed Member

    Joined:
    Dec 31, 2005
    Messages:
    206
    Location:
    Paris (fr)
    Not if the write of the pointer is atomic, which is the case here (single bus operation).

    Of course, the end pointer must be updated (by the writer) once the new datas have been fully written.

    Similarly, the begin pointer must be updated (by the reader) once all datas was read.
     
    Last edited by a moderator: Feb 4, 2016
  9. twetmore

    twetmore Still Fresh

    Joined:
    May 23, 2006
    Messages:
    2
    rixed,

    Ah, I think I see. By updating the end pointer after updating the buffer, the 920 would be prevented from reading the new data in the buffer.

    The remaining problem (or design decision) is with the 940 writing "past" the begin pointer. This "problem" is separate from the locking issue. Since the 940 does not examine the begin (read) pointer, you could end up with something like:

    Code:
    START				 Buffer: _ _ _ _ _
    							  E
    							  B
    940T writes "abc"	 Buffer: a b c _ _
    									E
    							  B
    920T reads "a"		Buffer: a b c _ _
    									E
    								B
    940T writes "defg"	Buffer: f g c d e
    								  E
    								B
    920T reads "g"		Buffer: f g c d e
    								  E
    								  B
    RESULT
      940T sends: abcdefg
      920T receives: ag
    
    -Tony
     
    Last edited by a moderator: Feb 4, 2016
  10. rixed

    rixed Member

    Joined:
    Dec 31, 2005
    Messages:
    206
    Location:
    Paris (fr)
    The 940 must find out if there are room to fit new datas, as well as the 920 must find out if there are available datas waiting.
    This is standard circular buffer things.
     
    Last edited by a moderator: Feb 4, 2016
  11. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    Yes, I believe I did not do this check. For my little debugging facility I didn't care very much but thinking about it now, I see that people may want to adapt the code to other purposes so I should have done the 'buffer full' check. It would be better to return failure from that function than overrun the buffer.
     
    Last edited by a moderator: Feb 4, 2016
  12. satacoy

    satacoy Still Fresh

    Joined:
    May 1, 2006
    Messages:
    81
    I've been following along with the demo articles. Thanks Dzz for all the knowlege!

    I'm struggling a bit with the 940. Specifically, I'm having trouble accessing the MMSP2 registers from the 940.

    I understand needing to use MMAP from the 920, since it's using virtual addressing. On the 940 though, shouldn't I be able to just point to them at 0xC0000000? I'm subtracting 0x2000000 due to the address offset setup, but I'm still not getting the behavior I expect.

    So, to get the base of the registers, I'm doing:

    unsigned short * pusRegs = (unsigned short *) 0xC0000000 - 0x2000000;


    However button presses or vsync checks don't seem to be working. Any ideas?
     
  13. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    The cast is not behaving like you would want and you are subtracting too much. Try this:

    unsigned short * pusRegs = (unsigned short *) (0xC0000000 - 0x2000000);
     
    Last edited by a moderator: Feb 4, 2016
  14. Squidge

    Squidge Certified Guru

    Joined:
    Nov 16, 2003
    Messages:
    8,495
    Location:
    UK
    If you download the HH SDK, you can access all the registers by name on the 940.

    So for example, instead of this:

    value = pusRegs[0x1180];

    You can do this:

    value = MSP_GPIOAPINLVL;

    You can't do this on the 920 however, as it's infected with Linux :)

    The HH SDK can be downloaded from Rob Brown's website (don't know the url of hand), or via a mirror such as my own: http://www.ibiblio.org/paulc/gp2x-gen/sdk2x_191205d.tar.gz - look in the "include" dir.
     
  15. satacoy

    satacoy Still Fresh

    Joined:
    May 1, 2006
    Messages:
    81
    Ok, that was embarrassing! Guess my dusty C skills are a bit dustier than I thought.

    That got me a bit farther, but now I'm hitting a bigger issue that I've been chasing for a few days now. The Lock and Unlock procedures are causing me grief. Boiling it down to the simplest test case I can think of, if you replace "Do940Stuff" in the 6b article source with this:

    Code:
    void Do940Stuff()
    {
      g_pSharedData = (SharedData *) 0x1E00000;
    
      SendDebug("Testing from the 940!");
      SendDebug("Hello!");
    
      unsigned short * pusRegs = (unsigned short *) (0xC0000000 - 0x2000000);
    
      while(1){
    	// See if the left should button is pressed
    	if((pusRegs[0x1184>>1] & 0x400) == 0) {
    	  Lock((unsigned int *) (&g_pSharedData->uLock));
    	  Unlock((unsigned int *) (&g_pSharedData->uLock));
    	}
      }
    }
    
    Everything locks up once you hit the left shoulder button. I can't figure out why, I'm obviously unlocking after every lock. If I let go of the left should button, no locking should be occuring on the 940, so the 920 should get the lock and continue on it's merry way.

    I eventually started suspecting the Lock or Unlock function. Doing a bit of research on the web about ARM assembly, they look OK, although my ARM assembly is much weaker than my already poor C skills.
     
    Last edited by a moderator: Feb 4, 2016
  16. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    Yeah, that that makes it obvious just by looking at it what's going on! :lol:

    That HH thing sounds pretty nifty. Now that the gp2x boots pretty fast I might be interested in working in that environment. Can you do file I/O to the SD card from an HH app?


    I'll check it out and let you know what I find! If there is an issue with the synchronization primitives I'm eager to find it!
     
    Last edited by a moderator: Feb 4, 2016
  17. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    I'm afraid I am not able to reproduce any problem :( With this code the only thing that happens for me is that the frame time counter goes up from 31 to 42. It goes back to 31 when I release the button. I think this is because the 940 code is hammering that poor lock variable pretty hard, which is both eating some memory bandwidth and delaying access on the 920. I think.

    Anyway, check out http://gp2xgamer.com/satacoy.zip for my version (including binaries). If those binaries lock up your gp2x then maybe there's some other difference at play here. In a different thread it was pointed out that continually hammering memory from the 940 can confuse the 920 kernel (at least to the point of killing USB, maybe other effects) because it's starving it for resources. You could try adding a little delay function like this:

    Code:
    
    asm (
    "DoDelay:\n"
    "  subs r0, r0, #1\n"
    "  bne DoDelay\n"
    "  mov pc, lr\n"
    );
    void DoDelay(int nCount);
    
    inside the while loop.
     
    Last edited by a moderator: Feb 4, 2016
  18. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    Actually, I was just able to reproduce the issue, it doesn't happen all the time for me. I'll investigate.
     
  19. satacoy

    satacoy Still Fresh

    Joined:
    May 1, 2006
    Messages:
    81
    Your binary also locks up my unit. The time does jump up to 42, but then the unit is locked, and no combination of button hitting/releasing will bring it back to life.

    I've tried adding delays to my code yesterday, but end up with the same issue regardless of how long I delay. I'll play around with it a bit more, and try your delay code to see if it makes any difference. Bizarre!



    That's good. I was starting to think I was losing my mind! :eek:
     
    Last edited by a moderator: Feb 4, 2016
  20. Dzz

    Dzz stmia r0!, {r2-r9}

    Joined:
    Jan 30, 2006
    Messages:
    1,098
    There's definitely something not right going on here. I'm beginning to suspect that the hardware support to make sure the SWP instruction is atomic might not be implemented in the MagicEyes memory controller. I'm not sure how to figure out whether that's the case or not.
     
    Last edited by a moderator: Feb 4, 2016

Share This Page

Loading...