# 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 this machine writes both: | 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 U-Boot's `flashaddr`, 5,785,356 B | the board's U-Boot, over TFTP from this machine on a straight Ethernet cable | works — §6 | **What a brintOS machine does.** It drives the JTAG adapter through WebUSB, it opens the board's serial console the same way once P1's USB-serial bridge is granted to it (§5.1), and it serves the kernel to the board's U-Boot through a USB Ethernet adapter cabled straight to the board (§6). The adapter needs **Developer mode**, and a kernel with the USB Ethernet drivers, which this image has: 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. §6.4 times full SOM programming: U-Boot, kernel and test, from this machine. 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 unless a section gives another date, and the outputs are quoted from those runs. ## 1. What you need, and what connects where Two USB cables, a USB Ethernet adapter and an Ethernet cable. Everything lands on your Mac or Linux PC; nothing needs a second computer, and the board takes no separate power supply. The adapter route (§6) was measured from a Linux host only: on a Mac, the console from the brintOS shell is not expected to work (§2.1) and an adapter granted from a Mac has not been tried, so read the console from the Mac (§5) and serve the kernel from it (§6.5). 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, a SEGGER J-Link, §4.3, | or an ST-LINK/V2, §4.4) | +-- USB A-to-mini-B -------------------- P1, mini-USB §5, §6 | console AND power | `-- USB -- USB Ethernet adapter ---- Ethernet cable ---- RJ45 the kernel, §6 The Ethernet cable runs from the adapter straight to the board's RJ45; nothing else is on it. (§6.5 serves the kernel from a host instead.) | Link | Purpose | Needed for | |---|---|---| | JTAG adapter — the Olimex, a J-Link (§4.3) or an ST-LINK/V2 (§4.4): 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. | | A USB Ethernet adapter, USB to the Mac, and a straight Ethernet cable from it to the board's **RJ45** | TFTP transfer of the kernel, from this machine to the board's U-Boot | §6. The one tried is a TP-Link USB 10/100 LAN (`2357:0602`); which others work is in `usb-ethernet-and-tftp.md`. The machine needs Developer mode for it (§1.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. Both images ship with this machine (§9). What you need is the machine itself: ### 1.1 Your own copy of the machine — fork it The station image lives in a public machine of ours, `https://brintos.io/sergei/core-image-brintos-manufacturing`. **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/core-image-brintos-manufacturing` and press **Fork** (the direct link is `https://brintos.io/new?fork=sergei/core-image-brintos-manufacturing`). Take it from **`latest`**, or from a dated tag (below). 3. In *your* machine, turn on **Settings → General → Developer mode**. The station carries its own HardwareJS runtime in `/boot` and runs on it only in Developer mode. With it off, brintos.io runs its own release: Expect cannot spawn there, so the console and minicom self-tests fail. 4. Work in your machine from then on: `https://brintos.io//core-image-brintos-manufacturing`. **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. **Snapshots.** `latest` is the newest station image, published only after it passes the station test plan on brintos.io. A dated tag, such as `2026-10-01-minicom-serial`, never changes, and its description says what it holds and its known issues. A tag ending in `-dev` carries our runtime and needs Developer mode on. The snapshot selector next to the machine name shows which one a page is showing, and `/usr/share/brintos-hardwarejs-runtime/PROVENANCE` in the guest names the HardwareJS build the machine carries. ## 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, or the ST-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`; an ST-Link: `0483:3748`). **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, programs it, and has openocd verify what it wrote. It takes no backup of the eNVM and does not read the flash back unless you ask, with `--backup` and `--readback` (§4.1). 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), `--adapter stlink` an ST-LINK/V2 (§4.4). 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`, `jlink` and `stlink`. An unknown name fails before any hardware is touched. | | `--transport swd` | `jtag` or `swd` (an ST-Link takes `hla_swd` or `hla_jtag` instead); 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; a U-Boot of your own and its record: §7. | | `--backup` | read the whole 1 MB eNVM out before writing. A new board carries nothing worth saving, so a production run takes none; take one on a board whose contents you do not already have, because it is the only copy of what the board 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 --backup …`. | | `--readback` | after openocd's own verify, read the flash back and compare it with the image on this side. A production run needs none: a bad U-Boot does not reach its prompt. | | `--no-backup`, `--no-readback` | accepted, so older scripts keep working; they change nothing. | | `--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 The first openocd run in a fresh session loads the 26 MB openocd binary: on 2026-09-25, a run with no backup and no read-back took **62.2 s** first, then **21.2 s** and **17.9 s**, against 2.9 s on a host. A run that seems to hang for half a minute before printing anything is this, not a dead adapter. A run with `--backup --readback` (backup, write, read-back) after that, measured on 2026-10-07/08 on our SOM-BSB-EXT boards (Chrome: the median of seven runs; single runs, through either probe, took from 52 to 76 s): | Where | Olimex ARM-USB-TINY-H | ST-LINK/V2 | |---|---|---| | Chrome on brintos.io | 58 s | 61 s | | a host running the same openocd | 11.5 s | 14.4 s | More than half of it is the 1 MiB backup. The rest is mostly what each USB transfer costs inside a machine, not the JTAG clock: openocd waits for each answer before it sends the next request, a transfer costs a machine several milliseconds where a host spends well under one, and a 119 KB write makes 1,200 to 2,500 of them. Each openocd start inside a machine also costs between 1 and 10 s before its first transfer. In earlier versions of `som-program-f7` the read-back after the write ran at the 150 kHz connect clock (125 kHz on an ST-Link): it followed a halt, and only a reset raises the clock. It now follows a reset, as the backup does. In Chrome the read-back step went from 15–21 s to about 6–7 s, and a restore of a 1 MiB backup (`--readback --image `), whose read-back is 1 MiB too, from 178 s to 86 s through the Olimex (one run each, the same board; the ST-Link's is in §4.4). The read-back figures in §4.3 are from before that. ### 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, when the backup and the read-back were still the default (they are `--backup --readback` now). 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 dump and the write ran at 2000 kHz, after the reset each makes. The read-backs ran at the 150 kHz connect clock, which is why the restore's took 138 s; `som-program-f7` now reads back after a reset (§4.2). **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. On the nRF9160, programming is openocd's own `program`; there is no `som-program-f7` for the nRF91. **On an nRF9151, read the two rules below first:** 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 order, an erase and then `program` in a new session, is safe on the nRF9160 only. **An nRF9151 SOM, measured on 2026-10-05** (SOM-NRF9151-BSB-1A, a J-Link EDU Mini, Chrome on macOS). The probe prints `nRF9151-LACA 1024kB Flash, 256kB RAM` at `VTarget = 2.94 V`. b0, both MCUboot slots and the application read back equal to the 1.2-114 release, at about 6.8 KiB/s through the EDU Mini. A full 1 MiB `dump_image` took 154 s and passed `verify_image`; `program` of that dump with `verify` took 5 min 48 s, and the board rebooted into 1.2-114. **Disable the application's watchdog first.** The 1.2-114 application resets the chip 3–6 s after openocd halts it (a 2.5 s feed and a 10 s timeout), and the halted dump or program then dies with `SWD: Read data parity mismatch`. Run `watchdog disable` on the board's own shell before starting. It stays off across `reset halt` and comes back at the next boot. **On an nRF9151, two mistakes cannot be undone:** - **A reset of an erased chip locks debug access** (APPROTECT), and `program` starts with `reset init`. Never run `program` after a separate `flash erase_sector`. Erase, write and verify in one openocd session with no reset in between, and reset only after the verify passes. - **The UICR is write-once** (b0's key hashes, and a counter b0 updates at run time). Emcraft's hex files carry UICR records: strip everything from `0xFF0000` up before writing, and compare the UICR rather than write it. The one-session form, for a binary image of the application flash (a `.bin` from `dump_image` carries no UICR): openocd -f interface/jlink.cfg -c "transport select swd" -f target/nordic/nrf91.cfg -c init -c "reset halt" -c "flash erase_sector 0 0 last" -c "flash write_image 0x00000000" -c "verify_image 0x00000000" -c "reset run" -c shutdown It prints `erased sectors 0 through 255 on flash bank 0`, `wrote 1048576 bytes from file …` and `verified 1048576 bytes`, and resets only once the verify has passed; a failed step stops openocd before the `reset run`. Measured on the nRF9160-DK on 2026-10-07 (erase 66.8 s, write 51.3 s, verify 1.4 s, the factory image back byte for byte); not yet run on an nRF9151. Once in two runs it stopped at once with `Error: Target not halted` and `failed erasing sectors 0 to 255`: the reset had not halted the core, nothing was erased, and the same command then passed. **Reading needs no halt.** A `dump_image` straight after `init`, with the watchdog running, read 1 MiB in 57 s (18 KiB/s through the EDU Mini), 2.6 times the halted rate. That was measured through a board config for the SOM; the same read through the configs above has not been run: openocd -f interface/jlink.cfg -c "transport select swd" -f target/nordic/nrf91.cfg -c init -c "dump_image 0 0x100000" -c shutdown ### 4.4 Through a standalone ST-LINK/V2, over SWD som-program-f7 --adapter stlink A standalone ST-LINK/V2 has the same 20-pin JTAG connector as the Olimex, so it goes on P3 and attaches the same way (§3); `lsusb` then lists `0483:3748 STMicroelectronics ST-LINK/V2`. On a Linux host its node has to be writable by the user running Chrome, which openocd's `60-openocd.rules` or ST's `49-stlinkv2.rules` arrange; no host driver binds it. Its openocd driver (`hla`) offers `hla_swd` first and `hla_jtag` second, not the plain `jtag` and `swd` the other adapters take, and the board config takes the adapter's first, so no `--transport` is needed. Only `hla_swd` has been measured on this board. Measured on 2026-10-07, in Chrome on brintos.io, with an ST-LINK/V2 (firmware V2J32S7) on our SOM-BSB-EXT, by the same procedure as the J-Link run in §4.3. `som-program-f7` backed the eNVM up into `/tmp` and wrote U-Boot, with the backup and the read-back that were then the default (`--backup --readback` now). The bank was then erased, U-Boot written into the blank bank and read back as the image, the backup written back and read back as the backup, and a final dump equalled the original: som-program-f7: adapter: interface/stlink.cfg (override) Info : STLINK V2J32S7 (API v2) VID:PID 0483:3748 Info : [STM32F756.cpu] Cortex-M7 r0p0 processor detected ** Verified OK ** som-program-f7: readback: 6e060662…4096 matches the image som-program-f7: PASS -- wrote /usr/share/brintos-som/stm32f7/u-boot.bin to 0x08000000 The same procedure ran again on 2026-10-08, on another of our boards, with the read-back after a reset. How long the pieces took on brintos.io (Chrome) that day: - `SOM_BACKUP_DIR=/tmp som-program-f7 --adapter stlink --backup --readback`, its 1 MiB backup and 119 KB read-back included: 52 s; - the erase of all eight sectors: 17 s for the whole openocd run; - U-Boot's write, verify and read-back into the blank bank: 21 s; - the restore of the 1 MiB backup, with its 1 MiB read-back: 89 s; - a final dump of 1 MiB into `/tmp`: 24 s for the dump itself (42 KiB/s), 34 s for the openocd run. The ST-Link has a fixed set of SWD clocks and runs the nearest one at or below the clock asked for: the board config's 2000 kHz runs as 1800 kHz, and openocd says so on each reset (`Unable to match requested speed 2000 kHz, using 1800 kHz`). In earlier versions of `som-program-f7` the read-backs followed a halt and ran at the connect clock (150 kHz asked for, which the ST-Link runs as 125 kHz). On 2026-10-07 the same five pieces took 83, 18, 37, 201 and 35 s, the 1 MiB restore longer than everything else together. Not all of that gap is the read-back: that day's first connect also took 34 s against 14 s. Over seven runs of each in Chrome, a 119 KB read-back took 4.6–12.3 s after a reset and 16.8–23.3 s after a halt, about 15 s apart at the median. **Only a standalone ST-LINK/V2.** An STLINK-V3, and the ST-LINK/V2-1 built into Nucleo and Discovery boards, also carry a USB mass-storage interface, and that stops them on brintos.io (§8). ## 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 — from this machine, over a straight cable 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 `${image}` by TFTP from `${serverip}`, writes it at `${flashaddr}` and saves the environment. Addressing is static, out of U-Boot's own environment; no DHCP is involved. This machine is the TFTP server: a USB Ethernet adapter granted to it, cabled straight to the board's RJ45. ### 6.1 This machine's side With Developer mode on, attach the adapter in the **USB devices** tab, wait about ten seconds, then in the console: ip link set eth1 up ip addr add 192.168.7.1/24 dev eth1 /etc/init.d/tftpd-hpa start If the adapter is not `eth1`, find it by its MAC address (`grep -l /sys/class/net/*/address`). The kernel is already in the TFTP root: `/var/lib/tftpboot/stm32f7` is a link to `/usr/share/brintos-som/stm32f7/`, so the board asks for `stm32f7/rootfs.uImage` and nothing is copied first. A kernel of your own: §7. The adapter's host set-up (a Linux host's kernel holds it until a udev rule lets go), what was measured, and what does not work: `/usr/share/brintos-som-station/usb-ethernet-and-tftp.md`. ### 6.2 At the board's U-Boot prompt Open the console from this machine (§5.1; on a Mac, from the Mac, §5) and stop at `STM32F7-SOM>`. The countdown is short, so start a reset in the background first, then open the console and press Ctrl-C about once a second until the prompt appears and stays: (sleep 8; openocd -f board/emcraft-som-stm32f7.cfg -c init -c "reset run" -c shutdown >/tmp/ocd.log 2>&1) & som-console The reset also releases the reset line an opened Olimex holds (§5.1); openocd's first run in a session takes about a minute (§4.2). That command drives the default adapter; for another, name its config first, as `--adapter` would (§4.1), e.g. `openocd -c "set BRINTOS_JTAG_IFACE interface/jlink.cfg" -f board/…`. At the prompt: printenv flashaddr # where update will write: §6.3 setenv ipaddr 192.168.7.2 setenv serverip 192.168.7.1 setenv image stm32f7/rootfs.uImage run update From our bench, 2026-10-08 (§6.4 has the timings): Auto-negotiation...completed. STM32_MAC: link UP (100/Half) TFTP from server 192.168.7.1; our IP address is 192.168.7.2 Filename 'stm32f7/rootfs.uImage'. Load address: 0xc0007fb4 Loading: T ################################################################# … Bytes transferred = 5785356 (58470c hex) … Erased 45 sectors Copy to Flash... done Saving Environment to Flash... Check the copy in NOR against the image rather than trusting it: STM32F7-SOM> crc32 ${flashaddr} ${kernelsize} CRC32 for 60020000 ... 605a470b ==> 87504267 `87504267` is the `crc32:` line of `/usr/share/brintos-som/stm32f7/rootfs.uImage.PROVENANCE`. Then `reset`: `bootcmd=run flashboot`, so the board boots what was just written: ## Booting kernel from Legacy Image at 60020000 ... Image Name: Linux-4.2.0-cortexm-2.1.2 Verifying Checksum ... OK … Linux version 4.2.0-cortexm-2.1.2 … / # The kernel takes its address from U-Boot (`flashboot` runs `addip`), so the board's Linux reaches this machine at once: `ping -c 3 192.168.7.1` there, and `ping -c 3 192.168.7.2` here, answered three of three each way. Console habits worth knowing: - **A bare Enter at the U-Boot prompt repeats the last command.** After `run update` that is a second write. Wake the prompt with Ctrl-C instead. - **This U-Boot has no `ping` command.** To test the cable from U-Boot without writing anything, fetch into RAM and check it: `tftp ${loadaddr} stm32f7/rootfs.uImage`, then `crc32 ${loadaddr} ${filesize}`. - `update` ends with `saveenv`, so the addresses are stored with the kernel. - A fetch can start `Loading: T` or `Loading: T T`: U-Boot timing out on its first requests and retrying, about 5 s each by default, and then running to the end. Most first fetches after a reset lose the first: the board's link goes down and up as the fetch starts, the board sends its first ARP request as soon as its link is up, and in the loads traced the adapter passed that packet on before it told this machine the link was up, so this machine dropped its answer. A second fetch in the same U-Boot session starts at once. `setenv tftptimeout 1000` before the fetch makes U-Boot ask again after 1 s, its minimum, instead of 5 s; it also asks this machine's TFTP server to resend after 1 s. It applies to every wait in the fetch: U-Boot allows ten (`T`) and gives the fetch up at the eleventh (`Retry count exceeded`). From a machine you launch as its owner, where more waits timed out, runs on 2026-10-12 spent up to six. `update` saves the variable with the rest; `setenv tftptimeout` and `saveenv` remove it from the saved environment, and a U-Boot that has already run a fetch with it keeps 1 s until a `reset`. ### 6.3 Where `flashaddr` points `update` writes wherever `flashaddr` says, so read it first on a board you did not program yourself: | NOR address | Contents | |---|---| | `0x60000000` | the saved U-Boot environment, 128 KB | | `0x60020000` | the kernel, in this U-Boot's default environment (`flashaddr=60020000`) | A board whose environment has been erased, which is every board the run in §6.4 programs, takes the default and writes the kernel at `0x60020000`. A board can carry a saved environment naming another address: one of ours had `flashaddr=600A0000`, with a splash image at `0x60020000`. ### 6.4 Full SOM programming, the way the VF6 station does it One command runs the production station's whole run for this SOM (`target_som-stm32f7.exp`), unattended: `som-mfg-f7`. Attach the three devices (§3, §5.1, §6.1: the Olimex on P3, the board's FT232R on P1, the Ethernet adapter cabled to the board's RJ45), then: som-mfg-f7 --dry-run # the MAC it would give, the console, the adapter; touches no board som-mfg-f7 # program the board It finds the console (`som-console --find`) and the adapter (the one USB network interface), gives the adapter `192.168.7.1/24` and starts the TFTP server if they are not already so, and then runs steps 1 to 7 below. It prints a line per step with its time and ends with one line, `som-mfg-f7: PASS -- programmed in s` or `som-mfg-f7: FAIL -- step (): `, exit status 0 or 1. On a failure it also prints the last of what the board said; the whole console transcript is in `/tmp/som-mfg-f7-