diff --git a/README.md b/README.md index 11b73e4..53482f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ltchiptool -Universal, easy-to-use GUI flashing/dumping tool for BK7231, RTL8710B and RTL8720C. Also contains some CLI utilities for binary firmware manipulation. +Universal, easy-to-use GUI flashing/dumping tool for BK7231, LN882H, RTL8710B and RTL8720C. Also contains some CLI utilities for binary firmware manipulation.
diff --git a/ltchiptool/soc/interface.py b/ltchiptool/soc/interface.py index 0858047..4af3055 100644 --- a/ltchiptool/soc/interface.py +++ b/ltchiptool/soc/interface.py @@ -28,6 +28,9 @@ def get(cls, family: Family) -> "SocInterface": if family.is_child_of("realtek-ambz2"): from .ambz2 import AmebaZ2Main return AmebaZ2Main(family) + if family.is_child_of("lightning-ln882x"): + from .ln882x import LN882xMain + return LN882xMain(family) # fmt: on raise NotImplementedError(f"Unsupported family - {family.name}") @@ -38,6 +41,7 @@ def get_family_names(cls) -> List[str]: "beken-72xx", "realtek-ambz", "realtek-ambz2", + "lightning-ln882x", ] ######################### diff --git a/ltchiptool/soc/ln882x/__init__.py b/ltchiptool/soc/ln882x/__init__.py new file mode 100644 index 0000000..852c182 --- /dev/null +++ b/ltchiptool/soc/ln882x/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Etienne Le Cousin 2025-01-02. + +from .main import LN882xMain + +__all__ = [ + "LN882xMain", +] diff --git a/ltchiptool/soc/ln882x/binary.py b/ltchiptool/soc/ln882x/binary.py new file mode 100644 index 0000000..f85d005 --- /dev/null +++ b/ltchiptool/soc/ln882x/binary.py @@ -0,0 +1,98 @@ +# Copyright (c) Etienne Le Cousin 2025-01-02. + +from abc import ABC +from datetime import datetime +from logging import warning +from os import stat +from os.path import basename, dirname, join, realpath +from typing import IO, List, Optional, Union +import json + +from ltchiptool import SocInterface +from ltchiptool.util.fileio import chext +from ltchiptool.util.fwbinary import FirmwareBinary +from ltchiptool.util.lvm import LVM + +from .util import MakeImageTool, OTATOOL + + +class LN882xBinary(SocInterface, ABC): + def elf2bin(self, input: str, ota_idx: int) -> List[FirmwareBinary]: + toolchain = self.board.toolchain + + bootfile = join(LVM.get().path(), f"cores", self.family.name, f"misc", self.board["build.bootfile"]) + part_cfg = join(dirname(input), "flash_partition_cfg.json") + + self.gen_partcfg_json(part_cfg) + + # build output names + output_fw = FirmwareBinary( + location=input, + name=f"firmware", + subname="", + ext="bin", + title="Flash Image", + description="Complete image with boot for flashing at offset 0", + public=True, + ) + + fw_bin = chext(input, "bin") + # objcopy ELF -> raw BIN + toolchain.objcopy(input, fw_bin) + + # Make firmware image + mkimage = MakeImageTool() + mkimage.boot_filepath = bootfile + mkimage.app_filepath = fw_bin + mkimage.flashimage_filepath = output_fw.path + mkimage.part_cfg_filepath = part_cfg + mkimage.ver_str = "1.0" + mkimage.swd_crp = 0 + mkimage.doAllWork() + + # Make ota image + ota_tool = OTATOOL() + ota_tool.input_filepath = output_fw.path + ota_tool.output_dir = dirname(input) + ota_tool.doAllWork() + + output_ota = FirmwareBinary.load( + location = ota_tool.output_filepath, + obj = { + "filename": basename(ota_tool.output_filepath), + "title": "Flash OTA Image", + "description": "Compressed App image for OTA flashing", + "public": True, + } + ) + _, ota_size, _ = self.board.region("ota") + if stat(ota_tool.output_filepath).st_size > ota_size: + warning( + f"OTA size too large: {ota_tool.output_filepath} > {ota_size} (0x{ota_size:X})" + ) + + return output_fw.group() + + def gen_partcfg_json(self, output: str): + flash_layout = self.board["flash"] + + # find all partitions + partitions = [] + for name, layout in flash_layout.items(): + part = {} + (offset, _, length) = layout.partition("+") + offset = int(offset, 16) + length = int(length, 16) + part["partition_type"] = name.upper() + part["start_addr"] = f"0x{offset:08X}" + part["size_KB"] = length // 1024 + partitions.append(part) + + partcfg: dict = { + "vendor_define": [], # boot and part_tab should be there but it's not needed + "user_define": partitions # so put all partitions in user define + } + # export file + with open(output, "w") as f: + json.dump(partcfg, f, indent="\t") + diff --git a/ltchiptool/soc/ln882x/flash.py b/ltchiptool/soc/ln882x/flash.py new file mode 100644 index 0000000..2040176 --- /dev/null +++ b/ltchiptool/soc/ln882x/flash.py @@ -0,0 +1,48 @@ +# Copyright (c) Etienne Le Cousin 2025-01-02. + +import logging +import struct +from abc import ABC +from binascii import crc32 +from logging import DEBUG, debug, warning +from typing import IO, Generator, List, Optional, Tuple, Union + +from ltchiptool import SocInterface +from ltchiptool.util.flash import FlashConnection, FlashFeatures, FlashMemoryType +from ltchiptool.util.intbin import gen2bytes, inttole32 +from ltchiptool.util.logging import VERBOSE, verbose +from ltchiptool.util.misc import sizeof +from ltchiptool.util.streams import ProgressCallback +from uf2tool import OTAScheme, UploadContext + +LN882x_GUIDE = [ + "Connect UART1 of the LN882x to the USB-TTL adapter:", + [ + ("PC", "LN882x"), + ("RX", "TX1 (GPIOA2 / P2)"), + ("TX", "RX1 (GPIOA3 / P3)"), + ("", ""), + ("GND", "GND"), + ], + "Using a good, stable 3.3V power supply is crucial. Most flashing issues\n" + "are caused by either voltage drops during intensive flash operations,\n" + "or bad/loose wires.", + "The UART adapter's 3.3V power regulator is usually not enough. Instead,\n" + "a regulated bench power supply, or a linear 1117-type regulator is recommended.", + "To enter download mode, the chip has to be rebooted while the flashing program\n" + "is trying to establish communication.\n" + "In order to do that, you need to bridge CEN/BOOT pin (GPIOA9) to GND with a wire.", +] + + +class LN882xFlash(SocInterface, ABC): + info: List[Tuple[str, str]] = None + + def flash_get_features(self) -> FlashFeatures: + return FlashFeatures() + + def flash_get_guide(self) -> List[Union[str, list]]: + return LN882X_GUIDE + + def flash_get_docs_url(self) -> Optional[str]: + return "https://docs.libretiny.eu/link/flashing-ln882x" diff --git a/ltchiptool/soc/ln882x/main.py b/ltchiptool/soc/ln882x/main.py new file mode 100644 index 0000000..cf207e9 --- /dev/null +++ b/ltchiptool/soc/ln882x/main.py @@ -0,0 +1,34 @@ +# Copyright (c) Etienne Le Cousin 2025-01-02. + +from abc import ABC +from logging import info +from typing import Optional + +from ltchiptool import Family +from ltchiptool.models import OTAType +from ltchiptool.soc import SocInterfaceCommon + +from .binary import LN882xBinary +from .flash import LN882xFlash + + +class LN882xMain( + LN882xBinary, + LN882xFlash, + SocInterfaceCommon, + ABC, +): + def __init__(self, family: Family) -> None: + super().__init__() + self.family = family + + def hello(self): + info("Hello from LN882x") + + @property + def ota_type(self) -> Optional[OTAType]: + return OTAType.SINGLE + + @property + def ota_supports_format_1(self) -> bool: + return True diff --git a/ltchiptool/soc/ln882x/util/__init__.py b/ltchiptool/soc/ln882x/util/__init__.py new file mode 100644 index 0000000..6b539b4 --- /dev/null +++ b/ltchiptool/soc/ln882x/util/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) Etienne Le Cousin 2025-01-02. + +from .makeimage import MakeImageTool +from .ota_image_generator import OTATOOL + +__all__ = [ + "MakeImageTool", + "OTATOOL", +] diff --git a/ltchiptool/soc/ln882x/util/boot_header.py b/ltchiptool/soc/ln882x/util/boot_header.py new file mode 100644 index 0000000..508a930 --- /dev/null +++ b/ltchiptool/soc/ln882x/util/boot_header.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +# +# Copyright 2021 Shanghai Lightning Semiconductor Technology Co., LTD + +# 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 zlib +import struct +from .ln_tools import * + + +class BootHeader: + + BOOT_HEADER_SIZE = (4 + 2 + 2 + 4 * 4) + + CRP_VALID_FLAG = 0x46505243 + + BOOT_START_ADDR = 0 + BOOT_SIZE_LIMIT = (1024 * 24) + + def __init__(self, other_buf) -> None: + self.__bootram_target_addr = 0 + self.__bootram_bin_length = 0 # 2bytes + self.__bootram_crc_offset = 0 # 2bytes + self.__bootram_crc_value = 0 + self.__bootram_vector_addr = 0 + self.__crp_flag = 0 + self.__boot_header_crc = 0 + + if not (isinstance(other_buf, bytearray) or isinstance(other_buf, bytes)): + raise TypeError("Error: other_buf MUST be a bytearray or bytes!!!") + + if len(other_buf) < BootHeader.BOOT_HEADER_SIZE: + raise ValueError("Error: other_buf MUST have at least {} bytes!!!".format(BootHeader.BOOT_HEADER_SIZE)) + + self.__buffer = bytearray(BootHeader.BOOT_HEADER_SIZE) + self.__buffer[:] = other_buf[0:BootHeader.BOOT_HEADER_SIZE] + + items = struct.unpack(" bytearray: + struct.pack_into("> (8*shift) ) + return val + + +def dump_bytes_in_hex(byte_arr=None, lineSize=16, bytesMax=256, title=""): + """ + Print byte array in hex format. + lineSize: print how many items each line. + bytesMax: print how many items at most. (-1, print the whole byte array.) + title: + """ + + if title: + print("\n---------- {} ----------".format(title)) + + if bytesMax == -1: + bytesMax = len(byte_arr) + elif bytesMax > len(byte_arr): + bytesMax = len(byte_arr) + else: + pass + + for cnt in range(0, bytesMax): + if cnt % lineSize == 0: + print("{_so:08X} |".format(_so=cnt), end=" ") + print("{_b:02X}".format(_b=byte_arr[cnt]), end=" ") + if cnt % lineSize == (lineSize-1): + print("") + + +def check_python_version(): + major = sys.version_info.major + minor = sys.version_info.minor + if (major == 3) and (minor >= 6): + return True + else: + print('WARNING: Python 2 or Python 3 versions older than 3.6 are not supported.', file=sys.stderr) + exit(-100) + return False diff --git a/ltchiptool/soc/ln882x/util/makeimage.py b/ltchiptool/soc/ln882x/util/makeimage.py new file mode 100644 index 0000000..31569ba --- /dev/null +++ b/ltchiptool/soc/ln882x/util/makeimage.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +# +# Copyright 2021 Shanghai Lightning Semiconductor Technology Co., LTD + +# 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 +import json + +from .boot_header import * +from .image_header import * +from .part_desc_info import * + + +class MakeImageTool: + def __init__(self) -> None: + self.__boot_filepath = None + self.__app_filepath = None + self.__flashimage_filepath = None + self.__part_cfg_filepath = None + self.__ver_str = None + self.__ver_major = 0 + self.__ver_minor = 0 + self.__swd_crp = 0 + self.__verbose = 0 + + self.__part_desc_info_list = [] + self.__partbuf_bootram = None + self.__partbuf_parttab = None + self.__partbuf_nvds = None + self.__partbuf_app = None + self.__partbuf_kv = None + self.__partbuf_eeprom = None + + def readPartCfg(self) -> bool: + try: + with open(self.part_cfg_filepath, "r", encoding="utf-8") as fObj: + root_node = json.load(fp=fObj) + vendor_node = root_node["vendor_define"] + user_node = root_node["user_define"] + + for node in vendor_node: + parttype = part_type_str2num(node["partition_type"]) + startaddr = int(node["start_addr"], 16) + partsize = node["size_KB"] * 1024 + + part_info = PartDescInfo(parttype=parttype, startaddr=startaddr, partsize=partsize) + self.__part_desc_info_list.append(part_info) + + for node in user_node: + parttype = part_type_str2num(node["partition_type"]) + startaddr = int(node["start_addr"], 16) + partsize = node["size_KB"] * 1024 + + part_info = PartDescInfo(parttype=parttype, startaddr=startaddr, partsize=partsize) + self.__part_desc_info_list.append(part_info) + + except Exception as err: + print("Error: open partition cfg file failed: {}".format(str(err))) + return False + + if len(self.__part_desc_info_list) >= 4: + print("----------" * 10) + for item in self.__part_desc_info_list: + print(item) + print("----------" * 10) + + return True + + return False + + def genPartBufPartTab(self) -> bool: + parttab_part = self.getPartDescInfoFromList(PART_TYPE_PART_TAB) + if not parttab_part: + print("Error: partition table has not been found!!!") + return False + + part_tab_buffer = bytearray(parttab_part.part_size) + part_tab_buffer = part_tab_buffer.replace(b'\x00', b'\xFF') + + offset = 0 + for part in self.__part_desc_info_list: + if isinstance(part, PartDescInfo): + if (part.part_type == PART_TYPE_BOOT) or (part.part_type == PART_TYPE_PART_TAB) or (part.part_type == PART_TYPE_INVALID): + continue + part_tab_buffer[offset:(offset+PARTITION_DESC_INFO_SIZE)] = part.toBytes() + offset += PARTITION_DESC_INFO_SIZE + + part_tab_buffer[offset:(offset+PARTITION_DESC_INFO_SIZE)] = bytearray(PARTITION_DESC_INFO_SIZE)[:] + + self.__partbuf_parttab = part_tab_buffer + return True + + def getPartDescInfoFromList(self, part_type) -> PartDescInfo: + if not isinstance(part_type, int): + raise TypeError("Error: part_type MUST be an int value!!!") + + for part_info in self.__part_desc_info_list: + if isinstance(part_info, PartDescInfo): + if part_info.part_type == part_type: + return part_info + return None + + def checkFileSize(self) -> bool: + boot_fileinfo = os.stat(self.boot_filepath) + app_fileinfo = os.stat(self.app_filepath) + + max_boot_filesize = self.getPartDescInfoFromList(PART_TYPE_BOOT).part_size + max_app_filesize = self.getPartDescInfoFromList(PART_TYPE_APP).part_size + + if boot_fileinfo.st_size >= max_boot_filesize: + print("FAIL -- checking {}".format(self.boot_filepath)) + return False + print("PASS -- checking {}".format(self.boot_filepath)) + + if app_fileinfo.st_size >= max_app_filesize: + print("FAIL -- checking {}".format(self.app_filepath)) + return False + print("PASS -- checking {}".format(self.app_filepath)) + + + return True + + def genPartBufBootRam(self) -> bool: + boot_part = self.getPartDescInfoFromList(PART_TYPE_BOOT) + if not boot_part: + print("Error: BOOT partition has not been found!!!") + return False + + bootram_buffer = bytearray(boot_part.part_size) + bootram_buffer = bootram_buffer.replace(b'\x00', b'\xFF') + + fileInfo = os.stat(self.boot_filepath) + try: + with open(self.boot_filepath, "rb") as fObj: + bootram_content = fObj.read() + bootheader = BootHeader(bootram_content) + if self.swd_crp == 0: + bootheader.crp_flag = 0 + else: + bootheader.crp_flag = BootHeader.CRP_VALID_FLAG + + bootram_buffer[0:BootHeader.BOOT_HEADER_SIZE] = bootheader.toByteArray()[:] + bootram_buffer[BootHeader.BOOT_HEADER_SIZE:fileInfo.st_size] = bootram_content[BootHeader.BOOT_HEADER_SIZE:fileInfo.st_size] + self.__partbuf_bootram = bootram_buffer + + except Exception as err: + print("Error: open boot file failed: {}".format(str(err))) + return False + + return True + + def genPartBufKV(self) -> bool: + kv_part = self.getPartDescInfoFromList(PART_TYPE_KV) + if kv_part: + kv_buffer = bytearray(kv_part.part_size) + kv_buffer = kv_buffer.replace(b'\x00', b'\xFF') + self.__partbuf_kv = kv_buffer + return True + + def genPartBufEEPROM(self) -> bool: + eeprom_part = self.getPartDescInfoFromList(PART_TYPE_SIMU_EEPROM) + if eeprom_part: + eeprom_buffer = bytearray(eeprom_part.part_size) + eeprom_buffer = eeprom_buffer.replace(b'\x00', b'\xFF') + self.__partbuf_eeprom = eeprom_buffer + return True + + def genPartBufAPP(self) -> bool: + app_part = self.getPartDescInfoFromList(PART_TYPE_APP) + if not app_part: + print("Error: APP part is not found in the partition table!!!") + return False + + try: + with open(self.app_filepath, "rb") as fObj: + app_content = fObj.read() + + image_header = ImageHeader(bytearray(256)) + image_header.image_type = IMAGE_TYPE_ORIGINAL + image_header.setVerMajor(self.__ver_major) + image_header.setVerMinor(self.__ver_minor) + image_header.img_size_orig = len(app_content) + image_header.img_crc32_orig = zlib.crc32(app_content) + + temp = bytearray(image_header.toBytes()) + temp.extend(app_content) + self.__partbuf_app = temp + except Exception as err: + print("Error: open app file failed: {}".format(str(err))) + return False + + if not self.__partbuf_app: + return False + + return True + + def writeOutputFile(self) -> bool: + if not self.__partbuf_bootram: + print("Error: ramcode has not been processed!!!") + return False + + if not self.__partbuf_parttab: + print("Error: partition table has not been processed!!!") + return False + + if not self.__partbuf_nvds: + nvds_part = self.getPartDescInfoFromList(PART_TYPE_NVDS) + if nvds_part: + nvds_buffer = bytearray(nvds_part.part_size) + nvds_buffer = nvds_buffer.replace(b'\x00', b'\xFF') + self.__partbuf_nvds = nvds_buffer + + if not self.__partbuf_app: + print("Error: app has not been processed!!!") + return False + + if not self.__partbuf_kv: + print("Error: KV has not been processed!!!") + return False + + try: + with open(self.flashimage_filepath, "wb") as fObj: + # ram code + ramcode_part = self.getPartDescInfoFromList(PART_TYPE_BOOT) + fObj.seek(ramcode_part.start_addr, os.SEEK_SET) + fObj.write(self.__partbuf_bootram) + + # partition table + parttab_part = self.getPartDescInfoFromList(PART_TYPE_PART_TAB) + fObj.seek(parttab_part.start_addr, os.SEEK_SET) + fObj.write(self.__partbuf_parttab) + + # APP + app_part = self.getPartDescInfoFromList(PART_TYPE_APP) + fObj.seek(app_part.start_addr, os.SEEK_SET) + fObj.write(self.__partbuf_app) + + # NVDS + nvds_part = self.getPartDescInfoFromList(PART_TYPE_NVDS) + if (not nvds_part) and nvds_part.start_addr < app_part.start_addr: + fObj.seek(nvds_part.start_addr, os.SEEK_SET) + fObj.write(self.__partbuf_nvds) + + # KV + kv_part = self.getPartDescInfoFromList(PART_TYPE_KV) + if kv_part.start_addr < app_part.start_addr: + fObj.seek(kv_part.start_addr, os.SEEK_SET) + fObj.write(self.__partbuf_kv) + + # SIMU_EEPROM + eeprom_part = self.getPartDescInfoFromList(PART_TYPE_SIMU_EEPROM) + if eeprom_part and (eeprom_part.start_addr < app_part.start_addr): + fObj.seek(eeprom_part.start_addr, os.SEEK_SET) + fObj.write(self.__partbuf_eeprom) + except Exception as err: + print("Error: open file failed: {}!!!".format(str(err))) + return False + + return True + + def doAllWork(self) -> bool: + if not self.readPartCfg(): + return False + + if not self.genPartBufPartTab(): + return False + + if not self.checkFileSize(): + print("Error: file size check failed!!!") + return False + + if not self.genPartBufBootRam(): + print("Error: ram code wrong!!!") + return False + + if not self.genPartBufKV(): + print("Error: KV wrong!!!") + return False + + if not self.genPartBufEEPROM(): + print("Error: EEPROM wrong!!!") + return False + + if not self.genPartBufAPP(): + print("Error: process app content!!!") + return False + + if not self.writeOutputFile(): + print("Error: final store!!!") + return False + + return True + + @property + def boot_filepath(self): + return self.__boot_filepath + + @boot_filepath.setter + def boot_filepath(self, boot): + if isinstance(boot, str): + if os.path.exists(boot): + self.__boot_filepath = boot + else: + raise ValueError("Error: not exist: {} !!!".format(boot)) + else: + raise TypeError("Error: boot MUST be a str!!!") + + @property + def app_filepath(self): + return self.__app_filepath + + @app_filepath.setter + def app_filepath(self, app): + if isinstance(app, str): + if os.path.exists(app): + self.__app_filepath = app + else: + raise ValueError("Error: not exist: {} !!!".format(app)) + else: + raise TypeError("Error: app MUST be a str!!!") + + @property + def flashimage_filepath(self): + return self.__flashimage_filepath + + @flashimage_filepath.setter + def flashimage_filepath(self, flashimage): + if isinstance(flashimage, str): + dest_dir = os.path.dirname(flashimage) + if os.path.exists(dest_dir): + self.__flashimage_filepath = flashimage + else: + raise ValueError("Error: directory for {} NOT exist!!!".format(flashimage)) + else: + raise TypeError("Error: flashimage MUST be a str!!!") + + @property + def part_cfg_filepath(self): + return self.__part_cfg_filepath + + @part_cfg_filepath.setter + def part_cfg_filepath(self, part_cfg): + if isinstance(part_cfg, str): + if os.path.exists(part_cfg): + self.__part_cfg_filepath = part_cfg + else: + raise ValueError("Error: not exist: {}".format(part_cfg)) + else: + raise TypeError("Error: part_cfg MUST be a str!!!") + + @property + def ver_str(self): + return self.__ver_str + + @ver_str.setter + def ver_str(self, ver): + """ + `ver` is a str with format ".", such as "1.2" or "2.3". + """ + if isinstance(ver, str): + temp_list = ver.split(".") + if (len(temp_list) == 2) and temp_list[0].isnumeric() and temp_list[1].isnumeric(): + self.__ver_str = ver + self.__ver_major = int(temp_list[0]) + self.__ver_minor = int(temp_list[1]) + else: + raise ValueError("Error: ver MUST be like '1.2' (major.minor)") + else: + raise TypeError("Error: ver MUST be a str!!!") + + @property + def verbose(self): + return self.__verbose + + @verbose.setter + def verbose(self, verbose): + if isinstance(verbose, int): + self.__verbose = verbose % 3 + else: + raise TypeError("Error: verbose MUST be [0, 1, 2]") + + @property + def swd_crp(self) -> int: + return self.__swd_crp + + @swd_crp.setter + def swd_crp(self, crp): + if isinstance(crp, int): + if crp == 0: + self.__swd_crp = 0 + else: + self.__swd_crp = 1 + else: + raise TypeError("Error: crp MUST be one of [0, 1]!!!") + + def __str__(self): + output_str = ( "\n------ mkimage ------\n" \ + "2nd boot: {_boot}\n" \ + "app.bin : {_app}\n" \ + "output : {_flash}\n" \ + "part_cfg: {_part}\n" \ + "ver str : {_ver}\n" \ + .format(_boot=self.boot_filepath, _app=self.app_filepath, + _flash=self.flashimage_filepath, _part=self.part_cfg_filepath, _ver=self.ver_str)) + return output_str + + +if __name__ == "__main__": + """ + The following arguments are required: + --boot /path/to/boot_ln88xx.bin, that is ramcode; + --app /path/to/app.bin, that is compiler output; + --output /path/to/flashimage.bin, that is our final image file which can be downloaded to flash; + --part /path/to/flash_partition_cfg.json, that is configuration for flash partition; + --ver APP version, like "1.2", but is only used for LN SDK boot, not for user app version; + + The following arguments are optional: + --crp which change the SWD behavior, 0 -- SWD protect is disabled; 1 -- SWD protect is enabled; + + Usage + ===== + python3 makeimage.py -h + """ + prog = os.path.basename(__file__) + desc = "makeimage tool for LN88XX" + parser = argparse.ArgumentParser(prog=prog, description=desc) + parser.add_argument("--boot", help="/path/to/boot_ln88xx.bin", type=str) + parser.add_argument("--app", help="/path/to/app.bin", type=str) + parser.add_argument("--output", help="/path/to/flashimage.bin, that is output filepath", type=str) + parser.add_argument("--part", help="/path/to/flash_partition_cfg.json", type=str) + parser.add_argument("--ver", help="APP version (only used for LN SDK boot), such as 1.2", type=str) + parser.add_argument("--crp", help="SWD protect bit [0 -- disable, 1 -- enable]", type=int, choices=[0, 1]) + + args = parser.parse_args() + + if args.boot is None: + print("Error: /path/to/boot_ln88xx.bin has not been set!!!") + exit(-1) + + if args.app is None: + print("Error: /path/to/app.bin has not been set!!!") + exit(-2) + + if args.output is None: + print("Error: /path/to/flashimage.bin has not been set!!!") + exit(-3) + + if args.part is None: + print("Error: /path/to/flash_partition_cfg.json has not been set!!!") + exit(-4) + + if args.ver is None: + print("Error: LN SDK boot version has not been set!!!") + exit(-5) + + mkimage = MakeImageTool() + mkimage.boot_filepath = args.boot + mkimage.app_filepath = args.app + mkimage.flashimage_filepath = args.output + mkimage.part_cfg_filepath = args.part + mkimage.ver_str = args.ver + + if args.crp: + mkimage.swd_crp = args.crp + + if not mkimage.doAllWork(): + exit(-1) + + exit(0) diff --git a/ltchiptool/soc/ln882x/util/ota_image_generator.py b/ltchiptool/soc/ln882x/util/ota_image_generator.py new file mode 100644 index 0000000..8f1f08f --- /dev/null +++ b/ltchiptool/soc/ln882x/util/ota_image_generator.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +# +# Copyright 2021 Shanghai Lightning Semiconductor Technology Co., LTD + +# 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 lzma +import sys +import os +import struct +import zlib +import shutil +import argparse +from .part_desc_info import * +from .image_header import * + + +class OTATOOL: + def __init__(self): + self.part_desc_info_list = [] + self.image_header = None + self.app_content = None + self.output_filepath = None + self.__input_filepath = None + + def readPartTab(self) -> bool: + try: + with open(self.input_filepath, "rb") as fInputObj: + fInputObj.seek(PARTITION_TAB_OFFSET, os.SEEK_SET) + ptable_buffer = fInputObj.read(PARTITION_TAB_SIZE) + + offset = 0 + while offset < PARTITION_TAB_SIZE: + part_desc_info_buffer = ptable_buffer[offset : (offset + PARTITION_DESC_INFO_SIZE)] + part_type, start_addr, part_size, part_crc32 = struct.unpack("= PART_TYPE_INVALID: + break + crc32_recalc = zlib.crc32(part_desc_info_buffer[0:(3*4)]) & 0xFFFFFFFF + if part_crc32 != crc32_recalc: + break + # print("type: {_pt:>12}, start_addr: 0x{_sa:08X}, part_size: 0x{_ps:08X}, part_crc32: 0x{_pc:08X}" + # .format(_pt=part_type, _sa=start_addr, _ps=part_size, _pc=part_crc32)) + + part_desc_info_obj = PartDescInfo(part_type, start_addr, part_size, part_crc32) + self.part_desc_info_list.append(part_desc_info_obj) + offset += PARTITION_DESC_INFO_SIZE + except Exception as err: + print("Error: open file failed: {}".format(str(err))) + return False + if len(self.part_desc_info_list) >= 3: + return True + else: + return False + + def readAPP(self): + if len(self.part_desc_info_list) < 3: + print("Please make sure that partition table has at least 3 items!!!") + return False + + app_desc_info = None + for desc_info in self.part_desc_info_list: + if isinstance(desc_info, PartDescInfo): + if desc_info.part_type == PART_TYPE_APP: + app_desc_info = desc_info + break + + if app_desc_info is None: + print("Please make sure that APP partition is in the partition table!!!") + return False + + try: + with open(self.input_filepath, "rb") as fInputObj: + fInputObj.seek(app_desc_info.start_addr, os.SEEK_SET) + app_image_header_buffer = fInputObj.read(256) + + # image header + self.image_header = ImageHeader(app_image_header_buffer) + + # app content + if self.image_header.image_type == IMAGE_TYPE_ORIGINAL: + fInputObj.seek(app_desc_info.start_addr + 256, os.SEEK_SET) + self.app_content = fInputObj.read(self.image_header.img_size_orig) + else: + print("Not supported image type, which is {_t}".format(_t=image_type_num2str(self.image_header.image_type))) + return False + except Exception as err: + print("Error: open file failed: {}".format(str(err))) + return False + + return True + + def processOTAImage(self): + if (self.image_header is None) or (self.app_content is None): + print("No valid app image header or app conent found!!!") + return False + + app_content_size_before_lzma = len(self.app_content) + my_filter = [ + { + "id": lzma.FILTER_LZMA1, + "dict_size": 4*1024, # 4KB, (32KB max) + "mode": lzma.MODE_NORMAL, + }, + ] + lzc = lzma.LZMACompressor(format=lzma.FORMAT_ALONE, filters=my_filter) + out1 = lzc.compress(self.app_content) + content_after_lzma = bytearray(b"".join([out1, lzc.flush()])) + + content_after_lzma[5] = get_num_at_byte(app_content_size_before_lzma, 0) + content_after_lzma[6] = get_num_at_byte(app_content_size_before_lzma, 1) + content_after_lzma[7] = get_num_at_byte(app_content_size_before_lzma, 2) + content_after_lzma[8] = get_num_at_byte(app_content_size_before_lzma, 3) + + content_after_lzma[9] = 0 + content_after_lzma[10] = 0 + content_after_lzma[11] = 0 + content_after_lzma[12] = 0 + + app_content_size_after_lzma = len(content_after_lzma) + + self.app_content = content_after_lzma + crc32_after_lzma = zlib.crc32(content_after_lzma) + + self.image_header.image_type = IMAGE_TYPE_ORIGINAL_XZ + ota_ver_major = self.image_header.getVerMajor() + ota_ver_minor = self.image_header.getVerMinor() + self.image_header.ver = ((ota_ver_major << 8) | ota_ver_minor) & 0xFFFF + self.image_header.img_size_orig_xz = app_content_size_after_lzma + self.image_header.img_crc32_orig_xz = crc32_after_lzma + self.image_header.reCalcCRC32() + + return True + + def writeOTAImage(self): + """ + OTA image, XZ format. + """ + ota_filename = "{_a}-ota-xz-v{_ma}.{_mi}.bin" \ + .format(_a= os.path.basename(self.input_filepath).split(".")[0], + _ma=self.image_header.getVerMajor(), _mi=self.image_header.getVerMinor()) + self.output_filepath = os.path.join(self.output_dir, ota_filename) + + if os.path.exists(self.output_filepath): + shutil.rmtree(self.output_filepath, ignore_errors=True) + + try: + with open(self.output_filepath, "wb") as fOutObj: + fOutObj.write(self.image_header.toBytes()) + fOutObj.write(self.app_content) + except Exception as err: + print("Error: write file failed: {}".format(str(err))) + return False + + if not os.path.exists(self.output_filepath): + print("Failed to build: {_ota}".format(_ota=self.output_filepath)) + return False + + return True + + def doAllWork(self) -> bool: + if not self.readPartTab(): + return False + if not self.readAPP(): + return False + if not self.processOTAImage(): + return False + if not self.writeOTAImage(): + return False + return True + + @property + def input_filepath(self): + return self.__input_filepath + + @input_filepath.setter + def input_filepath(self, filepath): + """ + Absolute filepath of flashimage.bin. + """ + if isinstance(filepath, str): + if os.path.exists(realpath(filepath)): + self.__input_filepath = realpath(filepath) + else: + raise ValueError("not exist: {_f}".format(_f=filepath)) + else: + raise TypeError("filepath MUST be a valid string") + + @property + def output_dir(self): + return self.__output_dir + + @output_dir.setter + def output_dir(self, filepath): + """ + Indicates the directory where to save ota.bin, normally it's the same + directory as flashimage.bin. + The output filename is `flashimage-ota-v{X}.{Y}.bin`, where X/Y is the + major/minor version of flashimage.bin. + """ + if isinstance(filepath, str): + if os.path.exists(filepath): + self.__output_dir = filepath + else: + raise ValueError("dir not exist: {_f}".format(_f=filepath)) + else: + raise TypeError("dir MUST be a valid string") + + +if __name__ == "__main__": + prog = os.path.basename(__file__) + usage = ("\nargv1: /path/to/flashimage.bin \n" + "Example: \n" + "python3 {_p} E:/ln_sdk/build/bin/flashimage.bin".format(_p=prog)) + + parser = argparse.ArgumentParser(prog=prog, usage=usage) + parser.add_argument("path_to_flashimage", help="absolute path of flashimage.bin") + + print(sys.argv) + args = parser.parse_args() + + flashimage_filepath = args.path_to_flashimage + ota_save_dir = os.path.dirname(flashimage_filepath) + + ota_tool = OTATOOL() + ota_tool.input_filepath = flashimage_filepath + ota_tool.output_dir = ota_save_dir + + if not ota_tool.doAllWork(): + exit(-1) + + print("Succeed to build: {}".format(ota_tool.output_filepath)) + + exit(0) diff --git a/ltchiptool/soc/ln882x/util/part_desc_info.py b/ltchiptool/soc/ln882x/util/part_desc_info.py new file mode 100644 index 0000000..1410709 --- /dev/null +++ b/ltchiptool/soc/ln882x/util/part_desc_info.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +# +# Copyright 2021 Shanghai Lightning Semiconductor Technology Co., LTD + +# 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 zlib +from .ln_tools import * +from .boot_header import BootHeader + +# partition table start addr and size. +PARTITION_TAB_OFFSET = BootHeader.BOOT_START_ADDR + BootHeader.BOOT_SIZE_LIMIT +PARTITION_TAB_SIZE = 1024 * 4 + +PARTITION_DESC_INFO_SIZE = 4 + 4 + 4 + 4 + +PART_TYPE_APP = 0 +PART_TYPE_OTA = 1 +PART_TYPE_KV = 2 +PART_TYPE_NVDS = 3 +PART_TYPE_SIMU_EEPROM = 4 +PART_TYPE_USER = 5 +PART_TYPE_INVALID = 6 +PART_TYPE_BOOT = 7 +PART_TYPE_PART_TAB = 8 + +__PART_TYPE_DICT = { + PART_TYPE_APP : "APP", + PART_TYPE_OTA : "OTA", + PART_TYPE_KV : "KV", + PART_TYPE_NVDS : "NVDS", + PART_TYPE_SIMU_EEPROM : "SIMU_EEPROM", + PART_TYPE_USER : "USER", + PART_TYPE_INVALID : "INVALID", + PART_TYPE_BOOT : "BOOT", + PART_TYPE_PART_TAB : "PART_TAB" +} + + +def part_type_num2str(type_num=PART_TYPE_INVALID): + return __PART_TYPE_DICT.get(type_num, __PART_TYPE_DICT.get(PART_TYPE_INVALID)) + + +def part_type_str2num(type_str): + for k, v in __PART_TYPE_DICT.items(): + if v == type_str: + return k + return PART_TYPE_INVALID + + +class PartDescInfo(object): + def __init__(self, parttype=0, startaddr=0, partsize=0, partcrc32=0): + self.part_type = parttype + self.start_addr = startaddr + self.part_size = partsize + self.__part_crc32 = partcrc32 + + self.buffer = bytearray(4 * 4) + for i in range(0, 4 * 4): + self.buffer[i] = 0 + + self.toBytes() + + def toBytes(self) -> bytearray: + self.buffer[0] = get_num_at_byte(self.part_type, 0) + self.buffer[1] = get_num_at_byte(self.part_type, 1) + self.buffer[2] = get_num_at_byte(self.part_type, 2) + self.buffer[3] = get_num_at_byte(self.part_type, 3) + + self.buffer[4] = get_num_at_byte(self.start_addr, 0) + self.buffer[5] = get_num_at_byte(self.start_addr, 1) + self.buffer[6] = get_num_at_byte(self.start_addr, 2) + self.buffer[7] = get_num_at_byte(self.start_addr, 3) + + self.buffer[8] = get_num_at_byte(self.part_size, 0) + self.buffer[9] = get_num_at_byte(self.part_size, 1) + self.buffer[10] = get_num_at_byte(self.part_size, 2) + self.buffer[11] = get_num_at_byte(self.part_size, 3) + + self.reCalCRC32() + + return self.buffer + + def reCalCRC32(self): + self.__part_crc32 = zlib.crc32(self.buffer[0:12]) + + self.buffer[12] = get_num_at_byte(self.part_crc32, 0) + self.buffer[13] = get_num_at_byte(self.part_crc32, 1) + self.buffer[14] = get_num_at_byte(self.part_crc32, 2) + self.buffer[15] = get_num_at_byte(self.part_crc32, 3) + + @property + def part_type(self): + return self.__part_type + + @part_type.setter + def part_type(self, t): + if isinstance(t, int): + self.__part_type = t + else: + raise TypeError("part_type MUST be assigned to an int value (0~5)") + + @property + def start_addr(self): + return self.__start_addr + + @start_addr.setter + def start_addr(self, addr): + if isinstance(addr, int): + self.__start_addr = addr + else: + raise TypeError("start_addr MUST be assigned to an int value") + + @property + def part_size(self): + return self.__part_size + + @part_size.setter + def part_size(self, s): + if isinstance(s, int): + self.__part_size = s + else: + raise TypeError("part_size MUST be assigned to an int value") + + @property + def part_crc32(self): + return self.__part_crc32 + + # readonly + # @part_crc32.setter + # def part_crc32(self, crc32): + # if isinstance(crc32, int): + # self.__part_crc32 = crc32 + # else: + # raise TypeError("part_crc32 MUST be assigned to an int value") + + def __str__(self) -> str: + output = ("partition_type: {_p:>12}, start_addr: 0x{_sa:08X}, size_KB: 0x{_sz:08X}, crc32: 0x{_c:08X}" + .format(_p=part_type_num2str(self.part_type), _sa=self.start_addr, _sz=self.part_size, _c=self.part_crc32)) + return output