Hi! > Motorola is using a custom TS 27.010 based serial port line discipline > for various devices on the modem. These devices can be accessed on > dedicated channels using Linux kernel serdev-ngsm driver. > > For the GNSS on these devices, we need to kick the GNSS device at a > desired rate. Otherwise the GNSS device stops sending data after a > few minutes. The rate we poll data defaults to 1000 ms, and can be > specified with a module option rate_ms between 1 to 16 seconds. > > [Tony Lindgren did most of the work here, I just converted it to be > normal serdev.] > > Signed-off-by: Pavel Machek <pa...@ucw.cz>
20 days... ping...? Pavel > diff --git a/drivers/gnss/Kconfig b/drivers/gnss/Kconfig > index bd12e3d57baa..a7c449d8428c 100644 > --- a/drivers/gnss/Kconfig > +++ b/drivers/gnss/Kconfig > @@ -13,6 +13,14 @@ menuconfig GNSS > > if GNSS > > +config GNSS_MOTMDM > + tristate "Motorola Modem TS 27.010 serdev GNSS receiver support" > + depends on SERIAL_DEV_N_GSM > + help > + Say Y here if you have a Motorola modem using TS 27.010 line > + discipline for GNSS such as a Motorola Mapphone series device > + like Droid 4. > + > config GNSS_SERIAL > tristate > > diff --git a/drivers/gnss/Makefile b/drivers/gnss/Makefile > index 451f11401ecc..f5afc2c22a3b 100644 > --- a/drivers/gnss/Makefile > +++ b/drivers/gnss/Makefile > @@ -6,6 +6,9 @@ > obj-$(CONFIG_GNSS) += gnss.o > gnss-y := core.o > > +obj-$(CONFIG_GNSS_MOTMDM) += gnss-motmdm.o > +gnss-motmdm-y := motmdm.o > + > obj-$(CONFIG_GNSS_SERIAL) += gnss-serial.o > gnss-serial-y := serial.o > > diff --git a/drivers/gnss/motmdm.c b/drivers/gnss/motmdm.c > new file mode 100644 > index 000000000000..bfd50e130631 > --- /dev/null > +++ b/drivers/gnss/motmdm.c > @@ -0,0 +1,409 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * Motorola Modem TS 27.010 serdev GNSS driver > + * > + * Copyright (C) 2018 - 2020 Tony Lindgren <t...@atomide.com> > + * Copyright (C) 2020 - 2021 Pavel Machek <pa...@ucw.cz> > + * > + * Based on drivers/gnss/sirf.c driver example: > + * Copyright (C) 2018 Johan Hovold <jo...@kernel.org> > + */ > + > +#include <linux/errno.h> > +#include <linux/gnss.h> > +#include <linux/init.h> > +#include <linux/kernel.h> > +#include <linux/module.h> > +#include <linux/of.h> > +#include <linux/platform_device.h> > +#include <linux/serdev-gsm.h> > +#include <linux/slab.h> > + > +#define MOTMDM_GNSS_TIMEOUT 1000 > +#define MOTMDM_GNSS_RATE 1000 > + > +/* > + * Motorola MDM GNSS device communicates over a dedicated TS 27.010 channel > + * using custom data packets. The packets look like AT commands embedded into > + * a Motorola invented packet using format like "U1234AT+MPDSTART=0,1,100,0". > + * But it's not an AT compatible serial interface, it's a packet interface > + * using AT like commands. > + */ > +#define MOTMDM_GNSS_HEADER_LEN 5 /* > U1234 */ > +#define MOTMDM_GNSS_RESP_LEN (MOTMDM_GNSS_HEADER_LEN + 4) /* U1234+MPD */ > +#define MOTMDM_GNSS_DATA_LEN (MOTMDM_GNSS_RESP_LEN + 1) /* U1234~+MPD */ > +#define MOTMDM_GNSS_STATUS_LEN (MOTMDM_GNSS_DATA_LEN + 7) /* > STATUS= */ > +#define MOTMDM_GNSS_NMEA_LEN (MOTMDM_GNSS_DATA_LEN + 8) /* NMEA=NN, */ > + > +enum motmdm_gnss_status { > + MOTMDM_GNSS_UNKNOWN, > + MOTMDM_GNSS_INITIALIZED, > + MOTMDM_GNSS_DATA_OR_TIMEOUT, > + MOTMDM_GNSS_STARTED, > + MOTMDM_GNSS_STOPPED, > +}; > + > +struct motmdm_gnss_data { > + struct gnss_device *gdev; > + struct device *modem; > + struct serdev_device *serdev; > + struct delayed_work restart_work; > + struct mutex mutex; /* For modem commands */ > + ktime_t last_update; > + int status; > + unsigned char *buf; > + size_t len; > + wait_queue_head_t read_queue; > + unsigned int parsed:1; > +}; > + > +static unsigned int rate_ms = MOTMDM_GNSS_RATE; > +module_param(rate_ms, uint, 0644); > +MODULE_PARM_DESC(rate_ms, "GNSS refresh rate between 1000 and 16000 ms > (default 1000 ms)"); > + > +/* > + * Note that multiple commands can be sent in series with responses coming > + * out-of-order. For GNSS, we don't need to care about the out-of-order > + * responses, and can assume we have at most one command active at a time. > + * For the commands, can use just a jiffies base packet ID and let the modem > + * sort out the ID conflicts with the modem's unsolicited message ID > + * numbering. > + */ > +int motmdm_gnss_send_command(struct motmdm_gnss_data *ddata, > + const u8 *buf, int len) > +{ > + struct gnss_device *gdev = ddata->gdev; > + const int timeout_ms = 1000; > + unsigned char cmd[128]; > + int ret, cmdlen; > + > + cmdlen = len + MOTMDM_GNSS_HEADER_LEN + 1; > + if (cmdlen > 128) > + return -EINVAL; > + > + mutex_lock(&ddata->mutex); > + memset(ddata->buf, 0, ddata->len); > + ddata->parsed = false; > + snprintf(cmd, cmdlen, "U%04li%s", jiffies % 10000, buf); > + > + ret = serdev_device_write(ddata->serdev, cmd, cmdlen, > MAX_SCHEDULE_TIMEOUT); > + if (ret < 0) > + goto out_unlock; > + > + serdev_device_wait_until_sent(ddata->serdev, 0); > + > + ret = wait_event_timeout(ddata->read_queue, ddata->parsed, > + msecs_to_jiffies(timeout_ms)); > + if (ret == 0) { > + ret = -ETIMEDOUT; > + goto out_unlock; > + } else if (ret < 0) { > + goto out_unlock; > + } > + > + if (!strstr(ddata->buf, ":OK")) { > + dev_err(&gdev->dev, "command %s error %s\n", > + cmd, ddata->buf); > + ret = -EPIPE; > + } > + > + ret = len; > + > +out_unlock: > + mutex_unlock(&ddata->mutex); > + > + return ret; > +} > + > +/* > + * Android uses AT+MPDSTART=0,1,100,0 which starts GNSS for a while, > + * and then GNSS needs to be kicked with an AT command based on a > + * status message. > + */ > +static void motmdm_gnss_restart(struct work_struct *work) > +{ > + struct motmdm_gnss_data *ddata = > + container_of(work, struct motmdm_gnss_data, > + restart_work.work); > + struct gnss_device *gdev = ddata->gdev; > + const unsigned char *cmd = "AT+MPDSTART=0,1,100,0"; > + int error; > + > + ddata->last_update = ktime_get(); > + > + error = motmdm_gnss_send_command(ddata, cmd, strlen(cmd)); > + if (error < 0) { > + /* Timeouts can happen, don't warn and try again */ > + if (error != -ETIMEDOUT) > + dev_warn(&gdev->dev, "%s: could not start: %i\n", > + __func__, error); > + > + schedule_delayed_work(&ddata->restart_work, > + msecs_to_jiffies(MOTMDM_GNSS_RATE)); > + } > +} > + > +static void motmdm_gnss_start(struct gnss_device *gdev, int delay_ms) > +{ > + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); > + ktime_t now, next, delta; > + int next_ms; > + > + now = ktime_get(); > + next = ktime_add_ms(ddata->last_update, delay_ms); > + delta = ktime_sub(next, now); > + next_ms = ktime_to_ms(delta); > + > + if (next_ms < 0) > + next_ms = 0; > + if (next_ms > delay_ms) > + next_ms = delay_ms; > + > + schedule_delayed_work(&ddata->restart_work, msecs_to_jiffies(next_ms)); > +} > + > +static int motmdm_gnss_stop(struct gnss_device *gdev) > +{ > + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); > + const unsigned char *cmd = "AT+MPDSTOP"; > + > + cancel_delayed_work_sync(&ddata->restart_work); > + > + return motmdm_gnss_send_command(ddata, cmd, strlen(cmd)); > +} > + > +static int motmdm_gnss_init(struct gnss_device *gdev) > +{ > + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); > + const unsigned char *cmd = "AT+MPDINIT=1"; > + int error; > + > + error = motmdm_gnss_send_command(ddata, cmd, strlen(cmd)); > + if (error < 0) > + return error; > + > + motmdm_gnss_start(gdev, 0); > + > + return 0; > +} > + > +static int motmdm_gnss_finish(struct gnss_device *gdev) > +{ > + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); > + const unsigned char *cmd = "AT+MPDINIT=0"; > + int error; > + > + error = motmdm_gnss_stop(gdev); > + if (error < 0) > + return error; > + > + return motmdm_gnss_send_command(ddata, cmd, strlen(cmd)); > +} > + > +static int motmdm_gnss_receive_data(struct serdev_device *serdev, > + const unsigned char *buf, size_t len) > +{ > + struct motmdm_gnss_data *ddata = serdev_device_get_drvdata(serdev); > + struct gnss_device *gdev = ddata->gdev; > + const unsigned char *msg; > + size_t msglen; > + int error = 0; > + > + if (len <= MOTMDM_GNSS_RESP_LEN) > + return 0; > + > + /* Handle U1234+MPD style command response */ > + if (buf[MOTMDM_GNSS_HEADER_LEN] != '~') { > + msg = buf + MOTMDM_GNSS_RESP_LEN; > + strncpy(ddata->buf, msg, len - MOTMDM_GNSS_RESP_LEN); > + ddata->parsed = true; > + wake_up(&ddata->read_queue); > + > + return len; > + } > + > + if (len <= MOTMDM_GNSS_DATA_LEN) > + return 0; > + > + /* Handle U1234~+MPD style unsolicted message */ > + switch (buf[MOTMDM_GNSS_DATA_LEN]) { > + case 'N': /* UNNNN~+MPDNMEA=NN, */ > + msg = buf + MOTMDM_GNSS_NMEA_LEN; > + msglen = len - MOTMDM_GNSS_NMEA_LEN; > + > + /* > + * Firmware bug: Strip out extra duplicate line break always > + * in the data > + */ > + msglen--; > + > + /* > + * Firmware bug: Strip out extra data based on an > + * earlier line break in the data > + */ > + if (msg[msglen - 5 - 1] == 0x0a) > + msglen -= 5; > + > + error = gnss_insert_raw(gdev, msg, msglen); > + WARN_ON(error); > + break; > + case 'S': /* UNNNN~+MPDSTATUS=N,NN */ > + msg = buf + MOTMDM_GNSS_STATUS_LEN; > + msglen = len - MOTMDM_GNSS_STATUS_LEN; > + > + switch (msg[0]) { > + case '1': > + ddata->status = MOTMDM_GNSS_INITIALIZED; > + break; > + case '2': > + ddata->status = MOTMDM_GNSS_DATA_OR_TIMEOUT; > + if (rate_ms < MOTMDM_GNSS_RATE) > + rate_ms = MOTMDM_GNSS_RATE; > + if (rate_ms > 16 * MOTMDM_GNSS_RATE) > + rate_ms = 16 * MOTMDM_GNSS_RATE; > + motmdm_gnss_start(gdev, rate_ms); > + break; > + case '3': > + ddata->status = MOTMDM_GNSS_STARTED; > + break; > + case '4': > + ddata->status = MOTMDM_GNSS_STOPPED; > + break; > + default: > + ddata->status = MOTMDM_GNSS_UNKNOWN; > + break; > + } > + break; > + case 'X': /* UNNNN~+MPDXREQ=N for updated xtra2.bin needed */ > + default: > + break; > + } > + > + return len; > +} > + > +static const struct serdev_device_ops gnss_serdev_ops = { > + .receive_buf = motmdm_gnss_receive_data, > + .write_wakeup = serdev_device_write_wakeup, > +}; > + > +static int motmdm_gnss_open(struct gnss_device *gdev) > +{ > + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); > + int error; > + > + serdev_device_set_client_ops(ddata->serdev, &gnss_serdev_ops); > + > + error = serdev_device_open(ddata->serdev); > + if (error) { > + return error; > + } > + > + error = motmdm_gnss_init(gdev); > + if (error) { > + return error; > + } > + > + return 0; > +} > + > +static void motmdm_gnss_close(struct gnss_device *gdev) > +{ > + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); > + int error; > + > + error = motmdm_gnss_finish(gdev); > + if (error < 0) > + dev_warn(&gdev->dev, "%s: close failed: %i\n", > + __func__, error); > + > + serdev_device_close(ddata->serdev); > +} > + > +static int motmdm_gnss_write_raw(struct gnss_device *gdev, > + const unsigned char *buf, > + size_t count) > +{ > + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); > + > + return serdev_device_write(ddata->serdev, buf, count, > MAX_SCHEDULE_TIMEOUT); > + serdev_device_wait_until_sent(ddata->serdev, 0); > + > + return count; > +} > + > +static const struct gnss_operations motmdm_gnss_ops = { > + .open = motmdm_gnss_open, > + .close = motmdm_gnss_close, > + .write_raw = motmdm_gnss_write_raw, > +}; > + > +static int motmdm_gnss_probe(struct serdev_device *serdev) > +{ > + struct device *dev = &serdev->dev; > + struct motmdm_gnss_data *ddata; > + struct gnss_device *gdev; > + int ret; > + > + ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL); > + if (!ddata) > + return -ENOMEM; > + > + ddata->serdev = serdev; > + ddata->modem = dev->parent; > + ddata->len = PAGE_SIZE; > + mutex_init(&ddata->mutex); > + INIT_DELAYED_WORK(&ddata->restart_work, motmdm_gnss_restart); > + init_waitqueue_head(&ddata->read_queue); > + > + ddata->buf = devm_kzalloc(dev, ddata->len, GFP_KERNEL); > + if (!ddata->buf) > + return -ENOMEM; > + > + serdev_device_set_drvdata(serdev, ddata); > + > + gdev = gnss_allocate_device(dev); > + if (!gdev) > + return -ENOMEM; > + > + gdev->type = GNSS_TYPE_NMEA; > + gdev->ops = &motmdm_gnss_ops; > + gnss_set_drvdata(gdev, ddata); > + ddata->gdev = gdev; > + > + ret = gnss_register_device(gdev); > + if (ret) > + gnss_put_device(ddata->gdev); > + > + return ret; > +} > + > +static void motmdm_gnss_remove(struct serdev_device *serdev) > +{ > + struct motmdm_gnss_data *data = serdev_device_get_drvdata(serdev); > + > + gnss_deregister_device(data->gdev); > + gnss_put_device(data->gdev); > +}; > + > +#ifdef CONFIG_OF > +static const struct of_device_id motmdm_gnss_of_match[] = { > + { .compatible = "motorola,mapphone-mdm6600-gnss" }, > + {}, > +}; > +MODULE_DEVICE_TABLE(of, motmdm_gnss_of_match); > +#endif > + > +static struct serdev_device_driver motmdm_gnss_driver = { > + .driver = { > + .name = "gnss-mot-mdm6600", > + .of_match_table = of_match_ptr(motmdm_gnss_of_match), > + }, > + .probe = motmdm_gnss_probe, > + .remove = motmdm_gnss_remove, > +}; > +module_serdev_device_driver(motmdm_gnss_driver); > + > +MODULE_AUTHOR("Tony Lindgren <t...@atomide.com>"); > +MODULE_DESCRIPTION("Motorola Mapphone MDM6600 GNSS receiver driver"); > +MODULE_LICENSE("GPL v2"); > > > -- > http://www.livejournal.com/~pavelmachek -- http://www.livejournal.com/~pavelmachek
signature.asc
Description: PGP signature