wtorek, 24 sierpnia 2021

STM32H7 - Lwip FreeRtos - TCP Server, TCP Client

W tym poście chciałbym opisać sposób konfiguracji LWIP z FreeRtos. Projekt testowałem na układzie STM32H723ZI (Nucleo).

[Źródło: https://www.st.com/content/st_com/en/products/evaluation-tools/product-evaluation-tools/mcu-mpu-eval-tools/stm32-mcu-mpu-eval-tools/stm32-nucleo-boards/nucleo-h753zi.html#overview]

Program:


Konfiguracja zegarów jest następująca:


Poniżej linker script dla pamięci Flash (STM32H723ZGTX_FLASH.ld). 

  1. /*
  2. ******************************************************************************
  3. **
  4. **  File        : LinkerScript.ld
  5. **
  6. **  Author      : STM32CubeIDE
  7. **
  8. **  Abstract    : Linker script for STM32H7 series
  9. **                1024Kbytes FLASH and 192Kbytes RAM
  10. **
  11. **                Set heap size, stack size and stack location according
  12. **                to application requirements.
  13. **
  14. **                Set memory bank area and size if external memory is used.
  15. **
  16. **  Target      : STMicroelectronics STM32
  17. **
  18. **  Distribution: The file is distributed as is, without any warranty
  19. **                of any kind.
  20. **
  21. *****************************************************************************
  22. ** @attention
  23. **
  24. ** Copyright (c) 2020 STMicroelectronics.
  25. ** All rights reserved.
  26. **
  27. ** This software component is licensed by ST under BSD 3-Clause license,
  28. ** the "License"; You may not use this file except in compliance with the
  29. ** License. You may obtain a copy of the License at:
  30. **                        opensource.org/licenses/BSD-3-Clause
  31. **
  32. ****************************************************************************
  33. */
  34.  
  35. /* Entry Point */
  36. ENTRY(Reset_Handler)
  37.  
  38. /* Highest address of the user mode stack */
  39. _estack = 0x20020000;    /* end of RAM */
  40. /* Generate a link error if heap and stack don't fit into RAM */
  41. _Min_Heap_Size = 0x2000 ;      /* required amount of heap  */
  42. _Min_Stack_Size = 0x4000 ; /* required amount of stack */
  43.  
  44. /* Specify the memory areas */
  45. MEMORY
  46. {
  47.   ITCMRAM (xrw)    : ORIGIN = 0x00000000,   LENGTH = 64K
  48.   RAM     (xrw)    : ORIGIN = 0x20000000,   LENGTH = 128K
  49.   FLASH    (rx)    : ORIGIN = 0x08000000,   LENGTH = 1024K
  50.   RAM_D1  (xrw)    : ORIGIN = 0x24000000,   LENGTH = 320K
  51.   RAM_D2  (xrw)    : ORIGIN = 0x30000000,   LENGTH = 32K
  52.   RAM_D3  (xrw)    : ORIGIN = 0x38000000,   LENGTH = 16K
  53. }
  54.  
  55. /* Define output sections */
  56. SECTIONS
  57. {
  58.   /* The startup code goes first into FLASH */
  59.   .isr_vector :
  60.   {
  61.     . = ALIGN(4);
  62.     KEEP(*(.isr_vector)) /* Startup code */
  63.     . = ALIGN(4);
  64.   } >FLASH
  65.  
  66.   /* The program code and other data goes into FLASH */
  67.   .text :
  68.   {
  69.     . = ALIGN(4);
  70.     *(.text)           /* .text sections (code) */
  71.     *(.text*)          /* .text* sections (code) */
  72.     *(.glue_7)         /* glue arm to thumb code */
  73.     *(.glue_7t)        /* glue thumb to arm code */
  74.     *(.eh_frame)
  75.  
  76.     KEEP (*(.init))
  77.     KEEP (*(.fini))
  78.  
  79.     . = ALIGN(4);
  80.     _etext = .;        /* define a global symbols at end of code */
  81.   } >FLASH
  82.  
  83.   /* Constant data goes into FLASH */
  84.   .rodata :
  85.   {
  86.     . = ALIGN(4);
  87.     *(.rodata)         /* .rodata sections (constants, strings, etc.) */
  88.     *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
  89.     . = ALIGN(4);
  90.   } >FLASH
  91.  
  92.   .ARM.extab   : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH
  93.   .ARM : {
  94.     __exidx_start = .;
  95.     *(.ARM.exidx*)
  96.     __exidx_end = .;
  97.   } >FLASH
  98.  
  99.   .preinit_array     :
  100.   {
  101.     PROVIDE_HIDDEN (__preinit_array_start = .);
  102.     KEEP (*(.preinit_array*))
  103.     PROVIDE_HIDDEN (__preinit_array_end = .);
  104.   } >FLASH
  105.   .init_array :
  106.   {
  107.     PROVIDE_HIDDEN (__init_array_start = .);
  108.     KEEP (*(SORT(.init_array.*)))
  109.     KEEP (*(.init_array*))
  110.     PROVIDE_HIDDEN (__init_array_end = .);
  111.   } >FLASH
  112.   .fini_array :
  113.   {
  114.     PROVIDE_HIDDEN (__fini_array_start = .);
  115.     KEEP (*(SORT(.fini_array.*)))
  116.     KEEP (*(.fini_array*))
  117.     PROVIDE_HIDDEN (__fini_array_end = .);
  118.   } >FLASH
  119.  
  120.   /* used by the startup to initialize data */
  121.   _sidata = LOADADDR(.data);
  122.  
  123.   /* Initialized data sections goes into RAM, load LMA copy after code */
  124.   .data :
  125.   {
  126.     . = ALIGN(4);
  127.     _sdata = .;        /* create a global symbol at data start */
  128.     *(.data)           /* .data sections */
  129.     *(.data*)          /* .data* sections */
  130.     *(.RamFunc)        /* .RamFunc sections */
  131.     *(.RamFunc*)       /* .RamFunc* sections */
  132.  
  133.     . = ALIGN(4);
  134.     _edata = .;        /* define a global symbol at data end */
  135.   } >RAM AT> FLASH
  136.  
  137.   /* Uninitialized data section */
  138.   . = ALIGN(4);
  139.   .bss :
  140.   {
  141.     /* This is used by the startup in order to initialize the .bss secion */
  142.     _sbss = .;         /* define a global symbol at bss start */
  143.     __bss_start__ = _sbss;
  144.     *(.bss)
  145.     *(.bss*)
  146.     *(COMMON)
  147.  
  148.     . = ALIGN(4);
  149.     _ebss = .;         /* define a global symbol at bss end */
  150.     __bss_end__ = _ebss;
  151.   } >RAM
  152.  
  153.   /* User_heap_stack section, used to check that there is enough RAM left */
  154.   ._user_heap_stack :
  155.   {
  156.     . = ALIGN(8);
  157.     PROVIDE ( end = . );
  158.     PROVIDE ( _end = . );
  159.     . = . + _Min_Heap_Size;
  160.     . = . + _Min_Stack_Size;
  161.     . = ALIGN(8);
  162.   } >RAM
  163.  
  164.   .lwip_sec (NOLOAD) : {
  165.     . = ABSOLUTE(0x30000000);
  166.     *(.RxDecripSection)
  167.    
  168.     . = ABSOLUTE(0x30000060);
  169.     *(.TxDecripSection)
  170.    
  171.     . = ABSOLUTE(0x30000200);
  172.     *(.RxArraySection)
  173.   } >RAM_D2 AT> ROM
  174.  
  175.   /* Remove information from the standard libraries */
  176.   /DISCARD/ :
  177.   {
  178.     libc.a ( * )
  179.     libm.a ( * )
  180.     libgcc.a ( * )
  181.   }
  182.  
  183.   .ARM.attributes 0 : { *(.ARM.attributes) }
  184. }

Jak w poprzednich przykładach dla STM32H7 należy go zmodyfikować w celu dołożenia deskryptorów ETH oraz buforów do pamięci RAM_D2. Należy zmodyfikować tylko część dla pamięci FLASH. Drugi plik dla pamięci _RAM może zostać pomięty, służy on do uruchamiania kodu z pamięci RAM.

Drugim kluczowym elementem jest konfiguracja Cortex-M7. Należy uruchomić ICache i DCache wraz z MPU (ang. Memmory Protection Unit). 


Konfiguracja wygenerowana w kodzie wygląda następująco:

  1. void MPU_Config(void)
  2. {
  3.   MPU_Region_InitTypeDef MPU_InitStruct = {0};
  4.  
  5.   /* Disables the MPU */
  6.   HAL_MPU_Disable();
  7.   /** Initializes and configures the Region and the memory to be protected
  8.   */
  9.   MPU_InitStruct.Enable = MPU_REGION_ENABLE;
  10.   MPU_InitStruct.Number = MPU_REGION_NUMBER0;
  11.   MPU_InitStruct.BaseAddress = 0x30000000;
  12.   MPU_InitStruct.Size = MPU_REGION_SIZE_256B;
  13.   MPU_InitStruct.SubRegionDisable = 0x0;
  14.   MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
  15.   MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
  16.   MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
  17.   MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
  18.   MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
  19.   MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
  20.  
  21.   HAL_MPU_ConfigRegion(&MPU_InitStruct);
  22.   /** Initializes and configures the Region and the memory to be protected
  23.   */
  24.   MPU_InitStruct.Enable = MPU_REGION_ENABLE;
  25.   MPU_InitStruct.Number = MPU_REGION_NUMBER1;
  26.   MPU_InitStruct.BaseAddress = 0x30004000;
  27.   MPU_InitStruct.Size = MPU_REGION_SIZE_16KB;
  28.   MPU_InitStruct.SubRegionDisable = 0x0;
  29.   MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
  30.   MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
  31.   MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
  32.   MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
  33.   MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
  34.   MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
  35.  
  36.   HAL_MPU_ConfigRegion(&MPU_InitStruct);
  37.   /* Enables the MPU */
  38.   HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
  39.  
  40. }

Powyższa konfiguracja wydaje się działać poprawnie, natomiast dobrze by było maksymalnie ją ulepszyć w celu poprawnego działania aplikacji. Bardzo dobre przykłady można znaleźć na githubie, pod tym linkiem

W związku z tym, że pamięć RAM_D2 jest w tym mikrokontrolerze dosyć ograniczona (32kB). Z tego powodu należy przenieść bufor np. RX do innej części pamięci. Będę tutaj postępował zgodnie z wytycznymi z powyższego linku.

Konfiguracja bloków pamięci:


Pierwsza część ma zabezpieczać nie uprawniony dostęp do nieużywanej pamięci. Druga część dotyczy pamięci dla stosu LWIP. Ostatnia część jest wykorzystywana dla deskryptorów. 

Konfiguracja pamięci buforów w ETH:


Modyfikacje biblioteki LWIP:


Po wygenerowaniu projektu należy odpowiednio skonfigurować plik .FLASH:

  1.   /* Uninitialized data section */
  2.   . = ALIGN(4);
  3.   .bss :
  4.   {
  5.     /* This is used by the startup in order to initialize the .bss secion */
  6.     _sbss = .;         /* define a global symbol at bss start */
  7.     __bss_start__ = _sbss;
  8.     *(.bss)
  9.     *(.bss*)
  10.     *(COMMON)
  11.    
  12.     . = ALIGN(32);
  13.     *(.Rx_PoolSection)
  14.     . = ALIGN(4);
  15.     _ebss = .;         /* define a global symbol at bss end */
  16.     __bss_end__ = _ebss;
  17.   } >RAM
  18.  
  19.   /* User_heap_stack section, used to check that there is enough RAM left */
  20.   ._user_heap_stack :
  21.   {
  22.     . = ALIGN(8);
  23.     PROVIDE ( end = . );
  24.     PROVIDE ( _end = . );
  25.     . = . + _Min_Heap_Size;
  26.     . = . + _Min_Stack_Size;
  27.     . = ALIGN(8);
  28.   } >RAM
  29.  
  30.   .lwip_sec (NOLOAD) :
  31.   {
  32.     . = ABSOLUTE(0x30000000);
  33.     *(.RxDecripSection)
  34.    
  35.     . = ABSOLUTE(0x30000100);
  36.     *(.TxDecripSection)
  37.    
  38.   } >RAM_D2

Następnie przechodzimy do liku ethernetif.c. W pliku powinna znajdować się sekcja dla buforów RX oraz TX:

  1. #if defined ( __ICCARM__ ) /*!< IAR Compiler */
  2.  
  3. #pragma location=0x30000000
  4. ETH_DMADescTypeDef  DMARxDscrTab[ETH_RX_DESC_CNT]; /* Ethernet Rx DMA Descriptors */
  5. #pragma location=0x30000100
  6. ETH_DMADescTypeDef  DMATxDscrTab[ETH_TX_DESC_CNT]; /* Ethernet Tx DMA Descriptors */
  7.  
  8. #elif defined ( __CC_ARM )  /* MDK ARM Compiler */
  9.  
  10. __attribute__((at(0x30000000))) ETH_DMADescTypeDef  DMARxDscrTab[ETH_RX_DESC_CNT]; /* Ethernet Rx DMA Descriptors */
  11. __attribute__((at(0x30000100))) ETH_DMADescTypeDef  DMATxDscrTab[ETH_TX_DESC_CNT]; /* Ethernet Tx DMA Descriptors */
  12.  
  13. #elif defined ( __GNUC__ ) /* GNU Compiler */
  14.  
  15. ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT] __attribute__((section(".RxDecripSection"))); /* Ethernet Rx DMA Descriptors */
  16. ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT] __attribute__((section(".TxDecripSection")));   /* Ethernet Tx DMA Descriptors */
  17.  
  18. #endif

