piątek, 29 września 2017

[8.2] STM32F7 - Serwer HTTP, CGI oraz SGI

Ten przykład chciałbym poświęcić na przygotowanie programu obsługującego serwer HTTP. Do przygotowanie projektu posłużyłem się przykładami umieszczonymi dla mikrokontrolera STM32F4.

[Źródło: http://www.st.com/en/evaluation-tools/32f746gdiscovery.html]

Uruchomienie strony internetowej:


Uruchomienie biblioteki LWIP odbywa się przez wywołanie następującej funkcji:

W przypadku ustanowienia DHCP funkcja MX_LWIP_Init pomija operacje związane ze statycznym IP:

  1. void MX_LWIP_Init(void)
  2. {
  3.     char usartBuffer[50] = {0}; /* Buffer for USART 1*/
  4. #if !LWIP_DHCP
  5.   IP_ADDRESS[0] = IP_ADDR0;
  6.   IP_ADDRESS[1] = IP_ADDR1;
  7.   IP_ADDRESS[2] = IP_ADDR2;
  8.   IP_ADDRESS[3] = IP_ADDR3;
  9.   NETMASK_ADDRESS[0] = NETMASK_ADDR0;
  10.   NETMASK_ADDRESS[1] = NETMASK_ADDR1;
  11.   NETMASK_ADDRESS[2] = NETMASK_ADDR2;
  12.   NETMASK_ADDRESS[3] = NETMASK_ADDR3;
  13.   GATEWAY_ADDRESS[0] = GW_ADDR0;
  14.   GATEWAY_ADDRESS[1] = GW_ADDR1;
  15.   GATEWAY_ADDRESS[2] = GW_ADDR2;
  16.   GATEWAY_ADDRESS[3] = GW_ADDR3;
  17. #endif
  18.     /* Initilialize the LwIP stack */
  19.   lwip_init();
  20. #if LWIP_DHCP
  21.   ipaddr.addr = 0;
  22.   netmask.addr = 0;
  23.   gw.addr = 0;
  24. #else
  25.   IP4_ADDR(&ipaddr, IP_ADDRESS[0], IP_ADDRESS[1], IP_ADDRESS[2], IP_ADDRESS[3]);
  26.   IP4_ADDR(&netmask, NETMASK_ADDRESS[0], NETMASK_ADDRESS[1] , NETMASK_ADDRESS[2], NETMASK_ADDRESS[3]);
  27.   IP4_ADDR(&gw, GATEWAY_ADDRESS[0], GATEWAY_ADDRESS[1], GATEWAY_ADDRESS[2], GATEWAY_ADDRESS[3]);  
  28. #endif
  29.   /* add the network interface */
  30.   netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &ethernet_input);
  31.   sprintf(usartBuffer,"IP Adres to: %lu\r\n", ipaddr);
  32.   Usart_Uart_SendString(USART1, usartBuffer, LF_CR);
  33.   /*  Registers the default network interface */
  34.   netif_set_default(&gnetif);
  35.   if (netif_is_link_up(&gnetif))
  36.   {
  37.     /* When the netif is fully configured this function must be called */
  38.     netif_set_up(&gnetif);
  39.   }
  40.   else
  41.   {
  42.     /* When the netif link is down this function must be called */
  43.        netif_set_down(&gnetif);
  44.   }  
  45.   /* Set the link callback function, this function is called on change of link status*/
  46.   netif_set_link_callback(&gnetif, ethernetif_update_config);
  47. #if LWIP_DHCP
  48.   dhcp_start(&gnetif);
  49. #endif
  50. /* USER CODE BEGIN 3 */
  51. /* USER CODE END 3 */
  52. }

Aby uruchomić stronę internetową należy rozpocząć od wywołania poniższej funkcji:

  1. HTTP_INIT_STATUS_t STM32_HTTP_Server_Init(void)
  2. {
  3.   HTTP_INIT_STATUS_t status=HTTP_INIT_OK;
  4.   /* If not initialized already */
  5.   if(Initialized == 0){ MX_LWIP_Init(); }
  6.   /* HTTP */
  7.   httpd_init();
  8.   /* NVIC init for systick, needed it is not enable earlier */
  9.   /*
  10.    *  HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
  11.    *  HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
  12.    */
  13.   /* Send information about connection status */
  14.   User_notification(&gnetif);
  15.   if(status == HTTP_INIT_OK){
  16.     serverActiveStatus = 1;
  17.   }
  18.   Initialized = 1;
  19.   return status;
  20. }

Wyłączenie strony internetowej. Tą funkcje można wykorzystywać np. jako podpiętą pod przycisk, lub gdy potrzeba wyłączyć sprawdzanie strony internetowej by skupić program na wykonywaniu innych czynności:

  1. uint8_t STM32_HTTP_Server_DeInit(void)
  2. {
  3.     if (!serverActiveStatus){
  4.         /* If not initialized, return error */
  5.         return 0;
  6.     }
  7.     httpd_deinit();
  8.     serverActiveStatus = 0;
  9.     return 1; /* OK */
  10. }

Funkcja odpowiedzialna za czyszczenie zmiennych czasowych oraz do wywołaniu funkcji ethernetif_input. Sprawdza ona czy został odebrany jakiś pakiet danych. Jeśli tak to wywoływana jest odpowiednia funkcja obsługująca:

  1. void STM32_HTTP_Server_Update(void)
  2. {
  3.   ethernetif_input(&gnetif);
  4.   sys_check_timeouts();
  5. #ifdef LWIP_DHCP
  6.    DHCP_Periodic_Handle(&gnetif);
  7. #endif
  8. }

Główna funkcja programu odpowiedzialna za uruchomienie serwera HTTP wygląda następująco:

  1.   /* USER CODE BEGIN 2 */
  2.   EnableUartUsart();            /* Use for debug purpose */
  3.   Usart_Uart_SendString(USART1,"Enable USART", LF_CR);
  4.   STM32_HTTP_Server_Init();     /* Init Http serwer */
  5.   Usart_Uart_SendString(USART1,"Serwer Enable", LF_CR);
  6.   /* USER CODE END 2 */
  7.   /* Infinite loop */
  8.   /* USER CODE BEGIN WHILE */
  9.   while (1)
  10.   {
  11.   /* USER CODE END WHILE */
  12.   /* USER CODE BEGIN 3 */
  13.       STM32_HTTP_Server_Update();   /* Update information, check if anything change */
  14.   }
  15.   /* USER CODE END 3 */


CGI:


Pozwala na komunikacje pomiędzy  stroną internetową a płytką z skonfigurowaną jako serwer. Dzięki temu uzyskuje się komunikacje ze strony na płytkę mikrokontrolerem. Daje to możliwość sterowania diodami, serwomechanizmami, silnikami, przekaźnikami i właściwie wszystkimi innymi rzeczami którymi można sterować.

CGI uruchamiane jest w funkcji httpd_init. W tym przypadku do dyspozycji są tylko CGI dla sterowania diodami led:

  1. /* Html request for "/leds.cgi" will start LEDS_CGI_Handler */
  2. const tCGI LEDS_CGI={"/leds.cgi", LEDS_CGI_Handler};
  3. tCGI CGI_TAB[1];

  1. void httpd_cgi_init(void)
  2. {
  3.   /* configure CGI handlers (LEDs control CGI) */
  4.   CGI_TAB[0] = LEDS_CGI;
  5.   http_set_cgi_handlers(CGI_TAB, 1);
  6. }

Http_set_cgi_handlers:

  1. void http_set_cgi_handlers(const tCGI *cgis, int num_handlers)
  2. {
  3.   LWIP_ASSERT("no cgis given", cgis != NULL);
  4.   LWIP_ASSERT("invalid number of handlers", num_handlers > 0);
  5.   g_pCGIs = cgis;
  6.   g_iNumCGIs = num_handlers;
  7. }

Obsługa CGI oraz SGI zdefiniowana jest w pliku httpd_cgi_ssi.h. Do roboty odpowiedzi ze strony internetowej służy LEDS_CGI_Handler:

  1. const char * LEDS_CGI_Handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[])
  2. {
  3.   uint32_t i=0;
  4.   if (iIndex==0)
  5.   {
  6.     /* Initialize GPIO only once */
  7.     if (GPIO_not_configured == 1) {
  8.       GPIO_Configuration();
  9.       GPIO_not_configured=0;
  10.     }
  11.    
  12.     /* Check cgi parameter : example GET /leds.cgi?led=2&led=4 */
  13.     for (i=0; i<iNumParams; i++)
  14.     {
  15.       /* check parameter "led" */
  16.       if (strcmp(pcParam[i] , "led")==0)  
  17.       {
  18.         if(strcmp(pcValue[i], "1") ==0) {
  19.             GPIOI->BSRR = GPIO_PIN_1;
  20.         }
  21.         else if(strcmp(pcValue[i], "2") ==0) {
  22.             GPIOI->BSRR = GPIO_PIN_2;
  23.         }
  24.         else if(strcmp(pcValue[i], "3") ==0){
  25.             GPIOI->BSRR = GPIO_PIN_3;
  26.         }
  27.         else if(strcmp(pcValue[i], "4") ==0){
  28.             GPIOI->BSRR = GPIO_PIN_4;
  29.         }
  30.       }
  31.     }
  32.   }
  33.   /* ser URI after CGI call */
  34.   return "/STM32F7xxLED.html";  
  35. }

