linux开发随笔

/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/include/

如果将网卡比喻为身份证

插入的usb网卡代表windows(当然,windows可以有很多网卡),开发板自身有两网卡,有两个身份证,用eth0。虚拟机有两个虚拟网卡。桥接网卡用于和开发板,windows主机沟通。

usb网卡ip为192.168.5.10

ubuntu的桥接网卡(ens36)ip为192.168.5.11,桥接网卡为192.168.15.134

开发板etn0网卡ip为192.168.5.9,注意,开发板ip可能会失效。每次应该检查。

配置完后,三个网卡可以互相ping通
 

手工设置的方法很简单,但是每次启动开发板都要重新设置,在开发板串口 中执行命令即可: ifconfig eth0 192.168.5.9

开发板出厂自带linux系统。通过mobaexterm创建串口连接,可以将开发板当成一台真linux电脑。

NFS,网络文件系统,类似云盘。

前提,

启动NFS服务

虚拟机的挂载目录为    /home/book/nfs_rootfs

开发板的挂载目录为    /mnt/

权限

ubuntu自己挂载自己测试

挂载,将ubuntu目录挂载到开发板,ubuntu就像云盘。

测试

在ubuntu新建文件,立即在开发板访问

 

当然,在开发板新建文件,ubuntu也能立即访问到。挂载的两个文件夹就像是一个文件夹,不论另一个怎么改变,另一个也会很快改变。

注意,ubuntu或开发板重启,数据线断开等等,挂载会断开,建议每次都挂载,

第一个应用程序

注意,由于不同设备cpu架构可能不同,指令集不同,所以就需要不同的编译链。同样的源码,经不同的编译链编译后,可以运行在不同架构的设备,在一台设备上编译要运行在另一个平台的程序,就叫交叉编译,必须要用适应目标架构的交叉编译链。否则无法运行。

比如arm-buildroot-linux-gnueabihf-gcc -o hello hello.c,在ubuntu虚拟机将hello.c文件用arm-buildroot-linux-gnueabihf-gcc编译链生成的可执行文件hello,才能运行在板子上。而不能直接用gcc。

第一个驱动程序

么编译驱动程序之前要先编译内核

开发板上运行到内核是出厂时烧录的,你编译驱动时用的内核是你自己编译 的,这两个内核不一致时会导致一些问题。所以我们编译驱动程序前,要把自己 编译出来到内核放到板子上去,替代原来的内核。

板子使用新编译出来的内核时,板子上原来的其他驱动也要更换为新编译出 来的。所以在编译我们自己的第 1 个驱动程序之前,要先编译内核、模块,并且 放到板子上去。

编译内核

不 同 的 开 发 板 对 应 不 同 的 配 置 文 件 , 配 置 文 件 位 于 内 核 源 码 arch/arm/configs/目录。编译完成 zImage 后才可编译设备树文件。

编译完成后,在 arch/arm/boot 目录下生成 zImage 内核文件, 在 arch/arm/boot/dts 目录下生成设备树的二进制文件 100ask_imx6ull14x14.dtb。把这 2 个文件复制到/home/book/nfs_rootfs 目录备用,这个目录是挂载到了开发板的/mnt目录

编译内核模块

进入内核源码目录后,就可以编译内核模块了:

book@100ask: cd ~/100ask_imx6ull-sdk/Linux-4.9.88/

book@100ask:~/100ask_imx6ull-sdk/Linux-4.9.88$ make module

把模块安装在 nfs 目录“/home/book/nfs_rootfs/”下备用

book@100ask:~/100ask_imx6ull-sdk/Linux-4.9.88$ make ARCH=arm INSTALL_MOD_PATH=/home /book/nfs_rootfs modules_install(这两行是一行)

这时候,在 Ubuntu 的/home/book/nfs_rootfs 目录下,已经有了 zImage、 dtb 文件,并且有 lib/modules 子目录(里面含有各种模块)。 接下来要把这些文件复制到开发板上。

安装内核和模块到开发板上

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt(再挂载,防止已经断开)

cp /mnt/zImage /boot

cp /mnt/100ask_imx6ull-14x14.dtb /boot

cp /mnt/lib/modules /lib -rfd

sync

最后重启开发板,它就使用新的 zImage、dtb、模块了。

编译驱动

将驱动源码从windows复制到ubuntu,修改驱动文件里的makefile文件的linux内核源码路径,在ubuntu编译后,备份到nfs_rootfs文件夹。当然也可以直接从windows复制到nfs_rootfa文件夹,然后修改makefile并编译,一样的。

开发板安装驱动

首先检查开发板ip,测试是否能ping通ubuntu,再挂载,如果重复挂载会提示busy,没影响。

