当前位置: 首页 > article >正文

STM32网络通讯之LWIP下载移植项目设计(十六)

STM32F407 系列文章 - ETH-LWIP-Transplant(十六)


目录

前言

一、软件设计

二、下载移植实现

1.lwip下载

1.下载方式一

2.下载方式二

2.lwip文件介绍

3.lwip移植

4.添加PHY驱动代码

5.修改ethernetif文件

6.添加用户代码

7.main文件实现

8.效果演示

总结


前言

一般对于许多嵌入式系统或单片机,在其资源受限的环境下,要想实现网络通讯,并保证资源的高效利用和稳定的网络通信,我们一般采用一种轻量级的网络协议lwIP。TI公司的STM32芯片一般都会自带一路以太网口,用于网络通讯,但因其内存资源受限,所以都用采用一种小型化、轻量级的lwIP网络协议,只需十几KB的RAM和大约40K的ROM即可运行,既可以在无操作系统环境下工作,也可以与各种操作系统配合使用,使其成为资源受限的嵌入式系统的理想选择。一般市场上所卖的板子都带这一功能的,需准备STM32F407开发板一块和网线一根。


一、软件设计

前面博文STM32之LWIP网络通讯设计介绍(十四)-CSDN博客讲述了对STM32实现LWIP网络通讯的前提性要求介绍,包含使用到的网络协议、MAC内核、PHY驱动芯片、通讯连接示意图、以及硬件电路原理设计图,为网络通讯软件开发提供了设计指导。STM32网络通讯的软件设计,在上一篇文章STM32网络通讯之CubeMX实现LWIP项目设计(十五)-CSDN博客,采用的是通过可视化工具STM32CubeMX完成对lwIP通讯的配置,一键化生成工程代码。今天将采用另一种实现方法,即先下载lwIP,然后完成lwIP移植到STM32工程项目中,在完成其通讯配置,这种实现方式较前面的有一定的困难,对熟悉lwip通讯有一定的要求,如果对lwip不是足够了解,博主推荐采用前篇文章实现。

二、下载移植实现

通过下载、移植lwIP到STM32工程项目中,实现网络通讯。

1.lwip下载

点击打开LWIP官网lwIP - 轻量级 TCP/IP 堆栈,显示界面如下所示。在上面既可以下载所需版本lwip,也可以获得相应技术支持。

1.下载方式一

在上面找到下载区,点击进入后,选择我们需要下载的版本,如下所示。这里为了与前面的CubeMX工具上lwip版本保持一致,也选择lwip 2.12版本。

点击上面的版本进行下载时,有时无法响应,这时我们可以采取git方式下,即方式二下载。

2.下载方式二

选择上面的git存储区,点击“lwIP-轻量级 TCPIP堆栈”(lwip.git - lwIP - 轻量级 TCPIP 堆栈),选择对应的版本进行下载。在该选项下面还有一个“lwIP Contrib - 为轻量级 TCP/IP 堆栈提供的代码”选项(contrib.git - lwIP Contrib - 用户例程代码)即官方Contrib提供的demo例程代码,这里选择lwip-contrib-STABLE-2_1_0_RELEASE.tar.gz进行下载(后面需要用到里面的文件)。

 也可以直接点击下载网址进行下载,提供对应版本下载地址如下:

https://git.savannah.nongnu.org/cgit/lwip.git/snapshot/lwip-STABLE-2_1_2_RELEASE.tar.gzicon-default.png?t=O83Ahttps://git.savannah.nongnu.org/cgit/lwip.git/snapshot/lwip-STABLE-2_1_2_RELEASE.tar.gz下载完后,可以看到如下大小的压缩包。

2.lwip文件介绍

对上面下载的压缩包进行解压,得到如下画面。

上图中的lwip2.1.2文件夹包含了许多文件和子文件夹,关于里面的文件我们不需要关心,主要是记录lwIP源码更新、开源软件license、描述lwIP的特点、介绍lwIP源码包的文件目录信息等等,无关紧要。另外还有三个文件夹doc、src、test,其中doc文件夹里面是关于LwIP的一些文档,可以看成是应用和移植LwIP的指南;test文件夹里面是测试LwIP内核性能的源码,将它们和LwIP源码加入到工程中一起编译,调用它们提供的函数,可以获得许多与LwIP内核性能有关的指标;src文件夹是lwIP源码包中最重要的,它是lwIP的内核文件,也是我们移植到工程中的重要文件。

打开src文件夹,如下所示,主要讲解下面5个文件夹。

api文件夹里面装的是NETCONN API和Socket API相关的源文件,只有在操作系统的环境中,才 能被编译。apps文件夹里面装的是应用程序的源文件,包括常见的应用程序,如httpd、mqtt、tftp、sntp、snmp等。core文件夹里面是LwIP的内核源文件,后续会详细讲解。include文件夹里面是LwIP所有模块对应的头文件。netif文件夹里面是与网卡移植有关的文件,这些文件为我们移植网卡提供了模板,我们可以直接 使用。

LwIP内核是由一系列模块组合而成的,这些模块包括:TCP/IP协议栈的各种协议、内存管理模 块、数据包管理模块、网卡管理模块、网卡接口模块、基础功能类模块、API模块。每个模块是由相关的几个源文件和头文件组成的,通过头文件对外声明一些函数、宏、数据类型,使得其它模块可以方便地调用此模块的功能。而构成每个模块的头文件都被组织在了include目录中,而源文件则根据类型被分散地组织在 api、apps、core、netif目录中。下面的子级文件在不作介绍,具体可以查看LwIP的官方说明文档。

3.lwip移植

打开我们stm32项目工程文件,如下。如果没有Middlewares文件夹,则创建它,然后在 Middlewares文件夹下创建了一个名为“lwip”的子文件夹。在“lwip”文件夹下,我们又创建了两个子文件夹:arch和lwip_app。arch文件夹用于存放lwIP系统的配置文件;而lwip_app文件夹则用于存放用户自定义的文件,例如应用程序的源代码等。最后将上述的lwip-STABLE-2_1_2_RELEASE文件夹里面的src文件夹拷贝到lwip文件夹里面。

​ 

打开stm32工程,并添加 Middlewares/lwip/src、Middlewares/lwip/lwip_app 和 Middlewares/lwip/arch这3个分组,如下所示。

​  

在Middlewares/lwip/src分组添加src/api 路径下、和src/core路径下的除ipv6文件夹的全部.c文件,另添加src/netif路径下的ethernet.c文件,如下图所示。

​​

而arch文件夹主要存放lwipopts.h、cc.h、ethernetif.c/h这四个文件都可以在“lwip-contrib-STABLE-2_1_0_RELEASE”文件包下获取。然后再Middlewares/lwip/arch分组添加arch文件夹下的.c文件,显示如下。

添加/移植完后,在keil上显示如下文件。

4.添加PHY驱动代码

这里添加的PHY驱动文件代码,就相当于上一篇博文CubeMX生成项目工程下的lwip.c/h和ethernetif.c/h文件的代码,因为CubeMX界面上配置好的PHY驱动参数,生成为lwip.c/h和ethernetif.c/h文件,lwip官方代码是不带驱动代码的,因其不知道你用哪款驱动芯片。添加PHY驱动代码实现步骤如下:

在工程项目路径Drivers\BSP文件夹中,创建“ETHERNET”文件夹,如下所示。

然后在“ETHERNET”文件夹下新建ethernet.c和ethernet.h这两个文件,并将这两个文件添加到项目工程中Drivers/BSP分组下,如下所示。

​  

在这两个文件需要完成以太网驱动初始化和MAC的驱动程序,源代码如下示例。

#include "./BSP/ETHERNET/EthDriver.h"

ETH_HandleTypeDef g_eth_handler;    /* 以太网句柄 */
LWIP_DEV_INFO g_lwipdev;            /* lwip控制结构体 */
struct netif g_netif;               /* 定义一个全局的网络接口 */

void lwip_dhcp_process(struct netif *netif);
/**
 * @breif       lwip 设备信息默认设置
 * @param       lwipx  : lwip控制结构体指针
 * @retval      无
 */
uint8_t lwip_dev_info_set(LWIP_DEV_INFO *lwipx)
{
    /* 默认远端IP为:192.168.1.113 */
    lwipx->remoteip[0] = 192;
    lwipx->remoteip[1] = 168;
    lwipx->remoteip[2] = 1;
    lwipx->remoteip[3] = 113;
    /* MAC地址设置 */
    lwipx->mac[0] = 0xB8;
    lwipx->mac[1] = 0xAE;
    lwipx->mac[2] = 0x1D;
    lwipx->mac[3] = 0x00;
    lwipx->mac[4] = 0x01;
    lwipx->mac[5] = 0x00;
    /* 默认本地IP为:192.168.1.201 */
    lwipx->localip[0] = 192;
    lwipx->localip[1] = 168;
    lwipx->localip[2] = 1;
    lwipx->localip[3] = 201;
    /* 默认子网掩码:255.255.255.0 */
    lwipx->netmask[0] = 255;
    lwipx->netmask[1] = 255;
    lwipx->netmask[2] = 255;
    lwipx->netmask[3] = 0;
    /* 默认网关:192.168.1.1 */
    lwipx->gateway[0] = 192;
    lwipx->gateway[1] = 168;
    lwipx->gateway[2] = 1;
    lwipx->gateway[3] = 1;
		/* 默认远端主机端口:8888 */
		lwipx->remoteport = UDP_PORT;
		/* 默认本机主机端口:8888 */
		lwipx->localport = UDP_PORT;
    lwipx->dhcpstatus = 0; /* 没有DHCP */
		return 0;
}
/**
 * @brief       以太网芯片初始化
 * @param       无
 * @retval      0,成功
 *              1,失败
 */
uint8_t ethernet_init(void)
{
    uint8_t macaddress[6];
    macaddress[0] = g_lwipdev.mac[0];
    macaddress[1] = g_lwipdev.mac[1];
    macaddress[2] = g_lwipdev.mac[2];
    macaddress[3] = g_lwipdev.mac[3];
    macaddress[4] = g_lwipdev.mac[4];
    macaddress[5] = g_lwipdev.mac[5];
    g_eth_handler.Instance = ETH;
    g_eth_handler.Init.AutoNegotiation = ETH_AUTONEGOTIATION_ENABLE;    /* 使能自协商模式 */
    g_eth_handler.Init.Speed = ETH_SPEED_100M;                          /* 速度100M,如果开启了自协商模式,此配置就无效 */
    g_eth_handler.Init.DuplexMode = ETH_MODE_FULLDUPLEX;                /* 全双工模式,如果开启了自协商模式,此配置就无效 */
    g_eth_handler.Init.PhyAddress = ETHERNET_PHY_ADDRESS;               /* 以太网芯片的地址 */
    g_eth_handler.Init.MACAddr = macaddress;                            /* MAC地址 */
    g_eth_handler.Init.RxMode = ETH_RXINTERRUPT_MODE;                   /* 中断接收模式 */
    g_eth_handler.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;         /* 硬件帧校验 */
    g_eth_handler.Init.MediaInterface = ETH_MEDIA_INTERFACE_RMII;       /* RMII接口 */
    if (HAL_ETH_Init(&g_eth_handler) == HAL_OK)
        return 0;   /* 成功 */
    else
        return 1;  /* 失败 */
}
/**
 * @brief       ETH底层驱动,时钟使能,引脚配置
 *    @note     此函数为虚函数被重写 此函数会被HAL_ETH_Init()调用
 * @param       heth:以太网句柄
 * @retval      无
 */
void HAL_ETH_MspInit(ETH_HandleTypeDef *heth)
{
    GPIO_InitTypeDef gpio_init_struct;
    __HAL_RCC_ETH_CLK_ENABLE();       /* 开启ETH时钟 */
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOG_CLK_ENABLE();
    /* 网络引脚设置 RMII接口
     * ETH_MDIO -------------------------> PA2
     * ETH_MDC --------------------------> PC1
     * ETH_RMII_REF_CLK------------------> PA1
     * ETH_RMII_CRS_DV ------------------> PA7
     * ETH_RMII_RXD0 --------------------> PC4
     * ETH_RMII_RXD1 --------------------> PC5
     * ETH_RMII_TX_EN -------------------> PG11
     * ETH_RMII_TXD0 --------------------> PG13
     * ETH_RMII_TXD1 --------------------> PG14
     * ETH_RESET-------------------------> PD3
     */
    /* PA1,2,7 */
    gpio_init_struct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_7;
    gpio_init_struct.Mode = GPIO_MODE_AF_PP;                /* 推挽复用 */
    gpio_init_struct.Pull = GPIO_NOPULL;                    /* 不带上下拉 */
    gpio_init_struct.Speed = GPIO_SPEED_HIGH;               /* 高速 */
    gpio_init_struct.Alternate = GPIO_AF11_ETH;             /* 复用为ETH功能 */
    HAL_GPIO_Init(GPIOA, &gpio_init_struct);    /* ETH_CLK,ETH_MDIO,ETH_CRS引脚模式设置 */
    /* PC1,4,5 */
    gpio_init_struct.Pin = GPIO_PIN_1|GPIO_PIN_4|GPIO_PIN_5;
    HAL_GPIO_Init(GPIOC, &gpio_init_struct);    /* ETH_MDC,ETH_RXD0,ETH_RXD1初始化 */
    /* PG11,13,14 */
    gpio_init_struct.Pin = GPIO_PIN_11|GPIO_PIN_13|GPIO_PIN_14;
    HAL_GPIO_Init(GPIOG, &gpio_init_struct);  /* ETH_TX_EN,ETH_TXD0,ETH_TXD1初始化 */
    /* PD3复位引脚 */
    gpio_init_struct.Pin = GPIO_PIN_3;              /* ETH_RESET初始化 */
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    HAL_GPIO_Init(GPIOD, &gpio_init_struct);
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_RESET);   /* 硬件复位 */
    delay_ms(50);
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_SET);     /* 复位结束 */
    HAL_NVIC_SetPriority(ETH_IRQn, 1, 0);                   /* 网络中断优先级应该高一点 */
    HAL_NVIC_EnableIRQ(ETH_IRQn);
}
/**
 * @breif       LWIP初始化(LWIP启动的时候使用)
 * @param       无
 * @retval      0,成功
 *              1,设备IP信息错误
 *              2,以太网芯片初始化失败
 *              3,网卡添加失败.
 */
uint8_t MX_LWIP_Init(void)
{   
    ip4_addr_t ipaddr;                   /* ip地址 */
    ip4_addr_t netmask;                  /* 子网掩码 */
    ip4_addr_t gw;                       /* 默认网关 */
    if (lwip_dev_info_set(&g_lwipdev))   /* 设置默认IP等信息 0:ok 1:fail */
				return 1;												 /* 设置IP信息失败 */
	  if (ethernet_init())                 /* 初始化以太网芯片 0:ok 1:fail */
				return 3;                        /* 以太网芯片初始化失败 */
    lwip_init();                         /* 初始化LWIP内核 Initilialize the LwIP stack without RTOS */
#if LWIP_DHCP                            /* 使用动态IP */
    ip_addr_set_zero_ip4(&ipaddr);       /* 对IP地址、子网掩码及网关清零 */
    ip_addr_set_zero_ip4(&netmask);
    ip_addr_set_zero_ip4(&gw);
#else   /* 使用静态IP */
    IP4_ADDR(&ipaddr, g_lwipdev.localip[0], g_lwipdev.localip[1], g_lwipdev.localip[2], g_lwipdev.localip[3]);
    IP4_ADDR(&netmask, g_lwipdev.netmask[0], g_lwipdev.netmask[1], g_lwipdev.netmask[2], g_lwipdev.netmask[3]);
    IP4_ADDR(&gw, g_lwipdev.gateway[0], g_lwipdev.gateway[1], g_lwipdev.gateway[2], g_lwipdev.gateway[3]);
    g_lwipdev.dhcpstatus = 0XFF; /* 获取失败 */
#endif
		/* 向网卡列表中添加一个网口add the network interface (IPv4/IPv6) without RTOS */
    if (netif_add(&g_netif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &ethernet_input) == NULL) /* 调用netif_add()返回值,用于判断网络初始化是否成功 */
        return 2;                        /* 网卡添加失败 */
		netif_set_default(&g_netif);         /* 网口添加成功,设置netif为默认网口,并且打开netif网口 */
		if (netif_is_link_up(&g_netif))
				netif_set_up(&g_netif);          /* 当netif完全配置后,调用此函数打开netif网口 */
		else
				netif_set_down(&g_netif);        /* 当netif链接关闭时,调用此函数关闭netif网口 */
		lwip_link_status_updated(&g_netif);  /* DHCP链接状态更新函数 */
		netif_set_link_callback(&g_netif, lwip_link_status_updated); /*设置链接回调函数,此函数在链接状态更改时调用 */
#if LWIP_DHCP                            /* 如果使用DHCP的话 */
    g_lwipdev.dhcpstatus = 0;            /* DHCP标记为0 */
#endif
    return 0;                            /* 操作OK. */
}

/**
 * @breif       读取以太网芯片寄存器值
 * @param       reg:读取的寄存器地址
 * @retval      无
 */
uint32_t ethernet_read_phy(uint16_t reg)
{
    uint32_t regval;
    HAL_ETH_ReadPHYRegister(&g_eth_handler, reg, &regval);
    return regval;
}
/**
 * @breif       向以太网芯片指定地址写入寄存器值
 * @param       reg   : 要写入的寄存器
 * @param       value : 要写入的寄存器
 * @retval      无
 */
void ethernet_write_phy(uint16_t reg, uint16_t value)
{
    uint32_t temp = value;
    HAL_ETH_WritePHYRegister(&g_eth_handler, reg, temp);
}
/**
 * @breif       获得网络芯片的速度模式
 * @param       无
 * @retval      1:获取100M成功
                0:失败
 */
uint8_t ethernet_chip_get_speed(void)
{
    uint8_t speed;
    #if(PHY_TYPE == LAN8720) 
    speed = ~((ethernet_read_phy(PHY_SR) & PHY_SPEED_STATUS));         /* 从LAN8720的31号寄存器中读取网络速度和双工模式 */
    #elif(PHY_TYPE == YT8512C)
    speed = ((ethernet_read_phy(PHY_SR) & PHY_SPEED_STATUS) >> 14);    /* 从YT8512C的17号寄存器中读取网络速度和双工模式 */
    #endif
    return speed;
}

/**
 * @breif       获取接收到的帧长度
 * @param       dma_rx_desc : 接收DMA描述符
 * @retval      frameLength : 接收到的帧长度
 */
