Re: [PATCH 3/3] TTY/slave: add driver for w2sg0004 GPS

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 




On Fri, 12 Dec 2014 08:59:44 +1100
, NeilBrown <neilb@xxxxxxx>
 wrote:
> This uart-attatched GPS device has a toggle which turns
> both on and off.  For reliable use we need to know what
> start it is in.
> 
> So it registers with the tty for recv events when the tty
> is open, and optionally configures the RX pin as a GPIO
> interrupt when the tty is closed.
> 
> If it detects data when it should be off, or a lack of data
> when it should be on, it toggles the line.
> 
> A regulator can also be configured to power the antenna.
> In this case an rfkill device is created.  When the rfkill
> is 'blocked' or the device is otherwise powered down,
> the regulator is turned off.
> 
> Signed-off-by: NeilBrown <neil@xxxxxxxxxx>
> ---
>  .../devicetree/bindings/serial/slave-w2sg0004.txt  |   35 ++
>  drivers/tty/slaves/Kconfig                         |    6 
>  drivers/tty/slaves/Makefile                        |    3 
>  drivers/tty/slaves/tty-w2sg0004.c                  |  412 ++++++++++++++++++++
>  4 files changed, 455 insertions(+), 1 deletion(-)
>  create mode 100644 Documentation/devicetree/bindings/serial/slave-w2sg0004.txt
>  create mode 100644 drivers/tty/slaves/tty-w2sg0004.c
> 
> diff --git a/Documentation/devicetree/bindings/serial/slave-w2sg0004.txt b/Documentation/devicetree/bindings/serial/slave-w2sg0004.txt
> new file mode 100644
> index 000000000000..c9e7838b3198
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/serial/slave-w2sg0004.txt

This isn't a binding for a serial device, it is a binding for a GPS
device, which happens to be tty attached.
Documentation/devicetree/bindings/gps perhaps?

> @@ -0,0 +1,35 @@
> +w2sg0004 UART-attached GPS receiver
> +
> +Required properties:
> +- compatbile: "tty,w2sg0004"

'tty' is the wrong prefix. It should be the vendor abbreviation for the
GPS vendor.

> +- gpios: gpio used to toggle 'on/off' pin
> +- interrupts: interrupt generated by RX pin when device should
> +              be idle
> +- pinctrl: "default", "idle"
> +	     "idle" setting is active when device should be off.
> +	     This can remap the RX pin to a GPIO interrupt.
> +
> +Optional properties:
> +- vdd-supply: regulator, e.g. to power antenna
> +
> +
> +The w2sg0004 uses a pin-toggle both to power-on and to
> +power-off, so the driver needs to detect what state it is in.
> +It does this by detecting characters on the RX line.
> +When it should be off, these can optionally be detected by a GPIO.
> +
> +The node for this device must be the child of a UART.

This may need some tweaking to be more portable. ie. if it is wired into
a platform with a separate gpio pin also wired to the rx line so that
pinctrl manipulation isn't needed. I would make the pinctrl settings
optional.

> +
> +Example:
> +&uart2 {
> +	gps {
> +		compatible = "tty,w2sg0004";
> +		vdd-supply = <&vsim>;
> +		gpios = <&gpio5 17 0>; /* GPIO_145 */
> +		interrupts-extended = <&gpio5 19 0>; /* GPIO_147 */
> +		/* When idle, switch RX to be an interrupt */
> +		pinctrl-names = "default", "idle";
> +		pinctrl-0 = <&uart2_pins>;
> +		pinctrl-1 = <&uart2_pins_rx_gpio>;
> +	};
> +};
> diff --git a/drivers/tty/slaves/Kconfig b/drivers/tty/slaves/Kconfig
> index 2dd1acf80f8c..7a669faaf02d 100644
> --- a/drivers/tty/slaves/Kconfig
> +++ b/drivers/tty/slaves/Kconfig
> @@ -9,4 +9,10 @@ if TTY_SLAVE
>  config TTY_SLAVE_REGULATOR
>  	tristate "TTY slave device powered by regulator"
>  
> +config TTY_SLAVE_W2SG0004
> +	tristate "W2SG0004 GPS on/off control"
> +	help
> +	  Enable on/off control of W2SG0004 GPS when attached
> +	  to a UART.
> +

