回顾
前面我们分析过LWIP上物理层的结构与LWIP内存管理的底层原理实现,其实都是为了分析LWIP启动流程打下基础。在分析LWIP启动流程之前。还需要知道虚拟网卡的概念。LWIP是一个轻量级的TCP/IP协议栈。在常见的物理层一般都有:以外网、WIFI、NBIOT 等联网硬件接口。显然LWIP不仅仅是支持以外网的一种结构的,其他的网络接口都支持。如果单片机含有多个网口或者是多个WIFI + 以外网接口的情况下。如何判断当前是使用哪一个网络接口、如何判断哪一个接口收到或者发送数据?这就需要对这些网络接口进行统一的管理。而用软件的方式来管理这些网咯硬件接口方式我们简称“虚拟网卡”
虚拟网卡
在LWIP中,虚拟网卡是通过网络接口驱动程序来实现的。它通常由操作系统提供的网络驱动程序或LWIP库自带的驱动程序来实现。虚拟网卡在底层通过一组API与LWIP协议栈进行通信。
虚拟网卡的主要功能包括以下几个方面:
-
网络数据收发:虚拟网卡可以接收来自网络的数据包,并将其传递给LWIP协议栈进行处理。同样,它也可以将由LWIP协议栈生成的数据包发送到网络中。 -
数据包处理:虚拟网卡可以对接收到的数据包进行解析和处理。它可以检查数据包的目的地址,根据路由表确定下一跳地址,并将数据包发送到正确的目标。 -
网络配置:虚拟网卡可以接收和设置网络配置参数。例如,它可以接收IP地址、子网掩码、默认网关等信息,并将其传递给LWIP协议栈进行配置。 -
接口状态管理:虚拟网卡可以监控网络接口的状态,例如连接状态、链路状态等,并将状态信息传递给LWIP协议栈。这样,协议栈可以根据接口状态进行相应的处理。
在LWIP中每一个网卡都使用一个 netif 结构体来抽象,多个网卡就有多个 netif,这些 netif 以链表的形式链接起来,形参一个单向的链表。其接口源码如下
struct netif
{
/* 指向下一个 netif 结构的指针 */
struct netif *next;
/* IP 地址相关配置 */
ip_addr_t ip_addr; /* 网络接口的 IP 地址 */
ip_addr_t netmask; /* 子网掩码 */
ip_addr_t gw; /* 网关地址 */
/* 该函数向 IP 层输入数据包 */
netif_input_fn input;
/* 该函数发送 IP 包 */
netif_output_fn output;
/* 该函数实现底层数据包发送 */
netif_linkoutput_fn linkoutput;
/* 该字段用户可以自由设置,例如用于指向一些底层设备相关的信息 */
void *state;
void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX +
LWIP_NUM_NETIF_CLIENT_DATA];
/* 该接口允许的最大数据包长度 */
u16_t mtu;
/* 该接口物理地址长度 */
u8_t hwaddr_len;
/* 该接口的物理地址 */
u8_t hwaddr[NETIF_MAX_HWADDR_LEN];
/* 该接口的状态、属性字段 */
u8_t flags;
/* 该接口的名字 */
char name[2];
/* 接口的编号 */
u8_t num;
/* 需要发送的路由器请求消息的数量 */
u8_t rs_count;
};
其中上面比较注意的参数有:next[指针]、用于组成链表使用。output[输出函数]、intput[输入函数]。这两个函数是物理层与IP层进行数据交换的时候的回调函数指针。
网卡组成链表源代码。(添加网卡)
// 首先定义需要添加的网卡全局变量结构
struct netif g_lwip_netif; /* 定义一个全局的网络接口 */
// 再定义一个全局的默认网卡链表结构
struct netif *netif_list; /*这个其实也是一个网卡全局结构体变量*/
// 在源代码中会调用添加网卡的API
netif_init_flag = netif_add(&g_lwip_netif, (const ip_addr_t *)&ipaddr, (const ip_addr_t *)&netmask, (const ip_addr_t *)&gw, NULL, ðernetif_init, &tcpip_input);
// 其源码就是返回一个网卡结构体的首地址
struct netif * netif_add(struct netif *netif,const ip4_addr_t *ipaddr, const ip4_addr_t *netmask, const ip4_addr_t *gw,void *state, netif_init_fn init, netif_input_fn input)
{
····
// 这里是对网卡的基本属性进行赋值
/* 清空主机 IP 地址、子网掩码、网关等信息。 */
ip_addr_set_zero_ip4(&netif->ip_addr);
ip_addr_set_zero_ip4(&netif->netmask);
ip_addr_set_zero_ip4(&netif->gw);
netif->output = netif_null_output_ip4;
/* 传输的最大数据长度 */
netif->mtu = 0;
/* 网络的接口状态 */
netif->flags = 0;
memset(netif->client_data, 0, sizeof(netif->client_data));
/* 传递进来的参数填写网卡 state、input 等字段的相关信息 */
netif->state = state;
/* 并为当前网卡 分配唯一标识 num */
netif->num = netif_num;
/* 网卡输入 */
netif->input = input;
/* 调用网卡设置函数 netif_set_addr()设置网卡 IP 地址、子网掩码、网关 */
netif_set_addr(netif, ipaddr, netmask, gw);
/* 为 netif 调用用户指定的初始化函数 */
if (init(netif) != ERR_OK) {
return NULL;
}
/* 将这个 netif 添加到列表中 */
netif->next = netif_list;
netif_list = netif;
mib2_netif_added(netif);
netif_invoke_ext_callback(netif, LWIP_NSC_NETIF_ADDED, NULL);
return netif;
····
}
// 上面最最重要的代码段就是,实现了每新增一个网卡都组成了一个单向的链表
// 实现了网卡的链表管理
netif->next = netif_list;
netif_list = netif;
设置默认的网卡
struct netif *netif_default; // 全局的默认网卡指针
/* 注册默认的网络接口 */
netif_set_default(&xnetif);
void netif_set_default(struct netif *netif)
{
if (netif == NULL)
{
/* 删除默认路由 */
mib2_remove_route_ip4(1, netif);
}
else
{
/* 添加默认路由 */
mib2_add_route_ip4(1, netif);
}
netif_default = netif; /* 其实本质就是将要设置默认的网卡地址指向全局默认的网卡结构体即可 */
}
移除网卡
移除网卡需要修改网卡的状态以及重置数据,最后在链表中找到它的位置后移除。
#define NETIF_FOREACH(netif) for ((netif) = netif_list; (netif) != NULL; (netif) = (netif)->next)
void netif_remove(struct netif *netif)
{
// 将网卡对应TCP、UDP、RAW控制块上保存的IP地址清空
if (!ip4_addr_isany_val(*netif_ip4_addr(netif)))
{
netif_do_ip_addr_changed(netif_ip_addr4(netif), NULL);
}
// 如果网卡正在运行,那么需要将其状态修改为禁止状态
if (netif_is_up(netif))
{
netif_set_down(netif);
}
/* 如果是默认网卡,需要将默认网卡指向为空*/
if (netif_default == netif)
{
netif_set_default(NULL);
}
/* 如果是单向链表中的第一个那么需要从链表中删除*/
if (netif_list == netif)
{
netif_list = netif->next;
}
else // 否则需要遍历链表找找位置后删除
{
struct netif *tmp_netif;
// 遍历链表操作
NETIF_FOREACH(tmp_netif)
{
if (tmp_netif->next == netif)
{
tmp_netif->next = netif->next;
break;
}
}
if (tmp_netif == NULL)
{
return;
}
}
if (netif->remove_callback)
{
netif->remove_callback(netif); // 移除后调用回调函数
}
}
网络数据包
TCP/IP 协议本质上就是对数据包的处理过程,lwIP 作者为了提高对数据包的处理工作效率,它提供一种高效的数据包管理机制,使得各层之间对数据包灵活操作,同时避免在各层之 间的复制数据的巨大开销和减少各层间的传递时间。在 linux 的 BSD 协议中,它描述数据包的 结构体叫做 mbuf,而 lwIP 与它类似的结构叫做 pbuf,pbuf 数据包的种类和大小也可以说是多 种多样的,从网卡读取出来的数据包可以是一千个字节也可以是几个字节的 IP 数据报,这些 数据包可能存在于 RAM 和 ROM 中,这个根据用户来决定的,所以 lwIP 为了处理的数据高效, 它需要把这些数据进行统一的管理。
pbuf 结构 如下,本质上也是组成一个单链表
struct pbuf {
/* pbuf 链表中指向下一个 pbuf 结构 */
struct pbuf *next;
/* 数据指针,指向该 pbuf 所记录的数据区域 */
void *payload;
/* 当前 pbuf 及后续所有 pbuf 中所包含的数据总长度 */
u16_t tot_len;
/* 当前 pbuf 中数据的长度 */
u16_t len;
/* 当前 pbuf 的类型 */
u8_t type;
/* 状态位未用到 */
u8_t flags;
/* 指向该 pbuf 的指针数,即该 pbuf 被引用的次数 */
LWIP_PBUF_REF_T ref;
/* 对于传入的数据包,它包含输入 netif 的索引 */
u8_t if_idx;
};
对于上图,我们要格外注意的是type 这个字段根据不同的字段会有四种不同的puf结构
PBUF_RAM 类型
一般协议栈中要发送的数据都是采用这种形式,这个类型也是常用的类型之一,申请 PBUF_RAM 类型的 pbuf 时协议栈会在内存堆中分配相应空间,这里的大小包括如前面所述的 pbuf 结构和相应数据缓冲区的大小,并且它们是在一片连续的存储空间。分配完成后的结构如下图所示:
注意Puf不是指向数据区域的首部,而是指向整个内存控制块的首部,与数据区域有一定的offset偏移。这个 offset偏移量常用来存储 TCP 报文首部、IP 首部等。当然 layer 的大小也可以是 0,具体是多少就与数据包的申请方式有关
PBUF_POOL 类型
PBUF_POOL 类型这种类型的 pbuf 可以在极短的时间内得到分配。在网卡接收数据包的时候,我们就使用这种方式包装数据或者存储接收到的数据。其中在系统初始化内存池的时候,还会初始化两类与数据报 pbuf 密切相关的 POOL,如下源码所示:
LWIP_PBUF_MEMPOOL(PBUF, MEMP_NUM_PBUF, 0, "PBUF_REF/ROM")
LWIP_PBUF_MEMPOOL(PBUF_POOL,PBUF_POOL_SIZE, PBUF_POOL_BUFSIZE, "PBUF_POOL")
因此PBUF_POOL 类型结构如下
PBUF_ROM和 PBUF_REF 类型
剩余的两个 PBUF_ROM 和 PBUF_REF 比较类似,它们都是在内存池中分配一个相应的pbuf 结构,但不申请数据区的空间,它们两者的区别在于 PBUF_ROM 指向 ROM 空间内的数 据,后者指向 RAM 空间内的某段数据。在发送某些静态数据时,可以采用这两种类型的 pbuf,这可以大大节省协议栈的内存空间,结构如下图所示:
对于一个数据包来讲,它可能使用上述任意的 pbuf 类型来描述,还可以一大串不同类型的 pbuf 连在一起,共同保存一个数据包的数据,如下图所示:
puf 常用的API
1. pbuf_alloc 函数
static void
pbuf_init_alloced_pbuf(struct pbuf *p, void *payload, u16_t tot_len, u16_t len,
pbuf_type type, u8_t flags)
{
p->next = NULL; /* 指向 NULL */
p->payload = payload; /* 指向数据区域 */
p->tot_len = tot_len; /* 总长度 */
p->len = len; /* 该 pbuf 长度 */
p->type_internal = (u8_t)type; /* 申请的 pbuf 类型 */
p->flags = flags; /* 状态位 */
p->ref = 1; /* 指向该 pbuf 的指针数,即该 pbuf 被引用的次数 */
p->if_idx = NETIF_NO_INDEX; /* 对于传入的数据包,它包含输入 netif 的索引 */
}
struct pbuf * pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
struct pbuf *p;
u16_t offset = (u16_t)layer; /* 申请那个层的首部 */
/* 判断以太网首部 */
switch (type)
{
case PBUF_REF: /* 失败 */
case PBUF_ROM:
p = pbuf_alloc_reference(NULL, length, type);
break;
case PBUF_POOL:
{
struct pbuf *q, *last;
u16_t rem_len; /* 总大小 */
p = NULL;
last = NULL;
rem_len = length;/* rem_len 赋值为总长度 */
do
{
u16_t qlen; /* 减去首部的长度 */
/* 申请内存池 */
q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
if (q == NULL)/* 申请内存池失败 */
{
PBUF_POOL_IS_EMPTY();
if (p)
{
pbuf_free(p);
}
return NULL;
}
/* 总长度减去 offset(首部大小)并赋值给 qlen(去除首部的长度)
LWIP_MIN(x , y) (((x) < (y)) ? (x) : (y)) */
qlen = LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED –
LWIP_MEM_ALIGN_SIZE(offset)));
/* 分配后初始化 struct pbuf 成员 */
pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q +
SIZEOF_STRUCT_PBUF + offset)),
rem_len, qlen, type, 0);
if (p == NULL)/* 第一次分配 p 必定指向 NULL */
{
/* pbuf 链分配头 */
p = q;
}
else
/* 让前面的 pbuf 指向这个 pbuf */
last->next = q;
}
last = q;
/* 判断是否还有剩余长度 */
rem_len = (u16_t)(rem_len - qlen);
offset = 0;
} while (rem_len > 0);/* 如果有剩余,还需要执行一次 do 语句 */
break;
}
case PBUF_RAM:
{
u16_t payload_len = (u16_t)(LWIP_MEM_ALIGN_SIZE(offset) +
LWIP_MEM_ALIGN_SIZE(length));
mem_size_t alloc_len =
(mem_size_t)(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF) + payload_len);
if ((payload_len < LWIP_MEM_ALIGN_SIZE(length)) || (alloc_len < LWIP_MEM_ALIGN_SIZE(length)))
{
return NULL;
}
/* 如果要在 RAM 中分配 pbuf,请为它分配内存。 */
p = (struct pbuf *)mem_malloc(alloc_len);
if (p == NULL)
{
return NULL;
}
pbuf_init_alloced_pbuf(p, LWIP_MEM_ALIGN((void *)((u8_t *)p +
SIZEOF_STRUCT_PBUF + offset)),
length, length, type, 0);
break;
}
default:
return NULL;
}
return p;
}
2. pbuf_free 函数
u8_t pbuf_free(struct pbuf *p)
{
u8_t alloc_src;
struct pbuf *q;
u8_t count;
/* 如果数据包为空则返回 0 */
if (p == NULL)
return 0;
PERF_START;
count = 0;
/* 判断数据包不为空 */
while (p != NULL)
{
LWIP_PBUF_REF_T ref;
SYS_ARCH_DECL_PROTECT(old_level);
SYS_ARCH_PROTECT(old_level);
/* 减少引用计数(指向 pbuf 的指针数) */
ref = --(p->ref);
SYS_ARCH_UNPROTECT(old_level);
if (ref == 0)
{
/* 为了下一次迭代,请记住链中的下一个 pbuf */
q = p->next;
alloc_src = pbuf_get_allocsrc(p);
#if LWIP_SUPPORT_CUSTOM_PBUF
if ((p->flags & PBUF_FLAG_IS_CUSTOM) != 0)
{
struct pbuf_custom *pc = (struct pbuf_custom *)p;
pc->custom_free_function(p);
}
else
#endif /* LWIP_SUPPORT_CUSTOM_PBUF */
{
/* 判断释放的内存池的类型 */
if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL)
{
memp_free(MEMP_PBUF_POOL, p);
/* is this a ROM or RAM referencing pbuf? */
}
else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF)
{
memp_free(MEMP_PBUF, p);
/* type == PBUF_RAM */
}
else if (alloc_src == PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP)
{
mem_free(p);
}
else
{
}
}
count++;
/* 继续到下一个 pbuf */
p = q;
}
else
{
p = NULL;
}
}
return count;
}
硬件接口初始化
前面铺垫了内存管理、网卡、数据包等一堆的知识点就是为了能更好理解LWIP的启动流程。对于其中流程第一步其实很简单就是:初始化硬件接口,如下图就是之前所说的STM32实现以太网的框图,以及Mac内核与PHY的硬件连接图。所以首先,必须先初始化硬件接口。下面以LAN8720A的RMII接口初始化为例。
第一步:设置系统时钟、以太网的工作模式、速度、介质(这里使用RMII)
在LWIP分析一中主要说明了速度与工作模式主要是通过SMI站管理器上的MDIO接口实现的,通过MDIO接口给PHY的第31个寄存器的2:4字段写实现控制。这些操作都在HAL_ETH_Init函数中实现
HAL_StatusTypeDef HAL_ETH_Init(ETH_HandleTypeDef *heth)
{
uint32_t tmpreg1 = 0U, phyreg = 0U;
uint32_t hclk = 60000000U;
uint32_t tickstart = 0U;
uint32_t err = ETH_SUCCESS;
// 如果以太网接口状态为复位状态
if(heth->State == HAL_ETH_STATE_RESET)
{
/* 分配锁资源并且初始化它 */
heth->Lock = HAL_UNLOCKED;
// 如果使能了注册以太网时候会产生回调函数
#if (USE_HAL_ETH_REGISTER_CALLBACKS == 1)
ETH_InitCallbacksToDefault(heth);
if(heth->MspInitCallback == NULL)
{
// 初始化时钟、GPIO、中断
heth->MspInitCallback = HAL_ETH_MspInit;
}
// 初始化完成后使用回调函数
heth->MspInitCallback(heth);
#else
// 如果没有初始化注册回调函数,那么直接初始化GPIO、时钟、中断即可
HAL_ETH_MspInit(heth);
#endif
}
/* 使能系统时钟*/
__HAL_RCC_SYSCFG_CLK_ENABLE();
/* Select MII or RMII Mode*/
// 通过系统的PMC 寄存器以及初始化函数HAL_ETH_MspInit实现MII或者RMII 这里使用RMII
SYSCFG->PMC &= ~(SYSCFG_PMC_MII_RMII_SEL);
SYSCFG->PMC |= (uint32_t)heth->Init.MediaInterface;
/* 以太网软件复位 /
/ 设置SWR位:复位所有MAC子系统的内部寄存器和逻辑 /
/ 复位后,所有寄存器将保持其相应的复位值 */
(heth->Instance)->DMABMR |= ETH_DMABMR_SR;
/*-------------------------------- MAC 初始化----------------------*/
/* 获得以太网MACMIIAR的值 */
tmpreg1 = (heth->Instance)->MACMIIAR;
/* Clear CSR Clock Range CR[2:0] bits */
// 清除CSR时钟寄存器的CR【0:2】位
tmpreg1 &= ETH_MACMIIAR_CR_MASK;
/* 获取时钟频率的值*/
hclk = HAL_RCC_GetHCLKFreq();
// 下面都是配置时钟操作
/* Set CR bits depending on hclk value */
if((hclk >= 20000000U)&&(hclk < 35000000U))
{
/* CSR Clock Range between 20-35 MHz */
tmpreg1 |= (uint32_t)ETH_MACMIIAR_CR_Div16;
}
else if((hclk >= 35000000U)&&(hclk < 60000000U))
{
/* CSR Clock Range between 35-60 MHz */
tmpreg1 |= (uint32_t)ETH_MACMIIAR_CR_Div26;
}
else if((hclk >= 60000000U)&&(hclk < 100000000U))
{
/* CSR Clock Range between 60-100 MHz */
tmpreg1 |= (uint32_t)ETH_MACMIIAR_CR_Div42;
}
else if((hclk >= 100000000U)&&(hclk < 150000000U))
{
/* CSR Clock Range between 100-150 MHz */
tmpreg1 |= (uint32_t)ETH_MACMIIAR_CR_Div62;
}
else /* ((hclk >= 150000000)&&(hclk <= 183000000)) */
{
/* CSR Clock Range between 150-183 MHz */
tmpreg1 |= (uint32_t)ETH_MACMIIAR_CR_Div102;
}
/* Write to ETHERNET MAC MIIAR: Configure the ETHERNET CSR Clock Range */
(heth->Instance)->MACMIIAR = (uint32_t)tmpreg1;
/*-------------------- PHY 初始化以及配置 ----------------*/
/* 重置PHY模式 */
if((HAL_ETH_WritePHYRegister(heth, PHY_BCR, PHY_RESET)) != HAL_OK)
{
/* Config MAC and DMA */
// 初始化MAC和DMA
ETH_MACDMAConfig(heth, err);
/* Set the ETH peripheral state to READY */
// 设置以太网外围状态为就绪状态
heth->State = HAL_ETH_STATE_READY;
}
// 注意 这里的函数非常重要。也通过对PHY的BCR寄存器进行写的操作实现了通信的速度以及双工模式
// 设置双工
if((phyreg & PHY_DUPLEX_STATUS) != (uint32_t)RESET)
{
/* Set Ethernet duplex mode to Full-duplex following the auto-negotiation */
(heth->Init).DuplexMode = ETH_MODE_FULLDUPLEX;
}
else
{
/* Set Ethernet duplex mode to Half-duplex following the auto-negotiation */
(heth->Init).DuplexMode = ETH_MODE_HALFDUPLEX;
}
/* 设置速度*/
if((phyreg & PHY_SPEED_STATUS) == PHY_SPEED_STATUS)
{
/* Set Ethernet speed to 10M following the auto-negotiation */
(heth->Init).Speed = ETH_SPEED_10M;
}
else
{
/* Set Ethernet speed to 100M following the auto-negotiation */
(heth->Init).Speed = ETH_SPEED_100M;
}
}
/* Config MAC and DMA */
ETH_MACDMAConfig(heth, err);
/* Set ETH HAL State to Ready */
heth->State= HAL_ETH_STATE_READY;
/* Return function status */
return HAL_OK;
}
第二步引脚初始化
ETH_HandleTypeDef 结构体可能包含以下元素:
-
heth:以太网模块的句柄。它用于标识和访问特定的以太网模块实例。 -
Init:以太网模块的初始化参数。这些参数包括以太网模块的工作模式、速度、全双工或半双工模式等。 -
Instance:以太网模块的实例号。在某些情况下,可能存在多个以太网模块实例,通过实例号可以区分它们。 -
State:以太网模块的状态。这可能是一个枚举值,表示以太网模块当前的状态,例如初始化完成、发送中、接收中等。 -
MACAddr:以太网模块的MAC地址。这是一个包含6个字节的数组,用于唯一标识以太网模块在网络中的物理地址。 -
TxDesc:以太网发送描述符。这是一个指向发送描述符的指针,描述符用于管理发送数据包的信息。 -
RxDesc:以太网接收描述符。这是一个指向接收描述符的指针,描述符用于管理接收数据包的信息。
typedef struct
{
ETH_DMADescTypeDef *FSRxDesc; /*!< First Segment Rx Desc */
ETH_DMADescTypeDef *LSRxDesc; /*!< Last Segment Rx Desc */
uint32_t SegCount; /*!< Segment count */
uint32_t length; /*!< Frame length */
uint32_t buffer; /*!< Frame buffer */
} ETH_DMARxFrameInfos;
/**
* @brief ETH Handle Structure definition
*/
#if (USE_HAL_ETH_REGISTER_CALLBACKS == 1)
typedef struct __ETH_HandleTypeDef
#else
typedef struct
#endif /* USE_HAL_ETH_REGISTER_CALLBACKS */
{
ETH_TypeDef *Instance; /*!< Register base address */
ETH_InitTypeDef Init; /*!< Ethernet Init Configuration */
uint32_t LinkStatus; /*!< Ethernet link status */
ETH_DMADescTypeDef *RxDesc; /*!< Rx descriptor to Get */
ETH_DMADescTypeDef *TxDesc; /*!< Tx descriptor to Set */
ETH_DMARxFrameInfos RxFrameInfos; /*!< last Rx frame infos */
__IO HAL_ETH_StateTypeDef State; /*!< ETH communication state */
HAL_LockTypeDef Lock; /*!< ETH Lock */
#if (USE_HAL_ETH_REGISTER_CALLBACKS == 1)
void (* TxCpltCallback) ( struct __ETH_HandleTypeDef * heth); /*!< ETH Tx Complete Callback */
void (* RxCpltCallback) ( struct __ETH_HandleTypeDef * heth); /*!< ETH Rx Complete Callback */
void (* DMAErrorCallback) ( struct __ETH_HandleTypeDef * heth); /*!< DMA Error Callback */
void (* MspInitCallback) ( struct __ETH_HandleTypeDef * heth); /*!< ETH Msp Init callback */
void (* MspDeInitCallback) ( struct __ETH_HandleTypeDef * heth); /*!< ETH Msp DeInit callback */
#endif
__lwip_dev g_lwipdev; /* lwip控制结构体 */
/*lwip控制结构体*/
typedef struct
{
uint8_t mac[6]; /* MAC地址 */
uint8_t remoteip[4]; /* 远端主机IP地址 */
uint8_t ip[4]; /* 本机IP地址 */
uint8_t netmask[4]; /* 子网掩码 */
uint8_t gateway[4]; /* 默认网关的IP地址 */
uint8_t dhcpstatus; /* dhcp状态 */
/* 0, 未获取DHCP地址;*/
/* 1, 进入DHCP获取状态*/
/* 2, 成功获取DHCP地址*/
/* 0XFF,获取失败 */
uint8_t link_status; /* 连接状态 */
display_fn lwip_display_fn; /* 显示函数指针 */
}__lwip_dev;
/**
* @brief ETH底层驱动,时钟使能,引脚配置
* @note 此函数会被HAL_ETH_Init()调用
* @param heth:以太网句柄
* @retval 无
*/
void HAL_ETH_MspInit(ETH_HandleTypeDef *heth)
{
GPIO_InitTypeDef gpio_init_struct;
ETH_CLK_GPIO_CLK_ENABLE(); /* 开启ETH_CLK时钟 */
ETH_MDIO_GPIO_CLK_ENABLE(); /* 开启ETH_MDIO时钟 */
ETH_CRS_GPIO_CLK_ENABLE(); /* 开启ETH_CRS时钟 */
ETH_MDC_GPIO_CLK_ENABLE(); /* 开启ETH_MDC时钟 */
ETH_RXD0_GPIO_CLK_ENABLE(); /* 开启ETH_RXD0时钟 */
ETH_RXD1_GPIO_CLK_ENABLE(); /* 开启ETH_RXD1时钟 */
ETH_TX_EN_GPIO_CLK_ENABLE(); /* 开启ETH_TX_EN时钟 */
ETH_TXD0_GPIO_CLK_ENABLE(); /* 开启ETH_TXD0时钟 */
ETH_TXD1_GPIO_CLK_ENABLE(); /* 开启ETH_TXD1时钟 */
ETH_RESET_GPIO_CLK_ENABLE(); /* 开启ETH_RESET时钟 */
__HAL_RCC_ETH_CLK_ENABLE(); /* 开启ETH时钟 */
/* 网络引脚设置 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 = ETH_CLK_GPIO_PIN;
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(ETH_CLK_GPIO_PORT, &gpio_init_struct); /* ETH_CLK引脚模式设置 */
gpio_init_struct.Pin = ETH_MDIO_GPIO_PIN;
HAL_GPIO_Init(ETH_MDIO_GPIO_PORT, &gpio_init_struct); /* ETH_MDIO引脚模式设置 */
gpio_init_struct.Pin = ETH_CRS_GPIO_PIN;
HAL_GPIO_Init(ETH_CRS_GPIO_PORT, &gpio_init_struct); /* ETH_CRS引脚模式设置 */
/* PC1 */
gpio_init_struct.Pin = ETH_MDC_GPIO_PIN;
HAL_GPIO_Init(ETH_MDC_GPIO_PORT, &gpio_init_struct); /* ETH_MDC初始化 */
/* PC4 */
gpio_init_struct.Pin = ETH_RXD0_GPIO_PIN;
HAL_GPIO_Init(ETH_RXD0_GPIO_PORT, &gpio_init_struct); /* ETH_RXD0初始化 */
/* PC5 */
gpio_init_struct.Pin = ETH_RXD1_GPIO_PIN;
HAL_GPIO_Init(ETH_RXD1_GPIO_PORT, &gpio_init_struct); /* ETH_RXD1初始化 */
/* PG11,13,14 */
gpio_init_struct.Pin = ETH_TX_EN_GPIO_PIN;
HAL_GPIO_Init(ETH_TX_EN_GPIO_PORT, &gpio_init_struct); /* ETH_TX_EN初始化 */
gpio_init_struct.Pin = ETH_TXD0_GPIO_PIN;
HAL_GPIO_Init(ETH_TXD0_GPIO_PORT, &gpio_init_struct); /* ETH_TXD0初始化 */
gpio_init_struct.Pin = ETH_TXD1_GPIO_PIN;
HAL_GPIO_Init(ETH_TXD1_GPIO_PORT, &gpio_init_struct); /* ETH_TXD1初始化 */
/* 复位引脚 */
gpio_init_struct.Pin = ETH_RESET_GPIO_PIN; /* ETH_RESET初始化 */
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 无上下拉 */
gpio_init_struct.Speed = GPIO_SPEED_HIGH; /* 高速 */
HAL_GPIO_Init(ETH_RESET_GPIO_PORT, &gpio_init_struct);
ETHERNET_RST(0); /* 硬件复位 */
delay_ms(50);
ETHERNET_RST(1); /* 复位结束 */
HAL_NVIC_SetPriority(ETH_IRQn, 6, 0); /* 网络中断优先级应该高一点 */
HAL_NVIC_EnableIRQ(ETH_IRQn);
}
/**
* @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) //HAL_ETH_MspInitHAL_ETH_Init调用
{
return 0; /* 成功 */
}
else
{
return 1; /* 失败 */
}
}
/**
* @breif 中断服务函数
* @param 无
* @retval 无
*/
void ETH_IRQHandler(void)
{
if (ethernet_get_eth_rx_size(g_eth_handler.RxDesc))
{
lwip_pkt_handle(); /* 处理以太网数据,即将数据提交给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接收中断标志位 */
}
第三步:TXFIFO、RXFIFO 源码。主要是记录TX/RX FIFO的数据究竟存在于哪里。
ETH_HandleTypeDef g_eth_handler; /* 以太网句柄 */
ETH_DMADescTypeDef *g_eth_dma_rx_dscr_tab; /* 以太网DMA接收描述符数据结构体指针 */
ETH_DMADescTypeDef *g_eth_dma_tx_dscr_tab; /* 以太网DMA发送描述符数据结构体指针 */
uint8_t *g_eth_rx_buf; /* 以太网底层驱动接收buffers指针 */
uint8_t *g_eth_tx_buf; /* 以太网底层驱动发送buffers指针 */
// 第一步:申请 TX、RX的内存,这里采用了内存池的分配方式,分配了两个四个内存块,g_eth_dma_rx_dscr_tab与g_eth_dma_tx_dscr_tab 是存储以太网DMA描述符内存块,这两个tab就是的地址就是TX-FIFO 与RX-FIFO的地址,而g_eth_rx_buf与g_eth_tx_buf 就是用户要接收或者发送的数据一般都是以链表形式存储
/**
* @breif 为ETH底层驱动申请内存
* @param 无
* @retval 0,正常
* 1,失败
*/
uint8_t ethernet_mem_malloc(void)
{
if ((g_eth_dma_rx_dscr_tab || g_eth_dma_tx_dscr_tab || g_eth_rx_buf || g_eth_tx_buf) == NULL)
{
g_eth_dma_rx_dscr_tab = mymalloc(SRAMIN, ETH_RXBUFNB * sizeof(ETH_DMADescTypeDef)); /* 申请内存 */
g_eth_dma_tx_dscr_tab = mymalloc(SRAMIN, ETH_TXBUFNB * sizeof(ETH_DMADescTypeDef)); /* 申请内存 */
g_eth_rx_buf = mymalloc(SRAMIN, ETH_RX_BUF_SIZE * ETH_RXBUFNB); /* 申请内存 */
g_eth_tx_buf = mymalloc(SRAMIN, ETH_TX_BUF_SIZE * ETH_TXBUFNB); /* 申请内存 */
if (!(uint32_t)&g_eth_dma_rx_dscr_tab || !(uint32_t)&g_eth_dma_tx_dscr_tab || !(uint32_t)&g_eth_rx_buf || !(uint32_t)&g_eth_tx_buf)
{
ethernet_mem_free();
return 1; /* 申请失败 */
}
}
return 0; /* 申请成功 */
}
// 第二步:在ethernetif_init 的初始化中会调用函数,这个函数会初始化发送以及接收的描述符,开始DMA传输。简单点说就是将以太网DMA描述符g_eth_dma_rx_dscr_tab 、g_eth_dma_tx_dscr_tab 的存储地址交给以太网句柄下的元素RxCpltCallback、TxCpltCallback 方便用户通过以太网句柄调用数据。
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功能*/
/* 创建一个信号量 */
g_rx_semaphore = xSemaphoreCreateBinary();
/* 创建处理ETH_MAC的任务 */
sys_thread_new("eth_thread",
ethernetif_input, /* 任务入口函数 */
netif, /* 任务入口函数参数 */
NETIF_IN_TASK_STACK_SIZE,/* 任务栈大小 */
NETIF_IN_TASK_PRIORITY); /* 任务的优先级 */
/* 网卡状态信息标志位,是很重要的控制字段,它包括网卡功能使能、广播*/
/* 使能、 ARP 使能等等重要控制位*/
netif->flags = NETIF_FLAG_BROADCAST|NETIF_FLAG_ETHARP|NETIF_FLAG_LINK_UP; /*广播 ARP协议 链接检测*/
HAL_ETH_DMATxDescListInit(&g_eth_handler,g_eth_dma_tx_dscr_tab,g_eth_tx_buf,ETH_TXBUFNB); /*初始化发送描述符*/
HAL_ETH_DMARxDescListInit(&g_eth_handler,g_eth_dma_rx_dscr_tab,g_eth_rx_buf,ETH_RXBUFNB); /*初始化接收描述符*/
HAL_ETH_Start(&g_eth_handler); /*开启MAC和DMA*/
}
最后,总结一下源码中的其中流程
-
申请内存ethernet_mem_malloc:申请以太网DMA描述符[g_eth_dma_rx_dscr_tab、g_eth_dma_tx_dscr_tab]、数据缓冲区buf[g_eth_rx_buf、g_eth_tx_buf]。 -
初始化以太网芯片ethernet_init:调用HAL_ETH_Init(g_eth_handler)[g_eth_handler是以太网句柄]函数。这个函数实现对以太网句柄中的元素进行初始化:如自动协商、以太网速度、双工模式、PHY地址、MAC地址、RMII接口进行初始化。接着HAL_ETH_Init 会调用HAL_ETH_MspInit 函数使能系统需要用到的时钟、GPIO初始化等。然后、直接根据上面的初始化信息对系统时钟、速度、双工、RMII等属性的寄存器底层操作。最后开启DMA -
添加网卡:主要是想通过网卡结构中的netif_input_fn input 与g_eth_dma_rx_dscr_tab 地址对应,实现通过网卡进行数据的处理。