jz2440_简单字符设备LED驱动程序

1. 字符设备驱动程序框架

在这里插入图片描述
在Linux系统上,应用层的应用程序,如App1、App2通过C library(C库)提供的open、write、read的读写操纵接口,间接调用system call的接口。

C库调用system call是通过SWI指令来实现的。SWI是一个异常中断,SWI通过提供的val,从而转到相应的内核接口sys_open、sys_read、sys_write等,去执行对应的函数。

1.1 例子

以点亮一个LED为例:
在这里插入图片描述

  1. 应用程序使用库提供的open 函数打开代表LED的设备文件;

    fd = open("/dev/led", O_RDWR);
    
  2. 库根据open函数传入的参数执行swi指令,这条指令会引起CPU异常,进入内核;

  3. 内核的异常处理函数根据这些参数找到相应的驱动程序,返回一个文件句柄给库,进而返回给应用程序。

    C库返回一个文件描述符给应用程序,实际的文件句柄由C库保存。

  4. 应用程序得到文件描述符,使用库提供的writeioctl函数发出控制命令。

  5. 库根据writeioctl函数传入的参数执行swi指令,这条指令会引起CPU异常,进入内核。

  6. 内核的异常处理函数根据这些参数调用驱动程序的相关函数,点亮LED。

库提供给应用程序的openreadwriteioctlmmap等接口函数被称为系统调用,它们都是设置好相关寄存器后,执行某条指令引发异常进入内核。对于ARM架构的CPU,这条指令为swi

驱动程序从不主动运行,它是被动的:根据应用程序的要求进行初始化,根据应用程序的要求进行读写。驱动程序加载进内核时,只是告诉内核“我在这里,我能做这些工作”,至于这些“工作”何时开始,取决于应用程序。当然,用户也可以写一个基于系统时钟触发的驱动程序,让它自动点亮LED。

2. 编写一个字符设备驱动程序

2.1 驱动程序模块化编写

  1. 需要编写一个注册函数和一个注销函数

    全局变量和宏定义:

    /* GPIO引脚地址 */
    #define S3C2440_GPFCON	0x56000050
    
    /* 设备名 */
    #define DEVICE_NAME		"s3c2440_leds"
    
    static unsigned int major;	/* 主设备号 */
    static struct class *s3c2440_leds_class;		
    static struct class_device *s3c2440_leds_dev;	
    

    file_operation结构体:

    static struct file_operations s3c2440_leds_fops = {
        .owner   =	THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
        .open    =	s3c2440_leds_open,
        .release =	s3c2440_leds_close,
        .write   =	s3c2440_leds_write,
    };
    

    注册函数:

    static int __init s3c2440_leds_init(void)
    {
        /* 注册字符设备
         * 参数为主设备号、设备名字、file_operations结构;
         * 这样,主设备号就和具体的file_operations结构联系起来了,
         * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
         * LED_MAJOR可以设为0,表示由内核自动分配主设备号, 此时反回值就是主设备号
         */
        major = register_chrdev(0, DEVICE_NAME, &s3c2440_leds_fops);	/* 注册驱动程序 */
        if (major < 0) {
    	      printk(DEVICE_NAME " can't register major number\n");
    	      return major;
        }
    	
    	/* 创建逻辑类 */
    	s3c2440_leds_class = class_create(THIS_MODULE, "s3c2440_leds");
    	if (IS_ERR(s3c2440_leds_class)) 
    		return PTR_ERR(s3c2440_leds_class);
    
    	/* 在class目录下创建一个设备,使得mdev可以根据设备信息
    	 * 生成设备节点——/dev/s3c2440_leds 
    	 */
    	s3c2440_leds_dev = class_device_create(s3c2440_leds_class, NULL, 
                                               MKDEV(major, 0), NULL, "s3c2440_leds");
    	if (unlikely(IS_ERR(s3c2440_leds_dev)))
    		return PTR_ERR(s3c2440_leds_dev);
        
       	/* 驱动程序可操作的虚拟地址,映射到GIPO的物理地址映射,
       	 * 从而实现通过指针操作GPIO引脚 
       	 */
    	gpfcon = (volatile unsigned long*)ioremap(S3C2440_GPFCON, 16);
    	gpfdat = gpfcon + 1;
    	gpfup  = gpfdat + 1;
    	
        printk(DEVICE_NAME " initialized\n");
        return 0;
    }
    

    S3C2440_GPFCON的地址和gpfcon、gpfdat、gpfup的寄存器的地址设置,看下面参考第3点的数据手册。

    注销函数:

    static void __exit s3c2440_leds_exit(void)
    {
        /* 卸载驱动程序 */
    	class_device_destroy(s3c2440_leds_class, MKDEV(major, 0));
    	class_destroy(s3c2440_leds_class);
    	unregister_chrdev(major, DEVICE_NAME);
    	iounmap(gpfcon);
    	printk(DEVICE_NAME " unregister\n");
    }
    
  2. 将驱动的加载(注册)函数和卸载(注销)函数,注册进内核(或者说是告知内核)

    /* 这两行指定驱动程序的初始化函数和卸载函数 */
    module_init(s3c2440_leds_init);
    module_exit(s3c2440_leds_exit);
    
  3. 编写openclose函数,该需要配置led的GIPO引脚为输出引脚。结合jz2440的原理图以及s3c2440芯片数据手册。
    在这里插入图片描述
    open函数:

    static int s3c2440_leds_open(struct inode *inode, struct file *file)
    {
    	/* 配置GPIO4\5\6为输出引脚 */
    	*gpfcon &= ~((0x3 << 8) | (0x3 << 10) | (0x3 << 12)); /* 清零 */
    	*gpfcon |=  ((0x1 << 8) | (0x1 << 10) | (0x1 << 12)); /* 设置为输出 */
    	return 0;
    }
    

    close函数:

    static int s3c2440_leds_close(struct inode *inode, struct file *file)
    {
        /* 什么都不干 */
    	return 0;
    }
    
  4. 编写write函数。write函数可以操作点亮LED或者熄灭LED。

    static int s3c2440_leds_write(struct file * file, 
                                  const char __user *buf, size_t count, loff_t *ppos)
    {
    	int val;
    	unsigned long ret;
    	ret = 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;
    }
    

