Archive for September 7, 2026
Running modern FreeBSD on the BeagleBone Black
0FreeBSD 13.3 was the last release that booted on a BeagleBone Black. AM335x was removed from the armv7 GENERIC kernel config, the drivers left behind bit-rotted, and 13.x has since been purged from the download mirrors — it survives only on archive.freebsd.org/old-releases.
This is a working port for 14.x and 15.x, validated on real hardware, plus the patches and a full walkthrough. It boots to multi-user with serial console, microSD root, eMMC, Ethernet, USB, I2C, GPIO, and — through device tree overlays — ADC, SPI and PWM.
Most of this is on its way upstream. Some of it has already landed. Upstreaming is slow, and there is no reason for the patches to sit in a queue in the meantime.
Credit, first
The part that makes the board boot at all is not mine. The ti_sysc / clock / simple-pm-bus rework for AM335x is Oskar Holmlund’s (oh@FreeBSD.org), posted to FreeBSD Phabricator as D46703, D46712, D46713, D46714, D41888, D46722, D46723 and D46746. Benny Goemans (github.com/malavon) rebased that stack onto stable/14 and carried a musb clock-name hunk that the narrower Phabricator diffs had lost.
Two of those landed in main on 2026-08-28 while this was being written — D46712 and D46713. D46714 is accepted and waiting. If you want to help, the most useful thing you can do is test the remaining revisions and say so on Phabricator.
My own contribution is layers 1 through 3 below: bring-up fixes, two new drivers, and an experimental USB DMA offload.
What actually works, and where
| 14.x | 15.x | current (16.0) | |
|---|---|---|---|
| Boots to multi-user on hardware | Yes | Yes | No — see below |
| buildkernel KERNCONF=AM335X | Yes | Yes | Yes |
| Platform prerequisite applies | Yes | Yes | Partly — config only |
| Driver layers apply and compile | Yes | Yes | Yes |
| Device attach failures | 0 | 0 | untested |
| Reboot soak | untested | 6 clean cycles | untested |
If you are going to try current first
A fair number of people will, so let me be direct about what you get.
current builds. Verified in a clean room: pristine main at 6e1436d74827, apply the eight current/ patches in order, and buildkernel KERNCONF=AM335X produces a 10.4 MB kernel with ti_rng.o, ti_edma3.o, ti_sdhci.o, musb_otg.o, am3359_cppi41.o and am335x_musb.o all compiled in. If you want to check that something has not rotted against current, that is enough to work with.
Getting there needed three things beyond the drivers, and one of them is a finding worth passing on.
main’s sys/arm/ti does not currently compile
Not because of anything in this bundle. D46712 “Remove clock_common.*” was committed on 2026-08-28 as 541aa93794e6. Its replacement, D46723, has not been committed. Five files in main still call the helpers that removal took away:
sys/arm/ti/ti_sysc.c (2 uses)
sys/arm/ti/clk/ti_divider_clock.c (13 uses)
sys/arm/ti/clk/ti_dpll_clock.c (13 uses)
sys/arm/ti/clk/ti_gate_clock.c (13 uses)
sys/arm/ti/clk/ti_mux_clock.c (13 uses)
All five are standard in files.ti, so no kernel configuration can leave them out. Nobody upstream trips over it because AM335x was removed from armv7 GENERIC in 3416e102c4e9, so no in-tree kernel config compiles that directory at all. The breakage is invisible precisely because the hardware was dropped.
0b-restore-clock-common.diff bridges the window: it restores the two files verbatim from 541aa93794e6^ and puts the .c back in files.ti. That is
The other two:
current does not boot a board. Booting also needs the
Refreshing D46723 is its author’s call. Publishing a forked rebase of someone’s in-review work is not a favour to them, so there is no rebase of it in this bundle. If you want a running board today, use the 14.x or 15.x lane.
One more thing about current. main is 16.0-CURRENT, and 32-bit ARM is on notice in exactly that release:
sys/kern/init_main.c:397
"WARNING: 32-bit kernels are deprecated and may be removed in FreeBSD 16.0."
armv7 is Tier 2. Anything not landed before the 16.0 branch dies with the architecture. That is the real deadline on all of this.
What you need
- A BeagleBone Black. Mine is a rev B3, AM3358 ES2.0.
- A FreeBSD build host. Cross-building is the only sane option — do not try to build on the board. Mine is amd64, 8 cores, 32 GB.
- A 3.3V USB serial adapter. There is no video console worth using and you will need the console before you need the network.
- A microSD card, 4 GB or larger.
Everything below runs unprivileged. The image pipeline uses
Wiring the serial console
The console is on the J1 header, not P9. Six pins near the P9 side of the board, next to the “BEAGLEBONE” silkscreen.
J1-1 GND -> adapter GND
J1-4 board RX -> adapter TX
J1-5 board TX -> adapter RX
115200 8N1
Do not connect the adapter’s VCC pin. The board powers itself; feeding 3.3V or 5V back into J1 is a good way to damage it.
1. Get the source
The patches are cut against specific commits.
git clone https://git.freebsd.org/src.git /usr/src-bbb
cd /usr/src-bbb
# 14.x lane
git checkout b88d20eb0241 # stable/14, 2026-08-21
# 15.x lane
git checkout c41e308ef27b # stable/15, 2026-08-23
# current lane (driver layers only)
git checkout 6e1436d74827 # main, 2026-09-04
They will apply to nearby commits too. These are the ones I tested.
2. Apply the patches
The set is layered so you can stop where you like. Apply them in order; each assumes the one before it.
cd /usr/src-bbb
git apply /path/to/14/0-platform-prereq.diff
git apply /path/to/14/1-bringup-fixes.diff
git apply /path/to/14/2-drivers.diff
git apply /path/to/14/3-usb-dma-experimental.diff # optional
The
Layer 0 — platform prerequisite
Holmlund’s stack plus the AM335X kernel config. Without this the board does not boot. Credit as above. If you are only interested in getting a board running, this is the layer that matters.
Layer 1 — bring-up fixes
Small things that were wrong or bit-rotted. The one worth naming: three TI clock drivers took a register offset from the wrong node, which faults at boot with
Layer 2 — drivers
Two additions, both upstream and awaiting review:
- SD/MMC data phase through EDMA3 (D59124, D59125). The SDHCI controller on this part cannot do its own DMA, so the driver was doing PIO for every sector. Handing the data phase to EDMA3 gives 98% of PIO throughput at 1/15 the CPU and 1/40 the interrupt time. That is the single biggest quality-of-life change here — the board stops spending itself on the SD card.
- Hardware random number generator (ti_rng). AM335x has a SafeXcel EIP-75 TRNG that FreeBSD never used. It registers with random(4) as a fast entropy source. Measured over 2 MB of raw output: entropy 7.999908 bits/byte, chi-square p 30%, serial correlation −0.000718.
Layer 3 — experimental USB DMA
Every knob in this layer is off by default. It offloads USB bulk transfers to the AM335x CPPI 4.1 DMA engine. On a fast device: 19.6 MB/s at 24.1 CPU-ms/MB, against 12.2 MB/s at 39.3 for PIO — 61% more throughput for 39% less CPU per byte.
It is published as experimental for honest reasons. It fixes a real wedge — sustained bulk writes would hang a mass storage device within tens to hundreds of megabytes — and after the fix it survived 7 consecutive 512 MB runs and 3.5 GB total with zero failures. But the frame-interrupt handling is not refcounted across endpoints, which is correct for one active DMA endpoint and latent for several. There are known cosmetic issues still to clean up. Enable it if you want to test it, not if you want a quiet machine:
sysctl hw.musb_dma=1 hw.musb_tx_dma=1
Bonus: a GEOM fix that is not BBB-specific
3. Build the kernel and world
cd /usr/src-bbb
export MAKEOBJDIRPREFIX=/usr/obj-bbb
make -j8 buildworld TARGET=arm TARGET_ARCH=armv7
make -j8 buildkernel TARGET=arm TARGET_ARCH=armv7 KERNCONF=AM335X
World takes a while. The kernel alone is a few minutes and is all you need if you are iterating on drivers.
4. Build a bootable image, without root
You need
pkg install u-boot-beaglebone
# installs into /usr/local/share/u-boot/u-boot-beaglebone/
Stage the root filesystem.
ROOTFS=/usr/bbb/rootfs
make installworld distribution installkernel \
TARGET=arm TARGET_ARCH=armv7 KERNCONF=AM335X \
DESTDIR=$ROOTFS -DNO_ROOT
Write the config files. Serial console is not the default and you will see nothing without it:
# $ROOTFS/boot/loader.conf
console="comconsole"
comconsole_speed="115200"
boot_verbose="YES"
# $ROOTFS/etc/fstab
/dev/ufs/rootfs / ufs rw 1 1
# $ROOTFS/etc/rc.conf
hostname="bbb"
ifconfig_cpsw0="DHCP"
sshd_enable="YES"
Any file you create by hand must be declared in METALOG or
for f in boot/loader.conf etc/fstab etc/rc.conf; do
echo "./$f type=file uname=root gname=wheel mode=0644" >> $ROOTFS/METALOG
done
# -DNO_ROOT never emits some debug directories, and makefs needs parents
# before children. Dropping usr/lib/debug fixes both, and saves 457 MB.
rm -rf $ROOTFS/usr/lib/debug
grep -v '^\./usr/lib/debug' $ROOTFS/METALOG | sort -u > $ROOTFS/METALOG.sorted
Build the FAT boot partition:
FATDIR=/usr/bbb/fat
mkdir -p $FATDIR/EFI/BOOT
cp /usr/local/share/u-boot/u-boot-beaglebone/MLO $FATDIR/
cp /usr/local/share/u-boot/u-boot-beaglebone/u-boot.img $FATDIR/
cp $MAKEOBJDIRPREFIX/usr/src-bbb/arm.armv7/stand/efi/loader_lua/loader_lua.efi \
$FATDIR/EFI/BOOT/bootarm.efi
cp -R $ROOTFS/boot/dtb $FATDIR/
makefs -t msdos -o fat_type=16 -o sectors_per_cluster=8 \
-o media_descriptor=240 -o reserved_sectors=1 \
-s 50m /usr/bbb/boot.fat $FATDIR
Then the UFS root, a BSD label around it, and the MBR:
makefs -t ffs -F $ROOTFS/METALOG.sorted -o version=2,label=rootfs \
-s 3g /usr/bbb/root.ufs $ROOTFS
mkimg -s bsd -p freebsd-ufs:=/usr/bbb/root.ufs -o /usr/bbb/root.bsd
mkimg -s mbr \
-p fat32lba:=/usr/bbb/boot.fat \
-p freebsd:=/usr/bbb/root.bsd \
-a 1 -o /usr/bbb/bbb.img
The boot ROM rule that will waste your evening
The AM335x boot ROM requires that the MBR partition entry’s sector count equal the FAT filesystem’s sector count. If they differ it rejects the card silently — no console output at all, not even a character. A dead-looking board and a perfectly good kernel.
Two consequences, both counter-intuitive:
- Do not pass
or-Tto-H. CHS geometry makes it round the partition size up to a track boundary (102400 → 102438 sectors) and that breaks the rule.mkimg
-
is load-bearing. With 512-byte clusters,sectors_per_cluster=8caps a 50 MiB FAT16 volume at 66069 sectors inside a 102400-sector partition, because of FAT16’s 65525-cluster limit. 4 KiB clusters let the filesystem span the whole partition. Also:makefsmust be decimalmedia_descriptor;240refusesmakefsas “illegal number”.0xf0
And two pieces of widely repeated folklore that are false. The known-good 13.5 image violates both and boots fine:
- “MLO must be the first file in the root directory.” It need not be.
- “HiddenSectors must equal the partition LBA.” It need not.
5. Write the card
dd if=/usr/bbb/bbb.img of=/dev/da0 bs=1m conv=sync status=progress
Then verify the card, not the file. Two traps here, both of which have cost me real time:
sh verify-image.sh /dev/da0
ok MBR signature @0x1FE 55aa
ok partition 1 active flag 80
ok partition 1 type (FAT) 0x0c
info partition 1 start LBA 2048
ok BPB BytesPerSector 512
info BPB SectorsPerCluster 0x08
info FAT filesystem sectors 102400
ok MBR p1 size == FAT sector count 102400
All documented boot ROM requirements satisfied.
The last check is the one that matters. If it says
6. Boot it
Open the serial port before setting the speed. This one is genuinely surprising: opening
cat /dev/cuaU0 > console.log &
stty -f /dev/cuaU0 115200 cs8 -parenb -cstopb clocal raw -echo
Insert the card, apply power, and you should see the ROM hand off to MLO, then u-boot, then the FreeBSD loader.
FreeBSD 15.1-STABLE (AM335X) arm
CPU: Cortex-A8 r3p2 (ECO: 0x00000000)
cpsw0: <Three-Port Switch Ethernet Subsystem> ...
ti_rng0: <TI EIP-75 random number generator> ...
random: registering fast source TI EIP-75 RNG
Iterating without touching the card
Pulling the card for every kernel build gets old immediately. The board can fetch over HTTP:
# build host
cp $MAKEOBJDIRPREFIX/usr/src-bbb/arm.armv7/sys/AM335X/kernel /srv/deploy/kernel
python3 -m http.server 8080 --directory /srv/deploy &
# on the board, over the console
cp -a /boot/kernel /boot/kernel.working
fetch -o /tmp/k http://BUILDHOST:8080/kernel
cp /tmp/k /boot/kernel/kernel && sync && reboot
Use
What is not done
Stated plainly, because a port that only lists its wins is not much use:
- current does not boot, for the reason above.
- No reboot soak on 14.x. The six clean cycles were on 15.x.
- PRUSS — needs a PRU toolchain, not attempted.
- eQEP — no FreeBSD driver exists at all.
- RTC — no battery fitted, so untested.
- SPI loopback and electrical verification at the header pins — needs a jumper and instruments.
- The USB DMA layer has the open items listed above. It is off by default for a reason.
Upstream status
If you would rather wait for this to arrive in the tree than patch it yourself, these are the things to watch. Testing them and saying so on the review is worth more than another +1.
| Change | Where | Status |
|---|---|---|
| Remove clock_common | D46712 | Committed 2026-08-28 |
| AM335x clock names | D46713 | Committed 2026-08-28 |
| syscon for PRCM/PRM | D46714 | Accepted |
| AM335X kernel config | D46703 | Needs review |
| ti,clksel driver | D41888 | Needs review |
| simple-pm-bus | D46722 | Needs review |
| TI clock rework | D46723 | Needs review — blocks current |
| cpsw compatible | D46746 | Needs review |
| sys/intr.h include | D59118 | Needs review |
| softc member name | D59119 | Needs review |
| clksel register offset | D59120 | Needs review |
| EDMA3 split binding | D59124 | Needs review |
| SDHCI EDMA3 offload | D59125 | Needs review |
| geom_part access leak | PR 297777 | In triage |
| Evidence for the AM335x work | PR 297800 | Open |
The licence is FreeBSD’s: two-clause BSD.