LINUX驱动篇(二)驱动开发

系列文章目录


文章目录


总结

字符设备驱动:加载卸载、注册注销(设备号)、操作函数、许可注册

函数指针的用法、主设备号、从设备号、地址映射MMU

虚拟地址和物理地址的重新映射ioremap,解映射,iounmap,这里的物理地址不是DDR,是独立的物理地址空间。

设备树修改,里面加哪些内容
pinctrl

临界区保护和原子操作

介绍

三大类驱动:

字符设备:字节流进行输入输出的设备:点灯、IIC、SPI

块设备:复杂,厂家会写好,以存储块为基础,存储器设备驱动,EMMC、NAND、SD卡、U盘

网络设备:不管是有线还是无线的网络,USB WIFI也算(USB接口是字符设备)。

Linux内核使用的是4.1.15,支持设备树,后面要看内核!!!

字符设备驱动

工作原理

顶层应用程序通过调用系统库,进入内核,操作底层的驱动程序来控制硬件

应用程序在用户空间,驱动程序算内核部分,其中的桥梁就是open这些c库函数

驱动程序中有对应的open函数等,对应着这些系统调用的函数

Linux内核中的include/linux/fs.h.文件中有驱动操作函数集合,file_operations结构体


这里补充个函数指针的语法

定义成函数指针,这样后面驱动程序里面编写的时候,就可以将自己编写的open函数,比如里面添加一些自己的功能,直接赋值给函数指针,内核调用的时候,调用统一的接口就行了

注意参数类型和顺序和个数其实都是一样的,只是对自定义结构体内部函数重新赋值,方便

c 复制代码
// 示例:一个简单的字符设备驱动
static int my_open(struct inode *inode, struct file *filp) { /* ... */ }
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *fpos) { /* ... */ }

// 定义设备的操作集合
struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,    // 指向驱动自定义的函数
    .read = my_read,    // 指向驱动自定义的函数
    .write = NULL,      // 不支持写入操作
};

当用户程序调用read(fd, buf, size)时:

内核通过文件描述符fd找到对应的file_operations结构体。

检查read指针是否有效,若有效则调用它,否则返回错误(如-EINVAL)。


常用函数:

第 1589 行,owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。

第 1590 行,llseek 函数用于修改文件当前的读写位置。

第 1591 行,read 函数用于读取设备文件。

第 1592 行,write 函数用于向设备文件写入(发送)数据。

第 1596 行,poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。

第 1597 行,unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。

第 1598 行,compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,

32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是

unlocked_ioctl。

第 1599 行,mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓

冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用

程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。

第 1601 行,open 函数用于打开设备文件。

第 1603 行,release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。

第 1604 行,fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。

第 1605 行,aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的

数据。

驱动框架

加载卸载

一般不编译进内核里面,修改负责,搞成一个模块(.ko文件)

注册这两种操作函数,参数 xxx_init 就是需要注册的具体函数名,当使用"insmod"、"rmmod"就会调用xxx_init和xxx_exit

原名叫insert module 、remove module

c 复制代码
static int __init xxx_init(void)
{
 /* 入口函数具体内容 */
 return 0;
 }
 
 /* 驱动出口函数 */
 static void __exit xxx_exit(void)
 {
/* 出口函数具体内容 */
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

modprobe:module probe,模型探索,会智能提供模块依赖

比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。modprobe就不需要。

c 复制代码
insmod drv.ko
modprobe drv.ko

rmmod drv.ko
modprobe -r drv.ko

注册注销

注册放在init函数里面,注销放在exit里面,多用static保证安全性

c 复制代码
static struct file_operations test_fops;

static int __init xxx_init(void)
{
 /* 注册字符设备驱动 */
 int retvalue = 0;
 retvalue = register_chrdev(200, "chrtest", &test_fops);
if(retvalue < 0){
/* 字符设备注册失败,自行处理 */
 }
 return 0;
 }
 
 /* 驱动出口函数 */
 static void __exit xxx_exit(void)
 {
/* 出口函数具体内容 */
  unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

下面解释一下函数意思
register_chrdev 函数用于注册字符设备,设备号,设备类型,设备的操作函数结构体

major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。

name:设备名字,指向一串字符串。

fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。

unregister_chrdev 函数用户注销字符设备,设备号和设备名

major:要注销的设备对应的主设备号。

name:要注销的设备对应的设备名。

命令"cat /proc/devices"可查看使用了的设备号

设备号详解
c 复制代码
// include/linux/types.h 
typedef __u32 __kernel_dev_t;
//include/uapi/asm-generic/int-ll64.h
typedef unsigned int __u32;

这里名字加了__,是为了区分用户空间和内核空间

dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型,高 12 位为主设备号,低 20 位为次设备号
所以主设备号范围0-4095

次设备号是主设备号下的,比如我们led设备有多个,这样就用一个主,多个次,这样就能扩充数百万的设备

静态分配:有一些主设备号,Linux内核开发者分配掉了,用"cat /proc/devices"查看的包括了这一部分

推荐动态分配:注册前向系统申请,不用自己找一个没用过的

c 复制代码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

dev:保存申请到的设备号。

baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递

增。一般 baseminor 为 0,也就是说次设备号从 0 开始。

count:要申请的设备号数量。

name:设备名字。

注销字符设备要释放:

c 复制代码
void unregister_chrdev_region(dev_t from, unsigned count)

from:要释放的设备号。

count:表示从 from 开始,要释放的设备号数量。

打开关闭等操作

这里里面就不写具体内容了,展示一下结构框架,最后要添加LICENSE,作者信息,LICENSE采用GPL协议

c 复制代码
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
	/* 用户实现具体功能 */
	return 0;
}

/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	/* 用户实现具体功能 */
	return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp,const char __user *buf,size_t cnt, loff_t *offt)
{
	/* 用户实现具体功能 */
	return 0;
}

static int chrtest_release(struct inode *inode, struct file *filp)
{
	/* 用户实现具体功能 */
	return 0;
}

static struct file_operations test_fops = {
	.owner = THIS_MODULE, 
	.open = chrtest_open,
	.read = chrtest_read,
	.write = chrtest_write,
	.release = chrtest_release,
};

/* 驱动入口函数 */
static int __init xxx_init(void)
{
	/* 入口函数具体内容 */
	int retvalue = 0;
	
	/* 注册字符设备驱动 */
	retvalue = register_chrdev(200, "chrtest", &test_fops);
	if(retvalue < 0){
	/* 字符设备注册失败,自行处理 */
	}
	return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

MODULE_LICENSE("GPL");
 MODULE_AUTHOR("xxx");

LINUX内部采用GPL协议,因为其是开源协议,任何链接到内核的代码,必须要遵循GPL协议,并公开源码,其实就是标注我接受开源

实例分析

待实验
Linux首先只有printk函数,运行在内核态,printf是运行在用户态,驱动程序在内核态

printk可以根据日志级别对消息分类,在文件 include/linux/kern_levels.h 里(这个文件在linux源码里面)

不显示调用消息级别,会默认4,只有优先级高于 7 的消息才能显示在控制台上

c 复制代码
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */

//例子:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n"); 

参数 offt 是相对于文件首地址的偏移,kerneldata 里面保存着用户空间要读取的数据,先将 kerneldata 数组中的数据拷贝到读缓冲区 readbuf 中,通过函数 copy_to_user 将readbuf 中的数据复制到参数 buf 中。因为内核空间不能直接操作用户空间的内存

c 复制代码
static char readbuf[100];		/* 读缓冲区 */

static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	
	/* 向用户空间发送数据 */
	memcpy(readbuf, kerneldata, sizeof(kerneldata));
	retvalue = copy_to_user(buf, readbuf, cnt);
	if(retvalue == 0){
		printk("kernel senddata ok!\r\n");
	}else{
		printk("kernel senddata failed!\r\n");
	}
	
	//printk("chrdevbase read!\r\n");
	return 0;
}

这里的__user就是提醒我们注意这是用户空间的,要用函数拷贝,同理用户空间也不能直接访问内核空间的内存

c 复制代码
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
copy_from_user函数同理

led驱动编写

地址映射

MMU:内存管理单元,memory manage unit,老版本的linux必须有,新版本的支持无mmu的
完成虚拟地址到物理空间的映射,即地址映射
内存保护,设置存储器的访问权限,设置虚拟空间的缓冲特性

虚拟地址(VA,Virtual Address)、物理地址(PA,PhyscicalAddress)。
对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间

虚拟地址比物理地址大,那么

malloc申请的是虚拟空间,若物理空间不足,但虚拟空间还够,就能申请,但是标记未访问,实际访问的时候,若物理空间+swap不满足会触发错误,终止进程(自然malloc返回的是虚拟地址)

Swap(交换空间) 是 Linux 系统中用于扩展可用内存的磁盘空间,当物理内存(RAM)不足时,内核会将不活跃的内存页(Pages)临时转移到磁盘上的 Swap 区域,从而腾出物理内存供其他进程使用。

ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间 ,定 义 在arch/arm/include/asm/io.h 文件中

