1. 程式人生 > 其它 >九.GPIO中斷試驗2——通用中斷服務程式構成

九.GPIO中斷試驗2——通用中斷服務程式構成

在上一章我們大概講了中斷原理,並且在放出來彙編的中斷向量表和預留的中斷服務函式,下面我們就要結合前面學過的知識完善這些中斷服務函式。

復位中斷函式

I.MX6U在上電開始或復位的時候就會呼叫這個復位中斷,在這個中斷要做的工作有:

  1. 關閉全域性終端
  2. 關閉I Cache、D Cache、MMU
  3. 設定中斷向量表偏移量(非必要,可以放在C環境下執行)
  4. 設定各個模式下的棧指標:要注意點是I.MX6UL的堆疊是向下增長的,在設定堆疊指標的時候一定要注意4位元組對齊,不要設定個類似0x00000003種不是4的倍數這種的地址,注意DDR的地址範圍是0x80000000到0x9FFFFFFF(512MB)或0x8FFFFFFF(256MB)
  5. 使能全域性中斷

上面的流程在後面講UBoot的時候會分析,這裡只放結論,下面我們一步步來說明

關閉中斷

全域性終端的關閉只需要通過指令即可完成

cpsid i

關閉I/DCache、MMU

關閉I Cache、D Cache和MMU的操作要藉助CP15協處理器的c1暫存器來完成,而c1暫存器應反射為SCTLR暫存器,《Cortex-A7 Technical ReferenceManua.pdf》4.3.27裡還直接給出了這個暫存器的讀寫指令

我們採用讀——改——寫的方式,將SCTLR暫存器裡的值複製到R0暫存器裡,只休改需要修改的bit,完成後再寫入SCTLR即可。

設定各個模式

中斷向量偏移

中斷向量偏移的設定要通過CP15的c12對應的VBVR暫存器來操作

對應的引數手冊裡也都給出來了,把偏移的地址給他就行了。這裡加個附加指令:DSB和ISB用來同步資料清洗流水線,保證前面的指令都執行完成後才執行後面的指令。

設定各個模式下的棧指標

從下面的圖可以看出來,除了User模式和Sys模式,每個工作模式都是用自己的sp指標,所以我們需要進入每個工作模式設定對應的sp指標。

我們這一章主要用IRQ,這個sp_IRQ是必須要設定的。做的時候就是修改CPSR暫存器的bit[4:0],進入對應的模式然後給指標賦值就可以了

使能中斷

中斷等使能也是直接一個指令就可以了,也可以像前面講原理的時候說的修改CPSR對應的bit。

程式碼結構

下面的程式碼就是整個中斷復位函式

/*復位中斷服務函式 */
Reset_Handler:

    CPSID i                 @禁止全域性中斷

/*操作CP15協處理器,關閉ICache DCache,MMU*/

    MRC p15,0,r0,c1,c0,0    @讀取SCTLR暫存器的資料到R0暫存器中

    BIC r0,r0,#(1<<12)       @清零bit12,關閉I Cache
    BIC r0,r0,#(1<<11)       @清零bit11,關閉分支預測
    BIC r0,r0,#(1<<2)        @清零bit2,關閉D Cache
    BIC r0,r0,#(1<<1)        @清零bit1,關閉對齊控制
    BIC r0,r0,#(1<<0)        @清零bit0,關閉MMU

    MCR p15,0,r0,c1,c0,0    @將R0裡的資料寫入到SCTLR暫存器中

#if 0
/*設定中斷向量偏移(此步驟可以在C語言裡實現)修改P15協處理器VBVR暫存器 */
    LDR r0,=0x87800000
    DSB
    ISB   @同步指令,清洗流水線
    MCR p15,0,r0,c12,c0,0   @設定VBVR暫存器=0x87800000
    DSB
    ISB
#endif

/*設定各個模式下sp指標 */

    /*進入IRQ模式 */
    MRS r0,CPSR
    BIC r0,r0,#0x1f
    ORR r0,r0,#0x12
    MSR CPSR,r0
    LDR sp,=0x80600000

    /*進入SYS模式 */
    MRS r0,CPSR
    BIC r0,r0,#0x1f
    ORR r0,r0,#0x1f
    MSR CPSR,r0
    LDR sp,=0x80400000

    /*進入SVC模式 */
    MRS r0,CPSR
    BIC r0,r0,#0x1f
    ORR r0,r0,#0x13
    MSR CPSR,r0
    LDR sp,=0x80200000

    CPSIE i             @使能全域性中斷
    b main

上面的程式碼我們只設置了IRQ模式、SYS模式和SVC模式的sp指標,每種模式的棧大小都是2MB,作為裸機開發已經夠用了。當所有初始化都完成後就可以跳轉到最後的main函式。