Na początku konfigurowalne są piny od diod. Następnie stan diody zostają uruchomione bądź wyłączone w zależności od wysłanej komendy. Po wykonaniu komendy w zależności od wysłanej wiadomości zostaje uruchomiony URI dla cgi.

SSI:


Używane jest do przesłania informacji z czujników czy informacji sieciowych itp. Komunikacja następuje tylko w jedną stronę pozwalając na wypisanie tych danych ekranie.

W przykładzie wypisywane są przypadkowe informacje przesyłane z dwóch zmiennych do układu.

Dla przykładu wysyłamy dane z ADC, tak nazywany adcconvert1, aby ta dana była widoczna na stronie to należy tą nazwę umieścić w kodzie HTML w postaci <p><!-#temperature-></p>.

Uruchomienie SSI odbywa się poprzez następującą funkcje:

  1. void httpd_ssi_init(void)
  2. {  
  3.   /* configure SSI handlers (ADC page SSI) */
  4.   http_set_ssi_handler(ADC_Handler, (char const **)TAGS, 1);
  5. }

Http_set_ssi_handler:

  1. void http_set_ssi_handler(tSSIHandler ssi_handler, const char **tags, int num_tags)
  2. {
  3.   LWIP_DEBUGF(HTTPD_DEBUG, ("http_set_ssi_handler\n"));
  4.   LWIP_ASSERT("no ssi_handler given", ssi_handler != NULL);
  5.   LWIP_ASSERT("no tags given", tags != NULL);
  6.   LWIP_ASSERT("invalid number of tags", num_tags > 0);
  7.   g_pfnSSIHandler = ssi_handler;
  8.   g_ppcTags = tags;
  9.   g_iNumTags = num_tags;
  10. }

Funkcja sprawdza przesłane parametry. Następnie ustawania dane do zmiennych globalnych.

Obsługa zdarzenia dla SSI odbywa się w obsłudze zdarzenia od ADC. Strona będzie cyklicznie odświeżana gdzie wartość ADC będzie zmieniana zgodnie z odczytem.

  1. u16_t ADC_Handler(int iIndex, char *pcInsert, int iInsertLen)
  2. {
  3.   /* We have only one SSI handler iIndex = 0 */
  4.   if (iIndex ==0)
  5.   {  
  6.     char Digit1=0, Digit2=0, Digit3=0, Digit4=0;
  7.     uint32_t ADCVal = 0;        
  8.      /* configure ADC if not yet configured */
  9.      if (ADC_not_configured ==1)      
  10.      {
  11.         ADC_Configuration();
  12.         ADC_not_configured=0;
  13.      }
  14.      
  15.      HAL_ADC_PollForConversion(&hadc, 10);
  16.      /* get ADC conversion value */
  17.      ADCVal =  HAL_ADC_GetValue(&hadc);
  18.      
  19.      /* convert to Voltage,  step = 0.8 mV */
  20.      ADCVal = (uint32_t)(ADCVal * 0.8);  
  21.      
  22.      /* get digits to display */
  23.      
  24.      Digit1= ADCVal/1000;
  25.      Digit2= (ADCVal-(Digit1*1000))/100;
  26.      Digit3= (ADCVal-((Digit1*1000)+(Digit2*100)))/10;
  27.      Digit4= ADCVal -((Digit1*1000)+(Digit2*100)+ (Digit3*10));
  28.        
  29.      /* prepare data to be inserted in html */
  30.      *pcInsert       = (char)(Digit1+0x30);
  31.      *(pcInsert + 1) = (char)(Digit2+0x30);
  32.      *(pcInsert + 2) = (char)(Digit3+0x30);
  33.      *(pcInsert + 3) = (char)(Digit4+0x30);
  34.    
  35.     /* 4 characters need to be inserted in html*/
  36.     return 4;
  37.   }
  38.   return 0;
  39. }

