关键词: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初始化进入退出,以及耗时。