Natomiast należy jeszcze dopisać część dla Rx_PoolSection zdefiniowanej w pamięci RAM:

  1. #if defined ( __ICCARM__ ) /*!< IAR Compiler */
  2. #pragma location = 0x30000200
  3. extern u8_t memp_memory_RX_POOL_base[];
  4.  
  5. #elif defined ( __CC_ARM )  /* MDK ARM Compiler */
  6. __attribute__((at(0x30000200)) extern u8_t memp_memory_RX_POOL_base[];
  7.  
  8. #elif defined ( __GNUC__ ) /* GNU Compiler */
  9. __attribute__((section(".Rx_PoolSection"))) extern u8_t memp_memory_RX_POOL_base[];
  10. #endif

Ostatnim elementem jest zdefiniowanie makra DATA_IN_D2_RAM:


Po wygenerowaniu projektu na samym początku sprawdzam ping do urządzenia:


Działanie projektu jako serwer echo TCP jest następujące:


Pełną konfigurację wszystkich elementów systemu można podglądać w udostępnionych plikach. 

Uruchomienie domyślnego wątku:

  1. osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 256);
  2. defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);


W nim zostanie uruchomione LWIP (wygenerowane przez CubeMx) oraz drugi wątek obsługujący połączenie TCP:

  1. void StartDefaultTask(void const * argument)
  2. {
  3.   /* init code for LWIP */
  4.   MX_LWIP_Init();
  5.   /* USER CODE BEGIN 5 */
  6.   osThreadDef(echoTask, StartEchoTask, osPriorityNormal, 0, configMINIMAL_STACK_SIZE);
  7.   echoTaskHandle = osThreadCreate(osThread(echoTask), NULL);
  8.   /* Infinite loop */
  9.   for(;;)
  10.   {
  11.       osDelay(1000);
  12.   }
  13.   /* USER CODE END 5 */
  14. }

