Nook Simple Touch (NST)


Hardware

Rooting

You probably want to have your device rooted and the process is really quite easy. Essentially you only have to burn NookManager to an sd card (I use dd for this). If you insert the SD card into your Nook and turn it on it will boot into the NookManager application. Continue without wifi, do a backup and click root. The backup will take a while so go make a coffee or something. It will not let you root if you didn’t do the backup, at least it was like that for me.

My firmware was 1.2.2 which isn’t compatible with the original image, but someone updated it. I’ve linked both versions below. The only hurdle I had here was due to a faulty SD card, which was booting NookManager but never completed the backup. After switching it up it worked just fine.

Mounting the boot partition

If we want to extract or modify the boot partition from a running device, we need to have root access. The following commands work once we access an adb shell (adb shell).

mount -o rw,remount rootfs /
mkdir /boot
mount -t vfat /dev/block/mmcblk0p1 /boot

This will mount the boot partition into a folder that we can access through the shell. Of course once can always dump the entire partition into an image and pull it via adb:

# on an ADB shell:

dd if=/dev/block/mmcblk0p1 of=path-to-img bs=1M

# on your computer

adb pull path-to-img .

Uramdisk hacking

The boot process for the NST uses U-boot, which loads a ramdisk image to perform the system initialization. By modifying the ramdisk, we can gain control of what gets run on initialization, so for example we can just launch whatever program we want and avoid launching the Dalvik VM (Zygote). My understanding of this process is a bit basic still, so treat these notes with a pinch of salt.

In a Linux system we need to follow the following steps to modify the uramdisk. Note that sudo is important to preserve root permissions in the ramdisk files.

# Use dd to strip the image header
dd bs=1 skip=64 if=uramdisk.img of=uramdisk-no-header.img
# Use zcat/gzip to expand the contents
zcat uramdisk-no-header.img > uramdisk-uncompressed.img
# Use cpio to separate it into files
mkdir ramdisk
cd ramdisk
sudo cpio -i --no-absolute-filenames < ../uramdisk-uncompressed.img
# Edit what you want in init.rc
# ...
# Use cpio to combine the files and gzip to compress the contents
shopt -s dotglob
sudo find . | sudo cpio -H newc -o | gzip > ../uramdisk.cpio.gz
# Use mkimage to wrap the contents
mkimage -A arm -O linux -T ramdisk -n "Initial Ram Disk" -d ../uramdisk.cpio.gz ../uramdisk.img.new

Unfortunately, at the moment this doesn’t fully seem to work for me. I suspect the mkimage command is the one creating problems, so for now I am using the Windows imgutil with wine, which does the trick. I’ll revisit this in the future if I figured out the solution.

With imgutil we can extract and modify the init.rc file that controls the programs run during initialization.

wine imgutil uramdisk /x /v init.rc
vim init.rc
wine imgutil uramdisk /r /v init.rc

Other resources

Running arbitrary code on the Nook

If you have access to an adb shell, you can already run programs or commands in your Nook. For example try the following commands:

adb shell
# cat /dev/urandom > /dev/graphics/fb0

This should fill your screen with some random noise, but if you interact with the device (by using the home button for example) the original applications will appear on top of your beautiful noise. We can stop the Dalvik VM with the shell command stop, at which point you have exclusive access to the machine resources (other than the rest of running processes of course, but this should be fine). If you used NookManager for rooting you can now write sh compatible shell scripts or run native ARMv7 compatible code, which we will discuss soon.

To make development easier, I just modified the NookManager ramdisk to avoid running any operations, having a clean slate to work with while my SD card is on. If I want to access the regular Nook OS, I just remove the SD and reboot the device. Magic!

Native programming

On the NST we can runk 32-bit ELF-LSB executables compiled for ARMv7, but unfortunately the process can be a bit painful. I’ve found that recent versions of the Android NDK or GCC/clang compilers don’t work on the NST, they segfault even with a simple hello world or a program that just returns 0.

Luckily we can still find some older NDK versions in the interwebs. Older Android NDKs also want you to create a standalone toolchain. The following will take care of that, and of course feel free to change the intall directory to wherever you want.

curl -O http://dl.google.com/android/repository/android-ndk-r12b-linux-x86_64.zip
unzip android-ndk-r12b-linux-x86_64.zip
sudo ./android-ndk-r12b/build/tools/make-standalone-toolchain.sh --arch=arm64 --install-dir=/opt/android-ndk-nst

Now you can cross compile C code with GCC go nuts and try a hello world why don’t ya!

/opt/android-ndk-nst/bin/arm-linux-androideabi-gcc hello.c -o hello
adb push hello /tmp/hello
adb shell
# cd /tmp
# ./hello
Hello world!

Note that it is possible that the NST doesn’t have all features you come to expect from the standard library or system headers. But you can make use of linux/fb.h and linux/input.h which will come in handy. In general try to have as few dependencies as possible to avoid headaches.

The framebuffer is located at /dev/graphics/fb0 and is a 16bpp display, so if you have 32-bit RGBA colors, you may want to convert them to rgb565 like so:

u16
rgb565(u32 rgba) {
    u16 r = (rgba >> 16  & 0xFF);
    u16 g = (rgba >> 8   & 0xFF);
    u16 b = (rgba >> 0   & 0xFF);
    r = r >> 3;
    g = g >> 2;
    b = b >> 3;
    return (r << 11) | (g << 5) | b;
}

When working with the Linux framebuffer device, I like to MMAP it so that I can just write to it with something like fb[idx] = color. In a regular PC, this will update the screen immediately, but this is not the case on the NST. As a workaround we can perform a write syscall with 0 bytes whenever we want, which will trigger the screen update.

int fb = open("/dev/graphics/fb0", O_RDWR);
write(fb, "0", 0);

Similarly, if we want to perform a full screen refresh to clear potential e-ink ghosting, we can write to /sys/class/graphics/fb0/epd_refresh as follows:

int refresh = open("/sys/class/graphics/fb0/epd_refresh", O_RDWR);
write(refresh, "1", 1);

You can handle inputs as you normally would with input.h. Considering the following mapping of I/O devices.

Other resources