最近在做Linux内核移植,总体的感觉是这样的,想要彻底的阅读Linux内核代码几乎是不可能的,至少这还不是嵌入式学期初期的重要任务。内核代码解压后有250M左右,据统计,有400多万行,而且涉及到了软件和硬件两方面的诸多知识,凭一人之力在短时间内阅读Linux内核代码是根本不可能的,强行阅读可能会打消我们嵌入式学习的积极性,最后甚至可能放弃嵌入式学习,如果真的想阅读内核代码来提高自己水平的话可以等熟练掌握嵌入式以后再回过头来阅读,这样理解也会更深刻,更透彻。
我认为Linux内核移植的初期阶段应该将重点放在分析内核设备驱动上。实际上,Linux内核的移植就是设备驱动的移植,内核本身不会直接访问硬件,是通过驱动程序来间接控制硬件的,而其他的高级功能如内存管理,进程管理等是通用的,无需做其他配置,所以我们只需要配置相关的驱动即可实现Linux内核移植。驱动移植的关键在于了解在驱动的结构,本文将以Nand驱动为例,分析Linux内核的驱动结构。
在分析驱动结构之前,还需要了解下内核识别设备的方式,内核通过驱动程序识别设备的方法有两种,一种是驱动程序本身带有设备信息,比如开始地址、中断号等,加载驱动时就可以根据驱动中的信息来识别设备;另一种是驱动程序本身没有设备信息,但是内核中已经根据其他方式确定了很多设备信息,加载驱动时将驱动程序与这些设备逐个比较,确定两者是否匹配,如果匹配就可以使用该驱动来识别设备了。内核常采用的是第二种方式,这样方式可将各种设备集中在一个文件中管理,当开发板的配置改变时便于修改代码。对应的,内核文件include/linux/platform_device.h中定义了两个结构,一个是platform_device,用来描述设备信息,一个是platform_driver,用来描述驱动信息,内核启动后首先构造链表将plartfrom_device结构组织起来得到一个设备链表,当加载某个驱动时根据platform_driver提供的信息与设备链表一一进行匹配,这就是内核设备识别的大体过程,具体的过程比这复杂很多,这里不做过多研究。下面我们开始分析Linux内核的Nand驱动。
这里以Linux内核的3.5.3中默认的mini2440开发板为例,首先定位到arm/arm/mach-s3c24xx/mach-mini2440.c,然后找到如下结构:
static struct platform_device *mini2440_devices[] __initdata = {显然,这里就是内核需要的设备列表,通过后面的mini2440_init函数中的
&s3c_device_ohci,
&s3c_device_wdt,
&s3c_device_i2c0,
&s3c_device_rtc,
&s3c_device_usbgadget,
&mini2440_device_eth,
&mini2440_led1,
&mini2440_led2,
&mini2440_led3,
&mini2440_led4,
&mini2440_button_device,
&s3c_device_nand,
&s3c_device_sdi,
&s3c_device_iis,
&uda1340_codec,
&mini2440_audio,
&samsung_asoc_dma,
};
platform_add_devices(mini2440_devices, ARRAY_SIZE(mini2440_devices));注册到内核,然后由内核进行管理,显然,跟我们分析的Nand相关的就是s3c_device_nand,这就代表我们开发版上的Nand flash,我们先定位到它的定义,在arch/arm/plat-samsung/devs.c中有如下代码
static struct resource s3c_nand_resource[] = { [0] = DEFINE_RES_MEM(S3C_PA_NAND, SZ_1M),};struct platform_device s3c_device_nand = { .name = "s3c2410-nand", .id = -1, .num_resources = ARRAY_SIZE(s3c_nand_resource), .resource = s3c_nand_resource,};第二个 结构就是s3c_device_nand的定义,之所以带上第一个结构是因为定义s3c_device_nand时用到了s3c_nand_resource,我们先看一下s3c_device_nand的定义,s3c_device_nand只明确定义了Nand设备的名称和设备ID,并没有给出具体的寄存器信息,加上s3c_nand_resource的名字带有资源的意思,因此我们断定,寄存器信息应该在s3c_nand_resource中,从s3c_nand_resource的定义中我们只能看到很少的信息,要想了解具体信息需要看一下struct resource和宏DEFINE_RES_MEM的定义及
struct resource {这里 可以看到,struct resource中定义了起始,结束,名字等信息,我们再来看一下DEFINE_RES_MEM的定义
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
#define DEFINE_RES_NAMED(_start, _size, _name, _flags) \ { \ .start = (_start), \ .end = (_start) + (_size) - 1, \ .name = (_name), \ .flags = (_flags), \ }#define DEFINE_RES_MEM_NAMED(_start, _size, _name) \ DEFINE_RES_NAMED((_start), (_size), (_name), IORESOURCE_MEM)#define DEFINE_RES_MEM(_start, _size) \ DEFINE_RES_MEM_NAMED((_start), (_size), NULL)我这里整合了一下上面的信息,将相关的宏都做了一下追踪,因此,s3c_nand_resource的实际定义为
{ .start = (S3C_PA_NAND), .end = (S3C_PA_NAND) + (SZ_1M) - 1, .name = (NULL), .flags = (IORESOURCE_MEM), }追踪可知,S3C_PA_NAND定义如下
#define S3C2410_PA_NAND (0x4E000000)#define S3C24XX_PA_NAND S3C2410_PA_NAND#define S3C_PA_NAND S3C24XX_PA_NAND
也就是说,S3C_PA_NAND是Nand flash寄存器首地址,而SZ_1M明显是个长度,因此,这里的resource实际上是Nand flash寄存器首地址跟接下来的1M空间,可是,Nand的寄存器并没有那么多,这又是为什么呢?这些信息有什么用又在哪里用到了呢?答案很简单,这肯定是给驱动程序使用的了,带着这个疑问我们继续分析代码。定位到/drivers/mtd/nand/s3c2410.c,浏览代码可以看到驱动结构定义
static struct platform_driver s3c24xx_nand_driver = {可以看到,这里指定了结构中的各种操作的函数指针,从名字上可以看出probe是加载驱动程序后执行的第一个函数,remove是移除驱动前最后执行的函数,suspend是挂起操作,等等。先不着急分析这些函数,先来看看内核是如何加载驱动的,s3c24xx_nand_driver又是如何注册到内核的。往下浏览代码可以看到
.probe = s3c24xx_nand_probe,
.remove = s3c24xx_nand_remove,
.suspend = s3c24xx_nand_suspend,
.resume = s3c24xx_nand_resume,
.id_table = s3c24xx_driver_ids,
.driver = {
.name = "s3c24xx-nand",
.owner = THIS_MODULE,
},
};
static int __init s3c2410_nand_init(void){ printk("S3C24XX NAND Driver, (c) 2004 Simtec Electronics\n"); return platform_driver_register(&s3c24xx_nand_driver);}static void __exit s3c2410_nand_exit(void){ platform_driver_unregister(&s3c24xx_nand_driver);}module_init(s3c2410_nand_init);module_exit(s3c2410_nand_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Ben Dooks <ben@simtec.co.uk>");MODULE_DESCRIPTION("S3C24XX MTD NAND driver");显然,加载该驱动时s3c2410_nand_init函数将s3c24xx_nand_driver注册到了内核,卸载该驱动时s3c2410_nand_exit将s3c24xx_nand_driver注销,但是这两个函数也不过是两个普通函数,内核如何知道加载驱动时运行s3c2410_nand_init,卸载驱动时运行s3c2410_nand_exit呢?下面的module_init和module_exit解决了这个问题,它们分别告诉内核驱动程序的入口和出口。至于下面的MODULE_LICENSE指定了内核的权限协议,这里指定内核为GPL协议的,只有符合这个协议才能调用这个协议内的函数,因此是驱动程序必须的部分,剩下的两行是驱动的作者和描述,无关紧要,可以没有。现在我们明白了内核如何加载驱动了,我们再去分析probe函数,往上浏览代码可以找到
static int s3c24xx_nand_probe(struct platform_device *pdev){ struct s3c2410_platform_nand *plat = to_nand_plat(pdev); enum s3c_cpu_type cpu_type; struct s3c2410_nand_info *info; struct s3c2410_nand_mtd *nmtd; struct s3c2410_nand_set *sets; struct resource *res; int err = 0; int size; int nr_sets; int setno; cpu_type = platform_get_device_id(pdev)->driver_data; pr_debug("s3c2410_nand_probe(%p)\n", pdev); info = kzalloc(sizeof(*info), GFP_KERNEL); if (info == NULL) { dev_err(&pdev->dev, "no memory for flash info\n"); err = -ENOMEM; goto exit_error; } platform_set_drvdata(pdev, info); spin_lock_init(&info->controller.lock); init_waitqueue_head(&info->controller.wq); /* get the clock source and enable it */ info->clk = clk_get(&pdev->dev, "nand"); if (IS_ERR(info->clk)) { dev_err(&pdev->dev, "failed to get clock\n"); err = -ENOENT; goto exit_error; } s3c2410_nand_clk_set_state(info, CLOCK_ENABLE); /* allocate and map the resource */ /* currently we assume we have the one resource */ res = pdev->resource; size = resource_size(res); info->area = request_mem_region(res->start, size, pdev->name); if (info->area == NULL) { dev_err(&pdev->dev, "cannot reserve register region\n"); err = -ENOENT; goto exit_error; } info->device = &pdev->dev; info->platform = plat; info->regs = ioremap(res->start, size); info->cpu_type = cpu_type; if (info->regs == NULL) { dev_err(&pdev->dev, "cannot reserve register region\n"); err = -EIO; goto exit_error; } dev_dbg(&pdev->dev, "mapped registers at %p\n", info->regs); /* initialise the hardware */ err = s3c2410_nand_inithw(info); if (err != 0) goto exit_error; sets = (plat != NULL) ? plat->sets : NULL; nr_sets = (plat != NULL) ? plat->nr_sets : 1; info->mtd_count = nr_sets; /* allocate our information */ size = nr_sets * sizeof(*info->mtds); info->mtds = kzalloc(size, GFP_KERNEL); if (info->mtds == NULL) { dev_err(&pdev->dev, "failed to allocate mtd storage\n"); err = -ENOMEM; goto exit_error; } /* initialise all possible chips */ nmtd = info->mtds; for (setno = 0; setno < nr_sets; setno++, nmtd++) { pr_debug("initialising set %d (%p, info %p)\n", setno, nmtd, info); s3c2410_nand_init_chip(info, nmtd, sets); nmtd->scan_res = nand_scan_ident(&nmtd->mtd, (sets) ? sets->nr_chips : 1, NULL); if (nmtd->scan_res == 0) { s3c2410_nand_update_chip(info, nmtd); nand_scan_tail(&nmtd->mtd); s3c2410_nand_add_partition(info, nmtd, sets); } if (sets != NULL) sets++; } err = s3c2410_nand_cpufreq_register(info); if (err < 0) { dev_err(&pdev->dev, "failed to init cpufreq support\n"); goto exit_error; } if (allow_clk_suspend(info)) { dev_info(&pdev->dev, "clock idle support enabled\n"); s3c2410_nand_clk_set_state(info, CLOCK_SUSPEND); } pr_debug("initialised ok\n"); return 0; exit_error: s3c24xx_nand_remove(pdev); if (err == 0) err = -EINVAL; return err;}对于我们的Nand驱动来说,调用这个函数的参数当然是s3c_device_nand,阅读代码就可以知道前面定义每个变量的原理了。我看到函数开头定义的res就想到了我们前面定义的s3c_nand_resource,往下看能看到
res = pdev->resource; size = resource_size(res);也就是说,这里引用了我们前面定义的s3c_device_nand,我们看下他如何使用的(如果前面的已经忘记了,没关系,退回去看一下),紧接着下面几行代码
info->area = request_mem_region(res->start, size, pdev->name); if (info->area == NULL) { dev_err(&pdev->dev, "cannot reserve register region\n"); err = -ENOENT; goto exit_error; }显然,这里的request_mem_region用到的参数实际上就是我们前面定义的s3c_device_nand中的start,size当然就是end-start得到的,还有就是设备的名字,我们前面定义的是"s3c2410-nand",从函数名称可以看出,这里是通过res来申请mem资源,具体的可以自己阅读下代码,实际上request_mem_region是个宏,它调用了另外一个函数,这里我就不作分析了。继续往下看,又看到一行
info->regs = ioremap(res->start, size);ioremap函数的作用是将物理地址影射到虚拟地址,这里就是将s3c_device_nand中记录的Nand寄存器首地址开始的1M空间作了映射,这也就理解为什么是1M空间了,因为内核的一级页表是以1M为单位的,现在就清楚为什么要定义这个s3c_nand_resource了,因为Linux内核使用的地址空间是启动MMU后的虚拟地址空间,而我们给出的寄存器地址是物理地址,内核需要将寄存器的物理地址映射到虚拟地址才能正确访问寄存器,到这里我们知道驱动程序已经可以正确访问Nand寄存器了,前面的疑惑解开了。
继续往下看代码,到for循环处停下来,我们需要注意一下这部分代码,因为我们看到了s3c2410_nand_init_chip,从函数名称上很容易可以看出,这就是Nand的初始化代码,但是这里为什么要使用一个for循环呢?我们看到循环控制变量是nr_sets,往上可以找到
sets = (plat != NULL) ? plat->sets : NULL;也就是说sets和nr_sets是从plat中获取的,再往上找plat
nr_sets = (plat != NULL) ? plat->nr_sets : 1;
struct s3c2410_platform_nand *plat = to_nand_plat(pdev);在函数的开头部分我们找到了plat的定义,看来plat是pdev中获取到的,我们跟踪进入这个to_nand_plat函数看个究竟
static struct s3c2410_platform_nand *to_nand_plat(struct platform_device *dev){ return dev->dev.platform_data;}这个函数很简单,就是直接返回了s3c_nand_device中的dev成员的platform_data,而前面我们看到的代码中没有出现这个变量,从plat定义处指出的类型可知,这个platform_data的类型是s3c2410_platform_nand,这时,我们可以回到最开始的文件,arch/arm/mach-s3c24xx/mach-mini2440.h,可以找到mini2440_init函数中有这样一行代码
s3c_nand_set_platdata(&mini2440_nand_info);这就是上面platform_data的来源,找到mini2440_nand_info的定义也就找到了上面用到的platform_data
/* NAND Flash on MINI2440 board */static struct mtd_partition mini2440_default_nand_part[] __initdata = { [0] = { .name = "u-boot", .size = SZ_256K, .offset = 0, }, [1] = { .name = "u-boot-env", .size = SZ_128K, .offset = SZ_256K, }, [2] = { .name = "kernel", /* 5 megabytes, for a kernel with no modules * or a uImage with a ramdisk attached */ .size = 0x00500000, .offset = SZ_256K + SZ_128K, }, [3] = { .name = "root", .offset = SZ_256K + SZ_128K + 0x00500000, .size = MTDPART_SIZ_FULL, }, };static struct s3c2410_nand_set mini2440_nand_sets[] __initdata = { [0] = { .name = "nand", .nr_chips = 1, .nr_partitions = ARRAY_SIZE(mini2440_default_nand_part), .partitions = mini2440_default_nand_part, .flash_bbt = 1, /* we use u-boot to create a BBT */ },};static struct s3c2410_platform_nand mini2440_nand_info __initdata = { .tacls = 0, .twrph0 = 25, .twrph1 = 15, .nr_sets = ARRAY_SIZE(mini2440_nand_sets), .sets = mini2440_nand_sets, .ignore_unset_ecc = 1,};在该文件中,我们找到了三个相互嵌套的结构,最下面一个就是最终赋值给platform_data的变量mini2440_nand_info,mini2440_nand_info除了指定了Nand的时序外还有两个成员,明显是匹配出现的,就是nr_sets和sets,分别指定了sets数组指针和sets数组长度,而上面的结构体就是sets的定以,从结构体名字可知,这时nand_flash芯片,也就是内核可以同时支持多个Nand,mini2440开发板中只有一块Nand,所以这个数组只有一个元素,mini2440_nand_sets指定了芯片的名称,chip数目,另外还有两个变量,也是成对出现的,就是nr_partitions和partitions,这两个就是上面的mtd_partition结构,也就是Nand分区表,这样就清楚了plartform_data中的数据,然后我们继续阅读s3c24xx_nand_probe函数(drivers/mtd/nand/s3c2410.c中),还是循环处,我们追踪进入s3c2410_nand_init_chip,浏览代码可以知道,这个函数实际上完成了下面几件事情
(1)初始化了chip中的各种操作函数指针并赋值给了nmtd->mtd.priv。
(2)初始化了info的sel_*成员,显然是Nand片选所用
(3)初始化了nmtd的几个成员
nmtd,info,set是该函数的三个参数,理解了这几个参数也就理解了这个函数的作用。info显然就是s3c24xx_nand_init中的s3c2410_nand_info,nmtd是info->mtds,而info->mtds是kzmalloc开辟的大小为size的内核空间,kzmalloc是kernel zero malloc,也就是开辟了size大小的空间清全部设置为0,也就是nmtds就是空的mtd数组,sets来就前面我定义的mini2440_nand_sets,这样三个参数都知道什么意思了,再去看代码就很简单了。(刚才去打了半小时电话,思路有点乱,不过大体上看了下,这个函数里面没有复杂的操作,相信大家很容易看懂)。
执行完s3c2410_nand_init之后就执行了nand_scan_ident,这是内核函数我就不做分析了,大家自己跟一下就可以知道,这个函数完成了nand_chip其他未指定函数指针的初始化,并获取了Nand的ID信息等,接下来又s3c2410_nand_update_chip,nand_scan_tail,s3c2410_nand_add_partitions,其中nand_scan_tail是通用的内核函数,而s3c2410_nand_update_chip是ecc相关的操作,我们只分析s3c2410_nand_add_partitions,从名字上讲,s3c2410开头的函数肯定不是内核通用函数,也就是说,这实际上是我们需要自行完成的函数,当然,也是可以借鉴的函数,追踪进入s3c2410_nand_add_partitions,看看内核是如何知道分区信息的。
static int s3c2410_nand_add_partition(struct s3c2410_nand_info *info,
struct s3c2410_nand_mtd *mtd,
struct s3c2410_nand_set *set)
{
if (set)
mtd->mtd.name = set->name;
return mtd_device_parse_register(&mtd->mtd, NULL, NULL,
set->partitions, set->nr_partitions);
}
这个函数也很简单,仅设置了下mtd的nand然后就调用和mtd_core.c中的mtd_device_parse_register函数,从参数可以知道,该函数向内核注册了Nand分区信息。这样我们就基本上看完了Linux内核Nand驱动部分的结构。
在结尾之前我还要提到一个问题,就是内核驱动的匹配问题,在platform_device定义时内核指定的名称是s3c2410-nand
struct platform_device s3c_device_nand = {但是我们的开发版是s3c2440的核,两者的Nand控制器是不相同的,内核又怎么正确加载到s3c2440-nand的呢?答案在arch/arm/mach-s3c24xx/s3c2440.c中
.name = "s3c2410-nand",
.id = -1,
.num_resources = ARRAY_SIZE(s3c_nand_resource),
.resource = s3c_nand_resource,
};
void __init s3c244x_map_io(void){ /* register our io-tables */ iotable_init(s3c244x_iodesc, ARRAY_SIZE(s3c244x_iodesc)); /* rename any peripherals used differing from the s3c2410 */ s3c_device_sdi.name = "s3c2440-sdi"; s3c_device_i2c0.name = "s3c2440-i2c"; s3c_nand_setname("s3c2440-nand"); s3c_device_ts.name = "s3c2440-ts"; s3c_device_usbgadget.name = "s3c2440-usbgadget";}从这里就可以看到答案了,原来s3c_nand_setname("s3c2440-nand")将platform_device的name更改了,具体的调用关系是这样的,mini2440_map_io()----->s3c24xx_init_io()---->s3c_init_cpu()---->cpu->map_io()----->s3c2440_map_io()---->s3c_device_nand.name="s3c2440-nand",这样就不难理解了,至少内核加载驱动时是要查找与s3c2440-nand重名的驱动,但是我们的s3c24xx_nand_driver中指定的驱动名称为s3c24xx-nand,难道内核这么智能,能将s3c24xx-nand自动匹配到s3c2440-nand?当然这不可能,自己查看s3c24xx_nand_driver的各个变量就可以知道答案了,答案在drivers/mtd/nand/s3c2410.c中,原来s3c24xx_nand_driver有个成员id_table,其具体定义为
static struct platform_device_id s3c24xx_driver_ids[] = { { .name = "s3c2410-nand", .driver_data = TYPE_S3C2410, }, { .name = "s3c2440-nand", .driver_data = TYPE_S3C2440, }, { .name = "s3c2412-nand", .driver_data = TYPE_S3C2412, }, { .name = "s3c6400-nand", .driver_data = TYPE_S3C2412, /* compatible with 2412 */ }, { } };这就可以猜到了,原来内核设备匹配驱动时并不是或者说不仅仅是匹配s3c24xx_nand_driver.driver.name与设备的name,也会跟s3c24xx_nand_driver中id_table的各个元素的name进行匹配,只要匹配了其中任何一个则认为驱动匹配成功,继而执行驱动的probe函数对设备进行初始化。如果还不确定这里的关系可以追踪一下s3c24xx_nand_init中的注册函数,原来platform_driver_register调用driver_register之前对s3c24xx_nand_driver.driver.bus指针进行的初始化
struct bus_type platform_bus_type = { .name = "platform", .dev_attrs = platform_dev_attrs, .match = platform_match, .uevent = platform_uevent, .pm = &platform_dev_pm_ops,};int platform_driver_register(struct platform_driver *drv){ drv->driver.bus = &platform_bus_type; if (drv->probe) drv->driver.probe = platform_drv_probe; if (drv->remove) drv->driver.remove = platform_drv_remove; if (drv->shutdown) drv->driver.shutdown = platform_drv_shutdown; return driver_register(&drv->driver);}从platform_bus_type的成员可知,match函数就是进行驱动匹配的,我们来看一下这个函数的定义,追踪platfrom_match
static int platform_match(struct device *dev, struct device_driver *drv){ struct platform_device *pdev = to_platform_device(dev); struct platform_driver *pdrv = to_platform_driver(drv); /* Attempt an OF style match first */ if (of_driver_match_device(dev, drv)) return 1; /* Then try to match against the id table */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* fall-back to driver name match */ return (strcmp(pdev->name, drv->name) == 0);}显然,第二个if就是判断的id_table,下面的这个函数肯定是循环比较的id_table中每个元素的.name跟device.name
static const struct platform_device_id *platform_match_id( const struct platform_device_id *id, struct platform_device *pdev){ while (id->name[0]) { if (strcmp(pdev->name, id->name) == 0) { pdev->id_entry = id; return id; } id++; } return NULL;}果然,这里的确是比较的设备名称跟id_table中的名称,我们的设备名称是s3c2440-nand,我们的id_table中定义了与之对应的元素,所以驱动匹配成功继而运行了改驱动的probe函数。好了,到这里我遇到的绝大多数问题已经解决了,最后,我们需要总结一下添加Nand驱动的步骤:
(1)定义resource,保证可以以物理地址方式正确访问Nand寄存器。(默认有)这样,就可以实现Nand驱动的安装,到这里,我们就理解了Nand驱动结构,也就知道了移植教程中的各个步骤,为什么设置分区表,为什么要定义s3c2410开头的几个结构。其实这种移植是基于s3c2410的移植,从软件结构的角度来讲这种移植破坏了Linux内核本身的结构,跟Linux内核本身的结构有所背离,但是这里我们做的是Nand驱动原理的分析,就不讨论这个问题了,如果以后有机会会将我认为合理的代码贴出来请大家指点。
(2)定义platform_device,这是内核记录的硬件信息,要注册到内核设备列表。
(3)定义mtd_partition,设置Nand分区。
(4)定义s3c2410_nand_set,枚举所有Nand芯片信息。
(5)定义s3c2410_platform_nand,这是驱动程序初始化Nand是需要的数据,包括分区信息的和芯片时序等必要信息。
(6)将s3c2410_platform_nand赋值给mtd_device->dev.platform_data。
(7)将Nand设备结构注册到设备列表。