Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/services/modem/images/cmux_frame.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
482 changes: 482 additions & 0 deletions doc/services/modem/images/cmux_state_machine.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions doc/services/modem/images/modem_pipes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
122 changes: 122 additions & 0 deletions doc/services/modem/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ A modem backend will internally contain an instance of a modem_pipe
structure, alongside any buffers and additional structures required
to abstract away its underlying mechanism.

.. image:: images/modem_pipes.svg
:alt: Modem pipes
:align: center

The modem backend will return a pointer to its internal modem_pipe
structure when initialized, which will be used to interact with the
backend through the modem pipe API.
Expand All @@ -50,6 +54,17 @@ bi-directional streams of data, called DLCI channels. The module
attaches to a single modem backend, exposing multiple modem backends,
each representing a DLCI channel.

Protocol defines simple framing for splitting each DLC into small chunks of data.

.. image:: images/cmux_frame.svg
:alt: CMUX basic frame
:align: center

Zephyr implements the basic frame type, with build-time configurable MTU size.

The module also implements power-saving using CMUX Power Saving Command (PSC) for
modems that support it. See the :ref:`cmux-power-saving` section below for more details.

.. doxygengroup:: modem_cmux

Modem pipelink
Expand All @@ -63,3 +78,110 @@ the users of said pipes. See
use the modem pipelink between device driver and application.

.. doxygengroup:: modem_pipelink

.. _cmux-power-saving:

CMUX Power Saving
*****************

The 3GPP TS 27.010 specifies a power saving mechanism for CMUX which can be used in
Zephyr when the modem supports it.

The power saving mechanism is covered in the following sections on the specification:

* 5.2.5 Inter-frame Fill
* 5.4.6.3.2 Power Saving Control (PSC) message
* 5.4.7 Power Control and Wake-up Mechanisms

The power saving mechanism allows runtime power management for the UART device
used by the CMUX module. When there is no data to be sent or received on any
DLCI channel, the CMUX module will enter the idle state after a configurable
timeout. In the idle state, the CMUX module will send a Power Saving Control message to the
modem, requesting it to enter a low power state. The CMUX module may then
close the pipe device, allowing the UART device to be powered down if
runtime power management is enabled.

When data is to be sent or received on any DLCI channel, the CMUX module
will exit the idle state and wakes the modem up by sending flag characters
until it receives a flag character from the modem.

Some modems allow UART to be powered down only when the DTR (Data Terminal Ready)
signal is de-asserted. In this case, a UART device with DTR support can be used
with the CMUX module to control the DTR signal based on the power state of the UART.

Waking up on incoming data when UART is powered down requires a modem that supports
RING signal to wake up the host.
The RING signal is handled by the modem driver and it opens the pipe device when
the RING signal is detected, allowing the CMUX module to wake up the modem and
process incoming data.

The :zephyr_file:`subsys/modem/modem_cmux.c` module implements the power saving mechanism using the following state machine.

.. image:: images/cmux_state_machine.svg
:alt: CMUX state machine when using power saving
:align: center

Within a connected state, ``modem_cmux_process_received_byte()`` is required to reply repeated flag characters as described in 5.2.5 Inter-frame Fill at the specification.
Idle timer is kept running and cleared on every sent or received frame. Timer expiry will initiate transitioning to power saving modes.

Within the POWERSAVE state all DLC pipes remain open but the pipe towards UART is blocked or closed, so all data is buffered within CMUX ringbuffers to wait for waking up.
Within this state, repeated flag characters are also replied to allow remote end proceed with wake-up procedure as described in 5.4.7.
If pipe is closed, it allows UART device to be powered down if runtime power management is enabled.

When idle timer expires on CONNECTED state, CMUX state machine blocks all DLC pipes and sends PSC command for remote end to initiate transitioning to POWERSAVE state.
When PSC command is replied, CMUX transitions to POWERSAVE mode.

When within the CONNECTED state, the remote end may send a PSC command to initiate the transitioning to power saving mode. CMUX blocks all DLC pipes and sends a PSC response.
When TX buffers are emptied, CMUX enters the POWERSAVE state.

