tan-cli 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3510 @@
1
+ # Copyright (c) 2026 Alp Lab AB
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """
4
+ `--emit zephyr-board` -- generate the per-core Zephyr board tree
5
+ (`board.yml`, `Kconfig.alp_<board>`, the twister board metadata `.yaml`,
6
+ and -- where the underlying hardware facts already exist in metadata --
7
+ the `_defconfig`, pinctrl `.dtsi`, `Kconfig.defconfig`, and board `.dts`)
8
+ straight from the SoM preset + SoC JSON, instead of hand-authoring them
9
+ under `zephyr/boards/alp/<board>/` (issue #523).
10
+
11
+ RELOCATED (was alp-sdk `scripts/gen_zephyr_board.py`). Reached as:
12
+
13
+ tan generate --target zephyr-board --core <core_id> \\
14
+ --output build/boards/<board_dir>/
15
+
16
+ Unlike every other `--emit` mode this one writes a *directory* of files,
17
+ not a single stream -- `tan.planner_emit.render_tree` / `write_tree` handle
18
+ that distinction (as `alp_project.py` did); this module only returns
19
+ ``{relative_path: content}`` (paths relative to the board-tree root, e.g.
20
+ ``"alp_e1m_aen801_m55_hp/board.yml"``).
21
+
22
+ Two mechanical changes came with the move, both on failure paths only: the SoM
23
+ preset and the pinmux YAML are read through `tan.planner`'s own loader, so a
24
+ missing preset or an unparseable YAML raises `OrchestratorError` where alp-sdk's
25
+ wrapper called `sys.exit` -- a raise is reportable as a coded envelope, and a
26
+ `SystemExit` inside `tan`'s own process is not. The generated bytes are
27
+ unchanged, pinned by `tests/parity/test_planner_emit_parity.py`.
28
+
29
+ A third is import-shaped: alp-sdk's `scripts/sentinels.py` has no counterpart
30
+ under `tan/planner/`, so the `is_tbd()` this module calls is spelled as the
31
+ local `_is_tbd` below -- same normalisation, same semantics.
32
+
33
+ Scope (issue #523, first slice; extended by #655 slice 1 and alp-sdk#1964,
34
+ both hand-ported here as tan-cli#1156):
35
+
36
+ - Alif Ensemble (the `aen` family, e.g. E1M-AEN801 m55_hp/m55_he) is
37
+ fully generated: every file in the hand-authored board tree except
38
+ `board.cmake` and `Kconfig` (see NOT GENERATED below) is produced
39
+ here, byte-identical to the committed tree -- proven by alp-sdk's
40
+ `tests/scripts/test_gen_zephyr_board.py`, whose `HAND_MAINTAINED`
41
+ exemption set is exactly those two names.
42
+
43
+ Its pinctrl `.dtsi` and `.dts` take a THIRD source alongside the SoM
44
+ preset + SoC JSON: `metadata/e1m_modules/aen/on-module-links.yaml`,
45
+ the authority for pads that reach no E1M edge pin (BRD_I2C on
46
+ P7_0/P7_1 and the RV-3028 alarm on P15_0) and for the devices that
47
+ hang off them. `metadata/pinmux/aen.yaml` cannot carry those: it is
48
+ a generated projection of the EDGE pad TSVs, so an on-module pad has
49
+ no row there by construction. Same shape, and same reasoning, as
50
+ `supervisor-links.yaml` below.
51
+ - Renesas RZ/V2N-family boards (`v2n` / `v2n-m1`, e.g. E1M-V2N101,
52
+ E1M-V2M101 `m33_sm`) generate the family-agnostic files (`board.yml`,
53
+ `Kconfig.alp_<board>`, the twister `.yaml`) PLUS the pinctrl `.dtsi`
54
+ and `_defconfig`, sourced from
55
+ `metadata/e1m_modules/v2n/supervisor-links.yaml` (#655) -- a
56
+ hand-authored, family-scoped source for the Renesas-side pin
57
+ assignments of the on-module GD32G553 supervisor bridge (SCI7
58
+ Simple-SPI, RIIC8/BRD_I2C, the disabled SCI0 console).
59
+
60
+ That source was deliberately NOT placed under
61
+ `metadata/pinmux/<family>.yaml`, despite that tree already carrying
62
+ every fact these two files need: `metadata/pinmux/*.yaml` is itself
63
+ GENERATED DO-NOT-EDIT output of `scripts/gen_pinmux_capability.py`
64
+ (from the family's TSVs), so it cannot also be a hand-authored
65
+ generation SOURCE without becoming circular (a generator reading its
66
+ own generated output as ground truth). `supervisor-links.yaml`
67
+ instead REFERENCES `metadata/pinmux/v2n.yaml` by
68
+ (silicon_peripheral, silicon_pad) -- alp-sdk's `scripts/
69
+ validate_metadata.py` cross-checks every row resolves to exactly one
70
+ `owner: "renesas"` pad there -- rather than re-declaring pad<->signal
71
+ authority a second time.
72
+
73
+ The board `.dts` is ALSO generated as of #655 slice 2 (alp-sdk
74
+ `e4f2f60e4`): `emit_zephyr_board()`'s `v2n`/`v2n-m1` branch calls
75
+ `_v2n_dts()`, which reuses the upstream RZ/V2N SoC devicetree and adds
76
+ the same on-module GD32G553 supervisor links `_v2n_pinctrl_dtsi()`
77
+ reads. A SoM preset's `topology.m33_sm.openamp_ipc` (boolean, absent
78
+ means false) gates the OpenAMP/MHU-B reserved-memory block and the
79
+ CAN-FD-unavailable analysis -- E1M-V2N101 and E1M-V2M101 both set it
80
+ (the same RZ/V2N die and MHU-B, #1948); a preset that leaves it false
81
+ gets neither block. `Kconfig.defconfig` remains
82
+ the one hand-authored file in this family (no metadata source for it
83
+ yet).
84
+
85
+ NOT GENERATED (any family): `board.cmake` and a bare `Kconfig`.
86
+ BOTH must be copied across by hand when a generated board tree is
87
+ used as the board directory -- nothing in the emit, the CLI output,
88
+ or the JSON envelope says so, which is why they are named here.
89
+
90
+ - `board.cmake`. The hand-authored AEN pair (HP/HE) intentionally
91
+ carries asymmetric prose -- one board's file hosts the full
92
+ SETOOLS/J-Link bench-bring-up runbook, the sibling's just points at
93
+ it -- which is a documentation choice, not a hardware fact, so
94
+ there is nothing in metadata for it to derive from.
95
+ Flasher/debugger `board_runner_args()` wiring stays hand-authored
96
+ until that asymmetry is either resolved (duplicate the full
97
+ rationale in both) or the prose itself is promoted into a metadata
98
+ field. Without it a board has no runner, so `west flash` has
99
+ nothing to flash with.
100
+ - `Kconfig` (the bare name, NOT the generated `Kconfig.<board>` /
101
+ `Kconfig.defconfig`). It sets
102
+ `CPU_HAS_CUSTOM_FIXED_SOC_MPU_REGIONS default y`, which
103
+ `zephyr/CMakeLists.txt` uses to pull in the custom E8 MPU region
104
+ table (`zephyr/soc-bridge/alif/mpu_regions_e8.c`) via
105
+ `zephyr_sources_ifdef`. Zephyr's `hwm_v2.cmake` unconditionally
106
+ `osource`s a bare `Kconfig` in a board directory whether or not it
107
+ exists, so it survives regeneration exactly as `board.cmake` does
108
+ -- and its absence is SILENT: the build succeeds and falls back to
109
+ Zephyr's generic 2-region FLASH_0/SRAM_0 map, whose whole-flash
110
+ FLASH_0 region is unsafe on Flow D (production MRAM boot).
111
+ """
112
+
113
+ from __future__ import annotations
114
+
115
+ import re
116
+ import textwrap
117
+ from pathlib import Path
118
+ from typing import Any, NoReturn
119
+
120
+ # NO module-scope `import json` any more (tan-cli#1162): `_load_soc_spec` was
121
+ # its only user and now reads through `document_guards.read_json_mapping`.
122
+ from tan.core.document_guards import DocumentGuards
123
+
124
+ from .loader import _load_yaml
125
+ from .project_loader import _resolve_sku
126
+ from .sdk_capability import (
127
+ AEN_ATOC_RESERVATION,
128
+ AEN_SOM_POWER_DOMAINS,
129
+ AEN_ZEPHYR_PERIPHERALS_DTSI,
130
+ require_capability,
131
+ )
132
+ from .som_metadata import resolve_soc_path
133
+ from .whole_device_alias import is_whole_device_alias
134
+
135
+
136
+ def _is_tbd(value: Any) -> bool:
137
+ """True iff @p value is the `TBD` placeholder sentinel.
138
+
139
+ RELOCATED spelling of alp-sdk `scripts/sentinels.py::is_tbd` (alp-sdk
140
+ #1048), which has no counterpart under `tan/planner/`: it is imported
141
+ upstream from both `scripts/` and `alp_orchestrate/`, and relocating it
142
+ would add a module to the hand-port audit for one three-line function.
143
+ Compares case- and surrounding-whitespace-insensitively (`"tbd"`,
144
+ `"Tbd"`, `" TBD "` all match) but only on an exact match after that
145
+ normalisation -- `"TBD_RESERVED"` is a real value, not a placeholder.
146
+ """
147
+ return isinstance(value, str) and value.strip().upper() == "TBD"
148
+
149
+
150
+ _COPYRIGHT_C = (
151
+ "/*\n"
152
+ " * Copyright (c) 2026 Alp Lab AB\n"
153
+ " * SPDX-License-Identifier: Apache-2.0\n"
154
+ )
155
+ _COPYRIGHT_HASH = (
156
+ "# Copyright (c) 2026 Alp Lab AB\n"
157
+ "# SPDX-License-Identifier: Apache-2.0\n"
158
+ )
159
+
160
+
161
+ class ZephyrBoardEmitError(ValueError):
162
+ """Raised when metadata needed for a requested board is missing."""
163
+
164
+
165
+ #: The malformed-document register, bound to THIS module's curated class.
166
+ _GUARDS = DocumentGuards(ZephyrBoardEmitError)
167
+
168
+
169
+ # ---------------------------------------------------------------------
170
+ # Family-agnostic naming + resolution
171
+ # ---------------------------------------------------------------------
172
+
173
+
174
+ def _load_soc_spec(sku_preset: dict[str, Any], metadata_root: Path) -> dict[str, Any]:
175
+ """The SoC JSON this SoM's `silicon:` ref names, as a mapping.
176
+
177
+ RELOCATED divergence from alp-sdk's own `scripts/alp_orchestrate/
178
+ zephyr_board.py` (tan-cli#1162): upstream still spells the last two
179
+ lines as an `is_file()` pre-flight plus a bare `json.loads(soc_path.
180
+ read_text(...))`. Read, parse and shape-check now go through
181
+ `tan/core/document_guards.py`'s register, for the reasons
182
+ `read_json_mapping` documents.
183
+
184
+ NOT on the `tan build` path, contrary to tan-cli#1162's own body, and
185
+ established by walking callers rather than assumed: the only caller is
186
+ `emit_zephyr_board` (this module), reached only through
187
+ `planner_emit.py:287`, so only from `tan generate --emit zephyr-board`.
188
+ `loader.py:385` and `secure.py:102` take `_aen_role_slot0_map` and two
189
+ constants from this module, never this function. The handler that
190
+ actually catches what this raises is `generate_cmd.py:890`'s
191
+ `except BaseException` -- coded, not a bare traceback, but naming the
192
+ exception class instead of the file.
193
+
194
+ `no SoC spec at <path>` is preserved byte for byte through `absent=`;
195
+ the shapes the pre-flight folded into it (a directory, a denied mode, a
196
+ symlink loop) now say `cannot read`, which is true where it was not.
197
+ """
198
+ silicon = sku_preset.get("silicon")
199
+ if not silicon:
200
+ raise ZephyrBoardEmitError(f"SoM {sku_preset.get('sku')!r} declares no silicon:")
201
+ soc_path = resolve_soc_path(silicon, metadata_root)
202
+ if soc_path is None:
203
+ raise ZephyrBoardEmitError(f"silicon ref {silicon!r} is not a triple-colon string")
204
+ return _GUARDS.read_json_mapping(
205
+ soc_path, what="SoC spec", absent=f"no SoC spec at {soc_path}")
206
+
207
+
208
+ def _resolve_variant(sku_preset: dict[str, Any], soc_spec: dict[str, Any]) -> dict[str, Any]:
209
+ """Resolve the SoM preset's silicon variant against the SoC JSON.
210
+
211
+ A DECLARED `silicon_variant:` is authoritative and must name a real
212
+ `variants[].order_code` -- `docs/porting-new-som.md` says so. When
213
+ it names none this raises instead of falling through to the
214
+ `alp_module_skus` reverse lookup: silently answering with a
215
+ different variant is how a typo (`...LS0` -> `...LSO`) or an
216
+ un-published silicon revision emits a whole board tree named after
217
+ a part number the preset does not declare, carrying the OLD
218
+ variant's `mram_mb` / DTCM size / board-target name -- exit 0, no
219
+ diagnostic. Every other missing fact in this module raises; so
220
+ does this one. The reverse lookup stays for presets that declare
221
+ no variant at all (or `TBD`).
222
+ """
223
+ declared = sku_preset.get("silicon_variant")
224
+ variants = soc_spec.get("variants") or []
225
+ if declared and not _is_tbd(declared):
226
+ for v in variants:
227
+ if v.get("order_code") == declared:
228
+ return v
229
+ known = ", ".join(str(v.get("order_code")) for v in variants) or "<none>"
230
+ raise ZephyrBoardEmitError(
231
+ f"SoM {sku_preset.get('sku')!r} declares silicon_variant "
232
+ f"{declared!r}, which is not an order_code in "
233
+ f"{soc_spec.get('ref')} variants[] ({known}) -- fix the preset, "
234
+ "or add the variant to the SoC JSON if this is new silicon")
235
+ sku = sku_preset.get("sku")
236
+ for v in variants:
237
+ if sku in (v.get("alp_module_skus") or []):
238
+ return v
239
+ raise ZephyrBoardEmitError(
240
+ f"can't resolve a silicon_variant for {sku_preset.get('sku')!r}")
241
+
242
+
243
+ def _find_core(soc_spec: dict[str, Any], core_id: str) -> dict[str, Any]:
244
+ for c in soc_spec.get("cores", []):
245
+ if c.get("id") == core_id:
246
+ return c
247
+ raise ZephyrBoardEmitError(
248
+ f"core {core_id!r} not found in {soc_spec.get('ref')} cores[]")
249
+
250
+
251
+ def _board_dir_name(sku: str, core_id: str) -> str:
252
+ """`E1M-AEN801` + `m55_hp` -> `alp_e1m_aen801_m55_hp`."""
253
+ if not sku.startswith("E1M-"):
254
+ raise ZephyrBoardEmitError(f"unrecognised SKU prefix: {sku!r}")
255
+ slug = sku[len("E1M-"):].lower()
256
+ return f"alp_e1m_{slug}_{core_id}"
257
+
258
+
259
+ def _board_naming(
260
+ sku_preset: dict[str, Any], core_id: str, variant: dict[str, Any], core: dict[str, Any],
261
+ ) -> tuple[str, str, str, str]:
262
+ """Return (dir_name, cluster, variant_tag, basename)."""
263
+ dir_name = _board_dir_name(sku_preset["sku"], core_id)
264
+ cluster = core.get("zephyr_cpucluster")
265
+ if not cluster:
266
+ raise ZephyrBoardEmitError(
267
+ f"core {core_id!r} has no zephyr_cpucluster in its SoC spec entry -- "
268
+ "add one before generating this board (see soc-spec-v1.schema.json)")
269
+ variant_tag = (variant.get("zephyr_soc_variant") or variant["order_code"]).lower()
270
+ basename = f"{dir_name}_{variant_tag}_{cluster}"
271
+ return dir_name, cluster, variant_tag, basename
272
+
273
+
274
+ def _topology_entry(sku_preset: dict[str, Any], core_id: str) -> dict[str, Any]:
275
+ topo = (sku_preset.get("topology") or {}).get(core_id)
276
+ if not topo:
277
+ raise ZephyrBoardEmitError(
278
+ f"SoM {sku_preset.get('sku')!r} has no topology.{core_id} entry")
279
+ return topo
280
+
281
+
282
+ def _generated_banner(
283
+ style: str, sku: str, soc_json_rel: str, extra_source: str | None = None,
284
+ ) -> list[str]:
285
+ """3-line "auto-generated, do not edit" banner, matching the wording
286
+ convention `gen_soc_caps.py` uses for `include/alp/soc_caps.h`.
287
+
288
+ *extra_source* names an additional generation-authority file beyond the
289
+ SoM preset + SoC JSON -- the V2N/V2M pinctrl.dtsi/_defconfig read
290
+ `metadata/e1m_modules/v2n/supervisor-links.yaml` for every pad, PFC
291
+ triple and I2C address they emit, and a banner that omits it sends a
292
+ maintainer to two files that carry neither."""
293
+ c = " *" if style == "c" else "#"
294
+ sku_rel = f"metadata/e1m_modules/{sku}.yaml"
295
+ sources = f"{sku_rel} + {soc_json_rel}"
296
+ if extra_source:
297
+ sources += f" + {extra_source}"
298
+ return [
299
+ f"{c} Auto-generated by `scripts/alp_project.py --emit zephyr-board` from",
300
+ f"{c} {sources}.",
301
+ f"{c} DO NOT EDIT BY HAND -- regenerate.",
302
+ ]
303
+
304
+
305
+ def _with_generated_banner(
306
+ content: str, style: str, sku: str, soc_json_rel: str,
307
+ extra_source: str | None = None,
308
+ ) -> str:
309
+ """Insert the generated-file banner into *content*.
310
+
311
+ - When *content* already opens with the standard Copyright/SPDX
312
+ header (every AEN file; the V2N-family Kconfig/twister files),
313
+ the banner is inserted right after the SPDX line, ahead of the
314
+ file's own prose -- purely additive, no existing line moves or
315
+ disappears.
316
+ - When *content* has no header at all (the V2N-family `board.yml`,
317
+ matching its committed hand-authored original -- see `_board_yml`),
318
+ the banner is prepended as a leading comment block instead.
319
+ """
320
+ lines = content.split("\n")
321
+ banner = _generated_banner(style, sku, soc_json_rel, extra_source)
322
+ if style == "c" and len(lines) >= 3 and lines[0] == "/*" and lines[1].startswith(" * Copyright"):
323
+ lines[3:3] = banner
324
+ return "\n".join(lines)
325
+ if style == "hash" and lines and lines[0].startswith("# Copyright"):
326
+ lines[2:2] = banner
327
+ return "\n".join(lines)
328
+ # No pre-existing header: prepend the banner as its own block.
329
+ return "\n".join(banner) + "\n" + content
330
+
331
+
332
+ def _sku_family_slug(sku: str) -> str:
333
+ """AEN/V2N/V2M -- see `som_metadata._sku_family` (kept independent here, as
334
+ it was in alp-sdk, so this module carries no import-cycle risk on the fuller
335
+ SKU-family table; the two must agree, pinned by the test)."""
336
+ m = re.match(r"^E1M-(AEN|V2N|V2M)", sku)
337
+ if not m:
338
+ raise ZephyrBoardEmitError(f"unrecognised SoM SKU pattern: {sku!r}")
339
+ return {"AEN": "aen", "V2N": "v2n", "V2M": "v2n-m1"}[m.group(1)]
340
+
341
+
342
+ # ---------------------------------------------------------------------
343
+ # Family-agnostic files: board.yml, Kconfig.alp_<board>, twister .yaml
344
+ # ---------------------------------------------------------------------
345
+
346
+
347
+ def _board_yml(dir_name: str, full_name: str, variant_tag: str, family: str) -> str:
348
+ # Hand-authored inconsistency across families: AEN's board.yml carries
349
+ # the copyright header, the V2N-family one doesn't. Preserved exactly
350
+ # rather than "fixed", per the byte-equivalence pin -- see the
351
+ # `--emit zephyr-board` module docstring.
352
+ header = _COPYRIGHT_HASH if family == "aen" else ""
353
+ return (
354
+ header +
355
+ "board:\n"
356
+ f" name: {dir_name}\n"
357
+ f" full_name: {full_name}\n"
358
+ " vendor: alp\n"
359
+ " socs:\n"
360
+ f" - name: {variant_tag}\n"
361
+ )
362
+
363
+
364
+ def _kconfig_board(dir_name: str, variant_tag: str, cluster: str) -> str:
365
+ soc_sym = f"SOC_{variant_tag.upper()}_{cluster.upper()}"
366
+ board_sym = dir_name.upper()
367
+ return (
368
+ _COPYRIGHT_HASH +
369
+ "\n"
370
+ f"config BOARD_{board_sym}\n"
371
+ f"\tselect {soc_sym}\n"
372
+ )
373
+
374
+
375
+ def _aen_dtcm_kib(variant: dict[str, Any], core_id: str) -> int:
376
+ role = core_id.split("_")[-1].upper() # "m55_hp" -> "HP"
377
+ banks = variant.get("sram_banks_kb") or {}
378
+ for name, size in banks.items():
379
+ if name.endswith(f"_{role}_DTCM"):
380
+ return int(size)
381
+ raise ZephyrBoardEmitError(
382
+ f"variant {variant.get('order_code')} sram_banks_kb has no *_{role}_DTCM entry")
383
+
384
+
385
+ def _twister_yaml(
386
+ dir_name: str, variant_tag: str, cluster: str, twister_name: str,
387
+ family: str, sku_preset: dict[str, Any], core_id: str, variant: dict[str, Any],
388
+ ) -> str:
389
+ identifier = f"{dir_name}/{variant_tag}/{cluster}"
390
+ has_supervisor_mcu = bool((sku_preset.get("on_module") or {}).get("supervisor_mcu"))
391
+
392
+ if family == "aen":
393
+ ram_kib = _aen_dtcm_kib(variant, core_id)
394
+ flash_kib = round(float(variant["mram_mb"]) * 1024)
395
+ return (
396
+ f"identifier: {identifier}\n"
397
+ f"name: {twister_name}\n"
398
+ "type: mcu\n"
399
+ "arch: arm\n"
400
+ "vendor: alp\n"
401
+ "toolchain:\n"
402
+ " - zephyr\n"
403
+ " - gnuarmemb\n"
404
+ "supported:\n"
405
+ " - uart\n"
406
+ f"ram: {ram_kib}\n"
407
+ f"flash: {flash_kib}\n"
408
+ )
409
+
410
+ # Non-AEN families: no on-die MRAM/TCM concept surfaced here (the A55
411
+ # cluster boots Linux from eMMC/xSPI; the M33 board file has no
412
+ # zephyr,flash of its own), so no ram:/flash:. supported: lists only
413
+ # what the board dts leaves `status = "okay"` (audit CM33-14): the GD32
414
+ # bridge's SCI7 Simple-SPI plus its GPIO chip-select port. RIIC8
415
+ # (A55/Linux-exclusive) and the sci0 console UART are disabled, so
416
+ # i2c/uart are not advertised. Same PCB, same GD32 bridge on every
417
+ # current V2N-family SoM (porting-a-new-som "same-PCB families" rule).
418
+ supported = ["gpio", "spi"] if has_supervisor_mcu else []
419
+ lines = [
420
+ f"identifier: {identifier}",
421
+ f"name: {twister_name}",
422
+ "type: mcu",
423
+ "arch: arm",
424
+ "toolchain:",
425
+ " - zephyr",
426
+ " - gnuarmemb",
427
+ "supported:" if supported else "supported: []",
428
+ ]
429
+ lines += [f" - {p}" for p in supported]
430
+ lines.append("vendor: alp")
431
+ return "\n".join(lines) + "\n"
432
+
433
+
434
+ # ---------------------------------------------------------------------
435
+ # Alif Ensemble (aen) family -- fully generated
436
+ # ---------------------------------------------------------------------
437
+
438
+ # Fixed MCUboot flash-partition sizes for the AEN sysbuild profile
439
+ # (MCUboot + a signed application image, swap-using-scratch). These are
440
+ # an SDK build-policy choice, not a per-chip hardware fact -- every AEN
441
+ # SKU's on-die MRAM is split the same way regardless of total size, so
442
+ # they stay generator constants rather than metadata fields; only the
443
+ # two symmetric image slots derive from the variant's actual MRAM size.
444
+ _AEN_MCUBOOT_KIB = 64
445
+ _AEN_SCRATCH_KIB = 64
446
+ _AEN_STORAGE_KIB = 96
447
+
448
+ # The SE-owned band at the very TOP of the App MRAM window (alp-sdk#1289).
449
+ #
450
+ # Alif's SETOOLS (`app-gen-toc` / `app-write-mram`) does not place the ATOC
451
+ # application table at a fixed address: it top-anchors the generated package
452
+ # at the App MRAM window end and grows DOWNWARD, sized to the package. There
453
+ # is therefore no compile-time constant to read -- the placement happens at
454
+ # provisioning time, not link time -- so the layout has to reserve a band
455
+ # rather than a specific address.
456
+ #
457
+ # 32 KiB covers the worst case with margin. Three independent figures, all
458
+ # measured/derived against the same window top 0x80580000:
459
+ #
460
+ # 5552 B observed on the E1M-AEN801 bench 2026-08-08 -- ATOC magic
461
+ # `ckBS` (0x53426B63) read at 0x8057EA50
462
+ # 13552 B an E7 SETOOLS transcript in the Alif DFP
463
+ # (alifsemi/alif_ensemble-cmsis-dfp, docs/Overview.md:193-224):
464
+ # package at 0x8057cb10
465
+ # 23696 B worst case computed from SETOOLS' own build/app-package-map.txt
466
+ # sizing rule -- 2560 B per signed user image + 2880 B fixed
467
+ # tool-managed + (48 + 32*N) TOC -- at the 8-image maximum. That
468
+ # rule reproduces the 5552 B observed above exactly.
469
+ #
470
+ # 32768 - 23696 = 9072 B spare at 8 images. UNVERIFIED: no documented
471
+ # maximum TOC entry count could be found, so the 8-image worst case is an
472
+ # assumption, not a specification -- widen this band rather than narrowing
473
+ # it if a larger configuration ever appears.
474
+ #
475
+ # Taken OUT of storage (128 -> 96 + 32) rather than out of the image slots
476
+ # on purpose: `_AEN_MCUBOOT_KIB + _AEN_SCRATCH_KIB + _AEN_STORAGE_KIB +
477
+ # _AEN_ATOC_KIB` still sums to the old 256 KiB, so `image_kib` below is
478
+ # unchanged and no committed AEN board's slot geometry moves.
479
+ _AEN_ATOC_KIB = 32
480
+
481
+ # Per-role documentation asymmetries in the committed AEN board tree.
482
+ # These are prose choices (which sibling's ITCM comment got the
483
+ # bench-verified annotation), not hardware facts -- like the comment
484
+ # scaffolding in alp_project_emit._emit_dts_overlay / _emit_hw_info_h,
485
+ # they live as generator template text keyed by role rather than as
486
+ # metadata.
487
+ _AEN_BENCH_KNOWN_ITCM = {"hp": False, "he": True}
488
+
489
+ # Base of the Alif Ensemble on-die App MRAM window. Family-wide, not
490
+ # per-part: upstream Zephyr declares `mram: flash@80000000` in EVERY
491
+ # Ensemble SoC dtsi (dts/arm/alif/ensemble/ae1c1f4051920ph0.dtsi,
492
+ # ae402fa0e5597le0.dtsi, ae612fa0e5597ls0.dtsi, ae822fa0e5597ls0.dtsi),
493
+ # and E1M-AEN801.yaml's `memory_map:` anchors its `mcuboot` region at the
494
+ # same 0x80000000. It is the soc-nv-flash child's offset-0 origin, so a
495
+ # SoM whose map disagrees is REFUSED rather than emitted -- see
496
+ # _aen_flash_partitions.
497
+ _AEN_MRAM_BASE = 0x80000000
498
+
499
+
500
+ def _aen_part(soc_spec: dict[str, Any]) -> str:
501
+ """The Ensemble part designator -- `"E8"`, `"E3"`, ... -- from the
502
+ SoC JSON's required `part` field.
503
+
504
+ Was a generator constant hardcoded to E8, which put "Alif Ensemble
505
+ E8" and `<alif/ensemble_e8_peripherals.dtsi>` into EVERY `E1M-AEN*`
506
+ board tree regardless of silicon. `part` is `required` by
507
+ soc-spec-v1.schema.json, so there is nothing to guess.
508
+ """
509
+ part = soc_spec.get("part")
510
+ if not part or _is_tbd(part):
511
+ raise ZephyrBoardEmitError(
512
+ f"SoC spec {soc_spec.get('ref')} declares no usable `part` -- "
513
+ "the AEN board tree names it in prose and derives the SoC "
514
+ "peripherals overlay from it")
515
+ return str(part)
516
+
517
+
518
+ def _aen_family_display(soc_spec: dict[str, Any]) -> str:
519
+ """Vendor-short + family + part, as used in AEN board-tree prose
520
+ ("Alif Ensemble E8").
521
+
522
+ `Alif` is fixed by the code path -- the `aen` family slug IS Alif
523
+ Ensemble -- because soc_spec's own `vendor:` field spells out the
524
+ legal name "Alif Semiconductor" for provenance blocks. `family`
525
+ ("Ensemble") and `part` ("E8") are read from the SoC JSON, both
526
+ `required` by the schema.
527
+ """
528
+ family = soc_spec.get("family")
529
+ if not family or _is_tbd(family):
530
+ raise ZephyrBoardEmitError(
531
+ f"SoC spec {soc_spec.get('ref')} declares no usable `family`")
532
+ return f"Alif {family} {_aen_part(soc_spec)}"
533
+
534
+
535
+ def _aen_peripherals_dtsi(soc_spec: dict[str, Any], metadata_root: Path) -> str:
536
+ """The `#include <...>` path of THIS SoC's Zephyr peripherals overlay.
537
+
538
+ Read from the SoC JSON's `zephyr_peripherals_dtsi`, never derived
539
+ from the part name: the overlay declares the SoC's actual peripheral
540
+ and NPU node set (the E8's includes `ethosu85`; an E3 has 2x U55 and
541
+ no U85), so including the wrong family's file produces a board that
542
+ builds and then misbehaves on silicon. alp-sdk ships exactly one
543
+ today (`zephyr/dts/alif/ensemble_e8_peripherals.dtsi`), so every
544
+ other Ensemble part must add its own and declare it here rather than
545
+ inherit the E8's.
546
+
547
+ `zephyr_peripherals_dtsi` is itself a ported requirement (alp-sdk#1352,
548
+ tan-cli#591) that no released alp-sdk carries yet: an absent value
549
+ checks `require_capability` FIRST, so a checkout that predates the
550
+ field raises `SdkTooOldError` naming that, rather than the generic
551
+ authoring-gap message below, which only fires once the checkout is
552
+ proven new enough to know the field at all.
553
+ """
554
+ dtsi = soc_spec.get("zephyr_peripherals_dtsi")
555
+ if not dtsi or _is_tbd(dtsi):
556
+ require_capability(metadata_root, AEN_ZEPHYR_PERIPHERALS_DTSI)
557
+ raise ZephyrBoardEmitError(
558
+ f"SoC spec {soc_spec.get('ref')} "
559
+ f"({_aen_part(soc_spec)}) has no `zephyr_peripherals_dtsi` -- "
560
+ "the AEN board .dts must include this SoC's own peripherals "
561
+ "overlay, and alp-sdk ships one only for the E8 "
562
+ "(zephyr/dts/alif/ensemble_e8_peripherals.dtsi). Add the "
563
+ "overlay under zephyr/dts/alif/ and declare it in the SoC "
564
+ "JSON before generating this board; do NOT fall back to "
565
+ "another part's file, whose peripheral and NPU node set is "
566
+ "different silicon")
567
+ return str(dtsi)
568
+
569
+
570
+ def _aen_require_disjoint_slot0(
571
+ sku: str, sku_preset: "dict[str, Any]",
572
+ memory_map: "list[dict[str, Any]] | None",
573
+ ) -> None:
574
+ """Refuse a DUAL-M55 AEN SoM that declares no `<role>_slot0` window (#1446).
575
+
576
+ `_aen_role_slot0_map` returns None when no role declares one, which drops
577
+ the caller onto the stock SYMMETRIC layout -- `m55_he` and `m55_hp` slot0
578
+ at the same address, so flashing one core silently clobbers the other's
579
+ window. That is the #1069 defect, and it is how E1M-AEN301/401/501/601/701
580
+ shipped until alp-sdk#1445 gave them explicit maps.
581
+
582
+ The half-authored case (one role declares a window, its sibling does not)
583
+ already raises inside `_aen_role_slot0_map`. This closes the fully
584
+ unauthored case, which was silent.
585
+
586
+ Single-M55 SoMs are unaffected: with no sibling core there is nothing to
587
+ clobber, so the symmetric layout stays correct for them.
588
+
589
+ Costs nothing on the current corpus -- after alp-sdk#1445 every AEN SoM
590
+ declares disjoint windows, so this never fires today. Its value is the
591
+ NEXT dual-M55 AEN SoM, which is refused at authoring time instead of
592
+ discovered by someone flashing one core and losing the other's slot0 on
593
+ a bench.
594
+ """
595
+ if memory_map and any(
596
+ isinstance(r.get("name"), str) and r["name"].endswith("_slot0")
597
+ for r in memory_map
598
+ ):
599
+ return
600
+ cores = sku_preset.get("topology") or {}
601
+ m55 = sorted(c for c in cores if c in ("m55_he", "m55_hp"))
602
+ if len(m55) < 2:
603
+ return
604
+ raise ZephyrBoardEmitError(
605
+ f"SoM {sku!r} has two M55 cores ({', '.join(m55)}) but its preset "
606
+ f"declares no per-core `<role>_slot0` region, so both would boot from "
607
+ f"the SAME MRAM slot0 address -- flashing one core silently corrupts "
608
+ f"the other's slot0 window (#1069). Declare disjoint `he_slot0` / "
609
+ f"`hp_slot0` regions in this SoM preset's `memory_map:`; "
610
+ f"metadata/e1m_modules/E1M-AEN801.yaml is the shape to copy (#1446)."
611
+ )
612
+
613
+
614
+ def _aen_role_slot0_map(
615
+ memory_map: "list[dict[str, Any]] | None", role: str,
616
+ ) -> "dict[str, Any] | None":
617
+ """Return this role's disjoint-slot0 memory_map entry, or None.
618
+
619
+ Dual-M55 AEN SoMs (#1069: both cores boot from the same physical App
620
+ MRAM) declare a `memory_map:` block in their SoM preset with one region
621
+ per core named `<role>_slot0` (`accessible_from: [m55_<role>]` only).
622
+ A SoM with only ONE M55 has no sibling to collide with and keeps the
623
+ stock symmetric two-slot layout -- see _aen_flash_partitions below.
624
+
625
+ #1446: this used to say the stock layout also covered "single-M55
626
+ aen401/aen601". Both of those are DUAL-M55 -- measured from their own
627
+ presets, E1M-AEN401 is [m55_he, m55_hp] and E1M-AEN601 is
628
+ [a32_cluster, m55_he, m55_hp] -- so that sentence was the justification
629
+ for a silent fallback that put two cores' slot0 at one address, and it
630
+ is why five shipping SoMs carried a clobbering layout until
631
+ alp-sdk#1445. `_aen_require_disjoint_slot0` now refuses that state
632
+ outright; this function keeps returning None only for the genuinely
633
+ single-M55 case.
634
+
635
+ Returning None is only legitimate when NO role declares a slot0
636
+ window. A map that declares a SIBLING core's `<role>_slot0` but not
637
+ this one's is a half-authored map, and answering None for it drops
638
+ the caller onto the stock symmetric layout, whose `slot0_partition`
639
+ lands exactly on top of the sibling's declared window -- silently
640
+ undoing the #1069 disjoint-window fix and re-creating the
641
+ bench-confirmed corruption it exists to prevent. So that state
642
+ RAISES.
643
+ """
644
+ if not memory_map:
645
+ return None
646
+ suffix = "_slot0"
647
+ declared_roles = [
648
+ str(r["name"])[: -len(suffix)]
649
+ for r in memory_map
650
+ if isinstance(r.get("name"), str) and r["name"].endswith(suffix)
651
+ ]
652
+ if not declared_roles:
653
+ return None
654
+ core_id = f"m55_{role}"
655
+ for region in memory_map:
656
+ if region.get("name") != f"{role}{suffix}":
657
+ continue
658
+ if region.get("accessible_from") != [core_id]:
659
+ raise ZephyrBoardEmitError(
660
+ f"AEN memory_map region '{role}{suffix}' must declare "
661
+ f"accessible_from: [{core_id}] (a disjoint per-core slot0 "
662
+ f"window is reachable from exactly one core); it declares "
663
+ f"{region.get('accessible_from')!r}")
664
+ return region
665
+ raise ZephyrBoardEmitError(
666
+ f"AEN memory_map declares a disjoint slot0 window for "
667
+ f"{sorted(declared_roles)} but none for role {role!r} -- add a "
668
+ f"'{role}{suffix}' region (accessible_from: [{core_id}]). "
669
+ "Falling back to the stock symmetric layout here would place this "
670
+ "core's slot0_partition on top of the sibling's declared window "
671
+ "(#1069)")
672
+
673
+
674
+ def _aen_check_extents(
675
+ partitions: "list[tuple[str, int, int]]", total_kib: int, where: str,
676
+ ) -> None:
677
+ """Every emitted partition must fit inside the flash node it is emitted
678
+ into, and no two may overlap.
679
+
680
+ Neither branch used to check this. The stock branch's
681
+ `offset != total_kib * 1024` sum assertion is NOT a fits-in-MRAM
682
+ check -- all five stock sizes derive from `total_kib`, so it is a
683
+ tautology that can never fire. The disjoint branch copied `base` /
684
+ `size_kib` verbatim out of the SoM preset and checked only that each
685
+ region existed with an integer `base`, so a mis-authored map emitted
686
+ a `partition@` node outside its own `mram_storage` `reg` -- MCUboot's
687
+ flash_map and the alif MRAM driver would then compute erase/program
688
+ addresses past the end of the device.
689
+ """
690
+ limit = total_kib * 1024
691
+ ordered = sorted(partitions, key=lambda p: p[1])
692
+ prev_label, prev_end = None, 0
693
+ for label, off, size_kib in ordered:
694
+ if not isinstance(size_kib, int) or size_kib <= 0:
695
+ raise ZephyrBoardEmitError(
696
+ f"{where}: partition {label!r} has a non-positive size "
697
+ f"({size_kib!r} KiB)")
698
+ end = off + size_kib * 1024
699
+ if off < 0 or end > limit:
700
+ raise ZephyrBoardEmitError(
701
+ f"{where}: partition {label!r} spans "
702
+ f"0x{off:x}..0x{end:x} but the mram_storage flash node is "
703
+ f"only {total_kib} KiB (0x0..0x{limit:x}) -- the partition "
704
+ "would sit outside the flash device it is declared in")
705
+ if off < prev_end:
706
+ raise ZephyrBoardEmitError(
707
+ f"{where}: partition {label!r} at 0x{off:x} overlaps "
708
+ f"{prev_label!r}, which ends at 0x{prev_end:x}")
709
+ prev_label, prev_end = label, end
710
+
711
+
712
+ def _aen_check_map_overlaps(
713
+ memory_map: "list[dict[str, Any]]", mram_base: int, total_kib: int,
714
+ ) -> None:
715
+ """Cross-check the WHOLE declared `memory_map:`, not just the regions
716
+ this core's partition table happens to name.
717
+
718
+ The sibling core's `<role>_slot0` window is not a partition here, so
719
+ _aen_check_extents cannot see a map that overlaps it; this can.
720
+ Regions with a non-integer `base` (a `TBD` sentinel) are skipped --
721
+ they are declarations of intent, not placements. No shipped AEN
722
+ preset authors one today (#2053 resolved the last holdout,
723
+ `mram_main`, on all seven), but the skip stays live for any future
724
+ region or preset that does.
725
+
726
+ A region whose resolved extent equals the App MRAM window EXACTLY
727
+ (the whole-device alias -- `mram_main`, on every AEN preset since
728
+ #2053 resolved its `base` off the `"TBD"` sentinel) is excluded from
729
+ the pairwise overlap comparison below -- it deliberately spans the
730
+ same window `mcuboot` /
731
+ `he_slot0` / `hp_slot0` / `reserved` / `storage` / `atoc` subdivide,
732
+ so comparing it against its own partitions would flag every one of
733
+ them. `whole_device_alias.is_whole_device_alias()` carries the
734
+ identical "extent == aperture exactly" predicate `classify_region()`
735
+ and `is_partition_inside_aperture()` (`tan/planner/aperture.py`)
736
+ already use for this same case --
737
+ imported by both from one sibling module, whose own docstring
738
+ records why it stays a separate file, so those two can't drift
739
+ (alp-sdk#2073). Upstream additionally records that alp-sdk's own
740
+ `check_atoc_reservation.py` hand-writes this same comparison instead
741
+ of importing it; tan relocates no counterpart of that gate, so that
742
+ gap is alp-sdk's alone and the call sites named above are the whole
743
+ surface here. The exclusion is bounds-checked like every other row
744
+ and applies to nothing looser than an exact match: a region that
745
+ merely CONTAINS another without matching the aperture exactly still
746
+ overlaps and is still refused below; and more than one row matching
747
+ the aperture exactly is refused outright, not silently allowed to
748
+ co-exist unchecked against each other.
749
+ """
750
+ placed = [
751
+ (str(r.get("name")), r["base"], int(r["size_kib"]))
752
+ for r in memory_map
753
+ if isinstance(r.get("base"), int) and isinstance(r.get("size_kib"), int)
754
+ ]
755
+ limit = mram_base + total_kib * 1024
756
+ aperture = (mram_base, limit)
757
+ for name, base, size_kib in placed:
758
+ end = base + size_kib * 1024
759
+ if base < mram_base or end > limit:
760
+ raise ZephyrBoardEmitError(
761
+ f"AEN memory_map region {name!r} spans "
762
+ f"0x{base:x}..0x{end:x}, outside the {total_kib} KiB App "
763
+ f"MRAM window 0x{mram_base:x}..0x{limit:x} declared by this "
764
+ "variant's mram_mb")
765
+ aliases = [
766
+ (name, base, size_kib) for name, base, size_kib in placed
767
+ if is_whole_device_alias((base, base + size_kib * 1024), aperture)
768
+ ]
769
+ if len(aliases) > 1:
770
+ alias_names = ", ".join(repr(name) for name, _b, _s in aliases)
771
+ raise ZephyrBoardEmitError(
772
+ f"AEN memory_map declares {len(aliases)} regions ({alias_names}) "
773
+ f"whose extent equals the {total_kib} KiB App MRAM window "
774
+ f"0x{mram_base:x}..0x{limit:x} exactly -- only one whole-device "
775
+ "alias to the App MRAM window is allowed; rename or remove the "
776
+ "duplicate(s).")
777
+ overlap_candidates = [row for row in placed if row not in aliases]
778
+ overlap_candidates.sort(key=lambda r: r[1])
779
+ for (a_name, a_base, a_kib), (b_name, b_base, _b_kib) in zip(
780
+ overlap_candidates, overlap_candidates[1:]):
781
+ a_end = a_base + a_kib * 1024
782
+ if b_base < a_end:
783
+ raise ZephyrBoardEmitError(
784
+ f"AEN memory_map regions {a_name!r} (ends 0x{a_end:x}) and "
785
+ f"{b_name!r} (starts 0x{b_base:x}) overlap")
786
+
787
+
788
+ def _aen_slot0_sizes_display(memory_map: "list[dict[str, Any]]") -> str:
789
+ """`"HE 2688 KiB + HP 2688 KiB"` -- every declared per-core code slot,
790
+ in `memory_map:` declaration order.
791
+
792
+ The #1069 rationale sentence in the generated `.dts` used to carry
793
+ the AEN801-only literals "both 2688 KiB code slots fit the 5632 KiB
794
+ App MRAM alongside the ~2.6 MiB NPU MRAM-model budget" unconditionally,
795
+ ~100 lines above a partition map derived from this same
796
+ `memory_map:` -- so any dual-M55 AEN SoM with different slot sizes
797
+ emitted a board file stating its MRAM budget two contradictory ways,
798
+ with the prose (the part an engineer sizing an NPU model reads)
799
+ being the wrong one. The `~2.6 MiB` figure is a policy number from
800
+ `zephyr/sysbuild/aen/README.md` that no generator input carries, so
801
+ it is not interpolated -- the sentence points at the README instead.
802
+ """
803
+ suffix = "_slot0"
804
+ parts = [
805
+ f"{str(r['name'])[: -len(suffix)].upper()} {r['size_kib']} KiB"
806
+ for r in memory_map
807
+ if isinstance(r.get("name"), str) and r["name"].endswith(suffix)
808
+ ]
809
+ return " + ".join(parts)
810
+
811
+
812
+ def _aen_raise_missing_region(
813
+ region_name: str, label: str, metadata_root: Path,
814
+ ) -> NoReturn:
815
+ """Raise the right refusal for a missing disjoint-slot0 region.
816
+
817
+ `atoc` is a ported requirement (alp-sdk#1289, tan-cli#591) that no
818
+ released alp-sdk carries yet: checked FIRST, via `require_capability`
819
+ against the bound checkout's OWN `metadata/quality-tasks-v1.json`
820
+ registry -- never against the shape of THIS SoM preset's map. A
821
+ checkout that doesn't know the concept raises `SdkTooOldError` naming
822
+ it; only once the checkout is proven new enough does an absent `atoc`
823
+ region fall through to the generic authoring-gap message below, same as
824
+ every other region name.
825
+
826
+ This used to be a heuristic instead: "every OTHER required region is
827
+ present and placed, so this preset is a complete pre-#1289 layout" --
828
+ inferring the checkout's vintage from the shape of one board's map.
829
+ Imprecise by construction (a hand-edited preset that merely LOOKS
830
+ pre-#1289-shaped hit the same wording as a genuinely old checkout);
831
+ `require_capability` answers the real question directly instead.
832
+ """
833
+ if region_name == "atoc":
834
+ require_capability(metadata_root, AEN_ATOC_RESERVATION)
835
+ raise ZephyrBoardEmitError(
836
+ f"AEN disjoint-slot0 memory_map is missing an integer-`base` "
837
+ f"region named {region_name!r} (needed for {label!r}) -- this SoM "
838
+ "preset's `memory_map:` is incomplete; add the region, or drop the "
839
+ "per-core `<role>_slot0` windows to fall back to the stock "
840
+ "symmetric two-slot layout")
841
+
842
+
843
+ def _aen_flash_partitions(
844
+ total_kib: int, metadata_root: Path, role: str | None = None,
845
+ memory_map: "list[dict[str, Any]] | None" = None,
846
+ ) -> "tuple[int, list[tuple[str, int, int]]]":
847
+ """Return (mram_base, [(label, offset_bytes, size_kib), ...]).
848
+
849
+ *metadata_root* is threaded through to `_aen_raise_missing_region`,
850
+ which needs it to answer "does the bound alp-sdk checkout know about
851
+ `atoc` at all" before blaming this SoM preset for omitting it
852
+ (tan-cli#591).
853
+
854
+ *mram_base* is the absolute address the emitted `mram_storage`
855
+ soc-nv-flash child is anchored at, i.e. the origin every returned
856
+ offset is relative to. It is returned rather than re-derived by the
857
+ caller so the DTS node address and the partition offsets can never
858
+ disagree -- `_aen_dts` used to write the node address as a hardcoded
859
+ `0x80000000` literal while the offsets came from the metadata base.
860
+
861
+ Stock layout (no per-role memory_map entry for *role*): the
862
+ original symmetric two-slot MCUboot swap-using-scratch map --
863
+ mcuboot, image-0 (primary/code-partition), image-1 (secondary,
864
+ OTA), scratch, storage -- unchanged. This now applies only to a
865
+ genuinely single-M55 AEN SoM that declares no `<role>_slot0`
866
+ region. E1M-AEN401 and E1M-AEN601 are DUAL-M55 (see
867
+ `_aen_role_slot0_map` above) and have generated the disjoint-slot0
868
+ layout below since alp-sdk#1445 -- `_aen_require_disjoint_slot0` refuses
869
+ to let a dual-M55 SoM fall back to this stock layout at all
870
+ (alp-sdk#1482, tan-cli#896: docstring-only re-sync, "still what
871
+ every single-M55 AEN SKU (aen401, aen601) generates" was stale
872
+ prose, not a behavioural gap -- both SKUs already generated the
873
+ disjoint layout below).
874
+
875
+ Disjoint-slot0 layout (*role*'s `<role>_slot0` memory_map entry is
876
+ declared, #1069): mcuboot, this role's own slot0 (labelled
877
+ "image-0" so downstream label maps / the `image_kib` lookup in
878
+ _aen_dts don't need a second code path), reserved (the ex-scratch
879
+ headroom, unused -- OTA is deferred since a swap-sized second slot
880
+ on both cores no longer fits the MRAM budget), storage, atoc. No
881
+ image-1: this is CONFIG_SINGLE_APPLICATION_SLOT=y (see
882
+ zephyr/sysbuild/aen/README.md), the only silicon-proven MCUboot
883
+ mode on this part. The OTHER role's slot0 lives outside this
884
+ table entirely (a disjoint physical window this board never
885
+ touches) -- see metadata/e1m_modules/E1M-AEN801.yaml `memory_map:`
886
+ for the full 6-region physical map.
887
+
888
+ BOTH layouts end in `atoc` (alp-sdk#1289) -- the SE-owned band SETOOLS
889
+ top-anchors the ATOC application table into. It is a declared,
890
+ named region precisely so no app can mount NVS or a filesystem on
891
+ top of the boot table: before this, the last region was `storage`
892
+ in both branches, and on the bench a slot0 app happily erased
893
+ inside it while the live ATOC sat 0x1EA50 further up the SAME
894
+ partition. Keep `atoc` last in both paths; see _AEN_ATOC_KIB for
895
+ the sizing evidence.
896
+ """
897
+ slot0_region = _aen_role_slot0_map(memory_map, role) if role else None
898
+ if slot0_region is not None:
899
+ by_name = {r["name"]: r for r in memory_map}
900
+ mcuboot_region = by_name.get("mcuboot")
901
+ if mcuboot_region is None or not isinstance(mcuboot_region.get("base"), int):
902
+ raise ZephyrBoardEmitError(
903
+ "AEN disjoint-slot0 memory_map is missing an integer-`base` "
904
+ "'mcuboot' region -- its base anchors the soc-nv-flash "
905
+ "child's offset-0 origin")
906
+ mram_base = mcuboot_region["base"]
907
+ if mram_base != _AEN_MRAM_BASE:
908
+ raise ZephyrBoardEmitError(
909
+ f"AEN memory_map anchors 'mcuboot' at 0x{mram_base:x}, but "
910
+ f"the Ensemble App MRAM window starts at "
911
+ f"0x{_AEN_MRAM_BASE:x} (upstream Zephyr declares `mram: "
912
+ "flash@80000000` in every ensemble SoC dtsi). The mcuboot "
913
+ "base IS the soc-nv-flash child's offset-0 origin, so a map "
914
+ "that starts elsewhere would place every partition at the "
915
+ "wrong absolute address")
916
+ _aen_check_map_overlaps(memory_map, mram_base, total_kib)
917
+ out: list[tuple[str, int, int]] = []
918
+ for label, region_name in (
919
+ ("mcuboot", "mcuboot"),
920
+ ("image-0", f"{role}_slot0"),
921
+ ("reserved", "reserved"),
922
+ ("storage", "storage"),
923
+ ("atoc", "atoc"),
924
+ ):
925
+ region = by_name.get(region_name)
926
+ if region is None or not isinstance(region.get("base"), int):
927
+ _aen_raise_missing_region(region_name, label, metadata_root)
928
+ out.append((label, region["base"] - mram_base, region["size_kib"]))
929
+ _aen_check_extents(out, total_kib, "AEN disjoint-slot0 memory_map")
930
+ return mram_base, out
931
+
932
+ reserved = (_AEN_MCUBOOT_KIB + _AEN_SCRATCH_KIB + _AEN_STORAGE_KIB
933
+ + _AEN_ATOC_KIB)
934
+ remaining = total_kib - reserved
935
+ if remaining <= 0 or remaining % 2:
936
+ raise ZephyrBoardEmitError(
937
+ f"AEN MRAM size {total_kib} KiB doesn't split evenly into two "
938
+ f"image slots after reserving {reserved} KiB for "
939
+ "mcuboot/scratch/storage/atoc")
940
+ image_kib = remaining // 2
941
+
942
+ offset = 0
943
+ out: list[tuple[str, int, int]] = []
944
+ for label, size_kib in (
945
+ ("mcuboot", _AEN_MCUBOOT_KIB),
946
+ ("image-0", image_kib),
947
+ ("image-1", image_kib),
948
+ ("image-scratch", _AEN_SCRATCH_KIB),
949
+ ("storage", _AEN_STORAGE_KIB),
950
+ # LAST, and must stay last: this is the band SETOOLS top-anchors the
951
+ # ATOC into (alp-sdk#1289). The `offset != total_kib * 1024`
952
+ # assertion below is what keeps it flush against the window top.
953
+ ("atoc", _AEN_ATOC_KIB),
954
+ ):
955
+ out.append((label, offset, size_kib))
956
+ offset += size_kib * 1024
957
+ if offset != total_kib * 1024:
958
+ raise ZephyrBoardEmitError("AEN flash partition arithmetic didn't sum to total")
959
+ _aen_check_extents(out, total_kib, "AEN stock two-slot layout")
960
+ return _AEN_MRAM_BASE, out
961
+
962
+
963
+ def _aen_console_pinmux_rows(metadata_root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
964
+ """Return (rx_row, tx_row) for the E1M `UART0` pad from `pinmux/aen.yaml`."""
965
+ pm = _load_yaml(metadata_root / "pinmux" / "aen.yaml")
966
+ rows = {r.get("e1m_function"): r for r in (pm.get("pads") or [])
967
+ if r.get("e1m_function") in ("UART0_TX", "UART0_RX")}
968
+ if "UART0_RX" not in rows or "UART0_TX" not in rows:
969
+ raise ZephyrBoardEmitError(
970
+ "metadata/pinmux/aen.yaml has no UART0_TX/UART0_RX rows for the "
971
+ "E1M console pad")
972
+ return rows["UART0_RX"], rows["UART0_TX"]
973
+
974
+
975
+ def _pin_macro(row: dict[str, Any]) -> str:
976
+ return f"PIN_{row['silicon_pad']}__{row['silicon_peripheral']}"
977
+
978
+
979
+ def _uart_node_label(row: dict[str, Any]) -> str:
980
+ m = re.match(r"^([A-Za-z]+\d+)", row["silicon_peripheral"])
981
+ if not m:
982
+ raise ZephyrBoardEmitError(
983
+ "can't derive a UART node label from silicon_peripheral "
984
+ f"{row['silicon_peripheral']!r}")
985
+ return m.group(1).lower()
986
+
987
+
988
+ # ---------------------------------------------------------------------
989
+ # AEN on-module (non-edge) links: BRD_I2C + its devices, the RTC alarm
990
+ # ---------------------------------------------------------------------
991
+
992
+
993
+ def _load_aen_on_module_links(metadata_root: Path) -> dict[str, Any]:
994
+ """Load + shape-check `metadata/e1m_modules/aen/on-module-links.yaml`.
995
+
996
+ The AEN counterpart of `_load_supervisor_links()`, and the ONLY
997
+ authority for pads that reach no E1M edge pin: `metadata/pinmux/aen.yaml`
998
+ is a generated projection of the EDGE pad TSVs and carries no row for
999
+ P7_0 / P7_1 (BRD_I2C) or P15_0 (RTC_ALARM), which run SoC <-> on-module
1000
+ chip and never leave the module. `e1m_i2c0` (SoC I2C2) is the one
1001
+ exception -- its pads DO have an edge row (it also reaches the EVK
1002
+ carrier's sensor bus), but its DT-node shape lives here too because the
1003
+ SoM's own 24C128 manifest EEPROM sits on the same physical bus (see the
1004
+ `e1m_i2c0` entry's own comment in on-module-links.yaml). Family-scoped
1005
+ -- one file backs every AEN SKU and both M55 cores.
1006
+ """
1007
+ path = metadata_root / "e1m_modules" / "aen" / "on-module-links.yaml"
1008
+ if not path.is_file():
1009
+ raise ZephyrBoardEmitError(
1010
+ f"no {path} -- the AEN pinctrl.dtsi/.dts emitters need the "
1011
+ "on-module pad + device source (BRD_I2C, e1m_i2c0, the RV-3028 "
1012
+ "alarm)")
1013
+ doc = _load_yaml(path)
1014
+ links = doc.get("on_module_links")
1015
+ if not isinstance(links, dict):
1016
+ raise ZephyrBoardEmitError(f"{path} has no on_module_links: block")
1017
+ for key in ("brd_i2c", "e1m_i2c0", "rtc_alarm"):
1018
+ if key not in links:
1019
+ raise ZephyrBoardEmitError(
1020
+ f"{path} on_module_links: is missing {key!r}")
1021
+ risk = links["rtc_alarm"].get("risk")
1022
+ if risk is not None and not isinstance(risk, dict):
1023
+ raise ZephyrBoardEmitError(
1024
+ f"{path} on_module_links.rtc_alarm.risk must be a map keyed by "
1025
+ "SoC `part` designator (e.g. {\"E8\": \"...\"}), not a bare "
1026
+ "string -- a part-scoped risk note needs the part key (#1988)")
1027
+ bench_validation = links["e1m_i2c0"].get("bench_validation")
1028
+ if bench_validation is not None and not isinstance(bench_validation, dict):
1029
+ raise ZephyrBoardEmitError(
1030
+ f"{path} on_module_links.e1m_i2c0.bench_validation must be a map "
1031
+ "keyed by SoC `part` designator (e.g. {\"E8\": \"...\"}), not a "
1032
+ "bare string -- a part-scoped bench note needs the part key "
1033
+ "(#2046, following #1988's pattern)")
1034
+ return links
1035
+
1036
+
1037
+ def _c_comment(text: str, indent: str, width: int = 79) -> list[str]:
1038
+ """Reflow a metadata `evidence:` / `risk:` string into a C block comment.
1039
+
1040
+ Keeps the prose in the YAML (one source per hardware fact) instead of
1041
+ duplicating it as template text here.
1042
+ """
1043
+ body = " ".join(text.split())
1044
+ prefix = f"{indent} * "
1045
+ wrapped = textwrap.wrap(body, width=width - len(prefix)) or [""]
1046
+ return [f"{indent}/*"] + [f"{prefix}{line}".rstrip() for line in wrapped] + [f"{indent} */"]
1047
+
1048
+
1049
+ def _dt_property(name: str, value: Any) -> str:
1050
+ """One devicetree property line from a metadata `properties:` entry."""
1051
+ if isinstance(value, bool):
1052
+ if not value:
1053
+ raise ZephyrBoardEmitError(
1054
+ f"on-module-links.yaml property {name!r}: `false` has no "
1055
+ "devicetree meaning -- omit the property instead")
1056
+ return f"{name};"
1057
+ if isinstance(value, int):
1058
+ return f"{name} = <{value}>;"
1059
+ if isinstance(value, str):
1060
+ return f'{name} = "{value}";'
1061
+ raise ZephyrBoardEmitError(
1062
+ f"on-module-links.yaml property {name!r}: unsupported value type "
1063
+ f"{type(value).__name__}")
1064
+
1065
+
1066
+ def _aen_i2c_device_nodes(
1067
+ bus: dict[str, Any], links: dict[str, Any], indent: str = "\t",
1068
+ ) -> list[str]:
1069
+ """The `&i2c0` child nodes for the on-module devices, from metadata.
1070
+
1071
+ Only devices the metadata actually lists get a node -- the DNP=1 OPTIGA
1072
+ Trust M is absent from `devices:` for exactly that reason, so it can
1073
+ never be emitted as a phantom node.
1074
+ """
1075
+ lines: list[str] = []
1076
+ for dev in bus.get("devices") or []:
1077
+ lines.append("")
1078
+ if dev.get("evidence"):
1079
+ lines += _c_comment(dev["evidence"], indent)
1080
+ lines.append(f"{indent}{dev['node_label']}: {dev['node_name']} {{")
1081
+ lines.append(f'{indent}\tcompatible = "{dev["compatible"]}";')
1082
+ lines.append(f"{indent}\treg = <{dev['address_7bit']:#04x}>;")
1083
+ for name, value in (dev.get("properties") or {}).items():
1084
+ lines.append(f"{indent}\t{_dt_property(name, value)}")
1085
+ int_link = dev.get("int_gpio_link")
1086
+ if int_link:
1087
+ alarm = links[int_link]
1088
+ flag = "GPIO_ACTIVE_LOW" if alarm.get("active_low") else "GPIO_ACTIVE_HIGH"
1089
+ lines.append(
1090
+ f"{indent}\tint-gpios = <&{alarm['gpio_node']} "
1091
+ f"{alarm['gpio_pin']} {flag}>;")
1092
+ lines.append(f"{indent}}};")
1093
+ return lines
1094
+
1095
+
1096
+ def _aen_pinctrl_dtsi(
1097
+ role: str, sku_display: str, rx_row: dict[str, Any], tx_row: dict[str, Any],
1098
+ family_display: str, links: dict[str, Any], part: str,
1099
+ ) -> str:
1100
+ other_role = "he" if role == "hp" else "hp"
1101
+ rx_macro, tx_macro = _pin_macro(rx_row), _pin_macro(tx_row)
1102
+ uart_node = _uart_node_label(rx_row)
1103
+
1104
+ if role == "hp":
1105
+ confirm_block = (
1106
+ f" * (In an {other_role.upper()}+{role.upper()} AMP image the two cores share this single physical console;\n"
1107
+ " * during bring-up each core is flashed/run independently.) Confirmed against\n"
1108
+ " * the SoM netlist + metadata/e1m_modules/aen/from-alif.tsv.\n"
1109
+ )
1110
+ else:
1111
+ confirm_block = (
1112
+ " * Confirmed against the SoM netlist + metadata/e1m_modules/aen/from-alif.tsv\n"
1113
+ f" * (rows {tx_row['e1m_pad']} = {tx_row['e1m_function']} = "
1114
+ f"{tx_row['silicon_peripheral']}/{tx_row['silicon_pad']}, "
1115
+ f"{rx_row['e1m_pad']} = {rx_row['e1m_function']} = "
1116
+ f"{rx_row['silicon_peripheral']}/{rx_row['silicon_pad']}).\n"
1117
+ )
1118
+
1119
+ return (
1120
+ _COPYRIGHT_C +
1121
+ " *\n"
1122
+ f" * {sku_display} ({family_display}) Cortex-M55-{role.upper()} carrier pin control.\n"
1123
+ " *\n"
1124
+ " * Console: the E1M edge \"UART0\" maps to Alif "
1125
+ f"{uart_node.upper()} ({rx_row['silicon_pad']} RX_A / "
1126
+ f"{tx_row['silicon_pad']} TX_A) on\n"
1127
+ " * the AEN module PCB; the Alp E1M-EVK carrier wires its USB-UART console to it.\n"
1128
+ + confirm_block +
1129
+ " *\n"
1130
+ " *\n"
1131
+ " * BRD_I2C (SoC I2C0, function C) is wired here too: it is an ON-MODULE bus\n"
1132
+ " * carrying SoM-resident parts (RTC + temperature sensor), present on every\n"
1133
+ " * board built from this SoM -- not a per-app choice, so it belongs in the\n"
1134
+ " * board layer rather than in each consumer's overlay. Pads, macros and pad\n"
1135
+ " * config come from metadata/e1m_modules/aen/on-module-links.yaml.\n"
1136
+ " *\n"
1137
+ " * SoC I2C2 (function C, portable alp-i2c0) is wired here too, for the same\n"
1138
+ " * reason: the SoM's own 24C128 manifest EEPROM is bridge/DNP-selected onto\n"
1139
+ " * it, so every board built from this SoM needs the controller enabled --\n"
1140
+ " * even though the same pads also carry the EVK carrier's sensor bus once\n"
1141
+ " * they leave the module (docs/bring-up-aen.md Sec 5.1).\n"
1142
+ " *\n"
1143
+ " * Remaining GPIO / SPI / Ethernet pin groups are added alongside their\n"
1144
+ " * drivers (the alp-sdk Alif peripheral drivers).\n"
1145
+ " */\n"
1146
+ "\n"
1147
+ "#include <zephyr/dt-bindings/pinctrl/alif-ensemble-pinctrl.h>\n"
1148
+ "\n"
1149
+ "&pinctrl {\n"
1150
+ f"\tpinctrl_{uart_node}: pinctrl_{uart_node} {{\n"
1151
+ "\t\tgroup0 {\n"
1152
+ f"\t\t\tpinmux = <{rx_macro}>;\n"
1153
+ "\t\t\tinput-enable;\n"
1154
+ "\t\t};\n"
1155
+ "\n"
1156
+ "\t\tgroup1 {\n"
1157
+ f"\t\t\tpinmux = <{tx_macro}>;\n"
1158
+ "\t\t};\n"
1159
+ "\t};\n"
1160
+ "\n"
1161
+ + _aen_i2c_pinctrl_group(links) +
1162
+ "\n"
1163
+ + _aen_e1m_i2c0_pinctrl_group(links, part) +
1164
+ "};\n"
1165
+ )
1166
+
1167
+
1168
+ def _aen_i2c_pinctrl_group(links: dict[str, Any]) -> str:
1169
+ """The BRD_I2C pinctrl group (+ the RTC alarm pad), from metadata.
1170
+
1171
+ The alarm pad rides in THIS group rather than one of its own because
1172
+ `snps,designware-gpio` (the lpgpio controller's binding) has no
1173
+ `pinctrl-0` property: on Alif the pad input buffer is a PADCTRL bit only
1174
+ pinctrl can set, gpio_dw drives direction in the controller and never
1175
+ touches the pad, so an un-referenced group would simply never be applied
1176
+ and the alarm line would read 0 forever. Same pattern as P2_6 (the
1177
+ CC3501E READY input) riding in `pinctrl_spi1`.
1178
+ """
1179
+ bus = links["brd_i2c"]
1180
+ alarm = links["rtc_alarm"]
1181
+ sda = _pin_by_peripheral(bus["pins"], "I2C0_SDA_C")
1182
+ scl = _pin_by_peripheral(bus["pins"], "I2C0_SCL_C")
1183
+ return (
1184
+ "\t/*\n"
1185
+ f"\t * {sda['net'].rsplit('_', 1)[0]} = SoC {bus['peripheral']} function C: "
1186
+ f"{sda['silicon_pad']} {sda['net'].rsplit('_', 1)[-1]} / "
1187
+ f"{scl['silicon_pad']} {scl['net'].rsplit('_', 1)[-1]},\n"
1188
+ "\t * the on-module housekeeping bus (see the board .dts for the parts on it).\n"
1189
+ "\t *\n"
1190
+ "\t * BOTH pins get input-enable, in one group -- Alif's own DFP sets\n"
1191
+ f"\t * PADCTRL_READ_ENABLE on {sda['silicon_pad']} AND {scl['silicon_pad']}. "
1192
+ "With it on SDA only, the\n"
1193
+ "\t * i2c_dw controller cannot SENSE SCL (clock-stretch detect / arbitration)\n"
1194
+ "\t * and NACKs every address for a reason that looks electrical but is not.\n"
1195
+ "\t *\n"
1196
+ "\t * bias-pull-up is DSC=1, a REAL pull-up: pinctrl_soc.h's field comment\n"
1197
+ "\t * (soc/alif/ensemble/pinctrl_soc.h:24) and its ALIF_PINCTRL_BIAS_CFG()\n"
1198
+ "\t * macro (:44-48) both map bias-pull-up -> DSC=1 unconditionally -- there\n"
1199
+ "\t * is NO inversion between the devicetree property and the DSC field, on\n"
1200
+ "\t * this bus or the e1m_i2c0 one below (#2046 corrected that group's\n"
1201
+ "\t * comment, which used to claim the opposite). Do NOT copy the\n"
1202
+ "\t * I2C2/EEPROM overlay's bias-pull-down: DSC=2 is a pull-DOWN, harmless\n"
1203
+ "\t * there only because that bus has external carrier pull-ups (R137/R144).\n"
1204
+ "\t * This net has NO external pull-up at all, so a pull-down would park both\n"
1205
+ "\t * lines low and look like a busy bus.\n"
1206
+ "\t *\n"
1207
+ "\t * drive-open-drain is deliberately NOT set, so bit 23 [DRV] stays 0 =\n"
1208
+ "\t * PUSH-PULL: the pad actively drives the high phase instead of relying on\n"
1209
+ "\t * a pull-up this net does not have. input-schmitt-enable + max\n"
1210
+ "\t * drive-strength/slew-rate cover the resulting slow edges. Bench-proven\n"
1211
+ "\t * 2026-09-05 on E1M-AEN801 2626-R2: every non-response is a clean rc=-5\n"
1212
+ "\t * (-EIO) NACK, zero -ETIMEDOUT, zero \"User Abort\".\n"
1213
+ "\t */\n"
1214
+ f"\t{bus['pinctrl_group_label']}: {bus['pinctrl_group_label']} {{\n"
1215
+ "\t\tgroup0 {\n"
1216
+ f"\t\t\tpinmux = <{_pin_macro(sda)}>, <{_pin_macro(scl)}>;\n"
1217
+ "\t\t\tinput-enable;\n"
1218
+ "\t\t\tbias-pull-up;\n"
1219
+ "\t\t\tinput-schmitt-enable;\n"
1220
+ "\t\t\tdrive-strength = <12>;\n"
1221
+ "\t\t\tslew-rate = \"fast\";\n"
1222
+ "\t\t};\n"
1223
+ "\n"
1224
+ "\t\t/*\n"
1225
+ f"\t\t * {alarm['net']}: the RV-3028 /INT line on {alarm['silicon_pad']} "
1226
+ f"({alarm['gpio_node']} bit {alarm['gpio_pin']}).\n"
1227
+ "\t\t * Muxed + input-enabled here because snps,designware-gpio has no\n"
1228
+ "\t\t * pinctrl-0 of its own (see this function's docstring); without it the\n"
1229
+ "\t\t * pad input buffer stays off and the alarm reads 0 forever.\n"
1230
+ "\t\t */\n"
1231
+ "\t\tgroup1 {\n"
1232
+ f"\t\t\tpinmux = <{_pin_macro(alarm)}>;\n"
1233
+ "\t\t\tinput-enable;\n"
1234
+ "\t\t};\n"
1235
+ "\t};\n"
1236
+ )
1237
+
1238
+
1239
+ def _aen_e1m_i2c0_pinctrl_group(links: dict[str, Any], part: str) -> str:
1240
+ """The `e1m_i2c0` (SoC I2C2) pinctrl group, from metadata.
1241
+
1242
+ Unlike `_aen_i2c_pinctrl_group()` this bus carries NO alarm-style
1243
+ passenger pad and gets NO device child nodes in `_aen_e1m_i2c0_dts()`
1244
+ below (its only genuinely on-module device, the 24C128 EEPROM, is
1245
+ addressed by 7-bit address over the bus, not a DT device node -- see
1246
+ the `e1m_i2c0` entry's own comment in on-module-links.yaml).
1247
+
1248
+ `bias-pull-down` here is intentional and matches Alif's own reference
1249
+ I2C pinctrl (metadata says why). `part` (the Ensemble part designator
1250
+ this board tree is for, e.g. "E8") picks which of on_module_links'
1251
+ `e1m_i2c0.bench_validation` map's citations -- if any -- this comment
1252
+ carries: the only bench run on record is against the E8, and #2046
1253
+ found that citation being emitted unqualified into every OTHER AEN
1254
+ part's board tree (E3 included) as the same class of leak #1988 fixed
1255
+ for `rtc_alarm.risk`. Unlike that fix, a part with no entry here does
1256
+ NOT get silence -- #2046 is explicit that the citation must not be
1257
+ silently dropped, only qualified, because the emitted PAD VALUE
1258
+ (bias-pull-down) is unchanged and still correct for every part.
1259
+ """
1260
+ bus = links["e1m_i2c0"]
1261
+ sda = _pin_by_peripheral(bus["pins"], "I2C2_SDA_C")
1262
+ scl = _pin_by_peripheral(bus["pins"], "I2C2_SCL_C")
1263
+ bench = (bus.get("bench_validation") or {})
1264
+ this_part_bench = bench.get(part)
1265
+ if this_part_bench:
1266
+ bench_text = f"input-enable + bias-pull-down: {this_part_bench}"
1267
+ else:
1268
+ e8_bench = bench.get("E8", "")
1269
+ bench_text = (
1270
+ "input-enable + bias-pull-down: bench-validated ONLY on the E8 "
1271
+ f"-- {e8_bench} This board tree is for the {part}; that bench "
1272
+ f"run has NOT been independently repeated on the {part}'s own "
1273
+ "silicon (#2046, following the #1988 per-part-evidence "
1274
+ "pattern). The config below is still what this board tree "
1275
+ "emits -- on_module_links.yaml is one file for every AEN SKU "
1276
+ f"and no {part}-specific bench run exists yet to confirm or "
1277
+ "override it -- but that silence is not proof either way."
1278
+ )
1279
+ # Reflowed to house-style comment width (metadata prose has no line
1280
+ # breaks of its own to preserve, unlike the hand-wrapped paragraphs
1281
+ # below) -- the raw bench_validation string can run well past 79
1282
+ # columns unwrapped, same reason _c_comment() exists for evidence/risk
1283
+ # strings elsewhere in this file.
1284
+ bench_lines = [line + "\n" for line in _c_comment(bench_text, "\t")[1:-1]]
1285
+ return (
1286
+ "\t/*\n"
1287
+ f"\t * e1m_i2c0 = SoC {bus['peripheral']} function C: {sda['silicon_pad']} SDA / "
1288
+ f"{scl['silicon_pad']} SCL,\n"
1289
+ "\t * portable alp-i2c0 (E1M edge bus 0 -- ALP_E1M_I2C0 /\n"
1290
+ "\t * EVK_I2C_BUS_SENSORS). Carries the SoM's own 24C128 manifest EEPROM;\n"
1291
+ "\t * the same pads also reach the EVK carrier's sensor bus once they leave\n"
1292
+ "\t * the module (docs/bring-up-aen.md Sec 5.1 -- two separate buses).\n"
1293
+ "\t *\n"
1294
+ + "".join(bench_lines) +
1295
+ "\t *\n"
1296
+ "\t * bias-pull-down really is a pull-down (DSC=2), and bias-pull-up really\n"
1297
+ "\t * would be a pull-up (DSC=1) -- pinctrl_soc.h does NOT invert the two.\n"
1298
+ "\t * Its ALIF_PINCTRL_BIAS_CFG() macro (soc/alif/ensemble/pinctrl_soc.h:44-48)\n"
1299
+ "\t * maps bias-pull-up -> DSC=1 and bias-pull-down -> DSC=2 unconditionally,\n"
1300
+ "\t * exactly as the field's own comment at :24 describes. An earlier version\n"
1301
+ "\t * of this comment (and of examples/aen/aen-eeprom-manifest's overlay) blamed\n"
1302
+ "\t * bias-pull-up's reported dead bus on that encoding \"reading inverted vs\n"
1303
+ "\t * the Alif pad hardware\" -- pinctrl_soc.h has no such inversion, so that\n"
1304
+ "\t * explanation is RETRACTED (#2046), and the dead-bus report it was invented\n"
1305
+ "\t * to explain is UNCONFIRMED, not disproven: nobody has re-run bias-pull-up\n"
1306
+ "\t * here in a controlled bench trial since. What IS established, matching\n"
1307
+ "\t * the BRD_I2C group's own reasoning above: this bus, unlike BRD_I2C, has\n"
1308
+ "\t * the EVK carrier's R137/R144 pull-ups stuffed, so a real internal\n"
1309
+ "\t * pull-down is harmless -- the strong external pull-up dominates a weak\n"
1310
+ "\t * internal one -- which is why bias-pull-down is bench-proven correct as\n"
1311
+ "\t * emitted. Do NOT change it to bias-pull-up on the strength of either\n"
1312
+ "\t * reading; that needs a fresh register-level bench comparison, not a theory.\n"
1313
+ "\t */\n"
1314
+ f"\t{bus['pinctrl_group_label']}: {bus['pinctrl_group_label']} {{\n"
1315
+ "\t\tgroup0 {\n"
1316
+ # SCL, SDA -- matches the bench-validated overlay's own pinmux
1317
+ # order verbatim (examples/aen/aen-i2c2-eeprom-regcheck and
1318
+ # aen-eeprom-manifest), not the SDA-first order the BRD_I2C group
1319
+ # above happens to use.
1320
+ f"\t\t\tpinmux = <{_pin_macro(scl)}>, <{_pin_macro(sda)}>;\n"
1321
+ "\t\t\tinput-enable;\n"
1322
+ "\t\t\tbias-pull-down;\n"
1323
+ "\t\t};\n"
1324
+ "\t};\n"
1325
+ )
1326
+
1327
+
1328
+ def _aen_brd_i2c_dts(links: dict[str, Any], part: str) -> list[str]:
1329
+ """`&i2c0` (BRD_I2C) + its on-module device nodes, `&lpgpio`, and the
1330
+ aliases that make all three reachable portably.
1331
+
1332
+ Board layer, not a per-app overlay and not the SoC peripherals dtsi:
1333
+ these parts are soldered on the E1M-AEN SoM, so every app built for this
1334
+ board should get them for free -- while
1335
+ `zephyr/dts/alif/ensemble_e8_peripherals.dtsi` describes the E8 DIE, which
1336
+ every AEN SKU shares whether or not it carries these chips.
1337
+
1338
+ *part* (the SoC JSON's `part`, e.g. ``"E8"``, ``"E3"``) gates the
1339
+ alarm-node risk comment below -- see that block for why.
1340
+
1341
+ RELOCATED divergence, RETIRED (alp-sdk#1988 / tan-cli#493): this used to
1342
+ gate on a hardcoded ``if part == "E8"`` against a plain-string
1343
+ `on_module_links.rtc_alarm.risk` (tan's own independent fix for the same
1344
+ hazard `_aen_brd_i2c_dts` names -- an E8-only DFP warning bleeding into
1345
+ an E3/E4/E6 board tree). alp-sdk's own fix for the same issue landed
1346
+ later and reshapes the metadata field itself into a per-part map
1347
+ (``risk: {E8: "..."}``) rather than gating in code -- ported here to
1348
+ match: an unstated part gets no paragraph, and any future DFP that
1349
+ confirms the same quirk is added to the map, not to a code branch.
1350
+ """
1351
+ bus = links["brd_i2c"]
1352
+ alarm = links["rtc_alarm"]
1353
+ sda = _pin_by_peripheral(bus["pins"], "I2C0_SDA_C")
1354
+ scl = _pin_by_peripheral(bus["pins"], "I2C0_SCL_C")
1355
+ devices = bus.get("devices") or []
1356
+
1357
+ lines = [
1358
+ "/*",
1359
+ f" * BRD_I2C -- the on-module housekeeping bus: SoC {bus['peripheral']} in its function-C",
1360
+ f" * muxing ({sda['silicon_pad']} SDA / {scl['silicon_pad']} SCL), NOT the slave-only LPI2C0 it was",
1361
+ " * believed to be before #1848. Driven by UPSTREAM",
1362
+ ' * Zephyr i2c_dw ("snps,designware-i2c"), ADR 0017 Tier-1 -- no vendored code.',
1363
+ " *",
1364
+ " * ISOLATED: R93/R94 (the 0-ohm bridge to the I2C2/EEPROM segment) are DNP, so",
1365
+ " * the 24C128 manifest EEPROM is NOT on this bus -- it stays on i2c2. The",
1366
+ " * OPTIGA Trust M (IC1, @0x30) is DNP=1 on this batch and so has NO node here;",
1367
+ " * its silence is a negative control, not a fault.",
1368
+ " *",
1369
+ " * Both device drivers are upstream and Kconfig `default y` on their own",
1370
+ " * DT_HAS_*_ENABLED, so an app needs only CONFIG_RTC / CONFIG_SENSOR in its",
1371
+ " * prj.conf -- the board layer deliberately forces neither.",
1372
+ " */",
1373
+ f"&{bus['dt_label']} {{",
1374
+ '\tstatus = "okay";',
1375
+ f"\tpinctrl-0 = <&{bus['pinctrl_group_label']}>;",
1376
+ '\tpinctrl-names = "default";',
1377
+ f"\tclock-frequency = <{bus['clock_frequency_macro']}>;",
1378
+ ]
1379
+ lines += _aen_i2c_device_nodes(bus, links)
1380
+ lines += ["};", ""]
1381
+
1382
+ lines += [
1383
+ "/*",
1384
+ f" * {alarm['gpio_node']} -- needed so the RV-3028 alarm on {alarm['silicon_pad']} "
1385
+ f"({alarm['net']}, bit {alarm['gpio_pin']} =>",
1386
+ f" * IRQ {alarm['gpio_irq']}) is usable as an interrupt source. Active LOW,",
1387
+ " * open-drain, pulled up on-module by R98 (100k to +1V8, FITTED). The pad",
1388
+ " * itself is muxed in the pinctrl dtsi's BRD_I2C group (that binding has no",
1389
+ " * pinctrl-0 of its own).",
1390
+ ]
1391
+ # The port-15 control-register warning, straight from the metadata's own
1392
+ # per-part `risk:` map (alp-sdk#1988) -- evidenced only against this
1393
+ # board's own SoC part's DFP: on-module-links.yaml's `risk:` names only
1394
+ # "The AE822 DFP" -- the E8 part's datasheet -- so a family-scoped
1395
+ # emission bled E8 register-layout guidance into an E1M-AEN301 (E3)
1396
+ # board tree (tan-cli#493's
1397
+ # test_a_non_e8_aen_sku_includes_its_own_peripherals_overlay caught it).
1398
+ # A part with no entry in the map gets no paragraph: an unstated DFP
1399
+ # citation is not evidence for this silicon.
1400
+ risk_text = (alarm.get("risk") or {}).get(part)
1401
+ if risk_text:
1402
+ lines += [" *"]
1403
+ lines += _c_comment(risk_text, "")[1:-1]
1404
+ lines += [
1405
+ " */",
1406
+ f"&{alarm['gpio_node']} {{",
1407
+ '\tstatus = "okay";',
1408
+ "};",
1409
+ "",
1410
+ ]
1411
+
1412
+ alias_lines = [f"\t\t{bus['alias']} = &{bus['dt_label']};"]
1413
+ for dev in devices:
1414
+ if dev.get("alias"):
1415
+ alias_lines.append(f"\t\t{dev['alias']} = &{dev['node_label']};")
1416
+ lines += [
1417
+ "/*",
1418
+ " * Portable aliases. The bus alias is what alp_i2c_open(.bus_id = N)",
1419
+ " * resolves (src/backends/i2c/zephyr_drv.c -> DT_ALIAS(alp_i2cN)), matching",
1420
+ " * the on-module EEPROM's own convention on i2c2; the device aliases let an",
1421
+ " * example say DT_ALIAS(rtc) / DT_ALIAS(alp_temp0) instead of hardcoding",
1422
+ " * this board's node labels.",
1423
+ " */",
1424
+ "/ {",
1425
+ "\taliases {",
1426
+ *alias_lines,
1427
+ "\t};",
1428
+ "};",
1429
+ "",
1430
+ ]
1431
+ return lines
1432
+
1433
+
1434
+ def _aen_e1m_i2c0_dts(links: dict[str, Any]) -> list[str]:
1435
+ """`&i2c2` (e1m_i2c0, portable alp-i2c0) + its bus alias.
1436
+
1437
+ No device child nodes (see `_aen_e1m_i2c0_pinctrl_group()`'s
1438
+ docstring): the only genuinely on-module part on this bus, the 24C128
1439
+ manifest EEPROM, is addressed by 7-bit address over the bus rather
1440
+ than a DT device node, the same way
1441
+ examples/aen/aen-i2c2-eeprom-regcheck and
1442
+ examples/aen/aen-eeprom-manifest already open it. The EVK carrier's
1443
+ sensor/IO-expander/INA236 parts that also ride this bus once it
1444
+ leaves the module are the EVK board layer's concern, not this SoM
1445
+ board layer's.
1446
+ """
1447
+ bus = links["e1m_i2c0"]
1448
+ sda = _pin_by_peripheral(bus["pins"], "I2C2_SDA_C")
1449
+ scl = _pin_by_peripheral(bus["pins"], "I2C2_SCL_C")
1450
+
1451
+ return [
1452
+ "/*",
1453
+ f" * e1m_i2c0 -- SoC {bus['peripheral']} in its function-C muxing "
1454
+ f"({sda['silicon_pad']} SDA / {scl['silicon_pad']} SCL), portable",
1455
+ f" * {bus['alias']} (E1M edge bus 0). Carries the SoM's own 24C128 manifest",
1456
+ " * EEPROM; the same physical bus also reaches the EVK carrier's sensor bus",
1457
+ " * once the pads leave the module (docs/bring-up-aen.md Sec 5.1). Driven by",
1458
+ ' * UPSTREAM Zephyr i2c_dw ("snps,designware-i2c"), ADR 0017 Tier-1 -- no',
1459
+ " * vendored code.",
1460
+ " */",
1461
+ f"&{bus['dt_label']} {{",
1462
+ '\tstatus = "okay";',
1463
+ f"\tpinctrl-0 = <&{bus['pinctrl_group_label']}>;",
1464
+ '\tpinctrl-names = "default";',
1465
+ f"\tclock-frequency = <{bus['clock_frequency_macro']}>;",
1466
+ "};",
1467
+ "",
1468
+ "/*",
1469
+ " * Portable alias for e1m_i2c0. alp_i2c_open(.bus_id = ALP_E1M_I2C0)",
1470
+ " * resolves this via src/backends/i2c/zephyr_drv.c -> DT_ALIAS(alp_i2c0);",
1471
+ " * matches the EEPROM manifest reader's own",
1472
+ " * CONFIG_ALP_SDK_HW_INFO_EEPROM_I2C_BUS_ID=0 convention.",
1473
+ " */",
1474
+ "/ {",
1475
+ "\taliases {",
1476
+ f"\t\t{bus['alias']} = &{bus['dt_label']};",
1477
+ "\t};",
1478
+ "};",
1479
+ "",
1480
+ ]
1481
+
1482
+
1483
+ def _aen_ethos_u(soc_spec: dict[str, Any]) -> tuple[str, str] | None:
1484
+ """The Arm Ethos-U accelerator this SoC's silicon carries, as
1485
+ ``(accelerator_config_kconfig, dt_node_label)`` -- or ``None`` when the
1486
+ SoC has no NPU.
1487
+
1488
+ A SoC carrying more than one Ethos-U variant (the E8 has 2x U55 + 1x U85)
1489
+ builds the core driver for its MOST capable NPU -- the U85 -- because the
1490
+ upstream Arm Ethos-U driver compiles exactly one accelerator config per
1491
+ image (``ETHOS_U_NPU_CONFIG`` is a Kconfig ``choice``). Vela targets must
1492
+ match (``--accelerator-config ethos-u85-256`` for the U85 case).
1493
+
1494
+ Used only to flip the (already-declared, disabled) NPU DT node to
1495
+ ``status="okay"`` on the generated silicon board -- harmless on its own
1496
+ (``CONFIG_ETHOS_U`` is ``default n``, so no driver binds until an inference
1497
+ app selects it via ``ALP_SDK_INFERENCE_BACKEND_ETHOS_U_AEN``).
1498
+ """
1499
+ caps = soc_spec.get("capabilities") or {}
1500
+ if (caps.get("ethos_u85_count") or 0) > 0:
1501
+ return ("ETHOS_U85_256", "ethosu85")
1502
+ if (caps.get("ethos_u55_count") or 0) > 0:
1503
+ return ("ETHOS_U55_256", "ethosu55")
1504
+ return None
1505
+
1506
+
1507
+ def _aen_defconfig(
1508
+ uart_node: str, rx_row: dict[str, Any], tx_row: dict[str, Any],
1509
+ slot0_base: int,
1510
+ ) -> str:
1511
+ # CONFIG_USE_DT_CODE_PARTITION is the Kconfig half of the very
1512
+ # `zephyr,code-partition = &slot0_partition;` chosen that _aen_dts()
1513
+ # (below) lays down for every board this function serves -- the two are
1514
+ # emitted as a pair, from the same MRAM partition map, so the board can
1515
+ # never carry the DT fact without the Kconfig that honours it. Without
1516
+ # it Zephyr leaves FLASH_LOAD_OFFSET at 0 and the image links at the MRAM
1517
+ # base 0x80000000, on top of the boot partition: the reset vector comes
1518
+ # out 0x8000xxxx, which `alif_flash` rejects ("unrecognised reset vector
1519
+ # ... expected a slot0-XIP image") and which faults if it is written
1520
+ # anyway. Set here, in the board layer, rather than in each app's
1521
+ # prj.conf, because the link offset is a property of THIS BOARD's flash
1522
+ # map, not of any app -- so it reaches every consumer of the board
1523
+ # equally (plain `west build`, twister, the board.yaml planner, `tan`).
1524
+ # It is also where upstream Zephyr boards with a fixed bootloader slot
1525
+ # set it (`grep -l USE_DT_CODE_PARTITION=y zephyr/boards/**/*_defconfig`).
1526
+ #
1527
+ # A Flow C ITCM RAM-run deliberately undoes it (a board defconfig is the
1528
+ # lowest-precedence Kconfig layer): `scripts/bench/aen/aen-flowc-itcm.conf`
1529
+ # + `aen-flowc-itcm.overlay`. MCUboot's own image is unaffected -- it
1530
+ # re-points the chosen at &boot_partition via mcuboot's app.overlay.
1531
+ return (
1532
+ _COPYRIGHT_HASH +
1533
+ "\n"
1534
+ "CONFIG_ARM_MPU=y\n"
1535
+ "CONFIG_HW_STACK_PROTECTION=y\n"
1536
+ "\n"
1537
+ "# Link the image into the board's `zephyr,code-partition`\n"
1538
+ f"# (slot0_partition, MRAM 0x{slot0_base:08x}) instead of the MRAM base --\n"
1539
+ "# without it the reset vector lands at 0x8000xxxx and no AEN flash\n"
1540
+ "# flow accepts the image. Flow C (ITCM RAM-run) overrides it via\n"
1541
+ "# scripts/bench/aen/aen-flowc-itcm.conf.\n"
1542
+ "CONFIG_USE_DT_CODE_PARTITION=y\n"
1543
+ "\n"
1544
+ # Silicon pads read from metadata/pinmux/aen.yaml, exactly as the
1545
+ # sibling .dts and -pinctrl.dtsi do -- they used to be the string
1546
+ # literal "P3_4/P3_5", the AEN801 console pads, on every SKU.
1547
+ f"# Console: Alif {uart_node.upper()} (E1M edge \"UART0\", "
1548
+ f"{rx_row['silicon_pad']}/{tx_row['silicon_pad']}) "
1549
+ "-- NS16550-class UART.\n"
1550
+ "CONFIG_SERIAL=y\n"
1551
+ "CONFIG_CONSOLE=y\n"
1552
+ "CONFIG_UART_CONSOLE=y\n"
1553
+ "CONFIG_UART_INTERRUPT_DRIVEN=y\n"
1554
+ "CONFIG_UART_NS16550=y\n"
1555
+ )
1556
+
1557
+
1558
+ # Board-level LOG_MODE default for every AEN board (alp-sdk issue #1373,
1559
+ # ported from alp-sdk#1407). Emitted into the generated `Kconfig.defconfig`;
1560
+ # the two hand-authored AEN board trees (e1m_aen401_m55_hp, e1m_aen601_m55_hp)
1561
+ # carry a byte-identical copy on the alp-sdk side, kept in step by that repo's
1562
+ # own `tests/scripts/test_gen_zephyr_board.py`.
1563
+ #
1564
+ # WHY: Zephyr's own `choice LOG_MODE` default is LOG_MODE_DEFERRED
1565
+ # (subsys/logging/Kconfig.mode), which no AEN board ever set or unset -- it
1566
+ # is inherited. Deferred mode hands every record to the LOG_PROCESS_THREAD
1567
+ # (CONFIG_LOG_PROCESS_THREAD is `default y`), and CONFIG_LOG_PRINTK is
1568
+ # `default y if PRINTK`, so printk() -- including the Alp SDK boot banner --
1569
+ # is deferred with it. The AEN bench procedure deliberately runs apps whose
1570
+ # main() never yields (`for (;;) { k_busy_wait(1000); }`) because an idling
1571
+ # M55 makes the Secure Enclave gate the DAP and the SE-UART together. A
1572
+ # main() that never yields never lets the log thread run: with
1573
+ # CONFIG_LOG_PROCESS_THREAD_CUSTOM_PRIORITY=n (the default) log_core.c runs
1574
+ # that thread at K_LOWEST_APPLICATION_THREAD_PRIO (= CONFIG_NUM_PREEMPT_
1575
+ # PRIORITIES - 1), strictly below main's CONFIG_MAIN_THREAD_PRIORITY=0, and
1576
+ # time-slicing rotates only among READY threads of EQUAL priority -- so a
1577
+ # busy-looping main() at 0 starves it outright and a healthy, fault-free
1578
+ # board emits ZERO bytes. Measured on E1M-AEN801 silicon: PC inside
1579
+ # `z_impl_k_busy_wait`, IPSR = 000 (NoException), CFSR @ 0xE000ED28 =
1580
+ # 00000000, zero UART bytes in a 15 s capture; the same source with
1581
+ # LOG_MODE_MINIMAL prints the banner and both LOG_INF lines. LOG_MODE_MINIMAL
1582
+ # formats and writes in the calling context, so it cannot be starved by a
1583
+ # non-yielding main().
1584
+ #
1585
+ # WHY A CHOICE `default` AND NOT `CONFIG_LOG_MODE_MINIMAL=y` IN THE BOARD
1586
+ # `_defconfig`: the `_defconfig` form assigns the symbol unconditionally,
1587
+ # including on the 47 `CONFIG_LOG=n` fragments under examples/aen/, where
1588
+ # the choice is invisible. Zephyr's scripts/kconfig/kconfig.py then runs
1589
+ # check_assigned_choice_values() and prints "The choice symbol
1590
+ # LOG_MODE_MINIMAL ... was selected (set =y), but no symbol ended up as the
1591
+ # choice selection" on every one of those builds. A Kconfig.defconfig
1592
+ # default is inert when LOG=n. Precedence is unchanged either way:
1593
+ # Kconfig.zephyr sources the board's Kconfig.defconfig ahead of subsys/
1594
+ # Kconfig precisely so board defaults outrank upstream ones, and an app that
1595
+ # wants the deferred backend pipeline still overrides this with
1596
+ # CONFIG_LOG_MODE_DEFERRED=y in its own prj.conf.
1597
+ _AEN_LOG_MODE_DEFAULT = (
1598
+ "# Logging: default to LOG_MODE_MINIMAL, not Zephyr's inherited\n"
1599
+ "# LOG_MODE_DEFERRED. Deferred mode needs CONFIG_LOG_PROCESS_THREAD to run,\n"
1600
+ "# and the AEN bench procedure runs apps whose main() never yields (a\n"
1601
+ "# non-yielding busy loop is what keeps the Secure Enclave from gating the\n"
1602
+ "# DAP and the SE-UART -- see docs/debugging-aen.md section 4). A\n"
1603
+ "# non-yielding main() starves the log thread, and because CONFIG_LOG_PRINTK\n"
1604
+ "# routes printk() through the same queue the Alp SDK banner disappears too:\n"
1605
+ "# a running, fault-free board prints ZERO bytes. Measured on E1M-AEN801\n"
1606
+ "# silicon (issue #1373) -- PC inside z_impl_k_busy_wait, IPSR = 000,\n"
1607
+ "# CFSR @ 0xE000ED28 = 00000000, and no UART output at all. Minimal mode\n"
1608
+ "# formats in the calling context, so the same app prints. An app that\n"
1609
+ "# wants the deferred pipeline (backends, runtime filtering, timestamps)\n"
1610
+ "# overrides this with CONFIG_LOG_MODE_DEFERRED=y in its prj.conf.\n"
1611
+ "choice LOG_MODE\n"
1612
+ "\tdefault LOG_MODE_MINIMAL\n"
1613
+ "endchoice\n"
1614
+ "\n"
1615
+ )
1616
+
1617
+
1618
+ def _aen_kconfig_defconfig(dir_name: str, role: str, part: str) -> str:
1619
+ board_sym = dir_name.upper()
1620
+ role_u = role.upper()
1621
+ # NUM_IRQS=480 is an Ensemble-FAMILY constant, not a per-part one:
1622
+ # upstream Zephyr defaults it to 480 for every RTSS cluster it ships
1623
+ # (soc/alif/ensemble/{e1c,e4,e6,e8}/Kconfig.defconfig.*_rtss_{hp,he}).
1624
+ # The part designator in the sentence is NOT -- it used to be the
1625
+ # literal "E8" on every E1M-AEN* board.
1626
+ return (
1627
+ _COPYRIGHT_HASH +
1628
+ "\n"
1629
+ f"if BOARD_{board_sym}\n"
1630
+ "\n"
1631
+ f"# The Ensemble {part} RTSS-{role_u} has CONFIG_NUM_IRQS=480, so the ARMv8-M vector table is\n"
1632
+ "# (16 + 480) * 4 = 1984 bytes and the hardware VTOR must be 2 KiB (0x800) aligned.\n"
1633
+ "# For an MCUboot chain-loaded image the linker therefore places _vector_table at\n"
1634
+ "# slot_base + 0x800, but Zephyr hard-defaults the MCUboot header (ROM_START_OFFSET,\n"
1635
+ "# which is also imgtool --header-size) to 0x200 under BOOTLOADER_MCUBOOT and makes\n"
1636
+ "# it non-promptable. MCUboot then jumps to slot_base + 0x200 (header end) and reads\n"
1637
+ "# a garbage SP/PC from the 0x600 padding gap -> instant HardFault -> reset/reboot\n"
1638
+ "# loop, never reaching the app. Bump the header reservation to match the vector\n"
1639
+ "# alignment so the jump target == _vector_table. (Affects only chain-loaded images;\n"
1640
+ "# MCUboot itself builds with BOOTLOADER_MCUBOOT=n.)\n"
1641
+ "config ROM_START_OFFSET\n"
1642
+ "\tdefault 0x800 if BOOTLOADER_MCUBOOT\n"
1643
+ "\n"
1644
+ "# alp-sdk's own chips/tmp112/tmp112.c defines tmp112_init(), and upstream\n"
1645
+ "# Zephyr's zephyr/drivers/sensor/ti/tmp112/tmp112.c defines the SAME symbol\n"
1646
+ "# for the SAME part. The two only collide at LINK time, and only when an\n"
1647
+ "# app pulls in both -- which CONFIG_SENSOR=y does for free on this board,\n"
1648
+ "# because the ti,tmp112 devicetree node below auto-selects upstream's\n"
1649
+ "# driver regardless of whether the app uses it (#2043). Board-scope the\n"
1650
+ "# fix instead of leaving every app to rediscover it: default the upstream\n"
1651
+ "# driver OFF here. This is not a permanent no -- Kconfig.zephyr sources a\n"
1652
+ "# board's Kconfig.defconfig ahead of subsys/Kconfig (see the LOG_MODE\n"
1653
+ "# comment below for the same precedence fact), so this default only\n"
1654
+ "# supplies a value when nothing else assigns the symbol. An app that\n"
1655
+ "# genuinely wants the upstream driver is therefore expected to win by\n"
1656
+ "# setting CONFIG_TMP112=y explicitly in its own prj.conf -- ordinary\n"
1657
+ "# Kconfig behaviour for a user assignment versus a `default` -- and that\n"
1658
+ "# is exactly why examples/aen/aen-temp-sensor sets it by name.\n"
1659
+ "#\n"
1660
+ "# CONFIRMED ON THE REAL BOARD TARGET, not just by mechanism. `west build\n"
1661
+ "# -p always -b alp_e1m_aen801_m55_he/ae822fa0e5597ls0/rtss_he\n"
1662
+ "# examples/aen/aen-temp-sensor` links\n"
1663
+ "# zephyr/drivers/sensor/ti/tmp112/libdrivers__sensor__ti__tmp112.a (step\n"
1664
+ "# 139/148), and the resolved .config carries CONFIG_TMP112=y,\n"
1665
+ "# CONFIG_SENSOR=y and CONFIG_DT_HAS_TI_TMP112_ENABLED=y with this board's\n"
1666
+ "# `default n` in force -- the explicit CONFIG_TMP112=y in\n"
1667
+ "# examples/aen/aen-temp-sensor/prj.conf really does win, on silicon-\n"
1668
+ "# targeted Kconfig, not only in theory. If that build ever fails on\n"
1669
+ "# CONFIG_TMP112, this derivation is what needs revisiting, not the\n"
1670
+ "# build.\n"
1671
+ "config TMP112\n"
1672
+ "\tdefault n\n"
1673
+ "\n"
1674
+ + _AEN_LOG_MODE_DEFAULT +
1675
+ f"endif # BOARD_{board_sym}\n"
1676
+ )
1677
+
1678
+
1679
+ #: Per-device state -> the singular ("is ...") / plural ("are ...") verb
1680
+ #: phrase both `_aen_ospi_device_clause()` and `_aen_ospi_population_clause()`
1681
+ #: read, so the two forms can't drift apart (alp-sdk#2062 review round 4).
1682
+ _AEN_OSPI_STATE_PHRASE = {
1683
+ "populated": "populated",
1684
+ "not_populated": "not populated",
1685
+ "optional": "BOM-optional, not assumed populated",
1686
+ }
1687
+
1688
+ #: Why this SDK's AEN board never uses the OSPI0 NOR/HyperRAM for XIP boot,
1689
+ #: EVEN when a preset populates (or might populate) them -- alp-sdk#2062
1690
+ #: review round 3: "OSPI_XIP_SER does not exist on this die" is a true, cited
1691
+ #: fact (Alif `soc_features.h` `SOC_FEAT_OSPI_HAS_XIP_SER 0` for AE822), but it
1692
+ #: is NOT proof XIP is impossible on this silicon -- Alif's own
1693
+ #: `ospi_psram_xip.c` compiles `ospi_control_xip_ss()` out under that same
1694
+ #: guard and STILL calls `aes_enable_xip()`; `flash_ospi_alif.c` documents
1695
+ #: AE822 doing XiP "through a single FIFO location". The real reasons this
1696
+ #: driver stack never attempts it: hal_alif's OWN `alif_hal_ospi_xip_enable()`
1697
+ #: unconditionally targets the absent `XIP_SER` register (a bus fault, not a
1698
+ #: graceful no-op), and `flash_ospi_alif.c`'s `flash_driver_api` -- read/
1699
+ #: read_jedec_id/sfdp_read, bench-verified on E1M-AEN803's fitted NOR -- never
1700
+ #: calls it either (#915) -- so there is no in-tree driver path that would use
1701
+ #: OSPI0 for XIP today regardless of population. Never follow the die-level
1702
+ #: fact alone with a "so" -- always cite this instead.
1703
+ _AEN_OSPI_XIP_GAP = (
1704
+ "hal_alif's alif_hal_ospi_xip_enable() targets the XIP_SER register, "
1705
+ "absent on this die, and flash_ospi_alif.c's flash_driver_api never "
1706
+ "calls it -- #915")
1707
+
1708
+
1709
+ def _aen_ospi_device_state(
1710
+ dev: "dict[str, Any] | None") -> tuple[str, "str | None"]:
1711
+ """(state, chip-or-None) for one `on_module` OSPI-family device dict
1712
+ (`ospi_memories.ospi0` or `hyperram`). *dev* is `None` when the block
1713
+ itself is ABSENT from the preset -- the schema only requires
1714
+ `on_module.silicon`, so omitting `ospi_memories:`/`hyperram:` entirely
1715
+ is valid, a different fact from a declared-but-empty block (alp-sdk#2062
1716
+ review round 4: the old code collapsed both to `{}` via `... or {}`,
1717
+ so an undeclared device hit the `assembled` default same as a
1718
+ declared one with the key merely omitted, and got printed as
1719
+ "populated"). `state` is `"absent"` for that case; callers must leave
1720
+ an absent device out of the clause text entirely.
1721
+
1722
+ For a DECLARED block, `state` is one of `"populated"`, `"not_populated"`,
1723
+ `"optional"` (alp-sdk#2062 review round 2 -- `assembled` is a TRI-STATE
1724
+ field, not a bool: `"optional"` is Python-truthy, so a naive
1725
+ `bool(dev.get("assembled"))` printed "populated" for the AEN301-701
1726
+ SKUs' genuinely-unresolved BOM question; a MISSING `assembled` key on a
1727
+ DECLARED device means populated per the schema default `true` (see the
1728
+ bug that default itself caused, documented at `metadata/e1m_modules/
1729
+ E1M-AEN801.yaml`'s own `hyperram:` comment), not "not populated"). `chip`
1730
+ is `None` when the field is `TBD`/absent so callers never print a
1731
+ placeholder part name -- `_is_tbd()` is this module's own relocated
1732
+ spelling of the helper `alp_orchestrate` uses for the same job."""
1733
+ if dev is None:
1734
+ return "absent", None
1735
+ chip = dev.get("chip")
1736
+ if not chip or _is_tbd(chip):
1737
+ chip = None
1738
+ assembled = dev.get("assembled", True)
1739
+ if assembled == "optional":
1740
+ return "optional", chip
1741
+ return ("populated" if assembled else "not_populated"), chip
1742
+
1743
+
1744
+ def _aen_ospi_device_clause(label: str, state: str, chip: "str | None") -> str:
1745
+ """One device's own clause, e.g. `"OSPI0 NOR (IS25WX256-JHLE) is
1746
+ populated"` / `"HyperRAM is not populated"` / `"OSPI0 NOR is
1747
+ BOM-optional, not assumed populated"`. Never called with `state ==
1748
+ "absent"` -- callers exclude an absent device before reaching here."""
1749
+ named = f"{label} ({chip})" if chip else label
1750
+ return f"{named} is {_AEN_OSPI_STATE_PHRASE[state]}"
1751
+
1752
+
1753
+ def _aen_ospi_population_clause(
1754
+ sku_preset: dict[str, Any]) -> tuple[str, str, int]:
1755
+ """Return `(clause, category, populated_count)` describing the SoM
1756
+ preset's own OSPI0 NOR + HyperRAM population, read from `on_module.
1757
+ ospi_memories.ospi0` / `on_module.hyperram` rather than a hardcoded
1758
+ claim (alp-sdk#2062: this used to hardcode Macronix/Winbond as
1759
+ "BOM-optional and NOT populated on the current batch" for every AEN SKU
1760
+ alike -- both the board-header banner and the MRAM-partition-map comment
1761
+ now call THIS one function, so they cannot disagree with each other or
1762
+ with the real preset the way a board emitted for a from-scratch
1763
+ "populated" preset showed they could: the old banner said populated,
1764
+ the old partition-map comment still said "not populated on this
1765
+ batch" two paragraphs later in the SAME file).
1766
+
1767
+ An ABSENT device block (see `_aen_ospi_device_state()`) is left out of
1768
+ *clause* entirely -- it is never named "not populated", since there is
1769
+ no declared device to describe.
1770
+
1771
+ `category` is `"populated"` if either device is definitely populated,
1772
+ `"optional"` if neither is definite but at least one MIGHT be
1773
+ (`assembled: "optional"`), else `"not_populated"` (every declared
1774
+ device is `assembled: false`, and an absent block contributes
1775
+ nothing). `populated_count` is how many of the (present) devices are
1776
+ definitely `"populated"` -- callers need it for singular/plural
1777
+ agreement when exactly one of two declared devices is populated."""
1778
+ on_module = sku_preset.get("on_module") or {}
1779
+ ospi_memories = on_module.get("ospi_memories")
1780
+ ospi0 = ospi_memories.get("ospi0") if ospi_memories else None
1781
+ hyperram = on_module.get("hyperram")
1782
+ nor_state, nor_chip = _aen_ospi_device_state(ospi0)
1783
+ ram_state, ram_chip = _aen_ospi_device_state(hyperram)
1784
+
1785
+ devices = [(label, state, chip) for label, state, chip in (
1786
+ ("OSPI0 NOR", nor_state, nor_chip), ("HyperRAM", ram_state, ram_chip))
1787
+ if state != "absent"]
1788
+ populated_count = sum(1 for _, state, _ in devices if state == "populated")
1789
+
1790
+ if not devices:
1791
+ return ("no OSPI0 NOR or HyperRAM is declared on this SoM preset",
1792
+ "not_populated", 0)
1793
+
1794
+ states = {state for _, state, _ in devices}
1795
+ if "populated" in states:
1796
+ category = "populated"
1797
+ elif "optional" in states:
1798
+ category = "optional"
1799
+ else:
1800
+ category = "not_populated"
1801
+
1802
+ if len(devices) == 2 and devices[0][1] == devices[1][1]:
1803
+ (nor_label, state, nchip), (ram_label, _, rchip) = devices
1804
+ nor_named = f"{nor_label} ({nchip})" if nchip else nor_label
1805
+ ram_named = f"{ram_label} ({rchip})" if rchip else ram_label
1806
+ clause = f"{nor_named} + {ram_named} are {_AEN_OSPI_STATE_PHRASE[state]}"
1807
+ else:
1808
+ clause = "; ".join(_aen_ospi_device_clause(label, state, chip)
1809
+ for label, state, chip in devices)
1810
+ return clause, category, populated_count
1811
+
1812
+
1813
+ def _aen_ospi_storage_banner_lines(sku_preset: dict[str, Any]) -> list[str]:
1814
+ """Describe the SoM's OSPI0 NOR + HyperRAM for the board-header banner
1815
+ -- see `_aen_ospi_population_clause()`, the shared source this and the
1816
+ MRAM-partition-map comment both read, and `_AEN_OSPI_XIP_GAP` for why
1817
+ a populated (or possibly-populated) device still never sees XIP boot
1818
+ here."""
1819
+ clause, category, populated_count = _aen_ospi_population_clause(sku_preset)
1820
+ if category == "populated":
1821
+ # Singular/plural: "neither" only holds when BOTH declared devices
1822
+ # are actually populated (alp-sdk#2062 review round 4) -- a mixed
1823
+ # preset (one populated, one not/absent) gets the singular form.
1824
+ who = "neither is" if populated_count == 2 else "the populated device is not"
1825
+ tail = f"{clause}; {who} used for XIP boot here ({_AEN_OSPI_XIP_GAP});"
1826
+ elif category == "optional":
1827
+ tail = (f"{clause}, so no external XIP / flash device is assumed -- "
1828
+ f"even if fitted, the same gap applies ({_AEN_OSPI_XIP_GAP});")
1829
+ else:
1830
+ tail = f"{clause}, so there is no external XIP / flash device;"
1831
+ wrapped = textwrap.wrap(tail, width=68, break_on_hyphens=False)
1832
+ return [
1833
+ " * - runs boot + storage from on-die MRAM only. The SoM's",
1834
+ *(f" * {line}" for line in wrapped),
1835
+ ]
1836
+
1837
+
1838
+ def _aen_mram_only_comment_lines(sku_preset: dict[str, Any], tail: str) -> list[str]:
1839
+ """The MRAM partition-map comment's own "why MRAM-only" sentence,
1840
+ from the same `_aen_ospi_population_clause()` the banner reads
1841
+ (alp-sdk#2062 review round 2: this used to hardcode "the SoM OSPI NOR +
1842
+ HyperRAM are not populated on this batch" unconditionally, so a preset
1843
+ that DOES populate them got a board tree contradicting its own banner
1844
+ two paragraphs up). *tail* is the caller's own ending clause (what lives
1845
+ in MRAM as a result); MRAM-only itself holds regardless of population
1846
+ -- see `_AEN_OSPI_XIP_GAP`; the die-level XIP_SER absence alone does
1847
+ NOT imply it (round 3)."""
1848
+ clause, category, _populated_count = _aen_ospi_population_clause(sku_preset)
1849
+ if category == "populated":
1850
+ sentence = (f"MRAM-only regardless: {clause}; {_AEN_OSPI_XIP_GAP} "
1851
+ f"(see the banner above), so {tail}")
1852
+ elif category == "optional":
1853
+ sentence = (f"MRAM-only: {clause}; even if fitted, the same gap "
1854
+ f"applies ({_AEN_OSPI_XIP_GAP}, see the banner above), "
1855
+ f"so {tail}")
1856
+ else:
1857
+ sentence = f"MRAM-only: {clause}, so {tail}"
1858
+ return [f" * {line}" for line in textwrap.wrap(sentence, width=76, break_on_hyphens=False)]
1859
+
1860
+
1861
+ def _aen_dts(
1862
+ sku: str, sku_preset: dict[str, Any], core_id: str, soc_spec: dict[str, Any],
1863
+ variant: dict[str, Any],
1864
+ dir_name: str, basename: str, rx_row: dict[str, Any], tx_row: dict[str, Any],
1865
+ metadata_root: Path, links: dict[str, Any],
1866
+ ethos_u: tuple[str, str] | None = None,
1867
+ memory_map: "list[dict[str, Any]] | None" = None,
1868
+ power_domains: "dict[str, Any] | None" = None,
1869
+ wired_pads: "list[dict[str, Any]] | None" = None,
1870
+ ) -> str:
1871
+ role = core_id.split("_")[-1] # "hp" / "he"
1872
+ role_u = role.upper()
1873
+ other_role = "he" if role == "hp" else "hp"
1874
+ other_role_u = other_role.upper()
1875
+ other_core_id = f"m55_{other_role}"
1876
+
1877
+ core = _find_core(soc_spec, core_id)
1878
+ other_core = _find_core(soc_spec, other_core_id)
1879
+
1880
+ itcm_base, dtcm_base = core["itcm_global_base"], core["dtcm_global_base"]
1881
+ other_itcm, other_dtcm = other_core["itcm_global_base"], other_core["dtcm_global_base"]
1882
+
1883
+ display_variant = variant["order_code"]
1884
+ slug = sku[len("E1M-"):].lower()
1885
+ compatible = f"alp,e1m-{slug}-m55-{role}"
1886
+ model = f"Alp {sku} Cortex-M55 {role_u} carrier"
1887
+ uart_node = _uart_node_label(rx_row)
1888
+
1889
+ part = _aen_part(soc_spec)
1890
+ family_display = _aen_family_display(soc_spec)
1891
+ peripherals_dtsi = _aen_peripherals_dtsi(soc_spec, metadata_root)
1892
+
1893
+ total_kib = round(float(variant["mram_mb"]) * 1024)
1894
+ mram_base, partitions = _aen_flash_partitions(
1895
+ total_kib, metadata_root, role, memory_map)
1896
+ image_kib = dict((label, size) for label, _off, size in partitions)["image-0"]
1897
+ disjoint_slot0 = _aen_role_slot0_map(memory_map, role) is not None
1898
+
1899
+ bench_known = _AEN_BENCH_KNOWN_ITCM[role]
1900
+ itcm_known_suffix = " (also bench-known)" if bench_known else ""
1901
+
1902
+ lines: list[str] = []
1903
+ lines += [
1904
+ "/*",
1905
+ " * Copyright (c) 2026 Alp Lab AB",
1906
+ " * SPDX-License-Identifier: Apache-2.0",
1907
+ " *",
1908
+ f" * {sku} ({family_display}, {display_variant}) Cortex-M55-{role_u} carrier,",
1909
+ " * for the Alp E1M-EVK.",
1910
+ " *",
1911
+ f" * Reuses the upstream Alif {part} SoC + RTSS-{role_u} cluster devicetree and:",
1912
+ " * - retargets the console from the DevKit's UART2 to the E1M carrier console",
1913
+ f" * (Alif {uart_node.upper()}, {rx_row['silicon_pad']}/{tx_row['silicon_pad']} -- the E1M edge \"UART0\");",
1914
+ *_aen_ospi_storage_banner_lines(sku_preset),
1915
+ " * - lays down a production MCUboot partition map in MRAM.",
1916
+ ]
1917
+ if disjoint_slot0:
1918
+ lines += [
1919
+ " *",
1920
+ f" * NOTE (AMP, #1069): {role_u}'s slot0 lives in its OWN disjoint MRAM window,",
1921
+ f" * physically separate from {other_role_u}'s -- both cores' images can be",
1922
+ " * resident in MRAM at once (see the partition map below and",
1923
+ f" * metadata/e1m_modules/{sku}.yaml `memory_map:`). OTA is DEFERRED",
1924
+ " * (CONFIG_SINGLE_APPLICATION_SLOT=y, no secondary/scratch slot): the",
1925
+ f" * per-core code slots ({_aen_slot0_sizes_display(memory_map)}) share the",
1926
+ f" * {total_kib} KiB App MRAM with boot, storage and the NPU MRAM-model",
1927
+ " * budget -- see zephyr/sysbuild/aen/README.md for the sizing rationale.",
1928
+ ]
1929
+ elif role == "hp":
1930
+ lines += [
1931
+ " *",
1932
+ " * NOTE (AMP): this per-core board takes the full MRAM view for single-image",
1933
+ f" * bring-up (one core flashed/run at a time). An {other_role_u}+{role_u} co-resident AMP image",
1934
+ " * partitions MRAM between the two cores at the system-integration layer",
1935
+ " * (sysbuild); see docs/heterogeneous-builds.md.",
1936
+ ]
1937
+ lines += [
1938
+ " *",
1939
+ f" * Inter-core IPC (the APSS<->RTSS-{role_u} MHUv2 doorbell pair) + the remaining",
1940
+ " * on-module peripheral buses (GPIO / SPI / Ethernet) are added alongside the",
1941
+ " * alp-sdk Alif peripheral drivers; this base wires boot, console and the",
1942
+ " * on-module BRD_I2C housekeeping bus (RTC + temperature sensor, below).",
1943
+ " */",
1944
+ "",
1945
+ "/dts-v1/;",
1946
+ "",
1947
+ f"#include <alif/ensemble/{display_variant.lower()}.dtsi>",
1948
+ f"#include <alif/ensemble/common/ensemble_rtss_{role}.dtsi>",
1949
+ f"#include <{peripherals_dtsi}>",
1950
+ "#include <zephyr/dt-bindings/gpio/gpio.h>",
1951
+ "#include <zephyr/dt-bindings/i2c/i2c.h>",
1952
+ f'#include "{dir_name}-pinctrl.dtsi"',
1953
+ "",
1954
+ "/ {",
1955
+ f'\tmodel = "{model}";',
1956
+ f'\tcompatible = "{compatible}";',
1957
+ "",
1958
+ "\tchosen {",
1959
+ "\t\tzephyr,flash = &mram_storage;",
1960
+ "\t\t/*",
1961
+ "\t\t * MCUboot's flash_map_extended.c derives FLASH_DEVICE_BASE /",
1962
+ "\t\t * FLASH_DEVICE_ID from zephyr,flash-controller (the controller",
1963
+ "\t\t * node, NOT the soc-nv-flash child) -- mirrors the fork's",
1964
+ "\t\t * e1.dtsi chosen. Without it MCUboot's flash_map fails to link.",
1965
+ "\t\t */",
1966
+ "\t\tzephyr,flash-controller = &mram;",
1967
+ "\t\tzephyr,sram = &dtcm;",
1968
+ f"\t\tzephyr,console = &{uart_node};",
1969
+ f"\t\tzephyr,shell-uart = &{uart_node};",
1970
+ "\t\tzephyr,code-partition = &slot0_partition;",
1971
+ "\t};",
1972
+ "};",
1973
+ "",
1974
+ "/*",
1975
+ " * ITCM/DTCM global_base -- central CPU-local -> system-bus address translation.",
1976
+ " *",
1977
+ " * hal_alif's soc_memory_map.h local_to_global() translates a CPU-local TCM",
1978
+ " * pointer into the address other bus masters (the Ethos-U NPU AXI master, the",
1979
+ " * SD/DMA controllers) must be programmed with, by reading",
1980
+ " * DT_PROP(DT_NODELABEL(itcm|dtcm), global_base). Upstream Zephyr's",
1981
+ f" * arm,itcm/arm,dtcm bindings (ensemble_rtss_{role}.dtsi, included above) declare no",
1982
+ " * such property, so re-`compatible` the nodes onto the alp-sdk",
1983
+ " * alif,itcm/alif,dtcm bindings (zephyr/dts/bindings/memory-controllers/), which",
1984
+ " * require global_base. Done here in the per-core board (not a per-example",
1985
+ f" * overlay) so every {role_u} app on this board inherits the same translation.",
1986
+ " *",
1987
+ " * Values transcribed from the Apache-2.0 zephyr_alif fork",
1988
+ f" * dts/arm/alif/ensemble/common/ensemble_rtss_{role}.dtsi:",
1989
+ f" * itcm global_base = {hex(itcm_base)}{itcm_known_suffix}",
1990
+ f" * dtcm global_base = {hex(dtcm_base)}",
1991
+ f" * These are the RTSS-{role_u} global TCM aliases; the RTSS-{other_role_u} core uses a different",
1992
+ f" * window ({hex(other_itcm)} / {hex(other_dtcm)}), set in the _{other_role} board dts.",
1993
+ " */",
1994
+ "&itcm {",
1995
+ f'\t/* fork ensemble_rtss_{role}.dtsi: itcm compatible = "alif,itcm" */',
1996
+ '\tcompatible = "alif,itcm";',
1997
+ f"\t/* fork ensemble_rtss_{role}.dtsi: itcm global_base = <{hex(itcm_base)}> */",
1998
+ f"\tglobal_base = <{hex(itcm_base)}>;",
1999
+ "};",
2000
+ "",
2001
+ "&dtcm {",
2002
+ f'\t/* fork ensemble_rtss_{role}.dtsi: dtcm compatible = "alif,dtcm" */',
2003
+ '\tcompatible = "alif,dtcm";',
2004
+ f"\t/* fork ensemble_rtss_{role}.dtsi: dtcm global_base = <{hex(dtcm_base)}> */",
2005
+ f"\tglobal_base = <{hex(dtcm_base)}>;",
2006
+ "};",
2007
+ "",
2008
+ "/*",
2009
+ " * On-die MRAM as a real flash controller (ADR 0017 Tier-2).",
2010
+ " *",
2011
+ f" * Upstream Zephyr's {part} SoC dtsi exposes the MRAM only as a memory region",
2012
+ f' * (`mram: flash@{mram_base:x}`, compatible "zephyr,memory-region","soc-nv-flash"):',
2013
+ " * XIP-readable but with NO flash driver, so FLASH_HAS_DRIVER_ENABLED stays",
2014
+ " * unset and MCUboot's flash_map has nothing to read/write slot0 with.",
2015
+ " *",
2016
+ " * We retarget the upstream `&mram` node into the fork's flash-controller",
2017
+ " * hierarchy WITHOUT editing upstream zephyr/: override `&mram`'s compatible to",
2018
+ ' * the controller compatible "alif,mram-flash-controller" (which the in-tree',
2019
+ " * flash_mram_alif driver binds, setting FLASH_HAS_DRIVER_ENABLED), and add a",
2020
+ " * `mram_storage` soc-nv-flash child carrying the program/erase geometry the",
2021
+ " * driver reads (DT_NODELABEL(mram_storage): erase-block-size / write-block-size",
2022
+ " * / reg). This mirrors the fork e1.dtsi node structure exactly (controller",
2023
+ f" * `mram_flash@{mram_base:x}` + `mram_storage@{mram_base:x}` soc-nv-flash child); the only",
2024
+ " * difference is we reuse the upstream `mram` nodelabel as the controller rather",
2025
+ " * than declaring a fresh `mram_flash` node, since upstream owns the node.",
2026
+ ]
2027
+ partitions_total_kib = sum(size for _label, _off, size in partitions)
2028
+ if disjoint_slot0:
2029
+ lines += [
2030
+ " *",
2031
+ f" * MRAM partition map (this {role_u} core's {partitions_total_kib} KiB view of the",
2032
+ f" * {total_kib} KiB App MRAM) for the sysbuild/aen MCUboot profile (MCUboot + a",
2033
+ " * signed application image, CONFIG_SINGLE_APPLICATION_SLOT=y -- no OTA/swap",
2034
+ " * slot, deferred per #1069). Offsets are relative to the soc-nv-flash child",
2035
+ f" * base (MRAM 0x{mram_base:x}). {other_role_u}'s disjoint slot0 window is NOT a",
2036
+ " * partition in this table (it's outside this core's own view); see",
2037
+ f" * metadata/e1m_modules/{sku}.yaml `memory_map:` for the full physical map:",
2038
+ " *",
2039
+ ]
2040
+ else:
2041
+ if role == "hp":
2042
+ lines += [
2043
+ f" * Kept byte-for-byte identical to the {other_role_u}-sibling board so a future HE+HP AMP",
2044
+ " * sysbuild sees one MRAM partition map.",
2045
+ ]
2046
+ lines += [
2047
+ " *",
2048
+ f" * MRAM partition map ({total_kib} KiB total) for the sysbuild/aen MCUboot profile",
2049
+ " * (MCUboot + a signed application image, swap-using-scratch). Offsets are",
2050
+ f" * relative to the soc-nv-flash child base (MRAM 0x{mram_base:x}):",
2051
+ " *",
2052
+ ]
2053
+ labels_display = {
2054
+ "mcuboot": "mcuboot ",
2055
+ "image-0": "image-0 ",
2056
+ "image-1": "image-1 ",
2057
+ "image-scratch": "scratch ",
2058
+ "reserved": "reserved",
2059
+ "storage": "storage ",
2060
+ "atoc": "atoc ",
2061
+ }
2062
+ trailers = {
2063
+ "image-0": " (primary slot, code-partition)",
2064
+ "image-1": " (secondary slot for OTA)",
2065
+ "reserved": " (ex-scratch; unused, OTA deferred)",
2066
+ "storage": " (settings / NVS)",
2067
+ "atoc": " (SE-owned: SETOOLS top-anchors the ATOC here -- do NOT write, #1289)",
2068
+ }
2069
+ for label, off, size in partitions:
2070
+ lines.append(
2071
+ f" * {labels_display[label]} 0x{off:06x} {size} KiB{trailers.get(label, '')}"
2072
+ )
2073
+ if disjoint_slot0:
2074
+ lines += [
2075
+ f" * = {partitions_total_kib} KiB (of {total_kib} KiB App MRAM total)",
2076
+ " *",
2077
+ *_aen_mram_only_comment_lines(
2078
+ sku_preset,
2079
+ "boot, this core's own slot0, reserved headroom, and "
2080
+ "storage all live in MRAM."),
2081
+ " */",
2082
+ ]
2083
+ else:
2084
+ lines += [
2085
+ f" * = {total_kib} KiB",
2086
+ " *",
2087
+ *_aen_mram_only_comment_lines(
2088
+ sku_preset,
2089
+ "all of boot, both image slots, scratch, and storage live "
2090
+ "in MRAM."),
2091
+ " */",
2092
+ ]
2093
+ lines += [
2094
+ "&mram {",
2095
+ '\tcompatible = "alif,mram-flash-controller";',
2096
+ "\t#address-cells = <1>;",
2097
+ "\t#size-cells = <1>;",
2098
+ "\t/*",
2099
+ "\t * Drop the upstream memory-region properties: this node is no longer a",
2100
+ '\t * "zephyr,memory-region" (it is a flash controller now), and the',
2101
+ "\t * controller addresses its soc-nv-flash child by absolute `reg`, not via",
2102
+ "\t * `ranges`. flash-controller.yaml / base.yaml declare neither property,",
2103
+ "\t * so they must be removed or DT binding-check fails.",
2104
+ "\t */",
2105
+ "\t/delete-property/ zephyr,memory-region;",
2106
+ "\t/delete-property/ ranges;",
2107
+ "",
2108
+ # Node address + `reg` base come from the SAME `mram_base` the
2109
+ # partition offsets above are relative to -- they used to be a
2110
+ # hardcoded 0x80000000 literal while the offsets were computed from
2111
+ # the metadata-declared `mcuboot` base, an invariant documented only
2112
+ # in an error string and never enforced.
2113
+ f"\tmram_storage: mram_storage@{mram_base:x} {{",
2114
+ '\t\tcompatible = "soc-nv-flash";',
2115
+ f"\t\treg = <{hex(mram_base)} DT_SIZE_K({total_kib})>;",
2116
+ "\t\terase-block-size = <1024>;",
2117
+ "\t\twrite-block-size = <16>;",
2118
+ "",
2119
+ "\t\tpartitions {",
2120
+ '\t\t\tcompatible = "fixed-partitions";',
2121
+ "\t\t\t#address-cells = <1>;",
2122
+ "\t\t\t#size-cells = <1>;",
2123
+ "",
2124
+ ]
2125
+
2126
+ partition_node_labels = {
2127
+ "mcuboot": "boot_partition",
2128
+ "image-0": "slot0_partition",
2129
+ "image-1": "slot1_partition",
2130
+ "image-scratch": "scratch_partition",
2131
+ "reserved": "reserved_partition",
2132
+ "storage": "storage_partition",
2133
+ "atoc": "atoc_partition",
2134
+ }
2135
+ partition_dt_labels = {
2136
+ "mcuboot": "mcuboot",
2137
+ "image-0": "image-0",
2138
+ "image-1": "image-1",
2139
+ "image-scratch": "image-scratch",
2140
+ "reserved": "reserved",
2141
+ "storage": "storage",
2142
+ "atoc": "atoc",
2143
+ }
2144
+ for i, (label, off, size) in enumerate(partitions):
2145
+ node = partition_node_labels[label]
2146
+ dt_label = partition_dt_labels[label]
2147
+ lines += [
2148
+ f"\t\t\t{node}: partition@{off:x} {{",
2149
+ f'\t\t\t\tlabel = "{dt_label}";',
2150
+ f"\t\t\t\treg = <{hex(off)} DT_SIZE_K({size})>;",
2151
+ "\t\t\t};",
2152
+ ]
2153
+ if i != len(partitions) - 1:
2154
+ lines.append("")
2155
+
2156
+ lines += [
2157
+ "\t\t};",
2158
+ "\t};",
2159
+ "};",
2160
+ "",
2161
+ f"/* Carrier console: Alif {uart_node.upper()} on {rx_row['silicon_pad']} (RX) / "
2162
+ f"{tx_row['silicon_pad']} (TX), 115200 8N1. */",
2163
+ f"&{uart_node} {{",
2164
+ '\tstatus = "okay";',
2165
+ f"\tpinctrl-0 = <&pinctrl_{uart_node}>;",
2166
+ '\tpinctrl-names = "default";',
2167
+ "\tcurrent-speed = <115200>;",
2168
+ "};",
2169
+ "",
2170
+ ]
2171
+ lines += _aen_brd_i2c_dts(links, part)
2172
+ lines += _aen_e1m_i2c0_dts(links)
2173
+ if power_domains:
2174
+ lines += _aen_som_power_dts(sku_preset, power_domains, links, wired_pads or [])
2175
+
2176
+ if ethos_u is not None:
2177
+ _accel, node = ethos_u
2178
+ # Enable the Arm Ethos-U NPU node this SoC carries. The full node
2179
+ # (reg/interrupts/compatible/secure-enable) is already declared
2180
+ # status="disabled" in this SoC's peripherals dtsi (the
2181
+ # `zephyr_peripherals_dtsi` included above -- e.g.
2182
+ # zephyr/dts/alif/ensemble_e8_peripherals.dtsi for the E8); a silicon
2183
+ # board just flips it on. Harmless -- CONFIG_ETHOS_U is `default n`,
2184
+ # so no driver binds until an inference app pulls it in via
2185
+ # ALP_SDK_INFERENCE_BACKEND_ETHOS_U_AEN (select ETHOS_U if
2186
+ # DT_HAS_ARM_ETHOS_U_ENABLED). A native_sim build uses the native_sim
2187
+ # board (no such node) and so stays on the tflite-micro stub kernel.
2188
+ lines += [
2189
+ f"/* Arm Ethos-U NPU: enable the {node} node declared in the SoC "
2190
+ "peripherals dtsi (see ALP_SDK_INFERENCE_BACKEND_ETHOS_U_AEN). */",
2191
+ f"&{node} {{",
2192
+ '\tstatus = "okay";',
2193
+ "};",
2194
+ "",
2195
+ ]
2196
+ return "\n".join(lines)
2197
+
2198
+
2199
+ def _load_aen_wired_lpgpio_pads(metadata_root: Path) -> list[dict[str, Any]]:
2200
+ """`wired_lpgpio_pads:` of on-module-links.yaml: LPGPIO pads the module wires
2201
+ to an on-module chip without a power-domain control (the OSPI INTn / RESETn
2202
+ nets). `check_som_power_domains.py` pins each to its TSV row."""
2203
+ doc = _load_yaml(metadata_root / "e1m_modules" / "aen" / "on-module-links.yaml")
2204
+ pads = doc.get("wired_lpgpio_pads") or []
2205
+ for p in pads:
2206
+ if not re.fullmatch(r"P15_[0-7]", str(p.get("silicon_pad", ""))):
2207
+ raise ZephyrBoardEmitError(
2208
+ f"wired_lpgpio_pads entry {p.get('signal')!r}: silicon_pad "
2209
+ f"{p.get('silicon_pad')!r} is not an LPGPIO pad P15_0..P15_7")
2210
+ return pads
2211
+
2212
+
2213
+ def _load_aen_power_domains(metadata_root: Path) -> dict[str, Any]:
2214
+ """Load + shape-check `power_domains:` of
2215
+ `metadata/e1m_modules/aen/on-module-links.yaml` (v2, alp-sdk#2784).
2216
+
2217
+ The JSON schema (`on-module-links-v2.schema.json`) pins the shape in
2218
+ `validate_metadata.py`; this refuses only what the emitter itself cannot
2219
+ work without, so a hand-made `--metadata-root` copy fails loudly instead
2220
+ of emitting a board with no SoM power node.
2221
+ """
2222
+ path = metadata_root / "e1m_modules" / "aen" / "on-module-links.yaml"
2223
+ doc = _load_yaml(path)
2224
+ if doc.get("schemaVersion") != "on-module-links-v2":
2225
+ # tan-side (tan-cli#591 pattern): a bound checkout that predates
2226
+ # alp-sdk#2784 is an SDK floor, not this file's authoring gap.
2227
+ require_capability(metadata_root, AEN_SOM_POWER_DOMAINS)
2228
+ raise ZephyrBoardEmitError(
2229
+ f"{path} schemaVersion is {doc.get('schemaVersion')!r}, expected "
2230
+ "'on-module-links-v2' (the `power_domains:` block, alp-sdk#2784)")
2231
+ domains = doc.get("power_domains")
2232
+ if not isinstance(domains, dict) or not domains:
2233
+ raise ZephyrBoardEmitError(f"{path} has no power_domains: block")
2234
+ return domains
2235
+
2236
+
2237
+ def _aen_power_domain_presence(
2238
+ domain: dict[str, Any], on_module: dict[str, Any],
2239
+ ) -> "tuple[str, str | None] | None":
2240
+ """`(state, chip)` for a power domain on THIS SoM preset, or `None` when
2241
+ the domain is absent. `state` is `"populated"` or `"optional"`.
2242
+
2243
+ Presence is never assumed from the family: it is read from the SKU's own
2244
+ `on_module` block (`presence.on_module_key`, a dotted path). A string
2245
+ value (`wifi_ble: cc3501e`) is present unless it is a TBD placeholder; a
2246
+ block with the tri-state `assembled:` follows that field via
2247
+ `_aen_ospi_device_state()` (false -> absent, "optional" -> BOM variant).
2248
+ `presence.family_invariant` is for the one domain no SKU preset describes.
2249
+ """
2250
+ presence = domain["presence"]
2251
+ if presence.get("family_invariant"):
2252
+ return "populated", domain.get("chip")
2253
+ node: Any = on_module
2254
+ for part in presence["on_module_key"].split("."):
2255
+ node = node.get(part) if isinstance(node, dict) else None
2256
+ if node is None:
2257
+ return None
2258
+ if isinstance(node, dict):
2259
+ state, chip = _aen_ospi_device_state(node)
2260
+ if state == "not_populated":
2261
+ return None
2262
+ return state, chip or domain.get("chip")
2263
+ if not node or _is_tbd(node):
2264
+ return None
2265
+ return "populated", str(node)
2266
+
2267
+
2268
+ #: `alp,stop-hold` strength order: the weakest control decides.
2269
+ _AEN_STOP_HOLD_RANK = {"no": 0, "unproven": 1, "yes": 2}
2270
+
2271
+ #: Control role -> the DT gpio property name `alp,som-power-domain` uses.
2272
+ _AEN_POWER_ROLE_PROP = {
2273
+ "reset": "reset-gpios",
2274
+ "rail_enable": "enable-gpios",
2275
+ "powerdown": "powerdown-gpios",
2276
+ "enable": "enable-gpios",
2277
+ "wake_in": "wake-gpios",
2278
+ }
2279
+
2280
+
2281
+ def _aen_som_power_dts(
2282
+ sku_preset: dict[str, Any], domains: dict[str, Any],
2283
+ links: dict[str, Any], wired_pads: list[dict[str, Any]],
2284
+ ) -> list[str]:
2285
+ """The `alp,som-power` node: one `alp,som-power-domain` child per domain
2286
+ this SKU actually carries (alp-sdk#2784, metadata-only -- nothing in the
2287
+ SDK drives these pads yet).
2288
+
2289
+ Pad, polarity and action facts come from `power_domains:` in
2290
+ on-module-links.yaml; whether the domain exists on this SKU comes from the
2291
+ SKU's own `on_module:` block (`_aen_power_domain_presence()`).
2292
+ """
2293
+ on_module = sku_preset.get("on_module") or {}
2294
+ device_labels = {
2295
+ d["node_label"]
2296
+ for d in (links["brd_i2c"].get("devices") or []) if d.get("node_label")}
2297
+ children: list[str] = []
2298
+ for name, dom in domains.items():
2299
+ found = _aen_power_domain_presence(dom, on_module)
2300
+ if found is None:
2301
+ continue
2302
+ state, chip = found
2303
+ label = dom.get("device_label")
2304
+ if label and label not in device_labels:
2305
+ raise ZephyrBoardEmitError(
2306
+ f"power_domains.{name}.device_label {label!r} is not a "
2307
+ "node_label under on_module_links.brd_i2c.devices")
2308
+ action = dom["default_action"]
2309
+ actions = dom["actions"]
2310
+ if action not in actions:
2311
+ raise ZephyrBoardEmitError(
2312
+ f"power_domains.{name}.default_action {action!r} is not in "
2313
+ "its actions:")
2314
+ by_signal = {c["signal"]: c for c in dom["controls"]}
2315
+ # The hold level that matters is the default action's own pad; a
2316
+ # command-only action falls back to the domain's pads (weakest wins).
2317
+ ctl = by_signal.get(actions[action].get("control", ""))
2318
+ held = ([ctl] if ctl else
2319
+ [c for c in dom["controls"] if c["direction"] == "out"])
2320
+ stop_hold = (min((c["holds_through_stop"] for c in held),
2321
+ key=_AEN_STOP_HOLD_RANK.__getitem__)
2322
+ if held else "none")
2323
+ node = name.replace("_", "-")
2324
+ child = [""]
2325
+ child += _c_comment(dom["evidence"], "\t\t")
2326
+ child += [f"\t\tsom_pd_{name}: {node} {{",
2327
+ '\t\t\tcompatible = "alp,som-power-domain";',
2328
+ f'\t\t\talp,role = "{name}";']
2329
+ seen_props: set[str] = set()
2330
+ for c in dom["controls"]:
2331
+ prop = _AEN_POWER_ROLE_PROP[c["role"]]
2332
+ if prop in seen_props:
2333
+ raise ZephyrBoardEmitError(
2334
+ f"power_domains.{name}: two controls map to {prop}")
2335
+ seen_props.add(prop)
2336
+ flag = ("GPIO_ACTIVE_LOW" if c["polarity"] == "active_low"
2337
+ else "GPIO_ACTIVE_HIGH")
2338
+ child.append(
2339
+ f"\t\t\t{prop} = <&{c['gpio_node']} {c['gpio_pin']} {flag}>;"
2340
+ f" /* {c['signal']} {c['silicon_pad']} */")
2341
+ if dom.get("secondary_rtc"):
2342
+ child.append("\t\t\talp,primary-rtc;")
2343
+ if label:
2344
+ child.append(f"\t\t\talp,device = <&{label}>;")
2345
+ if chip:
2346
+ child.append(f'\t\t\talp,chip = "{chip}";')
2347
+ child += [
2348
+ f'\t\t\talp,default-action = "{action}";',
2349
+ "\t\t\talp,default-modes = "
2350
+ + ", ".join(f'"{m}"' for m in dom["default_modes"]) + ";",
2351
+ f'\t\t\talp,stop-hold = "{stop_hold}";',
2352
+ f'\t\t\talp,restore = "{dom["restore"]["method"]}";']
2353
+ if actions.get("rail_off", {}).get("opt_in"):
2354
+ child.append("\t\t\talp,rail-off-opt-in;")
2355
+ if state == "optional":
2356
+ child.append("\t\t\talp,bom-optional;")
2357
+ deps = sorted({d["kind"] for d in dom.get("dependents") or []})
2358
+ if deps:
2359
+ child.append("\t\t\talp,dependents = "
2360
+ + ", ".join(f'"{d}"' for d in deps) + ";")
2361
+ child.append("\t\t};")
2362
+ children += child
2363
+
2364
+ # Every output control pad the layer drives, muxed and pad-configured by the
2365
+ # `alp,som-power` node's own pinctrl-0 state. Pad config is input-enable +
2366
+ # schmitt (0x23 with the default 4 mA drive on the LP pads: the value the
2367
+ # bench-proven CC3501E bring-up writes by hand) so the output driver is on
2368
+ # and the pad can be read back. Port 15 is the LPGPIO island. The group sets
2369
+ # mux / pad configuration only, never direction or level (those are GPIO-port
2370
+ # state), so applying it to a pad whose domain is KEEP_ALIVE changes nothing.
2371
+ pads: list[tuple[int, int]] = []
2372
+ for name, dom in domains.items():
2373
+ if _aen_power_domain_presence(dom, on_module) is None:
2374
+ continue
2375
+ for c in dom["controls"]:
2376
+ m = re.fullmatch(r"P(\d+)_(\d)", c["silicon_pad"])
2377
+ if c["direction"] != "out" or m is None:
2378
+ continue
2379
+ pad = (int(m.group(1)), int(m.group(2)))
2380
+ if pad not in pads:
2381
+ pads.append(pad)
2382
+ pads.sort()
2383
+ pinmux = ", ".join(
2384
+ f"<PIN_P{port}_{pin}__{'LPGPIO' if port == 15 else 'GPIO'}>"
2385
+ for port, pin in pads)
2386
+
2387
+ # LPGPIO pads the module wires without a domain control, so a wake-pad
2388
+ # claim is per board (src/backends/power/som_power.c reads this property).
2389
+ wired_line = []
2390
+ if wired_pads:
2391
+ cells = " ".join(str(int(p["silicon_pad"][4:])) for p in wired_pads)
2392
+ wired_line = [
2393
+ "\t\t/* SoM-wired LPGPIO pads without a domain node (alif-ospi.tsv):",
2394
+ "\t\t * " + ", ".join(f"{p['signal']} {p['silicon_pad']}" for p in wired_pads) + ".",
2395
+ "\t\t * Never an application wake pad. */",
2396
+ f"\t\talp,wired-lpgpio-pads = <{cells}>;"]
2397
+
2398
+ lines = [
2399
+ "/*",
2400
+ " * SoM power domains (alp-sdk#2784): which on-module chips this SKU carries,",
2401
+ " * the SoC pads that gate them, and the default quiesce action per domain.",
2402
+ " * Presence follows this SKU's `on_module` block; pads, polarity and actions",
2403
+ " * come from `power_domains:` in metadata/e1m_modules/aen/on-module-links.yaml.",
2404
+ " * The SoM power layer (src/backends/power/som_power.c) reads this node and",
2405
+ " * applies its pinctrl-0 state before it first drives a pad. `alp,stop-hold` is",
2406
+ " * \"unproven\" for every LPGPIO (P15_n) pad until the STOP bench shows the output",
2407
+ " * holds.",
2408
+ " */",
2409
+ "&pinctrl {",
2410
+ "\tpinctrl_som_power: pinctrl_som_power {",
2411
+ "\t\tgroup0 {",
2412
+ f"\t\t\tpinmux = {pinmux};",
2413
+ "\t\t\tinput-enable;",
2414
+ "\t\t\tinput-schmitt-enable;",
2415
+ "\t\t};",
2416
+ "\t};",
2417
+ "};",
2418
+ "",
2419
+ "/ {",
2420
+ "\tsom_power: som-power {",
2421
+ '\t\tcompatible = "alp,som-power";',
2422
+ "\t\tpinctrl-0 = <&pinctrl_som_power>;",
2423
+ '\t\tpinctrl-names = "default";',
2424
+ *wired_line,
2425
+ *children,
2426
+ "\t};",
2427
+ "};",
2428
+ "",
2429
+ ]
2430
+ return lines
2431
+
2432
+
2433
+ # ---------------------------------------------------------------------
2434
+ # V2N-family files: pinctrl.dtsi, _defconfig (#655)
2435
+ # ---------------------------------------------------------------------
2436
+
2437
+
2438
+ def _load_supervisor_links_doc(metadata_root: Path) -> dict[str, Any]:
2439
+ """Load `metadata/e1m_modules/v2n/supervisor-links.yaml` as a whole doc.
2440
+
2441
+ Family-scoped (see the module docstring): this one file backs both the
2442
+ `v2n` and `v2n-m1` families' pinctrl.dtsi / _defconfig, so it is not
2443
+ looked up per-SoM the way `_aen_console_pinmux_rows()` reads
2444
+ `pinmux/aen.yaml`.
2445
+ """
2446
+ path = metadata_root / "e1m_modules" / "v2n" / "supervisor-links.yaml"
2447
+ if not path.is_file():
2448
+ raise ZephyrBoardEmitError(
2449
+ f"no {path} -- the V2N/V2M pinctrl.dtsi/_defconfig emitters need "
2450
+ "the on-module GD32G553 supervisor pin-wiring source (#655)")
2451
+ return _load_yaml(path)
2452
+
2453
+
2454
+ def _v2n_declared_families(metadata_root: Path) -> list[str]:
2455
+ """The `families:` list `supervisor-links.yaml` declares it backs.
2456
+
2457
+ Load-bearing: `emit_zephyr_board()`'s V2N/V2M branch gates on this list
2458
+ instead of a hardcoded `("v2n", "v2n-m1")` tuple, so dropping a family
2459
+ here (e.g. removing `v2n-m1`) actually stops that family's
2460
+ pinctrl.dtsi/_defconfig from being generated rather than silently
2461
+ continuing on a stale local constant nothing else reads.
2462
+ """
2463
+ doc = _load_supervisor_links_doc(metadata_root)
2464
+ families = doc.get("families")
2465
+ if not isinstance(families, list) or not families:
2466
+ raise ZephyrBoardEmitError(
2467
+ "metadata/e1m_modules/v2n/supervisor-links.yaml has no "
2468
+ "non-empty families: list")
2469
+ return families
2470
+
2471
+
2472
+ def _load_supervisor_links(metadata_root: Path) -> dict[str, Any]:
2473
+ """Load + shape-check the `supervisor_links:` block of
2474
+ `metadata/e1m_modules/v2n/supervisor-links.yaml`."""
2475
+ doc = _load_supervisor_links_doc(metadata_root)
2476
+ links = doc.get("supervisor_links")
2477
+ if not isinstance(links, dict):
2478
+ raise ZephyrBoardEmitError(
2479
+ "metadata/e1m_modules/v2n/supervisor-links.yaml has no "
2480
+ "supervisor_links: block")
2481
+ for key in ("console", "gd32_spi", "brd_i2c"):
2482
+ if key not in links:
2483
+ raise ZephyrBoardEmitError(
2484
+ "metadata/e1m_modules/v2n/supervisor-links.yaml "
2485
+ f"supervisor_links: is missing {key!r}")
2486
+ return links
2487
+
2488
+
2489
+ def _pin_by_peripheral(pins: list[dict[str, Any]], peripheral: str) -> dict[str, Any]:
2490
+ for p in pins:
2491
+ if p.get("silicon_peripheral") == peripheral:
2492
+ return p
2493
+ raise ZephyrBoardEmitError(
2494
+ f"no {peripheral!r} row in this link's pins[] "
2495
+ "(supervisor-links.yaml / on-module-links.yaml)")
2496
+
2497
+
2498
+ def _rzv_pinmux(row: dict[str, Any]) -> str:
2499
+ return f"RZV_PINMUX({row['pfc_port']}, {row['pfc_pin']}, {row['pfc_func']})"
2500
+
2501
+
2502
+ #: RZ/V2N PFC alternate-function role -> mnemonic suffix for the gd32_spi
2503
+ #: link's `silicon_peripheral` roles ("GD32_SPI.MOSI" -> "MOSI", etc.).
2504
+ #: SCLK is the one exception to a literal pass-through: the RZ/V2N PFC
2505
+ #: table names that alternate function "SCK", not "SCLK".
2506
+ _V2N_SPI_ROLE_MNEMONIC = {"MOSI": "MOSI", "MISO": "MISO", "SCLK": "SCK"}
2507
+
2508
+
2509
+ def _v2n_sci_channel(dt_label: str) -> str:
2510
+ """`"sci7"` -> `"7"` -- the SCI channel digit `gd32_spi.dt_label`
2511
+ encodes, used to build the PFC alternate-function mnemonics
2512
+ (`TXD7_MOSI7`, `RXD7_MISO7`, `SCK7`) instead of hardcoding them."""
2513
+ m = re.match(r"^sci([0-9]+)$", dt_label)
2514
+ if not m:
2515
+ raise ZephyrBoardEmitError(
2516
+ f"gd32_spi.dt_label {dt_label!r} is not sci<N> -- can't derive "
2517
+ "the RZ/V2N PFC alternate-function mnemonics from it")
2518
+ return m.group(1)
2519
+
2520
+
2521
+ def _v2n_spi_pfc_mnemonic(row: dict[str, Any], channel: str) -> str:
2522
+ """RZ/V2N PFC alternate-function mnemonic for a `gd32_spi` pin row,
2523
+ e.g. (`silicon_peripheral="GD32_SPI.MOSI"`, channel `"7"`) -> `"MOSI7"`.
2524
+ """
2525
+ role = row["silicon_peripheral"].rsplit(".", 1)[-1]
2526
+ return f"{_V2N_SPI_ROLE_MNEMONIC[role]}{channel}"
2527
+
2528
+
2529
+ def _v2n_assignable_m33(
2530
+ metadata_root: Path, soc_spec: dict[str, Any],
2531
+ ) -> list[tuple[str, dict[str, Any], list[tuple[dict[str, Any], tuple[str, int, int]]]]]:
2532
+ """(instance, entry, [(row, (port, pin, func))]) for every `assignable:`
2533
+ instance of core-ownership.yaml that carries an `m33:` devicetree block --
2534
+ the peripherals the CM33 board tree declares `disabled`, left for a
2535
+ project's `--emit dts-overlay` / `zephyr-conf` to enable (board.yaml
2536
+ `ownership:`). The PFC function codes are the SoC JSON's
2537
+ `linux_dt[soc_instance].pinmux` -- one silicon-fact source for both cores."""
2538
+ from .ownership import instance_pfc
2539
+ path = metadata_root / "e1m_modules" / "v2n" / "core-ownership.yaml"
2540
+ if not path.is_file():
2541
+ return []
2542
+ out = []
2543
+ for inst, e in sorted((_load_yaml(path).get("assignable") or {}).items()):
2544
+ if "m33" not in e:
2545
+ continue
2546
+ rows = instance_pfc(soc_spec, e)
2547
+ for r, pfc in rows:
2548
+ if pfc is None:
2549
+ raise ZephyrBoardEmitError(
2550
+ f"core-ownership.yaml assignable.{inst} has an m33: block but the SoC "
2551
+ f"linux_dt.{e.get('soc_instance')}.pinmux has no function code for "
2552
+ f"{r['peripheral']}")
2553
+ out.append((inst, e, rows))
2554
+ return out
2555
+
2556
+
2557
+ def _v2n_assignable_pinctrl(assignable: list) -> str:
2558
+ """`&pinctrl { ... };` groups for the assignable instances (empty when
2559
+ none). Pins are the rows' own pfc_* triples -- no literals here."""
2560
+ out = ""
2561
+ for inst, e, rows in assignable:
2562
+ pc = e["m33"]["pinctrl"]
2563
+ pins = "".join(
2564
+ ("\t\t\tpinmux = " if i == 0 else "\t\t\t\t ")
2565
+ + f"<RZV_PINMUX({port}, {pin}, {func})>{';' if i == len(rows) - 1 else ','} "
2566
+ f"/* {r['peripheral']} {r['pad']} */\n"
2567
+ for i, (r, (port, pin, func)) in enumerate(rows))
2568
+ out += (
2569
+ "\n&pinctrl {\n"
2570
+ f"\t/* {inst}: pad group for the CM33 when a project assigns it (board.yaml\n"
2571
+ "\t * `ownership:`); unreferenced while the node stays disabled. */\n"
2572
+ f"\t{pc['group_label']}: {pc['node']} {{\n"
2573
+ f"\t\t{pc['child_node']} {{\n"
2574
+ f"{pins}"
2575
+ "\t\t};\n\t};\n};\n")
2576
+ return out
2577
+
2578
+
2579
+ def _v2n_assignable_dts(assignable: list) -> list[str]:
2580
+ """Board-dts lines declaring each assignable node `disabled` with its
2581
+ pinctrl group; the owning project's overlay flips it to `okay`."""
2582
+ lines: list[str] = []
2583
+ for inst, e, _rows in assignable:
2584
+ m = e["m33"]
2585
+ lines += [
2586
+ "/*",
2587
+ f" * {inst} (assignable, metadata/e1m_modules/v2n/core-ownership.yaml): disabled on",
2588
+ " * the board; a project that assigns it to m33 enables it in its own overlay.",
2589
+ " */",
2590
+ f"&{m['dt_label']} {{",
2591
+ f"\tpinctrl-0 = <&{m['pinctrl']['group_label']}>;",
2592
+ '\tpinctrl-names = "default";',
2593
+ '\tstatus = "disabled";',
2594
+ "};",
2595
+ "",
2596
+ ]
2597
+ return lines
2598
+
2599
+
2600
+ def _v2n_pinctrl_dtsi(links: dict[str, Any], assignable: list = ()) -> str:
2601
+ """`<board>-pinctrl.dtsi` for a V2N/V2M `m33_sm` board.
2602
+
2603
+ Prose (the GD32 SPI block comment, the console/BRD_I2C one-liners)
2604
+ lives here as template text, matching `_aen_pinctrl_dtsi()`'s pattern;
2605
+ the pad numbers, PFC (port, pin, func) triples, DT node/group/child
2606
+ names, and the GD32 I2C address all come from
2607
+ `metadata/e1m_modules/v2n/supervisor-links.yaml`.
2608
+ """
2609
+ console = links["console"]
2610
+ gd32_spi = links["gd32_spi"]
2611
+ brd_i2c = links["brd_i2c"]
2612
+
2613
+ txd0 = _pin_by_peripheral(console["pins"], "UART0_TXD0")
2614
+ rxd0 = _pin_by_peripheral(console["pins"], "UART0_RXD0")
2615
+ mosi = _pin_by_peripheral(gd32_spi["pins"], "GD32_SPI.MOSI")
2616
+ miso = _pin_by_peripheral(gd32_spi["pins"], "GD32_SPI.MISO")
2617
+ sclk = _pin_by_peripheral(gd32_spi["pins"], "GD32_SPI.SCLK")
2618
+ sda = _pin_by_peripheral(brd_i2c["pins"], "RIIC8_SDA8")
2619
+ scl = _pin_by_peripheral(brd_i2c["pins"], "RIIC8_SCL8")
2620
+ cs0 = gd32_spi["gpio_chip_select"]
2621
+
2622
+ # PFC alternate-function mnemonics + the console's own TXD0 reference,
2623
+ # derived from metadata (gd32_spi.dt_label's channel digit +
2624
+ # silicon_peripheral) rather than hardcoded, so a pfc_func or channel
2625
+ # change can't silently desync this prose from the RZV_PINMUX(...)
2626
+ # triples it documents.
2627
+ spi_channel = _v2n_sci_channel(gd32_spi["dt_label"])
2628
+ mosi_mnem = f"TXD{spi_channel}_{_v2n_spi_pfc_mnemonic(mosi, spi_channel)}"
2629
+ miso_mnem = f"RXD{spi_channel}_{_v2n_spi_pfc_mnemonic(miso, spi_channel)}"
2630
+ sclk_mnem = _v2n_spi_pfc_mnemonic(sclk, spi_channel)
2631
+ console_mnem = txd0["silicon_peripheral"].split("_", 1)[1]
2632
+
2633
+ return (
2634
+ _COPYRIGHT_C +
2635
+ " */\n"
2636
+ "\n"
2637
+ "#include <zephyr/dt-bindings/gpio/gpio.h>\n"
2638
+ "#include <zephyr/dt-bindings/pinctrl/renesas/pinctrl-rzv2n.h>\n"
2639
+ "\n"
2640
+ "&pinctrl {\n"
2641
+ f"\t/* {console['peripheral']} console: {txd0['silicon_pad']} TXD / "
2642
+ f"{rxd0['silicon_pad']} RXD */\n"
2643
+ f"\t{console['pinctrl_group_label']}: {console['pinctrl_node']} {{\n"
2644
+ f"\t\t{console['pinctrl_child_node']} {{\n"
2645
+ f"\t\t\tpinmux = <{_rzv_pinmux(txd0)}>, /* TXD */\n"
2646
+ f"\t\t\t\t <{_rzv_pinmux(rxd0)}>; /* RXD */\n"
2647
+ "\t\t\t/* RXD resets Hi-Z with no pull: a floating RXD0 raises sci0 eri.\n"
2648
+ "\t\t\t * Renesas' own RSCI UART pin groups always carry bias-pull-up\n"
2649
+ "\t\t\t * (audit CM33-01/IO-01/CM33-M3). Whole group; a pull-up on TXD is\n"
2650
+ "\t\t\t * harmless. */\n"
2651
+ "\t\t\tbias-pull-up;\n"
2652
+ "\t\t};\n"
2653
+ "\t};\n"
2654
+ "\n"
2655
+ "\t/*\n"
2656
+ f"\t * GD32G553 SPI fast path on SCI channel {spi_channel} (clock-synchronous Simple-SPI).\n"
2657
+ f"\t * Per the RZ/V2N PFC (Table 1.2-3): {mosi['silicon_pad']} = {mosi_mnem} (func{mosi['pfc_func']}), "
2658
+ f"{miso['silicon_pad']} =\n"
2659
+ f"\t * {miso_mnem} (func{miso['pfc_func']}), {sclk['silicon_pad']} = {sclk_mnem} (func{sclk['pfc_func']}). Calibrated against the known-good\n"
2660
+ f"\t * console pin {txd0['silicon_pad']} = {console_mnem} (func{txd0['pfc_func']}) and the P52 TXD1/SCK0/DE0/CTS0N column run.\n"
2661
+ "\t *\n"
2662
+ f"\t * {cs0['silicon_pad']} (SS{spi_channel}) is deliberately NOT muxed here: master Simple-SPI has no\n"
2663
+ f"\t * hardware slave-select, so {cs0['silicon_pad']} is driven as a GPIO chip-select "
2664
+ f"({cs0['gpio_node']} pin {cs0['gpio_pin']};\n"
2665
+ "\t * see cs-gpios in the board dts). Leaving it out keeps it in GPIO mode.\n"
2666
+ "\t */\n"
2667
+ f"\t{gd32_spi['pinctrl_group_label']}: {gd32_spi['pinctrl_node']} {{\n"
2668
+ f"\t\t{gd32_spi['pinctrl_child_node']} {{\n"
2669
+ f"\t\t\tpinmux = <{_rzv_pinmux(mosi)}>, /* {mosi_mnem.split('_', 1)[1]} {mosi['silicon_pad']} */\n"
2670
+ f"\t\t\t\t <{_rzv_pinmux(miso)}>, /* {miso_mnem.split('_', 1)[1]} {miso['silicon_pad']} */\n"
2671
+ f"\t\t\t\t <{_rzv_pinmux(sclk)}>; /* {sclk_mnem} {sclk['silicon_pad']} */\n"
2672
+ "\t\t};\n"
2673
+ "\t};\n"
2674
+ "\n"
2675
+ f"\t/* BRD_I2C ({brd_i2c['peripheral']}): on-module GD32G553 I2C slave @ "
2676
+ f"0x{brd_i2c['peer_address_7bit']:02x} */\n"
2677
+ f"\t{brd_i2c['pinctrl_group_label']}: {brd_i2c['pinctrl_node']} {{\n"
2678
+ f"\t\t{brd_i2c['pinctrl_child_node']} {{\n"
2679
+ f"\t\t\tpinmux = <{_rzv_pinmux(sda)}>, /* SDA {sda['silicon_pad']} */\n"
2680
+ f"\t\t\t\t <{_rzv_pinmux(scl)}>; /* SCL {scl['silicon_pad']} */\n"
2681
+ "\t\t};\n"
2682
+ "\t};\n"
2683
+ "};\n"
2684
+ ) + _v2n_assignable_pinctrl(assignable)
2685
+
2686
+
2687
+ def _v2n_defconfig(links: dict[str, Any], soc_spec: dict[str, Any]) -> str:
2688
+ """`<board>_defconfig` for a V2N/V2M `m33_sm` board.
2689
+
2690
+ `CONFIG_XIP=n` has no supervisor-link source -- it's a documented V2N/
2691
+ V2M family constant (the CM33 image always runs from RAM on this
2692
+ family), kept as a literal here rather than invented metadata. The
2693
+ console-disable prose and the CONFIG_* symbol names are template text
2694
+ (matching `_aen_defconfig()`'s pattern); which block gets emitted is
2695
+ still gated on the metadata's own `status:` per link, so a
2696
+ supervisor-links.yaml edit that (incorrectly) re-enables the console or
2697
+ disables the GD32 bridge fails loudly here instead of silently drifting
2698
+ the committed `_defconfig`.
2699
+ """
2700
+ rc_a55, _, rc_size = _ram_console(soc_spec)
2701
+ win_base = _openamp_carveout(soc_spec)["cm33_ns_base"]
2702
+ console = links["console"]
2703
+ gd32_spi = links["gd32_spi"]
2704
+ brd_i2c = links["brd_i2c"]
2705
+ if console.get("status") != "disabled":
2706
+ raise ZephyrBoardEmitError(
2707
+ "supervisor-links.yaml console link must stay status: disabled "
2708
+ "for this _defconfig template -- its floating-RX fault (see "
2709
+ "the console pins' evidence: strings) is why the CM33 serial "
2710
+ "console is off; re-enabling it needs a matching _defconfig "
2711
+ "rewrite, not just a metadata flip")
2712
+ if gd32_spi.get("status") != "enabled":
2713
+ raise ZephyrBoardEmitError(
2714
+ "supervisor-links.yaml gd32_spi link must stay status: enabled "
2715
+ "for this _defconfig template to turn on CONFIG_SPI")
2716
+ if brd_i2c.get("status") != "disabled":
2717
+ raise ZephyrBoardEmitError(
2718
+ "supervisor-links.yaml brd_i2c link must stay status: disabled "
2719
+ "-- RIIC8/BRD_I2C is Cortex-A55/Linux-exclusive, so this "
2720
+ "_defconfig template does NOT turn on CONFIG_I2C for it; "
2721
+ "re-enabling it needs a matching _defconfig rewrite, not just "
2722
+ "a metadata flip")
2723
+ return (
2724
+ _COPYRIGHT_HASH +
2725
+ "\n"
2726
+ "CONFIG_XIP=n\n"
2727
+ "\n"
2728
+ "# CM33 serial console (sci0 = EVK \"Pmod USB-UART\") DISABLED on this board:\n"
2729
+ "# the only console is the A55's; sci0 is not wired as a CM33 console here, and\n"
2730
+ "# opening it faults on the floating RX (sci0 eri -> Zephyr fatal -> hang before\n"
2731
+ "# main()). Re-enable UART_CONSOLE (and sci0 in the dts) only with a Pmod\n"
2732
+ "# attached AND once Linux cannot gate the rsci_0_* clocks (audit CM33-05).\n"
2733
+ "CONFIG_SERIAL=y\n"
2734
+ "CONFIG_UART_CONSOLE=n\n"
2735
+ "\n"
2736
+ "# Enabling sci0 also needs CONFIG_UART_INTERRUPT_DRIVEN=y: the FSP always\n"
2737
+ "# enables the rxi/eri IRQs, and the Zephyr driver only IRQ_CONNECTs them\n"
2738
+ "# under it -- otherwise an unhandled NVIC line goes fatal. Not set here:\n"
2739
+ "# with sci0 disabled it resolves to n and Kconfig warns.\n"
2740
+ "\n"
2741
+ "# RAM console: the CM33 has no UART and no SWD yet, so printk lands in a\n"
2742
+ f"# fixed buffer (zephyr,ram-console node in the dts, inside the {win_base:#x}\n"
2743
+ "# window Linux reserves) that the A55 reads post-mortem via /dev/mem at\n"
2744
+ f"# {rc_a55:#x}. CONFIG_CONSOLE=y only pulls the RAM console in; with\n"
2745
+ "# UART_CONSOLE=n nothing touches sci0. Size must equal the dts node's reg.\n"
2746
+ "CONFIG_CONSOLE=y\n"
2747
+ "CONFIG_RAM_CONSOLE=y\n"
2748
+ f"CONFIG_RAM_CONSOLE_BUFFER_SIZE={rc_size}\n"
2749
+ "# With CONFIG_LOG=y, printk is routed into the logging core by default\n"
2750
+ "# (LOG_PRINTK), and no CM33 log backend reaches this buffer, so it would\n"
2751
+ "# stay empty. Keep printk direct to the RAM console.\n"
2752
+ "CONFIG_LOG_PRINTK=n\n"
2753
+ "\n"
2754
+ "# On-module GD32G553 supervisor bridge transport. SPI only: RIIC8/\n"
2755
+ "# BRD_I2C is Cortex-A55/Linux-exclusive, so no CONFIG_I2C here.\n"
2756
+ "CONFIG_SPI=y\n"
2757
+ )
2758
+
2759
+
2760
+ def _v2n_part_display(order_code: str) -> tuple[str, str]:
2761
+ """Split an RZ/V2N `silicon_variant` order code into the board `.dts`
2762
+ header's part display (`"R9A09G056N44GBG"` -> `"R9A09G056-N44"`, the
2763
+ package suffix dropped and a hyphen inserted before the variant code)
2764
+ and the upstream Zephyr SoC dtsi include path both n44 and n48gbg
2765
+ variants share (`"arm/renesas/rz/rzv/r9a09g056.dtsi"`). Raises rather
2766
+ than guessing if a future RZ/V2N part doesn't fit this
2767
+ base+variant+package shape -- see the no-inventing-values rule.
2768
+ """
2769
+ m = re.match(r"^(R9A09G056)(N[0-9]+)([A-Z]+)$", order_code)
2770
+ if not m:
2771
+ raise ZephyrBoardEmitError(
2772
+ f"silicon_variant order_code {order_code!r} doesn't match the "
2773
+ "RZ/V2N R9A09G056<variant><pkg> shape the board .dts header "
2774
+ "and #include path expect")
2775
+ base, variant_code, _pkg = m.groups()
2776
+ return f"{base}-{variant_code}", f"arm/renesas/rz/rzv/{base.lower()}.dtsi"
2777
+
2778
+
2779
+ _V2N_OPENAMP_TAIL: tuple[str, ...] = (
2780
+ '',
2781
+ '/*',
2782
+ ' * No &canfd node here (alp-sdk#1146): CAN-FD cannot be enabled on this',
2783
+ ' * core at the pinned Zephyr v4.4.1 / hal_renesas 06282060fa, and what is',
2784
+ ' * missing is vendor data this tree does not carry -- not lines nobody has',
2785
+ ' * got round to writing. Upstream DOES ship the driver',
2786
+ ' * (drivers/can/can_renesas_rz_canfd.c) and both bindings',
2787
+ ' * (dts/bindings/can/renesas,rz-canfd.yaml + renesas,rz-canfd-global.yaml),',
2788
+ ' * and src/backends/can/zephyr_drv.c already binds any DT_ALIAS(alp_canN),',
2789
+ ' * so this is an enablement gap and not a missing backend. Four things',
2790
+ ' * block the enablement:',
2791
+ ' *',
2792
+ ' * 1. No FSP CAN-FD module for RZ/V. CONFIG_CAN_RENESAS_RZ_CANFD',
2793
+ ' * selects USE_RZ_FSP_CANFD (Zephyr drivers/can/Kconfig.renesas_rz),',
2794
+ ' * which compiles fsp/src/${SOC_SERIES_PREFIX}/r_canfd/r_canfd.c',
2795
+ ' * (hal_renesas drivers/rz/CMakeLists.txt). hal_renesas carries that',
2796
+ ' * module for RZ/G only: fsp/src/rzg/r_canfd/ and',
2797
+ ' * fsp/inc/instances/rzg/r_canfd.h exist, the rzv counterparts do',
2798
+ ' * not. Setting the symbol under SOC_SERIES_RZV2N is a CMake',
2799
+ " * configure error on a missing source file, and the driver's",
2800
+ ' * #include "r_canfd.h" does not resolve on the rzv include path.',
2801
+ ' *',
2802
+ ' * 2. No CAN-FD node in the SoC devicetree. arm/renesas/rz/rzv/',
2803
+ ' * r9a09g056.dtsi declares none -- no label to reference, no reg and',
2804
+ ' * no interrupts to inherit. The only RZ SoC dtsi that carries them',
2805
+ " * is RZ/G3S's r9a08g045.dtsi (canfd-global@400c0000, global",
2806
+ ' * interrupts 373 and 374, per-channel 375..380). DO NOT COPY THOSE',
2807
+ ' * NUMBERS: different SoC, different vector map, different base. On',
2808
+ ' * THIS part the CAN-FD register base is 0x42440000 -- hal_renesas',
2809
+ ' * .../Include/R9A09G056N/iodefines/canfd_iodefine.h, R_CANFD_BASE.',
2810
+ " * The `(@ 0x400B0000)` in that same file's R_CANFD struct",
2811
+ " * doc-comment is a template annotation, not this SoC's base; it is",
2812
+ ' * the second trap in one header.',
2813
+ ' *',
2814
+ " * 3. No static NVIC vector exists to write. RZ/V2N's CM33 gives CAN-FD",
2815
+ " * no dedicated vector at all: bsp_irq_id.h's IRQn_Type enum has zero",
2816
+ ' * CANFD entries, and all 20 CAN-FD sources live in the separate',
2817
+ ' * IRQSELn_Type enum (354..373 -- per-channel err/rec/trx, plus',
2818
+ ' * CANFD_INTRCANGERR_IRQSELn 366 and CANFD_INTRCANGRECC_IRQSELn 367',
2819
+ ' * for the global controller). Reaching the NVIC means routing an',
2820
+ ' * IRQSEL source onto one of the selectable slots',
2821
+ ' * SEL0_IRQn..SEL126_IRQn (NVIC 353..479) at run time, through',
2822
+ " * R_BSP_SelectIrqSet() (bsp/mcu/all/bsp_mcu_api.h). Upstream's dtsi",
2823
+ ' * hands those slots out itself (adc0 = 403 = SEL50, gpt0 = 406..408',
2824
+ ' * = SEL53..55, on up to 453) and both CAN-FD bindings require',
2825
+ ' * `interrupts`, so choosing slots here means inventing an allocation',
2826
+ ' * that collides with whatever upstream picks when it adds the nodes.',
2827
+ ' *',
2828
+ ' * 4. No PFC function index for the pads. The module bonds out two of',
2829
+ " * the SoC's six controllers -- CANFD2 (CTX2 = P84, CRX2 = P85) and",
2830
+ ' * CANFD3 (CTX3 = P86, CRX3 = P87), per',
2831
+ ' * metadata/e1m_modules/v2n/renesas-peripheral-map.tsv -- but',
2832
+ ' * metadata/pinmux/v2n.yaml carries pad <-> signal only, with no',
2833
+ ' * alternate-function number. RZV_PINMUX() needs one, and it comes',
2834
+ " * from the RZ/V2N hardware manual's PFC table (the same source",
2835
+ ' * sci7_spi_pins above cites), not from this tree.',
2836
+ ' *',
2837
+ ' * What IS already in place, so a future port does not re-derive it: the',
2838
+ ' * clock path. RZ_CLOCK_CANFD(ch) exists',
2839
+ ' * (dt-bindings/clock/renesas_rzv_clock.h) and',
2840
+ ' * clock_control_renesas_rz_cpg.c already maps RZ_IP_CANFD onto',
2841
+ ' * R_BSP_MODULE_START(FSP_IP_CANFD, ch); the rzv2n bsp_feature.h declares',
2842
+ ' * 6 channels, CAN-FD support and 96 RX message buffers for this part.',
2843
+ ' *',
2844
+ " * Carrier note: the TCAN1044 transceivers' standby line is NOT a Renesas",
2845
+ ' * pad. CAN_STBY belongs to the GD32 IO-MCU on PB13',
2846
+ ' * (metadata/pinmux/v2n.yaml, metadata/e1m_modules/v2n/gd32-io-mcu-map.tsv),',
2847
+ ' * so taking the transceivers out of standby is a bridge GPIO call, not a',
2848
+ ' * pinctrl entry.',
2849
+ ' *',
2850
+ ' * Until those clear, <alp/can.h> on this core returns ALP_ERR_NOT_READY',
2851
+ ' * from src/backends/can/zephyr_drv.c -- the alp-canN aliases resolve to',
2852
+ ' * nothing.',
2853
+ ' */',
2854
+ '',
2855
+ '/*',
2856
+ ' * OpenAMP / RPMsg carve-out for the M33-SM <-> A55-Linux link (alp-sdk #683,',
2857
+ ' * Path B Phase 1). Addresses are the Renesas RZ/V Multi-OS Package memory',
2858
+ " * map, NOT the paper `ipc:` carve-out in this project's board.yaml. The two",
2859
+ ' * are different regions by construction, not by omission: `resolve_carve_outs()`',
2860
+ " * (scripts/alp_orchestrate/carveout.py) allocates board.yaml's `ipc:` entry",
2861
+ ' * from `ocram_low` (512 KiB, non-cacheable -- the only region it can reach',
2862
+ ' * from both `a55_cluster` and `m33_sm`; metadata/socs/renesas/rzv2n/n44.json)',
2863
+ ' * purely because the resolver needs *some* region and that is the only',
2864
+ ' * candidate -- nothing in this firmware reads or writes the address it',
2865
+ ' * resolves to. The real transport uses the hand-authored DDR window below',
2866
+ " * instead, sized from this responder's OpenAMP resource table (512 rpmsg",
2867
+ " * buffers x 512 B x 2 vrings -- m33_sm/src/resource_table.h's",
2868
+ ' * RSC_TABLE_NUM_RPMSG_BUFF). That sizing happens to consume the whole of',
2869
+ ' * `ocram_low` when expressed as `carve_out_kb` in board.yaml (both examples',
2870
+ ' * on this SoM request 512, i.e. all 512 KiB of `ocram_low`) -- harmless',
2871
+ ' * today because nothing consumes the resolved address, but it means a',
2872
+ ' * second `ipc:` entry on this SoM has no ocram_low headroom left to land',
2873
+ ' * in. Whether the fix is teaching the resolver about this DDR window, or',
2874
+ ' * shrinking the paper `carve_out_kb` values to leave ocram_low headroom, is',
2875
+ ' * an open sizing question for whoever picks it up next -- it is not settled',
2876
+ ' * by #683, which is about bench-validating the real `/dev/rpmsg` transport,',
2877
+ ' * not this address reconciliation.',
2878
+ ' *',
2879
+ ' * CORRECTED (alp-sdk #683, address root-cause fix): this region used to be',
2880
+ ' * quoted at the RZ/V2L CM33-NS offset (0x62f00000, i.e. A55 - 0x20000000),',
2881
+ ' * which is the V2N xSPI-NOR secure window on THIS SoC -- writes there never',
2882
+ ' * reach DRAM. The authoritative V2N map (Renesas FSP',
2883
+ ' * drivers/rz/fsp/src/rzv/bsp/mcu/rzv2n/bsp_slave_address.h, confirmed by',
2884
+ " * this board's upstream ddr node) is CM33-secure 0x80000000 / CM33-NS",
2885
+ ' * 0x90000000 / A55 0x40000000, a 256 MiB window, i.e. A55 phys = CM33-NS -',
2886
+ ' * 0x50000000. Of that window only A55 0x48000000..0x4FFFFFFF is real,',
2887
+ ' * populated NS RAM (0x40000000..0x47FFFFFF reads back 0xFF) -- the base',
2888
+ ' * below is chosen so the whole carve-out (0x4F700000..0x4FFFFFFF) sits',
2889
+ ' * entirely inside that populated range, right up against its top. The',
2890
+ ' * rsctbl/vring/shm regions below are all quoted in the CM33 view, matching',
2891
+ " * upstream's openamp_linux_zephyr sample; see resource_table.h for the",
2892
+ ' * CM33->A55 translation macro this same fix corrected.',
2893
+ ' *',
2894
+ ' * mbox1 is hand-authored: unlike the RZ/V2L SoC dtsi (r9a07g054.dtsi, which',
2895
+ ' * ships mbox1/mbox3/mbox4/mbox5), the upstream RZ/V2N SoC dtsi',
2896
+ ' * (r9a09g056.dtsi) has no MHU node at all yet -- added here at board level',
2897
+ ' * per the "board .dts stays hand-authored for the v2n family"',
2898
+ ' * (scripts/gen_zephyr_board.py) rather than editing the vendored SoC dtsi.',
2899
+ ' *',
2900
+ ' * RESOLVED (alp-sdk #683 Phase 2): RZ/V2N\'s MHU hardware is the newer "MHU-B"',
2901
+ ' * variant (hal_renesas drivers/rz/fsp/src/rzv/bsp/mcu/rzv2n/bsp_mhu_b.h) with',
2902
+ ' * only channels {5, 11, 17, 23} valid (BSP_FEATURE_MHU_B_NS_VALID_CHANNEL_MASK)',
2903
+ ' * and a non-linear per-channel register/IRQ pairing -- NOT the plain linear MHU',
2904
+ " * that Zephyr's mbox_renesas_rz_mhu.c + FSP r_mhu_ns.c implement (that pair does",
2905
+ ' * not compile for rzv2n). This node now uses the vendored MHU-B port instead',
2906
+ ' * (compatible `renesas,rz-mhu-b-mbox`, driver',
2907
+ ' * zephyr/drivers/mbox/mbox_renesas_rz_mhu_b.c, FSP module',
2908
+ " * zephyr/drivers/mbox/r_mhu_b_ns/r_mhu_b_ns.c -- see that file's header for the",
2909
+ ' * register-semantic notes and the "FLAG FOR REVIEW" send_type caveat, and',
2910
+ " * mbox_renesas_rz_mhu_b.c's header for the bounded #697 verification status:",
2911
+ ' * init/attach and A55->M33 receive are silicon-proven, mbox_send() M33->A55',
2912
+ ' * is unexercised and does not reach the A55 on this topology).',
2913
+ " * `channel = <5>` is the lowest MHU-B-valid channel. Per bsp_mhu_b.h's",
2914
+ ' * R_BSP_MHU_B_NS_REG_PAIR_BODY, channel 5 sends via R_MHU_NS36 (base',
2915
+ ' * 0x50480480) and receives via R_MHU_NS8 (base 0x50480100) -- `reg` below',
2916
+ ' * documents the send register only (the driver resolves both from the',
2917
+ ' * pair-body table by channel number, not from this property; the binding',
2918
+ ' * schema requires `reg` regardless). `interrupts = <293 2>` is',
2919
+ ' * MHU_MSG5_NS_IRQn (bsp_irq_id.h), which r_mhu_b_ns.c matches against',
2920
+ " * bsp_mhu_b.h's R_BSP_MHU_B_NS_SEND_TYPE_RSP_BODY to derive send_type = RSP",
2921
+ ' * for this instance.',
2922
+ ' */',
2923
+ '/ {',
2924
+ '\treserved-memory {',
2925
+ '\t\t#address-cells = <1>;',
2926
+ '\t\t#size-cells = <1>;',
2927
+ '\t\tranges;',
2928
+ '',
2929
+ '\t\t/* Whole OpenAMP region as one reservation to save MPU entries',
2930
+ "\t\t * (matches the vendor sample's rationale). */",
2931
+ '\t\topenamp_shm: memory@@carveout.hex@ {',
2932
+ '\t\t\tcompatible = "zephyr,memory-region";',
2933
+ '\t\t\treg = <@carveout.addr@ @carveout.size@>;',
2934
+ '\t\t\tzephyr,memory-region = "openamp_memory";',
2935
+ '\t\t\tzephyr,memory-attr = <DT_MEM_ARM(ATTR_MPU_IO)>;',
2936
+ '\t\t};',
2937
+ '\t};',
2938
+ '',
2939
+ '\t/* CM33-NS view minus A55 view of the OpenAMP reservation (from metadata) */',
2940
+ '\tzephyr,user {',
2941
+ '\t\talp,cm33-ns-to-a55-offset = <@carveout.offset@>;',
2942
+ '\t};',
2943
+ '',
2944
+ '\tchosen {',
2945
+ '\t\t/* The A55 master (DRIVER role) allocates every rpmsg buffer from',
2946
+ '\t\t * vring_shm1 (@vring-shm1.a55@ A55 / @vring-shm1.addr@ CM33-NS) -- the',
2947
+ '\t\t * "mst-alloc = vring-shm1" contract in the backend REFERENCE. The',
2948
+ "\t\t * M33's descriptor-translation window MUST cover that pool, so it",
2949
+ '\t\t * points at vring_shm1, not vring_shm0 (whose window ends exactly at',
2950
+ '\t\t * @vring-shm0.end@, leaving every buffer outside it). Silicon-root-caused',
2951
+ '\t\t * alongside the vring-DA fix (#683/#697 bench, 2026-07-11). */',
2952
+ '\t\tzephyr,ipc_shm = &vring_shm1;',
2953
+ '\t\tzephyr,ipc = &mbox_consumer;',
2954
+ '\t};',
2955
+ '',
2956
+ '\trsctbl: memory@@rsctbl.hex@ {',
2957
+ '\t\tcompatible = "mmio-sram";',
2958
+ '\t\treg = <@rsctbl.addr@ @rsctbl.size@>;',
2959
+ '\t};',
2960
+ '',
2961
+ "\t/* Widened from the RZ/V2L layout's 8-byte mhu1_shm (@ +0x1008) to a",
2962
+ '\t * full 4 KiB region immediately after rsctbl, so it lines up with the',
2963
+ "\t * A55/kernel-overlay side's @mhu-shm.a55hex@.mhu-shm node 1:1 (alp-sdk #683",
2964
+ '\t * address fix). */',
2965
+ '\tmhu1_shm: memory@@mhu-shm.hex@ {',
2966
+ '\t\tcompatible = "mmio-sram";',
2967
+ '\t\treg = <@mhu-shm.addr@ @mhu-shm.size@>;',
2968
+ '\t};',
2969
+ '',
2970
+ '\tvring_ctrl0: memory@@vring-ctl0.hex@ {',
2971
+ '\t\tcompatible = "mmio-sram";',
2972
+ '\t\treg = <@vring-ctl0.addr@ @vring-ctl0.size@>;',
2973
+ '\t};',
2974
+ '',
2975
+ '\tvring_ctrl1: memory@@vring-ctl1.hex@ {',
2976
+ '\t\tcompatible = "mmio-sram";',
2977
+ '\t\treg = <@vring-ctl1.addr@ @vring-ctl1.size@>;',
2978
+ '\t};',
2979
+ '',
2980
+ '\tvring_shm0: memory@@vring-shm0.hex@ {',
2981
+ '\t\tcompatible = "mmio-sram";',
2982
+ '\t\treg = <@vring-shm0.addr@ @vring-shm0.size@>;',
2983
+ '\t};',
2984
+ '',
2985
+ '\t/* The rpmsg buffer pool: the A55 master allocates from here, so this is',
2986
+ "\t * the M33's zephyr,ipc_shm window (see the chosen node above).",
2987
+ '\t * vring_shm0 is kept reserved so the region layout still matches the A55',
2988
+ "\t * kernel overlay's 6 nodes 1:1. */",
2989
+ '\tvring_shm1: memory@@vring-shm1.hex@ {',
2990
+ '\t\tcompatible = "mmio-sram";',
2991
+ '\t\treg = <@vring-shm1.addr@ @vring-shm1.size@>;',
2992
+ '\t};',
2993
+ '',
2994
+ '\tmbox_consumer: mbox-consumer {',
2995
+ '\t\tcompatible = "vnd,mbox-consumer";',
2996
+ '\t\tmboxes = <&mbox1 1>, <&mbox1 0>;',
2997
+ '\t\tmbox-names = "tx", "rx";',
2998
+ '\t};',
2999
+ '',
3000
+ '\tsoc {',
3001
+ '\t\t/* CA55_0 <-> CM33 -- see the MHU-B note above. */',
3002
+ '\t\tmbox1: mhu@50480480 {',
3003
+ '\t\t\tcompatible = "renesas,rz-mhu-b-mbox";',
3004
+ '\t\t\tchannel = <5>;',
3005
+ '\t\t\treg = <0x50480480 0x20>; /* R_MHU_NS36 (send reg) -- see the MHU-B note above */',
3006
+ '\t\t\ttx-mask = <0x00000002>; /* Channel 1 is for TX */',
3007
+ '\t\t\trx-mask = <0x00000001>; /* Channel 0 is for RX */',
3008
+ '\t\t\tchannels-count = <2>;',
3009
+ '\t\t\tinterrupts = <293 2>; /* MHU_MSG5_NS_IRQn, bsp_irq_id.h -- verified against the vendor header */',
3010
+ '\t\t\tinterrupt-names = "mhuns";',
3011
+ '\t\t\t#mbox-cells = <1>;',
3012
+ '\t\t\tshared-memory = <&mhu1_shm>;',
3013
+ '\t\t\tstatus = "okay";',
3014
+ '\t\t};',
3015
+ '\t};',
3016
+ '};',
3017
+ )
3018
+
3019
+
3020
+ def _openamp_carveout(soc_spec: dict[str, Any]) -> dict[str, Any]:
3021
+ """The SoC spec's `openamp_carveout` block, refused with a coded error
3022
+ naming the first missing field -- never a bare `KeyError` out of `tan
3023
+ generate --target zephyr-board` against an alp-sdk that predates the
3024
+ block (alp-sdk#2685, the floor this change raises tan to)."""
3025
+ where = f"SoC spec {soc_spec.get('ref')} `openamp_carveout`"
3026
+ c = soc_spec.get("openamp_carveout")
3027
+ if not isinstance(c, dict) or not c:
3028
+ raise ZephyrBoardEmitError(
3029
+ f"{where} is missing -- the V2N/V2M board emits the OpenAMP window "
3030
+ "and the CM33 RAM console from it (needs alp-sdk b04bb0f7a or newer)")
3031
+
3032
+ def need(block: dict[str, Any], name: str, path: str) -> Any:
3033
+ if not isinstance(block, dict) or block.get(name) is None:
3034
+ raise ZephyrBoardEmitError(
3035
+ f"{where} has no `{path}{name}` field")
3036
+ return block[name]
3037
+
3038
+ for f in ("cm33_ns_base", "a55_base", "size"):
3039
+ need(c, f, "")
3040
+ regions = need(c, "regions", "")
3041
+ if not isinstance(regions, dict):
3042
+ raise ZephyrBoardEmitError(f"{where} `regions` is not a mapping")
3043
+ for name, r in regions.items():
3044
+ for f in ("offset", "size"):
3045
+ need(r, f, f"regions.{name}.")
3046
+ rc = need(c, "ram_console", "")
3047
+ for f in ("offset", "size"):
3048
+ need(rc, f, "ram_console.")
3049
+ return c
3050
+
3051
+
3052
+ def _openamp_subst(tail: tuple[str, ...], soc_spec: dict[str, Any]) -> list[str]:
3053
+ """Fill the OpenAMP window tokens from the SoC's `openamp_carveout`
3054
+ (metadata/socs/**.json; the one declaration the Linux DT, the backend
3055
+ header and scripts/check_amp_window.py also read). Tokens:
3056
+ `@carveout.{addr,hex,size}@` for the whole reservation and, per name in
3057
+ `regions`, `@<name>.{addr,hex,size,end,a55,a55hex}@` -- addr/hex/end in
3058
+ the CM33-NS view, a55/a55hex in the A55 view."""
3059
+ c = _openamp_carveout(soc_spec)
3060
+ base, a55 = c["cm33_ns_base"], c["a55_base"]
3061
+ tok = {
3062
+ "@carveout.addr@": f"{base:#x}",
3063
+ "@carveout.hex@": f"{base:x}",
3064
+ "@carveout.size@": f"{c['size']:#x}",
3065
+ "@carveout.offset@": f"{base - a55:#x}",
3066
+ }
3067
+ for name, r in c["regions"].items():
3068
+ off, size = r["offset"], r["size"]
3069
+ tok.update({
3070
+ f"@{name}.addr@": f"{base + off:#x}",
3071
+ f"@{name}.hex@": f"{base + off:x}",
3072
+ f"@{name}.size@": f"{size:#x}",
3073
+ f"@{name}.end@": f"{base + off + size:#x}",
3074
+ f"@{name}.a55@": f"{a55 + off:#x}",
3075
+ f"@{name}.a55hex@": f"{a55 + off:x}",
3076
+ })
3077
+ out = []
3078
+ for line in tail:
3079
+ for k, v in tok.items():
3080
+ line = line.replace(k, v)
3081
+ out.append(line)
3082
+ return out
3083
+
3084
+
3085
+ def _ram_console(soc_spec: dict[str, Any]) -> tuple[int, int, int]:
3086
+ """(A55 address, CM33-NS address, size) of the CM33 RAM console, from the
3087
+ SoC's `openamp_carveout.ram_console` (the one declaration; the dts node,
3088
+ CONFIG_RAM_CONSOLE_BUFFER_SIZE and scripts/gen_amp_window.py all read it)."""
3089
+ c = _openamp_carveout(soc_spec)
3090
+ off = c["ram_console"]["offset"]
3091
+ return c["a55_base"] + off, c["cm33_ns_base"] + off, c["ram_console"]["size"]
3092
+
3093
+
3094
+ def _v2n_dts(
3095
+ sku: str, dir_name: str, soc_spec: dict[str, Any], variant: dict[str, Any],
3096
+ sku_preset: dict[str, Any], links: dict[str, Any],
3097
+ assignable: list = (),
3098
+ ) -> str:
3099
+ """Board `.dts` for a V2N/V2M `m33_sm` board (#655 slice 2).
3100
+
3101
+ Reuses the upstream RZ/V2N SoC devicetree; adds the on-module GD32G553
3102
+ supervisor links from `supervisor-links.yaml` (the same source
3103
+ `_v2n_pinctrl_dtsi()` reads). Only SoMs whose
3104
+ `topology.m33_sm.openamp_ipc` preset field is true also get the
3105
+ OpenAMP/MHU-B reserved-memory block and the CAN-FD-unavailable
3106
+ analysis (alp-sdk #683/#1146) -- E1M-V2M101 does not carry either
3107
+ block in the currently-committed tree, so this generator reproduces
3108
+ that gap rather than "fixing" it; the byte-identity pin
3109
+ (`tests/scripts/test_gen_zephyr_board.py`) is against what is
3110
+ committed today, not against what a more "complete" V2M101 board
3111
+ would look like.
3112
+ """
3113
+ slug = sku[len("E1M-"):].lower()
3114
+ compatible = f"alp,e1m-{slug}-m33-sm"
3115
+ model = f"Alp {sku} Cortex-M33 system manager"
3116
+
3117
+ part_display, soc_dtsi_include = _v2n_part_display(variant["order_code"])
3118
+ family = soc_spec.get("family")
3119
+ if not family or _is_tbd(family):
3120
+ raise ZephyrBoardEmitError(f"SoC spec {soc_spec.get('ref')} declares no usable `family`")
3121
+ family_display = f"{family} {part_display}"
3122
+ if (sku_preset.get("on_module") or {}).get("npu") == "deepx_dxm1":
3123
+ family_display += " + DEEPX DX-M1"
3124
+
3125
+ console = links["console"]
3126
+ gd32_spi = links["gd32_spi"]
3127
+ brd_i2c = links["brd_i2c"]
3128
+ wdt = _find_core(soc_spec, "m33_sm").get("watchdog") or {}
3129
+ if not wdt:
3130
+ raise ZephyrBoardEmitError(
3131
+ f"SoC spec {soc_spec.get('ref')} core m33_sm declares no `watchdog` "
3132
+ "block (base/size/counting_clock_hz) -- the V2N/V2M board emits the "
3133
+ "CM33 watchdog node and the alp-wdt0 alias from it")
3134
+ wdt_base = wdt["base"]
3135
+ txd0 = _pin_by_peripheral(console["pins"], "UART0_TXD0")
3136
+ rxd0 = _pin_by_peripheral(console["pins"], "UART0_RXD0")
3137
+ mosi = _pin_by_peripheral(gd32_spi["pins"], "GD32_SPI.MOSI")
3138
+ miso = _pin_by_peripheral(gd32_spi["pins"], "GD32_SPI.MISO")
3139
+ sclk = _pin_by_peripheral(gd32_spi["pins"], "GD32_SPI.SCLK")
3140
+ sda = _pin_by_peripheral(brd_i2c["pins"], "RIIC8_SDA8")
3141
+ scl = _pin_by_peripheral(brd_i2c["pins"], "RIIC8_SCL8")
3142
+ cs0 = gd32_spi["gpio_chip_select"]
3143
+ pads = links.get("gd32_pads") # optional: GD32 SWD + ATTN pads (alp,gd32-pads)
3144
+ peer_addr = brd_i2c["peer_address_7bit"]
3145
+ ch = _v2n_sci_channel(gd32_spi["dt_label"]) # "7"
3146
+
3147
+ has_openamp = bool(
3148
+ (sku_preset.get("topology") or {}).get("m33_sm", {}).get("openamp_ipc"))
3149
+ rc_a55, rc_addr, rc_size = _ram_console(soc_spec)
3150
+ carveout = _openamp_carveout(soc_spec)
3151
+ win_base, win_size = carveout["cm33_ns_base"], carveout["size"]
3152
+
3153
+ lines: list[str] = [
3154
+ "/*",
3155
+ " * Copyright (c) 2026 Alp Lab AB",
3156
+ " * SPDX-License-Identifier: Apache-2.0",
3157
+ " *",
3158
+ f" * {sku} ({family_display}) Cortex-M33 system-manager.",
3159
+ " *",
3160
+ " * Reuses the upstream RZ/V2N SoC devicetree + the EVK SCI_UART0 console and",
3161
+ " * adds the on-module GD32G553 supervisor links:",
3162
+ f" * - SCI{ch} Simple-SPI (alias {gd32_spi['alias']}): {mosi['silicon_pad']} MOSI{ch} / "
3163
+ f"{miso['silicon_pad']} MISO{ch} / {sclk['silicon_pad']} SCK{ch} +",
3164
+ f" * {cs0['silicon_pad']} GPIO chip-select (master SCI-SPI has no hardware slave-select)",
3165
+ f" * - RIIC8 / BRD_I2C: {sda['silicon_pad']} SDA / {scl['silicon_pad']} SCL "
3166
+ f"(GD32 slave @ 0x{peer_addr:02x}) -- Cortex-A55/Linux-exclusive,",
3167
+ " * NOT wired on this board: no alias, &i2c8 stays disabled below.",
3168
+ " *",
3169
+ " * The on-module silicon is the n44 variant; Zephyr only models the n48gbg",
3170
+ " * SoC, which is devicetree-identical for the M33 + SPI/I2C peripherals",
3171
+ " * (the n44/n48 delta is GPU/ISP/crypto fusing only).",
3172
+ " */",
3173
+ "",
3174
+ "/dts-v1/;",
3175
+ "",
3176
+ "#include <zephyr/dt-bindings/i2c/i2c.h>",
3177
+ "#include <zephyr/dt-bindings/gpio/gpio.h>",
3178
+ ]
3179
+ if has_openamp:
3180
+ lines.append("#include <zephyr/dt-bindings/memory-attr/memory-attr-arm.h>")
3181
+ lines += [
3182
+ f"#include <{soc_dtsi_include}>",
3183
+ f'#include "{dir_name}-pinctrl.dtsi"',
3184
+ "",
3185
+ "/ {",
3186
+ f'\tmodel = "{model}";',
3187
+ f'\tcompatible = "{compatible}";',
3188
+ "",
3189
+ "\tchosen {",
3190
+ "\t\tzephyr,sram = &sram;",
3191
+ "\t\tzephyr,ram-console = &ram_console;",
3192
+ "\t\t/*",
3193
+ "\t\t * No CM33 serial console on this board. The only console is the",
3194
+ "\t\t * A55's (Linux, on the shared debug UART); sci0 here is the EVK",
3195
+ '\t\t * "Pmod USB-UART", which is NOT wired as a CM33 console on the SoM.',
3196
+ "\t\t * Bringing it up enables RX on a floating RXD, whose receive-error",
3197
+ "\t\t * interrupt (sci0 eri = NVIC 114) escalates to a Zephyr fatal",
3198
+ "\t\t * (arch_system_halt) and hangs the CM33 before main() ever runs.",
3199
+ "\t\t * Leave the UART console unset (sci0 is disabled below); only the",
3200
+ "\t\t * RAM console is chosen. Re-add zephyr,console/zephyr,shell-uart and",
3201
+ "\t\t * re-enable sci0 only when a Pmod USB-UART is attached.",
3202
+ "\t\t */",
3203
+ "\t};",
3204
+ "",
3205
+ "\taliases {",
3206
+ f"\t\t{gd32_spi['alias']} = &gd32_spi;",
3207
+ "\t\talp-wdt0 = &wdt0;",
3208
+ "\t};",
3209
+ "",
3210
+ "\tsram: memory@8003000 {",
3211
+ '\t\tcompatible = "mmio-sram";',
3212
+ "\t\treg = <0x08003000 0xfbfff>;",
3213
+ "\t};",
3214
+ "",
3215
+ "\t/*",
3216
+ f"\t * RAM console buffer (audit CM33-M2): {rc_size // 1024} KiB at CM33-NS {rc_addr:#x} (A55",
3217
+ f"\t * view {rc_a55:#x}), from the SoC's openamp_carveout.ram_console: inside the",
3218
+ f"\t * {win_size // 1048576} MiB {win_base:#x} window Linux reserves no-map and clear of every",
3219
+ "\t * region listed there (rsctbl with the liveness beacon in its top 16",
3220
+ "\t * bytes, mhu-shm, the vring control and buffer areas).",
3221
+ "\t */",
3222
+ f"\tram_console: memory@{rc_addr:x} {{",
3223
+ '\t\tcompatible = "zephyr,memory-region";',
3224
+ '\t\tzephyr,memory-region = "RAM_CONSOLE";',
3225
+ f"\t\treg = <{rc_addr:#x} {rc_size:#x}>;",
3226
+ "\t};",
3227
+ "",
3228
+ "\t/*",
3229
+ "\t * alp pin map for alp_spi/alp_gpio id resolution. alp_spi_open(cs_pin_id=N)",
3230
+ "\t * resolves its chip-select gpio_dt_spec from gpios[N] of this node (see the",
3231
+ "\t * SPI backend's alp_z_gpio_resolve()). Index 0 = the GD32 SPI chip-select",
3232
+ f"\t * on {cs0['silicon_pad']}, matching the example's cs_pin_id = 0.",
3233
+ ]
3234
+ lines += [
3235
+ "\t */",
3236
+ "\talp_pins: alp-pins {",
3237
+ '\t\tcompatible = "alp,pin-array";',
3238
+ f"\t\tgpios = <&{cs0['gpio_node']} {cs0['gpio_pin']} GPIO_ACTIVE_LOW>;",
3239
+ "\t};",
3240
+ ]
3241
+ if pads:
3242
+
3243
+ def pol(pad: dict[str, Any]) -> str:
3244
+ return "GPIO_ACTIVE_LOW" if pad["active_low"] else "GPIO_ACTIVE_HIGH"
3245
+
3246
+ def spec(role: str) -> str:
3247
+ pad = pads[role]
3248
+ return f"<&{pad['gpio_node']} {pad['gpio_pin']} {pol(pad)}>"
3249
+
3250
+ lines += [
3251
+ "",
3252
+ "\t/*",
3253
+ "\t * The GD32 control pads outside the SPI link (protocol v0.15, section 3.17).",
3254
+ "\t * A DEDICATED node, deliberately not entries of alp_pins above (whose index 0",
3255
+ "\t * is the GD32 SPI chip-select): resolved only through the INTERNAL pad opener",
3256
+ "\t * (the portable alp_gpio_open() refuses GD32G553_PAD_ID_*). All four are plain GPIO, inputs out of",
3257
+ "\t * reset. swclk and attn are the SAME pad (P71 = GD32 PA14): the host drives it",
3258
+ "\t * only inside an SWD session, with GD32_NRST asserted first, and otherwise uses",
3259
+ "\t * it as the bridge ATTN input with a rising-edge IRQ. nrst is open-drain on the",
3260
+ "\t * board; the Renesas GPIO driver has no open-drain mode, so the SWD driver",
3261
+ "\t * emulates it (low = assert, input = release) and never drives the pad high.",
3262
+ "\t */",
3263
+ "\tgd32_pads: gd32-pads {",
3264
+ '\t\tcompatible = "alp,gd32-pads";',
3265
+ f"\t\tswdio-gpios = {spec('swdio')};",
3266
+ f"\t\tswclk-gpios = {spec('swclk')};",
3267
+ f"\t\tnrst-gpios = {spec('nrst')};",
3268
+ f"\t\tattn-gpios = {spec('attn')};",
3269
+ "\t};",
3270
+ ]
3271
+ lines += [
3272
+ "};",
3273
+ "",
3274
+ "/*",
3275
+ f' * {console["dt_label"]} = EVK "Pmod USB-UART" console. DISABLED on this board: it is not wired',
3276
+ " * as a CM33 console here, and opening it (RX enabled on a floating RXD) faults",
3277
+ " * the CM33 in its receive-error ISR (sci0 eri / NVIC 114) -> Zephyr fatal ->",
3278
+ " * arch_system_halt, before the app runs. Keep pinctrl for easy re-enable.",
3279
+ " */",
3280
+ f"&{console['dt_label']} {{",
3281
+ f"\tpinctrl-0 = <&{console['pinctrl_group_label']}>;",
3282
+ '\tpinctrl-names = "default";',
3283
+ '\tstatus = "disabled";',
3284
+ "",
3285
+ "\tuart0: uart {",
3286
+ "\t\tcurrent-speed = <115200>;",
3287
+ '\t\tstatus = "disabled";',
3288
+ "\t};",
3289
+ "};",
3290
+ "",
3291
+ "/*",
3292
+ f" * GD32 supervisor SPI fast path -- SCI channel {ch} in clock-synchronous",
3293
+ ' * "Simple SPI" mode (sci7@42802800, compatible renesas,rz-sci-b). The board',
3294
+ f" * wires the GD32 to {mosi['silicon_pad']}/{miso['silicon_pad']}/{sclk['silicon_pad']}/{cs0['silicon_pad']}, "
3295
+ f"which per the RZ/V2N PFC (Table 1.2-3) are",
3296
+ f" * MOSI{ch}/MISO{ch}/SCK{ch}/SS{ch} = SCI channel {ch} -- NOT the dedicated SPI_B IP. The sci{ch}",
3297
+ " * parent (from the SoC dtsi) already supplies reg, channel = <7> and the fixed",
3298
+ " * CM33 vectors (eri=156, rxi=157, txi=158, tei=159); here we enable it, point it",
3299
+ " * at the SCI7 SPI pins, and attach the SPI child.",
3300
+ " *",
3301
+ " * Master Simple-SPI has NO hardware slave-select (the FSP sets CCR0.SSE only in",
3302
+ f" * slave mode), so the GD32 chip-select on {cs0['silicon_pad']} is driven as a GPIO via cs-gpios",
3303
+ f" * ({cs0['gpio_node']} pin {cs0['gpio_pin']}) -- the GD32's PA8 CS-EXTI needs per-transaction framing.",
3304
+ " */",
3305
+ f"&{gd32_spi['dt_label']} {{",
3306
+ f"\tpinctrl-0 = <&{gd32_spi['pinctrl_group_label']}>;",
3307
+ '\tpinctrl-names = "default";',
3308
+ '\tstatus = "okay";',
3309
+ "",
3310
+ "\tgd32_spi: spi {",
3311
+ '\t\tcompatible = "renesas,rz-sci-b-spi";',
3312
+ "\t\t#address-cells = <1>;",
3313
+ "\t\t#size-cells = <0>;",
3314
+ f"\t\tcs-gpios = <&{cs0['gpio_node']} {cs0['gpio_pin']} GPIO_ACTIVE_LOW>;",
3315
+ '\t\tstatus = "okay";',
3316
+ "\t};",
3317
+ "};",
3318
+ "",
3319
+ f"&{cs0['gpio_node']} {{",
3320
+ '\tstatus = "okay";',
3321
+ "};",
3322
+ "",
3323
+ ]
3324
+ if pads and pads["attn"].get("tint_slot") is not None and pads["swdio"]["gpio_node"] == cs0["gpio_node"]:
3325
+ raise SystemExit("gd32_pads.attn.tint_slot set but the pads block is skipped (swdio shares CS0's gpio node)")
3326
+ if pads and pads["swdio"]["gpio_node"] != cs0["gpio_node"]:
3327
+ tint = pads["attn"].get("tint_slot")
3328
+ if tint is not None:
3329
+ if pads["attn"]["gpio_node"] != pads["swdio"]["gpio_node"]:
3330
+ raise SystemExit("gd32_pads.attn.tint_slot needs attn on the swdio gpio node")
3331
+ lines += [
3332
+ "/*",
3333
+ " * ATTN (P71) interrupt: the RZ GPIO driver routes a pin to a TINT slot through",
3334
+ " * the port's `irqs`; without it alp_gpio_irq_enable() has no route and the",
3335
+ " * supervisor stays on the staging-gap fallback. The slot is a shared ICU",
3336
+ " * resource, claimed here for the CM33 (supervisor-links.yaml gd32_pads.attn).",
3337
+ " */",
3338
+ f"&tint{tint} {{",
3339
+ '\tstatus = "okay";',
3340
+ "};",
3341
+ "",
3342
+ ]
3343
+ lines += [f"&{pads['swdio']['gpio_node']} {{", '\tstatus = "okay";']
3344
+ if tint is not None:
3345
+ lines.append(f"\tirqs = <&tint{tint} {pads['attn']['gpio_pin']}>;")
3346
+ lines += ["};", ""]
3347
+ lines += [
3348
+ "/*",
3349
+ f" * {brd_i2c['peripheral']} / BRD_I2C is Cortex-A55/Linux-exclusive",
3350
+ " * (metadata/e1m_modules/v2n/core-ownership.yaml) -- the CM33 must never",
3351
+ " * master it, so this node stays disabled and carries no alias (no",
3352
+ " * alp_i2c_open() bus_id resolves to it on this board). The pinctrl group",
3353
+ f" * ({brd_i2c['pinctrl_group_label']} in {dir_name}-pinctrl.dtsi) is kept for",
3354
+ " * reference only -- an unreferenced group claims no pin.",
3355
+ " */",
3356
+ f"&{brd_i2c['dt_label']} {{",
3357
+ '\tstatus = "disabled";',
3358
+ "};",
3359
+ "",
3360
+ ] + _v2n_assignable_dts(assignable) + [
3361
+ "/*",
3362
+ f" * wdt0 = the Cortex-M33's own watchdog (base {wdt_base} and counting clock",
3363
+ " * from the SoC spec's m33_sm `watchdog` block; hal_renesas R9A09G056N",
3364
+ " * wdt_iodefine.h R_WDT0_BASE). The upstream SoC devicetree",
3365
+ " * (arm/renesas/rz/rzv/r9a09g056.dtsi) declares no watchdog, and upstream's",
3366
+ " * `renesas,rz-wdt` driver cannot bind RZ/V2N, so the node uses the alp-sdk",
3367
+ " * driver zephyr/drivers/watchdog/wdt_renesas_rzv.c (compatible",
3368
+ " * `renesas,rzv-wdt`; alp-sdk#2660). DISABLED by default: an expiry resets the",
3369
+ " * WHOLE SoM, not just the M33, so a product opts in with `&wdt0 { status =",
3370
+ ' * "okay"; };`. <alp/wdt.h> then reaches it through alias alp-wdt0. Enabling it',
3371
+ " * also needs the CPG module clocks held for the CM33 (renesas,cm33-owned-clocks,",
3372
+ " * docs/bench/rzv-wdt0-cm33.md). clock-freq is the WDT0 counting clock",
3373
+ " * (WDT_0_clk_loco, 24 MHz Main OSC; RZ/V2N hardware manual Table 4.4-2).",
3374
+ " */",
3375
+ "&{/soc} {",
3376
+ f"\twdt0: watchdog@{wdt_base[2:]} {{",
3377
+ '\t\tcompatible = "renesas,rzv-wdt";',
3378
+ f"\t\treg = <{wdt_base} {wdt['size']}>;",
3379
+ f"\t\tclock-freq = <{wdt['counting_clock_hz']}>;",
3380
+ '\t\tstatus = "disabled";',
3381
+ "\t};",
3382
+ "};",
3383
+ ]
3384
+ if has_openamp:
3385
+ lines += _openamp_subst(_V2N_OPENAMP_TAIL, soc_spec)
3386
+ lines.append("")
3387
+ return "\n".join(lines)
3388
+
3389
+
3390
+ # ---------------------------------------------------------------------
3391
+ # Top-level entry point
3392
+ # ---------------------------------------------------------------------
3393
+
3394
+
3395
+ def emit_zephyr_board(
3396
+ sku: str, core_id: str, metadata_root: Path,
3397
+ ) -> dict[str, str]:
3398
+ """Return {relative_path: content} for the requested SoM+core's board tree.
3399
+
3400
+ *relative_path* is rooted at the board directory (e.g.
3401
+ ``"alp_e1m_aen801_m55_hp/board.yml"``) so the caller can write the
3402
+ result straight under a `--board-root`-style output directory.
3403
+ """
3404
+ sku_preset = _resolve_sku(sku, metadata_root)
3405
+ soc_spec = _load_soc_spec(sku_preset, metadata_root)
3406
+ variant = _resolve_variant(sku_preset, soc_spec)
3407
+ core = _find_core(soc_spec, core_id)
3408
+ dir_name, cluster, variant_tag, basename = _board_naming(
3409
+ sku_preset, core_id, variant, core)
3410
+ topo = _topology_entry(sku_preset, core_id)
3411
+ full_name = topo.get("zephyr_full_name")
3412
+ if not full_name:
3413
+ raise ZephyrBoardEmitError(
3414
+ f"topology.{core_id}.zephyr_full_name is required for "
3415
+ f"--emit zephyr-board (SoM {sku!r}); add it to "
3416
+ f"metadata/e1m_modules/{sku}.yaml")
3417
+ twister_name = topo.get("zephyr_twister_name") or full_name
3418
+ family = _sku_family_slug(sku)
3419
+
3420
+ files: dict[str, str] = {
3421
+ f"{dir_name}/board.yml": _board_yml(dir_name, full_name, variant_tag, family),
3422
+ f"{dir_name}/Kconfig.{dir_name}": _kconfig_board(dir_name, variant_tag, cluster),
3423
+ f"{dir_name}/{basename}.yaml": _twister_yaml(
3424
+ dir_name, variant_tag, cluster, twister_name, family,
3425
+ sku_preset, core_id, variant),
3426
+ }
3427
+
3428
+ # relpath -> the extra generation-authority file its banner must name.
3429
+ banner_extra_source: dict[str, str] = {}
3430
+ if family == "aen":
3431
+ rx_row, tx_row = _aen_console_pinmux_rows(metadata_root)
3432
+ role = core_id.split("_")[-1]
3433
+ uart_node = _uart_node_label(rx_row)
3434
+ memory_map = sku_preset.get("memory_map")
3435
+ _aen_require_disjoint_slot0(sku, sku_preset, memory_map)
3436
+ # This role's own disjoint slot0 base (#1069), falling back to the
3437
+ # stock symmetric-layout address (the App MRAM base + the mcuboot
3438
+ # partition, same for every single-M55 AEN SKU and for AEN801
3439
+ # boards with no memory_map override) when this SoM declares no
3440
+ # per-role `<role>_slot0` region -- see _aen_role_slot0_map /
3441
+ # _aen_flash_partitions.
3442
+ slot0_region = _aen_role_slot0_map(memory_map, role)
3443
+ slot0_base = (slot0_region["base"] if slot0_region
3444
+ else _AEN_MRAM_BASE + _AEN_MCUBOOT_KIB * 1024)
3445
+ # The on-module (non-edge) pad + device source: BRD_I2C's P7_0/P7_1 and
3446
+ # the RV-3028 alarm on P15_0 reach no E1M edge pin, so metadata/pinmux/
3447
+ # aen.yaml (an edge-pad projection) has no row for them -- see
3448
+ # _load_aen_on_module_links().
3449
+ on_module_links = _load_aen_on_module_links(metadata_root)
3450
+ aen_pinctrl_relpath = f"{dir_name}/{dir_name}-pinctrl.dtsi"
3451
+ aen_dts_relpath = f"{dir_name}/{basename}.dts"
3452
+ files[aen_pinctrl_relpath] = _aen_pinctrl_dtsi(
3453
+ role, sku, rx_row, tx_row, _aen_family_display(soc_spec),
3454
+ on_module_links, _aen_part(soc_spec))
3455
+ files[f"{dir_name}/{basename}_defconfig"] = _aen_defconfig(
3456
+ uart_node, rx_row, tx_row, slot0_base)
3457
+ files[f"{dir_name}/Kconfig.defconfig"] = _aen_kconfig_defconfig(
3458
+ dir_name, role, _aen_part(soc_spec))
3459
+ files[aen_dts_relpath] = _aen_dts(
3460
+ sku, sku_preset, core_id, soc_spec, variant, dir_name, basename,
3461
+ rx_row, tx_row, metadata_root, on_module_links,
3462
+ _aen_ethos_u(soc_spec), memory_map,
3463
+ _load_aen_power_domains(metadata_root),
3464
+ _load_aen_wired_lpgpio_pads(metadata_root))
3465
+ banner_extra_source.update(dict.fromkeys(
3466
+ (aen_pinctrl_relpath, aen_dts_relpath),
3467
+ "metadata/e1m_modules/aen/on-module-links.yaml"))
3468
+ elif family in _v2n_declared_families(metadata_root):
3469
+ # `Kconfig.defconfig` stays hand-authored in this slice (it does
3470
+ # not exist in the V2N/V2M board directories); the board `.dts`
3471
+ # does not -- #655 slice 2 closed that gap (alp-sdk e4f2f60e4).
3472
+ # Which families this branch covers is read from
3473
+ # metadata/e1m_modules/v2n/supervisor-links.yaml's own
3474
+ # `families:` list, not a local constant -- dropping `v2n-m1`
3475
+ # there stops V2M emission without a code change.
3476
+ supervisor_links = _load_supervisor_links(metadata_root)
3477
+ v2n_pinctrl_relpath = f"{dir_name}/{dir_name}-pinctrl.dtsi"
3478
+ v2n_defconfig_relpath = f"{dir_name}/{basename}_defconfig"
3479
+ v2n_dts_relpath = f"{dir_name}/{basename}.dts"
3480
+ assignable = _v2n_assignable_m33(metadata_root, soc_spec)
3481
+ files[v2n_pinctrl_relpath] = _v2n_pinctrl_dtsi(supervisor_links, assignable)
3482
+ files[v2n_defconfig_relpath] = _v2n_defconfig(supervisor_links, soc_spec)
3483
+ files[v2n_dts_relpath] = _v2n_dts(
3484
+ sku, dir_name, soc_spec, variant, sku_preset, supervisor_links,
3485
+ assignable)
3486
+ # None of the three files carries a single pad, PFC triple or I2C
3487
+ # address from sku_rel/soc_json_rel below -- every one of those
3488
+ # facts comes from supervisor-links.yaml, so its path is threaded
3489
+ # into all three files' banners too (below), not just the
3490
+ # family-agnostic ones.
3491
+ banner_extra_source.update(dict.fromkeys(
3492
+ (v2n_pinctrl_relpath, v2n_defconfig_relpath, v2n_dts_relpath),
3493
+ "metadata/e1m_modules/v2n/supervisor-links.yaml"))
3494
+
3495
+ # `_load_soc_spec()` above already raised ZephyrBoardEmitError if
3496
+ # `sku_preset["silicon"]` didn't resolve, so `soc_path` can't be None
3497
+ # here -- but that's a dependency on call order, not a guarantee this
3498
+ # function itself enforces, so don't drop the None-guard idiom.
3499
+ soc_path = resolve_soc_path(sku_preset["silicon"], metadata_root)
3500
+ if soc_path is None:
3501
+ raise ZephyrBoardEmitError(
3502
+ f"silicon ref {sku_preset['silicon']!r} is not a triple-colon string")
3503
+ soc_json_rel = f"metadata/{soc_path.relative_to(metadata_root).as_posix()}"
3504
+ for relpath in list(files):
3505
+ style = "c" if relpath.endswith((".dts", "-pinctrl.dtsi")) else "hash"
3506
+ extra_source = banner_extra_source.get(relpath)
3507
+ files[relpath] = _with_generated_banner(
3508
+ files[relpath], style, sku, soc_json_rel, extra_source)
3509
+
3510
+ return files