linux驱动篇

linux驱动开发与裸机开发的区别

裸机直接操作寄存器,有些mcu提供了库,但还是很底层

1、linux驱动开发直接操作寄存器很麻烦不现实,主要是根据linux驱动框架进行开发(就是有很多操作都是一样的,我们只需要对一个程序模板进行一些修改就可以实现不同驱动的开发)

2、驱动最终的表现就是/dev/xxx这样的一个文件(应用程序打开驱动文件或者关闭驱动文件)

3、内核支持设备树文件,一个dts文件,文件描述了板子的设备信息(使用了哪些引脚,引脚是高电平还是低电平,地址是多少之类的)

三大驱动分类

字符驱动

从最简单的点 灯到 I2C、SPI、音频等都属于字符设备驱动的类型

块驱动

所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、SD 卡和 U 盘等存储 设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备

网络设备驱动

就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。一个设备可以 属于多种设备驱动类型,比如 USB WIFI,其使用 USB 接口

字符设备驱动

在单片机中驱动与应用是在一个文件中,但是linux中驱动与应用程序是分开的

驱动获得外设、或者传感器数据,控制外设然后提交给应用程序

应用程序运行在用户空间,驱动程序运行在内核空间,用户空间的程序无法直接访问内核空间的数据,一般有三种方法

1、系统调用(比如应用函数使用一个open函数*(c语言库函数),然后open函数就会调用内核中的open驱动对应的函数)

2、异常(中断)

3、陷入·

实验一、虚拟节点LED灯

字符设备驱动框架

字符设备驱动的编写主要就是驱动对应的open、close、read。。。其实就是

file_operations结构体的成员变量的实现

驱动模块的加载与卸载

Linux驱动程序可以编译到kernel里面,也就是zImage,也可以编译为模块,.ko。测试的时候只需要加载.ko模块就可以

加载驱动会用到加载命令:insmod

移除驱动使用命令rmmod。

驱动模块加载成功以后可以使用lsmod查看一下。

三、字符设备的注册与注销

1、我们需要向系统注册一个字符设备,使用函数register_chrdev。

2、卸载驱动的时候需要注销掉前面注册的字符设备,使用函数unregister_chrdev,注销字符设备。

四、设备号

1,Linux内核使用dev_t。

typedef __kernel_dev_t       dev_t;

typedef __u32 __kernel_dev_t;

typedef unsigned int __u32;

2、Linux内核将设备号分为两部分:主设备号和次设备号。主设备号占用前12位,次设备号占用低20位。

主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备

3、设备号的操作函数,或宏

从dev_t获取主设备号和次设备号,MAJOR(dev_t),MINOR(dev_t)。也可以使用主设备号和次设备号构成dev_t,通过MKDEV(major,minor)

五、file_operations的具体实现

file_operation就是驱动的操作函数

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*mremap)(struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
};

六、字符设备驱动框架的搭建

七、chrdevbase虚拟设备驱动的完善

代码

驱动核心代码

file_operations结构体是驱动实际操作函数集合

register_chrdev字符设置注册函数,也就是将设置写到一个特定的数组中去

unregister_chrdev注销驱动设置

应用的write、read会通过内核调用到驱动中的write、read函数

linux中一切都是文件,要对驱动写入数据要先打开驱动文件

将fileoperation结构体存放到一个对应的数组中,这就叫做注册到内核中,内核会根据主设备号在这个数组中存放查找我们结构体,注册设备时候设置的设备名字就会在开发板中生成一个对应的设置节点

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: chrdevbase.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: chrdevbase驱动文件。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/

#define CHRDEVBASE_MAJOR	200				/* 主设备号 */
#define CHRDEVBASE_NAME		"chrdevbase" 	/* 设备名     */

static char readbuf[100];		/* 读缓冲区 */
static char writebuf[100];		/* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase open!\r\n");
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
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;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	/* 接收用户空间传递给内核的数据并且打印出来 */
	retvalue = copy_from_user(writebuf, buf, cnt);
	if(retvalue == 0){
		printk("kernel recevdata:%s\r\n", writebuf);
	}else{
		printk("kernel recevdata failed!\r\n");
	}
	
	//printk("chrdevbase write!\r\n");
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase release!\r\n");
	return 0;
}

/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE,	
	.open = chrdevbase_open,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
	.release = chrdevbase_release,
};

/*
 * @description	: 驱动入口函数 
 * @param 		: 无
 * @return 		: 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
	int retvalue = 0;

	/* 注册字符设备驱动 */
	retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
	if(retvalue < 0){
		printk("chrdevbase driver register failed\r\n");
	}
	printk("chrdevbase init!\r\n");
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
	printk("chrdevbase exit!\r\n");
}

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

/* 
 * LICENSE和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");


应用代码

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: chrdevbaseApp.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: chrdevbase驱测试APP。
其他	   	: 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>
  			 argv[2] 1:读文件
  			 argv[2] 2:写文件		
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/

static char usrdata[] = {"usr data!"};

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	char readbuf[100], writebuf[100];

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

	filename = argv[1];

	/* 打开驱动文件 */
	fd  = open(filename, O_RDWR);
	if(fd < 0){
		printf("Can't open file %s\r\n", filename);
		return -1;
	}

	if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
		retvalue = read(fd, readbuf, 50);
		if(retvalue < 0){
			printf("read file %s failed!\r\n", filename);
		}else{
			/*  读取成功,打印出读取成功的数据 */
			printf("read data:%s\r\n",readbuf);
		}
	}

	if(atoi(argv[2]) == 2){
 	/* 向设备驱动写数据 */
		memcpy(writebuf, usrdata, sizeof(usrdata));
		retvalue = write(fd, writebuf, 50);
		if(retvalue < 0){
			printf("write file %s failed!\r\n", filename);
		}
	}

	/* 关闭设备 */
	retvalue = close(fd);
	if(retvalue < 0){
		printf("Can't close file %s\r\n", filename);
		return -1;
	}

	return 0;
}



实验二、LED灯实验(直接操作寄存器)

linux中不可以直接和裸机开发一样直接操作寄存器,因为会开启mmu实现内存映射,就是把开发板中的内存映射到4g的虚拟内存中去,访问的都是虚拟地址

使用函数ioremap直接操作物理地址实现linux'中对应的虚拟地址进行操作

iounmap函数释放ioremap函数的映射

在对映射后的内存进行操作有一些专门的函数,readl、writel、这类函数

在驱动写数据的时候要先将用户空间的数据搬到内核空间,应用写数据就是要将缓冲区的数据存放到文件中

代码中对硬件实际操作的函数就是led_switch函数

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: led.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: LED驱动文件。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/
#define LED_MAJOR		200		/* 主设备号 */
#define LED_NAME		"led" 	/* 设备名字 */

#define LEDOFF 	0				/* 关灯 */
#define LEDON 	1				/* 开灯 */
 
/* 寄存器物理地址 */
#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		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);//将用户空间的数据搬到内核空间
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 驱动硬件,打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
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;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
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);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

应用

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: ledApp.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: chrdevbase驱测试APP。
其他	   	: 无
使用方法	 :./ledtest /dev/led  0 关闭LED
		     ./ledtest /dev/led  1 打开LED		
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/

#define LEDOFF 	0
#define LEDON 	1

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[1];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开led驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

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

	/* 向/dev/led文件写入数据 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 关闭文件 */
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

实验三、linux新字符设备驱动实验

对注册设备的函数进行了修改,原来的注册函数会将次设备号都占了无法设置次设备号

不需要直接去查找空闲设备号

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)

参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一
般都是一个;参数 name 是设备名字

实际的驱动编写

 与旧框架相比字符设备号的注册分成两步

新创建了一个字符设置结构体来保存不同设备的私有属性cdev

1、设备号的释放与生成

2、设备号的注册

在将设备号注册进内核中后,就可以根据设备号来创建设备节点

新框架自动创建设置节点

1、创建设备类

2、创建设备

设备的私有属性

 驱动代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: newchrled.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: LED驱动文件。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/6/27 左忠凯创建
***************************************************************/
#define NEWCHRLED_CNT			1		  	/* 设备号个数 */
#define NEWCHRLED_NAME			"newchrled"	/* 名字 */
#define LEDOFF 					0			/* 关灯 */
#define LEDON 					1			/* 开灯 */
 
/* 寄存器物理地址 */
#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;

