STM32H7的ETH HAL库封装得比较好,真正做到了完全用HAL API函数操作,不需要再用寄存器操作。
而STM32F1的ETH HAL库就没有完全封装,有些操作必须要用寄存器完成,而且还需要单独改动HAL库结构体里面的变量。
(关于STM32F1的ETH HAL库用法,请参阅:https://blog.csdn.net/ZLK1214/article/details/105457370)
HAL库版本:STM32Cube_FW_H7_V1.9.0
Keil5工程下载地址:百度网盘 请输入提取码(提取码:p3xd)
开发板:
【电路连线】
晶振采用的是XTAL-3225封装的50MHz有源晶振。
引脚名称 | 单片机I/O口 |
---|---|
ETH_REF_CLK | PA1 |
ETH_MDIO | PA2 |
ETH_CRS_DV | PA7 |
ETH_TX_EN | PG11 |
ETH_TXD0 | PB12 |
ETH_TXD1 | PB13 |
ETH_MDC | PC1 |
ETH_RXD0 | PC4 |
ETH_RXD1 | PC5 |
DP83848_RST(复位) | PE4(普通I/O口) |
DP83848_INT(中断) | PE5(普通I/O口) |
【代码讲解】
程序里面使用的lwip2.1.3除了下面几个文件是修改过的以外,其余的都是官网的原始文件:
修改的文件:ethernetif.c(修改前的原始文件位于contrib-2.1.0.zip)
添加的文件:arch/cc.h lwipopts.h
(lwip 2.0.3版本中的ethernetif.c文件位于lwip-2.0.3.zip压缩包的src/netif文件夹下。而lwip 2.1.0~2.1.3版本中的ethernetif.c文件则被移动到了contrib-2.1.0.zip压缩包的examples/ethernetif文件夹里面了, 里面有一些细微的修改)
系统时钟的配置是在clock_init函数里面完成的:
/* 配置系统时钟 */
void clock_init(void)
{
HAL_StatusTypeDef status;
RCC_ClkInitTypeDef clk = {0};
RCC_OscInitTypeDef osc = {0};
HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY);
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0);
while (__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY) == RESET);
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_ON;
osc.PLL.PLLFRACN = 0;
osc.PLL.PLLM = 5;
osc.PLL.PLLN = 192;
osc.PLL.PLLP = 2;
osc.PLL.PLLQ = 2;
osc.PLL.PLLR = 2;
osc.PLL.PLLRGE = RCC_PLL1VCIRANGE_2;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
status = HAL_RCC_OscConfig(&osc);
if (status != HAL_OK)
abort();
clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 | RCC_CLOCKTYPE_D1PCLK1 | RCC_CLOCKTYPE_D3PCLK1;
clk.SYSCLKDivider = RCC_SYSCLK_DIV1;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_HCLK_DIV2;
clk.APB1CLKDivider = RCC_APB1_DIV2;
clk.APB2CLKDivider = RCC_APB2_DIV2;
clk.APB3CLKDivider = RCC_APB3_DIV2;
clk.APB4CLKDivider = RCC_APB4_DIV2;
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4);
}
该函数将系统时钟配置为480MHz:AHB时钟为240MHz,APB1~4时钟为120MHz。
初始化DP83848的函数是DP83848_Init,该函数是在netif_add添加网卡时调用的,调用关系如下:
main -> net_config -> netif_add(或netif_add_noaddr) -> ethernetif_init -> low_level_init -> DP83848_Init
其中,low_level_init函数的代码如下:
/**
* 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)
{
//struct ethernetif *ethernetif = netif->state;
/* set MAC hardware address length */
netif->hwaddr_len = ETHARP_HWADDR_LEN;
/* set MAC hardware address */
DP83848_Init();
DP83848_GetMACAddress(netif->hwaddr);
printf("MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n", netif->hwaddr[0], netif->hwaddr[1], netif->hwaddr[2], netif->hwaddr[3], netif->hwaddr[4], netif->hwaddr[5]);
/* maximum transfer unit */
netif->mtu = 1500;
/* device capabilities */
/* don't set NETIF_FLAG_ETHARP if this device is not an ethernet one */
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_MLD6; // 这里MLD6是启用IPv6多播
// 不设置NETIF_FLAG_LINK_UP这个标志, 开机时认为网络未接通
#if LWIP_IPV6 && LWIP_IPV6_MLD
/*
* For hardware/netifs that implement MAC filtering.
* All-nodes link-local is handled by default, so we must let the hardware know
* to allow multicast packets in.
* Should set mld_mac_filter previously. */
if (netif->mld_mac_filter != NULL) {
ip6_addr_t ip6_allnodes_ll;
ip6_addr_set_allnodes_linklocal(&ip6_allnodes_ll);
netif->mld_mac_filter(netif, &ip6_allnodes_ll, NETIF_ADD_MAC_FILTER);
}
#endif /* LWIP_IPV6 && LWIP_IPV6_MLD */
/* Do whatever else is needed to initialize interface. */
}
在这个函数里面,调用了DP83848_Init初始化网口,接着调用DP83848_GetMACAddress将网卡地址(在程序中由STM32单片机的器件ID生成)告诉lwip。netif->flags不设置NETIF_FLAG_LINK_UP标志,告诉lwip网卡现在没有网。NETIF_FLAG_MLD6表示启用IPv6多播功能,IPv6的运行依赖于多播,不打开多播的话IPv6是不能正常工作的。
接下来看看DP83848_Init函数:
void DP83848_Init(void)
{
int i;
uint32_t uid;
ETH_MACFilterConfigTypeDef macfilter;
GPIO_InitTypeDef gpio;
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
// PA1: ETH_REF_CLK, PA2: ETH_MDIO, PA7: ETH_CRS_DV
gpio.Alternate = GPIO_AF11_ETH;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pin = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &gpio);
// PB12~13: ETH_TXD0~1
gpio.Pin = GPIO_PIN_12 | GPIO_PIN_13;
HAL_GPIO_Init(GPIOB, &gpio);
// PC1: ETH_MDC, PC4~5: ETH_RXD0~1
gpio.Pin = GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5;
HAL_GPIO_Init(GPIOC, &gpio);
// PE4: DP83848_RST
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pin = GPIO_PIN_4;
HAL_GPIO_Init(GPIOE, &gpio);
// PE5: DP83848_INT
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pin = GPIO_PIN_5;
HAL_GPIO_Init(GPIOE, &gpio);
// PG11: ETH_TX_EN
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pin = GPIO_PIN_11;
HAL_GPIO_Init(GPIOG, &gpio);
HAL_Delay(100); // 延长复位时间, 以免通电后检测不到中断
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_4, GPIO_PIN_SET); // 撤销DP83848复位信号
// 根据器件ID生成MAC地址
uid = HAL_GetUIDw0() + HAL_GetUIDw1() + HAL_GetUIDw2();
memcpy(dp83848_mac + 3, &uid, 3);
// 初始化ETH
__HAL_RCC_ETH1MAC_CLK_ENABLE();
__HAL_RCC_ETH1RX_CLK_ENABLE();
__HAL_RCC_ETH1TX_CLK_ENABLE();
heth.Instance = ETH;
heth.Init.MACAddr = dp83848_mac;
heth.Init.MediaInterface = HAL_ETH_RMII_MODE;
heth.Init.RxBuffLen = ETH_MAX_PACKET_SIZE;
heth.Init.RxDesc = dp83848_rx;
heth.Init.TxDesc = dp83848_tx;
HAL_ETH_Init(&heth);
HAL_ETH_WritePHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_MICR, PHY_MICR_INT_OE | PHY_MICR_INT_EN); // 打开中断输出
HAL_ETH_WritePHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_MISR, PHY_MISR_LINK_INT_EN); // 打开链路状态中断
HAL_ETH_GetMACFilterConfig(&heth, &macfilter);
macfilter.BroadcastFilter = ENABLE; // 要使用IPv4, 必须要能接收广播帧
macfilter.PassAllMulticast = ENABLE; // 要使用IPv6, 必须要能接收多播帧
HAL_ETH_SetMACFilterConfig(&heth, &macfilter);
for (i = 0; i < ETH_RX_DESC_CNT; i++)
HAL_ETH_DescAssignMemory(&heth, i, dp83848_rxbuf[i], NULL);
}
在这个函数中,首先配置好ETH的GPIO引脚。电路板上DP83848默认是处于复位状态,PE4拉高后撤销复位,DP83848进入正常工作状态。
请注意要先延时,再撤销复位信号,以免开发板通电瞬间,DP83848复位时间太短,不能产生网线插拔中断信号,ETH以为没有插网线而不工作。
DP83848本身没有MAC地址(网卡地址),所以需要我们自己生成一个MAC地址,同时告诉STM32 ETH和lwip协议栈。这里的生成方式是,前三位固定为00:80:E1,后三位由STM32的3个32位器件ID值(HAL_GetUIDw0~2)相加生成。特别注意的是,网卡的MAC地址必须为单播地址(第一个字节为偶数),绝对不可以设置一个多播MAC地址(第一个字节为奇数),否则在很多路由器上不能正常通信!
接着调用HAL_ETH_Init函数初始化ETH,设置好MAC地址,选择MII或RMII接口,设置接收描述符和发送描述符。调用HAL_ETH_WritePHYRegister函数写DP83848的寄存器,打开中断输出,插拔网线时能产生外部中断。
HAL_ETH_SetMACFilterConfig函数配置MAC,开启多播和广播。必须要打开广播才能ping通IPv4地址,必须要打开多播才能ping通IPv6地址。
最后用HAL_ETH_DescAssignMemory函数将dp83848_rx接收描述符和dp83848_rxbuf接收缓冲区绑定,网卡收到的数据都是保存在这些缓冲区里面的。
到这里为止,lwip就初始化完成了。由net_config配置网卡的IPv4和IPv6地址。
static void net_config(int use_dhcp)
{
ip4_addr_t ipaddr, netmask, gw;
if (use_dhcp)
netif_add_noaddr(&netif_dp83848, NULL, ethernetif_init, netif_input);
else
{
IP4_ADDR(&ipaddr, 192, 168, 137, 20);
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 137, 1);
netif_add(&netif_dp83848, &ipaddr, &netmask, &gw, NULL, ethernetif_init, netif_input);
}
netif_set_default(&netif_dp83848);
netif_set_up(&netif_dp83848);
if (use_dhcp)
dhcp_start(&netif_dp83848);
netif_create_ip6_linklocal_address(&netif_dp83848, 1);
printf("IPv6 link-local address: %s\n", ipaddr_ntoa(netif_ip_addr6(&netif_dp83848, 0)));
netif_set_ip6_autoconfig_enabled((struct netif *)(uintptr_t)&netif_dp83848, 1);
}
当参数use_dhcp为0时,为固定IP地址、子网掩码和网关,当use_dhcp为1时,则通过DHCP获取。这里只需要执行dhcp_start就不用管了,插拔网线后,只要调用了netif_set_link_up/down函数,那么lwip自己会调用dhcp_network_changed函数重新通过DHCP获取地址。另外,判断DHCP是否获取到IP地址的函数是dhcp_supplied_address函数。
netif_create_ip6_linklocal_address函数创建网卡的IPv6本地链路地址(即fe80开头的地址),netif_set_ip6_autoconfig_enabled是通过SLAAC协议获取IPv6地址。这里进行(struct netif *)(uintptr_t)强制类型转换,仅仅是为了避免Keil编译器的警告,没有实际作用。
这里再强调一下,网卡必须要启用广播,IPv4才能正常工作。必须要启用多播,IPv6才能正常工作。
我们之前在ethernetif.c的low_level_init函数中去掉了netif->flags的NETIF_FLAG_LINK_UP选项,所以网卡默认状态是未连接状态。
netif_add添加网卡后,只要netif_set_up启用了网卡,就可以马上调用dhcp_start启动DHCP服务器,不需要等到连上网络后netif_set_link_up再启动DHCP。
此外,netif_set_link_up函数内部会调用dhcp_network_changed,进而调用dhcp_reboot,所以网络断开后再连接,DHCP会自动重新获取IP地址,不需要自己再去调用dhcp_start。
需要注意的是,dhcp_start只是启动DHCP服务器,函数返回时,IP地址还没有获取到。在裸机环境下,用dhcp_supplied_address判断是否获取到IP地址,应该在main函数的while(1)主循环里面进行。dhcp_start后马上用dhcp_supplied_address判断,判断结果肯定是没有获取到。
接下来看下main函数的主循环:
while (1)
{
if (DP83848_GetITStatus())
{
HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_MISR, &status);
printf("DP83848 interrupt occurred! status=%#x\n", status);
if (status & PHY_LINK_INTERRUPT)
{
// 这里必须要多读几次
HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_BSR, &value);
HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_BSR, &value);
if (value & PHY_LINKED_STATUS)
{
if (!netif_is_link_up(&netif_dp83848))
{
printf("Link is up!\n");
DP83848_Start();
netif_set_link_up(&netif_dp83848);
}
}
else
{
printf("Link is down!\n");
DP83848_Stop();
netif_set_link_down(&netif_dp83848);
}
}
}
while (HAL_ETH_IsRxDataAvailable(&heth))
ethernetif_input(&netif_dp83848);
display_ip();
sys_check_timeouts();
}
主循环中,if (DP83848_GetITStatus())判断是否产生了DP83848中断,如果有中断就处理中断。
HAL_ETH_IsRxDataAvailable函数判断是否收到了网卡数据,如果收到了网卡数据,则交给lwip处理。
display_ip是我们自己实现的IP地址显示函数,当DHCP获取到IPv4地址后,以及SLAAC获取到IPv6地址后,将地址通过串口打印出来。显示IP地址的代码如下:
// 显示DHCP分配的IP地址
static void display_ip(void)
{
const ip_addr_t *addr;
static uint8_t ip_displayed = 0;
static uint8_t ip6_displayed = 0;
int i, dns = 0;
if (dhcp_supplied_address(&netif_dp83848))
{
if (ip_displayed == 0)
{
ip_displayed = 1;
printf("DHCP supplied address!\n");
printf("IP address: %s\n", ipaddr_ntoa(&netif_dp83848.ip_addr));
printf("Subnet mask: %s\n", ipaddr_ntoa(&netif_dp83848.netmask));
printf("Default gateway: %s\n", ipaddr_ntoa(&netif_dp83848.gw));
dns = 1;
}
}
else
ip_displayed = 0;
for (i = 1; i < LWIP_IPV6_NUM_ADDRESSES; i++) // 0号地址是本地链路地址, 不需要显示
{
if (ip6_addr_isvalid(netif_ip6_addr_state(&netif_dp83848, i)))
{
if ((ip6_displayed & _BV(i)) == 0)
{
ip6_displayed |= _BV(i);
printf("IPv6 address %d: %s\n", i, ipaddr_ntoa(netif_ip_addr6(&netif_dp83848, i)));
dns = 1;
}
}
else
ip6_displayed &= ~_BV(i);
}
// 显示DNS服务器地址
// 在lwip中, IPv4 DHCP和IPv6 SLAAC获取到的DNS地址会互相覆盖
if (dns)
{
addr = dns_getserver(0);
if (ip_addr_isany(addr))
return;
printf("DNS Server: %s", ipaddr_ntoa(addr));
addr = dns_getserver(1);
if (!ip_addr_isany(addr))
printf(" %s", ipaddr_ntoa(addr));
printf("\n");
}
}
sys_check_timeouts函数是lwip内部的定时处理,必须在主循环中调用这个函数。
DP83848_GetITStatus()根据PE5是否为低电平来判断是否产生了中断,如果产生了就处理中断。读取PHY_MISR寄存器看看产生了哪些中断。如果板子上没有将DP83848的INT中断引脚接到单片机上,那么可以通过一直轮询PHY_MISR寄存器是否不为0,来判断是否产生了中断。当PHY_MISR寄存器的PHY_LINK_INTERRUPT位为1时,表明产生了网线插拔中断。读取PHY_BSR寄存器两次判断是插了网线还是拔了网线。必须要读取两次,读取一次是不行的。如果PHY_BSR寄存器的PHY_LINKED_STATUS位为1,则说明是插了网线,此时调用DP83848_Start打开ETH收发功能,然后netif_set_link_up告诉lwip现在网卡有网了。为了防止DP83848_Start函数重复调用,这里加了一个netif_is_link_up判断。
DP83848_Start函数的实现如下:
void DP83848_Start(void)
{
uint32_t value;
ETH_MACConfigTypeDef macconf;
// 只要打开了自动协商功能, 插上网线后就能自动开始协商, 不需要软件写寄存器来触发
do
{
HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_BSR, &value);
} while ((value & PHY_AUTONEGO_COMPLETE) == 0);
// 读取自动协商结果, 并写入STM32的ETH寄存器
HAL_ETH_ReadPHYRegister(&heth, DP83848_PHY_ADDRESS, PHY_SR, &value);
HAL_ETH_GetMACConfig(&heth, &macconf);
if (value & PHY_DUPLEX_STATUS)
macconf.DuplexMode = ETH_FULLDUPLEX_MODE;
else
macconf.DuplexMode = ETH_HALFDUPLEX_MODE;
if (value & PHY_SPEED_STATUS)
macconf.Speed = ETH_SPEED_10M;
else
macconf.Speed = ETH_SPEED_100M;
HAL_ETH_SetMACConfig(&heth, &macconf);
HAL_ETH_Start(&heth);
printf("ETH is started!\n");
}
插上网线后,DP83848会进行自动协商。这里要做的就是等待自动协商完毕,读取PHY_SR寄存器获取网络参数,然后将网络参数通过HAL_ETH_SetMACConfig告诉STM32,HAL_ETH_Start使能收发就行了。
网线拔掉时,只需要调用HAL_ETH_Stop函数,然后调用netif_set_link_down函数告诉lwip现在网卡没网了就行。
void DP83848_Stop(void)
{
HAL_ETH_Stop(&heth);
printf("ETH is stopped!\n");
}
最后,我们来看一下数据包的收发。底层处理数据包发送的函数是low_level_output函数,处理数据包接收的函数是low_level_input函数,这是移植lwip时最重要的两个函数。
首先看数据包的发送:
static err_t
low_level_output(struct netif *netif, struct pbuf *p)
{
err_t err = ERR_OK;
//struct ethernetif *ethernetif = netif->state;
struct pbuf *q;
int i = 0;
ETH_BufferTypeDef data[ETH_TX_DESC_CNT];
ETH_TxPacketConfig config = {0};
HAL_StatusTypeDef status;
//initiate transfer();
#if ETH_PAD_SIZE
pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif
if (p->tot_len > ETH_MAX_PACKET_SIZE || !netif_is_link_up(netif))
{
err = ERR_IF;
goto end;
}
for (q = p; q != NULL; q = q->next) {
/* Send the data from the pbuf to the interface, one pbuf at a
time. The size of the data in each pbuf is kept in the ->len
variable. */
//send data from(q->payload, q->len);
if (i >= ETH_TX_DESC_CNT)
{
err = ERR_IF;
break;
}
data[i].buffer = q->payload;
data[i].len = q->len;
if (q->next == NULL)
data[i].next = NULL;
else
data[i].next = &data[i + 1];
i++;
}
//signal that packet should be sent();
config.Attributes = ETH_TX_PACKETS_FEATURES_CSUM | ETH_TX_PACKETS_FEATURES_CRCPAD;
config.ChecksumCtrl = ETH_CHECKSUM_DISABLE; // 由软件计算网络层和传输层的校验和
config.CRCPadCtrl = ETH_CRC_PAD_INSERT; // 填充短帧, 并由硬件计算数据链路层校验和
config.Length = p->tot_len;
config.TxBuffer = data;
status = HAL_ETH_Transmit(&heth, &config, HAL_MAX_DELAY);
if (status != HAL_OK)
err = ERR_IF;
MIB2_STATS_NETIF_ADD(netif, ifoutoctets, p->tot_len);
if (((u8_t *)p->payload)[0] & 1) {
/* broadcast or multicast packet*/
MIB2_STATS_NETIF_INC(netif, ifoutnucastpkts);
} else {
/* unicast packet */
MIB2_STATS_NETIF_INC(netif, ifoutucastpkts);
}
/* increase ifoutdiscards or ifouterrors on error */
end:
printf("[Send] len=%u", p->tot_len);
if (err == ERR_OK)
printf("\n");
else
printf(" (failed)\n");
#if ETH_PAD_SIZE
pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif
if (err == ERR_OK)
LINK_STATS_INC(link.xmit);
else
MIB2_STATS_NETIF_INC(netif, ifouterrors);
return err;
}
发送前先做判断,如果发送的数据量大于缓冲区的大小(p->tot_len > ETH_TX_BUF_SIZE),或者网口没网(!netif_is_link_up(netif)),就返回错误。struct pbuf是要发送的数据,是一个链表,用for循环转换为ETH_BufferTypeDef形式的链表,再调用HAL_ETH_Transmit函数将数据包发送出去,发送的长度为p->tot_len。
再看看数据包的接收:
最开始main函数的主循环中,检测到有数据包后,调用ethernetif_input函数,这个函数又跳转到了low_level_input里面去了。
static struct pbuf *
low_level_input(struct netif *netif)
{
//struct ethernetif *ethernetif = netif->state;
struct pbuf *p, *q;
int i = 0;
u32_t len; // 这里len必须定义为32位整形
ETH_BufferTypeDef data = {0};
/* Obtain the size of the packet and put it into the "len"
variable. */
HAL_ETH_GetRxDataBuffer(&heth, &data);
HAL_ETH_GetRxDataLength(&heth, &len);
printf("[Recv] len=%u\n", len);
LWIP_ASSERT("buf.len == len", data.len == len);
#if ETH_PAD_SIZE
len += ETH_PAD_SIZE; /* allow room for Ethernet padding */
#endif
/* We allocate a pbuf chain of pbufs from the pool. */
p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
if (p != NULL) {
#if ETH_PAD_SIZE
pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif
/* We iterate over the pbuf chain until we have read the entire
* packet into the pbuf. */
for (q = p; q != NULL; q = q->next) {
/* Read enough bytes to fill this pbuf in the chain. The
* available data in the pbuf is given by the q->len
* variable.
* This does not necessarily have to be a memcpy, you can also preallocate
* pbufs for a DMA-enabled MAC and after receiving truncate it to the
* actually received size. In this case, ensure the tot_len member of the
* pbuf is the sum of the chained pbuf len members.
*/
//read data into(q->payload, q->len);
memcpy(q->payload, data.buffer + i, q->len);
i += q->len;
}
//acknowledge that packet has been read();
MIB2_STATS_NETIF_ADD(netif, ifinoctets, p->tot_len);
if (((u8_t *)p->payload)[0] & 1) {
/* broadcast or multicast packet*/
MIB2_STATS_NETIF_INC(netif, ifinnucastpkts);
} else {
/* unicast packet*/
MIB2_STATS_NETIF_INC(netif, ifinucastpkts);
}
#if ETH_PAD_SIZE
pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif
LINK_STATS_INC(link.recv);
} else {
//drop packet();
LINK_STATS_INC(link.memerr);
LINK_STATS_INC(link.drop);
MIB2_STATS_NETIF_INC(netif, ifindiscards);
}
// 确认收到数据
HAL_ETH_BuildRxDescriptors(&heth);
if (__HAL_ETH_DMA_GET_FLAG(&heth, ETH_DMA_RX_BUFFER_UNAVAILABLE_FLAG) != RESET)
{
printf("Receive buffer unavailable!\n");
__HAL_ETH_DMA_CLEAR_FLAG(&heth, ETH_DMA_RX_BUFFER_UNAVAILABLE_FLAG);
}
return p;
}
此函数先调用HAL_ETH_GetRxDataBuffer函数获取数据包的内容,再调用HAL_ETH_GetRxDataLength函数获取数据包的大小。然后用pbuf_alloc分配pbuf内存,把收到的数据包的内容复制到pbuf内存里面,复制完了之后要释放接收缓冲区,调用HAL_ETH_BuildRxDescriptors函数将该数据包占用的缓冲区还给DMA。最后,判断ETH_DMA_RX_BUFFER_UNAVAILABLE_FLAG标志位,检查之前是否因缓冲区满了。在H7的HAL库中,接收缓冲区满了不需要做额外处理(在F1里面还需要做额外处理)。
【程序运行结果】
开机时如果插了网线,串口的输出如下:
STM32H743ZI DP83848
SystemCoreClock=480000000
MAC address: 00:80:E1:61:84:93
IPv6 link-local address: FE80::280:E1FF:FE61:8493
DP83848 interrupt occurred! status=0x2c20
Link is up!
ETH is started!
[Send] len=350
[Send] len=86
[Send] len=78
[Send] len=86
[Send] len=86
[Send] len=350
[Recv] len=52
[Send] len=70
[Recv] len=52
[Send] len=350
[Recv] len=60
[Recv] len=52
[Send] len=70
[Recv] len=142
[Send] len=86
[Recv] len=86
[Recv] len=60
[Send] len=78
[Recv] len=52
[Send] len=86
IPv6 address 1: 2409:8A62:321:6D60:280:E1FF:FE61:8493
DNS Server: FE80::1
[Recv] len=590
[Send] len=350
[Recv] len=590
[Send] len=42
[Send] len=42
[Send] len=42
[Recv] len=52
[Send] len=42
DHCP supplied address!
IP address: 192.168.1.107
Subnet mask: 255.255.255.0
Default gateway: 192.168.1.1
DNS Server: 192.168.1.1
[Send] len=42
[Send] len=42
[Recv] len=86
[Send] len=86
[Recv] len=60
[Recv] len=52
[Send] len=42
[Send] len=42
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Send] len=86
[Recv] len=78
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Recv] len=52
[Recv] len=342
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Recv] len=52
[Recv] len=52
[Recv] len=52
[Recv] len=60
[Recv] len=52
从中可以看到ETH硬件会自动去除收到的数据包的填充部分,从而收到的数据包的长度len小于60字节。若想要像STM32F1那样不去除填充字节,可在DP83848_Start函数的HAL_ETH_GetMACConfig(&heth, &macconf)下方加一句macconf.AutomaticPadCRCStrip = DISABLE。
电脑可以ping通板子的IPv4地址和IPv6地址,以及NetBIOS计算机名:
电脑可以通过IPv4和IPv6地址,以及NetBIOS计算机名,访问板子上的网页服务器:
在路由器管理页面中也能看到STM32设备:
设备名STM32H743ZI_ETH是由ethernetif.c的ethernetif_init函数里面的netif->hostname变量指定的。