QNX startup程序分析

QNX相关历史文章:

  • QNX简介
  • QNX Neutrino微内核
  • QNX IPC机制
  • QNX进程管理器
  • QNX资源管理器
  • QNX字符I/O
  • QNX之编写资源管理器(一)
  • QNX之编写资源管理器(二)
  • QNX之编写资源管理器(三)
  • QNX之编写资源管理器(四)
  • QNX之编写资源管理器(五)
  • QNX之编写资源管理器(六)
  • QNX之编写资源管理器(七)
  • QNX之编写资源管理器(八)
  • QNX之编写资源管理器(九)
  • QNX之编写资源管理器(十)
  • QNX BSP分析
  • QNX OS镜像
  • 编写IPL程序

这篇文章主要描述QNX的startup程序功能及组成,分析了system page结构,以及该结构中跟硬件相关性较大的hwinfo段与callout段。

1. startup介绍

在一个可启动的QNX镜像中,startup是第一个启动程序,startup程序的作用包括:

  • 初始化硬件
    完成基础的硬件初始化,具体需要做多少初始化工作,取决于IPL loader中做了多少。有时只需要做很少的初始化:比如MMU、定时器、中断控制器。
  • 初始化系统页system page
    关于系统的信息存放在一个叫做系统页(system page)的数据结构中,包括处理器类型、总线类型,可用的系统RAM位置和大小、硬件配置、缓存、内存映射和地址空间、定时器参数等,存放该结构的页面区域是内存的专用区域。内核和应用软件都可以以只读的形式来访问这些信息。
  • 初始化callout
    系统页system page结构中还包含了内核callouts字段,内核callout用于提供一些板级相关的代码,比如内核调试、系统定时器、缓存控制、中断处理、系统重启等,最终被QNX内核调用。这些代码都由startup程序来提供,由内核来负责回调进而操作硬件。这样的实现可以让QNX系统与硬件进行解耦合,具备更好的移植性。
  • 加载并将控制权转交给镜像中的下一个程序
    在这个阶段所做的工作包括初始化MMU,创建处理分页、进程和异常的结构,使能中断等,之后就可以跳转到内核运行了。

2. startup程序结构

代码位于{BSP_ROOT_DIR}/src/hardware/startup目录中,以R-Car为例:


QNX startup程序分析_第1张图片

从图中可以看出,boards目录下放置的rcar_gens,表明是瑞萨R-Car的第三代SoC,在该目录下的rcar_h3和rcar_m3分别对应两个不同的系列,黄色箭头所指的_start.S为程序的总体入口。

从_start.S进去,会涉及到ARM V8处理器的一系列初始化和设置,这部分是通用的,最终会调到main()函数,而这个main()函数,正是R-Car的startup的一部分。main()函数位于上图中的boards/rcar_gen3/目录下。

每个startup程序,都会包含一个main()函数,main()函数的伪代码如下:

Global variables
 
main()
{
    Call add_callout_array (note 1)
 
    Argument parsing (note 2)
 
    Call init_raminfo (note 3)
 
    Remove ram used by modules in the image
 
    if (virtual)
        Call init_mmu (note 4)
 
    Call init_intrinfo (note 5)
 
    Call init_qtime (note 6)
 
    Call init_cacheattr (note 7)
 
    Call init_cpuinfo (note 8)
 
    Set hardware machine name
 
    Call init_system_private (note 9)
 
    Call print_syspage (note 10)
}

