QNX_startup程序分析内容讲解

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

锋影

email:174176320@qq.com

 

 

这篇文章主要描述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()函数的伪代码如下:


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

上述代码中对应的注释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组成,具体如下:


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

4. hwinfo

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

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

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


 
 
  1. struct hwi_prefix {
  2. uint16_t size;
  3. uint16_t name;
  4. };

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


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

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


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

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

  • hw,硬件树的根;
  • bus,硬件所在的总线,比如pci、eisa等,对应Bus Item;
  • devclass,设备的分类,比如serial、rtc等,对应Group Item;
  • device,实际的设备,比如8250、mc146818等,对应Device Item;
    目前提供了部分的预定义的Item,如下:

 
 
  1. /* Group Item, Item组,可以将多个Item组织在一起,它与文件系统中的目录具有相同的用途,比如/hw树中的devclass层就使用Group Item */
  2. #define HWI_TAG_NAME_group "Group"
  3. #define HWI_TAG_ALIGN_group (sizeof(uint32_t))
  4. struct hwi_group {
  5. struct hwi_item item;
  6. };
  7. /* Bus Item, 总线Item用于告诉系统硬件总线的信息 */
  8. #define HWI_TAG_NAME_bus "Bus"
  9. #define HWI_TAG_ALIGN_bus (sizeof(uint32))
  10. struct hwi_bus {
  11. struct hwi_item item;
  12. };
  13. /* Bus Item的名字可以是(不限于) */
  14. #define HWI_ITEM_BUS_PCI "pci"
  15. #define HWI_ITEM_BUS_ISA "isa"
  16. #define HWI_ITEM_BUS_EISA "eisa"
  17. #define HWI_ITEM_BUS_MCA "mca"
  18. #define HWI_ITEM_BUS_PCMCIA "pcmcia"
  19. #define HWI_ITEM_BUS_UNKNOWN "unknown"
  20. /* Device Item, 设备Item用于告诉系统单个设备的信息 */
  21. #define HWI_TAG_NAME_device "Device"
  22. #define HWI_TAG_ALIGN_device (sizeof(uint32))
  23. struct hwi_device {
  24. struct hwi_item item;
  25. uint32_t pnpid; /* 微软分配的即插即用设备标识符,只适用于播放媒体的设备,已经弃用 */
  26. };

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

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


 
 
  1. /* 分配一个Tag */
  2. void *hwi_alloc_tag(const char *name, unsigned size, unsigned align);
  3. /* 分配一个Item */
  4. void *hwi_alloc_item(const char *name, unsigned size,
  5. unsigned align, const char *itemname,
  6. unsigned owner);
  7. /* 查到Item中的信息 */
  8. unsigned hwi_find_item(unsigned start, ...);
  9. /* 根据Tag的指针,得到在hwinfo段中的offset */
  10. unsigned hwi_tag2off(void *);
  11. /* 根据offset, 来得到Tag */
  12. unsigned hwi_tag2off(void *);
  13. /* 根据tagname,得到Tag */
  14. unsigned hwi_find_tag(unsigned start, int curr_item, const char *tagname);
  15. /* 获取给定偏移量对应的Item的下一个Item在hwinfo段中的偏移量 */
  16. unsigned hwi_next_item( unsigned off);
  17. /* 获取给定偏移量对应的Tag的下一个Tag在hwinfo段中的偏移量 */
  18. 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?先来看几个数据结构:


 
 
  1. struct callout_rtn {
  2. unsigned *rw_size;
  3. void (*patcher)(PADDR_T paddr, uintptr_t vaddr, unsigned rtn_offset, unsigned rw_offset, void *data, const struct callout_rtn *src);
  4. unsigned rtn_size;
  5. uint8_t rtn_code[ 1];
  6. };
  7. struct callout_slot {
  8. unsigned offset;
  9. const struct callout_rtn *callout;
  10. };

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有几点注意的:

  • 开始和结束宏

 
 
  1. /* 包含头文件 */
  2. #include "callout.ah"
  3. /* 或者如下 */
  4. //.include "callout.ah"
  5. CALLOUT_START(timer_load_8254, 0, 0)
  6. 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例程的函数原型如下:


 
 
  1. /*
  2. * paddr, system page开始的物理地址
  3. * vaddr,允许读写访问system page的虚拟地址(仅供内核使用)
  4. * rtn_offset,从system page开始到内核Callout代码开始的偏移量
  5. * rw_offset,从system page开始到读写位置的偏移量,可以由所有在CALLOUT_START宏的第二个参数中具有相同值的内核Callout共享
  6. * data,指向callout_register_data()注册的任意数据的指针
  7. * src,指向callout_rtn结构的指针,被复制到适当的位置
  8. */
  9. void patcher( paddr_t paddr,
  10. paddr_t vaddr,
  11. unsigned rtn_offset,
  12. unsigned rw_offset,
  13. void *data,
  14. 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为例:


 
 
  1. /*
  2. * Patch interrupt callouts to access rw data.
  3. * The first call will also map the uart.
  4. *
  5. * Patcher routine takes the following arguments:
  6. * x0 - syspage paddr
  7. * x1 - vaddr of callout
  8. * x2 - offset from start of syspage to start of callout routine
  9. * x3 - offset from start of syspage to rw storage
  10. * x4 - patch data
  11. * x5 - callout_rtn
  12. */
  13. patch_debug:
  14. sub sp, sp, #16
  15. stp x19, x30, [sp]
  16. add x19, x0, x2 // x19 = address of callout routine
  17. /*
  18. * Map UART using patch_data parameter
  19. */
  20. mov x0, #0x1000
  21. ldr x1, [x4]
  22. bl callout_io_map
  23. /*
  24. * Patch callout with mapped virtual address in x0
  25. */
  26. CALLOUT_PATCH x19, w6, w7
  27. ldp x19, x30, [sp]
  28. add sp, sp, #16
  29. ret
  30. /*
  31. * -----------------------------------------------------------------------
  32. * void display_char_scif(struct sypage_entry *, char)
  33. *
  34. * x0: syspage pointer
  35. * x1: character
  36. * -----------------------------------------------------------------------
  37. */
  38. CALLOUT_START(display_char_scif, 0, patch_debug)
  39. mov x7, #0xabcd // UART base address (patched)
  40. movk x7, #0xabcd, lsl #16
  41. movk x7, #0xabcd, lsl #32
  42. movk x7, #0xabcd, lsl #48
  43. 0: ldr w2, [x7, #SCIF_SCFSR_OFF]
  44. tst w2, #SCIF_SCSSR_TDFE
  45. b.eq 0b
  46. and w0, w1, #0xff
  47. strb w0, [x7, #SCIF_SCFTDR_OFF]
  48. 1: ldr w2, [x7, #SCIF_SCFSR_OFF]
  49. tst w2, #SCIF_SCSSR_TEND
  50. b.eq 1b
  51. mov w2, #0
  52. strh w2, [x7, #SCIF_SCFSR_OFF]
  53. ret
  54. CALLOUT_END(display_char_scif)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值