|
[导读] 前一篇文章,介绍了如何将一个hello word模块编译进内核或者编译为动态加载内核模块,本篇来介绍一下如何利用Linux驱动模型来完成一个LED灯设备驱动。点一个灯有什么好谈呢?况且Linux下有专门的leds驱动子系统。 在很多嵌入式系统里,有可能需要实现数字开关量输出,比如:
嵌入式Linux一般需求千变万化,也不可能这些需求都有现成设备驱动代码可供使用,所以如何学会完成一个开关量输出设备的驱动,一方面点个灯可以比较快了解如何具体写一个字符类设备驱动,另一方面实际项目中对于开关量输出设备就可以这样干,所以是具有较强的实用价值的。 要完成这样一个开关量输出GPIO的驱动程序,需要梳理梳理下面这些概念:
字符设备是通过文件系统内的设备名称进行访问的,其本质是设备文件系统树的节点。故Linux下设备也是一个文件,Linux下字符设备在/dev目录下。可以在开发板的控制台或者编译的主Linux系统中利用ls -l /dev查看,如下图:
对于ls -l列出的属性,做一个比较细的解析:
细心的朋友或许会发现设备号属性,在有的文件夹下列出来不是这样,这就对了!普通文件夹下是这样:
差别在于一个是文件大小,一个是设备号。 再细心一点的朋友或许还会问,这些/dev下的文件时间属性为神马都相差无几?这是因为/dev设备树节点是在内核启动挂载设备驱动动态生成的,所以时间就是系统开机后按次序生成的,你如不信,不妨重启一下系统在查看一下。
回到设备号,设备号是一个32位无符号整型数,其中:
这怎么理解呢,看下串口类设备就比较清楚了:
主设备号一样证明这些设备共用了一个驱动程序,而次设备号不一样,则对应了不同的串口设备。那么怎么得到设备号呢? /*下列定义位于./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命令来列出这个文件内容:
字符设备由什么关键数据结构进行抽象的呢,来看看:
cdev中与字符设备驱动编程相关两个数据域:
文件操作符是一个庞大的数据结构,常规字符设备驱动一般需要实现下面一些函数指针:
先上代码(可左右滑动显示): #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"); 来总结一下要点:
#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 这样就实现了用户空间驱动底层设备了,实际应用代码就可以这样去访问底层的字符型设备。 总结一下本文总结了简单字符设备的驱动开发的一些要点,以及如何动态加载,在设备文件系统树上创建设备节点,并演示了驱动以及驱动使用的基本要点。 |
微信公众号
手机版