从点一个灯开始学写Linux字符设备驱动
[导读] 前一篇文章,介绍了如何将一个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, 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 <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—
本文分享自微信公众号 - 嵌入式客栈(embInn)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
用这10个小技巧加速Python编程
点击上方“小白学视觉”,选择加"星标"或“置顶” 重磅干货,第一时间送达 编码很有趣,而Python编码更有趣,因为有很多不同的方法可以实现相同的功能。但是,大多数时候都有一些首选的实现方法,有些人将其称为Pythonic。这些Pythonic的共同特征是实现的代码简洁明了。 用Python或任何编码语言进行编程不是像火箭一样的科学,而主要是关于技巧。 如果有意尝试使用Pythonic编码,那么这些技术将很快成为我们工具包的一部分,并且我们会发现在项目中使用它们变得越来越自然。 因此,让我们探索其中的一些简单技巧。 1.负索引 人们喜欢使用序列,因为当我们知道元素的顺序,我们就可以按顺序操作这些元素。在Python中,字符串、元组和列表是最常见的序列数据类型。我们可以使用索引访问单个项目。与其他主流编程语言一样,Python支持基于0的索引,在该索引中,我们在一对方括号内使用零访问第一个元素。此外,我们还可以使用切片对象来检索序列的特定元素,如下面的代码示例所示。 >>> # Positive Indexing... numbers = [1, 2, 3, 4, 5,...
- 下一篇
Mongoose 实现关联查询和踩坑记录
本文源自工作中的一个问题,在使用 Mongoose 做关联查询时发现使用 populate() 方法不能直接关联非 _id 之外的其它字段,在网上搜索时这块的解决方案也并不是很多,在经过一番查阅、测试之后,有两种可行的方案,使用 Mongoose 的 virtual 结合 populate 和 MongoDB 原生提供的 Aggregate 里面的 $lookup 阶段来实现。 文档内嵌与引用模式 MongoDB 是一种文档对象模型,使用起来很灵活,它的文档结构分为内嵌和引用两种类型。 内嵌是把相关联的数据保存在同一个文档内,我们可以用对象或数组的形式来存储,这样好处是我们可以在一个单一操作内完成,可以发送较少的请求到数据库服务端,但是这种内嵌类型也是一种冗余的数据模型,会造成数据的重复,如果很复杂的一对多或多对多的关系,表达起来就很复杂,也要注意内嵌还有一个最大的单条文档记录限制为 16MB。 引用模型是一种规范化的数据模型,通过主外键的方式来关联多个文档之间的引用关系,减少了数据的冗余,在使用这种数据模型中就要用到关联查询,也就是本文我们要讲解的重点。 图片来源:mongoing[...
相关文章
文章评论
共有0条评论来说两句吧...