IRQ中斷服務函式

IRQ中斷服務函式是重點,因為所有的外部中斷都會觸發這個IRQ中斷。這個中斷函式分兩部分,一部分是IRQ模式下中斷響應,在start.s裡的;還有一部分是C語言構成的中斷服務。流程為在彙編環境響應中斷、切換至IRQ模式下獲取中斷相關引數(主要就是ID)、保護現場、呼叫C函式執行中斷服務、

  1. 響應外部中斷並進入IRQ對應函式
  2. 保護現場、包括r0~r3、r12以及SPSR(CPSR的備份暫存器)
  3. 獲取中斷相關引數:主要就是中斷ID,用來判定是哪個外設發出的中斷請求。並儲存引數
  4. 切換至SVC模式,可以接受其他的中斷請求。
  5. 保護SVC模式現場(LR暫存器),跳轉至C函式執行中斷服務。在C語言中根據中斷ID進行相關程式(這裡有個疑問,使用BLX指令跳轉時是會儲存用到LR暫存器的)。
  6. 執行完C程式後,要通過軟體設定返回IRQ模式,將中斷ID寫回給EOIR,告訴中斷管理器這個中斷任務已經結束了
  7. 恢復現場,調整pc指標

這裡有幾個點要注意一下:

獲取中斷相關引數

這裡主要就是獲取中斷ID,中斷ID是在GICC_IAR[9:0]裡存放的,而GICC_IAR是在GIC的CPU Interface上偏移量0xC個位元組,CPU Interface的地址又是在GIC第基地址上偏移量0x2000和位元組,而GIC的基地址是通過CP15的c15獲取

中斷服務函式呼叫(C函式)

由於我們的這段彙編指令主要是操作處理器執行狀態,實際的外設操作是通過呼叫的C語言來完成的。C函式需要針對不同的中斷ID來調取不同的子函式,所以在呼叫C函式時需要傳遞引數,這個引數就是中斷ID。根據 ATPCS(ARM-Thumb Procedure Call Standard)定義的函式引數傳遞規則,在不超過4個形參時,形參是通過R0~R3這幾個暫存器傳遞的,如果形參超過4個,那麼要藉助堆疊進行引數傳遞了。所以,在前面的操作中,一定要保證最後的中斷ID是放在R0中,就可以被後面呼叫的C函式獲取。

恢復現場

最後恢復現場的時候要注意,不能直接把LR暫存器裡的值傳給PC指標,因為ARM的指令是三級流水線:取指、譯指和執行。也就是PC=當前執行指令地址+8比如下面的程式碼以及對應的地址

0x2000    MOV R1,R0 ;       執行
0x2004    MOV R2,R3 ;       譯指
0x2008    MOV R4,R5 ;       取指,當前PC指標

比如說當前PC指標指向地址為0x2008,但是實際執行的指令是0x2000地址的;此刻如果發生中斷,處理器在執行完0x2000地址內指令後去執行中斷任務,此刻LR儲存的是PC的值也就是0x2008,在中斷完成後,如果直接跳回LR儲存的地址2008,則2004的指令會不被執行,所以要把LR的值減4傳給PC,從2004那條指令重新開始執行。

程式碼結構

還是隻放IRQ中斷處理函式,後面備比較詳細,可以參考。暫時有個地方不理解:為什麼中間要轉到SVC模式處理中斷?留著疑問以後看看能不能解決吧!

