回顾
再上一章中,我们进行了rtl8188的驱动编译,以及舵机驱动的编写和编译。有了上一章的成果,就已经搭建好了嵌入式Linux系统的硬件环境。现在,就可以在这个硬件环境下进行软件编程了。
在系统分析中介绍过,本系统通过socket(套接字)来传递舵机的控制信息(旋转角度)。那么,就先介绍一下嵌入式Linux系统下的网络编程原理吧。
Linux网络编程原理
概述
Linux下的网络编程通过socket编程实现。Socket,即套接口,既是一种特殊的I/O,也是一种文件描述符。一个通信实体的socket由一个三元组定义,即:协议族,网络地址和传输层端口。通信双方的一个连接是用网络五元组来标识的,它是由双方相同协议族的两个本地三元组合成的,包括:协议族,本地网络地址、本地端口、远程网络地址和远程端口。
Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。
socket编程原理
下图表示了TCP协议下socket的工作原理和流程:
从服务器看,socket编程要结果以下步骤:
1.建立套接口
2.将本地端口与另一个端口联系起来
3.建立套接口队列
4.等待连接
5.接收信息
6.关闭套接口
所以,在实际编程中,先建立套接口,这一步骤可以用socket函数来实现,具体如下:
if( (s = socket(AF_INET, SOCK_STREAM, 0)) == -1 )
这段代码中使用到的socket函数的原型为:
int socket (int domain, int type, int protocol);
domain指协议族,type指套接口类型,protocol指对应套接字应使用哪个协议。返回值为套接字句柄。若返回-1,则表明建立失败。这里,AF_INET指用于跨机器间的通信。SOCK_STREAM提供TCP的套接字。在本程序中,用sockaddr_in 类型的参数servaddr来存储本地的协议族,网络地址及端口。下面对其进行初始化:
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(12345);
其协议族依然为AF_INET。INADDR为0.0.0.0,即本机地址,htonl将主机字节顺序转换 为网络字节顺序(有的机器高为在前,有的低位在前)。在手机端,将通信的端口号设置为12345,故在此处与手机匹配,将端口号也设置为12345。Htons将主机字节转换为网络字节顺序。
接下来使用bind函数将本机的一个端口与套接口连起来,代码如下:
if( bind(s, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error: %s(errno:%d)\n",strerror(errno),errno);
exit(0);
}
bind函数的第一个参数为创立的套接字句柄。第二个参数为存储本机IP地址和端口号的数据结构。第三个参数为第二个参数的长度。若成功调用,返回0,否则返回-1。
接下来用listen函数建立套接口队列,代码如下:
if( listen(s, 10) == -1){
printf("listen socket error: %s(errno:%d)\n",strerror(errno),errno);
exit(0);
}
这里s为套接口的句柄,10表示socket队列的最大连接数。正确调用返回0,否则返回-1。
接下来进行轮询,来检测是否有连接,代码如下:
while(1){
if((connfd = accept(s, (struct sockaddr*)NULL, NULL)) == -1){
printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
continue;
}
n = recv(connfd, buff, MAXLINE, 0);
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
close(connfd);
}
首先,调用等待连接函数accept。accept第一个参数为套接口句柄,第二个参数存储客户程序的IP地址及端口号,这里不做限定,故为NULL。第三个参数为第二个参数的长度。Accept函数为阻塞调用。当无连接时,阻塞进程,有连接时,若成功,返回一个新连接的套接口描述句柄,否则返回-1。
连接之后,用recv函数向buff中写入数据。Recv返回值为接收到字符串长度。接收之后在最后加上字符串的结束符。技术之后关闭这个新连接的套接口。
最后关闭开始时建立的套接口s。
socket舵机控制应用介绍
了解完socket编程的原理后,就可以编写“socket接收+舵机控制”的应用了。首先声明,从手机APP发来的socket内包含的是范围从1-180的字符串信息,代表了舵机的旋转角度。在知道这点的情况下,开始进行编程。
socket接收应用源码设计
在socket接收应用中,需要完成两个内容:
1.接收socket,并通过调用socket的API获得socket内部的字符串信息;
2.将字符串信息转化成int整形数格式,便于之后的舵机控制。
在上一节中,已经详细介绍过socket编程的原理流程,按照之前介绍的原理依次调用socket的API,就可以获得socket中的字符串信息。这里对接收socket并获得信息的过程不再讲述,读者可以参考附录中的源码来加深对socket编程的理解。
至于将字符串转化成int整形数格式,这里用到了函数int atoi(const char *nptr)。这个函数定义于stdlib.h中,专门用于把ascii字符串转化成int整形数。具体代码如下:
angle_value = atoi ( buff ); //将字符串转换位数字
printf("Angle Value: %d\n\n",angle_value);
if ( angle_value < 0 || angle_value > 180 ){//角度校验
printf( "angle format error\n" );
}
else{
zrcar_servo0_set( servo_dev, angle_value );
}
首先用atoi函数把接收到的字符串转换为int型数据,然后做一次校验。当角度大于180或者小于0时报错,而在角度在正常范围内时,调用舵机控制函数zrcar_servo0_set,这个函数会在下一节中具体介绍到。
舵机控制源码设计
这里会用到上一章讲到的舵机驱动中的相关函数,在这里不花费时间赘述,请大家参考上一章内容辅助理解
在上一章中,舵机已经在Linux系统中被抽象为设备文件zrcar_servo_dev。现在,我们就需要通过对这个文件进行修改,从而改变PWM波生成模块的工作状态。先从设计一个基本的设备文件读取函数开始介绍:
(1)舵机设备文件读取操作:
下面这个函数是一段是对舵机设备文件的最基本的读取操作:
int zrcar_servo_init(char *servo_dev){
int fd = open(servo_dev,O_RDWR);
TEST(fd > 0,"servo device init failed!");
close(fd);
return 0;
}
该函数的输入char *servo_dev是舵机设备文件的路径(在本次设计中该文件路径为:/dev/zrcar_servo_dev)。之后调用open函数。open函数会调用在驱动函数中注册的接口函数,使用上一章中定义的函数servo_open,打开舵机设备。之后用TEST函数校验舵机设备是否能成功打开,如果不能打开,就打印错误提示信息。最后关闭文件。
这个函数没有对设备做任何控制,只是用来验证是否可以打开舵机设备文件。
接下来,将会介绍如何通过对舵机设备文件进行修改来控制PWM波的占空比
(2)舵机设备文件修改操作:
下面这个函数用来对舵机设备文件做修改:
int zrcar_servo0_set(char *servo_dev, int angle){
int fd = open( servo_dev, O_RDWR );
TEST(fd > 0,"servo device open failed!\n");
angle = 500 + 2000/180.0 * angle;
ioctl(fd,SERVO0_SET,angle);
close(fd);
return 0;
}
首先用open函数打开舵机设备文件,
之后控制舵机设备,使用函数ioctl,该函数可以调用在上一章的舵机驱动中定义的舵机控制接口函数:
static long servo_ioctl(struct file *file, unsigned int cmd, unsigned long arg)。
使用该函数,应用程序就可以通过驱动程序访问舵机外设,控制舵机角度。
由于舵机角度控制ip核只负责产生PWM波,周期固定为20ms,占空比由传入的参数unsigned long arg控制,因此需要将舵机的角度0-180线性映射到500-2500(即高电平时间0.5ms~2.5ms),所以需要对角度用下面的公式进行换算:
arg = 500+2000/180.0*angle。
最后关闭舵机设备文件。这样,就可以修改输出的PWM波的占空比,从而控制舵机的旋转角度了。
源码编译
该应用程序编写完毕后,将该文件中的函数与socket程序结合在一起,编写MakeFile文件:
CROSS_COMPLIE = arm-linux-gnueabihf-
ARCH = arm
CC = $(CROSS_COMPILE)gcc
LD = $(CROSS_COMPILE)ld
CFLAGS = -g
all: socketServer.o servo_ctrl.o
${CC} -o socketServer socketServer.o servo_ctrl.o
clean:
rm ./*.o socketServer
其中CROSS_COMPLIE指定交叉编译工具,ARCH为程序运行CPU的架构,使用CC= arm-linux-gnueabihf-gcc,保证使用的是交叉编译工具,而不是电脑X86的gcc编译工具。这样,就可以在Linux系统下编译得到适用于Zybo开发板上的arm-v7架构处理器的可执行文件socketServer了。
最终成果
通过设计Zybo开发板上的“socket应用+舵机控制”应用,成功地把实验板上的无线网卡和舵机联系了起来,现在可以通过向rtl8188的指定端口发送1-180内的字符串信息来控制舵机的转动了。
附录
socket接收+舵机控制应用源码下载 提取码:df8u
下章预告
现在,已经完成了Zybo开发板上的全部软硬件设计内容。剩下的内容就是Android端的socket发送APP开发以及最终的测试环节了,这两部分的内容会在下一章详细介绍。