Linux学习系列八:操作网口

1.引言

一些相对高性能的单片机会带以太网接口,网口在MCU里算是比较复杂的外设了,因为它涉及到网络协议栈,通常情况下网络协议栈会运行在一个RTOS中,所以对普通单片机开发者来说网口使用起来相对难度较大一些。在Linux下网口是一个经常使用的接口,由于Linux具备成熟完备的网络通信协议栈,底层驱动厂家也都提供好了,所以使用起来相对方便的多。本篇对Linux下网口使用做个简单总结,希望对大家有所帮助。

2.环境介绍

2.1.硬件

1) 网上的一个第三方做的NUC972开发板:

有兴趣购买的朋友,可以去他们的淘宝店购买:

https://s.click.taobao.com/X8mza8w

本篇和板子打交道的主要是板子的网口。

2) 1根USB转RS232线、1根网线、1根电源线、1根Micrco USB线

2.2.软件

1) Uboot、Kernel我们继续使用上一篇文章用的。

2) Rootfs我们使用Buildroot来重新生成,NUC972 Buildroot的下载地址:https://github.com/OpenNuvoton/NUC970_Buildroot ,这里使用Buildroot重新制作Rootfs的原因是借助Buildroot工具来添加我们想要的东西,比如本篇我们需要的ssh功能,会非常的方便,相对于自己手工去移植就容易的多。也许你体会不到,感兴趣的话你可以参考网上教程手动去移植dropbear来实现ssh功能,通过两种方式对比,你就会深有感悟了。

3)交叉工具链arm_linux_4.8.tar.gz,还是上一篇文章用的,我猜测这个工具链也是Buildroot生成的。

3.Buildroot制作Rootfs

详细的步骤不再这里介绍了,大家可以参考我之前发过的一篇文章《使用Buildroot为I.MX6制作根文件系统》 ,有几点在此说明一下:

1.下载完官方提供的Buildroot后,进入到对应目录,执行以下指令:

make nuvoton_nuc972_defconfig
make

第一次编译时间会稍微有些长,大家要有耐心,因为它会在线下载很多文件。

2)关于交叉工具链的问题,采用的是Buildroot toolchain,选择这个Buildroot会从零开始制作工具链。编译完成后你可以看到在output/host/目录下会有全新制作好的工具链,个人猜测官方提供的工具链也是这么来的。

3)默认配置下没有选择dropbear,自己选上即可。

4)编译完成后,生成的rootfs是output/images/rootfs.tar,为了能够烧写到NUC972板子里,需要先解压,然后通过mkyaffs2去生成.img格式文件。

5) 将Uboot、Kernel、Rootfs等重新下载到板子里,配置一下dropbear和网口就可以使用了,使用passwd指令给root用户设置一个密码,设密码的好处是可以防止任何人都可以直接登录系统。

将网线连接板子和电脑,把电脑IP设置为192.168.0.50,在串口登录界面我们输入ifconfig eth0 192.168.0.100,为了保证开机后网络就可用,将这句话添加到/etc/init.d/rcS 文件结尾。这样后面我们就不用连接串口了,单独使用网口就可以登录Linux系统了,同时可以给板子传文件,不需要再按照之前那样通过U盘拷来拷去了,效率会大大的提高。

4.网口操作

4.1.相关命令

和网络相关的命令,经常使用的有ifconfig,前面配置网卡时用过,还有ping,用来测试网络通不通,其他还有route、ethtool等,等后面实际用到时再介绍。

4.2.C语言例子

平时用的最多就是udp和tcp通信,关于它们的基础介绍不再这里细述了,不太清楚的同学直接百度看两篇文章就行了。这里以UDP为例,下面我们来看一个十分经典的例子。

要实现的功能是:

  1. Client接收手动输入的数据
  2. Client将上述数据发送给Server端
  3. Server端将接收到的数据回传给Client

直接上代码:

/***********************************************
 * @{
 * @file  : udp_client.c
 * @brief : 
 * @author: TopSemic
 * @email : topsemic@sina.com
 * @date  : 2019-06-20
***********************************************/

//--------------------------------------------------
// Copyright (c) Topsemic
//--------------------------------------------------
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/un.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <arpa/inet.h> 

#define SEND_DEST_PORT 8888


