Chinaunix首页 | 论坛 | 博客
  • 博客访问: 260208
  • 博文数量: 49
  • 博客积分: 1400
  • 博客等级: 上尉
  • 技术积分: 540
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-08 10:33
文章分类

全部博文(49)

文章存档

2010年(2)

2009年(30)

2008年(17)

我的朋友

分类: LINUX

2009-04-05 20:10:12

这几天一直在整驱动,本人也是新手,才刚刚入门,但对很多东西都是不甚了解。Bootloader用的是U-boot,内核用的是2.6.15.1,文件系统用的是busybox制作而成的。整个linux系统在开发板上运行正常。我的目的是写个简单的Led驱动,在应用程序中可以控制Led灯的亮灭。由于是第一次接触驱动这块,我想把整个的驱动开发流程熟悉一遍,以后就是驱动编程的事了,这是本人的愚见。Led源程序是参照韦东山大哥编写的《嵌入式Linux应用开发完全手册》的第十九章简单的字符设备驱动程序里面的驱动程序。个人觉得这本书很不错,第一版的印刷错误比较多,但还是值得珍藏的一本。源程序如下:

 

#include

#include

#include

#include

#include

#include

#include

#include

 

#define DEVICE_NAME     "leds"  /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */

#define LED_MAJOR       231     /* 主设备号 */

 

/* 应用程序执行ioctl(fd, cmd, arg)时的第2个参数 */

#define IOCTL_LED_ON    0

#define IOCTL_LED_OFF   1

 

/* 用来指定LED所用的GPIO引脚 */

static unsigned long led_table [] = {

    S3C2410_GPB5,

    S3C2410_GPB6,

    S3C2410_GPB7,

    S3C2410_GPB8,

};

 

/* 用来指定GPIO引脚的功能:输出 */

static unsigned int led_cfg_table [] = {

    S3C2410_GPB5_OUTP,

    S3C2410_GPB6_OUTP,

    S3C2410_GPB7_OUTP,

    S3C2410_GPB8_OUTP,

};

 

/* 应用程序对设备文件/dev/leds执行open(...)时,

 * 就会调用s3c24xx_leds_open函数

 */

static int s3c24xx_leds_open(struct inode *inode, struct file *file)

{

    int i;

   

    for (i = 0; i < 4; i++) {

        // 设置GPIO引脚的功能:本驱动中LED所涉及的GPIO引脚设为输出功能

        s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]);

    }

    return 0;

}

 

/* 应用程序对设备文件/dev/leds执行ioclt(...)时,

 * 就会调用s3c24xx_leds_ioctl函数

 */

static int s3c24xx_leds_ioctl(

    struct inode *inode,

    struct file *file,

    unsigned int cmd,

    unsigned long arg)

{

    if (arg > 4) {

        return -EINVAL;

    }

   

    switch(cmd) {

    case IOCTL_LED_ON:

        // 设置指定引脚的输出电平为0

        s3c2410_gpio_setpin(led_table[arg], 0);

        return 0;

 

    case IOCTL_LED_OFF:

        // 设置指定引脚的输出电平为1

        s3c2410_gpio_setpin(led_table[arg], 1);

        return 0;

 

    default:

        return -EINVAL;

    }

}

 

/* 这个结构是字符设备驱动程序的核心

 * 当应用程序操作设备文件时所调用的openreadwrite等函数,

 * 最终会调用这个结构中指定的对应函数

 */

static struct file_operations s3c24xx_leds_fops = {

    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */

    .open   =   s3c24xx_leds_open,    

    .ioctl  =   s3c24xx_leds_ioctl,

};

 

/*

 * 执行“insmod s3c24xx_leds.ko”命令时就会调用这个函数

 */

static int __init s3c24xx_leds_init(void)

{

    int ret;

 

    /* 注册字符设备驱动程序

     * 参数为主设备号、设备名字、file_operations结构;

     * 这样,主设备号就和具体的file_operations结构联系起来了,

     * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数

     * LED_MAJOR可以设为0,表示由内核自动分配主设备号

     */

    ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops);

    if (ret < 0) {

      printk(DEVICE_NAME " can't register major number\n");

      return ret;

    }

   

    printk(DEVICE_NAME " initialized\n");

    return 0;

}

 

