#!/bin/sh # som-program-nrf91 -- program an Emcraft SOM-NRF9151's flash over SWD through a # J-Link, from a brintOS machine, and say exactly what was written from where. # # The nRF91 counterpart of som-program-f7, built from what the hand runs of # 2026-10-05 found on an SOM-NRF9151-BSB-1A (the station guide's section 4.3 # records the same three traps): # # * asset_tracker_v2 1.2-114 runs a watchdog that resets the SoC a few # seconds into any debugger halt. Its shell can turn it off, so this script # does that over the console, and then refuses to erase anything until the # CPU has stayed halted for 12 s without a reset. # * An erased nRF91x1 locks debug access at its next reset. So the erase, # write and verify run in one openocd session with no reset inside it, and # the board is reset only once all three passed. openocd's own `program` is # not used: it starts with `reset init`. # * The UICR holds write-once OTP (b0's key hashes at 0xFF8130). It is never # written. An image's UICR records are compared with the chip's instead: a # mismatch means the image is signed for other keys and would not boot. # # Every run prints one `image:` line naming the file and its sha256, and ends # with a single PASS or FAIL line that also says how long the run took. That # output is the record of what a board was programmed with. Exit status: 0 PASS, # 1 FAIL, 2 usage. set -u PROG=som-program-nrf91 SCRIPTS="${SOM_OPENOCD_SCRIPTS:-/usr/share/openocd/scripts}" BOARD_CFG="${SOM_BOARD_CFG:-$SCRIPTS/board/emcraft-som-nrf9151.cfg}" MEDIA=/mnt/media/nrf9151/app.hex FW_DIR=/usr/share/brintos-som/nrf9151 # the shipped releases, one directory each CONSOLE_ID=0403:6015 # the BSB's FT230X FLASH_SIZE=0x100000 GATE_SECONDS=12 image="" app="" addr="" speed="" adapter="" console="" use_console=1 expect_ver="" expect_from=argument modem_min="" erase=touched do_backup=1 check_only=0 dry_run=0 modem_check=1 split_dir="" # --split-only: the build-time split of a release, then exit fw_apps() { # the releases this machine carries, or "none" l=$(ls "$FW_DIR" 2>/dev/null | tr '\n' ' ') echo "${l:-none}" } usage() { printf '%s\n' \ "usage: $PROG [--app NAME | --image FILE] [--addr ADDR] [--expect TEXT] [--speed KHZ]" \ " [--adapter NAME] [--console DEV | --no-console]" \ " [--erase all|touched] [--no-backup] [--no-modem-check]" \ " [--check] [--dry-run]" \ "" \ " --app NAME a release this machine carries: $(fw_apps)" \ " --image FILE Intel HEX (preferred) or raw .bin to program" \ " --addr ADDR load address for a .bin (a .hex carries its own)" \ " --expect TEXT after the write, the board's \`version\` must contain TEXT" \ " --speed KHZ SWD clock (default: the board config's 1000)" \ " --adapter NAME another openocd interface config (default: jlink)" \ " --console DEV the SOM's console node (default: som-console --find --id $CONSOLE_ID)" \ " --no-console do not touch the console: no watchdog step, no boot check" \ " --erase MODE touched (default): only the 4 KiB pages the image covers;" \ " all: the whole 1 MiB first" \ " --no-backup skip reading out first what the erase will clear" \ " --no-modem-check do not read the modem firmware before writing" \ " --check probe the chip (and an --image's UICR) only; write nothing" \ " --dry-run resolve and report the image, touch no hardware" \ "" \ "Image resolution: --app (in $FW_DIR), --image, then $MEDIA." \ "An image's PROVENANCE beside it supplies --expect, and the oldest modem firmware" \ "it runs on: the station cannot update the modem, so an older one is refused." \ "Backup: to \$SOM_BACKUP_DIR, default /tmp (RAM, never stored with the machine)." \ "Never written: the UICR. Never run: nrf91_recover." } while [ $# -gt 0 ]; do case "$1" in --app) app=${2:-}; shift 2 || exit 2 ;; --image) image=${2:-}; shift 2 || exit 2 ;; --addr) addr=${2:-}; shift 2 || exit 2 ;; --expect) expect_ver=${2:-}; shift 2 || exit 2 ;; --speed) speed=${2:-}; shift 2 || exit 2 ;; --adapter) adapter=${2:-}; shift 2 || exit 2 ;; --console) console=${2:-}; shift 2 || exit 2 ;; --no-console) use_console=0; shift ;; --erase) erase=${2:-}; shift 2 || exit 2 ;; --no-backup) do_backup=0; shift ;; --no-modem-check) modem_check=0; shift ;; --check) check_only=1; shift ;; --dry-run) dry_run=1; shift ;; --split-only) split_dir=${2:-}; shift 2 || exit 2 ;; -h|--help) usage; exit 0 ;; *) echo "$PROG: unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done now() { date +%s; } T_START=$(now) took() { # how long this run has taken so far, for the PASS/FAIL line t=$(( $(now) - T_START )) echo "$((t / 60)) min $((t % 60)) s" } fail() { echo "$PROG: FAIL -- $* [$(took)]"; exit 1; } say() { echo "$PROG: $*"; } case "$erase" in all|touched) ;; *) fail "--erase must be all or touched" ;; esac if [ -n "$speed" ]; then case "$speed" in *[!0-9]*|0*) fail "bad --speed '$speed': give kHz as a whole number" ;; esac [ "$speed" -le 2000 ] || fail "--speed $speed: flash programming fails above 2000 kHz on this chip" fi # $((addr)) would read a word such as `size` as a variable, and a leading 0 as octal. num_ok() { case "$1" in 0[xX]?*) case "${1#0[xX]}" in *[!0-9a-fA-F]*) return 1 ;; esac ;; 0|[1-9]*) case "$1" in *[!0-9]*) return 1 ;; esac ;; *) return 1 ;; esac } # --- resolve the image -------------------------------------------------------- source_kind="" if [ -n "$app" ] && [ -n "$image" ]; then fail "--app and --image both name an image; give one" fi if [ -n "$app" ]; then case "$app" in */*|.*) fail "--app takes a release name: $(fw_apps)" ;; esac image=$FW_DIR/$app/$app.hex [ -r "$image" ] || fail "no release '$app' on this machine (it carries: $(fw_apps))" source_kind="release $app" elif [ -n "$image" ]; then source_kind="argument" elif [ -r "$MEDIA" ]; then image=$MEDIA; source_kind="media" elif [ "$check_only" -eq 0 ]; then fail "no image: no --image given and $MEDIA is not readable" fi kind="" if [ -n "$image" ]; then [ -r "$image" ] || fail "cannot read $image" size=$(stat -c %s "$image" 2>/dev/null) || fail "cannot stat $image" [ "$size" -gt 0 ] 2>/dev/null || fail "$image is empty" sum=$(sha256sum "$image" | cut -d' ' -f1) case "$image" in *.hex|*.HEX|*.ihex) kind=hex ;; *.bin|*.BIN) kind=bin ;; *) fail "cannot tell the format of $image: name it .hex or .bin" ;; esac if [ "$kind" = bin ]; then [ -n "$addr" ] || fail "a .bin needs --addr" num_ok "$addr" && [ $((addr)) -ge 0 ] || fail "bad --addr '$addr': give a decimal number, or 0x and hex digits" [ $((addr + size)) -le $((FLASH_SIZE)) ] || fail "$image at $addr runs past the 1 MiB application flash" fi say "image: $image ($source_kind, $kind, $size bytes)" say "sha256: $sum" prov=$(dirname "$image")/PROVENANCE if [ -r "$prov" ]; then say "provenance: $prov" sed 's/^/ | /' "$prov" if [ -z "$expect_ver" ]; then expect_ver=$(sed -n 's/^expect: *//p' "$prov" | sed -n 1p) [ -n "$expect_ver" ] && expect_from=provenance fi modem_min=$(sed -n 's/^modem-min: *//p' "$prov" | sed -n 1p) else say "provenance: none beside the image" fi fi say "target: nRF9151 application flash (1 MiB at 0x0) via $BOARD_CFG" if [ -z "$split_dir" ]; then [ -r "$BOARD_CFG" ] || fail "no board config at $BOARD_CFG" command -v openocd >/dev/null 2>&1 || fail "openocd is not installed" fi # --- working files ---------------------------------------------------------------- # Generated Tcl, logs and the UICR-free image live for this run only -- except # under --split-only, where the split is the product and is kept. if [ -n "$split_dir" ]; then [ "$kind" = hex ] || fail "--split-only takes an Intel HEX" W=$split_dir mkdir -p "$W" || fail "cannot create $W" else W=${TMPDIR:-/tmp}/$PROG.$$ mkdir -p "$W" || fail "cannot create $W" trap 'rm -rf "$W"' EXIT fi # --- split the image ---------------------------------------------------------------- # A .hex is checked record by record (shape, length, address) and its UICR # records (0xFF0000 and up) are taken out into a byte list for the compatibility # check; everything else must fall inside the 1 MiB application flash. Record # checksums are left to openocd, which refuses a bad one when it loads the file: # summing every byte here took minutes in the guest's awk. # A shipped release is split at build time (--split-only), into split/ beside # it: in the guest the awk below takes 18-39 s on the 1.2-114 files. The split is # used only while its recorded sha256 is the image's. pre=$(dirname "${image:-.}")/split if [ "$kind" = hex ] && [ -z "$split_dir" ] && [ -r "$pre/split.txt" ] && [ -r "$pre/image.hex" ] && [ -r "$pre/uicr.tcl" ] && [ "$(sed -n 's/^SHA //p' "$pre/split.txt")" = "$sum" ]; then hex_file=$pre/image.hex; uicr_tcl=$pre/uicr.tcl; split_txt=$pre/split.txt say "split: precomputed at build ($pre)" else hex_file=$W/image.hex; uicr_tcl=$W/uicr.tcl; split_txt=$W/split.txt fi if [ "$kind" = hex ] && [ "$split_txt" = "$W/split.txt" ]; then awk -v out="$W/image.hex" -v uicr="$W/uicr.tcl" ' function h(s) { return H[toupper(s)] } # 2 hex digits function h4(s) { return H[toupper(substr(s, 1, 2))] * 256 + H[toupper(substr(s, 3, 2))] } BEGIN { x = "0123456789ABCDEF" for (i = 0; i < 16; i++) for (j = 0; j < 16; j++) H[substr(x, i + 1, 1) substr(x, j + 1, 1)] = i * 16 + j base = 0; data = 0; hi = 0; nu = 0; bad = ""; printf "set uicr {" > uicr } { sub(/\r$/, "") if ($0 == "") next if (substr($0, 1, 1) != ":") { bad = "line " NR " is not a HEX record"; exit } n = h(substr($0, 2, 2)); t = substr($0, 8, 2) if (length($0) != 11 + 2 * n) { bad = "line " NR " has the wrong length"; exit } if (t == "04") base = h4(substr($0, 10, 4)) * 65536 if (t == "02") base = h4(substr($0, 10, 4)) * 16 if (t == "00") { a = base + h4(substr($0, 4, 4)) if (a >= 16711680) { # 0xFF0000: UICR, kept out for (i = 0; i < n; i++) printf " %d %d", a + i, h(substr($0, 10 + 2 * i, 2)) > uicr nu += n next } if (a + n > 1048576) { bad = sprintf("data at 0x%X is outside the application flash and the UICR", a); exit } data += n if (a + n > hi) hi = a + n for (p = int(a / 4096); p <= int((a + n - 1) / 4096); p++) P[p] = 1 } print > out } END { printf "}\n" > uicr if (bad != "") { print "BAD " bad; exit 1 } printf "DATA %d %d\nUICR %d\n", data, hi, nu np = 0; r = "" for (p = 0; p < 256; p++) if (p in P) { np++ if (!((p - 1) in P)) s0 = p if (!((p + 1) in P)) r = r sprintf(" %d:%d", s0 * 4096, (p - s0 + 1) * 4096) } printf "PAGES %d\nRANGES%s\n", np, r }' "$image" >"$W/split.txt" || fail "$image: $(sed -n 's/^BAD //p' "$W/split.txt")" if [ -n "$split_dir" ]; then echo "SHA $sum" >>"$W/split.txt" say "split: written to $W" exit 0 fi fi if [ "$kind" = hex ]; then data=$(sed -n 's/^DATA \([0-9]*\) .*/\1/p' "$split_txt") hi=$(sed -n 's/^DATA [0-9]* \([0-9]*\)/\1/p' "$split_txt") nu=$(sed -n 's/^UICR //p' "$split_txt") [ "${data:-0}" -gt 0 ] || fail "$image has no data inside the application flash" say "content: $data bytes up to $(printf 0x%x "$hi"); UICR records: $nu bytes (compared, never written)" pages=$(sed -n 's/^PAGES //p' "$split_txt") ranges=$(sed -n 's/^RANGES *//p' "$split_txt") write_file=$hex_file write_args="$hex_file" elif [ "$kind" = bin ]; then say "content: $size bytes at $addr" p0=$(( addr / 4096 )); p1=$(( (addr + size - 1) / 4096 )) pages=$(( p1 - p0 + 1 )); ranges="$(( p0 * 4096 )):$(( pages * 4096 ))" printf 'set uicr {}\n' >"$W/uicr.tcl" nu=0 write_file=$image write_args="$image $addr bin" else printf 'set uicr {}\n' >"$W/uicr.tcl" nu=0 fi # Overrides go in a prelude sourced BEFORE the board config, which reads them. : >"$W/prelude.tcl" if [ -n "$speed" ]; then echo "set BRINTOS_JTAG_SPEED $speed" >>"$W/prelude.tcl" say "speed: ${speed} kHz (override)" else say "speed: the board config's default (1000 kHz)" fi if [ -n "$adapter" ]; then iface="" case "$adapter" in */*) iface=$adapter ;; *.cfg) name=$adapter ;; *) name=$adapter.cfg ;; esac if [ -z "$iface" ]; then iface=interface/ftdi/$name [ -r "$SCRIPTS/interface/$name" ] && iface=interface/$name fi [ -r "$SCRIPTS/$iface" ] || fail "no such adapter config: $SCRIPTS/$iface" echo "set BRINTOS_JTAG_IFACE $iface" >>"$W/prelude.tcl" say "adapter: $iface (override)" else say "adapter: interface/jlink.cfg (the board config's default)" fi # Shared helpers for every openocd session: read a word, as a number. Words # only: byte and halfword reads of the UICR return 0xdead filler on this chip # (measured 2026-10-05), while word reads return its contents. The trailing # `return` keeps openocd from printing the last definition's name. printf '%s\n' \ 'proc r32 {a} { set s [mdw $a]; if {![regexp {:[ \t]*([0-9a-fA-F]+)} $s -> v]} { error "mdw $a: $s" }; return [expr 0x$v] }' \ 'return' \ >>"$W/prelude.tcl" ocd() { # $1 = phase Tcl, $2 = log openocd -f "$W/prelude.tcl" -f "$BOARD_CFG" -f "$1" >"$2" 2>&1 } show() { # the useful part of a log grep -v -E '^(Open On-Chip|Licensed under|For bug reports|[[:space:]]+http|Warn : Could not determine executable|Info : (Listening on port|starting gdb server)|swd$)' "$1" | sed 's/^/ | /' } mark() { # the value of the first "SOM =" line in a log sed -n "s/^SOM $1=//p" "$2" | sed -n 1p } # --- the console ---------------------------------------------------------------------- if [ "$use_console" -eq 1 ]; then if [ -z "$console" ]; then console=$(som-console --find --id "$CONSOLE_ID" 2>/dev/null) || console="" fi if [ -n "$console" ]; then say "console: $console" else say "console: none found (no watchdog step, no boot check)" fi else console="" say "console: not used (--no-console)" fi if [ -n "$expect_ver" ] && [ -z "$console" ] && [ "$check_only" -eq 0 ]; then if [ "$expect_from" = provenance ]; then say "expect: '$expect_ver' (from the provenance) -- not checked, no console" expect_ver="" else fail "--expect needs the SOM's console, and there is none; nothing was written" fi fi # The console is driven from the shell, not Expect: this image's expect needs # shared libraries, and the site's runtime cannot fork such a program # (brintos/hardwarejs#50) -- every `spawn` dies with "parent: sync byte write: # broken pipe" (measured 2026-10-05). A background dd reads the port while # printf writes a line to it. Waits are deadlines on `date`, because a sleep # right after the shell reaps a child can return at once # (brintos/linux-in-the-browser#120); nothing here uses cat (glibc#14). con_cmd() { # $1 = a shell command line for the SOM, $2 = seconds to collect : >"$W/con.out" dd if="$console" of="$W/con.out" bs=256 2>/dev/null & rd=$! end=$(( $(now) + 3 )) while [ ! -e "/proc/$rd/fd/3" ] && [ "$(now)" -lt "$end" ]; do sleep 1; done n=0 while [ $n -lt 3 ]; do # open(2) of a tty can fail EINTR here (brintos/linux#73) sh -c 'printf "\r%s\r" "$1" >"$2"' _ "$1" "$console" 2>/dev/null && break n=$((n + 1)) done end=$(( $(now) + $2 )) while [ "$(now)" -lt "$end" ]; do sleep 1; done kill "$rd" 2>/dev/null wait "$rd" 2>/dev/null tr -d '\r\033' <"$W/con.out" } wdt_off() { # prints disabled, already-disabled, absent, noshell or failed s=$(con_cmd "watchdog status" 2) case "$s" in *"command not found"*) echo absent; return ;; *"status: disabled"*) echo already-disabled; return ;; *"status: enabled"*) ;; *) echo noshell; return ;; esac con_cmd "watchdog disable" 2 >/dev/null case "$(con_cmd "watchdog status" 2)" in *"status: disabled"*) echo disabled ;; *) echo failed ;; esac } modem_fw() { # the modem firmware, e.g. nrf91x1_2.0.2, or ? s=$(con_cmd "at AT%SHORTSWVER" 3) v=$(printf '%s\n' "$s" | sed -n 's/.*%SHORTSWVER: *\([A-Za-z0-9_.-]*\).*/\1/p' | sed -n 1p) if [ -n "$v" ]; then echo "$v"; return; fi case "$s" in *"command not found"*) echo "?no AT shell in the running application" ;; *ERROR*) echo "?the modem answered ERROR" ;; *) echo "?no answer from the console" ;; esac } modem_at_least() { # $1 = the board's, $2 = the oldest the image runs on; both _ [ "${1%%_*}" = "${2%%_*}" ] || return 1 awk -v a="${1##*_}" -v b="${2##*_}" 'BEGIN { n = split(a, x, "."); m = split(b, y, "."); k = n > m ? n : m for (i = 1; i <= k; i++) { if (x[i] + 0 > y[i] + 0) exit 0; if (x[i] + 0 < y[i] + 0) exit 1 } exit 0 }' } boot_version() { # the text after "Firmware version: ", or ?noshell end=$(( $(now) + 40 )) while [ "$(now)" -lt "$end" ]; do v=$(con_cmd version 2 | sed -n 's/.*Firmware version: //p' | sed 's/\[[0-9;]*m//g' | sed -n 1p) [ -n "$v" ] && { echo "$v"; return; } done echo "?noshell" } if [ -n "$console" ]; then stty -F "$console" 115200 raw -echo -echoe -echok 2>/dev/null || fail "cannot set up the console $console; nothing was written" fi # The backup directory is checked before any hardware is touched. It defaults # to /tmp, which is RAM in a brintOS guest: the dump is the board's firmware, # and anything on the machine's stored filesystem is copied to `latest` by a # clean Halt, which anyone can download from a public machine (a station is # public). SOM_BACKUP_DIR picks another directory, e.g. to keep the dump. writable_dir() { [ -d "$1" ] && [ -w "$1" ] && ( : >"$1/.som-w.$$" ) 2>/dev/null && rm -f "$1/.som-w.$$" } backup_dir="" if [ "$do_backup" -eq 1 ] && [ "$check_only" -eq 0 ]; then backup_dir=${SOM_BACKUP_DIR:-/tmp} writable_dir "$backup_dir" || fail "the backup directory $backup_dir is not writable (set SOM_BACKUP_DIR); nothing was written" if [ "$backup_dir" = /tmp ]; then say "backup to: /tmp (RAM: it lasts only as long as this session; copy it off to keep it)" else say "backup to: $backup_dir (stored with the machine: a clean Halt publishes it if the machine is public)" fi fi if [ -n "${pages:-}" ]; then set -- $ranges say "pages: $pages of 256 (4 KiB) in $# range(s); erase: $erase" fi if [ -n "$modem_min" ] && [ "$modem_check" -eq 1 ]; then say "modem: needs $modem_min or later" fi if [ "$dry_run" -eq 1 ]; then say "dry-run, nothing touched" say "PASS -- resolved ${image:-no image} [$(took)]" exit 0 fi # --- 0. the modem firmware ---------------------------------------------------------- # The station cannot update the modem (nrfutil can, natively), so an image that # needs a newer one than the board carries is refused before anything is # touched. It can be read only while the running application has an AT shell. if [ -n "$modem_min" ] && [ "$modem_check" -eq 1 ]; then if [ -z "$console" ]; then say "modem: not read (no console); check it is $modem_min or later" else mfw=$(modem_fw) case "$mfw" in \?*) say "modem: not read (${mfw#?}); check it is $modem_min or later" ;; *) modem_at_least "$mfw" "$modem_min" || fail "the board's modem firmware is $mfw and this image needs $modem_min or later; update the modem natively (nrfutil device program --firmware mfw_nrf91x1_.zip), then retry; nothing was written" say "modem: $mfw (needs $modem_min or later)" ;; esac fi fi # --- 1. probe ------------------------------------------------------------------------- # Read-only: the chip, its protection, and the image's UICR against the chip's. # Only the bytes the image sets are compared. An 0xFF in the image's UICR means # "left unprogrammed", and b0 writes its monotonic counter into such OTP slots at # runtime (this board: 0xfffffffc at 0xFF8158 where the image has 0xffffffff). # openocd parses the whole image here (record checksums, the end record, record # types) without touching the target: the write session reads it only after the # erase. Words 0x0 and 0x4 both erased mean an earlier write did not finish. t0=$(now) img_test='echo "SOM image=none"' [ -n "$image" ] && img_test="if {[catch {test_image $write_args} e]} { echo \"SOM image=bad \$e\" } else { echo \"SOM image=ok\" }" printf '%s\n' \ 'init' \ "$img_test" \ 'echo [format "SOM part=0x%x" [r32 0x00FF0140]]' \ 'echo [format "SOM approtect=0x%08x" [r32 0x00FF8000]]' \ 'echo [format "SOM w0=0x%08x" [r32 0x0]]' \ 'echo [format "SOM w4=0x%08x" [r32 0x4]]' \ "source ${uicr_tcl:-$W/uicr.tcl}" \ 'set bad 0; set same 0; set free 0; set words [dict create]' \ 'foreach {a b} $uicr {' \ ' set wa [expr {$a & ~3}]' \ ' if {![dict exists $words $wa]} { dict set words $wa [r32 $wa] }' \ ' set got [expr {([dict get $words $wa] >> (8 * ($a & 3))) & 0xff}]' \ ' if {$b == 0xff} { incr free } elseif {$got == $b} { incr same } else {' \ ' incr bad' \ ' if {$bad <= 4} { echo [format "SOM uicr-diff: 0x%08x chip 0x%02x image 0x%02x" $a $got $b] }' \ ' }' \ '}' \ 'echo "SOM uicr-bad=$bad"' \ 'echo "SOM uicr-same=$same"' \ 'echo "SOM uicr-free=$free"' \ 'shutdown' >"$W/probe.tcl" ocd "$W/probe.tcl" "$W/probe.log" show "$W/probe.log" grep -q 'AP lock engaged' "$W/probe.log" && fail "the chip's debug access is locked (APPROTECT); this script does not recover chips; nothing was written" vt=$(sed -n 's/.*VTarget = \([0-9.]*\) V.*/\1/p' "$W/probe.log" | sed -n 1p) case "$vt" in 0.0*|"") [ -n "$vt" ] && fail "VTarget is $vt V: the board is unpowered or the cable is on the wrong header; nothing was written" ;; esac part=$(mark part "$W/probe.log") [ -n "$part" ] || fail "could not talk to the chip (is the J-Link attached?); nothing was written" case "$part" in 0x91??) ;; *) fail "this is not an nRF91 (FICR part $part); nothing was written" ;; esac if [ -n "$image" ] && [ "$(mark image "$W/probe.log")" != ok ]; then fail "openocd cannot load $image ($(grep -E '^Error' "$W/probe.log" | sed -n 1p)); nothing was written" fi say "chip: nRF$(echo "$part" | sed 's/^0x//') (FICR part $part), UICR APPROTECT $(mark approtect "$W/probe.log"), VTarget ${vt:-?} V" if [ "$(mark w0 "$W/probe.log")" = 0xffffffff ] && [ "$(mark w4 "$W/probe.log")" = 0xffffffff ]; then say "flash: erased at 0x0 (an earlier write did not finish?)" if [ "$check_only" -eq 0 ]; then retry=$(ls -dt "${TMPDIR:-/tmp}"/som-nrf91-retry-* 2>/dev/null | sed -n 1p) if [ -n "$retry" ]; then how="retry the last failed write, which does not reset: openocd -f $retry/prelude.tcl -f $BOARD_CFG -f $retry/retry.tcl" else how="write it natively (nrfutil device program) or from a session that does not reset" fi fail "the application flash is erased, and this run would reset the chip, which locks an erased nRF91x1. Do not reset or power-cycle it; $how; nothing was written" fi fi ubad=$(mark uicr-bad "$W/probe.log") if [ "$nu" -gt 0 ]; then [ "$ubad" = 0 ] || fail "the image's UICR differs from the chip's in ${ubad:-?} bytes: it is built for other keys and would not boot; nothing was written" say "uicr: the image's $(mark uicr-same "$W/probe.log") set UICR bytes match the chip ($(mark uicr-free "$W/probe.log") left unprogrammed by the image, not compared)" fi say "probe: $(( $(now) - t0 )) s" if [ "$check_only" -eq 1 ]; then say "PASS -- check: nRF91 part $part reachable${image:+, $image compatible} [$(took)]" exit 0 fi # --- 2. the watchdog -------------------------------------------------------------------- if [ -n "$console" ]; then wdt=$(wdt_off) say "watchdog: $wdt" [ "$wdt" = failed ] && fail "the board's watchdog would not turn off; nothing was written" fi # --- 3. the halt gate --------------------------------------------------------------------- # Whatever the console said: halt the CPU and watch it. A reset while halted # (DHCSR S_RESET_ST, or S_HALT gone) means something outside the SoC will reset # it in the middle of an erase. The first read clears the sticky bit the # `reset halt` itself set. The SPU must leave every flash region writable. The # body is one catch, so that a script error still ends in `reset run` rather # than a board left halted. (openocd's Tcl is Jim: `expr` takes one argument, # so every expression is braced.) t0=$(now) printf '%s\n' \ 'init' \ 'set ok 1' \ 'if {[catch {' \ ' reset halt' \ ' r32 0xE000EDF0' \ " for {set i 0} {\$i < $GATE_SECONDS} {incr i} {" \ ' sleep 1000' \ ' if {[catch {set d [r32 0xE000EDF0]}]} { set ok 0; echo "SOM gate-why=lost the target at ${i} s"; break }' \ ' if {($d & 0x02000000) || !($d & 0x00020000)} { set ok 0; echo [format "SOM gate-why=reset at %d s (DHCSR 0x%08x)" $i $d]; break }' \ ' }' \ ' if {$ok} {' \ ' for {set r 0} {$r < 32} {incr r} {' \ ' set p [r32 [expr {0x50003600 + 4 * $r}]]' \ ' if {($p & 0x100) || !($p & 0x2)} { set ok 0; echo [format "SOM gate-why=SPU flash region %d is 0x%x (locked or read-only)" $r $p]; break }' \ ' }' \ ' }' \ '} err]} { set ok 0; echo "SOM gate-why=script error: $err" }' \ 'if {$ok} { echo "SOM gate=pass" } else { echo "SOM gate=fail"; catch {reset run} }' \ 'shutdown' >"$W/gate.tcl" ocd "$W/gate.tcl" "$W/gate.log" if [ "$(mark gate "$W/gate.log")" != pass ]; then show "$W/gate.log" why=$(mark gate-why "$W/gate.log") fail "the halt gate failed: ${why:-see above}. A watchdog? Turn it off on the console (\`watchdog disable\`); nothing was written" fi say "gate: held halted ${GATE_SECONDS} s with no reset, flash unlocked ($(( $(now) - t0 )) s)" # --- 4. back up the current flash ------------------------------------------------------- backup="" if [ "$do_backup" -eq 1 ]; then t0=$(now) stem="$backup_dir/nrf91-flash-$(date -u +%Y%m%dT%H%M%SZ)" if [ "$erase" = all ] || [ -z "${ranges:-}" ]; then backup=$stem.bin; set -- "0:$(( FLASH_SIZE ))" say "backup: reading 1 MiB to $backup (about 1 min) ..." else backup=$stem.pages; set -- $ranges say "backup: reading the $pages pages the erase will clear, in $# range(s), to $stem-*.bin ..." fi # One session: each range dumped and read back against its dump. { echo 'init'; echo 'reset halt'; echo 'set ok 1' for r; do a=${r%%:*}; n=${r#*:} [ "$backup" = "$stem.bin" ] && f=$backup || f=$stem-$(printf 0x%08x "$a").bin echo "if {\$ok && [catch {dump_image $f $a $n} e]} { set ok 0; echo \"SOM backup=fail \$e\" }" echo "if {\$ok && [catch {verify_image $f $a bin} e]} { set ok 0; echo \"SOM backup=fail \$e\" }" done echo 'if {$ok} { echo "SOM backup=pass" }'; echo 'shutdown'; } >"$W/backup.tcl" ocd "$W/backup.tcl" "$W/backup.log" complete=1 for r; do a=${r%%:*}; n=${r#*:} [ "$backup" = "$stem.bin" ] && f=$backup || f=$stem-$(printf 0x%08x "$a").bin [ "$(stat -c %s "$f" 2>/dev/null)" = "$n" ] || complete=0 done if [ "$(mark backup "$W/backup.log")" != pass ] || [ $complete = 0 ]; then show "$W/backup.log" rm -f "$stem.bin" "$stem".pages "$stem"-0x*.bin ocd_reset_run=$W/run.tcl printf '%s\n' 'init' 'catch {reset run}' 'shutdown' >"$ocd_reset_run" ocd "$ocd_reset_run" "$W/run.log" fail "could not read the flash out; nothing was written" fi if [ "$backup" = "$stem.bin" ]; then say "backup sha256: $(sha256sum "$backup" | cut -d' ' -f1) ($(( $(now) - t0 )) s)" else for r; do a=${r%%:*}; n=${r#*:}; f=$stem-$(printf 0x%08x "$a").bin echo "$(basename "$f") $(printf 0x%08x "$a") $n $(sha256sum "$f" | cut -d' ' -f1)" done >"$backup" say "backup: $pages pages, list with sha256s in $backup ($(( $(now) - t0 )) s)" fi fi # --- 5. erase, write, verify -- one session, no reset inside ------------------------- # The first line is `reset halt` on the way in; a retry after a failure must use # a plain `halt` instead, because resetting an erased nRF91x1 locks it. mkwrite() { # $1 = "reset halt" or "halt", $2 = output file if [ "$erase" = all ]; then erase_step='flash erase_sector 0 0 last' write_step="flash write_image $write_args" else erase_step='echo "SOM note=erasing only the pages the image touches"' write_step="flash write_image erase $write_args" fi printf '%s\n' \ 'init' \ "$1" \ "set rc [catch {$erase_step} e]; echo \"SOM erase=\$rc \$e\"" \ "if {\$rc == 0} { set rc [catch {$write_step} e]; echo \"SOM write=\$rc \$e\" }" \ "if {\$rc == 0} { set rc [catch {verify_image $write_args} e]; echo \"SOM verify=\$rc \$e\" }" \ 'if {$rc == 0} { reset run; echo "SOM result=pass" } else { echo "SOM result=fail" }' \ 'shutdown' >"$2" } t0=$(now) mkwrite 'reset halt' "$W/write.tcl" say "writing: erase ($erase), write, verify ..." ocd "$W/write.tcl" "$W/write.log" show "$W/write.log" | grep -E 'SOM (erase|write|verify)=|Error|Warn' if [ "$(mark result "$W/write.log")" != pass ]; then show "$W/write.log" # Copied with sed, not cp: busybox cp may use sendfile, which does not # complete on this runtime (glibc#14). keep=${TMPDIR:-/tmp}/som-nrf91-retry-$$ mkdir -p "$keep" && sed -n p "$W/prelude.tcl" >"$keep/prelude.tcl" if [ "$kind" = hex ]; then sed -n p "$hex_file" >"$keep/image.hex" write_args="$keep/image.hex" fi mkwrite halt "$keep/retry.tcl" echo "$PROG: the target was left HALTED and NOT reset. Do not power-cycle or reset the board." echo "$PROG: retry the write without a reset:" echo " openocd -f $keep/prelude.tcl -f $BOARD_CFG -f $keep/retry.tcl" case "$backup" in *.bin) echo "$PROG: the board's previous contents are in $backup" ;; *.pages) echo "$PROG: the pages the erase cleared are listed in $backup; write each back with --image --addr
" ;; esac echo "$PROG: last resort, natively on a host: nrfutil device recover, then program the full .hex (UICR included)" fail "erase/write/verify failed -- the flash is in an unknown state" fi say "written: verified and reset ($(( $(now) - t0 )) s)" # --- 6. did it boot? ------------------------------------------------------------------------ booted="not checked (no console)" if [ -n "$console" ]; then booted=$(boot_version) say "booted: $booted" if [ -n "$expect_ver" ]; then case "$booted" in *"$expect_ver"*) ;; *) fail "written and verified, but the board did not boot as '$expect_ver' (it says: ${booted:-nothing})" ;; esac fi fi say "PASS -- wrote $image ($sum) to the nRF9151; booted: $booted [$(took)]"