LINUX驱动篇(二)驱动开发

发布于:2025-09-03 ⋅ 阅读:(20) ⋅ 点赞:(0)

系列文章目录



总结

字符设备驱动:加载卸载、注册注销(设备号)、操作函数、许可注册
函数指针的用法、主设备号、从设备号、地址映射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函数,比如里面添加一些自己的功能,直接赋值给函数指针,内核调用的时候,调用统一的接口就行了

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

// 示例:一个简单的字符设备驱动
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

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就不需要。

insmod drv.ko
modprobe drv.ko

rmmod drv.ko
modprobe -r drv.ko

注册注销

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

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”可查看使用了的设备号
在这里插入图片描述

设备号详解
// 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”查看的包括了这一部分

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

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:设备名字。

注销字符设备要释放:

void unregister_chrdev_region(dev_t from, unsigned count)

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

打开关闭等操作

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

/* 打开设备 */
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 的消息才能显示在控制台上

#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 中。因为内核空间不能直接操作用户空间的内存

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就是提醒我们注意这是用户空间的,要用函数拷贝,同理用户空间也不能直接访问内核空间的内存

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 文件中

#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是释放映射

void iounmap (volatile void __iomem *addr)

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

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驱动

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

/* 寄存器物理地址 */
#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 参数向量

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

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

改进驱动方式

总结

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

自动注册注销设备号

注册和注销函数register_chrdev 和 unregister_chrdev

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

采用自动申请:

//没有指定,自动申请
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)

注册注销代码:

	/* 注册字符设备驱动 */
	/* 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 文件

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。

相关函数

//初始化
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,

struct class *class_create (struct module *owner, const char *name)
void class_destroy(struct class *cls);

创建设备,删除设备

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)

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

/* 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”的子节点,在根节点“/”最后面输入如下所示内容

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。

  #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 的结构体来完成整型数据的原子操作

typedef struct {
 int counter;
} atomic_t;

atomic_t a;

在这里插入图片描述

自旋锁

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

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

自旋的意思就是原地打转

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

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. 结束采集。

二、


网站公告

今日签到

点亮在社区的每一天
去签到