| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * stf_camss.c |
| * |
| * Starfive Camera Subsystem driver |
| * |
| * Copyright (C) 2021-2023 StarFive Technology Co., Ltd. |
| * |
| * Author: Jack Zhu <jack.zhu@starfivetech.com> |
| * Author: Changhuang Liang <changhuang.liang@starfivetech.com> |
| * |
| */ |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/of_graph.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/videodev2.h> |
| #include <media/v4l2-fwnode.h> |
| #include <media/v4l2-mc.h> |
| |
| #include "stf-camss.h" |
| |
| static const char * const stfcamss_clocks[] = { |
| "wrapper_clk_c", |
| "ispcore_2x", |
| "isp_axi", |
| }; |
| |
| static const char * const stfcamss_resets[] = { |
| "wrapper_p", |
| "wrapper_c", |
| "axiwr", |
| "isp_top_n", |
| "isp_top_axi", |
| }; |
| |
| static const struct stf_isr_data stf_isrs[] = { |
| {"wr_irq", stf_wr_irq_handler}, |
| {"isp_irq", stf_isp_irq_handler}, |
| {"line_irq", stf_line_irq_handler}, |
| }; |
| |
| static int stfcamss_get_mem_res(struct stfcamss *stfcamss) |
| { |
| struct platform_device *pdev = to_platform_device(stfcamss->dev); |
| |
| stfcamss->syscon_base = |
| devm_platform_ioremap_resource_byname(pdev, "syscon"); |
| if (IS_ERR(stfcamss->syscon_base)) |
| return PTR_ERR(stfcamss->syscon_base); |
| |
| stfcamss->isp_base = devm_platform_ioremap_resource_byname(pdev, "isp"); |
| if (IS_ERR(stfcamss->isp_base)) |
| return PTR_ERR(stfcamss->isp_base); |
| |
| return 0; |
| } |
| |
| /* |
| * stfcamss_of_parse_endpoint_node - Parse port endpoint node |
| * @dev: Device |
| * @node: Device node to be parsed |
| * @csd: Parsed data from port endpoint node |
| * |
| * Return 0 on success or a negative error code on failure |
| */ |
| static int stfcamss_of_parse_endpoint_node(struct stfcamss *stfcamss, |
| struct device_node *node, |
| struct stfcamss_async_subdev *csd) |
| { |
| struct v4l2_fwnode_endpoint vep = { { 0 } }; |
| int ret; |
| |
| ret = v4l2_fwnode_endpoint_parse(of_fwnode_handle(node), &vep); |
| if (ret) { |
| dev_err(stfcamss->dev, "endpoint not defined at %pOF\n", node); |
| return ret; |
| } |
| |
| csd->port = vep.base.port; |
| |
| return 0; |
| } |
| |
| /* |
| * stfcamss_of_parse_ports - Parse ports node |
| * @stfcamss: STFCAMSS device |
| * |
| * Return number of "port" nodes found in "ports" node |
| */ |
| static int stfcamss_of_parse_ports(struct stfcamss *stfcamss) |
| { |
| struct device_node *node = NULL; |
| int ret, num_subdevs = 0; |
| |
| for_each_endpoint_of_node(stfcamss->dev->of_node, node) { |
| struct stfcamss_async_subdev *csd; |
| |
| if (!of_device_is_available(node)) |
| continue; |
| |
| csd = v4l2_async_nf_add_fwnode_remote(&stfcamss->notifier, |
| of_fwnode_handle(node), |
| struct stfcamss_async_subdev); |
| if (IS_ERR(csd)) { |
| ret = PTR_ERR(csd); |
| dev_err(stfcamss->dev, "failed to add async notifier\n"); |
| goto err_cleanup; |
| } |
| |
| ret = stfcamss_of_parse_endpoint_node(stfcamss, node, csd); |
| if (ret) |
| goto err_cleanup; |
| |
| num_subdevs++; |
| } |
| |
| return num_subdevs; |
| |
| err_cleanup: |
| of_node_put(node); |
| return ret; |
| } |
| |
| static int stfcamss_register_devs(struct stfcamss *stfcamss) |
| { |
| struct stf_capture *cap_yuv = &stfcamss->captures[STF_CAPTURE_YUV]; |
| struct stf_isp_dev *isp_dev = &stfcamss->isp_dev; |
| int ret; |
| |
| ret = stf_isp_register(isp_dev, &stfcamss->v4l2_dev); |
| if (ret < 0) { |
| dev_err(stfcamss->dev, |
| "failed to register stf isp%d entity: %d\n", 0, ret); |
| return ret; |
| } |
| |
| ret = stf_capture_register(stfcamss, &stfcamss->v4l2_dev); |
| if (ret < 0) { |
| dev_err(stfcamss->dev, |
| "failed to register capture: %d\n", ret); |
| goto err_isp_unregister; |
| } |
| |
| ret = media_create_pad_link(&isp_dev->subdev.entity, STF_ISP_PAD_SRC, |
| &cap_yuv->video.vdev.entity, 0, 0); |
| if (ret) |
| goto err_cap_unregister; |
| |
| cap_yuv->video.source_subdev = &isp_dev->subdev; |
| |
| return ret; |
| |
| err_cap_unregister: |
| stf_capture_unregister(stfcamss); |
| err_isp_unregister: |
| stf_isp_unregister(&stfcamss->isp_dev); |
| |
| return ret; |
| } |
| |
| static void stfcamss_unregister_devs(struct stfcamss *stfcamss) |
| { |
| stf_isp_unregister(&stfcamss->isp_dev); |
| stf_capture_unregister(stfcamss); |
| } |
| |
| static int stfcamss_subdev_notifier_bound(struct v4l2_async_notifier *async, |
| struct v4l2_subdev *subdev, |
| struct v4l2_async_connection *asc) |
| { |
| struct stfcamss *stfcamss = |
| container_of(async, struct stfcamss, notifier); |
| struct stfcamss_async_subdev *csd = |
| container_of(asc, struct stfcamss_async_subdev, asd); |
| enum stf_port_num port = csd->port; |
| struct stf_isp_dev *isp_dev = &stfcamss->isp_dev; |
| struct stf_capture *cap_raw = &stfcamss->captures[STF_CAPTURE_RAW]; |
| struct media_pad *pad; |
| int ret; |
| |
| if (port == STF_PORT_CSI2RX) { |
| pad = &isp_dev->pads[STF_ISP_PAD_SINK]; |
| } else { |
| dev_err(stfcamss->dev, "not support port %d\n", port); |
| return -EPERM; |
| } |
| |
| ret = v4l2_create_fwnode_links_to_pad(subdev, pad, 0); |
| if (ret) |
| return ret; |
| |
| ret = media_create_pad_link(&subdev->entity, 1, |
| &cap_raw->video.vdev.entity, 0, 0); |
| if (ret) |
| return ret; |
| |
| isp_dev->source_subdev = subdev; |
| cap_raw->video.source_subdev = subdev; |
| |
| return 0; |
| } |
| |
| static int stfcamss_subdev_notifier_complete(struct v4l2_async_notifier *ntf) |
| { |
| struct stfcamss *stfcamss = |
| container_of(ntf, struct stfcamss, notifier); |
| |
| return v4l2_device_register_subdev_nodes(&stfcamss->v4l2_dev); |
| } |
| |
| static const struct v4l2_async_notifier_operations |
| stfcamss_subdev_notifier_ops = { |
| .bound = stfcamss_subdev_notifier_bound, |
| .complete = stfcamss_subdev_notifier_complete, |
| }; |
| |
| static void stfcamss_mc_init(struct platform_device *pdev, |
| struct stfcamss *stfcamss) |
| { |
| stfcamss->media_dev.dev = stfcamss->dev; |
| strscpy(stfcamss->media_dev.model, "Starfive Camera Subsystem", |
| sizeof(stfcamss->media_dev.model)); |
| media_device_init(&stfcamss->media_dev); |
| |
| stfcamss->v4l2_dev.mdev = &stfcamss->media_dev; |
| } |
| |
| /* |
| * stfcamss_probe - Probe STFCAMSS platform device |
| * @pdev: Pointer to STFCAMSS platform device |
| * |
| * Return 0 on success or a negative error code on failure |
| */ |
| static int stfcamss_probe(struct platform_device *pdev) |
| { |
| struct stfcamss *stfcamss; |
| struct device *dev = &pdev->dev; |
| int ret, num_subdevs; |
| unsigned int i; |
| |
| stfcamss = devm_kzalloc(dev, sizeof(*stfcamss), GFP_KERNEL); |
| if (!stfcamss) |
| return -ENOMEM; |
| |
| stfcamss->dev = dev; |
| |
| for (i = 0; i < ARRAY_SIZE(stf_isrs); ++i) { |
| int irq; |
| |
| irq = platform_get_irq(pdev, i); |
| if (irq < 0) |
| return irq; |
| |
| ret = devm_request_irq(stfcamss->dev, irq, stf_isrs[i].isr, 0, |
| stf_isrs[i].name, stfcamss); |
| if (ret) { |
| dev_err(dev, "request irq failed: %d\n", ret); |
| return ret; |
| } |
| } |
| |
| stfcamss->nclks = ARRAY_SIZE(stfcamss->sys_clk); |
| for (i = 0; i < stfcamss->nclks; ++i) |
| stfcamss->sys_clk[i].id = stfcamss_clocks[i]; |
| ret = devm_clk_bulk_get(dev, stfcamss->nclks, stfcamss->sys_clk); |
| if (ret) { |
| dev_err(dev, "Failed to get clk controls\n"); |
| return ret; |
| } |
| |
| stfcamss->nrsts = ARRAY_SIZE(stfcamss->sys_rst); |
| for (i = 0; i < stfcamss->nrsts; ++i) |
| stfcamss->sys_rst[i].id = stfcamss_resets[i]; |
| ret = devm_reset_control_bulk_get_shared(dev, stfcamss->nrsts, |
| stfcamss->sys_rst); |
| if (ret) { |
| dev_err(dev, "Failed to get reset controls\n"); |
| return ret; |
| } |
| |
| ret = stfcamss_get_mem_res(stfcamss); |
| if (ret) { |
| dev_err(dev, "Could not map registers\n"); |
| return ret; |
| } |
| |
| platform_set_drvdata(pdev, stfcamss); |
| |
| v4l2_async_nf_init(&stfcamss->notifier, &stfcamss->v4l2_dev); |
| |
| num_subdevs = stfcamss_of_parse_ports(stfcamss); |
| if (num_subdevs < 0) { |
| ret = -ENODEV; |
| dev_err(dev, "Failed to get sub devices: %d\n", ret); |
| goto err_cleanup_notifier; |
| } |
| |
| ret = stf_isp_init(stfcamss); |
| if (ret < 0) { |
| dev_err(dev, "Failed to init isp: %d\n", ret); |
| goto err_cleanup_notifier; |
| } |
| |
| stfcamss_mc_init(pdev, stfcamss); |
| |
| ret = v4l2_device_register(stfcamss->dev, &stfcamss->v4l2_dev); |
| if (ret < 0) { |
| dev_err(dev, "Failed to register V4L2 device: %d\n", ret); |
| goto err_cleanup_media_device; |
| } |
| |
| ret = media_device_register(&stfcamss->media_dev); |
| if (ret) { |
| dev_err(dev, "Failed to register media device: %d\n", ret); |
| goto err_unregister_device; |
| } |
| |
| ret = stfcamss_register_devs(stfcamss); |
| if (ret < 0) { |
| dev_err(dev, "Failed to register subdevice: %d\n", ret); |
| goto err_unregister_media_dev; |
| } |
| |
| pm_runtime_enable(dev); |
| |
| stfcamss->notifier.ops = &stfcamss_subdev_notifier_ops; |
| ret = v4l2_async_nf_register(&stfcamss->notifier); |
| if (ret) { |
| dev_err(dev, "Failed to register async subdev nodes: %d\n", |
| ret); |
| pm_runtime_disable(dev); |
| goto err_unregister_subdevs; |
| } |
| |
| return 0; |
| |
| err_unregister_subdevs: |
| stfcamss_unregister_devs(stfcamss); |
| err_unregister_media_dev: |
| media_device_unregister(&stfcamss->media_dev); |
| err_unregister_device: |
| v4l2_device_unregister(&stfcamss->v4l2_dev); |
| err_cleanup_media_device: |
| media_device_cleanup(&stfcamss->media_dev); |
| err_cleanup_notifier: |
| v4l2_async_nf_cleanup(&stfcamss->notifier); |
| return ret; |
| } |
| |
| /* |
| * stfcamss_remove - Remove STFCAMSS platform device |
| * @pdev: Pointer to STFCAMSS platform device |
| * |
| * Always returns 0. |
| */ |
| static int stfcamss_remove(struct platform_device *pdev) |
| { |
| struct stfcamss *stfcamss = platform_get_drvdata(pdev); |
| |
| stfcamss_unregister_devs(stfcamss); |
| v4l2_device_unregister(&stfcamss->v4l2_dev); |
| media_device_cleanup(&stfcamss->media_dev); |
| v4l2_async_nf_cleanup(&stfcamss->notifier); |
| pm_runtime_disable(&pdev->dev); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id stfcamss_of_match[] = { |
| { .compatible = "starfive,jh7110-camss" }, |
| { /* sentinel */ }, |
| }; |
| |
| MODULE_DEVICE_TABLE(of, stfcamss_of_match); |
| |
| static int __maybe_unused stfcamss_runtime_suspend(struct device *dev) |
| { |
| struct stfcamss *stfcamss = dev_get_drvdata(dev); |
| int ret; |
| |
| ret = reset_control_bulk_assert(stfcamss->nrsts, stfcamss->sys_rst); |
| if (ret) { |
| dev_err(dev, "reset assert failed\n"); |
| return ret; |
| } |
| |
| clk_bulk_disable_unprepare(stfcamss->nclks, stfcamss->sys_clk); |
| |
| return 0; |
| } |
| |
| static int __maybe_unused stfcamss_runtime_resume(struct device *dev) |
| { |
| struct stfcamss *stfcamss = dev_get_drvdata(dev); |
| int ret; |
| |
| ret = clk_bulk_prepare_enable(stfcamss->nclks, stfcamss->sys_clk); |
| if (ret) { |
| dev_err(dev, "clock prepare enable failed\n"); |
| return ret; |
| } |
| |
| ret = reset_control_bulk_deassert(stfcamss->nrsts, stfcamss->sys_rst); |
| if (ret < 0) { |
| dev_err(dev, "cannot deassert resets\n"); |
| clk_bulk_disable_unprepare(stfcamss->nclks, stfcamss->sys_clk); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static const struct dev_pm_ops stfcamss_pm_ops = { |
| SET_RUNTIME_PM_OPS(stfcamss_runtime_suspend, |
| stfcamss_runtime_resume, |
| NULL) |
| }; |
| |
| static struct platform_driver stfcamss_driver = { |
| .probe = stfcamss_probe, |
| .remove = stfcamss_remove, |
| .driver = { |
| .name = "starfive-camss", |
| .pm = &stfcamss_pm_ops, |
| .of_match_table = stfcamss_of_match, |
| }, |
| }; |
| |
| module_platform_driver(stfcamss_driver); |
| |
| MODULE_AUTHOR("Jack Zhu <jack.zhu@starfivetech.com>"); |
| MODULE_AUTHOR("Changhuang Liang <changhuang.liang@starfivetech.com>"); |
| MODULE_DESCRIPTION("StarFive Camera Subsystem driver"); |
| MODULE_LICENSE("GPL"); |