字符设备驱动程序之LED驱动程序
驱动系统架构:①应用程序APP---调用open("/dev/xxx")、read()、write()
②系统调用接口(system call interface)----根据 SWI 传入的参数值的不同,即发生异常的原因,调用不同的异常处理函数。
③虚拟文件系统(VFS)----(sys_open,sys_read,....) 根据打开的不同设备文件,找到不同的驱动,调用相应的函数。例如,打开的是字符设备,则在内核定义的数组chrdev[ ]中查找,数组的索引是主设备号,内容是 file_operations 结构。
④硬件
问1:怎么写字符设备驱动程序?
答:
1. 写出open()、read()、write()等函数。
static int first_drv_open(struct inode *inode, struct file *file)
{
//printk("first_drv_open\n");
/*
* LED1,LED2,LED4对应GPB5、GPB6、GPB7、GPB8
*/
/* 配置GPB5,6,7,8为输出 */
*gpbcon &= ~((0x3<<(5*2)) | (0x3<<(6*2)) | (0x3<<(7*2)) | (0x3<<(8*2)));
*gpbcon |= ((0x1<<(5*2)) | (0x1<<(6*2)) | (0x1<<(7*2)) | (0x1<<(8*2)));
return 0;
}
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
//printk("first_drv_write\n");
copy_from_user(&val, buf, count); // copy_to_user();
if (val == 1)
{
// 点灯
*gpbdat &= ~((1<<5) | (1<<6) | (1<<7) | (1<<8));
}
else
{
// 灭灯
*gpbdat |= (1<<5) | (1<<6) | (1<<7) | (1<<8);
}
return 0;
}
2. 怎么告诉内核调用的是这些方法?
①定义一个 file_operations 结构,并用以上方法来初始化。
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = first_drv_open,
.write = first_drv_write,
};
②把这个file_operations 结构注册进内核。
major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核
③ 谁来调用这个驱动----驱动的入口
static struct class *firstdrv_class;
static struct class_device *firstdrv_class_dev;
static int first_drv_init(void)
{
major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核,主设备号写0--系统会自动给我们分配一个空缺的主设备号
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
gpbcon = (volatile unsigned long *)ioremap(0x56000010, 16);//ioremap--把物理地址映射成虚拟地址
gpbdat = gpbcon + 1;
return 0;
}
static void first_drv_exit(void)
{
unregister_chrdev(major, "first_drv"); // 卸载
class_device_unregister(firstdrv_class_dev);
class_destroy(firstdrv_class);
iounmap(gpbcon);
}
module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");
以上就实现了最简单的led字符设备驱动。
然后,编写一个简单的Makefile,把这个驱动编译成内核模块:
KERN_DIR = /work/system/linux-2.6.22.6 #指定用哪个linux内核
all:
make -C $(KERN_DIR) M=`pwd` modules #在当前目录下编译成modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += first_drv.o #obj-m 编译成模块
下面来编译和装载这个驱动模块:
#make 现在就可以在目录下看到这个led.ko文件了
#insmod ./led.ko 装载这个驱动
如果要卸载这个驱动,可以用:#rmmod led
问2:应用程序来怎么调用这个驱动?
答:在这个驱动中已经有自动注册设备节点的功能,即通过以下代码实现:
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "led");
成功的话,会在 /sys/class/ 目录下出现一个fistdrv类的目录,并且在这个./firstdrv/ 目录下存在一个led目录,./led目录下有一个dev文件。
通过:#cat dev 可以查看到主/次设备号。
MKDEV(major, 0) 就是通过这些信息来创建设备节点。
下面我们就是通过这个设备节点来操作led硬件,下面ledtest.c是测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
/* firstdrvtest on
* firstdrvtest off
*/
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/xyz", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}
if (argc != 2)
{
printf("Usage :\n");
printf("%s <on|off>\n", argv[0]);
return 0;
}
if (strcmp(argv[1], "on") == 0)
{
val = 1;
}
else
{
val = 0;
}
write(fd, &val, 4);
return 0;
}
问3:为什么我们一执行 insmod 、rmmod 这个设备节点就会自动生成和删除掉呢?
答:这是因为我们在脚本文件:/etc/init.d/reS 中,加入了
echo /sbin/mdev > /proc/sys/kernel/hotplug
它支持热插拔,一插入或拔掉就会自动生成设备节点。