/*

 * 执行”rmmod s3c24xx_leds.ko”命令时就会调用这个函数

 */

static void __exit s3c24xx_leds_exit(void)

{

    /* 卸载驱动程序 */

    unregister_chrdev(LED_MAJOR, DEVICE_NAME);

}

 

/* 这两行指定驱动程序的初始化函数和卸载函数 */

module_init(s3c24xx_leds_init);

module_exit(s3c24xx_leds_exit);

 

/* 描述驱动程序的一些信息,不是必须的 */

MODULE_AUTHOR("");             // 驱动程序的作者

MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver");   // 一些描述信息

MODULE_LICENSE("GPL");  

 

编写好了源程序,怎么把驱动加载进内核呢?(在此要说明的是,要注意开发板运行的内核和你编译驱动程序的内核版本要一致,否则要出错。比如我虚拟机用的是2.6.24.19版本,而我开发板的内核版本用的是2.6.15.1,所以我们编译内核的时候不能用虚拟机的内核源代码,我们可以把2.6.15.1的源代码放在虚拟机文件系统里,把驱动程序放在相应目录下编译即可。如我把它存放在:/home/yuaf/linux-2.6.15.1目录下。)

网上大概有三种办法:

第一种:把驱动程序放在内核源代码驱动程序相关目录下,如把上面的程序保存为s3c24xx_leds.c,由于Led属于字符设备,就可以把s3c24xx_leds.c放在 /home/yuaf/linux-2.6.15.1/drivers/char/目录下,之后修改Makefile,在文件中添加

obj-m  += s3c24xx_leds.o,然后回到 /home/yuaf/linux-2.6.15.1目录下,执行 make modules,就可以生成模块 drivers/char/s3c24xx_leds.ko.

第二种:

写一个bash文件,放在和驱动程序同一目录下。内容如下:

#!/bin/bash

 

arm-linux-gcc –D__KERNEL__ -I/home/yuaf/linux-2.6.15.1/include –DKBUILD_BASENAME=

s3c24xx_leds –c –o s3c24xx_leds.o s3c24xx_leds.c

运行此文件就可生成模块文件。

还需注意的是,若你的交叉编译环境路径不是对整个系统起作用的话,你可以指定你的交叉编译路径。如/usr/local/arm/gcc-3.4.5-glibc-2.3.6/bin/arm-linux-gcc,若不想指定路径的话,你得修改环境变量路径,/etc/environment文件里,在文件的后面添加交叉编译工具的路径就行。

第三种:

写一个Makefile文件,放在和驱动程序同一目录下。然后make就行。内容如下:

 

obj-m +=s3c24xx_leds.o

KDIR :=/home/yuaf/linux-2.6.15.1

PWD :=$(shell pwd)

 

default:

       $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

知道了怎么加载模块,接下来的是编译模块了,最开始我用的是第三种方法。Make一下,出现一大堆的错误,说这样没有定义,那样没有申明的。那肯定是驱动程序的头文件没有找到所致,我又回去看定义头文件也没有错啊,在linux内核目录下也找到了相应的文件。比较郁闷,之后我用了第一种办法,出现的效果和用第三种办法一样。没有办法,Google了一下,有人说重新把内核编译一下。我照着做了,还重新配置了一下内核,之后再编译,呵呵,还真好使,以前的错误全没了,在 driver/char/目录下就可以看到 s3c24xx_leds.ko文件了。

 

有了 s3c24xx_leds.ko文件,就把它放在nfs文件系统里,在minicom 里执行

Insmod s3c24xx_leds.ko,出现

insmod:Couldn’t find the kernel version the module was compiled for

不能找到编译模块的内核版本!!我用 modinfo s3c24xx_leds.ko查看模块信息,

vermagic:       2.6.15.1 ARMv4 gcc-3.4

在开发板上 uname –a 查看开发板的内核版本:

Linux lyt 2.6.15.1 #12 Fri Apr 6 13:34:56 CST 2007 armv4tl unknown

这两者的内核版本明显是一致的嘛!!这个郁闷的,这个问题困扰了我好几天,我几乎把google和百度上所有关于此问题的帖子、文章都看了,都没有找出解决的问题来。最后,我换了个文件系统,再加载的时候,出现