uint32_t  ethernet_get_eth_rx_size(ETH_DMADescTypeDef *dma_rx_desc)
{
    uint32_t frameLength = 0;

    if (((dma_rx_desc->Status & ETH_DMARXDESC_OWN) == (uint32_t)RESET) &&
        ((dma_rx_desc->Status & ETH_DMARXDESC_ES)  == (uint32_t)RESET) &&
        ((dma_rx_desc->Status & ETH_DMARXDESC_LS)  != (uint32_t)RESET))
    {
        frameLength = ((dma_rx_desc->Status & ETH_DMARXDESC_FL) >> ETH_DMARXDESC_FRAME_LENGTHSHIFT);
    }
    return frameLength;
}
/**
 * @breif       中断服务函数
 * @param       无
 * @retval      无
 */
void ETH_IRQHandler(void)
{
    if (ethernet_get_eth_rx_size(g_eth_handler.RxDesc)) {
				ethernetif_input(&g_netif);       /* 从网络缓冲区中读取接收到的数据包并将其发送给LWIP处理 */                                      
    }
    __HAL_ETH_DMA_CLEAR_IT(&g_eth_handler, ETH_DMA_IT_NIS);             /* 清除DMA中断标志位 */
    __HAL_ETH_DMA_CLEAR_IT(&g_eth_handler, ETH_DMA_IT_R);               /* 清除DMA接收中断标志位 */
}

/**
 * @breif       LWIP轮询任务
 * @param       无
 * @retval      无
 */
void lwip_periodic_handle(void)
{
    sys_check_timeouts();
#if LWIP_DHCP       /* 如果使用DHCP */
    /* Fine DHCP periodic process every 500ms */
    if (HAL_GetTick() - g_dhcp_fine_timer >= DHCP_FINE_TIMER_MSECS)
    {
        g_dhcp_fine_timer =  HAL_GetTick();
        /* process DHCP state machine */
        lwip_dhcp_process(&g_netif);
    }
#endif
}

#if LWIP_DHCP   /* 如果使用DHCP */
/**
 * @brief       lwIP的DHCP分配进程
 * @param       netif:网卡控制块
 * @retval      无
 */
void lwip_dhcp_process(struct netif *netif)
{
    uint32_t ip = 0;
    uint32_t netmask = 0;
    uint32_t gw = 0;
    struct dhcp *dhcp;
    uint8_t iptxt[20];                  /* 存储已分配的IP地址 */
    g_lwipdev.dhcpstatus = 1;           /* DHCP标记为1 */
    
    /* 根据DHCP状态进入执行相应的动作 */
    switch (g_lwip_dhcp_state)
    {
        case LWIP_DHCP_START:
        {
            /* 对IP地址、网关地址及子网页码清零操作 */
            ip_addr_set_zero_ip4(&netif->ip_addr);
            ip_addr_set_zero_ip4(&netif->netmask);
            ip_addr_set_zero_ip4(&netif->gw);
            /* 设置DHCP的状态为等待分配IP地址 */
            g_lwip_dhcp_state = LWIP_DHCP_WAIT_ADDRESS;
            /* 开启DHCP */
            dhcp_start(netif);
        }
        break;

        case LWIP_DHCP_WAIT_ADDRESS:
        {
            ip = g_netif.ip_addr.addr;       /* 读取新IP地址 */
            netmask = g_netif.netmask.addr;  /* 读取子网掩码 */
            gw = g_netif.gw.addr;            /* 读取默认网关 */
            printf ("等待DHCP分配网络参数......\r\n");
            
            if (dhcp_supplied_address(netif)) 
            {
                printf ("DHCP分配成功......\r\n");
                g_lwip_dhcp_state = LWIP_DHCP_ADDRESS_ASSIGNED;
                sprintf((char *)iptxt, "%s", ip4addr_ntoa((const ip4_addr_t *)&netif->ip_addr));
                printf ("IP address assigned by a DHCP server: %s\r\n", iptxt);
                
                if (ip != 0)
                {
                    g_lwipdev.dhcpstatus = 2;         /* DHCP成功 */
                    printf("网卡en的MAC地址为:................%d.%d.%d.%d.%d.%d\r\n", g_lwipdev.mac[0], g_lwipdev.mac[1], g_lwipdev.mac[2], g_lwipdev.mac[3], g_lwipdev.mac[4], g_lwipdev.mac[5]);
                    /* 解析出通过DHCP获取到的IP地址 */
                    g_lwipdev.localip[3] = (uint8_t)(ip >> 24);
                    g_lwipdev.localip[2] = (uint8_t)(ip >> 16);
                    g_lwipdev.localip[1] = (uint8_t)(ip >> 8);
                    g_lwipdev.localip[0] = (uint8_t)(ip);
                    printf("通过DHCP获取到IP地址..............%d.%d.%d.%d\r\n", g_lwipdev.localip[0], g_lwipdev.localip[1], g_lwipdev.localip[2], g_lwipdev.localip[3]);
                    /* 解析通过DHCP获取到的子网掩码地址 */
                    g_lwipdev.netmask[3] = (uint8_t)(netmask >> 24);
                    g_lwipdev.netmask[2] = (uint8_t)(netmask >> 16);
                    g_lwipdev.netmask[1] = (uint8_t)(netmask >> 8);
                    g_lwipdev.netmask[0] = (uint8_t)(netmask);
                    printf("通过DHCP获取到子网掩码............%d.%d.%d.%d\r\n", g_lwipdev.netmask[0], g_lwipdev.netmask[1], g_lwipdev.netmask[2], g_lwipdev.netmask[3]);
                    /* 解析出通过DHCP获取到的默认网关 */
                    g_lwipdev.gateway[3] = (uint8_t)(gw >> 24);
                    g_lwipdev.gateway[2] = (uint8_t)(gw >> 16);
                    g_lwipdev.gateway[1] = (uint8_t)(gw >> 8);
                    g_lwipdev.gateway[0] = (uint8_t)(gw);
                    printf("通过DHCP获取到的默认网关..........%d.%d.%d.%d\r\n", g_lwipdev.gateway[0], g_lwipdev.gateway[1], g_lwipdev.gateway[2], g_lwipdev.gateway[3]);
                }
            }
            else
            {
                dhcp = (struct dhcp *)netif_get_client_data(netif, LWIP_NETIF_CLIENT_DATA_INDEX_DHCP);

                /* DHCP超时 */
                if (dhcp->tries > LWIP_MAX_DHCP_TRIES)
                {
                    printf ("DHCP分配失败,进入静态分配......\r\n");
                    g_lwip_dhcp_state = LWIP_DHCP_TIMEOUT;

                    /* 停止DHCP */
                    dhcp_stop(netif);
                    g_lwipdev.dhcpstatus = 0XFF;
                    /* 使用静态IP地址 */
                    IP4_ADDR(&(g_netif.ip_addr), g_lwipdev.localip[0], g_lwipdev.localip[1], g_lwipdev.localip[2], g_lwipdev.localip[3]);
                    IP4_ADDR(&(g_netif.netmask), g_lwipdev.netmask[0], g_lwipdev.netmask[1], g_lwipdev.netmask[2], g_lwipdev.netmask[3]);
                    IP4_ADDR(&(g_netif.gw), g_lwipdev.gateway[0], g_lwipdev.gateway[1], g_lwipdev.gateway[2], g_lwipdev.gateway[3]);
                    netif_set_addr(netif, &g_netif.ip_addr, &g_netif.netmask, &g_netif.gw);
                    sprintf((char *)iptxt, "%s", ip4addr_ntoa((const ip4_addr_t *)&netif->ip_addr));
                    printf ("Static IP address: %s\r\n", iptxt);
                }
            }
        }
        break;
        case LWIP_DHCP_LINK_DOWN:
        {
            /* 停止DHCP */
            dhcp_stop(netif);
            g_lwip_dhcp_state = LWIP_DHCP_OFF; 
        }
        break;
        default: break;
    }
}
#endif