int main()
{
	int sockfd;
	int ret;
	struct sockaddr_in addr_sender;
	struct sockaddr_in addr_dest;
	
	int nlen = sizeof(struct sockaddr);
	int recvlen=0;	
    
        // sender address  
        bzero(&addr_sender,sizeof(addr_sender));
		
	// dest address
	bzero(&addr_dest,sizeof(struct sockaddr_in));//每个字节都用0填充
	addr_dest.sin_family=AF_INET;  
        addr_dest.sin_addr.s_addr=inet_addr("127.0.0.1");
   	addr_dest.sin_port=htons(SEND_DEST_PORT); 

	sockfd=socket(AF_INET,SOCK_DGRAM,0); //udp 创建套接字
	if(sockfd < 0) 
	{
        printf("create socket failure,sockfd:%d\n",sockfd);
		return -1;

	}
	
	//不断获取用户输入并发送给服务器,然后接受服务器数据
	while(1)
	{
		char buff[1024] = {0x00};
		printf("Please Input a string: ");
		fgets(buff,1024,stdin);
		
		sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&addr_dest, sizeof(struct sockaddr_in));
		recvlen = recvfrom(sockfd,buff,sizeof(buff),0,(struct sockaddr *)&addr_sender,(socklen_t *)&nlen);
	    if(recvlen > 0)
		{
			buff[recvlen] = 0x00;
			printf("Message form server: %s\n", buff);
			printf("sender ip:%s port:%d\n",inet_ntoa(addr_sender.sin_addr),ntohs(addr_sender.sin_port));
		}
		printf("**************************************\n");
	}
	
	close(sockfd);
	return 0;
}
/***********************************************
 * @{
 * @file  : udp_server.c
 * @brief : 
 * @author: TopSemic
 * @email : topsemic@sina.com
 * @date  : 2019-06-20
***********************************************/

//--------------------------------------------------
// Copyright (c) Topsemic
//--------------------------------------------------
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/un.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <arpa/inet.h> 

#define LOCAL_RECV_PORT 8888

int main()
{
	int sockfd;
	int ret;
	struct sockaddr_in addr_local;
	struct sockaddr_in addr_sender;
	
	char buff[1024]={0x00};
	int nlen = sizeof(struct sockaddr);
	int recvlen = 0;	
    
       // sender address  
        bzero(&addr_sender,sizeof(addr_sender));
	
	// local address
	bzero(&addr_local,sizeof(addr_local));
	addr_local.sin_family=AF_INET; // AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型
	addr_local.sin_addr.s_addr=htonl(INADDR_ANY);// INADDR_ANY 是指所有本机IP
	addr_local.sin_port= htons(LOCAL_RECV_PORT); // 绑定端口号

	sockfd=socket(AF_INET,SOCK_DGRAM,0); //UDP 创建套接字
	if(sockfd < 0) 
	{
        printf("create socket failure,sockfd:%d\n",sockfd);
		return -1;

	}
	
	ret=bind(sockfd,(struct sockaddr *)&addr_local,sizeof(addr_local));
	if(ret < 0) 
	{
		printf("bind failure\n");
		return -1;
	}

	while(1)
	{
		recvlen = recvfrom(sockfd,buff,sizeof(buff),0,(struct sockaddr *)&addr_sender,(socklen_t *)&nlen);
		if(recvlen > 0)
		{
			buff[recvlen] = 0x00;
			printf("udp server received data:%s\n",buff);
			printf("sender ip:%s port:%d\n",inet_ntoa(addr_sender.sin_addr),ntohs(addr_sender.sin_port));
			// 将接收到的数据原样发回去,发到接收到的IP和端口上
			sendto(sockfd, buff, recvlen, 0, (struct sockaddr *)& addr_sender, sizeof(struct sockaddr_in));
		}
        printf("**************************************\n");
	}
	close(sockfd);
	return 0;
}

首先我们在Ubuntu里使用gcc编译一下,注意不是交叉编译arm-linux-gcc,我们先在PC上先运行Server后运行Client,可以看下效果,它实现了上述我们想要的功能。

上述代码大家可以仔细去看一遍,有几处需要说明的地方:

1)UDP不同于TCP,不存在请求连接和受理过程,因此实际中是不明确区分服务器端和客户端的,上述命名为server和client只是方便描述而已,我是这么理解的:先发送数据(请求数据)后接收的是客户端,先接收数据后发送数据的是服务端。

2) 大家有没有注意到在server 例子里有调用bind函数,但是client例子里并没有,这个原因是什么呢?原因是这样,因为Server工作首先得接收数据,如果不绑定端口的话,那是没法知道该在哪里接收数据的。Client之所以不用绑定是因为它先发,发完紧接着是可以在发送的端口处接收到数据的。

