Building a wireless Android device using BeagleBone Black
Our previous posts were about code signing in Android, and they turned out to be surprisingly relevant with the announcement of the 'master key' code signing Android vulnerability. While details are yet to be formally released, it has been already patched and dissected, so we'll skip that one and try something different for a change. This post is not directly related to Android security, but will discuss some Android implementation details, so it might be of some interest to our regular readers. Without further ado, let's get closer to the metal than usual and build a wireless Android device (almost) from scratch.
Board introduction -- BeagleBone Black
- touch screen input
- wireless connectivity via WiFi
- battery powered
Linux Device Tree and cape support
sysfs
, resource conflict resolution (where possible), manual control over already loaded capes and more.Using the 3.8 kernel
sgx
in rowboat's top-level Makefile
like this:@@ -11,7 +13,7 @@
CLEAN_RULE = sgx_clean wl12xx_compat_clean kernel_clean clean
else
ifeq ($(TARGET_PRODUCT), beagleboneblack)
-rowboat: sgx
+#rowboat: sgx
CLEAN_RULE = sgx_clean kernel_clean clean
else
ifeq ($(TARGET_PRODUCT), beaglebone)
Note that the kernel alone is not enough though: the boot loader (Das U-Boot) needs to be able to load the (flattened) device tree blob, so we need to build a recent version of that as well. Android seems to run OK with this configuration, but there are still a few things that are missing. The first you might notice is ADB support.
ADB support
- Configure FunctionFS support in the kernel (
CONFIG_USB_FUNCTIONFS=y
): - Modify the boot parameters in uEnv.txt to set the vendor and product IDs, as well as the device serial number
- Setup the FunctionFS directory and mount it in your
init.am335xevm.usb.rc
file: - Delete all lines referencing
/sys/class/android_usb/android0/*
. (Those nodes are created by the native Android gadget driver and are not available when using FunctionFS.)
Device Drivers -> USB Support ->
USB Gadget Support -> USB Gadget Driver -> Function Filesystem
g_ffs.idVendor=0x18d1 g_ffs.idProduct=0x4e26 g_ffs.iSerialNumber=<serial>
on fs
mkdir /dev/usb-ffs 0770 shell shell
mkdir /dev/usb-ffs/adb 0770 shell shell
mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
adb devices
soon after the kernel has loaded. Now you can debug the OS using Eclipse and push and install files directly using ADB. That said, this won't help you at all if the device doesn't boot due to some kernel misconfiguration, so you should definitely get an FTDI cable (the BBB does not have an on-board FTDI chip) to be able to see kernel messages during boot and get an 'emergency' shell when necessary.cgroups patch
adb logcat
in a console and experimenting with the device, you will notice a lot of 'Failed setting process group' warnings like this one: W/ActivityManager( 349): Failed setting process group of 4911 to 0
W/SchedPolicy( 349): add_tid_to_cgroup failed to write '4911' (Permission denied);
Android's
ActivityManager
uses Linux control groups (cgroups) to run processes with different priorities (background, foreground, audio, system) by adding them to scheduling groups. In the mainline kernel this is only allowed to processes running as root
(EUID=0
), but Android changes this behaviour (naturally, with a patch) to only require the CAP_SYS_NICE
capability, which allows the ActivityManager
(running as system
in the system_server
process) to add app processes to scheduling groups. To get rid of this warning, you can disable scheduling groups by commenting out the code that sets up /dev/cpuctl/tasks
in init.rc
, or you can merge the modified functionality form Google's experimental 3.8 branch (which we've been trying to avoid all along...). Android hardware support
Touchscreen
We now have a functional Android development device running mostly without warnings, so it's time to look closer at requirement #1. As we mentioned, once we disable hardware acceleration, the LCD4 works fine with our 3.8 kernel, but a few things are still missing. The LCD4 comes with 5 directional GPIO buttons which are somewhat useful because scrolling on a resistive touchscreen takes some getting used to, but that is not the only thing they can be used for. We can remap them as Android system buttons (Back, Home, etc) by providing a key layout (.kl) file like this one:key 105 BACK WAKE
key 106 HOME WAKE
key 103 MENU WAKE
key 108 SEARCH WAKE
key 28 POWER WAKE
The GPIO keypad on the LCD identifies itself as 'gpio.12' (you can check this using the
getevent
command), so we need to name the layout file to 'gpio_keys_12.kl'. To achieve this we modify device.mk
in the BBB device directory (device/ti/beagleboneblack
): ...
# KeyPads
PRODUCT_COPY_FILES += \
$(LOCAL_PATH)/gpio-keys.kl:system/usr/keylayout/gpio_keys_12.kl \
...
Now that we are using hardware buttons, we might want to squeeze some more screen real estate from the LCD4 by not showing the system navigation bar. This is done by setting
config_showNavigationBar
to false
in the config.xml
framework overlay file for our board:<bool name="config_showNavigationBar">false</bool>
While playing with the screen, we notice that it's a bit dark. Increasing the brightness via the display settings however does not seem to work. A friendly error message in logcat tells us that Android can't open the
/sys/class/backlight/pwm-backlight/brightness
file. Screen brightness and LEDs are controlled by the lights
module on Android, so that's where we look first. There is a a hardware-specific one under the beagleboneblack device directory, but it only supports the LCD3 and LCD7 displays. Adding support for the LCD4 is simply a matter of finding the file that controls brightness under /sys. For the LCD4 it's called /sys/class/backlight/backlight.10/brightness
and works exactly like the other LCDs -- you get or set the brightness by reading or writing the backlight intensity level (0-100) as a string. We modify light.c
(full source on Github) to first try the LCD4 device and voila -- setting the brightness via the Android UI now works... not. It turns out the brightness
file is owned by root
and the Settings app doesn't have permission to write to it. We can change this permission in the board's init.am335xevm.rc file
:# PWM-Backlight for display brightness on LCD4 Cape
chmod 0666 /sys/class/backlight/backlight.10
This finally settles it, so we can cross requirement #1 off our list and try to tackle #2 -- wireless support.
WiFi adapter
rtl8192cu
driver. In addition to the kernel driver, this wireless adapter requires a binary firmware blob, so we need to make sure it's loaded along with the kernel modules. But before getting knee-deep into makefiles, let's briefly review the Android WiFi architecture. Like most hardware support in Android, it consists of a kernel layer (WiFi adapter driver modules), native daemon (wpa_supplicant
), HAL (wifi.c
in libharware_legacy
, communicates with wpa_supplicant
via its control socket), a framework service and its public interface (WifiService
and WifiManager
) and application/UI ('WiFi' screen in the Settings app, as well as SystemUI
, responsible for showing the WiFi status bar indicator). That may sound fairly straightforward, but the WifiService
implements some pretty complex state transitions in order to manage the underlying native WiFi support. Why is all the complexity needed? Android doesn't load kernel modules automatically, so the WifiStateMachine
will try to load kernel modules, find and load any necessary firmware, start the wpa_supplicant
daemon, scan for and connect to an AP, obtain an IP address via DHCP, check for and handle captive portals, and finally, if you are lucky, set up the connection and send out a broadcast to notify the rest of the system of the new network configuration. The wpa_supplicant
daemon alone can go through 13 different states, so things can get quite involved when those are combined.Going step-by-step through the porting guide, we first enable support for our WiFi adapter in the kernel. That results in 6 modules that need to be loaded in order, plus the firmware blob. The HAL (
wifi.c
) can only load a single module though, so we pre-load all modules in the board's init.am335xevm.rc
and set the wlan.driver.status
to ok
in order to prevent WifiService
from trying (and failing) to load the kernel module. We then define the wpa_supplicant
and dhcpd
services in the init file. Last, but not least, we need to set the wifi.interface
property to wlan0
, otherwise Android will silently try to use a test device and fail to start the wpa_supplicant
. Both properties are set as PRODUCT_PROPERTY_OVERRIDES
in device/ti/beagleboneblack/device.mk
(see device directory on Github). Here's how the relevant part from init.am335xevm.rc
looks like: on post-fs-data
# wifi
mkdir /data/misc/wifi/sockets 0770 wifi wifi
insmod /system/lib/modules/rfkill.ko
insmod /system/lib/modules/cfg80211.ko
insmod /system/lib/modules/mac80211.ko
insmod /system/lib/modules/rtlwifi.ko
insmod /system/lib/modules/rtl8192c-common.ko
insmod /system/lib/modules/rtl8192cu.ko
service wpa_supplicant /system/bin/wpa_supplicant \
-iwlan0 -Dnl80211 -c/data/misc/wifi/wpa_supplicant.conf \
-e/data/misc/wifi/entropy.bin
class main
socket wpa_wlan0 dgram 660 wifi wifi
disabled
oneshot
service dhcpcd_wlan0 /system/bin/dhcpcd -ABKL
class main
disabled
oneshot
service iprenew_wlan0 /system/bin/dhcpcd -n
class main
disabled
oneshot
In order to build the
wpa_supplicant daemon
, we then set BOARD_WPA_SUPPLICANT_DRIVER
and WPA_SUPPLICANT_VERSION
in device/ti/beagleboneblack/BoardConfig.mk
. Note the we are using the generic wpa_supplicant
, not the TI-patched one and the WEXT
driver instead of the NL80211
one (which requires a proprietary library to be linked in). Since we are preloading driver kernel modules, we don't need to define WIFI_DRIVER_MODULE_PATH
and WIFI_DRIVER_MODULE_NAME
. BOARD_WPA_SUPPLICANT_DRIVER := WEXT
WPA_SUPPLICANT_VERSION := VER_0_8_X
BOARD_WLAN_DEVICE := wlan0
To make the framework aware of our new WiFi device, we change
networkAttributes
and radioAttributes
in the config.xml
overlay file. Getting this wrong will lead to Android's ConnectionManager
totally ignoring WiFi even if you manage to connect and will result in the not too helpful 'No network connection' message. "1" here corresponds to the ConnectivityManager.TYPE_WIFI
connection type (the built-in Ethernet connection is "9", TYPE_ETHERNET
).<string-array name="networkAttributes" translatable="false">
...
<item>"wifi,1,1,1,-1,true"</item>
...
</string-array>
<string-array name="radioAttributes" translatable="false">
<item>"1,1"</item>
...
</string-array>
Finally, to make Android aware of our newly found WiFi features, we copy
android.hardware.wifi.xml
to /etc/permissions/
by adding it to device.mk
. This will take care of enabling the Wi-Fi screen in the Settings
app:PRODUCT_COPY_FILES := \
...
frameworks/native/data/etc/android.hardware.wifi.xml:system/etc/permissions/android.hardware.wifi.xml \
...
After we've rebuild rowboat and updated the root file system, you should be able to turn on WiFi and connect to an AP. Make sure you are using an AC power supply to power the BBB, because the WiFi adapter can draw quite a bit of current and you may not get enough via the USB cable. If the board is not getting enough power, you might experience failure to scan, dropping connections and other weird symptoms even if your configuration is otherwise correct. If WiFi support doesn't work for some reason, check the following:
- that the kernel module(s) and firmware (if any) is loaded (
dmesg
,lsmod
) logcat
output for relevant-lookin error messages- that the
wpa_supplicant
service is defined properly ininit.*.rc
and the daemon is started - that
/data/misc/wifi
andwpa_supplicant.conf
are available and have the right owner and permissions (wifi:wifi
and 0660) - that the
wifi.interface
andwlan.driver.status
properties are set correctly - use your debugger if all else fails
Battery power
VDD_5V
) on the board directly. We can use any USB battery pack that provides enough current (~1A) and has enough capacity to keep the device going by simply connecting it to the miniUSB port. Those can be rather bulky and you will need an extra cable, so let's look for other options. As can be expected, there is a cape for that. The aptly named Battery Cape plugs into the BBB's expansion connectors and provides power directly to the power rail. We can plug the LCD4 on top of it and get an integrated (if a bit bulky) battery-powered touchscreen device. The Battery Cape holds 4 AA batteries connected as two sets in parallel. It is not simply a glorified battery holder though -- it has a boost converter that can provide stable 1A current at 5V even if battery voltage fluctuates (1.8-5.5V). It does provide support for monitoring battery voltage via AIN4 input, but does not have a 'fuel gauge' chip so we can't display battery level in Android without adding additional circuitry. That is ways our mobile device cannot display the battery level (yet) and unfortunately won't be able to shut itself down when battery levels become critically low. That is something that definitely needs work, but for now we make the device always believe it's at 100% power by setting the hw.nobattery
property to true
. The alternative is to have it display the 'low battery' red warning icon all the time, so this approach is somewhat preferable. Four 1900 mAh batteries installed in the battery cape should provide enough power to run the device for a few hours even when using WiFi, so we can (tentatively) mark requirement #3 as fulfilled.Flashing the device
fastboot
tool over USB. The rowboat build does not have a recovery image, and while fastboot
is supported by TI's fork of U-Boot, the version we are using to load the DT blob does not support fastboot
yet. That leaves booting another OS in lieu of a recovery and flashing the eMMC form there, either manually or by using an automated flasher image. The flasher image simply runs a script at startup, so let's see how it works by doing it manually first. The latest BBB Angstrom bootable image (not the flasher one) is a good choice for our 'recovery' OS, because it is known to work on the BBB and has all the needed tools (fdisk
, mkfs.ext4
, etc.). After you dd
it to an SD card, mount the card on your PC and copy the Android boot files and rootfs
archive to an android/
directory. You can then boot from the SD card, get a root shell on the Angstrom and install Android to the eMMC from there.Android devices typically have a
boot
, system
and userdata
parition, as well as a recovery
one and optionally others. The boot partition contains the kernel and a ramdisk which gets mounted at the root of the device filesystem. system
contains the actual OS files and gets mounted read-only at /system
, while userdata
is mounted read-write at /data
and stores system and app data, as well user-installed apps. The partition layout used by the BBB is slightly different. The board ootloader will look for the first stage bootloader (SPL, named MLO in U-Boot) on the first FAT partition of the eMMC. It in turn will load the second state bootloader (u-boot.img
) which will then search for a OS image according to its configuration. On embedded devices U-Boot configuration is typically stored as a set of variables in NAND, replaced by the uEnv.txt
file on devices without NAND such as the BBB. Thus we need a FAT boot partition to host the SPL, u-boot.img
, uEnv.txt
, the kernel image and the DT blob. system
and userdata
will be formatted as EXT4 and will work as in typical Android devices.The default Angstrom installations creates only two partitions -- a DOS one for booting, and a Linux one that hosts Angstrom Linux. To prepare the eMMC for Android, you need to delete the Linux partition and create two new Linux partitions in its place -- one to hold Android system files and one for user data. If you don't plan to install too many apps, you can simply make them equal sized. When booting from the SD card, the eMMC device will be
/dev/block/mmcblk1
, with the first partition being /dev/block/mmcblk1p1
, the second /dev/block/mmcblk1p2
and so on. After creating those 3 partitions with fdisk
we format them with their respective filesystems:# mkfs.vfat -F 32 -n boot /dev/block/mmcblk1p1
# mkfs.ext4 -L rootfs /dev/block/mmcblk1p2
# mkfs.ext4 -L usrdata /dev/block/mmcblk1p3
Next, we mount
boot
and copy boot related files, then mount rootfs
and untar the rootfs.tar.bz2
archive. usrdata
can be left empty, it will be populated on first boot.# mkdir -p /mnt/1/
# mkdir -p /mnt/2/
# mount -t vfat /dev/block/mmcblk1p1 /mnt/1
# mount -t ext4 /dev/block/mmcblk1p2 /mnt/2
# cp MLO u-boot.img zImage uEnv.txt am335x-boneblack.dtb /mnt/1/
# tar jxvf rootfs.tar.bz2 -C /mnt/2/
# umount /mnt/1
# umount /mnt/2
With this, Android is installed on the eMMC and you can shutdown the 'recovery' OS, remove the SD card and boot from the eMMC. Note that the U-Boot used has been patched to probe whether the SD card is available and will automatically boot from it (without you needing to hold the BBB's user boot button), so if you don't remove the 'recovery' SD card, it will boot again.
We now have a working, touch screen Android device with wireless connectivity. Here's how it looks in action:
Our device is unlikely to win any design awards or replace your Nexus 7, but it could be used as the basis of dedicated Android devices, such as a wireless POS terminal or a SIP phone and extended even further by adding more capes or custom hardware as needed.
Summary
This entry was posted on at 7:51 PM and is filed under android, beagleboneblack. You can follow any responses to this entry through the RSS 2.0. You can