在上述源代码中,主要实现LWIP初始化功能,由MX_LWIP_Init()函数实现,该函数在LWIP启动的时候使用,被main调用。其中MX_LWIP_Init函数会调用lwip_dev_info_set、ethernet_init、lwip_init、netif_add、netif_is_link_up、netif_set_link_callback这五个函数,分别实现设置默认IP等信息、初始化以太网芯片、初始化LWIP内核、向网卡列表中添加一个网口、并打开该网口、设置链接回调函数在链接状态更改时调用等功能,具体实现方法请参考文中代码。

需要注意一点是的,ethernet_init()函数中将g_eth_handler以太网句柄变量中的接收模式设置为ETH_RXINTERRUPT_MODE中断接收模式,这个实现方式与CubeMX实现是有区别的,请注意,这一点直接影响后面在main上的使用方式,当然你也可以改成跟CubeMX实现同样的接收模式ETH_RXPOLLING_MODE。

以太网驱动初始化和MAC的驱动程序头文件代码如下示例。

#ifndef __ETHERNET_H
#define __ETHERNET_H

#include "ethernetif.h"
#define UDP_PORT     8888 		 /* 网络端口 */
typedef struct
{
    uint8_t mac[6];            /* MAC地址 */
    uint8_t remoteip[4];       /* 远端主机IP地址 */ 
    uint8_t localip[4];        /* 本机IP地址 */
    uint8_t netmask[4];        /* 子网掩码 */
    uint8_t gateway[4];        /* 默认网关的IP地址 */
	uint16_t remoteport;	   /* 远端主机端口 */
	uint16_t localport;        /* 本机主机端口 */
    uint8_t dhcpstatus;        /* dhcp状态 */
															 /* 0, 未获取DHCP地址;*/
															 /* 1, 进入DHCP获取状态*/
															 /* 2, 成功获取DHCP地址*/
															 /* 0XFF,获取失败 */
}LWIP_DEV_INFO;			           /* lwip控制结构体 */
extern LWIP_DEV_INFO g_lwipdev;     																		/* lwip控制结构体 */
extern ETH_HandleTypeDef g_eth_handler;                                 /* 以太网句柄 */

void ETH_IRQHandler(void);
uint32_t ethernet_read_phy(uint16_t reg);                               /* 读取以太网芯片寄存器值 */
void ethernet_write_phy(uint16_t reg, uint16_t value);                  /* 向以太网芯片指定地址写入寄存器值 */
uint8_t ethernet_chip_get_speed(void);                                  /* 获得以太网芯片的速度模式 */
void lwip_periodic_handle(void);                    /* lwip_periodic_handle */
uint8_t MX_LWIP_Init(void);                         /* LWIP初始化(LWIP启动的时候使用) */
void lwip_dhcp_process_handle(void);                /* DHCP处理任务 */

#endif

5.修改ethernetif文件

从“lwip-contrib-STABLE-2_1_0_RELEASE”文件包上获取的ethernetif.c/h这两个文件,需进行如下修改,ethernetif.h头文件代码示例。

#ifndef __ETHERNETIF_H__
#define __ETHERNETIF_H__

#include <string.h>
#include "lwip/udp.h"
#include "lwip/init.h"
#include "lwip/snmp.h"
#include "netif/etharp.h"
#include "lwip/dhcp.h"
#include "lwip/timeouts.h"
#include "./SYSTEM/delay/delay.h"

/* DHCP进程状态 */
#if LWIP_DHCP
#define LWIP_DHCP_OFF                   (uint8_t) 0   /* DHCP服务器关闭状态 */
#define LWIP_DHCP_START                 (uint8_t) 1   /* DHCP服务器启动状态 */
#define LWIP_DHCP_WAIT_ADDRESS          (uint8_t) 2   /* DHCP服务器等待分配IP状态 */
#define LWIP_DHCP_ADDRESS_ASSIGNED      (uint8_t) 3   /* DHCP服务器地址已分配状态 */
#define LWIP_DHCP_TIMEOUT               (uint8_t) 4   /* DHCP服务器超时状态 */
#define LWIP_DHCP_LINK_DOWN             (uint8_t) 5   /* DHCP服务器链接失败状态 */
#define LWIP_MAX_DHCP_TRIES                       4   /* DHCP服务器最大重试次数 */
extern uint32_t g_dhcp_fine_timer;                 		/* DHCP精细处理计时器 */
extern volatile uint8_t g_lwip_dhcp_state;         		/* DHCP状态初始化 */
#endif

err_t ethernetif_init(struct netif *netif); 				/* 网卡初始化函数 */
void lwip_link_status_updated(struct netif *netif); /* DHCP链接状态更新函数 */
void ethernetif_input(struct netif *netif);  				/* 数据包输入函数 */
u32_t sys_now(void);
#endif

