STM32 + PPP (GSM) + LwIP

STM32 + PPP (GSM) + LwIP
15 хв. читання
20 жовтня 2019

Більшість GSM модулів працюють по інтерфейсу UART, за допомогою AT-команд. Але для серйозних проєктів, використання AT команд несе певні труднощі:

  • контроль і обробка помилок
  • результат виконання команди повертається з тривалою затримкою
  • необхідно розбирати вхідні рядки нальоту

Потрібно розуміти, що з результатом виконання команди, в буфер може потрапити URC-код від вхідного дзвінка, SMS, прийняті дані та ін. Вхідний буфер з прийнятими рядками, доводиться розбирати спираючись лише на символи перенесення і «ехо» команди, а самі команди найчастіше сильно відрізняються форматом. З цих причин, використання AT вносить додаткову затримку, алгоритмічно її усунути практично неможливо, адже причина знаходиться в самому модулі та недосконалість його вбудованого ПЗ

У цьому прикладі я використовував SIM800C. Подивившись специфікацію і переконавшись у підтримці PPP, став вивчати способи реалізації. Для використання PPP, модуль перемикається кількома налаштувальними командами, після цього режим AT стає недоступним і фактично йде спілкування з вежею оператора безпосередньо, минаючи внутрішній стек модуля, що дозволяє значно прискорити обмін даними.

Приклад PPP-пакету: STM32 + PPP (GSM) + LwIP

Кожен пакет PPP починається і закінчується символом ~ (0x7E). Протокол підтримує аутентифікацію з'єднання, шифрування і стиснення даних, тому досить складний для написання власного рішення. Логічніше використовувати готовий стек що підтримує PPP, наприклад LwIP. Він підтримує PPPOS і PPPOE (serial Over і Ethernet), протоколи автентифікації PAP та CHAP, має добру репутацію та широко поширений.

Демо проєкт

Блок-схема: STM32 + PPP (GSM) + LwIP

Приклади розроблялися для мікроконтролера 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 термінал: STM32 + PPP (GSM) + LwIP

У структурах видно прийнятий пакет: STM32 + PPP (GSM) + LwIP

Підведемо підсумки.

Відмовившись від AT-команд, вдалося значно зменшити та спростити код з розбору і надсилання команд, складного (потенційно не надійного) розбір відповідей, прийому даних і URC кодів. AT-команди залишилися для початкового налаштування модему і запису параметрів APN.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (3)
  1. Serg Sapov
    Для використання PPP, модуль перемикається кількома налаштувальними командами, після цього режим AT стає недоступним і фактично йде спілкування з вежею оператора безпосередньо, минаючи внутрішній стек модуля, що дозволяє значно прискорити обмін даними.

    А якщо в цей час SMS прийде чи вхідний дзвінок і його треба обробити, то як?

    11 місяців тому ·
    0
Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація