精华内容
下载资源
问答
  • 中断唤醒系统流程

    千次阅读 2018-07-12 14:05:28
    一般来说,当设备需要唤醒系统的时候,会通过改变interrupt pin电平状态,而SoC会检测到这个变化,将SoC从睡眠中唤醒,该设备通过相关的子系统通知上层应用做出相应的处理。这就是中断唤醒的过程...
    1. 前言 
    
    

    曾几何时,不知道你是否想过外部中断是如何产生的呢?又是如何唤醒系统的呢?在项目中,一般具有中断唤醒的设备会有一个interrupt pin硬件连接到SoC的gpio pin。一般来说,当设备需要唤醒系统的时候,会通过改变interrupt pin电平状态,而SoC会检测到这个变化,将SoC从睡眠中唤醒,该设备通过相关的子系统通知上层应用做出相应的处理。这就是中断唤醒的过程。说起来很简洁,可以说是涵盖了软硬件两大块。是不是?

    为了使能设备的唤醒能力,设备驱动中会在系统suspend的时候通过enable_irq_wake(irq)接口使能设备SoC引脚的中断唤醒能力。然后呢?然后当然是万事大吉了,静静的等待设备中断的到来,最后唤醒系统。假设我们做一款手机,手机有一个压感传感器,重压点亮屏幕,轻压在灭屏的时候无响应,在亮屏的时候作为home键功能,压力值通过i2c总线读取(描述挺像iPhone8的home键!)。假如有一天,你突然发现重压按键,屏幕不亮。于是你开始探究所以然,聪明的你一定会先去用示波器测量irq pin的波形,此时你发现了重压按键,的确产生了一个电平信号的变化,此时可就怪不得硬件了。而你又发现插入USB使用ADB工具抓取log的情况下(Android的adb工具需要通过USB协议通信,一般不会允许系统休眠),重压可以亮屏。此时,我觉得就很有可能是唤醒系统了,但是系统醒来后又睡下去了,而你注册的中断服务函数中的代码没有执行完成就睡了。什么情况下会出现呢?试想一下,你通过request_irq接口注册的handle函数中queue work了一个延迟工作队列(主要干活的,类似下半部吧),由于时间太长,还没来得及调度呢,系统又睡下了,虽然你不愿意,但是事情就是可能这样发生的。那这一切竟然是为什么呢?作为驱动工程师最关注的恐怕就是如何避开这些问题呢?
    1) 设备唤醒cpu之后是立即跳转中断向量表指定的位置吗?如果不是,那么是什么时候才会跳转呢?
    2) 已经跳转到中断服务函数开始执行代码,后续就会调用你注册的中断handle 代码吗?如果不是,那中断服务函数做什么准备呢?而你注册的中断handle又会在什么时候才开始执行呢?
    3) 假如register_thread_irq方式注册的threaded irq中调用msleep(1000),睡眠1秒,请问系统此时会继续睡下去而没调度回来吗?因此导致msleep后续的操作没有执行。
    4) 如果在注册的中断handle中把主要的操作都放在delayed work中,然后queue delayed work,work延时1秒执行,请问系统此时会继续睡下去而没调度delayed work 吗?因此导致delayed work 中的操作没有执行呢?
    5) 如果4)成立的话,我们该如何编程避免这个问题呢?
    好了,本片文章就为你解答所有的疑问。
    注:文章代码分析基于linux-4.15.0-rc3。

    2. 中断唤醒流程

    现在还是假设你有一个上述的设备,现在你开始编写driver代码了。假设部分代码如下:

    static irqreturn_t smcdef_event_handler(int irq, void *private)
    {
        /* do something you want, like report input events through input subsystem */
    
        return IRQ_HANDLED;
    }
    
    static int smcdef_suspend(struct device *dev)
    {
        enable_irq_wake(irq);
    }
    
    static int smcdef_resume(struct device *dev)
    {
        disable_irq_wake(irq);
    }
    
    static int smcdef_probe(struct i2c_client *client,
            const struct i2c_device_id *id)
    {
        /* ... */
        request_thread_irq(irq,
                smcdef_event_handler,
                NULL,
                IRQF_TRIGGER_FALLING,
                "smcdef",
                pdata);
    
        return 0;
    }
    
    static int smcdef_remove(struct i2c_client *client)
    {
        return 0;
    }
    
    static const struct of_device_id smcdef_dt_ids[] = {
        {.compatible = "wowo,smcdef" },
        { }
    };
    MODULE_DEVICE_TABLE(of, smcdef_dt_ids);
    
    static SIMPLE_DEV_PM_OPS(smcdef_pm_ops, smcdef_suspend, smcdef_resume);
    
    static struct i2c_driver smcdef_driver = {
        .driver = {
            .name             = "smcdef",
            .of_match_table = of_match_ptr(smcdef_dt_ids),
            .pm                = &smcdef_pm_ops,
        },
        .probe  = smcdef_probe,
        .remove = smcdef_remove,
    };
    module_i2c_driver(smcdef_driver);
    
    MODULE_AUTHOR("smcdef");
    MODULE_DESCRIPTION("IRQ test");
    MODULE_LICENSE("GPL");

    在probe函数中通过request_thread_irq接口注册驱动的中断服务函数smcdef_event_handler,注意这里smcdef_event_handler的执行环境是中断上下文,thread_fn的方式下面也会介绍。


    2.1. enable_irq_wake

    当系统睡眠(echo “mem” > /sys/power/state)的时候,回想一下suspend的流程就会知道,最终会调用smcdef_suspend使能中断唤醒功能。enable_irq_wake主要工作是在irq_set_irq_wake中完成,代码如下:


    int irq_set_irq_wake(unsigned int irq, unsigned int on) 
    {
        unsigned long flags;
        struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, IRQ_GET_DESC_CHECK_GLOBAL);
        int ret = 0;
    
        /* wakeup-capable irqs can be shared between drivers that
         * don't need to have the same sleep mode behaviors.
         */
        if (on) {
            if (desc->wake_depth++ == 0) {
                ret = set_irq_wake_real(irq, on);
                if (ret)
                    desc->wake_depth = 0;
                else
                    irqd_set(&desc->irq_data, IRQD_WAKEUP_STATE);
            }
        } else {
            if (desc->wake_depth == 0) {
                WARN(1, "Unbalanced IRQ %d wake disable\n", irq);
            } else if (--desc->wake_depth == 0) {
                ret = set_irq_wake_real(irq, on);
                if (ret)
                    desc->wake_depth = 1;
                else
                    irqd_clear(&desc->irq_data, IRQD_WAKEUP_STATE);
            }
        }
        irq_put_desc_busunlock(desc, flags);
        return ret;
    }


    1) 首先在set_irq_wake_real函数中通过irq_chip的irq_set_wake回调函数设置SoC相关wakeup寄存器使能中断唤醒功能,如果不使能的话,即使设备在那疯狂的产生中断signal,SoC可不会理睬你哦!
    2) 设置irq的state为IRQD_WAKEUP_STATE,这步很重要,suspend流程会用到的。


    2.2. Suspend to RAM流程


    先画个图示意一下系统Suspend to RAM流程。我们可以看到图片画的很漂亮。从enter_state开始到suspend_ops->enter()结束。对于suspend_ops->enter()调用,我的理解是CPU停在这里了,待到醒来的时候,就从这里开始继续前行的脚步。

    STR流程.png


    1) enable_irq_wake()可以有两种途径,一是在driver的suspend函数中由驱动开发者主动调用;二是在driver的probe函数中调用dev_pm_set_wake_irq()和device_init_wakeup()。因为suspend的过程中会通过dev_pm_arm_wake_irq()打开所有wakeup source的irq wake功能。我更推荐途径1,因为系统已经帮我们做了,何必重复造轮子呢!
    2) 对于已经enable 并且使能wakeup的irq,置位IRQD_WAKEUP_ARMED,然后等待IRQ handler和threaded handler执行完成。后续详细分析这一块。
    3) 针对仅仅enable的irq,设置IRQS_SUSPENDED标志位,并disable irq。
    4) 图中第④步关闭noboot cpu,紧接着第⑤步diasble boot cpu的irq,即cpu不再响应中断。
    5) 在cpu sleep之前进行最后一步操作就是syscore suspend。既然是最后suspend,那一定是其他device都依赖的系统核心驱动。后面说说什么的设备会注册syscore suspend。

    2.3. resume流程


    假设我们使用的是gic-v3代码,边沿触发中断设备。现在设备需要唤醒系统了,产生一个边沿电平触发中断。此时会唤醒boot cpu(因为noboot cpu在suspend的时候已经被disable)。你以为此时就开始跳转中断服务函数了吗?no!还记得上一节说的吗?suspend之后已经diasble boot cpu的irq,因此中断不会立即执行。什么时候会执行呢?当然是等到local_irq_enable()之后。resume流程如下图。

    resume流程.png

    1) 首先执行syscore resume,马上为你讲解syscore的用意。
    2) arch_suspend_enable_irqs()结束后就会进入中断服务函数,因为中断打开了,interrupt controller的pending寄存器没有清除,因此触发中断。你以为此时会调用到你注册的中断handle吗?错了!此时中断服务函数还没执行你注册的handle就返回了。马上为你揭晓为什么。先等等。

    先说到这里,先看看什么是syscore。

    2.4. system core operations有什么用?


    先想一想为什么要等到syscore_resume之后才arch_suspend_enable_irqs()呢?试想一下,系统刚被唤醒,最重要的事情是不是先打开相关的时钟以及最基本driver(例如:gpio、irq_chip等)呢?因此syscore_resume主要是clock以及gpio的驱动resume,因为这是其他设备依赖的最基本设备。回想一下上一节中Susoend to RAM流程中,syscore_suspend也同样是最后suspend的,毕竟人家是大部分设备的基础,当然最后才能suspend。可以通过register_syscore_ops()接口注册syscore operation。


    2.5. gic interrupt controller中断执行流程


    接下来arch_suspend_enable_irqs()之后就是中断流程了,其函数执行流程如下。

    中断执行流程.png



    图片中是一个中断从汇编开始到结束的流程。假设我们的设备是边沿触发中断,那么一定会执行到handle_edge_irq(),如果你不想追踪代码,或者对中断流程不熟悉,我教你个方法,在注册的中断handle中加上一句WARN_ON(1);语句,请查看log信息即可。handle_edge_irq()代码如下:

    void handle_edge_irq(struct irq_desc *desc)
    {
        raw_spin_lock(&desc->lock);
    
        desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
    
        if (!irq_may_run(desc)) {
            desc->istate |= IRQS_PENDING;
            mask_ack_irq(desc);
            goto out_unlock;
        }
    
        /*
         * If its disabled or no action available then mask it and get
         * out of here.
         */
        if (irqd_irq_disabled(&desc->irq_data) || !desc->action) {
            desc->istate |= IRQS_PENDING;
            mask_ack_irq(desc);
            goto out_unlock;
        }
    
        kstat_incr_irqs_this_cpu(desc);
    
        /* Start handling the irq */
        desc->irq_data.chip->irq_ack(&desc->irq_data);
    
        do {
            if (unlikely(!desc->action)) {
                mask_irq(desc);
                goto out_unlock;
            }
    
            /*
             * When another irq arrived while we were handling
             * one, we could have masked the irq.
             * Renable it, if it was not disabled in meantime.
             */
            if (unlikely(desc->istate & IRQS_PENDING)) {
                if (!irqd_irq_disabled(&desc->irq_data) &&
                    irqd_irq_masked(&desc->irq_data))
                    unmask_irq(desc);
            }
    
            handle_irq_event(desc);
    
        } while ((desc->istate & IRQS_PENDING) &&
             !irqd_irq_disabled(&desc->irq_data));
    
    out_unlock:
        raw_spin_unlock(&desc->lock);
    }

    1) irq_may_run()判断irq是否有IRQD_WAKEUP_ARMED标志位,当然这里是有的。随后调用irq_pm_check_wakeup()清除IRQD_WAKEUP_ARMED flag顺便置位IRQS_SUSPENDED和IRQS_PENDING flag,又irq_disable关闭了中断。
    2) irq_may_run()返回false,因此这里直接返回了,所以你注册的中断handle并没有执行。你绝望,也没办法。当然这里也可以知道,唤醒系统的这次中断注册的handle的执行环境不是硬件中断上下文。

    2.6. dpm_resume_noirq()


    我们来继续分析2.3节resume的后续流程,把图继续搬过来。

    resume流程.png

    1) 继续enable所有的noboot cpu之后,开始dpm_resume_noirq()。这里为什么起名noirq呢?中断已经可以响应了,我猜测是这样的:虽然可以响应中断,但是也是仅限于suspend之前的enable_irq_wake的irq,因为其他irq已经被disable。并且具有唤醒功能的irq也仅仅是进入中断后设置一些flag就立即退出了,没有执行irq handle,因此相当于noirq。
    2) dpm_noirq_resume_devices()会调用”noirq” resume callbacks,这个就是struct dev_pm_ops结构体的resume_noirq成员。那么什么的设备驱动需要填充resume_noirq成员呢?我们考虑一个事情,到现在为止唤醒系统的irq的handle还没有执行,如果注册的中断handle是通过spi、i2c等方式通信,那么在即将执行之前,我们是不是应该首先resume spi、i2c等设备呢!所以说,很多设备依赖的设备,尽量填充resume_noirq成员,这样才比较合理。毕竟唤醒的设备是要使用的嘛!而gpio驱动就适合syscore resume,因为这里i2c设备肯定依赖gpio设备。大家可以看看自己平台的i2c、spi等设备驱动是不是都实现resume_noirq成员。当然了,前提是这个设备需要resume操作,如果不需要resume就可以使用,那么完全没有必要resume_noirq。所以,写driver也是要考虑很多问题的,driver应该实现哪些dev_pm_ops的回调函数?
    3) resume_device_irqs中会帮我们把已经enable_irq_wake的设备进行disable_irq_wake,但是前提是driver中通过2.2节中途径1的方式。
    4) resume_irqs继续调用,最终会enable所有在susoend中关闭的irq。
    5) check_irq_resend才是真正触发你注册的中断handle执行的真凶。

    check_irq_resend代码如下:


    void check_irq_resend(struct irq_desc *desc)
    {
        /*
         * We do not resend level type interrupts. Level type
         * interrupts are resent by hardware when they are still
         * active. Clear the pending bit so suspend/resume does not
         * get confused.
         */
        if (irq_settings_is_level(desc)) {
            desc->istate &= ~IRQS_PENDING;
            return;
        }
        if (desc->istate & IRQS_REPLAY)
            return;
        if (desc->istate & IRQS_PENDING) {
            desc->istate &= ~IRQS_PENDING;
            desc->istate |= IRQS_REPLAY;
    
            if (!desc->irq_data.chip->irq_retrigger ||
                !desc->irq_data.chip->irq_retrigger(&desc->irq_data)) {
    
                unsigned int irq = irq_desc_get_irq(desc);
    
                /* Set it pending and activate the softirq: */
                set_bit(irq, irqs_resend);
                tasklet_schedule(&resend_tasklet);
            }
        }
    }


    由于在之前分析已经设置了IRQS_PENDING flag,因此这里会tasklet_schedule(&resend_tasklet)并且置位irqs_resend变量中相应的bit位,代表软中断触发。然后就开始tasklet_schedule最终会唤醒ksoftirqd线程,在ksoftirqd线程中会调用你注册的中断handle,毕竟ksoftirqd线程优先级很高,所以很快就会调度了。具体调用过程可以参考wowo的softirq和tasklet文章。这里我们也可以得出中断handle执行的上下文环境是软中断上下文的结论。当然我们还是有必要分析一下tasklet最后一步resend_irqs()函数的作用,代码如下:


    /* Bitmap to handle software resend of interrupts: */
    static DECLARE_BITMAP(irqs_resend, IRQ_BITMAP_BITS);
    
    /*
     * Run software resends of IRQ's
     */
    static void resend_irqs(unsigned long arg)
    {
        struct irq_desc *desc;
        int irq;
    
        while (!bitmap_empty(irqs_resend, nr_irqs)) {
            irq = find_first_bit(irqs_resend, nr_irqs);
            clear_bit(irq, irqs_resend);
            desc = irq_to_desc(irq);
            local_irq_disable();
            desc->handle_irq(desc);
            local_irq_enable();
        }
    }
    /* Tasklet to handle resend: */
    static DECLARE_TASKLET(resend_tasklet, resend_irqs, 0);


    1)  irqs_resend是一个unsigned int类型的数组,每一个bit都代表一个irq是否resend。
    2)  resend_irqs是注册的resend_tasklet的callback函数,当tasklet_schedule(&resend_tasklet)之后就会被调度执行。
    3)  在resend_irqs函数中,通过判断irqs_resend变量中的每一个bit位是否为1(即是否需要resend,也就是调用irq注册的中断handle)。
    好了,现在可以解答清楚的解答第一个问题了:设备唤醒cpu之后是立即跳转中断向量表指定的位置吗?如果不是,那么是什么时候才会跳转呢?设备唤醒cpu之后并不是立即跳转中断向量执行中断,而是等到syscore_resume以及打开cpu中断之后才开始。第二个问题也有答案了,已经跳转到中断服务函数开始执行代码,后续就会调用你注册的中断handle 吗?如果不是,那中断服务函数做什么准备呢?而你注册的中断handle又会在什么时候才开始执行呢?第一次跳转执行中断仅仅是设置相关的flag并且disable_irq,在执行完成设备的resume noirq回调函数之后通过check_irq_resend中调度tasklet,最终执行注册的中断handle,至于为什么要这么做,前面分析也给了答案。


    2.7. IRQ handler会睡眠吗?


    你想过request_thread_irq函数注册的hardirq handler或者是threaded handler会执行一半时,系统会再一次的休眠下去吗?再看看2.2节的图,实际上对于所有已经打开的irq在suspend_device_irqs()会调用synchronize_irq()等待正在处理的hardirq handler或者threaded handler。synchronize_irq()代码如下:


    void synchronize_irq(unsigned int irq)
    {
        struct irq_desc *desc = irq_to_desc(irq);
    
        if (desc) {
            __synchronize_hardirq(desc);
            /*
             * We made sure that no hardirq handler is
             * running. Now verify that no threaded handlers are
             * active.
             */
            wait_event(desc->wait_for_threads,
                   !atomic_read(&desc->threads_active));
        }
    }


    1) __synchronize_hardirq()是等待hardirq handler执行完毕。
    2) 只要threads_active计数不为0就等待threaded handler执行完毕。

    __synchronize_hardirq()代码如下:


    static void __synchronize_hardirq(struct irq_desc *desc)
    {
        bool inprogress;
    
        do {
            unsigned long flags;
    
            /*
             * Wait until we're out of the critical section.  This might
             * give the wrong answer due to the lack of memory barriers.
             */
            while (irqd_irq_inprogress(&desc->irq_data))
                cpu_relax();
    
            /* Ok, that indicated we're done: double-check carefully. */
            raw_spin_lock_irqsave(&desc->lock, flags);
            inprogress = irqd_irq_inprogress(&desc->irq_data);
            raw_spin_unlock_irqrestore(&desc->lock, flags);
    
            /* Oops, that failed? */
        } while (inprogress);
    }


    irqd_irq_inprogress()是判断irq是否设置了IRQD_IRQ_INPROGRESS 标志位。标识hardirq thread正在执行,IRQD_IRQ_INPROGRESS在handle_irq_event()执行开始设置,等到handle_irq_event_percpu()执行完毕之后,同样在handle_irq_event()之后清除。因此hardirq handler执行结束之前系统不会睡眠。那么threaded handler情况也是这样吗?在__handle_irq_event_percpu()函数中通过__irq_wake_thread()函数唤醒irq_thread线程。__irq_wake_thread()函数如下:


    void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
    {
        /*
         * In case the thread crashed and was killed we just pretend that
         * we handled the interrupt. The hardirq handler has disabled the
         * device interrupt, so no irq storm is lurking.
         */
        if (action->thread->flags & PF_EXITING)
            return;
    
        /*
         * Wake up the handler thread for this action. If the
         * RUNTHREAD bit is already set, nothing to do.
         */
        if (test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags))
            return;
    
    
        desc->threads_oneshot |= action->thread_mask;
    
        atomic_inc(&desc->threads_active);
    
        wake_up_process(action->thread);
    }


    1) 如果irq的中断线程已经设置IRQTF_RUNTHREAD标志位,代表irq线程已经正在运行,因此无需重新唤醒,直接返回即可。
    2) 使用atomic_inc()增加threads_active计数,在synchronize_irq()函数中会判断threads_active计数是否为0来决定是否需要等待irq_thread执行完毕。

    说了这些,不知道你是否知道什么是irq_thread呢?我们通过request_thread_irq()函数指定thread_fn,这个thread_fn就是irq_thread线程最终调用的函数。而每个irq都会创建一个irq线程,创建的过程在setup_irq_thread()函数进行,setup_irq_thread()函数代码如下:


    static int setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
    {
        struct task_struct *t;
        struct sched_param param = {
            .sched_priority = MAX_USER_RT_PRIO/2,
        };
    
        if (!secondary) {
            t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
        } else {
            t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name);
            param.sched_priority -= 1;
        }
    
        if (IS_ERR(t))
            return PTR_ERR(t);
    
        sched_setscheduler_nocheck(t, SCHED_FIFO, &param);
    
        get_task_struct(t);
        new->thread = t;
    
        set_bit(IRQTF_AFFINITY, &new->thread_flags);
        return 0;
    }


    通过kthread_create()创建irq/irq-new->name的线程,该线程的入口函数值irq_thread。在irq_thread()中每执行完成一个thread_fn就会threads_active计数减1。
    现在可以考虑第三个问题了,假如register_thread_irq方式注册的threaded irq中调用msleep(1000),睡眠1秒,请问系统此时会继续睡下去而没调度回来吗?因此导致msleep后续的操作没有执行。答案就是不会,因为suspend时候会等待threaded handler执行完毕,所以系统不会睡眠,放心好了。


    2.8. 工作队列会睡眠吗?


    现在来思考一个按键消抖问题。如果你还不知道什么是按键消抖的话,我……。按键消抖在内核中通常是这样处理,通过变压触发中断,在中断handler中通过queue delayed work一段时间,计时结束执行按键上报处理。从内核的gpio_keys抠出部分代码如下:


    static void gpio_keys_gpio_work_func(struct work_struct *work)
    {
        struct gpio_button_data *bdata =
            container_of(work, struct gpio_button_data, work.work);
    
        gpio_keys_gpio_report_event(bdata);
    }
    
    static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
    {
        struct gpio_button_data *bdata = dev_id;
    
        mod_delayed_work(system_wq,
                 &bdata->work,
                 msecs_to_jiffies(bdata->software_debounce));
    
        return IRQ_HANDLED;
    }


    当按键按下,中断handler gpio_keys_gpio_isr执行,设定delayed work的定时器,等到定时器计时结束执行gpio_keys_gpio_work_func(),在gpio_keys_gpio_work_func()上报键值。你有考虑过一个问题吗?假如系统已经睡眠,此时第一次按下按键,是否有可能出现gpio_keys_gpio_work_func()函数没有执行,系统又继续睡眠,在第二次按键的时候执行第一次按键应该调用的gpio_keys_gpio_work_func()的情况吗?其实是有可能出现。只要bdata->software_debounce大于一定的时间就有可能出现。如果这个时间巧合,还有可能出现有时候正确上报,有时候没有上报。其实原因就是,内核的suspend只保证了IRQ handler的执行完成,并没有保证工作队列的执行完毕。

    这里说的问题是work_queue没有机会调度,系统就休眠了。如果使用的不是delayed work,就是普通的work,只是在work中使用类似msleep的操作,系统是否也会继续睡眠呢?修改代码如下:


    static void gpio_keys_gpio_work_func(struct work_struct *work)
    {
        struct gpio_button_data *bdata =
            container_of(work, struct gpio_button_data, work.work);
    
        msleep(1000);
        gpio_keys_gpio_report_event(bdata);
    }
    
    static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
    {
        struct gpio_button_data *bdata = dev_id;
    
        schedule_work(&bdata->work);
    
        return IRQ_HANDLED;
    }
    


    这里的gpio_keys_gpio_work_func()中添加一句msleep(1000)会怎么样呢?由于此时使用的不是delayed work,因此一般不会出现没有调度work就睡眠的情况,与上面的情况还是有点区别的。但是这里其实也是有可能睡眠的,一旦msleep(1000)语句执行完毕,系统满足sleep条件,此时系统还是有可能睡眠导致后面的操作没有执行。在下次唤醒系统的时候才可能执行。所以这种情况下也是危险的。

    结论就是:内核的suspend只保证了IRQ handler(hardirq handler or threaded handler)的执行完成,并没有保证工作队列的执行完毕。因此我们使用工作队列的话,必须要考虑这种情况的发生,并解决。

    2.9. 如何解决工作队列睡眠问题?


    系统suspend的过程中,主要是通过pm_wakeup_pending()判断suspend是否需要abort。如果你对我说的这一块不清楚,可以看看wowo其他几篇关于电源管理的文章。pm_wakeup_pending()主要是判断combined_event_count变量在suspend的过程中是否改变,如果改变suspend就应该abort。既然知道了原理,那么就好办了。在中断handler开始处增加combined_event_count计数,工作队列函数结尾位置减小combined_event_count计数即可。当然是不用你自己写代码,系统提供了接口函数pm_stay_awake()和pm_relax()。2.8节修改后的代码如下:


    static void gpio_keys_gpio_work_func(struct work_struct *work)
    {
        struct gpio_button_data *bdata =
            container_of(work, struct gpio_button_data, work.work);
    
        gpio_keys_gpio_report_event(bdata);
        if (bdata->button->wakeup)
            pm_relax(bdata->input->dev.parent);
    }
    
    static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
    {
        struct gpio_button_data *bdata = dev_id;
    
        if (bdata->button->wakeup) {
            const struct gpio_keys_button *button = bdata->button;
    
            pm_stay_awake(bdata->input->dev.parent);
        }
    
        mod_delayed_work(system_wq,
                 &bdata->work,
                 msecs_to_jiffies(bdata->software_debounce));
    
        return IRQ_HANDLED;
    }


    好了,现在你放心好了,即使你是在gpio_keys_gpio_work_func()中msleep(2000),系统也会等到pm_relax()执行之后系统才可能suspend。


    3. 驱动工程师建议


    看了这么多代码总是想说点东西。不管是建议还是什么。我由衷地希望驱动工程师可以写出完美没有bug并且简洁的代码。因此,这里有点小建议给驱动工程师(某些特性可能需要比较新的内核版本)。
    1) 如果设备具有唤醒系统的功能,请在probe函数中调用device_init_wakeup()和dev_pm_set_wake_irq()(注意调用顺序,先device_init_wakeup()再dev_pm_set_wake_irq())。毕竟这样系统suspend的时候会自动帮助我们enable_irq_wake()和disable_irq_wake(),何乐而不为呢!简单就是美。如果你是i2c设备,那么可以更完美。连probe函数里面也可以不用调用了。只需要在设备的dts中添加wakeup-source属性即可。i2c core会自动帮我们完成这些操作。
    2) 如果你习惯在driver的suspend()中关闭中断,在resum()中打开中断,我觉你没必要这么做,何必要这些冗余代码呢!
    3) 既然dts现在这么流行了,你又何必不用呢!设备dts中的interrupts属性都会指明中断触发type,那你就用嘛!怎么获取这个flag呢?irqd_get_trigger_type()可以通过dts获取irq的触发type。所以request_threaded_irq()的第四个参数irqflags可以使用irqd_get_trigger_type()获得。如果你的内核版本更新的话,还可以更简单,irqflags传入0即可,在request_threaded_irq()中会自动帮我们调用irqd_get_trigger_type()获取。当然了,我也看聪明的IC厂家提供的driver,在dts中自定义一个属性表明irqflags,在driver中获取。我只能猜测driver的编写者不知道irqd_get_trigger_type()接口吧!
    4) 如果中断下半部使用工作队列,请成对使用pm_stay_awake()和pm_relax()。否则,谁也无法保证系统不会再一次的睡眠。


    原创文章,转发请注明出处。蜗窝科技,www.wowotech.net。

    展开全文
  • linux中断系统 - 中断及执行流程

    千次阅读 2016-10-23 18:25:36
    linux中断系统系列文章计划总共由5篇文章组成,本篇会通过中断的执行流来整体介绍一下中断,并引出其他文章的内容简介,中断的代码基本在kernel/irq目录下,中断控制器的代码在drivers/irqchip目录下 内核...

    文章系列


    linux中断子系统 - 中断及执行流程
    linux中断子系统 - 申请中断
    linux中断子系统 - irq_desc的创建
    linux中断子系统 - 中断控制器的注册

    linux中断子系统系列文章计划总共由4篇文章组成,本篇会通过中断的执行流来整体介绍一下中断,并引出其他文章的内容简介,中断的代码基本在kernel/irq目录下,中断控制器的代码在drivers/irqchip目录下

    内核版本linux4.6.3

    1. 中断相关结构体介绍


    这里写图片描述

    通过参照一下ULK3的IRQ描述符图,本图描述了linux4.6.3版本中断中各个结构体所代表的对象,struct irq_desc代表的是一个中断描述符,一个中断所需要的资源都集中在这个结构体中描述,如果没有定义选项CONFIG_SPARSE_IRQ,irq_desc会在系统初始化的时候被分配到一个数组中存放,其中数组下标代表的就是virq(虚拟中断,而不是硬中断),如果定义了选项那么irq_desc会被分配到radix tree中;

    struct irq_data代表的是一个具体中断控制器,包括两个结构体,分别是irq_chip和irq_domain,其中irq_chip中的每个字段是描述了具体操作中断控制器的功能,irq_domain这个比较抽象,它主要的功能是对hwirq和virq创建映射,把某些中断所具有的同样功能抽象出来,然后组成一个domain,这样就可以被其他中断控制器所用,要理解这个结构体还是要多看代码仔细体会了;

    struct irqaction代表一个中断要做什么,此结构体组成一个链表,因为一个中断线上可能会挂载好几个设备,这几个设备会共用这个中断,具体是哪个设备需要判断。

    2. 中断进入执行流


    2.1 ARM64

    本文以arm64和gic-v3中断控制器为例来进行中断执行流的介绍,我把整个执行流分成三个阶段,第一阶段和第二阶段如下图所示

    ![这里写图片描述](https://img-blog.csdn.net/20161023172508452)

    2.1.1 第一阶段

    第一阶段的执行都是用汇编写的,开始进入el1的中断向量入口,然后会调用到handle_arch_irq,此变量为全局的,会在中断控制器初始化的时候设置函数变量,此处以gic-v3为例介绍,最后会调用gic_handle_irq,代码如下:

    static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
    {
        u32 irqnr;
    
        do {
            irqnr = gic_read_iar();
    
            if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
                int err;
    
                if (static_key_true(&supports_deactivate))
                    gic_write_eoir(irqnr);
    
                err = handle_domain_irq(gic_data.domain, irqnr, regs);---------进入第二阶段
                if (err) {
                    WARN_ONCE(true, "Unexpected interrupt received!\n");
                    if (static_key_true(&supports_deactivate)) {
                        if (irqnr < 8192)
                            gic_write_dir(irqnr);
                    } else {
                        gic_write_eoir(irqnr);
                    }
                }
                continue;
            }
            if (irqnr < 16) {
                gic_write_eoir(irqnr);
                if (static_key_true(&supports_deactivate))
                    gic_write_dir(irqnr);
    #ifdef CONFIG_SMP
                /*
                 * Unlike GICv2, we don't need an smp_rmb() here.
                 * The control dependency from gic_read_iar to
                 * the ISB in gic_write_eoir is enough to ensure
                 * that any shared data read by handle_IPI will
                 * be read after the ACK.
                 */
                handle_IPI(irqnr, regs);
    #else
                WARN_ONCE(true, "Unexpected SGI received!\n");
    #endif
                continue;
            }
        } while (irqnr != ICC_IAR1_EL1_SPURIOUS);
    }

    2.1.2 第二阶段

    第二阶段代码不复杂,首先找到virq,根据virq找到irq_desc,然后调用irq_desc->handle_irq,这里handle_irq是在申请中断的时候被设置,不过不管被设置成哪个函数最后都会调用handle_irq_event,此时进入第三阶段。

    2.1.3 第三阶段

    这里写图片描述

    此阶段是真正执行申请中断时所要执行的函数,代码如下:

    irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
    {
        irqreturn_t retval = IRQ_NONE;
        unsigned int flags = 0, irq = desc->irq_data.irq;
        struct irqaction *action;
    
        for_each_action_of_desc(desc, action) {
            irqreturn_t res;
    
            trace_irq_handler_entry(irq, action);
            res = action->handler(irq, action->dev_id);------如果中断是共享的,此函数中一定要判断一下是不是自己设备的中断
            trace_irq_handler_exit(irq, action, res);
    
            if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
                      irq, action->handler))
                local_irq_disable();
    
            switch (res) {
            case IRQ_WAKE_THREAD:
                /*
                 * Catch drivers which return WAKE_THREAD but
                 * did not set up a thread function
                 */
                if (unlikely(!action->thread_fn)) {
                    warn_no_thread(irq, action);
                    break;
                }
    
                __irq_wake_thread(desc, action);
    
                /* Fall through to add to randomness */
            case IRQ_HANDLED:
                flags |= action->flags;
                break;
    
            default:
                break;
            }
    
            retval |= res;
        }
    
        add_interrupt_randomness(irq, flags);
    
        if (!noirqdebug)
            note_interrupt(desc, retval);
        return retval;
    }

    并不是说arm64一定要以此流程来执行,而是因为arm64是后来出的架构,而handle_arch_irq也是后来才出现的,所以arm64就用新的框架来实现,而不会用以前的arch_irq_handler_default来实现,这个框架在arm32中也会使用,不过是要一定条件,如下一节介绍

    2.2 ARM32

    ![这里写图片描述](https://img-blog.csdn.net/20161023175031324) 在以前的版本中有另一种方式来实现第一阶段,不过到第二阶段的时候,就是一样的了。

    3. 中断返回


    看一下中断返回的汇编

    el1_irq:
        kernel_entry 1
        enable_dbg
    #ifdef CONFIG_TRACE_IRQFLAGS
        bl  trace_hardirqs_off
    #endif
    
        get_thread_info tsk
        irq_handler--------------------------------处理完,进入内核抢占
    
    #ifdef CONFIG_PREEMPT
        ldr w24, [tsk, #TI_PREEMPT]     // get preempt count
        cbnz    w24, 1f             // preempt count != 0
        ldr x0, [tsk, #TI_FLAGS]        // get flags
        tbz x0, #TIF_NEED_RESCHED, 1f   // needs rescheduling?
        bl  el1_preempt
    #ifdef CONFIG_PREEMPT
    el1_preempt:
        mov x24, lr
    1:  bl  preempt_schedule_irq        // irq en/disable is done inside
        ldr x0, [tsk, #TI_FLAGS]        // get new tasks TI_FLAGS
        tbnz    x0, #TIF_NEED_RESCHED, 1b   // needs rescheduling?
        ret x24
    #endif

    change log

    时间 修改
    2016.11.5 增加中断返回
    展开全文
  • ARM系统中断产生流程

    万次阅读 2011-06-20 14:40:00
    ++++++++++++++++++++++++++++++++++++++++++ 本文系本站原创,欢迎转载! 转载请注明出处: ... ++++++++++++++++++++++++++++++++++++++++++ ...中断源按照硬件位置分为外部中断源和内部中断源,外...

    ++++++++++++++++++++++++++++++++++++++++++

    本文系本站原创,欢迎转载! 转载请注明出处:

    http://blog.csdn.net/mr_raptor/article/details/6556258

    ++++++++++++++++++++++++++++++++++++++++++

     

     

    ARM920T系统中断产生流程总图

    中断源按照硬件位置分为外部中断源和内部中断源,外部中断源和内部中断源又包含子外部中断源和子内部中断源,如上图所示(画了一整天)。

    1. 子内部中断源的产生

    以UART0接收数据产生INT_RXD0中断为例,INT_RXD0产生后进入SUBSRCPND子中断源暂存寄存器,设置INT_RXD0对应的中断位,中断信号经过INTSUBMSK子中断屏蔽寄存器,如果INT_RXD0信号对应位没有被置位(屏蔽掉),中断信号继续向前传递,经过子内部中断源聚合器,将INT_RXD0聚合成对应的中断源信号INT_UART0,设置SRCPND中断源暂存寄存器里INT_UART0位,经过INTMSK中断屏蔽寄存器,如果INT_UART0信号没有被屏蔽掉,中断信号进入INTMOD中断模式寄存器判断是否为快速中断,如果被编程为快速中断,直接打断ARM内核,进入中断处理,如果中断信号为一般中断,进入中断优先级仲裁器进入优先级仲裁,如果INT_UART0信号为最高优先级或只有INT_UART0中断信号产生,则该中断信号被记录到INTPND最高优先级中断暂存寄存器,同时设置INTOFFSET的值为中断号28,最终将中断信号打断ARM内核进行中断处理。如果同时产生多个中断且INT_UART0不是最高优先级,则该中断信号不会被处理,等最高优先级信号处理完后,再次进行优先级仲裁,也就是说中断信号不消失,一直保存在SRCPND里,只到被处理为止。

    2. 内部中断源的产生

    该过程在子内部中断处理过程中已经包含,中断信号产生后直接进入SRCPND里,然后经历上述子内部中断后期处理过程。

    3. 子外部中断的产生

    外部中断源共有24个,其中EINT0~EINT3为外部中断源,EINT4_7,EINT8_23为复合中断源,他们包含有子外部中断源。

    由于外部硬件直接挂接到I/O Ports(详见S3C2440A硬件手册第9章)上的,我们要想让外设硬件中断得到处理,要先从EINT0~EINT23里选择中断信号,我们以EINT11为例,介绍子外部中断处理过程。

    通常CPU内部引出引脚都是复用的,也就是说一根CPU引脚可以有多种功能,可以设置其为输入信号线,输出信号线或中断信号线,要想让硬件产生中断,首先要对可以产生中断的引脚进行编程,设置该引脚为中断信号线。EINT11中断信号对应CPU引脚为GPG3,通过设置GPGCON[7:6] = 0b10,可以设置该引脚为中断信号线。

    表3-14 GPGCON寄存器

     

     

    设置了CPU管脚为中断信号线之后,还要通过设置EXTINT0寄存器来指定中断信号的触发方式:高电平触发,低电平触发,电平上升沿,下除沿,双沿触发。

     

    图3-9 电平信号触发示意图

    由于按键按下时让它产生中断,也就是从高电平变为低电平时产生(上节按键中断原理),因此我们设置EINT11中断信号的触发方式为下降沿触发,EXTINT1[14:12] = 0b01x

    表3-15 EXTINT1寄存器

     

     

    设置完触发方式之后,当外设中断信号线上的电平达到触发条件时,通过外部中断产生器产生中断信号,然后将子外部中断暂存寄存器EINTPND中对应的EINT11位置1,中断信号再进入EINTMSK子外部中断屏蔽寄存器,如果EINT11中断源信号没有被屏蔽,则EINT11中断信号进入子外部中断聚合器,复合成EINT8_23中断信号,然后再经历与前面子内部中断信号一样的处理机制。

    (1)EINTPEND外部中断暂存寄存器

    表3-16外部中断暂存寄存器(EINTPEND)

    寄存器名

    地址

    是否读写

    描述

    复位默认值

    EINTPEND

    0x560000A8

    R/W

    外部中断信号暂存寄存器

    0:没有中断请求信号

    1:中断请求信号产生

    0x0000000

     

    EINTPEND

    描述

    初始值

    EINT23

    [23]

    0 = 未产生中断 1 = 产生中断

    0

    EINT4

    [4]

    0 = 未产生中断 1 = 产生中断

    0

    保留位

    [3:0]

    0000

     

    (2)EINTMASK外部中断屏蔽寄存器

    表3-17外部中断屏蔽寄存器(EINTMASK)

    寄存器名

    地址

    是否读写

    描述

    复位默认值

    EINTMASK

    0x560000A4

    R/W

    外部中断信号屏蔽寄存器

    0:未屏蔽,中断可用

    1:屏蔽中断信号

    0x000FFFFF

     

    EINTMASK

    描述

    初始值

    EINT23

    [23]

    0 = 未屏蔽1 = 屏蔽中断

    1

    EINT4

    [4]

    0 = 未屏蔽1 = 屏蔽中断

    1

    保留位

    [3:0]

    1111

    4. 外部中断源的产生

    外部中断产生过程读者可以根据上面中断图自行分析。

     

    按键控制LED灯实验

     

     

     

     

    本实验分三个版本,分别针对三种开发板:友善之臂QQ2440,友善之臂MINI2440,天嵌TQ2440。每种开发板对应工程在:“sys_irq_开发板名”目录下。下面实验内容为针对MINI2440开发板。

    head.s:

    主要实现安装异常向量表,处理复位异常,初始化必要硬件,中断入口处理等功能。

    ;**********************************************************************

    ; 系统中断实验(MINI2440)

    ;**********************************************************************

    GPBCON   EQU      0x56000010

    GPBDAT    EQU      0x56000014        

             EXPORT SYS_IRQ

             AREA  SYS_IRQ,CODE,READONLY

             ENTRY

             ;**********************************************************************      

             ; 设置中断向量,除Reset和HandleIRQ外,其它异常都没有使用(如果不幸发生了,

             ; 将导致死机)

             ;**********************************************************************      

             ; 0x00: 复位Reset异常

             b       Reset

     

    ; 0x04: 未定义异常(未处理)

    HandleUndef

             b       HandleUndef

     

    ; 0x08: 软件中断异常(未处理)

    HandleSWI

             b       HandleSWI

     

    ; 0x0c: 指令预取异常(未处理)

    HandlePrefetchAbt

             b       HandlePrefetchAbt

     

    ; 0x10: 数据访问中止异常(未处理)

    HandleDataAbt

             b       HandleDataAbt

     

    ; 0x14: 未使用异常(未处理)

    HandleNotUsed

             b       HandleNotUsed

     

    ; 0x18: 一般中断异常,跳往HandleIRQ

             b       HandleIRQ

     

    ; 0x1c: 快速中断异常(未处理)

    HandleFIQ

               b       HandleFIQ

     

    Reset                                                                          ; 复位异常处理入口

             ; 关闭看门狗

               ldr  r0, = 0x53000000

               mov  r1, #0

               str  r1, [r0]

     

               bl  initmem

     

             ldr     sp,     =0x32000000                                  ; 设置管理模式栈指针

     

               IMPORT uart_init

             bl  uart_init                                                       ; UART串口初始化

     

               IMPORT irq_init

             bl      irq_init                                                      ; 系统中断初始化

     

               IMPORT key_init

             bl  key_init                                                       ; 按键初始化

     

               IMPORT led_init

             bl  led_init                                                        ; LED灯初始化

     

             msr   cpsr_cxsf, #0xd2                                      ; 切换到中断模式下

             ldr     sp,     =0x31000000                                  ; 设置中断模式栈指针

     

             msr   cpsr_cxsf, #0x13                                      ; 返回管理模式

              

             ldr     lr,      =halt_loop                                       ; 设置管理模式下返回地址

               IMPORT main

             ldr     pc,     =main                                              ; 跳入主函数main里执行

               ;***********************************************************************

               ; 中断处理

               ;***********************************************************************

    HandleIRQ

             sub  lr,lr,#4                                                                ; 修正返回地址

             stmdb  sp!,{r0-r12,lr}                                               ; 保存程序执行现场

             ldr  lr,=int_return                                                       ; 设置中断处理程序返回地址

               IMPORT handle_irq

             ldr  pc,=handle_irq                                           ; 跳入中断处理程序

     

    int_return                                                                      ; 中断处理返回标签

               ldmia  sp!,{r0-r12,pc}^                                             恢复程序执行现场,返回继续执行

              

    halt_loop

             b  halt_loop

     

    initmem

             ldr  r0, =0x48000000

               ldr  r1, =0x48000034

             ;ldr  r2, =memdata

             adr  r2, memdata

    initmemloop

               ldr  r3, [r2], #4

             str  r3, [r0], #4

             teq  r0, r1

             bne  initmemloop

             mov  pc,lr

     

    memdata

               DCD 0x22000000                ;BWSCON

             DCD 0x00000700                 ;BANKCON0    

             DCD 0x00000700                 ;BANKCON1    

             DCD 0x00000700                 ;BANKCON2    

             DCD 0x00000700               ;BANKCON3             

             DCD 0x00000700                 ;BANKCON4    

               DCD 0x00000700                 ;BANKCON5    

             DCD 0x00018005                 ;BANKCON6    

             DCD 0x00018005                 ;BANKCON7    

               DCD 0x008e07a3                  ;REFRESH        

               DCD 0x000000b1                 ;BANKSIZE      

             DCD 0x00000030                 ;MRSRB6 

             DCD 0x00000030                 ;MRSRB7

     

             END                                                                                     ; 代码结束

    该程序主要设置异常向量表,除了Reset异常和中断处理被处理以外,其它异常都未被处理,如果发生时,会产生死循环,Reset异常里主要实现了硬件的基本初始化,如:按键,LED灯等,设置栈指针,用于执行C程序,最后跳入C程序的main函数。在中断处理异常处理中首先修正返回地址,保存用户执行现场,跳入到中断处理例程中执行。

    sys_init.c:

    硬件初始化文件,里面包含LED,KEY的初始化函数。

    #include "register.h"

    #include "comm_fun.h"

     

    #define        TXD0READY    (1<<2)      //发送数据状态OK

    #define        RXD0READY   (1)            //接收数据状态OK

     

    /* UART串口初始化 */

    void uart_init()

    {

             GPHCON |= 0xa0;                //GPH2,GPH3 used as TXD0,RXD0

                      GPHUP     = 0x0;                //GPH2,GPH3内部上拉

             ULCON0   = 0x03;             //8N1         

             UCON0     = 0x05;             //查询方式为轮询或中断;时钟选择为PCLK

             UFCON0 = 0x00;                 //不使用FIFO

                      UMCON0 = 0x00;                //不使用流控

             UBRDIV0 = 12;                   //波特率为57600,PCLK=12Mhz

    }

     

    /* UART串口单个字符打印函数 */

    extern void putc(unsigned char c)

    {

             while( ! (UTRSTAT0 & TXD0READY) );

             UTXH0 = c;

    }

     

    /* UART串口接受单个字符函数 */

    extern unsigned char getc(void)

    {

             while( ! (UTRSTAT0 & RXD0READY) );

             return URXH0;

    }

     

    /* UART串口字符串打印函数 */

    extern int printk(const char* str)

    {

             int i = 0;

             while( str[i] ){

                        utc( (unsigned char) str[i++] );

             }

             return i;

    }

     

    /* 按键初始化 */

    int key_init()

    {

    // 设置K1,K2,K3,K4,K5,K6对应控制寄存器为中断模式

             GPGCON = (2<<0) | (2<<6) | (2<<10) | (2<<12) | (2<<14) | (2<<22);                  

             /*

            01x falling edge triggered下降沿触发

            10x Rising edge triggered上升沿触发

            11x Both edge triggered双沿触发

             */

               //  设置K1,K2,K3,K4,K5按键中断触发方式为上升沿触发

             EXTINT1 = (3<<0) | (3<<12) | (3<<20) | (3<<24) | (3<<28);     

               EXTINT2 = (3<<12);                              //  设置K6按键中断触发方式为上升沿触

               printk("按键初始化OK/r/n");                

             return 0;

    }

     

    /* Led1~Led4初始化*/

    #define LED1       (1<<5)                                   //LED1 GPBDAT[5]

    #define LED2       (1<<6)                                   //LED2 GPBDAT[6]

    #define LED3       (1<<7)                                   //LED3 GPBDAT[7]

    #define LED4       (1<<8)                                   //LED4 GPBDAT[8]

     

    /* 点亮对应num号led灯 */

    extern int led_on(int num)

    {

             switch(num)

             {

                        case 1:

                                 GPBDAT = GPBDAT & ~LED1; break;

                        case 2:

                                 GPBDAT = GPBDAT & ~LED2; break;

                        case 3:

                                 GPBDAT = GPBDAT & ~LED3; break;

                        case 4:

                                 GPBDAT = GPBDAT & ~LED4; break;

                        default:

                                 return 0;

             }

             return num;

    }

     

    /* 关闭num号led灯 */

    extern int led_off(int num)

    {

             switch(num)

             {

                        case 1:

                                 GPBDAT = GPBDAT | LED1; break;

                        case 2:

                                 GPBDAT = GPBDAT | LED2; break;

                        case 3:

                                 GPBDAT = GPBDAT | LED3; break;

                        case 4:

                                 GPBDAT = GPBDAT | LED4; break;

                        default:

                                 return 0;

             }

             return num;

    }

     

    /* 关闭全部led灯 */

    extern int all_led_off(void)

    {

             GPBDAT = GPBDAT | LED1 | LED2 | LED3 | LED4;

             return 0;

    }

     

    /* led灯初始化 */

    int led_init(void)

    {

             GPBCON = 0x15400;                             //设置GPB7为输出口

             all_led_off();

             printk("led初始化OK/r/n");

             return 0;

    }

     

    /* 中断初始化 */

    void irq_init(void)

    {

               // 打开KEY1~KEY6的屏蔽位

             INTMSK &= ~(1<<5);

             EINTMASK &= ~((1<<8) | (1<<11) | (1<<13) | (1<<14) | (1<<15) | (1<<19));

               printk("中断初始化OK/r/n");

    }

    该文件是相关硬件初始化程序,主要包含了看门狗驱动,按键驱动,系统中断驱动,LED驱动。

        handle_irq.c:

    中断处理函数,查出中断源,中断处理,清除中断源。

    #include "register.h"

    #include "comm_fun.h"

     

    #define EINT_Key_REQUEST       5                // Key中断源中断号(6个按键全部使用外部子中断)

    #define K1_EINT_BIT                   (1<<8)       // K1外部子中断位

    #define K2_EINT_BIT                   (1<<11)     // K2外部子中断位

    #define K3_EINT_BIT                   (1<<13)     // K3外部子中断位

    #define K4_EINT_BIT                   (1<<14)     // K4外部子中断位

    #define K5_EINT_BIT                   (1<<15)     // K5外部子中断位

    #define K6_EINT_BIT                   (1<<19)     // K6外部子中断位

    /* 系统中断处理函数 */

    void handle_irq()

    {

             unsigned long irqOffSet = INTOFFSET;          // 取得中断号

               all_led_off();                                                               // 关闭全部Led灯

               if(EINT_Key_REQUEST==irqOffSet){                    // Key中断产生(6个按键使用一个总中断号)

                        if(K1_EINT_BIT & EINTPEND){

                                 led_on(1);                                                 // 点亮Led1

                               printk("Key1 pressed/r/n");

                                 EINTPEND &= K1_EINT_BIT;             // 清除外部子中断源

                        }else if(K2_EINT_BIT & EINTPEND){

                                 led_on(2);                                                 // 点亮Led2

                               printk("Key2 pressed/r/n");

                                 EINTPEND &= K2_EINT_BIT;             // 清除外部子中断源

                        }else if(K3_EINT_BIT & EINTPEND){

                                 led_on(3);                                                 // 点亮Led3

                               printk("Key3 pressed/r/n");

                                 EINTPEND &= K3_EINT_BIT;             // 清除外部子中断源

                        }else if(K4_EINT_BIT & EINTPEND){

                                 led_on(4);                                                 // 点亮Led4

                               printk("Key4 pressed/r/n");

                                 EINTPEND &= K4_EINT_BIT;             // 清除外部子中断源

                        }else if(K5_EINT_BIT & EINTPEND){

                                 all_led_off(1);                                           // 熄灭全部Led

                               printk("Key5 pressed/r/n");

                                 EINTPEND &= K5_EINT_BIT;             // 清除外部子中断源

                      }else if(K6_EINT_BIT & EINTPEND){

                                 all_led_on();                                             // 点亮全部Led

                               printk("Key6 pressed/r/n");

                                 EINTPEND &= K6_EINT_BIT;             // 清除外部子中断源

                        }

             }

               SRCPND &= (1<<irqOffSet);                                   // 清除中断源

               INTPND = INTPND;                                                 // 清除中断结果

    }

    main.c:

    包含主函数和延时函数,主要实现字符串的循环打印。

    #include "register.h"

    #include "comm_fun.h"

     

    /* 延时 */

    void delay(int msec)

    {

             int i, j;

             for(i = 1000; i > 0; i--)

                        for(j = msec*10; j > 0; j--)

                        /* do nothing */;

    }

     

    /* 主函数 */

    int main()

    {

             while(1)

             {

                  printk("main函数在运行.../r/n");

                 delay(5);              //delay

           }

           return 0;

    }

     

     

    ++++++++++++++++++++++++++++++++++++++++++

    本文系本站原创,欢迎转载! 转载请注明出处:

    http://blog.csdn.net/mr_raptor/article/details/6556258

    ++++++++++++++++++++++++++++++++++++++++++

     

     

    展开全文
  • 单片机的中断系统 多级中断控制实例

    1.前言

    记录对51单片机中断、定时/计数器的重要知识点以及难点理解,并且举例中断在实际编程中的应用,从而加深对单片机中断、定时/计数器的理解,熟练的使用中断。

    2.什么是中断

    中断就是计算机在执行某一程序的过程中,由计算机系统内部或外部的某种原因而必须终止当前程序的运行先去执行相应的处理程序,然后再返回继续执行原程序

    3.什么是中断系统

    实现中断功能的软、硬件系统统称为中断系统。

    4.中断的流程

    在这里插入图片描述
    即:中断请求中断响应中断处理中断返回

    5.中断的优先级控制

    通常情况下,一个程序中可能会有多个中断,优先级越高的中断优先执行。如果在一个中断的服务过程中,有一个优先级更高的中断插入,则当前中断暂停,前往执行优先级更高的中断。当优先级高的中断执行完毕后再返回继续执行低优先级的中断。

    6.中断源

    MCS-51共有五个中断源

    1. 外部中断INT0INT1
    2. 定时/计数器T0T1的溢出中断
    3. 串行口的发送和接受中断(只占用一个中断源)
    中断源 功能
    INT0 外部中断0请求,由INT0引脚(P3.2)输入。低电平/负跳变有效,中断请求标志为IE0
    INT1 外部中断1请求,由INT1引脚(P3.3)输入。低电平/负跳变有效,中断请求标志为IE1
    T0 定时/计数器0溢出中断请求,中断标志位为TF0
    T1 定时/计数器1溢出中断请求,中断标志位为TF1
    RXD/TXD 串行口中断请求,中断请求标志位TIRI

    外部中断

    从单片机外部引脚INT0INT1输入中断请求信号的中断。
    外部中断的触发方式有两种电平触发IT0 =0跳变触发(边沿)IT0 = 1,可以通过定时/计数器控制寄存器TCON编程选择。

    7.与中断有关的特殊功能寄存器

    与中断有关的特殊功能寄存器一共有4个。

    1. 定时/计数器控制寄存器(TCON)、
    2. 串行口控制寄存器(SCON)、
    3. 中断允许控制寄存器(IE)、
    4. 中断优先级控制寄存器(IP)

    7.1.定时/计数器控制寄存器 TCON

    作用

    1. 控制定时/计数器T0T1的溢出中断
    2. 控制外部中断的触发方式.由IT0IT1控制
    3. 锁存外部中断请求标志位
    位地址 位定义 功能
    88H IT0 选择外部中断0的中断触发方式。由软件控制。IT0=0为电平触发方式IT0=1为下降沿触发方式
    89H IE0 选择外部中断1的中断触发方式。功能与IT0相似
    8AH

    7.2.串行口控制寄存器 SCON

    串行口的接收发送数据中断请求标志位(RITI)

    位定义 功能
    TI 串行口发送中断请求标志位。CPU每发送一帧数据,硬件置位1(TI=1),但是中断被响应时,需要在中断服务程序中通过软件对TI清零
    RI 串行口接受中断请求标志位。每接收一帧数据,硬件置位1(TI=1),但是中断被响应时,一样需要在中断服务程序中通过软件对TI清零

    串行口中断不能由硬件自动清除中断请求标志位,需要用户通过软件进行控制清零。

    7.3.中断允许控制寄存器 IE

    IE是控制中断的开关,通过对IE的清0和置1操作来控制中断的屏蔽和开放。

    中断允许控制寄存器IE对中断的开放与屏蔽实现两级控制,存在一个总的中断控制位EA

    位定义 功能
    EA 总中断允许控制位。当EA=0时,不允许任何中断请求。
    ES 串行口中断控制位。当ES=0时,不允许串行口中断;当EA=1ES=1时,允许串行口中断。
    ET1 定时/计数器1中断允许控制位。当ET1=0时,屏蔽T1的溢出中断;当EA=1且ET1=1时,允许T1溢出中断
    ET0 定时/计数器0中断允许控制位。功能与ET1相同。
    EX1 外部中断1的中断允许控制位。当EX1=0时,屏蔽外部中断1的中断请求;当EA=1EX1=1时,允许外部中断1的中断请求
    EX0 外部中断0的中断允许控制位。功能与EX1相同

    若某个中断源被允许,出来IE对应位置1外,还需要总中断控制位EA置1。

    实例:若允许片内两个定时/计数器中断,禁止其他中断源的中断请求,尝试编写出设置IE的响应指令

    #include <reg51.h>
    
    EX0 = 0; // 禁止外部中断0
    EX1 = 0; // 禁止外部中断1
    ES = 0;  // 禁止串行口中断
    ET0 = 1; // 允许定时/计数器0中断
    ET1 = 1; // 允许定时/计数器1中断
    EA = 1;  // 总中断控制器打开
    

    7.4.中断优先级控制寄存器 IP

    位定义 功能
    PS 串行口中断优先级控制位。PS=1,串行口中断为高优先级;PS=0,为低优先级。
    PT1 定时/计数器1中断优先级控制位。当PT1=0时,T1溢出中断为低优先级;当PT1=1时,T1溢出中断为高优先级。
    PT0 定时/计数器0中断优先级控制位。当PT0=0时,T0溢出中断为低优先级;当PT0=1时,T0溢出中断为高优先级。
    PX1 外部中断1的中断优先级控制位。当PX1=0时,外部中断1为低优先级;当PX1=1时,外部中断1为高优先级。
    PX0 外部中断0的中断优先级控制位。当PX0=0时,外部中断0为低优先级;当PX0=1时,外部中断0为高优先级。

    同级内第二优先级的次序
    外部中断0 > T0溢出中断 > 外部中断1 > T1溢出中断 > 串行口中断

    8.中断系统在实际编程中的应用

    8.1.实例一:中断的初始化

    #include <reg51.h>
    
    void init() // 中断的初始化函数
    {
        EA = 1; // 总中断控制位
        ES = 1; // 串行口中断允许
        EX0 = 1; // 外部中断0允许
        EX1 = 1; // 外部中断1允许
        ET0 = 1; // 定时/计数器0中断允许
        ET1 = 1; // 定时/计数器1中断允许
        IT0 = 1; // 选择外部中断0的触发方式
        IT1 = 1; // 选择外部中断1的触发方式
    }
    

    例1:假设允许外部中断0和1中断,并设定外部中断0为高级中断,外部中断1为低级中断,外部中断0为下降沿触发方式,外部中断1为电平触发方式。试写出该程序的中断初始化程序。

    #include <reg51.h>
    
    void init() // 
    {
        EA = 1; 打开中断控制
        EX0 = 1; 允许外部中断0
        EX1 = 1; 允许外部中断1
        IT0 = 1; 外部中断1采取边沿触发方式
        IT1 = 0; 外部中断0采取电平触发方式
        PX0 = 1; 外部中断0为高优先级
        PX1 = 0; 外部中断1为低优先级
    }
    
    

    8.2.实例二:利用中断控制LED闪烁形式

    要求

    用80C51单片机控制8个LED灯,在外部中断0输入引脚(P3.2)接一个开关K1。要求将外部中断0设置为下降沿触发,程序启动是8个LED以跑马灯的形式交替闪烁。每按一次开关K1,使引脚接地,产生一个下降沿触发的外部中断请求。在中断服务程序中,8个LED高四位和低四位交替闪烁5次,然后中断返回,8个LED继续以跑马灯形式闪烁。

    采用Protues+Keil仿真

    元器件

    • 单片机:80C51 *1
    • 开关按钮:Button *1
    • 电阻:MINRES470K *1
    • LED:LED-BLUE *8

    仿真图

    在这里插入图片描述
    代码

    #include<reg52.h>
    
    unsigned char code table[] = {0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f,0x00,0xff};	// 控制P2端口的状态
    
    unsigned char i,j,k,num;
    
    void delay()			 // 延时函数
    {
    	for(i = 0;i<100;i++)
    	{
    		for(j=0;j<200;j++)
    		;
    	}
    }
    
    void init()		   // 中断的初始化
    {
    	EA = 1;	   // 打开总中断控制
    	EX0 = 1;   // 允许外部中断0
    	IT0 = 1;   // 外部中断为下降沿触发方式
    }
    
    
    void main()	   
    {
    	init();
    	while(1)
    	{
    		for(num =0;num<10;num++)
    		{
    			P2 = table[num];
    			delay();
    		}
    	}
    }
    
    void int0() interrupt 0			   // 中断服务程序
    {
    	for(k = 0;k<5;k++){
    		P2 = 0xf0;
    		delay();
    		P2=0x0f;
    		delay();
    	}
    }
    

    8.3.实例三:多级中断控制LED不形式闪烁

    要求:在例2的基础上,在外部中断1输入引脚(P3.3)接一只按钮开关K2。当按下K1时,外部中断0下降沿触发方式触发,进入外部中断0服务程序,上下4个灯交替闪烁;此时按下K2,外部中断1下降沿触发方式触发,进入外部中断1服务程序,8个灯交替闪烁。当外部中断1响应完毕后,返回继续响应外部中断0,直到外部中断0响应完毕,返回执行主程序。
    首先我们分析一波中断初始化函数

    1. 两个外部中断0和1。外部中断0的服务程序为上下4灯交替闪烁,外部中断1的服务程序为8灯闪烁。即EA = 1; EX0 = 1; EX1 = 1;

    2. 优先级:外部中断1 > 外部中断0 即PX1 = 1; PX0 = 0;

    3. 触发方式:都为下降沿触发。即IT0 = 1; IT1 = 1;

    这样我们的中断初始化程序基本完成

    void init()
    {
        EA = 1;
        EX0 = 1; EX1 = 1;
        PX0 = 0; PX1 = 1;
        IT0 = 1; IT1 = 1;
    }
    
    

    其次我们再捯饬一下主程序

    void main()	   
    {
    	init();
    	while(1)
    	{
    		for(num =0;num<10;num++)
    		{
    			P2 = table[num];
    			delay();
    		}
    	}
    }
    
    

    另外我们再搞一下外部中断0的服务程序

    void int0() interrupt 0
    {
    	for(k = 0;k<5;k++)
        {
    		P2 = 0xf0;
    		delay();
    		P2=0x0f;
    		delay();
    	}
    }
    

    最后我们再他喵的弄一下外部中断1的服务程序

    
    void int1() interrupt 2			  // 外部中断1服务程序
    {
    	for(l = 0;l < 5; l++)
    	{
    		P2 = 0x00;
    		delay();
    		P2 = 0xff;
    		delay();
    	}
    }
    

    完整代码

    #include<reg52.h>
    
    unsigned char code table[] = {0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0x00, 0xff};	// 控制P2端口的状态
    
    unsigned char i, j, k, l, num;
    
    void delay()			 // 延时函数
    {
    	for(i = 0; i < 200; i++)
    	{
    		for(j = 0; j < 200; j++)
    		;
    	}
    }
    
    void init()		   // 中断的初始化
    {
    	EA = 1;
        EX0 = 1; EX1 = 1;
        PX0 = 0; PX1 = 1;
        IT0 = 1; IT1 = 1;
    }
    
    void main()	   
    {
    	init();
    	while(1)
    	{
    		for(num = 0; num < 10; num++)
    		{
    			P2 = table[num];
    			delay();
    		}
    	}
    }
    
    void int0() interrupt 0			   // 外部中断0中断服务程序
    {
    	EX0 = 0;
    	for(k = 0; k < 5; k++)
    	{
    		P2 = 0xf0;
    		delay();
    		P2 = 0x0f;
    		delay();
    		EX0 = 1;
    	}
    }
    
    void int1() interrupt 2			  // 外部中断1服务程序
    {
    	for(l = 0;l < 5; l++)
    	{
    		P2 = 0x00;
    		delay();
    		P2 = 0xff;
    		delay();
    	}
    }
    

    仿真图

    在这里插入图片描述
    文章来源:中断系统

    展开全文
  • 中断中断处理流程

    千次阅读 2019-09-21 21:10:28
    1. 中断概念 中断是指由于接收到来自外围硬件(相对于中央处理器和内存)的异步信号或来自软件的同步信号,而进行相应的硬件/软件处理。发出这样的信号称为进行中断请求(interrupt request,IRQ)。硬件中断...
  • 中断处理流程

    万次阅读 2017-12-16 22:28:16
    该方式实现比较简单,但CPU利用率很低,不适合多任务的系统。 2. 中断方式: CPU在告知硬件开始一项工作后,就去做别的事去了,当硬件完成了该项工作后,向CPU发送一个信号, 告知CPU它已经完成了这项工作。
  • 中断流程 中断向量

    千次阅读 2013-10-14 11:28:22
    巩固下中断流程,还有就是中断向量时不时又心虚了,还是记录下。 xxx中断流程: 单片机接收到中断后,  先屏蔽中断 查询RTE 表,以MSI 的形式发给IA. (假设中断号为67,RTE中对应的向量号位108) IA 发EOI 给...
  • 第十讲 51 单片机中断系统 一单片机中断系统 1. 中断的定义 所谓中断是指 CPU 正在处理某些事务的时候 外部又发生了某一事件请求 CPU 及时处理于 是 CPU 暂时中断当前的工作转而处理所发生的 事件处理完毕再回到原来...
  • 中断触发流程

    千次阅读 2012-12-28 09:08:07
    产生中断的每个设备都有一个相应的中断处理程序,中断处理程序通常不和特定的设备关联,而是和特定的中断关联的,也就是说,如果一个设备可以 产生多种不同的中断,那么该就可以对应多个中断处理程序,相应的,该...
  • STM8的中断系统以及外部中断详解

    万次阅读 2014-10-17 00:30:11
    STM8具有最多32的中断系统,在中断的处理上类似于cortexm系列的芯片,首先是每个中断的向量都是固化在系统内部的,用户需要向相应的中断向量flash位置写入中断处理函数,其二,每个中断向量都具有可编程的优先级,共有两位...
  • ARM中断处理流程

    千次阅读 2017-06-17 22:52:07
    这边文章主要是讲解一下,对ARM_Linux中断处理的一个流程介绍,在底层处理部分,不同的架构会有不一样处理,但Linux中断注册过程是一样的。   一、Linux中断的简易模型: 当一个IRQ产生时,会发生什么? 当一个...
  • 微机原理与接口技术 第6章 MCS-51中断系统 第一节 中断系统概述 二中断源 三中断的分类 四中断嵌套 五中断系统功能 软件排队流程图示例 中断系统功能续 第二节 51单片机的中断系统 能够产生中断申请的来源称为中断源...
  • 前面已经学习了中断的注册过程,下面由一张流程图来看一下当中断发生时的处理流程: 中断发生之后处理流程 a -- 具体的CPU architecture相关模块进行现场保护,然后调用machine driver执行对应的中断处理handler...
  • Linux中断处理流程

    千次阅读 2014-03-08 23:26:50
    为Linux中断系统相关数据结构的关系, irq_desc是一个全局数组,每个中断源对应一个descriptor,其成员handle_irq为每个中断setup时设置的五个函数指针中的一个, handle_level_irq为处理电平信号触发的中断源...
  • DSP2812之中断系统

    千次阅读 2018-11-04 19:53:19
    现对中断系统的执行流程作简要概述: 首先,如果有外设产生中断事件,则寄存器中相应的中断标志位被置1,如果相应的中断使能位被置位,那么外设将向PIE控制器发出一个中断请求。 其次,当外设向PIE控制器发送中断...
  • Exynos4412 中断处理流程详解

    千次阅读 2016-03-19 00:36:03
    Linux 中,当外设触发中断后,大体处理流程如下: a -- 具体CPU architecture相关的模块会进行现场保护,然后调用machine driver对应的中断处理handler; b -- machine driver对应的中断处理handler中会根据硬件的...
  • SylixOS中断流程

    2017-04-17 19:48:38
    本文章分析了ARM架构下SylixOS内核中断流程,共包含三部分: 分析SylixOS运用数组和双向链表实现中断服务函数的注册 阐述ARM硬件中断机制 分析SylixOS中断处理流程  SylixOS中断注册 SylixOS中断注册是通过数组和...
  • DSP 6678的中断系统

    千次阅读 2020-09-02 17:02:28
    1、6678的中断系统 1.1、系统事件 在谈SRIO的中断系统之前,先说一说6678DSP中断系统。C6678的CPU中断是由C66x CorePac Interrupt Controller配置的。该中断控制器(C66x CorePac Interrupt Controller),下文直接...
  • 中断系统 2017/11/21 23:30 参考: http://blog.csdn.net/u010479322/article/details/51447181 http://blog.51cto.com/9291927/1787523 1.异常向量表分析: (1)、复位(RESET)  a、当处理器...
  • 4.中断处理流程分析

    2019-09-21 12:18:08
    4.中断处理流程分析 在CPU工作的 过程中,经常需要与外设进行交互,交互的方式包括"轮询方式"和"中断方式"。 轮询方式: CPU不断地查询设备的状态。该方式实现比较简单,但是CPU的利用率很低,不适合多...
  • CPU检测中断CPU在执行每条程序之前会检测是否有中断到达,即中断控制器是否有发送中断信号过来查找IDTCPU根据中断向量到IDT中读取对应的中断描述符表项,根据段选择符合偏移确定中断服务程序的地址见附录2interrupt...
  • X86中断系统-中断源及PIC

    千次阅读 2016-10-26 22:26:07
     x86系统支持256个中断源,每个中断源使用0~255数字标识,该标识称作中断向量号(即CPU中断源的中断号,要与外部中断中断号IRQ n相区别),CPU通过获取中断向量号识别中断源。256个中断源可以分为: 其中: ...
  • F28335中断系统详解及其应用

    千次阅读 2020-08-06 15:36:41
    最近导师的项目中要应用到DSP F28335,由于之前一直对f28335的中断不是很理解,所以花了一点时间仔细看了一下《手把手教你学DSP:基于TMS28335》这本书讲解中断的环节,感觉有一定的收获,在此记录下来。 1 什么是...
  • 3.使能系统配置时钟SYSCFG 4.配置中断线,映射到相应GPIO端口 5.外部中断初始化,包括中断线,中断触发方式,中断模式和使能 6.配置中断抢占优先级和子优先级,配置之前在主函数比较靠前的位置先设置分组 7.编写中断...
  • arm_linux中断系统

    千次阅读 2018-04-20 15:22:37
     Linux中断系统4个部分:普通外设驱动、Linux kernel通用中断处理模块(硬件无关代码)、CPU架构相关处理、中断控制器驱动代码 1.1 Linux 中断系统分层 1.3. 两种中断请求(ARM):IRQ和FIQ1.4. 几个...
  • ARM-异常及中断处理中断中断中断中断/异常优先级异常和中断的区别ARM-7种异常类型ARM-异常中断的响应流程中断 汇编测试代码 中断 中断 1) 硬中断 在处理器中,中断是一个过程。即CPU在正常执行程序的过程中,...
  • 举一个生活例子来说明:小明在厨房干活,流程是烧水——洗菜——切菜——煮饭——煮汤。烧水需要10分钟,当小明加完水点火后,需要等10分钟才能烧完水。此时小明为了提高效率,不应该白白地10分钟,于是小明继续洗菜...
  • Linux内核中断系统处理机制-详细分析

    万次阅读 多人点赞 2018-08-23 23:09:24
    日期 ...中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续...
  • 中断在不同的系统中有不同的处理方式,在linux系统中有signal函数处理系统产生的中断,在汇编语言中通 mov ax,4c00hint 21h 可以看出不同的平台对中断,有不同对应的处理方式,相互之间存在一定差异,今天讨论...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 88,758
精华内容 35,503
关键字:

中断系统流程图