一文轻松学会linux字符设备驱动

时间:2022-07-25
本文章向大家介绍一文轻松学会linux字符设备驱动,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1.概述

在linux系统中许多外围设备都被规定为字符设备,诸如按键、触摸屏、重力传感器、LED、光敏传感器等,这些设备都需要字符设备驱动才能正常工作。本章就来实现一个标准的字符设备驱动框架模板,目的是为以后的设备驱动提供标准模板,提高开发效率与代码整洁度。

2. 编程思想

要想实现一个基础的代码模板,需要考虑到代码的标准化、独立性和可重用性。因此在写代码前需要构思一下字符设备驱动常用到哪些功能。这里列举一下常用到的功能,并一一记录实现的流程及意义。

2.1框架搭建

在实现字符驱动前,首先要做的是搭建字符设备驱动框架,先将固定的字符设备驱动框架搭建起来,然后再在相应的内容中添加相应的代码即可。这种框架大大减轻了开发者在编程中对代码流程设计的压力,同时为了方便以后的重用,这里将驱动通用的信息全部用driver_case、DRIVER_CASE代替。因此在重用时,只需要全局将driver_case、DRIVER_CASE替换成需要的字符即可。

#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/platform_device.h>

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

static ssize_t driver_case_write(struct file *file, const char __user *buf,
                             size_t size, loff_t *offset)
{
    return 0;
}

/* 驱动结构体 */
static struct file_operations driver_case_fops = {
    .owner = THIS_MODULE,
    .open  = driver_case_open,
    .write = driver_case_write,
};

static int driver_case_probe(struct platform_device *pdev)
{
    return 0;
}

int driver_case_remove(struct platform_device *pdev)
{
    return 0;
}

const struct of_device_id driver_case_table[] = {
    {
        .compatible = COMPATABLE_NAME,
    },
    {
    },
};

static struct platform_driver driver_case_device_driver = {
    .probe  = driver_case_probe,
    .remove = driver_case_remove,
    .driver = {
        .name = PLATFORM_NAME,
        .owner = THIS_MODULE,
        .of_match_table = driver_case_table,
    },
};


static int __init driver_case_init(void)
{
    platform_driver_register(&driver_case_device_driver);
    
    return 0;
}

static void __exit driver_case_exit(void)
{
    platform_driver_unregister(&driver_case_device_driver);
}

module_exit(driver_case_exit);
module_init(driver_case_init);

MODULE_LICENSE("GPL");

2.2 开头注释

一篇标准的代码头部需要注释,这些注释主要内容包括公司信息、文件名、作者、版本、描述、修改日志以及其他信息等。

/*
********************************************************************************
* Copyright (C),1999-2020, Jimi IoT Co., Ltd.
* File Name   :   driver_case.c
* Author      :   dongxiang
* Version     :   V1.0
* Description :   General driver template, if wanting to use it, you can use 
*                 global case matching to replace DRIVER_CASE and driver_case 
*                 with your custom driver name.
* Journal     :   2020-05-09 init v1.0 by dongxiang
* Others      :   
********************************************************************************
*/

2.3 LOG打印封装

在驱动调试的时候,经常会使用打印函数printk,但是不同的调试需要打印不同的固定信息。如果只是用printk来实现,代码会显得会乱,且后期难于屏蔽log。因此我们可以针对不同的打印,对printk进行定制化封装。

这里列举三个封装实例: PRINT_ERR用来打印报错log;PRINT_INFO用来打印正常log;PRINT_DEBUG用来打印调试log。可以看到PRINT_DEBUG前有DEBUG_LOG_SUPPORT宏,当注释掉宏时,PRINT_DEBUG便不会打印调试log,方便后期屏蔽调试log使用。

#define DEBUG_LOG_SUPPORT

