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