1. 程式人生 > >Linux中斷(interrupt)子系統之四:驅動程式介面層 & 中斷通用邏輯層

Linux中斷(interrupt)子系統之四:驅動程式介面層 & 中斷通用邏輯層

轉載地址:https://blog.csdn.net/DroidPhone/article/details/7497787

在本系列文章的第一篇:Linux中斷(interrupt)子系統之一:中斷系統基本原理,我把通用中斷子系統分為了4個層次,其中的驅動程式介面層和中斷通用邏輯層的界限實際上不是很明確,因為中斷通用邏輯層的很多介面,既可以被驅動程式使用,也可以被硬體封裝層使用,所以我把這兩部分的內容放在一起進行討論。

本章我將會討論這兩層對外提供的標準介面和內部實現機制,幾乎所有的介面都是圍繞著irq_desc和irq_chip這兩個結構體進行的,對這兩個結構體不熟悉的讀者可以現讀一下前面幾篇文章。

/*****************************************************************************************************/
宣告:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/

1.  irq的開啟和關閉

中斷子系統為我們提供了一系列用於irq的開啟和關閉的函式介面,其中最基本的一對是:

 

  • disable_irq(unsigned int irq);
  • enable_irq(unsigned int irq);

這兩個API應該配對使用,disable_irq可以被多次巢狀呼叫,要想重新開啟irq,enable_irq必須也要被呼叫同樣的次數,為此,irq_desc結構中的depth欄位專門用於這兩個API巢狀深度的管理。當某個irq首次被驅動程式申請時,預設情況下,設定depth的初始值是0,對應的irq處於開啟狀態。我們看看disable_irq的呼叫過程:

 

                                                                             圖1.1  disable_irq的呼叫過程

函式的開始使用非同步方式的內部函式__disable_irq_nosync(),所謂非同步方式就是不理會當前該irq是否正在被處理(有handler在執行或者有中斷執行緒尚未結束)。有些中斷控制器可能掛在某個慢速的總線上,所以在進一步處理前,先通過irq_get_desc_buslock獲得匯流排鎖(最終會呼叫chip->irq_bus_lock),然後進入內部函式__disable_irq:

 

 
  1. void __disable_irq(struct irq_desc *desc, unsigned int irq, bool suspend)

  2. {

  3. if (suspend) {

  4. if (!desc->action || (desc->action->flags & IRQF_NO_SUSPEND))

  5. return;

  6. desc->istate |= IRQS_SUSPENDED;

  7. }

  8.  
  9. if (!desc->depth++)

  10. irq_disable(desc);

  11. }

前面幾句是對suspend的處理,最後兩句,只有之前的depth為0,才會通過irq_disable函式,呼叫中斷控制器的回撥chip->irq_mask,否則只是簡單地把depth的值加1。irq_disable函式還會通過irq_state_set_disabled和irq_state_set_masked,設定irq_data.flag的IRQD_IRQ_DISABLED和IRQD_IRQ_MASK標誌。

 

disable_irq的最後,呼叫了synchronize_irq,該函式通過IRQ_INPROGRESS標誌,確保action連結串列中所有的handler都已經處理完畢,然後還要通過wait_event等待該irq所有的中斷執行緒退出。正因為這樣,在中斷上下文中,不應該使用該API來關閉irq,同時要確保呼叫該API的函式不能擁有該irq處理函式或執行緒的資源,否則就會發生死鎖!!如果一定要在這兩種情況下關閉irq,中斷子系統為我們提供了另外一個API,它不會做出任何等待動作:

 

  • disable_irq_nosync();

中斷子系統開啟irq的的API是:

 

 

  • enable_irq();

