X-012-KERNEL-serial early console的移植

作者:wowo 发布于:2016-10-2 22:50 分类:X Project

1. 前言

对Linux kernel工程师来说,最依赖的工具非printk莫属(不多解释,大家都懂)。因此,在Linux kernel移植的初期阶段,如果能够尽快地实现printk功能,将会为后续的工作带来极大的帮助。

在众多可用作printk输出的终端里面(串口、屏幕、USB、网络、等等),串口终端(也即串口驱动)无疑是实现起来最简单一种,因此也是嵌入式linux开发过程中(特别是早期阶段)最普遍使用的。

但是,受限于Linux TTY框架的复杂性[1],长久以来,在Kernel移植的初期阶段(各种功能都不ready,缺乏有效的调试手段),快速的实现serial driver也是一个不小的挑战。不过,随着serial subsystem中的early console功能的出现,这种状况得到了极大的改善。

本文将借助“X Project” kernel的开发过程,介绍serial early console功能的移植过程。

注1:博客中“Linux TTY子系统”的分析正在缓慢推进,不过还未涉及console、serial subsystem、earlycon等模块。因此我一直在纠结先写理论(分析),还是先写实践(移植说明)。结论是先写本文,理由是:虽然earlycon功能涉及到TTY子系统的复杂知识,如console driver、tty driver、serial driver等等,但从移植的角度看,我们可以什么都不懂。这种优雅的抽象和封装,是优秀软件(如Linux kernel)所必需具备的特性,也是我们软件人需要竭力追求的神圣目标。

2. 移植步骤

2.1 串口驱动

early console是linux serial subsystem的一个子功能,因此需要依附于具体平台的serial driver之下(放心,本文不涉及任何串口驱动的知识)。以“X Project” 所使用的bubblegum-96平台为例,我们需要在“drivers/tty/serial/”下新建一个serial driver,并修改kernel serial subsystem的Kconfig和Makefile文件,将其添加到kernel的编译系统中, 步骤如下:

1)新建serial driver

touch drivers/tty/serial/owl-serial.c

其中“owl”是bubblegum-96平台所使用SOC的代号,大家可以根据实际情况,使用和自己平台匹配的名称。

2)修改drivers/tty/serial/Kconfig和drivers/tty/serial/Makefile,将新建的serial driver加入到编译框架中

diff --git a/drivers/tty/serial/Kconfig b/drivers/tty/serial/Kconfig
index 13d4ed6..bbf4b69 100644
--- a/drivers/tty/serial/Kconfig
+++ b/drivers/tty/serial/Kconfig
@@ -1624,6 +1624,14 @@ config SERIAL_MVEBU_CONSOLE
          and warnings and which allows logins in single user mode)
          Otherwise, say 'N'.

+config SERIAL_OWL
+       tristate "Actions OWL serial port support"
+       depends on ARCH_OWL
+       select SERIAL_CORE
+       select SERIAL_CORE_CONSOLE
+       help
+         If you have a machine based on an Actions OWL CPU you
+         can enable its onboard serial ports by enabling this option.

endmenu

config SERIAL_MCTRL_GPIO
diff --git a/drivers/tty/serial/Makefile b/drivers/tty/serial/Makefile
index 8c261ad..3f75d73 100644
--- a/drivers/tty/serial/Makefile
+++ b/drivers/tty/serial/Makefile
@@ -91,6 +91,7 @@ obj-$(CONFIG_SERIAL_MEN_Z135) += men_z135_uart.o
obj-$(CONFIG_SERIAL_SPRD) += sprd_serial.o
obj-$(CONFIG_SERIAL_STM32)     += stm32-usart.o
obj-$(CONFIG_SERIAL_MVEBU_UART)        += mvebu-uart.o
+obj-$(CONFIG_SERIAL_OWL)       += owl-serial.o

 

我们为新建的serial driver指定了“SERIAL_OWL”配置项,该配置项依赖ARCH_OWL[2]。与此同时,我们需要选中“SERIAL_CORE”和“SERIAL_CORE_CONSOLE”两个配置项,以支持earlycon功能。

