cancel
Showing results for 
Search instead for 
Did you mean: 

Creating a dual IPv6 & IPv4 NetXDuo UDP application for STM32H7 using CubeMX

Laurids_PETERSEN
Community manager
Community manager

Summary

There exist a few helpful examples of UDP applications in our X-CUBE-AZRTOS-H7 package. However, you may find that these examples are not helpful in understanding the steps needed to bring up a similar application on your custom hardware. There are also no examples currently in the package of using IPv6. In this article, we go through the process of using CubeMX to bring up a dual IPv4/IPv6 NetXDuo application.

Prerequisites

  • NUCLEO-H743
  • CubeMX
  • X-CUBE-AZRTOS-H7
  • STM32Cube_FW_H7
  • Tera Term (or any serial terminal)
  • Packet sender (or any UDP packet sending utility that supports both IPv4 and IPv6)

With CubeMX downloaded, you can install the relevant packages by selecting help -> manage embedded software packages. 

1 Manage embedded software packages.png

 

 

Example code on github

The same steps that are described in this article were used to create an example project that is publicly available on the STM32 github, link:
https://github.com/stm32-hotspot/NUCLEO_H743_NetXDuo_UDP_IPv4_IPv6

CubeMX Configuration

For this project, we start from the board selector and find the NUCLEO-H743ZI.

2. NULCEO.png

 

 

 

 

 

 

 

 

3. Nulceo.png

When prompted, select no to initialize all peripherals with their default modes.

Once in the pinout configuration, to give us a blank slate, select pinout->Clear pinout.

4. Clear pinout.png

UART Configuration

First, we set up a UART that is connected to the onboard STLINK. This is used for printing the IPv6/IPv4 addresses, and to indicate to us the state of the application.

Select USART3 and set the mode to asynchronous. The default configurations for the peripheral are already what we want:

5. already what we want.png

We need to move the GPIOs to connect them to the onboard STLINK. In the GUI, you can CTRL+click and drag the pins to reassign them.

USART3_RX -> PD9
USART3_TX ->PD8

6. pda8.png

pd9.png

Ethernet Configuration

First, select the ETH peripheral, and enable it as an RMII interface, which is used on this board. We also need to modify the default addresses for the Ethernet descriptors. By default, it locates the descriptors in 0x30000000, which is SRAM1. The descriptors can be placed flexibly, but I prefer to place them, in SRAM3 located at 0x30040000, clear out of the way of any other RAM that you may use. 

7 ram that you may use.png

We need to change around some of the GPIOs for the ETH to reflect the board layout as well using CTRL+click and drag to the correct pins.

ETH_TXD0 -> PG13
ETH_TX_EN -> PG11

8. pg11.png

Next, in ETH GPIO settings, make sure all GPIOs are set to very High speed:

9. very high speed.png

In the NVIC tab, enable the global Ethernet interrupt:

10 global interrupt.png

MPU configuration

Since the ETH peripheral on the H7 requires that CACHE be enabled, we also need to enable the MPU (memory protection unit.) This ensures that the CACHE does not mess with the descriptors and receive buffers for the Ethernet.

The MPU configurations can be found in the CORTEX_M7 category under system core.

First, we want to enable ICAHCE and DCACHE, and set speculation default mode to enabled. Doing so creates a default configuration for MPU region 0, which covers the entire 4GB address space of the core with ALL ACCESS NOT PERMITTED. This gives a default configuration as a safety net, and now any subsequent configurations of MPU regions allows us to set the configurations for specific regions. When two MPU regions have overlapping addresses, the higher numbered region overwrites the configurations of the lower numbered regions for the address range that is configured in that higher numbered region.

11. higher numbered region.png

With that said, we configure region 1 to manage the access to the Ethernet descriptors and receive buffer. In this case, we want to ensure that the MPU has full access to the area. We adjust the TEX field level to level 1 to indicate that it is a shareable device, but we do not want the region to be cacheable. The region size here is set to 128KB to ensure that we are covering all the receive buffers. Should you want to be more efficient you can likely get away with configuring the region to be smaller than this:

12. configuring the region.png

Azure and NetXDuo Configuration

With the Ethernet configured, we can bring Azure NetXDuo into the project. Near the top of the application, you select software packs -> select components:

13. select components.png

Assuming that the X-CUBE-AZRTOS-H7 package is installed, it should be selectable as a drop-down in this menu.

As is the case with all the components in the Azure package, they rely on the use of the Azure RTOS: ThreadX, so we need to activate this:

