piątek, 20 stycznia 2017

[8,0] STM32F7 - LWIP, FreeRtos, Echo Serwer

W tym poście chciałbym przedstawić sposób konfiguracji oraz połączenia mikrokontrolera STM32F7 w konfiguracji echo serwer z komputerem.

Biblioteka LWIP (ang. lightweight IP)[1]:


Jest to biblioteka dzięki krórej w dosyć łatwy sposób można wykonać różne sposoby komunikacji sieciowej. Zawarto w niej protokoły IP, TCP, UDP, ICMP, ARP. W jej skład dodatkowo wchodzą takie moduły jak DHCP, DNS, AutoIP, SNMP, IGMP, SLIP, PPO. 

Jeśli chodzi o jej zaprogramowanie to do dyspozycji są trzy sposoby. Pierwszy z nich jest to interfejs surowy bez systemu operacyjnego, czyli podstawowy interfejs biblioteki. Drugi dotyczy jest to tzw interfejs sekwencyjny, który jest obsługiwane w trybie otwórz - czytaj - pisz - zamknij. Powoduje on, że wywołania tego systemu są blokujące, wobec czego musi on działać w wątkach. Dlatego często wykorzystuje się system operacyjny tzn. FreeRtos. Te dwa sposoby są właściwie najczęściej spotykane. Trzeci i ostatni z nich dotyczy interfejsu gniazd, czyli obudowywany jest interfejs sekwencyjny, który ma być zgodny z interfejsem gniazd stosownym w normalnych komputerach.

Z biblioteką LWIP oraz FreeRtos można przygotować takie programy jak np. serwer Web oparty na netconn API, serwer Web oparty na soketach, czy aplikacje TCP,UDP.

Tworzenie projektu CubeMx:


Generowanie projektu jest bardzo proste. Całość należy rozpocząć standardowo od nowego projektu gdzie wybieram płytkę discovery F7 z układem STM32F746NGHx. Następnie czyszczę przypisane piny do układu, tak aby uzyskać czysty mikrokontroler. 

Pierwszym elementem do włączenia jest RCC z kwarcem zewnętrznym:


W związku z tym, że ten projekt będzie wykorzystywał FreeRtos to należy zmienić timer ,odpowiedzialny podstawę czasu, z Systicka na jakikolwiek inny.


Teraz kolej na konfigurację Ethernetu z ustawieniami RMII (ang. reduced media-independent interface).


Kolejnym krokiem jest włączenie FreeRtos oraz LWIP:


Następnym etapem jest konfiguracja zegara:


No i na samym końcu zostało skonfigurowanie bibliotek LWIP oraz konfiguracja ETH. FreeRtos'a nie konfiguruje ponieważ potrzebne będzie tylko DefaultTask, w związku z czym nie ma potrzeby aby dodawać więcej. 


Dla Lwip należy wprowadzić IP, maskę oraz bramę domyślną.

Po tych operacjach można już wygenerować kod, trzeba jeszcze zwiększyć pamięć stosu oraz sterty dla systemu operacyjnego:


Kod Programu:

Teraz przejdę do obsługi programu.

Biblioteka wygenerowała wątek tzw. DefaultTaks. W nim będą realizowane wszystkie inicjalizacje.

  1.   //Biblioteki do dołączenia
  2.   /* USER CODE BEGIN Includes */
  3.   #include "lwip/opt.h"
  4.   #include "lwip/arch.h"
  5.   #include "lwip/api.h"
  6.   #include <string.h>
  7.   /* USER CODE END Includes */
  8.   /* Create the thread(s) */
  9.   /* definition and creation of defaultTask */
  10.   osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
  11.   defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
  12.   /* USER CODE BEGIN RTOS_THREADS */
  13.   /* add threads, ... */
  14.   /* USER CODE END RTOS_THREADS */
  15.   /* USER CODE BEGIN RTOS_QUEUES */
  16.   /* add queues, ... */
  17.   /* USER CODE END RTOS_QUEUES */
  18.   /* Start scheduler */
  19.   osKernelStart();
  20. ......
  21. ......
  22. ......
  23. /* StartDefaultTask function */
  24. void StartDefaultTask(void const * argument)
  25. {
  26.    MX_LWIP_Reset();
  27.   /* init code for LWIP */
  28.   MX_LWIP_Init();
  29.   /* USER CODE BEGIN 5 */
  30.   Echo_TCP();
  31.   /* USER CODE END 5 */
  32. }

