中斷喚醒系統流程


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代碼了。假設部分代碼如下:

 

  1. static irqreturn_t smcdef_event_handler(int irq, void *private)
  2. {
  3. /* do something you want, like report input events through input subsystem */
  4.  
  5. return IRQ_HANDLED;
  6. }
  7.  
  8. static int smcdef_suspend(struct device *dev)
  9. {
  10. enable_irq_wake(irq);
  11. }
  12.  
  13. static int smcdef_resume(struct device *dev)
  14. {
  15. disable_irq_wake(irq);
  16. }
  17.  
  18. static int smcdef_probe(struct i2c_client *client,
  19. const struct i2c_device_id *id)
  20. {
  21. /* ... */
  22. request_thread_irq(irq,
  23. smcdef_event_handler,
  24. NULL,
  25. IRQF_TRIGGER_FALLING,
  26. "smcdef",
  27. pdata);
  28.  
  29. return 0;
  30. }
  31.  
  32. static int smcdef_remove(struct i2c_client *client)
  33. {
  34. return 0;
  35. }
  36.  
  37. static const struct of_device_id smcdef_dt_ids[] = {
  38. {.compatible = "wowo,smcdef" },
  39. { }
  40. };
  41. MODULE_DEVICE_TABLE(of, smcdef_dt_ids);
  42.  
  43. static SIMPLE_DEV_PM_OPS(smcdef_pm_ops, smcdef_suspend, smcdef_resume);
  44.  
  45. static struct i2c_driver smcdef_driver = {
  46. .driver = {
  47. .name = "smcdef",
  48. .of_match_table = of_match_ptr(smcdef_dt_ids),
  49. .pm = &smcdef_pm_ops,
  50. },
  51. .probe = smcdef_probe,
  52. .remove = smcdef_remove,
  53. };
  54. module_i2c_driver(smcdef_driver);
  55.  
  56. MODULE_AUTHOR("smcdef");
  57. MODULE_DESCRIPTION("IRQ test");
  58. 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中完成,代碼如下:

 

  1. int irq_set_irq_wake(unsigned int irq, unsigned int on)
  2. {
  3. unsigned long flags;
  4. struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, IRQ_GET_DESC_CHECK_GLOBAL);
  5. int ret = 0;
  6.  
  7. /* wakeup-capable irqs can be shared between drivers that
  8. * don't need to have the same sleep mode behaviors.
  9. */
  10. if (on) {
  11. if (desc->wake_depth++ == 0) {
  12. ret = set_irq_wake_real(irq, on);
  13. if (ret)
  14. desc->wake_depth = 0;
  15. else
  16. irqd_set(&desc->irq_data, IRQD_WAKEUP_STATE);
  17. }
  18. } else {
  19. if (desc->wake_depth == 0) {
  20. WARN(1, "Unbalanced IRQ %d wake disable\n", irq);
  21. } else if (--desc->wake_depth == 0) {
  22. ret = set_irq_wake_real(irq, on);
  23. if (ret)
  24. desc->wake_depth = 1;
  25. else
  26. irqd_clear(&desc->irq_data, IRQD_WAKEUP_STATE);
  27. }
  28. }
  29. irq_put_desc_busunlock(desc, flags);
  30. return ret;
  31. }

 

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()代碼如下:

  1. void handle_edge_irq(struct irq_desc *desc)
  2. {
  3. raw_spin_lock(&desc->lock);
  4.  
  5. desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
  6.  
  7. if (!irq_may_run(desc)) {
  8. desc->istate |= IRQS_PENDING;
  9. mask_ack_irq(desc);
  10. goto out_unlock;
  11. }
  12.  
  13. /*
  14. * If its disabled or no action available then mask it and get
  15. * out of here.
  16. */
  17. if (irqd_irq_disabled(&desc->irq_data) || !desc->action) {
  18. desc->istate |= IRQS_PENDING;
  19. mask_ack_irq(desc);
  20. goto out_unlock;
  21. }
  22.  
  23. kstat_incr_irqs_this_cpu(desc);
  24.  
  25. /* Start handling the irq */
  26. desc->irq_data.chip->irq_ack(&desc->irq_data);
  27.  
  28. do {
  29. if (unlikely(!desc->action)) {
  30. mask_irq(desc);
  31. goto out_unlock;
  32. }
  33.  
  34. /*
  35. * When another irq arrived while we were handling
  36. * one, we could have masked the irq.
  37. * Renable it, if it was not disabled in meantime.
  38. */
  39. if (unlikely(desc->istate & IRQS_PENDING)) {
  40. if (!irqd_irq_disabled(&desc->irq_data) &&
  41. irqd_irq_masked(&desc->irq_data))
  42. unmask_irq(desc);
  43. }
  44.  
  45. handle_irq_event(desc);
  46.  
  47. } while ((desc->istate & IRQS_PENDING) &&
  48. !irqd_irq_disabled(&desc->irq_data));
  49.  
  50. out_unlock:
  51. raw_spin_unlock(&desc->lock);
  52. }

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代碼如下:

 

  1. void check_irq_resend(struct irq_desc *desc)
  2. {
  3. /*
  4. * We do not resend level type interrupts. Level type
  5. * interrupts are resent by hardware when they are still
  6. * active. Clear the pending bit so suspend/resume does not
  7. * get confused.
  8. */
  9. if (irq_settings_is_level(desc)) {
  10. desc->istate &= ~IRQS_PENDING;
  11. return;
  12. }
  13. if (desc->istate & IRQS_REPLAY)
  14. return;
  15. if (desc->istate & IRQS_PENDING) {
  16. desc->istate &= ~IRQS_PENDING;
  17. desc->istate |= IRQS_REPLAY;
  18.  
  19. if (!desc->irq_data.chip->irq_retrigger ||
  20. !desc->irq_data.chip->irq_retrigger(&desc->irq_data)) {
  21.  
  22. unsigned int irq = irq_desc_get_irq(desc);
  23.  
  24. /* Set it pending and activate the softirq: */
  25. set_bit(irq, irqs_resend);
  26. tasklet_schedule(&resend_tasklet);
  27. }
  28. }
  29. }

 

