一只鱼 さんのプロフィールEE小站フォトブログリストその他 ツール ヘルプ

ブログ


5月30日

Linux 2.6字符设备驱动程序样例

写这些东西还真是花时间啊,继续昨天的内容。
 
我写驱动的时候总希望能找到一个样例参考一下,可惜网上的例子基本找不到。还好友善之臂的文档里有些例子,但是说的很不详细,要是直接输入会有很多的编译错误。我的这个例子是一个控制LED的例子,用Linux就控制LED,当然是相当的弱智的哈哈。我用的是S3C2410,LED连接在GPB7~10上,灌电流方式驱动,IO配置寄存器GPBCON的物理地址0x56000010,IO数据寄存器GPBDAT的物理地址0x56000014。程序中的几个关键点,在我昨天的BLOG中有叙述。
 
首先编写一个叫做leds_test.c的文件,内容如下:
 
#include <linux/config.h>
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/init.h>
//Please configure your kernel first to use the following headers, because the directory "asm" is a short cut to your arch's "asm" directory.
//So do the headers in the "hardware" sub directory.
#include <asm/io.h>  //This header is for ioremap(), iounmap().
#include <asm/uaccess.h>  //This header is for get_user(), put_user().
#define NAME "led_test"
MODULE_AUTHOR("Lu Xianzi <cosine@126.com>");  //This line and the following 4 lines can be omitted.
MODULE_DESCRIPTION("LED Test Driver");
MODULE_LICENSE("GPL");
module_param(major, int, 0);
MODULE_PARM_DESC(major, "Major device number");
static int major = 231;  //Define device major
unsigned long * pREG;  //Definition of register base.
static ssize_t led_test_write(struct file *file, const char __user *data, size_t len, loff_t *ppos)
{
 char buf[256];
 size_t i;
 for (i = 0; i < len && i < 254; i++)
  if (get_user(buf[i], data + i))
   return -EFAULT;
   
 buf[i] = '\0';
 printk("LED Test - write: user_data %s\n", buf);
 return (len < 255 ? len : 255);
}
static ssize_t led_test_read(struct file *file, char __user *buf, size_t len, loff_t *ppos)
{
 char rbuf[4];
 size_t i;
 long tmp;
 
 tmp = * (volatile unsigned long *)(pREG + 1);
 rbuf[0] = tmp % 256;
 rbuf[1] = (tmp >> 8) % 256;
 rbuf[2] = (tmp >> 16) % 256;
 rbuf[3] = (tmp >> 24) % 256;
 
 if (len > 4)
  return 0;
 for (i = 0; i < len && i < 4; i++)
  if (put_user(rbuf[i], buf + i))
   return -EFAULT;
 printk("LED Test - read\n");
 return (len < 4 ? len : 4);
}
static int led_test_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
 unsigned long tmp;
 printk("LED Test - ioctl: param %u %lu\n", cmd, arg);
 switch (cmd)
 {
  case 0:
  case 1:
   if (arg > 3)
    return -EINVAL;
   if (!cmd)
    * (volatile unsigned long *)(pREG + 1) |= (0x80 << arg);
   else
    * (volatile unsigned long *)(pREG + 1) &= ~(0x80 << arg);
   tmp = * (volatile unsigned long *)pREG;
   printk("GPBCON = 0x%lx\n", tmp);
   tmp = * (volatile unsigned long *)(pREG + 1);
   printk("GPBDAT = 0x%lx\n", tmp);
   break;
  default:
   return -EINVAL;
 }
 return 1;
}
static int led_test_open(struct inode *inode, struct file *file)
{
 unsigned m = iminor(inode);
 if (m > 63)
  return -EINVAL;
 printk("LED Test driver opened!\n");
 return nonseekable_open(inode, file);
}
static int led_test_release(struct inode *inode, struct file *file)
{
 printk("LED Test driver released!\n");
 return 0;
}

