良许Linux教程网 干货合集 Linux设备驱动开发入门指南

Linux设备驱动开发入门指南

Linux设备驱动是Linux内核的重要组成部分,它负责与硬件设备进行交互,为用户态提供统一的接口。本文将介绍Linux设备驱动的基础知识,包括内核模块、设备树、总线、设备与驱动等概念,以及如何编写字符设备驱动、I2C驱动、SPI驱动、GPIO驱动等常见类型的驱动程序。

设备驱动充当了硬件和应用软件之间的纽带,它使得应用软件只需要调用系统软件的应用编程接口(API)就可让硬件去完成要求的工作。本文主要讲解了Linux设备驱动与硬件的关系,Linux设备驱动的开发模式以及内核中相关的重要基础数据结构。


设备驱动与硬件的关系

对设备驱动最通俗的解释就是“驱使硬件设备行动”。驱动与底层硬件直接打交道,按照硬件设备的具体工作方式,读写设备的寄存器,完成设备的轮询、中断处理、 DMA 通信,进行物理内存向虚拟内存的映射等,最终让通信设备能收发数据,让显示设备能显示文字和画面,让存储设备能记录文件和数据。

设备分类

Linux对将外设分为3类:

  • 字符设备

    字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、磁带驱动器、鼠标等。
    
  • 块设备

    块设备可以用任意顺序进行访问,以块为单位进行操作,如硬盘、软驱等。
    
  • 网络设备

    网络设备面向数据包的接收和发送而设计,它并不对应于文件系统的节点。
    

总体框图

Overview
Overview

机制与策略

机制强调“提供什么能力”,而策略旨在“如何使用这些能力”。因此驱动开发需要遵守的是驱动程序的角色是提供机制, 而不是策略。
编写内核代码来存取硬件, 但是不能强加特别的策略给用户, 因为不同的用户有不同的需求。驱动应当做到使硬件可用, 将所有关于如何使用硬件的事情留给应用程序。


内核模块

Linux 提供了一种代码动态地加载到内核中机制,这种机制被称为模块(Module)。具有如下特点:

  • 模块本身不被编译入内核映像, 从而控制了内核的大小。
  • 模块一旦被加载,它就和内核中的其他部分完全一样。
  • 可动态加载与移除,不需重启系统,节约开发时间。
  • 模块放在用户空间,这部分代码可以不开源。

因此驱动多数情况以内核模块的形式加载到内核。

组成

一个 Linux 内核模块主要由如下几个部分组成:

  1. 模块加载函数(一般需要)

    当通过 insmod 或 modprobe 命令加载内核模块时,模块的加载函数会自动被内核执行,完
    
    成本模块的相关初始化工作。
    static int __init initialization_function(void)
    {
        /* 初始化代码 */
    }
    module_init(initialization_function);
    
  2. 模块卸载函数(一般需要)

    当通过 rmmod 命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数
    
    相反的功能。
    static void __exit cleanup_function(void)
    {
        /* 释放代码 */
    }
    module_exit(cleanup_function);
    
  3. 模块许可证声明(必须)

    许可证( LICENSE)声明描述内核模块的许可权限,如果不声明 LICENSE,模块被加载时,
    
    将收到内核被污染 ( kernel tainted)的警告。在 Linux 2.6 内核中,可接受
    
    的 LICENSE 包括“ GPL”、“ GPL v2”、“ GPL and additional rights”、
    
    “ Dual BSD/GPL”、“ Dual MPL/GPL” 和“ Proprietary”。
    
  4. 模块参数(可选)

    模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。
    
    module_param(参数名,参数类型,参数读/写权限);
    module_param_array(数组名,数组类型,数组长,参数读/写权限);
    
  5. 模块导出符号(可选)

    内核模块可以导出符号( symbol,对应于函数或变量),这样其他模块可以使用本模块中的
    
    变量或函数。
    EXPORT_SYMBOL(符号名);
    EXPORT_SYMBOL_GPL(符号名);
    
  6. 模块作者等信息声明(可选)

    MODULE_AUTHOR(author);
    MODULE_DESCRIPTION(description);
    MODULE_VERSION(version_string);
    MODULE_DEVICE_TABLE(table_info);
    MODULE_ALIAS(alternate_name);
    

编译

# Makefile2.6

TARGET = demo_module

ifneq ($(KERNELRELEASE),)
#kbuild syntax. dependency relationshsip of files and target modules are listed here.

obj-m := $(TARGET).o 

else
# build from shell directly not in kernel root

CURDIR  = $(shell pwd)
KVER := $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build

all:
    $(MAKE) -C $(KDIR) M=$(CURDIR) modules

clean:
    $(MAKE) -C $(KDIR) M=$(CURDIR) clean

insert:
    sudo insmod $(TARGET).ko

remove:
    sudo rmmod $(TARGET)

endif

重要的数据结构

大部分的基础性的驱动操作包括3个重要的内核数据结构, 称为 file_operations, file, 和 inode。

file_operations

file_operations 结构体中的成员函数是设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的 open()、 write()、 read()、 close()等系统调用时最终被调用。

struct file_operations {
    struct module *owner; /* 拥有该结构的模块的指针,一般为 THIS_MODULES */ 
    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 *, char __user *, size_t, loff_t); /* 初始化一个异步的读取

操作*/
    ssize_t(*aio_write)(struct kiocb *, const char __user *, size_t, loff_t); /* 初始化一个异

步的写入操作*/
    int(*readdir)(struct file *, void *, filldir_t); /* 仅用于读取目录,对于设备文件,该字段

为 NULL */
    unsigned int(*poll)(struct file *, struct poll_table_struct*); /* 轮询函数,判断目前是否可

以进行非阻塞的读取或写入*/
    int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); /* 执行设

备 I/O 控制命令*/
    long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); /* 不使用 BLK 的文件系

统,将使用此种函数指针代替 ioctl */
    long(*compat_ioctl)(struct file *, unsigned int, unsigned long); /* 在 64 位系统

上, 32 位的 ioctl 调用,将使用此函数指针代替*/
    int(*mmap)(struct file *, struct vm_area_struct*); /* 用于请求将设备内存映射
    int(*open)(struct inode *, struct file*); /* 打开 */
    int(*release)(struct inode *, struct file*); /* 关闭*/
    int (*fsync) (struct file *, struct dentry *, int datasync); /* 刷新待处理的数据*/

    int(*aio_fsync)(struct kiocb *, int datasync); /* 异步 fsync */
    int(*fasync)(int, struct file *, int); /* 通知设备 FASYNC 标志发生变化*/
    ...
};

file

file 结构体代表一个打开的文件(设备对应于设备文件),系统中每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。

struct file {
    const struct file_operations *f_op; /* 和文件关联的操作*/
    unsigned int f_flags; /*文件标志,如 O_RDONLY、 O_NONBLOCK、 O_SYNC*/
    fmode_t f_mode; /*文件读/写模式, FMODE_READ 和 FMODE_WRITE*/
    loff_t f_pos; /* 当前读写位置*/
    void *private_data; /*文件私有数据,可存储自定义数据的指针*/
    ...
};

inode

VFS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是 Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。

struct inode {
    umode_t i_mode; /* inode 的权限 */
    uid_t i_uid; /* inode 拥有者的 id */
    gid_t i_gid; /* inode 所属的群组 id */
    dev_t i_rdev; /* 若是设备文件,此字段将记录设备的设备号 */
    loff_t i_size; /* inode 所代表的文件大小 */

    struct timespec i_atime; /* inode 最近一次的存取时间 */
    struct timespec i_mtime; /* inode 最近一次的修改时间 */
    struct timespec i_ctime; /* inode 的产生时间 */

    unsigned long i_blksize; /* inode 在做 I/O 时的区块大小 */
    unsigned long i_blocks; /* inode 所使用的 block 数,一个 block 为 512 byte*/

    struct block_device *i_bdev; /*若是块设备,为其对应的 block_device 结构体指针*/
    struct cdev *i_cdev; /*若是字符设备,为其对应的 cdev 结构体指针*/
    ...

本文总结了Linux设备驱动开发的基础知识和方法。通过阅读本文,你可以了解Linux设备驱动的工作原理和编程套路,为进一步学习和实践打下坚实的基础。在实际开发中,你还需要参考不同处理器和平台的文档和示例代码,以及Linux内核源码中提供的各种API和框架。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部