IRQ_Handler:

    PUSH {lr}                    /* 儲存lr暫存器內容 */
    PUSH {r0-r3, r12}            /* 儲存r0-r3,r12暫存器 */

    MRS r0, spsr                /* 讀取spsr暫存器 */
    PUSH {r0}                    /* 儲存spsr暫存器 */

    MRC p15, 4, r1, c15, c0, 0 /* 從CP15的C0暫存器CBAR內的值到R1暫存器中,獲取GIC基地址
                                * 參考文件ARM Cortex-A(armV7)程式設計手冊V4.0.pdf P49
                                * Cortex-A7 Technical ReferenceManua.pdf P68 P138*/
    
    ADD r1, r1, #0x2000         /* GIC基地址加0X2000,也就是GIC的CPU介面端基地址
                                 *至此R1儲存內容為GIC裡CPUInterface基地址 */
 
    LDR r0, [r1, #0xC]            /* GIC的CPU介面端基地址加0X0C就是GICC_IAR暫存器,從bit[9:0]獲取中斷ID
                                 *至此,R0儲存為GICC_IAR,對應中斷ID */

    push {r0, r1}                /* 儲存r0,r1 */

    CPS #0x13                    /* 進入SVC模式,允許其他中斷再次進去  注意R0和R1內為有用資料,後面使用時應注意*/
    
    PUSH {lr}                    /* 儲存SVC模式的lr暫存器 */

    LDR r2, =system_irqhandler    /* 載入C語言中斷處理函式到r2暫存器中 該函式需要傳參
                                 * 跳轉至定義的C語言中斷處理函式,中斷ID作為引數儲存在R0中 */

    BLX r2                         /* r2是指向函式的地址,所以用BX,加BXL是儲存跳轉處地址至LR暫存器,共返回時使用*/ 

    POP {lr}                    /* 執行完C語言中斷服務函式,lr出棧 */
    CPS #0x12                    /* 進入IRQ模式 */

    POP {r0, r1}                
    STR r0, [r1, #0X10]            /* 中斷執行完成,寫EOIR ,此步驟進行後,R0和R1的值就不重要了,可以被覆蓋*/

    POP {r0}                        
    MSR spsr_cxsf, r0            /* 恢復spsr */

    POP {r0-r3, r12}            /* r0-r3,r12出棧 */
    POP {lr}                    /* lr出棧 */
    
    SUBS pc, lr, #4                /* 將lr-4賦給pc */

基本上所有的語句都加了備註,哪個CPS是個類似於語法糖的用法,在《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》B9.3.2裡介紹過,可以直接賦值跳轉CPU模式

這個CPS開始讓我暈了半天,查了手冊才發現這種用法!

通用中斷程式

上面的程式完成了,下面主要就是處理中斷了,也就是指向的system_irqhandler函式。這裡使用了教程提供的一個基於恩智浦官方提供的SDK修改以後中斷相關的標頭檔案(core_ca7.h),標頭檔案裡提供了一系列介面,我們可以直接呼叫。在這個中斷驅動檔案中,核心函式就是被呼叫的system_irqhandler。它將根據不同的中斷ID去呼叫不同的函式。I.MX6U一共提供了160箇中斷源,所以就對應了160箇中斷函式,我們可以把這160個函式放在一個數組中,函式的索引值就是對應其中斷ID。當中斷髮生以後,函式system_irqhandler會根據傳遞過來的中斷ID找到對應的函式並處理就可以了。我們在bsp下建立新的資料夾int,裡面新建對應的檔案

宣告函式屬組

定義這個屬組,我們需要在標頭檔案裡宣告幾個函式和資料型別

/*宣告終端處理函式*/
typedef void(*system_irq_handler_t)(unsigned int gicciar,void *param);


/*建立中斷函式結構體*/
typedef struct _sys_irq_handle
{
    system_irq_handler_t irqHander; //中斷處理函式
    void *userParam;                //中斷處理函式的引數
}sys_irq_handle_t;

這裡我們宣告的是函式指標:*system_irq_handler_t,這個函式我們在C檔案裡需要使用,還有後面的結構體包含了函式本身以及函式需要對引數。

在宣告好了函式以及資料型別以後,我們就可以在.c檔案裡編寫對應功能的程式碼了

/*定義中斷處理函式表*/
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS]; //NUMBER_OF_INT_VECTORS為中斷ID個數

/*預設中斷處理函式*/
void default_irqhandler(unsigned int gcciar,void *userparam)
{
    while(1){}
}

先定義了一個表,這個表的資料型別就是我們在標頭檔案裡定義的結構體sys_irq_handle_t,表的大小就是中斷等個數160。然後,再定義一個預設的函式。這個函式是在對函式陣列初始化時候要要用到的,是一個空函式。函式兩個變數,一個是整形的中斷ID,另一個是指標。

初始化函式陣列

前面的工作完成了函式陣列的準備工作,然後我們要對這個陣列進行初始化

/*初始化中斷處理函式表*/
void system_irqtable_init(void)
{
    unsigned int i = 0;
    irqNesting = 0;
    for(i=0;i<NUMBER_OF_INT_VECTORS;i++)
    {
        irqTable[i].irqHander = default_irqhandler; //初始化為預設函式
        irqTable[i].userParam = NULL;               //引數為指標,指向空
    }
}

初始化的過程中做了個迴圈,在迴圈體裡將函式指向我們定義的空函式,引數也指向一個空值。

註冊中斷處理函式

到上面為止,已經可以實現中斷功能了,但問題是,並沒有實際的函式去執行中斷對應請求,而我們寫的中斷處理函式要執行必須要和那個中斷函式陣列關聯起來,這就需要寫一個函式,用來對中斷服務函式進行註冊

/*註冊中斷處理函式*/
void system_register_irqHandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
    irqTable[irq].irqHander = handler;
    irqTable[irq].userParam = userParam;
}

