二、Linux驱动之简单编写字符设备

接下来以点亮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()所做的映射。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值