static struct file_operations led_test_fops = {
 .owner   = THIS_MODULE,
 .ioctl   = led_test_ioctl,
 .write   = led_test_write,
 .read    = led_test_read,
 .open    = led_test_open,
 .release = led_test_release,
};
static int __init led_test_init(void)
{
 int ret;
 unsigned long tmp;
 
 printk("LXZ LED Test Driver.\n");
 ret = register_chrdev(major, NAME, &led_test_fops);
 if (ret < 0) {
  printk("Unable to register character device!\n");
  return ret;
 }
 pREG = ioremap(0x56000010, 0x20);
 printk("Virtual addr base = 0x%lx\n", (unsigned long)pREG);
 tmp = * (volatile unsigned long *)pREG;
 printk("GPBCON = 0x%lx\n", tmp);
 tmp = * (volatile unsigned long *)(pREG + 1);
 printk("GPBDAT = 0x%lx\n", tmp);
 printk("Seting LED Test Driver...\n"); 
 * (volatile unsigned long *)pREG = 0x155555;
 * (volatile unsigned long *)(pREG + 1) = 0xfff;
 tmp = * (volatile unsigned long *)pREG;
 printk("GPBCON = 0x%lx\n", tmp);
 tmp = * (volatile unsigned long *)(pREG + 1);
 printk("GPBDAT = 0x%lx\n", tmp);
 printk("LED Test Driver initiated.\n"); 
 return 0;
}
static void __exit led_test_cleanup(void)
{
 int ret;
 
 iounmap(pREG);
 
 ret = unregister_chrdev(major, NAME); 
 if (ret < 0)
  printk("Unable to register character device!\n");
 else
  printk("LED Test Driver unloaded!");
}
module_init(led_test_init);
module_exit(led_test_cleanup);

在驱动程序的目录下建立一个名为“Makefile”的文件,其内容只有一行:
 
obj-m := leds_test.o
 
编译之,我的linux内核存在/home/lxz/linux-2.6.11.7,所以编译命令为
 
make -k -C /home/lxz/linux-2.6.11.7 SUBDIRS=$PWD modules
 
编译后生成几个文件,其中leds_test.ko是我们需要的驱动模块。
 
然后在另外一个目录中编写一个叫做leds.c的文件,其内容如下
 
 #include <stdio.h>
 #include <fcntl.h>
  
 int main(int argc, char **argv)
 {
  int fd;
 int on, led_no;
 char buf[256] = {"1234567890"};
 unsigned long tmp;
 
 if (argc != 3 || sscanf(argv[1], "%d", &led_no) != 1 || sscanf(argv[2], "%d", &on) != 1 || on < 0 || on > 1 || led_no < 0 || led_no >3)
 {
  fprintf(stderr, "Usage: leds led_no 0|1\n");
  exit(1);
 }
 
 fd = open("/dev/leds", O_RDWR);
 if (fd < 0)
 {
  perror("open device leds");
  exit(1);
 }
 
 ioctl(fd, on, led_no);
 write(fd, buf, 10);
 read(fd, buf, 4);
 tmp = buf[0] + (buf[1] << 8) + (buf[2] << 16) + (buf[3] << 24);
 printf("User program read: GPBDAT = 0x%lx\n", tmp);
 close(fd);
 
 return 0;
 }
 
编译之,输入
 
arm-linux-gcc -o leds leds.c
 
然后把生成的leds_test.ko和leds这2个文件拷贝到你的文件系统中,如/home下,启动Linux。之后的过程如下:
 
/ # cd /home
/home # insmod leds_test.ko
Lxz LED Test Driver.
Virtual addr base = 0xc485e010
GPBCON = 0x44555
GPBDAT = 0x540
Seting LED Test Driver...
GPBCON = 0x155555
GPBDAT = 0x7ff
LED Test Driver initiated.
/home # mknod /dev/leds c 231 0
/home # ./leds 0 1
LED Test driver opened!
LED Test - ioctl: param 1 0
GPBCON = 0x155555
GPBDAT = 0x77f
LED Test - write: user_data 1234567890
LED Test - read
User proLED Test driver released!
gram read: GPBDAT = 0x77f
/home #
 
这里有个非常有趣的事情,你会发现内核的printk函数比客户程序的printf函数打印时出现一些混乱,我想应该是因为Linux不是一个实时系统,内核和用户程序分时执行的结果。
 
如果要卸载驱动模块,如下:
 
/ # rmmod leds_test
LED Test Driver unloaded!
/ #
 
5月29日

Linux字符设备驱动(转载)

这篇文章描述了在Linux 2.4下,如何建立一个虚拟的设备,对初学者来说很有帮助。原文地址:http://dev.yesky.com/186/2623186.shtml
 
