| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Driver for HiSilicon Hydra Home Agent (HHA). |
| * |
| * Copyright (c) 2025 HiSilicon Technologies Co., Ltd. |
| * Author: Yicong Yang <yangyicong@hisilicon.com> |
| * Yushan Wang <wangyushan12@huawei.com> |
| * |
| * A system typically contains multiple HHAs. Each is responsible for a subset |
| * of the physical addresses in the system, but interleave can make the mapping |
| * from a particular cache line to a responsible HHA complex. As such no |
| * filtering is done in the driver, with the hardware being responsible for |
| * responding with success for even if it was not responsible for any addresses |
| * in the range on which the operation was requested. |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/cache_coherency.h> |
| #include <linux/dev_printk.h> |
| #include <linux/init.h> |
| #include <linux/io.h> |
| #include <linux/iopoll.h> |
| #include <linux/kernel.h> |
| #include <linux/memregion.h> |
| #include <linux/module.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/mutex.h> |
| #include <linux/platform_device.h> |
| |
| #define HISI_HHA_CTRL 0x5004 |
| #define HISI_HHA_CTRL_EN BIT(0) |
| #define HISI_HHA_CTRL_RANGE BIT(1) |
| #define HISI_HHA_CTRL_TYPE GENMASK(3, 2) |
| #define HISI_HHA_START_L 0x5008 |
| #define HISI_HHA_START_H 0x500c |
| #define HISI_HHA_LEN_L 0x5010 |
| #define HISI_HHA_LEN_H 0x5014 |
| |
| /* The maintain operation performs in a 128 Byte granularity */ |
| #define HISI_HHA_MAINT_ALIGN 128 |
| |
| #define HISI_HHA_POLL_GAP_US 10 |
| #define HISI_HHA_POLL_TIMEOUT_US 50000 |
| |
| struct hisi_soc_hha { |
| /* Must be first element */ |
| struct cache_coherency_ops_inst cci; |
| /* Locks HHA instance to forbid overlapping access. */ |
| struct mutex lock; |
| void __iomem *base; |
| }; |
| |
| static bool hisi_hha_cache_maintain_wait_finished(struct hisi_soc_hha *soc_hha) |
| { |
| u32 val; |
| |
| return !readl_poll_timeout_atomic(soc_hha->base + HISI_HHA_CTRL, val, |
| !(val & HISI_HHA_CTRL_EN), |
| HISI_HHA_POLL_GAP_US, |
| HISI_HHA_POLL_TIMEOUT_US); |
| } |
| |
| static int hisi_soc_hha_wbinv(struct cache_coherency_ops_inst *cci, |
| struct cc_inval_params *invp) |
| { |
| struct hisi_soc_hha *soc_hha = |
| container_of(cci, struct hisi_soc_hha, cci); |
| phys_addr_t top, addr = invp->addr; |
| size_t size = invp->size; |
| u32 reg; |
| |
| if (!size) |
| return -EINVAL; |
| |
| addr = ALIGN_DOWN(addr, HISI_HHA_MAINT_ALIGN); |
| top = ALIGN(addr + size, HISI_HHA_MAINT_ALIGN); |
| size = top - addr; |
| |
| guard(mutex)(&soc_hha->lock); |
| |
| if (!hisi_hha_cache_maintain_wait_finished(soc_hha)) |
| return -EBUSY; |
| |
| /* |
| * Hardware will search for addresses ranging [addr, addr + size - 1], |
| * last byte included, and perform maintenance in 128 byte granules |
| * on those cachelines which contain the addresses. If a given instance |
| * is either not responsible for a cacheline or that cacheline is not |
| * currently present then the search will fail, no operation will be |
| * necessary and the device will report success. |
| */ |
| size -= 1; |
| |
| writel(lower_32_bits(addr), soc_hha->base + HISI_HHA_START_L); |
| writel(upper_32_bits(addr), soc_hha->base + HISI_HHA_START_H); |
| writel(lower_32_bits(size), soc_hha->base + HISI_HHA_LEN_L); |
| writel(upper_32_bits(size), soc_hha->base + HISI_HHA_LEN_H); |
| |
| reg = FIELD_PREP(HISI_HHA_CTRL_TYPE, 1); /* Clean Invalid */ |
| reg |= HISI_HHA_CTRL_RANGE | HISI_HHA_CTRL_EN; |
| writel(reg, soc_hha->base + HISI_HHA_CTRL); |
| |
| return 0; |
| } |
| |
| static int hisi_soc_hha_done(struct cache_coherency_ops_inst *cci) |
| { |
| struct hisi_soc_hha *soc_hha = |
| container_of(cci, struct hisi_soc_hha, cci); |
| |
| guard(mutex)(&soc_hha->lock); |
| if (!hisi_hha_cache_maintain_wait_finished(soc_hha)) |
| return -ETIMEDOUT; |
| |
| return 0; |
| } |
| |
| static const struct cache_coherency_ops hha_ops = { |
| .wbinv = hisi_soc_hha_wbinv, |
| .done = hisi_soc_hha_done, |
| }; |
| |
| static int hisi_soc_hha_probe(struct platform_device *pdev) |
| { |
| struct hisi_soc_hha *soc_hha; |
| struct resource *mem; |
| int ret; |
| |
| soc_hha = cache_coherency_ops_instance_alloc(&hha_ops, |
| struct hisi_soc_hha, cci); |
| if (!soc_hha) |
| return -ENOMEM; |
| |
| platform_set_drvdata(pdev, soc_hha); |
| |
| mutex_init(&soc_hha->lock); |
| |
| mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| if (!mem) { |
| ret = -ENOMEM; |
| goto err_free_cci; |
| } |
| |
| soc_hha->base = ioremap(mem->start, resource_size(mem)); |
| if (!soc_hha->base) { |
| ret = dev_err_probe(&pdev->dev, -ENOMEM, |
| "failed to remap io memory"); |
| goto err_free_cci; |
| } |
| |
| ret = cache_coherency_ops_instance_register(&soc_hha->cci); |
| if (ret) |
| goto err_iounmap; |
| |
| return 0; |
| |
| err_iounmap: |
| iounmap(soc_hha->base); |
| err_free_cci: |
| cache_coherency_ops_instance_put(&soc_hha->cci); |
| return ret; |
| } |
| |
| static void hisi_soc_hha_remove(struct platform_device *pdev) |
| { |
| struct hisi_soc_hha *soc_hha = platform_get_drvdata(pdev); |
| |
| cache_coherency_ops_instance_unregister(&soc_hha->cci); |
| iounmap(soc_hha->base); |
| cache_coherency_ops_instance_put(&soc_hha->cci); |
| } |
| |
| static const struct acpi_device_id hisi_soc_hha_ids[] = { |
| { "HISI0511", }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(acpi, hisi_soc_hha_ids); |
| |
| static struct platform_driver hisi_soc_hha_driver = { |
| .driver = { |
| .name = "hisi_soc_hha", |
| .acpi_match_table = hisi_soc_hha_ids, |
| }, |
| .probe = hisi_soc_hha_probe, |
| .remove = hisi_soc_hha_remove, |
| }; |
| |
| module_platform_driver(hisi_soc_hha_driver); |
| |
| MODULE_IMPORT_NS("CACHE_COHERENCY"); |
| MODULE_DESCRIPTION("HiSilicon Hydra Home Agent driver supporting cache maintenance"); |
| MODULE_AUTHOR("Yicong Yang <yangyicong@hisilicon.com>"); |
| MODULE_AUTHOR("Yushan Wang <wangyushan12@huawei.com>"); |
| MODULE_LICENSE("GPL"); |