鴻蒙輕核心M核的故障管家:Fault異常處理

華為雲開發者社群發表於2021-10-29
摘要:本文先簡單介紹下Fault異常型別,向量表及其程式碼,異常處理C語言程式,然後詳細分析下異常處理彙編函式實現程式碼。

本文分享自華為雲社群《鴻蒙輕核心M核原始碼分析系列十八 Fault異常處理》,作者:zhushy。

Fault異常處理模組與OpenHarmony LiteOS-M核心晶片架構相關,提供對HardFault、MemManage、BusFault、UsageFault等各種故障異常處理。有關Cortex-M晶片相關的知識不在本文討論,請自行參考《Cortex™-M7 Devices Generic User Guide》等官方資料。本文先簡單介紹下Fault異常型別,向量表及其程式碼,異常處理C語言程式,然後詳細分析下異常處理彙編函式實現程式碼。文中所涉及的原始碼,以OpenHarmony LiteOS-M核心為例,均可以在開源站點https://gitee.com/openharmony/kernel_liteos_m 獲取。

1、Fault Type異常型別

如下圖中的Fault型別表格所示,Fault表示各種故障,Handler表示故障處理機制,Bit Name標記故障的暫存器的Bit位,Fault status register故障狀態暫存器。該圖摘自《Cortex™-M7 Devices Generic User Guide》。

鴻蒙輕核心M核的故障管家:Fault異常處理

鴻蒙輕核心M核的故障管家:Fault異常處理

2、Vector table向量表

向量表包含棧指標的復位值和開始地址,也叫異常向量。異常可以看作特殊的中斷,異常編號Exception number, 中斷請求號IRQ number,偏移值offset,向量Vector的對應關係如下圖所示,本文主要關注NMI、HardFault、Memory management fault、Bus fault、Usage fault、SVCall等異常。

鴻蒙輕核心M核的故障管家:Fault異常處理

在中斷初始化時,會初始化該異常向量表,程式碼位置kernel\arch\arm\cortex-m7\gcc\los_interrupt.c。⑴處的HalExcNMI,⑵處的HalExcHardFault,⑶處的HalExcMemFault,⑷處的HalExcBusFault,⑸處的HalExcUsageFault,⑹處的HalExcSvcCall這些中斷異常處理函式定義在kernel\arch\arm\cortex-m7\gcc\los_exc.S。本文我們主要分析這些彙編函式的程式碼。

⑺處開始的這兩行程式碼也比較重要,通過更改系統處理控制與狀態暫存器(System Handler Control and State Register)的bit位來使能相應的異常,通過更改配置與控制暫存器(Configuration and Control Register)的bit位來使能除零異常。

LITE_OS_SEC_TEXT_INIT VOID HalHwiInit(VOID)
{
#if (LOSCFG_USE_SYSTEM_DEFINED_INTERRUPT == 1)
    UINT32 index;
    g_hwiForm[0] = 0;             /* [0] Top of Stack */
    g_hwiForm[1] = Reset_Handler; /* [1] reset */
    for (index = 2; index < OS_VECTOR_CNT; index++) { /* 2: The starting position of the interrupt */
        g_hwiForm[index] = (HWI_PROC_FUNC)HalHwiDefaultHandler;
    }
    /* Exception handler register */
⑴  g_hwiForm[NonMaskableInt_IRQn + OS_SYS_VECTOR_CNT]   = HalExcNMI;
⑵  g_hwiForm[HARDFAULT_IRQN + OS_SYS_VECTOR_CNT]        = HalExcHardFault;
⑶  g_hwiForm[MemoryManagement_IRQn + OS_SYS_VECTOR_CNT] = HalExcMemFault;
⑷  g_hwiForm[BusFault_IRQn + OS_SYS_VECTOR_CNT]         = HalExcBusFault;
⑸  g_hwiForm[UsageFault_IRQn + OS_SYS_VECTOR_CNT]       = HalExcUsageFault;
⑹  g_hwiForm[SVCall_IRQn + OS_SYS_VECTOR_CNT]           = HalExcSvcCall;
    g_hwiForm[PendSV_IRQn + OS_SYS_VECTOR_CNT]           = HalPendSV;
    g_hwiForm[SysTick_IRQn + OS_SYS_VECTOR_CNT]          = SysTick_Handler;

    /* Interrupt vector table location */
    SCB->VTOR = (UINT32)(UINTPTR)g_hwiForm;
#endif
#if (__CORTEX_M >= 0x03U) /* only for Cortex-M3 and above */
    NVIC_SetPriorityGrouping(OS_NVIC_AIRCR_PRIGROUP);
#endif

    /* Enable USGFAULT, BUSFAULT, MEMFAULT */*(volatile UINT32 *)OS_NVIC_SHCSR |= (USGFAULT | BUSFAULT | MEMFAULT);
    /* Enable DIV 0 and unaligned exception */
    *(volatile UINT32 *)OS_NVIC_CCR |= DIV0FAULT;

    return;
}

3、HalExcHandleEntry異常處理C程式入口

HalExcHandleEntry異常處理函式是彙編異常函式跳轉到C語言程式的入口,定義在檔案kernel\arch\arm\cortex-m7\gcc\los_interrupt.c,被kernel\arch\arm\cortex-m7\gcc\los_exc.S檔案中的彙編函式呼叫。函式引數由彙編程式中的R0-R3暫存器傳值進來,彙編程式中的暫存器和HalExcHandleEntry函式引數對應關係如下表所示:

鴻蒙輕核心M核的故障管家:Fault異常處理

下面我們分析下函式的原始碼,⑴處的標籤表示異常型別引數的高16位用於特色的標記,主要用於標記故障地址是否有效、是否故障發生在中斷中,是否支援浮點等。⑵處增加中斷計數和巢狀異常數目。⑶記錄異常型別,⑷處如果記錄了有效的故障地址,則獲取故障地址。⑸處如果當前執行任務存在時,若標記了異常發生在中斷,則記錄中斷號,並記錄異常發生在中斷內,否則記錄任務編號,並記錄異常發生在任務內。如果當前執行任務為空,則異常發生在初始化階段。⑹處如果異常型別裡包含支援浮點數的標記,則相應處理下。⑺處輸出異常資訊到控制檯。

LITE_OS_SEC_TEXT_INIT VOID HalExcHandleEntry(UINT32 excType, UINT32 faultAddr, UINT32 pid, EXC_CONTEXT_S *excBufAddr)
{
⑴  UINT16 tmpFlag = (excType >> 16) & OS_NULL_SHORT; /* 16: Get Exception Type */
⑵  g_intCount++;
    g_excInfo.nestCnt++;

⑶  g_excInfo.type = excType & OS_NULL_SHORT;

⑷  if (tmpFlag & OS_EXC_FLAG_FAULTADDR_VALID) {
        g_excInfo.faultAddr = faultAddr;
    } else {
        g_excInfo.faultAddr = OS_EXC_IMPRECISE_ACCESS_ADDR;
    }
⑸  if (g_losTask.runTask != NULL) {
        if (tmpFlag & OS_EXC_FLAG_IN_HWI) {
            g_excInfo.phase = OS_EXC_IN_HWI;
            g_excInfo.thrdPid = pid;
        } else {
            g_excInfo.phase = OS_EXC_IN_TASK;
            g_excInfo.thrdPid = g_losTask.runTask->taskID;
        }
    } else {
        g_excInfo.phase = OS_EXC_IN_INIT;
        g_excInfo.thrdPid = OS_NULL_INT;
    }
⑹  if (excType & OS_EXC_FLAG_NO_FLOAT) {
        g_excInfo.context = (EXC_CONTEXT_S *)((CHAR *)excBufAddr - LOS_OFF_SET_OF(EXC_CONTEXT_S, uwR4));
    } else {
        g_excInfo.context = excBufAddr;
    }

⑺  OsDoExcHook(EXC_INTERRUPT);
    OsExcInfoDisplay(&g_excInfo);
    HalSysExit();
}

