vscode:
https://note.youdao.com/ynoteshare/index.html?id=ea4796f0aa4344e11bae0d5bdfe32cc8&type=notebook&_time=1652253468820
DC22021-Linux设备驱动开发
https://note.youdao.com/ynoteshare/index.html?id=48935e109f3c8562ff6ffa57dfb988ad&type=notebook&_time=1654736812939
驱动:给应用层提供接口,控制硬件
ARM裸机驱动和驱动的区别?
ARM裸机驱动:开发的时候不需要内核的加持,直接从头到尾自己来编写代码即可
驱动:在linux内核基础上编写的基于内核来操作硬件的代码才是真实的驱动
字符设备驱动:按照字节流访问,只能顺序访问,不能无序访问的设备属于字符设备驱动(90%)
块设备驱动:按照block访问(512byte),可以顺序访问也可以无需访问的设备属于块设备驱动
网卡设备驱动:网卡设备驱动没有设备文件,通过网络协议栈发送给网卡驱动实现数据收发的代码就是网卡驱动
Makefile
arm?=x86
modname?=demo
ifeq($(arch),arm)
KERNELDIR:= /home/linux/linux-5.10.61 #在开发板上安装
else
KERNELDIR:= /lib/modules/$(shell uname -r)/build #在ubuntu上安装
endif
PWD:=$(shell pwd)
all:
make -C $(KERNELDIR) M=$(PWD) modules
@#从当前目录切换到内核的顶层目录下
@#读取内核顶层目录下的Makefile文件
@#执行make
@#make M=$(PWD) modules
@#make modules:模块化编译的命令,M指定只编译当前目录下的目录
clean:
make -C $(KERNELDIR) M=$(PWD) clean
@#清除编译的中间文件
obj-m:=modname
@obj-m:=hello.o
@hello-bojs += $(modname).o a.o
内核模块(day1)
内核三要素
入口:在入口分配资源 static int __init 名字(void)
出口:在出口释放资源 static void __exit 名字(void)
许可证:遵从GPL协议
//将入口函数的地址告诉给内核
module_init(demo_init);
//将出口函数的地址告诉给内核
module_exit(demo_exit);
//许可证
MODULE_LICENSE("GPL");
//模块的作者
MODULE_AUTHOR("azy azy_hd@163.com");
操作命令
sudo insmod demo.ko //安装模块
lsmod //查看模块 可以查看状态(OE+)
sudo rmmod demo //卸载模块
dmesg //主动查看内核中的所有目录(高于终端的级别,颜色红色)
sudo dmesg -c/-C //清空内核打印信息,-c回显后清楚,-C直切清楚
mknod /dev/myled c 241 0 //分配驱动节点
//myled是字符设备驱动的名字
//c/d :c是字符设备驱动,b是块设备驱动
//241:主设备号,0次设备号
modinfo xxx.ko //查看打印信息
内核模块中接收的函数
module_param(name,type,perm)
功能:接收命令行传递的参数
参数:
@name:变量名
@type:变量类型
/* Standard types are:
* byte, hexint, short, ushort, int, uint, long, ulong
* charp: a character pointer
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
*/
@perm:修饰文件的权限(0664)
module_param_array(name,type,nump,perm)
功能:接受命令行传递的数组
参数:
@name:数组名
@type:数组中成员类型
@nump:传递的成员的个数
@perm:修饰文件的权限(0664)
MODULE_PARM_DESC(_parm, desc)
功能:对变量信息的详细描述(可以通过modinfo xxx.ko查看)
参数:
@_parm:变量名
@desc:描述的字符串
#include <linux/init.h>
#include <linux/module.h>
int backlight = 127;
module_param(backlight, int, 0664);
MODULE_PARM_DESC(backlight, "this is backlight var range[0-255]");
char a = 'A';
module_param(a, byte, 0664);
MODULE_PARM_DESC(a, "this is char var");
char* p = "helloworld";
module_param(p, charp, 0664);
MODULE_PARM_DESC(p, "this is char* var");
int ww[10] = {0};
int len;
module_param_array(ww,int,&len,0664);
MODULE_PARM_DESC(ww, "this is int [10] var");
static int __init mycdev_init(void)
{
int i;
//shift+alt+f 代码对其
//追代码:ctrl+鼠标左键
//回退:alt + <-
//打开终端:ctrl + `
//终端和代码块切换 ctrl+j
//多行注释 ctrl + /
printk("backlight = %d\n", backlight);
printk("a = %d\n", a);
printk("p = %s\n", p);
for(i=0;i<len;i++){
printk("ww[%d] = %d\n",i,ww[i]);
}
return 0;
}
static void __exit mycdev_exit(void)
{
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
导出符号表API
EXPORT_SYMBOL_GPL(sym)
功能:导出符号表
参数:
@sym:函数名
注:
在编译的时候需要将A中的Module.symbers拷贝到B中,然后在编译B,
demoA:提供add函数
#include<linux/init.h>
#include<linux/module.h>
int add(int a,int b){
return a+b;
}
EXPORT_SYMBOL_GPL(add)
static int __init mycdev_init(void){
return 0;
}
static void __exit mycdev_exit(void){
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GOL");
demoB.c
#include<linux/init.h>
#include<linux/module.h>
extern int add(int,int);
static int __init mycdev_init(void){
prink("demoB usm=%d\n",add(100,200));
return 0;
}
static void _exit mycdev_exit(void){
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
编译demodA时会生成一个Module.symbers符号表,拷贝(内核5.4)到demodB目录下,然后编译demodB
如果内核在5.10的内核取消了拷贝字符表的方式,需要使用 KBUILD_EXTRA_SYMBOLS变量指定符号表的绝对路径 ,放到Makefile中
KBUILD_EXTRA_SYMBOLS := /home/linux/work/day1/04export/demoA/Module.symvers
需要先安装demoA,在安装demoB模块,卸载需要demoB然后在卸载demoA
字符设备驱动(day2)
#inculude<linux/fs.h>
int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
功能:注册字符设备驱动
参数:
@major:主设备号
major > 0 :静态指定设备号
major = 0 :系统自动分配主设备号
次设备号的区间{0,255}(256)
@name:字符设备驱动的名字
linux@ubuntu:/dev/input$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
主设备号 设备驱动的名字
@fops:操作方法结构体
struct file_operations {
int (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*release) (struct inode *, struct file *);
}
返回值:
major > 0 :成功返回0,失败返回错误码
major = 0 :成功返回主设备号,失败返回错误码
通过EIO可以找到内核中的错误码
void unregister_chrdev(unsigned int major, const char *name)
功能:注销字符设备驱动
参数:
@major:主设备号
@name:设备的名字
返回值:无
int major=0;
const struct file_operations fops = {
.open = mycdev_open,
.release = mycdev_close,
};
static int __init mycdev_init(void){
// 1.注册字符设备驱动
major = register_chrdev(0, CNAME, &fops);
if(major < 0){
printk("register char device driver error\n");
return major;
}
printk("register success major = %d\n",major);
return 0;
}
static void __exit mycdev_exit(void){
//注销字符设备驱动
unregister_chrdev(major,CNAME);
}
地址映射
#include <linux/io.h>
void *ioremap(unsigned long offset, unsigned long size)
功能:将物理地址映射成虚拟地址
参数:
@offset:物理地址
@size:大小
返回值:成功返回虚拟地址,失败返回NULL
void iounmap(volatile void *addr)
功能:取消地址映射
参数:
@addr:映射得到的地址
返回值:无
virt_moder = ioremap(PHY_LED1_MODER, 4);
if (virt_moder == NULL) {
printk("ioremap moder register error\n");
return -ENOMEM;
}
传输数据
#include <linux/uaccess.h>
int copy_to_user(void __user volatile *to, const void *from,
unsigned long n)
功能:将数据从内核空间拷贝到用户空间 (mycdev_read)
参数:
@to:用户空间的地址 (__user告诉编译器这个地址是用户空间的地址)
@from:内核的地址 //传的数据的地址
@n:大小(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数 EINVAL
int copy_from_user(void *to, const void __user volatile *from,
unsigned long n)
功能:将数据从用户空间拷贝到内核空间 (mycdev_write)
参数:
@to:内核空间的地址
@from:用户的地址
@n:大小(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
自动创建设备节点
(day3)
#include <linux/device.h>
struct class *class_create(owner, name)
功能:向上层提交目录名
参数:
@owner:THIS_MODULE(这个是和编译器及驱动安装相关的宏)
@name:目录名 //添加写的宏NAME "myled"
返回值:成功返回结构体的首地址,失败返回错误码指针
struct class *cls = class_create(THIS_MODULE,"hello");
if(IS_ERR(cls)){
printk("class create error\n");
return PTR_ERR(cls);
}
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
功能:向上层提交设备信息
参数:
@class:目录的句柄
@parent:NULL
@devt:设备号
MKDEV(major,minor) //合成设备号
MAJOR(dev) //根据设备号得到主设备号
MINOR(dev) //根据设备号得到次设备号
@drvdata:NULL
@fmt:设备节点的名字 "uart%d",i
返回值:成功返回结构体的首地址,失败返回错误码指针
void class_destroy(struct class *cls)
功能:销毁目录
参数:
@cls:目录的句柄
返回值:无
void device_destroy(struct class *class, dev_t devt)
功能:销毁目录下的信息
参数:
@cls:目录的句柄
@devt:设备号
返回值:无
struct class *led_class;
struct device *led_device;
led_class=class_create(THIS_MODULE,NAME);
if(IS_ERR(led_class)){
printk("class create error\n");
goto ERR3;
}
led_device=device_create(led_class,NULL,led_dev,NULL,NAME);
if(IS_ERR(led_device)){
printk("device create error\n");
goto ERR4;
}
#if 0
for(i=0;i<count;i++){
dev=device_create(cls,NULL,MKDEV(major,i),NULL,"led%d",i);
if(IS_ERR(dev)){
printk("device create error\n");
goto ERR5;
}
}
#endif
device_destroy(led_class,led_device);
class_destroy(led_class);
#if 0
for(--i;i>=0;i--){
device_destroy(cls,MKDEV(major,i));
}
class_destroy(cls);
#endif
ioctl函数
(day3)
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
功能:设备的控制
参数:
@fd :文件描述符
@request:命令码
见下面的解释
@... :可变参数,如果传,传递地址 //把发送过来的数据给这个参数
————————————————————————————————————————
fops:long (*unlocked_ioctl) (struct file *, unsigned int cmd , unsigned long arg);
功能:内核中实现的ioctl(request->cmd ...->arg)
参数:
@cmd:用户传递过来的命令码
@arg:用户传递过来的地址
返回值:成功返回0,失败返回错误码
#define LED_ON _IOW('l',1,int)
#define LED_OFF _IOW('l',0,int)
#define GET_CMD_SIZE(cmd) ((cmd>>16)&0x3fff)//右移16位和0x3fff取余就可以得到大小了
//用户层:
int main(int argc,const char * argv[]){
int fd;
char buf[128] = {0};
int which = 1;
if((fd = open("/dev/ttt",O_RDWR))==-1){
PRINT_ERR("open mycdev error");
}
while(1){
ioctl(fd,LED_ON,&which);//LED_ON对应着驱动的第二个参数,which对印着第三个参数
sleep(1);
ioctl(fd,LED_OFF,&which);
sleep(1);
}
close(fd);
return 0;
}
//底层
long myled_ioctl(struct file* file,unsigned int cmd, unsigned long arg){
int which, ret;
switch (cmd) {
case LED_ON:
ret = copy_from_user(&which, (void*)arg, GET_CMD_SIZE(LED_ON));
if (ret) {
printk("copy data form user error\n");
return -EINVAL;
}
printk("which = %d,LED%d ON\n", which, which);
if(which == 1) LED1_ON;
break;
case LED_OFF:
ret = copy_from_user(&which, (void*)arg, GET_CMD_SIZE(LED_ON));
if (ret) {
printk("copy data form user error\n");
return -EINVAL;
}
printk("which = %d,LED%d OFF\n", which, which);
if(which == 1) LED1_OFF;
break;
}
return 0;
}
//1.
const struct file_operations fops = {
.open = myled_open,
.unlocked_ioctl = myled_ioctl,
.release = myled_close,
};
分步流程
(day3)
#include<linux/cdev.h>
1.分配对象
struct cdev {
struct module *owner; //THIS_MODULE
const struct file_operations *ops; //操作方法结构体
struct list_head list; //内核的链表
dev_t dev; //设备号
unsigned int count; //设备的个数
} ;
struct cdev *cdev; //分配对象
struct cdev *pcdev = cdev_alloc();
struct cdev *cdev_alloc(void)
功能:为cdev的结构体指针分配内存
参数:
@无
返回值:成功返回结构体的首地址,失败返回NULL
2.初始化对象
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能:字符设备驱动的初始化
参数:
@cdev:cdev结构体指针
@fops:操作方法结构体
返回值:无
3.申请设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:静态指定设备号
参数:
@from:设备号的起始值 MKDEV(major,minor)
@count:个数
@name:设备的名字 cat /proc/devices查看
返回值:成功返回0,失败返回错误码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
功能:动态申请设备号
参数:
@dev:申请到的设备号
@baseminor:次设备号的起始值
@count:个数
@name:设备的名字 cat /proc/devices查看
返回值:成功返回0,失败返回错误码
4.对象注册
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
功能:字符设备驱动的注册
参数:
@p:cdev的结构体指针
@dev:申请到的设备号
@count:设备的个数
返回值:成功返回0,失败返回错误码
————————————————————————————————————————
void cdev_del(struct cdev *p)
功能:注销字符设备驱动
参数:
@p:cdev的指针
返回值:无
void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
@from:设备号的开始值 minor
@count:个数
返回值:无
void kfree(const void *objp)
#include<linux/slab.h>
功能:释放cdev_alloc申请的内存
参数:
@objp:cdev的首地址
返回值:无
//1.分配对象
struct cdev *cdev;//定义
cdev=cdev_alloc();//
//2.初始化对象
cdev_init(cdev,操作方法结构体fops)//没有返回值,操作方法结构体struct file_operations fops={};
//3.申请设备号 动态分配
denv_t devno; int minor //初始化 MKDEV(major,minot)
int count=0//申请设备号的个数
#denfine NAME "myled"
alloc_chrdev_region(&devno,&minor,count,NAME);
//4.对象注册
cdev_add(cdev,devno,count);
#define NAME "myled"
struct cdev *led_cdev;
dev_t led_dev;
int major;
const struct file_operations ofps={
};
static int __init mycdev_init(void){
int ret=0;
led_cdev=cdev_alloc();//
if(NULL==led_cdev){
printk("cdev_alloc error\n");
goto ERR;
}
cdev_init(led_cdev,&ofps);
ret=alloc_chrdev_region(&led_dev,major,0,NAME);//申请设备号
if(ret!=0){
printk("alloc chrdev region error\n");
goto ERR1;
}
ret=cdev_add(led_cdev,led_dev,1);//注册设备号,申请的子设备号+1
if(ret!=0){
printk("cdev add error\n");
goto ERR2;
}
return 0;
ERR3:
cdev_del(led_cdev);
ERR2:
unregister_chrdev_region(led_dev,0);
ERR1:
kfree(led_cdev);
ERR:
return -1;
}
静态和并发(day4)
中断屏蔽?
local_irq_disable(); //关闭中断
//临界资源
local_irq_enable(); //开启中断
自旋锁(重点)
spinlock_t lock; //定义自旋锁
void spin_lock_init(spinlock_t *lock) //初始化自旋锁
void spin_lock(spinlock_t *lock) //上锁
void spin_unlock(spinlock_t *lock) //解锁
信号量(重点)
struct semaphore lock; //定义信号量
void sema_init(struct semaphore *sem, int val)
//初始化信号量,只有val初始化为1的时候才有互斥的效果。
//如果val初始化为0,同步机制
void down(struct semaphore *sem); //上锁
int down_interruptible(struct semaphore *sem); //休眠的时候可以被信号打断
int down_killable(struct semaphore *sem); //当进程休眠的时候可以被kill掉
int down_trylock(struct semaphore *sem); //尝试获取锁
//获取到锁的时候返回0,失败返回1,即使没有资源也不会休眠
int down_timeout(struct semaphore *sem, long jiffies); //休眠的时候加上超时检查
//如果在超时时间到了还没有获取到信号量,返回-ETIME错误码,否则返回0
void up(struct semaphore *sem); //解锁
互斥体(会用)
struct mutex lock; //定义互斥体
mutex_init(&lock); //初始化互斥体
void mutex_lock(struct mutex *lock); //上锁
int mutex_trylock(struct mutex *lock); //尝试获取互斥体,成功返回1,失败返回0
void mutex_unlock(struct mutex *lock);//解锁
原子(会用)
atomic_t lock = ATOMIC_INIT(1); //定义并初始化原子变量
atomic_dec_and_test(&lock); //上锁
//减去1和0比较,如果结果为0表示获取锁成功了(成功返回真),否则获取锁失败了(失败返回假)
atomic_inc(&lock) //解锁
atomic_t lock = ATOMIC_INIT(-1); //定义并初始化原子变量
atomic_inc_and_test(&lock);//上锁
//加1和0比较,如果结果为0表示获取锁成功了(成功返回真),否则获取锁失败了(失败返回假)
atomic_dec(&lock) //解锁
IO模型(day4下、5)
非阻塞
如果驱动提供的是非阻塞的IO模型,当调用read函数读取数据的时候,不管数据是否准备好read函数都应该立即返回。
user:
open("/dev/mycdev0",O_RDWR|O_NONBLOCK) //以非阻塞的方式开发
read(fd,buf,sizeof(buf));
--------------------------------------------
kernel:
mycdev_read(file){
if(file->f_flags & O_NONBLOCK){
//非阻塞
//从硬件中读取数据,将数据返回到用户空间即可
}
}
阻塞
如果驱动提供的是阻塞的IO模型,当调用read函数读取数据的时候,如果数据没有准备好阻塞等待(进程休眠),如果硬件的数据准备好了,会产生硬件中断,在中断处理函数中唤醒休眠的进程。将准备好的数据拷贝到用户空间
user:
open("/dev/mycdev0",O_RDWR) //以阻塞的方式开发
read(fd,buf,sizeof(buf));
--------------------------------------------
kernel:
mycdev_read(file){
if(file->f_flags & O_NONBLOCK){
//非阻塞
}else{
//阻塞
//判断数据是否准备好,如果数据没有准备好,让进程休眠
}
}
1.定义等待队列头
wait_queue_head_t wq;
2.初始化等待队列头
init_waitqueue_head(&wq);
3.如果数据没有准备好就休眠
wait_event(wq, condition) //让进入不可中断的休眠态
wait_event_interruptible(wq, condition) //让进程进入可中断的休眠状态
//condition代表数据是否准备好的一个变量,如果是真代表数据准备好了,不需要休眠,
//如果为假表示数据没有准备好,进程需要休眠
4.唤醒
condition=1; //如果不设置为真,即使唤醒了也会再次进入休眠状态
wake_up(&wq);
wake_up_interruptible(&wq);
IO多路复用(day5)
在同一个APP应用程序同时监听多个硬件的数据,此时就需要使用IO多路复用机制中的select/poll/epoll来完成多个文件描述符的监听的过程,如果所有的文件描述符对应的数据都没有准备好,进程休眠。如果有一个或者多个硬件的数据准备好了,就会唤醒这个休眠的进程。select/poll/epoll就会返回,返回后从表中找到数据准备好的文件描述符,从对应的文件描述符中将数据拿到即可。
fd1 = open("/dev/mycdev0",O_RDWR);
fd2 = open("/dev/input/mouse0",O_RDWR);
fd_set rfds; //定义表
FD_ZERO(&rfds); //清空表
FD_SET(fd1,&rfds); //将fd1放入表中
FD_SET(fd2,&rfds); //将fd2放入表中
int select(maxfd+1,&rfds,NULL,NULL,NULL);
if(FD_ISSET(fd1,&rfds)){
read(fd1,buf1,sizeof(buf1));
}
if(FD_ISSET(fd2,&rfds)){
read(fd2,buf2,sizeof(buf2));
}
------------------------------------------------------------------
driver:fops: (应用层的select/poll/epoll都会调用驱动的同一个poll函数)
__poll_t (*poll) (struct file *file, struct poll_table_struct *wait)
{
grep ".poll =" * -nR //通过这种方式参考内核的实现
//1.定义一个mask的变量初值为0
__poll_t mask=0;
//2.调用poll_wait (阻塞相关)
poll_wait(file,&wq,wait) ;
//3.判断数据是否准备好
if(condition)
mask |= EPOLLIN;
4.返回mask
return mask;
}
1.写好非阻塞后
2.写poll函数
__poll_t mycdev_poll(struct file *file,
struct poll_table_struct *wait)
{
__poll_t mask=0;
//1.向上提交等待队列头
poll_wait(file,&wq,wait);
//2.返回EPOLLIN表示数据准备好了,返回0表示数据没有准备好
if(condition)
mask |= EPOLLIN;
return mask;
}
select(表)
1.select监听的最大文件描述是1024个
2.select有清空表的过程,需要反复从用户空间向内核空间拷贝表,效率低
3.当有文件描述符的对应驱动的数据准备好的时候,需要再次遍历找到准备好的文件描述符,效率低
poll:(结构体数组)
1.poll监听的最大文件描述没有个数限制
2.poll没有清空表的过程效率高
3.当有文件描述符的对应驱动的数据准备好的时候,需要再次遍历找到准备好的文件描述符,效率低
epoll:(红黑树)
1.epoll监听的最大文件描述没有个数限制
2.epoll没有清空表的过程效率高
3.epoll当在休眠的时候,如果有驱动的数据准备好,epoll能直接拿到准备好的文件描述符,不需要遍历,效率高。epoll是本世纪效率最高,最好用的IO多路复用机制
异步通知
在进程中注册一个信号处理函数,如果硬件的数据准备好的时候,会产生中断,在中断处理函数中给这个进程发送信号即可。如果内核没有发出信号应用程序,不需要阻塞,运行自己特有的代码即可。
//1.注册信号处理函数
signal(SIGIO ,信号处理函数)
//2.通过fcntl调用到底层的fasync函数
unsigned int flags = fcntl(fd,F_GETFL);
fcntl(fd,F_SETFL,flags|FASYNC);
//3.将当前的进程号告诉给内核
fcntl(fd,F_SETOWN,getpid());
-----------------------------------------
fops:
int (*fasync) (int fd, struct file *file, int on)
{
return fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
//功能:完成发信号前的初始化
}
void kill_fasync(struct fasync_struct **fp, int sig, int band)
//向进程发送信号,如果当数据准备好后发送这个信号,就会发送数据
struct fasync_struct *fapp;
kill_fasync(&fapp,SIGIO,POLL_IN);//发送信号告诉用户可以读数据 POLL_IN=1
int mycdev_fasync(int fd, struct file *file, int on){
return fasync_helper(fd,file,on,&fapp);
}
const struct file_operations fops = {
.open = mycdev_open,
.read = mycdev_read,
.write = mycdev_write,
.fasync = mycdev_fasync, //当收到用户发来的信号会执行这个函数
.release = mycdev_close,
};
设备树
设备树(Device Tree)是⼀种描述硬件信息的数据结构,设备树的源文件是dts或dtsi的文件这些文件经过DTC编译生成DTB的二进制文件。在Linux内核启动的时候就会解析这个DTB文件,将这个DTB中的文件解析成一种树状的结构,驱动就可以从这个结构体获取到设备信息,进而操作硬件。
特点:
1.在设备树中同名的节点会被合并
2.设备树的节点可以去别名
取别名的方法1:
node:mynode@0x12345678{
};
注:node就是mynode@12345678的别名
取别名的方法2:
aliases {
node1=&node;
};
3.节点可以被其他引用
&node{
};
4.节点中的建可以被删除
node:mynode@0x12345678{
p1="123";
};
&node{
/delete-property/p1; //删除节点中的p1键值对
}
关于设备树节点的命令和资料
设备树节点
linux-5.10.61/arch/arm/boot/dts/stm32mp157a-fsmp1a.dts
编译设备树 在linux-5.10.61目录下
make dtbs
拷贝到tftp中
cp arch/arm/boot/dts/stm32mp157a-fsmp1a.dtb ~/tftpboot
在内核启动之后,可以在如下位置找到节点,并且在这个目录下会根据属性名创建文件
/proc/device-tree/mynode@0x12345678 //mynode@0x12345678设备树的名
stm32mp151.dtsi //根设备树
stm32mp15xx-dkx.dtsi //别人写的
/home/linux/linux-5.10.61/Documentation/devicetree/bindings/i2c //都是别人写的
2.参考内核的帮助文档编写自己的设备树
stm32mp157a-fsmp1a.dts
设备树API
//每一个节点被内核解析后,内核就会为这个节点创建一个device_node的结构体
struct device_node {
const char *name; //节点名 mynode
const char *full_name; //节点全名 mynode@0x12345678
struct property *properties; //节点内的属性,这个property是一条链表
struct device_node *parent; //父节点
struct device_node *child; //子节点
struct device_node *sibling; //兄弟节点
};
struct property {
char *name; //键
int length; //值的长度
void *value; //值
struct property *next; //指向下一个键值对
}
struct device_node *of_find_node_by_path(const char *path)
功能:通过路径或者节点结构体
参数:
@path:路径
返回值:成功返回节点的首地址,失败返回NULL
struct device_node *of_get_child_by_name(const struct device_node *node,const char *name)
功能:从父节点解析子节点
参数:
@node:父节点node指针
@name:子节点名
返回值:成功返回子节点指针,失败返回NULL
struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
功能:通过节点名获取节点
参数:
@from: NULL,从根节点往下解析
@name:节点名
返回值:成功返回节点的首地址,失败返回NULL
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
功能:根据 compatible来获取节点
参数:
@from:NULL,从根开始解析
@type:NULL
@compatible:"厂商,设备名"
返回值:成功返回节点的首地址,失败返回NULL
struct property *of_find_property(const struct device_node *np,const char *name,
int *lenp)
功能:根据键的名字获取值
参数:
@np:节点首地址
@name:键的名字
@lenp:获取到的值的长度
返回值:成功返回property结构体指针,失败返回NULL
int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value)
功能:从property中获取一个u32的类型的数
参数:
@np:节点指针
@propname:键
@index:索引号
@out_value:获取到的结果
返回值:成功返回0,失败返回错误码
int of_property_read_variable_u8_array(const struct device_node *np,const char *propname, u8 *out_values,size_t sz_min, size_t sz_max)
功能:获取一个u8类型的数组
参数:
@np:节点指针
@propname:键
@out_value:获取到的结果
@sz_min:最小的下标
@sz_max:最大下标
返回值:成功返回0,失败返回错误码
int of_property_read_string_index(const struct device_node *np,const char *propname, int index, const char **output)
功能:根据索引号获取字符串
参数:
@np:节点指针
@propname:键
@index:索引号
@output:获取到的结果
返回值:成功返回0,失败返回错误码
gpio子系统
编写设备树
编写设备树的一般需要在一下目录中查找
/home/linux/linux-5.10.61/Documentation/devicetree/bindings/gpio/gpio.txt
gpio子系统API
int of_get_named_gpio(struct device_node *np,const char *propname, int index)
功能:根据节点结构体解析gpio编号
参数:
@np:节点指针 //获取节点后的node
@propname:键 //"led"
@index:索引号
返回值:成功返回gpio的编号,失败返回错误码
int gpio_request(unsigned gpio, const char *label)
功能:申请要使用的gpio
参数:
@gpio:gpio的编号
@label:标签名 //NULL
返回值:成功返回0,失败返回错误码
int gpio_direction_input(unsigned gpio)
功能:设置gpio的为输入
参数:
@gpio:gpio的编号
返回值:成功返回0,失败返回错误码
int gpio_direction_output(unsigned gpio, int value)
功能:设置gpio的为输出
参数:
@gpio:gpio的编号
@value:默认电平的状态 1高电平 0低电平
返回值:成功返回0,失败返回错误码
int gpio_get_value(unsigned gpio)
功能:读取管脚的值
参数:
@gpio:gpio的编号
返回值:1高电平 0低电平
void gpio_set_value(unsigned gpio, int value)
功能:设置输出电平的值
参数:
@gpio:gpio的编号
@value:1高电平 0低电平
返回值:无
void gpio_free(unsigned gpio)
功能:释放gpio
参数:
@gpio:gpio的编号
返回值:无
内核定时器
当前时间如何获取?
jiffies;内核时钟节拍数,从内核启动开始,一直在增加的一个数。
定时器加1走的时间?
定时器的频率配置在内核的.config中保存着
CONFIG_HZ=100
定时器加1走的时间是10ms
1.分配对象
struct timer_list {
struct hlist_node entry; //内核链表
unsigned long expires; //定时的时间
void (*function)(struct timer_list *); //定时器处理函数(定时到了执行的函数)
unsigned long data; //向定时器处理函数传递的参数
u32 flags; //0
};
struct timer_list mytimer; //定义
2.对象初始化
mytimer.expires = jiffies+HZ; //定时时间是1s
timer_setup(&mytimer, 定时器处理函数, 0); //定时器初始化函数
3.启动
void add_timer(struct timer_list *timer) //注册并启动定时器
int mod_timer(struct timer_list *timer, unsigned long expires) //再次启动定时器
4.注销
int del_timer(struct timer_list *timer) //删除定时器
新版gpio子系统接口
struct gpio_desc *gpiod_get_from_of_node(struct device_node *node,
const char *propname, int index,
enum gpiod_flags dflags,
const char *label) //一步到位
@node: 解析设备树的node号
@propname:键的名字"led"
@index:索引号
@dflags:赋初值GPIOD_OUT_HIGH ,可以通过enum gpiod_flags dflags中的gpio_flags追到
@label:标签名 0或NULL
int gpiod_request(struct gpio_desc *desc, const char *label)
int gpiod_direction_input(struct gpio_desc *desc)
int gpiod_direction_output(struct gpio_desc *desc, int value)
int gpiod_get_value(const struct gpio_desc *desc)
void gpiod_set_value(struct gpio_desc *desc, int value)
void gpiod_put(struct gpio_desc *desc) //释放
//获取设备树的节点
node=of_find_node_by_path("/mydemo");
if(NULL==node){
printk("of find node by path error\n");
goto ERR5;
}
//解析gpio
for(i=0;i<3;i++){
led_gpio[i]=gpiod_get_from_of_node(node,"led",i,GPIOD_OUT_LOW,0);//GPIOD_OUT_LOW初始化灯为灭
if(IS_ERR(led_gpio[i])){
printk("gpid get from of nade error\n");
goto ERR6;
}
}
for(i=0;i<3;i++){
gpiod_put(led_gpio[i]);//释放GPIO
}
中断(day7)
中断设备树解析函数
struct device_node *of_find_node_by_path(const char *path)
功能:通过路径或者节点结构体
参数:
@path:路径
返回值:成功返回节点的首地址,失败返回NULL
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
功能:解析得到软中断号
参数:
@dev:节点指针
@index:索引号
返回值:成功返回大于0的值,失败返回0
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
功能:注册中断
参数:
@irq:软中断号
@handler:中断处理函数的函数指针
//中断处理函数
irqreturn_t key_irq_handle(int irq, void *dev)
{
return IRQ_NONE ; //中断没有处理
return IRQ_HANDLED; //中断处理完成了
}
@flags:中断触发方式
IRQF_TRIGGER_RISING
IRQF_TRIGGER_FALLING
IRQF_TRIGGER_HIG
IRQF_TRIGGER_LOW
IRQF_SHARED //共享中断
@name:中断的名字 //cat /proc/interrupts
@dev:向中断处理函数传递的参数
返回值:成功返回0,失败返回错误码
void *free_irq(unsigned int irq, void *dev)
功能:释放中断
参数:
@irq:软中断号
@dev:向中断处理函数传递的参数
返回值:返回request_irq的第4个参数name
#define IRQNAME "key_irq" //中断的名字
struct device_node* node; //设备树的节点
unsigned int irqno; //软终端号
irqreturn_t key_irq_handle(int irq, void *dev)//中断处理函数第二个参数
{
return IRQ_HANDLED; //中断处理完成了
}
// 1.获取节点
node = of_find_node_by_path("/myirqs");
if (node == NULL) {
printk("get node error\n");
return -EINVAL;
}
// 2.根据节点获取软中断号
for (i = 0; i < ARRAY_SIZE(irqno); i++) {
irqno[i] = irq_of_parse_and_map(node, i);
if (irqno[i] == 0) {
printk("get irq number error\n");
return -EAGAIN;
}
// 3.注册中断
ret = request_irq(irqno[i], key_irq_handle,
IRQF_TRIGGER_FALLING, name[i], (void *)i);
if (ret) {
printk("request irq error\n");
return ret;
}
}
static void __exit mycdev_exit(void){
int i;
for (i = 0; i < ARRAY_SIZE(irqno); i++) {
free_irq(irqno[i], (void *)i);
}
}
中断底半部
在中断处理函数中不能够做延时或者耗时的操作,但是有的时候有希望在中断到来的时候做尽可能的操作,所以两者是矛盾的,内核为了解决这个矛盾推出中断底半部的机制。分别是软中断(32),tasklet,工作队列。
中断顶部 :紧急的,不耗时的操作
中断底半部:不紧急的,耗时的操作
例如:在网卡中断到来的时候,会从网络上读取数据包,这个数据读取的过程就是耗时操作,所以就可以把这个耗时操作放在中断底半部中完成。
tasklet机制
tasklet:tasklet是基于软中断实现的,但是没有个数限制,因为是通过链表实现的。
tasklet工作与中断上下文,在底半部处理函数中可以做耗时操作,但是不能做延时或者休眠的操作。tasklet是中断的一个部分,不能够脱离中断执行
1.分配对象
struct tasklet_struct //初始化
{
struct tasklet_struct *next; //指向下一个成员
unsigned long state; //状态
atomic_t count; //计数值
bool use_callback; //use_callback 真callback 假func
union {
void (*func)(unsigned long data); //底半部处理函数
void (*callback)(struct tasklet_struct *t);
};
unsigned long data; //向底半部处理函数传递参数
};
2.初始化对象
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) //旧版本
void tasklet_setup(struct tasklet_struct *t,
void (*callback)(struct tasklet_struct *)) //新版本
3.调用执行
void tasklet_schedule(struct tasklet_struct *t)
struct tasklet_struct mytask;//定义
void tasklet_callback(struct tasklet_struct* task){
int i=30;
while(--i){
printk("i = %d\n",i);
}
}
irqreturn_t key_irq_handle(int irq, void* dev){ //注册中断的第二个参数
tasklet_schedule(&mytask);//唤醒中断
return IRQ_HANDLED;
}
//初始化tasklet
tasklet_setup(&mytask, tasklet_callback);
工作队列机制
工作队列:工作队列工作与进程上下文,没有个数限制,可以脱离中断单独执行。
工作队列中可以有延时,耗时,甚至休眠的操作。
在内核启动的时候默认会启动一个events的线程,这个线程默认处于休眠状态,如果你想让这个线程执行你的代码,只需要将work提交到这个线程维护的队列中。唤醒这个休眠的线程,这个线程就会执行你的驱动的代码。
1.分配对象
struct work_struct {
atomic_long_t data; //携带的数据
struct list_head entry; //构成队列
work_func_t func; //底半部处理函数
};
typedef void (*work_func_t)(struct work_struct *work);//不用管
struct work_struct work; //定义
2.对象初始化
void work_func(struct work_struct *work){
}
INIT_WORK(&work, work_func); //初始化
3.调用执行
bool schedule_work(struct work_struct *work)
struct work_struct work;//定义
void work_func(struct work_struct *work){
int i=30;
while(--i){
mdelay(500);
printk("i = %d\n",i);
}
}
irqreturn_t key_irq_handle(int irq, void* dev){
schedule_work(&work); //唤醒
return IRQ_HANDLED;
}
//初始化工作队列
INIT_WORK(&work,work_func);
platform总线驱动
在linux内核中,将驱动分成了三个部分device,bus,driver,device是用来描述硬件设备的bus就是用来描述总线的,driver就相当于是驱动。device会被放到内核的klist_device链表中,driver会被放在klist_driver链表中通过bus完成匹配过程。当匹配成功之后会执行driver中的probe函数,当device和driver分离的时候就会执行driver中的remove函数。
API
只需要看驱动端就可以了,设备端后期演变成设备树
1.分配并初始化对象
struct platform_driver drv={ //操作方法结构体
int (*probe)(struct platform_device *); //匹配成功执行的函数
int (*remove)(struct platform_device *); //分离的时候执行的函数
struct device_driver driver; //父类
const struct platform_device_id *id_table; //2.idtable匹配
};
struct device_driver {
const char *name; //1.名字匹配 随便写
const struct of_device_id *of_match_table; //3.设备树匹配
};
2.注册
platform_driver_register(drv)
3.注销
void platform_driver_unregister(struct platform_driver *);
以下获取中断的设备信息
struct resource *platform_get_resource(struct platform_device *dev,unsigned int type, unsigned int index)
功能:在驱动中获取设备信息
参数:
@dev :platform_device的结构体指针
@type:资源的类型
@index:同类型资源的索引号
返回值:成功返回resource的结构体指针,失败返回NULL
int platform_get_irq(struct platform_device *dev, unsigned int index)
功能:获取中断类型的资源
参数:
@dev :platform_device的结构体指针
@index:中断类型资源的索引号
返回值:成功返回中断号,失败返回错误码
一建注册宏
#define module_platform_driver(__platform_driver) //使用他就可以了,不需要添加__init,__exit
module_driver(__platform_driver, platform_driver_register,
platform_driver_unregister)
#define module_driver(__driver, __register, __unregister, ...)
static int __init pdrv_init(void)
{
return platform_driver_register(&pdrv);
}
module_init(pdrv_init);
static void __exit pdrv_exit(void)
{
platform_driver_unregister(&pdrv);
}
module_exit(pdrv_exit);
struct resource *res;
int irqno;
int pdrv_probe(struct platform_device*pdev){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
return 0;
}
int pdrv_remove(struct platform_device*pdev){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
return 0;
}
struct platform_driver pdrv = {
.probe = pdrv_probe,
.remove = pdrv_remove,
.driver = {
.name = "duangduangduang",
},
};
module_platform_driver(pdrv);//使用它代替了init,exit
MODULE_LICENSE("GPL");
MODULE_DEVICE_TABLE
MODULE_DEVICE_TABLE(总线类型,idtable数组首地址); //MODULE_DEVICE_TABLE(of,oftable )
这个宏实现一个热插拔的效果,如果在驱动中实现了这宏,在安装
设备的时候,会自动将驱动一并安装上。例如在向ubuntu上插入U盘
的时候会将U盘的驱动安装成功。
测试过程如下:
1.先将pdev.ko和pdrv.ko放到ubuntu的如下目录中
/lib/modules/你的内核版本/kernel/drivers/platform/
2.执行depmod -a命令,让内核重新检索文件的位置
3.安装pdev.ko,pdrv.ko会被自动安装
const struct of_device_id oftable[] = {
{.compatible = "hqyj,myplatform",},
{/*end*/}
};
MODULE_DEVICE_TABLE(of,oftable);
struct platform_driver pdrv = {
.probe = pdrv_probe,
.remove = pdrv_remove,
.driver = {
.name = "duangduangduang",
.of_match_table = oftable,
},
};
module_platform_driver(pdrv);
MODULE_LICENSE("GPL");
platform中匹配的方法
const struct of_device_id *of_match_table;
struct of_device_id {
char name[32];
char type[32];
char compatible[128]; //通过本选项和设备树完成匹配
const void *data;
};
struct of_device_id oftable[] = {
{.compatible = "xxx,xx0",},
{.compatible = "xxx,xx1",},
{.compatible = "xxx,xx2",},
{/*end*/}
};
MODULE_DEVICE_TABLE(of,oftable);
struct platform_driver {
.driver = {
.name="duangduang", //随便填 但是必须填写
.of_match_table = oftable,
},
};
struct resource *res;
int irqno,gpiono;
int pdrv_probe(struct platform_device*pdev){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
//能够解析设备树中的reg成员
res = platform_get_resource(pdev,IORESOURCE_MEM,0);
if(res == NULL){
printk("platform get resource error\n");
return -EINVAL;
}
//能够直接得到设备树上的中断号并解析为软中断号
irqno = platform_get_irq(pdev,0);
if(irqno < 0){
printk("platform get irq error\n");
return irqno;
}
printk("addr = %#x,irqno = %d\n",res->start,irqno);
//
gpiono = of_get_named_gpio(pdev->dev.of_node,"led1",0);//pdev->dev.of_node 设备节点
if(gpiono < 0){
printk("get gpio number error\n");
return gpiono;
}
printk("gpio number = %d\n",gpiono);
return 0;
}
int pdrv_remove(struct platform_device*pdev){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
return 0;
}
const struct of_device_id oftable[] = {
{.compatible = "hqyj,myplatform",},
{/*end*/}
};
MODULE_DEVICE_TABLE(of,oftable);
struct platform_driver pdrv = {
.probe = pdrv_probe,
.remove = pdrv_remove,
.driver = {
.name = "duangduangduang",
.of_match_table = oftable, //中断
},
};
module_platform_driver(pdrv);
MODULE_LICENSE("GPL");
i2c总线驱动(day9)
i2c总线分析
两根线:
sda:数据线 scl:数据线
四种信号:
起始信号:当scl为高电平的时候,sda从高到低的跳变
停止信号:当scl为高电平的时候,sda从低到高的跳变
应答信号:在第九个时钟周期的时候,sda低电平表示应答
非应答信号:在第九个时钟周期的时候,sda持续高电平表示非应答
写的时序:
start+(7位从机地址 1位的写0)+ack+reg(8bit或16bit)+ack+data(8bit或16bit)+ack+stop
读的时序:
start+(7位从机地址 1位的写0)+ack+reg(8bit或16bit)+ack
start+(7位从机地址 1位的读1)+ack+data(8bit或16bit)+NO ack+stop
i2c总线速率:
100K 低速 400K 全速 3.4M 高速
i2c总线特点:
i2c是半双工的,同步,串行,具备通过从机地址寻址从机和应答校验的总线协议
总线API
1.分配对象及初始化
struct i2c_driver {
int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
int (*remove)(struct i2c_client *client);
struct device_driver driver;
};
struct device_driver {
const char *name;//随便写
const struct of_device_id *of_match_table;
//struct of_device_id of_match_table[]={
{.compatible="hqyj,si7006"}//设备树里写的名字
{}
};
};
2.注册
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)
3.注销
void i2c_del_driver(struct i2c_driver *driver);
4.一键注册的宏
module_i2c_driver(变量名);
int si7006_probe(struct i2c_client* client, const struct i2c_device_id* id){}
int si7006_remove(struct i2c_client* client){}
struct of_device_id oftable[] = {
{.compatible = "hqyj,si7006",},
{}
};
MODULE_DEVICE_TABLE(of, oftable);
struct i2c_driver si7006 = {
.probe = si7006_probe,
.remove = si7006_remove,
.driver = {
.name = "duang", //随便写,必须写
.of_match_table = oftable,
}
};
module_i2c_driver(si7006);
MODULE_LICENSE("GPL");
i2c相关结构体(day10)
当设备驱动(i2c_driver)和控制器驱动(i2c_adapter)就会在内核中创建一个i2c_client结构体,这个结构体就是记录那个adapter和那个driver匹配成功的对象。
//i2c 7位 8位 10位从机寻址过程
//https://www.totalphase.com/support/articles/200349176-7-bit-8-bit-and-10-bit-I2C-Slave-Addressing
struct i2c_client {
unsigned short flags; // 0写 1读
unsigned short addr; //从机地址
char name[I2C_NAME_SIZE]; //驱动的名字
struct i2c_adapter *adapter;//控制器驱动的对象
struct device dev; //这是设备的对象
};
消息结构体
struct i2c_msg {
__u16 addr; //从机地址struct i2c_client中有从机地址
__u16 flags; //读写标志位 0写 1读
__u16 len; //消息长度
__u8 *buf; //消息首地址
};
封装读写的的消息
start+(7位从机地址 1位的写0)+ack+reg(8bit或16bit)+ack+data(8bit或16bit)+ack+stop
char w_buf[] = {reg,data};
struct i2c_msg w_msg = {
.addr = client->addr,
.flags = 0,
.len = 2,
.buf = w_buf,
};
start+(7位从机地址 1位的写0)+ack+reg(8bit)+ack
start+(7位从机地址 1位的读1)+ack+data(8bit)+NO ack+stop
char r_buf[] = {reg};
char val;
struct i2c_msg r_msg[] = {
[0] = {
.addr = client->addr,
.flags = 0,
.len = 1,
.buf = r_buf,
},
[1] = {
.addr = client->addr,
.flags = 1,
.len = 1,
.buf = &val; //读取的数据给val
},
};
消息发送函数
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
功能:消息的发送函数
参数:
@adap:控制器的结构体对象//struct i2c_client 中有
@msgs:消息结构体的首地址//struct i2c_mag结构体 r_msg
@num:消息结构体的个数//ARRAY_SIZE(r_msg)
返回值:成功返回num,否则就是失败
stm32mp157a
i2c读取温湿度
0xe5 湿度 0xe3温度
设备树
0.i2c硬件的连接图 需要直到gpio引脚 和 从机地址
1.查看i2c控制器的设备树
stm32mp151.dtsi //根设备树
stm32mp15xx-dkx.dtsi //别人写的
/home/linux/linux-5.10.61/Documentation/devicetree/bindings/i2c //都是别人写的
2.参考内核的帮助文档编写自己的设备树
stm32mp157a-fsmp1a.dts
温湿度代码
#ifndef __SI7006_H__
#define __SI7006_H__
#define GET_HUM _IOR('l',2,int)
#define GET_TMP _IOR('l',3,int)
#define HUM_ADDR 0xe5
#define TMP_ADDR 0xe3
#endif
//驱动
#define I2CNAME "si7006"
struct i2c_client* gclient;
int major = 0;
struct class* cls;
struct device* dev;
int i2c_read_hum_tmp(unsigned char reg){
// 1.封装消息
int ret;
unsigned char r_buf[] = { reg };
unsigned short val;
struct i2c_msg r_msg[] = {
[0] = {
.addr = gclient->addr,
.flags = 0,
.len = 1,
.buf = r_buf,
},
[1] = {
.addr = gclient->addr,
.flags = 1,
.len = 2,
.buf = (char *)&val,
},
};
// 2.发送消息
ret = i2c_transfer(gclient->adapter, r_msg, ARRAY_SIZE(r_msg));
if (ret != ARRAY_SIZE(r_msg)) {
printk("i2c read hum or temp error\n");
return -EAGAIN;
}
return val >> 8 | val << 8;
}
long si7006_ioctl(struct file* file,unsigned int cmd, unsigned long arg){
int ret, data;
switch (cmd) {
case GET_HUM:
data = i2c_read_hum_tmp(HUM_ADDR);
if (data < 0) {
printk("i2c read error\n");
return data;
}
data = data & 0xffff; //返回的是int类型的所以需要把其它的清空了
ret = copy_to_user((void*)arg, &data, 4);
if (ret) {
printk("copy data to user error\n");
return -EINVAL;
}
break;
case GET_TMP:
data = i2c_read_hum_tmp(TMP_ADDR);
if (data < 0) {
printk("i2c read error\n");
return data;
}
data = data & 0xffff;
ret = copy_to_user((void*)arg, &data, 4);
if (ret) {
printk("copy data to user error\n");
return -EINVAL;
}
break;
}
return 0;
}
spi总线驱动
SPI 是串行外设接口(Serial Peripheral Interface)的缩写。 是 Motorola 公司推出的一种同步串行接口技术,是一种 高速的,全双工,同步的通信总线。
SPI优点
支持全双工通信
通信简单
数据传输速率快
1):高速、同步、全双工、非差分、总线式
2):主从机通信模式
缺点
没有指定的流控制,没有应答机制确认是否接收到数据,
所以跟IIC总线协议比较在数据的可靠性上有一定的缺陷。
spi时序解析
可以一主机多从机,具体和那个从机通讯通过cs片选决定。
MISO :主机输入,从机输出
MOSI :主机输出,从机输入
CLK :时钟线(只能主机控制)
CS :片选线
数据传输的四种方式:
CPOL(时钟极性) : 0:时钟起始位低电平 1:时钟起始为高电平
CPHA(时钟相位) :0:第一个时钟周期采样 1:第二个时钟周期采样
例如:
CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿。
CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。
总线API
1.分配并初始化对象
struct spi_driver {
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
struct device_driver driver;
};
struct device_driver {
const char *name;
const struct of_device_id *of_match_table;
//struct of_device_id of_device_id={
//MODULE_DEVICE_TABLE(of,of_match);
{compatible="hqyj,ic2"},
{}
};
}
2.注册
#define spi_register_driver(driver) \
__spi_register_driver(THIS_MODULE, driver)
3.注销
void spi_unregister_driver(struct spi_driver *sdrv)
4.一键注册的宏
module_spi_driver(结构体变量名);
spi收发数据
给寄存器读写用的,
int spi_write(struct spi_device *spi, const void *buf, size_t len) //发数据
功能:发送数据
参数:
@spi:probe函数的参数,需要把他弄成全局量使用
@buf:数据
@len:数据的大小
返回值:成功返回 0 | 失败返回错误码
int spi_read(struct spi_device *spi, void *buf, size_t len) //接收数据
int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx); //同时收发
#define NAME "m74hc595"
int major = 0;
struct class *cls;
struct device *dev;
struct spi_device *gspi;
u8 code[] = {
0x3f, //0
0x06, //1
0x5b, //2
0x4f, //3
0x66, //4
0x6d, //5
0x7d, //6
0x07, //7
0x7f, //8
0x6f, //9
0x77, //A
0x7c, //b
0x39, //c
0x5e, //d
0x79, //e
0x71, //f
};
u8 which[] = {
0x1, //sg0
0x2, //sg1
0x4, //sg2
0x8, //sg3
};
int m74hc595_open(struct inode *inode, struct file *file){
printk("%s:%d\n",__func__,__LINE__);
return 0;
}
long m74hc595_ioctl(struct file *file, unsigned int cmd, unsigned long args){
switch(cmd){
case SEG_WHICH:
spi_write(gspi,&which[args],1);
break;
case SEG_DAT:
spi_write(gspi,&code[args],1);
break;
default: printk("ioctl error\n");break;
}
return 0;
}
int m74hc595_close(struct inode *inode, struct file *file){
printk("%s:%d\n",__func__,__LINE__);
return 0;
}
struct file_operations fops = {
.open = m74hc595_open,
.unlocked_ioctl = m74hc595_ioctl,
.release = m74hc595_close,
};
int m74hc595_probe(struct spi_device *spi){
u8 buf[2] = {0xf,0x0};
printk("%s:%d\n",__func__,__LINE__);
gspi = spi;
spi_write(gspi,buf,ARRAY_SIZE(buf));
major = register_chrdev(0,NAME,&fops);
if(major < 0){
printk("register chrdev error\n");
return major;
}
cls = class_create(THIS_MODULE,NAME);
if(IS_ERR(cls)){
printk("class create error\n");
return PTR_ERR(cls);
}
dev = device_create(cls,NULL,MKDEV(major,0),NULL,NAME);
if(IS_ERR(dev)){
printk("device create error\n");
return PTR_ERR(dev);
}
return 0;
}
int m74hc595_remove(struct spi_device *spi)
{
printk("%s:%d\n",__func__,__LINE__);
device_destroy(cls,MKDEV(major,0));
class_destroy(cls);
unregister_chrdev(major,NAME);
return 0;
}
const struct of_device_id of_match[] = {
{.compatible = "hqyj,m74hc595",},
{},
};
MODULE_DEVICE_TABLE(of,of_match);
struct spi_driver m74hc595 = {
.probe = m74hc595_probe,
.remove = m74hc595_remove,
.driver = {
.name = "hello_m74hc595",
.of_match_table = of_match,
},
};
module_spi_driver(m74hc595);
MODULE_LICENSE("GPL");
PWM
蜂鸣器和风扇一样
struct pwm_device *pwm_get(struct device *dev, const char *con_id);
功能:申请一个PWM设备
参数:
@dev:platform_device中的dev
@con_id:NULL
返回值:成功返回 设备指针 | 失败返回 ERR_PTR()的错误码
int pwm_config(struct pwm_device *pwm, int duty_ns,int period_ns);
功能:配置PWM周期和占空比
参数:
@pwm:pwm设备指针
@duty_ns:时间(纳秒)
@period_ns:一个周期持续的时间(纳秒)
返回值: 成功返回 0 | 失败返回错误码
int pwm_enable(struct pwm_device *pwm); //先配置周期和占空比后使能
功能:使能pwm
参数:
@pwm:pwm设备指针
返回值:成功 0 | 失败返回错误码
int pwm_disable(struct pwm_device *pwm);
功能:关闭PWM
参数:
@pwm:pwm设备指针
返回值:
void pwm_free(struct pwm_device *pwm);
功能:释放申请的pwm
参数:
@pwm:pwm的指针
返回值:
#define BEENAME "mybee"
#define HZ_TO_NANOSECONDS(x) (1000000000UL/(x))
/*
struct pwm_beeper{
struct pwm_device *pwm;
unsigned long period;
};
struct pwm_beeper *beeper; //使用这个结构体也可以
*/
struct cdev *bee_cdev;
dev_t bee_dev;
int minor=0;
struct class *bee_class;
struct device *bee_up_device;
struct pwm_device *bee_pwm;//pwm
unsigned long period; //周期
int bee_open (struct inode *inode, struct file *file){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
return 0;
}
long bee_ioctl (struct file *file, unsigned int cmd, unsigned long age){
int ret;
int value;
ret=copy_from_user(&value,(void*)age,sizeof(value));
if(ret<0){
printk("copy from user error\n");
return -1;
}
if(value==0){
pwm_disable(bee_pwm);
}else{
period=HZ_TO_NANOSECONDS(value);
ret=pwm_config(bee_pwm,period/2,period);
if(ret){
printk("pwm config error\n");
return ret;
}
ret=pwm_enable(bee_pwm);
if(ret){
printk("pwm enable error\n");
return ret;
}
}
return 0;
}
int bee_close (struct inode *inode, struct file *file){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
pwm_disable(bee_pwm); //关闭PWM设备
return 0;
}
static const struct file_operations fops={
//操作方法结构体 使用的是ioctl
};
int bee_probe(struct platform_device *pdev){
int ret=0;
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
bee_up_device=&pdev->dev;//因为类型一样,所以只是用它接了一下,可以分装成结构体上面有结构体
//struct device *dev = &pdev->dev; //该这样写
bee_pwm=pwm_get(bee_up_device,NULL); //申请PWM设备
if(IS_ERR(bee_pwm)){
printk("pwm get err or\n");
goto ERR;
}
//低下是申请节点并注册
bee_cdev=cdev_alloc();
cdev_init(bee_cdev,&fops);
ret=alloc_chrdev_region(&bee_dev,minor,0,BEENAME);
ret=cdev_add(bee_cdev,bee_dev,1);
bee_class=class_create(THIS_MODULE,BEENAME);
bee_up_device=device_create(bee_class,NULL,bee_dev,NULL,BEENAME);
return 0;
//这里回收没有写
}
int bee_remove(struct platform_device *pdev){
device_destroy(bee_class,bee_dev);
class_destroy(bee_class);
pwm_disable(bee_pwm);//关闭PWM
pwm_free(bee_pwm);//释放pwm
cdev_del(bee_cdev);
unregister_chrdev_region(bee_dev,1);
kfree(bee_cdev);
return 0;
}
//platform一建注册宏
块设备驱动(day10)
内核分配的函数
void *kmalloc(size_t s, gfp_t gfp)
功能:分配对应的虚拟内存
参数:size:分配内存区的大小
flags:内存分配标志
GFP_KERNEL:内核可能被休眠,用于进程上下文中
GFP_ATOMIC:处理紧急的事务,用在中断上下文
返回值:对应虚拟地址
特点:最大128k , 分配虚拟地址,其虚拟地址空间连续,
物理地址空间也是连续,分配的内存必须是2的次幂的形式
类似函数:kzalloc = kmalloc+memset(,0,):分配虚拟内存区并清零
void kfree(const void *x)
功能:释放对应的虚拟内存
参数:x:虚拟内存的起始地址
返回值:无
void *vmalloc(unsigned long size)
功能:分配对应的虚拟内存
参数:size:分配内存区的大小
返回值:对应虚拟地址
特点:分配虚拟地址,其虚拟地址空间连续,
但是物理地址空间不一定连续
void vfree(const void *addr)
功能:释放对应的虚拟内存
参数:addr:虚拟内存区的首地址
返回值:无
unsigned long __get_free_page(gfp_t gfp)
功能:分配一个页的内存 4K
void free_page(unsigned long addr)
释放一个页的内存
unsigned long __get_free_pages(gfp_t gfp_mask, get_order(57600))
功能:分配多个页的内存
57600-->2^n :第二个参数填写的是n
n = get_order(57600)
void free_pages(unsigned long addr, unsigned long order)
释放多个页的内存
步骤
|driver:gendisk
1.分配对象 gendisk对象
2.对象初始化
3.初始化一个队列 head----request(read)----request(write)---...
//4.硬盘设备的初始化
5.注册、注销
函数
struct gendisk {
int major; //块设备的主设备号 需要调用函数 register_blkdev
int first_minor; //起始的次设备号,直接填
int minors; //设备的个数,分区的个数,说明硬盘最多可以分多少分区 alloc_disk函数搞定了
char disk_name[DISK_NAME_LEN]; //磁盘的名字,不用管alloc_disk已经完成必要的初始化了
struct disk_part_tbl *part_tbl; //磁盘的分区表的首地址,set_capacity需要设置一下↓
struct hd_struct part0;//part0分区的描述 需要调用函数 ↓
const struct block_device_operations *fops;//块设备的操作方法结构体 ↓
struct request_queue *queue;//队列(重要)需要调用函数 ↓
void *private_data;//私有数据,可以用它来传参
};
struct hd_struct { //分区的结构体
sector_t start_sect; //起始的扇区号
sector_t nr_sects; //扇区的个数
int partno; //分区号
};
struct block_device_operations { //操作方法结构体
int (*open) (struct block_device *, fmode_t);
int (*release) (struct gendisk *, fmode_t);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*getgeo)(struct block_device *, struct hd_geometry *);
//设置磁盘的磁头,磁道,扇区的个数的。hd_geometry
}
struct gendisk *alloc_disk(int minors)
//void put_disk(struct gendisk *disk)
//归还引用计数
功能:分配gendisk的内存,然后完成必要的初始化
参数:
@minors:分区的个数 //是gdendisk结构体里的
返回值:成功返回分配到的内存的首地址,失败返回NULL
int register_blkdev(unsigned int major, const char *name)
//void unregister_blkdev(unsigned int major, const char *name)
功能:申请设备设备驱动的主设备号
参数:
@major : 0:自动申请
>0 :静态指定
@name :名字 cat /proc/devices
返回值:
major=0 ;成功返回主设备号,失败返回错误码
major>0 :成功返回0 ,失败返回错误码
void set_capacity(struct gendisk *disk, sector_t size)
功能:设置磁盘的容量
struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,const struct blk_mq_ops *ops,unsigned int queue_depth,unsigned int set_flags)
//void blk_cleanup_queue(struct request_queue *q)
功能:用于在给定队列深度的情况下使用mq ops设置队列的助手,以及通过mq ops标志传递的助手
参数:
@被初始化的tag对象,tag被上层使用,里面包含硬件队列的个数,队列的操作方法结构体,标志位等,不用管直接定义使用,这个函数就是给blk_mq_tag_set *set初始化的给上层使用的
@放入到tag中的操作方法结构体
@ tag中指定支持的队列深度
@将tag中队列的处理标志位,例如BLK_MQ_F_SHOULD_MERGE, BLK_MQ_F_BLOCKING等
返回值:成功返回队列指针,失败返回错误码指针
3.注册、注销
void add_disk(struct gendisk *disk)
//注册
void del_gendisk(struct gendisk *disk)
//注销
// 1.分配gendisk
struct gendisk* mydisk;
mydisk = alloc_disk(4);
if (mydisk == NULL) {
printk("alloc disk memory error\n");
return -ENOMEM;
}
// 2.对象初始化
#define BLKSIZE (1 * 1024 * 1024) // 1M
int major = 0;
major = register_blkdev(0, DISKNAME);//分配节点
if (major < 0) {
printk("alloc block device number error\n");
return major;
}
struct gendisk* mydisk;
set_capacity(mydisk, BLKSIZE >> 9);//设置磁盘的容量
struct request_queue* q;
struct blk_mq_tag_set set;
blk_status_t mydisk_queue_rq(struct blk_mq_hw_ctx* ctx, const struct blk_mq_queue_data* hd){
//队列的读写操作
return BLK_STS_OK;
}
struct blk_mq_ops mqops = { //操作方法结构体
.queue_rq = mydisk_queue_rq,
};
q = blk_mq_init_sq_queue(&set, &mqops, 2, BLK_MQ_F_SHOULD_MERGE);//
if (IS_ERR(q)) {
printk("mutil queue init error\n");
return PTR_ERR(q);
}
mydisk->major = major;
mydisk->first_minor = 0; //次设备号
strcpy(mydisk->disk_name, DISKNAME); //名字
mydisk->fops = &fops; //操作方法结构体
mydisk->queue = q;
#include <linux/blk-mq.h>
#include <linux/genhd.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/hdreg.h>
#define BLKSIZE (1 * 1024 * 1024) // 1M
#define DISKNAME "mydisk"
struct gendisk* mydisk;
int major = 0;
struct request_queue* q;
struct blk_mq_tag_set set;
char *disk_addr = NULL; //磁盘的首地址
blk_status_t mydisk_queue_rq(struct blk_mq_hw_ctx* ctx,const struct blk_mq_queue_data* hd){
//队列的读写操作
return BLK_STS_OK;
}
struct blk_mq_ops mqops = {
.queue_rq = mydisk_queue_rq,
};
int mydisk_open(struct block_device*blkdev, fmode_t mode){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
return 0;
}
int mydisk_getgeo(struct block_device *blkdev, struct hd_geometry *hd){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);
/*
struct hd_geometry {
unsigned char heads; //磁头的个数
unsigned char sectors; //扇区格式
unsigned short cylinders;//磁道个数
};*/
hd->heads = 4;
hd->cylinders = 16;
hd->sectors = BLKSIZE/hd->heads/hd->cylinders/512;
return 0;
}
void mydisk_close(struct gendisk*disk, fmode_t mode){
printk("%s:%s:%d\n",__FILE__,__func__,__LINE__);\
}
struct block_device_operations fops = {
.open = mydisk_open,
.release = mydisk_close,
.getgeo = mydisk_getgeo,
};
static int __init mydisk_init(void){
// 1.分配gendisk
mydisk = alloc_disk(4);
if (mydisk == NULL) {
printk("alloc disk memory error\n");
return -ENOMEM;
}
// 2.对象初始化
major = register_blkdev(0, DISKNAME);
if (major < 0) {
printk("alloc block device number error\n");
return major;
}
set_capacity(mydisk, BLKSIZE >> 9);
q = blk_mq_init_sq_queue(&set, &mqops, 2, BLK_MQ_F_SHOULD_MERGE);
if (IS_ERR(q)) {
printk("mutil queue init error\n");
return PTR_ERR(q);
}
mydisk->major = major;
mydisk->first_minor = 0;
strcpy(mydisk->disk_name, DISKNAME);
mydisk->fops = &fops;
mydisk->queue = q;
// 3.分配1M的内存当成硬盘使用
disk_addr = vmalloc(BLKSIZE);
if(disk_addr == NULL){
printk("alloc disk memory error\n");
return -ENOMEM;
}
// 4.注册
add_disk(mydisk);
return 0;
}
static void __exit mydisk_exit(void){
del_gendisk(mydisk);
vfree(disk_addr);
blk_cleanup_queue(q);
unregister_blkdev(major,DISKNAME);
put_disk(mydisk);
}
module_init(mydisk_init);
module_exit(mydisk_exit);
MODULE_LICENSE("GPL");
处理相关结构体
struct request_queue{
/*双向链表数据结构,将所有加入到队列的IO请求组建成一个双向链表*/
struct list_head queue_head;
struct list_head requeue_list; //request队列
spinlock_t requeue_lock; //队列自旋锁
unsigned long nr_requests; /* 最大的请求数量 */
unsigned long queue_flags;/*当前请求队列的状QUEUE_FLAG_STOPPED*/
…
};
struct request{
struct list_head queuelist;/* 请求对象中的链表元素*/
struct request_queue *q; /* 指向存放当前请求的请求队列*/
unsigned int __data_len; /* 当前请求要求数据传输的总的数据量 */
sector_t __sector; /* 当前请求要求数据传输的块设备的起始扇区 */
struct bio *bio; /* bio对象所携带的信息转存至请求对象中*/
struct bio *biotail; /* bio链表*/
…
};
通常一个request请求可以包含多个bio,一个bio对应一个I/O请求
struct bio {
struct bio *bi_next; /* 指向当前bio的下一个对象*/
unsigned long bi_flags; /* 状态、命令等 */
unsigned long bi_rw; /* 表示READ/WRITE 读写*/
struct block_device *bi_bdev; /* 与请求相关联的块设备对象指针*/
unsigned short bi_vcnt; /* bi_io_vec数组中元素个数 */
unsigned short bi_idx; /* 当前处理的bi_io_vec数组元素索引 */
unsigned int bi_size; /* 本次传输需要传输的数据总量,byte(扇区大小整数倍) */
struct bio_vec *bi_io_vec;/* 指向一个IO向量的数组,数组中的内各元素对应一个物理页的page对象 */
};
struct bio_vec {
struct page *bv_page; //指向用于数据传输的页面所对应的struct page对象
unsigned int bv_len; //表示当前要传输的数据大小
unsigned int bv_offset;//表示数据在页面内的偏移量
};
队列处理函数
blk_mq_start_request(rq); //开始处理队列
blk_mq_end_request(rq, BLK_STS_OK); //结束队列处理
rq_for_each_segment(bvec, rq, iter) //从request->bio_vec
void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset; //将页地址转换为线性地址(内地址)
rq_data_dir(rq)) //从request获取本次读写的方向 WRITE 1 READ 0
dev_addr+(rq->__sector *512) //磁盘设备的地址