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就会表现为低电平。不会烧毁。

IIC Tools是一个开源的I2C库,IMX6ULL上已经配置有,如果没有的话,需要去官网下载源码,并且实现交叉编译,然后就能使用I2C Tools的一些可执行文件,直接读取/写入I2C设备,这样就不用自己写应用程序。

I2C Tools有两种协议,SMBus和I2C协议,都能直接操作I2C设备。SMBus主要使用i2cset和i2cget程序,而i2c主要使用i2ctransfer程序。

 要想访问I2C设备,需要I2C驱动程序,驱动程序Linux内核就有,那么如何知道设备挂载在哪一条总线呢,可以使用命令i2cdetect -y 0,表示查看0号总线的设备,-y表示yes,可以看到,0x1e就是板载的光照距离传感器的i2c地址。

 i2cset,写入i2c设备方法

-f表示强制,0为总线,0x1e为传感器的地址,第二个0为寄存器地址,0x4为数据 ,写入0x3后传感器被使能。

i2cget

 可以看到,随着光照强度变化,读出数据也在变化。

i2ctransfer用法,原始的i2c协议。

 w2@0x1e表示往0x1e设备写两个字节,为什么是两字节,因为第一个字节是寄存器地址,这是i2c特性,往一个寄存器写值,先要发送起始信号+设备地址和读写位+寄存器地址,再重复起始,发送写入的值。

读取数据

 同一个函数,要读取某个寄存器,也要先发送寄存器地址,再重复起始,读取数据。因为发送寄存器的时候指定了设备地址,后面读的时候就可以省略。

i2c源码阅读

可以看到设备节点与总线一一对应

通过ioctl指定设备地址,对于内核中有驱动的 (i2cdetect查看得到UU的设备),如果想直接使用i2c物理地址读写数据,传参时必须传入-f,表示强制。

 关于ioctl

 rdwr是结构体,包含两个成员,一个是message消息结构体,一个是消息条数,message结构体内部包含消息发送接收的设备地址,标志位,读还是写,存放数据的buff等等

 自己编写应用程序访问EEPROM

#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>
#include "i2cbusses.h"

/*
./at24c02 /dev/i2c-0 w "lilin is so handsome"
*/
int main(int argc, char **argv)
{
    unsigned char addr = 0x50;
    unsigned char *str;
    unsigned char memaddr=0;
    unsigned char buff[32]={0};
    char filename[10];
    int force = 1;
    int fd;
    if (argc != 4)
    {
        printf("Useage:\n");
        printf("%s /dev/i2c-0|i2c-1 w string\n", argv[0]);
        printf("%s /dev/i2c-0|i2c-1 r \n", argv[0]);
        return -1;
    }
    fd = open_i2c_dev(argv[1][0] - '0', filename, sizeof(filename), 0);
    if (fd < 0)
    {
        printf("can't open dev%s\n", argv[1]);
        return -1;
    }

    if (set_slave_addr(fd, addr, force) < 0)
    {
        printf("set addr failed\n");
        return -1;
    }
    if(argv[0][2]=='w'){
        str=argv[3];
        while (*str)
        {
        i2c_smbus_write_byte_data(fd,memaddr++,*str++);
            
        }
        i2c_smbus_write_byte_data(fd,memaddr,0);
        
    }else{
        //read,连续读
        i2c_smbus_read_i2c_block_data(fd,memaddr,sizeof(buff),buff);
        buff[sizeof(buff)-1]=0;//添加'\0'
        printf("get data %s\n",buff);
    }
}

上面代码中,i2csmbuses.c和i2csmbus.h都是i2ctools里面的,虚拟机是没有编译整合i2ctools的,要想在·编译的时候不报错,有两种办法,一是虚拟机编译i2ctools,一种是直接复制这两个文件,选择第二种。

 这些文件存放在windows与linux的共享文件夹,可以在虚拟机访问到,可以编译。

编写make命令

 CROSS_COMPILE表示使用交叉编译工具链,

先执行命令设置工具链

export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin

make后报错

 解决办法,需要提供i2ctools里的include文件夹,直接复制过来。

 并且修改Makefikle,-I表示指定文件夹路径

再编译

报错,是链接错误,提示未找到函数定义

这两个函数定义在i2ctools的lib下,复制过来。

然后修改Makefile,包含smbus.c

 再次编译

编译成功

使用cp命令将程序拷贝到网络文件系统

然后打开开发板,连接串口线,挂载网络文件系统,然后执行

开发板密码root

 用虚拟机ping开发板eth0的ip 192.168.5.9,失败,需要设置开发版网卡ip

执行ifconfig eth0 192.168.5.9

 执行后ping虚拟机和主机都能ping通,然后挂载网络文件系统

在开发板执行mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt

 然后就能在开发板执行rw_eeprom了

发现写入只能写入第一个字节,看文档发现,每次入一个字节要延迟一段时间twr,为10ms

 sleep单位是s,要使用单位为ms的nanosleep,虚拟机命令man nanosleep查看用法

修复代码,包含time.h,定义结构体并复制,然后每次写入调用nanosleep延时20ms

 重新编译测试

发现读取无法进行

检查代码发现,参数判断错误

改为argc!=4 && argc!=3即可

由于手里没有i2c的eeprom,未进行最后的测试,后续想办法

驱动开发部分

第一个驱动程序,01_hello-drv,目录结构如下

 首先,驱动程序是什么,是对一些硬件级别或者内核级别的操作的封装,使应用程序能直接调用,比如Linux内核有自己的open,read,write等函数,而驱动程序可以对其封装。

大概的编写步骤如下

先确定主设备号(可以传入0,让内核为驱动程序分配主设备号),在编写自己的file_operations结构体,该结构体内声明驱动程序自定义的函数,然后将该结构体内告诉内核,其实就是注册,可以理解为将该结构体放入内核的一个数组,注册通过内核提供的register_chrdev帮助函数,那么具体放入数组的哪一项呢,则根据主设备号确定,同理,打开设备节点时,也是根据主设备号在一堆驱动程序中找到对应的驱动。

那么谁来调用注册函数呢,有一个入口函数,安装驱动时就会调用入口函数,同样,卸载驱动时会调用出口函数,出口函数内会调用unregister_chrdev。

在之前的文件IO讲解中,m=open("dev/xxx",flags,mode),m就是返回的文件句柄,类型定义如下,在fs.h里,不难猜到,传入的参数保存在了m的相关成员里。可以看到,里面有个file_operation结构体。

打开字符设备时,内核中也有对应的struct file,注意上面的f_op成员,struct file_operation的定义如下

 当应用程序调用opne函数时,会根据主设备号在内核中的chrdevs[]里找到对应的file_operation,后面如果再对该文件调用read/write函数,则会自动调用file_operation里的专属的read/write函数。

第一次写驱动,不知道包含什么头文件,直接复制linux内核源码的misc.c文件包含的头文件,

确定主设备号,可以设为0 

 

定义自己的file_operation函数,前面已经看过,file_operation结构体内部有许多的成员,我们只需要对需要的成员赋值。

 实现上面的几个函数

找到file_operation结构体定义,参考对应函数

 参照修改后如下,加static是为了防止污染命名空间

先实现一个简单的功能,将应用程序传入的参数存到驱动里

需要创建一个buff

不能直接拷贝,要使用函数,这两个函数是用于内核与用户空间数据交换。printk是打印,MIN是取较小值的宏定义。

然后需要将file_operation结构体告诉内核,则要定义入口函数

 参考misc.c里的入口函数

如下

 

 入口函数出口函数写好了,可是对于Linux来说他不知道这是出口函数,所以还需要操作,module_driver可以替代moduke_inti和module_exit,并且驱动要想正常使用,必须遵循GPL协议,开源,否则无法使用。

下面是moduke_driver定义 

 提供设备信息,自动创建节点,继续编写完善入口函数

定义hello_class结构体变量

创建class

创建class后,还要创建一个dev,这样就能通过/dev/hello打开设备节点了

到此,简单的hello驱动程序已经完成,总体代码如下

printk是内核里的打印函数

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

#include <linux/uaccess.h> // copy_to_user & copy_from_user
#include <linux/moduleparam.h>
#include <linux/raid.h>
// 第一次编写驱动,不知道怎么写,复制misc.c里的头文件

#define MIN(a, b) (a < b ? a : b)

// 主设备号
static int major = 0;
static char kernel_buff[1024];
static struct class *hello_class;

static ssize_t (*hello_drv_read)(struct file *, char __user *buf, size_t size, loff_t *offset)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    copy_to_user(buf, kernel_buff, MIN1024, size);
    return MIN(1024, size);
}
static ssize_t (*hello_drv_write)(struct file *, char __user *buf, size_t size, loff_t *offset)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    copy_from_user(kernel_buff, buf, MIN(1024, size));
    return MIN(1024, size);
}

static int (*hello_drv_open)(struct inode *node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
}
static int (*hello_drv_close)(struct inode *node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
}

static struct file_operations hello_drv
{
    .owner = THIS_MODULE,
    .open = hello_drv_open,
    .read = hello_drv_read,
    .write = hello_drv_write,
    .release = hello_drv_close
};

// 入口函数
static int __init hello_init(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    int err;
    major = register_chrdev(0, "hello", &hello_drv);

    hello_class = class_create(THIS_MODULE, "hello_class");
    err = PTR_RET(hello_class);
    if (IS_ERR_VALUE(hello_class))
    {
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

        unregister_chrdev(major, "hello");
        return -1;
    }

    // major主设备号,从设备号为0
    //   /dev/hello
    device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
    class_destroy(hello_class);

    return 0;
}
// 出口函数
static void __exit hello_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    unregister_chrdev(major, "hello");
}

// 代替modle_init和modle_exit
module_driver(&hello_drv, hello_init, hello_exit);

MODULE_LICENSE("GPLv2");

接下来编写应用程序

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

int main(int argc, char const *argv[])
{
    int fd;
    int ret;
    int len;
    char buff[1024];

    if (argc < 2)
    {
        printf("Usage1: %s -w <string>\n", argv[0]);
        printf("Usage2: %s -r\n", argv[0]);
        return -1;
    }

    fd = open("dev/hello", O_RDWR);
    if (fd < 0)
    {
        printf("open /dev/hello failed\n");
        return -1;
    }

    if ((strcmp(argv[1], "-w") == 0) && argc == 3) // 写
    {
        len = strlen(argv[2]) + 1;
        len = len > 1024 ? 1024 : len;
        write(fd, argv[2], len);
    }
    else // 读
    {
        len = read(fd, buff, 1024);
        buff[1023] = 0; //'\0'
        printf("APP read: %s\n", buff);
    }

    close(fd);
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值