| // SPDX-License-Identifier: GPL-2.0 | |
| #include <linux/delay.h> | |
| #include <linux/err.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/io.h> | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/of.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/seq_file.h> | |
| #include <linux/slab.h> | |
| #include <linux/spmi.h> | |
| /* | |
| * SPMI register addr | |
| */ | |
| #define SPMI_CHANNEL_OFFSET 0x0300 | |
| #define SPMI_SLAVE_OFFSET 0x20 | |
| #define SPMI_APB_SPMI_CMD_BASE_ADDR 0x0100 | |
| #define SPMI_APB_SPMI_WDATA0_BASE_ADDR 0x0104 | |
| #define SPMI_APB_SPMI_WDATA1_BASE_ADDR 0x0108 | |
| #define SPMI_APB_SPMI_WDATA2_BASE_ADDR 0x010c | |
| #define SPMI_APB_SPMI_WDATA3_BASE_ADDR 0x0110 | |
| #define SPMI_APB_SPMI_STATUS_BASE_ADDR 0x0200 | |
| #define SPMI_APB_SPMI_RDATA0_BASE_ADDR 0x0204 | |
| #define SPMI_APB_SPMI_RDATA1_BASE_ADDR 0x0208 | |
| #define SPMI_APB_SPMI_RDATA2_BASE_ADDR 0x020c | |
| #define SPMI_APB_SPMI_RDATA3_BASE_ADDR 0x0210 | |
| #define SPMI_PER_DATAREG_BYTE 4 | |
| /* | |
| * SPMI cmd register | |
| */ | |
| #define SPMI_APB_SPMI_CMD_EN BIT(31) | |
| #define SPMI_APB_SPMI_CMD_TYPE_OFFSET 24 | |
| #define SPMI_APB_SPMI_CMD_LENGTH_OFFSET 20 | |
| #define SPMI_APB_SPMI_CMD_SLAVEID_OFFSET 16 | |
| #define SPMI_APB_SPMI_CMD_ADDR_OFFSET 0 | |
| /* Command Opcodes */ | |
| enum spmi_controller_cmd_op_code { | |
| SPMI_CMD_REG_ZERO_WRITE = 0, | |
| SPMI_CMD_REG_WRITE = 1, | |
| SPMI_CMD_REG_READ = 2, | |
| SPMI_CMD_EXT_REG_WRITE = 3, | |
| SPMI_CMD_EXT_REG_READ = 4, | |
| SPMI_CMD_EXT_REG_WRITE_L = 5, | |
| SPMI_CMD_EXT_REG_READ_L = 6, | |
| SPMI_CMD_REG_RESET = 7, | |
| SPMI_CMD_REG_SLEEP = 8, | |
| SPMI_CMD_REG_SHUTDOWN = 9, | |
| SPMI_CMD_REG_WAKEUP = 10, | |
| }; | |
| /* | |
| * SPMI status register | |
| */ | |
| #define SPMI_APB_TRANS_DONE BIT(0) | |
| #define SPMI_APB_TRANS_FAIL BIT(2) | |
| /* Command register fields */ | |
| #define SPMI_CONTROLLER_CMD_MAX_BYTE_COUNT 16 | |
| /* Maximum number of support PMIC peripherals */ | |
| #define SPMI_CONTROLLER_TIMEOUT_US 1000 | |
| #define SPMI_CONTROLLER_MAX_TRANS_BYTES 16 | |
| struct spmi_controller_dev { | |
| struct spmi_controller *controller; | |
| struct device *dev; | |
| void __iomem *base; | |
| spinlock_t lock; | |
| u32 channel; | |
| }; | |
| static int spmi_controller_wait_for_done(struct device *dev, | |
| struct spmi_controller_dev *ctrl_dev, | |
| void __iomem *base, u8 sid, u16 addr) | |
| { | |
| u32 timeout = SPMI_CONTROLLER_TIMEOUT_US; | |
| u32 status, offset; | |
| offset = SPMI_APB_SPMI_STATUS_BASE_ADDR; | |
| offset += SPMI_CHANNEL_OFFSET * ctrl_dev->channel + SPMI_SLAVE_OFFSET * sid; | |
| do { | |
| status = readl(base + offset); | |
| if (status & SPMI_APB_TRANS_DONE) { | |
| if (status & SPMI_APB_TRANS_FAIL) { | |
| dev_err(dev, "%s: transaction failed (0x%x)\n", | |
| __func__, status); | |
| return -EIO; | |
| } | |
| dev_dbg(dev, "%s: status 0x%x\n", __func__, status); | |
| return 0; | |
| } | |
| udelay(1); | |
| } while (timeout--); | |
| dev_err(dev, "%s: timeout, status 0x%x\n", __func__, status); | |
| return -ETIMEDOUT; | |
| } | |
| static int spmi_read_cmd(struct spmi_controller *ctrl, | |
| u8 opc, u8 slave_id, u16 slave_addr, u8 *__buf, size_t bc) | |
| { | |
| struct spmi_controller_dev *spmi_controller = dev_get_drvdata(&ctrl->dev); | |
| u32 chnl_ofst = SPMI_CHANNEL_OFFSET * spmi_controller->channel; | |
| unsigned long flags; | |
| u8 *buf = __buf; | |
| u32 cmd, data; | |
| int rc; | |
| u8 op_code, i; | |
| if (bc > SPMI_CONTROLLER_MAX_TRANS_BYTES) { | |
| dev_err(&ctrl->dev, | |
| "spmi_controller supports 1..%d bytes per trans, but:%zu requested\n", | |
| SPMI_CONTROLLER_MAX_TRANS_BYTES, bc); | |
| return -EINVAL; | |
| } | |
| switch (opc) { | |
| case SPMI_CMD_READ: | |
| op_code = SPMI_CMD_REG_READ; | |
| break; | |
| case SPMI_CMD_EXT_READ: | |
| op_code = SPMI_CMD_EXT_REG_READ; | |
| break; | |
| case SPMI_CMD_EXT_READL: | |
| op_code = SPMI_CMD_EXT_REG_READ_L; | |
| break; | |
| default: | |
| dev_err(&ctrl->dev, "invalid read cmd 0x%x\n", opc); | |
| return -EINVAL; | |
| } | |
| cmd = SPMI_APB_SPMI_CMD_EN | | |
| (op_code << SPMI_APB_SPMI_CMD_TYPE_OFFSET) | | |
| ((bc - 1) << SPMI_APB_SPMI_CMD_LENGTH_OFFSET) | | |
| ((slave_id & 0xf) << SPMI_APB_SPMI_CMD_SLAVEID_OFFSET) | /* slvid */ | |
| ((slave_addr & 0xffff) << SPMI_APB_SPMI_CMD_ADDR_OFFSET); /* slave_addr */ | |
| spin_lock_irqsave(&spmi_controller->lock, flags); | |
| writel(cmd, spmi_controller->base + chnl_ofst + SPMI_APB_SPMI_CMD_BASE_ADDR); | |
| rc = spmi_controller_wait_for_done(&ctrl->dev, spmi_controller, | |
| spmi_controller->base, slave_id, slave_addr); | |
| if (rc) | |
| goto done; | |
| for (i = 0; bc > i * SPMI_PER_DATAREG_BYTE; i++) { | |
| data = readl(spmi_controller->base + chnl_ofst + | |
| SPMI_SLAVE_OFFSET * slave_id + | |
| SPMI_APB_SPMI_RDATA0_BASE_ADDR + | |
| i * SPMI_PER_DATAREG_BYTE); | |
| data = be32_to_cpu((__be32 __force)data); | |
| if ((bc - i * SPMI_PER_DATAREG_BYTE) >> 2) { | |
| memcpy(buf, &data, sizeof(data)); | |
| buf += sizeof(data); | |
| } else { | |
| memcpy(buf, &data, bc % SPMI_PER_DATAREG_BYTE); | |
| buf += (bc % SPMI_PER_DATAREG_BYTE); | |
| } | |
| } | |
| done: | |
| spin_unlock_irqrestore(&spmi_controller->lock, flags); | |
| if (rc) | |
| dev_err(&ctrl->dev, | |
| "spmi read wait timeout op:0x%x slave_id:%d slave_addr:0x%x bc:%zu\n", | |
| opc, slave_id, slave_addr, bc + 1); | |
| else | |
| dev_dbg(&ctrl->dev, "%s: id:%d slave_addr:0x%x, read value: %*ph\n", | |
| __func__, slave_id, slave_addr, (int)bc, __buf); | |
| return rc; | |
| } | |
| static int spmi_write_cmd(struct spmi_controller *ctrl, | |
| u8 opc, u8 slave_id, u16 slave_addr, const u8 *__buf, size_t bc) | |
| { | |
| struct spmi_controller_dev *spmi_controller = dev_get_drvdata(&ctrl->dev); | |
| u32 chnl_ofst = SPMI_CHANNEL_OFFSET * spmi_controller->channel; | |
| const u8 *buf = __buf; | |
| unsigned long flags; | |
| u32 cmd, data; | |
| int rc; | |
| u8 op_code, i; | |
| if (bc > SPMI_CONTROLLER_MAX_TRANS_BYTES) { | |
| dev_err(&ctrl->dev, | |
| "spmi_controller supports 1..%d bytes per trans, but:%zu requested\n", | |
| SPMI_CONTROLLER_MAX_TRANS_BYTES, bc); | |
| return -EINVAL; | |
| } | |
| switch (opc) { | |
| case SPMI_CMD_WRITE: | |
| op_code = SPMI_CMD_REG_WRITE; | |
| break; | |
| case SPMI_CMD_EXT_WRITE: | |
| op_code = SPMI_CMD_EXT_REG_WRITE; | |
| break; | |
| case SPMI_CMD_EXT_WRITEL: | |
| op_code = SPMI_CMD_EXT_REG_WRITE_L; | |
| break; | |
| default: | |
| dev_err(&ctrl->dev, "invalid write cmd 0x%x\n", opc); | |
| return -EINVAL; | |
| } | |
| cmd = SPMI_APB_SPMI_CMD_EN | | |
| (op_code << SPMI_APB_SPMI_CMD_TYPE_OFFSET) | | |
| ((bc - 1) << SPMI_APB_SPMI_CMD_LENGTH_OFFSET) | | |
| ((slave_id & 0xf) << SPMI_APB_SPMI_CMD_SLAVEID_OFFSET) | | |
| ((slave_addr & 0xffff) << SPMI_APB_SPMI_CMD_ADDR_OFFSET); | |
| /* Write data to FIFOs */ | |
| spin_lock_irqsave(&spmi_controller->lock, flags); | |
| for (i = 0; bc > i * SPMI_PER_DATAREG_BYTE; i++) { | |
| data = 0; | |
| if ((bc - i * SPMI_PER_DATAREG_BYTE) >> 2) { | |
| memcpy(&data, buf, sizeof(data)); | |
| buf += sizeof(data); | |
| } else { | |
| memcpy(&data, buf, bc % SPMI_PER_DATAREG_BYTE); | |
| buf += (bc % SPMI_PER_DATAREG_BYTE); | |
| } | |
| writel((u32 __force)cpu_to_be32(data), | |
| spmi_controller->base + chnl_ofst + | |
| SPMI_APB_SPMI_WDATA0_BASE_ADDR + | |
| SPMI_PER_DATAREG_BYTE * i); | |
| } | |
| /* Start the transaction */ | |
| writel(cmd, spmi_controller->base + chnl_ofst + SPMI_APB_SPMI_CMD_BASE_ADDR); | |
| rc = spmi_controller_wait_for_done(&ctrl->dev, spmi_controller, | |
| spmi_controller->base, slave_id, | |
| slave_addr); | |
| spin_unlock_irqrestore(&spmi_controller->lock, flags); | |
| if (rc) | |
| dev_err(&ctrl->dev, "spmi write wait timeout op:0x%x slave_id:%d slave_addr:0x%x bc:%zu\n", | |
| opc, slave_id, slave_addr, bc); | |
| else | |
| dev_dbg(&ctrl->dev, "%s: id:%d slave_addr:0x%x, wrote value: %*ph\n", | |
| __func__, slave_id, slave_addr, (int)bc, __buf); | |
| return rc; | |
| } | |
| static int spmi_controller_probe(struct platform_device *pdev) | |
| { | |
| struct spmi_controller_dev *spmi_controller; | |
| struct spmi_controller *ctrl; | |
| struct resource *iores; | |
| int ret; | |
| ctrl = devm_spmi_controller_alloc(&pdev->dev, sizeof(*spmi_controller)); | |
| if (IS_ERR(ctrl)) { | |
| dev_err(&pdev->dev, "can not allocate spmi_controller data\n"); | |
| return PTR_ERR(ctrl); | |
| } | |
| spmi_controller = spmi_controller_get_drvdata(ctrl); | |
| spmi_controller->controller = ctrl; | |
| iores = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
| if (!iores) { | |
| dev_err(&pdev->dev, "can not get resource!\n"); | |
| return -EINVAL; | |
| } | |
| spmi_controller->base = devm_ioremap(&pdev->dev, iores->start, | |
| resource_size(iores)); | |
| if (!spmi_controller->base) { | |
| dev_err(&pdev->dev, "can not remap base addr!\n"); | |
| return -EADDRNOTAVAIL; | |
| } | |
| ret = of_property_read_u32(pdev->dev.of_node, "hisilicon,spmi-channel", | |
| &spmi_controller->channel); | |
| if (ret) { | |
| dev_err(&pdev->dev, "can not get channel\n"); | |
| return -ENODEV; | |
| } | |
| platform_set_drvdata(pdev, spmi_controller); | |
| dev_set_drvdata(&ctrl->dev, spmi_controller); | |
| spin_lock_init(&spmi_controller->lock); | |
| /* Callbacks */ | |
| ctrl->read_cmd = spmi_read_cmd; | |
| ctrl->write_cmd = spmi_write_cmd; | |
| ret = devm_spmi_controller_add(&pdev->dev, ctrl); | |
| if (ret) { | |
| dev_err(&pdev->dev, "spmi_controller_add failed with error %d!\n", ret); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static const struct of_device_id spmi_controller_match_table[] = { | |
| { | |
| .compatible = "hisilicon,kirin970-spmi-controller", | |
| }, | |
| {} | |
| }; | |
| MODULE_DEVICE_TABLE(of, spmi_controller_match_table); | |
| static struct platform_driver spmi_controller_driver = { | |
| .probe = spmi_controller_probe, | |
| .driver = { | |
| .name = "hisi_spmi_controller", | |
| .of_match_table = spmi_controller_match_table, | |
| }, | |
| }; | |
| static int __init spmi_controller_init(void) | |
| { | |
| return platform_driver_register(&spmi_controller_driver); | |
| } | |
| postcore_initcall(spmi_controller_init); | |
| static void __exit spmi_controller_exit(void) | |
| { | |
| platform_driver_unregister(&spmi_controller_driver); | |
| } | |
| module_exit(spmi_controller_exit); | |
| MODULE_DESCRIPTION("Hisilicon 3670 SPMI Controller driver"); | |
| MODULE_LICENSE("GPL v2"); | |
| MODULE_VERSION("1.0"); | |
| MODULE_ALIAS("platform:spmi_controller"); |