QNX相关历史文章:
这篇文章主要描述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为例:
从图中可以看出,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)