Linux下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得Windows的设备操作犹如文件一般。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作,如open ()、close ()、read ()、write () 等。
Linux主要将设备分为二类:字符设备和块设备。字符设备是指设备发送和接收数据以字符的形式进行;而块设备则以整个数据缓冲区的形式进行。字符设备的驱动相对比较简单。

  下面我们来假设一个非常简单的虚拟字符设备:这个设备中只有一个4个字节的全局变量int global_var,而这个设备的名字叫做"gobalvar"。对"gobalvar"设备的读写等操作即是对其中全局变量global_var的操作。

  驱动程序是内核的一部分,因此我们需要给其添加模块初始化函数,该函数用来完成对所控设备的初始化工作,并调用register_chrdev() 函数注册字符设备:

static int __init gobalvar_init(void)
{
 if (register_chrdev(MAJOR_NUM, " gobalvar ", &gobalvar_fops))
 {
  //…注册失败
 }
 else
 {
  //…注册成功
 }
}

  其中,register_chrdev函数中的参数MAJOR_NUM为主设备号,"gobalvar"为设备名,gobalvar_fops为包含基本函数入口点的结构体,类型为file_operations。当gobalvar模块被加载时,gobalvar_init被执行,它将调用内核函数register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。

  与模块初始化函数对应的就是模块卸载函数,需要调用register_chrdev()的"反函数" unregister_chrdev():

static void __exit gobalvar_exit(void)
{
 if (unregister_chrdev(MAJOR_NUM, " gobalvar "))
 {
  //…卸载失败
 }
 else
 {
  //…卸载成功
 }
}

  随着内核不断增加新的功能,file_operations结构体已逐渐变得越来越大,但是大多数的驱动程序只是利用了其中的一部分。对于字符设备来说,要提供的主要入口有:open ()、release ()、read ()、write ()、ioctl ()、llseek()、poll()等。

  open()函数 对设备特殊文件进行open()系统调用时,将调用驱动程序的open () 函数:

int (*open)(struct inode * ,struct file *);

  其中参数inode为设备特殊文件的inode (索引结点) 结构的指针,参数file是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用MINOR(inode-> i - rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误) 等;

  release()函数 当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release () 函数:

void (*release) (struct inode * ,struct file *) ;

  release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。

  read()函数 当对设备特殊文件进行read() 系统调用时,将调用驱动程序read() 函数:

ssize_t (*read) (struct file *, char *, size_t, loff_t *);

  用来从设备中读取数据。当该函数指针被赋为NULL 值时,将导致read 系统调用出错并返回-EINVAL("Invalid argument,非法参数")。函数返回非负值表示成功读取的字节数(返回值为"signed size"数据类型,通常就是目标平台上的固有整数类型)。

  globalvar_read函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:

static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
 …
 copy_to_user(buf, &global_var, sizeof(int));
 …
}

  write( ) 函数 当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数:

ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

  向设备发送数据。如果没有这个函数,write 系统调用会向调用程序返回一个-EINVAL。如果返回值非负,则表示成功写入的字节数。
globalvar_write函数中内核空间与用户空间的内存交互需要借助第2节所介绍的函数:

static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{

copy_from_user(&global_var, buf, sizeof(int));

}

  ioctl() 函数 该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:

int (*ioctl) (struct inode * ,struct file * ,unsigned int ,unsigned long);

  unsigned int参数为设备驱动程序要执行的命令的代码,由用户自定义,unsigned long参数为相应的命令提供参数,类型可以是整型、指针等。如果设备不提供ioctl 入口点,则对于任何内核未预先定义的请求,ioctl 系统调用将返回错误(-ENOTTY,"No such ioctl fordevice,该设备无此ioctl 命令")。如果该设备方法返回一个非负值,那么该值会被返回给调用程序以表示调用成功。

  llseek()函数 该函数用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回,原型为:

loff_t (*llseek) (struct file *, loff_t, int);

  poll()函数 poll 方法是poll 和select 这两个系统调用的后端实现,用来查询设备是否可读或可写,或是否处于某种特殊状态,原型为:

unsigned int (*poll) (struct file *, struct poll_table_struct *);
 
设备"gobalvar"的驱动程序的这些函数应分别命名为gobalvar_open、gobalvar_ release、gobalvar_read、gobalvar_write、gobalvar_ioctl,因此设备"gobalvar"的基本入口点结构变量gobalvar_fops 赋值如下:

