新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 設(shè)計應(yīng)用 > stm32 hard fault及堆棧探究

stm32 hard fault及堆棧探究

作者: 時間:2016-11-11 來源:網(wǎng)絡(luò) 收藏

在調(diào)試RTC過程中,程序在主循環(huán)中執(zhí)行兩次后就進(jìn)入hard fault的while(1)中斷,keil顯示調(diào)試窗口顯示imprecise data bus error。完善RTC配置的時序也無濟(jì)于事。網(wǎng)上查到一些hard fault的資料:

本文引用地址:http://m.butianyuan.cn/article/201611/317054.htm

<STM32F10xxx Cortex-M3 programming manual>2.3.2對hard fault, bus fault等有具體的解釋。keil的網(wǎng)站上也有概括性的解釋:hard fault由bus fault, memory management fault或usage fault引起,前者有固定的僅次于NMI的高優(yōu)先級;調(diào)試過程中出現(xiàn)的bus error屬于bus fault,是取指或取值時的內(nèi)存錯誤。ST論壇上對于hard fault的討論,大牛們說:

是由于讀寫了一個非法位置,

“100% of the hard faults Ive had are caused by variables accessing out of bounds.”,

"The Cortex-M3 pushes fault context on to the stack (some 8 dwords as I recall), I think Joseph Yiu has some example of instrumenting this. This could should permit you to determine the faulting PC. With this and the register info, and a map file you should be able to zero in on what is going on.

