Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

<div align="center">

Expand Down
1 change: 1 addition & 0 deletions ltchiptool/commands/soc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}


Expand Down
4 changes: 4 additions & 0 deletions ltchiptool/soc/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand All @@ -38,6 +41,7 @@ def get_family_names(cls) -> List[str]:
"beken-72xx",
"realtek-ambz",
"realtek-ambz2",
"lightning-ln882h",
]

#########################
Expand Down
7 changes: 7 additions & 0 deletions ltchiptool/soc/ln882h/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (c) Etienne Le Cousin 2025-01-02.

from .main import LN882hMain

__all__ = [
"LN882hMain",
]
118 changes: 118 additions & 0 deletions ltchiptool/soc/ln882h/binary.py
Original file line number Diff line number Diff line change
@@ -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()
204 changes: 204 additions & 0 deletions ltchiptool/soc/ln882h/flash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Copyright (c) Etienne Le Cousin 2025-01-02.

from abc import ABC
from binascii import crc32
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

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()
Loading
Loading