[toc]

背景介绍

在之前的BSP开发中,非常重要的一部分工作在于UBOOT的适配,但是不同平台的UBOOT之前存在较大差异,导致我们对于uboot的适配工作量很大。适配完成后代码也无法统一,新的方案修改很难同步到使用不同UBOOT的平台中。同时我们为了保证系统可靠性,对于UBOOT也有较高要求,UBOOT至少应该具备JFFS2文件系统或者UBI功能,但是很多时候厂商提供的UBOOT并不支持,适配这些功能也需要付出较大代价。还有就是在UBOOT开发过程中我们需要反复升级UBOOT本身很多时候会将UBOOT破坏导致系统变砖需要离线烧录。所以我们希望可以开发出一套独立的BOOT代码,运行于UBOOT之后内核启动之前,可以包含UBOOT的大部分功能,统一整体的BOOT逻辑。
下图时HIKBOOT整体框图:

1735279667183

新开发的BOOT具有如下几点重要特性:

  1. 底层功能完备:新开发的BOOT代码支持我们现有的所有芯片架构(ARM,ARM64,MIPS),具有完备支持剪裁的内置C库,支持这些平台的内核启动,同时和UBOOT代码具有较好通用性方便后续将驱动代码移入HIKBOOT。
  2. 完整网络协议栈:HIKBOOT支持一套完整的网络协议栈,可以在完成网卡收发包接口适配后,支持很多基于网络的应用功能(TFTP,HTTP,ICMP)。
  3. 兼容LIBBSP的驱动框架:HIKBOOT采用和LIBBSP十分类似的驱动框架,支持使用JSON解析框架将设备数据和设备驱动分离,具有良好的驱动代码复用性,相比LIBBSP,hikboot在驱动框架上更进一步将设备JSON解析过程在编译过程完成,可以将JSON设备树变成C文件,同时支持动态设备注册,更加灵活。
  4. 用户友好的支持扩展的命令行框架:HIKBOOT在适配串口驱动的情况下支持一套用户友好支持命令行系统,支持很多UBOOT兼容的功能命令,同时支持命令补全和候选项功能。
  5. VFS框架支持:HIKBOOT支持一套类似于linux vfs的文件系统框架,支持适配不同的文件系统。默认挂载cpio根文件系统。
  6. SCALL功能支持:在HIKBOOT中支持进行动态函数调用,方便进行功能调试。

任务管理

多进程模式

因为UBOOT本身是单线程BOOT程序,这种设计好处在于整体流程更加简单,不要实现不同架构的上下文切换逻辑,但是也有一定缺点就是这种单线程架构并不利于代码的维护和调试,uboot在为了解决单线程运行的问题在代码中做了大量妥协,在所有涉及到耗时的操作中,uboot都需要加入很多冗余代码比如喂狗操作,串口读取操作, 以及采用大量状态机机制避免忙等阻塞系统。因为HIKBOOT需要支持更复杂的代码逻辑,比如HTTP代码烧写逻辑,这个时候HTTP需要频繁和客户端交互,在处理烧写流程的时候就无法收包,需要额外设计状态机机制,会增加模块代码之间的耦合性,为了提升代码的可维护性,HIKBOOT默认情况下支持多进程模式。

上下文切换的基本原理就是在切换上下文的时候将当前CPU的寄存器保存到对应任务的数据结构中,然后将要切换到任务的寄存器恢复,就完成了依次上下文切换,

切换进程的大致流程如下:

1735476331805

HIKBOOT默认不支持通过中断切换上下文,只能通过任务主动让出CPU。

切换任务的核心函数是jump_fcontext,这个函数需要将当前CPU寄存器内容存储到内存中,并加载下一个任务之前存储的CPU内容。已arm64为例上下文切换代码如下,主要包含make_fcontext任务创建时初始化上下文,设置初始返回地址。以及jump_fcontext将当前寄存器保存到对应任务上下文中,然后从要切换的任务的上下文中恢复寄存器数值。

/*
 *  -------------------------------------------------  *
 *  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  *
 *  -------------------------------------------------  *
 *  | 0x0 | 0x4 | 0x8 | 0xc | 0x10| 0x14| 0x18| 0x1c|  *
 *  -------------------------------------------------  *
 *  |    d8     |    d9     |    d10    |    d11    |  *
 *  -------------------------------------------------  *
 *  -------------------------------------------------  *
 *  |  8  |  9  |  10 |  11 |  12 |  13 |  14 |  15 |  *
 *  -------------------------------------------------  *
 *  | 0x20| 0x24| 0x28| 0x2c| 0x30| 0x34| 0x38| 0x3c|  *
 *  -------------------------------------------------  *
 *  |    d12    |    d13    |    d14    |    d15    |  *
 *  -------------------------------------------------  *
 *  -------------------------------------------------  *
 *  |  16 |  17 |  18 |  19 |  20 |  21 |  22 |  23 |  *
 *  -------------------------------------------------  *
 *  | 0x40| 0x44| 0x48| 0x4c| 0x50| 0x54| 0x58| 0x5c|  *
 *  -------------------------------------------------  *
 *  |    x19    |    x20    |    x21    |    x22    |  *
 *  -------------------------------------------------  *
 *  -------------------------------------------------  *
 *  |  24 |  25 |  26 |  27 |  28 |  29 |  30 |  31 |  *
 *  -------------------------------------------------  *
 *  | 0x60| 0x64| 0x68| 0x6c| 0x70| 0x74| 0x78| 0x7c|  *
 *  -------------------------------------------------  *
 *  |    x23    |    x24    |    x25    |    x26    |  *
 *  -------------------------------------------------  *
 *  -------------------------------------------------  *
 *  |  32 |  33 |  34 |  35 |  36 |  37 |  38 |  39 |  *
 *  -------------------------------------------------  *
 *  | 0x80| 0x84| 0x88| 0x8c| 0x90| 0x94| 0x98| 0x9c|  *
 *  -------------------------------------------------  *
 *  |    x27    |    x28    |    FP     |     LR    |  *
 *  -------------------------------------------------  *
 *  -------------------------------------------------  *
 *  |  40 |  41 |  42 | 43  |           |           |  *
 *  -------------------------------------------------  *
 *  | 0xa0| 0xa4| 0xa8| 0xac|           |           |  *
 *  -------------------------------------------------  *
 *  |     PC    |   align   |           |           |  *
 *  -------------------------------------------------  *
 */
    .text
    .global make_fcontext
    .align 4
    .type make_fcontext, %function
make_fcontext:
    /* shift address in x0 (allocated stack) to lower 16 byte boundary */
    and x0, x0, ~0xF

    /* reserve space for context-data on context-stack */
    sub x0, x0, #0xb0

    /* third arg of make_fcontext() == address of context-function */
    /* store address as a PC to jump in */
    str x2, [x0, #0xa0]

    /* save address of finish as return-address for context-function */
    /* will be entered after context-function returns (LR register) */
    adr x1, finish
    str x1, [x0, #0x98]

    ret x30 /* return pointer to context-data (x0) */

finish:
    /* exit code is zero */
    mov  x0, #0
    /* exit application */
    bl exit

    .text
    .global jump_fcontext
    .align 4
    .type jump_fcontext, %function
jump_fcontext:
    /* prepare stack for GP + FPU */
    sub sp, sp, #0xb0

    /* save d8 - d15 */
    stp d8,  d9,  [sp, #0x00]
    stp d10, d11, [sp, #0x10]
    stp d12, d13, [sp, #0x20]
    stp d14, d15, [sp, #0x30]

    /* save x19-x30 */
    stp x19, x20, [sp, #0x40]
    stp x21, x22, [sp, #0x50]
    stp x23, x24, [sp, #0x60]
    stp x25, x26, [sp, #0x70]
    stp x27, x28, [sp, #0x80]
    stp x29, x30, [sp, #0x90]

    /* save LR as PC */
    str x30, [sp, #0xa0]

    /* store RSP (pointing to context-data) in X0 */
    mov x4, sp

    /* restore RSP (pointing to context-data) from X1 */
    mov sp, x0

    /* load d8 - d15 */
    ldp d8, d9, [sp, #0x00]
    ldp d10, d11, [sp, #0x10]
    ldp d12, d13, [sp, #0x20]
    ldp d14, d15, [sp, #0x30]

    /* load x19-x30 */
    ldp x19, x20, [sp, #0x40]
    ldp x21, x22, [sp, #0x50]
    ldp x23, x24, [sp, #0x60]
    ldp x25, x26, [sp, #0x70]
    ldp x27, x28, [sp, #0x80]
    ldp x29, x30, [sp, #0x90]

    /* return transfer_t from jump */
    /* pass transfer_t as first arg in context function */
    /* X0 == FCTX, X1 == DATA */
    mov x0, x4

    /* load pc */
    ldr x4, [sp, #0xa0]

    /* restore stack from GP + FPU */
    add sp, sp, #0xb0

    ret x4

1735286149812

在HIKBOOT中任务调度只会发生在任务主动发生调度的情况下(sleep , task_yield , mutex)。每个任务需要自觉在任务中插入这些调度点,防止一个任务长久占用CPU资源。

HIKBOOT默认使用了CFS调度器,其核心思想是通过红黑树来管理所有可运行的任务,确保每个任务都能获得公平的执行时间。CFS调度器使用红黑树来维护一个就绪队列,每个任务通过一个调度实体(sched_entity)来表示,并通过虚拟运行时间(vruntime)来决定任务的执行顺序。

CFS 调度器没有时间片的概念,CFS 的理念就是让每个进程拥有相同的使用 CPU 的时间。比如有 n 个可运行的进程,那么每个进程将能获取的处理时间为 1/n。

在 CFS 调度器中引用权重来代表进程的优先级。各个进程按照权重的比例来分配使用 CPU 的时间。比如2个进程 A 和 B, A 的权重为 100, B 的权重为200,那么 A 获得的 CPU 的时间为 100/(100+200) = 33%, B 进程获得的CPU 的时间为 200/(100+200) = 67%。

在引入权重之后,在一个调度周期中分配给进程的运行时间计算公式如下:

实际运行时间 = 调度周期 * 进程权重 / 所有进程权重之和

CFS调度器针对优先级又提出了nice值的概念,其实和权重是一一对应的关系。nice值就是一个具体的数字,取值范围是[-20, 19]。数值越小代表优先级越大,同时也意味着权重值越大,nice值和权重之间可以互相转换。内核提供了一个表格转换nice值和权重。数值越小代表优先级越大,同时也意味着权重值越大,nice值和权重之间可以互相转换。内核提供了一个表格转换nice值和权重。


nice值共有40个,与权重之间,每一个nice值相差10%左右。
static const uint32_t nice_to_weight[40] = {

 /* -20 */     88761,     71755,     56483,     46273,     36291,

 /* -15 */     29154,     23254,     18705,     14949,     11916,

 /* -10 */      9548,      7620,      6100,      4904,      3906,

 /*  -5 */      3121,      2501,      1991,      1586,      1277,

 /*   0 */      1024,       820,       655,       526,       423,

 /*   5 */       335,       272,       215,       172,       137,

 /*  10 */       110,        87,        70,        56,        45,

 /*  15 */        36,        29,        23,        18,        15,

};

数组的值可以看作是公式:weight = 1024 / (1.25^nice)计算得到。公式中的1.25取值依据是:进程每降低一个nice值,将多获得10% cpu的时间。公式中以1024权重为基准值计算得来,1024权重对应nice值为0,其权重被称为NICE_0_LOAD=1024。

虚拟运行的时间 = 实际运行时间 * NICE_0_LOAD / 该任务的权重;

可以看出,CFS通过红黑树尽量保证虚拟运行时间相同,那么权重越大,分到的运行时间越多。

1735525944234

(1) 每次调度发生时,根据各个任务的权重值,可以计算出运行时间 runtime;
(2) 运行时间runtime可以转换成虚拟运行时间vruntime,虚拟运行事件由任务权重和任务实际运行时间共同决定;
(3) 根据虚拟运行时间的大小,插入到CFS红黑树中,虚拟运行时间少的调度实体放置到左边;
(4) 在下一次任务调度的时候,选择虚拟运行时间少的调度实体来运行;

这种机制目前已支持ARM64,ARM32,MIPS32平台。

单进程模式

上面说的基于上下文切换机制实现的任务调度好处是代码更加容易实现避免纯裸机编程需要考虑的诸多问题,有利于更加复杂功能的编程实现,但是对于非网管平台以及不需要实现复杂裸机的地方以及没有实现上下文切换机制的平台依然可以使用单进程模式。

1735287125457

在这种模式下使用一条链表维护所有任务,在循环过程中会遍历所有链表,将链表中所有任务都有一个起始时间和运行周期,调度器会判断任务是否到运行时间,如果到达时间则会将任务对应函数运行一遍。运行完成后会更新任务起始时间。

1735287503440

启动流程

HIKBOOT启动于UBOOT之后在UBOOT跳转HIKBOOT之前已经完成C语言环境设置,所以HIKBOOT理论上不需要实现汇编启动代码。HIKBOOT支持两种主要的运行模式,一种是多进程模式,一种是单进程模式,两种模式的启动流程略有不同。

多进程模式

如下是多进程模式中的启动流程。HIKBOOT默认使用该模式。

1735281875648

  1. 首先在UBOOT中需要将HIKBOOT加载到HIKBOOT的链接地址中,然后直接跳转到该地址。
    对于一般的UBOOT来说在调试阶段可以使用类似的命令直接进行HIKBOOT调试,tftpboot 0x44000000 hikboot.bin;go 0x44000000
    在正式流程中需要从FLASH分区中读取。
  2. 内存初始化,主要是将HIKBOOT需要用到的那部分内存空间清一下0,同时将系统中用作堆内存的数组注册给内存管理模块。
  3. INITCALL机制,是一种思想上参考内核initcall的特殊机制,在的链接脚本中一般会有init段,然后在编译过程中会将一些初始化函数保存在这些init段中,这样的好处是非常明显的,不同模块之间的init代码可以非常有效的一起运行,避免出现类似UBOOT 直接使用数组管理的代码耦合情况。为了避免对链接脚本的直接修改,我们选择在编译过程中查看C文件中存在的这写宏定义。1735282462280如果 我们实现了一个Init函数同时希望加入初始化流程只需要 在C文件中添加 late_initcall(init_code);在hikboot编译过程中会将所有initcall组织成如下数组

1735282625860

hikboot会依次将这个数组中的函数调用, 通过这种方式即可在启动过程中很轻松的加入初始化代码。
  1. 在完成所有initcall后,hikboot会判断是否有打断启动时事件发生,如果有就会创建出IDLE和SHELL两个任务,之后在INIT进程结束后默认情况下会运行这两个任务。

单进程模式

单进程模式启动流程和多进程模式无明显区别。区别主要就是启动后任务执行方式,多进程采用上下文切换方式,单进程采用轮询任务链表方式。

1735288476403

驱动框架

HIKBOOT使用了类似于LIBBSP的驱动框架,串起整个驱动框架的是三个数据结构以及三张表。主要就是device,driver,class。下图主要体现了数据结构之间的关系。

1735289786106

相比LIBBSP的驱动框架HIKBOOT做了几点改进:

  1. 编译时进行设备树解析:HIKBOOT会将每个平台的所有设备树转化成多个C文件和一个H文件,C文件中主要包含一个公用的device_class_table表格和driver_table,每个设备树对应的device_class_table和device_table以及对应于每个device的私有数据pdata。H文件主要包含class_id的枚举,以及pdata类型的定义。这种方式规避进行json在板端解析造成的资源消耗,解决hikboot无法运行于资源紧张的单片机平台的问题。同时这种方式同时解决了驱动依赖文件系统的问题。
  2. 动态设备注册功能:LIBBSP的DEVICE_TABLE是在bsp_init的时候生成的,一旦生成device_table大小不可更改,不可后续插入设备,为了解决这个问题我们在CLASS中添加链表节点,索引动态DEVICE,支持通过bsp_register_device接口将设备动态的注册到驱动框架中。

因为涉及到BOARDID选择问题,所以说一般我们需要在bsp_init之前确定boardid从而选择一个合适的device_table进行运行。但是读取boardid又需要依赖gpio驱动,这里正常情况会有一个鸡生蛋蛋生鸡的问题,但是这里因为使用了动态设备注册,我们可以在bsp_init之前先注册gpio控制器驱动读取board然后在根据boardid选择device_class_table进行合并。

命令行功能

命令行功能依赖控制台驱动,基于ANYCALL代码修改而来,具备原有anycall大部分功能,支持命令补全,命令动态注册,动态函数调用等功能。

内置命令注册

hikboot使用一个链表管理所有内置命令支持通过register_command进行命令注册下面是注册命令的两种方式。

第一种是手动定义command_t,在使用initcall机制注册。

1735291161328

第二种使用写好的宏定义进行COMMAND定义和函数注册:

1735291248779

常用命令

help

显示命令信息

1735291673531

显示单个命令信息

1735291733003

meminfo

显示内存信息

1735293035264

netinfo

显示网络信息

1735293068181

version

显示版本信息

1735291421172

ps

ps显示正在运行的进程信息

1735291512508

md mw

查看内存,也可用于硬件调试

1735292414543

1735292479822

mtd

1735292522703

ubi

1735292547582

loadx

串口文件加载到内存

psh

hikboot命令行默认支持PSH,通过非对称加密算法方式系统被篡改。

1735526407583

环境变量

printenv 打印环境变量

setenv 设置环境变量

saveenv 保存环境变量

1735526450452

文件操作

cat ls write cd 等文件操作命令

1735291473064

mount 文件系统挂载命令

函数动态调用功能

进行动态函数调用的关键在于获取函数指针。和initcall原理类似,在编译的时候hikboot会扫描文件中所有的EXPORT_SYMBOL(func)宏定义,然后将所有函数以及函数名称放到一个数组中:

1735291908076

使用方式:

命令行输入scall,然后直接调用对应函数。

1735292034511

网络功能

HIKBOOT移植了一套开源的网络协议栈,并将其接入了驱动框架,只需要实现一个网卡驱动就可以使用一些常见的网络功能。

PING功能

1735292614786

TFTP功能:

1735292229939

1735292347819

HTTP功能

目前HIKBOOT支持通过WEB端查看系统信息,并进行系统升级与UBOOT升级。

信息页面

1735292817874

升级页面

1735292682307

文件传输中

1735292702653

文件信息确认

1735292726767

烧写中

1735292747694

升级成功

1735292792340

BOOT功能

A/B分区启动模式

在完成主要驱动的适配后就需要进行一个平台的BOOT功能适配,其实BOOT内核的本质就是进行地址跳转,并按照架构约定向内核进行传参,架构之间一般传参方式不同,目前以及适配完成ARM平台和ARM64平台的BOOT功能。

下面是在使用NAND的情况下需要使用的分区模式:

1735295423473

启动时HIKBOOT会选择A/B分区之一启动,启动失败从另一个分区启动,从而保证系统可靠性。

BOOT流程

hikboot 具有签名验证包校验等基本功能,在启动时候会从默认介质读取digicap的指定长度,然后从中提取内核根文件系统设备树并进行完整性和签名验证,验证成功后拷贝到指定位置调用ARCH层BOOT接口进行内核启动。在启动之前可能会调用FDT接口对设备树进行一些修改,比如设置cmdline, 设置initrd地址长度,设置内存信息等。

hikboot 在没有发生打断启动流程的情况下会直接运行do_auto_boot函数,下面是这个函数的执行流程:
1735524436174

  1. 执行bootmenu , 在这个函数执行之前需要设置可选BOOT菜单项,默认有七种可选项。
  2. bootpart:选择一个分区读取digicap启动
  3. 从网络读取digicap启动
  4. 正常启动,启动默认方式。
  5. 正常启动,但是通过设置标志,阻止后续APP的启动。
  6. 升级方式,支持TFTP和HTTP传入升级文件,会检查升级文件合法性后烧入flash。
  7. 选择分区:会从环境变量中读取启动分区然后读取digicap启动。
  8. bootd:传入digicap包地址,完成启动内核所有准备工作后启动内核。
  9. 解压必要组件:解压过程涉及到对文件的完整性校验合法性校验,解压,重定位等操作。
  10. 设置设备树,在设备树中设置启动参数,INITRD信息等。
  11. 运行MACH层BOOT代码,跳转内核。

ARM平台BOOT底层代码:
1735293540524

ARM64平台BOOT底层代码

1735293574215

适配流程

那么拿到一个全新的平台需要怎么进行适配呢。

板级代码文件添加

在文件添加上需要添加对应芯片平台的BSP目录和一个配置文件

1735293811122

dtree存放设备树

board 存放bsp平台代码

hikboot.lds 链接脚本

hikboot.mk 编译参数添加

Kconfig 板端配置

在这个写好后,要解决这个平台的编译问题,需要可以编出hikboot.bin。

MACHINE代码适配

对一个新的平台需要实现对应的MACHINE相关代码,实质就是需要在启动过程中注册一个machine相关结构体。

1735294351498

其中machine结构体的实现非常关键。

detect接口会在注册machine时被运行,一般需要在这里读取boardid。

reboot接口会在reboot命令运行时执行,用作系统重启。

logger接口用作调试打印。可以在驱动初始化之前进行一些信息打印。

bogomips用作系统延时,在没有实现timer驱动的情况下可以作为临时延时作用。

串口驱动

在实现machine代码之后就是需要实现一个串口设备和控制台设备,方便使用命令行。

1735294676019

两个功能实现正常的话,就可以进入串口控制台。

1735294786595

时钟适配

使用bogomips进行延时是不可靠的,同时多进程进行CFS调度也需要一个可靠的时钟,所以一般需要实现一个时钟驱动。

或者也可以直接提供get_ticks和get_tbclk接口。

网卡适配

需要实现网卡的收发包接口。

存储驱动适配

需要实现板子上flash的读写,一般nor和nand需要实现mtd驱动。mmc需要实现sdhci总线驱动。

其中网卡适配和存储驱动适配难度较高,需要结合原厂提供的uboot代码,进行移植。

BOOT功能

boot前半段的代码一般时统一的,hikboot会从flash中将digcap读出,并将内核根文件系统设备树读到内存指定位置并进行完整性和签名验证,然后运行底层bootlinux接口,一般来说这部分代码大部分是在ARCH层是共用的。代码启动命令一般是bootd digicap_address,启动的时候一般来说就是运行这条命令。可能在bootlinux之前不同平台会做一下其他不同设置,必要时需要添加。

适配进度

平台 MACHINE代码适配 串口命令行适配 时钟适配 网卡适配 存储驱动适配 BOOT功能
MTK7986
MTK7981
VEXPRESS
RTL8197VG  ✓
RTL8197H  ✓
MTK7621  ✓  ✓  ✓
STM32
最后修改日期: 2025年1月3日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。