MRS R0, PSP ; Read PSP
LDR R1, [R0, #24] ; Read Saved PC from Stack" 能看到出錯的PC值倒是一個很方便的事情,不過還沒試過。

回頭看自己的程序,從最簡邏輯開始燒寫運(yùn)行,發(fā)現(xiàn)當(dāng)增加到在Time_Display()時進(jìn)入了hard fault。檢查代碼,函數(shù)中定義了一個char類型數(shù)組,用于存放需要顯示到LCD上的時間字符串,但數(shù)組長度小于字符串長度。增大長度,就解決了問題。果然如大牛們所說,問題存在于數(shù)組越界。

之前我也犯過類似錯誤,可當(dāng)時的現(xiàn)象是,串口實際發(fā)出的數(shù)據(jù)和數(shù)組中的數(shù)據(jù)相比,后半部分時對時錯。當(dāng)時的變量為全局變量,此處變量為局部變量。查到如下說明:“一個由c/C++編譯的程序占用的內(nèi)存分為以下幾個部分: 1、棧區(qū)(stack)— 由編譯器自動分配釋放 ,存放函數(shù)的參數(shù)值,局部變量的值等。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。2、堆區(qū)(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結(jié)束時可能由OS回收 。注意它與數(shù)據(jù)結(jié)構(gòu)中的堆是兩回事,分配方式倒是類似于鏈表,呵呵。3、全局區(qū)(靜態(tài)區(qū))(static)—,全局變量和靜態(tài)變量的存儲是放在一塊的,初始化的全局變量和靜態(tài)變量在一塊區(qū)域, 未初始化的全局變量和未初始化的靜態(tài)變量在相鄰的另一塊區(qū)域。 - 程序結(jié)束后有系統(tǒng)釋放. 4、文字常量區(qū)—常量字符串就是放在這里的。 程序結(jié)束后由系統(tǒng)釋放. 5、程序代碼區(qū)—存放函數(shù)體的二進(jìn)制代碼。”全局變量儲存在全局區(qū),它的越界將影響其他變量的值,對程序運(yùn)行不會有致命影響。局部變量在棧中,同時入棧的還有函數(shù)的回退地址,函數(shù)參數(shù)等。本次出現(xiàn)問題的代碼段為:

  1. voidTime_Show(void)
  2. {
  3. while(1)
  4. {
  5. /*If1shasbeenelapased*/
  6. if(TimeDisplay==1)
  7. {
  8. uint32_tCounter=0;
  9. Counter=RTC_GetCounter();
  10. Time_Display(Counter);
  11. TimeDisplay=0;
  12. }
  13. }
  14. }
  15. voidTime_Display(uint32_tTimeVar)
  16. {
  17. uint32_tTHH=0,TMM=0,TSS=0;
  18. charbuf[10];
  19. /*ResetRTCCounterwhenTimeis23:59:59*/
  20. if(TimeVar==0x0001517F)
  21. {
  22. RTC_WaitForLastTask();
  23. RTC_SetCounter(0x0);
  24. /*WaituntillastwriteoperationonRTCregistershasfinished*/
  25. RTC_WaitForLastTask();
  26. }
  27. /*Computehours*/
  28. THH=TimeVar/3600;
  29. /*Computeminutes*/
  30. TMM=(TimeVar%3600)/60;
  31. /*Computeseconds*/
  32. TSS=(TimeVar%3600)%60;
  33. /*sprintf(buf,"0x%08x",buf);
  34. sprintf(buf,"0x%08x",&TimeVar);
  35. sprintf(buf,"0x%08x",&THH);
  36. sprintf(buf,"0x%08x",&TMM);
  37. sprintf(buf,"0x%08x",&TSS);
  38. */
  39. sprintf(buf,"%0.2d:%0.2d:%0.2d",THH,TMM,TSS);
  40. LCD_DisplayStringLine(LCD_LINE_1,buf);
  41. }

在Time_Display()中通過用sprintf將地址賦值給變量(即代碼中注視掉的sprintf語句),并在LCD上顯示的辦法觀察到,棧內(nèi)的變量分布情況為:

可以看出,棧從內(nèi)存地址高位向低位生長,參數(shù)在棧底,變量按照定義的順序依次往上摞。系統(tǒng)給buf多留了兩字節(jié)的空間,其余變量(包括函數(shù)參數(shù)timevar和局部變量TXX)在內(nèi)存中依次緊密排列,沒有出現(xiàn)windows中將函數(shù)回退地址的入棧時間放于參數(shù)之后,使參數(shù)和變量之間有四字節(jié)空隙的情況。這說明函數(shù)的回退地址和一些寄存器的入棧保存另有其他時機(jī)。同時注意到,代碼中有用sprintf取得變量地址的語句時,工作正常,不會進(jìn)入hardfault。因此有必要比較兩段代碼對內(nèi)存空間造成的影響。

1. 進(jìn)入hard fault是在Time_Show()函數(shù)一個循環(huán)執(zhí)行完畢時。因此有必要看一下匯編,了解具體對寄存器和內(nèi)存的數(shù)據(jù)讀寫操作:

  1. 208:if(TimeDisplay==1)
  2. 209:{
  3. 0x08000E724C05LDRr4,[pc,#20];@0x08000E88
  4. 210:uint32_tCounter=0;
  5. 0x08000E742500MOVSr5,#0x00
  6. 0x08000E766820LDRr0,[r4,#0x00]
  7. 0x08000E782801CMPr0,#0x01
  8. 0x08000E7AD1FCBNE0x08000E76
  9. 211:Counter=RTC_GetCounter();
  10. 0x08000E7CF7FFFE2CBL.WRTC_GetCounter(0x08000AD8)
  11. 212:Time_Display(Counter);
  12. 0x08000E80F7FFFF98BL.WTime_Display(0x08000DB4)
  13. 213:TimeDisplay=0;
  14. 0x08000E846025STRr5,[r4,#0x00]
  15. 214:}
  16. 0x08000E86E7F6B0x08000E76

在這一段中,R4存放變量TimeDisplay的地址,R0為TimeDisplay的值。循環(huán)的最后一步,寄存器R4中的地址加0作為新地址,R5從內(nèi)存中的該新地址取值存入。如果R4指向的地址非法,則讀取該地址很有可能產(chǎn)生hard fault。

2.查看Time_Display()的匯編

(1)添加了顯示變量地址的代碼,而無hard fault的情況。

主循環(huán)的起始部分匯編代碼如下,每次進(jìn)入循環(huán)只需將Time_Display()時入棧的回退地址彈出作為PC。

  1. 200:voidTime_Show(void)
  2. 0x08000E44B009ADDsp,sp,#0x24
  3. 0x08000E46BD00POP{pc}
  4. 0x08000E48517FSTRr7,[r7,r5]

剛進(jìn)入Time_Display()時的匯編代碼如下,進(jìn)入時將R0和LR寄存器壓入棧中。

  1. 165:voidTime_Display(uint32_tTimeVar)
  2. 0x08000DACE8BD4010POP{r4,lr}
  3. 0x08000DB0F7FFBEEAB.WRTC_WaitForLastTask(0x08000B88)
  4. 166:{
  5. 0x08000DB4B501PUSH{r0,lr}
  6. 0x08000DB6B088SUBsp,sp,#0x20
  7. 167:uint32_tTHH=0,TMM=0,TSS=0;
  8. 168:charbuf[10];

此時STM32芯片寄存器和內(nèi)存的情況如下圖所示。

根絕匯編中把R0和LR壓入棧中的指令,對應(yīng)LR和R0的值,在局部變量所在內(nèi)存空間尋找,可以發(fā)現(xiàn)LR最先入棧,接著是函數(shù)參數(shù)和其余變量,這和最開始打印出的各變量地址也是吻合的。因此,如果buf越界不是太多,只是改寫了其余局部變量的數(shù)據(jù),不影響回退地址。另外,查看函數(shù)所有匯編代碼,沒有對R4的操作。至函數(shù)執(zhí)行完成并返回,R4的值始終為0x20000000。綜上,函數(shù)可以繼續(xù)執(zhí)行而不會出錯。

(2)產(chǎn)生hard fault的情況。

主循環(huán)的起始部分匯編代碼如下,需要在Time_Display()后的寄存器值和回退地址都彈出。

  1. 196:voidTime_Show(void)
  2. 0x08000D90BD1FPOP{r0-r4,pc}
  3. 0x08000D94517FSTRr7,[r7,r5]

剛進(jìn)入Time_Display()時的匯編代碼如下,將R0-R4,及LR都壓入棧中。

  1. 165:voidTime_Display(uint32_tTimeVar)
  2. 0x08000D40E8BD4010POP{r4,lr}
  3. 0x08000D44F7FFBEEAB.WRTC_WaitForLastTask(0x08000B1C)
  4. 166:{
  5. 167:uint32_tTHH=0,TMM=0,TSS=0;
  6. 168:charbuf[10];
  7. 169:/*ResetRTCCounterwhenTimeis23:59:59*/
  8. 0x08000D48B51FPUSH{r0-r4,lr}
  9. 0x08000D4A4604MOVr4,r0

此時STM32芯片寄存器和內(nèi)存的情況如下圖所示。

此時buf的地址為0x200003ec,即R1的起始位置。變量和寄存器值的覆蓋關(guān)系,或許是編譯器檢測到R1~R3的值在出棧后將不會被使用,而對內(nèi)存進(jìn)行的優(yōu)化。此時內(nèi)存中沒有其他局部變量的位置,是因為在改動了代碼的情況下,編譯器判斷為,只需在寄存器里就可以完成計算操作,因此改變了函數(shù)的匯編代碼,沒有占用內(nèi)存空間。buf的賦值是按從低地址到高地址的順序進(jìn)行的。從內(nèi)存的分配圖中可以看出,如果buf越界,數(shù)組元素超過12個,就將影響到R4的內(nèi)容。而如1中所述,R4的內(nèi)容是Time_Display()退出后,需要讀取的內(nèi)存地址。如果經(jīng)sprintf()后,buf內(nèi)有15個字符,加上0x00,共16個字符,正好完全覆蓋R4,且R4的最高位為0x00,顯然是一個非法的內(nèi)存空間,因此將進(jìn)入hard fault。如果buf內(nèi)的字符數(shù)落在(12,16)區(qū)間內(nèi),R4的地址合法(仍為0x20開頭),不會進(jìn)入hard fault,但地址已被修改,錯誤的內(nèi)存空間中數(shù)值未知,程序跑飛。這些分析與實際測試結(jié)果是一致的。

問題得到了解釋,也不知花了一天時間分析這些值不值。出錯與否,除了程序本身的正確以外,編譯器將C翻譯成匯編的發(fā)揮程度也是很大的決定因素。想避免這些頭疼的問題,結(jié)論就一句話:數(shù)組不要越界。



關(guān)鍵詞: stm32hardfault堆

評論


技術(shù)專區(qū)

關(guān)閉