/* 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		: 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		: 打开设备
 * @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;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations newchrled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	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);



	/* 注册字符设备驱动 */

    /*1、分配和释放设备号*/
	/* 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);	

	/*1、新的字符设备注册方法*/
	
	/* 2、初始化cdev */
	newchrled.cdev.owner = THIS_MODULE;
	cdev_init(&newchrled.cdev, &newchrled_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

	/* 4、创建类 */
	newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.class)) {
		return PTR_ERR(newchrled.class);
	}

	/* 5、创建设备 */
	newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
	if (IS_ERR(newchrled.device)) {
		return PTR_ERR(newchrled.device);
	}
	
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
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);

	/* 注销字符设备驱动 */
	cdev_del(&newchrled.cdev);/*  删除cdev */
	unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */

	device_destroy(newchrled.class, newchrled.devid);
	class_destroy(newchrled.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

设备树

将描述设置的信息文件存放在内核之外,形成一个dtbs文件,我们内核可以读取dtb文件获取设备的信息

 也就是相当于说明书,一块板子有一个说明书,说明书告诉我们板子有什么东西,该怎么用

dts相当于c文件,就是dts源码d

dtc工具相当于gcc编译器,将dts编译成dtb

dtb相当于bin文件,或可执行文件

设备树的语法

/ 根节点

把设备树共有的部分做成一个库(dtsi)调用,不同板子特有的设备信息采用追加的方式写到设备树中(dts)

1、以根节点为例,要是这两个都有根节点,项目的实际根节点就是这二者之和。普通子节点要是多个文件都含有也是相互追加合并的

2、dts文件是被最先加载的,dtsi后被加载

3、在节点后面前加上@也是追加,一般加到根节点下面

4、intc: interrupt-controller@00a01000。int是标签,我们以后可以使用&intc去访问interrupt-controller

5、有的节点有很多级,只有最后一级才会生成具体的设备节点

6、设备树文件可以在根文件系统中体现出来,以文件夹的形式

dtsi文件中的i2c1截图有点问题,看成是i2c1就可以)

 在dts文件中对i2c2进行追加(

 

设备树在系统中的体现

系统启动以后可以在根文件系统里面看到设备树的节点信息,在/proc/device-tree/目录下存放这设备树信息

内核启动的时候会分析解析设备树,然后在/proc/device-tree/目录下呈现出来

/*自定义节点*/ 

my testnode: mytest@0101{

 

}

设备树的特殊节点

aliases 子节点

该节点用来定义结点的别名,,定义别名的目 的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label 来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点

aliases {
19 can0 = &flexcan1;
20 can1 = &flexcan2;

chosen子节点

chose并不是一个真实的设备,chosen节点主要为了uboot向linux内核传递数据,重点是bootaegs参数,在内核启动的时候会写入到chose节点中去

chosen {
 stdout-path = &uart1;
 };

特殊属性

1、compatible属性,值是字符串列表。

       根节点/下面的compatible。内核启动的时候会检查是否支持此平台。该属性用来将设备和驱动绑定起来,字符串列表用来选择设备所要使用的驱动程序,查找哪些驱动程序可以兼容这个设置就选择那个驱动程序

2、address-cells 和size-cells属性

描述子节点的地址信息

adderss-cells属性值决定了子节点reg属性中地址信息所占用的字长,size-cells决定了子节点reg属性中长度信息所占的字长

3、reg

(address,lengrh)一般用来描述设备地址空间范围,地址长度也就是几位的数据

绑定信息文档

设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属 性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在 Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为: Linux 源码目录/Documentation/devicetree/bindings,

 设备树常用 OF 操作函数

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的, 我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存 器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的

       1、驱动如何获取到设备树中节点信息。在驱动中使用OF函数获取设备树属性内容。

       2,驱动要想获取到设备树节点内容,首先要找到节点。

设备树下的led驱动实验

将硬件的寄存器地址写到设备树中,然后驱动代码通过解析设备树来获得寄存器地址

alphaled(节点名) {
2 #address-cells = <1>;   //地址
3 #size-cells = <1>;      //地址大小
4 compatible = "atkalpha-led";
5 status = "okay";
6 reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */  《地址   地址多少位》
7 0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
8 0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
9 0X0209C000 0X04 /* GPIO1_DR_BASE */
10 0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
11 };

设备节点结构体存放在设备结构体中,驱动代码通过函数通过设备节点指针访问属性

struct dtsled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
};

注册字符设备驱动,也就是将字符设备驱动节点写入一个数组中,便于内核查找

/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (dtsled.major) {		/*  定义了设备号 */
		dtsled.devid = MKDEV(dtsled.major, 0);
		register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME);	/* 申请设备号 */
		dtsled.major = MAJOR(dtsled.devid);	/* 获取分配号的主设备号 */
		dtsled.minor = MINOR(dtsled.devid);	/* 获取分配号的次设备号 */
	}
	printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);	
	
	/* 2、初始化cdev */
	dtsled.cdev.owner = THIS_MODULE;
	cdev_init(&dtsled.cdev, &dtsled_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

	/* 4、创建类 */
	dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
	if (IS_ERR(dtsled.class)) {
		return PTR_ERR(dtsled.class);
	}

	/* 5、创建设备 */
	dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
	if (IS_ERR(dtsled.device)) {
		return PTR_ERR(dtsled.device);
	}
	

驱动代码使用一些函数通过设备节点才回去设备树的属性

pinctrl和gpio子系统实验

pinctrl子系统

借助pinctrl来设置一个PIN的复用(用作什么功能)和电气属性。

 打开imx6ull.dtsi:

一个控制器可以管理很多个gpio口

根据设备的类型,创建对应的子节点,然后设备所用PIN都放到此节点

//iomuxc控制器
 &iomuxc {

 pinctrl-names = "default";
 pinctrl-0 = <&pinctrl_hog_1>;


//imx6ull开发板的引脚

  imx6ul-evk {
//一个引脚
  pinctrl_hog_1: hoggrp-1 {
  fsl,pins = <
  MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
  MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059
  MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059
  MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058
  >;
  };
......
//一个引脚
 pinctrl_flexcan1: flexcan1grp{
fsl,pins = <
MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX 0x1b020
MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX 0x1b020
 >;
};

 如何添加一个PIN的信息

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19         0x0090 0x031C 0x0000 0x5 0x0

<mux_reg     conf_reg  input_reg     mux_mode  input_val>

0x0090         0x031C  0x0000          0x5            0x0

IOMUXC父节点首地址0x020e0000,因此UART1_RTS_B这个PIN的mux寄存器地址 就是:0x020e0000+0x0090=0x020e 0090。

conf_reg:0x020e0000+0x031C=0x020e 031C,这个寄存器就是UART1_RTS_B的电气属性配置寄存器。

input_reg,便宜为0,表示UART1_RTS_B这个PIN没有input功能。

mux_mode:5表示复用为GPIO1_IO19,将其写入0x020e 0090

input_val:就是写入input_reg寄存器的值。

设备树中添加 pinctrl 节点模板

pinctrl_test: testgrp {
 fsl,pins = <
 MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /*config 是具体设置值*/
 >;
 }

gpio子系统

使用gpio子系统来控制gpio

1、gpio在设备树中的表示方法

定义了一个cd-gpios属性。

&usdhc1 {
	pinctrl-names = "default", "state_100mhz", "state_200mhz";
	pinctrl-0 = <&pinctrl_usdhc1>;
	pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
	pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
	cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
	keep-power-in-suspend;
	enable-sdio-wakeup;
	vmmc-supply = <&reg_sd1_vmmc>;
	status = "okay";
};
此处使用GPIO1_IO19。
		gpio1: gpio@0209c000 {
				compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
				reg = <0x0209c000 0x4000>;
				interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
					     <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
				gpio-controller;
				#gpio-cells = <2>;
				interrupt-controller;
				#interrupt-cells = <2>;

			};
·

2、驱动中对gpio的操作函数

如何从设备树中获取要使用的GPIO信息。of函数。

       1、首先,获取到GPIO所处的设备节点,比如of_find_node_by_path。

       2、获取GPIO编号, of_get_named_gpio函数,返回值就是GPIO编号。

       3、请求此编号的GPIO,gpio_request函数

       4、设置GPIO,输入或输出,gpio_direction_input或gpio_direction_output。

       5、如果是输入,那么通过gpio_get_value函数读取GPIO值,如果是输出,通过gpio_set_value设置GPIO值。

设备树中添加 gpio 节点模板

test {
 pinctrl-names = "default";
pinctrl-0 = <&pinctrl_test>;
gpio = <&gpio1 0 GPIO_ACTIVE_LOW>

在驱动程序中需要读取 gpio 属性内容,Linux 内核提供了几个与 GPIO 有关 的 OF 函数,常用的几个 OF 函数

设备树节点的创建与驱动框架的编写

功能描述

使用MX6U-ALPHA 开发板上的 LED 灯使用了 GPIO1_IO03 这个 引脚作为gpio口

1、修改设备树,添加pinctrl

pinctrl_led: ledgrp {
  fsl,pins = <

  //将 GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0。
  MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
  >;
  };

2、在设备树中添加设备节点

gpioled {
  #address-cells = <1>;
  #size-cells = <1>;
  compatible = "atkalpha-gpioled";
  pinctrl-names = "default";

 // pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点
  pinctrl-0 = <&pinctrl_led>;

下面这个与gpio子系统有关
//led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1这一组第四个引脚,低电平有效(也急速GPIO1d第三个引脚)
 led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;

  status = "okay";
  };

3、编写驱动程序

大部分与之前的一样,特点在与利用一个函数直接控制引脚

/* 3、设置 GPIO1_IO03 为输出,并且输出高电平,默认关闭 LED 灯 */

ret = gpio_direction_output(gpioled.led_gpio, 1);

beep蜂鸣器实验

1、在设备树文件中找到imu6ull板子的节点添加pinctr节点

pinctrl_beep: beepgrp {
fsl,pins = <
 MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x10B0 /* beep */ 
 >;
};

每一组引脚都有编号gpio5是指这个组的第五个引脚

SNVS_TAMPER1 这 个 PIN 复用为 GPIO5_IO01

宏MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 定义在 arch/arm/boot/dts/imx6ull-pinfunc-snvs.h 文件中。

2、添加 BEEP 设备节点

beep {
 #address-cells = <1>;
 #size-cells = <1>;
 compatible = "atkalpha-beep";
 pinctrl-names = "default";
 pinctrl-0 = <&pinctrl_beep>; //pin节点名
 beep-gpio = <&gpio5 1 GPIO_ACTIVE_HIGH>;
 status = "okay";
}

beep-gpio 属性指定了蜂鸣器所使用的 GPIO

linux并发与竞争实验

原子操作(变量)

在arm寄存器中不能直接对寄存器进行操作,必须借助内部的某些寄存器,这样就算简单的赋值也是很多步骤,为了被人打扰,实验一个原子函数,使得它不会被打扰

实验

也就是一次只允许一个应用程序打开驱动程序

在第一次打开文件然后使用就把里面的值减一,关闭的话就释放要加一,要是其他文件打开发现是<=0,直接关闭不可以使用。

自旋锁

一个·文件只有一个可以使用,要是其他想要使用,就会一直在原地打转知道可以使用这个文件为止

信号量

只有获得信号量的才可以使用该文件

互斥量

Linux定时器实验

在freertos和ucos中需要一个硬件定时器提供系统时钟,一般选择Systick(很多芯片都有)作为系统的时钟源

软件定时器与硬件定时器

  1. 硬件定时器是由硬件设备提供的计时器,而软件定时器是由操作系统内核提供的计时器。

  2. 硬件定时器是通过处理器上的时钟源(如CPU时钟或外部时钟源)来实现的,而软件定时器是通过内核中的计时器和定时器列表来实现的。

系统节拍

就是一秒操作系统定时器中断运行多少次数

jiffies

一个全局变量记录了系统运行的总时间

内核定时器

软件定时器不像硬件定时器一样,直接给周期值。设置期满以后的时间点。

       2、定时处理函数。

       3、内核定时器不是周期性的,一次定时时间到了以后就会关闭,除非重新打开。

超时函数  就是一个定时器,我们设置定时器是间是两秒

jiffies + 2   jiffies是获取当前的时间,当系统时间时间到了设定的时间就会触发

超时函数只会执行一次,要想重复执行必须要在定时处理函数中重启定时器

linux中断实验

 linux中断

       1、先知道你要使用的中断对应的中断号。

       2、先申请request_irq,此函数会激活中断。

       3、如果不用中断了,那就释放掉,使用free_irq。

       4、中断处理函数irqreturn_t (*irq_handler_t) (int, void *)

       5、使能和禁止中断,

1.3 上半部和下半部

软中断/tasklet/工作队列 区别_tuyerv的博客-CSDN博客

上半部处理不能被打断

、硬件中断、时间快的中断下班

下半部中断可以被打断

处理一些时间比较长的中断

处理下半部的方法:

       1、软中断(软件触发的中断)

a、产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构

      static struct softirq_action softirq_vec[NR_SOFTIRQS]   10个

       要使用软中断,要先注册,使用函数open_softir。注册以后使用raise_softirq触发。

       软中断我们不要去用!!

       软中断我们不要去用!!

      2、tasklet(基于软中断,在软中断基础上了一定机制)

由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:

一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行

多个不同类型的tasklet可以并行在多个CPU上

软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时

     

       也需要用到上半部,只是上半部的中断处理函数重点是调用tasklet_schedule。

       1、定义一个tasklet函数。

       2、初始化、重点是设置对应的处理函数

       3、工作队列

上面我们介绍的可延迟函数运行在中断上下文中(软中断的一个检查点就是do_IRQ退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。

将下半部耗时间的中断变成一个线程与应用线程一起运行

等处理完上半部就把work放进队列里面,后面会和其他线程一起参与调度

       4、线程化中断

对每一个中断都创建一个内核线程

 给中断创建一个线程,等上半部分函数执行结束也就是中断上半部分,就会调用线程函数执行下半部中断

设备树中断信息节点

#interrupt-cells指定interrupt的cells数量,也就是属性interrupts
interrupt-controller   中断控制器

interrupts,指定中断号,触发方式等。
interrupt-parent,指定父中断,也就是中断控制器

第一个 cells:中断类型,0 表示 SPI 中断,1 表示 PPI 中断。
第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断
号的范围为 0~15。
第三个 cells:标志,bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候
表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。bit[15:8]为 PPI 中
断的 CPU 掩码

  intc: interrupt-controller@00a01000 {
  compatible = "arm,cortex-a7-gic";
  #interrupt-cells = <3>;
  interrupt-controller;
  reg = <0x00a01000 0x1000>,
  <0x00a02000 0x100>;
};

在驱动代码中,为了获取从设备树中获取中断号,我们可以使用一些函数

linux阻塞实验

阻塞:当资源不可用的时候,应用程序就会挂起。当资源可用的时候,唤醒任务。应用程序使用open打开驱动文件,默认是阻塞方式打开。

非阻塞:当资源不可用的时候,应用程序轮询查看,或放弃。会有超时处理机制。应用程序在使用open打开驱动文件的时候,使用O_NONBLO(非阻塞)

等待队列(阻塞)

一旦进入阻塞进程就会休眠,这样cpu就可以让出资源。但设备文件可以操作的时候就要唤醒线程,这样就需要一个等待队列

       1、等待队列头

       wait_queue_head_t 需要定义一个。定义以后使用 init_waitqueue_head函数初始化。或者使用宏DECLARE_WAIT_QUEUE_HEAD。

      

       2、等待队列项

       wait_queue_t表四等待队列项,或者使用宏DECLARE_WAITQUEUE(name, tsk)。

      

       3、添加队列项到等待队列头(线程休眠)

       add_wait_queue函数

       4、移除等待队列项(线程需要唤醒)

       资源可用的时候使用remove_wait_queue函数移除。

       5、唤醒

       wake_up唤醒

非阻塞下访问

轮询

应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时,设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。
 

非阻塞的形式访问设备

select

select 函数能够监视的文件描述符数量有最大的限制

poll


poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。

epoll(异步)

会将那个io流发生了什么事情告诉我们,使用事件驱动,

当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问驱动程序file_operations 操作集中的 poll 函数就会执行

linux异步通知

利用中断机制度,核心是信号,算是软件上模拟中断

inux 应用程序可以通过阻塞或者非阻塞两种方式来访问驱动设备,通过阻塞方式访问,应用程序会处于休眠态,等待驱动设备可以使用。非阻塞方式会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。这两种方式都需要应用程序主动去查询设备的使用情况。如果能提供一种中断机制,驱动程序能主动向应用程序发出通知,驱动通知应用可以访问,然后应用程序在从驱动程序中读取或写入数据。Linux 提供了异步通知机制来完成此功能
 

异步通知的核心是信号

#define SIGHUP 1 /* 终端挂起或控制进程终止 */
#define SIGINT 2 /* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 3 /* 终端退出(Ctrl+\组合键) */
#define SIGILL 4 /* 非法指令 */
#define SIGTRAP 5 /* debug 使用,有断点指令产生 */
#define SIGABRT 6 /* 由 abort(3)发出的退出指令 */
#define SIGIOT 6 /* IOT 指令 */
#define SIGBUS 7 /* 总线错误 */
#define SIGFPE 8 /* 浮点运算错误 */
#define SIGKILL 9 /* 杀死、终止进程 */
 
......
 
#define SIGIO 29 /* 可以进行输入/输出操作 */
#define SIGPOLL SIGIO
/* #define SIGLOS 29 */
#define SIGPWR 30 /* 断点重启 */
#define SIGSYS 31 /* 非法的系统调用 */
#define SIGUNUSED 31 /* 未使用信号 */

信号相当于中断号不同的中断号代表了不同的中断,不同的中断所做的处理不同驱动程序可以通过向应用程序发送不同的信号来实现不同的功能。这里只需要注意SIGIO信号即可。

应用程序异步通知

1、注册信号处理函数

应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数设置信号的处理函数

2、将本应用程序的进程号告诉内核

3、开启异步通知

Linux之Platform设备驱动

驱动的分离与分隔

驱动分离

一个iic在不同芯片下的驱动代码是不同的,我们主机控制器驱动一定是不同的,但是设备驱动可以相同,这样就实现一个设备驱动匹配上多个平台(芯片)

主机控制器驱动是不需要我们自己写的,我们写具体设备驱动,所有的主机驱动都调用一个API接口去读取设备驱动数据

将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息  (比如从设备树中获
取到设备信息),根据获取到的设备信息来初始化设备。 驱动只负责驱动,设备只负责设备,总线法将两者进行匹配。这就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,即驱动分离

二、Platform 平台驱动模型

在 Linux 2.6 以后的设备驱动模型中, 需关心总线、 设备和驱动这 3 个实体, 总线将设备和驱动绑定。在系统每注册一个设备的时候, 会寻找与之匹配的驱动;同样的,在系统每注册一个驱动的时候, 会寻找与之匹配的设备, 而匹配由总线完成。

Linux 设备和驱动通常都需要挂接在一种总线上, 对于本身依附于 PCI、 USB、 I2C、 SPI 等
的设备而言, 这自然不是问题。但是在嵌入式系统里面, 在 SoC 系统中集成的独立外设控制器、 挂接在 SoC内存空间的外设等却不依附于此类总线。 基于这一背景, Linux 发明了一种虚拟的总线, 称为 platform 总线, 相应的设备称为 platform_device, 驱动称为 platform_driver。

通过这种方式实现了此类设备和驱动的分离, 增强设备驱动的可移植性。平台总线模型也称为 platform 总线模型,是 Linux 内核虚拟出来的一条总线, 不是真实的导线。 平台总线模型就是把原来的驱动C文件给分成了俩个 C 文件,一个是 device.c, 一个是 driver.c 。把稳定不变的放在 driver.c 里面, 需要改变的就放在device.c 里面。

平台总线模型将设备代码和驱动代码分离, 将与硬件设备相关的都放到 device.c 文件里面,驱动部分代码放到 driver.c 文件里面。所以对于大量的同类设备而言,只需要修改设备文件信息,驱动文件不用修改,可以提高代码的重用性,减少重复性代码。

1、platform设备

在 platform 平台下用platform_device结构体表示platform设备, 如果内核支持设备树的话就不用使用 platform_device 来描述设备, 使用设备树去描述platform_device即可

优先设备树来描述设备

struct platform_device {
    const char *name;
    int id;
    bool id_auto;
    struct device dev;
    u32 num_resources;
    struct resource *resource;
 
    const struct platform_device_id *id_entry;
    char *driver_override; /* Driver name to force a match */
 
    /* MFD cell pointer */
    struct mfd_cell *mfd_cell;
 
    /* arch specific additions */
    struct pdev_archdata archdata;
};

name:设备名字,要和所使用的 platform 驱动的 name 字段相同否则设备就无法匹配到对应的驱动

resource:指向一个资源结构体数组,即设备信息,比如外设寄存器等。 Linux 内核使用 resource结构体表示资源

struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    struct resource *parent, *sibling, *child;
};

在不支持设备树的Linux版本中,用户需要编写platform_device变量来描述设备信息,然后使用 platform_device_register 函数将设备信息注册到 Linux 内核中

int platform_device_register(struct platform_device *pdev)   //注册platform设备


void platform_device_unregister(struct platform_device *pdev)//注销platform设备

内核支持设备树,使用设备树去描述platform设备

2.platform 驱动


在 Linux 内核中, 用platform_driver结构体表示platform驱动,platform_driver 结构体定义指定名称的平台设备驱动注册函数和平台设备驱动注销函数,定义在include/linux/platform_device.

struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;
    bool prevent_deferred_probe;
};

probe 函数:  当驱动与设备匹配成功以后 probe 函数就会执行, 一般驱动的提供者会编写

remove函数:当 driver 和 device 任意一个 remove 的时, 就会执行该函数

driver: device_driver 结构体变量, Linux 内核里面大量使用到了面向对象的思维, device_driver相当于基类,提供了最基础的驱动框架。 plaform_driver 继承了这个基类,在此基础上又添加了一些特有的成员变量

id_table 表保存 id 信息。 这些 id 信息存放着platformd 驱动所支持的驱动类型。 id_table是个表(数组), 每个元素的类型为 platform_device_id, platform_device_id 结构体内容如下:
 

paltform_driver 中的 device_driver 成员变量的 name 和 of_match_table 这两个属性。其中name 属性用于传统的驱动与设备匹配,检查驱动和设备的 name 字段是不是相同。of_match_table 属性就是用于设备树下的驱动与设备检查。对于一个完整的驱动程序,必须提供有设备树和无设备树两种匹配方法

3.platform 总线

platform设备和platform驱动,相当于把设备和驱动分离了, 需要 platform 总线进行配, platform 设备和 platform 驱动进行内核注册时, 都是注册到总线上

当内核中有驱动注册时, 总线就会在右侧的设备中查找, 是否有匹配的设备, 同样的, 当有设备注册到内核中时, 也会在总线左侧查找是否有匹配的驱动。

在 Linux 内核中使用 bus_type 结构体表示总线, 此结构体定义在文件 include/linux/device.h

struct bus_type {
    const char *name; /* 总线名字 */
    const char *dev_name;
    struct device *dev_root;
    struct device_attribute *dev_attrs;
    const struct attribute_group **bus_groups; /* 总线属性 */
    const struct attribute_group **dev_groups; /* 设备属性 */
    const struct attribute_group **drv_groups; /* 驱动属性 */
 
    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
    int (*probe)(struct device *dev);
    int (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);
 
    int (*online)(struct device *dev);
    int (*offline)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    const struct dev_pm_ops *pm;
    const struct iommu_ops *iommu_ops;
    struct subsys_private *p;
    struct lock_class_key lock_key;
};

match 函数:完成设备和驱动之间匹配的,总线使用 match 函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。 match 函数有两个参数: dev 和 drv,这两个参数分别为 device 和 device_driver 类型,即设备和驱动。
 

bus_type

struct bus_type platform_bus_type = {
    .name = "platform",
    .dev_groups = platform_dev_groups,
    .match = platform_match,
    .uevent = platform_uevent,
    .pm = &platform_dev_pm_ops,
};

platform_match函数

static int platform_match(struct device *dev,struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);
 
    /*When driver_override is set,only bind to the matching driver*/
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);
 
    /* Attempt an OF style match first */
    if (of_driver_match_device(dev, drv))
        return 1;
 
    /* Then try ACPI style match */
    if (acpi_driver_match_device(dev, drv))
        return 1;
    /* Then try to match against the id table */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;
 
    /* fall-back to driver name match */
    return (strcmp(pdev->name, drv->name) == 0);
}

匹配方法

1、设备树采用的匹配方式

 设备树中的每个设备节点的 compatible 属性会和 device_driver 结构体of_match_table 表中的compatible匹配表中所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe 函数就会执行。

2、每个 platform_driver 结构体有一个 id_table成员变量,保存了很多 id 信息。这些 id 信息存放着这个 platformd 驱动所支持的驱动类型 //无设备树

3、直接比较驱动和设备的 name 字段(在platform_device、platform_driver等结构体下存在变量name)  //无设备树的情况下

在没有设备树的情况下,我们要注册一个platform_device 、platform_driver

在有设备树的情况下,我们只需要注册一个platform_driver、platform_device是通过设备树去描述的

正点原子platform笔记

一、linux  驱动分离与分层

驱动的分离与分隔

一个iic在不同芯片下的驱动代码是不同的,我们主机控制器驱动一定是不同的,但是设备驱动可以相同,这样就实现一个设备驱动匹配上多个平台(芯片)

主机控制器驱动是不需要我们自己写的,我们写具体设备驱动,所有的主机驱动都调用一个API接口去读取设备驱动数据

主机驱动是基类,具体设备驱动就是子类

二、总线-驱动-设备

       驱动-总线-设备。

根据驱动的分离与分层衍生出了总线(bus)-驱动(driver)-设备(device)驱动框架。

       总线代码我们不需要编写,linux内核提供给我们使用的。我们需要编写驱动和设备,当向总线注册驱动的时候,总线会从现有的所有设备中查找,看看哪个设备和此驱动匹配。同理,当向总线注册设备的时候总线也会在现有的驱动中查看与之匹配的驱动。

       驱动:是具体的设备驱动

       设备:设备属性,包括地址范围、如果是IIC的话还有IIC器件地址、速度

2.1 总线

       总线数据类型为:bus_type。向内核注册总线使用bus_register。

struct bus_type {
	const char		*name;
	const char		*dev_name;
	struct device		*dev_root;
	struct device_attribute	*dev_attrs;	/* use dev_groups instead */
	const struct attribute_group **bus_groups;
	const struct attribute_group **dev_groups;
	const struct attribute_group **drv_groups;

	int (*match)(struct device *dev, struct device_driver *drv); //匹配函数

};

总线主要工作就是完成总线下的设备和驱动之间的匹配。调用函数时match函数

2.2、驱动

驱动数据类型为device_driver,驱动程序向内核注册驱动采用driver_register。

驱动和设备匹配以后驱动里面的probe函数就会执行。

       使用driver_register注册驱动。

       driver_register

              -> bus_add_driver

                     -> driver_attach          //查找bus下所有设备,找预期匹配的。

                            ->bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);

                                   ->__driver_attach //每个设备都调用此函数,

