驱动篇:底层驱动移植(一)(摘录)
发布日期:2021-06-29 11:35:17 浏览次数:2 分类:技术文章

本文共 13375 字,大约阅读时间需要 44 分钟。

驱动篇:底层驱动移植(一)(摘录)

ARM Linux 底层驱动的组成和现状

为了让 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_ts3c2410_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__ >= 623static inline unsigned long arch_local_irq_save(void)4{
5unsigned long flags;67asm volatile(8 " mrs %0, cpsr9 " cpsid i"1011@ arch_local_irq_save\n": "=r" (flags) : : "memory", "cc");return flags;12}1314static inline void arch_local_irq_enable(void)15{
16asm volatile(17 "cpsie i18 :19 :20 : "memory", "cc");@ arch_local_irq_enable"21}2223static inline void arch_local_irq_disable(void)24{
25asm volatile(26 "27 :28 :cpsid i@ arch_local_irq_disable"29: "memory", "cc");30}31#else3233/*34 * Save the current interrupt enable state & disable IRQs35 */36static inline unsigned long arch_local_irq_save(void)37{
38unsigned long flags, temp;3940asm volatile(41 " mrs %0, cpsr42 " orr %1, %0, #128\n"43 " msr cpsr_c, %1"44 : "=r" (flags), "=r" (temp)45 : 46 : "memory", "cc");47@ arch_local_irq_save\n"return flags;48}4950/*51 * Enable IRQs52 */53static inline void arch_local_irq_enable(void)54{
55 unsigned long temp;56 asm volatile(57 " mrs %0, cpsr58 " bic %0, %0, #128\n"59 " msr cpsr_c, %0"60 : "=r" (temp) 61 : 62 : "memory", "cc");@ arch_local_irq_enable\n"63}6465/*66 * Disable IRQs67 */68static inline void arch_local_irq_disable(void)69{
70 unsigned long temp;71 asm volatile(72 " mrs %0, cpsr73 " orr %0, %0, #128\n"74 " msr cpsr_c, %0"75 : "=r" (temp) 76 : 77 : "memory", "cc");@ arch_local_irq_disable\n"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);elsedesc->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[i]; 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);}...}

转载地址:https://blog.csdn.net/zytgg123456/article/details/110639103 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:驱动篇:底层驱动移植(三)(摘录)
下一篇:驱动篇:inux 电源管理的系统架构和驱动(四)

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2024年04月19日 12时05分00秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章