2.2 完整的驱动程序

完整的驱动程序以及Makefile文件:

简单字符设备驱动程序——jz2440V3-C文档类资源-CSDN文库

2.3 测试程序

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

int main(int argc, char **argv)
{
	int fd;
	int val = 1;
	if (argc != 2)
	{
		printf("Usage :\n");
		printf("%s <on|off>\n", argv[0]);
		return 0;
	}
	
	fd = open("/dev/leds", O_RDWR);
	if (fd < 0) 
	{
		printf("can't open /dev/leds!\n");
	}

	if (strcmp(argv[1], "on") == 0) {
		printf("open led");
		val = 1;
	}
	else if (strcmp(argv[1], "off") == 0) {
		printf("close led");
		val = 0;
	} else {
		printf("Usage :\n");
		printf("%s <on|off>\n", argv[0]);
		close(fd);
		return 0;
	}
	
	write(fd, &val, sizeof(val));


	return 0;
}

3. 原理解析

3.1 register_chrdev详解

当我们需要注册字符设备的时候,需要 module_init() 中调用 register_chrdev() 注册。

下面主要介绍接口的实现过程与细节。

内核函数前面添加__ 代表内核级函数。

/*
 *@major: major device number or 0 for dynamic allocation
 *		  主设备号,当用户设置为0时,内核会动态分配一个设备号。
 *@baseminor: first of the requested range of minor numbers
 *			  次设备号,要在一定范围内从0开始
 *@count: the number of minor numbers required
 *		  次设备号的范围
 *@name: name of this range of devices
 *		 设备名称
 *@fops: file operations associated with this devices
 *		 文件系统的接口指针
 */
int __register_chrdev(unsigned int major, 
                      unsigned int baseminor, 
                      unsigned int count, 
                      const char *name,
                      const struct file_operations *fops)
{
    struct char_device_struct *cd;
    struct cdev *cdev;		/* 字符型设备,这个是真正的实用的 */
    int err = -ENOMEM;
    
    /* 检查设备号是否有效,注册设备到全局变量chrdevs[i]中
     * 当用户设置主设备号为0时,内核会动态分配一个设备号 
     */
    cd = __register_chrdev_region(major, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);

    /* 分配一个字符设备结构内存大小,返回这个结构,失败返回空。 */
	cdev = cdev_alloc();
	if (!cdev)
		goto out2;
    /* 设置设备用户,文件操作指针,设备名称。 */ 
	cdev->owner = fops->owner;
    cdev->ops = fops;
    kobject_set_name(&cdev->kobj, "%s", name);
    
    /* 添加设备到系统中module结构体链表中,使之模块立即生效。
     * 此后文件操作,可以正常使用,如果注册失败的话,释放以上的配置。 
     */
    err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
	if (err)
		goto out;
 
	cd->cdev = cdev;
	return major ? 0 : cd->major;
out:
	kobject_put(&cdev->kobj);
out2:
	kfree(__unregister_chrdev_region(cd->major, baseminor, count));
	return err;
}

