在linux系统中许多外围设备都被规定为字符设备,诸如按键、触摸屏、重力传感器、LED、光敏传感器等,这些设备都需要字符设备驱动才能正常工作。本章就来实现一个标准的字符设备驱动框架模板,目的是为以后的设备驱动提供标准模板,提高开发效率与代码整洁度。
要想实现一个基础的代码模板,需要考虑到代码的标准化、独立性和可重用性。因此在写代码前需要构思一下字符设备驱动常用到哪些功能。这里列举一下常用到的功能,并一一记录实现的流程及意义。
在实现字符驱动前,首先要做的是搭建字符设备驱动框架,先将固定的字符设备驱动框架搭建起来,然后再在相应的内容中添加相应的代码即可。这种框架大大减轻了开发者在编程中对代码流程设计的压力,同时为了方便以后的重用,这里将驱动通用的信息全部用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");
一篇标准的代码头部需要注释,这些注释主要内容包括公司信息、文件名、作者、版本、描述、修改日志以及其他信息等。
/*
********************************************************************************
* 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 :
********************************************************************************
*/
在驱动调试的时候,经常会使用打印函数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
在驱动编程中,要有面向对象编程思想。当需要定义一个驱动各种信息时,可以将所有相关的信息集合成一个结构体,各种类型信息定义在其结构体下的成员。这样不仅方便编程,而且能够快速理清各个信息变量之间的关系。 如下图,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;
};
驱动编程的入口函数中需要实现许多初始化工作,这些工作都大同小异,如果放到主干中不仅影响代码的可阅读性,同样影响代码的重用性。因此在编程过程中,针对实现特定功能的代码,需要将其模块化封装起来,只将模块入口放入主干之中。这里就列举出,在字符设备驱动编程中,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 data\n");
return NULL;
}
return pdata;
}
以上内容基本上实现了字符驱动需要的常用功能,这里编译并烧录到开发板,测试一下是否能够跑通;这里测试效果主要看,在应用层调用后,能否打印出从设备树获取的节点数据。
设备树代码:
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;
}
效果图:
本次文章主要介绍如何创建一个可重用的字符设备驱动代码模板。虽然看上去代码很少,但是也是经常一个多星期的推敲以及优化。再全局替换driver_case和DRIVER_CASE后,即可成为一个新的字符驱动,没有保留之前的痕迹。如此一来,以后的代码都可以采用此模板。
代码链接:https://github.com/LinuxTaoist/Linux_drivers/blob/master/driver_case/2.0/driver_case_test.c