4、Los_Exc異常處理彙編函式

上文介紹Vector table向量表時,已經提到了在檔案kernel\arch\arm\cortex-m7\gcc\los_exc.S中定義的的異常處理函式,如下。當發生Fault故障異常時,會排程執行這些異常處理函式,本節會詳細分析函式的原始碼來掌握核心如何處理這些發生的異常。這6個函式處理過程類似,我們選擇2個典型的函式進行分析。

 .global  HalExcNMI
    .global  HalExcHardFault
    .global  HalExcMemFault
    .global  HalExcBusFault
    .global  HalExcUsageFault
    .global  HalExcSvcCall

4.1 HalExcNMI

當發生NMI(Non Maskable Interrupt,不可遮蔽中斷)時,會觸發執行HalExcNMI彙編函式,該函式的執行流程如下圖。下文會結合該流程圖來閱讀函式程式碼。

鴻蒙輕核心M核的故障管家:Fault異常處理

HalExcNMI函式程式碼如下,⑴處給R0暫存器賦值OS_EXC_CAUSE_NMI,該值等於16,對應檔案kernel\arch\arm\cortex-m7\gcc\los_arch_interrupt.h中的異常型別巨集定義OS_EXC_CAUSE_NMI,均為16。該值對應HalExcHandleEntry函式的第一個引數。⑵處設定故障地址,該值對應HalExcHandleEntry函式的第二個引數。⑶處跳轉到函式osExcDispatch繼續執行。

    .type HalExcNMI, %function
    .global HalExcNMI
HalExcNMI:
    .fnstart
    .cantunwind
⑴  MOV  R0, #OS_EXC_CAUSE_NMI
⑵  MOV  R1, #0
⑶  B  osExcDispatch
    .fnend

下面分析的一些函式比較通用,其他異常處理函式也都會呼叫。

4.1.1 osExcDispatch函式

osExcDispatch函式程式碼如下,⑴處載入Interrupt Active Bit Registers中斷活躍位暫存器基地址。中斷活躍位暫存器共有8個,NVIC_IABR0-NVIC_IABR7,每個暫存器包含32位,可以對應32箇中斷號,共支援256箇中斷。其中,IABR[0]的 bit位0~31 分別對應中斷號0~31;IABR[1]的bit位0~31對應中斷32~63;其他以此類推。⑵處設定迴圈計數,對應8個暫存器,後文會迴圈遍歷8個暫存器查詢是否存在活躍的中斷。

    .type osExcDispatch, %function
    .global osExcDispatch
osExcDispatch:
    .fnstart
    .cantunwind
⑴  LDR   R2, =OS_NVIC_ACT_BASE
⑵  MOV   R12, #8                       // R12 is hwi check loop counter
    .fnend

4.1.2 _hwiActiveCheck函式

執行完上述osExcDispatch函式程式碼後,會繼續執行隨後的函式_hwiActiveCheck的程式碼。⑴處讀取活躍位暫存器的數值,然後執行⑵比較暫存器數值與0的大小,如果相等,說明該活躍位暫存器對應的中斷均不活躍,然後跳轉到_hwiActiveCheckNext。如果不等於0,則執行⑶,引數型別的高16位標記為中斷。⑷處程式碼根據中斷活躍位計算中斷號,並賦值給暫存器R2,該值對應HalExcHandleEntry函式的第三個引數。具體計算方式為,首先反轉活躍中斷位暫存器數值R3,並儲存到R2,然後計算高位0的數量。把計數值R12加1,然後左移5位(等於乘以32),然後加上R2,就是中斷號。

    .type _hwiActiveCheck, %function
    .global _hwiActiveCheck
_hwiActiveCheck:
    .fnstart
    .cantunwind
⑴  LDR   R3, [R2]                      // R3 store active hwi register when exc
⑵  CMP   R3, #0
    BEQ   _hwiActiveCheckNext

    // exc occurred in IRQ
