# 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 it can open the board's serial console the same way, once P1's USB-serial bridge is granted to it (§5.1). **A USB Ethernet adapter works on brintos.io in Developer mode**, with a kernel that has the USB Ethernet drivers: the site's own HardwareJS release refused the adapter when last measured (2026-09-30), and Developer mode runs the one in this image instead (§7). A board fetching its kernel from the machine has not been tried, so §4 runs in the brintOS shell, §5 runs in either place, and §6's TFTP transfer is done from the Mac. §7 says what is being done about the rest. The console from the shell works in the browser: on 2026-09-30, in Chrome on a Linux host, a brintos.io machine took P1's bridge over WebUSB as `/dev/ttyUSB0`, `som-console` found it, and minicom — driven by Expect — exchanged lines with the board at 115200. On our bench the board's shell and its U-Boot both answer through that node, and a `reset` typed there brings the U-Boot banner back through it. On a Mac, Apple's own FTDI driver holds P1's bridge (§2.1), so on a Mac read the console from the Mac, as in §5. 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, or a SEGGER J-Link, §4.3) | `-- 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 | |---|---|---| | JTAG adapter — the Olimex, or a J-Link (§4.3): 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. A **SEGGER J-Link** carries a serial port of its own (two CDC interfaces) beside the vendor interface openocd drives. On a Mac those CDC interfaces may go to Apple's CDC driver, and whether Chrome is then allowed to claim the J-Link has not been tried. If the frame refuses it, send us its message, as above. 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). The same driver is why §5.1, the console from the brintOS shell, is not expected to work on a Mac: the machine would need to claim that bridge, and macOS does not release it. ### 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. **Except P1's bridge, when you want the console in the machine (§5.1).** Then unbind it for that one board only, keyed on the bridge's serial number (`lsusb -v -d 0403:6001 | grep iSerial`; each SOM-BSB-EXT has its own). Every other FTDI device keeps its host tty, and this board has none while the rule is installed: # /etc/udev/rules.d/71-brintos-som-console.rules -- one pair per board SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="A505CMFS", MODE="0660", GROUP="plugdev", TAG+="uaccess" ACTION=="bind", SUBSYSTEM=="usb", DRIVER=="ftdi_sio", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="A505CMFS", RUN+="/bin/sh -c 'echo -n %k > /sys/bus/usb/drivers/ftdi_sio/unbind'" Tested on our host on 2026-09-29 with board `A6014SHO`. The node there was already writable, so only the second line was needed; keep the first where it is not. **A one-off `unbind` does not stick for the bridge**: when Chrome claims the device it resets it, the kernel re-probes, and `ftdi_sio` binds again — so the machine never gets it. The `ACTION=="bind"` line is what holds, because it unbinds again on every re-probe. If you do not want the rule for good, put the same file in `/run/udev/rules.d/` instead of `/etc/udev/rules.d/` and `udevadm control --reload`: it lasts until the host reboots, and the board's host tty comes back after that without anyone having to remember to delete a rule. **A SEGGER J-Link** (`1366:0105`) is held differently: the host's `cdc_acm` binds the two interfaces of its serial port (`/dev/ttyACM0`), and openocd drives the third, vendor-specific one. Its node is usually writable already. SEGGER's own `99-jlink.rules`, and openocd's `60-openocd.rules`, make it `0666`. Our Chrome attach (2026-10-05) was measured with `cdc_acm` released from both interfaces. With `cdc_acm` still bound it has not been tried, and the claim refusal above is the likely result. Release it as root (reverts on replug; take the bus path from `lsusb -t`): echo -n '3-4:1.0' | sudo tee /sys/bus/usb/drivers/cdc_acm/unbind echo -n '3-4:1.1' | sudo tee /sys/bus/usb/drivers/cdc_acm/unbind `cdc_acm` binds again whenever the J-Link re-enumerates. A replug does that, and so did powering the board off under it (§8). Release it again before the next attach. The UCL-SOM-BSB starter kit's console is a CP2104, held by the host's `cp210x` instead of `ftdi_sio`. Its line is the same with the driver and ids changed: ACTION=="bind", SUBSYSTEM=="usb", DRIVER=="cp210x", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ATTRS{serial}=="", RUN+="/bin/sh -c 'echo -n %k > /sys/bus/usb/drivers/cp210x/unbind'" The CP2104 has been measured in a brintOS guest (bound by `cp210x`, read and written), but not yet attached from a browser, so this line is untested there. ## 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 (or the J-Link) in Chrome's chooser. 3. `lsusb` in the machine's console should now list `15ba:002a` (a J-Link: `1366:0105 SEGGER J-Link`). **Attach one device at a time, on the running machine.** The site's runtime puts devices granted before Launch on one port (§8). If Chrome still remembers a grant from an earlier session, such as P1's bridge, that device is attached at Launch too; detach it in the USB devices tab if this session does not need it. With a J-Link on P3, **do not unplug P1**: it is the board's power, and the J-Link dropped off USB when it was replugged (§8). 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; `--adapter jlink` drives a SEGGER J-Link (§4.3). Takes a bare name, looked up in `interface/` and then `interface/ftdi/`, 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`, and `jlink`. An unknown name fails before any hardware is touched. | | `--transport swd` | `jtag` or `swd`; the adapter must offer it. Default: the adapter's first, which is `jtag` for the Olimex and the J-Link and `hla_swd` for an ST-Link. | | `--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 the `workdir:` the run prints, as `envm-.bin`: the first writable of `/var/tmp`, `/var/backups` and `/tmp`, or `$SOM_BACKUP_DIR`. In `/tmp` it lasts only as long as the session; launch your fork as its owner, where `/var/backups` is writable and kept, to keep it. **On brintos.io, `/var/backups` is slow and can fail:** it is the machine's stored root, and on 2026-10-05 the 1 MiB backup took 201.8 s there against 69.2 s in `/tmp`, on a day the same store refused other writes with `EIO` ([linux-in-the-browser#203](https://gitlab.com/brintos/linux-in-the-browser/-/work_items/203)). For a backup you need only for this session, as in a write with restore, run `SOM_BACKUP_DIR=/tmp som-program-f7 …`. | | `--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. With the default backup and read-back, the first run in a fresh session took up to about 4½ minutes (Chrome, 2026-10-01). Let it run. ### 4.3 Through a SEGGER J-Link, over JTAG or SWD som-program-f7 --adapter jlink # JTAG som-program-f7 --adapter jlink --transport swd # SWD The J-Link goes on P3 like the Olimex, and attaches the same way (§3). Nothing else changes: `--adapter` names the J-Link's interface config, and the board config leaves the transport to the adapter unless `--transport` names one. Measured on 2026-10-05, in Chrome on brintos.io, with a J-Link V9 on our SOM-BSB-EXT. The eNVM was backed up (1 MiB in 70 s) and erased. U-Boot was written over JTAG and read back as the image. The backup was then written back over SWD and read back as the backup, and a final dump equalled the original: som-program-f7: adapter: interface/jlink.cfg (override) som-program-f7: transport: swd (override) ** Verified OK ** som-program-f7: readback: 14d65a34…7eb8b matches the image som-program-f7: PASS -- wrote /tmp/envm-orig.bin to 0x08000000 How long the pieces took on brintos.io (Chrome, the same day, a J-Link V9): - a 1 MiB eNVM dump into `/tmp`: about 70 s (14.6–15.0 KiB/s); - the erase of all eight sectors: 9 s; - U-Boot's write, verify and 119 KB read-back: a few tens of seconds; - the 1 MiB restore's read-back over SWD: 138 s (7.4 KiB/s). The JTAG clock is 150 kHz at connect and 2000 kHz after the reset each run makes (`adapter speed: 2000 kHz`), so the time goes to USB round trips, not to the clock. **The nRF91.** openocd here carries the nRF53/nRF91 flash driver, backported to this openocd, and the Nordic target configs under `target/nordic/` as well as `target/`. The nRF91 is SWD-only and the J-Link offers JTAG first, so select SWD before the target config: openocd -f interface/jlink.cfg -c "transport select swd" -f target/nordic/nrf91.cfg On a machine with no nRF91 attached, that parses and creates `nrf91.cpu` with two flash banks, `nrf91.flash` and `nrf91.uicr`. **An nRF9160-DK, measured on 2026-10-05.** Use an external J-Link on the DK's 10-pin **Debug in** header, through a 20-to-10-pin Cortex-Debug adapter, with the DK powered from its micro-USB and its debug switch on nRF91. **Do not use the DK's own on-board J-Link from brintos.io.** It also carries a mass-storage drive, Chrome refuses to let a page claim that, and the site's runtime then refuses the whole device ([brintos/hardwarejs#41](https://gitlab.com/brintos/hardwarejs/-/work_items/41)). Attach only the external J-Link. With it on Debug in: openocd -f interface/jlink.cfg -c "transport select swd" -f target/nordic/nrf91.cfg -c init -c "flash probe 0" -c shutdown prints `VTarget = 1.795 V`, `SWD DPIDR 0x6ba02477`, `Cortex-M33 r0p2` and `nRF9160-SICA 1024kB Flash, 256kB RAM`. `VTarget = 0.000 V` means the cable is on the wrong header or the wrong way round. Programming is openocd's own `program`; there is no `som-program-f7` for the nRF91: openocd -f interface/jlink.cfg -c "transport select swd" -f target/nordic/nrf91.cfg -c init -c "program 0x00000000 verify" -c "reset run" -c shutdown In Chrome on brintos.io, the DK's 1 MiB application flash was backed up (`dump_image`), erased (`flash erase_sector 0 0 last`, 52 s), programmed back with `Verified OK`, and read back equal to the factory image. That covers an nRF9160. The nRF9151 SOM uses the same nRF91 driver but has not been programmed here. ## 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. ### 5.1 From the brintOS shell Grant P1's bridge to the machine the way §3 grants the Olimex: in the **USB devices** tab, pick `FT232R USB UART` (its serial names the board). On a Linux host that first needs the per-board rule in §2.2; on a Mac see §2.1. Then, in the machine's console: som-console --list # every USB-serial port, with its USB id and serial som-console # open the SOM's console in picocom, 115200 8N1; C-a C-x leaves minicom -b 115200 -D $(som-console --find) # the same in minicom; C-a x leaves minicom, and Expect scripts that drive it, run in the machine on brintos.io. Both are built without shared libraries on purpose: the HardwareJS release the site serves cannot fork a program that needs two or more shared libraries (brintos/hardwarejs#50), and Expect forks every time it spawns. `som-console` finds the console by the bridge's USB id, not by tty number, which matters as soon as a second USB-serial device is granted: an ARM-USB-OCD-H has an RS232 channel of its own and takes a `/dev/ttyUSB*` too. Where two SOM-BSB-EXT bridges are granted at once it refuses to guess; name one with `--serial`. `som-console --find` prints the node alone, for scripts and Expect: som-console --list # every port, with its USB id and serial som-console --find --serial A505CMFS # one board by its bridge's serial SOM_CONSOLE_SERIAL=A505CMFS som-console # the same, from the environment som-console --dev /dev/ttyUSB1 # or name the node directly **With the Olimex attached, the console stays silent until the first openocd run.** Once a machine has opened the adapter, its reset line holds the SOM in reset until openocd sets the adapter's pins. `som-program-f7` does that as part of the write; on its own, `openocd -f board/emcraft-som-stm32f7.cfg -c init -c shutdown` releases it, and the board boots. Measured on our host, outside brintOS: the console answered with the adapter idle, went silent once it was opened, and came back after that openocd command. Only one side uses the console at a time: while the machine holds P1's bridge, the host has no tty for it. To hand it back, detach the device in the USB devices tab and replug P1. ## 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 Where each piece of the kernel half stands: **The serial console — no longer a gap.** A machine can now open the SOM's UART and type at the U-Boot prompt (§5.1), which is where `update` runs. **Networking from the guest — works on the site in Developer mode.** The full set-up, measurements and limits are in `/usr/share/brintos-som-station/usb-ethernet-and-tftp.md`. In short: it needs a kernel with the USB Ethernet drivers (`linux!6`; check with `ls /sys/bus/usb/drivers/cdc_ether`). A USB Ethernet adapter granted to the machine then binds as an `ethN` interface. The one adapter tried is a TP-Link USB 10/100 LAN (`2357:0602`), which presents a CDC-ECM configuration and binds through `cdc_ether`. CDC-NCM, DM9601 and RTL8153 adapters have drivers too and were not tried; adapters that need the `r8152`, ASIX or AX88179 drivers do not bind (`linux#137`). Without Developer mode the site launches its own HardwareJS release, which predates the fix (`hardwarejs#54`) and refuses the adapter with `bad CDC descriptors` (last measured on 2026-09-30); in Developer mode the machine runs the HardwareJS in this image's `/boot`, which has it. Give the adapter an address by hand — DHCP does not work in the browser yet — and use the image's TFTP client and server: ip link set eth1 up ip addr add
/24 dev eth1 tftp -m binary -c get # fetch a file curl --tftp-blksize 8192 -o tftp:/// # the same, in larger blocks /etc/init.d/tftpd-hpa start # serve /var/lib/tftpboot The server's options are in `/etc/default/tftpd-hpa`. They leave out `-s`, whose `chroot` fails here, and a remap rule (`/etc/tftpd-hpa.remap`) serves relative names from `/var/lib/tftpboot` instead; `-u root` is there because this kernel cannot drop privileges. `-m binary` matters: the client's default mode is netascii, which is for text. Measured on 2026-10-06 in Chrome, five runs, with the fixed HardwareJS served in place of the site's and the TP-Link cabled to a LAN: the machine fetched U-Boot from a TFTP server on that LAN byte-for-byte, and a host on the LAN fetched it back from the machine on the first try each time (119,416 B in 5 to 7 s). Measured again the same day in Developer mode, on this image's own HardwareJS with nothing replaced: the same fetches both ways, and `curl --tftp-blksize 8192` fetched the 5.8 MB `rootfs.uImage` in 11 s where `tftp`'s 512-byte blocks take about two minutes. Not yet shown: a board fetching its kernel from the machine. **The Files tab cannot put the image into the machine.** We tried to put `rootfs.uImage` into the machine's filesystem through it, 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 without the network, 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. In Developer mode a kernel can instead be fetched into the running machine over TFTP, as above; fetch it into `/tmp`, because writing 5.8 MB into the machine's own filesystem took 337 s. What has not been tried is the board's U-Boot fetching it from the machine's TFTP server. ## 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. On this board a replug of P1 is also a power cycle — see the J-Link row below. | | The J-Link is listed by `lsusb` in the guest, but openocd fails with `LIBUSB_ERROR_NO_DEVICE` and the J-Link is then gone | The J-Link left the host's USB bus and came back as a new device, and the guest was not told. Seen on 2026-10-05 two seconds after P1 was replugged with the J-Link attached; the host's `cdc_acm` then held it again. Release `cdc_acm` (§2.2), reload the page, and attach the J-Link alone. A dry run and `lsusb` do not prove the J-Link is there; openocd's `init` is the first thing that talks to it. | | `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. | | `Error: newtap: nrf91.cpu missing IR length` | The J-Link's first transport is JTAG and the nRF91 is SWD-only. Put `-c "transport select swd"` before `-f target/nordic/nrf91.cfg` (§4.3). | | `som-program-f7: FAIL -- no such adapter config: …` | `--adapter` typo. The resolved path is in the message; the installed names are in §4.1. | | A Nordic DK's on-board J-Link (`1366:1055`) attaches, but `lsusb` shows no interfaces and openocd says `Failed to get configuration descriptor: LIBUSB_ERROR_NOT_FOUND` / `No J-Link device found`; the Log shows `claimInterface(7) refused … protected class` | The on-board J-Link's mass-storage drive (brintos/hardwarejs#41). Use an external J-Link on the DK's Debug in header (§4.3). | | `som-console: no USB-serial port matching …` | P1's bridge has not been granted to the machine, or the host still owns it (§2.2). `som-console --list` shows what the machine does have. | | `som-console: 2 ports match …` | Two SOM-BSB-EXT bridges are granted. Name one: `som-console --serial `. | | The console is silent, and the Olimex is attached | The opened adapter holds the SOM in reset (§5.1). Run any openocd command, e.g. `openocd -f board/emcraft-som-stm32f7.cfg -c init -c shutdown`; `som-program-f7` does it as part of the write. | | One of two granted devices is missing after Launch | brintos.io's runtime attaches every granted device to the same port at launch, so the guest sees one ([brintos/hardwarejs#55](https://gitlab.com/brintos/hardwarejs/-/work_items/55); fixed on `emc-dev` by hardwarejs!18 on 2026-10-02, not yet in a release the site serves). Detach the missing device in the USB tab and attach it again: one at a time on a running machine, each gets its own port. | | `cat ` never returns on brintos.io, and the Log shows `RESTART-SENTINEL-LEAK … nr=40` | busybox `cat` copies with `sendfile`, which the site's runtime does not complete ([brintos/glibc#14](https://gitlab.com/brintos/toolchain/glibc/-/work_items/14)). Press Ctrl-C; the shell is fine afterwards. Read the file with `sed -n p ` instead. | | Partway through a session, a file read never returns, or a write fails with `Unknown error 517` (`RESTART-SENTINEL-LEAK val=517` in the Log) | The session's root filesystem has stopped answering ([brintos/linux#134](https://gitlab.com/brintos/packages/modified/linux/-/work_items/134)). Nothing in the session recovers it: reboot the tab. In a fresh tab, run `som-program-f7` before anything else. | | `/usr/bin/: Bad address` when you run a program | That program needs two or more shared libraries and the HardwareJS release the site gave this machine cannot load them (brintos/hardwarejs#43). The station's own tools are built so this does not happen — tell us which program it was. | | Halt ends with "AggregateError: Filesystem shutdown failed; latest was not updated" | A brintos.io defect on recently created machines (brintos/linux-in-the-browser#205): what you changed inside the machine this session is not saved. The machine itself is unharmed and launches again as it was. | | 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`, built for wasm32, with the J-Link driver enabled and the nRF53/nRF91 flash support backported. The board config is `board/emcraft-som-stm32f7.cfg`, and it sources `mfg-jtag.cfg` — the one place the adapter is named.