//查看每个设备是否与驱动匹配

                      -> driver_match_device  //检查是否匹配。

                                     -> driver_probe_device 

                                          ->really_probe

                                                 -> drv->probe(dev);  //执行driver的probe函数

     向总线注册驱动的时候,会检查当前总线下的所有设备,有没有与此驱动匹配的设备,如果有的话就执行驱动里面的probe函数。

2.2、设备

struct device_driver {

       const char            *name;

       struct bus_type          *bus;            

       struct module       *owner;

       const char            *mod_name;  /* used for built-in modules */

       bool suppress_bind_attrs;   /* disables bind/unbind via sysfs */

       const struct of_device_id  *of_match_table;

       const struct acpi_device_id  *acpi_match_table;

       int (*probe) (struct device *dev);                 

       int (*remove) (struct device *dev);

       void (*shutdown) (struct device *dev);

       int (*suspend) (struct device *dev, pm_message_t state);

       int (*resume) (struct device *dev);

       const struct attribute_group **groups;

       const struct dev_pm_ops *pm;

       struct driver_private *p;

};

name:设备名字,要和所使用的 platform 驱动的 name 字段相同,否则设备就无法匹配到对应的驱动。比如对应的 platform 驱动的name字段为xxx-gpio,则此name字段也要设置为xxx-gpio。