開啟irq無需提供同步的版本,因為irq開啟前,沒有handler和執行緒在執行,我們關注一下他對depth的處理,他在內部函式__enable_irq中處理:

 

 

 
  1. void __enable_irq(struct irq_desc *desc, unsigned int irq, bool resume)

  2. {

  3. if (resume) {

  4. ......

  5. }

  6.  
  7. switch (desc->depth) {

  8. case 0:

  9. err_out:

  10. WARN(1, KERN_WARNING "Unbalanced enable for IRQ %d\n", irq);

  11. break;

  12. case 1: {

  13. ......

  14. irq_enable(desc);

  15. ......

  16. }

  17. default:

  18. desc->depth--;

  19. }

  20. }

當depth的值為1時,才真正地呼叫irq_enable(),它最終通過chip->unmask或chip->enable回撥開啟中斷控制器中相應的中斷線,如果depth不是1,只是簡單地減去1。如果已經是0,驅動還要呼叫enable_irq,說明驅動程式處理不當,造成enable與disable不平衡,核心會列印一句警告資訊:Unbalanced enable for IRQ xxx。
 

2.  中斷子系統內部資料結構訪問介面

我們知道,中斷子系統內部定義了幾個重要的資料結構,例如:irq_desc,irq_chip,irq_data等等,這些資料結構的各個欄位控制或影響著中斷子系統和各個irq的行為和實現方式。通常,驅動程式不應該直接訪問這些資料結構,直接訪問會破會中斷子系統的封裝性,為此,中斷子系統為我們提供了一系列的訪問介面函式,用於訪問這些資料結構。

存取irq_data結構相關欄位的API:

        irq_set_chip(irq, *chip) / irq_get_chip(irq)  通過irq編號,設定、獲取irq_cip結構指標;

        irq_set_handler_data(irq, *data) / irq_get_handler_data(irq)  通過irq編號,設定、獲取irq_desc.irq_data.handler_data欄位,該欄位是每個irq的私有資料,通常用於硬體封裝層,例如中斷控制器級聯時,父irq用該欄位儲存子irq的起始編號。

        irq_set_chip_data(irq, *data) / irq_get_chip_data(irq)  通過irq編號,設定、獲取irq_desc.irq_data.chip_data欄位,該欄位是每個中斷控制器的私有資料,通常用於硬體封裝層。

        irq_set_irq_type(irq, type)  用於設定中斷的電氣型別,可選的型別有:

 

 

  • IRQ_TYPE_EDGE_RISING
  • IRQ_TYPE_EDGE_FALLING
  • IRQ_TYPE_EDGE_BOTH
  • IRQ_TYPE_LEVEL_HIGH
  • IRQ_TYPE_LEVEL_LOW

 

 

        irq_get_irq_data(irq)  通過irq編號,獲取irq_data結構指標;

        irq_data_get_irq_chip(irq_data *d)  通過irq_data指標,獲取irq_chip欄位;

        irq_data_get_irq_chip_data(irq_data *d)  通過irq_data指標,獲取chip_data欄位;

        irq_data_get_irq_handler_data(irq_data *d)  通過irq_data指標,獲取handler_data欄位;

設定中斷流控處理回撥API:

        irq_set_handler(irq, handle)  設定中斷流控回撥欄位:irq_desc.handle_irq,引數handle的型別是irq_flow_handler_t。

        irq_set_chip_and_handler(irq, *chip, handle)  同時設定中斷流控回撥欄位和irq_chip指標:irq_desc.handle_irq和irq_desc.irq_data.chip。

        irq_set_chip_and_handler_name(irq, *chip, handle, *name)  同時設定中斷流控回撥欄位和irq_chip指標以及irq名字:irq_desc.handle_irq、irq_desc.irq_data.chip、irq_desc.name。

        irq_set_chained_handler(irq, *chip, handle)  設定中斷流控回撥欄位:irq_desc.handle_irq,同時設定標誌:IRQ_NOREQUEST、IRQ_NOPROBE、IRQ_NOTHREAD,該api通常用於中斷控制器的級聯,父控制器通過該api設定流控回撥後,同時設定上述三個標誌位,使得父控制器的中斷線不允許被驅動程式申請。

 

3.  在驅動程式中申請中斷

 

