tiny4412 串口驱动分析七 --- log打印的几个阶段之内核启动阶段(earlyprintk)

作者:彭东林

邮箱:pengdonglin137@163.com

 

开发板:tiny4412ADK+S700 4GB Flash

主机:Wind7 64位

虚拟机:Vmware+Ubuntu12_04

u-boot:U-Boot 2010.12

Linux内核版本:linux-3.0.31

Android版本:android-4.1.2

 

下面要分析的是内核Log打印的几个阶段

  1. 自解压阶段
  2. 内核启动阶段
  3. 内核启动完全以后
  4. shell终端下

在这个阶段内核log打印可以调用printk和printascii,同时printk又分为两个阶段,从刚才的分析中知道,printk最终调用的是有register_console注册的console_drivers的write函数,在tiny4412平台上调用register_console的地方有两处,第一处是在arch/arm/kernel/early_printk.c中,另一处就是在串口驱动注册中,具体是在driver/tty/serial/samsung.c中,下面我们开始分析。

printascii的实现:

首先printascii需要配置内核才能使用:

make LOCALVERSION="" ARCH=arm CROSS_COMPILE=arm-linux- menuconfig

 

Kernel hacking

     --- Kernel low-level debugging functions

 

这样就可以使用printascii了:

在printk中会调用printascii,

#ifdef        CONFIG_DEBUG_LL

         printascii(printk_buf);

#endif

print_asciii使用汇编实现的,在文件arch/arm/kernel/debug.S中:

.macro     addruart_current, rx, tmp1, tmp2
addruart   \tmp1, \tmp2
mrc        p15, 0, \rx, c1, c0
tst        \rx, #1
moveq      \rx, \tmp1
movne      \rx, \tmp2
.endm

ENTRY(printascii)
                   addruart_current r3, r1, r2
                   b       2f
1:                 waituart r2, r3
                   senduart r1, r3
                   busyuart r2, r3
                   teq   r1, #'\n'
                   moveq      r1, #'\r'
                   beq  1b

2:                 teq   r0, #0
                   ldrneb       r1, [r0], #1
                   teqne        r1, #0
                   bne  1b

                   mov pc, lr
ENDPROC(printascii)

其中 addruart 是在文件arch/arm/mach-exynos/include/mach/debug-macro.S中实现的,waituart、senduart以及busyuart是在arch/arm/plat-samsung/include/plat/debug-macro.S中实现的,大家可以参考这两个文件理解具体实现过程。

early_printk中调用register_console

tiny4412使用的内核默认是没有开启early_printk的,即log_buf中内容只有等driver/tty/serial下的串口驱动注册完成后才能输出到串口终端,在此之前调用printk的内容都缓存到log_buf中了,如果想提前使用的话,需要使能early_printk,下面说明一下如何使能early_printk。

make LOCALVERSION="" ARCH=arm CROSS_COMPILE=arm-linux- menuconfig

Kernel hacking

        -- Kernel low-level debugging functions

        --Early printk

 

 

要使能early_printk首先必须使能Kernel low-level debugging functions,因为early_printk最终也是使用printascii实现的,这样arch/arm/kernel/early_printk.c就会参加编译

在early_printk.c中:

static int __init setup_early_printk(char *buf)
{
    printk("%s enter\n", __func__);
    register_console(&early_console);
    return 0;
}
early_param("earlyprintk", setup_early_printk);

虽然内核配置了,但是要让内核调用setup_early_printk还必须在u-boot给内核传参的时候给bootargs在加上一个参数”earlyprintk”,如下:

set bootargs ‘console=ttySAC0,115200n8 androidboot.console=ttySAC0 uhost0=n ctp=2 skipcali=y vmalloc=384m lcd=S70 earlyprintk

那么内核是如何处理的呢?

在文件include/linux/init.h中:

#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 }

#define early_param(str, fn)                    \
    __setup_param(str, fn, fn, 1)

将early_param("earlyprintk", setup_early_printk);展开后:

static const char __setup_str_setup_early_printk[] __initconst __aligned(1) = "earlyprintk";
static struct obs_kernel_param __setup_setup_early_printk __used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) = \
{ __setup_str_setup_early_printk, setup_early_printk, 1 }

即上面的这个结构体被链接到了”.init.setup”段,在arch/arm/kernel/vmlinux.lds中:

  . = ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .;

将所有的.init.setup都链接到了__setup_start和__setup_end之间

