基于IMX6Q的uboot启动流程分析(1):uboot入口函数
基于IMX6Q的uboot启动流程分析(2):_main函数之board_init_f
基于IMX6Q的uboot启动流程分析(3):_main函数之relocate_code与board_init_r
基于IMX6Q的uboot启动流程分析(4):uboot中的串口设备
第3章:_main函数之relocate_code与board_init_r
3.1 relocate_code函数
上一小节,介绍了board_init_f
函数,主要的是对整个DRAM的内存进行了分配,以便uboot的重定位。
3.1.1 设置中间环境
在调用relocate_code
函数前,需设置中间环境,更新分配DRAM后的sp指针值和gd指针值,其内容如下
ldr r0, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */ //新的sp
bic r0, r0, #7 /* 8-byte alignment for ABI compliance */
mov sp, r0
ldr r9, [r9, #GD_NEW_GD] /* r9 <- gd->new_gd */
adr lr, here
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
add lr, lr, r0
#if defined(CONFIG_CPU_V7M)
orr lr, #1 /* As required by Thumb-only */
#endif
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code
将新的sp地址赋值给r0 = 0x4DF68F50
3.1.2 relocate_code函数详解
uboot重定位主要由relocate_code
函数实现,其定义在arch/arm/lib/relocate.S
文件中,内容为:
80 ENTRY(relocate_code)
81 ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
82 subs r4, r0, r1 /* r4 <- relocation offset */
83 beq relocate_done /* skip relocation */
84 ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */
85
86 copy_loop:
87 ldmia r1!, {r10-r11} /* copy from source address [r1] */
88 stmia r0!, {r10-r11} /* copy to target address [r0] */
89 cmp r1, r2 /* until source end address [r2] */
90 blo copy_loop
91
92 /*
93 * fix .rel.dyn relocations
94 */
95 ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
96 ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
97 fixloop:
98 ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
99 and r1, r1, #0xff
100 cmp r1, #R_ARM_RELATIVE
101 bne fixnext
102
103 /* relative fix: increase location by offset */
104 add r0, r0, r4
105 ldr r1, [r0]
106 add r1, r1, r4
107 str r1, [r0]
108 fixnext:
109 cmp r2, r3
110 blo fixloop
111
112 relocate_done:
113
114 #ifdef __XSCALE__
115 /*
116 * On xscale, icache must be invalidated and write buffers drained,
117 * even with cache disabled - 4.2.7 of xscale core developer's manual
118 */
119 mcr p15, 0, r0, c7, c7, 0 /* invalidate icache */
120 mcr p15, 0, r0, c7, c10, 4 /* drain write buffer */
121 #endif
122
123 /* ARMv4- don't know bx lr but the assembler fails to see that */
124
125 #ifdef __ARM_ARCH_4__
126 mov pc, lr
127 #else
128 bx lr
129 #endif
130
131 ENDPROC(relocate_code)
-
第81行,将__image_copy_start的值赋值给r1寄存器。由
uboot.map
文件得知__image_copy_start = 0x17800000
-
第82行,r4寄存器保存重定位前后位置的偏移量。r4 = r0 - r1 = 0x4DF68F50 - 0x17800000 = 0x36768F50。
-
第83行,跳转
relocate_done
函数,判断uboot重定位的目的地址是否和uboot源地址是否相等,如果相等的话,表示不需要重定位,跳到relocate_done处,继续运行,如果不相等的话,则是需要进行重定位。 -
第84行,r2寄存器保存uboot复制结束的地址
__image_copy_end
。由uboot.map文件得知,__image_copy_end = 0x1787ba9c。 -
第86-90行,
copy_loop
区域,执行拷贝,将uboot从低地址重定位到高地址。重定位过程描述为:- 第一:从r1,也就是__image_copy_start开始,读取uboot代码到r10和r11寄存器,一次就拷贝两个32位的数据,r1自动更新,保存下一个需要拷贝的地址,
- 第二:将r10和r11里面的数据,写到r0保存的地址,也就是uboot重定位的目的地址,r0自动更新。
- 第三:比较r1和r2是否相等,也就是判断uboot代码是否拷贝完成,如果没完成的话,跳转到copy_loop处,继续循环,直到uboot拷贝完成。
-
第95-110行,重定位.rel.dyn段,.rel.dyn段是存放.text段中需要重定位地址的集合。
补充:
一个可执行的bin文件,它的链接地址和运行地址一定要相等,也就是链接到哪个地址,运行之前就需要拷贝到哪个地址中进行运行,在前面的重定位后,运行地址和链接地址就不一样了,这样在进行寻址就会出问题,对.rel.dyn的定位问题进行修复,就是为了解决该问题。
至此uboot的重定位工作已经完成,将保存在低地址的uboot,自搬移到高地址中执行。
3.1.3 relocate_vectors函数详解
在重定位uboot后,需要调用relocate_vectors
函数,来重定位向量表。函数内容为:
ENTRY(relocate_vectors)
#ifdef CONFIG_CPU_V7M
/*
* On ARMv7-M we only have to write the new vector address
* to VTOR register.
*/
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
ldr r1, =V7M_SCB_BASE
str r0, [r1, V7M_SCB_VTOR]
#else
#ifdef CONFIG_HAS_VBAR
/*
* If the ARM processor has the security extensions,
* use VBAR to relocate the exception vectors.
*/
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
mcr p15, 0, r0, c12, c0, 0 /* Set VBAR */
#else
...
#endif
#endif
bx lr
ENDPROC(relocate_vectors)
在IMX6Q中,定义了CONFIG_HAS_VBAR
,因此这个函数只用执行以下语句:
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
mcr p15, 0, r0, c12, c0, 0 /* Set VBAR */
首先是将gd->relocaddr的值赋给r0,也就是r0里面保存重定位后uboot的首地址,中断向量表也是从这个首地址开始存储的,接下来,使用mcr指令,将r0的值写到CP15的VBAR寄存器中,其实就是将新的中断向量表首地址写到VBAR寄存器中,设置中断向量表偏移。
至此uboot的重定位与中断向量表重定位的工作已经完成。
3.2 board_init_r函数
2.1节对board_init_f
函数进行介绍,其作用gd结构体的成员变量,以及对DRAM内存进行规划。而本节board_init_r
函数是将一些板子需要的外设进行初始化。
3.2.1 设置board_init_r环境
调用board_init_r函数前,需要设置一些参数,其代码为:
bl c_runtime_cpu_setup /* we still call old routine here */
#endif
#if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)
#if !defined(CONFIG_SPL_BUILD) || !defined(CONFIG_SPL_EARLY_BSS)
CLEAR_BSS
#endif
...
#if ! defined(CONFIG_SPL_BUILD)
bl coloured_LED_init
bl red_led_on
#endif
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
#if CONFIG_IS_ENABLED(SYS_THUMB_BUILD)
ldr lr, =board_init_r /* this is auto-relocated! */
bx lr
#else
ldr pc, =board_init_r /* this is auto-relocated! */
#endif
/* we should not return here. */
#endif
首先是调用了函数c_runtime_cpu_setup
函数,关闭cache。然后调用CLEAR_BSS
清除bss段。接着将gd
地址赋值给r0,将gd->relocaddr
地址赋值给r1。board_init_r(gd_t *id, ulong dest_addr)
函数具有两个参数,第一个参数为gd
指针的值,第二个参数为gd->relocaddr
。最后跳转到board_init_r
函数执行。
3.2.2 board_init_r函数详解
board_init_r
函数定义在common/board_r.c
文件中。函数的主要内容为:
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
...
#ifdef CONFIG_NEEDS_MANUAL_RELOC
int i;
#endif
#if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
gd = new_gd;
#endif
gd->flags &= ~GD_FLG_LOG_READY;
#ifdef CONFIG_NEEDS_MANUAL_RELOC
for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
init_sequence_r[i] += gd->reloc_off;
#endif
if (initcall_run_list(init_sequence_r))
hang();
/* NOTREACHED - run_main_loop() does not return */
hang();
}
该函数的重点是initcall_run_list(init_sequence_r)
,init_sequence_r
是一个函数集合的数组,也就是函数初始化序列。其内容为:
641 static init_fnc_t init_sequence_r[] = { //函数初始化序列
642 initr_trace,
643 initr_reloc,
644 /* TODO: could x86/PPC have this also perhaps? */
645 #ifdef CONFIG_ARM
646 initr_caches,
647 /* Note: For Freescale LS2 SoCs, new MMU table is created in DDR.
648 * A temporary mapping of IFC high region is since removed,
649 * so environmental variables in NOR flash is not available
650 * until board_init() is called below to remap IFC to high
651 * region.
652 */
653 #endif
654 initr_reloc_global_data,
655 #if defined(CONFIG_SYS_INIT_RAM_LOCK) && defined(CONFIG_E500)
656 initr_unlock_ram_in_cache,
657 #endif
658 initr_barrier,
659 initr_malloc,
660 log_init,
661 initr_bootstage, /* Needs malloc() but has its own timer */
662 #if defined(CONFIG_CONSOLE_RECORD)
663 console_record_init,
664 #endif
665 #ifdef CONFIG_SYS_NONCACHED_MEMORY
666 noncached_init,
667 #endif
668 initr_of_live,
669 #ifdef CONFIG_DM
670 initr_dm,
671 #endif
672 #ifdef CONFIG_ADDR_MAP
673 initr_addr_map,
674 #endif
675 #if defined(CONFIG_ARM) || defined(CONFIG_NDS32) || defined(CONFIG_RISCV) || \
676 defined(CONFIG_SANDBOX)
677 board_init, /* Setup chipselects */
678 #endif
679 /*
680 * TODO: printing of the clock inforamtion of the board is now
681 * implemented as part of bdinfo command. Currently only support for
682 * davinci SOC's is added. Remove this check once all the board
683 * implement this.
684 */
685 #ifdef CONFIG_CLOCKS
686 set_cpu_clk_info, /* Setup clock information */
687 #endif
688 #ifdef CONFIG_EFI_LOADER
689 efi_memory_init,
690 #endif
691 initr_binman,
692 #ifdef CONFIG_FSP_VERSION2
693 arch_fsp_init_r,
694 #endif
695 initr_dm_devices,
696 stdio_init_tables,
697 serial_initialize,
698 initr_announce,
699 #if CONFIG_IS_ENABLED(WDT)
700 initr_watchdog,
701 #endif
702 INIT_FUNC_WATCHDOG_RESET
703 #if defined(CONFIG_NEEDS_MANUAL_RELOC) && defined(CONFIG_BLOCK_CACHE)
704 blkcache_init,
705 #endif
706 #ifdef CONFIG_NEEDS_MANUAL_RELOC
707 initr_manual_reloc_cmdtable,
708 #endif
709 arch_initr_trap,
710 #if defined(CONFIG_BOARD_EARLY_INIT_R)
711 board_early_init_r,
712 #endif
713 INIT_FUNC_WATCHDOG_RESET
714 #ifdef CONFIG_POST
715 post_output_backlog,
716 #endif
717 INIT_FUNC_WATCHDOG_RESET
718 #if defined(CONFIG_PCI_INIT_R) && defined(CONFIG_SYS_EARLY_PCI_INIT)
719 /*
720 * Do early PCI configuration _before_ the flash gets initialised,
721 * because PCU resources are crucial for flash access on some boards.
722 */
723 pci_init,
724 #endif
725 #ifdef CONFIG_ARCH_EARLY_INIT_R
726 arch_early_init_r,
727 #endif
728 power_init_board,
729 #ifdef CONFIG_MTD_NOR_FLASH
730 initr_flash,
731 #endif
732 INIT_FUNC_WATCHDOG_RESET
733 #if defined(CONFIG_PPC) || defined(CONFIG_M68K) || defined(CONFIG_X86)
734 /* initialize higher level parts of CPU like time base and timers */
735 cpu_init_r,
736 #endif
737 #ifdef CONFIG_CMD_NAND
738 initr_nand,
739 #endif
740 #ifdef CONFIG_CMD_ONENAND
741 initr_onenand,
742 #endif
743 #ifdef CONFIG_MMC
744 initr_mmc,
745 #endif
746 #ifdef CONFIG_XEN
747 xen_init,
748 #endif
749 #ifdef CONFIG_PVBLOCK
750 initr_pvblock,
751 #endif
752 initr_env,
753 #ifdef CONFIG_SYS_BOOTPARAMS_LEN
754 initr_malloc_bootparams,
755 #endif
756 INIT_FUNC_WATCHDOG_RESET
757 cpu_secondary_init_r,
758 #if defined(CONFIG_ID_EEPROM) || defined(CONFIG_SYS_I2C_MAC_OFFSET)
759 mac_read_from_eeprom,
760 #endif
761 INIT_FUNC_WATCHDOG_RESET
762 #if defined(CONFIG_PCI_INIT_R) && !defined(CONFIG_SYS_EARLY_PCI_INIT)
763 /*
764 * Do pci configuration
765 */
766 pci_init,
767 #endif
768 stdio_add_devices,
769 jumptable_init,
770 #ifdef CONFIG_API
771 api_init,
772 #endif
773 console_init_r, /* fully init console as a device */
774 #ifdef CONFIG_DISPLAY_BOARDINFO_LATE
775 console_announce_r,
776 show_board_info,
777 #endif
778 #ifdef CONFIG_ARCH_MISC_INIT
779 arch_misc_init, /* miscellaneous arch-dependent init */
780 #endif
781 #ifdef CONFIG_MISC_INIT_R
782 misc_init_r, /* miscellaneous platform-dependent init */
783 #endif
784 INIT_FUNC_WATCHDOG_RESET
785 #ifdef CONFIG_CMD_KGDB
786 initr_kgdb,
787 #endif
788 interrupt_init,
789 #if defined(CONFIG_MICROBLAZE) || defined(CONFIG_M68K)
790 timer_init, /* initialize timer */
791 #endif
792 #if defined(CONFIG_LED_STATUS)
793 initr_status_led,
794 #endif
795 /* PPC has a udelay(20) here dating from 2002. Why? */
796 #ifdef CONFIG_CMD_NET
797 initr_ethaddr,
798 #endif
799 #if defined(CONFIG_GPIO_HOG)
800 gpio_hog_probe_all,
801 #endif
802 #ifdef CONFIG_BOARD_LATE_INIT
803 board_late_init,
804 #endif
805 #ifdef CONFIG_FSL_FASTBOOT
806 initr_fastboot_setup,
807 #endif
808 #if defined(CONFIG_SCSI) && !defined(CONFIG_DM_SCSI)
809 INIT_FUNC_WATCHDOG_RESET
810 initr_scsi,
811 #endif
812 #ifdef CONFIG_BITBANGMII
813 bb_miiphy_init,
814 #endif
815 #ifdef CONFIG_PCI_ENDPOINT
816 pci_ep_init,
817 #endif
818 #ifdef CONFIG_CMD_NET
819 INIT_FUNC_WATCHDOG_RESET
820 initr_net,
821 #endif
822 #ifdef CONFIG_POST
823 initr_post,
824 #endif
825 #if defined(CONFIG_IDE) && !defined(CONFIG_BLK)
826 initr_ide,
827 #endif
828 #ifdef CONFIG_LAST_STAGE_INIT
829 INIT_FUNC_WATCHDOG_RESET
830 /*
831 * Some parts can be only initialized if all others (like
832 * Interrupts) are up and running (i.e. the PC-style ISA
833 * keyboard).
834 */
835 last_stage_init,
836 #endif
837 #ifdef CONFIG_CMD_BEDBUG
838 INIT_FUNC_WATCHDOG_RESET
839 bedbug_init,
840 #endif
841 #if defined(CONFIG_PRAM)
842 initr_mem,
843 #endif
844 #ifdef CONFIG_EFI_SETUP_EARLY
845 (init_fnc_t)efi_init_obj_list,
846 #endif
847 #if defined(AVB_RPMB) && !defined(CONFIG_SPL)
848 initr_avbkey,
849 #endif
850 #ifdef CONFIG_IMX_TRUSTY_OS
851 initr_tee_setup,
852 #endif
853 #ifdef CONFIG_FSL_FASTBOOT
854 initr_check_fastboot,
855 #endif
856 #ifdef CONFIG_DUAL_BOOTLOADER
857 initr_check_spl_recovery,
858 #endif
859 run_main_loop,
860 };
-
第642行,
initr_trace
,该函数是与初始化和调试跟踪相关的内容。 -
第643行,
initr_reloc
,该函数设置了gd->flags成员,标记uboot重定位完成。 -
第646行,
initr_reloc
,该函数用于初始化cache,使能cache。 -
第654行,
initr_reloc_global_data
,该函数用于初始化重定为后gd的一些成员变量。。 -
第659行,
initr_malloc
,该函数用于初始化malloc。int board_init(void) { /* address of boot parameters */ gd->bd->bi_boot_params = PHYS_SDRAM + 0x100; #if defined(CONFIG_DM_REGULATOR) regulators_enable_boot_on(false); #endif #ifdef CONFIG_MXC_SPI setup_spi(); #endif #ifdef CONFIG_SYS_I2C setup_i2c(1, CONFIG_SYS_I2C_SPEED, 0x7f, &i2c_pad_info1); #endif #if defined(CONFIG_PCIE_IMX) && !defined(CONFIG_DM_PCI) setup_pcie(); #endif #if defined(CONFIG_MX6DL) && defined(CONFIG_MXC_EPDC) setup_epdc(); #endif #ifdef CONFIG_FEC_MXC setup_fec(); #endif return 0; }
-
第696行,
stdio_init_tables
,该函数用于stdio相关初始化。 -
第 697行,
serial_initialize
,该函数初始化串口相关东西。 -
第738行,
initr_nand
,该函数用于nand flash初始化,需要时调用。 -
第744行,
initr_mmc
,该函数用来初始化和sd/mmc相关的接口,实际调用mmc_initialize
函数实现,定义在drivers/mmc/mmc.c
文件中,其内容为:int mmc_initialize(struct bd_info *bis) { static int initialized = 0; int ret; if (initialized) /* Avoid initializing mmc multiple times */ return 0; initialized = 1; #if !CONFIG_IS_ENABLED(BLK) #if !CONFIG_IS_ENABLED(MMC_TINY) mmc_list_init(); #endif #endif ret = mmc_probe(bis); if (ret) return ret; #ifndef CONFIG_SPL_BUILD print_mmc_devices(','); #endif mmc_do_preinit(); return 0; }
-
第744行,
initr_env
,该函数用来初始化环境变量。 -
第768行,
stdio_add_devices
,该函数用于初始化各种输入输出设备,例如LCD相关的设备。 -
第769行,
stdio_add_devices
,该函数用来初始化跳转表相关的内容。 -
第78行,
interrupt_init
,该函数用来初始化中断相关内容。IMX6Q为空,需要时使用。 -
第797行,
interrupt_init
,该函数用于初始化网络地址,用于获取网卡的MAC地址。 -
第803行,
board_late_init
,该函数用于板级后续的一些外设初始化。 -
第806行,
initr_fastboot_setup
,初始化fastboot。 -
第820行,
initr_net
,初始化以太网,实际调用eth_initialize
函数实现,其定义在net/eth-uclass.c
文件。 -
第859行,
run_main_loop
,该函数主循环函数,用于处理输入的命令,也就是uboot进入了命令行终端模式,内容为:static int run_main_loop(void) { #ifdef CONFIG_SANDBOX sandbox_main_loop_init(); #endif /* main_loop() can return to retry autoboot, if so just run it again */ for (;;) main_loop(); return 0; }
函数在无线循环执行
main_loop
函数,此函数将在下一小节详细介绍。至此
board_init_r
函数介绍完,主要是完成一些需要的外设初始化,不同的板子,会进行不同的初始化。
3.2.3 main_loop函数
main_loop
函数主要作用是实现目标板与人的交互,其定义在common/main.c
文件中,内容为:
40 void main_loop(void)
41 {
42 const char *s;
43
44 bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
45
46 if (IS_ENABLED(CONFIG_VERSION_VARIABLE))
47 env_set("ver", version_string); /* set version variable */
48
49 cli_init();
50
51 if (IS_ENABLED(CONFIG_USE_PREBOOT))
52 run_preboot_environment_command();
53
54 if (IS_ENABLED(CONFIG_UPDATE_TFTP))
55 update_tftp(0UL, NULL, NULL);
56
57 if (IS_ENABLED(CONFIG_EFI_CAPSULE_ON_DISK_EARLY))
58 efi_launch_capsules();
59
60 s = bootdelay_process();
61 if (cli_process_fdt(&s))
62 cli_secure_boot_cmd(s);
63
64 autoboot_command(s);
65
66 cli_loop();
67 panic("No CLI available");
68 }
-
第60行,
bootdelay_process
,该函数获取bootdelay
和bootcmd
参数的内容,返回值为环境变量bootcmd
的值。 -
第64行,
autoboot_command
,该函数的作用是,用于检测u-boot启动过程中的倒计时过程是否结束。倒计时结束之前是否被打断。函数定义在文件common/autoboot.c
,其内容为:void autoboot_command(const char *s) { debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>"); if (s && (stored_bootdelay == -2 || (stored_bootdelay != -1 && !abortboot(stored_bootdelay)))) { bool lock; int prev; lock = IS_ENABLED(CONFIG_AUTOBOOT_KEYED) && !IS_ENABLED(CONFIG_AUTOBOOT_KEYED_CTRLC); if (lock) prev = disable_ctrlc(1); /* disable Ctrl-C checking */ run_command_list(s, -1, 0); if (lock) disable_ctrlc(prev); /* restore Ctrl-C checking */ } if (IS_ENABLED(CONFIG_USE_AUTOBOOT_MENUKEY) && menukey == AUTOBOOT_MENUKEY) { s = env_get("menucmd"); if (s) run_command_list(s, -1, 0); } }
如果倒计时正常结束,那么将执行
run_command_list
,此函数会执行参数s指定的一系列命令,也就是bootcmd
中配置中的命令,一般配置为linux内核启动命令,因此linux内核启动。如果在倒计时结束前按下回车键,
run_command_list
就不会执行,autoboot_command
相当于空函数。run_command_list
函数定义在common/cli.c
文件,其内容为:int run_command_list(const char *cmd, int len, int flag) { int need_buff = 1; char *buff = (char *)cmd; /* cast away const */ int rcode = 0; if (len == -1) { len = strlen(cmd); #ifdef CONFIG_HUSH_PARSER /* hush will never change our string */ need_buff = 0; #else /* the built-in parser will change our string if it sees \n */ need_buff = strchr(cmd, '\n') != NULL; #endif } if (need_buff) { buff = malloc(len + 1); if (!buff) return 1; memcpy(buff, cmd, len); buff[len] = '\0'; } #ifdef CONFIG_HUSH_PARSER rcode = parse_string_outer(buff, FLAG_PARSE_SEMICOLON); #else #ifdef CONFIG_CMDLINE rcode = cli_simple_run_command_list(buff, flag); #else rcode = board_run_command(buff); #endif #endif if (need_buff) free(buff); return rcode; }
主要工作内容为,调用
parse_string_outer
函数解析并执行bootcmd
中配置中的命令。 -
第66行,
cli_init
,该函数是uboot命令行处理函数,输入的各种命令,进行各种操作就是cli_loop
函数来处理的,函数处理过程比较复杂,就不单独分析。
至此整个uboot2021启动流程分析完成,下面介绍uboot中的串口模块。