由於在之前分析已經設置了IRQS_PENDING flag,因此這里會tasklet_schedule(&resend_tasklet)並且置位irqs_resend變量中相應的bit位,代表軟中斷觸發。然后就開始tasklet_schedule最終會喚醒ksoftirqd線程,在ksoftirqd線程中會調用你注冊的中斷handle,畢竟ksoftirqd線程優先級很高,所以很快就會調度了。具體調用過程可以參考wowo的softirq和tasklet文章。這里我們也可以得出中斷handle執行的上下文環境是軟中斷上下文的結論。當然我們還是有必要分析一下tasklet最后一步resend_irqs()函數的作用,代碼如下:

 

  1. /* Bitmap to handle software resend of interrupts: */
  2. static DECLARE_BITMAP(irqs_resend, IRQ_BITMAP_BITS);
  3.  
  4. /*
  5. * Run software resends of IRQ's
  6. */
  7. static void resend_irqs(unsigned long arg)
  8. {
  9. struct irq_desc *desc;
  10. int irq;
  11.  
  12. while (!bitmap_empty(irqs_resend, nr_irqs)) {
  13. irq = find_first_bit(irqs_resend, nr_irqs);
  14. clear_bit(irq, irqs_resend);
  15. desc = irq_to_desc(irq);
  16. local_irq_disable();
  17. desc->handle_irq(desc);
  18. local_irq_enable();
  19. }
  20. }
  21. /* Tasklet to handle resend: */
  22. 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()代碼如下:

 

  1. void synchronize_irq(unsigned int irq)
  2. {
  3. struct irq_desc *desc = irq_to_desc(irq);
  4.  
  5. if (desc) {
  6. __synchronize_hardirq(desc);
  7. /*
  8. * We made sure that no hardirq handler is
  9. * running. Now verify that no threaded handlers are
  10. * active.
  11. */
  12. wait_event(desc->wait_for_threads,
  13. !atomic_read(&desc->threads_active));
  14. }
  15. }

 

1) __synchronize_hardirq()是等待hardirq handler執行完畢。
2) 只要threads_active計數不為0就,等待threaded handler執行完畢。