Konfiguracja ADC odbywa się z wykorzystaniem tej funkcji:

  1. static void ADC_Configuration(void)
  2. {
  3.   GPIO_InitTypeDef GPIO_InitStruct;
  4.   ADC_ChannelConfTypeDef sConfig;
  5.   /* Enable GPIOF clock */
  6.   __HAL_RCC_GPIOF_CLK_ENABLE();
  7.   /* Configure PF10 as analog input */
  8.   GPIO_InitStruct.Pin = GPIO_PIN_10;
  9.   GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
  10.   GPIO_InitStruct.Pull = GPIO_NOPULL;
  11.   HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
  12.   __HAL_RCC_ADC3_CLK_ENABLE();
  13.   /* ADC3 Configuration ------------------------------------------------------*/
  14.   hadc.Instance = ADC3;
  15.   hadc.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV2;
  16.   hadc.Init.Resolution = ADC_RESOLUTION_12B;
  17.   hadc.Init.ScanConvMode = DISABLE;
  18.   hadc.Init.ContinuousConvMode = ENABLE;
  19.   hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  20.   hadc.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T1_CC1;
  21.   hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  22.   hadc.Init.NbrOfConversion = 1;
  23.   HAL_ADC_Init(&hadc);
  24.   /* ADC3 Regular Channel Config */
  25.   sConfig.Channel = ADC_CHANNEL_8;
  26.   sConfig.Rank = 1;
  27.   sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;
  28.   sConfig.Offset = 0;
  29.   HAL_ADC_ConfigChannel(&hadc, &sConfig);
  30.   /* Enable EOC interupt */
  31.   HAL_ADC_Start(&hadc);
  32. }

DHCP:


Jest używane w celu przyznania adresu IP dla urządzenia, w tym przypadku dla STM32.

W programie zostało to przygotowane tak aby dane odnośnie IP były wysyłane przez UART do komputera. Bez tej funkcji można np wykorzystać program Colasoft MAC, który w zadanym zakresie adresów wyświetli podłączone urządzenia.

Można je włączyć bądź wyłączyć w programie za pomocą ustawienia odpowiedniej zmiennej.

W przypadku DHCP wymagane jest dodatkowe obsługiwanie wątku związanego z DHCP.

  1. void DHCP_Periodic_Handle(struct netif *netif)
  2. {
  3.   #ifdef LWIP_DHCP
  4.   /* Fine DHCP periodic process every 500ms */
  5.   if (HAL_GetTick() - DHCPTimer >= 1000)
  6.   {
  7.     DHCPTimer =  HAL_GetTick();
  8.     /* process DHCP state machine */
  9.     DHCP_thread(&gnetif);
  10.   }
  11. #endif
  12. }

