// SPDX-License-Identifier: GPL-2.0 /* * Copyright(c) 2019-2025, Intel Corporation. All rights reserved. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct intel_dg_nvm { struct kref refcnt; struct mtd_info mtd; struct mutex lock; /* region access lock */ void __iomem *base; void __iomem *base2; bool non_posted_erase; size_t size; unsigned int nregions; struct { const char *name; u8 id; u64 offset; u64 size; unsigned int is_readable:1; unsigned int is_writable:1; } regions[] __counted_by(nregions); }; #define NVM_TRIGGER_REG 0x00000000 #define NVM_VALSIG_REG 0x00000010 #define NVM_ADDRESS_REG 0x00000040 #define NVM_REGION_ID_REG 0x00000044 #define NVM_DEBUG_REG 0x00000000 /* * [15:0]-Erase size = 0x0010 4K 0x0080 32K 0x0100 64K * [23:16]-Reserved * [31:24]-Erase MEM RegionID */ #define NVM_ERASE_REG 0x00000048 #define NVM_ACCESS_ERROR_REG 0x00000070 #define NVM_ADDRESS_ERROR_REG 0x00000074 /* Flash Valid Signature */ #define NVM_FLVALSIG 0x0FF0A55A #define NVM_MAP_ADDR_MASK GENMASK(7, 0) #define NVM_MAP_ADDR_SHIFT 0x00000004 #define NVM_REGION_ID_DESCRIPTOR 0 /* Flash Region Base Address */ #define NVM_FRBA 0x40 /* Flash Region __n - Flash Descriptor Record */ #define NVM_FLREG(__n) (NVM_FRBA + ((__n) * 4)) /* Flash Map 1 Register */ #define NVM_FLMAP1_REG 0x18 #define NVM_FLMSTR4_OFFSET 0x00C #define NVM_ACCESS_ERROR_PCIE_MASK 0x7 #define NVM_FREG_BASE_MASK GENMASK(15, 0) #define NVM_FREG_ADDR_MASK GENMASK(31, 16) #define NVM_FREG_ADDR_SHIFT 12 #define NVM_FREG_MIN_REGION_SIZE 0xFFF #define NVM_NON_POSTED_ERASE_DONE BIT(23) #define NVM_NON_POSTED_ERASE_DONE_ITER 3000 static inline void idg_nvm_set_region_id(struct intel_dg_nvm *nvm, u8 region) { iowrite32((u32)region, nvm->base + NVM_REGION_ID_REG); } static inline u32 idg_nvm_error(struct intel_dg_nvm *nvm) { void __iomem *base = nvm->base; u32 reg = ioread32(base + NVM_ACCESS_ERROR_REG) & NVM_ACCESS_ERROR_PCIE_MASK; /* reset error bits */ if (reg) iowrite32(reg, base + NVM_ACCESS_ERROR_REG); return reg; } static inline u32 idg_nvm_read32(struct intel_dg_nvm *nvm, u32 address) { void __iomem *base = nvm->base; iowrite32(address, base + NVM_ADDRESS_REG); return ioread32(base + NVM_TRIGGER_REG); } static inline u64 idg_nvm_read64(struct intel_dg_nvm *nvm, u32 address) { void __iomem *base = nvm->base; iowrite32(address, base + NVM_ADDRESS_REG); return readq(base + NVM_TRIGGER_REG); } static void idg_nvm_write32(struct intel_dg_nvm *nvm, u32 address, u32 data) { void __iomem *base = nvm->base; iowrite32(address, base + NVM_ADDRESS_REG); iowrite32(data, base + NVM_TRIGGER_REG); } static void idg_nvm_write64(struct intel_dg_nvm *nvm, u32 address, u64 data) { void __iomem *base = nvm->base; iowrite32(address, base + NVM_ADDRESS_REG); writeq(data, base + NVM_TRIGGER_REG); } static int idg_nvm_get_access_map(struct intel_dg_nvm *nvm, u32 *access_map) { u32 fmstr4_addr; u32 fmstr4; u32 flmap1; u32 fmba; idg_nvm_set_region_id(nvm, NVM_REGION_ID_DESCRIPTOR); flmap1 = idg_nvm_read32(nvm, NVM_FLMAP1_REG); if (idg_nvm_error(nvm)) return -EIO; /* Get Flash Master Baser Address (FMBA) */ fmba = (FIELD_GET(NVM_MAP_ADDR_MASK, flmap1) << NVM_MAP_ADDR_SHIFT); fmstr4_addr = fmba + NVM_FLMSTR4_OFFSET; fmstr4 = idg_nvm_read32(nvm, fmstr4_addr); if (idg_nvm_error(nvm)) return -EIO; *access_map = fmstr4; return 0; } /* * Region read/write access encoded in the access map * in the following order from the lower bit: * [3:0] regions 12-15 read state * [7:4] regions 12-15 write state * [19:8] regions 0-11 read state * [31:20] regions 0-11 write state */ static bool idg_nvm_region_readable(u32 access_map, u8 region) { if (region < 12) return access_map & BIT(region + 8); /* [19:8] */ else return access_map & BIT(region - 12); /* [3:0] */ } static bool idg_nvm_region_writable(u32 access_map, u8 region) { if (region < 12) return access_map & BIT(region + 20); /* [31:20] */ else return access_map & BIT(region - 8); /* [7:4] */ } static int idg_nvm_is_valid(struct intel_dg_nvm *nvm) { u32 is_valid; idg_nvm_set_region_id(nvm, NVM_REGION_ID_DESCRIPTOR); is_valid = idg_nvm_read32(nvm, NVM_VALSIG_REG); if (idg_nvm_error(nvm)) return -EIO; if (is_valid != NVM_FLVALSIG) return -ENODEV; return 0; } static unsigned int idg_nvm_get_region(const struct intel_dg_nvm *nvm, loff_t from) { unsigned int i; for (i = 0; i < nvm->nregions; i++) { if ((nvm->regions[i].offset + nvm->regions[i].size - 1) >= from && nvm->regions[i].offset <= from && nvm->regions[i].size != 0) break; } return i; } static ssize_t idg_nvm_rewrite_partial(struct intel_dg_nvm *nvm, loff_t to, loff_t offset, size_t len, const u32 *newdata) { u32 data = idg_nvm_read32(nvm, to); if (idg_nvm_error(nvm)) return -EIO; memcpy((u8 *)&data + offset, newdata, len); idg_nvm_write32(nvm, to, data); if (idg_nvm_error(nvm)) return -EIO; return len; } static ssize_t idg_write(struct intel_dg_nvm *nvm, u8 region, loff_t to, size_t len, const unsigned char *buf) { size_t len_s = len; size_t to_shift; size_t len8; size_t len4; ssize_t ret; size_t to4; size_t i; idg_nvm_set_region_id(nvm, region); to4 = ALIGN_DOWN(to, sizeof(u32)); to_shift = min(sizeof(u32) - ((size_t)to - to4), len); if (to - to4) { ret = idg_nvm_rewrite_partial(nvm, to4, to - to4, to_shift, (u32 *)&buf[0]); if (ret < 0) return ret; buf += to_shift; to += to_shift; len_s -= to_shift; } if (!IS_ALIGNED(to, sizeof(u64)) && ((to ^ (to + len_s)) & GENMASK(31, 10))) { /* * Workaround reads/writes across 1k-aligned addresses * (start u32 before 1k, end u32 after) * as this fails on hardware. */ u32 data; memcpy(&data, &buf[0], sizeof(u32)); idg_nvm_write32(nvm, to, data); if (idg_nvm_error(nvm)) return -EIO; buf += sizeof(u32); to += sizeof(u32); len_s -= sizeof(u32); } len8 = ALIGN_DOWN(len_s, sizeof(u64)); for (i = 0; i < len8; i += sizeof(u64)) { u64 data; memcpy(&data, &buf[i], sizeof(u64)); idg_nvm_write64(nvm, to + i, data); if (idg_nvm_error(nvm)) return -EIO; } len4 = len_s - len8; if (len4 >= sizeof(u32)) { u32 data; memcpy(&data, &buf[i], sizeof(u32)); idg_nvm_write32(nvm, to + i, data); if (idg_nvm_error(nvm)) return -EIO; i += sizeof(u32); len4 -= sizeof(u32); } if (len4 > 0) { ret = idg_nvm_rewrite_partial(nvm, to + i, 0, len4, (u32 *)&buf[i]); if (ret < 0) return ret; } return len; } static ssize_t idg_read(struct intel_dg_nvm *nvm, u8 region, loff_t from, size_t len, unsigned char *buf) { size_t len_s = len; size_t from_shift; size_t from4; size_t len8; size_t len4; size_t i; idg_nvm_set_region_id(nvm, region); from4 = ALIGN_DOWN(from, sizeof(u32)); from_shift = min(sizeof(u32) - ((size_t)from - from4), len); if (from - from4) { u32 data = idg_nvm_read32(nvm, from4); if (idg_nvm_error(nvm)) return -EIO; memcpy(&buf[0], (u8 *)&data + (from - from4), from_shift); len_s -= from_shift; buf += from_shift; from += from_shift; } if (!IS_ALIGNED(from, sizeof(u64)) && ((from ^ (from + len_s)) & GENMASK(31, 10))) { /* * Workaround reads/writes across 1k-aligned addresses * (start u32 before 1k, end u32 after) * as this fails on hardware. */ u32 data = idg_nvm_read32(nvm, from); if (idg_nvm_error(nvm)) return -EIO; memcpy(&buf[0], &data, sizeof(data)); len_s -= sizeof(u32); buf += sizeof(u32); from += sizeof(u32); } len8 = ALIGN_DOWN(len_s, sizeof(u64)); for (i = 0; i < len8; i += sizeof(u64)) { u64 data = idg_nvm_read64(nvm, from + i); if (idg_nvm_error(nvm)) return -EIO; memcpy(&buf[i], &data, sizeof(data)); } len4 = len_s - len8; if (len4 >= sizeof(u32)) { u32 data = idg_nvm_read32(nvm, from + i); if (idg_nvm_error(nvm)) return -EIO; memcpy(&buf[i], &data, sizeof(data)); i += sizeof(u32); len4 -= sizeof(u32); } if (len4 > 0) { u32 data = idg_nvm_read32(nvm, from + i); if (idg_nvm_error(nvm)) return -EIO; memcpy(&buf[i], &data, len4); } return len; } static ssize_t idg_erase(struct intel_dg_nvm *nvm, u8 region, loff_t from, u64 len, u64 *fail_addr) { void __iomem *base2 = nvm->base2; void __iomem *base = nvm->base; const u32 block = 0x10; u32 iter = 0; u32 reg; u64 i; for (i = 0; i < len; i += SZ_4K) { iowrite32(from + i, base + NVM_ADDRESS_REG); iowrite32(region << 24 | block, base + NVM_ERASE_REG); if (nvm->non_posted_erase) { /* Wait for Erase Done */ reg = ioread32(base2 + NVM_DEBUG_REG); while (!(reg & NVM_NON_POSTED_ERASE_DONE) && ++iter < NVM_NON_POSTED_ERASE_DONE_ITER) { msleep(10); reg = ioread32(base2 + NVM_DEBUG_REG); } if (reg & NVM_NON_POSTED_ERASE_DONE) { /* Clear Erase Done */ iowrite32(reg, base2 + NVM_DEBUG_REG); } else { *fail_addr = from + i; return -ETIME; } } /* Since the writes are via sgunit * we cannot do back to back erases. */ msleep(50); } return len; } static int intel_dg_nvm_init(struct intel_dg_nvm *nvm, struct device *device, bool non_posted_erase) { u32 access_map = 0; unsigned int i, n; int ret; /* clean error register, previous errors are ignored */ idg_nvm_error(nvm); ret = idg_nvm_is_valid(nvm); if (ret) { dev_err(device, "The MEM is not valid %d\n", ret); return ret; } if (idg_nvm_get_access_map(nvm, &access_map)) return -EIO; for (i = 0, n = 0; i < nvm->nregions; i++) { u32 address, base, limit, region; u8 id = nvm->regions[i].id; address = NVM_FLREG(id); region = idg_nvm_read32(nvm, address); base = FIELD_GET(NVM_FREG_BASE_MASK, region) << NVM_FREG_ADDR_SHIFT; limit = (FIELD_GET(NVM_FREG_ADDR_MASK, region) << NVM_FREG_ADDR_SHIFT) | NVM_FREG_MIN_REGION_SIZE; dev_dbg(device, "[%d] %s: region: 0x%08X base: 0x%08x limit: 0x%08x\n", id, nvm->regions[i].name, region, base, limit); if (base >= limit || (i > 0 && limit == 0)) { dev_dbg(device, "[%d] %s: disabled\n", id, nvm->regions[i].name); nvm->regions[i].is_readable = 0; continue; } if (nvm->size < limit) nvm->size = limit; nvm->regions[i].offset = base; nvm->regions[i].size = limit - base + 1; /* No write access to descriptor; mask it out*/ nvm->regions[i].is_writable = idg_nvm_region_writable(access_map, id); nvm->regions[i].is_readable = idg_nvm_region_readable(access_map, id); dev_dbg(device, "Registered, %s id=%d offset=%lld size=%lld rd=%d wr=%d\n", nvm->regions[i].name, nvm->regions[i].id, nvm->regions[i].offset, nvm->regions[i].size, nvm->regions[i].is_readable, nvm->regions[i].is_writable); if (nvm->regions[i].is_readable) n++; } nvm->non_posted_erase = non_posted_erase; dev_dbg(device, "Registered %d regions\n", n); dev_dbg(device, "Non posted erase %d\n", nvm->non_posted_erase); /* Need to add 1 to the amount of memory * so it is reported as an even block */ nvm->size += 1; return n; } static int intel_dg_mtd_erase(struct mtd_info *mtd, struct erase_info *info) { struct intel_dg_nvm *nvm = mtd->priv; size_t total_len; unsigned int idx; ssize_t bytes; loff_t from; size_t len; u8 region; u64 addr; if (WARN_ON(!nvm)) return -EINVAL; if (!IS_ALIGNED(info->addr, SZ_4K) || !IS_ALIGNED(info->len, SZ_4K)) { dev_err(&mtd->dev, "unaligned erase %llx %llx\n", info->addr, info->len); info->fail_addr = MTD_FAIL_ADDR_UNKNOWN; return -EINVAL; } total_len = info->len; addr = info->addr; guard(mutex)(&nvm->lock); while (total_len > 0) { if (!IS_ALIGNED(addr, SZ_4K) || !IS_ALIGNED(total_len, SZ_4K)) { dev_err(&mtd->dev, "unaligned erase %llx %zx\n", addr, total_len); info->fail_addr = addr; return -ERANGE; } idx = idg_nvm_get_region(nvm, addr); if (idx >= nvm->nregions) { dev_err(&mtd->dev, "out of range"); info->fail_addr = MTD_FAIL_ADDR_UNKNOWN; return -ERANGE; } from = addr - nvm->regions[idx].offset; region = nvm->regions[idx].id; len = total_len; if (len > nvm->regions[idx].size - from) len = nvm->regions[idx].size - from; dev_dbg(&mtd->dev, "erasing region[%d] %s from %llx len %zx\n", region, nvm->regions[idx].name, from, len); bytes = idg_erase(nvm, region, from, len, &info->fail_addr); if (bytes < 0) { dev_dbg(&mtd->dev, "erase failed with %zd\n", bytes); info->fail_addr += nvm->regions[idx].offset; return bytes; } addr += len; total_len -= len; } return 0; } static int intel_dg_mtd_read(struct mtd_info *mtd, loff_t from, size_t len, size_t *retlen, u_char *buf) { struct intel_dg_nvm *nvm = mtd->priv; unsigned int idx; ssize_t ret; u8 region; if (WARN_ON(!nvm)) return -EINVAL; idx = idg_nvm_get_region(nvm, from); dev_dbg(&mtd->dev, "reading region[%d] %s from %lld len %zd\n", nvm->regions[idx].id, nvm->regions[idx].name, from, len); if (idx >= nvm->nregions) { dev_err(&mtd->dev, "out of range"); return -ERANGE; } from -= nvm->regions[idx].offset; region = nvm->regions[idx].id; if (len > nvm->regions[idx].size - from) len = nvm->regions[idx].size - from; guard(mutex)(&nvm->lock); ret = idg_read(nvm, region, from, len, buf); if (ret < 0) { dev_dbg(&mtd->dev, "read failed with %zd\n", ret); return ret; } *retlen = ret; return 0; } static int intel_dg_mtd_write(struct mtd_info *mtd, loff_t to, size_t len, size_t *retlen, const u_char *buf) { struct intel_dg_nvm *nvm = mtd->priv; unsigned int idx; ssize_t ret; u8 region; if (WARN_ON(!nvm)) return -EINVAL; idx = idg_nvm_get_region(nvm, to); dev_dbg(&mtd->dev, "writing region[%d] %s to %lld len %zd\n", nvm->regions[idx].id, nvm->regions[idx].name, to, len); if (idx >= nvm->nregions) { dev_err(&mtd->dev, "out of range"); return -ERANGE; } to -= nvm->regions[idx].offset; region = nvm->regions[idx].id; if (len > nvm->regions[idx].size - to) len = nvm->regions[idx].size - to; guard(mutex)(&nvm->lock); ret = idg_write(nvm, region, to, len, buf); if (ret < 0) { dev_dbg(&mtd->dev, "write failed with %zd\n", ret); return ret; } *retlen = ret; return 0; } static void intel_dg_nvm_release(struct kref *kref) { struct intel_dg_nvm *nvm = container_of(kref, struct intel_dg_nvm, refcnt); int i; pr_debug("freeing intel_dg nvm\n"); for (i = 0; i < nvm->nregions; i++) kfree(nvm->regions[i].name); mutex_destroy(&nvm->lock); kfree(nvm); } static int intel_dg_mtd_get_device(struct mtd_info *mtd) { struct mtd_info *master = mtd_get_master(mtd); struct intel_dg_nvm *nvm = master->priv; if (WARN_ON(!nvm)) return -EINVAL; pr_debug("get mtd %s %d\n", mtd->name, kref_read(&nvm->refcnt)); kref_get(&nvm->refcnt); return 0; } static void intel_dg_mtd_put_device(struct mtd_info *mtd) { struct mtd_info *master = mtd_get_master(mtd); struct intel_dg_nvm *nvm = master->priv; if (WARN_ON(!nvm)) return; pr_debug("put mtd %s %d\n", mtd->name, kref_read(&nvm->refcnt)); kref_put(&nvm->refcnt, intel_dg_nvm_release); } static int intel_dg_nvm_init_mtd(struct intel_dg_nvm *nvm, struct device *device, unsigned int nparts, bool writable_override) { struct mtd_partition *parts = NULL; unsigned int i, n; int ret; dev_dbg(device, "registering with mtd\n"); nvm->mtd.owner = THIS_MODULE; nvm->mtd.dev.parent = device; nvm->mtd.flags = MTD_CAP_NORFLASH; nvm->mtd.type = MTD_DATAFLASH; nvm->mtd.priv = nvm; nvm->mtd._write = intel_dg_mtd_write; nvm->mtd._read = intel_dg_mtd_read; nvm->mtd._erase = intel_dg_mtd_erase; nvm->mtd._get_device = intel_dg_mtd_get_device; nvm->mtd._put_device = intel_dg_mtd_put_device; nvm->mtd.writesize = SZ_1; /* 1 byte granularity */ nvm->mtd.erasesize = SZ_4K; /* 4K bytes granularity */ nvm->mtd.size = nvm->size; parts = kcalloc(nvm->nregions, sizeof(*parts), GFP_KERNEL); if (!parts) return -ENOMEM; for (i = 0, n = 0; i < nvm->nregions && n < nparts; i++) { if (!nvm->regions[i].is_readable) continue; parts[n].name = nvm->regions[i].name; parts[n].offset = nvm->regions[i].offset; parts[n].size = nvm->regions[i].size; if (!nvm->regions[i].is_writable && !writable_override) parts[n].mask_flags = MTD_WRITEABLE; n++; } ret = mtd_device_register(&nvm->mtd, parts, n); kfree(parts); return ret; } static int intel_dg_mtd_probe(struct auxiliary_device *aux_dev, const struct auxiliary_device_id *aux_dev_id) { struct intel_dg_nvm_dev *invm = auxiliary_dev_to_intel_dg_nvm_dev(aux_dev); struct intel_dg_nvm *nvm; struct device *device; unsigned int nregions; unsigned int i, n; int ret; device = &aux_dev->dev; /* count available regions */ for (nregions = 0, i = 0; i < INTEL_DG_NVM_REGIONS; i++) { if (invm->regions[i].name) nregions++; } if (!nregions) { dev_err(device, "no regions defined\n"); return -ENODEV; } nvm = kzalloc(struct_size(nvm, regions, nregions), GFP_KERNEL); if (!nvm) return -ENOMEM; kref_init(&nvm->refcnt); mutex_init(&nvm->lock); for (n = 0, i = 0; i < INTEL_DG_NVM_REGIONS; i++) { if (!invm->regions[i].name) continue; char *name = kasprintf(GFP_KERNEL, "%s.%s", dev_name(&aux_dev->dev), invm->regions[i].name); if (!name) continue; nvm->regions[n].name = name; nvm->regions[n].id = i; n++; } nvm->nregions = n; /* in case where kasprintf fail */ nvm->base = devm_ioremap_resource(device, &invm->bar); if (IS_ERR(nvm->base)) { ret = PTR_ERR(nvm->base); goto err; } if (invm->non_posted_erase) { nvm->base2 = devm_ioremap_resource(device, &invm->bar2); if (IS_ERR(nvm->base2)) { ret = PTR_ERR(nvm->base2); goto err; } } ret = intel_dg_nvm_init(nvm, device, invm->non_posted_erase); if (ret < 0) { dev_err(device, "cannot initialize nvm %d\n", ret); goto err; } ret = intel_dg_nvm_init_mtd(nvm, device, ret, invm->writable_override); if (ret) { dev_err(device, "failed init mtd %d\n", ret); goto err; } dev_set_drvdata(&aux_dev->dev, nvm); return 0; err: kref_put(&nvm->refcnt, intel_dg_nvm_release); return ret; } static void intel_dg_mtd_remove(struct auxiliary_device *aux_dev) { struct intel_dg_nvm *nvm = dev_get_drvdata(&aux_dev->dev); if (!nvm) return; mtd_device_unregister(&nvm->mtd); dev_set_drvdata(&aux_dev->dev, NULL); kref_put(&nvm->refcnt, intel_dg_nvm_release); } static const struct auxiliary_device_id intel_dg_mtd_id_table[] = { { .name = "i915.nvm", }, { .name = "xe.nvm", }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(auxiliary, intel_dg_mtd_id_table); static struct auxiliary_driver intel_dg_mtd_driver = { .probe = intel_dg_mtd_probe, .remove = intel_dg_mtd_remove, .driver = { /* auxiliary_driver_register() sets .name to be the modname */ }, .id_table = intel_dg_mtd_id_table }; module_auxiliary_driver(intel_dg_mtd_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Intel Corporation"); MODULE_DESCRIPTION("Intel DGFX MTD driver");