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,755 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Carve-out resolver -- board.yaml `ipc:` entries -> resolved shared-memory regions.
4
+
5
+ Each IPC entry maps to a `ResolvedCarveOut`: the shared-memory base/size come
6
+ from the SoM's resolved memory_map, the region id is a deterministic FNV-1a
7
+ hash, plus the default mailbox channel and page alignment. An entry that can't
8
+ resolve (missing mailbox metadata / no memory_map) becomes a blocked
9
+ ResolvedCarveOut carrying the reason. Extracted from alp_orchestrate as the #285
10
+ carve-out seam.
11
+
12
+ Depends only downward -- models (dataclasses; the metadata root now travels
13
+ on the BoardProject, tan-cli#573) and
14
+ som_metadata.resolve_memory_map; nothing calls back into the package __init__.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Optional
20
+
21
+ from .aperture import classify_region, region_extent, resolve_aperture
22
+ from .models import BoardProject, IpcEntry, ResolvedCarveOut
23
+ from .memregion import _PAGE, _region_size_bytes
24
+ from .som_metadata import resolve_memory_map
25
+
26
+
27
+ # Cap on how many per-region details join into one ineligibility reason
28
+ # (alp-sdk#1365 split B review, Nit). The joined string is emitted VERBATIM
29
+ # as a SINGLE-LINE C comment in `alp_system_ipc.h` and a single-line DTS
30
+ # comment (`headers.py`); on the real E1M-AEN801 preset (5 excluded
31
+ # regions -- today's ceiling: every AEN SKU's memory_map has exactly 7
32
+ # rows, and the widest `ipc:` endpoint set that isn't a32_cluster-only ever
33
+ # matches 5 of them) the uncapped string reached ~1.5 kB, and a preset with
34
+ # more `memory_map:` rows grows it unboundedly. Set one above today's
35
+ # proven ceiling so no current preset ever truncates (excluded regions can
36
+ # carry DIFFERENT reasons -- flash-class containment vs. an unresolved
37
+ # base -- so silently dropping one is a real loss of diagnostic precision,
38
+ # not just length). This still bounds genuinely pathological growth (a
39
+ # preset with many more rows) without touching any output produced by
40
+ # metadata that exists today.
41
+ _MAX_EXCLUDED_DETAIL = 6
42
+
43
+
44
+ def _fnv1a_32(data: bytes) -> int:
45
+ """FNV-1a 32-bit hash. 10 lines, no deps."""
46
+ h = 0x811c9dc5
47
+ for b in data:
48
+ h ^= b
49
+ h = (h * 0x01000193) & 0xFFFFFFFF
50
+ return h
51
+
52
+
53
+
54
+ def _align_down(value: int, alignment: int) -> int:
55
+ return value - (value % alignment)
56
+
57
+
58
+ def _resolve_mailbox_channel(
59
+ mailbox: dict[str, Any],
60
+ entry_name: str,
61
+ claimed: set[int],
62
+ ) -> tuple[Optional[int], Optional[str]]:
63
+ """Pick the mailbox channel for an IPC entry, or explain why none.
64
+
65
+ 1. A channel whose `reserved_for:` equals the entry name wins.
66
+ 2. Otherwise the lowest-id still-unclaimed `reserved_for: app`
67
+ channel is handed out (each channel carries one carve-out).
68
+ 3. Otherwise no channel: the caller blocks the entry. Channel 0 is
69
+ `alp_default_rpmsg`'s and is never handed to an unrelated entry.
70
+ """
71
+ channels = list(mailbox.get("channels") or [])
72
+ for ch in channels:
73
+ if ch.get("reserved_for") == entry_name:
74
+ return int(ch["id"]), None
75
+ free = sorted(int(ch["id"]) for ch in channels
76
+ if ch.get("reserved_for") == "app"
77
+ and int(ch["id"]) not in claimed)
78
+ if free:
79
+ return free[0], None
80
+ named = sorted({str(ch.get("reserved_for")) for ch in channels})
81
+ return None, (
82
+ f"ipc entry '{entry_name}' has no mailbox channel: none is "
83
+ f"`reserved_for: {entry_name}` and no unclaimed `reserved_for: "
84
+ f"app` channel is left (reservations: {', '.join(named) or 'none'}). "
85
+ f"Rename the entry to a reserved name or add a channel to the "
86
+ f"SoM preset's mailbox.channels.")
87
+
88
+
89
+ def _region_ipc_eligibility(
90
+ region: dict[str, Any],
91
+ aperture: tuple[int, int],
92
+ is_preset_authored: bool,
93
+ ) -> tuple[bool, str]:
94
+ """alp-sdk#1365 split B (P1): is `region` a legal IPC carve-out target?
95
+
96
+ Called only once `aperture` has already resolved non-`None` -- the
97
+ caller (`_candidate_regions` below) special-cases `aperture is None`
98
+ itself, honouring the legacy `carveout:` flag verbatim so this whole
99
+ function is a no-op on every non-Alif SoM (V2N/V2M).
100
+
101
+ Eligible iff the region's derived class (`aperture.classify_region`)
102
+ is not `flash`, AND -- for a region the SoM preset authored itself --
103
+ its `write_authority` is `customer_runtime`. A DERIVED region (SoC-level
104
+ `memory_regions`, or the silicon-variant fallback) needs no authority:
105
+ it is RAM by construction.
106
+
107
+ `mram_main` (metadata/e1m_modules/E1M-AEN{301,...,803}, all seven
108
+ presets) is the reason this function exists: it lists an
109
+ `a32_cluster`/`m55_*` endpoint, carries NO `carveout` key at all, and
110
+ -- back when its `base` stayed `"TBD"` -- was the only thing
111
+ standing between an `ipc:` entry and a carve-out inside the live
112
+ ATOC band (0x80578000..0x80580000). #2053 resolved `base` to
113
+ `0x80000000` on every AEN preset, so its extent now equals the
114
+ aperture exactly (the whole-device alias) and `classify_region`
115
+ calls it `"flash"` outright, which disqualifies it unconditionally a
116
+ few lines below (independent of `write_authority` entirely) -- the
117
+ hazard is closed on both sides of that resolution: before #2053 its
118
+ class was `"unresolved"` and its authored `write_authority:
119
+ composite` (never `customer_runtime`) already disqualified it via
120
+ the `cls == "unresolved"` tail further down (still reachable for any
121
+ future preset that authors an unresolved `base`; pinned directly by
122
+ `python/tests/planner/test_carveout_aperture_ordering.py`'s
123
+ `TestUnresolvedLegOrdering` now that no shipped preset reaches it
124
+ end-to-end).
125
+
126
+ AGREE contract (alp-sdk#1365 split B review, BLOCKER): `carveout:` is
127
+ a LEGACY OVERRIDE that must AGREE with a resolvable derived class. It
128
+ is NOT a second vote that can silently win against `flash`/`ram`, nor
129
+ against the `write_authority`-derived answer on an `unclassified`
130
+ row -- a disagreement is a metadata bug, refused loudly, naming BOTH
131
+ the derived class (with the addresses that produced it) and the
132
+ authored flag. Proven regression this closes: a preset-authored row
133
+ OUTSIDE the aperture with `write_authority: customer_runtime` AND
134
+ `carveout: false` used to resolve `status: ok` on the write_authority
135
+ answer alone, silently dropping the author's explicit `carveout:
136
+ false` -- exactly the OSPI-XIP-row shape `classify_region`'s
137
+ `"unclassified"` docstring warns must never become an IPC candidate
138
+ just because it resolves outside the aperture.
139
+ """
140
+ name = region.get("name")
141
+ cls = classify_region(region, aperture, is_preset_authored)
142
+ carveout = region.get("carveout")
143
+
144
+ def _agree_or_refuse(
145
+ class_desc: str,
146
+ derived_eligible: bool,
147
+ ) -> Optional[tuple[bool, str]]:
148
+ """None when `carveout:` is absent or agrees; else the refusal."""
149
+ if carveout is None or bool(carveout) == derived_eligible:
150
+ return None
151
+ ext = region_extent(region)
152
+ where = f" [0x{ext[0]:x}, 0x{ext[1]:x})" if ext else ""
153
+ verdict = "eligible" if derived_eligible else "not eligible"
154
+ return False, (
155
+ f"region {name!r}{where} derives {class_desc} against the "
156
+ f"SoC's declared on-die MRAM aperture [0x{aperture[0]:x}, "
157
+ f"0x{aperture[1]:x}) -- {verdict} for an IPC carve-out -- but "
158
+ f"its authored `carveout: {carveout!r}` disagrees; fix the "
159
+ f"metadata, the derived class is authoritative, not a "
160
+ f"silently-honoured override")
161
+
162
+ if cls == "flash":
163
+ disagreement = _agree_or_refuse("flash-class", False)
164
+ if disagreement is not None:
165
+ return disagreement
166
+ ext = region_extent(region)
167
+ where = f" [0x{ext[0]:x}, 0x{ext[1]:x})" if ext else ""
168
+ return False, (
169
+ f"region {name!r}{where} is flash-class (contained in the "
170
+ f"SoC's declared on-die MRAM aperture) -- not safe as an IPC "
171
+ f"carve-out target")
172
+
173
+ if cls == "ram":
174
+ # #2010: likely UNREACHABLE on every SoM shipped today. `cls ==
175
+ # "ram"` requires `not is_preset_authored`, i.e. a row derived
176
+ # from the SoC's own `memory_regions:` rather than authored by
177
+ # the SoM preset -- but every Alif preset that resolves an
178
+ # aperture at all also authors an explicit `memory_map:` block
179
+ # (`is_preset_authored` is True for all of them), and no
180
+ # non-Alif SoM ever resolves an aperture in the first place
181
+ # (`aperture is None` short-circuits before this function is
182
+ # even called -- see `_candidate_regions()`). `git grep -n
183
+ # "carveout:" metadata/socs/` returns zero hits, confirming no
184
+ # SoC-derived row carries the flag either. Kept, not deleted: a
185
+ # future minimal Alif SoM port that relies entirely on SoC-level
186
+ # `memory_regions:` (no preset `memory_map:` override) would
187
+ # make `is_preset_authored` False while an aperture still
188
+ # resolves, reaching this leg for real. Pinned by direct call
189
+ # (`TestRamLegReachability`,
190
+ # tests/scripts/test_orchestrate_carveout_aperture_ordering.py)
191
+ # since no board.yaml in this tree can reach it end-to-end.
192
+ disagreement = _agree_or_refuse(
193
+ "ram-class (outside the aperture, not preset-authored)", True)
194
+ if disagreement is not None:
195
+ return disagreement
196
+ return True, ""
197
+
198
+ if cls == "unclassified":
199
+ wa = region.get("write_authority")
200
+ derived_eligible = wa == "customer_runtime"
201
+ disagreement = _agree_or_refuse(
202
+ f"an unclassified class outside the aperture "
203
+ f"(write_authority={wa!r})", derived_eligible)
204
+ if disagreement is not None:
205
+ return disagreement
206
+ if derived_eligible:
207
+ return True, ""
208
+ return False, (
209
+ f"region {name!r} resolves outside the declared MRAM "
210
+ f"aperture (containment doesn't classify it) and its "
211
+ f"authored write_authority is {wa!r}, not "
212
+ f"'customer_runtime'")
213
+
214
+ # cls == "unresolved": this region's OWN base doesn't resolve. (The
215
+ # caller never reaches here when the SoC declares no aperture at all
216
+ # -- that case is handled before this function is called.) Nothing
217
+ # to compare against, so honour whichever authored flag is present
218
+ # rather than guess: the legacy `carveout:` FIRST -- it is the
219
+ # conservative, pre-split-B signal, honoured VERBATIM as the fallback
220
+ # answer here -- then `write_authority` when no `carveout:` is
221
+ # authored, then refuse. (alp-sdk#1365 split B review, MAJOR 2: this
222
+ # used to check `write_authority` first, silently dropping an
223
+ # authored `carveout: false` whenever `write_authority:
224
+ # customer_runtime` was also present.)
225
+ if carveout is not None:
226
+ if carveout is False:
227
+ return False, (
228
+ f"region {name!r} has an unresolved base "
229
+ f"({region.get('base')!r}); honouring its legacy "
230
+ f"`carveout: false`")
231
+ return True, ""
232
+ wa = region.get("write_authority")
233
+ if wa is not None:
234
+ if wa == "customer_runtime":
235
+ return True, ""
236
+ return False, (
237
+ f"region {name!r} has an unresolved base "
238
+ f"({region.get('base')!r}) so containment against the "
239
+ f"declared MRAM aperture can't be verified, and its "
240
+ f"authored write_authority is {wa!r}, not "
241
+ f"'customer_runtime'")
242
+ return False, (
243
+ f"region {name!r} has an unresolved base "
244
+ f"({region.get('base')!r}) and carries neither "
245
+ f"`write_authority` nor a legacy `carveout` flag -- class "
246
+ f"unresolved, never guessed (ADR-0034 clause 4)")
247
+
248
+
249
+ def _blocked_carve_out(entry: IpcEntry, reason: str) -> ResolvedCarveOut:
250
+ """Project an IpcEntry into a blocked ResolvedCarveOut.
251
+
252
+ Used when SoM metadata isn't ready yet (TBD addresses, missing
253
+ mailbox controller) or the board.yaml entry can't be satisfied
254
+ (no region, collision, etc.). The manifest records the entry as
255
+ `status: blocked` + `reason: ...` so reviewers see the gap; the
256
+ actual slice-build step is what trips on it.
257
+ """
258
+ return ResolvedCarveOut(
259
+ name=entry.name,
260
+ kind=entry.kind,
261
+ endpoints=list(entry.endpoints),
262
+ base=0, size=0, region="",
263
+ cacheable=bool(entry.cacheable) if entry.cacheable is not None else False,
264
+ src_ept=0, dst_ept=0, mailbox_channel=0,
265
+ status="blocked", reason=reason,
266
+ )
267
+
268
+
269
+ def resolve_carve_outs(
270
+ project: BoardProject,
271
+ ) -> list[ResolvedCarveOut]:
272
+ """Spec §6.1 algorithm.
273
+
274
+ 1. Sort ipc entries alphabetically by name.
275
+ 2. For each entry, pick the first memory region whose
276
+ accessible_from: covers every endpoint and whose `cacheable:`
277
+ attribute matches the entry's preference (non-cacheable by
278
+ default; explicit `cacheable: true` flips the preference).
279
+ 3. Emit a `status: blocked` entry when the matching region has a
280
+ TBD base / size (the SoM isn't HW-mapped yet); the manifest
281
+ records the block reason and the actual slice-build step is
282
+ what fails.
283
+ 4. Allocate top-down within the region, page-aligned (4 KiB), and
284
+ only inside the sub-extent every endpoint can actually address
285
+ (`access_windows:`, #553). An explicit `ipc[].address:` is
286
+ placed FIRST, bounds-checked against that same window and
287
+ overlap-checked against every carve-out already placed, so two
288
+ channels can never resolve onto one physical address and an
289
+ address in no region is refused instead of being labelled with
290
+ one (#552).
291
+ 5. Endpoint IDs: FNV-1a of the entry name, low byte ORed with
292
+ 0x400 for src, +1 for dst. Collision check at emit time.
293
+
294
+ Phase 3 strict-channel-reservation enforcement (spec §6.4):
295
+ - If the SoM preset's `mailbox.controller` is `TBD`, every
296
+ rpmsg entry lands blocked with a hint pointing at the preset
297
+ that owes the value.
298
+ - If the controller is set but no channel is `reserved_for:
299
+ alp_default_rpmsg` (and any `ipc[].kind == rpmsg` entry is
300
+ present), the rpmsg entries land blocked so the customer
301
+ adds an explicit reservation rather than silently dropping
302
+ the channel to 0.
303
+ """
304
+ if not project.ipc:
305
+ return []
306
+
307
+ # Derive the effective memory-region table. An explicit `memory_map:`
308
+ # block in the SoM preset wins verbatim (non-stock partitioning); when
309
+ # absent the helper derives the table from the SoC variant JSON so the
310
+ # orchestrator doesn't need to duplicate that logic.
311
+ memory_map = resolve_memory_map(project.som_preset,
312
+ project.effective_metadata_root())
313
+ mailbox = dict(project.som_preset.get("mailbox") or {})
314
+
315
+ # alp-sdk#1365 split B (P1): the SoC's declared on-die flash aperture
316
+ # (None on every non-Alif SoM, or an Alif SoC/variant that hasn't
317
+ # declared one), and whether THIS SoM preset authored `memory_map:`
318
+ # itself -- both feed `_region_ipc_eligibility()` below. `memory_map`
319
+ # above is an all-or-nothing derivation (`resolve_memory_map`'s own
320
+ # precedence rule): every row in it is preset-authored, or none is.
321
+ aperture = resolve_aperture(
322
+ project.som_preset, project.effective_metadata_root())
323
+ is_preset_authored = bool(project.som_preset.get("memory_map"))
324
+
325
+ # Phase 3 strict mailbox checks (spec §6.4). Surfaces preset
326
+ # bugs before the user spends time on a build that would silently
327
+ # collide on mailbox channel 0. When metadata is incomplete, the
328
+ # rpmsg entries land blocked rather than aborting resolution
329
+ # (Phase 4 acceptance §6.1: emit a manifest, fail the build).
330
+ has_rpmsg_entry = any(e.kind == "rpmsg" for e in project.ipc)
331
+ rpmsg_block_reason: Optional[str] = None
332
+ if has_rpmsg_entry:
333
+ controller = mailbox.get("controller")
334
+ # Case/whitespace-insensitive: "tbd", "Tbd", " TBD " are all the
335
+ # same hand-typed placeholder as a bare "TBD" (alp-sdk #1048).
336
+ controller_is_tbd = (
337
+ isinstance(controller, str) and controller.strip().upper() == "TBD"
338
+ )
339
+ if controller is None or controller_is_tbd:
340
+ rpmsg_block_reason = (
341
+ f"SoM {project.sku} mailbox controller is "
342
+ f"{'unset' if controller is None else 'TBD'}; "
343
+ f"carve-out resolution requires authoritative mailbox "
344
+ f"metadata. Fill `mailbox.controller:` in "
345
+ f"metadata/e1m_modules/{project.sku}.yaml with the "
346
+ f"vendor mailbox node name (e.g. `renesas_mhu`, "
347
+ f"`alif_mhuv2`) or remove the rpmsg "
348
+ f"entries from board.yaml.")
349
+ else:
350
+ reserved_tags = {
351
+ ch.get("reserved_for")
352
+ for ch in (mailbox.get("channels") or [])
353
+ }
354
+ if "alp_default_rpmsg" not in reserved_tags:
355
+ rpmsg_block_reason = (
356
+ f"no mailbox channel reserved for alp_default_rpmsg "
357
+ f"in {project.sku}; add one with `reserved_for: "
358
+ f"alp_default_rpmsg` to metadata/e1m_modules/"
359
+ f"{project.sku}.yaml mailbox.channels (e.g. "
360
+ f"`- {{ id: 0, reserved_for: alp_default_rpmsg }}`).")
361
+
362
+ # Per-region high-water-mark allocator state. Initialised lazily
363
+ # the first time we touch a region; returns None when the region
364
+ # carries a TBD base or unresolvable size.
365
+ region_top: dict[str, int] = {}
366
+ # Every span already handed out in a region, INCLUDING the ones
367
+ # pinned by an explicit `ipc[].address:`. Without this two carve-outs
368
+ # resolve onto one physical window and both report `ok` (#552).
369
+ placed_spans: dict[str, list[tuple[int, int, str]]] = {}
370
+
371
+ def _region_top_init(region: dict[str, Any]) -> tuple[Optional[int], Optional[str]]:
372
+ name = region["name"]
373
+ if name in region_top:
374
+ return region_top[name], None
375
+ # A region derived from the SoC variant JSON (no explicit
376
+ # `memory_map:` in the preset) carries name/size but NO `base`
377
+ # until the SoM is HW-mapped. Treat a missing base the same as
378
+ # an explicit `TBD` so an un-mapped SoM (e.g. AEN801, whose E8
379
+ # SoC JSON has no per-region base yet) lands a clean *blocked*
380
+ # carve-out instead of crashing with KeyError: 'base'.
381
+ base = region.get("base")
382
+ size_bytes = _region_size_bytes(region)
383
+ base_is_unmapped = (
384
+ base is None
385
+ or (isinstance(base, str) and base.strip().upper() == "TBD")
386
+ )
387
+ if base_is_unmapped:
388
+ return None, (
389
+ f"memory_map.base is {'unset' if base is None else 'TBD'} "
390
+ f"for region '{name}' in SoM {project.sku}; this SoM "
391
+ f"hasn't been HW-mapped yet so IPC carve-outs cannot be "
392
+ f"allocated. Add a `memory_map:` block to "
393
+ f"metadata/e1m_modules/{project.sku}.yaml (or per-region "
394
+ f"`base`) or remove the matching ipc entry from board.yaml.")
395
+ if size_bytes is None:
396
+ return None, (
397
+ f"memory_map.size is unresolvable for region '{name}' "
398
+ f"in SoM {project.sku} (size_mib / size_kib unset or "
399
+ f"TBD). Cannot allocate carve-outs.")
400
+ # Top-down allocator: top = base + size, page-aligned.
401
+ top = base + size_bytes
402
+ top = _align_down(top, _PAGE)
403
+ region_top[name] = top
404
+ return top, None
405
+
406
+ def _endpoint_window(
407
+ region: dict[str, Any],
408
+ endpoint_set: set[str],
409
+ ) -> tuple[int, int]:
410
+ """Sub-extent of `region` that EVERY endpoint can actually address.
411
+
412
+ `accessible_from:` is a yes/no per core; it cannot say "this core
413
+ reaches only part of the region". The RZ/V2N Cortex-M33 does
414
+ exactly that: per Renesas FSP `bsp_slave_address.h` its DDR window
415
+ is CM33-secure `0x80000000` / CM33-non-secure `0x90000000` -> A55
416
+ `0x40000000`, **256 MiB**, while `ddr_main` spans 4 GiB from
417
+ `0x48000000`. A top-down allocation therefore handed the M33
418
+ `0x147f80000` -- a 33-bit address that truncates to `0x47f80000`,
419
+ below the DDR base, when cast to a pointer on the M33 (#553).
420
+
421
+ `access_windows:` (optional, per region, keyed by core id) declares
422
+ the reachable slice as an absolute `base` + `size_mib`/`size_kib` in
423
+ the same address space as the region's own `base`. A region that
424
+ declares none is reachable in full, so this returns the whole
425
+ extent and nothing changes for it.
426
+ """
427
+ lo = int(region["base"])
428
+ hi = lo + (_region_size_bytes(region) or 0)
429
+ windows = region.get("access_windows") or {}
430
+ if not isinstance(windows, dict):
431
+ return lo, hi
432
+ for core in sorted(endpoint_set):
433
+ win = windows.get(core)
434
+ if not isinstance(win, dict):
435
+ continue
436
+ win_lo = win.get("base")
437
+ win_size = _region_size_bytes(win)
438
+ if not isinstance(win_lo, int) or win_size is None:
439
+ continue
440
+ lo = max(lo, win_lo)
441
+ hi = min(hi, win_lo + win_size)
442
+ return lo, hi
443
+
444
+ def _first_free_down(
445
+ top: int,
446
+ size_aligned: int,
447
+ spans: list[tuple[int, int, str]],
448
+ floor: int,
449
+ ) -> int:
450
+ """First page-aligned base at or below `top` colliding with nothing.
451
+
452
+ The top-down allocator must allocate AROUND spans already pinned by
453
+ an explicit `ipc[].address:`, not merely notice the collision --
454
+ refusing instead of skipping would break the ordinary case (a
455
+ project that pins one channel and lets the rest float). Returns a
456
+ base below `floor` when the region has no room left; the caller
457
+ reports that as a blocked entry.
458
+ """
459
+ base = _align_down(top - size_aligned, _PAGE)
460
+ moved = True
461
+ while moved and base >= floor:
462
+ moved = False
463
+ for lo, hi, _ in sorted(spans, reverse=True):
464
+ if base < hi and lo < base + size_aligned:
465
+ base = _align_down(lo - size_aligned, _PAGE)
466
+ moved = True
467
+ return base
468
+
469
+ def _candidate_regions(
470
+ entry: IpcEntry,
471
+ ) -> tuple[list[dict[str, Any]], Optional[str]]:
472
+ """Regions this entry may land in, ranked; or a block reason."""
473
+ prefers_cacheable = (bool(entry.cacheable)
474
+ if entry.cacheable is not None else False)
475
+ endpoint_set = set(entry.endpoints)
476
+
477
+ # Filter candidates: accessibility covers every endpoint, and the
478
+ # region is eligible for IPC carve-out allocation.
479
+ #
480
+ # alp-sdk#1365 split B (P1): eligibility is now DERIVED against the
481
+ # SoC's declared on-die MRAM aperture (`_region_ipc_eligibility`)
482
+ # instead of trusting the legacy `carveout:` flag outright -- that
483
+ # flag is what previously left `mram_main` (no `carveout` key at
484
+ # all, base `TBD`) eligible by default, one `base:` fill away from
485
+ # resolving an `a32_cluster` carve-out inside the live ATOC band.
486
+ # When the SoC declares NO aperture (`aperture is None` -- every
487
+ # non-Alif SoM, or an Alif SoC/variant that omits `soc_flash_base`),
488
+ # this keeps the exact pre-split-B `carveout: false` filter so
489
+ # those SoMs resolve byte-identically.
490
+ candidates: list[dict[str, Any]] = []
491
+ excluded_names: list[str] = [] # aperture is None: legacy path
492
+ excluded_detail: list[str] = [] # aperture resolved: derived path
493
+ for region in memory_map:
494
+ af = set(region.get("accessible_from") or [])
495
+ if not endpoint_set.issubset(af):
496
+ continue
497
+ if aperture is None:
498
+ if region.get("carveout") is False:
499
+ excluded_names.append(region["name"])
500
+ continue
501
+ candidates.append(region)
502
+ continue
503
+ eligible, reason = _region_ipc_eligibility(
504
+ region, aperture, is_preset_authored)
505
+ if not eligible:
506
+ excluded_detail.append(f"{region['name']!r} ({reason})")
507
+ continue
508
+ candidates.append(region)
509
+ if not candidates:
510
+ if excluded_names:
511
+ return [], (
512
+ f"ipc entry '{entry.name}' endpoints {entry.endpoints} "
513
+ f"only match memory_map region(s) {excluded_names} in "
514
+ f"SoM {project.sku}, and all of them are marked "
515
+ f"`carveout: false` (flash-class, not safe as shared "
516
+ f"memory); add an allocatable region (`carveout: true` "
517
+ f"or omitted) to metadata/e1m_modules/{project.sku}.yaml "
518
+ f"or remove the matching ipc entry from board.yaml.")
519
+ if excluded_detail:
520
+ shown = excluded_detail[:_MAX_EXCLUDED_DETAIL]
521
+ omitted = len(excluded_detail) - len(shown)
522
+ more = (
523
+ f"; and {omitted} more region(s) also excluded (reasons "
524
+ f"omitted, not necessarily the same one -- list capped "
525
+ f"at {_MAX_EXCLUDED_DETAIL})"
526
+ if omitted > 0 else "")
527
+ return [], (
528
+ f"ipc entry '{entry.name}' endpoints {entry.endpoints} "
529
+ f"only match memory_map region(s) in SoM {project.sku} "
530
+ f"that are ineligible for an IPC carve-out: "
531
+ f"{'; '.join(shown)}{more}. Add a region whose "
532
+ f"resolved base sits outside the SoC's declared MRAM "
533
+ f"aperture (and, if the SoM preset authors the row "
534
+ f"itself, carries `write_authority: "
535
+ f"customer_runtime`) to "
536
+ f"metadata/e1m_modules/{project.sku}.yaml, or remove "
537
+ f"the matching ipc entry from board.yaml.")
538
+ return [], (
539
+ f"ipc entry '{entry.name}' endpoints {entry.endpoints} "
540
+ f"have no matching memory_map region in SoM "
541
+ f"{project.sku}")
542
+
543
+ # Prefer the region whose `cacheable:` flag matches the entry's
544
+ # preference. Default carve-out is non-cacheable.
545
+ def _rank(r: dict[str, Any]) -> tuple[int, int]:
546
+ cacheable_match = (bool(r.get("cacheable")) == prefers_cacheable)
547
+ # Smaller region size first (avoid eating the giant DDR
548
+ # region with tiny carve-outs when ocram fits).
549
+ size_b = _region_size_bytes(r) or 1 << 62
550
+ return (0 if cacheable_match else 1, size_b)
551
+
552
+ candidates.sort(key=_rank)
553
+ return candidates, None
554
+
555
+ def _carve_size_aligned(entry: IpcEntry) -> int:
556
+ carve_size = entry.carve_out_kb * 1024
557
+ return ((carve_size + _PAGE - 1) // _PAGE) * _PAGE
558
+
559
+ def _overlap(
560
+ region_name: str,
561
+ base: int,
562
+ size_aligned: int,
563
+ ) -> Optional[tuple[int, int, str]]:
564
+ top = base + size_aligned
565
+ for lo, hi, peer in placed_spans.get(region_name, []):
566
+ if not (top <= lo or base >= hi):
567
+ return (lo, hi, peer)
568
+ return None
569
+
570
+ def _place_pinned(
571
+ entry: IpcEntry,
572
+ ) -> tuple[Optional[dict[str, Any]], int, Optional[str]]:
573
+ """Honour an explicit `ipc[].address:` -- bounds- and overlap-checked."""
574
+ candidates, block_reason = _candidate_regions(entry)
575
+ if block_reason is not None:
576
+ return None, 0, block_reason
577
+
578
+ base = entry.address or 0
579
+ if base % _PAGE != 0:
580
+ return None, 0, (
581
+ f"ipc entry '{entry.name}' explicit address "
582
+ f"0x{base:x} is not page-aligned (4 KiB)")
583
+
584
+ size_aligned = _carve_size_aligned(entry)
585
+ endpoint_set = set(entry.endpoints)
586
+
587
+ # The pinned range decides the region -- the ranked-first region is
588
+ # only a PREFERENCE, and labelling a pin with it was how
589
+ # `0xdeadb000` came back tagged `region: ocram_low` (#552).
590
+ windows: list[str] = []
591
+ chosen: Optional[dict[str, Any]] = None
592
+ tbd_reason: Optional[str] = None
593
+ for region in candidates:
594
+ _top, region_block_reason = _region_top_init(region)
595
+ if region_block_reason is not None:
596
+ tbd_reason = tbd_reason or region_block_reason
597
+ continue
598
+ lo, hi = _endpoint_window(region, endpoint_set)
599
+ windows.append(f"{region['name']} 0x{lo:x}..0x{hi:x}")
600
+ if lo <= base and base + size_aligned <= hi:
601
+ chosen = region
602
+ break
603
+ if chosen is None:
604
+ if not windows:
605
+ return None, 0, (tbd_reason or (
606
+ f"ipc entry '{entry.name}' explicit address 0x{base:x} "
607
+ f"cannot be checked: no candidate region in SoM "
608
+ f"{project.sku} carries a resolvable base + size"))
609
+ return None, 0, (
610
+ f"ipc entry '{entry.name}' explicit address range "
611
+ f"0x{base:x}..0x{base + size_aligned:x} lies outside every "
612
+ f"memory_map region endpoints {entry.endpoints} can reach "
613
+ f"on SoM {project.sku} (reachable: {', '.join(windows)}); "
614
+ f"drop the `address:` override to let the allocator place "
615
+ f"it, or move it inside one of those windows.")
616
+
617
+ region_name = chosen["name"]
618
+ clash = _overlap(region_name, base, size_aligned)
619
+ if clash is not None:
620
+ return None, 0, (
621
+ f"ipc entry '{entry.name}' explicit address 0x{base:x} "
622
+ f"({entry.carve_out_kb} KiB) overlaps carve-out "
623
+ f"'{clash[2]}' already placed at 0x{clash[0]:x}..0x"
624
+ f"{clash[1]:x} in region '{region_name}'")
625
+
626
+ placed_spans.setdefault(region_name, []).append(
627
+ (base, base + size_aligned, entry.name))
628
+ # A pin does NOT move the bump allocator's high-water mark -- it may
629
+ # sit deliberately low -- but `placed_spans` now makes the allocator
630
+ # step over it.
631
+ return chosen, base, None
632
+
633
+ def _place_auto(
634
+ entry: IpcEntry,
635
+ ) -> tuple[Optional[dict[str, Any]], int, Optional[str]]:
636
+ """Allocate top-down inside the endpoint-reachable window."""
637
+ candidates, block_reason = _candidate_regions(entry)
638
+ if block_reason is not None:
639
+ return None, 0, block_reason
640
+
641
+ chosen = candidates[0]
642
+ region_name = chosen["name"]
643
+
644
+ # Initialise the region (and surface any TBD field). Blocked
645
+ # regions emit a blocked entry rather than aborting.
646
+ _top, region_block_reason = _region_top_init(chosen)
647
+ if region_block_reason is not None:
648
+ return None, 0, region_block_reason
649
+
650
+ size_aligned = _carve_size_aligned(entry)
651
+ window_lo, window_hi = _endpoint_window(chosen, set(entry.endpoints))
652
+ # Descend from whichever is lower: the region's remaining top, or
653
+ # the ceiling of the reachable window (#553).
654
+ start = _align_down(min(region_top[region_name], window_hi), _PAGE)
655
+ base = _first_free_down(
656
+ start, size_aligned, placed_spans.get(region_name, []), window_lo)
657
+ if base < window_lo:
658
+ region_lo = int(chosen["base"])
659
+ region_hi = region_lo + (_region_size_bytes(chosen) or 0)
660
+ window_note = ""
661
+ if (window_lo, window_hi) != (region_lo, region_hi):
662
+ window_note = (
663
+ f"; endpoints {entry.endpoints} reach only "
664
+ f"0x{window_lo:x}..0x{window_hi:x} of it "
665
+ f"(0x{region_lo:x}..0x{region_hi:x})")
666
+ return None, 0, (
667
+ f"ipc entry '{entry.name}' ({entry.carve_out_kb} "
668
+ f"KiB) doesn't fit in region '{region_name}' "
669
+ f"after prior allocations{window_note}")
670
+
671
+ region_top[region_name] = base
672
+ placed_spans.setdefault(region_name, []).append(
673
+ (base, base + size_aligned, entry.name))
674
+ return chosen, base, None
675
+
676
+ # Sort entries alphabetically by name for determinism.
677
+ sorted_entries = sorted(project.ipc, key=lambda e: e.name)
678
+
679
+ # Placement runs in TWO passes: explicit `address:` pins first, then
680
+ # the bump allocator around them. A single name-sorted pass let an
681
+ # auto-allocated `a_chan` take `0x80000` before the pinned `b_chan`
682
+ # asked for it, and both came back `ok` on the same address (#552).
683
+ # Emission below still walks `sorted_entries`, so ordering -- and the
684
+ # endpoint-id collision report -- is unchanged. Keyed by INDEX, not
685
+ # name: board.schema.json does not require `ipc[].name` to be unique
686
+ # (a duplicate is caught by the endpoint-id check further down), and a
687
+ # name-keyed map would silently give the twins one placement.
688
+ placeable = [(i, e) for i, e in enumerate(sorted_entries)
689
+ if not (e.kind == "rpmsg" and rpmsg_block_reason is not None)]
690
+ placement: dict[int, tuple[Optional[dict[str, Any]], int, Optional[str]]] = {}
691
+ for index, entry in placeable:
692
+ if entry.address is not None:
693
+ placement[index] = _place_pinned(entry)
694
+ for index, entry in placeable:
695
+ if entry.address is None:
696
+ placement[index] = _place_auto(entry)
697
+
698
+ resolved: list[ResolvedCarveOut] = []
699
+ # Channels reserved by exact entry name are claimed up front so an
700
+ # earlier-sorted unnamed entry can never take one of them.
701
+ claimed_channels: set[int] = {
702
+ int(ch["id"]) for ch in (mailbox.get("channels") or [])
703
+ if ch.get("reserved_for") in {e.name for e in sorted_entries}}
704
+ seen_low_bytes: dict[int, str] = {}
705
+
706
+ for index, entry in enumerate(sorted_entries):
707
+ # Mailbox metadata blocked? rpmsg entries can't proceed.
708
+ if entry.kind == "rpmsg" and rpmsg_block_reason is not None:
709
+ resolved.append(_blocked_carve_out(entry, rpmsg_block_reason))
710
+ continue
711
+
712
+ chosen, base, place_reason = placement[index]
713
+ if place_reason is not None or chosen is None:
714
+ resolved.append(_blocked_carve_out(
715
+ entry, place_reason or "carve-out could not be placed"))
716
+ continue
717
+ region_name = chosen["name"]
718
+ carve_size_aligned = _carve_size_aligned(entry)
719
+
720
+ # Endpoint ID derivation.
721
+ h = _fnv1a_32(entry.name.encode("utf-8"))
722
+ low = h & 0x0FF
723
+ if low in seen_low_bytes:
724
+ resolved.append(_blocked_carve_out(entry, (
725
+ f"ipc entry '{entry.name}' endpoint-id low byte "
726
+ f"0x{low:02x} collides with prior entry "
727
+ f"'{seen_low_bytes[low]}'. Rename one of the channels.")))
728
+ continue
729
+ seen_low_bytes[low] = entry.name
730
+ src_ept = 0x400 | low
731
+ dst_ept = src_ept + 1
732
+
733
+ mbox, mbox_reason = _resolve_mailbox_channel(
734
+ mailbox, entry.name, claimed_channels)
735
+ if mbox is None:
736
+ resolved.append(_blocked_carve_out(entry, mbox_reason or ""))
737
+ continue
738
+ claimed_channels.add(mbox)
739
+
740
+ resolved.append(ResolvedCarveOut(
741
+ name=entry.name,
742
+ kind=entry.kind,
743
+ endpoints=list(entry.endpoints),
744
+ base=base,
745
+ size=carve_size_aligned,
746
+ region=region_name,
747
+ cacheable=bool(chosen.get("cacheable", False))
748
+ if entry.cacheable is None
749
+ else bool(entry.cacheable),
750
+ src_ept=src_ept,
751
+ dst_ept=dst_ept,
752
+ mailbox_channel=mbox,
753
+ ))
754
+
755
+ return resolved