Linux笔记(13)| 字符设备驱动基础入门

时间:2022-07-24
本文章向大家介绍Linux笔记(13)| 字符设备驱动基础入门,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

距离上一次更新有一段时间了,主要是最近更忙一些,一般来说,有时间我会尽量更新,如果比较忙的话就更新慢一些。

好了,言归正传,今天要分享的是linux驱动中的字符设备驱动,我们知道,对于嵌入式linux开发来说,主要是分为应用开发和驱动开发,在前面的文章当中,都是在介绍应用开发,因为应用开发相对来说难度更低一些,主要是调用系统的API接口来实现功能,而驱动开发和硬件有很大的关系,属于底层开发。

应用开发主要是实现用户的要求,驱动开发是让硬件能够“动起来”。这些层次关系大致就是:用户提出要求,应用开发者通过调用系统的API接口来实现功能,API接口是操作系统提供的,它的底层就是驱动程序,而驱动程序再往下就是操作系统内核,内核再往下就是硬件了。

前面的讲的应用开发虽然不是特别深(以后会慢慢加深),但是大致覆盖了涉及到的内容,还有一个线程没有讲,这个到后面再补充,从今天开始就正式进入驱动开发,linux驱动有字符设备驱动、块设备驱动和网络设备驱动,其中字符设备驱动用的非常多,而且相对容易一些,所以先从字符设备驱动开始。

1、准备工作

首先要准备linux内核源码,因为驱动模块的安装必须是在自己的系统上编译得到的内核源码树。源码可以在开发板上进行编译,也可以在主机Ubuntu中使用交叉编译工具链来编译,总之是要自己编译的,而不是别人编译的。

其次要挂载好nfs文件系统,因为我们写代码一般是在主机Ubuntu中,然后通过nfs传递到开发板中。

2、了解驱动模块的代码结构和安装、卸载

驱动模块是以.ko为后缀的文件,驱动代码编译好之后就会得到.ko文件,然后使用命令insmod,就可以安装到linux系统中,使用rmmod命令就可以卸载模块,此外,还有lsmod用来查看系统中已经安装的模块,modinfo用来打印某个模块的信息。

接下来看一下最简单的一个驱动模块的代码结构是怎么样的:

#include <linux/module.h>    // module_init  module_exit
#include <linux/init.h>      // __init   __exit
#include <linux/fs.h>

#define MYMAJOR    200
#define MYNAME    "testchar"
static int test_chrdev_open(struct inode *inode, struct file *file)
{
  // 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
  // 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
  printk(KERN_INFO "test_chrdev_openn"); 
  return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
  printk(KERN_INFO "test_chrdev_releasen");
  return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
  .owner    = THIS_MODULE,        // 惯例,直接写即可
  .open    = test_chrdev_open,      // 将来应用open打开这个设备时实际调用的
  .release  = test_chrdev_release,    // 就是这个.open对应的函数
};
// 模块安装函数
static int __init chrdev_init(void)
{  
  int ret = -1; 
  printk(KERN_INFO "chrdev_init helloworld initn");
  // 在module_init宏调用的函数中去注册字符设备驱动
  ret = register_chrdev(MYMAJOR, MYNAME, &test_fops);
  if (ret)
  {
    printk(KERN_ERR "register_chrdev failn");
    return -EINVAL;
  }
  printk(KERN_INFO "register_chrdev success...n");
  return 0;
}
// 模块下载函数
static void __exit chrdev_exit(void)
{
  printk(KERN_INFO "chrdev_exit helloworld exitn");

  // 在module_exit宏调用的函数中去注销字符设备驱动
  unregister_chrdev(MYMAJOR, MYNAME);

}
module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");        // 描述模块的许可证
MODULE_AUTHOR("aston");        // 描述模块的作者
MODULE_DESCRIPTION("module test");  // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");      // 描述模块的别名信息

这是一个最简单的驱动模块的代码组成,接下来一步步分析。

首先应该从module_init和module_exit这两个宏开始分析,第一个是模块的入口,第二个是模块的出口,里面的参数是前面自己定义的一个函数,当模块安装的时候,就会自动去执行module_init绑定的那个函数,同样的,当卸载模块的时候,就会自动执行module_exit绑定的那个函数。一般会在module_init里面向系统注册自己的字符设备驱动,其实就是给你分配一个主设备号,这个设备号可以是自己指定的,也可以让系统自动分配。这是靠register_chrdev这个函数来实现的。

在讲register_chrdev这个函数之前,要先讲一下file_operations这个结构体。这是一个非常重要的结构体,它的作用就是将系统的API接口和你自己写的驱动的接口“连接”起来。这个结构体里的内容非常多,但是常用的不多,一般就是open、read、write、release(相当于是close)。以下仅是部分截图,可以参考https://blog.csdn.net/littlelee111/article/details/10133759

里面大都是函数指针,其实就是将操作系统的接口和你自己写的函数实现连接起来。

现在再回到register_chrdev这个函数,这个函数就是向内核注册驱动,第一个参数是设备的主设备号,第二个参数是设备的名字,第三个参数就是刚刚那个结构体类型的指针。主要就是关心设备的主设备号和那个结构体就行。(其实还有次设备号)。

那么按照这个思路,要填充那个结构体,就要自己写一些函数,这些函数就是实现驱动功能的具体代码。今天以驱动led为例,所以主要是实现write功能。

这里还要先明确一个概念就是,在linux系统中有一个哲学思想,就是一切皆文件。我们的设备,也抽象成了一个个的设备文件,所以,要操作设备,实际上就是向这个设备文件写入内容或者读取内容。

那么接下来就简单分析一下write函数的实现,其他的也是类似。

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{

  unsigned char databuf[10];

  if(cnt >10)
    cnt =10;

    /*从用户空间拷贝数据到内核空间*/
    if(copy_from_user(databuf, buf, cnt)){
    return -EIO;
  }

  if(!memcmp(databuf,"on",2)) {  
    iowrite32(0 << 4, GPIO1_DR);  
  } else if(!memcmp(databuf,"off",3)) {
    iowrite32(1 << 4, GPIO1_DR);
  }
  /*写成功后,返回写入的字数*/
  return cnt;
}

这个函数里面首先是将用户空间的数据拷贝到内核空间,为什么要这样做呢?我们知道,linux系统实际上使用了虚拟内存技术(mmu),所以用户和内核根本就不在同一个内存空间里,无法直接传递数据,所以要调用copy_from_user函数。拿到用户的数据之后,就可以来操作寄存器了。因为驱动程序归根结底还是像裸机一样,要操作寄存器。但是这里有一点不同,就是在裸机当中,我们直接操作的是真实的物理地址,但是现在是虚拟地址,应该要用函数将物理地址转化为虚拟地址(地址映射)之后再进行操作。其他的就基本一样了。

关于地址映射,实际上有两种方法,一种是静态的,一种是动态的,这里直接调用ioremap函数来实现动态映射。静态映射的优点是效率高,因为在启动内核的时候就已经映射好了,缺点是映射好了就再也不能改变了。动态映射就是需要的时候再映射,不需要的时候可以取消映射,非常灵活,缺点是效率低,关于地址映射这里不多说。

写好了write函数,就要将自己的函数与系统API连接起来,其实就是前面讲过的那个机构体。大致就是这样,其他的类似。

  .write = led_write

然后最后就是最底下的那几个宏,这几个宏并不是很重要,了解一下就行。

MODULE_LICENSE("GPL");        // 描述模块的许可证
MODULE_AUTHOR("aston");        // 描述模块的作者
MODULE_DESCRIPTION("module test");  // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");      // 描述模块的别名信息

这样就基本分析完了最简单的驱动模块的代码结构。不过还有一些细节问题要稍微提一下。

1、函数修饰符:__init和__exit

__init,本质上是个宏定义,在内核源代码中就有#define __init xxxx。这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。整个内核中的所有的这类函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。

2、printk函数

(1)printk在内核源码中用来打印信息的函数,用法和printf非常相似。

(2)printk和printf最大的差别:printf是C库函数,是在应用层编程中使用的,不能在linux内核源代码中使用;printk是linux内核源代码中自己封装出来的一个打印函数,是内核源码中的一个普通函数,只能在内核源码范围内使用,不能在应用编程中使用。

(3)printk相比printf来说还多了个:打印级别的设置。printk的打印级别是用来控制printk打印的这条信息是否在终端上显示的。

完整的led驱动代码如下(来源:野火电子):

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>

#define DEV_MAJOR    0    /* 动态申请主设备号 */
#define DEV_NAME    "red_led"   /*led设备名字 */

/* GPIO虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO04;
static void __iomem *SW_PAD_GPIO1_IO04;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;


static int led_open(struct inode *inode, struct file *filp)
{
  return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
  return -EFAULT;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{

  unsigned char databuf[10];

  if(cnt >10)
    cnt =10;
    
    /*从用户空间拷贝数据到内核空间*/
    if(copy_from_user(databuf, buf, cnt)){
    return -EIO;
  }
      
  if(!memcmp(databuf,"on",2)) {  
    iowrite32(0 << 4, GPIO1_DR);  
  } else if(!memcmp(databuf,"off",3)) {
    iowrite32(1 << 4, GPIO1_DR);
  }
  /*写成功后,返回写入的字数*/
  return cnt;
}

static int led_release(struct inode *inode, struct file *filp)
{
  return 0;
}


static struct file_operations led_fops = {
  .owner = THIS_MODULE,
  .open = led_open,
  .read = led_read,
  .write = led_write,
  .release =   led_release,
};

int major = 0;
static int __init led_init(void)
{
  
  /* GPIO相关寄存器映射 */
    IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4);
  SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4);
    SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4);
  GPIO1_GDIR = ioremap(0x0209c004, 4);
  GPIO1_DR = ioremap(0x0209c000, 4);


  /* 使能GPIO1时钟 */
  iowrite32(0xffffffff, IMX6U_CCM_CCGR1);

  /* 设置GPIO1_IO04复用为普通GPIO*/
  iowrite32(5, SW_MUX_GPIO1_IO04);
  
    /*设置GPIO属性*/
  iowrite32(0x10B0, SW_PAD_GPIO1_IO04);

  /* 设置GPIO1_IO04为输出功能 */
  iowrite32(1 << 4, GPIO1_GDIR);

  /* LED输出高电平 */
  iowrite32(1<< 4, GPIO1_DR);

  /* 注册字符设备驱动 */
  major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops);
    printk(KERN_ALERT "led major:%dn",major);

  return 0;
}

static void __exit led_exit(void)
{
  /* 取消映射 */
  iounmap(IMX6U_CCM_CCGR1);
  iounmap(SW_MUX_GPIO1_IO04);
  iounmap(SW_PAD_GPIO1_IO04);
  iounmap(GPIO1_DR);
  iounmap(GPIO1_GDIR);

  /* 注销字符设备驱动 */
  unregister_chrdev(major, DEV_NAME);
}


module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL2");
MODULE_AUTHOR("embedfire ");
MODULE_DESCRIPTION("led_module");
MODULE_ALIAS("led_module");

写好了驱动代码之后,接下来就是要写对应的Makefile文件来编译,由于篇幅原因,剩下的内容在下一篇里继续。