#define PRINT_ERR(format,x...)    
do{ printk(KERN_ERR "ERROR: func: %s line: %04d info: " format,          
                                 __func__, __LINE__, ## x); }while(0)
#define PRINT_INFO(format,x...)   
do{ printk(KERN_INFO "[driver_case]" format, ## x); }while(0)

#ifdef DEBUG_LOG_SUPPORT
#define PRINT_DEBUG(format,x...)  
do{ printk(KERN_INFO "[driver_case] func: %s line: %d info: " format,    
                                 __func__, __LINE__, ## x); }while(0)
#else
#define PRINT_DEBUG(format,x...)  
#endif

2.4 结构体封装

在驱动编程中,要有面向对象编程思想。当需要定义一个驱动各种信息时,可以将所有相关的信息集合成一个结构体,各种类型信息定义在其结构体下的成员。这样不仅方便编程,而且能够快速理清各个信息变量之间的关系。

如下图,driver_case_dev 表示字符驱动常用到的信息类型, driver_case_platform_data 表示需要从设备树获取的数据。其中driver_case_dev 包含driver_case_platform_data。

struct driver_case_platform_data {
    int    gpio_num;
};

struct driver_case_dev {
    int    major;
    dev_t  devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct driver_case_platform_data *platform_data;
};

2.5 功能模块封装

驱动编程的入口函数中需要实现许多初始化工作,这些工作都大同小异,如果放到主干中不仅影响代码的可阅读性,同样影响代码的重用性。因此在编程过程中,针对实现特定功能的代码,需要将其模块化封装起来,只将模块入口放入主干之中。

这里就列举出,在字符设备驱动编程中,probe函数中要实现设备树数据的获取以及字符驱动接口的注册,将其一一封装。

字符驱动接口注册模块:

static int register_driver(void)
{
    PRINT_INFO("Entry %s n", __func__);
    
    /* 1. 设置设备号 
     * 主设备号已知, 静态注册;未知, 动态注册。
     */
#ifdef DEV_MAJOR
        driver_case.devid = MKDEV(DEV_MAJOR, 0);
        register_chrdev_region(driver_case.devid, DRIVER_CASE_NUM, DRIVER_CASE_NAME);
#else    
        alloc_chrdev_region(&driver_case.devid, 0, DRIVER_CASE_NUM, DRIVER_CASE_NAME);
        driver_case.major = MAJOR(driver_case.devid);    
#endif

    /* 2. 注册驱动结构体 */
    driver_case.cdev.owner = THIS_MODULE;
    cdev_init(&driver_case.cdev, &driver_case_fops);
    cdev_add(&driver_case.cdev, driver_case.devid, DRIVER_CASE_NUM);
    PRINT_DEBUG("driver_case_fops succesful! n");
    
    /* 3. 创建类 */
    driver_case.class = class_create(THIS_MODULE, DRIVER_CASE_CLASS_NAME);    
    if(IS_ERR(driver_case.class)) {
        PRINT_ERR("%s under class created failed! n", DRIVER_CASE_DEVICE_NAME);
        
        return ERROR;
    }
    
    /* 4.创建设备 */
    driver_case.device = device_create(driver_case.class, NULL, 
                                    driver_case.devid, NULL, DRIVER_CASE_DEVICE_NAME);
    if(NULL == driver_case.device) {   
        PRINT_ERR("%s device created failed! n", DRIVER_CASE_DEVICE_NAME); 
        return ERROR;    
    }

    return OK;
}

设备树数据获取模块:

static struct driver_case_platform_data  *driver_case_parse_dt(struct device *pdev)
{
    struct driver_case_platform_data *pdata;
    PRINT_INFO("Entry %s n", __func__);
    
    pdata = kzalloc(sizeof(*pdata), GFP_KERNEL);
    if (!pdata) {
        PRINT_ERR("could not allocate memory for platform datan");

        return NULL;
    }

    return pdata;
}

3.测试

以上内容基本上实现了字符驱动需要的常用功能,这里编译并烧录到开发板,测试一下是否能够跑通;这里测试效果主要看,在应用层调用后,能否打印出从设备树获取的节点数据。

设备树代码:

    driver_case {
        compatible = "dx, driver_case";
        gpio_num = <24>;
        label = "driver_case";0
        status = "okay";
    };

测试代码修改: 只需要将获取到的设备节点值打印出来即可。因此对driver_case_parse_dt 稍加修改

static struct driver_case_platform_data  *driver_case_parse_dt (
                                              struct device *pdev)
{
    struct driver_case_platform_data *pdata;
    struct device_node *np = pdev->of_node;
    const char *str;

    PRINT_INFO("Entry %s n", __func__);
    
    pdata = kzalloc(sizeof(*pdata), GFP_KERNEL);
    if (!pdata) {
        PRINT_ERR("Could not allocate memory for platform data n");

        return NULL;
    }
 
	if(of_property_read_u32(np, "gpio_num", &pdata->gpio_nums) < 0){
		PRINT_ERR("Get gpio_num from device tree failed! n");

		return NULL;
	}    
    PRINT_DEBUG("gpio_num: %d n", pdata->gpio_nums);
 
    if(of_property_read_string(np, "label", 
                               &str) < 0) {
    
		PRINT_ERR("Get label from device tree failed! n");

		return NULL;
    }
    memcpy(pdata->label, str, strlen(str));
    PRINT_DEBUG("label1: %s n", pdata->label);

    return pdata;
}

效果图:

效果图.png

4.总结

本次文章主要介绍如何创建一个可重用的字符设备驱动代码模板。虽然看上去代码很少,但是也是经常一个多星期的推敲以及优化。再全局替换driver_case和DRIVER_CASE后,即可成为一个新的字符驱动,没有保留之前的痕迹。如此一来,以后的代码都可以采用此模板。

代码链接: https://github.com/LinuxTaoist/Linux_drivers/blob/master/driver_case/2.0/driver_case_test.c

5.后记

本博客主要记录笔者在开发中的一些小总结,包括Linux驱动开发、单片机开发、C语言以及安卓驱动开发。如有技术交流需要,欢迎关注 公众号: “开源519”