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/commands/soc.py b/ltchiptool/commands/soc.py index 5ef40d5..cf5eff4 100644 --- a/ltchiptool/commands/soc.py +++ b/ltchiptool/commands/soc.py @@ -9,6 +9,7 @@ "rtltool": "ltchiptool/soc/ambz/util/rtltool.py", "ambztool": "ltchiptool/soc/ambz/util/ambztool.py", "ambz2tool": "ltchiptool/soc/ambz2/util/ambz2tool.py", + "ln882htool": "ltchiptool/soc/ln882h/util/ln882htool.py", } diff --git a/ltchiptool/soc/amb/system.py b/ltchiptool/soc/amb/system.py index db17167..a5ad5ae 100644 --- a/ltchiptool/soc/amb/system.py +++ b/ltchiptool/soc/amb/system.py @@ -6,7 +6,7 @@ from datastruct import DataStruct from datastruct.fields import adapter, alignto, bitfield, field -FF_16 = b"\xFF" * 16 +FF_16 = b"\xff" * 16 class FlashSpeed(IntEnum): diff --git a/ltchiptool/soc/ambz/binary.py b/ltchiptool/soc/ambz/binary.py index 7a5d214..aa8e0d5 100644 --- a/ltchiptool/soc/ambz/binary.py +++ b/ltchiptool/soc/ambz/binary.py @@ -24,14 +24,14 @@ def check_xip_binary( ) -> Optional[Tuple[int, int, bytes]]: if data[0:8] != header: return None - if data[16:32] != b"\xFF" * 16: + if data[16:32] != b"\xff" * 16: return None length, start = unpack(" Optional[Tuple[int, int, bytes]]: - return check_xip_binary(data, header=b"\x99\x99\x96\x96\x3F\xCC\x66\xFC") + return check_xip_binary(data, header=b"\x99\x99\x96\x96\x3f\xcc\x66\xfc") class AmebaZBinary(SocInterface, ABC): diff --git a/ltchiptool/soc/ambz/flash.py b/ltchiptool/soc/ambz/flash.py index b2c64e8..cff27f5 100644 --- a/ltchiptool/soc/ambz/flash.py +++ b/ltchiptool/soc/ambz/flash.py @@ -105,7 +105,7 @@ def flash_sw_reset(self) -> None: port.baudrate = 115200 sleep(0.1) # try software reset by writing the family ID, preceded by 55AA - magic_word = b"\x55\xAA" + self.family.id.to_bytes(length=4, byteorder="big") + magic_word = b"\x55\xaa" + self.family.id.to_bytes(length=4, byteorder="big") port.write(magic_word) sleep(0.5) port.baudrate = prev_baudrate @@ -178,7 +178,7 @@ def flash_get_chip_info(self) -> List[Tuple[str, str]]: syscfg2 = letoint(data[256 + 512 + 16 + 8 : 256 + 512 + 16 + 8 + 4]) system_data = data[256 + 512 + 16 + 16 : 256 + 512 + 16 + 16 + 128].ljust( - 4096, b"\xFF" + 4096, b"\xff" ) system = SystemData.unpack(system_data) diff --git a/ltchiptool/soc/ambz/util/ambzcode.py b/ltchiptool/soc/ambz/util/ambzcode.py index 72d2220..c0fdc4a 100644 --- a/ltchiptool/soc/ambz/util/ambzcode.py +++ b/ltchiptool/soc/ambz/util/ambzcode.py @@ -204,7 +204,7 @@ def read_efuse_otp(offset: int = 0) -> bytes: return ( b"\x02\x48\x01\x4b" b"\x98\x47\x03\xe0" - b"\x21\x3C\x00\x00" # EFUSE_OTP_Read32B() + b"\x21\x3c\x00\x00" # EFUSE_OTP_Read32B() ) + inttole32(AMBZ_DATA_ADDRESS + offset) @staticmethod diff --git a/ltchiptool/soc/ambz/util/ambztool.py b/ltchiptool/soc/ambz/util/ambztool.py index dc88e59..d3d7575 100644 --- a/ltchiptool/soc/ambz/util/ambztool.py +++ b/ltchiptool/soc/ambz/util/ambztool.py @@ -108,7 +108,7 @@ def read(self, io: IO[bytes], n: int) -> bytes: # increment saved address self.address += n # add padding to force sending N+4 packet size - data = data.ljust(n + 4, b"\xFF") + data = data.ljust(n + 4, b"\xff") return data diff --git a/ltchiptool/soc/ambz/util/rtltool.py b/ltchiptool/soc/ambz/util/rtltool.py index 0b3077e..8214c1b 100644 --- a/ltchiptool/soc/ambz/util/rtltool.py +++ b/ltchiptool/soc/ambz/util/rtltool.py @@ -29,7 +29,7 @@ CMD_XMD = b"\x07" # Go xmodem mode (write RAM/Flash mode) CMD_EFS = b"\x17" # Erase Flash Sectors CMD_RBF = b"\x19" # Read Block Flash -CMD_ABRT = b"\x1B" # End xmodem mode (write RAM/Flash mode) +CMD_ABRT = b"\x1b" # End xmodem mode (write RAM/Flash mode) CMD_GFS = b"\x21" # FLASH Get Status CMD_SFS = b"\x26" # FLASH Set Status @@ -283,7 +283,7 @@ def send_xmodem(self, stream, offset, size, retry=3): if not data: # end of stream print("send: at EOF") return False - data = data.ljust(packet_size, b"\xFF") + data = data.ljust(packet_size, b"\xff") pkt = ( struct.pack(" List[FirmwareBinary]: nmap_ota1 = self.board.toolchain.nm(input) # build the partition table - ptable = PartitionTable(user_data=b"\xFF" * 256) + ptable = PartitionTable(user_data=b"\xff" * 256) for region, type in config.ptable.items(): offset, length, _ = self.board.region(region) hash_key = config.keys.hash_keys[region] @@ -205,11 +205,11 @@ def elf2bin(self, input: str, ota_idx: int) -> List[FirmwareBinary]: with output.write() as f: f.write(data) with out_ptab.write() as f: - ptab = data[ptab_offset:ptab_end].rstrip(b"\xFF") + ptab = data[ptab_offset:ptab_end].rstrip(b"\xff") ptab = pad_data(ptab, 0x20, 0xFF) f.write(ptab) with out_boot.write() as f: - boot = data[boot_offset:boot_end].rstrip(b"\xFF") + boot = data[boot_offset:boot_end].rstrip(b"\xff") boot = pad_data(boot, 0x20, 0xFF) f.write(boot) with out_ota1.write() as f: @@ -230,15 +230,15 @@ def detect_file_type( return Detection.make("Realtek AmebaZ2 Flash Image", offset=0) if ( - data[0x40:0x44] != b"\xFF\xFF\xFF\xFF" + data[0x40:0x44] != b"\xff\xff\xff\xff" and data[0x48] == ImageType.BOOT.value ): return Detection.make("Realtek AmebaZ2 Bootloader", offset=0x4000) if ( - data[0xE0:0xE8].strip(b"\xFF") + data[0xE0:0xE8].strip(b"\xff") and data[0xE8] == ImageType.FWHS_S.value - and data[0x1A0:0x1A8].strip(b"\xFF") + and data[0x1A0:0x1A8].strip(b"\xff") and data[0x1A8] == SectionType.SRAM.value ): return Detection.make("Realtek AmebaZ2 Firmware", offset=None) diff --git a/ltchiptool/soc/ambz2/util/models/images.py b/ltchiptool/soc/ambz2/util/models/images.py index da6804a..7b55797 100644 --- a/ltchiptool/soc/ambz2/util/models/images.py +++ b/ltchiptool/soc/ambz2/util/models/images.py @@ -27,7 +27,7 @@ from .partitions import Bootloader, Firmware, PartitionTable from .utils import FF_32 -FLASH_CALIBRATION = b"\x99\x99\x96\x96\x3F\xCC\x66\xFC\xC0\x33\xCC\x03\xE5\xDC\x31\x62" +FLASH_CALIBRATION = b"\x99\x99\x96\x96\x3f\xcc\x66\xfc\xc0\x33\xcc\x03\xe5\xdc\x31\x62" @dataclass diff --git a/ltchiptool/soc/ambz2/util/models/utils.py b/ltchiptool/soc/ambz2/util/models/utils.py index c577a14..08d97eb 100644 --- a/ltchiptool/soc/ambz2/util/models/utils.py +++ b/ltchiptool/soc/ambz2/util/models/utils.py @@ -4,9 +4,9 @@ from datastruct import Adapter, Context -FF_48 = b"\xFF" * 48 -FF_32 = b"\xFF" * 32 -FF_16 = b"\xFF" * 16 +FF_48 = b"\xff" * 48 +FF_32 = b"\xff" * 32 +FF_16 = b"\xff" * 16 T = TypeVar("T") diff --git a/ltchiptool/soc/bk72xx/binary.py b/ltchiptool/soc/bk72xx/binary.py index 9bffc84..2e0e42f 100644 --- a/ltchiptool/soc/bk72xx/binary.py +++ b/ltchiptool/soc/bk72xx/binary.py @@ -29,7 +29,7 @@ def to_address(offs: int) -> int: def check_app_code_crc(data: bytes) -> Union[bool, None]: # b #0x40 # ldr pc, [pc, #0x14] - if data[0:8] == b"\x2F\x07\xB5\x94\x35\xFF\x2A\x9B": + if data[0:8] == b"\x2f\x07\xb5\x94\x35\xff\x2a\x9b": crc = CRC16.CMS.calc(data[0:32]) crc_found = betoint(data[32:34]) if crc == crc_found: @@ -183,13 +183,13 @@ def elf2bin(self, input: str, ota_idx: int) -> List[FirmwareBinary]: with out_ug.write() as ug: hdr = BytesIO() ota_bin = ota_data.getvalue() - hdr.write(b"\x55\xAA\x55\xAA") + hdr.write(b"\x55\xaa\x55\xaa") hdr.write(pad_data(version.encode(), 12, 0x00)) hdr.write(inttobe32(len(ota_bin))) hdr.write(inttobe32(sum(ota_bin))) ug.write(hdr.getvalue()) ug.write(inttobe32(sum(hdr.getvalue()))) - ug.write(b"\xAA\x55\xAA\x55") + ug.write(b"\xaa\x55\xaa\x55") ug.write(ota_bin) # close all files @@ -218,7 +218,7 @@ def detect_file_type( return Detection.make_unsupported("Beken Encrypted App") # raw firmware binary - if data[0:8] == b"\x0E\x00\x00\xEA\x14\xF0\x9F\xE5": + if data[0:8] == b"\x0e\x00\x00\xea\x14\xf0\x9f\xe5": return Detection.make_unsupported("Raw ARM Binary") # RBL file for OTA - 'download' partition diff --git a/ltchiptool/soc/bk72xx/util/binary.py b/ltchiptool/soc/bk72xx/util/binary.py index cb49eb7..26ef545 100644 --- a/ltchiptool/soc/bk72xx/util/binary.py +++ b/ltchiptool/soc/bk72xx/util/binary.py @@ -44,7 +44,7 @@ def __init__(self, coeffs: Union[bytes, str] = None) -> None: def crc(self, data: ByteSource, type: DataType = None) -> DataGenerator: for block in geniter(data, 32): if len(block) < 32: - block += b"\xFF" * (32 - len(block)) + block += b"\xff" * (32 - len(block)) crc = CRC16.CMS.calc(block) block += inttobe16(crc) if type: @@ -54,7 +54,7 @@ def crc(self, data: ByteSource, type: DataType = None) -> DataGenerator: def uncrc(self, data: ByteSource, check: bool = True) -> ByteGenerator: for block in geniter(data, 34): - if check and block != b"\xFF" * 34: + if check and block != b"\xff" * 34: crc = CRC16.CMS.calc(block[0:32]) crc_found = betoint(block[32:34]) if crc != crc_found: diff --git a/ltchiptool/soc/interface.py b/ltchiptool/soc/interface.py index 0858047..ccef695 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-ln882h"): + from .ln882h import LN882hMain + return LN882hMain(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-ln882h", ] ######################### diff --git a/ltchiptool/soc/ln882h/__init__.py b/ltchiptool/soc/ln882h/__init__.py new file mode 100644 index 0000000..2f2c3ed --- /dev/null +++ b/ltchiptool/soc/ln882h/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Etienne Le Cousin 2025-01-02. + +from .main import LN882hMain + +__all__ = [ + "LN882hMain", +] diff --git a/ltchiptool/soc/ln882h/binary.py b/ltchiptool/soc/ln882h/binary.py new file mode 100644 index 0000000..637b789 --- /dev/null +++ b/ltchiptool/soc/ln882h/binary.py @@ -0,0 +1,118 @@ +# Copyright (c) Etienne Le Cousin 2025-01-02. + +from abc import ABC +from logging import warning +from os import stat +from os.path import dirname, isfile +from shutil import copyfile +from typing import List + +from ltchiptool import SocInterface +from ltchiptool.util.fileio import chext, chname +from ltchiptool.util.fwbinary import FirmwareBinary + +from .util import OTATOOL, MakeImageTool +from .util.models import PartDescInfo, part_type_str2num + + +class LN882hBinary(SocInterface, ABC): + def elf2bin(self, input: str, ota_idx: int) -> List[FirmwareBinary]: + toolchain = self.board.toolchain + flash_layout = self.board["flash"] + + # find bootloader image + input_boot = chname(input, "boot.bin") + if not isfile(input_boot): + raise FileNotFoundError("Bootloader image not found") + + # build output names + output = FirmwareBinary( + location=input, + name="firmware", + offset=0, + title="Flash Image", + description="Complete image with boot for flashing at offset 0", + public=True, + ) + out_boot = FirmwareBinary( + location=input, + name="boot", + offset=self.board.region("boot")[0], + title="Bootloader Image", + ) + out_ptab = FirmwareBinary( + location=input, + name="part_tab", + offset=self.board.region("part_tab")[0], + title="Partition Table", + ) + out_app = FirmwareBinary( + location=input, + name="app", + offset=self.board.region("app")[0], + title="Application Image", + description="Firmware partition image for direct flashing", + public=True, + ) + out_ota = FirmwareBinary( + location=input, + name="ota", + offset=self.board.region("ota")[0], + title="OTA Image", + description="Compressed App image for OTA flashing", + public=True, + ) + # print graph element + output.graph(1) + + input_bin = chext(input, "bin") + # objcopy ELF -> raw BIN + toolchain.objcopy(input, input_bin) + + # Make Image Tool + # fmt: off + mkimage = MakeImageTool() + mkimage.boot_filepath = input_boot + mkimage.app_filepath = input_bin + mkimage.flashimage_filepath = output.path + mkimage.ver_str = "1.0" + mkimage.swd_crp = 0 + mkimage.readPartCfg = lambda : True + # fmt: off + + # find all partitions + for name, layout in flash_layout.items(): + (offset, _, length) = layout.partition("+") + part_info = PartDescInfo( + parttype = part_type_str2num(name.upper()), + startaddr = int(offset, 16), + partsize = int(length, 16) + ) + mkimage._MakeImageTool__part_desc_info_list.append(part_info) + + if not mkimage.doAllWork(): + raise RuntimeError("MakeImageTool: Fail to generate image") + + # write all parts to files + with out_boot.write() as f: + f.write(mkimage._MakeImageTool__partbuf_bootram) + with out_ptab.write() as f: + f.write(mkimage._MakeImageTool__partbuf_parttab) + with out_app.write() as f: + f.write(mkimage._MakeImageTool__partbuf_app) + + # Make ota image + ota_tool = OTATOOL() + ota_tool.input_filepath = output.path + ota_tool.output_dir = dirname(input) + if not ota_tool.doAllWork(): + raise RuntimeError("MakeImageTool: Fail to generate OTA image") + + copyfile(ota_tool.output_filepath, out_ota.path) + _, ota_size, _ = self.board.region("ota") + if stat(out_ota.path).st_size > ota_size: + warning( + f"OTA size too large: {out_ota.filename} > {ota_size} (0x{ota_size:X})" + ) + + return output.group() diff --git a/ltchiptool/soc/ln882h/flash.py b/ltchiptool/soc/ln882h/flash.py new file mode 100644 index 0000000..95cb3be --- /dev/null +++ b/ltchiptool/soc/ln882h/flash.py @@ -0,0 +1,200 @@ +# Copyright (c) Etienne Le Cousin 2025-01-02. + +from abc import ABC +from typing import IO, Generator, List, Optional, Tuple, Union + +from ltchiptool import SocInterface +from ltchiptool.util.flash import FlashConnection, FlashFeatures, FlashMemoryType +from ltchiptool.util.streams import ProgressCallback +from uf2tool import OTAScheme, UploadContext + +from .util.ln882htool import LN882hTool + +LN882H_GUIDE = [ + "Connect UART1 of the LN882h to the USB-TTL adapter:", + [ + ("PC", "LN882h"), + ("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 BOOT pin (GPIOA9) to GND with a wire.", +] + + +class LN882hFlash(SocInterface, ABC): + ln882h: Optional[LN882hTool] = None + info: List[Tuple[str, str]] = None + + def flash_get_features(self) -> FlashFeatures: + return FlashFeatures() + + def flash_get_guide(self) -> List[Union[str, list]]: + return LN882H_GUIDE + + def flash_get_docs_url(self) -> Optional[str]: + return "https://docs.libretiny.eu/link/flashing-ln882h" + + def flash_set_connection(self, connection: FlashConnection) -> None: + if self.conn: + self.flash_disconnect() + self.conn = connection + self.conn.fill_baudrate(115200) + + def flash_build_protocol(self, force: bool = False) -> None: + if not force and self.ln882h: + return + self.flash_disconnect() + self.ln882h = LN882hTool( + port=self.conn.port, + baudrate=self.conn.link_baudrate, + ) + self.flash_change_timeout(self.conn.timeout, self.conn.link_timeout) + + def flash_change_timeout(self, timeout: float = 0.0, link_timeout: float = 0.0): + self.flash_build_protocol() + if timeout: + self.ln882h.read_timeout = timeout + self.conn.timeout = timeout + if link_timeout: + self.ln882h.link_timeout = link_timeout + self.conn.link_timeout = link_timeout + + def flash_connect(self, callback: ProgressCallback = ProgressCallback()) -> None: + if self.ln882h and self.conn.linked: + return + self.flash_build_protocol() + assert self.ln882h + self.ln882h.link() + + def cb(i, n, t, sent): + callback.on_update(sent - cb.total_sent) + cb.total_sent = sent + + cb.total_sent = 0 + + callback.on_message(f"Loading Ram Code") + self.ln882h.ram_boot(cb) + self.conn.linked = True + + def flash_disconnect(self) -> None: + if self.ln882h: + self.ln882h.close() + self.ln882h = None + if self.conn: + self.conn.linked = False + + def flash_get_chip_info(self) -> List[Tuple[str, str]]: + if self.info: + return self.info + self.flash_connect() + assert self.ln882h + + flash_info = self.ln882h.command("flash_info")[-1] + flash_info = dict(s.split(":") for s in flash_info.split(",")) + + self.info = [ + ("Flash ID", flash_info["id"]), + ("Flash Size", flash_info["flash size"]), + ("Flash UUID", self.ln882h.command("flash_uid")[-1][10:]), + ("OTP MAC", self.ln882h.command("get_mac_in_flash_otp")[-2]), + ] + return self.info + + def flash_get_chip_info_string(self) -> str: + self.flash_connect() + assert self.ln882h + return "LN882H" + + def flash_get_size(self, memory: FlashMemoryType = FlashMemoryType.FLASH) -> int: + self.flash_connect() + assert self.ln882h + if memory == FlashMemoryType.EFUSE: + raise NotImplementedError("Memory type not readable via UART") + else: + # It appears that flash size is coded in the low byte of flash ID as 2^X + # Ex: LN882HKI id=0xEB6015 --> 0x15 = 21 --> flash_size = 2^21 = 2MB + flash_info = self.ln882h.command("flash_info")[-1] + flash_info = dict(s.split(":") for s in flash_info.split(",")) + flash_size = 1 << (int(flash_info["id"], 16) & 0xFF) + return flash_size + + def flash_read_raw( + self, + offset: int, + length: int, + verify: bool = True, + memory: FlashMemoryType = FlashMemoryType.FLASH, + callback: ProgressCallback = ProgressCallback(), + ) -> Generator[bytes, None, None]: + self.flash_connect() + assert self.ln882h + if memory == FlashMemoryType.EFUSE: + raise NotImplementedError("Memory type not readable via UART") + else: + gen = self.ln882h.flash_read( + offset=offset, + length=length, + verify=verify, + ) + yield from callback.update_with(gen) + + def flash_write_raw( + self, + offset: int, + length: int, + data: IO[bytes], + verify: bool = True, + callback: ProgressCallback = ProgressCallback(), + ) -> None: + self.flash_connect() + assert self.ln882h + + def cb(i, n, t, sent): + callback.on_update(sent - cb.total_sent) + cb.total_sent = sent + + cb.total_sent = 0 + + self.ln882h.flash_write( + offset=offset, + stream=data, + callback=cb, + ) + + def flash_write_uf2( + self, + ctx: UploadContext, + verify: bool = True, + callback: ProgressCallback = ProgressCallback(), + ) -> None: + # collect continuous blocks of data (before linking, as this takes time) + parts = ctx.collect_data(OTAScheme.FLASHER_SINGLE) + callback.on_total(sum(len(part.getvalue()) for part in parts.values())) + + # connect to chip + self.flash_connect() + + # write blocks to flash + for offset, data in parts.items(): + length = len(data.getvalue()) + data.seek(0) + callback.on_message(f"Writing (0x{offset:06X})") + gen = self.flash_write_raw( + offset=offset, + data=data, + length=length, + callback=callback, + ) + + callback.on_message("Booting firmware") + # reboot the chip + self.ln882h.disconnect() diff --git a/ltchiptool/soc/ln882h/main.py b/ltchiptool/soc/ln882h/main.py new file mode 100644 index 0000000..4b9353d --- /dev/null +++ b/ltchiptool/soc/ln882h/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 LN882hBinary +from .flash import LN882hFlash + + +class LN882hMain( + LN882hBinary, + LN882hFlash, + SocInterfaceCommon, + ABC, +): + def __init__(self, family: Family) -> None: + super().__init__() + self.family = family + + def hello(self): + info("Hello from LN882h") + + @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/ln882h/util/__init__.py b/ltchiptool/soc/ln882h/util/__init__.py new file mode 100644 index 0000000..6b539b4 --- /dev/null +++ b/ltchiptool/soc/ln882h/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/ln882h/util/ln882htool.py b/ltchiptool/soc/ln882h/util/ln882htool.py new file mode 100644 index 0000000..777c32f --- /dev/null +++ b/ltchiptool/soc/ln882h/util/ln882htool.py @@ -0,0 +1,256 @@ +# Copyright (c) Etienne Le Cousin 2025-02-23. + +from logging import debug, info +from os import path, stat +from tempfile import NamedTemporaryFile +from time import sleep, time +from typing import IO, Callable, Generator, Optional + +import click +from ymodem.Socket import ModemSocket + +from ltchiptool.util.cli import DevicePortParamType +from ltchiptool.util.serialtool import SerialToolBase + +_T_YmodemCB = Optional[Callable[[int, str, int, int], None]] + +LN882H_YM_BAUDRATE = 2000000 +LN882H_ROM_BAUDRATE = 115200 +LN882H_FLASH_ADDRESS = 0x0000000 +LN882H_RAM_ADDRESS = 0x20000000 +LN882H_BOOTRAM_FILE = "ramcode.bin" + + +class LN882hTool(SerialToolBase): + ramcode = False + + def __init__( + self, + port: str, + baudrate: int, + link_timeout: float = 10.0, + read_timeout: float = 0.2, + retry_count: int = 10, + ): + super().__init__(port, baudrate, link_timeout, read_timeout, retry_count) + self.ym = ModemSocket( + read=lambda size, timeout=2: self.read(size) or None, + write=lambda data, timeout=2: self.write(data), + packet_size=128, # it seems that ramcode doesn't support 1k packets for filename... + ) + + ######################################### + # Private # + ######################################### + + # Redefinition of readlines because romloader omit the last '\n' on some cmds... + def readlines(self) -> Generator[str, None, None]: + response = b"" + end = time() + self.read_timeout + self.s.timeout = self.read_timeout + while time() < end: + read = self.s.read_all() + if not read: + continue + end = time() + self.read_timeout + while b"\n" in read: + line, _, read = read.partition(b"\n") + line = (response + line).decode().strip() + if not line: + continue + yield line + response = b"" + response += read + if response: # add the last received "line" if any + yield response + raise TimeoutError("Timeout in readlines() - no more data received") + + ######################################### + # Basic commands - public low-level API # + ######################################### + + def command(self, cmd: str, waitresp: bool = True) -> str: + debug(f"cmd: {cmd}") + self.flush() + self.write(cmd.encode() + b"\r\n") + # remove ramcode echo + if self.ramcode: + self.read(len(cmd)) + return waitresp and self.resp() or None + + def resp(self) -> str: + r = [] + try: + for l in self.readlines(): + r.append(l) + except TimeoutError: + pass + if not r: + raise TimeoutError("No response") + debug(f"resp: {r}") + return r + + def ping(self) -> None: + self.ramcode = False + resp = self.command("version")[-1] + if resp == "RAMCODE": + self.ramcode = True + elif len(resp) != 20 or resp[11] != "/": + raise RuntimeError(f"Incorrect ping response: {resp!r}") + + def disconnect(self) -> None: + self.sw_reset() + + def link(self) -> None: + end = time() + self.link_timeout + while time() < end: + try: + self.ping() + return + except (RuntimeError, TimeoutError): + pass + raise TimeoutError("Timeout while linking") + + def sw_reset(self) -> None: + self.command("reboot", waitresp=False) + + def change_baudrate(self, baudrate: int) -> None: + if self.s.baudrate == baudrate: + return + self.flush() + self.command(f"baudrate {baudrate}", waitresp=False) + self.flush() + self.set_baudrate(baudrate) + + ############################################### + # Flash-related commands - for internal usage # + ############################################### + + def ram_boot( + self, + callback: _T_YmodemCB = None, + ) -> None: + if self.ramcode: + return + + info("Loading RAM Code...") + ramcode_file = path.join(path.dirname(__file__), LN882H_BOOTRAM_FILE) + ramcode_size = stat(ramcode_file).st_size + + self.command( + f"download [rambin] [0x{LN882H_RAM_ADDRESS:X}] [{ramcode_size}]", + waitresp=False, + ) + + self.push_timeout(3) + debug(f"YMODEM: transmitting to 0x{LN882H_RAM_ADDRESS:X}") + if not self.ym.send([ramcode_file], callback=callback): + self.pop_timeout() + raise RuntimeError("YMODEM transmission failed") + info("RAM Code successfully loaded.") + self.pop_timeout() + + # wait for boot start + sleep(2) + self.link() + + if not self.ramcode: + raise RuntimeError("RAM boot failed") + + ####################################### + # Memory-related commands - public API # + ####################################### + + def flash_read( + self, + offset: int, + length: int, + verify: bool = True, + chunk_size: int = 256, # maximum supported chunk size + ) -> Generator[bytes, None, None]: + self.link() + if not self.ramcode: + self.ram_boot() + + if chunk_size > 256: + raise RuntimeError( + f"Chunk size {chunk_size} exceeds the maximum allowed (256)" + ) + + for start in range(offset, offset + length, chunk_size): + count = min(start + chunk_size, offset + length) - start + debug(f"Dumping bytes: start=0x{start:X}, count=0x{count:X}") + + resp = self.command(f"flash_read 0x{start:X} 0x{count:X}")[-1] + data = bytearray.fromhex(resp.decode()) + + valid, data = self.ym._verify_recv_checksum(True, data) + if verify and not valid: + raise RuntimeError(f"Invalid checksum") + + yield data + + def flash_write( + self, + offset: int, + stream: IO[bytes], + callback: _T_YmodemCB = None, + ) -> None: + self.link() + prev_baudrate = self.s.baudrate + if not self.ramcode: + self.ram_boot() + + self.change_baudrate(LN882H_YM_BAUDRATE) + self.link() + + self.command(f"startaddr 0x{offset:X}") + + # Convert stream to temporary file before sending with YMODEM + tmp_file = NamedTemporaryFile() + with open(tmp_file.name, "wb") as f: + f.write(stream.getbuffer()) + + self.command(f"upgrade", waitresp=False) + + self.push_timeout(3) + debug(f"YMODEM: transmitting to 0x{offset:X}") + if not self.ym.send([f.name], callback=callback): + self.change_baudrate(prev_baudrate) + self.pop_timeout() + raise RuntimeError("YMODEM transmission failed") + + self.link() + + self.change_baudrate(prev_baudrate) + self.pop_timeout() + info("Flash Successful.") + + +@click.command( + help="LN882H flashing tool", +) +@click.option( + "-d", + "--device", + help="Target device port (default: auto detect)", + type=DevicePortParamType(), + default=(), +) +def cli(device: str): + ln882h = LN882HTool(port=device, baudrate=LN882H_ROM_BAUDRATE) + info("Linking...") + ln882h.link() + + info("Loading Ram code...") + ln882h.ram_boot() + + flash_info = ln882h.command("flash_info")[-1] + info(f"Received flash info: {flash_info}") + + info("Disconnecting...") + ln882h.disconnect() + + +if __name__ == "__main__": + cli() diff --git a/ltchiptool/soc/ln882h/util/makeimage.py b/ltchiptool/soc/ln882h/util/makeimage.py new file mode 100644 index 0000000..fb2bb42 --- /dev/null +++ b/ltchiptool/soc/ln882h/util/makeimage.py @@ -0,0 +1,483 @@ +#!/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. +# fmt: off +# isort:skip_file + +import argparse +import json + +from .models.boot_header import * +from .models.image_header import * +from .models.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/ln882h/util/models/__init__.py b/ltchiptool/soc/ln882h/util/models/__init__.py new file mode 100644 index 0000000..4eb12d6 --- /dev/null +++ b/ltchiptool/soc/ln882h/util/models/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) Etienne Le Cousin 2025-03-02. + +from .boot_header import BootHeader +from .image_header import ImageHeader +from .part_desc_info import * + +__all__ = [ + "BootHeader", + "ImageHeader", + "PartDescInfo", + "part_type_str2num", + "part_type_num2str", +] diff --git a/ltchiptool/soc/ln882h/util/models/boot_header.py b/ltchiptool/soc/ln882h/util/models/boot_header.py new file mode 100644 index 0000000..9d8e59b --- /dev/null +++ b/ltchiptool/soc/ln882h/util/models/boot_header.py @@ -0,0 +1,123 @@ +#!/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. +# fmt: off +# isort:skip_file + +import zlib +import struct + + +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/ln882h/util/models/part_desc_info.py b/ltchiptool/soc/ln882h/util/models/part_desc_info.py new file mode 100644 index 0000000..ef4959e --- /dev/null +++ b/ltchiptool/soc/ln882h/util/models/part_desc_info.py @@ -0,0 +1,153 @@ +#!/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. +# fmt: off +# isort:skip_file + +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 diff --git a/ltchiptool/soc/ln882h/util/ota_image_generator.py b/ltchiptool/soc/ln882h/util/ota_image_generator.py new file mode 100644 index 0000000..e54d663 --- /dev/null +++ b/ltchiptool/soc/ln882h/util/ota_image_generator.py @@ -0,0 +1,246 @@ +#!/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. +# fmt: off +# isort:skip_file + +import lzma +import sys +import os +import struct +import zlib +import shutil +import argparse +from .models.part_desc_info import * +from .models.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/ln882h/util/ramcode.bin b/ltchiptool/soc/ln882h/util/ramcode.bin new file mode 100644 index 0000000..d80ee44 Binary files /dev/null and b/ltchiptool/soc/ln882h/util/ramcode.bin differ diff --git a/ltchiptool/util/detection.py b/ltchiptool/util/detection.py index 0e11e65..0c67f72 100644 --- a/ltchiptool/util/detection.py +++ b/ltchiptool/util/detection.py @@ -13,16 +13,16 @@ FILE_TYPES = { "UF2": [ - (0x000, b"UF2\x0A"), - (0x004, b"\x57\x51\x5D\x9E"), - (0x1FC, b"\x30\x6F\xB1\x0A"), + (0x000, b"UF2\x0a"), + (0x004, b"\x57\x51\x5d\x9e"), + (0x1FC, b"\x30\x6f\xb1\x0a"), ], "ELF": [ - (0x00, b"\x7FELF"), + (0x00, b"\x7fELF"), ], "Tuya UG": [ - (0x00, b"\x55\xAA\x55\xAA"), - (0x1C, b"\xAA\x55\xAA\x55"), + (0x00, b"\x55\xaa\x55\xaa"), + (0x1C, b"\xaa\x55\xaa\x55"), ], } diff --git a/pyproject.toml b/pyproject.toml index 029779a..67dc0df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ importlib-metadata = "*" prettytable = "^3.3.0" bk7231tools = "^2.0.0" xmodem = "^0.4.6" +ymodem = "^1.5.1" wxPython = {version = "^4.2.0", optional = true} pywin32 = {version = "*", optional = true, markers = "sys_platform == 'win32'"} py-datastruct = "^1.0.0" diff --git a/uf2tool/models/block.py b/uf2tool/models/block.py index 5f3196d..d5b4c43 100644 --- a/uf2tool/models/block.py +++ b/uf2tool/models/block.py @@ -37,7 +37,7 @@ def encode(self) -> bytes: self.flags.has_tags = not not self.tags self.length = self.data and len(self.data) or 0 # UF2 magic 1 and 2 - data = b"\x55\x46\x32\x0A\x57\x51\x5D\x9E" + data = b"\x55\x46\x32\x0a\x57\x51\x5d\x9e" # encode integer variables data += inttole32(self.flags.encode()) data += inttole32(self.address) @@ -71,7 +71,7 @@ def encode(self) -> bytes: raise ValueError("Padding too long") data += self.padding data += b"\x00" * (512 - 4 - len(data)) - data += b"\x30\x6F\xB1\x0A" # magic 3 + data += b"\x30\x6f\xb1\x0a" # magic 3 return data def decode(self, data: bytes):