Funckja MX_LWIP_Init() inicjalizuje bibliotekę, przypisuje adresy itp. Taki zestaw jest minimalny do poprawnej pracy biblioteki jak i stosu, który potem można odpowiednio rozwijać w dodatkowych funkcjach.

MX_LWIP_Reset() jest funkcją dodaną. Czyści dane dotyczące IP, maski oraz bramy domyślnej:

  1. void MX_LWIP_Reset( void )
  2. {
  3.     ipaddr.addr = 0;
  4.     netmask.addr = 0;
  5.     gw.addr = 0;
  6. }

Poniżej funkcja włączająca bibliotekę wygenerowana przez CubeMx:

  1. void MX_LWIP_Init(void)
  2. {
  3.   /* IP addresses initialization */
  4.   IP_ADDRESS[0] = 160;
  5.   IP_ADDRESS[1] = 252;
  6.   IP_ADDRESS[2] = 100;
  7.   IP_ADDRESS[3] = 10;
  8.   NETMASK_ADDRESS[0] = 255;
  9.   NETMASK_ADDRESS[1] = 255;
  10.   NETMASK_ADDRESS[2] = 255;
  11.   NETMASK_ADDRESS[3] = 0;
  12.   GATEWAY_ADDRESS[0] = 169;
  13.   GATEWAY_ADDRESS[1] = 252;
  14.   GATEWAY_ADDRESS[2] = 100;
  15.   GATEWAY_ADDRESS[3] = 1;
  16.   /* Initilialize the LwIP stack with RTOS */
  17.   tcpip_init( NULL, NULL );
  18.   /* IP addresses initialization without DHCP (IPv4) */
  19.   IP4_ADDR(&ipaddr, IP_ADDRESS[0], IP_ADDRESS[1], IP_ADDRESS[2], IP_ADDRESS[3]);
  20.   IP4_ADDR(&netmask, NETMASK_ADDRESS[0], NETMASK_ADDRESS[1] , NETMASK_ADDRESS[2], NETMASK_ADDRESS[3]);
  21.   IP4_ADDR(&gw, GATEWAY_ADDRESS[0], GATEWAY_ADDRESS[1], GATEWAY_ADDRESS[2], GATEWAY_ADDRESS[3]);
  22.   /* add the network interface (IPv4/IPv6) with RTOS */
  23.   netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);
  24.   /* Registers the default network interface */
  25.   netif_set_default(&gnetif);
  26.   if (netif_is_link_up(&gnetif))
  27.   {
  28.     /* When the netif is fully configured this function must be called */
  29.     netif_set_up(&gnetif);
  30.   }
  31.   else
  32.   {
  33.     /* When the netif link is down this function must be called */
  34.     netif_set_down(&gnetif);
  35.   }
  36. /* USER CODE BEGIN 3 */
  37. /* USER CODE END 3 */
  38. }

Implementacji można dokonać na dwa sposoby, pierwszy z nich może być wykonany na porcie 80 opartych o netconn API. Wszystkie operacje podobnie jak następne zostały oparte o przykłady udostępnione przez ST dla F4.

  1. static void Echo_TCP(void)
  2. {
  3.     struct netconn *connection, *newconnection;
  4.     err_t err, accept_err;
  5.     struct netbuf* buffer;
  6.     void* data;
  7.     u16_t len;
  8.     err_t recv_err;
  9.     //Makro tworzące nową strukturę oraz inuicjalizyjące ją nowymi wartościami.
  10.     connection = netconn_new(NETCONN_TCP);
  11.     if (connection != NULL)
  12.     {
  13.         err = netconn_bind(connection, NULL, 80);       //Przypisz połączenie do portu 80
  14.         if (err == ERR_OK)
  15.         {
  16.             netconn_listen(connection);                 //Połączenie zamienione na tryb słuchania
  17.             while (1)
  18.             {
  19.                 //Funkcja oczekujaca na polaczenie od klienta
  20.                 accept_err = netconn_accept(connection, &newconnection);
  21.                 //Obsluga polaczenia
  22.                 if (accept_err == ERR_OK)
  23.                 {
  24.                     while ((recv_err = netconn_recv(newconnection, &buffer)) == ERR_OK)
  25.                     {
  26.                         do
  27.                         {
  28.                            netbuf_data(buffer, &data, &len);
  29.                            netconn_write(newconnection, data, len, NETCONN_COPY);
  30.                         }
  31.                         while (netbuf_next(buffer) >= 0);
  32.                        netbuf_delete(buffer);
  33.                     }
  34.                     //Zamknij polaczenie
  35.                     netconn_close(newconnection);
  36.                     netconn_delete(newconnection);
  37.                 }
  38.             }
  39.         }
  40.         else { netconn_delete(newconnection); }  //Nie udalo się nawiazac polaczenia
  41.     }
  42. }

