В этой статье работа идет с версией Zephyr 3.7.99.
Драйверы модема в Zephyr
При разработке устройства на Zephyr, которому требовалась сотовая связь, я решил использовать внешний модем с управлением через AT-команды. В Zephyr есть возможность пользоваться внешним модемом, управляемым AT-командами, используя драйвер. Для использования модема в Zephyr существует 2 вида драйверов: modem_cellular и modem_context.
modem_context дает более полный контроль над операциями с модемом. Но он, похоже, устарел. При попытке собрать проект с modem_context возникали конфликты между ним и другими частями системы. Поэтому решено было использовать modem_cellular.
modem_cellular представляет собой более высокоуровневый подход. Он обеспечивает интеграцию модема с сетевым стеком Zephyr, предоставляя стандартный интерфейс net_if.
Благодаря этому пользователь может работать с модемом через API управления сетью.
Проблема modem_cellular
modem_cellular забирает у пользователя возможность прямой передачи команд модему. Все операции с модемом проходят через другие драйверы сетевого стека. У modem_cellular есть некоторые функции в API, но они лишь возвращают некоторую информацию о модеме. Таким образом, мы лишены возможности пользоваться частью функционала модема. В этой статье имеются пути решения проблемы. Это будет показано на примере добавления возможности отправки AT-команд в драйвере modem_cellular.
Решение 1 – расширение api драйвера
Решение было предложено в ходе ответов на вопрос об использовании драйвера. В нем рекомендуется создать собственный драйвер на основе modem_cellular и реализовать в нем нужные функции.
В Zephyr для работы с AT-командами используется модуль modem_chat. Он создается драйвером, но недоступен в пользовательском пространстве. Поэтому в API мы добавим функцию, возвращающую этот modem_chat.
Изменения, которые нужно сделать для расширения API:
1. В файле include/zephyr/drivers/cellular.h добавляем новый typedef и обновляем структуру API:
/** API for getting modem chat instance */
typedef const struct modem_chat *(*cellular_api_get_chat)(const struct device *dev);
__subsystem struct cellular_driver_api {
cellular_api_configure_networks configure_networks;
cellular_api_get_supported_networks get_supported_networks;
cellular_api_get_signal get_signal;
cellular_api_get_modem_info get_modem_info;
cellular_api_get_registration_status get_registration_status;
cellular_api_get_chat get_chat;
};
2. Добавляем inline-функцию для пользовательского уровня в конец cellular.h:
/**
* @brief Get modem chat interface for the device
*
* @param dev Cellular network device instance
*
* @retval pointer to modem_chat if supported
*/
static inline const struct modem_chat *cellular_get_chat(const struct device *dev)
{
const struct cellular_driver_api *api =
(const struct cellular_driver_api *)dev->api;
if (api->get_chat == NULL) {
return NULL;
}
return api->get_chat(dev);
}
3. В драйвере modem_cellular.c добавляется функция:
static const struct modem_chat *modem_cellular_get_chat(const struct device *dev)
{
const struct modem_cellular_data *data = dev->data;
return &data->chat;
}
и подключается к API-таблице:
const static struct cellular_driver_api modem_cellular_api = {
.get_signal = modem_cellular_get_signal,
.get_modem_info = modem_cellular_get_modem_info,
.get_registration_status = modem_cellular_get_registration_status,
.get_chat = modem_cellular_get_chat,
};
Все сделано. Теперь можем пользоваться modem_chat с пользовательского уровня:
#include <zephyr/drivers/cellular.h>
#include <zephyr/drivers/modem/modem_chat.h>
void main(void)
{
struct device *modem = {DEVICE_DT_GET(DT_ALIAS(modem))}; // зависит от содержания device tree
struct modem_chat *chat = cellular_get_chat(dev);
}
Решение 2 – использование одной из структур данных драйвера на пользовательском уровне
Решение 1 более предпочтительно, но если нет желания редактировать системные файлы, то можно добиться того же результата, просто скопировав код объявления одной из структур из драйвера на пользовательский уровень. Ниже описано, как это сделать.
Сам драйвер способен оправлять AT-команды с помощью модуля modem_chat. Драйвер создает структуру modem_chat при инициализации. Наша задача – получить эту структуру, чтобы пользоваться ей на уровне прикладной программы для отправки AT-команд.
При изучении кода драйвера modem_cellular можно заметить, что указатель на modem_chat находится в другой структуре, описывающей модем. Это структура modem_cellular_data. А вот к этой структуре можно получить доступ из уровня прикладной программы, выполнив ряд манипуляций с кодом.
Обычно для получения драйвера вы выолняете:
struct device *modem = {DEVICE_DT_GET(DT_ALIAS(modem))}; // зависит от содержания device tree
В поле modem->data будет находится modem_cellular_data с нужным нам modem_chat. Но на уровне прикладной программы поле modem->data будет просто последовательностью байт. Нужно привести modem->data к типу modem_cellular_data. Для этого копируем из драйвера modem_cellular эту структуру и переносим в любое место на уровень прикладной программы. Получим такой файл (modem.h):
#ifndef _INCLUDE_MODEM_H_
#define _INCLUDE_MODEM_H_
#include <zephyr/types.h>
#include <zephyr/modem/backend/uart.h>
#include <zephyr/modem/chat.h>
#include <zephyr/modem/cmux.h>
#include <zephyr/drivers/cellular.h>
#include <zephyr/modem/chat.h>
#define MODEM_CELLULAR_DATA_IMEI_LEN (16)
#define MODEM_CELLULAR_DATA_MODEL_ID_LEN (65)
#define MODEM_CELLULAR_DATA_IMSI_LEN (23)
#define MODEM_CELLULAR_DATA_ICCID_LEN (22)
#define MODEM_CELLULAR_DATA_MANUFACTURER_LEN (65)
#define MODEM_CELLULAR_DATA_FW_VERSION_LEN (65)
enum modem_cellular_event {
MODEM_CELLULAR_EVENT_RESUME = 0,
MODEM_CELLULAR_EVENT_SUSPEND,
MODEM_CELLULAR_EVENT_SCRIPT_SUCCESS,
MODEM_CELLULAR_EVENT_SCRIPT_FAILED,
MODEM_CELLULAR_EVENT_CMUX_CONNECTED,
MODEM_CELLULAR_EVENT_DLCI1_OPENED,
MODEM_CELLULAR_EVENT_DLCI2_OPENED,
MODEM_CELLULAR_EVENT_TIMEOUT,
MODEM_CELLULAR_EVENT_REGISTERED,
MODEM_CELLULAR_EVENT_DEREGISTERED,
MODEM_CELLULAR_EVENT_BUS_OPENED,
MODEM_CELLULAR_EVENT_BUS_CLOSED,
};
enum modem_cellular_state {
MODEM_CELLULAR_STATE_IDLE = 0,
MODEM_CELLULAR_STATE_RESET_PULSE,
MODEM_CELLULAR_STATE_POWER_ON_PULSE,
MODEM_CELLULAR_STATE_AWAIT_POWER_ON,
MODEM_CELLULAR_STATE_RUN_INIT_SCRIPT,
MODEM_CELLULAR_STATE_CONNECT_CMUX,
MODEM_CELLULAR_STATE_OPEN_DLCI1,
MODEM_CELLULAR_STATE_OPEN_DLCI2,
MODEM_CELLULAR_STATE_RUN_DIAL_SCRIPT,
MODEM_CELLULAR_STATE_AWAIT_REGISTERED,
MODEM_CELLULAR_STATE_CARRIER_ON,
MODEM_CELLULAR_STATE_INIT_POWER_OFF,
MODEM_CELLULAR_STATE_POWER_OFF_PULSE,
MODEM_CELLULAR_STATE_AWAIT_POWER_OFF,
};
struct modem_cellular_data {
/* UART backend */
struct modem_pipe *uart_pipe;
struct modem_backend_uart uart_backend;
uint8_t uart_backend_receive_buf[CONFIG_LMT_MODEM_CELLULAR_UART_BUFFER_SIZES];
uint8_t uart_backend_transmit_buf[CONFIG_LMT_MODEM_CELLULAR_UART_BUFFER_SIZES];
/* CMUX */
struct modem_cmux cmux;
uint8_t cmux_receive_buf[CONFIG_LMT_MODEM_CELLULAR_CMUX_MAX_FRAME_SIZE];
uint8_t cmux_transmit_buf[2 * CONFIG_LMT_MODEM_CELLULAR_CMUX_MAX_FRAME_SIZE];
struct modem_cmux_dlci dlci1;
struct modem_cmux_dlci dlci2;
struct modem_pipe *dlci1_pipe;
struct modem_pipe *dlci2_pipe;
uint8_t dlci1_receive_buf[CONFIG_LMT_MODEM_CELLULAR_CMUX_MAX_FRAME_SIZE];
/* DLCI 2 is only used for chat scripts. */
uint8_t dlci2_receive_buf[CONFIG_LMT_MODEM_CELLULAR_CHAT_BUFFER_SIZES];
/* Modem chat */
struct modem_chat chat;
uint8_t chat_receive_buf[CONFIG_LMT_MODEM_CELLULAR_CHAT_BUFFER_SIZES];
uint8_t *chat_delimiter;
uint8_t *chat_filter;
uint8_t *chat_argv[32];
/* Status */
enum cellular_registration_status registration_status_gsm;
enum cellular_registration_status registration_status_gprs;
enum cellular_registration_status registration_status_lte;
uint8_t rssi;
uint8_t rsrp;
uint8_t rsrq;
uint8_t imei[MODEM_CELLULAR_DATA_IMEI_LEN];
uint8_t model_id[MODEM_CELLULAR_DATA_MODEL_ID_LEN];
uint8_t imsi[MODEM_CELLULAR_DATA_IMSI_LEN];
uint8_t iccid[MODEM_CELLULAR_DATA_ICCID_LEN];
uint8_t manufacturer[MODEM_CELLULAR_DATA_MANUFACTURER_LEN];
uint8_t fw_version[MODEM_CELLULAR_DATA_FW_VERSION_LEN];
/* PPP */
struct modem_ppp *ppp;
enum modem_cellular_state state;
const struct device *dev;
struct k_work_delayable timeout_work;
/* Power management */
struct k_sem suspended_sem;
/* Event dispatcher */
struct k_work event_dispatch_work;
uint8_t event_buf[8];
struct ring_buf event_rb;
struct k_mutex event_rb_lock;
};
#endif
Теперь можем пользоваться modem_chat с пользовательского уровня:
struct device *modem = {DEVICE_DT_GET(DT_ALIAS(modem))};
struct modem_cellular_data *modem_cellular_data = (struct modem_cellular_data *) modem->data;
struct modem_chat chat = modem_cellular_data->chat
Диаграмма зависимостей компонентов проекта решения 2 выглядит так:
Из драйвера скопирована структура, которую мы поместили в modem.h. Модуль работы с AT-командами использует скопированную структуру для получения modem_chat. Пример такого модуля представлен в следующем разделе.
Способ использования modem_chat
Чтобы использовать modem_chat, нужно знать кое-что о его внутреннем устройстве. Для отправки AT-команд используются скрипты, которые составляются с помощью макросов. Помимо скрипта должен быть обработчик ответов для AT-команд. Ниже пример составления скрипта и его отправки. Другие примеры, как и остальные части кода из статьи, доступны на github.
// Modem setup
void ok_handler(struct modem_chat *chat, char **argv, uint16_t argc, void *user_data) {
// OK response received
}
MODEM_CHAT_MATCH_DEFINE(ok_match, "OK", "", ok_handler);
MODEM_CHAT_SCRIPT_CMDS_DEFINE(setup_script_cmds,
MODEM_CHAT_SCRIPT_CMD_RESP("AT", ok_match), // Check if the modem is responding
MODEM_CHAT_SCRIPT_CMD_RESP("AT+CMGF=1", ok_match), // Set SMS mode to text
MODEM_CHAT_SCRIPT_CMD_RESP("AT+CPMS=\"ME\",\"ME\",\"ME\"", ok_match), // Check memory storage. SM - sim, ME - modem
MODEM_CHAT_SCRIPT_CMD_RESP("AT+CNMI=2,1,0,0,0", ok_match)
);
MODEM_CHAT_SCRIPT_DEFINE(setup_script, setup_script_cmds, modem_chat_empty_matches, NULL, 10); // 10 second timeout
int setup_sms(struct modem_chat *chat) {
int ret = modem_chat_run_script(chat, &setup_script);
if (ret < 0) {
LOG_ERR("Failed to setup sms: %d", ret);
}
return ret;
}
Команды здесь составляются в определенном формате. Существуют скрипты, шаблоны совпадений и обработчики ответов. Скрипт – это набор AT-команд. Шаблон совпадений – значение, которое ожидается в ответ на отправленную команду (например, для команды, которая должна вернуть OK, шаблоном будет строка “OK”). Обработчик ответов – функция, которая будет вызвана, если на команду придет ответ, совпадающий с ожидаемым значением, то есть шаблоном.
В примере скрипт составлен в setup_script_cmds. Для каждой команды установлен шаблон ok_match. Для ok_match назначен обработчик ok_handler, который здесь ничего не делает. Запускается скрипт функцией modem_chat_run_script.