W funkcji powyżej obsługiwany jest timer, który wywoływany jest co określony czas. Proces DHCP odbywa się w DHCP_thread:

  1. static void DHCP_thread(void const * argument)
  2. {
  3.   struct netif *netif = (struct netif *) argument;
  4.   uint8_t iptxt[20];
  5.   char dane[50] = {0};
  6.   uint8_t i = 0;
  7.   struct dhcp *dhcp;
  8.   ip_addr_t ipaddr;
  9.   ip_addr_t netmask;
  10.   ip_addr_t gw;
  11.   char ip[3] = {0};
  12.   switch (DHCP_state)
  13.   {
  14.     case DHCP_START:
  15.         DHCP_state = DHCP_WAIT_ADDRESS;
  16.         Usart_Uart_SendString(USART1, "DHCP_WAIT_ADDRESS", LF_CR);
  17.     break;
  18.     case DHCP_WAIT_ADDRESS:
  19.           sprintf((char *)iptxt, "%s", ipaddr_ntoa((const ip_addr_t *)&netif->ip_addr));
  20.           for(i=0; i<20; i++)
  21.           {
  22.               if(iptxt[i] != '.'){
  23.                   ip[i] = iptxt[i];
  24.               }
  25.               else { break; }
  26.           }
  27.           if(== 3)
  28.           {
  29.               if(ip[0] != '0' || ip[1] != '0' || ip[2] != '0')
  30.               {
  31.                 sprintf(dane,"IP address assigned by a DHCP server: %s\n", iptxt);
  32.                 Usart_Uart_SendString(USART1,dane, LF_CR);
  33.                 DHCP_state = DHCP_ADDRESS_ASSIGNED;
  34.               }
  35.           }
  36.           else if(== 2)
  37.           {
  38.               if(ip[0] != '0' || ip[1] != '0')
  39.               {
  40.                 sprintf(dane,"IP address assigned by a DHCP server: %s\n", iptxt);
  41.                 Usart_Uart_SendString(USART1,dane, LF_CR);
  42.                 DHCP_state = DHCP_ADDRESS_ASSIGNED;
  43.               }
  44.           }
  45.           else
  46.           {
  47.              dhcp = (struct dhcp *)netif_get_client_data(netif, 0);
  48.              /* DHCP timeout */
  49.              if (dhcp->tries > MAX_DHCP_TRIES)
  50.              {
  51.                 Usart_Uart_SendString(USART1,"Timeout", LF_CR);
  52.                 DHCP_state = DHCP_TIMEOUT;
  53.                 /* Stop DHCP */
  54.                 dhcp_stop(netif);
  55.                 uint8_t IP_ADDRESS[4];
  56.                 uint8_t NETMASK_ADDRESS[4];
  57.                 uint8_t GATEWAY_ADDRESS[4];
  58.                 IP_ADDRESS[0] = IP_ADDR0;
  59.                 IP_ADDRESS[1] = IP_ADDR1;
  60.                 IP_ADDRESS[2] = IP_ADDR2;
  61.                 IP_ADDRESS[3] = IP_ADDR3;
  62.                 NETMASK_ADDRESS[0] = NETMASK_ADDR0;
  63.                 NETMASK_ADDRESS[1] = NETMASK_ADDR1;
  64.                 NETMASK_ADDRESS[2] = NETMASK_ADDR2;
  65.                 NETMASK_ADDRESS[3] = NETMASK_ADDR3;
  66.                 GATEWAY_ADDRESS[0] = GW_ADDR0;
  67.                 GATEWAY_ADDRESS[1] = GW_ADDR1;
  68.                 GATEWAY_ADDRESS[2] = GW_ADDR2;
  69.                 GATEWAY_ADDRESS[3] = GW_ADDR3;
  70.                 /* Static address used */
  71.                 IP4_ADDR(&ipaddr, IP_ADDRESS[0], IP_ADDRESS[1], IP_ADDRESS[2], IP_ADDRESS[3]);
  72.                 IP4_ADDR(&netmask, NETMASK_ADDRESS[0], NETMASK_ADDRESS[1] , NETMASK_ADDRESS[2], NETMASK_ADDRESS[3]);
  73.                 IP4_ADDR(&gw, GATEWAY_ADDRESS[0], GATEWAY_ADDRESS[1], GATEWAY_ADDRESS[2], GATEWAY_ADDRESS[3]);
  74.                 netif_set_addr(netif, &ipaddr, &netmask, &gw);
  75.              }
  76.           }
  77.     break;
  78.   }
  79. }

Funkcja wykonuje kilka rzeczy. Najpierw sprawdza wystąpienie odpowiedniego zdarzenia dla DHCP_state. Dzięki temu nadawany jest adres dynamiczny, który zostaje przesłany po interfejsie UART na terminal, bądź nadawany jest adres statyczny w przypadku problemów z nadaniem innego.

Generacja strony internetowej:


Wygenerowanie strony internetowej odbywa się za pomocą specjalnego programu, który pozwoli na przygotowanie kodu z formatu html na format szesnastkowy. Tak wygenerowany kod wstawia się do pliku fsdata.c. Do tego wykorzystuje się program makefsdata.

Musi on być umieszczony w jednym katalogu z folderem o nazwie fs. Ten folder zawierał będzie pliki wszystkich stron oraz zdjęć jakie mają zostać wygenerowane. Podwójne kliknięcie w makefsdata.exe będzie powodowało wygenerowanie projektu. Szczegóły odnośnie samego programu można przeczytać pod tym linkiem.

Obrazek poniżej prezentuje uproszczony sposób działania programu.


Pliki z projektem można znaleźć na dysku Google pod tym linkiem.

Aby go uruchomić najłatwiej ustanowić nowy projekt za pomocą CubeMx, wraz z wszystkimi potrzebnymi bibliotekami, po czym należy w gotowym projekcie podmienić biblioteki na te, które zostały dostarczone w tym poście.