| /* |
| * GPIO edge detection driver for ASR eCall transaction |
| * |
| * Copyright 2021 ASR Microelectronics (Shanghai) Co., Ltd. |
| * |
| * This file is subject to the terms and conditions of the GNU General |
| * Public License. See the file "COPYING" in the main directory of this |
| * archive for more details. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| */ |
| |
| #include <linux/version.h> |
| #include <linux/module.h> |
| #include <linux/kernel.h> |
| #include <linux/fs.h> |
| #include <linux/gpio.h> |
| #include <linux/uaccess.h> |
| #include <linux/delay.h> |
| #include <linux/edge_wakeup_mmp.h> |
| #include <linux/workqueue.h> |
| #include <linux/interrupt.h> |
| #include <linux/device.h> |
| #include <linux/platform_device.h> |
| #include <linux/slab.h> |
| #include <stddef.h> |
| |
| #define ECALL_DEVICE_NUM 2 |
| struct ecall_event_device { |
| char name[16]; |
| struct device *dev; |
| int state; |
| int gpio; |
| int irq; |
| struct delayed_work work; |
| struct workqueue_struct *wq; |
| spinlock_t lock; |
| }; |
| |
| static struct class *ecall_event_class; |
| static struct ecall_event_device *guedevice[ECALL_DEVICE_NUM]; |
| static const char* gpio_name[ECALL_DEVICE_NUM] = { |
| "gpio-auto-ecall", |
| "gpio-manual-ecall", |
| }; |
| |
| static void report_ecall_event(struct ecall_event_device *uedev, int state) |
| { |
| char name_buf[50]; |
| char *env[3]; |
| |
| snprintf(name_buf, sizeof(name_buf), "%s", uedev->name); |
| env[0] = name_buf; |
| env[1] = state ? "GPIO_UP" : "GPIO_DOWN"; |
| env[2] = NULL; |
| |
| if (state) |
| kobject_uevent_env(&uedev->dev->kobj, KOBJ_ONLINE, env); |
| else |
| kobject_uevent_env(&uedev->dev->kobj, KOBJ_OFFLINE, env); |
| printk(KERN_INFO"%s: uevent from %s [%s] is sent\n", |
| __func__, env[0], env[1]); |
| } |
| |
| static void ecall_event_work(struct work_struct *work) |
| { |
| struct ecall_event_device *uedev = container_of(to_delayed_work(work), struct ecall_event_device, work); |
| int state = gpio_get_value(uedev->gpio); |
| |
| if (state != uedev->state) { |
| uedev->state = state; |
| report_ecall_event(uedev, state); |
| } |
| } |
| |
| irqreturn_t ecall_event_handler(int irq, void *dev_id) |
| { |
| struct ecall_event_device *uedev = (struct ecall_event_device *)dev_id; |
| unsigned long flags = 0; |
| |
| spin_lock_irqsave(&uedev->lock, flags); |
| if (work_pending(&uedev->work.work)) |
| cancel_delayed_work(&uedev->work); |
| queue_delayed_work(uedev->wq, &uedev->work, HZ / 4); |
| spin_unlock_irqrestore(&uedev->lock, flags); |
| |
| printk(KERN_INFO"%s: irq[%d]\n", __func__, irq); |
| return IRQ_HANDLED; |
| } |
| |
| static ssize_t ecall_send_event(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct ecall_event_device *uedev = (struct ecall_event_device *)dev_get_drvdata(dev); |
| unsigned long state; |
| int ret = 0; |
| |
| ret = kstrtoul(buf, 10, &state); |
| report_ecall_event(uedev, (int)state); |
| if (ret) |
| return ret; |
| else |
| return count; |
| } |
| |
| static DEVICE_ATTR(send_event, 0220, NULL, ecall_send_event); |
| static struct device_attribute *ecall_event_attr[] = { |
| &dev_attr_send_event, |
| NULL |
| }; |
| |
| static int ecall_event_create_sys_device(struct device *dev) |
| { |
| int ret = 0; |
| struct device_attribute **attr = ecall_event_attr; |
| |
| for (; *attr; ++attr) { |
| ret = device_create_file(dev, *attr); |
| if (ret) |
| break; |
| } |
| |
| if (ret) { |
| for (--attr; attr >= ecall_event_attr; --attr) |
| device_remove_file(dev, *attr); |
| } |
| return 0; |
| } |
| |
| static int ecall_event_remove_sys_device(struct device *dev) |
| { |
| struct device_attribute **attr = ecall_event_attr; |
| |
| for (; *attr; ++attr) |
| device_remove_file(dev, *attr); |
| return 0; |
| } |
| |
| static int init_device(struct platform_device *pdev, int dev_num) |
| { |
| int ret = -1; |
| unsigned long tmp; |
| struct ecall_event_device *uedevice = guedevice[dev_num]; |
| |
| snprintf(uedevice->name, sizeof(uedevice->name) - 1, |
| "ecall%d", dev_num); |
| spin_lock_init(&uedevice->lock); |
| uedevice->dev = device_create(ecall_event_class, NULL, |
| MKDEV(0, dev_num), NULL, uedevice->name); |
| |
| if (IS_ERR(uedevice->dev)) { |
| printk(KERN_INFO"%s: create ecalldev failed!\n", __func__); |
| ret = PTR_ERR(uedevice->dev); |
| goto out; |
| } |
| |
| dev_set_drvdata(uedevice->dev, uedevice); |
| of_property_read_u32(pdev->dev.of_node, gpio_name[dev_num], &uedevice->gpio); |
| if (uedevice->gpio >= 0) { |
| tmp = dev_num; |
| ret = request_mfp_edge_wakeup(uedevice->gpio, |
| NULL, |
| (void *)tmp, &pdev->dev); |
| if (ret) { |
| dev_err(uedevice->dev, "failed to request edge wakeup.\n"); |
| goto edge_wakeup; |
| } |
| uedevice->state = gpio_get_value(uedevice->gpio); |
| } |
| |
| ret = ecall_event_create_sys_device(uedevice->dev); |
| if (ret < 0) { |
| printk(KERN_INFO"%s: create sys device failed!\n", __func__); |
| goto destroy_device; |
| } |
| |
| gpio_request(uedevice->gpio, uedevice->name); |
| gpio_direction_input(uedevice->gpio); |
| INIT_DELAYED_WORK(&uedevice->work, ecall_event_work); |
| uedevice->wq = create_workqueue(uedevice->name); |
| if (uedevice->wq == NULL) { |
| printk(KERN_INFO"%s: can't create work queue!\n", __func__); |
| ret = -ENOMEM; |
| goto free_gpio; |
| } |
| |
| uedevice->irq = gpio_to_irq(uedevice->gpio); |
| ret = request_irq(uedevice->irq, ecall_event_handler, |
| IRQF_SHARED | IRQF_TRIGGER_RISING | |
| IRQF_TRIGGER_FALLING | IRQF_ONESHOT, uedevice->name, |
| uedevice); |
| if (ret < 0) { |
| printk(KERN_INFO"%s: request irq failed!\n", __func__); |
| goto destroy_wq; |
| } |
| |
| device_init_wakeup(uedevice->dev, 1); |
| ret = 0; |
| goto out; |
| |
| destroy_wq: |
| destroy_workqueue(uedevice->wq); |
| free_gpio: |
| gpio_free(uedevice->gpio); |
| destroy_device: |
| device_destroy(ecall_event_class, MKDEV(0, dev_num)); |
| edge_wakeup: |
| if (uedevice->gpio >= 0) |
| remove_mfp_edge_wakeup(uedevice->gpio); |
| out: |
| return ret; |
| } |
| |
| static void deinit_device(int dev_num) |
| { |
| struct ecall_event_device *uedevice = guedevice[dev_num]; |
| |
| free_irq(uedevice->irq, uedevice); |
| if (uedevice->wq != NULL) |
| destroy_workqueue(uedevice->wq); |
| if (uedevice->gpio >= 0) |
| remove_mfp_edge_wakeup(uedevice->gpio); |
| gpio_free(uedevice->gpio); |
| ecall_event_remove_sys_device(uedevice->dev); |
| device_destroy(ecall_event_class, MKDEV(0, dev_num)); |
| } |
| |
| static int ecall_event_probe(struct platform_device *pdev) |
| { |
| int ret; |
| int dev_num; |
| |
| if (!ecall_event_class) |
| ecall_event_class = class_create(THIS_MODULE, "ecall_event"); |
| if (IS_ERR(ecall_event_class)) |
| return PTR_ERR(ecall_event_class); |
| |
| for (dev_num = 0; dev_num < ECALL_DEVICE_NUM; dev_num++) { |
| if (of_device_is_compatible(pdev->dev.of_node, "asr,ecall-event")) { |
| printk(KERN_INFO"%s: ecall-event\n", __func__); |
| } else { |
| printk(KERN_INFO"%s: unknown device\n", __func__); |
| goto deinit; |
| } |
| |
| guedevice[dev_num] = kzalloc(sizeof(struct ecall_event_device), GFP_KERNEL); |
| ret = init_device(pdev, dev_num); |
| if (ret < 0) |
| goto deinit; |
| } |
| |
| return 0; |
| |
| deinit: |
| deinit_device(dev_num); |
| class_destroy(ecall_event_class); |
| |
| return -1; |
| } |
| |
| static int ecall_event_remove(struct platform_device *pdev) |
| { |
| int i; |
| for (i = 0; i < ECALL_DEVICE_NUM; i++) |
| deinit_device(i); |
| class_destroy(ecall_event_class); |
| for (i = 0; i < ECALL_DEVICE_NUM; i++) { |
| if (guedevice[i]) |
| kfree(guedevice[i]); |
| } |
| return 0; |
| } |
| |
| static const struct of_device_id ecall_of_match[] = { |
| { .compatible = "asr,ecall-event",}, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, ecall_of_match); |
| |
| static struct platform_driver ecall_driver = { |
| .probe = ecall_event_probe, |
| .remove = ecall_event_remove, |
| .driver = { |
| .name = "ecall", |
| .owner = THIS_MODULE, |
| .of_match_table = ecall_of_match, |
| }, |
| }; |
| |
| module_platform_driver(ecall_driver); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Shuo Dai"); |
| MODULE_DESCRIPTION("ASR GPIO event for eCall"); |