From 69e283e0f41b1f3b9b94b39334dcffb4095e98c4 Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Tue, 21 Oct 2025 16:55:43 +0300 Subject: [PATCH 1/9] modem: cmux: Define encoding and decoding functions for commands Instead of relying non-standard compiler behavior, define encode and decode functions for all CMUX command structures. Final command is encoded into a shared buffer, because it is always copied directly to TX ringbuffer. Added also functions to validate commands. Signed-off-by: Seppo Takalo --- subsys/modem/modem_cmux.c | 314 +++++++++++++++++------ tests/subsys/modem/modem_cmux/src/main.c | 19 ++ 2 files changed, 250 insertions(+), 83 deletions(-) diff --git a/subsys/modem/modem_cmux.c b/subsys/modem/modem_cmux.c index acb1db3bdddab..a35cfe21b5895 100644 --- a/subsys/modem/modem_cmux.c +++ b/subsys/modem/modem_cmux.c @@ -30,7 +30,8 @@ LOG_MODULE_REGISTER(modem_cmux, CONFIG_MODEM_CMUX_LOG_LEVEL); * * PN would be 10 bytes, but that is not implemented */ -#define MODEM_CMUX_CMD_DATA_SIZE_MAX 5 +#define MODEM_CMUX_CMD_DATA_SIZE_MAX 5 /* Max size of information field of UIH frame */ +#define MODEM_CMUX_CMD_HEADER_SIZE 2 /* command type + length */ #define MODEM_CMUX_CMD_FRAME_SIZE_MAX (MODEM_CMUX_HEADER_SIZE + \ MODEM_CMUX_CMD_DATA_SIZE_MAX) @@ -80,7 +81,7 @@ struct modem_cmux_command_length { struct modem_cmux_command { struct modem_cmux_command_type type; struct modem_cmux_command_length length; - uint8_t value[]; + uint8_t value[MODEM_CMUX_CMD_DATA_SIZE_MAX - 2]; /* Subtract type and length bytes */ }; struct modem_cmux_msc_signals { @@ -98,53 +99,182 @@ struct modem_cmux_msc_addr { uint8_t dlci_address: 6; /**< DLCI channel address */ }; -struct modem_cmux_command_msc { - struct modem_cmux_command command; - uint8_t value[2]; -}; - static struct modem_cmux_dlci *modem_cmux_find_dlci(struct modem_cmux *cmux, uint8_t dlci_address); static void modem_cmux_dlci_notify_transmit_idle(struct modem_cmux *cmux); -static int modem_cmux_wrap_command(struct modem_cmux_command **command, const uint8_t *data, - uint16_t data_len) +static void set_state(struct modem_cmux *cmux, enum modem_cmux_state state) +{ + cmux->state = state; + k_event_set(&cmux->event, BIT(state)); +} + +static bool wait_state(struct modem_cmux *cmux, enum modem_cmux_state state, k_timeout_t timeout) +{ + return k_event_wait(&cmux->event, BIT(state), false, timeout) == BIT(state); +} + +static bool is_connected(struct modem_cmux *cmux) { - if ((data == NULL) || (data_len < 2)) { - return -EINVAL; + return cmux->state == MODEM_CMUX_STATE_CONNECTED; +} + +static bool modem_cmux_command_type_is_valid(const struct modem_cmux_command_type type) +{ + /* All commands are only 7 bits, so EA is always set */ + if (type.ea == 0) { + return false; + } + switch (type.value) { + case MODEM_CMUX_COMMAND_NSC: + case MODEM_CMUX_COMMAND_TEST: + case MODEM_CMUX_COMMAND_PSC: + case MODEM_CMUX_COMMAND_RLS: + case MODEM_CMUX_COMMAND_FCOFF: + case MODEM_CMUX_COMMAND_PN: + case MODEM_CMUX_COMMAND_RPN: + case MODEM_CMUX_COMMAND_FCON: + case MODEM_CMUX_COMMAND_CLD: + case MODEM_CMUX_COMMAND_SNC: + case MODEM_CMUX_COMMAND_MSC: + return true; + default: + return false; } +} - (*command) = (struct modem_cmux_command *)data; +static bool modem_cmux_command_length_is_valid(const struct modem_cmux_command_length length) +{ + /* All commands are shorter than 127 bytes, so EA is always set */ + if (length.ea == 0) { + return false; + } + if (length.value > (MODEM_CMUX_CMD_DATA_SIZE_MAX - MODEM_CMUX_CMD_HEADER_SIZE)) { + return false; + } + return true; +} - if (((*command)->length.ea == 0) || ((*command)->type.ea == 0)) { - return -EINVAL; +static bool modem_cmux_command_is_valid(const struct modem_cmux_command *command) +{ + if (!modem_cmux_command_type_is_valid(command->type)) { + return false; + } + if (!modem_cmux_command_length_is_valid(command->length)) { + return false; + } + /* Verify known value sizes as specified in 3GPP TS 27.010 + * section 5.4.6.3 Message Type and Actions + */ + switch (command->type.value) { + case MODEM_CMUX_COMMAND_PN: + return command->length.value == 8; + case MODEM_CMUX_COMMAND_TEST: + return (command->length.value > 0 && + command->length.value <= MODEM_CMUX_CMD_DATA_SIZE_MAX - 2); + case MODEM_CMUX_COMMAND_MSC: + return command->length.value == 2 || command->length.value == 3; + case MODEM_CMUX_COMMAND_NSC: + return command->length.value == 1; + case MODEM_CMUX_COMMAND_RPN: + return command->length.value == 1 || command->length.value == 8; + case MODEM_CMUX_COMMAND_RLS: + return command->length.value == 2; + case MODEM_CMUX_COMMAND_SNC: + return command->length.value == 1 || command->length.value == 3; + default: + return command->length.value == 0; } +} - if ((*command)->length.value != (data_len - 2)) { - return -EINVAL; +static struct modem_cmux_command_type modem_cmux_command_type_decode(const uint8_t byte) +{ + struct modem_cmux_command_type type = { + .ea = (byte & MODEM_CMUX_EA) ? 1 : 0, + .cr = (byte & MODEM_CMUX_CR) ? 1 : 0, + .value = (byte >> 2) & 0x3F, + }; + + if (type.ea == 0) { + return (struct modem_cmux_command_type){0}; } - return 0; + return type; } -static void set_state(struct modem_cmux *cmux, enum modem_cmux_state state) +static uint8_t modem_cmux_command_type_encode(const struct modem_cmux_command_type type) { - cmux->state = state; - k_event_set(&cmux->event, BIT(state)); + return (type.ea ? MODEM_CMUX_EA : 0) | + (type.cr ? MODEM_CMUX_CR : 0) | + ((type.value & 0x3F) << 2); } -static bool wait_state(struct modem_cmux *cmux, enum modem_cmux_state state, k_timeout_t timeout) +static struct modem_cmux_command_length modem_cmux_command_length_decode(const uint8_t byte) { - return k_event_wait(&cmux->event, BIT(state), false, timeout) == BIT(state); + struct modem_cmux_command_length length = { + .ea = (byte & MODEM_CMUX_EA) ? 1 : 0, + .value = (byte >> 1) & 0x7F, + }; + + if (length.ea == 0) { + return (struct modem_cmux_command_length){0}; + } + + return length; } -static bool is_connected(struct modem_cmux *cmux) +static uint8_t modem_cmux_command_length_encode(const struct modem_cmux_command_length length) { - return cmux->state == MODEM_CMUX_STATE_CONNECTED; + return (length.ea ? MODEM_CMUX_EA : 0) | + ((length.value & 0x7F) << 1); +} + +static struct modem_cmux_command modem_cmux_command_decode(const uint8_t *data, size_t len) +{ + if (len < 2) { + return (struct modem_cmux_command){0}; + } + + struct modem_cmux_command command = { + .type = modem_cmux_command_type_decode(data[0]), + .length = modem_cmux_command_length_decode(data[1]), + }; + + if (command.type.ea == 0 || command.length.ea == 0 || + command.length.value > MODEM_CMUX_CMD_DATA_SIZE_MAX || + (2 + command.length.value) > len) { + return (struct modem_cmux_command){0}; + } + + memcpy(&command.value[0], &data[2], command.length.value); + + return command; } -static struct modem_cmux_command *modem_cmux_command_wrap(const uint8_t *data) +/** + * @brief Encode command into a shared buffer + * + * Not a thread safe, so can only be used within a workqueue context and data + * must be copied out to a TX ringbuffer. + * + * @param command command to encode + * @param len encoded length of the command is written here + * @return pointer to encoded command buffer on success, NULL on failure + */ +static uint8_t *modem_cmux_command_encode(struct modem_cmux_command *command, uint16_t *len) { - return (struct modem_cmux_command *)data; + static uint8_t buf[MODEM_CMUX_CMD_DATA_SIZE_MAX]; + + __ASSERT_NO_MSG(len != NULL); + __ASSERT_NO_MSG(command != NULL); + __ASSERT_NO_MSG(modem_cmux_command_is_valid(command)); + + buf[0] = modem_cmux_command_type_encode(command->type); + buf[1] = modem_cmux_command_length_encode(command->length); + if (command->length.value > 0) { + memcpy(&buf[2], &command->value[0], command->length.value); + } + *len = 2 + command->length.value; + return buf; } static struct modem_cmux_msc_signals modem_cmux_msc_signals_decode(const uint8_t byte) @@ -421,7 +551,7 @@ static bool modem_cmux_transmit_cmd_frame(struct modem_cmux *cmux, const struct modem_cmux_frame *frame) { uint16_t space; - struct modem_cmux_command *command; + struct modem_cmux_command command; k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER); space = ring_buf_space_get(&cmux->transmit_rb); @@ -433,8 +563,9 @@ static bool modem_cmux_transmit_cmd_frame(struct modem_cmux *cmux, } modem_cmux_log_transmit_frame(frame); - if (modem_cmux_wrap_command(&command, frame->data, frame->data_len) == 0) { - modem_cmux_log_transmit_command(command); + command = modem_cmux_command_decode(frame->data, frame->data_len); + if (modem_cmux_command_is_valid(&command)) { + modem_cmux_log_transmit_command(&command); } modem_cmux_transmit_frame(cmux, frame); @@ -477,7 +608,7 @@ static int16_t modem_cmux_transmit_data_frame(struct modem_cmux *cmux, static void modem_cmux_acknowledge_received_frame(struct modem_cmux *cmux) { - struct modem_cmux_command *command; + struct modem_cmux_command_type command; struct modem_cmux_frame frame; uint8_t data[MODEM_CMUX_CMD_DATA_SIZE_MAX]; @@ -488,8 +619,9 @@ static void modem_cmux_acknowledge_received_frame(struct modem_cmux *cmux) memcpy(&frame, &cmux->frame, sizeof(cmux->frame)); memcpy(data, cmux->frame.data, cmux->frame.data_len); - modem_cmux_wrap_command(&command, data, cmux->frame.data_len); - command->type.cr = 0; + command = modem_cmux_command_type_decode(data[0]); + command.cr = 0; + data[0] = modem_cmux_command_type_encode(command); frame.data = data; frame.data_len = cmux->frame.data_len; @@ -516,29 +648,34 @@ static void modem_cmux_send_msc(struct modem_cmux *cmux, struct modem_cmux_dlci .rtr = dlci->state == MODEM_CMUX_DLCI_STATE_OPEN ? 1 : 0, .dv = 1, }; - struct modem_cmux_command_msc cmd = { - .command = { - .type = { - .ea = 1, - .cr = 1, - .value = MODEM_CMUX_COMMAND_MSC, - }, - .length = { - .ea = 1, - .value = sizeof(cmd.value), - }, + struct modem_cmux_command cmd = { + .type = { + .ea = 1, + .cr = 1, + .value = MODEM_CMUX_COMMAND_MSC, + }, + .length = { + .ea = 1, + .value = 2, }, .value[0] = modem_cmux_msc_addr_encode(addr), .value[1] = modem_cmux_msc_signals_encode(signals), }; + uint16_t len; + uint8_t *data = modem_cmux_command_encode(&cmd, &len); + + if (data == NULL) { + return; + } + struct modem_cmux_frame frame = { .dlci_address = 0, .cr = cmux->initiator, .pf = false, .type = MODEM_CMUX_FRAME_TYPE_UIH, - .data = (void *)&cmd, - .data_len = sizeof(cmd), + .data = data, + .data_len = len, }; LOG_DBG("Sending MSC command for DLCI %u, FC:%d RTR: %d DV: %d", addr.dlci_address, @@ -655,41 +792,44 @@ static void modem_cmux_on_control_frame_ua(struct modem_cmux *cmux) static void modem_cmux_respond_unsupported_cmd(struct modem_cmux *cmux) { struct modem_cmux_frame frame = cmux->frame; - struct modem_cmux_command *cmd; + struct modem_cmux_command cmd = modem_cmux_command_decode(frame.data, frame.data_len); - if (modem_cmux_wrap_command(&cmd, frame.data, frame.data_len) < 0) { + if (!modem_cmux_command_is_valid(&cmd)) { LOG_WRN("Invalid command"); return; } - struct { - /* 3GPP TS 27.010: 5.4.6.3.8 Non Supported Command Response (NSC) */ - struct modem_cmux_command nsc; - struct modem_cmux_command_type value; - } nsc_cmd = { - .nsc = { - .type = { - .ea = 1, - .cr = 0, - .value = MODEM_CMUX_COMMAND_NSC, - }, - .length = { - .ea = 1, - .value = 1, - }, + + /* 3GPP TS 27.010: 5.4.6.3.8 Non Supported Command Response (NSC) */ + struct modem_cmux_command nsc_cmd = { + .type = { + .ea = 1, + .cr = 0, + .value = MODEM_CMUX_COMMAND_NSC, + }, + .length = { + .ea = 1, + .value = 1, }, - .value = cmd->type, + .value[0] = modem_cmux_command_type_encode(cmd.type), }; - frame.data = (uint8_t *)&nsc_cmd; - frame.data_len = sizeof(nsc_cmd); + uint16_t len; + uint8_t *data = modem_cmux_command_encode(&nsc_cmd, &len); + + if (data == NULL) { + return; + } + + frame.data = data; + frame.data_len = len; modem_cmux_transmit_cmd_frame(cmux, &frame); } static void modem_cmux_on_control_frame_uih(struct modem_cmux *cmux) { - struct modem_cmux_command *command; + struct modem_cmux_command command; if ((cmux->state != MODEM_CMUX_STATE_CONNECTED) && (cmux->state != MODEM_CMUX_STATE_DISCONNECTING)) { @@ -697,18 +837,19 @@ static void modem_cmux_on_control_frame_uih(struct modem_cmux *cmux) return; } - if (modem_cmux_wrap_command(&command, cmux->frame.data, cmux->frame.data_len) < 0) { + command = modem_cmux_command_decode(cmux->frame.data, cmux->frame.data_len); + if (!modem_cmux_command_is_valid(&command)) { LOG_WRN("Invalid command"); return; } - modem_cmux_log_received_command(command); + modem_cmux_log_received_command(&command); - if (!command->type.cr) { + if (!command.type.cr) { LOG_DBG("Received response command"); - switch (command->type.value) { + switch (command.type.value) { case MODEM_CMUX_COMMAND_CLD: - modem_cmux_on_cld_command(cmux, command); + modem_cmux_on_cld_command(cmux, &command); break; default: /* Responses to other commands are ignored */ @@ -717,13 +858,13 @@ static void modem_cmux_on_control_frame_uih(struct modem_cmux *cmux) return; } - switch (command->type.value) { + switch (command.type.value) { case MODEM_CMUX_COMMAND_CLD: - modem_cmux_on_cld_command(cmux, command); + modem_cmux_on_cld_command(cmux, &command); break; case MODEM_CMUX_COMMAND_MSC: - modem_cmux_on_msc_command(cmux, command); + modem_cmux_on_msc_command(cmux, &command); break; case MODEM_CMUX_COMMAND_FCON: @@ -1321,8 +1462,6 @@ static void modem_cmux_disconnect_handler(struct k_work *item) { struct k_work_delayable *dwork = k_work_delayable_from_work(item); struct modem_cmux *cmux = CONTAINER_OF(dwork, struct modem_cmux, disconnect_work); - struct modem_cmux_command *command; - uint8_t data[2]; if (cmux->state == MODEM_CMUX_STATE_DISCONNECTING) { disconnect(cmux); @@ -1331,12 +1470,21 @@ static void modem_cmux_disconnect_handler(struct k_work *item) k_work_schedule(&cmux->disconnect_work, MODEM_CMUX_T1_TIMEOUT); } - command = modem_cmux_command_wrap(data); - command->type.ea = 1; - command->type.cr = 1; - command->type.value = MODEM_CMUX_COMMAND_CLD; - command->length.ea = 1; - command->length.value = 0; + struct modem_cmux_command command = { + .type.ea = 1, + .type.cr = 1, + .type.value = MODEM_CMUX_COMMAND_CLD, + .length.ea = 1, + .length.value = 0, + }; + + uint16_t len; + uint8_t *data = modem_cmux_command_encode(&command, &len); + + if (data == NULL) { + return; + } + struct modem_cmux_frame frame = { .dlci_address = 0, @@ -1344,7 +1492,7 @@ static void modem_cmux_disconnect_handler(struct k_work *item) .pf = false, .type = MODEM_CMUX_FRAME_TYPE_UIH, .data = data, - .data_len = sizeof(data), + .data_len = len, }; /* Transmit close down command */ diff --git a/tests/subsys/modem/modem_cmux/src/main.c b/tests/subsys/modem/modem_cmux/src/main.c index 67833baabe76c..b40af7e0de898 100644 --- a/tests/subsys/modem/modem_cmux/src/main.c +++ b/tests/subsys/modem/modem_cmux/src/main.c @@ -937,4 +937,23 @@ ZTEST(modem_cmux, test_modem_cmux_invalid_cr) zassert_false(events, "Wrong CMD should have been ignored"); } +ZTEST(modem_cmux, test_modem_cmux_invalid_command) +{ + static uint8_t invalid_cmd[] = {0xF9, 0x03, 0xEF, 0x09, 0x00, + 0x00, 0x00, 0x00, 0xFB, 0xF9}; + uint32_t events; + + modem_backend_mock_put(&bus_mock, invalid_cmd, + sizeof(invalid_cmd)); + + events = k_event_wait_all(&cmux_event, + (MODEM_CMUX_EVENT_CONNECTED | MODEM_CMUX_EVENT_DISCONNECTED), + false, K_SECONDS(1)); + + zassert_false(events, "Wrong CMD should have been ignored"); + + /* Invalid command should not cause any response */ + zassert_equal(0, modem_backend_mock_get(&bus_mock, buffer1, sizeof(buffer1))); +} + ZTEST_SUITE(modem_cmux, NULL, test_modem_cmux_setup, test_modem_cmux_before, NULL, NULL); From 7ca1c992d1b4bbb81f3ebafd995aa0ce85acaabb Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Thu, 23 Oct 2025 16:39:48 +0300 Subject: [PATCH 2/9] drivers: modem: Implement support for DTR signal DTR signal on UART extends the power saving by allowing host to indicate the remote end that the UART is not in active state. Signed-off-by: Seppo Takalo --- drivers/modem/modem_cellular.c | 9 +++++++++ dts/bindings/modem/zephyr,cellular-modem-device.yaml | 8 ++++++++ include/zephyr/modem/backend/uart.h | 2 ++ subsys/modem/backends/modem_backend_uart.c | 1 + subsys/modem/backends/modem_backend_uart_async.c | 8 ++++++++ subsys/modem/backends/modem_backend_uart_async_hwfc.c | 8 ++++++++ 6 files changed, 36 insertions(+) diff --git a/drivers/modem/modem_cellular.c b/drivers/modem/modem_cellular.c index c09c9a12795ad..ec2a8ba1018cd 100644 --- a/drivers/modem/modem_cellular.c +++ b/drivers/modem/modem_cellular.c @@ -187,6 +187,7 @@ 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 dtr_gpio; uint16_t power_pulse_duration_ms; uint16_t reset_pulse_duration_ms; uint16_t startup_time_ms; @@ -2198,6 +2199,7 @@ static int modem_cellular_init(const struct device *dev) { struct modem_cellular_data *data = (struct modem_cellular_data *)dev->data; struct modem_cellular_config *config = (struct modem_cellular_config *)dev->config; + const struct gpio_dt_spec *dtr_gpio = NULL; data->dev = dev; @@ -2221,9 +2223,15 @@ 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->dtr_gpio)) { + gpio_pin_configure_dt(&config->dtr_gpio, GPIO_OUTPUT_INACTIVE); + dtr_gpio = &config->dtr_gpio; + } + { const struct modem_backend_uart_config uart_backend_config = { .uart = config->uart, + .dtr_gpio = dtr_gpio, .receive_buf = data->uart_backend_receive_buf, .receive_buf_size = ARRAY_SIZE(data->uart_backend_receive_buf), .transmit_buf = data->uart_backend_transmit_buf, @@ -2991,6 +2999,7 @@ MODEM_CHAT_SCRIPT_DEFINE(sqn_gm02s_periodic_chat_script, .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, {}), \ + .dtr_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_dtr_gpios, {}), \ .power_pulse_duration_ms = (power_ms), \ .reset_pulse_duration_ms = (reset_ms), \ .startup_time_ms = (startup_ms), \ diff --git a/dts/bindings/modem/zephyr,cellular-modem-device.yaml b/dts/bindings/modem/zephyr,cellular-modem-device.yaml index bff39d7f9f613..66c7fd91d092f 100644 --- a/dts/bindings/modem/zephyr,cellular-modem-device.yaml +++ b/dts/bindings/modem/zephyr,cellular-modem-device.yaml @@ -17,3 +17,11 @@ properties: mdm-wake-gpios: type: phandle-array description: GPIO for modem wake + + mdm-dtr-gpios: + type: phandle-array + description: | + GPIO for modem data terminal ready. + + Asserted (logical high) when UART is active and + deasserted (logical low) when UART is inactive, powered down or in low power mode. diff --git a/include/zephyr/modem/backend/uart.h b/include/zephyr/modem/backend/uart.h index 73054b792d8e4..6407af8cf6afa 100644 --- a/include/zephyr/modem/backend/uart.h +++ b/include/zephyr/modem/backend/uart.h @@ -68,6 +68,7 @@ struct modem_backend_uart_async { struct modem_backend_uart { const struct device *uart; + const struct gpio_dt_spec *dtr_gpio; struct modem_pipe pipe; struct k_work_delayable receive_ready_work; struct k_work transmit_idle_work; @@ -85,6 +86,7 @@ struct modem_backend_uart { struct modem_backend_uart_config { const struct device *uart; + const struct gpio_dt_spec *dtr_gpio; /* Address must be word-aligned when CONFIG_MODEM_BACKEND_UART_ASYNC_HWFC is enabled. */ uint8_t *receive_buf; uint32_t receive_buf_size; diff --git a/subsys/modem/backends/modem_backend_uart.c b/subsys/modem/backends/modem_backend_uart.c index 82242a76c0196..13a35406f182f 100644 --- a/subsys/modem/backends/modem_backend_uart.c +++ b/subsys/modem/backends/modem_backend_uart.c @@ -39,6 +39,7 @@ struct modem_pipe *modem_backend_uart_init(struct modem_backend_uart *backend, memset(backend, 0x00, sizeof(*backend)); backend->uart = config->uart; + backend->dtr_gpio = config->dtr_gpio; k_work_init_delayable(&backend->receive_ready_work, modem_backend_uart_receive_ready_handler); k_work_init(&backend->transmit_idle_work, modem_backend_uart_transmit_idle_handler); diff --git a/subsys/modem/backends/modem_backend_uart_async.c b/subsys/modem/backends/modem_backend_uart_async.c index 9b5edc8965c52..1615120a4d5e0 100644 --- a/subsys/modem/backends/modem_backend_uart_async.c +++ b/subsys/modem/backends/modem_backend_uart_async.c @@ -11,6 +11,7 @@ LOG_MODULE_REGISTER(modem_backend_uart_async, CONFIG_MODEM_MODULES_LOG_LEVEL); #include +#include #include enum { @@ -157,6 +158,10 @@ static int modem_backend_uart_async_open(void *data) atomic_clear(&backend->async.common.state); ring_buf_reset(&backend->async.receive_rb); + if (backend->dtr_gpio) { + gpio_pin_set_dt(backend->dtr_gpio, 1); + } + atomic_set_bit(&backend->async.common.state, MODEM_BACKEND_UART_ASYNC_STATE_RX_BUF0_USED_BIT); atomic_set_bit(&backend->async.common.state, MODEM_BACKEND_UART_ASYNC_STATE_RECEIVING_BIT); @@ -268,6 +273,9 @@ static int modem_backend_uart_async_close(void *data) atomic_clear_bit(&backend->async.common.state, MODEM_BACKEND_UART_ASYNC_STATE_OPEN_BIT); uart_tx_abort(backend->uart); uart_rx_disable(backend->uart); + if (backend->dtr_gpio) { + gpio_pin_set_dt(backend->dtr_gpio, 0); + } return 0; } diff --git a/subsys/modem/backends/modem_backend_uart_async_hwfc.c b/subsys/modem/backends/modem_backend_uart_async_hwfc.c index 61d307604e5c5..3aaaad2e196c7 100644 --- a/subsys/modem/backends/modem_backend_uart_async_hwfc.c +++ b/subsys/modem/backends/modem_backend_uart_async_hwfc.c @@ -11,6 +11,7 @@ LOG_MODULE_REGISTER(modem_backend_uart_async_hwfc, CONFIG_MODEM_MODULES_LOG_LEVEL); #include +#include #include struct rx_buf_t { @@ -221,6 +222,10 @@ static int modem_backend_uart_async_hwfc_open(void *data) return -ENOMEM; } + if (backend->dtr_gpio) { + gpio_pin_set_dt(backend->dtr_gpio, 1); + } + atomic_clear(&backend->async.common.state); atomic_set_bit(&backend->async.common.state, MODEM_BACKEND_UART_ASYNC_STATE_OPEN_BIT); @@ -357,6 +362,9 @@ static int modem_backend_uart_async_hwfc_close(void *data) uart_rx_disable(backend->uart); } + if (backend->dtr_gpio) { + gpio_pin_set_dt(backend->dtr_gpio, 0); + } return 0; } From ea0bc5de921896148cb9b23905441e5990350f5c Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Tue, 14 Oct 2025 21:04:46 +0000 Subject: [PATCH 3/9] modem: cmux: Implement Power Saving Control message Signal powersaving mode for the remote end using PSC command. Wakes up the remote end from powersaving mode by sending flag characters. This method is defined in 3GPP TS 27.010. Sections 5.4.6.3.2 Power Saving Control (PSC) and 5.4.7 Power Control and Wake-up Mechanisms. Essentially it is one PSC command to indicate a sleep state, and then repeated flag characters to wake up the remote end or indicate that we have been woken up. Signed-off-by: Seppo Takalo --- include/zephyr/modem/cmux.h | 5 ++ subsys/modem/Kconfig | 8 ++ subsys/modem/modem_cmux.c | 175 +++++++++++++++++++++++++++++++----- 3 files changed, 168 insertions(+), 20 deletions(-) diff --git a/include/zephyr/modem/cmux.h b/include/zephyr/modem/cmux.h index bf349980e2c95..912edb1b79ef4 100644 --- a/include/zephyr/modem/cmux.h +++ b/include/zephyr/modem/cmux.h @@ -72,6 +72,10 @@ enum modem_cmux_state { MODEM_CMUX_STATE_DISCONNECTED = 0, MODEM_CMUX_STATE_CONNECTING, MODEM_CMUX_STATE_CONNECTED, + MODEM_CMUX_STATE_ENTER_POWERSAVE, + MODEM_CMUX_STATE_POWERSAVE, + MODEM_CMUX_STATE_CONFIRM_POWERSAVE, + MODEM_CMUX_STATE_WAKEUP, MODEM_CMUX_STATE_DISCONNECTING, }; @@ -187,6 +191,7 @@ struct modem_cmux { /* Synchronize actions */ struct k_event event; + k_timepoint_t t3_timepoint; /* Statistics */ #if CONFIG_MODEM_STATS diff --git a/subsys/modem/Kconfig b/subsys/modem/Kconfig index dfcb7ddae9a38..8b4a411c26f65 100644 --- a/subsys/modem/Kconfig +++ b/subsys/modem/Kconfig @@ -64,6 +64,14 @@ config MODEM_CMUX_WORK_BUFFER_SIZE_EXTRA Extra bytes to add to the work buffers used by the CMUX module. The default size of these buffers is MODEM_CMUX_MTU + 7 (CMUX header size). +config MODEM_CMUX_T3_TIMEOUT + int "CMUX T3 timeout in seconds" + range 1 255 + default 10 + help + Response Timer for wake-up procedure(T3). + Time in seconds before the link is considered dead. + module = MODEM_CMUX module-str = modem_cmux source "subsys/logging/Kconfig.template.log_config" diff --git a/subsys/modem/modem_cmux.c b/subsys/modem/modem_cmux.c index a35cfe21b5895..a14be736742d0 100644 --- a/subsys/modem/modem_cmux.c +++ b/subsys/modem/modem_cmux.c @@ -37,9 +37,7 @@ LOG_MODULE_REGISTER(modem_cmux, CONFIG_MODEM_CMUX_LOG_LEVEL); #define MODEM_CMUX_T1_TIMEOUT (K_MSEC(330)) #define MODEM_CMUX_T2_TIMEOUT (K_MSEC(660)) - -#define MODEM_CMUX_EVENT_CONNECTED_BIT (BIT(0)) -#define MODEM_CMUX_EVENT_DISCONNECTED_BIT (BIT(1)) +#define MODEM_CMUX_T3_TIMEOUT (K_SECONDS(CONFIG_MODEM_CMUX_T3_TIMEOUT)) enum modem_cmux_frame_types { MODEM_CMUX_FRAME_TYPE_RR = 0x01, @@ -101,6 +99,7 @@ struct modem_cmux_msc_addr { static struct modem_cmux_dlci *modem_cmux_find_dlci(struct modem_cmux *cmux, uint8_t dlci_address); static void modem_cmux_dlci_notify_transmit_idle(struct modem_cmux *cmux); +static void modem_cmux_tx_bypass(struct modem_cmux *cmux, const uint8_t *data, size_t len); static void set_state(struct modem_cmux *cmux, enum modem_cmux_state state) { @@ -118,6 +117,22 @@ static bool is_connected(struct modem_cmux *cmux) return cmux->state == MODEM_CMUX_STATE_CONNECTED; } +static bool is_powersaving(struct modem_cmux *cmux) +{ + return cmux->state == MODEM_CMUX_STATE_POWERSAVE; +} + +static bool is_waking_up(struct modem_cmux *cmux) +{ + return cmux->state == MODEM_CMUX_STATE_WAKEUP; +} + +static bool is_transitioning_to_powersave(struct modem_cmux *cmux) +{ + return (cmux->state == MODEM_CMUX_STATE_ENTER_POWERSAVE || + cmux->state == MODEM_CMUX_STATE_CONFIRM_POWERSAVE); +} + static bool modem_cmux_command_type_is_valid(const struct modem_cmux_command_type type) { /* All commands are only 7 bits, so EA is always set */ @@ -581,7 +596,7 @@ static int16_t modem_cmux_transmit_data_frame(struct modem_cmux *cmux, k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER); - if (cmux->flow_control_on == false) { + if (cmux->flow_control_on == false || is_transitioning_to_powersave(cmux)) { k_mutex_unlock(&cmux->transmit_rb_lock); return 0; } @@ -630,6 +645,34 @@ static void modem_cmux_acknowledge_received_frame(struct modem_cmux *cmux) } } +static void modem_cmux_send_psc(struct modem_cmux *cmux) +{ + uint16_t len; + uint8_t *data = modem_cmux_command_encode(&(struct modem_cmux_command){ + .type.ea = 1, + .type.cr = 1, + .type.value = MODEM_CMUX_COMMAND_PSC, + .length.ea = 1, + .length.value = 0, + }, &len); + + if (data == NULL) { + return; + } + + struct modem_cmux_frame frame = { + .dlci_address = 0, + .cr = cmux->initiator, + .pf = true, + .type = MODEM_CMUX_FRAME_TYPE_UIH, + .data = data, + .data_len = len, + }; + + LOG_DBG("Sending PSC command"); + modem_cmux_transmit_cmd_frame(cmux, &frame); +} + static void modem_cmux_send_msc(struct modem_cmux *cmux, struct modem_cmux_dlci *dlci) { if (cmux == NULL || dlci == NULL) { @@ -827,12 +870,28 @@ static void modem_cmux_respond_unsupported_cmd(struct modem_cmux *cmux) modem_cmux_transmit_cmd_frame(cmux, &frame); } +static void modem_cmux_on_psc_command(struct modem_cmux *cmux) +{ + LOG_DBG("Received power saving command"); + k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER); + set_state(cmux, MODEM_CMUX_STATE_CONFIRM_POWERSAVE); + modem_cmux_acknowledge_received_frame(cmux); + k_mutex_unlock(&cmux->transmit_rb_lock); +} + +static void modem_cmux_on_psc_response(struct modem_cmux *cmux) +{ + LOG_DBG("Enter power saving"); + k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER); + set_state(cmux, MODEM_CMUX_STATE_POWERSAVE); + k_mutex_unlock(&cmux->transmit_rb_lock); +} + static void modem_cmux_on_control_frame_uih(struct modem_cmux *cmux) { struct modem_cmux_command command; - if ((cmux->state != MODEM_CMUX_STATE_CONNECTED) && - (cmux->state != MODEM_CMUX_STATE_DISCONNECTING)) { + if (cmux->state < MODEM_CMUX_STATE_CONNECTED) { LOG_DBG("Unexpected UIH frame"); return; } @@ -851,6 +910,9 @@ static void modem_cmux_on_control_frame_uih(struct modem_cmux *cmux) case MODEM_CMUX_COMMAND_CLD: modem_cmux_on_cld_command(cmux, &command); break; + case MODEM_CMUX_COMMAND_PSC: + modem_cmux_on_psc_response(cmux); + break; default: /* Responses to other commands are ignored */ break; @@ -875,6 +937,10 @@ static void modem_cmux_on_control_frame_uih(struct modem_cmux *cmux) modem_cmux_on_fcoff_command(cmux); break; + case MODEM_CMUX_COMMAND_PSC: + modem_cmux_on_psc_command(cmux); + break; + default: LOG_DBG("Unknown control command"); modem_cmux_respond_unsupported_cmd(cmux); @@ -1140,6 +1206,11 @@ static void modem_cmux_on_frame(struct modem_cmux *cmux) modem_cmux_advertise_receive_buf_stats(cmux); #endif + if (is_powersaving(cmux) || is_waking_up(cmux)) { + set_state(cmux, MODEM_CMUX_STATE_CONNECTED); + LOG_DBG("Exit powersave on received frame"); + } + if (cmux->frame.dlci_address == 0) { modem_cmux_on_control_frame(cmux); } else { @@ -1170,8 +1241,10 @@ static void modem_cmux_process_received_byte(struct modem_cmux *cmux, uint8_t by switch (cmux->receive_state) { case MODEM_CMUX_RECEIVE_STATE_SOF: + cmux->frame_header_len = 0; if (byte == MODEM_CMUX_SOF) { cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_RESYNC; + cmux->frame_header_len++; break; } @@ -1183,6 +1256,20 @@ static void modem_cmux_process_received_byte(struct modem_cmux *cmux, uint8_t by * 0xF9 could also be a valid address field for DLCI 62. */ if (byte == MODEM_CMUX_SOF) { + /* Use "header_len" to count SOF bytes, only start transmitting + * flag bytes after receiving more than 3 flags. + * Don't reply flags if we are transitioning between modes or + * if T3 timer is still active (suppress residual flags). + */ + cmux->frame_header_len++; + if ((is_powersaving(cmux) || + (is_connected(cmux) && sys_timepoint_expired(cmux->t3_timepoint))) && + cmux->frame_header_len > 3) { + modem_cmux_tx_bypass(cmux, &(char){MODEM_CMUX_SOF}, 1); + } + if (is_waking_up(cmux)) { + k_work_reschedule(&cmux->transmit_work, K_NO_WAIT); + } break; } @@ -1351,21 +1438,15 @@ static void modem_cmux_receive_handler(struct k_work *item) int ret; /* Receive data from pipe */ - ret = modem_pipe_receive(cmux->pipe, cmux->work_buf, sizeof(cmux->work_buf)); - if (ret < 1) { - if (ret < 0) { - LOG_ERR("Pipe receiving error: %d", ret); + while ((ret = modem_pipe_receive(cmux->pipe, cmux->work_buf, sizeof(cmux->work_buf))) > 0) { + /* Process received data */ + for (int i = 0; i < ret; i++) { + modem_cmux_process_received_byte(cmux, cmux->work_buf[i]); } - return; } - - /* Process received data */ - for (int i = 0; i < ret; i++) { - modem_cmux_process_received_byte(cmux, cmux->work_buf[i]); + if (ret < 0) { + LOG_ERR("Pipe receiving error: %d", ret); } - - /* Reschedule received work */ - modem_work_schedule(&cmux->receive_work, K_NO_WAIT); } static void modem_cmux_dlci_notify_transmit_idle(struct modem_cmux *cmux) @@ -1381,6 +1462,51 @@ static void modem_cmux_dlci_notify_transmit_idle(struct modem_cmux *cmux) } } +/** Transmit bytes bypassing the CMUX buffers. + * Causes modem_cmux_transmit_handler() to be rescheduled as a result of TRANSMIT_IDLE event. + */ +static void modem_cmux_tx_bypass(struct modem_cmux *cmux, const uint8_t *data, size_t len) +{ + if (cmux == NULL) { + return; + } + + modem_pipe_transmit(cmux->pipe, data, len); +} + +static bool powersave_wait_wakeup(struct modem_cmux *cmux) +{ + static const uint8_t wakeup_pattern[] = {MODEM_CMUX_SOF, MODEM_CMUX_SOF, MODEM_CMUX_SOF, + MODEM_CMUX_SOF, MODEM_CMUX_SOF}; + int ret; + + if (is_powersaving(cmux)) { + LOG_DBG("Power saving mode, wake up first"); + set_state(cmux, MODEM_CMUX_STATE_WAKEUP); + cmux->t3_timepoint = sys_timepoint_calc(MODEM_CMUX_T3_TIMEOUT); + modem_cmux_tx_bypass(cmux, wakeup_pattern, sizeof(wakeup_pattern)); + return true; + } + + if (is_waking_up(cmux)) { + if (sys_timepoint_expired(cmux->t3_timepoint)) { + LOG_ERR("Wake up timed out, link dead"); + set_state(cmux, MODEM_CMUX_STATE_DISCONNECTED); + modem_cmux_raise_event(cmux, MODEM_CMUX_EVENT_DISCONNECTED); + return true; + } + if (cmux->receive_state != MODEM_CMUX_RECEIVE_STATE_RESYNC) { + /* Retry single flag, until remote wakes up */ + modem_cmux_tx_bypass(cmux, &(uint8_t){MODEM_CMUX_SOF}, 1); + return true; + } + set_state(cmux, MODEM_CMUX_STATE_CONNECTED); + LOG_DBG("Woke up from power saving mode"); + } + + return false; +} + static void modem_cmux_transmit_handler(struct k_work *item) { struct k_work_delayable *dwork = k_work_delayable_from_work(item); @@ -1403,6 +1529,11 @@ static void modem_cmux_transmit_handler(struct k_work *item) break; } + if (powersave_wait_wakeup(cmux)) { + k_mutex_unlock(&cmux->transmit_rb_lock); + return; + } + reserved_size = ring_buf_get_claim(&cmux->transmit_rb, &reserved, UINT32_MAX); ret = modem_pipe_transmit(cmux->pipe, reserved, reserved_size); @@ -1423,11 +1554,14 @@ static void modem_cmux_transmit_handler(struct k_work *item) } } - k_mutex_unlock(&cmux->transmit_rb_lock); - if (transmit_rb_empty) { + if (cmux->state == MODEM_CMUX_STATE_CONFIRM_POWERSAVE) { + set_state(cmux, MODEM_CMUX_STATE_POWERSAVE); + LOG_DBG("Entered power saving mode"); + } modem_cmux_dlci_notify_transmit_idle(cmux); } + k_mutex_unlock(&cmux->transmit_rb_lock); } static void modem_cmux_connect_handler(struct k_work *item) @@ -1726,6 +1860,7 @@ void modem_cmux_init(struct modem_cmux *cmux, const struct modem_cmux_config *co cmux->user_data = config->user_data; cmux->receive_buf = config->receive_buf; cmux->receive_buf_size = config->receive_buf_size; + cmux->t3_timepoint = sys_timepoint_calc(K_NO_WAIT); sys_slist_init(&cmux->dlcis); ring_buf_init(&cmux->transmit_rb, config->transmit_buf_size, config->transmit_buf); k_mutex_init(&cmux->transmit_rb_lock); From 14aa98f1c7988832641612756cf30c7513bee0ae Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Fri, 10 Oct 2025 11:14:53 +0300 Subject: [PATCH 4/9] drivers: modem: Implement runtime power management for CMUX CMUX driver can enable the support for idle-timer in devicetree and can be requested to shut down the pipe during sleep. Then UART backend put the actual device into sleep when pipe is closed. Waking up is requested by sending data to DLC pipe or by manually opening the uart_pipe. Modem may request similar wake-up by a RING interrupt which would open the same pipe. When UART is powered and pipe is not closed, CMUX wake-up procedure is automatic. Either end may initiate the wake-up. Signed-off-by: Seppo Takalo --- drivers/modem/modem_cellular.c | 31 +++--- .../modem/zephyr,cellular-modem-device.yaml | 19 ++++ include/zephyr/modem/cmux.h | 14 +++ .../modem/backends/modem_backend_uart_async.c | 13 +++ .../backends/modem_backend_uart_async_hwfc.c | 14 +++ subsys/modem/modem_cmux.c | 96 ++++++++++++++++++- 6 files changed, 171 insertions(+), 16 deletions(-) diff --git a/drivers/modem/modem_cellular.c b/drivers/modem/modem_cellular.c index ec2a8ba1018cd..8809421c5abf2 100644 --- a/drivers/modem/modem_cellular.c +++ b/drivers/modem/modem_cellular.c @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include @@ -193,6 +195,9 @@ struct modem_cellular_config { 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; @@ -2205,7 +2210,6 @@ 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); @@ -2252,6 +2256,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); @@ -2989,11 +2996,8 @@ 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, {}), \ @@ -3002,14 +3006,19 @@ MODEM_CHAT_SCRIPT_DEFINE(sqn_gm02s_periodic_chat_script, .dtr_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_dtr_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)), \ }; \ diff --git a/dts/bindings/modem/zephyr,cellular-modem-device.yaml b/dts/bindings/modem/zephyr,cellular-modem-device.yaml index 66c7fd91d092f..c2d2e9e00a88f 100644 --- a/dts/bindings/modem/zephyr,cellular-modem-device.yaml +++ b/dts/bindings/modem/zephyr,cellular-modem-device.yaml @@ -25,3 +25,22 @@ properties: Asserted (logical high) when UART is active and deasserted (logical low) when UART is inactive, powered down or in low power mode. + + 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 diff --git a/include/zephyr/modem/cmux.h b/include/zephyr/modem/cmux.h index 912edb1b79ef4..18bb6fff980d4 100644 --- a/include/zephyr/modem/cmux.h +++ b/include/zephyr/modem/cmux.h @@ -159,6 +159,12 @@ struct modem_cmux { enum modem_cmux_state state; bool flow_control_on : 1; bool initiator : 1; + /** Enable runtime power management */ + bool enable_runtime_power_management; + /** Close pipe on power save */ + bool close_pipe_on_power_save; + /** Idle timeout for power save */ + k_timeout_t idle_timeout; /* Work lock */ bool attached : 1; @@ -188,10 +194,12 @@ struct modem_cmux { struct k_work_delayable transmit_work; struct k_work_delayable connect_work; struct k_work_delayable disconnect_work; + struct k_work_delayable runtime_pm_work; /* Synchronize actions */ struct k_event event; k_timepoint_t t3_timepoint; + k_timepoint_t idle_timepoint; /* Statistics */ #if CONFIG_MODEM_STATS @@ -220,6 +228,12 @@ struct modem_cmux_config { uint8_t *transmit_buf; /** Size of transmit buffer in bytes [149, ...] */ uint16_t transmit_buf_size; + /** Enable runtime power management */ + bool enable_runtime_power_management; + /** Close pipe on power save */ + bool close_pipe_on_power_save; + /** Idle timeout for power save */ + k_timeout_t idle_timeout; }; /** diff --git a/subsys/modem/backends/modem_backend_uart_async.c b/subsys/modem/backends/modem_backend_uart_async.c index 1615120a4d5e0..0cfcff9e86edd 100644 --- a/subsys/modem/backends/modem_backend_uart_async.c +++ b/subsys/modem/backends/modem_backend_uart_async.c @@ -11,6 +11,7 @@ LOG_MODULE_REGISTER(modem_backend_uart_async, CONFIG_MODEM_MODULES_LOG_LEVEL); #include +#include #include #include @@ -158,6 +159,11 @@ static int modem_backend_uart_async_open(void *data) atomic_clear(&backend->async.common.state); ring_buf_reset(&backend->async.receive_rb); + ret = pm_device_runtime_get(backend->uart); + if (ret < 0) { + LOG_ERR("Failed to power on UART: %d", ret); + return ret; + } if (backend->dtr_gpio) { gpio_pin_set_dt(backend->dtr_gpio, 1); } @@ -269,6 +275,7 @@ static int modem_backend_uart_async_receive(void *data, uint8_t *buf, size_t siz static int modem_backend_uart_async_close(void *data) { struct modem_backend_uart *backend = (struct modem_backend_uart *)data; + int ret; atomic_clear_bit(&backend->async.common.state, MODEM_BACKEND_UART_ASYNC_STATE_OPEN_BIT); uart_tx_abort(backend->uart); @@ -276,6 +283,12 @@ static int modem_backend_uart_async_close(void *data) if (backend->dtr_gpio) { gpio_pin_set_dt(backend->dtr_gpio, 0); } + ret = pm_device_runtime_put_async(backend->uart, K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to power off UART: %d", ret); + return ret; + } + modem_pipe_notify_closed(&backend->pipe); return 0; } diff --git a/subsys/modem/backends/modem_backend_uart_async_hwfc.c b/subsys/modem/backends/modem_backend_uart_async_hwfc.c index 3aaaad2e196c7..df69c40b6679b 100644 --- a/subsys/modem/backends/modem_backend_uart_async_hwfc.c +++ b/subsys/modem/backends/modem_backend_uart_async_hwfc.c @@ -11,6 +11,7 @@ LOG_MODULE_REGISTER(modem_backend_uart_async_hwfc, CONFIG_MODEM_MODULES_LOG_LEVEL); #include +#include #include #include @@ -222,6 +223,11 @@ static int modem_backend_uart_async_hwfc_open(void *data) return -ENOMEM; } + ret = pm_device_runtime_get(backend->uart); + if (ret < 0) { + LOG_ERR("Failed to power on UART: %d", ret); + return ret; + } if (backend->dtr_gpio) { gpio_pin_set_dt(backend->dtr_gpio, 1); } @@ -352,6 +358,7 @@ static int modem_backend_uart_async_hwfc_receive(void *data, uint8_t *buf, size_ static int modem_backend_uart_async_hwfc_close(void *data) { struct modem_backend_uart *backend = (struct modem_backend_uart *)data; + int ret; atomic_clear_bit(&backend->async.common.state, MODEM_BACKEND_UART_ASYNC_STATE_OPEN_BIT); uart_tx_abort(backend->uart); @@ -365,6 +372,13 @@ static int modem_backend_uart_async_hwfc_close(void *data) if (backend->dtr_gpio) { gpio_pin_set_dt(backend->dtr_gpio, 0); } + ret = pm_device_runtime_put_async(backend->uart, K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to power off UART: %d", ret); + return ret; + } + modem_pipe_notify_closed(&backend->pipe); + return 0; } diff --git a/subsys/modem/modem_cmux.c b/subsys/modem/modem_cmux.c index a14be736742d0..3f1bb3bf41e20 100644 --- a/subsys/modem/modem_cmux.c +++ b/subsys/modem/modem_cmux.c @@ -10,6 +10,8 @@ LOG_MODULE_REGISTER(modem_cmux, CONFIG_MODEM_CMUX_LOG_LEVEL); #include #include #include +#include +#include #include @@ -100,6 +102,7 @@ struct modem_cmux_msc_addr { static struct modem_cmux_dlci *modem_cmux_find_dlci(struct modem_cmux *cmux, uint8_t dlci_address); static void modem_cmux_dlci_notify_transmit_idle(struct modem_cmux *cmux); static void modem_cmux_tx_bypass(struct modem_cmux *cmux, const uint8_t *data, size_t len); +static void runtime_pm_keepalive(struct modem_cmux *cmux); static void set_state(struct modem_cmux *cmux, enum modem_cmux_state state) { @@ -497,10 +500,17 @@ static void modem_cmux_bus_callback(struct modem_pipe *pipe, enum modem_pipe_eve modem_work_schedule(&cmux->receive_work, K_NO_WAIT); break; + case MODEM_PIPE_EVENT_OPENED: + cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_SOF; + modem_work_schedule(&cmux->transmit_work, K_NO_WAIT); + break; case MODEM_PIPE_EVENT_TRANSMIT_IDLE: + /* If we keep UART open in power-save, we should avoid waking up on RX idle */ + if (!cmux->close_pipe_on_power_save && is_powersaving(cmux)) { + break; + } modem_work_schedule(&cmux->transmit_work, K_NO_WAIT); break; - default: break; } @@ -885,6 +895,10 @@ static void modem_cmux_on_psc_response(struct modem_cmux *cmux) k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER); set_state(cmux, MODEM_CMUX_STATE_POWERSAVE); k_mutex_unlock(&cmux->transmit_rb_lock); + + if (cmux->close_pipe_on_power_save) { + modem_pipe_close_async(cmux->pipe); + } } static void modem_cmux_on_control_frame_uih(struct modem_cmux *cmux) @@ -1437,6 +1451,8 @@ static void modem_cmux_receive_handler(struct k_work *item) struct modem_cmux *cmux = CONTAINER_OF(dwork, struct modem_cmux, receive_work); int ret; + runtime_pm_keepalive(cmux); + /* Receive data from pipe */ while ((ret = modem_pipe_receive(cmux->pipe, cmux->work_buf, sizeof(cmux->work_buf))) > 0) { /* Process received data */ @@ -1462,6 +1478,46 @@ static void modem_cmux_dlci_notify_transmit_idle(struct modem_cmux *cmux) } } +static void modem_cmux_runtime_pm_handler(struct k_work *item) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(item); + struct modem_cmux *cmux = CONTAINER_OF(dwork, struct modem_cmux, runtime_pm_work); + + if (!cmux->enable_runtime_power_management) { + return; + } + + bool expired = sys_timepoint_expired(cmux->idle_timepoint); + + if (!expired) { + return; + } + + if (is_connected(cmux) && expired) { + LOG_DBG("Idle timeout, entering power saving mode"); + set_state(cmux, MODEM_CMUX_STATE_ENTER_POWERSAVE); + modem_cmux_send_psc(cmux); + k_work_reschedule(&cmux->runtime_pm_work, MODEM_CMUX_T3_TIMEOUT); + return; + } + if (cmux->state == MODEM_CMUX_STATE_ENTER_POWERSAVE) { + LOG_WRN("PSC timeout, not entering power saving mode"); + set_state(cmux, MODEM_CMUX_STATE_CONNECTED); + runtime_pm_keepalive(cmux); + return; + } +} + +static void runtime_pm_keepalive(struct modem_cmux *cmux) +{ + if (cmux == NULL || !cmux->enable_runtime_power_management) { + return; + } + + cmux->idle_timepoint = sys_timepoint_calc(cmux->idle_timeout); + k_work_reschedule(&cmux->runtime_pm_work, cmux->idle_timeout); +} + /** Transmit bytes bypassing the CMUX buffers. * Causes modem_cmux_transmit_handler() to be rescheduled as a result of TRANSMIT_IDLE event. */ @@ -1483,6 +1539,17 @@ static bool powersave_wait_wakeup(struct modem_cmux *cmux) if (is_powersaving(cmux)) { LOG_DBG("Power saving mode, wake up first"); set_state(cmux, MODEM_CMUX_STATE_WAKEUP); + + if (cmux->close_pipe_on_power_save) { + ret = modem_pipe_open(cmux->pipe, K_FOREVER); + if (ret < 0) { + LOG_ERR("Failed to open pipe for wake up (%d)", ret); + set_state(cmux, MODEM_CMUX_STATE_DISCONNECTED); + modem_cmux_raise_event(cmux, MODEM_CMUX_EVENT_DISCONNECTED); + return true; + } + } + cmux->t3_timepoint = sys_timepoint_calc(MODEM_CMUX_T3_TIMEOUT); modem_cmux_tx_bypass(cmux, wakeup_pattern, sizeof(wakeup_pattern)); return true; @@ -1522,18 +1589,22 @@ static void modem_cmux_transmit_handler(struct k_work *item) modem_cmux_advertise_transmit_buf_stats(cmux); #endif + if (!is_transitioning_to_powersave(cmux)) { + runtime_pm_keepalive(cmux); + } + while (true) { transmit_rb_empty = ring_buf_is_empty(&cmux->transmit_rb); - if (transmit_rb_empty) { - break; - } - if (powersave_wait_wakeup(cmux)) { k_mutex_unlock(&cmux->transmit_rb_lock); return; } + if (transmit_rb_empty) { + break; + } + reserved_size = ring_buf_get_claim(&cmux->transmit_rb, &reserved, UINT32_MAX); ret = modem_pipe_transmit(cmux->pipe, reserved, reserved_size); @@ -1558,6 +1629,9 @@ static void modem_cmux_transmit_handler(struct k_work *item) if (cmux->state == MODEM_CMUX_STATE_CONFIRM_POWERSAVE) { set_state(cmux, MODEM_CMUX_STATE_POWERSAVE); LOG_DBG("Entered power saving mode"); + if (cmux->close_pipe_on_power_save) { + modem_pipe_close_async(cmux->pipe); + } } modem_cmux_dlci_notify_transmit_idle(cmux); } @@ -1693,6 +1767,13 @@ static int modem_cmux_dlci_pipe_api_transmit(void *data, const uint8_t *buf, siz struct modem_cmux *cmux = dlci->cmux; int ret = 0; + if (size == 0 || buf == NULL) { + /* Allow empty transmit request to wake up CMUX */ + runtime_pm_keepalive(cmux); + k_work_reschedule(&cmux->transmit_work, K_NO_WAIT); + return 0; + } + if (dlci->flow_control) { return 0; } @@ -1861,6 +1942,10 @@ void modem_cmux_init(struct modem_cmux *cmux, const struct modem_cmux_config *co cmux->receive_buf = config->receive_buf; cmux->receive_buf_size = config->receive_buf_size; cmux->t3_timepoint = sys_timepoint_calc(K_NO_WAIT); + cmux->enable_runtime_power_management = config->enable_runtime_power_management; + cmux->close_pipe_on_power_save = config->close_pipe_on_power_save; + cmux->idle_timeout = + cmux->enable_runtime_power_management ? config->idle_timeout : K_FOREVER; sys_slist_init(&cmux->dlcis); ring_buf_init(&cmux->transmit_rb, config->transmit_buf_size, config->transmit_buf); k_mutex_init(&cmux->transmit_rb_lock); @@ -1868,6 +1953,7 @@ void modem_cmux_init(struct modem_cmux *cmux, const struct modem_cmux_config *co k_work_init_delayable(&cmux->transmit_work, modem_cmux_transmit_handler); k_work_init_delayable(&cmux->connect_work, modem_cmux_connect_handler); k_work_init_delayable(&cmux->disconnect_work, modem_cmux_disconnect_handler); + k_work_init_delayable(&cmux->runtime_pm_work, modem_cmux_runtime_pm_handler); k_event_init(&cmux->event); set_state(cmux, MODEM_CMUX_STATE_DISCONNECTED); From 6a3801cf1568bd72d8eb9e70f49c4529fd0ab88f Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Thu, 9 Oct 2025 15:47:56 +0300 Subject: [PATCH 5/9] drivers: modem: cellular: Use k_pipe instead of ringbuffer Ringbuffer is not safe in ISR but k_pipe without waiting is. So use pipe for events, so that possible GPIO callbacks from ISR content can post events. Signed-off-by: Seppo Takalo --- drivers/modem/modem_cellular.c | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/drivers/modem/modem_cellular.c b/drivers/modem/modem_cellular.c index 8809421c5abf2..41beedcf98f82 100644 --- a/drivers/modem/modem_cellular.c +++ b/drivers/modem/modem_cellular.c @@ -168,8 +168,7 @@ 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; @@ -727,26 +726,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; + enum modem_cellular_event event; + const size_t len = sizeof(event); - k_mutex_lock(&data->event_rb_lock, K_FOREVER); - - events_cnt = (uint8_t)ring_buf_get(&data->event_rb, events, sizeof(data->event_buf)); - - 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); } @@ -2211,7 +2202,7 @@ 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); From bf677da3963a5cbfa6ffcec8d85b370ec7f09deb Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Wed, 10 Sep 2025 16:17:12 +0300 Subject: [PATCH 6/9] drivers: modem: Implement support for RING indicator Use ring indicator to wake up the CMUX device from sleep. Only used for runtime power management, but same event could be used for initiating idle -> connected as well. Signed-off-by: Seppo Takalo --- drivers/modem/modem_cellular.c | 59 ++++++++++++++++++- .../modem/zephyr,cellular-modem-device.yaml | 4 ++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/drivers/modem/modem_cellular.c b/drivers/modem/modem_cellular.c index 41beedcf98f82..09c76a5c8ef9a 100644 --- a/drivers/modem/modem_cellular.c +++ b/drivers/modem/modem_cellular.c @@ -97,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 { @@ -172,6 +173,9 @@ struct modem_cellular_data { struct k_mutex api_lock; struct modem_cellular_event_cb cb; + + /* Ring interrupt */ + struct gpio_callback ring_gpio_cb; }; struct modem_cellular_user_pipe { @@ -188,6 +192,7 @@ 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; struct gpio_dt_spec dtr_gpio; uint16_t power_pulse_duration_ms; uint16_t reset_pulse_duration_ms; @@ -289,6 +294,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 ""; @@ -1319,7 +1326,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; } @@ -1364,7 +1374,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; } @@ -1413,7 +1426,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; } @@ -2172,6 +2188,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 @@ -2218,6 +2243,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"); + } + if (modem_cellular_gpio_is_enabled(&config->dtr_gpio)) { gpio_pin_configure_dt(&config->dtr_gpio, GPIO_OUTPUT_INACTIVE); dtr_gpio = &config->dtr_gpio; @@ -2994,6 +3046,7 @@ MODEM_CHAT_SCRIPT_DEFINE(sqn_gm02s_periodic_chat_script, .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, {}), \ .dtr_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, mdm_dtr_gpios, {}), \ .power_pulse_duration_ms = (power_ms), \ .reset_pulse_duration_ms = (reset_ms), \ diff --git a/dts/bindings/modem/zephyr,cellular-modem-device.yaml b/dts/bindings/modem/zephyr,cellular-modem-device.yaml index c2d2e9e00a88f..d2ab0056841b2 100644 --- a/dts/bindings/modem/zephyr,cellular-modem-device.yaml +++ b/dts/bindings/modem/zephyr,cellular-modem-device.yaml @@ -18,6 +18,10 @@ properties: type: phandle-array description: GPIO for modem wake + mdm-ring-gpios: + type: phandle-array + description: GPIO for modem ring indicator + mdm-dtr-gpios: type: phandle-array description: | From d53d5d9b6bf9425ca421e56a763d7c7f1e4a616d Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Fri, 10 Oct 2025 16:30:16 +0300 Subject: [PATCH 7/9] doc: modem: Add documentation regarding CMUX power saving Add documentation and state machine diagrams for CMUX power saving feature and how to use it with Zephyr. Signed-off-by: Seppo Takalo --- doc/services/modem/images/cmux_frame.svg | 9 + .../modem/images/cmux_state_machine.svg | 482 ++++++++++++++++++ doc/services/modem/images/modem_pipes.svg | 9 + doc/services/modem/index.rst | 122 +++++ 4 files changed, 622 insertions(+) create mode 100644 doc/services/modem/images/cmux_frame.svg create mode 100644 doc/services/modem/images/cmux_state_machine.svg create mode 100644 doc/services/modem/images/modem_pipes.svg diff --git a/doc/services/modem/images/cmux_frame.svg b/doc/services/modem/images/cmux_frame.svg new file mode 100644 index 0000000000000..1ddfbaf026c76 --- /dev/null +++ b/doc/services/modem/images/cmux_frame.svg @@ -0,0 +1,9 @@ + + + + +