struct file_operations gobalvar_fops = {
 read: gobalvar_read,
 write: gobalvar_write,
};

  上述代码中对gobalvar_fops的初始化方法并不是标准C所支持的,属于GNU扩展语法。

  完整的globalvar.c文件源代码如下:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");

#define MAJOR_NUM 254 //主设备号

static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);

//初始化字符设备驱动的file_operations结构体
struct file_operations globalvar_fops =
{
 read: globalvar_read, write: globalvar_write,
};
static int global_var = 0; //"globalvar"设备的全局变量

static int __init globalvar_init(void)
{
 int ret;

 //注册设备驱动
 ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
 if (ret)
 {
  printk("globalvar register failure");
 }
 else
 {
  printk("globalvar register success");
 }
 return ret;
}

static void __exit globalvar_exit(void)
{
 int ret;

 //注销设备驱动
 ret = unregister_chrdev(MAJOR_NUM, "globalvar");
 if (ret)
 {
  printk("globalvar unregister failure");
 }
 else
 {
  printk("globalvar unregister success");
 }
}

static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
 //将global_var从内核空间复制到用户空间
 if (copy_to_user(buf, &global_var, sizeof(int)))
 {
  return - EFAULT;
 }
 return sizeof(int);
}

static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{
 //将用户空间的数据复制到内核空间的global_var
 if (copy_from_user(&global_var, buf, sizeof(int)))
 {
  return - EFAULT;
 }
 return sizeof(int);
}

module_init(globalvar_init);
module_exit(globalvar_exit);

  运行:

gcc -D__KERNEL__ -DMODULE -DLINUX -I /usr/local/src/linux2.4/include -c -o globalvar.o globalvar.c

  编译代码,运行:

inmod globalvar.o

  加载globalvar模块,再运行:

cat /proc/devices

  发现其中多出了"254 globalvar"一行,如下图:
 
       
 
接着我们可以运行:

mknod /dev/globalvar c 254 0

  创建设备节点,用户进程通过/dev/globalvar这个路径就可以访问到这个全局变量虚拟设备了。我们写一个用户态的程序globalvartest.c来验证上述设备:

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
main()
{
 int fd, num;
 //打开"/dev/globalvar"
 fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
 if (fd != -1 )
 {
  //初次读globalvar
  read(fd, &num, sizeof(int));
  printf("The globalvar is %d\n", num);

  //写globalvar
  printf("Please input the num written to globalvar\n");
  scanf("%d", &num);
  write(fd, &num, sizeof(int));

  //再次读globalvar
  read(fd, &num, sizeof(int));
  printf("The globalvar is %d\n", num);

  //关闭"/dev/globalvar"
  close(fd);
 }
 else
 {
  printf("Device open failure\n");
 }
}

  编译上述文件:

gcc -o globalvartest.o globalvartest.c

  运行

./globalvartest.o

  可以发现"globalvar"设备可以正确的读写。

Linux 2.6 内核下字符设备(Character Device)驱动编写概述

做人要厚道,转载请注明。有人摘录我BLOG中的话当作自己说的。我认为只要能找出出处的摘录,都会注明来源,以方便阅读的人做进一步的搜索。

花了4天的时间基本整明白了怎么写一个字符设备的驱动,呵呵,我也不知道原理,紧紧是从网上找到了很多文章,加以综合,搞出一个不明原理的HOWTO,趁我的大脑还没有变成浆糊,把这些写出来。内核移植和文件系统构建部分先暂缓。

向大家推荐一个网站,http://www.linuxforum.net/index.php,上面的版主很热心,而且之前的文章很多,可以给大家很大的帮助。

什么是字符设备?我也搞不清楚,哈哈,快要毕业了,速成的结果。目前我弄明白的是:字符设备是以字节为单位来读写的,与字符设备相对应的,块设备是以块为单位来读写的。例如我在总线上扩展的FPGA,它的控制字映射到总线上,每次读写一个字(16位)。

