1. Linux驱动程序的分类

Linux 中主要分为三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。

1、字符设备驱动:因为软件操作设备是是以字节为单位进行的,是按照字节流进行读写操作的一种设备。典型的如LCD、蜂鸣器、SPI、触摸屏等驱动,都属于字符设备驱动的范畴。大部分的驱动程序都是属于字符设备驱动。

2、块设备驱动:块设备驱动是相对于字符设备驱动而定义的,因为块设备被软件操作时,是以块为单位进行操作的(块指的是多个字节组成一个块)。块设备大多指的都是各种存储类类设备,比如EMMC、SD卡、NANDFlash、U盘等等。

3、网络设备驱动:专门针对网络设备而设计的一种驱动,不管是有线还是无线网络,都属于网络设备驱动。

另外,一个设备可以属于多种设备驱动类型,比如 USB WIFI设备,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

2. 与Linux驱动开发相关的介绍

1、Linux下的应用程序是如何调用驱动程序的

应用程序在使用C库函数所提供的 open/read/write 等等函数时,最终会进入到内核里面,调用内核所提供的 sys_open/sys_read/sys_write 等等函数。而此时如果内核发现应用程序需要访问的是驱动的话,那么就会调用该驱动程序所提供的 drv_open/drv_read/drv_write 等函数;如果发现应用程序访问的不过是普通文件的话,那么内核就会调用访问普通文件的那套函数。下图形象的给出了调用关系:

驱动程序实际上起到承上启下的作用,上承应用程序,对下则实现了具体的硬件操作。

2、Linux驱动程序的两种运行方式

  • 可以把驱动程序编译进内核里面,这样内核启动后就会自动运行驱动程序了;
  • 将驱动程序编译成以.ko为后缀模块文件,然后在Linux启动后,我们自己手动安装驱动程序。一般来说,这种方式在开发驱动阶段常用。

3、Linux驱动开发中常用的几个命令

  • insmod(install module):用于安装以Linux的驱动模块
  • rmmod(remove module):卸载驱动模块
  • lsmod(list module):打印出当前内核中已经安装的模块
  • modinfo(module information):打印出某个 xxx.ko 文件的模块信息。用法:modinfo xxx.ko

3. Linux驱动开发需要准备的工作

1、已经安装好的交叉编译工具链

​ 我们开发的驱动程序是要运行在ARM架构上的,所以需要准备好ARM架构的编译工具链。一般来说我们使用开发板厂商,或者SoC原厂提供交叉编译工具链即可。具体如何安装交叉编译工具链这里不多啰嗦了。

2、准备已经配置和编译好你对应板子的内核源码

​ 驱动程序是运行在内核空间的,不同于应用程序运行在用户空间。驱动程序已经是属于内核的一部分了,而编译驱动程序需要借助于内核源码来编译。

​ 另外,内核源码的版本一定要和你板子上实际运行的版本相一致,否则编译出来的驱动程序会因为版本不同而无法在你的板子上运行。

3、你的开发板接线正常,网络正常(要保证开发板和ubuntu之间可以相互ping通,因为我们通过nfs方式把ubuntu编译好的 xxx.ko 等文件传输到开发板)。

4. show出你的代码

4.1 hello驱动的编写

我们在编写应用程序的时候,首先也是先学会如何再电脑屏幕上输出 “helllo world”。同样的,我们编写的第一个驱动程序,也是先学会hello驱动,该驱动不涉及任何的硬件操作,而且也是属于字符设备驱动的范畴。主要实现的功能是:

1、应用程序调用 open、read、write 等函数时,对应的驱动函数都打印出内核信息;

2、应用程序调用 write 函数时,传入的数据保存在驱动中;

3、应用程序调用 read 函数时,把驱动中保存的数据再返回给应用程序,并打印出来

驱动程序的编写其实也是有迹可循的,主要的编写步骤如下:

  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

其中,驱动程序核心中的核心就是 file_operations 这个结构体了。在这个结构体里面,就是要实现这个驱动程序自己的 open、read、write等函数,并通过Linux内核提供的接口注册到内核里面去。而其他的一些步骤都是为了遵循LInux驱动程序的编写规范,用于完善这个驱动程序的。