Flag


1 octet

Address


1 octet

Control


1 octet

Flag


1 octet

Length indicator


1 or 2 octet

Information


Unspecified length

FCS


1 octet

diff --git a/doc/services/modem/images/cmux_state_machine.svg b/doc/services/modem/images/cmux_state_machine.svg new file mode 100644 index 0000000000000..a27dee309e5a8 --- /dev/null +++ b/doc/services/modem/images/cmux_state_machine.svg @@ -0,0 +1,482 @@ + + + + + + + + + + CONNECTED +   + + + + + CONFIRM_POWERSAVE + Send PSC command ack + + + + + WAKEUP + Start transmitting flags + + + + + + ENTER_POWERSAVE + + + + + POWERSAVE + + + + + + + + + + + pipe transmit + reset idle timer + + + + + + Idle timeout expires + + + + + + PSC command acknowledged + + + + + + Receive PSC command + + + + + + Transmit ringbuffer empty + + + + + + DLC pipe transmit + + + + + + flags received + + + + + + Receive frame + + + + diff --git a/doc/services/modem/images/modem_pipes.svg b/doc/services/modem/images/modem_pipes.svg new file mode 100644 index 0000000000000..f662ad84caef8 --- /dev/null +++ b/doc/services/modem/images/modem_pipes.svg @@ -0,0 +1,9 @@ + + + + +
UART
driver
UART
API
Chat
Modem
UART backend
UART
API
PIPE
API
CMUX
PIPE
API
PIPE
API
Alternative
PPP
PPP
Chat
Alternative
PIPE
API
diff --git a/doc/services/modem/index.rst b/doc/services/modem/index.rst index 97ae11ecbc89d..cf1e90d5502e1 100644 --- a/doc/services/modem/index.rst +++ b/doc/services/modem/index.rst @@ -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. @@ -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 @@ -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. From e6b9c43ab224e79badc1d8e29d5f92ea95d0167a Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Mon, 13 Oct 2025 11:36:49 +0300 Subject: [PATCH 8/9] modem: cmux: Add struct cmux_config into struct cmux Instead of copying all fields from cmux_config into run-time struct cmux, just have the configuration structure as a member. Signed-off-by: Seppo Takalo --- include/zephyr/modem/cmux.h | 63 +++++++++++++++---------------------- subsys/modem/modem_cmux.c | 59 ++++++++++++++++------------------ 2 files changed, 52 insertions(+), 70 deletions(-) diff --git a/include/zephyr/modem/cmux.h b/include/zephyr/modem/cmux.h index 18bb6fff980d4..0faa8d5d4b388 100644 --- a/include/zephyr/modem/cmux.h +++ b/include/zephyr/modem/cmux.h @@ -53,6 +53,30 @@ enum modem_cmux_event { typedef void (*modem_cmux_callback)(struct modem_cmux *cmux, enum modem_cmux_event event, void *user_data); +/** + * @brief Contains CMUX instance configuration data + */ +struct modem_cmux_config { + /** Invoked when event occurs */ + modem_cmux_callback callback; + /** Free to use pointer passed to event handler when invoked */ + void *user_data; + /** Receive buffer */ + uint8_t *receive_buf; + /** Size of receive buffer in bytes [127, ...] */ + uint16_t receive_buf_size; + /** Transmit buffer */ + uint8_t *transmit_buf; + /** Size of transmit buffer in bytes [149, ...] */ + uint16_t transmit_buf_size; + /** Enable runtime power management */ + bool enable_runtime_power_management; + /** Close pipe on power save */ + bool close_pipe_on_power_save; + /** Idle timeout for power save */ + k_timeout_t idle_timeout; +}; + /** * @cond INTERNAL_HIDDEN */ @@ -148,10 +172,6 @@ struct modem_cmux { /* Bus pipe */ struct modem_pipe *pipe; - /* Event handler */ - modem_cmux_callback callback; - void *user_data; - /* DLCI channel contexts */ sys_slist_t dlcis; @@ -159,12 +179,6 @@ struct modem_cmux { enum modem_cmux_state state; bool flow_control_on : 1; bool initiator : 1; - /** Enable runtime power management */ - bool enable_runtime_power_management; - /** Close pipe on power save */ - bool close_pipe_on_power_save; - /** Idle timeout for power save */ - k_timeout_t idle_timeout; /* Work lock */ bool attached : 1; @@ -172,10 +186,6 @@ struct modem_cmux { /* Receive state*/ enum modem_cmux_receive_state receive_state; - - /* Receive buffer */ - uint8_t *receive_buf; - uint16_t receive_buf_size; uint16_t receive_buf_len; uint8_t work_buf[MODEM_CMUX_WORK_BUFFER_SIZE]; @@ -206,36 +216,13 @@ struct modem_cmux { struct modem_stats_buffer receive_buf_stats; struct modem_stats_buffer transmit_buf_stats; #endif + struct modem_cmux_config config; }; /** * @endcond */ -/** - * @brief Contains CMUX instance configuration data - */ -struct modem_cmux_config { - /** Invoked when event occurs */ - modem_cmux_callback callback; - /** Free to use pointer passed to event handler when invoked */ - void *user_data; - /** Receive buffer */ - uint8_t *receive_buf; - /** Size of receive buffer in bytes [127, ...] */ - uint16_t receive_buf_size; - /** Transmit buffer */ - uint8_t *transmit_buf; - /** Size of transmit buffer in bytes [149, ...] */ - uint16_t transmit_buf_size; - /** Enable runtime power management */ - bool enable_runtime_power_management; - /** Close pipe on power save */ - bool close_pipe_on_power_save; - /** Idle timeout for power save */ - k_timeout_t idle_timeout; -}; - /** * @brief Initialize CMUX instance * @param cmux CMUX instance diff --git a/subsys/modem/modem_cmux.c b/subsys/modem/modem_cmux.c index 3f1bb3bf41e20..194541e867916 100644 --- a/subsys/modem/modem_cmux.c +++ b/subsys/modem/modem_cmux.c @@ -400,7 +400,7 @@ static uint32_t modem_cmux_get_receive_buf_length(struct modem_cmux *cmux) static uint32_t modem_cmux_get_receive_buf_size(struct modem_cmux *cmux) { - return cmux->receive_buf_size; + return cmux->config.receive_buf_size; } static uint32_t modem_cmux_get_transmit_buf_length(struct modem_cmux *cmux) @@ -483,11 +483,11 @@ static void modem_cmux_log_received_command(const struct modem_cmux_command *com static void modem_cmux_raise_event(struct modem_cmux *cmux, enum modem_cmux_event event) { - if (cmux->callback == NULL) { + if (cmux->config.callback == NULL) { return; } - cmux->callback(cmux, event, cmux->user_data); + cmux->config.callback(cmux, event, cmux->config.user_data); } static void modem_cmux_bus_callback(struct modem_pipe *pipe, enum modem_pipe_event event, @@ -506,7 +506,7 @@ static void modem_cmux_bus_callback(struct modem_pipe *pipe, enum modem_pipe_eve break; case MODEM_PIPE_EVENT_TRANSMIT_IDLE: /* If we keep UART open in power-save, we should avoid waking up on RX idle */ - if (!cmux->close_pipe_on_power_save && is_powersaving(cmux)) { + if (!cmux->config.close_pipe_on_power_save && is_powersaving(cmux)) { break; } modem_work_schedule(&cmux->transmit_work, K_NO_WAIT); @@ -896,7 +896,7 @@ static void modem_cmux_on_psc_response(struct modem_cmux *cmux) set_state(cmux, MODEM_CMUX_STATE_POWERSAVE); k_mutex_unlock(&cmux->transmit_rb_lock); - if (cmux->close_pipe_on_power_save) { + if (cmux->config.close_pipe_on_power_save) { modem_pipe_close_async(cmux->pipe); } } @@ -1244,8 +1244,8 @@ static void modem_cmux_drop_frame(struct modem_cmux *cmux) #if defined(CONFIG_MODEM_CMUX_LOG_LEVEL_DBG) struct modem_cmux_frame *frame = &cmux->frame; - frame->data = cmux->receive_buf; - modem_cmux_log_frame(frame, "dropped", MIN(frame->data_len, cmux->receive_buf_size)); + frame->data = cmux->config.receive_buf; + modem_cmux_log_frame(frame, "dropped", MIN(frame->data_len, cmux->config.receive_buf_size)); #endif } @@ -1369,9 +1369,9 @@ static void modem_cmux_process_received_byte(struct modem_cmux *cmux, uint8_t by break; } - if (cmux->frame.data_len > cmux->receive_buf_size) { + if (cmux->frame.data_len > cmux->config.receive_buf_size) { LOG_ERR("Indicated frame data length %u exceeds receive buffer size %u", - cmux->frame.data_len, cmux->receive_buf_size); + cmux->frame.data_len, cmux->config.receive_buf_size); modem_cmux_drop_frame(cmux); break; @@ -1383,8 +1383,8 @@ static void modem_cmux_process_received_byte(struct modem_cmux *cmux, uint8_t by case MODEM_CMUX_RECEIVE_STATE_DATA: /* Copy byte to data */ - if (cmux->receive_buf_len < cmux->receive_buf_size) { - cmux->receive_buf[cmux->receive_buf_len] = byte; + if (cmux->receive_buf_len < cmux->config.receive_buf_size) { + cmux->config.receive_buf[cmux->receive_buf_len] = byte; } cmux->receive_buf_len++; @@ -1397,9 +1397,9 @@ static void modem_cmux_process_received_byte(struct modem_cmux *cmux, uint8_t by break; case MODEM_CMUX_RECEIVE_STATE_FCS: - if (cmux->receive_buf_len > cmux->receive_buf_size) { - LOG_WRN("Receive buffer overrun (%u > %u)", - cmux->receive_buf_len, cmux->receive_buf_size); + if (cmux->receive_buf_len > cmux->config.receive_buf_size) { + LOG_WRN("Receive buffer overrun (%u > %u)", cmux->receive_buf_len, + cmux->config.receive_buf_size); modem_cmux_drop_frame(cmux); break; } @@ -1433,7 +1433,7 @@ static void modem_cmux_process_received_byte(struct modem_cmux *cmux, uint8_t by } /* Process frame */ - cmux->frame.data = cmux->receive_buf; + cmux->frame.data = cmux->config.receive_buf; modem_cmux_on_frame(cmux); /* Await start of next frame */ @@ -1483,7 +1483,7 @@ static void modem_cmux_runtime_pm_handler(struct k_work *item) struct k_work_delayable *dwork = k_work_delayable_from_work(item); struct modem_cmux *cmux = CONTAINER_OF(dwork, struct modem_cmux, runtime_pm_work); - if (!cmux->enable_runtime_power_management) { + if (!cmux->config.enable_runtime_power_management) { return; } @@ -1510,12 +1510,12 @@ static void modem_cmux_runtime_pm_handler(struct k_work *item) static void runtime_pm_keepalive(struct modem_cmux *cmux) { - if (cmux == NULL || !cmux->enable_runtime_power_management) { + if (cmux == NULL || !cmux->config.enable_runtime_power_management) { return; } - cmux->idle_timepoint = sys_timepoint_calc(cmux->idle_timeout); - k_work_reschedule(&cmux->runtime_pm_work, cmux->idle_timeout); + cmux->idle_timepoint = sys_timepoint_calc(cmux->config.idle_timeout); + k_work_reschedule(&cmux->runtime_pm_work, cmux->config.idle_timeout); } /** Transmit bytes bypassing the CMUX buffers. @@ -1540,7 +1540,7 @@ static bool powersave_wait_wakeup(struct modem_cmux *cmux) LOG_DBG("Power saving mode, wake up first"); set_state(cmux, MODEM_CMUX_STATE_WAKEUP); - if (cmux->close_pipe_on_power_save) { + if (cmux->config.close_pipe_on_power_save) { ret = modem_pipe_open(cmux->pipe, K_FOREVER); if (ret < 0) { LOG_ERR("Failed to open pipe for wake up (%d)", ret); @@ -1629,7 +1629,7 @@ static void modem_cmux_transmit_handler(struct k_work *item) if (cmux->state == MODEM_CMUX_STATE_CONFIRM_POWERSAVE) { set_state(cmux, MODEM_CMUX_STATE_POWERSAVE); LOG_DBG("Entered power saving mode"); - if (cmux->close_pipe_on_power_save) { + if (cmux->config.close_pipe_on_power_save) { modem_pipe_close_async(cmux->pipe); } } @@ -1936,18 +1936,13 @@ void modem_cmux_init(struct modem_cmux *cmux, const struct modem_cmux_config *co __ASSERT_NO_MSG(config->transmit_buf != NULL); __ASSERT_NO_MSG(config->transmit_buf_size >= MODEM_CMUX_DATA_FRAME_SIZE_MAX); - memset(cmux, 0x00, sizeof(*cmux)); - cmux->callback = config->callback; - cmux->user_data = config->user_data; - cmux->receive_buf = config->receive_buf; - cmux->receive_buf_size = config->receive_buf_size; - cmux->t3_timepoint = sys_timepoint_calc(K_NO_WAIT); - cmux->enable_runtime_power_management = config->enable_runtime_power_management; - cmux->close_pipe_on_power_save = config->close_pipe_on_power_save; - cmux->idle_timeout = - cmux->enable_runtime_power_management ? config->idle_timeout : K_FOREVER; + *cmux = (struct modem_cmux){ + .t3_timepoint = sys_timepoint_calc(K_NO_WAIT), + .config = *config, + }; sys_slist_init(&cmux->dlcis); - ring_buf_init(&cmux->transmit_rb, config->transmit_buf_size, config->transmit_buf); + ring_buf_init(&cmux->transmit_rb, cmux->config.transmit_buf_size, + cmux->config.transmit_buf); k_mutex_init(&cmux->transmit_rb_lock); k_work_init_delayable(&cmux->receive_work, modem_cmux_receive_handler); k_work_init_delayable(&cmux->transmit_work, modem_cmux_transmit_handler); From 74f8d9a5a815c349fb6db6a28ad4ae4186602549 Mon Sep 17 00:00:00 2001 From: Seppo Takalo Date: Mon, 13 Oct 2025 12:00:33 +0300 Subject: [PATCH 9/9] modem: pipe: Don't return EPERM on closed pipe When working on CMUX power saving, it is typical that we end up closing the pipe before the last RX_READY event is handled from workqueue, so we end up receiving -EPERM which is not really a fatal error. Pipes recover when they are re-opened. So drop this error and return zero instead, like modem_pipe_open() and close() does. Signed-off-by: Seppo Takalo --- subsys/modem/modem_pipe.c | 4 ++-- tests/subsys/modem/modem_pipe/src/main.c | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/subsys/modem/modem_pipe.c b/subsys/modem/modem_pipe.c index bf642b3984dc5..ef66b9085ea61 100644 --- a/subsys/modem/modem_pipe.c +++ b/subsys/modem/modem_pipe.c @@ -136,7 +136,7 @@ void modem_pipe_attach(struct modem_pipe *pipe, modem_pipe_api_callback callback int modem_pipe_transmit(struct modem_pipe *pipe, const uint8_t *buf, size_t size) { if (!pipe_test_events(pipe, PIPE_EVENT_OPENED_BIT)) { - return -EPERM; + return 0; } pipe_clear_events(pipe, PIPE_EVENT_TRANSMIT_IDLE_BIT); @@ -146,7 +146,7 @@ int modem_pipe_transmit(struct modem_pipe *pipe, const uint8_t *buf, size_t size int modem_pipe_receive(struct modem_pipe *pipe, uint8_t *buf, size_t size) { if (!pipe_test_events(pipe, PIPE_EVENT_OPENED_BIT)) { - return -EPERM; + return 0; } pipe_clear_events(pipe, PIPE_EVENT_RECEIVE_READY_BIT); diff --git a/tests/subsys/modem/modem_pipe/src/main.c b/tests/subsys/modem/modem_pipe/src/main.c index 3cb330550fd52..7889d111d1b95 100644 --- a/tests/subsys/modem/modem_pipe/src/main.c +++ b/tests/subsys/modem/modem_pipe/src/main.c @@ -327,6 +327,14 @@ static void test_pipe_notify_receive_ready(void) "Unexpected state %u", (uint32_t)atomic_get(&test_state)); } +static void test_pipe_receive_closed(void) +{ + /* Try to receive from a closed pipe - should return 0 */ + zassert_equal(modem_pipe_receive(test_pipe, test_buffer, test_buffer_size), 0, + "Reading from closed pipe should return 0"); + zassert_false(test_backend.receive_called, "receive should not be called on closed pipe"); +} + ZTEST(modem_pipe, test_async_open_close) { test_pipe_open(); @@ -397,5 +405,16 @@ ZTEST(modem_pipe, test_attach) test_pipe_attach_receive_not_ready_transmit_idle(); } +ZTEST(modem_pipe, test_receive_closed) +{ + test_pipe_open(); + test_reset(); + test_pipe_async_transmit(); + test_reset(); + test_pipe_close(); + /* Test reading from a closed pipe should return 0 */ + test_pipe_receive_closed(); +} + ZTEST_SUITE(modem_pipe, NULL, modem_backend_fake_setup, modem_backend_fake_before, modem_backend_fake_after, NULL);