在Linux下和无操作系统情况下,对总线上地址的访问是不同的,Linux提供的内存虚拟内存机制使用户程序无法直接接触到物理内存——这就需要驱动程序这个桥梁。我们先从用户的角度出发,看看怎么使用设备。做人要厚道,下面这段文字来自友善之臂的文档(这个文档可以在他们的主页上下到,http://www.arm9.com.cn/product_more.asp?id=1185

 
Linux操作系统将所有的设备(而不仅是存储器里的文件)全部都看成文件,都纳入文件系统的范畴,都通过文件的操作界面进行操作。这意味着:
 
每一个设备都至少由文件系统的一个文件代表,因而都有一个“文件名”。每个这样的“设备文件”都唯一地确定了系统中地一项设备。应用程序通过设备地文件寻找访问具体地设备,而设备则象普通文件一样受到文件系统访问权限控制机制地保护。
 
应用程序通常可以通过系统调用open()“打开”这个设备文件,建立起与目标设备的连接。代表着该设备的文件节点中记载着建立这种连接所需的信息。对于执行该应用程序的进程而言,建立起的连接就表现为一个已经打开的文件。
 
打开了代表着目标设备的文件,即建立起与设备的连接后,就可以通过read()write()ioctl()等常规的文件操作对目标设备进行操作。
 

目前我的理解是驱动程序所要做的,是将硬件设备(通常是物理地址)与文件操作相关联。当然,我觉得这句话不太全面,以后我学明白原理以后再回来改,FIXME。

驱动程序需要完成的工作有:初始化设备、管理设备、提供文件读写操作的接口、处理设备出现的错误等。

那么驱动程序怎么才能被内核使用呢?或者说怎么样才能把驱动程序加入我的系统中呢?有两种方式:一是把驱动程序直接编译进内核;二是使用模块加载的方式。我采用的是第二种。第一种的细节问题我没有怎么研究,大致上就是写好驱动程序之后,放到内核代码的目录里,修改Makefile文件,与内核一同编译。可以参考《Linux 字符设备驱动程序的设计》,潘俊强、刘莉,杭州应用工程技术学院学报,第12卷第4期,2000年12月。我在百度上搜到这个文章而且下载的,应该还能搜到。
着重介绍第二种方式,因为这可以给调试带来很大的方便,我相信大家的机器不会快到编译一遍有几万个文件的内核和编译一个小文件速度差不多的程度,另外就算你有网络下载内核的开发板,600~800k和十多k还是有差距的,呵呵。

先说下我调试模块的方法,在MTD中留个USER分区,然后用VIVI将USER分区的映像用串口下载到NAND Flash,启动内核,挂载USER分区。如果把驱动程序模块和使用驱动程序模块的测试程序都放在根文件系统里,每次都下载实在比较费事。当然,如果你的板子已经能在Linux下访问网络,那太好了,nfs也好,tftp也好,速度就更快了。

下面开始Step by Step:

  1. 配置内核,到你的内核的目录下,make menuconfig,第三项“Loadable module support”,选上“Enable loadable module support”,其他的随便,我就多选了个“Module unloading”。然后,重新编译内核。
  2. 配置Shell,我用的是Busybox 1.00,到你的Busybox目录下,make menuconfig,找到“Linux Module Utilties”,选上“insmod”、“Support version 2.6.x Linux kernels”、“rmmod”、“Support taintd module checking with new kernels”,这里很奇怪,选上“lsmod”和“modprobe”我的Busybox就不工作了,我觉得是和我用的lib有关。因为我还没有成功编译libs,所以就拉倒,反正insmod,rmmod也够用了。解释下,insmod是挂载模块的命令,rmmod是卸载命令。然后重新编译Busybox,重新构建文件系统映像,我用的是cramfs;哈哈,还没有移植yaffs,为了毕业先将就了。
  3. 烧写新的内核和文件系统,说下,最好备份一个可用的内核和Shell的配置文件,相信来看这篇文章的都不是高手,弄不明白那些选项和编译器、库以及各种头文件的关联关系,也许你修改一下再编译,内核或者Shell就不好使了。
  4. 编写内核驱动程序。这个部分请看我下一篇文章,我也是看了那位高手的HOWTO才会明白其中细节的。
  5. 编写使用驱动程序的测试程序。这是用来调试编写的驱动是不是真的好使,这个部分也请看我后面的文章。

在编写内核驱动程序的时候,需要注意的有这几点(建议你看完下一篇文章之后再回来看这下面的东西):

  • Linux 2.6和2.4内核的字符设备驱动的标准模版不同,网上能够搜到的多半是2.4的东西,需要修改。

2.4内核的驱动模版是
#define MODULE
#include <linux/module.h>
#include <linux/config.h>
static int __init init_module(void)
{    /*      * code here      */}
static void __exit cleanup_module(void)
{    /*      * code here      */}

2.6内核的驱动模版是
#include <linux/module.h>
#include <linux/config.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
static int __init name_of_initialization_routine(void)
{    /* code goes here */    return 0; }
static void __exit name_of_cleanup_routine(void)
{    /* code goes here */              }
module_init(name_of_initialization_routine);
module_exit(name_of_cleanup_routine);

(这个模版来源于ZDNET的一篇文档,但原文有些错误,可以在http://www.itpapers.com.cn/上搜索“Linux 2.6内核移植—硬件驱动篇”)

  • Linux 2.6和2.4内核的字符设备驱动的注册方法不同,而且使用了dev_t这个设备ID的类型,提供了MAJOR(kdev_t dev)和MINOR(kdev_t dev)两个宏来获得设备的ID。

2.6内核的注册方法

A)静态注册
int register_chrdev_region(dev_t from, unsigned count, char *name);
B)动态注册
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, char *name);

