Fork me on GitHub
0%

ENC28J60及Lwip协议栈移植适配

整体架构

image

image

硬件

对于硬件连接不需要进行过多的赘述,只需要将管脚正确连接即可。电源方面:3V3、GND和RESET管脚,对于ENC28J60有软件复位功能,但是两个RESET的功能不一致,所以将硬件RESET管脚连接起来保证功能的稳定性;数据传输SPI方面:CLK、MISO、MOSI和CS;其它:INT,这个中断的作用是当ENC28J60有数据要发送给MCU时或者存在一些状态错误的时候,这个中断会产生。当然,如果MCU软件部分采用轮询模式,循环查询ENC28J60 某个寄存器是否有数据,则不需要连接这个管脚。但是建议使用中断模式,提高主控效率。

软件

  • 驱动层。实现硬件抽象和必要的数据发送接收函数。
  • 协议栈。根据驱动函数实现协议栈的初始化、数据发送、数据接收接口。
  • 应用层。使用协议栈的API编写Demo,包括添加网卡、网络配置、DHCP或静态IP配置、数据通讯等等。

SPI驱动

SPI驱动是依赖具体的MCU型号的,所以只需要根据提供的驱动代码进行编程即可。主要实现的函数为:SPI初始化、SPI发送数据、SPI接收数据。

  • 初始化函数需要实现SPI的配置信息。ENC28J60的SPI配置信息固定:时钟相位(0,0)、MSB first(大端)、CS低有效、最大传输速率10Mb/s
  • 发送接收函数直接调用驱动接口进一步是实现即可。传输方式(DMA/CPU)、接收中断(下降沿有效)

ENC28J60网卡驱动

网卡驱动厂商一般都会提供网卡驱动代码,而网卡驱动代码的流程都是有迹可循的,对照手册查看时序和寄存器操作流程即可。而需要用户实现就是基于特定的MCU平台调用spi相关函数实现 enc_readopc、enc_writeopc、enc_readbuf、enc_writebuf四个函数。

整体上,ENC28J60包括MAC和PHY,要想访问到特定的寄存器需要 bank + 地址两部分组成,对于其8K的RAM访问需要用户编程写寄存器来划分发送和接收缓冲区的边界,并读写数据。

Lwip协议栈

关于Lwip协议栈的移植在网上有比较多的示例。最终用户只需要自己实现ethernet.c文件里面的5个函数即可:

1
2
3
4
5
static void low_level_init(struct netif *netif)
static err_t low_level_output(struct netif *netif, struct pbuf *p)
static struct pbuf * low_level_input(struct netif *netif)
void ethernetif_input(void *pParams)
err_t ethernetif_init(struct netif *netif)

low_level_init

网卡的初始化函数。它主要用来完成网卡复位及参数初始化,主要包含以下几个步骤

1
2
3
4
5
6
7
8
9
static void low_level_init(struct netif *netif)
{
// 设置标志位
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
// 将 ethernetif_input 作为单独的线程跑起来(数据接收)
thread_create("receive data", 1024*4,(thread_func_t)ethernetif_input, netif);
// 网卡初始化
ENC28J60_Init(netif->hwaddr);
}

low_level_output

网卡数据包的发送函数。将内核数据包pbuf发送出去,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static unsigned char send_buffer[1500];
static err_t low_level_output(struct netif *netif, struct pbuf *p)
{
struct pbuf *q = NULL;
unsigned int templen = 0;
// 遍历所有的pbuf,将数据重新排列保证不丢数据
for(q = p;q != NULL;q = q->next) {
memcpy(&send_buffer[templen],q->payload,q->len);
templen +=q->len;
if(templen>1500 || templen > p->tot_len) {
┆ LWIP_DEBUGF(NETIF_DEBUG, ("PacketSend : error; templen = %d tot_len = %d", templen, p->tot_len));
}
}
/* 调用网卡发送函数,将数据发送出去 */
if(templen == p->tot_len) {
ENCPacketSend(send_buffer, templen);
}
}

low_level_input

