Peripherals

Peripherals

1. Introduction

The Nintendo DS and DSi has several peripherals that you’re unlikely to use in most applications, but can be very useful in some specific applications.

2. Battery

You can check the status of the battery easily, but the information you can obtain depends on the DS model:

  • Original DS: You can only check if the battery level is high or low.
  • DS Lite: You can also check if the charger is connected or not. Some of them support reading 15 charge levels.
  • DSi: You can check if the charger is connected or not, and 15 different charge levels.

Libnds abstracts the different consoles and it provides one single function to check this information from both the ARM9 and ARM7:

u32 value = getBatteryLevel();
unsigned int battery_level = value & BATTERY_LEVEL_MASK;
bool charger_connected = value & BATTERY_CHARGER_CONNECTED;

printf("Current charge level: %u\n", battery_level);
printf("Charger connected: %s\n", charger_connected ? "Yes" : "No");

You can check this code in action in this example: examples/peripherals/battery_status

3. Hardware divider and square root

The ARM9 has access to some registers that can calculate integer divisions and square roots by hardware. This can help you optimize applications because the CPUs of the DS don’t support division or square root and they need to be calculated by software. The registers work asynchronously, so you can even let the hardware calculate something while you do other things:

div32_asynch(7000, 3); // Start division
u32 sqrt_out = sqrt64(12345 * 12345);
u32 div_out = div32_result(); // Read division result

You can check all helpers provided by libnds in the documentation here. There are some helpers to do regular integer division and square root, as well as some helpers to do fixed point divisions and square roots.

You can check this code in action in this example: examples/peripherals/maths_coprocessor

4. Real Time Clock

You can get the current time by using the standard C function time(). You can use its returned value the same way you’d use it in any C program. For example:

#include <time.h>

// ...

char str[100];

time_t t = time(NULL);
struct tm *tmp = localtime(&t);

if (strftime(str, sizeof(str), "%Y-%m-%dT%H:%M:%S%z", tmp) == 0)
    snprintf(str, sizeof(str), "Failed to get time");

printf("%s", str);

You can also set the date and time in the RTC chip. This must be done from the ARM7, libnds doesn’t provide any helper to do it from the ARM9 at the moment. You can set the time and date simultaneously or just the time:

rtcTimeAndDate rtc_time_date = { ... };

if (rtcTimeAndDateSet(&rtc_time_date) != 0)
{
    // Error
}

rtcTime rtc_time = { ... };

if (rtcTimeet(&rtc_time) != 0)
{
    // Error
}

Remember to subtract 2000 from the real year when writing the values of a rtcTimeAndDate struct!

Games like Animal Crossing detect manual changes of the time/date in the firmware settings because the firmware settings store the fact that you’ve modified the RTC settings (PersonalData->rtcOffset). Changing the values with libnds won’t affect that flag, so the change won’t be detected!

You can check this code in action in this example: examples/time/rtc_set_get

5. Firmware user settings

Players can set settings like their name, birthday, etc, in a small flash memory present in all DS models. This memory also stores the Wi-Fi connection settings, for example.

The default ARM7 core in BlocksDS reads the settings with function readUserSettings(), which is called at the beginning of main(). You may have to wait for a couple of frames for the function to run and load the settings.

You can access them with the PersonalData pointer. You can see information about its members here. For example, you can just type PersonalData->birthDay to read the user birth day.

The name and personal message are encoded as UTF-16LE format, not UTF-8. You can use function utf16_to_utf8() of libnds to convert it.

Check the following example to see how to do this in practice: examples/firmware/user_settings

6. LCD backlight

The backlight of the LCD screens can be adjusted by the application. This is useful when entering sleep mode, for example. However, different DS models have different capabilities, Libnds provides systemSetBacklightLevel(), which takes a value between 0 and 5, and this is how it behaves in different models:

  • DSi: 5 levels of brightness (1 to 5, or PM_BACKLIGHT_MIN to PM_BACKLIGHT_MAX).
  • DS Lite: 4 levels of brightness (2 to 5). Level 1 is internally set to level 2.
  • DS: The screen can be turned off or on. Levels 1 to 5 are internally set to level 5 (full brightness). Some models of the DS support the same levels of brightness of the DS Lite. In them, the function behaves the same way as on DS Lite.

Level 0 (PM_BACKLIGHT_OFF) turns the backlight off.

DS and DS Lite consoles support turning on and off individual screens, but systemSetBacklightLevel() doesn’t support controlling the two screens independently.

On DSi the brightness setting is persistent and it will be the setting used the next time the console is turned on.

You can check this code in action in this example: examples/peripherals/backlight_level

7. Microphone

