lwip,是由Adam Dunkels开发的小型开源的TCP/IP协议栈;支持TCPIP协议族的核心协议;主要包括:ARP/ICMP/TCP/UDP/IPV4/IPV6/DHCP等;最主要的特点是运行所需的RAM和ROM少(只需十几KB的RAM和四十KB的ROM就可以运行),这使LWIP协议栈适合在低端的嵌入式系统中使用.
对于stm32f103来说没有网口,通信就得靠enc28j60
兼容 IEEE802.3 IEEE802.3 协议的以太网控制器 。
集成 MAC MAC和 10 BASE 10 BASE 10 BASE 10 BASE-T物理层 。
支持全双工和半模式 。
数据冲突时可编程自动重发 。
SPI 接口速度可达 接口速度可达 10Mbps 10Mbps 。
8K 数据接收和发送双端口 数据接收和发送双端口 RAM 。
提供快速数据移动的内部 DMA 控制器 控制器 。
可配置的接收和发送缓冲区大小 。
两个可编程 LED 输出 。
带 7个中断源的两引脚 个中断源的两引脚 。
TTL TTL 电平输入 。
SPI接口,充当主控制器和ENC28J60之间的通道
控制寄存器,用于控制和监视ENC28J60.
双端口RAM缓冲器,用于接收和发送数据包。
判优器,当DMA、发送和接受模块发出请求时对RAM缓冲器的访问进行控制。
总线接口,对通过SPI接收的数据和命令进行解析。
MAC(Medium Access Control),实现符合IEEE802.3标准的MAC逻辑。
因为LWIP可以支持的结构体,因为LWIP可以支持多个网络接口,当设备
有多个网络接口的话LWIP就会把所有的netif结构体组成链表来管理这些网络接口
struct netif {
struct netif *next; //指向下过一个netif 结构体
ip_addr_t ip_addr; //网络接口IP 地址
ip_addr_t netmask; //子网掩码
ip_addr_t gw; //默认网关
netif_input_fn input; //IP 层接收数据函数
netif_output_fn output; //IP 层发送数据包调用
netif_linkoutput_fn linkoutput; //底层数据包发送
void *state; //设备的状态信息
u16_t mtu; //该网络接口最大允许传输的数据长度
u8_t hwaddr_len; //物理地址长度
u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //该网络接口的物理地址
u8_t flags; //该网络接口的状态和属性
char name[2]; //该网络接口的名字
u8_t num; //该网络接口的编号
}
typedef struct
{
u8 mac[6]; //MAC 地址
u8 remoteip[4]; // 远端主机 IP 地址
u8 ip[4]; u8 ip[4]; // 本机 IP 地址
u8 netmask[4];
u8 netmask[4];// 子网掩码
u8 gateway[4];// 默认网关的 IP 地址
vu8 dhcpstatus;//dhcp状态
//0, 未获取DHCP 地址 ;
//1,进入 DHCP获取状态
//2,成功获取 DHCP地址
//0XFF,获取失败
}__lwip_dev;
//LWIP初始化(LWIP启动的时候使用)
//返回值:0,成功
// 1,内存错误
// 2,DM9000初始化失败
// 3,网卡添加失败.
u8 lwip_comm_init(void)
{
struct netif *Netif_Init_Flag; //调用netif_add()函数时的返回值,用于判断网络初始化是否成功
struct ip_addr ipaddr; //ip地址
struct ip_addr netmask; //子网掩码
struct ip_addr gw; //默认网关
if(lwip_comm_mem_malloc())return 1; //内存申请失败
if(ENC28J60_Init())return 2; //初始化ENC28J60
lwip_init(); //初始化LWIP内核
lwip_comm_default_ip_set(&lwipdev); //设置默认IP等信息
#if LWIP_DHCP //使用动态IP
ipaddr.addr = 0;
netmask.addr = 0;
gw.addr = 0;
#else //使用静态IP
IP4_ADDR(&ipaddr,lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);
IP4_ADDR(&netmask,lwipdev.netmask[0],lwipdev.netmask[1] ,lwipdev.netmask[2],lwipdev.netmask[3]);
IP4_ADDR(&gw,lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);
printf("网卡en的MAC地址为:................%d.%d.%d.%d.%d.%d\r\n",lwipdev.mac[0],lwipdev.mac[1],lwipdev.mac[2],lwipdev.mac[3],lwipdev.mac[4],lwipdev.mac[5]);
printf("静态IP地址........................%d.%d.%d.%d\r\n",lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);
printf("子网掩码..........................%d.%d.%d.%d\r\n",lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]);
printf("默认网关..........................%d.%d.%d.%d\r\n",lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);
#endif
Netif_Init_Flag=netif_add(&lwip_netif,&ipaddr,&netmask,&gw,NULL,ðernetif_init,ðernet_input);//向网卡列表中添加一个网口
#if LWIP_DHCP //如果使用DHCP的话
lwipdev.dhcpstatus=0; //DHCP标记为0
dhcp_start(&lwip_netif); //开启DHCP服务
#endif
if(Netif_Init_Flag==NULL)return 3;//网卡添加失败
else//网口添加成功后,设置netif为默认值,并且打开netif网口
{
netif_set_default(&lwip_netif); //设置netif为默认网口
netif_set_up(&lwip_netif); //打开netif网口
}
return 0;//操作OK.
}
上面这个函数主要完成以下功能:
1 、调用 lwip_comm_mem_malloc() 函数完成前面提到的内存堆 ram_heap 和内存池
memp_memory 的内存分配。
2、调用 ENC28J60_Init ()函数完成对 ENC28J60 的初始化,ENC28J60_Init ()函数在
enc28j60.c 文件中,由 ALIENTEK 提供,前面已经介绍过了。
3、调用 lwip_init 函数完成 LWIP 的内核初始化,lwip_init()通过调用各个模块的初始化函数来完成各个模块的初始化,比如内存初始化函数、数据包结构初始化函数、网络接口初始化函数、IP 和 TCP 等的初始化函数,lwip_init()在 init.c 文件中,属于 LWIP 源码。
4、调用 ip_comm_default_ip_set()函数设置静态地址等信息,此函数由 ALIENTEK 提供。
5、判断是否使用 DHCP,如果使用 DHCP 的话就通过 DHCP 服务来获取 IP 地址、子网掩码和默认网关等信息,如果使用静态 IP 地址的话就用 ip_comm_default_ip_se()函数设置的地址信息。
6、我们通过 netif_add()函数来完成网卡的注册,netif_add()在 netif.c 文件中,此函数属于LWIP 源码。netif_add()函数中的参数 lwip_netif 是我们定义的一个网络接口,这个函数除了使用上面说的 IP 地址,子网掩码和默认网关作为参数外,还使用了两个函数地址作为参数:ethernetif_init 和 ethernet_input , 这两个函数地址会被赋值给 netif 结构体的相关字段,
ethernetif_init()在下一节会讲到,这个函数由 ALIENTEK 提供,ethernet_input()函数在 etharp.c文件中,属于 LWIP 源码,是 ARP 层数据包输入函数。
7、如果使用 DHCP 的话就开启 DHCP 服务,通过调用 dhcp_start()函数开启 DHCP 服务。
8、当网卡注册成功后使用netif_set_default()设置此网卡为默认网卡,并且使用netif_set_up()函数打开此网卡。
此函数也是我们所写的函数硬件驱动完后,的第一个函数。也是对LWIP初始化的函数,此函数运行成功则LWIP初始化成功。此函数完后就要定时去处理内核维护函数。lwip_periodic_handle();
**void lwip_dhcp_process_handle(void)**此函数定时执行放在内核维护的定时中断里面。状态机模式执行。
//DHCP处理任务
void lwip_dhcp_process_handle(void)
{
u32 ip=0,netmask=0,gw=0;
switch(lwipdev.dhcpstatus)
{
case 0: //开启DHCP
dhcp_start(&lwip_netif);
lwipdev.dhcpstatus = 1; //等待通过DHCP获取到的地址
printf("正在查找DHCP服务器,请稍等...........\r\n");
break;
case 1: //等待获取到IP地址
{
ip=lwip_netif.ip_addr.addr; //读取新IP地址
netmask=lwip_netif.netmask.addr;//读取子网掩码
gw=lwip_netif.gw.addr; //读取默认网关
if(ip!=0) //正确获取到IP地址的时候
{
lwipdev.dhcpstatus=2; //DHCP成功
printf("网卡en的MAC地址为:................%d.%d.%d.%d.%d.%d\r\n",lwipdev.mac[0],lwipdev.mac[1],lwipdev.mac[2],lwipdev.mac[3],lwipdev.mac[4],lwipdev.mac[5]);
//解析出通过DHCP获取到的IP地址
lwipdev.ip[3]=(uint8_t)(ip>>24);
lwipdev.ip[2]=(uint8_t)(ip>>16);
lwipdev.ip[1]=(uint8_t)(ip>>8);
lwipdev.ip[0]=(uint8_t)(ip);
printf("通过DHCP获取到IP地址..............%d.%d.%d.%d\r\n",lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);
//解析通过DHCP获取到的子网掩码地址
lwipdev.netmask[3]=(uint8_t)(netmask>>24);
lwipdev.netmask[2]=(uint8_t)(netmask>>16);
lwipdev.netmask[1]=(uint8_t)(netmask>>8);
lwipdev.netmask[0]=(uint8_t)(netmask);
printf("通过DHCP获取到子网掩码............%d.%d.%d.%d\r\n",lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]);
//解析出通过DHCP获取到的默认网关
lwipdev.gateway[3]=(uint8_t)(gw>>24);
lwipdev.gateway[2]=(uint8_t)(gw>>16);
lwipdev.gateway[1]=(uint8_t)(gw>>8);
lwipdev.gateway[0]=(uint8_t)(gw);
printf("通过DHCP获取到的默认网关..........%d.%d.%d.%d\r\n",lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);
}else if(lwip_netif.dhcp->tries>LWIP_MAX_DHCP_TRIES) //通过DHCP服务获取IP地址失败,且超过最大尝试次数
{
lwipdev.dhcpstatus=0XFF;//DHCP超时失败.
//使用静态IP地址
IP4_ADDR(&(lwip_netif.ip_addr),lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);
IP4_ADDR(&(lwip_netif.netmask),lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]);
IP4_ADDR(&(lwip_netif.gw),lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);
printf("DHCP服务超时,使用静态IP地址!\r\n");
printf("网卡en的MAC地址为:................%d.%d.%d.%d.%d.%d\r\n",lwipdev.mac[0],lwipdev.mac[1],lwipdev.mac[2],lwipdev.mac[3],lwipdev.mac[4],lwipdev.mac[5]);
printf("静态IP地址........................%d.%d.%d.%d\r\n",lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);
printf("子网掩码..........................%d.%d.%d.%d\r\n",lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]);
printf("默认网关..........................%d.%d.%d.%d\r\n",lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);
}
}
break;
default : break;
}
}
我们通过判断 lwipdev 结构体的 dhcpstatus 字段判断是否使用 DHCP 服务,当 dhcpstatus=0 时表示开启 DHCP,我们调用 dhcp_start()函数开启相应网络接口的 DHCP 服务 dhcp_start()函数由 LWIP 源码提供。开启 DHCP 以后让 dhcpstatus=1,表示开始进行 DHCP,等待 DHCP 完成。当 DHCP
完成以后让 dhcpstatus=2 ,表示 DHCP 成功。下次执行此函数直default,但是当 DHCP 重试次数大于LWIP_MAX_DHCP_TRIES 时,意味着 DHCP 失败,这时dhcpstatus=0XFF,表示 DHCP 失败, 并且使用静态 IP 地址。
在 LWIP 的源码中有个 opt.h 的文件,这个文件是裁剪和配置 LWIP 的,不过我们最好不要直接在 opt.h 里面做修改,我们可以打开 opt.h 文件看一下,里面的配置都是条件编译的,如果我们在其他地方有定义过的话那么在 opt.h 里面的定义不起作用了。所以我们可以新建一个.h 的文件来裁剪和配置 LWIP,我们前面提过在 LWIP->lwip_app->lwip_comm 下有一个 lwipopts.h 的文件,这个文件就是用来裁剪与配置LWIP
——UDP 协议是 TCP/IP 协议栈中传输层协议,是一个简单的面向数据报的协议,在传输层中还有另一个重要的协议,那就是 TCP 协议。UDP 不提供数据包分组、组装,不能对数据包进行排序,当报文发送出去后无法知道是否安全、完整的到达。UDP 除了这些缺点外肯定有它自身的优势,由于 UDP 不属于连接型协议,因而消耗资源小,处理速度快,所以通常在音频、视频和普通数据传输时使用 UDP 较多。
数据报结构如下图所示
校验和首先在数据发送方通过特殊的算法计算得出,在传递到接收方之后,还需要再重新计算。如果某个数据报在传输过程中被第三方篡改或者由于线路噪音等原因受到损坏,发送和接收方的校验计算和将不会相符,由此 UDP 协议可以检测是否出错。
——在 LWIP 源码中 udp.c 和 udp.h 这两个文件是关于 UDP 协议的,这两个文件中有很多函数, 要想对 UDP 有详细的了解的话可以查阅这两个文件。在这里推荐一本我本人认为很好的 LWIP 参考书《嵌入式网络那些事 LWIP 协议栈深度剖析与实战演练》,作者朱升林,在这本书中对于 LWIP 的源码做了非常详细的讲解,想要研究 LWIP 源码的可以看看这本书,不过这本书中采用的是 1.3.2 版本的 LWIP,本教程采用的是 1.4.1 版本的 LWIP,所以有些函数会有差别,这一点大家一定要注意一下。
udp.c 中与 UDP 报文处理有关的函数之间的关系如图
LWIP的RAW API编程方式是基于回调机制,当我们初始化应用的时候我们必须为内核中的不同时间注册相应的回调函数,当相应的时间发生的时候这些回调函数就会被调用。
RAW API 函数 描述
———— ———————UDP 的 RAW API 功能函数
//UDP测试
void udp_demo_test(void)
{
err_t err;
struct udp_pcb *udppcb; //定义一个UDP服务器控制块
struct ip_addr rmtipaddr; //用来保存远端ip地址
u8 *tbuf;
u8 key;
u8 res=0;
// u8 t=0;
//initsardata();
udp_demo_set_remoteip();//设置远端IP
tbuf=mymalloc(SRAMIN,200); //申请内存
if(tbuf==NULL)
return ; //内存申请失败了,直接退出
sprintf((char*)tbuf,"Local IP:%d.%d.%d.%d",lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);//服务器IP
//LCD_ShowString(30,150,210,16,16,tbuf);
sprintf((char*)tbuf,"Remote IP:%d.%d.%d.%d",lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2],lwipdev.remoteip[3]);//远端IP
//LCD_ShowString(30,170,210,16,16,tbuf);
sprintf((char*)tbuf,"Remote Port:%d",UDP_DEMO_PORT);//客户端端口号
//LCD_ShowString(30,190,210,16,16,tbuf);
POINT_COLOR=BLUE;//字体为蓝色
//LCD_ShowString(30,210,210,16,16,"STATUS:Disconnected"); //如果本地准备成功会覆盖此数据
//创建一个UDP控制块
udppcb=udp_new();//他自己的内存池创建
initsardata();
if(udppcb)//创建成功
{
IP4_ADDR(&rmtipaddr,lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2],lwipdev.remoteip[3]);//打包成u32 位
err=udp_connect(udppcb,&rmtipaddr,UDP_DEMO_PORT);//UDP客户端连接到指定IP地址和端口号的服务器
if(err==ERR_OK)
{
err=udp_bind(udppcb,IP_ADDR_ANY,UDP_DEMO_PORT);//绑定本地IP地址与端口号
if(err==ERR_OK) //绑定完成
{
udp_recv(udppcb,udp_demo_recv,NULL);//注册接收回调函数 给相关的udppcb结构体元素赋值
printf("STATUS:Connected \r\n");//标记连接上了(UDP是非可靠连接,这里仅仅表示本地UDP已经准备好)
udp_demo_flag |= 1<<5; //标记已经连接上
POINT_COLOR=RED;
//LCD_ShowString(30,230,lcddev.width-30,lcddev.height-190,16,"Receive Data:");//提示消息
POINT_COLOR=BLUE;//蓝色字体
}else res=1;
}else res=1;
}else res=1;
while(res==0)//连接成功
{
//Open_Elengine_2();
key=KEY_Scan(0);
if(key==WKUP_PRES)break;
if(key==KEY0_PRES)//KEY0按下了,发送数据
{
udp_demo_senddata(udppcb);
}
// if((sendrecv.humi&0x0f)==0x01)//如果humi标志位为1则发送数据到上位机
// {
//
// sendrecv.humi=sendrecv.humi&0xf0;//将该位清零
// }
if(udp_demo_flag&1<<6)//是否收到数据?
{
udp_demo_senddata(udppcb);
elengine_wyx = 1;
//LCD_Fill(30,250,lcddev.width-1,lcddev.height-1,WHITE);//清上一次数据
//LCD_ShowString(30,250,lcddev.width-30,lcddev.height-230,16,udp_demo_recvbuf);//显示接收到的数据
udp_demo_flag&=~(1<<6);//标记数据已经被处理了.
}
lwip_periodic_handle();//轮询任务 维持LWIP内核
delay_ms(2);
//DHT11_test();
}
udp_demo_connection_close(udppcb);
myfree(SRAMIN,tbuf);
}
在上面代码中我们完成了一下几个功能:
1、调用 udp_new()函数创建一个 UDP 控制块 upcb。 2、当创建成功以后,调用 udp_connect()函数连接到指定的 IP 地址和端口号的主机,如果
UDP 控制块 upcb 创建失败的话 res 等于 1。
3、连接成功以后调用 udp_bind()函数绑定本地 IP 地址和端口号,连接失败的话就让 res 等于 1。
4、绑定成功以后使用 udp_recv()函数注册 UDP 的接收函数,并且置位
udp_demo_flag 的bit5,表示已经连接上(UDP 是非可靠连接,这里仅仅表示本地 UDP 已经准备好),如果绑定失败的话令 res 等 1。
5、如果 res 等于 0 的话那么表示以上 4 个步骤都成功了,成功以后就进入 while 循环,失败的话就调用 udp_demo_connection_close()函数断开连接,并且释放内存。
6、进入 while 以后我们通过按键来做不同的处理,当按下 KWY_UP 键的时
候退出循环,当按下 KWY0 键的时候发送数据。通过读取 udp_demo_flag 的 bit6 来判断是否接收到数据,当接收到数据的时候就在 LCD 上显示接收到的数据。
7、这点特别重要!在 while 循环中一定要调用函lwip_periodic_handle(),如果不调用这两个函数那么 LWIP 的内核就不能运行,那么网络肯定不会工作!
至此,我们关于 LWIP的RAW API UDP通信就说完了,关于RAW编程接口UDP的代码及实现部分请看