ethernetif.c源文件代码示例,具体函数解释见文中代码处,或者参考前面一篇博文STM32之LWIP网络通讯设计介绍(十四)-CSDN博客提供lwip参考书籍,野火的lwIP开发指南【免费】嵌入式开发:基于野火STM32的LwIP应用开发指南、或原子的lwIP开发指南。

#include "./BSP/ETHERNET/EthDriver.h"

/* 定义这些以更好地描述您的网络接口 Network interface name */
#define IFNAME0 'e'
#define IFNAME1 'n'
/* Private variables ---------------------------------------------------------*/
__ALIGN_BEGIN ETH_DMADescTypeDef  DMARxDscrTab[ETH_RXBUFNB] __ALIGN_END; /* 以太网DMA接收描述符数据结构体 */
__ALIGN_BEGIN ETH_DMADescTypeDef  DMATxDscrTab[ETH_TXBUFNB] __ALIGN_END; /* 以太网DMA发送描述符数据结构体 */
__ALIGN_BEGIN uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] __ALIGN_END; /* 以太网底层驱动接收buffer */
__ALIGN_BEGIN uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] __ALIGN_END; /* 以太网底层驱动发送buffer */

#if LWIP_DHCP
uint32_t g_dhcp_fine_timer = 0;                         /* DHCP精细处理计时器 */
__IO uint8_t g_lwip_dhcp_state = LWIP_DHCP_OFF;         /* DHCP状态初始化 */
#endif

struct ethernetif {
    struct eth_addr *ethaddr;
    /* Add whatever per-interface state that is needed here. */
};

/* Forward declarations. */
void  ethernetif_input(struct netif *netif);
/**
 * 在此功能中 应初始化硬件
 * 被ethernetif_init()调用
 */
static void
low_level_init(struct netif *netif)
{
    netif->hwaddr_len = ETHARP_HWADDR_LEN; /* 设置MAC地址长度,为6个字节 */
    /* 初始化MAC地址,设置什么地址由用户自己设置,但是不能与网络中其他设备MAC地址重复 */
    netif->hwaddr[0] = g_lwipdev.mac[0]; 
    netif->hwaddr[1] = g_lwipdev.mac[1]; 
    netif->hwaddr[2] = g_lwipdev.mac[2];
    netif->hwaddr[3] = g_lwipdev.mac[3];   
    netif->hwaddr[4] = g_lwipdev.mac[4];
    netif->hwaddr[5] = g_lwipdev.mac[5];
    netif->mtu = 1500; /* 最大允许传输单元,允许该网卡广播和ARP功能 */
    /* 网卡状态信息标志位,是很重要的控制字段,它包括网卡功能使能、广播 */
    /* 使能、 ARP 使能等等重要控制位 */
    netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;      /* 广播 ARP协议 链接检测 */
    HAL_ETH_DMATxDescListInit(&g_eth_handler,DMATxDscrTab,&Tx_Buff[0][0],ETH_TXBUFNB); /* 初始化发送描述符 */
    HAL_ETH_DMARxDescListInit(&g_eth_handler,DMARxDscrTab,&Rx_Buff[0][0],ETH_RXBUFNB); /* 初始化接收描述符 */
    HAL_ETH_Start(&g_eth_handler); /* 开启ETH */
}

/**
 * @brief       通知用户网络接口配置状态
 * @param       netif:网卡控制块
 * @retval      无
 */
void lwip_link_status_updated(struct netif *netif)
{
    if (netif_is_up(netif))
    {
#if LWIP_DHCP
        /* Update DHCP state machine */
        g_lwip_dhcp_state = LWIP_DHCP_START;
#endif /* LWIP_DHCP */
    }
    else
    {
#if LWIP_DHCP
        /* Update DHCP state machine */
        g_lwip_dhcp_state = LWIP_DHCP_LINK_DOWN;
#endif /* LWIP_DHCP */
    }
}

static err_t
low_level_output(struct netif *netif, struct pbuf *p)
{
    err_t errval;
    struct pbuf *q;
    
    uint8_t *buffer = (uint8_t *)(g_eth_handler.TxDesc->Buffer1Addr);
    __IO ETH_DMADescTypeDef *DmaTxDesc;
    uint32_t framelength = 0;
    uint32_t bufferoffset = 0;
    uint32_t byteslefttocopy = 0;
    uint32_t payloadoffset = 0;
    DmaTxDesc = g_eth_handler.TxDesc;
    bufferoffset = 0;
#if ETH_PAD_SIZE
  pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif
    /* 从pbuf中拷贝要发送的数据 */
    for (q = p;q != NULL;q = q->next)
    {
        /* 判断此发送描述符是否有效,即判断此发送描述符是否归以太网DMA所有 */
        if ((DmaTxDesc->Status & ETH_DMATXDESC_OWN) != (uint32_t)RESET)
        {
            errval = ERR_USE;
            goto error;               /* 发送描述符无效,不可用 */
        }
        byteslefttocopy = q->len;     /* 要发送的数据长度 */
        payloadoffset = 0; 
        
        /* 将pbuf中要发送的数据写入到以太网发送描述符中,有时候我们要发送的数据可能大于一个以太网
           描述符的Tx Buffer,因此我们需要分多次将数据拷贝到多个发送描述符中 */
        while ((byteslefttocopy + bufferoffset) > ETH_TX_BUF_SIZE )
        {
            /* 将数据拷贝到以太网发送描述符的Tx Buffer中 */
            memcpy((uint8_t*)((uint8_t*)buffer + bufferoffset),(uint8_t*)((uint8_t*)q->payload + payloadoffset),(ETH_TX_BUF_SIZE - bufferoffset));
            /* DmaTxDsc指向下一个发送描述符 */
            DmaTxDesc = (ETH_DMADescTypeDef *)(DmaTxDesc->Buffer2NextDescAddr);
            /* 检查新的发送描述符是否有效 */
            if ((DmaTxDesc->Status & ETH_DMATXDESC_OWN) != (uint32_t)RESET)
            {
                errval = ERR_USE;
                goto error;     /* 发送描述符无效,不可用 */
            }
            
            buffer = (uint8_t *)(DmaTxDesc->Buffer1Addr);   /* 更新buffer地址,指向新的发送描述符的Tx Buffer */
            byteslefttocopy = byteslefttocopy - (ETH_TX_BUF_SIZE - bufferoffset);
            payloadoffset = payloadoffset + (ETH_TX_BUF_SIZE - bufferoffset);
            framelength = framelength + (ETH_TX_BUF_SIZE - bufferoffset);
            bufferoffset = 0;
        }
        /* 拷贝剩余的数据 */
        memcpy( (uint8_t*)((uint8_t*)buffer + bufferoffset),(uint8_t*)((uint8_t*)q->payload+payloadoffset),byteslefttocopy );
        bufferoffset = bufferoffset + byteslefttocopy;
        framelength = framelength + byteslefttocopy;
    }
    
    /* 当所有要发送的数据都放进发送描述符的Tx Buffer以后就可发送此帧了 */
    HAL_ETH_TransmitFrame(&g_eth_handler,framelength);
    errval = ERR_OK;
error:            
    /* 发送缓冲区发生下溢,一旦发送缓冲区发生下溢TxDMA会进入挂起状态 */
    if ((g_eth_handler.Instance->DMASR & ETH_DMASR_TUS) != (uint32_t)RESET)
    {
        /* 清除下溢标志 */
        g_eth_handler.Instance->DMASR = ETH_DMASR_TUS;
        /* 当发送帧中出现下溢错误的时候TxDMA会挂起,这时候需要向DMATPDR寄存器 */
        /* 随便写入一个值来将其唤醒,此处我们写0 */
        g_eth_handler.Instance->DMATPDR = 0;
    }
    
#if ETH_PAD_SIZE
  pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif
    return errval;
}
/**
 * Should allocate a pbuf and transfer the bytes of the incoming
 * packet from the interface into the pbuf.
 *
 * @param netif the lwip network interface structure for this ethernetif
 * @return a pbuf filled with the received packet (including MAC header)
 *         NULL on memory error
 */