系統啟動階段,中斷子系統完成了必要的初始化工作,為驅動程式申請中斷服務做好了準備,通常,我們用一下API申請中斷服務:

 

 
  1. request_threaded_irq(unsigned int irq, irq_handler_t handler,

  2. irq_handler_t thread_fn,

  3. unsigned long flags, const char *name, void *dev);

        irq  需要申請的irq編號,對於ARM體系,irq編號通常在平臺級的程式碼中事先定義好,有時候也可以動態申請。

 

        handler  中斷服務回撥函式,該回調執行在中斷上下文中,並且cpu的本地中斷處於關閉狀態,所以該回調函式應該只是執行需要快速響應的操作,執行時間應該儘可能短小,耗時的工作最好留給下面的thread_fn回撥處理。

        thread_fn  如果該引數不為NULL,核心會為該irq建立一個核心執行緒,當中斷髮生時,如果handler回撥返回值是IRQ_WAKE_THREAD,核心將會啟用中斷執行緒,在中斷執行緒中,該回調函式將被呼叫,所以,該回調函式執行在程序上下文中,允許進行阻塞操作。

        flags  控制中斷行為的位標誌,IRQF_XXXX,例如:IRQF_TRIGGER_RISING,IRQF_TRIGGER_LOW,IRQF_SHARED等,在include/linux/interrupt.h中定義。

        name  申請本中斷服務的裝置名稱,同時也作為中斷執行緒的名稱,該名稱可以在/proc/interrupts檔案中顯示。

        dev  當多個裝置的中斷線共享同一個irq時,它會作為handler的引數,用於區分不同的裝置。

下面我們分析一下request_threaded_irq的工作流程。函式先是根據irq編號取出對應的irq_desc例項的指標,然後分配了一個irqaction結構,用引數handler,thread_fn,irqflags,devname,dev_id初始化irqaction結構的各欄位,同時做了一些必要的條件判斷:該irq是否禁止申請?handler和thread_fn不允許同時為NULL,最後把大部分工作委託給__setup_irq函式:

 

 
  1. desc = irq_to_desc(irq);

  2. if (!desc)

  3. return -EINVAL;

  4.  
  5. if (!irq_settings_can_request(desc) ||

  6. WARN_ON(irq_settings_is_per_cpu_devid(desc)))

  7. return -EINVAL;

  8.  
  9. if (!handler) {

  10. if (!thread_fn)

  11. return -EINVAL;

  12. handler = irq_default_primary_handler;

  13. }

  14.  
  15. action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);

  16. if (!action)

  17. return -ENOMEM;

  18.  
  19. action->handler = handler;

  20. action->thread_fn = thread_fn;

  21. action->flags = irqflags;

  22. action->name = devname;

  23. action->dev_id = dev_id;

  24.  
  25. chip_bus_lock(desc);

  26. retval = __setup_irq(irq, desc, action);

  27. chip_bus_sync_unlock(desc);

進入__setup_irq函式,如果引數flag中設定了IRQF_SAMPLE_RANDOM標誌,它會呼叫rand_initialize_irq,以便對隨機數的生成產生影響。如果申請的不是一個執行緒巢狀中斷(關於執行緒巢狀中斷,請參閱Linux中斷(interrupt)子系統之三:中斷流控處理層中的handle_nested_irq一節),而且提供了thread_fn引數,它將建立一個核心執行緒:

 
  1. if (new->thread_fn && !nested) {

  2. struct task_struct *t;

  3.  
  4. t = kthread_create(irq_thread, new, "irq/%d-%s", irq,

  5. new->name);

  6. if (IS_ERR(t)) {

  7. ret = PTR_ERR(t);

  8. goto out_mput;

  9. }

  10. /*

  11. * We keep the reference to the task struct even if

  12. * the thread dies to avoid that the interrupt code

  13. * references an already freed task_struct.

  14. */

  15. get_task_struct(t);

  16. new->thread = t;

  17. }