14. activate this.png

And activate NetXDuo core:

15. activate netxduo core.png

Lastly, we need to make sure the package is aware of what hardware driver and interface it needs to use for the PHY layer:

16 phy layer.png

Under middlewares and software packages, we need to select the Azure H7 middleware, and be sure to enable all components previously selected:

17 we previously selected.png

In the AzureRTOS application tab, I prefer to change my NetXDuo memory pool size to 30K to give myself more wiggle room on allocating packets and increase the stack sizes. I also prefer to change the IPv4 address to something more distinctive, but most of these parameters are flexible depending on the specific needs and constraints of your application:

18 constraints of your application.png

Since we intend to use IPv6 in the application, we need to enable it in the NetXDuo tab. Enabling this feature effects the code of creating a define which uses preprocessors to allow for compilation of all IPv6 related code. By using only IPv4, you decrease the memory footprint of the NetXDuo library:

15. activate netxduo core.png

Changing System Time Base to Accommodate ThreadX

When using an RTOS, the RTOS is configured by default to use the SysTick timer for managing the timing of its threads. Since the default configuration for the HAL libraries is to also use the SysTick for its timing operations, we need to change the timebase for the system. Allow only ThreadX alone to use the SysTick. In SYS, change the timebase source to TIM6.

20 timebase.png

Clock Configuration

The last thing we need to do is configure the clock. Before we go to the clock configuration tab, we must change the RCC settings. This Nucleo board sources a high accuracy reference 8MHz clock from the clock output of the STLINK, so we set the HSE to BYPASS clock source. When working with Ethernet applications, it is recommended to use some sort of stable and accurate, external high speed oscillator, typically a quartz oscillator as opposed to the internal high speed oscillator.

We also want to set the voltage scale to scale 1 to allow up to 400MHz core system clock frequency:

21 system clock frequency.png

Now, open the clock configuration tab. Set the HSE frequency to 8MHz, select HSE as the PLL source, and PLL output as the system clock source:

22 pll output system source.png

Then enter 400 into the box for the system clock speed, and it should automatically change the PLL settings to achieve 400MHz with your selected sources:

 

23. selected sources.png

Generate Code

That is it for the CubeMX configuration. Navigate to the project manager tab, give your project a name, and generate code for STM32CubeIDE:

24. stmcubeIDE.png

Adding Necessary Code in CubeIDE

Linker File

Before moving onto the application level code, we must add some code to the linker file. Because the Ethernet descriptors and NetX memory pool to be located in a specific memory address range, we need to make sure the linker reflects this. The default generated linker does not account for our specific configurations. First, open the linker file STM32H743ZITX_FLASH.ld

25. STM32H743ZITX_FLASH.ld.png

By default, the linker already has a definition for the RAM in which our descriptors and Ethernet receive buffers will live:

26. ethernet receive buffers will live.png

Now, we just need to add a new memory section somewhere in the SECTIONS brackets in the linker.

.tcp_sec (NOLOAD) : 
  {
	
    . = ABSOLUTE(0x30040000);
    *(.RxDecripSection)

    . = ABSOLUTE(0x30040060);
    *(.TxDecripSection)
    
    . = ABSOLUTE(0x30040200);
    *(.NetXPoolSection)
	
  } >RAM_D2 AT> FLASH

The section is placed in RAM_D2, but specifically places in the aforementioned SRAM3 that we intended. These sections are named specifically to work with code that generated by CubeMX that attempts to assign buffers to Linker sections using attributes, but the necessary code is not in the linker by default. Here are the relevant snippets:

27 1 relevant snippers.png

27 2 relevant snippers.png

Application Level Code

First, in main.c, lets add the code that allows us to use the printf function with our USART virtual com port. In main.c, user code begin 0 section:

/* USER CODE BEGIN 0 */

int __io_putchar(int ch)
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the USART1 and Loop until the end of transmission */
  HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 0xFFFF);

  return ch;
}

/* USER CODE END 0 */

Finally, we can add the code which actually creates the UDP socket and receives a message. Since NetXDuo is designed to work with ThreadX, the generated code has already set up a framework to create an entry task for your NetXDuo application. In main.c, CubeMX has already generated code that, calls a function to initialize ThreadX and NetXDuo. After execution starts, you will eventually end up in the file app_netxduo.c, the function MX_NetXDuo_Init. This is the function that initializes the IPv4 and creates the entry thread for the NetXDuo app. This file is where we add our relevant application code.

28 relevant application code.png

