本设备树串口驱动基于linux3.0.4内核版本
start kernel()
从start kernel函数开始追踪,函数原型如下,函数实体路径见./init/main.c。可以看到该函数实体包含两个有关于串口初始化函数的调用实体,第一个是early_printk函数实现,第二个是标准设备树串口驱动实现,下面本文将会逐步进行追踪分析。
asmlinkage void __init start_kernel(void)
{
.............
setup_arch(&command_line);
.............
console_init();
.............
}
setup_arch()
首先,追踪分析early_printk初始化调用流程,继续追踪setup_arch()函数实体,函数原型如下,函数实体路径见./arch/mips/kernel/setup.c。第一个函数功能是探测cpu属于什么架构,常见有arm/mips/x86架构,本文剖析是基于探测mips架构。第二个函数功能是解析boot传参环境变量与命令行,初始化linux操作系统环境变量。第三个函数就是本文重点关注setup_early_printk()函数实体,即串口早期的打印初始化流程。
void __init setup_arch(char **cmdline_p)
{
cpu_probe();
prom_init();
#ifdef CONFIG_EARLY_PRINTK
setup_early_printk();
#endif
.............
.............
setup_early_printk()
继续追踪setup_early_printk()函数实体,函数原型如下,函数实体路径见
./arch/mips/kernel/early_printk.c,关于register_console()注册函数会在标准设备树串口驱动注册进行讲解,不在此处赘述。本文重点关注结构体struct early_console内成员变量.write = early_console_write,可以看到函数实体early_console_write()实现了简单prom_putchar()打印封装。对于prom_putchar()这函数实现,由于不同平台硬件架构会有不同的串口地址与之对应,故需要开发者自行设计函数存放路径与函数实体。
extern void prom_putchar(char);
static void __init
early_console_write(struct console *con, const char *s, unsigned n)
{
while (n-- && *s) {
if (*s == '\n')
prom_putchar('\r');
prom_putchar(*s);
s++;
}
}
static struct console early_console __initdata = {
.name = "early",
.write = early_console_write,
.flags = CON_PRINTBUFFER | CON_BOOT,
.index = -1
};
static int early_console_initialized __initdata;
void __init setup_early_printk(void)
{
if (early_console_initialized)
return;
early_console_initialized = 1;
register_console(&early_console);
}
至此early printk串口流程分析完成,比较简单。
题外寄语:若不作此处早期的串口打印初始化流程分析,博主觉得对于介绍下面的标准设备树串口驱动注册不是一个完整的系统剖析文章。
console_init()
接下来,回到start kernel函数,追踪分析console_init()函数实体,函数原型如下,函数实体路径见drivers/tty/tty_io.c。tty_ldisc_begin()函数暂时不作追踪分析,直接看while流程处理,call作了初始化赋值__con_initcall_start,跑while循环调用call函数一直到__con_initcall_end结束,那么此处call上面挂载一个什么功能的函数实体,
void __init console_init(void)
{
initcall_t *call;
tty_ldisc_begin();
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
继续追踪源码,函数原型如下,函数实体路径见./include/asm-generic/vmlinux.lds.h
,可以看到__con_initcall_start与__con_initcall_end之间多了一个.con_initcall.init,全部被定义成一个CON_INITCALL宏。CON_INITCALL宏在内核启动流程之前会被翻译成符号表,在内核启动过程进行初始化。有关于vmlinux.lds.h文件与vmlinux.lds.h文件编译成System.map符号表文件,本文不作赘述,感兴趣的小伙伴可以继续追踪研究System.map
#define CON_INITCALL \
VMLINUX_SYMBOL(__con_initcall_start) = .; \
*(.con_initcall.init) \
VMLINUX_SYMBOL(__con_initcall_end) = .;
继续追踪源码.con_initcall.init实体,函数原型如下,函数实体路径见./include/linux/init.h
,可以看出.con_initcall.init实体被定义成一个宏console_initcall(fn),其中fn又是什么,本文继续追踪console_initcall(fn)
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__used __section(.con_initcall.init) = fn
console_initcall(fn)
继续追踪函数实体console_initcall(fn)初始化赋值,追踪分析变量fn在什么地方被赋值,函数初始化如下,函数初始化路径见./drivers/tty/serial/8250.c。可以看到这里的fn等于标准设备树串口驱动serial8250_console_init()。至此,层次越来越清楚了,追到了熟悉标准串口驱动。
static int __init serial8250_console_init(void)
{
if (nr_uarts > UART_NR)
nr_uarts = UART_NR;
serial8250_isa_init_ports();
register_console(&serial8250_console);
return 0;
}
console_initcall(serial8250_console_init);
小结:
1.early_printk初始化流程:start kernel()–>setup_arch()–>setup_early_printk()
2.console 初始化流程分两步
第一步,注册标准设备树串口驱动console_initcall()–>serial8250_console_init()
第二步,调用注册好的标准设备树串口驱动start kernel()–>console_init()
来源:CSDN
作者:银河湾仔
链接:https://blog.csdn.net/bbsn2019/article/details/104646935