3) 实际工作中我发现好多人包括我自己会经常被端口给绕晕。这里再总结一下,UDP接收的时候需要bind一个端口号(这个端口自己设备的端口),才可以接从这个端口接收数据,收到数据后会得到对方的IP地址和发送端口号。发送的时候指明对方的IP和端口即可,本机的发送端口随机分配,不需要绑定端口。

为了验证不绑定端口发送的端口是随机分配的,我们可以再做个小试验,我们把Client关掉,再重新打开一次,我们看一下前后两次打印的端口信息,我们可以看到两次的端口号是不同的。

4) 在调用socket创建套接字时,该函数的第二个参数传递 SOCK_DGRAM,指明使用的是UDP协议。如果是TCP的话,该参数是SOCK_STREAM.

5) addr_local成员变量赋值时使用htonl(INADDR_ANY)来自动获取IP地址。

使用INADDR_ANY的好处是,当软件运行到其他主机或者主机IP地址改变时,不用再更改源码重新编译,也不用在启动软件时手动输入。而且,如果一台主机中已分配多个IP地址,那么只要端口号一致,就可以从不同的IP地址接收数据。

6) 在Client里发送时我们制定的IP是127.0.0.1,这是一个比较特殊的ip地址,你用ifconfig看看,在Ubuntu下和板子上都可以看到:

从网上找了一段英文描述:

127.0.0.1 is the loopback Internet protocol (IP) address also referred to as the “localhost.” The address is used to establish an IP connection to the same machine or computer being used by the end-user. 可以简单的理解是代表本机自己。

下一步我们把Client代码交叉编译放到板子上跑一下试试,我们需要做两处细微的改动:

第一处addr_dest.sin_addr.s_addr=inet_addr(“127.0.0.1”);改为:

addr_dest.sin_addr.s_addr=inet_addr("192.168.0.50");

192.168.0.50 是PC端的IP地址。

第二处while(1)中的这三句话

char buff[1024] = {0x00};
printf("Please Input a string: ");
fgets(buff,1024,stdin);

改为:

char buff[1024] = "Hello TopSemic Friends!";
//printf("Please Input a string: ");
//fgets(buff,1024,stdin);

目的是让client自动的发送数据、接收数据,不再等待用户输入信息。

在Ubuntu下通过scp指令直接将文件放到板子的/opt目录里

scp udp_client root@192.168.0.100:/opt

另外我们直接在Ubuntu下通过ssh指令登录Linux系统

ssh root@192.168.0.100:/opt

退出的话输入exit即可,就可以返回到Ubuntu的命令行窗口。

这样登录板子、给板子上传文件过程都可以很方便的在Ubuntu里操作了,相比之前的Windows串口登录、U盘传输文件方便很多。

我满怀欣喜的在Ubuntu下运行udp_server,Ubuntu下ssh登录到板子里运行udp_client,以为直接就能运行成功,结果出现了意外情况,实际是压根就没有结果输出。

可是明明虚拟机Ubuntu都能登录到板子上也能ping通,板子也能ping 192.168.0.50这个IP,为什么udp却通不了呢。后来经过一段时间琢磨解决了这个问题。解决办法如下:

虚拟机默认的网络设置模式是下面所示的NAT模式,

我们把它修改为下图所示的桥接模式:

然后进入把网线拔掉重新连接一下,在Ubuntu虚拟机里修改一下网络配置

将虚拟机的有线连接改成手动配置的固定IP,192.168.0.xx网段(不要和Windows 以及板子IP冲突)。可以ifconfig验证一下是否设置成功

这时再通过登录到板子上ping 192.168.0.80,是可以ping通的。之前ping 192.168.0.50 那是Windows主机的IP,可以通不代表和虚拟机能通。

最后把上述代码里IP改掉,

addr_dest.sin_addr.s_addr=inet_addr("192.168.0.80");

重新再编译下载运行一次,就可以正常工作了。

补充一点:板子平时调试,也会经常使用Windows下的网络调试助手,该工具使用只要正确配置协议类型、本地主机地址、本地主机端口,远程主机,之后发送,就可以查看结果了。

比如我们也可以在Windows开启网络调试助手,模拟客户端和虚拟机Server通信,如下:

5.实际工作总结

举一个实际工作中非常普遍容易犯的错误。

