个人技术分享

由于硬件设计错误,需要把串口从uart0更改到uart2。本实验基于友善之臂的nanopi进行串口移植

移植前需要了解uboot以及uboot-spl的启动流程,这里参考:
tiny210(s5pv210)上电启动流程(BL0-BL2)
uboot流程——uboot-spl代码流程
uboot流程——uboot启动流程

SPL和Uboot

在更改串口之前,我们需要先了解一下spl和uboot分别做了什么:

  • SPL:由IROM加载到SRAM,主要负责初始化DDR并加载+跳转uboot到DDR
  • Uboot:由SPL加载到DDR,主要负责板级外设的初始化并加载+跳转Linux到DDR

SPL和Uboot在arch级的初始化步骤是一样的,差异集中在板级的初始化(串口的初始化就在板级)

串口移植

在更改串口前,我们先想一想使用一个串口需要做的准备:

  • 串口的GPIO初始化
  • 串口初始化

只要保证了这两步,一个串口的配置就算完成了。

所以我们的工作就可以分为SPL的串口配置和Uboot的串口配置,我们将从源码的角度来看看更改串口需要做哪些事情。

SPL串口移植

uart_init

SPL的串口初始化函数在board_init_f(arch/arm/mach-sunxi/board.c):

void board_init_f(ulong dummy)
{
	spl_init();
	preloader_console_init();	// 串口初始化

#ifdef CONFIG_SPL_I2C_SUPPORT
	/* Needed early by sunxi_board_init if PMU is enabled */
	i2c_init(CONFIG_SYS_I2C_SPEED, CONFIG_SYS_I2C_SLAVE);
#endif
	sunxi_board_init();

#if (CONFIG_CONS_INDEX > 1) && defined(CONFIG_MACH_SUN8I_H3)
	/* the sunxi kernel needs uart0 to be initialized by the bootloader */

	/* configure uart0 GPIOs */
	sunxi_gpio_set_cfgpin(SUNXI_GPA(4), SUN8I_H3_GPA_UART0);
	sunxi_gpio_set_cfgpin(SUNXI_GPA(5), SUN8I_H3_GPA_UART0);
	sunxi_gpio_set_pull(SUNXI_GPA(5), SUNXI_GPIO_PULL_UP);

	/* initialize uart0 */
	NS16550_init((NS16550_t)(SUNXI_UART0_BASE), CONFIG_SYS_NS16550_CLK / 16 / CONFIG_BAUDRATE);
#endif
}

board_init_f进行了spl所需要的外设初始化,而我们的串口初始化就在preloader_console_init():

void preloader_console_init(void)
{
	gd->baudrate = CONFIG_BAUDRATE;

	serial_init();		/* serial communications setup */

	gd->have_console = 1;

#if CONFIG_IS_ENABLED(BANNER_PRINT)
	puts("\nU-Boot " SPL_TPL_NAME " " PLAIN_VERSION " (" U_BOOT_DATE " - "
	     U_BOOT_TIME " " U_BOOT_TZ ")\n");
#endif
#ifdef CONFIG_SPL_DISPLAY_PRINT
	spl_display_print();
#endif
}

它会调用serial_inti(drivers/serial/serial.c)进行串口的初始化:

int serial_init(void)
{
	gd->flags |= GD_FLG_SERIAL_READY;
	return get_current()->start();
}

get_current()会调用default_serial_console(),最终返回一个串口设备,进入default_serial_console():

__weak struct serial_device *default_serial_console(void)
{
#if CONFIG_CONS_INDEX == 1
	return &eserial1_device;
#elif CONFIG_CONS_INDEX == 2
	return &eserial2_device;
#elif CONFIG_CONS_INDEX == 3
	return &eserial3_device;
#elif CONFIG_CONS_INDEX == 4
	return &eserial4_device;
#elif CONFIG_CONS_INDEX == 5
	return &eserial5_device;
#elif CONFIG_CONS_INDEX == 6
	return &eserial6_device;
#else
#error "Bad CONFIG_CONS_INDEX."
#endif
}

可以看到我们选择串口的宏出现了,CONFIG_CONS_INDEX负责串口的选择。值得注意的是这里的索引是从1开始的,我们的uart2对应这里的serial3,所以我们需要在config中添加:

CONFIG_CONS_INDEX = 3

gpio_init

串口的gpio初始化在s_init()里调用(lowlevel_init->s_init):

