From 707cb8c358f6cab07999e8c21b8011ff956963b4 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 25 Aug 2025 17:25:42 +0000 Subject: [PATCH 1/4] driver: add RKUSBMaskromDriver bootstrap driver Add a RKUSBMaskromDriver bootstrap driver with an accompanying rkusbmaskrom agent to support bootstrapping targets with Rockchip SoCs. The rkusbmaskrom agent expects the target to be in MASKROM mode and send images to BootROM using vendor specific 0x471 and 0x472 control transfers, in 4 KiB chunks. The first image is loaded to SRAM using 0x471 and is expected to initialize DRAM and then return back to BootROM. A second image can then be loaded to start of DRAM using 0x472. Signed-off-by: Jonas Karlman --- doc/configuration.rst | 35 +++++++ labgrid/driver/__init__.py | 3 +- labgrid/driver/usbloader.py | 49 ++++++++++ labgrid/util/agents/rkusbmaskrom.py | 142 ++++++++++++++++++++++++++++ tests/test_agent.py | 5 +- tests/test_rkusbmaskrom.py | 122 ++++++++++++++++++++++++ 6 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 labgrid/util/agents/rkusbmaskrom.py create mode 100644 tests/test_rkusbmaskrom.py diff --git a/doc/configuration.rst b/doc/configuration.rst index 08429d67f..ea2db7998 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -784,6 +784,7 @@ Arguments: Used by: - `RKUSBDriver`_ + - `RKUSBMaskromDriver`_ NetworkMXSUSBLoader ~~~~~~~~~~~~~~~~~~~ @@ -2647,6 +2648,40 @@ Arguments: - usb_loader (str): optional, key in :ref:`images ` containing the path of a first-stage bootloader image to write +RKUSBMaskromDriver +~~~~~~~~~~~~~~~~~~ +An :any:`RKUSBMaskromDriver` is used to upload an image into a device in the +*Rockchip USB Maskrom state*. +This is useful to bootstrap a bootloader onto a device. + +Binds to: + loader: + - `RKUSBLoader`_ + - `NetworkRKUSBLoader`_ + +Implements: + - :any:`BootstrapProtocol` + +.. code-block:: yaml + + targets: + main: + drivers: + RKUSBMaskromDriver: + initial: 'rkmaskrom471' + image: 'rkmaskrom472' + + images: + rkmaskrom471: 'path/to/u-boot-rockchip-usb471.bin' + rkmaskrom472: 'path/to/u-boot-rockchip-usb472.bin' + +Arguments: + - initial (str): optional, key in :ref:`images ` containing the path + of an image that typically initializes DRAM of the target + - image (str): optional, key in :ref:`images ` containing the path + of a bootloader image to load to start of DRAM, or to SRAM when an initial image is unused + - delay (float, default=0.001): delay in seconds between loading initial and secondary image + UUUDriver ~~~~~~~~~ A :any:`UUUDriver` is used to upload an image into a device in the *NXP USB diff --git a/labgrid/driver/__init__.py b/labgrid/driver/__init__.py index edf1ad2b1..f1331905b 100644 --- a/labgrid/driver/__init__.py +++ b/labgrid/driver/__init__.py @@ -16,7 +16,8 @@ DigitalOutputPowerDriver, YKUSHPowerDriver, \ USBPowerDriver, SiSPMPowerDriver, NetworkPowerDriver, \ PDUDaemonDriver -from .usbloader import MXSUSBDriver, IMXUSBDriver, BDIMXUSBDriver, RKUSBDriver, UUUDriver +from .usbloader import MXSUSBDriver, IMXUSBDriver, BDIMXUSBDriver, RKUSBDriver, \ + RKUSBMaskromDriver, UUUDriver from .usbsdmuxdriver import USBSDMuxDriver from .usbsdwiredriver import USBSDWireDriver from .common import Driver diff --git a/labgrid/driver/usbloader.py b/labgrid/driver/usbloader.py index 5f4f90762..7a74625d6 100644 --- a/labgrid/driver/usbloader.py +++ b/labgrid/driver/usbloader.py @@ -3,8 +3,10 @@ from ..factory import target_factory from ..protocol import BootstrapProtocol +from ..resource.remote import NetworkRKUSBLoader from ..step import step from .common import Driver +from ..util.agentwrapper import AgentWrapper from ..util.managedfile import ManagedFile from ..util.timeout import Timeout from ..util.helper import processwrapper @@ -154,6 +156,53 @@ def load(self, filename=None): raise +@target_factory.reg_driver +@attr.s(eq=False) +class RKUSBMaskromDriver(Driver, BootstrapProtocol): + bindings = {"loader": {"RKUSBLoader", NetworkRKUSBLoader}} + initial = attr.ib(default=None) + image = attr.ib(default=None) + delay = attr.ib(default=0.001, validator=attr.validators.instance_of(float)) + + def __attrs_post_init__(self): + super().__attrs_post_init__() + self.wrapper = None + + def on_activate(self): + if isinstance(self.loader, NetworkRKUSBLoader): + host = self.loader.host + else: + host = None + self.wrapper = AgentWrapper(host) + self.proxy = self.wrapper.load('rkusbmaskrom') + + def on_deactivate(self): + self.proxy = None + if self.wrapper: + self.wrapper.close() + self.wrapper = None + + @Driver.check_active + @step(args=['filename']) + def load(self, filename=None): + images = [] + if self.initial: + images.append(self.target.env.config.get_image_path(self.initial)) + if not filename and self.image: + filename = self.target.env.config.get_image_path(self.image) + if filename: + images.append(filename) + if not images: + raise Exception("No images to load") + + args = [self.loader.busnum, self.loader.devnum] + for path in images: + mf = ManagedFile(path, self.loader) + mf.sync_to_resource() + args.append(mf.get_remote_path()) + self.proxy.load(*args, delay=self.delay) + + @target_factory.reg_driver @attr.s(eq=False) class UUUDriver(Driver, BootstrapProtocol): diff --git a/labgrid/util/agents/rkusbmaskrom.py b/labgrid/util/agents/rkusbmaskrom.py new file mode 100644 index 000000000..ad102ee9b --- /dev/null +++ b/labgrid/util/agents/rkusbmaskrom.py @@ -0,0 +1,142 @@ +""" +This module implements the communication protocol to load an image to SRAM, +that typically initializes DRAM, followed by optionally loading a secondary +image to start of DRAM, when a Rockchip device is in MASKROM mode. +""" +from time import sleep + +import usb.core +import usb.util + + +RK_RC4_KEY = [ + 0x7c, 0x4e, 0x03, 0x04, 0x55, 0x05, 0x09, 0x07, + 0x2d, 0x2c, 0x7b, 0x38, 0x17, 0x0d, 0x17, 0x11, +] + + +# polynomial: 0x1021 +CRC16_TABLE = [ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, +] + + +def crc16_ccitt_false(data, crc=0xffff): + for byte in data: + crc = ((crc << 8) & 0xff00) ^ CRC16_TABLE[((crc >> 8) & 0xff) ^ byte] + return crc & 0xffff + + +def rc4_ksa(key): + keylength = len(key) + S = list(range(256)) + j = 0 + for i in range(256): + j = (j + S[i] + key[i % keylength]) % 256 + S[i], S[j] = S[j], S[i] + return S + + +def rc4_prga(S): + i = 0 + j = 0 + while True: + i = (i + 1) % 256 + j = (j + S[i]) % 256 + S[i], S[j] = S[j], S[i] + K = S[(S[i] + S[j]) % 256] + yield K + + +class RKUSBMaskrom: + def __init__(self, **args): + self._dev = usb.core.find(**args) + if self._dev is None: + raise ValueError("Device not found") + if self._dev.idVendor != 0x2207: + raise ValueError(f"Unsupported device VID {self._dev.idVendor:x}") + if self._dev.bcdUSB & 0x0001: + raise ValueError("Device in LOADER mode") + + def __enter__(self): + if self._dev.is_kernel_driver_active(0): + self._dev.detach_kernel_driver(0) + usb.util.claim_interface(self._dev, 0) + return self + + def __exit__(self, exc_type, exc_value, traceback): + usb.util.release_interface(self._dev, 0) + usb.util.dispose_resources(self._dev) + + def load(self, code, path): + with open(path, 'rb') as f: + data = bytearray(f.read()) + + # encrypt data using the known rockchip key for older devices + if self._dev.idProduct < 0x3500 and \ + self._dev.idProduct not in (0x110c, 0x110e, 0x110f): + keystream = rc4_prga(rc4_ksa(RK_RC4_KEY)) + data = bytearray([byte ^ next(keystream) for byte in data]) + + # ensure crc16 fit in the last chunk + if len(data) % 4096 == 4095: + data.append(0) + + # append crc16 of data + crc = crc16_ccitt_false(data) + data.append(crc >> 8) + data.append(crc & 0xff) + + # extra chunk to signal end of transfer + if len(data) % 4096 == 0: + data.append(0) + + # transfer all chunks + for i in range(0, len(data), 4096): + chunk = data[i:i + 4096] + self._dev.ctrl_transfer(64, 12, 0, code, chunk, 5000) + + +def handle_load(busnum, devnum, initial, secondary=None, delay=None): + with RKUSBMaskrom(bus=busnum, address=devnum) as maskrom: + maskrom.load(0x471, initial) + if secondary is not None: + if delay: + sleep(delay) + maskrom.load(0x472, secondary) + + +methods = { + "load": handle_load, +} diff --git a/tests/test_agent.py b/tests/test_agent.py index f5ac21653..278de0082 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -106,6 +106,9 @@ def test_all_modules(): methods = aw.list() assert 'sysfsgpio.set' in methods assert 'sysfsgpio.get' in methods + aw.load('rkusbmaskrom') + methods = aw.list() + assert 'rkusbmaskrom.load' in methods aw.load('usb_hid_relay') methods = aw.list() assert 'usb_hid_relay.set' in methods @@ -114,4 +117,4 @@ def test_all_modules(): def test_import_modules(): import labgrid.util.agents import labgrid.util.agents.dummy - from labgrid.util.agents import deditec_relais8, sysfsgpio + from labgrid.util.agents import deditec_relais8, rkusbmaskrom, sysfsgpio diff --git a/tests/test_rkusbmaskrom.py b/tests/test_rkusbmaskrom.py new file mode 100644 index 000000000..cce5160c6 --- /dev/null +++ b/tests/test_rkusbmaskrom.py @@ -0,0 +1,122 @@ +from tempfile import NamedTemporaryFile +import pytest + +from labgrid.driver import RKUSBMaskromDriver +from labgrid.protocol import BootstrapProtocol +from labgrid.resource import RKUSBLoader +from labgrid.resource.remote import NetworkRKUSBLoader +from labgrid.util.agents.rkusbmaskrom import crc16_ccitt_false, methods, rc4_ksa, rc4_prga +from labgrid.util.agentwrapper import AgentWrapper + + +@pytest.fixture(scope='function') +def local_rkusbloader(target): + port = RKUSBLoader(target, name=None) + port.avail = True + yield port + + +@pytest.fixture(scope='function') +def remote_rkusbloader(target): + yield NetworkRKUSBLoader(target, + name=None, + host="localhost", + busnum=0, + devnum=1, + path='/dev/bus/usb/002/003', + vendor_id=0x0, + model_id=0x0, + ) + + +@pytest.fixture(scope='function') +def rkusbmaskrom_driver(target, mocker): + load_mock = mocker.patch.object(AgentWrapper, 'load') + driver = RKUSBMaskromDriver(target, name=None) + target.activate(driver) + yield driver, load_mock.return_value + target.deactivate(driver) + load_mock.reset_mock() + + +@pytest.fixture(scope='function') +def tempfile(target): + with NamedTemporaryFile() as f: + yield f + + +class TestRKUSBMaskromDriver: + def test_instanziation_local(self, target, local_rkusbloader): + driver = RKUSBMaskromDriver(target, name=None) + assert (isinstance(driver, RKUSBMaskromDriver)) + assert (isinstance(driver, BootstrapProtocol)) + + def test_instanziation_remote(self, target, remote_rkusbloader): + driver = RKUSBMaskromDriver(target, name=None) + assert (isinstance(driver, RKUSBMaskromDriver)) + assert (isinstance(driver, BootstrapProtocol)) + + @pytest.mark.parametrize("filename", ['', None]) + def test_load_no_images(self, local_rkusbloader, rkusbmaskrom_driver, filename): + driver, _ = rkusbmaskrom_driver + with pytest.raises(Exception, match="No images to load"): + driver.load(filename) + + def test_load(self, local_rkusbloader, rkusbmaskrom_driver, tempfile): + driver, proxy_mock = rkusbmaskrom_driver + driver.load(tempfile.name) + proxy_mock.load.assert_called_once_with( + local_rkusbloader.busnum, + local_rkusbloader.devnum, + tempfile.name, + delay=driver.delay, + ) + + +def test_crc16_ccitt_false(): + crc = crc16_ccitt_false("123456789".encode()) + assert crc == 0x29b1 + + +def test_rc4_keystream(): + # from RFC 6229: Test Vectors for the Stream Cipher RC4 + key = list(bytes.fromhex("1ada31d5cf688221c109163908ebe51debb46227c6cc8b37641910833222772a")) + keystream = rc4_prga(rc4_ksa(key)) + assert [next(keystream) for _ in range(16)] == list(bytes.fromhex("dd5bcb0018e922d494759d7c395d02d3")) + assert [next(keystream) for _ in range(16)] == list(bytes.fromhex("c8446f8f77abf737685353eb89a1c9eb")) + for _ in range(4048): + next(keystream) + assert [next(keystream) for _ in range(16)] == list(bytes.fromhex("d5a39e3dfcc50280bac4a6b5aa0dca7d")) + assert [next(keystream) for _ in range(16)] == list(bytes.fromhex("370b1c1fe655916d97fd0d47ca1d72b8")) + + +def test_rkusbmaskrom_device_not_found(mocker, tempfile): + find_mock = mocker.patch("usb.core.find") + find_mock.return_value = None + with pytest.raises(ValueError, match="Device not found"): + methods["load"](0, 1, tempfile.name, delay=0.001) + + +def test_rkusbmaskrom_unsupported_vendor(mocker, tempfile): + dev_mock = mocker.patch("usb.core.find").return_value + dev_mock.idVendor = 0x1234 + with pytest.raises(ValueError, match="Unsupported device VID 1234"): + methods["load"](0, 1, tempfile.name, delay=0.001) + + +def test_rkusbmaskrom_loader_mode(mocker, tempfile): + dev_mock = mocker.patch("usb.core.find").return_value + dev_mock.idVendor = 0x2207 + dev_mock.bcdUSB = 0x0001 + with pytest.raises(ValueError, match="Device in LOADER mode"): + methods["load"](0, 1, tempfile.name, delay=0.001) + + +@pytest.mark.parametrize("pid", [0x110a, 0x110c, 0x330a, 0x350a]) +def test_rkusbmaskrom_load(mocker, tempfile, pid): + dev_mock = mocker.patch("usb.core.find").return_value + dev_mock.idVendor = 0x2207 + dev_mock.idProduct = pid + dev_mock.bcdUSB = 0x0000 + methods["load"](0, 1, tempfile.name, delay=0.001) + dev_mock.ctrl_transfer.assert_called() From d797b8c8b772725983631554ef20d8e679884f81 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Sat, 30 Aug 2025 16:37:28 +0000 Subject: [PATCH 2/4] RKUSBMaskromDriver: add support for using vendor loader images Add support for using vendor loader image files, typically created using the vendor boot_merger tool in the rkbin repo. Signed-off-by: Jonas Karlman --- doc/configuration.rst | 3 +- labgrid/util/agents/rkusbmaskrom.py | 117 ++++++++++++++++++++++++---- tests/test_rkusbmaskrom.py | 7 +- 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/doc/configuration.rst b/doc/configuration.rst index ea2db7998..b809a1293 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -2679,7 +2679,8 @@ Arguments: - initial (str): optional, key in :ref:`images ` containing the path of an image that typically initializes DRAM of the target - image (str): optional, key in :ref:`images ` containing the path - of a bootloader image to load to start of DRAM, or to SRAM when an initial image is unused + of a bootloader image to load to start of DRAM, or to SRAM when an initial image is unused, or + the path to a vendor loader image typically created using the vendor boot_merger tool - delay (float, default=0.001): delay in seconds between loading initial and secondary image UUUDriver diff --git a/labgrid/util/agents/rkusbmaskrom.py b/labgrid/util/agents/rkusbmaskrom.py index ad102ee9b..cb63077c0 100644 --- a/labgrid/util/agents/rkusbmaskrom.py +++ b/labgrid/util/agents/rkusbmaskrom.py @@ -3,6 +3,8 @@ that typically initializes DRAM, followed by optionally loading a secondary image to start of DRAM, when a Rockchip device is in MASKROM mode. """ +from collections import namedtuple +from struct import unpack from time import sleep import usb.core @@ -52,12 +54,55 @@ ] +# polynomial: 0x04c10db7 +CRC32_TABLE = [ + 0x00000000, 0x04c10db7, 0x09821b6e, 0x0d4316d9, 0x130436dc, 0x17c53b6b, 0x1a862db2, 0x1e472005, + 0x26086db8, 0x22c9600f, 0x2f8a76d6, 0x2b4b7b61, 0x350c5b64, 0x31cd56d3, 0x3c8e400a, 0x384f4dbd, + 0x4c10db70, 0x48d1d6c7, 0x4592c01e, 0x4153cda9, 0x5f14edac, 0x5bd5e01b, 0x5696f6c2, 0x5257fb75, + 0x6a18b6c8, 0x6ed9bb7f, 0x639aada6, 0x675ba011, 0x791c8014, 0x7ddd8da3, 0x709e9b7a, 0x745f96cd, + 0x9821b6e0, 0x9ce0bb57, 0x91a3ad8e, 0x9562a039, 0x8b25803c, 0x8fe48d8b, 0x82a79b52, 0x866696e5, + 0xbe29db58, 0xbae8d6ef, 0xb7abc036, 0xb36acd81, 0xad2ded84, 0xa9ece033, 0xa4aff6ea, 0xa06efb5d, + 0xd4316d90, 0xd0f06027, 0xddb376fe, 0xd9727b49, 0xc7355b4c, 0xc3f456fb, 0xceb74022, 0xca764d95, + 0xf2390028, 0xf6f80d9f, 0xfbbb1b46, 0xff7a16f1, 0xe13d36f4, 0xe5fc3b43, 0xe8bf2d9a, 0xec7e202d, + 0x34826077, 0x30436dc0, 0x3d007b19, 0x39c176ae, 0x278656ab, 0x23475b1c, 0x2e044dc5, 0x2ac54072, + 0x128a0dcf, 0x164b0078, 0x1b0816a1, 0x1fc91b16, 0x018e3b13, 0x054f36a4, 0x080c207d, 0x0ccd2dca, + 0x7892bb07, 0x7c53b6b0, 0x7110a069, 0x75d1adde, 0x6b968ddb, 0x6f57806c, 0x621496b5, 0x66d59b02, + 0x5e9ad6bf, 0x5a5bdb08, 0x5718cdd1, 0x53d9c066, 0x4d9ee063, 0x495fedd4, 0x441cfb0d, 0x40ddf6ba, + 0xaca3d697, 0xa862db20, 0xa521cdf9, 0xa1e0c04e, 0xbfa7e04b, 0xbb66edfc, 0xb625fb25, 0xb2e4f692, + 0x8aabbb2f, 0x8e6ab698, 0x8329a041, 0x87e8adf6, 0x99af8df3, 0x9d6e8044, 0x902d969d, 0x94ec9b2a, + 0xe0b30de7, 0xe4720050, 0xe9311689, 0xedf01b3e, 0xf3b73b3b, 0xf776368c, 0xfa352055, 0xfef42de2, + 0xc6bb605f, 0xc27a6de8, 0xcf397b31, 0xcbf87686, 0xd5bf5683, 0xd17e5b34, 0xdc3d4ded, 0xd8fc405a, + 0x6904c0ee, 0x6dc5cd59, 0x6086db80, 0x6447d637, 0x7a00f632, 0x7ec1fb85, 0x7382ed5c, 0x7743e0eb, + 0x4f0cad56, 0x4bcda0e1, 0x468eb638, 0x424fbb8f, 0x5c089b8a, 0x58c9963d, 0x558a80e4, 0x514b8d53, + 0x25141b9e, 0x21d51629, 0x2c9600f0, 0x28570d47, 0x36102d42, 0x32d120f5, 0x3f92362c, 0x3b533b9b, + 0x031c7626, 0x07dd7b91, 0x0a9e6d48, 0x0e5f60ff, 0x101840fa, 0x14d94d4d, 0x199a5b94, 0x1d5b5623, + 0xf125760e, 0xf5e47bb9, 0xf8a76d60, 0xfc6660d7, 0xe22140d2, 0xe6e04d65, 0xeba35bbc, 0xef62560b, + 0xd72d1bb6, 0xd3ec1601, 0xdeaf00d8, 0xda6e0d6f, 0xc4292d6a, 0xc0e820dd, 0xcdab3604, 0xc96a3bb3, + 0xbd35ad7e, 0xb9f4a0c9, 0xb4b7b610, 0xb076bba7, 0xae319ba2, 0xaaf09615, 0xa7b380cc, 0xa3728d7b, + 0x9b3dc0c6, 0x9ffccd71, 0x92bfdba8, 0x967ed61f, 0x8839f61a, 0x8cf8fbad, 0x81bbed74, 0x857ae0c3, + 0x5d86a099, 0x5947ad2e, 0x5404bbf7, 0x50c5b640, 0x4e829645, 0x4a439bf2, 0x47008d2b, 0x43c1809c, + 0x7b8ecd21, 0x7f4fc096, 0x720cd64f, 0x76cddbf8, 0x688afbfd, 0x6c4bf64a, 0x6108e093, 0x65c9ed24, + 0x11967be9, 0x1557765e, 0x18146087, 0x1cd56d30, 0x02924d35, 0x06534082, 0x0b10565b, 0x0fd15bec, + 0x379e1651, 0x335f1be6, 0x3e1c0d3f, 0x3add0088, 0x249a208d, 0x205b2d3a, 0x2d183be3, 0x29d93654, + 0xc5a71679, 0xc1661bce, 0xcc250d17, 0xc8e400a0, 0xd6a320a5, 0xd2622d12, 0xdf213bcb, 0xdbe0367c, + 0xe3af7bc1, 0xe76e7676, 0xea2d60af, 0xeeec6d18, 0xf0ab4d1d, 0xf46a40aa, 0xf9295673, 0xfde85bc4, + 0x89b7cd09, 0x8d76c0be, 0x8035d667, 0x84f4dbd0, 0x9ab3fbd5, 0x9e72f662, 0x9331e0bb, 0x97f0ed0c, + 0xafbfa0b1, 0xab7ead06, 0xa63dbbdf, 0xa2fcb668, 0xbcbb966d, 0xb87a9bda, 0xb5398d03, 0xb1f880b4, +] + + def crc16_ccitt_false(data, crc=0xffff): for byte in data: crc = ((crc << 8) & 0xff00) ^ CRC16_TABLE[((crc >> 8) & 0xff) ^ byte] return crc & 0xffff +def crc32_rkboot(data, crc=0x0): + for byte in data: + crc = ((crc << 8) & 0xffffff00) ^ CRC32_TABLE[((crc >> 24) & 0xff) ^ byte] + return crc & 0xffffffff + + def rc4_ksa(key): keylength = len(key) S = list(range(256)) @@ -79,6 +124,36 @@ def rc4_prga(S): yield K +def get_rkboot_entries(data, header): + RKBootEntry = namedtuple('RKBootEntry', [ + 'size', 'type', 'dataOffset', 'dataSize', 'dataDelay', + ]) + for code in (0x471, 0x472): + entries = getattr(header, f'code{code:x}Num') + offset = getattr(header, f'code{code:x}Offset') + size = getattr(header, f'code{code:x}Size') + for _ in range(entries): + entry = RKBootEntry._make(unpack(' 0: + return header + return None + + class RKUSBMaskrom: def __init__(self, **args): self._dev = usb.core.find(**args) @@ -99,15 +174,18 @@ def __exit__(self, exc_type, exc_value, traceback): usb.util.release_interface(self._dev, 0) usb.util.dispose_resources(self._dev) - def load(self, code, path): - with open(path, 'rb') as f: - data = bytearray(f.read()) + def load(self, code, bytesOrPath): + if isinstance(bytesOrPath, bytes): + data = bytearray(bytesOrPath) + else: + with open(bytesOrPath, 'rb') as f: + data = bytearray(f.read()) - # encrypt data using the known rockchip key for older devices - if self._dev.idProduct < 0x3500 and \ - self._dev.idProduct not in (0x110c, 0x110e, 0x110f): - keystream = rc4_prga(rc4_ksa(RK_RC4_KEY)) - data = bytearray([byte ^ next(keystream) for byte in data]) + # encrypt data using the known rockchip key for older devices + if self._dev.idProduct < 0x3500 and \ + self._dev.idProduct not in (0x110c, 0x110e, 0x110f): + keystream = rc4_prga(rc4_ksa(RK_RC4_KEY)) + data = bytearray([byte ^ next(keystream) for byte in data]) # ensure crc16 fit in the last chunk if len(data) % 4096 == 4095: @@ -129,12 +207,25 @@ def load(self, code, path): def handle_load(busnum, devnum, initial, secondary=None, delay=None): + with open(initial, 'rb') as f: + data = f.read() + header = parse_rkboot_header(data) + if header is None and secondary is not None: + with open(secondary, 'rb') as f: + data = f.read() + header = parse_rkboot_header(data) with RKUSBMaskrom(bus=busnum, address=devnum) as maskrom: - maskrom.load(0x471, initial) - if secondary is not None: - if delay: - sleep(delay) - maskrom.load(0x472, secondary) + if header is not None: + for code, entry_data, entry_delay in get_rkboot_entries(data, header): + maskrom.load(code, entry_data) + if entry_delay: + sleep(entry_delay) + else: + maskrom.load(0x471, initial) + if secondary is not None: + if delay: + sleep(delay) + maskrom.load(0x472, secondary) methods = { diff --git a/tests/test_rkusbmaskrom.py b/tests/test_rkusbmaskrom.py index cce5160c6..2e3b21414 100644 --- a/tests/test_rkusbmaskrom.py +++ b/tests/test_rkusbmaskrom.py @@ -5,7 +5,7 @@ from labgrid.protocol import BootstrapProtocol from labgrid.resource import RKUSBLoader from labgrid.resource.remote import NetworkRKUSBLoader -from labgrid.util.agents.rkusbmaskrom import crc16_ccitt_false, methods, rc4_ksa, rc4_prga +from labgrid.util.agents.rkusbmaskrom import crc16_ccitt_false, crc32_rkboot, methods, rc4_ksa, rc4_prga from labgrid.util.agentwrapper import AgentWrapper @@ -78,6 +78,11 @@ def test_crc16_ccitt_false(): assert crc == 0x29b1 +def test_crc32_rkboot(): + crc = crc32_rkboot("123456789".encode()) + assert crc == 0x889a9615 + + def test_rc4_keystream(): # from RFC 6229: Test Vectors for the Stream Cipher RC4 key = list(bytes.fromhex("1ada31d5cf688221c109163908ebe51debb46227c6cc8b37641910833222772a")) From ef1db3ab819d6ddfe79d91ff0a0ff33a1d4a3ea0 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Sun, 14 Sep 2025 20:29:05 +0000 Subject: [PATCH 3/4] RKUSBMaskromDriver: add support for using vendor idblock v2 images Add support for using vendor idblock v2 image files, typically created using the U-Boot mkimage or Barebox rkimage tools. Signed-off-by: Jonas Karlman --- doc/configuration.rst | 3 +- labgrid/util/agents/rkusbmaskrom.py | 52 +++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/doc/configuration.rst b/doc/configuration.rst index b809a1293..88ac18688 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -2680,7 +2680,8 @@ Arguments: of an image that typically initializes DRAM of the target - image (str): optional, key in :ref:`images ` containing the path of a bootloader image to load to start of DRAM, or to SRAM when an initial image is unused, or - the path to a vendor loader image typically created using the vendor boot_merger tool + the path to a vendor loader image typically created using the vendor boot_merger tool, or the + path to an idblock image typically created using the U-Boot mkimage or Barebox rkimage tools - delay (float, default=0.001): delay in seconds between loading initial and secondary image UUUDriver diff --git a/labgrid/util/agents/rkusbmaskrom.py b/labgrid/util/agents/rkusbmaskrom.py index cb63077c0..8f1f08a39 100644 --- a/labgrid/util/agents/rkusbmaskrom.py +++ b/labgrid/util/agents/rkusbmaskrom.py @@ -3,6 +3,7 @@ that typically initializes DRAM, followed by optionally loading a secondary image to start of DRAM, when a Rockchip device is in MASKROM mode. """ +import hashlib from collections import namedtuple from struct import unpack from time import sleep @@ -124,7 +125,7 @@ def rc4_prga(S): yield K -def get_rkboot_entries(data, header): +def get_rkboot_entries(data, header, _): RKBootEntry = namedtuple('RKBootEntry', [ 'size', 'type', 'dataOffset', 'dataSize', 'dataDelay', ]) @@ -139,7 +140,28 @@ def get_rkboot_entries(data, header): offset += size -def parse_rkboot_header(data): +def get_newidblock_entries(data, header, delay): + RKImageEntry = namedtuple('RKImageEntry', [ + 'offset', 'size', 'address', 'flag', 'counter', 'digest' + ]) + offset, size = 120, 88 + for _ in range(header.num_images): + entry = RKImageEntry._make(unpack(' 0: - return header - return None + return header, get_rkboot_entries + RKNewIDBlockHeader = namedtuple('RKNewIDBlockHeader', [ + 'tag', 'size', 'num_images', 'boot_flag', + ]) + if tag in (0x534e4b52, 0x53534b52): + header = RKNewIDBlockHeader._make(unpack(' 0: + if (header.boot_flag & 0xf) == 1: + digest = hashlib.sha256(data[:1536]).digest() + elif (header.boot_flag & 0xf) == 2: + digest = hashlib.sha512(data[:1536]).digest() + else: + digest = None + if (header.boot_flag & 0xf0) == 0 and digest is not None and \ + digest != data[1536:1536 + len(digest)]: + raise ValueError("Digest mismatch for header") + return header, get_newidblock_entries + return None, None class RKUSBMaskrom: @@ -209,14 +247,14 @@ def load(self, code, bytesOrPath): def handle_load(busnum, devnum, initial, secondary=None, delay=None): with open(initial, 'rb') as f: data = f.read() - header = parse_rkboot_header(data) + header, get_image_entries = parse_image_header(data) if header is None and secondary is not None: with open(secondary, 'rb') as f: data = f.read() - header = parse_rkboot_header(data) + header, get_image_entries = parse_image_header(data) with RKUSBMaskrom(bus=busnum, address=devnum) as maskrom: if header is not None: - for code, entry_data, entry_delay in get_rkboot_entries(data, header): + for code, entry_data, entry_delay in get_image_entries(data, header, delay): maskrom.load(code, entry_data) if entry_delay: sleep(entry_delay) From 6bf32602cfa43942f247e05d7e62c49bc4b8aabb Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Sun, 28 Sep 2025 21:17:41 +0000 Subject: [PATCH 4/4] RKUSBMaskromDriver: add support for using vendor idblock v1 images Add support for using vendor idblock v1 image files, typically created using the U-Boot mkimage tool. Signed-off-by: Jonas Karlman --- labgrid/util/agents/rkusbmaskrom.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/labgrid/util/agents/rkusbmaskrom.py b/labgrid/util/agents/rkusbmaskrom.py index 8f1f08a39..31c304edb 100644 --- a/labgrid/util/agents/rkusbmaskrom.py +++ b/labgrid/util/agents/rkusbmaskrom.py @@ -161,6 +161,24 @@ def get_newidblock_entries(data, header, delay): offset += size +def get_idblock_entries(data, header, delay): + offset, size = header.init_offset * 512, header.init_size * 512 + entry_data = data[offset:offset + size] + if header.disable_rc4: + keystream = rc4_prga(rc4_ksa(RK_RC4_KEY)) + entry_data = bytes([byte ^ next(keystream) for byte in entry_data]) + yield 0x471, entry_data, delay + if header.init_boot_size > header.init_size: + offset = (header.init_offset + header.init_size) * 512 + size = (header.init_boot_size - header.init_size) * 512 + if size != 524288: + entry_data = data[offset:offset + size] + if header.disable_rc4: + keystream = rc4_prga(rc4_ksa(RK_RC4_KEY)) + entry_data = bytes([byte ^ next(keystream) for byte in entry_data]) + yield 0x472, entry_data, 0 + + def parse_image_header(data): tag = int.from_bytes(data[:4], 'little') RKBootHeader = namedtuple('RKBootHeader', [ @@ -189,6 +207,15 @@ def parse_image_header(data): digest != data[1536:1536 + len(digest)]: raise ValueError("Digest mismatch for header") return header, get_newidblock_entries + RKIDBlockHeader0 = namedtuple('RKIDBlockHeader0', [ + 'tag', 'disable_rc4', 'init_offset', 'init_size', 'init_boot_size', + ]) + if tag == 0xfcdc8c3b: + keystream = rc4_prga(rc4_ksa(RK_RC4_KEY)) + data = bytes(byte ^ next(keystream) for byte in data[:512]) + header = RKIDBlockHeader0._make(unpack(' 0: + return header, get_idblock_entries return None, None