Linux建立Nor Flash分区
韩大卫@吉林师范大学
接上文章<<linux系统Nor Flash芯片初始化及驱动>>, 当Nor Flash 芯片在flash芯片驱动器里链表chip_drvs_list中找到并调用名为”cfi_probe”的驱动后, 完成芯片初始化阶段, 接着进入linux对Flash建立分区阶段.
在 arch/mips/cavium-octeon/flash_setup.c 中
static struct map_info flash_map;
static int __init flash_init(void)
{
union cvmx_mio_boot_reg_cfgx region_cfg;
//从bootbus总线上获取flash的基地址.
region_cfg.u64 = cvmx_read_csr(CVMX_MIO_BOOT_REG_CFGX(0));
if (region_cfg.s.en) {
//将全局数据结构struct map_info flash_map命名为octeon_nor0
flash_map.name = "octeon_nor0";
//物理地址和大小
flash_map.phys = region_cfg.s.base << 16;
flash_map.size = 0x1fc00000 - flash_map.phys;
flash_map.bankwidth = 1;
//使用ioremap()将32M 大小的Flash的物理地址映射到虚拟地址上.
flash_map.virt = ioremap(flash_map.phys, flash_map.size);
pr_notice("Bootbus flash: Setting flash for %luMB flash at "
"0x%08llx\n", flash_map.size >> 20, flash_map.phys);
simple_map_init(&flash_map);
/*
调用do_map_probe()进入Nor Flash芯片初始化阶段,该函数会在Flash芯片驱动器列表中找到名为cfi_probe的驱动器, 并调用其probe()函数, 准备好read/wirte/ioctl等函数的实现方法.
*/
mymtd = do_map_probe("cfi_probe", &flash_map);
if (mymtd) {
mymtd->owner = THIS_MODULE;
#ifdef CONFIG_MTD_PARTITIONS
/*
对FLash芯片成功探测(调用过probe)后, linux进入处理Flash分区阶段
*/
nr_parts = parse_mtd_partitions(mymtd,
part_probe_types,
&parts, 0);
if (nr_parts > 0)
/*
nr_parts>0 ,说明解析到存在多个分区, 那么添加各个分区
*/
add_mtd_partitions(mymtd, parts, nr_parts);
else
add_mtd_device(mymtd);
#else
//由于定义了CONFIG_MTD_PARTITIONS
宏, 不执行该函数
add_mtd_device(mymtd);
#endif
} else {
pr_err("Failed to register MTD device for flash\n");
}
}
return 0;
}
late_initcall(flash_init);
调用do_map_probe()后, 成功的话返回一个重要的数据结构struct mtd_info.
Struct mtd_info是linux描述MTD类型设备的数据结构, 里面有mtd设备等待初始化的信息(变量)和一些设备操作方法(函数指针).
经过do_map_probe()的初始化, 其中有一些成员已经得到赋值, 请参考drivers/mtd/chips/cfi_probe.c 中的cfi_probe()函数.
parse_mtd_partitions(mymtd,
part_probe_types,&parts, 0);
解析器类型: 有cmdlinepart和 RedBoot两中, 如没有定义CONFIG_MTD_REDBOOT_PARTS, 那只将cmdlinepart编译在内, 最后只连接cmdlinepart.o.
static const char *part_probe_types[] = {
"cmdlinepart",
#ifdef CONFIG_MTD_REDBOOT_PARTS
"RedBoot",
#endif
NULL
};
parse_mtd_partitions()定义在drivers/mtd/mtdpart.c 中:
int parse_mtd_partitions(struct mtd_info *master, const char **types,
struct mtd_partition **pparts, unsigned long origin)
{
struct mtd_part_parser *parser;
int ret = 0;
//循环次数: 解析器的个数, 这里只有一个解析器cmdlinepart, 故只循环一次
for ( ; ret <= 0 && *types; types++) {
//获取cmdlinepart解析器
parser = get_partition_parser(*types);
if (!parser && !request_module("%s", *types))
parser = get_partition_parser(*types);
if (!parser) {
printk(KERN_NOTICE "%s partition parsing not available\n",
*types);
continue;
}
/*
获取成功的话, 调用其解析函数 parse_fn(), 类似于do_map_probe()
后者是获取驱动器cfi_probe, 获取成功的话, 调用其探测函数Probe()
*/
ret = (*parser->parse_fn)(master, pparts, origin);
/*
parse_fn()返回解析到的分区个数.在此打印出相关信息,此信息可在dmesg中看到.
parser->name 为cmdlinepart, master->name为octeon_nor0
*/
if (ret > 0) {
printk(KERN_NOTICE "%d %s partitions found on MTD device %s\n",
ret, parser->name, master->name);
}
/*
减少调用get_partition_parser ()时增加模块使用计数器个数, 释放解析器模块使用权
#define put_partition_parser(p) do { module_put((p)->owner); } while(0)
*/
put_partition_parser(parser);
}
return ret;
}
get_partition_parser() 定义如下:
static struct mtd_part_parser *get_partition_parser(const char *name)
{
struct mtd_part_parser *p, *ret = NULL;
spin_lock(&part_parser_lock);
//遍历分区解析器链表part_parsers
list_for_each_entry(p, &part_parsers, list)
//在链表中获取解析器名为name的节点, 并增加其模块使用计数, 取得解析器模块使用权
if (!strcmp(p->name, name) && try_module_get(p->owner)) {
ret = p;
break;
}
spin_unlock(&part_parser_lock);
return ret;
}
总的来说, parse_mtd_partitions()函数类似do_map_probe(), 都是传入一个指定参数, 之后的工作都交给linux的mtd层去实现.
另一方面, 在late_initcall(flash_init)调用之前, linux已经向内核注册了名为”cmdlinepart”的解析器, 等待被调用, 完成使命, 体现出自身价值.
在drivers/mtd/cmdlinepart.c中:
module_init(cmdline_parser_init);
注: module_init()优先级为6, 高于优先级为7的late_initcall(). 数值越低优先级越高.
static int __init cmdline_parser_init(void)
{
//向分区解析器链表中添加名为”cmdlinepart” 的解析器
return register_mtd_parser(&cmdline_parser);
}
/*
注: 这是driver/mtd/mtdpart.c提供的API:
int register_mtd_parser(struct mtd_part_parser *p)
{
spin_lock(&part_parser_lock);
//向分区解析器链表中添加成员
list_add(&p->list, &part_parsers);
spin_unlock(&part_parser_lock);
return 0;
}
*/
cmdline_parser 定义如下:
static int mtdpart_setup(char *s)
{
cmdline = s;
return 1;
}
__setup("mtdparts=", mtdpart_setup);
static struct mtd_part_parser cmdline_parser = {
.owner = THIS_MODULE,
// cmdlinepart的 解析函数为: parse_cmdline_partitions
.parse_fn = parse_cmdline_partitions,
.name = "cmdlinepart",
};
关于__setup("mtdparts=", mtdpart_setup)解释一下:
__setup()宏 定义在include/linux/init.h 中:
#define __setup(str, fn) \
__setup_param(str, fn, fn, 0)
#define __setup_param(str, unique_id, fn, early) \
static const char __setup_str_##unique_id[] __initconst \
__aligned(1) = str; \
static struct obs_kernel_param __setup_##unique_id \
__used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
__setup宏是linux用来注册关键字及提供处理函数. Str即为关键字, fn为对应的处理函数.
经过上面的展开后, __setup()宏会将传进来的的函数指针放到.init.setup段.而.init.setup是在.init.text下的.
/* 注:
mips架构下: 在arch/mips/kernel/vmlinux.lds.S
__init_begin = .
INIT_TEXT_SECTION(PAGE_SIZE)
INIT_DATA_SECTION()
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
INIT_DATA \
INIT_SETUP(initsetup_align) \
INIT_CALLS \
CON_INITCALL \
SECURITY_INITCALL \
INIT_RAM_FS \
}
在INIT_SETUP() 宏中,
#define INIT_SETUP(initsetup_align) \
. = ALIGN(initsetup_align); \
VMLINUX_SYMBOL(__setup_start) = .; \
*(.init.setup) \
VMLINUX_SYMBOL(__setup_end) = .;
可以看到: 经过层层包装,.init.setup字段放在了.init.text字段中.
同样, 在arm架构中:arch/arm/kernel/vmlinux.lds.S
.init : { /* Init code and data */
INIT_TEXT
...
__setup_start = .;
*(.init.setup)
__setup_end = .;
//直接就可以看到__setup的位置.
可以看到, __setup()宏将传进来的函数指针放到.init.setup字段中.
linux定义了parse_args()函数处理由__setup()传进来参数, 当pa rse_args()解析uboot传给内核的启动参数, 匹配__setup()传进来的str, 匹配成功后调用其fn函数.
在init/main.c 的 __init start_kernel(void) 中:
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
当parse_args()解析到配置命令字符串中有"mtdparts=" 时,mtdpart_setup()函数便会自动得到调用:将 boot_command_line 字符串保存到本地变量static char *cmdline中.
另外, start_kernel()中有这样的语句:
printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
通过这个打印可以看到Uboot传给内核的配置命令字符串:
在我的系统中的打印信息是:
Kernel command line: bootoctlinux 0x1dd00000 coremask=0x1 mtdparts=octeon_nor0:1m(BOOT),7m(LINUX),21m(CONFIG),3m(MD) console=ttyS0,115200
其中”bootoctlinux 0x1dd00000 coremask=0x1 mtdparts=octeon_nor0:1m(BOOT),7m(LINUX),21m(CONFIG),3m(MD) console=ttyS0,115200”
就是 boot_command_line配置命令字符串.
在此可能有些疑问, 这样的配置命令字符串是怎样产生的,以及内核在哪里得到这个配置命令字符串static_command_line的?
第一个问题跟uboot的配置有关, 需要一大堆文字来解释说明, 请参考作者其他博文, 这里只简单说个大概:
uboot启动时, 将Uboot环境变量(如linux_args, loadaddr, bootcmd,numcores等, 在uboot环境里使用printenv命令可以看到)作为编译参数, 传给Uboot的启动linux时的调用参数, 在该调用函数中, 会获取并处理这些环境变量, 得到我们见到的配置命令字符串, 最后调用一段用汇编语言写的代码, 将其存入特定寄存器中.
在linux内核启动时, 在init/main.c的 start_kernel()中: setup_arch(&command_line)这里获取到了Uboot传下来的配置命令字符串, 并调用setup_command_line(command_line)将command_line 复制static_command_line, 之后调用parse_args()对其进行处理.
总的来说, 可以简单理解为: cmdlinepart通过mtdpart_setup()函数保存了内核启动时由uboot传进来的含有”mtdparts=”的配置命令字符串, 并该字符串传给本地指针static char *cmdline, 在接下来的parse_cmdline_partitions()中, 根据此配置命令字符串作出解析.最后实现该配置.
在parse_mtd_partitions() 中使用的(*parser->parse_fn)(master, pparts, origin)的实现函数就是drivers/mtd/cmdlinepart.c 中的 parse_cmdline_partitions, 定义如下:
static int parse_cmdline_partitions(struct mtd_info *master,
struct mtd_partition **pparts,
unsigned long origin)
{
unsigned long offset;
int i;
struct cmdline_mtd_partition *part;
const char *mtd_id = master->name;
/* parse command line */
if (!cmdline_parsed)
/*
在此函数中解析了linux启动时的mtdparts配置命令字符串.即”bootoctlinux 0x1dd00000 coremask=0x1 mtdparts=octeon_nor0:1m(BOOT),7m(LINUX),21m(CONFIG),3m(MD) console=ttyS0,115200”,
初始化并返回一个全局的分区描述数据结构static struct cmdline_mtd_partition *partitions
struct cmdline_mtd_partition {
struct cmdline_mtd_partition *next;
//下个分区的指针
char *mtd_id; //mtd_id
int num_parts;
//分区编号
struct mtd_partition *parts;
//包含了分区名,大小, 偏移等信息
};
*/
mtdpart_setup_real(cmdline);
/*
在获得了分区描述信息partitions后, 将每一个分区的partitions传给参数
struct mtd_partition **pparts, 完成解析函数真正的作用. 等解析函数退出后, flash_init()便可以调用add_mtd_partitions()将每个 struct mtd_partition 添加到linux的mtd核心中.后面会看到这样的操作.
*/
for(part = partitions; part; part = part->next)
{
if ((!mtd_id) || (!strcmp(part->mtd_id, mtd_id)))
{
for(i = 0, offset = 0; i < part->num_parts; i++)
{
if (part->parts[i].offset == OFFSET_CONTINUOUS)
part->parts[i].offset = offset;
else
offset = part->parts[i].offset;
if (part->parts[i].size == SIZE_REMAINING)
part->parts[i].size = master->size - offset;
if (offset + part->parts[i].size > master->size)
{
printk(KERN_WARNING ERRP
"%s: partitioning exceeds flash size, truncating\n",
part->mtd_id);
part->parts[i].size = master->size - offset;
part->num_parts = i;
}
offset += part->parts[i].size;
}
//将每个分区的struct cmdline_mtd_partition 传给函数参数pparts.
*pparts = kmemdup(part->parts,
sizeof(*part->parts) * part->num_parts,
GFP_KERNEL);
if (!*pparts)
return -ENOMEM;
//在最后一次循环时,返回分区号, 即分区个数
return part->num_parts;
}
}
return 0;
}
parse_cmdline_partitions()调用结束后, 返回系统存在多少个分区, parse_mtd_partitions()函数也随之调用完成. flash_init() 走到add_mtd_partitions(mymtd, parts, nr_parts);
在drivers/mtd/mtdpart.c 中:
int add_mtd_partitions(struct mtd_info *master,
const struct mtd_partition *parts,
int nbparts)
{
struct mtd_part *slave;
uint64_t cur_offset = 0;
int i;
printk(KERN_NOTICE "Creating %d MTD partitions on \"%s\":\n", nbparts, master->name);
//nbparts 为前面函数解析到的分区个数
for (i = 0; i < nbparts; i++) {
/*
parts为每个分区的数据结构数组指针
add_one_partition()初始化并填充一个linux描述mtd分区的数据结构:struct mtd_part,
主要是填充struct mtd_part 内的 struct mtd_info, 为mtd_info内的函数指针填充实现函数等.
比如给每个分区的mtd_info 填充好read, write, lock/unlock, erase等函数.
最后把指针交给slave, slave可利用地址偏移offset进行下个分区处理.
在add_one_partition() 内, 会打印出该分区的地址偏移值, 大小, 和slave分区的名字.
在dmesg里可以看到这样的信息:
[ 33.891465] 0x000000000000-0x000000100000 : "BOOT"
[ 33.896708] 0x000000100000-0x000000800000 : "LINUX"
[ 33.901975] 0x000000800000-0x000001d00000 : "CONFIG"
[ 33.907329] 0x000001d00000-0x000002000000 : "MD"
*/
slave = add_one_partition(master, parts + i, i, cur_offset);
if (!slave)
return -ENOMEM;
//当前偏移值
cur_offset = slave->offset + slave->mtd.size;
}
return 0;
}
EXPORT_SYMBOL(add_mtd_partitions);
总的来说, 就是经过了以上步骤, 实现了uboot传给内核的配置命令字符串所代表的配置. 根据配置命令字符串建立了相应的分区.