全部博文(395)
分类: 嵌入式
2011-04-19 18:44:39
转载自:
http://blog.sina.com.cn/s/blog_5d2a81780100d989.html这几天一直在整驱动,本人也是新手,才刚刚入门,但对很多东西都是不甚了解。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;
}
}
/* 这个结构是字符设备驱动程序的核心
* 当应用程序操作设备文件时所调用的open、read、write等函数,
* 最终会调用这个结构中指定的对应函数
*/
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.3的release版本,我用3.4.3版本编译busybox,生成了bin、sbin、linuxrc文件,其他文件都是拷贝以前好使的根文件系统的。可是启动文件系统在
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
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范围从2至36,或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函 数的,并执行之。在整个编译和加载驱动程序的过程中要注意的几个要点就是:第一:交叉编译器的版本问题,不同的内核版本需要不同的交叉编译器。第二:开发 板上运行的内核版本和你编译驱动模块的内核版本是否一致,不一致会出现加载不成功,会提示你找不到内核版本。第三:若前面两者都没有错误,你得考虑你的文 件系统的问题,若你的文件系统用较低版本的交叉编译器编译,也有可能导致模块加载不成功。我就是出现这样的问题,花费了我好几天的时间,比较笨啊!还需要 学习东西简直是太多太多啦!!