学习驱动程序不久,看LDD3大概有4章吧,最开始写了个hello world驱动,后来是个面向内存的字符设备驱动,后者让我清楚了量子和量子集的使用,但是没有个真正的,肉眼看得见的设备真是有点不爽,查了些资料,参考了下其他书,打算自己写个LED的linux驱动,学了字符设备驱动的话写这个其实挺简单的。
带系统的驱动跟裸机的驱动可不一样啊,裸机的驱动像单片机那种,你只要把端口设0或者设1就可以控制那些设备,多简单,与带操作系统的驱动程序不同。第一,单片机驱动控制的地址是物理地址,而带操作系统的驱动控制的是虚拟地址,这个在linux上可以很方便的用ioremap函数把物理地址映射为虚拟地址,这个函数在<asm/io.h>声明;第二,带操作系统的驱动,是在内核空间工作的,你要提供API给用户,用户用这些接口来写他们的应用程序,他们不会关心底层是怎么运行的,好比餐厅里的,操作系统的驱动是厨师,应用程序是顾客,这样说可能明白很多。
看下图,是linux的应用程序调用的基本框架,上部黄色的是用户空间,下部蓝色的是内核空间。从上往下,程序员在最上层写程序,操作一些文件,这些文件可以是普通的文件,也可能是设备文件等等,这些操作例如open、read、write、ioctl等,这些操作,到了C库这一层,C库会产生一个swi val中断或者说异常,这个异常就会使应用程序从用户空间进入到内核空间,下面一层叫System all interface,系统调用接口,这一层会调用下面的虚拟文件系统VFS(Virtual File System)的sys_open、sys_write这些函数,这些函数就会根据不同的文件不同的属性,设备文件中还会根据设备号,来寻找特定的驱动程序,这些驱动程序就是我们要写的,里面带led_open、led_write这些函数。
这些虚拟文件系统函数sys_open等是怎么找到我们的驱动程序里的led_open等的呢?其实在虚拟文件系统中有我们的很多种类的文件,普通文件、目录文件、设备文件等,就设备文件中的字符设备举例,它会建立一个字符设备的数组,这些数组以主设备号为下标,貌似从0到255,这些数组里面装的是一些file_operations结构指针,这些file_operations是在驱动程序里面定义的,而file_operations结构里面就包含了我们的驱动的操作函数led_open、led_write等,数组成员不同,file_operations结构就不同,驱动程序就不同,操作的设备就不同......大概是这样
上面是为了帮助大家理清思路,大牛无视就行了,
下面正题了,简单的在linux下的led驱动程序。
开发平台:
ubuntu10.04
测试平台:
TQ2440,操作系统为linux,内核版本linux-2.6.30.4
//leds.c #include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/kdev_t.h> #include <linux/kernel.h> #include <asm/io.h> #define GPBOUT (1<<(5*2)) |(1<<(6*2)) | (1<<(7*2)) | (1<<(8*2)); //设置GPB5/6/7/8为输出 int major = 0; struct cdev* leds_cdev; volatile unsigned long *GPBCON = NULL; volatile unsigned long *GPBDAT = NULL; static int leds_open(struct inode *inode, struct file *filp) { *GPBCON = 0; //控制端口清零 *GPBCON = GPBOUT; return 0; } static int leds_ioctl(struct inode* inode, struct file* filp, unsigned int cmd, unsigned long arg) { if (arg > 4) { printk("deyond the led no\n"); } switch (cmd) { case 1: *GPBDAT |= (1<<(arg + 4)); //灭灯 break; case 0: *GPBDAT &= ~(1<<(arg + 4)); //亮灯 break; default: printk("cmd error\n"); break; } return 0; } struct file_operations leds_fops = { .owner = THIS_MODULE, .open = leds_open, .ioctl = leds_ioctl, }; static int __init leds_init(void) { dev_t dev; leds_cdev = cdev_alloc(); alloc_chrdev_region(&dev, 0, 1, "leds"); major = MAJOR(dev); leds_cdev->ops = &leds_fops; cdev_init(&leds_cdev, &leds_fops); cdev_add(&leds_cdev, dev, 1); GPBCON = (volatile unsigned int *)ioremap(0x56000010, 16); //将物理地址映射到虚拟地址,GPBCON物理地址为0x56000010 GPBDAT = GPBCON + 1; //GPBDAT物理地址为0x56000014 return 0; } static void __exit leds_exit(void) { dev_t dev; dev = MKDEV(major, 0); cdev_del(&leds_cdev); unregister_chrdev_region(dev, 1); } module_init(leds_init); module_exit(leds_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("不做超哥已多年"); MODULE_DESCRIPTION("leds");测试程序:
//leds_test.c #include <stdio.h> #include <linux/types.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #define ON 0 #define OFF 1 int main(int argc, char* argv[]) { int led_no; if (argc != 3) { printf("Usage: %s <led_no> <ON/OFF>\n", argv[0]); exit(0); } int fd; fd = open("/dev/leds", O_RDWR); if (fd < 0) { printf("open error\n"); exit(0); } led_no = strtoul(argv[1], 0, 0); //将字符串转换成无符号长整型数 if (!strcmp(argv[2], "ON")) ioctl(fd, ON, led_no); else if (!strcmp(argv[2], "OFF")) ioctl(fd, OFF, led_no); else exit(0); return 0; }编译驱动模块,在arm板上加载,交叉编译测试程序,在板子上运行成功
命令是./leds_test led_no ON/OFF 如 ./led_test 1 ON就是点亮led1灯
点亮四个灯后效果:
看到可以控制灯亮灭了,你是否想起了当初玩单片机时点led灯时的兴奋呢......哈哈哈哈,好兴奋啊