| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * Copyright (C) 2025 Advanced Micro Devices, Inc. |
| */ |
| |
| #include <linux/irq.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/vmalloc.h> |
| #include <media/v4l2-ioctl.h> |
| |
| #include "isp4.h" |
| #include "isp4_debug.h" |
| #include "isp4_hw_reg.h" |
| |
| #define ISP4_DRV_NAME "amd_isp_capture" |
| #define ISP4_FW_RESP_RB_IRQ_STATUS_MASK \ |
| (ISP_SYS_INT0_STATUS__SYS_INT_RINGBUFFER_WPT9_INT_MASK | \ |
| ISP_SYS_INT0_STATUS__SYS_INT_RINGBUFFER_WPT12_INT_MASK) |
| |
| static const struct { |
| const char *name; |
| u32 status_mask; |
| u32 en_mask; |
| u32 ack_mask; |
| u32 rb_int_num; |
| } isp4_irq[ISP4SD_MAX_FW_RESP_STREAM_NUM] = { |
| /* The IRQ order is aligned with the isp4_subdev.fw_resp_thread order */ |
| { |
| .name = "isp_irq_global", |
| .status_mask = |
| ISP_SYS_INT0_STATUS__SYS_INT_RINGBUFFER_WPT12_INT_MASK, |
| .en_mask = ISP_SYS_INT0_EN__SYS_INT_RINGBUFFER_WPT12_EN_MASK, |
| .ack_mask = ISP_SYS_INT0_ACK__SYS_INT_RINGBUFFER_WPT12_ACK_MASK, |
| .rb_int_num = 4, /* ISP_4_1__SRCID__ISP_RINGBUFFER_WPT12 */ |
| }, |
| { |
| .name = "isp_irq_stream1", |
| .status_mask = |
| ISP_SYS_INT0_STATUS__SYS_INT_RINGBUFFER_WPT9_INT_MASK, |
| .en_mask = ISP_SYS_INT0_EN__SYS_INT_RINGBUFFER_WPT9_EN_MASK, |
| .ack_mask = ISP_SYS_INT0_ACK__SYS_INT_RINGBUFFER_WPT9_ACK_MASK, |
| .rb_int_num = 0, /* ISP_4_1__SRCID__ISP_RINGBUFFER_WPT9 */ |
| }, |
| }; |
| |
| void isp4_intr_enable(struct isp4_subdev *isp_subdev, u32 index, bool enable) |
| { |
| u32 intr_en; |
| |
| /* Synchronize ISP_SYS_INT0_EN writes with the IRQ handler's writes */ |
| spin_lock_irq(&isp_subdev->irq_lock); |
| intr_en = isp4hw_rreg(isp_subdev->mmio, ISP_SYS_INT0_EN); |
| if (enable) |
| intr_en |= isp4_irq[index].en_mask; |
| else |
| intr_en &= ~isp4_irq[index].en_mask; |
| |
| isp4hw_wreg(isp_subdev->mmio, ISP_SYS_INT0_EN, intr_en); |
| spin_unlock_irq(&isp_subdev->irq_lock); |
| } |
| |
| static void isp4_wake_up_resp_thread(struct isp4_subdev *isp_subdev, u32 index) |
| { |
| struct isp4sd_thread_handler *thread_ctx = |
| &isp_subdev->fw_resp_thread[index]; |
| |
| thread_ctx->resp_ready = true; |
| wake_up_interruptible(&thread_ctx->waitq); |
| } |
| |
| static irqreturn_t isp4_irq_handler(int irq, void *arg) |
| { |
| struct isp4_subdev *isp_subdev = arg; |
| u32 intr_ack = 0, intr_en = 0, intr_status; |
| int seen = 0; |
| |
| /* Get the ISP_SYS interrupt status */ |
| intr_status = isp4hw_rreg(isp_subdev->mmio, ISP_SYS_INT0_STATUS); |
| intr_status &= ISP4_FW_RESP_RB_IRQ_STATUS_MASK; |
| |
| /* Find which ISP_SYS interrupts fired */ |
| for (size_t i = 0; i < ARRAY_SIZE(isp4_irq); i++) { |
| if (intr_status & isp4_irq[i].status_mask) { |
| intr_ack |= isp4_irq[i].ack_mask; |
| intr_en |= isp4_irq[i].en_mask; |
| seen |= BIT(i); |
| } |
| } |
| |
| /* |
| * Disable the ISP_SYS interrupts that fired. Must be done before waking |
| * the response threads, since they re-enable interrupts when finished. |
| * The lock synchronizes RMW of INT0_EN with isp4_enable_interrupt(). |
| */ |
| spin_lock(&isp_subdev->irq_lock); |
| intr_en = isp4hw_rreg(isp_subdev->mmio, ISP_SYS_INT0_EN) & ~intr_en; |
| isp4hw_wreg(isp_subdev->mmio, ISP_SYS_INT0_EN, intr_en); |
| spin_unlock(&isp_subdev->irq_lock); |
| |
| /* |
| * Clear the ISP_SYS interrupts. This must be done after the interrupts |
| * are disabled, so that ISP FW won't flag any new interrupts on these |
| * streams, and thus we don't need to clear interrupts again before |
| * re-enabling them in the response thread. |
| */ |
| isp4hw_wreg(isp_subdev->mmio, ISP_SYS_INT0_ACK, intr_ack); |
| |
| /* |
| * The operation `(seen >> i) << i` is logically equivalent to |
| * `seen &= ~BIT(i)`, with fewer instructions after compilation. |
| */ |
| for (int i; (i = ffs(seen)); seen = (seen >> i) << i) |
| isp4_wake_up_resp_thread(isp_subdev, i - 1); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int isp4_capture_probe(struct platform_device *pdev) |
| { |
| int irq[ISP4SD_MAX_FW_RESP_STREAM_NUM]; |
| struct device *dev = &pdev->dev; |
| struct isp4_subdev *isp_subdev; |
| struct isp4_device *isp_dev; |
| int ret; |
| |
| isp_dev = devm_kzalloc(dev, sizeof(*isp_dev), GFP_KERNEL); |
| if (!isp_dev) |
| return -ENOMEM; |
| |
| dev->init_name = ISP4_DRV_NAME; |
| |
| isp_subdev = &isp_dev->isp_subdev; |
| isp_subdev->mmio = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(isp_subdev->mmio)) |
| return dev_err_probe(dev, PTR_ERR(isp_subdev->mmio), |
| "isp ioremap fail\n"); |
| |
| for (size_t i = 0; i < ARRAY_SIZE(isp4_irq); i++) { |
| irq[i] = platform_get_irq(pdev, isp4_irq[i].rb_int_num); |
| if (irq[i] < 0) |
| return dev_err_probe(dev, irq[i], |
| "fail to get irq %d\n", |
| isp4_irq[i].rb_int_num); |
| |
| ret = devm_request_irq(dev, irq[i], isp4_irq_handler, |
| IRQF_NO_AUTOEN, isp4_irq[i].name, |
| isp_subdev); |
| if (ret) |
| return dev_err_probe(dev, ret, "fail to req irq %d\n", |
| irq[i]); |
| } |
| |
| isp_dev->v4l2_dev.mdev = &isp_dev->mdev; |
| |
| strscpy(isp_dev->mdev.model, "amd_isp41_mdev", |
| sizeof(isp_dev->mdev.model)); |
| isp_dev->mdev.dev = dev; |
| media_device_init(&isp_dev->mdev); |
| |
| snprintf(isp_dev->v4l2_dev.name, sizeof(isp_dev->v4l2_dev.name), |
| "AMD-V4L2-ROOT"); |
| ret = v4l2_device_register(dev, &isp_dev->v4l2_dev); |
| if (ret) { |
| dev_err_probe(dev, ret, "fail register v4l2 device\n"); |
| goto err_clean_media; |
| } |
| |
| pm_runtime_set_suspended(dev); |
| pm_runtime_enable(dev); |
| spin_lock_init(&isp_subdev->irq_lock); |
| ret = isp4sd_init(&isp_dev->isp_subdev, &isp_dev->v4l2_dev, irq); |
| if (ret) { |
| dev_err_probe(dev, ret, "fail init isp4 sub dev\n"); |
| goto err_pm_disable; |
| } |
| |
| ret = media_create_pad_link(&isp_dev->isp_subdev.sdev.entity, |
| 0, |
| &isp_dev->isp_subdev.isp_vdev.vdev.entity, |
| 0, |
| MEDIA_LNK_FL_ENABLED | |
| MEDIA_LNK_FL_IMMUTABLE); |
| if (ret) { |
| dev_err_probe(dev, ret, "fail to create pad link\n"); |
| goto err_isp4_deinit; |
| } |
| |
| ret = media_device_register(&isp_dev->mdev); |
| if (ret) { |
| dev_err_probe(dev, ret, "fail to register media device\n"); |
| goto err_isp4_deinit; |
| } |
| |
| platform_set_drvdata(pdev, isp_dev); |
| isp_debugfs_create(isp_dev); |
| |
| return 0; |
| |
| err_isp4_deinit: |
| isp4sd_deinit(&isp_dev->isp_subdev); |
| err_pm_disable: |
| pm_runtime_disable(dev); |
| v4l2_device_unregister(&isp_dev->v4l2_dev); |
| err_clean_media: |
| media_device_cleanup(&isp_dev->mdev); |
| |
| return ret; |
| } |
| |
| static void isp4_capture_remove(struct platform_device *pdev) |
| { |
| struct isp4_device *isp_dev = platform_get_drvdata(pdev); |
| struct device *dev = &pdev->dev; |
| |
| isp_debugfs_remove(isp_dev); |
| |
| media_device_unregister(&isp_dev->mdev); |
| isp4sd_deinit(&isp_dev->isp_subdev); |
| pm_runtime_disable(dev); |
| v4l2_device_unregister(&isp_dev->v4l2_dev); |
| media_device_cleanup(&isp_dev->mdev); |
| } |
| |
| static struct platform_driver isp4_capture_drv = { |
| .probe = isp4_capture_probe, |
| .remove = isp4_capture_remove, |
| .driver = { |
| .name = ISP4_DRV_NAME, |
| } |
| }; |
| |
| module_platform_driver(isp4_capture_drv); |
| |
| MODULE_ALIAS("platform:" ISP4_DRV_NAME); |
| MODULE_IMPORT_NS("DMA_BUF"); |
| |
| MODULE_DESCRIPTION("AMD ISP4 Driver"); |
| MODULE_AUTHOR("Bin Du <bin.du@amd.com>"); |
| MODULE_AUTHOR("Pratap Nirujogi <pratap.nirujogi@amd.com>"); |
| MODULE_LICENSE("GPL"); |