The DS has a microphone that games use for things like:

  • Detecting if the user blows on the screen.
  • Speech recognition (like Nintendogs).
  • Record some audio and play it back to the player later.

The original DS can record audio with 12 bits of accuracy, the DSi can record it with 16 bits of accuracy. Apart from that, there are no significant differences from the point of view of the developer.

You need to:

  • Allocate a buffer in main RAM where the ARM7 will save the microphone output.
  • Write a callback function that receives the data recorded by the ARM7 as the buffer gets filled. This callback must copy the data into another buffer if you want to preserve the recording.
  • Call soundMicRecord() so that the ARM7 starts recording audio and calling your callback.
  • Call soundMicOff() to stop recording.

You will need to be careful with the data cache of the ARM9. If your callback reads data from the buffer, that data gets loaded to the cache. If the ARM7 writes to the same memory later, but that data is still in the cache, the ARM9 will read the old values instead of the new ones! There are two good solutions:

  • Read from a mirror of the buffer. Use memUncached() to get a pointer to that data that doesn’t make the CPU load data to the cache. However, this is a bit slow in general.
  • Use DMA to copy the data out of this temporary buffer into another buffer. DMA doesn’t load anything to the cache, so you don’t need to worry about it.

You can check an example of how to record audio and play it back here: examples/audio/microphone

8. DSi cameras

The DSi has two cameras with a resolution of 640x480 pixels. You can access them from libnds, but you need to handle a few things manually.

  1. Initialize the camera with cameraInit(). Select the camera you want to use with cameraSelect(CAMERA_INNER) or cameraSelect(CAMERA_OUTER) (you can switch cameras later if you want, as long as no transfer is active).

  2. You will need to use a NDMA channel to transfer data. Let’s call it CAMERA_NDMA_CHANNEL.

  3. Start displaying the camera preview to the user. The preview is just a captured image that is smaller than usual (256x192 pixels). Wait until the previous NDMA transfer is finished with ndmaBusy(CAMERA_NDMA_CHANNEL) and that the camera transfer itself is finished with cameraTransferActive().

    Once the transfer is done you can start a preview transfer. The following code can be used to transfer the data to background 3, assuming you’ve configured it as a 16-bit bitmap:

    cameraStartTransfer(bgGetGfxPtr(3),
                        MCUREG_APT_SEQ_CMD_PREVIEW,
                        CAMERA_NDMA_CHANNEL);

    The preview data will be saved as 16-bit RGBA format.

  4. When the user presses the right button you can capture an image with the full resolution of the camera. This image will be stored in YUV format, so you need to allocate a temporary buffer (called yuv in this example) and call:

    cameraStartTransfer(yuv, MCUREG_APT_SEQ_CMD_CAPTURE, CAMERA_NDMA_CHANNEL);

    This capture will take longer than the preview capture, and you will need to provide your own code to convert from YUV to RGB if you want to use the data for something.

  5. When you’ve finished using the camera, call cameraDeinit().

You can check a full example of how to take photos and store them in the SD card here: examples/peripherals/camera

The DSi camera official application can store photos in the SD card or in the internal NAND memory of the DSi. You can access the photos with libnds as well. All you have to do is to initialize the NAND with nandInit() and check the files in path "nand2:/photo/DCIM".

There’s a demo that displays the photos in that folder here: examples/demos/photo_slideshow

9. Slot-2 devices

There are lots of Slot-2 cartridges with special hardware. Some of them came as additional controllers for Nintendo DS games, but some of them are just GBA games with peripherals that can be used from a DS game:

  • Piano keyboard: Used by game “Easy Piano”.
  • Rumble pak: Used by several games.
  • Guitar Grip: Used by the “Guitar Hero” series.
  • The Opera “Nintendo DS Browser” has a Slot-2 cartridge with 8 MiB of RAM.
  • Solar sensor: Used by “Boktai 1”, “Boktai 2” and “Boktai 3”.
  • Tilt sensor: Used by “Koro Koro Puzzle” and “Yoshi’s Universal Gravitation”.
  • Rumble + Gyroscope: Used by “WarioWare”.
  • Paddle Controller: Used by a few Taito arcade games.

There are also some GBA/DS flashcarts with special hardware or RAM:

  • SuperCard SD: External RAM. Some models (SuperCard Rumble) have rumble.
  • M3: External RAM
  • G6: External RAM
  • EZ-Flash III/IV/Omega/Omega Definitive Edition: External RAM. Some models have rumble.
  • EZ-Flash 3in1: External RAM. Some models have rumble.
  • EverDrive GBA: External RAM.

