Більшість GSM модулів працюють по інтерфейсу UART, за допомогою AT-команд. Але для серйозних проєктів, використання AT команд несе певні труднощі:
- контроль і обробка помилок
- результат виконання команди повертається з тривалою затримкою
- необхідно розбирати вхідні рядки нальоту
Потрібно розуміти, що з результатом виконання команди, в буфер може потрапити URC-код від вхідного дзвінка, SMS, прийняті дані та ін. Вхідний буфер з прийнятими рядками, доводиться розбирати спираючись лише на символи перенесення і «ехо» команди, а самі команди найчастіше сильно відрізняються форматом. З цих причин, використання AT вносить додаткову затримку, алгоритмічно її усунути практично неможливо, адже причина знаходиться в самому модулі та недосконалість його вбудованого ПЗ
У цьому прикладі я використовував SIM800C. Подивившись специфікацію і переконавшись у підтримці PPP, став вивчати способи реалізації. Для використання PPP, модуль перемикається кількома налаштувальними командами, після цього режим AT стає недоступним і фактично йде спілкування з вежею оператора безпосередньо, минаючи внутрішній стек модуля, що дозволяє значно прискорити обмін даними.
Приклад PPP-пакету:
Кожен пакет PPP починається і закінчується символом ~ (0x7E). Протокол підтримує аутентифікацію з'єднання, шифрування і стиснення даних, тому досить складний для написання власного рішення. Логічніше використовувати готовий стек що підтримує PPP, наприклад LwIP. Він підтримує PPPOS і PPPOE (serial Over і Ethernet), протоколи автентифікації PAP та CHAP, має добру репутацію та широко поширений.
Демо проєкт
Блок-схема:
Приклади розроблялися для мікроконтролера STM32 під FreeRTOS.
Старт програми, налаштування периферії, створення завдань
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
// Налаштування gsm, периферії модуля і LwIP
InitGsmUart();
// Створення завдання з прийому-відправлення пакета і встановлення з'єднання
xTaskCreate(StartThread, "Start", configMINIMAL_STACK_SIZE * 2, 0, tskIDLE_PRIORITY + 1, & taskInitHandle);
// Старт процесів
osKernelStart(NULL, NULL);
while (1) {}
}
void StartThread(void * argument) {
gsmTaskInit();
// підключення, відправка/прийом даних
xTaskCreate(connectTask, "connectTask", configMINIMAL_STACK_SIZE * 1, 0, tskIDLE_PRIORITY + 1, NULL);
// Після видаляємо завдання
vTaskDelete(NULL);
}
«Верхній» рівень. Установка з'єднання, приймання даних і дзеркальне відправлення
// Кількість з'єднань. У цьому прикладі 1, може бути більше, обмежена розміром RAM контролера і можливостями LwIP (останній легко настроюється)
#define GSM_MAX_CONNECTION 1
// Структура для роботи з даними
typedef struct {
uint8_t * rxBuff;
uint16_t rxLen;
}
sBuff[GSM_MAX_CONNECTION];
sBuff buff = {
0
};
void connectTask(void * pServiceNum) {
bool connectState = false;
eRetComm status = eError;
uint16_t delay = 0;
uint8_t serviceNum = * (uint8_t * ) pServiceNum;
xSemaphoreHandle xRxPppData; // семафор на прийом від ppp
xRxPppData = GsmLLR_GetRxSemphorePoint(serviceNum);
for (;;) {
/* Код основного завдання */
if (connectState == true) {
//Якщо є підключення до сервера
while (GsmLLR_ConnectServiceStatus(serviceNum) == eOk) {
//Читаємо дані в буфер
buff[serviceNum].rxLen = getRxData(serviceNum, xRxPppData, & (buff[serviceNum].rxBuff));
if (buff[serviceNum].rxLen != 0) {
//Якщо прийшли дані то відправляємо назад
if (GsmLLR_TcpSend(serviceNum, buff[serviceNum].rxBuff, buff[serviceNum].rxLen) == eOk) {
printf("Connect:#%i SendData OK\r\n", serviceNum);
} else {
printf("Connect:#%i SendData ERROR\r\n", serviceNum);
connectState = false;
}
}
}
//Якщо розрив з'єднання
printf("Connect:#%i connection lost\r\n", serviceNum);
GsmLLR_DisconnectService(serviceNum);
connectState = false;
delay = 1000;
} else {
// з'єднання закрито, налаштовуємо
printf("Connect:#%i connecting...", serviceNum);
// Встановлюємо з'єднання
if (GsmLLR_ConnectService(serviceNum) == eOk) {
printf("Connect:#%i connected", serviceNum);
connectState = true;
} else { // не вийшло підключитися
printf("Connect:#%i ERROR", serviceNum);
delay = GSM_CONNECTION_ERROR_DELAY;
connectState = false;
}
}
vTaskDelay(delay / portTICK_RATE_MS);
}
}
// Прийом даних
uint16_t getRxData(uint8_t serviceNum, xSemaphoreHandle xRxPppData, uint8_t ** ppBufPacket) {
uint16_t retLen = 0;
uint16_t size = 0;
if (xSemaphoreTake(xRxPppData, 1000 / portTICK_PERIOD_MS) == pdTRUE) {
size = gsmLLR_TcpGetRxCount(serviceNum);
if (size > 1512) {
retLen = 0;
} else {
retLen = GsmLLR_TcpReadData(serviceNum, ppBufPacket, size);
}
}
return retLen;
}
Налаштування GSM, докладно
void gsmTaskInit(void) {
xTaskCreate(vGsmTask, "GSM", configMINIMAL_STACK_SIZE * 2, 0, tskIDLE_PRIORITY + 1, & gsmInitTaskId);
while ((!gsmState.init) || (!pppIsOpen)) {
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
/* Завдання ініціалізації та управління GSM модулем */
void vGsmTask(void * pvParameters) {
// низькорівневі ініціалізації
GsmLLR_Init();
GsmLLR2_Init();
GsmPPP_Init();
// поки перефірія не готова
while ((gsmState.initLLR != true) && (gsmState.initLLR2 != true)) {};
if (GsmLLR_PowerUp() != eOk) {
GsmLLR_ModuleLost();
}
for (;;) {
// ініціалізація
if (gsmState.init == false) {
// якщо модуль перестав відповідати
if (gsmState.notRespond == true) {
printf("GSM: INIT Module lost\r\n");
GsmLLR_ModuleLost();
continue;
}
// готовність модуля
if (GsmLLR_ATAT() != eOk) {
gsmState.notRespond = true;
continue;
}
// відключення попереджень по живленню
if (GsmLLR_WarningOff() != eOk) {
gsmState.notRespond = true;
continue;
}
// установки відповіді
if (GsmLLR_FlowControl() != eOk) {
gsmState.notRespond = true;
continue;
}
// читаємо IMEI
if (GsmLLR_GetIMEI(aIMEI) != eOk) {
gsmState.notRespond = true;
continue;
}
DBGInfo("GSM: module IMEI=%s\r\n", aIMEI);
// читаємо IMSI
if (GsmLLR_GetIMSI(aIMSI) != eOk) {
gsmState.notRespond = true;
continue;
}
printf("GSM: module IMSI=%s\r\n", aIMSI);
// Software Версія
if (GsmLLR_GetModuleSoftWareVersion(aVerionSoftware) != eOk) {
gsmState.notRespond = true;
continue;
}
// вивід повідомлення про реєстрацію мережі (URC)
if (GsmLLR_AtCREG() != eOk) {
gsmState.notRespond = true;
continue;
}
printf("GSM: CREG OK\r\n");
// читаємо рівень сигналу
if (GsmLLR_UpdateCSQ( & gsmCsqValue) != eOk) {
printf("GSM: Get CSQ ERROR, -RELOAD\r\n");
gsmState.notRespond = true;
continue;
} else {
printf("GSM: CSQ value %d\r\n", gsmCsqValue);
// формат SMS
if (GsmLLR_SmsModeSelect(sms_TEXT) != eOk) {
gsmState.notRespond = true;
continue;
}
//видаляємо sms
vTaskDelay(DELAY_REPLY_INIT / portTICK_RATE_MS);
if (GsmLLR_SmsClearAll() != eOk) {
printf("GSM: clear SMS ERROR, -RELOAD\r\n");
gsmState.notRespond = true;
continue;
}
printf("GSM: Clear SMS Ok\r\n");
printf("GSM: INIT PPPP\r\n");
if (GsmLLR_StartPPP( & connectionSettings.gsmSettings) == eOk) {
printf("GSM: INIT PPPP - PPP RUN\r\n");
xQueueReset(uartParcerStruct.uart.rxQueue);
uartParcerStruct.ppp.pppModeEnable = true;
uartParcerStruct.uart.receiveState = true;
gsmState.init = true;
} else {
printf("GSM: INIT PPPP - PPP ERROR!!!\r\n");
gsmState.notRespond = true;
continue;
}
}
}
vTaskDelay(1000 / portTICK_RATE_MS);
}
}
Підняття PPP.
Для початку сесії використовується 4 команди — comPPP_0-4. Як вони відправляються і розбирається відповідь ми не розглядаємо, це тема для окремої статті. Розглянемо лише в загальному вигляді:
char * comPPP_0[] = {
"AT+CGDCONT=1,\"IP\","
};
char * comPPP_2[] = {
"AT+CGQMIN=1,0,0,0,0,0"
};
char * comPPP_3[] = {
"AT+CGQREQ=1,2,4,3,6,31"
};
char * comPPP_4[] = {
"ATD*99***1#"
};
eRetComm GsmLLR_StartPPP(sGsmSettings * pSettings) {
printf("StartPPP\r\n");
sResultCommand resultCommand;
char ** comPPP_Mass[3] = {
comPPP_2,
comPPP_3,
comPPP_4
};
uint8_t * pData = NULL;
if (GsmLLR_GetMutex() == true) {
pData = pvPortMalloc(GSM_MALLOC_COMMAND_SIZE);
if (pData != NULL) {
memset(pData, 0, GSM_MALLOC_COMMAND_SIZE);
sprintf((char * ) pData, "%s%s", comPPP_0[0], (char * ) pSettings -> gprsApn);
RunAtCommand((char * ) pData, & resultCommand);
// Лічильник команд, поки не відправили всі
uint8_t stepIndex = 0;
while (stepIndex != (3)) {
uint16_t len = strlen((char * ) * comPPP_Mass[stepIndex]);
sprintf((char * ) pData, "%s", (char * ) * comPPP_Mass[stepIndex]);
RunAtCommand((char * ) pData, & resultCommand);
stepIndex++;
}
memset(pData, 0, GSM_MALLOC_COMMAND_SIZE);
vPortFree(pData);
}
GsmLLR_GiveMutex();
}
return eOk;
}
З коду завдання vGsmTask, слідують, що у разі успішного виконання «GsmLLR_StartPPP» — виставляється прапор pppModeEnable та очищається чергу uartParcerStruct.uart.rxQueue. Прапор pppModeEnable зображає поточний режим модуля. Обмін між перериванням UART і стеком/розбирачем команд — йде через чергу.
Завдання сесія PPP на рівні GSM
char * comPPP_0[] = {
"AT+CGDCONT=1,\"IP\","
};
char * comPPP_2[] = {
"AT+CGQMIN=1,0,0,0,0,0"
};
char * comPPP_3[] = {
"AT+CGQREQ=1,2,4,3,6,31"
};
char * comPPP_4[] = {
"ATD*99***1#"
};
eRetComm GsmLLR_StartPPP(sGsmSettings * pSettings) {
printf("StartPPP\r\n");
sResultCommand resultCommand;
char ** comPPP_Mass[3] = {
comPPP_2,
comPPP_3,
comPPP_4
};
uint8_t * pData = NULL;
if (GsmLLR_GetMutex() == true) {
pData = pvPortMalloc(GSM_MALLOC_COMMAND_SIZE);
if (pData != NULL) {
memset(pData, 0, GSM_MALLOC_COMMAND_SIZE);
sprintf((char * ) pData, "%s%s", comPPP_0[0], (char * ) pSettings -> gprsApn);
RunAtCommand((char * ) pData, & resultCommand);
// Лічильник команд, поки не відправили всі
uint8_t stepIndex = 0;
while (stepIndex != (3)) {
uint16_t len = strlen((char * ) * comPPP_Mass[stepIndex]);
sprintf((char * ) pData, "%s", (char * ) * comPPP_Mass[stepIndex]);
RunAtCommand((char * ) pData, & resultCommand);
stepIndex++;
}
memset(pData, 0, GSM_MALLOC_COMMAND_SIZE);
vPortFree(pData);
}
GsmLLR_GiveMutex();
}
return eOk;
}
Відправляємо дані через TCP термінал:
У структурах видно прийнятий пакет:
Підведемо підсумки.
Відмовившись від AT-команд, вдалося значно зменшити та спростити код з розбору і надсилання команд, складного (потенційно не надійного) розбір відповідей, прийому даних і URC кодів. AT-команди залишилися для початкового налаштування модему і запису параметрів APN.
Коментарі (3)