LwIP学习笔记——STM32 ENC28J60移植与入门

0.前言
    去年(2013年)的整理了LwIP相关代码,并在STM32上“裸奔”成功。一直没有时间深入整理,在这里借博文整理总结。LwIP的移植过程细节很多,博文也不可能一一详解个别部分只能点到为止。
    【本文要点】
    【1】不带操作系统的LwIP移植,LwIP版本为1.4.1。
    【2】MCU为STM32F103VE,网卡为ENC28J60。
    【3】移植过程重点描述ethernetif.c和LwIP宏配置等。
    【4】一个简单的TCP echo例子。
    【5】力求简单,没有DHCP功能,甚至没有用到网卡中断。
    【代码仓库】
    代码仓库位于 Bitbucket(要源代码请点击这里)。博文中不能把每个细节描述清楚,更多内容请参考代码仓库中的具体代码。
    【硬件说明】
    测试平台使用奋斗版,原理图请参考代码仓库中的DOC目录。

    【参考博文】
    学习嵌入式网络是一个循序渐进的过程,从浅入深从简单到复杂。
    【1】 ENC28J60学习笔记——学习网卡
    【2】 STM32NET学习笔记——索引——理解TCPIP协议栈
    【3】 uIP学习笔记——初次应用协议栈
    【4】 Yeelink平台使用——远程控制 RT Thread + LwIP+ STM32——更加实用的做法


1.ethernetif.c的相关修改
    虽然LwIP移植过程比较复杂,但是只要结合网卡具体功能,耐心修改ethernetif.c即可。ethernetif.c重点实现网卡的三个功能,初始化,发送和接收。
    为了更好的配合lwIP,修改了 ENC28J60学习笔记中部分驱动函数。(换句话说,想要从0开始移植LwIP必须对操作网卡非常熟悉)
    【1】初始化
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 */
  netif->hwaddr[0] = 'A';
  netif->hwaddr[1] = 'R';
  netif->hwaddr[2] = 'M';
  netif->hwaddr[3] = 'N';
  netif->hwaddr[4] = 'E';
  netif->hwaddr[5] = 'T';
 
  /* 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_LINK_UP;
 
  /* Do whatever else is needed to initialize interface. */
  enc28j60_init(netif->hwaddr); // 【1】
}
    【说明】
        【1】 enc28j60_init(netif->hwaddr); low_level_init中指定了enc28j60中的网卡地址。

    【2】发送
static err_t
low_level_output(struct netif *netif, struct pbuf *p)
{
  struct ethernetif *ethernetif = netif->state;
  struct pbuf *q;

  enc28j60_init_send(p->tot_len); //【1】initiate transfer();
 
#if ETH_PAD_SIZE
  pbuf_header(p, -ETH_PAD_SIZE); /* drop the padding word */
#endif

  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. */
    enc28j60_writebuf( q->payload, q->len ); //【2】send data from(q->payload, q->len);
  }

  enc28j60_start_send(); //【3】signal that packet should be sent();

#if ETH_PAD_SIZE
  pbuf_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif
 
  LINK_STATS_INC(link.xmit);

  return ERR_OK;
}
    【说明】
        【1】enc28j60_init_send(p->tot_len); 初始化发送缓冲区大小, pbuf结构为一个链表,第一个pbuf结构体中的tot_len字段代表整个以太网数据包的大小。
        【2】enc28j60_writebuf( q->payload, q->len ); 通过遍历链表把内容填入ENC28J60的缓冲区中。
        【3】enc28j60_start_send();启动网卡发送。

    【3】接收
static struct pbuf *
low_level_input(struct netif *netif)
{
  struct ethernetif *ethernetif = netif->state;
  struct pbuf *p, *q;
  u16_t len;

  len = enc28j60_packet_getlen(); // 【1】

#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_header(p, -ETH_PAD_SIZE); /* drop the padding word */
#endif

    for(q = p; q != NULL; q = q->next) {
      enc28j60_readbuf (q->payload, q->len ); //【2】read data into(q->payload, q->len);
    }
    enc28j60_finish_receive(); //【3】acknowledge that packet has been read();

#if ETH_PAD_SIZE
    pbuf_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif

    LINK_STATS_INC(link.recv);
  } else {
    enc28j60_finish_receive(); //【4】drop packet();
    LINK_STATS_INC(link.memerr);
    LINK_STATS_INC(link.drop);
  }

  return p;
}
    【说明】
        【1】len = enc28j60_packet_getlen(); 获得网卡中数据包的长度。
        【2】enc28j60_readbuf (q->payload, q->len);把网卡中的内容复制到内存池中。
        【3】enc28j60_finish_receive();接收完成,移动网卡中缓冲区指针。

    【4】应用
        【1】LwIP网卡硬件初始化调用ethernetif_init即可,该函数中调用了low_level_init,并指定了网卡输出函数low_level_output。
        【2】一旦网卡有数据进入,应立即代用ethernetif_input函数。可以使用中断方法或查询方法。

