xref: /aosp_15_r20/external/pigweed/pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Converts ST generated .icf linker files into basic .ld linker files"""
15
16
17import pathlib
18
19
20def parse_icf(icf_file: str) -> tuple[dict, dict]:
21    """Parse ICF linker file.
22
23    ST only provides .icf linker files for many products, so there is a need
24    to generate basic GCC compatible .ld files for all products.
25    This parses the basic features from the .icf format well enough to work
26    for the ST's .icf files that exist in `cmsis_device`
27
28    Args:
29        icf_file: .icf linker file read into a string
30
31    Returns:
32        (regions, blocks) where
33            `regions` is a map from region_name -> (start_hex, end_hex)
34            `blocks` is a map from block_name -> {feature_1: val_1,...}
35
36    Raises:
37        IndexError if .icf is malformed (at least compared to how ST makes them)
38    """
39    symbols = {}
40    regions = {}  # region: (start_addr, end_addr)
41    blocks = {}
42    for line in icf_file.split('\n'):
43        line = line.strip()
44        if line == '' or line.startswith('/*') or line.startswith('//'):
45            continue
46        tokens = line.split()
47        if len(tokens) < 2:
48            continue
49        if tokens[0] == 'define':
50            if tokens[1] == 'symbol':
51                symbols[tokens[2]] = tokens[4].strip(';')
52            elif tokens[1] == 'region':
53                regions[tokens[2].split('_')[0]] = (
54                    tokens[5],
55                    tokens[7].strip('];'),
56                )
57            elif tokens[1] == 'block':
58                blocks[tokens[2]] = {
59                    tokens[4]: tokens[6].strip(','),
60                    tokens[7]: tokens[9],
61                }
62    parsed_regions = {
63        region: (
64            symbols[start] if start in symbols else start,
65            symbols[end] if end in symbols else end,
66        )
67        for region, (start, end) in regions.items()
68    }
69
70    parsed_blocks = {
71        name: {k: symbols[v] if v in symbols else v for k, v in fields.items()}
72        for name, fields in blocks.items()
73    }
74
75    return (parsed_regions, parsed_blocks)
76
77
78def icf_regions_to_ld_regions(icf_regions: dict) -> dict:
79    """Converts .icf regions to .ld regions
80
81    The .icf format specifies the beginning and end of each region, while
82    .ld expects the beginning and a length string.
83
84    Args:
85        icf_regions: icf_regions parsed with `parse_icf()`
86
87    Returns:
88        A map from `region_name` -> (start_hex, length_str)
89    """
90    ld_regions = {}
91    for region, (start, end) in icf_regions.items():
92        start_dec = int(start, 16)
93        end_dec = int(end, 16)
94        length = end_dec - start_dec + 1
95        length_str = str(length)
96        if length % 1024 == 0:
97            length_str = f'{int(length/1024)}K'
98
99        # Some icf scripts incorrectly have an exclusive region end.
100        # This corrects for that.
101        elif (length - 1) % 1024 == 0:
102            length_str = f'{int((length-1)/1024)}K'
103
104        # ST's gcc linker scripts generally use FLASH instead of ROM
105        if region == 'ROM':
106            region = 'FLASH'
107
108        ld_regions[region] = (start, length_str)
109
110    return ld_regions
111
112
113def create_ld(ld_regions: dict, blocks: dict) -> str:
114    """Create .ld file from template.
115
116    This creates a barebones .ld file that *should* work for most single core
117    stm32 families. It only contains regions for RAM and FLASH.
118
119    This template can be bypassed in GN if a more sophisticated linker file
120    is required.
121
122    Args:
123        ld_regions: generated by `icf_regions_to_ld_regions()`
124        blocks: generated by `parse_icf`
125
126    Returns:
127        a string linker file with the RAM/FLASH specified by the given reginos.
128
129    Raises:
130        KeyError if ld_regions does not contain 'RAM' and 'FLASH'
131    """
132    return f"""\
133ENTRY(Reset_Handler)
134_estack = ORIGIN(RAM) + LENGTH(RAM);
135
136_Min_Heap_Size = {blocks['HEAP']['size']};
137_Min_Stack_Size = {blocks['CSTACK']['size']};
138
139MEMORY
140{{
141  RAM (xrw) : ORIGIN = {ld_regions['RAM'][0]}, LENGTH = {ld_regions['RAM'][1]}
142  FLASH (rx) : ORIGIN = {ld_regions['FLASH'][0]}, LENGTH = {ld_regions['FLASH'][1]}
143}}
144
145SECTIONS
146{{
147
148  /* The ARMv8-M architecture requires this is at least aligned to 128 bytes,
149   * and aligned to a power of two that is greater than 4 times the number of
150   * supported exceptions. 512 has been selected as it accommodates most vector
151   * tables.
152   */
153  .isr_vector :
154  {{
155    . = ALIGN(512);
156    KEEP(*(.isr_vector))
157    . = ALIGN(4);
158  }} >FLASH
159
160  .text :
161  {{
162    . = ALIGN(4);
163    *(.text)
164    *(.text*)
165    *(.glue_7)
166    *(.glue_7t)
167    *(.eh_frame)
168
169    KEEP (*(.init))
170    KEEP (*(.fini))
171
172    . = ALIGN(4);
173    _etext = .;
174  }} >FLASH
175
176  .rodata :
177  {{
178    . = ALIGN(4);
179    *(.rodata)
180    *(.rodata*)
181    . = ALIGN(4);
182  }} >FLASH
183
184  .ARM.extab   : {{
185    . = ALIGN(4);
186    *(.ARM.extab* .gnu.linkonce.armextab.*)
187    . = ALIGN(4);
188  }} >FLASH
189
190  .ARM : {{
191    . = ALIGN(4);
192    __exidx_start = .;
193    *(.ARM.exidx*)
194    __exidx_end = .;
195    . = ALIGN(4);
196  }} >FLASH
197
198  .preinit_array     :
199  {{
200    . = ALIGN(4);
201    PROVIDE_HIDDEN (__preinit_array_start = .);
202    KEEP (*(.preinit_array*))
203    PROVIDE_HIDDEN (__preinit_array_end = .);
204    . = ALIGN(4);
205  }} >FLASH
206
207  .init_array :
208  {{
209    . = ALIGN(4);
210    PROVIDE_HIDDEN (__init_array_start = .);
211    KEEP (*(SORT(.init_array.*)))
212    KEEP (*(.init_array*))
213    PROVIDE_HIDDEN (__init_array_end = .);
214    . = ALIGN(4);
215  }} >FLASH
216
217  .fini_array :
218  {{
219    . = ALIGN(4);
220    PROVIDE_HIDDEN (__fini_array_start = .);
221    KEEP (*(SORT(.fini_array.*)))
222    KEEP (*(.fini_array*))
223    PROVIDE_HIDDEN (__fini_array_end = .);
224    . = ALIGN(4);
225  }} >FLASH
226
227  _sidata = LOADADDR(.data);
228  .data :
229  {{
230    . = ALIGN(4);
231    _sdata = .;
232    *(.data)
233    *(.data*)
234    . = ALIGN(4);
235    _edata = .;
236  }} >RAM AT> FLASH
237
238  . = ALIGN(4);
239  .bss :
240  {{
241    _sbss = .;
242    __bss_start__ = _sbss;
243    *(.bss)
244    *(.bss*)
245    *(COMMON)
246
247    . = ALIGN(4);
248    _ebss = .;
249    __bss_end__ = _ebss;
250  }} >RAM
251
252  /* The ARMv7-M architecture may require 8-byte alignment of the stack pointer
253   * rather than 4 in some contexts and implementations, so this region is
254   * 8-byte aligned (see ARMv7-M Architecture Reference Manual DDI0403E
255   * section B1.5.7).
256   */
257  ._user_heap_stack :
258  {{
259    . = ALIGN(8);
260    PROVIDE ( end = . );
261    PROVIDE ( _end = . );
262    . = . + _Min_Heap_Size;
263    . = . + _Min_Stack_Size;
264    . = ALIGN(8);
265  }} >RAM
266
267  /DISCARD/ :
268  {{
269    libc.a ( * )
270    libm.a ( * )
271    libgcc.a ( * )
272  }}
273
274  .ARM.attributes 0 : {{ *(.ARM.attributes) }}
275}}
276    """
277
278
279def icf_to_ld(icf_path: pathlib.Path, ld_path: pathlib.Path | None):
280    """Convert icf file into an ld file.
281
282    Note: This only works for ST generated .icf files.
283
284    Args:
285        icf_path: path to .icf file to convert
286        ld_path: path to write generated .ld file or None.
287                 If None, the .ld file is written to stdout.
288    """
289    with open(icf_path, 'rb') as icf_file:
290        icf_str = icf_file.read().decode('utf-8', errors='ignore')
291
292    icf_regions, blocks = parse_icf(icf_str)
293    ld_regions = icf_regions_to_ld_regions(icf_regions)
294    ld_str = create_ld(ld_regions, blocks)
295
296    if ld_path:
297        with open(ld_path, 'w') as ld_file:
298            ld_file.write(ld_str)
299    else:
300        print(ld_str)
301