在虚拟化的环境下要使用串口,会涉及到两方面的内容:第一是domain OS的uart使用,第二是hypervisior的uart使用。在这里,domain OS使用的是linux,hypervisior使用xen,所以讲述的时候会以linux以及xen为基础展开。
如果想更早地使用printk函数,比如在安装注册UART驱动之前就使用printk,这时就需要自己去注册console。
更早地、单独地注册console,有两种方法:
earlycon是新的、推荐的方法,在内核已经有驱动的前提下,通过设备树或cmdline指定寄存器地址即可
源码为:arch\arm\kernel\early_printk.c,要使用它,必须实现这几点:
early_param("earlyprintk", setup_early_printk);
early_param 宏注册的内核选项必须要在其他内核选项之前被处理,只需要知道当从cmdline中查找到对应字符串时,对应的setup函数会被调用。
sunxi平台下,自己单独搞了一个CONFIG_AW_EARLY_PRINTK宏以及early_printk.c(bsp/drivers/uart),实在匪夷所思。
static int __init setup_early_printk(char *buf)
{
......
/* parse paddr, like earlyprintk=sunxi-uart,0x05000000 */
if (!strncmp(buf, ",0x", 3)) {
paddr = simple_strtoul(buf + 1, &e, 16); /* get mmio paddr */
}
if (paddr) { // set vaddr
set_fixmap_io(FIX_EARLYCON_MEM_BASE, paddr);
early_base = (void __iomem *)(fix_to_virt(FIX_EARLYCON_MEM_BASE)
|(paddr & (PAGE_SIZE - 1)));
}
printch = sunxi_uart_printch; /* if name == "sunxi-uart" */
early_console = &early_console_dev; /* 设置了early_console的实现 */
register_console(&early_console_dev); /* 向kernel注册一个console */
}
static struct console early_console_dev = {
.name = "earlycon",
.write = early_write, /* 实现console输出的入口,核心函数 */
.flags = CON_PRINTBUFFER | CON_BOOT,
.index = -1,
};
CON_PRINTBUFFER标识,表示注册这个console的时候,需要把printk的buf中的log通过这个console进行输出。
CON_BOOT标识,表示这是一个boot console(bcon)。当启动过程了注册其他非boot console的时候,需要先卸载掉这个console。
early_write也就是要实现printk和early_printk的核心。
static void early_write(struct console *con, const char *s, unsigned n)
{
while (n-- > 0) {
if (*s == '\n')
printch('\r');
printch(*s);
s++;
}
}
static void sunxi_uart_printch(char ch)
{
while (!(readl_relaxed(early_base + (UART_USR << 2)) & UART_USR_NF));
writel_relaxed(ch, early_base + (UART_TX << 2));
}
注意:bootloader中对于uart已经初始化完成并且可以正常使用的基础上,直接往uart的tx寄存器中写入数据,从而实现串口输出的目的。
至此,有两种方法通过early_console来输出log
直接调用early_console->write(early_console, buf, n)
early_printk函数就是通过这种方法实现的
通过标准printk接口调用到console的write函数
printk函数就是通过这种方法实现
kernel/printk/printk.c
#ifdef CONFIG_EARLY_PRINTK
struct console *early_console;
asmlinkage __visible void early_printk(const char *fmt, ...)
{
va_list ap;
char buf[512];
int n;
if (!early_console)
return;
va_start(ap, fmt);
n = vscnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
early_console->write(early_console, buf, n);
}
#endif
直接通过early_console->write来进行console的输出。
当register_console(&early_console_dev)完成之后,console子系统中的console_drivers就存在了early_console_dev这个console。
经过printk的标准调用之后
printk->vprintk->console_unlock->call_console_drivers
在call_console_drivers调用如下
for_each_console(con) {
con->write(con, text, len);
}
#define for_each_console(con) \
for (con = console_drivers; con != NULL; con = con->next)
early_console_dev作为当前console_drivers一个con,其write函数也会被调用。
early_console_dev->write(con, text, len);
earlycon就是early console的意思,实现的功能跟early printk是一样的,只是更灵活。我们知道,对于console,最主要的是里面的write函数:它不使用中断,相对简单。
源码为:drivers/tty/serial/earlycon.c,要使用它,必须实现这几点:
配置内核,选择:CONFIG_SERIAL_EARLYCON & CONFIG_OF_EARLY_FLATTREE
内核中实现:printch函数
在cmdline中添加earlycon
注意,cmdline信息中,"earlycon"或“earlycon=xxx”(earlycon=uart8250,mmio32,0x02500000)都会触发param_setup_earlycon函数
early_param("earlycon", param_setup_earlycon)
static int __init param_setup_earlycon(char *buf)
{
int err;
/* Just 'earlycon' is a valid param for devicetree and ACPI SPCR. */
if (!buf || !buf[0]) {
if (IS_ENABLED(CONFIG_ACPI_SPCR_TABLE)) {
earlycon_acpi_spcr_enable = true;
return 0;
} else if (!buf) {
return early_init_dt_scan_chosen_stdout(); /* earlycon不带参数,需要从设备树里解析 */
}
}
err = setup_earlycon(buf);
if (err == -ENOENT || err == -EALREADY) /* earlycon带有参数,不需要解析设备树 */
return 0;
return err;
}
搜索dts,找到相应的节点。
int __init early_init_dt_scan_chosen_stdout(void)
offset = fdt_path_offset(fdt, "/chosen");
if (offset < 0)
offset = fdt_path_offset(fdt, "/chosen@0");
p = fdt_getprop(fdt, offset, "stdout-path", &l);
if (!p)
p = fdt_getprop(fdt, offset, "linux,stdout-path", &l);
......
for (match = __earlycon_table; match < __earlycon_table_end; match++) {
if (of_setup_earlycon(match, offset, options) == 0) /* 匹配上了,运行of_setup_earlycon */
return 0;
首先从chosen节点中获取stdout-path或者linux,stdout-path属性,这两个属性指明了标准输入输出串口的dtsi节点路径。然后调用of_setup_earlycon进行下一步setup。
int __init of_setup_earlycon(const struct earlycon_id *match, unsigned long node, const char *options)
struct uart_port *port = &early_console_dev.port;
port->iotype = UPIO_MEM;
port->mapbase = addr;
port->uartclk = BASE_BAUD * 16;
port->membase = earlycon_map(port->mapbase, SZ_4K); /* 获取寄存器地址 */
earlycon_init(&early_console_dev, match->name);
err = match->setup(&early_console_dev, options); /* 调用对应的平台函数,比如sunxi_early_console_setup来设置write输出函数 */
register_console(early_console_dev.con); /* 调用register_console向console子系统注册early_console_dev.con */
到此,printk就可以通过early_console_dev.con->write来进行输出log了。
每个earlycon都对应一个earlycon_id,所有的earlycon_id都被维护__earlycon_table中。定义方法如下:
OF_EARLYCON_DECLARE(uart0, "", sunxi_early_console_setup)
static int __init sunxi_early_console_setup(struct earlycon_device *dev, const char *opt)
{
if (!dev->port.membase)
return -ENODEV;
dev->con->write = sunxi_early_serial_write;
return 0;
}
@TODO
系统刚起来的时候,还没注册串口等设备,此时无法通过正常的console来输出log。为此,可以使用early prink来打印信息。
ps:通过输出设备(比如串口设备)的简单write方法直接打印数据,write由平台实现
xen的early_printk要使用,必须把对应的宏打开:CONFIG_EARLY_PRINTK。这决定着early_puts函数是空还是有具体实现:
/* xen/include/xen/early_printk.h */
#ifdef CONFIG_EARLY_PRINTK
void early_puts(const char *s, size_t nr);
#else
#define early_puts NULL
#endif
在xen中,printk的流程如下:
printk
vprintk_common
__putstr
console_serial_puts /* 核心在于console_serial_puts函数 */
################################################################################################################################
static void (*serial_steal_fn)(const char *, size_t nr) = early_puts; /* 若定义了CONFIG_EARLY_PRINTK,则该函数不为空 */
void console_serial_puts(const char *s, size_t nr)
{
if ( serial_steal_fn != NULL ) /* 若定义了CONFIG_EARLY_PRINTK,则这里会执行early_puts函数 */
serial_steal_fn(s, nr);
else
serial_puts(sercon_handle, s, nr); /* 否则,使用uart driver console */
/* Copy all serial output into PV console */
pv_console_puts(s, nr);
}
early_puts的实现如下:
/* xen/arch/arm/early_printk.c */
void early_puts(const char *s, size_t nr)
{
while ( nr-- > 0 )
{
if (*s == '\n')
early_putch('\r');
early_putch(*s);
s++;
}
/*
* Wait the UART has finished to transfer all characters before
* to continue. This will avoid lost characters if Xen abort.
*/
early_flush();
}
early_putch的实现如下:
/* xen/arch/arm/arm64/debug.S */
/*
* Print a character on the UART - this function is called by C
* x0: character to print
*/
GLOBAL(early_putch)
ldr x15, =EARLY_UART_VIRTUAL_ADDRESS /* uart首地址映射后可访问的虚拟地址 */
early_uart_ready x15, 1
early_uart_transmit x15, w0
ret
/* Flush the UART - this function is called by C */
GLOBAL(early_flush)
ldr x15, =EARLY_UART_VIRTUAL_ADDRESS /* x15 := VA UART base address */
early_uart_ready x15, 1
ret
#define EARLY_UART_VIRTUAL_ADDRESS \
(FIXMAP_ADDR(FIXMAP_CONSOLE) + (CONFIG_EARLY_UART_BASE_ADDRESS & ~PAGE_MASK)) /* CONFIG_EARLY_UART_BASE_ADDRESSD对应物理uart的地址 */
PRINT的软件流程
#define PRINT(_s) \
mov x3, lr ; \
adr x0, 98f ; \
bl puts ; \
mov lr, x3 ; \
RODATA_STR(98, _s)
/* Print early debug messages.
* x0: Nul-terminated string to print.
* x23: Early UART base address
* Clobbers x0-x1 */
puts:
early_uart_ready x23, 1
ldrb w1, [x0], #1 /* Load next char */
cbz w1, 1f /* Exit on nul */
early_uart_transmit x23, w1
b puts
1:
ret
ENDPROC(puts)
xen的console是怎么实现的?
uart初始化
GLOBAL(start)
bl init_uart
ldr x23, =CONFIG_EARLY_UART_BASE_ADDRESS
start_xen
--> arm_uart_init
--> dt_uart_init /* xen/drivers/char/arm-uart.c */
--> if (!console_has("dtuart")) /* 必须带这个参数 */
return;
--> dt_find_node_by_path(devpath) /* 给xen使用的uart节点 */
--> device_init(dev, DEVICE_SERIAL, options) /* 串口初始化 */
--> desc->init(dev, data) /* ns16550_uart_dt_init */
--> ns16550结构体uart的初始化
--> serial_register_uart(idx, &ns16550_driver, uart) /* 核心 */
--> com[idx].driver = driver;
--> com[idx].uart = uart;
--> dt_device_set_used_by(dev, DOMID_XEN)
--> console_init_preirq
--> serial_init_preirq()
--> com[i].driver->init_preirq(&com[i]) /* ns16550_init_preirq */
--> if ((sh = serial_parse_handle(p)) >= 0)
{
sercon_handle = sh;
/*
printk打印会判断serial_steal_fn函数是否为空,不为空,就使用eralyprintk打印。
这里置为空,让printk使用uart driver的api来打印
*/
serial_steal_fn = NULL;
}
修改sunxi平台的打印串口,涉及到以下几方面:
模块 | 说明 |
---|---|
clk | 具体查看对应平台的spec,改动点是gate位以及reset位 |
pinctrl | 具体查看对应平台的spec,把引脚修改为tx以及rx功能 |
uart | 具体查看对应平台的spec,涉及UART_MCR、UART_LCR、UART_DLM、UART_DLL、UART_FCR寄存器,主要就是配置波特率、校验位、数据位以及停止位 |
env.cfg文件 | 在device/config/chips/xxx/configs/default/目录下,xxx为具体chip。里面会配置bootargs,设置console、earlyprintk等信息 |
sys_config.fex文件 | 在device/config/chips/xxx/configs/yyy/sys_config.fex目录下,xxx为具体chip,yyy为对应的版型。里面的uart_para会设置boot0以及uboot使用的打印串口。默认是uart0 |
具体逻辑可以参考boot0代码的sunxi_serial_init函数:brandy-2.0/spl/drivers/serial.c