2.lwipopt.h配置简述
    lwip中的配置选项非常的多,了解所有的配置非常不容易。本博文参考STM32官方的两个例子总结得到。  
#ifndef __LWIPOPTS_H__
#define __LWIPOPTS_H__

#define SYS_LIGHTWEIGHT_PROT 0

#define NO_SYS 1

#define NO_SYS_NO_TIMERS 1

/* ---------- Memory options ---------- */
/* MEM_ALIGNMENT: should be set to the alignment of the CPU for which
   lwIP is compiled. 4 byte alignment -> define MEM_ALIGNMENT to 4, 2
   byte alignment -> define MEM_ALIGNMENT to 2. */
#define MEM_ALIGNMENT 4

/* MEM_SIZE: the size of the heap memory. If the application will send
a lot of data that needs to be copied, this should be set high. */
#define MEM_SIZE (5*1024)

/* MEMP_NUM_PBUF: the number of memp struct pbufs. If the application
   sends a lot of data out of ROM (or other static memory), this
   should be set high. */
#define MEMP_NUM_PBUF 10
/* MEMP_NUM_UDP_PCB: the number of UDP protocol control blocks. One
   per active UDP "connection". */
#define MEMP_NUM_UDP_PCB 6
/* MEMP_NUM_TCP_PCB: the number of simulatenously active TCP
   connections. */
#define MEMP_NUM_TCP_PCB 10
/* MEMP_NUM_TCP_PCB_LISTEN: the number of listening TCP
   connections. */
#define MEMP_NUM_TCP_PCB_LISTEN 6
/* MEMP_NUM_TCP_SEG: the number of simultaneously queued TCP
   segments. */
#define MEMP_NUM_TCP_SEG 12
/* MEMP_NUM_SYS_TIMEOUT: the number of simulateously active
   timeouts. */
#define MEMP_NUM_SYS_TIMEOUT 3


/* ---------- Pbuf options ---------- */
/* PBUF_POOL_SIZE: the number of buffers in the pbuf pool. */
#define PBUF_POOL_SIZE 10

/* PBUF_POOL_BUFSIZE: the size of each pbuf in the pbuf pool. */
#define PBUF_POOL_BUFSIZE 1500


/* ---------- TCP options ---------- */
#define LWIP_TCP 1
#define TCP_TTL 255

/* Controls if TCP should queue segments that arrive out of
   order. Define to 0 if your device is low on memory. */
#define TCP_QUEUE_OOSEQ 0

/* TCP Maximum segment size. */
#define TCP_MSS (1500 - 40)	 /* TCP_MSS = (Ethernet MTU - IP header size - TCP header size) */

/* TCP sender buffer space (bytes). */
#define TCP_SND_BUF (2*TCP_MSS)

/* TCP sender buffer space (pbufs). This must be at least = 2 *
   TCP_SND_BUF/TCP_MSS for things to work. */
#define TCP_SND_QUEUELEN (6 * TCP_SND_BUF)/TCP_MSS

/* TCP receive window. */
#define TCP_WND (2*TCP_MSS)


/* ---------- ICMP options ---------- */
#define LWIP_ICMP 1

/* ---------- DHCP options ---------- */
/* Define LWIP_DHCP to 1 if you want DHCP configuration of
   interfaces. DHCP is not implemented in lwIP 0.5.1, however, so
   turning this on does currently not work. */
#define LWIP_DHCP 0

/* ---------- UDP options ---------- */
#define LWIP_UDP 1
#define UDP_TTL 255

/* ---------- Statistics options ---------- */
#define LWIP_STATS 0
#define LWIP_PROVIDE_ERRNO 1

/**
 * LWIP_NETCONN==1: Enable Netconn API (require to use api_lib.c)
 */
#define LWIP_NETCONN 0

/**
 * LWIP_SOCKET==1: Enable Socket API (require to use sockets.c)
 */
#define LWIP_SOCKET 0