c 复制代码
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)

void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
	return arch_ioremap_caller(phys_addr, size, mtype,__builtin_return_address(0));
}

是个宏,phys_addr:要映射的物理起始地址。size:要映射的内存空间大小。mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。

返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。

iounmap是释放映射

c 复制代码
void iounmap (volatile void __iomem *addr)

对映射后的内存进行读写操作,分别是不同位数的读写操作

c 复制代码
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

LED驱动

这里就放一些前面没了解的代码

c 复制代码
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

/*
 * @description	: 驱动出口函数
 */
static int __init led_init(void)
{
	int retvalue = 0;
	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	/* 6、注册字符设备驱动 */
	retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
	if(retvalue < 0){
		printk("register chrdev failed!\r\n");
		return -EIO;
	}
	return 0;
}

//驱动出口函数
static void __exit led_exit(void)
{
	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

	/* 注销字符设备驱动 */
	unregister_chrdev(LED_MAJOR, LED_NAME);
}

补充一点,当运行./program foo bar,argc是3,文件路径是一个元素,foo是一个元素 bar是一个元素

./ledApp /dev/led 0,所以应用程序里面写的是三个元素

argc:Argument Count 参数数量

argv:argument Vector 参数向量

c 复制代码
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
 }

filename = argv[1];
databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */

改进驱动方式

总结

规范自定义设备结构体,并设置为私有设备;自动创建设备号;初始化设备,并向内核加入设备;自动创建设备节点

自动注册注销设备号

注册和注销函数register_chrdev 和 unregister_chrdev

原来的register_chrdev 需要知道哪个设备号没用,而且会把所有的子设备号分走

采用自动申请:

c 复制代码
//没有指定,自动申请
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//指定主次设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
//统一释放注册函数
void unregister_chrdev_region(dev_t from, unsigned count)

注册注销代码:

c 复制代码
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (newchrled.major) {		/*  定义了设备号 */
		newchrled.devid = MKDEV(newchrled.major, 0);
		register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */
		newchrled.major = MAJOR(newchrled.devid);	/* 获取分配号的主设备号 */
		newchrled.minor = MINOR(newchrled.devid);	/* 获取分配号的次设备号 */
	}
	printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);	
	
	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */

为了规范化,采用字符设备结构体cdev,cdev 结构体在 include/linux/cdev.h 文件

c 复制代码
struct cdev {
 struct kobject kobj;
 struct module *owner;
 const struct file_operations *ops;
 struct list_head list;
 dev_t dev;
 unsigned int count;
};

ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。

相关函数

c 复制代码
//初始化
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
//向Linux添加字符设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
//卸载驱动从内核删除字符设备
void cdev_del(struct cdev *p)

设备号,唯一标识一个设备,供内核识别和管理。
设备节点,用户空间访问设备的接口,本质是文件系统中的特殊文件。通常位于 /dev 目录下(如 /dev/sda、/dev/ttyUSB0)

自动创建设备节点

之前的代码还要命令窗modprobe 加载驱动,mknod手动创建设备节点

驱动中实现自动创建设备节点后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。

mdev实现自动功能,而且热插拔事件也由它管理

创建类,参数 owner 一般为 THIS_MODULE,

c 复制代码
struct class *class_create (struct module *owner, const char *name)
void class_destroy(struct class *cls);

创建设备,删除设备

c 复制代码
struct device *device_create(struct class *class, 
								struct device *parent,
								dev_t devt, 
								void *drvdata, 
								const char *fmt, ...)
void device_destroy(struct class *class, dev_t devt)

所以最后代码,创建个字符设备结构体,规范

c 复制代码
/* newchrled设备结构体 */
struct newchrled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
};
struct newchrled_dev newchrled;	/* led设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}

filp 是 struct file 类型的指针,代表一个打开的文件对象。每当用户空间程序通过 open() 打开设备文件(如 /dev/mydevice)时,内核会创建一个 struct file 实例

private_data 是 struct file 中的一个 void* 类型成员,专门用于驱动存储设备私有数据。它的生命周期与文件对象绑定:当用户调用 open() 时初始化,在 close() 时释放。在后续的 read、write、ioctl 等操作中,通过 filp->private_data 快速获取设备数据,无需每次重新查找。

设备树

设备树在DTS(Decive Tree Source)文件中

树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。DTS就是这样的。

.dts文件,这样不同的开发板直接用这一个文件,然后配置就行了,不然每个开发板都有一个信息文件。

一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。

DTS 是设备树源码文件,DTB 是将DTS 编译以后得到的二进制文件。使用DTB文件编译

详细的就先跳过,直接看实战怎么改

设备树LED驱动实验

打开 imx6ull-alientek-emmc.dts 文件,在根节点"/"下创建一个名为"alphaled"的子节点,在根节点"/"最后面输入如下所示内容

c 复制代码
alphaled {
 #address-cells = <1>;
 #size-cells = <1>;
 compatible = "atkalpha-led";
status = "okay";
 reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
 0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
 0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
 0X0209C000 0X04 /* GPIO1_DR_BASE */
 0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
 };

