root/drivers/nvmem/microchip-otpc.c
// 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");