接下来以点亮LED灯为例编写第一个字符设备驱动。编写驱动分下面几步:
1. 查看原理图、数据手册,了解设备的操作方法;
2. 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;
3. 设计所要实现的操作,比如open、close、read、write 等函数;
4. 测试。
1. 分析设备
本人使用的是JZ2440v3开发板,该开发板CPU使用的是S3C2440A,LED与CPU连接如下:
可以看到3个LED分别连接到2440的GPF4、GPF5、GPF6上面,引脚输出低电平时LED点亮,输出高电平时LED熄灭。
再定位到S3C2440A芯片手册:
从上面资料可知信息:
1. 想要控制开发板上面的3个LED灯,需要控制2440的GPF3、4、5三个引脚为Output模式,所以要配置GPFCON寄存器的位[11:10]=01,[9:8]=01,[7:6]=01。
2. 想要控制LED的亮与灭还需要控制GPFDAT寄存器对应位。
3. GPFCON寄存器的物理地址为0x56000050,GPFDAT寄存器的物理地址为0x56000054。
2. 编写代码
驱动程序led_drv.c如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/cdev.h>
int major; //主设备号
static struct cdev led_cdev;
static struct class *led_class;
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
static int led_open(struct inode *inode, struct file *file)
{
/* 配置GPF4,5,6为输出 */
*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
return 0;
}
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
copy_from_user(&val, buf, count);
if (val == 1)
{
// 点灯
*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
}
else
{
// 灭灯
*gpfdat |= (1<<4) | (1<<5) | (1<<6);
}
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = led_open,
.write = led_write,
};
static int led_init(void)
{
int result;
dev_t devid = MKDEV(major, 0); //从主设备号major,次设备号0得到dev_t类型
if (major)
{
result=register_chrdev_region(devid, 1, "led"); //注册字符设备
}
else
{
result=alloc_chrdev_region(&devid, 0, 1, "led"); //注册字符设备
major = MAJOR(devid); //从dev_t类型得到主设备
}
if(result<0)
return result;
cdev_init(&led_cdev, &led_fops);
cdev_add(&led_cdev, devid, 1);
led_class = class_create(THIS_MODULE, "led");
class_device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); //映射到虚拟地址
gpfdat = gpfcon + 1;
return 0;
}
static void led_exit(void)
{
/*卸载*/
class_device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
cdev_del(&led_cdev);
unregister_chrdev_region(MKDEV(major, 0), 1);
iounmap(gpfcon);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
应用程序led_test.c如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
/* led_text on
* led_text off
*/
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/led", 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;
}
Makefile如下:
KERN_DIR = /work/system/linux-2.6.22.6 //内核目录
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += led_drv.o
3. 测试
内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10
将led_drv.c、led_text.c、Makefile三个文件放入网络文件系统内,在ubuntu该目录下执行:
make
arm-linux-gcc -o led_test led_test.c
在挂载了网络文件系统的开发板上进入相同目录,执行“ls”查看:
装载驱动:
insmod led_drv.ko
运行测试程序:
./led_test on或者./led_test off
就能看到3个led灯打开或者熄灭了。
4. 知识点
4.1 注册字符设备
内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region()、alloc_chrdev_region() 和 register_chrdev()。
(1)register_chrdev 比较老的内核注册的形式 早期的驱动
(2)register_chrdev_region/alloc_chrdev_region + cdev 新的驱动形式,需要配合下面两个函数使用:
cdev_init(&led_cdev, &led_fops);
cdev_add(&led_cdev, devid, 1);
区别: register_chrdev函数是老版本里面的设备号注册函数,可以实现静态和动态注册两种方法,主要是通过给定的主设备号是否为0来进行区别,为0的时候为动态注册。register_chrdev_region以及alloc_chrdev_region就是将上述函数的静态和动态注册设备号进行了拆分的强化。
4.1.1 register_chrdev_region
函数原型:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
from:设备编号。
count:连续编号范围,是这组设备号的大小(也是次设备号的个数)。
name:编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称。
对应卸载函数:
unregister_chrdev_region(MKDEV(major, 0), 1);
4.1.2 alloc_chrdev_region
函数原型:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
dev:获得一个分配到的设备,可以用MAJOR宏和MINOR宏,将主设备号和次设备号提取出来。
baseminor:次设备号的基准,从第几个次设备号开始分配。
count:次设备号的个数。
name:驱动的名字。
alloc_chrdev_region函数,来让内核自动给我们分配设备号。
说明:register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的。更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。自动分配的设备号,我们必须去知道他的主次设备号,否则后面没法去创建他对应的设备文件。
对应卸载函数:
unregister_chrdev_region(MKDEV(major, 0), 1);
4.1.3 register_chrdev
函数原型:
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
对应卸载函数:
int unregister_chrdev(unsigned int major, const char *name);
4.2 创建设备节点
4.2.1 手动创建设备节点
在开发板上执行“insmod led_drv.ko”安装好驱动后,再执行:
#mknod /dev/led c 252 0
这样就创建出了主设备号252,次设备0的/dev/led这个设备节点。
4.2.2 自动创建设备节点
使用下面函数:
led_class = class_create(THIS_MODULE, "led");
class_device_create(led_class, NULL, MKDEV(major, 0), NULL, "led");
调用class_create会在/sys/class/下创建类目录,再调用class_device_create函数来在/dev目录下创建相应的设备节点。效果与手动创建设备节点相同。
对应卸载函数:
class_device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
5. ioremap
几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,CPU对IO端口的编址方式有两种:
a -- I/O 映射方式(I/O-mapped)
典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。
b -- 内存映射方式(Memory-mapped)
RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。
但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。
Linux在io.h头文件中声明了函数ioremap,用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中(这里是内核空间),原型如下:
#define ioremap(cookie,size) __ioremap(cookie,size,0)
__ioremap函数原型为:
void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);
phys_addr:要映射的起始的IO地址
size:要映射的空间的大小
flags:要映射的IO空间和权限有关的标志
该函数返回映射后的内核虚拟地址(3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。在本例中就是通过读写ioremap之后的虚拟地址进行控制io引脚的。
对应函数:
void iounmap(void * addr);
用于取消ioremap()所做的映射。