为了让 Linux 在一个全新的 ARM SoC 上运行,需要提供大量的底层支撑,如定时器节拍、中断控制器、 SMP 启动、 CPU 热插拔以及底层的 GPIO 、时钟、 pinctrl 和 DMA 硬件的封装等。
定时器节拍、中断控制器、 SMP 启动和CPU 热插拔这几部分相对来说没有像早期 GPIO 、时钟、 pinctrl 和 DMA 的实现那么杂乱,基本上有个固定的套路。定时器节拍为 Linux 基于时间片的调度机制以及内核和用户空间的定时器提供支撑,中断控制器的驱动则使得 Linux 内核的工程师可以直接调用 local_irq_disable ()、 disable_irq ()等通用的中断 API ,而 SMP 启动支持则用于让 SoC 内部的多个 CPU 核都投入运行, CPU 热插拔则运行运行时挂载或拔除 CPU 。这些工作,在 Linux 3.0 之后的内核中, Linux 社区对比逐步进行了良好的层次划分和架构设计。
在 GPIO 、时钟、 pinctrl 和 DMA 驱动方面,在 Linux 2.6 时代,内核已或多或少有 GPIO 、时钟等底层驱动的架构,但是核心层的代码太薄弱,各 SoC 在这些基础设施实现方面存在巨大差异,而且每个 SoC 仍然需要实现大量的代码。pinctrl 和 DMA 则最为混乱,几乎各家公司都定义了自己独特的实现和 API 。
社区必须改变这种局面,于是 Linux 社区在 2011 年后进行了如下工作,这些工作在目前的 Linux 内核中基本准备就绪:
·STEricsson 公司的工程师 Linus Walleij 提供了新的 pinctrl 驱动架构,内核中新增加一个drivers/pinctrl 目录,支撑SoC 上的引脚复用,各个 SoC 的实现代码统一放入该目录。
·
ti 公司的工程师 Mike Turquette 提供了通过时钟框架,让具体 SoC 实现 clk_ops ()成员函数,并通过
clk_register ()、 clk_register_clkdev ()注册时钟源以及源与设备的对应关系,具体的时钟驱动都统一迁移到drivers/clk 目录中。
· 建议各 SoC 统一采用 dma engine 架构实现 DMA 驱动,该架构提供了通用的 DMA 通道 API ,如
dma engine_prep_slave_single ()、 dma engine_submit ()等,要求 SoC 实现 dma_device 的成员函数,实现代码统一放入 drivers/dma 目录中。
· 在 GPIO 方面, drivers/gpio 下的 gpiolib 已能与新的 pinctrl 完美共存,实现引脚的 GPIO 和其他功能之间的复用,具体的 SoC 只需实现通用的 gpio_chip 结构体的成员函数。
经过以上工作,基本上就把芯片底层基础架构方面的驱动架构统一了,实现方法也统一了。另外,目前 GPIO 、时钟、 pinmux 等都能良好地进行设备树的映射处理,譬如我们可以方便地在 .dts 中定义一个设备要的时钟、pinmux 引脚以及 GPIO 。
除了上述基础设施以外,在将 Linux 移植入新的 SoC 过程中,工程师常常强烈依赖于早期的 printk 功能,内核则提供了相关的 DEBUG_LL 和 EARLY_PRINTK 支持,只需要 SoC 提供商实现少量的回调函数或宏。
内核节拍驱动
Linux 2.6 的早期( Linux2.6.21 之前)内核是基于节拍设计的,一般 SoC 公司在将 Linux 移植到自己芯片上的时候,会从芯片内部找一个定时器,并将该定时器配置为赫兹的频率,在每个时钟节拍到来时,调用 ARM Linux 内核核心层的 timer_tick ()函数,从而引发系统里的一系列行为。如 Linux 2.6.17 中 arch/arm/mach-s3c2410/time.c 的做法类似于代码清单所示:
代码清单 20.1 早期内核的节拍驱动
/*
* IRQ handler for the timer
*/
static irqreturn_t
s3c2410_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
write_seqlock(&xtime_lock);
timer_tick(regs);
write_sequnlock(&xtime_lock);
return IRQ_HANDLED;
}
static struct irqaction s3c2410_timer_irq = {
.name = "S3C2410 Timer Tick",
.flags = SA_INTERRUPT | SA_TIMER,
.handler = s3c2410_timer_interrupt,
};
static void __init s3c2410_timer_init (void)
{
s3c2410_timer_setup();
setup_irq(IRQ_TIMER4, &s3c2410_timer_irq);
}
将硬件的 TIMER4 定时器配置为周期触发中断,每个中断到来就会自动调用内核函数
timer_tick ()。
当前 Linux 多采用无节拍方案,并支持高精度定时器,内核的配置一般会使能 NO_HZ (即无节拍,或者说动态节拍)和 HIGH_RES_TIMERS 。要强调的是无节拍并不是说系统中没有时钟节拍,而是说这个节拍不再像以前那样周期性地产生。无节拍意味着,根据系统的运行情况,以事件驱动的方式动态决定下一个节拍在何时发生。
如果画一个时间轴,周期节拍的系统节拍中断发生的时序如图所示:
在当前的 Linux 系统中, SoC 底层的定时器被实现为一个 clock_event_device 和 clocksource 形式的驱动。在clock_event_device 结构体中,实现其 set_mode ()和 set_next_event ()成员函数;在 clocksource 结构体中,主要实现 read ()成员函数。而在定时器中断服务程序中,不再调用 timer_tick (),而是调用 clock_event_device 的event_handler ()成员函数。一个典型 SoC 的底层节拍定时器驱动形如代码清单所示:
新内核基于 clocksource 和 clock_event 的节拍驱动
static irqreturn_t xxx_timer_interrupt(int irq, void *dev_id)
{
struct clock_event_device *ce = dev_id;
...
ce->event_handler(ce);
return IRQ_HANDLED;
}
/* read 64-bit timer counter */
static cycle_t xxx_timer_read(struct clocksource *cs)
{
u64 cycles;
/* read the 64-bit timer counter */
cycles = readl_relaxed(xxx_timer_base + LATCHED_HI);7 cycles=(cycles<<32)|readl_relaxed(xxx_timer_base + LATCHED_LO);
return cycles;20}
static int xxx_timer_set_next_event(unsigned long delta,
struct clock_event_device *ce)
{
unsigned long now, next;
now = readl_relaxed(xxx_timer_base + LATCHED_LO);
next = now + delta;
writel_relaxed(next, xxx_timer_base + SIRFSOC_TIMER_MATCH_0);
...
}
static void xxx_timer_set_mode(enum clock_event_mode mode,
struct clock_event_device *ce)
{
switch (mode) {
case CLOCK_EVT_MODE_PERIODIC:
...
case CLOCK_EVT_MODE_ONESHOT:
...
case CLOCK_EVT_MODE_SHUTDOWN:
...
case CLOCK_EVT_MODE_UNUSED:
case CLOCK_EVT_MODE_RESUME:
break;
}
}
static struct clock_event_device xxx_clockevent = {
.name = "xxx_clockevent",
.rating = 200,
.features = CLOCK_EVT_FEAT_ONESHOT,
.set_mode = xxx_timer_set_mode,
.set_next_event = xxx_timer_set_next_event,53};
static struct clocksource xxx_clocksource = {
.name = "xxx_clocksource",
.rating = 200,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
.read = xxx_timer_read,
.suspend = xxx_clocksource_suspend,
.resume = xxx_clocksource_resume,
};
static struct irqaction xxx_timer_irq = {
.name = "xxx_tick",
.flags = IRQF_TIMER,
.irq = 0,
.handler = xxx_timer_interrupt,
.dev_id = &xxx_clockevent,
};
static void __init xxx_clockevent_init(void)
{
clockevents_calc_mult_shift(&xxx_clockevent, CLOCK_TICK_RATE, 60);
xxx_clockevent.max_delta_ns =
clockevent_delta2ns(-2, &xxx_clockevent);
xxx_clockevent.min_delta_ns =
clockevent_delta2ns(2, &xxx_clockevent);
xxx_clockevent.cpumask = cpumask_of(0);
clockevents_register_device(&xxx_clockevent);
}
/* initialize the kernel jiffy timer source */
static void __init xxx_timer_init(void)
{
...
BUG_ON(clocksource_register_hz(&xxx_clocksource, CLOCK_TICK_RATE));
BUG_ON(setup_irq(xxx_timer_irq.irq, &xxx_timer_irq));
xxx_clockevent_init();
}
struct sys_timer xxx_timer = {
.init = xxx_timer_init,
};
在上述代码中,我们特别关注如下的函数:
1.clock_event_device 的 set_next_event 成员函数 xxx_timer_set_next_event ()
该函数的 delta 参数是 Linux 内核传递给底层定时器的一个差值,它的含义是下一次节拍中断产生的硬件
定时器中计数器的值相对于当前计数器的差值。我们在该函数中将硬件定时器设置为在 “ 当前计数器计数值
+delta” 的时刻产生下一次节拍中断。 xxx_clockevent_init ()函数中设置了可接受的最小和最大
delta 值对应的纳秒数,即xxx_clockevent.min_delta_ns 和 xxx_clockevent.max_delta_ns 。
2.clocksource 的 read 成员函数 xxx_timer_read ()
该函数可读取出从开机到当前时刻定时器计数器已经走过的值,无论有没有设置当计数器达到某值时产生中
断,硬件的计数总是在进行的(我们要理解,计数总是在进行,而计数到某值后要产生中断则需要软件设
置)。因此,该函数给 Linux 系统提供了一个底层的准确的参考时间。
3. 定时器的中断服务程序 xxx_timer_interrupt ()
在该中断服务程序中,直接调用 clock_event_device 的 event_handler ()成员函数,
event_handler ()成员函数的具体工作也是 Linux 内核根据 Linux 内核配置和运行情况自行设置的。
4.clock_event_device 的 set_mode 成员函数 xxx_timer_set_mode ()
用于设置定时器的模式以及恢复、关闭等功能,目前一般采用 ONESHOT 模式,即一次一次产生中断。当然新版
的 Linux 也可以使用老的周期性模式,如果内核在编译的时候未选择 NO_HZ ,该底层的定时器驱动依然可以
为内核的运行提供支持。
些函数的结合使得 ARM Linux 内核底层所需要的时钟得以运行。下面举一个典型的场景,假定定时器的晶振时钟频率为 1MHz (即计数器每加 1 等于 1μs ),应用程序通过 nanosleep () API 睡眠 100μs ,内核会据此换算出下一次定时器中断的 delta 值为 100 ,并间接调用 xxx_timer_set_next_event ()去设置硬件让其在 100μs 后产生中断。100μs 后,中断产生, xxx_timer_interrupt ()被调用, event_handler ()会间接唤醒睡眠的进程并导致nanosleep ()函数返回,从而让用户进程继续。
这里要特别强调的是,对于多核处理器来说,一般的做法是给每个核分配一个独立的定时器,各个核根据自身的运行情况动态地设置自己时钟中断发生的时刻。看一下我们所运行的 ARM vexpress 的中断( GIC 29twd )即知:
而比较低效率的方法则是只给 CPU0 提供定时器,由 CPU0 将定时器中断通过 IPI ( Inter Processor Interrupt ,处理器间中断)广播到其他核。对于 ARM 来讲, 1 号 IPIIPI_TIMER 就是来负责这个广播的,从 arch/arm/kernel/smp.c 可以看出:
enum ipi_msg_type {
IPI_WAKEUP,
IPI_TIMER,
IPI_RESCHEDULE,
IPI_CALL_FUNC,
IPI_CALL_FUNC_SINGLE,
IPI_CPU_STOP,
};
中断控制器驱动
在 Linux 内核中,各个设备驱动可以简单地调用 request_irq ()、 enable_irq ()、 disable_irq ()、 local_irq_disable ()、local_irq_enable ()等通用 API 来完成中断申请、使能、禁止等功能。在将 Linux 移植到新的 SoC 时,芯片供应商需要提供该部分 API 的底层支持。
local_irq_disable ()、 local_irq_enable ()的实现与具体中断控制器无关,对于 ARM v6 以上的体系结构而言,是直接调用 CPSID/CPSIE 指令进行,而对于 ARM v6 以前的体系结构,则是通过 MRS 、 MSR 指令来读取和设置 ARM 的 CPSR 寄存器。由此可见, local_irq_disable ()、 local_irq_enable ()针对的并不是外部的中断控制器,而是直接让 CPU 本身不响应中断请求。相关的实现位于 arch/arm/include/asm/irqflags.h 中,如代码清单所示:
ARM Linux local_irq_disable () /enable ()底层实现
1#if __LINUX_ARM_ARCH__ >= 6
2
3static inline unsigned long arch_local_irq_save(void)
4{
5
unsigned long flags;
6
7
asm volatile(
8 " mrs %0, cpsr
9 " cpsid i"
10
11
@ arch_local_irq_save
"
: "=r" (flags) : : "memory", "cc");
return flags;
12}
13
14static inline void arch_local_irq_enable(void)
15{
16
asm volatile(
17 "
cpsie i
18 :
19 :
20 : "memory", "cc");
@ arch_local_irq_enable"
21}
22
23static inline void arch_local_irq_disable(void)
24{
25
asm volatile(
26 "
27 :
28 :
cpsid i
@ arch_local_irq_disable"29
: "memory", "cc");
30}
31#else
32
33/*
34 * Save the current interrupt enable state & disable IRQs
35 */
36static inline unsigned long arch_local_irq_save(void)
37{
38
unsigned long flags, temp;
39
40
asm volatile(
41 " mrs %0, cpsr
42 " orr %1, %0, #128
"
43 " msr cpsr_c, %1"
44 : "=r" (flags), "=r" (temp)
45 : 46 : "memory", "cc");
47
@ arch_local_irq_save
"
return flags;
48}
49
50/*
51 * Enable IRQs
52 */
53static inline void arch_local_irq_enable(void)
54{
55 unsigned long temp;
56 asm volatile(
57 " mrs %0, cpsr
58 " bic %0, %0, #128
"
59 " msr cpsr_c, %0"
60 : "=r" (temp) 61 : 62 : "memory", "cc");
@ arch_local_irq_enable
"63}
64
65/*
66 * Disable IRQs
67 */
68static inline void arch_local_irq_disable(void)
69{
70 unsigned long temp;
71 asm volatile(
72 " mrs %0, cpsr
73 " orr %0, %0, #128
"
74 " msr cpsr_c, %0"
75 : "=r" (temp) 76 : 77 : "memory", "cc");
@ arch_local_irq_disable
"
78}
79 #endif
与 local_irq_disable ()和 local_irq_enable ()不同, disable_irq ()、 enable_irq ()针对的则是中断控制器,因此它们适用的对象是某个中断。 disable_irq ()的字面意思是暂时屏蔽掉某中断(其实在内核的实现层面上做了延后屏蔽),直到 enable_irq ()后再执行 ISR 。实际上,屏蔽中断可以发生在外设、中断控制器、 CPU 三个位置,如图 20.3 所示。对于外设端,是从源头上就不产生中断信号给中断控制器,由于它高度依赖于外设于本身,所以 Linux 不提供标准的 API 而是由外设的驱动直接读写自身的寄存器。
在内核中,通过 irq_chip 结构体来描述中断控制器。该结构体内部封装了中断 mask 、 unmask 、 ack 等成员函数,其定义于include/linux/irq.h 中,如代码清单所示:
struct irq_chip {
struct device *parent_device;
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);
void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);
void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);
void (*irq_calc_mask)(struct irq_data *data);
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
int (*irq_request_resources)(struct irq_data *data);
void (*irq_release_resources)(struct irq_data *data);
void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);
int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);
void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
unsigned long flags;
};
各个芯片公司会将芯片内部的中断控制器实现为 irq_chip 驱动的形式。受限于中断控制器硬件的能力,这些成员函数并不一定需要全部实现,有时候只需要实现其中的部分函数即可。譬如drivers/pinctrl/sirf/pinctrl-sirf.c 驱动中的下面代码部分:
static struct irq_chip sirfsoc_irq_chip = {
.name = "sirf-gpio-irq",
.irq_ack = sirfsoc_gpio_irq_ack,
.irq_mask = sirfsoc_gpio_irq_mask,
.irq_unmask = sirfsoc_gpio_irq_unmask,
.irq_set_type = sirfsoc_gpio_irq_type,
};
我们只实现了其中的 ack 、 mask 、 unmask 和 set_type 成员函数, ack 函数用于清中断, mask 、 unmask 用于中断屏蔽和取消中断屏蔽、 set_type 则用于配置中断的触发方式,如高电平、低电平、上升沿、下降沿等。至于到 enable_irq ()的时候,虽然没有实现 irq_enable ()成员函数,但是内核会间接调用 irq_unmask ()成员函数,这点从 kernel/irq/chip.c 中可以看出:
void irq_enable(struct irq_desc *desc)
{
irq_state_clr_disabled(desc);
if (desc->irq_data.chip->irq_enable)
desc->irq_data.chip->irq_enable(&desc->irq_data);
else
desc->irq_data.chip->irq_unmask(&desc->irq_data);
irq_state_clr_masked(desc);
}
在芯片内部,中断控制器可能不止 1 个,多个中断控制器之间还很可能是级联的。举个例子,假设芯片内部有一个中断控制器,支持 32 个中断源,其中有 4 个来源于 GPIO 控制器外围的 4 组 GPIO ,每组 GPIO 上又有 32 个中断(许多芯片的GPIO 控制器也同时是一个中断控制器),其关系如图 20.4 所示。
那么,一般来讲,在实际操作中, gpio0_0-gpio0_31 这些引脚本身在第 1 级会使用中断号 28 ,而这些引脚本身的中断号在实现与 GPIO 控制器对应的 irq_chip 驱动时,我们又会把它映射到 Linux 系统的 32-63 号中断。同理, gpio1_0~gpio1_31 这些引脚本身在第 1 级会使用中断号 29 ,而这些引脚本身的中断号在实现与 GPIO 控制器对应的 irq_chip 驱动时,我们又会把它映射到 Linux 系统的 64-95 号中断,以此类推。对于中断号的使用者而言,无须看到这种 2 级映射关系。如果某设备想申请与 gpio1_0 这个引脚对应的中断,它只需要申请 64 号中断即可。这个关系图看起来如图 20.5 所示。
要特别注意的是,上述图 20.4 和 20.5 中所涉及的中断号的数值,无论是 base 还是具体某个 GPIO 对应的中断号是多少,都不一定是如图 20.4 和图 20.5 所描述的简单线性映射。 Linux 使用 IRQ Domain 来描述一个中断控制器所管理的中断源。换句话说,每个中断控制器都有自己的 Domain 。我们可以将 IRQ Domain 看作是 IRQ 控制器的软件抽象。在添加 IRQDomain 的时候,内核中存在的映射方法有: irq_domain_add_legacy ()、 irq_domain_add_linear ()、irq_domain_add_tree ()等。
irq_domain_add_legacy ()实际上是一种过时的方法,它一般是由 IRQ 控制器驱动直接指定中断源硬件意义上的偏移(一般称为 hwirq )和 Linux 逻辑上的中断号的映射关系。类似图 20.5 的指定映射可以被这种方法弄出来。
irq_domain_add_linear ()则在中断源和 irq_desc 之间建立线性映射,内核针对这个 IRQ Domain 维护了一个 hwirq 和 Linux逻辑 IRQ 之间关系的一个表,这个时候我们其实也完全不关心逻辑中断号了; irq_domain_add_tree ()则更加灵活,逻辑中断号和 hwirq 之间的映射关系是用一棵 radix 树来描述的,我们需要通过查找的方法来寻找 hwirq 和 Linux 逻辑 IRQ 之间的关系,一般适合某中断控制器支持非常多中断源的情况。
实际上,在当前的内核中,中断号更多的是一个逻辑概念,具体数值是多少不是很关键。人们更多的是关心在设备树中设置正确的 interrupt_parrent 和相对该 interrupt_parent 的偏移。以 drivers/pinctrl/sirf/pinctrl-sirf.c 的 irq_chip 部分为例,在 sirfsoc_gpio_probe ()函数中,每组 GPIO 的中断都通过gpiochip_set_chained_irqchip ()级联到上一级中断控制器的中断。
二级 GPIO 中断级联到一级中断控制器
static int sirfsoc_gpio_probe(struct device_node *np)
{
...
for (i = 0; i < SIRFSOC_GPIO_NO_OF_BANKS; i++) {
bank = &sgpio->sgpio_bank
;
spin_lock_init(&bank->lock);7 bank->parent_irq = platform_get_irq(pdev, i);
if (bank->parent_irq < 0) {
err = bank->parent_irq;
goto out_banks;
}
gpiochip_set_chained_irqchip(&sgpio->chip.gc,
&sirfsoc_irq_chip,
bank->parent_irq,
sirfsoc_gpio_handle_irq);
}
...
}
原作者:金乌秃鹫