如果irq_desc結構中斷action連結串列不為空,說明這個irq已經被其它裝置申請過,也就是說,這是一個共享中斷,所以接下來會判斷這個新申請的中斷與已經申請的舊中斷的以下幾個標誌是否一致:

 

 

  • 一定要設定了IRQF_SHARED標誌
  • 電氣觸發方式要完全一樣(IRQF_TRIGGER_XXXX)
  • IRQF_PERCPU要一致
  • IRQF_ONESHOT要一致

檢查這些條件都是因為多個裝置試圖共享一根中斷線,試想一下,如果一個裝置要求上升沿中斷,一個裝置要求電平中斷,當中斷到達時,核心將不知如何選擇合適的流控操作。完成檢查後,函式找出action連結串列中最後一個irqaction例項的指標。

 

 

 
  1. /* add new interrupt at end of irq queue */

  2. do {

  3. thread_mask |= old->thread_mask;

  4. old_ptr = &old->next;

  5. old = *old_ptr;

  6. } while (old);

  7. shared = 1;

如果這不是一個共享中斷,或者是共享中斷的第一次申請,函式將初始化irq_desc結構中斷執行緒等待結構:wait_for_threads,disable_irq函式會使用該欄位等待所有irq執行緒的結束。接下來設定中斷控制器的電氣觸發型別,然後處理一些必要的IRQF_XXXX標誌位。如果沒有設定IRQF_NOAUTOEN標誌,則呼叫irq_startup()開啟該irq,在irq_startup()函式中irq_desc中的enable_irq/disable_irq巢狀深度欄位depth設定為0,代表該irq已經開啟,如果在沒有任何disable_irq被呼叫的情況下,enable_irq將會列印一個警告資訊。

 

 

 
  1. if (irq_settings_can_autoenable(desc))

  2. irq_startup(desc);

  3. else

  4. /* Undo nested disables: */

  5. desc->depth = 1;

接著,設定cpu和irq的親緣關係:

 

 

 
  1. /* Set default affinity mask once everything is setup */

  2. setup_affinity(irq, desc, mask);

然後,把新的irqaction例項連結到action連結串列的最後:

 

 

 
  1. new->irq = irq;

  2. *old_ptr = new;

最後,喚醒中斷執行緒,註冊相關的/proc檔案節點:

 

 

 
  1. if (new->thread)

  2. wake_up_process(new->thread);

  3.  
  4. register_irq_proc(irq, desc);

  5. new->dir = NULL;

  6. register_handler_proc(irq, new);

至此,irq的申請宣告完畢,當中斷髮生時,處理的路徑將會沿著:irq_desc.handle_irq,irqaction.handler,irqaction.thread_fn(irqaction.handler的返回值是IRQ_WAKE_THREAD)這個過程進行處理。下圖表明瞭某個irq被申請後,各個資料結構之間的關係:

 

                                                     圖3.1  irq各個資料結構之間的關係

4.  動態擴充套件irq編號

在ARM體系的移動裝置中,irq的編號通常在平臺級或板級程式碼中事先根據硬體的連線定義好,最大的irq數目也用NR_IRQS常量指定。幾種情況下,我們希望能夠動態地增加系統中irq的數量:

  • 配置了CONFIG_SPARSE_IRQ核心配置項,使用基數樹動態管理irq_desc結構。
  • 針對多功能複合裝置,內部具備多箇中斷源,但中斷觸發引腳只有一個,為了實現驅動程式的跨平臺,不希望這些中斷源的irq被硬編碼在板級程式碼中。

中斷子系統為我們提供了以下幾個api,用於動態申請/擴充套件irq編號:

        irq_alloc_desc(node)  申請一個irq,node是對應記憶體節點的編號;

        irq_alloc_desc_at(at, node)  在指定位置申請一個irq,如果指定位置已經被佔用,則申請失敗;

        irq_alloc_desc_from(from, node)  從指定位置開始搜尋,申請一個irq;

        irq_alloc_descs(irq, from, cnt, node)  申請多個連續的irq編號,從from位置開始搜尋;

        irq_free_descs(irq, cnt)  釋放irq資源;