#endif /* __LWIPOPTS_H__ */
    【具体说明和修改】
    【1】未使用操作系统,所有NO_SYS定义为1,LWIP_NETCONN定义为0(表示不使用),LWIP_SOCKET定义为0(表示不使用)。
    【2】NO_SYS_NO_TIMERS定义为1,该定义为LwIP1.4.0以上版本增加,具体可参考LwIP修改文档。
    【3】LWIP_DHCP被定义为0,关闭了DHCP功能以简化代码。
    【4】相比STM32官方例子,去除了校验码相关配置全部使用软件校验。STM32官方案例中使用了代码EMAC功能的MCU,该系列MCU中包括硬件校验功能,但是ENC28J60并没有此功能,所以只能开启LwIP中的软件校验功能。
    
3.LwIP相关初始化
void LwIP_Config (void)
{
    struct ip_addr ipaddr;
    struct ip_addr netmask;
    struct ip_addr gw;
   
    // 调用LWIP初始化函数
    lwip_init();
   
    IP4_ADDR(&ipaddr, 192, 168, 1, 16); // 设置网络接口的ip地址
    IP4_ADDR(&netmask, 255, 255, 255, 0);	 // 子网掩码
    IP4_ADDR(&gw, 192, 168, 1, 1);	 // 网关
   
    // 初始化enc28j60与LWIP的接口,参数为网络接口结构体、ip地址、
    // 子网掩码、网关、网卡信息指针、初始化函数、输入函数
    netif_add(&enc28j60, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_input);
   
    // 把enc28j60设置为默认网卡
    netif_set_default(&enc28j60);
   
    netif_set_up(&enc28j60);
}
    【说明】
        【1】通过netif_add初始化网卡IP地址,子网掩码和网关地址。此处使用静态IP地址。
        【2】netif_add需要传入两个函数指针,分别是网卡初始化函数和接收内容处理函数。ethernetif_init位于ethernetif.c而 ethernet_input并不位于ethernetif.c,此处也 不能使用ethernetif_input,其实ethernet_input在函数ethernetif_input被调用,但是ethernet_input变了一个样子:
        netif-> input(p, netif)!=ERR_OK
        【3】在带操作系统的移植中,最后一个参数使用tcpip_input。

4.while(1)部分
    timer_typedef tcp_timer, arp_timer;
   
    /* 设定查询定时器 ARP定时器 */
    timer_set(&tcp_timer, CLOCK_SECOND / 10); // tcp处理定时器 100ms
    timer_set(&arp_timer, CLOCK_SECOND * 5); // arp处理定时器 5s
    while (1) {
       
        if (enc28j60_packet_getcount() != 0) {
            ethernetif_input(&enc28j60);
        }
       
        // TCP 定时处理
        if (timer_expired(&tcp_timer)) {
            timer_set(&tcp_timer, CLOCK_SECOND / 4);
            tcp_tmr();
        }
       
        // ARP 定时处理
        if (timer_expired(&arp_timer)) {
            timer_set(&arp_timer, CLOCK_SECOND * 5);
            etharp_tmr();
        }
    }
    【说明】
    while(1)循环包括3个主要功能
    【1】一旦接受到数据包,立刻调用 ethernetif_input。此处使用查询法而不是中断法(中断法效果相似)
    【2】定期处理TCP链接,定时时间为100ms,可根据情况适当缩小时间间隔。
    【3】定期更新ARP缓冲,可根据情况适当扩大时间间隔。
    【4】此处的timer通过systick实现,具体实现请参考代码仓库。

4.基本测试
    【1】ping实验
    此时网卡的静态IP地址为192.168.1.16,通过ping指令发送16个数据包
    ping 192.168.1.16 -n 16
 
图1 ping实验
    【2】TCP Echo例子
    LwIP提供很多示例,TCP Echo示例位于contrib-1.4.1的apps文件夹中,文件夹名为tcpecho_raw)。修改TCP侦听端口为10086。
    err = tcp_bind(echo_pcb, IP_ADDR_ANY, 10086);

图2 TCP Echo例子

5.总结
    【1】移植和应用LwIP一定要耐心细致。
    【2】一旦网卡接收到数据,应调用ethernetif_input函数,调用该函数让数据进入LwIP协议栈。
    【3】 netif_add(&enc28j60, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &ethernet_input);最后一个参数为ethernet_input,千万必要写成ethernetif_input。

