From 68e60a7483226c84888b0defc747be4c1bedb2d9 Mon Sep 17 00:00:00 2001 From: David Garske Date: Thu, 20 Aug 2026 18:57:53 -0700 Subject: [PATCH] Add NXP i.MX95 post-quantum demo for the A55 cluster and Cortex-M7 --- README.md | 1 + imx95-pqc-demo/.gitignore | 5 + imx95-pqc-demo/README.md | 173 +++++++ imx95-pqc-demo/charts/imx95-m7-code-size.png | Bin 0 -> 29653 bytes .../charts/imx95-m7-verify-time.png | Bin 0 -> 23308 bytes imx95-pqc-demo/container/Dockerfile | 68 +++ imx95-pqc-demo/container/build-aarch64.sh | 98 ++++ imx95-pqc-demo/container/build-image.sh | 43 ++ imx95-pqc-demo/container/docker-compose.yml | 42 ++ imx95-pqc-demo/container/entrypoint.sh | 133 ++++++ imx95-pqc-demo/demo/demo-run.sh | 39 ++ imx95-pqc-demo/demo/install-autostart.sh | 26 ++ imx95-pqc-demo/demo/m7-console-tail.sh | 49 ++ imx95-pqc-demo/demo/m7-rpmsg-log.sh | 40 ++ imx95-pqc-demo/demo/m7-start.sh | 27 ++ imx95-pqc-demo/demo/stage.sh | 41 ++ imx95-pqc-demo/demo/twopane.py | 177 ++++++++ imx95-pqc-demo/demo/wolfssl-demo.service | 30 ++ imx95-pqc-demo/gallery/README.md | 116 +++++ imx95-pqc-demo/gallery/docker-compose.yml | 48 ++ imx95-pqc-demo/gallery/publish.sh | 52 +++ imx95-pqc-demo/m7/build.sh | 36 ++ imx95-pqc-demo/m7/imx95_wolfboot.overlay | 67 +++ imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt | 4 + imx95-pqc-demo/m7/zephyr-app/prj.conf | 35 ++ imx95-pqc-demo/m7/zephyr-app/src/main.c | 426 ++++++++++++++++++ imx95-pqc-demo/tools/memtool.c | 259 +++++++++++ 27 files changed, 2035 insertions(+) create mode 100644 imx95-pqc-demo/.gitignore create mode 100644 imx95-pqc-demo/README.md create mode 100644 imx95-pqc-demo/charts/imx95-m7-code-size.png create mode 100644 imx95-pqc-demo/charts/imx95-m7-verify-time.png create mode 100644 imx95-pqc-demo/container/Dockerfile create mode 100755 imx95-pqc-demo/container/build-aarch64.sh create mode 100755 imx95-pqc-demo/container/build-image.sh create mode 100644 imx95-pqc-demo/container/docker-compose.yml create mode 100755 imx95-pqc-demo/container/entrypoint.sh create mode 100755 imx95-pqc-demo/demo/demo-run.sh create mode 100755 imx95-pqc-demo/demo/install-autostart.sh create mode 100755 imx95-pqc-demo/demo/m7-console-tail.sh create mode 100755 imx95-pqc-demo/demo/m7-rpmsg-log.sh create mode 100755 imx95-pqc-demo/demo/m7-start.sh create mode 100755 imx95-pqc-demo/demo/stage.sh create mode 100644 imx95-pqc-demo/demo/twopane.py create mode 100644 imx95-pqc-demo/demo/wolfssl-demo.service create mode 100644 imx95-pqc-demo/gallery/README.md create mode 100644 imx95-pqc-demo/gallery/docker-compose.yml create mode 100755 imx95-pqc-demo/gallery/publish.sh create mode 100755 imx95-pqc-demo/m7/build.sh create mode 100644 imx95-pqc-demo/m7/imx95_wolfboot.overlay create mode 100644 imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt create mode 100644 imx95-pqc-demo/m7/zephyr-app/prj.conf create mode 100644 imx95-pqc-demo/m7/zephyr-app/src/main.c create mode 100644 imx95-pqc-demo/tools/memtool.c diff --git a/README.md b/README.md index 608a0d1..33ce7c0 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This repository contains example applications using [wolfBoot](https://github.co * wolfSSH SCP transfer firmware update mechanism, with [freeRTOS on Freescale K64F](freeRTOS-Freescale-K64F-scp) * BLE-GATT FOTA service using [RIOT-OS and Nimble on Nordic nRF52](riotOS-nrf52840dk-ble) * Measured boot demo using [wolfTPM on STM32F4](test-app-STM32F4-measured-boot) + * Post-quantum demo on both clusters of the [NXP i.MX95](imx95-pqc-demo): ML-DSA-87 verified boot of the Cortex-M7 alongside wolfCrypt ML-KEM/ML-DSA benchmarks on the Cortex-A55 cluster ## License diff --git a/imx95-pqc-demo/.gitignore b/imx95-pqc-demo/.gitignore new file mode 100644 index 0000000..7bd2981 --- /dev/null +++ b/imx95-pqc-demo/.gitignore @@ -0,0 +1,5 @@ +# Zephyr build output (m7/build.sh writes here) +m7/build/ +# wolfSSL source export and cross-build trees (container/build-aarch64.sh) +container/wolfssl-src/ +container/build/ diff --git a/imx95-pqc-demo/README.md b/imx95-pqc-demo/README.md new file mode 100644 index 0000000..709ede7 --- /dev/null +++ b/imx95-pqc-demo/README.md @@ -0,0 +1,173 @@ +# Post-quantum on both clusters of the NXP i.MX95 + +A two-pane demo on a single screen, running entirely on a Toradex SMARC iMX95: + +| pane | what it shows | +|---|---| +| **left** | wolfCrypt ML-KEM / ML-DSA benchmarks in a container on the six Cortex-A55 cores, under Torizon OS | +| **right** | wolfBoot performing **ML-DSA-87 verified boot** of the Cortex-M7, with its console relayed to Linux | + +The point of the pairing: the same post-quantum algorithms, at the same security +level, on two very different cores of one SoC - a Linux application cluster and +a bare-metal real-time core. + +## Measured on hardware + +Cortex-M7 at 800 MHz, DWT cycle counter, caches enabled: + +| | wolfBoot text | verify + boot | at 800 MHz | +|---|---|---|---| +| ECDSA P-256 | 22,548 B | 1,741,288 cycles | 2.177 ms | +| **ML-DSA-87** | **21,608 B** | **4,201,179 cycles** | **5.251 ms** | +| ML-DSA-87, Zephyr payload | 21,608 B | 5,916,540 cycles | 7.396 ms | + +Two results worth calling out. **Post-quantum verified boot costs 5.25 ms** - +2.4x the cycles of ECDSA P-256 and irrelevant against any real boot time. And +the **ML-DSA-87 bootloader is 940 bytes smaller than the ECDSA one**: ML-DSA +verification is SHAKE plus polynomial arithmetic and never pulls in the +big-integer math P-256 needs, so for a verify-only workload post-quantum can +cost *less* flash. + +Verification also scales far better than payload size suggests - 1,004 B to +54,080 B is **54x the payload for 1.41x the verify**, because the lattice +signature check is a fixed cost and only the hash grows. + +> The M7's I- and D-caches are **disabled out of reset**. Any i.MX95 M7 +> benchmark taken without enabling them is wrong by one to two orders of +> magnitude - we measured SHA-256 at 1.1 MiB/s before enabling them and +> 29.2 MiB/s after. + +## Layout + +``` +container/ wolfCrypt PQC benchmark container for the A55 cluster (left pane) +m7/ Zephyr RPMsg payload wolfBoot verifies and boots (right pane) +demo/ board-side orchestration and the two-pane renderer +gallery/ Torizon Demo Gallery submission: compose file and partner metadata +tools/ memtool - mmap-based /dev/mem access for the M7 console +charts/ the measured figures, as images +``` + +## Requirements + +- Toradex SMARC iMX95 (or another i.MX95 board) running Torizon OS +- wolfBoot with the `imx95_m7` target +- A Zephyr workspace (4.4.0 or newer) and an `arm-none-eabi` toolchain +- Docker on the board (Torizon ships it) + +## Building + +**1. wolfBoot for the M7**, signing with ML-DSA-87: + +```sh +cp config/examples/imx95-m7.config .config +make SIGN=ML_DSA ML_DSA_LEVEL=5 IMAGE_SIGNATURE_SIZE=4627 DEBUG_UART=1 +``` + +`DEBUG_UART=1` is what makes wolfBoot write its verification log to the +shared-memory console the right pane reads. + +**2. The Zephyr payload:** + +```sh +cd m7 && ZEPHYR_BASE=~/zephyrproject/zephyr ./build.sh +``` + +Then sign it with the same key wolfBoot was built with: + +```sh +IMAGE_HEADER_SIZE=12288 ML_DSA_LEVEL=5 ./tools/keytools/sign --ml_dsa --sha256 \ + m7/build/zephyr/payload.bin wolfboot_signing_private_key.der 1 +``` + +**3. The benchmark container:** + +```sh +cd container && WOLFSSL_REPO=/path/to/wolfssl ./build-aarch64.sh all && ./build-image.sh +``` + +## Running + +```sh +BOARD=torizon@ WOLFBOOT=/path/to/wolfboot ./demo/stage.sh +``` + +then on the board: + +```sh +sudo bash ~/demo/demo-run.sh /dev/tty1 # /dev/tty1 is the HDMI console +``` + +Omit the argument to render on the current terminal instead. + +### Starting it automatically + +Because replaying the demo means power-cycling the board (see below), running it +by hand also means logging back in afterwards, over a network link that does not +always come up. Installing the unit makes cutting and restoring power the entire +replay: + +```sh +sudo bash ~/demo/install-autostart.sh # enable +sudo bash ~/demo/install-autostart.sh --off # disable, restore the getty +``` + +It takes tty1 from `getty@tty1`, so the HDMI console is the demo rather than a +login prompt. + +### Reading the verify log over RPMsg + +The right pane does not need this, but the same log can be read as an ordinary +Linux tty: + +```sh +sudo bash ~/demo/m7-rpmsg-log.sh +``` + +Order matters. The payload relays the whole console ring as soon as its endpoint +has a destination address, which the host supplies while binding the channel - +strictly before `/dev/ttyRPMSG*` exists. No reader can be attached for that first +pass, and the log it sends is discarded by a tty nobody has open. The payload +therefore treats any byte written to the tty as a request to rewind and send the +log again, so the script attaches a reader first and then pokes. + +## Notes that will save you time + +**The M7 starts once per Linux boot.** `echo stop > .../state` fails on this BSP +with "Interrupted system call" and the core stays running, so replaying the boot +means a real power cycle of the board - not a restart. Plan the demo around +that. + +**The payload is not the upstream `openamp_rsc_table` sample, deliberately.** +That sample assumes Linux loaded *its* ELF: the remote declares vrings with +`da = FW_RSC_ADDR_ANY` and Linux writes the resolved addresses and the virtio +status back into the resource table it loaded. Here Linux loads **wolfBoot**, so +those values land in wolfBoot's table while the sample polls its own - and it +waits forever in `rproc_virtio_wait_remote_ready()`. This payload uses fixed +vring addresses matching wolfBoot's table and skips the wait, because there is +nothing to wait for: Linux registers virtio0 before it releases the core. + +**The right pane reads the console ring directly, not `rpmsg_tty`.** wolfBoot +writes its log before any RPMsg endpoint exists, so the ring is the only source +that contains the verification output - and reading it needs no driver, no +endpoint binding and no module load. + +**Nothing may sit unflushed in the page cache.** The replay beat is a hard power +cut, so a file written seconds earlier is simply gone after the next cycle - a +staged payload, or the autostart unit, silently reverts to what was there +before. `stage.sh` and `install-autostart.sh` both `sync`; verify a hand-copied +payload with `md5sum` rather than a timestamp. + +**Torizon has no tmux**, and its rootfs is read-only OSTree, so there is nothing +to install. `demo/twopane.py` renders two fixed columns with a full redraw +instead. + +**Do not quote the benchmark's "Cycles per byte" or "Cycles/op" columns on an +A55.** They derive from the 24 MHz generic timer rather than the 1.8 GHz core +clock and are wrong by roughly 75x. Use ops/sec and MB/s, or pass +`-freq 1800000000`. The demo renderer strips those columns for this reason. + +## Support + +wolfSSL is dual licensed under GPLv3 or a commercial license. Questions: +support@wolfssl.com diff --git a/imx95-pqc-demo/charts/imx95-m7-code-size.png b/imx95-pqc-demo/charts/imx95-m7-code-size.png new file mode 100644 index 0000000000000000000000000000000000000000..5db15d37b11e75d7190994d2790e1088c350f9b4 GIT binary patch literal 29653 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

Xp+bZCo4Ib`d3*i)}wPQ9@4 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ⅈ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 I4Y%0;0iWavX#fBK literal 0 HcmV?d00001 diff --git a/imx95-pqc-demo/charts/imx95-m7-verify-time.png b/imx95-pqc-demo/charts/imx95-m7-verify-time.png new file mode 100644 index 0000000000000000000000000000000000000000..622f8f5b9ba8266da6bcae27e5f52b482685432e GIT binary patch literal 23308 zcmeFZWl&sCyDv%-B!K`)a8Dq(1$PJ}KyYVpcN=`rfk1%Z2^!qpWpLL77+eR3Ft`tH zXZXMS-TTz7d#mn;Q@85Y-s=lpy?XWZWBtgln^0v%X>3eVOcWFpY?&{gRZ&pT*-=oQ zK%PHE_TYFOF`}TnLXr6_uI`b#cZa2?ewO|4P*L~tS#1)B`B7Iy9Yq^~GTew{R^|iN zUOSo)Q=J+j*COfq_fQ!wL~v)D+HcX~T&<>MbVQeNN5<(vb>PjleW!51JZfpT7Jnc^ zU?RyI3*zx*9WH-=|G=#n>-O|$&*xk<4qU^eq8S(0dOqa_txU+JJU-Qzf&Xft{0`!N zTru)Fc^CP(QWO+_pD#K8G*I4BjWhhK^*iK$h9Noe|C%9GPd@f*T7%R9LU-}hK`5E# z1nrvl?i&MChvygDlQJly)pB))y?#O^_Y5dj+6qu$yQZew&LrcJgWr>ngFV+_9%|Ru z*Ed&~{+3o`Xs)gr8c}j}gKHPfD47~{h9y0*lBL4WQFPolb~KLVUjqSXC}mF5w{+=@ zYk4?NK9=+phj~=1&rv*G;rJg3{$Es@>WM$7*xBH2xyHTuv>3&e7#!AopR0m??Nsy( zIrL*VQv2CW%bHFO4#sK3010z8sF|NIF%jD{gAZkv-l%s~vH#da*vOY0a6kZ9oQ&%)NA8-BWe;8T($DX)9T4Htx>9sPIiYnhzW#HpUcIu`+oYg1t zrh@$p=YRmz$LR{CNzpt)FmK}RL)ZyxdBv8i;!pG;{XqexW$q7``;*5` zyXUnY`QvoTG;{9nTHO5WW7;NI)<8wwn)^AbCC#>|jo%IV$IHub zI+bn-iQ1hfWgmIm5&1(3@>S#^C)Y9&E-0+4LsEAXt{+QNUXjE<;LKGATxt~JD(M)% zBvH^>%VnI9?v+I4(;looJQGubsetbk)stV98BiG$bMy1E@#>Ti>X@<)EVO~4`4l0M zGwrAf8MEIQE0h$rs203527b1CeU^~Ts(mt*xnT$v)|m+|2J8YerKkvtaib%Ay<8>q z3U#cQn;BguP6$fe$!%}*(IunPIDqg%sfxkmnw2?@-H0-K1tDG@Qz!X}oXQm3eXZmB zYlNMt+v2CTRVO@4Xc{NCIxWhNlrpQ4utvtBId~uL%{}A&*@GT8hty18rPeK}cKsH# zmxLB_fvv__2Ms;4b^nvPUy@h~b1wapj%MHPq-H5kHU-yzw9W_Bts?r0>eTr4^bf*9 zrpHmc*$hAP9F+zl8u}lELD6FebLVymF#)Niwb4sS*9)0PUuobD!_2l|acLFUpp%lK zRzn_Jm@2x>nJ)s?0sGPCyt5K45USKx$-6uayD#;epTM-8c&|Hr{!%G=0*}zt*u8I9 z_hSy#?H>>;I^-7fI?oNn3-R>A7j0ks0h#gJSBFhl=S0+XSYhp z3WqKjw)f4FPA@!L#{zoCaFxVyS{|MK8E%>aSNcxY{#TjI9UIAp+rcdRNvN`nx~SvME~6*ZG*~J`daD~k+G)W{Nbq-mx!ncimDTHHGl6(b zH)y#_A7lt}5_!Mf3X+;}m|m^?6+TpOavIqekgCT){nJLA26UKI4cgF2uEe>Aoaux6K?tL%sq6Rz&O|4HT0zAh$oIqfi1p$VdMFp1+uSm7i-#m3m;%s!@tvkMk10vh|sEf_^R<;U> zQZ$7$s%q1X*-=~ZC|17_O~4`}gY`xMRgWzhRgx-^fXx`paUby-i>rh}z*f`Zy-!@B z{Q>PS+9;NIERjo&og@Huet|lRV*m%C*S~HHkb1< zP{boqA*rZYA82>k6FtKoQDoSyM8xH{(I2(hSIYc~MZmj8;YGJw`OIpifSz?6+xROA ziUXMybEupzP&!m=KO+5Rd&ibXK99{^YG5+;TbwD5>2ST2?&tS+XDHgUkkrTsbylXdk*lAy9$w_3uz z#Wa8)yV*kDh^on=;jaYXrbtH1M%+!P9!DvaE%6s}G$i-AL(;uC83*-_4T4=)^+|ZV zyI}zIx(~u?j);hd_n^}n%f__ELwEgPB>yw=FE3(OsUF+J-sQ$%@SbpS+ z&c}HM?JtSPTzUk*MA^e2`P0;|^4J`PPaxfKt15SH_VRqUh)|c!z2Q`sQB7aRv*f*o zJOy1;e)6bQGC?gKK{~UMgg0{V=|D-RoKL`2Zq?-P zTaM<}#vw*ETI44A?l~P_cs!0lXk5Kv8LAUQ#tglm2-LU$L2ANY)=xrm7T8*GO^7$< zj1vjShhl?&%=AY_w6y&U$_QQ!d(v2h2*soSEZ5Gck)_nXLn^#6uR)WJfYgZ2vhY#9 zS1cMATTf+pje2{h3XM3TH>_|iI#d=H*_=Q-{{UHt1s zPB$=);CWrDTZ)UxrsSgh^3Up) zTGt|sFglyRMrB&dH*77g0+k?JFsHy|9PVH(;{^kPsmPz_820*?3 zO!kHO<*=>FxdmHB5!sx*gzhUGka=LSYiaz^s=&%1}-@h zdPMa;Ac??=WHOctOg|R&t9YTm&RUoXyjg9!wNO2);_&Ra>wby(g-{YCX_o+TB@uzy zh>%6KrXVS@+WLBmNIL3i3Q+vt&7@ty-6HB3x*pkR%}p>jox{TXybIJm4QnK>#w|m= z6Dv%iIIU5SLe$wcjvJW9Ej&<`Dl%X^z}GZRtx#Ck>btxrNs_{u$NMJv*UzdZm~4KI z`zx1t+3gSJKu_RONkxP=c(NhUQ1^1I?@l0p*B~5f#oZW1FHt(J6L~Y;F5rIYUuobg zLFq$nyikJ0Ynkk@Iuw8c%M%99*L6bzL$PGLE5E|Xf`>Y#3M(3Q-diHgAvwTRpMQaR{ab?%sss)+fh(`%E8Ap-1M&&mskC zc~dx6>;O9ZoHu0~RQgoBGC4Q=;$ZPAC`wk_(*};1v`aA>mQQ0`iW6T&|K!(gT6N4Z z_LI-%#FP)*ETCp%k{eA%p#f@KRubbMI|2_I>xy6Jr;rY|T1uU8tG?NKUmmbGn7n81`K3N zPvxluEbc^!2~!ppQp63s5(9|U!?K!oDW`keAnPf{WJFMdRIu!TCiw=MfMIC80#z$b zpV=yhQ#YP8j?#A<_vi1uJBJm@(}P!?XlTmO00X1HII5Dny;(LeCY~1N3qqg-&2}e6Kf#kHpa@pK(~^q&hZ^?D#hGYdTOGvyHp^xKiSTLnR3~s)51}z( zoX>q{pm#L98@lTAXe%gXZzG!+2QrTw6-rEh+R_}%qS~I3OuGF6R2ttFSM;}A3dbdR z9vNvm7#NgSbwOMu#o>y_=l44WL~Oc(ZQBcubqlqKr`D)P;G7ioDp!>p(}43mhI%uH zyE+Dkmj1}VA)$O?_u7+|$!a5&acDhDWeVp~29LK!6tN^)Z~KLltjW4U5#lF0hmq>` zI=y@bF{o_&^Z4wTj9k8=L9BhrY{tnsWq1DpT+!s$mRD0(DS4gO{P?QdyBs!Zhm&f@ zw390tAz5-yvLz*|M$x3{R~>0adNVD*4WT3>89@U32C8eJ+Vm8WrYLph0XTbbwyFsU*wLCFsQlz+ z$)A>lMgb8obEnxW**cQ*xl$&n(4_XOerFR=I%|t|2d_D?#{s17L=DjrdI!wW>8LvXV z)Zo`j8)sG&CFDy*wWts+^ekxmw~T%Y*G`}oeCjcYN){)j#fWb+EOf5ueMjt49agGR z@8tTf`FQ>=NOgiCug!kqqUbD#h=rAjxUu+SC4t$7E?w}B4POJ$B?=nLO3YQaIYdl+ zs4^v&w@*Yuv0=T=DW>o@7ev_4XRVn(DarFEI9J8AZ+N8o3)dFZp#3C1#XI9poiy@S zthS!o;au+xQuo1`w6c%^z^mfjzQm_*iEM`lw%HAlj?Y)BrPb%U9c{S}Jnnuvc8BIh z-vqQO%^JJVa=ctQ>ZpY^z8*NOfu3t&KjA@A<0vxDAaH4&vxeO^Com-;?xnNzjZ9w5 zlJ8I#vBMO;+FPU+Twi7#ox|YU+a$crmzO0|At;^nCwmeCW05P1e2M!8bD)xD09lbd`s5sd65ku+MI+W zPl~pB?2o_GPa94qUFCh#{R={2T&5Cvv$#hDxkz z=0OkO{q)l!$=SH6!$pR6HO%Q2gb0bYcS6N%y0<>nXYbB~sqOKbikIQh#bng_WzkA6 z@p8W!O_vUF^MSuc-6AIMR8hCcNPwA2IB#*A8Sh%-Dt2y0g8N^pYMS zXth1=@cx9OygL@NL;cP*8hqr*pIdGzU+VzKjwhlGD{DLW&11(1zKA5%`0jmMtqT6` z*lIw}N+TtiKNbJt z6HA`AYHMF9Bss<7EXYq3!c>h-ABs@=b+neN@{Ejky^8e0-qTxLHo=zcT{nGiWAX$4 z5Q#2slXJpWF28R{eDO*lN8p)vZ{l=*)e!JC2d++yv>>8m>+*xN_;40sf|@P%>$Q?| zEF!~|rqou8{;J*yLxLr5TfNdH-IXp@xUd6X7oX;Z`Wq}FD)E-l>#As0Wk8ZM|x|oygbyuNgTWo0~ zFxrb3k~z0|GTpOSpMMYqM0ooxrMx1aFN}~ zVN~dfo0jlFa(u;*%z6@9xlx*gSV$&g%Ma6zsj3rKqtIXIg2;W-1_B34UVS?W)J>qY ztGmwiw1ZXWRq%<`sms7_Usy|E#l5RCwH(S2DBf;?gJG+KqIl};BMZ(0#sUz(ZrwEn zWpo<|Xo%NA5*>Nl_G9G|q{&|So?Xz6%aWDy0b$Y!D48ja<8={lYz`<2 zO>Vty{ni(V*FU7&m}_^ED$;jy$B1`caB0&GksV~rX%;Qr;X7;<2o#}+r})^ceK;HU z1e|56mXMar?`ykg1m9>(@^(pK!56i-3Q!rUe1YUm?YDHV>Ju!A`0AUR7WM>rx#!LI z*^R1VTfG;mF89ZCx3j$RAqC^#zDbMRtyPx^i22l;uezzI??;_=6CDm{3BBVLt~`G& z9W>`LU{xNTo%?qcQRC{qRz9}0^~}M9 zT1ewTCc^@JeBMy_c4pD$xQxM3Qlfpkjfy^4A3-z`uU)uP^8y!b#k<%h-x8mStRvX*7z%E*Qid#UCkn+6H1SLC`6=sCk$$fqs-86)~KchAr|gs}HZFfns|sgQ5{A0sr-uXD4_QHNT# zuNT{%(I%ZWBL4z-J|O`f49SA#`*WY~?EyI6M|5F|T{j~kg|}C_k0MwhwR!G8b1AkM zrwv?spn8}N;>cYgkiy-%T}-(U%RMD}{O!yq=qD&GGX979F82%jbN6XA;M=pUk@$W^ z0IazD`Nt5a`V-tcuPd?xY+~03DjJDj(zwXaK5{(*NG^t;JP&V<{F29+Q2gt&3Ml_+ zq5MUz?O&}cBoy>d>!WC{%D=jP;95WCpUCHbhW*2a|C0`W1Px@iy(2D_*$X zd{X|7XyMYg_w_*D&j~}}T+^sJUi9LEfN*d(54*hn67Ux^tCn)|nTlLAsrD;f9 zz`viE5A*10+As-n4#8s5iRgbikzl^ZSFzL^ZKNsZ6bqP|nSDZ9zQ8y`2iAhyRSa_a z5Doiw9XI5Al%tpbtqt$;x&P2!9cOs2NzELEEJP~%9PfbmqEpMxJ&#FCIL>H_LYNSl z{Rs(_mbYJP7NKU3Th-d!?>s|s4EZ7D#*pA!@!&ZgV~a;tH0oz4q$0H)~HiTF}fv*eZ|1+WlTtymm<7@$0`~ zWac$b;3uV!`EuJsQj;GQraU9|m%r(9B$O(xX%#(Q1!!THN-Wz*h~j@<184DY9}+a( z#X^gmKX;QwU}NIGX}+4r2C`|To7*7(hRQZnWPBHCo}t&leyzGp=iE9RJNrYCrf#=m z7$_?iV`>%Mf)Jq6SI87?DVjHE^9ATfx2D68P>bbYvS+L`DW#XmUKhm`D|rTYvi;GRR)L&yJst1|)``iT@iOuT>yZ+t zks)n*NtL$iLh(@3{s=i@{IUxIa@rfQWW0;Ll4b=Qq~};*#0fdmyko3`m!;iq{*B95 ziL1psU3HsyNNwS-DP4`S=f!*+=1?2C(-1)->6ECravO=Q? z44P%3bIIaZ)-oF!9A*ab%i=iva`|9-*cDHu=rEo;m|SmjFNDFm2K+jPx7m`O`r!it z_H;Tsb#Zsupe1jVF78mq)4OtYeJpFh;=+ObG1ucEpCFSM@}x}|D!Una5v!}!z+;|t zn70>i!ZBrv#Rpzkv7*So11hDqKsp*OTH^|G0JIHD-sYcALaeJ;4XE9;sx_g8vG%8t z5z*t$k@zV7y6c+e5(z*x1N~pxPWw;HGc{>`LC>PqE&aG}T7TPOms6NOI?)C9TosC# zI>Uy2t*-*6-op`{p)hzN8lC%UTc())4An|4jHAxQA0vFs)QS4LKkUHkpWzJY!(B5 ze<6~R2;iw{bNSy^1-Dhi^e;c^%jwqmG#@D(7sjb@5k_LtM?6JYNvIUEi)xbpV(r0Y zI)$WhE&;TzkyJ!rhN}A>@L=NA%a!kOZ#%B(p9z*K7 z*;cbEN=2FjIs-`d7$PG?LF$mqFvSR1`c*uw<)-L*;h)zt@!$5~fz|?qj1gXf5{$N> z)&zF>;_GPN-LpFyLb!U$1i&sC-(~UD9!^NkM#5pIO6+p<8y&iQ;7u+ODMK5VAVl*^ z*x2{OXbw}g;fSyHE9J7_N0(rt`jhD&CcbdUA{hzEel~AxrWo=NkyO zY+lP8ykB;IV!~i-exbduk9$w8LOrN(7YlWUlc66%EU#>>{*Q_hBJJ@<5C6aiPEF=} zXSK7$(QlVmdgla0{;GcO4lA$YGRTG=7JcdnB5~@Wjf9de! zS%`p~It<2vzK635k(c9=Hn*}VdQetw}8Aw?NcJfo3j}a zYG(AE+cCxI+r0@^pl|nJL@65 z+;OTJZm;tYpLby3F*Kp$wwO*abtXr`wM023#&Y|!U_G6L@Jw0jcc4;)mv48ydZtAicpG_nh-o^*kZEBN>Aj@>E1 zO?UWULKIfwQ^pVu+hfGf;2TO0yUWvxA>C?Uzx3@sa~b5}6GiJ==SLv>`e%XqPZ=7T zcnGu~rb&4=5p8h3-PQ26)7@_BQk_BIf&#yFKNCC9FbQtuEg`jzZi<9U1AX_@8K&=HZMI>l>!>^^VBd{ z)Zeev{^J&#%}<_iEg}$ACO4P7Scz}J{AcM{@q}xnVyNwS1sNY@5+LQ9DCjfm82Bh& zQ=6j(h*1G|Kuoov$q>bFafeFjJlXvN!L3RjyO9H;c0S z&C_i1lP@LNeb>U&w)CJ*!?+a`S@esmvnLi%_bW5a&JblQa%VX$Yc!XbFT{>b^lkF7 zqdImR^3?XOv(v9PKI{y@6rZEFp6h4KarmH=(aLalM^S)^6#96amEiUh6vuD@(x|1$ zH|tS+n+}XtE|lhc4jB)(lZfl9?}2YNhe^EbPxMmOgT-U_XV)EVmwbdAOOkF)_)fT4!b0uKoVSJ9QaIDgUvW(Mm`(+B$SD_;ggkZ%w8hAW7wXJb zdyK1se_R{M3E)lQ0CBZVI96;P#;uP67in|eoSTVdHA zNSkJm^STT&hHx?n?`-E*nf66`2?`2-*+znOwz;cG01y#G6}@i5K{38O)W)Q*cgva> z4W)yXK3HYu{!REy*peLdHp3L{;Zy@w&4EpaQzY5|R&duxr+Ueq#-k6hn|MMtR zj)>NV?4V(FgIi(p+nJ#m!L1|9;ULfHT0pso@j@e0>k6FI0%`tV1it|X>(}CGA{f1uIT)LtJ9r8CBsLE* zC6LwEf*{m9(qD*SuSnC$%QUGA7kEIR#r%ppsH_w3QYUGsDRyuxQgsoB@v*0cA*dX*>jG@MO&3ySpv&E<5>Ot@^^LefS>Ieq!Sr! zi>~S0V`B_|wB&uR{OfC5i#^jQ7lOot(;CyinM8FhT6?ZjXX}|uAl0e`9)2N8j{>2KJK4E>*HXB#;*su=c2+E0@2JvVfnzC#ne{mYxY+Nj$?bTP^4NU zC>JA`489TUhyNVzD`52(%33S+=}n=A`=fAagKf6=+*}ZLxg6s3d8T& zBq1J~N@fvN>7K|aD`PR7kSUEY^K8>To(B9Ob0m|fM<~|nAq6-}5m>qxDp?%t%({t*?C9@TokBt78j5mg zqRr})-u=E)9Ky!3KKg*>>Of#rS{tN;?wis=lc}-#;FBn59MHZ0b7wIOU^%eRWJtQu z$tUP3Yhovd_ndUHFU^~f!sQni@5T^usz(k^#DVq$OF*{?GBskIawyp!+f>X;t|YC| z(|_$tLw)w`F^DJ3M}&?H&l;vs1x3s^Fw>svBnmbkm)MyuQfPR%?)x<3sJUB|=ec*# zm%YnxZ)PyzJCyTu(YLP`W*tZy(KhtGQy6u#&> z6@v~XsziJUHD8S$rbOIbj83>ka$()ZrkO2-mFGPGi53Zt=qAbO-kigx$#KGa%|sJg z#e9_v7Jsb53e*yyTg#cQ@B+DCJZ_ff+1`SDy2FYyTJW;X&#mVd4cd9l$Z<223sUMtAJ$&M(MR%mA$u9Etct8 zMCI{)zK*lBtQVA0_(9g?P6PP?ghVPNbSxPuj=TA>Pmr0@Ymu{-KsJk9+m3G?v$@3j z(xkZ`i!jGSF64a7|D-=$Z30T*FYUozq#B=3`biAgNQekj0T!iozvM6@&Q#)n9t*!V z41HQn4dnN~Z04u#la#xkt-g>!hhpQ>%zm(FC^2s3Ftv4`!zKcpWgx7t`{qJd+X9r0 zSr~?(%tNe&k=Y0iJWanVBv54mvZ)a$USv=CfaLqU42!T26CtHR1Xkm{QaPQ?&<^{< zy-LizdtB(Ps}rN4nZ3aWmXqshf6n(#;x>JC_2@B3k2EU!4{U^DX2!}zn8bQkLtVn7 z^Ya=<((WIOsA`%wbo|@SStkYX)%Q~(`zb@8!b@;Ha&MQOa+~7=p`l{d2QI3r8?lt! znU-Nr^QP?_)H!J>m}K>8)?3;a-bcl2IZCVlKVw0s`&Xa*mub9q#2t2wd>@QjM>#Ih zc4-8MI+3MGB(N+&!;=+o8)u{M;g^OiH980&%c;n}$OZhjs{8vBU6yU$AuF>K8U+rm zhnUYFkq+T?8kDW0a@QN=J)e)#8#%3yXwVVyV4)@2kcpa1)pn|e^4aJa&i+JM&01ZlfxQUmTHWbbJB2G#_fRYdu`{*d@Z^YYYC5Ki z%8yaR?>OewY>jPVy065iz*}HTvTKxHp{rnur)-vTp@f7t_uThWXKR_CMY#x{_-xxR zRvF&js8=_b{(M%a1+CiY8BbGLjuhjG!sLlkq3Mhcm>S9%uN1;JI5zXfV^nqHSM0!x zl)e(lgng+R3FC%gU^eCo#EbDD#B>lsy}qax<9At8OOHpJ>=Ewtg(~E-cWD^6ZZkg9 zT{CgT9*L-u%wo3R+HI7_PD|k~;hlBmCQ{-aXMQ|9-ifEh;H^*+ajqH*D8`5>h@^t` z$0omUED&J79T_ew!ib0z+Xr8Z%dkJtR$G?P#tasmvl=?(wgg#DMJxJT%ES9lB z74&cBK>5kRgQyF9W^lRguF0pPosjK`C6*DTCyIm~?8oxjb06@zv6~8HuoAF-0WI)wV3NgDBPYlUn^f$S9oM>0C5NV%X;@yjQJ_No z_DpGnEs0`6PrM|J%5H>Ne7}^fli5wD!7&SEh1Z$TuhsfbRBSt@#AwD#r9Rv!-^y^} z+eCxvbn}_;$DiS*~)QEcRhGZ+Q5^T*Wx?co|!)Q1yZ$%bvR`q)+7f< z1Ri!xzwb;Wxu8+#l8Np^Pj2y7%)SC@3@`tfsiE=;NayPMcU{?PM6Td*uM|DWVkP)d zu60^-zwDEesf=bDIHkb%W+CU$|GECh{iZ}E-^SYqL90?ieEqZN{+A@AXY^;l#1ZY% zjb6{7^U*?#X5}=gerkhmMg2y*<&pNL!==-XiRfkTyQ0yy|5TVVB=)ypu_3m1ePWik zl0(H|Kg@N}Bm~9wviU8>SEiG!alTmoEb!prqS9Zt@;Z+S*G@?Foxw(U28aDZAk$31 zvtVJaty5gh{kyBbb%w3+7O`3} zdb-+9jV-#=;Q^*1_8^Dfsh&y@SXgoK)^6)^D#5q%;FS0K!e(=J;?QO39ibp}0xA zEq}V3A#C3yPFvgM$!2MpUnTYYJ2Au&wti6KtSm3zb&**~&R|!a=Phq0hj-qz5a#@PRv&*Vf1kC^Tg4!@!p?EQ zOWZDVcEt7R`WmZiuBDX9zTU)5*Vw|L@onKvFTL<=W(U6G;ppc;ot75gwbA2~gnk8- zgO=h6Y0SKPe0_{4xtGnn^8VR!58sGeJ+^buq zymNV<+>*jjt^QbF$R~>dcy>+j_F`n%ElFA$De@ZqCqjrS=W__(X+9?J(bga z)2^BtnqgwM&z?Fs7HgzLkzW=`z4D@ak7M=f6*G76Lt-b&R=Y%9_814fM*o_f9X*)D4z_M|JF;^>;cKnj^~;!(&X*7!&a*S>eN z_s8!7%M3P!_I-kTk!FlAu`8W!u3jVszlmoA`;bgo0Xt$&r+UX>gniL z##6VNzZb+rD~V16dfUvZU?W{9u1)7aS1=(>~t6pAz0t>j%JEQaR8uYDwBCemmdT{T_+IvFLy&DYk~ z)?g(l_d%V<&8QIkak@@&e9CrtiG~)0@U@x!{`F!y5~xL!_IzBR&<3ujf)O@@%AEHm zO--506~9GVNNhG85b&s}eE(=?!9@Hz^V8&Ag)HR;*`r}n%NqcQz32#7J|7%i65816 zKB?Q0qE6u3cpUqQs29uP*w3>CWSjQ`Hch<|!pkc1ei2ak^cfQ~lRZ&nKaS7QI{Tjr z1yY!_p!ve$9lj_uL%m!F6zQR_4{j&cmphtzBOMUhzesavX+fn2;?~ z?#H^m2G5|eGye=7S}ptYlbSvtZv}%ekCS4xcd)nR{+~B16Y_bB^rODz{MORX*(cg? z-F0m_`1JpSniA5Pi<0Qkh5C{k<72W0*xb@o+QFe!(5T&d?spuV6nzh|k9{ zbCz-8wC9@)&2=s8UP2g;Ngs{+VM&P&hva1IS$UhJ;$Y}qQ4vPed_;=c#d&%nKB2N2 z0Df$JU{)kDQ!Jn{`qT5;8t88Rk2k+Ph|5?2ZAH9Ym=L2I%FzyHE?G$X5pa!7ay*%H zcX&t_S)r#L1F<*V$*Lazq^ow*a;_uKXULckPiPDFVLTShnk<9BtCeY#g6%*mp64y_>1u2UTRZmM||s&^n#kXLqLIT zOtYp0^#}k2AWhYPvTT>`PrO6DJmtYTG-6$-HHpWUbmM8^23|LiNvV`IHIMk;3mzA= zUiM=p=KaLUHHodc;*4(Y*`P^hMhFKWFNgi-?gn6ztHvb=r_#O438vggvBD$5fcfcVs zu|mKN#+UP%OS#^nfnuhjR!=}Y(yJus+vMB1W$Eq2BXP z#=|Y`W6Kd~>VSEwET+lwnor5DBa8|}>&e)4s>y8mj0F%0%+jY#sA$TVfq?%_F)WaZ zv5!AWt4_oKj!hTPm_~RncxL@}Rk@wZ8EIt&w|ccSRWyODet3+yH`QkU33v?5osTy~ zvYOcRI;ErmS8~@(jA58wt_#f-*jQ><^%5F+woj{3{E9p?o0{7~v~=qe;uH8pO4yPj zEPK%IK6N#zbp|KeApVo%(y1HtVY0m$i{01givhA_HRC+%xn0XV2i1NfHxfNm@s{C( zDZ}oD8rPb>2KlNfl&iW6d`PiW0Gh5Yu zgnsB3Jhje|!|PMennawJMFNc2g%RCJQdok6(Y}TZ1~iAHuy$NGbXUi^hq*eZ`4OHc zuXEt?-|Z%Z+olR0s(cPwUR1CwIc8hmbPVQq*%e`~&D2EW|Gqwd?wuQ=1!o)y)TG1c_BtjM_4_4|HG~LSljxJcK26h;NE4@PnEw9!29V|4MZTB;`votNhr|!;_ zdcR#gAI><>1K-Kr?IbEu$S#_?Vv3H}=*4~Ncfabnhla3)Wo&Gwla1D#sd{g)--upa zF5*u6rGq-}{DI2#Z-W-K)y|>i&s13(6(Nuhcc*s+wmsR3vv?QH=x&5o2rC!8H`<8Nu%63vll1x5w&%dljz6Dk z5h&io#u^u#nsE5w-RM!Bd!JL@u(4bd-CmR5cRjIWznoz^`n9n5a?}qfO3fo#K@~8- zx0U4)=%I47#)mwYH3KL}Bf7VoAzDr&s(4NsECP#c;%-sY6`kEL>a8tm`nDY3{LjBY zYDi;yXaN1ez=$wm{JlwecXwfvx{2v0NHg_T9k&fJa~!zSKkk|?bkw=5>R0^YyT|>a z^|KwaV2`Gz6_$Ii?&f(#+2z&Zbl)3SykR1ODUnS7?qjk`#vB!g)p>ksKn7TrnEoeZZ^woB@!Vc3rSrLbteH>{3lfQ|Ky0$iU33mW#X{#wl%k)1T0X!mfNI(>F$wRIMsb-;rY2IhYc^C9I$X}vFMs)@ zc7=Mk{Z-jb<2brc)f%PVJ3aLp)&t*cKs`XU{PT6P_L=44wyJ`;r*roFUdU zLeReQy`So3En$YBy#SDnjh8TkLzjshfGP><_LG+bpWmbNd{w+}YQw##PQ*`Q)3GO# z8;R2|ZKAelP#I}E+Nl(sEFU{Y3ILRFcLZJKitk#?Qu-|7qqLau@?W|wdTVnH-^JSi z_;^W+aAby5H`11CVd46!xoRt)dkDi<55VR9f<+P-pUY zTKYII$zp_e#VEJ8XN;bKlfU0*fz5lxGRi6qn7EL~4%moC@4mi{rHk-raG`&)^?{t| z{`lgq(Q+ZEWH6rRK)NEnjZO3Zc#M8$_$jW~?0#8qt+C8)`Y12v54HF_ghbw}+5c6{ z)yFfr|NZ1y4=PlL&Q?cxW{Hx-ip0q&&e()TQms6s*a(d}S!GH(Dq@I+&BK%=hIvR7 zF^nX(c^Wnvn=wz@?n~!>o!|ZM{m1>fufMK8Ki}_leIDNL&*$@gb)T279UN0bWklqc z+q*?_KLy?hOV8Z5Xv#@x{FRQvwr;GfS_>e7>u0onPWX@cub_Bwdaha4KT^PE zReaCX%QTxHX`2Ote}sd!b(^QRyZB|9$BpY(@fG7+{SMV0w1^T;M_SNTW!ngj*^uq* zTvZp@jb=lDY-bz)hu`}tt|WA`Jvv->$n{E_wwyRACT8g{VAD_}uHxT$`=3S}g|Crg z!8iu=EZc=+eAu+$3`iy*xNB`&IDaJ+!>k{DHlgOKsg+r(HmSWWI__?Z;&2!jQV?=b z4eoqJJ&qnRSxzuBD;543sm|>`Z`1pA+bB64)#3O0u;QaaAG%%bysZbVy(F%xW=ur( z!_)f|(CVr`7}dikwPy-iFD&PM@__kr!Nh8-X=!o?e!DM?Oay)kGEeVXNgu`rQ&)eR zQX8Xe7*7=!z=pv+wYU(3ufg&g(qu6Fn3#nH*Srr;40!M9bw&d7@J7T|*t@Yc_SQa+ zIVcC*^(^csQO2P(xE(xbM!&e?Y zd$eOCPdqJBHP6|!KE16#_GFp6!x|ve5)~&wr(=iN%JYF~IJg!zb3xFd)MBE7=RZD^ zHhAR?PY904%l{!E=XUu@V(bKiX=5!8e9Ax%7pudzMVnvgEVsH~Htn6U!0Zpwfln&c z3i{&eBH~9j^jG=!-WdskG}T3}#uJ_5^UboQc9WG6fxM{cFkM9_ZD=c4;arq=t7UBXup?Csatk zCFZuC@?R)#nS$-{>?-Siy7U?UTjI52LN{!3ZlS292J3aQ$S}x#{I*q=ssJ-1?nLno zj0`Sxb~?odM7`bSfFik437$K9$9HIC2S!-;h#9^BtlF3@?XJGG;6o{rk+0EhzVVJ5U*%p zwz)vR1Ujzet^2CC(R8TT;3#ab-eKE&H8rykG1X_UBUDE_q^R3GP-Ov2|Hx*c{rx-UExU{4$+o7m0H|1Q9hR%~U>F7oF> zU(J3RmCwb1A%mBL7x9n`VG=dMHiXqgRWJ?fwCJ#Sp~I4_rsf8GOx>u?%q0Syolp1w z`~n1???#IW1&x;TcXyE;mLnpO!wkpG*w-|2l6?t7IRRQ^_S7e{v1GWo`028ek*58t ztHQ2uC>B6#VLw5{UT{(8!j_QeoCb?Rj%@j>h{R5O}WE*EG$`QepYkM5k67tu} z=-vM3kzsq)R1!|@`UV|okRAbZD~(;sk_cPl7IyAr*E!u~8UaY_h#Q2)pVP0ixl|O> zIbX`l@!w=+K#CSudmh;`Wt}*cNwSM6bK^xty2{e-Q)(aml`F2#aYcs+@y5i1ns#cA zA3TdEf0+{&KHgqub;khktd3phQFV%Su^5e`(raPIB4}fxDDyG0k?ai-roE4M zC+m@u!p!PW9sO2Fcbl$mj1h4wBQbB%XuqhzEYgZa2<@!Rfu)e}76nGypoh6ZGkOjW zjn}W{DHGt&D3-0>-XljEubqCpfA`@(Ci<>?q%3ER#9XlveAf4?*02Nuq|Gr`TOfZN zf-Ndr24;+nUeh4Dk+toni`-P+r^!Af1Z3G%-xzS+QRkB(WRBN^jw3Cpo%$D)=4afY zwV7|b#{*qGFky4-=lsAhl@2_N>-6!(Df8XozGZM#K8eK9#UPN$&$U*2<@(y?Nh&?0 zreGIRaq%|sS?J5k$_9jA6~hb82^63uv09mVX{E8jHyhr!+*Z}Vrxt(=CES?}GO8c* zT_QE;`nTJ696+06k?u{LUPc!OrOdYW0(8xCrLdJfrSPKwZKsH zF)TYdRj&y;4e}yK#2w_I=;g1q3Y@QhJ&bfQ@^)=+`bY%ZMOt|VdpP4hr+;@;aP=z; z62r7P18BNk->-?e6SvK0SkV0wZSu-V1j{VQ+boEUgpnZ0R)z{vCsG3VwLlMZ9qXrR zX?^vg*s`f6_`y~}#fS#_|_Fg4Z9(QDU+Em`$5X_%! z@$%Y-b1*#tQ^Qr~xVLl0sX9lHnOn5%LV^_YuPdHEgX-nvbK&ir!pqOaaJJ1PBt=Q$ zSb5GDz3S}S@dTWd-bx#&$(JYypWr#5Xk8`@sEV=AvRoJ zeU}k=`v(#qN!(ai*2oL4CI4;DfVY+y(NoJ0e4&;aRlY?pbtA6Tni&+Mvy=e0{rG(<7B}C!r~pbI=LXU0ptd_S`29Ex-H$cmd?67Yvye zniRx->M*VvT>O*$k|kMDP=nRzK59Wnik0o*BV~2FY%$2P00}2s|CCwpwoNV5j{2?X(_&#%Kv$FibsmD0kl-Ly6`mgF z(!onDrz@K8d*15lA*mwXy(7-93^oe&$j{(pOn>W(`(G1OPz`ZID`|CT3%AxOb{>msTD zjZ23CEZph&r6k^hTTO$yXY&rzf0a<(&OwHx_lbsc>+O|39^HKp!ST9UE!3Xw^R5gE z&$ra0njP?-fRy7x#mbFTJk+>J*Q6L~KY^P@t|V3KoVRAac=}^<{MihJSspeZrkp%G zPsoF2KE2FwS5{UW6y}V`t*Z}d`RA)Pcv|fr?waxZ)%tDR$G<{78gi(~Af&w3!5wOI zX9$#Sn$)zqjnm_Suf}B8FThsmItv+_O_8_w2@kdyzPW)QbkL2DLhSl&_L##)JFfSr z@6%T|@x_uX<63lNnBRD`p>;WhqJo-HRFFcPBwSUg?*cwcJ{xNm3J;9!m-Z%ZSlP6* z2XbP+BpKuUoead=CET8zkW7}@Zp+~+r^lssuxqW+;yU~?3*7y|&p^VxKZ=s9up;S# zY_qR`tjx-n(Pa(ghb<8`<^iF}W_vyQPXB1ks^2q0x2gYT|QU zMDcx^NLfs3nnHi8+FYAjQ1&c$>(&&)E-)fnjr^fmGXIWcBa88TE~qvMkSMK}imrqS z>(qIe5<_>CLw948tyLUNR-Nq{8@X`t$M?8sQB~bWot`2}{}X_0yjrS=m9R6!gu_^q zpgD7~-Aots$n~ZNY2Oo8zdHvwr(5(wF#|b`bEqV+b@;jD3uTLBx_VijmK6Plmid*5 zz1RK%OCQfvZdxP{UepYI=Sqr(n*V0FB)AsB+oJ*uTRixO2LMX1B9GKmf*s`1wtWWc zW4Kf%6ZYF!joU4reICQGJzxSnSmc=~e-vpZ;gpx090u>y?8ysvVX_Go7WoH?7*0%E zV!`rP^phe=jawkqjXXP(vw=I/dev/null || true; \ + cp /build/$b/examples/server/server /out/$b/bin/ 2>/dev/null || true; \ + cp /build/$b/wolfssl/options.h /out/$b/; \ + done; \ + cp /src/wolfssl/README.md /out/ 2>/dev/null || true + +# ---------------------------------------------------------------- runtime --- +FROM debian:bookworm-slim + +LABEL org.opencontainers.image.title="wolfCrypt PQC benchmark (i.MX95)" \ + org.opencontainers.image.description="Live ML-KEM / ML-DSA benchmarks on the Cortex-A55 cluster" \ + org.opencontainers.image.vendor="wolfSSL Inc." + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + util-linux procps ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /out/ /opt/wolfssl/ +COPY entrypoint.sh /opt/wolfssl/ +RUN chmod +x /opt/wolfssl/entrypoint.sh + +WORKDIR /opt/wolfssl +ENTRYPOINT ["/opt/wolfssl/entrypoint.sh"] diff --git a/imx95-pqc-demo/container/build-aarch64.sh b/imx95-pqc-demo/container/build-aarch64.sh new file mode 100755 index 0000000..310eb12 --- /dev/null +++ b/imx95-pqc-demo/container/build-aarch64.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Cross-build wolfSSL for the Toradex SMARC i.MX95 (6x Cortex-A55, aarch64). +# +# Builds two configurations out-of-tree so the wolfSSL source checkout stays clean: +# baseline -- --enable-armasm (NEON; works on any A55) +# sha3-crypto -- --enable-armasm=sha3-crypto (ARMv8.2 SHA3 instructions, FEAT_SHA3) +# +# The pair exists because ML-DSA has NO ARM assembly in wolfSSL: on aarch64 its +# speed comes entirely from the SHA-3/SHAKE backend, so this is the only lever +# that moves ML-DSA numbers. FEAT_SHA3 is optional on Cortex-A55, so the +# sha3-crypto build may not be runnable on the actual silicon -- check +# "grep Features /proc/cpuinfo" for 'sha3' on the board before shipping it. +# +# Static libraries only, so the binaries run under qemu-aarch64 with no +# LD_LIBRARY_PATH juggling. +# +# Usage: ./build-aarch64.sh [baseline|sha3-crypto|all] + +set -euo pipefail + +# Point this at your wolfSSL checkout: +# WOLFSSL_REPO=/path/to/wolfssl ./build-aarch64.sh all +WOLFSSL_REPO="${WOLFSSL_REPO:?set WOLFSSL_REPO to your wolfSSL checkout}" +WOLFSSL_REF="${WOLFSSL_REF:-HEAD}" +DEMO_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)" +# Pristine export of the repo. The developer's checkout is already configured +# in-tree, and autotools refuses an out-of-tree build against a configured +# source dir. Exporting rather than running "make distclean" over there keeps +# their working tree and build state untouched. +WOLFSSL_SRC="${DEMO_DIR}/wolfssl-src" +HOST_TRIPLE=aarch64-linux-gnu +JOBS="$(nproc)" + +export_src() { + if [ -x "${WOLFSSL_SRC}/configure" ]; then + echo "=== source export already present: ${WOLFSSL_SRC} ===" + return + fi + echo "=== exporting ${WOLFSSL_REPO} @ ${WOLFSSL_REF} -> ${WOLFSSL_SRC} ===" + rm -rf "${WOLFSSL_SRC}" + mkdir -p "${WOLFSSL_SRC}" + git -C "${WOLFSSL_REPO}" archive "${WOLFSSL_REF}" | tar -x -C "${WOLFSSL_SRC}" + echo "=== autogen.sh ===" + (cd "${WOLFSSL_SRC}" && ./autogen.sh > autogen.log 2>&1) \ + || { tail -40 "${WOLFSSL_SRC}/autogen.log"; exit 1; } +} + +# Shared across both configurations. --enable-mldsa is REQUIRED: it is off by +# default, and without it the benchmark silently emits no ML-DSA rows at all +# rather than failing. +COMMON_OPTS=( + "--host=${HOST_TRIPLE}" + --enable-mlkem + --enable-mldsa + --enable-sp + --enable-sp-asm + --enable-keygen + --enable-sha3 + --enable-curve25519 + --enable-ed25519 + --disable-shared + --enable-static +) + +build_one() { + local name="$1" armasm="$2" + local builddir="${DEMO_DIR}/build/${name}" + + echo "=== [${name}] configure (${armasm}) ===" + rm -rf "${builddir}" + mkdir -p "${builddir}" + ( + cd "${builddir}" + "${WOLFSSL_SRC}/configure" \ + "${COMMON_OPTS[@]}" \ + "${armasm}" \ + > configure.log 2>&1 || { tail -40 configure.log; exit 1; } + ) + + echo "=== [${name}] make -j${JOBS} ===" + make -C "${builddir}" -j"${JOBS}" > "${builddir}/build.log" 2>&1 \ + || { tail -60 "${builddir}/build.log"; exit 1; } + + echo "=== [${name}] OK ===" + file "${builddir}/wolfcrypt/benchmark/benchmark" || true +} + +target="${1:-all}" +export_src +case "${target}" in + baseline) build_one baseline "--enable-armasm" ;; + sha3-crypto) build_one sha3-crypto "--enable-armasm=sha3-crypto" ;; + all) + build_one baseline "--enable-armasm" + build_one sha3-crypto "--enable-armasm=sha3-crypto" + ;; + *) echo "usage: $0 [baseline|sha3-crypto|all]" >&2; exit 2 ;; +esac diff --git a/imx95-pqc-demo/container/build-image.sh b/imx95-pqc-demo/container/build-image.sh new file mode 100755 index 0000000..7c09512 --- /dev/null +++ b/imx95-pqc-demo/container/build-image.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Build the arm64 demo container. +# +# Cross-compiles in the builder stage on the native build platform, so this is +# fast even though the output image is linux/arm64. Validate the result under +# binfmt on the x86 bench before it ever touches the board. +# +# Publishing is deliberately opt-in: pass --push (and set REGISTRY) only when +# you actually intend to make the image public. +# +# Usage: +# ./build-image.sh # build + load locally +# ./build-image.sh --push # build + push to $REGISTRY (requires login) + +set -euo pipefail + +DEMO_DIR="$(cd "${BASH_SOURCE[0]%/*}" && pwd)" +REGISTRY="${REGISTRY:-ghcr.io/wolfssl}" +IMAGE="${IMAGE:-wolfcrypt-pqc-imx95}" +TAG="${TAG:-latest}" +PLATFORM="${PLATFORM:-linux/arm64}" +REF="${REGISTRY}/${IMAGE}:${TAG}" + +if [ ! -d "${DEMO_DIR}/wolfssl-src" ]; then + echo "wolfssl-src/ missing -- run ./build-aarch64.sh first to export it" >&2 + exit 1 +fi + +OUTPUT=(--load) +if [ "${1:-}" = "--push" ]; then + OUTPUT=(--push) + echo "=== PUBLISHING to ${REF} ===" + echo "=== this makes the image publicly pullable; Ctrl-C within 5s to abort ===" + sleep 5 +fi + +set -x +docker buildx build \ + --platform "${PLATFORM}" \ + -t "${REF}" \ + -f "${DEMO_DIR}/Dockerfile" \ + "${OUTPUT[@]}" \ + "${DEMO_DIR}" diff --git a/imx95-pqc-demo/container/docker-compose.yml b/imx95-pqc-demo/container/docker-compose.yml new file mode 100644 index 0000000..b70e590 --- /dev/null +++ b/imx95-pqc-demo/container/docker-compose.yml @@ -0,0 +1,42 @@ +# Torizon OS deployment unit for the wolfCrypt PQC benchmark pane. +# +# Deploy with: +# docker compose up (on the board, or via Torizon Cloud) +# +# The container writes to stdout; the demo host captures that into the left +# tmux pane on the HDMI console. Part 2's Cortex-M7 wolfBoot output arrives +# over RPMsg (rpmsg_tty) into the right pane of the same tmux session. + +services: + wolfcrypt-pqc: + image: wolfssl/wolfcrypt-pqc:latest + container_name: wolfcrypt-pqc + restart: unless-stopped + + # Live console demo: keep the TTY so the ANSI banner renders and output is + # not block-buffered behind a pipe. + tty: true + stdin_open: true + + environment: + # auto -- pick sha3-crypto only if the CPU advertises FEAT_SHA3 + # ab -- alternate builds each cycle to show the SHA-3 lever live + MODE: "${MODE:-auto}" + LOOP: "1" + PAUSE: "${PAUSE:-5}" + + # The entrypoint reads /proc/cpuinfo to decide which build is safe to run, + # so the host's real CPU feature list must be visible. + volumes: + - /proc/cpuinfo:/proc/cpuinfo:ro + - ./results:/results + + # Benchmarks are CPU-bound and want the whole A55 cluster. Leave unset to + # use all cores; pin here if the M7-side demo needs headroom. + # cpuset: "0-5" + + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/imx95-pqc-demo/container/entrypoint.sh b/imx95-pqc-demo/container/entrypoint.sh new file mode 100755 index 0000000..06876c8 --- /dev/null +++ b/imx95-pqc-demo/container/entrypoint.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Left-pane demo loop: live wolfCrypt PQC benchmarks on the i.MX95 A55 cluster. +# +# Environment: +# MODE=auto|baseline|sha3-crypto|ab which wolfSSL build to run (default auto) +# LOOP=1|0 run continuously (default 1) +# PAUSE= pause between cycles (default 5) +# ONCE_ARGS="..." override the algorithm set + +set -uo pipefail + +WOLF_ROOT=/opt/wolfssl +# Named at runtime rather than baked in: the image is plain aarch64 and runs on +# any Arm64 Torizon module, so naming one SoC in the banner would be wrong on +# the others. A container does not normally see /proc/device-tree, so the model +# is only available when the host exposes it; brace the redirect so a missing +# file is silent rather than a shell error on stderr. +soc_model() { { tr -d '\0' < /proc/device-tree/model; } 2>/dev/null; } +SOC_LABEL="${SOC_LABEL:-$(soc_model)}" +SOC_LABEL="${SOC_LABEL:-Arm64 module}" +MODE="${MODE:-auto}" +LOOP="${LOOP:-1}" +PAUSE="${PAUSE:-5}" + +B=$'\033[1m'; DIM=$'\033[2m'; CYAN=$'\033[36m'; GREEN=$'\033[32m' +YELLOW=$'\033[33m'; RED=$'\033[31m'; R=$'\033[0m' +[ -t 1 ] || { B=""; DIM=""; CYAN=""; GREEN=""; YELLOW=""; RED=""; R=""; } + +have_sha3() { grep -m1 '^Features' /proc/cpuinfo 2>/dev/null | grep -qw sha3; } + +select_build() { + case "${MODE}" in + baseline|sha3-crypto) echo "${MODE}" ;; + auto) + # FEAT_SHA3 is optional on Cortex-A55. Running the sha3-crypto + # binary without it faults, so this check is not cosmetic. + if have_sha3; then echo sha3-crypto; else echo baseline; fi ;; + *) echo baseline ;; + esac +} + +banner() { + local build="$1" feats ver + feats="$(grep -m1 '^Features' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | sed 's/^ //')" + ver="$("${WOLF_ROOT}/${build}/bin/benchmark" -? 2>&1 | grep -m1 -o 'wolfSSL version [0-9.]*')" + + echo "${CYAN}${B}" + echo " wolfSSL / wolfCrypt post-quantum benchmarks" + echo " ${SOC_LABEL} -- Torizon OS" + echo "${R}${DIM} ------------------------------------------------------------${R}" + printf " %-14s %s\n" "Library:" "${ver:-unknown}" + printf " %-14s %s\n" "Build:" "${build}" + printf " %-14s %s\n" "Cores:" "$(nproc)" + printf " %-14s %s\n" "Kernel:" "$(uname -r)" + printf " %-14s %s\n" "CPU flags:" "${feats:-unavailable}" + + if [ "${build}" = "sha3-crypto" ]; then + printf " %-14s ${GREEN}%s${R}\n" "Keccak:" \ + "ARMv8.2 SHA3 instructions (EOR3/RAX1/XAR/BCAX)" + elif have_sha3; then + printf " %-14s ${YELLOW}%s${R}\n" "Keccak:" \ + "NEON only -- CPU has FEAT_SHA3, build does not use it" + else + printf " %-14s ${YELLOW}%s${R}\n" "Keccak:" \ + "NEON only -- this CPU has no FEAT_SHA3" + fi + # ML-DSA has no ARM assembly in wolfSSL, so its numbers ride entirely on + # the Keccak backend named above. Say so, rather than let a reader assume + # a hand-tuned ML-DSA path exists. + printf " ${DIM}%s${R}\n" "ML-KEM: NEON NTT + 3-way NEON Keccak. ML-DSA: no ARM asm; Keccak-bound." + echo "${DIM} ------------------------------------------------------------${R}" + echo +} + +run_cycle() { + local build="$1" bin="${WOLF_ROOT}/$1/bin/benchmark" + if [ ! -x "${bin}" ]; then + echo "${RED}missing benchmark binary: ${bin}${R}" >&2 + return 1 + fi + if [ -n "${ONCE_ARGS:-}" ]; then + # shellcheck disable=SC2086 + "${bin}" ${ONCE_ARGS} + else + "${bin}" -ml-kem-512 -ml-kem-768 -ml-kem-1024 \ + -ml-dsa-44 -ml-dsa-65 -ml-dsa-87 \ + -ecc -rsa -sha3-256 -shake256 + fi +} + +main() { + local build cycle=0 status= + build="$(select_build)" + + if [ "${MODE}" = auto ] && [ "${build}" = baseline ] && have_sha3; then + : # unreachable, kept for clarity + fi + if [ "${MODE}" = sha3-crypto ] && ! have_sha3; then + echo "${RED}WARNING: MODE=sha3-crypto but this CPU does not advertise FEAT_SHA3." >&2 + echo " The binary is likely to fault with SIGILL.${R}" >&2 + fi + + while :; do + cycle=$((cycle + 1)) + if [ "${MODE}" = ab ]; then + # Alternate builds so the SHA-3 lever is visible live, side by side + # across cycles, without restarting the container. + if [ $((cycle % 2)) -eq 1 ]; then build=baseline + elif have_sha3; then build=sha3-crypto + else build=baseline + fi + fi + + banner "${build}" + printf " ${GREEN}${B}[RUNNING]${R} benchmark cycle %d, started %s\n\n" \ + "${cycle}" "$(date -u '+%H:%M:%SZ')" + + if run_cycle "${build}"; then + status="${GREEN}${B}[OK]${R}" + else + status="${RED}${B}[FAILED]${R}" + fi + + [ "${LOOP}" = "1" ] || break + echo + printf " %b cycle %d complete, next run in %ss\n" \ + "${status}" "${cycle}" "${PAUSE}" + sleep "${PAUSE}" + echo + done +} + +main "$@" diff --git a/imx95-pqc-demo/demo/demo-run.sh b/imx95-pqc-demo/demo/demo-run.sh new file mode 100755 index 0000000..12c0fc5 --- /dev/null +++ b/imx95-pqc-demo/demo/demo-run.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Two-pane i.MX95 demo. Runs ON THE BOARD as root. +# +# left - wolfCrypt PQC benchmarks in a container on the Cortex-A55 cluster +# right - wolfBoot ML-DSA-87 verified boot of the Cortex-M7 +# +# Everything is local to the board, so once this is running nothing depends on +# a host being connected. +# +# sudo bash demo-run.sh # render on this terminal +# sudo bash demo-run.sh /dev/tty1 # render on the HDMI console +# +# The M7 can only be started ONCE per Linux boot, so re-running the demo needs a +# full board power cycle. That is the "reboot beat" - and it has to come from +# the host relay or a person, since the board cannot power-cycle itself. +set -uo pipefail + +DEMO=/home/torizon/demo +TARGET=${1:-} + +echo "== starting the benchmark container ==" +# "down" first, deliberately. A container created by an earlier run survives a +# reboot while its compose network does not, so a bare "up -d" fails with +# "network not found" and the left pane silently stays empty. Since the +# demo's replay beat IS a power cycle, that is exactly when it would bite. +( cd "$DEMO" && docker compose down --remove-orphans >/dev/null 2>&1 || true ) +( cd "$DEMO" && docker compose up -d ) || exit 1 + +echo "== releasing the Cortex-M7 ==" +bash "$DEMO/m7-start.sh" || echo " (M7 already running - power cycle to replay the boot)" + +echo "== rendering ==" +if [ -n "$TARGET" ]; then + # A getty owns tty1; stop it first or the two fight over the console. + systemctl stop getty@"$(basename "$TARGET")" 2>/dev/null || true + exec python3 "$DEMO/twopane.py" > "$TARGET" 2>/dev/null +else + exec python3 "$DEMO/twopane.py" +fi diff --git a/imx95-pqc-demo/demo/install-autostart.sh b/imx95-pqc-demo/demo/install-autostart.sh new file mode 100755 index 0000000..d09a0fd --- /dev/null +++ b/imx95-pqc-demo/demo/install-autostart.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Install and enable the boot-time demo unit. Run ON THE BOARD as root. +# +# sudo bash install-autostart.sh # install + enable +# sudo bash install-autostart.sh --off # disable, restore the getty +set -euo pipefail +UNIT=/etc/systemd/system/wolfssl-demo.service +HERE="$(cd "$(dirname "$0")" && pwd)" + +if [ "${1:-}" = "--off" ]; then + systemctl disable --now wolfssl-demo.service 2>/dev/null || true + systemctl start getty@tty1.service 2>/dev/null || true + sync + echo "demo autostart disabled, getty on tty1 restored" + exit 0 +fi + +install -m 0644 "$HERE/wolfssl-demo.service" "$UNIT" +systemctl daemon-reload +systemctl enable wolfssl-demo.service +# The demo's replay beat is a hard power cut, so nothing may sit in the page +# cache: an unsynced unit file is simply gone after the next cycle. +sync +echo "installed and enabled: $UNIT" +echo "the demo now starts on boot; power cycle is the whole replay" +echo "disable with: sudo bash $HERE/install-autostart.sh --off" diff --git a/imx95-pqc-demo/demo/m7-console-tail.sh b/imx95-pqc-demo/demo/m7-console-tail.sh new file mode 100755 index 0000000..f66e8d0 --- /dev/null +++ b/imx95-pqc-demo/demo/m7-console-tail.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Right demo pane: live view of the Cortex-M7 console. +# +# Runs ON THE BOARD (the HDMI output is the board's own), so no host connection +# is involved once the demo is running. +# +# wolfBoot writes its log to a shared-memory ring at 0x80F00000 before any RPMsg +# endpoint exists, so this reads the ring directly rather than going through +# rpmsg_tty. That is deliberate: the ring is the only source that contains +# wolfBoot's own PQC verification output, and it needs no driver, no endpoint +# binding and no module load. +# +# memtool dumps the whole ring each call, so track how much has already been +# shown and print only what is new. +# +# Must run as root (/dev/mem). The demo launcher starts the whole tmux session +# under sudo so no password prompt can appear mid-demo. +set -uo pipefail + +MEMTOOL=${MEMTOOL:-/home/torizon/bin/memtool} +ADDR=${ADDR:-0x80F00000} +INTERVAL=${INTERVAL:-0.5} + +printf '\033[1;36m' +cat <<'BANNER' ++--------------------------------------------------------------+ +| NXP i.MX95 Cortex-M7 - wolfBoot secure boot | +| ML-DSA-87 (NIST level 5) post-quantum verified boot | ++--------------------------------------------------------------+ +BANNER +printf '\033[0m\n' +echo "waiting for the M7 to boot ..." +echo + +shown=0 +while true; do + out=$("$MEMTOOL" con "$ADDR" 2>/dev/null) || { sleep "$INTERVAL"; continue; } + len=${#out} + if [ "$len" -gt "$shown" ]; then + printf '%s' "${out:$shown}" + shown=$len + elif [ "$len" -lt "$shown" ]; then + # Ring was re-initialised (the M7 restarted): start over. + printf '\n\033[1;33m--- M7 restarted ---\033[0m\n' + printf '%s' "$out" + shown=$len + fi + sleep "$INTERVAL" +done diff --git a/imx95-pqc-demo/demo/m7-rpmsg-log.sh b/imx95-pqc-demo/demo/m7-rpmsg-log.sh new file mode 100755 index 0000000..6d5d79d --- /dev/null +++ b/imx95-pqc-demo/demo/m7-rpmsg-log.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Read wolfBoot's verify log from the M7 over RPMsg. Runs ON THE BOARD as root. +# +# Order matters, and not in the obvious way. The payload relays the whole +# console ring as soon as its endpoint has a destination address, which the +# host supplies while binding the channel. That is strictly before +# /dev/ttyRPMSG* exists, so a reader can never be attached in time for the +# first pass, and the log it sends is discarded by a tty nobody has open. +# +# The payload therefore treats any byte written to the tty as a request to +# rewind and send the log again. So: attach the reader, then poke. +# +# The demo's right pane does NOT use this path - it reads the shared-memory +# ring directly, which needs no driver, no bind and no module load. This script +# is for showing the same log arriving as a normal Linux tty. +set -uo pipefail + +SECS=${SECS:-8} +modprobe imx_rpmsg_tty 2>/dev/null || true + +TTY="" +for _ in $(seq 1 30); do + TTY=$(ls /dev/ttyRPMSG* 2>/dev/null | head -1) + [ -n "$TTY" ] && break + sleep 1 +done +if [ -z "$TTY" ]; then + echo "no /dev/ttyRPMSG* - is the M7 running? (cat /sys/class/remoteproc/remoteproc1/state)" >&2 + exit 1 +fi + +# raw so the log is not line-edited on its way through the line discipline +stty -F "$TTY" raw -echo clocal + +timeout "$SECS" cat "$TTY" & +reader=$! +sleep 1 +printf '\n' > "$TTY" # request the replay +wait $reader 2>/dev/null +exit 0 diff --git a/imx95-pqc-demo/demo/m7-start.sh b/imx95-pqc-demo/demo/m7-start.sh new file mode 100755 index 0000000..f0ef7a0 --- /dev/null +++ b/imx95-pqc-demo/demo/m7-start.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Load wolfBoot + the signed payload onto the Cortex-M7 and release the core. +# Runs ON THE BOARD as root, so the demo needs no host connection. +# +# The M7 can only be started ONCE per Linux boot - "echo stop" fails on this BSP +# with "Interrupted system call" and the core stays running. So a second run of +# the demo needs a full power cycle, not a restart. That is why the demo's +# "reboot beat" is a real power cycle. +set -euo pipefail + +FW=/home/torizon/demo/fw +M=/home/torizon/bin/memtool +RP=/sys/class/remoteproc/remoteproc1 +BOOT_ADDR=0x80100000 +STATUS=0x80F10000 + +if [ "$(cat $RP/state)" = "running" ]; then + echo "M7 is already running - power cycle the board to run the demo again" >&2 + exit 1 +fi + +$M fill $STATUS 32 0 +$M load $BOOT_ADDR "$FW/payload.bin" +echo "$FW" > /sys/module/firmware_class/parameters/path +echo start > $RP/state +sleep 1 +echo "M7 state: $(cat $RP/state)" diff --git a/imx95-pqc-demo/demo/stage.sh b/imx95-pqc-demo/demo/stage.sh new file mode 100755 index 0000000..131f5d4 --- /dev/null +++ b/imx95-pqc-demo/demo/stage.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Copy the demo onto the board. Uses ssh keys - no passwords here. +# +# BOARD=torizon@ WOLFBOOT=/path/to/wolfboot ./stage.sh +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +DEMO="$(cd "$HERE/.." && pwd)" +BOARD="${BOARD:?set BOARD, e.g. BOARD=torizon@192.168.1.50}" +WOLFBOOT="${WOLFBOOT:?set WOLFBOOT to your wolfBoot build directory}" +PAYLOAD="${PAYLOAD:-$DEMO/m7/build/zephyr/payload_v1_signed.bin}" + +[ -f "$WOLFBOOT/wolfboot.elf" ] || { echo "no wolfboot.elf in $WOLFBOOT" >&2; exit 1; } +[ -f "$PAYLOAD" ] || { echo "no signed payload at $PAYLOAD" >&2; exit 1; } + +ssh "$BOARD" 'mkdir -p ~/demo/fw ~/demo/results ~/bin' + +scp -q "$DEMO/demo/twopane.py" "$DEMO/demo/demo-run.sh" \ + "$DEMO/demo/m7-start.sh" "$DEMO/demo/m7-console-tail.sh" \ + "$DEMO/demo/m7-rpmsg-log.sh" "$DEMO/demo/install-autostart.sh" \ + "$DEMO/demo/wolfssl-demo.service" "$BOARD:~/demo/" +scp -q "$DEMO/container/docker-compose.yml" "$BOARD:~/demo/" +scp -q "$WOLFBOOT/wolfboot.elf" "$BOARD:~/demo/fw/rproc-imx-rproc-fw" +scp -q "$PAYLOAD" "$BOARD:~/demo/fw/payload.bin" + +# memtool is how both the demo and the M7 console reader reach /dev/mem. +aarch64-linux-gnu-gcc -O2 -o /tmp/memtool "$DEMO/tools/memtool.c" +scp -q /tmp/memtool "$BOARD:~/bin/memtool" + +# The demo is replayed by cutting power, so anything still in the page cache is +# lost on the next cycle and the board silently runs the previous payload. +# Flush, then verify the payload by content rather than by timestamp. +ssh "$BOARD" 'sync' +want=$(md5sum "$PAYLOAD" | cut -d' ' -f1) +got=$(ssh "$BOARD" 'md5sum ~/demo/fw/payload.bin' | cut -d' ' -f1) +if [ "$want" != "$got" ]; then + echo "payload mismatch after staging: local $want, board $got" >&2 + exit 1 +fi + +echo "staged and verified. On the board: sudo bash ~/demo/demo-run.sh /dev/tty1" diff --git a/imx95-pqc-demo/demo/twopane.py b/imx95-pqc-demo/demo/twopane.py new file mode 100644 index 0000000..f3d1c3a --- /dev/null +++ b/imx95-pqc-demo/demo/twopane.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Two-pane i.MX95 demo renderer. + +Torizon OS ships no tmux, screen or dtach, and its rootfs is read-only OSTree, +so there is nothing to install. For a demo that is exactly two fixed panes of +append-only text, a full redraw is simpler and more predictable than shipping a +static multiplexer: no incremental cursor management, and a resize or a stray +escape sequence cannot corrupt the layout permanently. + + left - wolfCrypt PQC benchmarks, in a container on the Cortex-A55 cluster + right - wolfBoot ML-DSA-87 verified boot of the Cortex-M7 + +Run as root (the right pane reads /dev/mem via memtool), on the console you +want it displayed on: + + sudo python3 twopane.py # current terminal + sudo python3 twopane.py > /dev/tty1 # the HDMI console +""" + +import os +import re +import shutil +import subprocess +import sys +import time + +CONTAINER = os.environ.get("CONTAINER", "wolfcrypt-pqc") +MEMTOOL = os.environ.get("MEMTOOL", "/home/torizon/bin/memtool") +CONSOLE_ADDR = os.environ.get("CONSOLE_ADDR", "0x80F00000") +INTERVAL = float(os.environ.get("INTERVAL", "1.0")) + +ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") + +# The benchmark's cycle columns derive from the 24 MHz generic timer, not the +# 1.8 GHz core clock, so they are wrong by roughly 75x. They must never appear +# on a demo screen someone might photograph. Strip them; ops/sec and ms are the +# numbers that are actually correct. +CYCLES = re.compile(r",?\s*\d+\s+cycles\s+[\d.]+\s+Cycles/op\s*$") +CPB = re.compile(r"\s*Cycles per byte\s*=\s*[\d.]+\s*$") + +# The raw benchmark line is far too wide for half a console, and truncating it +# cuts off ops/sec - the one number worth showing. Condense to +# " " so each result fits on one readable row. +OPS = re.compile(r"^(.*?)\s+\d+ ops took [\d.]+ sec, avg [\d.]+ ms,\s+([\d.]+) ops/sec") +THRU = re.compile(r"^(\S.*?)\s+[\d.]+ [KMG]iB took [\d.]+ seconds,\s+([\d.]+) ([KMG]iB/s)") + + +# The HDMI console on this board is 80x25, so each pane gets ~38 columns. +# Benchmark labels have to lose their redundant parameter fields to fit. +SHORTEN = ( + ("[ SECP256R1]", "P-256"), + ("[ SECP256R1]", "P-256"), + ("ML-KEM 512 128", "ML-KEM-512"), + ("ML-KEM 768 192", "ML-KEM-768"), + ("ML-KEM 1024 256", "ML-KEM-1024"), + ("ML-DSA 44", "ML-DSA-44"), + ("ML-DSA 65", "ML-DSA-65"), + ("ML-DSA 87", "ML-DSA-87"), + ("RSA 2048", "RSA-2048"), +) + + +def shorten(name): + for a, b in SHORTEN: + name = name.replace(a, b) + return " ".join(name.split()) + + +def condense(ln, width): + m = THRU.match(ln) + if m: + unit = m.group(3) if width >= 46 else m.group(3).replace("iB/s", "B/s") + value = f"{float(m.group(2)):,.1f} {unit}" + else: + m = OPS.match(ln) + if not m: + return ln + unit = "ops/sec" if width >= 46 else "/s" + value = f"{float(m.group(2)):,.0f} {unit}" + + # Pad the label to exactly what is left, so the value always lands flush + # right and nothing is clipped. + wname = max(6, width - len(value) - 1) + return f"{shorten(m.group(1))[:wname]:<{wname}} {value:>{len(value)}}" + +LEFT_TITLE = " A55 x6: wolfCrypt PQC " +RIGHT_TITLE = " M7: wolfBoot ML-DSA-87 " + + +def run(cmd): + try: + p = subprocess.run(cmd, shell=True, capture_output=True, + text=True, timeout=5) + return p.stdout + except Exception: + return "" + + +def clean(text, width, drop_cycles=False): + """Strip ANSI so column widths are computed on what is actually shown.""" + out = [] + for ln in text.splitlines(): + ln = ANSI.sub("", ln).expandtabs(4).rstrip() + if drop_cycles: + ln = CYCLES.sub("", ln) + ln = CPB.sub("", ln) + ln = condense(ln.rstrip().rstrip(","), width) + out.append(ln) + return out + + +def fit(lines, width, height, wrap=False): + """Last `height` lines, fitted to `width`. + + The benchmark pane truncates: its lines are already condensed to fit, and a + wrapped one would cost extra rows and break alignment. The wolfBoot pane + wraps: its output is short, it is the point of the demo, and losing the end + of "wc_MlDsaKey_Verify returned OK" would defeat the purpose. + """ + out = [] + for ln in lines: + if not wrap: + out.append(ln[:width]) + continue + if not ln: + out.append("") + while ln: + out.append(ln[:width]) + ln = ln[width:] + return out[-height:] if len(out) > height else out + [""] * (height - len(out)) + + +def main(): + while True: + cols, rows = shutil.get_terminal_size((100, 30)) + half = (cols - 3) // 2 + body = rows - 4 + + left = fit(clean(run(f"docker logs --tail 400 {CONTAINER} 2>&1"), half, drop_cycles=True), half, body) + right_raw = run(f"{MEMTOOL} con {CONSOLE_ADDR} 2>/dev/null") + if not right_raw.strip(): + right_raw = "waiting for the Cortex-M7 to boot ...\n" + right = fit(clean(right_raw, half), half, body, wrap=True) + + buf = ["\x1b[?25l\x1b[H"] # hide cursor, home - no clear, see note above + buf.append("\x1b[1;36m" + "NXP i.MX95 - post-quantum on both clusters".center(cols) + "\x1b[0m") + buf.append("\x1b[1;33m" + LEFT_TITLE.ljust(half) + " | " + + RIGHT_TITLE.ljust(half) + "\x1b[0m") + buf.append("-" * cols) + for i in range(body): + buf.append(left[i].ljust(half) + " \x1b[1;30m|\x1b[0m " + right[i].ljust(half)) + + # Pad to the full width so the previous frame is fully overwritten. + # Pad on VISIBLE length: these lines contain colour escapes, and + # ljust() on the raw string counts those bytes and silently clips real + # characters off the right-hand pane. + painted = [] + for i, ln in enumerate(buf): + if i == 0: + painted.append(ln) + continue + visible = len(ANSI.sub("", ln)) + painted.append(ln + " " * max(0, cols - visible)) + sys.stdout.write("\n".join(painted) + "\x1b[J") + sys.stdout.flush() + time.sleep(INTERVAL) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + pass + finally: + # Always give the cursor back, or the console is left unusable. + sys.stdout.write("\x1b[?25h\n") + sys.stdout.flush() diff --git a/imx95-pqc-demo/demo/wolfssl-demo.service b/imx95-pqc-demo/demo/wolfssl-demo.service new file mode 100644 index 0000000..956d68a --- /dev/null +++ b/imx95-pqc-demo/demo/wolfssl-demo.service @@ -0,0 +1,30 @@ +[Unit] +# Two-pane i.MX95 post-quantum demo, started automatically at boot. +# +# The point of this unit is the replay beat. The Cortex-M7 can only be started +# once per Linux boot, so re-running the demo needs a full power cycle. Without +# autostart that also means someone logging in and typing a command afterwards, +# over a network link that comes up unreliably. With it, cutting and restoring +# power is the entire replay, and nothing depends on a host being connected. +Description=wolfSSL i.MX95 post-quantum two-pane demo +Documentation=https://github.com/wolfSSL/wolfBoot-examples/pull/13 +After=docker.service +Requires=docker.service +# tty1 is the HDMI console; a getty there fights the renderer for it. +Conflicts=getty@tty1.service + +[Service] +Type=simple +ExecStart=/bin/bash /home/torizon/demo/demo-run.sh /dev/tty1 +# A dead screen is worse than a retry on a show floor. Re-running is safe: the +# M7 start is skipped when the core is already running, and the compose project +# is torn down and brought back up on each pass. +Restart=on-failure +RestartSec=5 +StandardInput=null +StandardOutput=journal +StandardError=journal +TimeoutStartSec=0 + +[Install] +WantedBy=multi-user.target diff --git a/imx95-pqc-demo/gallery/README.md b/imx95-pqc-demo/gallery/README.md new file mode 100644 index 0000000..c2c1a32 --- /dev/null +++ b/imx95-pqc-demo/gallery/README.md @@ -0,0 +1,116 @@ +# wolfCrypt Post-Quantum Benchmarks - Torizon Demo Gallery entry + +Submission package for the [Torizon Demo Gallery](https://www.torizon.io/demo-gallery), +prepared against the [partner guidelines](https://developer.toradex.com/torizon/application-development/demo-gallery/demo-gallery-partner-guidelines/). + +## Demo name + +wolfCrypt Post-Quantum Benchmarks + +## Description + +Live NIST post-quantum cryptography benchmarks on the Arm cores of a Torizon +module, running continuously on stock Torizon OS. The demo measures ML-KEM (FIPS 203) +and ML-DSA (FIPS 204) against the classical ECDSA, ECDHE and RSA they replace, +and prints each result as it completes. + +The headline it demonstrates is counter-intuitive: post-quantum key +establishment is **faster** than the classical cryptography it replaces on this +part. ML-KEM-768 encapsulation runs at 17,112 ops/sec against 2,865 for an +ECDHE P-256 agreement, a 6x speedup, and ML-DSA-44 verifies 1.7x faster than +ECDSA P-256. + +## Value proposition + +Anyone shipping a connected device has to migrate to post-quantum cryptography +for CRA and IEC 62443 timelines. The usual assumption is that this costs +performance on embedded silicon. This demo shows, on real hardware rather than +in a datasheet, where that assumption holds and where it does not, so an +integrator can plan a migration with measured numbers. + +## Expected behavior + +On start the container prints a banner naming the wolfSSL version, the selected +build and the CPU features it detected, then marks the cycle `[RUNNING]` with a +timestamp, runs the benchmark set printing one line per algorithm and operation, +and closes the cycle with `[OK]` or `[FAILED]`. It pauses briefly and repeats, +so the screen is never static and the current state is always legible at a +glance rather than inferred from output scrolling. + +Results are also written to `/results` in a named volume for later collection. + +## Hardware + +| | | +|---|---| +| Verified on | Toradex SMARC iMX95 Hexa 8GB (PN 00961100) on the Toradex SMARC Development Board | +| Expected to run on | any Arm64 Toradex module running Torizon OS | +| Torizon OS | 7.7.0 and newer | +| Architecture | arm64 / aarch64 | +| Peripherals | none required | +| Display | optional; output is a text console, readable over ssh or on HDMI | + +The image contains no i.MX95-specific code. It is plain aarch64 and selects its +wolfSSL build from the CPU features the host reports at runtime, so it should +run unchanged on other Arm64 modules; the numbers will differ with the core and +clock. The i.MX95 is the part we have measured, and the only one we claim. + +No carrier-specific hardware, no peripherals to connect, and no user-specific +configuration are needed. Everything is optional tuning through the environment +variables documented in `docker-compose.yml`, including `SOC_LABEL`, which names +the board in the banner (a container cannot normally read +`/proc/device-tree/model`). + +## Deployment + +```sh +docker compose up -d +``` + +`restart: always` brings it back after provisioning and after every reboot. + +## Container image + +- Registry: Docker Hub, `wolfssl/wolfcrypt-pqc` +- Tags: a pinned version (`1.0.0`) and `latest`, pushed together +- Platform: `linux/arm64` +- Base image: `debian:bookworm-slim` +- Size: about 129 MB +- Built from `../container/`, which cross-compiles wolfSSL for aarch64 on the + native build platform rather than under emulation + +Publish with `./publish.sh` (`VERSION=1.0.0 ./publish.sh` after `docker login`). +Docker Hub repositories are private by default; the Gallery needs this one set +to public once, in the repository's settings, after the first push. + +Two wolfSSL builds ship in the image: a NEON baseline that runs on any +Cortex-A55, and one using the Armv8.2 SHA-3 instructions. The entrypoint reads +`/proc/cpuinfo` and picks the safe one at runtime, because FEAT_SHA3 is optional +in the architecture and running the SHA-3 build without it faults. On the i.MX95 +that resolves to the baseline build. + +## External dependencies + +None beyond the base image. wolfSSL is compiled from source into the image and +linked statically, so there is nothing to install on the host and nothing +pulled at runtime. + +## Licensing + +wolfSSL is dual licensed under **GPLv3** or a commercial license from wolfSSL +Inc. Everything in this image is redistributable and publicly demonstrable +under GPLv3. The Debian base image carries its own upstream licenses. + +## Links + +- wolfSSL: https://www.wolfssl.com +- Source for this demo: https://github.com/wolfSSL/wolfBoot-examples +- Contact: facts@wolfssl.com + +## Note on the Cortex-M7 half + +The full demo pairs these benchmarks with wolfBoot performing ML-DSA-87 verified +boot on the i.MX95's Cortex-M7. That half is deliberately **not** part of this +Gallery entry: it requires wolfBoot flashed to the module and privileged access +to `/dev/mem`, so it cannot be plug-and-play on an unmodified Torizon OS image. +It is documented in the parent directory for anyone who wants to reproduce it. diff --git a/imx95-pqc-demo/gallery/docker-compose.yml b/imx95-pqc-demo/gallery/docker-compose.yml new file mode 100644 index 0000000..f2581e6 --- /dev/null +++ b/imx95-pqc-demo/gallery/docker-compose.yml @@ -0,0 +1,48 @@ +# wolfCrypt post-quantum benchmarks - Torizon Demo Gallery deployment. +# +# Self-contained: pulls one public image and starts on its own after +# provisioning. No host packages, no build step, no configuration required. +# +# docker compose up -d (on the board, or through Torizon Cloud) +# +# This is the Linux half of a larger demo. The Cortex-M7 half needs wolfBoot +# flashed to the module and privileged access to /dev/mem, so it is deliberately +# not part of the Gallery entry - see ../README.md for that. + +services: + wolfcrypt-pqc: + image: wolfssl/wolfcrypt-pqc:latest + container_name: wolfcrypt-pqc + # "always" rather than "unless-stopped": the demo has to come back on its + # own after provisioning and after every reboot. + restart: always + + # Keep a TTY so the banner renders in colour and output is line-buffered + # rather than held in a pipe's block buffer. + tty: true + + environment: + # auto pick the SHA-3 build only if the CPU advertises FEAT_SHA3 + # baseline force the NEON build + # sha3-crypto force the FEAT_SHA3 build (faults on parts without it) + # ab alternate each cycle to show the SHA-3 lever live + # Optional: name the board in the banner. A container cannot normally + # read /proc/device-tree, so without this it reads "Arm64 module". + SOC_LABEL: "${SOC_LABEL:-Toradex SMARC iMX95}" + MODE: "${MODE:-auto}" + LOOP: "1" + PAUSE: "${PAUSE:-5}" + + # Results are kept in a named volume so the demo needs no pre-created host + # directory and survives a container replacement. + volumes: + - wolfcrypt-results:/results + + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +volumes: + wolfcrypt-results: diff --git a/imx95-pqc-demo/gallery/publish.sh b/imx95-pqc-demo/gallery/publish.sh new file mode 100755 index 0000000..21f00fb --- /dev/null +++ b/imx95-pqc-demo/gallery/publish.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Build and publish the Gallery image to Docker Hub. Run on an x86 host with +# docker buildx; the build cross-compiles for aarch64 rather than emulating it. +# +# docker login +# VERSION=1.0.0 ./publish.sh +# +# Pushes : and :latest. The Demo Gallery needs the package +# to be PUBLIC: Docker Hub repositories default to private, so set visibility to +# public once, in the repository's Settings, after the first push. +set -euo pipefail + +REPO="${REPO:-wolfssl/wolfcrypt-pqc}" +VERSION="${VERSION:?set VERSION, e.g. VERSION=1.0.0}" +HERE="$(cd "$(dirname "$0")" && pwd)" +CTX="$HERE/../container" + +# The Docker builder stage compiles wolfSSL itself, so all it needs in the +# context is a pristine source export - not the full cross-build that +# build-aarch64.sh also performs. Export it here when it is missing, so +# publishing is one command rather than two with a non-obvious ordering. +if [ ! -x "$CTX/wolfssl-src/configure" ]; then + if [ -z "${WOLFSSL_REPO:-}" ]; then + echo "wolfssl-src/ missing in $CTX" >&2 + echo "point WOLFSSL_REPO at a wolfSSL checkout and re-run, e.g." >&2 + echo " WOLFSSL_REPO=~/GitHub/wolfssl VERSION=$VERSION $0" >&2 + echo "(or run ../container/build-aarch64.sh, which exports it as a side effect)" >&2 + exit 1 + fi + echo "=== exporting $WOLFSSL_REPO @ ${WOLFSSL_REF:-HEAD} -> $CTX/wolfssl-src ===" + # A pristine export, not a copy: the developer's checkout is configured + # in-tree and autotools refuses to build against one. + rm -rf "$CTX/wolfssl-src" + mkdir -p "$CTX/wolfssl-src" + git -C "$WOLFSSL_REPO" archive "${WOLFSSL_REF:-HEAD}" | tar -x -C "$CTX/wolfssl-src" + ( cd "$CTX/wolfssl-src" && ./autogen.sh >autogen.log 2>&1 ) \ + || { tail -20 "$CTX/wolfssl-src/autogen.log"; exit 1; } +fi + +ver=$(sed -n "s/^PACKAGE_VERSION='\(.*\)'/\1/p" "$CTX/wolfssl-src/configure" | head -1) +echo "=== building $REPO:$VERSION from wolfSSL ${ver:-unknown} ===" + +# linux/arm64 only: every Torizon module this targets is Arm64, and a second +# architecture would double build time for an image nothing would pull. +docker buildx build --platform linux/arm64 \ + -t "$REPO:$VERSION" -t "$REPO:latest" \ + --push "$CTX" + +echo +echo "pushed $REPO:$VERSION and $REPO:latest" +echo "if this was the first push, set the repository to PUBLIC at:" +echo " https://hub.docker.com/r/$REPO/settings" diff --git a/imx95-pqc-demo/m7/build.sh b/imx95-pqc-demo/m7/build.sh new file mode 100755 index 0000000..3d678f4 --- /dev/null +++ b/imx95-pqc-demo/m7/build.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Build the Zephyr RPMsg payload for the i.MX95 Cortex-M7, linked into +# wolfBoot's boot partition. +# +# Requires a Zephyr workspace and an arm-none-eabi toolchain. The Zephyr SDK's +# arm-zephyr-eabi works too; this defaults to the system toolchain because it is +# usually already present. +# +# ZEPHYR_BASE=~/zephyrproject/zephyr ./build.sh +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +BUILD="${BUILD:-$HERE/build}" +BOARD="${BOARD:-imx95_evk/mimx9596/m7/ddr}" + +: "${ZEPHYR_BASE:?set ZEPHYR_BASE to your Zephyr checkout}" +export ZEPHYR_BASE +export ZEPHYR_TOOLCHAIN_VARIANT="${ZEPHYR_TOOLCHAIN_VARIANT:-gnuarmemb}" +export GNUARMEMB_TOOLCHAIN_PATH="${GNUARMEMB_TOOLCHAIN_PATH:-/usr}" + +# The upstream openamp_rsc_table board overlay supplies the shared-memory and +# mailbox nodes; ours relocates the image and enables the MU and the console +# region. Both are needed. +SAMPLE_OVERLAY="$ZEPHYR_BASE/samples/subsys/ipc/openamp_rsc_table/boards/imx95_evk_mimx9596_m7.overlay" + +west build -p always -b "$BOARD" -d "$BUILD" "$HERE/zephyr-app" \ + -- -DDTC_OVERLAY_FILE="$HERE/imx95_wolfboot.overlay;$SAMPLE_OVERLAY" + +arm-none-eabi-objcopy -O binary \ + "$BUILD/zephyr/wolfboot_openamp.elf" "$BUILD/zephyr/payload.bin" + +echo +echo "payload: $BUILD/zephyr/payload.bin" +echo "sign it with wolfBoot's keytool, e.g.:" +echo " IMAGE_HEADER_SIZE=12288 ML_DSA_LEVEL=5 ./tools/keytools/sign --ml_dsa --sha256 \\" +echo " $BUILD/zephyr/payload.bin wolfboot_signing_private_key.der 1" diff --git a/imx95-pqc-demo/m7/imx95_wolfboot.overlay b/imx95-pqc-demo/m7/imx95_wolfboot.overlay new file mode 100644 index 0000000..6d33de9 --- /dev/null +++ b/imx95-pqc-demo/m7/imx95_wolfboot.overlay @@ -0,0 +1,67 @@ +#include + +/* + * i.MX95 Cortex-M7: place a Zephyr image where wolfBoot boots it, and expose + * the Message Unit that Linux uses to kick RPMsg. + * + * 1. Relocation + * ------------- + * The stock imx95_evk/mimx9596/m7/ddr target links at 0x80000000, which + * collides with wolfBoot's partition map. wolfBoot boots the payload in place + * at BOOT + IMAGE_HEADER_SIZE: + * + * BOOT partition 0x80100000 (4 MiB) + * IMAGE_HEADER_SIZE 0x3000 (12288, sized for ML-DSA-87) + * payload entry 0x80103000 <- must match test-app/ARM-imx95_m7.ld + * + * 2 MiB is carved here, well inside the 4 MiB partition and clear of the + * wolfBoot console ring at 0x80F00000 and the status block at 0x80F10000. + * + * 2. Message Unit + * --------------- + * The board's device tree points remoteproc-cm7 at mailbox@42430000, which the + * MIMX9596 CM7 header names MU7_MUA - the *A55* side of the pair. The M7 owns + * the B side: MU7_MUB at 0x42440000, NVIC vector MU7_B_IRQn = 207. + * + * Zephyr already describes that node upstream as &mu7, disabled by default, so + * all that is needed here is to enable it. Without it Linux's kick has nothing + * to talk to and the kernel logs "imx_rproc_kick: failed (0, err:-62)". + */ + +&ddr { + reg = <0x80103000 0x00200000>; +}; + +/* + * 3. wolfBoot's console ring and status block + * ------------------------------------------- + * The payload reads wolfBoot's shared-memory console at 0x80F00000 to relay it + * over RPMsg. That address is outside this image's linked SRAM, and Zephyr + * enables the ARM MPU on this SoC (CONFIG_ARM_MPU is select'ed, so prj.conf + * cannot turn it off), so the access faults. + * + * The failure is quiet and easy to misread: the payload boots, announces its + * RPMsg channel, faults on the first read of the console header, resets, and + * announces again - roughly every 15 ms. On the Linux side that looks like + * "creating channel ... already exist" repeating forever, and the RAM console + * reads back all zeros because .bss is wiped on every reset. Nothing says + * "fault". + * + * Declaring the region with an MPU attribute gives it a proper entry. 128 KiB + * covers the 64 KiB console page at 0x80F00000 and the status block at + * 0x80F10000. + */ + +&mu7 { + rx-channels = <4>; + status = "okay"; +}; + +/ { + wbconsole: memory@80f00000 { + compatible = "zephyr,memory-region", "mmio-sram"; + reg = <0x80F00000 DT_SIZE_K(128)>; + zephyr,memory-region = "WBCONSOLE"; + zephyr,memory-attr = ; + }; +}; diff --git a/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt b/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt new file mode 100644 index 0000000..238cfda --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(imx95_m7_wolfboot_openamp) +target_sources(app PRIVATE src/main.c) diff --git a/imx95-pqc-demo/m7/zephyr-app/prj.conf b/imx95-pqc-demo/m7/zephyr-app/prj.conf new file mode 100644 index 0000000..13da979 --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/prj.conf @@ -0,0 +1,35 @@ +CONFIG_KERNEL_BIN_NAME="wolfboot_openamp" +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_HEAP_MEM_POOL_SIZE=8192 + +CONFIG_OPENAMP=y +CONFIG_IPM=y +CONFIG_IPM_MBOX=y +CONFIG_MBOX_NXP_IMX_MU=y +CONFIG_MBOX_INIT_PRIORITY=0 +CONFIG_OPENAMP_WITH_DCACHE=y + +# The resource table Linux reads belongs to wolfBoot, not to this payload, so +# the subsystem's own table is deliberately NOT built - it would describe vrings +# that nobody honours. See src/main.c. +CONFIG_OPENAMP_RSC_TABLE=n + +CONFIG_CLOCK_CONTROL=y +CONFIG_ARM_SCMI=y + +# No M7 UART is routed on this carrier; logs go to a RAM buffer read with +# memtool, and the interesting output is relayed over RPMsg instead. +CONFIG_UART_CONSOLE=n +CONFIG_RAM_CONSOLE=y +CONFIG_RAM_CONSOLE_BUFFER_SIZE=8192 +CONFIG_PRINTK=y +CONFIG_LOG=y +CONFIG_LOG_MODE_MINIMAL=y +CONFIG_LOG_DEFAULT_LEVEL=3 + +# The payload reads two regions outside its own linked SRAM: wolfBoot's console +# ring at 0x80F00000 and the RPMsg vrings at 0x88000000. Zephyr's MPU config +# does not cover them, and the access faults - which shows up as the payload +# announcing its RPMsg channel and then resetting in a ~15 ms loop, with the +# RAM console reading back all zeros because .bss is wiped on every reset. +# wolfBoot itself runs this target with NO_MPU=1, so match that. diff --git a/imx95-pqc-demo/m7/zephyr-app/src/main.c b/imx95-pqc-demo/m7/zephyr-app/src/main.c new file mode 100644 index 0000000..ffdcb48 --- /dev/null +++ b/imx95-pqc-demo/m7/zephyr-app/src/main.c @@ -0,0 +1,426 @@ +/* + * wolfBoot-aware Zephyr RPMsg payload for the i.MX95 Cortex-M7. + * + * Brings up the M7 side of the RPMsg link that wolfBoot's resource table + * declared to Linux, and relays the M7's shared-memory console - wolfBoot's own + * verify log included - to the A55 as a virtual TTY. + * + * + * Why this is not samples/subsys/ipc/openamp_rsc_table + * --------------------------------------------------- + * That sample assumes Linux loaded *its* ELF. The standard remoteproc contract + * is that the remote declares vrings with da = FW_RSC_ADDR_ANY, Linux allocates + * them, and Linux writes the resolved addresses and the virtio status back into + * the resource table it loaded. The remote then reads its own table. + * + * Here Linux loads wolfBoot, not this payload. So Linux writes the resolved + * values into wolfBoot's .resource_table, while the sample polls the table + * linked into its own image - two different structures. The sample gets as far + * as rproc_virtio_wait_remote_ready() and waits forever for a status nobody + * will ever write. + * + * CONFIG_OPENAMP_COPY_RSC_TABLE looks like the bridge and is not: it memcpy's + * the payload's own unresolved table over the shared location before reading it + * back. + * + * The fix is to stop relying on host writeback. Both sides use FIXED addresses: + * wolfBoot's table hardcodes them, so Linux honours them, and this payload uses + * the same constants. They come from the board's own reserved-memory nodes, so + * the two sides agree by construction rather than by convention: + * + * vdev0vring0 0x88000000 32 KiB + * vdev0vring1 0x88008000 32 KiB + * vdevbuffer 0x88020000 1 MiB + * + * Because the addresses are fixed there is nothing to wait for, and the host is + * ready by construction: Linux registers virtio0 during rproc_start, before it + * releases the core that eventually runs this code. + * + * KEEP IN SYNC with the resource table in wolfBoot's hal/imx95_m7.c. If the + * vring geometry there changes, change it here too - a mismatch shows up as + * silence, not as an error. + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +LOG_MODULE_REGISTER(wolfboot_openamp, LOG_LEVEL_INF); + +/* --- Must match wolfBoot's resource table (hal/imx95_m7.c) --- */ +#define VRING0_DA 0x88000000UL /* host -> remote */ +#define VRING1_DA 0x88008000UL /* remote -> host */ +#define VRING_ALIGN 0x1000U +#define VRING_NUM 256U + +/* vrings plus the 1 MiB vdevbuffer at 0x88020000 */ +#define SHM_BASE 0x88000000UL +#define SHM_SIZE 0x00120000UL + +#define VDEV_ID 0xFFU +#define VRING0_ID 0U +#define VRING1_ID 1U + +/* --- wolfBoot's shared-memory console (hal/imx95_m7.h) --- */ +#define CONSOLE_BASE 0x80F00000UL +#define CONSOLE_HDR_SIZE 16U +#define CONSOLE_MAGIC 0x4E4F4357UL /* "WCON" */ + +/* The Linux imx_rpmsg_tty driver binds this name and creates /dev/ttyRPMSG*. */ +#define TTY_CHANNEL_NAME "rpmsg-virtual-tty-channel" + +/* Progress published to wolfBoot's app status block, read from Linux with + * "memtool r 0x80F10010 4". The RAM console proved unreliable for this - a + * fault can halt the core before anything is flushed - so the state goes to a + * plain shared word instead: + * + * +0x00 magic 'ZOAM' + * +0x04 progress: 1 platform_init, 2 vdev up, 3 endpoint created + * +0x08 heartbeat, increments every loop pass + * +0x0C 1 once the endpoint has a destination (host has replied) + */ +#define APP_STATUS_ADDR 0x80F10010UL +#define APP_STATUS_MAGIC 0x5A4F414DUL /* "ZOAM" */ + +/* The M7 runs with D-cache enabled (wolfBoot turns it on), so a plain store + * lands in cache and the A55 never sees it. wolfBoot's own console code cleans + * the cache after every write for exactly this reason; do the same here. + * Without it the status words read back as zero from Linux - or worse, appear + * intermittently as lines happen to get evicted. */ +static void app_status(uint32_t idx, uint32_t val) +{ + volatile uint32_t *st = (volatile uint32_t *)APP_STATUS_ADDR; + + st[0] = APP_STATUS_MAGIC; + st[idx] = val; + sys_cache_data_flush_range((void *)APP_STATUS_ADDR, 32); +} + +struct console_hdr { + volatile uint32_t magic; + volatile uint32_t wr; /* total bytes ever written, monotonic */ + volatile uint32_t size; + volatile uint32_t rsvd; +}; + +/* rproc_virtio_create_vdev() only reads the header fields of this structure; + * the vrings are supplied separately to rproc_virtio_init_vring(). They are + * declared anyway so the layout matches a real resource table entry. + * + * status is pre-set to DRIVER_OK rather than waiting for the host to write it: + * see the file header for why there is no writeback to wait for. */ +struct local_vdev_rsc { + struct fw_rsc_vdev vdev; + struct fw_rsc_vdev_vring vring[2]; +}; + +static struct local_vdev_rsc vdev_rsc = { + .vdev = { + .type = RSC_VDEV, + .id = VIRTIO_ID_RPMSG, + .notifyid = 0, + .dfeatures = 1U << VIRTIO_RPMSG_F_NS, + .gfeatures = 1U << VIRTIO_RPMSG_F_NS, + .config_len = 0, + .status = VIRTIO_CONFIG_STATUS_DRIVER_OK, + .num_of_vrings = 2, + }, + .vring = { + { VRING0_DA, VRING_ALIGN, VRING_NUM, VRING0_ID, 0 }, + { VRING1_DA, VRING_ALIGN, VRING_NUM, VRING1_ID, 0 }, + }, +}; + +static const struct device *const ipm_handle = DEVICE_DT_GET(DT_CHOSEN(zephyr_ipc)); + +/* A fault here halts the core with nothing useful in the RAM console - the log + * backend does not get a chance to flush. Publish the reason and PC to the + * status block instead, which is a plain store and always works: + * + * +0x10 fault reason (Zephyr K_ERR_*) + * +0x14 faulting PC + * +0x18 0xDEADBEEF marker that a fault happened at all + */ +void k_sys_fatal_error_handler(unsigned int reason, const struct arch_esf *esf) +{ + volatile uint32_t *st = (volatile uint32_t *)APP_STATUS_ADDR; + + st[4] = (uint32_t)reason; + st[5] = (esf != NULL) ? (uint32_t)esf->basic.pc : 0xFFFFFFFFU; + st[6] = 0xDEADBEEFU; + sys_cache_data_flush_range((void *)APP_STATUS_ADDR, 32); + + for (;;) { + /* halt - leave the state readable from the A55 */ + } +} + +static metal_phys_addr_t shm_physmap[] = { SHM_BASE }; +static metal_phys_addr_t rsc_physmap[] = { (metal_phys_addr_t)(uintptr_t)&vdev_rsc }; + +static struct metal_io_region shm_io_data; +static struct metal_io_region rsc_io_data; +static struct metal_io_region *shm_io = &shm_io_data; +static struct metal_io_region *rsc_io = &rsc_io_data; + +static struct rpmsg_virtio_device rvdev; +static struct rpmsg_endpoint tty_ept; + +static K_SEM_DEFINE(kick_sem, 0, 1); + +static void ipm_callback(const struct device *dev, void *context, + uint32_t id, volatile void *data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(context); + ARG_UNUSED(id); + ARG_UNUSED(data); + k_sem_give(&kick_sem); +} + +static int mailbox_notify(void *priv, uint32_t id) +{ + ARG_UNUSED(priv); + ipm_send(ipm_handle, 0, id, &id, sizeof(id)); + return 0; +} + +/* Set when the host writes to the TTY, asking for the console log again. + * + * The relay flushes the whole ring the moment the endpoint has a destination + * address, which is before any reader can realistically be attached: the + * /dev/ttyRPMSG node does not exist until the host has bound the channel, and + * by the time a reader opens it the one-shot replay has already been sent and + * dropped. Treating any inbound byte as a replay request makes the log + * retrievable on demand, so "attach a reader, then poke the TTY" works every + * time instead of racing the bind. */ +static volatile bool replay_req; +static uint32_t rx_count; + +static int tty_recv(struct rpmsg_endpoint *ept, void *data, size_t len, + uint32_t src, void *priv) +{ + ARG_UNUSED(ept); + ARG_UNUSED(data); + ARG_UNUSED(src); + ARG_UNUSED(priv); + LOG_DBG("rx %u bytes from the host, replaying console", + (unsigned int)len); + rx_count++; + app_status(10, rx_count); + replay_req = true; + return RPMSG_SUCCESS; +} + +static void ns_bind_cb(struct rpmsg_device *rdev, const char *name, uint32_t src) +{ + ARG_UNUSED(rdev); + ARG_UNUSED(src); + LOG_INF("host announced service '%s'", name); +} + +static int platform_init(void) +{ + struct metal_init_params params = METAL_INIT_DEFAULTS; + int ret; + + ret = metal_init(¶ms); + if (ret != 0) { + LOG_ERR("metal_init failed: %d", ret); + return ret; + } + + metal_io_init(shm_io, (void *)SHM_BASE, shm_physmap, SHM_SIZE, -1, 0, NULL); + metal_io_init(rsc_io, &vdev_rsc, rsc_physmap, sizeof(vdev_rsc), -1, 0, NULL); + + if (!device_is_ready(ipm_handle)) { + LOG_ERR("IPM device not ready"); + return -ENODEV; + } + + ipm_register_callback(ipm_handle, ipm_callback, NULL); + + ret = ipm_set_enabled(ipm_handle, 1); + if (ret != 0) { + LOG_ERR("ipm_set_enabled failed: %d", ret); + return ret; + } + + return 0; +} + +static struct rpmsg_device *rpmsg_start(void) +{ + struct virtio_device *vdev; + int ret; + + /* VIRTIO_DEV_DEVICE: this core is the device/remote, Linux is the driver. */ + vdev = rproc_virtio_create_vdev(VIRTIO_DEV_DEVICE, VDEV_ID, &vdev_rsc.vdev, + rsc_io, NULL, mailbox_notify, NULL); + if (vdev == NULL) { + LOG_ERR("rproc_virtio_create_vdev failed"); + return NULL; + } + + /* No rproc_virtio_wait_remote_ready() here on purpose - the status it polls + * is written into wolfBoot's table, not this one, and the host is already + * up by the time this code runs. */ + + ret = rproc_virtio_init_vring(vdev, 0, VRING0_ID, (void *)VRING0_DA, + shm_io, VRING_NUM, VRING_ALIGN); + if (ret != 0) { + LOG_ERR("init vring 0 failed: %d", ret); + return NULL; + } + + ret = rproc_virtio_init_vring(vdev, 1, VRING1_ID, (void *)VRING1_DA, + shm_io, VRING_NUM, VRING_ALIGN); + if (ret != 0) { + LOG_ERR("init vring 1 failed: %d", ret); + return NULL; + } + + ret = rpmsg_init_vdev(&rvdev, vdev, ns_bind_cb, shm_io, NULL); + if (ret != 0) { + LOG_ERR("rpmsg_init_vdev failed: %d", ret); + return NULL; + } + + return rpmsg_virtio_get_rpmsg_device(&rvdev); +} + +/* Copy whatever is new in wolfBoot's console ring out to the endpoint. + * + * The ring publishes `wr` as a monotonic count of bytes ever written, so the + * reader keeps its own position and copies (wr - pos) from data[pos % size]. + * Starting pos at 0 means the first pass replays everything already in the + * buffer, which is the point: wolfBoot's verify log is written long before any + * RPMsg endpoint exists, and replaying it is the only way the A55 ever sees it. + */ +static void relay_console(struct rpmsg_endpoint *ept, uint32_t *pos) +{ + const struct console_hdr *h = (const struct console_hdr *)CONSOLE_BASE; + const volatile uint8_t *data = + (const volatile uint8_t *)(CONSOLE_BASE + CONSOLE_HDR_SIZE); + static uint32_t sent_total; + uint8_t chunk[256]; + uint32_t wr, size, avail, i; + int ret; + + /* wolfBoot wrote this ring with its own cache cleans, but this core may + * still hold stale lines for it, so invalidate before every read. */ + sys_cache_data_invd_range((void *)CONSOLE_BASE, CONSOLE_HDR_SIZE); + + if (h->magic != CONSOLE_MAGIC) { + return; + } + + size = h->size; + wr = h->wr; + if (size == 0U || wr == *pos) { + return; + } + + avail = wr - *pos; + if (avail > size) { + /* Reader fell behind; report the gap rather than emit corrupt text. */ + static const char msg[] = "\r\n[console overrun - output dropped]\r\n"; + + (void)rpmsg_send(ept, msg, sizeof(msg) - 1U); + *pos = wr - size; + avail = size; + } + + sys_cache_data_invd_range((void *)(CONSOLE_BASE + CONSOLE_HDR_SIZE), size); + + while (avail > 0U) { + uint32_t n = (avail > sizeof(chunk)) ? (uint32_t)sizeof(chunk) : avail; + + for (i = 0; i < n; i++) { + chunk[i] = data[(*pos + i) % size]; + } + + ret = rpmsg_send(ept, chunk, (int)n); + app_status(7, (uint32_t)ret); /* last send result */ + app_status(9, avail); /* bytes still pending */ + if (ret < 0) { + /* Host buffer full: leave the rest for the next pass. */ + return; + } + sent_total += n; + app_status(8, sent_total); + + *pos += n; + avail -= n; + } +} + +int main(void) +{ + struct rpmsg_device *rpdev; + uint32_t pos = 0; + uint32_t beat = 0; + int ret; + + LOG_INF("wolfBoot RPMsg payload starting"); + + ret = platform_init(); + if (ret != 0) { + return ret; + } + app_status(1, 1); + + rpdev = rpmsg_start(); + if (rpdev == NULL) { + return -EIO; + } + app_status(1, 2); + + ret = rpmsg_create_ept(&tty_ept, rpdev, TTY_CHANNEL_NAME, + RPMSG_ADDR_ANY, RPMSG_ADDR_ANY, tty_recv, NULL); + if (ret != 0) { + LOG_ERR("rpmsg_create_ept failed: %d", ret); + return ret; + } + + app_status(1, 3); + LOG_INF("endpoint '%s' announced", TTY_CHANNEL_NAME); + + while (1) { + /* Service the host's kick if one arrived, then push console output. */ + if (k_sem_take(&kick_sem, K_MSEC(50)) == 0) { + rproc_virtio_notified(rvdev.vdev, VRING1_ID); + } + + /* Only relay once the host has bound the endpoint and a destination + * address is known. Sending earlier fails, and the failed send makes + * OpenAMP re-announce the name service - which shows up on the Linux + * side as "creating channel ... already exist" repeating forever and + * stops the tty driver from ever attaching. */ +#ifdef RELAY_DISABLED + (void)pos; +#else + app_status(2, ++beat); + app_status(3, is_rpmsg_ept_ready(&tty_ept) ? 1U : 0U); + app_status(11, tty_ept.dest_addr); + + if (is_rpmsg_ept_ready(&tty_ept)) { + if (replay_req) { + replay_req = false; + pos = 0; /* rewind; relay_console reports any lost span */ + } + relay_console(&tty_ept, &pos); + } +#endif + } + + return 0; +} diff --git a/imx95-pqc-demo/tools/memtool.c b/imx95-pqc-demo/tools/memtool.c new file mode 100644 index 0000000..eb3e4ba --- /dev/null +++ b/imx95-pqc-demo/tools/memtool.c @@ -0,0 +1,259 @@ +/* memtool.c - peek/poke/load physical memory via /dev/mem on the i.MX95. + * + * Torizon's busybox has no devmem applet, and plain dd on /dev/mem fails with + * EFAULT, so map the page explicitly instead. + * + * Usage: + * memtool r [words] read (default 8 words) + * memtool w write one 32-bit word + * memtool load copy a file to physical memory + * memtool fill + * + * Copyright (C) 2026 wolfSSL Inc. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int map_region(off_t phys, size_t len, void **base, void **ptr, int *fd) +{ + long pagesz = sysconf(_SC_PAGESIZE); + off_t aligned = phys & ~((off_t)pagesz - 1); + size_t offset = (size_t)(phys - aligned); + size_t maplen = len + offset; + + *fd = open("/dev/mem", O_RDWR | O_SYNC); + if (*fd < 0) { + fprintf(stderr, "open /dev/mem: %s\n", strerror(errno)); + return -1; + } + *base = mmap(NULL, maplen, PROT_READ | PROT_WRITE, MAP_SHARED, *fd, aligned); + if (*base == MAP_FAILED) { + fprintf(stderr, "mmap 0x%llx (len %zu): %s\n", + (unsigned long long)aligned, maplen, strerror(errno)); + close(*fd); + return -1; + } + *ptr = (void *)((char *)*base + offset); + return (int)maplen; +} + +int main(int argc, char **argv) +{ + void *base = NULL, *ptr = NULL; + int fd = -1, maplen; + off_t addr; + + if (argc < 3) + goto usage; + + addr = (off_t)strtoull(argv[2], NULL, 16); + + if (strcmp(argv[1], "r") == 0) { + unsigned n = (argc > 3) ? (unsigned)strtoul(argv[3], NULL, 0) : 8; + unsigned i; + + maplen = map_region(addr, n * 4, &base, &ptr, &fd); + if (maplen < 0) + return 1; + for (i = 0; i < n; i++) { + if ((i % 4) == 0) + printf("\n%08llx: ", (unsigned long long)(addr + i * 4)); + printf("%08x ", ((volatile uint32_t *)ptr)[i]); + } + printf("\n"); + } + else if (strcmp(argv[1], "w") == 0) { + uint32_t val; + + if (argc < 4) + goto usage; + val = (uint32_t)strtoul(argv[3], NULL, 16); + maplen = map_region(addr, 4, &base, &ptr, &fd); + if (maplen < 0) + return 1; + *(volatile uint32_t *)ptr = val; + } + else if (strcmp(argv[1], "fill") == 0) { + size_t len; + int byte; + + if (argc < 5) + goto usage; + len = (size_t)strtoul(argv[3], NULL, 0); + byte = (int)strtoul(argv[4], NULL, 16); + maplen = map_region(addr, len, &base, &ptr, &fd); + if (maplen < 0) + return 1; + /* Same device-memory constraint as "load" below: libc's memset emits + * unaligned and multi-register stores, which raise SIGBUS against the + * /dev/mem mapping on this part. Fill in aligned 32-bit words. */ + { + volatile uint32_t *dst = (volatile uint32_t *)ptr; + uint32_t word; + size_t words, i; + + word = (uint32_t)(byte & 0xff); + word |= (word << 8); + word |= (word << 16); + words = (len + 3) / 4; + for (i = 0; i < words; i++) + dst[i] = word; + } + } + else if (strcmp(argv[1], "load") == 0) { + struct stat st; + FILE *f; + size_t got; + + if (argc < 4) + goto usage; + if (stat(argv[3], &st) != 0) { + fprintf(stderr, "stat %s: %s\n", argv[3], strerror(errno)); + return 1; + } + f = fopen(argv[3], "rb"); + if (f == NULL) { + fprintf(stderr, "open %s: %s\n", argv[3], strerror(errno)); + return 1; + } + maplen = map_region(addr, (size_t)st.st_size, &base, &ptr, &fd); + if (maplen < 0) { + fclose(f); + return 1; + } + /* Do NOT fread() straight into the mapping. /dev/mem hands back a + * device-memory mapping on this part, where the unaligned and + * multi-register stores libc's memcpy emits raise SIGBUS. Stage the + * file in normal memory and copy it across in aligned 32-bit words. */ + { + unsigned char *buf = malloc((size_t)st.st_size + 4); + volatile uint32_t *dst = (volatile uint32_t *)ptr; + const uint32_t *src; + size_t words, i; + + if (buf == NULL) { + fprintf(stderr, "malloc %lld failed\n", (long long)st.st_size); + fclose(f); + return 1; + } + memset(buf, 0, (size_t)st.st_size + 4); + got = fread(buf, 1, (size_t)st.st_size, f); + src = (const uint32_t *)buf; + words = ((size_t)st.st_size + 3) / 4; + for (i = 0; i < words; i++) + dst[i] = src[i]; + free(buf); + } + fclose(f); + if (got != (size_t)st.st_size) { + fprintf(stderr, "short read: %zu of %lld\n", + got, (long long)st.st_size); + munmap(base, (size_t)maplen); + close(fd); + return 1; + } + printf("loaded %lld bytes to 0x%llx\n", + (long long)st.st_size, (unsigned long long)addr); + } + else if (strcmp(argv[1], "con") == 0) { + /* Dump the M7 shared-memory console ring buffer (see + * wolfboot hal/uart/uart_drv_imx95_m7.c). Header is + * magic / wr / size / rsvd, followed by the text. */ + volatile uint32_t *hdr; + uint32_t magic, wr, bufsz, i; + unsigned char *out; + + maplen = map_region(addr, 16, &base, &ptr, &fd); + if (maplen < 0) + return 1; + hdr = (volatile uint32_t *)ptr; + magic = hdr[0]; + wr = hdr[1]; + bufsz = hdr[2]; + munmap(base, (size_t)maplen); + close(fd); + base = NULL; fd = -1; + + if (magic != 0x4E4F4357UL) { /* "WCON" */ + fprintf(stderr, "no console at 0x%llx (magic 0x%08x)\n", + (unsigned long long)addr, magic); + return 1; + } + if (bufsz == 0 || bufsz > (64U * 1024U * 1024U)) { + fprintf(stderr, "implausible console size %u\n", bufsz); + return 1; + } + if (wr == 0) { + fprintf(stderr, "console empty\n"); + return 0; + } + if (wr > bufsz) { + fprintf(stderr, + "[console overran: %u bytes written into a %u byte buffer; " + "showing the most recent %u]\n", wr, bufsz, bufsz); + } + + maplen = map_region(addr + 16, bufsz, &base, &ptr, &fd); + if (maplen < 0) + return 1; + /* The copy below moves whole 32-bit words, so round the allocation up + * to the word count rather than bufsz + 1: a ring size that is not a + * multiple of 4 would otherwise be written up to 2 bytes past the end. */ + out = malloc(((size_t)bufsz + 3u) / 4u * 4u + 1u); + if (out == NULL) { + fprintf(stderr, "malloc failed\n"); + munmap(base, (size_t)maplen); + close(fd); + return 1; + } + /* /dev/mem hands back device memory: copy with aligned 32-bit reads, + * never memcpy (which raises SIGBUS with unaligned/vector loads). */ + { + volatile uint32_t *src = (volatile uint32_t *)ptr; + uint32_t words = (bufsz + 3) / 4; + uint32_t *dst = (uint32_t *)out; + + for (i = 0; i < words; i++) + dst[i] = src[i]; + } + + if (wr <= bufsz) { + fwrite(out, 1, wr, stdout); + } + else { + /* Ring wrapped: oldest byte is at wr % bufsz. */ + uint32_t start = wr % bufsz; + fwrite(out + start, 1, bufsz - start, stdout); + fwrite(out, 1, start, stdout); + } + fflush(stdout); + free(out); + } + else { + goto usage; + } + + if (base != NULL && base != MAP_FAILED) + munmap(base, (size_t)maplen); + if (fd >= 0) + close(fd); + return 0; + +usage: + fprintf(stderr, + "usage:\n" + " memtool r [words]\n" + " memtool w \n" + " memtool load \n" + " memtool fill \n" + " memtool con dump M7 shared-memory console\n"); + return 1; +}