tpgf 发表于 2025-3-13 13:45

STM32F4 UDP组播通信:填一填ST官方HAL库的坑

先说写作本文的原因,由于开项目开发中需要用到UDP组播接收的功能,但是ST官方没有提供合适的参考,使用STM32CubeMX生成的代码也是不能直接使用的,而我在网上找了一大圈,也没有一个能够直接解决的方案,deepseek、ChatGPT给的方案也不能直接用,羡慕迷茫阶段。然而随着项目进度告急,我也只能硬着头皮摸着石头过河了,总结了大多数资料的观点,加上deepseek的帮助,最终找到了关键点,因此希望写一个记录文档,同时也给大家参考,若有高手也可以指点指点。

开发环境
单片机型号是STM32F407VGT6,以太网控制器是LAN7820,软件版本如下

STM32CubeMX6.12.1
IAR 9.50.2
STM32Cube FW_F4 V1.28.1


使用STM32CubeMX生成代码
配置JTAG



配置以太网外设,选择RMII,打开以太网全局中断



打开串口1方便调试,添加DMA



启用LWIP
这里的关键是勾选右上角的Show Advance Parameters,然后使能LWIP_MULTICAST_TX_OPTONS (Multicast Tx support)



使能LWIP_IGMP (IGMP module)



PHY选择LAN8742,实际上使用的是LAN8720



设置PHY的复位引脚,默认输出为高



最后配置时钟



生成代码
关键代码
生成的代码是不能直接使用的,需要做一些关键修改,这里添加了组播初始化函数和回调函数

/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file         : main.c
* @brief          : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "lwip.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <string.h>
#include "lwip/udp.h"
#include "lwip/igmp.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
// 定义组播地址和端口
u16_t multicast_port=8554;
uint8_t multicast_ip={226,0,0,80};

void multicast_receive_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) {   
    if (p != NULL) {
      static uint8_t buff;
      memcpy(buff, p->payload, p->len);
      HAL_UART_Transmit_DMA(&huart1, (uint8_t *)&buff, p->len);
      pbuf_free(p);
    }
}

// 初始化MULTICAST接收协议控制块
void multicast_receive_init(void) {
    struct udp_pcb *pcb;
    ip4_addr_t ip;

    IP4_ADDR(&ip, multicast_ip, multicast_ip, multicast_ip, multicast_ip);
    igmp_joingroup(IP4_ADDR_ANY, &ip);
    pcb = udp_new();
    pcb->so_options |= SOF_REUSEADDR; // 允许地址重用
//    pcb->mcast_ttl   = 1;            // 组播TTL(默认1,限制在本地网络)
    if (pcb != NULL) {
      udp_bind(pcb, IP4_ADDR_ANY, multicast_port);
      udp_recv(pcb, multicast_receive_callback, NULL);
    }
}
/* USER CODE END 0 */

/**
* @briefThe application entry point.
* @retval int
*/
int main(void)
{

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

/* MCU Configuration--------------------------------------------------------*/

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Configure the system clock */
SystemClock_Config();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
MX_LWIP_Init();
/* USER CODE BEGIN 2 */
multicast_receive_init();
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
      MX_LWIP_Process();
      /* USER CODE END WHILE */

      /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}

/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
RCC_OscInitStruct.PLL.PLLM = 8;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
    Error_Handler();
}

/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
    Error_Handler();
}
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
* @briefPeriod elapsed callback in non blocking mode
* @NOTE   This function is calledwhen TIM1 interrupt took place, inside
* HAL_TIM_IRQHandler(). It makes a direct call to HAL_IncTick() to increment
* a global variable "uwTick" used as application time base.
* @paramhtim : TIM handle
* @retval None
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* USER CODE BEGIN Callback 0 */

/* USER CODE END Callback 0 */
if (htim->Instance == TIM1) {
    HAL_IncTick();
}
/* USER CODE BEGIN Callback 1 */

/* USER CODE END Callback 1 */
}

/**
* @briefThis function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}

#ifdefUSE_FULL_ASSERT
/**
* @briefReports the name of the source file and the source line number
*         where the assert_param error has occurred.
* @paramfile: pointer to the source file name
* @paramline: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
   ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */




另外一个关键的地方是需要在.\LWIP\Target\ethernetif.c文件中的low_level_init函数添加 强制接收所有组播包 以及 确保启用 IGMP 支持