When any of DLC pipes try to transmit data during POWERSAVE state, CMUX buffers it and moves to WAKEUP state that initiates wake-up procedure as specified in 5.4.7 by sending repeated stream of flag characters.
Remote end replies the flag characters to indicate it is ready to receive data. CMUX then stops sending flag characters and moves back to CONNECTED state, resuming normal operation.

The CMUX power saving mechanism can be configured using the following Device Tree properties:

.. code-block:: yaml

cmux-enable-runtime-power-save:
type: boolean
description: Enable runtime power saving using CMUX PSC commands.
This requires modem to support CMUX and PSC commands while keeping the data
connection active.
cmux-close-pipe-on-power-save:
type: boolean
description: Close the modem pipe when entering power save mode.
When runtime power management is enabled, this closes the UART.
This requires modem to support waking up the UART using RING signal.
cmux-idle-timeout-ms:
type: int
description: Time in milliseconds after which CMUX will enter power save mode.
default: 10000


Example Device Tree setup for CMUX with power saving:

.. code-block:: devicetree

&uart1 {
status = "okay";
zephyr,pm-device-runtime-auto;

uart_dtr: uart-dtr {
compatible = "zephyr,uart-dtr";
dtr-gpios = <&interface_to_nrf9160 4 GPIO_ACTIVE_LOW>;
status = "okay";
zephyr,pm-device-runtime-auto;

modem: modem {
compatible = "nordic,nrf91-slm";
status = "okay";
mdm-ring-gpios = <&interface_to_nrf9160 5 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
zephyr,pm-device-runtime-auto;
cmux-enable-runtime-power-save;
cmux-close-pipe-on-power-save;
cmux-idle-timeout-ms = <5000>;
};
};
};

The above example shows a UART device with DTR support being used by a modem that supports CMUX and PSC commands. The DTR signal is used to control the power state of the UART.
The RING signal from the modem is used to wake up the modem subsystem when it is powered down.
113 changes: 83 additions & 30 deletions drivers/modem/modem_cellular.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include <zephyr/modem/backend/uart.h>
#include <zephyr/net/ppp.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#include <zephyr/sys/atomic.h>

#include <zephyr/logging/log.h>
Expand Down Expand Up @@ -95,6 +97,7 @@ enum modem_cellular_event {
MODEM_CELLULAR_EVENT_PPP_DEAD,
MODEM_CELLULAR_EVENT_MODEM_READY,
MODEM_CELLULAR_EVENT_APN_SET,
MODEM_CELLULAR_EVENT_RING,
};