Na początku wykorzystywane jest makro netconn_new, które tworzy nowe połączenie z układem. Następnie sprawdzane jest jego status, czy udało się nawiązać połączenie. Makro netconn_listen ustawia ustawia serwer TCP w tryb nasłuchiwania. Dalej w pętli while następuje wejście do funkcji blokującej netconn_accpet, która oczekuje na połączenie od klienta. Gdy połączenie zostaje zaakceptowane następuje oczekiwanie na przesłanie danych. Dane zostają odebrane i wprowadzone do bufora. Funkcja netconn_write przesyła te dane ponownie do układu. Całość znajduje się w pętli do while, która przechodzi dalej dopiero gdy dane zostaną przesłane. Po ich transmisji buffor zostaje zwolniony. Na samym końcu połączenie zostaje zamknięta oraz bufor zostaje opróżniony.

Drugi sposób działania z wykorzystaniem netconn będzie polegał na cyklicznym przesyłaniu danych. Taki sposób można wykorzystać np. do zbierania danych z kilku czujników. Dzięki wprowadzeniu danych w odpowiednio przygotowaną ramkę dane można odpowiednio obrobić w dedykowanym programie.

Przykładowy format ramki danych [DAT1;DAT2;...DATN], tak przygotowane informacje, jak wspomniałem wcześniej, można łatwo rozdzielić i przygotować, czy to poprzez wysyłanie informacji w kodach ASCII czy zapisane szesnastkowo. W tym przypadku całą procedurę obsługi zawarłem w StartDefaultTask.

  1. void StartDefaultTask(void const * argument)
  2. {
  3.   struct netconn *conn, *newconn;
  4.   err_t err, accept_err;
  5.   char buffer[50] = {0};
  6.   uint8_t zmienna1 = 0x01;
  7.   uint8_t zmienna2 = 0x02;
  8.   uint8_t zmienna3 = 0x03;
  9.   uint8_t zmienna4 = 0x04;
  10.   uint8_t zmienna5 = 0x05;
  11.   MX_LWIP_Reset();
  12.   /* init code for LWIP */
  13.   MX_LWIP_Init();
  14.   conn = netconn_new(NETCONN_TCP);
  15.   if (conn != NULL)
  16.   {
  17.       err = netconn_bind(conn, NULL, 80);                   //Podłączenie
  18.       if (err == ERR_OK)
  19.       {
  20.           netconn_listen(conn);                         //Przejscie w tryb nasłuchiwania
  21.           accept_err = netconn_accept(conn, &newconn);  //Akceptacja połączenia
  22.       }
  23.   }
  24.   /* USER CODE BEGIN 5 */
  25.   /* Infinite loop */
  26.   for(;;)
  27.   {
  28.       if(accept_err == ERR_OK)
  29.       {
  30.           memset(buffer, sizeof(buffer), 0x00);
  31.           sprintf(buffer, "[%x;%x;%x;%x;%x]", zmienna1++, zmienna2++, zmienna3++, zmienna4++, zmienna5++);
  32.           netconn_write(newconn, (const unsigned char*)buffer, strlen(buffer), NETCONN_COPY);
  33.           osDelay(10000);
  34.       }
  35.   }
  36.   /* USER CODE END 5 */
  37. }

Druga wersja oparta o TCP wypuszczony dla STM32F4 z lekką modyfikacją. Działa ona także dla wersji bez FreeRtos. Funkcje zawarte w tym przykładzie są implementacjami tych które zostały zdefiniowane w bibliotece LWIP.

  1. /* StartDefaultTask function */
  2. void StartDefaultTask(void const * argument)
  3. {
  4.   MX_LWIP_Reset();
  5.   /* init code for LWIP */
  6.   MX_LWIP_Init();
  7.   tcp_echoserver_init();
  8.   /* USER CODE BEGIN 5 */
  9.   /* Infinite loop */
  10.   for(;;)
  11.   {
  12.      ethernetif_input(&gnetif);
  13.      osDelay(1);
  14.   }
  15.   /* USER CODE END 5 */
  16. }

