xen-uart

xen-uart

在虚拟化的环境下要使用串口,会涉及到两方面的内容:第一是domain OS的uart使用,第二是hypervisior的uart使用。在这里,domain OS使用的是linux,hypervisior使用xen,所以讲述的时候会以linux以及xen为基础展开。

一、Linux

如果想更早地使用printk函数,比如在安装注册UART驱动之前就使用printk,这时就需要自己去注册console。

更早地、单独地注册console,有两种方法:

  • early_printk:自己实现write函数,不涉及设备树,简单明了
  • earlycon:通过设备树传入硬件信息,跟内核中驱动程序匹配

earlycon是新的、推荐的方法,在内核已经有驱动的前提下,通过设备树或cmdline指定寄存器地址即可

1. early_printk

源码为:arch\arm\kernel\early_printk.c,要使用它,必须实现这几点:

  • 配置内核,选择:CONFIG_EARLY_PRINTK
  • 内核中实现:printch函数
  • cmdline中添加:earlyprintk,例如earlyprintk=sunxi-uart,0x05000000

①解析cmdline中的early_printk

early_param("earlyprintk", setup_early_printk);

early_param 宏注册的内核选项必须要在其他内核选项之前被处理,只需要知道当从cmdline中查找到对应字符串时,对应的setup函数会被调用。

②setup_early_printk

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 */
}

③early_console_dev实现

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++;
	}
}

④printch实现

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

  1. 直接调用early_console->write(early_console, buf, n)

    early_printk函数就是通过这种方法实现的

  2. 通过标准printk接口调用到console的write函数

    printk函数就是通过这种方法实现

⑤early_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的输出。

⑥printk软件流程

当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);

2. earlycon

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函数

① 解析cmdline中的earlycon参数(drivers/of/fdt.c)

early_param("earlycon", param_setup_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;
}

③ early_init_dt_scan_chosen_stdout(drivers/of/fdt.c)

搜索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。

④ of_setup_earlycon

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都对应一个earlycon_id,所有的earlycon_id都被维护__earlycon_table中。定义方法如下:

OF_EARLYCON_DECLARE(uart0, "", sunxi_early_console_setup)

⑥ 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;
}

3. console

@TODO

二、Xen

1. xen early_prink

系统刚起来的时候,还没注册串口等设备,此时无法通过正常的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)     

2. xen console

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

三、MISC

修改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

你可能感兴趣的:(虚拟化,linux,arm开发)