id :用来区分如果设备名字相同的时,通过在后面添加一个数字来代表不同的设备
 

设备数据类型为device,通过device_register向内核注册设备。

struct device {

       struct device         *parent;

       struct device_private    *p;

       struct kobject kobj;

       const char            *init_name; /* initial name of the device */

       const struct device_type *type;

       struct mutex         mutex;    /* mutex to synchronize calls to

                                    * its driver.

                                    */

       struct bus_type   *bus;             /* type of bus device is on */

       struct device_driver *driver;     /* which driver has allocated this

                                      device */

……

};

向总线注册设备的时候,使用device_register。

device_register
		-> device_add
			-> bus_add_device
		    -> bus_probe_device
				->device_attach
					->bus_for_each_drv(dev->bus, NULL, dev, __device_attach);
						-> __device_attach
							-> driver_match_device  //匹配驱动
								->bus->match
							->driver_probe_device  //执行此函数
								-> really_probe
									-> drv->probe(dev); //匹配之后执行函数

驱动与设备匹配以后驱动的probe函数就会执行,probe函数就是驱动编写人员去编写的!!!!

上面的总线(bus_type)、设备(device)、驱动(device_driver)都是注册在内核中

三、platform平台驱动模型

据总线-驱动-设备驱动模型,IIC、SPI、USB这样实实在在的总线是完全匹配的,但是要有一些外设是没法归结为具体的总线:比如定时器、RTC、LCD等。为此linux内核创造了一个虚拟的总线:platform总线。

       1、方便开发,linux提出了驱动分离与分层。

       2、进一步引出了驱动-总线-设备驱动模型,或者框架。

       3、对于SOC内部的RTC,timer等等不好归结为具体的总线,为此linux内核提出了一个虚拟总线:platform总线(platroem_bus),platform设备(platform_device)和platform驱动(platform_driver)

