uboot移植与源码分析总结(3)-Serial驱动分析

之前简单实现过s5pv210的串口驱动,能正常使用,不过只能支持一个串口。串口驱动的实现还是比较简单的。

UBoot的Serial驱动主要功能有如下几点:

  • 输出各种调试信息、启动信息;
  • 提供命令交互接口;
  • 支持低速的文件传输;

不过,现在新出的电脑很多都不再提供串口,所以需要自己配一个USB转RS232的转接线。质量建议选好的,相应的驱动也要装个稳定点的,否则一不小心电脑就容易蓝屏死机,我是遇到过好多次这种情况。

从uboot官网上下载的最新uboot源码中已经提供了s5pv210的serial驱动,所以不需要自己写,简单的改了改uboot配置文件就可以在tiny210开发板上用。所以下面的内容不谈移植,主要了解uboot对串口是如何处理的。

所有涉及的文件为driver/serial/{serial.c、serial_s5p.c}。

UBoot的串口设备抽像结构及操作接口(serial.c)

UBoot将串口设备抽像为struct serial_device,这个结构包含了所有串口的基本操作接口。接口实现比较简单,无非是初始化、反初始化、接收发送字符/字符串、设置波特率。而且由于uboot本身就是一个前后台程序,所以在实现这些操作接口时跟我们平时写前后台的串口驱动代码没有任何区别。

struct serial_device { 
    char    name[16];

    int    (*start)(void); 
    int    (*stop)(void); 
    void    (*setbrg)(void); 
    int    (*getc)(void); 
    int    (*tstc)(void); 
    void    (*putc)(const char c); 
    void    (*puts)(const char *s); 
#if CONFIG_POST & CONFIG_SYS_POST_UART 
    void    (*loop)(int); 
#endif 
    struct serial_device    *next; 
};

然后,UBoot定义了两个指针和一个注册函数:

void serial_register(struct serial_device *dev)

static struct serial_device *serial_devices; 
static struct serial_device *serial_current;

这样,如果系统中如果有多个设备的话,就会逐个通过serial_register(dev)加入到一个串口设备链表中。 每个设备都有一个名称,当需要切换当前的串口设备时,则调用serial_assign(name)。

所有的设备都在serial_initialize()中注册到该链表中。

截图00

这个链表没有提供删除操作,对于一个bootloader而言,这个功能没必要。个人甚至认为在绝大数情况下,多串口的支持也不是必要的。

在选定当前使用的设备后,就可以调用下面的操作接口来完成通信操作。这些接口调用了struct serial_device结构中的底层接口:

  • int serial_init(void)
  • void serial_reinit_all(void)
  • void serial_setbrg(void)
  • int serial_getc(void)
  • int serial_tstc(void)
  • void serial_putc(const char c)
  • void serial_puts(const char *s)
  • void default_serial_puts(const char *s)

s5pv210的串口驱动实现(serial_s5p.c)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

uboot的串口初始化机制有些奇怪,除了使用了很多GCC扩展特性外,代码写的比较糟糕。

一般来说,无论是支持什么设备,driver/serial.c这个文件是不应该修改的,它应当被设计成跟具体的设备无关。但是它采用了下面的实现代码:

void serial_initialize(void) 

    mpc8xx_serial_initialize(); 
    ns16550_serial_initialize(); 
    pxa_serial_initialize(); 
    s3c24xx_serial_initialize();               // s3c2400的串口初始化 
    s5p_serial_initialize();                      // s5pv210的串口初始化

   ……………………………………………….. 
    serial_assign(default_serial_console()->name); 
}

也就是说,serial_initialize包含了uboot能支持的各种mcu串口初始化函数。如果你想支持一种新的MCU,则必须修改serial.c文件,将初始化函数加入到serial_initialize中。

serial_initialize中虽然包含了很多初始化函数,但不是每个函数都需要加入到编译中。在针对s5pv210的编译中,其它的诸如mpc8xx_serial_initialize()之类调用,会被替换成调用空的serial_null函数。

#define serial_initfunc(name)                    \ 
    void name(void)                        \ 
        __attribute__((weak, alias("serial_null")));

serial_initfunc(mpc8xx_serial_initialize);

s5pv210有多个串口,所以s5p_serial_initialize()主要完成的操作是注册多个struct serial_device结构。当然,可以只注册需要使用的其中一个。

