From ec72d5b807877665ec00447bb32eed165eb679af Mon Sep 17 00:00:00 2001 From: Ben Levinsky Date: Tue, 1 Sep 2026 11:28:38 -0700 Subject: [PATCH 1/2] assists: zephyr: generate Cortex-R52 TCM configuration Cortex-R52 firmware can use BTCM for writable data and exception stacks before normal C initialization. If its local TCM region remains disabled, those accesses can reach the system address map and corrupt memory owned by another processor. Normalize the R52 local TCM layout to ATCM at 0x0, BTCM at 0x10000, and CTCM at 0x18000. Emit configuration words that Zephyr consumes early during reset to enable the selected B and C banks while leaving the existing ATCM configuration unchanged. Update the R52 fixture and generator checks for the corrected CTCM address and generated configuration values. Signed-off-by: Ben Levinsky --- lopper/assists/zephyr_linker.py | 26 +++++++++++++++++++ lopper/assists/zephyr_memory.py | 25 +++++++++--------- .../domains/openamp-zephyr-linker-r52.dts | 4 +-- lopper_sanity.py | 11 ++++++-- tests/test_zephyr_memory.py | 25 ++++++++++++++++++ 5 files changed, 74 insertions(+), 17 deletions(-) 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_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" + ] From 35bbca4aa3b452165d5bfddd5cf86cbfaded222d Mon Sep 17 00:00:00 2001 From: Ben Levinsky Date: Wed, 2 Sep 2026 08:59:55 -0700 Subject: [PATCH 2/2] tests: cover Cortex-R52 TCM configuration generation Exercise the generated .tcm_config block through the linker renderer so the words Zephyr reads at reset stay pinned. Symbol names are a cross-repository ABI. Renaming them on one side alone makes Zephyr fall back to its weak defaults instead of failing, so assert the exact spelling alongside the bank-derived words, the ATCM placement, the unselected-bank case, and the DDR profile that emits no configuration at all. Signed-off-by: Ben Levinsky --- tests/test_zephyr_linker_tcm.py | 123 ++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/test_zephyr_linker_tcm.py 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)))