linux 写led驱动程序,通过一个LED驱动程序开始学写Linux字符设备驱动

本篇来介绍一下如何利用Linux驱动模型来完成一个LED灯设备驱动。点一个灯有什么好谈呢?况且Linux下有专门的leds驱动子系统。

点灯有啥好聊呢?

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

阀门/继电器控制

蜂鸣器

......

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

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

设备挂载

关键数据结构

设备编号

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

204900710_1_20201019051218616

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

204900710_2_20201019051218725

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

204900710_3_20201019051218897

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

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

204900710_4_20201019051218991常见文件类型:d: directory 文件夹

l: link  符号链接

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

s: socket 套接字文件

c: char 字符型设备文件

b: block 块设备文件

-:常规文件

回到设备号,设备号是一个32位无符号整型数,其中:12位用来表示主设备号,用于标识设备对应的驱动程序。

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

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

204900710_5_20201019051219100

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

/*下列定义位于./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 <

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))

#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))

#define MKDEV(ma,mi) (((ma) <

使用举例:/* 主设备号 */

MAJOR(dev_t dev);

/* 次设备号 */

MINOR(dev_t dev);

设备挂载

为简化问题,本文描述一下动态加载设备驱动模块,暂不考虑设备树。参考<>一书。可参照前文将驱动编译成模块,然后利用下面脚步动态加载模块。由前面描述,知道设备最终需要在/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命令来列出这个文件内容:

204900710_6_20201019051219178

关键数据结构

字符设备由什么关键数据结构进行抽象的呢,来看看:file_operations定义在./include/linux/fs.h

cdev定义在./include/linux/cdev.h204900710_7_20201019051219256

cdev中与字符设备驱动编程相关两个数据域:const struct file_operations *ops;

dev_t dev;设备编号

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

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

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

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

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

点灯设备

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

#include 

#include 

#include   /* printk() */

#include 

#include 

#include       /* everything... */

#include 

#include    /* 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') 

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, 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!\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 

#include 

#include 

#include 

#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

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

总结一下

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值