Poniżej kod dla TCP Serwera z dodatkowym wysyłaniem danych po UART:

  1. void StartEchoTask(void const *argument)
  2. {
  3.   struct netconn *conn, *newconn;
  4.   err_t err, accept_err;
  5.   struct netbuf *buf;
  6.   void *data;
  7.   u16_t len;
  8.  
  9.   LWIP_UNUSED_ARG(argument);
  10.  
  11.   conn = netconn_new(NETCONN_TCP);
  12.  
  13.   if (conn != NULL) {
  14.     err = netconn_bind(conn, NULL, 1001);
  15.  
  16.     if (err == ERR_OK) {
  17.       netconn_listen(conn);
  18.  
  19.       while (1) {
  20.         accept_err = netconn_accept(conn, &newconn);
  21.  
  22.         if (accept_err == ERR_OK) {
  23.           while (netconn_recv(newconn, &buf) == ERR_OK) {
  24.             do {
  25.               netbuf_data(buf, &data, &len);
  26.               netconn_write(newconn, data, len, NETCONN_COPY);
  27.               HAL_UART_Transmit(&huart1, (uint8_t*)data, len, 1000);
  28.               HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
  29.             }
  30.             while (netbuf_next(buf) >= 0);
  31.             netbuf_delete(buf);
  32.           }
  33.  
  34.           netconn_close(newconn);
  35.           netconn_delete(newconn);
  36.         }
  37.       }
  38.     }
  39.     else {
  40.       netconn_delete(newconn);
  41.     }
  42.   }
  43. }