3.1、platform总线注册

platform_bus_init

       -> bus_register

注册的内容就是:

struct bus_type platform_bus_type = {

       .name           = "platform",

       .dev_groups   = platform_dev_groups,

       .match           = platform_match,

       .uevent          = platform_uevent,

       .pm        = &platform_dev_pm_ops,

}

对于platform平台而言,platform_match函数就是月老,负责驱动和设备的匹配

3.2 platform驱动

结构体为platform_drive

struct platform_driver {

       int (*probe)(struct platform_device *);

       int (*remove)(struct platform_device *);

       void (*shutdown)(struct platform_device *);

       int (*suspend)(struct platform_device *, pm_message_t state);

       int (*resume)(struct platform_device *);

       struct device_driver driver;

-> const struct of_device_id    *of_match_table;

-> const char             *name;

       const struct platform_device_id *id_table;

       bool prevent_deferred_probe;

};

使用platform_driver_register向内核注册platform驱动

platform_driver_register

              -> __platform_driver_register   (platform_driver)

                     -> 设置driver的probe为platform_drv_probe, //如果platform_driver的

                                                                                                  // probe函数有效的话。

                     -> driver_register

                            ->执行device_drive->probe,对于platform总线,也就是platform_drv_probe函数。而platform_drv_probe函数会执行platform_driver下的probe函数。

结论:向内核注册platform驱动的时候,如果驱动和设备匹配成功,最终会执行platform_driver的probe函数

3.3 platform设备

结构体platform_device:

struct platform_device {

       const char     *name;

       int          id;

       bool       id_auto;

       struct device  dev;

       u32        num_resources;

       struct resource      *resource;

       const struct platform_device_id  *id_entry;

       char *driver_override; /* Driver name to force a match */

       /* MFD cell pointer */

       struct mfd_cell *mfd_cell;

       /* arch specific additions */

       struct pdev_archdata   archdata;

};

1、无设备树的时候,此时需要驱动开发人员编写设备注册文件,使用platform_device_register函数注册设备。

 2,有设备树,修改设备树的设备节点即可。

   当设备与platform的驱动匹配以后,就会执行platform_driver->probe函数。

3.4 platform匹配过程

根据前面的分析,驱动和设备匹配是通过bus->match函数,platform总线下的match函数就是:platform_match。

platform_match   

       -> of_driver_match_device,设备树

       -> acpi_driver_match_device ACPI类型的

       -> platform_match_id 根据platform_driver-> id_table

       -> strcmp(pdev->name, drv->name)  //最终的就是比较字符串,就是platform_device->name,和platform_driver->driver->name。无设备树情况下使用

(pdev->name, drv->name)  //最终的就是比较字符串,就是platform_device->name,和platform_driver->driver->name。无设备树情况下使用。

       1、有设备树的时候:

       of_driver_match_device

              -> of_match_device(drv->of_match_table, dev)  //of_match_table非常重要,

类型为of_device_id。

//compatible属性

platform平台驱动的编写(无设备树)

platform平台设备驱动编写(有设备树)

总线是内核代码不需要写,platform_device描述放到设备树中不需要我们编写,我们只需要实现platform_driver

1、在设备树中创建设备节点

1 gpioled {
2 #address-cells = <1>;
3 #size-cells = <1>;
4 compatible = "atkalpha-gpioled";
5 pinctrl-names = "default";
6 pinctrl-0 = <&pinctrl_led>;
7 led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
8 status = "okay";
9 };

compatible 属性值为“atkalpha-gpioled”,因此一会在编写 platform驱动的时候 of_match_table 属性表中要有“atkalpha-gpioled

都是当驱动和设
备匹配成功以后就会执行 probe 函数。我们需要在 probe 函数里面执行字符设备驱动那一套,
当注销驱动模块的时候 remove 函数就会执行,都是大同小异

probe函数

static int led_probe(struct platform_device *dev)
 {
 printk("led driver and device was matched!\r\n");
 /* 1、设置设备号 */
 if (leddev.major) {
 leddev.devid = MKDEV(leddev.major, 0);
 register_chrdev_region(leddev.devid, LEDDEV_CNT,
LEDDEV_NAME);
 } else {
 alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT,
LEDDEV_NAME);
 leddev.major = MAJOR(leddev.devid);
 }

 /* 2、注册设备 */
 cdev_init(&leddev.cdev, &led_fops);
cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);

 /* 3、创建类 */
 leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
 if (IS_ERR(leddev.class)) {
 return PTR_ERR(leddev.class);
 }

 /* 4、创建设备 */
 leddev.device = device_create(leddev.class, NULL, leddev.devid,
NULL, LEDDEV_NAME);
 if (IS_ERR(leddev.device)) {
 return PTR_ERR(leddev.device);
 }

 /* 5、初始化 IO */
 leddev.node = of_find_node_by_path("/gpioled");
 if (leddev.node == NULL){
 printk("gpioled node nost find!\r\n");
 return -EINVAL;
 }

 leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);
 if (leddev.led0 < 0) {
 printk("can't get led-gpio\r\n");
 return -EINVAL;
 }

 gpio_request(leddev.led0, "led0");
 gpio_direction_output(leddev.led0, 1); /*设置为输出,默认高电平 */
 return 0;
 }

remobe 函数,当卸载 platform 驱动的时候此函数就会执行。在此函数里面
释放内存、注销字符设备等,也就是将原来驱动卸载函数里面的工作全部都放到 remove 函数中
完成。

驱动和设备匹配成功后,设备信息就会从设备树节点转换为platform_device结构体

platform平台提供了很多api函数获取设备信息

代码


描述	   	: 设备树下的platform驱动

***************************************************************/

#define LEDDEV_CNT		1				/* 设备号长度 	*/
#define LEDDEV_NAME		"dtsplatled"	/* 设备名字 	*/
#define LEDOFF 			0
#define LEDON 			1

/* leddev设备结构体 */
struct leddev_dev{
	dev_t devid;				/* 设备号	*/
	struct cdev cdev;			/* cdev		*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备		*/
	int major;					/* 主设备号	*/	
	struct device_node *node;	/* LED设备节点 */
	int led0;					/* LED灯GPIO标号 */
};

struct leddev_dev leddev; 		/* led设备 */

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led0_switch(u8 sta)
{
	if (sta == LEDON )
		gpio_set_value(leddev.led0, 0);
	else if (sta == LEDOFF)
		gpio_set_value(leddev.led0, 1);	
}

/*
 * @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 = &leddev; /* 设置私有数据  */
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[2];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {

		printk("kernel write failed!\r\n");
		return -EFAULT;
	}
	
	ledstat = databuf[0];
	if (ledstat == LEDON) {
		led0_switch(LEDON);
	} else if (ledstat == LEDOFF) {
		led0_switch(LEDOFF);
	}
	return 0;
}