void s5p_serial_initialize(void) 

    serial_register(&s5p_serial0_device); 
    serial_register(&s5p_serial1_device); 
    serial_register(&s5p_serial2_device); 
    serial_register(&s5p_serial3_device); 
}

因为各个串口的操作方式都差不多,只不过寄存器的地址不同,所以使用同样的底层接口函数,通过dev_index来区分当前所使用的设备。

  • void serial_setbrg_dev(const int dev_index)
  • int serial_init_dev(const int dev_index)
  • int serial_getc_dev(const int dev_index)
  • void serial_putc_dev(const char c, const int dev_index)
  • int serial_tstc_dev(const int dev_index)
  • void serial_puts_dev(const char *s, const int dev_index)

之后便是用这些函数来填充多个s5p_serialx_device结构,源码中使用了C语言的宏特性,避免反复写很类似的代码。不过看起来比较费劲,可读性不强。之后就是调用前面提到的s5p_serial_initialize来完成设备结构注册。

/* Multi serial device functions */ 
#define DECLARE_S5P_SERIAL_FUNCTIONS(port) \ 
int s5p_serial##port##_init(void) { return serial_init_dev(port); } \ 
void s5p_serial##port##_setbrg(void) { serial_setbrg_dev(port); } \ 
int s5p_serial##port##_getc(void) { return serial_getc_dev(port); } \ 
int s5p_serial##port##_tstc(void) { return serial_tstc_dev(port); } \ 
void s5p_serial##port##_putc(const char c) { serial_putc_dev(c, port); } \ 
void s5p_serial##port##_puts(const char *s) { serial_puts_dev(s, port); }

#define INIT_S5P_SERIAL_STRUCTURE(port, __name) {    \ 
    .name    = __name,                \ 
    .start    = s5p_serial##port##_init,        \ 
    .stop    = NULL,                    \ 
    .setbrg    = s5p_serial##port##_setbrg,        \ 
    .getc    = s5p_serial##port##_getc,        \ 
    .tstc    = s5p_serial##port##_tstc,        \ 
    .putc    = s5p_serial##port##_putc,        \ 
    .puts    = s5p_serial##port##_puts,        \ 
}

DECLARE_S5P_SERIAL_FUNCTIONS(0); 
struct serial_device s5p_serial0_device = 
    INIT_S5P_SERIAL_STRUCTURE(0, "s5pser0"); 
DECLARE_S5P_SERIAL_FUNCTIONS(1); 
struct serial_device s5p_serial1_device = 
    INIT_S5P_SERIAL_STRUCTURE(1, "s5pser1"); 
DECLARE_S5P_SERIAL_FUNCTIONS(2); 
struct serial_device s5p_serial2_device = 
    INIT_S5P_SERIAL_STRUCTURE(2, "s5pser2"); 
DECLARE_S5P_SERIAL_FUNCTIONS(3); 
struct serial_device s5p_serial3_device = 
    INIT_S5P_SERIAL_STRUCTURE(3, "s5pser3");

这些源码中,我认为比较有价值的是UBoot将串口设备抽像为struct serial_device,以及对多串口设备的支持。之前在别的地方也看到不过类似的思路,值得借鉴。

