| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * BBD PPS GPIO core file | |
| * | |
| * Copyright (C) 2024 Google LLC | |
| */ | |
| #include <linux/init.h> | |
| #include <linux/kernel.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/mod_devicetable.h> | |
| #include <linux/module.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/slab.h> | |
| #include <linux/gpio/consumer.h> | |
| #include <linux/list.h> | |
| #include <linux/property.h> | |
| #include <linux/timer.h> | |
| /* Info for each registered platform device */ | |
| struct bbd_pps_gpio_device_data { | |
| int irq; /* IRQ used as PPS source */ | |
| struct gpio_desc *gpio_pin; /* GPIO port descriptors */ | |
| __u32 assert_sequence; /* PPS assert event seq # */ | |
| struct timespec64 g_assert_elapsed_tu; /* PPS elapsed rt assert seq # */ | |
| bool assert_falling_edge; | |
| }; | |
| static ssize_t pps_assert_show(struct device *dev, | |
| struct device_attribute *attr, char *buf) | |
| { | |
| struct bbd_pps_gpio_device_data *bbd_pps_gpio = dev_get_drvdata(dev); | |
| return sysfs_emit(buf, "%lld.%09ld#%u\n", | |
| (long long)bbd_pps_gpio->g_assert_elapsed_tu.tv_sec, | |
| bbd_pps_gpio->g_assert_elapsed_tu.tv_nsec, | |
| bbd_pps_gpio->assert_sequence); | |
| } | |
| static DEVICE_ATTR_RO(pps_assert); | |
| /* Handle the PPS pulse in the interrupt handler */ | |
| static irqreturn_t bbd_pps_gpio_handler(int irq, void *data) | |
| { | |
| struct bbd_pps_gpio_device_data *bbd_pps_gpio_device = data; | |
| struct timespec64 ts64 = { .tv_sec = 0, .tv_nsec = 0 }; | |
| /* | |
| * Calculate the monotonic clock from the timespec clock and stores the result | |
| * in timespec64 format using ktime_get_boottime_ts64() for compatibility with | |
| * the Android sensor system. | |
| */ | |
| ktime_get_boottime_ts64(&ts64); | |
| bbd_pps_gpio_device->g_assert_elapsed_tu = ts64; | |
| bbd_pps_gpio_device->assert_sequence++; | |
| return IRQ_HANDLED; | |
| } | |
| static int bbd_pps_gpio_setup(struct device *dev) | |
| { | |
| struct bbd_pps_gpio_device_data *data = dev_get_drvdata(dev); | |
| data->gpio_pin = devm_gpiod_get(dev, NULL, GPIOD_IN); | |
| if (IS_ERR(data->gpio_pin)) | |
| return dev_err_probe(dev, PTR_ERR(data->gpio_pin), | |
| "failed to request PPS GPIO\n"); | |
| data->assert_falling_edge = | |
| device_property_read_bool(dev, "assert-falling-edge"); | |
| return 0; | |
| } | |
| /* | |
| * Different from the mainline PPS GPIO driver, only two things | |
| * 1. Probe the device-tree. | |
| * 2. Register the GPIO interrupt. | |
| */ | |
| static int bbd_pps_gpio_probe(struct platform_device *pdev) | |
| { | |
| struct bbd_pps_gpio_device_data *data; | |
| struct device *dev = &pdev->dev; | |
| int ret; | |
| unsigned long int_flags; | |
| /* allocate space for device info */ | |
| data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); | |
| if (!data) | |
| return -ENOMEM; | |
| dev_set_drvdata(dev, data); | |
| /* GPIO setup */ | |
| ret = bbd_pps_gpio_setup(dev); | |
| if (ret) | |
| return ret; | |
| /* IRQ setup */ | |
| ret = gpiod_to_irq(data->gpio_pin); | |
| if (ret < 0) { | |
| dev_err(dev, "failed to map GPIO to IRQ: %d\n", ret); | |
| return -EINVAL; | |
| } | |
| data->irq = ret; | |
| /* register IRQ interrupt handler */ | |
| int_flags = data->assert_falling_edge ? IRQF_TRIGGER_FALLING : | |
| IRQF_TRIGGER_RISING; | |
| ret = devm_request_irq(dev, data->irq, bbd_pps_gpio_handler, int_flags, | |
| "BBD_GPIO", data); | |
| if (ret) { | |
| dev_err(dev, "failed to acquire IRQ %d\n", data->irq); | |
| return -EINVAL; | |
| } | |
| if (device_create_file(dev, &dev_attr_pps_assert)) { | |
| dev_err(dev, "failed to create device file pps_assert"); | |
| return -EINVAL; | |
| } | |
| return 0; | |
| } | |
| static int bbd_pps_gpio_remove(struct platform_device *pdev) | |
| { | |
| struct bbd_pps_gpio_device_data *data = platform_get_drvdata(pdev); | |
| disable_irq_nosync(data->irq); | |
| dev_dbg(&pdev->dev, "removed IRQ %d as PPS source\n", data->irq); | |
| device_remove_file(&pdev->dev, &dev_attr_pps_assert); | |
| return 0; | |
| } | |
| static int bbd_pps_gpio_suspend(struct device *dev) | |
| { | |
| struct bbd_pps_gpio_device_data *data = dev_get_drvdata(dev); | |
| disable_irq_nosync(data->irq); | |
| return 0; | |
| } | |
| static int bbd_pps_gpio_resume(struct device *dev) | |
| { | |
| struct bbd_pps_gpio_device_data *data = dev_get_drvdata(dev); | |
| enable_irq(data->irq); | |
| return 0; | |
| } | |
| static const struct dev_pm_ops bbd_pps_pm_ops = { | |
| .suspend = bbd_pps_gpio_suspend, | |
| .resume = bbd_pps_gpio_resume, | |
| }; | |
| static const struct of_device_id pps_gpio_dt_ids[] = { | |
| { | |
| .compatible = "bbd-pps-gpio", | |
| }, | |
| { /* sentinel */ } | |
| }; | |
| MODULE_DEVICE_TABLE(of, pps_gpio_dt_ids); | |
| static struct platform_driver bbd_pps_gpio_driver = { | |
| .probe = bbd_pps_gpio_probe, | |
| .remove = bbd_pps_gpio_remove, | |
| .driver = { | |
| .name = "bbd_pps_gpio", | |
| .of_match_table = pps_gpio_dt_ids, | |
| .pm = &bbd_pps_pm_ops, | |
| }, | |
| }; | |
| /* | |
| * Module init stuff | |
| */ | |
| static int __init bbd_pps_gpio_init(void) | |
| { | |
| return platform_driver_register(&bbd_pps_gpio_driver); | |
| } | |
| static void __exit bbd_pps_gpio_exit(void) | |
| { | |
| platform_driver_unregister(&bbd_pps_gpio_driver); | |
| } | |
| module_init(bbd_pps_gpio_init); | |
| module_exit(bbd_pps_gpio_exit); | |
| MODULE_AUTHOR("Cheng Change <[email protected]>"); | |
| MODULE_DESCRIPTION("Special PPS GPIO Handler"); | |
| MODULE_LICENSE("GPL"); |