. "$BST_LIB" require stm32flash stm32flash/present # stm32flash 0.7 (meta-oe recipes-support) -- Geoffrey McRae's flash program for # the ST serial bootloader. It opens a tty (or an /dev/i2c-* bus), sends the # 0x7F autobaud byte, walks the GET / GET-VERSION / GET-ID handshake of AN3155, # looks the returned product id up in its compiled-in device table, and then # reads, writes, erases, CRCs or jumps into flash. # # THIS IMAGE EXPOSES NO TTY THAT WILL OPEN, AND NO STM32 ON THE FAR END OF ONE. # There is no /dev/ttyS0 -- stm32flash's own documented example device -- and # /dev/ttySER0, which the wasm32 driver registers on every image, refuses open(2) # with -ENODEV until a host WebSerial port attaches (the grant boundary, # linux-in-the-browser#60 M2). That is a property of the platform, and the probe # near the end of this file establishes it rather than assuming it. # # WHAT MAKES THE PROTOCOL ASSERTABLE ANYWAY is two facts about the source, both # read out of stm32flash-0.7 and both confirmed by running it: # # 1. serial_common.c maps a baud rate of 0 to SERIAL_BAUD_KEEP, and # serial_posix.c::serial_posix_open() answers that with the comment "user # takes care of all port setup" and a bare open(device, O_RDWR|O_NOCTTY) -- # no isatty, no tcgetattr, no tcsetattr. So `-b 0 ` makes AN ORDINARY # FILE a valid port. Without -b 0 the same file reaches serial_setup(), # whose tcsetattr fails on a non-tty; both halves are asserted below. # # 2. A regular file has ONE shared offset for reads and writes. stm32flash's # protocol is strictly alternating -- write a frame, read the answer -- so a # file whose bytes are laid out at the offsets the READS land on is a # byte-exact bootloader responder, and the bytes stm32flash WROTE are left # in the same file at the offsets the writes landed on, where od can check # them frame by frame. _boot() below is that responder. # # So this suite asserts FOR REAL, with no tty and no STM32 anywhere: # # the usage and version banners and which stream each uses; the whole of the # argument validation including the built-in baud-rate table; the Intel-HEX and # raw-binary parsers and the fall-back between them; the i2c device-name # routing; the port-open failure ladder and the exit code the ticket names; the # AN3155 handshake end to end (INIT, GET-VERSION, GET, GET-ID) with the emitted # frames checked byte for byte; the DEVICE TABLE LOOKUP and the full device # dump for product id 0x410; and Read Memory, Write Memory, Erase and Go, # each verified both by the data that crossed and by the frame that carried it. # # PROBED, then skipped -- only the handshake over /dev/ttySER0, the one thing # that genuinely needs the port. The probe is stm32flash's own open of it, so a # probe that fails IS the port failing rather than a stand-in for it. # # Every invocation is bounded by `timeout`. Nothing here can block: every read # stm32flash makes is against a regular file, where a short read is EOF and # serial_posix_read turns it straight into PORT_ERR_TIMEDOUT. # # Every expectation below was executed against stm32flash 0.7 built natively on # x86-64 before it was written down, rc, text and bytes compared. TO="bst_bound 20" t=${TMPDIR:-/tmp}/bst-stm32flash.$$ mkdir -p "$t" # A device name nothing can open, for the port-open ladder. ABSENT=/dev/bst-stm32flash-absent # _case : ok iff exits AND its # combined output contains . stm32flash puts its diagnostics on stderr # and its progress on stdout, and several cases care about both, so the two are # merged here and separated explicitly in the cases that pin a stream. _case() { _cid=$1; _cwant=$2; _cn=$3; shift 3 _co=$($TO "$@" &1); _cr=$? case "$_co" in *"$_cn"*) _ch=1 ;; *) _ch=0 ;; esac if [ "$_cr" -eq "$_cwant" ] && [ "$_ch" -eq 1 ]; then bst_ok "$_cid" elif bst_execfail "$_co"; then bst_unrun "$_cid" "$_co" else bst_notok "$_cid" "rc=$_cr want=$_cwant needle:'$_cn' got:'$(_bst_1line "$_co")'"; fi } # _hex : the whole file as lower-case space-separated hex on one line. # od wraps at sixteen bytes, so the newline becomes a separator rather than # being deleted -- dropping it would silently glue byte 16 to byte 17. _hex() { od -An -tx1 "$1" | tr '\n' ' ' | tr -s ' ' | sed 's/^ //;s/ $//'; } # _hex_tail : the same, from offset 33 -- the exchanges that follow the # handshake. Each byte occupies three columns ("xx "), so byte 33 begins at # column 100. cut is used rather than `od -j` because busybox's od is not # guaranteed to carry the skip option. _hex_tail() { _hex "$1" | cut -c100-; } # _boot [extra] : write a file-backed STM32 bootloader responder. # # The 33 bytes below are the answers to the four exchanges stm32_init() makes, # each sitting at exactly the offset that exchange's read lands on. The bytes at # the offsets stm32flash WRITES to are filler and are overwritten by the run: # # 0 W 0x7F the INIT autobaud byte # 1 R 0x79 ACK # 2-3 W GET-VERSION frame 0x01 0xFE # 4 R 0x79 ACK # 5-7 R version 0x31, option1 0x00, option2 0x00 (PORT_GVR_ETX) # 8 R 0x79 ACK # 9-10 W GET frame 0x00 0xFF # 11 R 0x79 ACK # 12 R 0x0B -- eleven command bytes follow the version byte # 13 R bootloader version 0x31 # 14-24 R the eleven supported commands # 25 R 0x79 ACK # 26-27 W GET-ID frame 0x02 0xFD # 28 R 0x79 ACK # 29 R 0x01 -- two id bytes follow # 30-31 R product id 0x0410 # 32 R 0x79 ACK # # 0x410 is chosen because it is the first entry of dev_table.c and the one whose # geometry the dump below pins. $2, when given, is appended: for Read, Write, # Erase and Go, whose own exchanges continue from offset 33. _boot() { printf '\000\171\000\000\171\061\000\000\171\000\000\171\013\061\000\001\002\021\041\061\103\143\163\202\222\171\000\000\171\001\004\020\171' > "$1" [ -n "$2" ] && printf "$2" >> "$1" : } # --- the package ------------------------------------------------------------- check stm32flash/path test -x /usr/bin/stm32flash check_out stm32flash/on-path "/usr/bin/stm32flash" command -v stm32flash # The page is packaged UNCOMPRESSED -- stm32flash-doc carries # /usr/share/man/man1/stm32flash.1 with no .gz, checked against the built # package rather than assumed from the .gz other ports here ship. check stm32flash/manpage test -s /usr/share/man/man1/stm32flash.1 # --- the usage banner -------------------------------------------------------- # -h is the only path in the program that exits 0 from parse_options, and it # writes to STDERR: show_help() is one fprintf(stderr, ...). Both halves are # pinned, because a port that moved the banner to stdout would still pass a # 2>&1 needle test. _case stm32flash/help-rc 0 "Usage: " stm32flash -h _case stm32flash/help-usage 0 "[-bvngfhc] [-[rw] filename] [tty_device | i2c_device]" stm32flash -h _case stm32flash/help-baud 0 "-b rate Baud rate (default 57600)" stm32flash -h _case stm32flash/help-start 0 "-S address[:length]" stm32flash -h _case stm32flash/help-frame 0 "-F RX_length[:TX_length]" stm32flash -h _case stm32flash/help-gpio 0 "'dtr', 'rts' or 'brk' representing serial port signal" stm32flash -h _case stm32flash/help-example 0 "Read 100 bytes of flash from 0x1000 to stdout:" stm32flash -h _hs=$($TO stm32flash -h 2>/dev/null &1 >/dev/null) check_eq stm32flash/baud-table-count "26" "$(printf '%s\n' "$_bt" | grep -c '^ [0-9][0-9]*$')" check_eq stm32flash/baud-table-first "1200" "$(printf '%s\n' "$_bt" | sed -n '2p' | tr -d ' ')" check_eq stm32flash/baud-table-last "4000000" "$(printf '%s\n' "$_bt" | sed -n '27p' | tr -d ' ')" # getopt's own complaint for an unknown letter is NOT fatal here: parse_options # has no `case '?'`, so the loop simply continues and the run goes on to the # port. Both halves are asserted, because "it printed a diagnostic" and "it # carried on" are separate claims. _case stm32flash/unknown-option-diag 1 "invalid option" stm32flash -Z "$ABSENT" _case stm32flash/unknown-option-goes-on 1 "Failed to open port: $ABSENT" stm32flash -Z "$ABSENT" # --- the port-open failure ladder, and the exit code ------------------------ # port_open() walks its interface list; serial_posix refuses a name it cannot # open and i2c refuses anything not called /dev/i2c-*, so all three lines below # are emitted in order and main() exits 1. This is the documented status for a # port that cannot be opened, and it is what the ticket asks be pinned. _case stm32flash/probe-serial-name 1 'Error probing interface "serial_posix"' stm32flash "$ABSENT" _case stm32flash/cannot-handle 1 "Cannot handle device \"$ABSENT\"" stm32flash "$ABSENT" _case stm32flash/failed-open 1 "Failed to open port: $ABSENT" stm32flash "$ABSENT" check_rc stm32flash/open-absent-rc 1 $TO stm32flash "$ABSENT" # --- a file that opens but is not a tty -------------------------------------- # Without -b 0 the port goes through serial_setup(), which is the whole termios # path: cfmakeraw, cfsetispeed/cfsetospeed, tcsetattr, and a read-back that # compares every one of c_iflag/c_oflag/c_cflag/c_lflag. On a regular file # isatty(2) is false -- a warning only -- and tcsetattr(2) fails with ENOTTY, # which is the "Failed to apply settings" below. Both are real assertions about # this kernel's termios, not about stm32flash. : > "$t/plain" _case stm32flash/not-a-tty 1 "Warning: Not a tty: $t/plain" stm32flash "$t/plain" _case stm32flash/termios-refused 1 "Error: Failed to apply settings" stm32flash "$t/plain" # --- i2c device-name routing ------------------------------------------------- # The i2c back end claims a device iff the name starts with /dev/i2c-, so these # two reach i2c_open() with no i2c bus anywhere. The address check happens before # the open, which is why the out-of-range arm reports the range and not the file. _case stm32flash/i2c-addr-range 1 "I2C address out of range [0x03-0x77]" stm32flash -a 0x02 /dev/i2c-bst-absent _case stm32flash/i2c-open-fails 1 'Unable to open special file "/dev/i2c-bst-absent"' stm32flash -a 0x39 /dev/i2c-bst-absent _case stm32flash/i2c-probe-name 1 'Error probing interface "i2c"' stm32flash -a 0x39 /dev/i2c-bst-absent # --- the version banner and which stream it uses ----------------------------- # The banner is printed to `diag`, which is stdout for every action except # `-r -`: reading flash TO STDOUT moves diag to stderr so the two do not mix. # That switch is the reason the second pair below is worth its own cases. _case stm32flash/banner-version 1 "stm32flash 0.7" stm32flash "$ABSENT" _case stm32flash/banner-url 1 "http://stm32flash.sourceforge.net/" stm32flash "$ABSENT" _bo=$($TO stm32flash "$ABSENT" 2>/dev/null /dev/null "$t/hex" _case stm32flash/hex-parser 1 "Using Parser : Intel HEX" stm32flash -w "$t/hex" "$ABSENT" _case stm32flash/hex-location 1 "Location : 0x8000000" stm32flash -w "$t/hex" "$ABSENT" _case stm32flash/hex-size 1 "Size : 16" stm32flash -w "$t/hex" "$ABSENT" # One byte of the data record's checksum is wrong. PARSER_ERR_INVALID_FILE sends # main() down its silent fall-back to the binary parser, which then reports the # file's own byte length -- so the size below is the proof the checksum was # actually validated rather than the record merely parsed. printf ':10000000000400205D010008610100086301000899\n:00000001FF\n' > "$t/badck" _case stm32flash/hex-bad-checksum-falls-back 1 "Using Parser : Raw BINARY" stm32flash -w "$t/badck" "$ABSENT" _case stm32flash/hex-bad-checksum-size 1 "Size : 56" stm32flash -w "$t/badck" "$ABSENT" printf '\001\002\003\004' > "$t/bin" _case stm32flash/binary-forced 1 "Size : 4" stm32flash -w "$t/bin" -f "$ABSENT" _case stm32flash/parser-missing-file 1 "Intel HEX ERROR: System Error" stm32flash -w "$t/nosuch" "$ABSENT" # --- the handshake with nothing on the far end ------------------------------- # An EMPTY port file is the honest picture of a wire with no STM32 on it: the # INIT byte is written, the read that follows hits EOF, serial_posix_read turns # a zero-length read into PORT_ERR_TIMEDOUT, and stm32_send_init_seq() retries # once and gives up with the message below. This is what the ticket calls the # handshake failing for want of a device, asserted rather than assumed. : > "$t/empty" _case stm32flash/init-timeout 1 "Failed to init device, timeout." stm32flash -b 0 "$t/empty" check_eq stm32flash/init-sent-two-init-bytes "7f 7f" "$(_hex "$t/empty")" # -b 0 is the no-termios path, so the isatty warning that the plain case above # asserts must be ABSENT here. That is the assertion that the two paths are # genuinely different rather than one path that happens to work twice. _io=$($TO stm32flash -b 0 "$t/empty" &1) case "$_io" in *"Not a tty"*) bst_notok stm32flash/baud-keep-skips-termios "isatty checked on the SERIAL_BAUD_KEEP path: '$(_bst_1line "$_io")'" ;; *) bst_ok stm32flash/baud-keep-skips-termios ;; esac # --- the handshake, for real ------------------------------------------------- # The full AN3155 sequence against the file-backed responder. rc is 0: with no # action requested stm32flash's whole job is to identify the device. _boot "$t/id" _idout=$($TO stm32flash -b 0 "$t/id" &1); _idrc=$? if bst_execfail "$_idout"; then for _c in handshake-rc iface-cfg-empty bl-version option1 option2 device-id \ devtable-ram devtable-flash devtable-optram devtable-sysram handshake-frames; do bst_unrun "stm32flash/$_c" "$_idout" done else check_eq stm32flash/handshake-rc "0" "$_idrc" # SERIAL_BAUD_KEEP leaves setup_str empty, so the interface line ends at the # colon -- the one visible difference a KEEP port makes in the output. _n() { case "$_idout" in *"$2"*) bst_ok "stm32flash/$1" ;; *) bst_notok "stm32flash/$1" "want:'$2' got:'$(_bst_1line "$_idout")'" ;; esac; } _n iface-cfg-empty "Interface serial_posix: " _n bl-version "Version : 0x31" _n option1 "Option 1 : 0x00" _n option2 "Option 2 : 0x00" # THE DEVICE TABLE. The id 0x410 is answered by the responder; everything to # the right of it, and every line below it, is read out of the compiled-in # dev_table.c entry -- the name, the RAM window and the bootloader's 512-byte # reservation in it, the flash size and first-sector geometry, the option-byte # count and the system-memory size. A table that failed to build, or an entry # that came out of the port with the wrong widths, changes these. _n device-id "Device ID : 0x0410 (STM32F10xxx Medium-density)" _n devtable-ram "- RAM : Up to 20KiB (512b reserved by bootloader)" _n devtable-flash "- Flash : Up to 128KiB (size first sector: 4x1024)" _n devtable-optram "- Option RAM : 16b" _n devtable-sysram "- System RAM : 2KiB" # And the other direction: the bytes stm32flash PUT ON THE WIRE, left in the # port file at the offsets it wrote them to. 7f is INIT; 01 fe, 00 ff and # 02 fd are GET-VERSION, GET and GET-ID with the AN3155 complement byte. check_eq stm32flash/handshake-frames \ "7f 79 01 fe 79 31 00 00 79 00 ff 79 0b 31 00 01 02 11 21 31 43 63 73 82 92 79 02 fd 79 01 04 10 79" \ "$(_hex "$t/id")" fi # --- Read Memory, for real --------------------------------------------------- # Four bytes at the flash base. The responder's tail carries the three ACKs the # read exchange needs and then the four bytes of "flash content", so the file # stm32flash writes can be compared byte for byte -- and the frame it sent # (11 ee, the address big-endian with its XOR, then the length-1 byte 03 fc) # is left in the port file to be compared with it. _boot "$t/rd" '\000\000\171\000\000\000\000\000\171\000\000\171STM3' _rdout=$($TO stm32flash -b 0 -r "$t/rd.out" -S 0x08000000:4 "$t/rd" &1); _rdrc=$? if bst_execfail "$_rdout"; then bst_unrun stm32flash/read-rc "$_rdout"; bst_unrun stm32flash/read-progress "$_rdout" bst_unrun stm32flash/read-bytes "$_rdout"; bst_unrun stm32flash/read-frame "$_rdout" else check_eq stm32flash/read-rc "0" "$_rdrc" case "$_rdout" in *"Read address 0x08000004 (100.00%)"*) bst_ok stm32flash/read-progress ;; *) bst_notok stm32flash/read-progress "got:'$(_bst_1line "$_rdout")'" ;; esac check_eq stm32flash/read-bytes "53 54 4d 33" "$(_hex "$t/rd.out")" check_eq stm32flash/read-frame \ "11 ee 79 08 00 00 00 08 79 03 fc 79 53 54 4d 33" \ "$(_hex_tail "$t/rd")" fi # --- Write Memory, for real -------------------------------------------------- # -e 0 sets no_erase, so this is the write exchange alone. The frame left behind # carries 31 ce (Write Memory), the address with its XOR, then the length-1 byte, # the four payload bytes and the XOR checksum 07 over all five -- which is the # whole of AN3155's write frame, checked as bytes rather than as a claim. printf 'ABCD' > "$t/payload" _boot "$t/wr" '\000\000\171\000\000\000\000\000\171\000\000\000\000\000\000\171' _wrout=$($TO stm32flash -b 0 -e 0 -S 0x08000000:4 -w "$t/payload" "$t/wr" &1); _wrrc=$? if bst_execfail "$_wrout"; then bst_unrun stm32flash/write-rc "$_wrout"; bst_unrun stm32flash/write-progress "$_wrout" bst_unrun stm32flash/write-frame "$_wrout" else check_eq stm32flash/write-rc "0" "$_wrrc" case "$_wrout" in *"Wrote address 0x08000004 (100.00%)"*) bst_ok stm32flash/write-progress ;; *) bst_notok stm32flash/write-progress "got:'$(_bst_1line "$_wrout")'" ;; esac check_eq stm32flash/write-frame \ "31 ce 79 08 00 00 00 08 79 03 41 42 43 44 07 79" \ "$(_hex_tail "$t/wr")" fi # --- Write with verify, for real --------------------------------------------- # -v makes main() read each block back and compare it byte for byte with what it # sent. Upstream declares the comparison buffer as `uint8_t compare[len]`, a VLA # whose address escapes into stm32_read_memory on a frame that also sits on a # cross-frame setjmp/longjmp unwind path -- the shape the coroutinize pass once # refused outright (hwjs-cc#15) and now gives heap storage. The case asserts # that code path rather than merely the link: the responder returns the same # four bytes that were written, and "and verified" in the progress line is the # comparison having run and agreed. printf 'ABCD' > "$t/vpl" _boot "$t/vf" '\000\000\171\000\000\000\000\000\171\000\000\000\000\000\000\171\000\000\171\000\000\000\000\000\171\000\000\171ABCD' _vfout=$($TO stm32flash -b 0 -e 0 -S 0x08000000:4 -w "$t/vpl" -v "$t/vf" &1); _vfrc=$? if bst_execfail "$_vfout"; then bst_unrun stm32flash/verify-rc "$_vfout"; bst_unrun stm32flash/verify-progress "$_vfout" bst_unrun stm32flash/verify-frame "$_vfout" else check_eq stm32flash/verify-rc "0" "$_vfrc" case "$_vfout" in *"Wrote and verified address 0x08000004 (100.00%)"*) bst_ok stm32flash/verify-progress ;; *) bst_notok stm32flash/verify-progress "got:'$(_bst_1line "$_vfout")'" ;; esac # The write frame followed by the read-back frame, in one dump: the second # half is the verify read that the patched buffer receives. check_eq stm32flash/verify-frame \ "31 ce 79 08 00 00 00 08 79 03 41 42 43 44 07 79 11 ee 79 08 00 00 00 08 79 03 fc 79 41 42 43 44" \ "$(_hex_tail "$t/vf")" fi # --- Erase and Go, for real -------------------------------------------------- # -o with no page list is the mass erase: 43 bc, then ff 00, which is AN3155's # "all pages" special code and its checksum. _boot "$t/er" '\000\000\171\000\000\171' _erout=$($TO stm32flash -b 0 -o "$t/er" &1); _errc=$? if bst_execfail "$_erout"; then bst_unrun stm32flash/erase-rc "$_erout"; bst_unrun stm32flash/erase-frame "$_erout" else check_eq stm32flash/erase-rc "0" "$_errc" check_eq stm32flash/erase-frame \ "43 bc 79 ff 00 79" \ "$(_hex_tail "$t/er")" fi # -g runs in main()'s close: block, after everything else has finished, and it # is the only case here whose message is printed as two halves around the # exchange -- so the text asserts the "done." that only an ACK produces. _boot "$t/go" '\000\000\171\000\000\000\000\000\171' _goout=$($TO stm32flash -b 0 -g 0x08000000 "$t/go" &1) if bst_execfail "$_goout"; then bst_unrun stm32flash/go-done "$_goout"; bst_unrun stm32flash/go-frame "$_goout" else case "$_goout" in *"Starting execution at address 0x08000000... done."*) bst_ok stm32flash/go-done ;; *) bst_notok stm32flash/go-done "got:'$(_bst_1line "$_goout")'" ;; esac check_eq stm32flash/go-frame \ "21 de 79 08 00 00 00 08 79" \ "$(_hex_tail "$t/go")" fi # --- probe: is there a serial port on this machine? -------------------------- # The probe is stm32flash's OWN open of /dev/ttySER0 on the SERIAL_BAUD_KEEP # path -- the same open(2) the real handshake would make, with no termios in the # way to fail first. Three worlds are self-consistent and one is not: # # no /dev/ttySER0 -> no bootloader handshake over a tty is possible. # /dev/ttySER0, open refused -> the grant boundary; no host WebSerial port is # attached (linux-in-the-browser#60 M2), the same # unattended state 59-picocom and 60-jpnevulator # record for the same device. # /dev/ttySER0, open granted -> RUN the handshake against whatever is there. # # A refusal is not a red: it is this image's documented unattended state. if [ ! -c /dev/ttySER0 ]; then bst_ok stm32flash/probe-serial _why="/dev/ttySER0 is absent on this kernel" _have=0 else bst_ok stm32flash/probe-serial _so=$($TO stm32flash -b 0 -c /dev/ttySER0 &1) case "$_so" in *"Failed to open port: /dev/ttySER0"*) _have=0 _why="stm32flash's own open of /dev/ttySER0 was refused: $(_bst_1line "$_so")" ;; *) _have=1 ;; esac fi if [ "$_have" -eq 0 ]; then bst_skip stm32flash/tty-handshake "$_why" else # A port that opens with no STM32 behind it is not a failure of the port, so # the init timeout is reported as the absence it is rather than as a red. _to=$($TO stm32flash -b 0 /dev/ttySER0 &1); _tor=$? if [ "$_tor" -eq 0 ]; then bst_ok stm32flash/tty-handshake elif [ "$_tor" -eq 124 ]; then bst_skip stm32flash/tty-handshake "/dev/ttySER0 opened but nothing answered within the bound" else case "$_to" in *"Failed to init device"*) bst_skip stm32flash/tty-handshake "/dev/ttySER0 opened; no STM32 answered the init sequence" ;; *) bst_notok stm32flash/tty-handshake "rc=$_tor $(_bst_1line "$_to")" ;; esac fi fi rm -rf "$t"