__synchronize_hardirq()代碼如下:

 

  1. static void __synchronize_hardirq(struct irq_desc *desc)
  2. {
  3. bool inprogress;
  4.  
  5. do {
  6. unsigned long flags;
  7.  
  8. /*
  9. * Wait until we're out of the critical section. This might
  10. * give the wrong answer due to the lack of memory barriers.
  11. */
  12. while (irqd_irq_inprogress(&desc->irq_data))
  13. cpu_relax();
  14.  
  15. /* Ok, that indicated we're done: double-check carefully. */
  16. raw_spin_lock_irqsave(&desc->lock, flags);
  17. inprogress = irqd_irq_inprogress(&desc->irq_data);
  18. raw_spin_unlock_irqrestore(&desc->lock, flags);
  19.  
  20. /* Oops, that failed? */
  21. } while (inprogress);
  22. }

 

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()函數如下:

 

  1. void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
  2. {
  3. /*
  4. * In case the thread crashed and was killed we just pretend that
  5. * we handled the interrupt. The hardirq handler has disabled the
  6. * device interrupt, so no irq storm is lurking.
  7. */
  8. if (action->thread->flags & PF_EXITING)
  9. return;
  10.  
  11. /*
  12. * Wake up the handler thread for this action. If the
  13. * RUNTHREAD bit is already set, nothing to do.
  14. */
  15. if (test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags))
  16. return;
  17.  
  18.  
  19. desc->threads_oneshot |= action->thread_mask;
  20.  
  21. atomic_inc(&desc->threads_active);
  22.  
  23. wake_up_process(action->thread);
  24. }

 

1) 如果irq的中斷線程已經設置IRQTF_RUNTHREAD標志位,代表irq線程已經正在運行,因此無需重新喚醒,直接返回即可。
2) 使用stomic_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()函數代碼如下:

 

  1. static int setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
  2. {
  3. struct task_struct *t;
  4. struct sched_param param = {
  5. .sched_priority = MAX_USER_RT_PRIO/2,
  6. };
  7.  
  8. if (!secondary) {
  9. t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
  10. } else {
  11. t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name);
  12. param.sched_priority -= 1;
  13. }
  14.  
  15. if (IS_ERR(t))
  16. return PTR_ERR(t);
  17.  
  18. sched_setscheduler_nocheck(t, SCHED_FIFO, &param);
  19.  
  20. get_task_struct(t);
  21. new->thread = t;
  22.  
  23. set_bit(IRQTF_AFFINITY, &new->thread_flags);
  24. return 0;
  25. }

 

通過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摳出部分代碼如下:

 

  1. static void gpio_keys_gpio_work_func(struct work_struct *work)
  2. {
  3. struct gpio_button_data *bdata =
  4. container_of(work, struct gpio_button_data, work.work);
  5.  
  6. gpio_keys_gpio_report_event(bdata);
  7. }
  8.  
  9. static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
  10. {
  11. struct gpio_button_data *bdata = dev_id;
  12.  
  13. mod_delayed_work(system_wq,
  14. &bdata->work,
  15. msecs_to_jiffies(bdata->software_debounce));
  16.  
  17. return IRQ_HANDLED;
  18. }

 

當按鍵按下,中斷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的操作,系統是否也會繼續睡眠呢?修改代碼如下:

 

  1. static void gpio_keys_gpio_work_func(struct work_struct *work)
  2. {
  3. struct gpio_button_data *bdata =
  4. container_of(work, struct gpio_button_data, work.work);
  5.  
  6. msleep(1000);
  7. gpio_keys_gpio_report_event(bdata);
  8. }
  9.  
  10. static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
  11. {
  12. struct gpio_button_data *bdata = dev_id;
  13.  
  14. schedule_work(&bdata->work);
  15.  
  16. return IRQ_HANDLED;
  17. }

 

這里的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節修改后的代碼如下:

 

  1. static void gpio_keys_gpio_work_func(struct work_struct *work)
  2. {
  3. struct gpio_button_data *bdata =
  4. container_of(work, struct gpio_button_data, work.work);
  5.  
  6. gpio_keys_gpio_report_event(bdata);
  7. if (bdata->button->wakeup)
  8. pm_relax(bdata->input->dev.parent);
  9. }
  10.  
  11. static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
  12. {
  13. struct gpio_button_data *bdata = dev_id;
  14.  
  15. if (bdata->button->wakeup) {
  16. const struct gpio_keys_button *button = bdata->button;
  17.  
  18. pm_stay_awake(bdata->input->dev.parent);
  19. }
  20.  
  21. mod_delayed_work(system_wq,
  22. &bdata->work,
  23. msecs_to_jiffies(bdata->software_debounce));
  24.  
  25. return IRQ_HANDLED;
  26. }

 

好了,現在你放心好了,即使你是在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()。否則,誰也無法保證系統不會再一次的睡眠。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM