物联网5(驱动开发)

1.驱动

        在《嵌入式之嵌入式系统》文章中介绍到驱动用于控制硬件,为操作系统或应用程序提供控制硬件的接口。

2.用户态和内核态

        用户态和内核态是操作系统的两种运行级别,它们的主要区别在于权限、系统调用、CPU指令、中断处理、内存访问和运行环境等方面。

用户态 内核态

用户态是最低权限的运行状态。

不能直接访问操作系统内核数据结构和程序。

访问的内存空间和对象受到限制。

占用的CPU可被其他程序抢占。

内核态是最高权限的运行状态。

可以访问系统的所有资源。

可以使CPU执行所有的指令。

可以访问所有的内存地址。

3.驱动程序和应用程序

应用程序 驱动程序

属于用户态。

通过操作系统或驱动操作硬件。

由用户态函数(如系统调用和库函数)实现:

  • 文件操作函数:如打开文件、读取文件内容、写入文件、关闭文件等;
  • 网络通信函数:如创建套接字、绑定地址、监听连接、发送和接收数据等;
  • 内存管理函数:如分配内存、释放内存、拷贝数据等;
  • 用户界面函数:如创建窗口、绘制图形、处理用户输入等。

属于内核态。

直接操作硬件。

由内核态函数实现:

  • 设备初始化函数:用于初始化硬件设备,配置其参数,并准备其进行通信;
  • 数据传输函数:用于从设备读取数据或将数据写入设;
  • 中断处理函数:用于处理设备产生的中断,执行相应的中断服务程序;
  • 错误处理函数:用于检测和处理设备通信中可能发生的错。

4.驱动类型

        市面上有很多设备,每种设备使用的驱动各不相同,但驱动大致可以分为三类:字符设备驱动、块设备驱动和网络设备驱动。

4.1.字符设备驱动

        字符设备,如鼠标、音箱、打印机等,是指以字节(即8个比特位)为单位进行传输的设备,操作字符设备的驱动被称为字符设备驱动。

4.2.块设备驱动

        块设备与字符设备正好相反,块设备是以块为单位进行传输的设备,常指存储器设备,如机械硬盘、固态硬盘、eMMC、NAND、SD卡、U盘等,操作块设备的驱动被称为块设备驱动。

        块设备在进行数据传输时,会将数据分割成固定大小的块,然后按照块的顺序进行传输。

4.3.网络设备驱动

        网络设备是将服务器、PC、手机等终端设备相互链接的设备,如网卡、路由器、交换机等,操作网络设备的驱动被称为块设备驱动。

        网络设备驱动负责网络数据的传输和接受。

        本文章不对网络设备驱动讲解。

5.设备文件

        设备文件用于表示系统中的设备,也可以理解为设备被抽象为文件,这使得设备操作与文件操作相似,以便通过操作文件来操作设备。设备文件通常存在于/dev目录下。

        设备文件分为字符设备文件和块设备文件,分别用于表示字符设备和块设备,网络设备没有设备文件。

        普通文件有文件类型、权限、硬连接数量、所有者、所属组、文件大小、更新时间、文件名这几个属性。设备文件的属性与普通文件的属性大致相同,但设备文件没有文件大小属性,而有设备号。字符设备文件和块设备文件的文件类型分别由c和b字母表示。

下图为设备文件属性

        设备号分为主设备号和从设备号。主设备号用于标识一个驱动程序,可取范围为1至254。从设备号用于标识使用某一驱动程序的各个具体设备,可取范围为0至255。如下图,8是主设备号,用于标识驱动程序;0、1、2、5是从设备号,用于标识使用这一驱动程序的各个储存分区。

其他说明

  • 字符设备文件的主设备号和块设备文件的主设备号可重复使用;
  • 不同主设备号下的从设备号可重复使用;
  • 通过cat /proc/devices命令可查看主设备号的使用情况。

6.可加载内核模块

        向内核添加驱动有两种方法。一种方式是在编译内核前,先将驱动程序添加到源码中,然后再编译内核源码并烧写,此种方法实现步骤请查看《物联网之嵌入式系统启动过程》文章的“添加Linux内核功能”章节,此种方法不利于调试。另一种是采用可加载内核模块方法,可加载内核模块,简称LKM,是Linux内核向外部提供的一个接口,使得外部程序能够在系统运行时动态地加载和卸载内核功能单元。这些模块具有独立的功能,可以被单独编译,在系统运行时它们被链接到内核,作为内核的一部分在内核空间运行。

6.1.可加载内核模块的实现

第1步:编写模块代码

  • 第①步:创建C源文件,如module.c
  • 第②步:在文件中添加<linux/module.h>和<linux/kernel.h>头文件
  • 第③步:定义模块的初始化函数,代码如下:
static int __init xxx_init(void)
{
    // 初始化代码

    return 0;  // 返回0表示初始化成功
}

// 说明:
// __init:是一个宏。用于告诉编译器此函数只在模块的加载期间使用
// xxx_init:初始化函数名称。初始化函数名称常以__init结尾
  • 第④步:定义模块的清理函数,代码如下:
static int __exit xxx_exit(void)
{
    // 清理代码
}

// 说明:
// __exit:是一个宏。用于告诉编译器此函数只在模块的卸载期间使用
// xxx_ exit:清理函数名称。清理函数名称常以__exit结尾
  • 第⑤步:注册模块的初始化函数和清理函数,代码如下:
module_init(xxx_init)   // module_init是一个宏,用来告诉内核哪个函数作为模块的初始化函数

module_exit(xxx_exit)  // module_exit是一个宏,用来告诉内核哪个函数作为模块的清理函数
  • 第⑥步:提供有关模块的额外信息,代码如下:
MODULE_LICENSE(module_license)          // 指定模块的许可证类型
MODULE_AUTHOR(author_name)              // 指定模块的作者
MODULE_DESCRIPTION(module_description)  // 指定模块的描述信息

第2步:编译模块

  • 第①步:创建一个名为Makefile的文件,用于编译模块,代码如下:
# obj-m变量用于指定需要编译为模块的目标文件
# <target_file_name>同C源文件的名称
obj-m := <target_file_name>.o

# KERNELDIR指向Linux内核源码的目录。
# 内核源码中包含了编译内核模块所需的脚本、头文件和Makefile
KERNELDIR = <kernel_directory>

# PWD指向内核模块所在目录。
# 用于告诉内核源代码目录中的Makefile,内核模块的源码在哪个目录下
PWD = $(shell pwd)

default:
# 编译内核模块
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
# 删除中间文件
rm -rf *.order *.mod.* *.o *.symvers

clean:
# 删除<target_file_name>.ko文件
rm -rf *.ko
  • 第②步:执行make命令编译模块,编译成功后会产生一个<arget_file_name>.ko文件

第3步:在开发板上加载和卸载内核模块

  • 执行insmod <arget_file_name>.ko命令加载模块。
  • 执行rmmod <arget_file_name>命令卸载模块。
  • 执行modinfo <arget_file_name>.ko命令查看模块的详细信息。
  • 执行lsmod命令查看系统中所有已加载的模块。

6.2.可加载内核模块的示例

第1步:编写模块代码

  • 第①步:创建module.c文件
  • 第②③④⑤⑥步:向module.c文件添加如下代码
#include <linux/module.h>
#include <linux/kernel.h>

static int __init demo_init(void)
{
    printk(KERN_INFO "模块被加载进内核\n");

    return 0;
}  

static void __exit demo_exit(void)
{
    printk(KERN_INFO "模块从内核中卸载\n");
}  

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bob");
MODULE_DESCRIPTION("Demo description");

第2步:编译模块

  • 第①步:创建名为Makefile的文件,代码如下:
obj-m := module.o
KERNELDIR = /home/lg/share/kernel
PWD = $(shell pwd)

default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
rm -rf *.order *.mod.* *.o *.symvers

clean:
rm -rf *.ko
  • 第②步:执行make命令编译模块,编译成功后会产生一个module.ko文件,如下图:

第3步:在开发板上加载和卸载内核模块

  • 执行insmod module.ko命令加载模块

  • 执行rmmod module命令卸载模块

7.字符设备驱动开发

        虽然字符设备驱动为应用程序提供操作硬件的接口,但应用程序不能直接使用字符设备驱动提供的接口,这是因为应用程序工作在用户态,而驱动程序工作在内核态。应用程序通过系统调用调用驱动程序,系统调用通过file_operations结构体与驱动程序关联起来。

        file_operations结构体里面包含一组函数指针,是对系统调用的统一管理,并指向驱动程序里的函数,以使系统调用与驱动程序相互关联起来。file_operations结构体的成员后续介绍。

7.1.字符设备驱动开发过程

7.1.1.过程

第1步:编写字符设备驱动代码

  • 第①步:创建C源文件,如driver.c
  • 第②步:根据file_operations结构体的成员编写驱动函数
  • 第③步:file_operations结构体的相应成员指向驱动函数
  • 第④步:通过register_chrdev()函数注册字符设备驱动
  • 第⑤步:通过unregister_chrdev()函数注销字符设备驱动

第2步:执行make命令编译字符设备驱动

第3步:在开发板执行如下命令创建设备文件

mknod device_name {b | c} major minor

// 作用:用于创建字符设备文件和块设备文件
// device_name:设备文件名称
// b:表示块设备文件
// c:表示字符设备文件
// major:主设备号
// minor:从设备号

第4步:测试

7.1.2.示例

第1步:编写字符设备驱动代码

  • 第①步:创建char_driver.c文件
  • 第②③④⑤步:根据file_operations结构体的成员编写驱动函数
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>

// 编写打开文本文件的驱动函数
static int demo_open(struct inode *pinode, struct file *pfile)
{
    printk(KERN_WARNING "我是驱动: 文本文件已打开\n");
    return 0;
}

// 编写读取文本文件的驱动函数
static ssize_t demo_read(struct file *pfile, char __user *pbuf, size_t count, loff_t *off)
{
    int ret;
    char data[] = "我是内核!";
    int len = min(count, sizeof(data));
    ret = copy_to_user(pbuf, data, len);

    return len;
}

// 编写写入文本文件的驱动函数
static ssize_t demo_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *off)
{
    int ret;
    char data[100] = "";
    int len = min(count, sizeof(data));
    ret = copy_from_user(data, pbuf, len);
    printk(KERN_WARNING "我是驱动: 收到应用程序的信息是“%s”\n", data);

    return count;
}

// 编写关闭文本文件的驱动函数
static int demo_release(struct inode *pinode, struct file *pfile)
{
    printk(KERN_WARNING "我是驱动: 文本文件已关闭\n");
    return 0;
}

// 通过file_operations结构体指向驱动函数
static struct file_operations fops = {
    // 表示当前正在被编译的内核模块拥有fops结构体成员所指驱动函数的操作权限,THIS_MODULE表示当前正在被编译的内核模块。
    .owner = THIS_MODULE,  
    .open = demo_open,
    .read = demo_read,
    .write = demo_write,
    .release = demo_release,
};

static int __init demo_init(void)
{
    // 注册字符设备驱动
    register_chrdev(240,"char_driver",&fops);
    printk(KERN_INFO "我是驱动: 模块加载成功-驱动注册成功\n");

    return 0;
}

static void __exit demo_exit(void)
{
    // 注销字符设备驱动
    unregister_chrdev(240,"char_driver");
    printk(KERN_INFO "我是驱动: 驱动注销成功-模块卸载成功\n");
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bob");
MODULE_DESCRIPTION("Demo description");

第2步:执行make命令编译字符设备驱动

第3步:在开发板执行如下命令创建设备文件

mknod /dev/char_driver c 240 88

第4步:测试

  • 第①步:创建名为read_app.c的应用程序,代码如下:
#include <stdio.h>
  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值