在内核启动的时候:

start_kernel
  --- setup_arch(&command_line); 
  //这个函数的目的是获得u-boot传给内核的参数(bootargs),并将参数存放在command_line中,然后解析command_line,并调用相关的函数处理--- parse_early_param()   (arch/arm/kernel/setup.c)
                    --- strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
                    --- parse_early_options(tmp_cmdline);

parse_early_options用于解析tmp_cmdline

void __init parse_early_options(char *cmdline)
{
    parse_args("early options", cmdline, NULL, 0, do_early_param);
}

在文件kernel/params.c中:

int parse_args(const char *name,
           char *args,
           const struct kernel_param *params,
           unsigned num,
           int (*unknown)(char *param, char *val))
{
    char *param, *val;

    /* Chew leading spaces */
    args = skip_spaces(args); // 跳过args开头的空格

    while (*args) {
        int ret;
        int irq_was_disabled;
/*
    如:cmdline是“console=ttySAC0,115200n8 androidboot.console=ttySAC0”
    执行next_arg后:
    params=”console”, val=” ttySAC0,115200n8” args=” androidboot.console=ttySAC0”
*/
        args = next_arg(args, &param, &val); // 获得下一个参数的位置,
        irq_was_disabled = irqs_disabled();
/*
    parse_one所做的主要就是将params和val传递给unknown处理,这里unknown就是do_early_param,所以下面分析do_early_param
*/
        ret = parse_one(param, val, params, num, unknown);
        …
        }
    }

    /* All parsed OK. */
    return 0;
}

 

static int __init do_early_param(char *param, char *val)
{
    const struct obs_kernel_param *p;
/*
   还记得early_param("earlyprintk", setup_early_printk)展开后的结果吗?
   其中early为1,str是 “earlyprintk”,setup_func就是setup_early_printk
*/
    for (p = __setup_start; p < __setup_end; p++) {
        if ((p->early && strcmp(param, p->str) == 0) ||
            (strcmp(param, "console") == 0 &&
             strcmp(p->str, "earlycon") == 0)
        ) {
            if (p->setup_func(val) != 0)
…
        }
    }
    return 0;
}

下面我们就分析setup_early_printk:

static int __init setup_early_printk(char *buf)
{
    register_console(&early_console);
    return 0;
}

结构体early_console 的定义如下:

static struct console early_console = {
    .name =        "earlycon",
    .write =    early_console_write,
    .flags =    CON_PRINTBUFFER | CON_BOOT,
    .index =    -1,
};

看一下early_console_write干了什么:

static void early_console_write(struct console *con, const char *s, unsigned n)
{
    early_write(s, n);
}

 

static void early_write(const char *s, unsigned n)
{
    while (n-- > 0) {
        if (*s == '\n')
            printch('\r');
        printch(*s);
        s++;
    }
}

可以看到,它调用的是printch,它在文件arch/arm/kernel/debug.S中实现:

ENTRY(printascii)
        addruart_current r3, r1, r2
        b    2f
1:      waituart r2, r3
        senduart r1, r3
        busyuart r2, r3
        teq    r1, #'\n'
        moveq    r1, #'\r'
        beq    1b
2:      teq    r0, #0
        ldrneb    r1, [r0], #1
        teqne    r1, #0
        bne    1b
        mov    pc, lr
ENDPROC(printascii)

ENTRY(printch)
        addruart_current r3, r1, r2
        mov    r1, r0
        mov    r0, #0
        b    1b
ENDPROC(printch)

这样当driver/tty/serial下的驱动尚未注册时,printk就已经可以使用了,它最终调用的是early_console_write输出到串口终端的

下面我们分析一个函数register_console

/*
   这段话最好看一下
 * The console driver calls this routine during kernel initialization
 * to register the console printing procedure with printk() and to
 * print any messages that were printed by the kernel before the
 * console driver was initialized.
 *
 * This can happen pretty early during the boot process (because of
 * early_printk) - sometimes before setup_arch() completes - be careful
 * of what kernel features are used - they may not be initialised yet.
 *
 * There are two types of consoles - bootconsoles (early_printk) and
 * "real" consoles (everything which is not a bootconsole) which are
 * handled differently.
 *  - Any number of bootconsoles can be registered at any time.
 *  - As soon as a "real" console is registered, all bootconsoles
 *    will be unregistered automatically.
 *  - Once a "real" console is registered, any attempt to register a
 *    bootconsoles will be rejected
 */