之后需要和文件操作结构体联系起来
包含 <linux/cdev.h>,利用struct cdev和file_operations连接
struct cdev *cdev_alloc(void);
oid cdev_init(struct cdev *cdev, struct file_operations *fops);
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
分别为,申请cdev结构,和fops连接,将设备加入到系统中.

删除设备:
void cdev_del(struct cdev *cdev);
只有在cdev_add执行成功才可运行。

2.4内核的注册方法
int register_chrdev(unsigned major, char * name, struct file_operation * fops);

删除设备
int unregister_chrdev(unsigned major, char * name);

哈哈,好像2.4简单啊,为什么2.6要那么做呢?我的想法是这样的话字符设备可以不用和文件操作联系起来而用其他方法访问吧,不对的话不要笑我。但是在2.6内核中,register_chrdev仍然是可以用的。我看了下它的代码,是这样的:

int register_chrdev(unsigned int major, const char *name,
      struct file_operations *fops)
{
 struct char_device_struct *cd;
 struct cdev *cdev;
 char *s;
 int err = -ENOMEM;

 cd = __register_chrdev_region(major, 0, 256, 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);
 for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
  *s = '!';
  
 err = cdev_add(cdev, MKDEV(cd->major, 0), 256);
 if (err)
  goto out;

 cd->cdev = cdev;

 return major ? 0 : cd->major;
out:
 kobject_put(&cdev->kobj);
out2:
 kfree(__unregister_chrdev_region(cd->major, 0, 256));
 return err;
}

整个过程不就是对register_chrdev_region、cdev_add之类函数的调用么,呵呵。

  • 内核地址和物理地址不同。也就是说仅仅知道了设备物理地址是根本无法访问设备的。这里需要在物理地址和虚拟地址之间转换。转换的方法是调用ioremap(addr, size)(原型在asm/io.h中),要问这两个参数什么类型的,我也不知道,实在没时间看那些复杂的宏。反正就是用物理地址和整形大小,能出正确的结果。举个例子:

unsigned long tmp;
unsigned long * pREG;
// Note:   pREG is a pointer which points to a 4-bytes long integer, "pREG + 4" is equal to
//         add 16-bytes offset to the physical address.
//         Added by Lu Xianzi 2007.5.29

pREG = ioremap(0x560000000, 0x20);
tmp = * (volatile unsigned long *) (pREG + 4);

值得一提的是,pREG在驱动程序中应该是一个全局变量,在初始化时因该将其赋值,在模块卸载时应使用iounmap(ptr),来取消地址转换。
这种地址的映射不是简单的将一个地址转化为另一个地址,在内核中有一个表来记录这种映射,超过ioremap所注册范围大小的映射是不成立的,例如,访问0x56000080就不能在pREG的基础上简单的加上偏移(我的理解,FIXME)。

  • 内核地址和用户空间地址不同,内核虚拟出一个环境,用户程序会认为自己好像拥有了整个物理空间。实际上内核空间是从0xc0000000开始的。内核空间和用户空间的指针所指的位置是不一样的,需要交换数据时,应使用get_user(var, ptr)和put_user(var, ptr)来进行数据的交换(原型在asm/uaccess.h中),这在下一篇文章中会提到。

  • 内核和2.6内核模块的编译命令不同。

2.4内核是
#arm-linux-gcc -D__KERNEL__ -I[你的内核的位置]/include -DKBUILD_BASENAME=[你的模块的名字] -DMODULE -c -o [你要生成的模块文件的名字].o [驱动程序源文件的名字].c

