编程语言
首页 > 编程语言> > [转]QNX_startup程序分析内容讲解

[转]QNX_startup程序分析内容讲解

作者:互联网

如果你认为本系列文章对你有所帮助,请大家有钱的捧个钱场,点击此处赞助,赞助额0.1元起步,多少随意

声明:本文只用于个人学习交流,若不慎造成侵权,请及时联系我,立即予以改正

锋影

email:174176320@qq.com

 

 

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

1. startup介绍

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

2. startup程序结构

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

 

从图中可以看出,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如下:

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

/* 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,有以下步骤:

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,以输出调试或检测信息。

包括:

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

Clock/timer

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

Callout包括:

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

Interrupt controller

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

Callout包括:

Stubs包括:

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

Cache controller

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

内核Callout的原型如下:

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

System reset

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

Power management

每当需要激活电源管理时,会去调用power(),而这个Callout是特定于CPU的。通常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)

 

标签:callout,struct,hwi,QNX,Callout,startup,Item,内核,讲解
来源: https://blog.csdn.net/xjhhjx/article/details/90735918