在本系列文章的第一篇: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:
點擊(此處)折疊或打開
void __disable_irq(struct irq_desc *desc, unsigned int irq, bool
suspend)
{
if (suspend) {
if (!desc->action || (desc->action->flags & IRQF_NO_SUSPEND))
return;
desc->istate |= IRQS_SUSPENDED;
}
if (!desc->depth++)
irq_disable(desc);
}
前面幾句是對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中處理:
點擊(此處)折疊或打開
void __enable_irq(struct irq_desc *desc, unsigned int irq, bool resume)
{
if (resume) {
......
}
switch (desc->depth) {
case 0:
err_out:
WARN(1, KERN_WARNING "Unbalanced enable for IRQ %d\n", irq);
break;
case 1: {
......
irq_enable(desc);
......
}
default:
desc->depth--;
}
}
當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申請中斷服務:
點擊(此處)折疊或打開
request_threaded_irq(unsigned int irq, irq_handler_t
handler,
irq_handler_t thread_fn,
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函數:
點擊(此處)折疊或打開
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
chip_bus_lock(desc);
retval = __setup_irq(irq, desc, action);
chip_bus_sync_unlock(desc);
進入__setup_irq函數,如果參數flag中設置了IRQF_SAMPLE_RANDOM標志,它會調用rand_initialize_irq,以便對隨機數的生成產生影響。如果申請的不是一個線程嵌套中斷(關於線程嵌套中斷,請參閱Linux中斷(interrupt)子系統之三:中斷流控處理層中的handle_nested_irq一節),而且提供了thread_fn參數,它將創建一個內核線程:
點擊(此處)折疊或打開
if (new->thread_fn && !nested) {
struct task_struct *t;
t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
new->name);
if (IS_ERR(t)) {
ret = PTR_ERR(t);
goto out_mput;
}
/*
* We keep the reference to the task struct even if
* the thread dies to avoid that the interrupt code
* references an already freed task_struct.
*/
get_task_struct(t);
new->thread = t;
}
如果irq_desc結構中斷action鏈表不為空,說明這個irq已經被其它設備申請過,也就是說,這是一個共享中斷,所以接下來會判斷這個新申請的中斷與已經申請的舊中斷的以下幾個標志是否一致:
一定要設置了IRQF_SHARED標志
電氣觸發方式要完全一樣(IRQF_TRIGGER_XXXX)
IRQF_PERCPU要一致
IRQF_ONESHOT要一致
檢查這些條件都是因為多個設備試圖共享一根中斷線,試想一下,如果一個設備要求上升沿中斷,一個設備要求電平中斷,當中斷到達時,內核將不知如何選擇合適的流控操作。完成檢查後,函數找出action鏈表中最後一個irqaction實例的指針。
點擊(此處)折疊或打開
/* add new interrupt at end of
irq queue */
do {
thread_mask |= old->thread_mask;
old_ptr = &old->next;
old = *old_ptr;
} while (old);
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將會打印一個警告信息。
點擊(此處)折疊或打開
if (irq_settings_can_autoenable(desc))
irq_startup(desc);
else
/* Undo nested disables: */
desc->depth = 1;
接著,設置cpu和irq的親緣關系:
點擊(此處)折疊或打開
/* Set default affinity
mask once everything is setup */
setup_affinity(irq, desc, mask);
然後,把新的irqaction實例鏈接到action鏈表的最後:
點擊(此處)折疊或打開
new->irq = irq;
*old_ptr = new;
最後,喚醒中斷線程,注冊相關的/proc文件節點:
點擊(此處)折疊或打開
if (new->thread)
wake_up_process(new->thread);
register_irq_proc(irq, desc);
new->dir = NULL;
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,然後在中斷處理程序中通過讀取設備的內部寄存器,獲取中斷源,然後根據不同的中斷源做出不同的處理,以下是一個簡化後的代碼:
點擊(此處)折疊或打開
static int xxx_probe(device *dev)
{
......
irq = get_irq_from_dev(dev);
ret = request_threaded_irq(irq, NULL, xxx_irq_thread,
IRQF_TRIGGER_RISING,
"xxx_dev", NULL);
......
return 0;
}
static irqreturn_t xxx_irq_thread(int irq, void *data)
{
......
irq_src = read_device_irq();
switch (irq_src) {
case IRQ_SUB_DEV0:
ret = handle_sub_dev0_irq();
break;
case IRQ_SUB_DEV1:
ret = handle_sub_dev1_irq();
break;
......
default:
ret = IRQ_NONE;
break;
}
......
return ret;
}
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編號,但是它們沒有獨立的中斷線程,只是共享了父中斷的中斷服務線程