从点一个灯开始学写Linux字符设备驱动

摘要:
注意事项和星标嵌入客栈及时交付。[指南]上一篇文章介绍了如何将hellowword模块编译到内核或动态加载的内核模块中。本文描述了如何使用Linux驱动程序模型来驱动LED灯设备。此外,Linux还有一个特殊的LED驱动程序子系统。因此,Linux下的设备也是一个文件。Linux下的字符设备位于/dev/目录中。请参阅《˂》一书。

关注、星标嵌入式客栈,精彩及时送达

从点一个灯开始学写Linux字符设备驱动第1张

[导读] 前一篇文章,介绍了如何将一个hello word模块编译进内核或者编译为动态加载内核模块,本篇来介绍一下如何利用Linux驱动模型来完成一个LED灯设备驱动。点一个灯有什么好谈呢?况且Linux下有专门的leds驱动子系统。

点灯有啥好聊呢?

在很多嵌入式系统里,有可能需要实现数字开关量输出,比如:

  • LED状态显示

  • 阀门/继电器控制

  • 蜂鸣器

  • ......

嵌入式Linux一般需求千变万化,也不可能这些需求都有现成设备驱动代码可供使用,所以如何学会完成一个开关量输出设备的驱动,一方面点个灯可以比较快了解如何具体写一个字符类设备驱动,另一方面实际项目中对于开关量输出设备就可以这样干,所以是具有较强的实用价值的。

要完成这样一个开关量输出GPIO的驱动程序,需要梳理梳理下面这些概念:

  • 设备编号

  • 设备挂载

  • 关键数据结构

设备编号

字符设备是通过文件系统内的设备名称进行访问的,其本质是设备文件系统树的节点。故Linux下设备也是一个文件,Linux下字符设备在/dev目录下。可以在开发板的控制台或者编译的主Linux系统中利用ls -l /dev查看,如下图:

从点一个灯开始学写Linux字符设备驱动第2张

对于ls -l列出的属性,做一个比较细的解析:

从点一个灯开始学写Linux字符设备驱动第3张

细心的朋友或许会发现设备号属性,在有的文件夹下列出来不是这样,这就对了!普通文件夹下是这样:

从点一个灯开始学写Linux字符设备驱动第4张

差别在于一个是文件大小,一个是设备号。

再细心一点的朋友或许还会问,这些/dev下的文件时间属性为神马都相差无几?这是因为/dev设备树节点是在内核启动挂载设备驱动动态生成的,所以时间就是系统开机后按次序生成的,你如不信,不妨重启一下系统在查看一下。

从点一个灯开始学写Linux字符设备驱动第5张

常见文件类型:

  • d: directory 文件夹

  • l: link  符号链接

  • p: FIFO pipe 管道文件,可以用mkfifo命令生成创建

  • s: socket 套接字文件

  • c: char 字符型设备文件

  • b: block 块设备文件

  • -:常规文件

回到设备号,设备号是一个32位无符号整型数,其中:

  • 12位用来表示主设备号,用于标识设备对应的驱动程序。

  • 20位用来表示次设备号,用于正确确定设备文件所指的设备。

这怎么理解呢,看下串口类设备就比较清楚了:

从点一个灯开始学写Linux字符设备驱动第6张

主设备号一样证明这些设备共用了一个驱动程序,而次设备号不一样,则对应了不同的串口设备。那么怎么得到设备号呢?

/*下列定义位于./include/linux/types.h */
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

/* 下面宏用于生成主设备号,次设备号       */
/* 下列定义位于./include/linux/Kdev_t.h */
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

使用举例:

/* 主设备号 */
MAJOR(dev_t dev); 
/* 次设备号 */
MINOR(dev_t dev);

设备挂载

为简化问题,本文描述一下动态加载设备驱动模块,暂不考虑设备树。参考<<Linux设备驱动程序>>一书。可参照前文将驱动编译成模块,然后利用下面脚步动态加载模块。由前面描述,知道设备最终需要在/dev目录下生成一个设备文件,那么这个设备文件节点是怎么生成呢,看看下面的脚本:

#!/bin/sh
#-----------------------------------------------------------------------
module="led"
device="led"
mode="664"
group="staff"

# 利用insmod命令加载设备模块
insmod -f $module.ko $* || exit 1
# 获取系统分配的主设备号 
major=`cat /proc/devices | awk "\$2=="$module" {print \$1}"`

# 删除旧节点
rm -f /dev/${device} 

#创建设备文件节点
mknod /dev/${device} c $major 0
#设置设备文件节点属性
chgrp $group /dev/${device}
chmod $mode  /dev/${device}

这里要提一下/proc/devices,这是一个文件记录了字符和块设备的主设备号,以及分配到这些设备号的设备名称。比如使用cat命令来列出这个文件内容:

从点一个灯开始学写Linux字符设备驱动第7张

关键数据结构

字符设备由什么关键数据结构进行抽象的呢,来看看:

  • file_operations定义在./include/linux/fs.h

  • cdev定义在./include/linux/cdev.h

从点一个灯开始学写Linux字符设备驱动第8张

cdev中与字符设备驱动编程相关两个数据域:

  • const struct file_operations *ops;

  • dev_t dev;设备编号

文件操作符是一个庞大的数据结构,常规字符设备驱动一般需要实现下面一些函数指针:

  • read:用来实现从设备中读取数据

  • write:用于实现写入数据到设备

  • ioctl:实现执行设备特定命令的方法

  • open:用实现打开一个设备文件

  • release:当file结构被释放时,将调用这个接口函数

点灯设备

先上代码(可左右滑动显示):

#include <linux/module.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/kernel.h>  /* printk() */
#include <linux/major.h>
#include <linux/cdev.h>
#include <linux/fs.h>      /* everything... */
#include <linux/gpio.h>
#include <asm/uaccess.h>   /* copy_*_user */

/*这里具体参考不同开发板的电路 GPIOC24 */
#define LED_CTRL   (2*32+24)  

static const unsigned int led_pad_cfg = LED_CTRL;

struct t_led_dev{
 struct cdev cdev;
 unsigned char value; 
};

struct t_led_dev  led_dev;
static dev_t led_major;
static dev_t led_minor=0; 

static int led_open(struct inode * inode,struct file * filp)
{ 
 filp->private_data = &led_dev; 
 
 printk ("led is opened!
");          
 return 0;
}

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

static ssize_t led_read(struct file * file, 
                        char __user * buf,
                        size_t count, 
                        loff_t *ppos)
{
 ssize_t ret=1;   
 if(copy_to_user(&(led_dev.value),buf,1))
  return -EFAULT;
 printk ("led is read!
");
 return ret;
}

static ssize_t led_write(struct file * filp, 
                         const char __user *buf,
                         size_t count,loff_t *ppos)
{
 unsigned char value; 
    ssize_t retval = 0;
 if(copy_from_user(&value,buf,1))
  return -EFAULT;
 
 if(value&0x01)
  gpio_set_value(led_pad_cfg, 1);
 else
  gpio_set_value(led_pad_cfg, 0);

    printk ("led is written!
");
 return retval;
}

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

static void led_setup_cdev(struct t_led_dev * dev, int index)
{ 
    /* 初始化字符设备驱动数据域 */
 int err,devno = MKDEV(led_major,led_minor+index);
 cdev_init(&(dev->cdev),&led_fops);
 dev->cdev.owner = THIS_MODULE;
 dev->cdev.ops = &led_fops;
    /* 字符设备注册 */
 err = cdev_add(&(dev->cdev),devno,1);
 if(err)
  printk(KERN_NOTICE "Error %d adding led %d",err,index); 
}

static int led_gpio_init(void)
{
 if (gpio_request(LED_CTRL, "led") < 0) {
  printk("Led request gpio failed
");
  return -1;
 }
 
 printk("Led gpio requested ok
");
 
 gpio_direction_output(LED_CTRL, 1); 
 gpio_set_value(LED_CTRL, 1);
 
 return 0;
}
/* 注销设备 */
void led_cleanup(void)
{ 
 dev_t devno = MKDEV(led_major, led_minor); 
 gpio_set_value(LED_CTRL, 0); 
 gpio_free(LED_CTRL);

 cdev_del(&led_dev.cdev); 
 unregister_chrdev_region(devno, 1);    //注销设备号  
}

/* 注册设备 */
static int led_init(void)
{ 
 int result;
 dev_t dev = MKDEV( led_major, 0 );
 /* 动态分配设备号 */
 result = alloc_chrdev_region(&dev, 0, 1, "led");      
 if(result<0)
  return result; 
 
 led_major = MAJOR(dev);
 
 memset(&led_dev,0,sizeof(struct t_led_dev));
 led_setup_cdev(&led_dev,0);

 led_gpio_init();
 printk ("led device initialised!
"); 
 
 return result;
}

module_init(led_init);
module_exit(led_cleanup);

MODULE_DESCRIPTION("Led device demo");
MODULE_AUTHOR("embinn");
MODULE_LICENSE("GPL");

来总结一下要点:

  • init函数,需要用module_init宏包起来,本例中即为led_init,module_init宏的作用就是选编译为模块或进内核的底层实现,建议刚开始不必深究。一般而言主要实现:

    • 申请分配主设备号alloc_chrdev_region

    • 为特定设备相关数据结构分配内存

    • 将入口函数(open read write等)与字符设备驱动的cdev抽象数据结构关联

    • 将主设备与驱动程序cdev相关联

    • 申请硬件资源,初始化硬件

    • 调用cdev_add注册设备

  • exit函数,一样需要用module_exit包起来,主要负责:

    • 释放硬件资源

    • 调用cdev_del删除设备

    • 调用unregister_chrdev_region注销设备号

  • 用户空间与驱动数据交换

    • copy_to_user,如其名一样,将内核空间数据信息传递到用户空间

    • copy_from_user,如其名一样,从用户空间拷贝数据进内核空间

  • 善用printk进行驱动调试,这是内核打印函数。

  • gpio相关操作函数,这里就不一一列举其作用了,比较容易理解。

测试驱动

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define READ_SIZE 10

int main(int argc, char **argv){
 int fd,count;
 float value;
 unsigned char buf[READ_SIZE+1];
 printf( "Cmd argv[0]:%s,argv[1]:%s,argv[2]:%s
",argv[0],argv[1],argv[2] );
 
 if( argc<2 ){
  printf( "[Usage: test device_name ]
" );
  exit(0);
 }
    if(strlen(argv[2]!=1)
        printf( "Invalid parameter
" );
    
 if(( fd = open(argv[1],O_WRONLY  ))<0){
  printf( "Error:can not open the device: %s
",argv[1] );
  exit(1);
 }
    
    if(argv[2][0] == '1')
        buf[0] = 1;
    else if(argv[2][0] == '0')
        buf[0] = 0;
    else
        printf( "Invalid parameter
" );

 printf("write: %d
",buf[0]);
 if( (count = write( fd, buf ,1 ))<0 ){
  perror("write error.
");
  exit(1);  
 }
 
 close(fd);
 printf("close device %s
",argv[1] );
 return 0;
}

编译成可执行文件,调用前面的脚本加载设备后,在/dev下就可以看到led设备了。比如测试代码编译成ledTest执行文件,则使用下面命令运行测试程序就可以看到led控制效果了:

/*打开led 具体取决电路是高有效还是低有效*/
./ledTest /dev/led 1
./ledTest /dev/led 0

这样就实现了用户空间驱动底层设备了,实际应用代码就可以这样去访问底层的字符型设备。

总结一下

本文总结了简单字符设备的驱动开发的一些要点,以及如何动态加载,在设备文件系统树上创建设备节点,并演示了驱动以及驱动使用的基本要点。

本文辛苦原创,如喜欢请点赞/在看/分享支持,不胜感激!

END

往期精彩推荐,点击即可阅读

▲Linux内核中I2C总线及设备长啥样? 

▲学Linux驱动:应先了解总线驱动模型

▲看思维导图:一文带你学Verilog HDL语言

从点一个灯开始学写Linux字符设备驱动第9张

免责声明:文章转载自《从点一个灯开始学写Linux字符设备驱动》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇腾讯位置服务教你绘制行政区划边界apache 2.4 访问权限配置下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

VMware Workstation 中安装Redhat linux 9.0

这几天在VMware Workstation 汉化版4.52 build-8848中安装Red hat linux 9.0时,碰到了Vmware tools无法安装的情况,在加载Vmware tools时候,碰到了很多麻烦的时候,参考了很多资料,请教过了很多朋友,终于弄明白了,写出来希望能对大家能有所帮助。同时,在这里,我也特别感谢w1ang、letmei...

Gitblit搭建及Git协作开发流程

1. 概述 目前主流的是git作为自己代码管理,但是采用github需要付费才能够使用,如果不付费,代码需要公开。创业团队及小型开发团队都有必要搭建自己的代码服务器,自己摸索需要一定的时间,会赶不及项目进度。在此作者把自己的经验作为文档写下来,方便互联网各位创业者参考,能很快的搭建自己所需要的代码服务器。 同时作者也把最基本的开发流程,代码提交,代码上传,...

Linux centosVMware 命令 lvm、磁盘故障小案例

一、lvm命令 LVM:逻辑分区管理,可基于动态的扩展缩小硬件设备的使用空间,注意:lvm磁盘复杂,由于使用lvm,数据丢失恢复起来有一定风险。概念:pv、VG、lvpv(物理卷,有pp基本单位构成):物理磁盘VG(卷组,由pv基本单位构成):一个或多个物理磁盘(容量)的集合lv(逻辑卷,由lp基本单位构成):VG下划分出来的使用分区(空间)fs:file...

Linux之Ansible

一、安装ansible 环境是centos7.0 主管服务器ip:192.168.175.134,只需安装ansible在本机即可,其余服务器无需安装,ansible通讯是用ssh 首先更换yum源 cd /etc/yum.repos.d/ cp CentOS-Base.repo CentOS-Base.repo.bak wget -O /etc/yu...

关于VScode切换、拉取、推送、合并分支,并解决冲突

一.切换分支输入命令“git branch -a”,查看远程分支输入命令“git checkout dev”,切换到分支dev输入命令“git status”,查看分支状态,比如是否有未保存的修改、未解决的冲突 二.拉取分支git pull:拉取远程的数据同步到自己的目录的命令,前提是没有未保存的代码以及没有未解决的冲突其它拉取方法: 左侧导航栏找到源代码...

Linux服务之httpd基本配置详解

一、基本介绍 1、版本 httpd-1.3 httpd-2.0 httpd-2.2 httpd-2.4 目前为止最新的版本是httpd-2.4.6,但是这里我用的是系统自带的RPM包安装的httpd-2.2.15版本的,最新版本配置可能会有所不同 2、模型: 高度模块化:DSO MPM:Multipath Processing Modu...