mail archive of the barebox mailing list
 help / color / mirror / Atom feed
From: Ahmad Fatoum <a.fatoum@pengutronix.de>
To: barebox@lists.infradead.org
Cc: Ahmad Fatoum <a.fatoum@pengutronix.de>
Subject: [PATCH 04/10] common: add barebox TLV support
Date: Fri, 11 Apr 2025 09:40:39 +0200	[thread overview]
Message-ID: <20250411074045.2019372-5-a.fatoum@pengutronix.de> (raw)
In-Reply-To: <20250411074045.2019372-1-a.fatoum@pengutronix.de>

barebox TLV is a scheme for storing factory data on non-volatile
storage. Unlike state, it's meant to be read-only and if content
is limited to already specified tags, it can be extended later on,
without modifying the bootloader binary.

Signed-off-by: Ahmad Fatoum <a.fatoum@pengutronix.de>
---
 .../bindings/nvmem/barebox,tlv.yaml           |  58 +++++
 common/Kconfig                                |  24 ++
 common/Makefile                               |   1 +
 common/tlv/Makefile                           |   4 +
 common/tlv/barebox.c                          | 183 +++++++++++++++
 common/tlv/bus.c                              | 133 +++++++++++
 common/tlv/drv.c                              |  49 ++++
 common/tlv/parser.c                           | 211 ++++++++++++++++++
 common/tlv/register.c                         |  94 ++++++++
 include/string.h                              |   5 +
 include/tlv/format.h                          |  69 ++++++
 include/tlv/tlv.h                             |  99 ++++++++
 test/self/Kconfig                             |   7 +
 test/self/Makefile                            |   1 +
 test/self/tlv.c                               |  89 ++++++++
 test/self/tlv.dts                             |  27 +++
 16 files changed, 1054 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/nvmem/barebox,tlv.yaml
 create mode 100644 common/tlv/Makefile
 create mode 100644 common/tlv/barebox.c
 create mode 100644 common/tlv/bus.c
 create mode 100644 common/tlv/drv.c
 create mode 100644 common/tlv/parser.c
 create mode 100644 common/tlv/register.c
 create mode 100644 include/tlv/format.h
 create mode 100644 include/tlv/tlv.h
 create mode 100644 test/self/tlv.c
 create mode 100644 test/self/tlv.dts

diff --git a/Documentation/devicetree/bindings/nvmem/barebox,tlv.yaml b/Documentation/devicetree/bindings/nvmem/barebox,tlv.yaml
new file mode 100644
index 000000000000..b54ab600e309
--- /dev/null
+++ b/Documentation/devicetree/bindings/nvmem/barebox,tlv.yaml
@@ -0,0 +1,58 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://barebox.org/schemas/nvmem/barebox,tlv.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: barebox TLV factory data
+
+description: |
+  barebox TLV is a scheme for storing factory data on non-volatile
+  storage. Unlike state, it's meant to be read-only and if content
+  is limited to already specified tags, it can be extended later on,
+  without modifying the bootloader binary.
+
+  Variables can not yet be defined as NVMEM device subnodes.
+
+maintainers:
+  - Ahmad Fatoum <a.fatoum@pengutronix.de>
+
+properties:
+  compatible:
+    items:
+      - enum:
+        - barebox,tlv-v1        # magic: 0x61bb95f2
+      - const: barebox,tlv
+
+  reg:
+    maxItems: 1
+
+required:
+  - compatible
+  - label
+  - reg
+
+allOf:
+  - $ref: partition.yaml#
+
+additionalProperties: false
+
+examples:
+  - |
+    partitions {
+        compatible = "fixed-partitions";
+        #address-cells = <1>;
+        #size-cells = <1>;
+
+        partition@0 {
+            reg = <0x0 0x100000>;
+            label = "barebox";
+            read-only;
+        };
+
+        partition@100000 {
+            compatible = "barebox,tlv";
+            label = "tlv";
+            reg = <0x100000 0x10000>;
+        };
+    };
diff --git a/common/Kconfig b/common/Kconfig
index 2d2be0f7c4f6..ee2737c93eb9 100644
--- a/common/Kconfig
+++ b/common/Kconfig
@@ -1151,6 +1151,30 @@ config BTHREAD
 	  scheduled within delay loops and the console idle to asynchronously
 	  execute actions, like checking for link up or feeding a watchdog.
 
+config TLV
+	bool "barebox TLV support"
+	depends on OFDEVICE
+	help
+	  barebox TLV is a scheme for storing factory data on non-volatile
+	  storage. Unlike state, it's meant to be read-only.
+
+config TLV_DRV
+	bool "barebox TLV generic driver"
+	depends on TLV
+	default y
+	help
+	  barebox,tlv devices in the device tree will be matched against
+	  a compatible decoder via the 4-byte magic header.
+
+config TLV_BAREBOX
+	bool "barebox TLV common format"
+	depends on TLV
+	depends on PARAMETER
+	select PRINTF_HEXSTR
+	default y
+	help
+	  Decoder support for the common barebox TLV format.
+
 config STATE
 	bool "generic state infrastructure"
 	select CRC32
