Nook Simple Touch (NST)
Hardware
- CPU: 32bit TI OMAP 3621 (ARMv7, ARM Cortext-A8). 800MHz
- RAM: 256 MB
- Storage: 2GB + microSD
- Display: 6in, 600x800 @ 167 PPI (eink) ED060SCE(LF)C1, touchscreen.
- Power: 3.7V, 5.66 Wh (1530 mAh).
- Stock OS: Android 2.1
- Model number: BNRV300
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 .
- Source: NST mounting the boot partition
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
- Hacking ram disks
- XDA forum: modifying uramdisk with imgutil
- Notes on NST by drewbug
- Android booting (elinux)
- The Android Init Language
- Boot a Raspberry Pi 4 using u-boot and Initramfs
- How U-boot loads Linux Kernel?
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.
- Side buttons:
/dev/input/event0 - Home and power buttons:
/dev/input/event1 - Touchscreen:
/dev/input/event2