向一个新的 ARM 平台移植 Xenomai

目的

本文档是 Gilles Chanteperdrix 为 Xenomai 项目撰写的文章1的一个延伸,详细介绍了将中断流水线引入 ARM 内核所带来的变化。

本文致力于引导读者了解如何移植 I-pipe 核心到一个新的 ARM SoC,从而实现一个实时的、双内核的系统。

术语

如果你正在阅读本文档,那么你将有几乎将 I-pipe 运行在一个基于 ARM 的开发板上。开发板的一些例子是:beagleboardbeagleboneraspberry 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服务,也不会持有任何常规自旋锁。另外,一个独立的处理函数必须被实现(或者如果需要持有自旋锁,则原本的自旋锁需要被转化为I-pipe自旋锁,需要保证被其覆盖的临界区比较短)。

  • 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
2
3
4
5
6
7
8
9
static irqreturn_t omap2_gp_timer_interrupt(int irq, void *dev_id)
{
struct clock_event_device *evt = &clockevent_gpt;

__omap_dm_timer_write_status(&clkev, OMAP_TIMER_INT_OVERFLOW);

evt->event_handler(evt);
return IRQ_HANDLED;
}

对于函数 __omap_dm_timer_write_status() 的调用确认了硬件计时器中断的级别。

1
2
3
4
5
6
7
static struct clock_event_device clockevent_gpt = {
.name = "gp timer",
.features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,
.shift = 32,
.set_next_event = omap2_gp_timer_set_next_event,
.set_mode = omap2_gp_timer_set_mode,
};

上面的代码展示了 Linux 中对于硬件计时器处理 one-shot 模式的支持代码。进一步观察表明 omap2_gp_timer_set_next_event() 没有调用任何 Linux 服务,而 Linux 服务是无法在 out-of-bound 上下文中被调用的。因此,这些代码可以安全地与协同内核共享。通过以下修改来支持 I-pipe 内核:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static void omap2_gp_timer_ack(void)
{
__omap_dm_timer_write_status(&clkev, OMAP_TIMER_INT_OVERFLOW);
}

static irqreturn_t omap2_gp_timer_interrupt(int irq, void *dev_id)
{
struct clock_event_device *evt = &clockevent_gpt;

if (!clockevent_ipipe_stolen(evt))
omap2_gp_timer_ack();

evt->event_handler(evt);
return IRQ_HANDLED;
}

#ifdef CONFIG_IPIPE
static struct ipipe_timer omap_shared_itimer = {
.ack = omap2_gp_timer_ack,
.min_delay_ticks = 3,
};
#endif /* CONFIG_IPIPE */

static struct clock_event_device clockevent_gpt = {
.features = CLOCK_EVT_FEAT_PERIODIC |
CLOCK_EVT_FEAT_ONESHOT,
.rating = 300,
.set_next_event = omap2_gp_timer_set_next_event,
.set_state_shutdown = omap2_gp_timer_shutdown,
.set_state_periodic = omap2_gp_timer_set_periodic,
.set_state_oneshot = omap2_gp_timer_shutdown,
.tick_resume = omap2_gp_timer_shutdown,
};

static void __init omap2_gp_clockevent_init(int gptimer_id,
const char *fck_source)
{
/* ... */
#ifdef CONFIG_IPIPE
/* ... */
omap_shared_itimer.irq = clkev.irq;
clockevent_gpt.ipipe_timer = &omap_shared_itimer;
/* ... */
#endif /* CONFIG_IPIPE */

clockevents_register_device(&clockevent_gpt);

/* ... */
}

高精度计数器

由于协同内核的计时器管理基于一个运行在 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
2
3
4
5
menuconfig ARCH_SOCFPGA
bool "Altera SOCFPGA family"
depends on ARCH_MULTI_V7
...
select IPIPE_ARM_KUSER_TSC if IPIPE

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 基于自由运行的计数器,并且需要读取两次(有是会读出错误值,但两次之中总有一次是正确的)

    如果你所使用的硬件不属于上述情况之一,你需要:

    1. 添加一个你的硬件所属于的类型(IPIPE_TSC_TYPE_xxx

    2. 添加一个读取该计数器并将其扩展到 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifdef CONFIG_IPIPE
static struct __ipipe_tscinfo tsc_info = {
.type = IPIPE_TSC_TYPE_FREERUNNING,
.u = {
{
.mask = 0xffffffff,
},
},
};
#endif /* CONFIG_IPIPE */

void __init davinci_timer_init(void)
{
#ifdef CONFIG_IPIPE
tsc_info.freq = davinci_clock_tick_rate;
tsc_info.counter_vaddr = (void *)(timers[TID_CLOCKSOURCE].base +
timers[TID_CLOCKSOURCE].tim_off);
tsc_info.u.counter_paddr = timers[TID_CLOCKSOURCE].pbase +
timers[TID_CLOCKSOURCE].tim_off;
__ipipe_tsc_register(&tsc_info);
/* ... */
#endif /* CONFIG_IPIPE */
}

TODO!()

中断控制器

IC handlers

flow handlers

CONFIG_MULTI_IRQ_HANDLER

多处理器系统

GPIO

实时驱动程序中的 GPIO

GPIO 作为中断源

I-pipe 自旋锁


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!