接口代码分析:

struct char_device_struct *cd:字符设备结构体指针,用于检测存储使用。

static struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int baseminor;
	int minorct;
	char name[64];
	struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

__register_chrdev_region():检查设备号是否有效,注册设备到全局变量chrdevs[i]

内核中有字符设备和块设备表,根据设备类型和主设备号就能找到对应的结构,从而访问结构中的ops操作函数

3.2 理解cdev_add源代码

cdev_add的源代码如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    p->dev = dev;
    p->count = count;
    return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}

kobj_map的源码如下:

int kobj_map(struct kobj_map *domain, 
			 dev_t dev, 
             unsigned long range,
			 struct module *module, 
             kobj_probe_t *probe,
             int (*lock)(dev_t, void *), 
             void *data)
{
    unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
    unsigned index = MAJOR(dev);
    unsigned i;
    struct probe *p;

    if (n > 255)
        n = 255;  // 检测设备号是否在范围内
    
    p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
    if (p == NULL)
        return -ENOMEM;

    for (i = 0; i < n; i++, p++) {   // 设置字符module结构的配置
        p->owner = module;
        p->get = probe;
        p->lock = lock;
        p->dev = dev;
        p->range = range;
        p->data = data;
    }
    
    mutex_lock(domain->lock);
    for (i = 0, p -= n; i < n; i++, p++, index++) {
        struct probe **s = &domain->probes[index % 255]; // 添加到全局模块中
        while (*s && (*s)->range < range)
            s = &(*s)->next;
        p->next = *s;
        *s = p;
    }
    mutex_unlock(domain->lock);

    return 0;
}

3.3 理解设备节点自动创建

为什么填加了class_create(THIS_MODULE, "s3c2440_leds");class_device_create(s3c2440_leds_class, NULL, MKDEV(major, 0), NULL, "leds");就能够自动创建设备节点了呢?

原因是,两者为系统提供了系统信息。

class_create创建了在内核中创建一个逻辑类,即内核会在/sys/class目录下创建相应类名的目录(也就是创建一个s3c2440_leds 目录)。

class_device_create会在/sys/class/s3c2440_leds 的下创建一个leds目录,这个目录里面有三个文件:

# ls -l
-r--r--r--    1 0        0            4096 Jan  1 00:03 dev
lrwxrwxrwx    1 0        0               0 Jan  1 00:03 subsystem -> ../../../class/s3c2440_leds
--w-------    1 0        0            4096 Jan  1 00:03 uevent
  • dev:里面记录的是设备的主设备号和次设备号。
  • subsystem:是一个目录的符号连接,指向当前目录。
  • uevent:是一个只写文件

从上面我们就可以知道通过将设备节点主设备号和次设备号写到dev文件中就可以为内核提供信息。

有了这些信息之后,设备节点是如何被创建的呢?

原因就在/etc/init.d/rcS 脚本文件中:

#!/bin/sh
ifconfig eth0 192.168.1.17

mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

echo /sbin/mdev > /proc/sys/kernel/hotplug 我们将mdev复制到hotplug 程序,当内核中有设备加载或者卸载时就会调用hotplug 程序,由hotplug 进行自动创建和删除设备节点。

4. 测试

Ubuntu:将生成的leds.ko和leds_test拷贝到nfs服务器的目录下:

make
arm-linux-gcc-3.4.5 leds_test.c -o leds_test
cp leds.ko leds_test ~/nfs_rootfs/

jz2440

  1. 挂载文件系统

    mount -t nfs -o intr,nolock,rsize=1024,wsize=1024 192.168.137.3:/home/binwatson/nfs_rootfs /mnt
    
  2. 加载模块以及测试:

    insmod leds.ko
    ./leds_test on  # 开灯
    ./leds_test	off # 关灯
    rmmod leds.ko
    
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值