cd进入开发板/mnt/01_hello_drv目录,执行insmod hello_drv.ko装载驱动程序,

 

开发板如何访问到插入的SD卡数据呢,答案也是挂载,但这个不是网络文件系统的挂载。

将sD卡的sda1分区挂载到开发板的/mnt/下,就可以读写SD卡了

当然,还有虚拟的挂载,比如,开发板自动挂载的内核文件,可以查看内核资源

 当然也可以手动挂载,因为是虚拟的,所以不用设备节点

c是char设备,b是block设备,那两个数字呢,第一个是设备的主设备号,另一个是次设备号,次设备号对应一个设备的不同硬件部位

读写文件,查看linux的读写文件函数,可以用man命令

FrameBuffer 帧缓冲是存储图像像素数据的内存区域

实验

在屏幕实现描点函数

步骤,打开 LCD 设备节点,获取屏幕参数,映射 Framebuffer,最后实现描点函数。

 

获取屏幕的bit_per_pixel,就是每个像素点用多少位表示颜色

 

源码

#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/fb.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>

static int fd_fb;
static struct fb_var_screeninfo var;	/* Current var */
static int screen_size;
static unsigned char *fb_base;
static unsigned int line_width;
static unsigned int pixel_width;

void lcd_put_pixel(int x, int y, unsigned int color)
{
	unsigned char *pen_8 = fb_base+y*line_width+x*pixel_width;
	unsigned short *pen_16;	
	unsigned int *pen_32;	

	unsigned int red, green, blue;	

	pen_16 = (unsigned short *)pen_8;
	pen_32 = (unsigned int *)pen_8;

	switch (var.bits_per_pixel)//获取每个像素用多少位表示颜色
	{
		case 8:
		{
			*pen_8 = color;
			break;
		}
		case 16:
		{
			/* 565 */
			red   = (color >> 16) & 0xff;
			green = (color >> 8) & 0xff;
			blue  = (color >> 0) & 0xff;
			color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
			*pen_16 = color;
			break;
		}
		case 32:
		{
			*pen_32 = color;
			break;
		}
		default:
		{
			printf("can't surport %dbpp\n", var.bits_per_pixel);
			break;
		}
	}
}

int main(int argc, char **argv)
{
	int i;
	
	fd_fb = open("/dev/fb0", O_RDWR);//打开设备节点
	if (fd_fb < 0)
	{
		printf("can't open /dev/fb0\n");//打开失败
		return -1;
	}
	if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))//获取屏幕可变参数
	{
		printf("can't get var\n");
		return -1;
	}

	line_width  = var.xres * var.bits_per_pixel / 8;
	pixel_width = var.bits_per_pixel / 8;
	screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
	fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
	if (fb_base == (unsigned char *)-1)
	{
		printf("can't mmap\n");//映射失败
		return -1;
	}

	/* 清屏: 全部设为白色 */
	memset(fb_base, 0xff, screen_size);

	/* 随便设置出100个为红色 */
	for (i = 0; i < 100; i++)
		lcd_put_pixel(var.xres/2+i, var.yres/2, 0xFF0000);
	
	munmap(fb_base , screen_size);//解除对内存区域的映射关系
	close(fd_fb);//关闭设备节点
	
	return 0;	
}

字符和编码

首先,字符集和编码格式并不是一一对应的关系,比如unicode字符集,就有几种不同的utf编码,常用的是utf-8,在编写c语言文件时,保存的时候会有编码格式,而在linnux编译执行时,默认是utf-8,所以在使用gcc时,可以加入参数,指定源文件编码格式和可执行文件编码格式。

如下,同样是"A中"这两个字符,由于源文件保存编码不同,执行结果也不同,GB2312一个汉字两个字节,而UTF83个字节。

 怎么解决呢,在gcc编译时加入参数即可,如下,-finput-charset是源文件字符编码,-fexec-charset是生成的可执行文件的字符编码,这样就能在编译阶段转换编码格式。

 可以看到,utf-8的源文件,生成的执行文件是GB2312的,同样GB2312源文件生成的可执行文件是UTF-8的。

显示汉字实验

HZK16文件是一些常用汉字的点阵数据,通过GB2312索引,所以源c文件要以GB2312格式保存(或者如上在编译时指定编码格式),在虚拟机编译,拷贝到nfs目录,到开发板执行

 试验成功。但是"中"显示错误,变成了涓,为什么。因为源文件是UTF-8格式,虽然Linux编译默认也是UTF-8。但是我们使用的汉字库使用GB2312索引,所以,在编译时,要指定可执行文件的编码格式为GB2312

 

 

代码分析

