From e38e997f907ca38b64c65592f373e01a26ba6a49 Mon Sep 17 00:00:00 2001 From: Nick Ward Date: Thu, 12 Jun 2025 22:52:24 +1000 Subject: [PATCH 1/4] modem: backends: add Quectel I2C modem backend To be used by the Quectel lx6 GNSS driver. Signed-off-by: Nick Ward --- include/zephyr/modem/backend/quectel_i2c.h | 53 ++++ subsys/modem/backends/CMakeLists.txt | 1 + subsys/modem/backends/Kconfig | 5 + .../backends/modem_backend_quectel_i2c.c | 244 ++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 include/zephyr/modem/backend/quectel_i2c.h create mode 100644 subsys/modem/backends/modem_backend_quectel_i2c.c diff --git a/include/zephyr/modem/backend/quectel_i2c.h b/include/zephyr/modem/backend/quectel_i2c.h new file mode 100644 index 0000000000000..d72559ff715f8 --- /dev/null +++ b/include/zephyr/modem/backend/quectel_i2c.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Nick Ward + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#ifndef ZEPHYR_MODEM_BACKEND_QUECTEL_I2C_ +#define ZEPHYR_MODEM_BACKEND_QUECTEL_I2C_ +struct modem_backend_quectel_i2c_config { + const struct i2c_dt_spec i2c; + uint16_t i2c_poll_interval_ms; + uint8_t *receive_buf; + uint8_t *transmit_buf; + uint16_t receive_buf_size; + uint16_t transmit_buf_size; +}; + +struct modem_backend_quectel_i2c { + struct i2c_dt_spec i2c; + uint16_t i2c_poll_interval_ms; + uint8_t *transmit_buf; + uint16_t transmit_buf_size; + uint16_t transmit_i; + struct modem_pipe pipe; + struct k_work_delayable poll_work; + struct k_work notify_receive_ready_work; + struct k_work notify_transmit_idle_work; + struct k_work notify_closed_work; + struct ring_buf receive_ring_buf; + struct k_spinlock receive_rb_lock; + bool suppress_next_lf; + int64_t next_cmd_earliest_time; + bool open; + +#if CONFIG_MODEM_STATS + struct modem_stats_buffer receive_buf_stats; + struct modem_stats_buffer transmit_buf_stats; +#endif +}; + +struct modem_pipe * +modem_backend_quectel_i2c_init(struct modem_backend_quectel_i2c *backend, + const struct modem_backend_quectel_i2c_config *config); + +#endif /* ZEPHYR_MODEM_BACKEND_QUECTEL_I2C_ */ diff --git a/subsys/modem/backends/CMakeLists.txt b/subsys/modem/backends/CMakeLists.txt index e2247a8a09ddb..29cb17195c312 100644 --- a/subsys/modem/backends/CMakeLists.txt +++ b/subsys/modem/backends/CMakeLists.txt @@ -3,6 +3,7 @@ zephyr_library() +zephyr_library_sources_ifdef(CONFIG_MODEM_BACKEND_QUECTEL_I2C modem_backend_quectel_i2c.c) zephyr_library_sources_ifdef(CONFIG_MODEM_BACKEND_TTY modem_backend_tty.c) zephyr_library_sources_ifdef(CONFIG_MODEM_BACKEND_UART modem_backend_uart.c) zephyr_library_sources_ifdef(CONFIG_MODEM_BACKEND_UART_ISR modem_backend_uart_isr.c) diff --git a/subsys/modem/backends/Kconfig b/subsys/modem/backends/Kconfig index 2ce81c6dce11c..86326b09705c3 100644 --- a/subsys/modem/backends/Kconfig +++ b/subsys/modem/backends/Kconfig @@ -64,3 +64,8 @@ endif # MODEM_BACKEND_UART_ASYNC_HWFC endif # MODEM_BACKEND_UART_ASYNC endif # MODEM_BACKEND_UART + +config MODEM_BACKEND_QUECTEL_I2C + bool "Modem Quectel I2C backend module" + select MODEM_PIPE + select RING_BUFFER diff --git a/subsys/modem/backends/modem_backend_quectel_i2c.c b/subsys/modem/backends/modem_backend_quectel_i2c.c new file mode 100644 index 0000000000000..c39db62d0e893 --- /dev/null +++ b/subsys/modem/backends/modem_backend_quectel_i2c.c @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2025 Nick Ward + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +/* I2C peripheral buffer capacity */ +#define READ_I2C_DATA_LENGTH 255 + +#include +LOG_MODULE_REGISTER(modem_backend_quectel_i2c, CONFIG_MODEM_MODULES_LOG_LEVEL); + +static void modem_backend_quectel_i2c_receive_ready_handler(struct k_work *work) +{ + struct modem_backend_quectel_i2c *backend = + CONTAINER_OF(work, struct modem_backend_quectel_i2c, notify_receive_ready_work); + + modem_pipe_notify_receive_ready(&backend->pipe); +} + +static void modem_backend_quectel_i2c_transmit_idle_handler(struct k_work *work) +{ + struct modem_backend_quectel_i2c *backend = + CONTAINER_OF(work, struct modem_backend_quectel_i2c, notify_transmit_idle_work); + + modem_pipe_notify_transmit_idle(&backend->pipe); +} + +static void modem_backend_quectel_i2c_notify_closed_handler(struct k_work *work) +{ + struct modem_backend_quectel_i2c *backend = + CONTAINER_OF(work, struct modem_backend_quectel_i2c, notify_closed_work); + + modem_pipe_notify_closed(&backend->pipe); +} + +static void modem_backend_quectel_i2c_poll_work_handler(struct k_work *work) +{ + struct modem_backend_quectel_i2c *backend = + CONTAINER_OF(k_work_delayable_from_work(work), + struct modem_backend_quectel_i2c, poll_work); + uint8_t buf[READ_I2C_DATA_LENGTH]; + bool receive_ready = false; + k_spinlock_key_t key; + bool lf; + int ret; + + if (!backend->open) { + /* Then we previously couldn't immediately stop this work */ + k_work_submit(&backend->notify_closed_work); + return; + } + + ret = i2c_read_dt(&backend->i2c, buf, sizeof(buf)); + if (ret < 0) { + LOG_ERR("i2c_read: %d", ret); + modem_pipe_notify_closed(&backend->pipe); + return; + } + + key = k_spin_lock(&backend->receive_rb_lock); + + /* Determine if we have received data and filter out consecutive LFs */ + for (size_t i = 0; i < sizeof(buf); i++) { + if (buf[i] == '\0') { + /* After exiting backup mode the read often contains zeros */ + continue; + } + lf = (buf[i] == '\n'); + if (!lf || (lf && !backend->suppress_next_lf)) { + ring_buf_put(&backend->receive_ring_buf, &buf[i], 1); + receive_ready = true; + } + backend->suppress_next_lf = lf; + } + + k_spin_unlock(&backend->receive_rb_lock, key); + + if (receive_ready) { + modem_pipe_notify_receive_ready(&backend->pipe); + } + + k_work_schedule(&backend->poll_work, K_MSEC(backend->i2c_poll_interval_ms)); +} + +static int modem_backend_quectel_i2c_open(void *data) +{ + struct modem_backend_quectel_i2c *backend = (struct modem_backend_quectel_i2c *)data; + + backend->open = true; + k_work_schedule(&backend->poll_work, K_NO_WAIT); + + modem_pipe_notify_opened(&backend->pipe); + + return 0; +} + +static int modem_backend_quectel_i2c_transmit(void *data, const uint8_t *buf, size_t size) +{ + struct modem_backend_quectel_i2c *backend = (struct modem_backend_quectel_i2c *)data; + int ret; + + for (size_t i = 0; i < size; i++) { + backend->transmit_buf[backend->transmit_i] = buf[i]; + backend->transmit_i++; + if (buf[i] == '\n') { + k_work_cancel(&backend->notify_transmit_idle_work); + +#if CONFIG_MODEM_STATS + modem_stats_buffer_advertise_length(&backend->receive_buf_stats, + backend->transmit_i); +#endif + + k_sleep(K_TIMEOUT_ABS_MS(backend->next_cmd_earliest_time)); + + ret = i2c_write_dt(&backend->i2c, backend->transmit_buf, + backend->transmit_i); + if (ret < 0) { + LOG_ERR("i2c_write: %d", ret); + k_work_submit(&backend->notify_closed_work); + return ret; + } + + backend->next_cmd_earliest_time = k_uptime_get() + 10; + + k_work_submit(&backend->notify_transmit_idle_work); + + backend->transmit_i = 0; + } else if (backend->transmit_i >= backend->transmit_buf_size) { + LOG_ERR("%u bytes of TX data dropped:", backend->transmit_i); + backend->transmit_i = 0; + } + } + +#if CONFIG_MODEM_STATS + modem_stats_buffer_advertise_length(&backend->receive_buf_stats, backend->transmit_i); +#endif + + return size; +} + +static int modem_backend_quectel_i2c_receive(void *data, uint8_t *buf, size_t size) +{ + struct modem_backend_quectel_i2c *backend = (struct modem_backend_quectel_i2c *)data; + k_spinlock_key_t key; + uint32_t received; + bool empty; + + key = k_spin_lock(&backend->receive_rb_lock); + +#if CONFIG_MODEM_STATS + uint32_t length = ring_buf_size_get(&backend->receive_ring_buf); + + modem_stats_buffer_advertise_length(&backend->receive_buf_stats, length); +#endif + + received = ring_buf_get(&backend->receive_ring_buf, buf, size); + empty = ring_buf_is_empty(&backend->receive_ring_buf); + k_spin_unlock(&backend->receive_rb_lock, key); + + if (!empty) { + k_work_submit(&backend->notify_receive_ready_work); + } + + return (int)received; +} + +static int modem_backend_quectel_i2c_close(void *data) +{ + struct modem_backend_quectel_i2c *backend = (struct modem_backend_quectel_i2c *)data; + int ret; + + ret = k_work_cancel_delayable(&backend->poll_work); + if (ret == 0) { + k_work_submit(&backend->notify_closed_work); + } + backend->open = false; + + return 0; +} + +static const struct modem_pipe_api modem_backend_quectel_i2c_api = { + .open = modem_backend_quectel_i2c_open, + .transmit = modem_backend_quectel_i2c_transmit, + .receive = modem_backend_quectel_i2c_receive, + .close = modem_backend_quectel_i2c_close, +}; + +#if CONFIG_MODEM_STATS +static void init_stats(struct modem_backend_quectel_i2c *backend) +{ + char name[CONFIG_MODEM_STATS_BUFFER_NAME_SIZE]; + uint32_t receive_buf_size; + + receive_buf_size = ring_buf_capacity_get(&backend->receive_ring_buf); + + snprintk(name, sizeof(name), "%s_%s", backend->i2c->name, "rx"); + modem_stats_buffer_init(&backend->receive_buf_stats, name, receive_buf_size); + + snprintk(name, sizeof(name), "%s_%s", backend->i2c->name, "tx"); + modem_stats_buffer_init(&backend->transmit_buf_stats, name, transmit_buf_stats); +} +#endif + +struct modem_pipe *modem_backend_quectel_i2c_init(struct modem_backend_quectel_i2c *backend, + const struct modem_backend_quectel_i2c_config *config) +{ + __ASSERT_NO_MSG(config != NULL); + __ASSERT_NO_MSG(config->i2c.bus != NULL); + __ASSERT_NO_MSG(config->receive_buf != NULL); + __ASSERT_NO_MSG(config->receive_buf_size > 0); + + memset(backend, 0x00, sizeof(*backend)); + + backend->i2c = config->i2c; + backend->i2c_poll_interval_ms = config->i2c_poll_interval_ms; + backend->transmit_buf = config->transmit_buf; + backend->transmit_buf_size = config->transmit_buf_size; + backend->suppress_next_lf = true; + backend->next_cmd_earliest_time = 0; + + ring_buf_init(&backend->receive_ring_buf, config->receive_buf_size, config->receive_buf); + backend->transmit_i = 0; + backend->open = false; + + k_work_init_delayable(&backend->poll_work, modem_backend_quectel_i2c_poll_work_handler); + k_work_init(&backend->notify_receive_ready_work, + modem_backend_quectel_i2c_receive_ready_handler); + k_work_init(&backend->notify_transmit_idle_work, + modem_backend_quectel_i2c_transmit_idle_handler); + k_work_init(&backend->notify_closed_work, modem_backend_quectel_i2c_notify_closed_handler); + +#if CONFIG_MODEM_STATS + init_stats(backend); +#endif + + modem_pipe_init(&backend->pipe, backend, &modem_backend_quectel_i2c_api); + + return &backend->pipe; +} From 5d80da97eb6f503d11bfc1d1a004be87b92f9e28 Mon Sep 17 00:00:00 2001 From: Nick Ward Date: Fri, 27 Jun 2025 09:23:38 +1000 Subject: [PATCH 2/4] drivers: gnss: add Quectel lx6 driver Add Quectel lx6 driver, tested with L96 on I2C bus. Signed-off-by: Nick Ward --- drivers/gnss/CMakeLists.txt | 1 + drivers/gnss/Kconfig | 1 + drivers/gnss/Kconfig.quectel_lx6 | 61 ++ drivers/gnss/gnss_quectel_lx6.c | 758 ++++++++++++++++++++++ dts/bindings/gnss/quectel,l96-common.yaml | 23 + dts/bindings/gnss/quectel,l96-i2c.yaml | 10 + dts/bindings/gnss/quectel,l96-uart.yaml | 10 + include/zephyr/drivers/gnss/quectel_lx6.h | 33 + 8 files changed, 897 insertions(+) create mode 100644 drivers/gnss/Kconfig.quectel_lx6 create mode 100644 drivers/gnss/gnss_quectel_lx6.c create mode 100644 dts/bindings/gnss/quectel,l96-common.yaml create mode 100644 dts/bindings/gnss/quectel,l96-i2c.yaml create mode 100644 dts/bindings/gnss/quectel,l96-uart.yaml create mode 100644 include/zephyr/drivers/gnss/quectel_lx6.h diff --git a/drivers/gnss/CMakeLists.txt b/drivers/gnss/CMakeLists.txt index 7ffd07b3dcab1..c0355fd9a981b 100644 --- a/drivers/gnss/CMakeLists.txt +++ b/drivers/gnss/CMakeLists.txt @@ -10,6 +10,7 @@ zephyr_library_sources_ifdef(CONFIG_GNSS_NMEA0183 gnss_nmea0183.c) zephyr_library_sources_ifdef(CONFIG_GNSS_NMEA0183_MATCH gnss_nmea0183_match.c) zephyr_library_sources_ifdef(CONFIG_GNSS_NMEA_GENERIC gnss_nmea_generic.c) zephyr_library_sources_ifdef(CONFIG_GNSS_QUECTEL_LCX6G gnss_quectel_lcx6g.c) +zephyr_library_sources_ifdef(CONFIG_GNSS_QUECTEL_LX6 gnss_quectel_lx6.c) zephyr_library_sources_ifdef(CONFIG_GNSS_U_BLOX_F9P gnss_u_blox_f9p.c gnss_ubx_common.c) zephyr_library_sources_ifdef(CONFIG_GNSS_U_BLOX_M8 gnss_u_blox_m8.c diff --git a/drivers/gnss/Kconfig b/drivers/gnss/Kconfig index 94f2b56147fd9..e0193424a4fff 100644 --- a/drivers/gnss/Kconfig +++ b/drivers/gnss/Kconfig @@ -82,6 +82,7 @@ source "subsys/logging/Kconfig.template.log_config" rsource "Kconfig.emul" rsource "Kconfig.generic" rsource "Kconfig.quectel_lcx6g" +rsource "Kconfig.quectel_lx6" rsource "Kconfig.u_blox_f9p" rsource "Kconfig.u_blox_m8" rsource "Kconfig.luatos_air530z" diff --git a/drivers/gnss/Kconfig.quectel_lx6 b/drivers/gnss/Kconfig.quectel_lx6 new file mode 100644 index 0000000000000..f94fe43dd539a --- /dev/null +++ b/drivers/gnss/Kconfig.quectel_lx6 @@ -0,0 +1,61 @@ +# Copyright (c) 2025 Nick Ward +# SPDX-License-Identifier: Apache-2.0 + +config GNSS_QUECTEL_LX6 + bool "Quectel LX6 GNSS modem driver" + default y + depends on GNSS + depends on DT_HAS_QUECTEL_L96_ENABLED + depends on GNSS_REFERENCE_FRAME_WGS84 + select I2C if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),i2c) + select SERIAL if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),uart) + select MODEM_MODULES + select MODEM_BACKEND_QUECTEL_I2C if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),i2c) + select MODEM_BACKEND_UART if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),uart) + select MODEM_CHAT + select GNSS_PARSE + select GNSS_NMEA0183 + select GNSS_NMEA0183_MATCH + help + Enable Quectel LX6 series GNSS modem driver. + +if GNSS_QUECTEL_LX6 + +config GNSS_QUECTEL_LX6_RX_BUF_SIZE + int "Size of backend receive buffer" + default 256 + +config GNSS_QUECTEL_LX6_TX_BUF_SIZE + int "Size of backend transmit buffer" + default 64 + +config GNSS_QUECTEL_LX6_I2C_POLL_MS + int "GNSS I2C polling interval (ms)" + range 2 500 + default 500 + depends on $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),i2c) + help + The interval in milliseconds at which the GNSS module is polled via I2C + to read NMEA data. This value should be less than the GNSS fix interval + to avoid data loss. + +if GNSS_SATELLITES + +config GNSS_QUECTEL_LX6_SAT_ARRAY_SIZE + int "Size of GNSS satellites array" + default 24 + +endif # GNSS_SATELLITES + +config GNSS_QUECTEL_LX6_RESET_ON_INIT + bool "Reset Quectel lx6 module during initialization" + depends on GPIO + help + If enabled, the driver will toggle the reset pin during initialization. + The reset pin must be defined in the device tree as reset-gpios. + This will issue a hardware reset to the lx6 module by pulling the + RESET pin low for at least 10ms, then releasing it. This may clear + volatile RAM data but will preserve backup RAM content, allowing + faster TTFF after reset. + +endif # GNSS_QUECTEL_LX6 diff --git a/drivers/gnss/gnss_quectel_lx6.c b/drivers/gnss/gnss_quectel_lx6.c new file mode 100644 index 0000000000000..86f8568384405 --- /dev/null +++ b/drivers/gnss/gnss_quectel_lx6.c @@ -0,0 +1,758 @@ +/* + * Copyright (c) 2025 Nick Ward + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gnss_nmea0183.h" +#include "gnss_nmea0183_match.h" +#include "gnss_parse.h" + +#include + +#include +LOG_MODULE_REGISTER(quectel_lx6, CONFIG_GNSS_LOG_LEVEL); + +#define SUPPORTED_SYSTEMS (GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | \ + GNSS_SYSTEM_GALILEO | GNSS_SYSTEM_BEIDOU | GNSS_SYSTEM_QZSS) + +#define QUECTEL_LX6_SCRIPT_TIMEOUT_S 10U + +#define QUECTEL_LX6_PMTK_NAV_MODE_NORMAL 0 +#define QUECTEL_LX6_PMTK_NAV_MODE_FITNESS 1 +#define QUECTEL_LX6_PMTK_NAV_MODE_AVIATION 2 +#define QUECTEL_LX6_PMTK_NAV_MODE_BALLOON 3 /* Unused high-altitude purposes */ +#define QUECTEL_LX6_PMTK_NAV_MODE_STATIONARY 4 + +#define QUECTEL_LX6_PMTK_PPS_CONFIG_DISABLED 0 +#define QUECTEL_LX6_PMTK_PPS_CONFIG_ENABLED_AFTER_FIRST_FIX 1 +#define QUECTEL_LX6_PMTK_PPS_CONFIG_ENABLED_3D_FIX_ONLY 2 +#define QUECTEL_LX6_PMTK_PPS_CONFIG_ENABLED_2D_3D_FIX_ONLY 3 +#define QUECTEL_LX6_PMTK_PPS_CONFIG_ALWAYS 4 + +struct quectel_lx6_config { + bool i2c_bus; + struct gpio_dt_spec reset; + struct gpio_dt_spec vcc; + const enum gnss_pps_mode pps_mode; + const uint16_t pps_pulse_width; + union { + struct modem_backend_quectel_i2c_config i2c_config; + struct modem_backend_uart_config uart_config; + } backend; +}; + +struct quectel_lx6_data { + struct gnss_nmea0183_match_data match_data; +#if CONFIG_GNSS_SATELLITES + struct gnss_satellite satellites[CONFIG_GNSS_QUECTEL_LX6_SAT_ARRAY_SIZE]; +#endif + + union { + struct modem_backend_quectel_i2c i2c; + struct modem_backend_uart uart; + } backend; + + struct modem_pipe *pipe; + uint8_t backend_receive_buf[CONFIG_GNSS_QUECTEL_LX6_RX_BUF_SIZE]; + uint8_t backend_transmit_buf[CONFIG_GNSS_QUECTEL_LX6_TX_BUF_SIZE]; + + struct modem_chat chat; + uint8_t chat_receive_buf[256]; + uint8_t chat_delimiter[2]; + uint8_t *chat_argv[32]; + + uint8_t pmtk_request_buf[64]; + uint8_t pmtk_match_buf[32]; + struct modem_chat_match pmtk_match; + struct modem_chat_script_chat pmtk_script_chat; + struct modem_chat_script pmtk_script; + + struct k_sem sem; + +#if CONFIG_GNSS_QUECTEL_LX6_RESET_ON_INIT + bool oneshot_reset; +#endif +}; + +/* System message - Startup */ +MODEM_CHAT_MATCH_DEFINE(pmtk104_success_match, "$PMTK010,001*2E", "", NULL); +MODEM_CHAT_SCRIPT_CMDS_DEFINE( + full_cold_start_script_cmds, + MODEM_CHAT_SCRIPT_CMD_RESP("$PMTK104*37", pmtk104_success_match) +); +MODEM_CHAT_SCRIPT_NO_ABORT_DEFINE(full_cold_start_script, full_cold_start_script_cmds, + NULL, QUECTEL_LX6_SCRIPT_TIMEOUT_S); + +MODEM_CHAT_MATCHES_DEFINE(unsol_matches, + MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback), + MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback), +#if CONFIG_GNSS_SATELLITES + MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback), +#endif +); + +static int quectel_lx6_configure_pps(const struct device *dev) +{ + uint8_t pps_mode = QUECTEL_LX6_PMTK_PPS_CONFIG_DISABLED; + const struct quectel_lx6_config *config = dev->config; + struct quectel_lx6_data *data = dev->data; + int ret; + + switch (config->pps_mode) { + case GNSS_PPS_MODE_DISABLED: + pps_mode = QUECTEL_LX6_PMTK_PPS_CONFIG_DISABLED; + break; + + case GNSS_PPS_MODE_ENABLED: + pps_mode = QUECTEL_LX6_PMTK_PPS_CONFIG_ALWAYS; + break; + + case GNSS_PPS_MODE_ENABLED_AFTER_LOCK: + pps_mode = QUECTEL_LX6_PMTK_PPS_CONFIG_ENABLED_AFTER_FIRST_FIX; + break; + + case GNSS_PPS_MODE_ENABLED_WHILE_LOCKED: + return -ENOTSUP; + } + + ret = gnss_nmea0183_snprintk(data->pmtk_request_buf, sizeof(data->pmtk_request_buf), + "PMTK285,%u,%u", pps_mode, config->pps_pulse_width); + if (ret < 0) { + return ret; + } + + ret = modem_chat_script_chat_set_request(&data->pmtk_script_chat, data->pmtk_request_buf); + if (ret < 0) { + return ret; + } + + ret = gnss_nmea0183_snprintk(data->pmtk_match_buf, sizeof(data->pmtk_match_buf), + "PMTK001,285,3"); + if (ret < 0) { + return ret; + } + + ret = modem_chat_match_set_match(&data->pmtk_match, data->pmtk_match_buf); + if (ret < 0) { + return ret; + } + + return modem_chat_run_script(&data->chat, &data->pmtk_script); +} + +int quectel_lx6_cold_start(const struct device *dev) +{ + const struct quectel_lx6_config *config = dev->config; + struct quectel_lx6_data *data = dev->data; + int ret; + + ret = modem_chat_run_script(&data->chat, &full_cold_start_script); + if (ret < 0) { + LOG_ERR("Failed to full cold restart GNSS: %d", ret); + modem_pipe_close(data->pipe, K_SECONDS(10)); + return ret; + } + + if (config->i2c_bus) { + /* Closing pipe while LX6 is not responsive to I2C commands */ + modem_pipe_close(data->pipe, K_SECONDS(10)); + + k_sleep(K_SECONDS(1)); + + ret = modem_pipe_open(data->pipe, K_SECONDS(10)); + if (ret < 0) { + LOG_ERR("Failed to open modem pipe: %d", ret); + return ret; + } + } else { + k_sleep(K_SECONDS(1)); + } + + return 0; +} + +static int quectel_lx6_resume(const struct device *dev) +{ + const struct quectel_lx6_config *config = dev->config; + struct quectel_lx6_data *data = dev->data; + int ret; + + LOG_DBG("Resume"); + + if (config->vcc.port != NULL) { + ret = gpio_pin_configure_dt(&config->vcc, GPIO_OUTPUT_ACTIVE); + if (ret < 0) { + LOG_ERR("Failed to set VCC high: %d", ret); + return ret; + } + k_msleep(250); + } else { + /* TODO add check that either VCC or FORCE_ON are defined */ + return -ENOTSUP; + } + + ret = modem_pipe_open(data->pipe, K_SECONDS(10)); + if (ret < 0) { + LOG_ERR("Failed to open modem pipe: %d", ret); + return ret; + } + + ret = modem_chat_attach(&data->chat, data->pipe); + if (ret < 0) { + LOG_ERR("Failed to attach chat: %d", ret); + modem_pipe_close(data->pipe, K_SECONDS(10)); + return ret; + } + + ret = quectel_lx6_configure_pps(dev); + if (ret < 0) { + LOG_ERR("Failed to configure PPS: %d", ret); + modem_pipe_close(data->pipe, K_SECONDS(10)); + return ret; + } + + return ret; +} + +static int quectel_lx6_suspend(const struct device *dev) +{ + const struct quectel_lx6_config *config = dev->config; + struct quectel_lx6_data *data = dev->data; + int ret; + + LOG_DBG("Suspend"); + + if (config->vcc.port != NULL) { + ret = gpio_pin_configure_dt(&config->vcc, GPIO_OUTPUT_INACTIVE); + if (ret < 0) { + LOG_ERR("[%s] VCC pin active", dev->name); + } + } else { + /* TODO add check that either VCC or FORCE_ON are defined */ + return -ENOTSUP; + } + + modem_pipe_close(data->pipe, K_SECONDS(10)); + + return ret; +} + +#if CONFIG_GNSS_QUECTEL_LX6_RESET_ON_INIT +static int quectel_lx6_gpio_reset(const struct device *dev) +{ + const struct quectel_lx6_config *config = dev->config; + int ret; + + if ((config->reset.port != NULL) && (config->vcc.port != NULL)) { + ret = gpio_pin_set_dt(&config->vcc, 1); + if (ret < 0) { + LOG_ERR("[%s] couldn't config VCC", dev->name); + return ret; + } + ret = gpio_pin_set_dt(&config->reset, 0); /* Inactive is high */ + if (ret < 0) { + LOG_ERR("Failed to inactivate reset pin: %d", ret); + } + k_msleep(4); /* > 2ms */ + ret = gpio_pin_set_dt(&config->reset, 1); /* Active is low */ + if (ret < 0) { + LOG_ERR("Failed to activate reset pin: %d", ret); + } + k_msleep(12); /* Pulldown > 10ms */ + ret = gpio_pin_set_dt(&config->reset, 0); /* Inactive is high */ + if (ret < 0) { + LOG_ERR("Failed to inactivate reset pin: %d", ret); + } + } else { + LOG_WRN("[%s] couldn't reset", dev->name); + } + + return ret; +} +#endif + +static int quectel_lx6_turn_on(const struct device *dev) +{ + const struct quectel_lx6_config *config = dev->config; + struct quectel_lx6_data *data = dev->data; + int ret; + + LOG_INF("Turn on"); + + if (config->vcc.port != NULL) { + ret = gpio_pin_configure_dt(&config->vcc, GPIO_OUTPUT_ACTIVE); + if (ret < 0) { + LOG_ERR("[%s] couldn't activate VCC", dev->name); + return ret; + } + } + +#if CONFIG_GNSS_QUECTEL_LX6_RESET_ON_INIT + if (!data->oneshot_reset) { + data->oneshot_reset = true; + quectel_lx6_gpio_reset(dev); + } +#endif + + k_msleep(250); + + ret = modem_pipe_open(data->pipe, K_SECONDS(10)); + if (ret < 0) { + LOG_ERR("Failed to open modem pipe: %d", ret); + } + + return ret; +} + +static int quectel_lx6_turn_off(const struct device *dev) +{ + const struct quectel_lx6_config *config = dev->config; + struct quectel_lx6_data *data = dev->data; + int ret; + + LOG_INF("Turn off"); + + if (config->vcc.port != NULL) { + ret = gpio_pin_configure_dt(&config->vcc, GPIO_INPUT); + if (ret < 0) { + LOG_ERR("[%s] couldn't avoid back powering VCC", dev->name); + return ret; + } + } + + return modem_pipe_close(data->pipe, K_SECONDS(10)); +} + +static int quectel_lx6_pm_action(const struct device *dev, enum pm_device_action action) +{ + struct quectel_lx6_data *data = dev->data; + int ret = -ENOTSUP; + + (void)k_sem_take(&data->sem, K_FOREVER); + + switch (action) { + case PM_DEVICE_ACTION_SUSPEND: + ret = quectel_lx6_suspend(dev); + break; + + case PM_DEVICE_ACTION_RESUME: + ret = quectel_lx6_resume(dev); + break; + + case PM_DEVICE_ACTION_TURN_ON: + ret = quectel_lx6_turn_on(dev); + break; + + case PM_DEVICE_ACTION_TURN_OFF: + ret = quectel_lx6_turn_off(dev); + break; + + default: + break; + } + + (void)k_sem_give(&data->sem); + + return ret; +} + +static int quectel_lx6_set_fix_rate(const struct device *dev, uint32_t fix_interval_ms) +{ + struct quectel_lx6_data *data = dev->data; + int ret; + + if (IN_RANGE(fix_interval_ms, 100, 999)) { + /* TODO limit fix interval depending on UART baudrate */ + return -ENOSYS; + } + + /* Full range of lx6 */ + if (!IN_RANGE(fix_interval_ms, 100, 10000)) { + return -EINVAL; + } + + if (fix_interval_ms > 1000) { + if (fix_interval_ms % 1000 != 0) { + return -EINVAL; + } + } + + (void)k_sem_take(&data->sem, K_FOREVER); + + ret = gnss_nmea0183_snprintk(data->pmtk_request_buf, sizeof(data->pmtk_request_buf), + "PMTK220,%u", fix_interval_ms); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_script_chat_set_request(&data->pmtk_script_chat, data->pmtk_request_buf); + if (ret < 0) { + goto out_unlock; + } + + ret = gnss_nmea0183_snprintk(data->pmtk_match_buf, sizeof(data->pmtk_match_buf), + "PMTK001,220,3,%u", fix_interval_ms); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_match_set_match(&data->pmtk_match, data->pmtk_match_buf); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_run_script(&data->chat, &data->pmtk_script); + if (ret < 0) { + goto out_unlock; + } + +out_unlock: + (void)k_sem_give(&data->sem); + + return ret; +} + +static int quectel_lx6_set_navigation_mode(const struct device *dev, + enum gnss_navigation_mode mode) +{ + struct quectel_lx6_data *data = dev->data; + uint8_t navigation_mode = 0; + int ret; + + switch (mode) { + case GNSS_NAVIGATION_MODE_ZERO_DYNAMICS: + navigation_mode = QUECTEL_LX6_PMTK_NAV_MODE_STATIONARY; + break; + + case GNSS_NAVIGATION_MODE_LOW_DYNAMICS: + navigation_mode = QUECTEL_LX6_PMTK_NAV_MODE_FITNESS; + break; + + case GNSS_NAVIGATION_MODE_BALANCED_DYNAMICS: + navigation_mode = QUECTEL_LX6_PMTK_NAV_MODE_NORMAL; + break; + + case GNSS_NAVIGATION_MODE_HIGH_DYNAMICS: + navigation_mode = QUECTEL_LX6_PMTK_NAV_MODE_AVIATION; + break; + } + + (void)k_sem_take(&data->sem, K_FOREVER); + + ret = gnss_nmea0183_snprintk(data->pmtk_request_buf, sizeof(data->pmtk_request_buf), + "PMTK886,%u", navigation_mode); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_script_chat_set_request(&data->pmtk_script_chat, data->pmtk_request_buf); + if (ret < 0) { + goto out_unlock; + } + + ret = gnss_nmea0183_snprintk(data->pmtk_match_buf, sizeof(data->pmtk_match_buf), + "PMTK001,886,3"); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_match_set_match(&data->pmtk_match, data->pmtk_match_buf); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_run_script(&data->chat, &data->pmtk_script); + if (ret < 0) { + goto out_unlock; + } + +out_unlock: + (void)k_sem_give(&data->sem); + + return ret; +} + +static int quectel_lx6_set_enabled_systems(const struct device *dev, gnss_systems_t systems) +{ + struct quectel_lx6_data *data = dev->data; + gnss_systems_t supported_systems; + char msg[12]; + int ret; + + supported_systems = SUPPORTED_SYSTEMS; + + if (((~supported_systems) & systems) != 0) { + LOG_ERR("Unsupported system"); + return -EINVAL; + } + +#define UNSUPPORTED_COMBO0 (GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU) +#define UNSUPPORTED_COMBO1 (GNSS_SYSTEM_GALILEO | GNSS_SYSTEM_BEIDOU) + + if ((UNSUPPORTED_COMBO0 & systems) == UNSUPPORTED_COMBO0) { + LOG_ERR("GLONASS and BDS cannot be enabled at the same time"); + return -EINVAL; + } + + if ((UNSUPPORTED_COMBO1 & systems) == UNSUPPORTED_COMBO1) { + LOG_ERR("GALILEO and BDS cannot be enabled at the same time"); + return -EINVAL; + } + + (void)k_sem_take(&data->sem, K_FOREVER); + + ret = snprintf(msg, sizeof(msg), "%u,%u,%u,0,%u", + (0 < (systems & GNSS_SYSTEM_GPS)), + (0 < (systems & GNSS_SYSTEM_GLONASS)), + (0 < (systems & GNSS_SYSTEM_GALILEO)), + (0 < (systems & GNSS_SYSTEM_BEIDOU))); + if (ret < 0) { + goto out_unlock; + } + + ret = gnss_nmea0183_snprintk(data->pmtk_request_buf, sizeof(data->pmtk_request_buf), + "PMTK353,%s", msg); + /* Note GNSS_SYSTEM_QZSS needs to be handled elsewhere */ + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_script_chat_set_request(&data->pmtk_script_chat, data->pmtk_request_buf); + if (ret < 0) { + goto out_unlock; + } + + ret = gnss_nmea0183_snprintk(data->pmtk_match_buf, sizeof(data->pmtk_match_buf), + "PMTK001,353,3,%s", msg); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_match_set_match(&data->pmtk_match, data->pmtk_match_buf); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_run_script(&data->chat, &data->pmtk_script); + if (ret < 0) { + goto out_unlock; + } + + ret = gnss_nmea0183_snprintk(data->pmtk_request_buf, sizeof(data->pmtk_request_buf), + "PMTK351,%u", (0 < (systems & GNSS_SYSTEM_QZSS))); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_script_chat_set_request(&data->pmtk_script_chat, data->pmtk_request_buf); + if (ret < 0) { + goto out_unlock; + } + + ret = gnss_nmea0183_snprintk(data->pmtk_match_buf, sizeof(data->pmtk_match_buf), + "PMTK001,351,3"); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_match_set_match(&data->pmtk_match, data->pmtk_match_buf); + if (ret < 0) { + goto out_unlock; + } + + ret = modem_chat_run_script(&data->chat, &data->pmtk_script); + if (ret < 0) { + goto out_unlock; + } + +out_unlock: + (void)k_sem_give(&data->sem); + + return ret; +} + +static inline bool search_mode_enabled(const char *arg) +{ + return arg[0] == '1'; +} + +static int quectel_lx6_get_supported_systems(const struct device *dev, gnss_systems_t *systems) +{ + *systems = SUPPORTED_SYSTEMS; + + return 0; +} + +static DEVICE_API(gnss, gnss_api) = { + .set_fix_rate = quectel_lx6_set_fix_rate, + .get_fix_rate = NULL, /* not supported */ + .set_navigation_mode = quectel_lx6_set_navigation_mode, + .get_navigation_mode = NULL, /* not supported */ + .set_enabled_systems = quectel_lx6_set_enabled_systems, + .get_enabled_systems = NULL, /* not supported */ + .get_supported_systems = quectel_lx6_get_supported_systems, +}; + +static int quectel_lx6_init_nmea0183_match(const struct device *dev) +{ + struct quectel_lx6_data *data = dev->data; + + const struct gnss_nmea0183_match_config config = { + .gnss = dev, +#if CONFIG_GNSS_SATELLITES + .satellites = data->satellites, + .satellites_size = ARRAY_SIZE(data->satellites), +#endif + }; + + return gnss_nmea0183_match_init(&data->match_data, &config); +} + +static void quectel_lx6_init_pipe(const struct device *dev) +{ + const struct quectel_lx6_config *config = dev->config; + struct quectel_lx6_data *data = dev->data; + + if (config->i2c_bus) { +#ifdef CONFIG_MODEM_BACKEND_QUECTEL_I2C + data->pipe = modem_backend_quectel_i2c_init(&data->backend.i2c, + &config->backend.i2c_config); +#endif + } else { +#ifdef CONFIG_MODEM_BACKEND_UART + data->pipe = modem_backend_uart_init(&data->backend.uart, + &config->backend.uart_config); +#endif + } +} + +static void quectel_lx6_init_pmtk_script(const struct device *dev) +{ + struct quectel_lx6_data *data = dev->data; + + modem_chat_match_init(&data->pmtk_match); + modem_chat_match_set_separators(&data->pmtk_match, ",*"); + + modem_chat_script_chat_init(&data->pmtk_script_chat); + modem_chat_script_chat_set_response_matches(&data->pmtk_script_chat, + &data->pmtk_match, 1); + + modem_chat_script_init(&data->pmtk_script); + modem_chat_script_set_name(&data->pmtk_script, "pmtk"); + modem_chat_script_set_script_chats(&data->pmtk_script, &data->pmtk_script_chat, 1); + modem_chat_script_set_abort_matches(&data->pmtk_script, NULL, 0); + modem_chat_script_set_timeout(&data->pmtk_script, 10); +} + +static int quectel_lx6_init(const struct device *dev) +{ + struct quectel_lx6_data *data = dev->data; + int ret; + + k_sem_init(&data->sem, 1, 1); + +#if CONFIG_GNSS_QUECTEL_LX6_RESET_ON_INIT + data->oneshot_reset = false; +#endif + + ret = quectel_lx6_init_nmea0183_match(dev); + if (ret < 0) { + return ret; + } + + quectel_lx6_init_pipe(dev); + + const struct modem_chat_config chat_config = { + .user_data = data, + .receive_buf = data->chat_receive_buf, + .receive_buf_size = ARRAY_SIZE(data->chat_receive_buf), + .delimiter = data->chat_delimiter, + .delimiter_size = ARRAY_SIZE(data->chat_delimiter), + .filter = NULL, + .filter_size = 0, + .argv = data->chat_argv, + .argv_size = ARRAY_SIZE(data->chat_argv), + .unsol_matches = unsol_matches, + .unsol_matches_size = ARRAY_SIZE(unsol_matches), + }; + + ret = modem_chat_init(&data->chat, &chat_config); + if (ret < 0) { + return ret; + } + + quectel_lx6_init_pmtk_script(dev); + + return pm_device_driver_init(dev, quectel_lx6_pm_action); +} + +#define LX6_INST_NAME(inst, name) \ + _CONCAT(_CONCAT(_CONCAT(_CONCAT(name, _), DT_DRV_COMPAT), _), inst) + +#define LX6_BACKEND_I2C_CONFIG(inst) \ + .i2c = I2C_DT_SPEC_INST_GET(inst), \ + .i2c_poll_interval_ms = CONFIG_GNSS_QUECTEL_LX6_I2C_POLL_MS, \ + .receive_buf = LX6_INST_NAME(inst, data).backend_receive_buf, \ + .receive_buf_size = ARRAY_SIZE(LX6_INST_NAME(inst, data).backend_receive_buf), \ + .transmit_buf = LX6_INST_NAME(inst, data).backend_transmit_buf, \ + .transmit_buf_size = ARRAY_SIZE(LX6_INST_NAME(inst, data).backend_transmit_buf) + +#define LX6_BACKEND_UART_CONFIG(inst) \ + .uart = DEVICE_DT_GET(DT_INST_BUS(inst)), \ + .receive_buf = LX6_INST_NAME(inst, data).backend_receive_buf, \ + .receive_buf_size = ARRAY_SIZE(LX6_INST_NAME(inst, data).backend_receive_buf), \ + .transmit_buf = LX6_INST_NAME(inst, data).backend_transmit_buf, \ + .transmit_buf_size = ARRAY_SIZE(LX6_INST_NAME(inst, data).backend_transmit_buf) + +#define LX6_CONFIG_COMMON(inst) \ + .reset = GPIO_DT_SPEC_INST_GET_OR(inst, reset_gpios, {0}), \ + .vcc = GPIO_DT_SPEC_INST_GET_OR(inst, vcc_gpios, {0}), \ + .pps_mode = DT_INST_STRING_UPPER_TOKEN(inst, pps_mode), \ + .pps_pulse_width = DT_INST_PROP(inst, pps_pulse_width) + +#define LX6_CONFIG_I2C(inst) \ + { \ + .i2c_bus = true, \ + .backend.i2c_config = { LX6_BACKEND_I2C_CONFIG(inst) }, \ + LX6_CONFIG_COMMON(inst) \ + } + +#define LX6_CONFIG_UART(inst) \ + { \ + .i2c_bus = false, \ + .backend.uart_config = { LX6_BACKEND_UART_CONFIG(inst) }, \ + LX6_CONFIG_COMMON(inst) \ + } + +#define LX6_DEVICE(inst) \ + static struct quectel_lx6_data LX6_INST_NAME(inst, data) = { \ + .chat_delimiter = {'\r', '\n'}, \ + }; \ + \ + static const struct quectel_lx6_config LX6_INST_NAME(inst, config) = \ + COND_CODE_1(DT_INST_ON_BUS(inst, i2c), \ + (LX6_CONFIG_I2C(inst)), \ + (LX6_CONFIG_UART(inst))); \ + \ + PM_DEVICE_DT_INST_DEFINE(inst, quectel_lx6_pm_action); \ + \ + DEVICE_DT_INST_DEFINE(inst, quectel_lx6_init, PM_DEVICE_DT_INST_GET(inst), \ + &LX6_INST_NAME(inst, data), &LX6_INST_NAME(inst, config), \ + POST_KERNEL, CONFIG_GNSS_INIT_PRIORITY, &gnss_api); + +#define DT_DRV_COMPAT quectel_l96 +DT_INST_FOREACH_STATUS_OKAY(LX6_DEVICE) +#undef DT_DRV_COMPAT diff --git a/dts/bindings/gnss/quectel,l96-common.yaml b/dts/bindings/gnss/quectel,l96-common.yaml new file mode 100644 index 0000000000000..4a5737e90c6e1 --- /dev/null +++ b/dts/bindings/gnss/quectel,l96-common.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2025 Nick Ward +# SPDX-License-Identifier: Apache-2.0 + +include: + - gnss-pps.yaml + +properties: + reset-gpios: + type: phandle-array + description: | + GPIO connected to the RESET pin of the Quectel L96 GNSS module. + - Drive low for at least 10ms to reset the module. + - Drive high (or release if open-drain) to complete the reset cycle. + Volatile RAM data may be lost, but backup RAM is retained, + allowing faster time to first fix (TTFF) after reset. + + vcc-gpios: + type: phandle-array + description: | + Cutting off VCC and keeping V_BCKP powered will make the + module enter into backup mode from full on mode. As long as + the VCC pin is powered, the module will enter into full on + mode immediately. diff --git a/dts/bindings/gnss/quectel,l96-i2c.yaml b/dts/bindings/gnss/quectel,l96-i2c.yaml new file mode 100644 index 0000000000000..df54f26e2d000 --- /dev/null +++ b/dts/bindings/gnss/quectel,l96-i2c.yaml @@ -0,0 +1,10 @@ +# Copyright (c) 2025 Nick Ward +# SPDX-License-Identifier: Apache-2.0 + +description: Quectel L96 GNSS modem accessed through I2C bus + +compatible: "quectel,l96" + +include: + - i2c-device.yaml + - quectel,l96-common.yaml diff --git a/dts/bindings/gnss/quectel,l96-uart.yaml b/dts/bindings/gnss/quectel,l96-uart.yaml new file mode 100644 index 0000000000000..b0326df379d33 --- /dev/null +++ b/dts/bindings/gnss/quectel,l96-uart.yaml @@ -0,0 +1,10 @@ +# Copyright (c) 2025 Nick Ward +# SPDX-License-Identifier: Apache-2.0 + +description: Quectel L96 GNSS modem accessed through UART bus + +compatible: "quectel,l96" + +include: + - uart-device.yaml + - quectel,l96-common.yaml diff --git a/include/zephyr/drivers/gnss/quectel_lx6.h b/include/zephyr/drivers/gnss/quectel_lx6.h new file mode 100644 index 0000000000000..a1861d198f83f --- /dev/null +++ b/include/zephyr/drivers/gnss/quectel_lx6.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Nick Ward + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_GNSS_QUECTEL_LX6_H_ +#define ZEPHYR_DRIVERS_GNSS_QUECTEL_LX6_H_ + +#include + +/** + * @brief Perform a full cold restart of the Quectel LX6 GNSS module. + * + * This function sends the PMTK104 command to trigger a full cold start. + * A full cold start clears all saved GNSS data, including ephemeris, + * almanac, time, position, and configuration. The module will perform + * a complete re-acquisition of satellite data upon reboot. + * + * @note This function must only be called when the driver is active and not + * suspended. + * + * After calling this function, the GNSS module may be temporarily unavailable + * for several hundred milliseconds during its reboot. + * + * @param dev Pointer to the Quectel LX6 device + * + * @retval 0 on success + * @retval -errno code otherwise + */ +int quectel_lx6_cold_start(const struct device *dev); + +#endif /* ZEPHYR_DRIVERS_GNSS_QUECTEL_LX6_H_ */ From 47983b6835e5a92e7dd798be2db3b1510fae03b6 Mon Sep 17 00:00:00 2001 From: Nick Ward Date: Fri, 27 Jun 2025 09:54:34 +1000 Subject: [PATCH 3/4] drivers: gnss: quectel lx6: add WARN_EXPERIMENTAL for UART bus I2C build of driver tested but UART build untested and experimental. Signed-off-by: Nick Ward --- drivers/gnss/Kconfig.quectel_lx6 | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/gnss/Kconfig.quectel_lx6 b/drivers/gnss/Kconfig.quectel_lx6 index f94fe43dd539a..58acea6d6269a 100644 --- a/drivers/gnss/Kconfig.quectel_lx6 +++ b/drivers/gnss/Kconfig.quectel_lx6 @@ -9,6 +9,7 @@ config GNSS_QUECTEL_LX6 depends on GNSS_REFERENCE_FRAME_WGS84 select I2C if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),i2c) select SERIAL if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),uart) + select WARN_EXPERIMENTAL if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),uart) select MODEM_MODULES select MODEM_BACKEND_QUECTEL_I2C if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),i2c) select MODEM_BACKEND_UART if $(dt_compat_on_bus,$(DT_COMPAT_QUECTEL_L96),uart) From 2ca9ac16cdd8d18363f46a0739827f9eb3eee41d Mon Sep 17 00:00:00 2001 From: Nick Ward Date: Fri, 27 Jun 2025 09:28:57 +1000 Subject: [PATCH 4/4] tests: drivers: build_all: gnss: add lx6 driver Add I2C and UART build variants. Signed-off-by: Nick Ward --- tests/drivers/build_all/gnss/app.overlay | 20 ++++++++++++++++++++ tests/drivers/build_all/gnss/prj.conf | 2 ++ 2 files changed, 22 insertions(+) diff --git a/tests/drivers/build_all/gnss/app.overlay b/tests/drivers/build_all/gnss/app.overlay index 7b177c8d97849..eb635480f7a91 100644 --- a/tests/drivers/build_all/gnss/app.overlay +++ b/tests/drivers/build_all/gnss/app.overlay @@ -9,6 +9,21 @@ #address-cells = <1>; #size-cells = <1>; + test_i2c: i2c@11112222 { + #address-cells = <1>; + #size-cells = <0>; + compatible = "zephyr,i2c-emul-controller"; + reg = <0x11112222 0x1000>; + status = "okay"; + clock-frequency = <100000>; + + gnss_l96_i2c: l96@10 { + compatible = "quectel,l96"; + pps-mode = "GNSS_PPS_MODE_ENABLED"; + reg = <0x10>; + }; + }; + test_uart: uart@0 { compatible = "vnd,serial"; reg = <0x0 0x1000>; @@ -28,6 +43,11 @@ pps-mode = "GNSS_PPS_MODE_ENABLED"; }; + gnss_l96: l96 { + compatible = "quectel,l96"; + pps-mode = "GNSS_PPS_MODE_ENABLED"; + }; + gnss_m8: m8 { compatible = "u-blox,m8"; }; diff --git a/tests/drivers/build_all/gnss/prj.conf b/tests/drivers/build_all/gnss/prj.conf index 945eb0df71726..e228edb0fadbd 100644 --- a/tests/drivers/build_all/gnss/prj.conf +++ b/tests/drivers/build_all/gnss/prj.conf @@ -1,3 +1,5 @@ CONFIG_SERIAL=y CONFIG_UART_INTERRUPT_DRIVEN=y CONFIG_GNSS=y +CONFIG_I2C=y +CONFIG_EMUL=y