网卡数据包接收函数。需要将网卡传过来的数据封装成pbuf的形式传给协议栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static unsigned char recv_buffer[2000];
static struct pbuf* low_level_input(struct netif *netif)
{
struct pbuf* p, q;
unsigned int recvlen, i;
// 调用网卡接收函数
recvlen = ENCPacketRcv(recv_buffer,2000);
// 申请pbuf所需要的内存空间
p = pbuf_alloc(PBUF_RAW,recvlen,PBUF_RAM);
q = p;
while(q!=NULL) {
// 封装pbuf结构体
memcpy(q->payload,&recv_buffer[i],q->len);
┆ i += q->len;
// 下一个pload
┆ q = q->next;
if(i>recvlen) break;
}
return p;
}

ethernetif_input

调用网卡数据包接收函数 low_level_input 从网卡处读取一个数据包。

对于数据接受来讲,如果使用中断:ETH_IRQHandler() → ethernetif_input() → netif->input(p, netif) → tcpip_input() → tcpip_inpkt() → tcpip_thread()。当以太网有数据需要Master来接收,首先给主机端产生一个中断,主机端需要做的是将以太网接受数据的过程放在ethernetif_input函数中实现,后面都是通用流程一步步调用,最终将数据返回给应用层。
这里还需要注意的是,这里接受数据的过程将可能是耗时的操作,不能放在中断服务程序里面进行,要么使用delay work要么使用thread进行实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ethernetif_input(void* param)
{
struct pbuf *p = NULL;
struct netif* netif = (struct netif*)param;
while(1) {
// 等待网卡中断或者循环读取网卡寄存器,判断是否有可读数据。满足条件继续执行
TRY_GET_NEXT_FRAGMENT:
// 调用 low_level_input 函数接收数据
┆ p = low_level_input(netif);
// 将数据发送到协议栈
if (netif->input(p, netif) != ERR_OK) {
┆ ┆ pbuf_free(p);
┆ ┆ p = NULL;
┆ }
else
┆ ┆ goto TRY_GET_NEXT_FRAGMENT;
}
}

ethernetif_init

网卡初始化函数。需要完成netif字段的初始化,该函数在上层 netif_add 的时候会调用到

1
2
3
4
5
6
7
8
9
10
err_t ethernetif_init(struct netif *netif)
{
netif->name[0] = IFNAME0;
netif->name[1] = IFNAME1;
netif->output = etharp_output;
// 设置回调函数
netif->linkoutput = low_level_output;
// 调用初始化
low_level_init(netif);
}

应用层

展示DHCP配网的基本流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void dhcp_config(uint8_t* hwaddr)
{
static struct netif netif;
ip_addr_t ipaddr, gw, netmask;
// Start lwip
tcpip_init(NULL, NULL);
// Set data zero
ip_addr_set_zero_ip4(&gw);
ip_addr_set_zero_ip4(&ipaddr);
ip_addr_set_zero_ip4(&netmask);
// Add netif
netif_add(&netif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input);
/* Registers the default network interface */
netif_set_default(&netif);
if (netif_is_link_up(&netif)) {
/* When the netif is fully configured this function must be called */
┆ netif_set_up(&netif);
} else {
/* When the netif link is down this function must be called */
┆ netif_set_down(&netif);
}
// Start dhcp service
dhcp_start(&netif);
}

FAQ

  1. 分析发现MCU可以从ENC28J60接收到数据包,但是格式不正确?

    分析:多次测试查看数据包内容是否一致。如果不一致可能是中断导致,中断响应不及时等原因;如果一致则可能是发送数据包不正确、数据发送或者接收流程有问题。

    定位:对于ENC28J60有一个8K RAM,按照数据手册将数据从接收缓冲区全部读出验证发现数据是正确的,但是解析不正确,最终定位到ENC_readOpc接口有一个无效字节的问题,对于read buffer所有数据都是有效的,调用这个接口就把有效数据丢掉了进而导致的问题的出现。

  2. 数据不正确的调试方法?

    • 信号质量。包括电压大小、信号稳定性等等
    • 确认数据不正确是错的一致还是错的不一致,进而确认问题是随机产生还是可能配置导致的bug
    • 对比正确的数据格式,分析可能存在的配置错误
  3. ping命令显示主机不可达并且一段时间自行恢复?

    首先可以通过 arp -n 命令查看arp缓存,这种要么是网络存在问题要么就是防火墙或者arp的问题。将设备上Lwip协议栈中 DHCP_DOES_ARP_CHECK 这个宏的配置改成1即可

参考资料

  1. ENC28J60中文手册
  2. ENC28J60学习笔记——AVRNET项目
  3. 《lwip学习1》-数据流篇