# Flashing an STM32F7 SOM from a brintOS shell Handoff, 2026-09-25. Emcraft **STM32F7-SOM** on a **SOM-BSB-EXT** baseboard, programmed from a terminal inside a brintOS machine running in a browser tab. The board needs two images, and today they are not written from the same place: | Half | What is written | Written from | State | |---|---|---|---| | U-Boot | eNVM at `0x08000000`, 119,416 B | the brintOS shell, over JTAG through WebUSB | works, one command — §4 | | Linux kernel (`rootfs.uImage`) | NOR at `0x600A0000`, 5,785,356 B | a host, at the board's U-Boot prompt, over TFTP | works — §6. Not yet from the brintOS shell, for two separate reasons — §7 | **What a brintOS machine can and cannot do today.** It drives the JTAG adapter through WebUSB, and that is the whole of it. **Networking to the board and a serial console are not supported yet** — a machine has no route to your bench network and cannot open the board's UART. So §4 runs in the brintOS shell, while §5 (reading the console) and §6 (TFTP, and typing at the U-Boot prompt) are done from the Mac. §7 says what is being done about it. Everything below was run on our bench on 2026-09-25 and the outputs are quoted from those runs. ## 1. What you need, and what connects where Two USB cables and, for the kernel half, Ethernet. Everything lands on your Mac; nothing needs a second computer, and the board takes no separate power supply. Mac (Chrome) SOM-BSB-EXT + STM32F7-SOM | +-- USB -- Olimex ARM-USB-TINY-H ------- P3, the JTAG header U-Boot, §4 | (or ARM-USB-OCD-H, §4.1) | `-- USB A-to-mini-B -------------------- P1, mini-USB §5, §6 console AND power Ethernet, for §6 only -- either way round: A. board --- your local network --- a TFTP server already on it B. board --- straight cable --- the Mac, with the Mac serving TFTP | Link | Purpose | Needed for | |---|---|---| | Olimex JTAG adapter: USB to the Mac, 20-pin ribbon to **P3** | the only path that writes eNVM | §4 — the U-Boot half. This is the one the brintOS machine drives, through Chrome's WebUSB. | | Plain USB A-to-mini-B cable, Mac to **P1** | powers the board **and** carries its console, 115200 8N1 | Power: everything. Console: §5 (confirm the banner) and §6 (typing U-Boot commands) — the U-Boot flash itself does not use it. | | Ethernet, from the board's RJ45 either into your local network or straight to the Mac | TFTP transfer of the kernel | §6 only. **A TFTP server has to be on that network.** On the local network it is whatever server you already run; on a direct link it is the Mac — §6.1. | P1 carries power and serial over the one cable, so two things follow: use a real data cable (a charge-only mini-USB cable powers the board and gives you no console), and **pulling P1 is how you power-cycle the board**. The USB-serial bridge is on the baseboard — ours enumerates as an FTDI FT232R — so no special serial cable is needed. A USB-C-only Mac needs an adapter or hub for both: the Olimex is USB-A, and P1 wants A-to-mini-B unless you have a C-to-mini-B cable. You also need **`rootfs.uImage`** on the Mac for §6 (5,785,356 B), and a machine: ### 1.1 Your own copy of the machine — fork it The station image lives in a public machine of ours, `https://brintos.io/sergei/brintos-10-f7-station`. **Fork it into your own account before you use it.** Running ours directly gives you a read-only root: the writable `live` root of a machine belongs to its owner, and a visitor's session gets the published snapshot as it stands. 1. Sign in to brintos.io in Chrome. 2. Open `https://brintos.io/sergei/brintos-10-f7-station` and press **Fork** (the direct link is `https://brintos.io/new?fork=sergei/brintos-10-f7-station`). Take it from **`latest`** — the published snapshot, which is the one that carries the station tools. 3. Work in *your* machine from then on: `https://brintos.io//brintos-10-f7-station`. **Launch** is on that page. A fork is instantaneous — it copies a root pointer, not the image — so this costs you nothing but the click. What it buys is a `live` root you own and can write to, which is what a session needs. Two snapshots exist on ours: `live`, which is our working root and not yours to write, and `latest`, the published snapshot (captured 2026-09-25 14:57 UTC). The snapshot selector next to the machine name is where you see which one a page is showing. **What that snapshot holds:** a rootfs built 2026-09-24 with openocd 0.12 and the board config, its station tools refreshed 2026-09-25 (`som-program-f7`, the `mfg-jtag.cfg` seam, the board config and the self-test), and openocd's Olimex interface configs — `olimex-arm-usb-tiny-h`, `olimex-arm-usb-ocd-h`, `olimex-arm-usb-ocd`. Verified in the guest after publishing: `som-program-f7` reads back sha256 `736989c5…`, the same bytes as the layer's. ## 2. Let the browser reach the adapter A machine attaches a USB device only if Chrome can both **open** the device node and **claim** its interfaces. The frame reports either failure with one sentence: *"granted, but not attached: the OS driver owns this device, so the browser cannot claim it"*. ### 2.1 On macOS The node-permission half does not exist — macOS has no udev and Chrome needs no rule to open a USB device. What can still bite is Apple's FTDI serial driver attaching to the adapter's channels, which is the second failure above. Plug the Olimex in and look: ls /dev/cu.usbserial-* Entries that appear when the Olimex is plugged in (and vanish when it is unplugged) mean macOS's own driver has taken it. Chrome may then be refused the claim, with the message above. **We have not run this on a Mac** — our bench is Linux, so treat this as the one step that may need a round trip. If the claim is refused, tell us what the frame said and we will work it out; the classic workaround is unloading the driver (`sudo kextunload -b com.apple.driver.AppleUSBFTDI`), which recent macOS may refuse. Failing that, the U-Boot half runs from any Linux host with the same command, and only that half needs the browser at all. P1's serial bridge is a different USB device and stays with macOS's driver — that is what you want; it is how you read the board (§5). ### 2.2 On a Linux host Two host problems, either of which blocks the claim: - the node `/dev/bus/usb//` is `root:root 0664`, so `open()` fails; - `ftdi_sio` is bound to the adapter's interface, so `claimInterface()` fails. `lsusb` lists the device in both cases — enumeration does not need the claim, so a device you can see is not a device the browser can use. One-off repair, as root (both revert on replug): sudo chmod 666 /dev/bus/usb/003/010 # your bus/dev from lsusb echo -n '3-4:1.1' | sudo tee /sys/bus/usb/drivers/ftdi_sio/unbind Permanent, tested on our host 2026-09-17: # /etc/udev/rules.d/70-brintos-webusb.rules SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTRS{idVendor}=="15ba", ATTRS{idProduct}=="002a", MODE="0660", GROUP="plugdev", TAG+="uaccess" ACTION=="bind", SUBSYSTEM=="usb", DRIVER=="ftdi_sio", ATTRS{idVendor}=="15ba", ATTRS{idProduct}=="002a", RUN+="/bin/sh -c 'echo -n %k > /sys/bus/usb/drivers/ftdi_sio/unbind'" `udevadm control --reload`, then replug. Use `002b` in place of `002a` for an ARM-USB-OCD-H. Do not apply the second rule to a serial adapter — unbinding `ftdi_sio` from one takes its `/dev/ttyUSB*` away. ## 3. Attach the adapter to the machine 1. Open **your fork's** page (§1.1) and **Launch**. 2. Go to the **USB devices** tab, click **Attach USB device**, pick the Olimex in Chrome's chooser. 3. `lsusb` in the machine's console should now list `15ba:002a`. If an attach was refused once, **reload the machine page before re-granting**. Chrome keeps a stale handle and every later grant then reports the frame's other message, "the device disconnected before it could attach — replug it and try again", while the host shows the device present with no driver bound. ## 4. Write U-Boot — from the brintOS shell som-program-f7 That is the whole command. It resolves the image, backs up the eNVM, programs, verifies, reads the flash back and compares. Output from a real run (elisions marked): som-program-f7: image: /usr/share/brintos-som/stm32f7/u-boot.bin (built-in, 119416 bytes) som-program-f7: sha256: 6e060662217fcf52c2a5d7a300616e9bbec20b6e2cfe42eb219469416e844096 som-program-f7: provenance: /usr/share/brintos-som/stm32f7/PROVENANCE | version: U-Boot 2010.03-cortexm-2.1.2 (Oct 23 2016 - 14:21:51) | … som-program-f7: target: eNVM 0x08000000 on /usr/share/openocd/scripts/board/emcraft-som-stm32f7.cfg som-program-f7: speed: the board config's default after reset som-program-f7: adapter: the seam's default in /usr/share/openocd/scripts/mfg-jtag.cfg som-program-f7: programming ... Info : JTAG tap: STM32F756.cpu tap/device found: 0x5ba00477 (mfg: 0x23b (ARM Ltd), part: 0xba00, ver: 0x5) Info : [STM32F756.cpu] Cortex-M7 r0p0 processor detected ** Programming Started ** ** Programming Finished ** ** Verify Started ** ** Verified OK ** ** Resetting Target ** som-program-f7: the board was reset and is running what was just written. som-program-f7: PASS -- wrote /usr/share/brintos-som/stm32f7/u-boot.bin to 0x08000000 One `PASS`/`FAIL` line ends every run, and the `image:` + `sha256:` lines are the record of what the board was given. ### 4.1 Flags | Flag | Use | |---|---| | `--adapter olimex-arm-usb-ocd-h` | drive an ARM-USB-OCD-H. Takes a bare name, a filename, or a path under `/usr/share/openocd/scripts`. Installed: `olimex-arm-usb-tiny-h`, `olimex-arm-usb-ocd-h`, `olimex-arm-usb-ocd`, `olimex-jtag-tiny`, `olimex-arm-jtag-swd`. An unknown name fails before any hardware is touched. | | `--speed 1000` | JTAG clock after reset, kHz. Default 2000, which is safe out of reset (the core runs from the 16 MHz HSI and the adapter wants to stay under F_CPU/6) and is what the write costs: 141 KiB/s at 2 MHz against 11.8 KiB/s at 150 kHz. | | `--image FILE` | program something else. Resolution order is `--image`, then `/mnt/media/stm32f7/u-boot.bin`, then the built-in. | | `--no-backup` | skip the 1 MB pre-flash eNVM dump. Don't, on a board whose contents you do not already have — that dump is the only copy of what it arrived with. It lands in `/var/tmp/envm-.bin`. | | `--no-readback` | trust openocd's own verify instead of re-reading the flash. | | `--dry-run` | resolve and print the image, touch no hardware. | **An ARM-USB-OCD-H needs that flag and nothing else** — no file edited, no snapshot published. `--adapter` sets `BRINTOS_JTAG_IFACE` before the board config is sourced, and `/usr/share/openocd/scripts/mfg-jtag.cfg` — which every board config sources, and which names the default — reads it there. Both adapter configs ship in the image. That indirection exists because a running machine's filesystem is per-session: an edit inside a machine holds for that session and survives it only if you publish a snapshot of your fork, which copies the image. A flag costs neither. ### 4.2 How long it takes Measured in the guest, `--no-backup --no-readback`: **62.2 s** on the first run of a fresh session, then **21.2 s** and **17.9 s**. The same command on a host is 2.9 s. So about 40 s of that first run is the cold exec of a 26 MB openocd binary, not JTAG traffic, and it does not repeat. A run that seems to hang for half a minute before printing anything is this, not a dead adapter. ## 5. Confirm on the serial console Open the console — that is the P1 cable, and on macOS it shows up as a `/dev/cu.usbserial-*` entry: ls /dev/cu.usbserial-* screen /dev/cu.usbserial-XXXX 115200 # `Ctrl-A` then `k` to quit (`picocom -b 115200 /dev/cu.usbserial-XXXX` does the same if you have it. On our Linux bench the same device is `/dev/ttyUSB`.) Reset the board and watch. Expected, verbatim: U-Boot 2010.03-cortexm-2.1.2 (Oct 23 2016 - 14:21:51) CPU : STM32F7 (Cortex-M7) Freqs: SYSCLK=200MHz,HCLK=200MHz,PCLK1=50MHz,PCLK2=100MHz Board: STM32F7 SOM Rev 1.A, www.emcraft.com DRAM: 32 MB Flash: 16 MB … Hit any key to stop autoboot: 1 STM32F7-SOM> `STM32F7-SOM>` is this build's prompt. `STM32F7-SOM-UCL>` means the board is running the LCD starter kit's cm-2.4.0 build instead — a different product, not an older copy of this one. ## 6. Write the kernel — host side, over TFTP U-Boot carries a macro for exactly this, `update`: update=tftp ${image};prot off ${flashaddr} +${filesize};era ${flashaddr} +${filesize}; cp.b ${loadaddr} ${flashaddr} ${filesize};setenv kernelsize ${filesize}; setenv filesize; setenv fileaddr;saveenv It fetches by TFTP, so this half needs **a TFTP server the board can reach**. U-Boot addresses it statically out of its own environment — no DHCP is involved either way. ### 6.1 The network and the TFTP server **A. The board on your local network.** Plug the board's RJ45 into the same network as a TFTP server you already run, put `rootfs.uImage` under that server's root, and give the board a free address on that subnet: setenv ipaddr setenv serverip setenv netmask 255.255.255.0 This is how our bench runs it: `serverip=192.168.1.5`, board `192.168.1.123`. **B. Straight cable to the Mac.** No switch, no DHCP, no router — the Mac is then the TFTP server: 1. System Settings → Network → the Ethernet interface → Configure IPv4: **Manually**, address `192.168.7.1`, mask `255.255.255.0`, no router. 2. On the board: setenv ipaddr 192.168.7.2 setenv serverip 192.168.7.1 setenv netmask 255.255.255.0 3. Serve the file. macOS has carried a `tftpd` serving `/private/tftpboot`; if `/System/Library/LaunchDaemons/tftp.plist` exists on your version, sudo launchctl load -w /System/Library/LaunchDaemons/tftp.plist sudo cp rootfs.uImage /private/tftpboot/ If it does not, any TFTP server will do — Homebrew's `tftp-hpa`, or a throwaway one such as `python3 -m py3tftp` run in the directory holding the image. Either way the board asks for whatever `image` names, **relative to the server's root**, so keep the two consistent. A straight cable is fine for B; both ends auto-MDIX. U-Boot prints `Auto-negotiation...completed` and `link UP (100/Half)` when the link is good — if you get neither, nothing below will work, and it is the cable or the addressing, not the board. ### 6.2 Writing it **Read the board's own `flashaddr` before you use it.** The NOR map on this board is not what the default environment compiled into U-Boot says: | NOR address | Contents | |---|---| | `0x60000000` | the saved U-Boot environment, 128 KB | | `0x60020000` | the splash BMP (`splashaddr`, `splashimage`) | | `0x600A0000` | the kernel — `flashaddr` in the **saved** environment | The compiled-in default is `flashaddr=60020000`, which is the splash. A board whose environment sector has been erased will therefore write the kernel over the splash and then try to boot a BMP. So: STM32F7-SOM> printenv flashaddr flashaddr=600A0000 Then name the file and run the macro: setenv image rootfs.uImage # relative to the TFTP server's root run update Ours took **38 s** for 5,785,356 B, TFTP included (our server is `192.168.1.5` and keeps the image under `psl/stmf7/cm-2.1.2/`): Filename 'psl/stmf7/cm-2.1.2/rootfs.uImage'. Load address: 0xc0007fb4 … Bytes transferred = 5785356 (58470c hex) Un-Protected 45 sectors … Erased 45 sectors Copy to Flash... done Saving Environment to Flash... done `update` ends with `saveenv`, so the board now records what it holds — `kernelsize=58470C` and the image name. Check it against the file rather than trusting the copy: STM32F7-SOM> crc32 ${flashaddr} ${kernelsize} CRC32 for 600a0000 ... 6062470b ==> 87504267 and the same CRC32 over the file on the host: python3 -c "import zlib,sys;print('%08x' % zlib.crc32(open(sys.argv[1],'rb').read()))" rootfs.uImage 87504267 Boot it: STM32F7-SOM> run flashboot ## Booting kernel from Legacy Image at 600a0000 ... Image Name: Linux-4.2.0-cortexm-2.1.2 Verifying Checksum ... OK … init started: BusyBox v1.17.0 (2016-10-25 20:08:57 MSK) / # `bootcmd=run flashboot`, so a plain reset now boots from NOR. Two console habits worth knowing: - **A bare Enter at the U-Boot prompt repeats the last command.** Ours re-ran `run update` that way. Harmless here, since the macro is idempotent; not harmless in general. - `bootdelay=1`, so there is one second to interrupt autoboot. To put a different kernel back, the same macro with a different name: `setenv image ; run update`. ## 7. Why the kernel half is not in the brintOS shell yet Three independent gaps, all being tracked: **No serial console from the guest.** Nothing in a brintOS machine reaches the SOM's UART yet, and `update` is a U-Boot command. A machine can drive JTAG but cannot type at a U-Boot prompt. **No networking from the guest to your bench.** A machine has no route to the network the board and the TFTP server sit on, so running the transfer from inside the guest is not an option either — even with a console. **The image cannot be got into the machine.** We tried to put `rootfs.uImage` into the machine's filesystem and it failed: - The **Files** tab exists only while the machine is *stopped*. A launched guest has no Files tab, so a file cannot be added to a running target at all. - With the machine stopped, uploading through the Files tab's own control fails at the last step. `POST …/fs/blobs/presign-batch` answers 200, and then every direct `PUT` to the blob store errors — the page logs `brintos-fs: flush error (will retry)` and then **`FLUSH failed: EIO`**. Nothing lands: paging the machine's whole filesystem afterwards — 5,526 entries — turns up no `rootfs.uImage`. Seen from our headless Chrome, and reported from an ordinary unproxied browser too. Being filed with brintos.io. So today anything the station must write has to be **in the image**, which is how U-Boot gets there: a recipe installs it at `/usr/share/brintos-som/stm32f7/` with a `PROVENANCE` file beside it. A 5.8 MB kernel could ride along the same way once there is something in the guest that can write it. ## 8. When it does not work | Symptom | Cause / what to do | |---|---| | Chrome's chooser offers no device | The host owns it — §2.1 on macOS, §2.2 on Linux. Seeing it in `lsusb` / System Information proves nothing. | | "granted, but not attached: the OS driver owns this device" | Same. Repair, then **reload the page** before re-granting. On macOS this is the case we have not tested — send us the message. | | "the device disconnected before it could attach" | A stale Chrome handle after an earlier refusal. Reload the page; a replug was not needed here. | | `Error: unable to open ftdi device` inside the guest | The machine has not been given the device, or it was revoked. Re-attach in the USB devices tab. | | `JTAG tap … tap/device found` missing, or `all ones`/`all zeroes` | Cable, orientation, or the board is unpowered. Nothing to do with the image. | | `som-program-f7: FAIL -- no such adapter config: …` | `--adapter` typo. The resolved path is in the message; the installed names are in §4.1. | | Nothing on the console, board seems dead | P1 is power as well as console. A charge-only mini-USB cable, or a hub that will not supply the port, gives you exactly this. Re-seat P1 — that is also the power-cycle. | | First run takes a minute with no output | The cold exec, §4.2. | | `halt` inside the guest hangs | Known: PID 1 is bash and `reboot(2)` is unimplemented. `sync`, then use the frame's **Halt**. | ## 9. What exactly gets written The image and its provenance ship together, and the recipe recomputes the hash at build time, so a binary swapped in without its metadata fails the build: image: u-boot.bin target: Emcraft STM32F7 SOM (STM32F756), eNVM at 0x08000000 version: U-Boot 2010.03-cortexm-2.1.2 (Oct 23 2016 - 14:21:51) prompt: STM32F7-SOM> origin: tftp://192.168.1.5/psl/stmf7/cm-2.1.2/u-boot.bin sha256: 6e060662217fcf52c2a5d7a300616e9bbec20b6e2cfe42eb219469416e844096 size: 119416 bytes The openocd in the image is `0.12.0+dev-00150-g91bd43134-dirty (2026-09-22-08:29)`, built for wasm32. The board config is `board/emcraft-som-stm32f7.cfg`, and it sources `mfg-jtag.cfg` — the one place the adapter is named.