Drivers/gps maybe? Do we have any other gps drivers in-tree?

>  endif
> diff --git a/drivers/tty/slaves/Makefile b/drivers/tty/slaves/Makefile
> index e636bf49f1cc..ba528fa27110 100644
> --- a/drivers/tty/slaves/Makefile
> +++ b/drivers/tty/slaves/Makefile
> @@ -1,2 +1,3 @@
>  
> -obj-$(CONFIG_TTY_SLAVE_REGULATOR) += tty-reg.o
> +obj-$(CONFIG_TTY_SLAVE_REGULATOR)	+= tty-reg.o
> +obj-$(CONFIG_TTY_SLAVE_W2SG0004)	+= tty-w2sg0004.o
> diff --git a/drivers/tty/slaves/tty-w2sg0004.c b/drivers/tty/slaves/tty-w2sg0004.c
> new file mode 100644
> index 000000000000..7c7ae479bf41
> --- /dev/null
> +++ b/drivers/tty/slaves/tty-w2sg0004.c
> @@ -0,0 +1,412 @@
> +/*
> + * tty-w2sg0004.c - tty-slave for  w2sg0004 GPS device
> + *
> + * Copyright (C) 2014  NeilBrown <neil@xxxxxxxxxx>
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License version 2 as
> + * published by the Free Software Foundation.
> + *
> + * The w2sg0004 is turned 'on' or 'off' by toggling a line, which
> + * is normally connected to a GPIO.  Thus you need to know the current
> + * state in order to determine how to achieve some particular state.
> + * The only way to detect the state is by detecting transitions on
> + * its TX line (our RX line).
> + * So this tty slave listens for 'recv' events and deduces the GPS is
> + * on if it has received one recently.
> + * If suitably configure, and if the hardware is capable, it also
> + * enables an interrupt (presumably via a GPIO connected to the RX
> + * line via pinctrl) when the tty is inactive and treat and interrupts
> + * as an indication that the device is 'on' and should be turned 'off'.
> + *
> + * Driver also listens for open/close and trys to turn the GPS on if it is
> + * off and the tty is open.  On final 'close', the GPS is then turned
> + * off.
> + *
> + * When the device is opened, the GPIO is toggled immediately and then
> + * again after 2 seconds of no data.  If there is still no data the
> + * toggle happens are 4, 8, 16 seconds etc.
> + *
> + * When the device is closed, the GPIO is toggled immediately and
> + * if interrupts are received after 1 second it is toggled again
> + * (and again and again with exponentially increasing delays while
> + * interrupts continue).
> + *
> + * If a regulator is configured (e.g. to power the antenna), that is
> + * enabled/disabled on open/close.
> + *
> + * During system suspend the GPS is turned off even if the tty is
> + * open.  No repeat attempts are made.
> + * Possibly it should be possible to keep the GPS on with some
> + * configuration.
> + */
> +
> +#include <linux/module.h>
> +#include <linux/slab.h>
> +#include <linux/err.h>
> +#include <linux/regulator/consumer.h>
> +#include <linux/gpio.h>
> +#include <linux/of_gpio.h>
> +#include <linux/interrupt.h>
> +#include <linux/platform_device.h>
> +#include <linux/tty.h>
> +#include <linux/delay.h>
> +#include <linux/pm_runtime.h>
> +#include <linux/rfkill.h>
> +
> +struct w2sg_data {
> +	int		gpio;
> +	int		irq;	/* irq line from RX pin when pinctrl
> +				 * set to 'idle' */
> +	struct regulator *reg;
> +
> +	unsigned long	last_toggle;	/* jiffies when last toggle completed. */
> +	unsigned long	backoff;	/* jiffies since last_toggle when
> +					 * we try again
> +					 */
> +	enum {Idle, Down, Up} state;	/* state-machine state. */
> +	bool		requested, is_on;
> +	bool		suspended;
> +	bool		reg_enabled;
> +
> +	struct delayed_work work;
> +	spinlock_t	lock;
> +	struct device	*dev;
> +
> +	struct rfkill	*rfkill;
> +};
> +
> +/*
> + * There seems to 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 10ms raised level.
> + */
> +
> +static void toggle_work(struct work_struct *work)
> +{
> +	struct w2sg_data *data = container_of(
> +		work, struct w2sg_data, work.work);
> +
> +	spin_lock_irq(&data->lock);
> +	switch (data->state) {
> +	case Up:
> +		data->state = Idle;
> +		if (data->requested == data->is_on)
> +			break;
> +		if (!data->requested)
> +			/* Assume it is off unless activity is detected */
> +			break;
> +		/* Try again in a while unless we get some activity */
> +		dev_dbg(data->dev, "Wait %dusec until retry\n",
> +		       jiffies_to_msecs(data->backoff));
> +		schedule_delayed_work(&data->work, data->backoff);
> +		break;
> +	case Idle:
> +		if (data->requested == data->is_on)
> +			break;
> +
> +		/* Time to toggle */
> +		dev_dbg(data->dev, "Starting toggle to turn %s\n",
> +			data->requested ? "on" : "off");
> +		data->state = Down;
> +		spin_unlock_irq(&data->lock);
> +		gpio_set_value_cansleep(data->gpio, 0);
> +		schedule_delayed_work(&data->work,
> +				      msecs_to_jiffies(10));
> +		return;
> +
> +	case Down:
> +		data->state = Up;
> +		data->last_toggle = jiffies;
> +		dev_dbg(data->dev, "Toggle completed, should be %s now.\n",
> +			data->is_on ? "off" : "on");
> +		data->is_on = ! data->is_on;
> +		spin_unlock_irq(&data->lock);
> +
> +		gpio_set_value_cansleep(data->gpio, 1);
> +		schedule_delayed_work(&data->work,
> +				      msecs_to_jiffies(10));
> +		return;
> +	}
> +	spin_unlock_irq(&data->lock);
> +}
> +
> +static irqreturn_t tty_w2_isr(int irq, void *dev_id)
> +{
> +	struct w2sg_data *data = dev_id;
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&data->lock, flags);
> +	if (!data->requested && !data->is_on && data->state == Idle &&
> +	    time_after(jiffies, data->last_toggle + data->backoff)) {
> +		data->is_on = 1;
> +		data->backoff *= 2;
> +		dev_dbg(data->dev, "Received data, must be on. Try to turn off\n");
> +		if (!data->suspended)
> +			schedule_delayed_work(&data->work, 0);
> +	}
> +	spin_unlock_irqrestore(&data->lock, flags);
> +	return IRQ_HANDLED;
> +}
> +
> +static int tty_w2_runtime_resume(struct device *slave)
> +{
> +	struct w2sg_data *data = dev_get_drvdata(slave);
> +	unsigned long flags;
> +
> +	if (!data->reg_enabled &&
> +	    data->reg &&
> +	    !rfkill_blocked(data->rfkill))
> +		if (regulator_enable(data->reg) == 0)
> +			data->reg_enabled = true;
> +
> +	spin_lock_irqsave(&data->lock, flags);
> +	if (!data->requested) {
> +		dev_dbg(data->dev, "Device open - turn GPS on\n");
> +		data->requested = true;
> +		data->backoff = HZ;
> +		if (data->irq) {
> +			disable_irq(data->irq);
> +			pinctrl_pm_select_default_state(slave);
> +		}
> +		if (!data->suspended && data->state == Idle)
> +			schedule_delayed_work(&data->work, 0);
> +	}
> +	spin_unlock_irqrestore(&data->lock, flags);
> +	return 0;
> +}
> +
> +static int tty_w2_rfkill_set_block(void *vdata, bool blocked)
> +{
> +	struct w2sg_data *data = vdata;
> +
> +	dev_dbg(data->dev, "rfkill_set_blocked %d\n", blocked);
> +	if (blocked && data->reg_enabled)
> +		if (regulator_disable(data->reg) == 0)
> +			data->reg_enabled = false;
> +	if (!blocked &&
> +	    !data->reg_enabled && data->reg &&
> +	    !pm_runtime_suspended(data->dev))
> +		if (regulator_enable(data->reg) == 0)
> +			data->reg_enabled = true;
> +	return 0;
> +}
> +
> +static struct rfkill_ops tty_w2_rfkill_ops = {
> +	.set_block = tty_w2_rfkill_set_block,
> +};
> +
> +static int tty_w2_runtime_suspend(struct device *slave)
> +{
> +	struct w2sg_data *data = dev_get_drvdata(slave);
> +	unsigned long flags;
> +
> +	dev_dbg(data->dev, "Device closed - turn GPS off\n");
> +	if (data->reg && data->reg_enabled)
> +		if (regulator_disable(data->reg) == 0)
> +			data->reg_enabled = false;
> +
> +	spin_lock_irqsave(&data->lock, flags);
> +	if (data->requested) {
> +		data->requested = false;
> +		data->backoff = HZ;
> +		if (data->irq) {
> +			pinctrl_pm_select_idle_state(slave);
> +			enable_irq(data->irq);
> +		}
> +		if (!data->suspended && data->state == Idle)
> +			schedule_delayed_work(&data->work, 0);
> +	}
> +	spin_unlock_irqrestore(&data->lock, flags);
> +	return 0;
> +}
> +
> +static int tty_w2_suspend(struct device *dev)
> +{
> +	/* Ignore incoming data and just turn device off.
> +	 * we cannot really wait for a separate thread to
> +	 * do things, so we disable that and do it all
> +	 * here
> +	 */
> +	struct w2sg_data *data = dev_get_drvdata(dev);
> +
> +	spin_lock_irq(&data->lock);
> +	data->suspended = true;
> +	spin_unlock_irq(&data->lock);
> +
> +	cancel_delayed_work_sync(&data->work);
> +	if (data->state == Down) {
> +		dev_dbg(data->dev, "Suspending while GPIO down - raising\n");
> +		msleep(10);
> +		gpio_set_value_cansleep(data->gpio, 1);
> +		data->last_toggle = jiffies;
> +		data->is_on = !data->is_on;
> +		data->state = Up;
> +	}
> +	if (data->state == Up) {
> +		msleep(10);
> +		data->state = Idle;
> +	}
> +	if (data->is_on) {
> +		dev_dbg(data->dev, "Suspending while GPS on: toggling\n");
> +		gpio_set_value_cansleep(data->gpio, 0);
> +		msleep(10);
> +		gpio_set_value_cansleep(data->gpio, 1);
> +		data->is_on = 0;
> +	}
> +	return 0;
> +}
> +
> +static int tty_w2_resume(struct device *dev)
> +{
> +	struct w2sg_data *data = dev_get_drvdata(dev);
> +
> +	spin_lock_irq(&data->lock);
> +	data->suspended = false;
> +	spin_unlock_irq(&data->lock);
> +	schedule_delayed_work(&data->work, 0);
> +	return 0;
> +}
> +
> +static const struct dev_pm_ops tty_w2_pm_ops = {
> +	SET_SYSTEM_SLEEP_PM_OPS(tty_w2_suspend, tty_w2_resume)
> +	SET_RUNTIME_PM_OPS(tty_w2_runtime_suspend,
> +			   tty_w2_runtime_resume,
> +			   NULL)
> +};
> +
> +static bool toggle_on_probe = false;
> +
> +static int tty_w2_probe(struct platform_device *pdev)
> +{
> +	struct w2sg_data *data;
> +	struct regulator *reg;
> +	int err;
> +
> +	if (pdev->dev.parent == NULL)
> +		return -ENODEV;
> +
> +	data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
> +	if (!data)
> +		return -ENOMEM;
> +
> +	reg = devm_regulator_get(&pdev->dev, "vdd");
> +	if (IS_ERR(reg)) {
> +		err = PTR_ERR(reg);
> +		if (err != -ENODEV)
> +			goto out;
> +	} else
> +		data->reg = reg;
> +
> +	data->irq = platform_get_irq(pdev, 0);
> +	if (data->irq < 0) {
> +		err = data->irq;
> +		goto out;
> +	}
> +	dev_dbg(&pdev->dev, "IRQ configured: %d\n", data->irq);
> +
> +	data->last_toggle = jiffies;
> +	data->backoff = HZ;
> +	data->state = Idle;
> +	data->gpio = of_get_named_gpio(pdev->dev.of_node, "gpios", 0);
> +	if (data->gpio < 0) {
> +		err = data->gpio;
> +		goto out;
> +	}
> +	dev_dbg(&pdev->dev, "GPIO configured: %d\n", data->gpio);
> +	spin_lock_init(&data->lock);
> +	INIT_DELAYED_WORK(&data->work, toggle_work);
> +	err = devm_gpio_request_one(&pdev->dev, data->gpio,
> +				    GPIOF_OUT_INIT_HIGH,
> +				    "tty-w2sg0004-on-off");
> +	if (err)
> +		goto out;
> +
> +	if (data->irq) {
> +		irq_set_status_flags(data->irq, IRQ_NOAUTOEN);
> +		err = devm_request_irq(&pdev->dev, data->irq, tty_w2_isr,
> +				       IRQF_TRIGGER_FALLING,
> +				       "tty-w2sg0004", data);
> +	}
> +	if (err)
> +		goto out;
> +
> +	if (data->reg) {
> +		data->rfkill = rfkill_alloc("GPS", &pdev->dev, RFKILL_TYPE_GPS,
> +					    &tty_w2_rfkill_ops, data);
> +		if (!data->rfkill) {
> +			err = -ENOMEM;
> +			goto out;
> +		}
> +		err = rfkill_register(data->rfkill);
> +		if (err) {
> +			dev_err(&pdev->dev, "Cannot register rfkill device");
> +			rfkill_destroy(data->rfkill);
> +			goto out;
> +		}
> +	}
> +	platform_set_drvdata(pdev, data);
> +	data->dev = &pdev->dev;
> +	pm_runtime_enable(&pdev->dev);
> +	if (data->irq) {
> +		pinctrl_pm_select_idle_state(&pdev->dev);
> +		enable_irq(data->irq);
> +	}
> +	if (toggle_on_probe) {
> +		dev_dbg(data->dev, "Performing initial toggle\n");
> +		gpio_set_value_cansleep(data->gpio, 0);
> +		msleep(10);
> +		gpio_set_value_cansleep(data->gpio, 1);
> +		msleep(10);
> +	}
> +out:
> +	dev_dbg(data->dev, "Probed: err=%d\n", err);
> +	return err;
> +}
> +module_param(toggle_on_probe, bool, 0);
> +MODULE_PARM_DESC(toggle_on_probe, "simulate power-on with GPS active");
> +
> +static int tty_w2_remove(struct platform_device *pdev)
> +{
> +	struct w2sg_data *data = dev_get_drvdata(&pdev->dev);
> +	if (data->rfkill) {
> +		rfkill_unregister(data->rfkill);
> +		rfkill_destroy(data->rfkill);
> +	}
> +	return 0;
> +}
> +
> +static struct of_device_id tty_w2_dt_ids[] = {
> +	{ .compatible = "tty,w2sg0004", },
> +	{}
> +};
> +
> +static struct platform_driver tty_w2_driver = {
> +	.driver.name	= "tty-w2sg0004",
> +	.driver.owner	= THIS_MODULE,
> +	.driver.pm	= &tty_w2_pm_ops,
> +	.driver.of_match_table = tty_w2_dt_ids,
> +	.probe		= tty_w2_probe,
> +	.remove		= tty_w2_remove,
> +};
> +
> +static int __init tty_w2_init(void)
> +{
> +	return platform_driver_register(&tty_w2_driver);
> +}
> +module_init(tty_w2_init);
> +
> +static void __exit tty_w2_exit(void)
> +{
> +	platform_driver_unregister(&tty_w2_driver);
> +}
> +module_exit(tty_w2_exit);

module_platform_driver() is your friend. :-)
> +
> +MODULE_AUTHOR("NeilBrown <neilb@xxxxxxx>");
> +MODULE_DESCRIPTION("Serial port device which turns on W2SG0004 GPS");
> +MODULE_LICENSE("GPL v2");
> 
> 

--
To unsubscribe from this list: send the line "unsubscribe devicetree" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html




[Index of Archives]     [Device Tree Compilter]     [Device Tree Spec]     [Linux Driver Backports]     [Video for Linux]     [Linux USB Devel]     [Linux PCI Devel]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [XFree86]     [Yosemite Backpacking]
  Powered by Linux