In this file, we will need to include main.h to allow us to use printf:

/* USER CODE BEGIN Includes */
#include "main.h"
/* USER CODE END Includes */

We also add a helper macro and helper function for us to print out our IPv4 and IPv6 addresses on the UART virtual com port:

/* USER CODE BEGIN PD */
#define PRINT_IP_ADDRESS(addr)           do { \
                                              printf("Device IPv4 Address: %lu.%lu.%lu.%lu \r\n", \
                                              (addr >> 24) & 0xff,                    \
                                              (addr >> 16) & 0xff,                    \
                                              (addr >> 8) & 0xff,                     \
                                              (addr & 0xff));                         \
                                         } while(0)
/* USER CODE END PD */
/* USER CODE BEGIN PFP */

void printIPv6(NXD_ADDRESS ipv6)
{
	printf("Device IPv6 Address: %x:%x:%x:%x:%x:%x:%x:%x\r\n",
			(unsigned int)ipv6.nxd_ip_address.v6[0] >> 16,
			(unsigned int)ipv6.nxd_ip_address.v6[0] & 0xFFFF,
			(unsigned int)ipv6.nxd_ip_address.v6[1] >> 16,
			(unsigned int)ipv6.nxd_ip_address.v6[1] & 0xFFFF,
			(unsigned int)ipv6.nxd_ip_address.v6[2] >> 16,
			(unsigned int)ipv6.nxd_ip_address.v6[2] & 0xFFFF,
			(unsigned int)ipv6.nxd_ip_address.v6[3] >> 16,
			(unsigned int)ipv6.nxd_ip_address.v6[3] & 0xFFFF
	);
}
/* USER CODE END PFP */

 Wewant to add some global variables for later use with IP address initialization. Then to get our UART handle from main.c so we can output UDP packets on the virtual COM port

/* USER CODE BEGIN PV */
NX_UDP_SOCKET UDPSocket;
ULONG IpAddress;
ULONG NetMask;
NXD_ADDRESS ipv6_address;

extern UART_HandleTypeDef huart3;
/* USER CODE END PV */

The function MX_NetXDuo_Init already initializes everything needed to create and use a UDP socket with IPv4 addressing. In addition we need to add some code to this function that enables and initializes the IPv6 addressing. At the bottom of the MX_NetXDuo_Init function, in USER CODE BEGIN MX_NetXDuo_Init:

 

/* USER CODE BEGIN MX_NetXDuo_Init */
  
  /* Assume ip has been created and IPv4 services (such as ARP,
       ICMP, have been enabled before doing IPv6 init */

    /* Enable IPv6 */
    ret = nxd_ipv6_enable(&NetXDuoEthIpInstance);

    if(ret != NX_SUCCESS)
    {
        /* nxd_ipv6_enable failed. */
    	Error_Handler();
    }

    /* Configure the IPv6 address */
    
    /* 
     * This IPv6 address is specific to my routers available IPv6 subnet addresses
     */
    ipv6_address.nxd_ip_version = NX_IP_VERSION_V6;
    ipv6_address.nxd_ip_address.v6[0] = 0x26001702;
    ipv6_address.nxd_ip_address.v6[1] = 0x4eb29780;
    ipv6_address.nxd_ip_address.v6[2] = 0x00000000;
    ipv6_address.nxd_ip_address.v6[3] = 0x0000ABCD;
    /* Configure global address of the primary interface. */
    ret = nxd_ipv6_address_set(&NetXDuoEthIpInstance, 0,
                                  &ipv6_address, 64, NX_NULL);

    /* Enable ICMPv6 */
    ret = nxd_icmp_enable(&NetXDuoEthIpInstance);
    if(ret != NX_SUCCESS)
    {
        /* nxd_icmp_enable failed. */
    	Error_Handler();
    }

  /* USER CODE END MX_NetXDuo_Init */

Before initializing IPv6, it is necessary to have the IPv4 address configured, and IPv4 ICMP and UDP enabled. In the above code, we are enabling IPv6 ICMP, which is not necessary for UDP communications. However, it is necessary if you want the board to respond to pings that use IPv6.

Lastly, we can add the code that actually starts the UDP port and receives the packets. At the bottom of the file, in nx_app_thread_entry, we add this code:

static VOID nx_app_thread_entry (ULONG thread_input)
{
  /* USER CODE BEGIN Nx_App_Thread_Entry 0 */
    UINT ret;
    ULONG bytes_read;

    ULONG    ipv6_prefix;
    UINT     ipv6_interface_index;

    UCHAR data_buffer[512];
    NX_PACKET *data_packet;

    NXD_ADDRESS device_address;

    /*
     * Print IPv4
     */
    ret = nx_ip_address_get(&NetXDuoEthIpInstance, &IpAddress, &NetMask);
    if (ret != TX_SUCCESS)
    {
    Error_Handler();
    }
    else
    {
        PRINT_IP_ADDRESS(IpAddress);
    }


    /*
     * Print IPv6
     */
    nxd_ipv6_address_get( &NetXDuoEthIpInstance, 0, &device_address, &ipv6_prefix, &ipv6_interface_index );
    printIPv6(device_address);


    /*
     * Create a UDP Socket and bind to it
     */

    ret = nx_udp_socket_create(&NetXDuoEthIpInstance, &UDPSocket, "UDP Server Socket", NX_IP_NORMAL, NX_FRAGMENT_OKAY, NX_IP_TIME_TO_LIVE, 512);
    if (ret != NX_SUCCESS)
    {
        Error_Handler();
    }

  /* Bind to port 6000 */
    ret = nx_udp_socket_bind(&UDPSocket, 6000, TX_WAIT_FOREVER);
    if (ret != NX_SUCCESS)
    {
        Error_Handler();
    }
    else
    {
        printf("UDP Server listening on PORT 6000.. \r\n");
    }

    /*
     * Main Task Loop
     * Waits 1 second (100 centiseconds) for each UDP packet. If received, print out message
     */
    while (1)
    {
        TX_MEMSET(data_buffer, '\0', sizeof(data_buffer));

        /* wait for data for 1 sec */
        ret = nx_udp_socket_receive(&UDPSocket, &data_packet, 100);

        if (ret == NX_SUCCESS)
        {
            nx_packet_data_retrieve(data_packet, data_buffer, &bytes_read);

        /* Print our received data on UART com port*/
              HAL_UART_Transmit(&huart3, (uint8_t *)data_buffer, bytes_read, 0xFFFF);
              printf("\r\n");    // new line to make print out more readable
        }

    }
  /* USER CODE END Nx_App_Thread_Entry 0 */

It prints out both preconfigured static IPv4 and IPv6 address on the COM port. In addition creates a UDP socket and binds it to port 6000. Finally, the main loop continuously waits for packets and prints them out on the COM port.

At this point, the project is ready to run. Connect the NUCLEO-H743 to your PC, and click the debug symbol in CubeIDE, keep debug configurations at default.

Testing the Application

Setting up Tera Term

With the board plugged in, Tera Term should recognize the STLINK virtual com port. Select it an OK:

29 com port.png

Now in setup->terminal, New-Line receive to AUTO and hit OK

30 auto term and hit ok.png

And setup->serial port, set the baud rate to 115200 and hit New Setting:

31 hit new setting.png

Send UDP messages to H743 with packet sender

For testing, I am using an application called packet sender. It supports sending UDP messages over both IPv4 and IPv6.
https://packetsender.com/

Before we can send any packets to the device, we need to connect the H743 board to a router. I have the H743 Ethernet board connected to one of my home network router's Ethernet ports. It is important that you have a router that support IPv6 if you want the IPv6 tests to work. It is also important that the addresses you have assigned to the H743 in software reflect valid subnet addresses for your router. You may need to open your router settings in a web browser for this information.

With the board hooked up to the router, plugged into a PC via USB, and Tera Term configured, if you reset the board you should see the IPv4 address, IPv6 address, and UDP port printed on Tera Term:

32 tera term.png

In the packet sender we can configure the UDP packet to reflect these parameters, send the packets over the network, and see the messages print out on the terminal:

33 1 print terminal.png

33 2 print terminal.png

33 3 print.png

 

Comments
sabari1
Associate III

if we are sending more than 1500 bytes(any value more than mentioned MTU) from the packet sender.is the nx_udp_socket_receive function will give the entire data as once or it will give fragmented data(means it will print the large chunk of data in tries).

 

Rajesh Tripathi
Associate III

Very well written article. Please write more article about using ThreadX middleware on STM32H7.

hakan23
Associate II

I agree, well written article
I only have one problem 

When sending more then 10 UDP messages from the Packet Sender, the STM32 application seems to crash ??

 

Any ideas ??

 

 

Mariano Abad
Associate III

Nice article, however after receiving 10 UDP packets it stops responding.

Version history
Last update:
‎2023-12-07 05:38 AM
Updated by: