diff --git a/lopper/assists/zephyr_linker.py b/lopper/assists/zephyr_linker.py index 93c495cd..879f4b18 100644 --- a/lopper/assists/zephyr_linker.py +++ b/lopper/assists/zephyr_linker.py @@ -15,6 +15,8 @@ LayoutError, parse_layout, zephyr_argument_parser, ) +TCM_REGION_ENABLE = 0x1 + def is_compat(node, compat_string_to_test): """Identify the assist compatibility string. @@ -104,6 +106,30 @@ def _render_linker(layout, user_contents=None): template = template.replace( "#include ", common_ram) custom_lines = [] + if layout.profile == "r52-tcm": + memories_by_kind = {memory.kind: memory for memory in layout.memories} + atcm = memories_by_kind.get("ATCM") + btcm = memories_by_kind.get("BTCM") + ctcm = memories_by_kind.get("CTCM") + if atcm is None: + raise LayoutError("R52 TCM profile requires ATCM") + custom_lines.extend(( + " /* Cortex-R52 TCM configuration consumed before stack setup.", + " * Bit 0 enables a local-address mapping; a zero word leaves", + " * the bank's existing boot-firmware configuration unchanged.", + " */", + " .tcm_config :", + " {", + " . = ALIGN(4);", + " z_arm_tcm_a_region = .;", + " LONG(0x00000000)", + " z_arm_tcm_b_region = .;", + f" LONG(0x{((btcm.origin | TCM_REGION_ENABLE) if btcm else 0):08x})", + " z_arm_tcm_c_region = .;", + f" LONG(0x{((ctcm.origin | TCM_REGION_ENABLE) if ctcm else 0):08x})", + f" }} > {atcm.name}", + "", + )) for section in (item for item in layout.sections if item.custom): custom_address = "" if section.offset is not None: diff --git a/lopper/assists/zephyr_memory.py b/lopper/assists/zephyr_memory.py index 89a215dc..901e1155 100644 --- a/lopper/assists/zephyr_memory.py +++ b/lopper/assists/zephyr_memory.py @@ -13,6 +13,8 @@ import re import sys +from lopper.log import _warning + sys.path.append(os.path.dirname(__file__)) try: @@ -43,6 +45,10 @@ "readable", "writable", "executable", "cacheable", "shareable", "userspace", "static", } +TCM_LOCAL_ORIGINS = { + "cortexr5": {"ATCM": 0x0, "BTCM": 0x20000}, + "cortexr52": {"ATCM": 0x0, "BTCM": 0x10000, "CTCM": 0x18000}, +} class LayoutError(ValueError): @@ -598,14 +604,6 @@ def _infer_profile(processor, memories, sections, entry): if is_r52 and vector_offset % 32: raise LayoutError( "Cortex-R52 vector_table offset must be 32-byte aligned") - expected = ({"BTCM": 0x10000, "CTCM": 0x20000} if is_r52 - else {"BTCM": 0x20000}) - for kind, origin in expected.items(): - memory = next((item for item in memories if item.kind == kind), None) - if memory and memory.origin != origin: - raise LayoutError( - f"{kind} must use local address 0x{origin:x} for " - f"processor '{processor}'") return "r52-tcm" if is_r52 else "r5-tcm" if vector_memory.kind == "DDR" and is_r52: return "r52-ddr" @@ -753,11 +751,12 @@ def _normalized_memory(node, processor): name = _linker_name(node) kind = _memory_kind(node) origin, length = _memory_range(node) - if processor == "cortexr52": - origin = {"ATCM": 0x0, "BTCM": 0x10000, - "CTCM": 0x20000}.get(kind, origin) - elif processor == "cortexr5": - origin = {"ATCM": 0x0, "BTCM": 0x20000}.get(kind, origin) + local_origin = TCM_LOCAL_ORIGINS.get(processor, {}).get(kind) + if local_origin is not None and origin != local_origin: + _warning( + f"{node.abs_path}: SDT declares {kind} at 0x{origin:x}; " + f"normalizing to 0x{local_origin:x} for {processor}") + origin = local_origin return Memory(name, node, origin, length, _memory_policy(node, kind), kind) diff --git a/lopper/selftest/domains/openamp-zephyr-linker-r52.dts b/lopper/selftest/domains/openamp-zephyr-linker-r52.dts index 8e1c54b9..1dd9f878 100644 --- a/lopper/selftest/domains/openamp-zephyr-linker-r52.dts +++ b/lopper/selftest/domains/openamp-zephyr-linker-r52.dts @@ -26,8 +26,8 @@ label = "BTCM"; mpu-policy = "readable", "writable", "cacheable", "static"; }; - ctcm: r52_0a_ctcm_global@20000 { - reg = <0x20000 0x8000>; + ctcm: r52_0a_ctcm_global@18000 { + reg = <0x18000 0x8000>; label = "CTCM"; mpu-policy = "readable", "writable", "cacheable", "static"; }; diff --git a/lopper_sanity.py b/lopper_sanity.py index 7c5ba349..b33e61da 100755 --- a/lopper_sanity.py +++ b/lopper_sanity.py @@ -2365,6 +2365,13 @@ def zephyr_linker_generator_sanity_test(): r52_passed = r52_passed and \ "DATA_LOAD_REGION ATCM" in contents and \ "TEXT_ADDRESS ORIGIN(DDR) + 0x20" in contents and \ + ". = ALIGN(4);" in contents and \ + "z_arm_tcm_a_region = .;" in contents and \ + "LONG(0x00000000)" in contents and \ + "z_arm_tcm_b_region = .;" in contents and \ + "LONG(0x00010001)" in contents and \ + "z_arm_tcm_c_region = .;" in contents and \ + "LONG(0x00018001)" in contents and \ "SECTION_PROLOGUE(_TEXT_SECTION_NAME " \ "TEXT_ADDRESS,,)" in contents if r52_passed: @@ -2492,7 +2499,7 @@ def zephyr_linker_generator_sanity_test(): overlap_fixture = "/tmp/openamp-zephyr-overlapping-mpu.dts" Path(overlap_fixture).write_text( r52_fixture.replace("reg = <0x100000 0x80000>;", - "reg = <0x21000 0x80000>;", 1) + "reg = <0x19000 0x80000>;", 1) .replace(', "static";', ';'), encoding="utf-8") overlap_command = [ @@ -2506,7 +2513,7 @@ def zephyr_linker_generator_sanity_test(): check=False) overlap_log = overlap_result.stdout + overlap_result.stderr if "MPU regions overlap:" in overlap_log and \ - "overlap [0x21000, 0x28000)" in overlap_log: + "overlap [0x19000, 0x20000)" in overlap_log: test_passed("OpenAMP Zephyr MPU overlap validation") else: print(overlap_log) diff --git a/tests/test_zephyr_linker_tcm.py b/tests/test_zephyr_linker_tcm.py new file mode 100644 index 00000000..e4f7bcfd --- /dev/null +++ b/tests/test_zephyr_linker_tcm.py @@ -0,0 +1,123 @@ +"""Tests for Cortex-R52 TCM configuration in generated Zephyr linkers.""" + +# Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause + +import os +import re +import sys + +import pytest + +# Lopper loads assists as top-level modules. Mirror that here so the classes +# the generator consumes and raises are the ones this test compares against. +sys.path.append( + os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "lopper", "assists")) + +from zephyr_linker import _render_linker # noqa: E402 +from zephyr_memory import ( # noqa: E402 + Layout, + LayoutError, + Memory, + MemoryPolicy, + Section, +) + +TCM_POLICY = (MemoryPolicy.READABLE | MemoryPolicy.WRITABLE | + MemoryPolicy.EXECUTABLE) +CODE_SECTIONS = ("vector_table", "text", "rodata") +DATA_SECTIONS = ("data", "bss", "noinit", "heap", "stack") + + +def _memory(kind, origin, length=0x8000): + """Build one normalized linker memory of the given bank kind.""" + return Memory(kind, None, origin, length, TCM_POLICY, kind) + + +def _layout(*memories, profile="r52-tcm", code="ATCM", data="BTCM"): + """Build a minimal R52 layout that renders a complete linker script.""" + sections = tuple( + [Section(name, code) for name in CODE_SECTIONS] + + [Section(name, data) for name in DATA_SECTIONS]) + return Layout("cortexr52", profile, "_vector_table", "4.3", + tuple(memories), sections, None, "RPU_ZEPHYR.ld") + + +def _tcm_words(script): + """Return the ordered symbol and word pairs of the .tcm_config block.""" + return re.findall( + r"(z_arm_tcm_[abc]_region) = \.;\s*\n\s*LONG\((0x[0-9a-f]{8})\)", + script) + + +def test_tcm_config_declares_the_zephyr_startup_symbols(): + """Zephyr reads these exact symbols before its stack is usable. + + The names are a cross-repository ABI: renaming one side alone makes + Zephyr silently fall back to its weak defaults instead of failing. + """ + script = _render_linker(_layout( + _memory("ATCM", 0x0, 0x10000), + _memory("BTCM", 0x10000), + _memory("CTCM", 0x18000))) + + assert _tcm_words(script) == [ + ("z_arm_tcm_a_region", "0x00000000"), + ("z_arm_tcm_b_region", "0x00010001"), + ("z_arm_tcm_c_region", "0x00018001"), + ] + + +def test_tcm_config_is_placed_in_the_vector_bank(): + """The words are read at reset, so they load with the vector image.""" + script = _render_linker(_layout( + _memory("ATCM", 0x0, 0x10000), + _memory("BTCM", 0x10000), + _memory("CTCM", 0x18000))) + + start = script.index(".tcm_config") + assert script[script.index("} >", start):].startswith("} > ATCM") + + +def test_tcm_config_words_follow_the_selected_bank_origins(): + """Each word carries its bank's local base, not a hard-coded address.""" + script = _render_linker(_layout( + _memory("ATCM", 0x0, 0x10000), + _memory("BTCM", 0x20000), + _memory("CTCM", 0x30000))) + + assert _tcm_words(script) == [ + ("z_arm_tcm_a_region", "0x00000000"), + ("z_arm_tcm_b_region", "0x00020001"), + ("z_arm_tcm_c_region", "0x00030001"), + ] + + +def test_tcm_config_leaves_an_unselected_bank_unchanged(): + """A domain without CTCM emits a zero word, clearing the enable bit.""" + script = _render_linker(_layout( + _memory("ATCM", 0x0, 0x10000), + _memory("BTCM", 0x10000))) + + assert _tcm_words(script) == [ + ("z_arm_tcm_a_region", "0x00000000"), + ("z_arm_tcm_b_region", "0x00010001"), + ("z_arm_tcm_c_region", "0x00000000"), + ] + + +def test_ddr_profile_emits_no_tcm_config(): + """DDR-booted R52 images do not remap their local banks.""" + script = _render_linker(_layout( + _memory("DDR", 0x9800100, 0x5de00), + profile="r52-ddr", code="DDR", data="DDR")) + + assert ".tcm_config" not in script + assert "z_arm_tcm_" not in script + + +def test_tcm_profile_requires_atcm(): + """Without ATCM there is no reset-reachable home for the words.""" + with pytest.raises(LayoutError, match="requires ATCM"): + _render_linker(_layout(_memory("BTCM", 0x10000))) diff --git a/tests/test_zephyr_memory.py b/tests/test_zephyr_memory.py index fe4aed9a..b541c3ba 100644 --- a/tests/test_zephyr_memory.py +++ b/tests/test_zephyr_memory.py @@ -3,6 +3,7 @@ # Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved. # SPDX-License-Identifier: BSD-3-Clause +from lopper.assists import zephyr_memory from lopper.assists.zephyr_memory import _domain_memory_nodes, _normalized_memories from lopper.tree import LopperNode, LopperTree @@ -79,3 +80,27 @@ def test_linker_name_retains_unit_address_without_label(): memories = _normalized_memories((first, second), "cortexr5") assert tuple(memory.name for memory in memories) == ("SRAM_0", "SRAM_20000") + + +def test_r52_tcm_origin_normalization_warns(monkeypatch): + """An SDT/local-address mismatch is visible while retaining normalization.""" + tree = LopperTree() + axi = LopperNode(-1, "/axi") + tree + axi + axi["#address-cells"] = [1] + axi["#size-cells"] = [1] + ctcm = LopperNode(-1, "/axi/ctcm@20000") + tree + ctcm + ctcm["reg"] = [0x20000, 0x8000] + ctcm["zephyr,memory-region"] = ["CTCM"] + ctcm["mpu-policy"] = ["readable", "writable", "static"] + warnings = [] + monkeypatch.setattr(zephyr_memory, "_warning", warnings.append) + + memory = zephyr_memory._normalized_memory(ctcm, "cortexr52") + + assert memory.origin == 0x18000 + assert warnings == [ + "/axi/ctcm@20000: SDT declares CTCM at 0x20000; " + "normalizing to 0x18000 for cortexr52" + ]