<think>好的,我现在需要详细说明U-Boot及其启动流程。首先,我应该先了解U-Boot是什么,它的主要功能和用途。U-Boot,全称是Das U-Boot,是一个开源的引导加载程序,主要用于嵌入式系统。它支持多种架构,如ARM、MIPS、PowerPC等,常用于引导Linux内核。 接下来,我需要理解U-Boot的启动流程。用户可能想知道从设备上电到加载内核的整个过程。启动流程通常分为几个阶段,每个阶段的具体任务可能有所不同,但大致可以分为以下几个步骤: 1. **ROM Bootloader**:设备上电后,首先执行的是芯片内部ROM中的引导代码。这部分代码通常是固化在硬件中的,负责初始化最基本的硬件,如时钟、内存控制器,并加载下一阶段的引导程序(如SPL)。 2. **SPL (Secondary Program Loader)**:由于内部ROM空间有限,SPL作为第二阶段引导加载程序,负责进一步初始化硬件(如DRAM),并加载完整的U-Boot到内存中。SPL通常是U-Boot的一部分,经过编译后生成的小型镜像。 3. **U-Boot主程序**:SPL加载完成后,跳转到U-Boot的主程序。U-Boot会进行更全面的硬件初始化,包括串口、网卡、存储设备等,然后读取环境变量,根据配置加载内核镜像、设备树文件(DTB)和根文件系统。 4. **加载内核**:U-Boot将内核镜像、设备树和可能的initramfs加载到内存中的指定地址,然后通过bootm或bootz命令启动内核,传递必要的参数。 在这个过程中,需要考虑到不同硬件平台的差异,比如启动介质(NOR Flash、eMMC、SD卡等)、内存布局、环境变量的存储位置(如EEPROM、Flash中的特定分区)等。 另外,U-Boot还支持交互式命令,用户可以在启动过程中打断自动流程,进入命令行界面,手动执行命令进行调试或配置。环境变量在U-Boot中非常重要,它们决定了启动参数和流程,比如bootcmd定义了自动执行的命令序列。 可能还需要解释U-Boot的编译和配置过程,比如通过make menuconfig配置目标板,选择所需的驱动和功能,然后编译生成u-boot.bin等文件。此外,不同开发板的配置文件位于configs目录下,如mx6ullevk_defconfig对应特定的开发板。 在启动流程中,设备树(Device Tree)的作用也很重要,它描述了硬件配置,使得内核无需硬编码硬件信息。U-Boot负责将设备树二进制文件(DTB)加载到内存,并传递给内核。 还要注意U-Boot的启动阶段可能涉及多个文件,比如SPL、U-Boot proper,以及如何将它们烧写到存储设备的正确位置。例如,在SD卡启动时,SPL可能需要存放在特定的偏移量位置,以便ROM Bootloader能够找到并加载它。 可能存在的疑问点包括:SPL和U-Boot的关系,如何配置环境变量,如何处理不同的启动介质,U-Boot如何内核交互,以及如何调试启动问题。需要确保这些点在回答中都有所覆盖,并且结构清晰,步骤明确。 最后,需要验证这些信息是否正确,比如确认U-Boot的启动阶段划分是否准确,SPL的作用是否描述正确,以及加载内核的具体命令和流程是否符合实际。如果有不确定的地方,可能需要查阅U-Boot的官方文档或源码进行确认,确保回答的准确性。</think>--- ### **U-Boot详解启动流程分析** --- #### **1. U-Boot概述** **定义**: U-Boot(Universal Boot Loader)是嵌入式系统领域使用最广泛的开源引导加载程序,支持**ARM、MIPS、PowerPC、RISC-V**等30+处理器架构,兼容**NOR Flash、eMMC、NAND、SD卡**等多种存储介质。 **核心功能**: - 硬件初始化(时钟/DDR/外设) - 多阶段引导加载 - 固件升级(USB DFU/TFTP) - 环境变量管理 - 操作系统映像加载(Linux/FreeRTOS/VxWorks) - 安全启动(Verified Boot) --- #### **2. 典型启动流程** ```mermaid sequenceDiagram participant ROM Code participant SPL participant U-Boot participant Kernel ROM Code->>SPL: 加载SPL到SRAM SPL->>U-Boot: 初始化DRAM,加载U-Boot到内存 U-Boot->>U-Boot: 外设初始化(网卡/USB/存储) U-Boot->>Kernel: 加载内核镜像、设备树、initramfs Kernel->>Kernel: 解压并启动操作系统 ``` --- #### **3. 关键阶段详解** **3.1 ROM Bootloader** - **硬件行为**:芯片上电后自动执行固化在ROM中的代码 - **主要任务**: - 初始化基础时钟(CPU/总线) - 检测启动介质(通过GPIO或OTP配置) - 加载SPL到内部SRAM(典型大小:64-256KB) **示例代码(i.MX6ULL)**: ```c /* arch/arm/mach-imx/imx6ull/lowlevel_init.S */ bl setup_pll // 配置PLL时钟 bl setup_dcdr // DDR控制器初始化 ldr r0, =SPL_LOAD_ADDR bl load_image // 从SD卡加载SPL ``` --- **3.2 SPL阶段** - **二进制文件**:`u-boot-spl.bin` - **内存限制**:通常在SRAM中运行(无DRAM支持) - **核心任务**: 1. 初始化DRAM控制器 2. 设置内存映射表 3. 加载完整U-Boot到DRAM 4. 验证U-Boot镜像签名(安全启动场景) **关键配置选项**: ```makefile # configs/mx6ullevk_defconfig CONFIG_SPL=y CONFIG_SPL_DM=y CONFIG_SPL_LOAD_FIT=y ``` --- **3.3 U-Boot主程序** - **入口函数**:`lib/init/board_init.c`中的`board_init_f()` - **启动阶段划分**: ```c board_init_f() // 初始化全局数据结构(gd_t) ↓ relocate_code() // 重定位到DRAM高端地址 ↓ board_init_r() // 完整外设初始化 ``` **硬件初始化顺序**: 1. 串口调试输出(`DEBUG_PORT`) 2. 内存控制器配置 3. 存储设备初始化(MMC/SATA) 4. 网络接口(PHY初始化) 5. 环境变量加载(`env_relocate()`) --- **3.4 环境变量管理** - **存储位置**:Flash专用分区(如eMMC的ENV分区) - **关键变量**: ```bash bootcmd=mmc dev 0; ext4load mmc 0:1 0x80800000 zImage; \ ext4load mmc 0:1 0x83000000 imx6ull.dtb; \ bootz 0x80800000 - 0x83000000 bootargs=console=ttymxc0,115200 root=/dev/mmcblk0p2 ro ``` **操作命令**: ```bash # 查看环境变量 printenv # 修改并保存 setenv ipaddr 192.168.1.100 saveenv ``` --- #### **4. 内核加载过程** **典型bootm命令流程**: ```c do_bootm() ↓ bootm_start() // 解析镜像头信息 ↓ bootm_load_os() // 加载内核到指定地址 ↓ boot_fn = boot_os[os] // 选择启动方法(Linux/NetBSD等) ↓ do_bootm_linux() // 设置ATAGs/FDT,跳转到内核入口 ``` **ARM平台内存布局示例**: | 地址范围 | 用途 | |------------------|-----------------------| | 0x80000000 | 内核加载地址 | | 0x81000000 | 设备树地址 | | 0x82000000 | initramfs地址 | --- #### **5. 安全启动实现** **数字签名验证流程**: ```mermaid graph LR A[SPL] --> B{验证U-Boot签名} B -->|有效| C[加载U-Boot] B -->|无效| D[进入恢复模式] C --> E[U-Boot验证内核签名] E -->|有效| F[启动内核] E -->|无效| G[终止启动] ``` **关键配置**: ```makefile CONFIG_SPL_FIT_SIGNATURE=y # 启用FIT镜像签名 CONFIG_SPL_CRYPTO_SUPPORT=y # 加密算法支持 CONFIG_SPL_RSA_VERIFY=y # RSA验证 ``` --- #### **6. 调试优化** **6.1 常用调试手段**: - 串口日志输出(`CONFIG_DEBUG_UART`) - LED指示灯控制(`led_set_state()`) - 内存检测(`mtest`命令) - 启动暂停(`CONFIG_AUTOBOOT_KEYED`) **6.2 性能优化技巧**: ```c /* 加速存储设备访问 */ #define CONFIG_SYS_MMC_ENV_DEV 0 #define CONFIG_SYS_MMC_MAX_BLK_COUNT 1024 // 增大块传输数量 /* 压缩镜像尺寸 */ #define CONFIG_LZMA // 使用LZMA压缩算法 ``` --- #### **7. 典型移植步骤** 1. **创建板级目录**: ```bash board/<vendor>/<board>/ ├── Kconfig ├── Makefile └── <board>.c ``` 2. **配置设备树**: ```dts /dts-v1/; / { model = "Custom Board"; memory@80000000 { reg = <0x80000000 0x20000000>; }; }; ``` 3. **实现关键驱动**: - 串口(`drivers/serial/serial_xxx.c`) - 存储设备(`drivers/mmc/mmc_xxx.c`) - GPIO控制 --- #### **8. 启动时间分析** **典型时间分布(ARM Cortex-A9@1GHz)**: | 阶段 | 耗时(ms) | |----------------|-----------| | ROM Code | 12 | | SPL执行 | 58 | | U-Boot初始化 | 320 | | 内核加载 | 92 | | **总计** | **482** | **优化方向**: - 启用SPL框架(减少初始化步骤) - 预计算CRC32(替代运行时计算) - 并行初始化外设 --- 通过这种层次化的启动架构,U-Boot实现了从硬件初始化到操作系统引导的完整控制流程,其模块化设计和可移植性使其成为嵌入式系统开发的核心组件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值