系列文章目录


文章目录

  • 系列文章目录
  • 总结
  • 介绍
  • 字符设备驱动
    • 工作原理
    • 驱动框架
      • 加载卸载
      • 注册注销
        • 设备号详解
      • 打开关闭等操作
      • 实例分析
      • led驱动编写
        • 地址映射
    • LED驱动
    • 改进驱动方式
      • 总结
      • 自动注册注销设备号
      • 自动创建设备节点
    • 设备树
      • 设备树LED驱动实验
    • pinctrl和gpio
    • 并发和竞争
      • 原子操作
      • 自旋锁
  • 块设备驱动
  • 网络设备驱动
  • V4L2驱动框架
  • 二、


总结

字符设备驱动:加载卸载、注册注销(设备号)、操作函数、许可注册
函数指针的用法、主设备号、从设备号、地址映射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.kormmod 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. 结束采集。

二、

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/95434.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/95434.shtml
英文地址,请注明出处:http://en.pswp.cn/web/95434.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【工具】开源大屏设计器 自用整理

【工具】开源大屏设计器 自用整理 GoView低代码数据可视化 GoView 说明文档 | 低代码数据可视化开发平台 JimuReport积木报表(免费报表工具) https://github.com/jeecgboot/JimuReport 「数据可视化&#xff1a;报表、大屏、数据看板」积木报表是一款类Excel操作风格&#xf…

.NetCore MVC

这个是我自己记得笔记&#xff0c;最好有点基础看我的。 html 辅助标签 Html.DropList 分布视图 使用 RenderPartialAsync 呈现分部视图。 此方法不返回 IHtmlContent。 它将呈现的输出直接流式传输到响应。 因为该方法不返回结果&#xff0c;所以必须在 Razor 代码块内调用它…

@GitLab 介绍部署使用详细指南

文章目录**GitLab 介绍&部署&使用详细指南****1. GitLab 介绍与核心概念****1.1 什么是 GitLab&#xff1f;****1.2 核心特性****1.3 版本区别****2. 部署指南 (以 Ubuntu 22.04 LTS 为例)****2.1 环境准备****2.2 安装步骤****2.3 重要配置文件****3. 基本使用入门***…

如何通过 AI IDE 集成开发工具快速生成简易留言板系统

在当今快速迭代的软件开发环境中&#xff0c;AI 辅助编程工具已经成为开发者提高效率的重要手段。本文将详细介绍如何利用 AI IDE 集成开发工具快速构建一个功能完整的简易留言板系统&#xff0c;涵盖从需求分析到部署上线的全过程&#xff0c;并提供完整代码、流程图、Prompt …

机器学习:从技术原理到实践应用的深度解析

目录引言一.什么是机器学习&#xff08;ML&#xff09;&#xff1f;——从技术本质到核心目标1.与传统编程的本质区别&#xff1a;规则的“来源不同”2.核心目标&#xff1a;在“偏差-方差权衡”下优化性能指标二.机器学习的核心分类——基于“数据标签”与“学习范式”的技术划…

[muduo网络库]-muduo库TcpServer类解析

本贴用于记录muduo库的学习过程&#xff0c;以下是关于TcpServer的个人理解。 TcpServer内含Acceptor、threadpool等类&#xff0c;算是把主线程所有要做的事封装了起来。 重要成员变量 EventLoop *loop_; // baseloop 用户自定义的loopconst std::string ipPort_;const std…

工作两年,最后从css转向tailwind了!

菜鸟上班已经两年了&#xff0c;从一个对技术充满热情的小伙子&#xff0c;变成了一个职场老鸟了。自以为自己在不停的学习&#xff0c;但是其实就是学一些零碎的知识点&#xff0c;比如&#xff1a;vue中什么东西没见过、js什么特性没用过、css新出了个啥 …… 菜鸟感觉自己也…

macOS 更新后找不到钥匙串访问工具的解决方案

macOS 更新后找不到钥匙串访问工具的解决方案 随着macOS的不断更新&#xff0c;一些系统工具的位置可能会发生变化&#xff0c;给用户带来不便。钥匙串访问&#xff08;Keychain Access&#xff09;是macOS中一个非常重要的工具&#xff0c;用于管理密码、证书等敏感信息。最近…

深入理解Go 与 PHP 在参数传递上的核心区别