/* 设备操作函数 */
static struct file_operations led_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.write = led_write,
};

/*
 * @description		: flatform驱动的probe函数,当驱动与
 * 					  设备匹配以后此函数就会执行
 * @param - dev 	: platform设备
 * @return 			: 0,成功;其他负值,失败
 */

//匹配成功以后的加载函数
static int led_probe(struct platform_device *dev)
{	
	printk("led driver and device was matched!\r\n");
	/* 1、设置设备号 */
	if (leddev.major) {
		leddev.devid = MKDEV(leddev.major, 0);
		register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
	} else {
		alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
		leddev.major = MAJOR(leddev.devid);
	}

	/* 2、注册设备      */
	cdev_init(&leddev.cdev, &led_fops);
	cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);

	/* 3、创建类      */
	leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
	if (IS_ERR(leddev.class)) {
		return PTR_ERR(leddev.class);
	}

	/* 4、创建设备 */
	leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
	if (IS_ERR(leddev.device)) {
		return PTR_ERR(leddev.device);
	}

	/* 5、初始化IO */	
	leddev.node = of_find_node_by_path("/gpioled");
	if (leddev.node == NULL){
		printk("gpioled node nost find!\r\n");
		return -EINVAL;
	} 
	
	leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);
	if (leddev.led0 < 0) {
		printk("can't get led-gpio\r\n");
		return -EINVAL;
	}

	gpio_request(leddev.led0, "led0");
	gpio_direction_output(leddev.led0, 1); /* led0 IO设置为输出,默认高电平	*/
	return 0;
}

/*
 * @description		: platform驱动的remove函数,移除platform驱动的时候此函数会执行
 * @param - dev 	: platform设备
 * @return 			: 0,成功;其他负值,失败
 */
 //卸载驱动函数
static int led_remove(struct platform_device *dev)
{
	gpio_set_value(leddev.led0, 1); 	/* 卸载驱动的时候关闭LED */
	gpio_free(leddev.led0);				/* 释放IO 			*/

	cdev_del(&leddev.cdev);				/*  删除cdev */
	unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 注销设备号 */
	device_destroy(leddev.class, leddev.devid);
	class_destroy(leddev.class);
	return 0;
}

/* 匹配列表 */
static const struct of_device_id led_of_match[] = {
	{ .compatible = "atkalpha-gpioled" },
	{ /* Sentinel */ }
};

/* platform驱动结构体 */
static struct platform_driver led_driver = {
	.driver		= {
		.name	= "imx6ul-led",			/* 驱动名字,用于和设备匹配,无设备树情况使用 */
		.of_match_table	= led_of_match, /* 设备树匹配表 		 */
	},
	.probe		= led_probe,
	.remove		= led_remove,
};
		
/*
 * @description	: 驱动模块加载函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init leddriver_init(void)
{
	return platform_driver_register(&led_driver);
}

/*
 * @description	: 驱动模块卸载函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit leddriver_exit(void)
{
	platform_driver_unregister(&led_driver);
}

module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");



Linux   MISC驱动实验

板子上的某些外设无法进行分类的时候就可使用MISI驱动

主设备号时候10,次设备号根据不同设备不同,而且不需要我们手动创建cdev,现在我们可以直接使用 misc_register 一个函数来完组成的步骤。当我们卸载设备驱动模块的时候需要调用 misc_deregister 函数来注销掉 MISC 设备

int misc_deregister(struct miscdevice *misc)
 alloc_chrdev_region(); /* 申请设备号 */
2 cdev_init(); /* 初始化 cdev */
3 cdev_add(); /* 添加 cdev */
4 class_create(); /* 创建类 */
5 device_create(); /* 创建设备 */

因为不需要注册设备,与普通的驱动框架对比是probe加载函数和remove卸载

设备的注销与卸载MISC驱动只需一个函数就可以实现

MISC驱动框架

static int miscbeep_probe(struct platform_device *dev)
{
	int ret = 0;

	printk("beep driver and device was matched!\r\n");
	/* 设置BEEP所使用的GPIO */
	/* 1、获取设备节点:beep */
	miscbeep.nd = of_find_node_by_path("/beep");
	if(miscbeep.nd == NULL) {
		printk("beep node not find!\r\n");
		return -EINVAL;
	} 

	/* 2、 获取设备树中的gpio属性,得到BEEP所使用的BEEP编号 */
	miscbeep.beep_gpio = of_get_named_gpio(miscbeep.nd, "beep-gpio", 0);
	if(miscbeep.beep_gpio < 0) {
		printk("can't get beep-gpio");
		return -EINVAL;
	}

	/* 3、设置GPIO5_IO01为输出,并且输出高电平,默认关闭BEEP */
	ret = gpio_direction_output(miscbeep.beep_gpio, 1);
	if(ret < 0) {
		printk("can't set gpio!\r\n");
	}
	
	/* 一般情况下会注册对应的字符设备,但是这里我们使用MISC设备
  	 * 所以我们不需要自己注册字符设备驱动,只需要注册misc设备驱动即可
	 */
	ret = misc_register(&beep_miscdev);
	if(ret < 0){
		printk("misc device register failed!\r\n");
		return -EFAULT;
	}

	return 0;
}

普通框架

/匹配成功以后的加载函数
static int led_probe(struct platform_device *dev)
{	
	printk("led driver and device was matched!\r\n");
	/* 1、设置设备号 */
	if (leddev.major) {
		leddev.devid = MKDEV(leddev.major, 0);
		register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
	} else {
		alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
		leddev.major = MAJOR(leddev.devid);
	}

	/* 2、注册设备      */
	cdev_init(&leddev.cdev, &led_fops);
	cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);

	/* 3、创建类      */
	leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
	if (IS_ERR(leddev.class)) {
		return PTR_ERR(leddev.class);
	}

	/* 4、创建设备 */
	leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
	if (IS_ERR(leddev.device)) {
		return PTR_ERR(leddev.device);
	}

	/* 5、初始化IO */	
	leddev.node = of_find_node_by_path("/gpioled"); //、获取设备节点:beep */
	if (leddev.node == NULL){
		printk("gpioled node nost find!\r\n");
		return -EINVAL;
	} 
	
	leddev.led0 = of_get_named_gpio(leddev.node, "led-gpio", 0);/* 2、 获取设备树中的gpio属性,得到BEEP所使用的BEEP编号 */
	if (leddev.led0 < 0) {
		printk("can't get led-gpio\r\n");
		return -EINVAL;
	}

	gpio_request(leddev.led0, "led0");
	gpio_direction_output(leddev.led0, 1); /* led0 IO设置为输出,默认高电平	*/
	return 0;
}

Linux INPUT子系统实验

该子系统是一个字符设备,input核心层会帮我们写好这样一个驱动程序,注册这个一个字符设备,主设备号是13

我们需要做的完善具体的输入设备在INPUT子系统的框架下

input 就是输入的意思,因此 input 子系统就是管理输入的子系统,和 pinctrl、gpio 子系统
一样,都是 Linux 内核针对某一类设备而创建的框架,如按键输入、键盘、鼠标、触摸屏等
等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,
鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心
应用层的事情,我们只需要按照要求上报这些输入事件即可。为此 input 子系统分为 input 驱动
层、input 核心层、input 事件处理层,最终给用户空间提供可访问的设备节点,input 子系统框

 驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行
处理。
事件层:主要和用户空间进行交互

1、input_dev   

初始化并注册input_dev,evbit 表示输入事件,比如按键对应的事件就是EV_KEY,如果连按加上EV_REP,设置按键对应的键值也就是keybit

申请并初始化并注册

 我们不需要去注册字符设备,input子系统会在核心层帮我们注册。我们只需要注册一个input设备就可以,input_dev结构体表示input设备                                                                                                                                                                             

struct input_dev {
 const char *name;
 const char *phys;
 const char *uniq;
struct input_id id;

unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];
 unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; /* 事件类型的位图 */
 unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; /* 按键值的位图 */
 unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; /* 相对坐标的位图 */
 unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; /* 绝对坐标的位图 */
 unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; /* 杂项事件的位图 */
 unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; /*LED 相关的位图 */
 unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];/* sound 有关的位图 */
 unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; /* 压力反馈的位图 */
 unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; /*开关状态的位图 */
......
 bool devres_managed;
 };

evbit 表示输入事件类型,可选的事件类型定义在 include/uapi/linux/input.h 文件
中,事件类型如下,我们在使用某个事件的时候还需要注册该事件

 ·2、上报输入事件

内核怎么知道你按了没有,我们有专门的函数上报事件,最后使用同步函数

当我们向 Linux 内核注册好 input_dev 以后还不能高枕无的使用 input 设备,input 设备都
是具有输入功能的,但是具体是什么样的输入值 Linux 内核是不知道的,我们需要获取到具体
的输入值,或者说是输入事件,然后将输入事件上报给 Linux 内核。比如按键,我们需要在按
键中断处理函数,或者消抖定时器中断函数中将按键值上报给 Linux 内核,这样 Linux 内核才
能获取到正确的输入值。不同的事件,其上报事件的 API 函数不同,我们依次来看一下一些常
用的事件上报 API 函数

void input_event(struct input_dev *dev,
unsigned int type,
unsigned int code,
int value

dev:需要上报的 input_dev。
type: 上报的事件类型,比如 EV_KEY。
code:事件码,也就是我们注册的按键值,比如 KEY_0、KEY_1 等等。
value:事件值,比如 1 表示按键按下,0 表示按键松开

应用程序可以通过input_event来获得输入事件数据

#define KEYINPUT_CNT     1
#define KEYINPUT_NAME    "keyinput"
#define KEY_NUM            1
#define KEY0VALUE          0X01
#define INVAKEY            0XFF

//中断io描述结构体,一按下按键就触发中断
/* key结构体 */
struct irq_keydesc{
    int gpio;               /* io编号 */
    int irqnum;             /* 中断号 */
    unsigned char value;    /* 键值 */
    char name[10];          /* 名字 */
    irqreturn_t (*handler) (int, void *);  /* 中断处理函数 */
};


/* keyinput设备结构体 */
struct keyinput_dev{
    struct device_node *nd;
    struct irq_keydesc irqkey[KEY_NUM];
    struct timer_list timer; 

    struct input_dev *inputdev; /* 输入设备 */
};

struct keyinput_dev keyinputdev; /* irq设备 */

/* 按键中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    struct keyinput_dev *dev = dev_id;

    dev->timer.data = (volatile long)dev_id;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 定时器函数20ms定时 */

	return IRQ_HANDLED;
}

//定时器服务函数,用于按键消抖,定时器到了以后
//再次读取按键值,如果按键还是处于按下状态就表示按键有效
/* 定时器处理函数 */
static void timer_func(unsigned long arg) {
    int value = 0;
    struct keyinput_dev *dev = (struct keyinput_dev*)arg;

    value = gpio_get_value(dev->irqkey[0].gpio);
    if(value == 0)   {          /* 按下 */
        /* 上报按键值 */  
        input_event(dev->inputdev, EV_KEY, KEY_0, 1);
        input_sync(dev->inputdev);
    } else if(value == 1) {     /* 释放 */
        /* 上报按键值 */ 
        input_event(dev->inputdev, EV_KEY, KEY_0, 0);
        input_sync(dev->inputdev);
    }
}

/* 按键初始化 */
static int keyio_init(struct keyinput_dev *dev)
{
    int ret  = 0;
    int i = 0;

    /* 1,按键初始化 */
    dev->nd = of_find_node_by_path("/key");
    if(dev->nd == NULL) {
        ret = -EINVAL;
        goto fail_nd;
    }

    for(i = 0; i < KEY_NUM; i++) {
        dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i);
    }

    for(i = 0; i < KEY_NUM; i++) {
        memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));
        sprintf(dev->irqkey[i].name, "KEY%d", i);
        gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name);
        gpio_direction_input(dev->irqkey[i].gpio);

        dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); /* 获取中断号 */
#if 0
        dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->nd, i);
#endif

    }

    dev->irqkey[0].handler = key0_handler;
    dev->irqkey[0].value  = KEY_0;

    /* 2,按键中断初始化 */
    for(i = 0; i < KEY_NUM; i++) {
        ret = request_irq(dev->irqkey[i].irqnum, dev->irqkey[i].handler, 
                         IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, 
                         dev->irqkey[i].name, &keyinputdev);
        if(ret) {
            printk("irq %d request failed!\r\n", dev->irqkey[i].irqnum);
            goto fail_irq;
        }
    }

    /* 3、初始化定时器 */
    init_timer(&keyinputdev.timer);
    keyinputdev.timer.function = timer_func;


    return 0;

fail_irq:
    for(i = 0; i < KEY_NUM; i++) {
        gpio_free(dev->irqkey[i].gpio);
    }
fail_nd:
    return ret;
}

/* 驱动入口函数 */
static int __init keyinput_init(void)
{

    int ret = 0;

    /* 1、初始化IO */
    ret = keyio_init(&keyinputdev);
    if(ret < 0) {
        goto fail_keyinit;
    }

    /* 2、注册input_dev */
    keyinputdev.inputdev = input_allocate_device();
    if(keyinputdev.inputdev == NULL) {
        ret = -EINVAL;
        goto fail_keyinit; 
    }

    keyinputdev.inputdev->name = KEYINPUT_NAME;
    __set_bit(EV_KEY, keyinputdev.inputdev->evbit); /* 按键事件 */
    __set_bit(EV_REP, keyinputdev.inputdev->evbit); /* 重复事件 */
    __set_bit(KEY_0, keyinputdev.inputdev->keybit); /* 按键值 */

    ret = input_register_device(keyinputdev.inputdev);
 	if (ret) {  
		goto fail_input_register;
	}

    return 0;

fail_input_register:
    input_free_device(keyinputdev.inputdev);
fail_keyinit:
    return ret;

}

/* 驱动出口函数 */
static void __exit keyinput_exit(void)
{
    int i = 0;
    /*1、释放中断 */
    for(i = 0; i < KEY_NUM; i++) {
        free_irq(keyinputdev.irqkey[i].irqnum, &keyinputdev);
    }
    /* 2,释放IO */
    for(i = 0; i < KEY_NUM; i++) {
        gpio_free(keyinputdev.irqkey[i].gpio);
    }

    /* 3,删除定时器 */
    del_timer_sync(&keyinputdev.timer);

    /* 4、注销input_dev */
    input_unregister_device(keyinputdev.inputdev);
    input_free_device(keyinputdev.inputdev);
}