属性#address-cells 和#size-cells 都为 1,表示reg属性中起始地址一个字长,地址长度也是一个字长,这里是五个寄存器,每个寄存器都是4字节,32位,一个字,所以是1。

c 复制代码
  #address-cells = <2>;
  #size-cells = <1>;
  reg = <0x00000000 0x40000000 0x1000>; // 64位地址0x40000000,长度0x1000

reg 属性,非常重要!reg 属性设置了驱动里面所要使用的寄存器物理地址,比如第 6 行的"0X020C406C 0X04"表示 I.MX6ULL 的 CCM_CCGR1 寄存器,其中寄存器首地址为 0X020C406C,长度为 4 个字节。

设备树中创建节点,增加属性值

在驱动程序中,读取节点属性值,其他的操作不变

pinctrl和gpio

前面直接操作寄存器太繁琐了,容易出问题,上pinctrl(pin control)系统

1.获取设备树中 pin 信息。

2.根据获取到的 pin 信息来设置 pin 的复用功能

3.根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。

打开 imx6ull-alientek-emmc.dts,开始了,上辅助配置,hog1热插拔相关

并发和竞争

在多个任务共同操作同一段内存或者设备的情况,甚至中断都能访问的资源叫做共享资源

并发就是多个"用户"同时访问同一个共享资源

原因:多线程并发访问;抢占式并发访问;中断程序并发访问;SMP(多核)核间并发访问

原子操作

linux提供了原子操作的变量和函数

atomic_t 的结构体来完成整型数据的原子操作

c 复制代码
typedef struct {
 int counter;
} atomic_t;

atomic_t a;

自旋锁

原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区。

如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理

自旋的意思就是原地打转

那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。所以自旋锁适用于短时期的轻量级加锁

c 复制代码
spinlock_t lock; //定义自旋锁

注意会死锁,如果睡眠或阻塞

中断里面可以用自旋锁,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生,关闭本地中断

还有读写自旋锁,一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁

顺序锁:以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作,如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性

自旋锁使用事项:

因为在等待自旋锁的时候处于"自旋"状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。

自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。

不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须"自旋",等待锁被释放,然而你正处于"自旋"状态,根本没法释放锁。结果就是自己把自己锁死了!

在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。

块设备驱动

网络设备驱动

现在不需要网卡了,集成到一个芯片里面了,

SOC内部有网络外设MAC,之后还要配一个PHY芯片

如果没有,会有外置MAC芯片,SRAM接口

内部的 MAC 外设会通过 MII 或者 RMII 接口来连接外部的 PHY 芯片,MII/RMII 接口用来

传输网络数据。

配置或读取 PHY 芯片,读写 PHY 的内部寄存器,叫做 MIDO,MDIO 很类似 IIC,也是两根线,一根数据线叫做 MDIO,一根时钟线叫做 MDC。

V4L2驱动框架

  1. 首先是打开摄像头设备;
  2. 查询设备的属性或功能;
  3. 设置设备的参数,譬如像素格式、帧大小、帧率;
  4. 申请帧缓冲、内存映射;
  5. 帧缓冲入队;
  6. 开启视频采集;
  7. 帧缓冲出队、对采集的数据进行处理;
  8. 处理完后,再次将帧缓冲入队,往复;
  9. 结束采集。

二、

相关推荐
FoldWinCard4 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
零涂毕业设计6 小时前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud1236 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
霸道流氓气质6 小时前
KMS 密钥管理服务(Key Management Service)原理与实践
linux·服务器·数据库
SakitamaX6 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.6 小时前
Linux系统管理
linux·运维
阿龙的工作记录7 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手7 小时前
Linux的基本指令
linux·运维·服务器
hkj88088 小时前
Ubuntu 切换java版本
java·linux·ubuntu
撩得Android一次心动9 小时前
Linux编程笔记3【个人用】
linux·笔记