以上這些申請函式(巨集),會為我們申請相應的irq_desc結構並初始化為預設狀態,要想這些irq能夠正常工作,我們還要使用第二節提到的api,對必要的欄位進行設定,例如:

  • irq_set_chip_and_handler_name
  • irq_set_handler_data
  • irq_set_chip_data

對於沒有配置CONFIG_SPARSE_IRQ核心配置項的核心,irq_desc是一個數組,根本不可能做到動態擴充套件,但是很多驅動又確實使用到了上述api,尤其是mfd驅動,這些驅動並沒有我們一定要配置CONFIG_SPARSE_IRQ選項,要想不對這些驅動做出修改,你只能妥協一下,在你的板級程式碼中把NR_IRQS定義得大一些,留出足夠的保留數量

5.  多功能複合裝置的中斷處理

在移動裝置系統中,存在著大量的多功能複合裝置,最常見的是一個晶片中,內部集成了多個功能部件,或者是一個模組單元內部集成了功能部件,這些內部功能部件可以各自產生中斷請求,但是晶片或者硬體模組對外只有一箇中斷請求引腳,我們可以使用多種方式處理這些裝置的中斷請求,以下我們逐一討論這些方法。

 

5.1  單一中斷模式 

       對於這種複合裝置,通常裝置中會提供某種方式,以便讓CPU獲取真正的中斷來源, 方式可以是一個內部暫存器,gpio的狀態等等。單一中斷模式是指驅動程式只申請一個irq,然後在中斷處理程式中通過讀取裝置的內部暫存器,獲取中斷源,然後根據不同的中斷源做出不同的處理,以下是一個簡化後的程式碼:

 

 
  1. static int xxx_probe(device *dev)

  2. {

  3. ......

  4. irq = get_irq_from_dev(dev);

  5.  
  6. ret = request_threaded_irq(irq, NULL, xxx_irq_thread,

  7. IRQF_TRIGGER_RISING,

  8. "xxx_dev", NULL);

  9. ......

  10. return 0;

  11. }

  12.  
  13. static irqreturn_t xxx_irq_thread(int irq, void *data)

  14. {

  15. ......

  16. irq_src = read_device_irq();

  17. switch (irq_src) {

  18. case IRQ_SUB_DEV0:

  19. ret = handle_sub_dev0_irq();

  20. break;

  21. case IRQ_SUB_DEV1:

  22. ret = handle_sub_dev1_irq();

  23. break;

  24. ......

  25. default:

  26. ret = IRQ_NONE;

  27. break;

  28. }

  29. ......

  30. return ret;

  31. }

 

5.2  共享中斷模式

共享中斷模式充分利用了通用中斷子系統的特性,經過前面的討論,我們知道,irq對應的irq_desc結構中的action欄位,本質上是一個連結串列,這給我們實現中斷共享提供了必要的基礎,只要我們以相同的irq編號多次申請中斷服務,那麼,action連結串列上就會有多個irqaction例項,當中斷髮生時,中斷子系統會遍歷action連結串列,逐個執行irqaction例項中的handler回撥,根據handler回撥的返回值不同,決定是否喚醒中斷執行緒。需要注意到是,申請多箇中斷時,irq編號要保持一致,flag引數最好也能保持一致,並且都要設上IRQF_SHARED標誌。在使用共享中斷時,最好handler和thread_fn都要提供,在各自的中斷處理回撥handler中,做出以下處理:

 

  • 判斷中斷是否來自本裝置;
  • 如果不是來自本裝置:
    • 直接返回IRQ_NONE;
  • 如果是來自本裝置:
    • 關閉irq;
    • 返回IRQ_WAKE_THREAD,喚醒中斷執行緒,thread_fn將會被執行;

5.3  中斷控制器級聯模式

