基于HAL库实现ETH以太网

F4xx 系列控制器内部集成了一个以太网外设,它实际是一个通过 DMA 控制器进行介质访问控制(MAC),它的功能就是实现 MAC 层的任务。 借助以太网外设,STM32F4xx 控制器可以通过 ETH 外设按照 IEEE 802.3-2002 标准发送和接收 MAC 数据包。在该外设的数据收发中数据发送时,先将数据有存储器以 DMA 传输到发送 TX FIFO 进行缓冲,然后由 MAC 内核发送; 接收数据时,RX FIFO 先接收以太网数据帧,再由 DMA 传输至存储器。

Lwip协议支持TCP/IP协议且比较轻量所以在我们使用以太网外设进行通信时常常使用该协议。当然对于IP地址的分配,DHCP协议可以帮助我们来进行动态的IP地址分配,所以我们也会使用到。

接下来我们就在以太网接口上移植Lwip协议,并使用DHCP来动态分配IP地址,CubeMX配置如下:

可见在CubeMX中有Lwip协议的配置,这也方便了我们对Lwip协议的移植使用。我们还需要配置一个GPIO口作为ETH复位引脚,如下所示:

程序即详解如下:

cpp 复制代码
while (1)
  {
		MX_LWIP_Process();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		dhcp = netif_dhcp_data(&gnetif);
		printf("DHCP IP address: %s\n", ip4addr_ntoa(&dhcp->offered_ip_addr));
		printf("DHCP Subnet mask: %s\n", ip4addr_ntoa(&dhcp->offered_sn_mask));
		printf("DHCP Default gateway: %s\n", ip4addr_ntoa(&dhcp->offered_gw_addr));
		HAL_Delay(1000);
  }

在main函数循环中调用MX_LWIP_Process();该函数是Lwip协议栈的主要处理函数,用以处理网络数据包的收发,获取协议栈的各种状态,处理DHCP协议等我们将其在循环中调用用来时刻检查状态和处理数据。然后调用data函数获取网络客户端的数据,但是前面的函数得到的值是二进制的数据,所以我们要使用ip4addr_ntoa函数将其转化成标准的ip格式,便于我们查看。

cpp 复制代码
static void low_level_init(struct netif *netif)
{
  HAL_StatusTypeDef hal_eth_init_status = HAL_OK;
  /* Start ETH HAL Init */

   uint8_t MACAddr[6] ;
  heth.Instance = ETH;
  MACAddr[0] = 0x00;
  MACAddr[1] = 0x80;
  MACAddr[2] = 0xE1;
  MACAddr[3] = 0x00;
  MACAddr[4] = 0x00;
  MACAddr[5] = 0x00;
  heth.Init.MACAddr = &MACAddr[0];
  heth.Init.MediaInterface = HAL_ETH_RMII_MODE;
  heth.Init.TxDesc = DMATxDscrTab;
  heth.Init.RxDesc = DMARxDscrTab;
  heth.Init.RxBuffLen = 1536;

  /* USER CODE BEGIN MACADDRESS */
	// 复位网卡的代码
	HAL_GPIO_WritePin(ETH_NRST_GPIO_Port,ETH_NRST_Pin,GPIO_PIN_RESET);
	HAL_Delay(55);
	HAL_GPIO_WritePin(ETH_NRST_GPIO_Port,ETH_NRST_Pin,GPIO_PIN_SET);
	HAL_Delay(55);
  /* USER CODE END MACADDRESS */

在初始化函数中要对我们配置的复位引脚进行操作来对网卡进行复位,复位后网卡才能正常启动。

当然我们也可以不使用HDCP协议来动态获取IP可以通过自己的设置,设置静态IP来实现TCP服务器。下面我们来看CubeMX配置:

程序及详解如下:

cpp 复制代码
tcp_server_start(8000);
	printf("Static IP address: %s\n", ip4addr_ntoa(&gnetif.ip_addr));
	printf("Subnet mask: %s\n", ip4addr_ntoa(&gnetif.netmask));
	printf("Default gateway: %s\n", ip4addr_ntoa(&gnetif.gw));
	
  /* USER CODE END 2 */

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

    /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */
}

首先我们调用tcp_server_start(8000)启动tcp服务器8000是本地端口,该函数具体内容如下:

