树莓派(五)驱动开发

一、初识驱动

1.简介

Linux下的设备通常可分为三类:

  1. 字符设备
  2. 块设备
  3. 网络设备

常见字符设备有鼠标、键盘、串口、控制台等。

常见的块设备有各种硬盘、flash磁盘、RAM磁盘等。

在Linux里一个网络设备也可以叫做一个网络接口,如eth0,应用程序是通过Socket而不是设备节点来访问网络设备,在系统里根本就不存在网络设备节点。网络接口没有像字符设备和块设备一样的设备号,只有一个唯一的名字,如eth0、eth1等,而这个名字也不需要与设备文件节点对应。

在Linux中,所有设备都以文件的形式存放在/dev目录下,都是通过文件的方式进行访问,设备节点是Linux内核对设备的抽象,一个设备节点就是一个文件。应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序,而驱动程序负责将这些标准调用映射到实际硬件。

2.涉及到的概念

设备节点:Linux下被创建在/dev目录,是连接内核与用户层的枢纽,就是设备是接到对应哪种接口的哪个ID 上。 相当于硬盘的inode一样的东西,记录了硬件设备的位置和信息。

设备号包含主设备好和次设备号。

主设备号:驱动程序在初始化时,会注册它的驱动及对应主设备号到系统中,这样当应用程序访问设备节点时,系统就知道它所访问的驱动程序了。你可以通过/proc/devices文件来查看系统设备的主设备号。

次设备号:驱动程序遍历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。这样当应用程序访问设备节点时驱动程序就可以根据次设备号知道它说访问的设备了。

驱动程序:设备驱动程序(device driver),简称驱动程序(driver),是一个允许高级(High level)计算机软件(computer software)与硬件(hardware)交互的程序,这种程序建立了一个硬件与硬件,或硬件与软件沟通的界面,经由主板上的总线(bus)或其它沟通子系统(subsystem)与硬件形成连接的机制,这样的机制使得硬件设备(device)上的数据交换成为可能。想象平时我们说的写驱动,例如点led灯的驱动,就是简单的io操作。

系统调用:应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,必须使用一个叫做 "系统调用" 的方法来实现,因为用户空间不能直接对内核进行操作,而 "系统调用" 相当于是用户程序和内核之间的接口,通过这个接口,用户程序就能安全而又受控地访问底层操作系统,从而实现对底层驱动的操作。open、close、write 和 read 等这些函数是有 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。

3.彼此的关联

应用程序是运行在用户空间的,而驱动位于内核的,设备又是具体的硬件层面,三者是如何联系在一起的呢?

用户程序调用C库open函数,会触发软中断,然后通过系统调用进入内核态,系统调用中也有与C库open函数对应的sys_open函数,然后sys_open通过设备名称访问到设备节点,而创建设备节点时是需要指定主设备号和次设备号的,那么顺着设备号就可以找到驱动程序和相应设备,驱动程序中也有属于自己的drv_open等函数,最后通过这些函数来指挥对应设备硬件。

二、基于内核框架编写驱动代码

驱动开发大致可分为两步:  

  1. 添加驱动:编写驱动程序,然后加载到内核
  2. 调用驱动:用户空间open后,调用驱动程序

编写驱动程序时,要准备好设备名称、设备号、设备驱动函数(通过操作寄存器去操作IO口) 

编写驱动程序的流程:

  1. 确定主设备号,也可以让内核分配
  2. 定义自己的file_operations结构体
  3. 实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体
  4. 把file_operations结构体告诉内核:register_chrdev
  5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
  6. 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
  7. 其他完善:提供设备信息,自动创建设备节点:class_create, device_create

register_chrdev()

register_chrdev函数是Linux设备驱动程序中非常重要的函数,它用于动态地分配主设备号和次设备号,并将主设备号和设备驱动程序所用的file_operations结构体绑定在一起。

函数原型:

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
  • major表示主设备号,如果它被设置为0,则表示系统自动分配主设备号
  • name表示设备名称
  • fops为设备驱动程序的操作函数指针,它包含了所有与设备相关的操作函数

返回值:

当register_chrdev函数成功被调用时,它会返回分配的主设备号,否则会返回一个负数,表示分配失败。当主设备号被成功分配后,你就可以在file_operations结构体中使用这个主设备号了。

以下是驱动程序:

#include <linux/fs.h>		 //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>	 //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件
 
static struct class *pin4_class;  
static struct device *pin4_class_dev;
 
static dev_t devno;                //设备号,包括主设备号和次设备号
static int major =231;  		   //主设备号
static int minor =0;			   //次设备号
static char *module_name="pin4";   //模块名,或者设备节点名称,位于/dev目录下
 
//pin4_read函数
//static int pin4_read(struct file *file,char __user *buf,size_t count, loff_t *ppos)
//{
//    printk("pin4_write\n");
//    return 0;
//}
 
//pin4_open函数,实现对应的open函数,然后填入file_operations结构体
static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");  //内核的打印函数,和printf类似
   
    return 0;
}
 
//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
	printk("pin4_write\n");
    return 0;
}

//定义自己的file_operations结构体,将之前实现的open、write等函数填入
static struct file_operations pin4_fops = {
 
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};
 
//初始化驱动函数
int __init pin4_drv_init(void)   //驱动的真正入口
{
 
    int ret;
    devno = MKDEV(major,minor);  //创建设备号

    //把file_operations结构体告诉内核,注册驱动程序
    ret   = register_chrdev(major, module_name,&pin4_fops); 
 
    //由代码通过以下两步,在/dev下自动生成设备文件
    //调用class_create先在/sys/class下创建一个类,类名为myfirstdemo
    pin4_class=class_create(THIS_MODULE,"myfirstdemo");
    //再调用device_create自动创建设备文件,设备名为module_name
    pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);  //创建设备文件
 
 
    return 0;
}
 
void __exit pin4_drv_exit(void)
{
 
    device_destroy(pin4_class,devno);       //卸载设备
    class_destroy(pin4_class);              //删除类
    unregister_chrdev(major, module_name);  //卸载驱动
 
}
 
module_init(pin4_drv_init);  //入口:内核加载驱动的时候,这个宏会被调用,而真正的驱动入口是它调用的函数
module_exit(pin4_drv_exit);  //出口:卸载驱动程序时,出口函数调用unregister_chrdev
MODULE_LICENSE("GPL v2");

代码补充:

static的作用

内核代码数量庞大,为了防止与其他的文件有变量命名冲突,static限定变量的作用域仅仅只在这个文件。

结构体成员变量的单独赋值

static struct file_operations pin4_fops = {
 
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};

 这是内核代码中常见的对结构体的操作方式,单独给指定结构体元素赋值,其他不管。

 注意:在keil的编译工具环境中不允许这样写,linux可以。

结构体file_operations 

在SourceInsight中查看结构体file_operations,可以发现很多的函数指针,这些函数名跟系统上层文件的操作差不多。

如果上层想要实现read,就复制过来,按照格式改一改。

上层对应底层,上层想要用read,底层就要有read的支持。

三、交叉编译驱动程序

打开虚拟机,进入Linux源码目录下字符设备驱动目录/driver/char。将上述驱动框架复制到这里,创建名为pin4driver.c。

修改Makefile文件

运行以下指令,进行配置,使得工程编译时可以编译这个文件。

vi Makefile

模仿这些文件的编译方式,以编译成模块的方式编译pin4driver.c。

将以下配置加入Makefile。

obj-m     += pin4driver.o

 进行模块编译

之前编译内核的时候用的是这个命令。

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs

 现在只需进行模块编译,不需要生成zImage,dtbs文件。

所以,回到源码目录执行以下指令。

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 modules

执行完编译命令后,回到/driver/char目录,会发现多了一些文件如下图。 

 把 .ko 文件发送到树莓派

运行以下指令发送 .ko 文件。

scp pin4driver.ko pi@192.168.10.112:/home/pi

再写一个应用程序 pin4test.c 来测试驱动。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
 
int main()
{
	int fd;
	fd = open("/dev/pin4",O_RDWR);
	if(fd < 0){
		printf("open failed\n");
		perror("reson");
	}else{
		printf("open success\n");
	}
	fd = write(fd,'1',1);//写一个字符'1',写一个字节
	return 0;
}

 使用交叉编译工具进行编译。

arm-linux-gnueabihf-gcc pin4test.c -o pin4test

 将生成文件发送到树莓派。

scp pin4test pi@192.168.10.112:/home/pi

 四、树莓派装载驱动

运行以下命令进行装载。

sudo insmod pin4driver.ko

运行这个指令,就是在底层运行初始化驱动函数。

可以通过 lsmod 指令来查看内核挂载的驱动。

或者直接到 /dev 目录下查看。

ls /dev

 如果要卸载驱动就运行以下指令。

sudo rmmod pin4driver

运行应用程序 pin4test 

sudo ./pin4test

检查是否成功:demsg指令可以查看内核打印信息

用dmesg命令显示内核缓冲区信息,并通过管道筛选与pin4相关信息。

dmesg | grep pin4

可以看到这两个打印信息,说明内核的printk已经被成功调用,我们已经成功完成了上层对内核的调用 !  

 五、参考博文

01linux驱动学习--基本驱动框架 - 码农教程 (manongjc.com)

Linux驱动device_create创建字符设备文件-CSDN博客

class_create()函数应用_class_create函数-CSDN博客

register_chrdev函数详解_笔记大全_设计学院 (python100.com)

嵌入式Linux驱动复习(1)hello驱动_can't open file /dev/hello-CSDN博客

【Linux】基于框架编写驱动代码、驱动代码编译和测试-CSDN博客

树莓派(十一)树莓派驱动开发入门:从读懂框架到自己写驱动(上)-CSDN博客

嵌入式Linux驱动复习(3)设备节点、驱动、最简单的LED驱动编写_.write = led_write什么意思-CSDN博客 

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值