BEAGLEBONE BLACK BARE METAL DEVELOPEMENT

2014-12-08

Not so long ago, I wrote a small OS prototype for the Cortex-A8 CPU. I was using qemu but now I wanted to play with a real device.
So I decided to give it a shot with my BeagleBone Black.

Booting

The beaglebone black's AM3359 chip has an internal ROM (located at 0x40000000) that contains boot code.
That boot code will attempt to boot from many sources but I am only interested in the eMMC booting.
The boot code will expect the eMMC to be partitioned and that the first partition is FAT32. I don't
know if there is anyway to just use the eMMC as raw memory and have the AM3359 boot code to just load
whatever is at the bottom of the flash without any partitions, so I will live with the FAT32 concept.
I want to use u-boot because I want to be able to update my kernel with tftp. The stock BBB will have
the eMMC formatted with a FAT32 partition with uboot on it. I will make a u-boot script that
downloads my kernel from the tftp server, copy it in flash memory and then have u-boot load that
kernel from flash memory into RAM. That last step is not necessary but I want to do it because at a later
point in time, I will remove the tftp part from the u-boot script and only have the kernel in flash be loaded
in RAM.

The proper way to do this, would be to store the kernel file and all of my application files in the ext2 partition
that is already present in the eMMC. But then, I would need a EXT2 driver in my kernel so that it could
load the application files from the flash. I don't wanna bother writing a ext2 driver for now so I will hack my way
though this instead. So instead of getting uboot to download the kernel and applications in a eMMC partition,
I will get it to write the kernel at a fixed location (0x04000000) in the eMMC. This will most probably overwrite
a part of the 1st or second partition but I really don't care at that point. As long as I don't overwrite
the partition table and the begining of the FAT32 partition where u-boot sits. Then all applications
will be written one after the other just after the kernel in a linked-list style.

According to section 2.1 of the TI reference manual for the AM335x, the ROM starts at 0x40000000. But then, in section
26.1.3.1, they say that the ROM starts at 0x20000. This is very confusing. It turns out that when booting, memory location
0x40000000 is aliased to 0x00000000. The CPU starts executing there, and some ROM code jumps to the "public ROM code". The public ROM code starts at ROM_BASE+0x20000. Since memory is aliased, 0x200000 is the
same as 0x40020000. Section 26.1.4.2 says that the ROM code relocates the interrupt vector to 0x200000, probably using
CP15 register c12. When the ROM code finds the x-loader (MLO) in flash memory, it loads it in SRAM at 0x402F0400. At this point, system behavior is defined by u-boot (MLO was built with u-boot).
What was confusing me at first was that I thought that the eMMC mapped to 0x00000000. Turns out that this memory
is not directly addressable. So if I need to retrieve my applications from eMMC, I will need to write a eMMC driver
because the eMMC is only accessible through EMMC1. Now that I understand how eMMC works, I realize that it was foolish
of me to think that it could be directly addressable. The MMC1 peripheral will allow you to communicate with the on-board
eMMC but you still need to write your own code to interface it using the SD/MMC protocol.
I had a really hard time finding information on how to read the eMMC. The TI documentation is good at explaining how to use the
MMC controller but they don't explain how to actually communicate with the eMMC. And that's normal since the eMMC is board dependant.
The eMMC is accessible through MMC1. The TI documentation explains how to initialize the device but since we know that the
board contains eMMC, we don't have to go through all the trouble of detecting card types etc. I was really surprised of
how it was hard to find good documentation on how to use the MMC/SD protocol. I can't really explain what I did, all I know
is that it works, and the code will definitely not be portable to another board. I read the TRM and also looked at another source code
and trought trial and error, I was able to read the eMMC. The file emmc.S in my source code is pretty easy to understand. I was not able to send the proper command to set the device in "block addressing mode" and to change the bus
width. Like I said, this information is kinda hard to find. I'll have to do a lot more researching to make this work.

I want uboot to download my kernel from tftp and load it in memory. There doesn't seem to be
any easy way to do this. I couldn't find a way to install uboot on my BBB without installing
a full eMMC image containing linux. So I decided to just use the stock eMMC image but modify uboot
to boot my kernel instead of the installed linux. But it seems that changing the environment
variable "bootcmd" is impossible from uboot on the BBB. But there is the uenv.txt file
residing on the FAT partition that I can change to contain my own script to download my kernel.
Well, that to is impossible to modify directly from uboot.

So I ended creating an SD card with an angstrom image, boot from the SD card, mount the eMMC FAT32 partition
and edit the uenv.txt file. I modifed it to look like this:

Now everytime I want to update the uenv.txt file, I need to boot from the SD card because I am destroying the 2nd partition
on the eMMC with my kernel since I use raw writing on the eMMC. This is not a nice solution but it works for now

Software IRQ

The software IRQs on the BBB work in a completely different way than the realview-pb-8 board.
On the BBB, software IRQs are not dedicated IRQs. You get a register that allows you to trigger an IRQ
that is tied to a hardware IRQ already. So You can only use software IRQ to fake a hardware IRQ. This means
that you could send a software IRQ 95 but that would be the same as if you would get a timer7 IRQ. You
actually need to unmask IRQ 95 for this to work, but unmasking IRQ 95 will also allow you to get TIMER7 IRQs.
In my case, this is excellent. Because my timer7 IRQ calls my scheduler code. So a Yield() function would just
trigger that IRQ artificially using the software IRQ register.

User-mode handling of IRQs

User-mode threads can register interrupt handlers in order to be notified when GPIO is triggered.
The way this works is that whenever an interrupt is received, if a user-handler is defined, then
the page table is changed to the page table base address of the thread that is interested in receiving
the event. Then, a jump to the handler is done. So the CPU stays in IRQ mode, but the page table is changed
and the user-mode handler is executed in IRQ mode.

The code

There is a lot more I could describe in here but the source code might a better source of documentation. Basically, other things I have
accomplished is:

AM3358 interrupt controller

AM3358 timer

SPI driver for a port expander (MCP23S18) and for an EEPROM chip (25aa256)