void register_console(struct console *newcon)
{
    int i;
    unsigned long flags;
    struct console *bcon = NULL;

    /*
     * before we register a new CON_BOOT console, make sure we don't
     * already have a valid console
       这个判断的目的是:看当前系统是否有”real”类型的console注册,如果有的话,直接返回,即”real”类型和”bootconsoles”类型的console不能共存,
如果注册了”real”类型的console的话,则会对”bootconsoles”类型的console进行unregister 如果已经有”real”类型的console,则注册”bootconsoles”类型的console时会失败,”bootconsoles”类型的console的flags设置CON_BOOT位。 这里在内核刚启动,还没有任何console注册,console_drivers是NULL
*/ if (console_drivers && newcon->flags & CON_BOOT) { /* find the last or real console */ for_each_console(bcon) { if (!(bcon->flags & CON_BOOT)) { printk(KERN_INFO "Too late to register bootconsole %s%d\n", newcon->name, newcon->index); return; } } } if (console_drivers && console_drivers->flags & CON_BOOT) bcon = console_drivers; // perferred_console和selected_console的初始值都是-1,但是如果在bootargs中设置了类似“console=ttySAC0,115200n8”时,在解析console参数时会设置selected_console,这个一会分析 if (preferred_console < 0 || bcon || !console_drivers) preferred_console = selected_console; if (newcon->early_setup) // 如果有的话,则调用 newcon->early_setup(); /* * See if we want to use this console driver. If we * didn't select a console we take the first one * that registers here. */ if (preferred_console < 0) { if (newcon->index < 0) newcon->index = 0; if (newcon->setup == NULL || newcon->setup(newcon, NULL) == 0) { newcon->flags |= CON_ENABLED; if (newcon->device) { newcon->flags |= CON_CONSDEV; preferred_console = 0; } } } /* * See if this console matches one we selected on * the command line. // console_cmdline会在解析bootargs的console参数时设置 */ for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++) { if (strcmp(console_cmdline[i].name, newcon->name) != 0) continue; if (newcon->index >= 0 && newcon->index != console_cmdline[i].index) continue; if (newcon->index < 0) newcon->index = console_cmdline[i].index; if (newcon->setup && newcon->setup(newcon, console_cmdline[i].options) != 0) break; newcon->flags |= CON_ENABLED; newcon->index = console_cmdline[i].index; if (i == selected_console) { // selected_console是从bootargs中解析出来的 newcon->flags |= CON_CONSDEV; preferred_console = selected_console; } break; } if (!(newcon->flags & CON_ENABLED)) return; /* * If we have a bootconsole, and are switching to a real console, * don't print everything out again, since when the boot console, and * the real console are the same physical device, it's annoying to * see the beginning boot messages twice */ if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) newcon->flags &= ~CON_PRINTBUFFER; /* * Put this console in the list - keep the * preferred driver at the head of the list. */ console_lock(); // 这里会把跟selected_console一样的ttySAC尽量往前放 if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) { newcon->next = console_drivers; console_drivers = newcon; if (newcon->next) newcon->next->flags &= ~CON_CONSDEV; } else { newcon->next = console_drivers->next; console_drivers->next = newcon; } if (newcon->flags & CON_PRINTBUFFER) { spin_lock_irqsave(&logbuf_lock, flags); con_start = log_start; spin_unlock_irqrestore(&logbuf_lock, flags); exclusive_console = newcon; } console_unlock(); console_sysfs_notify(); /* * By unregistering the bootconsoles after we enable the real console * we get the "console xxx enabled" message on all the consoles - * boot consoles, real consoles, etc - this is to ensure that end * users know there might be something in the kernel's log buffer that * went to the bootconsole (that they do not see on the real console) */ if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV) && !keep_bootcon) { // 在命令行中可以设置参数,将keep_bootcon置1,就不会将bootconsole注销了 /* we need to iterate through twice, to make sure we print * everything out, before we unregister the console(s) 如果使能了early_printk的话,下面的这条log会打印两次,因为一次是从real console,另一次是从boot consoles。原因是 当real已经注册时(上面更新了console_drivers),
但是此时boot consoles还尚未unregister。
*/ printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled\n", newcon->name, newcon->index); for_each_console(bcon) if (bcon->flags & CON_BOOT) unregister_console(bcon); // unregister boot consoles } else { printk(KERN_INFO "%sconsole [%s%d] enabled\n", (newcon->flags & CON_BOOT) ? "boot" : "" , newcon->name, newcon->index); } }

上面我们分析了early_printk,下面我们分析当内核启动之后的printk打印的实现。

当Linux内核启动之后,或者更确切的说是串口驱动注册后,使用的是real console,会把boot consoles都disable。

内核起来后,使用串口终端可以登录,在用户空间(对于android)是/system/bin/sh跟用户交互,它接收用户通过串口输入的命令,然后执行,它虽然在最底层都会操作串口硬件寄存器,但是跟内核的printk还不一样,前者走的是tty驱动架构,他们在底层是通过不同的机制来操作uart控制器的。下面一块分析。

在此之前,我们还要看一下u-boot给内核传参:

console=ttySAC0,115200n8 androidboot.console=ttySAC0 uhost0=n ctp=2 skipcali=y vmalloc=384m lcd=S70

由于tiny4412一共有4个串口,我们使用了com0和com3,在参数中的console参数console=ttySAC0,115200n8告诉Linux,将来sh要运行在ttySAC0上,它的波特率是115200,每帧8位。也就是我们要通过com0与Linux交互。

在此可以做一个实验,如果我们执行

“hello world”会通过串口ttySAC0打印到终端

但是执行

什么反应也没有,因为我们接在com0上而不是com3上。后面我们会尝试修改默认的串口,从com0改成com3.

那么系统是如何解析console=ttySAC0,115200n8的呢?

在kernel/printk.c中:

__setup("console=", console_setup);

在include/linux/init.h中:

#define __setup(str, fn)                    \
    __setup_param(str, fn, fn, 0)

而__setup_param我们在上面已经分析了,最终__setup("console=", console_setup);

的展开结果是:

static const char __setup_str_console_setup[] __initconst    \
        __aligned(1) = "console="; 

    static struct obs_kernel_param __setup_console_setup    \
        __used __section(.init.setup)            \
        __attribute__((aligned((sizeof(long)))))    \
        = { __setup_str_console_setup, console_setup, 0 }

可以看到它也链接到了”.init.setup”段。在内核启动的时候:

asmlinkage void __init start_kernel(void)
{
    char * command_line;
    extern const struct kernel_param __start___param[], __stop___param[];
    ...
    parse_args("Booting kernel", static_command_line, __start___param,
           __stop___param - __start___param,
           &unknown_bootoption);
    ......
}

 

int parse_args(const char *name,
           char *args,
           const struct kernel_param *params,
           unsigned num,
           int (*unknown)(char *param, char *val))
{
    char *param, *val;

    DEBUGP("Parsing ARGS: %s\n", args);

    /* Chew leading spaces */
    args = skip_spaces(args);

    while (*args) {
        int ret;
        int irq_was_disabled;

        args = next_arg(args, &param, &val);
        …
        ret = parse_one(param, val, params, num, unknown); 
        // 在parse_one中会调用unknown函数,并将param和val传给它
        …
}

 

static int __init unknown_bootoption(char *param, char *val)
{
    /* Change NUL term back to "=", to make "param" the whole string. */
    if (val) {
        /* param=val or param="val"? */
        if (val == param+strlen(param)+1)
            val[-1] = '=';
        else if (val == param+strlen(param)+2) {
            val[-2] = '=';
            memmove(val-1, val, strlen(val)+1);
            val--;
        } else
            BUG();
    }

    /* Handle obsolete-style parameters */
    if (obsolete_checksetup(param))
        return 0;

    /* Unused module parameter. */
    if (strchr(param, '.') && (!val || strchr(param, '.') < val))
        return 0;

    if (panic_later)
        return 0;

    if (val) {
        /* Environment option */
        unsigned int i;
        for (i = 0; envp_init[i]; i++) {
            if (i == MAX_INIT_ENVS) {
                panic_later = "Too many boot env vars at `%s'";
                panic_param = param;
            }
            if (!strncmp(param, envp_init[i], val - param))
                break;
        }
        envp_init[i] = param;
    } else {
        /* Command line option */
        unsigned int i;
        for (i = 0; argv_init[i]; i++) {
            if (i == MAX_INIT_ARGS) {
                panic_later = "Too many boot init vars at `%s'";
                panic_param = param;
            }
        }
        argv_init[i] = param;
    }
    return 0;
}

对于“console=ttySAC0,115200n8”符合这个条件

static int __init obsolete_checksetup(char *line)
{
    const struct obs_kernel_param *p;
    int had_early_param = 0;

    p = __setup_start;
    do {
        int n = strlen(p->str);
        if (!strncmp(line, p->str, n)) {
            if (p->early) {
                /* Already done in parse_early_param?
                 * (Needs exact match on param part).
                 * Keep iterating, as we can have early
                 * params and __setups of same names 8( */
                if (line[n] == '\0' || line[n] == '=')
                    had_early_param = 1;
            } else if (!p->setup_func) {
                printk(KERN_WARNING "Parameter %s is obsolete,"
                       " ignored\n", p->str);
                return 1;
            } else if (p->setup_func(line + n))
                return 1;
        }
        p++;
    } while (p < __setup_end);

    return had_early_param;
}

这样就会调用console_setup,并把参数:ttySAC0,115200n8 传给str变量

static int __init console_setup(char *str)
{
    char buf[sizeof(console_cmdline[0].name) + 4]; /* 4 for index */
    char *s, *options, *brl_options = NULL;
    int idx;

    /*
     * Decode str into name, index, options.
     */
    if (str[0] >= '0' && str[0] <= '9') {
        strcpy(buf, "ttyS");
        strncpy(buf + 4, str, sizeof(buf) - 5);
    } else {
        strncpy(buf, str, sizeof(buf) - 1);
    }

   //这里将ttySAC0,115200n8分成了两个字符串” ttySAC0”和”115200n8”
    buf[sizeof(buf) - 1] = 0;
    if ((options = strchr(str, ',')) != NULL)  
        *(options++) = 0;  // 此时options指向字符串”115200n8”

    //这里会从ttySAC0中将0解析出来,这个循环执行完成后,*s就是’0’
    for (s = buf; *s; s++)
        if ((*s >= '0' && *s <= '9') || *s == ',')
            break;
    idx = simple_strtoul(s, NULL, 10);  // 将字符’0’转化成10进制类型的0,然后赋值给idx
    *s = 0;  // 将’0’所在的位置为0,那么就将”ttySAC0”变成了”ttySAC”

    __add_preferred_console(buf, idx, options, brl_options);
    console_set_on_cmdline = 1;
    return 1;
}

 

name=”ttySAC”, idx=0, options=”115200n8”, brl_options=NULL

static int __add_preferred_console(char *name, int idx, char *options,
                   char *brl_options)
{
    struct console_cmdline *c;
    int i;
    
// 此时console_cmdline还没有赋值,所以name[0]是空,循环不成立
// 如果在bootargs中传了两个console参数,那么在解析第二个console参数的时候name[0]就不是空的了
    for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)
        if (strcmp(console_cmdline[i].name, name) == 0 &&
              console_cmdline[i].index == idx) {
                if (!brl_options)
                    selected_console = i;
                return 0;
        }
    if (i == MAX_CMDLINECONSOLES)
        return -E2BIG;
// 从这里可以看出,如果bootargs中传递了两个console参数,如console=ttySAC0,console=ttySAC3,那么最终selected_console就是3
    if (!brl_options) 
        selected_console = i;
    c = &console_cmdline[i];
    strlcpy(c->name, name, sizeof(c->name));   // “ttySAC”
    c->options = options;                   // “115200n8”
    c->index = idx;                         // 0
    return 0;
}

这里可以想一想,如果在bootargs中传递了两个console,如”console=ttySAC0,115200n8 console=ttySAC3,115200n8”,那么:

console_cmdline[0].name = “ttySAC”

console_cmdline[0].index = 0

console_cmdline[0].options = "115200n8"

 

console_cmdline[1].name = “ttySAC”

console_cmdline[1].index = 3

console_cmdline[1].options = "115200n8"

 

selected_console = 1

对于tiny4412,有四个串口,所以会调用四次register_console,但是只有一次能成功,那一次呢?如果bootargs中console参数是ttySAC0,那么只有名为ttySAC0的串口才能成功调用register_console。具体过程,我们下面分析。是在register_console中调用newcon->setup时,因为port的mapbase没有设置而返回错误。因为这四个串口在注册时,会调用probe四次,probe的时候才会为这个port的mapbase赋值,然后才调用register_console的。

 

posted @ 2015-03-15 15:27  摩斯电码  阅读(6221)  评论(0编辑  收藏  举报