Poniżej aplikacja klienta:

  1. void StartTcpClientTask(void const *argument)
  2. {
  3.   err_t err;
  4.   struct netconn *conn;
  5.   struct netbuf *buf;
  6.   void *data;
  7.   uint16_t len; //buffer length
  8.  
  9.   LWIP_UNUSED_ARG(argument);
  10.  
  11.   while (1)
  12.   {
  13.     if (gnetif.ip_addr.addr == 0 || gnetif.netmask.addr == 0 || gnetif.gw.addr == 0) {
  14.       osDelay(1000);
  15.       continue;
  16.     }
  17.     else {
  18.       osDelay(20);
  19.     }
  20.  
  21.     conn = netconn_new(NETCONN_TCP);
  22.  
  23.     if (conn != NULL) {
  24.       IP4_ADDR(&server_addr, SERVER_IP_1_ADDR, SERVER_IP_2_ADDR, SERVER_IP_3_ADDR, SERVER_IP_4_ADDR);
  25.       err = netconn_connect(conn, &server_addr, SERVER_PORT);
  26.  
  27.       if (err != ERR_OK) {
  28.         netconn_delete(conn);
  29.         continue;
  30.       }
  31.       HAL_UART_Transmit(&huart1, (uint8_t*)"zzz\r\n", 5, 1000);
  32.  
  33.       while (netconn_recv(conn, &buf) == ERR_OK) {
  34.           do {
  35.               netbuf_data(buf, &data, &len);
  36.               memcpy(recData, data, len);
  37.  
  38.               netconn_write(conn, data, len, NETCONN_COPY);
  39.               HAL_UART_Transmit(&huart1, (uint8_t*)"REC: ", 5, 1000);
  40.               HAL_UART_Transmit(&huart1, (uint8_t*)&recData[0], len, 1000);
  41.               HAL_UART_Transmit(&huart1, (uint8_t*)"\r\n", 2, 1000);
  42.               HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
  43.              
  44.               if(recData[0] == 'c' && recData[1] == 'l' && recData[2] == 'o') {
  45.                   HAL_UART_Transmit(&huart1, (uint8_t*)"CLOSE", 5, 1000);
  46.                   netconn_close(conn);
  47.                   netconn_delete(conn);
  48.               }
  49.  
  50.             } while (netbuf_next(buf) >= 0);
  51.             netbuf_delete(buf);
  52.        }
  53.  
  54.        netconn_close(conn);
  55.        netconn_delete(conn);
  56.  
  57.     }
  58.   }
  59. }

Uruchomienie wątku klienta następuje jak wcześniej dla serwera po uruchomieniu biblioteki LWIP:

  1. void StartClientTask(void)
  2. {
  3.     osThreadDef(tcpClientTask, StartTcpClientTask, osPriorityNormal, 0, configMINIMAL_STACK_SIZE);
  4.     tcpClientTaskHandle = osThreadCreate(osThread(tcpClientTask), NULL);
  5. }
  6.  
  7. void StartDefaultTask(void const * argument)
  8. {
  9.   /* init code for LWIP */
  10.   MX_LWIP_Init();
  11.   /* USER CODE BEGIN 5 */
  12.   StartClientTask();
  13.   /* Infinite loop */
  14.   for(;;)
  15.   {
  16.       osDelay(1000);
  17.   }

  18.   /* USER CODE END 5 */
  19. }

Efekt działania programu jest następujący:



Projekt można pobrać pod tym linkiem.