cancel
Showing results for 
Search instead for 
Did you mean: 

How to create a bare metal HAL Ethernet application on STM32H563/STM32H723

STea
ST Employee

Summary

To understand how middleware like NetXDuo and LwIP use the STM32 HAL Ethernet driver, this article demonstrates a bare metal application to send and receive Ethernet frames.

Prerequisites 

Hardware

Software

  • Terminal emulation software (Putty, Tera Term) 

Objectives 

This application guides STM32Cube HAL API users to: 

  • Configure the Ethernet peripheral pinout.  
  • Configure the Ethernet transmit (Tx) and receive (Rx) descriptors. 
  • Construct Ethernet frames to be sent and received upon triggering an external interrupt. 

Step-by-step instructions

1. Board configuration using CubeMX

1.1 Board initialization

The first step is to open STM32CubeMX and search for one of the boards we mentioned in the prerequisites section. In our case, we use the STM32H563ZI.

ST800_0-1719223045923.png

You are prompted to decide whether to activate TrustZone.

ST800_1-1719223045924.png

Select [Without TrustZone activated] as we are not TrustZone for this example.

Before starting the Ethernet configuration, it is recommended to clear all the default-mapped pins. This can be done by following the steps outlined below:

ST800_2-1719223045926.png

1.2 UART configuration

To simplify the debugging process, we use UART to print out debug messages to a serial terminal. To do this, we need to configure the appropriate UART that is connected to the on-board STLINK virtual COM port (VCP). By inspecting the STM32H563ZI board schematic and searching for VCP, we find that USART3 is connected to the VCP by default.

ST800_3-1719223045931.png

We enable USART3 in asynchronous mode and retain the default configuration. The only modification required is the pin assignment to ensure that UART is mapped to the virtual COM ports. This means we need to assign USART3_RX to PD9 and USART3_TX to PD8.

 ST800_4-1719223045932.png

1.3 GPIO configuration

Next, we configure the GPIO for the user button to act as an external interrupt, initiating data transmission/reception upon activation. According to UM3115, the user button is mapped to PC13. Configure this in STM32CubeMX as follows:

ST800_5-1719223045933.png

1.4 Ethernet peripheral configuration

For Ethernet configuration, begin by enabling the peripheral in RMII mode, which is supported by our board and uses fewer pins (7 pins) compared to MII mode (16 pins). As with the USART3 configuration, ensure that the pins are correctly mapped to the MCU. Adhere to the pin configuration as detailed in the board schematic:

ST800_6-1719223045938.png

Next, increase the GPIO pins' maximum output speed to [Very High] in the GPIO Settings. To do this quickly, select the first pin, hold Shift, and then select the last pin to highlight all pins in between. Now, scroll down and set the maximum output speed to [Very High] as shown below:

ST800_7-1719223045939.png

Finally, since we are using Ethernet in interrupt mode for this example, we need to enable the Ethernet global interrupt in the NVIC Settings.

ST800_8-1719223045940.png

1.5 NVIC configuration

As previously mentioned, this example operates in interrupt mode, so we must configure the NVIC by enabling the EXTI Line 13 interrupt, which corresponds to the user button.

ST800_1-1719321481334.png

1.6 Clock configuration

For clock configuration, start by adjusting the RCC mode to select Crystal/Ceramic Resonator as the High Speed Clock. In the Clock Configuration tab, use HSI as the PLL1 Source Mux. Use PLLCLK as the System Clock Mux to achieve a frequency of 250MHz, as illustrated in the clock tree.

ST800_10-1719223045943.png

ST800_11-1719223045945.png

1.7 Linker Settings

Ethernet frames can be up to 1524 bytes in size, or up to 9000 bytes with jumbo frames enabled. Therefore, it is essential to increase the heap size to accommodate 4 Rx Buffers. The significance of this configuration is explained later in the guide.

You may ask yourself, why 4 Rx Buffers? In the Ethernet Configuration tab of STM32CubeMX, we find the following:

ST800_12-1719223045946.png

To calculate the necessary heap space, multiply the Rx Buffer Length by the Number of Rx Descriptors, as each buffer is associated with a descriptor:

Required Heap Space = 1524 bytes per buffer * 4 buffers = 6096 bytes

This equals 0x17D0 in hexadecimal. We round it up to 0x2000 to allow for any additional requirements.

Note: In our case, we test with simple messages so we use small buffers.

ST800_13-1719223045946.png

All that remains is to choose a name for the project and click [Generate Code]. You will receive a warning about not enabling ICACHE. Select [Yes] to proceed with code generation.

2. Application programming in STM32CubeIDE

2.1 Configure USART3

As mentioned, we'll use USART3 to output debug messages through the ST-Link Virtual COM Port (VCP). To simplify the process, we'll redirect the printf function to HAL_UART_Transmit. Implement this by adding the following code snippets to the appropriate sections of our application:

 

/* USER CODE BEGIN PD */
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
/* USER CODE END PD */
/* USER CODE BEGIN 0 */
int __io_putchar(int ch)
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the USART3 and Loop until the end of transmission */
  HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 0xFFFF);
  return ch;
}
/* USER CODE END 0 */

 

Don’t forget to include <stdio.h> since we are going to use printf.

2.2 Configuring PHY

Before configuring Ethernet descriptors, we must set up the physical interface. This involves importing the PHY library into our project and preparing the configuration for the initialization function. To import the source and header files:

  • Drag lan8742.h into the Inc/ folder.
  • Drag lan8742.c into the Src/ folder.

These files are typically located at:

C:\Users\%USERNAME%\STM32Cube\Repository\STM32Cube_FW_H5_V1.1.1\Drivers\BSP\Components\lan8742

If done correctly, your folder structure should now include:

ST800_14-1719223045948.png

Once the files are in the correct folders, you can include lan8742.h in your project as follows:

ST800_15-1719223045949.png

For PHY initialization, declare a struct and assign private IO functions to it, enabling the following tasks:

  • Initialize the MDIO clock, corresponding to the signal used for sending and receiving data to and from PHY registers.
  • Deinitialize the PHY.
  • Read the contents of a specific register.
  • Write data to a specific register.
  • Get the current tick value.

Declare the struct and write the function prototypes in the appropriate area as follows:

 

/* USER CODE BEGIN PFP */
int32_t ETH_PHY_INTERFACE_Init(void);
int32_t ETH_PHY_INTERFACE_DeInit (void);
int32_t ETH_PHY_INTERFACE_ReadReg(uint32_t DevAddr, uint32_t RegAddr, uint32_t *pRegVal);
int32_t ETH_PHY_INTERFACE_WriteReg(uint32_t DevAddr, uint32_t RegAddr, uint32_t RegVal);
int32_t ETH_PHY_INTERFACE_GetTick(void);
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
lan8742_Object_t LAN8742;
lan8742_IOCtx_t  LAN8742_IOCtx = {ETH_PHY_INTERFACE_Init,
                                  ETH_PHY_INTERFACE_DeInit,
                                  ETH_PHY_INTERFACE_WriteReg,
                                  ETH_PHY_INTERFACE_ReadReg,
                                  ETH_PHY_INTERFACE_GetTick};

 

Now, implement the previously declared functions so they can be utilized when called. Copy the following implementations into your main file:

 

/* USER CODE BEGIN 4 */
int32_t ETH_PHY_INTERFACE_Init(void)
{
  /* Configure the MDIO Clock */
  HAL_ETH_SetMDIOClockRange(&heth);
  return 0;
}
int32_t ETH_PHY_INTERFACE_DeInit (void)
{
  return 0;
}
int32_t ETH_PHY_INTERFACE_ReadReg(uint32_t DevAddr, uint32_t RegAddr, uint32_t *pRegVal)
{
  if(HAL_ETH_ReadPHYRegister(&heth, DevAddr, RegAddr, pRegVal) != HAL_OK)
  {
    return -1;
  }
  return 0;
}
int32_t ETH_PHY_INTERFACE_WriteReg(uint32_t DevAddr, uint32_t RegAddr, uint32_t RegVal)
{
  if(HAL_ETH_WritePHYRegister(&heth, DevAddr, RegAddr, RegVal) != HAL_OK)
  {
    return -1;
  }
  return 0;
}
int32_t ETH_PHY_INTERFACE_GetTick(void)
{
  return HAL_GetTick();
}
/* USER CODE END 4 */

 