$run_return_data []; $ret $this->handleData($event_req_info, $run_return_data); public function handleData($event_req_info, &$run_return_data): array {$run_return_data [ //使用引用变量返回数据shop_id > $shop_id,request_id > $request_…

【Dify智能体】2025 最新版Linux部署Dify教程(Ubuntu)

一、前言 Dify 是一款开源的智能体工作流平台,可以用来快速构建 AI 应用。相比手动搭建复杂的依赖环境,Docker Compose 部署方式更简单、更快速、更稳定。本文将一步步带你在 Ubuntu 22.04 + Docker Compose v2 上安装 Dify,并给出常见问题与优化方案。 ps:如果还没有安装…

基础思想:动态规划与贪心算法

一、动态规划核心思想&#xff1a;将复杂问题分解为相互重叠的子问题&#xff0c;通过保存子问题的解来避免重复计算&#xff08;记忆化&#xff09;。动态规划需要通过子问题的最优解&#xff0c;推导出最终问题的最优解&#xff0c;因此这种方法特别注重子问题之间的转移关系…

使用生成对抗网络增强网络入侵检测性能

文章目录前言一、GAN 模型介绍二、研究方法1.数据集选择与处理2.IDS 基线模型构建3. GAN 模型设计与样本生成4.生成样本质量评估三、实验评估四、总结前言 网络入侵检测系统&#xff08;Network Intrusion Detection System, NIDS&#xff09;在保护关键数字基础设施免受网络威…

VR森林经营模拟体验带动旅游经济发展

将VR森林经营模拟体验作为一种独特的旅游项目&#xff0c;正逐渐成为旅游市场的新热点。游客们无需长途跋涉前往深山老林&#xff0c;只需在旅游景区的VR体验中心&#xff0c;戴上VR设备&#xff0c;就能开启一场奇妙的森林之旅。在虚拟森林中&#xff0c;他们可以尽情探索&…

Vue2存量项目国际化改造踩坑

Vue2存量项目国际化改造踩坑 一、背景 在各类业务场景中&#xff0c;国际化作为非常重要的一部分已经有非常多成熟的方案&#xff0c;但对于一些存量项目则存在非常的改造成本&#xff0c;本文将分享一个的Vue2项目国际化改造方案&#xff0c;通过自定义Webpack插件自动提取中文…

硬件开发(1)—单片机(1)

1.单片机相关概念1.CPU&#xff1a;中央处理器&#xff0c;数据运算、指令处理&#xff0c;CPU性能越高&#xff0c;完成指令处理和数据运算的速度越快核心&#xff1a;指令解码执行数据运算处理2.MCU&#xff1a;微控制器&#xff0c;集成度比较高&#xff0c;将所有功能集成到…

Elasticsearch面试精讲 Day 4:集群发现与节点角色

【Elasticsearch面试精讲 Day 4】集群发现与节点角色 在“Elasticsearch面试精讲”系列的第四天&#xff0c;我们将深入探讨Elasticsearch分布式架构中的核心机制——集群发现&#xff08;Cluster Discovery&#xff09;与节点角色&#xff08;Node Roles&#xff09;。这是构…

微信小程序长按识别图片二维码

提示&#xff1a;二维码图片才能显示识别菜单1.第一种方法添加属性&#xff1a;show-menu-by-longpress添加属性&#xff1a;show-menu-by-longpress <image src"{{shop.wx_qrcode}}" mode"widthFix" show-menu-by-longpress></image>2.第二种…

智能化数据平台:AI 与大模型驱动的架构升级

在前面的文章中,我们探讨了 存算分离与云原生,以及 流批一体化计算架构 的演进趋势。这些演进解决了“算力与数据效率”的问题。但在今天,企业在数据平台上的需求已经从 存储与计算的统一,逐步走向 智能化与自动化。 尤其是在 AI 与大模型快速发展的背景下,数据平台正在发…

解锁 Vue 动画的终极指南:Vue Bits 实战进阶教程,让你的Vue动画比原生动画还丝滑,以及动画不生效解决方案。

一条 Splash Cursor 的 10 秒 Demo 视频曾创下 200 万 播放量&#xff0c;让 React Bits 风靡全球。如今&#xff0c;Vue 开发者终于迎来了官方移植版 —— Vue Bits。 在现代 Web 开发中&#xff0c;UI 动效已成为提升用户体验的关键因素。Vue Bits 作为 React Bits 的官方 Vu…

《微服务协作实战指南:构建全链路稳健性的防御体系》

在微服务架构从“技术尝鲜”迈向“规模化落地”的进程中&#xff0c;服务间的协作不再是简单的接口调用&#xff0c;而是涉及超时控制、事务一致性、依赖容错、配置同步等多维度的复杂博弈。那些潜藏于协作链路中的隐性Bug&#xff0c;往往不是单一服务的功能缺陷&#xff0c;而…