From 8e21982109ff55033a82ffd368a486f46b869f77 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Wed, 29 Jan 2025 22:36:27 +0000 Subject: [PATCH 01/78] rockchip: mkimage: Split size_and_off and size_and_nimage Split 32-bit size_and_off and size_and_nimage fields of the v2 image format header into their own 16-bit size, offset and num_images fields. Set num_images based on number of images passed by the datafile parameter and size based on the offset to the hash field to fix using a single init data file and no boot data file for the v2 image format. Signed-off-by: Jonas Karlman Signed-off-by: Alexey Charkov --- tools/rkcommon.c | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/tools/rkcommon.c b/tools/rkcommon.c index b39777fc060..034896d57f8 100644 --- a/tools/rkcommon.c +++ b/tools/rkcommon.c @@ -34,15 +34,16 @@ enum hash_type { /** * struct image_entry * - * @size_and_off: [31:16]image size;[15:0]image offset - * @address: default as 0xFFFFFFFF + * @offset: image offset (unit as 512 byte blocks) + * @size: image size (unit as 512 byte blocks) + * @address: load address (default as 0xFFFFFFFF) * @flag: no use * @counter: no use * @hash: hash of image - * */ struct image_entry { - uint32_t size_and_off; + uint16_t offset; + uint16_t size; uint32_t address; uint32_t flag; uint32_t counter; @@ -56,16 +57,17 @@ struct image_entry { * This is stored at SD card block 64 (where each block is 512 bytes) * * @magic: Magic (must be RK_MAGIC_V2) - * @size_and_nimage: [31:16]number of images;[15:0] - * offset to hash field of header(unit as 4Byte) - * @boot_flag: [3:0]hash type(0:none,1:sha256,2:sha512) - * @signature: hash or signature for header info - * + * @size: offset to hash field of header (unit as 4 bytes) + * @num_images: number of images + * @boot_flag: [3:0] hash type (0:none, 1:sha256, 2:sha512) + * @images: images + * @hash: hash or signature for header info */ struct header0_info_v2 { uint32_t magic; uint8_t reserved[4]; - uint32_t size_and_nimage; + uint16_t size; + uint16_t num_images; uint32_t boot_flag; uint8_t reserved1[104]; struct image_entry images[4]; @@ -351,17 +353,18 @@ static void rkcommon_set_header0_v2(void *buf, struct image_tool_params *params) printf("Image Type: Rockchip %s boot image\n", rkcommon_get_spl_hdr(params)); memset(buf, '\0', RK_INIT_OFFSET * RK_BLK_SIZE); - hdr->magic = cpu_to_le32(RK_MAGIC_V2); - hdr->size_and_nimage = cpu_to_le32((2 << 16) + 384); + hdr->magic = cpu_to_le32(RK_MAGIC_V2); hdr->boot_flag = cpu_to_le32(HASH_SHA256); sector_offset = 4; image_size_array[0] = spl_params.init_size; image_size_array[1] = spl_params.boot_size; for (i = 0; i < 2; i++) { + if (!image_size_array[i]) + break; image_sector_count = image_size_array[i] / RK_BLK_SIZE; - hdr->images[i].size_and_off = cpu_to_le32((image_sector_count - << 16) + sector_offset); + hdr->images[i].offset = cpu_to_le16(sector_offset); + hdr->images[i].size = cpu_to_le16(image_sector_count); hdr->images[i].address = 0xFFFFFFFF; hdr->images[i].counter = cpu_to_le32(i + 1); image_ptr = buf + sector_offset * RK_BLK_SIZE; @@ -370,6 +373,8 @@ static void rkcommon_set_header0_v2(void *buf, struct image_tool_params *params) sector_offset = sector_offset + image_sector_count; } + hdr->num_images = cpu_to_le16(i); + hdr->size = cpu_to_le16(offsetof(typeof(*hdr), hash) / sizeof(uint32_t)); do_sha256_hash(buf, (void *)hdr->hash - buf, hdr->hash); } @@ -516,10 +521,8 @@ void rkcommon_print_header(const void *buf, struct image_tool_params *params) return; } - init_size = header0_v2.images[0].size_and_off >> 16; - init_size = init_size * RK_BLK_SIZE; - boot_size = header0_v2.images[1].size_and_off >> 16; - boot_size = boot_size * RK_BLK_SIZE; + init_size = le16_to_cpu(header0_v2.images[0].size) * RK_BLK_SIZE; + boot_size = le16_to_cpu(header0_v2.images[1].size) * RK_BLK_SIZE; } else { ret = rkcommon_parse_header(buf, &header0, &spl_info); @@ -533,8 +536,9 @@ void rkcommon_print_header(const void *buf, struct image_tool_params *params) } image_type = ret; - init_size = header0.init_size * RK_BLK_SIZE; - boot_size = header0.init_boot_size * RK_BLK_SIZE - init_size; + init_size = le16_to_cpu(header0.init_size) * RK_BLK_SIZE; + boot_size = le16_to_cpu(header0.init_boot_size) * RK_BLK_SIZE - + init_size; printf("Image Type: Rockchip %s (%s) boot image\n", spl_info->spl_hdr, From 30e266f76dfaf4b7a211addb2323d107256a239e Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Wed, 29 Jan 2025 22:36:28 +0000 Subject: [PATCH 02/78] rockchip: mkimage: Print image information for all embedded images The v2 image format can embed up to 4 data files compared to the two init and boot data files using the older image format. Add support for displaying more of the image header information that exists in the v2 image format, e.g. image load address and flag. Example for v2 image format: > tools/mkimage -l rk3576_idblock_v1.09.107.img Rockchip Boot Image (v2) Image 1: 4096 @ 0x1000 - Load address: 0x3ffc0000 Image 2: 77824 @ 0x2000 - Load address: 0x3ff81000 Image 3: 262144 @ 0x15000 Example for older image format: > tools/mkimage -l u-boot-rockchip.bin Rockchip RK32 (SD/MMC) Boot Image Init Data: 20480 @ 0x800 Boot Data: 112640 @ 0x5800 Signed-off-by: Jonas Karlman Signed-off-by: Alexey Charkov --- tools/rkcommon.c | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/tools/rkcommon.c b/tools/rkcommon.c index 034896d57f8..fe0afc5629b 100644 --- a/tools/rkcommon.c +++ b/tools/rkcommon.c @@ -350,8 +350,6 @@ static void rkcommon_set_header0_v2(void *buf, struct image_tool_params *params) uint8_t *image_ptr = NULL; int i; - printf("Image Type: Rockchip %s boot image\n", - rkcommon_get_spl_hdr(params)); memset(buf, '\0', RK_INIT_OFFSET * RK_BLK_SIZE); hdr->magic = cpu_to_le32(RK_MAGIC_V2); hdr->boot_flag = cpu_to_le32(HASH_SHA256); @@ -505,6 +503,29 @@ int rkcommon_verify_header(unsigned char *buf, int size, return -ENOENT; } +static void rkcommon_print_header_v2(const struct header0_info_v2 *hdr) +{ + uint32_t val; + int i; + + printf("Rockchip Boot Image (v2)\n"); + + for (i = 0; i < le16_to_cpu(hdr->num_images); i++) { + printf("Image %u: %u @ 0x%x\n", + le32_to_cpu(hdr->images[i].counter), + le16_to_cpu(hdr->images[i].size) * RK_BLK_SIZE, + le16_to_cpu(hdr->images[i].offset) * RK_BLK_SIZE); + + val = le32_to_cpu(hdr->images[i].address); + if (val != 0xFFFFFFFF) + printf("- Load address: 0x%x\n", val); + + val = le32_to_cpu(hdr->images[i].flag); + if (val) + printf("- Flag: 0x%x\n", val); + } +} + void rkcommon_print_header(const void *buf, struct image_tool_params *params) { struct header0_info header0; @@ -521,8 +542,7 @@ void rkcommon_print_header(const void *buf, struct image_tool_params *params) return; } - init_size = le16_to_cpu(header0_v2.images[0].size) * RK_BLK_SIZE; - boot_size = le16_to_cpu(header0_v2.images[1].size) * RK_BLK_SIZE; + rkcommon_print_header_v2(&header0_v2); } else { ret = rkcommon_parse_header(buf, &header0, &spl_info); @@ -540,15 +560,16 @@ void rkcommon_print_header(const void *buf, struct image_tool_params *params) boot_size = le16_to_cpu(header0.init_boot_size) * RK_BLK_SIZE - init_size; - printf("Image Type: Rockchip %s (%s) boot image\n", - spl_info->spl_hdr, + printf("Rockchip %s (%s) Boot Image\n", spl_info->spl_hdr, (image_type == IH_TYPE_RKSD) ? "SD/MMC" : "SPI"); - } - printf("Init Data Size: %d bytes\n", init_size); + printf("Init Data: %d @ 0x%x\n", init_size, + le16_to_cpu(header0.init_offset) * RK_BLK_SIZE); - if (boot_size != RK_MAX_BOOT_SIZE) - printf("Boot Data Size: %d bytes\n", boot_size); + if (boot_size != RK_MAX_BOOT_SIZE) + printf("Boot Data: %d @ 0x%x\n", boot_size, init_size + + le16_to_cpu(header0.init_offset) * RK_BLK_SIZE); + } } void rkcommon_rc4_encode_spl(void *buf, unsigned int offset, unsigned int size) From 5c0de9d1811529919692b727dae867e5303ca6a2 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Wed, 29 Jan 2025 22:36:29 +0000 Subject: [PATCH 03/78] rockchip: mkimage: Print boot0 and boot1 parameters The v2 image format embeds boot0 and boot1 parameters, the vendor tool boot_merger may write these parameters based on the rkboot miniall.ini files. E.g. a RK3576 boot image may contain a boot1 parameter that signals BootROM or vendor blobs to use 1 GHz instead of the regular 24 MHz rate for the high precision timer. Add support for printing boot0 and boot1 parameters, e.g.: > tools/mkimage -l rk3576_idblock_v1.09.107.img Rockchip Boot Image (v2) Boot1 2: 0x100 Image 1: 4096 @ 0x1000 - Load address: 0x3ffc0000 Image 2: 77824 @ 0x2000 - Load address: 0x3ff81000 Image 3: 262144 @ 0x15000 Signed-off-by: Jonas Karlman Signed-off-by: Alexey Charkov --- tools/rkcommon.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tools/rkcommon.c b/tools/rkcommon.c index fe0afc5629b..992c90791ff 100644 --- a/tools/rkcommon.c +++ b/tools/rkcommon.c @@ -62,6 +62,8 @@ struct image_entry { * @boot_flag: [3:0] hash type (0:none, 1:sha256, 2:sha512) * @images: images * @hash: hash or signature for header info + * + * Other fields are not used by U-Boot */ struct header0_info_v2 { uint32_t magic; @@ -69,7 +71,9 @@ struct header0_info_v2 { uint16_t size; uint16_t num_images; uint32_t boot_flag; - uint8_t reserved1[104]; + uint8_t reserved1[32]; + uint32_t boot0_param[10]; + uint32_t boot1_param[8]; struct image_entry images[4]; uint8_t reserved2[1064]; uint8_t hash[512]; @@ -510,6 +514,18 @@ static void rkcommon_print_header_v2(const struct header0_info_v2 *hdr) printf("Rockchip Boot Image (v2)\n"); + for (i = 0; i < ARRAY_SIZE(hdr->boot0_param); i++) { + val = le32_to_cpu(hdr->boot0_param[i]); + if (val) + printf("Boot0 %d: 0x%x\n", i, val); + } + + for (i = 0; i < ARRAY_SIZE(hdr->boot1_param); i++) { + val = le32_to_cpu(hdr->boot1_param[i]); + if (val) + printf("Boot1 %d: 0x%x\n", i, val); + } + for (i = 0; i < le16_to_cpu(hdr->num_images); i++) { printf("Image %u: %u @ 0x%x\n", le32_to_cpu(hdr->images[i].counter), From 310fa986f0d07db378d42caf9a9b45a547de0c49 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Wed, 29 Jan 2025 22:36:30 +0000 Subject: [PATCH 04/78] rockchip: mkimage: Add option to change image offset alignment The vendor boot_merger tool support a ALIGN parameter that is used to define offset alignment of the embedded images. Vendor use this for RK3576 to change offset alignment from the common 2 KiB to 4 KiB, presumably it may have something to do with UFS. Testing with eMMC has shown that using a 512-byte alignment also work. Add support for overriding offset alignment in case this is needed for e.g. RK3576 in the future. Signed-off-by: Jonas Karlman Signed-off-by: Alexey Charkov --- tools/rkcommon.c | 75 +++++++++++++++++++++++++++++++----------------- tools/rkcommon.h | 2 -- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/tools/rkcommon.c b/tools/rkcommon.c index 992c90791ff..b413cd3f7e4 100644 --- a/tools/rkcommon.c +++ b/tools/rkcommon.c @@ -140,6 +140,7 @@ struct spl_info { const uint32_t spl_size; const bool spl_rc4; const uint32_t header_ver; + const uint32_t align; }; static struct spl_info spl_infos[] = { @@ -200,14 +201,19 @@ static struct spl_info *rkcommon_get_spl_info(char *imagename) return NULL; } -static int rkcommon_get_aligned_size(struct image_tool_params *params, - const char *fname) +static bool rkcommon_is_header_v2(struct image_tool_params *params) { - int size; + struct spl_info *info = rkcommon_get_spl_info(params->imagename); - size = imagetool_get_filesize(params, fname); - if (size < 0) - return -1; + return (info->header_ver == RK_HEADER_V2); +} + +static int rkcommon_get_aligned_size(struct image_tool_params *params, int size) +{ + struct spl_info *info = rkcommon_get_spl_info(params->imagename); + + if (info->align) + return ROUND(size, info->align * RK_BLK_SIZE); /* * Pad to a 2KB alignment, as required for init/boot size by the ROM @@ -216,6 +222,27 @@ static int rkcommon_get_aligned_size(struct image_tool_params *params, return ROUND(size, RK_SIZE_ALIGN); } +static int rkcommon_get_header_size(struct image_tool_params *params) +{ + int header_size = rkcommon_is_header_v2(params) ? + sizeof(struct header0_info_v2) : + sizeof(struct header0_info); + + return rkcommon_get_aligned_size(params, header_size); +} + +static int rkcommon_get_aligned_filesize(struct image_tool_params *params, + const char *fname) +{ + int size; + + size = imagetool_get_filesize(params, fname); + if (size < 0) + return -1; + + return rkcommon_get_aligned_size(params, size); +} + int rkcommon_check_params(struct image_tool_params *params) { int i, size; @@ -238,14 +265,14 @@ int rkcommon_check_params(struct image_tool_params *params) spl_params.boot_file += 1; } - size = rkcommon_get_aligned_size(params, spl_params.init_file); + size = rkcommon_get_aligned_filesize(params, spl_params.init_file); if (size < 0) return EXIT_FAILURE; spl_params.init_size = size; /* Boot file is optional, and only for back-to-bootrom functionality. */ if (spl_params.boot_file) { - size = rkcommon_get_aligned_size(params, spl_params.boot_file); + size = rkcommon_get_aligned_filesize(params, spl_params.boot_file); if (size < 0) return EXIT_FAILURE; spl_params.boot_size = size; @@ -302,13 +329,6 @@ bool rkcommon_need_rc4_spl(struct image_tool_params *params) return info->spl_rc4; } -static bool rkcommon_is_header_v2(struct image_tool_params *params) -{ - struct spl_info *info = rkcommon_get_spl_info(params->imagename); - - return (info->header_ver == RK_HEADER_V2); -} - static void do_sha256_hash(uint8_t *buf, uint32_t size, uint8_t *out) { sha256_context ctx; @@ -321,12 +341,13 @@ static void do_sha256_hash(uint8_t *buf, uint32_t size, uint8_t *out) static void rkcommon_set_header0(void *buf, struct image_tool_params *params) { struct header0_info *hdr = buf; - uint32_t init_boot_size; + uint32_t init_boot_size, init_offset; - memset(buf, '\0', RK_INIT_OFFSET * RK_BLK_SIZE); + init_offset = rkcommon_get_header_size(params) / RK_BLK_SIZE; + memset(buf, '\0', init_offset * RK_BLK_SIZE); hdr->magic = cpu_to_le32(RK_MAGIC); hdr->disable_rc4 = cpu_to_le32(!rkcommon_need_rc4_spl(params)); - hdr->init_offset = cpu_to_le16(RK_INIT_OFFSET); + hdr->init_offset = cpu_to_le16(init_offset); hdr->init_size = cpu_to_le16(spl_params.init_size / RK_BLK_SIZE); /* @@ -354,10 +375,10 @@ static void rkcommon_set_header0_v2(void *buf, struct image_tool_params *params) uint8_t *image_ptr = NULL; int i; - memset(buf, '\0', RK_INIT_OFFSET * RK_BLK_SIZE); + sector_offset = rkcommon_get_header_size(params) / RK_BLK_SIZE; + memset(buf, '\0', sector_offset * RK_BLK_SIZE); hdr->magic = cpu_to_le32(RK_MAGIC_V2); hdr->boot_flag = cpu_to_le32(HASH_SHA256); - sector_offset = 4; image_size_array[0] = spl_params.init_size; image_size_array[1] = spl_params.boot_size; @@ -383,11 +404,12 @@ static void rkcommon_set_header0_v2(void *buf, struct image_tool_params *params) void rkcommon_set_header(void *buf, struct stat *sbuf, int ifd, struct image_tool_params *params) { - struct header1_info *hdr = buf + RK_SPL_HDR_START; - if (rkcommon_is_header_v2(params)) { rkcommon_set_header0_v2(buf, params); } else { + int header_size = rkcommon_get_header_size(params); + struct header1_info *hdr = buf + header_size; + rkcommon_set_header0(buf, params); /* Set up the SPL name (i.e. copy spl_hdr over) */ @@ -395,12 +417,12 @@ void rkcommon_set_header(void *buf, struct stat *sbuf, int ifd, memcpy(&hdr->magic, rkcommon_get_spl_hdr(params), RK_SPL_HDR_SIZE); if (rkcommon_need_rc4_spl(params)) - rkcommon_rc4_encode_spl(buf, RK_SPL_HDR_START, + rkcommon_rc4_encode_spl(buf, header_size, spl_params.init_size); if (spl_params.boot_file) { if (rkcommon_need_rc4_spl(params)) - rkcommon_rc4_encode_spl(buf + RK_SPL_HDR_START, + rkcommon_rc4_encode_spl(buf + header_size, spl_params.init_size, spl_params.boot_size); } @@ -625,7 +647,7 @@ int rkcommon_vrec_header(struct image_tool_params *params, * 4 bytes of these images can safely be overwritten using the * boot magic. */ - tparams->header_size = RK_SPL_HDR_START; + tparams->header_size = rkcommon_get_header_size(params); /* Allocate, clear and install the header */ tparams->hdr = malloc(tparams->header_size); @@ -643,7 +665,8 @@ int rkcommon_vrec_header(struct image_tool_params *params, params->orig_file_size = tparams->header_size + spl_params.init_size + spl_params.boot_size; - params->file_size = ROUND(params->orig_file_size, RK_SIZE_ALIGN); + params->file_size = rkcommon_get_aligned_size(params, + params->orig_file_size); /* Ignoring pad len, since we are using our own copy_image() */ return 0; diff --git a/tools/rkcommon.h b/tools/rkcommon.h index 5d2770a80f1..c887a659a95 100644 --- a/tools/rkcommon.h +++ b/tools/rkcommon.h @@ -10,9 +10,7 @@ enum { RK_BLK_SIZE = 512, RK_SIZE_ALIGN = 2048, - RK_INIT_OFFSET = 4, RK_MAX_BOOT_SIZE = 512 << 10, - RK_SPL_HDR_START = RK_INIT_OFFSET * RK_BLK_SIZE, RK_SPL_HDR_SIZE = 4, }; From 47b75406d4b4a3ae69f32a1f5ccd28d934eb3ee8 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Wed, 29 Jan 2025 22:36:31 +0000 Subject: [PATCH 05/78] rockchip: mkimage: Add support for up to 4 input files The v2 image format can support up to 4 embedded images that can be loaded by the BootROM using the back-to-bootrom method. Currently two input files can be passed in using the datafile parameter, separated by a colon (":"). Extend the datafile parameter parsing to support up to 4 input files separated by a colon (":") for use with the v2 image format. Signed-off-by: Jonas Karlman Signed-off-by: Alexey Charkov --- tools/rkcommon.c | 93 +++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/tools/rkcommon.c b/tools/rkcommon.c index b413cd3f7e4..a52d1f36d77 100644 --- a/tools/rkcommon.c +++ b/tools/rkcommon.c @@ -167,17 +167,15 @@ static struct spl_info spl_infos[] = { /** * struct spl_params - spl params parsed in check_params() * - * @init_file: Init data file path - * @init_size: Aligned size of init data in bytes - * @boot_file: Boot data file path - * @boot_size: Aligned size of boot data in bytes + * @file: image file path + * @size: aligned size of image in bytes */ struct spl_params { - char *init_file; - uint32_t init_size; - char *boot_file; - uint32_t boot_size; + struct { + char *file; + uint32_t size; + } images[4]; }; static struct spl_params spl_params = { 0 }; @@ -257,31 +255,32 @@ int rkcommon_check_params(struct image_tool_params *params) if (!rkcommon_get_spl_info(params->imagename)) goto err_spl_info; - spl_params.init_file = params->datafile; + spl_params.images[0].file = params->datafile; + for (i = 1; i < ARRAY_SIZE(spl_params.images); i++) { + spl_params.images[i].file = + strchr(spl_params.images[i - 1].file, ':'); + if (!spl_params.images[i].file) + break; - spl_params.boot_file = strchr(spl_params.init_file, ':'); - if (spl_params.boot_file) { - *spl_params.boot_file = '\0'; - spl_params.boot_file += 1; + *spl_params.images[i].file = '\0'; + spl_params.images[i].file += 1; } - size = rkcommon_get_aligned_filesize(params, spl_params.init_file); - if (size < 0) - return EXIT_FAILURE; - spl_params.init_size = size; + for (i = 0; i < ARRAY_SIZE(spl_params.images); i++) { + if (!spl_params.images[i].file) + break; - /* Boot file is optional, and only for back-to-bootrom functionality. */ - if (spl_params.boot_file) { - size = rkcommon_get_aligned_filesize(params, spl_params.boot_file); + size = rkcommon_get_aligned_filesize(params, + spl_params.images[i].file); if (size < 0) return EXIT_FAILURE; - spl_params.boot_size = size; + spl_params.images[i].size = size; } - if (spl_params.init_size > rkcommon_get_spl_size(params)) { + if (spl_params.images[0].size > rkcommon_get_spl_size(params)) { fprintf(stderr, "Error: SPL image is too large (size %#x than %#x)\n", - spl_params.init_size, rkcommon_get_spl_size(params)); + spl_params.images[0].size, rkcommon_get_spl_size(params)); return EXIT_FAILURE; } @@ -348,7 +347,7 @@ static void rkcommon_set_header0(void *buf, struct image_tool_params *params) hdr->magic = cpu_to_le32(RK_MAGIC); hdr->disable_rc4 = cpu_to_le32(!rkcommon_need_rc4_spl(params)); hdr->init_offset = cpu_to_le16(init_offset); - hdr->init_size = cpu_to_le16(spl_params.init_size / RK_BLK_SIZE); + hdr->init_size = cpu_to_le16(spl_params.images[0].size / RK_BLK_SIZE); /* * init_boot_size needs to be set, as it is read by the BootROM @@ -358,10 +357,11 @@ static void rkcommon_set_header0(void *buf, struct image_tool_params *params) * see https://patch.msgid.link/CANbgqAQ5p-fE4T-Ye9UAMs-nRGU8AP_yxDf+5SbZbP8bxLa0dg@mail.gmail.com/ * for a more detailed explanation by Andy Yan */ - if (spl_params.boot_file) - init_boot_size = spl_params.init_size + spl_params.boot_size; + if (spl_params.images[1].file) + init_boot_size = spl_params.images[0].size + + spl_params.images[1].size; else - init_boot_size = spl_params.init_size + RK_MAX_BOOT_SIZE; + init_boot_size = spl_params.images[0].size + RK_MAX_BOOT_SIZE; hdr->init_boot_size = cpu_to_le16(init_boot_size / RK_BLK_SIZE); rc4_encode(buf, RK_BLK_SIZE, rc4_key); @@ -371,7 +371,6 @@ static void rkcommon_set_header0_v2(void *buf, struct image_tool_params *params) { struct header0_info_v2 *hdr = buf; uint32_t sector_offset, image_sector_count; - uint32_t image_size_array[2]; uint8_t *image_ptr = NULL; int i; @@ -379,19 +378,17 @@ static void rkcommon_set_header0_v2(void *buf, struct image_tool_params *params) memset(buf, '\0', sector_offset * RK_BLK_SIZE); hdr->magic = cpu_to_le32(RK_MAGIC_V2); hdr->boot_flag = cpu_to_le32(HASH_SHA256); - image_size_array[0] = spl_params.init_size; - image_size_array[1] = spl_params.boot_size; - for (i = 0; i < 2; i++) { - if (!image_size_array[i]) + for (i = 0; i < ARRAY_SIZE(spl_params.images); i++) { + if (!spl_params.images[i].size) break; - image_sector_count = image_size_array[i] / RK_BLK_SIZE; + image_sector_count = spl_params.images[i].size / RK_BLK_SIZE; hdr->images[i].offset = cpu_to_le16(sector_offset); hdr->images[i].size = cpu_to_le16(image_sector_count); hdr->images[i].address = 0xFFFFFFFF; hdr->images[i].counter = cpu_to_le32(i + 1); image_ptr = buf + sector_offset * RK_BLK_SIZE; - do_sha256_hash(image_ptr, image_size_array[i], + do_sha256_hash(image_ptr, spl_params.images[i].size, hdr->images[i].hash); sector_offset = sector_offset + image_sector_count; } @@ -418,13 +415,13 @@ void rkcommon_set_header(void *buf, struct stat *sbuf, int ifd, if (rkcommon_need_rc4_spl(params)) rkcommon_rc4_encode_spl(buf, header_size, - spl_params.init_size); + spl_params.images[0].size); - if (spl_params.boot_file) { + if (spl_params.images[1].file) { if (rkcommon_need_rc4_spl(params)) rkcommon_rc4_encode_spl(buf + header_size, - spl_params.init_size, - spl_params.boot_size); + spl_params.images[0].size, + spl_params.images[1].size); } } } @@ -662,8 +659,9 @@ int rkcommon_vrec_header(struct image_tool_params *params, * We need to store the original file-size (i.e. before padding), as * imagetool does not set this during its adjustment of file_size. */ - params->orig_file_size = tparams->header_size + - spl_params.init_size + spl_params.boot_size; + params->orig_file_size = tparams->header_size; + for (int i = 0; i < ARRAY_SIZE(spl_params.images); i++) + params->orig_file_size += spl_params.images[i].size; params->file_size = rkcommon_get_aligned_size(params, params->orig_file_size); @@ -750,16 +748,13 @@ static int copy_file(struct image_tool_params *params, int ifd, int rockchip_copy_image(int ifd, struct image_tool_params *params) { - int ret; - - ret = copy_file(params, ifd, spl_params.init_file, - spl_params.init_size); - if (ret) - return ret; + int i, ret; - if (spl_params.boot_file) { - ret = copy_file(params, ifd, spl_params.boot_file, - spl_params.boot_size); + for (i = 0; i < ARRAY_SIZE(spl_params.images); i++) { + if (!spl_params.images[i].size) + break; + ret = copy_file(params, ifd, spl_params.images[i].file, + spl_params.images[i].size); if (ret) return ret; } From 3db39820063f8a2c252fae9b6e3257bbe745c87a Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Wed, 29 Jan 2025 22:36:32 +0000 Subject: [PATCH 06/78] rockchip: mkimage: Add option for image load address and flag The v2 image format supports defining a load address and flag for each embedded image. Add initial support for writing the image load address and flag to the v2 image format header. This may later be used for RK3576 to embed a minimal initial image that if required to fix booting from SD-card due to a BootROM issue. Signed-off-by: Jonas Karlman Signed-off-by: Alexey Charkov --- tools/rkcommon.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/rkcommon.c b/tools/rkcommon.c index a52d1f36d77..81361cde482 100644 --- a/tools/rkcommon.c +++ b/tools/rkcommon.c @@ -169,12 +169,16 @@ static struct spl_info spl_infos[] = { * * @file: image file path * @size: aligned size of image in bytes + * @address: image load address + * @flag: no use */ struct spl_params { struct { char *file; uint32_t size; + uint32_t address; + uint32_t flag; } images[4]; }; @@ -385,7 +389,8 @@ static void rkcommon_set_header0_v2(void *buf, struct image_tool_params *params) image_sector_count = spl_params.images[i].size / RK_BLK_SIZE; hdr->images[i].offset = cpu_to_le16(sector_offset); hdr->images[i].size = cpu_to_le16(image_sector_count); - hdr->images[i].address = 0xFFFFFFFF; + hdr->images[i].address = spl_params.images[i].address ?: 0xFFFFFFFF; + hdr->images[i].flag = spl_params.images[i].flag; hdr->images[i].counter = cpu_to_le32(i + 1); image_ptr = buf + sector_offset * RK_BLK_SIZE; do_sha256_hash(image_ptr, spl_params.images[i].size, From ef88cc1ebdac834ce2a2536038f7edd1f752e1ab Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Tue, 28 Jan 2025 01:30:12 +0000 Subject: [PATCH 07/78] rockchip: mkimage: Add rk3576 align and sd-card workaround The BootROM on RK3576 has an issue loading boot images from an SD-card. This issue can be worked around by injecting an initial boot image before TPL that: writel(0x3ffff800, 0x3ff803b0) Prepend an image containing binary code that does this and return to BootROM to load next image, TPL. Signed-off-by: Jonas Karlman [switch from pre-built binary to from-source build via existing Makefiles] Signed-off-by: Alexey Charkov --- arch/arm/mach-rockchip/rk3576/Makefile | 10 ++++++++ arch/arm/mach-rockchip/rk3576/rk3576-boost.c | 25 ++++++++++++++++++++ tools/rkcommon.c | 23 +++++++++++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 arch/arm/mach-rockchip/rk3576/rk3576-boost.c diff --git a/arch/arm/mach-rockchip/rk3576/Makefile b/arch/arm/mach-rockchip/rk3576/Makefile index cbc58257deb..464def20c17 100644 --- a/arch/arm/mach-rockchip/rk3576/Makefile +++ b/arch/arm/mach-rockchip/rk3576/Makefile @@ -7,3 +7,13 @@ obj-y += rk3576.o obj-y += clk_rk3576.o obj-y += syscon_rk3576.o + +# BootROM "boost" stub: a freestanding bit of AArch64 code that pokes a fix-up +# value into SRAM so the ROM can boot from SD/eMMC. mkimage prepends it to the +# init image, so it must exist in the build dir. +quiet_cmd_rk3576boost = BOOST $@ + cmd_rk3576boost = $(CC) -nostdlib -ffreestanding -Os -c -o $@.o $< && \ + $(OBJCOPY) -O binary -j .text $@.o $@ +$(obj)/rk3576-boost.bin: $(src)/rk3576-boost.c FORCE + $(call if_changed,rk3576boost) +always-y += rk3576-boost.bin diff --git a/arch/arm/mach-rockchip/rk3576/rk3576-boost.c b/arch/arm/mach-rockchip/rk3576/rk3576-boost.c new file mode 100644 index 00000000000..2ffcaaacf5c --- /dev/null +++ b/arch/arm/mach-rockchip/rk3576/rk3576-boost.c @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright Contributors to the U-Boot project. + +/* + * Early initialization code to be executed before RAM training on RK3576. + * At the moment this only contains a BootROM fixup for booting from an SD card + * + * Anything in this file is to be compiled into a freestanding raw executable + * binary and prepended to the RKNS image by mkimage during build + */ + +#include + +#define SYS_SRAM_BASE 0x3ff80000 +#define OFFSET 0x03b0 + +int _start(void) +{ + uint32_t *sram = (void*)(SYS_SRAM_BASE + OFFSET); + + /* set unknown value in sram to fix boot from sdmmc */ + *(sram) = 0x3ffff800; + + return 0; +} diff --git a/tools/rkcommon.c b/tools/rkcommon.c index 81361cde482..56e6be40b96 100644 --- a/tools/rkcommon.c +++ b/tools/rkcommon.c @@ -158,7 +158,7 @@ static struct spl_info spl_infos[] = { { "rk3506", "RK35", 0xC000 - 0x1000, false, RK_HEADER_V2 }, { "rk3528", "RK35", 0x10000 - 0x1000, false, RK_HEADER_V2 }, { "rk3568", "RK35", 0x10000 - 0x1000, false, RK_HEADER_V2 }, - { "rk3576", "RK35", 0x80000 - 0x1000, false, RK_HEADER_V2 }, + { "rk3576", "RK35", 0x80000 - 0x1000, false, RK_HEADER_V2, 8 }, { "rk3588", "RK35", 0x100000 - 0x1000, false, RK_HEADER_V2 }, { "rv1108", "RK11", 0x1800, false, RK_HEADER_V1 }, { "rv1126", "110B", 0x10000 - 0x1000, false, RK_HEADER_V1 }, @@ -288,6 +288,27 @@ int rkcommon_check_params(struct image_tool_params *params) return EXIT_FAILURE; } + if (!strcmp(params->imagename, "rk3576")) { + char *boost = "arch/arm/mach-rockchip/rk3576/rk3576-boost.bin"; + + size = rkcommon_get_aligned_filesize(params, boost); + if (size < 0) { + fprintf(stderr, + "Error: rk3576-boost.bin is required but missing\n"); + return EXIT_FAILURE; + } + + for (i = ARRAY_SIZE(spl_params.images) - 1; i > 0; i--) { + spl_params.images[i] = spl_params.images[i - 1]; + } + + spl_params.images[0].file = boost; + spl_params.images[0].size = size; + + spl_params.images[0].address = 0x3ffc0000; + spl_params.images[1].address = 0x3ff81000; + } + return EXIT_SUCCESS; err_spl_info: From 94b420690c3b20e369504b6fcd9a72d5b39192db Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 25 Nov 2025 22:47:35 +0400 Subject: [PATCH 08/78] board: rockchip: Set SFC1 pin mode on FriendlyElec NanoPi M5 NanoPi M5 uses the M1 pin configuration for its serial flash controller. Set the pin mode to M1 explicitly to enable SPL loading from SPI flash. Signed-off-by: Alexey Charkov --- arch/arm/dts/rk3576-nanopi-m5-u-boot.dtsi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arch/arm/dts/rk3576-nanopi-m5-u-boot.dtsi b/arch/arm/dts/rk3576-nanopi-m5-u-boot.dtsi index 0767fbb8168..aa939f99f31 100644 --- a/arch/arm/dts/rk3576-nanopi-m5-u-boot.dtsi +++ b/arch/arm/dts/rk3576-nanopi-m5-u-boot.dtsi @@ -15,6 +15,9 @@ }; &sfc1 { + pinctrl-0 = <&fspi1m1_csn0 &fspi1m1_pins>; + pinctrl-names = "default"; + flash@0 { bootph-pre-ram; bootph-some-ram; From 3a0c35b873095a07edcd450fd0e93971de653d1b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 22 Oct 2025 16:15:45 +0400 Subject: [PATCH 09/78] board: rockchip: Add RK3576 EVB1 RK3576 EVB1 is the evaluation board for the RK3576 SoC, which also serves as the reference design for board vendors. Enable building images for it using upstream DTS and a minimal defconfig. Signed-off-by: Alexey Charkov --- arch/arm/mach-rockchip/rk3576/MAINTAINERS | 5 ++ configs/evb-rk3576_defconfig | 75 +++++++++++++++++++++++ doc/board/rockchip/rockchip.rst | 1 + 3 files changed, 81 insertions(+) create mode 100644 configs/evb-rk3576_defconfig diff --git a/arch/arm/mach-rockchip/rk3576/MAINTAINERS b/arch/arm/mach-rockchip/rk3576/MAINTAINERS index 79cf9e97f70..a8dadca6486 100644 --- a/arch/arm/mach-rockchip/rk3576/MAINTAINERS +++ b/arch/arm/mach-rockchip/rk3576/MAINTAINERS @@ -28,6 +28,11 @@ S: Maintained F: arch/arm/dts/rk3576-rock-4d* F: configs/rock-4d-rk3576_defconfig +ROCKCHIP-EVB1-RK3576 +M: Alexey Charkov +S: Maintained +F: configs/evb-rk3576_defconfig + SIGE5-RK3576 M: Jonas Karlman S: Maintained diff --git a/configs/evb-rk3576_defconfig b/configs/evb-rk3576_defconfig new file mode 100644 index 00000000000..681acf43570 --- /dev/null +++ b/configs/evb-rk3576_defconfig @@ -0,0 +1,75 @@ +CONFIG_ARM=y +CONFIG_SKIP_LOWLEVEL_INIT=y +CONFIG_COUNTER_FREQUENCY=24000000 +CONFIG_ARCH_ROCKCHIP=y +CONFIG_SPL_GPIO=y +CONFIG_DEFAULT_DEVICE_TREE="rockchip/rk3576-evb1-v10" +CONFIG_ROCKCHIP_RK3576=y +CONFIG_SYS_LOAD_ADDR=0x40c00800 +CONFIG_DEBUG_UART_BASE=0x2AD40000 +CONFIG_DEBUG_UART_CLOCK=24000000 +CONFIG_PCI=y +CONFIG_DEBUG_UART=y +CONFIG_DEFAULT_FDT_FILE="rockchip/rk3576-evb1-v10.dtb" +# CONFIG_DISPLAY_CPUINFO is not set +CONFIG_SPL_MAX_SIZE=0x40000 +# CONFIG_SPL_RAW_IMAGE_SUPPORT is not set +CONFIG_SPL_DM_RESET=y +CONFIG_SPL_UFS_SUPPORT=y +CONFIG_CMD_MEMINFO=y +CONFIG_CMD_MEMINFO_MAP=y +CONFIG_CMD_ADC=y +CONFIG_CMD_GPIO=y +CONFIG_CMD_GPT=y +CONFIG_CMD_I2C=y +CONFIG_CMD_MISC=y +CONFIG_CMD_MMC=y +CONFIG_CMD_PCI=y +CONFIG_CMD_UFS=y +CONFIG_CMD_USB=y +CONFIG_CMD_ROCKUSB=y +CONFIG_CMD_USB_MASS_STORAGE=y +# CONFIG_CMD_SETEXPR is not set +CONFIG_CMD_RNG=y +CONFIG_CMD_REGULATOR=y +# CONFIG_SPL_DOS_PARTITION is not set +CONFIG_OF_SPL_REMOVE_PROPS="clock-names interrupt-parent assigned-clocks assigned-clock-rates assigned-clock-parents" +CONFIG_BUTTON=y +CONFIG_BUTTON_GPIO=y +# CONFIG_USB_FUNCTION_FASTBOOT is not set +CONFIG_ROCKCHIP_GPIO=y +CONFIG_SYS_I2C_ROCKCHIP=y +CONFIG_LED=y +CONFIG_LED_GPIO=y +CONFIG_SUPPORT_EMMC_RPMB=y +CONFIG_MMC_DW=y +CONFIG_MMC_DW_ROCKCHIP=y +CONFIG_MMC_SDHCI=y +CONFIG_MMC_SDHCI_SDMA=y +CONFIG_MMC_SDHCI_ROCKCHIP=y +CONFIG_PHY_REALTEK=y +CONFIG_DWC_ETH_QOS=y +CONFIG_DWC_ETH_QOS_ROCKCHIP=y +CONFIG_NVME_PCI=y +CONFIG_PCIE_DW_ROCKCHIP=y +CONFIG_PHY_ROCKCHIP_INNO_USB2=y +CONFIG_PHY_ROCKCHIP_NANENG_COMBOPHY=y +CONFIG_PHY_ROCKCHIP_USBDP=y +CONFIG_DM_PMIC=y +CONFIG_PMIC_RK8XX=y +CONFIG_REGULATOR_RK8XX=y +CONFIG_SCSI=y +CONFIG_BAUDRATE=1500000 +CONFIG_DEBUG_UART_SHIFT=2 +CONFIG_SYS_NS16550_MEM32=y +CONFIG_SYSRESET_PSCI=y +CONFIG_USB=y +CONFIG_USB_XHCI_HCD=y +CONFIG_USB_DWC3=y +CONFIG_USB_DWC3_GENERIC=y +CONFIG_USB_GADGET=y +CONFIG_USB_GADGET_DOWNLOAD=y +CONFIG_USB_FUNCTION_ROCKUSB=y +CONFIG_UFS=y +CONFIG_UFS_ROCKCHIP=y +CONFIG_ERRNO_STR=y diff --git a/doc/board/rockchip/rockchip.rst b/doc/board/rockchip/rockchip.rst index de2df3046e2..d96706a238a 100644 --- a/doc/board/rockchip/rockchip.rst +++ b/doc/board/rockchip/rockchip.rst @@ -150,6 +150,7 @@ List of mainline supported Rockchip boards: - Generic RK3576 (generic-rk3576) - Luckfox Omni3576 (omni3576-rk3576) - Radxa ROCK 4D (rock-4d-rk3576) + - Rockchip EVB1 RK3576 V10 (evb-rk3576) * rk3588 - ArmSoM Sige7 (sige7-rk3588) From 01d1b2617241a00af93e1476365ddddf6e712f59 Mon Sep 17 00:00:00 2001 From: Anton Burticica Date: Tue, 20 Jan 2026 09:54:08 +0200 Subject: [PATCH 10/78] usb: gadget: f_fastboot: make EP buffer size configurable Introduce CONFIG_USB_FUNCTION_FASTBOOT_EP_BUFFER_SIZE to allow customization of the endpoint buffer size used for fastboot transfers. The EP_BUFFER_SIZE must always be an integral multiple of the maxpacket size (64, 512, or 1024 bytes depending on USB speed), as controllers like DWC3 expect bulk OUT requests to be divisible by maxpacket size. On DWC3 controllers operating in SuperSpeed mode, increasing the endpoint buffer size from the default value significantly improves download throughput - from approximately 50 MB/s to over 170 MB/s. This is particularly beneficial for flashing large images during development or production. The configurable buffer size allows board maintainers to tune the trade-off between memory usage and transfer performance based on their specific requirements and available resources. Change-Id: 0c3402e0-3855-4d3f-b6c1-293793eebc4a Signed-off-by: Anton Burticica --- drivers/fastboot/Kconfig | 12 ++++++++++++ drivers/usb/gadget/f_fastboot.c | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/drivers/fastboot/Kconfig b/drivers/fastboot/Kconfig index 90212fcf9ef..fb063b2c6ab 100644 --- a/drivers/fastboot/Kconfig +++ b/drivers/fastboot/Kconfig @@ -26,6 +26,18 @@ config USB_FUNCTION_FASTBOOT help This enables the USB part of the fastboot gadget. +config USB_FUNCTION_FASTBOOT_EP_BUFFER_SIZE + hex "USB fastboot endpoint buffer size" + depends on USB_FUNCTION_FASTBOOT + default 0x1000 + help + The buffer size used for USB endpoint transfers in fastboot. + This must always be an integral multiple of the maxpacket size + (64 or 512 or 1024), otherwise transfers may fail on certain + controllers like DWC3 that expect bulk OUT requests to be + divisible by maxpacket size. + The default value is 0x1000 (4096 bytes). + config UDP_FUNCTION_FASTBOOT depends on NET_LEGACY select FASTBOOT diff --git a/drivers/usb/gadget/f_fastboot.c b/drivers/usb/gadget/f_fastboot.c index 8df0e3f331d..8350651baf9 100644 --- a/drivers/usb/gadget/f_fastboot.c +++ b/drivers/usb/gadget/f_fastboot.c @@ -31,7 +31,7 @@ #define RX_ENDPOINT_MAXIMUM_PACKET_SIZE_1_1 (0x0040) #define TX_ENDPOINT_MAXIMUM_PACKET_SIZE (0x0040) -#define EP_BUFFER_SIZE 4096 +#define EP_BUFFER_SIZE (CONFIG_USB_FUNCTION_FASTBOOT_EP_BUFFER_SIZE) /* * EP_BUFFER_SIZE must always be an integral multiple of maxpacket size * (64 or 512 or 1024), else we break on certain controllers like DWC3 From 813dd86b831a5fbed8aaeb3b4f7815168a5d4cfb Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 14:19:58 +0400 Subject: [PATCH 11/78] HACK: Add Ethernet nodes to Luckfox Omni3576 DTS This change will go via the upstream DTS tree, so just put it in here temporarily to get Ethernet working on Omni3576 boards. Signed-off-by: Alexey Charkov --- .../rockchip/rk3576-luckfox-core3576.dtsi | 35 +++++++++++++++++++ .../rockchip/rk3576-luckfox-omni3576.dts | 35 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi index 749f0a54b47..9c64254263f 100644 --- a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi +++ b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi @@ -225,6 +225,21 @@ cpu-supply = <&vdd_cpu_lit_s0>; }; +&gmac0 { + clock_in_out = "output"; + phy-mode = "rgmii-rxid"; + phy-handle = <&rgmii_phy0>; + pinctrl-names = "default"; + pinctrl-0 = <ð0m0_miim + ð0m0_tx_bus2 + ð0m0_rx_bus2 + ð0m0_rgmii_clk + ð0m0_rgmii_bus + ðm0_clk0_25m_out>; + tx_delay = <0x20>; + status = "okay"; +}; + &gpu { mali-supply = <&vdd_gpu_s0>; status = "okay"; @@ -645,6 +660,20 @@ }; }; +&mdio0 { + rgmii_phy0: ethernet-phy@0 { + compatible = "ethernet-phy-id001c.c916"; + reg = <0x0>; + clocks = <&cru REFCLKO25M_GMAC0_OUT>; + pinctrl-names = "default"; + pinctrl-0 = <&gmac0_rst>; + realtek,aldps-enable; + reset-assert-us = <20000>; + reset-deassert-us = <100000>; + reset-gpios = <&gpio2 RK_PB3 GPIO_ACTIVE_LOW>; + }; +}; + &pcie0 { pinctrl-names = "default"; pinctrl-0 = <&pcie_reset>; @@ -654,6 +683,12 @@ }; &pinctrl { + gmac { + gmac0_rst: ethphy0-rst { + rockchip,pins = <2 RK_PB3 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hdmi { hdmi_con_en: hdmi-con-en { rockchip,pins = <4 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts index 6c75959adfe..c221fefedea 100644 --- a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts +++ b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts @@ -30,7 +30,42 @@ }; }; +&gmac1 { + clock_in_out = "output"; + phy-handle = <&rgmii_phy1>; + phy-mode = "rgmii-rxid"; + pinctrl-names = "default"; + pinctrl-0 = <ð1m0_miim + ð1m0_tx_bus2 + ð1m0_rx_bus2 + ð1m0_rgmii_clk + ð1m0_rgmii_bus + ðm0_clk1_25m_out>; + tx_delay = <0x20>; + status = "okay"; +}; + +&mdio1 { + rgmii_phy1: ethernet-phy@0 { + compatible = "ethernet-phy-id001c.c916"; + reg = <0x0>; + clocks = <&cru REFCLKO25M_GMAC1_OUT>; + pinctrl-names = "default"; + pinctrl-0 = <&gmac1_rst>; + realtek,aldps-enable; + reset-assert-us = <20000>; + reset-deassert-us = <100000>; + reset-gpios = <&gpio2 RK_PB4 GPIO_ACTIVE_LOW>; + }; +}; + &pinctrl { + gmac { + gmac1_rst: ethphy1-rst { + rockchip,pins = <2 RK_PB4 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + leds { led_green_pin: led-green-pin { rockchip,pins = <1 RK_PD5 RK_FUNC_GPIO &pcfg_pull_none>; From a61629dfa405204d6b3d197abe3a09b2be62865f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 15:42:13 +0400 Subject: [PATCH 12/78] arm64: dts: rockchip: Drop incorrect eMMC regulators on Luckfox Core3576 Remove the supply regulators from the eMMC controller node on Luckfox Core3576, as they cause the eMMC to endlessly re-tune phase at startup, and are most likely wrong: 1. The vendor DTS doesn't define those regulators 2. The VCCQ regulator referenced here is actually used for the SD card, and it is unlikely that both can share the same regulator with an 1.8-3.3V range, whereas eMMC only expects 1.8V 3. Rockchip reference schematic, which this board broadly follows, drives the VCCQ supply of eMMC flash from VCC_1V8_S0 rather than VCCIO_SD_S0, where VCC_1V8_S0 is a fixed 1.8V load switch with VIN tied to VCC_1V8_S3 and EN tied to VCCA_1V8_S0 (a.k.a. PMIC PLDO1, not PLDO5) There is no published schematic for the Core3576 SoM unfortunately. Cc: stable@vger.kernel.org Fixes: d7ad90d22abe ("arm64: dts: rockchip: Add Luckfox Omni3576 Board support") Signed-off-by: Alexey Charkov --- dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi | 2 -- 1 file changed, 2 deletions(-) diff --git a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi index 9c64254263f..e74cefc0a5e 100644 --- a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi +++ b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi @@ -747,8 +747,6 @@ no-sd; no-sdio; non-removable; - vmmc-supply = <&vcc_3v3_s3>; - vqmmc-supply = <&vccio_sd_s0>; status = "okay"; }; From 089bf0e001efc1a56126112ae5414208b958ca18 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 17:06:12 +0400 Subject: [PATCH 13/78] Enable Ethernet in Luckfox Omni3576 defconfig Signed-off-by: Alexey Charkov --- configs/omni3576-rk3576_defconfig | 3 +++ dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi | 5 ++--- dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/configs/omni3576-rk3576_defconfig b/configs/omni3576-rk3576_defconfig index 13ef3112f1b..d1df3b8aca7 100644 --- a/configs/omni3576-rk3576_defconfig +++ b/configs/omni3576-rk3576_defconfig @@ -41,6 +41,9 @@ CONFIG_MMC_DW_ROCKCHIP=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_SDMA=y CONFIG_MMC_SDHCI_ROCKCHIP=y +CONFIG_PHY_REALTEK=y +CONFIG_DWC_ETH_QOS=y +CONFIG_DWC_ETH_QOS_ROCKCHIP=y CONFIG_NVME_PCI=y CONFIG_PCIE_DW_ROCKCHIP=y CONFIG_PHY_ROCKCHIP_INNO_USB2=y diff --git a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi index e74cefc0a5e..acfbb297851 100644 --- a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi +++ b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-core3576.dtsi @@ -662,12 +662,11 @@ &mdio0 { rgmii_phy0: ethernet-phy@0 { - compatible = "ethernet-phy-id001c.c916"; + compatible = "ethernet-phy-ieee802.3-c22"; reg = <0x0>; clocks = <&cru REFCLKO25M_GMAC0_OUT>; pinctrl-names = "default"; pinctrl-0 = <&gmac0_rst>; - realtek,aldps-enable; reset-assert-us = <20000>; reset-deassert-us = <100000>; reset-gpios = <&gpio2 RK_PB3 GPIO_ACTIVE_LOW>; @@ -685,7 +684,7 @@ &pinctrl { gmac { gmac0_rst: ethphy0-rst { - rockchip,pins = <2 RK_PB3 RK_FUNC_GPIO &pcfg_pull_none>; + rockchip,pins = <2 RK_PB3 RK_FUNC_GPIO &pcfg_pull_up>; }; }; diff --git a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts index c221fefedea..5ab0c7f3b54 100644 --- a/dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts +++ b/dts/upstream/src/arm64/rockchip/rk3576-luckfox-omni3576.dts @@ -47,12 +47,11 @@ &mdio1 { rgmii_phy1: ethernet-phy@0 { - compatible = "ethernet-phy-id001c.c916"; + compatible = "ethernet-phy-ieee802.3-c22"; reg = <0x0>; clocks = <&cru REFCLKO25M_GMAC1_OUT>; pinctrl-names = "default"; pinctrl-0 = <&gmac1_rst>; - realtek,aldps-enable; reset-assert-us = <20000>; reset-deassert-us = <100000>; reset-gpios = <&gpio2 RK_PB4 GPIO_ACTIVE_LOW>; @@ -62,7 +61,7 @@ &pinctrl { gmac { gmac1_rst: ethphy1-rst { - rockchip,pins = <2 RK_PB4 RK_FUNC_GPIO &pcfg_pull_none>; + rockchip,pins = <2 RK_PB4 RK_FUNC_GPIO &pcfg_pull_up>; }; }; From 04dfba4246f8c85ae3cdea6075592f86fc9930f3 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 31 Jul 2026 14:33:05 +0400 Subject: [PATCH 14/78] disk: part_efi: Size the partition entry array from the block size gpt_fill_header() computes last_usable_lba and first_usable_lba from hardcoded block counts that silently assumes 512-byte sectors and the default number of partition entries. The partition entry array holds GPT_ENTRY_NUMBERS entries of 128 bytes each, so the number of blocks it needs depends on both the entry count and the block size. write_gpt_table() derives that count correctly and writes the backup array at last_usable_lba + 1, so whenever the two disagree the backup array no longer immediately precedes the backup GPT header. Furthermore, on a device with 4096-byte native sectors, such as UFS flash, and the default 128 entries, the array is 4 blocks rather than 32. Current code reserves 34 blocks at each end of the disk for any block size and wastes about 114 KiB at each end. Add a new helper gpt_pte_blocks() and use it in all four places which currently calculate the number of blocks each in its own way, so the layout written by gpt_fill_header() and the extent written by write_gpt_table() cannot drift apart again. With this, first_usable_lba on 4096-byte sectors is 6, last_usable_lba is lba - 6, and the backup array occupies lba - 5 .. lba - 2, immediately preceding the backup header, as the UEFI specification describes. Note that this changes the 512-byte layout too for boards that do not use the default entry count. CONFIG_EFI_PARTITION_ENTRIES_NUMBERS is "default 56 if ARCH_SUNXI" and is set to 64 by a number of Rockchip defconfigs: entries array blocks first_usable_lba last_usable_lba 56 14 34 -> 16 lba - 34 -> lba - 16 64 16 34 -> 18 lba - 34 -> lba - 18 128 32 34 -> 34 lba - 34 -> lba - 34 Existing partition tables stay readable either way, since is_gpt_valid() locates the entry array from the on-disk partition_entry_lba. Only newly written tables change. Partitions given without an explicit start= will now be placed lower on those boards; on sunxi first_usable_lba lands on the 8 KiB SPL offset, so such boards should keep specifying start= explicitly. While here, report both LBAs when the requested layout does not fit, since the existing "Partitions layout exceeds disk size" debug message gives no clue as to by how much, making it less helpful in debugging. Co-developed-by: Anton Burticica Signed-off-by: Anton Burticica Signed-off-by: Alexey Charkov --- disk/part_efi.c | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/disk/part_efi.c b/disk/part_efi.c index d8b17ec2e91..af311867a28 100644 --- a/disk/part_efi.c +++ b/disk/part_efi.c @@ -388,10 +388,23 @@ static int set_protective_mbr(struct blk_desc *desc) return 0; } +/** + * gpt_pte_blocks() - number of blocks taken up by a partition entry array + * + * @desc: block device descriptor + * @gpt_h: GPT header describing the array + * Return: number of blocks the array occupies on @desc + */ +static u32 gpt_pte_blocks(struct blk_desc *desc, const gpt_header *gpt_h) +{ + return DIV_ROUND_UP(le32_to_cpu(gpt_h->num_partition_entries) * + le32_to_cpu(gpt_h->sizeof_partition_entry), + desc->blksz); +} + int write_gpt_table(struct blk_desc *desc, gpt_header *gpt_h, gpt_entry *gpt_e) { - const int pte_blk_cnt = BLOCK_CNT((gpt_h->num_partition_entries - * sizeof(gpt_entry)), desc); + const int pte_blk_cnt = gpt_pte_blocks(desc, gpt_h); u32 calc_crc32; debug("max lba: %x\n", (u32)desc->lba); @@ -453,9 +466,7 @@ int gpt_fill_pte(struct blk_desc *desc, size_t hdr_end = hdr_start + 1; size_t pte_start = gpt_h->partition_entry_lba; - size_t pte_end = pte_start + - gpt_h->num_partition_entries * gpt_h->sizeof_partition_entry / - desc->blksz; + size_t pte_end = pte_start + gpt_pte_blocks(desc, gpt_h); for (i = 0; i < parts; i++) { /* partition starting lba */ @@ -482,7 +493,9 @@ int gpt_fill_pte(struct blk_desc *desc, gpt_e[i].starting_lba = cpu_to_le64(start); if (offset > (last_usable_lba + 1)) { - log_debug("Partitions layout exceeds disk size\n"); + log_debug("Partitions layout exceeds disk size: " + LBAFU " > " LBAFU "\n", + offset, last_usable_lba + 1); return -E2BIG; } /* partition ending lba */ @@ -604,18 +617,29 @@ static uint32_t partition_entries_offset(struct blk_desc *desc) int gpt_fill_header(struct blk_desc *desc, gpt_header *gpt_h, char *str_guid, int parts_count) { + u32 pte_sectors; + gpt_h->signature = cpu_to_le64(GPT_HEADER_SIGNATURE_UBOOT); gpt_h->revision = cpu_to_le32(GPT_HEADER_REVISION_V1); gpt_h->header_size = cpu_to_le32(sizeof(gpt_header)); + gpt_h->num_partition_entries = cpu_to_le32(GPT_ENTRY_NUMBERS); + gpt_h->sizeof_partition_entry = cpu_to_le32(sizeof(gpt_entry)); + + /* + * Number of blocks occupied by the partition entry array. For the + * default of 128 entries that is 32 blocks on 512-byte sectors, but + * only 4 blocks on 4096-byte sectors. + */ + pte_sectors = gpt_pte_blocks(desc, gpt_h); + gpt_h->my_lba = cpu_to_le64(1); gpt_h->alternate_lba = cpu_to_le64(desc->lba - 1); - gpt_h->last_usable_lba = cpu_to_le64(desc->lba - 34); + /* Reserve space for backup GPT header (1) + backup partition entries */ + gpt_h->last_usable_lba = cpu_to_le64(desc->lba - pte_sectors - 2); gpt_h->partition_entry_lba = cpu_to_le64(partition_entries_offset(desc)); gpt_h->first_usable_lba = - cpu_to_le64(le64_to_cpu(gpt_h->partition_entry_lba) + 32); - gpt_h->num_partition_entries = cpu_to_le32(GPT_ENTRY_NUMBERS); - gpt_h->sizeof_partition_entry = cpu_to_le32(sizeof(gpt_entry)); + cpu_to_le64(le64_to_cpu(gpt_h->partition_entry_lba) + pte_sectors); gpt_h->header_crc32 = 0; gpt_h->partition_entry_array_crc32 = 0; @@ -747,8 +771,7 @@ static void restore_primary_gpt_header(gpt_header *gpt_h, struct blk_desc *desc) static int write_one_gpt_table(struct blk_desc *desc, gpt_header *gpt_h, gpt_entry *gpt_e) { - const int pte_blk_cnt = BLOCK_CNT((gpt_h->num_partition_entries - * sizeof(gpt_entry)), desc); + const int pte_blk_cnt = gpt_pte_blocks(desc, gpt_h); lbaint_t start; int ret = 0; From 850f94fc78b715489541586e05c2b8afaf06205b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 31 Jul 2026 14:34:16 +0400 Subject: [PATCH 15/78] disk: part_efi: Add missing endianness conversions Several GPT header fields are read without converting from little-endian, which is wrong on big-endian hosts. gpt_fill_pte() takes my_lba and partition_entry_lba raw when working out the region a partition must not overlap, so on a big-endian host both bounds are byte-swapped garbage and the overlap check does not do anything useful. gpt_verify_partitions() compares the loop counter against num_partition_entries raw, so its "More partitions than allowed!" guard never triggers. It also swaps gpt_part_size, which is already in host order, having been computed from two le64_to_cpu() results a few lines above. Drop the conversion rather than adding one. All of this is a no-op on little-endian targets. Signed-off-by: Alexey Charkov --- disk/part_efi.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/disk/part_efi.c b/disk/part_efi.c index af311867a28..062389c3af0 100644 --- a/disk/part_efi.c +++ b/disk/part_efi.c @@ -462,10 +462,10 @@ int gpt_fill_pte(struct blk_desc *desc, char *str_type_guid; unsigned char *bin_type_guid; #endif - size_t hdr_start = gpt_h->my_lba; + size_t hdr_start = le64_to_cpu(gpt_h->my_lba); size_t hdr_end = hdr_start + 1; - size_t pte_start = gpt_h->partition_entry_lba; + size_t pte_start = le64_to_cpu(gpt_h->partition_entry_lba); size_t pte_end = pte_start + gpt_pte_blocks(desc, gpt_h); for (i = 0; i < parts; i++) { @@ -851,7 +851,7 @@ int gpt_verify_partitions(struct blk_desc *desc, gpt_e = *gpt_pte; for (i = 0; i < parts; i++) { - if (i == gpt_head->num_partition_entries) { + if (i == le32_to_cpu(gpt_head->num_partition_entries)) { pr_err("More partitions than allowed!\n"); return -1; } @@ -877,7 +877,7 @@ int gpt_verify_partitions(struct blk_desc *desc, (unsigned long long)gpt_part_size, (unsigned long long)partitions[i].size); - if (le64_to_cpu(gpt_part_size) != partitions[i].size) { + if (gpt_part_size != partitions[i].size) { /* We do not check the extend partition size */ if ((i == parts - 1) && (partitions[i].size == 0)) continue; From b0387d888ee475489374e5ab196ea93e2957ad79 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 31 Jul 2026 12:42:02 +0400 Subject: [PATCH 16/78] disk: part_mac: Fix stack corruption on devices with large blocks part_test_mac(), part_print_mac() and part_get_info_mac() each declare a single-block buffer sized after the descriptor struct: ALLOC_CACHE_ALIGN_BUFFER(mac_driver_desc_t, ddesc, 1); ALLOC_CACHE_ALIGN_BUFFER(mac_partition_t, mpart, 1); Both structs are 512 bytes, but every blk_dread() below them asks for one block, which transfers desc->blksz bytes. On a device with 4096-byte logical blocks that writes 4096 bytes into a 512-byte on-stack buffer and corrupts the stack. part_test_mac() runs on every block device during partition probing, so on sandbox with CONFIG_MAC_PARTITION=y this crashes on any access at all to a device with large blocks, for instance: host bind 0 disk.img 4096 part list host 0 Pad the buffers out to the block size with ALLOC_CACHE_ALIGN_BUFFER_PAD(), which is what part_efi.c already does for its own block buffers. Signed-off-by: Alexey Charkov --- disk/part_mac.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/disk/part_mac.c b/disk/part_mac.c index dd3ce0be832..f0afab65d75 100644 --- a/disk/part_mac.c +++ b/disk/part_mac.c @@ -37,8 +37,8 @@ static int part_mac_read_pdb(struct blk_desc *desc, int part, */ static int part_test_mac(struct blk_desc *desc) { - ALLOC_CACHE_ALIGN_BUFFER(mac_driver_desc_t, ddesc, 1); - ALLOC_CACHE_ALIGN_BUFFER(mac_partition_t, mpart, 1); + ALLOC_CACHE_ALIGN_BUFFER_PAD(mac_driver_desc_t, ddesc, 1, desc->blksz); + ALLOC_CACHE_ALIGN_BUFFER_PAD(mac_partition_t, mpart, 1, desc->blksz); ulong i, n; if (part_mac_read_ddb(desc, ddesc)) { @@ -64,8 +64,8 @@ static int part_test_mac(struct blk_desc *desc) static void part_print_mac(struct blk_desc *desc) { ulong i, n; - ALLOC_CACHE_ALIGN_BUFFER(mac_driver_desc_t, ddesc, 1); - ALLOC_CACHE_ALIGN_BUFFER(mac_partition_t, mpart, 1); + ALLOC_CACHE_ALIGN_BUFFER_PAD(mac_driver_desc_t, ddesc, 1, desc->blksz); + ALLOC_CACHE_ALIGN_BUFFER_PAD(mac_partition_t, mpart, 1, desc->blksz); ldiv_t mb, gb; if (part_mac_read_ddb(desc, ddesc)) { @@ -208,8 +208,8 @@ static int part_mac_read_pdb(struct blk_desc *desc, int part, static int part_get_info_mac(struct blk_desc *desc, int part, struct disk_partition *info) { - ALLOC_CACHE_ALIGN_BUFFER(mac_driver_desc_t, ddesc, 1); - ALLOC_CACHE_ALIGN_BUFFER(mac_partition_t, mpart, 1); + ALLOC_CACHE_ALIGN_BUFFER_PAD(mac_driver_desc_t, ddesc, 1, desc->blksz); + ALLOC_CACHE_ALIGN_BUFFER_PAD(mac_partition_t, mpart, 1, desc->blksz); if (part_mac_read_ddb(desc, ddesc)) return -1; From ba0738388271a837ba32580da1bea309ec67988f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 31 Jul 2026 12:42:02 +0400 Subject: [PATCH 17/78] test: dm: part: Check the GPT layout for large sector sizes Add a test that runs gpt_fill_header() and gpt_fill_pte() over a range of block sizes and checks that the partition entry array is sized and placed consistently, in particular that the backup array ends exactly where the backup GPT header begins. Neither function performs any block I/O, so the test builds a synthetic struct blk_desc rather than needing a block device with a configurable block size. The expected array size is taken from the GPT header fields rather than from GPT_ENTRY_NUMBERS, so the test still holds for builds with a non-default CONFIG_EFI_PARTITION_ENTRIES_NUMBERS, such as ARCH_SUNXI with 56 entries. Without the preceding fixes this fails on the first block size other than 512: test/dm/part.c:313, dm_test_part_gpt_blksz(): entry_lba + pte_blks == first_lba: Expected 0x12 (18), got 0x22 (34) Signed-off-by: Alexey Charkov --- test/dm/part.c | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/test/dm/part.c b/test/dm/part.c index ad37d7f406f..4b48322fb80 100644 --- a/test/dm/part.c +++ b/test/dm/part.c @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -248,3 +250,101 @@ static int dm_test_part_get_info_by_uuid(struct unit_test_state *uts) return 0; } DM_TEST(dm_test_part_get_info_by_uuid, UTF_SCAN_PDATA | UTF_SCAN_FDT); + +/* + * Check that the GPT layout adapts to the block size of the device. Neither + * gpt_fill_header() nor gpt_fill_pte() does any block I/O, so a synthetic + * descriptor is enough here; partition_entries_offset() reads the device tree + * '/config' node, which is available without scanning for devices. + */ +static int dm_test_part_gpt_blksz(struct unit_test_state *uts) +{ + static const struct { + unsigned long blksz; + lbaint_t lba; + } cases[] = { + { 512, 0x2000 }, /* 4MB, the traditional 34-block layout */ + { 1024, 0x1000 }, + { 2048, 0x800 }, + { 4096, 0x400 }, /* 4K native media, e.g. UFS */ + { 8192, 0x200 }, + { 16384, 0x100 }, /* the array fits in a single block... */ + { 32768, 0x40 }, /* ...on a device smaller than 34 blocks */ + }; + + char str_disk_guid[UUID_STR_LEN + 1] = + "8d60b397-1bb6-4d33-80ee-b1587d24c2f8"; + struct disk_partition part; + struct blk_desc desc; + gpt_header gpt_h; + gpt_entry *gpt_e; + int i; + + gpt_e = calloc(GPT_ENTRY_NUMBERS, sizeof(gpt_entry)); + ut_assertnonnull(gpt_e); + + for (i = 0; i < ARRAY_SIZE(cases); i++) { + u64 entry_lba, first_lba, last_lba; + u32 pte_blks; + + memset(&desc, '\0', sizeof(desc)); + memset(&gpt_h, '\0', sizeof(gpt_h)); + desc.blksz = cases[i].blksz; + desc.log2blksz = LOG2(desc.blksz); + desc.lba = cases[i].lba; + + ut_assertok(gpt_fill_header(&desc, &gpt_h, str_disk_guid, 1)); + + /* + * Size the array with BLOCK_CNT() rather than the + * DIV_ROUND_UP() that gpt_pte_blocks() uses, so that this + * checks the layout instead of restating the implementation. + */ + pte_blks = BLOCK_CNT(le32_to_cpu(gpt_h.num_partition_entries) * + le32_to_cpu(gpt_h.sizeof_partition_entry), + (&desc)); + entry_lba = le64_to_cpu(gpt_h.partition_entry_lba); + first_lba = le64_to_cpu(gpt_h.first_usable_lba); + last_lba = le64_to_cpu(gpt_h.last_usable_lba); + + ut_asserteq_64(1, le64_to_cpu(gpt_h.my_lba)); + ut_asserteq_64(desc.lba - 1, le64_to_cpu(gpt_h.alternate_lba)); + + /* the primary array ends where the usable area begins */ + ut_assert(entry_lba >= 2); + ut_asserteq_64(entry_lba + pte_blks, first_lba); + ut_assert(first_lba < last_lba); + + /* + * write_gpt_table() puts the backup array at + * last_usable_lba + 1, so it has to end exactly where the + * backup header begins + */ + ut_asserteq_64(le64_to_cpu(gpt_h.alternate_lba), + last_lba + 1 + pte_blks); + + /* a partition with no start and no size fills the disk */ + memset(&part, '\0', sizeof(part)); + disk_partition_set_uuid(&part, str_disk_guid); + ut_assertok(gpt_fill_pte(&desc, &gpt_h, gpt_e, &part, 1)); + ut_asserteq_64(first_lba, le64_to_cpu(gpt_e[0].starting_lba)); + ut_asserteq_64(last_lba, le64_to_cpu(gpt_e[0].ending_lba)); + + /* a partition overlapping the entry array is rejected */ + part.start = entry_lba; + part.size = 1; + ut_asserteq(-ENOSPC, + gpt_fill_pte(&desc, &gpt_h, gpt_e, &part, 1)); + + /* so is one running past the end of the usable area */ + part.start = first_lba; + part.size = last_lba - first_lba + 2; + ut_asserteq(-E2BIG, + gpt_fill_pte(&desc, &gpt_h, gpt_e, &part, 1)); + } + + free(gpt_e); + + return 0; +} +DM_TEST(dm_test_part_gpt_blksz, 0); From 6a8d38d1ff2140b6146a0ed0c5db1d7810d436ea Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 31 Jul 2026 13:33:32 +0400 Subject: [PATCH 18/78] test: py: gpt: Bind the disk image in test_gpt_write_part_type() Unlike every other test in this file, test_gpt_write_part_type() never binds the disk image it operates on. It has worked since it was added only because some test that ran before it left host 0 bound to the image this fixture provides. That breaks as soon as a test which binds something else is added above it, and it means the test silently depends on collection order rather than on its own fixture. Bind the image like the other tests do. Fixes: 7a598e633a53 ("test/py: tests: gpt: add test_gpt_write_part_type") Signed-off-by: Alexey Charkov --- test/py/tests/test_gpt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/py/tests/test_gpt.py b/test/py/tests/test_gpt.py index e6d8792ac1f..fb4c8ba67d4 100644 --- a/test/py/tests/test_gpt.py +++ b/test/py/tests/test_gpt.py @@ -338,6 +338,7 @@ def test_gpt_write(state_disk_image, ubman): def test_gpt_write_part_type(state_disk_image, ubman): """Test the gpt command with part type uuid.""" + ubman.run_command('host bind 0 ' + state_disk_image.path) output = ubman.run_command('gpt write host 0 "name=part1,type=data,size=1M;name=part2,size=512K,type=system;name=part3,size=65536,type=u-boot-env;name=part4,size=65536,type=375a56f7-d6c9-4e81-b5f0-09d41ca89efe;name=part5,size=-,type=linux"') assert 'Writing GPT: success!' in output output = ubman.run_command('part list host 0') From 81ae9b24f3402f4e5ff6af04096c071b44bab774 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 31 Jul 2026 13:33:32 +0400 Subject: [PATCH 19/78] test: py: gpt: Test the GPT layout on a device with 4096-byte blocks Bind a blank image with a 4096-byte logical block size, let U-Boot write a GPT to it, and then check the result both through 'part list' and by unpacking the primary and backup headers straight out of the image. The interesting assertion is that the backup partition entry array ends immediately before the backup header in the last block, which is what regresses when the array size is computed with a hardcoded block count. No sgdisk is needed since U-Boot creates the table itself, so unlike the other tests here this one has no requiredtool marker. The number of entries comes from the build config rather than being hardcoded to 128, so the expected LBAs are recomputed for builds that set CONFIG_EFI_PARTITION_ENTRIES_NUMBERS differently. Signed-off-by: Alexey Charkov --- test/py/tests/test_gpt.py | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/test/py/tests/test_gpt.py b/test/py/tests/test_gpt.py index fb4c8ba67d4..823fa40a929 100644 --- a/test/py/tests/test_gpt.py +++ b/test/py/tests/test_gpt.py @@ -6,6 +6,7 @@ import os import pytest +import struct import utils """ @@ -45,6 +46,28 @@ def parse_gpt_parts(disk_str): return parts +# Layout of gpt_header from include/part_efi.h. It is __packed and +# little-endian, and struct.calcsize() of this is 92, its header_size. +GPT_HEADER_FMT = '<8sIIIIQQQQ16sQIII' +GPT_HEADER_FIELDS = ( + 'signature', 'revision', 'header_size', 'header_crc32', 'reserved1', + 'my_lba', 'alternate_lba', 'first_usable_lba', 'last_usable_lba', + 'disk_guid', 'partition_entry_lba', 'num_partition_entries', + 'sizeof_partition_entry', 'partition_entry_array_crc32') + +def parse_gpt_header(blob): + """Unpack a raw GPT header. + + Args: + blob: Bytes starting at the first byte of the header + + Returns: + A dict mapping each gpt_header field name to its value + """ + size = struct.calcsize(GPT_HEADER_FMT) + return dict(zip(GPT_HEADER_FIELDS, + struct.unpack(GPT_HEADER_FMT, blob[:size]))) + class GptTestDiskImage(object): """Disk Image used by the GPT tests.""" @@ -93,6 +116,36 @@ def __init__(self, ubman): cmd = ('cp', persistent, self.path) utils.run_and_log(ubman, cmd) +class BlankDiskImage(object): + """Zero-filled disk image with no partition table on it.""" + + def __init__(self, ubman, size=4 * 1024 * 1024): + """Initialize a new BlankDiskImage object. + + Args: + ubman: A U-Boot console. + size: Image size in bytes. Must be a multiple of the block size + the image is later bound with. + + Returns: + Nothing. + """ + + self.size = size + self.path = os.path.join(ubman.config.result_dir, + 'test_gpt_blank_disk_image.bin') + ubman.log.action('Generating ' + self.path) + with open(self.path, 'wb') as fd: + fd.truncate(size) + +@pytest.fixture(scope='function') +def blank_disk_image(ubman): + """pytest fixture to provide an empty BlankDiskImage object to tests. + This is function-scoped because it uses ubman, which is also + function-scoped.""" + + return BlankDiskImage(ubman) + @pytest.fixture(scope='function') def state_disk_image(ubman): """pytest fixture to provide a GptTestDiskImage object to tests. @@ -330,6 +383,68 @@ def test_gpt_write(state_disk_image, ubman): output = ubman.run_command('gpt guid host 0') assert '375a56f7-d6c9-4e81-b5f0-09d41ca89efe' in output +@pytest.mark.boardspec('sandbox') +@pytest.mark.buildconfigspec('cmd_gpt') +@pytest.mark.buildconfigspec('cmd_part') +def test_gpt_write_4k_sectors(blank_disk_image, ubman): + """Test the GPT layout written to a device with 4096-byte sectors. + + The partition entry array takes up fewer blocks on a device with large + sectors, and the backup array has to end immediately before the backup + header in the last block whatever the block size is. + """ + + blksz = 4096 + entry_sz = 128 + nblocks = blank_disk_image.size // blksz + entries = int(ubman.config.buildconfig.get( + 'config_efi_partition_entries_numbers', 128)) + pte_blocks = -(-entries * entry_sz // blksz) + + first_usable = 2 + pte_blocks + last_usable = nblocks - pte_blocks - 2 + + ubman.run_command(f'host bind 0 {blank_disk_image.path} {blksz}') + + output = ubman.run_command('gpt write host 0 "name=all,size=0"') + assert 'Writing GPT: success!' in output + output = ubman.run_command('gpt verify host 0') + assert 'Verify GPT: success!' in output + + output = ubman.run_command('part list host 0') + assert f'0x{first_usable:08x}\t0x{last_usable:08x}\t"all"' in output + + with open(blank_disk_image.path, 'rb') as fd: + data = fd.read() + + # The primary header sits in block 1 and describes the whole layout + prim = parse_gpt_header(data[blksz:]) + assert prim['signature'] == b'EFI PART' + assert prim['my_lba'] == 1 + assert prim['alternate_lba'] == nblocks - 1 + assert prim['partition_entry_lba'] == 2 + assert prim['first_usable_lba'] == first_usable + assert prim['last_usable_lba'] == last_usable + assert prim['num_partition_entries'] == entries + assert prim['sizeof_partition_entry'] == entry_sz + + # The backup header sits in the very last block... + bkp = parse_gpt_header(data[(nblocks - 1) * blksz:]) + assert bkp['signature'] == b'EFI PART' + assert bkp['my_lba'] == nblocks - 1 + assert bkp['alternate_lba'] == 1 + assert bkp['first_usable_lba'] == first_usable + assert bkp['last_usable_lba'] == last_usable + + # ...and its entry array must end immediately before it + assert bkp['partition_entry_lba'] == last_usable + 1 + assert bkp['partition_entry_lba'] + pte_blocks == nblocks - 1 + + # The backup array holds the same entries as the primary one + pte_len = entries * entry_sz + bkp_off = bkp['partition_entry_lba'] * blksz + assert data[bkp_off:bkp_off + pte_len] == data[2 * blksz:2 * blksz + pte_len] + @pytest.mark.boardspec('sandbox') @pytest.mark.buildconfigspec('cmd_gpt') @pytest.mark.buildconfigspec('cmd_part') From 8e7e71ecdaea0013f0cb8d83a0ae94c7120819e9 Mon Sep 17 00:00:00 2001 From: Anton Burticica Date: Tue, 20 Jan 2026 09:54:09 +0200 Subject: [PATCH 20/78] fastboot: Align protocol buffer sizes with Android fastboot client Update FASTBOOT_COMMAND_LEN and FASTBOOT_RESPONSE_LEN to match the buffer sizes defined in the Android fastboot client: https://android.googlesource.com/platform/system/core/+/refs/heads/main/fastboot/constants.h #define FB_COMMAND_SZ 4096 #define FB_RESPONSE_SZ 256 The original 64-byte command limit dates back to the early fastboot protocol specification. Modern Android fastboot clients support commands up to 4096 bytes, enabling: - Longer partition names in flash/erase commands - Extended getvar queries with complex arguments - OEM commands with substantial payloads Change-Id: 1aaaa615-0811-4f81-949c-1ae4b9935fb0 Signed-off-by: Anton Burticica --- include/fastboot.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/include/fastboot.h b/include/fastboot.h index b106d617749..48682d2e22d 100644 --- a/include/fastboot.h +++ b/include/fastboot.h @@ -26,9 +26,15 @@ */ #define FASTBOOT_MULTIRESPONSE_START ("MORE") -/* The 64 defined bytes plus \0 */ -#define FASTBOOT_COMMAND_LEN (64 + 1) -#define FASTBOOT_RESPONSE_LEN (64 + 1) +/* + * Fastboot protocol buffer sizes aligned with Android fastboot client: + * https://android.googlesource.com/platform/system/core/+/refs/heads/main/fastboot/constants.h + * + * FB_COMMAND_SZ 4096 - max command host sends + * FB_RESPONSE_SZ 256 - max response host expects + */ +#define FASTBOOT_COMMAND_LEN (4096 + 1) +#define FASTBOOT_RESPONSE_LEN (256 + 1) /** * All known commands to fastboot From 103103610fb9ff69dbf028571746590cf45346a1 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 6 May 2026 22:36:34 +0400 Subject: [PATCH 21/78] vidconsole: add word-wrap fallback in vidconsole_measure() for bitmap consoles The bitmap console driver does not implement ops->measure, so vidconsole_measure() fell back to returning a single-line bounding box (y1 = y_charsize) with x1 = x_charsize * strlen(text), regardless of any pixel-width limit passed by the caller. This caused Expo scene objects using CENTRE or RIGHT alignment to compute incorrect x offsets for long strings. For example, the ~68- character help prompt on a 220 px wide scene produced: x1 = 6 * 68 = 408 px, xofs = (220 - 408) / 2 = -94 px pushing the text ~94 pixels off the left edge of the display. Add a word-wrapping fallback that is taken when a pixel-width limit is provided and the caller requests line records (lines != NULL). The fallback iterates character-by-character, breaks at the last space before an overflow, and populates the vidconsole_mline array and the overall bbox.y1 in the same way a truetype driver would, so that callers that lay out text using the returned metrics (e.g. scene_render_txt) produce correctly wrapped, on-screen output. --- drivers/video/vidconsole-uclass.c | 77 ++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/drivers/video/vidconsole-uclass.c b/drivers/video/vidconsole-uclass.c index f1b2d61bd8f..327f94a90d9 100644 --- a/drivers/video/vidconsole-uclass.c +++ b/drivers/video/vidconsole-uclass.c @@ -642,8 +642,81 @@ int vidconsole_measure(struct udevice *dev, const char *name, uint size, bbox->valid = true; bbox->x0 = 0; bbox->y0 = 0; - bbox->x1 = priv->x_charsize * strlen(text); - bbox->y1 = priv->y_charsize; + + /* + * Fallback for bitmap consoles that do not implement ops->measure. + * When a pixel limit is provided and the caller wants line records, + * do a simple word-wrapping pass so that multi-line text objects + * (e.g. the Expo help prompts) render correctly. + */ + if (limit > 0 && lines) { + const int cw = priv->x_charsize; + const int ch = priv->y_charsize; + const char *s = text; + const char *line_start = text; + const char *last_space = NULL; + int x = 0, last_space_x = 0, max_x = 0; + + alist_empty(lines); + bbox->y1 = 0; + + for (;;) { + bool at_end = !*s; + bool overflow = !at_end && (x + cw > limit); + + if (*s == ' ') { + last_space = s; + last_space_x = x; + } + + if (at_end || overflow) { + struct vidconsole_mline mline; + const char *wrap_at; + int line_w; + + if (overflow && last_space && + last_space > line_start) { + /* word-wrap at last space */ + wrap_at = last_space; + line_w = last_space_x; + } else { + wrap_at = s; + line_w = x; + } + + mline.start = line_start - text; + mline.len = wrap_at - line_start; + mline.bbox.x0 = 0; + mline.bbox.y0 = bbox->y1; + mline.bbox.x1 = line_w; + mline.bbox.y1 = bbox->y1 + ch; + mline.bbox.valid = true; + if (!alist_add(lines, mline)) + return -ENOMEM; + + max_x = max(max_x, line_w); + bbox->y1 += ch; + + if (at_end) + break; + + /* resume after the wrapped space */ + line_start = wrap_at + 1; + last_space = NULL; + x = 0; + s = line_start; + continue; + } + + x += cw; + s++; + } + + bbox->x1 = max_x; + } else { + bbox->x1 = priv->x_charsize * strlen(text); + bbox->y1 = priv->y_charsize; + } return 0; } From 709c4943dd5b647eac93a041d04a7186aa12698b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 22 Apr 2026 14:21:32 +0400 Subject: [PATCH 22/78] video: Add support for Flipper One display Add a simple video uclass driver for the Flipper One's 256x144 8-bit grayscale SPI display. The display accepts a full frame in the BPP8 format (8-bit grayscale) in a single SPI write transaction, padded to a line stride of 258 bytes (256 pixels + 2 footer bytes per row). No other configuration is required as long as SPI mode 3 (CPOL=1, CPHA=1) is used and the SPI clock is up to 24 MHz. Signed-off-by: Alexey Charkov --- drivers/video/Kconfig | 10 ++ drivers/video/Makefile | 1 + drivers/video/flipper-one-display.c | 151 ++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 drivers/video/flipper-one-display.c diff --git a/drivers/video/Kconfig b/drivers/video/Kconfig index 15000e21840..7dc148d743f 100644 --- a/drivers/video/Kconfig +++ b/drivers/video/Kconfig @@ -367,6 +367,16 @@ config VIDEO_EFI the graphics device is configured by the EFI BIOS already. This can in principle be used with any platform that has an EFI BIOS. +config VIDEO_FLIPPER_ONE + bool "Enable video support for Flipper One display" + depends on DM_SPI && DM_GPIO && VIDEO_BPP8 + help + Enable support for the Flipper One 256x144 8-bit grayscale SPI display. + The display uses SPI mode 3 (CPOL=1, CPHA=1) at up to 24 MHz and expects + a single continuous SPI write transaction per frame. Each frame consists + of 144 rows of 258 bytes: 256 grayscale pixels followed by 2 footer bytes + which are ignored by the display controller. + config VIDEO_VESA bool "Enable VESA video driver support" help diff --git a/drivers/video/Makefile b/drivers/video/Makefile index 082b8967982..2ad6063b96d 100644 --- a/drivers/video/Makefile +++ b/drivers/video/Makefile @@ -54,6 +54,7 @@ obj-$(CONFIG_VIDEO_COREBOOT) += coreboot.o obj-$(CONFIG_VIDEO_DW_HDMI) += dw_hdmi.o obj-$(CONFIG_VIDEO_DW_MIPI_DSI) += dw_mipi_dsi.o obj-$(CONFIG_VIDEO_EFI) += efi.o +obj-$(CONFIG_VIDEO_FLIPPER_ONE) += flipper-one-display.o obj-y += imx/ obj-$(CONFIG_VIDEO_IVYBRIDGE_IGD) += ivybridge_igd.o obj-$(CONFIG_VIDEO_LCD_PANEL_EDP_4_LANE_1620M_VIA_ANX9804) += anx9804.o diff --git a/drivers/video/flipper-one-display.c b/drivers/video/flipper-one-display.c new file mode 100644 index 00000000000..bbd6c976f87 --- /dev/null +++ b/drivers/video/flipper-one-display.c @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Video driver for the Flipper One 256x144 grayscale SPI display. + * + * The display accepts one SPI write transaction per frame. Each row + * consists of 256 bytes of 8-bit grayscale pixel data (left justified) + * followed by 2 footer bytes, giving a frame size of 258 * 144 = 37152 + * bytes, VIDEO_BPP8 format (8-bit grayscale) + * + * SPI wiring: mode 3 (CPOL=1, CPHA=1), up to 24 MHz, no MISO. + * + * Copyright (c) 2026 Flipper FZCO + */ + +#include +#include +#include +#include +#include +#include + +/* Physical display dimensions */ +#define DISPLAY_WIDTH 256 +#define DISPLAY_HEIGHT 144 + +/* + * Wire frame layout: 256 pixels + 2 footer bytes per row. + * The footer bytes are ignored by the display. + */ +#define ROW_BYTES 258 +#define FRAME_BYTES (ROW_BYTES * DISPLAY_HEIGHT) + +/** + * struct flipper_one_priv - Driver private data + * @last_spi_sync_ms: Absolute timestamp (ms) of the most recent completed SPI + * frame transfer, used to enforce a per-driver rate limit. + * The video uclass rate-limiting (VIDEO_SYNC_MS) runs + * *after* calling ops->video_sync, so without this guard + * every vidconsole character write and every cyclic tick + * would trigger a full 37 KiB SPI transfer. + * @active_gpio: Optional GPIO asserted (active-low: driven low) for the + * duration of each SPI frame transfer to signal the display MCU + * that a new frame is incoming. + */ +struct flipper_one_priv { + ulong last_spi_sync_ms; + struct gpio_desc active_gpio; +}; + +/** + * flipper_one_sync() - Push the internal framebuffer to the display + */ +static int flipper_one_sync(struct udevice *vid) +{ + struct flipper_one_priv *priv = dev_get_priv(vid); + struct video_priv *uc_priv = dev_get_uclass_priv(vid); + int ret; + + /* + * The video uclass calls ops->video_sync() unconditionally — the + * VIDEO_SYNC_MS rate-limit check only runs *after* this callback + * returns. Guard the expensive SPI transfer here so it fires at most + * once per VIDEO_SYNC_MS milliseconds regardless of how often + * vidconsole_putc() or the cyclic mechanism calls video_sync(). + */ + if (get_timer(priv->last_spi_sync_ms) < CONFIG_VIDEO_SYNC_MS) + return 0; + + ret = dm_spi_claim_bus(vid); + if (ret) { + dev_err(vid, "failed to claim SPI bus: %d\n", ret); + return ret; + } + + dm_gpio_set_value(&priv->active_gpio, 1); + + /* + * Send the entire frame in one transaction so that chip-select + * remains active throughout (SPI_XFER_BEGIN asserts CS before the + * first bit; SPI_XFER_END deasserts it after the last bit). + */ + ret = dm_spi_xfer(vid, FRAME_BYTES * 8, uc_priv->fb, NULL, + SPI_XFER_BEGIN | SPI_XFER_END); + if (ret) + dev_err(vid, "failed to send frame: %d\n", ret); + + dm_gpio_set_value(&priv->active_gpio, 0); + + dm_spi_release_bus(vid); + + priv->last_spi_sync_ms = get_timer(0); + + return ret; +} + +static int flipper_one_probe(struct udevice *dev) +{ + struct flipper_one_priv *priv = dev_get_priv(dev); + struct video_priv *uc_priv = dev_get_uclass_priv(dev); + int ret; + + uc_priv->xsize = DISPLAY_WIDTH; + uc_priv->ysize = DISPLAY_HEIGHT; + uc_priv->bpix = VIDEO_BPP8; + uc_priv->rot = 0; + uc_priv->line_length = ROW_BYTES; + + /* + * The active-gpios line is asserted (logical 1 = GPIO driven to its + * active state, which is physically low for GPIO_ACTIVE_LOW) for the + * duration of each SPI frame transfer. The display MCU uses this + * signal to detect frame boundaries. The GPIO is optional in + * device-tree but required for the display to accept frames. + */ + ret = gpio_request_by_name(dev, "active-gpios", 0, &priv->active_gpio, + GPIOD_IS_OUT); + if (ret && ret != -ENOENT) { + dev_err(dev, "failed to request active-gpios: %d\n", ret); + return ret; + } + + return 0; +} + +static int flipper_one_bind(struct udevice *dev) +{ + struct video_uc_plat *plat = dev_get_uclass_plat(dev); + plat->size = FRAME_BYTES; + + return 0; +} + +static const struct video_ops flipper_one_ops = { + .video_sync = flipper_one_sync, +}; + +static const struct udevice_id flipper_one_ids[] = { + { .compatible = "flipper,one-display" }, + { } +}; + +U_BOOT_DRIVER(flipper_one_display) = { + .name = "flipper_one_display", + .id = UCLASS_VIDEO, + .of_match = flipper_one_ids, + .ops = &flipper_one_ops, + .bind = flipper_one_bind, + .probe = flipper_one_probe, + .plat_auto = sizeof(struct video_uc_plat), + .priv_auto = sizeof(struct flipper_one_priv), +}; From 4a070002b6dbc55354ea138d1fa0827fd5fa5839 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 6 May 2026 16:50:16 +0400 Subject: [PATCH 23/78] input: Add support for Flipper One buttons via onboard MCU Flipper One has an onboard MCU which exposes its front panel buttons over the I2C interconnect bus. Add a small keyboard driver to use these buttons for menu navigation. Signed-off-by: Alexey Charkov --- drivers/input/Kconfig | 8 ++ drivers/input/Makefile | 1 + drivers/input/flipper_one_btns.c | 181 +++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 drivers/input/flipper_one_btns.c diff --git a/drivers/input/Kconfig b/drivers/input/Kconfig index df8fbf1551d..2bfcde35b23 100644 --- a/drivers/input/Kconfig +++ b/drivers/input/Kconfig @@ -46,6 +46,14 @@ config APPLE_SPI_KEYB laptops based on Apple SoCs. These keyboards use an Apple-specific HID-over-SPI protocol. +config FLIPPER_ONE_BTNS + bool "Enable Flipper One MCU buttons as keyboard" + depends on DM_KEYBOARD && DM_I2C + help + Enable support for the physical buttons on the Flipper One device, + accessed via the onboard MCU over I2C. Exposes the D-pad, center, + back and app switch buttons as keyboard input for menu navigation. + config BUTTON_KEYBOARD bool "Buttons as keyboard" depends on DM_KEYBOARD && DM_GPIO diff --git a/drivers/input/Makefile b/drivers/input/Makefile index 1303fcdb0b7..10e61def69f 100644 --- a/drivers/input/Makefile +++ b/drivers/input/Makefile @@ -6,6 +6,7 @@ obj-$(CONFIG_$(PHASE_)CROS_EC_KEYB) += cros_ec_keyb.o obj-$(CONFIG_$(PHASE_)OF_CONTROL) += key_matrix.o obj-$(CONFIG_$(PHASE_)DM_KEYBOARD) += input.o keyboard-uclass.o +obj-$(CONFIG_FLIPPER_ONE_BTNS) += flipper_one_btns.o obj-$(CONFIG_BUTTON_KEYBOARD) += button_kbd.o obj-$(CONFIG_CPCAP_POWER_BUTTON) += cpcap_pwrbutton.o diff --git a/drivers/input/flipper_one_btns.c b/drivers/input/flipper_one_btns.c new file mode 100644 index 00000000000..eebc2d3ffdd --- /dev/null +++ b/drivers/input/flipper_one_btns.c @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Flipper One MCU buttons keyboard driver + * Copyright (C) 2026 Flipper FZCO + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * MCU register addresses are 16-bit, sent big-endian over I2C (per i2c_set_chip_offset_len(2)). + * Register values are 16-bit little-endian. + */ +#define FOMCU_REG_INTSTS_INPUT 0x0100 +#define FOMCU_INTSTS_INPUT_BTN BIT(0) + +#define FOMCU_REG_INPUT_BTNS 0x0200 + +/* Button bits in FOMCU_REG_INPUT_BTNS */ +#define FO_BTN_VIEW BIT(0) +#define FO_BTN_ESCAPE BIT(1) +#define FO_BTN_POWER BIT(2) +#define FO_BTN_EDIT BIT(3) +#define FO_BTN_RUN BIT(4) +#define FO_BTN_APPSELECT BIT(5) +#define FO_BTN_BACK BIT(6) +#define FO_BTN_DOWN BIT(7) +#define FO_BTN_RIGHT BIT(8) +#define FO_BTN_CENTER BIT(9) +#define FO_BTN_LEFT BIT(10) +#define FO_BTN_UP BIT(11) +#define FO_BTN_PTT BIT(12) + +/* Mask of all bit positions that have a non-RESERVED keycode mapping */ +#define FO_BTN_MAPPED_MASK (FO_BTN_APPSELECT | FO_BTN_BACK | \ + FO_BTN_DOWN | FO_BTN_RIGHT | \ + FO_BTN_CENTER | FO_BTN_LEFT | FO_BTN_UP) + +/* + * Keycode table indexed by button bit position in FOMCU_REG_INPUT_BTNS. + * KEY_RESERVED (0) means the button is not mapped. + * + * The input layer converts KEY_UP/DOWN/LEFT/RIGHT to ANSI escape sequences + * which cli_ch_process folds back to control characters recognised by + * bootmenu_conv_key(). KEY_ENTER -> '\r' -> BKEY_SELECT, + * KEY_ESC -> '\e' -> BKEY_QUIT. + */ +static const int fo_btn_keycodes[] = { + [0] = KEY_RESERVED, /* VIEW */ + [1] = KEY_RESERVED, /* ESCAPE */ + [2] = KEY_RESERVED, /* POWER */ + [3] = KEY_RESERVED, /* EDIT */ + [4] = KEY_RESERVED, /* RUN */ + [5] = KEY_TAB, /* APPSELECT */ + [6] = KEY_ESC, /* BACK */ + [7] = KEY_DOWN, /* DOWN */ + [8] = KEY_RIGHT, /* RIGHT */ + [9] = KEY_ENTER, /* CENTER */ + [10] = KEY_LEFT, /* LEFT */ + [11] = KEY_UP, /* UP */ + [12] = KEY_RESERVED, /* PTT */ +}; + +struct fo_keyb_priv { + unsigned long last_btns; +}; + +static int fo_read_reg(struct udevice *dev, u32 reg, u16 *val) +{ + __le16 buf; + int ret; + + ret = dm_i2c_read(dev, reg, (u8 *)&buf, sizeof(buf)); + if (ret) + return ret; + + *val = le16_to_cpu(buf); + return 0; +} + +/* + * Called by the input layer when its fifo is empty. Reads the interrupt + * status (clearing it in the process) and, if a button event is signalled, + * refreshes last_btns from the hardware. Then passes the full currently-held + * keycode set to input_send_keycodes(), which handles repeat timing and + * ASCII/ANSI conversion. + */ +static int fo_read_keys(struct input_config *input) +{ + struct udevice *dev = input->dev; + struct fo_keyb_priv *priv = dev_get_priv(dev); + int keycodes[ARRAY_SIZE(fo_btn_keycodes)]; + unsigned long i; + int n = 0; + u16 reg; + + /* + * Read interrupt status to detect changes. The read also clears the + * pending bits, preventing spurious events when the next OS sets up + * its own interrupt handler. + */ + if (!fo_read_reg(dev, FOMCU_REG_INTSTS_INPUT, ®) && + (reg & FOMCU_INTSTS_INPUT_BTN)) { + if (!fo_read_reg(dev, FOMCU_REG_INPUT_BTNS, ®)) + priv->last_btns = reg & FO_BTN_MAPPED_MASK; + } + + for_each_set_bit(i, &priv->last_btns, ARRAY_SIZE(fo_btn_keycodes)) + keycodes[n++] = fo_btn_keycodes[i]; + + input_send_keycodes(input, keycodes, n); + return 1; +} + +static int fo_keyb_start(struct udevice *dev) +{ + struct fo_keyb_priv *priv = dev_get_priv(dev); + u16 reg; + int ret; + + /* + * Drain any interrupt status accumulated before the driver started so + * that the first real poll starts from a clean baseline. + */ + fo_read_reg(dev, FOMCU_REG_INTSTS_INPUT, ®); + + /* + * Snapshot the current button state. Without this, buttons already + * held at startup would look like fresh presses on the first poll. + */ + ret = fo_read_reg(dev, FOMCU_REG_INPUT_BTNS, ®); + if (!ret) + priv->last_btns = reg & FO_BTN_MAPPED_MASK; + + return 0; +} + +static int fo_keyb_probe(struct udevice *dev) +{ + struct keyboard_priv *uc_priv = dev_get_uclass_priv(dev); + struct stdio_dev *sdev = &uc_priv->sdev; + struct input_config *input = &uc_priv->input; + int ret; + + /* MCU register addresses are 16-bit wide */ + ret = i2c_set_chip_offset_len(dev, 2); + if (ret) + return ret; + + input_add_tables(input, false); + input_set_delays(input, 250, 50); + input->dev = dev; + input->read_keys = fo_read_keys; + + strcpy(sdev->name, "flipper-one-btns"); + return input_stdio_register(sdev); +} + +static const struct keyboard_ops fo_keyb_ops = { + .start = fo_keyb_start, +}; + +static const struct udevice_id fo_keyb_ids[] = { + { .compatible = "flipper,one-mcu" }, + { } +}; + +U_BOOT_DRIVER(flipper_one_keyb) = { + .name = "flipper_one_btns", + .id = UCLASS_KEYBOARD, + .of_match = fo_keyb_ids, + .probe = fo_keyb_probe, + .ops = &fo_keyb_ops, + .priv_auto = sizeof(struct fo_keyb_priv), +}; From 50a52bc693fcc6ebac1ee443edb65d1540889804 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 11 Feb 2026 11:47:29 +0400 Subject: [PATCH 24/78] HACK: Import Flipper One DTS from our Linux branch This will ultimately need to come from upstream, but for now we need to get it in here so that we can build a working image for the device. Drop DP nodes for now, as they are not yet included in rk3576.dtsi in its U-boot version. Signed-off-by: Alexey Charkov --- .../rk3576-flipper-one-rev-f0b0c1.dts | 1763 +++++++++++++++++ 1 file changed, 1763 insertions(+) create mode 100644 dts/upstream/src/arm64/rockchip/rk3576-flipper-one-rev-f0b0c1.dts diff --git a/dts/upstream/src/arm64/rockchip/rk3576-flipper-one-rev-f0b0c1.dts b/dts/upstream/src/arm64/rockchip/rk3576-flipper-one-rev-f0b0c1.dts new file mode 100644 index 00000000000..54dc14fce58 --- /dev/null +++ b/dts/upstream/src/arm64/rockchip/rk3576-flipper-one-rev-f0b0c1.dts @@ -0,0 +1,1763 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * Copyright (c) 2026 Flipper FZCO + */ + +/dts-v1/; + +#include +#include +#include +#include +#include +#include +#include +#include "rk3576.dtsi" + +/ { + model = "Flipper One rev. F0B0C1"; + compatible = "flipper,one-rev-f0b0c1", "rockchip,rk3576"; + + aliases { + ethernet0 = &gmac0; + ethernet1 = &gmac1; + }; + + battery: battery { + compatible = "simple-battery"; + charge-full-design-microamp-hours = <3100000>; + constant-charge-current-max-microamp = <3100000>; + constant-charge-voltage-max-microvolt = <8650000>; + device-chemistry = "lithium-ion-polymer"; + operating-range-celsius = <0 60>; + voltage-min-design-microvolt = <6000000>; + }; + + chosen: chosen { + stdout-path = "serial0:1500000n8"; + }; + + hdmi-con { + compatible = "hdmi-connector"; + type = "a"; + + port { + hdmi_con_in: endpoint { + remote-endpoint = <&hdmi_out_con>; + }; + }; + }; + + leds { + compatible = "gpio-leds"; + pinctrl-0 = <&led_cd2>, <&led_cd3>; + pinctrl-names = "default"; + + led-cd2 { + color = ; + function = LED_FUNCTION_HEARTBEAT; + gpios = <&gpio0 RK_PD2 GPIO_ACTIVE_HIGH>; + linux,default-trigger = "heartbeat"; + }; + + led-cd3 { + color = ; + function = LED_FUNCTION_STATUS; + gpios = <&gpio0 RK_PD3 GPIO_ACTIVE_HIGH>; + linux,default-trigger = "default-on"; + }; + }; + + vcc8v4_sys: regulator-vcc8v4-sys { + compatible = "regulator-fixed"; + regulator-name = "vcc8v4_sys"; + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <8400000>; + regulator-max-microvolt = <8400000>; + /* Powered by the charger's SYS output */ + }; + + vcc3v3_control: regulator-vcc3v3-control { + compatible = "regulator-fixed"; + regulator-name = "vcc3v3_control"; + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + vin-supply = <&vcc8v4_sys>; + }; + + gpio_vcc3v3: regulator-vcc3v3-extgpio { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "gpio_vcc3v3"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + gpios = <&gpio_expander 0xe GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc3v3_control>; + }; + + vcc3v3_m2: regulator-vcc3v3-m2 { + compatible = "regulator-fixed"; + regulator-name = "vcc3v3_m2"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + enable-active-high; + gpios = <&gpio0 RK_PB1 GPIO_ACTIVE_HIGH>; + pinctrl-0 = <&m2_pwr_en>; + pinctrl-names = "default"; + startup-delay-us = <5000>; + vin-supply = <&vcc8v4_sys>; + }; + + vcc3v3_rtc_s5: regulator-vcc3v3-rtc-s5 { + compatible = "regulator-fixed"; + regulator-name = "vcc3v3_rtc_s5"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + /* Powered by the system battery */ + }; + + vcc5v0_device_s0: regulator-vcc5v0-device-s0 { + compatible = "regulator-fixed"; + regulator-name = "vcc5v0_device"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; + /* + * Controlled by the GPIO expander pin P13, but turning it off + * makes the expander I2C bus stuck and unrecoverable due to + * the USB MUX misbehavior on the same bus + */ + vin-supply = <&vcc8v4_sys>; + }; + + gpio_vcc5v0: regulator-vcc5v0-extgpio { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "gpio_vcc5v0"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; + gpios = <&gpio_expander 0xd GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc5v0_device_s0>; + }; + + vusb_typea_up: regulator-vcc5v0-vusb-typea-up { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "vusb_typea_up"; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; + gpios = <&gpio_expander 0xa GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc5v0_device_s0>; + }; + + vcc5v0_sys_s5: regulator-vcc5v0-sys-s5 { + compatible = "regulator-fixed"; + regulator-name = "vcc5v0_sys_s5"; + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; + /* + * Controlled by the GPIO expander pin P14, but we + * can't let this pin to ever change state, as it makes + * the system lose power + */ + vin-supply = <&vcc8v4_sys>; + }; + + vcc_2v0_pldo_s3: regulator-vcc-2v0-pldo-s3 { + compatible = "regulator-fixed"; + regulator-name = "vcc_2v0_pldo_s3"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <2000000>; + regulator-max-microvolt = <2000000>; + /* PMIC_EXT_EN_OUT */ + vin-supply = <&vcc5v0_sys_s5>; + }; + + vcc_1v1_nldo_s3: regulator-vcc-1v1-nldo-s3 { + compatible = "regulator-fixed"; + regulator-name = "vcc_1v1_nldo_s3"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <1100000>; + regulator-max-microvolt = <1100000>; + /* PMIC_EXT_EN_OUT */ + vin-supply = <&vcc5v0_sys_s5>; + }; + + vdd2l_0v9_ddr_s3: regulator-vdd2l-0v9-ddr-s3 { + compatible = "regulator-fixed"; + regulator-name = "vdd2l_0v9_ddr_s3"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <900000>; + regulator-max-microvolt = <900000>; + /* VCC_3V3_S3 */ + vin-supply = <&vcc5v0_sys_s5>; + }; + + vdd1v1_hub: regulator-vdd-1v1-hub { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "vdd1v1_hub"; + regulator-min-microvolt = <1100000>; + regulator-max-microvolt = <1100000>; + gpios = <&gpio_expander 0x9 GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc5v0_sys_s5>; + }; + + vcc_3v3_s0: regulator-vcc-3v3-s0 { + compatible = "regulator-fixed"; + regulator-name = "vcc_3v3_s0"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + /* VCCA_1V8_S0 */ + vin-supply = <&vcc_3v3_s3>; + }; + + vcc_1v8_s0: regulator-vcc-1v8-s0 { + compatible = "regulator-fixed"; + regulator-name = "vcc_1v8_s0"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + /* VCCA_1V8_S0 */ + vin-supply = <&vcc_1v8_s3>; + }; + + vccio_1v8_s0: regulator-vccio-1v8-s0 { + compatible = "regulator-fixed"; + regulator-name = "vccio_1v8_s0"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + /* VCCA_1V8_S0 */ + vin-supply = <&vcc_1v8_s3>; + }; + + vcc1v8_ufs_vccq2_s0: regulator-vcc1v8-ufs-vccq2-s0 { + compatible = "regulator-fixed"; + regulator-name = "vcc1v8_ufs_vccq2_s0"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + vin-supply = <&vcc_1v8_s3>; + }; + + rfkill-onboard-wifi { + compatible = "rfkill-gpio"; + default-blocked; + radio-type = "wlan"; + label = "Onboard WiFi+BT"; + pinctrl-0 = <&wifi_pmu_en>; + pinctrl-names = "default"; + shutdown-gpios = <&gpio1 RK_PD5 GPIO_ACTIVE_HIGH>; + }; + + rfkill-m2-wdisable1 { + compatible = "rfkill-gpio"; + radio-type = "wwan"; + label = "M.2 WWAN"; + pinctrl-0 = <&m2b_w_disable1>; + pinctrl-names = "default"; + shutdown-gpios = <&gpio2 RK_PB4 GPIO_ACTIVE_HIGH>; + }; + + rfkill-m2-wdisable2 { + compatible = "rfkill-gpio"; + radio-type = "wwan"; + label = "M.2 WWAN (WDISABLE2)"; + pinctrl-0 = <&m2b_w_disable2>; + pinctrl-names = "default"; + shutdown-gpios = <&gpio4 RK_PA0 GPIO_ACTIVE_HIGH>; + }; + + sound { + compatible = "simple-audio-card"; + simple-audio-card,name = "On-board Analog NAU8822"; + simple-audio-card,bitclock-master = <&masterdai>; + simple-audio-card,format = "i2s"; + simple-audio-card,frame-master = <&masterdai>; + simple-audio-card,mclk-fs = <256>; + simple-audio-card,pin-switches = "Headphones", "Speaker", + "Headset Microphone", "Internal Microphone"; + simple-audio-card,routing = + "Headphones", "LHP", + "Headphones", "RHP", + "Speaker", "LSPK", + "Speaker", "RSPK", + "Line Out", "AUXOUT1", + "Line Out", "AUXOUT2", + "LMICP", "Internal Microphone", + "LMICN", "Internal Microphone", + "RMICP", "Headset Microphone"; + simple-audio-card,widgets = + "Headphones", "Headphones", + "Line Out", "Line Out", + "Speaker", "Speaker", + "Microphone", "Internal Microphone", + "Microphone", "Headset Microphone"; + + simple-audio-card,cpu { + sound-dai = <&sai2>; + }; + + masterdai: simple-audio-card,codec { + sound-dai = <&nau8822>; + system-clock-frequency = <12288000>; + }; + }; + + typea_up_con: usb-a-connector { + compatible = "usb-a-connector"; + data-role = "host"; + label = "USB-A (up)"; + power-role = "source"; + vbus-supply = <&vusb_typea_up>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + typea_up_con_hs: endpoint { + remote-endpoint = <&hub_2_0_ds2>; + }; + }; + + port@1 { + reg = <1>; + typea_up_con_ss: endpoint { + remote-endpoint = <&hub_3_0_ds2>; + }; + }; + }; + }; + + typec_up_con: usb-c-connector { + compatible = "usb-c-connector"; + data-role = "host"; + label = "USB-C (up)"; + power-role = "source"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + typec_up_con_hs: endpoint { + remote-endpoint = <&hub_2_0_ds3>; + }; + }; + + port@1 { + reg = <1>; + typec_up_con_ss: endpoint { + remote-endpoint = <&usb_mux_in>; + }; + }; + }; + }; +}; + +&cpu_l0 { + cpu-supply = <&vdd_cpu_lit_s0>; +}; + +&cpu_l1 { + cpu-supply = <&vdd_cpu_lit_s0>; +}; + +&cpu_l2 { + cpu-supply = <&vdd_cpu_lit_s0>; +}; + +&cpu_l3 { + cpu-supply = <&vdd_cpu_lit_s0>; +}; + +&cpu_b0 { + cpu-supply = <&vdd_cpu_big_s0>; +}; + +&cpu_b1 { + cpu-supply = <&vdd_cpu_big_s0>; +}; + +&cpu_b2 { + cpu-supply = <&vdd_cpu_big_s0>; +}; + +&cpu_b3 { + cpu-supply = <&vdd_cpu_big_s0>; +}; + +&combphy0_ps { + status = "okay"; +}; + +&combphy1_psu { + status = "okay"; +}; + +&gmac0 { + clock_in_out = "output"; + phy-handle = <&rgmii_phy0>; + phy-mode = "rgmii-id"; + phy-supply = <&vccio_1v8_s0>; + pinctrl-names = "default"; + pinctrl-0 = <ð0m0_miim + ð0m0_tx_bus2 + ð0m0_rx_bus2 + ð0m0_rgmii_clk + ð0m0_rgmii_bus + ðm0_clk0_25m_out>; + status = "okay"; +}; + +&gmac1 { + clock_in_out = "output"; + phy-handle = <&rgmii_phy1>; + phy-mode = "rgmii-id"; + phy-supply = <&vccio_1v8_s0>; + pinctrl-names = "default"; + pinctrl-0 = <ð1m0_miim + ð1m0_tx_bus2 + ð1m0_rx_bus2 + ð1m0_rgmii_clk + ð1m0_rgmii_bus + ðm0_clk1_25m_out>; + status = "okay"; +}; + +&gpio2 { + m2-poweroff-hog { + gpios = ; + output-low; + line-name = "M.2 Full Card Power Off Disable"; + gpio-hog; + }; +}; + +&gpio3 { + m2-reset-hog { + gpios = ; + output-low; + line-name = "M.2 Reset Disable"; + gpio-hog; + }; +}; + +&gpu { + mali-supply = <&vdd_gpu_s0>; + status = "okay"; +}; + +&hdmi { + frl-enable-gpios = <&gpio0 RK_PC3 GPIO_ACTIVE_LOW>; + status = "okay"; +}; + +&hdmi_in { + hdmi_in_vp0: endpoint { + remote-endpoint = <&vp0_out_hdmi>; + }; +}; + +&hdmi_out { + hdmi_out_con: endpoint { + remote-endpoint = <&hdmi_con_in>; + }; +}; + +&hdmi_sound { + status = "okay"; +}; + +&hdptxphy { + status = "okay"; +}; + +&i2c0 { + clock-frequency = <400000>; + pinctrl-0 = <&i2c0m1_xfer &i2c0_sda_pullup>; + pinctrl-names = "default"; + status = "okay"; + + embedded-controller@69 { + compatible = "flipper,one-mcu"; + reg = <0x69>; + interrupt-parent = <&gpio2>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&audio_headset_int &cpu_int>; + wakeup-source; + }; +}; + +&i2c1 { + status = "okay"; + + rk806: pmic@23 { + compatible = "rockchip,rk806"; + reg = <0x23>; + interrupt-parent = <&gpio0>; + interrupts = ; + gpio-controller; + #gpio-cells = <2>; + pinctrl-names = "default"; + pinctrl-0 = <&pmic_pins>, <&rk806_dvs1_null>, + <&rk806_dvs2_null>, <&rk806_dvs3_null>; + system-power-controller; + + vcc1-supply = <&vcc5v0_sys_s5>; + vcc2-supply = <&vcc5v0_sys_s5>; + vcc3-supply = <&vcc5v0_sys_s5>; + vcc4-supply = <&vcc5v0_sys_s5>; + vcc5-supply = <&vcc5v0_sys_s5>; + vcc6-supply = <&vcc5v0_sys_s5>; + vcc7-supply = <&vcc5v0_sys_s5>; + vcc8-supply = <&vcc5v0_sys_s5>; + vcc9-supply = <&vcc5v0_sys_s5>; + vcc10-supply = <&vcc5v0_sys_s5>; + vcc11-supply = <&vcc_2v0_pldo_s3>; + vcc12-supply = <&vcc5v0_sys_s5>; + vcc13-supply = <&vcc_1v1_nldo_s3>; + vcc14-supply = <&vcc_1v1_nldo_s3>; + vcca-supply = <&vcc5v0_sys_s5>; + + rk806_dvs1_null: dvs1-null-pins { + pins = "gpio_pwrctrl1"; + function = "pin_fun0"; + }; + + rk806_dvs2_null: dvs2-null-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun0"; + }; + + rk806_dvs3_null: dvs3-null-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun0"; + }; + + rk806_dvs1_slp: dvs1-slp-pins { + pins = "gpio_pwrctrl1"; + function = "pin_fun1"; + }; + + rk806_dvs1_pwrdn: dvs1-pwrdn-pins { + pins = "gpio_pwrctrl1"; + function = "pin_fun2"; + }; + + rk806_dvs1_rst: dvs1-rst-pins { + pins = "gpio_pwrctrl1"; + function = "pin_fun3"; + }; + + rk806_dvs2_slp: dvs2-slp-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun1"; + }; + + rk806_dvs2_pwrdn: dvs2-pwrdn-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun2"; + }; + + rk806_dvs2_rst: dvs2-rst-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun3"; + }; + + rk806_dvs2_dvs: dvs2-dvs-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun4"; + }; + + rk806_dvs2_gpio: dvs2-gpio-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun5"; + }; + + rk806_dvs3_slp: dvs3-slp-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun1"; + }; + + rk806_dvs3_pwrdn: dvs3-pwrdn-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun2"; + }; + + rk806_dvs3_rst: dvs3-rst-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun3"; + }; + + rk806_dvs3_dvs: dvs3-dvs-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun4"; + }; + + rk806_dvs3_gpio: dvs3-gpio-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun5"; + }; + + regulators { + vdd_cpu_big_s0: dcdc-reg1 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <950000>; + regulator-ramp-delay = <12500>; + regulator-name = "vdd_cpu_big_s0"; + regulator-enable-ramp-delay = <400>; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdd_npu_s0: dcdc-reg2 { + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <950000>; + regulator-ramp-delay = <12500>; + regulator-name = "vdd_npu_s0"; + regulator-enable-ramp-delay = <400>; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdd_cpu_lit_s0: dcdc-reg3 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <950000>; + regulator-ramp-delay = <12500>; + regulator-name = "vdd_cpu_lit_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + regulator-suspend-microvolt = <750000>; + }; + }; + + vcc_3v3_s3: dcdc-reg4 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + regulator-name = "vcc_3v3_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + regulator-suspend-microvolt = <3300000>; + }; + }; + + vdd_gpu_s0: dcdc-reg5 { + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <900000>; + regulator-ramp-delay = <12500>; + regulator-name = "vdd_gpu_s0"; + regulator-enable-ramp-delay = <400>; + + regulator-state-mem { + regulator-off-in-suspend; + regulator-suspend-microvolt = <850000>; + }; + }; + + vddq_ddr_s0: dcdc-reg6 { + regulator-always-on; + regulator-boot-on; + regulator-name = "vddq_ddr_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdd_logic_s0: dcdc-reg7 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <800000>; + regulator-name = "vdd_logic_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcc_1v8_s3: dcdc-reg8 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-name = "vcc_1v8_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + regulator-suspend-microvolt = <1800000>; + }; + }; + + vdd2_ddr_s3: dcdc-reg9 { + regulator-always-on; + regulator-boot-on; + regulator-name = "vdd2_ddr_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + }; + }; + + vdd_ddr_s0: dcdc-reg10 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <1200000>; + regulator-name = "vdd_ddr_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcca_1v8_s0: pldo-reg1 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-name = "vcca_1v8_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcca1v8_pldo2_s0: pldo-reg2 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-name = "vcca1v8_pldo2_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdda_1v2_s0: pldo-reg3 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1200000>; + regulator-max-microvolt = <1200000>; + regulator-name = "vdda_1v2_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcca_3v3_s0: pldo-reg4 { + regulator-enable-ramp-delay = <500>; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + regulator-ramp-delay = <12500>; + regulator-name = "vcca_3v3_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vccio_3v3_sd_s0: pldo-reg5 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <3300000>; + regulator-name = "vccio_3v3_sd_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcca1v8_pldo6_s3: pldo-reg6 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-name = "vcca1v8_pldo6_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + regulator-suspend-microvolt = <1800000>; + }; + }; + + vdd_0v75_s3: nldo-reg1 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <750000>; + regulator-name = "vdd_0v75_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + regulator-suspend-microvolt = <750000>; + }; + }; + + vdda_ddr_pll_s0: nldo-reg2 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <850000>; + regulator-max-microvolt = <850000>; + regulator-name = "vdda_ddr_pll_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdda0v75_hdmi_s0: nldo-reg3 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <837500>; + regulator-max-microvolt = <837500>; + regulator-name = "vdda0v75_hdmi_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdda_0v85_s0: nldo-reg4 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <850000>; + regulator-max-microvolt = <850000>; + regulator-name = "vdda_0v85_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdda_0v75_s0: nldo-reg5 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <750000>; + regulator-max-microvolt = <750000>; + regulator-name = "vdda_0v75_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + }; + }; +}; + +&i2c2 { + clock-frequency = <400000>; + pinctrl-0 = <&i2c2m0_xfer_dr5>; + pinctrl-names = "default"; + status = "okay"; + + gpio_expander: gpio@20 { + compatible = "ti,tca6416"; + reg = <0x20>; + gpio-controller; + #gpio-cells = <2>; + #interrupt-cells = <2>; + interrupt-controller; + interrupt-parent = <&gpio2>; + interrupts = ; + pinctrl-0 = <&expander_int>; + pinctrl-names = "default"; + vcc-supply = <&vcc3v3_control>; + + usb2-switch-hog { + gpios = <8 GPIO_ACTIVE_HIGH>; + output-high; + line-name = "Main Type-C USB 2.0 pins switch to CPU"; + gpio-hog; + }; + + vcc5v0-device-s0-hog { + gpios = <0xb GPIO_ACTIVE_HIGH>; + output-high; + line-name = "Peripheral power supply"; + gpio-hog; + }; + + vcc5v0-sys-s5-hog { + gpios = <0xc GPIO_ACTIVE_HIGH>; + output-high; + line-name = "Main power supply to the system PMIC"; + gpio-hog; + }; + }; + + usbc0: typec-portc@22 { + compatible = "fcs,fusb302"; + reg = <0x22>; + interrupt-parent = <&gpio_expander>; + interrupts = <3 IRQ_TYPE_LEVEL_LOW>; + vbus-supply = <&vusb_typec>; + + connector { + compatible = "usb-c-connector"; + label = "USB-C"; + data-role = "dual"; + op-sink-microwatt = <10000000>; + /* fusb302 supports PD Rev 2.0 Ver 1.2 */ + pd-revision = /bits/ 8 <0x2 0x0 0x1 0x2>; + power-role = "dual"; + self-powered; + sink-pdos = ; + source-pdos = ; + try-power-role = "sink"; + + altmodes { + displayport { + svid = /bits/ 16 <0xff01>; + vdo = <0xffffffff>; + }; + }; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + usbc0_hs: endpoint { + remote-endpoint = <&usb_drd0_hs_ep>; + }; + }; + port@1 { + reg = <1>; + usbc0_ss: endpoint { + remote-endpoint = <&usbdp_phy_ss_out>; + }; + }; + port@2 { + reg = <2>; + usbc0_sbu: endpoint { + remote-endpoint = <&usbdp_phy0_dp_out>; + }; + }; + }; + }; + }; + + ina4_1: power-sensor@40 { + compatible = "ti,ina4230"; + reg = <0x40>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vdd_0v75_s3"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@1 { + reg = <1>; + label = "vcc_3v3_control"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <4000000>; + }; + + input@2 { + reg = <2>; + label = "vdd0v85_ddr_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + + input@3 { + reg = <3>; + label = "vcc_3v3_s3"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <5000000>; + }; + }; + + ina4_2: power-sensor@41 { + compatible = "ti,ina4230"; + reg = <0x41>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vddq0v51_ddr_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + + input@1 { + reg = <1>; + label = "vdd0v75_npu_s0"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <5000000>; + }; + + input@2 { + reg = <2>; + label = "vdd0v75_gpu_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + + input@3 { + reg = <3>; + label = "vdd0v75_logic_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + }; + + ina4_3: power-sensor@43 { + compatible = "ti,ina4230"; + reg = <0x43>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vdd0v75_cpu_big_s0"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <6500000>; + }; + + input@1 { + reg = <1>; + label = "vdda_1v2_s0"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@2 { + reg = <2>; + label = "vcca_1v8_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <500000>; + }; + + input@3 { + reg = <3>; + label = "vdd0v75_cpu_lit_s0"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <5000000>; + }; + }; + + ina4_4: power-sensor@44 { + compatible = "ti,ina4230"; + reg = <0x44>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vdda_0v75_s0"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@1 { + reg = <1>; + label = "vdda_0v85_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <500000>; + }; + + input@2 { + reg = <2>; + label = "vdda0v75_hdmi_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <500000>; + }; + + input@3 { + reg = <3>; + label = "vdda0v85_ddr_pll_s0"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + }; + + ina_sys: power-sensor@45 { + compatible = "ti,ina219"; + reg = <0x45>; + #io-channel-cells = <1>; + label = "vcc8v4_sys"; + shunt-resistor = <4000>; + vs-supply = <&vcc3v3_control>; + }; + + ina4_5: power-sensor@46 { + compatible = "ti,ina4230"; + reg = <0x46>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vcca_3v3_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <500000>; + }; + + input@1 { + reg = <1>; + label = "vccio_3v3_sd_s0"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@2 { + reg = <2>; + label = "vdd2_1v05_ddr_s3"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <2500000>; + }; + + input@3 { + reg = <3>; + label = "vcc_1v8_s3"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + }; + + usbmux: usb-mux@47 { + compatible = "ti,hd3ss3220"; + reg = <0x47>; + interrupt-parent = <&gpio1>; + interrupts = ; + pinctrl-0 = <&usb_mux_int>; + pinctrl-names = "default"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + usb_mux_in: endpoint { + remote-endpoint = <&typec_up_con_ss>; + }; + }; + + port@1 { + reg = <1>; + usb_mux_out: endpoint { + remote-endpoint = <&hub_3_0_ds3>; + }; + }; + }; + }; + + gauge: fuel-gauge@55 { + compatible = "ti,bq28z610"; /* really bq28z620: tbc if we need a new compatible */ + reg = <0x55>; + monitored-battery = <&battery>; + }; + + bq25792: charger@6b { + compatible = "ti,bq25792"; + reg = <0x6b>; + input-current-limit-microamp = <3300000>; + interrupt-parent = <&gpio_expander>; + interrupts = <2 IRQ_TYPE_LEVEL_LOW>; + monitored-battery = <&battery>; + power-supplies = <&usbc0>; + + regulators { + vusb_typec: vbus { + regulator-max-microamp = <3320000>; + regulator-max-microvolt = <22000000>; + regulator-min-microamp = <0>; + regulator-min-microvolt = <2800000>; + regulator-name = "vusb_typec"; + }; + }; + }; +}; + +&i2c5 { + pinctrl-0 = <&i2c5m3_xfer>; + pinctrl-names = "default"; + status = "okay"; + + /* MIPI CSI camera + * PDN GPIO: GPIO3 RK_PC6 <&cam_pdn> + * CLK: CAM_CLK2_OUT_M0 <&cam_clk2m0_clk2> + */ +}; + +&i2c6 { + pinctrl-0 = <&i2c6m3_xfer>; + pinctrl-names = "default"; + status = "okay"; + + nau8822: audio-codec@1a { + compatible = "nuvoton,nau8822"; + reg = <0x1a>; + + /* all supplies: VCCA_3V3_S0 */ + assigned-clocks = <&cru CLK_SAI2_MCLKOUT_TO_IO>; + assigned-clock-rates = <12288000>; + clock-names = "mclk"; + clocks = <&cru CLK_SAI2_MCLKOUT_TO_IO>; + pinctrl-names = "default"; + pinctrl-0 = <&sai2m0_mclk>; + vdda-supply = <&vcca_3v3_s0>; + vddb-supply = <&vcca_3v3_s0>; + vddc-supply = <&vcca_3v3_s0>; + vddspk-supply = <&vcca_3v3_s0>; + #sound-dai-cells = <0>; + nuvoton,spk-btl; + }; +}; + +&i2c8 { + pinctrl-0 = <&i2c8m2_xfer>; + pinctrl-names = "default"; + status = "okay"; + + hym8563: rtc@51 { + compatible = "haoyu,hym8563"; + reg = <0x51>; + clock-output-names = "hym8563"; + interrupt-parent = <&gpio0>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&rtc_int &clk_32k_pins>; + wakeup-source; + #clock-cells = <0>; + }; +}; + +&mdio0 { + rgmii_phy0: ethernet-phy@1 { + compatible = "ethernet-phy-id001c.c916"; + reg = <0x1>; + clocks = <&cru REFCLKO25M_GMAC0_OUT>; + assigned-clocks = <&cru REFCLKO25M_GMAC0_OUT>; + assigned-clock-rates = <25000000>; + interrupt-parent = <&gpio1>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&rgmii_phy0_int &rgmii_phy0_rst>; + reset-assert-us = <20000>; + reset-deassert-us = <100000>; + reset-gpios = <&gpio1 RK_PB5 GPIO_ACTIVE_LOW>; + wakeup-source; + realtek,aldps-enable; + }; +}; + +&mdio1 { + rgmii_phy1: ethernet-phy@1 { + compatible = "ethernet-phy-id001c.c916"; + reg = <0x1>; + clocks = <&cru REFCLKO25M_GMAC1_OUT>; + assigned-clocks = <&cru REFCLKO25M_GMAC1_OUT>; + assigned-clock-rates = <25000000>; + interrupt-parent = <&gpio1>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&rgmii_phy1_int &rgmii_phy1_rst>; + reset-assert-us = <20000>; + reset-deassert-us = <100000>; + reset-gpios = <&gpio1 RK_PB4 GPIO_ACTIVE_LOW>; + wakeup-source; + realtek,aldps-enable; + }; +}; + +&pcie0 { + pinctrl-names = "default"; + pinctrl-0 = <&pcie0m1_pins &pcie0_rst>; + reset-gpios = <&gpio1 RK_PC3 GPIO_ACTIVE_HIGH>; + vpcie3v3-supply = <&vcc3v3_m2>; + status = "okay"; +}; + +&pinctrl { + camera { + cam_pdn: cam-pdn { + rockchip,pins = <3 RK_PC6 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + display { + spi0m0_wo_miso: spi0m0-wo-miso { + rockchip,pins = + /* spi0m0_csn0 */ + <0 RK_PC6 11 &pcfg_pull_none>, + /* spi0_clk_m0 */ + <0 RK_PC7 11 &pcfg_pull_none>, + /* spi0_mosi_m0 */ + <0 RK_PD0 11 &pcfg_pull_none>; + }; + + spi0m0_gpiomiso: spi0m0-gpiomiso { + rockchip,pins = <0 RK_PD1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + gpio-expander { + expander_int: expander-int { + rockchip,pins = <2 RK_PA7 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + hdmi { + hdmi_enable_frl: hdmi-enable-frl { + rockchip,pins = <0 RK_PC3 RK_FUNC_GPIO &pcfg_pull_down>; + }; + }; + + hym8563 { + rtc_int: rtc-int { + rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + i2c0 { + i2c0_sda_pullup: i2c0-sda-pullup { + rockchip,pins = <0 RK_PC5 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + i2c2 { + i2c2m0_xfer_dr5: i2c2m0-xfer-dr5 { + rockchip,pins = + /* i2c2_scl_m0 */ + <0 RK_PB7 9 &pcfg_pull_none_drv_level_5_smt>, + /* i2c2_sda_m0 */ + <0 RK_PC0 9 &pcfg_pull_none_drv_level_5_smt>; + }; + }; + + leds { + led_cd2: led-cd2 { + rockchip,pins = <0 RK_PD2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + led_cd3: led-cd3 { + rockchip,pins = <0 RK_PD3 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + m2 { + m2_pwr_en: m2-pwren { + rockchip,pins = <0 RK_PB1 RK_FUNC_GPIO &pcfg_pull_down>; + }; + + m2b_w_disable1: m2-rfkill-wdisable1 { + rockchip,pins = <2 RK_PB4 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + m2b_w_disable2: m2-rfkill-wdisable2 { + rockchip,pins = <4 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + mcu { + audio_headset_int: audio-headset-int { + rockchip,pins = <0 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + cpu_int: cpu-int { + rockchip,pins = <2 RK_PA6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + network { + rgmii_phy0_int: rgmii-phy0-int { + rockchip,pins = <1 RK_PC1 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + rgmii_phy0_rst: rgmii-phy0-rst { + rockchip,pins = <1 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + rgmii_phy1_int: rgmii-phy1-int { + rockchip,pins = <1 RK_PC2 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + rgmii_phy1_rst: rgmii-phy1-rst { + rockchip,pins = <1 RK_PB4 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + pcie0 { + pcie0_rst: pcie0-rst { + rockchip,pins = <1 RK_PC3 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + usb { + hub_reset: usb-hub-reset { + rockchip,pins = <0 RK_PC4 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + usb_mux_int: usb-mux-int { + rockchip,pins = <1 RK_PC0 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + usbc0_sbu1: usbc0-sbu1 { + rockchip,pins = <4 RK_PC4 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + usbc0_sbu2: usbc0-sbu2 { + rockchip,pins = <4 RK_PC5 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + wifibt { + wifi_pmu_en: wifi-pmu-en { + rockchip,pins = <1 RK_PD5 RK_FUNC_GPIO &pcfg_pull_down>; + }; + + wifi_wgpio0: bt-wake-host { + rockchip,pins = <1 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + wifi_wgpio1: wifi-wake-host { + rockchip,pins = <1 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; +}; + +&sai0 { + /* Serial audio for M.2 WWAN */ + pinctrl-names = "default"; + pinctrl-0 = <&sai0m2_lrck + &sai0m2_sclk + &sai0m2_sdi0 + &sai0m2_sdo0>; + status = "okay"; +}; + +&sai2 { + status = "okay"; +}; + +&sai6 { + status = "okay"; +}; + +&sdmmc { + bus-width = <4>; + cap-sd-highspeed; + cd-gpios = <&gpio0 RK_PA7 GPIO_ACTIVE_LOW>; + disable-wp; + no-sdio; + no-mmc; + sd-uhs-sdr104; + vmmc-supply = <&vcc_3v3_s3>; + vqmmc-supply = <&vccio_3v3_sd_s0>; + status = "okay"; +}; + +&saradc { + vref-supply = <&vcca_1v8_s0>; + status = "okay"; +}; + +&spi0 { + pinctrl-0 = <&spi0m0_wo_miso &spi0m0_gpiomiso>; + status = "okay"; + + display@0 { + compatible = "flipper,one-display"; + reg = <0>; + active-gpios = <&gpio0 RK_PD1 GPIO_ACTIVE_LOW>; + spi-cpha; + spi-cpol; + spi-max-frequency = <20000000>; + spi-rx-bus-width = <0>; + }; +}; + +&u2phy0 { + status = "okay"; +}; + +&u2phy0_otg { + status = "okay"; +}; + +&u2phy1 { + status = "okay"; +}; + +&u2phy1_otg { + status = "okay"; +}; + +&uart0 { + status = "okay"; +}; + +&uart4 { + pinctrl-0 = <&uart4m1_xfer>; + pinctrl-names = "default"; + status = "okay"; + + /* MCU interconnect */ +}; + +&ufshc { + vcc-supply = <&vcc_3v3_s3>; + vccq2-supply = <&vcc1v8_ufs_vccq2_s0>; + status = "okay"; +}; + +&usbdp_phy { + mode-switch; + orientation-switch; + pinctrl-names = "default"; + pinctrl-0 = <&usbc0_sbu1 &usbc0_sbu2>; + sbu1-dc-gpios = <&gpio4 RK_PC4 GPIO_ACTIVE_HIGH>; + sbu2-dc-gpios = <&gpio4 RK_PC5 GPIO_ACTIVE_HIGH>; + status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + + port@0 { + reg = <0>; + + usbdp_phy_ss_out: endpoint { + remote-endpoint = <&usbc0_ss>; + }; + }; + + port@1 { + reg = <1>; + + usbdp_phy_ss_in: endpoint { + remote-endpoint = <&usb_drd0_ss_ep>; + }; + }; + + port@2 { + reg = <2>; + + usbdp_phy_dp_in: endpoint { + }; + }; + + port@3 { + reg = <3>; + + usbdp_phy0_dp_out: endpoint { + remote-endpoint = <&usbc0_sbu>; + }; + }; + }; +}; + +&usb_drd0_dwc3 { + usb-role-switch; + dr_mode = "otg"; + status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + usb_drd0_hs_ep: endpoint { + remote-endpoint = <&usbc0_hs>; + }; + }; + + port@1 { + reg = <1>; + usb_drd0_ss_ep: endpoint { + remote-endpoint = <&usbdp_phy_ss_in>; + }; + }; + }; +}; + +&usb_drd1_dwc3 { + #address-cells = <1>; + #size-cells = <0>; + dr_mode = "host"; + pinctrl-0 = <&hub_reset>; + pinctrl-names = "default"; + status = "okay"; + + /* 2.0 hub */ + hub_2_0: hub@1 { + compatible = "usb3431,6241"; + reg = <1>; + peer-hub = <&hub_3_0>; + reset-gpios = <&gpio0 RK_PC4 GPIO_ACTIVE_LOW>; + vdd1v1-supply = <&vdd1v1_hub>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@1 { + reg = <1>; + /* ext USB port 2.54mm */ + }; + + port@2 { + reg = <2>; + hub_2_0_ds2: endpoint { + remote-endpoint = <&typea_up_con_hs>; + }; + }; + + port@3 { + reg = <3>; + hub_2_0_ds3: endpoint { + remote-endpoint = <&typec_up_con_hs>; + }; + }; + + port@4 { + reg = <4>; + /* M.2 */ + }; + }; + }; + + /* 3.0 hub */ + hub_3_0: hub@2 { + compatible = "usb3431,6341"; + reg = <2>; + #address-cells = <1>; + #size-cells = <0>; + peer-hub = <&hub_2_0>; + vdd1v1-supply = <&vdd1v1_hub>; + + device@1 { + /* WiFi-BT module connection */ + compatible = "usbe8d,7961"; + reg = <1>; + #address-cells = <2>; + #size-cells = <0>; + + interface@0 { + /* BT part */ + compatible = "usbife8d,7961.config1.0"; + reg = <0 1>; + interrupt-parent = <&gpio1>; + interrupts = ; + interrupt-names = "wakeup"; + pinctrl-0 = <&wifi_wgpio0>; + pinctrl-names = "default"; + }; + + interface@0,3 { + /* WiFi part */ + compatible = "usbife8d,7961.config1.3"; + reg = <0 3>; + interrupt-parent = <&gpio1>; + interrupts = ; + pinctrl-0 = <&wifi_wgpio1>; + pinctrl-names = "default"; + }; + }; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@2 { + reg = <2>; + hub_3_0_ds2: endpoint { + remote-endpoint = <&typea_up_con_ss>; + }; + }; + + port@3 { + reg = <3>; + hub_3_0_ds3: endpoint { + remote-endpoint = <&usb_mux_out>; + }; + }; + + port@4 { + reg = <4>; + /* M.2 */ + }; + }; + }; +}; + +&vop { + status = "okay"; +}; + +&vop_mmu { + status = "okay"; +}; + +&vp0 { + vp0_out_hdmi: endpoint@ROCKCHIP_VOP2_EP_HDMI0 { + reg = ; + remote-endpoint = <&hdmi_in_vp0>; + }; +}; From 7dc1abf9f6a2a18b43a5cb027f84f2f37510844a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 11 Feb 2026 11:53:16 +0400 Subject: [PATCH 25/78] board: rockchip: Add Flipper One Flipper One is a portable multi-tool device with a built-in display and various interfaces, built around the Rockchip RK3576 SoC. Add a new defconfig file for building U-Boot for this device. Signed-off-by: Alexey Charkov --- .../rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi | 19 +++++ arch/arm/mach-rockchip/rk3576/Kconfig | 7 ++ configs/flipper-one-rk3576_defconfig | 84 +++++++++++++++++++ include/configs/flipper-one-rk3576.h | 17 ++++ 4 files changed, 127 insertions(+) create mode 100644 arch/arm/dts/rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi create mode 100644 configs/flipper-one-rk3576_defconfig create mode 100644 include/configs/flipper-one-rk3576.h diff --git a/arch/arm/dts/rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi b/arch/arm/dts/rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi new file mode 100644 index 00000000000..542e6560095 --- /dev/null +++ b/arch/arm/dts/rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * Copyright (c) 2026 Flipper FZCO + */ + +#include "rk3576-u-boot.dtsi" + +/* + * The video driver needs its framebuffer reserved before relocation. + * Mark spi0 and the display node so they are bound during the pre-relocation + * DM scan and video_reserve() can carve out the framebuffer memory. + */ +&spi0 { + bootph-all; + + display@0 { + bootph-all; + }; +}; diff --git a/arch/arm/mach-rockchip/rk3576/Kconfig b/arch/arm/mach-rockchip/rk3576/Kconfig index f347caf8904..e53e9a3bd85 100644 --- a/arch/arm/mach-rockchip/rk3576/Kconfig +++ b/arch/arm/mach-rockchip/rk3576/Kconfig @@ -1,5 +1,11 @@ if ROCKCHIP_RK3576 +config TARGET_FLIPPER_ONE_RK3576 + bool "Flipper One" + help + Flipper One is a handheld networking multitool for enthusiasts + using the Rockchip RK3576. + config TARGET_ROC_PC_RK3576 bool "Firefly ROC-RK3576-PC" help @@ -18,6 +24,7 @@ config SYS_SOC source board/firefly/roc-pc-rk3576/Kconfig config SYS_CONFIG_NAME + default "flipper-one-rk3576" if TARGET_FLIPPER_ONE_RK3576 default "rk3576_common" endif diff --git a/configs/flipper-one-rk3576_defconfig b/configs/flipper-one-rk3576_defconfig new file mode 100644 index 00000000000..626b63678fc --- /dev/null +++ b/configs/flipper-one-rk3576_defconfig @@ -0,0 +1,84 @@ +CONFIG_ARM=y +CONFIG_SKIP_LOWLEVEL_INIT=y +CONFIG_COUNTER_FREQUENCY=24000000 +CONFIG_ARCH_ROCKCHIP=y +CONFIG_SPL_GPIO=y +CONFIG_DEFAULT_DEVICE_TREE="rockchip/rk3576-flipper-one-rev-f0b0c1" +CONFIG_ROCKCHIP_RK3576=y +CONFIG_TARGET_FLIPPER_ONE_RK3576=y +CONFIG_SYS_LOAD_ADDR=0x48000000 +CONFIG_DEBUG_UART_BASE=0x2AD40000 +CONFIG_DEBUG_UART_CLOCK=24000000 +CONFIG_PCI=y +CONFIG_DEBUG_UART=y +CONFIG_DEFAULT_FDT_FILE="rockchip/rk3576-flipper-one-rev-f0b0c1.dtb" +# CONFIG_DISPLAY_CPUINFO is not set +CONFIG_CYCLIC_MAX_CPU_TIME_US=50000 +CONFIG_SPL_MAX_SIZE=0x40000 +# CONFIG_SPL_RAW_IMAGE_SUPPORT is not set +CONFIG_SPL_DM_RESET=y +CONFIG_SPL_UFS_SUPPORT=y +CONFIG_CMD_TCPM=y +CONFIG_CMD_MEMINFO=y +CONFIG_CMD_MEMINFO_MAP=y +CONFIG_CMD_ADC=y +CONFIG_CMD_GPIO=y +CONFIG_CMD_GPT=y +CONFIG_CMD_I2C=y +CONFIG_CMD_MISC=y +CONFIG_CMD_MMC=y +CONFIG_CMD_UFS=y +CONFIG_CMD_USB=y +CONFIG_CMD_ROCKUSB=y +CONFIG_CMD_USB_MASS_STORAGE=y +# CONFIG_CMD_SETEXPR is not set +CONFIG_CMD_RNG=y +CONFIG_CMD_REGULATOR=y +# CONFIG_SPL_DOS_PARTITION is not set +CONFIG_OF_SPL_REMOVE_PROPS="clock-names interrupt-parent assigned-clocks assigned-clock-rates assigned-clock-parents" +CONFIG_BUTTON=y +CONFIG_BUTTON_GPIO=y +# CONFIG_USB_FUNCTION_FASTBOOT is not set +CONFIG_ROCKCHIP_GPIO=y +CONFIG_DM_PCA953X=y +CONFIG_SYS_I2C_ROCKCHIP=y +CONFIG_SUPPORT_EMMC_RPMB=y +CONFIG_MMC_DW=y +CONFIG_MMC_DW_ROCKCHIP=y +CONFIG_MMC_SDHCI=y +CONFIG_MMC_SDHCI_SDMA=y +CONFIG_MMC_SDHCI_ROCKCHIP=y +CONFIG_PHY_ROCKCHIP_INNO_USB2=y +CONFIG_PHY_ROCKCHIP_NANENG_COMBOPHY=y +CONFIG_PHY_ROCKCHIP_USBDP=y +CONFIG_DM_PMIC=y +CONFIG_PMIC_RK8XX=y +CONFIG_REGULATOR_RK8XX=y +CONFIG_SCSI=y +CONFIG_BAUDRATE=1500000 +CONFIG_DEBUG_UART_SHIFT=2 +CONFIG_SYS_NS16550_MEM32=y +CONFIG_ROCKCHIP_SPI=y +CONFIG_SYSRESET_PSCI=y +CONFIG_USB=y +CONFIG_USB_XHCI_HCD=y +CONFIG_USB_DWC3=y +CONFIG_USB_DWC3_GENERIC=y +CONFIG_TYPEC_TCPM=y +CONFIG_TYPEC_FUSB302=y +CONFIG_USB_ONBOARD_HUB=y +CONFIG_USB_GADGET=y +CONFIG_USB_GADGET_DOWNLOAD=y +CONFIG_USB_FUNCTION_ROCKUSB=y +CONFIG_UFS=y +CONFIG_UFS_ROCKCHIP=y +CONFIG_VIDEO=y +CONFIG_VIDEO_FONT_6X8=y +# CONFIG_VIDEO_FONT_8X16 is not set +# CONFIG_BACKLIGHT is not set +# CONFIG_VIDEO_BPP16 is not set +# CONFIG_VIDEO_BPP32 is not set +# CONFIG_SYS_WHITE_ON_BLACK is not set +# CONFIG_PANEL is not set +CONFIG_VIDEO_FLIPPER_ONE=y +CONFIG_ERRNO_STR=y diff --git a/include/configs/flipper-one-rk3576.h b/include/configs/flipper-one-rk3576.h new file mode 100644 index 00000000000..62e43dca3d3 --- /dev/null +++ b/include/configs/flipper-one-rk3576.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Configuration for the Flipper One (RK3576) + * + * Copyright (c) 2026 Flipper FZCO + */ + +#ifndef __CONFIG_FLIPPER_ONE_RK3576_H +#define __CONFIG_FLIPPER_ONE_RK3576_H + +#define ROCKCHIP_DEVICE_SETTINGS \ + "stdout=serial,vidconsole\0" \ + "stderr=serial,vidconsole\0" + +#include "rk3576_common.h" + +#endif /* __CONFIG_FLIPPER_ONE_RK3576_H */ From 90bc4cc559695715ee536bccfa8f6ba32116634f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 8 May 2026 13:49:30 +0400 Subject: [PATCH 26/78] usb: dwc3-generic: bring up clocks and resets before initializing the PHY dwc3_glue_probe() currently fetches the "usb3-phy" handle and calls generic_phy_init() on it as the very first thing, before the glue's own clocks and resets are touched. Only afterwards does it call dwc3_glue_clk_init() and dwc3_glue_reset_init(), and later still generic_phy_power_on(). For most platforms this happens to work because the controller has been held in reset since power-on, so whatever state it presents on its PIPE interface is benign while the PHY runs its bring-up sequence. It breaks, however, when an earlier stage has already taken the controller out of reset. A typical case is the Rockchip BootROM in USB download (Maskrom) mode on RK3576: it leaves USB3OTG0 active so that it can talk to the host over USB 2.0, which means the DWC3 wrapper is clocked and out of reset by the time U-Boot probes it. The wrapper then keeps driving phy_reset / phy_powerdown / phy_rate / etc. into the USBDP combo PHY's PMA while rk3588_udphy_init() tries to bring up the LCPLL, which never reaches AFC/LOCK_DONE and times out with: rockchip_udphy phy@2b010000: cmn ana lcpll lock timeout rockchip_udphy phy@2b010000: failed to init usbdp combophy Linux's dwc3-of-simple deasserts the glue resets and enables the glue clocks in its own probe, and only then calls of_platform_populate(), so the dwc3 child (and therefore phy_init()) cannot run until the wrapper is in a known state. Mirror that ordering here by moving the clk/reset init ahead of generic_phy_init(). This is a prerequisite for any per-compatible "pulse the reset before deasserting it" fix in dwc3_glue_reset_init() to actually take effect on platforms where the controller may be left running by an earlier boot stage. With the old ordering, such a pulse would happen after the PHY had already failed its LCPLL poll and would have no effect. No functional change is expected on platforms where the controller is already in reset at probe time. Signed-off-by: Alexey Charkov --- drivers/usb/dwc3/dwc3-generic.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/drivers/usb/dwc3/dwc3-generic.c b/drivers/usb/dwc3/dwc3-generic.c index 2356b3bc0aa..29844041a41 100644 --- a/drivers/usb/dwc3/dwc3-generic.c +++ b/drivers/usb/dwc3/dwc3-generic.c @@ -645,16 +645,6 @@ int dwc3_glue_probe(struct udevice *dev) int ret; struct phy phy; - ret = generic_phy_get_by_name(dev, "usb3-phy", &phy); - if (!ret) { - ret = generic_phy_init(&phy); - if (ret) - return ret; - } else if (ret != -ENOENT && ret != -ENODATA) { - debug("could not get phy (err %d)\n", ret); - return ret; - } - glue->regs = dev_read_addr_size_index(dev, 0, &glue->size); ret = dwc3_glue_clk_init(dev, glue); @@ -671,6 +661,16 @@ int dwc3_glue_probe(struct udevice *dev) return ret; } + ret = generic_phy_get_by_name(dev, "usb3-phy", &phy); + if (!ret) { + ret = generic_phy_init(&phy); + if (ret) + return ret; + } else if (ret != -ENOENT && ret != -ENODATA) { + debug("could not get phy (err %d)\n", ret); + return ret; + } + device_find_first_child(dev, &child); if (!child) return 0; From 1e8a6655b0b4ef2afc28c37b107020451acc571c Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 8 May 2026 13:56:00 +0400 Subject: [PATCH 27/78] usb: dwc3-generic: pulse the glue reset line on probe for all variants dwc3_glue_reset_init() currently only calls reset_deassert_bulk() on the resets it has just acquired (with a pre-existing assert+udelay exception for "qcom,dwc3"). If the controller has already been taken out of reset by an earlier boot stage, deasserting an already-deasserted reset is a no-op, and the glue probe leaves whatever state that stage left behind in place. This becomes a problem when the controller has been actively used before U-Boot runs. For example, on Rockchip RK3576 the BootROM keeps USB3OTG0 powered up and clocked while it talks to the host in USB download (Maskrom) mode. With the controller still alive on its PIPE interface, the subsequent USBDP combo PHY bring-up cannot reliably get its LCPLL to lock and the PHY .init callback fails with: rockchip_udphy phy@2b010000: cmn ana lcpll lock timeout Drop the qcom-only special case and always assert the reset, wait briefly, then deassert it. A bulk-assert followed by a small delay and a bulk-deassert is harmless on platforms where the controller was already in reset (the reset just gets re-pulsed before the rest of the glue runs), and forces the controller into a known state on platforms where it was not. This relies on the preceding reordering of dwc3_glue_probe() so that the glue resets are toggled before generic_phy_init() runs; otherwise the PHY would already have observed the stale controller state and failed before this code is reached. Closes: https://lore.kernel.org/u-boot/CAKTNdwGo434ShEsP=e=uUAbJzVxfQPPfNPeTxOOqwbuoCyzRjw@mail.gmail.com/ Signed-off-by: Alexey Charkov --- drivers/usb/dwc3/dwc3-generic.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/drivers/usb/dwc3/dwc3-generic.c b/drivers/usb/dwc3/dwc3-generic.c index 29844041a41..84b983a5dae 100644 --- a/drivers/usb/dwc3/dwc3-generic.c +++ b/drivers/usb/dwc3/dwc3-generic.c @@ -597,14 +597,12 @@ static int dwc3_glue_reset_init(struct udevice *dev, else if (ret) return ret; - if (device_is_compatible(dev, "qcom,dwc3")) { - reset_assert_bulk(&glue->resets); - /* We should wait at least 6 sleep clock cycles, that's - * (6 / 32764) * 1000000 ~= 200us. But some platforms - * have slower sleep clocks so we'll play it safe. - */ - udelay(500); - } + reset_assert_bulk(&glue->resets); + /* We should wait at least 6 sleep clock cycles, that's + * (6 / 32764) * 1000000 ~= 200us. But some platforms + * have slower sleep clocks so we'll play it safe. + */ + udelay(500); ret = reset_deassert_bulk(&glue->resets); if (ret) { reset_release_bulk(&glue->resets); From e64f59a47416a90e128e3f3ff20bd4abdd17c104 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 7 May 2026 13:33:26 +0400 Subject: [PATCH 28/78] pxe_utils: fix W=1 kernel-doc warnings Add the missing parameter and enum value descriptions reported by scripts/kernel-doc when building with W=1: - get_relfile(): document @type - get_pxelinux_path(): document @pxefile_addr_r - enum lex_state: document L_NORMAL, L_KEYWORD, L_SLITERAL - get_token(): document @t and @state No functional change. Signed-off-by: Alexey Charkov --- boot/pxe_utils.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/boot/pxe_utils.c b/boot/pxe_utils.c index 8c1310dabeb..f856a2fc143 100644 --- a/boot/pxe_utils.c +++ b/boot/pxe_utils.c @@ -95,6 +95,7 @@ int format_mac_pxe(char *outbuf, size_t outbuf_len) * @ctx: PXE context * @file_path: File path to read (relative to the PXE file) * @file_addr: Address to load file to + * @type: Image type used to record the loaded file in the bootflow * @filesizep: If not NULL, returns the file size in bytes * Returns 1 for success, or < 0 on error */ @@ -162,6 +163,7 @@ int get_pxe_file(struct pxe_context *ctx, const char *file_path, * * @ctx: PXE context * @file: Filename to process (relative to pxelinux.cfg/) + * @pxefile_addr_r: Address to load the file to * Returns 1 for success, -ENAMETOOLONG if the resulting path is too long. * or other value < 0 on other error */ @@ -914,6 +916,13 @@ static const struct token keywords[] = { * Since pxe(linux) files don't have a token to identify the start of a * literal, we have to keep track of when we're in a state where a literal is * expected vs when we're in a state a keyword is expected. + * + * @L_NORMAL: Outside any specific lexical context; whitespace is skipped + * and the next non-blank token determines what is read + * @L_KEYWORD: A keyword is expected; the next word is matched against the + * keyword table and tagged with the matching token type + * @L_SLITERAL: A string literal is expected; characters are read up to the + * end of the line */ enum lex_state { L_NORMAL = 0, @@ -1018,6 +1027,9 @@ static void get_keyword(struct token *t) * * @p: Points to a pointer to the current position in the input being processed. * Updated to point at the first character after the current token + * @t: Token to populate with the type and value of what was read + * @state: Current lexer state, controlling whether a keyword or a string + * literal is expected next */ static void get_token(char **p, struct token *t, enum lex_state state) { From 209d5062870ce98d4a2e0c2b12fe6df682f1d27d Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 7 May 2026 13:04:47 +0400 Subject: [PATCH 29/78] pxe_utils: accept "options" as synonym for "append" The Boot Loader Specification [1] type #2 entry files use the keyword "options" for the kernel command line, which corresponds to extlinux's "append". Recognising it here lets the existing pxelinux parser ingest BLS entries unchanged, paving the way for a BLS bootmeth that reuses the parser instead of duplicating it. No effect on existing extlinux/pxelinux files: "options" is not a valid keyword in those formats, so it cannot collide with prior usage. [1] https://uapi-group.org/specifications/specs/boot_loader_specification/ Signed-off-by: Alexey Charkov --- boot/pxe_utils.c | 1 + 1 file changed, 1 insertion(+) diff --git a/boot/pxe_utils.c b/boot/pxe_utils.c index f856a2fc143..02bd43ac46e 100644 --- a/boot/pxe_utils.c +++ b/boot/pxe_utils.c @@ -894,6 +894,7 @@ static const struct token keywords[] = { {"linux", T_LINUX}, {"localboot", T_LOCALBOOT}, {"append", T_APPEND}, + {"options", T_APPEND}, {"initrd", T_INITRD}, {"include", T_INCLUDE}, {"devicetree", T_FDT}, From 192a4ca27883417d361b1dd31f943ce269198606 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 7 May 2026 13:13:01 +0400 Subject: [PATCH 30/78] pxe_utils: extract per-entry key parsing into parse_label_keys() Split the body of parse_label() into a standalone parse_label_keys() helper that walks key/value lines and populates a pre-existing struct pxe_label. parse_label() becomes a thin wrapper that creates the label, reads its name, attaches it to the menu, and delegates. This is a pure refactor: the new helper contains the original loop verbatim, with the local variable declarations moved to its scope. No call sites or behaviour change. A subsequent change will export this helper so callers parsing formats that lack a 'label' header (notably Boot Loader Specification type #2 entries) can populate a label directly from a file body without duplicating the parser. Signed-off-by: Alexey Charkov --- boot/pxe_utils.c | 58 ++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/boot/pxe_utils.c b/boot/pxe_utils.c index 02bd43ac46e..2cc9c1e8aeb 100644 --- a/boot/pxe_utils.c +++ b/boot/pxe_utils.c @@ -1288,34 +1288,20 @@ static int parse_label_kernel(char **c, struct pxe_label *label) } /* - * Parses a label and adds it to the list of labels for a menu. - * - * A label ends when we either get to the end of a file, or - * get some input we otherwise don't have a handler defined - * for. + * Parse the body of a label: the sequence of key/value lines that follow + * the 'label NAME' header. Stops at end-of-file or at a token that doesn't + * belong inside a label (which is pushed back so the caller can handle it). * + * Returns 1 on success, < 0 on error. */ -static int parse_label(char **c, struct pxe_menu *cfg) +static int parse_label_keys(char **c, struct pxe_menu *cfg, + struct pxe_label *label) { struct token t; + char *s; int len; - char *s = *c; - struct pxe_label *label; int err; - label = label_create(); - if (!label) - return -ENOMEM; - - err = parse_sliteral(c, &label->name); - if (err < 0) { - printf("Expected label name: %.*s\n", (int)(*c - s), s); - label_destroy(label); - return -EINVAL; - } - - list_add_tail(&label->list, &cfg->labels); - while (1) { s = *c; get_token(c, &t, L_KEYWORD); @@ -1397,6 +1383,36 @@ static int parse_label(char **c, struct pxe_menu *cfg) } } +/* + * Parses a label and adds it to the list of labels for a menu. + * + * A label ends when we either get to the end of a file, or + * get some input we otherwise don't have a handler defined + * for. + * + */ +static int parse_label(char **c, struct pxe_menu *cfg) +{ + char *s = *c; + struct pxe_label *label; + int err; + + label = label_create(); + if (!label) + return -ENOMEM; + + err = parse_sliteral(c, &label->name); + if (err < 0) { + printf("Expected label name: %.*s\n", (int)(*c - s), s); + label_destroy(label); + return -EINVAL; + } + + list_add_tail(&label->list, &cfg->labels); + + return parse_label_keys(c, cfg, label); +} + /* * This 16 comes from the limit pxelinux imposes on nested includes. * From 7c3f61e3e0693fa3751ce2cc496a2839e3ddbd4f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 7 May 2026 13:19:28 +0400 Subject: [PATCH 31/78] pxe_utils: export per-entry label helpers Drop the static qualifier from label_create(), label_destroy(), parse_label_keys() and label_boot(), and declare them in to make them reusable. The intended consumer is a Boot Loader Specification bootmeth, where each on-disk file under loader/entries/ is a single entry with no 'label' header and no menu to traverse: the bootmeth derives the label name from the filename, hands the file body to parse_label_keys(), and invokes label_boot() directly. No behaviour change: the implementations are unchanged and existing in-tree callers in pxe_utils.c continue to use them as before. Signed-off-by: Alexey Charkov --- boot/pxe_utils.c | 37 ++++-------------------------- include/pxe_utils.h | 56 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/boot/pxe_utils.c b/boot/pxe_utils.c index 2cc9c1e8aeb..ced27129b35 100644 --- a/boot/pxe_utils.c +++ b/boot/pxe_utils.c @@ -218,15 +218,7 @@ static int get_relfile_envaddr(struct pxe_context *ctx, const char *file_path, return get_relfile(ctx, file_path, file_addr, type, filesizep); } -/** - * label_create() - crate a new PXE label - * - * Allocates memory for and initializes a pxe_label. This uses malloc, so the - * result must be free()'d to reclaim the memory. - * - * Returns a pointer to the label, or NULL if out of memory - */ -static struct pxe_label *label_create(void) +struct pxe_label *label_create(void) { struct pxe_label *label; @@ -239,20 +231,7 @@ static struct pxe_label *label_create(void) return label; } -/** - * label_destroy() - free the memory used by a pxe_label - * - * This frees @label itself as well as memory used by its name, - * kernel, config, append, initrd, fdt, fdtdir and fdtoverlay members, if - * they're non-NULL. - * - * So - be sure to only use dynamically allocated memory for the members of - * the pxe_label struct, unless you want to clean it up first. These are - * currently only created by the pxe file parsing code. - * - * @label: Label to free - */ -static void label_destroy(struct pxe_label *label) +void label_destroy(struct pxe_label *label) { free(label->name); free(label->kernel_label); @@ -542,7 +521,7 @@ static void label_boot_extension(struct pxe_context *ctx, * Returns does not return on success, otherwise returns 0 if a localboot * label was processed, or 1 on error */ -static int label_boot(struct pxe_context *ctx, struct pxe_label *label) +int label_boot(struct pxe_context *ctx, struct pxe_label *label) { char *bootm_argv[] = { "bootm", NULL, NULL, NULL, NULL }; char *zboot_argv[] = { "zboot", NULL, "0", NULL, NULL }; @@ -1287,15 +1266,7 @@ static int parse_label_kernel(char **c, struct pxe_label *label) return 1; } -/* - * Parse the body of a label: the sequence of key/value lines that follow - * the 'label NAME' header. Stops at end-of-file or at a token that doesn't - * belong inside a label (which is pushed back so the caller can handle it). - * - * Returns 1 on success, < 0 on error. - */ -static int parse_label_keys(char **c, struct pxe_menu *cfg, - struct pxe_label *label) +int parse_label_keys(char **c, struct pxe_menu *cfg, struct pxe_label *label) { struct token t; char *s; diff --git a/include/pxe_utils.h b/include/pxe_utils.h index 0378f2889f7..e639e59e5dc 100644 --- a/include/pxe_utils.h +++ b/include/pxe_utils.h @@ -202,6 +202,62 @@ void handle_pxe_menu(struct pxe_context *ctx, struct pxe_menu *cfg); */ struct pxe_menu *parse_pxefile(struct pxe_context *ctx, ulong menucfg); +/** + * label_create() - Allocate and zero-initialise a struct pxe_label + * + * The result must be freed with label_destroy(). + * + * Return: pointer to the new label, or NULL if out of memory + */ +struct pxe_label *label_create(void); + +/** + * label_destroy() - Free a struct pxe_label and its string members + * + * Frees @label as well as memory used by its name, kernel, config, append, + * initrd, fdt, fdtdir and fdtoverlays members, if they are non-NULL. All + * such members must therefore reference malloc()'d memory. + * + * @label: Label to free + */ +void label_destroy(struct pxe_label *label); + +/** + * parse_label_keys() - Parse the body of a label + * + * Walks the sequence of key/value lines that follow a 'label NAME' header, + * populating @label. Stops at end-of-file or at a token that does not + * belong inside a label (which is pushed back so the caller can handle it). + * + * This is exported so that callers handling formats which lack a 'label' + * header (e.g. Boot Loader Specification type #2 entry files) can populate + * a pre-created label directly from a file body. + * + * @c: Pointer to the cursor into the file being parsed; updated on return + * @cfg: Menu the label belongs to (used for 'menu default' bookkeeping) + * @label: Label to populate; must already be allocated and (when called for + * a file that has a 'label' header) attached to @cfg->labels + * Return: 1 on success, < 0 on error + */ +int parse_label_keys(char **c, struct pxe_menu *cfg, struct pxe_label *label); + +/** + * label_boot() - Boot according to the contents of a single pxe_label + * + * On success this function does not return; on failure it returns to + * let the caller try a different label or surface an error. + * + * The kernel is staged at $kernel_addr_r, an optional initrd at + * $ramdisk_addr_r, and an optional FDT at $fdt_addr_r. If the label + * has an 'append' string it overwrites $bootargs. + * + * @ctx: PXE context + * @label: Label to boot + * Return: does not return on success; 0 if a 'localboot' label was + * processed; 1 on error + */ +int label_boot(struct pxe_context *ctx, struct pxe_label *label); + /** * format_mac_pxe() - Convert a MAC address to PXE format * From c56c1b2887dcf2906a1494d0f78b503ac8f9889a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 7 May 2026 14:17:24 +0400 Subject: [PATCH 32/78] pxe_utils: optionally ignore unknown keys in parse_label_keys() The parser currently treats any keyword it does not recognise inside a label body as the end of that label, pushing the token back so the caller can dispatch it as menu-level input. That is correct for extlinux/pxelinux, where the only thing legitimately following a label body is another 'label' (or a top-level 'menu ...') line. Other formats that share enough syntax to reuse this parser have different rules. The Boot Loader Specification, in particular, lists a number of entry-level keys (title, version, sort-key, machine-id, architecture, ...) that this parser knows nothing about, and the spec explicitly requires implementations to silently ignore unrecognised keys rather than treat them as a structural boundary. Add an ignore_unknown flag to parse_label_keys(): when set, the default switch case consumes the rest of the offending line via eol_or_eof() and continues parsing instead of returning. The existing extlinux caller passes false and so is unaffected. Signed-off-by: Alexey Charkov --- boot/pxe_utils.c | 29 +++++++++++++++++++++++++++-- include/pxe_utils.h | 8 +++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/boot/pxe_utils.c b/boot/pxe_utils.c index ced27129b35..3f2c7f69bdb 100644 --- a/boot/pxe_utils.c +++ b/boot/pxe_utils.c @@ -1266,7 +1266,8 @@ static int parse_label_kernel(char **c, struct pxe_label *label) return 1; } -int parse_label_keys(char **c, struct pxe_menu *cfg, struct pxe_label *label) +int parse_label_keys(char **c, struct pxe_menu *cfg, struct pxe_label *label, + bool ignore_unknown) { struct token t; char *s; @@ -1339,7 +1340,31 @@ int parse_label_keys(char **c, struct pxe_menu *cfg, struct pxe_label *label) case T_EOL: break; + case T_EOF: + if (ignore_unknown) { + /* + * BLS-style callers parse a standalone label + * body, so there is no outer context to push + * T_EOF back into — stop cleanly here. + */ + return 1; + } + /* + * For pxelinux/extlinux, fall through so the default + * case pushes T_EOF back for the top-level parser. + */ + fallthrough; default: + if (ignore_unknown) { + /* + * Skip the rest of the line and keep going. + * Used for formats like the Boot Loader + * Specification, where the spec mandates that + * unknown keys must be silently ignored. + */ + eol_or_eof(c); + break; + } /* * put the token back! we don't want it - it's the end * of a label and whatever token this is, it's @@ -1381,7 +1406,7 @@ static int parse_label(char **c, struct pxe_menu *cfg) list_add_tail(&label->list, &cfg->labels); - return parse_label_keys(c, cfg, label); + return parse_label_keys(c, cfg, label, false); } /* diff --git a/include/pxe_utils.h b/include/pxe_utils.h index e639e59e5dc..653e1a7d866 100644 --- a/include/pxe_utils.h +++ b/include/pxe_utils.h @@ -237,9 +237,15 @@ void label_destroy(struct pxe_label *label); * @cfg: Menu the label belongs to (used for 'menu default' bookkeeping) * @label: Label to populate; must already be allocated and (when called for * a file that has a 'label' header) attached to @cfg->labels + * @ignore_unknown: If true, silently skip unknown keys (and consume the + * rest of their lines) instead of stopping. This matches the Boot + * Loader Specification's requirement that unknown keys be ignored. + * If false (extlinux/pxelinux behaviour), an unknown token is pushed + * back so the caller can treat it as the start of the next label. * Return: 1 on success, < 0 on error */ -int parse_label_keys(char **c, struct pxe_menu *cfg, struct pxe_label *label); +int parse_label_keys(char **c, struct pxe_menu *cfg, struct pxe_label *label, + bool ignore_unknown); /** * label_boot() - Boot according to the contents of a single pxe_label From 4650d71deb09b21cfe83fd7098b175fb9ade4ca3 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 7 May 2026 14:31:04 +0400 Subject: [PATCH 33/78] pxe_utils: accept "title" inside a label as a synonym for "menu label" The parser already understands the "title" keyword at the menu level (via "menu title"). Boot Loader Specification [1] type #2 entry files use a bare "title" line at the entry level to give the human-readable name of the entry, the closest extlinux equivalent of which is "menu label" inside a label body. Make parse_label_keys() honour "title" the same way it honours "menu label", populating label->menu. This lets BLS entries surface their pretty name through the existing label->menu plumbing without the BLS bootmeth (or any other caller) having to special-case it. No effect on existing extlinux/pxelinux files: those use "menu label" inside a label, not bare "title", so this change is purely additive. [1] https://uapi-group.org/specifications/specs/boot_loader_specification/ Signed-off-by: Alexey Charkov --- boot/pxe_utils.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/boot/pxe_utils.c b/boot/pxe_utils.c index 3f2c7f69bdb..5e98c9df565 100644 --- a/boot/pxe_utils.c +++ b/boot/pxe_utils.c @@ -1284,6 +1284,19 @@ int parse_label_keys(char **c, struct pxe_menu *cfg, struct pxe_label *label, err = parse_label_menu(c, cfg, label); break; + case T_TITLE: + /* + * Equivalent to 'menu label' inside a label body. + * Boot Loader Specification entries use a bare + * 'title' line for the human-readable name; honour + * it here so the existing parser handles BLS files + * natively. extlinux/pxelinux files conventionally + * use 'menu label' instead, so this is additive. + */ + if (!label->menu) + err = parse_sliteral(c, &label->menu); + break; + case T_KERNEL: case T_LINUX: err = parse_label_kernel(c, label); From 73370acb13bb74102e546b1daf727f141fa863e9 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 7 May 2026 14:44:21 +0400 Subject: [PATCH 34/78] boot: add a minimal bootmeth for the Boot Loader Specification Add a bootmeth that finds and boots Boot Loader Specification (BLS) type #1 entry files [1]. On each block-device partition it scans, the bootmeth looks for files matching 'loader/entries/*.conf' (where comes from bootstd_get_prefixes(), typically '/' and '/boot/'), picks the highest-sorting filename, parses it, and exposes it as a bootflow. Implementation reuses the existing pxelinux infrastructure. For now the entry chosen on a partition is purely the lexicographic maximum of *.conf filenames; sort-key / version field handling (spec-mandated tiebreakers) and boot-counting (the '+TRIES_LEFT' filename suffix) are left as TODOs. Likewise, only the top-sorted entry is surfaced because the bootstd framework currently allows one bootflow per (bootmeth, partition); exposing every discovered entry will require a framework extension. Type #2 BLS (drop-in directory of EFI binaries) is out of scope here; existing EFI bootmeths cover that use case. [1] https://uapi-group.org/specifications/specs/boot_loader_specification/ Signed-off-by: Alexey Charkov --- boot/Kconfig | 17 +++ boot/Makefile | 1 + boot/bootmeth_bls.c | 333 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 boot/bootmeth_bls.c diff --git a/boot/Kconfig b/boot/Kconfig index c67dc0ba493..c6f18aa2665 100644 --- a/boot/Kconfig +++ b/boot/Kconfig @@ -651,6 +651,23 @@ config BOOTMETH_EXTLINUX_PXE This provides a way to try out standard boot on an existing boot flow. +config BOOTMETH_BLS + bool "Bootdev support for Boot Loader Specification entries" + select PXE_UTILS + default y + help + Enables support for booting via Boot Loader Specification type #1 + entry files. The bootmeth scans each filesystem it finds for files + matching 'loader/entries/*.conf' (or '/boot/loader/entries/*.conf') + and boots the highest-sorting entry. + + The specification is here: + + https://uapi-group.org/specifications/specs/boot_loader_specification/ + + Type #2 BLS (drop-in directory of EFI binaries) is not handled here; + use BOOTMETH_EFI_BOOTMGR for that. + config BOOTMETH_EFILOADER bool "Bootdev support for EFI boot" depends on EFI_BINARY_EXEC diff --git a/boot/Makefile b/boot/Makefile index 7fb56e7ef37..0ce6fd1cd05 100644 --- a/boot/Makefile +++ b/boot/Makefile @@ -30,6 +30,7 @@ obj-$(CONFIG_$(PHASE_)BOOTSTD_PROG) += prog_boot.o obj-$(CONFIG_$(PHASE_)BOOTMETH_EXTLINUX) += bootmeth_extlinux.o obj-$(CONFIG_$(PHASE_)BOOTMETH_EXTLINUX_PXE) += bootmeth_pxe.o +obj-$(CONFIG_$(PHASE_)BOOTMETH_BLS) += bootmeth_bls.o obj-$(CONFIG_$(PHASE_)BOOTMETH_EFILOADER) += bootmeth_efi.o obj-$(CONFIG_$(PHASE_)BOOTMETH_CROS) += bootm.o bootm_os.o bootmeth_cros.o obj-$(CONFIG_$(PHASE_)BOOTMETH_QFW) += bootmeth_qfw.o diff --git a/boot/bootmeth_bls.c b/boot/bootmeth_bls.c new file mode 100644 index 00000000000..3df4f20a11d --- /dev/null +++ b/boot/bootmeth_bls.c @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Bootmethod for the Boot Loader Specification (type #1 entry files) + * + * Reuses the pxelinux parser/boot path: each on-disk entry is read into a + * struct pxe_label via parse_label_keys() and booted via label_boot(). + * + * Spec: https://uapi-group.org/specifications/specs/boot_loader_specification/ + * + * TODO: a partition typically holds several BLS entries, but the bootstd + * framework currently allows only one bootflow per (bootmeth, partition) + * pair, so this bootmeth surfaces only the highest-sorting entry. Once the + * framework grows a way for a bootmeth to emit multiple bootflows from a + * single partition, this should expose every discovered entry so the user + * can pick from the standard 'bootflow menu' UI rather than be limited to + * the default pick. + */ + +#define LOG_CATEGORY UCLASS_BOOTSTD + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BLS_DIR "loader/entries" +#define BLS_SUFFIX ".conf" + +static int bls_check(struct udevice *dev, struct bootflow_iter *iter) +{ + int ret; + + /* This only works on block devices */ + ret = bootflow_iter_check_blk(iter); + if (ret) + return log_msg_ret("blk", ret); + + return 0; +} + +static int bls_getfile(struct pxe_context *ctx, const char *file_path, + char *file_addr, enum bootflow_img_t type, ulong *sizep) +{ + struct extlinux_info *info = ctx->userdata; + ulong addr; + int ret; + + addr = simple_strtoul(file_addr, NULL, 16); + + /* Allow up to 1GB */ + *sizep = 1 << 30; + ret = bootmeth_read_file(info->dev, info->bflow, file_path, addr, + type, sizep); + if (ret) + return log_msg_ret("read", ret); + + return 0; +} + +/** + * bls_pick_entry() - Find the highest-sorting *.conf across bootstd prefixes + * + * Walks ``/loader/entries/`` for each prefix in @prefixes and + * returns the lexicographically maximum full path seen. + * + * The spec leaves ordering between prefixes unspecified; comparing full + * paths is a deterministic-and-cheap stand-in. + * + * The Boot Loader Specification says entries should be sorted by sort-key + * (descending), then version (descending), then filename (descending). For + * the time being only the filename criterion is implemented, which is + * sufficient for most distros that encode kernel version into the filename. + * + * TODO: implement proper spec-compliant ordering. That requires reading + * each candidate entry, parsing its 'sort-key' and 'version' fields (the + * latter compared with strverscmp()-style logic), and only falling back to + * filename order when those tie. + * + * @prefixes: NULL-terminated array of bootstd prefixes to search + * @desc: Block descriptor (used to re-mount per prefix) + * @bflow: Bootflow being populated (used to re-mount per prefix) + * @fullp: Returns the chosen full path (allocated), or NULL if none + * Return: 0 on success, -ENOENT if no entry was found, < 0 on other error + */ +static int bls_pick_entry(const char *const *prefixes, struct blk_desc *desc, + struct bootflow *bflow, char **fullp) +{ + char dirpath[256]; + char *best = NULL; + int ret; + int i; + + for (i = 0; prefixes && prefixes[i]; i++) { + struct fs_dir_stream *dirs; + struct fs_dirent *dent; + + /* fs_closedir() below resets the global fs_type. */ + ret = bootmeth_setup_fs(bflow, desc); + if (ret) { + free(best); + return log_msg_ret("fs", ret); + } + + snprintf(dirpath, sizeof(dirpath), "%s%s", + prefixes[i], BLS_DIR); + dirs = fs_opendir(dirpath); + if (!dirs) + continue; + + while ((dent = fs_readdir(dirs))) { + size_t len = strlen(dent->name); + char *full; + + if (dent->type != FS_DT_REG) + continue; + if (len <= strlen(BLS_SUFFIX)) + continue; + if (strcmp(dent->name + len - strlen(BLS_SUFFIX), + BLS_SUFFIX)) + continue; + + full = malloc(strlen(dirpath) + 1 + len + 1); + if (!full) { + free(best); + fs_closedir(dirs); + return -ENOMEM; + } + sprintf(full, "%s/%s", dirpath, dent->name); + + if (!best || strcmp(full, best) > 0) { + free(best); + best = full; + } else { + free(full); + } + } + fs_closedir(dirs); + } + + if (!best) + return -ENOENT; + + *fullp = best; + + return 0; +} + +/* + * TODO: BLS entry filenames may carry a boot-counter suffix of the form + * '+TRIES_LEFT[-TRIES_DONE]' immediately before the .conf extension (see + * the spec section on "Boot counting"). When that is implemented, this + * bootmeth should: + * - parse and strip the suffix from the displayed entry name, + * - skip entries whose TRIES_LEFT has reached zero, + * - decrement TRIES_LEFT (renaming the file) on each boot attempt. + * For now the suffix is left intact in the entry name and ignored. + */ +static int bls_read_bootflow(struct udevice *dev, struct bootflow *bflow) +{ + struct blk_desc *desc; + const char *const *prefixes; + struct udevice *bootstd; + struct pxe_label *label = NULL; + struct pxe_menu scratch = {}; + char *fpath = NULL; + const char *base; + char *body; + int ret; + + ret = uclass_first_device_err(UCLASS_BOOTSTD, &bootstd); + if (ret) + return log_msg_ret("std", ret); + + /* We require a partitioned block device */ + if (!bflow->blk || !bflow->part) + return -ENOENT; + + desc = dev_get_uclass_plat(bflow->blk); + prefixes = bootstd_get_prefixes(bootstd); + + ret = bls_pick_entry(prefixes, desc, bflow, &fpath); + if (ret) + return log_msg_ret("scan", ret); + + base = strrchr(fpath, '/'); + base = base ? base + 1 : fpath; + + /* + * bls_pick_entry() finished with fs_closedir(), which resets the + * global fs_type. Re-mount the partition so bootmeth_try_file()'s + * internal fs_size() call can find the right filesystem driver. + */ + ret = bootmeth_setup_fs(bflow, desc); + if (ret) { + free(fpath); + return log_msg_ret("fs", ret); + } + + ret = bootmeth_try_file(bflow, desc, NULL, fpath); + if (ret) { + free(fpath); + return log_msg_ret("try", ret); + } + + ret = bootmeth_alloc_file(bflow, SZ_64K, 1, BFI_EXTLINUX_CFG); + if (ret) { + free(fpath); + return log_msg_ret("read", ret); + } + + label = label_create(); + if (!label) { + ret = -ENOMEM; + goto err; + } + + /* + * BLS files have no 'label NAME' header — derive the label name from + * the basename (without the .conf suffix) so messages are useful. + */ + label->name = strndup(base, strlen(base) - strlen(BLS_SUFFIX)); + if (!label->name) { + ret = -ENOMEM; + goto err; + } + + body = bflow->buf; + ret = parse_label_keys(&body, &scratch, label, true); + if (ret < 0) + goto err; + + /* + * scratch is only used to give parse_label_keys() somewhere safe to + * stash menu-level state (e.g. a stray 'menu default' line). BLS + * entry files don't contain such lines but defensively free anything + * that did get allocated. + */ + free(scratch.default_label); + + /* + * label->menu was populated either from a BLS 'title' line (the + * spec-mandated human-readable name) or from a stray 'menu label' + * the parser may have picked up. Fall back to the filename-derived + * label name when neither is present. + */ + bflow->os_name = strdup(label->menu ? label->menu : label->name); + if (!bflow->os_name) { + ret = -ENOMEM; + goto err; + } + + bflow->bootmeth_priv = label; + free(fpath); + + return 0; + +err: + if (label) + label_destroy(label); + free(fpath); + return log_msg_ret("bls", ret); +} + +static int bls_boot(struct udevice *dev, struct bootflow *bflow) +{ + struct cmd_tbl cmdtp = {}; /* dummy */ + struct pxe_context ctx; + struct extlinux_info info; + struct pxe_label *label = bflow->bootmeth_priv; + int ret; + + if (!label) + return log_msg_ret("lbl", -ENOENT); + + info.dev = dev; + info.bflow = bflow; + + /* + * BLS paths are absolute relative to the filesystem root of the + * partition the entry lives on. allow_abs_path=true honours that; + * passing NULL as the bootfile keeps the prefix empty so absolute + * paths are not rebased. + */ + ret = pxe_setup_ctx(&ctx, &cmdtp, bls_getfile, &info, true, + NULL, false, false); + if (ret) + return log_msg_ret("ctx", -EINVAL); + + ret = label_boot(&ctx, label); + pxe_destroy_ctx(&ctx); + if (ret) + return log_msg_ret("boot", -EINVAL); + + return 0; +} + +static int bls_bootmeth_bind(struct udevice *dev) +{ + struct bootmeth_uc_plat *plat = dev_get_uclass_plat(dev); + + plat->desc = IS_ENABLED(CONFIG_BOOTSTD_FULL) ? + "Boot Loader Specification" : "bls"; + + return 0; +} + +static struct bootmeth_ops bls_bootmeth_ops = { + .check = bls_check, + .read_bootflow = bls_read_bootflow, + .read_file = bootmeth_common_read_file, + .boot = bls_boot, +}; + +static const struct udevice_id bls_bootmeth_ids[] = { + { .compatible = "u-boot,bls" }, + { } +}; + +U_BOOT_DRIVER(bootmeth_2bls) = { + .name = "bootmeth_bls", + .id = UCLASS_BOOTMETH, + .of_match = bls_bootmeth_ids, + .ops = &bls_bootmeth_ops, + .bind = bls_bootmeth_bind, +}; From 02476d6a6dc8e563e1c61e786d1d159a5b2ec114 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 25 May 2026 19:13:04 +0400 Subject: [PATCH 35/78] bootstd: add seq parameter to bootmeth read_bootflow op Add an integer 'seq' parameter to the bootmeth read_bootflow operation to pave way for bootmeths returning more than one entry per tuple of (method, partition, bootdev). If available entries are exhausted the bootmeths return -ENOENT, which terminates the iteration. No functional change. Signed-off-by: Alexey Charkov --- boot/bootdev-uclass.c | 2 +- boot/bootmeth-uclass.c | 7 ++++--- boot/bootmeth_android.c | 5 ++++- boot/bootmeth_bls.c | 5 ++++- boot/bootmeth_cros.c | 5 ++++- boot/bootmeth_efi.c | 5 ++++- boot/bootmeth_efi_mgr.c | 5 ++++- boot/bootmeth_extlinux.c | 5 ++++- boot/bootmeth_pxe.c | 5 ++++- boot/bootmeth_qfw.c | 5 ++++- boot/bootmeth_rauc.c | 5 ++++- boot/bootmeth_sandbox.c | 5 ++++- boot/bootmeth_script.c | 5 ++++- boot/vbe_abrec.h | 3 ++- boot/vbe_abrec_fw.c | 7 +++++-- boot/vbe_simple.c | 8 ++++++-- boot/vbe_simple.h | 3 ++- boot/vbe_simple_fw.c | 7 +++++-- drivers/misc/qfw.c | 2 +- fs/sandbox/host_bootdev.c | 2 +- include/bootmeth.h | 12 ++++++++++-- net/eth_bootdev.c | 2 +- 22 files changed, 82 insertions(+), 28 deletions(-) diff --git a/boot/bootdev-uclass.c b/boot/bootdev-uclass.c index 55e1a6c4e02..e921d33fb08 100644 --- a/boot/bootdev-uclass.c +++ b/boot/bootdev-uclass.c @@ -189,7 +189,7 @@ int bootdev_find_in_blk(struct udevice *dev, struct udevice *blk, } log_debug("method %s\n", bflow->method->name); - ret = bootmeth_read_bootflow(bflow->method, bflow); + ret = bootmeth_read_bootflow(bflow->method, bflow, 0); if (ret) return log_msg_ret("method", ret); diff --git a/boot/bootmeth-uclass.c b/boot/bootmeth-uclass.c index c841dd0d6d4..1fa5807bdf7 100644 --- a/boot/bootmeth-uclass.c +++ b/boot/bootmeth-uclass.c @@ -39,14 +39,15 @@ int bootmeth_check(struct udevice *dev, struct bootflow_iter *iter) return ops->check(dev, iter); } -int bootmeth_read_bootflow(struct udevice *dev, struct bootflow *bflow) +int bootmeth_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { const struct bootmeth_ops *ops = bootmeth_get_ops(dev); if (!ops->read_bootflow) return -ENOSYS; - return ops->read_bootflow(dev, bflow); + return ops->read_bootflow(dev, bflow, seq); } int bootmeth_set_bootflow(struct udevice *dev, struct bootflow *bflow, @@ -102,7 +103,7 @@ int bootmeth_get_bootflow(struct udevice *dev, struct bootflow *bflow) return -ENOSYS; bootflow_init(bflow, NULL, dev); - return ops->read_bootflow(dev, bflow); + return ops->read_bootflow(dev, bflow, 0); } int bootmeth_setup_iter_order(struct bootflow_iter *iter, bool include_global) diff --git a/boot/bootmeth_android.c b/boot/bootmeth_android.c index ec255b072af..ed5933eb9d3 100644 --- a/boot/bootmeth_android.c +++ b/boot/bootmeth_android.c @@ -208,8 +208,11 @@ static int configure_bootloader_version(struct bootflow *bflow) PLAIN_VERSION, false); } -static int android_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int android_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; struct blk_desc *desc = dev_get_uclass_plat(bflow->blk); struct disk_partition misc; struct android_priv *priv; diff --git a/boot/bootmeth_bls.c b/boot/bootmeth_bls.c index 3df4f20a11d..5859371d320 100644 --- a/boot/bootmeth_bls.c +++ b/boot/bootmeth_bls.c @@ -163,8 +163,11 @@ static int bls_pick_entry(const char *const *prefixes, struct blk_desc *desc, * - decrement TRIES_LEFT (renaming the file) on each boot attempt. * For now the suffix is left intact in the entry name and ignored. */ -static int bls_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int bls_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; struct blk_desc *desc; const char *const *prefixes; struct udevice *bootstd; diff --git a/boot/bootmeth_cros.c b/boot/bootmeth_cros.c index c7b862e512a..19b591a9799 100644 --- a/boot/bootmeth_cros.c +++ b/boot/bootmeth_cros.c @@ -362,8 +362,11 @@ static int cros_read_kernel(struct bootflow *bflow) return 0; } -static int cros_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int cros_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; const struct vb2_kernel_preamble *preamble; struct disk_partition info; struct vb2_keyblock *hdr; diff --git a/boot/bootmeth_efi.c b/boot/bootmeth_efi.c index e187dc39912..bb1d6c847d8 100644 --- a/boot/bootmeth_efi.c +++ b/boot/bootmeth_efi.c @@ -302,8 +302,11 @@ static int distro_efi_read_bootflow_net(struct bootflow *bflow) return 0; } -static int distro_efi_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int distro_efi_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; int ret; log_debug("dev='%s', part=%d\n", bflow->dev->name, bflow->part); diff --git a/boot/bootmeth_efi_mgr.c b/boot/bootmeth_efi_mgr.c index 6e70c36ad99..f453ecd9e6b 100644 --- a/boot/bootmeth_efi_mgr.c +++ b/boot/bootmeth_efi_mgr.c @@ -45,8 +45,11 @@ static int efi_mgr_check(struct udevice *dev, struct bootflow_iter *iter) return 0; } -static int efi_mgr_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int efi_mgr_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; struct efi_mgr_priv *priv = dev_get_priv(dev); efi_status_t ret; efi_uintn_t size; diff --git a/boot/bootmeth_extlinux.c b/boot/bootmeth_extlinux.c index 921d721a27b..b5ffa646ad1 100644 --- a/boot/bootmeth_extlinux.c +++ b/boot/bootmeth_extlinux.c @@ -132,8 +132,11 @@ static int extlinux_fill_info(struct bootflow *bflow) return 0; } -static int extlinux_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int extlinux_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; struct blk_desc *desc; const char *const *prefixes; struct udevice *bootstd; diff --git a/boot/bootmeth_pxe.c b/boot/bootmeth_pxe.c index faa8d729b15..38ad9bedccf 100644 --- a/boot/bootmeth_pxe.c +++ b/boot/bootmeth_pxe.c @@ -59,7 +59,7 @@ static int extlinux_pxe_check(struct udevice *dev, struct bootflow_iter *iter) } static int extlinux_pxe_read_bootflow(struct udevice *dev, - struct bootflow *bflow) + struct bootflow *bflow, int seq) { const char *addr_str; char fname[200]; @@ -69,6 +69,9 @@ static int extlinux_pxe_read_bootflow(struct udevice *dev, char *buf; int ret; + if (seq) + return -ENOENT; + addr_str = env_get("pxefile_addr_r"); if (!addr_str) return log_msg_ret("pxeb", -EPERM); diff --git a/boot/bootmeth_qfw.c b/boot/bootmeth_qfw.c index 028c2481583..2af15fba027 100644 --- a/boot/bootmeth_qfw.c +++ b/boot/bootmeth_qfw.c @@ -28,8 +28,11 @@ static int qfw_check(struct udevice *dev, struct bootflow_iter *iter) return -ENOTSUPP; } -static int qfw_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int qfw_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; struct udevice *qfw_dev = dev_get_parent(bflow->dev); ulong load, initrd; int ret; diff --git a/boot/bootmeth_rauc.c b/boot/bootmeth_rauc.c index d7b0c686ffe..f05d1c14a21 100644 --- a/boot/bootmeth_rauc.c +++ b/boot/bootmeth_rauc.c @@ -138,8 +138,11 @@ static int distro_rauc_scan_parts(struct bootflow *bflow) return -1; } -static int distro_rauc_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int distro_rauc_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; struct distro_rauc_priv *priv = NULL; int ret = 0; char *slot; diff --git a/boot/bootmeth_sandbox.c b/boot/bootmeth_sandbox.c index 92ba2e3f050..9ee05f5a755 100644 --- a/boot/bootmeth_sandbox.c +++ b/boot/bootmeth_sandbox.c @@ -18,8 +18,11 @@ static int sandbox_check(struct udevice *dev, struct bootflow_iter *iter) return 0; } -static int sandbox_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int sandbox_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; /* pretend we are ready */ bflow->state = BOOTFLOWST_READY; diff --git a/boot/bootmeth_script.c b/boot/bootmeth_script.c index cd50977cc40..bbccd48dbd8 100644 --- a/boot/bootmeth_script.c +++ b/boot/bootmeth_script.c @@ -152,8 +152,11 @@ static int script_read_bootflow_net(struct bootflow *bflow) return 0; } -static int script_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int script_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; const struct udevice *media = dev_get_parent(bflow->dev); struct udevice *bootstd; int ret; diff --git a/boot/vbe_abrec.h b/boot/vbe_abrec.h index 63c73297351..db0fb752327 100644 --- a/boot/vbe_abrec.h +++ b/boot/vbe_abrec.h @@ -75,7 +75,8 @@ struct abrec_state { * @bflow: Place to put the created bootflow, on success * @return 0 if OK, -ve on error */ -int abrec_read_bootflow_fw(struct udevice *dev, struct bootflow *bflow); +int abrec_read_bootflow_fw(struct udevice *dev, struct bootflow *bflow, + int seq); /** * vbe_simple_read_state() - Read the VBE simple state information diff --git a/boot/vbe_abrec_fw.c b/boot/vbe_abrec_fw.c index d52bd9ddff0..f48f13f156b 100644 --- a/boot/vbe_abrec_fw.c +++ b/boot/vbe_abrec_fw.c @@ -60,8 +60,11 @@ static const char *const pick_names[] = {"A", "B", "Recovery"}; * @blow: Place to put the created bootflow, on success * @return 0 if OK, -ve on error */ -int abrec_read_bootflow_fw(struct udevice *dev, struct bootflow *bflow) +int abrec_read_bootflow_fw(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; struct udevice *media = dev_get_parent(bflow->dev); struct udevice *meth = bflow->method; struct abrec_priv *priv = dev_get_priv(meth); @@ -225,7 +228,7 @@ static int abrec_load_from_image(struct spl_image_info *image, log_debug("bootdev %s\n", bdev->name); bootflow_init(&bflow, bdev, meth); - ret = bootmeth_read_bootflow(meth, &bflow); + ret = bootmeth_read_bootflow(meth, &bflow, 0); log_debug("\nfw ret=%d\n", ret); if (ret) return log_msg_ret("rd", ret); diff --git a/boot/vbe_simple.c b/boot/vbe_simple.c index c6766c532f2..5487d0d1478 100644 --- a/boot/vbe_simple.c +++ b/boot/vbe_simple.c @@ -81,13 +81,17 @@ static int vbe_simple_get_state_desc(struct udevice *dev, char *buf, return 0; } -static int vbe_simple_read_bootflow(struct udevice *dev, struct bootflow *bflow) +static int vbe_simple_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq) { int ret; + if (seq) + return -ENOENT; + if (CONFIG_IS_ENABLED(BOOTMETH_VBE_SIMPLE_FW)) { if (vbe_phase() == VBE_PHASE_FIRMWARE) { - ret = vbe_simple_read_bootflow_fw(dev, bflow); + ret = vbe_simple_read_bootflow_fw(dev, bflow, seq); if (ret) return log_msg_ret("fw", ret); return 0; diff --git a/boot/vbe_simple.h b/boot/vbe_simple.h index dc3f70052b0..3417e480cac 100644 --- a/boot/vbe_simple.h +++ b/boot/vbe_simple.h @@ -45,7 +45,8 @@ struct simple_state { * @blow: Place to put the created bootflow, on success * @return 0 if OK, -ve on error */ -int vbe_simple_read_bootflow_fw(struct udevice *dev, struct bootflow *bflow); +int vbe_simple_read_bootflow_fw(struct udevice *dev, struct bootflow *bflow, + int seq); /** * vbe_simple_read_state() - Read the VBE simple state information diff --git a/boot/vbe_simple_fw.c b/boot/vbe_simple_fw.c index cb5534fc731..9bc921dce8d 100644 --- a/boot/vbe_simple_fw.c +++ b/boot/vbe_simple_fw.c @@ -48,8 +48,11 @@ binman_sym_declare(ulong, vpl, size); * @blow: Place to put the created bootflow, on success * @return 0 if OK, -ve on error */ -int vbe_simple_read_bootflow_fw(struct udevice *dev, struct bootflow *bflow) +int vbe_simple_read_bootflow_fw(struct udevice *dev, struct bootflow *bflow, + int seq) { + if (seq) + return -ENOENT; struct udevice *media = dev_get_parent(bflow->dev); struct udevice *meth = bflow->method; struct simple_priv *priv = dev_get_priv(meth); @@ -113,7 +116,7 @@ static int simple_load_from_image(struct spl_image_info *image, log_debug("bootdev %s\n", bdev->name); bootflow_init(&bflow, bdev, meth); - ret = bootmeth_read_bootflow(meth, &bflow); + ret = bootmeth_read_bootflow(meth, &bflow, 0); log_debug("\nfw ret=%d\n", ret); if (ret) return log_msg_ret("rd", ret); diff --git a/drivers/misc/qfw.c b/drivers/misc/qfw.c index 8a11637ca7f..4ef26da452e 100644 --- a/drivers/misc/qfw.c +++ b/drivers/misc/qfw.c @@ -118,7 +118,7 @@ static int qfw_get_bootflow(struct udevice *dev, struct bootflow_iter *iter, return log_msg_ret("max", -ESHUTDOWN); log_debug("reading bootflow with method: %s\n", bflow->method->name); - ret = bootmeth_read_bootflow(bflow->method, bflow); + ret = bootmeth_read_bootflow(bflow->method, bflow, 0); if (ret) return log_msg_ret("method", ret); diff --git a/fs/sandbox/host_bootdev.c b/fs/sandbox/host_bootdev.c index 3f74972a9f8..e204c4b5314 100644 --- a/fs/sandbox/host_bootdev.c +++ b/fs/sandbox/host_bootdev.c @@ -31,7 +31,7 @@ static int host_get_bootflow(struct udevice *dev, struct bootflow_iter *iter, bflow->state = BOOTFLOWST_MEDIA; bflow->fs_type = FS_TYPE_SANDBOX; - ret = bootmeth_read_bootflow(bflow->method, bflow); + ret = bootmeth_read_bootflow(bflow->method, bflow, 0); if (ret) return log_msg_ret("method", ret); diff --git a/include/bootmeth.h b/include/bootmeth.h index 2a492dfd73a..78d08e8e88d 100644 --- a/include/bootmeth.h +++ b/include/bootmeth.h @@ -91,9 +91,14 @@ struct bootmeth_ops { * @dev: Bootmethod device to use * @bflow: On entry, provides dev, hwpart, part and method. * Returns updated bootflow if found + * @seq: Sub-entry index within (method, part, bootdev). 0 is the + * first/only entry. Bootmeths that produce a single bootflow per + * tuple should return -ENOENT for any non-zero @seq. Bootmeths + * that can produce multiple entries should return them in order + * starting at 0, and -ENOENT when @seq is past the last entry. * Return: 0 if OK, -ve on error */ - int (*read_bootflow)(struct udevice *dev, struct bootflow *bflow); + int (*read_bootflow)(struct udevice *dev, struct bootflow *bflow, int seq); /** * set_bootflow() - set the bootflow for a device @@ -220,9 +225,12 @@ int bootmeth_check(struct udevice *dev, struct bootflow_iter *iter); * @dev: Bootmethod device to check * @bflow: On entry, provides dev, hwpart, part and method. * Returns updated bootflow if found + * @seq: Sub-entry index within (method, part, bootdev). See the op + * documentation for semantics. * Return: 0 if OK, -ve on error */ -int bootmeth_read_bootflow(struct udevice *dev, struct bootflow *bflow); +int bootmeth_read_bootflow(struct udevice *dev, struct bootflow *bflow, + int seq); /** * bootmeth_set_bootflow() - set the bootflow for a device diff --git a/net/eth_bootdev.c b/net/eth_bootdev.c index b0fca6e8313..2b6870a222e 100644 --- a/net/eth_bootdev.c +++ b/net/eth_bootdev.c @@ -47,7 +47,7 @@ static int eth_get_bootflow(struct udevice *dev, struct bootflow_iter *iter, /* See distro_pxe_read_bootflow() for the standard impl of this */ log_debug("dhcp complete - reading bootflow with method '%s'\n", bflow->method->name); - ret = bootmeth_read_bootflow(bflow->method, bflow); + ret = bootmeth_read_bootflow(bflow->method, bflow, 0); log_debug("reading bootflow returned %d\n", ret); if (ret) return log_msg_ret("method", ret); From 0951347dfc51a83eb151531a55fdf15202d97c5c Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 25 May 2026 19:19:03 +0400 Subject: [PATCH 36/78] bootstd: add cur_subseq axis to the bootflow iterator Extend struct bootflow_iter with a new axis - cur_subseq - that indexes sub-entries within a single (method, partition, bootdev) tuple. The iterator now stays on the same tuple after a successful bootflow_check() and asks the bootmeth for cur_subseq + 1 on the next call, until the bootmeth returns -ENOENT (or any other error) to mark the tuple exhausted. This enables bootmeths to emit multiple bootflows per tuple, which in turn allows the use of a bootflow menu for selecting different boot entries from a single boot source. No behavior change for existing bootmeths. Signed-off-by: Alexey Charkov --- boot/bootdev-uclass.c | 8 ++++++-- boot/bootflow.c | 39 ++++++++++++++++++++++++++++++++++++++- include/bootflow.h | 5 +++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/boot/bootdev-uclass.c b/boot/bootdev-uclass.c index e921d33fb08..2b8de758f28 100644 --- a/boot/bootdev-uclass.c +++ b/boot/bootdev-uclass.c @@ -110,7 +110,11 @@ int bootdev_find_in_blk(struct udevice *dev, struct udevice *blk, snprintf(partstr, sizeof(partstr), "part_%x", iter->part); else strcpy(partstr, "whole"); - snprintf(name, sizeof(name), "%s.%s", dev->name, partstr); + if (iter->cur_subseq) + snprintf(name, sizeof(name), "%s.%s.%d", dev->name, partstr, + iter->cur_subseq); + else + snprintf(name, sizeof(name), "%s.%s", dev->name, partstr); bflow->name = strdup(name); if (!bflow->name) return log_msg_ret("name", -ENOMEM); @@ -189,7 +193,7 @@ int bootdev_find_in_blk(struct udevice *dev, struct udevice *blk, } log_debug("method %s\n", bflow->method->name); - ret = bootmeth_read_bootflow(bflow->method, bflow, 0); + ret = bootmeth_read_bootflow(bflow->method, bflow, iter->cur_subseq); if (ret) return log_msg_ret("method", ret); diff --git a/boot/bootflow.c b/boot/bootflow.c index d8a4a81a838..cd955657d37 100644 --- a/boot/bootflow.c +++ b/boot/bootflow.c @@ -25,6 +25,13 @@ static_assert(BOOTMETH_MAX_COUNT <= enum { BF_NO_MORE_PARTS = -ESHUTDOWN, BF_NO_MORE_DEVICES = -ENODEV, + /* + * Internal sentinel placed in iter->err when the last bootflow_check() + * succeeded. Tells iter_incr() to stay on the current + * (method, partition, bootdev) tuple and let the bootmeth produce the + * next sub-entry (cur_subseq + 1). Not exposed to bootmeths or callers. + */ + BF_TRY_NEXT_SEQ = -EAGAIN, }; static const char *const bootflow_img[BFI_COUNT - BFI_FIRST] = { @@ -300,6 +307,19 @@ static int iter_incr(struct bootflow_iter *iter) if (iter->err == BF_NO_MORE_DEVICES) return BF_NO_MORE_DEVICES; + /* + * The previous bootflow_check() succeeded and bumped cur_subseq. + * Stay on the same (method, part, bootdev) so the bootmeth can + * return its next sub-entry. + */ + if (iter->err == BF_TRY_NEXT_SEQ) { + iter->err = 0; + return 0; + } + + /* Leaving the current tuple - reset the sub-entry axis. */ + iter->cur_subseq = 0; + /* Get the next boothmethod */ for (iter->cur_method++; iter->cur_method < iter->num_methods; iter->cur_method++) { @@ -519,6 +539,13 @@ static int bootflow_check(struct bootflow_iter *iter, struct bootflow *bflow) if (!ret) { log_debug("Bootdev '%s' part %d method '%s': Found bootflow\n", dev->name, iter->part, iter->method->name); + /* + * Arrange for the next iter_incr() to retry the same tuple so + * the bootmeth can emit its next sub-entry. cur_subseq is + * advanced now so it reflects the index of the next call. + */ + iter->err = BF_TRY_NEXT_SEQ; + iter->cur_subseq++; return 0; } @@ -611,7 +638,17 @@ int bootflow_scan_next(struct bootflow_iter *iter, struct bootflow *bflow) return 0; iter->err = ret; if (ret != BF_NO_MORE_PARTS && ret != -ENOSYS) { - if (iter->flags & BOOTFLOWIF_ALL) + /* + * A failure while probing additional sub-entries + * (cur_subseq > 0) just means the current + * (method, part, bootdev) tuple has no more + * entries. Don't surface it as a bootflow (even + * in 'all' mode); free the partially-populated + * bflow and move on to the next method. + */ + if (iter->cur_subseq) + bootflow_free(bflow); + else if (iter->flags & BOOTFLOWIF_ALL) return log_msg_ret("all", ret); } } else { diff --git a/include/bootflow.h b/include/bootflow.h index 1cfdda403ac..0b209e172b3 100644 --- a/include/bootflow.h +++ b/include/bootflow.h @@ -255,6 +255,10 @@ enum { * @cur_label: Current label being processed * @num_methods: Number of bootmeth devices in @method_order * @cur_method: Current method number, an index into @method_order + * @cur_subseq: Current sub-entry index within (method, part, bootdev). 0 for + * the first/only entry. Bumped by one each time a bootflow is + * successfully returned for the current tuple, and reset to 0 when the + * iterator advances to a new method, partition or bootdev. * @first_glob_method: Index of first global method within @method_order[], if * any, else -1 * @cur_prio: Current priority being scanned @@ -286,6 +290,7 @@ struct bootflow_iter { int cur_label; int num_methods; int cur_method; + int cur_subseq; int first_glob_method; enum bootdev_prio_t cur_prio; struct udevice **method_order; From d4e87a75d2520a64d60dfc8e57a59f621a6718b7 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 25 May 2026 19:22:34 +0400 Subject: [PATCH 37/78] bootmeth: bls: emit one bootflow per BLS entry Adjust bls_pick_entry() to enumerate every .conf file in the directory, sort them in descending filename order and return the seq-th entry. User-visible effect: 'bootflow scan' now lists each BLS entry individually (e.g. .part_2, .part_2.1, ...) and 'bootflow menu' lets the user pick between them. Signed-off-by: Alexey Charkov --- boot/bootmeth_bls.c | 163 +++++++++++++++++++++++++++++--------------- 1 file changed, 109 insertions(+), 54 deletions(-) diff --git a/boot/bootmeth_bls.c b/boot/bootmeth_bls.c index 5859371d320..b658f2b37b4 100644 --- a/boot/bootmeth_bls.c +++ b/boot/bootmeth_bls.c @@ -7,13 +7,10 @@ * * Spec: https://uapi-group.org/specifications/specs/boot_loader_specification/ * - * TODO: a partition typically holds several BLS entries, but the bootstd - * framework currently allows only one bootflow per (bootmeth, partition) - * pair, so this bootmeth surfaces only the highest-sorting entry. Once the - * framework grows a way for a bootmeth to emit multiple bootflows from a - * single partition, this should expose every discovered entry so the user - * can pick from the standard 'bootflow menu' UI rather than be limited to - * the default pick. + * Every *.conf file in /loader/entries/ is surfaced as its own + * bootflow via the bootstd iterator's sub-entry axis. The bootmeth is + * called repeatedly with seq = 0, 1, 2, ... for the same (partition, + * bootdev) tuple and returns -ENOENT once the entry list is exhausted. */ #define LOG_CATEGORY UCLASS_BOOTSTD @@ -29,11 +26,48 @@ #include #include #include +#include +#include #include #define BLS_DIR "loader/entries" #define BLS_SUFFIX ".conf" +/** + * struct bls_priv - per-bootmeth-instance state + * + * Caches a single flat list of all *.conf entries discovered across every + * bootstd prefix on a given (bootdev, partition) tuple so that successive + * read_bootflow() calls answer from RAM instead of re-walking the + * filesystem. Sorted ascending by full path; read_bootflow() indexes from + * the end so the seq-th entry the iterator surfaces is the seq-th newest. + * + * Invalidated on seq == 0 (new scan) or when the (bootdev, partition) key + * changes. + * + * @paths: alist of ``char *`` full paths (strdup'd, owned), sorted + * ascending by full path. + * @cache_blk: Block device the cache was built against. + * @cache_part: Partition number the cache was built against. + */ +struct bls_priv { + struct alist paths; + struct udevice *cache_blk; + int cache_part; +}; + +static void bls_cache_drop(struct bls_priv *priv) +{ + char **arr = priv->paths.data; + uint i; + + for (i = 0; i < priv->paths.count; i++) + free(arr[i]); + alist_empty(&priv->paths); + priv->cache_blk = NULL; + priv->cache_part = 0; +} + static int bls_check(struct udevice *dev, struct bootflow_iter *iter) { int ret; @@ -66,12 +100,14 @@ static int bls_getfile(struct pxe_context *ctx, const char *file_path, } /** - * bls_pick_entry() - Find the highest-sorting *.conf across bootstd prefixes + * bls_load_all() - Enumerate every BLS *.conf across all bootstd prefixes * - * Walks ``/loader/entries/`` for each prefix in @prefixes and - * returns the lexicographically maximum full path seen. + * Walks ``/loader/entries/`` for each prefix in @prefixes, collects + * every regular file with a ``.conf`` suffix as a strdup'd full path in + * @paths, then sorts the merged result ascending by full path (callers + * index from the end for the spec's descending order). * - * The spec leaves ordering between prefixes unspecified; comparing full + * The spec leaves ordering between prefixes unspecified; sorting full * paths is a deterministic-and-cheap stand-in. * * The Boot Loader Specification says entries should be sorted by sort-key @@ -83,18 +119,12 @@ static int bls_getfile(struct pxe_context *ctx, const char *file_path, * each candidate entry, parsing its 'sort-key' and 'version' fields (the * latter compared with strverscmp()-style logic), and only falling back to * filename order when those tie. - * - * @prefixes: NULL-terminated array of bootstd prefixes to search - * @desc: Block descriptor (used to re-mount per prefix) - * @bflow: Bootflow being populated (used to re-mount per prefix) - * @fullp: Returns the chosen full path (allocated), or NULL if none - * Return: 0 on success, -ENOENT if no entry was found, < 0 on other error */ -static int bls_pick_entry(const char *const *prefixes, struct blk_desc *desc, - struct bootflow *bflow, char **fullp) +static int bls_load_all(struct alist *paths, struct blk_desc *desc, + struct bootflow *bflow, + const char *const *prefixes) { char dirpath[256]; - char *best = NULL; int ret; int i; @@ -104,10 +134,8 @@ static int bls_pick_entry(const char *const *prefixes, struct blk_desc *desc, /* fs_closedir() below resets the global fs_type. */ ret = bootmeth_setup_fs(bflow, desc); - if (ret) { - free(best); + if (ret) return log_msg_ret("fs", ret); - } snprintf(dirpath, sizeof(dirpath), "%s%s", prefixes[i], BLS_DIR); @@ -129,26 +157,23 @@ static int bls_pick_entry(const char *const *prefixes, struct blk_desc *desc, full = malloc(strlen(dirpath) + 1 + len + 1); if (!full) { - free(best); fs_closedir(dirs); return -ENOMEM; } sprintf(full, "%s/%s", dirpath, dent->name); - if (!best || strcmp(full, best) > 0) { - free(best); - best = full; - } else { + if (!alist_add(paths, full)) { free(full); + fs_closedir(dirs); + return -ENOMEM; } } fs_closedir(dirs); } - if (!best) - return -ENOENT; - - *fullp = best; + if (paths->count) + qsort(paths->data, paths->count, paths->obj_size, + strcmp_compar); return 0; } @@ -166,15 +191,15 @@ static int bls_pick_entry(const char *const *prefixes, struct blk_desc *desc, static int bls_read_bootflow(struct udevice *dev, struct bootflow *bflow, int seq) { - if (seq) - return -ENOENT; + struct bls_priv *priv = dev_get_priv(dev); struct blk_desc *desc; const char *const *prefixes; struct udevice *bootstd; struct pxe_label *label = NULL; struct pxe_menu scratch = {}; - char *fpath = NULL; + const char *fpath; const char *base; + char **slot; char *body; int ret; @@ -189,41 +214,51 @@ static int bls_read_bootflow(struct udevice *dev, struct bootflow *bflow, desc = dev_get_uclass_plat(bflow->blk); prefixes = bootstd_get_prefixes(bootstd); - ret = bls_pick_entry(prefixes, desc, bflow, &fpath); - if (ret) - return log_msg_ret("scan", ret); + /* + * A fresh scan of this tuple always starts at seq == 0; rebuild then + * so callers see current on-disk state. seq > 0 only happens after a + * successful seq == 0 (the iterator only bumps cur_subseq on + * success). Still, check the block device and partition just in case + */ + if (!seq || priv->cache_blk != bflow->blk || + priv->cache_part != bflow->part) { + bls_cache_drop(priv); + ret = bls_load_all(&priv->paths, desc, bflow, prefixes); + if (ret) + return log_msg_ret("scan", ret); + priv->cache_blk = bflow->blk; + priv->cache_part = bflow->part; + } + + if (seq >= (int)priv->paths.count) + return -ENOENT; + /* Sorted ascending by full path; index from the end for newest-first. */ + slot = alist_getw(&priv->paths, priv->paths.count - 1 - seq, char *); + fpath = *slot; base = strrchr(fpath, '/'); base = base ? base + 1 : fpath; /* - * bls_pick_entry() finished with fs_closedir(), which resets the + * bls_load_all() finished with fs_closedir(), which resets the * global fs_type. Re-mount the partition so bootmeth_try_file()'s * internal fs_size() call can find the right filesystem driver. */ ret = bootmeth_setup_fs(bflow, desc); - if (ret) { - free(fpath); + if (ret) return log_msg_ret("fs", ret); - } ret = bootmeth_try_file(bflow, desc, NULL, fpath); - if (ret) { - free(fpath); + if (ret) return log_msg_ret("try", ret); - } ret = bootmeth_alloc_file(bflow, SZ_64K, 1, BFI_EXTLINUX_CFG); - if (ret) { - free(fpath); + if (ret) return log_msg_ret("read", ret); - } label = label_create(); - if (!label) { - ret = -ENOMEM; - goto err; - } + if (!label) + return log_msg_ret("lbl", -ENOMEM); /* * BLS files have no 'label NAME' header — derive the label name from @@ -261,14 +296,12 @@ static int bls_read_bootflow(struct udevice *dev, struct bootflow *bflow, } bflow->bootmeth_priv = label; - free(fpath); return 0; err: if (label) label_destroy(label); - free(fpath); return log_msg_ret("bls", ret); } @@ -315,6 +348,25 @@ static int bls_bootmeth_bind(struct udevice *dev) return 0; } +static int bls_bootmeth_probe(struct udevice *dev) +{ + struct bls_priv *priv = dev_get_priv(dev); + + alist_init_struct(&priv->paths, char *); + + return 0; +} + +static int bls_bootmeth_remove(struct udevice *dev) +{ + struct bls_priv *priv = dev_get_priv(dev); + + bls_cache_drop(priv); + alist_uninit(&priv->paths); + + return 0; +} + static struct bootmeth_ops bls_bootmeth_ops = { .check = bls_check, .read_bootflow = bls_read_bootflow, @@ -333,4 +385,7 @@ U_BOOT_DRIVER(bootmeth_2bls) = { .of_match = bls_bootmeth_ids, .ops = &bls_bootmeth_ops, .bind = bls_bootmeth_bind, + .probe = bls_bootmeth_probe, + .remove = bls_bootmeth_remove, + .priv_auto = sizeof(struct bls_priv), }; From 821534f8105229a0c1d024e1eea9f5759657a7f7 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 22 Apr 2026 22:23:30 +0400 Subject: [PATCH 38/78] expo: Make bootflow_menu_new() scale to display size Current bootflow_menu_new() creates a menu with a fixed screen size of 1366x768, which is unusable on smaller displays. Convert it to use integer scaling from the same reference layout. Signed-off-by: Alexey Charkov --- boot/bootflow_menu.c | 52 +++++++++++++++++++++++++++++--------------- boot/scene.c | 1 + boot/scene_menu.c | 30 +++++++++++++++++++------ include/bootflow.h | 4 +++- include/expo.h | 2 ++ test/boot/bootflow.c | 2 +- 6 files changed, 64 insertions(+), 27 deletions(-) diff --git a/boot/bootflow_menu.c b/boot/bootflow_menu.c index 4a442e16850..92636988e19 100644 --- a/boot/bootflow_menu.c +++ b/boot/bootflow_menu.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -32,8 +33,16 @@ struct menu_priv { struct udevice *last_bootdev; }; -int bootflow_menu_new(struct expo **expp) +int bootflow_menu_new(struct expo **expp, int width, int height) { + /* + * All positions are expressed as proportional fractions of the + * reference design (1366 x 768, the classic WXGA resolution used by + * the sandbox display). Use integer scaling so the layout fits any + * display resolution. + */ +#define SX(v) ((v) * width / 1366) +#define SY(v) ((v) * height / 768) struct scene_obj_menu *menu; struct menu_priv *priv; struct scene *scn; @@ -57,20 +66,22 @@ int bootflow_menu_new(struct expo **expp) ret = scene_box(scn, "box", OBJ_BOX, 2, NULL); if (ret < 0) return log_msg_ret("bmb", ret); - ret |= scene_obj_set_bbox(scn, OBJ_BOX, 30, 90, 1366 - 30, 720); + ret |= scene_obj_set_bbox(scn, OBJ_BOX, SX(30), SY(90), + width - SX(30), height - max(SY(48), 6 * char_h)); ret = scene_menu(scn, "main", OBJ_MENU, &menu); - ret |= scene_obj_set_pos(scn, OBJ_MENU, MARGIN_LEFT, 100); + ret |= scene_obj_set_pos(scn, OBJ_MENU, SX(100), SY(100)); ret |= scene_txt_str(scn, "title", OBJ_MENU_TITLE, STR_MENU_TITLE, "U-Boot - Boot Menu", NULL); - ret |= scene_obj_set_bbox(scn, OBJ_MENU_TITLE, 0, 32, - SCENEOB_DISPLAY_MAX, 30); + ret |= scene_obj_set_bbox(scn, OBJ_MENU_TITLE, 0, SY(32), + SCENEOB_DISPLAY_MAX, SY(32) + SY(30)); ret |= scene_obj_set_halign(scn, OBJ_MENU_TITLE, SCENEOA_CENTRE); logo = video_get_u_boot_logo(); if (logo) { ret |= scene_img(scn, "ulogo", OBJ_U_BOOT_LOGO, logo, NULL); - ret |= scene_obj_set_pos(scn, OBJ_U_BOOT_LOGO, 1165, 100); + ret |= scene_obj_set_pos(scn, OBJ_U_BOOT_LOGO, + width - SX(1366 - 1165), SY(100)); } ret |= scene_txt_str(scn, "prompt1a", OBJ_PROMPT1A, STR_PROMPT1A, @@ -86,14 +97,14 @@ int bootflow_menu_new(struct expo **expp) ret |= scene_txt_str(scn, "autoboot", OBJ_AUTOBOOT, STR_AUTOBOOT, "The highlighted entry will be executed automatically in %ds.", NULL); - ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1A, 0, 590, - SCENEOB_DISPLAY_MAX, 30); - ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1B, 0, 620, - SCENEOB_DISPLAY_MAX, 30); - ret |= scene_obj_set_bbox(scn, OBJ_PROMPT2, 100, 650, - 1366 - 100, 700); - ret |= scene_obj_set_bbox(scn, OBJ_AUTOBOOT, 0, 720, - SCENEOB_DISPLAY_MAX, 750); + ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1A, 0, SY(590), + SCENEOB_DISPLAY_MAX, SY(590) + SY(30)); + ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1B, 0, SY(620), + SCENEOB_DISPLAY_MAX, SY(620) + SY(30)); + ret |= scene_obj_set_bbox(scn, OBJ_PROMPT2, SX(100), SY(650), + width - SX(100), SY(700)); + ret |= scene_obj_set_bbox(scn, OBJ_AUTOBOOT, 0, height, + SCENEOB_DISPLAY_MAX, height + SY(30)); ret |= scene_obj_set_halign(scn, OBJ_PROMPT1A, SCENEOA_CENTRE); ret |= scene_obj_set_halign(scn, OBJ_PROMPT1B, SCENEOA_CENTRE); ret |= scene_obj_set_halign(scn, OBJ_PROMPT2, SCENEOA_CENTRE); @@ -115,6 +126,8 @@ int bootflow_menu_new(struct expo **expp) *expp = exp; +#undef SX +#undef SY return 0; } @@ -218,18 +231,21 @@ int bootflow_menu_add_all(struct expo *exp) int bootflow_menu_setup(struct bootstd_priv *std, bool text_mode, struct expo **expp) { + struct video_priv *vid_priv; struct udevice *dev; struct expo *exp; int ret; - ret = bootflow_menu_new(&exp); - if (ret) - return log_msg_ret("bmn", ret); - /* For now we only support a video console */ ret = uclass_first_device_err(UCLASS_VIDEO, &dev); if (ret) return log_msg_ret("vid", ret); + vid_priv = dev_get_uclass_priv(dev); + + ret = bootflow_menu_new(&exp, vid_priv->xsize, vid_priv->ysize); + if (ret) + return log_msg_ret("bmn", ret); + ret = expo_set_display(exp, dev); if (ret) return log_msg_ret("dis", ret); diff --git a/boot/scene.c b/boot/scene.c index fa8f540bfb0..14b9bb6d840 100644 --- a/boot/scene.c +++ b/boot/scene.c @@ -756,6 +756,7 @@ int scene_arrange(struct scene *scn) ret = scene_calc_arrange(scn, &arr); if (ret < 0) return log_msg_ret("arr", ret); + arr.xsize = xsize; list_for_each_entry(obj, &scn->obj_head, sibling) { handle_alignment(obj->horiz, obj->vert, &obj->bbox, &obj->dims, diff --git a/boot/scene_menu.c b/boot/scene_menu.c index 23404172093..45b45d331e2 100644 --- a/boot/scene_menu.c +++ b/boot/scene_menu.c @@ -98,11 +98,22 @@ static int update_pointers(struct scene_obj_menu *menu, uint id, bool point) /* adjust the pointer object to point to the selected item */ if (menu->pointer_id && item && point) { struct scene_obj *label; + struct expo *exp = scn->expo; + int xsize = 0; + int ptr_ofs; + + if (exp->display) { + struct video_priv *vid = dev_get_uclass_priv(exp->display); + + xsize = vid->xsize; + } + ptr_ofs = xsize ? 200 * xsize / 1366 : 200; label = scene_obj_find(scn, item->label_id, SCENEOBJT_NONE); ret = scene_obj_set_pos(scn, menu->pointer_id, - menu->obj.bbox.x0 + 200, label->bbox.y0); + menu->obj.bbox.x0 + ptr_ofs, + label->bbox.y0); if (ret < 0) return log_msg_ret("ptr", ret); } @@ -258,11 +269,14 @@ int scene_menu_arrange(struct scene *scn, struct expo_arrange_info *arr, } /* - * Currently everything is hard-coded to particular columns so this - * won't work on small displays and looks strange if the font size is - * small. This can be updated once text measuring is supported in - * vidconsole + * Column positions are expressed relative to a 1366-pixel-wide + * reference design and scaled to the actual display width so the + * layout works on small screens (e.g. 256x144) as well as large ones. + * Fall back to the reference values when no display is attached. */ + int key_ofs = arr->xsize ? 230 * arr->xsize / 1366 : 230; + int desc_ofs = arr->xsize ? 280 * arr->xsize / 1366 : 280; + sel_id = menu->cur_item_id; list_for_each_entry(item, &menu->item_head, sibling) { bool selected; @@ -294,13 +308,15 @@ int scene_menu_arrange(struct scene *scn, struct expo_arrange_info *arr, stack && !open && !selected); if (item->key_id) { - ret = scene_obj_set_pos(scn, item->key_id, x + 230, y); + ret = scene_obj_set_pos(scn, item->key_id, + x + key_ofs, y); if (ret < 0) return log_msg_ret("key", ret); } if (item->desc_id) { - ret = scene_obj_set_pos(scn, item->desc_id, x + 280, y); + ret = scene_obj_set_pos(scn, item->desc_id, + x + desc_ofs, y); if (ret < 0) return log_msg_ret("des", ret); } diff --git a/include/bootflow.h b/include/bootflow.h index 0b209e172b3..e2dd1fdf29c 100644 --- a/include/bootflow.h +++ b/include/bootflow.h @@ -514,9 +514,11 @@ int bootflow_iter_check_system(const struct bootflow_iter *iter); * bootflows to it. * * @expp: Returns the expo created + * @width: Display width in pixels + * @height: Display height in pixels * Returns 0 on success, -ve on error */ -int bootflow_menu_new(struct expo **expp); +int bootflow_menu_new(struct expo **expp, int width, int height); /** * bootflow_menu_add_all() - Add all bootflows to a menu diff --git a/include/expo.h b/include/expo.h index 4dee479e9a0..26e155f8cb7 100644 --- a/include/expo.h +++ b/include/expo.h @@ -493,9 +493,11 @@ struct scene_obj_txtedit { * struct expo_arrange_info - Information used when arranging a scene * * @label_width: Maximum width of labels in scene + * @xsize: Display width in pixels, 0 if no display */ struct expo_arrange_info { int label_width; + int xsize; }; /** diff --git a/test/boot/bootflow.c b/test/boot/bootflow.c index 1cc137c9700..1e219f33ce9 100644 --- a/test/boot/bootflow.c +++ b/test/boot/bootflow.c @@ -940,7 +940,7 @@ static int bootflow_menu_theme(struct unit_test_state *uts) ut_assertok(scan_mmc4_bootdev(uts)); - ut_assertok(bootflow_menu_new(&exp)); + ut_assertok(bootflow_menu_new(&exp, 1366, 720)); ut_assertok(bootflow_menu_add_all(exp)); node = ofnode_path("/bootstd/theme"); ut_assert(ofnode_valid(node)); From 5805a53d300e74f638134a2eb32ff6cea8005293 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 6 May 2026 22:40:12 +0400 Subject: [PATCH 39/78] expo: fix menu pointer overdrawn by label text The Expo menu rendered the pointer (">") and then the per-item label text in that order. Both were positioned at x = menu_bbox.x0 + a scaled pixel offset, so on small displays (e.g. 256x144 where the scaled offset rounds down) the label started at the same x as the pointer and overwrote it, leaving no visible selection indicator. Place the pointer at x = menu_bbox.x0 + theme->menu_inset, which is the leftmost content column used by all other objects. Reserve the pointer's character-cell width for the label: label_x = menu_inset + ptr_width + 2 This guarantees the pointer is never overdrawn regardless of display size. --- boot/scene_menu.c | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/boot/scene_menu.c b/boot/scene_menu.c index 45b45d331e2..b7240ec2bae 100644 --- a/boot/scene_menu.c +++ b/boot/scene_menu.c @@ -97,22 +97,18 @@ static int update_pointers(struct scene_obj_menu *menu, uint id, bool point) /* adjust the pointer object to point to the selected item */ if (menu->pointer_id && item && point) { + const struct expo_theme *theme = &scn->expo->theme; struct scene_obj *label; - struct expo *exp = scn->expo; - int xsize = 0; - int ptr_ofs; - - if (exp->display) { - struct video_priv *vid = dev_get_uclass_priv(exp->display); - - xsize = vid->xsize; - } - ptr_ofs = xsize ? 200 * xsize / 1366 : 200; label = scene_obj_find(scn, item->label_id, SCENEOBJT_NONE); + /* + * Place the pointer at the leftmost column (menu inset) so it + * is never overdrawn by the label text, which is rendered after + * the pointer in object order. + */ ret = scene_obj_set_pos(scn, menu->pointer_id, - menu->obj.bbox.x0 + ptr_ofs, + menu->obj.bbox.x0 + theme->menu_inset, label->bbox.y0); if (ret < 0) return log_msg_ret("ptr", ret); From dcc373dd0464b009110c1de133f2c5610b14b705 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 6 May 2026 22:40:39 +0400 Subject: [PATCH 40/78] expo: compute menu column positions from actual item content widths The key and description column x-offsets were previously fixed proportions of the reference 1366-pixel design width (230 px and 280 px respectively). On displays narrower than ~350 px the scaled values are smaller than the actual label text, causing the key and description columns to overlap the label column. Compute the offsets dynamically instead: after scene_menu_calc_dims() has equalised all item widths, read the bounding-box widths of the first item's label and key objects and derive: key_ofs = label_ofs + label_col_w + gap desc_ofs = key_ofs + key_col_w + gap Fall back to the reference-design proportional values when the menu has no items. --- boot/scene_menu.c | 59 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/boot/scene_menu.c b/boot/scene_menu.c index b7240ec2bae..13e179c196a 100644 --- a/boot/scene_menu.c +++ b/boot/scene_menu.c @@ -265,13 +265,52 @@ int scene_menu_arrange(struct scene *scn, struct expo_arrange_info *arr, } /* - * Column positions are expressed relative to a 1366-pixel-wide - * reference design and scaled to the actual display width so the - * layout works on small screens (e.g. 256x144) as well as large ones. - * Fall back to the reference values when no display is attached. + * Reserve space for the pointer character to the left of the label + * so that label text never overdraws the pointer. The pointer is + * rendered in object-head order *before* the per-item text objects, + * so if the label starts at the same x position it will overdraw it. */ - int key_ofs = arr->xsize ? 230 * arr->xsize / 1366 : 230; - int desc_ofs = arr->xsize ? 280 * arr->xsize / 1366 : 280; + int ptr_width = 0; + + if (menu->pointer_id) + scene_obj_get_hw(scn, menu->pointer_id, &ptr_width); + int label_ofs = theme->menu_inset + ptr_width + (ptr_width ? 2 : 0); + + /* + * Compute column positions dynamically from the actual item content + * widths so that columns never overlap, regardless of display size. + * scene_menu_calc_dims() has already equalised all item widths, so + * reading the first item's objects gives the column widths. + * + * Fall back to reference-design proportional offsets when the menu + * has no items (shouldn't happen in practice). + */ + int label_col_w = 0; + int key_col_w = 6; /* minimum: one bitmap char */ + int key_ofs, desc_ofs; + const int col_gap = 4; /* pixels between columns */ + + if (!list_empty(&menu->item_head)) { + const struct scene_menitem *first; + struct scene_obj *lobj, *kobj; + + first = list_first_entry(&menu->item_head, + struct scene_menitem, sibling); + lobj = scene_obj_find(scn, first->label_id, SCENEOBJT_NONE); + if (lobj) + label_col_w = lobj->bbox.x1 - lobj->bbox.x0; + kobj = scene_obj_find(scn, first->key_id, SCENEOBJT_NONE); + if (kobj) + key_col_w = kobj->bbox.x1 - kobj->bbox.x0; + } + + if (label_col_w > 0) { + key_ofs = label_ofs + label_col_w + col_gap; + desc_ofs = key_ofs + key_col_w + col_gap; + } else { + key_ofs = arr->xsize ? 230 * arr->xsize / 1366 : 230; + desc_ofs = arr->xsize ? 280 * arr->xsize / 1366 : 280; + } sel_id = menu->cur_item_id; list_for_each_entry(item, &menu->item_head, sibling) { @@ -293,11 +332,13 @@ int scene_menu_arrange(struct scene *scn, struct expo_arrange_info *arr, selected = sel_id == item->id; /* - * Put the label on the left, then leave a space for the - * pointer, then the key and the description + * Put the pointer at the leftmost column, then the label, + * then the key and the description. The pointer is placed + * by update_pointers() at x + menu_inset; the label starts + * just to the right of the pointer's character cell. */ ret = scene_obj_set_pos(scn, item->label_id, - x + theme->menu_inset, y); + x + label_ofs, y); if (ret < 0) return log_msg_ret("nam", ret); scene_obj_set_hide(scn, item->label_id, From ad4b49bd11876521ce59b537dfeffadfe5afc2c2 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 6 May 2026 22:42:19 +0400 Subject: [PATCH 41/78] expo: size bootflow menu text bboxes from actual console font height bootflow_menu_new() set bounding boxes for the title, navigation hint and help text using hardcoded pixel values derived from a specific font height. Switching font (e.g. to 6x8) or running at different resolutions could leave text clipped or pushed off-screen. Add a char_h parameter to bootflow_menu_new(). When non-zero it is the actual pixel height of one character cell, obtained from the vidconsole's y_charsize field. When zero (e.g. in unit tests without a live console) the function falls back to max(SY(16), 1) so the layout remains sane. Replace every magic pixel constant with a multiple of char_h: - title bbox y1: max(SY(32) + SY(30), char_h) -- at least one line - prompt1 y0: min(SY(590), height - 6*char_h) -- 6 lines from bottom - prompt1 height: max(SY(30), 2*char_h) -- fits 2 wrapped lines - prompt2 y0: min(SY(650), height - 4*char_h) -- 4 lines from bottom Also guard the U-Boot logo: skip it when the reserved right-side column (scaled from the 1366 px reference) is narrower than 64 px, preventing a clipped logo strip from bleeding over the menu box on small displays such as the 256x144 Flipper One screen. In bootflow_menu_setup(), obtain the vidconsole as a child of the video device (mirroring expo_set_display()) and pass its y_charsize; fall back to 0 if no console is found. Update the unit test to pass 0 for char_h (reference-design default). --- boot/bootflow_menu.c | 61 ++++++++++++++++++++++++++++++++++++-------- include/bootflow.h | 5 +++- test/boot/bootflow.c | 2 +- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/boot/bootflow_menu.c b/boot/bootflow_menu.c index 92636988e19..a53f2a1e77a 100644 --- a/boot/bootflow_menu.c +++ b/boot/bootflow_menu.c @@ -33,7 +33,7 @@ struct menu_priv { struct udevice *last_bootdev; }; -int bootflow_menu_new(struct expo **expp, int width, int height) +int bootflow_menu_new(struct expo **expp, int width, int height, int char_h) { /* * All positions are expressed as proportional fractions of the @@ -43,6 +43,13 @@ int bootflow_menu_new(struct expo **expp, int width, int height) */ #define SX(v) ((v) * width / 1366) #define SY(v) ((v) * height / 768) + /* + * Use the actual console character height when provided; otherwise + * fall back to the SY-scaled reference value so the layout is + * self-consistent even without a live console (e.g. in unit tests). + */ + if (!char_h) + char_h = max(SY(16), 1); struct scene_obj_menu *menu; struct menu_priv *priv; struct scene *scn; @@ -73,12 +80,22 @@ int bootflow_menu_new(struct expo **expp, int width, int height) ret |= scene_obj_set_pos(scn, OBJ_MENU, SX(100), SY(100)); ret |= scene_txt_str(scn, "title", OBJ_MENU_TITLE, STR_MENU_TITLE, "U-Boot - Boot Menu", NULL); + /* + * Ensure the title bbox is at least one character-cell tall so it is + * not clipped on small displays + */ ret |= scene_obj_set_bbox(scn, OBJ_MENU_TITLE, 0, SY(32), - SCENEOB_DISPLAY_MAX, SY(32) + SY(30)); + SCENEOB_DISPLAY_MAX, + max(SY(32) + SY(30), char_h)); ret |= scene_obj_set_halign(scn, OBJ_MENU_TITLE, SCENEOA_CENTRE); logo = video_get_u_boot_logo(); - if (logo) { + /* + * Only add the logo when the reserved right-side column is at least + * 64 px wide so that a clipped strip does not bleed over the menu + * box on small displays (e.g. 256x144 where the column is only 37 px). + */ + if (logo && SX(1366 - 1165) >= 64) { ret |= scene_img(scn, "ulogo", OBJ_U_BOOT_LOGO, logo, NULL); ret |= scene_obj_set_pos(scn, OBJ_U_BOOT_LOGO, width - SX(1366 - 1165), SY(100)); @@ -97,12 +114,23 @@ int bootflow_menu_new(struct expo **expp, int width, int height) ret |= scene_txt_str(scn, "autoboot", OBJ_AUTOBOOT, STR_AUTOBOOT, "The highlighted entry will be executed automatically in %ds.", NULL); - ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1A, 0, SY(590), - SCENEOB_DISPLAY_MAX, SY(590) + SY(30)); - ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1B, 0, SY(620), - SCENEOB_DISPLAY_MAX, SY(620) + SY(30)); - ret |= scene_obj_set_bbox(scn, OBJ_PROMPT2, SX(100), SY(650), - width - SX(100), SY(700)); + /* + * Prompt1A/B: up to 2 wrapped lines (navigation hint). + * Prompt2: up to 4 wrapped lines (boot instructions). + * All positions are anchored from the bottom so they remain visible + * on small displays; on large displays the SY() values dominate. + */ + ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1A, 0, + min(SY(590), height - 6 * char_h), + SCENEOB_DISPLAY_MAX, + min(SY(590), height - 6 * char_h) + max(SY(30), 2 * char_h)); + ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1B, 0, + min(SY(620), height - 6 * char_h), + SCENEOB_DISPLAY_MAX, + min(SY(620), height - 6 * char_h) + max(SY(30), 2 * char_h)); + ret |= scene_obj_set_bbox(scn, OBJ_PROMPT2, SX(100), + min(SY(650), height - 4 * char_h), + width - SX(100), height); ret |= scene_obj_set_bbox(scn, OBJ_AUTOBOOT, 0, height, SCENEOB_DISPLAY_MAX, height + SY(30)); ret |= scene_obj_set_halign(scn, OBJ_PROMPT1A, SCENEOA_CENTRE); @@ -242,7 +270,20 @@ int bootflow_menu_setup(struct bootstd_priv *std, bool text_mode, return log_msg_ret("vid", ret); vid_priv = dev_get_uclass_priv(dev); - ret = bootflow_menu_new(&exp, vid_priv->xsize, vid_priv->ysize); + { + struct udevice *cons; + struct vidconsole_priv *vc_priv = NULL; + int char_h = 0; + + if (!device_find_first_child_by_uclass(dev, UCLASS_VIDEO_CONSOLE, + &cons)) + vc_priv = dev_get_uclass_priv(cons); + if (vc_priv) + char_h = vc_priv->y_charsize; + + ret = bootflow_menu_new(&exp, vid_priv->xsize, vid_priv->ysize, + char_h); + } if (ret) return log_msg_ret("bmn", ret); diff --git a/include/bootflow.h b/include/bootflow.h index e2dd1fdf29c..9a2d4aa6bdb 100644 --- a/include/bootflow.h +++ b/include/bootflow.h @@ -516,9 +516,12 @@ int bootflow_iter_check_system(const struct bootflow_iter *iter); * @expp: Returns the expo created * @width: Display width in pixels * @height: Display height in pixels + * @char_h: Console character height in pixels; used to size text bboxes + * correctly regardless of the selected font. Pass 0 to let the + * function use a sensible default (the SY-scaled reference value). * Returns 0 on success, -ve on error */ -int bootflow_menu_new(struct expo **expp, int width, int height); +int bootflow_menu_new(struct expo **expp, int width, int height, int char_h); /** * bootflow_menu_add_all() - Add all bootflows to a menu diff --git a/test/boot/bootflow.c b/test/boot/bootflow.c index 1e219f33ce9..35393476404 100644 --- a/test/boot/bootflow.c +++ b/test/boot/bootflow.c @@ -940,7 +940,7 @@ static int bootflow_menu_theme(struct unit_test_state *uts) ut_assertok(scan_mmc4_bootdev(uts)); - ut_assertok(bootflow_menu_new(&exp, 1366, 720)); + ut_assertok(bootflow_menu_new(&exp, 1366, 720, 0)); ut_assertok(bootflow_menu_add_all(exp)); node = ofnode_path("/bootstd/theme"); ut_assert(ofnode_valid(node)); From ed0d31a1525e97e09ec6b3c83e0f51715008648b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 22 Jun 2026 23:27:52 +0400 Subject: [PATCH 42/78] expo: make layout elastic to fit smaller displays Signed-off-by: Alexey Charkov --- .../rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi | 12 + boot/bootflow_menu.c | 12 + boot/expo.c | 1 + boot/scene.c | 12 +- boot/scene_menu.c | 245 ++++++++++++++---- include/expo.h | 10 + 6 files changed, 238 insertions(+), 54 deletions(-) diff --git a/arch/arm/dts/rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi b/arch/arm/dts/rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi index 542e6560095..d1b0637983b 100644 --- a/arch/arm/dts/rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi +++ b/arch/arm/dts/rk3576-flipper-one-rev-f0b0c1-u-boot.dtsi @@ -5,6 +5,18 @@ #include "rk3576-u-boot.dtsi" +/ { + bootstd { + compatible = "u-boot,boot-std"; + + theme { + menu-pos-x = <8>; + menu-inset = <0>; + font-size = <8>; + }; + }; +}; + /* * The video driver needs its framebuffer reserved before relocation. * Mark spi0 and the display node so they are bound during the pre-relocation diff --git a/boot/bootflow_menu.c b/boot/bootflow_menu.c index a53f2a1e77a..5c299cc3bdb 100644 --- a/boot/bootflow_menu.c +++ b/boot/bootflow_menu.c @@ -99,6 +99,18 @@ int bootflow_menu_new(struct expo **expp, int width, int height, int char_h) ret |= scene_img(scn, "ulogo", OBJ_U_BOOT_LOGO, logo, NULL); ret |= scene_obj_set_pos(scn, OBJ_U_BOOT_LOGO, width - SX(1366 - 1165), SY(100)); + } else { + /* + * No logo/preview column is shown, so the right side of the box + * is empty. Let the menu justify its columns out to the box's + * inner right edge (less the border and a small gap) instead of + * bunching them in the reference column band on the left. + */ + struct scene_obj_box *box; + + box = scene_obj_find(scn, OBJ_BOX, SCENEOBJT_BOX); + if (box) + menu->fill_x1 = box->obj.bbox.x1 - box->width - 4; } ret |= scene_txt_str(scn, "prompt1a", OBJ_PROMPT1A, STR_PROMPT1A, diff --git a/boot/expo.c b/boot/expo.c index 94413acd381..9c3086aaa43 100644 --- a/boot/expo.c +++ b/boot/expo.c @@ -288,6 +288,7 @@ int expo_apply_theme(struct expo *exp, ofnode node) ofnode_read_u32(node, "menuitem-gap-y", &theme->menuitem_gap_y); ofnode_read_u32(node, "menu-title-margin-x", &theme->menu_title_margin_x); + ofnode_read_u32(node, "menu-pos-x", &theme->menu_pos_x); white_on_black = ofnode_read_bool(node, "white-on-black"); if (exp->display) video_set_white_on_black(exp->display, white_on_black); diff --git a/boot/scene.c b/boot/scene.c index 14b9bb6d840..f5a9af1be8d 100644 --- a/boot/scene.c +++ b/boot/scene.c @@ -1072,10 +1072,20 @@ int scene_apply_theme(struct scene *scn, struct expo_theme *theme) switch (obj->type) { case SCENEOBJT_NONE: case SCENEOBJT_IMAGE: - case SCENEOBJT_MENU: case SCENEOBJT_BOX: case SCENEOBJT_TEXTLINE: break; + case SCENEOBJT_MENU: + /* + * Move the menu to the themed x-position if one is + * given, keeping its y. The item columns are laid out + * relative to the menu, so this shifts the whole row. + */ + if (theme->menu_pos_x) + scene_obj_set_pos(scn, obj->id, + theme->menu_pos_x, + obj->bbox.y0); + break; case SCENEOBJT_TEXTEDIT: scene_txted_set_font(scn, obj->id, NULL, theme->font_size); diff --git a/boot/scene_menu.c b/boot/scene_menu.c index 13e179c196a..31d34780c08 100644 --- a/boot/scene_menu.c +++ b/boot/scene_menu.c @@ -77,6 +77,183 @@ struct scene_menitem *scene_menuitem_find_val(const struct scene_obj_menu *menu, return NULL; } +/* + * Reference column x-offsets (from the menu's left edge) for a 1366-pixel-wide + * reference design: the label sits at the menu inset, then the pointer, key and + * description columns. + */ +#define MENU_REF_WIDTH 1366 +#define MENU_REF_PTR_OFS 200 +#define MENU_REF_KEY_OFS 230 +#define MENU_REF_DESC_OFS 280 + +/* Minimum blank space kept between adjacent columns when compressed */ +#define MENU_COL_GAP 4 + +/** + * scene_menu_calc_cols() - Work out the x-offset of each menu column + * + * The reference design places the label, pointer, key and description columns + * at fixed offsets (menu_inset, 200, 230, 280) from the menu's left edge, + * scaled down proportionally to the actual display width. + * + * The blank space between columns is treated like an Android-style elastic + * spacer: it stretches up to the reference spacing when there is room and + * compresses (down to a minimum gap) when space is tight. Crucially the whole + * row is budgeted together against the available display width, so slack left + * between early columns is reclaimed to make room for a wide later column + * rather than letting that column run off-screen. Content is only clipped once + * even the fully-compressed row no longer fits. + * + * On a display at least as wide as the reference, every spacer reaches its + * reference value and the layout is pixel-identical to the reference design. + * + * scene_menu_calc_dims() equalises all item widths beforehand, so the first + * item's objects provide the per-column content widths. + * + * @scn: Scene containing the menu + * @menu: Menu to calculate columns for + * @label_ofsp: Returns the label column x-offset (may be NULL) + * @ptr_ofsp: Returns the pointer column x-offset (may be NULL) + * @key_ofsp: Returns the key column x-offset (may be NULL) + * @desc_ofsp: Returns the description column x-offset (may be NULL) + */ +static void scene_menu_calc_cols(struct scene *scn, struct scene_obj_menu *menu, + int *label_ofsp, int *ptr_ofsp, int *key_ofsp, + int *desc_ofsp) +{ + const struct expo_theme *theme = &scn->expo->theme; + struct expo *exp = scn->expo; + int label_w = 0, ptr_w = 0, key_w = 0, desc_w = 0; + int ref_ptr, ref_key, ref_desc; + int ref_sp_ptr, ref_sp_key, ref_sp_desc; + int flex_ptr, flex_key, flex_desc, total_flex; + int sp_ptr, sp_key, sp_desc, avail; + int label_ofs, ptr_ofs, key_ofs, desc_ofs; + int xsize = 0; + + if (exp->display) { + struct video_priv *vid = dev_get_uclass_priv(exp->display); + + xsize = vid->xsize; + } + + /* Scaled reference offsets (fall back to raw values if no display) */ + ref_ptr = xsize ? MENU_REF_PTR_OFS * xsize / MENU_REF_WIDTH : + MENU_REF_PTR_OFS; + ref_key = xsize ? MENU_REF_KEY_OFS * xsize / MENU_REF_WIDTH : + MENU_REF_KEY_OFS; + ref_desc = xsize ? MENU_REF_DESC_OFS * xsize / MENU_REF_WIDTH : + MENU_REF_DESC_OFS; + + if (!list_empty(&menu->item_head)) { + const struct scene_menitem *first; + struct scene_obj *obj; + + first = list_first_entry(&menu->item_head, + struct scene_menitem, sibling); + obj = scene_obj_find(scn, first->label_id, SCENEOBJT_NONE); + if (obj) + label_w = obj->bbox.x1 - obj->bbox.x0; + obj = scene_obj_find(scn, first->key_id, SCENEOBJT_NONE); + if (obj) + key_w = obj->bbox.x1 - obj->bbox.x0; + obj = scene_obj_find(scn, first->desc_id, SCENEOBJT_NONE); + if (obj) + desc_w = obj->bbox.x1 - obj->bbox.x0; + } + if (menu->pointer_id) + scene_obj_get_hw(scn, menu->pointer_id, &ptr_w); + + /* The label column starts at the menu inset (a fixed left indent) */ + label_ofs = theme->menu_inset; + + /* + * The blank space ('spacer') each column wants before it to land on + * its reference offset, and how much of each is stretch above the + * minimum gap (i.e. how much it can give up under pressure). + */ + ref_sp_ptr = ref_ptr - (label_ofs + label_w); + ref_sp_key = ref_key - (ref_ptr + ptr_w); + ref_sp_desc = ref_desc - (ref_key + key_w); + + flex_ptr = max(0, ref_sp_ptr - MENU_COL_GAP); + flex_key = max(0, ref_sp_key - MENU_COL_GAP); + flex_desc = max(0, ref_sp_desc - MENU_COL_GAP); + total_flex = flex_ptr + flex_key + flex_desc; + + /* Start every spacer fully compressed, then hand out the slack below */ + sp_ptr = MENU_COL_GAP; + sp_key = MENU_COL_GAP; + sp_desc = MENU_COL_GAP; + + if (menu->fill_x1) { + /* + * Justify mode: stretch the columns so the row fills out to + * menu->fill_x1 (an absolute x-coordinate). This lets the menu + * use the full available width instead of bunching on the left + * within the reference column band. The slack above the fully + * compressed layout is shared between the spacers - in + * proportion to their reference flex where there is any, else + * spread evenly. + */ + int min_right = label_ofs + label_w + ptr_w + key_w + desc_w + + 3 * MENU_COL_GAP; + int target = menu->fill_x1 - menu->obj.bbox.x0; + int extra = max(0, target - min_right); + + if (total_flex) { + sp_ptr += flex_ptr * extra / total_flex; + sp_key += flex_key * extra / total_flex; + sp_desc += flex_desc * extra / total_flex; + } else { + int third = extra / 3; + + sp_ptr += third; + sp_key += third; + sp_desc += extra - 2 * third; + } + } else { + /* + * Decide how much stretch we can afford. 'avail' is the space + * available above the fully-compressed layout (every spacer at + * MENU_COL_GAP): capped at total_flex it yields the exact + * reference layout, clamped at 0 it yields full compression. + * The whole row is budgeted at once, so slack between early + * columns is given up to make room for a wide later column + * instead of letting it run off-screen. With no display + * attached we assume ample width (reference layout). + */ + avail = total_flex; + if (xsize) { + int budget = xsize - menu->obj.bbox.x0; + int min_right = label_ofs + label_w + ptr_w + key_w + + desc_w + 3 * MENU_COL_GAP; + + avail = clamp(budget - min_right, 0, total_flex); + } + + if (total_flex) { + sp_ptr += flex_ptr * avail / total_flex; + sp_key += flex_key * avail / total_flex; + sp_desc += flex_desc * avail / total_flex; + } + } + + ptr_ofs = label_ofs + label_w + sp_ptr; + key_ofs = ptr_ofs + ptr_w + sp_key; + desc_ofs = key_ofs + key_w + sp_desc; + + if (label_ofsp) + *label_ofsp = label_ofs; + if (ptr_ofsp) + *ptr_ofsp = ptr_ofs; + if (key_ofsp) + *key_ofsp = key_ofs; + if (desc_ofsp) + *desc_ofsp = desc_ofs; +} + /** * update_pointers() - Update the pointer object and handle highlights * @@ -97,18 +274,21 @@ static int update_pointers(struct scene_obj_menu *menu, uint id, bool point) /* adjust the pointer object to point to the selected item */ if (menu->pointer_id && item && point) { - const struct expo_theme *theme = &scn->expo->theme; struct scene_obj *label; + int ptr_ofs; label = scene_obj_find(scn, item->label_id, SCENEOBJT_NONE); /* - * Place the pointer at the leftmost column (menu inset) so it - * is never overdrawn by the label text, which is rendered after - * the pointer in object order. + * Place the pointer in its own column, just to the right of the + * label. scene_menu_calc_cols() guarantees this offset clears + * the label content, so the label text (rendered after the + * pointer) never overdraws it, even on small displays. */ + scene_menu_calc_cols(scn, menu, NULL, &ptr_ofs, NULL, NULL); + ret = scene_obj_set_pos(scn, menu->pointer_id, - menu->obj.bbox.x0 + theme->menu_inset, + menu->obj.bbox.x0 + ptr_ofs, label->bbox.y0); if (ret < 0) return log_msg_ret("ptr", ret); @@ -265,52 +445,13 @@ int scene_menu_arrange(struct scene *scn, struct expo_arrange_info *arr, } /* - * Reserve space for the pointer character to the left of the label - * so that label text never overdraws the pointer. The pointer is - * rendered in object-head order *before* the per-item text objects, - * so if the label starts at the same x position it will overdraw it. - */ - int ptr_width = 0; - - if (menu->pointer_id) - scene_obj_get_hw(scn, menu->pointer_id, &ptr_width); - int label_ofs = theme->menu_inset + ptr_width + (ptr_width ? 2 : 0); - - /* - * Compute column positions dynamically from the actual item content - * widths so that columns never overlap, regardless of display size. - * scene_menu_calc_dims() has already equalised all item widths, so - * reading the first item's objects gives the column widths. - * - * Fall back to reference-design proportional offsets when the menu - * has no items (shouldn't happen in practice). + * Work out the label, pointer, key and description column offsets. + * These reproduce the reference layout on large displays and compress + * elastically on small ones (see scene_menu_calc_cols()). */ - int label_col_w = 0; - int key_col_w = 6; /* minimum: one bitmap char */ - int key_ofs, desc_ofs; - const int col_gap = 4; /* pixels between columns */ + int label_ofs, key_ofs, desc_ofs; - if (!list_empty(&menu->item_head)) { - const struct scene_menitem *first; - struct scene_obj *lobj, *kobj; - - first = list_first_entry(&menu->item_head, - struct scene_menitem, sibling); - lobj = scene_obj_find(scn, first->label_id, SCENEOBJT_NONE); - if (lobj) - label_col_w = lobj->bbox.x1 - lobj->bbox.x0; - kobj = scene_obj_find(scn, first->key_id, SCENEOBJT_NONE); - if (kobj) - key_col_w = kobj->bbox.x1 - kobj->bbox.x0; - } - - if (label_col_w > 0) { - key_ofs = label_ofs + label_col_w + col_gap; - desc_ofs = key_ofs + key_col_w + col_gap; - } else { - key_ofs = arr->xsize ? 230 * arr->xsize / 1366 : 230; - desc_ofs = arr->xsize ? 280 * arr->xsize / 1366 : 280; - } + scene_menu_calc_cols(scn, menu, &label_ofs, NULL, &key_ofs, &desc_ofs); sel_id = menu->cur_item_id; list_for_each_entry(item, &menu->item_head, sibling) { @@ -332,10 +473,8 @@ int scene_menu_arrange(struct scene *scn, struct expo_arrange_info *arr, selected = sel_id == item->id; /* - * Put the pointer at the leftmost column, then the label, - * then the key and the description. The pointer is placed - * by update_pointers() at x + menu_inset; the label starts - * just to the right of the pointer's character cell. + * Put the label on the left, then the pointer column (placed by + * update_pointers()), then the key and the description. */ ret = scene_obj_set_pos(scn, item->label_id, x + label_ofs, y); diff --git a/include/expo.h b/include/expo.h index 26e155f8cb7..03435a36c9a 100644 --- a/include/expo.h +++ b/include/expo.h @@ -83,12 +83,17 @@ struct expo_action { * @menuitem_gap_y: Gap between menu items in pixels * @menu_title_margin_x: Gap between right side of menu title and left size of * menu label + * @menu_pos_x: Absolute x-position (pixels) for the menu's left edge, or 0 to + * keep the position chosen when the menu was created. Lets a board with a + * small or unusually-proportioned display move the menu (and thus its + * item columns) without changing the default layout for everyone else. */ struct expo_theme { u32 font_size; u32 menu_inset; u32 menuitem_gap_y; u32 menu_title_margin_x; + u32 menu_pos_x; }; /** @@ -391,6 +396,10 @@ struct scene_obj_txt { * @title_id: ID of the title text, or 0 if none * @cur_item_id: ID of the current menu item, or 0 if none * @pointer_id: ID of the object pointing to the current selection + * @fill_x1: If non-zero, the absolute x-coordinate that the item columns + * should be justified (stretched) out to, so the row fills the available + * width instead of bunching on the left. 0 keeps the natural reference + * layout. * @item_head: List of items in the menu */ struct scene_obj_menu { @@ -398,6 +407,7 @@ struct scene_obj_menu { uint title_id; uint cur_item_id; uint pointer_id; + int fill_x1; struct list_head item_head; }; From 91bbd8aba16f561be8025ab0c03266b5f4d8c282 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 23 Jun 2026 12:00:38 +0400 Subject: [PATCH 43/78] expo: Reset console when exiting a graphical Expo menu When exiting a graphical Expo menu, the console cursor is left indented to the left edge of the last menu column, which wastes screen space and looks awkward. Reset the console when exiting the menu with e.g. Ctrl+C, so that the console can resume normally from top-left of the screen. Signed-off-by: Alexey Charkov --- cmd/bootflow.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cmd/bootflow.c b/cmd/bootflow.c index 081f64e12ce..5550587978f 100644 --- a/cmd/bootflow.c +++ b/cmd/bootflow.c @@ -17,6 +17,7 @@ #include #include #include +#include /** * report_bootflow_err() - Report where a bootflow failed @@ -137,6 +138,17 @@ __maybe_unused static int bootflow_handle_menu(struct bootstd_priv *std, std->cur_bootflow = bflow; *bflowp = bflow; } + + /* + * While rendering, the menu moves the vidconsole cursor to position its + * text columns, which also shifts the console's left margin (xstart). + * If we simply return, later console output is indented to the last + * menu column and the menu remains on screen. Clear the display and + * reset the cursor so the console continues cleanly from the top-left. + */ + if (!text_mode && exp->cons) + vidconsole_clear_and_reset(exp->cons); + expo_destroy(exp); if (ret) return ret; From def8d01099d9196a789e1648c87c3de4cd28208e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 23 Jun 2026 14:09:17 +0400 Subject: [PATCH 44/78] expo: bootflow_menu: truncate long strings in label and description Label and description strings in each menu entry can be arbitrarily long, but the menu box has a fixed width. To avoid awkward layout issues when the screen is small and/or the strings are overly long, limit the label width to 1/5 of the menu box width, and the description width to 2/3 of the menu box width, truncating the excess. Signed-off-by: Alexey Charkov --- boot/bootflow_menu.c | 116 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 4 deletions(-) diff --git a/boot/bootflow_menu.c b/boot/bootflow_menu.c index 5c299cc3bdb..3331269b5a8 100644 --- a/boot/bootflow_menu.c +++ b/boot/bootflow_menu.c @@ -21,6 +21,7 @@ #include #include #include "bootflow_internal.h" +#include "scene_internal.h" /** * struct menu_priv - information about the menu @@ -33,6 +34,94 @@ struct menu_priv { struct udevice *last_bootdev; }; +/* + * Caps on how much of the horizontal space inside the menu box each text + * column may occupy, expressed as a fraction of that space. The leftmost + * column (the bootdev label) is kept short so it never crowds out the rest, + * while the rightmost column (the OS/bootflow name) is allowed to be much + * wider but still bounded so it cannot run past the menu box. + */ +#define MENU_LABEL_MAX_NUM 1 /* 20% (1/5) for the label column */ +#define MENU_LABEL_MAX_DEN 5 +#define MENU_DESC_MAX_NUM 2 /* 2/3 for the description column */ +#define MENU_DESC_MAX_DEN 3 + +/** + * menu_trunc_strdup() - Duplicate a string, truncated to a maximum pixel width + * + * Copies @str into a newly allocated buffer, trimming it so that it occupies + * no more than @max_px pixels when rendered with a font whose nominal + * character width is @char_w. If the metrics are unknown (@char_w or @max_px + * not positive) the string is copied unchanged. + * + * @str: String to copy (must not be NULL) + * @max_px: Maximum width in pixels, or <= 0 for no limit + * @char_w: Nominal character width in pixels, or <= 0 if unknown + * Return: Newly allocated (possibly truncated) string, or NULL if out of memory + */ +static char *menu_trunc_strdup(const char *str, int max_px, int char_w) +{ + int max_chars; + char *out; + + if (char_w <= 0 || max_px <= 0) + return strdup(str); + + max_chars = max_px / char_w; + if (max_chars < 1) + max_chars = 1; + if (strlen(str) <= max_chars) + return strdup(str); + + out = malloc(max_chars + 1); + if (!out) + return NULL; + memcpy(out, str, max_chars); + out[max_chars] = '\0'; + + return out; +} + +/** + * menu_avail_width() - Work out the usable width inside the menu box + * + * Reads the geometry straight from the OBJ_BOX object: its bounding box gives + * the outer extent and its line width gives the border that eats into the + * interior on each side. This avoids duplicating the box placement maths from + * bootflow_menu_new(). + * + * @scn: Scene containing the menu box + * Return: Interior width of the menu box in pixels, or 0 if unknown + */ +static int menu_avail_width(struct scene *scn) +{ + struct scene_obj_box *box; + + box = scene_obj_find(scn, OBJ_BOX, SCENEOBJT_BOX); + if (!box) + return 0; + + return (box->obj.bbox.x1 - box->obj.bbox.x0) - 2 * box->width; +} + +/** + * menu_char_width() - Get the nominal character width of the menu font + * + * @exp: Expo containing the menu + * Return: Nominal character width in pixels, or 0 if unknown + */ +static int menu_char_width(struct expo *exp) +{ + struct vidconsole_priv *vc; + + if (!exp->cons) + return 0; + + vc = dev_get_uclass_priv(exp->cons); + + return vc->x_charsize; +} + int bootflow_menu_new(struct expo **expp, int width, int height, int char_h) { /* @@ -175,10 +264,12 @@ int bootflow_menu_add(struct expo *exp, struct bootflow *bflow, int seq, struct scene **scnp) { struct menu_priv *priv = exp->priv; - char str[2], *label, *key; + char str[2], *label, *key, *desc; + const char *desc_src; struct udevice *media; struct scene *scn; const char *name; + int avail, char_w; uint preview_id; uint scene_id; bool add_gap; @@ -201,13 +292,31 @@ int bootflow_menu_add(struct expo *exp, struct bootflow *bflow, int seq, name = "usb"; else name = media->name; - label = strdup(name); + /* + * Trim the label and description columns to a fraction of the space + * inside the menu box so that long bootdev or OS names cannot push the + * layout off the right edge on small displays. + */ + avail = menu_avail_width(scn); + char_w = menu_char_width(exp); + + label = menu_trunc_strdup(name, avail * MENU_LABEL_MAX_NUM / + MENU_LABEL_MAX_DEN, char_w); if (!label) { free(key); return log_msg_ret("nam", -ENOMEM); } + desc_src = bflow->os_name ? bflow->os_name : bflow->name; + desc = menu_trunc_strdup(desc_src, avail * MENU_DESC_MAX_NUM / + MENU_DESC_MAX_DEN, char_w); + if (!desc) { + free(label); + free(key); + return log_msg_ret("dsc", -ENOMEM); + } + add_gap = priv->last_bootdev != bflow->dev; /* disable this gap for now, since it looks a little ugly */ @@ -218,8 +327,7 @@ int bootflow_menu_add(struct expo *exp, struct bootflow *bflow, int seq, ret |= scene_txt_str(scn, "label", ITEM_LABEL + seq, STR_LABEL + seq, label, NULL); ret |= scene_txt_str(scn, "desc", ITEM_DESC + seq, STR_DESC + seq, - bflow->os_name ? bflow->os_name : - bflow->name, NULL); + desc, NULL); ret |= scene_txt_str(scn, "key", ITEM_KEY + seq, STR_KEY + seq, key, NULL); preview_id = 0; From 6d44d448191e1b58531901832254d0600de8a79d Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 6 May 2026 17:00:12 +0400 Subject: [PATCH 45/78] Enable graphical boot menu with button navigation on Flipper One Signed-off-by: Alexey Charkov --- configs/flipper-one-rk3576_defconfig | 7 +++++++ include/configs/flipper-one-rk3576.h | 3 +++ 2 files changed, 10 insertions(+) diff --git a/configs/flipper-one-rk3576_defconfig b/configs/flipper-one-rk3576_defconfig index 626b63678fc..956cd64ba93 100644 --- a/configs/flipper-one-rk3576_defconfig +++ b/configs/flipper-one-rk3576_defconfig @@ -11,6 +11,11 @@ CONFIG_DEBUG_UART_BASE=0x2AD40000 CONFIG_DEBUG_UART_CLOCK=24000000 CONFIG_PCI=y CONFIG_DEBUG_UART=y +CONFIG_BOOTDELAY=0 +CONFIG_AUTOBOOT_KEYED=y +CONFIG_AUTOBOOT_PROMPT="Autoboot in %d seconds, to stop use 's' key\n" +CONFIG_AUTOBOOT_STOP_STR="s" +CONFIG_BOOTCOMMAND="bootflow scan -l; if bootflow menu; then cls; bootflow boot; else run bootcmd; fi" CONFIG_DEFAULT_FDT_FILE="rockchip/rk3576-flipper-one-rev-f0b0c1.dtb" # CONFIG_DISPLAY_CPUINFO is not set CONFIG_CYCLIC_MAX_CPU_TIME_US=50000 @@ -42,6 +47,8 @@ CONFIG_BUTTON_GPIO=y CONFIG_ROCKCHIP_GPIO=y CONFIG_DM_PCA953X=y CONFIG_SYS_I2C_ROCKCHIP=y +CONFIG_DM_KEYBOARD=y +CONFIG_FLIPPER_ONE_BTNS=y CONFIG_SUPPORT_EMMC_RPMB=y CONFIG_MMC_DW=y CONFIG_MMC_DW_ROCKCHIP=y diff --git a/include/configs/flipper-one-rk3576.h b/include/configs/flipper-one-rk3576.h index 62e43dca3d3..f0c817bb10e 100644 --- a/include/configs/flipper-one-rk3576.h +++ b/include/configs/flipper-one-rk3576.h @@ -9,9 +9,12 @@ #define __CONFIG_FLIPPER_ONE_RK3576_H #define ROCKCHIP_DEVICE_SETTINGS \ + "stdin=serial,flipper-one-btns\0" \ "stdout=serial,vidconsole\0" \ "stderr=serial,vidconsole\0" +#define BOOT_TARGETS "mmc0 scsi" + #include "rk3576_common.h" #endif /* __CONFIG_FLIPPER_ONE_RK3576_H */ From 1377b12275bc0ab550c899debb1c7d7652494ae9 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 24 Jun 2026 13:57:46 +0400 Subject: [PATCH 46/78] expo: bootflow_menu: drop misleading wording in prompt text Existing prompts in the bootflow menu appear to have been copied verbatim from GRUB, and current U-boot code doesn't implement the functionality implied by the wording (edit arguments, drop to console, autoboot). Remove the misleading wording from the prompts, which also makes them short enough to fit below the menu box for a cleaner layout. Signed-off-by: Alexey Charkov --- boot/bootflow_menu.c | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/boot/bootflow_menu.c b/boot/bootflow_menu.c index 3331269b5a8..e183a34fc7a 100644 --- a/boot/bootflow_menu.c +++ b/boot/bootflow_menu.c @@ -163,7 +163,7 @@ int bootflow_menu_new(struct expo **expp, int width, int height, int char_h) if (ret < 0) return log_msg_ret("bmb", ret); ret |= scene_obj_set_bbox(scn, OBJ_BOX, SX(30), SY(90), - width - SX(30), height - max(SY(48), 6 * char_h)); + width - SX(30), height - max(SY(48), 2 * char_h)); ret = scene_menu(scn, "main", OBJ_MENU, &menu); ret |= scene_obj_set_pos(scn, OBJ_MENU, SX(100), SY(100)); @@ -203,18 +203,13 @@ int bootflow_menu_new(struct expo **expp, int width, int height, int char_h) } ret |= scene_txt_str(scn, "prompt1a", OBJ_PROMPT1A, STR_PROMPT1A, - "Use the \x18 and \x19 keys to select which entry is highlighted.", + "Use the \x18 and \x19 keys to switch entries", NULL); ret |= scene_txt_str(scn, "prompt1b", OBJ_PROMPT1B, STR_PROMPT1B, - "Use the UP and DOWN keys to select which entry is highlighted.", + "Use the UP and DOWN keys to switch entries", NULL); ret |= scene_txt_str(scn, "prompt2", OBJ_PROMPT2, STR_PROMPT2, - "Press enter to boot the selected OS, 'e' to edit the commands " - "before booting or 'c' for a command-line. ESC to return to " - "previous menu", NULL); - ret |= scene_txt_str(scn, "autoboot", OBJ_AUTOBOOT, STR_AUTOBOOT, - "The highlighted entry will be executed automatically in %ds.", - NULL); + "Press Enter to boot, ESC to exit", NULL); /* * Prompt1A/B: up to 2 wrapped lines (navigation hint). * Prompt2: up to 4 wrapped lines (boot instructions). @@ -222,28 +217,24 @@ int bootflow_menu_new(struct expo **expp, int width, int height, int char_h) * on small displays; on large displays the SY() values dominate. */ ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1A, 0, - min(SY(590), height - 6 * char_h), + height - max(SY(48), 2 * char_h), SCENEOB_DISPLAY_MAX, - min(SY(590), height - 6 * char_h) + max(SY(30), 2 * char_h)); + height - max(SY(24), char_h)); ret |= scene_obj_set_bbox(scn, OBJ_PROMPT1B, 0, - min(SY(620), height - 6 * char_h), + height - max(SY(48), 2 * char_h), SCENEOB_DISPLAY_MAX, - min(SY(620), height - 6 * char_h) + max(SY(30), 2 * char_h)); + height - max(SY(24), char_h)); ret |= scene_obj_set_bbox(scn, OBJ_PROMPT2, SX(100), - min(SY(650), height - 4 * char_h), + height - max(SY(24), char_h), width - SX(100), height); - ret |= scene_obj_set_bbox(scn, OBJ_AUTOBOOT, 0, height, - SCENEOB_DISPLAY_MAX, height + SY(30)); ret |= scene_obj_set_halign(scn, OBJ_PROMPT1A, SCENEOA_CENTRE); ret |= scene_obj_set_halign(scn, OBJ_PROMPT1B, SCENEOA_CENTRE); ret |= scene_obj_set_halign(scn, OBJ_PROMPT2, SCENEOA_CENTRE); ret |= scene_obj_set_valign(scn, OBJ_PROMPT2, SCENEOA_CENTRE); - ret |= scene_obj_set_halign(scn, OBJ_AUTOBOOT, SCENEOA_CENTRE); use_font = IS_ENABLED(CONFIG_CONSOLE_TRUETYPE); scene_obj_set_hide(scn, OBJ_PROMPT1A, use_font); scene_obj_set_hide(scn, OBJ_PROMPT1B, !use_font); - scene_obj_set_hide(scn, OBJ_AUTOBOOT, use_font); ret |= scene_txt_str(scn, "cur_item", OBJ_POINTER, STR_POINTER, ">", NULL); From a3d61e5f3c53cada3ff3c237b29e1bbcd6d4bd3f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 29 Jun 2026 21:33:38 +0400 Subject: [PATCH 47/78] rockchip: Remove RK3576 pinctrl binding from U-Boot pre-relocation phase The pinctrl driver has to scan over 200 subnodes of its DT node, and this takes ~0.6s with the D-cache off in board_init_f. Given that the SPL has already muxed the console and PMIC pins, there is no urgency to bind pinctrl in board_init_f. Instead, defer the binding to board_init_r, which takes ~13ms with the D-cache on. Signed-off-by: Alexey Charkov --- arch/arm/dts/rk3576-u-boot.dtsi | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/arch/arm/dts/rk3576-u-boot.dtsi b/arch/arm/dts/rk3576-u-boot.dtsi index 8db9495c6d4..1b144c3d20e 100644 --- a/arch/arm/dts/rk3576-u-boot.dtsi +++ b/arch/arm/dts/rk3576-u-boot.dtsi @@ -124,7 +124,16 @@ }; &pinctrl { - bootph-all; + /* + * Keep pinctrl in SPL (bootph-pre-ram), but NOT in U-Boot proper's + * board_init_f: bootph-all would force a pre-relocation bind, and the + * driver's .bind = dm_scan_fdt_dev then walks every pin-group child + * against the flat DT with the D-cache off (~0.6s). With bootph-pre-ram + * the node binds in board_init_r instead (live tree + cache, ~13ms). + * Console/PMIC pins are already muxed by SPL, so pre-reloc pinctrl is + * not needed here. + */ + bootph-pre-ram; }; &pmu1_grf { From fe739d6ac9d759dcce51e90752e4a6278493a4d9 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 29 Jun 2026 21:39:51 +0400 Subject: [PATCH 48/78] board_f: Make printing reset information optional Current code tries to print information about the reset method whenever sysreset support is enabled. This causes the core to try to bind the sysreset device in board_init_f, even if the driver doesn't support reset status reporting, causing pure waste on some platforms (e.g. on Rockchip RK3576 it increases the boot time by ~0.4s even though the driver doesn't support reset status reporting). Make the printing of reset information optional, so that it can be enabled only on platforms where it is supported and useful. Only four sysreset drivers actually implement the get_status() op, so default the new symbol to y wherever one of them can be in use to avoid regressing those platforms: - sysreset_sandbox - sysreset_mpc83xx - sysreset_psci, whose weak stub get_status() is only overridden by i.MX9; - the pca9450 PMIC sysreset child driver. Signed-off-by: Alexey Charkov --- common/board_f.c | 4 ++-- drivers/sysreset/Kconfig | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/common/board_f.c b/common/board_f.c index 85b888d4bb8..7d1f80cd7fb 100644 --- a/common/board_f.c +++ b/common/board_f.c @@ -122,7 +122,7 @@ static int display_text_info(void) return 0; } -#ifdef CONFIG_SYSRESET +#ifdef CONFIG_SYSRESET_PRINT_RESETINFO static int print_resetinfo(void) { struct udevice *dev; @@ -963,7 +963,7 @@ static void initcall_run_f(void) INITCALL(display_options); /* say that we are here */ INITCALL(display_text_info); /* show debugging info if required */ INITCALL(checkcpu); -#if CONFIG_IS_ENABLED(SYSRESET) +#if CONFIG_IS_ENABLED(SYSRESET_PRINT_RESETINFO) INITCALL(print_resetinfo); #endif /* display cpu info (and speed) */ diff --git a/drivers/sysreset/Kconfig b/drivers/sysreset/Kconfig index 16ef434a8d9..a046dd02e66 100644 --- a/drivers/sysreset/Kconfig +++ b/drivers/sysreset/Kconfig @@ -43,6 +43,20 @@ config VPL_SYSRESET if SYSRESET +config SYSRESET_PRINT_RESETINFO + bool "Print reset information during boot" + default y if SANDBOX + default y if SYSRESET_MPC83XX + default y if SYSRESET_PSCI && ARCH_IMX9 + default y if DM_PMIC_PCA9450 + help + Print information about the cause of the last reset during early + boot, as reported by the get_status() method of the sysreset + drivers. This requires probing all sysreset devices in + board_init_f(), which costs boot time, so only enable it if at + least one of the sysreset drivers in use actually implements + get_status(). + config SYSRESET_CMD_RESET bool "sysreset implementation of the reset command" default y From 4d5af4031f1ccb65072203905af94fe9a8c86bc9 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 30 Jul 2026 18:55:12 +0400 Subject: [PATCH 49/78] lib: div64: fix off-by-one shift in div64_u64() and div64_u64_rem() fls() counts bits starting from 1, so shifting the operands right by 1 + fls(high) discards one more bit of the divisor than intended. Both functions estimate the quotient from the shifted operands and then fix it up with a single decrement/increment, so an estimate that is off by more than one cannot be repaired and the result comes out wrong. This only affects BITS_PER_LONG == 32, where these are the out-of-line implementations; on 64-bit the header provides plain C division. The error is only reachable when the quotient is large, which needs a divisor just above 2^32. For example: dividend = 15559272575191414037 divisor = 4333540799 expected = 3590429465 actual = 3590429468 (off by 3) A sweep over 6.4M random operand pairs, stratified by the width of the divisor's high word, mismatches a __int128 reference 8260 times before this change and never after it. All failures have a divisor with one or two significant bits above bit 32; uniformly random 64-bit divisors are closer to 2^63 and yield quotients of ~1, which hides the problem. Port of Linux commit cdc94a374931 ("lib/div64.c: off by one in shift"), which fixed the same code and cites [1]. In-tree users of div64_u64() that are built for 32-bit targets include the Aspeed, Meson and Cadence TTC PWM drivers, the Versaclock and wrpll-cln28hpc clock drivers, and the DWC3 USB core. Link: https://bugzilla.kernel.org/show_bug.cgi?id=202391 [1] Fixes: 0342e335ba88 ("lib: div64: sync with Linux") Signed-off-by: Alexey Charkov --- lib/div64.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/div64.c b/lib/div64.c index 779d7521f69..14d402ce4c7 100644 --- a/lib/div64.c +++ b/lib/div64.c @@ -109,7 +109,7 @@ u64 div64_u64_rem(u64 dividend, u64 divisor, u64 *remainder) quot = div_u64_rem(dividend, divisor, &rem32); *remainder = rem32; } else { - int n = 1 + fls(high); + int n = fls(high); quot = div_u64(dividend >> n, divisor >> n); if (quot != 0) @@ -147,7 +147,7 @@ u64 div64_u64(u64 dividend, u64 divisor) if (high == 0) { quot = div_u64(dividend, divisor); } else { - int n = 1 + fls(high); + int n = fls(high); quot = div_u64(dividend >> n, divisor >> n); if (quot != 0) From a353ac176237311921d7d60ca8145f52124f6d49 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 30 Jul 2026 18:59:26 +0400 Subject: [PATCH 50/78] lib: div64: use abs64() for the 64-bit operands of div64_s64() Both operands of div64_s64() are s64, but U-Boot's abs() is not 64-bit safe. Unlike its Linux counterpart, which dispatches on the argument type down to long long, U-Boot's abs() evaluates its argument as int whenever sizeof(x) != sizeof(long) and yields a long. The header even says so: "abs() should not be used for 64-bit types (s64, u64, long long) - use abs64() for those." So on BITS_PER_LONG == 32 both operands are silently truncated to 32 bits before the division. Simulating the macro with long narrowed to 32 bits shows what reaches div64_u64(): x= -4294967296 abs()= 0 abs64()= 4294967296 x= -5000000000 abs()= 705032704 abs64()= 5000000000 x=-9223372036854775807 abs()= 1 abs64()= 9223372036854775807 A zero from the first case makes the subsequent division a divide by zero rather than merely imprecise. div64_s64() has no in-tree callers today, so this is a latent bug and not a regression. Note that the abs() in div_s64_rem() is correct as-is and deliberately left alone. Fixes: 0342e335ba88 ("lib: div64: sync with Linux") Signed-off-by: Alexey Charkov --- lib/div64.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/div64.c b/lib/div64.c index 14d402ce4c7..f5624279086 100644 --- a/lib/div64.c +++ b/lib/div64.c @@ -171,7 +171,12 @@ s64 div64_s64(s64 dividend, s64 divisor) { s64 quot, t; - quot = div64_u64(abs(dividend), abs(divisor)); + /* + * Unlike Linux, U-Boot's abs() is not 64-bit safe: it evaluates its + * argument as int when sizeof(x) != sizeof(long), so both operands + * need abs64() here. + */ + quot = div64_u64(abs64(dividend), abs64(divisor)); t = (dividend ^ divisor) >> 63; return (quot ^ t) - t; From 7bf77181b2affadc01fc100ac3cf60bdfee5478f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 30 Jul 2026 19:01:52 +0400 Subject: [PATCH 51/78] lib: div64: inline __iter_div_u64_rem() into its only user __iter_div_u64_rem() is only split out in Linux for its vDSO header, which has no use in U-Boot. Inline the body of the helper directly into its only user, which is the iter_div_u64_rem() wrapper in this same file, and drop the static inline, so that a subsequent resync of include/linux/math64.h against Linux does not have to carry an exception for it. include/vdso/math64.h is deliberately not created: its only other resident, mul_u64_u32_add_u64_shr(), has no U-Boot user. No functional change: the generated code is identical, verified by comparing objdump -d of lib/div64.o before and after for evb-rk3288-rk808_defconfig. The compiler was already inlining the sole call. iter_div_u64_rem() itself has no in-tree callers either, but it is upstream API and is left in place. Signed-off-by: Alexey Charkov --- include/linux/math64.h | 19 ------------------- lib/div64.c | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/include/linux/math64.h b/include/linux/math64.h index 70a7ee3ff1d..eb560e3183a 100644 --- a/include/linux/math64.h +++ b/include/linux/math64.h @@ -118,25 +118,6 @@ static inline s64 div_s64(s64 dividend, s32 divisor) u32 iter_div_u64_rem(u64 dividend, u32 divisor, u64 *remainder); -static __always_inline u32 -__iter_div_u64_rem(u64 dividend, u32 divisor, u64 *remainder) -{ - u32 ret = 0; - - while (dividend >= divisor) { - /* The following asm() prevents the compiler from - optimising this loop into a modulo operation. */ - asm("" : "+rm"(dividend)); - - dividend -= divisor; - ret++; - } - - *remainder = dividend; - - return ret; -} - #ifndef mul_u32_u32 /* * Many a GCC version messes this up and generates a 64x64 mult :-( diff --git a/lib/div64.c b/lib/div64.c index f5624279086..9db7499fcd3 100644 --- a/lib/div64.c +++ b/lib/div64.c @@ -192,6 +192,21 @@ EXPORT_SYMBOL(div64_s64); */ u32 iter_div_u64_rem(u64 dividend, u32 divisor, u64 *remainder) { - return __iter_div_u64_rem(dividend, divisor, remainder); + u32 ret = 0; + + while (dividend >= divisor) { + /* + * The following asm() prevents the compiler from + * optimising this loop into a modulo operation. + */ + asm("" : "+rm"(dividend)); + + dividend -= divisor; + ret++; + } + + *remainder = dividend; + + return ret; } EXPORT_SYMBOL(iter_div_u64_rem); From b64ea66ee5c4f55888fe748be2d63aeb6e2deebd Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 30 Jul 2026 19:11:10 +0400 Subject: [PATCH 52/78] lib: div64: sync with Linux v7.1 Bring the structure of this file in line with Linux v7.1 lib/math/div64.c, so that future resyncs are a near-verbatim diff. No functional change: the compiled lib/div64.o is byte-identical before and after, verified by comparing objdump -d output for both evb-rk3288-rk808_defconfig (32-bit, where these out-of-line helpers are actually built) and generic-rk3576_defconfig (64-bit). - Add the SPDX license identifier, as upstream did in commit b24413180f56 ("License cleanup: add SPDX GPL-2.0 license identifier to files with no license"). - Demote the kernel-doc blocks on div64_u64_rem() and div64_u64() to plain comments and drop the one on div64_s64() entirely, following upstream commit d28a1de5d112 ("math64: favor kernel-doc from header files"). The kernel-doc for these lands in in a later patch of this series; keeping it in both places would only let the two copies drift apart. - Guard iter_div_u64_rem() with #ifndef iter_div_u64_rem, matching upstream, so an arch can override it the same way it can override the other helpers here. Two U-Boot-local deviations are kept deliberately and are now marked as such so that the next resync does not silently drop them: - __div64_32() carries a no_instrument_function attribute, needed because CONFIG_TRACE builds with -finstrument-functions and this function is reachable from tracing code via do_div(). - The includes stay as they are. provides U-Boot's no-op EXPORT_SYMBOL() in place of upstream's , and provides abs()/abs64() in place of upstream's , which U-Boot does not have. The u32/u64 spelling is also left alone rather than converted to Linux's uint32_t/uint64_t: U-Boot's already made the opposite choice, and matching Linux here would make the two files inconsistent for no benefit. Signed-off-by: Alexey Charkov --- lib/div64.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/div64.c b/lib/div64.c index 9db7499fcd3..e3553c3956e 100644 --- a/lib/div64.c +++ b/lib/div64.c @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0 /* * Copyright (C) 2003 Bernardo Innocenti * @@ -27,9 +28,9 @@ #ifndef __div64_32 /* - * Don't instrument this function as it may be called from tracing code, since - * it needs to read the timer and this often requires calling do_div(), which - * calls this function. + * U-Boot addition, not present upstream: don't instrument this function as it + * may be called from tracing code, since it needs to read the timer and this + * often requires calling do_div(), which calls this function. */ uint32_t __attribute__((weak, no_instrument_function)) __div64_32(u64 *n, u32 base) @@ -87,7 +88,7 @@ s64 div_s64_rem(s64 dividend, s32 divisor, s32 *remainder) EXPORT_SYMBOL(div_s64_rem); #endif -/** +/* * div64_u64_rem - unsigned 64bit divide with 64bit divisor and remainder * @dividend: 64bit dividend * @divisor: 64bit divisor @@ -127,7 +128,7 @@ u64 div64_u64_rem(u64 dividend, u64 divisor, u64 *remainder) EXPORT_SYMBOL(div64_u64_rem); #endif -/** +/* * div64_u64 - unsigned 64bit divide with 64bit divisor * @dividend: 64bit dividend * @divisor: 64bit divisor @@ -161,11 +162,6 @@ u64 div64_u64(u64 dividend, u64 divisor) EXPORT_SYMBOL(div64_u64); #endif -/** - * div64_s64 - signed 64bit divide with 64bit divisor - * @dividend: 64bit dividend - * @divisor: 64bit divisor - */ #ifndef div64_s64 s64 div64_s64(s64 dividend, s64 divisor) { @@ -190,6 +186,7 @@ EXPORT_SYMBOL(div64_s64); * Iterative div/mod for use when dividend is not expected to be much * bigger than divisor. */ +#ifndef iter_div_u64_rem u32 iter_div_u64_rem(u64 dividend, u32 divisor, u64 *remainder) { u32 ret = 0; @@ -210,3 +207,4 @@ u32 iter_div_u64_rem(u64 dividend, u32 divisor, u64 *remainder) return ret; } EXPORT_SYMBOL(iter_div_u64_rem); +#endif From b6d12c35e8278234bedb4cbbd4bd8f9dd03f191e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 30 Jul 2026 20:55:10 +0400 Subject: [PATCH 53/78] lib: math64: sync with Linux v7.1 U-Boot's copy was last synced with Linux in 2017 by commit 0342e335ba88 ("lib: div64: sync with Linux"). Since then it has only been touched twice locally, and both times in ways that increased the divergence rather than tracking upstream. Take the file from the v7.1 tag. The immediate motivation is DIV64_U64_ROUND_UP(). Commit 3adc17f60bf8 ("lib: div64: Add support for round up of div64_u64") added it inside the #if BITS_PER_LONG == 64 branch, whereas Linux defines it unconditionally after that block. Any 32-bit user therefore fails to compile: error: implicit declaration of function 'DIV64_U64_ROUND_UP'; did you mean 'DIV_ROUND_UP'? This has been surfaced while reworking fractional PLL logic for Rockchip RK3588/RK3576, which unexpectedly failed to compile on 32-bit variants [1]. Newly available as a result: DIV64_U64_ROUND_UP() on 32-bit, DIV_U64_ROUND_UP(), DIV64_U64_ROUND_CLOSEST(), DIV_U64_ROUND_CLOSEST(), DIV_S64_ROUND_CLOSEST(), roundup_u64(), add_u64_u32() and mul_s64_u64_shr(), plus upstream's kernel-doc for everything that was already here and __always_inline on mul_u64_u32_shr()/mul_u64_u64_shr(). The file is otherwise a verbatim copy of v7.1. Four deviations: - The includes. U-Boot has no , no and no . do_div() comes from U-Boot's , BITS_PER_LONG from , and abs64() from , which is where U-Boot keeps the rest of what upstream puts in . - mul_s64_u64_shr() uses abs64() rather than abs(), because U-Boot's abs() evaluates its argument as int for anything that is not a long. - mul_u64_add_u64_div_u64() and the mul_u64_u64_div_u64() and mul_u64_u64_div_u64_roundup() macros built on it are not ported. They would need ~110 lines of lib/math/div64.c brought along, plus a u128 type and OPTIMIZER_HIDE_VAR(), and U-Boot has no users for them yet. - abs_diff(), added locally by commit f1f4a1d1d835 ("compat: math64: add abs_diff()"), is kept. Upstream has it in . Build tested for evb-rk3288-rk808 (32-bit LE), generic-rk3576 (64-bit LE), malta (32-bit BE, which is what compiles the __BIG_ENDIAN union paths in mul_u64_u64_shr() and mul_u64_u32_div()), sandbox and tools-only, with both gcc and clang. All six DIV*_ROUND_* macros plus roundup_u64(), mul_s64_u64_shr(), add_u64_u32() and abs_diff() were additionally instantiated on 32-bit ARM to confirm they now resolve. Closes: https://patchwork.ozlabs.org/project/uboot/patch/20260723-rk3588-fracpll-v2-2-382d113f3a80@flipper.net/#3731361 [1] Signed-off-by: Alexey Charkov --- include/linux/math64.h | 182 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 171 insertions(+), 11 deletions(-) diff --git a/include/linux/math64.h b/include/linux/math64.h index eb560e3183a..78c184eb85b 100644 --- a/include/linux/math64.h +++ b/include/linux/math64.h @@ -1,8 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0 */ #ifndef _LINUX_MATH64_H #define _LINUX_MATH64_H #include #include +#include #include #if BITS_PER_LONG == 64 @@ -12,6 +14,11 @@ /** * div_u64_rem - unsigned 64bit divide with 32bit divisor with remainder + * @dividend: unsigned 64bit dividend + * @divisor: unsigned 32bit divisor + * @remainder: pointer to unsigned 32bit remainder + * + * Return: sets ``*remainder``, then returns dividend / divisor * * This is commonly provided by 32bit archs to provide an optimized 64bit * divide. @@ -24,6 +31,11 @@ static inline u64 div_u64_rem(u64 dividend, u32 divisor, u32 *remainder) /** * div_s64_rem - signed 64bit divide with 32bit divisor with remainder + * @dividend: signed 64bit dividend + * @divisor: signed 32bit divisor + * @remainder: pointer to signed 32bit remainder + * + * Return: sets ``*remainder``, then returns dividend / divisor */ static inline s64 div_s64_rem(s64 dividend, s32 divisor, s32 *remainder) { @@ -33,6 +45,11 @@ static inline s64 div_s64_rem(s64 dividend, s32 divisor, s32 *remainder) /** * div64_u64_rem - unsigned 64bit divide with 64bit divisor and remainder + * @dividend: unsigned 64bit dividend + * @divisor: unsigned 64bit divisor + * @remainder: pointer to unsigned 64bit remainder + * + * Return: sets ``*remainder``, then returns dividend / divisor */ static inline u64 div64_u64_rem(u64 dividend, u64 divisor, u64 *remainder) { @@ -42,17 +59,22 @@ static inline u64 div64_u64_rem(u64 dividend, u64 divisor, u64 *remainder) /** * div64_u64 - unsigned 64bit divide with 64bit divisor + * @dividend: unsigned 64bit dividend + * @divisor: unsigned 64bit divisor + * + * Return: dividend / divisor */ static inline u64 div64_u64(u64 dividend, u64 divisor) { return dividend / divisor; } -#define DIV64_U64_ROUND_UP(ll, d) \ - ({ u64 _tmp = (d); div64_u64((ll) + _tmp - 1, _tmp); }) - /** * div64_s64 - signed 64bit divide with 64bit divisor + * @dividend: signed 64bit dividend + * @divisor: signed 64bit divisor + * + * Return: dividend / divisor */ static inline s64 div64_s64(s64 dividend, s64 divisor) { @@ -92,10 +114,14 @@ extern s64 div64_s64(s64 dividend, s64 divisor); /** * div_u64 - unsigned 64bit divide with 32bit divisor + * @dividend: unsigned 64bit dividend + * @divisor: unsigned 32bit divisor * * This is the most common 64bit divide and should be used if possible, * as many 32bit archs can optimize this variant better than a full 64bit * divide. + * + * Return: dividend / divisor */ #ifndef div_u64 static inline u64 div_u64(u64 dividend, u32 divisor) @@ -107,6 +133,10 @@ static inline u64 div_u64(u64 dividend, u32 divisor) /** * div_s64 - signed 64bit divide with 32bit divisor + * @dividend: signed 64bit dividend + * @divisor: signed 32bit divisor + * + * Return: dividend / divisor */ #ifndef div_s64 static inline s64 div_s64(s64 dividend, s32 divisor) @@ -128,17 +158,28 @@ static inline u64 mul_u32_u32(u32 a, u32 b) } #endif +#ifndef add_u64_u32 +/* + * Many a GCC version also messes this up. + * Zero extending b and then spilling everything to stack. + */ +static inline u64 add_u64_u32(u64 a, u32 b) +{ + return a + b; +} +#endif + #if defined(CONFIG_ARCH_SUPPORTS_INT128) && defined(__SIZEOF_INT128__) #ifndef mul_u64_u32_shr -static inline u64 mul_u64_u32_shr(u64 a, u32 mul, unsigned int shift) +static __always_inline u64 mul_u64_u32_shr(u64 a, u32 mul, unsigned int shift) { return (u64)(((unsigned __int128)a * mul) >> shift); } #endif /* mul_u64_u32_shr */ #ifndef mul_u64_u64_shr -static inline u64 mul_u64_u64_shr(u64 a, u64 mul, unsigned int shift) +static __always_inline u64 mul_u64_u64_shr(u64 a, u64 mul, unsigned int shift) { return (u64)(((unsigned __int128)a * mul) >> shift); } @@ -147,18 +188,14 @@ static inline u64 mul_u64_u64_shr(u64 a, u64 mul, unsigned int shift) #else #ifndef mul_u64_u32_shr -static inline u64 mul_u64_u32_shr(u64 a, u32 mul, unsigned int shift) +static __always_inline u64 mul_u64_u32_shr(u64 a, u32 mul, unsigned int shift) { - u32 ah, al; + u32 ah = a >> 32, al = a; u64 ret; - al = a; - ah = a >> 32; - ret = mul_u32_u32(al, mul) >> shift; if (ah) ret += mul_u32_u32(ah, mul) << (32 - shift); - return ret; } #endif /* mul_u64_u32_shr */ @@ -209,6 +246,27 @@ static inline u64 mul_u64_u64_shr(u64 a, u64 b, unsigned int shift) #endif +#ifndef mul_s64_u64_shr +static inline u64 mul_s64_u64_shr(s64 a, u64 b, unsigned int shift) +{ + u64 ret; + + /* + * Extract the sign before the multiplication and put it back + * afterwards if needed. + * + * Note: Linux uses abs() here, which U-Boot's abs() cannot do: + * it evaluates its argument as int for anything that is not a long. + */ + ret = mul_u64_u64_shr(abs64(a), b, shift); + + if (a < 0) + ret = -((s64) ret); + + return ret; +} +#endif /* mul_s64_u64_shr */ + #ifndef mul_u64_u32_div static inline u64 mul_u64_u32_div(u64 a, u32 mul, u32 divisor) { @@ -238,6 +296,108 @@ static inline u64 mul_u64_u32_div(u64 a, u32 mul, u32 divisor) } #endif /* mul_u64_u32_div */ +/* + * Not ported from Linux: mul_u64_add_u64_div_u64() and the + * mul_u64_u64_div_u64() / mul_u64_u64_div_u64_roundup() macros built on it. + * They need ~110 lines of lib/math/div64.c brought along, plus a u128 type + * and OPTIMIZER_HIDE_VAR(), and U-Boot has no users for them yet. Port the + * lot when the first one turns up. + */ + +/** + * DIV64_U64_ROUND_UP - unsigned 64bit divide with 64bit divisor rounded up + * @ll: unsigned 64bit dividend + * @d: unsigned 64bit divisor + * + * Divide unsigned 64bit dividend by unsigned 64bit divisor + * and round up. + * + * Return: dividend / divisor rounded up + */ +#define DIV64_U64_ROUND_UP(ll, d) \ + ({ u64 _tmp = (d); div64_u64((ll) + _tmp - 1, _tmp); }) + +/** + * DIV_U64_ROUND_UP - unsigned 64bit divide with 32bit divisor rounded up + * @ll: unsigned 64bit dividend + * @d: unsigned 32bit divisor + * + * Divide unsigned 64bit dividend by unsigned 32bit divisor + * and round up. + * + * Return: dividend / divisor rounded up + */ +#define DIV_U64_ROUND_UP(ll, d) \ + ({ u32 _tmp = (d); div_u64((ll) + _tmp - 1, _tmp); }) + +/** + * DIV64_U64_ROUND_CLOSEST - unsigned 64bit divide with 64bit divisor rounded to nearest integer + * @dividend: unsigned 64bit dividend + * @divisor: unsigned 64bit divisor + * + * Divide unsigned 64bit dividend by unsigned 64bit divisor + * and round to closest integer. + * + * Return: dividend / divisor rounded to nearest integer + */ +#define DIV64_U64_ROUND_CLOSEST(dividend, divisor) \ + ({ u64 _tmp = (divisor); div64_u64((dividend) + _tmp / 2, _tmp); }) + +/** + * DIV_U64_ROUND_CLOSEST - unsigned 64bit divide with 32bit divisor rounded to nearest integer + * @dividend: unsigned 64bit dividend + * @divisor: unsigned 32bit divisor + * + * Divide unsigned 64bit dividend by unsigned 32bit divisor + * and round to closest integer. + * + * Return: dividend / divisor rounded to nearest integer + */ +#define DIV_U64_ROUND_CLOSEST(dividend, divisor) \ + ({ u32 _tmp = (divisor); div_u64((u64)(dividend) + _tmp / 2, _tmp); }) + +/** + * DIV_S64_ROUND_CLOSEST - signed 64bit divide with 32bit divisor rounded to nearest integer + * @dividend: signed 64bit dividend + * @divisor: signed 32bit divisor + * + * Divide signed 64bit dividend by signed 32bit divisor + * and round to closest integer. + * + * Return: dividend / divisor rounded to nearest integer + */ +#define DIV_S64_ROUND_CLOSEST(dividend, divisor)( \ +{ \ + s64 __x = (dividend); \ + s32 __d = (divisor); \ + ((__x > 0) == (__d > 0)) ? \ + div_s64((__x + (__d / 2)), __d) : \ + div_s64((__x - (__d / 2)), __d); \ +} \ +) + +/** + * roundup_u64 - Round up a 64bit value to the next specified 32bit multiple + * @x: the value to up + * @y: 32bit multiple to round up to + * + * Rounds @x to the next multiple of @y. For 32bit @x values, see roundup and + * the faster round_up() for powers of 2. + * + * Return: rounded up value. + */ +static inline u64 roundup_u64(u64 x, u32 y) +{ + return DIV_U64_ROUND_UP(x, y) * y; +} + +/* + * U-Boot addition, not present in Linux's : Linux keeps + * abs_diff() in , which U-Boot does not have. Its other + * contents live in here, but abs_diff() was added to this + * header instead, so leave it be rather than move it around. + */ + /** * abs_diff - return absolute value of the difference between the arguments * @a: the first argument From ee3ee32c9facbb26460d5386479c72e9868680f5 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 13 Jul 2026 14:27:42 +0400 Subject: [PATCH 54/78] clk: rockchip: pll: drop misleading fout in rockchip_rk3588_pll_k_get() The calculation in rockchip_rk3588_pll_k_get() uses an intermediate variable misleadingly named fout, which is not the output frequency. Drop it and inline its calculation into the k assignment, which also improves clarity. While at that, also drop the redundant reassignment of fref which doesn't change within this function. Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk_pll.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/drivers/clk/rockchip/clk_pll.c b/drivers/clk/rockchip/clk_pll.c index 9dec40b1fe8..d0df3b8fb49 100644 --- a/drivers/clk/rockchip/clk_pll.c +++ b/drivers/clk/rockchip/clk_pll.c @@ -169,18 +169,15 @@ rockchip_pll_clk_set_by_auto(ulong fin_hz, static u32 rockchip_rk3588_pll_k_get(u32 m, u32 p, u32 s, u64 fin_hz, u64 fvco) { - u64 fref, fout, ffrac; + u64 fref, ffrac; u32 k = 0; fref = fin_hz / p; ffrac = fvco - (m * fref); - fout = ffrac * 65536; - k = fout / fref; + k = ffrac * 65536 / fref; if (k > 32767) { - fref = fin_hz / p; ffrac = ((m + 1) * fref) - fvco; - fout = ffrac * 65536; - k = ((fout * 10 / fref) + 7) / 10; + k = ((ffrac * 65536 * 10 / fref) + 7) / 10; if (k > 32767) k = 0; else From 4af14351607c592f9c8e8a79f569a536d2236166 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 13 Jul 2026 14:33:05 +0400 Subject: [PATCH 55/78] clk: rockchip: pll: fix rounding of negative k in RK3588 frac PLL Current code uses magical constants when rounding up the magnitude of negative fractional PLL component k. This leads to overshooting the requested rate when the calculated fractional part has less than 0.3 in its decimal part due to failure to round up the fractional part. Use a proper rounding up function to avoid overshooting the requested rate and make the calculation more readable. Fixes: 6bfb37e70209 ("clk: rockchip: rk3588: fix up the frac pll calculation") Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk_pll.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/clk/rockchip/clk_pll.c b/drivers/clk/rockchip/clk_pll.c index d0df3b8fb49..69d2d182dcb 100644 --- a/drivers/clk/rockchip/clk_pll.c +++ b/drivers/clk/rockchip/clk_pll.c @@ -11,6 +11,7 @@ #include #include #include +#include static struct rockchip_pll_rate_table rockchip_auto_table; @@ -177,7 +178,10 @@ rockchip_rk3588_pll_k_get(u32 m, u32 p, u32 s, u64 fin_hz, u64 fvco) k = ffrac * 65536 / fref; if (k > 32767) { ffrac = ((m + 1) * fref) - fvco; - k = ((ffrac * 65536 * 10 / fref) + 7) / 10; + /* + * Round up to avoid overshooting requested rate for negative k + */ + k = DIV64_U64_ROUND_UP(ffrac * 65536, fref); if (k > 32767) k = 0; else From 3f12e374b74cd9acb8dd4251b624895b1fd0ba5a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 13 Jul 2026 14:43:13 +0400 Subject: [PATCH 56/78] clk: rockchip: pll: fix RK3588 frac PLL result for k=-32768 Current code needlessly sets the k value to 0 when it is calculated as -32768, which is a valid value for the RK3588 frac PLL. This results in the PLL output frequency being higher than requested when the requested frequency is exactly halfway between two integer-multiplier PLL output frequencies. Negative k values can never go below -32768 either, because that case is handled just above this code, so the check for k > 32767 is redundant. What remains of the if statement is a hand-rolled two's complement negation of the result, so write it out as such for clarity, and return the true S16 type of k as specified in the TRM. Fixes: 6bfb37e70209 ("clk: rockchip: rk3588: fix up the frac pll calculation") Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk_pll.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/drivers/clk/rockchip/clk_pll.c b/drivers/clk/rockchip/clk_pll.c index 69d2d182dcb..c6fbeb71c77 100644 --- a/drivers/clk/rockchip/clk_pll.c +++ b/drivers/clk/rockchip/clk_pll.c @@ -167,11 +167,11 @@ rockchip_pll_clk_set_by_auto(ulong fin_hz, return rate_table; } -static u32 +static s16 rockchip_rk3588_pll_k_get(u32 m, u32 p, u32 s, u64 fin_hz, u64 fvco) { u64 fref, ffrac; - u32 k = 0; + int k; fref = fin_hz / p; ffrac = fvco - (m * fref); @@ -181,11 +181,7 @@ rockchip_rk3588_pll_k_get(u32 m, u32 p, u32 s, u64 fin_hz, u64 fvco) /* * Round up to avoid overshooting requested rate for negative k */ - k = DIV64_U64_ROUND_UP(ffrac * 65536, fref); - if (k > 32767) - k = 0; - else - k = ~k + 1; + k = -(int)DIV64_U64_ROUND_UP(ffrac * 65536, fref); } return k; } From 933e40f5e98b588c355e4d679774a8e134e4875e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 13 Jul 2026 17:47:35 +0400 Subject: [PATCH 57/78] clk: rockchip: pll: let rockchip_rk3588_pll_k_get update m directly Selecting the next integer multiplier m is coupled to setting a negative fractional coefficient k. The current code checks for negative k in two separate places, which is error-prone. Let rockchip_rk3588_pll_k_get update m directly, to make it the single source of truth for the final value of the integer multiplier m, which also reduces the number of scattered conditional branches in the code. Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk_pll.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/drivers/clk/rockchip/clk_pll.c b/drivers/clk/rockchip/clk_pll.c index c6fbeb71c77..6324c11091a 100644 --- a/drivers/clk/rockchip/clk_pll.c +++ b/drivers/clk/rockchip/clk_pll.c @@ -168,16 +168,21 @@ rockchip_pll_clk_set_by_auto(ulong fin_hz, } static s16 -rockchip_rk3588_pll_k_get(u32 m, u32 p, u32 s, u64 fin_hz, u64 fvco) +rockchip_rk3588_pll_k_get(u32 *m, u32 p, u32 s, u64 fin_hz, u64 fvco) { u64 fref, ffrac; int k; fref = fin_hz / p; - ffrac = fvco - (m * fref); + ffrac = fvco - (*m) * fref; k = ffrac * 65536 / fref; if (k > 32767) { - ffrac = ((m + 1) * fref) - fvco; + /* + * The requested rate is closer to the next integer multiplier + * m, so pick it and use a negative fractional coefficient k + */ + *m += 1; + ffrac = (*m) * fref - fvco; /* * Round up to avoid overshooting requested rate for negative k */ @@ -201,7 +206,10 @@ rockchip_rk3588_pll_frac_by_auto(unsigned long fin_hz, unsigned long fout_hz) for (m = 64; m <= 1023; m++) { if ((fvco >= m * fin_hz / p) && (fvco < (m + 1) * fin_hz / p)) { - k = rockchip_rk3588_pll_k_get(m, p, s, + u32 m_tmp = m; + + k = rockchip_rk3588_pll_k_get(&m_tmp, + p, s, fin_hz, fvco); if (!k) @@ -209,10 +217,7 @@ rockchip_rk3588_pll_frac_by_auto(unsigned long fin_hz, unsigned long fout_hz) rate_table->p = p; rate_table->s = s; rate_table->k = k; - if (k > 32767) - rate_table->m = m + 1; - else - rate_table->m = m; + rate_table->m = m_tmp; return rate_table; } } From accc77e43a0476782556dbe6a621224da9988e16 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 13 Jul 2026 18:14:55 +0400 Subject: [PATCH 58/78] clk: rockchip: pll: fractional PLL coefficient is two's complement The TRM defines the fractional PLL adjustment coefficient as a signed two's complement number, 16 bits wide, so store it as such to avoid confusion. Signed-off-by: Alexey Charkov --- arch/arm/include/asm/arch-rockchip/clock.h | 2 +- drivers/clk/rockchip/clk_pll.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/arch/arm/include/asm/arch-rockchip/clock.h b/arch/arm/include/asm/arch-rockchip/clock.h index 95b08bfd046..f9bfdfb8a6a 100644 --- a/arch/arm/include/asm/arch-rockchip/clock.h +++ b/arch/arm/include/asm/arch-rockchip/clock.h @@ -104,7 +104,7 @@ struct rockchip_pll_rate_table { unsigned int m; unsigned int p; unsigned int s; - unsigned int k; + int k; }; enum rockchip_pll_type { diff --git a/drivers/clk/rockchip/clk_pll.c b/drivers/clk/rockchip/clk_pll.c index 6324c11091a..ebe90b55ea0 100644 --- a/drivers/clk/rockchip/clk_pll.c +++ b/drivers/clk/rockchip/clk_pll.c @@ -195,8 +195,9 @@ static struct rockchip_pll_rate_table * rockchip_rk3588_pll_frac_by_auto(unsigned long fin_hz, unsigned long fout_hz) { struct rockchip_pll_rate_table *rate_table = &rockchip_auto_table; - u32 p, m, s, k; + u32 p, m, s; u64 fvco; + s16 k; for (s = 0; s <= 6; s++) { fvco = (u64)fout_hz << s; From d09b6079f1764627b4987912461809f46881a8b5 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 13 Jul 2026 18:17:33 +0400 Subject: [PATCH 59/78] clk: rockchip: pll: fix overflow and drop manual two's complement in rk3588_pll_get_rate Current code calculates the fractional component in 32 bits before assigning it to a 64-bit holding variable, causing overflow for real-world values of k, given that OSC_HZ is 24000000U. It also does bitwise manual massaging of an unsigned representation of what is actually a two's complement signed value, which is confusing and makes the code harder to read. Read k into a properly signed type and promote operands to avoid overflow, which also enables the use of div_s64() to express the math more clearly. Fixes: b851c006a150 ("clk: rockchip: pll: Add pll_rk3588 type for rk3588") Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk_pll.c | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/drivers/clk/rockchip/clk_pll.c b/drivers/clk/rockchip/clk_pll.c index ebe90b55ea0..9d529242be3 100644 --- a/drivers/clk/rockchip/clk_pll.c +++ b/drivers/clk/rockchip/clk_pll.c @@ -559,9 +559,10 @@ static int rk3588_pll_set_rate(struct rockchip_pll_clock *pll, static ulong rk3588_pll_get_rate(struct rockchip_pll_clock *pll, void __iomem *base, ulong pll_id) { - u32 m, p, s, k; + u32 m, p, s; u32 con = 0, shift, mode; - u64 rate, postdiv; + u64 rate; + s16 k; con = readl(base + pll->mode_offset); shift = pll->mode_shift; @@ -588,25 +589,7 @@ static ulong rk3588_pll_get_rate(struct rockchip_pll_clock *pll, rate = OSC_HZ / p; rate *= m; - if (k & BIT(15)) { - /* fractional mode */ - u64 frac_rate64; - - k = (~(k - 1)) & RK3588_PLLCON2_K_MASK; - frac_rate64 = OSC_HZ * k; - postdiv = p; - postdiv *= 65536; - do_div(frac_rate64, postdiv); - rate -= frac_rate64; - } else { - /* fractional mode */ - u64 frac_rate64 = OSC_HZ * k; - - postdiv = p; - postdiv *= 65536; - do_div(frac_rate64, postdiv); - rate += frac_rate64; - } + rate += div_s64((s64)OSC_HZ * k, p * 65536); rate = rate >> s; return rate; case RKCLK_PLL_MODE_DEEP: From 49ac4721df5af1c51201e49b4b4bc9fac6c93a88 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 27 Jul 2026 14:23:56 +0400 Subject: [PATCH 60/78] rockchip: rk3576: boost: enable I-cache in EL3 The USB boot path in the RK3576 boot ROM is very slow to jump to the 2nd stage uploaded via the Maskrom 0x472 command. This is due to a very slow unoptimized CRC16 routine checking the 0x472 payload byte-by-byte and fetching ~100 instructions per byte from uncached ROM. Enable the I-cache in EL3 before jumping to the 2nd stage, which provides a ~16x speedup (~2 MB/s) for the 0x472 path. This is most relevant for booting meaningful (megabytes-sized) payloads via USB, as the slow CRC routine is never called from other boot paths. The change is harmless for normal boot from persistent storage though, so can be enabled unconditionally. Signed-off-by: Alexey Charkov --- arch/arm/mach-rockchip/rk3576/rk3576-boost.c | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/arch/arm/mach-rockchip/rk3576/rk3576-boost.c b/arch/arm/mach-rockchip/rk3576/rk3576-boost.c index 2ffcaaacf5c..10ba4ce72c9 100644 --- a/arch/arm/mach-rockchip/rk3576/rk3576-boost.c +++ b/arch/arm/mach-rockchip/rk3576/rk3576-boost.c @@ -2,8 +2,7 @@ // Copyright Contributors to the U-Boot project. /* - * Early initialization code to be executed before RAM training on RK3576. - * At the moment this only contains a BootROM fixup for booting from an SD card + * Early initialization code to be executed before RAM training on RK3576 * * Anything in this file is to be compiled into a freestanding raw executable * binary and prepended to the RKNS image by mkimage during build @@ -16,10 +15,27 @@ int _start(void) { - uint32_t *sram = (void*)(SYS_SRAM_BASE + OFFSET); + uint32_t *sram = (void *)(SYS_SRAM_BASE + OFFSET); + uint64_t sctlr; /* set unknown value in sram to fix boot from sdmmc */ *(sram) = 0x3ffff800; + /* + * Enable the EL3 instruction cache before returning to the BootROM. + * + * The ROM runs with MMU and caches off. After a Maskrom USB download + * to DRAM (command 0x472) it verifies the *entire* image with a + * bit-serial CRC-16 executed from slow, non-cacheable on-chip ROM. + * With the I-cache off, every byte costs ~101 uncached ROM instruction + * fetches, so image verification can take noticeable time (~124 KB/s). + * Turning on the EL3 I-cache lets the tiny CRC loop run from L1I, + * measured ~2 MB/s (~16x speedup). The bit persists into the ROM's + * 0x472/CRC path. Harmless for the SD/storage path (small 2nd stage). + */ + asm volatile("mrs %0, sctlr_el3" : "=r"(sctlr)); + sctlr |= (1UL << 12); + asm volatile("msr sctlr_el3, %0; isb" :: "r"(sctlr) : "memory"); + return 0; } From 6e401ab28be478b68b39e7ea500ed6a2a5e3c8a3 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 29 Jul 2026 22:01:31 +0400 Subject: [PATCH 61/78] spl: Fix link when LZMA is enabled without legacy image format support spl_load_legacy_lzma() lives in common/spl/spl_legacy.c, which is only built when CONFIG_$(PHASE_)LEGACY_IMAGE_FORMAT is set, but the call site in _spl_load() is guarded by CONFIG_SPL_LZMA alone. Enabling LZMA decompression without the legacy image format therefore fails to link: ld: common/spl/spl.o: in function `_spl_load': include/spl_load.h:73: undefined reference to `spl_load_legacy_lzma' CONFIG_SPL_LZMA is also needed to support LZMA-compressed payloads with a FIT-only SPL, so add the missing condition to avoid including the legacy image related function call when legacy image support is not built. Signed-off-by: Alexey Charkov --- include/spl_load.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/spl_load.h b/include/spl_load.h index 525e0c9e86c..31ed5651f7b 100644 --- a/include/spl_load.h +++ b/include/spl_load.h @@ -63,6 +63,7 @@ static inline int _spl_load(struct spl_image_info *spl_image, return spl_load_imx_container(spl_image, info, offset); if (IS_ENABLED(CONFIG_SPL_LZMA) && + CONFIG_IS_ENABLED(LEGACY_IMAGE_FORMAT) && image_get_magic(header) == IH_MAGIC && image_get_comp(header) == IH_COMP_LZMA) { spl_image->flags |= SPL_COPY_PAYLOAD_ONLY; From fb3a4af6114a2f744359802762c77c77ded0b6bf Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 30 Jul 2026 16:30:33 +0400 Subject: [PATCH 62/78] spl: fit: Fill in the image descriptor when skipping a zero-size image load_simple_fit() is expected to fill in the image_info structure it receives upon successful return, but the path which skips a zero-sized image returns success without touching it. The result is that spl_fit_record_loadable() then publishes whatever else the descriptor happened to hold in /fit-images under the skipped image's name: the size and entry point of the previous loadable, or - for the first one, since image_info is declared without an initialiser - uninitialised stack. This is reachable whenever a FIT carries an image node with no content, which binman produces for an optional blob that was not supplied, such as an OP-TEE which the build did not provide. Ensure that the image_info structure is filled in with a size and entry point before returning, same way as other successful paths do (but skipping the actual load). Fixes: 6d99f866952b ("spl: fit: Skip attempting to load 0 length image") Signed-off-by: Alexey Charkov --- common/spl/spl_fit.c | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/common/spl/spl_fit.c b/common/spl/spl_fit.c index 18bff7b8d4a..0dbc0ba8153 100644 --- a/common/spl/spl_fit.c +++ b/common/spl/spl_fit.c @@ -193,6 +193,33 @@ static int get_aligned_image_size(struct spl_load_info *info, int data_size, return ALIGN(data_size, spl_get_bl_len(info)); } +/** + * fit_fill_image_info(): describe a loaded image to the caller + * @fit: points to the FIT image + * @node: offset of the DT node describing the image + * @image_info: filled in with where the image ended up and how big it is; + * ignored if NULL + * @load_addr: address the image was loaded to + * @size: number of bytes loaded, which may be zero + */ +static void fit_fill_image_info(const void *fit, int node, + struct spl_image_info *image_info, + ulong load_addr, ulong size) +{ + ulong entry_point; + + if (!image_info) + return; + + image_info->load_addr = load_addr; + image_info->size = size; + + if (!fit_image_get_entry(fit, node, &entry_point)) + image_info->entry_point = entry_point; + else + image_info->entry_point = FDT_ERROR; +} + /** * load_simple_fit(): load the image described in a certain FIT node * @info: points to information about the device to load data from @@ -291,6 +318,7 @@ static int load_simple_fit(struct spl_load_info *info, ulong fit_offset, if (!len) { log_warning("%s: Skip load '%s': image size is 0!\n", __func__, fit_get_name(fit, node, NULL)); + fit_fill_image_info(fit, node, image_info, load_addr, 0); return 0; } @@ -385,17 +413,7 @@ static int load_simple_fit(struct spl_load_info *info, ulong fit_offset, memmove(load_ptr, src, length); } - if (image_info) { - ulong entry_point; - - image_info->load_addr = load_addr; - image_info->size = length; - - if (!fit_image_get_entry(fit, node, &entry_point)) - image_info->entry_point = entry_point; - else - image_info->entry_point = FDT_ERROR; - } + fit_fill_image_info(fit, node, image_info, load_addr, length); log_debug("- done loading\n"); upl_add_image(fit, node, load_addr, length); From 118d3981055c9a92a0412c2815736aeab09a591e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 30 Jul 2026 15:03:26 +0400 Subject: [PATCH 63/78] binman: Do not dedent docstrings twice on Python 3.13+ WriteDocs() and write_bintool_docs() strip four characters from the start of every docstring line but the first, to undo the indentation the source file gives them. Since Python 3.13 the compiler already removes the common indentation from docstrings [1], so this removes four characters of actual text from every line of every entry and bintool description: $ binman entry-docs | head ... that an image node whose only content is an optional entry which was is an example showing ATF, TEE and a device tree all combined:: Use inspect.cleandoc() instead, which produces the same result on both older and newer interpreters. The existing tests only checked that some output was produced, so they missed this entirely; make them also confirm that a known line of a known description survives intact. Link: https://github.com/python/cpython/issues/81283 [1] Signed-off-by: Alexey Charkov --- tools/binman/bintool.py | 6 ++++-- tools/binman/entry.py | 9 +++++++-- tools/binman/ftest.py | 19 +++++++++++++++++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/tools/binman/bintool.py b/tools/binman/bintool.py index 9c76c8881a4..4cf8d5b165b 100644 --- a/tools/binman/bintool.py +++ b/tools/binman/bintool.py @@ -12,6 +12,7 @@ import collections import glob import importlib +import inspect import multiprocessing import os import shutil @@ -463,9 +464,10 @@ def WriteDocs(modules, test_missing=None): if test_missing == name: docs = None if docs: - lines = docs.splitlines() + # See the note in Entry.WriteDocs() about cleandoc() + lines = inspect.cleandoc(docs).splitlines() first_line = lines[0] - rest = [line[4:] for line in lines[1:]] + rest = lines[1:] hdr = 'Bintool: %s: %s' % (name, first_line) print(hdr) print('-' * len(hdr)) diff --git a/tools/binman/entry.py b/tools/binman/entry.py index ce7ef28e94b..9b39c711889 100644 --- a/tools/binman/entry.py +++ b/tools/binman/entry.py @@ -6,6 +6,7 @@ from collections import namedtuple import importlib +import inspect import os import pathlib import sys @@ -858,9 +859,13 @@ def WriteDocs(modules, test_missing=None): if test_missing == name: docs = None if docs: - lines = docs.splitlines() + # Use cleandoc() rather than removing a fixed four characters + # of indent: since Python 3.13 the compiler already strips the + # common indent from docstrings, so doing it again here would + # eat the first four characters of every line + lines = inspect.cleandoc(docs).splitlines() first_line = lines[0] - rest = [line[4:] for line in lines[1:]] + rest = lines[1:] hdr = 'Entry: %s: %s' % (name.replace('_', '-'), first_line) # Create a reference for use by rST docs diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py index 9553eb6b736..c5c67c7e68b 100644 --- a/tools/binman/ftest.py +++ b/tools/binman/ftest.py @@ -1805,7 +1805,15 @@ def testEntryDocs(self): """Test for creation of entry documentation""" with terminal.capture() as (stdout, stderr): control.WriteEntryDocs(control.GetEntryModules()) - self.assertTrue(len(stdout.getvalue()) > 0) + out = stdout.getvalue() + self.assertTrue(len(out) > 0) + + # The body of each docstring must come out dedented but otherwise + # intact. Check a heading which several etypes use, since truncating + # the indent by too much would silently eat the start of every line. + self.assertIn('\nProperties / Entry arguments:\n', out) + self.assertIn('\nEntry: atf-bl31: ARM Trusted Firmware (ATF) BL31 blob\n', + out) def testEntryDocsMissing(self): """Test handling of missing entry documentation""" @@ -5491,7 +5499,14 @@ def testBintoolDocs(self): """Test for creation of bintool documentation""" with terminal.capture() as (stdout, stderr): control.write_bintool_docs(control.bintool.Bintool.get_tool_list()) - self.assertTrue(len(stdout.getvalue()) > 0) + out = stdout.getvalue() + self.assertTrue(len(out) > 0) + + # As in testEntryDocs(), check that the body is dedented but intact + self.assertIn('\nBintool: mkimage: Image generation for U-Boot\n', out) + self.assertIn( + '\nThis bintool supports running `mkimage` with some basic parameters as\n', + out) def testBintoolDocsMissing(self): """Test handling of missing bintool documentation""" From 0d7ae63fab49ca792228acc0d8a5e0330f0d17d6 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 28 Jul 2026 19:51:10 +0400 Subject: [PATCH 64/78] spl: Decide about Falcon mode boot only once Board implementations of spl_start_uboot() are not required to be idempotent - the documented examples sample a GPIO or read a character from the SPL console - so calling it more than once can yield different answers. Add spl_falcon_boot(), which calls spl_start_uboot() at most once and caches the result. Signed-off-by: Alexey Charkov --- common/spl/spl.c | 18 ++++++++++++++++++ include/spl.h | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/common/spl/spl.c b/common/spl/spl.c index 722b18c98ed..abbd56ca2fa 100644 --- a/common/spl/spl.c +++ b/common/spl/spl.c @@ -105,6 +105,24 @@ __weak int spl_start_uboot(void) return 1; } +/* + * Decide about Falcon mode boot exactly once per SPL run. Board + * implementations of spl_start_uboot() are not required to be idempotent - + * they may sample a button or read a character from the console - so asking + * twice can yield two different answers. That matters because both the loader + * and the code which finally enters the OS (e.g. spl_invoke_atf()) need the + * decision, and they must not disagree about it. + */ +bool spl_falcon_boot(void) +{ + static int falcon = -1; + + if (falcon < 0) + falcon = !spl_start_uboot(); + + return falcon; +} + /* * Weak default function for arch specific zImage check. Return zero * and fill start and end address if image is recognized. diff --git a/include/spl.h b/include/spl.h index 5078d7525ab..8da82e87aa3 100644 --- a/include/spl.h +++ b/include/spl.h @@ -757,6 +757,17 @@ void __noreturn jump_to_image_optee(struct spl_image_info *spl_image); */ int spl_start_uboot(void); +/** + * spl_falcon_boot() - Check whether Falcon mode boot was requested + * + * Wrapper around spl_start_uboot() which calls it at most once and caches the + * answer, so that everything taking part in a single SPL run agrees on it. + * Prefer this over calling spl_start_uboot() directly. + * + * Return: true if SPL should start the OS, false if U-Boot must be started + */ +bool spl_falcon_boot(void); + /** * spl_display_print() - Display a board-specific message in SPL * From 231de4cab595eac7dbaa873bdd551782162be15a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 8 Jun 2026 19:51:46 +0400 Subject: [PATCH 65/78] spl: atf: support Linux as BL33 with TFA Modern SoCs such as Rockchip RK3576 need TFA to be running to provide firmware services to the OS. Enable the TFA boot flow to allow using Linux as BL33 (including its calling convention) to facilitate Falcon mode boot on such SoCs. Signed-off-by: Alexey Charkov --- common/spl/Kconfig | 22 ++++++++++++++++++-- common/spl/spl_atf.c | 49 +++++++++++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/common/spl/Kconfig b/common/spl/Kconfig index 0618f42c941..7c639ed8f76 100644 --- a/common/spl/Kconfig +++ b/common/spl/Kconfig @@ -1230,22 +1230,40 @@ config SPL_BOOTZ help Boot a linux zimage from memory in falcon boot. + This provides an entry path which is unused when the OS is entered + through TF-A, since SPL then only places the kernel in memory and + BL31 is what enters it. It remains selectable: SPL can still enter + a kernel itself on such a platform, dropping to EL2 without TF-A + resident, just without any firmware services being available to the + OS afterwards. + config SPL_BOOTI bool "Allow booting an Image style Linux kernel from SPL" depends on SPL_OS_BOOT && !SPL_OS_BOOT_SECURE - default y if ARM64 || RISCV + default y if (ARM64 || RISCV) && !SPL_ATF select SPL_LIB_BOOTI help Boot an uncompressed linux kernel image from memory in falcon boot. + This provides an entry path which is unused when the OS is entered + through TF-A, since SPL then only places the kernel in memory and + BL31 is what enters it, so it does not default to y in that case. It + remains selectable: SPL can still enter a kernel itself on such a + platform, dropping to EL2 without TF-A resident, just without any + firmware services being available to the OS afterwards. + config SPL_OS_BOOT_ARGS bool "Allow SPL to load args for kernel in falcon mode" depends on (SPL_OS_BOOT || SPL_LOAD_FIT_OPENSBI_OS_BOOT) && !SPL_OS_BOOT_SECURE - default y if !SPL_OS_BOOT_SECURE + default y if !SPL_OS_BOOT_SECURE && !SPL_ATF help This option enables the SPL to load an args file (usually the FDT) alongside the kernel image in falcon boot mode. + This is not needed when the OS is entered through TF-A, since the + device tree is then taken from the same FIT as the kernel, which is + why it does not default to y in that case. + config SPL_PAYLOAD_ARGS_ADDR hex "Address in memory to load 'args' file for Falcon Mode to" depends on SPL_OS_BOOT_ARGS diff --git a/common/spl/spl_atf.c b/common/spl/spl_atf.c index 17acd3665df..0cec5d8d46f 100644 --- a/common/spl/spl_atf.c +++ b/common/spl/spl_atf.c @@ -15,6 +15,7 @@ #include #include #include +#include #include /* Holds all the structures we need for bl31 parameter passing */ @@ -87,8 +88,8 @@ struct bl31_params *bl2_plat_get_bl31_params_default(ulong bl32_entry, SET_PARAM_HEAD(bl33_ep_info, ATF_PARAM_EP, ATF_VERSION_1, ATF_EP_NON_SECURE); - /* BL33 expects to receive the primary CPU MPID (through x0) */ - bl33_ep_info->args.arg0 = 0xffff & read_mpidr(); + /* Pass the FDT address in x0, per the TF-A BL33 / Linux boot protocol. */ + bl33_ep_info->args.arg0 = fdt_addr; bl33_ep_info->pc = bl33_entry; bl33_ep_info->spsr = SPSR_64(MODE_EL2, MODE_SP_ELX, DISABLE_ALL_EXECPTIONS); @@ -162,8 +163,8 @@ struct bl_params *bl2_plat_get_bl31_params_v2_default(ulong bl32_entry, SET_PARAM_HEAD(bl_params_node->ep_info, ATF_PARAM_EP, ATF_VERSION_2, ATF_EP_NON_SECURE); - /* BL33 expects to receive the primary CPU MPID (through x0) */ - bl_params_node->ep_info->args.arg0 = 0xffff & read_mpidr(); + /* Pass the FDT address in x0, per the TF-A BL33 / Linux boot protocol. */ + bl_params_node->ep_info->args.arg0 = fdt_addr; bl_params_node->ep_info->pc = bl33_entry; bl_params_node->ep_info->spsr = SPSR_64(MODE_EL2, MODE_SP_ELX, DISABLE_ALL_EXECPTIONS); @@ -189,7 +190,8 @@ static inline void raw_write_daif(unsigned int daif) typedef void __noreturn (*atf_entry_t)(struct bl31_params *params, void *plat_params); static void __noreturn bl31_entry(ulong bl31_entry, ulong bl32_entry, - ulong bl33_entry, ulong fdt_addr) + ulong bl33_entry, ulong fdt_addr, + ulong plat_param) { atf_entry_t atf_entry = (atf_entry_t)bl31_entry; void *bl31_params; @@ -206,7 +208,7 @@ static void __noreturn bl31_entry(ulong bl31_entry, ulong bl32_entry, if (!CONFIG_IS_ENABLED(SYS_DCACHE_OFF)) dcache_disable(); - atf_entry(bl31_params, (void *)fdt_addr); + atf_entry(bl31_params, (void *)plat_param); } static int spl_fit_images_find(void *blob, int os) @@ -267,10 +269,11 @@ ulong spl_fit_images_get_entry(void *blob, int node) void __noreturn spl_invoke_atf(struct spl_image_info *spl_image) { - ulong bl32_entry = 0; - ulong bl33_entry = CONFIG_TEXT_BASE; - void *blob = spl_image->fdt_addr; + bool falcon = CONFIG_IS_ENABLED(OS_BOOT) && spl_falcon_boot(); + void *blob = spl_image_fdt_addr(spl_image); + ulong bl33_entry = CONFIG_TEXT_BASE; ulong platform_param = (ulong)blob; + ulong bl32_entry = 0; int node; /* @@ -283,15 +286,29 @@ void __noreturn spl_invoke_atf(struct spl_image_info *spl_image) bl32_entry = spl_fit_images_get_entry(blob, node); /* - * Find the U-Boot binary (in /fit-images) load addreess or - * entry point (if different) and pass it as the BL3-3 entry - * point. - * This will need to be extended to support Falcon mode. + * Find BL33 entry point. In Falcon mode, prefer Linux when requested. + * Fall back to U-Boot if Linux cannot be resolved, unless secure Falcon + * mode is in effect, in which case an unsigned fallback is not allowed. */ + node = -FDT_ERR_NOTFOUND; + if (falcon) + node = spl_fit_images_find(blob, IH_OS_LINUX); - node = spl_fit_images_find(blob, IH_OS_U_BOOT); - if (node >= 0) + if (node < 0) { + if (falcon && CONFIG_IS_ENABLED(OS_BOOT_SECURE)) + panic("SPL: TF-A: no Linux BL33 image for secure Falcon boot"); + node = spl_fit_images_find(blob, IH_OS_U_BOOT); + } + + if (node >= 0) { bl33_entry = spl_fit_images_get_entry(blob, node); + } else if (falcon) { + /* + * Falcon mode was requested, so U-Boot proper was never loaded + * and CONFIG_TEXT_BASE holds nothing we could enter. + */ + panic("SPL: TF-A: no BL33 image to boot"); + } /* * If ATF_NO_PLATFORM_PARAM is set, we override the platform @@ -307,5 +324,5 @@ void __noreturn spl_invoke_atf(struct spl_image_info *spl_image) * using similar logic. */ bl31_entry(spl_image->entry_point, bl32_entry, - bl33_entry, platform_param); + bl33_entry, (ulong)blob, platform_param); } From 65274936586688268b8bc99b4f0ac319fa331202 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 8 Jun 2026 19:51:59 +0400 Subject: [PATCH 66/78] spl: ufs: add Falcon mode load path In Falcon mode, the SPL loads a FIT image containing a Linux kernel instead of U-boot proper, and it may need to fall back to loading U-boot if Linux is unavailable. Add support for images containing a Linux kernel, optionally on a different UFS LUN and/or offset vs. U-boot proper to enable fallback. Signed-off-by: Alexey Charkov --- common/spl/Kconfig | 23 ++++++++++++++++++ common/spl/spl_ufs.c | 58 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/common/spl/Kconfig b/common/spl/Kconfig index 7c639ed8f76..0e0c60eb979 100644 --- a/common/spl/Kconfig +++ b/common/spl/Kconfig @@ -1653,6 +1653,16 @@ config SPL_UFS_RAW_U_BOOT_DEVNUM SCSI device number, which might differ from the UFS LUN if you have multiple SCSI devices attached and recognized by the SPL. +config SPL_UFS_RAW_OS_DEVNUM + int "Falcon mode: SCSI device number of the UFS device to load OS from" + depends on SPL_UFS_SUPPORT && SPL_OS_BOOT + default SPL_UFS_RAW_U_BOOT_DEVNUM + help + UFS devices are usually configured with multiple LUNs, which present + themselves as sequentially numbered SCSI devices. This option controls + which of them SPL will attempt to load a Falcon OS payload from. + It allows putting Linux FITs on a different UFS LU than U-Boot. + config SPL_UFS_RAW_U_BOOT_SECTOR hex "Address on the UFS to load U-Boot from" depends on SPL_UFS_SUPPORT @@ -1661,6 +1671,19 @@ config SPL_UFS_RAW_U_BOOT_SECTOR Address on the block device to load U-Boot from. Units: UFS sectors (1 sector = 4096 bytes). +config SPL_UFS_RAW_OS_SECTOR + hex "Falcon mode: address on the UFS to load OS image from" + depends on SPL_UFS_SUPPORT && SPL_OS_BOOT + default SPL_UFS_RAW_U_BOOT_SECTOR + help + Address on the block device to load a Falcon OS FIT/image from. + Units: UFS sectors (1 sector = 4096 bytes). + + The default shares the region with U-Boot, which suits Falcon-only + boot: the OS image is written where U-Boot would have gone, so only + one region has to be reserved and there is no fallback. Point this + at a separate location to keep a working U-Boot to fall back to. + config SPL_WATCHDOG bool "Support watchdog drivers" imply SPL_WDT if !HW_WATCHDOG diff --git a/common/spl/spl_ufs.c b/common/spl/spl_ufs.c index cef1843f40f..d35b5e7ad33 100644 --- a/common/spl/spl_ufs.c +++ b/common/spl/spl_ufs.c @@ -20,13 +20,38 @@ static ulong spl_ufs_load_read(struct spl_load_info *load, ulong off, ulong size return blk_dread(bd, sector, count, buf) << bd->log2blksz; } +static int spl_ufs_load_image_raw_os(struct spl_image_info *spl_image, + struct spl_boot_device *bootdev, + struct spl_load_info *load, + struct blk_desc *bd) +{ + ulong sector = config_opt_enabled(CONFIG_SPL_OS_BOOT, + CONFIG_SPL_UFS_RAW_OS_SECTOR, + CONFIG_SPL_UFS_RAW_U_BOOT_SECTOR); + int err; + + err = spl_load(spl_image, bootdev, load, 0, + sector << bd->log2blksz); + if (err) + return err; + + if (spl_image->os != IH_OS_LINUX && spl_image->os != IH_OS_TEE && + spl_image->os != IH_OS_ARM_TRUSTED_FIRMWARE) { + puts("Expected OS image is not found\n"); + return -ENOENT; + } + + return 0; +} + static int spl_ufs_load_image(struct spl_image_info *spl_image, struct spl_boot_device *bootdev) { unsigned long sector = CONFIG_SPL_UFS_RAW_U_BOOT_SECTOR; + struct spl_load_info load, os_load; + struct blk_desc *bd, *os_bd; int devnum = CONFIG_SPL_UFS_RAW_U_BOOT_DEVNUM; - struct spl_load_info load; - struct blk_desc *bd; + int os_devnum; int err; /* try to recognize storage devices immediately */ @@ -36,6 +61,35 @@ static int spl_ufs_load_image(struct spl_image_info *spl_image, return -ENODEV; spl_load_init(&load, spl_ufs_load_read, bd, bd->blksz); + if (CONFIG_IS_ENABLED(OS_BOOT) && spl_falcon_boot()) { + os_devnum = config_opt_enabled(CONFIG_SPL_OS_BOOT, + CONFIG_SPL_UFS_RAW_OS_DEVNUM, + CONFIG_SPL_UFS_RAW_U_BOOT_DEVNUM); + if (os_devnum == devnum) { + os_bd = bd; + os_load = load; + } else { + os_bd = blk_get_devnum_by_uclass_id(UCLASS_SCSI, os_devnum); + if (!os_bd) { + puts("spl_ufs_load_image: UFS OS device not found\n"); + if (CONFIG_IS_ENABLED(OS_BOOT_SECURE)) + return -ENODEV; + goto load_uboot; + } + spl_load_init(&os_load, spl_ufs_load_read, os_bd, os_bd->blksz); + } + + err = spl_ufs_load_image_raw_os(spl_image, bootdev, &os_load, os_bd); + if (!err) + return 0; + + puts("spl_ufs_load_image: Failed to load falcon payload\n"); + log_debug("(error=%d)\n", err); + if (CONFIG_IS_ENABLED(OS_BOOT_SECURE)) + return err; + } + +load_uboot: err = spl_load(spl_image, bootdev, &load, 0, sector << bd->log2blksz); if (err) { puts("spl_ufs_load_image: ufs block read error\n"); From 8e7a4f856627b9239debda94c4129e5bdf25da4f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 28 Jul 2026 16:50:32 +0400 Subject: [PATCH 67/78] binman: Add support for externally provided Linux kernel blob Add an simple named Linux kernel blob type to binman, activated by the "LINUX_KERNEL" make variable. No processing is done on the passed blob. Signed-off-by: Alexey Charkov --- Makefile | 1 + tools/binman/etype/linux_kernel.py | 22 ++++++++++++++++++++++ tools/binman/missing-blob-help | 5 +++++ 3 files changed, 28 insertions(+) create mode 100644 tools/binman/etype/linux_kernel.py diff --git a/Makefile b/Makefile index c5cc8276fa0..246efa2c23b 100644 --- a/Makefile +++ b/Makefile @@ -1692,6 +1692,7 @@ cmd_binman = $(srctree)/tools/binman/binman $(if $(BINMAN_DEBUG),-D) \ $(if $(KEYDIR),-a keydir=$(KEYDIR)) \ -a atf-bl1-path=${BL1} \ -a atf-bl31-path=${BL31} \ + -a linux-kernel-path=${LINUX_KERNEL} \ -a tee-os-path=${TEE} \ -a ti-dm-path=${TI_DM} \ -a opensbi-path=${OPENSBI} \ diff --git a/tools/binman/etype/linux_kernel.py b/tools/binman/etype/linux_kernel.py new file mode 100644 index 00000000000..74bef0ed824 --- /dev/null +++ b/tools/binman/etype/linux_kernel.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (c) 2026 Flipper FZCO +# +# Entry-type module for Linux kernel binary blob + +from binman.etype.blob_named_by_arg import Entry_blob_named_by_arg + + +class Entry_linux_kernel(Entry_blob_named_by_arg): + """Linux kernel image blob + + Properties / Entry arguments: + - linux-kernel-path: Filename of file to read into entry. This is + typically an uncompressed ARM64 Image. + + This entry allows binman FIT templates to consume a kernel provided via + make variable, similar to how BL31 is passed to atf-bl31. + """ + + def __init__(self, section, etype, node): + super().__init__(section, etype, node, 'linux-kernel') + self.external = True diff --git a/tools/binman/missing-blob-help b/tools/binman/missing-blob-help index 27dc620bd35..c8c56e3ad9b 100644 --- a/tools/binman/missing-blob-help +++ b/tools/binman/missing-blob-help @@ -32,6 +32,11 @@ If CONFIG_WDT_K3_RTI_LOAD_FW is enabled, a firmware image is needed for the R5F core(s) to trigger the system reset. One possible source is https://github.com/siemens/k3-rti-wdt. +linux-kernel: +A Linux kernel image is required to build a Falcon mode FIT image, which +lets SPL start the OS directly instead of U-Boot proper. Build with +LINUX_KERNEL=/path/to/Image, or disable Falcon mode image generation. + opensbi: See the documentation for your board. The OpenSBI git repo is at https://github.com/riscv/opensbi.git From 55f91f97ee7a1486cb9b826d96402bcad4719d4b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 23 Jul 2026 16:07:47 +0400 Subject: [PATCH 68/78] binman: Add support for externally provided Linux initrd blob Add a simple named Linux ramdisk blob type to binman, activated by the "LINUX_INITRD" make variable. No processing is done on the blob. Signed-off-by: Alexey Charkov --- Makefile | 1 + tools/binman/etype/linux_initrd.py | 23 +++++++++++++++++++++++ tools/binman/missing-blob-help | 5 +++++ 3 files changed, 29 insertions(+) create mode 100644 tools/binman/etype/linux_initrd.py diff --git a/Makefile b/Makefile index 246efa2c23b..81f49b791e6 100644 --- a/Makefile +++ b/Makefile @@ -1693,6 +1693,7 @@ cmd_binman = $(srctree)/tools/binman/binman $(if $(BINMAN_DEBUG),-D) \ -a atf-bl1-path=${BL1} \ -a atf-bl31-path=${BL31} \ -a linux-kernel-path=${LINUX_KERNEL} \ + -a linux-initrd-path=${LINUX_INITRD} \ -a tee-os-path=${TEE} \ -a ti-dm-path=${TI_DM} \ -a opensbi-path=${OPENSBI} \ diff --git a/tools/binman/etype/linux_initrd.py b/tools/binman/etype/linux_initrd.py new file mode 100644 index 00000000000..9603bc279c2 --- /dev/null +++ b/tools/binman/etype/linux_initrd.py @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (c) 2026 Flipper FZCO +# +# Entry-type module for a Linux initramfs/initrd binary blob + +from binman.etype.blob_named_by_arg import Entry_blob_named_by_arg + + +class Entry_linux_initrd(Entry_blob_named_by_arg): + """Linux initramfs (initrd) image blob + + Properties / Entry arguments: + - linux-initrd-path: Filename of file to read into entry. This is + typically a compressed cpio archive (e.g. initramfs.cpio.gz). + + This entry allows binman FIT templates to consume an initramfs provided via + a make variable, similar to how the kernel is passed to linux-kernel. It is + optional: when no path is supplied the entry is simply dropped. + """ + + def __init__(self, section, etype, node): + super().__init__(section, etype, node, 'linux-initrd', required=False) + self.external = True diff --git a/tools/binman/missing-blob-help b/tools/binman/missing-blob-help index c8c56e3ad9b..81806091939 100644 --- a/tools/binman/missing-blob-help +++ b/tools/binman/missing-blob-help @@ -32,6 +32,11 @@ If CONFIG_WDT_K3_RTI_LOAD_FW is enabled, a firmware image is needed for the R5F core(s) to trigger the system reset. One possible source is https://github.com/siemens/k3-rti-wdt. +linux-initrd: +An initramfs to embed in a Falcon mode FIT image, so that the OS can be +started from SPL without a root filesystem being available yet. This is +optional; build with LINUX_INITRD=/path/to/initramfs.cpio.gz to include one. + linux-kernel: A Linux kernel image is required to build a Falcon mode FIT image, which lets SPL start the OS directly instead of U-Boot proper. Build with From a9fdc12dffccf5ce0037e754fe0c4b164eedba02 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 23 Jul 2026 16:09:17 +0400 Subject: [PATCH 69/78] binman: Add support for pre-patching FDTs in a FIT with a /chosen node When a generated FIT is used to boot Linux directly, bypassing U-Boot proper (Falcon mode), there is no runtime code to discover the kernel command line and (optional) initrd location and include them in a FDT. To facilitate easier preparation of a ready-to-boot FIT, add support for pre-patching the FDTs in a FIT with a preconfigured /chosen node including the bootargs and initrd location. Signed-off-by: Alexey Charkov --- tools/binman/entry.py | 8 +++ tools/binman/etype/fit.py | 103 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/tools/binman/entry.py b/tools/binman/entry.py index 9b39c711889..8164be34705 100644 --- a/tools/binman/entry.py +++ b/tools/binman/entry.py @@ -637,6 +637,14 @@ def GetData(self, required=True): self.Detail('GetData: size %s' % to_hex_size(self.data)) return self.data + def GetNode(self): + """Get the devicetree node which describes this entry + + Returns: + Node: Node for this entry + """ + return self._node + def GetPaddedData(self, data=None): """Get the data for an entry including any padding diff --git a/tools/binman/etype/fit.py b/tools/binman/etype/fit.py index 32caa03a7e1..750ff5b47ed 100644 --- a/tools/binman/etype/fit.py +++ b/tools/binman/etype/fit.py @@ -304,6 +304,40 @@ class Entry_fit(Entry_section): See :ref:`fdtgrep_filter` for more information. + Patching /chosen into the generated device trees + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + When the OS is booted directly from this FIT (e.g. Falcon mode) U-Boot + proper never runs, so its usual runtime /chosen fixups do not happen. Two + optional properties on the `@fdt-SEQ` node make binman patch each generated + device tree at build time: + + fit,bootargs + A string written to /chosen/bootargs of every generated device tree + (any existing value is overwritten). Typically set to `CONFIG_BOOTARGS` + via the preprocessed devicetree source. + + fit,initrd + The image name of a loadable (e.g. "ramdisk") to use as the initramfs. + Its `load` address and packed size are written to + /chosen/linux,initrd-start and /chosen/linux,initrd-end (at the width of + the device tree's root #address-cells) and reserved in the FDT + memory-reservation map, mirroring fdt_initrd(). If the referenced image + is absent or empty (e.g. an optional initramfs that was not supplied) + the initrd properties are omitted. + + For example:: + + images { + @fdt-SEQ { + description = "fdt-NAME"; + type = "flat_dt"; + compression = "none"; + fit,bootargs = "console=ttyS2,1500000 root=/dev/mmcblk0p2"; + fit,initrd = "ramdisk"; + }; + }; + Generating nodes from an ELF file (split-elf) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -749,6 +783,66 @@ def _run_fdtgrep(self, infile, phase, outfile): return self.fdtgrep.create_for_phase(infile, phase, outfile, self._remove_props) + def _patch_fdt_chosen(self, data, bootargs, initrd_name): + """Patch /chosen (bootargs and initrd) into an embedded FDT + + This writes a kernel command line and/or initramfs location into the + /chosen node of a device tree that binman is about to embed in the FIT. + It is used for Falcon-mode images, where U-Boot proper never runs to + perform these fixups itself. + + Args: + data (bytes): Device-tree contents to patch + bootargs (str or None): Command line to store in /chosen/bootargs, + or None to leave the command line unchanged + initrd_name (str or None): Image name of the loadable to use as the + initramfs. Its 'load' address and packed size are written to + /chosen/linux,initrd-start and /chosen/linux,initrd-end (at the + width of the DTB's root #address-cells) and reserved in the FDT + memory-reservation map, mirroring fdt_initrd(). Ignored when the + referenced image is absent or empty, or when None. + + Returns: + bytes: The patched device-tree contents + + Raises: + ValueError: the referenced image has data but no 'load' address + """ + fdt = libfdt.Fdt(bytearray(data)) + fdt.resize(fdt.totalsize() + 1024 + (len(bootargs) if bootargs else 0)) + + chosen = fdt.path_offset('/chosen', libfdt.QUIET_NOTFOUND) + if chosen == -libfdt.FDT_ERR_NOTFOUND: + chosen = fdt.add_subnode(0, 'chosen') + + if bootargs: + fdt.setprop_str(chosen, 'bootargs', bootargs) + + if initrd_name: + entry = self._priv_entries.get(initrd_name) + initrd = entry.GetData(required=False) if entry else None + size = len(initrd) if initrd else 0 + if size: + start = fdt_util.GetInt(entry.GetNode(), 'load') + if start is None: + self.Raise(f"fit,initrd image '{initrd_name}' has no " + "'load' address") + end = start + size + # Use the device tree's root #address-cells for the width, as + # fdt_initrd() does (4 bytes per cell) + cells = 4 * libfdt.check_err( + libfdt.fdt_address_cells(fdt._fdt, 0)) + fdt.setprop(chosen, 'linux,initrd-start', + start.to_bytes(cells, 'big')) + fdt.setprop(chosen, 'linux,initrd-end', + end.to_bytes(cells, 'big')) + # Reserve the initramfs region so the kernel does not reuse it + libfdt.check_err( + libfdt.fdt_add_mem_rsv(fdt._fdt, start, size)) + + fdt.pack() + return bytes(fdt.as_bytearray()[:fdt.totalsize()]) + def _build_input(self): """Finish the FIT by adding the 'data' properties to it @@ -857,6 +951,8 @@ def _gen_fdt_nodes(node, depth, in_images): else: fname = tools.get_input_filename(fdt_fname + '.dtb') fdt_phase = None + bootargs = None + initrd_name = None with fsw.add_node(node_name): for pname, prop in node.props.items(): if pname == 'fit,firmware': @@ -876,6 +972,10 @@ def _gen_fdt_nodes(node, depth, in_images): fsw.property('compatible', prop.bytes) elif pname == 'fit,fdt-phase': fdt_phase = fdt_util.GetString(node, pname) + elif pname == 'fit,bootargs': + bootargs = fdt_util.GetString(node, pname) + elif pname == 'fit,initrd': + initrd_name = fdt_util.GetString(node, pname) elif pname.startswith('fit,'): self._raise_subnode( node, f"Unknown directive '{pname}'") @@ -896,6 +996,9 @@ def _gen_fdt_nodes(node, depth, in_images): data = tools.read_file(phase_fname) else: data = tools.read_file(fname) + if bootargs is not None or initrd_name is not None: + data = self._patch_fdt_chosen(data, bootargs, + initrd_name) fsw.property('data', data) for subnode in node.subnodes: From d2bc06123e03fc28f3ee144afc778462c9fd124b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 23 Jul 2026 16:10:30 +0400 Subject: [PATCH 70/78] binman: tests: Add test coverage for a FIT with embedded Linux+initrd and /chosen Add a test for a FIT image with a fake Linux kernel and initrd, and a valid device tree into which a /chosen node is added (containing a bootargs property, as is relevant for Falcon mode boot). Signed-off-by: Alexey Charkov --- tools/binman/ftest.py | 107 ++++++++++++++++++ tools/binman/test/fit/fit_chosen.dts | 62 ++++++++++ .../binman/test/fit/fit_chosen_no_initrd.dts | 60 ++++++++++ tools/binman/test/fit/fit_chosen_no_load.dts | 57 ++++++++++ 4 files changed, 286 insertions(+) create mode 100644 tools/binman/test/fit/fit_chosen.dts create mode 100644 tools/binman/test/fit/fit_chosen_no_initrd.dts create mode 100644 tools/binman/test/fit/fit_chosen_no_load.dts diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py index c5c67c7e68b..064f7f6daec 100644 --- a/tools/binman/ftest.py +++ b/tools/binman/ftest.py @@ -4405,6 +4405,113 @@ def testFitFdtNotInList(self): self.assertIn("default-dt entry argument 'test-fdt3' not found in fdt list: test-fdt1, test-fdt2", str(e.exception)) + def testFitChosen(self): + """Test patching /chosen (bootargs + initrd) into generated FIT FDTs""" + kernel = b'\x01\x02\x03\x04 kernel' + initrd = b'initramfs-contents' + self._MakeInputFile('test-kernel.bin', kernel) + self._MakeInputFile('test-initrd.bin', initrd) + entry_args = { + 'of-list': 'model1 model2', + 'default-dt': 'model2', + 'linux-kernel-path': 'test-kernel.bin', + 'linux-initrd-path': 'test-initrd.bin', + } + testdir, _ = self.SetupAlternateDts() + data = self._DoReadFileDtb( + 'fit/fit_chosen.dts', use_real_dtb=True, update_dtb=True, + entry_args=entry_args, extra_indirs=[testdir])[0] + fit_data = data[len(U_BOOT_DATA):-len(U_BOOT_NODTB_DATA)] + fit = fdt.Fdt.FromData(fit_data) + fit.Scan() + + # ramdisk is loaded at 0x1000 (see fit_chosen.dts) + rd_start = 0x1000 + rd_end = rd_start + len(initrd) + for seq in (1, 2): + fnode = fit.GetNode('/images/fdt-%d' % seq) + self.assertIsNotNone(fnode) + dtb = fdt.Fdt.FromData(fnode.props['data'].bytes) + dtb.Scan() + chosen = dtb.GetNode('/chosen') + self.assertIsNotNone(chosen) + self.assertEqual(b'console=ttyS0 root=/dev/ram\0', + chosen.props['bootargs'].bytes) + start = int.from_bytes(chosen.props['linux,initrd-start'].bytes, + 'big') + end = int.from_bytes(chosen.props['linux,initrd-end'].bytes, 'big') + self.assertEqual(rd_start, start) + self.assertEqual(rd_end, end) + # The initramfs region must be reserved in the FDT + fdt_obj = dtb.GetFdtObj() + self.assertEqual(1, fdt_obj.num_mem_rsv()) + _, addr, size = fdt_obj.get_mem_rsv(0) + self.assertEqual(rd_start, addr) + self.assertEqual(len(initrd), size) + + def testFitChosenNoInitrd(self): + """Test a Falcon-mode FIT where the optional initramfs is not supplied""" + self._MakeInputFile('test-kernel.bin', b'\x01\x02\x03\x04 kernel') + entry_args = { + 'of-list': 'model1 model2', + 'default-dt': 'model2', + 'linux-kernel-path': 'test-kernel.bin', + 'linux-initrd-path': '', + } + testdir, _ = self.SetupAlternateDts() + with terminal.capture() as (stdout, stderr): + data = self._DoReadFileDtb( + 'fit/fit_chosen_no_initrd.dts', use_real_dtb=True, + update_dtb=True, entry_args=entry_args, + extra_indirs=[testdir], allow_fake_blobs=False)[0] + self.assertRegex( + stderr.getvalue(), + "Image '.*' is missing optional external blobs but is still functional: linux-initrd") + + fit_data = data[len(U_BOOT_DATA):-len(U_BOOT_NODTB_DATA)] + fit = fdt.Fdt.FromData(fit_data) + fit.Scan() + + for seq in (1, 2): + # The node is still emitted and still listed as a loadable, as is + # already the case for an optional OP-TEE which was not supplied + # (see testFitFirmwareLoadables), but it carries no data + cnode = fit.GetNode('/configurations/config-%d' % seq) + self.assertEqual(b'kernel\0', cnode.props['firmware'].bytes) + self.assertEqual(['ramdisk'], + fdt_util.GetStringList(cnode, 'loadables')) + self.assertEqual(b'', fit.GetNode('/images/ramdisk') + .props['data'].bytes) + + # bootargs is still patched in, but the initrd properties are not + fnode = fit.GetNode('/images/fdt-%d' % seq) + dtb = fdt.Fdt.FromData(fnode.props['data'].bytes) + dtb.Scan() + chosen = dtb.GetNode('/chosen') + self.assertEqual(b'console=ttyS0\0', chosen.props['bootargs'].bytes) + self.assertNotIn('linux,initrd-start', chosen.props) + self.assertNotIn('linux,initrd-end', chosen.props) + self.assertEqual(0, dtb.GetFdtObj().num_mem_rsv()) + + def testFitChosenNoLoad(self): + """Test a fit,initrd image which is missing its load address""" + self._MakeInputFile('test-kernel.bin', b'\x01\x02\x03\x04 kernel') + self._MakeInputFile('test-initrd.bin', b'initramfs-contents') + entry_args = { + 'of-list': 'model1 model2', + 'default-dt': 'model2', + 'linux-kernel-path': 'test-kernel.bin', + 'linux-initrd-path': 'test-initrd.bin', + } + testdir, _ = self.SetupAlternateDts() + with self.assertRaises(ValueError) as e: + self._DoReadFileDtb( + 'fit/fit_chosen_no_load.dts', use_real_dtb=True, + update_dtb=True, entry_args=entry_args, + extra_indirs=[testdir]) + self.assertIn("fit,initrd image 'ramdisk' has no 'load' address", + str(e.exception)) + def testFitExtblobMissingHelp(self): """Test display of help messages when an external blob is missing""" control.missing_blob_help = control._ReadMissingBlobHelp() diff --git a/tools/binman/test/fit/fit_chosen.dts b/tools/binman/test/fit/fit_chosen.dts new file mode 100644 index 00000000000..ae2e4a084af --- /dev/null +++ b/tools/binman/test/fit/fit_chosen.dts @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/dts-v1/; + +/ { + #address-cells = <1>; + #size-cells = <1>; + + binman { + u-boot { + }; + fit { + description = "test-desc"; + #address-cells = <1>; + fit,fdt-list = "of-list"; + + images { + kernel { + description = "Linux kernel"; + type = "kernel"; + arch = "arm64"; + os = "linux"; + compression = "none"; + load = <0x100>; + entry = <0x100>; + linux-kernel { + }; + }; + ramdisk { + description = "ramdisk"; + type = "ramdisk"; + compression = "none"; + load = <0x1000>; + linux-initrd { + }; + }; + @fdt-SEQ { + description = "fdt-NAME.dtb"; + type = "flat_dt"; + compression = "none"; + fit,bootargs = "console=ttyS0 root=/dev/ram"; + fit,initrd = "ramdisk"; + hash { + algo = "sha256"; + }; + }; + }; + + configurations { + default = "@config-DEFAULT-SEQ"; + @config-SEQ { + description = "conf-NAME.dtb"; + firmware = "kernel"; + loadables = "ramdisk"; + fdt = "fdt-SEQ"; + }; + }; + }; + u-boot-nodtb { + }; + }; +}; diff --git a/tools/binman/test/fit/fit_chosen_no_initrd.dts b/tools/binman/test/fit/fit_chosen_no_initrd.dts new file mode 100644 index 00000000000..82468e94dcd --- /dev/null +++ b/tools/binman/test/fit/fit_chosen_no_initrd.dts @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/dts-v1/; + +/ { + #address-cells = <1>; + #size-cells = <1>; + + binman { + u-boot { + }; + fit { + description = "test-desc"; + #address-cells = <1>; + fit,fdt-list = "of-list"; + + images { + kernel { + description = "Linux kernel"; + type = "kernel"; + arch = "arm64"; + os = "linux"; + compression = "none"; + load = <0x100>; + entry = <0x100>; + linux-kernel { + }; + }; + ramdisk { + description = "ramdisk"; + type = "ramdisk"; + compression = "none"; + load = <0x1000>; + linux-initrd { + optional; + }; + }; + @fdt-SEQ { + description = "fdt-NAME.dtb"; + type = "flat_dt"; + compression = "none"; + fit,bootargs = "console=ttyS0"; + fit,initrd = "ramdisk"; + }; + }; + + configurations { + default = "@config-DEFAULT-SEQ"; + @config-SEQ { + description = "conf-NAME.dtb"; + fit,firmware = "kernel", "ramdisk"; + fit,loadables; + fdt = "fdt-SEQ"; + }; + }; + }; + u-boot-nodtb { + }; + }; +}; diff --git a/tools/binman/test/fit/fit_chosen_no_load.dts b/tools/binman/test/fit/fit_chosen_no_load.dts new file mode 100644 index 00000000000..7a3d393dd21 --- /dev/null +++ b/tools/binman/test/fit/fit_chosen_no_load.dts @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/dts-v1/; + +/ { + #address-cells = <1>; + #size-cells = <1>; + + binman { + u-boot { + }; + fit { + description = "test-desc"; + #address-cells = <1>; + fit,fdt-list = "of-list"; + + images { + kernel { + description = "Linux kernel"; + type = "kernel"; + arch = "arm64"; + os = "linux"; + compression = "none"; + load = <0x100>; + entry = <0x100>; + linux-kernel { + }; + }; + ramdisk { + description = "ramdisk"; + type = "ramdisk"; + compression = "none"; + linux-initrd { + }; + }; + @fdt-SEQ { + description = "fdt-NAME.dtb"; + type = "flat_dt"; + compression = "none"; + fit,initrd = "ramdisk"; + }; + }; + + configurations { + default = "@config-DEFAULT-SEQ"; + @config-SEQ { + description = "conf-NAME.dtb"; + firmware = "kernel"; + loadables = "ramdisk"; + fdt = "fdt-SEQ"; + }; + }; + }; + u-boot-nodtb { + }; + }; +}; From 6b827c284c9fd1d1699485f2efc1d36f8b0ca106 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 8 Jun 2026 19:52:16 +0400 Subject: [PATCH 71/78] rockchip: spl: default to Falcon mode boot in SPL if enabled Common code calls into a platform specific function to determine whether to attempt OS boot in Falcon mode. There is currently no platform logic to drive that decision on Rockchip, so attempt Falcon mode boot whenever it is enabled in the configuration. This can be overridden by board code if needed with appropriate logic, such as checking for a button state to skip Falcon mode boot when pressed. Signed-off-by: Alexey Charkov --- arch/arm/mach-rockchip/spl.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/arch/arm/mach-rockchip/spl.c b/arch/arm/mach-rockchip/spl.c index e989c148079..54dec06737e 100644 --- a/arch/arm/mach-rockchip/spl.c +++ b/arch/arm/mach-rockchip/spl.c @@ -153,6 +153,35 @@ void spl_board_prepare_for_boot(void) cleanup_before_linux(); } +#if CONFIG_IS_ENABLED(OS_BOOT) +/** + * board_spl_start_uboot() - decides whether to boot the OS or U-Boot proper + * + * If Falcon mode (direct Linux boot via TF-A from SPL) is selected via config, + * try booting to Linux first, with automatic fallback to U-Boot if it fails to + * load. Boards that want to implement conditional U-Boot/Linux selection logic + * override this, for instance to look at a button. + * + * Returns + * 0 to boot the OS directly + * 1 to boot U-Boot proper + */ +__weak int board_spl_start_uboot(void) +{ + return 0; +} + +/* + * This overrides the weak default in common/spl/spl.c, so it must not itself + * be weak: two weak definitions of the same symbol would leave the choice + * between them up to archive link order. + */ +int spl_start_uboot(void) +{ + return board_spl_start_uboot(); +} +#endif + #if CONFIG_IS_ENABLED(RAM_DEVICE) && IS_ENABLED(CONFIG_SPL_LOAD_FIT) binman_sym_declare_optional(ulong, payload, image_pos); binman_sym_declare_optional(ulong, payload, size); From a9e5d8d72d395755ad44c21a1d7dbf98153275f0 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 9 Jun 2026 12:11:07 +0400 Subject: [PATCH 72/78] rockchip: binman: Add support for Falcon mode FIT images with TF-A+Linux Modern Rockchip SoCs such as RK3576 require TF-A to be running to provide firmware services to the OS. To enable booting Linux in Falcon mode on such SoCs, add a binman template for generation of FIT images containing appropriately split TF-A and TEE binaries (as is currently done by binman for U-boot proper images), externally provided Linux kernel and optionally an initrd. Any FDTs provided to the generator will be pre-patched with a /chosen node containing an initrd load address and size (if an initrd is provided) and a bootargs property containing the kernel command line (if provided), so that the kernel can be booted directly without external preprocessing. A config fragment rockchip-falcon.config can be used to enable Falcon mode boot and the building of TF-A+Falcon with binman, e.g.: make nanopi-m5-rk3576_defconfig \ rockchip-falcon.config This will produce a FIT image u-boot-rockchip-falcon.itb containing TF-A, TEE, Linux kernel and optionally an initrd, which can be flashed to UFS and booted in Falcon mode. This can also be combined with the existing rockchip-ramboot.config fragment to obtain images suitable for booting from RAM, e.g.: make nanopi-m5-rk3576_defconfig \ rockchip-falcon.config \ rockchip-ramboot.config The resulting images can be uploaded directly to RAM via Maskrom with no storage or USB loader required, e.g.: rockusb download-sram u-boot-rockchip-usb471.bin rockusb download-ddr u-boot-rockchip-usb472-falcon.bin Signed-off-by: Alexey Charkov --- arch/arm/dts/rockchip-u-boot.dtsi | 204 ++++++++++++++++++++++---- arch/arm/mach-rockchip/Kconfig | 49 +++++++ board/rockchip/rockchip-falcon.config | 3 + 3 files changed, 225 insertions(+), 31 deletions(-) create mode 100644 board/rockchip/rockchip-falcon.config diff --git a/arch/arm/dts/rockchip-u-boot.dtsi b/arch/arm/dts/rockchip-u-boot.dtsi index 4ba6a87e78a..5e23ff76175 100644 --- a/arch/arm/dts/rockchip-u-boot.dtsi +++ b/arch/arm/dts/rockchip-u-boot.dtsi @@ -35,6 +35,16 @@ #define HAS_FIT #endif +#ifdef CONFIG_ROCKCHIP_FALCON_IMAGE +/* Falcon-mode load addresses, relative to the SoC DRAM base. */ +#define FALCON_KERNEL_LOAD \ + (CFG_SYS_SDRAM_BASE + CONFIG_ROCKCHIP_FALCON_KERNEL_LOAD) +#define FALCON_FDT_LOAD \ + (CFG_SYS_SDRAM_BASE + CONFIG_ROCKCHIP_FALCON_FDT_LOAD) +#define FALCON_INITRD_LOAD \ + (CFG_SYS_SDRAM_BASE + CONFIG_ROCKCHIP_FALCON_INITRD_LOAD) +#endif + / { binman: binman { multiple-images; @@ -44,37 +54,13 @@ #ifdef CONFIG_SPL &binman { #ifdef HAS_FIT - fit_template: template-1 { - type = "fit"; #ifdef CONFIG_ARM64 - description = "FIT image for U-Boot with bl31 (TF-A)"; -#else - description = "FIT image with OP-TEE"; -#endif - #address-cells = <1>; - fit,fdt-list = "of-list"; - fit,external-offset = ; - fit,align = <512>; + /* + * Shared ARM Trusted Firmware (BL31) and optional OP-TEE split-elf + * images, reused by both the U-Boot and Falcon Linux FIT templates. + */ + fit_firmware_template: template-firmware { images { - u-boot { - description = "U-Boot"; - type = "standalone"; - os = "u-boot"; - arch = FIT_ARCH; - compression = FIT_UBOOT_COMP; - load = ; - entry = ; - u-boot-nodtb { - compress = FIT_UBOOT_COMP; - }; -#ifdef FIT_HASH_ALGO - hash { - algo = FIT_HASH_ALGO; - }; -#endif - }; - -#ifdef CONFIG_ARM64 @atf-SEQ { fit,operation = "split-elf"; description = "ARM Trusted Firmware"; @@ -114,7 +100,42 @@ }; #endif }; -#else /* !CONFIG_ARM64 */ + }; + }; +#endif /* CONFIG_ARM64 */ + + fit_template: template-1 { + type = "fit"; +#ifdef CONFIG_ARM64 + description = "FIT image for U-Boot with bl31 (TF-A)"; + insert-template = <&fit_firmware_template>; +#else + description = "FIT image with OP-TEE"; +#endif + #address-cells = <1>; + fit,fdt-list = "of-list"; + fit,external-offset = ; + fit,align = <512>; + images { + u-boot { + description = "U-Boot"; + type = "standalone"; + os = "u-boot"; + arch = FIT_ARCH; + compression = FIT_UBOOT_COMP; + load = ; + entry = ; + u-boot-nodtb { + compress = FIT_UBOOT_COMP; + }; +#ifdef FIT_HASH_ALGO + hash { + algo = FIT_HASH_ALGO; + }; +#endif + }; + +#ifndef CONFIG_ARM64 op-tee { description = "OP-TEE"; type = "tee"; @@ -132,7 +153,7 @@ }; #endif }; -#endif /* CONFIG_ARM64 */ +#endif /* !CONFIG_ARM64 */ @fdt-SEQ { description = "fdt-NAME"; @@ -161,6 +182,97 @@ }; }; }; + +#ifdef CONFIG_ROCKCHIP_FALCON_IMAGE + fit_falcon_template: template-falcon { + type = "fit"; + description = "FIT image for Falcon Linux with bl31 (TF-A)"; + insert-template = <&fit_firmware_template>; + #address-cells = <1>; + fit,fdt-list = "of-list"; + fit,external-offset = ; + fit,align = <512>; + + images { + kernel { + description = "Linux kernel"; + type = "kernel"; + arch = FIT_ARCH; + os = "linux"; + compression = FIT_UBOOT_COMP; + load = ; + entry = ; + + linux-kernel { + compress = FIT_UBOOT_COMP; + }; +#ifdef FIT_HASH_ALGO + hash { + algo = FIT_HASH_ALGO; + }; +#endif + }; + + ramdisk { + description = "ramdisk"; + type = "ramdisk"; + arch = FIT_ARCH; + compression = "none"; + load = ; + + linux-initrd { + optional; + }; +#ifdef FIT_HASH_ALGO + hash { + algo = FIT_HASH_ALGO; + }; +#endif + }; + + @fdt-SEQ { + description = "fdt-NAME"; + type = "flat_dt"; + arch = FIT_ARCH; + compression = "none"; + load = ; +#ifdef CONFIG_USE_BOOTARGS + fit,bootargs = CONFIG_BOOTARGS; +#endif + fit,initrd = "ramdisk"; +#ifdef FIT_HASH_ALGO + hash { + algo = FIT_HASH_ALGO; + }; +#endif + }; + }; + + configurations { + default = "@config-DEFAULT-SEQ"; + @config-SEQ { + description = "NAME.dtb"; + fit,firmware = "atf-1", "kernel", "ramdisk"; + fit,loadables; + fit,compatible; + fdt = "fdt-SEQ"; +#ifdef FIT_HASH_ALGO + hash { + algo = FIT_HASH_ALGO; + }; +#endif + }; + }; + }; + + falcon-fit { + filename = "u-boot-rockchip-falcon.itb"; + + fit { + insert-template = <&fit_falcon_template>; + }; + }; +#endif #endif /* HAS_FIT */ simple-bin { @@ -260,6 +372,36 @@ }; }; }; + +#ifdef CONFIG_ROCKCHIP_FALCON_IMAGE + simple-bin-usb472-falcon { + filename = "u-boot-rockchip-usb472-falcon.bin"; + pad-byte = <0x00>; + + u-boot-spl { + }; + + payload { + type = "section"; + align = ; + /* + * Must be named 'u-boot-any', unlike the non-Falcon + * image which can call this 'fit': SPL declares + * binman_sym(u_boot_any, image_pos) unconditionally, and + * binman resolves that symbol against an entry of that + * exact name or one called u-boot[-elf|-img|-nodtb]. + * Those only exist here by way of a U-Boot image inside + * the FIT, which a Falcon FIT does not carry, so without + * this name the symbol cannot be resolved and binman + * fails. The type property is what selects the etype. + */ + u-boot-any { + type = "fit"; + insert-template = <&fit_falcon_template>; + }; + }; + }; +#endif #endif /* CONFIG_ROCKCHIP_MASKROM_IMAGE */ }; #endif /* CONFIG_SPL */ diff --git a/arch/arm/mach-rockchip/Kconfig b/arch/arm/mach-rockchip/Kconfig index 1a2e7847c9e..321afc0fbdf 100644 --- a/arch/arm/mach-rockchip/Kconfig +++ b/arch/arm/mach-rockchip/Kconfig @@ -758,6 +758,55 @@ config ROCKCHIP_MASKROM_IMAGE Rockchip SoCs support maskrom mode boot over USB. Enable this option to produce maskrom mode boot images containing U-Boot. +config ROCKCHIP_FALCON_IMAGE + bool "Build a Falcon mode image for Rockchip" + depends on SPL_OS_BOOT && ARM64 && SPL_ATF && SPL_FIT + help + Enable this option to produce a FIT image which SPL can use to boot + Linux directly via TF-A, bypassing U-Boot proper (Falcon mode). + + The image bundles TF-A (BL31), an optional OP-TEE, a Linux kernel + and an optional initramfs, and its device trees are pre-patched with + a /chosen node so that no further processing is needed before boot. + The kernel is mandatory and must be supplied at build time with + LINUX_KERNEL=/path/to/Image; pass LINUX_INITRD=/path/to/initramfs to + include an initramfs as well. + + This only controls image generation. Say N to build a Falcon-capable + SPL without bundling a kernel into the U-Boot image, for example when + the OS payload is assembled and written to storage separately. + +config ROCKCHIP_FALCON_KERNEL_LOAD + hex "Falcon mode kernel load offset" + depends on ROCKCHIP_FALCON_IMAGE + default 0x10000000 + help + Offset from the start of DRAM at which the Falcon mode kernel is + loaded and entered. + + All three Falcon mode load offsets have to be checked against the + memory layout of the SoC and of the firmware in use. None of the + three payloads may overlap each other, and none of them may overlap + the regions BL31 and OP-TEE are linked to run from - note that + binman splits their ELFs, so each of those occupies several regions + rather than one contiguous range. + +config ROCKCHIP_FALCON_FDT_LOAD + hex "Falcon mode devicetree load offset" + depends on ROCKCHIP_FALCON_IMAGE + default 0x1c000000 + help + Offset from the start of DRAM at which the Falcon mode devicetree is + loaded. The same constraints as for the kernel offset apply. + +config ROCKCHIP_FALCON_INITRD_LOAD + hex "Falcon mode initramfs load offset" + depends on ROCKCHIP_FALCON_IMAGE + default 0x1e000000 + help + Offset from the start of DRAM at which the Falcon mode initramfs is + loaded. The same constraints as for the kernel offset apply. + config LNX_KRNL_IMG_TEXT_OFFSET_BASE default TEXT_BASE diff --git a/board/rockchip/rockchip-falcon.config b/board/rockchip/rockchip-falcon.config new file mode 100644 index 00000000000..ac903f16f46 --- /dev/null +++ b/board/rockchip/rockchip-falcon.config @@ -0,0 +1,3 @@ +CONFIG_ROCKCHIP_FALCON_IMAGE=y +CONFIG_SPL_OS_BOOT=y +CONFIG_SYS_SPI_KERNEL_OFFS=0x0 From 2981bffc34e74e92cc83ffd1fd3a7f697772040c Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 29 Jul 2026 20:43:14 +0400 Subject: [PATCH 73/78] doc: falcon: Describe booting the OS through TF-A Falcon Mode documentation only covers SPL entering the kernel itself, which is not what happens on ARM64 SoCs needing ARM Trusted Firmware resident to provide firmware services: SPL loads a FIT holding BL31 as its firmware image and the kernel as a loadable, and BL31 enters the kernel as BL33. Describe that flow, what the kernel image may be given that neither booti_setup() nor bootz_setup() runs, how it differs from the classic flow, and how to configure and build it on Rockchip. Signed-off-by: Alexey Charkov --- doc/board/rockchip/rockchip.rst | 9 +++ doc/develop/falcon.rst | 131 +++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/doc/board/rockchip/rockchip.rst b/doc/board/rockchip/rockchip.rst index d96706a238a..453206103e0 100644 --- a/doc/board/rockchip/rockchip.rst +++ b/doc/board/rockchip/rockchip.rst @@ -326,6 +326,15 @@ To build rk3588 boards: make evb-rk3588_defconfig make CROSS_COMPILE=aarch64-linux-gnu- +Falcon mode +""""""""""" + +ARM64 Rockchip SoCs can also boot Linux straight from SPL through TF-A, +without U-Boot proper running at all. Enabling CONFIG_ROCKCHIP_FALCON_IMAGE +makes binman build a ``u-boot-rockchip-falcon.itb`` holding TF-A, the kernel +and an optional initramfs, which are passed in with LINUX_KERNEL and +LINUX_INITRD. See :doc:`../../develop/falcon` for the details. + Flashing -------- diff --git a/doc/develop/falcon.rst b/doc/develop/falcon.rst index 3fc23f65c45..f29aaee06a1 100644 --- a/doc/develop/falcon.rst +++ b/doc/develop/falcon.rst @@ -95,11 +95,138 @@ Function that a board must implement ------------------------------------ void spl_board_prepare_for_linux(void) - optional, called from SPL before starting the kernel + optional, called from SPL before starting the kernel. Not called when the + kernel is entered through TF-A, see `Falcon Mode with TF-A`_; use + spl_board_prepare_for_boot() instead, which is called on every path. spl_start_uboot() required, returns "0" if SPL should start the kernel, "1" if U-Boot - must be started. + must be started. It may be called from more than one place in a single + SPL run, so prefer spl_falcon_boot() when querying the result - that + caches the answer, which matters for implementations that are not + idempotent, such as those sampling a button. + +Falcon Mode with TF-A +--------------------- + +On ARM64 SoCs which need ARM Trusted Firmware resident to provide firmware +services to the OS, the kernel cannot usefully be entered directly from SPL. +Instead SPL loads a FIT holding BL31 as its ``firmware`` image and the kernel +as a ``loadable``, hands control to BL31, and BL31 enters the kernel as BL33. + +SPL picks BL33 in spl_invoke_atf(), which looks for an image with +``os = "linux"`` in the ``/fit-images`` node it appended to the device tree +and falls back to ``os = "u-boot"``. The kernel is entered at EL2 with the +device tree address in x0, as the arm64 boot protocol requires. + +Nothing generates a FIT holding both an OS and a U-Boot payload, so the two +are separate images, and it is the loader which decides where to read from. +In Falcon mode it tries the OS image first and falls back to the U-Boot one +if that cannot be loaded, unless secure Falcon mode is in effect, in which +case there is no fallback. For UFS the two are addressed by +CONFIG_SPL_UFS_RAW_OS_DEVNUM and CONFIG_SPL_UFS_RAW_OS_SECTOR against +CONFIG_SPL_UFS_RAW_U_BOOT_DEVNUM and CONFIG_SPL_UFS_RAW_U_BOOT_SECTOR, so +they can live on different logical units, at different offsets, or both. +spl_invoke_atf() then hands over to whichever of the two actually loaded, +which is what makes that fallback work. + +Pointing both at one location is also possible, and is what the defaults do: +only one region then has to be reserved, at the cost of having nothing left +to fall back to. + +What the kernel image may be +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The payload is an ordinary FIT image node with ``type = "kernel"`` and +``os = "linux"``. SPL copies it to its ``load`` address, and BL31 enters it +at ``entry``, or at ``load`` if no entry point is given. + +Neither booti_setup() nor bootz_setup() runs in this flow - SPL does not +enter the kernel itself, it only places it in memory for BL31 - so there is +no inspection of the arm64 Image header and no relocation to the text offset +the kernel asks for. The addresses in the FIT have to be directly usable, +which for an arm64 ``Image`` means 2 MiB aligned, and clear of the device +tree, of the initramfs and of every region BL31 and OP-TEE are linked to run +from - each of those is split into several regions when it is supplied as an +ELF, rather than occupying one contiguous range. + +The image is loaded verbatim unless CONFIG_SPL_GZIP or CONFIG_SPL_LZMA is +enabled, in which case the FIT ``compression`` property may be set to +``gzip`` or ``lzma`` and SPL decompresses while loading. Two limits apply +then. The uncompressed image has to fit in CONFIG_SYS_BOOTM_LEN. And the +compressed one is first staged at CONFIG_SYS_LOAD_ADDR, so that address has +to clear both the region being decompressed to and, when the FIT is itself +already in memory, the FIT: booting from RAM reads the image out of DRAM at +CONFIG_SPL_LOAD_FIT_ADDRESS, and a staging buffer landing inside it makes +the copy overwrite its own source, which shows up as:: + + ## Checking hash(es) for Image kernel ... sha256 error! + +The device trees in the FIT +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each device tree in the FIT can be pre-patched at build time with a +``/chosen`` node holding the kernel command line and the initramfs location, +so that nothing has to fix them up before boot. See the ``fit,bootargs`` and +``fit,initrd`` properties of binman's :ref:`etype_fit`. + +Differences from the classic flow +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* The device tree comes from the same FIT as the kernel, so no separate + 'args' file is involved and CONFIG_SPL_OS_BOOT_ARGS is not needed. + +* CONFIG_SPL_BOOTI provides an entry path which is unused here. + +* Because U-Boot proper is never loaded in this flow, SPL panics rather than + jumping to CONFIG_TEXT_BASE if neither a kernel nor a U-Boot image can be + resolved from the FIT. + +Neither of the first two defaults to y when CONFIG_SPL_ATF is enabled, so +nothing has to be turned off by hand. + +Example: Rockchip RK3576 +~~~~~~~~~~~~~~~~~~~~~~~~ + +Set the following in the board defconfig:: + + CONFIG_SPL_OS_BOOT=y + CONFIG_ROCKCHIP_FALCON_IMAGE=y + +On a board whose SPL can load from SPI flash, CONFIG_SYS_SPI_KERNEL_OFFS +becomes visible and has no default, so give it a value suiting the flash +layout even if the kernel is loaded from elsewhere. The load addresses of +the kernel, device tree and initramfs default to offsets from the start of +DRAM which clear BL31 and OP-TEE, and can be adjusted with +CONFIG_ROCKCHIP_FALCON_KERNEL_LOAD and friends. + +Enabling CONFIG_SPL_LZMA or CONFIG_SPL_GZIP additionally compresses the +kernel in the image, trading some decompression time for a smaller one - for +a 30 MiB arm64 Image, roughly 35% of the original with LZMA and 39% with +gzip. Note that this compresses U-Boot itself as well, since both use the +same setting. + +The kernel and, optionally, an initramfs are passed in at build time:: + + $ make BL31=.../bl31.elf \ + ROCKCHIP_TPL=.../rk3576_ddr_lp4_2112MHz_lp5_2736MHz_v1.13.bin \ + LINUX_KERNEL=.../Image \ + LINUX_INITRD=.../ramdisk.cpio.zst + +This produces ``u-boot-rockchip-falcon.itb``, to be written at +CONFIG_SPL_UFS_RAW_OS_SECTOR (or the equivalent for the boot medium in use). +Its default shares the region with U-Boot, which suits Falcon-only boot; +point it elsewhere to keep a U-Boot image to fall back to. + +With CONFIG_ROCKCHIP_MASKROM_IMAGE enabled, an equivalent maskrom payload +``u-boot-rockchip-usb472-falcon.bin`` is built as well, which can be loaded +straight into RAM:: + + rockusb download-sram u-boot-rockchip-usb471.bin + rockusb download-ddr u-boot-rockchip-usb472-falcon.bin + +Rockchip attempts Falcon Mode boot whenever it is enabled. Boards which need +a runtime choice override board_spl_start_uboot(). Environment variables --------------------- From b5b70eeb5a377cf72643255bcc26a5cd88d11199 Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Sat, 1 Aug 2026 00:55:20 +0000 Subject: [PATCH 74/78] fs: btrfs: fix zstd decompression of compressed inline extents The kernel compresses an inline extent as a whole block: run_delalloc_inline() calls btrfs_compress_bio(inode, 0, blocksize, ...), so the data is zero-filled past EOF and the resulting zstd frame declares a content size of one block. The extent item records the unaligned file size though - __cow_file_range_inline() passes i_size down to insert_inline_extent(), which stores it as ram_bytes. btrfs_read_extent_inline() sizes its decompression buffer from ram_bytes, so for a 1900-byte file the destination is 1900 bytes while the frame decodes to 4096. Since commit 918adf8e0733 ("btrfs: Use U-Boot API for decompression") btrfs decompresses through the common U-Boot helper, which uses the one-shot zstd_decompress_dctx(). That API requires the destination to cover the whole frame and fails with dstSize_tooSmall, error 70, otherwise. The streaming ZSTD_decompressStream() path it replaced stopped once the output buffer was full, so it never hit this. The kernel side does not notice because fs/btrfs/zstd.c streams into its own buffer and copies out at most destlen. Allocate a full block for the decompression buffer and copy only ram_bytes back to the caller. An inline extent never spans more than one block, which bounds the allocation. This shows up on RK3399 and ODROID-N2 as "zstd_decompress: failed to decompress: 70" (armbian/build#9651, #10208), where it breaks fdt apply on zstd-compressed overlays. Images built with mkfs.btrfs --rootdir --compress zstd do not reproduce it, since btrfs-progs writes a frame whose content size already equals ram_bytes. Only files written at runtime through the kernel trip it. Fixes: 918adf8e0733 ("btrfs: Use U-Boot API for decompression") Signed-off-by: Cole Munz Reviewed-by: Qu Wenruo --- fs/btrfs/inode.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 3998ffc2c81..53f1059e1ef 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -362,9 +362,11 @@ int btrfs_read_extent_inline(struct btrfs_path *path, struct btrfs_file_extent_item *fi, char *dest) { struct extent_buffer *leaf = path->nodes[0]; + struct btrfs_fs_info *fs_info = leaf->fs_info; int slot = path->slots[0]; char *cbuf = NULL; char *dbuf = NULL; + u32 dbuf_size; u32 csize; u32 dsize; int ret; @@ -380,8 +382,17 @@ int btrfs_read_extent_inline(struct btrfs_path *path, /* Compressed extent, prepare the compressed and data buffer */ dsize = btrfs_file_extent_ram_bytes(leaf, fi); + /* + * The kernel compresses an inline extent as a whole block, zero-filling + * the tail past EOF, so the stream can decompress to more than + * ram_bytes. zstd's one-shot API rejects a destination that cannot + * hold the entire frame, so give the decompressor a full block and copy + * only ram_bytes back out. An inline extent never spans more than one + * block, which bounds the allocation. + */ + dbuf_size = max_t(u32, dsize, fs_info->sectorsize); cbuf = malloc(csize); - dbuf = malloc(dsize); + dbuf = malloc(dbuf_size); if (!cbuf || !dbuf) { ret = -ENOMEM; goto out; @@ -389,7 +400,7 @@ int btrfs_read_extent_inline(struct btrfs_path *path, read_extent_buffer(leaf, cbuf, btrfs_file_extent_inline_start(fi), csize); ret = btrfs_decompress(btrfs_file_extent_compression(leaf, fi), - cbuf, csize, dbuf, dsize); + cbuf, csize, dbuf, dbuf_size); if (ret < 0) { ret = -EIO; goto out; From 91f664536b94334216557927eb064c117f1dc971 Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Sun, 2 Aug 2026 09:35:19 +0000 Subject: [PATCH 75/78] fs: btrfs: report file sizes from readdir btrfs_readdir() zeroes the dirent and fills in only the name and the type, so dent->size stays 0 and every file is listed as zero bytes: => ls host 0 / 0 f_192k.bin 0 small_3k.bin Reads themselves are fine, since btrfs_read() takes the size from btrfs_size(), which does its own inode item lookup. It affects EFI too: dir_read() in lib/efi_loader/efi_file.c copies dent->size into both file_size and physical_size, so an EFI application enumerating a directory on btrfs sees every file as empty, which is the generic-code path Alexey's readdir series moves btrfs onto. The custom listing that fs_ls_generic() replaced looked the inode item up and printed the real size, and every other filesystem in the tree fills dent->size in its own readdir: ext4fs.c:327, exfat io.c:805, erofs fs.c:186, squashfs sqfs.c:1095 and fat.c:1555. btrfs_next_dir_entry() already has the dir item mapped, so read the key it points at while we are there and hand it back to the caller, and use that to reach the inode item. A subvolume entry points at a root item instead and has no size of its own, so leave that one at 0. => ls host 0 / 196608 f_192k.bin 3000 small_3k.bin Fixes: 31cf3f177823 ("fs: btrfs: use fs_ls_generic() and drop custom implementation") Signed-off-by: Cole Munz --- fs/btrfs/btrfs.c | 24 +++++++++++++++++++++++- fs/btrfs/ctree.h | 3 ++- fs/btrfs/dir-item.c | 7 ++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/btrfs.c b/fs/btrfs/btrfs.c index e663dda12e8..b2856be0662 100644 --- a/fs/btrfs/btrfs.c +++ b/fs/btrfs/btrfs.c @@ -93,7 +93,10 @@ int btrfs_readdir(struct fs_dir_stream *fs_dirs, struct fs_dirent **dentp) struct btrfs_dir_stream *dirs = container_of(fs_dirs, struct btrfs_dir_stream, parent); struct btrfs_fs_info *fs_info = current_fs_info; struct fs_dirent *dent = &dirs->dirent; + struct btrfs_inode_item *ii; struct btrfs_root *root; + struct btrfs_path path; + struct btrfs_key location; struct btrfs_key key; u8 type; int ret; @@ -110,13 +113,32 @@ int btrfs_readdir(struct fs_dir_stream *fs_dirs, struct fs_dirent **dentp) memset(dent, 0, sizeof(*dent)); ret = btrfs_next_dir_entry(root, dirs->ino, &dirs->offset, dent->name, - sizeof(dent->name), &type); + sizeof(dent->name), &type, &location); if (ret < 0) return ret; if (ret > 0) return -ENOENT; dent->type = btrfs_dirent_type_to_fs_type(type); + + /* + * A subvolume entry points at a root item rather than an inode, and + * has no size of its own. Everything else carries one, and the fs + * layer prints it, so look it up. + */ + if (location.type == BTRFS_INODE_ITEM_KEY) { + btrfs_init_path(&path); + ret = btrfs_search_slot(NULL, root, &location, &path, 0, 0); + if (ret == 0) { + ii = btrfs_item_ptr(path.nodes[0], path.slots[0], + struct btrfs_inode_item); + dent->size = btrfs_inode_size(path.nodes[0], ii); + } + btrfs_release_path(&path); + if (ret < 0) + return ret; + } + *dentp = dent; return 0; } diff --git a/fs/btrfs/ctree.h b/fs/btrfs/ctree.h index 3fa9a8c9c02..cd3fd669f9a 100644 --- a/fs/btrfs/ctree.h +++ b/fs/btrfs/ctree.h @@ -1221,7 +1221,8 @@ struct btrfs_dir_item *btrfs_lookup_dir_item(struct btrfs_trans_handle *trans, const char *name, int name_len, int mod); int btrfs_next_dir_entry(struct btrfs_root *root, u64 ino, u64 *offset, - char *namebuf, int namebuf_len, u8 *ftype); + char *namebuf, int namebuf_len, u8 *ftype, + struct btrfs_key *location); /* inode.c */ int btrfs_lookup_path(struct btrfs_root *root, u64 ino, const char *filename, struct btrfs_root **root_ret, u64 *ino_ret, diff --git a/fs/btrfs/dir-item.c b/fs/btrfs/dir-item.c index c7b87d60d98..6edda34818b 100644 --- a/fs/btrfs/dir-item.c +++ b/fs/btrfs/dir-item.c @@ -126,12 +126,16 @@ struct btrfs_dir_item *btrfs_lookup_dir_item(struct btrfs_trans_handle *trans, * @namebuf: caller buffer that receives the NUL-terminated name * @namebuf_len: size of @namebuf in bytes * @ftype: receives the BTRFS_FT_* type of the entry + * @location: receives the key the entry points at, so the caller can + * reach the inode item without searching for the name + * again * * Return: 0 if an entry was returned, 1 when the directory is exhausted, * -ve on error. */ int btrfs_next_dir_entry(struct btrfs_root *root, u64 ino, u64 *offset, - char *namebuf, int namebuf_len, u8 *ftype) + char *namebuf, int namebuf_len, u8 *ftype, + struct btrfs_key *location) { struct btrfs_path path; struct btrfs_key key; @@ -180,6 +184,7 @@ int btrfs_next_dir_entry(struct btrfs_root *root, u64 ino, u64 *offset, (unsigned long)(di + 1), name_len); namebuf[name_len] = '\0'; *ftype = btrfs_dir_type(path.nodes[0], di); + btrfs_dir_item_key_to_cpu(path.nodes[0], di, location); ret = 0; out: From 03cc419206faddd01abd35867cec28c02ac6e2ab Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Sun, 2 Aug 2026 09:35:22 +0000 Subject: [PATCH 76/78] fs: btrfs: release the path when btrfs_search_slot() fails The U-Boot copy of btrfs_search_slot() returns on error with the nodes it has descended through still attached to the path. The kernel one releases the path on any error unless p->skip_release_on_error is set, and callers written against that convention treat a failed search as owning nothing. btrfs_size() is one: it returns straight away on a search error and never reaches its btrfs_release_path() call, so the attached extent buffer references leak. Route both error exits through a release of the path. The error returns of read_node_slot() carry no extra reference, so the path is the only thing to clean up. Suggested-by: Qu Wenruo Signed-off-by: Cole Munz --- fs/btrfs/ctree.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c index 8e932adc425..48c50e556b1 100644 --- a/fs/btrfs/ctree.c +++ b/fs/btrfs/ctree.c @@ -425,8 +425,10 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, level = btrfs_header_level(b); p->nodes[level] = b; ret = check_block(fs_info, p, level); - if (ret) - return -1; + if (ret) { + ret = -1; + goto err; + } ret = btrfs_bin_search(b, key, &slot); if (level != 0) { if (ret && slot > 0) @@ -461,8 +463,10 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, break; b = read_node_slot(fs_info, b, slot); - if (!extent_buffer_uptodate(b)) - return -EIO; + if (!extent_buffer_uptodate(b)) { + ret = -EIO; + goto err; + } } else { p->slots[level] = slot; /* @@ -479,6 +483,10 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, } } return 1; + +err: + btrfs_release_path(p); + return ret; } /* From a4e4b13ef17667dca75f7852c84f8c9cfd368558 Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Sun, 2 Aug 2026 09:35:26 +0000 Subject: [PATCH 77/78] fs: btrfs: deduplicate the inode size lookup btrfs_readdir() and btrfs_size() both open code the same search for an inode item to read its size field. Move it into one helper. Signed-off-by: Cole Munz --- fs/btrfs/btrfs.c | 75 +++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/fs/btrfs/btrfs.c b/fs/btrfs/btrfs.c index b2856be0662..f5f6d638ffd 100644 --- a/fs/btrfs/btrfs.c +++ b/fs/btrfs/btrfs.c @@ -88,14 +88,42 @@ static unsigned int btrfs_dirent_type_to_fs_type(u8 dirent_type) } } +/* + * Read the size stored in an inode item. A missing item is -ENOENT and + * leaves *size untouched. + */ +static int btrfs_get_inode_size(struct btrfs_root *root, u64 ino, u64 *size) +{ + struct btrfs_inode_item *ii; + struct btrfs_path path; + struct btrfs_key key; + int ret; + + key.objectid = ino; + key.type = BTRFS_INODE_ITEM_KEY; + key.offset = 0; + + btrfs_init_path(&path); + ret = btrfs_search_slot(NULL, root, &key, &path, 0, 0); + if (ret < 0) + return ret; + if (ret > 0) + ret = -ENOENT; + if (!ret) { + ii = btrfs_item_ptr(path.nodes[0], path.slots[0], + struct btrfs_inode_item); + *size = btrfs_inode_size(path.nodes[0], ii); + } + btrfs_release_path(&path); + return ret; +} + int btrfs_readdir(struct fs_dir_stream *fs_dirs, struct fs_dirent **dentp) { struct btrfs_dir_stream *dirs = container_of(fs_dirs, struct btrfs_dir_stream, parent); struct btrfs_fs_info *fs_info = current_fs_info; struct fs_dirent *dent = &dirs->dirent; - struct btrfs_inode_item *ii; struct btrfs_root *root; - struct btrfs_path path; struct btrfs_key location; struct btrfs_key key; u8 type; @@ -127,16 +155,13 @@ int btrfs_readdir(struct fs_dir_stream *fs_dirs, struct fs_dirent **dentp) * layer prints it, so look it up. */ if (location.type == BTRFS_INODE_ITEM_KEY) { - btrfs_init_path(&path); - ret = btrfs_search_slot(NULL, root, &location, &path, 0, 0); - if (ret == 0) { - ii = btrfs_item_ptr(path.nodes[0], path.slots[0], - struct btrfs_inode_item); - dent->size = btrfs_inode_size(path.nodes[0], ii); - } - btrfs_release_path(&path); - if (ret < 0) + u64 size; + + ret = btrfs_get_inode_size(root, location.objectid, &size); + if (ret < 0 && ret != -ENOENT) return ret; + if (!ret) + dent->size = size; } *dentp = dent; @@ -173,10 +198,8 @@ int btrfs_exists(const char *file) int btrfs_size(const char *file, loff_t *size) { struct btrfs_fs_info *fs_info = current_fs_info; - struct btrfs_inode_item *ii; struct btrfs_root *root; - struct btrfs_path path; - struct btrfs_key key; + u64 isize; u64 ino; u8 type; int ret; @@ -191,27 +214,13 @@ int btrfs_size(const char *file, loff_t *size) printf("Not a regular file: %s\n", file); return -ENOENT; } - btrfs_init_path(&path); - key.objectid = ino; - key.type = BTRFS_INODE_ITEM_KEY; - key.offset = 0; - - ret = btrfs_search_slot(NULL, root, &key, &path, 0, 0); - if (ret < 0) { - printf("Cannot lookup ino %llu\n", ino); + ret = btrfs_get_inode_size(root, ino, &isize); + if (ret) { + printf("Cannot read size of ino %llu\n", ino); return ret; } - if (ret > 0) { - printf("Ino %llu does not exist\n", ino); - ret = -ENOENT; - goto out; - } - ii = btrfs_item_ptr(path.nodes[0], path.slots[0], - struct btrfs_inode_item); - *size = btrfs_inode_size(path.nodes[0], ii); -out: - btrfs_release_path(&path); - return ret; + *size = isize; + return 0; } int btrfs_read(const char *file, void *buf, loff_t offset, loff_t len, From b297244d720da2956d77700e18df4a78fc24e0d3 Mon Sep 17 00:00:00 2001 From: lachytonner Date: Mon, 3 Aug 2026 20:01:56 +1200 Subject: [PATCH 78/78] lib: zstd: decompress every frame, not just the first zstd_decompress() probes the input with zstd_find_frame_compressed_size() and hands that single frame to zstd_decompress_dctx(). A zstd payload is allowed to be several frames concatenated, and when it is, everything after the first frame is silently dropped: the caller gets a short buffer and no error. On btrfs this shows up as truncated reads of zstd-compressed files. Both read paths funnel into this helper - decompress_zstd() for regular extents and btrfs_read_extent_inline() via btrfs_decompress() for inline ones - so a device-tree overlay stored on a compressed subvolume comes back short and "fdt apply" fails. Storing the file uncompressed is currently the only reliable workaround. Loop over the input instead, decoding frames until the output buffer is full or the input stops beginning with a frame header. Stopping on a non-frame header preserves the trailing-junk tolerance the size probe was added for, so a single frame followed by padding still decodes exactly as before, and input that never had a frame is still rejected. Add a regression test covering a two-frame payload. Without this change it decodes 166 of 350 bytes. Signed-off-by: lachytonner --- lib/zstd/zstd.c | 82 +++++++++++++++++++++++++++++++++--------- test/lib/compression.c | 49 +++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 16 deletions(-) diff --git a/lib/zstd/zstd.c b/lib/zstd/zstd.c index 14bde369068..77823162dd7 100644 --- a/lib/zstd/zstd.c +++ b/lib/zstd/zstd.c @@ -8,14 +8,40 @@ #include #include #include +#include #include #include +/** + * zstd_frame_starts_at() - does a zstd frame header begin here? + * + * @src: buffer to inspect + * @size: bytes available at @src + * Return: true if @src begins with a zstd or skippable frame magic + */ +static bool zstd_frame_starts_at(const void *src, size_t size) +{ + u32 magic; + + if (size < sizeof(magic)) + return false; + + magic = get_unaligned_le32(src); + + return magic == ZSTD_MAGICNUMBER || + (magic & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START; +} + int zstd_decompress(struct abuf *in, struct abuf *out) { - zstd_dctx *ctx; - size_t wsize, len; + const u8 *in_pos = abuf_data(in); + size_t in_left = abuf_size(in); + u8 *out_pos = abuf_data(out); + size_t out_left = abuf_size(out); + size_t wsize, total = 0; + unsigned int frames = 0; void *workspace; + zstd_dctx *ctx; int ret; wsize = zstd_dctx_workspace_bound(); @@ -34,27 +60,51 @@ int zstd_decompress(struct abuf *in, struct abuf *out) } /* - * Find out how large the frame actually is, there may be junk at - * the end of the frame that zstd_decompress_dctx() can't handle. + * The payload may be several frames concatenated - pzstd and multiple + * appended writes both produce those, and zstd_decompress_dctx() only + * ever decodes one. Keep decoding until the output fills up or the + * input stops looking like a frame; a non-frame tail is the junk the + * size probe below has always been here to tolerate. */ - len = zstd_find_frame_compressed_size(abuf_data(in), abuf_size(in)); - if (zstd_is_error(len)) { - log_err("%s: failed to detect compressed size: %d\n", __func__, - zstd_get_error_code(len)); - ret = -EINVAL; - goto do_free; + while (out_left && zstd_frame_starts_at(in_pos, in_left)) { + size_t csize, dsize; + + /* + * Find out how large the frame actually is, there may be junk at + * the end of the frame that zstd_decompress_dctx() can't handle. + */ + csize = zstd_find_frame_compressed_size(in_pos, in_left); + if (zstd_is_error(csize)) { + log_err("%s: failed to detect compressed size: %d\n", + __func__, zstd_get_error_code(csize)); + ret = -EINVAL; + goto do_free; + } + + dsize = zstd_decompress_dctx(ctx, out_pos, out_left, in_pos, + csize); + if (zstd_is_error(dsize)) { + log_err("%s: failed to decompress: %d\n", __func__, + zstd_get_error_code(dsize)); + ret = -EINVAL; + goto do_free; + } + + in_pos += csize; + in_left -= csize; + out_pos += dsize; + out_left -= dsize; + total += dsize; + frames++; } - len = zstd_decompress_dctx(ctx, abuf_data(out), abuf_size(out), - abuf_data(in), len); - if (zstd_is_error(len)) { - log_err("%s: failed to decompress: %d\n", __func__, - zstd_get_error_code(len)); + if (!frames) { + log_err("%s: no zstd frame found\n", __func__); ret = -EINVAL; goto do_free; } - ret = len; + ret = total; do_free: free(workspace); return ret; diff --git a/test/lib/compression.c b/test/lib/compression.c index 31b6e5b1eb4..042aa9488a7 100644 --- a/test/lib/compression.c +++ b/test/lib/compression.c @@ -138,6 +138,32 @@ static const char zstd_compressed[] = "\x01\xe4\xf4\x6e\xfa"; static const unsigned long zstd_compressed_size = sizeof(zstd_compressed) - 1; +/* + * The same text as two concatenated zstd frames, as produced by pzstd or by + * appending one compressed write to another: + * head -c 166 /tmp/plain.txt | zstd -19 -c > /tmp/multi.zst + * tail -c +167 /tmp/plain.txt | zstd -19 -c >> /tmp/multi.zst + * Decoding only the first frame silently truncates the output. + */ +static const char zstd_multiframe_compressed[] = + "\x28\xb5\x2f\xfd\x24\xa6\x5d\x02\x00\x62\x85\x10\x11\xa0\xed\x78" + "\xb8\x5e\xdd\x2c\x5a\xdd\xd2\x8d\xfa\xb7\xbc\xdf\x33\x23\x40\x70" + "\x6b\xf6\xd8\xe7\xe6\x1e\x3b\xc9\xcb\x59\x1e\x98\x9d\x55\xaf\xd7" + "\xb6\xec\x0c\xf7\x0c\x39\x11\x20\x24\x65\xf9\xe3\x2e\xbb\x9c\x57" + "\xd9\x18\x19\x3e\xe6\xcc\xeb\x4a\xa8\xd3\xf7\x6c\x3f\x02\x01\x00" + "\xe8\x85\xaa\x32\xf1\xa3\x63\x2c\x28\xb5\x2f\xfd\x24\xb8\x0d\x04" + "\x00\x92\x4a\x1d\x16\x90\x59\x07\xc0\x7e\x24\x98\x76\x1b\xca\x6f" + "\xc1\x8e\x3b\xf5\x2f\x9a\xf2\x28\xdf\x2f\x1d\x47\xd0\x23\xa9\x7a" + "\x76\x76\xf9\xfa\x70\x3e\x4a\x66\x41\xe7\x9e\xaf\x64\x7a\x61\xb7" + "\x56\x98\x7f\x2f\x9d\x90\x4a\xf1\x04\x4c\x46\xa6\xa5\x03\xd7\x2c" + "\xa7\x55\xd1\xab\xf9\xe5\x1d\x2c\x1c\xe5\x7a\x5e\x85\xea\xf0\xd3" + "\xba\x29\x9f\x19\x69\x4a\xf5\xf0\x31\x87\x4e\x53\xbe\xb6\xab\x13" + "\x4a\x98\x5f\x78\x81\x66\xac\xc4\x45\x1f\x3e\xad\x03\xf8\x85\x2e" + "\xa9\x99\x96\x3e\x7c\x34\xe1\x83\x08\x02\x00\x18\x1b\x65\x12\x15" + "\x41\x0a\x09\xd1\xe4\x57"; +static const unsigned long zstd_multiframe_compressed_size = + sizeof(zstd_multiframe_compressed) - 1; + #define TEST_BUFFER_SIZE 512 typedef int (*mutate_func)(struct unit_test_state *uts, void *, unsigned long, @@ -505,6 +531,29 @@ static int compression_test_zstd(struct unit_test_state *uts) } LIB_TEST(compression_test_zstd, 0); +/* + * A payload of several concatenated frames must decode in full, not just up to + * the end of the first frame. + */ +static int compression_test_zstd_multiframe(struct unit_test_state *uts) +{ + struct abuf in_buf, out_buf; + char out[TEST_BUFFER_SIZE]; + int ret; + + memset(out, 0, sizeof(out)); + abuf_init_set(&in_buf, (void *)zstd_multiframe_compressed, + zstd_multiframe_compressed_size); + abuf_init_set(&out_buf, out, sizeof(out)); + + ret = zstd_decompress(&in_buf, &out_buf); + ut_asserteq(strlen(plain), ret); + ut_asserteq_mem(plain, out, strlen(plain)); + + return 0; +} +LIB_TEST(compression_test_zstd_multiframe, 0); + static int compress_using_none(struct unit_test_state *uts, void *in, unsigned long in_size, void *out, unsigned long out_max,