3)配置kernel,开启TTY、serial等功能,并使能我们新加入的serial driver

cd ~/work/xprj/build
make kernel-config

#选中如下的配置项
   Device Drivers  --->
      Character devices  --->
           [*] Enable TTY
           Serial drivers  --->
               [*] Actions OWL serial port support

完成后make kernel重新编译即可。

2.2 early console的移植

实现early console的移植过程非常简单,包括:

1)实现一个early console的setup接口,并调用serial core提供的注册接口(EARLYCON_DECLARE)将其注册到kernel中,如下:

int __init earlycon_owl_setup(struct earlycon_device *device, const char *opt)
{
        /* TODO */
        uart5_base = early_ioremap(UART5_BASE, 4);

        device->con->write = earlycon_owl_write;
        return 0;
}
EARLYCON_DECLARE(owl_serial, earlycon_owl_setup);

其中“owl_serial”是early console的名称,earlycon_owl_setup是开始使用之前的初始化接口,需要在这个初始化接口中做两件事情:

进行一些必要的初始化,如例子中的寄存器map;

为printk输出制定一个write接口----earlycon_owl_write。

注2:“EARLYCON_DECLARE”是实现early console的一种方法,另外我们也可以使用device tree的方式,为了不增加复杂度,本文就不提及相关的实现了。

注3:为了简单,early console和u-boot[3]使用相同的串口,并且使用相同的配置,因此不需要额外的初始化操作。

注4:虽然配置不变,我们还是需要访问串口寄存器输出字符串,因此需要map相应的IO地址,由于early console需要在很早的时候使用,此时mm还没有初始化,因此我们可以用early_ioremap[4]进行map。

2)earlycon_owl_write的实现

console输出需要字符串处理,可以借用serial core的标准接口----uart_console_write:

static void earlycon_owl_write(struct console *con, const char *s, unsigned n)
{
        /* TODO */
        uart_console_write(NULL, s, n, owl_serial_putc);
}

我们需要做的就是提供一个字符输出的API----owl_serial_putc,具体可参考代码以及“X-004-UBOOT-串口驱动移植(Bubblegum-96平台)[3]”中有关的描述。

3)移植完成后重新编译kernel即可

3. 使用说明

移植完成后,可以通过kernel的命令行参数,告诉kernel在启动的时候使用该early console,参数的格式如下:

earlycon=owl_serial

其中“earlycon”是关键字,“owl_serial”是early console的名字。

命令行参数可以通过u-boot传入,为了测试方便,可以暂时加到kernel的配置项中[5],如下:

#
# Boot options
#
-CONFIG_CMDLINE=""
+CONFIG_CMDLINE="earlycon=owl_serial"

以上改动具体可参考下面的patch:

https://github.com/wowotechX/linux/commit/1285ab5e6e5c5bb263a1d715fe04cca2d217bd98

4. 测试和调试

移植完成后,按照“README.bubblegum96[6]”中的步骤,编译并运行kernel,发现printk信息没有如约出现,没关系,兵来将挡,水来土掩,开始调试了。

4.1 使用点LED的方式,定位问题所在位置

该方法在u-boot移植的初期,也是用过,应该是轻车熟路了[7]。不过在kernel中有点稍微的不一样,我们需要将GPIO有关的寄存器map成虚拟地址才能使用,代码如下(只为暂时调试使用,因而没有上传到代码仓库):

1)在init/main.c中添加LED debug有关的函数

+void __init xprj_earlyyyy_debug(void)
+{
+       static void __iomem *gpioa_outen = NULL;
+       static void __iomem *gpioa_outdat = NULL;
+
+       if (gpioa_outen == NULL) {
+               gpioa_outen = early_ioremap(0xe01b0000, 4);
+               gpioa_outdat = early_ioremap(0xe01b0008, 4);
+       }
+
+       writel(0xffffffff, gpioa_outen);
+       writel(0xffffffff, gpioa_outdat);
+}

注5:为了可以尽早(在kernel内存管理模块初始化之前)使用,我们使用early_ioremap获取寄存器的虚拟地址[4]