⑶  ORR   R0, R0, #FLAG_HWI_ACTIVE
⑷  RBIT  R2, R3
    CLZ   R2, R2
    AND   R12, R12, #1
    ADD   R2, R2, R12, LSL #5               // calculate R2 (hwi number) as pid
    .fnend

4.1.3 _ExcInMSP函式和_NoFloatInMsp函式

如果有活躍的中斷,則繼續執行後續的程式碼。處理中斷時,使用的主棧處理函式_ExcInMSP。⑴處比較異常返回值和#0XFFFFFFED的大小,如果相等說明支援浮點計算則繼續執行後續程式碼,如果不相等則不支援浮點計算,會跳轉到函式_NoFloatInMsp函式。有關異常返回值的更多資訊請參考《Cortex™-M7 Devices Generic User Guide》表格Table 2-15 Exception return behavior。

如果支援浮點計算時,執行⑵把棧指標加上104賦值給R3暫存器,然後壓棧,該值對應HalExcHandleEntry函式的第四個引數。104的大小應該來源於結構體EXC_CONTEXT_S。⑶處把暫存器PRIMASK數值複製到R12暫存器,然後把R4-R12暫存器壓棧。⑷處把浮點暫存器壓棧,⑸處跳轉到函式_handleEntry。

當不支援浮點計算時,執行函式_NoFloatInMsp。⑹處把棧指標加上32賦值給R3暫存器,然後壓棧,該值對應HalExcHandleEntry函式的第四個引數。然後把R3壓棧,把暫存器PRIMASK數值複製到R12,然後壓棧R4-R12。和支援浮點時的差別就是,不需要壓棧D8-D15暫存器。⑺處把引數型別高位上加上不支援浮點的標記,然後跳轉到函式_handleEntry。

    .type _ExcInMSP, %function
    .global _ExcInMSP
_ExcInMSP:
    .fnstart
    .cantunwind
⑴  CMP   LR, #0XFFFFFFED
    BNE   _NoFloatInMsp
⑵  ADD   R3, R13, #104
    PUSH  {R3}
⑶  MRS   R12, PRIMASK                  // store message-->exc: disable int?
    PUSH {R4-R12}                       // store message-->exc: {R4-R12}
#if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \
     (defined(__FPU_USED) && (__FPU_USED == 1U)))
⑷  VPUSH {D8-D15}
#endif
⑸  B     _handleEntry
  .fnend

    .type _NoFloatInMsp, %function
    .global _NoFloatInMsp
_NoFloatInMsp:
    .fnstart
    .cantunwind
⑹  ADD   R3, R13, #32
    PUSH  {R3} // save IRQ SP            // store message-->exc: MSP(R13)

    MRS   R12, PRIMASK                  // store message-->exc: disable int?
    PUSH {R4-R12}                       // store message-->exc: {R4-R12}
⑺  ORR   R0, R0, #FLAG_NO_FLOAT
    B     _handleEntry
  .fnend

4.1.4 _hwiActiveCheckNext函式

遍歷中斷活躍位暫存器時,如果前一個暫存器沒有活躍的中斷則執行函式_hwiActiveCheckNext判斷下一個暫存器是否有活躍的中斷。⑴處把活躍位暫存器地址偏移4位元組,計數減1,如果還有其他活躍位暫存器,則跳轉到函式_hwiActiveCheck繼續判斷。否則執行後續的程式碼,⑵處載入System Handler Control and State Register(縮寫SHCSRS)系統處理控制與狀態暫存器的地址,然後載入半位元組數值。⑶處載入掩碼0xC00,該數值二進位制的第10、第11位為1。SHCSRS暫存器的第11位對應SysTick異常活躍位,第10位對應PendSV異常活躍位。⑷處R2、R3進行邏輯與計算,然後把結果與0進行比較,如果結果為0,說明沒有發生ysTick異常或PendSV異常。如果結果為1,說明發生了異常,需要執行⑸跳轉到函式_ExcInMSP繼續執行,上文已分析該函式。⑹處獲取全域性變數g_taskScheduled的地址,然後獲取其數值,與1進行比較。如果等於1,說明系統已經開始任務排程,會繼續執行後續的程式碼。如果不為1,系統未排程,處於初始化階段,需要跳轉到函式_ExcInMSP繼續執行。

如果系統開始了任務排程,此時使用程式棧PSP,執行⑺,判斷系統是否支援浮點計算。如果支援則繼續執行,否則跳轉到函式_NoFloatInPsp。⑻處開始的程式碼和函式_NoFloatInPsp可以對比著閱讀,前者需要壓棧浮點暫存器,後者不需要。⑻處把棧指標複製到R2暫存器,然後把棧指標減去96。⑼處把PSP執行緒棧指標值賦值給R3暫存器,然後把R3加104賦值給暫存器R12,計算出來的值是任務棧指標,然後進行壓棧。

⑽處複製PRIMASK暫存器數值到R12,然後把暫存器R4-R12壓棧,接著壓棧浮點暫存器D8-D15。⑾處從PSP棧指標開始把R4-R11、D8-D15出棧,然後從R13棧指標開始把D8-D15、R4-R11進行壓棧。⑿處跳轉到函式_handleEntry繼續指向。

    .type _hwiActiveCheckNext, %function
    .global _hwiActiveCheckNext
_hwiActiveCheckNext:
    .fnstart
    .cantunwind
⑴  ADD   R2, R2, #4                        // next NVIC ACT ADDR
    SUBS  R12, R12, #1
    BNE   _hwiActiveCheck

    /*NMI interrupt exception*/
⑵  LDR   R2, =OS_NVIC_SHCSRS
    LDRH  R2,[R2]
⑶  LDR   R3,=OS_NVIC_SHCSR_MASK
⑷  AND   R2, R2,R3
    CMP   R2,#0
⑸  BNE   _ExcInMSP
    // exc occured in Task or Init or exc
    // reserved for register info from task stack

⑹  LDR  R2, =g_taskScheduled
    LDR  R2, [R2]
    TST  R2, #1                         // OS_FLG_BGD_ACTIVE
    BEQ  _ExcInMSP                      // if exc occurred in Init then branch
⑺  CMP   LR, #0xFFFFFFED               //auto push floating registers
    BNE   _NoFloatInPsp

    // exc occurred in Task
⑻  MOV   R2,  R13
    SUB   R13, #96                      // add 8 Bytes reg(for STMFD)

⑼  MRS   R3,  PSP
    ADD   R12, R3, #104
    PUSH  {R12}                         // save task SP

⑽  MRS   R12, PRIMASK
    PUSH {R4-R12}
    VPUSH {D8-D15}

    // copy auto saved task register

⑾  LDMFD R3!, {R4-R11}                  // R4-R11 store PSP reg(auto push when exc in task)
    VLDMIA  R3!, {D8-D15}
    VSTMDB  R2!, {D8-D15}
    STMFD R2!, {R4-R11}
⑿  B     _handleEntry
  .fnend

    .type _NoFloatInPsp, %function
    .global _NoFloatInPsp
_NoFloatInPsp:
    .fnstart
    .cantunwind
    MOV   R2,  R13                      // no auto push floating registers
    SUB   R13, #32                      // add 8 Bytes reg(for STMFD)

    MRS   R3,  PSP
    ADD   R12, R3, #32
    PUSH  {R12}                         // save task SP

    MRS   R12, PRIMASK
    PUSH {R4-R12}

    LDMFD R3, {R4-R11}                  // R4-R11 store PSP reg(auto push when exc in task)
    STMFD R2!, {R4-R11}
    ORR   R0, R0, #FLAG_NO_FLOAT
  .fnend

4.1.5 _handleEntry函式

繼續分析函式_handleEntry。程式碼很簡單,⑴把棧指標複製給R3,該值對應HalExcHandleEntry函式的第四個引數。⑵處關閉中斷,關閉Fault異常,然後執行⑵跳轉到C語言的函式HalExcHandleEntry。

_handleEntry:
    .fnstart
    .cantunwind
