| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * Copyright (C) 2025 Loongson Technology Corporation Limited |
| * |
| * Author: Yinggang Gu <guyinggang@loongson.cn> |
| * Author: Qunqin Zhao <zhaoqunqin@loongson.cn> |
| */ |
| |
| #include <linux/acpi.h> |
| #include <linux/delay.h> |
| #include <linux/device.h> |
| #include <linux/dma-mapping.h> |
| #include <linux/errno.h> |
| #include <linux/init.h> |
| #include <linux/interrupt.h> |
| #include <linux/iopoll.h> |
| #include <linux/kernel.h> |
| #include <linux/mfd/core.h> |
| #include <linux/mfd/loongson-se.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| |
| struct loongson_se { |
| void __iomem *base; |
| spinlock_t dev_lock; |
| struct completion cmd_completion; |
| |
| void *dmam_base; |
| int dmam_size; |
| |
| struct mutex engine_init_lock; |
| struct loongson_se_engine engines[SE_ENGINE_MAX]; |
| }; |
| |
| struct loongson_se_controller_cmd { |
| u32 command_id; |
| u32 info[7]; |
| }; |
| |
| static int loongson_se_poll(struct loongson_se *se, u32 int_bit) |
| { |
| u32 status; |
| int err; |
| |
| spin_lock_irq(&se->dev_lock); |
| |
| /* Notify the controller that the engine needs to be started */ |
| writel(int_bit, se->base + SE_L2SINT_SET); |
| |
| /* Polling until the controller has forwarded the engine command */ |
| err = readl_relaxed_poll_timeout_atomic(se->base + SE_L2SINT_STAT, status, |
| !(status & int_bit), |
| 1, LOONGSON_ENGINE_CMD_TIMEOUT_US); |
| |
| spin_unlock_irq(&se->dev_lock); |
| |
| return err; |
| } |
| |
| static int loongson_se_send_controller_cmd(struct loongson_se *se, |
| struct loongson_se_controller_cmd *cmd) |
| { |
| u32 *send_cmd = (u32 *)cmd; |
| int err, i; |
| |
| for (i = 0; i < SE_SEND_CMD_REG_LEN; i++) |
| writel(send_cmd[i], se->base + SE_SEND_CMD_REG + i * 4); |
| |
| err = loongson_se_poll(se, SE_INT_CONTROLLER); |
| if (err) |
| return err; |
| |
| return wait_for_completion_interruptible(&se->cmd_completion); |
| } |
| |
| int loongson_se_send_engine_cmd(struct loongson_se_engine *engine) |
| { |
| /* |
| * After engine initialization, the controller already knows |
| * where to obtain engine commands from. Now all we need to |
| * do is notify the controller that the engine needs to be started. |
| */ |
| int err = loongson_se_poll(engine->se, BIT(engine->id)); |
| |
| if (err) |
| return err; |
| |
| return wait_for_completion_interruptible(&engine->completion); |
| } |
| EXPORT_SYMBOL_GPL(loongson_se_send_engine_cmd); |
| |
| struct loongson_se_engine *loongson_se_init_engine(struct device *dev, int id) |
| { |
| struct loongson_se *se = dev_get_drvdata(dev); |
| struct loongson_se_engine *engine = &se->engines[id]; |
| struct loongson_se_controller_cmd cmd; |
| |
| engine->se = se; |
| engine->id = id; |
| init_completion(&engine->completion); |
| |
| /* Divide DMA memory equally among all engines */ |
| engine->buffer_size = se->dmam_size / SE_ENGINE_MAX; |
| engine->buffer_off = (se->dmam_size / SE_ENGINE_MAX) * id; |
| engine->data_buffer = se->dmam_base + engine->buffer_off; |
| |
| /* |
| * There has no engine0, use its data buffer as command buffer for other |
| * engines. The DMA memory size is obtained from the ACPI table, which |
| * ensures that the data buffer size of engine0 is larger than the |
| * command buffer size of all engines. |
| */ |
| engine->command = se->dmam_base + id * (2 * SE_ENGINE_CMD_SIZE); |
| engine->command_ret = engine->command + SE_ENGINE_CMD_SIZE; |
| |
| mutex_lock(&se->engine_init_lock); |
| |
| /* Tell the controller where to find engine command */ |
| cmd.command_id = SE_CMD_SET_ENGINE_CMDBUF; |
| cmd.info[0] = id; |
| cmd.info[1] = engine->command - se->dmam_base; |
| cmd.info[2] = 2 * SE_ENGINE_CMD_SIZE; |
| |
| if (loongson_se_send_controller_cmd(se, &cmd)) |
| engine = NULL; |
| |
| mutex_unlock(&se->engine_init_lock); |
| |
| return engine; |
| } |
| EXPORT_SYMBOL_GPL(loongson_se_init_engine); |
| |
| static irqreturn_t se_irq_handler(int irq, void *dev_id) |
| { |
| struct loongson_se *se = dev_id; |
| u32 int_status; |
| int id; |
| |
| spin_lock(&se->dev_lock); |
| |
| int_status = readl(se->base + SE_S2LINT_STAT); |
| |
| /* For controller */ |
| if (int_status & SE_INT_CONTROLLER) { |
| complete(&se->cmd_completion); |
| int_status &= ~SE_INT_CONTROLLER; |
| writel(SE_INT_CONTROLLER, se->base + SE_S2LINT_CL); |
| } |
| |
| /* For engines */ |
| while (int_status) { |
| id = __ffs(int_status); |
| complete(&se->engines[id].completion); |
| int_status &= ~BIT(id); |
| writel(BIT(id), se->base + SE_S2LINT_CL); |
| } |
| |
| spin_unlock(&se->dev_lock); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int loongson_se_init(struct loongson_se *se, dma_addr_t addr, int size) |
| { |
| struct loongson_se_controller_cmd cmd; |
| int err; |
| |
| cmd.command_id = SE_CMD_START; |
| err = loongson_se_send_controller_cmd(se, &cmd); |
| if (err) |
| return err; |
| |
| cmd.command_id = SE_CMD_SET_DMA; |
| cmd.info[0] = lower_32_bits(addr); |
| cmd.info[1] = upper_32_bits(addr); |
| cmd.info[2] = size; |
| |
| return loongson_se_send_controller_cmd(se, &cmd); |
| } |
| |
| static const struct mfd_cell engines[] = { |
| { .name = "loongson-rng" }, |
| { .name = "tpm_loongson" }, |
| }; |
| |
| static int loongson_se_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct loongson_se *se; |
| int nr_irq, irq, err, i; |
| dma_addr_t paddr; |
| |
| se = devm_kmalloc(dev, sizeof(*se), GFP_KERNEL); |
| if (!se) |
| return -ENOMEM; |
| |
| dev_set_drvdata(dev, se); |
| init_completion(&se->cmd_completion); |
| spin_lock_init(&se->dev_lock); |
| mutex_init(&se->engine_init_lock); |
| |
| dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); |
| if (device_property_read_u32(dev, "dmam_size", &se->dmam_size)) |
| return -ENODEV; |
| |
| se->dmam_base = dmam_alloc_coherent(dev, se->dmam_size, &paddr, GFP_KERNEL); |
| if (!se->dmam_base) |
| return -ENOMEM; |
| |
| se->base = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(se->base)) |
| return PTR_ERR(se->base); |
| |
| writel(SE_INT_ALL, se->base + SE_S2LINT_EN); |
| |
| nr_irq = platform_irq_count(pdev); |
| if (nr_irq <= 0) |
| return -ENODEV; |
| |
| for (i = 0; i < nr_irq; i++) { |
| irq = platform_get_irq(pdev, i); |
| err = devm_request_irq(dev, irq, se_irq_handler, 0, "loongson-se", se); |
| if (err) |
| dev_err(dev, "failed to request IRQ: %d\n", irq); |
| } |
| |
| err = loongson_se_init(se, paddr, se->dmam_size); |
| if (err) |
| return err; |
| |
| return devm_mfd_add_devices(dev, PLATFORM_DEVID_NONE, engines, |
| ARRAY_SIZE(engines), NULL, 0, NULL); |
| } |
| |
| static const struct acpi_device_id loongson_se_acpi_match[] = { |
| { "LOON0011", 0 }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(acpi, loongson_se_acpi_match); |
| |
| static struct platform_driver loongson_se_driver = { |
| .probe = loongson_se_probe, |
| .driver = { |
| .name = "loongson-se", |
| .acpi_match_table = loongson_se_acpi_match, |
| }, |
| }; |
| module_platform_driver(loongson_se_driver); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Yinggang Gu <guyinggang@loongson.cn>"); |
| MODULE_AUTHOR("Qunqin Zhao <zhaoqunqin@loongson.cn>"); |
| MODULE_DESCRIPTION("Loongson Security Engine chip controller driver"); |