跑过trap_init之后,跑进console_init就停止了。问题出在console_init里面。从字面上看是控制台初始化。而我们现在的控制是串口0。所以这部分的问题应该出在串口上。之前可以打印信息是因为用了prom_printf,这个函数定义在./arch/mips/arc/console.c文件里:
void prom_printf(char *fmt, ...)
{
va_list args;
char ppbuf[1024];
char *bptr;
va_start(args, fmt);
vsprintf(ppbuf, fmt, args);
bptr = ppbuf;
while (*bptr != 0) {
if (*bptr == '\n')
prom_putchar('\r');
prom_putchar(*bptr++);
}
va_end(args);
}
从这段代码中可以得到两个重要信息:第一个,打印的字符串最长为1024个字节;第二,打印的实际执行者是prom_putchar(),这个执行者与平台相关,定义在./arch/mips/mips-boards/soc3210-boards/soc-soc/prom.c中。prom_putchar的实际执行者是putDebugChar,定义在./arch/mips/mips-boards/soc3210-boards/soc-soc/dbg_io.c中。可以看到实际的硬件操作了。
像《Linux Mips Porting Guide》所说的,prom_printf是eary_printk。
实际的printk的初始化应该在console_init中。
修改控制台串口的参数
进入console_init函数之后:
void __init console_init(void)
{
initcall_t *call;
/* Setup the default TTY line discipline. */
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
/*
* set up the console device so that later boot sequences can
* inform about problems etc..
*/
#ifdef CONFIG_EARLY_PRINTK
disable_early_printk();
#endif
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
发现,好像初始化串口的代码找不到,只有一段怪怪的代码:
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
上网查了一些资料,了解到__con_initcall_start实际上是一个地址值,在./arch/mips/kernel/vmlinux.lds.S中指定了。打开vmlinux.lds.S文件,看到此部分代码:
__con_initcall_start = .;
.con_initcall.init : { *(.con_initcall.init) }
__con_initcall_end = .;
显然,__con_initcall_start这个地址值,实际上保存了 .con_initcall.init这个符号,这个值在include/linux/init.h里定义了。查看此部分代码:
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__attribute_used__ __attribute__((__section__(".con_initcall.init")))=fn
显然,通过宏函数console_initcall来指定一个函数指针,而这个函数指针保存在.con_initcall.init这个段中。
也就是说,(*call)();的实际执行者是console_initcall指定的函数。所以要找到这个指定的函数,这个函数一定在串口驱动里面。
打开./drivers/serial/8250.c,我们可以找到:
console_initcall(serial8250_console_init);
也就是调用serial8250_console_init来进行初始化串口的。
static int __init serial8250_console_init(void)
{
serial8250_isa_init_ports();
register_console(&serial8250_console);
return 0;
}
serial8250_isa_init_ports
这个函数主要是获取一些硬件信息,如串口的IO基地址呀之类的:
for (i = 0, up = serial8250_ports;
i < ARRAY_SIZE(old_serial_port) && i < nr_uarts;
i++, up++) {
up->port.iobase = old_serial_port[i].port;
up->port.irq = irq_canonicalize(old_serial_port[i].irq);
up->port.uartclk = old_serial_port[i].baud_base * 16;
up->port.flags = old_serial_port[i].flags;
up->port.hub6 = old_serial_port[i].hub6;
up->port.membase = old_serial_port[i].iomem_base;
up->port.iotype = old_serial_port[i].io_type;
up->port.regshift = old_serial_port[i].iomem_reg_shift;
if (share_irqs)
up->port.flags |= UPF_SHARE_IRQ;
}
从这段代码里看到,我们的硬件基本信息是来自于一个叫做old_serial_port的数组。
如果有多个串口要进行初始化,那么串口的数量决定于old_serial_port的长度和nr_uarts这个变量。
先来看nr_uarts这个变量:
static unsigned int nr_uarts = CONFIG_SERIAL_8250_RUNTIME_UARTS;
也就是说这个变量值取决于CONFIG_SERIAL_8250_RUNTIME_UARTS这个宏,这个宏是CONFIG为前缀的,应该是在配置内核里决定的,所以可以到./include/linux/autoconf.h中去找,就在这个文件里定义的。龙芯3210支持两路UART,所以这个值设置大于或者等于2就可以了。
接下来把重点放在old_serial_port这个数组上。
static const struct old_serial_port old_serial_port[] = {
SERIAL_PORT_DFNS /* defined in asm/serial.h */
};
这个数组的成员是由宏SERIAL_PORT_DFNS指定,所以在./include/asm-mips/serial.h中找到这个宏定义:
#define SERIAL_PORT_DFNS \
DDB5477_SERIAL_PORT_DEFNS \
EV96100_SERIAL_PORT_DEFNS \
IP32_SERIAL_PORT_DEFNS \
ITE_SERIAL_PORT_DEFNS \
IVR_SERIAL_PORT_DEFNS \
JAZZ_SERIAL_PORT_DEFNS \
STD_SERIAL_PORT_DEFNS \
MOMENCO_OCELOT_G_SERIAL_PORT_DEFNS \
MOMENCO_OCELOT_C_SERIAL_PORT_DEFNS \
MOMENCO_OCELOT_SERIAL_PORT_DEFNS \
MOMENCO_OCELOT_3_SERIAL_PORT_DEFNS
可以了解到,这个宏里根据每个平台的不同,定义了串口硬件相关的值。所以在这里,我们加入龙芯3210的串口定义:
#define SERIAL_PORT_DFNS \
SOC32101_PORT_DENFS \
DDB5477_SERIAL_PORT_DEFNS \
EV96100_SERIAL_PORT_DEFNS \
IP32_SERIAL_PORT_DEFNS \
ITE_SERIAL_PORT_DEFNS \
IVR_SERIAL_PORT_DEFNS \
JAZZ_SERIAL_PORT_DEFNS \
STD_SERIAL_PORT_DEFNS \
MOMENCO_OCELOT_G_SERIAL_PORT_DEFNS \
MOMENCO_OCELOT_C_SERIAL_PORT_DEFNS \
MOMENCO_OCELOT_SERIAL_PORT_DEFNS \
MOMENCO_OCELOT_3_SERIAL_PORT_DEFNS
注意到,加入到最前面,因为放到后面的话,nr_uarts的值,也就是CONFIG_SERIAL_8250_RUNTIME_UARTS在配置内核时要设置比较大的值,否则是不能获取到SOC32101_PORT_DENFS这个参数的,所以放在最前面就行了。
然后再定义SOC32101_PORT_DENFS的值:
#ifdef CONFIG_SOC_SOC
#include <asm/soc-soc/soc_soc.h>
#include <asm/soc-soc/soc_soc_int.h>
#ifdef BASE_BAUD
#undef BASE_BAUD
#endif
#define BASE_BAUD (44000000/16)
#define SOC32101_PORT_DENFS \
{ 0, BASE_BAUD, SOC_SOC_UART0_BASE, \
SOC_SOC_UART0_IRQ, STD_COM_FLAGS }, \
{ 0, BASE_BAUD, SOC_SOC_UART1_BASE, \
SOC_SOC_UART1_IRQ, STD_COM_FLAGS },
#else
#define SOC32101_PORT_DENFS
#endif
register_console(&serial8250_console);
如果说上一个函数是获取信息,那么这个函数就是实际操作。主要是设置实际的硬件参数,如波特率呀停止位等的设置。
首先是分析从bootloader传入来的命令参数,然后把这些关于串口配置的参数传入到serial8250_console_setup。
再次编译,运行。
发现,还是在console_init里停住了。
一路用打印,查找,调试,终于发现程序在第一次写串口的寄存器操作时死机了。
->serial8250_console_setup
->uart_set_options
->serial8250_set_termios
->serial_dl_write
->_serial_dl_write
->outb
outb函数出问题了,也就是对IO操作的时候死机了。系统对IO的操作outb:
#define __BUILD_IOPORT_SINGLE(pfx, bwlq, type, p, slow) \
\
static inline void pfx##out##bwlq##p(type val, unsigned long port) \
{ \
volatile type *__addr; \
type __val; \
\
__addr = (void *)__swizzle_addr_##bwlq(mips_io_port_base + port); \
\
__val = pfx##ioswab##bwlq(__addr, val); \
\
/* Really, we want this to be atomic */ \
BUILD_BUG_ON(sizeof(type) > sizeof(unsigned long)); \
\
*__addr = __val; \
slow; \
}
实则上调用这个函数,不过怎么调用到这个函数,现在还没弄清楚。
回到我们的问题,这个函数中会把地址从port转为实际的操作的地址__addr,这里有个基地址mips_io_port_base,这个地址在setup_arch里进行了初始化:
->setup_arch
->arch_mem_init
->plat_mem_setup
->set_io_port_base(PTR_PAD(0xbf000000));
发现 mips_io_port_base = 0xbf000000,而UART0的基地址是0x1f004080,那么实际的操作地址__addr是两者相加,那么变成了0xde004080了???这个是一个错误的地址,怪不得会死机了。
可以猜得出,本意是相或,这样就可以转为正确的地址了,也可以set_io_port_base传入参数改为0xa0000000。这样应该就没问题了。
编译运行,OK,过了,但是出现乱码!!!
初步分析原因:
串口打印出现乱码一般情况下就是波特率设置有问题,但查看设置参数,传入的波特率确实是115200,那就是设置串口时钟分频有问题了。
找到计算串口时钟的代码:
quot = (port->uartclk + (8 * baud)) / (16 * baud);
没有问题,对照旧的可用内核,公式也是这样。
后来通过对照,发现去掉:
serial_outp(up, UART_LCR, cval | UART_LCR_DLAB);
就可以了。
这个操作是设置串口的线控制器的,其中UART_LCR_DLAB是使偏移地址为0和1的寄存器为分频寄存器的,默认情况下是数据寄存器。
照看3210手册,确定应该是有这个操作的,但是又非要去掉才正常。搞不明白,可能是芯片自身的问题。
到此为此,内核可以跑到命令行了,也就是控制台,基本完成了内核的移植,文件系统是用ramdisk。
对于驱动,还要调试。
PS:内核基本上可以跑起来了,在这个调试的过程中,对内核的启动有了比较清晰的认识,遇到问题最終还是得查看代码。首先是确定问题出现在地方,然后再去分析为什么出现这个问题,然后再去解决。在确定代码可能出现的地方,这次用的是比较笨的办法,就是通过在很多地方插入prom_printf。当然也不能盲目的插入,可以先看代码,觉得可能出现问题的地方,或者搞得不太懂的地方再printf,另外也可以用二分法来确定位置,这个就更死板了。完成了基本的移植工作,但是还有很多地方还是没有完全搞清楚,例如cache和TLB的一些设置了,还得继续深入学习。