向一个新的 ARM 平台移植 Xenomai
目的
本文档是 Gilles Chanteperdrix 为 Xenomai 项目撰写的文章1的一个延伸,详细介绍了将中断流水线引入 ARM 内核所带来的变化。
本文致力于引导读者了解如何移植 I-pipe 核心到一个新的 ARM SoC,从而实现一个实时的、双内核的系统。
术语
如果你正在阅读本文档,那么你将有几乎将 I-pipe 运行在一个基于 ARM 的开发板上。开发板的一些例子是:beagleboard,beaglebone,raspberry pi。
开发板是使用了基于 ARM 的 SoC 构建的。一些 SoC 的例子是:Atmel AT91RM9200,Atmel AT91SAM9263,TI OMAP3530,TI OMAP4430,Freescale IMX53。我们使用 SoC 家族粗略地指定一组 SoC,它们具有许多相似的外设,从而这些外设的驱动程序可以共享。举个例子,对于“AT91”家族,其中包括 SoC AT91RM9200 和 AT91SAM9263 等等。
SoC 的核心则是处理器核,它们实现了 ARM 指令集。处理器核的例子有 ARM 926EJ-S,Intel/Marvell Xscale,Marvell Feroceon,ARM Cortex A8,ARM Cortex A9。
最后,处理器核实现了一种 ARM 架构,或者说 ARM 指令集的某个版本。ARM 架构包括 armv4,armv5,armv6 以及 armv7。
举个例子来说,IGEPv2 开发板 使用了 TI OMAP3530 SoC,属于 OMAP SoC 家族,基于 ARM Cortex A8 处理器核,实现了 armv7 架构。
自 4.14 版本内核开始,I-pipe 已经不再支持 armv4 和 armv5 架构,只有armv6 仍然支持。
定位需要移植的 ARM 架构代码
在一切开始之前,你应该确定开发板所使用的 SoC,处理器核以及架构,然后定位到对应的 SoC 以及开发板的特定代码上。
为了得到这些信息,你可以使用 Linux 内核源码中存在于各个子目录中的 Kconfig 与 Makefile 文件。Linux 源码通过将目录命名为 arch/arm/mach-X 或者 arch/arm/plat-X 来指定基于 ARM 的 SoC 或者 SoC 家族 X。还有一些代码可能位于 drivers/ 目录中,特别是 drivers/clocksource,drivers/gpio 或者 drivers/irqchip。
一些由 I-pipe 核心所管理的设备(硬件计时器,高精度计数器,中断控制器,GPIO 控制器)也许对于每一个 SoC 都是不同的,因此为了运行 I-pipe,需要进行适配。
如果处理器核为 ARM CortexA9,那么事情将会变得稍微简单一点。这是因为其核内集成了中断控制器,硬件计时器和高精度计数器,而这些设备的驱动程序已经被移植到I-pipe 中了。
硬件计时器
I-pipe 的“客户端”(如协同内核)需要一个可编程的硬件计时器,以
one-shot 模式进行计时。I-pipe 核心中使用 struct ipipe_timer
对其进行抽象。
对于大多数 ARM SoC,硬件计时器的细节对于每一个 SoC 或者 SoC
家族是特定的,因此在每一个 SoC 的基础上都必须要添加
ipipe_timer
描述符。有很多方法都可以在 I-pipe
核心中实现这个计时器描述符。
A9 计时器
如果你所使用的 SoC 不是基于 ARM Cortex A9 核的,请跳到下一节。对于搭载了 ARM Cortex A9 核的
SoC,硬件计时器是由处理器核提供的,与 SoC
之间关联不大:带来的好处是计时器这部分代码已经在 I-pipe
核心代码中得到移植,并支持了 struct ipipe_timer
描述符(详见 arch/arm/kernel/smp_twd.c)。需要注意的是,在为你的 SoC
编译内核时,应保证将 ARM Cortex A9 的硬件计时器代码编译进内核中。
为此,你应该确保 smp_twd
计时器已注册,它声明了一个时钟源,并使用了包含 twd-timer
的字符串。
如果 SoC 没有使用 smp_twd
计时器,并且也没有内核配置选项可以选择它,那么你就需要使用下一节的方法注册 per-cpu 计时器。
在某些情况下,Cortex A9 计数器的 Linux 支持代码可能会在 I-pipe更新补丁打上后给出不准确的计时器频率校准结果,这种结果将导致计时器中断提前触发。通过提供适当的设备树,驱动器可以自动确定适当的时钟频率,而无需进行任何不精确的校准。
非 A9 计时器
你需要看一看你使用的 SoC 的硬件计时器支持代码。通常情况下,可以在
drivers/clocksource 或 arch/arm/mach-X/time.c 或 arch/arm/plat-Y/time.c
中找到它们。假设你的开发板使用设备树文件,你应该查找包含
-timer
的设备,并尝试在上述位置之一找到相应的文件。
假设硬件计时器是由 clock_event_device
驱动的,并且支持
one-shot 模式(clock_event_device 的 features
域包含
CLOCK_EVT_FEAT_ONESHOT
),那么你的工作将会变得简单。否则,你需要找到包含硬件计时器寄存器说明的文档,看看它是什么类型的计时器(递减器或带匹配寄存器的自由运行计数器),以及如何工作在
one-shot 模式下。
最后你需要决定是协同内核与 Linux 是共享同一个硬件计时器还是使用不同的计时器(一些 SoC 拥有好几个可用的硬件计时器)。推荐使用相同的计时器。
ipipe_timer
结构在某种程度上继承了
clock_event_device
结构,增加了协同内核通过中断管道(I-pipe)从计时器硬件接收高精度事件所需的一组功能。下列成员被定义在文件
include/linux/ipipe_tickdev.h 中:
int irq
这是计时器中断所使用的 IRQ 号。
void (*request)(struct ipipe_timer *timer, int steal)
该回调函数将在协同内核开始使用硬件计时器时被 I-pipe 核心唤醒。它需要将硬件计时器设置为 one-shot 模式。当参数
steal
的值为true
时,意味着协同内核获得了 Linux 内核正在使用的计时器的控制权。如果硬件计时器的 Linux 支持代码使用了
clock_event_device
结构,支持 one-shot 模式,并且 I-pipe 核心与 Linux 使用同一个计数器,那么这个处理函数可以被移除。在这种情况下,I-pipe 核心将调用对应clock_event_device
结构中默认的set_mode
处理函数。int (*set)(unsigned long ticks, void *timer)
在协同内核请求为硬件计时器写入下一个时钟事件时,该处理函数会被调用。它将控制硬件计时器的流逝以
ticks
为一个单位。举个例子,如果硬件计时器是基于自减器的,那么这个处理函数就应该将自减寄存器设置为
ticks
值。如果硬件计时器是基于一个自由运行的计数器和一个匹配寄存器,这个处理函数则需要将匹配寄存器设置为当前计数器与
ticks
值之和。如果函数运行成功,将返回0;否则,如果延时过短,则将返回一个负值(对于自由运行的计数器和匹配寄存器而言,这种情况可以通过以下方法判断:在设置完匹配寄存器后重新读取计数器的值,如果它超过了匹配寄存器的值,则说明延时过短,导致这个函数运行失败)。
同样地,如果硬件计时器的 Linux 支持代码使用了
clock_event_device
结构,支持 one-shot 模式,并且 I-pipe 核心与 Linux 使用同一个计数器,那么这个处理函数可以被移除。在这种情况下,I-pipe 核心将调用对应clock_event_device
结构中默认的set_next_event
处理函数。
必须注意的是,这个处理函数是在协同内核上下文中被调用的,因此它不会调用任何常规的Linux服务,也不会持有任何常规自旋锁。另外,一个独立的处理函数必须被实现(或者如果需要持有自旋锁,则原本的自旋锁需要被转化为
void (*ack)(void)
这个处理函数在计时器中断时被调用,它应该在硬件计时器级别确认计时器中断。提供这样的函数几乎总是必要的。
如果这个硬件计时器是与 Liunx 共享的,那么这部分代码已经在 Linux 计时器中断中实现了。这些代码应该被修改为当这个计时器不被协同内核控制时,仅确认计时器中断。参考例子以避免重复确认。
void (*release)(struct ipipe_timer *timer)
这个处理函数在协同内核释放硬件计时器时被 I-pipe 核心调用。它的作用是将计时器恢复到调用
request
时的状态。举个例子,如果计时器运行在 periodic 模式下,然后request
将其切换到了 one-shot 模式,那么这个处理函数就会将其切换回 periodic 模式。同样地,如果硬件计时器的 Linux 支持代码使用了
clock_event_device
结构,支持 one-shot 模式,并且 I-pipe 核心与 Linux 使用同一个计数器,那么这个处理函数可以被移除。在这种情况下,I-pipe 核心将调用对应clock_event_device
结构中默认的set_mode
处理函数。const char *name
计时器的名称。
如果 I-pipe 核心与 Linux 使用同一个计数器,那么这个设置可以被移除,这种情况下将使用
clock_event_device
描述符中该计时器所使用的名称。unsigned int rating
计时器的级别。如果支持的多个硬件计时器有不同级别,那么协同内核将使用级别最高的那一个。
如果 I-pipe 核心与 Linux 使用同一个计数器,那么这个设置可以被移除,这种情况下将使用
clock_event_device
描述符中该计时器所使用的级别。unsigned long freq
硬件计时器的频率。通常这个值可以通过时钟框架的
clk_get_rate()
函数获取。如果 I-pipe 核心与 Linux 使用同一个计数器,那么这个设置可以被移除,这种情况下将使用
clock_event_device
描述符中该计时器所使用的频率。unsigned int min_delay_ticks
以 ticks 计的硬件时钟最小延迟。几乎对于所有基于计数器和匹配寄存器的计时器而言,都有一个阈值,低于该值时将不能编程。当你使用一个过短的值编入这类计时器,其内部的计数器将持续增加,直到溢出后才能与匹配寄存器相匹配,浙江导致整个计时器停止很长一段时间,然后突然重启。
如果这个最小延迟被称作墙时钟延迟而不是硬件滴答数,函数
ipipe_timer_ns2ticks()
可以用来进行转换,前提是ipipe_timer.freq
已经设置好。如果 I-pipe 核心与 Linux 使用同一个计数器,那么这个设置可以被移除,这种情况下将使用
clock_event_device
描述符中该计时器所使用的延迟。const struct cpumask *cpumask
一个 CPU 掩码包括了这个计时器将要运行的一组 CPU。在 SMP 系统中,将存在许多
ipipe_timer
结构体,每一个对应 CPU 掩码中的一个成员。如果 I-pipe 核心与 Linux 使用同一个计数器,那么这个设置可以被移除,这种情况下将使用
clock_event_device
描述符中该计时器所使用的掩码。
一旦这个结构被声明,有两种方法向 I-pipe 核心注册:
如果硬件计时器的 Linux 支持代码使用了
clock_event_device
结构,并且 I-pipe 核心与 Linux 使用同一个计数器,那么其成员ipipe_timer
应该指向此结构,相当于在常规内核调用clockevents_register_device()
时自动地进行了注册。否则,需要手动调用
ipipe_timer_register()
进行注册。
例子
作为一个例子,我们看一看 I-pipe 核心中 OMAP3 的代码。在引入 I-pipe 前,代码如下:
1 |
|
对于函数 __omap_dm_timer_write_status()
的调用确认了硬件计时器中断的级别。
1 |
|
上面的代码展示了 Linux 中对于硬件计时器处理 one-shot
模式的支持代码。进一步观察表明
omap2_gp_timer_set_next_event()
没有调用任何 Linux 服务,而
Linux 服务是无法在 out-of-bound
上下文中被调用的。因此,这些代码可以安全地与协同内核共享。通过以下修改来支持
I-pipe 内核:
1 |
|
高精度计数器
由于协同内核的计时器管理基于一个运行在 one-shot 模式的计时器,为了应用能够测量较短的时间间隔,需要使用一个高精度计数器。
同样地,计数器的使用也与 SoC 有很大的关联。为了纪念第一个在 x86 处理器上使用 I-pipe 技术运行的 Xenomai 协同内核,这个高精度计数器被称为 tsc(timestamp counter 的简写)。
对于计时器管理,一个名为 __ipipe_tscinfo
的结构体需要被注册到 I-pipe 内核。你需要保证编译选项
"CONFIG_IPIPE_ARM_KUSER_TSC" 被开启。举个例子,在
arch/arm/mach-socfpga/Kconfig 中,你将看到:
1 |
|
A9 计数器
如果你使用的 SoC 不是基于 ARM Cortex A9 核的,跳转到下一节。对于基于 ARM Cortex A9 核的
SoC,硬件使用的高精度计数器是由处理器核提供的(或者说“全局计时器”)。因为这个硬件是
SoC 无关的,为了支持 I-pipe 而已经存在的
__ipipe_tscinfo
(arch/arm/kernel/smp_twd.c)可复用。
非 A9 计数器
定义在 arch/arm/include/asm/ipipe.h 的 __ipipe_tscinfo
结构体拥有如下成员:
unsigned int type
计数器的类型,可能的取值为:
IPIPE_TSC_TYPE_FREERUNNING
该 tsc 基于自由运行的计数器
IPIPE_TSC_TYPE_DECREMENTER
该 tsc 基于自减器
IPIPE_TSC_TYPE_FREERUNNING_COUNTDOWN
该 tsc 基于自由运行的计数器,是自减的
IPIPE_TSC_TYPE_FREERUNNING_TWICE
该 tsc 基于自由运行的计数器,并且需要读取两次(有是会读出错误值,但两次之中总有一次是正确的)
如果你所使用的硬件不属于上述情况之一,你需要:
添加一个你的硬件所属于的类型(
IPIPE_TSC_TYPE_xxx
)添加一个读取该计数器并将其扩展到 64 位值的函数(用汇编语言)。参见 arch/arm/kernel/ipipe_tsc_asm.S 与 arch/arm/kernel/ipipe_tsc.c 以获取更多细节。需要注意,汇编函数的实现需要被限制到 96 字节,或者 24 x 32 比特的指令。
unsigned int freq
计数器的频率
unsigned long counter_vaddr
计数器的虚拟地址(在内和空间)
unsigned long u.counter_paddr
计数器的物理地址
unsigned long u.mask
显示计数器有效比特位的掩码。
举个例子,0xffffffff 说明是 32 位计数器,0xffff 表示是 16 位计数器。只有有限的一组值可以表示每种计数器的类型。如果你需要一种不支持的值,arch/arm/kernel/ipipe_tsc.c 和 arch/arm/kernel/ipipe_tsc_asm.S 需要被修改。
一旦类型为
__ipipe_tscinfo
的变量被定义,它可以通过__ipipe_tsc_register()
函数被注册到 I-pipe 内核。
例子
作为一个例子,我们可以看到 arch/arm/mach-davinci/time.c 中:
1 |
|
TODO!()
中断控制器
IC handlers
flow handlers
CONFIG_MULTI_IRQ_HANDLER
多处理器系统
GPIO
实时驱动程序中的 GPIO
GPIO 作为中断源
I-pipe 自旋锁
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!