diff --git a/pyocd/target/builtin/target_LPC55S69Jxxxxx.py b/pyocd/target/builtin/target_LPC55S69Jxxxxx.py index ac5dec854..643fd4ecb 100644 --- a/pyocd/target/builtin/target_LPC55S69Jxxxxx.py +++ b/pyocd/target/builtin/target_LPC55S69Jxxxxx.py @@ -1,5 +1,5 @@ # pyOCD debugger -# Copyright (c) 2019-2020 Arm Limited +# Copyright (c) 2019-2022 Arm Limited # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,13 +18,17 @@ from ...core.memory_map import (FlashRegion, RamRegion, RomRegion, MemoryMap) from ...debug.svd.loader import SVDFile +# Generated from 'LPC55XX_640.FLM' (LPC55xx IAP 608kB Flash) +# Originating from 'NXP.LPC55S69_DFP.13.1.0.pack' +# digest = c672c27550f789743829bb8832245f8c6f0c8ea81b9291b53827849eeabbe52c, file size = 22316 +# algo version = 0x101, algo size = 1696 (0x6a0) # Note: the DFP has both S and NS flash algos, but they are exactly the same except for the address range. FLASH_ALGO = { 'load_address' : 0x20000000, # Flash algorithm as a hex string 'instructions': [ - 0xE00ABE00, 0x062D780D, 0x24084068, 0xD3000040, 0x1E644058, 0x1C49D1FA, 0x2A001E52, 0x4770D1F2, + 0xe7fdbe00, 0xf240b580, 0xf2c00004, 0xf6420000, 0xf84961e0, 0xf2401000, 0xf2c52000, 0x21000000, 0x1080f8c0, 0x1084f8c0, 0x1180f8c0, 0x71fbf647, 0xf6406001, 0x21ff6004, 0x0000f2c5, 0x01def2cc, 0xf04f6001, 0x210240a0, 0xf2407001, 0xf2c00010, 0x44480000, 0xf874f000, 0xbf182800, 0xbd802001, 0x47702000, @@ -81,21 +85,33 @@ ], # Relative function addresses - 'pc_init': 0x20000021, - 'pc_unInit': 0x2000007d, - 'pc_program_page': 0x200000d1, - 'pc_erase_sector': 0x200000a9, - 'pc_eraseAll': 0x20000081, + 'pc_init': 0x20000005, + 'pc_unInit': 0x20000061, + 'pc_program_page': 0x200000b5, + 'pc_erase_sector': 0x2000008d, + 'pc_eraseAll': 0x20000065, - 'static_base' : 0x20000000 + 0x00000020 + 0x00000650, - 'begin_stack' : 0x20000900, + 'static_base' : 0x20000000 + 0x00000004 + 0x00000650, + 'begin_stack' : 0x20001000, 'begin_data' : 0x20000000 + 0x1000, 'page_size' : 0x200, 'analyzer_supported' : False, 'analyzer_address' : 0x00000000, - 'page_buffers' : [0x20001000, 0x20001200], # Enable double buffering + # Enable double buffering + 'page_buffers' : [ + 0x20001000, + 0x20001200 + ], 'min_program_length' : 0x200, + # Relative region addresses and sizes + 'ro_start': 0x0, + 'ro_size': 0x650, + 'rw_start': 0x650, + 'rw_size': 0x4, + 'zi_start': 0x654, + 'zi_size': 0x48, + # Flash information 'flash_start': 0x0, 'flash_size': 0x98000, diff --git a/scripts/generate_flash_algo.py b/scripts/generate_flash_algo.py index e98c00b25..91edf79a9 100755 --- a/scripts/generate_flash_algo.py +++ b/scripts/generate_flash_algo.py @@ -19,6 +19,7 @@ import os import argparse import colorama +import hashlib from datetime import datetime import struct import binascii @@ -37,7 +38,7 @@ BLOB_HEADER = '0xe7fdbe00,' HEADER_SIZE = 4 -STACK_SIZE = 0x200 +STACK_SIZE = 0x800 PYOCD_TEMPLATE = \ """# pyOCD debugger @@ -56,6 +57,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Generated from '{{ filename }}'{%- if algo.flash_info.name %}{{ " (%s)" % algo.flash_info.name.decode('utf-8') }}{% endif %} +{%- if pack_file %} +# Originating from '{{ pack_file }}' +{%- endif %} +# digest = {{ digest }}, file size = {{ file_size}} +# {{ 'algo version = 0x%x, algo size = %d (0x%x)' % (algo.flash_info.version, algo_size + header_size, algo_size + header_size) }} FLASH_ALGO = { 'load_address' : {{'0x%08x' % entry}}, @@ -78,7 +85,11 @@ 'page_size' : {{'0x%x' % algo.page_size}}, 'analyzer_supported' : False, 'analyzer_address' : 0x00000000, - 'page_buffers' : [{{'0x%08x' % (entry + 4096)}}, {{'0x%08x' % (entry + 4096 + algo.page_size)}}], # Enable double buffering + # Enable double buffering + 'page_buffers' : [ + {{'0x%08x' % (page_buffers[0])}}, + {{'0x%08x' % (page_buffers[1])}} + ], 'min_program_length' : {{'0x%x' % algo.page_size}}, # Relative region addresses and sizes @@ -131,7 +142,7 @@ def format_algo_data(self, spaces, group_size, fmt): blob = binascii.b2a_hex(self.algo_data) line_list = [] for i in range(0, len(blob), group_size): - line_list.append('"' + blob[i:i + group_size] + '"') + line_list.append('"' + blob[i:i + group_size].decode() + '"') return ("\n" + padding).join(line_list) elif fmt == "c": blob = self.algo_data[:] @@ -145,7 +156,7 @@ def format_algo_data(self, spaces, group_size, fmt): line_list.append(", ".join(group)) return (",\n" + padding).join(line_list) else: - raise Exception("Unsupported format %s" % fmt) + raise ValueError("Unsupported format %s" % fmt) def process_template(self, template_text, data_dict=None): """ @@ -175,6 +186,7 @@ def main(): "address of the flash blob in target RAM.") parser.add_argument("--stack-size", default=STACK_SIZE, type=str_to_num, help="Stack size for the algo " f"(default {STACK_SIZE}).") + parser.add_argument("--pack-path", default=None, help="Path to pack file from which flash algo is from") parser.add_argument("-i", "--info-only", action="store_true", help="Only print information about the flash " "algo, do not generate a blob.") parser.add_argument("-o", "--output", default="pyocd_blob.py", help="Path of output file " @@ -184,7 +196,7 @@ def main(): parser.add_argument('-c', '--copyright', help="Set copyright owner.") args = parser.parse_args() - if not args.copyright: + if not args.copyright and not args.info_only: print(f"{colorama.Fore.YELLOW}Warning! No copyright owner was specified. Defaulting to \"PyOCD Authors\". " f"Please set via --copyright, or edit output.{colorama.Style.RESET_ALL}") @@ -199,21 +211,73 @@ def main(): print(algo.flash_info) - if args.info_only: - return - - # Allocate stack after algo and its rw data, with top and bottom rounded to 8 bytes. - stack_base = args.blob_start + HEADER_SIZE + algo.rw_start + algo.rw_size + # Allocate stack after algo and its rw/zi data, with top and bottom rounded to 8 bytes. + stack_base = (args.blob_start + HEADER_SIZE + + algo.rw_start + algo.rw_size # rw_start incorporates instruction size + + algo.zi_size) stack_base = (stack_base + 7) // 8 * 8 sp = stack_base + args.stack_size sp = (sp + 7) // 8 * 8 + page_buffers = [ + ((sp + algo.page_size - 1) // algo.page_size * algo.page_size), + ((sp + algo.page_size - 1) // algo.page_size * algo.page_size) + algo.page_size, + ] + + # Increase stack size by placing the SP at the base of first page buffer. + sp = page_buffers[0] + + print(f"load addr: {args.blob_start:#010x}") + print(f"header: {HEADER_SIZE:#x} bytes") + print(f"data: {len(algo.algo_data):#x} bytes") + print(f"ro: {algo.ro_start:#010x} + {algo.ro_size:#x} bytes") + print(f"rw: {algo.rw_start:#010x} + {algo.rw_size:#x} bytes") + print(f"zi: {algo.zi_start:#010x} + {algo.zi_size:#x} bytes") + print(f"stack: {stack_base:#010x} .. {sp:#010x} ({sp - stack_base:#x} bytes)") + print(f"buffer[0]: {page_buffers[0]:#010x}") + print(f"buffer[1]: {page_buffers[1]:#010x}") + + print("\nSymbol offsets:") + for n, v in algo.symbols.items(): + if v >= 0xffffffff: + continue + print(f"{n}:{' ' * (11 - len(n))} {v:#010x}") + + if args.info_only: + return + + pack_file = None + if args.pack_path and os.path.exists(args.pack_path) and 'cmsis-pack-manager' in args.pack_path: + (rest, version) = (os.path.split(args.pack_path)) + (rest, pack) = (os.path.split(rest)) + (_, vendor) = (os.path.split(rest)) + pack_file = '%s.%s.%s' % (vendor, pack, version) + elif args.pack_path: + pack_file = os.path.split(args.pack_path)[-1] + else: + print(f"{colorama.Fore.YELLOW}Warning! No CMSIS Pack was set." + f"Please set the path or file name of the CMSIS pack via --pack-path.{colorama.Style.RESET_ALL}") + + file_handle.seek(0) + flm_content = file_handle.read() + hash = hashlib.sha256() + hash.update(flm_content) + + if len(algo.sector_sizes) > 1: + print(f"{colorama.Fore.YELLOW}Warning! Flash has more than one sector size. Remember to create one flash memory region for each sector size range.{colorama.Style.RESET_ALL}") + data_dict = { + 'filename': os.path.split(args.elf_path)[-1], + 'digest': hash.hexdigest(), + 'file_size': len(flm_content), + 'pack_file': pack_file, + 'algo_size': len(algo.algo_data), 'name': os.path.splitext(os.path.split(args.elf_path)[-1])[0], 'prog_header': BLOB_HEADER, 'header_size': HEADER_SIZE, 'entry': args.blob_start, 'stack_pointer': sp, + 'page_buffers': page_buffers, 'year': datetime.now().year, 'copyright_owner': args.copyright or "PyOCD Authors", } diff --git a/test/verify_flash_algos.py b/test/verify_flash_algos.py new file mode 100755 index 000000000..e5a17c768 --- /dev/null +++ b/test/verify_flash_algos.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2021 Chris Reed +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +from importlib import import_module +from pathlib import Path +from typing import (Any, Dict, Iterator, List) + +import pyocd +from pyocd.core.memory_map import MemoryRange + +class FlashAlgoVerifyFailure(Exception): + pass + +FlashAlgoDict = Dict[str, Any] + +def range_str(range: MemoryRange) -> str: + return f"[{range.start:#010x}..{range.end:#010x}]" + +class FlashAlgoVerifier: + + REQUIRED_ENTRY_POINTS = ( + 'pc_init', + 'pc_program_page', + 'pc_erase_sector', + ) + + MINIMUM_STACK_SIZE = 256 + + def __init__(self, module_name: str, name: str, algo: FlashAlgoDict) -> None: + # print(f"Examining: {module_name}.{name}") + self.module_name = module_name + self.name = name + self.algo = algo + + # Get layout values. + try: + self.load_addr = algo['load_address'] + self.instr_size = len(algo['instructions']) * 4 + self.instr_top = self.load_addr + self.instr_size + self.instr_range = MemoryRange(start=self.load_addr, length=self.instr_size) + self.static_base = algo['static_base'] + self.stack_top = algo['begin_stack'] + self.page_buffers = sorted(algo.get('page_buffers', [algo['begin_data']])) + except KeyError as err: + raise FlashAlgoVerifyFailure(f"flash algo dict missing required key: {err}") from None + + # Compute page size + try: + self.page_size = algo['page_size'] + except KeyError as err: + if len(self.page_buffers) > 1: + self.page_size = self.page_buffers[1] - self.page_buffers[0] + else: + print(f"Warning: page_size key is not available and unable to compute page size for {self.module_name}.{self.name}") + self.page_size = 128 + + # Collect entry points. + self.entry_points = { + k: v + for k, v in algo.items() + if k.startswith('pc_') + } + if not all((n in self.entry_points) for n in self.REQUIRED_ENTRY_POINTS): + raise FlashAlgoVerifyFailure("flash algo dict missing required entry point") + + def verify(self) -> None: + # Entry points must be within instructions. + for name, addr in self.entry_points.items(): + is_disabled = (addr in (0, None)) + + # Make sure required entry points are not disabled. + if (name in self.REQUIRED_ENTRY_POINTS) and is_disabled: + raise FlashAlgoVerifyFailure("required entry point '{name}' is disabled (value {addr})") + + # Verify entry points are either disabled or reside within the loaded address range. + if not (is_disabled or self.instr_range.contains_address(addr)): + raise FlashAlgoVerifyFailure(f"entry point '{name}' not within instructions {range_str(self.instr_range)}") + + # Static base should be within the instructions, since the instructions are supposed to contain + # both rw and zi ready for loading. + if not self.instr_range.contains_address(self.static_base): + raise FlashAlgoVerifyFailure(f"static base {self.static_base:#010x} not within instructions {range_str(self.instr_range)}") + + # Verify stack basics. + if self.instr_range.contains_address(self.stack_top): + raise FlashAlgoVerifyFailure(f"stack top {self.stack_top:#010x} is within instructions {range_str(self.instr_range)}") + + buffers_top = self.page_buffers[-1] + self.page_size + + # Compute max stack size. + if self.stack_top > self.instr_top and self.stack_top <= self.page_buffers[0]: + stack_size = self.stack_top - self.instr_top + elif self.stack_top > buffers_top: + stack_size = self.stack_top - buffers_top + else: + stack_size = 0 + print(f"Warning: unable to compute stack size for {self.module_name}.{self.name}") + + stack_range = MemoryRange(start=(self.stack_top - stack_size), length=stack_size) + + # Minimum stack size. + if (stack_size != 0) and (stack_size < self.MINIMUM_STACK_SIZE): + raise FlashAlgoVerifyFailure(f"stack size {stack_size} is below minimum {self.MINIMUM_STACK_SIZE}") + + # Page buffers. + for base_addr in self.page_buffers: + buffer_range = MemoryRange(start=base_addr, length=self.page_size) + + if buffer_range.intersects_range(self.instr_range): + raise FlashAlgoVerifyFailure(f"buffer {base_addr:#010x} overlaps instructsion {range_str(self.instr_range)}") + if buffer_range.intersects_range(stack_range): + raise FlashAlgoVerifyFailure(f"buffer {range_str(buffer_range)} overlaps stack {range_str(stack_range)}") + + +def collect_modules(dotted_path: str, dir_path: Path) -> Iterator[str]: + """@brief Yield dotted names of all modules contained within the given package.""" + for entry in sorted(dir_path.iterdir(), key=lambda v: v.name): + # Primitive tests for modules and packages. + is_subpackage = (entry.is_dir() and (entry / "__init__.py").exists()) + is_module = entry.suffix == ".py" + module_name = dotted_path + '.' + entry.stem + + # Yield this module's name. + if is_module: + yield module_name + # Recursively yield modules from valid sub-packages. + elif is_subpackage: + for name in collect_modules(module_name, entry): + yield name + + +def is_algo_dict(n: str, o: Any) -> bool: + """@brief Test whether a dict contains a flash algo.""" + return (isinstance(o, Dict) + and (n != '__builtins__') + and 'instructions' in o + and 'pc_program_page' in o) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Flash algo verifier") + parser.add_argument("module", nargs='*', help="pyOCD module name containing flash algos to verify") + args = parser.parse_args() + + pyocd_path = Path(pyocd.__file__).parent.resolve() + # print(f"pyocd package path: {pyocd_path}") + + if not args.module: + target_module_names = collect_modules('pyocd.target.builtin', pyocd_path / 'target' / 'builtin') + else: + target_module_names = args.module + + for module_name in target_module_names: + # print(f"Importing: {module_name}") + module = import_module(module_name) + + # Scan for algo dictionaries in the module. This assumes they are defined at the module level, + # which is the case for all current targets. + algos_iter = ((n, v) for n, v in module.__dict__.items() if is_algo_dict(n, v)) + + for name, algo in algos_iter: + try: + FlashAlgoVerifier(module_name, name, algo).verify() + except FlashAlgoVerifyFailure as err: + print(f"Error: {module_name}.{name}: {err}") + + +if __name__ == "__main__": + main()