struct modem_cellular_event_cb {
Expand Down Expand Up @@ -166,11 +169,13 @@ struct modem_cellular_data {
/* Event dispatcher */
struct k_work event_dispatch_work;
uint8_t event_buf[8];
struct ring_buf event_rb;
struct k_mutex event_rb_lock;
struct k_pipe event_pipe;

struct k_mutex api_lock;
struct modem_cellular_event_cb cb;

/* Ring interrupt */
struct gpio_callback ring_gpio_cb;
};

struct modem_cellular_user_pipe {
Expand All @@ -187,11 +192,15 @@ struct modem_cellular_config {
struct gpio_dt_spec power_gpio;
struct gpio_dt_spec reset_gpio;
struct gpio_dt_spec wake_gpio;
struct gpio_dt_spec ring_gpio;
uint16_t power_pulse_duration_ms;
uint16_t reset_pulse_duration_ms;
uint16_t startup_time_ms;
uint16_t shutdown_time_ms;
bool autostarts;
bool cmux_enable_runtime_power_save;
bool cmux_close_pipe_on_power_save;
k_timeout_t cmux_idle_timeout;
const struct modem_chat_script *init_chat_script;
const struct modem_chat_script *dial_chat_script;
const struct modem_chat_script *periodic_chat_script;
Expand Down Expand Up @@ -284,6 +293,8 @@ static const char *modem_cellular_event_str(enum modem_cellular_event event)
return "modem ready";
case MODEM_CELLULAR_EVENT_APN_SET:
return "apn set";
case MODEM_CELLULAR_EVENT_RING:
return "RING";
}

return "";
Expand Down Expand Up @@ -721,26 +732,18 @@ static void modem_cellular_event_dispatch_handler(struct k_work *item)
struct modem_cellular_data *data =
CONTAINER_OF(item, struct modem_cellular_data, event_dispatch_work);

uint8_t events[sizeof(data->event_buf)];
uint8_t events_cnt;

k_mutex_lock(&data->event_rb_lock, K_FOREVER);

events_cnt = (uint8_t)ring_buf_get(&data->event_rb, events, sizeof(data->event_buf));
enum modem_cellular_event event;
const size_t len = sizeof(event);

k_mutex_unlock(&data->event_rb_lock);

for (uint8_t i = 0; i < events_cnt; i++) {
modem_cellular_event_handler(data, (enum modem_cellular_event)events[i]);
while (k_pipe_read(&data->event_pipe, (uint8_t *)&event, len, K_NO_WAIT) == len) {
modem_cellular_event_handler(data, (enum modem_cellular_event)event);
}
}

static void modem_cellular_delegate_event(struct modem_cellular_data *data,
enum modem_cellular_event evt)
{
k_mutex_lock(&data->event_rb_lock, K_FOREVER);
ring_buf_put(&data->event_rb, (uint8_t *)&evt, 1);
k_mutex_unlock(&data->event_rb_lock);
k_pipe_write(&data->event_pipe, (const uint8_t *)&evt, sizeof(evt), K_NO_WAIT);
k_work_submit(&data->event_dispatch_work);
}

Expand Down Expand Up @@ -1322,7 +1325,10 @@ static void modem_cellular_run_dial_script_event_handler(struct modem_cellular_d
case MODEM_CELLULAR_EVENT_SUSPEND:
modem_cellular_enter_state(data, MODEM_CELLULAR_STATE_INIT_POWER_OFF);
break;

case MODEM_CELLULAR_EVENT_RING:
LOG_INF("RING received!");
modem_pipe_open_async(data->uart_pipe);
break;
default:
break;
}
Expand Down Expand Up @@ -1367,7 +1373,10 @@ static void modem_cellular_await_registered_event_handler(struct modem_cellular_
case MODEM_CELLULAR_EVENT_SUSPEND:
modem_cellular_enter_state(data, MODEM_CELLULAR_STATE_INIT_POWER_OFF);
break;

case MODEM_CELLULAR_EVENT_RING:
LOG_INF("RING received!");
modem_pipe_open_async(data->uart_pipe);
break;
default:
break;
}
Expand Down Expand Up @@ -1416,7 +1425,10 @@ static void modem_cellular_carrier_on_event_handler(struct modem_cellular_data *
modem_ppp_release(data->ppp);
modem_cellular_enter_state(data, MODEM_CELLULAR_STATE_INIT_POWER_OFF);
break;

case MODEM_CELLULAR_EVENT_RING:
LOG_INF("RING received!");
modem_pipe_open_async(data->uart_pipe);
break;
default:
break;
}
Expand Down Expand Up @@ -2175,6 +2187,15 @@ static void net_mgmt_event_handler(struct net_mgmt_event_callback *cb, uint64_t
}
}

static void modem_cellular_ring_gpio_callback(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
struct modem_cellular_data *data =
CONTAINER_OF(cb, struct modem_cellular_data, ring_gpio_cb);

modem_cellular_delegate_event(data, MODEM_CELLULAR_EVENT_RING);
}

static void modem_cellular_init_apn(struct modem_cellular_data *data)
{
#ifdef CONFIG_MODEM_CELLULAR_APN
Expand Down Expand Up @@ -2203,9 +2224,8 @@ static int modem_cellular_init(const struct device *dev)

k_mutex_init(&data->api_lock);
k_work_init_delayable(&data->timeout_work, modem_cellular_timeout_handler);

k_work_init(&data->event_dispatch_work, modem_cellular_event_dispatch_handler);
ring_buf_init(&data->event_rb, sizeof(data->event_buf), data->event_buf);
k_pipe_init(&data->event_pipe, data->event_buf, sizeof(data->event_buf));

k_sem_init(&data->suspended_sem, 0, 1);

Expand All @@ -2221,6 +2241,33 @@ static int modem_cellular_init(const struct device *dev)
gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT_ACTIVE);
}