Poniżej procedura uruchomienia nowego serwera TCP:

  1. //Inicjalizacja echoserwera
  2. //Do stworzenia polaczenia tcp_new | tcp_bind | tcp_listen | tcp_accept | tcp_accepted | tcp_connect
  3. void tcp_echoserver_init(void)
  4. {
  5.   //Stworzenie nowego tcp pcb (protocol control block)
  6.   tcp_echoserver_pcb = tcp_new();
  7.   if (tcp_echoserver_pcb != NULL)
  8.   {
  9.     err_t err;          //Zmienna przechowująca informacje o stanie inicjalizacji
  10.     //Przypisanie TCP PCB do lokalnego adresu IP oraz portu
  11.     //1001 zwykly port     //7 echo port
  12.     err = tcp_bind(tcp_echoserver_pcb, IP_ADDR_ANY, 1001);
  13.     if (err == ERR_OK)
  14.     {
  15.       //Rozpoczecie nasluchiwania dla klientow TCP
  16.       //Wykonywanie psywnego otwarcia.
  17.       tcp_echoserver_pcb = tcp_listen(tcp_echoserver_pcb);
  18.      
  19.       //Rejestrowanie funkcji zwrotnej, króra obsługuje faktyczne otwieranie
  20.       //połączenia
  21.       tcp_accept(tcp_echoserver_pcb, tcp_echoserver_accept);
  22.     }
  23.     else
  24.     {
  25.       //Przenosi element do MEMP_TCP_PCB następnie zwalnia tcp_echoserver_pcb
  26.       memp_free(MEMP_TCP_PCB, tcp_echoserver_pcb);
  27.     }
  28.   }
  29. }

Pomiędzy wywołaniami funkcji tcp_listen oraz tcp_accept. Serwer nie może odebrać połączenia.

Dane są odbierane i przesyłane, za to odpowiedzialne jest funkcja tcp_echoserwer_send:

  1. static void tcp_echoserver_send(struct tcp_pcb *tpcb, struct tcp_echoserver_struct *es)
  2. {
  3.   struct pbuf *ptr;
  4.   char data[40] = {0};
  5.   char buffereth[100] = {0};
  6.   uint8_t i = 0;
  7.   err_t wr_err = ERR_OK;
  8.   while ((wr_err == ERR_OK) &&
  9.          (es->!= NULL) &&
  10.          (es->p->len <= tcp_sndbuf(tpcb)))
  11.   {
  12.     ptr = es->p;
  13.     wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);
  14.     if (wr_err == ERR_OK)
  15.     {
  16.       u16_t plen;
  17.       u8_t freed;
  18.       plen = ptr->len;
  19.       es->= ptr->next;
  20.       if(es->!= NULL)
  21.       {
  22.         pbuf_ref(es->p);
  23.       }
  24.       do
  25.       {
  26.         freed = pbuf_free(ptr);
  27.       }
  28.       while(freed == 0);
  29.      tcp_recved(tpcb, plen);
  30.    }
  31.    else if(wr_err == ERR_MEM)
  32.    {
  33.      es->= ptr;
  34.    }
  35.    else
  36.    {
  37.      /* other problem ?? */
  38.    }
  39.   }
  40. }

W przypadku tak zdefiniowanego serwera w bardzo łatwy sposób można odbierać dane i przesyłać własne, dzięki temu można uzyskać komunikację dwustronną. Poprzez wysyłanie odpowiedniej komendy mikrokontroler będzie na nie reagował. Dodatkowo w bardzo prosty sposób można przygotować dedykowany do urządzenia program, np. w C# lub C++.

Poniżej funkcja odbierająca dane i reagująca na komendy CHECK oraz END. Pierwsza odsyła zmodyfikowaną odpowiedź, druga natomiast zamyka połączenie z serwerem.

Całość projektu można ściągnąć pod tym linkiem.

Bibliografia:


[1] Peczarski M. - Mikrokontrolery STM32 w sieci Ethernet w przykładach
[2] LwiP TCP/IP stack demonstation for stm32f4x7