Linux驱动入门——编写第一个驱动

简介: Linux驱动入门——编写第一个驱动

前言

在编译驱动程序之前要先编译内核,原因有三点:

  • 驱动程序要用到内核文件
  • 编译驱动时用的内核、开发板上运行到内核,要一致
  • 更换板子上的内核后,板子上的其他驱动也要更换


编译内核步骤看我之前写过的文章,编译替换内核_设备树_驱动_IMX6ULL-CSDN博客


驱动入门知识

1.首先我们通常都是在Linux的终端上打开一个可执行文件,然后可执行文件就会执行程序。那么这个可执行文件做了什么呢?


2.可执行文件先是在应用层读取程序,其中会有很多库函数,库函数是属于内核之中。而内核又会往下调用驱动层程序。最终驱动层控制具体硬件。


  • 其实应用程序到库是比较容易理解的,比如我们刚学习C语言的时候,使用了printf,scanf等等这些函数。而这些函数就在库中。
  • 库可以和系统内核相连接,具体怎么实现的我也不太清楚。
  • 我们写了一个驱动程序,就需要告诉内核,这个过程叫做注册。我们注册了驱动之后,内核里面就会有这个驱动程序的信息,然后上层应用就可以调用。

3.所以我们只需要知道,咱们需要编写两个程序,一个是驱动层的,一个是应用层的,最后驱动层需要注册进入内核,应用层才能够使用。其他的先不要管。


4.我们在应用层调用read函数,对应驱动层的read函数。write函数和write函数对应。open函数和open函数对应。close函数和release函数对应(这个为什么不一样我也不清楚)。


5.我们对Linux 应用程序对驱动程序的调用流程有一个简单了解之后,我得知道整个程序编写流程应该怎么做。至于流程为什么是这样的,我们记住即可。因为这些都是人规定的,如果之后学的深了再进行深究也不迟,现在我们主要是入门      


1.APP 打开的文件在内核中如何表示

APP 打开文件时,可以得到一个整数,这个整数被称为文件句柄。对于 APP 的每一个文件句柄,在内核里面都有一个“struct file”与之对应。

我们使用 open 打开文件时,传入的 flags、mode 等参数会被记录在内核中对应的 struct file 结构体里(f_flags、f_mode):

int open(const char *pathname, int flags, mode_t mode);

去读写文件时,文件的当前偏移地址也会保存在 struct file 结构体的 f_pos 成员里。


299708a89b8876d43b82b344ef07b2d6_b87e8243833a4b078a75fbf193400cac.png


2.打开字符设备节点时,内核中也有对应的 struct file

注意这个结构体中的结构体:struct file_operations *f_op,这是由驱动程序提供的。


结构体 struct file_operations 的定义如下:

编写 Hello 驱动程序步骤

主要为一下七个步骤:

  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


1.流程介绍

<1>我们首先需要编写一个file_operations类型的结构体,这个结构体用于管理驱动程序。之后我们将驱动程序注册进入内核之后,我们在应用层调用这个驱动,那么就可以直接通过这个结构体来操作驱动中的open,write,read等函数。


<2>实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体。这样我们在应用层调用open,write,read等函数,就是调用这个驱动了。


这个时候有人可能会问了,有这么多个驱动,我怎么知道open对应的是哪一个驱动?很简单,咱们在写应用层程序的时候,是不是第一个参数是需要传入一个设备号。系统根据这个设备号来判断是调用的哪一个驱动。


<3>把 file_operations 结构体告诉内核: register_chrdev。我们写了一个驱动,但是内核是不知道的。那么怎么办呢?我们就去注册他,内核就明白,有了这个驱动,然后给他分配一个设备号。之后应用层就可以根据这个设备号来调用驱动层了。


<4> 这个时候,有人就有疑问了,谁来注册这个结构体?于是我们需要一个入口函数来进行注册,安装驱动程序时,就会去调用这个入口函数。


<5>有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev。


<6>最后需要加入GPL协议。因为Linux是遵顼GPL协议的,所以你如果需要使用Linux其他的驱动层函数,就必须遵顼GPL协议,强制要求开源代码。根据这个协议,你可以要求所有使用Linux的厂商提供驱动层源代码,同时别人也可以要求你公开你的驱动层代码,这个是相互的。不过很多厂商为了规避这个协议,驱动源代码很简单,复杂的东西放在应用层。至于还有一个作者名字的添加,随便写不写。                          


2.驱动代码:

hello_drv.c

#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结构体                   */
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);
}
 
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);
}
 
static int hello_drv_open (struct inode *node, struct file *file)
{
  printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
  return 0;
}
 
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 */
 
 
  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");
}
 
 
/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */
 
module_init(hello_init);
module_exit(hello_exit);
 
MODULE_LICENSE("GPL");
 
 

3.应用层代码:

hello_drv_test.c

 
#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_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;
}
 
 

怎么把.c 文件编译为驱动程序.ko?


这要借助内核的顶层 Makefile,先设置好交叉编译工具链,编译好你的板子所用的内核,然后修改 Makefile 指定内核源码路径,最后即可执行 make 命令编译驱动程序和测试程序。


4.本驱动程序的 Makefile 内容:

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
 
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 clean
        rm -rf modules.order
        rm -f hello_drv_test
 
obj-m   += hello_drv.o

5.上机实验:

执行 make 命令编译驱动程序和测试程序

启动单板后,可以通过 NFS 挂载 Ubuntu 的某个目录,访问该目录中的程序。


打开内核打印:echo "7 4 1 7" > /proc/sys/kernel/printk


关闭内核打印:echo 0       4       0      7  > /proc/sys/kernel/printk


insmod 就是install module的缩写(载入模块)


insmod hello_drv.ko装载驱动


ls /dev/hello -l // 驱动程序会生成设备节点 驱动程序会生成设备节点


lsmod 确认驱动已经安装


我们知道驱动已经安装好了,那么我们需要知道这个驱动的设备号


cat /proc/devices,查看当前已经被使用掉的设备号


驱动名字与我们在驱动层使用register_chrdev()函数的第二个参数有关

012c0eda1b0bda95fff51f6319cf4391_57b2d6a27b8c4174958894a0bd3d86f1.png

./hello_drv_test // 查看测试程序的用法

./hello_drv_test -w zglnb // 往驱动程序中写入字符串

./hello_drv_test -r // 从驱动程序中读出字符串

相关文章
|
3月前
|
机器学习/深度学习 安全 网络协议
Linux防火墙iptables命令管理入门
本文介绍了关于Linux防火墙iptables命令管理入门的教程,涵盖了iptables的基本概念、语法格式、常用参数、基础查询操作以及链和规则管理等内容。
242 73
|
1月前
|
Unix Linux Shell
linux入门!
本文档介绍了Linux系统入门的基础知识,包括操作系统概述、CentOS系统的安装与远程连接、文件操作、目录结构、用户和用户组管理、权限管理、Shell基础、输入输出、压缩打包、文件传输、软件安装、文件查找、进程管理、定时任务和服务管理等内容。重点讲解了常见的命令和操作技巧,帮助初学者快速掌握Linux系统的基本使用方法。
73 3
|
2月前
|
机器学习/深度学习 Linux 编译器
Linux入门3——vim的简单使用
Linux入门3——vim的简单使用
61 1
|
2月前
|
Linux Shell Windows
Linux入门1——初识Linux指令
Linux入门1——初识Linux指令
36 0
Linux入门1——初识Linux指令
|
2月前
|
存储 数据可视化 Linux
Linux 基础入门
Linux 基础入门
|
2月前
|
Linux Go 数据安全/隐私保护
Linux入门2——初识Linux权限
Linux入门2——初识Linux权限
30 0
|
4月前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
55 6
|
3月前
|
Linux API
Linux里的高精度时间计时器(HPET)驱动 【ChatGPT】
Linux里的高精度时间计时器(HPET)驱动 【ChatGPT】
|
3月前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】