串口(PL011)在Linux启动运行过程中扮演的角色

发布时间 2023-06-04 11:31:44作者: ArnoldLu

关键词:PL011、earlyprintk、AMBA、UART、tty、console等等。

串口虽然是一种简单的工具,但是在Linux启动、运行、调试中扮演了重要角色。其稳定、易用、高效(某些场景)。

串口依赖的模块少,在FPGA初期调试中扮演重要角色。往往是CPU基本功能可用后,即可使能串口进行功能调试。

下面记录PL011在Linux不同阶段扮演的角色:

  • 在zImage解压阶段输出调试日志。
  • 在kernel启动初始阶段作为earlyprintk输出日志。
  • 在kernel运行阶段作为console,提供printk()输出和以及交互通道。

1. Kernel Low-Level调试

在Kernel hacking中选择:

Kernel low-level debugging functions
  Kernel low-level debugging port(Kernel low-level debugging via ARM Ltd PL01x Primecell UART)--选择PL01x作为low-level调试串口设备。
Kernel low-level debugging via PL011
Physical base address of debug UART--配置DEBUG_LL串口设备的物理和虚拟基地址。
Virtual base address of debug UART Enable decompressor debugging via DEBUG_LL output--zImage解压缩使用DEBUG_LL作为调试输出,使用DEBUG_LL中实现的putc()函数。

底层驱动提供UART功能macro:

  • addruart:获取uart设备寄存器基地址的物理地址(r1)和虚拟地址(r2)。
  • senduart:将char写入到uart。
  • waituartcts:如果使用流控,检查CTS标志位。
  • waituarttxrdy:检查发送FIFO是否满,如果已满则循环等待。
  • busyuart:检查UART是否正在发送数据,如果正在发送循环等待。

以PL01x实现为例:

#ifdef CONFIG_DEBUG_UART_PHYS
        .macro    addruart, rp, rv, tmp
        ldr    \rp, =CONFIG_DEBUG_UART_PHYS
        ldr    \rv, =CONFIG_DEBUG_UART_VIRT
        .endm