int fd_fb;
struct fb_var_screeninfo var;	/* Current var */
int screen_size;
unsigned char *fbmem;
unsigned int line_width;
unsigned int pixel_width;

int fd_hzk16;
struct stat hzk_stat;
unsigned char *hzkmem;



/**********************************************************************
 * 函数名称: lcd_put_pixel
 * 功能描述: 在LCD指定位置上输出指定颜色(描点)
 * 输入参数: x坐标,y坐标,颜色
 * 输出参数: 无
 * 返 回 值: 会
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 * 2020/05/12	     V1.0	  zh(angenao)	      创建
 ***********************************************************************/ 
void lcd_put_pixel(int x, int y, unsigned int color)
{
	unsigned char *pen_8 = fbmem+y*line_width+x*pixel_width;
	unsigned short *pen_16;	
	unsigned int *pen_32;	

	unsigned int red, green, blue;	

	pen_16 = (unsigned short *)pen_8;
	pen_32 = (unsigned int *)pen_8;

	switch (var.bits_per_pixel)
	{
		case 8:
		{
			*pen_8 = color;
			break;
		}

		case 16:
		{
			/* 565 */
			red   = (color >> 16) & 0xff;
			green = (color >> 8) & 0xff;
			blue  = (color >> 0) & 0xff;
			color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
			*pen_16 = color;
			break;
		}
		case 32:
		{
			*pen_32 = color;
			break;
		}
		default:
		{
			printf("can't surport %dbpp\n", var.bits_per_pixel);
			break;
		}
	}
}
/**********************************************************************
 * 函数名称: lcd_put_ascii
 * 功能描述: 在LCD指定位置上显示一个8*16的字符
 * 输入参数: x坐标,y坐标,ascii码
 * 输出参数: 无
 * 返 回 值: 无
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 * 2020/05/12	     V1.0	  zh(angenao)	      创建
 ***********************************************************************/ 
void lcd_put_ascii(int x, int y, unsigned char c)
{
	unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];
	int i, b;
	unsigned char byte;

	for (i = 0; i < 16; i++)
	{
		byte = dots[i];
		for (b = 7; b >= 0; b--)
		{
			if (byte & (1<<b))
			{
				/* show */
				lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */
			}
			else
			{
				/* hide */
				lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */
			}
		}
	}
}
/**********************************************************************
 * 函数名称: lcd_put_chinese
 * 功能描述: 在LCD指定位置上显示一个16*16的汉字
 * 输入参数: x坐标,y坐标,ascii码
 * 输出参数: 无
 * 返 回 值: 无
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 * 2020/05/12	     V1.0	  zh(angenao)	      创建
 ***********************************************************************/ 
void lcd_put_chinese(int x, int y, unsigned char *str)
{
	unsigned int area  = str[0] - 0xA1;
	unsigned int where = str[1] - 0xA1;
	unsigned char *dots = hzkmem + (area * 94 + where)*32;
	unsigned char byte;

	int i, j, b;
	for (i = 0; i < 16; i++)
		for (j = 0; j < 2; j++)
		{
			byte = dots[i*2 + j];
			for (b = 7; b >=0; b--)
			{
				if (byte & (1<<b))
				{
					/* show */
					lcd_put_pixel(x+j*8+7-b, y+i, 0xffffff); /* 白 */
				}
				else
				{
					/* hide */
					lcd_put_pixel(x+j*8+7-b, y+i, 0); /* 黑 */
				}	
			}
		}
}

int main(int argc, char **argv)
{
	unsigned char str[] = "中";
	
	fd_fb = open("/dev/fb0", O_RDWR);//打开freambuffer
	if (fd_fb < 0)
	{
		printf("can't open /dev/fb0\n");
		return -1;
	}

	if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))//获取framebuffer信息
	{
		printf("can't get var\n");
		return -1;
	}

	line_width  = var.xres * var.bits_per_pixel / 8;
	pixel_width = var.bits_per_pixel / 8;
	screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
	fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);//映射framebuffer
	if (fbmem == (unsigned char *)-1)
	{
		printf("can't mmap\n");
		return -1;
	}

	fd_hzk16 = open("HZK16", O_RDONLY);//打开汉字库文件
	if (fd_hzk16 < 0)
	{
		printf("can't open HZK16\n");
		return -1;
	}
	if(fstat(fd_hzk16, &hzk_stat))
	{
		printf("can't get fstat\n");
		return -1;
	}
	hzkmem = (unsigned char *)mmap(NULL , hzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk16, 0);//映射汉字库
	if (hzkmem == (unsigned char *)-1)
	{
		printf("can't mmap for hzk16\n");
		return -1;
	}

	/* 清屏: 全部设为黑色 */
	memset(fbmem, 0, screen_size);

    lcd_put_ascii(var.xres/2, var.yres/2, 'A'); /*在屏幕中间显示8*16的字母A*/
	
	printf("chinese code: %02x %02x\n", str[0], str[1]);
	lcd_put_chinese(var.xres/2 + 8,  var.yres/2, str);

	munmap(fbmem , screen_size);
	close(fd_fb);
	
	return 0;	
}

FreeType实验

如下是Ubuntu虚拟机上适用于IMX_6ULL开发板的交叉编译工具链位置,可以看到几个目录,我们可以将自己写的头文件和库放到对应位置,头文件一般放到sysroot/user/include下。库文件(.so)一般放到sysroot/user/lib下

 比如我们使用交叉编译链时,包含的stdio.h在哪里呢,也在/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/arm-buildroot-linux-gnueabihf/sysroot/usr/include/,如下,当然,如果使用linux自带的gcc,包含的头文件在/usr/include

 

 万能命令

解压FreeType

 进入,可以看到有config文件

 使用万能命令,生成makefile

 执行make,成功,如果失败,原因是FreeType依赖于其他库,不过,imx_6ull工具链已经有了zlib,按理来说还要编译安装libpng,不过这里成功了,不管了

  make成功

make install,如下,成功,在./temp里有了include和lib文件夹

将include里和lib里全部拷贝到工具链下的/usr/include和/usr/lib,-rf表示递归

 

 到这里,我们就手动编译安装了FreeType

wchar测试

 每一个wchar类型的字符,都将以unicode码保存,即使文件保存格式不是unicode

第一次报错,因为

FreeType显示汉字

错误,找不到头文件,问题是因为之前编译的FreeType在工具链的usr/include/freetype2/目录,所以找不到,只需要将freetype2下所有文件移动到include即可

 移动

 再次编译,报错,函数未定义,只需要加上参数编译即可

 成功

 拷贝到网络文件系统的

 板子执行

 还可以让字体发生旋转,代码略微变化,编译参数要指定-lm,表示数学库

 拷贝运行,第一个参数必须是角度,第二个才是大小

输入系统

如下,查看event0设备节点数据,假设屏幕是event0,在没有点击屏幕时,是没有输出的,这时候的hexdump程序在休眠,当点击屏幕后,hexdump会被唤醒,就会输出数据

 当点击屏幕后,会触发中断,中断服务函数就会到驱动程序去获取数据,传给核心层,转化为统一格式后再传给事件层,事件层再唤醒应用程序,并将数据传给它。

如下,这些数据不是一个事件,每一行都是一个事件,因为人点击屏幕不是一瞬间完成的。

上面的数据表示什么意思呢,linux通过一个结构体描述一个事件,上面的输入事件同理。当然,结构体内还有事件发生的时间(从系统运行到现在)。

 

 

事件类型

如何查看设备节点的详细信息呢,如下 

描述输入设备的结构体,可以看到,一个输入设备有十个数组,就像bitmap,对于evbit,就是支持的事件类型。

 比如上面的event0设备,EV=b,二进制为1011,即支持0,1,3号事件。分别是同步事件,按键事件,绝对位移事件。

 

 绝对位移事件又分为多种属性,或者说子事件,如下B:ABS=2658000 3

再次分析之前的hexbump输出的数据

如下,0003表示支持绝对位移事件,0039表示ID,0035是x方向的位移,0036是y方向的位移。

编写程序,查看输入设备的信息,比如支持哪些事件

其实linux已经有完整的库函数,不过为了训练,我们还是手动实现,首先man 2 open查看open函数需要哪些头文件

 另外,还要用到evdev.c里的

evdev_do_ioctl,ioctl不就是获取信息吗,cmd参数定义在input.h,不过不是linux内核里,而是编译链里 input.h

如下,input.h在编译链下include/linux/里,那个alsa是声卡的,不管他,即程序要包含<linux/input.h>

 还要用linux自己的ioctl函数,open ioctl,即要包含<sys/ioctl.h>

代码

#include <stdio.h>
#include <linux/input.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