diff --git a/common/Makefile b/common/Makefile
index 9b67187561bf..3ff3d9ec98ba 100644
--- a/common/Makefile
+++ b/common/Makefile
@@ -67,6 +67,7 @@ obj-$(CONFIG_RESET_SOURCE)	+= reset_source.o
 obj-$(CONFIG_SHELL_HUSH)	+= hush.o
 obj-$(CONFIG_SHELL_SIMPLE)	+= parser.o
 obj-$(CONFIG_STATE)		+= state/
+obj-$(CONFIG_TLV)		+= tlv/
 obj-$(CONFIG_RATP)		+= ratp/
 obj-$(CONFIG_BOOTCHOOSER)	+= bootchooser.o
 obj-$(CONFIG_UIMAGE)		+= uimage_types.o uimage.o
diff --git a/common/tlv/Makefile b/common/tlv/Makefile
new file mode 100644
index 000000000000..ea982f229a08
--- /dev/null
+++ b/common/tlv/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-y += parser.o bus.o register.o drv.o
+obj-$(CONFIG_TLV_DRV) += drv.o barebox.o
+obj-$(CONFIG_TLV_BAREBOX) += barebox.o
diff --git a/common/tlv/barebox.c b/common/tlv/barebox.c
new file mode 100644
index 000000000000..a5febc3b12c3
--- /dev/null
+++ b/common/tlv/barebox.c
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <common.h>
+#include <net.h>
+#include <tlv/tlv.h>
+
+int tlv_handle_serial(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	const char *str;
+
+	str = __tlv_format_str(dev, map, len, val);
+	if (!str)
+		return -ENOMEM;
+
+	barebox_set_serial_number(str);
+	return 0;
+}
+
+int tlv_handle_eth_address(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	int i;
+
+	if (len % ETH_ALEN != 0)
+		return -EINVAL;
+
+	for (i = 0; i < len / ETH_ALEN; i++)
+		eth_register_ethaddr(i, val + i * ETH_ALEN);
+
+	return tlv_format_mac(dev, map, len, val);
+}
+
+int tlv_handle_eth_address_seq(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	u8 eth_addr[ETH_ALEN];
+	int eth_count;
+
+	eth_count = *val;
+
+	if (len != 1 + ETH_ALEN)
+		return -EINVAL;
+
+	memcpy(eth_addr, val + 1, ETH_ALEN);
+
+	for (int i = 0; i < eth_count; i++, eth_addr_inc(eth_addr)) {
+		eth_register_ethaddr(i, eth_addr);
+		tlv_format_mac(dev, map, ETH_ALEN, eth_addr);
+	}
+
+	return 0;
+}
+
+static const char *__tlv_format(struct tlv_device *dev, struct tlv_mapping *map, char *buf)
+{
+	struct param_d *param;
+	int ret;
+
+	if (!buf)
+		return NULL;
+
+	param = dev_add_param_fixed(&dev->dev, map->prop, NULL);
+	if (!IS_ERR(param))
+		param->value = buf; /* pass ownership */
+
+	ret = of_append_property(tlv_of_node(dev), map->prop, buf, strlen(buf) + 1);
+	if (ret)
+		return NULL;
+
+	return buf;
+}
+
+#define tlv_format(tlvdev, map, ...) ({ __tlv_format(tlvdev, map, basprintf(__VA_ARGS__)) ? 0 : -ENOMEM; })
+
+const char * __tlv_format_str(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	return __tlv_format(dev, map, basprintf("%.*s", len, val));
+}
+
+int tlv_format_str(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	return __tlv_format_str(dev, map, len, val) ? 0 : -ENOMEM;
+}
+
+int tlv_format_hex(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	return tlv_format(dev, map, "%*ph", len, val);
+}
+
+int tlv_format_blob(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	struct param_d *param;
+
+	param = dev_add_param_fixed(&dev->dev, map->prop, NULL);
+	if (!IS_ERR(param))
+		param->value = basprintf("%*phN", len, val);
+
+	return of_append_property(tlv_of_node(dev), map->prop, val, len);
+}
+
+static struct device_node *of_append_node(struct device_node *root, const char *name)
+{
+	struct device_node *np;
+
+	np = of_get_child_by_name(root, name);
+	if (np)
+		return np;
+
+	return of_new_node(root, name);
+}
+
+int tlv_format_mac(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	struct device_node *np = tlv_of_node(dev);
+	struct property *pp;
+	char propname[sizeof("address-4294967295")];
+	int base = 0, i, ret;
+
+	if (len % 6 != 0)
+		return -EINVAL;
+
+	np = of_append_node(np, map->prop);
+	if (!np)
+		return -ENOMEM;
+
+	for_each_property_of_node(np, pp)
+		base++;
+
+	for (i = base; i < base + len / 6; i++) {
+		snprintf(propname, sizeof(propname), "address-%u", i);
+		ret = of_property_sprintf(np, propname, "%*phC", 6, val);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+int tlv_format_dec(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val)
+{
+	switch (len) {
+	case 1:
+		return tlv_format(dev, map, "%u", *(u8 *)val);
+	case 2:
+		return tlv_format(dev, map, "%u", get_unaligned_be16(val));
+	case 4:
+		return tlv_format(dev, map, "%u", get_unaligned_be32(val));
+	case 8:
+		return tlv_format(dev, map, "%llu", get_unaligned_be64(val));
+	default:
+		return tlv_format_hex(dev, map, len, val);
+	}
+}
+
+struct tlv_mapping barebox_tlv_v1_mappings[] = {
+	{ 0x0002, tlv_format_str, "device-hardware-release" },
+	{ 0x0003, tlv_format_dec, "factory-timestamp" },
+	{ 0x0004, tlv_handle_serial, "device-serial-number"},
+	{ 0x0005, tlv_format_dec, "modification" },
+	{ 0x0006, tlv_format_str, "featureset" },
+	{ 0x0007, tlv_format_str, "pcba-serial-number"},
+	{ 0x0008, tlv_format_str, "pcba-hardware-release"},
+	{ 0x0011, tlv_handle_eth_address, "ethernet-address" },
+	{ 0x0012, tlv_handle_eth_address_seq, "ethernet-address" },
+	{ /* sentintel */ },
+};
+
+static struct tlv_mapping *mappings[] = { barebox_tlv_v1_mappings, NULL };
+static struct of_device_id of_matches[] = {
+	{ .compatible = "barebox,tlv-v1" },
+	{ /* sentinel */}
+};
+
+static struct tlv_decoder barebox_tlv_v1 = {
+	.magic = TLV_MAGIC_BAREBOX_V1,
+	.driver.name = "barebox-tlv-v1",
+	.driver.of_compatible = of_matches,
+	.mappings = mappings,
+};
+
+static int tlv_register_default(void)
+{
+	return tlv_register_decoder(&barebox_tlv_v1);
+}
+device_initcall(tlv_register_default);
diff --git a/common/tlv/bus.c b/common/tlv/bus.c
new file mode 100644
index 000000000000..366f67120ec2
--- /dev/null
+++ b/common/tlv/bus.c
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <common.h>
+#include <driver.h>
+#include <init.h>
+#include <tlv/tlv.h>
+#include <linux/err.h>
+#include <of.h>
+
+static void tlv_devinfo(struct device *dev)
+{
+	struct tlv_device *tlvdev = to_tlv_device(dev);
+
+	printf("Magic: %08x\n", tlvdev->magic);
+}
+
+struct tlv_device *tlv_register_device(struct tlv_header *header,
+				       struct device *parent)
+{
+	struct tlv_device *tlvdev;
+	const char *name = NULL;
+	char *buf = NULL;
+	struct device *dev;
+	static int id = 0;
+
+	tlvdev = xzalloc(sizeof(*tlvdev));
+
+	dev = &tlvdev->dev;
+
+	dev->bus = &tlv_bus;
+	devinfo_add(dev, tlv_devinfo);
+	dev->platform_data = header;
+	tlvdev->magic = be32_to_cpu(header->magic);
+	dev->parent = parent ?: tlv_bus.dev;
+	dev->id = DEVICE_ID_SINGLE;
+
+	if (parent)
+		name = of_alias_get(parent->device_node);
+	if (!name)
+		name = buf = basprintf("tlv%u", id++);
+
+	dev->device_node = of_new_node(of_new_node(NULL, NULL), name);
+	dev->device_node->dev = dev;
+	dev_set_name(dev, name);
+	register_device(dev);
+
+	free(buf);
+	return tlvdev;
+}
+
+void tlv_free_device(struct tlv_device *tlvdev)
+{
+	tlv_of_unregister_fixup(tlvdev);
+
+	devinfo_del(&tlvdev->dev, tlv_devinfo);
+	unregister_device(&tlvdev->dev);
+
+	free(tlvdev->dev.platform_data);
+	of_delete_node(tlvdev->dev.device_node);
+
+	free(tlvdev);
+}
+
+static int tlv_bus_match(struct device *dev, struct driver *drv)
+{
+	struct tlv_decoder *decoder = to_tlv_decoder(drv);
+	struct tlv_device *tlvdev = to_tlv_device(dev);
+
+	return decoder->magic == tlvdev->magic ? 0 : -1;
+}
+
+static int tlv_bus_probe(struct device *dev)
+{
+	return dev->driver->probe(dev);
+}
+
+static void tlv_bus_remove(struct device *dev)
+{
+	if (dev->driver->remove)
+		dev->driver->remove(dev);
+}
+
+struct bus_type tlv_bus = {
+	.name = "barebox-tlv",
+	.match = tlv_bus_match,
+	.probe = tlv_bus_probe,
+	.remove = tlv_bus_remove,
+};
+
+struct tlv_device *tlv_ensure_probed_by_alias(const char *alias)
+{
+	struct device_node *np;
+	struct device *dev;
+
+	np = of_find_node_by_alias(NULL, alias);
+	if (!np)
+		return ERR_PTR(-EINVAL);
+
+	of_partition_ensure_probed(np);
+
+	bus_for_each_device(&tlv_bus, dev) {
+		if (dev->parent && dev->parent->device_node == np)
+			return to_tlv_device(dev);
+	}
+
+	return ERR_PTR(-EPROBE_DEFER);
+}
+
+static void tlv_bus_info(struct device *dev)
+{
+	struct driver *drv;
+
+	puts("Registered Magic:\n");
+	bus_for_each_driver(&tlv_bus, drv) {
+		struct tlv_decoder *tlvdrv = to_tlv_decoder(drv);
+
+		printf("  %08x (%s)\n", tlvdrv->magic, tlvdrv->driver.name);
+	}
+}
+
+static int tlv_bus_register(void)
+{
+	int ret;
+
+	ret = bus_register(&tlv_bus);
+	if (ret)
+		return ret;
+
+	devinfo_add(tlv_bus.dev, tlv_bus_info);
+
+	return 0;
+}
+postcore_initcall(tlv_bus_register);
diff --git a/common/tlv/drv.c b/common/tlv/drv.c
new file mode 100644
index 000000000000..baba30808683
--- /dev/null
+++ b/common/tlv/drv.c
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <driver.h>
+#include <tlv/tlv.h>
+#include <init.h>
+#include <of.h>
+#include <of_device.h>
+#include <stdio.h>
+
+static int barebox_tlv_probe(struct device *dev)
+{
+	const struct of_device_id *match;
+	struct tlv_device *tlvdev;
+	struct cdev *cdev;
+	char *backend_path;
+
+	match = of_match_device(dev->driver->of_match_table, dev);
+	/*
+	 * We only match with the device if this driver is the most specific match
+	 * because we don't want to incorrectly bind to a device that has a more
+	 * specific driver.
+	 */
+	if (match && of_property_match_string(dev->of_node, "compatible",
+					      match->compatible) != 0)
+		return -ENODEV;
+
+	cdev = cdev_by_device_node(dev->of_node);
+	if (!cdev)
+		return -EINVAL;
+
+	backend_path = basprintf("/dev/%s", cdev_name(cdev));
+
+	tlvdev = tlv_register_device_by_path(backend_path, dev);
+	free(backend_path);
+
+	return PTR_ERR_OR_ZERO(tlvdev);
+}
+
+static const __maybe_unused struct of_device_id tlv_ids[] = {
+	{ .compatible = "barebox,tlv" },
+	{ /* sentinel */ }
+};
+
+static struct driver barebox_tlv_driver = {
+	.name = "tlv",
+	.probe = barebox_tlv_probe,
+	.of_compatible = tlv_ids,
+};
+core_platform_driver(barebox_tlv_driver);
diff --git a/common/tlv/parser.c b/common/tlv/parser.c
new file mode 100644
index 000000000000..468eeafceda1
--- /dev/null
+++ b/common/tlv/parser.c
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#define pr_fmt(fmt) "barebox-tlv: " fmt
+
+#include <common.h>
+#include <tlv/tlv.h>
+#include <fcntl.h>
+#include <libfile.h>
+#include <linux/stat.h>
+#include <crc.h>
+#include <net.h>
+
+int tlv_parse(struct tlv_device *tlvdev,
+	      const struct tlv_decoder *decoder)
+{
+	const struct tlv *tlv = NULL;
+	struct tlv_mapping *map = NULL;
+	struct tlv_header *header = tlv_device_header(tlvdev);
+	u32 magic, size;
+	int ret = 0;
+	u32 crc = ~0;
+
+
+	magic = be32_to_cpu(header->magic);
+
+	size = tlv_total_len(header);
+
+	crc = crc32_be(crc, header, size - 4);
+	if (crc != tlv_crc(header)) {
+		pr_warn("Invalid CRC32. Should be %08x\n", crc);
+		return -EILSEQ;
+	}
+
+	for_each_tlv(header, tlv) {
+		struct tlv_mapping **mappings;
+		u16 tag = TLV_TAG(tlv);
+		int len = TLV_LEN(tlv);
+		const void *val = TLV_VAL(tlv);
+
+		pr_debug("[%04x] %*ph\n", tag, len, val);
+
+		for (mappings = decoder->mappings; *mappings; mappings++) {
+			for (map = *mappings; map->tag; map++) {
+				if (map->tag == tag)
+					goto done;
+			}
+		}
+
+done:
+		if (!map || !map->tag) {
+			if (tag)
+				pr_warn("skipping unknown tag: %04x\n", tag);
+			continue;
+		}
+
+		ret = map->handle(tlvdev, map, len, val);
+		if (ret < 0)
+			return ret;
+	}
+
+	return PTR_ERR_OR_ZERO(tlv);
+}
+
+struct tlv_device *tlv_register_device_by_path(const char *path, struct device *parent)
+{
+	struct tlv_header *header;
+	struct tlv_device *tlvdev;
+	size_t size;
+
+	header = tlv_read(path, &size);
+	if (IS_ERR(header))
+		return ERR_CAST(header);
+
+	tlvdev = tlv_register_device(header, parent);
+	if (IS_ERR(tlvdev))
+		free(header);
+
+	return tlvdev;
+}
+
+int of_tlv_fixup(struct device_node *root, void *ctx)
+{
+	struct device_node *chosen, *conf, *ethaddrs;
+	struct eth_ethaddr *addr;
+
+	chosen = of_create_node(root, "/chosen");
+	if (!chosen)
+		return -ENOMEM;
+
+	conf = of_copy_node(chosen, ctx);
+
+	ethaddrs = of_get_child_by_name(conf, "ethernet-address");
+	if (!ethaddrs)
+		return 0;
+
+	list_for_each_entry(addr, &ethaddr_list, list) {
+		char propname[sizeof("address-4294967295")];
+		const u8 *enetaddr_a;
+		u8 enetaddr_b[ETH_ALEN];
+		struct property *pp;
+
+		if (!eth_of_get_fixup_node(root, NULL, addr->ethid))
+			continue;
+
+		snprintf(propname, sizeof(propname), "address-%u", addr->ethid);
+		pp = of_find_property(ethaddrs, propname, NULL);
+		if (!pp)
+			continue;
+
+		enetaddr_a = of_property_get_value(pp);
+		if (string_to_ethaddr(addr->ethaddr, enetaddr_b))
+			continue;
+
+		if (memcmp(enetaddr_a, enetaddr_b, ETH_ALEN))
+			continue;
+
+		of_delete_property(pp);
+	}
+
+	return 0;
+}
+
+int tlv_of_register_fixup(struct tlv_device *tlvdev)
+{
+	return of_register_fixup(of_tlv_fixup, tlv_of_node(tlvdev));
+}
+
+void tlv_of_unregister_fixup(struct tlv_device *tlvdev)
+{
+	of_unregister_fixup(of_tlv_fixup, tlv_of_node(tlvdev));
+}
+
+struct tlv_header *tlv_read(const char *filename, size_t *nread)
+{
+	struct tlv_header *header = NULL, *tmpheader;
+	int size, fd, ret;
+
+	fd = open(filename, O_RDONLY);
+	if (fd < 0)
+		return ERR_PTR(fd);
+
+	header = malloc(128);
+	if (!header) {
+		ret = -ENOMEM;
+		goto err;
+	}
+
+	ret = read_full(fd, header, sizeof(*header));
+	if (ret >= 0 && ret != sizeof(*header))
+		ret = -ENODATA;
+	if (ret < 0)
+		goto err;
+
+	size = tlv_total_len(header);
+
+	tmpheader = realloc(header, size);
+	if (!tmpheader) {
+		struct stat st;
+
+		ret = fstat(fd, &st);
+		if (ret)
+			ret = -EIO;
+		else if (size > st.st_size)
+			ret = -ENODATA;
+		else
+			ret = -ENOMEM;
+		goto err;
+	}
+	header = tmpheader;
+
+	ret = read_full(fd, header->tlvs, size - sizeof(*header));
+	if (ret < 0)
+		goto err;
+
+	/* file might have been truncated, but this will be handled
+	 * in tlv_parse
+	 */
+
+	if (nread)
+		*nread = sizeof(*header) + ret;
+
+	close(fd);
+	return header;
+err:
+	free(header);
+	close(fd);
+	return ERR_PTR(ret);
+}
+
+static struct tlv *__tlv_next(const struct tlv *tlv)
+{
+	return (void *)tlv + 4 + TLV_LEN(tlv);
+}
+
+struct tlv *tlv_next(const struct tlv_header *header,
+			     const struct tlv *tlv)
+{
+	void *tlvs_start = (void *)&header->tlvs[0], *tlvs_end, *next_tlv;
+
+	tlv = tlv ? __tlv_next(tlv) : tlvs_start;
+
+	tlvs_end = tlvs_start + get_unaligned_be32(&header->length_tlv);
+	if (tlv == tlvs_end)
+		return NULL;
+
+	next_tlv = __tlv_next(tlv);
+	if (next_tlv > tlvs_end)
+		return ERR_PTR(-ENODATA);
+
+	return (void *)tlv;
+}
diff --git a/common/tlv/register.c b/common/tlv/register.c
new file mode 100644
index 000000000000..a6d95fb8e091
--- /dev/null
+++ b/common/tlv/register.c
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2022 Ahmad Fatoum <a.fatoum@pengutronix.de>
+ */
+
+#include <common.h>
+#include <driver.h>
+#include <linux/nvmem-consumer.h>
+#include <of.h>
+#include <tlv/tlv.h>
+#include <libfile.h>
+#include <fcntl.h>
+
+#include <linux/err.h>
+
+static int tlv_probe_from_magic(struct device *dev)
+{
+	struct tlv_device *tlvdev = to_tlv_device(dev);
+	int ret;
+
+	ret = tlv_parse(tlvdev, to_tlv_decoder(dev->driver));
+	if (ret)
+		return ret;
+
+	return tlv_of_register_fixup(tlvdev);
+}
+
+static int tlv_probe_from_compatible(struct device *dev)
+{
+	struct tlv_decoder *decoder = to_tlv_decoder(dev->driver);
+	struct tlv_header *header;
+	struct tlv_device *tlvdev;
+	struct cdev *cdev;
+	char *backend_path;
+	size_t size;
+	u32 magic;
+	int ret;
+
+	cdev = cdev_by_device_node(dev->of_node);
+	if (!cdev)
+		return -EINVAL;
+
+	backend_path = basprintf("/dev/%s", cdev_name(cdev));
+
+	header = tlv_read(backend_path, &size);
+	free(backend_path);
+
+	if (IS_ERR(header))
+		return PTR_ERR(header);
+
+	magic = be32_to_cpu(header->magic);
+	if (magic != decoder->magic) {
+		dev_err(dev, "got magic %08x, but %08x expected\n",
+			magic, decoder->magic);
+		ret = -EILSEQ;
+		goto err;
+	}
+
+	tlvdev = tlv_register_device(header, dev);
+	if (IS_ERR(tlvdev)) {
+		ret = PTR_ERR(tlvdev);
+		goto err;
+	}
+
+	return 0;
+err:
+	free(header);
+	return ret;
+}
+
+int tlv_register_decoder(struct tlv_decoder *decoder)
+{
+	int ret;
+
+	if (decoder->driver.bus)
+		return -EBUSY;
+
+	if (!decoder->driver.probe)
+		decoder->driver.probe = tlv_probe_from_magic;
+	decoder->driver.bus = &tlv_bus;
+
+	ret = register_driver(&decoder->driver);
+	if (ret)
+		return ret;
+
+	if (!decoder->driver.of_compatible)
+		return 0;
+
+	decoder->_platform_driver.name = basprintf("%s-pltfm", decoder->driver.name);
+	decoder->_platform_driver.of_compatible = decoder->driver.of_compatible;
+	decoder->_platform_driver.probe = tlv_probe_from_compatible;
+
+	return platform_driver_register(&decoder->_platform_driver);
+}
diff --git a/include/string.h b/include/string.h
index 986ccd83dd73..c16529c21273 100644
--- a/include/string.h
+++ b/include/string.h
@@ -45,4 +45,9 @@ static inline const char *nonempty(const char *s)
 	return isempty(s) ? NULL : s;
 }
 
+static inline bool is_nul_terminated(const char *val, size_t len)
+{
+	return strnlen(val, len) != len;
+}
+
 #endif /* __STRING_H */
diff --git a/include/tlv/format.h b/include/tlv/format.h
new file mode 100644
index 000000000000..a32ec917a434
--- /dev/null
+++ b/include/tlv/format.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later OR MIT
+ *
+ * barebox TLVs are preceded by a 12 byte header: 4 bytes for magic,
+ * 4 bytes for TLV sequence length (in bytes) and 4 bytes for
+ * the length of the signature. Each tag consists of at least four
+ * bytes: 2 bytes for the tag and two bytes for the length (in bytes)
+ * and as many bytes as the length. The TLV sequence must be equal
+ * to the TLV sequence length in the header. It can be followed by a
+ * signature of the length described in the header. At the end
+ * is a (big-endian) CRC-32/MPEG-2 of the whole structure. All
+ * integers are in big-endian. Tags and magic have their MSB set
+ * if they are vendor-specific. The second MSB is 0 for tag
+ * and 1 for magic.
+ */
+
+#ifndef __TLV_FORMAT_H_
+#define __TLV_FORMAT_H_
+
+#include <linux/compiler.h>
+#include <linux/types.h>
+#include <asm/unaligned.h>
+#include <linux/build_bug.h>
+
+#define TLV_MAGIC_BAREBOX_V1		0x61bb95f2
+
+#define TLV_IS_VENDOR_SPECIFIC(val)	((*(u8 *)&(val) & 0x80) == 0x80)
+#define TLV_IS_GENERIC(val)		((*(u8 *)&(val) & 0x80) != 0x80)
+
+struct tlv {
+	/*
+	 * _tag:15 (MSB): product specific
+	 */
+	__be16 _tag;
+	__be16 _len; /* in bytes */
+	u8 _payload[];
+} __packed;
+
+
+#define TLV_TAG(tlv) get_unaligned_be16(&(tlv)->_tag)
+#define TLV_LEN(tlv) get_unaligned_be16(&(tlv)->_len)
+#define TLV_VAL(tlv) ((tlv)->_payload)
+
+struct tlv_header {
+	__be32 magic;
+	__be32 length_tlv; /* in bytes */
+	__be32 length_sig; /* in bytes */
+	struct tlv tlvs[];
+};
+static_assert(sizeof(struct tlv_header) == 3 * 4);
+
+#define for_each_tlv(tlv_head, tlv) \
+	for (tlv = tlv_next(tlv_head, NULL); !IS_ERR_OR_NULL(tlv); tlv = tlv_next(tlv_head, tlv))
+
+static inline size_t tlv_total_len(const struct tlv_header *header)
+{
+	return sizeof(struct tlv_header) + get_unaligned_be32(&header->length_tlv)
+		+ get_unaligned_be32(&header->length_sig) + 4;
+}
+
+/*
+ * Retrieves the CRC-32/MPEG-2 CRC32 at the end of a TLV blob. Parameters:
+ * Poly=0x04C11DB7, Init=0xFFFFFFFF, RefIn=RefOut=false, XorOut=0x00000000
+ */
+static inline u32 tlv_crc(const struct tlv_header *header)
+{
+	return get_unaligned_be32((void *)header + tlv_total_len(header) - sizeof(__be32));
+}
+
+#endif
diff --git a/include/tlv/tlv.h b/include/tlv/tlv.h
new file mode 100644
index 000000000000..b1635fdbf500
--- /dev/null
+++ b/include/tlv/tlv.h
@@ -0,0 +1,99 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __TLV_H_
+#define __TLV_H_
+
+#include <linux/compiler.h>
+#include <linux/kernel.h>
+#include <linux/types.h>
+#include <linux/err.h>
+#include <asm/unaligned.h>
+#include <unistd.h>
+#include <driver.h>
+
+#include <tlv/format.h>
+
+struct tlv *tlv_next(const struct tlv_header *header, const struct tlv *tlv);
+
+struct tlv_header *tlv_read(const char *filename, size_t *nread);
+
+struct device_node;
+struct tlv_device;
+struct tlv_mapping;
+
+struct tlv_mapping {
+	u16 tag;
+	int (*handle)(struct tlv_device *dev, struct tlv_mapping *map,
+		      u16 len, const u8 *val);
+	const char *prop;
+};
+
+extern struct tlv_mapping barebox_tlv_v1_mappings[];
+
+extern const char *__tlv_format_str(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+extern int tlv_format_str(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+extern int tlv_format_dec(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+extern int tlv_format_hex(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+extern int tlv_format_mac(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+extern int tlv_format_blob(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+extern int tlv_handle_serial(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+extern int tlv_handle_eth_address(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+extern int tlv_handle_eth_address_seq(struct tlv_device *dev, struct tlv_mapping *map, u16 len, const u8 *val);
+
+struct tlv_decoder {
+	u32 magic;
+	void *driverata;
+	struct tlv_mapping **mappings;
+	struct driver driver;
+	/* private members */
+	struct driver _platform_driver;
+	struct list_head list;
+};
+
+struct tlv_device {
+	struct device dev;
+	u32 magic;
+};
+
+static inline struct device_node *tlv_of_node(struct tlv_device *tlvdev)
+{
+	return tlvdev->dev.device_node;
+}
+
+struct tlv_device *tlv_register_device(struct tlv_header *header, struct device *parent);
+static inline struct tlv_header *tlv_device_header(struct tlv_device *tlvdev)
+{
+	return tlvdev->dev.platform_data;
+}
+
+void tlv_free_device(struct tlv_device *tlvdev);
+
+int tlv_register_decoder(struct tlv_decoder *decoder);
+
+int tlv_parse(struct tlv_device *tlvdev,
+	      const struct tlv_decoder *decoder);
+
+int of_tlv_fixup(struct device_node *root, void *ctx);
+
+int tlv_of_register_fixup(struct tlv_device *tlvdev);
+void tlv_of_unregister_fixup(struct tlv_device *tlvdev);
+
+extern struct bus_type tlv_bus;
+
+static inline struct tlv_decoder *to_tlv_decoder(struct driver *drv)
+{
+	if (drv->bus == &tlv_bus)
+		return container_of(drv, struct tlv_decoder, driver);
+	if (drv->bus == &platform_bus)
+		return container_of(drv, struct tlv_decoder, _platform_driver);
+	return NULL;
+}
+
+static inline struct tlv_device *to_tlv_device(struct device *dev)
+{
+	return container_of(dev, struct tlv_device, dev);
+}
+
+struct tlv_device *tlv_register_device_by_path(const char *path, struct device *parent);
+struct tlv_device *tlv_ensure_probed_by_alias(const char *alias);
+
+#endif
diff --git a/test/self/Kconfig b/test/self/Kconfig
index 33e478aee882..33d05e4cf205 100644
--- a/test/self/Kconfig
+++ b/test/self/Kconfig
@@ -45,6 +45,7 @@ config SELFTEST_ENABLE_ALL
 	select SELFTEST_REGULATOR if REGULATOR_FIXED
 	select SELFTEST_TEST_COMMAND if CMD_TEST
 	select SELFTEST_IDR
+	select SELFTEST_TLV
 	help
 	  Selects all self-tests compatible with current configuration
 
@@ -117,4 +118,10 @@ config SELFTEST_IDR
 	bool "idr selftest"
 	select IDR
 
+config SELFTEST_TLV
+	bool "TLV selftest"
+	select TLV
+	select BASE64
+	select BOARD_LXA
+
 endif
diff --git a/test/self/Makefile b/test/self/Makefile
index 666a8f9ee7e2..8cd2a4c526c8 100644
--- a/test/self/Makefile
+++ b/test/self/Makefile
@@ -18,6 +18,7 @@ obj-$(CONFIG_SELFTEST_SETJMP) += setjmp.o
 obj-$(CONFIG_SELFTEST_REGULATOR) += regulator.o test_regulator.dtbo.o
 obj-$(CONFIG_SELFTEST_TEST_COMMAND) += test_command.o
 obj-$(CONFIG_SELFTEST_IDR) += idr.o
+obj-$(CONFIG_SELFTEST_TLV) += tlv.o tlv.dtb.o
 
 ifdef REGENERATE_KEYTOC
 
diff --git a/test/self/tlv.c b/test/self/tlv.c
new file mode 100644
index 000000000000..c22335f264c5
--- /dev/null
+++ b/test/self/tlv.c
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <common.h>
+#include <crypto/rsa.h>
+#include <bselftest.h>
+#include <base64.h>
+#include <console.h>
+#include <tlv/tlv.h>
+
+BSELFTEST_GLOBALS();
+
+static const char cpu_tlv_encoded[] =
+"vCiN/gAAAH4AAAAAAAIAGGd1dGVmZWUyLUQwMS1SMDEtVjAxLUM/PwADAAgAAAAAZ+"
+"Z4XgAEAAswMDA0MC4wMDAxMAAFAAEAAAYACGJhc2UsUG9FAAcACzAwMDM2LjAwMDEwA"
+"AgAGGd1dGVmZWUyLVMwMS1SMDEtVjAxLUM/PwASAAcEGHTioAPA6zhwRw==";
+
+static const char io_tlv_encoded[] =
+"3KWocAAAAEQAAAAAAAMACAAAAABn5nheAAUAAQAABgAEYmFzZQAHAAswMDAzNy4wMDA"
+"xMAAIABhndXRlZmVlMi1TMDItUjAxLVYwMS1DPz+xJ7bM";
+
+static u8 *base64_decode_alloc(const char *encoded, size_t *len)
+{
+	size_t enclen = strlen(encoded);
+	u8 *buf = xmalloc(enclen);
+	*len = decode_base64(buf, enclen, encoded);
+	return buf;
+}
+
+static void assert_equal(struct device_node *a, struct device_node *b)
+{
+	int ret;
+
+	ret = of_diff(a, b, -1);
+	if (ret == 0)
+		return;
+
+	pr_warn("comparison of %s and %s failed: no differences expected, %u found.\n",
+		a->full_name, b->full_name, ret);
+	of_diff(a, b, 1);
+	failed_tests++;
+}
+
+static void test_lxa_tlv(void)
+{
+	extern char __dtb_tlv_start[], __dtb_tlv_end[];
+	struct tlv_device *cpu_tlvdev, *io_tlvdev;
+	size_t cpu_bloblen, io_bloblen;
+	void *cpu_blob = base64_decode_alloc(cpu_tlv_encoded, &cpu_bloblen);
+	void *io_blob = base64_decode_alloc(io_tlv_encoded, &io_bloblen);
+	struct device_node *expected, *np;
+
+	total_tests = 4;
+
+	expected = of_unflatten_dtb(__dtb_tlv_start,
+				    __dtb_tlv_end - __dtb_tlv_start);
+	if (WARN_ON(IS_ERR(expected))) {
+		skipped_tests = total_tests;
+		return;
+	}
+
+	cpu_tlvdev = tlv_register_device(cpu_blob, NULL);
+	if (IS_ERR(cpu_tlvdev)) {
+		free(cpu_blob);
+		failed_tests++;
+		skipped_tests++;
+	}
+
+	io_tlvdev = tlv_register_device(io_blob, NULL);
+	if (IS_ERR(io_tlvdev)) {
+		free(io_blob);
+		failed_tests++;
+		skipped_tests++;
+	}
+
+	for_each_child_of_node(expected, np) {
+		if (!IS_ERR(cpu_tlvdev) && !strcmp(np->full_name, "/tlv0"))
+			assert_equal(tlv_of_node(cpu_tlvdev), np);
+		if (!IS_ERR(io_tlvdev) && !strcmp(np->full_name, "/tlv1"))
+			assert_equal(tlv_of_node(io_tlvdev), np);
+	}
+
+	if (!IS_ERR(cpu_tlvdev))
+		tlv_free_device(cpu_tlvdev);
+	if (!IS_ERR(io_tlvdev))
+		tlv_free_device(io_tlvdev);
+}
+bselftest(parser, test_lxa_tlv);
diff --git a/test/self/tlv.dts b/test/self/tlv.dts
new file mode 100644
index 000000000000..40906279bafd
--- /dev/null
+++ b/test/self/tlv.dts
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/dts-v1/;
+/ {
+	tlv0 {
+		device-hardware-release = "gutefee2-D01-R01-V01-C??";
+		factory-timestamp = "1743157342";
+		serial-number = "00040.00010";
+		modification = "0";
+		featureset = "base,PoE";
+		pcba-serial-number = "00036.00010";
+		pcba-hardware-release = "gutefee2-S01-R01-V01-C??";
+		ethernet-address {
+			address-0 = "18:74:e2:a0:03:c0";
+			address-1 = "18:74:e2:a0:03:c1";
+			address-2 = "18:74:e2:a0:03:c2";
+			address-3 = "18:74:e2:a0:03:c3";
+		};
+	};
+
+	tlv1 {
+		factory-timestamp = "1743157342";
+		modification = "0";
+		featureset = "base";
+		pcba-serial-number = "00037.00010";
+		pcba-hardware-release = "gutefee2-S02-R01-V01-C??";
+	};
+};
-- 
2.39.5




  parent reply	other threads:[~2025-04-11  7:44 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-04-11  7:40 [PATCH 00/10] Add barebox TLV infrastructure Ahmad Fatoum
2025-04-11  7:40 ` [PATCH 01/10] net: factor out eth_of_get_fixup_node Ahmad Fatoum
2025-04-11  7:40 ` [PATCH 02/10] net: export list of registered ethernet addresses Ahmad Fatoum
2025-04-11  7:40 ` [PATCH 03/10] common: add optional systemd.hostname generation Ahmad Fatoum
2025-04-11  7:40 ` Ahmad Fatoum [this message]
2025-04-14 14:49   ` [PATCH 04/10] common: add barebox TLV support Sascha Hauer
2025-04-14 14:57     ` Ahmad Fatoum
2025-04-14 15:06       ` Sascha Hauer
2025-04-11  7:40 ` [PATCH 05/10] commands: add TLV debugging command Ahmad Fatoum
2025-04-11  7:40 ` [PATCH 06/10] scripts: add bareboxtlv host/target tool Ahmad Fatoum
2025-04-11  7:40 ` [PATCH 07/10] boards: add decoder for LXA TLV v1 format Ahmad Fatoum
2025-04-11  7:40 ` [PATCH 08/10] scripts: Add Barebox TLV Generator Tooling Ahmad Fatoum
2025-04-14 15:00   ` Sascha Hauer
2025-04-11  7:40 ` [PATCH 09/10] doc: Add User-Documentation for Barebox TLV Ahmad Fatoum
2025-04-11  7:40 ` [PATCH 10/10] ARM: stm32mp: lxa: enable TLV support for TAC & FairyTux2 Ahmad Fatoum

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250411074045.2019372-5-a.fatoum@pengutronix.de \
    --to=a.fatoum@pengutronix.de \
    --cc=barebox@lists.infradead.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox