• 三种Linux字符设备驱动写法-1:最简单的基本框架


    学习目的:Linux驱动整体思路有所了解,掌握最简单的Linux字符设备驱动程序写法,本文以led为例。

    注意:编写驱动前需要有内核源码,并且经过编译,且该源码与装载驱动的开发板的内核要一致,否则会出现版本不匹配等各种奇怪问题。

    先放基本流程再细讲,编写Linux字符设备驱动程序的流程基本步骤如下:

           1、查看原理图、数据手册,了解设备的操作方法;
      2、在内核中找到相近的驱动程序,以它作为模板进行开发,有时候需要从零开始;
      3、注册驱动程序,给用户空间提供访问的接口,如在/dev/目录下生成设备文件;
      4、设计所要实现的操作,如open(),close(),read(),write()、ioctl()等函数;
      5、如果有需要实现中断服务;
      6、编译该驱动程序到内核中,编写应用程序测试驱动程序。

    1. 基本概念引入

    在linux系统中,进入/dev目录就可以查看设备

    eed1bfe4203a49a797212d930f0ac636.png

    • c:字符设备
    • b:块设备
    • d:目录
    • l:符号链接

    主设备号: 区分某一类的设备。如上图agpgart设备,主设备号为10。

    次设备号: 用于区分同一类设备的不同个体或不同分区。agpgart设备,次设备号为175。

    主次设备号合起来是真正的设备号,就像身份证: 前六位数字,就代表是某个地区,用于区分不同地方的人。后面的其他数字是具体到某个人。

    如何编译一个驱动程序

    假设写了一个led驱动程序的源码leddrv.c,一个应用程序led_test.c,下面这个makefile同时编译了驱动和应用。

    makefile:

    1. KERN_DIR = /home/linux-4.4
    2. all:
    3. make -C $(KERN_DIR) M=`pwd` modules
    4. $(CROSS_COMPILE)gcc -o led_test led_test.c
    5. clean:
    6. make -C $(KERN_DIR) M=`pwd` modules clean
    7. rm -rf modules.order
    8. rm -f leddrv
    9. obj-m += leddrv.o

    KERN_DIR表示内核源码目录,KERN_DIR目录中包含了内核驱动模块所需要的各种头文件及依赖。

    make -C $(KERN_DIR) M=`pwd` modules

    这句命令是make modules命令的扩展,-C选项的作用是指将当前的工作目录转移到指定目录,即(KERN_DIR)目录,程序到(shell pwd)当前目录查找模块源码,将其编译,生成.ko文件。

    如何装载一个驱动程序

    输入以下命令:

    insmod leddrv.ko

     如何卸载驱动程序:

    输入以下命令

    rmmod leddrv

    2. 驱动程序基本框架

    2.1 应用程序如何调用驱动程序

    Linux有以下三种设备:

    1. 字符设备:只能一个字节一个字节的读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序进行。字符设备是面向流的设备,常见的字符设备如鼠标、键盘、串口、控制台、LED等。
    2. 块设备:是指可以从设备的任意位置读取一定长度的数据设备。块设备如硬盘、磁盘、U盘和SD卡等存储设备。
    3. 网络设备:网络设备比较特殊,不在是对文件进行操作,而是由专门的网络接口来实现。应用程序不能直接访问网络设备驱动程序。在/dev目录下也没有文件来表示网络设备。

    对于字符设备和块设备来说,当一个设备的驱动加载成功,就会在/dev目录下生成对应的文件。linux用户程序通过设备文件或叫做设备节点来使用驱动程序操作字符设备和块设备。

    以LED为例,与LED对应的驱动设备文件/dev/led。既然是文件,访问肯定与open、read、write等文件io脱不开关系。使用open(),close()来打开与关闭驱动设备文件。使用write()函数来向驱动设备文件写入参数,根据参数来控制LED。

    总得来说就是这样的过程:在用户空间(应用程序)使用open,write,read,close等函数,通过系统调用,进入内核空间,调用驱动程序的open,write,read,close等函数,进而操作具体硬件。

    a279215052634af2b730ebb00a2b91d8.png

    了解到基本过程后,问题就来了:

    1.驱动程序如何生成设备节点(设备文件)

    2.如何使得应用程序的open()、read()、write()等函数与驱动程序的open()、read()、write()等函数建立联系?

    这两个问题也就是驱动基本框架的核心。

    2.2 驱动程序如何生成设备节点

    Linux 2.6 开始引入了动态设备管理, 用 udev 作为设备管理器(应用在x86), 相比之前的静态设备管理,在使用上更加方便灵活。

    udev 根据 sysfs 系统提供的设备信息实现对/dev 目录下设备节点的动态管理,包括设备节点的创建、删除等。

    常用在嵌入式系统中的是udev的简化版—mdev,作用是在系统启动和热插拔或动态加载驱动程序时,自动创建设备节点。文件系统的/dev目录下的设备节点都是由mdev创建的。

    举个例子:

    insmod led_drv.ko时候,mdev自动帮我们在/dev目录下创建设备节点

    rmmod led_drv时候,mdev自动帮我们在/dev目录下删除设备节点

    若要编写一个能用 mdev 管理的设备驱动,需要在驱动代码中调用 class_create()为设备创建一个 class 类,再调用 device_create()为每个设备创建对应的设备

    备注:register_chrdev()为2.6内核以前的老方法,只不过也一样能用,而且步骤少,这里为了便于新手理解,讲老方法,新方法多了几步,可以自行搜索。

    1. static int myled_init(void)
    2. {
    3.     major = register_chrdev(0, "myled", &myled_oprs);
    4.     led_class = class_create(THIS_MODULE, "myled");
    5.     device_create(led_class, NULL, MKDEV(major, 0), "led"); /* /dev/led */
    6.     return 0;
    7. }
    8. static void myled_exit(void)
    9. {
    10.     unregister_chrdev(major, "myled");
    11.     device_destroy(led_class,  MKDEV(major, 0));
    12.     class_destroy(led_class);
    13. }
    14. module_init(myled_init);
    15. module_exit(myled_exit);

    谁来注册驱动程序?需要一个入口函数:安装驱动程序时,就会去调用这个入口函数;而卸载的时候,需要一个出口函数,卸载时会调用。

    我们使用 insmod 命令加载驱动时,实际上就是调用 myled_init 函数;使用 rmmod 命令卸载驱动时,实际上就是调用 myled_exit函数。而程序中的module_init()和module_exit()就是告诉内核哪个函数是入口,哪个是出口。

    一点点来分析这一段代码

    2.2.1 register_chrdev()和unregister_chrdev()

    首先是register_chrdev(),注册字符设备驱动

    源码:

    1. static inline int register_chrdev(unsigned int major, const char *name,
    2. const struct file_operations *fops)
    3. {
    4. return __register_chrdev(major, 0, 256, name, fops);
    5. }

    初学,到这就行,没必要深入看__register_chrdev(major, 0, 256, name, fops)函数,我们只需要知道:

    1.第一个参数,majior,主设备号,可手动指定,当major(主设备号)为0时,自动分配一个主设备号,并在函数结束时返回

    2.第二个参数,name,为驱动程序名字,不是很重要,命名规范即可

    3.第三个参数,fops,是一个file_operations结构体,这个在后面再讲

     major = register_chrdev(0, "myled", &myled_oprs);

    所以上面这行代码的意思就是自动分配主设备号,放入major,字符设备名字为myled,file_operations结构体myled_oprs放入内核。

    unregister_chrdev(),销毁字符设备驱动:

    1. static inline void unregister_chrdev(unsigned int major, const char *name)
    2. {
    3. __unregister_chrdev(major, 0, 256, name);
    4. }

    只有两个参数,majior和name,无返回值。

    2.2.2 class_create()和class_destroy()

    class_create两个参数第一个是owner,一般传入THIS_MODULE,第二个参数传入类的名字,返回class指针,所以需要提前定义static struct class *led_class,来接收一下返回值。

    class_destroy()只需传入class变量即可销毁传入的class。

    led_class = class_create(THIS_MODULE, "myled");

    2.2.3 device_create()和device_destroy()

    devce_create()创建设备:

    1. struct device *device_create(struct class *class, struct device *parent,
    2. dev_t devt, const char *fmt, ...)
    3. {
    4. 内容省略
    5. }

    成功返回device结构体指针

    第一个参数:*class,传入用class_create()创建的类

    第二个参数:设备的父节点,一般传入NULL

    第三个参数:devt,传入完整的设备号

    第四个参数:*fmt,传入设备名字

    device_create(led_class, NULL, MKDEV(major, 0), "led");

    device_destroy()销毁设备:

    1. void device_destroy(struct class *class, dev_t devt)
    2. {
    3. 省略
    4. }

    传入类和设备号。

    2.3 实现自己的file_operations结构体

    file_operations结构体使得应用程序的open()、read()、write()等函数与驱动程序的open()、read()、write()等函数建立联系。

    在2.2.1中讲了注册函数register_chrdev(unsigned int major, const char *name,const struct file_operations *fops),第三个参数fops,就是file_operations结构体,现在来实现他。

    首先看一下结构体源码:

    1. struct file_operations {
    2. struct module *owner;
    3. loff_t (*llseek) (struct file *, loff_t, int);
    4. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    5. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    6. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    7. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    8. int (*readdir) (struct file *, void *, filldir_t);
    9. unsigned int (*poll) (struct file *, struct poll_table_struct *);
    10. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    11. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    12. int (*mmap) (struct file *, struct vm_area_struct *);
    13. int (*open) (struct inode *, struct file *);
    14. int (*flush) (struct file *, fl_owner_t id);
    15. int (*release) (struct inode *, struct file *);
    16. int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    17. int (*aio_fsync) (struct kiocb *, int datasync);
    18. int (*fasync) (int, struct file *, int);
    19. int (*lock) (struct file *, int, struct file_lock *);
    20. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    21. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    22. int (*check_flags)(int);
    23. int (*flock) (struct file *, int, struct file_lock *);
    24. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    25. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    26. int (*setlease)(struct file *, long, struct file_lock **);
    27. long (*fallocate)(struct file *file, int mode, loff_t offset,
    28. loff_t len);
    29. };

    很多内容,对于初学,并不需要全掌握,在工作中很多也是用不到的,在这里讲最常用的open,write,owner,release

    自己实现了这些函数后填入file_operations结构体即可,

    例如:

    1. static struct file_operations myled_oprs = {
    2. .owner = THIS_MODULE,
    3. .open = led_open,
    4. .write = led_write,
    5. .release = led_release,
    6. };

    首先要捋一捋这些函数有哪些功能,我们要编写led的驱动程序,那么在应用程序中:

    open()即是初始化GPIO,read()即是读取led状态,write()即是通过参数,改变led状态。

    涉及参数还有一点,应用程序工作在用户空间,驱动程序工作在内核空间,数据互相不可见,因此需要两个函数,用于互相传递数据,也就是在应用程序的read()、write()和驱动程序led_read()、led_write()之间传递数据:

    copy_from_user()和copy_to_user():

    1. copy_from_user(void *to, const void __user *from, unsigned long n)
    2. copy_to_user(void __user *to, const void *from, unsigned long n)

    其中还涉及到io地址映射的问题,也就是ioremap,不了解的可以看看这篇文章

    Linux 内核访问外设I/O资源的方式

    2.3.1 open()

    1. static unsigned long gpio_va;
    2. #define GPIO_OFT(x) ((x) - 0x56000000)
    3. #define GPFCON (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000050)))
    4. #define GPFDAT (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000054)))
    1. static int led_open (struct inode *node, struct file *filp)
    2. {
    3. gpio_va = ioremap(0x56000000, 0x100000);
    4. GPFCON &= ~(0x3<<(5*2));
    5. GPFCON |= (1<<(5*2));
    6. return 0;
    7. }

    在我们这个驱动led的程序里open函数里只做两件事就够了,一个是ioremap,一个是配置io引脚。

    2.3.2 write()

    1. static ssize_t led_write (struct file *filp, const char __user *buf, size_t size, loff_t *off)
    2. {
    3. unsigned char val;
    4. copy_from_user(&val, buf, 1);
    5. if (val)
    6. {
    7. GPFDAT = 0;
    8. }
    9. else
    10. {
    11. GPFDAT = ~GPFDAT;
    12. }
    13. return 1;
    14. }

    write()函数也很简单,根据应用程序传递的val的值让GPFDAT寄存器为1或0,从而控制led灯的亮灭。

    2.3.3 release()

    1. static int led_release (struct inode *node, struct file *filp)
    2. {
    3. iounmap(gpio_va);
    4. return 0;
    5. }

    这里只需要释放之前映射的虚拟地址。 

    3. 完整代码

    驱动程序:

    1. #include <linux/module.h>
    2. #include <linux/kernel.h>
    3. #include <linux/fs.h>
    4. #include <linux/init.h>
    5. #include <linux/delay.h>
    6. #include <linux/uaccess.h>
    7. #include <asm/irq.h>
    8. #include <asm/io.h>
    9. #include <linux/cdev.h>
    10. static volatile unsigned int *GPFCON,*GPFDAT;
    11. static int major;
    12. static struct class *led_class;
    13. static unsigned long gpio_va;
    14. #define GPIO_OFT(x) ((x) - 0x56000000)
    15. #define GPFCON (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000050)))
    16. #define GPFDAT (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000054)))
    17. static int led_open (struct inode *node, struct file *filp)
    18. {
    19. gpio_va = ioremap(0x56000000, 0x100000);
    20. GPFCON &= ~(0x3<<(5*2));
    21. GPFCON |= (1<<(5*2));
    22. return 0;
    23. }
    24. static ssize_t led_write (struct file *filp, const char __user *buf, size_t size, loff_t *off)
    25. {
    26. unsigned char val;
    27. copy_from_user(&val, buf, 1);
    28. if (val)
    29. {
    30. GPFDAT = 0;
    31. }
    32. else
    33. {
    34. GPFDAT = ~GPFDAT;
    35. }
    36. return 1;
    37. }
    38. static int led_release (struct inode *node, struct file *filp)
    39. {
    40. iounmap(gpio_va);
    41. return 0;
    42. }
    43. static struct file_operations myled_oprs = {
    44. .owner = THIS_MODULE,
    45. .open = led_open,
    46. .write = led_write,
    47. .release = led_release,
    48. };
    49. static int __init myled_init(void)
    50. {
    51. major = register_chrdev(0, "myled", &myled_oprs);
    52. led_class = class_create(THIS_MODULE, "myled");
    53. device_create(led_class, NULL, MKDEV(major, 0),"led"); /* /dev/led */
    54. return 0;
    55. }
    56. static void __exit myled_exit(void)
    57. {
    58. unregister_chrdev(major, "myled");
    59. device_destroy(led_class, MKDEV(major, 0));
    60. class_destroy(led_class);
    61. }
    62. module_init(myled_init);
    63. module_exit(myled_exit);
    64. MODULE_LICENSE("GPL");

    应用程序:

    通过open打开设备节点,通过write写入val值控制led亮灭。

    1. #include <sys/types.h>
    2. #include <sys/stat.h>
    3. #include <fcntl.h>
    4. #include <stdio.h>
    5. /* ledtest on ledtest off*/
    6. int main(int argc, char **argv)
    7. {
    8. int fd;
    9. unsigned char val = 1;
    10. fd = open("/dev/led", O_RDWR);
    11. if (fd < 0)
    12. {
    13. printf("can't open!\n");
    14. }
    15. if (argc != 2)
    16. {
    17. printf("Usage :\n");
    18. printf("%s \n", argv[0]);
    19. return 0;
    20. }
    21. if (strcmp(argv[1], "on") == 0)
    22. {
    23. val = 1;
    24. }
    25. else
    26. {
    27. val = 0;
    28. }
    29. write(fd, &val, 1);
    30. close(fd);
    31. return 0;
    32. }

    开发板对应的内核源码目录中编译后生成后缀名为ko的驱动程序。

    装载驱动:insmod led_drv.ko

    通过命令:./ledtest on使led亮,./ledtest off使led灭

    如果存在问题:

    通过 cat  /proc/devices 查看设备是否注册成功

    通过 ls /sys/class 查看是否有我们申请的类myled

    通过 ls /dev 查看设备节点是否创建成功

    再一步步排除问题。

     

  • 相关阅读:
    JavaScript:实现检查给定的字符串是否为回文算法(附完整源码)
    51单片机应用
    Java EE 用户删除和修改功能
    主流嵌入式操作系统有哪些
    linux升级glibc-2.28
    clickhouse使用入门
    slurm是什么,怎么用? For slurm和For Pytorch有什么区别和联系?
    Java自学路线图之Java进阶自学
    浅谈无线测温产品在菲律宾某工厂配电项目的应用
    如何实现基于场景的接口自动化测试用例?
  • 原文地址:https://blog.csdn.net/freestep96/article/details/126987964