static struct pbuf *
low_level_input(struct netif *netif)
{  
    struct pbuf *p, *q;
    u16_t len;
    uint8_t *buffer;
    __IO ETH_DMADescTypeDef *dmarxdesc;
    uint32_t bufferoffset = 0;
    uint32_t payloadoffset = 0;
    uint32_t byteslefttocopy = 0;
    uint32_t i = 0;
    if (HAL_ETH_GetReceivedFrame(&g_eth_handler) != HAL_OK)  /* 判断是否接收到数据 */
    return NULL;
    len = g_eth_handler.RxFrameInfos.length;                /* 获取接收到的以太网帧长度 */
    
#if ETH_PAD_SIZE
  len += ETH_PAD_SIZE; /* allow room for Ethernet padding */
#endif
    
    buffer = (uint8_t *)g_eth_handler.RxFrameInfos.buffer;  /* 获取接收到的以太网帧的数据buffer */
  
    p = pbuf_alloc(PBUF_RAW,len,PBUF_POOL);                 /* 申请pbuf */
    
    if (p != NULL)                                           /* pbuf申请成功 */
    {
        dmarxdesc = g_eth_handler.RxFrameInfos.FSRxDesc;    /* 获取接收描述符链表中的第一个描述符 */
        bufferoffset = 0;
        
        for (q = p;q != NULL;q = q->next)
        {
            byteslefttocopy = q->len;
            payloadoffset = 0;
            
            /* 将接收描述符中Rx Buffer的数据拷贝到pbuf中 */
            while ((byteslefttocopy + bufferoffset) > ETH_RX_BUF_SIZE )
            {
                /* 将数据拷贝到pbuf中 */
                memcpy((uint8_t*)((uint8_t*)q->payload+payloadoffset),(uint8_t*)((uint8_t*)buffer + bufferoffset),(ETH_RX_BUF_SIZE - bufferoffset));
                 /* dmarxdesc向下一个接收描述符 */
                dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
                /* 更新buffer地址,指向新的接收描述符的Rx Buffer */
                buffer = (uint8_t *)(dmarxdesc->Buffer1Addr);
 
                byteslefttocopy = byteslefttocopy - (ETH_RX_BUF_SIZE - bufferoffset);
                payloadoffset = payloadoffset + (ETH_RX_BUF_SIZE - bufferoffset);
                bufferoffset = 0;
            }
            /* 拷贝剩余的数据 */
            memcpy((uint8_t*)((uint8_t*)q->payload + payloadoffset),(uint8_t*)((uint8_t*)buffer + bufferoffset),byteslefttocopy);
            bufferoffset = bufferoffset + byteslefttocopy;
        }
    }
    else
    {
        /* drop packet();  丢包函数自行编写 */
        LINK_STATS_INC(link.memerr);
        LINK_STATS_INC(link.drop);
        MIB2_STATS_NETIF_INC(netif, ifindiscards);
    }
    
    /* 释放DMA描述符 */
    dmarxdesc = g_eth_handler.RxFrameInfos.FSRxDesc;
    
    for (i = 0;i < g_eth_handler.RxFrameInfos.SegCount; i ++)
    {  
        dmarxdesc->Status |= ETH_DMARXDESC_OWN;       /* 标记描述符归DMA所有 */
        dmarxdesc = (ETH_DMADescTypeDef *)(dmarxdesc->Buffer2NextDescAddr);
    }
    
    g_eth_handler.RxFrameInfos.SegCount = 0;           /* 清除段计数器 */
    
    if ((g_eth_handler.Instance->DMASR & ETH_DMASR_RBUS) != (uint32_t)RESET)  /* 接收缓冲区不可用 */
    {
        /* 清除接收缓冲区不可用标志 */
        g_eth_handler.Instance->DMASR = ETH_DMASR_RBUS;
        /* 当接收缓冲区不可用的时候RxDMA会进去挂起状态,通过向DMARPDR写入任意一个值来唤醒Rx DMA */
        g_eth_handler.Instance->DMARPDR = 0;
    }
    
    return p;
}

/**
 * This function should be called when a packet is ready to be read
 * from the interface. It uses the function low_level_input() that
 * should handle the actual reception of bytes from the network
 * interface. Then the type of the received packet is determined and
 * the appropriate input function is called.
 *
 * @param netif the lwip network interface structure for this ethernetif
 */
