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.
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:
Aby uruchomić stronę internetową należy rozpocząć od wywołania poniższej funkcji:
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:
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:
Główna funkcja programu odpowiedzialna za uruchomienie serwera HTTP wygląda następująco:
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:
Http_set_cgi_handlers:
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:
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.
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:
Http_set_ssi_handler:
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.
Konfiguracja ADC odbywa się z wykorzystaniem tej funkcji:
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.
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:
- void MX_LWIP_Init(void)
- {
- char usartBuffer[50] = {0}; /* Buffer for USART 1*/
- #if !LWIP_DHCP
- IP_ADDRESS[0] = IP_ADDR0;
- IP_ADDRESS[1] = IP_ADDR1;
- IP_ADDRESS[2] = IP_ADDR2;
- IP_ADDRESS[3] = IP_ADDR3;
- NETMASK_ADDRESS[0] = NETMASK_ADDR0;
- NETMASK_ADDRESS[1] = NETMASK_ADDR1;
- NETMASK_ADDRESS[2] = NETMASK_ADDR2;
- NETMASK_ADDRESS[3] = NETMASK_ADDR3;
- GATEWAY_ADDRESS[0] = GW_ADDR0;
- GATEWAY_ADDRESS[1] = GW_ADDR1;
- GATEWAY_ADDRESS[2] = GW_ADDR2;
- GATEWAY_ADDRESS[3] = GW_ADDR3;
- #endif
- /* Initilialize the LwIP stack */
- lwip_init();
- #if LWIP_DHCP
- ipaddr.addr = 0;
- netmask.addr = 0;
- gw.addr = 0;
- #else
- IP4_ADDR(&ipaddr, IP_ADDRESS[0], IP_ADDRESS[1], IP_ADDRESS[2], IP_ADDRESS[3]);
- IP4_ADDR(&netmask, NETMASK_ADDRESS[0], NETMASK_ADDRESS[1] , NETMASK_ADDRESS[2], NETMASK_ADDRESS[3]);
- IP4_ADDR(&gw, GATEWAY_ADDRESS[0], GATEWAY_ADDRESS[1], GATEWAY_ADDRESS[2], GATEWAY_ADDRESS[3]);
- #endif
- /* add the network interface */
- netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_input);
- sprintf(usartBuffer,"IP Adres to: %lu\r\n", ipaddr);
- Usart_Uart_SendString(USART1, usartBuffer, LF_CR);
- /* Registers the default network interface */
- netif_set_default(&gnetif);
- if (netif_is_link_up(&gnetif))
- {
- /* When the netif is fully configured this function must be called */
- netif_set_up(&gnetif);
- }
- else
- {
- /* When the netif link is down this function must be called */
- netif_set_down(&gnetif);
- }
- /* Set the link callback function, this function is called on change of link status*/
- netif_set_link_callback(&gnetif, ethernetif_update_config);
- #if LWIP_DHCP
- dhcp_start(&gnetif);
- #endif
- /* USER CODE BEGIN 3 */
- /* USER CODE END 3 */
- }
Aby uruchomić stronę internetową należy rozpocząć od wywołania poniższej funkcji:
- HTTP_INIT_STATUS_t STM32_HTTP_Server_Init(void)
- {
- HTTP_INIT_STATUS_t status=HTTP_INIT_OK;
- /* If not initialized already */
- if(Initialized == 0){ MX_LWIP_Init(); }
- /* HTTP */
- httpd_init();
- /* NVIC init for systick, needed it is not enable earlier */
- /*
- * HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
- * HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
- */
- /* Send information about connection status */
- User_notification(&gnetif);
- if(status == HTTP_INIT_OK){
- serverActiveStatus = 1;
- }
- Initialized = 1;
- return status;
- }
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:
- uint8_t STM32_HTTP_Server_DeInit(void)
- {
- if (!serverActiveStatus){
- /* If not initialized, return error */
- return 0;
- }
- httpd_deinit();
- serverActiveStatus = 0;
- return 1; /* OK */
- }
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:
- void STM32_HTTP_Server_Update(void)
- {
- ethernetif_input(&gnetif);
- sys_check_timeouts();
- #ifdef LWIP_DHCP
- DHCP_Periodic_Handle(&gnetif);
- #endif
- }
Główna funkcja programu odpowiedzialna za uruchomienie serwera HTTP wygląda następująco:
- /* USER CODE BEGIN 2 */
- EnableUartUsart(); /* Use for debug purpose */
- Usart_Uart_SendString(USART1,"Enable USART", LF_CR);
- STM32_HTTP_Server_Init(); /* Init Http serwer */
- Usart_Uart_SendString(USART1,"Serwer Enable", LF_CR);
- /* USER CODE END 2 */
- /* Infinite loop */
- /* USER CODE BEGIN WHILE */
- while (1)
- {
- /* USER CODE END WHILE */
- /* USER CODE BEGIN 3 */
- STM32_HTTP_Server_Update(); /* Update information, check if anything change */
- }
- /* 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:
- /* Html request for "/leds.cgi" will start LEDS_CGI_Handler */
- const tCGI LEDS_CGI={"/leds.cgi", LEDS_CGI_Handler};
- tCGI CGI_TAB[1];
- void httpd_cgi_init(void)
- {
- /* configure CGI handlers (LEDs control CGI) */
- CGI_TAB[0] = LEDS_CGI;
- http_set_cgi_handlers(CGI_TAB, 1);
- }
Http_set_cgi_handlers:
- void http_set_cgi_handlers(const tCGI *cgis, int num_handlers)
- {
- LWIP_ASSERT("no cgis given", cgis != NULL);
- LWIP_ASSERT("invalid number of handlers", num_handlers > 0);
- g_pCGIs = cgis;
- g_iNumCGIs = num_handlers;
- }
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:
- const char * LEDS_CGI_Handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[])
- {
- uint32_t i=0;
- if (iIndex==0)
- {
- /* Initialize GPIO only once */
- if (GPIO_not_configured == 1) {
- GPIO_Configuration();
- GPIO_not_configured=0;
- }
- /* Check cgi parameter : example GET /leds.cgi?led=2&led=4 */
- for (i=0; i<iNumParams; i++)
- {
- /* check parameter "led" */
- if (strcmp(pcParam[i] , "led")==0)
- {
- if(strcmp(pcValue[i], "1") ==0) {
- GPIOI->BSRR = GPIO_PIN_1;
- }
- else if(strcmp(pcValue[i], "2") ==0) {
- GPIOI->BSRR = GPIO_PIN_2;
- }
- else if(strcmp(pcValue[i], "3") ==0){
- GPIOI->BSRR = GPIO_PIN_3;
- }
- else if(strcmp(pcValue[i], "4") ==0){
- GPIOI->BSRR = GPIO_PIN_4;
- }
- }
- }
- }
- /* ser URI after CGI call */
- return "/STM32F7xxLED.html";
- }
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:
- void httpd_ssi_init(void)
- {
- /* configure SSI handlers (ADC page SSI) */
- http_set_ssi_handler(ADC_Handler, (char const **)TAGS, 1);
- }
Http_set_ssi_handler:
- void http_set_ssi_handler(tSSIHandler ssi_handler, const char **tags, int num_tags)
- {
- LWIP_DEBUGF(HTTPD_DEBUG, ("http_set_ssi_handler\n"));
- LWIP_ASSERT("no ssi_handler given", ssi_handler != NULL);
- LWIP_ASSERT("no tags given", tags != NULL);
- LWIP_ASSERT("invalid number of tags", num_tags > 0);
- g_pfnSSIHandler = ssi_handler;
- g_ppcTags = tags;
- g_iNumTags = num_tags;
- }
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.
- u16_t ADC_Handler(int iIndex, char *pcInsert, int iInsertLen)
- {
- /* We have only one SSI handler iIndex = 0 */
- if (iIndex ==0)
- {
- char Digit1=0, Digit2=0, Digit3=0, Digit4=0;
- uint32_t ADCVal = 0;
- /* configure ADC if not yet configured */
- if (ADC_not_configured ==1)
- {
- ADC_Configuration();
- ADC_not_configured=0;
- }
- HAL_ADC_PollForConversion(&hadc, 10);
- /* get ADC conversion value */
- ADCVal = HAL_ADC_GetValue(&hadc);
- /* convert to Voltage, step = 0.8 mV */
- ADCVal = (uint32_t)(ADCVal * 0.8);
- /* get digits to display */
- Digit1= ADCVal/1000;
- Digit2= (ADCVal-(Digit1*1000))/100;
- Digit3= (ADCVal-((Digit1*1000)+(Digit2*100)))/10;
- Digit4= ADCVal -((Digit1*1000)+(Digit2*100)+ (Digit3*10));
- /* prepare data to be inserted in html */
- *pcInsert = (char)(Digit1+0x30);
- *(pcInsert + 1) = (char)(Digit2+0x30);
- *(pcInsert + 2) = (char)(Digit3+0x30);
- *(pcInsert + 3) = (char)(Digit4+0x30);
- /* 4 characters need to be inserted in html*/
- return 4;
- }
- return 0;
- }
Konfiguracja ADC odbywa się z wykorzystaniem tej funkcji:
- static void ADC_Configuration(void)
- {
- GPIO_InitTypeDef GPIO_InitStruct;
- ADC_ChannelConfTypeDef sConfig;
- /* Enable GPIOF clock */
- __HAL_RCC_GPIOF_CLK_ENABLE();
- /* Configure PF10 as analog input */
- GPIO_InitStruct.Pin = GPIO_PIN_10;
- GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
- GPIO_InitStruct.Pull = GPIO_NOPULL;
- HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
- __HAL_RCC_ADC3_CLK_ENABLE();
- /* ADC3 Configuration ------------------------------------------------------*/
- hadc.Instance = ADC3;
- hadc.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV2;
- hadc.Init.Resolution = ADC_RESOLUTION_12B;
- hadc.Init.ScanConvMode = DISABLE;
- hadc.Init.ContinuousConvMode = ENABLE;
- hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
- hadc.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T1_CC1;
- hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT;
- hadc.Init.NbrOfConversion = 1;
- HAL_ADC_Init(&hadc);
- /* ADC3 Regular Channel Config */
- sConfig.Channel = ADC_CHANNEL_8;
- sConfig.Rank = 1;
- sConfig.SamplingTime = ADC_SAMPLETIME_56CYCLES;
- sConfig.Offset = 0;
- HAL_ADC_ConfigChannel(&hadc, &sConfig);
- /* Enable EOC interupt */
- HAL_ADC_Start(&hadc);
- }
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.
- void DHCP_Periodic_Handle(struct netif *netif)
- {
- #ifdef LWIP_DHCP
- /* Fine DHCP periodic process every 500ms */
- if (HAL_GetTick() - DHCPTimer >= 1000)
- {
- DHCPTimer = HAL_GetTick();
- /* process DHCP state machine */
- DHCP_thread(&gnetif);
- }
- #endif
- }
W funkcji powyżej obsługiwany jest timer, który wywoływany jest co określony czas. Proces DHCP odbywa się w DHCP_thread:
- static void DHCP_thread(void const * argument)
- {
- struct netif *netif = (struct netif *) argument;
- uint8_t iptxt[20];
- char dane[50] = {0};
- uint8_t i = 0;
- struct dhcp *dhcp;
- ip_addr_t ipaddr;
- ip_addr_t netmask;
- ip_addr_t gw;
- char ip[3] = {0};
- switch (DHCP_state)
- {
- case DHCP_START:
- DHCP_state = DHCP_WAIT_ADDRESS;
- Usart_Uart_SendString(USART1, "DHCP_WAIT_ADDRESS", LF_CR);
- break;
- case DHCP_WAIT_ADDRESS:
- sprintf((char *)iptxt, "%s", ipaddr_ntoa((const ip_addr_t *)&netif->ip_addr));
- for(i=0; i<20; i++)
- {
- if(iptxt[i] != '.'){
- ip[i] = iptxt[i];
- }
- else { break; }
- }
- if(i == 3)
- {
- if(ip[0] != '0' || ip[1] != '0' || ip[2] != '0')
- {
- sprintf(dane,"IP address assigned by a DHCP server: %s\n", iptxt);
- Usart_Uart_SendString(USART1,dane, LF_CR);
- DHCP_state = DHCP_ADDRESS_ASSIGNED;
- }
- }
- else if(i == 2)
- {
- if(ip[0] != '0' || ip[1] != '0')
- {
- sprintf(dane,"IP address assigned by a DHCP server: %s\n", iptxt);
- Usart_Uart_SendString(USART1,dane, LF_CR);
- DHCP_state = DHCP_ADDRESS_ASSIGNED;
- }
- }
- else
- {
- dhcp = (struct dhcp *)netif_get_client_data(netif, 0);
- /* DHCP timeout */
- if (dhcp->tries > MAX_DHCP_TRIES)
- {
- Usart_Uart_SendString(USART1,"Timeout", LF_CR);
- DHCP_state = DHCP_TIMEOUT;
- /* Stop DHCP */
- dhcp_stop(netif);
- uint8_t IP_ADDRESS[4];
- uint8_t NETMASK_ADDRESS[4];
- uint8_t GATEWAY_ADDRESS[4];
- IP_ADDRESS[0] = IP_ADDR0;
- IP_ADDRESS[1] = IP_ADDR1;
- IP_ADDRESS[2] = IP_ADDR2;
- IP_ADDRESS[3] = IP_ADDR3;
- NETMASK_ADDRESS[0] = NETMASK_ADDR0;
- NETMASK_ADDRESS[1] = NETMASK_ADDR1;
- NETMASK_ADDRESS[2] = NETMASK_ADDR2;
- NETMASK_ADDRESS[3] = NETMASK_ADDR3;
- GATEWAY_ADDRESS[0] = GW_ADDR0;
- GATEWAY_ADDRESS[1] = GW_ADDR1;
- GATEWAY_ADDRESS[2] = GW_ADDR2;
- GATEWAY_ADDRESS[3] = GW_ADDR3;
- /* Static address used */
- IP4_ADDR(&ipaddr, IP_ADDRESS[0], IP_ADDRESS[1], IP_ADDRESS[2], IP_ADDRESS[3]);
- IP4_ADDR(&netmask, NETMASK_ADDRESS[0], NETMASK_ADDRESS[1] , NETMASK_ADDRESS[2], NETMASK_ADDRESS[3]);
- IP4_ADDR(&gw, GATEWAY_ADDRESS[0], GATEWAY_ADDRESS[1], GATEWAY_ADDRESS[2], GATEWAY_ADDRESS[3]);
- netif_set_addr(netif, &ipaddr, &netmask, &gw);
- }
- }
- break;
- }
- }
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.