FreeBSD 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.x15.xcurrent (16.0)
Boots to multi-user on hardwareYesYesNo — see below
buildkernel KERNCONF=AM335XYesYesYes
Platform prerequisite appliesYesYesPartly — config only
Driver layers apply and compileYesYesYes
Device attach failures00untested
Reboot soakuntested6 clean cyclesuntested

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

git show
of the parent commit and nothing else — no design input from me. Do not send it upstream. When D46723 lands it rewrites every one of those call sites and this patch has to be dropped.

The other two:

0-kernel-config.diff
is Holmlund’s AM335X config (D46703) plus one line of mine,
device ti_rng
files.ti
marks that driver
optional ti_rng
, so without a device line it is silently not compiled at all. And
1-bringup-fixes.diff
is D59118 and D59119, without which
ti_gpio.c
fails on an incomplete
struct intr_irqsrc
. That one is a nice confirmation that D59118 is still a live bug in main, not a stale report.

current does not boot a board. Booting also needs the

ti_sysc
/ clock /
simple-pm-bus
stack, and the clock rework in it (D46723) no longer applies to
main
. The reason is worth stating precisely, because it is a good problem to have: D46712 and D46713 were committed on 2026-08-28, and in landing they moved the clock code past what D46723 was written against. The stack is not stalled, it is arriving out of order.

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

make -DNO_ROOT
,
makefs -F METALOG
and
mkimg
rather than
release.sh
, which needs
mdconfig
and root. The only step that wants privilege is writing the finished image to the card.

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.

stable/14
and
stable/15
move, so check out specific commits within them that the patch sets are guaranteed to apply against.


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

current/
lane is split differently —
2a-ti_edma3.diff
,
2b-ti_sdhci.diff
,
2c-ti_rng.diff
, then
3-usb-dma-experimental.diff
— because
main
refactored
random(4)
and the RNG driver needed a port. There is no layer 0 or 1 there, for the reason given above.

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

FAR=ffd00001
the moment a
ti,clksel
parent is involved. Upstream as D59120, with D59118 and D59119 alongside.

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

geom_part-access-leak.diff
fixes a 13-year-old access leak in
g_part_spoiled()
that wedges every subsequent USB disk attach after a partition table is resized. It is machine-independent, applies to all three branches, and has nothing to do with the BeagleBone — I found it while chasing something else. Filed as PR 297777.

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

AM335X
is the kernel config added by layer 0. It does not exist in an unpatched tree — this is the config from D46703, and it is why AM335x support cannot simply be switched on in GENERIC any more.

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

MLO
and
u-boot.img
from the port — do not scavenge them out of an old 13.x image:


pkg install u-boot-beaglebone
# installs into /usr/local/share/u-boot/u-boot-beaglebone/

Stage the root filesystem.

-DNO_ROOT
records ownership and permissions into a
METALOG
file instead of applying them, so none of this needs privilege:


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

makefs
silently leaves it out of the image. This is the single most annoying thing about the unprivileged path:


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
    -T
    or
    -H
    to
    mkimg
    .
    CHS geometry makes it round the partition size up to a track boundary (102400 → 102438 sectors) and that breaks the rule.
  • sectors_per_cluster=8
    is load-bearing.
    With 512-byte clusters,
    makefs
    caps 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:
    media_descriptor
    must be decimal
    240
    ;
    makefs
    refuses
    0xf0
    as “illegal number”.

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.

-a 1
marks partition 1 active, which the ROM does require.

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:

dd
can exit 0 having written nothing at all if the media is absent or the device node is stale, and
dd bs=1
fails on raw devices on FreeBSD because reads must be sector-aligned.

verify-image.sh
is in the bundle and does this for you. Point it at the card, not the image — checking the file you just built tells you nothing about what reached the media:


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

WILL NOT BOOT
, the card will produce a completely silent board — no console output at all — and you will spend the evening suspecting your kernel.

6. Boot it

Open the serial port before setting the speed. This one is genuinely surprising: opening

/dev/cuaU0
applies the termios from
/dev/cuaU0.init
, which is 9600 and needs root to change. If you
stty
first and open second, your settings are silently discarded and you get total silence from a board that is booting perfectly.


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

nextboot -k kernel.working
to arm a one-shot fallback so a bad kernel recovers itself on the next reboot instead of stranding you.

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.

ChangeWhereStatus
Remove clock_commonD46712Committed 2026-08-28
AM335x clock namesD46713Committed 2026-08-28
syscon for PRCM/PRMD46714Accepted
AM335X kernel configD46703Needs review
ti,clksel driverD41888Needs review
simple-pm-busD46722Needs review
TI clock reworkD46723Needs review — blocks current
cpsw compatibleD46746Needs review
sys/intr.h includeD59118Needs review
softc member nameD59119Needs review
clksel register offsetD59120Needs review
EDMA3 split bindingD59124Needs review
SDHCI EDMA3 offloadD59125Needs review
geom_part access leakPR 297777In triage
Evidence for the AM335x workPR 297800Open

The licence is FreeBSD’s: two-clause BSD.