裸机开发之驱动开发

2023-05-16

一、驱动开发的基础理解   

        在计算中,设备驱动程序是一种计算机程序,用于操作或控制连接到计算机的特定类型的设备。驱动程序提供了与硬件设备的软件接口,使操作系统和其他计算机程序可以访问硬件功能,而无需了解有关所使用硬件的精确细节。

驱动程序通过硬件连接到的计算机总线或通信子系统与设备进行通信。当调用程序调用驱动程序中的例程时,驱动程序向设备发出命令。设备将数据发送回驱动程序后,驱动程序可以调用原始调用程序中的例程。驱动程序依赖于硬件且特定于操作系统。它们通常为那些有必要的时间异步的硬件接口提供终端处理。

        驱动开发就是在操作系统的基础上实现驱动程序。也可以理解为驱动程序就是中间件,让应用层去控制底层寄存器进行工作。

        其实编写的驱动文件也是.c为结尾的文件,但是其在编译阶段是调用的内核来进行编译,所生成的也是满足该内核的驱动文件.ko结尾文件,所以在内核使用的时候也就是没问题的。

1、驱动框架

一个基本的驱动框架如下所示:        4部分组成

1、头文件
        #include <linux/init.h>
        #include <linux/module.h>

2、驱动入口函数的声明,在内核加载驱动时,执行哪个函数;在内核卸载驱动时,执行哪个函数
        module_init(hello_init);           //声明:加载时的入口声明
        module_exit(hello_exit);         //声明:卸载时的入口声明

3、加载函数、卸载函数的实现
        //加载函数的实现:当内核加载驱动(内核执行这个驱动时,就会调用的函数)
        static int __init hello_init(void)
        {

            return 0;
        }

        //卸载函数的实现:当内核卸载驱动(内核删除这个驱动时,就会调用的函数)
        static void __exit hello_exit(void)
        {

        }

4、协议选择GPL
        MODULE_LICENSE("GPL");

例如下面一个LED灯的示例程序

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>

unsigned int * gpx1con;
unsigned int * gpx1dat;

//驱动与应用程序函数关联
int led_open (struct inode * inode, struct file * file)
{
	printk("led_open\n");
	return 0;
}

int led_close (struct inode * inode, struct file * file)
{
	printk("led_close\n");
	return 0;
}

ssize_t led_read (struct file * file, char __user * data, size_t size, loff_t * ops)
{
	//读取数据
	printk("led_read");
	copy_to_user(data,"nihao",6);
	return 0;
}
ssize_t led_write (struct file * file, const char __user * data, size_t size, loff_t * ops)
{
	int num = 0;
	copy_from_user(&num,data,size);
	printk("num = %d\n",num);
	if(num == 1){	
		*gpx1dat |= 1;
	}
	else{
		*gpx1dat &= ~1;
	}
	return 0;
}

const struct file_operations fops = {
	.open = led_open,//当应用程序调用open时,驱动则执行结构体成员open对应的赋值函数
	.release = led_close,
	.read = led_read,
	.write = led_write,
};
//入口实现
//加载 insmod
static int __init led_init(void)
{
	//字符设备框架
	//1、申请设备号,驱动必须有一个设备号来区别其他驱动
	int ret = -1;
	ret = register_chrdev(250,"led",&fops);
	if(ret == 0){
		printk("register ok\n");
	}
	//2、创建设备节点---设备文件
	//创建设备节点信息结构体
	struct class * cls_led = class_create(THIS_MODULE,"led cls");
	//创建设备文件
	dev_t devt = 250<<20 | 0;//设备号
	struct device * led_dev = device_create(cls_led,NULL,devt,NULL,"led%d",3);
	if(led_dev != NULL){
		printk("device create ok\n");
	}
	//初始化硬件
	//地址映射
	gpx1con = ioremap(0x11000c20,4);//指针变量就是映射寄存器地址
	*gpx1con = *gpx1con & ~(0xf) | 0x1;
	gpx1dat = ioremap(0x11000c24,4);
	*gpx1dat |= 1;
	return 0;
}
//卸载
static void __exit led_exit(void)
{
	与初始化逆序过程进行卸载
	//1、映射释放(中断释放)
	iounmap(映射的虚拟内存地址);----释放映射地址
	//2、释放设备文件
	void device_destroy(struct class * class,dev_t devt);
	//3、释放设备文件结构体
	void class_destroy(struct class * cls)
    //4、释放设备号
	void unregister_chrdev(unsigned int major,const char * name)
}
//入口声明
module_init(led_init);
module_exit(led_exit);

