Home New Tutorials Programs
1-R4DS Setup 2-Software Space Invaders
Old Tutorials DSEmu
1-Setup 4-Sound 7-FIFO
2-Framebuffer 5-SRAM 8-Interrupts
3-Keys 6-Filesystems 9-Microphone
10-Extended Rotation Background

Homebrew Nintendo DS Development Part 6

Filesystems, Keys and Sound

This tutorial was tested and works with libnds dated 2005-12-12 and DSEmu 0.4.9 (Although sound will not work in DSEmu, the program will still run).

In tutorial four we had the problem of including sound files in the program. The way it was done in that tutorial was to convert the samples to binary files using 'objcopy' and linking them into the program.

The problem with this approach is it takes up valuable memory. In DevKitPRO the ARM7 program is only allowed 64KB of code. That's not much room for sound samples. Although the ARM9 can have more it's still limited.

The approach we are going to use in this tutorial is to use a filesystem. A filesystem is a way of storing the files in a similar manner to that on a standard OS filesystem. You can reference the file by filename and iterate over a list of files. The files are stored seperate from the program code so they don't take up valuable RAM. They are instead stored on the cartridge and accessed as required.

The advantage of this approach is you can have many files, limited only by the size of the game cartridge. The disadvantage is your program must be stored on the game cartridge. You can't use this method for 'downloadable' games that don't run off the cartridge itself.

We're also going to cover how to send instructions from the ARM9 to the ARM7 requesting sound samples to be played. If you recall in tutorial four we controlled when to play the samples from the ARM7 when the X and Y buttons were pressed. This time, the buttons will be detected from the ARM9 and a message is sent to the ARM7 to play the sample.

Finally we'll address the problem of the user pressing a key and that key press being detected over multiple frames.

The program that this tutorial demonstrates will enable selecting a sound sample from a list of samples in the filesystem and allowing playing that sample on a selected channel. The channels can be changed and other samples played while existing samples are still playing.

The Gameboy Advance Filesystem: GBFS

The Nintendo DS has a built in filesystem in the 'NDS' file format. The program, 'ndstool', can be used to add and remove files from it. Unfortunately there are no homebrew libraries available yet that allow reading this filesystem from the DS hardware.

In the meantime there is an excellent filesystem library called GBFS for the Gameboy Advance. This also happens to work well on the DS. The library was written by Damian Yerrick and is available for download here. A local copy is mirrored here.

The readme file in the download explains how to use it. Basically you will want to put all the executable files in your path somewhere. Then you can use 'gbfs.exe' to create a filesystem in a file and 'lsgbfs.exe' to list the contents of that filesystem. This file containing the filesystem can then be appended to the program binary. An example of using this tool is:

d:\source\>gbfs filesystem.gbfs InvHit.raw BaseHit.raw
      4080 InvHit.raw
      6548 BaseHit.raw
d:\source\>lsgbfs filesystem.gbfs
      4080 InvHit.raw
      6548 BaseHit.raw
d:\source\>cat myprog_tmp.nds filesystem.gbfs >myprog.nds

I had to make one minor change to the GBFS source code to get it to compile for the DS using DevKitPRO. That is on line 119 of 'libgfs.c', I had to cast the result of 'bsearch' to '(const GBFS_ENTRY*)':

  here = (const GBFS_ENTRY*)bsearch(key, dirbase,
                 n_entries, sizeof(GBFS_ENTRY),
                 namecmp);

Without this change the following error message occurs:

libgbfs.c:121: error: invalid conversion from `void*' to `const GBFS_ENTRY*'
make: *** [libgbfs.o] Error 1

Appending the GBFS file

Once the GBFS file is created it needs to be appended to the .nds file. It needs to be appended on a 256 byte boundary so the first thing to do to the .nds file is use 'padbin' from DevKitPro to pad it out to 256 bytes and then use 'cat' to append the GBFS to the .nds:

  ndstool -c gbfs_demo1.nds -9 arm9.bin -7 arm7.bin
  padbin 256 gbfs_demo1.nds
  cat gbfs_demo1.nds sounds.gbfs >gbfs_demo1_tmp.nds
  mv gbfs_demo1_tmp.nds gbfs_demo1.nds

