系列文章目录
文章目录
总结
字符设备驱动:加载卸载、注册注销(设备号)、操作函数、许可注册
函数指针的用法、主设备号、从设备号、地址映射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驱动框架
- 首先是打开摄像头设备;
- 查询设备的属性或功能;
- 设置设备的参数,譬如像素格式、帧大小、帧率;
- 申请帧缓冲、内存映射;
- 帧缓冲入队;
- 开启视频采集;
- 帧缓冲出队、对采集的数据进行处理;
- 处理完后,再次将帧缓冲入队,往复;
- 结束采集。