void s_init(void)
{
	```
	clock_init();
	timer_init();
	gpio_init();	// gpio初始化
#ifndef CONFIG_DM_I2C
	i2c_init_board();
#endif
	eth_init_board();
}

uart的gpio初始化:

static int gpio_init(void)
{
#if defined(CONFIG_MACH_SUNXI_H3_H5)
	/* enable R_PIO GPIO access */
	prcm_apb0_enable(PRCM_APB0_GATE_PIO);
#endif

	__maybe_unused uint val;
#if CONFIG_CONS_INDEX == 1 && defined(CONFIG_UART0_PORT_F)
#if defined(CONFIG_MACH_SUN4I) || \
    defined(CONFIG_MACH_SUN7I) || \
    defined(CONFIG_MACH_SUN8I_R40)
	/* disable GPB22,23 as uart0 tx,rx to avoid conflict */
	sunxi_gpio_set_cfgpin(SUNXI_GPB(22), SUNXI_GPIO_INPUT);
	sunxi_gpio_set_cfgpin(SUNXI_GPB(23), SUNXI_GPIO_INPUT);
#endif
···
#elif CONFIG_CONS_INDEX == 1 && defined(CONFIG_MACH_SUNXI_H3_H5)	// 只针对uart0做了初始化
	sunxi_gpio_set_cfgpin(SUNXI_GPA(4), SUN8I_H3_GPA_UART0);
	sunxi_gpio_set_cfgpin(SUNXI_GPA(5), SUN8I_H3_GPA_UART0);
	sunxi_gpio_set_pull(SUNXI_GPA(5), SUNXI_GPIO_PULL_UP);
#elif CONFIG_CONS_INDEX == 1 && defined(CONFIG_MACH_SUN50I)
	sunxi_gpio_set_cfgpin(SUNXI_GPB(8), SUN50I_GPB_UART0);
	sunxi_gpio_set_cfgpin(SUNXI_GPB(9), SUN50I_GPB_UART0);
	sunxi_gpio_set_pull(SUNXI_GPB(9), SUNXI_GPIO_PULL_UP);
···

	return 0;
}

在这里我们只进行了uart0的gpio初始化,所以我们添加:

#elif CONFIG_CONS_INDEX == 3 && defined(CONFIG_MACH_SUNXI_H3_H5)
	sunxi_gpio_set_cfgpin(SUNXI_GPA(0), SUN8I_H3_GPA_UART2);
	sunxi_gpio_set_cfgpin(SUNXI_GPA(1), SUN8I_H3_GPA_UART2);
	sunxi_gpio_set_pull(SUNXI_GPA(0), SUNXI_GPIO_PULL_UP);

到这里我们的SPL的串口移植已经完成,编译uboot可以看到:
在这里插入图片描述

Uboot串口移植

uart_init

Uboot的串口初始化函数在board_init_f(commom/board_f.c):

static const init_fnc_t init_sequence_f[] = {
	```
#if defined(CONFIG_BOARD_EARLY_INIT_F)
	board_early_init_f,
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_SYS_FSL_CLK) || defined(CONFIG_M68K)
	/* get CPU and bus clocks according to the environment variable */
	get_clocks, /* get CPU and bus clocks (etc.) */
#endif
#if !defined(CONFIG_M68K)
	timer_init, /* initialize timer */
#endif
#if defined(CONFIG_BOARD_POSTCLK_INIT)
	board_postclk_init,
#endif
	env_init,		   /* initialize environment */
	init_baud_rate,	   /* initialze baudrate settings */
	serial_init,	   /* serial communications setup */
	console_init_f,	   /* stage 1 init of console */
	display_options,   /* say that we are here */
	display_text_info, /* show debugging info if required */
	checkcpu,