Accessing the filesystem

From within the Nintendo DS program a number of functions are provided in a 'libgbfs.c' which need to be compiled and linked into the executable for the processor that will be using the filesystem. In this example we are accessing it from the ARM9 so it is linked into the ARM9 executable.

The filesystem is located at the end of our NDS file, and is stored in the GBA cartridge ROM area. Tutorial five showed that this memory area starts at memory address 0x8000000 up to 0x0A000000 where the cartridge SRAM area starts. We need to map this area so that the ARM9 can access it. Tutorial five showed how to do this using the WAIT_CR register:

  // Map Game Cartridge memory to ARM9
  WAIT_CR &= ~0x80;

Once the memory area is mapped we call 'find_first_gbfs_file' to locate the first file in the filesystem. The result of this call is a pointer to an object of type GBFS_FILE, which works in a similar manner to a FILE structure in C stdio.

'find_first_gbfs_file' will search through memory looking for the filesystem. It needs a pointer in memory to start from. I use the start of the cartridge ROM we mapped:

  /* Start searching from the beginning of cartridge memory */
  GBFS_FILE const* gbfs_file = 
    find_first_gbfs_file((void*)0x08000000);

Once a GBFS_FILE pointer is obtained we can search for specific files. The 'gbfs_get_obj' call does this, taking a filename and returning a pointer to the memory location of that file. It also optionally sets a supplied pointer to the length of the file:

    uint32 length = 0;
    uint8* data    = (uint8*)gbfs_get_obj(gbfs_file, 
			  	         "BaseInv.raw",
				         &length);

The file data can then be operated on directly using the pointer given or copied to RAM for manipulation. There are other gbfs functions that can be used and they're all explained in the readme file included in the gbfs distribution.

Handling Key presses

Tutorial three showed how to detect button presses. Unfortunately the way it checked for a button and acted on it does not work well for the example we are going to use here.

We want pressing the 'down' key to decrease the channel number displayed on the screen, and 'up' to increase it. Based on code from Tutorial three I originally tried code like the following:

 while(1) {
   int keys = ~REG_KEYINPUT;
   [...]   
   if(keys & KEY_DOWN) {
     if(current_channel == 0)
       current_channel = 15;
     else
       --current_channel;
  [...]
  swiWaitForVBlank();
 }   

The problem with this is that a user will press the down key and it will stay down for multiple frames, and therefore multiple runs through the 'while loop'. This results in the channel being decreased multiple times for each press. There is little chance of the user only pressing the button for 1/60th of a second to get a single channel change.

Tutorial three didn't exhibit this problem because we were changing the colour of a block on the key press. It didn't matter if it was changed to the same colour multiple times.

There is a well known way of working around this from the Gameboy Advance. This excellent GBA 'Keys' Tutorial explains how to do this in the Advanced keys section.

'libnds' has a similar system to this implemented with the following functions in keys.h:

void scanKeys();
void keysInit();
u32 keysHeld();
u32 keysDown();
u32 keysUp();

All that is required is to call the 'scanKeys' function every frame, and use the other functions to detect the changes in key state. You will also need to call 'keysInit' before using these functions. Our previous example, fixed to use this system, would be:

 keysInit();
 while(1) {
   swiWaitForVBlank();
   scanKeys();
   [...]
   if(keysDown() & KEY_DOWN) {
     if(current_channel == 0)
       current_channel = 15;
     else
       --current_channel;
  [...]
 }   

Now pressing the down key will only decrease the channel number once, no matter how long it is held down.

Interproccessor Communication

As was mentioned in Tutorial Four, only the ARM7 can access the sound registers. Ideally we need a way of sending commands from the ARM9 to the ARM7. This would not only be useful for sound, but also for wireless and other ARM7 only functionality.

The approach I've taken was inspired from a MOD playing demo application, dssound, that was posted to the GBADEV forums.

'libnds' actually has a means to send sound playing commands from the ARM9 but I wasn't able to get this working. Since I started writing this tutorial, an example of how to use this was posted to the GBADEV forums. The approach taken here can be easily extended for things other than sound but the ndslib approach is definitely worth a look.

Commands

The interprocess communications system used in this example uses a 'Command' structure to define commands that can be sent from the ARM9 to the ARM7. This is defined in command.h.

/* The ARM9 fills out values in this structure to tell the ARM7 what
   to do. */
struct Command {
  CommandType commandType;
  union {
    void* data;  
    PlaySampleSoundCommand playSample;    
  };
};

A 'Command' is a discriminated union. It has a 'commandType' member that defines the type of command (play sound, connect via wireless, etc), and a union which has the data required for the command. Currently there is either a pointer to unstructured data, or an instance of a 'PlaySampleSoundCommand'.

The 'CommandType' is an enumeration of commands that can be sent:

/* Enumeration of commands that the ARM9 can send to the ARM7 */
enum CommandType {
  PLAY_ONE_SHOT_SAMPLE
};

The 'Command' structure can be extended for new commands by adding the command type to the enumeration, and adding the data that the command needs to the union.

PlaySampleSoundCommand

The 'PlaySampleSoundCommand' holds the data required to play a sound. This includes a pointer to the raw sound data, frequency, volume, etc:

/* Command parameters for playing a sound sample */
struct PlaySampleSoundCommand
{
  int channel;
  int frequency;
  void* data;
  int length;
  int volume;
};

A command to play sounds can therefore be created with code like:

  Command command;
  command.commandType = PLAY_ONE_SHOT_SAMPLE;
  command.playSample.channel = 0;
  command.playSample.frequency = 11127;
  command.playSample.data = [...];
  [...etc...]

CommandControl

A circular array of commands is kept in shared memory, accessable by the ARM7 and ARM9. An index into that array for where new incoming commands will be placed is kept in a 'currentCommand' member:

/* Maximum number of commands */
#define MAX_COMMANDS 20

/* A structure shared between the ARM7 and ARM9. The ARM9
   places commands here and the ARM7 reads and acts upon them.
*/
struct CommandControl {
  Command command[MAX_COMMANDS];
  int currentCommand;
};

/* Address of the shared CommandControl structure */
#define commandControl ((CommandControl*)((uint32)(IPC) + sizeof(TransferRegion)))

The libnds library has a shared IPC variable, of type 'TransferRegion'. The shared 'CommandControl' variable is placed directly after this variable, ensuring it is in the shared memory area. That's what the 'commandControl' define is doing above.

If you use other libraries that have shared IPC variables that hook into the system in the same manner you'll need to modify this code to be positioned so that they don't overwrite each other.

Commands and the ARM9

From the ARM9, to place a command onto the queue of commands to be run you need to update the 'Command' in the array indexed by 'currentCommand', then increment 'currentCommand'. You should wrap 'currentCommand' back to zero if it goes over MAX_COMMANDS:

void CommandPlayOneShotSample(
  int channel, 
  int frequency, 
  void* data, 
  int length, 
  int volume)
{
  Command* command = 
    &commandControl->command[commandControl->currentCommand];
  PlaySampleSoundCommand* ps = &command->playSample;

  command->commandType = PLAY_ONE_SHOT_SAMPLE; 
  ps->channel = channel;
  ps->frequency = frequency;
  ps->data = data;
  ps->length = length;
  ps->volume = volume;

  commandControl->currentCommand++;
  commandControl->currentCommand &= MAX_COMMANDS-1;
}

From the ARM9 you also need to call an initialization function to set the ComandControl shared variable to an initial state:

void CommandInit() {
  memset(commandControl, 0, sizeof(CommandControl));  
}

This ARM9 command related code is stored in command9.cpp. New commands can be added to this file, as well as updating the Command union in command.h.

Commands and the ARM7

The ARM7 related code is in command7.cpp. The function 'CommandProcessCommands' must be called regularly from the ARM7. I call it from the VBlank interrupt. It iterates through all commands in the CommandControl structure that it hasn't yet processed, find out the type, and performs an action depending on that type:

void CommandProcessCommands() {
  static int currentCommand = -1;
  
  while(currentCommand != commandControl->currentCommand) {
    Command* command = &commandControl->command[currentCommand];
    switch(command->commandType) {
    case PLAY_ONE_SHOT_SAMPLE:
      CommandPlayOneShotSample(&command->playSample);
      break;      
    }
    currentCommand++;
    currentCommand &= MAX_COMMANDS-1;
  }
}

If the commands are extended this function will need to be modified to switch on the type of the command and call a function, passing it the data from the Command union. The only routine so far is 'CommandPlayOneShotSample' which does the usual ARM7 sound playing code, as described in tutorial four:

static void CommandPlayOneShotSample(PlaySampleSoundCommand* ps)
{
  int channel = ps->channel;

  SCHANNEL_CR(channel) = 0;
  SCHANNEL_TIMER(channel) = SOUND_FREQ(ps->frequency);
  SCHANNEL_SOURCE(channel) = (uint32)ps->data;
  SCHANNEL_LENGTH(channel) = ps->length >> 2;  
  SCHANNEL_CR(channel) = 
    SCHANNEL_ENABLE | 
    SOUND_ONE_SHOT | 
    SOUND_8BIT | 
    SOUND_VOL(ps->volume);
}

Usage

With this code in place, playing sounds from the ARM9 is as simple as calling 'CommandPlayOneShotSample' which does all the IPC work:

    if(keysDown() & KEY_A) {
      [...]
      CommandPlayOneShotSample(current_channel, 
			       current_file->frequency, 
			       sound_buffer, 
			       current_file->length, 
			       0x3F);
    }

Putting it all together

Although we could iterate through the GBFS filesystem to find out what files are there, I've created a structure in the demo program that holds the filename, a user friendly display name, and the frequency which that file is to be played:

/* Pointers to sound data */
struct SoundFile {
  char const* filename;
  char const* name;
  int frequency;  
  uint8*  data;
  uint32 length;
};

static SoundFile soundFiles[] = {
  { "BaseHit.raw", "Base Hit", 11127, 0, 0 },
  { "InvHit.raw", "Invader Hit", 11127, 0, 0 },
  { "Shot.raw", "Shot", 11127, 0, 0 },
  { "Ufo.raw", "Ufo", 11127, 0, 0 },
  { "UfoHit.raw", "Ufo Hit", 11127, 0, 0 },
  { "Walk1.raw", "Walk1", 11127, 0, 0 },
  { "Walk2.raw", "Walk2", 11127, 0, 0 },
  { "Walk3.raw", "Walk3", 11127, 0, 0 },
  { "Walk4.raw", "Walk4", 11127, 0, 0 },
  { 0,0,0,0,0 }
};

This structure also holds the length and pointer into the GBFS file where the file is located. That data is populated during initialisation by mapping the cartridge memory and using the GBFS routines:

static void InitSoundFiles()
{
  // Map Game Cartridge memory to ARM9
  WAIT_CR &= ~0x80;
  
  /* Start searching from the beginning of cartridge memory */
  GBFS_FILE const* gbfs_file = 
    find_first_gbfs_file((void*)0x08000000);

  unsigned int max_length = 0;
  SoundFile* file = soundFiles;
  while(file->filename) {
    file->data = (uint8*)gbfs_get_obj(gbfs_file, 
				      file->filename, 
				      &file->length);
    if(file->length > max_length)
      max_length = file->length;
    file++;
  }
  sound_buffer = (uint8*)malloc(max_length);
  current_file = soundFiles;
}

Notice that I also create a sound buffer that holds a pointer to dynamically allocated memory of the largest sound file. This is used for copying the data from the cartridge memory to the buffer to send to the ARM7 to play. I wasn't able to succeed in having the ARM7 play the sound directly from the cartridge memory. Unfortunately this limits the largest sample that you can play to that which can be held in RAM. I'll look into how to work around this in a future tutorial.

The 'main' routine just handles the key presses and the VBlank routine displays the text:

void on_irq() 
{	
  if(REG_IF & IRQ_VBLANK) {
    // Clear screen
    printf("\x1b[2J");

    printf("GBFS Demo Program\n\n");
    
    printf("Press 'A' to play current file.\n");
    printf("'left/right' changes file.\n");
    printf("'up/down' changes channel.\n\n");
    
    printf("File:    %s (%d)\n", current_file->name,  current_file->length);
    printf("Channel: %d\n", current_channel);

    // Tell the DS we handled the VBLANK interrupt
    VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;
    REG_IF |= IRQ_VBLANK;
  }
  else {
    // Ignore all other interrupts
    REG_IF = REG_IF;
  }
}

void InitInterruptHandler()
{
  REG_IME = 0;
  IRQ_HANDLER = on_irq;
  REG_IE = IRQ_VBLANK;
  REG_IF = ~0;
  DISP_SR = DISP_VBLANK_IRQ;
  REG_IME = 1;
}

int main(void)
{
  powerON(POWER_ALL);  
  videoSetMode(MODE_0_2D | DISPLAY_BG0_ACTIVE);
  vramSetBankA(VRAM_A_MAIN_BG);
  BG0_CR = BG_MAP_BASE(31);
  BG_PALETTE[255] = RGB15(31,31,31);
  lcdSwap();
  InitInterruptHandler();
  consoleInitDefault((u16*)SCREEN_BASE_BLOCK(31), (u16*)CHAR_BASE_BLOCK(0), 16);

  CommandInit();
  InitSoundFiles();
  keysInit();

  while(1) {
    swiWaitForVBlank();
    scanKeys();

    if(keysDown() & KEY_UP) {
      if(++current_channel > 15)
	current_channel = 0;
    }
    if(keysDown() & KEY_DOWN) {
      if(current_channel == 0)
	current_channel = 15;
      else
	--current_channel;
    }
    if(keysDown() & KEY_LEFT) {
      if(--current_file < soundFiles)
	current_file = soundFiles;
    }
    if(keysDown() & KEY_RIGHT) {
      if((++current_file)->filename == 0) 
	--current_file;
    }
    if(keysDown() & KEY_A) {
      dmaCopy(current_file->data, sound_buffer, current_file->length);
      CommandPlayOneShotSample(current_channel, 
			       current_file->frequency, 
			       sound_buffer, 
			       current_file->length, 
			       0x3F);
    }
  }

  return 0;
}

The raw sound data is copied from the cartridge memory to our local sound buffer using a DMA copy when the 'A' key is pressed. Then the sound is requested to be played. A DMA copy is basically a fast, CPU efficient, hardware accelerated copy.

On the ARM7 side, the only change to the standard template is to call the 'CommandProcessCommands' function mentioned previously in the VBlank interrupt routine.

Building the Demo

The complete example program is 'gbfs_demo1'. The ARM9 code is in arm9_main.cpp. It uses the console routines to print information similar to the first tutorial. The ARM7 code is in arm7_main.cpp. A Makefile file is supplied to build everything.

The complete source code is supplied in gbfs_demo1.zip and you can download the gbfs_demo1.nds and gbfs_demo1.nds.gba files for running on the emulators or hardware.

Conclusion

This tutorial covered a lot of areas. It showed how to load files from an included filesystem that is appended to the program binary. It showed how to send commands from the ARM9 to the ARM7 to play sounds. And finally it went through how to detect key presses without repeat notifications of the same keypress.

There are some issues with the method shown here of appending the GBFS file to the .NDS file. The main one is that the program must be stored on a flash cartridge. It cannot be transferred wirelessly using 'WMB'. This is because WMB will only send the .bin files inside the .NDS. A workaround for this is to convert the GBFS file to a binary file using objcopy, as shown in tutorial four and link the file into the ARM9 binary. This has the disadvantage of limiting you to the size of the GBFS file as it must fit into main RAM. Which approach you choose depends on the size of what you want to attach and whether you need WMB support.

There is the possiblity of appending the GBFS file to the end of the ARM9 binary instead of the .NDS. Although it seems that this should work it has problems. This same memory area is used by devkitpro for unintialized global variables and malloc memory space. I don't suggest using this approach as if you use malloc or unintialized gobals things will break.

I'd like to thank the forum posters in the GBADEV forums, where I learnt much of the information for this tutorial, and the author of the MOD playing example program for showing an approach to interprocessor communication .

As always, any comments or suggestions are welcomed. See my contact details below.