上述代码中对应的注释note如下:

  • note 1
    将callout添加到system page结构中,上文中提到过callout本质上是回调函数,这个过程相当于把信息注册进系统页这个结构中;
  • note 2
    参数解析,对传入的ASCII字符进行处理,通过'switch case'来选择对应的项,操作包括:Reboot、输出通道的选择(kprintf或stdout)、CPU频率/时钟频率/定时器频率、Reserve内存、确定在SMP系统中CPU个数等;
  • note 3
    确定可用系统RAM的位置和大小,并在system page结构中初始化asinfo结构,如果已经知道了RAM的确切数量和位置,可以使用一个自定义的函数(在这个函数中可以使用add_ram来进行硬编码)来代替这个函数;
  • note 4
    设置MMU,通过设置页表,来完成物理地址到虚拟地址的转换;
  • note 5
    设置中断系统的相关信息,比如中断向量表相关;
  • note 6
    初始化system page中的qtime结构,qtime结构包含关于系统上的基准时间信息,以及其他与时间相关的信息;
  • note 7
    初始化缓存相关内容,包含片内和片外的缓存气筒,对所有平台,这部分都只是一个占位符,没有实现;
  • note 8
    初始化CPU相关信息,比如CPU类型、速度、功能、性能和缓存大小等;
  • note 9
    这个模块在所有平台上,都不需要修改,完成的工作包括:找到所有需要启动的引导镜像,并用这些信息填充结构;告诉内核镜像文件系统的位置;为system page结构分配实际的存储空间等;
  • note 10
    打印system page结构中的所有成员内容,其中全局变量debug_level用于确定输出的内容,debug_level至少为2才能打印任何内容,debug_level为3将打印子结构中的信息,system page对应的数据结构如下,这个结构中的字段,有些可通过startup库来初始化,有些则需要自己去实现代码来填充;

3. system page

从startup的代码中可以看出,在main函数中的所有处理,基本都是围绕这个system page结构来展开,完成相应段的初始化。最终在write_syspage_memory()之后,通知system page已经ready了,再调用startnext()进入下一个阶段的运行,也就是启动QNX内核。QNX内核读取这个内存区域来获取系统信息。

system page的结构,由不同的section组成,具体如下:

/*
 * contains at least the following:
 */
struct syspage_entry {
    uint16_t            size;
    uint16_t            total_size;
    uint16_t            type;
    uint16_t            num_cpu;
    syspage_entry_info  system_private;
    syspage_entry_info  asinfo;  /* address space information 结构数组,用于描述不同部分的内存映射,比如RAM、SRAM、Flash、I/O范围等,当procnto为进程地址空间管理虚拟地址时,会使用asinfo中的信息来获取可以从RAM中的何处分配内存。内存映射采用树状格式,地址范围可以有父节点,比如/memory/io/memclass/...,其中memclass可以是ram、rom、flash等 */
    syspage_entry_info  hwinfo;
    syspage_entry_info  cpuinfo;
    syspage_entry_info  cacheattr;  /* 关于片内和片外缓存系统配置的信息,该区域还包含了用于内核控制缓存操作的Callout,cacheattr结构由init_cpuinfo()和init_cacheattr()来填充,cacheattr条目组织在一个链表中,结构体中的next成员表示下一级缓存条目的索引 */
    syspage_entry_info  qtime;  /* 关于系统上显示时间的基准信息,以及其他与时间相关的信息 */
    syspage_entry_info  callout;
    syspage_entry_info  callin;
    syspage_entry_info  typed_strings;
    syspage_entry_info  strings;
    syspage_entry_info  intrinfo;   /* 中断系统信息,还包含了用于操作中断控制器硬件的内核Callout */
    syspage_entry_info  smp;
    syspage_entry_info  pminfo;
     
    union {
        struct x86_syspage_entry    x86;
        struct ppc_syspage_entry    ppc;
        struct mips_syspage_entry   mips;
        struct arm_syspage_entry    arm;
        struct sh_syspage_entry     sh;
    } un;
};

4. hwinfo

注意到上文中提到system page结构中,有一个syspage_entry_info hwinfo成员,这个结构包含了硬件平台的信息,包括总线类型、设备、中断等。

hwinfo段不是由一个单独结构或相同类型的数组组成,而是由一些标签化的结构组成,这些结构作为一个整体来描述电路板上的硬件。在hwinfo段中,有两个概念,一个是Tag,一个是Item。

Tag:
Tag结构用于描述硬件组件的特定方面的信息,Tag都是以下边这个结构开头

struct hwi_prefix {
    uint16_t        size;
    uint16_t        name;
};

目前提供了几个预定义的Tag,如下所示:

/* 它给出了寄存器的位置(不管是在I/O空间还是在内存空间),如果有多个寄存器组,则Item中可能会有多个这样的Tag */
#define HWI_TAG_NAME_location   "location"
#define HWI_TAG_ALIGN_location  (sizeof(uint64))
struct hwi_location {
    struct hwi_prefix   prefix;
    uint32_t            len;      /* 寄存器范围的长度 */
    uint64_t            base;     /* 寄存器的物理基地址 */
    uint16_t            regshift; /* Indicates the shift for each register access. */
    uint16_t            addrspace; /* 从asinfo部分开始的偏移量,以字节为单位,这个成员用于标识寄存器是内存映射还是在单独的IO地址空间 */
};
 
/* 给出了设备的中断号 */
#define HWI_TAG_NAME_irq        "irq"
#define HWI_TAG_ALIGN_irq       (sizeof(uint32))
struct hwi_irq {
    struct hwi_prefix   prefix;
    uint32_t            vector;   /* 逻辑向量中断号 */
};
 
/* 这个Tag,用于填充,保证字节能对齐 */
#define HWI_TAG_NAME_pad        "pad"
#define HWI_TAG_ALIGN_pad       (sizeof(uint32))
struct hwi_pad {
    struct hwi_prefix   prefix;
};

Item:
Item是Tag的集合,用于描述一个硬件组件的完整信息。每个Item中的第一个Tag都是以struct hwi_item开始,结构如下所示:

struct hwi_item {
    struct hwi_prefix   prefix;
    uint16_t            itemsize;    /* 到下一项Item开始的距离,以4字节为单位 */
    uint16_t            itemname;    /* 这个字段是整型,存放的是system page中strings段中对应的偏移量,用于描述Item的名称 */
    uint16_t            owner;       /* 这个字段用于将Item组织成树状结构,类似于文件系统的层次结构,存放的值是hwinfo段中对应的偏移量 */
    uint16_t            kids;        /* 当前项的子项数目 */
};

通过将Item组织,可以在hwinfo中构造一个设备树,比如通常设备层次结构为:/hw/bus/devclass/device

  • hw,硬件树的根;
  • bus,硬件所在的总线,比如pci、eisa等,对应Bus Item;
  • devclass,设备的分类,比如serial、rtc等,对应Group Item;
  • device,实际的设备,比如8250、mc146818等,对应Device Item;
    目前提供了部分的预定义的Item,如下:
/* Group Item, Item组,可以将多个Item组织在一起,它与文件系统中的目录具有相同的用途,比如/hw树中的devclass层就使用Group Item  */
#define HWI_TAG_NAME_group  "Group"
#define HWI_TAG_ALIGN_group (sizeof(uint32_t))
struct hwi_group {
    struct hwi_item     item;
};
 
/* Bus Item, 总线Item用于告诉系统硬件总线的信息 */
#define HWI_TAG_NAME_bus    "Bus"
#define HWI_TAG_ALIGN_bus   (sizeof(uint32))
struct hwi_bus {
    struct hwi_item     item;
};
/* Bus Item的名字可以是(不限于) */
#define HWI_ITEM_BUS_PCI        "pci"
#define HWI_ITEM_BUS_ISA        "isa"
#define HWI_ITEM_BUS_EISA       "eisa"
#define HWI_ITEM_BUS_MCA        "mca"
#define HWI_ITEM_BUS_PCMCIA     "pcmcia"
#define HWI_ITEM_BUS_UNKNOWN    "unknown"
 
/* Device Item, 设备Item用于告诉系统单个设备的信息 */
#define HWI_TAG_NAME_device     "Device"
#define HWI_TAG_ALIGN_device    (sizeof(uint32))
struct hwi_device {
    struct hwi_item     item;
    uint32_t            pnpid;    /* 微软分配的即插即用设备标识符,只适用于播放媒体的设备,已经弃用 */
};

上述的Item和Tag只是预定义的,用户可以创建自己需要的Item。

构建hwinfo段的接口:
针对hwinfo的Tag和Item,提供了一下相关的操作接口,如下:

/* 分配一个Tag */
void *hwi_alloc_tag(const char *name, unsigned size, unsigned align);
 
/* 分配一个Item */
void *hwi_alloc_item(const char *name, unsigned size,
                     unsigned align, const char *itemname,
                     unsigned owner);
 
/* 查到Item中的信息 */
unsigned hwi_find_item(unsigned start, ...);
 
/* 根据Tag的指针,得到在hwinfo段中的offset */
unsigned hwi_tag2off(void *);
 
/* 根据offset, 来得到Tag */
unsigned hwi_tag2off(void *);
 
/* 根据tagname,得到Tag */
unsigned hwi_find_tag(unsigned start, int curr_item, const char *tagname);
 
/* 获取给定偏移量对应的Item的下一个Item在hwinfo段中的偏移量 */
unsigned hwi_next_item( unsigned off);
 
/* 获取给定偏移量对应的Tag的下一个Tag在hwinfo段中的偏移量 */
unsigned hwi_next_tag( unsigned off, int curr_item );

要构建一个Item,有以下步骤:

  • 调用hwi_alloc_item()接口来构建一个顶层的Item,它的owner字段被设置成HWI_NULL_OFF;
  • 调用hwi_alloc_tag()接口来添加任何想要的Tag结构;
  • 调用hwi_alloc_item()来创建一个新的Item,这个Item可以是刚创建Item的第一个子项,也可以是另一个顶层的Item。

5. 内核Callout

什么是Callout?先来看几个数据结构:

struct callout_rtn {
    unsigned    *rw_size;
    void        (*patcher)(PADDR_T paddr, uintptr_t vaddr, unsigned rtn_offset, unsigned rw_offset, void *data, const struct callout_rtn *src);
    unsigned    rtn_size;
    uint8_t        rtn_code[1];
};
 
struct callout_slot {
    unsigned                    offset;
    const struct callout_rtn    *callout;
};

Callout是独立的代码片段,可以认为是一些由startup来提供的回调函数,在QNX内核中绑定调用,用于执行特定于硬件的操作。不需要静态地将这些代码链接到QNX内核中,这样做也就能将QNX内核与硬件相关的操作分离。

Callout例程通常以汇编的形式给出,Callout例程作为startup程序的一部分,在内核启动时它将被覆盖,为了避免这种情况,startup程序会将这些Callouts例程从加载的位置拷贝到一个安全的位置,所以Callouts的代码需要是位置无关(position-indepentent)的。

内核使用SoC的Application Binary Interface(ABI)来向Callout传递数据或者从Callout获取数据。当尝试为开发板编写Callout时,首先需要去熟悉板子的ABI接口文档。

5.1 内核Callout类别

startup库为内核提供了内核Callout,用于处理不同类别的任务,可以使用这些Callout当做模板来编写自己的Callout。

Kernel Debug

内核在需要打印一些内部调试信息或遇到错误时,需要用到Debug Callout,以输出调试或检测信息。

包括:

  • display_char(),从内核接收到字符,并将它输出给UART或其他设备;
  • poll_key(),传递一个字符给内核,如果字符不可用,则返回-1;
  • break_detect(),检测是否有中断;

当内核希望与串口、控制台或其他设备交互时,比如打印一些内部调试信息,会调用这些接口,其中poll_key()和break_detect()是可选的。

Clock/timer

内核使用这部分的Callout与硬件定时器交互(定时器/计数器芯片),在很多情况下,一个开发板上可能有多个计时器,可以在启动代码中选定一个Callout来使用。内核使用硬件定时器来产生周期性中断,用于软件定时器、调度、更新系统时间或其他软件时间等。

Callout包括:

  • timer_load(),负责将内核传递的值填充到硬件中,Callout会将写入硬件计时器的值写入到qtime_entry中的timer_load字段,这样内核就可以看到实际使用了哪个值;
  • timer_reload(),内核在中断开始时调用timer_reload(),如果timer_reload()返回1,则将中断视为时钟tick,当有多个中断源可以产生相同的中断时,timer_reload()返回值可以用于将时钟tick中断源与其他中断源区分开来;
  • timer_value(),返回定时器芯片内部的计数值,内核可以调用timer_value()获取下一个中断到来的时间;

内核使用这些Callout来与硬件定时器芯片交互。

Interrupt controller

中断控制器接口包括内核Callout和Stubs两部分

Callout包括:

  • mask(),mask某个中断向量;
  • unmask(),unmask某个中断向量;
  • config(),发现指定中断级别的配置;

Stubs包括:

  • interrupt_id_*(),负责将中断级别配置进CPU寄存器中,并Mask处理。Mask在处理边缘触发中断的情况下是必要的,可以防止在完成响应之前再次中断。
    需要做的工作包括:1)从某种中断状态寄存器中读取信息;2)执行一些位操作来确定中断级别;3)将中断级别值写入通用的CPU寄存器中,以便内核使用;4)在出现故障或错误断言的情况下,在GPR中写入-1,表明没有中断内核;5)操作enable和mask寄存器来屏蔽中断;

  • interrupt_eoi_*(),End of Interrupt(EOI)
    需要做的工作包括:1)告诉中断控制器,中断已经被处理;2)打开中断级别的掩码,解除屏蔽;3)在某些情况下,内核Callout会操作寄存器中的其他位,以提示中断控制器重新计算它接收到的输入。

stubs这部分代码直接集成到了内核代码中,它们的调用方式与其他的Callout调用方式不一样,不能从这两个Callout中间返回,必须执行到最后。

Cache controller

根据系统中的缓存控制器电路,可能也需要为内核提供与缓存控制器相关的Callout,用于在内核中执行某些特定功能时使部分缓存失效。

内核Callout的原型如下:

  • control(),需要传给这个Callout一些Flags标记、地址(虚拟地址或物理地址,取决于system page结构中cacheattr数组中的Flag值),需要影响的cache line数量;
    这个接口返回所影响的cache lines,返回0表明所有的cache都被invalidate了。在有的处理器架构中,缓存控制器与CPU紧密耦合,这也意味着内核不必与缓存控制器通信。

一般不太可能需要自己实现这个Callout,大多数情况下startup标准库中都提供了接口,能正常使用。

System reset

每当内核需要重新启动机器是,会调用Callout中的reboot()接口,这个可以让开发人员做一些定制化操作,比如在某些事情发生时进行重启操作。sysmgr_reboot()最终会调用到reboot()。

Power management

每当需要激活电源管理时,会去调用power(),而这个Callout是特定于CPU的。通常CPU的电源模式有以下几种:

  • Active or Running,系统正在运行应用程序,一些外围设备可能处于空闲或关闭状态;
  • Idle,系统没有运行应用程序,CPU停止,代码全部或部分驻留在内存中;
  • Standby,系统没有运行应用程序,CPU停止,代码没有驻留在内存中;
  • Shutdown,最小或零功率状态,CPU、内存和设备都关闭了电源;

5.2 Callout编写

如果startup库中的内核Callout不支持目标硬件平台,或者任何可用的特定于硬件的内核Callout也不支持目标硬件平台,那就需要自己去实现Callout了。

Callout都以汇编的形式给出来,文件的命名约定为callout_category_device.S,其中category有:cache、debug、interrupt、timer、reboot等几种,device指的是特定的设备,比如在R-Car中使用了串口,命名为callout_debug_scif.S。

在编写Callout之前,需要先查看硬件文档,以便了解内核Callout需要做什么,才能在目标硬件上完成它的任务。一般可以拷贝功能相近的Callout文件,然后在它的基础上进行修改。

编写内核Callout有几点注意的:

  • 开始和结束宏
/* 包含头文件 */
#include "callout.ah"
/* 或者如下 */
//.include "callout.ah"
 
CALLOUT_START(timer_load_8254, 0, 0)
CALLOUT_END(timer_load_8254)

CALLOUT_START宏,表示Callout的起始,有三个参数,分别代表例程名字、四字节变量的地址(该地址包含了Callout需要的读写存储量)、patcher例程的地址(0表示不需要patching)

CALLOUT_END宏,表示Callout的结束,参数与CALLOUT_START宏中的第一个参数一样。

当这个Callout被内核选中的话,CALLOUT_START和CALLOUT_END之间的代码会被拷贝到一个安全的内存区域,方便内核使用。

  • patcher例程
    如果为其编写内核Callout的设备可以出现在不同的开发板中的不同位置,则需要一个patch例程来将寄存器地址添加到内核Callout代码中。

内核Callout是startup库中的一部分,因此设计的很灵活,不会硬编码寄存器地址,而是假设寄存器地址是通过patch进来的,寄存器地址都是来自板级代码中,在板级目录的代码中可以找到。如果内核Callout只访问CPU寄存器,则不需要这个patch操作。

patcher例程的函数原型如下:

/*
 * paddr, system page开始的物理地址
 * vaddr,允许读写访问system page的虚拟地址(仅供内核使用)
 * rtn_offset,从system page开始到内核Callout代码开始的偏移量
 * rw_offset,从system page开始到读写位置的偏移量,可以由所有在CALLOUT_START宏的第二个参数中具有相同值的内核Callout共享
 * data,指向callout_register_data()注册的任意数据的指针
 * src,指向callout_rtn结构的指针,被复制到适当的位置
 */
void patcher( paddr_t paddr,
    paddr_t vaddr,
    unsigned rtn_offset,
    unsigned rw_offset,
    void *data,
    struct callout_rtn *src );

这个例程会在内核Callout被拷贝到最终位置时立马被调用。patcher例程不必使用汇编实现,但通常都是通过汇编实现,因此可以将其保存在它patching的源文件中,与CALLOUT_START/CALLOUT_END组织的代码放在一块。

  • 分配读写空间
    在某些情况下,内核Callout需要访问一些静态读写存储,特别是为了能够与其他内核Callout共享信息时。由于内核Callout代码是位置无关的,因此它不能有静态读写存储,可以在system page的末尾将少量的内存分配给内核Callout作为读写存储。使用CALLOUT_START宏的第二个参数来指定一个四字节变量的地址,该变量包含内核Callout所需的读写存储量。

Callout 示例
Callout的编写如下,以R-Car的callout_debug_scif.S为例:

/*
 * Patch interrupt callouts to access rw data.
 * The first call will also map the uart.
 *
 * Patcher routine takes the following arguments:
 *    x0 - syspage paddr
 *    x1 - vaddr of callout
 *    x2 - offset from start of syspage to start of callout routine
 *    x3 - offset from start of syspage to rw storage
 *    x4 - patch data
 *    x5 - callout_rtn
 */
 
patch_debug:
    sub        sp, sp, #16
    stp        x19, x30, [sp]
 
    add        x19, x0, x2                // x19 = address of callout routine
 
    /*
     * Map UART using patch_data parameter
     */
    mov        x0, #0x1000
    ldr        x1, [x4]
    bl        callout_io_map
 
    /*
     * Patch callout with mapped virtual address in x0
     */
    CALLOUT_PATCH    x19, w6, w7
 
    ldp        x19, x30, [sp]
    add        sp, sp, #16
    ret
 
/*
 * -----------------------------------------------------------------------
 * void    display_char_scif(struct sypage_entry *, char)
 *
 * x0: syspage pointer
 * x1: character
 * -----------------------------------------------------------------------
 */
CALLOUT_START(display_char_scif, 0, patch_debug)
    mov        x7, #0xabcd                // UART base address (patched)
    movk    x7, #0xabcd, lsl #16
    movk    x7, #0xabcd, lsl #32
    movk    x7, #0xabcd, lsl #48
 
0:    ldr        w2, [x7, #SCIF_SCFSR_OFF]
    tst        w2, #SCIF_SCSSR_TDFE
    b.eq    0b
 
    and        w0, w1, #0xff
    strb    w0, [x7, #SCIF_SCFTDR_OFF]
 
1:    ldr        w2, [x7, #SCIF_SCFSR_OFF]
    tst        w2, #SCIF_SCSSR_TEND
    b.eq    1b
 
    mov        w2, #0
    strh    w2, [x7, #SCIF_SCFSR_OFF]
 
    ret
CALLOUT_END(display_char_scif)

你可能感兴趣的:(QNX startup程序分析)