Eric's Place
Welcome to the DEEP WEB. No... wait... still the regular web.

Flashing a Beaglebone Black the COOL Way

Let’s take a moment to appreciate the Beaglebone Black, a low-cost ARM development board that seems to pop up constantly in my career. While the Raspberry Pi has been showered in accolades and blog posts as the de-facto standard for cheap single board ARM computers, the Beaglebone Black is quiety being integrated into many industrial projects. And if you’re wondering why:

  • It is readily available for purchase in Canada for ~$75. Rapsberry Pis are often either impossible to find or sold at highly inflated prices here;
  • It uses the Texas Instruments AM335x SoC, which has great publicly available documentation;
  • It has built-in eMMC storage, which is way more reliable than the SD card storage Raspeberry Pis rely on (except for certain recent compute modules which finally include eMMC).

Now about that eMMC storage: Normally to flash it with an image, you start with a special “flasher” image burned onto a removable SD card. Then you can run a script that will mount the eMMC drive and setup the partition table, bootloader (U-Boot), and rootfs for you. Power off, remove the SD card, and power back on. Bam, eMMC booting! But what if we don’t want to use an SD card at all in this process? Is such a thing even possible?

Nope, end of blog post, you can go back to worrying if you’ll be replaced by ChatGPT.

Nah, just kidding. Let’s do eeeet.

Give it the Boot

I think it’s worth explaining how the Beaglebone Black is booted, mostly for when I will inevitably forget in a few months and need something to refer back to.

The first step is to get some insight into the boot process using the celebrated publicly available documentation, page 4101. We learn that the first step is a hardcoded boot ROM which will hunt for something to boot in sequence across several peripherals. Table 26-7 shows us the possible boot sequences with respect to the values of the SYSBOOT[4:0] pins (shared with LCD_DATA). The default boot sequence should be eMMC -> SD Card -> UART0 -> USB0, with this changing to SPI0 -> SD Card -> USB0 -> UART0 when the S2 button on the board is held down.

If you’re wondering how I figured this out, I used Google I read the pin config values on a running system (This utility is very helpful, or the devmem utility in Busybox). These pins are latched on boot into a register at address 0x44E10040 (0x44E10000 from page 158 + 0x40 from page 765). I got 0x0040033c. With the S2 button held down, the value changes to 0x00400338, which gives us SPI0 -> SD Card -> USB0 -> UART0, skipping the eMMC alotgether.

On a brand spanking new BBB, the eMMC should be empty, so assuming we also don’t insert an SD card or USB key, it will attempt to boot from the serial port on UART0 using XMODEM. So how do we access UART0?

Accessories not Included

UART0 gets its own VIP pin header: J1. There are 6 pins, 3 of which do absolutely zilch:

  • Pin 1 -> GND (next to the little dot);
  • Pin 4 -> RXD;
  • Pin 5 -> TXD;

These pins emit and receive signals at 3.3V, so we’ll need an adapter to connect it to a normal desktop computer. You want to buy something called a “USB to TTL Serial adapter”, which should expose individual wires you can use to hook up the above pins. Make sure to connect the RXD pin of the header to TXD on the adapter, and vice-versa for the TXD pin.

To access the serial port, we’ll need a terminal emulator running on your PC. TeraTerm and MobaXTerm are good options for Windows. On Linux we have minicom or even just screen. The options are 8-N-1 115200, which is a cool way of saying 8-bits of data, no parity, one stop bit, and a bitrate of 115200.

Lastly, we need some sort of program the handles the XModem protocol for us. Some terminal emulators have this built-in, but I just wrote a short Python program to do this using the xmodem package. And by writing it into a script, we get bonus automation for the next time. The PyPi page gives us pretty much all we need:

    import serial
    from xmodem import XMODEM
    ser = serial.Serial('<COM Port>', timeout=0)
    def getc(size, timeout=1):
        return ser.read(size) or None

    def putc(data, timeout=1):
        return ser.write(data)

    modem = XMODEM(getc, putc)

    stream = open('<file to send>', 'rb')
    modem.send(stream)

Bare Your Box

So we have a setup that will allow the Beaglebone Black to boot via UART0, and so now we just need something to boot. There’s a catch though: At this point DDR will not have been initialized, so whatever we transfer will be loaded into internal static memory (SRAM). This SRAM is quite limited in size at 128KB, though (and only a subset of that is available), so our program won’t be able to do very much. At this stage we usually want to run a “bootloader” that sets up some peripherals (like DDR) and launches Linux. In our case we also want to be able to use it to flash the root filesystem, and we simply can’t squeeze all that functionality into < 128KB. Luckily, the people who develop bootloaders are pretty smart and thought of this: What if we build a minimal “primary” bootloader executable, and then have that load the full-featured bootloader once DDR is available? The bootloader used with the BBB, U-Boot does exactly this: It will build both an Mmc LOader (MLO) image that fits in SRAM, and a larger u-boot.img file.

But U-Boot has a fatal drawback in that it can’t seem to “stream” a file to eMMC, and we don’t have enough RAM to hold an entire root filesystem image. However, there is an alternative bootloader project called Barebox that does have this functionality, so let’s use that instead. An alternative approach would be to boot a Linux kernel zImage and device tree via U-Boot with a rootfs mounted on a Network File Storage (NFS) or a minimal one that resides entirely in-memory, and use that environment to flash the eMMC.