if (modem_cellular_gpio_is_enabled(&config->ring_gpio)) {
int ret;

ret = gpio_pin_configure_dt(&config->ring_gpio, GPIO_INPUT);
if (ret < 0) {
LOG_ERR("Failed to configure ring GPIO (%d)", ret);
return ret;
}

gpio_init_callback(&data->ring_gpio_cb, modem_cellular_ring_gpio_callback,
BIT(config->ring_gpio.pin));

ret = gpio_add_callback(config->ring_gpio.port, &data->ring_gpio_cb);
if (ret < 0) {
LOG_ERR("Failed to add ring GPIO callback (%d)", ret);
return ret;
}

ret = gpio_pin_interrupt_configure_dt(&config->ring_gpio, GPIO_INT_EDGE_TO_ACTIVE);
if (ret < 0) {
LOG_ERR("Failed to configure ring GPIO interrupt (%d)", ret);
return ret;
}

LOG_DBG("Ring GPIO interrupt configured");
}

{
const struct modem_backend_uart_config uart_backend_config = {
.uart = config->uart,
Expand All @@ -2244,6 +2291,9 @@ static int modem_cellular_init(const struct device *dev)
.receive_buf_size = ARRAY_SIZE(data->cmux_receive_buf),
.transmit_buf = data->cmux_transmit_buf,
.transmit_buf_size = ARRAY_SIZE(data->cmux_transmit_buf),
.enable_runtime_power_management = config->cmux_enable_runtime_power_save,
.close_pipe_on_power_save = config->cmux_close_pipe_on_power_save,
.idle_timeout = config->cmux_idle_timeout,
};

modem_cmux_init(&data->cmux, &cmux_config);
Expand Down Expand Up @@ -2981,26 +3031,29 @@ MODEM_CHAT_SCRIPT_DEFINE(sqn_gm02s_periodic_chat_script,

/* Helper to define modem instance */
#define MODEM_CELLULAR_DEFINE_INSTANCE(inst, power_ms, reset_ms, startup_ms, shutdown_ms, start, \
set_baudrate_script, \
init_script, \
dial_script, \
periodic_script, \
shutdown_script) \
set_baudrate_script, init_script, dial_script, \
periodic_script, shutdown_script) \
static const struct modem_cellular_config MODEM_CELLULAR_INST_NAME(config, inst) = { \
.uart = DEVICE_DT_GET(DT_INST_BUS(inst)), \
.power_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_power_gpios, {}), \
.reset_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_reset_gpios, {}), \
.wake_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_wake_gpios, {}), \
.ring_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_ring_gpios, {}), \
.power_pulse_duration_ms = (power_ms), \
.reset_pulse_duration_ms = (reset_ms), \
.startup_time_ms = (startup_ms), \
.startup_time_ms = (startup_ms), \
.shutdown_time_ms = (shutdown_ms), \
.autostarts = DT_INST_PROP_OR(inst, autostarts, (start)), \
.set_baudrate_chat_script = (set_baudrate_script), \
.init_chat_script = (init_script), \
.dial_chat_script = (dial_script), \
.cmux_enable_runtime_power_save = \
DT_INST_PROP_OR(inst, cmux_enable_runtime_power_save, 0), \
.cmux_close_pipe_on_power_save = \
DT_INST_PROP_OR(inst, cmux_close_pipe_on_power_save, 0), \
.cmux_idle_timeout = K_MSEC(DT_INST_PROP_OR(inst, cmux_idle_timeout_ms, 0)), \
.set_baudrate_chat_script = (set_baudrate_script), \
.init_chat_script = (init_script), \
.dial_chat_script = (dial_script), \
.periodic_chat_script = (periodic_script), \
.shutdown_chat_script = (shutdown_script), \
.shutdown_chat_script = (shutdown_script), \
.user_pipes = MODEM_CELLULAR_GET_USER_PIPES(inst), \
.user_pipes_size = ARRAY_SIZE(MODEM_CELLULAR_GET_USER_PIPES(inst)), \
}; \
Expand Down
Loading