module_init(keyinput_init);
module_exit(keyinput_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

Linux LCD驱动实验

显存空间,我们要点亮某个像素只需要在像素地址写入一个代表颜色的数据就可以,需要申请空间,framebuff不是实际的物理地址,而是映射地址,将屏幕缓冲区映射到用户空间

通过framebuffer 机制将底层的LCD抽象为/dev/fbx  ,应用程序可以通过操作/dev/fbxl来操作屏幕

framebuffer 在内核中表示就是fb_info结构体,屏幕驱动重点初始化 fb_info里面的各个成员变量。初始化fb_info以后,通过register_frambuffer函数像内核注册fb_info

外设的驱动都是需要我们写一个结构体,然后注册到内核中去

Lcd 驱动接口程序 NPX已经编写好了,我们需要做的就是按照所使用的LCD来修改设备树。

1、LCD所使用的IO配置

目前不需要改写

2、LCD屏幕节点修改,修改相应的属性值,换成我们所使用的LCD屏幕参数

&lcdif {
   pinctrl-names = "default";
   pinctrl-0 = <&pinctrl_lcdif_dat /* 使用到的 IO */
          &pinctrl_lcdif_ctrl
   display = <&display0>;
   status = "okay";
 
  display0: display { /* LCD 属性信息 */
  bits-per-pixel = <24>; /* 一个像素占用几个 bit */
  bus-width = <24>; /* 总线宽度 */

  display-timings {
  native-mode = <&timing0>; /* 时序信息 */
  timing0: timing0 {
 clock-frequency = <51200000>; /* LCD 像素时钟,单位 Hz */
 hactive = <1024>; /* LCD X 轴像素个数 */
 vactive = <600>; /* LCD Y 轴像素个数 */
 hfront-porch = <160>; /* LCD hfp 参数 */
 hback-porch = <140>; /* LCD hbp 参数 */
 hsync-len = <20>; /* LCD hspw 参数 */
 vback-porch = <20>; /* LCD vbp 参数 */
 vfront-porch = <12>; /* LCD vfp 参数 */
 vsync-len = <3>; /* LCD vspw 参数 */
 hsync-active = <0>; /* hsync 数据线极性 */
 vsync-active = <0>; /* vsync 数据线极性 */
 de-active = <1>; /* de 数据线极性 */
 pixelclk-active = <0>; /* clk 数据线先极性 */
      };
    };
  };
};

3、LCD背光节点修改,要根据实际所使用的背光IO来修改相应的设备节点参数

目前不需要修改

Linux  RTC驱动实验

 Linux llc 驱动实验(半双工,只有一根sda线,同一时间只能单向通信)

Linux SPI驱动实验(全双工,可以读和写)

1、设备树是一种描述系统硬件结构的数据结构,在Linux内核中用于连接设备驱动和硬件设备。在SPI驱动框架中,设备树通常包含了与SPI控制器相关的属性信息,以及连接到SPI总线上的每个设备的相关信息。

因此,设备树需要与控制器驱动匹配,以确保控制器驱动能够正确地管理SPI总线。同时,设备树也需要与连接到SPI总线上的每个设备的设备驱动匹配,以确保设备驱动能够正确地管理与设备相关的信息,例如设备寄存器和寄存器位字段等等。

因此,为了确保系统能够正确地工作,设备树必须与控制器驱动和设备驱动都进行匹配。这通常是通过在设备树中使用正确的设备节点和属性来实现的。

2、设备树与设备驱动匹配和设备树与控制器驱动匹配这两个函数通常在设备驱动的probe函数和控制器驱动的probe函数中分别完成

3、在SPI驱动框架中,设备树与设备驱动之间的匹配关系通常由平台总线负责bus函数

4、在SPI驱动框架中,设备树与控制器驱动之间的匹配关系通常由控制器驱动负责,而不是总线负责,这个被核心层调用spi_register_controller()

linux下的spi主机驱动框架

主机控制器驱动:soc的spi外设驱动

设备驱动:具体的spi芯片驱动

spi驱动框架分三层:核心层、控制器驱动层、外设驱动层

1、sp核心层(spi总线)

提供SPI控制器驱动和 设备驱动 的注册方法、注销方法、SPI通信硬件无关接口

总线类型

​
struct bus_type spi_bus_type = {
            .name        = "spi",
            .dev_attrs    = spi_dev_attrs,
            .match        = spi_match_device, //该函数负责匹配控制器驱动与外设驱动
            .uevent        = spi_uevent,
            .pm        = &spi_pm,
        };

​

// spi总线注册过程
        postcore_initcall(spi_init);
            spi_init(void)
                bus_register(&spi_bus_type);            //注册spi总线

spi总线类型,通过bus_register()函数,将spi总线注册进总线,成功注册在sys/bus下即可找到节点

设备与驱动是由总线中的匹配函数实现的

 static int spi_match_device(struct device *dev, struct device_driver *drv)
        {
            const struct spi_device    *spi = to_spi_device(dev);
            const struct spi_driver    *sdrv = to_spi_driver(drv);

            /* Attempt an OF style match */
            if (of_driver_match_device(dev, drv))
                return 1;

            if (sdrv->id_table)
                return !!spi_match_id(sdrv->id_table, spi);

            return strcmp(spi->modalias, drv->name) == 0;
        }

2、控制器驱动

写在内核中,供给设备驱动调用

主要包含SPI硬件体系结构中适配器(spi控制器)的控制,用于产生SPI 读写时序

主要数据结构:spi_master(spi_controller)

SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册
spi_master

对底层外设操作的函数具体sguixian

spi_master描述一个spi主机控制器驱动

  主要定义了SPI总线主控制器的设备类,通过调用class_register()函数注册设备类,成功注册后,在/sys/class目录下即可找到spi_master文件节点。

struct spi_master {
 struct device dev;
 
   struct list_head list;
......
   s16 bus_num;

    /* chipselects will be integral to many controllers; some others
    * might use board-specific GPIOs.
    */
    u16 num_chipselect;
 
    /* some SPI controllers pose alignment requirements on DMAable
    * buffers; let protocol drivers know about these requirements.
    */
     u16 dma_alignment;

    /* spi_device.mode flags understood by this controller driver */
    u16 mode_bits;
 
    /* bitmask of supported bits_per_word for transfers */
    u32 bits_per_word_mask;
......
    /* limits on transfer speed */
    u32 min_speed_hz;
    u32 max_speed_hz;
 
    /* other constraints relevant to this driver */
    u16 flags;
......


  /* lock and mutex for SPI bus locking */
    spinlock_t bus_lock_spinlock;
    struct mutex bus_lock_mutex;

    /* flag indicating that the SPI bus is locked for exclusive use */
     bool bus_lock_flag;
......
    int (*setup)(struct spi_device *spi);

......
    int (*transfer)(struct spi_device *spi, //api函数

    struct spi_message *mesg);
......
    int (*transfer_one_message)(struct spi_master *master,//api函数
    struct spi_message *mesg);
......
   };

api函数负责寄存器底层读写,我们设备驱动只需要调用这里函数就可对寄存器进行操作

3、spi设备驱动

通过SPI主机驱动与CPU交换数据

主要数据结构:spi_device和spi_driver

spi_driver描述一个spi外设驱动

在外设驱动实现中调用spi_register_driver(struct spi_driver *sdrv),向spi总线挂载spi_driver,该函数在drivers\spi\spi.c中实现
  

static struct spi_driver mc33880_driver = {
        .driver = {
            .name        = DRIVER_NAME,
            .owner        = THIS_MODULE,
        },
        .probe        = mc33880_probe,
        .remove        = __devexit_p(mc33880_remove),
    };

当 SPI 设备和驱动匹配成功,以后 probe 函数就会执行。

同样的,spi_driver 初始化完成以后需要向 Linux 内核注册,spi_driver 注册函数为
spi_register_driver

注意:

内核加载设备树会将它转换成一结构体

spi-device:每一个spi_device下都有一个spi_master

对寄存器操作

代码部分

1、修改设备树

串口驱动框架   

与I2C、SPI 一样,Linux 也提供了串口驱动框架,我们只需要按照相应的串口框架编写驱
动程序即可。串口驱动没有什么主机端和设备端之分,就只有一个串口驱动,而且这个驱动也已经由 NXP 官方已经编写好了,我们真正要做的就是在设备树中添加所要使用的串口节点信息,当驱动程序和串口设备匹配成功,窗口就会被驱动起来,行程dev/tty文件

 多点电容触摸屏驱驱动 

原理图

触摸芯片FT5426

Type B:适用于有硬件追踪并能区分触摸点的触摸设备,此类型设备通过 slot 更新某一个
触摸点的信息,FT5426 就属于此类型,一般的多点电容触摸屏 IC 都有此能力

示例代码 64.1.3.1 Type B 触摸点数据上报时序
1 ABS_MT_SLOT 0  设置当前的槽位(slot)为0。多点触摸设备可以支持多个触摸点,每个触摸点都对应一个槽位  
2 ABS_MT_TRACKING_ID 45 设置当前槽位的跟踪ID(tracking ID)为45。跟踪ID用于标识一个唯一的触摸
3 ABS_MT_POSITION_X x[0]
4 ABS_MT_POSITION_Y y[0]
5 ABS_MT_SLOT 1         设置当前的槽位为1,切换到下一个槽位
6 ABS_MT_TRACKING_ID 46
7 ABS_MT_POSITION_X x[1]
8 ABS_MT_POSITION_Y y[1]
9 SYN_REPORT

第一行上报ABS_MT_SLOT 事件,也就是触摸点对应的 SLOT。每次上报一个触摸点坐
标之前要先使用 input_mt_slot 函数上报当前触摸点 SLOT,触摸点的 SLOT 其实就是触摸点 ID,
需要由触摸 IC 提供

第 2 行,根据 Type B 的要求,每个 SLOT 必须关联一个 ABS_MT_TRACKING_ID,通过
修改 SLOT 关联的 ABS_MT_TRACKING_ID 来完成对触摸点的添加、替换或删除。具体用到
的函数就是 input_mt_report_slot_state,如果是添加一个新的触摸点,那么此函数的第三个参数
active 要设置为 true,linux 内核会自动分配一个 ABS_MT_TRACKING_ID 值,不需要用户去指
定具体的 ABS_MT_TRACKING_ID 值

第 3 行,上报触摸点 0 的 X 轴坐标,使用函数 input_report_abs 来完成。

第 4 行,上报触摸点 0 的 Y 轴坐标,使用函数 input_report_abs 来完成

第 5~8 行,和第 1~4 行类似,只是换成了上报第二个点 的(X,Y)坐标信息

第 9 行,当所有的触摸点坐标都上传完毕以后就得发送 SYN_REPORT 事件,使用 input_sync
函数来完成。

中断

中断线程化

驱动框架

1、12c驱动框架

2、复位引脚和中断引脚、包括中断

3、input子系统框架

4、初始化触摸ic、中断、input子系统

5、在中断服务函数里面读取触摸坐标值,然后上报坐标

最后就是在中断服务程序中上报读取到的坐标信息,根据所使用的多点电容触摸设备类型
选择使用 Type A 还是 Type B 时序。大多数的设备都是 Type B 类型

 设备树的修改

 INT ->GPIO_IO09

RST ->

I2C_SDA

I2C_SCL

1、添加 FT5426使用的io

使用了四个io,一个复位io、一个中断io、i2c2的scl和sda

中断io

pinctrl_tsc: tscgrp {
   fsl,pins = <
         MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0xF080 /* TSC_INT */
      >;
};

复位io

触摸屏复位引脚使用的是 SNVS_TAMPER9,因此复位引脚信息要添加到 iomuxc_snvs 节
点下

pinctrl_tsc_reset: tsc_reset {
   fsl,pins = <
         MX6ULL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x10B0
   >;
 }

i2c的SCL、SDA                                                                             

pinctrl_i2c2: i2c2grp {
  fsl,pins = <
     MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0
     MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0
     >;
 }

2、添加FT5426节点

FT5426 这个触摸 IC 挂载 I2C2 下,因此需要向 I2C2 节点下添加一个子节点

&i2c2 {
   clock_frequency = <100000>;
   pinctrl-names = "default";
   pinctrl-0 = <&pinctrl_i2c2>;//i2c2d的io
   status = "okay";
   
ft5426: ft5426@38 {
       compatible = "edt,edt-ft5426";
       reg = <0x38>;        //触摸芯片寄存器地址
       pinctrl-names = "default";
       pinctrl-0 = <&pinctrl_tsc    //中断io
           &pinctrl_tsc_reset >;    //复位io
       interrupt-parent = <&gpio1>;  //interrupt-parent属性描述中断IO对应的GPIO组为GPIO1
       interrupts = <9 0>;           //interrupt属性描述中断io对应的GPIO为gpio5_io09
       reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
       interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
     };
}

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值