int main(int argc, char **argv)
{

    char *ev_names[]={
        "EV_SYN",
        "EV_KEY",
        "EV_REL",
        "EV-ABS",
        "EV_MSC",
        "EV_SW",
        "NULL",
        "NULL",
        "NULL",
        "NULL",
        "NULL",
        "NULL",
        "NULL",
        "NULL",
        "NULL",
        "NULL",
        "NULL",
        "EV_LED",
        "EV_SND",
        "NULL",
        "EV_REP",
        "EV_FF",
        "EV-PWR"
    };

    int fd; // 句柄
    int err;
    struct input_id id; // 结构体在input.h里面
    int i,j;

    unsigned int evbit[2];
    int len;

    /*用法  ./view_dev_info /dev/input/eventX*/
    if (argc != 2)
    {
        printf("用法  ./view_dev_info /dev/input/eventX");
        return -1; // 失败
    }

    fd = open(argv[1], O_RDWR); // 可读可写
    if (fd < 0)
    {
        printf("ERR: %s open failed", argv[1]);
        return -1;
    }

    // 第二个参数,根据input.h里的cmd宏定义设置,是获取id还是支持的事件位图evbit
    err = ioctl(fd, EVIOCGID, &id);
    if (err == 0)
    {
        printf("bustype = 0x%s\n", id.bustype);
        printf("vendor = 0x%s\n", id.vendor);
        printf("product = 0x%s\n", id.product);
        printf("version = 0x%s\n", id.version);
    }
    else
        return -1 ;


    // 存在evbit里,len只是长度,LEN==2
    len = ioctl(fd, EVIOCGBIT(0, 8), &evbit); // 0表示读取evbit,改为1就是读取keybit第二个参数是读取的字节数

    if (len > 0 && len <= 8)
    {
        for(i=0;i<8;i++){
            unsigned char *byte=(unsigned char *)evbit[i];
            for(j=0;j<8;i++){
                if(*byte &= (1<<j)){
                    printf("%S  ",ev_names[i*8+j]);
                }
            }
        }
    }
    return 0;
}

上面的程序是查看屏幕支持的事件,下面将以此为基础,编程实现读取屏幕数据,当点击屏幕时,输出,type,code,value等等。有多种方式,有直接读取,休眠唤醒读取,POLL读取,直接读取如果没有数据就立即返回,休眠唤醒的话无数据休眠,有数据唤醒,POLL就是在超时时间内一直读取,时间一到就返回。

直接读取和休眠唤醒读取

#include <string.h>//strcmp要
#include <unistd.h>//read函数需要

传入参数O_NOOBLOCK就是以非阻塞方式打开

 然后读取数据,

 

POLL方式读取

在打开设备节点时,必须以非阻塞方式打开,因为POLL就是在超时时间内一直读取。

 用man查看poll函数,包含头文件poll.h,关于poll函数用法,第一个参数是一个结构体数组,第二个是nfds_t结构体,然后是超时时间

异步通知方式

步骤大概如下

 查看signal函数

 getpid

fcntl

 

 部分代码

tslib,即toach screen库,是关于触摸屏的开源库,有很多封装好的程序。

1-5是为了配置开发环境,以后自己编程时,编译会用到

1,。先解压

 2.再配置

 3.make编译

4.安装

5.拷贝到工具链

 6.挂载

7.拷贝到开发板,这里是为了配置开发板的运行环境

 测试

ts_print_mt,成功

 ts_test_mt

无反应,要关闭GUI

网络编程

TCP,可靠的,面向链接的

 先写服务端

 domain选择AF_INET,因为是ipv4

 type参数

 bind函数

 可以用这个结构体,但不太好,用sockaddr_in结构体更好

 将端口转为字节序的函数

memset

listen,参数backlog是最大同时监听数

 accept

inet_ntoa,将网络ip转为字符串,ascll码

fork,创建子进程的函数,是为了实现接收多个客户端

 fork函数返回值,创建成功的话,子进程的pid将会返回给父进程,并且将0返回给子进程,如果创建子进程失败,就将-1返回给父进程。

recv函数

 tcp_server完整代码

#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int iSocketClient_fd;
    int iAddrLen;
    int iSocketServer_fd;
    int iRet;
    struct sockaddr_in tSocketServerAddr;
    struct sockaddr_in tSocketClientAddr;

    int iRecvLen;
    unsigned char ucRecvBuf[1000];
    int iClient_ID = -1;

    // 创建socket句柄
    //  protocol,协议
    iSocketServer_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (iSocketServer_fd == -1)
    {
        printf("Scoket Error\r");
        return -1;
    }

    // 通过结构体设置服务端的ip端口等
    tSocketServerAddr.sin_family = AF_INET;
    tSocketServerAddr.sin_port = htons(8888);       // 将8888端口转为网络字节序
    tSocketServerAddr.sin_addr.s_addr = INADDR_ANY; // 本机所有ip
    memset(&tSocketServerAddr.sin_zero, 0, 8);

    // 将socket句柄与ip结构体绑定
    iRet = bind(iSocketServer_fd, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
    if (iRet == -1)
    {
        printf("Bind Error\r");
        return -1;
    }

    // listen,监听socket句柄,最大同时监听10个
    iRet = listen(iSocketServer_fd, 10);
    if (iRet == -1)
    {
        printf("Listen Error\r");
        return -1;
    }

    while (1)
    {
        iAddrLen = sizeof(struct sockaddr);
        iSocketClient_fd = accept(iSocketServer_fd, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
        if (iSocketClient_fd != -1) // 有客户端连接成功
        {
            iClient_ID++; // 区分不同的客户端
            printf("Get Connect from %d : %s\n", iClient_ID, inet_ntoa(tSocketClientAddr.sin_addr));
            if (!fork())
            {
                // 子进程源码
                while (1)
                {
                    // 接收客户端数据并显示
                    // 源,目的,长度
                    iRecvLen = recv(iSocketClient_fd, ucRecvBuf, 999, 0);
                    if (iRecvLen <= 0)
                    {
                        close(iSocketClient_fd);
                        return -1;
                    }
                    else
                    {
                        ucRecvBuf[iRecvLen] = '\0'; // 结束符
                        printf("Get Msg From Client %d : %s\n", iClient_ID, ucRecvBuf);
                    }
                }
            }
        }
    }
    close(iSocketServer_fd);
    return 0;
}

客户端

connect

 fgets,从标准输入获得数据

 send,发送数据

 测试

先在ubuntu直接运行server,再用mobax连接ubuntu运行client,运行client要带参数192.168.15.134,这是NAT网卡ip,本实验的服务端和客户端是同一台机器。

 

 如上,试验成功

ps -A,可以看到,有两个tcp_server,因为获得连接后创建了一个子进程。

 当退出client时,client和server子进程本应退出,client确实彻底退出了,但server子进程为什么变成了僵尸进程,因说白了,需要父进程给他收尸,有点FreeRTOS的钩子函数的意思。 

 

 解决办法

,在tcp_server.c里调用函数signal ,要包含signal.h

 重新编译,重新运行server和client,退出client,ps -A查看进程,可以看到。server子进程彻底被杀,只留了父进程

udp编程

服务端,udp服务端不再需要Listen

recvfrom函数

 udp的服务端程序,变化主要在while循环里

socket函数的参数变为了SOCK_DGRAM

 while循环里调用recvfrom

客户端

第一种,相较于tcp的客户端,只改变socket函数参数为SOCK_DGRAM,也就是说,connect函数依然存在,不过并未真正连接。这样的话,只需调用send,因为conect会包含发送数据的目的地tSocketServerAddr。

第二种,删除connect,调用sendto,指定目的地

 第一种测试

 第二个,测试

出现问题,修改代码

 问题依然存在,客户端输入数据按下回车键,客户端程序立马退出,服务端未收到数据

多线程编程

为什么要用多线程呢,因为线程之间通信方便,开销小,进程间通信往往通过队列交换数据,开销大。同一个进程的不同线程可以访问到全局变量。

linux资源分配以进程为单位,而调度以线程为单位

 pthread_create,线程创建函数

代码编写

上传虚拟机

编译,发现要链接库,使用-lpthread

 加上&后台运行,ps查看进程可以看到一个pthread

 使用ps -T查看线程,可以发现两个线程

或者cd  /proc/61599,ls task/,可以看到两个线程号

在此代码基础上修改,实现一个线程获取标准输入,另一个线程打印出数据,数据存在全局变量

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

static char g_buf[1000];
static int g_has_data;

static void *my_thread_func(void *data)
{
    while (1)
    {
        while (!g_has_data);
        printf("rec:%s\n", g_buf);
        g_has_data = 0;
    }
}

int main(int argc, char **argv)
{
    int ret;
    pthread_t t_id;
    ret = pthread_create(&t_id, NULL, my_thread_func, NULL);
    if (ret != 0)
    {
        printf("线程创建失败");
        return -1;
    }
    while (1)
    {
        fgets(g_buf, 1000, stdin); // 从标准输入获取数据
        g_has_data = 1;
        // 通知接收线程
    }
    return 0;
}

编译同样-lpthread,运行前杀掉上次的进程,kill -9

运行如下,测试成功

top命令查看cpu,发现一个pthread1就消耗了百分百cpu

 怎么修改降低cpu占用

通过信号量机制

 代码如下

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>

static char g_buf[1000];
static int g_has_data;
static sem_t g_sem;

static void *my_thread_func(void *data)
{
    while (1)
    {
        sem_wait(&g_sem);
        printf("rec:%s\n", g_buf);
        g_has_data = 0;
    }
}

int main(int argc, char **argv)
{
    int ret;
    pthread_t t_id;
    sem_init(&g_sem,0,0);
    ret = pthread_create(&t_id, NULL, my_thread_func, NULL);
    if (ret != 0)
    {
        printf("线程创建失败");
        return -1;
    }
    while (1)
    {
        fgets(g_buf, 1000, stdin); // 从标准输入获取数据
        sem_post(&g_sem);//唤醒接收线程
    }
    return 0;
}

运行后,top命令查看,在前面根本找不到pthread2,说明资源占用极少。

分析以上代码,发现还有问题,比如当正在打印g_buf时,这时候输入了新数据,这个数组里就会有新老数据混合,怎么解决呢。

互斥量

代码主要增加了

 编译运行后发现,无法打印出数据,分析问题,是因为main函数里while太快,还没来得及打印,互斥量就又被主线程获取了,因为g_buf被互斥锁锁住了,导致一直无法打印

修改方法,加一个局部变量缓冲数组,因为memcpy速度是很快的。memcpy要包含string.h

编译运行,收到了数据,但是出现了乱码

 具体代码如下

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>

static char g_buf[1000];
static sem_t g_sem;
static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER;

static void *my_thread_func(void *data)
{
    while (1)
    {
        sem_wait(&g_sem);

        pthread_mutex_lock(&g_tMutex);
        printf("rec:%s\n", g_buf);
        pthread_mutex_unlock(&g_tMutex);
    }
}

int main(int argc, char **argv)
{
    char temp_buf[1000];
    int ret;
    pthread_t t_id;
    sem_init(&g_sem, 0, 0);
    ret = pthread_create(&t_id, NULL, my_thread_func, NULL);
    if (ret != 0)
    {
        printf("线程创建失败");
        return -1;
    }
    while (1)
    {
        fgets(g_buf, 1000, stdin);     // 从标准输入获取数据
        pthread_mutex_lock(&g_tMutex); // 上锁
        memcpy(g_buf, temp_buf, 1000);
        pthread_mutex_unlock(&g_tMutex); // 开锁

        sem_post(&g_sem); // 唤醒接收线程
    }
    return 0;
}

关于同步互斥,还有一种办法,条件变量。条件变量是和互斥量一起用的,相当于代替信号量。用作同步

 

 主要变化代码,没有了信号量

测试发现没问题,关于乱码,经过分析,是fgets参数错了。将g_buf改为temp_buf即可 

TTY体系是什么

是由电传打字机演变而来的,键盘打字输入,然后纸张输出,这是物理终端,对于linux虚拟机,每开一个Terminal窗口,在./dev下就会多一个tty,从1开始,tty1,tty2等等,这些都是虚拟终端。

 如下,tty3输出了第一个hello,第二个没输出,因为第二个是要在tty4输出

tty3

tty4

上面说到虚拟终端从tty1开始,那tty0是啥呢,是指的当前前台终端,只有前台终端能接受键盘输入

下图,从tty3输入命令,发送数据到tty0,即发送到前台窗口,后续切换到tty4等窗口,也能收到。谁是前台谁收到

/dev/tty表示此终端,表示自己的终端,在tty1里表示tty1,在tty2里表示tty2,将上图红框替换为/dev/tty,就只有tty3能收到数据,其他窗口收不到。

控制台和终端

 修改内核cmdline

 修改内核cmdline后,因为指定了两个console,所以/dev/console指的是后一个,下面的消息只有tty3能接收到,换一个比如tty4执行这段命令,消息依旧会显示到tty3。

tty驱动程序框架

 形象解释,比如开发板通过串口和电脑连接,在电脑用串口显示板子的命令行界面,此时输入ls,将会显示所有文件,这个过程是怎样的,首先ls\n被从串口发送到板子,板子里的串口驱动接收到,将会把字符发给行规层line discipine,行规层检查,发现是ls换行,就把ls上传给app,这里app是shell,app查看文件,把结果发回行规层,再发给串口驱动程序,通过串口硬件发给PC的串口驱动,然后PC就能看到ls执行结果。

 上面的行规层,可以设为原始模式,将检查字符等工作完全交给应用层。后面的串口编程,也就是多了行规层的设置。

串口应用程序实验

imx6ull板子的系统已经支持板子上的串口硬件,如果是Mp157,还要修改替换设备树文件,如下,查看所有tty

储备知识

 

c_Lflag,本地标志 

c_iflag

 

开始编程

#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
#include <error.h>
#include <stdio.h>

int open_port(char *com)
{
    int fd;
    fd=open(com,O_RDWR | O_NOCTTY | O_NDELAY);
    if(fd==-1)
    {
        perror("open dev %s error",com);
        return -1;
    }
    if(fcntl(fd,F_SETFL,0)<0)//设置串口为阻塞态
    {
        perror("fcntl error");
        return -1;
    }
    return fd;
}

int set_opt(int fd,int nSpeed,int nBits,char nEvent,int nStop)//设置选项
{
    struct  termios newtio,oldtio;
    if(tcgetattr(fd,&oldtio)!=0)//获取驱动程序默认的参数,存入oldtio
    {
        perror("SetupSerial 1");
        return -1;
    }
    bzero(&newtio,sizeof(newtio));//参数清零
    newtio.c_cflag |= CLOCAL | CREAD;
    newtio.c_cflag &= ~CSIZE;

    newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);//清除,使用原始模式
    newtio.c_oflag &=~OPOST;//原始输出

    switch (nBits)//数据位个数
    {
    case 7:
        newtio.c_cflag |=CS7;
        break;
    case 8:
        newtio.c_cflag |=CS8;
        break;
    }

    switch (nEvent)
    {
    case 'O':
        newtio.c_cflag |= PARENB;//进行奇偶校验
        newtio.c_cflag |= PARODD;//选择奇校验
        newtio.c_iflag |= (INPCK | ISTRIP);//打开输入奇偶校验,去除字符第8位
        break;
        case 'E':
        newtio.c_iflag |= (INPCK | ISTRIP);
        newtio.c_cflag |= PARENB;
        newtio.c_cflag &= ~PARODD;//选择偶校验
        break;
        case 'N':
        newtio.c_cflag &= ~PARENB;//无校验
        break;
    }

    switch(nSpeed)
    {
    case 2400:
        cfsetispeed(&newtio,B2400);//control flag set input speed
        cfsetospeed(&newtio,B2400);
        break;
    case 4800:
        cfsetispeed(&newtio,B4800);
        cfsetospeed(&newtio,B4800);
        break;
    case 9600:
        cfsetispeed(&newtio,B9600);
        cfsetospeed(&newtio,B9600);
        break;
    case 115200:
        cfsetispeed(&newtio,B115200);
        cfsetospeed(&newtio,B115200);
    default:
        cfsetispeed(&newtio,B9600);
        cfsetospeed(&newtio,B9600);
    }

    if(nStop==1)
        newtio.c_cflag &= ~CSTOPB;//CSTOPB表示2位停止位,取反则1位
    else if(nStop==2)
        newtio.c_cflag |=CSTOPB;

    newtio.c_cc[VMIN]=10;//读数据时,最少读10字节,读满才返回
    newtio.c_cc[VTIME]=1;//10秒内没读到第一个位,就直接返回

    tcflush(fd,TCIFLUSH);//清除正在接受的数据

    if((tcsetattr(fd,TCSANOW,&newtio))!=0)//设置串口
    {
        perror("com set error");
        return -1;
    }
    return 0;
}