insmod: Not configured to support old kernels

google了一下,这是由于文件系统是由编译器2.95.3编译的。而模块和文件系统都是用比较高版本的编译器编译的。解决方法是:重新制作文件系统,用较高版本的编译器编译,我编译内核用的是3.4.3release版本,我用3.4.3版本编译busybox,生成了binsbinlinuxrc文件,其他文件都是拷贝以前好使的根文件系统的。可是启动文件系统在

Freeing init memory 卡壳了。就停在那儿了,我换了个3.4.5版本的再编译就好使了。

这样进入文件系统,加载模块好使了,加载上了。

lsmod查看输出如下:

s3c24xx_leds 2080 0 - Live 0xbf000000

最后,就是编写应用程序,源程序如下:

 

#include

#include

#include

#include

 

#define IOCTL_LED_ON    0

#define IOCTL_LED_OFF   1

 

void usage(char *exename)

{

    printf("Usage:\n");

    printf("    %s \n", exename);

    printf("    led_no = 1, 2\n");

}

 

int main(int argc, char **argv)

{

    unsigned int led_no;

    int fd = -1;

   

    if (argc != 3)

        goto err;

       

    fd = open("/dev/leds", 0);  // 打开设备

    if (fd < 0) {

        printf("Can't open /dev/leds\n");

        return -1;

    }

   

    led_no = strtoul(argv[1], 0, 0) - 1;    // 操作哪个LED

    if (led_no > 3)

        goto err;

   

    if (!strcmp(argv[2], "on")) {

        ioctl(fd, IOCTL_LED_ON, led_no);    // 点亮它

    } else if (!strcmp(argv[2], "off")) {

        ioctl(fd, IOCTL_LED_OFF, led_no);   // 熄灭它

    } else {

        goto err;

    }

   

    close(fd);

    return 0;

   

err:

    if (fd > 0)

        close(fd);

    usage(argv[0]);

    return -1;

}

其中 strtoul(argv[1],0,0)表示的是把argv[1]按十进制转换,具体的说明见下面:

 

定义函数
      unsigned long int strtoul(const char *nptr,char **endptr,int base);
函数说明
      strtoul()
会将参数nptr字符串根据参数base来转换成无符号的长整型数。 参数base范围从236,或0。参数base代表采用的进制方式,如base值为10则采用10进制,若base值为16则采用16进制数等。当 base值为0时则是采用10进制做转换,但遇到如'0x'前置字符则会使用16进制做转换。一开始strtoul()会扫描参数nptr字符串,跳过前 面的空格字符串,直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时('')结束转换,并将结果返回。若参数endptr不为NULL,则 会将遇到不合条件而终止的nptr中的字符指针由endptr返回。
返回值
     
返回转换后的长整型数,否则返回ERANGE并将错误代码存入errno中。
附加说明
      ERANGE
指定的转换字符串超出合法范围.

最后就是交叉编译应用程序,把可执行文件放在nfs文件系统里,按照说明的用法运行程序,就可以看到开发板上的Led的亮灭了。

如:led_test 1 on,led灯就亮了。

 

总结:第一次接触驱动,对驱动程序了解更深了一步。驱动程序是在内核态运行的,而我们的应用程序是在用户态下运行的。编写驱动程序,其实也就是填充file_operations里面的成员函数,常用的用 open,read,write,ioctl等函数,还要实现模块的初始化和卸载模块时执行的函数,之后就是把写的应用程序注册到file_operation结构体里。而应用程序调用 open,read,ioctl等函数,也就是通过主设备号和次设备号找到相应的file_operations中的open,read,ioctl函数的,并执行之。在整个编译和加载驱动程序的过程中要注意的几个要点就是:第一:交叉编译器的版本问题,不同的内核版本需要不同的交叉编译器。第二:开发板上运行的内核版本和你编译驱动模块的内核版本是否一致,不一致会出现加载不成功,会提示你找不到内核版本。第三:若前面两者都没有错误,你得考虑你的文件系统的问题,若你的文件系统用较低版本的交叉编译器编译,也有可能导致模块加载不成功。我就是出现这样的问题,花费了我好几天的时间,比较笨啊!还需要学习东西简直是太多太多啦!!

 

阅读(2011) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~