summaryrefslogtreecommitdiffstats
path: root/drivers/nvmem/microchip-otpc.c
diff options
context:
space:
mode:
authorAdrian Betschart <adrian.betschart@cinemaone.ch>2026-09-11 17:13:14 +0200
committerAlex Deucher <alexander.deucher@amd.com>2026-10-01 17:18:05 -0400
commit106b4a2a78b2a2f14885604891957b84bba502db (patch)
tree5d40eef2428fb1a4791ab46a4b19091a5910eac2 /drivers/nvmem/microchip-otpc.c
downloadlinux-stable-106b4a2a78b2a2f14885604891957b84bba502db.tar.gz
linux-stable-106b4a2a78b2a2f14885604891957b84bba502db.zip
drm/amd/display: Try RGB before YCbCr 4:4:4 in stream validationgrafted
amdgpu_dm_create_validate_stream_for_sink() walks encoding_order[] and uses the first encoding that validates. YCbCr 4:4:4 is listed before RGB, so an HDMI sink that advertises 4:4:4 gets YCbCr 4:4:4 whenever the "color format" property is left at AUTO, even though RGB fits the same link. That contradicts the documented AUTO behaviour for HDMI in enum drm_connector_color_format (RGB, falling back to YCbCr 4:2:0 only when the bandwidth is not available or the mode is 4:2:0-only), which the amdgpu implementation of the property also describes. It also leaves the "Broadcast RGB" property without effect on such sinks, since the quantization range it selects only applies to RGB output. Try RGB first. The mask still holds every encoding the sink supports, so a mode that cannot carry RGB falls back exactly as before. For reference, v7.2 picked RGB here unless YCbCr 4:4:4 was forced through debugfs, while earlier kernels picked YCbCr 4:4:4 for any HDMI sink that advertised it. Fixes: 0b0ff65d3ca1 ("drm/amd/display: Refactor stream validation") Suggested-by: Adolfo Rodrigues <adolfotregosa@gmail.com> Assisted-by: Claude Code:claude-fable-5-1 Signed-off-by: Adrian Betschart <adrian.betschart@cinemaone.ch> Reviewed-by: Fangzhi Zuo <jerry.zuo@amd.com> Tested-by: Adolfo Rodrigues <adolfotregosa@gmail.com> Tested-by: Daniel Wheeler <daniel.wheeler@amd.com> Signed-off-by: Alex Deucher <alexander.deucher@amd.com> (cherry picked from commit 7de432bc753133764dc40d37f9388da56de251e7)
Diffstat (limited to 'drivers/nvmem/microchip-otpc.c')
-rw-r--r--drivers/nvmem/microchip-otpc.c289
1 files changed, 289 insertions, 0 deletions
diff --git a/drivers/nvmem/microchip-otpc.c b/drivers/nvmem/microchip-otpc.c
new file mode 100644
index 000000000..df979e854
--- /dev/null
+++ b/drivers/nvmem/microchip-otpc.c
@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * OTP Memory controller
+ *
+ * Copyright (C) 2022 Microchip Technology Inc. and its subsidiaries
+ *
+ * Author: Claudiu Beznea <claudiu.beznea@microchip.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/nvmem-provider.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+
+#define MCHP_OTPC_CR (0x0)
+#define MCHP_OTPC_CR_READ BIT(6)
+#define MCHP_OTPC_MR (0x4)
+#define MCHP_OTPC_MR_ADDR GENMASK(31, 16)
+#define MCHP_OTPC_AR (0x8)
+#define MCHP_OTPC_SR (0xc)
+#define MCHP_OTPC_SR_READ BIT(6)
+#define MCHP_OTPC_HR (0x20)
+#define MCHP_OTPC_HR_SIZE GENMASK(15, 8)
+#define MCHP_OTPC_DR (0x24)
+
+#define MCHP_OTPC_NAME "mchp-otpc"
+#define MCHP_OTPC_SIZE (11 * 1024)
+
+/**
+ * struct mchp_otpc - OTPC private data structure
+ * @base: base address
+ * @dev: struct device pointer
+ * @packets: list of packets in OTP memory
+ * @npackets: number of packets in OTP memory
+ */
+struct mchp_otpc {
+ void __iomem *base;
+ struct device *dev;
+ struct list_head packets;
+ u32 npackets;
+};
+
+/**
+ * struct mchp_otpc_packet - OTPC packet data structure
+ * @list: list head
+ * @id: packet ID
+ * @offset: packet offset (in words) in OTP memory
+ */
+struct mchp_otpc_packet {
+ struct list_head list;
+ u32 id;
+ u32 offset;
+};
+
+static struct mchp_otpc_packet *mchp_otpc_id_to_packet(struct mchp_otpc *otpc,
+ u32 id)
+{
+ struct mchp_otpc_packet *packet;
+
+ if (id >= otpc->npackets)
+ return NULL;
+
+ list_for_each_entry(packet, &otpc->packets, list) {
+ if (packet->id == id)
+ return packet;
+ }
+
+ return NULL;
+}
+
+static int mchp_otpc_prepare_read(struct mchp_otpc *otpc,
+ unsigned int offset)
+{
+ u32 tmp;
+
+ /* Set address. */
+ tmp = readl_relaxed(otpc->base + MCHP_OTPC_MR);
+ tmp &= ~MCHP_OTPC_MR_ADDR;
+ tmp |= FIELD_PREP(MCHP_OTPC_MR_ADDR, offset);
+ writel_relaxed(tmp, otpc->base + MCHP_OTPC_MR);
+
+ /* Set read. */
+ tmp = readl_relaxed(otpc->base + MCHP_OTPC_CR);
+ tmp |= MCHP_OTPC_CR_READ;
+ writel_relaxed(tmp, otpc->base + MCHP_OTPC_CR);
+
+ /* Wait for packet to be transferred into temporary buffers. */
+ return read_poll_timeout(readl_relaxed, tmp, !(tmp & MCHP_OTPC_SR_READ),
+ 10000, 2000, false, otpc->base + MCHP_OTPC_SR);
+}
+
+/*
+ * OTPC memory is organized into packets. Each packets contains a header and
+ * a payload. Header is 4 bytes long and contains the size of the payload.
+ * Payload size varies. The memory footprint is something as follows:
+ *
+ * Memory offset Memory footprint Packet ID
+ * ------------- ---------------- ---------
+ *
+ * 0x0 +------------+ <-- packet 0
+ * | header 0 |
+ * 0x4 +------------+
+ * | payload 0 |
+ * . .
+ * . ... .
+ * . .
+ * offset1 +------------+ <-- packet 1
+ * | header 1 |
+ * offset1 + 0x4 +------------+
+ * | payload 1 |
+ * . .
+ * . ... .
+ * . .
+ * offset2 +------------+ <-- packet 2
+ * . .
+ * . ... .
+ * . .
+ * offsetN +------------+ <-- packet N
+ * | header N |
+ * offsetN + 0x4 +------------+
+ * | payload N |
+ * . .
+ * . ... .
+ * . .
+ * +------------+
+ *
+ * where offset1, offset2, offsetN depends on the size of payload 0, payload 1,
+ * payload N-1.
+ *
+ * The access to memory is done on a per packet basis: the control registers
+ * need to be updated with an offset address (within a packet range) and the
+ * data registers will be update by controller with information contained by
+ * that packet. E.g. if control registers are updated with any address within
+ * the range [offset1, offset2) the data registers are updated by controller
+ * with packet 1. Header data is accessible though MCHP_OTPC_HR register.
+ * Payload data is accessible though MCHP_OTPC_DR and MCHP_OTPC_AR registers.
+ * There is no direct mapping b/w the offset requested by software and the
+ * offset returned by hardware.
+ *
+ * For this, the read function will return the first requested bytes in the
+ * packet. The user will have to be aware of the memory footprint before doing
+ * the read request.
+ */
+static int mchp_otpc_read(void *priv, unsigned int off, void *val,
+ size_t bytes)
+{
+ struct mchp_otpc *otpc = priv;
+ struct mchp_otpc_packet *packet;
+ u32 *buf = val;
+ u32 offset;
+ size_t len = 0;
+ int ret, payload_size;
+
+ /*
+ * We reach this point with off being multiple of stride = 4 to
+ * be able to cross the subsystem. Inside the driver we use continuous
+ * unsigned integer numbers for packet id, thus divide off by 4
+ * before passing it to mchp_otpc_id_to_packet().
+ */
+ packet = mchp_otpc_id_to_packet(otpc, off / 4);
+ if (!packet)
+ return -EINVAL;
+ offset = packet->offset;
+
+ while (len < bytes) {
+ ret = mchp_otpc_prepare_read(otpc, offset);
+ if (ret)
+ return ret;
+
+ /* Read and save header content. */
+ *buf++ = readl_relaxed(otpc->base + MCHP_OTPC_HR);
+ len += sizeof(*buf);
+ offset++;
+ if (len >= bytes)
+ break;
+
+ /* Read and save payload content. */
+ payload_size = FIELD_GET(MCHP_OTPC_HR_SIZE, *(buf - 1));
+ writel_relaxed(0UL, otpc->base + MCHP_OTPC_AR);
+ do {
+ *buf++ = readl_relaxed(otpc->base + MCHP_OTPC_DR);
+ len += sizeof(*buf);
+ offset++;
+ payload_size--;
+ } while (payload_size >= 0 && len < bytes);
+ }
+
+ return 0;
+}
+
+static int mchp_otpc_init_packets_list(struct mchp_otpc *otpc, u32 *size)
+{
+ struct mchp_otpc_packet *packet;
+ u32 word, word_pos = 0, id = 0, npackets = 0, payload_size;
+ int ret;
+
+ INIT_LIST_HEAD(&otpc->packets);
+ *size = 0;
+
+ while (*size < MCHP_OTPC_SIZE) {
+ ret = mchp_otpc_prepare_read(otpc, word_pos);
+ if (ret)
+ return ret;
+
+ word = readl_relaxed(otpc->base + MCHP_OTPC_HR);
+ payload_size = FIELD_GET(MCHP_OTPC_HR_SIZE, word);
+ if (!payload_size)
+ break;
+
+ packet = devm_kzalloc(otpc->dev, sizeof(*packet), GFP_KERNEL);
+ if (!packet)
+ return -ENOMEM;
+
+ packet->id = id++;
+ packet->offset = word_pos;
+ INIT_LIST_HEAD(&packet->list);
+ list_add_tail(&packet->list, &otpc->packets);
+
+ /* Count size by adding header and paload sizes. */
+ *size += 4 * (payload_size + 1);
+ /* Next word: this packet (header, payload) position + 1. */
+ word_pos += payload_size + 2;
+
+ npackets++;
+ }
+
+ otpc->npackets = npackets;
+
+ return 0;
+}
+
+static struct nvmem_config mchp_nvmem_config = {
+ .name = MCHP_OTPC_NAME,
+ .type = NVMEM_TYPE_OTP,
+ .read_only = true,
+ .word_size = 4,
+ .stride = 4,
+ .reg_read = mchp_otpc_read,
+};
+
+static int mchp_otpc_probe(struct platform_device *pdev)
+{
+ struct nvmem_device *nvmem;
+ struct mchp_otpc *otpc;
+ u32 size;
+ int ret;
+
+ otpc = devm_kzalloc(&pdev->dev, sizeof(*otpc), GFP_KERNEL);
+ if (!otpc)
+ return -ENOMEM;
+
+ otpc->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(otpc->base))
+ return PTR_ERR(otpc->base);
+
+ otpc->dev = &pdev->dev;
+ ret = mchp_otpc_init_packets_list(otpc, &size);
+ if (ret)
+ return ret;
+
+ mchp_nvmem_config.dev = otpc->dev;
+ mchp_nvmem_config.add_legacy_fixed_of_cells = true;
+ mchp_nvmem_config.size = size;
+ mchp_nvmem_config.priv = otpc;
+ nvmem = devm_nvmem_register(&pdev->dev, &mchp_nvmem_config);
+
+ return PTR_ERR_OR_ZERO(nvmem);
+}
+
+static const struct of_device_id __maybe_unused mchp_otpc_ids[] = {
+ { .compatible = "microchip,sama7g5-otpc", },
+ { },
+};
+MODULE_DEVICE_TABLE(of, mchp_otpc_ids);
+
+static struct platform_driver mchp_otpc_driver = {
+ .probe = mchp_otpc_probe,
+ .driver = {
+ .name = MCHP_OTPC_NAME,
+ .of_match_table = of_match_ptr(mchp_otpc_ids),
+ },
+};
+module_platform_driver(mchp_otpc_driver);
+
+MODULE_AUTHOR("Claudiu Beznea <claudiu.beznea@microchip.com>");
+MODULE_DESCRIPTION("Microchip SAMA7G5 OTPC driver");
+MODULE_LICENSE("GPL");