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

嵌入式客栈 2020-08-16 00:00
关注、星标 嵌入式客栈 ,精彩及时送达

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

点灯有啥好聊呢?

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

  • LED状态显示
  • 阀门/继电器控制
  • 蜂鸣器
  • ......

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

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

  • 设备编号
  • 设备挂载
  • 关键数据结构

设备编号

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

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

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

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

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

常见文件类型:

  • d: directory 文件夹
  • l: link  符号链接
  • p: FIFO pipe 管道文件,可以用mkfifo命令生成创建
  • s: socket 套接字文件
  • c: char 字符型设备文件
  • b: block 块设备文件
  • -:常规文件

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

  • 12位用来表示主设备号,用于标识设备对应的驱动程序。
  • 20位用来表示次设备号,用于正确确定设备文件所指的设备。

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

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

/*下列定义位于./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命令来列出这个文件内容:

关键数据结构

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

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

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!\n");          
 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!\n");
 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!\n");
 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\n");
  return -1;
 }
 
 printk("Led gpio requested ok\n");
 
 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, 01"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!\n"); 
 
 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\n",argv[0],argv[1],argv[2] );
 
 if( argc<2 ){
  printf"[Usage: test device_name ]\n" );
  exit(0);
 }
    if(strlen(argv[2]!=1)
        printf"Invalid parameter\n" );
    
 if(( fd = open(argv[1],O_WRONLY  ))<0){
  printf"Error:can not open the device: %s\n",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\n" );

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

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

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

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

总结一下

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

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

END

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




▲Linux内核中I2C总线及设备长啥样?  
学Linux驱动:应先了解总线驱动模型
看思维导图:一文带你学Verilog HDL语言

嵌入式客栈 欢迎关注嵌入式客栈,主要分享嵌入式Linux系统构建、嵌入式linux驱动开发、单片机技术、FPGA开发、信号处理、工业通讯等技术主题。欢迎关注,一起交流,一起进步!
评论
  • 在智能网联汽车中,各种通信技术如2G/3G/4G/5G、GNSS(全球导航卫星系统)、V2X(车联网通信)等在行业内被广泛使用。这些技术让汽车能够实现紧急呼叫、在线娱乐、导航等多种功能。EMC测试就是为了确保在复杂电磁环境下,汽车的通信系统仍然可以正常工作,保护驾乘者的安全。参考《QCT-基于LTE-V2X直连通信的车载信息交互系统技术要求及试验方法-1》标准10.5电磁兼容试验方法,下面将会从整车功能层面为大家解读V2X整车电磁兼容试验的过程。测试过程揭秘1. 设备准备为了进行电磁兼容试验,技
    北汇信息 2025-01-09 11:24 52浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 82浏览
  • 1月7日-10日,2025年国际消费电子产品展览会(CES 2025)盛大举行,广和通发布Fibocom AI Stack,赋智千行百业端侧应用。Fibocom AI Stack提供集高性能模组、AI工具链、高性能推理引擎、海量模型、支持与服务一体化的端侧AI解决方案,帮助智能设备快速实现AI能力商用。为适应不同端侧场景的应用,AI Stack具备海量端侧AI模型及行业端侧模型,基于不同等级算力的芯片平台或模组,Fibocom AI Stack可将TensorFlow、PyTorch、ONNX、
    物吾悟小通 2025-01-08 18:17 43浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 115浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 151浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 80浏览
  • 在过去十年中,自动驾驶和高级驾驶辅助系统(AD/ADAS)软件与硬件的快速发展对多传感器数据采集的设计需求提出了更高的要求。然而,目前仍缺乏能够高质量集成多传感器数据采集的解决方案。康谋ADTF正是应运而生,它提供了一个广受认可和广泛引用的软件框架,包含模块化的标准化应用程序和工具,旨在为ADAS功能的开发提供一站式体验。一、ADTF的关键之处!无论是奥迪、大众、宝马还是梅赛德斯-奔驰:他们都依赖我们不断发展的ADTF来开发智能驾驶辅助解决方案,直至实现自动驾驶的目标。从新功能的最初构思到批量生
    康谋 2025-01-09 10:04 43浏览
  • 一个真正的质量工程师(QE)必须将一件产品设计的“意图”与系统的可制造性、可服务性以及资源在现实中实现设计和产品的能力结合起来。所以,可以说,这确实是一种工程学科。我们常开玩笑说,质量工程师是工程领域里的「侦探」、「警察」或「律师」,守护神是"墨菲”,信奉的哲学就是「墨菲定律」。(注:墨菲定律是一种启发性原则,常被表述为:任何可能出错的事情最终都会出错。)做质量工程师的,有时会不受欢迎,也会被忽视,甚至可能遭遇主动或被动的阻碍,而一旦出了问题,责任往往就落在质量工程师的头上。虽然质量工程师并不负
    优思学院 2025-01-09 11:48 66浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 105浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 98浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球中空长航时无人机产值达到9009百万美元,2024-2030年期间年复合增长率CAGR为8.0%。 环洋市场咨询机构出版了的【全球中空长航时无人机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球中空长航时无人机总体规模,包括产量、产值、消费量、主要生产地区、主要生产商及市场份额,同时分析中空长航时无人机市场主要驱动因素、阻碍因素、市场机遇、挑战、新产品发布等。报告从中空长航时
    GIRtina 2025-01-09 10:35 39浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2025-01-09 09:58 33浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