Building Barebox is fairly straightforward and it produces an MLO and full image just like U-Boot does. And since we’re compiling for bare metal we can use any arm-linux-gnueabihf toolchain without having to worry about glibc incompatibilities, bonus!

  1. Install the gcc-arm-linux-gnueabihf toolchain;
  2. Clone the barebox repo: git clone https://github.com/saschahauer/barebox.git;
  3. Setup environment variables for cross compilation: export ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
  4. Setup the default configuration for the MLO make am335x_mlo_defconfig
  5. Build the MLO: make -j$(nproc)
  6. Setup the default configuration for the full image: make omap_defconfig
  7. Repeat: make -j$(nproc)
  8. You should now have barebox-am33xx-beaglebone-mlo.img and barebox-am33xx-beaglebone.img in your images/ folder. You can also configure which features are compiled into Barebox by using the make menuconfig command.

Flash Gordon

Alright, now with everything set up, it’s time for the fun part. Assuming your Beaglebone is indeed blank and no SD card is inserted, opening the serial port should reveal a series of C characters being printed to the terminal. That’s the Boot ROM waiting for XMODEM to send a file, great!

So close the terminal, and use your favourite XMODEM utility to send the images/barebox-am33xx-beaglebone-mlo.img file. It will take some time (things moved a bit more slowly when it was invented 47 years ago), and then eventually give you…another XMODEM prompt.

Remember, the idea was just to set up the DDR memory on the Beaglebone so we can store the “proper” Barebox image. So now repeat the same procedure with images/barebox-am33xx-beaglebone.img. Naturally, this new transfer will take several times longer than before, but eventually it should complete and you should now have a nice console prompt on the serial port:

barebox@TI AM335x BeagleBone black:/

So to recap: We just loaded a small program (primary bootloader), which in turn allowed up to load a larger program (secondary bootloader), and now we want to use this program to flash an image to the Beaglebone’s internal FLASH memory (eMMC). Now we want to flash an actual Linux distribution.

So first let’s pick one from https://beagleboard.org/latest-images. We need one that is < 4GB, because we obviously only have 4GB of eMMC space available. I’m partial to the 1GB console images myself.

With that in hand, we need to fire up a Trivial File Transfer Protocol (TFTP) server to serve up the image on our host PC. TFTP is another protocol that time forgot but the embedded systems industry remembered. There are some implementations available for Windows or Linux. And of course, there is a Python library as well.

Now we need to give the Beaglebone some network connectivity, so plug an Ethernet cable into the Beaglebone, and plug the other end into either your router or another PC.

If connected to a router, you can grab an IP by running the dhcp command:

barebox@TI AM335x BeagleBone black:/ dhcp

Otherwise, you can set a static IP:

barebox@TI AM335x BeagleBone black:/ eth0.ipaddr=192.168.1.x barebox@TI AM335x BeagleBone black:/ eth0.netmask=255.255.255.0

And last but not least, we need to tell Barebox where our TFTP server is:

barebox@TI AM335x BeagleBone black:/ eth0.serverip=192.168.1.x

Where 192.168.1.x is the IP address of your host PC.

Now we can finally flash our image:

barebox@TI AM335x BeagleBone black:/ tftp bone-debian-10.3-console-armhf-2020-04-06-1gb.img /dev/mmc0

Assuming you set everything up correctly, a little progress bar should appear and the image will (slowly) be written to the eMMC. Be patient! If you get a series of Ts, you made a mistake somewhere.

Let er’ rip!

Once this process is complete, possibly after grabbing a coffee or reading posts on your favourite blog, all you need to do is power-cycle the Beaglebone! If you don’t want to touch the plug because your fingers are covered in Cheeto dust, you can just use the reset command in the console.

Let there be embedded Linux! It should start booting and drop you into a Linux console. You’ll likely notice two things:

  1. The eMMC only seems to be 1GB, but it should be 4GB;
  2. The boot process was run using U-Boot, what happened to Barebox?;

Well kids, when you raw-copy a 1GB SD card image with a partition table, bootloader, and rootfs, you get a … 1GB image. Luckily those kind Texans did us a solid and provide us a script to grow the rootfs. The script should also be available in the image itself in /opt/scripts.

And this also answers our second question: Since we flashed an image with U-Boot, we got…U-Boot. Barebox was only running from RAM and disappeared like a fart in the wind as soon as we rebooted the board. It’s actually not a bad thing; The new Beaglebone images put the bootloader at specific addresses in the first 4MB before the ext4 filesystem, but currently Barebox only supports the old boot method with a primary FAT partition containing the MLO and boot image. If I ever get around to it, I’ll make a pull-request to add the new boot method in Barebox. Until then, we all live in a yellow U-Boot I guess.

Mais Pourquoi?

So if you made it all the way here, you might have one question: Why? How is this remotely better than just loading up a $10 SD card with the image? Well, remember when I mentioned the Beaglebone Black being deployed in actual products? This enables us to perform field upgrades or re-image corrupted systems without having to dismantle anything, or flash an initital image without having to do anything except connect a couple of cables. Also, I work very hard at being lazy and this is a great way to flash boards at the office without having to leave the comfort of my home…office.

May your TFTP transfer ACK every packet 🙏🏻