注6:为了简单,这里粗暴的把GPIOA的所有GPIO都输出高电平了。

2)根据earlycon的初始化逻辑,在earlycon初始化的路径上,调用debug API,点亮LED。第一个地方是setup_earlycon(earlycon的具体流程,会在下一篇文章中分析):

+extern void __init xprj_earlyyyy_debug(void);
int __init setup_earlycon(char *buf)
{
        const struct earlycon_id *match;
+#if 1
+       xprj_earlyyyy_debug();
+       while (1);
+#endif

+

无法点亮!

接着往前跟踪,放到param_setup_earlycon中:

@@ -204,6 +210,11 @@ static int __init param_setup_earlycon(char *buf)

{
        int err;
+#if 1
+       xprj_earlyyyy_debug();
+       while (1);
+#endif

+

还是无法点亮!

怀疑earlycon的代码压根没有执行,检查drivers/tty/serial/Makefile,发现由CONFIG_SERIAL_EARLYCON控制:

obj-$(CONFIG_SERIAL_EARLYCON) += earlycon.o

仿照drivers/tty/serial/Kconfig中其它serial driver,在OWL_SERIAL中select该配置项,如下:

@@ -1629,6 +1629,7 @@ config SERIAL_OWL
        depends on ARCH_OWL
        select SERIAL_CORE
        select SERIAL_CORE_CONSOLE
+       select SERIAL_EARLYCON
        help
          If you have a machine based on an Actions OWL CPU you
          can enable its onboard serial ports by enabling this option.

配置kernel后,再次编译测试,得到如下的执行过程:

param_setup_earlycon          OK
setup_earlycon                     OK
earlycon_owl_setup              OK
earlycon_owl_write               FAIL

看来初始化OK了,没有人调用console的write接口?

注7:上面debug的过程中,下面的代码会导致编译错误:

extern void __init xprj_earlyyyy_debug(void);
static void earlycon_owl_write(struct console *con, const char *s, unsigned n)
{
#if 1
        xprj_earlyyyy_debug();
        while (1);
#endif

错误信息为:

WARNING: modpost: Found 2 section mismatch(es).
To see full details build your kernel with:
make CONFIG_DEBUG_SECTION_MISMATCH=y'
FATAL: modpost: Section mismatches detected.
Set CONFIG_SECTION_MISMATCH_WARN_ONLY=y to allow them.
make[3]: *** [vmlinux.o] Error 1
make[2]: *** [vmlinux] Error 2

原因是我们在非__init的section中(earlycon_owl_write)调用了__init section(xprj_earlyyyy_debug)的代码,按照提示说的,打开CONFIG_SECTION_MISMATCH_WARN_ONLY配置项即可编译通过:

    Kernel hacking  --->
           Compile-time checks and compiler options  ---> 
                 [*] Make section mismatch errors non-fatal

4.2 使能printk的配置项

依稀记得linux kernel的printk有配置项可以控制使能与否,检查kernel的相关代码,果然如此。重新配置kernel使能该配置项:

    General setup  --->
          [*] Enable support for printk

再次运行,串口有输出了:

Starting kernel ...
flushing dcache successfully.
Booting Linux on physical CPU 0xrFailed to find device node for bpDBPDentry cacInode-cache hash table entries: software IO TLB [mem 0x79cb4000-Memory: 1993624K/2097152K availa0ffSsaK  

觉得奇奇怪怪的?没有换行?没有格式化输出?

4.3 检查earlycon驱动

再仔细检查一下drivers/tty/serial/owl-serial.c中owl_serial_putc的实现,有个地方写错了(从u-boot抄过来的时候大意了),修改一下:

static void owl_serial_putc(struct uart_port *port, int ch)
{
-       if (readl(uart5_base + UART_STAT) & UART_STAT_TFFU)
-               return;
+       /* wait for TX FIFO untill it is not full */
+       while (readl(uart5_base + UART_STAT) & UART_STAT_TFFU)
+               ;
        writel(ch, uart5_base + UART_TXDAT);
}

终于OKAY了:

Booting Linux on physical CPU 0x0
Linux version 4.6.0-rc5+ (pengo@ubuntu) (gcc version 4.8.3 20131202 (prerelease) (crosstool-NG linaro-1.13.1-4.8-2013.12 - Linaro GCC 2013.11) ) #8 SMP Mon Oct 3 23:36:39 PDT 2016
Boot CPU: AArch64 Processor [410fd032]
earlycon: owl_serial0 at I/O port 0x0 (options '')
bootconsole [owl_serial0] enabled
Failed to find device node for boot cpu
missing boot CPU MPIDR, not enabling secondaries
percpu: Embedded 14 pages/cpu @ffffffc07ffdd000 s28032 r0 d29312 u57344
Detected VIPT I-cache on CPU0
Built 1 zonelists in Zone order, mobility grouping on.  Total pages: 516096
Kernel command line: earlycon=owl_serial
PID hash table entries: 4096 (order: 3, 32768 bytes)
Dentry cache hash table entries: 262144 (order: 9, 2097152 bytes)
Inode-cache hash table entries: 131072 (order: 8, 1048576 bytes)
software IO TLB [mem 0x79cb4000-0x7dcb4000] (64MB) mapped at [ffffffc079cb4000-ffffffc07dcb3fff]
Memory: 1993624K/2097152K available (1028K kernel code, 78K rwdata, 120K rodata, 116K init,
201K bss, 103528K reserved, 0K cma-reserved)
Virtual kernel memory layout:
    modules : 0xffffff8000000000 - 0xffffff8008000000   (   128 MB)
    vmalloc : 0xffffff8008000000 - 0xffffffbdbfff0000   (   246 GB)
      .text : 0xffffff8008080000 - 0xffffff8008181000   (  1028 KB)
    .rodata : 0xffffff8008181000 - 0xffffff80081a0000   (   124 KB)
      .init : 0xffffff80081a0000 - 0xffffff80081bd000   (   116 KB)
      .data : 0xffffff80081bd000 - 0xffffff80081d0800   (    78 KB)
    fixed   : 0xffffffbffe7fd000 - 0xffffffbffec00000   (  4108 KB)
    PCI I/O : 0xffffffbffee00000 - 0xffffffbfffe00000   (    16 MB)
    memory  : 0xffffffc000000000 - 0xffffffc080000000   (  2048 MB)
SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
Hierarchical RCU implementation.
        Build-time adjustment of leaf fanout to 64.
        RCU restricting CPUs from NR_CPUS=64 to nr_cpu_ids=1.
RCU: Adjusting geometry for rcu_fanout_leaf=64, nr_cpu_ids=1
NR_IRQS:64 nr_irqs:64 0
Kernel panic - not syncing: No interrupt controller found.
---[ end Kernel panic - not syncing: No interrupt controller found.

终于从蛮荒地世界中走出来了,庆祝吧~~!!

注8:上述过程可参考如下patch:

https://github.com/wowotechX/linux/commit/1e18b4d2a5df61e5f495dbc071fe2ad2740d7061

5. 参考文档

[1] Linux TTY framework(2)_软件架构

[2] X-009-KERNEL-Linux kernel的移植(Bubblegum-96平台)

[3] X-004-UBOOT-串口驱动移植(Bubblegum-96平台)

[4] Fix-Mapped Addresses

[5] Linux kernel内核配置解析(5)_Boot options(基于ARM64架构)

[6] README.bubblegum96,https://github.com/wowotechX/doc/blob/master/README.bubblegum96

[7] 通过点亮LED的方法调试嵌入式代码

 

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net

标签: Linux Kernel console earlycon printk

评论:

foxtang
2017-12-10 12:05
老师,arm926t的芯片,local_flush_tlb_all()后就没打印了,也没重启,是不是earlycon已经不能用了?
wowo
2017-12-11 09:47
@foxtang:可以试试点灯看看跑到哪里了。靠猜不是一个好习惯:-)
foxtang
2017-12-11 11:01
@wowo:暂时注释掉可以跑下去,等串口驱动做好了再打开看看。
foxtang
2017-12-08 16:40
开mmu之前可以点灯,开了之后就点不了了,有什么办法在开了mmu后点灯?
wowo
2017-12-08 22:37
@foxtang:可以参考这篇文章:http://www.wowotech.net/memory_management/fixmap.html
关于这个话题,之前碰巧讨论过:-)
foxtang
2017-12-08 16:31
serial early console 能在32位的arm上用吗?比如2440。
wowo
2017-12-08 22:36
@foxtang:应该可以的,这个不区分平台。
温柔海洋
2016-10-09 14:17
还有个问题请教下:

注3:为了简单,early console和u-boot[3]使用相同的串口,并且使用相同的配置,因此不需要额外的初始化操作。

这句话意思是不是说只要uboot阶段能成功驱动串口打印出信息,Linux内核初始化早期阶段如果想做early_serial_console功能的话,就不用再初始化配置串口了,直接操作串口发送数据就行了。因为这个时候,串口波特率设置,控制寄存器设置都是沿用uboot阶段的配置。换句话说,就是uboot阶段对SOC各个外设控制器(SPI,I2C,UART,lcd,sdio...还有它们的clock tree设置),在Linux kernel初始化阶段不会自动清零,除非手动再次执行外设相关驱动(比如在do_basic_setup里面执行各个外设驱动init函数),再次去配置寄存器。
wowo
2016-10-09 15:56
@温柔海洋:是的.
温柔海洋
2016-10-09 16:00
@wowo:谢谢指点,非常感谢你的技术文章,你的文章如果将来出书了,肯定大卖。
wowo
2016-10-09 19:45
@温柔海洋:多谢鼓励,现在还只是写着玩玩,出书的事情还没考虑呢:-)
ooonebook
2016-10-09 10:21
hi,wowo
移植步骤写得很详细,学习到了。:D有个问题请教下
----注2:“EARLYCON_DECLARE”是实现early console的一种方法,另外我们也可以使用device tree的方式,为了不增加复杂度,本文就不提及相关的实现了。----
这个有点困惑,我觉得EARLYCON_DECLARE应该是必须的,我的理解是这个宏是把earlycon_id放在一个init段里面,这个device tree个人感觉应该做不到。我觉得是earlycon的串口指定可以用earlycon=owl_serial来指定也可以用device tree里面的“linux,stdout-path”的属性来指定。
wowo
2016-10-09 13:38
@ooonebook:抱歉,我这里的表述有一些歧义,其实我想说的另一种方法,是OF_EARLYCON_DECLARE,例如:
static int __init s5pv210_early_console_setup(struct earlycon_device *device,
                                              const char *opt)
{
        device->port.private_data = &s5pv210_early_console_data;
        return samsung_early_console_setup(device, opt);
}
OF_EARLYCON_DECLARE(s5pv210, "samsung,s5pv210-uart",
                        s5pv210_early_console_setup);
到最后都会将earlycon信息注册到__earlycon_table。
                    
其实使用of_的方式,隐含了的思路是:串口驱动是现成的,可以摇身一变把它变成earlycon。当然,这种方法是标准做法,最后一定要变成这样。
而本文所展示的,是最简单的做法:在串口驱动还没有的情况下,怎么用最简单的方法实现earlycon。
ooonebook
2016-10-09 15:33
@wowo:OK,理解你的意思了,谢谢wowo
wowo
2016-10-09 19:44
@ooonebook:不用客气哈~~
温柔海洋
2016-10-08 17:51
再问个问题,我这边飞思卡尔imx6内核中又找到一个early_console的实现,没有通过CONFIG_DEBUG_LL来完成串口的寄存器级别收发,而是通过ioremap,enable_clk等内核API来完成的。但是这个early console 的初始化,竟然放在了start_kernel里面比较靠后的time_init 里面。这样的话,这个脱离汇编代码的early console实现,也不是个真正的早期串口console。

你的early_ioremap 比较不错,有了它,应该在start_kernel里面很早位置处(比如在page_init之前,是否可以?),就可以操作控制外设了。能大概讲下它和fixmap结合起来时,到底具体是怎么实现的啊。

page_init里面会调用具体机器的map_io,建立device 的静态映射。之后也可以通过IO_ADDRESS直接访问外设寄存器,我好奇的是你的early_ioremap如果依赖fixmap的话,并且也能在setup_arch-> page_init这些函数之前被成功被调用发挥作用的话,究竟实现思路是什么。
wowo
2016-10-08 18:12
@温柔海洋:是的,以前arm早期的打印,是在一个叫做debug.S的汇编代码里面弄的,后来printk.c也加过一个early console,不过是比较晚才能用。
对arm64平台来说,利用early_ioremap+earlycon,在setup_arch时候就能用了。
至于early_ioremap的原理,请参考这篇文章:
http://www.wowotech.net/memory_management/fixmap.html
温柔海洋
2016-10-09 09:14
@wowo:你的early_ioremap是得到setup_arch阶段才可以用,还有一种方法是setup_arch->pageinit->device_iomap->具体machine的map_io调用时,建立一些针对soc所有外设的静态映射表。一旦建立起来后,用IO_ADDRESS宏也可以方便地对外设寄存器进行操作(拉GPIO电平,配置串口clock,寄存器)。
这种提前建立好静态映射的方法,和你的文章中的early_ioremap起的作用效果是否相同?都是映射建立好后,就可以随心所欲操作外设了?

还有这两种方法都是要在setup_arch阶段最后建立页表时才能发挥作用,如果要在更靠前面的地方打印信息的话,是否就不能不借助CONFIG_DEBUG_LL对应的那个汇编代码串口收发指令来工作了啊?
wowo
2016-10-09 09:50
@温柔海洋:关于early的ioremap,再整理一下思路,我的理解是这样的:
1. early_ioremap是一个比较新的功能,我检查了手上的两份kernel:
一份是3.10,只有arm64平台提供了该功能(arch/arm64/include/asm/fixmap.h);
另一份是x project所使用的kernel(版本是基于4.6.0-rc5的一个分支),arm和arm64都支持了(arch/arm/include/asm/fixmap.h)。

2. early_ioremap什么时候可以使用
(这里的描述基于4.6.0-rc5。)

early_ioremap在start_kernel-->setup_arch-->early_fixmap_init/early_ioremap_init之后,就可以使用了。
至于你提到的start_kernel-->setup_arch-->paging_init-->devicemaps_init,首先,它没有early_ioremap早,其次,好像只有arm平台才有(也就是说将要被抛弃了)。具体能否使用,我也没有研究过。

3. printk可用的时机
其实在"start_kernel-->setup_arch-->parse_early_param之后就可以使用了。
再往前,我觉得,没有打印的必要,因为都是arch无关的标准代码,不会出问题。之所以需要打印,是因为我们执行了arch相关的代码,有出问题的可能。
当然,如果一定要打印,只能用一些非标的东西,例如CONFIG_DEBUG_LL。
misaka
2016-12-13 14:02
@wowo:其实现在earlyprintk就是注册个early console 底层的操作函数是调用low level实现的printascii之类的函数  还是有联系的。
温柔海洋
2016-10-08 17:32
问下,我的kernel版本是3.0.35,里面搜early_ioremap,搜到的都是ia64和x86平台的,arm平台没有实现这个函数。
还有我的这个版本内核early_prink的实现,最终通过CONFIG_DEBUG_LL对应的debug.S 里面的几个汇编函数来实现串口寄存器级别的发送字符操作过程,你文章中的early printk的实现应该和CONFIG_DEBUG_LL没有任何关系。

是不是高版本的Linux内核 早期串口输出字符已经完全脱离了CONFIG_DEBUG_LL?而且高版本的也有arm平台的early_ioremap的实现,可以让早期串口输出可以放在start_kernel里面的任何位置?
wowo
2016-10-08 18:09
@温柔海洋:earlycon是一个比较新的东西,具体是哪个版本加进来的,我没有看.
至于early_ioremap,因该很早之前就有了.
温柔海洋
2016-10-08 14:54
非常好的文章,赞一个

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog