From patchwork Thu May 3 09:35:49 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "H. Nikolaus Schaller" X-Patchwork-Id: 10377515 Return-Path: Received: from mail.wl.linuxfoundation.org (pdx-wl-mail.web.codeaurora.org [172.30.200.125]) by pdx-korg-patchwork.web.codeaurora.org (Postfix) with ESMTP id 7859760327 for ; Thu, 3 May 2018 09:41:38 +0000 (UTC) Received: from mail.wl.linuxfoundation.org (localhost [127.0.0.1]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id 7162B2907B for ; Thu, 3 May 2018 09:41:38 +0000 (UTC) Received: by mail.wl.linuxfoundation.org (Postfix, from userid 486) id 6492B29082; Thu, 3 May 2018 09:41:38 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on pdx-wl-mail.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-2.9 required=2.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,MAILING_LIST_MULTI autolearn=unavailable version=3.3.1 Received: from bombadil.infradead.org (bombadil.infradead.org [198.137.202.133]) (using TLSv1.2 with cipher AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.wl.linuxfoundation.org (Postfix) with ESMTPS id 62BA72907B for ; Thu, 3 May 2018 09:41:37 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=lists.infradead.org; s=bombadil.20170209; h=Sender: Content-Transfer-Encoding:Content-Type:MIME-Version:Cc:List-Subscribe: List-Help:List-Post:List-Archive:List-Unsubscribe:List-Id:References: In-Reply-To:Message-Id:Date:Subject:To:From:Reply-To:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Owner; bh=LwGI8kkzKOPrmersvMSiGdQj0riZYfEPf92tu/c0BZ0=; b=szvM4f2hKB23iFAqqtpwko3OJx RYBRz5u/jI8TXSh7IMu8nVKTW9Y89NJXgXqhg8IcSmLfEmM6CdkSrj1Jxa09CtzIxgibeSYwsbAgc 9YDwXCJgNS3DdEPABaaaqC4UpBbVH89sGuIj91Vea/aQUnnSwAxfe+ij8qFDn+TU+JOMTACh9odj/ X2EUPx7L+81vTCDuNWB2HGILo8FgCJanz6ksSweyfWeyUmnloFiaIxkLtIxpua+zSRH5b2zUVLdHF X5cgsc6vYlJ3T3oqYNyWVw6IXjWtdKr4hj4oj8UgvxNTZq6sWbggvpWcbNkVa1hTOlqD8xSe7lJzA KAxtfMZQ==; Received: from localhost ([127.0.0.1] helo=bombadil.infradead.org) by bombadil.infradead.org with esmtp (Exim 4.90_1 #2 (Red Hat Linux)) id 1fEAju-0003lN-L1; Thu, 03 May 2018 09:41:26 +0000 Received: from mo6-p03-ob.smtp.rzone.de ([2a01:238:20a:202:5303::5]) by bombadil.infradead.org with esmtps (Exim 4.90_1 #2 (Red Hat Linux)) id 1fEAf0-0000GI-27 for linux-arm-kernel@lists.infradead.org; Thu, 03 May 2018 09:36:27 +0000 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; t=1525340157; s=strato-dkim-0002; d=goldelico.com; h=References:In-Reply-To:References:In-Reply-To:Message-Id:Date: Subject:Cc:To:From:X-RZG-CLASS-ID:X-RZG-AUTH:From:Subject:Sender; bh=HJmIhBnfD38E/aASNo9WT4FW67a9F/FhzOW8BFc7n8M=; b=LusL8VdmMuLkW9gmAYmtE+acIN+p8Q8UyAtmc/yk2FqFJp61j9SO8oiE4U5Fyp2GcZ ILpJqLqiQHQ5MEDs7BZwDGTEfjaHywNjw4Eq14qcaPLs2K3yYSAg3YWnffHZ3zA0jAI3 ygXi6iqQOg1QN2Yv9ZFJkwqflJ71MS7BBwPW0OHKPMQWjNuchoKRvrfmd02niAuuLWeL VGbaWdbWEiI6lkyekPMTE2taWp7ibanozZoNvM/vYgmboEE+AIPBdF6qBzMtVvlGc/gZ ShVw41aDFRVuEjxn9wleBEQHIUisDPEetFN6QJACRAdVDBGjWm4IBj8T4UEUTlRzn3DK GPug== X-RZG-AUTH: ":JGIXVUS7cutRB/49FwqZ7WcJeFKiMhflhwDubTJ9o12DNO4Ij0Nfyq4sHQ==" X-RZG-CLASS-ID: mo00 Received: from localhost.localdomain by smtp.strato.de (RZmta 43.8 DYNA|AUTH) with ESMTPSA id Y0796du439Zu48U (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (curve secp521r1 with 521 ECDH bits, eq. 15360 bits RSA)) (Client did not present a certificate); Thu, 3 May 2018 11:35:56 +0200 (CEST) From: "H. Nikolaus Schaller" To: Johan Hovold , afd@ti.com, Rob Herring , Mark Rutland , =?UTF-8?q?Beno=C3=AEt=20Cousson?= , Tony Lindgren , Russell King , Arnd Bergmann , Greg Kroah-Hartman , "H. Nikolaus Schaller" , Kevin Hilman , =?UTF-8?q?Andreas=20F=C3=A4rber?= , Thierry Reding , Jonathan Cameron , =?UTF-8?q?Noralf=20Tr=C3=B8nnes?= , David Lechner Subject: [PATCH v7 4/6] misc gnss serdev: Add w2sg0004 power control driver Date: Thu, 3 May 2018 11:35:49 +0200 Message-Id: <5dc006ea2fde674ba89969fd8aa542d26a157fd9.1525340143.git.hns@goldelico.com> X-Mailer: git-send-email 2.12.2 In-Reply-To: References: In-Reply-To: References: X-CRM114-Version: 20100106-BlameMichelson ( TRE 0.8.0 (BSD) ) MR-646709E3 X-CRM114-CacheID: sfid-20180503_023622_478924_3043C898 X-CRM114-Status: GOOD ( 29.58 ) X-BeenThere: linux-arm-kernel@lists.infradead.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: devicetree@vger.kernel.org, linux-omap@vger.kernel.org, linux-kernel@vger.kernel.org, kernel@pyra-handheld.com, letux-kernel@openphoenux.org, linux-arm-kernel@lists.infradead.org MIME-Version: 1.0 Sender: "linux-arm-kernel" Errors-To: linux-arm-kernel-bounces+patchwork-linux-arm=patchwork.kernel.org@lists.infradead.org X-Virus-Scanned: ClamAV using ClamSMTP Add driver for Wi2Wi W2SG0004 GPS module connected to some SoC UART. This chip is an older version of modern SirfStar modules and is lacking a WAKEUP output. Hence we need a different strategy to determine the real power state. This driver uses serdev API hooks to monitor the UART traffic before forwarding to the gnss framework. Especially, it detects the situation where the module is turned on (sends data) but should be off. This can happen if it was already turned on during (re)boot or power-on-reset. Additionally, "rfkill block/unblock gps" is supported to control power of an external LNA. The driver concept is based on code developed by Neil Brown but strongly simplified and heavily adapted to use the new serdev API introduced in v4.11 and the upcoming GNSS framework. Signed-off-by: H. Nikolaus Schaller --- drivers/gnss/w2sg0004.c | 435 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 drivers/gnss/w2sg0004.c diff --git a/drivers/gnss/w2sg0004.c b/drivers/gnss/w2sg0004.c new file mode 100644 index 000000000000..7e844ab7b31a --- /dev/null +++ b/drivers/gnss/w2sg0004.c @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Driver for power controlling the w2sg0004 GPS receiver. + * + * Copyright (C) 2013 Neil Brown + * Copyright (C) 2015-2018 H. Nikolaus Schaller , + * Golden Delicious Computers + * + * This receiver has an ON/OFF pin which must be toggled to + * turn the device 'on' or 'off'. A high->low->high toggle + * will switch the device on if it is off, and off if it is on. + * + * Contrary to newer Sirf based modules is not possible to directly + * detect the power state of the w2sg0004. + * + * However, when it is on it will send characters on a UART line + * regularly. + * + * To detect that the power state is out of sync (e.g. if GPS + * was enabled before a reboot), we monitor the serdev data stream + * and compare with what the driver thinks about the state. + * + * In addition we register as a rfkill client so that we can + * control the LNA power. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * There seems to be restrictions on how quickly we can toggle the + * on/off line. data sheets says "two rtc ticks", whatever that means. + * If we do it too soon it doesn't work. + * So we have a state machine which uses the common work queue to ensure + * clean transitions. + * When a change is requested we record that request and only act on it + * once the previous change has completed. + * A change involves a 10ms low pulse, and a 990ms raised level, so only + * one change per second. + */ + +enum w2sg_state { + W2SG_IDLE, /* is not changing state */ + W2SG_PULSE, /* activate on/off impulse */ + W2SG_NOPULSE /* deactivate on/off impulse */ +}; + +struct w2sg_data { + struct gnss_device *gdev; + struct regulator *lna_regulator; + struct gpio_desc *on_off_gpio; /* the on-off gpio */ + struct rfkill *rf_kill; + struct serdev_device *uart; /* uart connected to the chip */ + bool lna_blocked; /* rfkill block gps is active */ + bool lna_is_off; /* LNA is currently off */ + bool is_on; /* current power state (0/1) */ + unsigned long last_toggle; + unsigned long backoff; /* time to wait since last_toggle */ + enum w2sg_state state; /* state engine state */ + bool requested; /* requested power state (0/1) */ + bool suspended; + struct delayed_work work; + int discard_count; +}; + +static int w2sg_set_lna_power(struct w2sg_data *data) +{ + int ret = 0; + bool off = data->suspended || !data->requested || data->lna_blocked; + + if (off != data->lna_is_off) { + data->lna_is_off = off; + if (!IS_ERR_OR_NULL(data->lna_regulator)) { + if (off) + regulator_disable(data->lna_regulator); + else + ret = regulator_enable(data->lna_regulator); + } + } + + return ret; +} + +static void w2sg_set_power(struct w2sg_data *data, bool val) +{ + if (val && !data->requested) { + data->requested = true; + } else if (!val && data->requested) { + data->backoff = HZ; + data->requested = false; + } else + return; + + if (!data->suspended) + schedule_delayed_work(&data->work, 0); +} + +/* called each time data is received by the UART (i.e. sent by the w2sg0004) */ +static int w2sg_uart_receive_buf(struct serdev_device *serdev, + const unsigned char *rxdata, + size_t count) +{ + struct w2sg_data *data = + (struct w2sg_data *) serdev_device_get_drvdata(serdev); + + if (!data->requested && !data->is_on) { + /* + * we have received characters while the w2sg + * should have been be turned off + */ + data->discard_count += count; + if ((data->state == W2SG_IDLE) && + time_after(jiffies, + data->last_toggle + data->backoff)) { + /* Should be off by now, time to toggle again */ + dev_dbg(&serdev->dev, "w2sg00x4 has sent %d characters data although it should be off!\n", + data->discard_count); + + data->discard_count = 0; + + data->is_on = true; + data->backoff *= 2; + if (!data->suspended) + schedule_delayed_work(&data->work, 0); + } + return count; + } + + /* + * pass to user-space + */ + + if (data->requested) + return gnss_insert_raw(data->gdev, rxdata, count); + + data->discard_count += count; + + return count; +} + +/* try to toggle the power state by sending a pulse to the on-off GPIO */ +static void toggle_work(struct work_struct *work) +{ + struct w2sg_data *data = container_of(work, struct w2sg_data, + work.work); + + w2sg_set_lna_power(data); /* update LNA power state */ + + switch (data->state) { + case W2SG_IDLE: + if (data->requested == data->is_on) + return; + + gpiod_set_value_cansleep(data->on_off_gpio, 1); + data->state = W2SG_PULSE; + + schedule_delayed_work(&data->work, + msecs_to_jiffies(10)); + break; + + case W2SG_PULSE: + gpiod_set_value_cansleep(data->on_off_gpio, 0); + data->last_toggle = jiffies; + data->state = W2SG_NOPULSE; + data->is_on = !data->is_on; + + schedule_delayed_work(&data->work, + msecs_to_jiffies(10)); + break; + + case W2SG_NOPULSE: + data->state = W2SG_IDLE; + + break; + + } +} + +static int w2sg_rfkill_set_block(void *pdata, bool blocked) +{ + struct w2sg_data *data = pdata; + + data->lna_blocked = blocked; + + if (!data->suspended) + schedule_delayed_work(&data->work, 0); + + return 0; +} + +static struct rfkill_ops w2sg0004_rfkill_ops = { + .set_block = w2sg_rfkill_set_block, +}; + +static struct serdev_device_ops serdev_ops = { + .receive_buf = w2sg_uart_receive_buf, +}; + +static int w2sg_gps_open(struct gnss_device *gdev) +{ /* user-space has opened our interface */ + struct w2sg_data *data = gnss_get_drvdata(gdev); + + w2sg_set_power(data, true); + + return 0; +} + +static void w2sg_gps_close(struct gnss_device *gdev) +{ /* user-space has finally closed our interface */ + struct w2sg_data *data = gnss_get_drvdata(gdev); + + w2sg_set_power(data, false); +} + +static int w2sg_gps_send(struct gnss_device *gdev, + const unsigned char *buffer, size_t count) +{ /* raw data coming from user space */ + struct w2sg_data *data = gnss_get_drvdata(gdev); + + /* simply pass down to UART */ + return serdev_device_write_buf(data->uart, buffer, count); +} + +static const struct gnss_operations w2sg_gnss_ops = { + .open = w2sg_gps_open, + .close = w2sg_gps_close, + .write_raw = w2sg_gps_send, +}; + +static int w2sg_probe(struct serdev_device *serdev) +{ + struct w2sg_data *data; + struct rfkill *rf_kill; + struct gnss_device *gdev; + int err; + + data = devm_kzalloc(&serdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->on_off_gpio = devm_gpiod_get_index(&serdev->dev, + "enable", 0, + GPIOD_OUT_LOW); + if (IS_ERR(data->on_off_gpio)) { + /* defer until we have the gpio */ + if (PTR_ERR(data->on_off_gpio) == -EPROBE_DEFER) + return -EPROBE_DEFER; + dev_err(&serdev->dev, "could not get the enable-gpio"); + return PTR_ERR(data->on_off_gpio); + } + + gpiod_direction_output(data->on_off_gpio, false); + + data->lna_regulator = devm_regulator_get_optional(&serdev->dev, + "lna"); + if (IS_ERR(data->lna_regulator)) { + /* defer until we can get the regulator */ + if (PTR_ERR(data->lna_regulator) == -EPROBE_DEFER) + return -EPROBE_DEFER; + + data->lna_regulator = NULL; /* ignore other errors */ + } + + gdev = gnss_allocate_device(&serdev->dev); + if (!gdev) + return -ENOMEM; + + gdev->ops = &w2sg_gnss_ops; + gnss_set_drvdata(gdev, data); + + data->gdev = gdev; + data->uart = serdev; + + data->lna_blocked = true; + data->lna_is_off = true; + + data->is_on = false; + data->requested = false; + data->state = W2SG_IDLE; + data->last_toggle = jiffies; + data->backoff = HZ; + + INIT_DELAYED_WORK(&data->work, toggle_work); + + serdev_device_set_drvdata(serdev, data); + serdev_device_set_client_ops(data->uart, &serdev_ops); + + err = serdev_device_open(data->uart); + if (err < 0) + goto err_put_gnss; + + err = serdev_device_set_baudrate(data->uart, 9600); + if (err < 0) + goto err_close_serdev; + + serdev_device_set_flow_control(data->uart, false); + + err = gnss_register_device(gdev); + if (err < 0) + goto err_close_serdev; + + rf_kill = rfkill_alloc("GPS", &serdev->dev, RFKILL_TYPE_GPS, + &w2sg0004_rfkill_ops, data); + if (!rf_kill) { + err = -ENOMEM; + goto err_deregister_gnss; + } + + err = rfkill_register(rf_kill); + if (err) { + dev_err(&serdev->dev, "Cannot register rfkill device\n"); + goto err_destroy_rfkill; + } + + data->rf_kill = rf_kill; + + /* keep off until user space requests the device */ + w2sg_set_power(data, false); + + return 0; + +err_destroy_rfkill: + rfkill_destroy(data->rf_kill); + +err_deregister_gnss: + gnss_deregister_device(data->gdev); + +err_close_serdev: + serdev_device_close(data->uart); + +err_put_gnss: + gnss_put_device(data->gdev); + + return err; +} + +static void w2sg_remove(struct serdev_device *serdev) +{ + struct w2sg_data *data = serdev_device_get_drvdata(serdev); + + rfkill_destroy(data->rf_kill); + + gnss_deregister_device(data->gdev); + + cancel_delayed_work_sync(&data->work); + + serdev_device_close(data->uart); + + gnss_put_device(data->gdev); +} + +static int __maybe_unused w2sg_suspend(struct device *dev) +{ + struct w2sg_data *data = dev_get_drvdata(dev); + + data->suspended = true; + + cancel_delayed_work_sync(&data->work); + + w2sg_set_lna_power(data); /* shuts down if needed */ + + if (data->state == W2SG_PULSE) { + msleep(10); + gpiod_set_value_cansleep(data->on_off_gpio, 0); + data->last_toggle = jiffies; + data->is_on = !data->is_on; + data->state = W2SG_NOPULSE; + } + + if (data->state == W2SG_NOPULSE) { + msleep(10); + data->state = W2SG_IDLE; + } + + if (data->is_on) { + gpiod_set_value_cansleep(data->on_off_gpio, 1); + msleep(10); + gpiod_set_value_cansleep(data->on_off_gpio, 0); + data->is_on = false; + } + + return 0; +} + +static int __maybe_unused w2sg_resume(struct device *dev) +{ + struct w2sg_data *data = dev_get_drvdata(dev); + + data->suspended = false; + + schedule_delayed_work(&data->work, 0); /* enables LNA if needed */ + + return 0; +} + +static const struct of_device_id w2sg0004_of_match[] = { + { .compatible = "wi2wi,w2sg0004" }, + {}, +}; +MODULE_DEVICE_TABLE(of, w2sg0004_of_match); + +SIMPLE_DEV_PM_OPS(w2sg_pm_ops, w2sg_suspend, w2sg_resume); + +static struct serdev_device_driver w2sg_driver = { + .probe = w2sg_probe, + .remove = w2sg_remove, + .driver = { + .name = "w2sg0004", + .owner = THIS_MODULE, + .pm = &w2sg_pm_ops, + .of_match_table = of_match_ptr(w2sg0004_of_match) + }, +}; + +module_serdev_device_driver(w2sg_driver); + +MODULE_AUTHOR("NeilBrown "); +MODULE_AUTHOR("H. Nikolaus Schaller "); +MODULE_DESCRIPTION("w2sg0004 GPS power management driver"); +MODULE_LICENSE("GPL v2");