From 90480629ad703fc58866f180cae20ba05175c899 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 21 Jun 2026 19:01:43 +0800 Subject: [PATCH 1/5] feat(test): armcc/AC6 firmware corpus E2E (T5c) Add 3 prebuilt STM32CubeF1 STM32F103RB-Nucleo firmware compiled under Keil MDK + Arm Compiler 6 (armclang) as committed ELF (.axf) E2E fixtures, exercising armcc codegen alongside the existing gcc sample: - nucleo_f103rb_tim_timebase.ac6.axf (TIM UIF / interrupt) - nucleo_f103rb_uart_printf.ac6.axf (USART TX, printf retarget) - nucleo_f103rb_gpio_iotoggle.ac6.axf (GPIO toggle) Each boots clean (0 fault) at 2,000,000 steps; wired into ctest via test_firmware_armcc. CI has no Keil so binaries are committed; test/firmware/armcc/REGENERATE.md documents the AC5->AC6 migration recipe (uAC6=1, drop --C99, drop v6Lang) and the WSL-fs -> local-NTFS build constraint. --- test/CMakeLists.txt | 13 +++ test/firmware/armcc/REGENERATE.md | 58 +++++++++++ .../armcc/nucleo_f103rb_gpio_iotoggle.ac6.axf | Bin 0 -> 45640 bytes .../armcc/nucleo_f103rb_tim_timebase.ac6.axf | Bin 0 -> 143500 bytes .../armcc/nucleo_f103rb_uart_printf.ac6.axf | Bin 0 -> 92552 bytes test/test_firmware_armcc.cpp | 91 ++++++++++++++++++ 6 files changed, 162 insertions(+) create mode 100644 test/firmware/armcc/REGENERATE.md create mode 100755 test/firmware/armcc/nucleo_f103rb_gpio_iotoggle.ac6.axf create mode 100755 test/firmware/armcc/nucleo_f103rb_tim_timebase.ac6.axf create mode 100755 test/firmware/armcc/nucleo_f103rb_uart_printf.ac6.axf create mode 100644 test/test_firmware_armcc.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 51b27d0..93c51d7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -171,6 +171,19 @@ if(TARGET hello_firmware AND TARGET blink_firmware AND TARGET systick_firmware) gtest_discover_tests(test_e2e) endif() +# ── armcc/AC6 固件语料 E2E(T5c)── +# 预编译 .axf fixture 提交进 test/firmware/armcc/;CI 无 Keil 故不重建。 +add_executable(test_firmware_armcc + test_firmware_armcc.cpp +) +target_link_libraries(test_firmware_armcc + PRIVATE micro_forge GTest::gtest_main +) +target_compile_definitions(test_firmware_armcc PRIVATE + ARMCC_FW_DIR="${CMAKE_CURRENT_SOURCE_DIR}/firmware/armcc" +) +gtest_discover_tests(test_firmware_armcc) + # ── CLI 测试(drives the `micro-forge` binary) ── if(TARGET micro-forge AND TARGET hello_firmware) add_executable(test_cli diff --git a/test/firmware/armcc/REGENERATE.md b/test/firmware/armcc/REGENERATE.md new file mode 100644 index 0000000..82a687b --- /dev/null +++ b/test/firmware/armcc/REGENERATE.md @@ -0,0 +1,58 @@ +# armcc/AC6 固件语料 — 重生成指南 (T5c) + +`test/firmware/armcc/*.ac6.axf` 是 STM32CubeF1 **STM32F103RB-Nucleo** 示例在 +**Keil MDK + Arm Compiler 6 (armclang)** 下预编译的 ELF fixture,作为 ctest E2E +回归门禁。CI 无 Keil,故二进制提交进仓库;仅在需要刷新 codegen 时本地重编。 + +## 当前语料 + +| fixture | CubeF1 示例 | 覆盖 | +|---|---|---| +| `nucleo_f103rb_tim_timebase.ac6.axf` | TIM/TIM_TimeBase | TIM UIF / 中断 | +| `nucleo_f103rb_uart_printf.ac6.axf` | UART/UART_Printf | USART TX (printf 重定向) | +| `nucleo_f103rb_gpio_iotoggle.ac6.axf` | GPIO/GPIO_IOToggle | GPIO 翻转 | + +每份均 `ELF32 / ARM / entry=0x80000ed`,与 Keil F103.axf 同款,`Stm32f103Soc::load_elf` 直接加载。 + +## 环境 + +- Keil MDK + AC6 (armclang) + **STM32F1xx_DFP 2.4.1** + CMSIS 6.x +- Pack 仓库自定义在 `D:\MDK-Pack`;UV4 在 `D:\MDK\UV4\UV4.exe` +- headless 构建:`UV4.exe -b -o `(退出码 0=干净 / 1=警告但成功出 .axf / 2+=错) + +## 为什么不能在 WSL 文件系统上编 + +Keil 的编译器响应文件(`.__i`)在 WSL fs(9p,经 `\\wsl.localhost` 或映射盘符)上 +**创建失败**。必须编在**本地 NTFS**。故把最小子树复制到 `D:\mf\STM32CubeF1\` +(保留 `..\..\..\..\..\Drivers` 的相对目录深度),在那里编。 + +## CubeF1 AC5→AC6 迁移补丁(每个 .uvprojx 必做 3 处) + +CubeF1 示例工程是 AC5 配置;新版 MDK 只有 AC6,需三处补丁: + +1. **选 AC6** — 在 `` 层(紧跟 `ARM-ADS` 之后) + 插入 `1`。(注意:必须在 `` 直接子级,不是 `` 内。) +2. **删 `--C99`** — `--C99` 清空 + (AC5 flag,AC6 不认;C99 由 AC6 默认提供)。 +3. **删语言标准字段** — 删 ``/``/``。 + 否则 GUI 切 AC6 时 Keil 写入的值会强制 C90(`inline` 报错);删掉走 AC6 默认 gnu11。 + +GUI 等价操作:Options for Target → Target 页 → ARM Compiler 选 **AC6**; +C/C++ 页 → Language C 选 **gnu11**(勿选 C90);不要勾 AC5 的 C99 mode。 + +## 重编步骤 + +1. 复制最小子树到 `D:\mf\STM32CubeF1\`: + `Drivers/{STM32F1xx_HAL_Driver, BSP, CMSIS}`(CMSIS 可去 DSP)+ 选定的 `Projects/STM32F103RB-Nucleo/Examples/`。 +2. 对每个 `.uvprojx` 应用上述 3 补丁。 +3. 跑 `D:\mf\build_corpus.bat`(遍历编 GPIO/TIM/UART)。 +4. 把产出的 `MDK-ARM\STM32F103RB_Nucleo\STM32F103RB_Nucleo.axf` 拷成 + `test/firmware/armcc/nucleo_f103rb_.ac6.axf`。 +5. 新增示例:在 `test/test_firmware_armcc.cpp` 加一个 + `TEST(FirmwareArmcc, BootsClean)`。 + +## 常见坑 + +- **WSL→Windows interop 偶发挂**(`exec format error`):挂了就在 Windows 原生跑 `.bat`(双击或 PowerShell)。`/mnt/d/mf` 从 WSL 仍可正常读写,故编完拷回不阻塞。 +- **免费版(无 license)** 32KB 代码大小限制;Nucleo 小示例远低于此。 +- 第三方 `third_party/STM32CubeF1` 的 `.uvprojx` 补丁**不入库**(vendored 子模块保持干净);本文件即权威配方。 diff --git a/test/firmware/armcc/nucleo_f103rb_gpio_iotoggle.ac6.axf b/test/firmware/armcc/nucleo_f103rb_gpio_iotoggle.ac6.axf new file mode 100755 index 0000000000000000000000000000000000000000..fd9f3a0760730a1a3e27f980fa51ba860fe9530d GIT binary patch literal 45640 zcmeHw34B$>+4szubIv_Cxw*L^EFmCXAV5U8B!GyxWru_WLK1Kz4Iv~X8j_d=K|un7 z;!=yER&B+lu5A_6+NymO?ef;8T5zlSTD8^|tF{ecb>;g%bI#m*?n&ZSzxR9J-|xF{ zX6FCQGtWHp%syvw@4CX$BF;HeAB|-)LjPdQx_digPvOxtW=v;c&@)+xB}>*!orRIi zx+j5Ix8k`2&q82-BD0h%<=>CEGZ07kUW6aQGXnU3%k$RZz%JI4c9_j!vy)IhD!VzS zd$tMu`JTT29zprYej^|9&tY5qbNCjci2cxf`|ZY@9xkqV&&RHx9h&~ryn`UiG>1FI!c-X4ik!d^i0=AR&4E`u*#4ULrzqsSE z$$#&ezhOtPkwsX~XWhBIEblkHT+_!Q)x0fv)=n{Y{=p+_Knt?1EQ61IvNsf%-@_8{ z_~*XOb|QsFDcH@#txeE-0wQ_LjXB4@uW7!I#lHFP-)N-g7ablKp4XVu8z}76-aKrD zt9Fig-_r8%Ag%E{C=(kW)MC_$OCA(&9@bra`^59WZysjh`6z#k^LbE$jR|{s!i79S^kGW7jKYwxakjSYo^-WU{F^y*vhnLWoPmK85_e+m~cwsZI4z?LGE z1to2NvUjw0U0(7VY|N9rDF~(SqR=3OhR6`bhIUPPo2X+DPLR}WgbW#)icokr+j&`I z$^Crd;GIt#9=zj;!=WAXdo2H8%A-g3hu}ZcsO{DLiEon~EeBhE@ix%|ezsF98ejDA zf$nt~JN2pIQ-(Drk9#O*!uy8d|JXcuT>g+H&#ZpITz6o@_T1hy@ta;vq&MDo;KG%8 zJzPs)lHRDn=U4L^C-w+!(P3>z&Oy7DP)EBEmUX-(Z{7azc0OU;9yVd@9&N{C@AZ5S zEu~R=pNp&VbKe)aytZw2PH)(5J&hD*446}E!o%6BgvL;TwS?MR_d|K0LKDUtb9+LZ zFA-tZbLQb>xW-}DGXeF~{TSNjLy%%3wVBgv@u{mw&iwu7claA0{G_*6FS`Ge-n|xD z?y;XFPeM%_)~hogncs6HYjhDC!^V-U&Lx>eZI4l&p@-%j3@m=Wmj&K9%(8yHPm5gH zbf*MA*7wic$;RHAe?v|X?eWI^lzi5ctg*dI^8H+&B@QghUs}L2 z=j?>t#qhQebcECekjfJp(IP~8&XRozWAb{sm*n(()+S4vsqf_Bw%O=A`RjL}+!o4h z=|!&|W-JN1``G%tz-xrU2xsO$g%BZ%xjV0UcepXPSNE^?A(emF%eg+|<<}0gQ11S> zYoCVCqj<>z!W4H~vG@fs^Ch&M&=b8U zvpM;vpv^96JmauFQp>b<@yzkNFBRrXJj|&bUBq@?$`8w&NB0PBk>2dcq1Qe)i*kE6 zA5Xd0HZtV4`2^L48Q9e(wbChV|BVKN64D|$({SKc6zDv5> z8lMns??uG#6EBCGIH-~JS&g+#;hw{c?3=YDyo&i!Ukb0v@lAT^uT5++W`gy0UL@nL zZaz7uM_CFrC(N7%i9(?4`tj{q6>})%`+sF1k(0%04)X#H9 zXqg9LGdmUA+0#_>{u7qM1WBIpl}LWtoSf5|xHxH}(M6xPGwcb}TmlfE7j zvqt`z?F>VA8Yf6|*0Z^rkLJkdjn7DTve2mq+P5D$^wMWu-T&N}bv8-9soQ{ibvHjI zr)PUN<~nx&+qNB;V~3whS!9QG2NprreSKuH3CMi{3(ssga_HvIw8=(M(r)C>mQOgJlfbuN4&BMG z&mF}w`BkUxPu=cDfjDHi(7U@o`x}$?sEXQS-GROL9XV7Q&3XT8KikvRIQne1zkBLs zdH&tC2(^Va=jprAKdB9`M_x_{ZH8qn%{rT`Gz$GO44-gV(vKV(ja;a|Wx-Zqly5k4 zc{qwTFlIvgA__+8~z3x?e7j}Eyn{V`%B`KKIXf37IQ#9YubL7yP|GEEW{aF2;ai5$LdR` z8JLH~nDE#q|IV_W{JW-wi;f)H_c7|SC_MVep@W|+3m-Z3$Bz%~(cZS68g#VvB%Ap0 z?zY*mMwq|NpB!{FoA?PZ<~RA_n$S(3<>wj#289w514+T;P>9y2U^Gcj|cXRM*!<>;z&$91r)yy zj>JSeWkM$CBe88sOqP6ss8|DYekzxI2nLg6{zzoZgpB0lBzFi52GJNxaWMc6iZB$; z%t`xx-f&yX3Bykv&RA*$h3khtcskRD*^H#_7np$b?rQ)?Q!+qQkwntejG4ygF1V!% z?zan`;4XM#tl)vDI9c!{yWqhHkCiG+2BG>P)kynLKZT^agO!fr*{NiJ(-Y4~JPqjv zQMt2`CN)vkGua_6CTWOE5^?owmjO&ITxnyNp;(Jf zku=%r>}o+_s8Ui5r)*Ryu&iBB0&csyZ8Ow}Ylg$T)s;r^R1w2fy`{(OSDZ?jj&Pa= zbg&5{k;YY}N5!}FQAeRJ+%4j0Vy7&^l=;G8s-=t`pqj^c>B#sJjP$rOLV--H%FCd;PUkqK|deHq~H;yGz+Q zjPo`+G*q`z6CA@snarr+<@n!klW|w5J)B_%bWftuN`*kvu4rGBhKjag7447aCntym zyP^~A(JG)utE5jj8&Y&o-((XJh91nsc^2wFF(w zEWYn0d+m`(V>j6`6t#z}lazi$5L=(sj#CY(UjC?|(ZAyxK{N(k-B}6QbroNA{dITJ zfMwx1f{&2}`#O%d~u!W%;cHE72FQf{X=_hCWYVjSBdU zWfUOo3V$8!&F|a58PMVPcs8Q! zK^VFqm~kb)T)RwMs&6(fF)#LQvNrnjf|C>jSmn3F!+*Mm|EHe# zp8=D8s{IEb?{;aIoVd1DC27&|C@)wZE91kE0T@^2#SVTOK7$UH3*(?9S_XxcH$SoaVvl9-IM8b@~$eq=1h#Bk$;}ZSU-AtLf-mF>Pu?cGk32 zOCudDzjZ}hYfF7g=d5sU`-(8Zrsn$ga6@Z*xNybN`en=NmxX6UrcMu6wYE0T3S-i) z?N|{$>$rw#$7fH^%Bp4svlg#tSX^0EHf?Ht*V6i;?8W8nt!LNQb#~aqB4kyOH+f!H zU2}cw;=)z6E83dtI~LC_FP<-*n&SCYt;?4;*Do$Bm^(SQqD)$QBuYSfYZdh!^_?|y zYFm~;ExRJ9u=C1_qeQ9=Sp#!w+m{v9b~VTNi7YFltbRpVZAWEK7 zR9Qm{JCGngj_9(sDi`HPMG-+(Swa2MuH|K|EzuaF%E~LRZ&_BkC`u=qti;OI9aT+r z=R_ljC~LEzz9Ft8L{vs$$D>m5`dZd2St-B@;L>RD9fHnB+kwFGc(m1qfHlJ+58ydG zJV$>5o=?|afp;bz?R-N#kDg;R0GMmxgqZIfcw!}-UOfC1{att#w>~D%r@>6@S_HKV z4B5tFNiF85R9sw_0F}`_#=zw)@s|h(eg|h6u>KHF%GB@T!SMj|iEs~R!DAHoG1syG zaAJPKt>9Dze#!&E6BW3Zhk^wP+{Xt8=PIzBrv)n&*uh5z&r;xi9u6*1-~pb&lF3Mk zxZJ=3^>9ALV_eHax@N55!8V}lF<==NLcNs$q^q$G>Rq5fH`MD^U_I2kSb+^t?=l50 zf_hg1+(gngLcOgjW)sf{-k`w6JTtgWflK(L;Exp8%(H@bD{v{F9=uP1%lORTg9=>E zPYON^a3AG!11eY6DWV8?o(XjK*eO9U{Vhnh)7{3LYdGgJ-~*7=zV-)1#2FMY8LRh1xnl z3G@K#hD)Wr5Q4eVmqX+Tl5n~oJ5f+A-S=an=;N&uKxI}A#aShAHLDV?VCJ##Ebjj~q7!}v=O$p| zA)c6N+{FVm0431cK(7fbRRFyvuv`K3n!q^@ zjnMaL1)8Ama|)adeRnHx4)lFRfoACYx&kYB8Vi01BGOjY6HfRU@hr6HD30*Qw~@CmMG`ra|l zbs~N5GNW6CKh%v&Rrp_)ffb|7(AOI+p?e0Z?&xC(!gX8aD}?bJp-AISvu z&cA|So}dTJDaNx7pP9{!msEJVZoH|&$6G9seiEx?zG;*NP zh8e#@w%P>Fu1ug3ZIdOc15<$A1@|_@?uAbu8uc}_XuQBH5c7SEwTn8q9ikm>r`Qa) zOI!zcrMMIB1>ym?Ys7EhUMP0KT_^qomx(=axxn=lrioAC3SnTz)>MFw1- zm;%=lv*7wgA>0H}4mVNM!3~I&aFfKPaD(E9aFfM$xFPW?xGCagxP!!>;iifMa0iP7 zKdM-y!5u2b!yP83!A%pnaEFU|aMQ&SxFf_0xFf}RaE}t};f@lQ!#!GD4>v6CfIC_| z0C$Yo33se`8g7Po5$-tg7TodT1Gp2!A-I{sglUfvL*PzidOiwla-#!wG}pnI2eNop z^M%B3H7f()5fH`iG-jQuz*CxT6)5nuW?6Fpb`je%T7dZ1`Se7R zW4sQ>q?Vh4GgEOb59pd$!>zNF0L=4NEr84v^Ssrd0Oon?90f4XTjwf(dEV+$0Q0=H z8eljLN0{d=G-hJqM6a z`Cy*6UILIdz&vmLmjamQt#<(C6C37v>wSQ;2w?0;#oA^b6rxJai3tz98u%IW-mw>#nfDf>c|Q z#~rR_370|WC}gb7($nD?x^W#S=527!0TxZDyC7yDYdp|d7-+;|)SWC5fodMMj#V+= z;oVjiz;cSI;kP8sQZY+7<4LEgs9GN8NyRE|DevYYGMkF2igP9es)CQMwcD!1Wk#Q%meHakGT+aB@K4Ylq}G$1l8k4i)b9k-VIe z>9Fw#B^>%&KvKtsNBt9UE6x`6J1KS)-ommHh{M3h^q-~<3j1hqXlkucw!8w5fI09#dc!tk~vw#BY;Z#%Lhj5zkuwg6Vu7VgkT+;6oNAvP^prri* z&gI0w231noN>zbNO;X*Sc*h{XtE;JGj}CnSWLdUHi6OikbI|pO*Pjq+pMcON=cGZ) zNLafP*6tEEQ3`7#Vb2U53T`Rv87b^%fV5rIcZsx95q?Yx8x0C(K+<6&CHo`kgAtnu zH-I2Yd?Qv;epkz?nUE@3zWL;q9Y(f^j1b`heo zfiuWr*Ez>J;zpksw~j0}KG`-Gn*e@!+{3GPlHhYWNzSC|qYr7Wck%=_;&RgFU8i}< z#`LajjzQZfCe6oFAFu-?z;3{4vLva$wCPw)vDDwk!gd2rw;Qc&z$5IiY``OBm?fTs zR0fGWhV-FrIj??$G`)gP+y?ce-LK$e_=&#*mVNOGS&CzM1!WCR5^^lBq|6;OX(x>H zHl*oS@nL92zXxQKULzNIfhf`v0p7urwiwPoDUb{2V+xFiV?yTe9^^QP0_TA;iUL<7 zFcAUJU!$^KuUgObwz*_6?54Pvitz(J@m?g6#rT0N#w1+S!(}AI-3l#ULF6Y=-iUva zyiM*Z*ksF`K-rDQ!>j34ihi#w{=~gX?!C6$JggRGlH3P*LRvax&IbW{3g{v3QhMIi zN6({4=3{)~aY!Qde9V@2Ba)YsykA4l?;!F=Qr?JklJ~G%&xiZyIg?~QZ|k{Q$$j3I z`#!{AU}ySE(DOD#(lWyIHo8!7#t2qZ!^eF*B1HcC2;Q)5Y4V=El<$8c~; zL}QTkBRCI2C_lx22Rw@>{E|Fhj8kKY#Kgt;z&nUyfxi=bU>{s9@F`s3FzD9>ZBgiL1+dW%y{iB=`l0s}z(zmxp#s?GhxRIf zjeh8W0<_Uj5@?qx)ZXZY9eW=SukJ`e19~b0gIeli=y&ai(w?DOV1s=WRcY`cDRzLU zL(?d{5Y`9BF!geaS6@TqGdRgf`wQf(AvtI8DZs-|L|>M2&*CIvcq;l^E)Jr!#S*7a zhFaS!`!$eufcr-98KCI2-^oLOjilY{@IL@%QmTOVCZry9x(`Rmh#jR}#b!|MLo}%r zHdUht&eSP0K2*jPNig(eikNMOODOEn7r4Tw$HHvb!&rDcN@A+-B=WIB7BGSWl#(jP zioa8VmjeIH#T-R4BCBSh`t%ja-{er0&=AI3j+2?fpl`Dn1g^E zDcER~P`>Bm)sK<}?}@bYF^oJ$xxFW)r~L@{KVA4~;Fn@}crWlV<-m9DDCeluO!_?d6OtZ^|3J?TCio*5cxXW)WaxlyiC8B$hS)JIO3xZ zRhD+Up2%E$CN-0L`{COGK3VoPNX4s15Z|NvcwR0W)}#75l*DGplMGqJFv>`yb(XA} zQN~8FjOPpdIN?OhrIbLHhZd5&dNw7c6Ee+rGOq+P)vft1%e+a2Kh!htRN;SFnGdOOZy@t=6+RHk{GAFP9-R4KD*Q=W z=DP_082OkuxUwEXKtqPU6D9a3JBGCDyk{KPJ=rK z&&_z~vkO(DornPEIpPQrMG!}795X$D#qBt4ycj_TZDI_?iAiuZaU5KMaTWA=6wJ|c z;rVo<86gdyy3yc>l_~XXAxW5%33^nP+ioaa}%!dB+6u@lgzd!-Z zhW?ETU^euBR{_k1{%aJ#Z0P@?0+~{)q~lfE*(V%tDUS6gUw%&Q#ze7H}28M=3VtVdyjnqp%>-SWXp! zX_y?m_=L8LN!uDen3qdsYw#Bj6t&fzhR~jtiq{5Ip zl?wHC{FA_2NM4=9@}3LBgoNyyDf{jGZ?MU*Jz$ecY>!i7JMRK(+BGELe#vR;Zwt7b z1niFLzgs397Q&iDmSDF`JeXH(qvX4t88D1nQV>Hx))Ow6Y zMIKIPA@U|b!N!@g-M?Va#R6UcHS zzzG0axet(q$LgX0{trRM3+?a0!wc;@;L+y4`cEeu-`&5<{Ki6jC_=$6UTYgt8b{ni4w3D#n`iB>DzfVBZ`l65WIpmjIgWb2o3 zL)P;J39c29jZmPvcpi5dq;SRAf;10E>!X0Lv3OCIvgFDyI2z7kkg@AYq+L%S3*Z*2ha$W{oUQj}(O0K1+*i2~U51S%E4t|xGs0`qv7`EMp&egfwXkfSeOqjE8`2F_Lq=;dpx z0`&5=LjijEx<&zd`MOB~dii>V0`&6r8U^U(>kk#c%o?~=0h(F;GtmVS3gG+}Wb;`q zF=YN$3*7JUn0Q$d__+djSrYiA0(e;x*a>hU(wg+LB#Er*-%Rr8W$$ZgQSOpSC6!Fbs~;xnFJe>_4%FWg^>;CR-8 zsUH&c9Z7X*^$}4&aO*jY3iOGaGJ`0?HCL)iq8#IuoX;7e`Pv;v&w|)6<1-_$Yji8gV@^^l^o zkH8FppWrVD9l;=TTyMj@w8glWMo)fe?!4KHmo~L5jzktWwbV6tEvsjd$dt66zqY zrf$VFMAJe*$JHE--hPW4IX4m$HW^8`8Hu;QW+ZJg0wVQMBS{2aF?g1=mt-c>iv3^? zCWycWLu464MPjd!HpLi`Z6uZ&Vmi?} z;8jIAgQ%=IXBbA?bq%O}dOG4d(N*j0p%ynEHF_s6uH;-7#?S=uqeOJd=R0Vm6b}YU zj8n3V;D6}4!Nr@M5xav>q8v(eyG%4PLEMzM%{a*{F^)FHlf1+jZkCwKjgn2&%SM$M z6&PbnaA9l|I2z2!hEEhn44nS$Xy1VFE{w_y;ao8pCb*!;!|sc3^taYOP3vd*5iM+yBPxr7R(i)Y$h=9ii8 z)yPON|1SRqSR(>9jC2pIRGKTbufPRCGNyPvA!0BYq>`>E}VWpW{Yy0XC(&#fUD!%cB%QiE}pxy=|xS)-soUb#Wp zHP%q>c5?oKlk=Wf&M(WHf1+}}p}(9TadN(qa{j5x`9C~4U$1heo*=8@ErId=mNjV4 z7|s47Wb?jh%{Ev_d>~Z4xmr@Mum>qJLbQ2(4kESu_waT`XKVT$xxU&O&@-ZZ>2TuV z|FRLtS+RIhsa(E%2>pVk3N0|=7mae=VXsfzgBd0#4DesRT(Q?B?v$>DOdqndQUcvx zhRVu)3Nv1E!fl2qGm?*$oqNZH4Mu_~UPOez;Fe&P8fobM{)qU=g(b3EJf&gn@$g!? z_IQKR-)f3mkqYhh9geoO4a*Wp_-j-`qmpogSHfT{oUp!0U1tp5WTYNz_-;j3)b;+z z8gR*bJC;T<%`Ubz`wR&@nyvTBg(?^0`gn4-EBXGlvS8l>Jw7?L5RQPg3CR*o@dpNbcAwv1 zj5N{pHqeq@++-*E8(?agDZURFG{uMLn3Ob#rT$15F)1RhUV~~g#a4YA=spu8-Uk<0 zVItk2Z;+%iBXyH0{(OOyaI3B)lqv~-x}bz)|4Z9o%!fjiB#-8}#0Im(I=w_(f<}Pd z#zV+E5t==_<`t6uFyIhO6TgLKAyYgKNL!H*G^1%UIU=5bW|%;J0?kUSaEW;N0%^35 zog!Ui7pXg{P_#&&Lm`;uLBr~#A^y4Zkugw2hLG122KVOm`1u=1i~R=G0BKRGw7Bg2 zZN^wkW0*N+nlMaCgZN38TwT8kQYgz1#YSj~AtoCJ0$&xH;;t@f_uo5axZgIz3w_P- zQ#2o}{4c>7?>uTL5#x*!7<#S@CYi;iIK0|1lof`e7rX+Kp|_YK^%Yb6%C5F2^%AGz zqt*7<1>0=%M$AXSJq>FJoA<%KywAd-sIzBvjPDy^*$cK|UqK!GlC`f;z!+kRA;_)Q zF2z-%kAeSzQlJrkq;cdn#5@iS7Cj9nzo;+oC#d)gQ#@mAqunZ1WdPOk_yO5oZb3Uo zvcL4HLoI>UOim}HSM6duOvbS_YA^cTbFyEc3aACC!HO zBGf7OAez=1tI@_KNHo!-u3L?D+dfiBV0Dr5nkhbA)nEiLDEUf^TqFsgnZZSmNC(2m zb9fa}d}_k@8&@}A6(ZiWB3L4!Ig}WKN{nMnk=|fTgUO0a@pFuJQxUZo9`^PP28tAd z{;5AOL=(o0RVA7w4W?%Bhb?ZTB#2ZTAC!p`UqQjs z6nTwH_z_NK{Py@b$ef3oi--qTK59%ruk>RNjmDN_W}_Bpy?^)0660hHtKg^Y!9)zk zh@-(-CB1$nwSUoz1zd_uawU?0@R^eZ6BmJFhAB2-AUY8oD8@9bPG=%GnkGmbJb28t zErA>1gz3g{x5^o2YzdNQtF*@>h()w9t^^{o#)qTiq`KGKDneFnPmM0HfBa!Cvd(k}uXVC-=M7^Q^DF%Bt+& zS{{D+!tHBnqA4SOxtNi2NxC1E9J~n=J*Z*S8*~sEg^)z}lEgry%YFL6urlnRy zJdWiMM(#T`Z7SXon&S1fy%>Y;)E-3>d>soJ3RBHaF^11WVgdLMEbB&L{fTi?uIel7u|F#3 zHk}5SjYwu7880)8Ne+i=H2H;Yk0xix(WI^4(d6G68U{Rw`kEuczq@n%nkQ=mZ#4Va zWMW6zXNd9cJDu}3nAvs4W~kjFy@I5ZG_-%=-*!n;QyFH1*knO3BgOlOJ}w~(-FX8Y zsia4YY1@p6*wU2HN%WmY=#N;m`6ID}b~V#GO7VN=v@^Cc*e)~MKP79*3|C&Rmhs1E z5+jM4FTJ!xujSZ$VkE-|WxF(W+3!BEgp^AKdY38g2V=i$h)3fhRGa@5sJ?{hSkqy} zj(IzDtP#vM{EfyS>@gGPnc|MMkwmCs0x{CXqJJP$v z+l9zPq|{ASN@bnb28?-Uz?csZBWUI4Rq&I6B5uHV(Qk@-28wuOpopjABk0?u$jJ1@ zm)`Fh>|AzI`pvkfBHxB;EbHbfgipuASvg$!QK^V60}rv0FK-pGO34Gq&4xFul8yW^ zc>JuLg}S?Yc>2xsQhOF2F7NRoV5g+FO*HJ%TXl`Cokac&8P2+yd`hB&zE!8tDR>ej zT03d1zEvljgJ+P7w$(yhXsb^2B0OpCI300;t>l7(Vk_{Bba5cYlJQY4Fvt&;leD#f z=*73iwg%SfB#jW66~C?EP(X)#qS5>6!5%td2XEEw7|K09l^u&x=wriMb+QL;N|9}J z@~JXH2yfN54hBcsRz2(HtpTEj@eGcOkuoy2>J&p2mo8(Gic*K{)QX4V$rj^XJm7#W zq;|*$Xe1E%kO@}#QMFHmsd%W7kWY1I*PE(EcfCLN?x!iMY&u{xx#Vb^1PZ4iwvKlERRcFb3;!@l)KHHsSw1JoPEjKT9o^+xez zi*HhI^!f-%Wt&33SB*$#`*``y6hR-VU}7Xr;xJenJ3f8DXwUdTDao|djM4LHTnNix z+$fXdC=!uT3&>l9hZI>5my=?oHU)9yDaMLEjH>%llcdf@O`ONR+lC58@+iuYhmicZ zqa2<3i~0hA14L2zX&~gX&f@Z#1THf@GA4dq8T3|+WKsu%9^_GB`s(3f1dsf<^`HSAGRQj}PhS}_1)Ov|um^dHF}|q~ zBgNWP+sANOh_Y?Em^kU9^q?q`$@|W6v8W14K_7Rj0rH}1rMbpa>y(-XLOZWM8dCJP zQMEMI!KCDk$1|`=9Y*lTk17~}{|S0fKi!B2R=fh~ov{7RV&xd!N9sEyt@7TeE z@a(J>QdZq~zzB@txIUckScN85%6Ly(04bV0vh2)=u#gAet4NY6e99)|lWZuXM0n}(nkAxHXOEQ_~u#s_No=}t` zW3Z$rDj5dLK8F`6Xk%c}f}dhYyP<5YO`~P8D_&`i4kvpNSfld~QDbDn!AcbDR!`bO?!n1WdJ(hsxNBL1% z(2U)V$J5Oy_t!zT1)N0!{)UIMvt(*GO2F+P+d~tz9QjdEcOvR%G8MJtXcPrd-nfoJ z^FTjsa~9ZI@F3rn@NBL)6+o5i0V;>53=}m7&r%r`XDW(%9M9)6Y7u$(_DKkcs{&Bv zhm=JVBabS-ZxfSTaO_^WfIP(*zmTG1Q(`6u!zXzmJS4F%Bjrc2{S*sSxz~YKqY$JK zX<)5}A{LpErx@ciqZ2tqvM?S74Ys!_|UZ#+{1zot*Sf^IyQ;n{}gYbO7H#(q6!b(-08@LY%I%M_tXGX^Whlkh0I@@XYWx=~r$`-xY4-DKu zko@M>x^q}pOGnf47JLCT{?>$b!I?I-rjzB)$t!QK@2IP)UD{k9YC4Kf^j-*jT;yhEq&X=m>5o$vc~^L@W%zR#D-_w$|d?yr*X^9}NzFOEku zi~GL#Z)uA0+$d1D$_KiEusCLO`cpT&mbd@B<=eNtGXcp_VP0!XS#8HTH0>0(bk-DC zENEe|I*yjRb2`SC;Iz{U zW;-m43hY2tZdrL@g&nS_%C`eWrMZ=J92)wCOj~$*RdH4mmFjRmL0g469_L0N8g zO@3o-OG|yTlg3SQ(z;2G3^%DCS;zGiKcg?{c$<`4ke^-SWVNs|uVzkpO@8jY?3$|6 z?RcVI6 z?O$Q36BYWUE3^OpMG=(~k6cd{V;zM>8yvNS%4WrtSgR>G6|sQDxp^_#bXM%*IF8L* zRBW#UvGkPT%KFao_NLbMrq0!~+grQZnp&2#I6^$C!XgawQq4*^K{_&K_aj3JFKns9 zf<}phr26_5ZPEN#W!KVy;_|Q_aK}sBS7j!L33BX3WSdhIRl33!ISN)RP=tb}4nD4v*<(2Z?3Oxr=heDZ*E;D z{zzV~>jtbhnt;8Lu@o^|MEP9Sh|=r@n*2E#rK+1md{~%O73a@Y>$hl9m(`+eslUjW zPSpBhrH$k5hwQbEo8M)hsCsJg=+rQ=t2k7I`mG!{nP#}9_;LZJ8q#v=6O6`_j(J@x zma^D_^nb*P!v2f2Z+|&aYcE=hYU^l?Og$zx_I~LwS!=JM-HFM@U%pYHfz00P7GUd` zS6avl=SdIS#^@*=H}ro?8{+cQDx#ZX+M~`bJOdk<@+#?LqwCtnviTTYPpiP7YA;yS z1|zE9H{Tl3cA%iXxpp;Mu}pe4Alp$CdKWsP+#VIR*Pp9)2py|C>S#CBKs0+-Odd8> zXvDOak-g0kFbC~dCRRcVutda;TlN8Lfx0?Wc@zWoC$8>8Vx$HhT3P;l55*pB@+&K& z**k-cn?wT!G|G-s$stbl68mZb&i<64y@#iNOT8;AqupB$_Vhzva$Dx?!JOSW(ErfQ zVcU7_s8;m9mgP7bC~B@;5nNa$9t;kYlMl-Q;T!JqY|Hn z6>&YqN5$E{z86O-#ki{oc2`v8il!pNct<0drHqITNcM3L#jJ{*`hB%t2z>fz;Eg?@@m(qcp99&R9+cvGs z+P|&ysDJAm1%Dp#?XP&c&Z9*hZAS)JYS;{NQi^sg*?p<1<#M>FqHqCrvvbarZN^RN zhc>qlTR~w_?!r>L%abmYm1<6Q=3j9%`2S}?)VKOMzxn@XK`l%k6=80sJu?C4T;_k( zSx|pVbWA>9VU_+Z?HA->)E6B)6&GOXQ^*SPW|!qJjGLLhV7V>d1EAx}ZeG^W7G-r! zT_C%VI&GJ;O?@Yg8uol;hvjHSaoA?75yc%JU17&I8kJogZPV&@bJNJPrz-* zPsT|IO!1ptnDnDxyjAgzzF`|{_P2c60GEFD9-)gw>O=Ts1>h4dvvHUN(r=-D#}iLq zB&R;ae+j_b$e;A&`1b&O`Xv^+heh~L@YRR-_bULOuy7P6GT{VZ$O_x?g!~dVm4xCa zd-(H#Gr^C<_!CUQitTs{bp*s2{gf*_i!MK90f9=9les6mE^~oL=zc=2CKLkH)ChNC9{k{D3C&XXGKgx+GfnP=5@!+TO zk-@$SKVA6yBK{`Cs}I?q>WBJ6mLtE-6Mq3PT^b8I@mqi&ciI=}a|iGfuK34Y=|kGy zBUQM_PHllcgzJFuk@c0Lakj#am*pdT0q{SZ@(u#t;K6i1ey=Nj8}I=a{)Gqs+Jj&4 z;6DL>=1Tve2Y(8TuZNX?{WJ#pB%q;$$q?3O>=gC~0Mi5^_)!St(iZh37UyxxPa@!-2W7~kd)C+|5Ae#3)5@L;;W z>eeUOgOB!L`XwGWf362F^x#Gh?)KoV9{eK@e$az|?ZGd5@E#BT$b*CUl@Pc7nI23( zg6fX11RjU>Iv$;l`ctigsS^_J0LB<5=L^F0YcXfI@U_6zz?tBu_I@+)CSbaaPW zZr9@S9L*Pmp8&oCIN!ho^=KHO{OCvL za)B2)@{=)A+yESQ@FZaTrlkCJBFaApxQi|^q8|{hAdJh6lN|nf;9G%7KE%zAK-|xaZfFA-zc6RxJ9|10R@Uy^vXS}5R-ULnpzTJtZABno(89yoh zAn;BXzaL*H_@pa73{2|{vNw&F5#SzRK#Z@P(J$SF0x^6VFg{@n^WfLmQefyBCVdIh z@9`pW6t4viN3mcVfira?5kCgQjAfDb`r2mPIjdzEoq+0#{P1lsEnRiZ^{qI7h`-q3 ze8sSb47NyVO<8?APHSg3w=S)1?(n2@4jB6gLrPlw(itjN-0oBVNI!3824JS6nJl2m770TzLu_H5!`s` zq}!QwxHdIqMN?gSD+QV$b_$ND+E+(9SOj0?9m#KPZmzHE%)lqh`LJbm1IK3fn{4Xw z*;8=}+1%7wQxg?4fc)wGN0dV|Or|<}Yi;h6=%^NKH*g zU2P}M=9*h;m*GgiZFL`F3*8R%a-Lt`-b(UVURP7|vfR$j_NJv>o%J0V8C7#|38XT2 z<@Aj5jDn208F?9%8Rdl;xwA^APF+|!J$qs4)a;DYXO(7WXH#f621a8Jv) z5YIx~gIhEsI5;TGD9p>9nNfxV;Q6?Kk%42dnHl9c_MMqg zn44F;k}6~os(&;87I!^_)1Gb4f?h-1ms& zp;Lbm^5mtz2zfoGzX*Ax)nA0X6x3gYyq?rwguJArBIvkGUczw`qBnEmi0-3oN)}fL zBE%^lJ%@A4aPBZEQuN-JBDiiH#gpW%Bo|5DK#C{Dah2Ow&J+g?sIE7IaW{^lJobe& z1=Zyy7mh1)>@J()kSA=;)vP#LeCCqPbxkXd)xOK+BE{$FT-#o2xyQKn&Z!u*I*Mw$nmeOCPhBsJ668^k zd?!w!=)`27CeiI`&&wItS(y8bteA#Kl0bpzp_cooZQ$!O{aut&GjD%yq2pgyt9vKv zEqQJGviKtRy%OubLhIbFR0eh5gvIKBN)j~{FlK34Qr8VmmC_zy<7z<8GVzrn=a_g> zbcTulvxW-1LTYWnG(&epGBWaNJL)lt#?eZfR>11+*C(-8Ew-}JoBj?Lnh1^7O&Fy+ z+Gt%yYdKnPIsOdYwJ207Qi}ba@jQCX1tR-*||}iHLW7AW}dz1 zv?aH8b+&c&7r(flB(PKUBeTCW9kTpXnv-$#yP~#zbqD6`j!CxL+1iO^?4+)i zu8#U;lUCNZ)MM$AK=0%F{Q66t{p&BVaU%kc4^aBN#hh}Ma_8~UtS)?T=#V(H+&7|l?`pwu+V~_OmPcZM;LvA zOx7_g`X%JCy-TZS58~`yJmiQ6+dtFdCDQdXFU{FhxXD=Ow%Wh$0;+ct(TaZj4f=4O zKjJ~LaJ=r>i$M4{kX_rc!WkZ>)9`>ABnmol{nD=Gk(DkR@ZUzl#bSh&W5&dcye)MN<~%Sl$yT%cGB1TCc!xNx%ExWu^&#VDJUwRGBq+i zlC6GL3DY6{7!%Z?+hO%EzP!dgzbur*(3;w%OYt^Cp@#O_75EJ&JBo$} z6~<^9rQ;%+BDd2mCG73&1Q=LZ1fybS?Na!i?b2^)#oydS>g>h}a-$Q$XfHq09^Yx- zZ-p`V&%}WMj!L@aL4Y0ir7p>#2S-fwkzX@}vxDe|<`w$TpCuC?cK`GV&jAmeKd6t& zllUl1e25R94!$%c#EF9^--(bs^&!40O6+`8+-P7Grl_Cdueh60NcB;15fD_E;?nTv z<$s_9Cg-E#(BI?u_7vbJs7ETUSX_~Z@AX>7UQX%DN8hrbWK-;RyquBW*5a_LxEc@N z_s_!_H~k5`^HF?f1G|pT*~aVeTZK4(p(;5E51EqePG5cC)CEE~lDUPkYrv;IO5XV( txXi^KzK6c3EcQ`V(+!eJL$NP`F9+wW>Z9T=1;L%~;O)p)F~L`S{|8jCPxSx* literal 0 HcmV?d00001 diff --git a/test/firmware/armcc/nucleo_f103rb_tim_timebase.ac6.axf b/test/firmware/armcc/nucleo_f103rb_tim_timebase.ac6.axf new file mode 100755 index 0000000000000000000000000000000000000000..ab0b335449a31dd4578629b4710c1beff01d11f5 GIT binary patch literal 143500 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 + +#include "arch/arm/cortex_m3/cortex_m3.hpp" +#include "chips/stm32f1/stm32f103_soc.hpp" + +#include +#include +#include + +using namespace micro_forge; +using namespace micro_forge::chips::stm32f1; + +namespace { + +std::vector read_file(const char* path) { + std::ifstream f(path, std::ios::binary); + if (!f) { + return {}; + } + return {std::istreambuf_iterator(f), {}}; +} + +// Boot a firmware to its steady loop; return "" on a clean run (no fault), +// otherwise a diagnostic string. Using a plain return value (not ASSERT_*) +// keeps the assertions inside the test body where gtest can short-circuit. +std::string boot_clean_or_diag(const char* path, size_t steps) { + auto data = read_file(path); + if (data.empty()) { + return std::string("firmware fixture missing: ") + path; + } + auto soc = Stm32f103Soc::create(); + if (!soc.has_value()) { + return "Stm32f103Soc::create() failed"; + } + auto r = (*soc)->load_elf(data); + if (!r.has_value()) { + return "ELF load failed"; + } + (*soc)->run(steps); + + auto state = (*soc)->machine().cpu->state(); + if (!state.has_value()) { + return "cpu->state() failed"; + } + if (*state == cpu::CPU::State::Faulted) { + char buf[64]; + auto cm3 = (*soc)->cortex_m3_cpu(); + auto pc = cm3->pc(); + std::snprintf(buf, sizeof(buf), "faulted at PC=0x%08lx", + static_cast(pc.has_value() ? *pc : 0xDEAD)); + return buf; + } + return {}; +} + +} // namespace + +// 2,000,000 steps mirrors the budget used to reach the main loop of the Keil +// F103 firmware (see document/notes/007). The gate is "boots clean, no fault". + +TEST(FirmwareArmcc, TimTimeBaseBootsClean) { + EXPECT_EQ(boot_clean_or_diag( + ARMCC_FW_DIR "/nucleo_f103rb_tim_timebase.ac6.axf", 2'000'000), + "") + << "TIM_TimeBase (armcc/AC6) failed to boot clean"; +} + +TEST(FirmwareArmcc, UartPrintfBootsClean) { + EXPECT_EQ(boot_clean_or_diag( + ARMCC_FW_DIR "/nucleo_f103rb_uart_printf.ac6.axf", 2'000'000), + "") + << "UART_Printf (armcc/AC6) failed to boot clean"; +} + +TEST(FirmwareArmcc, GpioIoToggleBootsClean) { + EXPECT_EQ(boot_clean_or_diag( + ARMCC_FW_DIR "/nucleo_f103rb_gpio_iotoggle.ac6.axf", 2'000'000), + "") + << "GPIO_IOToggle (armcc/AC6) failed to boot clean"; +} From 80ccdd465c6579b682692defdde182cf3544f890 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 21 Jun 2026 19:18:56 +0800 Subject: [PATCH 2/5] refactor(cortex-m3): split thumb32 decode into dataproc/loadstore families (T0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cortex_m3_thumb32.cpp exceeded the DIRECTIVES 700-line cap (809 lines) and the instruction-coverage work would push it further. Extract the eight largest execute_32bit blocks into two family files: cortex_m3_thumb32_dataproc.cpp — addsub-plain-imm, dataproc-imm, dataproc-reg (shifted), shift-reg cortex_m3_thumb32_loadstore.cpp — load/store-single, tbb/tbh, strd/ldrd, stm/ldm execute_32bit is now a thin dispatcher (masks unchanged, same order); the rr/wr/br/bw operand lambdas are promoted to private members so the split-out handlers reuse them. Pure move — 247/247 tests green, behavior unchanged. thumb32.cpp 809->376; both new files <270. --- CMakeLists.txt | 2 + include/arch/arm/cortex_m3/cortex_m3.hpp | 17 + src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp | 647 +++--------------- .../cortex_m3/cortex_m3_thumb32_dataproc.cpp | 245 +++++++ .../cortex_m3/cortex_m3_thumb32_loadstore.cpp | 267 ++++++++ 5 files changed, 638 insertions(+), 540 deletions(-) create mode 100644 src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp create mode 100644 src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 08f1729..dfcb81c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,8 @@ set(MICRO_FORGE_SOURCES src/arch/arm/cortex_m3/cortex_m3.cpp src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp + src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp + src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp src/arch/arm/cortex_m3/cortex_m3_interrupt.cpp src/arch/arm/cortex_m3/cortex_m3_reset.cpp src/arch/toy/cpu.cpp diff --git a/include/arch/arm/cortex_m3/cortex_m3.hpp b/include/arch/arm/cortex_m3/cortex_m3.hpp index 9cbf082..9f0b4e7 100644 --- a/include/arch/arm/cortex_m3/cortex_m3.hpp +++ b/include/arch/arm/cortex_m3/cortex_m3.hpp @@ -59,6 +59,23 @@ class CortexM3CPU : public CPU { Expected fetch16(addr_t addr); CPUExpected execute_16bit(uint16_t insn); CPUExpected execute_32bit(uint16_t hw1, uint16_t hw2); + // 32-bit Thumb-2 family handlers — split out of execute_32bit so no single + // translation unit exceeds the DIRECTIVES 700-line cap. Each returns the + // result of its (already mask-matched) block; execute_32bit dispatches. + CPUExpected t32_addsub_plain_imm(uint16_t hw1, uint16_t hw2); + CPUExpected t32_dataproc_imm(uint16_t hw1, uint16_t hw2); + CPUExpected t32_dataproc_reg(uint16_t hw1, uint16_t hw2); + CPUExpected t32_shift_reg(uint16_t hw1, uint16_t hw2); + CPUExpected t32_loadstore_single(uint16_t hw1, uint16_t hw2); + CPUExpected t32_tbb_tbh(uint16_t hw1, uint16_t hw2); + CPUExpected t32_strd_ldrd(uint16_t hw1, uint16_t hw2); + CPUExpected t32_stm_ldm(uint16_t hw1, uint16_t hw2); + // Operand helpers shared across the 32-bit handlers (promoted from the + // execute_32bit-local lambdas so the split-out handlers can use them). + data_t rr(uint8_t idx); + CPUExpected wr(uint8_t idx, data_t val); + CPUExpected br(addr_t addr, Width w); + CPUExpected bw(addr_t addr, data_t val, Width w); CPU::CPUExpected read_pc_raw() const; CPU::CPUExpected write_reg(uint8_t index, data_t value); diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp index 720e64e..63dd265 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp @@ -8,43 +8,112 @@ namespace micro_forge::cpu::arm::cortex_m3 { using namespace thumb; +// ── Operand helpers shared across the 32-bit Thumb-2 handlers ── +// Promoted from execute_32bit-local lambdas so the family handlers split into +// cortex_m3_thumb32_{loadstore,dataproc}.cpp can use them. Bodies unchanged. +data_t CortexM3CPU::rr(uint8_t idx) { + return regs_.read(idx).value_or(0); +} +CPU::CPUExpected CortexM3CPU::wr(uint8_t idx, data_t val) { + auto res = write_reg(idx, val); + if (!res) { + return std::unexpected{res.error()}; + } + return {}; +} +CPU::CPUExpected CortexM3CPU::br(addr_t addr, Width w) { + if (!bus_) { + record_bus_fault(BusError::InvalidDevice, addr, w); + return std::unexpected{CPUError::DataAccessFault}; + } + auto v = bus_->read(addr, w); + if (!v) { + record_bus_fault(v.error(), addr, w); + return std::unexpected{CPUError::DataAccessFault}; + } + return *v; +} +CPU::CPUExpected CortexM3CPU::bw(addr_t addr, data_t val, Width w) { + if (!bus_) { + record_bus_fault(BusError::InvalidDevice, addr, w); + return std::unexpected{CPUError::DataAccessFault}; + } + auto v = bus_->write(addr, val, w); + if (!v) { + record_bus_fault(v.error(), addr, w); + return std::unexpected{CPUError::DataAccessFault}; + } + return {}; +} + // ── 32-bit Thumb-2 decode ── +// +// This is the dispatcher: each mask is checked in the same order as before the +// split; the large data-processing and load/store families delegate to the +// t32_* handlers in cortex_m3_thumb32_{dataproc,loadstore}.cpp. Branches, +// MOVW/MOVT, MSR/MRS, bitfield, multiply/divide stay inline. CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { - auto rr = [&](uint8_t idx) -> data_t { - return regs_.read(idx).value_or(0); - }; - auto wr = [&](uint8_t idx, data_t val) -> CPUExpected { - auto res = write_reg(idx, val); - if (!res) { - return std::unexpected{res.error()}; - } - return {}; - }; - auto br = [&](addr_t addr, Width w) -> CPUExpected { - if (!bus_) { - record_bus_fault(BusError::InvalidDevice, addr, w); - return std::unexpected{CPUError::DataAccessFault}; - } - auto v = bus_->read(addr, w); - if (!v) { - record_bus_fault(v.error(), addr, w); - return std::unexpected{CPUError::DataAccessFault}; + auto read_special = [&]() -> data_t { + uint8_t sysm = hw2 & 0xFFu; + switch (sysm) { + case 0x00: + return xpsr_ & (PSR_N | PSR_Z | PSR_C | PSR_V); + case 0x08: + return msp_; + case 0x09: + return psp_; + case 0x10: + return primask_; + case 0x11: + return basepri_; + case 0x13: + return faultmask_; + case 0x14: + return control_; + default: + return 0; } - return *v; }; - auto bw = [&](addr_t addr, data_t val, Width w) -> CPUExpected { - if (!bus_) { - record_bus_fault(BusError::InvalidDevice, addr, w); - return std::unexpected{CPUError::DataAccessFault}; - } - auto v = bus_->write(addr, val, w); - if (!v) { - record_bus_fault(v.error(), addr, w); - return std::unexpected{CPUError::DataAccessFault}; + + auto write_special = [&](data_t value) -> CPUExpected { + uint8_t sysm = hw2 & 0xFFu; + switch (sysm) { + case 0x00: + xpsr_ = (xpsr_ & ~(PSR_N | PSR_Z | PSR_C | PSR_V)) | + (value & (PSR_N | PSR_Z | PSR_C | PSR_V)) | PSR_T; + return {}; + case 0x08: + msp_ = value & ~0x3u; + if (in_handler_mode_ || !(control_ & 0x2u)) { + return write_reg(13, msp_); + } + return {}; + case 0x09: + psp_ = value & ~0x3u; + if (!in_handler_mode_ && (control_ & 0x2u)) { + return write_reg(13, psp_); + } + return {}; + case 0x10: + primask_ = value & 1u; + return {}; + case 0x11: + basepri_ = value & 0xFFu; + return {}; + case 0x13: + faultmask_ = value & 1u; + return {}; + case 0x14: { + control_ = value & 0x3u; + data_t active_sp = + (!in_handler_mode_ && (control_ & 0x2u)) ? psp_ : msp_; + return write_reg(13, active_sp); + } + default: + return std::unexpected{CPUError::IllegalInstruction}; } - return {}; }; // ── BL / BLX ── @@ -156,67 +225,6 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { return {}; } - auto read_special = [&]() -> data_t { - uint8_t sysm = hw2 & 0xFFu; - switch (sysm) { - case 0x00: - return xpsr_ & (PSR_N | PSR_Z | PSR_C | PSR_V); - case 0x08: - return msp_; - case 0x09: - return psp_; - case 0x10: - return primask_; - case 0x11: - return basepri_; - case 0x13: - return faultmask_; - case 0x14: - return control_; - default: - return 0; - } - }; - - auto write_special = [&](data_t value) -> CPUExpected { - uint8_t sysm = hw2 & 0xFFu; - switch (sysm) { - case 0x00: - xpsr_ = (xpsr_ & ~(PSR_N | PSR_Z | PSR_C | PSR_V)) | - (value & (PSR_N | PSR_Z | PSR_C | PSR_V)) | PSR_T; - return {}; - case 0x08: - msp_ = value & ~0x3u; - if (in_handler_mode_ || !(control_ & 0x2u)) { - return write_reg(13, msp_); - } - return {}; - case 0x09: - psp_ = value & ~0x3u; - if (!in_handler_mode_ && (control_ & 0x2u)) { - return write_reg(13, psp_); - } - return {}; - case 0x10: - primask_ = value & 1u; - return {}; - case 0x11: - basepri_ = value & 0xFFu; - return {}; - case 0x13: - faultmask_ = value & 1u; - return {}; - case 0x14: { - control_ = value & 0x3u; - data_t active_sp = - (!in_handler_mode_ && (control_ & 0x2u)) ? psp_ : msp_; - return write_reg(13, active_sp); - } - default: - return std::unexpected{CPUError::IllegalInstruction}; - } - }; - // ── MRS ── if ((hw1 & 0xFFF0) == 0xF3E0 && (hw2 & 0xF000) == 0x8000) { return wr(thumb32::hw2_rd4(hw2), read_special()); @@ -263,214 +271,20 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { } // ── Add/subtract (plain imm12): insn[25]=1 (hw1[9]) ── - // addw/subw (S=0) and adds.w/subs.w (S=1). imm12 = i:imm3:imm8 packed - // PLAIN (not Thumb2ExpandImm). op = hw1[8:5]: 0000=ADD, 0101=SUB; S=hw1[4]. - // Modified-immediate (insn[25]=0, e.g. cmp.w #0x110000) keeps its own form. if ((hw1 & 0xF800) == 0xF000 && (hw1 & 0x0200) != 0 && (hw2 & 0x8000) == 0) { - uint8_t op = (hw1 >> 5) & 0xF; - bool s_bit = (hw1 >> 4) & 1; - uint8_t rn = hw1 & 0xF; - uint8_t rd = (hw2 >> 12) & 0xF; - uint32_t imm12 = (((hw1 >> 10) & 0x1u) << 11) | - (((hw2 >> 12) & 0x7u) << 8) | (hw2 & 0xFFu); - uint32_t a = rr(rn); - uint32_t result; - bool is_sub; - switch (op) { - case 0x0: - is_sub = false; - result = a + imm12; - break; // ADD.W/addw - case 0x5: - is_sub = true; - result = a - imm12; - break; // SUB.W/subw - default: - return std::unexpected{CPUError::IllegalInstruction}; - } - if (s_bit) { - update_flags(is_sub ? FlagPostOperation::Sub - : FlagPostOperation::Add, - a, imm12, result); - } - return wr(rd, result); + return t32_addsub_plain_imm(hw1, hw2); } // ── Data processing (modified immediate): insn[25]=0 ── if ((hw1 & 0xF800) == 0xF000 && (hw1 & 0x0200) == 0 && (hw2 & 0x8000) == 0) { - uint8_t op2 = (hw1 >> 5) & 0xF; - bool s_bit = (hw1 >> 4) & 1; - uint8_t rn = thumb32::dp_rn(hw1); - uint8_t rd = thumb32::dp_rd(hw2); - uint32_t imm32 = - thumb32::expand_imm12((hw1 >> 10) & 1, (hw2 >> 12) & 7, hw2 & 0xFF); - uint32_t rn_val = rr(rn); - uint32_t result; - - switch (op2) { - case 0: - result = rn_val & imm32; - break; // AND - case 1: - result = rn_val & ~imm32; - break; // BIC - case 2: - result = (rn == 15) ? imm32 : (rn_val | imm32); - break; // ORR/MOV - case 4: - result = rn_val ^ imm32; - break; // EOR - case 8: - result = rn_val + imm32; - break; // ADD - case 10: - result = rn_val + imm32 + ((xpsr_ & PSR_C) ? 1u : 0u); - break; // ADC - case 11: { - uint32_t borrow = (xpsr_ & PSR_C) ? 0u : 1u; - result = rn_val - imm32 - borrow; - break; // SBC - } - case 13: - result = rn_val - imm32; - break; // SUB - case 14: - result = imm32 - rn_val; - break; // RSB - default: - return std::unexpected{CPUError::IllegalInstruction}; - } - - if (s_bit) { - if (op2 == 8 || op2 == 10 || op2 == 13 || op2 == 14 || op2 == 11) { - uint32_t flag_rhs = - op2 == 10 ? imm32 + ((xpsr_ & PSR_C) ? 1u : 0u) - : op2 == 11 ? imm32 + ((xpsr_ & PSR_C) ? 0u : 1u) - : imm32; - update_flags(op2 <= 10 ? FlagPostOperation::Add - : FlagPostOperation::Sub, - rn_val, flag_rhs, result); - } else { - update_nz(result); - } - } - // CMP/CMN/TST/TEQ: S=1, Rd=15 → flags only, no register write - if (s_bit && rd == 15) { - return {}; - } - return wr(rd, result); + return t32_dataproc_imm(hw1, hw2); } // ── Load/Store single (immediate): str/ldr/strb/ldrb/strh/ldrh .W ── - // hw1[7] selects the immediate form: - // 1 → imm12 offset (T2/T3): addr = Rn + imm12, no writeback. - // 0 → imm8 with addressing modes, op = hw2[11:8]: - // 0=offset+, C=offset-, B=post+, 9=post-, F=pre+, D=pre-. if ((hw1 & 0xFF00) == 0xF800) { - uint8_t rn = hw1 & 0xF; - bool load = (hw1 >> 4) & 1; - uint8_t size = (hw1 >> 5) & 0x3; - uint8_t rt = (hw2 >> 12) & 0xF; - uint32_t rn_val = rr(rn); - Width width; - switch (size) { - case 0: - width = Width::Byte; - break; - case 1: - width = Width::HalfWord; - break; - case 2: - width = Width::Word; - break; - default: - return std::unexpected{CPUError::IllegalInstruction}; - } - - // ── LDR.W (literal): Rn == PC ── - // `ldr.w Rt, [pc, #imm12]` — PC-relative literal pool load (compiled - // `LDR Rd, =const`). addr = Align(PC+4, 4) + imm12, no writeback. - // Store-to-PC-relative is UNDEFINED → rejected for !load. - if (rn == 15) { - if (!load) { - return std::unexpected{CPUError::IllegalInstruction}; - } - uint32_t imm12 = hw2 & 0xFFFu; - auto pc_res = read_pc_raw(); - if (!pc_res) { - return std::unexpected{pc_res.error()}; - } - addr_t addr = ((*pc_res + 4) & ~0x3u) + imm12; - auto r = br(addr, width); - if (!r) { - return std::unexpected{r.error()}; - } - return wr(rt, *r); - } - - // Resolve effective address + optional writeback per immediate form. - addr_t addr = 0; - bool writeback = false; - data_t wb_val = 0; - if ((hw1 >> 7) & 1) { - // imm12 offset form (no writeback). - addr = rn_val + (hw2 & 0xFFFu); - } else { - uint8_t op = (hw2 >> 8) & 0xF; - uint32_t imm8 = hw2 & 0xFF; - switch (op) { - case 0x0: // [Rn, #+imm8] - addr = rn_val + imm8; - break; - case 0xC: // [Rn, #-imm8] - addr = rn_val - imm8; - break; - case 0xB: // [Rn], #+imm8 (post-index) - addr = rn_val; - wb_val = rn_val + imm8; - writeback = true; - break; - case 0x9: // [Rn], #-imm8 (post-index) - addr = rn_val; - wb_val = rn_val - imm8; - writeback = true; - break; - case 0xF: // [Rn, #+imm8]! (pre-index) - addr = rn_val + imm8; - wb_val = addr; - writeback = true; - break; - case 0xD: // [Rn, #-imm8]! (pre-index) - addr = rn_val - imm8; - wb_val = addr; - writeback = true; - break; - default: - return std::unexpected{CPUError::IllegalInstruction}; - } - } - - if (load) { - auto v = br(addr, width); - if (!v) { - return std::unexpected{v.error()}; - } - auto w = wr(rt, *v); - if (!w) { - return w; - } - } else { - auto w = bw(addr, rr(rt), width); - if (!w) { - return w; - } - } - if (writeback) { - return wr(rn, wb_val); - } - return {}; + return t32_loadstore_single(hw1, hw2); } // ── UDIV / SDIV ── @@ -533,274 +347,27 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { // ── Data processing (shifted register): AND, ORR, EOR, ADD, SUB, etc. ── if ((hw1 & 0xFE00) == 0xEA00 && (hw2 & 0x8000) == 0) { - uint8_t op = (hw1 >> 5) & 0xF; - bool s_bit = (hw1 >> 4) & 1; - uint8_t rn = hw1 & 0xF; - uint8_t rd = (hw2 >> 8) & 0xF; - uint8_t rm = hw2 & 0xF; - uint8_t imm3 = (hw2 >> 12) & 0x7; - uint8_t imm2 = (hw2 >> 6) & 0x3; - uint8_t shift_type = (hw2 >> 4) & 0x3; - uint8_t shift_n = (imm3 << 2) | imm2; - - uint32_t rm_val = rr(rm); - - uint32_t shifted; - switch (shift_type) { - case 0: - shifted = shift_n == 0 ? rm_val : rm_val << shift_n; - break; - case 1: - shifted = rm_val >> (shift_n == 0 ? 0 : shift_n); - break; - case 2: { - if (shift_n == 0) { - shifted = rm_val; - } else { - uint32_t sign = rm_val & 0x80000000u; - shifted = rm_val >> shift_n; - if (sign) { - shifted |= (0xFFFFFFFFu << (32 - shift_n)); - } - } - break; - } - default: - return std::unexpected{CPUError::IllegalInstruction}; - } - - uint32_t rn_val = rr(rn); - uint32_t result; - switch (op) { - case 0: - result = rn_val & shifted; - break; - case 1: - result = rn_val & ~shifted; - break; - case 2: - result = (rn == 15) ? shifted : (rn_val | shifted); - break; - case 3: - result = ~shifted; - break; - case 4: - result = rn_val ^ shifted; - break; - case 8: - result = rn_val + shifted; - break; - case 13: - result = rn_val - shifted; - break; - case 14: - result = shifted - rn_val; - break; - default: - return std::unexpected{CPUError::IllegalInstruction}; - } - - if (s_bit) { - if (op == 8 || op == 13 || op == 14) { - update_flags(op <= 8 ? FlagPostOperation::Add - : FlagPostOperation::Sub, - rn_val, shifted, result); - } else { - update_nz(result); - } - } - // CMP/CMN/TST/TEQ: S=1, Rd=15 → flags only, no register write. - if (s_bit && rd == 15) { - return {}; - } - return wr(rd, result); + return t32_dataproc_reg(hw1, hw2); } // ── Shift register (LSL/LSR/ASR/ROR register) ── if ((hw1 & 0xFF00) == 0xFA00 && (hw2 & 0xF0F0) == 0xF000) { - uint8_t rn = hw1 & 0xF; - bool s_bit = (hw1 >> 4) & 1; - uint8_t shift_type = (hw1 >> 5) & 0x3; - uint8_t rd = (hw2 >> 8) & 0xF; - uint8_t rm = hw2 & 0xF; - - uint32_t value = rr(rn); - uint32_t shift = rr(rm) & 0xFFu; - uint32_t result = value; - - switch (shift_type) { - case 0: // LSL - result = shift == 0 ? value : (shift < 32 ? value << shift : 0); - break; - case 1: // LSR - result = shift == 0 ? value : (shift < 32 ? value >> shift : 0); - break; - case 2: // ASR - if (shift == 0) { - result = value; - } else if (shift >= 32) { - result = (value & 0x80000000u) ? 0xFFFFFFFFu : 0; - } else { - result = static_cast( - static_cast(value) >> shift); - } - break; - case 3: { // ROR - uint32_t rot = shift & 31u; - result = - rot == 0 ? value : ((value >> rot) | (value << (32 - rot))); - break; - } - } - - auto w = wr(rd, result); - if (!w) { - return w; - } - if (s_bit) { - update_nz(result); - } - return {}; + return t32_shift_reg(hw1, hw2); } // ── TBB / TBH (Table Branch) ── if ((hw1 & 0xFFF0) == 0xE8D0 && (hw2 & 0xF0F0) == 0xF000) { - uint8_t rn = hw1 & 0xF; - uint8_t rm = hw2 & 0xF; - bool H = (hw2 >> 4) & 1; - - uint32_t pc_val = rr(15) + 4; - uint32_t base = (rn == 15) ? pc_val : rr(rn); - uint32_t index = (rm == 15) ? 0u : rr(rm); - - uint32_t halfwords; - if (H) { - auto v = br(base + index * 2, Width::HalfWord); - if (!v) { - return std::unexpected{v.error()}; - } - halfwords = *v; - } else { - auto v = br(base + index, Width::Byte); - if (!v) { - return std::unexpected{v.error()}; - } - halfwords = *v; - } - - addr_t target = pc_val + halfwords * 2; - return write_pc(target); + return t32_tbb_tbh(hw1, hw2); } // ── STRD / LDRD (Store/Load Dual, immediate offset) ── if ((hw1 & 0xFE40) == 0xE840) { - bool P = (hw1 >> 8) & 1; - bool U = (hw1 >> 7) & 1; - bool W = (hw1 >> 5) & 1; - bool L = (hw1 >> 4) & 1; - uint8_t rn = hw1 & 0xF; - uint8_t rt = (hw2 >> 12) & 0xF; - uint8_t rt2 = (hw2 >> 8) & 0xF; - uint32_t offset = static_cast((hw2 & 0xFF)) * 4; - - uint32_t rn_val = rr(rn); - addr_t offset_addr = U ? (rn_val + offset) : (rn_val - offset); - addr_t addr = P ? offset_addr : rn_val; - - if (L) { - auto v1 = br(addr, Width::Word); - if (!v1) { - return std::unexpected{v1.error()}; - } - auto v2 = br(addr + 4, Width::Word); - if (!v2) { - return std::unexpected{v2.error()}; - } - auto w1 = wr(rt, *v1); - if (!w1) { - return w1; - } - auto w2 = wr(rt2, *v2); - if (!w2) { - return w2; - } - } else { - auto w1 = bw(addr, rr(rt), Width::Word); - if (!w1) { - return w1; - } - auto w2 = bw(addr + 4, rr(rt2), Width::Word); - if (!w2) { - return w2; - } - } - - if (W) { - return wr(rn, offset_addr); - } - return {}; + return t32_strd_ldrd(hw1, hw2); } // ── STM / LDM (Store/Load Multiple) ── if ((hw1 & 0xFE40) == 0xE800) { - bool U = (hw1 >> 7) & 1; - bool W = (hw1 >> 5) & 1; - bool L = (hw1 >> 4) & 1; - uint8_t rn = hw1 & 0xF; - uint16_t rlist = hw2; - - int count = std::popcount(rlist); - if (count == 0) { - return std::unexpected{CPUError::IllegalInstruction}; - } - - uint32_t rn_val = rr(rn); - bool decrement = !U; - addr_t start_addr = - decrement ? rn_val - static_cast(count * 4) : rn_val; - addr_t addr = start_addr; - - if (L) { - for (int i = 0; i < 16; i++) { - if (rlist & (1 << i)) { - auto v = br(addr, Width::Word); - if (!v) { - return std::unexpected{v.error()}; - } - if (i == 15) { - auto w = write_pc(*v); - if (!w) { - return w; - } - } else { - auto w = wr(i, *v); - if (!w) { - return w; - } - } - addr += 4; - } - } - } else { - for (int i = 0; i < 16; i++) { - if (rlist & (1 << i)) { - data_t val = (i == 15) ? (rr(15) + 4) : rr(i); - auto w = bw(addr, val, Width::Word); - if (!w) { - return w; - } - addr += 4; - } - } - } - - if (W) { - uint32_t new_rn = decrement - ? rn_val - static_cast(count * 4) - : rn_val + static_cast(count * 4); - return wr(rn, new_rn); - } - return {}; + return t32_stm_ldm(hw1, hw2); } return std::unexpected{CPUError::IllegalInstruction}; diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp new file mode 100644 index 0000000..a35ec5c --- /dev/null +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp @@ -0,0 +1,245 @@ +#include "arch/arm/cortex_m3/cortex_m3.hpp" +#include "arch/arm/cortex_m3/thumb32_fields.hpp" + +#include + +namespace micro_forge::cpu::arm::cortex_m3 { + +using namespace thumb; + +// ── Add/subtract (plain imm12): insn[25]=1 (hw1[9]) ── +// addw/subw (S=0) and adds.w/subs.w (S=1). imm12 = i:imm3:imm8 packed. +// PLAIN (not Thumb2ExpandImm). op = hw1[8:5]: 0000=ADD, 0101=SUB; S=hw1[4]. +// Dispatched when (hw1 & 0xF800)==0xF000 && (hw1 & 0x0200)!=0 && (hw2 & 0x8000)==0. +CPU::CPUExpected CortexM3CPU::t32_addsub_plain_imm(uint16_t hw1, + uint16_t hw2) { + uint8_t op = (hw1 >> 5) & 0xF; + bool s_bit = (hw1 >> 4) & 1; + uint8_t rn = hw1 & 0xF; + uint8_t rd = (hw2 >> 12) & 0xF; + uint32_t imm12 = (((hw1 >> 10) & 0x1u) << 11) | + (((hw2 >> 12) & 0x7u) << 8) | (hw2 & 0xFFu); + uint32_t a = rr(rn); + uint32_t result; + bool is_sub; + switch (op) { + case 0x0: + is_sub = false; + result = a + imm12; + break; // ADD.W/addw + case 0x5: + is_sub = true; + result = a - imm12; + break; // SUB.W/subw + default: + return std::unexpected{CPUError::IllegalInstruction}; + } + if (s_bit) { + update_flags(is_sub ? FlagPostOperation::Sub + : FlagPostOperation::Add, + a, imm12, result); + } + return wr(rd, result); +} + +// ── Data processing (modified immediate): insn[25]=0 ── +// Dispatched when (hw1 & 0xF800)==0xF000 && (hw1 & 0x0200)==0 && (hw2 & 0x8000)==0. +CPU::CPUExpected CortexM3CPU::t32_dataproc_imm(uint16_t hw1, uint16_t hw2) { + uint8_t op2 = (hw1 >> 5) & 0xF; + bool s_bit = (hw1 >> 4) & 1; + uint8_t rn = thumb32::dp_rn(hw1); + uint8_t rd = thumb32::dp_rd(hw2); + uint32_t imm32 = + thumb32::expand_imm12((hw1 >> 10) & 1, (hw2 >> 12) & 7, hw2 & 0xFF); + uint32_t rn_val = rr(rn); + uint32_t result; + + switch (op2) { + case 0: + result = rn_val & imm32; + break; // AND + case 1: + result = rn_val & ~imm32; + break; // BIC + case 2: + result = (rn == 15) ? imm32 : (rn_val | imm32); + break; // ORR/MOV + case 4: + result = rn_val ^ imm32; + break; // EOR + case 8: + result = rn_val + imm32; + break; // ADD + case 10: + result = rn_val + imm32 + ((xpsr_ & PSR_C) ? 1u : 0u); + break; // ADC + case 11: { + uint32_t borrow = (xpsr_ & PSR_C) ? 0u : 1u; + result = rn_val - imm32 - borrow; + break; // SBC + } + case 13: + result = rn_val - imm32; + break; // SUB + case 14: + result = imm32 - rn_val; + break; // RSB + default: + return std::unexpected{CPUError::IllegalInstruction}; + } + + if (s_bit) { + if (op2 == 8 || op2 == 10 || op2 == 13 || op2 == 14 || op2 == 11) { + uint32_t flag_rhs = + op2 == 10 ? imm32 + ((xpsr_ & PSR_C) ? 1u : 0u) + : op2 == 11 ? imm32 + ((xpsr_ & PSR_C) ? 0u : 1u) + : imm32; + update_flags(op2 <= 10 ? FlagPostOperation::Add + : FlagPostOperation::Sub, + rn_val, flag_rhs, result); + } else { + update_nz(result); + } + } + // CMP/CMN/TST/TEQ: S=1, Rd=15 → flags only, no register write + if (s_bit && rd == 15) { + return {}; + } + return wr(rd, result); +} + +// ── Data processing (shifted register): AND, ORR, EOR, ADD, SUB, etc. ── +// Dispatched when (hw1 & 0xFE00)==0xEA00 && (hw2 & 0x8000)==0. +CPU::CPUExpected CortexM3CPU::t32_dataproc_reg(uint16_t hw1, uint16_t hw2) { + uint8_t op = (hw1 >> 5) & 0xF; + bool s_bit = (hw1 >> 4) & 1; + uint8_t rn = hw1 & 0xF; + uint8_t rd = (hw2 >> 8) & 0xF; + uint8_t rm = hw2 & 0xF; + uint8_t imm3 = (hw2 >> 12) & 0x7; + uint8_t imm2 = (hw2 >> 6) & 0x3; + uint8_t shift_type = (hw2 >> 4) & 0x3; + uint8_t shift_n = (imm3 << 2) | imm2; + + uint32_t rm_val = rr(rm); + + uint32_t shifted; + switch (shift_type) { + case 0: + shifted = shift_n == 0 ? rm_val : rm_val << shift_n; + break; + case 1: + shifted = rm_val >> (shift_n == 0 ? 0 : shift_n); + break; + case 2: { + if (shift_n == 0) { + shifted = rm_val; + } else { + uint32_t sign = rm_val & 0x80000000u; + shifted = rm_val >> shift_n; + if (sign) { + shifted |= (0xFFFFFFFFu << (32 - shift_n)); + } + } + break; + } + default: + return std::unexpected{CPUError::IllegalInstruction}; + } + + uint32_t rn_val = rr(rn); + uint32_t result; + switch (op) { + case 0: + result = rn_val & shifted; + break; + case 1: + result = rn_val & ~shifted; + break; + case 2: + result = (rn == 15) ? shifted : (rn_val | shifted); + break; + case 3: + result = ~shifted; + break; + case 4: + result = rn_val ^ shifted; + break; + case 8: + result = rn_val + shifted; + break; + case 13: + result = rn_val - shifted; + break; + case 14: + result = shifted - rn_val; + break; + default: + return std::unexpected{CPUError::IllegalInstruction}; + } + + if (s_bit) { + if (op == 8 || op == 13 || op == 14) { + update_flags(op <= 8 ? FlagPostOperation::Add + : FlagPostOperation::Sub, + rn_val, shifted, result); + } else { + update_nz(result); + } + } + // CMP/CMN/TST/TEQ: S=1, Rd=15 → flags only, no register write. + if (s_bit && rd == 15) { + return {}; + } + return wr(rd, result); +} + +// ── Shift register (LSL/LSR/ASR/ROR register) ── +// Dispatched when (hw1 & 0xFF00)==0xFA00 && (hw2 & 0xF0F0)==0xF000. +CPU::CPUExpected CortexM3CPU::t32_shift_reg(uint16_t hw1, uint16_t hw2) { + uint8_t rn = hw1 & 0xF; + bool s_bit = (hw1 >> 4) & 1; + uint8_t shift_type = (hw1 >> 5) & 0x3; + uint8_t rd = (hw2 >> 8) & 0xF; + uint8_t rm = hw2 & 0xF; + + uint32_t value = rr(rn); + uint32_t shift = rr(rm) & 0xFFu; + uint32_t result = value; + + switch (shift_type) { + case 0: // LSL + result = shift == 0 ? value : (shift < 32 ? value << shift : 0); + break; + case 1: // LSR + result = shift == 0 ? value : (shift < 32 ? value >> shift : 0); + break; + case 2: // ASR + if (shift == 0) { + result = value; + } else if (shift >= 32) { + result = (value & 0x80000000u) ? 0xFFFFFFFFu : 0; + } else { + result = static_cast( + static_cast(value) >> shift); + } + break; + case 3: { // ROR + uint32_t rot = shift & 31u; + result = + rot == 0 ? value : ((value >> rot) | (value << (32 - rot))); + break; + } + } + + auto w = wr(rd, result); + if (!w) { + return w; + } + if (s_bit) { + update_nz(result); + } + return {}; +} + +} // namespace micro_forge::cpu::arm::cortex_m3 diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp new file mode 100644 index 0000000..b20d74b --- /dev/null +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp @@ -0,0 +1,267 @@ +#include "arch/arm/cortex_m3/cortex_m3.hpp" +#include "arch/arm/cortex_m3/thumb32_fields.hpp" + +#include +#include + +namespace micro_forge::cpu::arm::cortex_m3 { + +using namespace thumb; + +// ── Load/Store single data item (.W): str/ldr/strb/ldrb/strh/ldrh .W ── +// hw1[7] selects the immediate form: +// 1 → imm12 offset (T2/T3): addr = Rn + imm12, no writeback. +// 0 → imm8 with addressing modes, op = hw2[11:8]: +// 0=offset+, C=offset-, B=post+, 9=post-, F=pre+, D=pre-. +// Dispatched from execute_32bit when (hw1 & 0xFF00) == 0xF800. +CPU::CPUExpected CortexM3CPU::t32_loadstore_single(uint16_t hw1, + uint16_t hw2) { + uint8_t rn = hw1 & 0xF; + bool load = (hw1 >> 4) & 1; + uint8_t size = (hw1 >> 5) & 0x3; + uint8_t rt = (hw2 >> 12) & 0xF; + uint32_t rn_val = rr(rn); + Width width; + switch (size) { + case 0: + width = Width::Byte; + break; + case 1: + width = Width::HalfWord; + break; + case 2: + width = Width::Word; + break; + default: + return std::unexpected{CPUError::IllegalInstruction}; + } + + // ── LDR.W (literal): Rn == PC ── + // `ldr.w Rt, [pc, #imm12]` — PC-relative literal pool load (compiled + // `LDR Rd, =const`). addr = Align(PC+4, 4) + imm12, no writeback. + // Store-to-PC-relative is UNDEFINED → rejected for !load. + if (rn == 15) { + if (!load) { + return std::unexpected{CPUError::IllegalInstruction}; + } + uint32_t imm12 = hw2 & 0xFFFu; + auto pc_res = read_pc_raw(); + if (!pc_res) { + return std::unexpected{pc_res.error()}; + } + addr_t addr = ((*pc_res + 4) & ~0x3u) + imm12; + auto r = br(addr, width); + if (!r) { + return std::unexpected{r.error()}; + } + return wr(rt, *r); + } + + // Resolve effective address + optional writeback per immediate form. + addr_t addr = 0; + bool writeback = false; + data_t wb_val = 0; + if ((hw1 >> 7) & 1) { + // imm12 offset form (no writeback). + addr = rn_val + (hw2 & 0xFFFu); + } else { + uint8_t op = (hw2 >> 8) & 0xF; + uint32_t imm8 = hw2 & 0xFF; + switch (op) { + case 0x0: // [Rn, #+imm8] + addr = rn_val + imm8; + break; + case 0xC: // [Rn, #-imm8] + addr = rn_val - imm8; + break; + case 0xB: // [Rn], #+imm8 (post-index) + addr = rn_val; + wb_val = rn_val + imm8; + writeback = true; + break; + case 0x9: // [Rn], #-imm8 (post-index) + addr = rn_val; + wb_val = rn_val - imm8; + writeback = true; + break; + case 0xF: // [Rn, #+imm8]! (pre-index) + addr = rn_val + imm8; + wb_val = addr; + writeback = true; + break; + case 0xD: // [Rn, #-imm8]! (pre-index) + addr = rn_val - imm8; + wb_val = addr; + writeback = true; + break; + default: + return std::unexpected{CPUError::IllegalInstruction}; + } + } + + if (load) { + auto v = br(addr, width); + if (!v) { + return std::unexpected{v.error()}; + } + auto w = wr(rt, *v); + if (!w) { + return w; + } + } else { + auto w = bw(addr, rr(rt), width); + if (!w) { + return w; + } + } + if (writeback) { + return wr(rn, wb_val); + } + return {}; +} + +// ── TBB / TBH (Table Branch) ── +// Dispatched when (hw1 & 0xFFF0) == 0xE8D0 && (hw2 & 0xF0F0) == 0xF000. +// (TBH's H-bit handling here is a known T1 bug — see coverage matrix F32-9.) +CPU::CPUExpected CortexM3CPU::t32_tbb_tbh(uint16_t hw1, uint16_t hw2) { + uint8_t rn = hw1 & 0xF; + uint8_t rm = hw2 & 0xF; + bool H = (hw2 >> 4) & 1; + + uint32_t pc_val = rr(15) + 4; + uint32_t base = (rn == 15) ? pc_val : rr(rn); + uint32_t index = (rm == 15) ? 0u : rr(rm); + + uint32_t halfwords; + if (H) { + auto v = br(base + index * 2, Width::HalfWord); + if (!v) { + return std::unexpected{v.error()}; + } + halfwords = *v; + } else { + auto v = br(base + index, Width::Byte); + if (!v) { + return std::unexpected{v.error()}; + } + halfwords = *v; + } + + addr_t target = pc_val + halfwords * 2; + return write_pc(target); +} + +// ── STRD / LDRD (Store/Load Dual, immediate offset) ── +// Dispatched when (hw1 & 0xFE40) == 0xE840. +CPU::CPUExpected CortexM3CPU::t32_strd_ldrd(uint16_t hw1, uint16_t hw2) { + bool P = (hw1 >> 8) & 1; + bool U = (hw1 >> 7) & 1; + bool W = (hw1 >> 5) & 1; + bool L = (hw1 >> 4) & 1; + uint8_t rn = hw1 & 0xF; + uint8_t rt = (hw2 >> 12) & 0xF; + uint8_t rt2 = (hw2 >> 8) & 0xF; + uint32_t offset = static_cast((hw2 & 0xFF)) * 4; + + uint32_t rn_val = rr(rn); + addr_t offset_addr = U ? (rn_val + offset) : (rn_val - offset); + addr_t addr = P ? offset_addr : rn_val; + + if (L) { + auto v1 = br(addr, Width::Word); + if (!v1) { + return std::unexpected{v1.error()}; + } + auto v2 = br(addr + 4, Width::Word); + if (!v2) { + return std::unexpected{v2.error()}; + } + auto w1 = wr(rt, *v1); + if (!w1) { + return w1; + } + auto w2 = wr(rt2, *v2); + if (!w2) { + return w2; + } + } else { + auto w1 = bw(addr, rr(rt), Width::Word); + if (!w1) { + return w1; + } + auto w2 = bw(addr + 4, rr(rt2), Width::Word); + if (!w2) { + return w2; + } + } + + if (W) { + return wr(rn, offset_addr); + } + return {}; +} + +// ── STM / LDM (Store/Load Multiple) ── +// Dispatched when (hw1 & 0xFE40) == 0xE800. +CPU::CPUExpected CortexM3CPU::t32_stm_ldm(uint16_t hw1, uint16_t hw2) { + bool U = (hw1 >> 7) & 1; + bool W = (hw1 >> 5) & 1; + bool L = (hw1 >> 4) & 1; + uint8_t rn = hw1 & 0xF; + uint16_t rlist = hw2; + + int count = std::popcount(rlist); + if (count == 0) { + return std::unexpected{CPUError::IllegalInstruction}; + } + + uint32_t rn_val = rr(rn); + bool decrement = !U; + addr_t start_addr = + decrement ? rn_val - static_cast(count * 4) : rn_val; + addr_t addr = start_addr; + + if (L) { + for (int i = 0; i < 16; i++) { + if (rlist & (1 << i)) { + auto v = br(addr, Width::Word); + if (!v) { + return std::unexpected{v.error()}; + } + if (i == 15) { + auto w = write_pc(*v); + if (!w) { + return w; + } + } else { + auto w = wr(i, *v); + if (!w) { + return w; + } + } + addr += 4; + } + } + } else { + for (int i = 0; i < 16; i++) { + if (rlist & (1 << i)) { + data_t val = (i == 15) ? (rr(15) + 4) : rr(i); + auto w = bw(addr, val, Width::Word); + if (!w) { + return w; + } + addr += 4; + } + } + } + + if (W) { + uint32_t new_rn = decrement + ? rn_val - static_cast(count * 4) + : rn_val + static_cast(count * 4); + return wr(rn, new_rn); + } + return {}; +} + +} // namespace micro_forge::cpu::arm::cortex_m3 From 8f272d12d6f16c63a2ce75f04819c1bf32909b71 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 21 Jun 2026 19:18:56 +0800 Subject: [PATCH 3/5] docs(notes): 008 armcc/AC6 firmware corpus (T5c) Companion note for the T5c corpus (ce2f6ab): the AC5->AC6 migration recipe, the WSL-fs build wall, and the headless build flow. --- .../notes/008-armcc-ac6-firmware-corpus.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 document/notes/008-armcc-ac6-firmware-corpus.md diff --git a/document/notes/008-armcc-ac6-firmware-corpus.md b/document/notes/008-armcc-ac6-firmware-corpus.md new file mode 100644 index 0000000..bc409fc --- /dev/null +++ b/document/notes/008-armcc-ac6-firmware-corpus.md @@ -0,0 +1,39 @@ +# 008 — armcc/AC6 固件语料 E2E (T5c) + +> 2026-06-21。把真实固件语料从「1 份 Keil F103.axf」(见 [007](007-cortex-m3-f103-keil-firmware.md))扩到「3 份 CubeF1 Nucleo 示例」,作为 ctest E2E 回归门禁,补 armcc codegen 多样性。 + +## 背景 + +007 证明 Keil/MDK 固件能跑、且一次就暴露 9 处 Cortex-M3 缺口。但只有 1 份 armcc 样本(用户自写 CubeMX 工程)。要验证「任意真实固件」,需要一批 armcc 编译的标准示例。 + +## 目标 + +- 用 Keil MDK headless 批量编 CubeF1 **STM32F103RB-Nucleo** 小示例(GPIO/TIM/UART)。 +- 产出的 `.axf` 作 ctest E2E fixture,**一次编译、提交进仓**,之后改动指令自动回归(CI 无 Keil)。 + +## 关键决策 + +1. **走 Keil headless(`UV4 -b`),不重生成工程**。CubeF1 自带 `.uvprojx`,直接编。 +2. **复制最小子树到本地 NTFS(`D:\mf\`),不在 WSL 文件系统编**(见陷阱 3)。 +3. **二进制 fixture 提交**,配方进 `test/firmware/armcc/REGENERATE.md`;vendored 子模块补丁不入库。 + +## 陷阱(本次核心价值,全是不靠运气撞出来的) + +1. **AC5→AC6 编译器墙**:CubeF1 示例工程是 **AC5(armcc)** 配置,新版 MDK 只剩 **AC6(armclang)**(AC5 已退役)。`UV4 -b` 报 `uses ARM-Compiler 'Default Compiler Version 5' which is not available`,直接 abort。 +2. **`` 的正确位置是 `` 直接子级**(紧跟 ``),**不是** `` 内。手插错位置会被静默忽略——这步靠 Keil GUI 切一次 AC6 才学到正确写法。 +3. **WSL 文件系统(9p)建不了 Keil 的 `.__i` 响应文件**。经 `\\wsl.localhost` 或映射盘符(`net use Z:`)都不行(同一 9p 后端)。**必须编在本地 NTFS**——把 Drivers + 选定 Examples 复制到 `D:\mf\STM32CubeF1\`,保留 `..\..\..\..\..\Drivers` 相对深度。 +4. **`--C99` 是 AC5 flag,AC6 不认**(`armclang: error: unknown argument: '--C99'`)。藏在 `` 里,删掉即可(C99 由 AC6 默认给)。 +5. **`` 陷阱**:GUI 切 AC6 时 Keil 会写入一个语言标准字段,实测强制成 C90 → `cmsis_armclang.h` 的 `inline` 报 `unknown type name`。**删 ``/``/``**,走 AC6 默认 gnu11。 +6. **WSL→Windows interop 偶发挂**(`exec format error`,UV4/cmd.exe/net.exe 全挂)。挂了就在 Windows 原生跑 `.bat`。`/mnt/d/mf` 从 WSL 仍可读写,故编完拷回不阻塞。 + +## 验证 + +- 3 份 `.axf` 全 `ELF32 / ARM / entry=0x80000ed`,`Stm32f103Soc::load_elf` 直接加载。 +- `FirmwareArmcc.{GpioIoToggle,TimTimeBase,UartPrintf}BootsClean` 三测全 **0 fault**(2,000,000 步)。 +- 全量 ctest **247/247** 绿。 +- **当前模拟器对 armcc codegen 零 fault**——和 F103.axf 一致,未提前暴露新指令缺口(那些等 T1/T2 主动挖)。 + +## 后续 + +- 想扩语料:照 `REGENERATE.md` 加示例 + 在 `test_firmware_armcc.cpp` 加 `BootsClean` 测试。 +- armcc 多样性的真正价值在 **T1/T2 修完指令后**——那时这些固件成了活体验收器。 From 2c18e207857e8193201e13ddf7d766612fae9e3c Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 21 Jun 2026 19:39:04 +0800 Subject: [PATCH 4/5] fix(cortex-m3): shift instructions update the Carry flag (T1a) LSLS/LSRS/ASRS/RORS (16-bit immediate, 16-bit register, and 32-bit register forms) called update_nz() which only touches N/Z, leaving C stale. ARMv7-M requires C = shifter carry-out (with the encoded-0=32, RRX, and shift-by->=32 special cases). Add a centralized thumb::barrel_shift(type, value, amount, carry_in) helper that returns both the shifted value and the carry-out, and route the three shift sites through it. Side fixes uncovered by centralizing: ASR-by-register >=32 used signed >>amount (UB) instead of sign-extension; ROR Rs==0 is now RRX (was a silent no-op). 5 new carry unit tests (read flags via MRS Rd,APSR). 252/252 green. --- include/arch/arm/cortex_m3/thumb_fields.hpp | 55 ++++++++++++++++ src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp | 64 ++++++++++++------- .../cortex_m3/cortex_m3_thumb32_dataproc.cpp | 35 +++------- test/test_cortex_m3_basic.cpp | 64 +++++++++++++++++++ 4 files changed, 168 insertions(+), 50 deletions(-) diff --git a/include/arch/arm/cortex_m3/thumb_fields.hpp b/include/arch/arm/cortex_m3/thumb_fields.hpp index 2254aed..dea7366 100644 --- a/include/arch/arm/cortex_m3/thumb_fields.hpp +++ b/include/arch/arm/cortex_m3/thumb_fields.hpp @@ -93,5 +93,60 @@ constexpr uint8_t decode_key(uint16_t insn) { return (insn >> 11) & 0x1Fu; } +/// ARMv7-M barrel shift, returning both the shifted value and the shifter +/// carry-out (the C flag source for shift instructions). Pure model of the +/// ARM shift operation — no CPU state. +/// +/// type: 0=LSL, 1=LSR, 2=ASR, 3=ROR. +/// `amount` is the RESOLVED shift amount: for immediate shifts the caller +/// converts the encoded field (LSR/ASR/ROR encoded 0 → 32; LSL encoded 0 → 0). +/// `carry_in` is the current C flag, used when amount==0 (LSL/LSR/ASR leave C +/// unchanged; ROR #0 is RRX). +struct ShiftOut { + uint32_t value; + bool carry; +}; + +inline ShiftOut barrel_shift(uint8_t type, uint32_t value, uint8_t amount, + bool carry_in) { + if (amount == 0) { + if (type == 3) { // ROR #0 → RRX: (C:value) >> 1, C = value[0] + return {(static_cast(carry_in) << 31) | (value >> 1), + (value & 1u) != 0}; + } + return {value, carry_in}; // LSL/LSR/ASR #0 → unchanged, C unchanged + } + switch (type) { + case 0: { // LSL + if (amount >= 32) { + return {0u, amount == 32 ? ((value & 1u) != 0) : false}; + } + return {value << amount, ((value >> (32 - amount)) & 1u) != 0}; + } + case 1: { // LSR + if (amount >= 32) { + return {0u, (value & 0x80000000u) != 0}; + } + return {value >> amount, ((value >> (amount - 1)) & 1u) != 0}; + } + case 2: { // ASR + if (amount >= 32) { + bool sign = (value & 0x80000000u) != 0; + return {sign ? 0xFFFFFFFFu : 0u, sign}; + } + return {static_cast(static_cast(value) >> amount), + ((value >> (amount - 1)) & 1u) != 0}; + } + default: { // ROR (amount > 0) + uint8_t r = amount & 31u; + if (r == 0) { + return {value, (value & 0x80000000u) != 0}; + } + return {(value >> r) | (value << (32 - r)), + ((value >> (r - 1)) & 1u) != 0}; + } + } +} + } // namespace arm::cortex_m3::thumb } // namespace micro_forge::cpu diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp index eaf393a..a6ee4f7 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp @@ -129,28 +129,27 @@ CPU::CPUExpected CortexM3CPU::execute_16bit(uint16_t insn) { case 0b00000: case 0b00001: case 0b00010: { - uint8_t op = (insn >> 11) & 0x3; + uint8_t op = (insn >> 11) & 0x3; // 0=LSL, 1=LSR, 2=ASR uint8_t imm = imm5(insn); uint8_t rm = rn3(insn); uint8_t rd = rd3(insn); data_t val = rr(rm); - data_t result; - - if (op == 0b00) { // LSL - result = imm == 0 ? val : val << imm; - } else if (op == 0b01) { // LSR - result = imm == 0 ? 0 : val >> imm; - } else { // ASR - result = - (imm == 0) - ? ((val & 0x80000000u) ? 0xFFFFFFFFu : 0) - : static_cast(static_cast(val) >> imm); - } + // LSR/ASR encoded shift of 0 means shift-by-32; LSL 0 = no shift. + uint8_t amount = (op == 0b00) ? imm : (imm == 0 ? 32 : imm); + auto [result, carry] = + barrel_shift(op, val, amount, (xpsr_ & PSR_C) != 0); auto res = wr(rd, result); if (!res) { return res; } update_nz(result); + // Shift instructions update C from the shifter carry-out (LSL #0 + // returns carry_in, so C is unchanged in that case). + if (carry) { + xpsr_ |= PSR_C; + } else { + xpsr_ &= ~PSR_C; + } break; } @@ -256,6 +255,9 @@ CPU::CPUExpected CortexM3CPU::execute_16bit(uint16_t insn) { uint8_t rd = rd3(insn); data_t a = rr(rd), b = rr(rm); data_t result; + // Set only by the shift-by-register ops (LSL/LSR/ASR/ROR): the + // shifter carry-out drives C. nullopt → C unchanged. + std::optional shift_carry; switch (op) { case 0x0: @@ -264,25 +266,34 @@ CPU::CPUExpected CortexM3CPU::execute_16bit(uint16_t insn) { case 0x1: result = a ^ b; break; - case 0x2: - result = a << (b & 0xFF); + case 0x2: { // LSL register + auto s = barrel_shift(0, a, b & 0xFF, (xpsr_ & PSR_C) != 0); + result = s.value; + shift_carry = s.carry; break; - case 0x3: - result = a >> (b & 0xFF); + } + case 0x3: { // LSR register + auto s = barrel_shift(1, a, b & 0xFF, (xpsr_ & PSR_C) != 0); + result = s.value; + shift_carry = s.carry; break; - case 0x4: - result = static_cast(static_cast(a) >> - (b & 0xFF)); + } + case 0x4: { // ASR register + auto s = barrel_shift(2, a, b & 0xFF, (xpsr_ & PSR_C) != 0); + result = s.value; + shift_carry = s.carry; break; + } case 0x5: result = a + b + ((xpsr_ & PSR_C) ? 1 : 0); break; case 0x6: result = a - b - ((xpsr_ & PSR_C) ? 0 : 1); break; - case 0x7: { - uint8_t n = (b & 0xFF) & 0x1F; - result = n ? ((a >> n) | (a << (32 - n))) : a; + case 0x7: { // ROR register + auto s = barrel_shift(3, a, b & 0xFF, (xpsr_ & PSR_C) != 0); + result = s.value; + shift_carry = s.carry; break; } case 0x8: @@ -317,6 +328,13 @@ CPU::CPUExpected CortexM3CPU::execute_16bit(uint16_t insn) { return res; } update_nz(result); + if (shift_carry) { + if (*shift_carry) { + xpsr_ |= PSR_C; + } else { + xpsr_ &= ~PSR_C; + } + } break; } diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp index a35ec5c..2da2ddb 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp @@ -204,33 +204,9 @@ CPU::CPUExpected CortexM3CPU::t32_shift_reg(uint16_t hw1, uint16_t hw2) { uint8_t rm = hw2 & 0xF; uint32_t value = rr(rn); - uint32_t shift = rr(rm) & 0xFFu; - uint32_t result = value; - - switch (shift_type) { - case 0: // LSL - result = shift == 0 ? value : (shift < 32 ? value << shift : 0); - break; - case 1: // LSR - result = shift == 0 ? value : (shift < 32 ? value >> shift : 0); - break; - case 2: // ASR - if (shift == 0) { - result = value; - } else if (shift >= 32) { - result = (value & 0x80000000u) ? 0xFFFFFFFFu : 0; - } else { - result = static_cast( - static_cast(value) >> shift); - } - break; - case 3: { // ROR - uint32_t rot = shift & 31u; - result = - rot == 0 ? value : ((value >> rot) | (value << (32 - rot))); - break; - } - } + uint8_t amount = rr(rm) & 0xFFu; + auto [result, carry] = + barrel_shift(shift_type, value, amount, (xpsr_ & PSR_C) != 0); auto w = wr(rd, result); if (!w) { @@ -238,6 +214,11 @@ CPU::CPUExpected CortexM3CPU::t32_shift_reg(uint16_t hw1, uint16_t hw2) { } if (s_bit) { update_nz(result); + if (carry) { + xpsr_ |= PSR_C; + } else { + xpsr_ &= ~PSR_C; + } } return {}; } diff --git a/test/test_cortex_m3_basic.cpp b/test/test_cortex_m3_basic.cpp index c47b522..b24ee90 100644 --- a/test/test_cortex_m3_basic.cpp +++ b/test/test_cortex_m3_basic.cpp @@ -223,3 +223,67 @@ TEST_F(CortexM3Test, CbzAndCbnzBranchWithoutTouchingStack) { EXPECT_EQ(reg(3), 9u); EXPECT_EQ(reg(13), 0x200u); } + +// ── Shift Carry flag (T1a): ARMv7-M shift instructions update C from the +// shifter carry-out. We read flags via `MRS R0, APSR` (0xF3EF 0x8000); the +// C flag is APSR bit 29. ── + +TEST_F(CortexM3Test, LslImmediateSetsCarryFlag) { + // LSLS R1, R2, #1 (0x0051): 0x80000000<<1 = 0, C = shifted-out bit31 = 1. + load_program({0x0051, 0xF3EF, 0x8000}); // LSLS R1,R2,#1 ; MRS R0,APSR + reset_cpu(); + start_cpu(); + set_reg(2, 0x80000000u); + step_cpu(); // LSLS + step_cpu(); // MRS (32-bit, one step) + EXPECT_NE(reg(0) & (1u << 29), 0u) << "LSL #1 of 0x80000000 must set C"; +} + +TEST_F(CortexM3Test, LsrImmediateSetsCarryFlag) { + // LSRS R1, R2, #1 (0x0851): 1>>1 = 0, C = shifted-out bit0 = 1. + load_program({0x0851, 0xF3EF, 0x8000}); + reset_cpu(); + start_cpu(); + set_reg(2, 1u); + step_cpu(); + step_cpu(); + EXPECT_NE(reg(0) & (1u << 29), 0u) << "LSR #1 of 1 must set C"; +} + +TEST_F(CortexM3Test, AsrBy32CarryAndSignExtend) { + // ASRS R1, R2 (0x1011, imm5=0 → ASR #32): 0x80000000 → 0xFFFFFFFF, + // C = sign bit (bit31) = 1. + load_program({0x1011, 0xF3EF, 0x8000}); + reset_cpu(); + start_cpu(); + set_reg(2, 0x80000000u); + step_cpu(); + step_cpu(); + EXPECT_EQ(reg(1), 0xFFFFFFFFu) << "ASR #32 sign-extends"; + EXPECT_NE(reg(0) & (1u << 29), 0u) << "ASR #32 C = sign bit"; +} + +TEST_F(CortexM3Test, LslByZeroLeavesCarryUnchanged) { + // LSLS R1,R2,#1 (0x0051) sets C=1; LSLS R3,R4 (0x0023, LSL #0 = MOV) + // must NOT touch C; then MRS. + load_program({0x0051, 0x0023, 0xF3EF, 0x8000}); + reset_cpu(); + start_cpu(); + set_reg(2, 0x80000000u); // first LSL forces C=1 + step_cpu(); // LSLS R1,R2,#1 → C=1 + step_cpu(); // LSLS R3,R4 (LSL #0) → C unchanged + step_cpu(); // MRS R0, APSR + EXPECT_NE(reg(0) & (1u << 29), 0u) << "LSL #0 must leave C unchanged"; +} + +TEST_F(CortexM3Test, LslRegisterSetsCarryFlag) { + // LSLS R1, R2 (register, 0x4091): R1 = R1 << R2; 0x80000000<<1 → 0, C=1. + load_program({0x4091, 0xF3EF, 0x8000}); + reset_cpu(); + start_cpu(); + set_reg(1, 0x80000000u); + set_reg(2, 1u); + step_cpu(); + step_cpu(); + EXPECT_NE(reg(0) & (1u << 29), 0u) << "register LSL must set C"; +} From 9c5d65fa32e5a6724bf6bfc81b644b6570bdd6df Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 21 Jun 2026 22:21:20 +0800 Subject: [PATCH 5/5] feat(cortex-m3): Thumb-2 instruction coverage sweep Extend Cortex-M3 Thumb-2 coverage: fix silent correctness bugs and implement base instructions that previously faulted. Silent errors fixed (computed/wrote wrong results without faulting): - register-offset load/store [Rn,Rm] computed r1+2 not r1+r2 - ORN.W reg dropped Rn; RSB.W flag operands reversed - LSR/ASR shift-by-32 left rm_val; CPSID/IE f hit PRIMASK - BKPT was a NOP; MUL.W Ra=15 folded raw PC; ADR.W used raw PC - TBH dispatch mask retained the H-bit; LDREX/STREX swallowed by STRD/LDRD Base instructions added (M3 scope): - ORN.W/MVN.W immediate; ROR/RRX shifted-register operand - SMLAL/UMLAL; LDRSB.W/LDRSH.W (sign-extend); CLZ/RBIT/REV.W family - SSAT/USAT (+ new APSR.Q bit 27); CLREX/NOP.W hints - MCR/MRC clean-fault (no coprocessor on M3) Nineteen targeted unit tests; ctest 271 green; all firmware E2E green. --- .../notes/009-thumb2-reg-offset-loadstore.md | 64 +++++ document/notes/010-thumb2-silent-bug-sweep.md | 44 +++ .../notes/011-thumb2-missing-instructions.md | 42 +++ include/arch/arm/cortex_m3/cortex_m3.hpp | 3 + include/arch/arm/cortex_m3/def.h | 1 + src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp | 32 ++- src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp | 82 ++++-- .../cortex_m3/cortex_m3_thumb32_dataproc.cpp | 157 ++++++++-- .../cortex_m3/cortex_m3_thumb32_loadstore.cpp | 155 ++++++---- test/test_cortex_m3_advanced.cpp | 271 ++++++++++++++++++ 10 files changed, 756 insertions(+), 95 deletions(-) create mode 100644 document/notes/009-thumb2-reg-offset-loadstore.md create mode 100644 document/notes/010-thumb2-silent-bug-sweep.md create mode 100644 document/notes/011-thumb2-missing-instructions.md diff --git a/document/notes/009-thumb2-reg-offset-loadstore.md b/document/notes/009-thumb2-reg-offset-loadstore.md new file mode 100644 index 0000000..c8b010b --- /dev/null +++ b/document/notes/009-thumb2-reg-offset-loadstore.md @@ -0,0 +1,64 @@ +# 009 — Thumb-2 寄存器偏移 load/store 修复(matrix §2 #9) + +> Thumb-2 全覆盖里程碑 · T1b。修 matrix §2 高危静默 bug #9(F32-8):`ldr/str.w [Rn,Rm]` 静默算错地址。 + +## 背景 + +Thumb-2 全编码覆盖矩阵([`document/ai/thumb2-coverage-matrix.md`](../ai/thumb2-coverage-matrix.md))§2 列出 11 处 🔴 高危「静默错误」——指令不算 fault,但结果悄悄算错,固件 0-fault 跑通反而掩盖了它们。T1a 已修 #1(shift Carry 不更新)。本批 T1b 修 **#9**:寄存器偏移 load/store 静默算错地址。 + +## 问题 + +`ldr.w r0,[r1,r2]`、`str.w r0,[r1,r2]`、`strb.w`、`ldrh.w` 等所有 **register-offset** 形式(编码 hw1=0xF8x1、hw2=0002 之类)被 `t32_loadstore_single` 误当成 imm8 offset+,hw2[7:0] 当成 imm8,算出 `r1+2` 而非 `r1+r2`。**不 fault**,模拟器不自知结果错。 + +``` +ldr.w r0,[r1,r2] → 期望 addr = 0x100 + 0x40 = 0x140 + → 实际 addr = 0x100 + 0x02 = 0x102 ← 静默错 +``` + +## 根因(objdump 裁定,非记忆) + +dispatch `(hw1 & 0xFF00) == 0xF800` 覆盖 0xF8xx 全空间。函数内 `(hw1>>7)&1` 区分 imm12(bit7=1)与其余(bit7=0)。问题在 bit7=0 的 else 分支: + +- 旧代码按 `op=hw2[11:8]` switch,`case 0x0` 当「`[Rn, #+imm8]`」。 +- 但 `arm-none-eabi-as` 实测:正向小立即数(`ldr.w [r1,#4]`)永远折叠进 imm12(T3,F8D1),**不会**用 imm8 offset+ 编码。所以在 bit7=0 空间,`op==0` 的**唯一**含义就是 register-offset。 +- 旧 `case 0x0` 既是死分支(无有效 imm8 offset+ 编码),又把 reg-offset 的 Rm/shift 字段当 imm8 解析 —— 双重错。 + +区分位(objdump 全形式实测裁定): + +| hw1[7] | hw2[11:8]=op | 形式 | +|--------|------|------| +| 1 | — | imm12(T3):`addr = Rn + imm12` | +| 0 | 0x0 | **register-offset**(T2):`addr = Rn + (Rm << shift2)` | +| 0 | C / B / 9 / F / D | imm8 addressing modes(T4):off- / post± / pre± | +| 0 | 其他 | IllegalInstruction | + +reg-offset 字段:`Rt=hw2[15:12]`、`shift=hw2[5:4]`(LSL 0–3)、`Rm=hw2[3:0]`,无 writeback。 + +## 修复 + +[`src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp`](../../src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp) `t32_loadstore_single`: + +1. **base 对齐下沉**:Rn=15 的 `Align(PC+4,4)` 从「literal 专享特判」下沉为通用 base —— reg-offset 以 PC 为基(`ldr.w r0,[pc,r1]`)也正确受益。 +2. **else 分支 carve out**:`op==0` 单独走 register-offset(`addr = base + (rr(rm) << shift)`);imm8 switch 删除无效的 `case 0x0`。 +3. store-to-PC-relative(imm12 Rn=15 & !load)拒绝,语义不变。 + +## 测试 + +两个新单测([`test/test_cortex_m3_advanced.cpp`](../../test/test_cortex_m3_advanced.cpp)): + +- `LoadStoreWideRegisterOffset`:`str.w [r1,r2]` 断言写到 **0x140**(+ regression guard 读 0x102 应无该值);`ldr.w [r1,r2,lsl#3]` 断言读 **0x300**(shift 是关键,bug 下会读 0x132)。 +- `LoadStoreWideRegisterOffsetByteHalf`:`strb.w [r1,r2]`(byte→0x110)、`ldrh.w [r1,r3]`(half←0x140)。 + +**关键设计**:断言**具体地址**(`bus_.read(addr, Width)`),不用 roundtrip —— roundtrip 的 str/ldr 用同一(错误)地址会互相抵消、掩盖 bug。这正是 #9 长期没被现有测试抓到的原因(`LoadStoreWideWordAndHalfwordImmediateOffsets` 就是 roundtrip)。 + +## 验证 + +- 全部编码经 `arm-none-eabi-as -mcpu=cortex-m3` + `objdump -d` 核验。 +- `ctest --test-dir build` 全量 **254 绿**(原 252 + 新增 2),无回归。 +- `cmake --build build -j$(nproc)` 全量编译绿。 + +## 陷阱 / 未竟 + +- **LDRSB.W / LDRSH.W(0xF9xx)仍未 dispatch**(matrix §3 缺失):`(hw1&0xFF00)==0xF800` 不匹配 0xF9xx,fallthrough → IllegalInstruction。这是整个 0xF9xx 空间缺失,范围更大,**另起一批**,本批不动。 +- matrix 引用的代码行号是 T0 拆分前的(`cortex_m3_thumb32.cpp:366-474`),拆分后实际落在 `cortex_m3_thumb32_loadstore.cpp`;不影响 #9 的判断与修复,行号全量更新属另一清理任务。 +- §2 剩余高危:#2 ORN.W 丢 Rn、#3 RSB 标志错、#4 LSR#0/ASR#0、#10 TBH→LDRD、#11 LDREX/STREX→STRD/LDRD —— 下一批候选。 diff --git a/document/notes/010-thumb2-silent-bug-sweep.md b/document/notes/010-thumb2-silent-bug-sweep.md new file mode 100644 index 0000000..b18541d --- /dev/null +++ b/document/notes/010-thumb2-silent-bug-sweep.md @@ -0,0 +1,44 @@ +# 010 — Thumb-2 §2 高危静默错误清零(T1c) + +> Thumb-2 全覆盖里程碑 · T1c。继 T1a(shift Carry)、T1b(reg-offset load/store,notes 009)之后,清零 matrix §2 剩余 9 处静默错误(#2–#8、#10、#11)。**§2 高危 11/11 全部修复**。 + +## 背景 + +matrix [`document/ai/thumb2-coverage-matrix.md`](../ai/thumb2-coverage-matrix.md) §2 列出 11 处 🔴「静默错误」——指令不 fault,但结果悄悄算错/写错。本批一口气修完剩余 9 处,每条配 objdump 核验 + 针对性单测。 + +## 修复(逐条 + 修复点) + +| # | bug | 修复 | +|---|-----|------| +| 2 | ORN.W reg 丢 Rn | `t32_dataproc_reg` op=3:`~shifted` → `Rn \| ~shifted`(Rn=15 退化为 MVN)。 | +| 3 | RSB.W 标志颠倒 | RSB(op=14)被减数是 shift 操作数;reg 与 imm 两处 `update_flags` 改传 `(Sub, shift-op, Rn)`。 | +| 4 | LSR/ASR shift-by-32 | `dataproc_reg` imm3:imm2==0:LSR→0、ASR→符号扩展(原返回 rm_val 不变)。 | +| 5 | CPSID/IE f 拨错寄存器 | `0xFFF0` 掩码忽略 bit[0] → `0xFFE0` 掩码 + bit4(E/D)/bit1(i)/bit0(f) 分发,FAULTMASK 正确。 | +| 6 | BKPT 静默 NOP | `0xBExx` 落 hints 的 NOP 兜底 → `trigger_hardfault()`(vector 3)。 | +| 7 | MUL.W Ra=15 叠 raw PC | MLA/MLS 块:`rr(15)` 加进乘积 → Ra=15 视作「无累加」(acc=0)。 | +| 8 | ADR.W off-by-2 | `t32_addsub_plain_imm`:ADDW/SUBW Rn=PC 用 `rr(15)`(raw PC)→ `Align(PC+4,4)`。 | +| 10 | TBH 掩码漏 H 位 | TBB/TBH dispatch hw2 掩码 `0xF0F0` 检查了 bit4(H)→ 改 `0xFFE0`(放开 [4:0]=H+Rm)。 | +| 11 | LDREX/STREX 撞 STRD/LDRD | 新 `t32_ldrex_strex`,mask `0xFF60==0xE840`(exclusive 空间 P=0&W=0;STRD/LDRD 必有 P 或 W,故不撞);单核 sim 简化为普通 LD/ST(STREX 总成功 Rd=0)。 | + +## 关键陷阱:#10 mask 首版写错,E2E 抓到 + +#10 第一版把 hw2 掩码写成了 `0xFF0F`(检查 hw2[3:0])。但 **TBB 的 Rm 字段就在 hw2[3:0]**。gcc hal_uart 固件的 `tbb [pc,r4]`(hw2=`0xF004`,Rm=4≠0)因此**不匹配** TBB/TBH dispatch → fall through 到新加的 #11 LDREX dispatch(`0xE8DF & 0xFF60 == 0xE840`)→ tbb 被当 LDREX(load,Rd=hw2[15:12]=15)→ `wr(15, 读到的字节)` → **PC 跳飞到 GPIOA(0x40010804)**。 + +`E2E.HalUartTransmit` 以 `InstructionFetchFault` 抓到(PC 跑到外设区)。反汇编 hal_uart 定位到 `tbb [pc,r4]`,才锁定是 #10 mask 而非 #11。正确 mask 是 `0xFFE0`(检查 [15:5],放开 [4:0]=H+Rm)。 + +**教训**: +- mask 改动必须 objdump 验证所有变体,尤其 Rm≠0 的 TBB(之前只验证了 `tbh [pc,r0]` 这种 Rm=0 的)。 +- 新加的「前置 dispatch mask」(LDREX)要确认不吞掉共享 hw1 空间的其它指令——TBB/TBH/LDREXB/H 同居 `0xE8Dx`。**dispatch 顺序(TBB/TBH → LDREX → STRD/LDRD)是 load-bearing**。 +- 间接测试(roundtrip / 0-fault 固件)掩盖静默错;**针对性单测(断言具体值/地址/标志)才是安全网**。这次正是 hal_uart E2E 救了场。 + +## 验证 + +- `ctest` 全量 **263/263 绿**(254 + 9 新单测,每条 bug 一个针对性断言:`test_cortex_m3_advanced.cpp`)。 +- 全部编码 `arm-none-eabi-as -mcpu=cortex-m3` + `objdump -d` 核验。 +- 固件 E2E(3 份 AC6 + gcc hal_uart)全绿,证明修复不破坏真实固件启动(且 hal_uart 的 tbb 反向验证了 #10)。 + +## 未竟(下一里程碑 T2) + +- §3 缺失指令(M3 范围内):**LDRSB.W/LDRSH.W(0xF9xx 整族)**、ORN.W/MVN.W imm、ROR(shifted-reg)+RRX、SMLAL/UMLAL(长乘累加)、CLZ/RBIT/REV.W 族、SSAT/USAT(+Q 标志)、CLREX/NOP.W hint 族、MCR/MRC 策略。 +- §4 作用域门禁(ARMv7E-M DSP 指令 clean-fault 验证)、§5 测试缺口 sweep。 +- 行 255 SBFX/UBFX dispatch 的 `|| (hw1&0xFB70)==0xF3C0` 是 tautological 死代码(pre-existing,clangd 报但 gcc 不报,功能靠 `is_unsigned` 正确)——可顺手清,非阻塞。 diff --git a/document/notes/011-thumb2-missing-instructions.md b/document/notes/011-thumb2-missing-instructions.md new file mode 100644 index 0000000..0f276be --- /dev/null +++ b/document/notes/011-thumb2-missing-instructions.md @@ -0,0 +1,42 @@ +# 011 — Thumb-2 §3 缺失指令补全(T2) + +> Thumb-2 全覆盖里程碑 · T2。补全 matrix §3「M3 范围内缺失指令」。继 T1(§2 静默错误 11/11 清零)之后,把模拟器从「能跑现有固件」推向「覆盖 ARMv7-M base 指令集」。全部 objdump 核验 + 单测,**ctest 271/271 绿**。 + +## 新增指令 + +| 指令 | 实现 | +|------|------| +| ORN.W / MVN.W imm | `dataproc_imm` 加 case 3:`Rn \| ~imm32`(Rn=15 退化为 MVN)。逻辑标志走 update_nz。 | +| ROR / RRX(shifted-reg operand) | `dataproc_reg` shift 加 case 3:`shift_n==0` → RRX(`(C<<31)\|(Rm>>1)`,读 PSR_C);否则 ROR by n。 | +| SMLAL / UMLAL | 扩长乘块(0xFBC0/0xFBE0):`RdHi:RdLo += signed/unsigned(Rn*Rm)`,read-before-write 累加。 | +| LDRSB.W / LDRSH.W | dispatch mask `0xFF00→0xFE00`(含 0xF9xx);handler `hw1[8]` 判 sign,load 后 byte/half sign-extend。sign store → IllegalInstruction。 | +| CLZ / RBIT / REV.W / REV16.W / REVSH.W | 新 `t32_misc_reverse`:CLZ=`std::countl_zero`,RBIT=位反转,REV/REV16/REVSH 复用 16 位族逻辑。CLZ vs REV.W 用 `hw1[7:4]`(0xB vs 0x9)区分。 | +| SSAT / USAT | 新 `t32_ssat_usat`:饱和到有/无符号范围,越界写 **APSR.Q**。sat 宽度 `hw2[4:0]`(SSAT +1),shift imm5=`(hw2[14:12]<<2)\|hw2[7:6]`。 | +| CLREX / NOP.W / YIELD.W / SEV.W | barrier handler 加 op=2(CLREX);新 `hw1==0xF3AF` handler(hints 全 no-op)。 | +| MCR / MRC | 无 handler → fall through 末尾 IllegalInstruction(架构上应 NoCoproc UsageFault;CPUError 无 NoCoproc,IllegalInstruction 为合理 clean fault)。M3 无协处理器,两份固件 0 命中。 | + +## 关键设计点 + +- **PSR_Q(bit27)**:新增(def.h),APSR 的 MRS/MSR(sysm 0x00)读写含 Q,SSAT/USAT 饱和时置位。 +- **dispatch 顺序 load-bearing**(延续 T1c #10 教训): + - SSAT/USAT(`0xF3xx`)必须**早于** dataproc-imm —— 否则 SSAT `0xF301` 命中 `(hw1&0xF800)==0xF000` 被当 ADD-imm。mask `0xFFD0` 放开 hw1[5](shift type)。 + - CLZ/RBIT/REV(`0xFA00` with op2≠0)在 shift_reg 之后 —— shift_reg 只收 op2==0。 + - LDRSB/SH 的 `0xFE00` mask 含 0xF8xx(无符号)+ 0xF9xx(符号)。 + - MCR/MRC(`0xEExx`)不匹配任何前置 dispatch,末尾兜底。 +- **字段位**(objdump 权威):`mov.w r3,r1,rrx = ea4f 0331` —— **Rd 在 hw2[11:8]、imm3 在 hw2[14:12] 是独立字段**(不是 hw2[15:12])。`usat r2,#5,r1 = f381 0205`(Rd=hw2[11:8])。这类掩码位错配是 T1c #10 的根因,本次全部 objdump 确认到位,一次通过。 + +## 验证 + +- `ctest` 全量 **271/271 绿**(263 + 8 新单测,`test_cortex_m3_advanced.cpp`)。 +- 全部编码 `arm-none-eabi-as -mcpu=cortex-m3` + `objdump -d` 核验。 +- 固件 E2E(3 AC6 + gcc hal_uart)全绿,不破坏真实固件启动。 + +## 未做(策略性) + +- **BLX imm(T1)**:现 IllegalInstruction,M3 无 ARM 态,正确 fault(matrix §3:「现即可,补语义说明」)。 +- **ARMv7E-M DSP**(QADD/QSUB/QDADD/QDSUB、PKHBT/PKHTB、SEL、SXTAH/UXTAH/SXTB16、UMAAL、SMLAD 族、USAD8 族):`arm-none-eabi-as -mcpu=cortex-m3` 拒绝 = M3 没有,保持 fault(matrix §4,作用域外)。需验证它们 clean-fault(不误解码进现有 handler)——这是 T3(§4 门禁)。 +- **T3/§4**:作用域外指令的 clean-fault 验证;**T4/§5**:测试缺口 sweep(post-index、LDRD 全模式、flag sweep)。 + +## 成果 + +matrix §3 缺失指令基本补全(M3 范围内),模拟器指令覆盖从「够跑现有固件」提升到「ARMv7-M base 指令集覆盖」。剩余 §4(作用域门禁)+ §5(测试缺口)为收尾验证类工作。 diff --git a/include/arch/arm/cortex_m3/cortex_m3.hpp b/include/arch/arm/cortex_m3/cortex_m3.hpp index 9f0b4e7..77e60c1 100644 --- a/include/arch/arm/cortex_m3/cortex_m3.hpp +++ b/include/arch/arm/cortex_m3/cortex_m3.hpp @@ -65,8 +65,11 @@ class CortexM3CPU : public CPU { CPUExpected t32_addsub_plain_imm(uint16_t hw1, uint16_t hw2); CPUExpected t32_dataproc_imm(uint16_t hw1, uint16_t hw2); CPUExpected t32_dataproc_reg(uint16_t hw1, uint16_t hw2); + CPUExpected t32_misc_reverse(uint16_t hw1, uint16_t hw2); + CPUExpected t32_ssat_usat(uint16_t hw1, uint16_t hw2); CPUExpected t32_shift_reg(uint16_t hw1, uint16_t hw2); CPUExpected t32_loadstore_single(uint16_t hw1, uint16_t hw2); + CPUExpected t32_ldrex_strex(uint16_t hw1, uint16_t hw2); CPUExpected t32_tbb_tbh(uint16_t hw1, uint16_t hw2); CPUExpected t32_strd_ldrd(uint16_t hw1, uint16_t hw2); CPUExpected t32_stm_ldm(uint16_t hw1, uint16_t hw2); diff --git a/include/arch/arm/cortex_m3/def.h b/include/arch/arm/cortex_m3/def.h index a66f49d..2a72a64 100644 --- a/include/arch/arm/cortex_m3/def.h +++ b/include/arch/arm/cortex_m3/def.h @@ -10,6 +10,7 @@ static constexpr data_t PSR_N = 1u << 31; static constexpr data_t PSR_Z = 1u << 30; static constexpr data_t PSR_C = 1u << 29; static constexpr data_t PSR_V = 1u << 28; +static constexpr data_t PSR_Q = 1u << 27; static constexpr data_t PSR_T = 1u << 24; static constexpr uint16_t REGCNT = 16; diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp index a6ee4f7..7f902ca 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb16.cpp @@ -49,13 +49,26 @@ CPU::CPUExpected CortexM3CPU::execute_16bit(uint16_t insn) { return {}; }; - // ── CPSIE i / CPSID i ── - if ((insn & 0xFFF0u) == 0xB660u) { - primask_ &= ~1u; - return {}; - } - if ((insn & 0xFFF0u) == 0xB670u) { - primask_ |= 1u; + // ── CPS effect {i,f}: CPSIE (enable) / CPSID (disable) ── + // 0xB66x (CPSIE) / 0xB67x (CPSID); bit4 = 0/1 (enable/disable), + // bit1 = i (PRIMASK), bit0 = f (FAULTMASK). The old 0xFFF0 mask ignored + // bit[1:0], so cpsie/cpsid f silently acted on PRIMASK, not FAULTMASK. + if ((insn & 0xFFE0u) == 0xB660u) { + bool disable = (insn >> 4) & 1u; + if (insn & 0x2u) { // i → PRIMASK + if (disable) { + primask_ |= 1u; + } else { + primask_ &= ~1u; + } + } + if (insn & 0x1u) { // f → FAULTMASK + if (disable) { + faultmask_ |= 1u; + } else { + faultmask_ &= ~1u; + } + } return {}; } @@ -550,6 +563,11 @@ CPU::CPUExpected CortexM3CPU::execute_16bit(uint16_t insn) { case 0b10111: { uint8_t sub_op = (insn >> 9) & 0x3; if (sub_op == 0b11) { + // BKPT #imm8 (0xBExx): no debugger attached → HardFault. + // (Was silently treated as NOP — coverage matrix §2 #6.) + if ((insn & 0xFF00u) == 0xBE00u) { + return trigger_hardfault(); + } if ((insn & 0xFF00u) == 0xBF00u && (insn & 0xFu) != 0) { uint8_t first_cond = (insn >> 4) & 0xFu; uint8_t mask = insn & 0xFu; diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp index 63dd265..3a820f5 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb32.cpp @@ -59,7 +59,7 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { uint8_t sysm = hw2 & 0xFFu; switch (sysm) { case 0x00: - return xpsr_ & (PSR_N | PSR_Z | PSR_C | PSR_V); + return xpsr_ & (PSR_N | PSR_Z | PSR_C | PSR_V | PSR_Q); case 0x08: return msp_; case 0x09: @@ -81,8 +81,9 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { uint8_t sysm = hw2 & 0xFFu; switch (sysm) { case 0x00: - xpsr_ = (xpsr_ & ~(PSR_N | PSR_Z | PSR_C | PSR_V)) | - (value & (PSR_N | PSR_Z | PSR_C | PSR_V)) | PSR_T; + xpsr_ = (xpsr_ & ~(PSR_N | PSR_Z | PSR_C | PSR_V | PSR_Q)) | + (value & (PSR_N | PSR_Z | PSR_C | PSR_V | PSR_Q)) | + PSR_T; return {}; case 0x08: msp_ = value & ~0x3u; @@ -215,16 +216,23 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { return wr(rd, val); } - // ── DMB / DSB / ISB ── + // ── DMB / DSB / ISB / CLREX ── if (hw1 == 0xF3BF && (hw2 & 0xFF0Fu) == 0x8F0Fu) { uint8_t option = hw2 & 0xFu; uint8_t op = (hw2 >> 4) & 0xFu; - if (option != 0xFu || (op != 0x4u && op != 0x5u && op != 0x6u)) { + // op=4 DSB, 5 DMB, 6 ISB; op=2 CLREX (no-op on a single-core sim). + if (option != 0xFu || + (op != 0x2u && op != 0x4u && op != 0x5u && op != 0x6u)) { return std::unexpected{CPUError::IllegalInstruction}; } return {}; } + // ── NOP.W / YIELD.W / SEV.W (T4 hints, f3af 80xx) ── no-op on this sim. + if (hw1 == 0xF3AF && (hw2 & 0xF000u) == 0x8000u) { + return {}; + } + // ── MRS ── if ((hw1 & 0xFFF0) == 0xF3E0 && (hw2 & 0xF000) == 0x8000) { return wr(thumb32::hw2_rd4(hw2), read_special()); @@ -270,6 +278,13 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { return wr(rd, result); } + // ── SSAT / USAT (saturate; writes APSR.Q) ── + // Must precede dataproc-imm, which also matches 0xF3xx (insn[25]=0) and + // would misread SSAT as ADD-imm. mask 0xFFD0 frees hw1[5] (shift type). + if ((hw1 & 0xFFD0u) == 0xF300u || (hw1 & 0xFFD0u) == 0xF380u) { + return t32_ssat_usat(hw1, hw2); + } + // ── Add/subtract (plain imm12): insn[25]=1 (hw1[9]) ── if ((hw1 & 0xF800) == 0xF000 && (hw1 & 0x0200) != 0 && (hw2 & 0x8000) == 0) { @@ -283,7 +298,9 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { } // ── Load/Store single (immediate): str/ldr/strb/ldrb/strh/ldrh .W ── - if ((hw1 & 0xFF00) == 0xF800) { + // 0xFE00 mask covers both 0xF8xx (unsigned str/ldr/b/h) and 0xF9xx + // (signed LDRSB.W/LDRSH.W); the handler sign-extends the 0xF9xx forms. + if ((hw1 & 0xFE00) == 0xF800) { return t32_loadstore_single(hw1, hw2); } @@ -317,27 +334,35 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { uint8_t rd = (hw2 >> 8) & 0xFu; uint8_t ra = (hw2 >> 12) & 0xFu; uint32_t product = rr(rn) * rr(rm); - uint32_t result = - (hw2 & 0x0010u) ? (rr(ra) - product) : (product + rr(ra)); + // MUL.W (Ra=15) is MLA/MLS without an accumulator; rr(15) would fold + // the raw PC into the product. Treat Ra=15 as "no accumulate". + uint32_t acc = (ra == 15) ? 0u : rr(ra); + uint32_t result = (hw2 & 0x0010u) ? (acc - product) : (product + acc); return wr(rd, result); } - // ── SMULL / UMULL ── - if (((hw1 & 0xFFF0u) == 0xFB80u || (hw1 & 0xFFF0u) == 0xFBA0u) && + // ── SMULL/UMULL (no accumulate) and SMLAL/UMLAL (accumulate) ── + uint16_t mp_hw1 = hw1 & 0xFFF0u; + if ((mp_hw1 == 0xFB80u || mp_hw1 == 0xFBA0u || + mp_hw1 == 0xFBC0u || mp_hw1 == 0xFBE0u) && (hw2 & 0x00F0u) == 0x0000u) { uint8_t rn = hw1 & 0xFu; uint8_t rm = hw2 & 0xFu; uint8_t rdlo = (hw2 >> 12) & 0xFu; uint8_t rdhi = (hw2 >> 8) & 0xFu; - uint64_t result; - if ((hw1 & 0xFFF0u) == 0xFB80u) { - result = static_cast( - static_cast(static_cast(rr(rn))) * - static_cast(static_cast(rr(rm)))); - } else { - result = - static_cast(rr(rn)) * static_cast(rr(rm)); - } + bool accumulate = (mp_hw1 == 0xFBC0u || mp_hw1 == 0xFBE0u); + bool is_signed = (mp_hw1 == 0xFB80u || mp_hw1 == 0xFBC0u); + uint64_t product = + is_signed + ? static_cast( + static_cast(static_cast(rr(rn))) * + static_cast(static_cast(rr(rm)))) + : static_cast(rr(rn)) * static_cast(rr(rm)); + // SMLAL/UMLAL accumulate the existing RdHi:RdLo (read before write). + uint64_t result = accumulate + ? product + (static_cast(rr(rdhi)) << 32) + + rr(rdlo) + : product; auto lo = wr(rdlo, static_cast(result)); if (!lo) { return lo; @@ -355,11 +380,28 @@ CPU::CPUExpected CortexM3CPU::execute_32bit(uint16_t hw1, uint16_t hw2) { return t32_shift_reg(hw1, hw2); } + // ── CLZ / RBIT / REV.W / REV16.W / REVSH.W ── + // 0xFA00 with op2 (hw2[7:4]) != 0; shift_reg above already took op2==0. + if ((hw1 & 0xFF00u) == 0xFA00u && (hw2 & 0xF000u) == 0xF000u && + (hw2 & 0x00F0u) != 0u) { + return t32_misc_reverse(hw1, hw2); + } + // ── TBB / TBH (Table Branch) ── - if ((hw1 & 0xFFF0) == 0xE8D0 && (hw2 & 0xF0F0) == 0xF000) { + // hw2 mask 0xFFE0 (not 0xF0F0) frees hw2[4:0] — TBH's H-bit at [4] and + // Rm at [3:0] — so TBH (0xF010) and TBB with Rm≠0 (e.g. 0xF004) are not + // disqualified; otherwise they fall through to STRD/LDRD or LDREX. + if ((hw1 & 0xFFF0) == 0xE8D0 && (hw2 & 0xFFE0) == 0xF000) { return t32_tbb_tbh(hw1, hw2); } + // ── LDREX / STREX (+B/+H) — single-core sim, no exclusive monitor ── + // Plain LD; STREX always reports success (Rd=0). Must precede the + // STRD/LDRD mask (0xFE40==0xE840), which otherwise swallows them. + if ((hw1 & 0xFF60u) == 0xE840u) { + return t32_ldrex_strex(hw1, hw2); + } + // ── STRD / LDRD (Store/Load Dual, immediate offset) ── if ((hw1 & 0xFE40) == 0xE840) { return t32_strd_ldrd(hw1, hw2); diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp index 2da2ddb..b679aa7 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb32_dataproc.cpp @@ -19,7 +19,17 @@ CPU::CPUExpected CortexM3CPU::t32_addsub_plain_imm(uint16_t hw1, uint8_t rd = (hw2 >> 12) & 0xF; uint32_t imm12 = (((hw1 >> 10) & 0x1u) << 11) | (((hw2 >> 12) & 0x7u) << 8) | (hw2 & 0xFFu); - uint32_t a = rr(rn); + uint32_t a; + if (rn == 15) { + // ADDW/SUBW with Rn=PC is ADR.W: base = Align(PC+4,4), not raw PC. + auto pc_res = read_pc_raw(); + if (!pc_res) { + return std::unexpected{pc_res.error()}; + } + a = (*pc_res + 4) & ~0x3u; + } else { + a = rr(rn); + } uint32_t result; bool is_sub; switch (op) { @@ -64,6 +74,9 @@ CPU::CPUExpected CortexM3CPU::t32_dataproc_imm(uint16_t hw1, uint16_t hw2) case 2: result = (rn == 15) ? imm32 : (rn_val | imm32); break; // ORR/MOV + case 3: + result = (rn == 15) ? ~imm32 : (rn_val | ~imm32); + break; // ORN/MVN case 4: result = rn_val ^ imm32; break; // EOR @@ -89,14 +102,18 @@ CPU::CPUExpected CortexM3CPU::t32_dataproc_imm(uint16_t hw1, uint16_t hw2) } if (s_bit) { - if (op2 == 8 || op2 == 10 || op2 == 13 || op2 == 14 || op2 == 11) { - uint32_t flag_rhs = - op2 == 10 ? imm32 + ((xpsr_ & PSR_C) ? 1u : 0u) - : op2 == 11 ? imm32 + ((xpsr_ & PSR_C) ? 0u : 1u) - : imm32; - update_flags(op2 <= 10 ? FlagPostOperation::Add - : FlagPostOperation::Sub, - rn_val, flag_rhs, result); + if (op2 == 8) { // ADD + update_flags(FlagPostOperation::Add, rn_val, imm32, result); + } else if (op2 == 10) { // ADC + update_flags(FlagPostOperation::Add, rn_val, + imm32 + ((xpsr_ & PSR_C) ? 1u : 0u), result); + } else if (op2 == 11) { // SBC = rn - imm - !C + update_flags(FlagPostOperation::Sub, rn_val, + imm32 + ((xpsr_ & PSR_C) ? 0u : 1u), result); + } else if (op2 == 13) { // SUB = rn - imm + update_flags(FlagPostOperation::Sub, rn_val, imm32, result); + } else if (op2 == 14) { // RSB = imm - rn; minuend is the immediate. + update_flags(FlagPostOperation::Sub, imm32, rn_val, result); } else { update_nz(result); } @@ -128,12 +145,12 @@ CPU::CPUExpected CortexM3CPU::t32_dataproc_reg(uint16_t hw1, uint16_t hw2) case 0: shifted = shift_n == 0 ? rm_val : rm_val << shift_n; break; - case 1: - shifted = rm_val >> (shift_n == 0 ? 0 : shift_n); + case 1: // LSR; imm3:imm2==0 means shift-by-32 → result 0. + shifted = shift_n == 0 ? 0u : (rm_val >> shift_n); break; - case 2: { + case 2: { // ASR; imm3:imm2==0 means shift-by-32 → sign-extend. if (shift_n == 0) { - shifted = rm_val; + shifted = (rm_val & 0x80000000u) ? 0xFFFFFFFFu : 0u; } else { uint32_t sign = rm_val & 0x80000000u; shifted = rm_val >> shift_n; @@ -143,6 +160,16 @@ CPU::CPUExpected CortexM3CPU::t32_dataproc_reg(uint16_t hw1, uint16_t hw2) } break; } + case 3: { // ROR; shift_n==0 means RRX (rotate-right-extend via C). + if (shift_n == 0) { + bool carry_in = (xpsr_ & PSR_C) != 0; + shifted = (carry_in ? 0x80000000u : 0u) | (rm_val >> 1); + } else { + uint8_t n = shift_n & 0x1Fu; + shifted = (rm_val >> n) | (rm_val << (32 - n)); + } + break; + } default: return std::unexpected{CPUError::IllegalInstruction}; } @@ -159,8 +186,8 @@ CPU::CPUExpected CortexM3CPU::t32_dataproc_reg(uint16_t hw1, uint16_t hw2) case 2: result = (rn == 15) ? shifted : (rn_val | shifted); break; - case 3: - result = ~shifted; + case 3: // ORN = Rn | ~shifted; Rn=15 collapses to MVN (~shifted). + result = (rn == 15) ? ~shifted : (rn_val | ~shifted); break; case 4: result = rn_val ^ shifted; @@ -179,10 +206,12 @@ CPU::CPUExpected CortexM3CPU::t32_dataproc_reg(uint16_t hw1, uint16_t hw2) } if (s_bit) { - if (op == 8 || op == 13 || op == 14) { - update_flags(op <= 8 ? FlagPostOperation::Add - : FlagPostOperation::Sub, - rn_val, shifted, result); + if (op == 8) { // ADD + update_flags(FlagPostOperation::Add, rn_val, shifted, result); + } else if (op == 13) { // SUB = rn - shifted + update_flags(FlagPostOperation::Sub, rn_val, shifted, result); + } else if (op == 14) { // RSB = shifted - rn; minuend is the operand. + update_flags(FlagPostOperation::Sub, shifted, rn_val, result); } else { update_nz(result); } @@ -223,4 +252,94 @@ CPU::CPUExpected CortexM3CPU::t32_shift_reg(uint16_t hw1, uint16_t hw2) { return {}; } +// ── CLZ / RBIT / REV.W / REV16.W / REVSH.W ── +// Dispatched when (hw1 & 0xFF00)==0xFA00 && (hw2 & 0xF000)==0xF000 && +// (hw2 & 0x00F0)!=0 (op2 present; shift_reg handles op2==0). +CPU::CPUExpected CortexM3CPU::t32_misc_reverse(uint16_t hw1, + uint16_t hw2) { + uint8_t rd = (hw2 >> 8) & 0xFu; + uint8_t rn = hw1 & 0xFu; + uint8_t op2 = (hw2 >> 4) & 0xFu; + uint32_t v = rr(rn); + + // CLZ (hw1[7:4]=0xB, op2=8) vs REV.W (hw1[7:4]=0x9, op2=8). + if (op2 == 0x8u && (hw1 & 0x00F0u) == 0x00B0u) { + return wr(rd, std::countl_zero(v)); + } + uint32_t result; + switch (op2) { + case 0x8u: // REV.W — byte-reverse (same result as 16-bit REV) + result = ((v & 0x000000FFu) << 24) | ((v & 0x0000FF00u) << 8) | + ((v & 0x00FF0000u) >> 8) | ((v & 0xFF000000u) >> 24); + break; + case 0x9u: // REV16.W — two halfword byte-swaps + result = ((v & 0x00FF00FFu) << 8) | ((v & 0xFF00FF00u) >> 8); + break; + case 0xAu: { // RBIT — bit-reverse all 32 bits + v = ((v & 0x55555555u) << 1) | ((v & 0xAAAAAAAAu) >> 1); + v = ((v & 0x33333333u) << 2) | ((v & 0xCCCCCCCCu) >> 2); + v = ((v & 0x0F0F0F0Fu) << 4) | ((v & 0xF0F0F0F0u) >> 4); + v = ((v & 0x00FF00FFu) << 8) | ((v & 0xFF00FF00u) >> 8); + result = (v << 16) | (v >> 16); + break; + } + case 0xBu: { // REVSH.W — sign-extend low halfword byte-swap + uint32_t r = ((v & 0x00FFu) << 8) | ((v & 0xFF00u) >> 8); + result = static_cast(static_cast( + static_cast(r & 0xFFFFu))); + break; + } + default: + return std::unexpected{CPUError::IllegalInstruction}; + } + return wr(rd, result); +} + +// ── SSAT / USAT (saturate; writes APSR.Q on saturation) ── +// Dispatched when (hw1 & 0xFFD0)==0xF300 (SSAT) / 0xF380 (USAT). +CPU::CPUExpected CortexM3CPU::t32_ssat_usat(uint16_t hw1, uint16_t hw2) { + bool is_usat = (hw1 & 0x0080u) != 0u; // bit7: SSAT=0, USAT=1 + bool asr = (hw1 & 0x0020u) != 0u; // bit5: shift type (1=asr, 0=lsl) + uint8_t rn = hw1 & 0xFu; + uint8_t rd = (hw2 >> 8) & 0xFu; + uint8_t field = hw2 & 0x1Fu; // sat width field + uint8_t imm3 = (hw2 >> 12) & 0x7u; + uint8_t imm2 = (hw2 >> 6) & 0x3u; + uint8_t shift = (imm3 << 2) | imm2; + + int32_t val = static_cast(rr(rn)); + val = asr ? (val >> shift) + : static_cast(static_cast(val) << shift); + int64_t v = val; + + if (is_usat) { + int64_t hi = (1ll << field) - 1; // USAT range [0, 2^field - 1] + uint32_t result; + if (v < 0) { + result = 0u; + xpsr_ |= PSR_Q; + } else if (v > hi) { + result = static_cast(hi); + xpsr_ |= PSR_Q; + } else { + result = static_cast(v); + } + return wr(rd, result); + } + // SSAT range [-2^(field), 2^(field) - 1] (sat = field + 1). + int64_t lo = -(1ll << field); + int64_t hi = (1ll << field) - 1; + uint32_t result; + if (v < lo) { + result = static_cast(static_cast(lo)); + xpsr_ |= PSR_Q; + } else if (v > hi) { + result = static_cast(hi); + xpsr_ |= PSR_Q; + } else { + result = static_cast(v); + } + return wr(rd, result); +} + } // namespace micro_forge::cpu::arm::cortex_m3 diff --git a/src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp b/src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp index b20d74b..e1a7803 100644 --- a/src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp +++ b/src/arch/arm/cortex_m3/cortex_m3_thumb32_loadstore.cpp @@ -20,7 +20,6 @@ CPU::CPUExpected CortexM3CPU::t32_loadstore_single(uint16_t hw1, bool load = (hw1 >> 4) & 1; uint8_t size = (hw1 >> 5) & 0x3; uint8_t rt = (hw2 >> 12) & 0xF; - uint32_t rn_val = rr(rn); Width width; switch (size) { case 0: @@ -36,66 +35,79 @@ CPU::CPUExpected CortexM3CPU::t32_loadstore_single(uint16_t hw1, return std::unexpected{CPUError::IllegalInstruction}; } - // ── LDR.W (literal): Rn == PC ── - // `ldr.w Rt, [pc, #imm12]` — PC-relative literal pool load (compiled - // `LDR Rd, =const`). addr = Align(PC+4, 4) + imm12, no writeback. - // Store-to-PC-relative is UNDEFINED → rejected for !load. + // hw1[8]=1 selects the 0xF9xx signed forms (LDRSB.W/LDRSH.W); the load + // path sign-extends. Signed stores do not exist. + bool sign = (hw1 >> 8) & 1; + if (sign && !load) { + return std::unexpected{CPUError::IllegalInstruction}; + } + + // Effective base register value. Rn=PC uses Align(PC+4,4) — covers both + // `ldr.w Rt, [pc, #imm12]` (literal pool, compiled `LDR Rd, =const`) and + // PC-relative register/imm8 forms. Store-to-PC-relative is UNDEFINED. + uint32_t base; if (rn == 15) { - if (!load) { - return std::unexpected{CPUError::IllegalInstruction}; - } - uint32_t imm12 = hw2 & 0xFFFu; auto pc_res = read_pc_raw(); if (!pc_res) { return std::unexpected{pc_res.error()}; } - addr_t addr = ((*pc_res + 4) & ~0x3u) + imm12; - auto r = br(addr, width); - if (!r) { - return std::unexpected{r.error()}; - } - return wr(rt, *r); + base = (*pc_res + 4) & ~0x3u; + } else { + base = rr(rn); } - // Resolve effective address + optional writeback per immediate form. + // Resolve effective address + optional writeback per form. + // hw1[7]=1 → imm12 offset (T2/T3), no writeback. + // hw1[7]=0, hw2[11:8]=0 → register offset [Rn, Rm, LSL #imm2] (T2). + // hw1[7]=0, hw2[11:8]≠0 → imm8 addressing modes (T4): C/B/9/F/D. + // (Per objdump: a positive small offset always folds into imm12, so the + // hw2[11:8]=0 slot is exclusively the register-offset form.) addr_t addr = 0; bool writeback = false; data_t wb_val = 0; if ((hw1 >> 7) & 1) { - // imm12 offset form (no writeback). - addr = rn_val + (hw2 & 0xFFFu); + // imm12 offset form (no writeback). Store-to-PC-relative is UNDEFINED. + if (rn == 15 && !load) { + return std::unexpected{CPUError::IllegalInstruction}; + } + addr = base + (hw2 & 0xFFFu); } else { uint8_t op = (hw2 >> 8) & 0xF; - uint32_t imm8 = hw2 & 0xFF; - switch (op) { - case 0x0: // [Rn, #+imm8] - addr = rn_val + imm8; - break; - case 0xC: // [Rn, #-imm8] - addr = rn_val - imm8; - break; - case 0xB: // [Rn], #+imm8 (post-index) - addr = rn_val; - wb_val = rn_val + imm8; - writeback = true; - break; - case 0x9: // [Rn], #-imm8 (post-index) - addr = rn_val; - wb_val = rn_val - imm8; - writeback = true; - break; - case 0xF: // [Rn, #+imm8]! (pre-index) - addr = rn_val + imm8; - wb_val = addr; - writeback = true; - break; - case 0xD: // [Rn, #-imm8]! (pre-index) - addr = rn_val - imm8; - wb_val = addr; - writeback = true; - break; - default: - return std::unexpected{CPUError::IllegalInstruction}; + if (op == 0x0) { + // Register offset: [Rn, Rm, LSL #imm2], no writeback. + uint8_t rm = hw2 & 0xF; + uint8_t shift = (hw2 >> 4) & 0x3; + addr = base + (rr(rm) << shift); + } else { + // imm8 with addressing modes (T4). + uint32_t imm8 = hw2 & 0xFFu; + switch (op) { + case 0xC: // [Rn, #-imm8] + addr = base - imm8; + break; + case 0xB: // [Rn], #+imm8 (post-index) + addr = base; + wb_val = base + imm8; + writeback = true; + break; + case 0x9: // [Rn], #-imm8 (post-index) + addr = base; + wb_val = base - imm8; + writeback = true; + break; + case 0xF: // [Rn, #+imm8]! (pre-index) + addr = base + imm8; + wb_val = addr; + writeback = true; + break; + case 0xD: // [Rn, #-imm8]! (pre-index) + addr = base - imm8; + wb_val = addr; + writeback = true; + break; + default: + return std::unexpected{CPUError::IllegalInstruction}; + } } } @@ -104,7 +116,15 @@ CPU::CPUExpected CortexM3CPU::t32_loadstore_single(uint16_t hw1, if (!v) { return std::unexpected{v.error()}; } - auto w = wr(rt, *v); + data_t val = *v; + if (sign) { // LDRSB.W / LDRSH.W: sign-extend byte/half to 32 bits. + val = (width == Width::Byte) + ? static_cast( + static_cast(static_cast(val))) + : static_cast( + static_cast(static_cast(val))); + } + auto w = wr(rt, val); if (!w) { return w; } @@ -151,6 +171,43 @@ CPU::CPUExpected CortexM3CPU::t32_tbb_tbh(uint16_t hw1, uint16_t hw2) { return write_pc(target); } +// ── LDREX / STREX (+B/+H) — single-core sim, no exclusive monitor ── +// Dispatched when (hw1 & 0xFF60)==0xE840: the exclusive space (P=0 & W=0), +// which STRD/LDRD never occupy (those always set P or W). LDREX → plain load; +// STREX → plain store with Rd=0 (no monitor → always "succeeds"). +// hw1[4]=L: STREX(0) / LDREX(1). hw1[7]: 0=word, 1=byte/half. +// word: LDREX Rd=hw2[15:12]; STREX Rt=hw2[15:12], Rd=hw2[11:8]. +// byte/h: LDREX Rd=hw2[15:12]; STREX Rt=hw2[15:12], Rd=hw2[3:0]; size=hw2[7:4]. +CPU::CPUExpected CortexM3CPU::t32_ldrex_strex(uint16_t hw1, + uint16_t hw2) { + uint8_t rn = hw1 & 0xFu; + bool load = (hw1 >> 4) & 1u; + bool byte_half = (hw1 >> 7) & 1u; + + Width width = Width::Word; + if (byte_half) { + width = (((hw2 >> 4) & 0xFu) == 5u) ? Width::HalfWord : Width::Byte; + } + uint32_t base = rr(rn); + + if (load) { + uint8_t rd = (hw2 >> 12) & 0xFu; + auto v = br(base, width); + if (!v) { + return std::unexpected{v.error()}; + } + return wr(rd, *v); + } + // STREX: store Rt, write success status Rd=0. + uint8_t rt = (hw2 >> 12) & 0xFu; + uint8_t rd = byte_half ? (hw2 & 0xFu) : ((hw2 >> 8) & 0xFu); + auto w = bw(base, rr(rt), width); + if (!w) { + return w; + } + return wr(rd, 0u); +} + // ── STRD / LDRD (Store/Load Dual, immediate offset) ── // Dispatched when (hw1 & 0xFE40) == 0xE840. CPU::CPUExpected CortexM3CPU::t32_strd_ldrd(uint16_t hw1, uint16_t hw2) { diff --git a/test/test_cortex_m3_advanced.cpp b/test/test_cortex_m3_advanced.cpp index c287266..2e4c575 100644 --- a/test/test_cortex_m3_advanced.cpp +++ b/test/test_cortex_m3_advanced.cpp @@ -192,6 +192,56 @@ TEST_F(CortexM3Test, StrbWidePreIndexNegative) { EXPECT_EQ(*v, 0x77u); } +TEST_F(CortexM3Test, LoadStoreWideRegisterOffset) { + // Register offset (T2): addr = Rn + (Rm << shift2). The #9 bug treated + // hw2[7:0] as imm8, computing r1+2 instead of r1+r2. + // str.w r0,[r1,r2] = F841 0002 → r1 + r2 + // ldr.w r4,[r1,r2,lsl#3] = F851 4032 → r1 + (r2<<3) + load_program({0xF841, 0x0002, 0xF851, 0x4032}); + reset_cpu(); + set_reg(1, 0x100u); + set_reg(2, 0x40u); + set_reg(0, 0x12345678u); + uint32_t lit = 0xCAFEF00Du; + ASSERT_TRUE(mem_.load(0x300u, {reinterpret_cast(&lit), 4}) + .has_value()); + start_cpu(); + + ASSERT_TRUE(cpu_->step().has_value()); // str.w r0,[r1,r2] → 0x140 + auto w = bus_.read(0x140u, Width::Word); + ASSERT_TRUE(w.has_value()); + EXPECT_EQ(*w, 0x12345678u); + // Regression guard: the buggy imm8 path wrote to r1+2 = 0x102 instead. + auto bad = bus_.read(0x102u, Width::Word); + ASSERT_TRUE(bad.has_value()); + EXPECT_NE(*bad, 0x12345678u); + + ASSERT_TRUE(cpu_->step().has_value()); // ldr.w r4,[r1,r2,lsl#3] → 0x300 + EXPECT_EQ(reg(4), 0xCAFEF00Du); +} + +TEST_F(CortexM3Test, LoadStoreWideRegisterOffsetByteHalf) { + // strb.w r0,[r1,r2] = F801 0002 (byte → r1+r2) + // ldrh.w r4,[r1,r3] = F831 4003 (half ← r1+r3) + load_program({0xF801, 0x0002, 0xF831, 0x4003}); + reset_cpu(); + set_reg(1, 0x100u); + set_reg(2, 0x10u); // strb → 0x110 + set_reg(3, 0x40u); // ldrh → 0x140 + set_reg(0, 0xABu); + uint16_t lit = 0xBEEFu; + ASSERT_TRUE(mem_.load(0x140u, {reinterpret_cast(&lit), 2}) + .has_value()); + start_cpu(); + + ASSERT_TRUE(cpu_->step().has_value()); // strb.w → 0x110 + auto b = bus_.read(0x110u, Width::Byte); + ASSERT_TRUE(b.has_value()); + EXPECT_EQ(*b, 0xABu); + ASSERT_TRUE(cpu_->step().has_value()); // ldrh.w r4,[r1,r3] → 0x140 + EXPECT_EQ(reg(4), 0xBEEFu); +} + TEST_F(CortexM3Test, CmpWideShiftedRegDoesNotWritePc) { // 0xEBB0 0F81 = cmp.w r0, r1, lsl #2 (Rd=15, S=1 → flags only). // The F103 HAL_RCC_ClockConfig PLL-wait opcode. r0=10, r1=3 → 10-(3<<2)=-2. @@ -401,3 +451,224 @@ TEST_F(CortexM3Test, TbbUsesPcPlusFourAsBranchBase) { ASSERT_TRUE(cpu_->step().has_value()); EXPECT_EQ(reg(1), 2u); } + +// ── T1 静默错误修复单测(coverage matrix §2 #2–#11)── + +TEST_F(CortexM3Test, OrnWideRegisterIncludesRn) { + // #2: orn.w r0,r1,r2 (ea61 0002) = r1 | ~r2; bug gave ~shifted (dropped Rn). + load_program({0xEA61, 0x0002}); + reset_cpu(); + set_reg(1, 0x000000FFu); + set_reg(2, 0x0000000Fu); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); + EXPECT_EQ(reg(0), 0xFFFFFFFFu); // 0xFF | ~0x0F +} + +TEST_F(CortexM3Test, RsbsWideCarryReflectsMinuend) { + // #3: rsbs r0,r1,#5 (f1d1 0005) = 5 - r1; r1=3 → C=1 (5>=3). mrs r2,apsr. + load_program({0xF1D1, 0x0005, 0xF3EF, 0x8200}); + reset_cpu(); + set_reg(1, 3u); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // rsbs + ASSERT_TRUE(cpu_->step().has_value()); // mrs r2,apsr + EXPECT_EQ(reg(0), 2u); + EXPECT_EQ(reg(2), 0x20000000u); // C set; bug gave C=(rn>=imm)=0 +} + +TEST_F(CortexM3Test, ShiftBy32InShiftedRegisterOperand) { + // #4: add.w r0,r1,r2,lsr #32 (eb01 0012); LSR#32 → shifted=0. + load_program({0xEB01, 0x0012}); + reset_cpu(); + set_reg(1, 0x10u); + set_reg(2, 0xFFFFFFFFu); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); + EXPECT_EQ(reg(0), 0x10u); // r1 + 0; bug gave r1 + r2 +} + +TEST_F(CortexM3Test, CpsFControlsFaultMaskNotPrimask) { + // #5: cpsid f (b671) → FAULTMASK, not PRIMASK. + load_program({0xB671, 0xF3EF, 0x8013, 0xF3EF, 0x8110}); // cpsid f; mrs r0,faultmask; mrs r1,primask + reset_cpu(); + start_cpu(); + for (int i = 0; i < 3; ++i) { + ASSERT_TRUE(cpu_->step().has_value()); + } + EXPECT_EQ(reg(0), 1u); // faultmask set + EXPECT_EQ(reg(1), 0u); // primask untouched (bug had set primask) +} + +TEST_F(CortexM3Test, BkptIsNotSilentlyNopped) { + // #6: bkpt #5 (be05) → HardFault entry (PC leaves the bkpt), not a NOP + // that simply advances PC to 2. + load_program({0xBE05}); + reset_cpu(); + start_cpu(); + [[maybe_unused]] auto _ = cpu_->step(); + EXPECT_NE(cpu_->pc().value_or(0xDEAD), 2u); +} + +TEST_F(CortexM3Test, MulWideRa15DoesNotFoldPc) { + // #7: mul.w r0,r1,r2 (fb01 f002); Ra=15 → no accumulate; bug added raw PC. + load_program({0xFB01, 0xF002}); + reset_cpu(); + set_reg(1, 3u); + set_reg(2, 5u); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); + EXPECT_EQ(reg(0), 15u); +} + +TEST_F(CortexM3Test, SubwPcUsesAlignedPcPlusFour) { + // #8: subw r0,pc,#4 (f2af 0004) at PC=0; base=Align(PC+4,4)=4 → r0=0. + // bug used raw PC=0 → r0=0xFFFFFFFC. + load_program({0xF2AF, 0x0004}); + reset_cpu(); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); + EXPECT_EQ(reg(0), 0u); +} + +TEST_F(CortexM3Test, TbhDispatchesViaTableBranchHandler) { + // #10: tbh [pc,r0,lsl#1] (e8df f010); H-bit must not misroute to LDRD. + // r0=0, table halfword at PC+4 = 0 → target = PC+4. + load_program({0xE8DF, 0xF010}); + reset_cpu(); + set_reg(0, 0u); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); + EXPECT_EQ(cpu_->pc().value_or(0), 4u); +} + +TEST_F(CortexM3Test, LdrexStrexBehaveAsPlainLoadStore) { + // #11: ldrex r0,[r1] (e851 0f00) → plain load; strex r3,r2,[r1] (e841 2300) + // → plain store + Rd=0 (no monitor → always succeeds). + load_program({0xE851, 0x0F00, 0xE841, 0x2300}); + uint32_t seed = 0xDEADBEEFu; + ASSERT_TRUE(mem_.load(0x100u, {reinterpret_cast(&seed), 4}) + .has_value()); + reset_cpu(); + set_reg(1, 0x100u); + set_reg(2, 0xCAFEu); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // ldrex + EXPECT_EQ(reg(0), 0xDEADBEEFu); + ASSERT_TRUE(cpu_->step().has_value()); // strex + EXPECT_EQ(reg(3), 0u); // success status + auto v = bus_.read(0x100u, Width::Word); + ASSERT_TRUE(v.has_value()); + EXPECT_EQ(*v, 0xCAFEu); +} + +// ── T2 缺失指令单测(coverage matrix §3)── + +TEST_F(CortexM3Test, OrnMvnWideImmediate) { + // orn.w r3,r1,#0x11 (f061 0311)=r1|~imm; mvn.w r0,#0x11 (f06f 0011)=~imm. + load_program({0xF061, 0x0311, 0xF06F, 0x0011}); + reset_cpu(); + set_reg(1, 0x000000FFu); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // orn → 0xFF | ~0x11 + EXPECT_EQ(reg(3), 0xFFFFFFFFu); + ASSERT_TRUE(cpu_->step().has_value()); // mvn → ~0x11 + EXPECT_EQ(reg(0), 0xFFFFFFEEu); +} + +TEST_F(CortexM3Test, RorRrxShiftedRegister) { + // mov.w r0,r1,ror#4 (ea4f 1031); msr apsr,r2 (set C); mov.w r3,r1,rrx (ea4f 0331). + load_program({0xEA4F, 0x1031, 0xF382, 0x8800, 0xEA4F, 0x0331}); + reset_cpu(); + set_reg(1, 0x12345678u); + set_reg(2, 0x20000000u); // PSR_C + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // ror#4 + EXPECT_EQ(reg(0), 0x81234567u); + ASSERT_TRUE(cpu_->step().has_value()); // msr sets C + ASSERT_TRUE(cpu_->step().has_value()); // rrx: (C<<31)|(r1>>1) + EXPECT_EQ(reg(3), 0x891A2B3Cu); +} + +TEST_F(CortexM3Test, SmlalUmlalWideAccumulate) { + // smlal r0,r1,r2,r3 (fbc2 0103); umlal r0,r1,r2,r3 (fbe2 0103). + // r2=r3=0xFFFFFFFF: signed product=1, unsigned=0xFFFFFFFE00000001. + load_program({0xFBC2, 0x0103, 0xFBE2, 0x0103}); + reset_cpu(); + set_reg(0, 0u); + set_reg(1, 0u); + set_reg(2, 0xFFFFFFFFu); + set_reg(3, 0xFFFFFFFFu); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // smlal: 0 + 1 + EXPECT_EQ(reg(0), 1u); + EXPECT_EQ(reg(1), 0u); + ASSERT_TRUE(cpu_->step().has_value()); // umlal: 1 + 0xFFFFFFFE00000001 + EXPECT_EQ(reg(0), 2u); + EXPECT_EQ(reg(1), 0xFFFFFFFEu); +} + +TEST_F(CortexM3Test, LdrsbLdrshWideSignExtend) { + // ldrsb.w r0,[r1,#4] (f991 0004); ldrsh.w r0,[r1,#8] (f9b1 0008). + load_program({0xF991, 0x0004, 0xF9B1, 0x0008}); + uint8_t b = 0x80; + uint16_t h = 0x8000; + ASSERT_TRUE(mem_.load(0x104u, {&b, 1}).has_value()); + ASSERT_TRUE(mem_.load(0x108u, {reinterpret_cast(&h), 2}) + .has_value()); + reset_cpu(); + set_reg(1, 0x100u); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // ldrsb → 0xFFFFFF80 + EXPECT_EQ(reg(0), 0xFFFFFF80u); + ASSERT_TRUE(cpu_->step().has_value()); // ldrsh → 0xFFFF8000 + EXPECT_EQ(reg(0), 0xFFFF8000u); +} + +TEST_F(CortexM3Test, ClzRbitRevWide) { + // clz r0,r1 (fab1 f081); rbit r0,r1 (fa91 f0a1); rev.w r0,r1 (fa91 f081). + load_program({0xFAB1, 0xF081, 0xFA91, 0xF0A1, 0xFA91, 0xF081}); + reset_cpu(); + set_reg(1, 0x00010000u); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // clz (bit16 → 15 leading zeros) + EXPECT_EQ(reg(0), 15u); + ASSERT_TRUE(cpu_->step().has_value()); // rbit (bit16 → bit15) + EXPECT_EQ(reg(0), 0x00008000u); + ASSERT_TRUE(cpu_->step().has_value()); // rev.w (byte-reverse) + EXPECT_EQ(reg(0), 0x00000100u); +} + +TEST_F(CortexM3Test, SsatUsatWideSaturation) { + // ssat r0,#5,r1 (f301 0004): 100→15,Q; usat r2,#5,r1 (f381 0205): 100→31,Q. + // mrs r3,apsr (f3ef 8300) reads Q. + load_program({0xF301, 0x0004, 0xF381, 0x0205, 0xF3EF, 0x8300}); + reset_cpu(); + set_reg(1, 100u); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // ssat → 15 + EXPECT_EQ(reg(0), 15u); + ASSERT_TRUE(cpu_->step().has_value()); // usat → 31 + EXPECT_EQ(reg(2), 31u); + ASSERT_TRUE(cpu_->step().has_value()); // mrs r3,apsr + EXPECT_NE(reg(3) & 0x08000000u, 0u); // Q set +} + +TEST_F(CortexM3Test, ClrexNopWideAreNoop) { + // clrex (f3bf 8f2f); nop.w (f3af 8000) — both advance PC, no fault. + load_program({0xF3BF, 0x8F2F, 0xF3AF, 0x8000}); + reset_cpu(); + start_cpu(); + ASSERT_TRUE(cpu_->step().has_value()); // clrex PC 0→4 + EXPECT_EQ(cpu_->pc().value_or(0), 4u); + ASSERT_TRUE(cpu_->step().has_value()); // nop.w PC 4→8 + EXPECT_EQ(cpu_->pc().value_or(0), 8u); +} + +TEST_F(CortexM3Test, McrMrcCoprocessorFaults) { + // mrc p15 (ee11 0f10) → no coprocessor → IllegalInstruction. + load_program({0xEE11, 0x0F10}); + reset_cpu(); + start_cpu(); + EXPECT_FALSE(cpu_->step().has_value()); // faults +}