#if defined(CONFIG_SYSRESET)
	print_resetinfo,
	```
#endif
}

Uboot的串口初始化也是serial_init()函数,但它与SPL的不同(下文会解释),它定义在drivers/serial/serial_uclass.c中:

int serial_init(void)
{
#if CONFIG_IS_ENABLED(SERIAL_PRESENT)
	serial_find_console_or_panic();
	gd->flags |= GD_FLG_SERIAL_READY;
#endif

	return 0;
}

serial_find_console_or_panic:

static void serial_find_console_or_panic(void)
{
	const void *blob = gd->fdt_blob;
	struct udevice *dev;
#ifdef CONFIG_SERIAL_SEARCH_ALL
	int ret;
#endif

	if (CONFIG_IS_ENABLED(OF_PLATDATA)) {
		uclass_first_device(UCLASS_SERIAL, &dev);
		if (dev) {
			gd->cur_serial_dev = dev;
			return;
		}
	} else if (CONFIG_IS_ENABLED(OF_CONTROL) && blob) {
		/* Live tree has support for stdout */
		if (of_live_active()) {
			struct device_node *np = of_get_stdout();

			if (np && !uclass_get_device_by_ofnode(UCLASS_SERIAL,
					np_to_ofnode(np), &dev)) {
				gd->cur_serial_dev = dev;
				return;
			}
		} else {
			if (!serial_check_stdout(blob, &dev)) {	// 最终调用的函数
				gd->cur_serial_dev = dev;
				return;
			}
		}
	}
···
}
#endif /* CONFIG_SERIAL_PRESENT */

serial_find_console_or_panic最终会调用serial_check_stdout函数,确认我们需要输出的终端:

#if CONFIG_IS_ENABLED(SERIAL_PRESENT)
static int serial_check_stdout(const void *blob, struct udevice **devp)
{
	int node = -1;
	const char *str, *p, *name;
	int namelen;

	/* Check for a chosen console */
	str = fdtdec_get_chosen_prop(blob, "stdout-path");	// 检查chosen节点
	if (str) {
		p = strchr(str, ':');
		namelen = p ? p - str : strlen(str);
		node = fdt_path_offset_namelen(blob, str, namelen);

		if (node < 0) {
			/*
			 * Deal with things like
			 *	stdout-path = "serial0:115200n8";
			 *
			 * We need to look up the alias and then follow it to
			 * the correct node.
			 */
			name = fdt_get_alias_namelen(blob, str, namelen);
			if (name)
				node = fdt_path_offset(blob, name);
		}
	}

	if (node < 0)
		node = fdt_path_offset(blob, "console");
	if (!uclass_get_device_by_of_offset(UCLASS_SERIAL, node, devp))
		return 0;

	/*
	 * If the console is not marked to be bound before relocation, bind it
	 * anyway.
	 */
	if (node > 0 && !lists_bind_fdt(gd->dm_root, offset_to_ofnode(node),
					devp, false)) {
		if (!device_probe(*devp))
			return 0;
	}

	return -ENODEV;
}

关注代码fdtdec_get_chosen_prop(blob, “stdout-path”),它会去找到目录树下的chosen节点,并解析stdout-path属性,stdout-path属性会指定输出的控制台信息,当前chosen节点(arch/arm/dts/sun8i-h3-nanopi.dtsi):

chosen {
                stdout-path = "serial0:115200n8";
        };

改为:

chosen {
                stdout-path = "serial2:115200n8";
        };

到这里只是设置标准输出为串口2,我们还需要进行串口的初始化和相应的gpio的初始化,gpio的初始化在SPL中已经完成了,而uart的初始化会由serial的DM模型完成,所以我们只需在设备树中使能uart即可。

将串口2的状态从失能改为使能,达到下图效果

serial@1c28800 {
            compatible = "snps,dw-apb-uart";
            reg = <0x01c28000 0x00000400>;
            interrupts = <0x00000000 0x00000000 0x00000004>;
            reg-shift = <0x00000002>;
            reg-io-width = <0x00000004>;
            clocks = <0x00000003 0x0000003e>;
            resets = <0x00000003 0x00000031>;
            dmas = <0x00000013 0x00000006 0x00000013 0x00000006>;
            dma-names = "rx", "tx";
            status = "okay";	// 从disable改为okay
        };

可以在dts文件里写入:

&uart2 {
        status = "okay";
};

至此,串口2成功打印:

在这里插入图片描述

(后续实验表明,即使串口2不使能也可以完成移植)

Uboot的DM模型

关于DM的详细讲解可参考文章:

U-Boot驱动模型DM
uboot 驱动模型

这里只是简单解释一下为什么SPL和Uboot在串口初始化链接的是不同的serial文件。

我们打开serial的Makefile(drivers/serial):

ifdef CONFIG_SPL_BUILD

ifeq ($(CONFIG_$(SPL_TPL_)BUILD)$(CONFIG_$(SPL_TPL_)DM_SERIAL),yy)
obj-y += serial-uclass.o
else
obj-y += serial.o
endif

else

ifdef CONFIG_DM_SERIAL
obj-y += serial-uclass.o
else
obj-y += serial.o
endif

可以看到之所以会链接不同的serial文件,是因为CONFIG_SPL_BUILD宏决定。

SPL所使用的serial_init(serial.c)函数相当于裸机的串口初始化,不会涉及设备树,所以我们的串口初始化以及对应gpio初始化的配置都是固化在代码里的。

而Uboot的serial_init(serial_uclass.c)函数是基于DM驱动模型的,整个serial都由DM管理。在DM初始化的时候它就会去设备树下寻找匹配的设备节点,并且完成相应的probe(初始化串口)。所以与其说serial_init是初始化串口,不如说它只是去获得我们的串口设备。

if (!serial_check_stdout(blob, &dev)) {
				gd->cur_serial_dev = dev;
				return;
			}