The last step in the PHY groundwork is to create a function for handling speed and duplex autonegotiation. This function will:

  • Check if the PHY is properly initialized by reading the Link state.
  • Assign the corresponding speed and duplex settings to the variables used to adjust the MAC configuration.
  • Call the Ethernet start function in interrupt mode to initiate transmission/reception at the MAC and DMA layers.

Copy the code for this function below the PHY initialization functions within the /* USER CODE BEGIN 4 */ section, and do not forget to declare the function prototype above.

 

void ETH_StartLink()
{
  ETH_MACConfigTypeDef MACConf = {0};
  int32_t PHYLinkState = 0U;
  uint32_t linkchanged = 0U, speed = 0U, duplex =0U;
  PHYLinkState = LAN8742_GetLinkState(&LAN8742);
  if(PHYLinkState <= LAN8742_STATUS_LINK_DOWN)
  {
    HAL_ETH_Stop(&heth);
  }
  else if(PHYLinkState > LAN8742_STATUS_LINK_DOWN)
  {
    switch (PHYLinkState)
    {
    case LAN8742_STATUS_100MBITS_FULLDUPLEX:
      duplex = ETH_FULLDUPLEX_MODE;
      speed = ETH_SPEED_100M;
      linkchanged = 1;
      break;
    case LAN8742_STATUS_100MBITS_HALFDUPLEX:
      duplex = ETH_HALFDUPLEX_MODE;
      speed = ETH_SPEED_100M;
      linkchanged = 1;
      break;
    case LAN8742_STATUS_10MBITS_FULLDUPLEX:
      duplex = ETH_FULLDUPLEX_MODE;
      speed = ETH_SPEED_10M;
      linkchanged = 1;
      break;
    case LAN8742_STATUS_10MBITS_HALFDUPLEX:
      duplex = ETH_HALFDUPLEX_MODE;
      speed = ETH_SPEED_10M;
      linkchanged = 1;
      break;
    default:
      break;
    }
    if(linkchanged)
    {
      HAL_ETH_GetMACConfig(&heth, &MACConf);
      MACConf.DuplexMode = duplex;
      MACConf.Speed = speed;
      MACConf.DropTCPIPChecksumErrorPacket = DISABLE;
      MACConf.ForwardRxUndersizedGoodPacket = ENABLE;
      HAL_ETH_SetMACConfig(&heth, &MACConf);
      HAL_ETH_Start_IT(&heth);  
      }
  }
}

 

Note that CRC checking is disabled in this example, as CRC calculation during transmission from the PC is not included. You are welcome to implement your own function and re-enable this feature in the MAC configuration. Additionally, for testing purposes with short payloads (size < 64 bytes), the ForwardRxUndersizedGoodPacket option should be enabled.

2.3 Configuring Ethernet

2.3.1 Buffers memory allocation

When HAL_ETH_START_IT() is called, it begins by constructing descriptors. It populates the previously zero-initialized descriptors with values corresponding to user-selected features, such as VLAN, TSO, PTP, etc. To build the receive descriptors, memory blocks must be allocated for each descriptor, necessitating the definition of the HAL_ETH_RxAllocateCallback function.

 

void HAL_ETH_RxAllocateCallback(uint8_t ** buff) {
  ETH_BufferTypeDef * p = malloc(100);
  if (p)
  {
    * buff = (uint8_t * ) p + offsetof(ETH_AppBuff, buffer);
    p -> next = NULL;
    p -> len = 100;
  } else {
    * buff = NULL;
  }
}

 

The HAL_ETH_RxAllocateCallback function begins by calling malloc (ensure to include <stdlib.h>) to allocate a block of 100 bytes. The buffer address is then aligned with the buffer address member in the ETH_AppBuff structure. This structure facilitates the organization of each frame and its contents in a forward-linked list. The struct is declared as follows:

 

/* USER CODE BEGIN PTD */
typedef struct {
    ETH_BufferTypeDef *AppBuff;
    uint8_t buffer[100]__ALIGNED(32);
} ETH_AppBuff;
/* USER CODE END PTD */

 

2.3.2 Frame organization

Since we are working with forward-linked lists, we need to define a function that constructs this chain of nodes, organizing it to ensure flawless frame reconstruction. In this case, we have HAL_ETH_RxLinkCallback(). The implementation is as follows: 

 

void HAL_ETH_RxLinkCallback(void ** pStart, void ** pEnd, uint8_t * buff, uint16_t Length)
{
  ETH_BufferTypeDef ** ppStart = (ETH_BufferTypeDef ** ) pStart;
  ETH_BufferTypeDef ** ppEnd = (ETH_BufferTypeDef ** ) pEnd;
  ETH_BufferTypeDef * p = NULL;
  p = (ETH_BufferTypeDef * )(buff - offsetof(ETH_AppBuff, buffer));
  p -> next = NULL;
  p -> len = 100;
  if (! * ppStart)
  {
    * ppStart = p;
  } else
  {
    ( * ppEnd) -> next = p;
  }
  * ppEnd = p;
}

 

2.3.3 initialization sequence

Now that we have prepared all the necessary functions to operate the Ethernet on the STM32H5, all that remains is to call these functions in MX_ETH_Init() as shown below:

 

  /* USER CODE BEGIN ETH_Init 2 */
  /* Set PHY IO functions */
  LAN8742_RegisterBusIO(&LAN8742, &LAN8742_IOCtx);
  /* Initialize the LAN8742 ETH PHY */
  LAN8742_Init(&LAN8742);
  /* Initialize link speed negotiation and start Ethernet peripheral */
  ETH_StartLink();
  /* USER CODE END ETH_Init 2 */

 

We can also define callback functions for HAL_ETH_TxCpltCallback and HAL_ETH_RxCpltCallback to be executed upon successful transmission or reception, respectively. 

 

void HAL_ETH_TxCpltCallback(ETH_HandleTypeDef * heth)
{
  printf("Packet Transmitted successfully!\r\n");
  fflush(0);
}
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef * heth)
{
  printf("Packet Received successfully!\r\n");
  fflush(0);
}

 

If all goes well, you should get no errors or warnings when you build the project.

ST800_16-1719223045950.png

3. Testing the application

To test our application, we construct a ping pong communication between an STM32H723 board and STM32H563 board. When one of the boards transmits a frame, the other board receives the data and sends a response. This process repeats itself until interrupted by the user.

4.1 Transmit configuration

We are going to construct a basic Ethernet frame, prepare the Tx Descriptor, and send it to another host. To do this, we start by creating a function that constructs an Ethernet frame from given arguments. A simple implementation might look like the following:

 

void ETH_ConstructEthernetFrame(ethernet_frame_t * frame, uint8_t * dest_mac, uint8_t * src_mac, uint8_t * type, uint8_t * payload, uint16_t payload_len)
{
  // Copy the destination MAC address
  memcpy(frame -> dest_mac, dest_mac, 6);
  // Copy the source MAC address
  memcpy(frame -> src_mac, src_mac, 6);
  // Set the Ethernet type field
  memcpy(frame -> type, type, 2);
  // Copy the payload data
  memcpy(frame -> payload, payload, payload_len);
}

 

To facilitate data placement in Ethernet frame fields, we create a struct that comprises these fields. It looks like the following:

 

typedef struct {
    uint8_t dest_mac[6];
    uint8_t src_mac[6];
    uint8_t type[2];
    uint8_t payload[100];
} ethernet_frame_t;

 

Next, we can construct our frame by calling the function we defined earlier with values of our choosing. Here, I have chosen to send a frame with a custom payload that I have encoded in hexadecimal and chose the STM32H723 board MAC address as the destination address.

 

 /* USER CODE BEGIN 1 */
 ETH_BufferTypeDef TxBuffer;
 ethernet_frame_t frame;
 uint8_t dest_mac[] = {0x00, 0x80, 0xE1, 0x00, 0x00, 0x10}; // Destination MAC Address
 uint8_t src_mac[] = {0x00, 0x80, 0xE1, 0x00, 0x00, 0x00};  // Source MAC Address
 uint8_t type[] = {0x08,0x00 }; // EtherType set to IPV4 packet
 uint8_t payload[] = {0x54,0x65,0x73,0x74,0x69,0x6e,0x67,0x20,0x45,0x74,0x68,0x65,0x72,0x6e,0x65,0x74,0x20,0x6f,0x6e,0x20,0x53,0x54,0x4d,0x33,0x32};
 uint16_t payload_len = sizeof(payload);
 /* USER CODE END 1 */

 

Now, we can call the frame construction function we just defined and prepare a transmit buffer, as illustrated in the example below.

 

/* USER CODE BEGIN 2 */
ETH_ConstructEthernetFrame(&frame, dest_mac, src_mac, type, payload, payload_len);
TxBuffer.buffer = (uint8_t *)&frame;
TxBuffer.len = sizeof(dest_mac) + sizeof(src_mac) + sizeof(type) + payload_len;
TxBuffer.next = NULL;
TxConfig.TxBuffer = &TxBuffer;
/* USER CODE END 2 */

 

Moving on, we utilize the user button configured in CubeMX to transmit a frame upon detecting a rising edge external interrupt. Therefore, we must define the HAL_GPIO_EXTI_Callback() function in the user section to be executed when the button is pressed.

 

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) 
{
  if (GPIO_Pin == GPIO_PIN_13) {
    HAL_ETH_Transmit_IT( & heth, & TxConfig);
    HAL_ETH_ReleaseTxPacket( & heth);
    HAL_ETH_ReadData( & heth, (void ** ) & frame_Rx);
  } else {
    __NOP();
  }
}

 

4.2 Callback configuration

To maintain the continuous exchange in the ping-pong communication protocol, it is essential to establish the appropriate callback functions corresponding to each event: Transmit complete and receive complete. In each of these functions, we perform a simple action: toggling an LED. Specifically, within the receive complete callback (denoted as RxCplt), we not only toggle an LED, but also initiate a new transmission upon the successful reception of data. This ensures that for every reception of a frame, another frame is transmitted in response, perpetuating the communication cycle. The following code snippet should be implemented identically on both boards to achieve the desired ping-pong effect:

 

void HAL_ETH_TxCpltCallback(ETH_HandleTypeDef * heth)
{
  printf("[%d] Packet Transmitted to H5!\r\n", ++inc);
  fflush(0);
  HAL_GPIO_WritePin(Led_Red_GPIO_Port, Led_Red_Pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(Led_Green_GPIO_Port, Led_Green_Pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(Led_Orange_GPIO_Port, Led_Orange_Pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(Led_Green_GPIO_Port, Led_Green_Pin, GPIO_PIN_SET);
}
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef * heth)
{
  HAL_ETH_ReadData(heth, (void ** ) & frame_Rx);
  printf("[%d] Packet Received from H5!\r\n", ++inc);
  fflush(0);
  HAL_ETH_Transmit_IT(heth, & TxConfig);
  HAL_ETH_ReleaseTxPacket(heth);
  HAL_GPIO_WritePin(Led_Red_GPIO_Port, Led_Red_Pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(Led_Green_GPIO_Port, Led_Green_Pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(Led_Orange_GPIO_Port, Led_Orange_Pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(Led_Orange_GPIO_Port, Led_Orange_Pin, GPIO_PIN_SET);
}

 

4.3 Demonstration

Finally, all that is left to do is to build the project and test it. To do that, we need to open a terminal emulator and connect to the serial COM port at the correct speed. After running our project and initializing all the necessary peripherals, we can attempt to send an Ethernet frame to the board. If successful, this triggers the callback function, which starts the ping pong communication between the two boards.

ST800_17-1719223045958.png

PS:  This example does not handle memory management of received packets. It serves only as a mean to explain the programming process of ethernet to ease the setup when using middlewares

Conclusion 

In this article, we tried to explain the necessary steps needed to implement a simple ethernet application that sends and receives frames and debug common problems. This example familiarizes the user with the Ethernet driver provided by ST and used by middleware.

References

Version history
Last update:
‎2024-07-22 02:00 AM
Updated by:
Contributors