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