cpp 复制代码
int tcp_server_start(uint16_t port)
{
    int ret = 0;
    struct tcp_pcb *pcb = NULL;
    err_t err = ERR_OK;
    /* create new TCP PCB structure */
    pcb = tcp_new();
    if (!pcb)
    {
        debug("Error creating PCB. Out of Memory\n\r");
        ret = -1;
        goto __exit;
    }
    /* bind to specified @port */
    err = tcp_bind(pcb, IP_ADDR_ANY, port);
    if (err != ERR_OK)
    {
        debug("Unable to bind to port %d: err = %d\n\r", port, err);
        ret = -2;
        goto __exit;
    }
    /* listen for connections */
    pcb = tcp_listen(pcb);
    if (!pcb)
    {
        debug("Out of memory while tcp_listen\n\r");
        ret = -3;
    }
    /* specify callback to use for incoming connections */
    tcp_accept(pcb, accept_callback);
    /* create success */
    debug("TCP echo server started @ port %d\n\r", port);
    return ret;
__exit:
    if (pcb)
        memp_free(MEMP_TCP_PCB, pcb);
    return ret;
}

首先调用CubeMX给我们生成的Lwip协议中的创建tcp控制块的函数,若创建成功则用pcb来接收该地址,即pcb指针指向该tcp控制块。然后将tcp链接到端口,然后调用函数对该端口进行监听然后设置链接接收回调,成功则返回正确的ret错误则将pcb空间释放,然后返回对应错误的ret。然后跳出这个函数后,同样将一些首先将其二进制形式转化,然后通过串口输入方便我们调试观察。

然后进入循环同样循环调用Lwip协议栈处理的主要函数,而且必须对该函数进行周期性的调用否则链接很容易断开。然后调用我们自定义的数据处理函数来对相应的数据进行处理,具体函数内容如下:

cpp 复制代码
int transfer_data(void)
{
    uint32_t nsend = 0;
    if (tcppcb != NULL && tcppcb->state == ESTABLISHED) /* 连接有效 */
    {
        if (TCP_RX_STA & 0x8000) /* 有数据收到 */
        {
            nsend = user_senddata(TCP_RX_BUF, TCP_RX_STA & 0x7FFF); /* 将接收到的数据发回去 */
            TCP_RX_STA = 0;
            debug("\r\n send %d bytes success.", nsend);
        }
    }
    return 0;
}

首先判断tcp控制块不为空,且其结构体中的状态变量为连接有效,若判断到有数据收到我们就可以将其用函数将该数据发送出去,并且使用debug输出一些信息。在发送函数中我的封装如下:

cpp 复制代码
static uint32_t tcp_server_send(struct tcp_pcb *tpcb, const void *buf, uint32_t len)
{
    uint32_t nwrite = 0, total = 0;
    const uint8_t *p = (const uint8_t *)buf;
    err_t err = ERR_OK;
    if (!tpcb)
        return 0;
    while ((err == ERR_OK) && (len != 0) && (tcp_sndbuf(tpcb) > 0))
    {
        nwrite = tcp_sndbuf(tpcb) >= len ? len : tcp_sndbuf(tpcb);
        err = tcp_write(tpcb, p, nwrite, 1);
        if (err == ERR_OK)
        {
            len -= nwrite;
            total += nwrite;
            p += nwrite;
        }
        tcp_output(tpcb);
    }
    return total;
}
相关推荐
凯子坚持 c33 分钟前
Doubao-Seed-Code模型深度剖析:Agentic Coding在Obsidian插件开发中的应用实践
网络·人工智能
元气满满-樱37 分钟前
思科:路由条目优化实验
网络·智能路由器
tan180°42 分钟前
Linux网络IP(下)(16)
linux·网络·后端·tcp/ip
视觉震撼1 小时前
本地机器远程连接配置与文件传输可行性检测工具
运维·服务器·网络·windows·php·apache
逼子格2 小时前
硬件工程师成长之路——知识汇总(持续更新)
嵌入式硬件·proteus·硬件工程·ad·keil·电路仿真·硬件工程师面试
abcefg_h2 小时前
TCP与UDP的区别
网络·tcp/ip·udp
qqssss121dfd2 小时前
计算机网络(第8版,谢希仁)第一章习题解答
网络·计算机网络
飞凌嵌入式2 小时前
飞凌嵌入式RK3568开发板的TFTP烧写文件系统指南
linux·嵌入式硬件·嵌入式
lingzhilab9 小时前
零知IDE——基于STM32F103RBT6与RFID-RC522的校园餐卡系统实现
stm32·单片机·嵌入式硬件