這個函式裡面有3個引數,引數一對應的是中斷id,第二個就是我們的中斷服務函式,第三個就是中斷函式需要傳遞的引數。中斷id用來對陣列進行索引,直接賦值就可以。這個函式需要在外部使用,記得在標頭檔案中宣告。

中斷服務函式

這個函式主要用於和彙編IRQ模式對接,就是核心的那個函式

void system_irqhandler(unsigned int gicciar)  
{
    uint32_t intNum = gicciar &0x3FF;             //bit[9:0]共10位,轉換過來就是0x3FF與運算就可獲取中斷ID

    /* 檢查中斷ID是否為異常*/
    if(intNum>=NUMBER_OF_INT_VECTORS)
    {
        return;     //中斷ID異常,直接返回 
    }

    irqNesting++;          //有新中斷,計數器+1
    irqTable[intNum].irqHander(intNum,irqTable[intNum].userParam);  //執行具體中斷處理
    irqNesting--;         //中斷處理完成,巢狀計數器-1                   
}

在函式中呼叫了變數irqNesting,是用作巢狀中斷的記錄,如果在中斷執行時有高優先順序的中斷進來,當前中斷會被打斷,巢狀計數器自增1,中斷處理完成後計數器自減1,如果計數器為0說明當前中斷無巢狀。

函式構成

下面分別是標頭檔案和c檔案

#ifndef __BSP_INT_H
#define __BSP_INT_H

#include "imx6ul.h"

/*宣告終端處理函式*/
typedef void(*system_irq_handler_t)(unsigned int gicciar,void *param);

/*建立中斷函式結構體*/
typedef struct _sys_irq_handle
{
    system_irq_handler_t irqHander; //中斷處理函式
    void *userParam;                //中斷處理函式的引數
}sys_irq_handle_t;

void int_init(void);
void system_irqtable_init(void);
void default_irqhandler(unsigned int gcciar,void *userParam);
void system_irqhandler(unsigned int gicciar);
void system_register_irqHandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam);
#endif
bsp_int.h
#include "bsp_int.h"

static unsigned int irqNesting;    //記錄中斷巢狀計數器

/*定義中斷處理函式表*/
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS]; //NUMBER_OF_INT_VECTORS為中斷ID個數

/*
* @description          :   預設中斷處理函式
* @param-gcciar         :   中斷ID
* @param-userParam      :   中斷服務處理函式引數
* @return               :   無
*/
void default_irqhandler(unsigned int gcciar,void *userParam)
{
    while(1){}
}

/*初始化中斷處理函式表*/
void system_irqtable_init(void)
{
    unsigned int i = 0;
    irqNesting = 0;
    for(i=0;i<NUMBER_OF_INT_VECTORS;i++)
    {
        irqTable[i].irqHander = default_irqhandler; //初始化為預設函式
        irqTable[i].userParam = NULL;               //引數為指標,指向空
    }
}



/*中斷初始化函式*/
void int_init(void)
{
    GIC_Init();                 //GIC初始化
    system_irqtable_init();     //初始化中斷函式表
    __set_VBAR(0x87800000);     //中斷向量表偏執
}

/*
* @description          :   註冊中斷處理函式
* @param-irq            :   中斷id
* @param-handler        :   中斷處理服務函式
* @param-userParam      :   中斷服務處理函式引數
* @return               :   無
*/
void system_register_irqHandler(IRQn_Type irq, 
                                system_irq_handler_t handler, 
                                void *userParam)
{
    irqTable[irq].irqHander = handler;          //函式
    irqTable[irq].userParam = userParam;        //引數
}

/*
*@desciption            :   C語言中斷服務,IRQ中斷呼叫函式
*@param-gicciar         :   中斷ID,在彙編處通過R0傳入
*@return                :   無
*/
void system_irqhandler(unsigned int gicciar)  
{
    uint32_t intNum = gicciar &0x3FF;             //bit[9:0]共10位,轉換過來就是0x3FF與運算就可獲取中斷ID

    /* 檢查中斷ID是否為異常*/
    if(intNum>=NUMBER_OF_INT_VECTORS)
    {
        return;     //中斷ID異常,直接返回 
    }

    irqNesting++;          //有新中斷,計數器+1
    irqTable[intNum].irqHander(intNum,irqTable[intNum].userParam);  //執行具體中斷處理
    irqNesting--;         //中斷處理完成,巢狀計數器-1                   
}
bsp_int.c

現在就完成了通用的中斷服務程式,現在需要對就是使用外設中斷、呼叫中斷程式。