⑴  MOV R3, R13                         // R13:the 4th param
⑵  CPSID I
    CPSID F
    B  HalExcHandleEntry

    NOP
  .fnend

4.2 HalExcUsageFault

當發生使用異常UsageFault時,會觸發執行HalExcUsageFault彙編函式,該函式的執行流程如下圖。下文會結合該流程圖來閱讀函式程式碼。

鴻蒙輕核心M核的故障管家:Fault異常處理

HalExcUsageFault函式程式碼如下,⑴處把可配置故障狀態暫存器Configurable Fault Status Register(CFSR)的地址複製到R0暫存器,然後讀取暫存器值到R0暫存器。⑵處把0x030F賦值給R1暫存器,然後左移16位。UsageFault Status Register使用故障狀態暫存器的有效性如下,即0-3,8-9為有效位,0x030F的二進位制對應這些有效位。⑶處進行邏輯與,這樣就計算出實際的使用故障對應的bit位。⑷處把R12賦值為0,然後會繼續執行後續的彙編程式碼osExcCommonBMU。

鴻蒙輕核心M核的故障管家:Fault異常處理

    .type HalExcUsageFault, %function
    .global HalExcUsageFault
HalExcUsageFault:
    .fnstart
    .cantunwind
⑴  LDR  R0, =OS_NVIC_FSR
    LDR  R0, [R0]

⑵  MOVW  R1, #0x030F
    LSL  R1, R1, #16
⑶  AND  R0, R0, R1
⑷  MOV  R12, #0

    .fnend

4.2.1 g_uwExcTbl陣列

在看osExcCommonBMU函式的程式碼之前需要了解下g_uwExcTbl陣列,g_uwExcTbl陣列定義在檔案kernel\arch\arm\cortex-m7\gcc\los_interrupt.c,程式碼如下。
該陣列包含32個元素,每個元素對應CFSR暫存器的一個bit位,元素數值在LiteOS-M中定義為異常型別。比如OS_EXC_UF_DIVBYZERO等於異常型別10,為除零異常。

UINT8 g_uwExcTbl[FAULT_STATUS_REG_BIT] = {
    0, 0, 0, 0, 0, 0, OS_EXC_UF_DIVBYZERO, OS_EXC_UF_UNALIGNED,
    0, 0, 0, 0, OS_EXC_UF_NOCP, OS_EXC_UF_INVPC, OS_EXC_UF_INVSTATE, OS_EXC_UF_UNDEFINSTR,
    0, 0, 0, OS_EXC_BF_STKERR, OS_EXC_BF_UNSTKERR, OS_EXC_BF_IMPRECISERR, OS_EXC_BF_PRECISERR, OS_EXC_BF_IBUSERR,
    0, 0, 0, OS_EXC_MF_MSTKERR, OS_EXC_MF_MUNSTKERR, 0, OS_EXC_MF_DACCVIOL, OS_EXC_MF_IACCVIOL
};

4.2.2 osExcCommonBMU函式

現在來分析下彙編程式碼osExcCommonBMU。⑴處計算出R0數值的高位0的個數,載入陣列全域性變數g_uwExcTbl地址到R3暫存器,然後執行⑵計算是第幾個陣列元素,載入元素值到R0暫存器。⑶處R0與R12進行邏輯或運算,沒有什麼影響。R0對應HalExcHandleEntry函式的第一個引數。後續會繼續執行osExcDispatch函式,前文已經分析過。

    .type osExcCommonBMU, %function
    .global osExcCommonBMU
osExcCommonBMU:
    .fnstart
    .cantunwind
⑴  CLZ  R0, R0
    LDR  R3, =g_uwExcTbl
⑵  ADD  R3, R3, R0
    LDRB R0, [R3]
⑶  ORR  R0, R0, R12
    .fnend

小結

本文介紹了Fault異常型別,向量表及其程式碼,異常處理C語言程式,異常處理彙編函式實現程式碼。感謝閱讀,如有任何問題、建議,都可以部落格下留言給我,謝謝。

參考資料

  • Cortex™-M7 Devices Generic User Guide Download

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章