//GPL声明
MODULE_LICENSE("GPL");
#include <sys/types.h>//进行应用的例程实现点灯
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
	int a = 1;
	int fd = open("/dev/led3",O_RDWR);
	while(1)
	{
	    a = 1;
	    write(fd,&a,4);
	    sleep(1);
	    a = 0;
	    write(fd,&a,4);
	    sleep(1);
	}
	close(fd);
	return 0;
}

虽然有很多知识没学过,后面再进行介绍,这个时候看着或许有一些是不懂的

        驱动程序也就是让其通过文件方式进行控制底层的硬件寄存器,在驱动中实现文件io接口功能(与应用关联),应用程序调用文件io时,驱动程序也调用对应的文件io接口函数
    在结构体 struct file_operations 每一个成员变量都代表绑定一个系统调用(文件io)函数,只要对结构体中的成员赋值,就代表值绑定上一个文件io函数  

  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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        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 (*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 **);
        long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
        int (*show_fdinfo)(struct seq_file *m, struct file *f);
    };//函数指针的集合,

        每个函数指针赋值为函数地址,就代表当应用程序调用对应的文件io函数时,驱动就执行函数指针赋值的对应函数,例如下面的LED驱动例程中的

const struct file_operations fops = {
	.open = led_open,//当应用程序调用open时,驱动则执行结构体成员open对应的赋值函数
	.release = led_close,
	.read = led_read,
	.write = led_write,
};

        当然,这些对应的函数则需要我们自己去按照格式进行操作,如open对应的led_open就是一个函数,看上面的例程就可以看到对应的操作。

在上例中的函数实现是将外面传输的数据是拷贝到另外一个地方的,保证不改变数据的内容

copy_to_user()
copy_from_user()

既将数据进行copy到那里去

        在基础的驱动框架中,有一个加载和卸载函数,这个函数是安装或者卸载这个驱动的将会去处理的函数。也就是这个里面写的这两个,一个对应初始化,而另外一个则对于着卸载的时候将会触发的。里面的led_init才是执行函数。

module_init(led_init);        //对应初始化
module_exit(led_exit);      //对应卸载

        最后还需要加入一个协议(通用公共许可协议GPL)才能进行在最后内核编译时通过,不然将发生内核污染的编译错误

MODULE_LICENSE("GPL");        //GPL声明

2、驱动开发之字符设备驱动模型

I/O设备大致可以分为两类:块设备(block device)字符设备(character device)

        字符设备发送或接收的是字符流,而不考虑任何块结构。字符设备无法编址,也不存在任何寻址操作打印机、网络接口、鼠标(用做指点设备),大多数与磁盘不同的设备均可被视为字符设备。

        块设备将信息存储在固定大小的块中,每个块都有自己的地址。数据块的大小通常在512字节到32768字节之间。块设备的基本特征是每个块都能独立与其他块而读写。磁盘是最常见的块设备。

我总结的是:只要不是存储在固定大小的地址中的设备都为字符设备。

   1、必须要有一个设备号,用于在内核中的众多设备驱动进行区分

    2、必须要有一个设备文件,用户必须知道设备驱动对应的设备节点(设备文件)

    3、驱动对设备的操作,与应用程序中的系统调用关联,其实就是文件操作

上面在led_init()函数里面就有了符号设备的操作了

//字符设备框架
	//1、申请设备号,驱动必须有一个设备号来区别其他驱动
	int ret = -1;
	ret = register_chrdev(250,"led",&fops);
	if(ret == 0){
		printk("register ok\n");
	}
	//2、创建设备节点---设备文件
	//创建设备节点信息结构体
	struct class * cls_led = class_create(THIS_MODULE,"led cls");
	//创建设备文件
	dev_t devt = 250<<20 | 0;//设备号
	struct device * led_dev = device_create(cls_led,NULL,devt,NULL,"led%d",3);
	if(led_dev != NULL){
		printk("device create ok\n");
	}

其中使用到了一些不认识的函数,如下

register_chrdev()        //用于创建一个设备号

int register_chrdev(unsigned int major,const char *name,const struct file_operations *fops)
参数1:unsigned int major:主设备号,次设备号自动分配
			设备号:32bit = 主设备号(12bit) + 次设备号(20bit)
			主设备号:表示同一类设备;次设备号:表示同一类设备中的不同设备
参数2:const char *name:描述一个设备驱动信息,自定义
参数3:const struct file_operations *fops:文件操作对象,函数关联(使用结构体来存储驱动与应用程序的关联)
		

class_create()             //创建设备节点信息结构体

struct class * class_create(owner,name);
参数1:owner:拥有者,一般THIS_MODULE
参数2:name:字符串,描述信息
返回值:struct class *  信息结构体

device_create()      //创建设备文件

struct device *device_create(	struct class *class,struct device *parent,dev_t devt,void *drvdata, const char *fmt, ...)
参数1:struct class *class:class结构体,创建的设备文件的信息内容。通过 class_create()函数创建
参数2:struct device *parent:表示父类对象,一般直接写NULL
参数3:dev_t devt:设备号
参数4:void *drvdata:私有数据,一般填NULL
参数5:const char *fmt, ...:设备文件名
返回值:struct device *---------设备节点对象(设备文件描述)
			成功返回地址,失败返回NULL

        驱动控制硬件,控制外设,其实就是控制地址,通过地址往寄存器写入、读出控制内核驱动是通过虚拟地址操作,则就需要用到另外的函数,地址映射函数  ioremap

void * ioremap(cookie,size);
参数1:cookie:物理地址
参数2:size:映射内容大小,字节
返回值:返回映射成功后的虚拟内存地址,操作虚拟内存地址中的内容就是操作对应的物理地址空间内容

示例就如下:

	//地址映射和硬件初始化
	gpx1con = ioremap(0x11000c20,4);//指针变量就是映射寄存器地址
	*gpx1con = *gpx1con & ~(0xf) | 0x1;
	gpx1dat = ioremap(0x11000c24,4);
	*gpx1dat |= 1;

到这里就已经将上面示例所用到的所有实例已经解释完了,看到这里再去看例程就可以看懂了。

注意:在卸载驱动的函数里,再写的时候要用创建时候的倒序进行释放。如下:

static void __exit led_exit(void)
{
	与初始化逆序过程进行卸载
	//1、映射释放(中断释放)
	iounmap(映射的虚拟内存地址);----释放映射地址
	//2、释放设备文件
	void device_destroy(struct class * class,dev_t devt);
	//3、释放设备文件结构体
	void class_destroy(struct class * cls)
    //4、释放设备号
	void unregister_chrdev(unsigned int major,const char * name)
}

二、文件IO模型

http://t.csdn.cn/e5u8z     这就是如何实现中断的文章,可以去了解哈,因为后面的文件IO模型就是根据中断来进行演示的。

文件io模型一共分为四种

1、阻塞        2、非阻塞        3、IO多路复用        4、异步信号

1、阻塞io模型------休眠等待

阻塞:当进程读取外部资源(数据),但是外部资源没有准备好,进程就进行休眠等待资源可用

在使用这个时候必须要知道那些能阻塞那些不能阻塞不然容易出现问题的。

在应用中:read、wirte、accept、scanf 默认都是阻塞的

那么如何在驱动中实现阻塞:

1.1 创建一个等待队列的头,用于判断是否接收到数据的

wait_queue_head_t head;
init_waitqueue_head(&head);

1.2 在需要等待的位置(没有数据),就阻塞等待

wait_event_interruptible(wq,condition)-----根据参数是否进行阻塞等待,完成阻塞等待
参数1:wq:等待队列头,把当前进程加入到哪个等待队列中
参数2:condition:是否执行阻塞等待的条件
        condition:真---不进行阻塞
        condition:假---进行阻塞

1.3 2、合适位置进行阻塞唤醒

wake_up_interruptible(&head);

实现例程:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/wait.h>
#include <linux/slab.h>
#include <linux/sched.h>

struct key_node
{
	unsigned int major;
	struct class * cls;
	struct device * dev;
	dev_t devno;
	wait_queue_head_t head;
};
char data;//获取硬件数据存储的变量
int condition;
struct key_node key;

ssize_t key_read (struct file * file, char __user * buf, size_t size, loff_t * ops)
{
	wait_event_interruptible(key.head,condition);//阻塞等待
	int ret;//拷贝数据
	printk("key read\n");
	ret = copy_to_user(buf,&data,1);
	condition = 0;//下一次能够阻塞
	return 0;
}
int key_open (struct inode * inode, struct file * file)
{
	printk("key open\n");
	return 0;
}
int key_release (struct inode * inode , struct file * file)
{
	printk("key close\n");
	return 0;
}
irqreturn_t key_irq_handler(int irqno, void * dev)//实现中断处理函数
{
	//获取到数据
	data = 'q';
	condition = 1;
	printk("irqno is %d\n",irqno);
	printk("input char '%c'\n",data);//可以认为是一个字符‘q’,也可以从数据寄存器中获取真实的值
	//唤醒,把进程从等待队列中拿出来继续执行
	wake_up_interruptible(&(key.head));
	return IRQ_HANDLED;
}
const struct file_operations fops = {
	.open = key_open,
	.release = key_release,
	.read = key_read,

};
static int __init key_drv_init(void)
{
	//1、申请设备号
	key.major = 230;
	register_chrdev(key.major,"key drv",&fops);
	//2、创建设备节点
	key.cls = class_create(THIS_MODULE,"cls");
	key.devno = MKDEV(key.major,0);
	key.dev = device_create(key.cls,NULL,key.devno,NULL,"key3");
	//4、硬件初始化
	//a、获取中断号
	struct device_node * node = of_find_node_by_path("/key3_node");
	int irqno = irq_of_parse_and_map(node,0);
	//b、申请中断
	request_irq(irqno,key_irq_handler,IRQF_TRIGGER_FALLING,"key interrupt",NULL);
	//irqreturn_t (*handler)(int, void *)函数指着
	//handler = key_irq_handler
	//创建等待队列头
	init_waitqueue_head(&(key.head));
	return 0;
}
static void __exit key_exit(void)
{}
module_init(key_drv_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");

如果按键没有按下,则一直卡在那个地方。知道按键按下(中断发生)

2、非阻塞--------快速通过

非阻塞:在进行读写操作时,如果没有数据,就立即返回,如果有数据读取数据然后立即返回

和上面所用到的函数都是一样的,只是差距在判断condition是否为真,为真就不阻塞。

3、IO多路复用

4、异步信号

        异步通知(异步信号):当有数据的时候,驱动就会发送信号(SIGIO)给应用,应用就可以异步接收数据,而不用主动接收(可以去完成其他工作)。

        异步信号需要 分为两步,一步是触发这个异步信号,另一个则是收到这个异步信号进行相应的处理操作,一般触发信号是在驱动中去完成什么去触发这个信号,而处理操作则是在应用程序里进行处理。

应用程序的流程(处理信号):

signal(SIGIO,catch_signal);        //1、设置信号处理方式,catch_signal是处理函数

fcntl(fd,F_SETOWN,getpid());     //2、设置当前进程为SIGIO信号的属主进程  
//3、将io模式设置为异步模式        ----异步信号设置
        int flags = fcntl(fd,F_GETFL);
        flags |= FASYNC;//添加异步属性
        fcntl(fd,F_SETFL,flags);

驱动程序的流程(发送信号):

1、需要和应用进程进行关联-------信号要发送给谁
      实现fasync接口,在接口中进行关联
        int key_fasync (int fd , struct file * file, int no)
         {

                 //记录信号发送给哪个应用

                return fasync_helper(fd,file,no,&(key.fasync));       
          }
2、在合适位置发送信号
                kill_fasync(&(key.fasync),SIGIO,POLLIN);

下面看一下修改后的驱动程序是怎么样的

#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/wait.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <asm/poll.h>

struct key_node
{
	unsigned int major;
	struct class * cls;
	struct device * dev;
	dev_t devno;
	wait_queue_head_t head;
	struct fasync_struct * fasync;
};

char data;
int condition = 0;
struct key_node key;

ssize_t key_read (struct file * file, char __user * buf, size_t size, loff_t * ops)
{
	
	if(((file->f_flags & O_NONBLOCK) != 0) && (condition == 0))//设置非阻塞
	{    //不管是阻塞还是非阻塞,只要有数据都要读取数据继续执行
		return -1;
	}
	wait_event_interruptible(key.head,condition);	//阻塞等待
	int ret;    //拷贝数据
	printk("key read\n");
	ret = copy_to_user(buf,&data,1);
	condition = 0;    //下一次能够阻塞
	data = '\0';    //把存储数据的缓冲区清空
	return 1;
}

int key_open (struct inode * inode, struct file * file)
{
	printk("key open\n");
	return 0;
}
int key_release (struct inode * inode , struct file * file)
{
	printk("key close\n");
	return 0;
}

irqreturn_t key_irq_handler(int irqno, void * dev)    //实现中断处理函数
{
	//获取到数据
	data = 'q';
	condition = 1;//变成非阻塞模式
	printk("input char '%c'\n",data);//可以认为是一个字符‘q’,也可以从数据寄存器中获取真实的值
	wake_up_interruptible(&(key.head));    //唤醒,把进程从等待队列中拿出来继续执行
	kill_fasync(&(key.fasync),SIGIO,POLLIN);	//发送信号
	return IRQ_HANDLED;
}

int key_fasync (int fd , struct file * file, int no)
{
	printk("key fasync \n");
	return fasync_helper(fd,file,no,&(key.fasync));//记录信号发送给哪个应用
}

const struct file_operations fops = {
	.open = key_open,
	.release = key_release,
	.read = key_read,
	.fasync = key_fasync,

};

static int __init key_drv_init(void)
{
	//1、申请设备号
	key.major = 230;
	register_chrdev(key.major,"key drv",&fops);
	//2、创建设备节点
	key.cls = class_create(THIS_MODULE,"cls");
	key.devno = MKDEV(key.major,0);
	key.dev = device_create(key.cls,NULL,key.devno,NULL,"key3");
	//4、硬件初始化
	//a、获取中断号
	struct device_node * node = of_find_node_by_path("/key3_node");	//获取设备树中的要使用的硬件节点
	int irqno = irq_of_parse_and_map(node,0);    //获取节点中的中断号
	//b、申请中断
	init_waitqueue_head(&(key.head));    //创建等待队列头
	request_irq(irqno,key_irq_handler,IRQF_TRIGGER_FALLING,"key interrupt",NULL);
	return 0;
}

static void __exit key_exit(void)
{
}

module_init(key_drv_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");

应用程序则是:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>

int fd;

void catch_signal(int signo)
{
	char num;
	if( read(fd,&num,1) < 0)
	{
		printf("no data;");
		return ;
	}
	printf("data is %c\n",num);
}

int main()
{
	fd = open("/dev/key3",O_RDONLY);//阻塞
	if(fd < 0)
	{
		perror("open error");
		return -1;
	}
	signal(SIGIO,catch_signal);	//1、设置信号处理方式
	fcntl(fd,F_SETOWN,getpid());	//2、设置当前进程为SIGIO信号的属主进程
	int flags = fcntl(fd,F_GETFL);	//3、将io模式设置为异步模式
	flags |= FASYNC;//添加异步属性
	fcntl(fd,F_SETFL,flags);
	//完成其他功能操作(没有按下时打印)
	while(1)
	{
		printf("hello world\n");
		sleep(1);
	}
	close(fd);
	return 0;
}

  5、分段处理中断数据(数据量耗时时用)

        中断实际上在处理上,实现的原则是“快进快出”,所以在遇见信息量太大,而无法完成快进快出的时候,则需要将信息进行分段处理。一部分先处理,剩下的在进行处理,也就是让处理器先处理一部分,然后处理器回到中断那里,而剩下的部分在进行处理。

则一般有下面三种方式(其实第二种方式已经包含了第一种方式)

        1、softirq:软中断,处理级别比较高,在内核机制中,需要修改内核源码功能
        2、tasklet:实际上就是内部调用了softirq
        3、workqueue:工作队列

tasklet:    

 初始化任务队列
void tasklet_init(struct tasklet_struct * t,void(* func)(unsigned long),unsigned long data)
参数1:struct tasklet_struct * t :任务队列头节点
参数2:void(* func)(unsigned long):下半部分的实现逻辑
参数3:unsigned long data:参数2函数的参数
在中断上半部分中,启动下半部分(放入内核线程中)
tasklet_schedule(struct tasklet_struct * t)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/wait.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <asm/poll.h>

struct key_drv
{
	unsigned int major;
	struct class * cls;
	struct device * dev;
	struct tasklet_struct t;//任务队列头
	int irqno;
};

struct key_drv key;
irqreturn_t key_irq_handler(int irqno, void * dev)//中断处理--------中断上半部分
{

	printk("%s\n",(char *)dev);
	printk("key data\n");
	tasklet_schedule(&(key.t));
	return IRQ_HANDLED;
}

void key_func(unsigned long data)
{
	printk("data is %d\n",data);
}
const struct file_operations fops;
static int __init key_drv_init(void)
{
	//1、申请设备号
	key.major = 250;
	register_chrdev(key.major,"key int",&fops);
	//2、创建设备节点
	key.cls = class_create(THIS_MODULE,"cls");
	key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key");
	//获取到设备号
	//获取设备树中的要使用的硬件节点
	struct device_node * node = of_find_node_by_path("/key3_node");
	//获取节点中的中断号
	key.irqno = irq_of_parse_and_map(node,0);
	//实现添加中断下半部分-------初始化任务队列
	tasklet_init(&(key.t),key_func,45);
	request_irq(key.irqno,key_irq_handler,IRQF_TRIGGER_FALLING,"key interrupt","key down");	//申请中断
	return 0;
}

static void __exit key_drv_exit(void)
{
	free_irq(key.irqno,"key down");    //中断释放
	device_destroy(key.cls,MKDEV(key.major,0));    //释放设备节点
	class_destroy(key.cls);    //释放结构体
	unregister_chrdev(key.major,"key int");    //释放设备号
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");

workqueue:

三、系统总线

四、平台总线

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

裸机开发之驱动开发 的相关文章

  • 蓝桥杯STM32F103RB数码管计时(秒表)

    STM32F103RB数码管定时 xff08 秒表 xff09 硬件单路 96 配置TIM2及其中断代码片如下 示例 96 96 中断执行函数代码片如下 示例 96 96 seg c 数码管 代码片如下 示例 96 完整工程下载 gt gt
  • 第七届蓝桥杯嵌入式(省赛)程序题

    第七届蓝桥杯 xff08 省赛 xff09 解读 43 程序 解读 xff1a 这里自己多读几遍设计任务以及要求再看下面 96 A 先搭总体框架 96 各初始化函数 96 LCD初始化 96 96 按键初始化 96 96 ADC初始化 96
  • STM32普通io口模拟pwm输出的三种方法

    STM32F103RB普通io口模拟pwm输出的第三种方法 xff08 周期占空比可调 xff09 第 xff08 一 xff09 种定时器中断产生pwm 96 第 xff08 一 xff09 种代码片 96 第 xff08 二 xff09
  • STM32 RS232通信实验

    stm32F103 RS232通信实验 什么是RS232 软件设计 完整工程下载 什么是RS232 先来看看UART传输所存在的问题 于是就有了RS232协议 这里注意使用的是负逻辑电平信号 在规定范围内的电平信号代表逻辑1或0 xff0c
  • MDK中变量无法添加到逻辑分析仪中原因

    MDK中变量无法添加到逻辑分析仪中原因 解决方法 去掉static 提示无法将变量添加到逻辑分析仪中 解决方法 去掉static 设置为bit 全速运行
  • 三,FreeRTOS之——动态创建多任务+优先级

    声明 xff1a 本专栏参考韦东山 xff0c 野火 xff0c 正点原子以及其他博主的FreeRTOS教程 xff0c 如若侵权请告知 xff0c 马上删帖致歉 xff0c 个人总结 xff0c 如有不对 xff0c 欢迎指正 动态创建多
  • ESP8266组网+STM32数据传输项目

    ESP8266 43 STM32数据传输项目 实验硬件 xff1a 项目关键词 xff1a 项目描述项目涉及知识 xff1a 1 ESP8266开发2 MQTT协议3 STM32 整体开发流程 xff1a 实验硬件 xff1a ESP826
  • 十一,FreeRTOS之——互斥信号量(优先级反转,优先级继承,递归锁)

    声明 xff1a 本专栏参考韦东山 xff0c 野火 xff0c 正点原子以及其他博主的FreeRTOS教程 xff0c 如若侵权请告知 xff0c 马上删帖致歉 xff0c 个人总结 xff0c 如有不对 xff0c 欢迎指正 互斥量理论
  • (一)裸机开发框架构建之---开发框架思想

    裸机开发框架构建 声明 xff1a 本专栏通过查阅资料以及自己对开发框架的理解所编写 xff0c 如有错误 xff0c 还请指正 为什么要使用框架 xff1f xff1f xff1f 我的框架分层思想体现 声明 xff1a 本专栏通过查阅资
  • PX4添加外置IMU传感器MPU-9250

    使用PX4 v1 13 2代码 xff0c 淘宝购买的MPU 9250传感器 MPU 9250 芯片架构图 实物图 手册 寄存器 https invensense tdk com wp content uploads 2015 02 RM
  • (二)裸机开发框架构建之---点灯大师

    裸机开发框架构建 3 设备管理层抽象出结构体初始化结构体第一种初始化方法 xff08 c89标准 xff09 第二种初始化方法 xff08 C99标准 xff09 2 硬件接口层1 硬件层硬件LED层初始化函数硬件层LED控制函数 4 应用
  • 1.freertos应用系列之cubemx创建freertos

    freertos应用全系列 xff08 写完关联更新 xff09 01 freertos应用系列之cubemx创建freertos 11 freertos应用系列之cubemx创建freertos 02 freertos应用系列之cubem
  • docker中镜像源推荐

    1 xff0c 个人建议使用 网易镜像源 镜像源有以下5种 1 网易 http hub mirror c 163 com 2 Docker中国区官方镜像 https registry docker cn com 3 ustc https d
  • VScode创建C++项目

    VScode创建C 43 43 项目 假设系统已经安装了MinGW64 插件 常用插件 创建Project 配置json文件 需要修改的地方都在下方注释说明 根据MinGW64安装位置进行修改 c cpp properties json s
  • C++的一个问题点,数组作为参数传递到函数之后,不能直接求出长度

    YU 原数组 xff0c 传递参数之后 结果是作为参数传进去之后是作为指针 xff0c 是不能求出长度的 xff0c 所以需要把长度提前求出作为参数传入该函数 反思 xff1a 最近C 43 43 Python xff0c java轮流用
  • 基于from flask import Flask,render_template 上传网页遇到的问题

    我们要上传多个页面形成一个网站 xff0c 首先我们需要在index xff08 一般这个都是首页面 xff09 查看其源码 找到类似 这段代码里面包括了前面的网站 xff0c 所以这时候我们只需要把它变成带使用的状态 xff0c 操作就是
  • 跨交换机的VLAN设置

    实现目标 xff1a 进行多台主机多个vlan接口进行互相通信 需要知识 xff1a 1 不同的vlan接口的是不能进行通信的 2 在要跨越多个交换机进行通信的时候要对进行交互的交换机进行共享vlan端口的设置 3 在设置网络号的时候应该注
  • Wireshark抓取cookie:用户名...,TCP报文等信息实战

    这里我们要先安装Wireshark xff0c 这里要注意的是一些低级版本刚刚下下来的时候是找不到网络接口的 xff0c 所以这时候要更新 xff0c 然后再下应该WinPro xff08 应该是这个 xff09 xff0c 之后就有网络接
  • 计算机网络知识点总结提纲(谢希仁)

    1 IOS OSI对王道书上的缩减总结 清晰pdf xff1a 链接 xff1a https pan baidu com s 1f6DqMsHky4kP8i9WQLvCew pwd 61 the3 提取码 xff1a the3 来自百度网盘

随机推荐

  • C++getline和 cin的探讨

    从结果可以看出 xff0c cin是会把空格部分舍弃的 如果是输入一个 然后空格在输入其他的 xff0c 因为cin默认把空格去调 xff0c 则后面的字符我的理解就是溢出 xff1f 所以报错了 getline功能就比较强大了 xff0c
  • Pixhawk RPi CM4 Baseboard 树莓派CM4安装Ubuntu20.04 server 配置ros mavros mavsdk

    文章目录 硬件安装Ubuntu Server20 04下载rpiboot工具下载imager刷写系统配置USB配置WIFI 开机安装桌面配置wifi配置串口安装ROS安装mavros安装MAVSDK PythonInternet设置最后 参
  • docker迁移镜像

    docker迁移本地镜像 本文为docker基本镜像操作之一 查看本镜像 docker images 迁移 xff08 拷贝 xff09 本地镜像到其他设备 1 打包 docker save o 路径 目标包名 tar 源镜像名 标签 2
  • C++Linux服务器学习之路——1

    前言 xff1a 为了让所学的计网知识融合于实际 xff0c 让操作系统里的理论去满足工程需求 xff0c 故通过借鉴30dayMakeServer的路线以及进行相应知识点的学习 part1 首先我们要理解socket 为应用层和传输层提供
  • 计网牛客刷图总结

    久不学忘记了 xff0c 1111 1111 61 255 xff0c ip地址是32位二进制组成 xff0c x 26就是说主机号有26位 xff0c 其他都是网络号 所以后面只有2位主机号 xff0c 234 61 11101010 x
  • C++力扣算法刷题算法分析

    span class token macro property span class token directive hash span span class token directive keyword include span spa
  • Invalid bound statement (not found)问题解决

    在网上基本的解决方案就是查看 namespace有没有对应 xff0c 但是我确定我的路径都是正确的 xff0c 如果发现这类问题可以先尝试确定路径的正确 之后如果还不行 xff0c 我们进行解决 xff1a 首先在target文件中查找是
  • C指针基础普及

    https www programiz com c programming c pointers 先放网站 xff0c 等我有时间再来补我的扩展
  • Vscode+Cmake配置并运行opencv环境(Windows和Ubuntu大同小异)

    之前在培训新生的时候 xff0c windows环境下配置opencv环境一直教的都是网上主流的vs studio配置属性表 xff0c 但是这个似乎对新生来说难度略高 虽然个人觉得完全是他们自己的问题 xff0c 加之暑假之后对cmake
  • Spring Aop的使用(含示例)

    介绍 在软件业 xff0c AOP为Aspect Oriented Programming的缩写 xff0c 意为 xff1a 面向切面编程 xff0c 通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术 AOP是OOP的延续
  • 超好用的开发工具-VScode插件EIDE

    EIDE介绍 一款适用于8051 STM8 Cortex M RSCv的单片机开发环境 在 vscode上提供8051 xff0c STM8 Cortex M xff0c RISC V 项目的开发编译烧录等功能 使用文档 xff1a 简介
  • 直流编码电机双闭环(速度+角度)控制

    目录 1 PID框图 2 pid控制器的表达式 3 传感器数据获取 4 硬件设计 5 工程配置 6 软件部分程序配置 7 调参过程记录 本文已更新 xff0c 加上曲线调试 xff0c 更好效果 xff0c 更多内容 xff0c 详情 xf
  • OPENMV配置记录(一)

    文章目录 1 刷写固件2 开始配置openmv3 图像获取与显示4 修改图像 xff0c 获取像素 xff0c 添加元素5 使用图像进行基本操作 颜色追踪6 xff0c 识别码7 模版匹配8 通过比例的方法来求解距离9 组合使用 正好回家带
  • 为什么你的软件编译时没问题,运行时却出错?—— Java 中的异常再复盘

    从开发工具谈起 xff1a 这是我平常用的几个编辑器 记得我刚开始学 C 语言 xff0c 学 Java 的时候 xff0c 还是用 Notepad 43 43 这种文本编辑器写代码 xff0c 老师说是为了打基础 xff0c 加深记忆 后
  • 使用stm32解析富斯i6接收机(IBUS)

    文章目录 1 通信协议解析说明2 驱动程序设计3 实测4 使用串口空闲中断 43 DMA接收5 源码 1 通信协议解析说明 常见的官方遥控器大概如下所示 xff1a 常用的搭配接收机 xff1a 这里需要注意的是 xff1a i6是可以刷十
  • 编码电机PID调试(速度环|位置环|跟随)

    文章目录 1 编码电机认识2 上位机波形显示1 功能介绍2 协议说明 3 速度环调试验证4 位置环调试验证5 实现跟随效果 前面的文章中有讲过编码电机串级PID相关的知识 xff0c 以及一些PID的调试经验 xff0c 这里我最近正好又把
  • 树莓派安装ubuntu mate记录

    文章目录 1 系统下载1 ubuntu下载2 ubuntu mate下载 2 系统安装3 系统使用1 ubuntu系统2 ubuntu mate系统 这个算个失败的记录贴吧 xff0c 这个系统安装过程不太流畅 xff0c 使用起来也有很多
  • 平衡小车的一些常见问题总结

    文章目录 1 基本理论2 直立环速度环串级pid3 代码差异的解释4 转向环 1 基本理论 PID控制 pid控制值对偏差进行比例 xff0c 积分和微分的控制 xff0c 分别是三个部分 xff0c 对应为比例单元 xff0c 积分单元和
  • Ubuntu下tar命令使用详解 .tar解压、.tar压缩

    1 tar参数选项2 tar压缩命令3 tar解压缩命令4 解压安装5 tar bz2解压缩命令6 Linux压缩和解压 bz2文件 bzip2 Linux tar 命令 在Linux平台 xff0c tar是主要的打包工具 tar命令通常
  • 裸机开发之驱动开发

    一 驱动开发的基础理解 在计算中 xff0c 设备驱动程序是一种计算机程序 xff0c 用于操作或控制连接到计算机的特定类型的设备 驱动程序提供了与硬件设备的软件接口 xff0c 使操作系统和其他计算机程序可以访问硬件功能 xff0c 而无