void
ethernetif_input(struct netif *netif)
{
    struct pbuf *p;

    /* move received packet into a new pbuf */
    p = low_level_input(netif);
    /* if no packet could be read, silently ignore this */
    if (p != NULL)
    {
        /* pass all packets to ethernet_input, which decides what packets it supports */
        if (netif->input(p, netif) != ERR_OK)
        {
            LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
            pbuf_free(p);
            p = NULL;
        }
    }

} 
/**
 * Should be called at the beginning of the program to set up the
 * network interface. It calls the function low_level_init() to do the
 * actual setup of the hardware.
 *
 * This function should be passed as a parameter to netif_add().
 *
 * @param netif the lwip network interface structure for this ethernetif
 * @return ERR_OK if the loopif is initialized
 *         ERR_MEM if private data couldn't be allocated
 *         any other err_t on error
 */
err_t
ethernetif_init(struct netif *netif)
{
    struct ethernetif *ethernetif;

    LWIP_ASSERT("netif != NULL", (netif != NULL));

    ethernetif = mem_malloc(sizeof(struct ethernetif));
    
    if (ethernetif == NULL)
    {
        LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_init: out of memory\n"));
        return ERR_MEM;
    }

#if LWIP_NETIF_HOSTNAME
  /* Initialize interface hostname */
  netif->hostname = "lwip";
#endif /* LWIP_NETIF_HOSTNAME */

    /*
    * Initialize the snmp variables and counters inside the struct netif.
    * The last argument should be replaced with your link speed, in units
    * of bits per second.
    */
    MIB2_INIT_NETIF(netif, snmp_ifType_ethernet_csmacd, LINK_SPEED_OF_YOUR_NETIF_IN_BPS);

    netif->state = ethernetif;
    netif->name[0] = IFNAME0;
    netif->name[1] = IFNAME1;
    /* We directly use etharp_output() here to save a function call.
    * You can instead declare your own function an call etharp_output()
    * from it if you have to do some checks before sending (e.g. if link
    * is available...) */
#if LWIP_IPV4
    netif->output = etharp_output;
#endif /* LWIP_IPV4 */
#if LWIP_IPV6
    netif->output_ip6 = ethip6_output;
#endif /* LWIP_IPV6 */
    netif->linkoutput = low_level_output;

    ethernetif->ethaddr = (struct eth_addr *) & (netif->hwaddr[0]);

    /* initialize the hardware */
    low_level_init(netif);

    return ERR_OK;
}
u32_t sys_now(void)
{
    return HAL_GetTick();
}

6.添加用户代码

将CubeMX实现的用户层代码添加进来,代码跟上面提供的一样,这里不在给出。添加到工程的 Middlewares\lwip\lwip_app\ 路径下,需添加的文件如下所示,包含源文件和头文件。

7.main文件实现

main()函数主要完成相关GPIO引脚、LWIP、系统时钟初始化配置;然后初始化用户UDP网络设置、和用户参数;最后进入主循环,处理网络上的数据,由UDP_Data_Process()函数完成。根据前面PHY驱动代码(ethernet.c文件中),将g_eth_handler以太网句柄变量中的接收模式设置为ETH_RXINTERRUPT_MODE中断接收模式(这个方式一实现是有区别的)。当ETH_IRQHandler中断服务函数接收到网络上数据后,调用ethernetif_input任务函数,它调用low_level_input函数获取描述符管理缓冲区的数据,并使用netif->input函数将缓冲区数据交给IP层,传递给pbuf数据包,由主循环上注册的接收回调函数UDP_Receive_Callback解析处理。main函数代码示例如下。

#include "eth_user.h"
int main(void)
{
	uint8_t retry = 0;
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
    delay_init(168);                    /* 延时初始化 */
	while (MX_LWIP_Init()) {            /* 初始化lwip,如果失败的话就重试3次 */
        retry++;
        if (retry > 3)
            break;                      /* lwip初始化失败 */
    }
    //if (!ethernet_read_phy(PHY_SR))     /* 检查CPU与PHY芯片是否通信成功 */
    //    printf("CPU与PHY驱动芯片通信失败\r\n");
    //uint8_t speed = ethernet_chip_get_speed();
	//	printf("网络速度:%d\r\n", speed);	
#if LWIP_DHCP
    while ((g_lwipdev.dhcpstatus != 2) && (g_lwipdev.dhcpstatus != 0XFF)) /* 等待DHCP获取成功/超时溢出 */
    {
        lwip_periodic_handle();  /* LWIP轮询任务 */
        delay_ms(1000);
    }
#endif
    User_UDP_Init();
    while (1)
    {
		UDP_Data_Process();
    }
}

8.效果演示

上述移植、修改、编写完代码后,进行编译运行,连接仿真器在线调式、或者直接烧写到板子中,打开网络监控助手,效果如下。


总结

 下面提供的代码,基于STM32F407ZGT芯片编写,可直接在原子开发板上运行,也可运行在各工程项目上,但需要注意各接口以及相应的引脚应和原子开发板上保持一致。

相应的代码链接:单片机STM32F407-Case程序代码例程-CSDN文库


http://www.kler.cn/a/507509.html

相关文章:

  • JVM直击重点
  • Vue.js组件开发-实现输入框与筛选逻辑
  • 【算法】枚举
  • 深度学习-87-大模型训练之预训练和微调所用的数据样式
  • BertTokenizerFast 和 BertTokenizer 的区别
  • STM32 FreeRTOS移植
  • Thrustmaster Hotas Warthog飞行操作杆开发
  • [cg] glDrawBuffers MRT的应用
  • 【0x3D】HCI_Remote_Host_Supported_Features_Notification事件详解
  • 1.7 ChatGPT:引领AI对话革命的致胜之道
  • 冯康简介,中国有限元先驱
  • 基于HPM6750实现一个UVC和MSC的符合类设备
  • Flink CDC解决数据库同步,异常情况下增量、全量问题
  • Vue前端框架概述
  • 数据持久层:构建应用程序与数据源的桥梁
  • Ncat: bind to :::7777: Address already in use报错问题解决
  • 牛客小白月赛109 A-C
  • vue2:实现上下两栏布局,可拖拽改变高度
  • 楚慧杯Web
  • 【Linux】常用指令详解二
  • 【Spring Cloud Alibaba】基于Spring Boot 3.x 搭建教程
  • CVPR 2024 机器学习方向总汇(多任务、联邦学习、迁移学习和对抗等)
  • PHP转向Python时需要注意的地方
  • 米塔 v0.921 PC/手机版双端 全MOD+全服装(MiSide)免安装中文版 游戏推荐 免费下载
  • RV1126+FFMPEG推流项目(3)VI模块视频编码流程
  • 在PyCharm中使用Anaconda中的虚拟环境