驱动代码的编写也不用完全都自己写,我们可以参考Linux内核提供的一些已有的驱动程序,下面我们就参考内核的一份 misc 驱动,编写我们自己的hello驱动程序,hello驱动代码如下:

#include <linux/module.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>/* 1. 确定主设备号                                                                 */
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;#define MIN(a, b) (a < b ? a : b)/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   *//** @description        : 从设备读取数据* @param - file   : 内核中的文件描述符* @param - buf      : 要存储读取的数据缓冲区(就是用户空间的内存地址)* @param - size    : 要读取的长度* @param - offset  : 相对于文件首地址的偏移量(一般读取信息后,指针都会偏移读取信息的长度)* @return            : 返回读取的字节数,如果读取失败则返回-1*/
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_to_user(buf, kernel_buf, MIN(1024, size));return MIN(1024, size);
}/** @description      : 向设备写数据* @param - file    : 内核中的文件描述符* @param - buf      : 要写给设备驱动的数据缓冲区* @param - size : 要写入的长度* @param - offset  : 相对于文件首地址的偏移量* @return            : 返回写入的字节数,如果写入失败则返回-1*/
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_from_user(kernel_buf, buf, MIN(1024, size));return MIN(1024, size);
}/** @description      : 打开设备* @param - node  : 设备节点* @param - file  : 文件描述符* @return           : 打开成功返回0,失败返回-1*/
static int hello_drv_open (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/** @description      : 关闭设备* @param - node  : 设备节点* @param - file  : 文件描述符* @return           : 关闭成功返回0,失败返回-1*/
static int hello_drv_close (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations hello_drv = {.owner   = THIS_MODULE,.open    = hello_drv_open,.read    = hello_drv_read,.write   = hello_drv_write,.release = hello_drv_close,
};/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数       */
static int __init hello_init(void)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello *//* 7. 其他完善:提供设备信息,自动创建设备节点                                 */hello_class = class_create(THIS_MODULE, "hello_class");err = PTR_ERR(hello_class);if (IS_ERR(hello_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "hello");return -1;}    device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */return 0;
}/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit hello_exit(void)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);device_destroy(hello_class, MKDEV(major, 0));class_destroy(hello_class);unregister_chrdev(major, "hello");
}/* 指定驱动的入口和出口,以及声明自己的驱动遵循GPL协议(不声明的话无法把驱动加载进内核) */
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

hello驱动程序的几点说明:

  1. 驱动程序要打印信息的时候,调用的函数是 printk ,而应用程序调用的是 printf

  2. 应用程序和驱动程序之间传递数据,不能使用简单的赋值或者memcpy等,要使用内核提供的 copy_from_user/copy_to_user 函数。当然如果需要传递大量数据的时候,还可以使用内存映射的方式

  3. 阅读一个驱动程序,首先要找到驱动程序的入口函数。上面的驱动入口函数就是hello_init函数,该函数做的事情就是向内核注册了一个 file_oprations 结构体,并且完成自动创建设备节点相关的代码

  4. file_oprations 结构体是驱动程序的核心,里面提供了本驱动 open/read/write/release 等成员,当应用程序调用了open/read/write/release 等函数时,就会导致对应驱动的这些成员函数被调用

4.2 编写hello应用程序测试

下面我们编写一个hello应用程序来测试我们编写好的驱动程序。该应用程序要实现的功能是:

  1. 向驱动程序写入一串字符串
  2. 把驱动程序保存起来的字符串读出来

hello_drv_test 应用程序代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>/* 该应用程序用法:* ./hello_drv_test -w abc    向hello驱动写入字符串 abc * ./hello_drv_test -r     读取驱动程序中保存的数据*/
int main(int argc, char **argv)
{int fd;char buf[1024];int len;/* 1. 判断参数 */if (argc < 2) {printf("Usage: %s -w <string>\n", argv[0]);printf("       %s -r\n", argv[0]);return -1;}/* 2. 打开文件 */fd = open("/dev/hello", O_RDWR);if (fd == -1){printf("can not open file /dev/hello\n");return -1;}/* 3. 写文件或读文件 */if ((0 == strcmp(argv[1], "-w")) && (argc == 3)){len = strlen(argv[2]) + 1;len = len < 1024 ? len : 1024;write(fd, argv[2], len);}else{len = read(fd, buf, 1024);        buf[1023] = '\0';printf("APP read : %s\n", buf);}close(fd);return 0;
}

4.3 驱动程序的Makefile文件

一个驱动程序最简单的Makefile包含以下内容:

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,根据具体情况指定# KERN_DIR 是指定内核源码的路径的,需要根据不同开发环境指定
KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4   all:make -C $(KERN_DIR) M=`pwd` modules # 本条指令是用于编译应用程序的,放在这里是为了不用在单独编译应用程序而已$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c clean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.orderrm -f hello_drv_testobj-m   += hello_drv.o        # obj-m 指定具体要编译的驱动程序

5. 在开发板上测试运行hello驱动

编写完上述的代码之后,就可以进行编译了。

直接在hello驱动所在的目录下,输入 make 即可编译了。编译完之后就会看到生成对应的 .ko 文件了。

然后我们把编译好的 .ko 文件,和测试驱动的应用程序都拷到 nfs 共享目录下,我的 nfs 共享目录是在 /home/lbh/nfs 下。

然后我们打开开发板后,进入到开发板的控制台。挂载 ubuntu 中的 nfs 共享目录到开发板的 /mnt 目录下,在开发板输入如下命令:

mount -t nfs -o nolock,nfsvers=3 192.168.1.33:/home/lbh/nfs /mnt

其中 192.168.1.33 这个IP地址是你的ubuntu的IP地址。

挂载完成之后,就可以去 /mnt 目录下看到了自己编译好的 .ko 文件和对应的应用程序文件了。

我们执行 insmod hello_drv.ko 命令,就可以把该驱动程序安装到内核中了。而且可以看到内核打印出了相应的信息,如下:

[ 293.594910] hello_drv: loading out-of-tree module taints kernel.
[ 293.616051] /home/lbh/linux/drv/hello_drv.c hello_init line 70

说明驱动加载成功了。

注意:如果板子没有看到打印信息的话,那么就输入如下命令把内核的打印信息打开:

echo "7 4 1 7" > /proc/sys/kernel/printk

当然有些板子内核打印信息是默认已经打开了的。或者我们输入 demsg 命令也可以看到内核的打印信息。

然后我们运行 hello_drv_test 应用程序来测试内核,都可以看到内核的打印信息,和我们读取到应用程序写给内核的字符串。

6. 和驱动调试有关的其他知识

  1. cat /proc/devices 命令可以查看当前系统是否有我们刚刚安装的驱动程序
  2. 安装了设备驱动之后,就会在我们的Linux系统 /dev目录下生成对应的设备文件了。linux中没一个驱动程序都有一个与之对应的设备文件,可以使用 ls /dev/hello -l 命令查看该驱动文件

Linux设备驱动开发入门之——hello驱动相关推荐

  1. Linux SD卡驱动开发(五) —— SD 卡驱动分析Core补充篇

    Core层中有两个重要函数 mmc_alloc_host 用于构造host,前面已经学习过,这里不再阐述:另一个就是 mmc_add_host,用于注册host 前面探测函数s3cmci_probe, ...

  2. Linux驱动开发(外传)---驱动开发调试方法

    前文回顾 <Linux驱动开发(一)-环境搭建与hello world> <Linux驱动开发(二)-驱动与设备的分离设计> <Linux驱动开发(三)-设备树> ...

  3. 驱动框架入门之LED-linux驱动开发第4部分-朱有鹏-专题视频课程

    驱动框架入门之LED-linux驱动开发第4部分-5199人已学习 课程介绍         本课程是linux驱动开发的第4个课程,主要内容是驱动框架的引入.通过led驱动框架和gpiolib的这两 ...

  4. Linux嵌入式驱动开发01——第一个驱动Hello World(附源码)

    文章目录 全系列传送门 引言 驱动介绍 Hello World 1. 包含头文件 2. 驱动模块的入口和出口 3. 声明信息 4. 功能实现 完整代码 编译 第一种方法 第二种方法 编译成模块 第一步 ...

  5. i.MX 6ULL 驱动开发 六:beep 驱动

    一.原理分析 通过原理图可以确定 beep 连接到 SNVS_TAMPER1 引脚上.根据 beep 原理,当 SNVS_TAMPER1 输出低电平时,beep 鸣叫. 通过数据手册确定 SNVS_T ...

  6. STM32MP157驱动开发——多点电容触摸屏驱动

    STM32MP157驱动开发--多点电容触摸屏驱动 一.简介 二.电容触摸屏驱动框架简介 多点触摸(MT)协议详解 三.驱动开发 1.添加 FT5426 设备节点 2.FT5426 节点配置 3.驱动 ...

  7. 《精通Linux设备驱动程序开发》——1.5 Linux发行版

    本节书摘来自异步社区<精通Linux设备驱动程序开发>一书中的第1章,第1.5节,作者:[印]Sreekrishnan Venkateswaran(斯里克里斯汉 温卡特斯瓦兰)著,更多章节 ...

  8. 《精通Linux设备驱动程序开发》——1.7 编译内核

    本节书摘来自异步社区<精通Linux设备驱动程序开发>一书中的第1章,第1.7节,作者:[印]Sreekrishnan Venkateswaran(斯里克里斯汉 温卡特斯瓦兰)著,更多章节 ...

  9. Linux 应用程序开发入门

    Linux 应用程序开发入门 Neo Chen (netkiller) <openunix@163.com> 版权 © 2011, 2012 http://netkiller.github ...

  10. 嵌入式Linux设备驱动程序开发指南9(平台设备驱动)——读书笔记

    平台设备驱动 九.平台设备驱动 9.1 平台设备驱动概述 9.2 GPIO驱动 9.2.1 简介 9.2.2 硬件名称 9.2.3 引脚控制器 9.2.4 引脚控制子系统 9.2.5 GPIO控制器驱 ...

最新文章

  1. Nature-2018-抗菌药物组合有望特异性治疗耐多药性的细菌感染
  2. 基于WeUI的Angular2开发
  3. 线程同步 生产者消费者 java_Java线程同步:生产者-消费者 模型(代码示例)
  4. 要嫁就嫁程序员!原因很简单:五成表示工资愿交给另一半!
  5. ELK日志管理之——elasticsearch部署
  6. DFS(入门题,走迷宫)
  7. 两个字符串的删除操作
  8. java 线程安全的单例_线程安全的单例模式的几种实现
  9. pku773_Happy 2006
  10. android 启动多个进程,Android开启多进程
  11. 机器学习和算法学习网址
  12. 摄像机、投影、3D旋转、缩放
  13. 自己封装的一个模拟下拉列表的插件
  14. 基于javaweb的本科生实习管理系统
  15. python代码解读_python源代码解读
  16. 齐杰文学CMS - 关关采集器2021可用19条采集规则
  17. 开机后台占用严重?教你如何清理常见后台占用
  18. 本科生学计算机视觉实际吗,成电信通学院本科生在全球计算机视觉顶会CVPR上发表研究成果...
  19. 任务调度:开源大数据调度框架Taier(太阿)
  20. 【FICO】标准成本、计划成本、实际成本、目标成本

热门文章

  1. Linux系列教程——Linux文件编辑、Linux用户管理
  2. Matlab2016b中文乱码怎么办
  3. Himall商城Html文本帮助类HtmlContentHelper(2)
  4. C# 使用X509Certificate2获取数字证书信息对接联通沃支付
  5. CentOS7和CentOS8 Asterisk 20.0.0 简单图形化界面5--libss7驱动7号信令
  6. C语言函数 思维导图
  7. 发那科FANUC系统选项U盘,可以随意添加机器人选项
  8. 屏幕尺寸、分辨率、DPI、PPI
  9. ffmpeg 下载网上m3u8的视频文件
  10. 成神之路——实施工程师