由于硬件设计错误,需要把串口从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的详细讲解可参考文章:
这里只是简单解释一下为什么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;
}