#endif

        .macro    senduart,rd,rx
        strb    \rd, [\rx, #UART01x_DR]
        .endm

        .macro    waituartcts,rd,rx
        .endm

        .macro    waituarttxrdy,rd,rx
1001:        ldr    \rd, [\rx, #UART01x_FR]
 ARM_BE8(    rev    \rd, \rd )
        tst    \rd, #UART01x_FR_TXFF
        bne    1001b
        .endm

        .macro    busyuart,rd,rx
1001:        ldr    \rd, [\rx, #UART01x_FR]
 ARM_BE8(    rev    \rd, \rd )
        tst    \rd, #UART01x_FR_BUSY
        bne    1001b
        .endm

PL010技术参考手册《ARM PrimeCell Technical Reference Manual》。

low-level对外提供putc()输出字符:首先获取uart寄存器物理地址基地址,然后判断CTS标志位;判断发送FIFO是否满,满则等待,不满在写数据到发送寄存器,最后等待发送完成。

#include CONFIG_DEBUG_LL_INCLUDE

ENTRY(putc)
    addruart r1, r2, r3
#ifdef CONFIG_DEBUG_UART_FLOW_CONTROL
    waituartcts r3, r1
#endif
    waituarttxrdy r3, r1
    senduart r0, r1
    busyuart r3, r1
    mov     pc, lr
ENDPROC(putc)

1.2 zImage解压打印

在zImage解压阶段,通过调用putstr()->putc()输出调试信息:

static void putstr(const char *ptr)
{
    char c;

    while ((c = *ptr++) != '\0') {
        if (c == '\n')
            putc('\r');
        putc(c);
    }

    flush();
}

1.2 earlyprintk

earlyprintk是内核console启动之前的调试输出接口,在bootargs中通过earlyprintk使能。

earlyprintk依赖于DEBUG_LL提供的printascii()。其他还包括printhex8、printch等。

如果command line中指定earlyprintk,则注册console,调用early_write()->printascii()通过串口输出。

extern void printascii(const char *);

static void early_write(const char *s, unsigned n)
{
    char buf[128];
    while (n) {
        unsigned l = min(n, sizeof(buf)-1);
        memcpy(buf, s, l);
        buf[l] = 0;
        s += l;
        n -= l;
        printascii(buf);
    }
}

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

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

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

early_param("earlyprintk", setup_early_printk);

2. Kernel UART console

postcore_initcall()对应的initcall等级为2,arch_initcall对应的initcall等级为3,arch_initcall_sync对应的initcall等级为3s。

amba、tty class、vtconsole class为postcore_initcall(),pl011为arch_initcall(),of_platform_default_populate_init()为arch_initcall_sync()。

首先amba总线,以及tty和console类完成初始化,然后注册pl011驱动,最后of_platform_default_populate_init()读取dts创建设备并进行驱动probe。

2.1 AMBA总线注册

amba_init()注册amba总线,由amba_match()函数负责device和driver的匹配。

amba_init(postcore_initcall)
    ->bus_register(amba_bustype)
        ->amba_probe
        ->amba_match
            ->amba_lookup
->amba_probe
->amba_remove
->amba_shutdown
->amba_pm
->amba_pm_runtime_suspend
->amba_pm_runtime_resume

amba_match()读取设备的id,然后查找匹配:

static int amba_match(struct device *dev, struct device_driver *drv)
{
    struct amba_device *pcdev = to_amba_device(dev);
    struct amba_driver *pcdrv = to_amba_driver(drv);

    if (!pcdev->periphid) {
        int ret = amba_read_periphid(pcdev);

        if (ret)
            return -EPROBE_DEFER;
        dev_set_uevent_suppress(dev, false);
        kobject_uevent(&dev->kobj, KOBJ_ADD);
    }

    /* When driver_override is set, only bind to the matching driver */
    if (pcdev->driver_override)
        return !strcmp(pcdev->driver_override, drv->name);

    return amba_lookup(pcdrv->id_table, pcdev) != NULL;
}

2.2 tty_class_init

tty_class_init()创建tty_class类,后续创建tty设备归类为tty_class。

2.3 PL011驱动

dts中配置compatible名称,寄存器、中断、时钟:

ap_serial0@xxxx {
   compatible = "arm,pl011", "arm,primecell";
   reg=<0xXXXXXXXX 0x1000>;  
   interrupts=<GIC_SPI xx IRQ_TYPE_LEVEL_HIGH>; 
   interrupt-parent=<&gic>; 
   clock-names="xxxx" ;
};

pl011_init()将pl011_driver驱动注册到amba总线上。

pl011_init(arch_initcall)
    ->amba_driver_register
        ->driver_register
          ->bus_add_driver
            ->driver_attach
              ->bus_for_each_dev
                ->__driver_attach
                    ->device_driver_attach
                    ->driver_probe_device

其中pl011_ids中定义了需要匹配的amba总线设备,在amba_mach()->amba_lookup中执行匹配。

当pl011的设备和驱动匹配后,调用pl011_probe()注册UART端口,以及console。

static struct amba_driver pl011_driver = {
    .drv = {
        .name    = "uart-pl011",
        .pm    = &pl011_dev_pm_ops,
        .suppress_bind_attrs = IS_BUILTIN(CONFIG_SERIAL_AMBA_PL011),
    },
    .id_table    = pl011_ids,
    .probe        = pl011_probe,
    .remove        = pl011_remove,
};

amba_reg定义了uart驱动tty设备名为ttyAMAx,console对应amba_console。

static struct uart_driver amba_reg = {
    .owner            = THIS_MODULE,
    .driver_name        = "ttyAMA",
    .dev_name        = "ttyAMA",
    .major            = SERIAL_AMBA_MAJOR,
    .minor            = SERIAL_AMBA_MINOR,
    .nr            = UART_NR,
    .cons            = AMBA_CONSOLE,
};

pl011_probe()在AMBA总线上和PL011设备匹配之后被调用,将UART注册到tty层,并更新之前earlyprink注册侧console。

pl011_probe
->pl011_setup_port
->pl011_register_port
->pl011_write()--屏蔽并清空UART中断。
->uart_register_driver--将UART注册为tty设备。
->tty_port_init
->tty_register_driver
->uart_add_one_port
->uart_configure_port
->uart_report_port
->register_console--注册console,即amba_console。

对PL011设备驱动的的amba_pl011_pops中定义了UART底层操作函数集:

static const struct uart_ops amba_pl011_pops = {
    .tx_empty    = pl011_tx_empty,
    .set_mctrl    = pl011_set_mctrl,
    .get_mctrl    = pl011_get_mctrl,
    .stop_tx    = pl011_stop_tx,
    .start_tx    = pl011_start_tx,
    .stop_rx    = pl011_stop_rx,
    .enable_ms    = pl011_enable_ms,
    .break_ctl    = pl011_break_ctl,
    .startup    = pl011_startup,
    .shutdown    = pl011_shutdown,
    .flush_buffer    = pl011_dma_flush_buffer,
    .set_termios    = pl011_set_termios,
    .type        = pl011_type,
    .release_port    = pl011_release_port,
    .request_port    = pl011_request_port,
    .config_port    = pl011_config_port,
    .verify_port    = pl011_verify_port,
#ifdef CONFIG_CONSOLE_POLL
    .poll_init     = pl011_hwinit,
    .poll_get_char = pl011_get_poll_char,
    .poll_put_char = pl011_put_poll_char,
#endif
};

 

2.4 根据dts创建设备of_platform_default_populate_init,并调用pl011驱动进行probe。

 of_platform_default_populate_init从设备树中获取数据,创建设备然后进行驱动probe。

of_platform_default_populate_init(arch_initcall_sync)
    ->of_platform_default_populate
      ->of_platform_populate--从设备树中获取数据创建platform设备。
        ->of_platform_bus_create
          ->of_amba_device_create(if "arm,primecell")--创建AMBA设备。
            ->amba_device_add
              ->amba_device_try_add
                ->device_add
                  ->bus_probe_device
                    ->device_initial_probe
                      ->__device_attach
                        ->bus_for_each_drv(dev->bus, NULL, &data, __device_attach_driver)
                          ->driver_probe_device
                            ->really_probe
                              ->dev->bus->probe(dev)
                                ->amba_probe
                                  ->调用to_amba_driver(dev->driver)->probe函数
                                    ->pl011_probe--对应pl011的驱动probe是pl011_probe()。
                                      ->pl011_register_port
                                        ->uart_add_one_port
                                          ->uart_configure_port
                                            ->register_console
        ->of_platform_device_craete_pdata

2.5 cmdline console

通过cmdline配置printk()的console:

console=ttyAMA0,115200 rdinit=/sbin/init root=/dev/ram1 rw ignore_level earlyprintk

console_setup被do_early_param()调用,解析命令行console中的值,逗号分割console设备名称和选项。多个选项通过逗号分割:

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

    if (_braille_console_setup(&str, &brl_options))
        return 1;

    if (str[0] >= '0' && str[0] <= '9') {
        strcpy(buf, "ttyS");
        strncpy(buf + 4, str, sizeof(buf) - 5);
    } else {
        strncpy(buf, str, sizeof(buf) - 1);
    }
    buf[sizeof(buf) - 1] = 0;
    options = strchr(str, ',');
    if (options)
        *(options++) = 0;
    for (s = buf; *s; s++)
        if (isdigit(*s) || *s == ',')
            break;
    idx = simple_strtoul(s, NULL, 10);
    *s = 0;

    __add_preferred_console(buf, idx, options, brl_options);--根据console指定设备名称,找到preferred_console的序号。
    console_set_on_cmdline = 1;
    return 1;
}
__setup("console=", console_setup);

/dev/ttyAMA0映射到/dev/console,对/dev/console或者/dev/ttyAMA0的写会输出到console中。

2.6 用户空间console

 getty打开/dev/console设备,弹出登录输入提示,然后启动login进行登录。

console::respawn:/sbin/getty -L console 0 vt100 # GENERIC_SERIAL

2.7 使用UART的一些调试手段

打开更多Log显示:

  • 在Makefile中增加KBUILD_CFLAGS += $(KCFLAGS) -DDEBUG
  • 修改init/main.c中的initcall_debug = true,或在bootargs中增加initcall_debug即可。查看各module初始化进入退出,以及耗时。