假设你的处理器通过网口和外部的一个设备通信,使用udp通信方式,正常的工作流程如下图,由你先发送数据过去,然后外部设备给你应答。

这个模型和上述Server、Client模型非常类似,你要实现的就是Client。也就是调用sendto函数先进行发送,然后调用recvfrom函数去接收。正常情况下程序这么写是没有问题的,但是实际中你得考虑很多的异常情况,比如正常工作的过程中外部设备突然断电再上电或者重启(但是你的CPU设备没有断电),这时会出现什么问题呢?由于外部设备断电,recvfrom函数就会因为收不到数据而阻塞,即使外部设备重新上电初始化后,它也因为没有收到数据而不会给出应答数据,导致你的recvfrom函数一直卡住不动。

这样的代码如果发布到现场,将会带来很大的隐患,因为现场出现上述情况很正常,还有比如你的CPU设备先上电,外部设备后上电也会出现上述问题。我之前项目就因为这个问题,导致客户抱怨产品有问题,客户发现如果通信失败,只有设备重上电才可以解决。

解决上述问题的办法也很简单,可以设置一个超时,使用setsockopt函数,让接收函数在超时时间内没有接收到数据时就返回就行了。返回后再接着重头发送数据即可,框架如下:

/* 设置阻塞超时 */
    struct timeval timeout = {3, 0}; // 设置3s超时
    if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(struct timeval)) < 0)
    {
        printf("time out setting failed\n");
    }
    .
    .
    .

    /* 数据阻塞接收 */
int receivePacketLen = recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)& addr_sender,&addrLen);
if(receivePacketLen != -1)
{
//接收到数据
 …
}
    else if (errno == EAGAIN)      //阻塞接收超时
    {
        printf("udp receive timeout!\n");
        return -1;
}

为了大家更直观的感受这个问题,我们在上面实验的基础上来模拟这个场景,我们先运行upd_client,后运行udp_server,大家看下现象,结果自然是没有数据输出。

道理不难想明白,client程序运行后,先发送了数据,然后就阻塞在读那里不动了。我们把程序简单修改下:

// Max Recv block timeout in second
#define gMaxRecvBlockTimeout 3
…
…
…
// Set recv timeout
    struct timeval timeout = {gMaxRecvBlockTimeout, 0};
    if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(struct timeval)) < 0)
    {
        printf("time out setting failed\n");
    }
	
	//不断获取用户输入并发送给服务器,然后接受服务器数据
	while(1)
	{
		char buff[1024] = "Hello TopSemic Friends!";
		//printf("Please Input a string: ");
		//fgets(buff,1024,stdin);
		
		sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&addr_dest, sizeof(struct sockaddr_in));
		recvlen = recvfrom(sockfd,buff,sizeof(buff),0,(struct sockaddr *)&addr_sender,(socklen_t *)&nlen);
	    if(recvlen > 0)
		{
			buff[recvlen] = 0x00;
			printf("Message form server: %s\n", buff);
			printf("sender ip:%s port:%d\n",inet_ntoa(addr_sender.sin_addr),ntohs(addr_sender.sin_port));
		}
		else if(errno == EAGAIN) // 阻塞接收超时
        {
            printf("udp receive timeout!\n");
        }
		printf("**************************************\n");
	}
	
	close(sockfd);
	return 0;

这时我们先运行client,

打印如上,然后再运行Server,就可以正常工作了,不会再出现上述问题。

6.结束语

本篇为大家介绍了Linux下以太网接口的使用,网络方面的知识博大精深,应用非常多,我这只是抛砖引玉,大家有什么经验欢迎多分享交流,可以在网页下方留言讨论,或者发邮件:Topsemic@sina.com ,微信公众号如下,欢迎关注:

本期相关的资料在百度网盘,链接: https://pan.baidu.com/s/1xZkbCAfKVRZOaJS27BiUzQ  提取码:hzmm;

里面包含了如下内容:

本篇文章完整内容见附件pdf文档:

本系列往期文章见:

1:Linux学习系列一:开发环境搭建

2:Linux 学习系列二:运行 Hello World

3:Linux学习系列三:uboot编译下载

4:Linux学习系列四:Kernel编译下载

5:Linux学习系列五:Nand Flash根文件系统制作

6:Linux学习系列六:操作GPIO

7:Linux学习系列七:操作UART

4+

Linux学习系列八:操作网口》上有1条评论

发表评论