2.6内核,必须在你的驱动程序源文件目录下建立一个Makefile,其中写上你要编译的源文件输出,如obj-m := [驱动程序源文件的名字].o,然后输入
# make -k -C [你的内核的位置] SUBDIRS=$PWD modules
2.4内核仅仅生成.o文件,2.6内核的模块扩展名是.ko。

  • 模块的挂载与卸载,很简单

挂载模块
insmod [模块文件的名字].ko
在文件系统中建立节点
mknod /dev/[你想建立的节点名字] c [MAJOR] [MINOR]
其中,MAJOR、MINOR分别是主和子设备号,MAJOR就是你register_chrdev时的那个MAJOR。
接下来就可以用open函数打开设备,用read,write,ioctl等函数访问设备,用close函数关闭设备,这些函数的原型在stdio.h中。

  • 用户程序注意事项

用户程序用open函数打开设备时,如果有写操作,请将open的参数设置为O_RDWR(是Operation的O,不是数字0,这个宏在fcntl.h中),如open("/dev/mydev", O_RDWR);,否则会出现write无法进行的错误。呵呵请恕我白痴,在这个问题上困扰很久。

  • 解释下struct file_operation,内容来自友善之臂的文档,另外《Linux 字符设备驱动程序的设计》中也有提及。
 
在系统内部,I/O设备的存/取通过一组固定的入口点来进行,这组入口点是由每个设备的设备驱动程序提供的。具体到Linux系统,设备驱动程序所提供的这组入口点由一个文件操作结构来向系统进行说明。file_operations结构定义于linux/fs.h文件中,随着内核的不断升级,file_operations结构也越来越大,不同版本的内核会稍有不同。
struct file_operations
{
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int,\
                  unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*readv) (struct file *, const struct iovec *,\
                      unsigned long, loff_t *);
    ssize_t (*writev) (struct file *, const struct iovec *,\
                       unsigned long, loff_t *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t,\
                         loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long,\
                    unsigned long, unsigned long, unsigned long);
};
file_operations结构中的成员全部是函数指针,所以实质上就是函数跳转表。每个进程对设备的操作,都会根据majorminor设备号,转换成对file_operations结构的访问。
常用的操作包括以下几种:
lseek,移动文件指针的位置,只能用于可以随机存取的设备。
read,进行读操作,参数buf为存放读取结果的缓冲区,count为所要读取的数据长度。返回值为负表示读取操作发生错误;否则,返回实际读取的字节数。对于字符型,要求读取的字节数和返回的实际读取字节数都必须是inode-I_blksize的倍数。
write,进行写操作,与read类似。
select,进行选择操作。如果驱动程序没有提供select入口,select操作将会认为已经准备好进行任何的I/O操作。
ioctl,进行读、写以外的其他操作,参数cmd为自定义的命令。
 

简而言之,就是你在文件操作中使用的open,read,write,ioctl,close等,都会调用你在file_operation中定义的相应的函数入口。

5月25日

回到Linux —— 一些ARM Linux开发琐事集锦

经过这么久,终于回到我写这个Blog的原因上了,我又开始搞ARM Linux了。

很后悔之前没有把很多问题写清楚,1年多了,我也忘得差不多了。幸好由于是虚拟机开发,Linux开发环境和我过去在Linux下写的代码在经历了几次系统重装后还健在,这个星期花了3~4天的时间基本上把之前的东西捡起来了。今天主要记录我遗漏的导致这个星期花很多时间整的内容。
 
首先是Source Navigator的安装,我想在本本上也来个Linux,安装Source Navigator的时候就遇到了问题。安装说明里如是说,解压之后:
 
    mkdir snbuild
    cd snbuild
    ../sourcenav-5.20/configure --prefix=/opt/sourcenav
    make
    (become root)
    make install

我在错误的地方(解压之后的文件夹里)建立了snbuild,网上搜了下还有不少人和我犯同样错误,这个会导致说某个文件No rule to make的错误。
写个提示符带目录名的:

    /home/lxz # tar xvzf sourcenav-5.2b2 -C ./
    /home/lxz # mkdir snbuild
    /home/lxz # cd snbuild
    /home/lxz/snbuild # ../sourcenav-5.2b2/configure --prefix=/opt/sourcenav
    /home/lxz/snbuild # make
    (become root)
    /home/lxz/snbuild # make install