多數多功能複合裝置內部提供了基本的中斷控制器功能,例如可以單獨地控制某個子中斷的開啟和關閉,並且可以方便地獲得子中斷源,對於這種裝置,我們可以把裝置內的中斷控制器實現為一個子控制器,然後使用中斷控制器級聯模式。這種模式下,各個子裝置擁有各自獨立的irq編號,中斷服務通過父中斷進行分發。

對於父中斷,具體的實現步驟如下:

  • 首先,父中斷的irq編號可以從板級程式碼的預定義中獲得,或者通過device的platform_data欄位獲得;
  • 使用父中斷的irq編號,利用irq_set_chained_handler函式修改父中斷的流控函式;
  • 使用父中斷的irq編號,利用irq_set_handler_data設定流控函式的引數,該引數要能夠用於判別子控制器的中斷來源;
  • 實現父中斷的流控函式,其中只需獲得並計運算元裝置的irq編號,然後呼叫generic_handle_irq即可;

對於子裝置,具體的實現步驟如下

  • 為裝置內的中斷控制器實現一個irq_chip結構,實現其中必要的回撥,例如irq_mask,irq_unmask,irq_ack等;
  • 迴圈每一個子裝置,做以下動作:
    • 為每個子裝置,使用irq_alloc_descs函式申請irq編號;
    • 使用irq_set_chip_data設定必要的cookie資料;
    • 使用irq_set_chip_and_handler設定子控制器的irq_chip例項和子irq的流控處理程式,通常使用標準的流控函式,例如handle_edge_irq;
  • 子裝置的驅動程式使用自身申請到的irq編號,按照正常流程申請中斷服務即可。

5.4  中斷執行緒巢狀模式

該模式與中斷控制器級聯模式大體相似,只不過級聯模式時,父中斷無需通過request_threaded_irq申請中斷服務,而是直接更換了父中斷的流控回撥,在父中斷的流控回撥中實現子中斷的二次分發。但是這在有些情況下會給我們帶來不便,因為流控回撥要獲取子控制器的中斷源,而流控回撥執行在中斷上下文中,對於那些子控制器需要通過慢速匯流排訪問的裝置,在中斷上下文中訪問顯然不太合適,這時我們可以把子中斷分發放在父中斷的中斷執行緒中進行,這就是我所說的所謂中斷執行緒巢狀模式。下面是大概的實現過程:

對於父中斷,具體的實現步驟如下:

  • 首先,父中斷的irq編號可以從板級程式碼的預定義中獲得,或者通過device的platform_data欄位獲得;
  • 使用父中斷的irq編號,利用request_threaded_irq函式申請中斷服務,需要提供thread_fn引數和dev_id引數;
  • dev_id引數要能夠用於判別子控制器的中斷來源;
  • 實現父中斷的thread_fn函式,其中只需獲得並計運算元裝置的irq編號,然後呼叫handle_nested_irq即可;

對於子裝置,具體的實現步驟如下

  • 為裝置內的中斷控制器實現一個irq_chip結構,實現其中必要的回撥,例如irq_mask,irq_unmask,irq_ack等;
  • 迴圈每一個子裝置,做以下動作:
    • 為每個子裝置,使用irq_alloc_descs函式申請irq編號;
    • 使用irq_set_chip_data設定必要的cookie資料;
    • 使用irq_set_chip_and_handler設定子控制器的irq_chip例項和子irq的流控處理程式,通常使用標準的流控函式,例如handle_edge_irq;
    • 使用irq_set_nested_thread函式,把子裝置irq的執行緒巢狀特性開啟;
  • 子裝置的驅動程式使用自身申請到的irq編號,按照正常流程申請中斷服務即可。

應為子裝置irq的執行緒巢狀特性被開啟,使用request_threaded_irq申請子裝置的中斷服務時,即是是提供了handler引數,中斷子系統也不會使用它,同時也不會為它建立中斷執行緒,子裝置的thread_fn回撥是在父中斷的中斷執行緒中,通過handle_nested_irq呼叫的,也就是說,儘管子中斷有自己獨立的irq編號,但是它們沒有獨立的中斷執行緒,只是共享了父中斷的中斷服務執行緒。