| // 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"); |