int main(int argc, char const *argv[])
{
    int fd;
    int iRet;
    char c;

    if(argc!=2){
        printf("Usage: \n");
        printf("%s <dev/ttyX>\n",argv[0]);
        return -1;
    }

    fd=(open_port(argv[1]));
    if(fd<0)
    {
        printf("open %s error \n",argv[1]);
        return -1;
    }
    iRet=set_opt(fd,115200,8,'N',1);
    if(iRet<0)
    {
        printf(("set port error \n"));
        return -1;
    }
    printf("Enter a character");

    while(1)
    {
        scanf("%c",&c);
        iRet=write(fd,&c,1);
        iRet=read(fd,&c,1);
        if(iRet==1)
        {
            printf("get: %02x %c\n",c,c);
        }
        else
        {
            printf("can't get data\n");
        }

    }
    return 0;
}

注意,如果出现以下问题

 原因是VMIN和VTIME为0,串口没读到就立即返回了。

 

IIC应用编程

IIC是同步半双工通信协议,两根线,SCL和SDA,SDA主从都能控制,SCL一般由主机控制,当然,有例外,比如从机需要自己做一些其他工作,就可以拉低SCL,主机就不会继续发送。一般SCL和SDA都要加上拉电阻,这是因为开漏输出无法输出高电平,开漏输出不用切换输入输出模式,并且开漏输出能实现线与功能,当然,IIC详细结构如下

 为什么看起来折磨复杂呢,因为这是开漏输出的硬件结构。这样开漏输出加上上拉电阻,比直接推挽输出安全,如下

 如果IIC协议使用推挽输出,理论可以,但是有问题,当一段输出高电平一端输出低电平,可能就会烧毁电路,而开漏输出加上上拉电阻就不会,观察上面的真值表,当一段输出0,MOS管断开,SDA电平由外部决定,即使另一端输出1,对应MOS管会导通,SDA就会表现为低电平。不会烧毁。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值