The Slot-2 API abstracts all of them and provides a unified API to use them:

  1. Start it with peripheralSlot2InitDefault(), which will autodetect the cartridge you have in the Slot-2.

  2. The name of the device can be obtained with peripheralSlot2GetName(), and a bit mask of all the features supported by the device with peripheralSlot2GetSupportMask(). This mask is a combination of the defines SLOT2_PERIPHERAL_*.

  3. You can detect if the device has RAM with peripheralSlot2RamStart(), which returns a pointer to the start of RAM if available or NULL if there is no external RAM. Use peripheralSlot2RamSize() to see the size of the available RAM. Also, some unusual devices have banked RAM. You can check the number of banks of the device with peripheralSlot2RamBanks().

    This sytem can also be used to access the additional 16 MiB of RAM available in Nintendo DSi debugger consoles (with 32 MiB instead of 16 MiB of RAM) and 3DS consoles running in DSi mode.
  4. You can detect if the device supports rumble by checking if flag SLOT2_PERIPHERAL_RUMBLE_ANY is set. Then you can use function rumbleGetMaxRawStrength() to detect the maximum strength supported by the device, and setRumble(strength) to activate it. Use setRumble(0) to disable the vibration.

    Some rumble cartridges are level-activated (they start vibrating by setting them to “ON”) and others are edge-activated (they move a bit each time they switch from “OFF” to “ON” and from “ON” to “OFF”). If you want to support all types of rumble, treat all cartridges as edge-activated cartridges and flip between “ON” and “OFF” in your game.
  5. If your device has a tilt sensor (SLOT2_PERIPHERAL_TILT) you will need to start a read with peripheralSlot2TiltStart() before you can read its value. You also need to do it every time you read its value:

    slot2TiltPosition pos;
    
    if (peripheralSlot2TiltRead(&pos))
    {
        printf("Tilt: X=%03X, Y=%03X", pos.x, pos.y);
        peripheralSlot2TiltStart();
    }
  6. If your device has a gyroscope (SLOT2_PERIPHERAL_GYRO_GPIO) you can read it with peripheralSlot2GyroScan().

  7. If your device has a solar sensor (SLOT2_PERIPHERAL_SOLAR_GPIO) you can read its value with peripheralSlot2SolarScanFast().

  8. If your device has a Guitar Grip (SLOT2_PERIPHERAL_GUITAR_GRIP) you need to treat it like the other keys in libnds. Use guitarGripScanKeys() to fetch and update the state of the keys, and then guitarGripKeysDown(), guitarGripKeysHeld() and guitarGripKeysUp(). You can use the defines GUITARGRIP_GREEN, GUITARGRIP_RED, GUITARGRIP_YELLOW and GUITARGRIP_BLUE to see the state of each button. You can also bypass the Slot-2 API and use guitarGripIsInserted() to detect the Guitar Grip if you aren’t interested in other devices.

  9. If you device has a Paddle Controller (SLOT2_PERIPHERAL_PADDLE) you can read its state with paddleRead().

  10. If your device has a Piano keyboard (SLOT2_PERIPHERAL_PIANO) you need to use pianoScanKeys() (and then pianoKeysDown(), pianoKeysHeld() and pianoKeysUp()). You can also bypass the Slot-2 API and use pianoIsInserted() to detect the piano if you aren’t interested in other devices. The names of the defines used to check the keys are PIANO_C, PIANO_CS, PIANO_D, PIANO_DS, PIANO_E, PIANO_F, PIANO_FS, PIANO_G, PIANO_GS, PIANO_A, PIANO_AS, PIANO_B and PIANO_C2.

Check this example to see all of this in action: examples/peripherals/slot2

10. DS Motion Card

The DS Motion Card is a Slot-1 cartridge with an accelerometer and a gyroscope. This device isn’t very popular because it requires the user to run homebrew from a Slot-2 cartridge, and most people just use Slot-1 devices because they are more convenient. In theory you could use them in a DSi easier than in a DS because you can run homebrew from the SD card of the DSi itself.

  1. Initialize the device with motion_init(). You can get its type with motion_get_type() and its name with motion_get_name(motion_get_type()).

  2. Ask the user to callibrate the accelerometer by placing the console on a flat surface. The measurements of the X and Y axes will be 0 and the one of the Z axis will be equal to G.

    motion_set_offs_x();
    motion_set_offs_y();
    motion_set_sens_z(motion_read_z());
  3. You can callibrate the gyroscope with motion_set_offs_gyro().

  4. Read the callibrated values with motion_acceleration_x(), motion_acceleration_y(), motion_acceleration_z() and motion_rotation().

  5. Call motion_deinit() when you’ve finished using the device.

Check the following example: examples/peripherals/motion_card