还有个错误,说什么“OSF*)”处Unexpected token )之类的,这个好像是Linux各种包安装的问题,另外路径错误好像也会有这个提示,我把Suse 10.1全部安装之后就没有这个问题了。
 
第二个,U-Boot网络下载内核、文件系统使用NFS,Windows下可以安装Omni NFS 5.2。这个软件的共享版支持2个用户连接,用来下载内核是足够了。另外使用Omni NFS的时候请把Windows防火墙禁用。
 
第三,之前我遇到的无法加载根文件系统或者说Busybox无法运行的问题已经找到原因了,有这几个方面共同作用:
1.S3C2410 NAND读写的ECC问题,必须取消ECC,或者使用Bootloader将NAND中的文件系统拷贝到RAM中做ramdisk
2.Busybox工作需要/lib下的共享库文件
3.Busybox静态库(即不使用/lib下的库文件)编译方式有问题
4.根文件系统中必须有“/dev”和“/mnt”,否则无法挂载console
目前我使用我的开发板自带的库文件,以及编译后的Busybox 1.00,成功加载root文件系统(cramfs格式,ramdisk)。解释下,ramdisk、bon、mtdblock是一个类别的东西,cramfs、jffs、yaffs、ext2是一个类别的东西,例如你可以在bon上用cramfs,也可以在ramdisk里用cramfs。
 
第四,mtdblock和bon的关系,目前我的理解是他们都是一种分区方式,当然需要相应的驱动,在vivi中用bon命令可以对NAND进行分区,如bon part add 0 192k 2m,bon命令分区过的NAND需要重新烧写vivi,而mtdblock分区的改变则不用。另外,bon和mtdblock信息是可以同时存在的。
 
第五,U-Boot的NAND WRITE命令在烧写NAND时不会自动擦除NAND Sector(vivi 会自动擦除的),需要手动进行,否则写入数据就不对了。
 
第六,ARM交叉编译器必须解压缩到/usr/local/arm,否则会提示编译器安装错误。
 
时隔一年,再次搞ARM Linux,发现能百度或者Google到的关于遇到的问题的中文文章还是这么少,甚至还有1997年的。希望我的这些琐事可以给你带来些帮助。
5月15日

发现TI 2407A的一个BUG

今天调串口程序,发现我的2407串口程序居然在有定时器中断时无法正常工作。进而研究发现TI的一个小BUG。赶快写下来,不然过几天我又忘记了。
我试过的中断优先级如下:
INT2 --- T3PINT
INT5 --- SCIRX,SCITX
INT1 --- SCIRX,SCITX
INT2 --- T3PINT
以及
INT1 --- SCIRX
INT2 --- T3PINT
INT5 --- SCITX
都出现一个问题——SCITX的中断有可能无法进入。而且如果SCIRX和SCITX处于同一个优先级,整个这个级别的中断(如INT1或INT5)都无法进入;这样造成的后果是SCIRX中断也无法进入,进而触发SCIRXST中的RX Err和OE位,使得SCI模块需要重启才能正常工作。
 
于是分析2407的中断结构,发现出现问题时外设中断请求PIRQ1中的SCITX请求位为1,按TI的Datasheet这个请求是发生中断时被内核自动清除的,而且正常情况下也是如此。于是只好人为在SCITX中断程序里向PIACKR1里的SCITX位写1来响应中断,且最好在中断程序的首尾都加上对这个位的操作。
 
哈哈,原理很抽象啊,说说这个BUG的表象吧,如果你遇到:发送串口数据后,同一优先级的中断,如CAN,SPI,SCI发送,PDPINTA/B,ADCINT(高优先模式),XINT1/2等这些中断无法响应,就是这个BUG造成的。
 
当然,这可能只是一批芯片的问题,我遇到问题的芯片的DINR = 0x0521,芯片型号批号TMS320LF2407APGEA CA-66A3TKW G4。
解决方法举例(不是100%的解决):
void interrupt INT?_handler()
{
    PIACKR1 = 0x0200;
    // 你的中断服务代码
    PIACKR1 = 0x0200;
}
当然别忘了清除相应的IFR位。
 
这样做仍然有非常低的概率出现中断无法响应的情况,请注意!以我的串口程序为例,以115200波特率传输,T3PR为20ms,目前测试到结果是,传输字节数大约500kb就会出一次错。