6.参考资料
    更多细节内容请参考图书资料
    【1】《嵌入式网络系统设计——基于Atmel ARM7系列》
    【2】《STM32嵌入式系统开发实战指南——FreeRTOS与LwIP联合移植》

  • 35
    点赞
  • 165
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
要实现stm32f03和enc28j60与PC进行UDP通信,需要按照以下步骤进行: 1. 配置硬件 首先,需要将enc28j60stm32f03进行连接,并对enc28j60进行初始化配置。具体的连接方式和初始化配置可以参考enc28j60的数据手册。 2. 配置lwIP lwIP是一款轻量级的TCP/IP协议栈,可以帮助我们快速地实现网络通信。在配置lwIP时,需要将它与stm32f03进行集成,并设置好IP地址、子网掩码、网关等参数。具体的配置方法可以参考lwIP的官方文档。 3. 编写代码 在配置好硬件和lwIP之后,需要编写代码实现UDP通信。代码的主要逻辑如下: - 初始化UDP协议,并绑定端口号; - 等待PC发送数据; - 接收到数据后进行处理; - 将处理后的数据发送回PC。 具体的代码实现可以参考以下示例代码: ```c #include "lwip/opt.h" #include "lwip/arch.h" #include "lwip/api.h" #include "lwip/sys.h" #include "lwip/udp.h" #include "lwip/tcp.h" #include "netif/etharp.h" #include "ethernetif.h" #include "enc28j60.h" /* IP地址、子网掩码、网关相关参数 */ #define IP_ADDR0 192 #define IP_ADDR1 168 #define IP_ADDR2 1 #define IP_ADDR3 100 #define NETMASK_ADDR0 255 #define NETMASK_ADDR1 255 #define NETMASK_ADDR2 255 #define NETMASK_ADDR3 0 #define GW_ADDR0 192 #define GW_ADDR1 168 #define GW_ADDR2 1 #define GW_ADDR3 1 /* UDP端口号 */ #define UDP_PORT 8888 /* 缓冲区大小 */ #define BUF_SIZE 512 /* 缓冲区 */ static uint8_t buf[BUF_SIZE]; /* 网络接口 */ static struct netif netif; /* UDP回调函数 */ static void udp_echo_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { /* 将数据打印出来 */ printf("[UDP] Received data: %s\n", (char *)p->payload); /* 发送数据回PC */ udp_sendto(pcb, p, addr, port); /* 释放pbuf */ pbuf_free(p); } /* 主函数 */ int main(void) { /* 初始化ENC28J60 */ enc28j60_init(); /* 初始化lwIP协议栈 */ lwip_init(); /* 设置网络接口的IP地址、子网掩码、网关 */ IP4_ADDR(&netif.ip_addr, IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3); IP4_ADDR(&netif.netmask, NETMASK_ADDR0, NETMASK_ADDR1, NETMASK_ADDR2, NETMASK_ADDR3); IP4_ADDR(&netif.gw, GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3); /* 注册网络接口 */ netif_add(&netif, NULL, NULL, NULL, NULL, &ethernetif_init, &ethernet_input); netif_set_default(&netif); netif_set_up(&netif); /* 创建UDP协议 */ struct udp_pcb *udp_pcb = udp_new(); if (udp_pcb == NULL) { printf("[ERROR] Failed to create UDP protocol\n"); return -1; } /* 绑定UDP端口号 */ err_t err = udp_bind(udp_pcb, IP_ADDR_ANY, UDP_PORT); if (err != ERR_OK) { printf("[ERROR] Failed to bind UDP port\n"); return -1; } /* 设置UDP回调函数 */ udp_recv(udp_pcb, udp_echo_recv, NULL); /* 进入主循环 */ while (1) { /* 处理网络接口中的数据 */ ethernetif_input(&netif); /* 休眠一段时间,让出CPU */ sys_msleep(10); } return 0; } ``` 需要注意的是,在使用lwIP协议栈时,需要在主循环中调用ethernetif_input函数来处理网络接口中的数据。此外,还需要使用sys_msleep函数来让出CPU,以便其他任务能够运行。 4. 测试 在编写完代码后,可以使用PC端的UDP工具(比如UDP Test Tool)来测试UDP通信是否正常。具体的测试方法如下: - 将PC的IP地址设置为192.168.1.100,子网掩码为255.255.255.0,网关为192.168.1.1; - 打开UDP Test Tool,并设置本地IP地址为192.168.1.100,端口号为8888; - 在UDP Test Tool中输入要发送的数据,点击“Send”按钮,如果一切正常,就应该能够收到开发板回传的数据。 综上所述,以上就是使用stm32f03和enc28j60实现与PC进行UDP通信的步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值