Не так давно друг предложил мне работу, связанную с созданием прошивки для микроконтроллера, который должен был связываться с сервером при помощи GSM-модуля SIM900D. Ранее я с программированием микроконтроллеров дела не имел, да и на C программировал последний раз в студенческие времена, но любопытство перевесило и я принялся за работу. Документация по данной железке присутствует в интернете, однако хороших примеров работы с TCP/IP в коде найти не удалось. Ничего не оставалось, кроме как обложиться документацией, запастись сигаретами и чаем и приступить к лавированию между граблями. А граблей оказалось немало. Собственно, поэтому я и написал эту статью — чтобы другим было легче.
Далее будет много AT-команд, не слишком много кода и очень много букв.
Требовалось написать код, который мог бы инициализировать GSM-модуль, устанавливать подключение с сервером, получать и отправлять произвольные данные, проверять состояние подключения и работать без сбоев. А также быть достаточно компактным, чтобы уместиться в ограниченной памяти микроконтроллера и оставить место для основной функциональности и еще чуть-чуть про запас.
Получился код на C, который может все, что было нужно. Из-за требований компактности, разбирать ответы и генерировать строки пришлось при помощи своего кода, который даже стыдно показать честному народу. Поэтому рекомендую всем использовать для этих целей регулярные выражения. Свой код я тоже собираюсь перевести на легковесный движок регулярных выражений, но уже после создания полнофункциональной прошивки.
Код требует функций/макросов для работы с последовательным портом, а также наличия функций memset и memcpy. Так что его с относительной легкостью можно перенести на другую платформу, не зацепив по пути кучу библиотек.
Программирование и тестирование проводилось под Windows 7. Код, полученный в результате, стал основным материалом для этой статьи. Я не стану приводить код полностью и комментировать его, а вместо этого покажу алгоритм настройки и работы с GSM-модулем.
Функции, которые требуются коду:
Функции, которые код предоставляет:
Это достаточно просто. Под целевой микроконтроллер есть макросы для отправки/получения данных через USART, но так как отлаживать такой код проще со стационарного компьютера, мне была предоставлена связка из переходника USB<->USART и GSM-модуля. Оставалось только научиться работать с последовательным портом под Windows. Это оказалось просто. Вкратце, последовательный порт представляется в ОС обычным файлом, передача информации осуществляется функциями ReadFile и WriteFile. Нужно только установить кое-какие параметры при помощи функций SetCommTimeouts и SetCommState.
Вот как выглядит функция инициализации порта:
После того, как последовательный порт настроен, в него можно отправлять AT-команды. Первой командой должна быть последовательность
Команда является простой строкой из ASCII-символов. Чтобы команду воспринял модуль, в ее конце нужно поставить символ перевода каретки
Примерно так и выглядят вспомогательные функции для отправки команды и получения ответа.
Самая большая функция в коде отвечает за настройку модуля. При инициализации отправляется много AT-команд. Я опишу их в порядке посылки модулю. Специально не расписываю аргументы и варианты ответов подробно, ибо их можно найти в документации.
После выполнения этих команд модуль будет готов к работе. Ну или не будет. Тут уж как повезет.
Создание подключения производится командой
Замечу, что при использовании протокола UDP по умолчанию датаграммы будут отсылаться и приниматься только с одного адреса. Для того, чтобы использовать UDP на полную катушку и отсылать/принимать данные с любых адресов, можно использовать так называемый расширенный режим UDP, настраиваемый командой
В ответ на команду можно получить несколько вариантов ответов. Если все хорошо, то после стандартного
Разорвать соединение можно командой
Данные передаются командой
Если все хорошо, то модуль в ответ на команду выдаст приглашение
Как видите, передача данных — процесс не слишком сложный. Поэтому переходим к самому интересному — приему данных.
Как только GSM-модуль принимает данные, он сигнализирует об этом, посылая в последовательный порт строку вида
Мне не доставляла радости мысль о том, что придется тем или иным образом отслеживать сообщения модуля. Однако, немного поигравшись с дебаггером, я выяснил, что никаких других асинхронных сообщений модуль не посылает, а также то, что после выполнения любой AT-команды это сообщение оказывается в начале буфера. Так как я составил макрос для отделения ответа от команды путем поиска подстроки
Так вот, принимать данные можно командой
В ответ мы получим нечто вида
Настоятельно рекомендую перед написанием кода освоиться в AT-командах при помощи PuTTY, который прекрасно работает с последовательным портом.
Надеюсь, информация из этой статьи поможет кому-нибудь написать код для своего SIM900. Вполне возможно, что принципы работы с GSM-модулем, изложенные выше, можно применить и к модулям других моделей, а, возможно, и производителей.
Далее будет много AT-команд, не слишком много кода и очень много букв.
Что было нужно
Требовалось написать код, который мог бы инициализировать GSM-модуль, устанавливать подключение с сервером, получать и отправлять произвольные данные, проверять состояние подключения и работать без сбоев. А также быть достаточно компактным, чтобы уместиться в ограниченной памяти микроконтроллера и оставить место для основной функциональности и еще чуть-чуть про запас.
Что получилось в итоге
Получился код на C, который может все, что было нужно. Из-за требований компактности, разбирать ответы и генерировать строки пришлось при помощи своего кода, который даже стыдно показать честному народу. Поэтому рекомендую всем использовать для этих целей регулярные выражения. Свой код я тоже собираюсь перевести на легковесный движок регулярных выражений, но уже после создания полнофункциональной прошивки.
Код требует функций/макросов для работы с последовательным портом, а также наличия функций memset и memcpy. Так что его с относительной легкостью можно перенести на другую платформу, не зацепив по пути кучу библиотек.
И как оно выглядит?
Программирование и тестирование проводилось под Windows 7. Код, полученный в результате, стал основным материалом для этой статьи. Я не стану приводить код полностью и комментировать его, а вместо этого покажу алгоритм настройки и работы с GSM-модулем.
Функции, которые требуются коду:
uint16_t init_serial_port(char *port_name)
Эта функция настраивает указанный последовательный порт. Под Windows.uint16_t puts_serial(uint8_t *buffer, uint16_t size)
А эта пишет строку байт в этот порт.gets_serial(uint8_t *buffer, uint16_t size)
Эта, соответственно, читает строку байт из последовательного порта.
Функции, которые код предоставляет:
init_gprs() & stop_gprs()
Соответственно инициализируют и вырубают GSM-модуль.uint16_t connect_gprs(uint8_t index, uint8_t mode, char *address, char *port)
Устанавливает подключение с сервером. Стоит отметить, что модуль умеет работать с протоколами TCP и UDP как в качестве клиента, так и будучи клиентом. Поддерживается максимум 8 одновременных подключений.uint16_t close_gprs(uint8_t index)
Закрывает указанное подключение.uint16_t send_gprs(uint8_t index, uint8_t *buffer, uint16_t size)
Отправка сообщения через указанное подключение.uint16_t recv_gprs(uint8_t index, uint8_t *buffer, uint16_t size)
Получение сообщения. Неблокирующая функция, что значит она не будет ждать появления данных в потоке, а вернет управление, если получать нечего. Стоит отметить, что такое поведение реализовать проще, чем блокирующее.
Как работать с последовательным портом
Это достаточно просто. Под целевой микроконтроллер есть макросы для отправки/получения данных через USART, но так как отлаживать такой код проще со стационарного компьютера, мне была предоставлена связка из переходника USB<->USART и GSM-модуля. Оставалось только научиться работать с последовательным портом под Windows. Это оказалось просто. Вкратце, последовательный порт представляется в ОС обычным файлом, передача информации осуществляется функциями ReadFile и WriteFile. Нужно только установить кое-какие параметры при помощи функций SetCommTimeouts и SetCommState.
Вот как выглядит функция инициализации порта:
uint16_t init_serial_port(char *port_name)
{
COMMTIMEOUTS timeouts;
DCB parameters;
int result;
serial_port_handle = CreateFile(port_name, // "\\\\.\\COMx"
GENERIC_READ | GENERIC_WRITE,
0, // Значения последующих параметров фиксированы при работе с портом
NULL,
OPEN_EXISTING,
0,
NULL);
if (serial_port_handle == INVALID_HANDLE_VALUE)
{
printf("Error opening a serial port!\n");
return 1;
}
// Максимальное время между чтением двух байт подряд
timeouts.ReadIntervalTimeout = 100;
// Следующее значение умножается на количество читаемых из порта символов
timeouts.ReadTotalTimeoutMultiplier = 0;
// и прибавляется к этому значению, получается максимальное время на выполнение
// всей операции
timeouts.ReadTotalTimeoutConstant = 1000;
// Значение то же, что и у предыдущих двух параметров, однако таймаут считается на запись.
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 1000;
result = SetCommTimeouts(serial_port_handle, &timeouts);
if (result == 0)
{
printf("Error setting timeouts for serial port!\n");
close_serial_port();
return 1;
}
// В параметры порта занесены самые простые настройки - без контроля
// четности, без управления потоком, 1 стоп-бит.
memset(¶meters,0,sizeof(parameters));
parameters.DCBlength = sizeof(DCB);
GetCommState(serial_port_handle, &parameters);
parameters.BaudRate = (DWORD)BAUD_RATE;
parameters.ByteSize = 8;
parameters.Parity = NOPARITY;
parameters.StopBits = ONESTOPBIT;
parameters.fAbortOnError = TRUE;
parameters.fDtrControl = DTR_CONTROL_DISABLE;
parameters.fRtsControl = RTS_CONTROL_DISABLE;
parameters.fBinary = TRUE;
parameters.fParity = FALSE;
parameters.fOutX = FALSE;
parameters.fInX = FALSE;
parameters.XonChar = (uint8_t)0x00;
parameters.XoffChar = (uint8_t)0xff;
parameters.fErrorChar = FALSE;
parameters.fNull = FALSE;
parameters.fOutxCtsFlow = FALSE;
parameters.fOutxDsrFlow = FALSE;
parameters.XonLim = 128;
parameters.XoffLim = 128;
result = SetCommState(serial_port_handle, &parameters);
if (result == 0)
{
printf("Error setting serial port parameters!\n");
close_serial_port();
return 1;
}
return 0;
}
Как происходит общение с GSM-модулем
После того, как последовательный порт настроен, в него можно отправлять AT-команды. Первой командой должна быть последовательность
"AT\r"
, позволяющая модулю автоматически настроить скорость передачи по последовательному порту. Ответ, который можно получить после этого из порта, будет выглядеть как "AT\r\r\nOK\r\n"
. Команда является простой строкой из ASCII-символов. Чтобы команду воспринял модуль, в ее конце нужно поставить символ перевода каретки
"\r"
. В ответ модуль передаст строку символов, состоящую из двух частей — команды, на которую модуль отвечает и отделенным от нее символами "\r\r\n" ответом, заканчивающимся символами "\r\n"
. Чтобы было удобнее разбирать ответы я создал макрос, который устанавливает указатель на начало ответа в принимающем буфере. Если хочется вывести ответ в консоль, нужно добавить нулевой символ в конец принятого сообщения.void at_send(char *cmd, uint16_t size)
{
uint16_t result;
cmd[size - 1] = '\r';
result = puts_serial(cmd, size);
return;
}
uint16_t at_recv(uint8_t *buffer, uint16_t size)
{
uint16_t result;
result = gets_serial(buffer, size);
return result;
}
Примерно так и выглядят вспомогательные функции для отправки команды и получения ответа.
Инициализация модуля
Самая большая функция в коде отвечает за настройку модуля. При инициализации отправляется много AT-команд. Я опишу их в порядке посылки модулю. Специально не расписываю аргументы и варианты ответов подробно, ибо их можно найти в документации.
"AT+CPIN=pin-code"
Как несложно догадаться, эта команда разблокирует SIM-карту путем ввода пин-кода. Чтобы проверить, требуется ли пин-код, можно использовать команду"AT+CPIN?"
."AT+CREG?"
Эта команда возвращает статус регистрации модуля в сети. Нужно выполнять ее, пока модуль не ответит, что в сети он зарегистрирован."AT+CGATT=1"
Заставляет модуль подключиться к GPRS. Проверить, подключен ли он, можно командой"AT+CGATT?"
."AT+CIPRXGET=1"
Включает получение данных, переданных через соединение, вручную. По умолчанию этот параметр отключен и данные передаются в последовательный порт сразу после получения. Это не слишком удобно, хотя и не критично — можно настроить модуль так, чтобы вместе с данными он передавал и заголовки IP, по которым можно определить, от кого был получен пакет. Я решил, что вручную данные получать проще и не ошибся. Как я понял, данная команда воспринимается только GSM-модулями SIM.COM."AT+CIPMUX=1"
По умолчанию модуль может устанавливать только одно подключение. Этот параметр включает возможность создавать несколько подключений. Отправка и прием данных будут отличаться только на один параметр — индекс подключения."AT+CSTT="internet""
APN — Access Point Name, имя точки доступа для GPRS. Для моего провайдера выглядит именно так."AT+CIICR"
Устанавливает беспроводное подключение GPRS. Может занять некоторое время, так что ее нужно выполнять в цикле и проверять ответ."AT+CIFSR"
Возвращает IP-адрес модуля. Я использую ее чтобы проверить, подключен ли модуль к интернету."AT+CDNSCFG="8.8.8.8","8.8.4.4""
Этой командой устанавливаются сервера DNS, которые будет использовать модуль."AT+CIPSTATUS"
Помимо данных о состоянии подключений эта команда дает информацию о том, готов ли модуль к установке соединений. Так что нужно проверить ее ответ.
После выполнения этих команд модуль будет готов к работе. Ну или не будет. Тут уж как повезет.
Установка и разрыв подключений
Создание подключения производится командой
"AT+CIPSTART=index,"mode","address","port""
. index
указывает порядковый номер подключения, может принимать значения от 0 до 7.mode
определяет протокол, который будет использоваться соединением. Может быть «TCP» или «UDP».address
задает адрес сервера. Если при настройке были указаны DNS-сервера, то можно использовать как IP-адрес, так и доменное имя.port
задает порт сервера, с которым будет устанавливаться соединение.
Замечу, что при использовании протокола UDP по умолчанию датаграммы будут отсылаться и приниматься только с одного адреса. Для того, чтобы использовать UDP на полную катушку и отсылать/принимать данные с любых адресов, можно использовать так называемый расширенный режим UDP, настраиваемый командой
"AT+CIPUDPMODE"
. За подробностями отсылаю к документации.В ответ на команду можно получить несколько вариантов ответов. Если все хорошо, то после стандартного
"OK"
через небольшой промежуток времени можно получить один из трех ответов:"index,ALREADY CONNECT"
это значит, что подключение с заданным индексом уже установлено и стоит его поискать."index,CONNECT OK"
тут все очевидно."index,CONNECT FAIL"
означает, что возникли проблемы с установкой соединения.
Разорвать соединение можно командой
"AT+CIPCLOSE=index"
. Разорвать все соединения и деактивировать интерфейс GPRS можно командой "AT+CIPSHUT"
.Передача данных
Данные передаются командой
"AT+CIPSEND=index,length"
, где index
указывает подключение, по которому нужно передать данные, а length
задает длину пакета данных. Кстати, узнать MTU для каждого подключения можно при помощи команды "AT+CIPSEND=?"
.Если все хорошо, то модуль в ответ на команду выдаст приглашение
">"
, после которого нужно переслать в последовательный порт данные. Как только модуль получит количество байт, равное length
, он скажет что-то типа "index,SEND OK"
. Вообще, можно не использовать параметр length
, однако в таком случае окончание пакета данных должно быть указано явно при помощи символа 0x1A
, в терминале сочетание Ctrl+Z. Для передачи произвольных данных такой вариант, очевидно, не подходит.Как видите, передача данных — процесс не слишком сложный. Поэтому переходим к самому интересному — приему данных.
Прием данных
Как только GSM-модуль принимает данные, он сигнализирует об этом, посылая в последовательный порт строку вида
"+CIPRXGET:1,index\r\n"
. Я честно не знаю, что означает единица, ибо данная функция модуля документирована достаточно слабо, но у меня она фигурирует во всех сообщениях о приеме пакета.Мне не доставляла радости мысль о том, что придется тем или иным образом отслеживать сообщения модуля. Однако, немного поигравшись с дебаггером, я выяснил, что никаких других асинхронных сообщений модуль не посылает, а также то, что после выполнения любой AT-команды это сообщение оказывается в начале буфера. Так как я составил макрос для отделения ответа от команды путем поиска подстроки
"\r\r\n"
, меня это никоим образом не задевало. Так что функция приема данных была реализована достаточно просто.Так вот, принимать данные можно командой
"AT+CIPRXGET=2,index,length"
. Двойка означает режим приема, в данном случае байты просто высыпаются в последовательный порт. Можно также задать получение данных в виде HEX-текста, видимо, ради предотвращения конфликтов с программным управлением потоком. Мне это не потребовалось, ибо управление потоком я вообще не использую. Параметр length
задает размер пакета данных, который мы желаем получить за один раз.В ответ мы получим нечто вида
"+CIPRXGET:2,index,received,excess\r\n__DATA__\r\nOK\r\n"
. В поле received
будет находиться количество байт, находящихся в пакете данных __DATA__
, а поле excess
будет содержать количество байт, ожидающих своей очереди в буфере модуля. Так что если поле received
равно нулю, можно с чистой совестью заявлять, что получать нечего. Собственно, пользуясь этим, я и реализовал неблокирующую функцию для приема данных.В заключение
Настоятельно рекомендую перед написанием кода освоиться в AT-командах при помощи PuTTY, который прекрасно работает с последовательным портом.
Надеюсь, информация из этой статьи поможет кому-нибудь написать код для своего SIM900. Вполне возможно, что принципы работы с GSM-модулем, изложенные выше, можно применить и к модулям других моделей, а, возможно, и производителей.