/*******************************************************************************
                     LL Driver Interface ( LwIP stack --> ETH)
*******************************************************************************/
/**
* @brief In this function, the hardware should be initialized.
* Called from ethernetif_init().
*
* @param netif the already initialized lwip network interface structure
*      for this ethernetif
*/
static void low_level_init(struct netif *netif)
{
HAL_StatusTypeDef hal_eth_init_status = HAL_OK;
/* Start ETH HAL Init */

   uint8_t MACAddr ;
heth.Instance = ETH;
MACAddr = 0x00;
MACAddr = 0x80;
MACAddr = 0xE1;
MACAddr = 0x00;
MACAddr = 0x00;
MACAddr = 0x00;
heth.Init.MACAddr = &MACAddr;
heth.Init.MediaInterface = HAL_ETH_RMII_MODE;
heth.Init.TxDesc = DMATxDscrTab;
heth.Init.RxDesc = DMARxDscrTab;
heth.Init.RxBuffLen = 1536;

/* USER CODE BEGIN MACADDRESS */

/* USER CODE END MACADDRESS */

hal_eth_init_status = HAL_ETH_Init(&heth);

memset(&TxConfig, 0 , sizeof(ETH_TxPacketConfig));
TxConfig.Attributes = ETH_TX_PACKETS_FEATURES_CSUM | ETH_TX_PACKETS_FEATURES_CRCPAD;
TxConfig.ChecksumCtrl = ETH_CHECKSUM_IPHDR_PAYLOAD_INSERT_PHDR_CALC;
TxConfig.CRCPadCtrl = ETH_CRC_PAD_INSERT;

/* End ETH HAL Init */

/* Initialize the RX POOL */
LWIP_MEMPOOL_INIT(RX_POOL);

#if LWIP_ARP || LWIP_ETHERNET

/* set MAC hardware address length */
netif->hwaddr_len = ETH_HWADDR_LEN;

/* set MAC hardware address */
netif->hwaddr =heth.Init.MACAddr;
netif->hwaddr =heth.Init.MACAddr;
netif->hwaddr =heth.Init.MACAddr;
netif->hwaddr =heth.Init.MACAddr;
netif->hwaddr =heth.Init.MACAddr;
netif->hwaddr =heth.Init.MACAddr;

/* maximum transfer unit */
netif->mtu = ETH_MAX_PAYLOAD;

/* Accept broadcast address and ARP traffic */
/* don't set NETIF_FLAG_ETHARP if this device is not an ethernet one */
#if LWIP_ARP
    netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP;
#else
    netif->flags |= NETIF_FLAG_BROADCAST;
#endif /* LWIP_ARP */

/* USER CODE BEGIN PHY_PRE_CONFIG */

/* USER CODE END PHY_PRE_CONFIG */
/* Set PHY IO functions */
LAN8742_RegisterBusIO(&LAN8742, &LAN8742_IOCtx);

/* Initialize the LAN8742 ETH PHY */
if(LAN8742_Init(&LAN8742) != LAN8742_STATUS_OK)
{
    netif_set_link_down(netif);
    netif_set_down(netif);
    return;
}

if (hal_eth_init_status == HAL_OK)
{
/* Get link state */
ethernet_link_check_state(netif);
}
else
{
    Error_Handler();
}
#endif /* LWIP_ARP || LWIP_ETHERNET */

/* USER CODE BEGIN LOW_LEVEL_INIT */

// --- 核心修复代码:直接操作寄存器 ---
ETH->MACFFR |= ETH_MACFFR_PAM;// 强制接收所有组播包
netif->flags |= NETIF_FLAG_IGMP;// 确保启用 IGMP 支持

/* USER CODE END LOW_LEVEL_INIT */
}



上位机软件
为了配合测试,这里用python写了一个简单的上位机测试软件

import socket
import struct

MCAST_GROUP = '226.0.0.80'
MCAST_PORT = 8554
LOCAL_IP = '192.168.88.2'# 替换为你的实际 IPv4 地址

# 创建 UDP 套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', MCAST_PORT))

# 通过 IP 地址绑定到指定接口(Windows 兼容方案)
sock.setsockopt(
    socket.IPPROTO_IP,
    socket.IP_ADD_MEMBERSHIP,
    socket.inet_aton(MCAST_GROUP) + socket.inet_aton(LOCAL_IP)
)

print(f"监听组播 {MCAST_GROUP}:{MCAST_PORT}...")
while True:
    data, addr = sock.recvfrom(1024)
    print(f"来自 {addr} 的消息: {data.decode()}")




# sender.py
import socket

MCAST_GROUP = '226.0.0.80'
MCAST_PORT = 8554
INTERFACE_IP = '192.168.88.2'# <--- 替换为你的实际 IPv4 地址

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(INTERFACE_IP))

message = "Multicast Test"
sock.sendto(message.encode(), (MCAST_GROUP, MCAST_PORT))
print("消息已发送")
sock.close()


实测效果
可以看到,使用脚本发送数据可以上位机软件成功接收。同时单片机收到数据之后通过串口发送到了串口助手,实验成功。



参考代码
https://github.com/dwgan/STM32-Multicast
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_39432978/article/details/146109101

页: [1]
查看完整版本: STM32F4 UDP组播通信:填一填ST官方HAL库的坑