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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
tan/planner/kconfig.py
ADDED
|
@@ -0,0 +1,2435 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Per-slice Kconfig (alp.conf) emitter.
|
|
4
|
+
|
|
5
|
+
`_slice_alp_conf` renders a Zephyr slice's Kconfig fragment off the parsed model
|
|
6
|
+
+ the resolved capabilities / chip-driver / library / peripheral enables;
|
|
7
|
+
`_emit_extra_library_profile` renders an extra `libraries:` profile fragment.
|
|
8
|
+
Extracted as the #285 kconfig emit seam. The slug / peripheral-Kconfig /
|
|
9
|
+
chip-subsystem tables come from the slugs.py leaf and the per-library
|
|
10
|
+
HW-accelerator matcher from the libraries.py layer -- both plain module-scope
|
|
11
|
+
imports now. In alp-sdk they were lazy in-body imports of `alp_project` /
|
|
12
|
+
`alp_project_emit` (a cycle-break there, since `alp_project` imported this
|
|
13
|
+
module), and after the relocation they were the last thing making an emit from
|
|
14
|
+
`tan` load alp-sdk's Python at all. Per-core `libraries:` Kconfig is read from
|
|
15
|
+
each library's manifest via `_per_core_library_kconfig`.
|
|
16
|
+
|
|
17
|
+
`_slice_alp_conf` itself is a thin composer over a set of `_emit_*` section
|
|
18
|
+
helpers (console / SoM capability / chip driver / subsystem / library /
|
|
19
|
+
inference / cross-core shmem cache / memory / power / storage / OTA /
|
|
20
|
+
diagnostics), each returning that section's `list[str]` fragment. Split out
|
|
21
|
+
mechanically as the #673 Phase-1 kconfig split -- behavior-preserving,
|
|
22
|
+
byte-identical emit output; see `check_emit_snapshots.py`.
|
|
23
|
+
|
|
24
|
+
This module is a pure string templater: it never runs kconfiglib/west, so
|
|
25
|
+
every emitted `CONFIG_*` line must be provable correct from this module's own
|
|
26
|
+
YAML/JSON inputs + its own `zephyr/kconfigs/*.kconfig` tree -- it cannot ask
|
|
27
|
+
the real Kconfig solver whether a symbol's dependency chain resolves.
|
|
28
|
+
Consequently the emitted alp.conf's Kconfig symbol set is coupled to the
|
|
29
|
+
alp-sdk module version that generated it, the same way `west.yml` deliberately
|
|
30
|
+
pins one Zephyr/module release at a time (docs/zephyr-version-policy.md); an
|
|
31
|
+
alp.conf generated by one alp-sdk version is not a promised-compatible input
|
|
32
|
+
to a Zephyr tree built against another. Detecting that cross-version skew
|
|
33
|
+
cleanly (rather than a Kconfig "assign to undefined symbol" abort) is #855's
|
|
34
|
+
job, not this module's -- this module only owns intra-version dependency
|
|
35
|
+
gating (see #874).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import re
|
|
41
|
+
from functools import lru_cache
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any, Optional
|
|
44
|
+
|
|
45
|
+
import yaml
|
|
46
|
+
|
|
47
|
+
from . import cameras as _cameras
|
|
48
|
+
from . import libraries as _library_layer
|
|
49
|
+
from . import sdk_compat as _sdk_compat
|
|
50
|
+
from .link_target import applies_to as link_applies_to
|
|
51
|
+
from .loader import _library_alias_table
|
|
52
|
+
from .models import BoardProject, OrchestratorError, Slice
|
|
53
|
+
from .ownership import project_m33_overlay
|
|
54
|
+
from .paths import REPO
|
|
55
|
+
from .partition import resolve_storage_partitions
|
|
56
|
+
from .slugs import (
|
|
57
|
+
_BLOCK_SLUGS,
|
|
58
|
+
_CHIP_SUBSYSTEMS,
|
|
59
|
+
_board_define_slug,
|
|
60
|
+
_is_tbd,
|
|
61
|
+
_slugs_from_helper_firmware,
|
|
62
|
+
_slugs_from_on_module,
|
|
63
|
+
_som_define_slug,
|
|
64
|
+
peripheral_kconfig,
|
|
65
|
+
)
|
|
66
|
+
from .som_metadata import (
|
|
67
|
+
resolve_capabilities,
|
|
68
|
+
silicon_to_kconfig,
|
|
69
|
+
som_unpopulated_capabilities,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _emit_extra_library_profile(
|
|
74
|
+
name: str,
|
|
75
|
+
profile_rel: str,
|
|
76
|
+
project: "BoardProject",
|
|
77
|
+
) -> list[str]:
|
|
78
|
+
"""Walk an extra_libraries `profile:` file and emit the per-class
|
|
79
|
+
first-match Kconfig lines for the active SoM.
|
|
80
|
+
|
|
81
|
+
Mirrors the curated-library `integration.zephyr.hw_backends` matcher
|
|
82
|
+
(folded into each `metadata/libraries/<lib>.yaml` manifest): each
|
|
83
|
+
accelerator class declares a `priority:` list whose entries carry
|
|
84
|
+
`silicon:` / `soc_family:` / `requires_cap:` matchers plus a
|
|
85
|
+
`kconfig:` directive. Entries marked `status: planned` or
|
|
86
|
+
`status: stub` are metadata only and are not emitted as active
|
|
87
|
+
build claims. First matching implemented entry per class wins; an
|
|
88
|
+
`sw_fallback:` block with `kconfig:` is always emitted at the end
|
|
89
|
+
so the build has a SW floor when no HW backend matches.
|
|
90
|
+
|
|
91
|
+
Returns the list of emitted lines (each line is a complete
|
|
92
|
+
`CONFIG_*=y` or `# ...` comment); empty list when the profile
|
|
93
|
+
parses but nothing matches and there's no sw_fallback.
|
|
94
|
+
|
|
95
|
+
Failures (malformed YAML, missing keys) emit a single `#`-prefixed
|
|
96
|
+
diagnostic comment so the customer sees the failure in the slice's
|
|
97
|
+
alp.conf rather than silent drop-out.
|
|
98
|
+
"""
|
|
99
|
+
# No longer a divergence: alp-sdk#1961 landed as alp-sdk PR #2005 and
|
|
100
|
+
# this repo's #1241 re-sync (to alp-sdk 15b2f32c) converged the two
|
|
101
|
+
# copies, so the RELOCATED marker that stood here is retired with
|
|
102
|
+
# tan-cli#1122. Upstream now spells these three lines identically --
|
|
103
|
+
# `.resolve()` dropped (nothing downstream needs a real path, only the
|
|
104
|
+
# read does) and `UnicodeDecodeError` in the clause, because it is a
|
|
105
|
+
# `ValueError` not an `OSError` and `read_text(encoding="utf-8")` would
|
|
106
|
+
# otherwise escape a function contracted never to raise.
|
|
107
|
+
try:
|
|
108
|
+
profile_path = REPO / profile_rel
|
|
109
|
+
doc = yaml.safe_load(profile_path.read_text(encoding="utf-8"))
|
|
110
|
+
except (OSError, UnicodeDecodeError, yaml.YAMLError) as e:
|
|
111
|
+
return [f"# extra_libraries[{name}] profile parse failed: {e}"]
|
|
112
|
+
if not isinstance(doc, dict):
|
|
113
|
+
return [f"# extra_libraries[{name}] profile is not a mapping"]
|
|
114
|
+
|
|
115
|
+
# Match keys come from the active SoM. Mirror
|
|
116
|
+
# libraries._SOC_FAMILY_TOKEN so extra_libraries entries can
|
|
117
|
+
# share existing curated-profile match keys when desired.
|
|
118
|
+
silicon_ref = project.som_preset.get("silicon") or ""
|
|
119
|
+
family = (project.som_preset.get("family") or "").lower()
|
|
120
|
+
soc_family_token: Optional[str] = None
|
|
121
|
+
if family.startswith("alif-ensemble"):
|
|
122
|
+
soc_family_token = "alif_ensemble"
|
|
123
|
+
elif family.startswith("renesas-rzv2n"):
|
|
124
|
+
soc_family_token = "renesas_rzv2n"
|
|
125
|
+
|
|
126
|
+
# resolve_capabilities merges SoC-JSON defaults + SoM overrides.
|
|
127
|
+
capabilities = resolve_capabilities(project.som_preset, project.effective_metadata_root())
|
|
128
|
+
|
|
129
|
+
def _cap_truthy(cap_name: str) -> bool:
|
|
130
|
+
v = capabilities.get(cap_name)
|
|
131
|
+
if v is None:
|
|
132
|
+
return False
|
|
133
|
+
if isinstance(v, bool):
|
|
134
|
+
return v
|
|
135
|
+
if isinstance(v, int):
|
|
136
|
+
return v > 0
|
|
137
|
+
s = str(v).lower()
|
|
138
|
+
if s in ("true", "yes"):
|
|
139
|
+
return True
|
|
140
|
+
if s in ("false", "no", "null", "none", "0", ""):
|
|
141
|
+
return False
|
|
142
|
+
try:
|
|
143
|
+
return int(s) > 0
|
|
144
|
+
except ValueError:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def _entry_matches(e: dict[str, Any]) -> bool:
|
|
148
|
+
status = str(e.get("status", "implemented")).strip().lower()
|
|
149
|
+
if status in {"planned", "stub"}:
|
|
150
|
+
return False
|
|
151
|
+
sili = e.get("silicon")
|
|
152
|
+
sf = e.get("soc_family")
|
|
153
|
+
cap = e.get("requires_cap")
|
|
154
|
+
if sili is not None and sili != silicon_ref:
|
|
155
|
+
return False
|
|
156
|
+
if sf is not None and sf != soc_family_token:
|
|
157
|
+
return False
|
|
158
|
+
if cap is not None and not _cap_truthy(str(cap)):
|
|
159
|
+
return False
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
out: list[str] = []
|
|
163
|
+
accelerators = doc.get("accelerators") or []
|
|
164
|
+
if isinstance(accelerators, list):
|
|
165
|
+
for cls in accelerators:
|
|
166
|
+
if not isinstance(cls, dict):
|
|
167
|
+
continue
|
|
168
|
+
cls_name = cls.get("class") or "<unknown>"
|
|
169
|
+
for p in (cls.get("priority") or []):
|
|
170
|
+
if not isinstance(p, dict):
|
|
171
|
+
continue
|
|
172
|
+
if not _entry_matches(p):
|
|
173
|
+
continue
|
|
174
|
+
kc = p.get("kconfig")
|
|
175
|
+
if isinstance(kc, str) and kc:
|
|
176
|
+
out.append(f"{kc} # {name} / {cls_name}")
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
sw = doc.get("sw_fallback")
|
|
180
|
+
if isinstance(sw, dict):
|
|
181
|
+
kc = sw.get("kconfig")
|
|
182
|
+
if isinstance(kc, str) and kc:
|
|
183
|
+
out.append(f"{kc} # {name} / sw_fallback")
|
|
184
|
+
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# --------------------------------------------------------------------
|
|
189
|
+
# Console backend auto-selection (issue: generic console).
|
|
190
|
+
#
|
|
191
|
+
# The Alp SDK is a generic, multi-OS SoM layer: the same example can be
|
|
192
|
+
# built for a Zephyr core (Cortex-M) or a Linux core (Cortex-A / Yocto),
|
|
193
|
+
# and the console backend that "printf lands on" differs per OS. Rather
|
|
194
|
+
# than force every app author to hand-pick console Kconfig (the old AEN
|
|
195
|
+
# bench examples hard-coded a RAM console, which then fought the board's
|
|
196
|
+
# real UART console), the generator selects it automatically from the
|
|
197
|
+
# slice's `os:` and lets the SoM manufacturer / customer override via a
|
|
198
|
+
# single `diagnostics.console:` knob in board.yaml.
|
|
199
|
+
#
|
|
200
|
+
# auto (default) -> os: zephyr => alp (UART console)
|
|
201
|
+
# os: yocto/ubuntu => linux (stdio/tty console)
|
|
202
|
+
# other => none
|
|
203
|
+
# alp | uart -> Zephyr UART console (the module's console UART,
|
|
204
|
+
# e.g. E1M edge UART0; the board layer owns the
|
|
205
|
+
# `zephyr,console` DT chosen + pinmux).
|
|
206
|
+
# ram -> Zephyr RAM console (bench flow: read the buffer
|
|
207
|
+
# over SWD when no serial line is available).
|
|
208
|
+
# linux -> Linux console (nothing to emit on the Zephyr side;
|
|
209
|
+
# meaningful only on a yocto slice).
|
|
210
|
+
# none | off -> emit nothing; inherit whatever the board provides.
|
|
211
|
+
# --------------------------------------------------------------------
|
|
212
|
+
_CONSOLE_ALIASES = {
|
|
213
|
+
"alp": "uart",
|
|
214
|
+
"uart": "uart",
|
|
215
|
+
"serial": "uart",
|
|
216
|
+
"ram": "ram",
|
|
217
|
+
"swd": "ram",
|
|
218
|
+
"linux": "linux",
|
|
219
|
+
"stdio": "linux",
|
|
220
|
+
"none": "none",
|
|
221
|
+
"off": "none",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _resolve_console(value: Optional[str], os_: str,
|
|
226
|
+
hw_console: bool = True) -> str:
|
|
227
|
+
"""Resolve the effective console backend for a slice.
|
|
228
|
+
|
|
229
|
+
``value`` is the raw ``diagnostics.console:`` knob (or ``None`` /
|
|
230
|
+
``"auto"`` for the OS-derived default); ``os_`` is the slice OS.
|
|
231
|
+
``hw_console`` is the SoM-topology fact (``topology.<id>.hw_console``):
|
|
232
|
+
a headless core (``False``) has no hardware UART console, so the
|
|
233
|
+
AUTO-resolved Zephyr default must be ``"none"`` (inherit the board
|
|
234
|
+
default) instead of ``"uart"`` -- emitting ``CONFIG_UART_CONSOLE=y``
|
|
235
|
+
on a core whose board has no serial driver is an "assigned y but got n"
|
|
236
|
+
Kconfig error, fatal to the Zephyr build (issue #717). An explicit
|
|
237
|
+
``diagnostics.console:`` still wins for a headless core (the caller may
|
|
238
|
+
upgrade a headless AUTO to ``"ram"`` via ``diagnostics.sim_console``,
|
|
239
|
+
#686). Returns one of ``"uart"``, ``"ram"``, ``"linux"``, ``"none"``.
|
|
240
|
+
"""
|
|
241
|
+
v = (value or "auto").strip().lower()
|
|
242
|
+
if v == "auto":
|
|
243
|
+
if os_ == "zephyr":
|
|
244
|
+
return "uart" if hw_console else "none"
|
|
245
|
+
if os_ in ("yocto", "ubuntu"):
|
|
246
|
+
return "linux"
|
|
247
|
+
return "none"
|
|
248
|
+
return _CONSOLE_ALIASES.get(v, "none")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
#: Floor for `CONFIG_RAM_CONSOLE_BUFFER_SIZE` when `diagnostics.console: ram`
|
|
252
|
+
#: selects the RAM console. An app that sets a larger size in its own
|
|
253
|
+
#: `prj.conf` keeps it (see `_app_ram_console_size`). Only `prj.conf` is
|
|
254
|
+
#: read: a size set in `boards/<board>.conf`, `prj_<board>.conf` or an app
|
|
255
|
+
#: `EXTRA_CONF_FILE` is not seen and still loses to this floor.
|
|
256
|
+
_RAM_CONSOLE_MIN_SIZE = 2048
|
|
257
|
+
|
|
258
|
+
_RAM_CONSOLE_SIZE_RE = re.compile(
|
|
259
|
+
r"^\s*CONFIG_RAM_CONSOLE_BUFFER_SIZE\s*=\s*(0[xX][0-9a-fA-F]+|\d+)\s*(?:#.*)?$")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _app_ram_console_size(project: BoardProject, slice_: Slice) -> int:
|
|
263
|
+
"""`CONFIG_RAM_CONSOLE_BUFFER_SIZE` the slice's own `prj.conf` sets, or 0.
|
|
264
|
+
|
|
265
|
+
The generated alp.conf is merged AFTER prj.conf, so a bare assignment
|
|
266
|
+
would clobber (and shrink) an app-set size -- the app's console then
|
|
267
|
+
wraps (tan-cli#1401). Best effort: no source dir, no app, or an
|
|
268
|
+
unreadable prj.conf all read as "app sets nothing". Limit: only
|
|
269
|
+
`prj.conf` is read, so sizes set in `boards/<board>.conf`,
|
|
270
|
+
`prj_<board>.conf` or an app `EXTRA_CONF_FILE` still lose.
|
|
271
|
+
"""
|
|
272
|
+
if project.source_dir is None or not slice_.app:
|
|
273
|
+
return 0
|
|
274
|
+
from .orchestrator import _zephyr_app_dir # lazy: orchestrator imports us
|
|
275
|
+
try:
|
|
276
|
+
prj = _zephyr_app_dir(slice_.app, project.source_dir) / "prj.conf"
|
|
277
|
+
text = prj.read_text(encoding="utf-8")
|
|
278
|
+
except (OSError, OrchestratorError, UnicodeDecodeError):
|
|
279
|
+
return 0
|
|
280
|
+
size = 0
|
|
281
|
+
for line in text.splitlines():
|
|
282
|
+
m = _RAM_CONSOLE_SIZE_RE.match(line)
|
|
283
|
+
if m:
|
|
284
|
+
# Last assignment wins, like Kconfig. Kconfig reads an int symbol
|
|
285
|
+
# as hex with a 0x prefix, else base 10 -- `016384` is 16384,
|
|
286
|
+
# where int(x, 0) would raise ValueError.
|
|
287
|
+
tok = m.group(1)
|
|
288
|
+
size = int(tok, 16) if tok[:2] in ("0x", "0X") else int(tok, 10)
|
|
289
|
+
return size
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _emit_zephyr_console(console: str, sim_console: bool = False,
|
|
293
|
+
ram_size: int = _RAM_CONSOLE_MIN_SIZE) -> list[str]:
|
|
294
|
+
"""Kconfig lines for a Zephyr slice's resolved console backend.
|
|
295
|
+
|
|
296
|
+
``sim_console`` distinguishes a RAM console selected explicitly via
|
|
297
|
+
``diagnostics.console: ram`` (bench SWD flow) from one auto-selected
|
|
298
|
+
for a headless core by ``diagnostics.sim_console: true`` (studio sim
|
|
299
|
+
streams the ring over ``uart_socket``); the emitted Kconfig is
|
|
300
|
+
identical, only the teaching comment differs.
|
|
301
|
+
"""
|
|
302
|
+
if console == "uart":
|
|
303
|
+
return [
|
|
304
|
+
"# Console: Alp UART console (auto-selected for os: zephyr).",
|
|
305
|
+
"# printf/LOG land on the module's console UART (the board layer",
|
|
306
|
+
"# owns the `zephyr,console` DT chosen + pinmux -- e.g. E1M edge",
|
|
307
|
+
"# UART0). Override in board.yaml with `diagnostics.console: ram`",
|
|
308
|
+
"# (SWD bench flow) or `none` (inherit the board default).",
|
|
309
|
+
"CONFIG_SERIAL=y",
|
|
310
|
+
"CONFIG_CONSOLE=y",
|
|
311
|
+
"CONFIG_UART_CONSOLE=y",
|
|
312
|
+
"CONFIG_UART_INTERRUPT_DRIVEN=y",
|
|
313
|
+
"",
|
|
314
|
+
]
|
|
315
|
+
if console == "ram":
|
|
316
|
+
header = (
|
|
317
|
+
[
|
|
318
|
+
"# Console: RAM console (board.yaml `diagnostics.sim_console: "
|
|
319
|
+
"true`).",
|
|
320
|
+
"# This core has no hardware UART console -- the studio",
|
|
321
|
+
"# simulator streams `ram_console_buf` over its `uart_socket`.",
|
|
322
|
+
]
|
|
323
|
+
if sim_console
|
|
324
|
+
else [
|
|
325
|
+
"# Console: RAM console (board.yaml `diagnostics.console: "
|
|
326
|
+
"ram`).",
|
|
327
|
+
"# No serial line on this bench -- read `ram_console_buf` "
|
|
328
|
+
"over SWD.",
|
|
329
|
+
]
|
|
330
|
+
)
|
|
331
|
+
return header + [
|
|
332
|
+
"# LOG is routed to printk so the RAM backend still captures it.",
|
|
333
|
+
"CONFIG_CONSOLE=y",
|
|
334
|
+
"CONFIG_RAM_CONSOLE=y",
|
|
335
|
+
f"CONFIG_RAM_CONSOLE_BUFFER_SIZE={ram_size}",
|
|
336
|
+
"CONFIG_UART_CONSOLE=n",
|
|
337
|
+
"",
|
|
338
|
+
]
|
|
339
|
+
# "linux" on a Zephyr slice is meaningless, and "none" means inherit
|
|
340
|
+
# the board default -- emit nothing in both cases.
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
_LINUX_WIRELESS_PROVIDER_PREFIXES = ("murata_", "cyw")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _wireless_provider(project: BoardProject) -> Optional[str]:
|
|
348
|
+
"""Return the SoM's on-module Wi-Fi/BLE provider slug, if known."""
|
|
349
|
+
om = project.som_preset.get("on_module") or {}
|
|
350
|
+
provider = om.get("wifi_ble")
|
|
351
|
+
if not isinstance(provider, str):
|
|
352
|
+
return None
|
|
353
|
+
provider = provider.strip()
|
|
354
|
+
# Case-insensitive: "tbd"/"Tbd" are the same hand-typed placeholder as
|
|
355
|
+
# a bare "TBD" (alp-sdk #1048); `provider` is already whitespace-
|
|
356
|
+
# stripped above.
|
|
357
|
+
if not provider or provider.upper() == "TBD":
|
|
358
|
+
return None
|
|
359
|
+
return provider
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _is_linux_wireless_provider(provider: Optional[str]) -> bool:
|
|
363
|
+
"""True when Wi-Fi/BLE data paths are owned by the Linux BSP stack."""
|
|
364
|
+
if provider is None:
|
|
365
|
+
return False
|
|
366
|
+
return provider.startswith(_LINUX_WIRELESS_PROVIDER_PREFIXES)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _zephyr_iot_kconfig(
|
|
370
|
+
project: BoardProject,
|
|
371
|
+
slice_: Slice,
|
|
372
|
+
resolved_chip_state: dict[str, bool],
|
|
373
|
+
) -> list[str]:
|
|
374
|
+
"""Kconfig lines for ``cores.<id>.iot`` on a Zephyr slice.
|
|
375
|
+
|
|
376
|
+
``resolved_chip_state`` is the same som -> board-populated -> project-chips
|
|
377
|
+
resolved on/off map ``_emit_chips`` prints from (see
|
|
378
|
+
``_resolve_chip_states``) -- the CC3501E bridge backend
|
|
379
|
+
(``ALP_SDK_WIFI_CC3501E`` / ``ALP_SDK_BLE_CC3501E``) ``depends on
|
|
380
|
+
ALP_SDK_CHIP_CC3501E``, so a board variant that turns the chip off via
|
|
381
|
+
``populated: { cc3501e: false }`` must not also get a Wi-Fi/BLE
|
|
382
|
+
``=y`` line that silently resolves to n (issue #874).
|
|
383
|
+
"""
|
|
384
|
+
iot = slice_.iot or {}
|
|
385
|
+
if not any(iot.get(k) for k in ("wifi", "mqtt", "tls", "ble")):
|
|
386
|
+
return []
|
|
387
|
+
|
|
388
|
+
provider = _wireless_provider(project)
|
|
389
|
+
lines: list[str] = [
|
|
390
|
+
f"# IoT features declared on core `{slice_.core_id}` "
|
|
391
|
+
"(board.yaml `cores.*.iot`)",
|
|
392
|
+
]
|
|
393
|
+
if provider:
|
|
394
|
+
lines.append(
|
|
395
|
+
f"# Wireless provider resolved from `{project.sku}` "
|
|
396
|
+
f"on_module.wifi_ble: {provider}.")
|
|
397
|
+
else:
|
|
398
|
+
lines.append("# Wireless provider is TBD/absent; using generic Zephyr stack gates.")
|
|
399
|
+
|
|
400
|
+
emitted: set[str] = set()
|
|
401
|
+
|
|
402
|
+
def add(line: str) -> None:
|
|
403
|
+
if line.startswith("CONFIG_"):
|
|
404
|
+
if line in emitted:
|
|
405
|
+
return
|
|
406
|
+
emitted.add(line)
|
|
407
|
+
lines.append(line)
|
|
408
|
+
|
|
409
|
+
def add_network_base() -> None:
|
|
410
|
+
add("CONFIG_NETWORKING=y")
|
|
411
|
+
add("CONFIG_NET_IPV4=y")
|
|
412
|
+
add("CONFIG_NET_SOCKETS=y")
|
|
413
|
+
|
|
414
|
+
linux_owned = _is_linux_wireless_provider(provider)
|
|
415
|
+
|
|
416
|
+
if iot.get("wifi"):
|
|
417
|
+
if provider == "cc3501e":
|
|
418
|
+
if resolved_chip_state.get("cc3501e"):
|
|
419
|
+
add("# Wi-Fi: AEN CC3501E bridge backend, not Zephyr wifi_mgmt.")
|
|
420
|
+
add("CONFIG_ALP_SDK_WIFI_CC3501E=y")
|
|
421
|
+
else:
|
|
422
|
+
add("# Wi-Fi: SoM wireless provider is CC3501E, but this board")
|
|
423
|
+
add("# does not populate the chip (board.yaml `populated.cc3501e: "
|
|
424
|
+
"false`);")
|
|
425
|
+
add("# no Wi-Fi backend is emitted for this slice.")
|
|
426
|
+
elif linux_owned:
|
|
427
|
+
add("# Wi-Fi: provider data path is Linux/BSP-owned on this SoM;")
|
|
428
|
+
add("# no Zephyr wifi_mgmt backend is emitted for this slice.")
|
|
429
|
+
else:
|
|
430
|
+
add("# Wi-Fi: generic Zephyr wifi_mgmt station path.")
|
|
431
|
+
add_network_base()
|
|
432
|
+
add("CONFIG_WIFI=y")
|
|
433
|
+
add("CONFIG_NET_MGMT=y")
|
|
434
|
+
add("CONFIG_NET_MGMT_EVENT=y")
|
|
435
|
+
add("CONFIG_NET_L2_WIFI_MGMT=y")
|
|
436
|
+
add("CONFIG_ALP_SDK_IOT_WIFI=y")
|
|
437
|
+
|
|
438
|
+
if iot.get("mqtt"):
|
|
439
|
+
add("# MQTT: Zephyr mqtt_client over the active socket provider.")
|
|
440
|
+
add_network_base()
|
|
441
|
+
add("CONFIG_NET_TCP=y")
|
|
442
|
+
add("CONFIG_MQTT_LIB=y")
|
|
443
|
+
add("CONFIG_ALP_SDK_IOT_MQTT=y")
|
|
444
|
+
|
|
445
|
+
if iot.get("tls"):
|
|
446
|
+
add("# TLS: credential store + TLS-capable protocol clients.")
|
|
447
|
+
# TLS_CREDENTIALS needs the networking base even when neither
|
|
448
|
+
# `wifi:` nor `mqtt:` is also set on this slice -- omitting it left
|
|
449
|
+
# CONFIG_TLS_CREDENTIALS=y silently resolving to n (unmet
|
|
450
|
+
# NETWORKING/NET_SOCKETS deps), issue #874. add_network_base()
|
|
451
|
+
# dedupes against the wifi/mqtt branches above.
|
|
452
|
+
add_network_base()
|
|
453
|
+
add("CONFIG_TLS_CREDENTIALS=y")
|
|
454
|
+
if iot.get("mqtt"):
|
|
455
|
+
add("CONFIG_MQTT_LIB_TLS=y")
|
|
456
|
+
|
|
457
|
+
if iot.get("ble"):
|
|
458
|
+
if provider == "cc3501e":
|
|
459
|
+
if resolved_chip_state.get("cc3501e"):
|
|
460
|
+
add("# BLE: AEN CC3501E bridge backend, not Zephyr HCI.")
|
|
461
|
+
add("CONFIG_ALP_SDK_BLE_CC3501E=y")
|
|
462
|
+
else:
|
|
463
|
+
add("# BLE: SoM wireless provider is CC3501E, but this board")
|
|
464
|
+
add("# does not populate the chip (board.yaml `populated.cc3501e: "
|
|
465
|
+
"false`);")
|
|
466
|
+
add("# no BLE backend is emitted for this slice.")
|
|
467
|
+
elif linux_owned:
|
|
468
|
+
add("# BLE: provider data path is Linux/BSP-owned on this SoM;")
|
|
469
|
+
add("# no Zephyr BT-host backend is emitted for this slice.")
|
|
470
|
+
else:
|
|
471
|
+
add("# BLE: generic Zephyr BT host path.")
|
|
472
|
+
add("CONFIG_BT=y")
|
|
473
|
+
add("CONFIG_BT_PERIPHERAL=y")
|
|
474
|
+
add("CONFIG_BT_CENTRAL=y")
|
|
475
|
+
add("CONFIG_ALP_SDK_BLE=y")
|
|
476
|
+
|
|
477
|
+
lines.append("")
|
|
478
|
+
return lines
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _yocto_iot_lines(project: BoardProject, slice_: Slice) -> list[str]:
|
|
482
|
+
"""local.conf lines for ``cores.<id>.iot`` on a Yocto slice."""
|
|
483
|
+
iot = slice_.iot or {}
|
|
484
|
+
if not any(iot.get(k) for k in ("wifi", "mqtt", "tls", "ble")):
|
|
485
|
+
return []
|
|
486
|
+
|
|
487
|
+
provider = _wireless_provider(project)
|
|
488
|
+
lines: list[str] = [
|
|
489
|
+
"",
|
|
490
|
+
f"# IoT features declared on core `{slice_.core_id}` "
|
|
491
|
+
"(board.yaml `cores.*.iot`)",
|
|
492
|
+
]
|
|
493
|
+
if provider:
|
|
494
|
+
lines.append(
|
|
495
|
+
f"# Wireless provider resolved from `{project.sku}` "
|
|
496
|
+
f"on_module.wifi_ble: {provider}.")
|
|
497
|
+
else:
|
|
498
|
+
lines.append("# Wireless provider is TBD/absent; emit generic Linux userland.")
|
|
499
|
+
|
|
500
|
+
image_pkgs: list[str] = []
|
|
501
|
+
packageconfig: list[str] = []
|
|
502
|
+
|
|
503
|
+
def add_unique(seq: list[str], value: str) -> None:
|
|
504
|
+
if value not in seq:
|
|
505
|
+
seq.append(value)
|
|
506
|
+
|
|
507
|
+
linux_owned = _is_linux_wireless_provider(provider)
|
|
508
|
+
|
|
509
|
+
if iot.get("wifi"):
|
|
510
|
+
if provider == "cc3501e":
|
|
511
|
+
lines.append("# Wi-Fi: CC3501E is controlled by the Zephyr/companion path.")
|
|
512
|
+
else:
|
|
513
|
+
if linux_owned:
|
|
514
|
+
lines.append(
|
|
515
|
+
"# Wi-Fi: Linux owns this provider's SDIO/firmware path; "
|
|
516
|
+
"BSP/machine recipes supply kernel/firmware packages.")
|
|
517
|
+
add_unique(image_pkgs, "wpa-supplicant")
|
|
518
|
+
add_unique(image_pkgs, "iw")
|
|
519
|
+
add_unique(image_pkgs, "wireless-regdb")
|
|
520
|
+
|
|
521
|
+
if iot.get("ble"):
|
|
522
|
+
if provider == "cc3501e":
|
|
523
|
+
lines.append("# BLE: CC3501E is controlled by the Zephyr/companion path.")
|
|
524
|
+
else:
|
|
525
|
+
if linux_owned:
|
|
526
|
+
lines.append(
|
|
527
|
+
"# BLE: Linux owns this provider's HCI path; BSP/machine "
|
|
528
|
+
"recipes supply controller firmware.")
|
|
529
|
+
add_unique(image_pkgs, "bluez5")
|
|
530
|
+
|
|
531
|
+
if iot.get("mqtt"):
|
|
532
|
+
add_unique(packageconfig, "mqtt")
|
|
533
|
+
|
|
534
|
+
if iot.get("tls"):
|
|
535
|
+
add_unique(packageconfig, "security")
|
|
536
|
+
add_unique(image_pkgs, "ca-certificates")
|
|
537
|
+
|
|
538
|
+
if image_pkgs:
|
|
539
|
+
lines.append(f'IMAGE_INSTALL:append = " {" ".join(image_pkgs)}"')
|
|
540
|
+
if packageconfig:
|
|
541
|
+
lines.append(
|
|
542
|
+
f'PACKAGECONFIG:append:pn-alp-sdk = " {" ".join(packageconfig)}"')
|
|
543
|
+
|
|
544
|
+
return lines
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _emit_baseline(slice_: Slice, diagnostics: dict[str, Any]) -> list[str]:
|
|
548
|
+
"""Baseline Kconfig lines: the fragment header comment + the core
|
|
549
|
+
log/print enables every Zephyr slice gets regardless of what it
|
|
550
|
+
declares in board.yaml.
|
|
551
|
+
"""
|
|
552
|
+
lines: list[str] = []
|
|
553
|
+
lines.append("# Auto-generated by scripts/alp_orchestrate.py "
|
|
554
|
+
"-- do not edit.")
|
|
555
|
+
lines.append(f"# Per-core Kconfig fragment for slice "
|
|
556
|
+
f"`{slice_.core_id}` ({slice_.os}).")
|
|
557
|
+
lines.append("")
|
|
558
|
+
lines.append("CONFIG_ALP_SDK=y")
|
|
559
|
+
lines.append("CONFIG_LOG=y")
|
|
560
|
+
lines.append("CONFIG_PRINTK=y")
|
|
561
|
+
if diagnostics.get("last_error", True):
|
|
562
|
+
lines.append("CONFIG_THREAD_LOCAL_STORAGE=y")
|
|
563
|
+
log_level = diagnostics.get("log_level")
|
|
564
|
+
if log_level is not None:
|
|
565
|
+
log_level_kc = {
|
|
566
|
+
"error": 1, "warn": 2, "info": 3, "debug": 4, "trace": 4,
|
|
567
|
+
}
|
|
568
|
+
if log_level in log_level_kc:
|
|
569
|
+
lines.append(f"CONFIG_LOG_DEFAULT_LEVEL={log_level_kc[log_level]}")
|
|
570
|
+
lines.append("")
|
|
571
|
+
return lines
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _emit_console(diagnostics: dict[str, Any], slice_: Slice,
|
|
575
|
+
project: Optional[BoardProject] = None) -> list[str]:
|
|
576
|
+
"""Console backend, auto-selected from the slice OS (overridable via
|
|
577
|
+
board.yaml `diagnostics.console:`). A Zephyr slice defaults to the
|
|
578
|
+
Alp UART console so `west build && attach a terminal` just works;
|
|
579
|
+
a customer on a serial-less bench flips to `ram`.
|
|
580
|
+
|
|
581
|
+
`diagnostics.sim_console: true` (issue #686) upgrades the AUTO-resolved
|
|
582
|
+
console of a HEADLESS core (`hw_console: false` -- no HW UART, e.g. the
|
|
583
|
+
RZ/V2N M33 system-manager) to the RAM console, so the studio simulator's
|
|
584
|
+
`uart_socket` isn't silent. Precedence: an explicit `diagnostics.
|
|
585
|
+
console:` always wins; a core that has a UART console is simulated
|
|
586
|
+
through its real UART node and needs no Kconfig change.
|
|
587
|
+
"""
|
|
588
|
+
raw = diagnostics.get("console")
|
|
589
|
+
console = _resolve_console(raw, slice_.os, slice_.hw_console)
|
|
590
|
+
auto = (raw or "auto").strip().lower() == "auto"
|
|
591
|
+
# A headless AUTO core resolves to "none" (above); `sim_console: true`
|
|
592
|
+
# upgrades it to the RAM console so the studio simulator's uart_socket
|
|
593
|
+
# isn't silent (#686). Precedence: an explicit `diagnostics.console:`
|
|
594
|
+
# (auto == False) always wins and is never upgraded.
|
|
595
|
+
sim = bool(diagnostics.get("sim_console")) and auto \
|
|
596
|
+
and not slice_.hw_console and console == "none"
|
|
597
|
+
if sim:
|
|
598
|
+
console = "ram"
|
|
599
|
+
# `diagnostics.link: itcm` (tan-cli#1350): a Flow C RAM-run's default
|
|
600
|
+
# observable is the RAM console, so an AUTO console is promoted to it. An
|
|
601
|
+
# explicit `console: uart` is NOT promoted: it keeps the board UART console
|
|
602
|
+
# and `link_target.itcm_conf` emits no RAM-console bits for it
|
|
603
|
+
# (tan-cli#1374). linux/none are refused by the loader (alp is a uart alias).
|
|
604
|
+
if auto and link_applies_to(diagnostics, slice_):
|
|
605
|
+
console = "ram"
|
|
606
|
+
ram_size = _RAM_CONSOLE_MIN_SIZE
|
|
607
|
+
if console == "ram" and project is not None:
|
|
608
|
+
ram_size = max(ram_size, _app_ram_console_size(project, slice_))
|
|
609
|
+
return _emit_zephyr_console(console, sim_console=sim, ram_size=ram_size)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# Alif "high-perf"/"high-efficiency" M55 pair -> the "M55-HP"/"M55-HE"
|
|
613
|
+
# short form the boot banner and Alif's own docs use, in place of the
|
|
614
|
+
# fuller "Cortex-M55" every other core type gets (_core_display_label).
|
|
615
|
+
_M55_SUBTYPE_SUFFIX = {"high-perf": "HP", "high-efficiency": "HE"}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _core_display_label(core: dict[str, Any]) -> str:
|
|
619
|
+
"""Boot-banner label for one `soc_spec["cores"]` entry.
|
|
620
|
+
|
|
621
|
+
"Cortex-<X>" from the `type` field for the general case (e.g.
|
|
622
|
+
"cortex-a32" -> "Cortex-A32"); the Alif M55 HP/HE pair gets the
|
|
623
|
+
shorter "M55-HP"/"M55-HE" form instead, because that IS the name
|
|
624
|
+
Alif's own docs and the AEN board trees use for them -- printing
|
|
625
|
+
"Cortex-M55-HE" nobody else calls it that would be its own kind of
|
|
626
|
+
dishonesty. Falls back to the bare (uppercased) type string when it
|
|
627
|
+
is not a "cortex-*" type at all (e.g. an NPU-only or vendor-specific
|
|
628
|
+
core class no SoC spec in the catalogue uses today).
|
|
629
|
+
"""
|
|
630
|
+
ctype = str(core.get("type") or "").strip()
|
|
631
|
+
subtype = str(core.get("subtype") or "").strip().lower()
|
|
632
|
+
if ctype == "cortex-m55" and subtype in _M55_SUBTYPE_SUFFIX:
|
|
633
|
+
return f"M55-{_M55_SUBTYPE_SUFFIX[subtype]}"
|
|
634
|
+
if ctype.lower().startswith("cortex-"):
|
|
635
|
+
return "Cortex-" + ctype.split("-", 1)[1].upper()
|
|
636
|
+
return ctype.upper() if ctype else "core"
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _soc_cpu_complement(soc_spec: dict[str, Any], active_core_id: Optional[str]) -> str:
|
|
640
|
+
"""Pre-formatted core list for `CONFIG_ALP_SDK_SOC_CPUS`: every core
|
|
641
|
+
the SoC JSON declares, the active one marked and listed first --
|
|
642
|
+
e.g. "M55-HE @160MHz (active) + M55-HP @400MHz + 2x Cortex-A32
|
|
643
|
+
@800MHz". A core with no `freq_mhz` (e.g. an internal-companion NPU
|
|
644
|
+
core with no independent clock in the SoC JSON) prints its label with
|
|
645
|
+
no `@...MHz` suffix rather than a fabricated frequency.
|
|
646
|
+
"""
|
|
647
|
+
cores = list(soc_spec.get("cores") or [])
|
|
648
|
+
cores.sort(key=lambda c: 0 if c.get("id") == active_core_id else 1)
|
|
649
|
+
parts: list[str] = []
|
|
650
|
+
for core in cores:
|
|
651
|
+
label = _core_display_label(core)
|
|
652
|
+
count = core.get("count") or 1
|
|
653
|
+
freq = core.get("freq_mhz")
|
|
654
|
+
prefix = f"{int(count)}x " if isinstance(count, (int, float)) and count > 1 else ""
|
|
655
|
+
freq_str = f" @{int(freq)}MHz" if isinstance(freq, (int, float)) else ""
|
|
656
|
+
marker = " (active)" if core.get("id") == active_core_id else ""
|
|
657
|
+
# `|<cluster>` lets alp_banner.c move the marker to the core the
|
|
658
|
+
# image is actually built for (alp-sdk#2469) -- see
|
|
659
|
+
# src/zephyr/alp_soc_cpus.h.
|
|
660
|
+
cluster = core.get("zephyr_cpucluster")
|
|
661
|
+
tag = f"|{cluster}" if cluster else ""
|
|
662
|
+
parts.append(f"{prefix}{label}{freq_str}{marker}{tag}")
|
|
663
|
+
return " + ".join(parts)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _soc_npu_complement(soc_spec: dict[str, Any]) -> str:
|
|
667
|
+
"""Pre-formatted NPU list for `CONFIG_ALP_SDK_SOC_NPUS`, e.g.
|
|
668
|
+
"Ethos-U85 + 2x Ethos-U55" -- same-`type` NPU instances (e.g. the
|
|
669
|
+
E8's two Ethos-U55s, one per M55 core) are grouped into a single
|
|
670
|
+
"Nx <Label>" entry rather than repeated, in the order their `type`
|
|
671
|
+
first appears in `soc_spec["npus"]`.
|
|
672
|
+
"""
|
|
673
|
+
order: list[str] = []
|
|
674
|
+
counts: dict[str, int] = {}
|
|
675
|
+
for npu in (soc_spec.get("npus") or []):
|
|
676
|
+
ntype = str(npu.get("type") or "").strip()
|
|
677
|
+
if not ntype:
|
|
678
|
+
continue
|
|
679
|
+
label = "-".join(seg.capitalize() for seg in ntype.split("-"))
|
|
680
|
+
if label not in counts:
|
|
681
|
+
order.append(label)
|
|
682
|
+
counts[label] = counts.get(label, 0) + 1
|
|
683
|
+
return " + ".join(
|
|
684
|
+
f"{counts[label]}x {label}" if counts[label] > 1 else label
|
|
685
|
+
for label in order)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _soc_display_name(soc_spec: dict[str, Any]) -> str:
|
|
689
|
+
"""Vendor + family + part for `CONFIG_ALP_SDK_SOC_NAME`, e.g. "Alif
|
|
690
|
+
Ensemble E8". `vendor` contributes only its first word (Alif
|
|
691
|
+
Semiconductor -> Alif); `part` is dropped when it already restates
|
|
692
|
+
`family` verbatim as a prefix (e.g. family "Ensemble" / part "Ensemble E8"
|
|
693
|
+
would otherwise read "Alif Ensemble Ensemble E8").
|
|
694
|
+
"""
|
|
695
|
+
vendor = str(soc_spec.get("vendor") or "").strip()
|
|
696
|
+
family = str(soc_spec.get("family") or "").strip()
|
|
697
|
+
part = str(soc_spec.get("part") or "").strip()
|
|
698
|
+
vendor_short = vendor.split()[0] if vendor else ""
|
|
699
|
+
if family and part and part.startswith(family):
|
|
700
|
+
segments = [vendor_short, part]
|
|
701
|
+
else:
|
|
702
|
+
segments = [vendor_short, family, part]
|
|
703
|
+
return " ".join(s for s in segments if s)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _emit_soc_summary(project: "BoardProject", slice_: "Slice") -> list[str]:
|
|
707
|
+
"""Boot-banner identity + system-summary Kconfig (`CONFIG_ALP_SDK_
|
|
708
|
+
SOC_*` / `CONFIG_ALP_SDK_SOM_*_MBIT`, `zephyr/kconfigs/core.kconfig`).
|
|
709
|
+
|
|
710
|
+
Every field here is transcribed, never invented, from data this
|
|
711
|
+
project already resolved: the SoC spec JSON (`project.soc_spec` --
|
|
712
|
+
`cores[]`, `npus[]`, `soc_ram_kb`, `soc_flash_mb`, the on-die SRAM +
|
|
713
|
+
MRAM the SoC itself carries) for the CONFIG_ALP_SDK_SOC_* facts, and
|
|
714
|
+
the SoM preset's `memory:` block (off-SoC OSPI RAM/flash the SKU's
|
|
715
|
+
BOM actually populates -- distinct from the SoC facts above: two SKUs
|
|
716
|
+
on the identical PCB/silicon can differ here, e.g. E1M-AEN803's 512
|
|
717
|
+
Mbit HyperRAM + 256 Mbit NOR vs E1M-AEN801, which populates neither)
|
|
718
|
+
for CONFIG_ALP_SDK_SOM_{DRAM,FLASH}_MBIT.
|
|
719
|
+
|
|
720
|
+
`alp_banner.c` prefers these over its devicetree fallback (one
|
|
721
|
+
image's chosen sram/flash REGION size, not the SoM's actual
|
|
722
|
+
hardware) whenever `CONFIG_ALP_SDK_SOC_CPUS` is non-empty -- see that
|
|
723
|
+
file's module docstring. A project whose SoC spec is missing a field
|
|
724
|
+
(no `npus`, no `soc_ram_kb`, ...) still gets whatever IS known; only
|
|
725
|
+
`CONFIG_ALP_SDK_SOC_NAME`/`_CPUS` gate the whole banner section on the
|
|
726
|
+
C side, so those two are the only ones this function must guarantee
|
|
727
|
+
non-empty whenever `project.soc_spec` resolves at all.
|
|
728
|
+
"""
|
|
729
|
+
soc_spec = project.soc_spec or {}
|
|
730
|
+
lines: list[str] = [
|
|
731
|
+
"# Boot-banner SoC identity + system summary (from the SoC spec "
|
|
732
|
+
"JSON + SoM preset `memory:`; see alp_banner.c).",
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
name = _soc_display_name(soc_spec)
|
|
736
|
+
if name:
|
|
737
|
+
lines.append(f'CONFIG_ALP_SDK_SOC_NAME="{name}"')
|
|
738
|
+
|
|
739
|
+
cpus = _soc_cpu_complement(soc_spec, slice_.core_id)
|
|
740
|
+
if cpus:
|
|
741
|
+
lines.append(f'CONFIG_ALP_SDK_SOC_CPUS="{cpus}"')
|
|
742
|
+
|
|
743
|
+
npus = _soc_npu_complement(soc_spec)
|
|
744
|
+
if npus:
|
|
745
|
+
lines.append(f'CONFIG_ALP_SDK_SOC_NPUS="{npus}"')
|
|
746
|
+
|
|
747
|
+
soc_ram_kb = soc_spec.get("soc_ram_kb")
|
|
748
|
+
if isinstance(soc_ram_kb, (int, float)) and soc_ram_kb > 0:
|
|
749
|
+
lines.append(f"CONFIG_ALP_SDK_SOC_SRAM_KB={int(soc_ram_kb)}")
|
|
750
|
+
|
|
751
|
+
soc_flash_mb = soc_spec.get("soc_flash_mb")
|
|
752
|
+
if isinstance(soc_flash_mb, (int, float)) and soc_flash_mb > 0:
|
|
753
|
+
lines.append(f"CONFIG_ALP_SDK_SOC_MRAM_KB={int(round(soc_flash_mb * 1024))}")
|
|
754
|
+
|
|
755
|
+
# Off-SoC OSPI memory the SoM SKU actually populates (som-preset
|
|
756
|
+
# `memory:` -- a MODULE fact, not a SoC one; see the SKU contrast
|
|
757
|
+
# above). `is_tbd`/absent/non-positive all mean "don't claim it".
|
|
758
|
+
mem = project.som_preset.get("memory") or {}
|
|
759
|
+
dram_mbit = mem.get("dram_mbit")
|
|
760
|
+
if isinstance(dram_mbit, (int, float)) and not _is_tbd(dram_mbit) and dram_mbit > 0:
|
|
761
|
+
lines.append(f"CONFIG_ALP_SDK_SOM_DRAM_MBIT={int(dram_mbit)}")
|
|
762
|
+
flash_mbit = mem.get("flash_mbit")
|
|
763
|
+
if isinstance(flash_mbit, (int, float)) and not _is_tbd(flash_mbit) and flash_mbit > 0:
|
|
764
|
+
lines.append(f"CONFIG_ALP_SDK_SOM_FLASH_MBIT={int(flash_mbit)}")
|
|
765
|
+
|
|
766
|
+
lines.append("")
|
|
767
|
+
return lines
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _emit_som_caps(
|
|
771
|
+
project: "BoardProject",
|
|
772
|
+
silicon: Optional[str],
|
|
773
|
+
kconfig: Optional[str],
|
|
774
|
+
) -> list[str]:
|
|
775
|
+
"""SoM-capability Kconfig lines: HW-manifest EEPROM wiring, the
|
|
776
|
+
resolved SoM-silicon Kconfig symbol, and the cross-EVK board facade /
|
|
777
|
+
`<alp/soc_caps.h>` capability-restriction selector.
|
|
778
|
+
"""
|
|
779
|
+
lines: list[str] = []
|
|
780
|
+
|
|
781
|
+
# HW manifest EEPROM: the SoM preset provides the default bus,
|
|
782
|
+
# while board.yaml `features.hw_info.eeprom` can pin the exact
|
|
783
|
+
# app/project address and manifest offset. Harmless on examples
|
|
784
|
+
# without that bus wired: the read returns NOT_READY and the banner
|
|
785
|
+
# says so.
|
|
786
|
+
hw_info_eeprom = project.hw_info_eeprom_config()
|
|
787
|
+
if hw_info_eeprom is not None:
|
|
788
|
+
source = hw_info_eeprom.get("source")
|
|
789
|
+
if source == "features.hw_info.eeprom":
|
|
790
|
+
lines.append(
|
|
791
|
+
"# HW manifest EEPROM from board.yaml `features.hw_info.eeprom`;")
|
|
792
|
+
else:
|
|
793
|
+
lines.append("# On-module HW manifest EEPROM -> portable bus 0 (alp-i2c0);")
|
|
794
|
+
lines.append("# enables alp_hw_info_read() + the console banner manifest line.")
|
|
795
|
+
lines.append(
|
|
796
|
+
"CONFIG_ALP_SDK_HW_INFO_EEPROM_I2C_BUS_ID="
|
|
797
|
+
f"{hw_info_eeprom['bus_id']}")
|
|
798
|
+
if source == "features.hw_info.eeprom":
|
|
799
|
+
lines.append(
|
|
800
|
+
"CONFIG_ALP_SDK_HW_INFO_EEPROM_ADDR_7BIT="
|
|
801
|
+
f"0x{hw_info_eeprom['addr_7bit']:02x}")
|
|
802
|
+
lines.append(
|
|
803
|
+
"CONFIG_ALP_SDK_HW_INFO_EEPROM_OFFSET="
|
|
804
|
+
f"{hw_info_eeprom['offset']}")
|
|
805
|
+
lines.append("")
|
|
806
|
+
|
|
807
|
+
# Build-time hw_rev this project resolved -- lets the boot banner warn
|
|
808
|
+
# when the LIVE EEPROM manifest disagrees (issue #1853). Emitted
|
|
809
|
+
# unconditionally (not scoped to the `hw_info_eeprom` block above): a
|
|
810
|
+
# SKU with no on_module.eeprom today still gets the
|
|
811
|
+
# symbol, so it isn't silently dropped if that SKU gains an EEPROM
|
|
812
|
+
# later, and it stays harmless meanwhile (alp_hw_info_read() never
|
|
813
|
+
# returns ALP_OK without a bus, so the banner's compare never runs).
|
|
814
|
+
# Same `hw_rev or ... or "unknown"` fallback chain as
|
|
815
|
+
# alp_project_emit/hw_info.py's `_emit_hw_info_h` (project.hw_rev is
|
|
816
|
+
# already `som.hw_rev or default_hw_rev`; add the same "unknown"
|
|
817
|
+
# floor here so the two resolvers can't disagree on the empty case).
|
|
818
|
+
# Emit the composed board designator (`2626-r2`), not the bare revision key,
|
|
819
|
+
# because that is what the module's EEPROM identity carries -- and this symbol
|
|
820
|
+
# exists only to be compared against it. Getting this wrong is not cosmetic:
|
|
821
|
+
# CONFIG_ALP_SDK_HW_REV_MISMATCH_FATAL promotes the banner's mismatch warning
|
|
822
|
+
# to k_panic(), so a write side that composes and a compare side that does not
|
|
823
|
+
# would halt boot on a correctly provisioned module.
|
|
824
|
+
#
|
|
825
|
+
# project.hw_rev itself stays the BARE key -- loader.py passes it to
|
|
826
|
+
# family_revision_known() as a lookup key into hw_revisions, where a composed
|
|
827
|
+
# value would not resolve.
|
|
828
|
+
_som_hw_rev = project.hw_rev
|
|
829
|
+
if _som_hw_rev:
|
|
830
|
+
from .loader import _sku_family_dir
|
|
831
|
+
_fam = _sku_family_dir(project.sku)
|
|
832
|
+
if _fam:
|
|
833
|
+
_som_hw_rev = _sdk_compat.board_designator(
|
|
834
|
+
_sdk_compat.load_family_table(project.effective_metadata_root(), _fam),
|
|
835
|
+
_som_hw_rev)
|
|
836
|
+
lines.append(f'CONFIG_ALP_SDK_SOM_HW_REV="{_som_hw_rev or "unknown"}"')
|
|
837
|
+
lines.append("")
|
|
838
|
+
|
|
839
|
+
if kconfig:
|
|
840
|
+
lines.append(f"# SoM silicon ({silicon} via {project.sku})")
|
|
841
|
+
lines.append(f"CONFIG_{kconfig}=y")
|
|
842
|
+
lines.append("")
|
|
843
|
+
|
|
844
|
+
# Cross-EVK board facade selector (<alp/board.h>).
|
|
845
|
+
# Emitted only when the project resolves to a named board preset
|
|
846
|
+
# (e.g. `preset: e1m-x-evk` -> board_name "E1M-X-EVK").
|
|
847
|
+
# CONFIG_COMPILER_OPT passes the define into every source file the
|
|
848
|
+
# Zephyr build compiles, so the facade header can resolve the
|
|
849
|
+
# correct EVK-specific BOARD_* aliases without per-app prj.conf
|
|
850
|
+
# entries or extra_args. Inline boards (no preset/name) skip this;
|
|
851
|
+
# native_sim testcases that do need the define set it via extra_args.
|
|
852
|
+
# Per-SKU capability-restriction gate for <alp/soc_caps.h>: emitted
|
|
853
|
+
# ONLY when the SoM preset declares `silicon_capabilities.unpopulated`
|
|
854
|
+
# (no SKU in the current catalogue does), so unrestricted SKUs keep a
|
|
855
|
+
# byte-identical fragment. Folded into the same CONFIG_COMPILER_OPT
|
|
856
|
+
# string as the board facade define because that Kconfig is
|
|
857
|
+
# single-value (last write wins, not additive).
|
|
858
|
+
som_def = ""
|
|
859
|
+
if som_unpopulated_capabilities(project.som_preset):
|
|
860
|
+
som_def = f"-DALP_SOM_{_som_define_slug(project.sku)}"
|
|
861
|
+
if project.board_name:
|
|
862
|
+
# Cross-EVK board facade selector for <alp/board.h>. NOTE:
|
|
863
|
+
# CONFIG_COMPILER_OPT is a single-value Kconfig string (last write
|
|
864
|
+
# wins, not additive) -- an app must NOT also set it in prj.conf or
|
|
865
|
+
# this -D is silently dropped; pass extra defines another way.
|
|
866
|
+
lines.append("# Cross-EVK board facade selector (<alp/board.h>);"
|
|
867
|
+
" CONFIG_COMPILER_OPT is single-value (do not also set in prj.conf).")
|
|
868
|
+
if som_def:
|
|
869
|
+
lines.append("# + ALP_SOM_* capability-restriction gate: this SKU "
|
|
870
|
+
"leaves silicon capabilities unpopulated "
|
|
871
|
+
"(SoM preset silicon_capabilities.unpopulated).")
|
|
872
|
+
opt = f"-DALP_BOARD_{_board_define_slug(project.board_name)}"
|
|
873
|
+
if som_def:
|
|
874
|
+
opt = f"{opt} {som_def}"
|
|
875
|
+
lines.append(f'CONFIG_COMPILER_OPT="{opt}"')
|
|
876
|
+
lines.append("")
|
|
877
|
+
elif som_def:
|
|
878
|
+
lines.append("# ALP_SOM_* capability-restriction gate (<alp/soc_caps.h>);"
|
|
879
|
+
" CONFIG_COMPILER_OPT is single-value (do not also set in prj.conf).")
|
|
880
|
+
lines.append(f'CONFIG_COMPILER_OPT="{som_def}"')
|
|
881
|
+
lines.append("")
|
|
882
|
+
|
|
883
|
+
return lines
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _slug_kconfig(slug: str) -> str:
|
|
887
|
+
"""Map a chip/block slug to its Kconfig symbol (CHIP_ or BLOCK_)."""
|
|
888
|
+
kind = "BLOCK" if slug in _BLOCK_SLUGS else "CHIP"
|
|
889
|
+
return f"CONFIG_ALP_SDK_{kind}_{slug.upper()}"
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def _resolve_chip_states(project: "BoardProject") -> dict[str, bool]:
|
|
893
|
+
"""Final resolved on/off state for every chip/block slug the project
|
|
894
|
+
touches -- the single source of truth `_emit_chips` prints its
|
|
895
|
+
`CONFIG_ALP_SDK_CHIP_*=` lines FROM (so the printed line and this map
|
|
896
|
+
can never disagree by construction) and any other emitter (e.g.
|
|
897
|
+
`_zephyr_iot_kconfig`'s CC3501E Wi-Fi/BLE gate) consults before gating
|
|
898
|
+
a dependent symbol (issue #874).
|
|
899
|
+
|
|
900
|
+
Precedence: on-module presence is the physical baseline (always
|
|
901
|
+
True). An explicit `populated: {chip: true/false}` entry then applies
|
|
902
|
+
the board's stated population, on-module chip or not. Finally, a
|
|
903
|
+
project-declared `chips:` entry can still turn a board-DNI'd chip ON
|
|
904
|
+
-- the documented escape hatch for a project that wires up a sensor
|
|
905
|
+
the board preset doesn't populate by default (e.g. `ai/cold-chain-
|
|
906
|
+
monitor`'s `chips: [bme280]` overriding the EVK preset's `populated:
|
|
907
|
+
{bme280: false}`) -- UNLESS the chip is ALSO on-module: an on-module
|
|
908
|
+
(SoM-intrinsic) component is fixed at SoM-fab time (see the `som:`
|
|
909
|
+
block's own description in board.schema.json) and re-listing it in
|
|
910
|
+
`chips:` is a redundant declaration, not a fresh override, so it must
|
|
911
|
+
not silently flip a board's `populated: {chip: false}` back on for a
|
|
912
|
+
chip ALSO claimed on-module (issue #874 follow-up: the triple-overlap
|
|
913
|
+
case -- on-module + populated:false + project `chips:` all naming the
|
|
914
|
+
same chip -- used to let the project-chips pass win regardless,
|
|
915
|
+
re-creating the exact WIFI_CC3501E=y-against-chip=n contradiction
|
|
916
|
+
#874 fixed).
|
|
917
|
+
"""
|
|
918
|
+
states: dict[str, bool] = {}
|
|
919
|
+
|
|
920
|
+
som_chips: set[str] = set()
|
|
921
|
+
om = project.som_preset.get("on_module") or {}
|
|
922
|
+
for slug in _slugs_from_on_module(om):
|
|
923
|
+
som_chips.add(slug)
|
|
924
|
+
hf = project.som_preset.get("helper_firmware") or []
|
|
925
|
+
for slug in _slugs_from_helper_firmware(hf):
|
|
926
|
+
som_chips.add(slug)
|
|
927
|
+
for chip in som_chips:
|
|
928
|
+
states[chip] = True
|
|
929
|
+
|
|
930
|
+
populated: dict[str, bool] = dict(
|
|
931
|
+
(project.board_preset or {}).get("populated") or {})
|
|
932
|
+
for chip, on in populated.items():
|
|
933
|
+
states[chip] = bool(on)
|
|
934
|
+
|
|
935
|
+
for chip in (project.chips or []):
|
|
936
|
+
if chip in som_chips:
|
|
937
|
+
continue
|
|
938
|
+
states[chip] = True
|
|
939
|
+
|
|
940
|
+
return states
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _chip_has_driver(slug: str) -> bool:
|
|
944
|
+
"""True when `chips/<slug>/` exists, i.e. there is something to compile.
|
|
945
|
+
|
|
946
|
+
This is the same criterion `_slugs_from_on_module` already applies to
|
|
947
|
+
`ospi_memories` and `hyperram`, whose docstring records the reason:
|
|
948
|
+
those parts have "no `chips/<part>/` driver, so their MPNs are NOT
|
|
949
|
+
extracted as chip slugs (emitting them as `CONFIG_ALP_SDK_CHIP_<X>`
|
|
950
|
+
would trip Zephyr's undefined-symbol guard)". The rule is applied there
|
|
951
|
+
per-field; it has to hold per-chip too, because a scalar `on_module:`
|
|
952
|
+
field can name a real chip manifest that has no driver behind it.
|
|
953
|
+
|
|
954
|
+
Driver presence is checked on disk rather than read from the manifest's
|
|
955
|
+
`driver_status:`, because the directory is what the Kconfig declaration
|
|
956
|
+
actually points at -- every entry in `zephyr/kconfigs/chips.kconfig`
|
|
957
|
+
reads "Compile chips/<part>/<part>.c". A status string can drift from
|
|
958
|
+
the tree; the directory cannot.
|
|
959
|
+
|
|
960
|
+
`REPO` is the BOUND alp-sdk checkout (`tan.planner_root`), not tan's own
|
|
961
|
+
tree -- `chips/` is a fact and facts did not relocate (ADR-0017).
|
|
962
|
+
"""
|
|
963
|
+
return (REPO / "chips" / slug).is_dir()
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
@lru_cache(maxsize=1)
|
|
967
|
+
def _declared_chip_symbols() -> frozenset[str]:
|
|
968
|
+
"""Every `ALP_SDK_CHIP_*` symbol the BOUND alp-sdk actually declares.
|
|
969
|
+
|
|
970
|
+
Parsed from the SDK's own `zephyr/**/*.kconfig` rather than inferred, so
|
|
971
|
+
this is the same fact Zephyr resolves against. Measured on alp-sdk `dev`:
|
|
972
|
+
80 declarations, all in `zephyr/kconfigs/chips.kconfig`, matching the 80
|
|
973
|
+
`chips/<slug>/` directories exactly.
|
|
974
|
+
|
|
975
|
+
An empty result means the SDK's kconfigs could not be read at all; the
|
|
976
|
+
caller treats that as "cannot verify" and stays silent rather than
|
|
977
|
+
refusing every build on a layout this parser does not understand.
|
|
978
|
+
"""
|
|
979
|
+
declared: set[str] = set()
|
|
980
|
+
for path in sorted((REPO / "zephyr").rglob("*.kconfig")):
|
|
981
|
+
try:
|
|
982
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
983
|
+
except OSError:
|
|
984
|
+
continue
|
|
985
|
+
declared.update(re.findall(r"^config (ALP_SDK_CHIP_\w+)", text, re.M))
|
|
986
|
+
return frozenset(declared)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _assert_chip_symbols_declared(slugs: "list[str]") -> None:
|
|
990
|
+
"""Refuse to write a `CONFIG_` line the bound SDK cannot resolve (#728).
|
|
991
|
+
|
|
992
|
+
`_chip_has_driver` decides what to emit from `chips/<slug>/` on disk.
|
|
993
|
+
That is the right *intent* -- the directory is what the declaration
|
|
994
|
+
compiles -- but it is an inference, and Zephyr resolves the declaration.
|
|
995
|
+
When the two disagree, the emitted line reaches Kconfig as
|
|
996
|
+
|
|
997
|
+
alp.conf:28: warning: attempt to assign the value 'y' to the
|
|
998
|
+
undefined symbol ALP_SDK_CHIP_DP83825
|
|
999
|
+
error: Aborting due to Kconfig warnings
|
|
1000
|
+
|
|
1001
|
+
which blames the app's generated config for a planner/SDK skew and takes
|
|
1002
|
+
out `tan build` for the whole SoM. That is how tan-cli#728 presented:
|
|
1003
|
+
released `tan 0.5.1`, whose vendored planner predates alp-sdk#1241/#1322,
|
|
1004
|
+
against an alp-sdk `dev` whose `ethernet_phy: dp83825` made an undriven
|
|
1005
|
+
chip reachable from `on_module:` for the first time.
|
|
1006
|
+
|
|
1007
|
+
A released binary cannot be retro-fixed, so this does not repair 0.5.1 --
|
|
1008
|
+
it stops the NEXT skew of this class from being discovered by Zephyr. The
|
|
1009
|
+
refusal names the symbol and the SDK, so the reader knows which pair
|
|
1010
|
+
disagrees rather than hunting a `CONFIG_` line they never wrote.
|
|
1011
|
+
|
|
1012
|
+
Silent when the SDK declares nothing this parser recognises: an
|
|
1013
|
+
unreadable or restructured kconfig tree is a reason to stay out of the
|
|
1014
|
+
way, not to refuse every build. Verification is the goal, not gatekeeping
|
|
1015
|
+
on a layout assumption.
|
|
1016
|
+
"""
|
|
1017
|
+
declared = _declared_chip_symbols()
|
|
1018
|
+
if not declared:
|
|
1019
|
+
return
|
|
1020
|
+
undeclared = sorted(
|
|
1021
|
+
s for s in slugs
|
|
1022
|
+
if (sym := _slug_kconfig(s).removeprefix("CONFIG_")).startswith("ALP_SDK_CHIP_")
|
|
1023
|
+
and sym not in declared
|
|
1024
|
+
)
|
|
1025
|
+
if not undeclared:
|
|
1026
|
+
return
|
|
1027
|
+
names = ", ".join(f"{_slug_kconfig(s)} (chip `{s}`)" for s in undeclared)
|
|
1028
|
+
raise OrchestratorError(
|
|
1029
|
+
f"the bound alp-sdk at {REPO} declares no Kconfig symbol for "
|
|
1030
|
+
f"{names}. Emitting it would write an assignment Zephyr cannot "
|
|
1031
|
+
f"resolve, failing the build with `attempt to assign the value 'y' "
|
|
1032
|
+
f"to the undefined symbol` and blaming the generated alp.conf. This "
|
|
1033
|
+
f"means tan's planner and this SDK disagree about which chips have "
|
|
1034
|
+
f"drivers -- usually a tan too old for the SDK (tan-cli#728). Update "
|
|
1035
|
+
f"tan, or bind an alp-sdk matching this tan."
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _emit_chips(
|
|
1040
|
+
project: "BoardProject",
|
|
1041
|
+
chip_subsystems_table: dict[str, tuple[str, ...]],
|
|
1042
|
+
) -> tuple[list[str], set[str], dict[str, bool]]:
|
|
1043
|
+
"""SoM-intrinsic + board-populated + project-declared chip drivers.
|
|
1044
|
+
|
|
1045
|
+
Every `CONFIG_ALP_SDK_CHIP_*=` / `CONFIG_ALP_SDK_BLOCK_*=` line is
|
|
1046
|
+
printed FROM `_resolve_chip_states`'s resolved map (single source of
|
|
1047
|
+
truth, issue #874) and printed EXACTLY ONCE regardless of how many of
|
|
1048
|
+
the three sources below mention it -- a chip already printed by an
|
|
1049
|
+
earlier block is skipped by a later one rather than re-asserted with
|
|
1050
|
+
a value that could disagree.
|
|
1051
|
+
|
|
1052
|
+
Returns the emitted Kconfig lines, the accumulated set of Zephyr
|
|
1053
|
+
subsystem names the enabled chip drivers require -- fed into
|
|
1054
|
+
`_emit_subsystems` alongside the slice's `peripherals:` array -- and
|
|
1055
|
+
the resolved chip on/off map.
|
|
1056
|
+
"""
|
|
1057
|
+
lines: list[str] = []
|
|
1058
|
+
chip_subsystems: set[str] = set()
|
|
1059
|
+
resolved_chip_state = _resolve_chip_states(project)
|
|
1060
|
+
printed: set[str] = set()
|
|
1061
|
+
|
|
1062
|
+
def _emit_block(header: str, candidates) -> None:
|
|
1063
|
+
chips = sorted(c for c in set(candidates) if c not in printed)
|
|
1064
|
+
if not chips:
|
|
1065
|
+
return
|
|
1066
|
+
_assert_chip_symbols_declared(chips)
|
|
1067
|
+
lines.append(header)
|
|
1068
|
+
for chip in chips:
|
|
1069
|
+
on = resolved_chip_state.get(chip, False)
|
|
1070
|
+
lines.append(f"{_slug_kconfig(chip)}={'y' if on else 'n'}")
|
|
1071
|
+
printed.add(chip)
|
|
1072
|
+
if on:
|
|
1073
|
+
for s in chip_subsystems_table.get(chip, ()):
|
|
1074
|
+
chip_subsystems.add(s)
|
|
1075
|
+
lines.append("")
|
|
1076
|
+
|
|
1077
|
+
# ----------------------------------------------------------------
|
|
1078
|
+
# SoM-intrinsic chip drivers — derived from on_module: + helper_firmware:
|
|
1079
|
+
# in the SoM preset. These are NOT declared by the customer; they
|
|
1080
|
+
# are determined by which SoM SKU the project targets. Swapping
|
|
1081
|
+
# `som.sku:` from E1M-V2N101 to E1M-AEN801 automatically swaps
|
|
1082
|
+
# the on-module chip set without any board.yaml changes.
|
|
1083
|
+
# ----------------------------------------------------------------
|
|
1084
|
+
som_chips: set[str] = set()
|
|
1085
|
+
om = project.som_preset.get("on_module") or {}
|
|
1086
|
+
if om:
|
|
1087
|
+
for slug in _slugs_from_on_module(om):
|
|
1088
|
+
som_chips.add(slug)
|
|
1089
|
+
hf = project.som_preset.get("helper_firmware") or []
|
|
1090
|
+
for slug in _slugs_from_helper_firmware(hf):
|
|
1091
|
+
som_chips.add(slug)
|
|
1092
|
+
|
|
1093
|
+
# A CONFIG_ALP_SDK_CHIP_<X> line is only meaningful when the symbol
|
|
1094
|
+
# exists, and the symbol only exists when there is a driver to compile:
|
|
1095
|
+
# every declaration in zephyr/kconfigs/chips.kconfig reads "Compile
|
|
1096
|
+
# chips/<part>/<part>.c". A manifest whose driver_status says no such
|
|
1097
|
+
# driver exists therefore has no symbol, and emitting the line anyway
|
|
1098
|
+
# writes a CONFIG_ assignment Zephyr cannot resolve (alp-sdk#1241).
|
|
1099
|
+
#
|
|
1100
|
+
# Measured over metadata/chips/*.yaml when this was added: 77 of 81
|
|
1101
|
+
# chips carry a declared symbol -- every `complete`, `partial` and
|
|
1102
|
+
# `stub` one, each with a real chips/<part>/ directory. The only
|
|
1103
|
+
# undeclared ones are the four `planned` Murata modules, which no SoM
|
|
1104
|
+
# populates. `ethernet_phy: dp83825` made an undriven chip reachable
|
|
1105
|
+
# from on_module: for the first time, which is how this surfaced.
|
|
1106
|
+
#
|
|
1107
|
+
# The chip stays in metadata -- it is real, populated hardware, and the
|
|
1108
|
+
# manifest is what documents the ADR-0023 layer-3 gap. It just does not
|
|
1109
|
+
# get a driver-selection Kconfig it has no driver for.
|
|
1110
|
+
som_chips = {s for s in som_chips if _chip_has_driver(s)}
|
|
1111
|
+
|
|
1112
|
+
_emit_block(
|
|
1113
|
+
f"# SoM-intrinsic chip drivers (from `{project.sku}` "
|
|
1114
|
+
f"on_module + helper_firmware)", som_chips)
|
|
1115
|
+
|
|
1116
|
+
# ----------------------------------------------------------------
|
|
1117
|
+
# Board-populated chip drivers. Single source: the resolved
|
|
1118
|
+
# board_preset dict, which comes from either the shared
|
|
1119
|
+
# metadata/boards/<preset>.yaml or the project's inline
|
|
1120
|
+
# top-level fields (synthesised by the loader). No project-level
|
|
1121
|
+
# override merge -- the schema's `oneOf` rule rejects mixing.
|
|
1122
|
+
#
|
|
1123
|
+
# Chip drivers compile per-chip via the
|
|
1124
|
+
# zephyr_library_sources_ifdef(CONFIG_ALP_SDK_CHIP_<NAME> ...)
|
|
1125
|
+
# (or CONFIG_ALP_SDK_BLOCK_<NAME> for the `button_led` / `pdm_mic`
|
|
1126
|
+
# SDK-level block helpers) entries in zephyr/CMakeLists.txt.
|
|
1127
|
+
#
|
|
1128
|
+
# We emit the chip-driver block on every zephyr slice — Kconfig
|
|
1129
|
+
# dedupes when multiple per-core fragments overlay onto the same
|
|
1130
|
+
# base. This keeps `--core <id>` invocations self-sufficient
|
|
1131
|
+
# (the slice carries everything it needs to compile) without
|
|
1132
|
+
# depending on cross-slice ordering.
|
|
1133
|
+
# ----------------------------------------------------------------
|
|
1134
|
+
populated: dict[str, bool] = dict(
|
|
1135
|
+
(project.board_preset or {}).get("populated") or {})
|
|
1136
|
+
_emit_block(
|
|
1137
|
+
"# Board-populated chip drivers (from the resolved "
|
|
1138
|
+
"board definition)", populated.keys())
|
|
1139
|
+
|
|
1140
|
+
# Project-declared chips (board.yaml top-level `chips:` array).
|
|
1141
|
+
# Adds chip drivers the application links directly via
|
|
1142
|
+
# <alp/chips/<name>.h> -- e.g. when the project plugs an external
|
|
1143
|
+
# sensor into a board's headers that's not in the board's
|
|
1144
|
+
# `populated:` table, or turns ON a chip the board preset DNIs by
|
|
1145
|
+
# default (`_resolve_chip_states` already folded that override into
|
|
1146
|
+
# the resolved value the "Board-populated" block above printed, so
|
|
1147
|
+
# there is nothing left to print here for such a chip -- this block
|
|
1148
|
+
# only has fresh lines for chips `populated:` never mentioned at
|
|
1149
|
+
# all). A chip that's ALSO on-module never reaches here with a
|
|
1150
|
+
# different value than the SoM-intrinsic block already printed
|
|
1151
|
+
# (`_resolve_chip_states` deliberately does not let a project
|
|
1152
|
+
# `chips:` entry override an on-module chip's resolved state).
|
|
1153
|
+
_emit_block(
|
|
1154
|
+
"# Project-declared chips (board.yaml `chips:` array)",
|
|
1155
|
+
project.chips or [])
|
|
1156
|
+
|
|
1157
|
+
return lines, chip_subsystems, resolved_chip_state
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _emit_subsystems(
|
|
1161
|
+
project: BoardProject, slice_: Slice, chip_subsystems: set[str]
|
|
1162
|
+
) -> list[str]:
|
|
1163
|
+
"""Zephyr subsystems required by the slice: union of (chip-driver-
|
|
1164
|
+
required subsystems, from `_emit_chips`) and (this core's
|
|
1165
|
+
`peripherals:` array, which adds to the union per spec §4.6).
|
|
1166
|
+
"""
|
|
1167
|
+
lines: list[str] = []
|
|
1168
|
+
periph_kconfig = peripheral_kconfig(project.effective_metadata_root())
|
|
1169
|
+
periph_subsystems: set[str] = set()
|
|
1170
|
+
for periph in slice_.peripherals or []:
|
|
1171
|
+
periph_subsystems.update(periph_kconfig.get(periph, ()))
|
|
1172
|
+
all_subsystems = chip_subsystems | periph_subsystems
|
|
1173
|
+
if all_subsystems:
|
|
1174
|
+
lines.append(f"# Zephyr subsystems required on core "
|
|
1175
|
+
f"`{slice_.core_id}` (chip drivers + peripherals)")
|
|
1176
|
+
for s in sorted(all_subsystems):
|
|
1177
|
+
lines.append(f"CONFIG_{s}=y")
|
|
1178
|
+
lines.append("")
|
|
1179
|
+
return lines
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def _per_core_library_kconfig(
|
|
1183
|
+
lib: str,
|
|
1184
|
+
manifest: Optional[dict],
|
|
1185
|
+
metadata_root: Path) -> Optional[list[str]]:
|
|
1186
|
+
"""Base-Kconfig lines for a per-core `libraries:` token, read from the
|
|
1187
|
+
library's ADR 0018 manifest (metadata/libraries/<canonical>.yaml) rather
|
|
1188
|
+
than a hand-maintained table (WS6-c #610 §6).
|
|
1189
|
+
|
|
1190
|
+
`manifest` is the already-resolved document from the ADR-0018 library
|
|
1191
|
+
layer (`libraries.resolve_selection`); pass it so the per-core channel
|
|
1192
|
+
reads exactly what the layer validated instead of re-reading the file
|
|
1193
|
+
behind the layer's back (alplabai/tan-cli#555). Pass `None` and the
|
|
1194
|
+
token is resolved to its canonical manifest through the
|
|
1195
|
+
metadata/library-aliases-v1.json alias table (read from `metadata_root`,
|
|
1196
|
+
the SAME tree `load_board_yaml(..., metadata_root=...)` resolved this
|
|
1197
|
+
project against -- #1485) and loaded here.
|
|
1198
|
+
|
|
1199
|
+
The emitted set is the union of the manifest's
|
|
1200
|
+
`integration.zephyr.kconfig` (the upstream module-enable line(s)) and its
|
|
1201
|
+
`integration.zephyr.hw_backends.sw_fallback.kconfig` (the SDK SW floor),
|
|
1202
|
+
module-enable first and the SW-fallback marker last, deduped -- computed
|
|
1203
|
+
by `libraries.zephyr_library_kconfig`, the same derivation the
|
|
1204
|
+
project-wide channel (`libraries.zephyr_kconfig_lines`) calls, so a
|
|
1205
|
+
library's base Kconfig set cannot drift between the two declaration
|
|
1206
|
+
channels (#1359).
|
|
1207
|
+
|
|
1208
|
+
Returns None when no manifest resolves (caller emits the pending-wire TODO).
|
|
1209
|
+
"""
|
|
1210
|
+
doc = manifest
|
|
1211
|
+
if doc is None:
|
|
1212
|
+
alias = _library_alias_table(metadata_root)
|
|
1213
|
+
canonical = alias.get(lib, lib)
|
|
1214
|
+
path = metadata_root / "libraries" / f"{canonical}.yaml"
|
|
1215
|
+
if not path.is_file():
|
|
1216
|
+
return None
|
|
1217
|
+
doc = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
1218
|
+
return _library_layer.zephyr_library_kconfig(doc)
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _emit_libraries(
|
|
1222
|
+
project: "BoardProject",
|
|
1223
|
+
slice_: Slice,
|
|
1224
|
+
) -> list[str]:
|
|
1225
|
+
"""Per-core `libraries:`, curated third-party `libraries:` (ADR 0018),
|
|
1226
|
+
and the open-set `extra_libraries:` escape hatch (v0.6 P2.1).
|
|
1227
|
+
|
|
1228
|
+
Per-core Kconfig is resolved from each library's manifest
|
|
1229
|
+
(`_per_core_library_kconfig`), not a hand-maintained enable table.
|
|
1230
|
+
|
|
1231
|
+
Both the project-wide and the core-scoped declaration channels go through
|
|
1232
|
+
the SAME ADR-0018 layer: `libraries.resolve_selection(..., slice_=)` is
|
|
1233
|
+
what refuses an unknown name, a failed `requires:` constraint or a library
|
|
1234
|
+
that cannot be wired on this target, and it is resolved FIRST so that
|
|
1235
|
+
refusal lands before a single line is emitted (alplabai/tan-cli#555).
|
|
1236
|
+
"""
|
|
1237
|
+
lines: list[str] = []
|
|
1238
|
+
|
|
1239
|
+
# name -> validated manifest for every library in scope on this slice.
|
|
1240
|
+
resolved: dict[str, dict] = {}
|
|
1241
|
+
if slice_.os == "zephyr":
|
|
1242
|
+
resolved = dict(_library_layer.resolve_selection(
|
|
1243
|
+
project, project.effective_metadata_root(), slice_=slice_))
|
|
1244
|
+
|
|
1245
|
+
if slice_.libraries:
|
|
1246
|
+
lines.append(f"# Libraries declared on core "
|
|
1247
|
+
f"`{slice_.core_id}`")
|
|
1248
|
+
for lib in sorted(slice_.libraries):
|
|
1249
|
+
kcs = _per_core_library_kconfig(
|
|
1250
|
+
lib, resolved.get(lib), project.effective_metadata_root())
|
|
1251
|
+
if kcs is None:
|
|
1252
|
+
lines.append(
|
|
1253
|
+
f"# TODO: wire library '{lib}' once its v0.4 enable lands")
|
|
1254
|
+
else:
|
|
1255
|
+
for kc in kcs:
|
|
1256
|
+
lines.append(kc)
|
|
1257
|
+
lines.append("")
|
|
1258
|
+
|
|
1259
|
+
# Project-wide curated third-party libraries (top-level `libraries:`,
|
|
1260
|
+
# ADR 0018). Emitted from the metadata/libraries/<name>.yaml manifests;
|
|
1261
|
+
# resolve_selection() has already rejected any incompatible selection.
|
|
1262
|
+
# Guard keeps a project with no `libraries:` byte-identical.
|
|
1263
|
+
library_lines = _library_layer.zephyr_kconfig_lines(
|
|
1264
|
+
project, slice_, project.effective_metadata_root())
|
|
1265
|
+
if library_lines:
|
|
1266
|
+
lines.append("# Curated third-party libraries "
|
|
1267
|
+
"(project `libraries:`, ADR 0018)")
|
|
1268
|
+
lines.extend(library_lines)
|
|
1269
|
+
lines.append("")
|
|
1270
|
+
|
|
1271
|
+
# ----------------------------------------------------------------
|
|
1272
|
+
# Open-set extra_libraries (v0.6 P2.1). Each entry declares either
|
|
1273
|
+
# inline `kconfig:` lines (emitted verbatim) or a `profile:` path
|
|
1274
|
+
# to a hw-backends.yaml-style file we walk with the same silicon /
|
|
1275
|
+
# soc_family / requires_cap matcher as the curated libraries.
|
|
1276
|
+
# _validate_consistency() guarantees exactly-one of kconfig/profile
|
|
1277
|
+
# and the profile file resolves, so the per-entry checks here are
|
|
1278
|
+
# just for emit-time safety.
|
|
1279
|
+
# ----------------------------------------------------------------
|
|
1280
|
+
if slice_.extra_libraries:
|
|
1281
|
+
lines.append(f"# Extra libraries declared on core "
|
|
1282
|
+
f"`{slice_.core_id}` (open-set escape hatch)")
|
|
1283
|
+
for entry in sorted(slice_.extra_libraries,
|
|
1284
|
+
key=lambda e: e.get("name", "")):
|
|
1285
|
+
name = entry.get("name", "<unnamed>")
|
|
1286
|
+
kc_lines = entry.get("kconfig")
|
|
1287
|
+
profile = entry.get("profile")
|
|
1288
|
+
if kc_lines:
|
|
1289
|
+
lines.append(f"# extra_libraries[{name}] (inline kconfig)")
|
|
1290
|
+
for kc in kc_lines:
|
|
1291
|
+
lines.append(str(kc))
|
|
1292
|
+
elif profile:
|
|
1293
|
+
lines.append(f"# extra_libraries[{name}] (profile: {profile})")
|
|
1294
|
+
profile_lines = _emit_extra_library_profile(
|
|
1295
|
+
name, profile, project)
|
|
1296
|
+
if profile_lines:
|
|
1297
|
+
lines.extend(profile_lines)
|
|
1298
|
+
else:
|
|
1299
|
+
lines.append(
|
|
1300
|
+
f"# (no matching backend for "
|
|
1301
|
+
f"silicon={project.som_preset.get('silicon')} "
|
|
1302
|
+
f"in {profile})")
|
|
1303
|
+
lines.append("")
|
|
1304
|
+
|
|
1305
|
+
return lines
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
def _slice_wants_inference(project: "BoardProject", slice_: Slice) -> bool:
|
|
1309
|
+
"""True when this slice genuinely uses `<alp/inference.h>`.
|
|
1310
|
+
|
|
1311
|
+
Two independent signals, either sufficient: `cores.<id>.inference:`
|
|
1312
|
+
declared (app-level tuning, e.g. `default_arena_kib:` -- the signal
|
|
1313
|
+
every inference example under examples/ai, examples/audio,
|
|
1314
|
+
examples/camera-vision declares), OR the slice's `libraries:` scope
|
|
1315
|
+
(project-wide AND core-scoped, via `libraries.scoped_names` -- #1359
|
|
1316
|
+
follow-up: reading `slice_.libraries` alone missed a project-wide
|
|
1317
|
+
`libraries: [tflite-micro]` entry) pulls in `tflite-micro` (resolved
|
|
1318
|
+
through the same alias table `_per_core_library_kconfig` uses -- a
|
|
1319
|
+
`cores:`-scoped top-level `libraries:` entry folds into
|
|
1320
|
+
`slice_.libraries` at load time, see `loader.py::_normalize_libraries`).
|
|
1321
|
+
The library signal exists because `cores.<id>.inference:` is app-level
|
|
1322
|
+
TUNING, not a declaration of intent -- an app that never overrides the
|
|
1323
|
+
arena default has no reason to write the block, and #874's adversarial
|
|
1324
|
+
follow-up found exactly that false negative (`examples/camera-vision/
|
|
1325
|
+
ai-object-detection-realtime`, `libraries: tflite-micro` with no
|
|
1326
|
+
`inference:` block). A slice with NEITHER signal stays clean (no
|
|
1327
|
+
unconditional emit returns, issue #874 item 4).
|
|
1328
|
+
"""
|
|
1329
|
+
if slice_.inference:
|
|
1330
|
+
return True
|
|
1331
|
+
alias = _library_alias_table(project.effective_metadata_root())
|
|
1332
|
+
return any(alias.get(lib, lib) == "tflite-micro"
|
|
1333
|
+
for lib in _library_layer.scoped_names(project, slice_=slice_))
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
def _emit_inference(
|
|
1337
|
+
project: "BoardProject",
|
|
1338
|
+
slice_: Slice,
|
|
1339
|
+
silicon: Optional[str],
|
|
1340
|
+
) -> list[str]:
|
|
1341
|
+
"""Inference dispatcher enables, driven entirely by the SoM preset's
|
|
1342
|
+
`capabilities:` matrix + SoC-JSON `cores[]` -- NOT by board.yaml.
|
|
1343
|
+
The customer never picks *which backend* at build time; the SDK
|
|
1344
|
+
compiles in every dispatcher the silicon supports, and apps choose
|
|
1345
|
+
per-handle at runtime via alp_inference_open(.backend=...).
|
|
1346
|
+
CPU/TFLM is the universal SW fallback and is on whenever this
|
|
1347
|
+
section emits at all.
|
|
1348
|
+
|
|
1349
|
+
*Whether* the section emits anything is gated on `_slice_wants_inference`
|
|
1350
|
+
(declared `cores.<id>.inference:` tuning OR a `libraries:` pull of
|
|
1351
|
+
`tflite-micro`). Emitting the backend switches unconditionally on
|
|
1352
|
+
every zephyr slice used to put a misleading `=y` line (silently
|
|
1353
|
+
resolving to n -- BACKEND_TFLM's `depends on TENSORFLOW_LITE_MICRO &&
|
|
1354
|
+
CPP` are genuinely-external symbols not every slice's prj.conf sets)
|
|
1355
|
+
in front of apps that never call alp_inference_open() at all
|
|
1356
|
+
(issue #874).
|
|
1357
|
+
|
|
1358
|
+
Two layers of variant detail are emitted alongside the umbrella
|
|
1359
|
+
switches so the SDK driver code + (upstream) kernel libraries
|
|
1360
|
+
compile against the right per-silicon variant, each only alongside
|
|
1361
|
+
the ALP_SDK_* parent it `depends on` in
|
|
1362
|
+
zephyr/kconfigs/iot-audio-inference.kconfig (issue #874 item 3):
|
|
1363
|
+
|
|
1364
|
+
- CONFIG_ALP_SDK_INFERENCE_ETHOS_U_VARIANT_{U55,U85}=y -- derived
|
|
1365
|
+
from the silicon capability counts (ethos_u{55,85}_count, resolved
|
|
1366
|
+
from the SoC JSON npus[]), the single source for which NPUs the
|
|
1367
|
+
part carries. U85 carries Arm's larger MAC array + TensorOptimized
|
|
1368
|
+
kernels; U55 carries the smaller MAC + reference kernels.
|
|
1369
|
+
U55/U85 depend on BACKEND_ETHOS_U_AEN.
|
|
1370
|
+
|
|
1371
|
+
- CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_{NEON,HELIUM,REF}=y -- picked
|
|
1372
|
+
from the SoC JSON's `cores[<slice.core_id>].vector_extension`
|
|
1373
|
+
so the CPU-side TFLM kernel set matches the target core's SIMD
|
|
1374
|
+
reality (NEON on A-cluster, Helium MVE on M55, scalar / REF
|
|
1375
|
+
on baseline M33 / M4). Per-core in case a single SoM hosts
|
|
1376
|
+
multiple core classes (E7 = A32 + M55, all three Helium /
|
|
1377
|
+
Neon flavours). All three depend on BACKEND_TFLM.
|
|
1378
|
+
"""
|
|
1379
|
+
if not _slice_wants_inference(project, slice_):
|
|
1380
|
+
return []
|
|
1381
|
+
|
|
1382
|
+
lines: list[str] = []
|
|
1383
|
+
|
|
1384
|
+
# resolve_capabilities merges SoC-JSON defaults with SoM overrides
|
|
1385
|
+
# so silicon-determined caps (ethos_u55_count, drp_ai, ...) resolve
|
|
1386
|
+
# even when removed from the SoM YAML (capability unification, slice 3b).
|
|
1387
|
+
capabilities = resolve_capabilities(project.som_preset, project.effective_metadata_root())
|
|
1388
|
+
inference_lines: list[str] = [
|
|
1389
|
+
# BACKEND_TFLM depends on TENSORFLOW_LITE_MICRO && CPP -- genuinely
|
|
1390
|
+
# external (upstream Zephyr / tflite-micro module) symbols, not
|
|
1391
|
+
# ALP_SDK_*. west.yml pulls the tflite-micro module in
|
|
1392
|
+
# unconditionally (no west group gate; see west.yml's
|
|
1393
|
+
# name-allowlist), so both resolve here rather than requiring every
|
|
1394
|
+
# inference app to hand-write them in its own prj.conf (#874; see
|
|
1395
|
+
# also #855 for cross-alp-sdk-version symbol skew). This is
|
|
1396
|
+
# effective on an alp-sdk-topdir west workspace (west.yml's own
|
|
1397
|
+
# manifest); a workspace initialised off a bare upstream
|
|
1398
|
+
# `zephyr` manifest (the repo's own twister CI, a plain
|
|
1399
|
+
# `west init -m .../zephyr`) has no tflite-micro module at all,
|
|
1400
|
+
# so TENSORFLOW_LITE_MICRO degrades gracefully to n (a Kconfig
|
|
1401
|
+
# "assigned y but got n" warning, not fatal -- the inference
|
|
1402
|
+
# dispatcher falls back to ALP_SDK_INFERENCE_SW_FALLBACK there).
|
|
1403
|
+
"CONFIG_CPP=y",
|
|
1404
|
+
# tflite-micro's headers require C++17 (e.g. tensorflow/lite/array.h
|
|
1405
|
+
# uses std::conditional_t / std::decay_t). Zephyr's C++ standard
|
|
1406
|
+
# `choice STD_CPP` defaults to C++11, so a TFLM build compiled at
|
|
1407
|
+
# -std=c++11 fails. Emit C++17 here whenever inference (TFLM) is
|
|
1408
|
+
# wanted so every inference project builds without hand-editing:
|
|
1409
|
+
# real NPU apps previously set CONFIG_STD_CPP17=y in their own
|
|
1410
|
+
# prj.conf; the emit now guarantees it on every platform (native_sim
|
|
1411
|
+
# + real silicon alike), matching what the AEN NPU builds already use.
|
|
1412
|
+
"CONFIG_STD_CPP17=y",
|
|
1413
|
+
"CONFIG_TENSORFLOW_LITE_MICRO=y",
|
|
1414
|
+
"CONFIG_ALP_SDK_INFERENCE_BACKEND_TFLM=y",
|
|
1415
|
+
]
|
|
1416
|
+
|
|
1417
|
+
# ---- G-2 -- CPU-class TFLM kernel selector --------------------
|
|
1418
|
+
# Look up this slice's core in the SoC spec; pick exactly one of
|
|
1419
|
+
# NEON / HELIUM / REF based on the vector_extension field. Defaults
|
|
1420
|
+
# to REF when the SoC JSON is silent (paper-correct on the scalar
|
|
1421
|
+
# M33s -- V2N m33_sm).
|
|
1422
|
+
#
|
|
1423
|
+
# `c.get("type")` is UNVALIDATED against `soc-spec-v1.schema.json`'s own
|
|
1424
|
+
# `"type": {"type": "string"}` -- the identical gap `presets_cmd.
|
|
1425
|
+
# core_type_lookup` and `topology.core_os_topology` closed for
|
|
1426
|
+
# tan-cli#957, same class, this call site missed by that sweep
|
|
1427
|
+
# (tan-cli#962): a schema-invalid SoC JSON (hand-authored, mid-
|
|
1428
|
+
# `porting-a-new-som`, or corrupted) with a truthy non-string `type`
|
|
1429
|
+
# reaches `.lower()` here and raises `AttributeError`, aborting `tan
|
|
1430
|
+
# build` for the whole slice on any board that reaches this branch
|
|
1431
|
+
# (`_slice_wants_inference` -- examples/ai, examples/audio,
|
|
1432
|
+
# examples/camera-vision). `isinstance`-guard to the same `""`
|
|
1433
|
+
# unresolved sentinel a missing `type` already produces, matching
|
|
1434
|
+
# `core_type_lookup`'s `c["type"] if isinstance(c.get("type"), str)
|
|
1435
|
+
# else ""`.
|
|
1436
|
+
tflm_kernel_kc: str = "CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_REF=y"
|
|
1437
|
+
for c in (project.soc_spec.get("cores") or []):
|
|
1438
|
+
if c.get("id") != slice_.core_id:
|
|
1439
|
+
continue
|
|
1440
|
+
raw_vec = c.get("vector_extension")
|
|
1441
|
+
vec = raw_vec.lower() if isinstance(raw_vec, str) else ""
|
|
1442
|
+
raw_type = c.get("type")
|
|
1443
|
+
ctype = raw_type.lower() if isinstance(raw_type, str) else ""
|
|
1444
|
+
if vec == "neon" or ctype.startswith("cortex-a"):
|
|
1445
|
+
tflm_kernel_kc = "CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_NEON=y"
|
|
1446
|
+
elif vec == "helium":
|
|
1447
|
+
tflm_kernel_kc = "CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_HELIUM=y"
|
|
1448
|
+
else:
|
|
1449
|
+
tflm_kernel_kc = "CONFIG_ALP_SDK_INFERENCE_TFLM_KERNEL_REF=y"
|
|
1450
|
+
break
|
|
1451
|
+
inference_lines.append(tflm_kernel_kc)
|
|
1452
|
+
|
|
1453
|
+
# ---- G-1 -- per-variant Ethos-U selector ---------------------
|
|
1454
|
+
# Which Ethos-U variants this SoM carries -- derived from the
|
|
1455
|
+
# silicon-determined capability counts (ethos_u{55,85}_count, resolved
|
|
1456
|
+
# from the SoC JSON npus[] via resolve_capabilities). This is the single
|
|
1457
|
+
# source: an on-die NPU cannot be depopulated at the SoM level, so the SoM
|
|
1458
|
+
# preset does NOT restate the variant list.
|
|
1459
|
+
ethos_variants: set[str] = set()
|
|
1460
|
+
if (capabilities.get("ethos_u55_count") or 0) > 0:
|
|
1461
|
+
ethos_variants.add("u55")
|
|
1462
|
+
if (capabilities.get("ethos_u85_count") or 0) > 0:
|
|
1463
|
+
ethos_variants.add("u85")
|
|
1464
|
+
ethos_present = bool(ethos_variants)
|
|
1465
|
+
if ethos_present:
|
|
1466
|
+
# Per-silicon Ethos-U backend (Slice 3 registry layout):
|
|
1467
|
+
# Alif Ensemble (AEN) -> _BACKEND_ETHOS_U_AEN
|
|
1468
|
+
# Parent-gating (#874 item 3): the variant switches `depend on` the
|
|
1469
|
+
# parent backend (zephyr/kconfigs/iot-audio-inference.kconfig).
|
|
1470
|
+
inference_lines.append("CONFIG_ALP_SDK_INFERENCE_BACKEND_ETHOS_U_AEN=y")
|
|
1471
|
+
allowed_variants = {"u55", "u85"}
|
|
1472
|
+
for v in sorted(ethos_variants & allowed_variants):
|
|
1473
|
+
inference_lines.append(f"CONFIG_ALP_SDK_INFERENCE_ETHOS_U_VARIANT_{v.upper()}=y")
|
|
1474
|
+
# Real Arm Ethos-U driver config -- the silicon-proven pair (bench:
|
|
1475
|
+
# E1M-AEN801/E8, person_detect U85). BACKEND_ETHOS_U_AEN already
|
|
1476
|
+
# `select ETHOS_U if DT_HAS_ARM_ETHOS_U_ENABLED` (silicon-only); these
|
|
1477
|
+
# assignments complete the driver config. The core driver compiles
|
|
1478
|
+
# exactly ONE accelerator config (ETHOS_U_NPU_CONFIG is a Kconfig
|
|
1479
|
+
# `choice`), so pick the single most-capable NPU this silicon carries.
|
|
1480
|
+
# On native_sim (no arm,ethos-u node) ETHOS_U stays off, so the
|
|
1481
|
+
# ETHOS_U*-dependent lines below no-op with a harmless Kconfig
|
|
1482
|
+
# "assigned y but got n" warning (same pattern as TENSORFLOW_LITE_MICRO
|
|
1483
|
+
# above). CONFIG_ETHOS_U_DCACHE=y links the correct-signature dcache
|
|
1484
|
+
# hooks (NOT CONFIG_ARM_ETHOS_U -- hal_alif's stale callback path);
|
|
1485
|
+
# CONFIG_DCACHE=n is the CPU<->NPU SRAM coherence mechanism; the
|
|
1486
|
+
# ethos_u driver's mutex/semaphore need a kernel heap (k_malloc).
|
|
1487
|
+
# Pick the most-capable variant this silicon carries (U85 > U55),
|
|
1488
|
+
# then read its MAC config from the SoC's npus[] `mac_per_cycle` -- NOT a
|
|
1489
|
+
# hardcode. The derived symbol must be a real ETHOS_U_NPU_CONFIG choice
|
|
1490
|
+
# member (hal_ethos_u), else it would silently no-op -- so validate and
|
|
1491
|
+
# fail loudly on a metadata mismatch.
|
|
1492
|
+
if "u85" in ethos_variants:
|
|
1493
|
+
variant_num = "85"
|
|
1494
|
+
else:
|
|
1495
|
+
variant_num = "55"
|
|
1496
|
+
npu_type = f"ethos-u{variant_num}"
|
|
1497
|
+
npus_of_type = [
|
|
1498
|
+
n for n in (project.soc_spec.get("npus") or [])
|
|
1499
|
+
if str(n.get("type", "")).lower() == npu_type and n.get("mac_per_cycle")
|
|
1500
|
+
]
|
|
1501
|
+
# Size the accelerator for the NPU instance THIS slice's core actually
|
|
1502
|
+
# drives, not the SoC's most-capable one. A part can carry two U55s of
|
|
1503
|
+
# the same variant but different MACs wired to different cores (E3/E5/E7:
|
|
1504
|
+
# a 256-MAC high-perf U55 on M55-HP + a 128-MAC high-efficiency U55 on
|
|
1505
|
+
# M55-HE -- npus[].paired_core in the SoC JSON). A 256-MAC command
|
|
1506
|
+
# stream errors a 128-MAC NPU at invoke (register-proven on E8), so a
|
|
1507
|
+
# blind max() would mis-size the HE slice. Prefer the core-paired
|
|
1508
|
+
# instance; fall back to the most-capable of the variant when the chosen
|
|
1509
|
+
# variant is not core-paired (the E8 U85, a shared SoC-level NPU --
|
|
1510
|
+
# Alif block name NPU_HG) or the SoC JSON predates paired_core.
|
|
1511
|
+
paired = [n["mac_per_cycle"] for n in npus_of_type
|
|
1512
|
+
if n.get("paired_core") == slice_.core_id]
|
|
1513
|
+
macs = [n["mac_per_cycle"] for n in npus_of_type]
|
|
1514
|
+
if paired:
|
|
1515
|
+
mac = paired[0]
|
|
1516
|
+
elif len({*macs}) > 1:
|
|
1517
|
+
# This variant has >1 distinct MAC array on the SoC but none is
|
|
1518
|
+
# paired_core=<this slice's core> -- a silent max() here would
|
|
1519
|
+
# re-introduce the #909 mis-sizing (a 256-MAC command stream errors
|
|
1520
|
+
# a 128-MAC NPU at invoke). Fail loudly, like the _valid_accel
|
|
1521
|
+
# guard below, so a missing/typo'd paired_core is caught at emit.
|
|
1522
|
+
raise ValueError(
|
|
1523
|
+
f"{silicon}: inference slice core {slice_.core_id!r} drives an "
|
|
1524
|
+
f"{npu_type} but the SoC JSON declares MAC variants {sorted({*macs})} "
|
|
1525
|
+
f"and none sets paired_core={slice_.core_id!r} -- add "
|
|
1526
|
+
f"npus[].paired_core so the build sizes the accelerator for this "
|
|
1527
|
+
f"core instead of guessing (see #909).")
|
|
1528
|
+
else:
|
|
1529
|
+
# Single MAC for this variant (or npus[]-less older spec): unambiguous.
|
|
1530
|
+
mac = macs[0] if macs else 256
|
|
1531
|
+
accel = f"ETHOS_U{variant_num}_{mac}"
|
|
1532
|
+
_valid_accel = {
|
|
1533
|
+
"ETHOS_U55_64", "ETHOS_U55_128", "ETHOS_U55_256",
|
|
1534
|
+
"ETHOS_U85_128", "ETHOS_U85_256", "ETHOS_U85_512",
|
|
1535
|
+
"ETHOS_U85_1024", "ETHOS_U85_2048",
|
|
1536
|
+
}
|
|
1537
|
+
if accel not in _valid_accel:
|
|
1538
|
+
raise ValueError(
|
|
1539
|
+
f"{silicon}: derived Ethos-U accelerator {accel!r} "
|
|
1540
|
+
f"(npus[].mac_per_cycle={mac} for {npu_type}) is not a valid "
|
|
1541
|
+
f"ETHOS_U_NPU_CONFIG choice member -- check the SoC JSON npus[]")
|
|
1542
|
+
inference_lines.append("CONFIG_ETHOS_U_DCACHE=y")
|
|
1543
|
+
inference_lines.append(f"CONFIG_{accel}=y")
|
|
1544
|
+
inference_lines.append("CONFIG_DCACHE=n")
|
|
1545
|
+
inference_lines.append("CONFIG_HEAP_MEM_POOL_SIZE=65536")
|
|
1546
|
+
# DRP-AI3 and DEEPX DX-M1 have NO Zephyr Kconfig -- deliberately.
|
|
1547
|
+
# Both engines are A55/Linux-side only (DRP-AI3 via the MERA/TVM
|
|
1548
|
+
# userspace runtime, DX-M1 via libdxrt over the A55's PCIe); an
|
|
1549
|
+
# M-class Zephyr slice cannot drive either (issues #58/#59), so it
|
|
1550
|
+
# gets TFLM only. Their build wiring lives on the cmake-args /
|
|
1551
|
+
# Yocto emit paths (_slice_cmake_args below).
|
|
1552
|
+
# SoM-declared AUTO accelerator order for the .alpmodel tiebreak
|
|
1553
|
+
# (zephyr/CMakeLists.txt maps it to -DALP_SDK_INFERENCE_AUTO_ORDER, the
|
|
1554
|
+
# same define name the Yocto and baremetal builds use).
|
|
1555
|
+
auto_order = _inference_auto_order(project.som_preset)
|
|
1556
|
+
if auto_order:
|
|
1557
|
+
inference_lines.append(
|
|
1558
|
+
f'CONFIG_ALP_SDK_INFERENCE_AUTO_ORDER="{",".join(auto_order)}"')
|
|
1559
|
+
lines.append("# Inference dispatchers (from SoM capabilities -- "
|
|
1560
|
+
"customer does not pick)")
|
|
1561
|
+
lines.extend(inference_lines)
|
|
1562
|
+
lines.append("")
|
|
1563
|
+
return lines
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def _emit_cross_core_shmem_cache(
|
|
1567
|
+
project: "BoardProject",
|
|
1568
|
+
slice_: Slice,
|
|
1569
|
+
existing_lines: list[str],
|
|
1570
|
+
) -> list[str]:
|
|
1571
|
+
"""CONFIG_DCACHE=n for a slice sharing a non-cacheable `raw_shmem` or
|
|
1572
|
+
`rpmsg` IPC carve-out with a peer core -- the hardware fact this symbol
|
|
1573
|
+
encodes (each core has its own D-cache; shared SRAM is cross-core
|
|
1574
|
+
incoherent unless the cache is off, or the app does explicit cache
|
|
1575
|
+
maintenance) is NOT specific to Ethos-U inference. Before this, the
|
|
1576
|
+
only place that emitted it was the Ethos-U branch of `_emit_inference`,
|
|
1577
|
+
so a non-inference cross-core project (e.g. `examples/multicore/
|
|
1578
|
+
mproc-mailbox`, an M55-HP<->M55-HE mailbox+shmem roundtrip with no
|
|
1579
|
+
`inference:` block) got no help from the generator and had to
|
|
1580
|
+
hand-write the line in `prj.conf` -- see alp-sdk PR #1080, which found
|
|
1581
|
+
three examples silently reporting `RESULT SKIP` while the debug AP
|
|
1582
|
+
watched their beacons advance: the hardware worked, the stale D-cache
|
|
1583
|
+
just hid it from the software.
|
|
1584
|
+
|
|
1585
|
+
Board-agnostic and keyed off the existing `ipc:` schema field (board-
|
|
1586
|
+
yaml §3.9), not a SoC/vendor check, so it fires for any silicon that
|
|
1587
|
+
declares a matching carve-out:
|
|
1588
|
+
|
|
1589
|
+
- `kind: rpmsg` is covered too (alp-sdk #1088's conservative fix),
|
|
1590
|
+
for the identical reason `raw_shmem` is: `<alp/rpc.h>` has no
|
|
1591
|
+
cache-maintenance layer (no `sys_cache_*` call anywhere under
|
|
1592
|
+
`src/` or `include/`, and no per-channel cache field). A
|
|
1593
|
+
`cacheable: true` rpmsg channel would therefore select a path with
|
|
1594
|
+
no maintenance behind it, so
|
|
1595
|
+
`loader.py` rejects `cacheable: true` on a `rpmsg` entry outright
|
|
1596
|
+
rather than silently honouring it -- any entry that reaches this
|
|
1597
|
+
function is already non-cacheable, and the D-cache goes off
|
|
1598
|
+
unconditionally for its endpoints. The real fix --
|
|
1599
|
+
`sys_cache_data_flush_range` / `sys_cache_data_invd_range` in
|
|
1600
|
+
`<alp/rpc.h>` -- remains open; see alp-sdk #1088.
|
|
1601
|
+
- `kind: mailbox_only` is excluded -- it carries no shared memory
|
|
1602
|
+
(doorbell + signal only), so there is nothing to keep coherent.
|
|
1603
|
+
- `kind: raw_shmem` closes the original #1080 case: `<alp/mproc.h>`'s
|
|
1604
|
+
raw shmem+mailbox primitives have no cache-maintenance layer
|
|
1605
|
+
either, so the SDK's zero-effort-safe default is to turn the
|
|
1606
|
+
D-cache off for every endpoint -- unless the entry opts out with
|
|
1607
|
+
an explicit `cacheable: true`, which is the app declaring it will
|
|
1608
|
+
do its own cache ops instead (board.schema.json
|
|
1609
|
+
`ipc_entry.cacheable`). This opt-out is `raw_shmem`-only; `rpmsg`
|
|
1610
|
+
has no opt-out (see above).
|
|
1611
|
+
|
|
1612
|
+
Must run after `_emit_inference` in `_slice_alp_conf` (the dedup below
|
|
1613
|
+
depends on that order, it does not detect the reverse case): skips
|
|
1614
|
+
emission if a line asserting the same symbol is already present, i.e.
|
|
1615
|
+
the Ethos-U inference branch already asserted it for this slice --
|
|
1616
|
+
kept as its own check rather than threading a bool out of
|
|
1617
|
+
`_emit_inference`, since the two triggers (Ethos-U NPU coherence vs.
|
|
1618
|
+
mproc shmem coherence) are unrelated hardware facts that happen to
|
|
1619
|
+
share one Kconfig symbol.
|
|
1620
|
+
"""
|
|
1621
|
+
if any("CONFIG_DCACHE=n" in line for line in existing_lines):
|
|
1622
|
+
return []
|
|
1623
|
+
needs_dcache_off = any(
|
|
1624
|
+
entry.kind in ("raw_shmem", "rpmsg")
|
|
1625
|
+
and slice_.core_id in entry.endpoints
|
|
1626
|
+
and not entry.cacheable
|
|
1627
|
+
for entry in project.ipc
|
|
1628
|
+
)
|
|
1629
|
+
if not needs_dcache_off:
|
|
1630
|
+
return []
|
|
1631
|
+
return [
|
|
1632
|
+
"# board.yaml ipc[].kind: raw_shmem or rpmsg names this core as",
|
|
1633
|
+
"# an endpoint with no explicit cacheable: true opt-out.",
|
|
1634
|
+
"# <alp/mproc.h> (raw_shmem) and <alp/rpc.h> (rpmsg) both have no",
|
|
1635
|
+
"# cache-maintenance layer, so the D-cache must stay off (not",
|
|
1636
|
+
"# just the carve-out's own region) for both sides to see each",
|
|
1637
|
+
"# other's writes without explicit clean/invalidate (PR #1080,",
|
|
1638
|
+
"# #1088).",
|
|
1639
|
+
"CONFIG_DCACHE=n",
|
|
1640
|
+
"",
|
|
1641
|
+
]
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def _emit_memory(slice_: Slice) -> list[str]:
|
|
1645
|
+
"""Per-slice memory tuning (board.yaml `cores.<id>.memory:`)."""
|
|
1646
|
+
lines: list[str] = []
|
|
1647
|
+
mem = slice_.memory or {}
|
|
1648
|
+
mem_lines: list[str] = []
|
|
1649
|
+
if mem.get("stack_kib"):
|
|
1650
|
+
mem_lines.append(f"CONFIG_MAIN_STACK_SIZE={int(mem['stack_kib']) * 1024}")
|
|
1651
|
+
if mem.get("isr_stack_kib"):
|
|
1652
|
+
mem_lines.append(f"CONFIG_ISR_STACK_SIZE={int(mem['isr_stack_kib']) * 1024}")
|
|
1653
|
+
if mem.get("heap_kib") is not None:
|
|
1654
|
+
mem_lines.append(f"CONFIG_HEAP_MEM_POOL_SIZE={int(mem['heap_kib']) * 1024}")
|
|
1655
|
+
if mem_lines:
|
|
1656
|
+
lines.append("# Per-slice memory tuning (board.yaml cores.<id>.memory:)")
|
|
1657
|
+
lines.extend(mem_lines)
|
|
1658
|
+
lines.append("")
|
|
1659
|
+
return lines
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
def _emit_power(slice_: Slice) -> list[str]:
|
|
1663
|
+
"""Per-slice power-management profile (board.yaml `cores.<id>.power:`)."""
|
|
1664
|
+
lines: list[str] = []
|
|
1665
|
+
pwr = slice_.power or {}
|
|
1666
|
+
pwr_lines: list[str] = []
|
|
1667
|
+
sleep_mode = (pwr.get("sleep_mode") or "disabled").lower()
|
|
1668
|
+
if sleep_mode != "disabled":
|
|
1669
|
+
pwr_lines.append("CONFIG_PM=y")
|
|
1670
|
+
pwr_lines.append("CONFIG_PM_DEVICE=y")
|
|
1671
|
+
# PM_STATE_* selection is silicon-family-specific; lowest-power
|
|
1672
|
+
# state the hierarchy keeps is mirrored in a single hint flag.
|
|
1673
|
+
pwr_lines.append(f"# Sleep target: {sleep_mode}")
|
|
1674
|
+
for wake in (pwr.get("wakeup_sources") or []):
|
|
1675
|
+
# Wake-source declarations land as hint comments only. Zephyr
|
|
1676
|
+
# marks a device as wakeup-capable via the DT `wakeup-source;`
|
|
1677
|
+
# property + a runtime pm_device_wakeup_enable() call; there is
|
|
1678
|
+
# no top-level CONFIG_PM_DEVICE_WAKE_<SUBSYS> Kconfig symbol, so
|
|
1679
|
+
# emitting one trips the build with `undefined symbol` warnings
|
|
1680
|
+
# (-> kconfig errors -> twister CMake build failure). The real
|
|
1681
|
+
# DT-overlay + runtime-enable plumbing lands per silicon in v0.7;
|
|
1682
|
+
# until then the wake-source list is documented in the generated
|
|
1683
|
+
# alp.conf for the customer to wire by hand.
|
|
1684
|
+
if isinstance(wake, str) and not wake.startswith("E1M_"):
|
|
1685
|
+
pwr_lines.append(
|
|
1686
|
+
f"# wakeup source: {wake} "
|
|
1687
|
+
"(DT wakeup-source; + pm_device_wakeup_enable() pending v0.7)")
|
|
1688
|
+
elif isinstance(wake, str):
|
|
1689
|
+
pwr_lines.append(f"# wakeup source: {wake} (per-silicon Kconfig pending)")
|
|
1690
|
+
if pwr_lines:
|
|
1691
|
+
lines.append("# Per-slice power-management profile (board.yaml cores.<id>.power:)")
|
|
1692
|
+
lines.extend(pwr_lines)
|
|
1693
|
+
lines.append("")
|
|
1694
|
+
return lines
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def _emit_storage(project: "BoardProject") -> list[str]:
|
|
1698
|
+
"""Storage partitions (board.yaml `storage:` block). Emits per-fs
|
|
1699
|
+
Kconfig for every resolved (non-blocked) partition. Blocked
|
|
1700
|
+
partitions produce a hint comment so the slice build still
|
|
1701
|
+
compiles but the runtime mount fails loudly via the DTS overlay.
|
|
1702
|
+
|
|
1703
|
+
Per-slice ownership of partitions is not modelled today: every
|
|
1704
|
+
Zephyr/baremetal slice gets the union. Kconfig dedupes when
|
|
1705
|
+
multiple per-core fragments overlay onto the same base. v0.7
|
|
1706
|
+
may add `core:` to storage entries for per-slice scoping.
|
|
1707
|
+
"""
|
|
1708
|
+
lines: list[str] = []
|
|
1709
|
+
partitions = resolve_storage_partitions(project)
|
|
1710
|
+
ok_partitions = [p for p in partitions if p.status == "ok"]
|
|
1711
|
+
fs_seen: set[str] = set()
|
|
1712
|
+
for p in ok_partitions:
|
|
1713
|
+
fs_seen.add(p.fs)
|
|
1714
|
+
fs_kconfig: list[str] = []
|
|
1715
|
+
if "littlefs" in fs_seen:
|
|
1716
|
+
fs_kconfig.append("CONFIG_FILE_SYSTEM=y")
|
|
1717
|
+
fs_kconfig.append("CONFIG_FILE_SYSTEM_LITTLEFS=y")
|
|
1718
|
+
if "fat" in fs_seen:
|
|
1719
|
+
fs_kconfig.append("CONFIG_FILE_SYSTEM=y")
|
|
1720
|
+
fs_kconfig.append("CONFIG_FAT_FILESYSTEM_ELM=y")
|
|
1721
|
+
if "ext4" in fs_seen:
|
|
1722
|
+
fs_kconfig.append("CONFIG_FILE_SYSTEM=y")
|
|
1723
|
+
fs_kconfig.append("CONFIG_FILE_SYSTEM_EXT2=y")
|
|
1724
|
+
if fs_kconfig or ok_partitions:
|
|
1725
|
+
lines.append("# Storage partitions (board.yaml `storage:`)")
|
|
1726
|
+
# FILE_SYSTEM=y appears twice in the list when multiple fs
|
|
1727
|
+
# types are enabled; dedupe before emit.
|
|
1728
|
+
for kc in sorted(set(fs_kconfig)):
|
|
1729
|
+
lines.append(kc)
|
|
1730
|
+
# Per-partition LITTLEFS partition labels surface as a hint
|
|
1731
|
+
# comment only. Modern Zephyr does NOT define a per-partition
|
|
1732
|
+
# CONFIG_FS_LITTLEFS_PARTITION_<NAME> Kconfig -- the partition
|
|
1733
|
+
# gets matched via the DT `fixed-partitions` node + the
|
|
1734
|
+
# chosen `zephyr,storage-partition` / FIXED_PARTITION_ID()
|
|
1735
|
+
# macro at runtime. Emitting a Kconfig stem trips Zephyr's
|
|
1736
|
+
# "assignment to undefined symbol" warning + aborts the build.
|
|
1737
|
+
for p in ok_partitions:
|
|
1738
|
+
if p.fs == "littlefs":
|
|
1739
|
+
lines.append(
|
|
1740
|
+
f"# partition[{p.name}] -> mount at runtime via "
|
|
1741
|
+
f"FIXED_PARTITION_ID({p.name}_partition)")
|
|
1742
|
+
blocked = [p for p in partitions if p.status == "blocked"]
|
|
1743
|
+
for p in blocked:
|
|
1744
|
+
lines.append(f"# BLOCKED storage[{p.name}]: "
|
|
1745
|
+
f"{p.reason or 'unknown reason'}")
|
|
1746
|
+
lines.append("")
|
|
1747
|
+
return lines
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def _split_server_url(url: str) -> tuple[str, Optional[int], Optional[str]]:
|
|
1751
|
+
"""Decompose an `ota.server.url` into (host, port, scheme).
|
|
1752
|
+
|
|
1753
|
+
board.yaml's `ota.server.url` is a full URI (`board.schema.json` types it
|
|
1754
|
+
`format: uri`). Zephyr's Hawkbit client wants the three parts SEPARATELY
|
|
1755
|
+
-- a bare host for `zsock_getaddrinfo()` / the TLS SNI name / the HTTP
|
|
1756
|
+
`Host:` header, an int port, and a TLS bool. Handing it the whole URI is
|
|
1757
|
+
a DNS lookup that can never resolve (alplabai/tan-cli#558).
|
|
1758
|
+
|
|
1759
|
+
A value carrying no `://` is taken as an already-bare host and returned
|
|
1760
|
+
verbatim. A `${VAR}` placeholder passes through here too, but
|
|
1761
|
+
`_slice_alp_conf` then refuses it: nothing substitutes a placeholder in a
|
|
1762
|
+
Kconfig fragment (issue #2696, `_refuse_live_kconfig_placeholders`). Host
|
|
1763
|
+
case is preserved (DNS is case-insensitive), so this parses the authority
|
|
1764
|
+
by hand rather than through `urlsplit`, whose `.hostname` lowercases.
|
|
1765
|
+
|
|
1766
|
+
Raises OrchestratorError on anything that cannot be expressed as those
|
|
1767
|
+
three parts rather than emitting a value the client cannot use.
|
|
1768
|
+
"""
|
|
1769
|
+
raw = url.strip()
|
|
1770
|
+
if "://" not in raw:
|
|
1771
|
+
return raw, None, None
|
|
1772
|
+
scheme, _, rest = raw.partition("://")
|
|
1773
|
+
scheme = scheme.lower()
|
|
1774
|
+
if scheme not in ("http", "https"):
|
|
1775
|
+
raise OrchestratorError(
|
|
1776
|
+
f"ota.server.url `{url}`: scheme `{scheme}` is not supported by "
|
|
1777
|
+
f"the Zephyr Hawkbit DDI client -- use http:// or https://")
|
|
1778
|
+
authority, sep, path = rest.partition("/")
|
|
1779
|
+
if sep and path not in ("", "/"):
|
|
1780
|
+
raise OrchestratorError(
|
|
1781
|
+
f"ota.server.url `{url}`: the Hawkbit DDI client builds its own "
|
|
1782
|
+
f"request paths (/{{tenant}}/controller/v1/...) and has no knob "
|
|
1783
|
+
f"for a base path, so `/{path}` cannot be honoured -- give the "
|
|
1784
|
+
f"scheme, host and port only")
|
|
1785
|
+
if "@" in authority:
|
|
1786
|
+
raise OrchestratorError(
|
|
1787
|
+
f"ota.server.url `{url}`: userinfo in the URL is not supported -- "
|
|
1788
|
+
f"Hawkbit authenticates with a DDI security token "
|
|
1789
|
+
f"(`ota.server.tenant`), not URL credentials")
|
|
1790
|
+
port: Optional[int] = None
|
|
1791
|
+
host = authority
|
|
1792
|
+
if authority.startswith("["): # bracketed IPv6 literal
|
|
1793
|
+
close = authority.find("]")
|
|
1794
|
+
if close < 0:
|
|
1795
|
+
raise OrchestratorError(
|
|
1796
|
+
f"ota.server.url `{url}`: unterminated IPv6 literal in the host")
|
|
1797
|
+
host = authority[:close + 1]
|
|
1798
|
+
tail = authority[close + 1:]
|
|
1799
|
+
if tail.startswith(":"):
|
|
1800
|
+
port_text = tail[1:]
|
|
1801
|
+
elif tail:
|
|
1802
|
+
raise OrchestratorError(
|
|
1803
|
+
f"ota.server.url `{url}`: unexpected `{tail}` after the "
|
|
1804
|
+
f"IPv6 host literal")
|
|
1805
|
+
else:
|
|
1806
|
+
port_text = ""
|
|
1807
|
+
else:
|
|
1808
|
+
host, _, port_text = authority.partition(":")
|
|
1809
|
+
if port_text:
|
|
1810
|
+
if not port_text.isdigit() or not 1 <= int(port_text) <= 65535:
|
|
1811
|
+
raise OrchestratorError(
|
|
1812
|
+
f"ota.server.url `{url}`: `{port_text}` is not a valid TCP port")
|
|
1813
|
+
port = int(port_text)
|
|
1814
|
+
if not host:
|
|
1815
|
+
raise OrchestratorError(
|
|
1816
|
+
f"ota.server.url `{url}`: no host to resolve")
|
|
1817
|
+
if port is None:
|
|
1818
|
+
port = 443 if scheme == "https" else 80
|
|
1819
|
+
return host, port, scheme
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def _hawkbit_server_lines(url: str) -> list[str]:
|
|
1823
|
+
"""`CONFIG_HAWKBIT_{SERVER,PORT,USE_TLS}` for one `ota.server.url`.
|
|
1824
|
+
|
|
1825
|
+
Grounded in the pinned Zephyr v4.4.1 tree:
|
|
1826
|
+
* `HAWKBIT_SERVER` -- "User address for the hawkbit server"
|
|
1827
|
+
(subsys/mgmt/hawkbit/Kconfig:68): bare host, fed to
|
|
1828
|
+
`zsock_getaddrinfo()`, the TLS hostname and the `Host:` header.
|
|
1829
|
+
* `HAWKBIT_PORT` -- int, `default 8080` (Kconfig:75).
|
|
1830
|
+
* `HAWKBIT_USE_TLS` -- `depends on NET_SOCKETS_SOCKOPT_TLS`
|
|
1831
|
+
(Kconfig:168), so the socket option has to be enabled alongside it
|
|
1832
|
+
or kconfiglib warns "assigned y but got n" and the Zephyr configure
|
|
1833
|
+
aborts.
|
|
1834
|
+
"""
|
|
1835
|
+
host, port, scheme = _split_server_url(url)
|
|
1836
|
+
lines = [f'CONFIG_HAWKBIT_SERVER="{host}"']
|
|
1837
|
+
if port is not None:
|
|
1838
|
+
lines.append(f"CONFIG_HAWKBIT_PORT={port}")
|
|
1839
|
+
if scheme == "https":
|
|
1840
|
+
lines.append("CONFIG_NET_SOCKETS_SOCKOPT_TLS=y")
|
|
1841
|
+
lines.append("CONFIG_HAWKBIT_USE_TLS=y")
|
|
1842
|
+
elif scheme is None:
|
|
1843
|
+
lines.append("# ota.server.url carries no scheme -- taken as a bare "
|
|
1844
|
+
"host; CONFIG_HAWKBIT_PORT / CONFIG_HAWKBIT_USE_TLS keep "
|
|
1845
|
+
"their Zephyr defaults (8080, plaintext).")
|
|
1846
|
+
return lines
|
|
1847
|
+
|
|
1848
|
+
|
|
1849
|
+
# Zephyr's CONFIG_HAWKBIT_POLL_INTERVAL is declared in MINUTES with
|
|
1850
|
+
# `range 1 43200` (subsys/mgmt/hawkbit/Kconfig:29-33) and is multiplied by
|
|
1851
|
+
# SEC_PER_MIN at runtime (subsys/mgmt/hawkbit/hawkbit.c:57).
|
|
1852
|
+
_HAWKBIT_POLL_MIN_MINUTES = 1
|
|
1853
|
+
_HAWKBIT_POLL_MAX_MINUTES = 43200
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
def _hawkbit_poll_line(poll_s: int) -> str:
|
|
1857
|
+
"""`CONFIG_HAWKBIT_POLL_INTERVAL` for one `ota.poll_interval_s`.
|
|
1858
|
+
|
|
1859
|
+
board.yaml states the interval in SECONDS; the Kconfig symbol is in
|
|
1860
|
+
MINUTES. Writing the seconds value through verbatim dilated every fleet's
|
|
1861
|
+
poll period 60x -- the schema's own 1800 s default became 1800 minutes =
|
|
1862
|
+
30 hours (alplabai/tan-cli#557). Convert, and refuse a value that cannot
|
|
1863
|
+
be expressed in whole minutes or that falls outside Zephyr's declared
|
|
1864
|
+
range, rather than emitting an out-of-range value the Zephyr configure
|
|
1865
|
+
will abort on (or, with the abort relaxed, silently replace with the
|
|
1866
|
+
default of 5).
|
|
1867
|
+
"""
|
|
1868
|
+
minutes, remainder = divmod(poll_s, 60)
|
|
1869
|
+
if remainder:
|
|
1870
|
+
raise OrchestratorError(
|
|
1871
|
+
f"ota.poll_interval_s: {poll_s} s cannot be expressed in "
|
|
1872
|
+
f"CONFIG_HAWKBIT_POLL_INTERVAL, which Zephyr declares in whole "
|
|
1873
|
+
f"MINUTES -- use {minutes * 60} or {(minutes + 1) * 60}")
|
|
1874
|
+
if not _HAWKBIT_POLL_MIN_MINUTES <= minutes <= _HAWKBIT_POLL_MAX_MINUTES:
|
|
1875
|
+
raise OrchestratorError(
|
|
1876
|
+
f"ota.poll_interval_s: {poll_s} s is {minutes} min, outside "
|
|
1877
|
+
f"CONFIG_HAWKBIT_POLL_INTERVAL's `range "
|
|
1878
|
+
f"{_HAWKBIT_POLL_MIN_MINUTES} {_HAWKBIT_POLL_MAX_MINUTES}` "
|
|
1879
|
+
f"(minutes) -- use {_HAWKBIT_POLL_MIN_MINUTES * 60} .. "
|
|
1880
|
+
f"{_HAWKBIT_POLL_MAX_MINUTES * 60} s")
|
|
1881
|
+
return f"CONFIG_HAWKBIT_POLL_INTERVAL={minutes}"
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
def _emit_ota(project: "BoardProject") -> list[str]:
|
|
1885
|
+
"""OTA Zephyr client (board.yaml `ota:` block, provider-driven dispatch).
|
|
1886
|
+
|
|
1887
|
+
Resolved design Q on the Zephyr-side OTA client (ADR 0009 follow-up):
|
|
1888
|
+
provider: mender -> Mender-MCU-client (out-of-tree, BSD-3)
|
|
1889
|
+
provider: hawkbit -> Zephyr Hawkbit DDI client (upstream)
|
|
1890
|
+
provider: mcumgr -> Zephyr MCUmgr (upstream; transport is the app's call)
|
|
1891
|
+
|
|
1892
|
+
Yocto slices keep the existing Mender server-mode dispatch in
|
|
1893
|
+
`_slice_local_conf`. This handles the Zephyr side: per-slice
|
|
1894
|
+
Kconfig that compiles the matching client in. Settings (server URL,
|
|
1895
|
+
poll interval, tenant token) thread through Kconfig string values
|
|
1896
|
+
when declared in `ota:`. A `${VAR}` placeholder may only land on a
|
|
1897
|
+
commented line here (the Mender hints): Zephyr does not expand one in a
|
|
1898
|
+
Kconfig fragment, so `_slice_alp_conf` refuses it on a live line
|
|
1899
|
+
(issue #2696).
|
|
1900
|
+
"""
|
|
1901
|
+
lines: list[str] = []
|
|
1902
|
+
ota = project.ota or {}
|
|
1903
|
+
provider = (ota.get("provider") or "").lower() if isinstance(ota, dict) else ""
|
|
1904
|
+
if provider and provider in ("mender", "hawkbit", "mcumgr"):
|
|
1905
|
+
lines.append(f"# OTA Zephyr client (board.yaml `ota.provider: {provider}`)")
|
|
1906
|
+
srv = (ota.get("server") or {}) if isinstance(ota.get("server"), dict) else {}
|
|
1907
|
+
poll = ota.get("poll_interval_s")
|
|
1908
|
+
if provider == "mender":
|
|
1909
|
+
# Mender-MCU-client. The west.yml `mender` group is NOT
|
|
1910
|
+
# active by default (the upstream mender-mcu-client
|
|
1911
|
+
# repository is pinned alongside the v0.7 OTA wiring), so
|
|
1912
|
+
# CONFIG_MENDER_* would resolve to undefined-symbol warnings
|
|
1913
|
+
# under twister and abort the build. Emit the settings as
|
|
1914
|
+
# hint comments so the configuration is visible to the
|
|
1915
|
+
# customer (and to anyone reviewing the generated alp.conf)
|
|
1916
|
+
# while keeping the build clean. When the mender west
|
|
1917
|
+
# group flips active the comments here go back to live
|
|
1918
|
+
# CONFIG_* lines in a single commit.
|
|
1919
|
+
lines.append("# Mender-MCU-client wiring is pending the v0.7 OTA module")
|
|
1920
|
+
lines.append("# (mender-mcu-client west group activation).")
|
|
1921
|
+
lines.append("# CONFIG_MENDER_MCU_CLIENT=y")
|
|
1922
|
+
if srv.get("url"):
|
|
1923
|
+
lines.append(f'# CONFIG_MENDER_SERVER_URL="{srv["url"]}"')
|
|
1924
|
+
if srv.get("tenant"):
|
|
1925
|
+
lines.append(f'# CONFIG_MENDER_TENANT_TOKEN="{srv["tenant"]}"')
|
|
1926
|
+
if ota.get("artifact_name"):
|
|
1927
|
+
lines.append(f'# CONFIG_MENDER_ARTIFACT_NAME="{ota["artifact_name"]}"')
|
|
1928
|
+
if isinstance(poll, int) and poll > 0:
|
|
1929
|
+
lines.append(f"# CONFIG_MENDER_UPDATE_POLL_INTERVAL={poll}")
|
|
1930
|
+
elif provider == "hawkbit":
|
|
1931
|
+
# Zephyr upstream's Hawkbit DDI client.
|
|
1932
|
+
lines.append("CONFIG_HAWKBIT=y")
|
|
1933
|
+
lines.append("CONFIG_HAWKBIT_SHELL=y")
|
|
1934
|
+
if srv.get("url"):
|
|
1935
|
+
lines.extend(_hawkbit_server_lines(str(srv["url"])))
|
|
1936
|
+
if isinstance(poll, int) and poll > 0:
|
|
1937
|
+
lines.append(_hawkbit_poll_line(poll))
|
|
1938
|
+
elif provider == "mcumgr":
|
|
1939
|
+
# MCUmgr is upstream; the transport (UART/BLE/UDP) stays the
|
|
1940
|
+
# app's call -- we only enable the base SMP server.
|
|
1941
|
+
lines.append("CONFIG_MCUMGR=y")
|
|
1942
|
+
lines.append("CONFIG_MCUMGR_GRP_IMG=y")
|
|
1943
|
+
lines.append("CONFIG_MCUMGR_GRP_OS=y")
|
|
1944
|
+
lines.append("# MCUmgr transport (UART/BLE/UDP) is the app's call;")
|
|
1945
|
+
lines.append("# enable the matching CONFIG_MCUMGR_TRANSPORT_* in your prj.conf.")
|
|
1946
|
+
lines.append("")
|
|
1947
|
+
return lines
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
# board.yaml `diagnostics.modules:` level -> the Zephyr log-choice suffix.
|
|
1951
|
+
# Zephyr's per-module choice is <MOD>_LOG_LEVEL_{OFF,ERR,WRN,INF,DBG,DEFAULT}
|
|
1952
|
+
# (subsys/logging/Kconfig.template.log_config); the int <MOD>_LOG_LEVEL is
|
|
1953
|
+
# PROMPTLESS and derived from that choice, so it cannot be assigned from a
|
|
1954
|
+
# fragment at all. Zephyr has no TRACE level -- `trace` maps to DBG, the same
|
|
1955
|
+
# value 4 the old int emit used.
|
|
1956
|
+
_LOG_LEVEL_CHOICE = {
|
|
1957
|
+
"off": "OFF",
|
|
1958
|
+
"error": "ERR",
|
|
1959
|
+
"warn": "WRN",
|
|
1960
|
+
"info": "INF",
|
|
1961
|
+
"debug": "DBG",
|
|
1962
|
+
"trace": "DBG",
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
# board.yaml `diagnostics.modules:` key -> (Zephyr log-module name, the
|
|
1966
|
+
# Kconfig symbol(s) whose `=y` proves the choice symbol is DECLARED -- one
|
|
1967
|
+
# name, or a tuple when the declaration is nested in more than one `if` --,
|
|
1968
|
+
# the symbol the module's log choice `depends on`).
|
|
1969
|
+
#
|
|
1970
|
+
# All three are load-bearing. A module's `<MOD>_LOG_LEVEL_<LEVEL>` choice
|
|
1971
|
+
# symbols exist only inside the `if` block(s) enclosing the module's
|
|
1972
|
+
# `source "...Kconfig.template.log_config"`, and the choice itself
|
|
1973
|
+
# `depends on` a logging gate -- plain `LOG` for the subsys/logging template,
|
|
1974
|
+
# `NET_LOG` for the networking one
|
|
1975
|
+
# (subsys/net/Kconfig.template.log_config.net: `depends on $(module-dep)`).
|
|
1976
|
+
# Both failure modes are real, and they are NOT the same failure:
|
|
1977
|
+
#
|
|
1978
|
+
# * assigning an UNDEFINED symbol is fatal -- alp.conf is loaded as a
|
|
1979
|
+
# handwritten fragment with `warn_assign_undef=True` and
|
|
1980
|
+
# `zephyr/scripts/kconfig/kconfig.py` turns that warning into
|
|
1981
|
+
# `err("Aborting due to Kconfig warnings")`;
|
|
1982
|
+
# * assigning a DEFINED-but-undeclared-here choice symbol is SILENT -- the
|
|
1983
|
+
# configure still exits 0, the override is discarded, and all you get is
|
|
1984
|
+
# `warning: The choice symbol <SYM> ... was selected (set =y), but no
|
|
1985
|
+
# symbol ended up as the choice selection`. Measured on real Zephyr
|
|
1986
|
+
# v4.4.1 (`cmake -DBOARD=native_sim samples/hello_world` + a fragment
|
|
1987
|
+
# carrying CONFIG_LOG=y / CONFIG_MBEDTLS=y / CONFIG_MBEDTLS_LOG_LEVEL_DBG=y:
|
|
1988
|
+
# EXIT=0, and the generated .config has no CONFIG_MBEDTLS_LOG_LEVEL_DBG
|
|
1989
|
+
# line at all). That silent branch is exactly what tan-cli#559 is about,
|
|
1990
|
+
# so the guard columns below have to be EXACT, not approximately right.
|
|
1991
|
+
#
|
|
1992
|
+
# So a line goes live only when EVERY guard symbol AND the logging gate are
|
|
1993
|
+
# already `=y` in this very fragment.
|
|
1994
|
+
#
|
|
1995
|
+
# Every entry below is transcribed from the pinned Zephyr v4.4.1 tree -- the
|
|
1996
|
+
# module name appears there as a literal `module = <NAME>` feeding a
|
|
1997
|
+
# log_config template, and each guard symbol is a real `config`/`menuconfig`
|
|
1998
|
+
# this emitter can put in the fragment. The guard column is the enclosing
|
|
1999
|
+
# `if` chain resolved across the whole `source` chain from Kconfig.zephyr
|
|
2000
|
+
# down, minus the parts a guard already implies (e.g. `if NETWORKING` around
|
|
2001
|
+
# every net_* row is implied by `NET_LOG`, which is declared inside it; the
|
|
2002
|
+
# hidden `BT_LOG` is `default y if LOG && BT`; `FILE_SYSTEM_LIB_LINK` is
|
|
2003
|
+
# `select`ed by `FILE_SYSTEM`).
|
|
2004
|
+
#
|
|
2005
|
+
# Two residual `if`s cannot be expressed here because they are NEGATIVE and
|
|
2006
|
+
# the guard set only knows `=y`: `if !NET_RAW_MODE` (hidden, default n, only
|
|
2007
|
+
# `select`ed by the IEEE-802.15.4 raw drivers) around net_core / net_tcp /
|
|
2008
|
+
# net_ipv4, and the `BT_STACK_SELECTION` choice defaulting to `BT_HCI` around
|
|
2009
|
+
# bluetooth. A board that selects `NET_RAW_MODE` or `BT_CUSTOM` would get the
|
|
2010
|
+
# silent-discard branch above; the SDK enables neither.
|
|
2011
|
+
#
|
|
2012
|
+
# A key that is not in this table (a typo, or a real Zephyr module the SDK
|
|
2013
|
+
# never enables) degrades to a comment; that is the safe direction -- a
|
|
2014
|
+
# missing entry costs a log-level override, a wrong entry costs the override
|
|
2015
|
+
# silently and leaves a Kconfig warning in the build.
|
|
2016
|
+
_LOG_MODULES: dict[str, tuple[str, str | tuple[str, ...], str]] = {
|
|
2017
|
+
# drivers/ -- subsys/logging/Kconfig.template.log_config (`depends on LOG`)
|
|
2018
|
+
"adc": ("ADC", "ADC", "LOG"), # drivers/adc/Kconfig:94
|
|
2019
|
+
"can": ("CAN", "CAN", "LOG"), # drivers/can/Kconfig:16
|
|
2020
|
+
"counter": ("COUNTER", "COUNTER", "LOG"), # drivers/counter/Kconfig:47
|
|
2021
|
+
"dac": ("DAC", "DAC", "LOG"), # drivers/dac/Kconfig:16
|
|
2022
|
+
"display": ("DISPLAY", "DISPLAY", "LOG"), # drivers/display/Kconfig:19
|
|
2023
|
+
"ethernet": ("ETHERNET", "ETH_DRIVER", "LOG"), # drivers/ethernet/Kconfig:23 (enable :6)
|
|
2024
|
+
"flash": ("FLASH", "FLASH", "LOG"), # drivers/flash/Kconfig:226
|
|
2025
|
+
"gpio": ("GPIO", "GPIO", "LOG"), # drivers/gpio/Kconfig:13
|
|
2026
|
+
"i2c": ("I2C", "I2C", "LOG"), # drivers/i2c/Kconfig:188
|
|
2027
|
+
"i2s": ("I2S", "I2S", "LOG"), # drivers/i2s/Kconfig:22
|
|
2028
|
+
"pwm": ("PWM", "PWM", "LOG"), # drivers/pwm/Kconfig:13
|
|
2029
|
+
"rtc": ("RTC", "RTC", "LOG"), # drivers/rtc/Kconfig:11
|
|
2030
|
+
"sensor": ("SENSOR", "SENSOR", "LOG"), # drivers/sensor/Kconfig:14
|
|
2031
|
+
"spi": ("SPI", "SPI", "LOG"), # drivers/spi/Kconfig:102
|
|
2032
|
+
"uart_console": ("UART_CONSOLE", "UART_CONSOLE", "LOG"), # drivers/console/Kconfig:286
|
|
2033
|
+
"watchdog": ("WDT", "WATCHDOG", "LOG"), # drivers/watchdog/Kconfig:43
|
|
2034
|
+
"wifi": ("WIFI", "WIFI", "LOG"), # drivers/wifi/Kconfig:11-12
|
|
2035
|
+
# subsys/
|
|
2036
|
+
"bluetooth": ("BT", "BT", "LOG"), # subsys/bluetooth/Kconfig.logging:17
|
|
2037
|
+
"fs": ("FS", "FILE_SYSTEM", "LOG"), # subsys/fs/Kconfig:144
|
|
2038
|
+
"modbus": ("MODBUS", "MODBUS", "LOG"), # subsys/modbus/Kconfig:94
|
|
2039
|
+
"pm": ("PM", "PM", "LOG"), # subsys/pm/Kconfig:55
|
|
2040
|
+
"pm_device": ("PM_DEVICE", "PM_DEVICE", "LOG"), # subsys/pm/Kconfig:118
|
|
2041
|
+
"tls_credentials": ("TLS_CREDENTIALS", "TLS_CREDENTIALS", "LOG"), # subsys/net/lib/tls_credentials/Kconfig:11
|
|
2042
|
+
"usb_device": ("USB_DEVICE", "USB_DEVICE_STACK", "LOG"), # subsys/usb/device/Kconfig:16
|
|
2043
|
+
# subsys/net -- Kconfig.template.log_config.net (`module-dep = NET_LOG`)
|
|
2044
|
+
"coap": ("COAP", "COAP", "NET_LOG"), # subsys/net/lib/coap/Kconfig:286-287
|
|
2045
|
+
"net_core": ("NET_CORE", "NETWORKING", "NET_LOG"), # subsys/net/ip/Kconfig.debug:48-49
|
|
2046
|
+
# NET_NATIVE is load-bearing: the log_config sits inside `if
|
|
2047
|
+
# NET_NATIVE_IPV4` (Kconfig.ipv4:44), a HIDDEN symbol -- `depends on
|
|
2048
|
+
# NET_NATIVE`, `default y if NET_IPV4` (subsys/net/ip/Kconfig:60-63). So
|
|
2049
|
+
# NET_IPV4=y alone does NOT declare the choice on an offload-only
|
|
2050
|
+
# (NET_NATIVE=n) target; NET_IPV4=y + NET_NATIVE=y does, exactly.
|
|
2051
|
+
"net_ipv4": ("NET_IPV4", ("NET_IPV4", "NET_NATIVE"), "NET_LOG"), # subsys/net/ip/Kconfig.ipv4:191-192
|
|
2052
|
+
"net_l2_ethernet": ("NET_L2_ETHERNET", "NET_L2_ETHERNET", "NET_LOG"), # subsys/net/l2/ethernet/Kconfig:13-14
|
|
2053
|
+
"net_sockets": ("NET_SOCKETS", "NET_SOCKETS", "NET_LOG"), # subsys/net/lib/sockets/Kconfig:443-444
|
|
2054
|
+
"net_tcp": ("NET_TCP", "NET_TCP", "NET_LOG"), # subsys/net/ip/Kconfig.tcp:16-17
|
|
2055
|
+
# modules/
|
|
2056
|
+
# MBEDTLS_DEBUG is the real guard, not MBEDTLS: `module = MBEDTLS` sits
|
|
2057
|
+
# inside `if MBEDTLS_DEBUG` (modules/mbedtls/Kconfig:89), itself nested in
|
|
2058
|
+
# `if MBEDTLS` (:31). MBEDTLS_DEBUG is a prompted bool defaulting n that
|
|
2059
|
+
# this emitter never writes, while CONFIG_MBEDTLS=y IS written (from
|
|
2060
|
+
# metadata/libraries/mbedtls.yaml) by iot-fleet-ota, production-deployment,
|
|
2061
|
+
# rpmsg-v2n, rpmsg-aen and heterogeneous-offload -- so guarding on MBEDTLS
|
|
2062
|
+
# alone made the ONE in-table module the SDK routinely enables emit a line
|
|
2063
|
+
# whose choice symbol is not declared.
|
|
2064
|
+
"mbedtls": ("MBEDTLS", ("MBEDTLS", "MBEDTLS_DEBUG"), "LOG"), # modules/mbedtls/Kconfig:89-93
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
|
|
2068
|
+
def _enabled_symbols(lines: list[str]) -> set[str]:
|
|
2069
|
+
"""The Kconfig symbols this fragment has already switched on.
|
|
2070
|
+
|
|
2071
|
+
Reads back the `CONFIG_<SYM>=y` lines built so far (annotations like
|
|
2072
|
+
`CONFIG_X=y # lib / class` included), which is the only authority on what
|
|
2073
|
+
this particular slice compiles in.
|
|
2074
|
+
"""
|
|
2075
|
+
out: set[str] = set()
|
|
2076
|
+
for line in lines:
|
|
2077
|
+
text = line.strip()
|
|
2078
|
+
if not text.startswith("CONFIG_") or "=" not in text:
|
|
2079
|
+
continue
|
|
2080
|
+
symbol, _, value = text.partition("=")
|
|
2081
|
+
if value.split("#", 1)[0].strip() == "y":
|
|
2082
|
+
out.add(symbol[len("CONFIG_"):])
|
|
2083
|
+
return out
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
def _emit_diagnostics(project: "BoardProject",
|
|
2087
|
+
slice_: Slice,
|
|
2088
|
+
emitted: list[str]) -> list[str]:
|
|
2089
|
+
"""Per-module log-level overrides (board.yaml `diagnostics.modules:`).
|
|
2090
|
+
|
|
2091
|
+
`board.schema.json` documents this knob as emitting
|
|
2092
|
+
`CONFIG_<MODULE>_LOG_LEVEL_<LEVEL>=y` -- the choice-symbol form. That is
|
|
2093
|
+
what is emitted here. The old `CONFIG_<MODULE>_LOG_LEVEL=<n>` int form
|
|
2094
|
+
could not build for ANY key (alplabai/tan-cli#559): the int symbol is
|
|
2095
|
+
promptless and derived, so Zephyr rejects assigning it even for a real
|
|
2096
|
+
module (`I2C_LOG_LEVEL ... is not directly user-configurable (has no
|
|
2097
|
+
prompt)`), and an unknown key was an assignment to an undefined symbol.
|
|
2098
|
+
|
|
2099
|
+
A live line is emitted only when the module is in `_LOG_MODULES` AND
|
|
2100
|
+
EVERY one of its guard symbols AND its logging gate (`LOG`, or `NET_LOG`
|
|
2101
|
+
for the networking modules) are already `=y` in `emitted` -- the
|
|
2102
|
+
conditions under which the choice symbol is declared at all. A module
|
|
2103
|
+
can carry more than one guard when its log_config template is nested in
|
|
2104
|
+
more than one `if`: `mbedtls` needs MBEDTLS **and** MBEDTLS_DEBUG,
|
|
2105
|
+
`net_ipv4` needs NET_IPV4 **and** NET_NATIVE. Getting that wrong is not
|
|
2106
|
+
caught by the build: the configure still exits 0 and merely warns that
|
|
2107
|
+
the choice symbol "was selected (set =y), but no symbol ended up as the
|
|
2108
|
+
choice selection", so the override vanishes silently -- the same
|
|
2109
|
+
silent-failure class tan-cli#559 is about.
|
|
2110
|
+
|
|
2111
|
+
Everything else (an ALP_* SDK module with no LOG_MODULE_REGISTER yet, a
|
|
2112
|
+
module that lives on another core, a typo) is downgraded to a comment
|
|
2113
|
+
naming the reason, so the fragment always configures.
|
|
2114
|
+
"""
|
|
2115
|
+
lines: list[str] = []
|
|
2116
|
+
modules = (project.diagnostics or {}).get("modules") or {}
|
|
2117
|
+
if modules:
|
|
2118
|
+
enabled = _enabled_symbols(emitted)
|
|
2119
|
+
lines.append("# Per-module log-level overrides (board.yaml diagnostics.modules:)")
|
|
2120
|
+
for mod, lvl in sorted(modules.items()):
|
|
2121
|
+
choice = _LOG_LEVEL_CHOICE.get(str(lvl).lower())
|
|
2122
|
+
if choice is None:
|
|
2123
|
+
continue
|
|
2124
|
+
known = _LOG_MODULES.get(str(mod).lower())
|
|
2125
|
+
if known is None:
|
|
2126
|
+
stem = str(mod).upper()
|
|
2127
|
+
reason = (
|
|
2128
|
+
"pending LOG_MODULE_REGISTER on this SDK module"
|
|
2129
|
+
if stem.startswith("ALP_") else
|
|
2130
|
+
f"no Zephyr log module `{mod}` the SDK can wire -- "
|
|
2131
|
+
f"check the spelling")
|
|
2132
|
+
lines.append(f"# CONFIG_{stem}_LOG_LEVEL_{choice}=y ({reason})")
|
|
2133
|
+
continue
|
|
2134
|
+
log_module, guard_symbols, log_gate = known
|
|
2135
|
+
if isinstance(guard_symbols, str):
|
|
2136
|
+
guard_symbols = (guard_symbols,)
|
|
2137
|
+
symbol = f"CONFIG_{log_module}_LOG_LEVEL_{choice}"
|
|
2138
|
+
missing = [g for g in guard_symbols if g not in enabled]
|
|
2139
|
+
if log_gate not in enabled:
|
|
2140
|
+
lines.append(f"# {symbol}=y (its log choice depends on "
|
|
2141
|
+
f"CONFIG_{log_gate}=y, not set on core "
|
|
2142
|
+
f"`{slice_.core_id}`)")
|
|
2143
|
+
elif missing:
|
|
2144
|
+
need = ", ".join(f"CONFIG_{g}=y" for g in missing)
|
|
2145
|
+
lines.append(f"# {symbol}=y (its log choice is declared only "
|
|
2146
|
+
f"under {need}, not set on core "
|
|
2147
|
+
f"`{slice_.core_id}`)")
|
|
2148
|
+
else:
|
|
2149
|
+
lines.append(f"{symbol}=y")
|
|
2150
|
+
lines.append("")
|
|
2151
|
+
return lines
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
def _slice_alp_conf(project: BoardProject, slice_: Slice) -> str:
|
|
2155
|
+
"""Per-core Kconfig fragment for a Zephyr slice.
|
|
2156
|
+
|
|
2157
|
+
Emits the full Kconfig the slice needs: baseline + log + silicon +
|
|
2158
|
+
per-core peripherals/libraries + SoM-intrinsic chip drivers (auto-
|
|
2159
|
+
derived from ``on_module:`` + ``helper_firmware:`` in the SoM preset)
|
|
2160
|
+
+ board-populated chip drivers (from board.yaml ``board.populated:``
|
|
2161
|
+
+ the board preset) + the Zephyr subsystem enables those chip drivers
|
|
2162
|
+
need (e.g. an enabled ``rv3028c7`` chip driver pulls in ``CONFIG_I2C=y``).
|
|
2163
|
+
|
|
2164
|
+
Swapping ``som.sku:`` in board.yaml automatically changes the SoM-
|
|
2165
|
+
intrinsic chip set with no other board.yaml edits required.
|
|
2166
|
+
|
|
2167
|
+
This is a thin composer: each section below is delegated to a
|
|
2168
|
+
dedicated ``_emit_*`` helper (console / SoM capability / chip driver
|
|
2169
|
+
/ subsystem / library / inference / cross-core shmem cache / memory /
|
|
2170
|
+
power / storage / OTA / diagnostics), called in the exact fixed order
|
|
2171
|
+
the fragment has always used. Split out mechanically as the #673
|
|
2172
|
+
Phase-1 kconfig split -- behavior-preserving for every section that
|
|
2173
|
+
predates the cross-core shmem cache one; ``check_emit_snapshots.py``'s
|
|
2174
|
+
fixtures (none of which declare a ``raw_shmem`` ``ipc:`` entry) stay
|
|
2175
|
+
byte-identical.
|
|
2176
|
+
"""
|
|
2177
|
+
silicon = project.som_preset.get("silicon")
|
|
2178
|
+
kconfig = silicon_to_kconfig(silicon, project.effective_metadata_root())
|
|
2179
|
+
diagnostics = project.diagnostics
|
|
2180
|
+
|
|
2181
|
+
lines: list[str] = []
|
|
2182
|
+
lines.extend(_emit_baseline(slice_, diagnostics))
|
|
2183
|
+
lines.extend(_emit_console(diagnostics, slice_, project))
|
|
2184
|
+
lines.extend(_emit_soc_summary(project, slice_))
|
|
2185
|
+
lines.extend(_emit_som_caps(project, silicon, kconfig))
|
|
2186
|
+
|
|
2187
|
+
chip_lines, chip_subsystems, resolved_chip_state = _emit_chips(
|
|
2188
|
+
project, _CHIP_SUBSYSTEMS)
|
|
2189
|
+
lines.extend(chip_lines)
|
|
2190
|
+
lines.extend(_emit_subsystems(project, slice_, chip_subsystems))
|
|
2191
|
+
|
|
2192
|
+
iot_lines = _zephyr_iot_kconfig(project, slice_, resolved_chip_state)
|
|
2193
|
+
if iot_lines:
|
|
2194
|
+
lines.extend(iot_lines)
|
|
2195
|
+
|
|
2196
|
+
lines.extend(_emit_libraries(project, slice_))
|
|
2197
|
+
# §D.lib.loader -- per-`libraries:` HW-accelerator backend wiring
|
|
2198
|
+
# (CONFIG_ALP_<LIB>_<BACKEND>=y). This is the single source both the
|
|
2199
|
+
# planner's build-plan `configArtefacts` and `alp_project.py --emit
|
|
2200
|
+
# zephyr-conf --core <id>` (what gen_example_alp_conf.py's pre-generation
|
|
2201
|
+
# for twister / bare `west build` mirrors) share
|
|
2202
|
+
# -- folded in here (2026-07-20) so the two paths cannot silently
|
|
2203
|
+
# diverge on a `libraries:` entry with a hw_backends matcher; see
|
|
2204
|
+
# docs/adr/0020-sdk-owns-build-execution.md addendum.
|
|
2205
|
+
#
|
|
2206
|
+
# Reads BOTH declaration channels (`libraries.scoped_names`), not just
|
|
2207
|
+
# `slice_.libraries`. A library's accelerator support is a property of
|
|
2208
|
+
# the library manifest and the SoM's silicon, not of whether board.yaml
|
|
2209
|
+
# spelled the selection as a bare/project-wide entry or a `cores:`-
|
|
2210
|
+
# scoped one -- the two forms must resolve to the identical hw-backend
|
|
2211
|
+
# set for the same core (alp-sdk #1359: a `cores:` entry was silently a
|
|
2212
|
+
# SUPERSET of the project-wide form, gaining HELIUM/ADC_DMA/etc. that
|
|
2213
|
+
# `cores:` reads as narrowing, not widening).
|
|
2214
|
+
hw_backend_lines = _library_layer._emit_library_hw_backends(
|
|
2215
|
+
_library_layer.scoped_names(project, slice_=slice_), project.sku,
|
|
2216
|
+
project.effective_metadata_root())
|
|
2217
|
+
if hw_backend_lines:
|
|
2218
|
+
lines.append("# §D.lib.loader -- per-library HW-accelerator "
|
|
2219
|
+
"wiring (auto-emitted).")
|
|
2220
|
+
lines.extend(hw_backend_lines)
|
|
2221
|
+
lines.append("")
|
|
2222
|
+
lines.extend(_emit_inference(project, slice_, silicon))
|
|
2223
|
+
lines.extend(_emit_cross_core_shmem_cache(project, slice_, lines))
|
|
2224
|
+
lines.extend(_emit_memory(slice_))
|
|
2225
|
+
lines.extend(_emit_power(slice_))
|
|
2226
|
+
lines.extend(_emit_storage(project))
|
|
2227
|
+
lines.extend(_emit_ota(project))
|
|
2228
|
+
# Passed the fragment built so far: a per-module log-level override is
|
|
2229
|
+
# only a real Kconfig symbol when THIS slice already switched the module
|
|
2230
|
+
# (and CONFIG_LOG) on -- see `_emit_diagnostics`.
|
|
2231
|
+
lines.extend(_emit_diagnostics(project, slice_, lines))
|
|
2232
|
+
# Per-product core ownership: Kconfig for the assignable peripherals this
|
|
2233
|
+
# project assigned to this (M33) core -- the board tree carries the nodes
|
|
2234
|
+
# disabled, so only an owning project enables them.
|
|
2235
|
+
own_kconfig = project_m33_overlay(project, slice_.core_id)[1]
|
|
2236
|
+
if own_kconfig:
|
|
2237
|
+
lines.append("# Assignable peripherals owned by this core (board.yaml "
|
|
2238
|
+
"`ownership:`).")
|
|
2239
|
+
lines.extend(own_kconfig)
|
|
2240
|
+
|
|
2241
|
+
text = "\n".join(lines) + "\n"
|
|
2242
|
+
_refuse_live_kconfig_placeholders(text, slice_.core_id)
|
|
2243
|
+
return text
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
def _refuse_live_kconfig_placeholders(text: str, core_id: str) -> None:
|
|
2247
|
+
"""Refuse a `${NAME}` placeholder on a live line of a Zephyr fragment.
|
|
2248
|
+
|
|
2249
|
+
A board.yaml value written as a placeholder (`ota.server.tenant:
|
|
2250
|
+
"${MENDER_TENANT_TOKEN}"`) is meant for the build host or the device to
|
|
2251
|
+
fill. A Zephyr Kconfig fragment can never do that: the pinned v4.4.1
|
|
2252
|
+
`scripts/kconfig/kconfiglib.py` `_load_config` only unescapes a `.conf`
|
|
2253
|
+
string value -- the `expandvars` call in that file belongs to the Kconfig
|
|
2254
|
+
*source* tokenizer, not the `.conf` loader -- and nothing in Zephyr's CMake
|
|
2255
|
+
expands a fragment either. So a live `CONFIG_HAWKBIT_SERVER="${HOST}"`
|
|
2256
|
+
would build firmware that carries the literal text `${HOST}` as its
|
|
2257
|
+
server name, silently. A commented line (the Mender-MCU-client hint lines
|
|
2258
|
+
in `_emit_ota`) is inert and allowed.
|
|
2259
|
+
"""
|
|
2260
|
+
# Imported here, not at the top: a new top-of-file line would shift every
|
|
2261
|
+
# line number that changelog fragments cite in this module.
|
|
2262
|
+
import re
|
|
2263
|
+
|
|
2264
|
+
for line in text.splitlines():
|
|
2265
|
+
if line.lstrip().startswith("#"):
|
|
2266
|
+
continue
|
|
2267
|
+
match = re.search(r"\$\{[^}]*\}", line)
|
|
2268
|
+
if match:
|
|
2269
|
+
raise OrchestratorError(
|
|
2270
|
+
f"core '{core_id}': the Zephyr config would carry the "
|
|
2271
|
+
f"placeholder `{match.group(0)}` literally (`{line.strip()}`) "
|
|
2272
|
+
f"-- Zephyr does not expand environment variables in a "
|
|
2273
|
+
f"Kconfig fragment, so the firmware would use the text "
|
|
2274
|
+
f"`{match.group(0)}` itself. Write the real value in "
|
|
2275
|
+
f"board.yaml, or set it in the app's own prj.conf")
|
|
2276
|
+
|
|
2277
|
+
|
|
2278
|
+
def _inference_auto_order(som_preset: dict) -> list[str]:
|
|
2279
|
+
"""The SoM preset's ordered AUTO accelerator preference, best first.
|
|
2280
|
+
|
|
2281
|
+
`inference.auto_order` is the single source: its first entry is the SoM's
|
|
2282
|
+
preferred backend.
|
|
2283
|
+
"""
|
|
2284
|
+
return list((som_preset.get("inference") or {}).get("auto_order") or [])
|
|
2285
|
+
|
|
2286
|
+
|
|
2287
|
+
def _slice_local_conf(project: BoardProject, slice_: Slice) -> str:
|
|
2288
|
+
"""Per-core local.conf snippet for a Yocto slice."""
|
|
2289
|
+
machine = slice_.machine or f"e1m-{project.sku.lower().replace('e1m-', '')}"
|
|
2290
|
+
lines: list[str] = []
|
|
2291
|
+
lines.append("# Auto-generated by scripts/alp_orchestrate.py "
|
|
2292
|
+
"-- append to local.conf.")
|
|
2293
|
+
lines.append(f"# Per-core slice `{slice_.core_id}` "
|
|
2294
|
+
f"(image: {slice_.image or 'custom'})")
|
|
2295
|
+
# Console: a Yocto slice uses the Linux console (kernel console = the
|
|
2296
|
+
# SoM's debug UART via the `console=` kernel cmdline; userspace printf
|
|
2297
|
+
# goes to stdout/the controlling tty). This is the Linux half of the
|
|
2298
|
+
# generic console auto-selection -- nothing to set in local.conf; the
|
|
2299
|
+
# BSP owns the `console=ttySC*` cmdline. (Zephyr slices get the Alp
|
|
2300
|
+
# UART console instead; see _emit_zephyr_console.)
|
|
2301
|
+
console = _resolve_console((project.diagnostics or {}).get("console"),
|
|
2302
|
+
slice_.os)
|
|
2303
|
+
if console == "linux":
|
|
2304
|
+
lines.append("# Console: Linux console (kernel `console=` cmdline "
|
|
2305
|
+
"-> SoM debug UART; userspace -> stdout/tty).")
|
|
2306
|
+
lines.append(f'MACHINE = "{machine}"')
|
|
2307
|
+
if slice_.peripherals:
|
|
2308
|
+
joined = ", ".join(sorted(slice_.peripherals))
|
|
2309
|
+
lines.append("# Peripherals declared for this Yocto slice are "
|
|
2310
|
+
f"BSP/kernel-owned ({joined}); no Zephyr Kconfig or "
|
|
2311
|
+
"local.conf package knob is emitted here.")
|
|
2312
|
+
iot_lines = _yocto_iot_lines(project, slice_)
|
|
2313
|
+
if iot_lines:
|
|
2314
|
+
lines.extend(iot_lines)
|
|
2315
|
+
# `cameras:` -> ALP_CAMERA_CAM<n>, the variable the kernel bbappend keys
|
|
2316
|
+
# the sensor devicetree include on (same resolver as Zephyr's -DSHIELD).
|
|
2317
|
+
lines.extend(_cameras.yocto_camera_lines(project, slice_))
|
|
2318
|
+
# Curated third-party libraries (top-level `libraries:`, ADR 0018) with a
|
|
2319
|
+
# Yocto integration section -- BOTH the project-wide entries and the ones
|
|
2320
|
+
# scoped to this core. Every recipe name comes from the library's own
|
|
2321
|
+
# `integration.yocto.image_install:`; nothing is derived from the library
|
|
2322
|
+
# name. (alplabai/tan-cli#555: the core-scoped half used to bypass the
|
|
2323
|
+
# library layer entirely and emit `lib-<name>` -- a recipe that RPROVIDES
|
|
2324
|
+
# nothing, so `bitbake alp-image-edge` died on `Nothing RPROVIDES
|
|
2325
|
+
# 'lib-mbedtls'`.) Guard keeps a project with no such libraries
|
|
2326
|
+
# byte-identical.
|
|
2327
|
+
for name in _library_layer.yocto_unwireable(
|
|
2328
|
+
project, slice_, project.effective_metadata_root()):
|
|
2329
|
+
lines.append(f"# library `{name}`: manifest declares no "
|
|
2330
|
+
f"`integration.yocto:` section -- nothing to add to "
|
|
2331
|
+
f"IMAGE_INSTALL on core `{slice_.core_id}`.")
|
|
2332
|
+
library_pkgs = _library_layer.yocto_image_install(
|
|
2333
|
+
project, slice_, project.effective_metadata_root())
|
|
2334
|
+
if library_pkgs:
|
|
2335
|
+
joined = " ".join(library_pkgs)
|
|
2336
|
+
lines.append(f'IMAGE_INSTALL:append = " {joined}"')
|
|
2337
|
+
# SoM-declared AUTO accelerator preference. Read by the alp-sdk recipe
|
|
2338
|
+
# (EXTRA_OECMAKE -> -DALP_SDK_INFERENCE_AUTO_ORDER); only the
|
|
2339
|
+
# .alpmodel selector (alp_model_select) consumes it, as a tiebreak. Weak `?=` so a hand-edited
|
|
2340
|
+
# local.conf wins; emitted only for presets that declare it.
|
|
2341
|
+
auto_order = _inference_auto_order(project.som_preset)
|
|
2342
|
+
if auto_order:
|
|
2343
|
+
lines.append(f'ALP_SDK_INFERENCE_AUTO_ORDER ?= "{",".join(auto_order)}"')
|
|
2344
|
+
if slice_.image:
|
|
2345
|
+
lines.append(f"# bitbake target: {slice_.image}")
|
|
2346
|
+
|
|
2347
|
+
# Mender OTA wiring (board.yaml `ota:` block). Emits Mender
|
|
2348
|
+
# `?=` (weak) assignments so hand-edited local.conf values in
|
|
2349
|
+
# the build dir still win.
|
|
2350
|
+
ota = project.ota or {}
|
|
2351
|
+
if ota.get("provider") == "mender":
|
|
2352
|
+
lines.append("")
|
|
2353
|
+
lines.append("# OTA (board.yaml `ota:` block)")
|
|
2354
|
+
lines.append('INHERIT += "mender-full"')
|
|
2355
|
+
if ota.get("artifact_name"):
|
|
2356
|
+
lines.append(f'MENDER_ARTIFACT_NAME ?= "{ota["artifact_name"]}"')
|
|
2357
|
+
srv = ota.get("server") or {}
|
|
2358
|
+
if srv.get("url"):
|
|
2359
|
+
lines.append(f'MENDER_SERVER_URL ?= "{srv["url"]}"')
|
|
2360
|
+
tenant = str(srv.get("tenant") or "")
|
|
2361
|
+
if "${" in tenant:
|
|
2362
|
+
# A ${NAME} placeholder is never expanded by BitBake from the
|
|
2363
|
+
# host environment, so a self-referencing `?=` would bake the
|
|
2364
|
+
# literal text (or fail to expand). Leave the token to the
|
|
2365
|
+
# documented local.conf override instead.
|
|
2366
|
+
lines.append(
|
|
2367
|
+
"# MENDER_TENANT_TOKEN: set it in conf/local.conf "
|
|
2368
|
+
"(meta-alp-sdk/README.md, Mender step 3); the board.yaml "
|
|
2369
|
+
"placeholder is not expanded by BitBake."
|
|
2370
|
+
)
|
|
2371
|
+
elif tenant:
|
|
2372
|
+
lines.append(f'MENDER_TENANT_TOKEN ?= "{tenant}"')
|
|
2373
|
+
sto = ota.get("storage") or {}
|
|
2374
|
+
if sto.get("device"):
|
|
2375
|
+
lines.append(f'MENDER_STORAGE_DEVICE_BASE ?= "{sto["device"]}"')
|
|
2376
|
+
if sto.get("boot_part_mb"):
|
|
2377
|
+
lines.append(f'MENDER_BOOT_PART_SIZE_MB ?= "{sto["boot_part_mb"]}"')
|
|
2378
|
+
if sto.get("total_size_mb"):
|
|
2379
|
+
lines.append(f'MENDER_STORAGE_TOTAL_SIZE_MB ?= "{sto["total_size_mb"]}"')
|
|
2380
|
+
if ota.get("poll_interval_s"):
|
|
2381
|
+
lines.append(f'MENDER_INVENTORY_INTERVAL ?= "{ota["poll_interval_s"]}"')
|
|
2382
|
+
|
|
2383
|
+
return "\n".join(lines) + "\n"
|
|
2384
|
+
|
|
2385
|
+
|
|
2386
|
+
def _slice_cmake_args(project: BoardProject, slice_: Slice) -> str:
|
|
2387
|
+
"""Per-core cmake -D args for a baremetal / yocto slice.
|
|
2388
|
+
|
|
2389
|
+
NPU-dispatch enables (DRP-AI, DEEPX) are driven from the SoM
|
|
2390
|
+
preset's `capabilities:` matrix -- never from board.yaml. On
|
|
2391
|
+
multi-NPU SKUs (V2M101 = DRP-AI3 + DEEPX DX-M1) every available
|
|
2392
|
+
NPU is enabled so apps can dispatch concurrent independent models
|
|
2393
|
+
via alp_inference_open(.backend=...) per-handle.
|
|
2394
|
+
"""
|
|
2395
|
+
family = project.som_preset.get("family") or "unknown"
|
|
2396
|
+
# resolve_capabilities merges SoC-JSON defaults with SoM overrides.
|
|
2397
|
+
capabilities = resolve_capabilities(project.som_preset, project.effective_metadata_root())
|
|
2398
|
+
lines: list[str] = []
|
|
2399
|
+
lines.append("# Auto-generated by scripts/alp_orchestrate.py "
|
|
2400
|
+
"-- pass to cmake.")
|
|
2401
|
+
lines.append(f"-DALP_SOM_SKU={project.sku}")
|
|
2402
|
+
lines.append(f"-DALP_SOM_FAMILY={family}")
|
|
2403
|
+
# Per-SKU capability-restriction gate for <alp/soc_caps.h>: emitted
|
|
2404
|
+
# ONLY when the SoM preset declares `silicon_capabilities.unpopulated`,
|
|
2405
|
+
# so every unrestricted SKU's cmake-args output stays byte-identical.
|
|
2406
|
+
if som_unpopulated_capabilities(project.som_preset):
|
|
2407
|
+
lines.append(f"-DALP_SOM_{_som_define_slug(project.sku)}")
|
|
2408
|
+
lines.append(f"-DALP_CORE_ID={slice_.core_id}")
|
|
2409
|
+
# Cross-EVK board facade selector (<alp/board.h>).
|
|
2410
|
+
# Emitted only when the project resolves to a named board preset
|
|
2411
|
+
# (e.g. `preset: e1m-x-evk` -> board_name "E1M-X-EVK").
|
|
2412
|
+
# Inline boards with no preset/name skip this; the facade defaults
|
|
2413
|
+
# to an #error forcing the builder to set it explicitly (e.g. via
|
|
2414
|
+
# extra_args for native_sim testcases).
|
|
2415
|
+
if project.board_name:
|
|
2416
|
+
lines.append(f"-DALP_BOARD_{_board_define_slug(project.board_name)}")
|
|
2417
|
+
if slice_.toolchain:
|
|
2418
|
+
lines.append(f"-DALP_TOOLCHAIN={slice_.toolchain}")
|
|
2419
|
+
# `cameras:` -> the same -DSHIELD the build command carries.
|
|
2420
|
+
shield = _cameras.shield_define_for_build(project, slice_)
|
|
2421
|
+
if shield:
|
|
2422
|
+
lines.append(f"-D{shield}")
|
|
2423
|
+
if capabilities.get("drp_ai"):
|
|
2424
|
+
# Must match the option name in src/yocto/CMakeLists.txt
|
|
2425
|
+
# (ALP_SDK_USE_DRPAI_V2N -- compiles inference_drpai.cpp).
|
|
2426
|
+
lines.append("-DALP_SDK_USE_DRPAI_V2N=ON")
|
|
2427
|
+
if capabilities.get("deepx_dxm1"):
|
|
2428
|
+
lines.append("-DALP_SDK_USE_DEEPX_DXM1=ON")
|
|
2429
|
+
# Project-wide curated third-party libraries (top-level `libraries:`,
|
|
2430
|
+
# ADR 0018) with a baremetal integration section. Guard keeps a project
|
|
2431
|
+
# with no such libraries byte-identical.
|
|
2432
|
+
for arg in _library_layer.baremetal_cmake_args(
|
|
2433
|
+
project, slice_, project.effective_metadata_root()):
|
|
2434
|
+
lines.append(arg)
|
|
2435
|
+
return "\n".join(lines) + "\n"
|