天天看點

嵌入式系統程式設計和調試技巧

嵌入式系統的開發,軟體的執行穩定可靠是很重要的。在晶片中,軟體是沒有品質的,但軟體的品質能夠決定一顆晶片的成敗。晶片設計中,性能是否能滿足設計要求,除了硬體設計、軟硬體配合的設計技巧,對于軟體來說。程式設計的一些技術和技巧相同重要。

本文講述我在晶片固件開發過程中使用的一些程式設計調試技巧。

針對在嵌入式系統開發中常見的問題,如實時系統下的同步問題,動态記憶體配置設定的記憶體洩漏問題,怎樣在程式設計階段預防BUG出現,調試階段怎樣及時發現問題和定位問題。總結下經驗,目的是開發一個穩定執行的固件,提高開發效率。提高執行性能。

一       程式設計調試技巧

(一)      動态記憶體配置設定還是靜态記憶體配置設定?

嵌入式系統開發中,動态記憶體配置設定和靜态記憶體配置設定各有利弊。動态記憶體配置設定靈活友善。但占用額外記憶體資源,調用時性能有損失,存在記憶體碎片問題。

在無線晶片固件開發中,在兩者的使用上主要考慮一下幾點:

  • CPU性能
  • 可用記憶體大小
  • 須要使用記憶體配置設定的調用頻率
  •  性能影響

在實際設計中:

  • 考慮malloc函數調用的開銷。全部的收發通路上對資料幀的處理、管理使用的資料結構都用靜态記憶體配置設定的方式。目的是為了降低處理時間。
  • l網站管理,密鑰管理,SDIO接口傳遞的外部指令。向上傳送的SDIO事件管理採用動态記憶體配置設定的方式。這些調用和資料幀收發處理次數相比不是一個數量級,有的是偶爾調用。
  • 一次性配置設定。存在于軟體整個生命周期的資料結構變量採用靜态記憶體配置設定。

(二)      使用ARM C庫還是自己寫一個?

嵌入式系統開發中常常會使用到C庫的一些函數,如malloc,free,memcpy,memset。printf,是自己寫一個呢還是利用ARM開發工具提供的C庫呢?

我的習慣和建議是最好使用ARM的C庫。

優勢就是

  • 使用友善。降低開發周期
  • ARM的C庫性能會更好。

我以memcpy為例,網上有篇分析arm memcpy彙編代碼的文章。ARM公司寫的代碼為什麼更優化,性能更好呢?它主要考慮了下面幾點:

  • 源位址和目的位址的首位址的位元組對齊問題
  •  拷貝位元組長度
  • 末尾位元組的對齊問題。
  • 盡量word拷貝
  • 盡量利用arm的批量拷貝彙編指令。

是以我不會去另外寫個memcpy函數。

使用memcpy時僅僅要注意,4-8個長的採用指派方式效率會高些。或者不影響性能的情況下怎麼用都無所謂。

ARMC庫有兩種庫:标準庫和MicroC庫。後者是非線程安全。在裸系統下使用。前者是線程安全的。能夠在實時系統下使用。對malloc的使用使用者須要實作一個保護和釋放保護的函數,供C庫使用。防止多線程調用malloc函數出現的同步問題。

(三)      怎樣預防和發現記憶體洩漏

使用動态記憶體配置設定。系統就可能出現記憶體洩漏,記憶體使用溢出,記憶體反複釋放等問題,假設直接使用malloc和free,非常難發現這種BUG。

在程式設計階段。重定義malloc和free函數能夠及時發現和定位這些問題。讓程式去發現問題,而不是自己去找問題或者根本不知道有問題。

重定義的malloc採用雙向連結清單管理全部動态配置設定的記憶體。以下是管理記憶體使用的資料結構:

嵌入式系統程式設計和調試技巧
嵌入式系統程式設計和調試技巧

每次記憶體配置設定配置設定如上大小的記憶體。包含三個部分,黃色部分為MEMORY_BLOCK資料結構。灰色部分為實際使用記憶體區。紅色部分(4個位元組)為尾部标記。

MEMORY_BLOCK儲存了雙向連結清單,配置設定的檔案名稱指針和檔案行号。配置設定的長度和頭部标記。

這樣在記憶體釋放的時候就能夠通過推斷标記釋放破壞來列印出錯資訊。重定義之後的malloc和free能夠發現的記憶體問題和提示資訊包含:

  • 檢查尾部标記是否破壞。非常可能本塊記憶體使用溢出了,或者被其它地方非法寫了。
  • 檢查頭部标記是否破壞,非常可能被位址上方的其它記憶體破壞,或者被其它地方非法寫了。
  • 在一塊配置設定的記憶體第一次釋放時,會把頭部标記該為其它值。這樣假設有反複釋放的情況,檢查會發現頭部标記和配置設定記憶體時設定的不一緻而發現問題。
  • 在系統退出時,應該全部記憶體都釋放了。這時候檢查記憶體管理連結清單,假設還有節點存在。說明有記憶體洩漏問題。
  • 對于未配置設定的記憶體,釋放指針指向的記憶體區時,因在記憶體管理連結清單中找不到比對的指針值。能夠發現有非法釋放的問題。
  • 出現記憶體釋放問題時,能夠列印調用配置設定函數的檔案名稱和行号,配置設定的大小。假設MEMORY_BLOCK區破壞了,則能夠檢視該資料塊的被填寫的内容作為進一步推斷問題的參考。

重定義的malloc和free函數能夠發現和定位絕大多數的記憶體使用的bug。能夠杜絕記憶體洩漏問題。假設破壞了MEMORY_BLOCK區,則能夠發現有問題。但定位須要自己再推斷。

(四)      注意大小端

在嵌入式系統程式設計中,注意大小端問題是基本要求。

  • 注意訪問的字段是大端還是小端格式的
  • 注意訪問的字段在不同體系結構CPU(大小端不同)的訪問問題。考慮代碼的可移植性問題。
  • 謹慎使用位域定義和操作,easy出現大小端問題。并且位域操作是須要較多指令才幹實作。能夠反彙編比較一下位域操作和位操作編譯結果的不同。在有些嵌入式C語言規範中禁止使用位域也是這種考慮。
  • 也不要有這種想法:我的代碼僅僅會在小端CPU上執行。

假設不想在小端CPU上運作的代碼移植到大端CPU時。改動代碼中大量讀寫操作。

在剛開始寫代碼時注意這個問題能夠避免以後吃大苦頭。

是以編寫大小端訪問的宏是必須的。代碼例如以下:

嵌入式系統程式設計和調試技巧

(五)      注意位元組對齊問題

因如今的嵌入式開發平台大部分是32位CPU,對51以及64位 CPU另外考慮。

通常動态記憶體配置設定記憶體位址是word對齊的。編譯器編譯的結構體變量首位址也是word對齊的。對于結構體中變量定義,以及訪問,對齊問題就須要程式設計者自己注意了。

變量定義基本原則:

  • 對結構體中變量是half-word(2個位元組長)。必須是2的倍數邊界對齊
  • 對結構體中是變量word型(4個位元組長)。必須是4的倍數邊界對齊
  • 對結構體中變量是char型的(1個位元組),能夠随意邊界對齊。假設是數組類型依據長度考慮。

對于以下的資料結構(左邊)定義,

嵌入式系統程式設計和調試技巧
嵌入式系統程式設計和調試技巧

盡管編譯器編譯時會進行位元組填充。建議使用顯式填充的方式定義(如上右資料結構)。

對于變量讀寫操作變量。在變量定義時考慮對齊能夠避免讀寫出現故障,比方代碼中有可能跨word邊界讀寫一個word。

有些CPU體系結構會出現訪問異常,有些CPU體系結構則讀(或寫)了一個錯誤值,但不會異常。對Intel的桌面平台的CPU,跨word邊界讀寫不會有問題。由于CPU已經幫你解決問題。但影響是代碼運作效率變差,這種代碼在windows平台是正常的,但到了嵌入式平台就會出現故障。是以根本的解決方案是在程式設計階段注意這個問題。

比方在無線網卡固件中,須要處理資料包中的字段,有些字段的起始位址是随機的。有可能是word對齊的,也有可能不是,訪問這種word變量時,增加__packedkeyword。

代碼例如以下:

u32data = *((u32 __packed *)da);

這樣編譯器在編譯時會編譯為按位元組讀取再合并為word的彙編代碼,不會出現讀取資料問題。

(六)      時刻關注同步訪問問題

在實時系統應用開發過程中,同步bug是常常碰到且比較難定位的bug。

是以在程式設計階段就進行考慮能避免後面調試時的痛苦。時刻關注同步訪問問題,在程式設計過程中時刻自問下,這個變量的操作是否會出現同步問題,是否有多個線程進行寫操作,釋放時是否還有其它線程在用着呢?以下對開發過程中使用的保護技術進行下介紹。

1          寄存器(變量)寫的同步問題

在嵌入式實時系統中,對寄存器或者記憶體中的變量的讀寫是非常普遍的事情。以寄存器為例。假設是裸系統(不採用實時作業系統),僅僅要把寄存器定義為volatile類型,就能夠避免硬體會異步改動導緻的軟體程式設計編譯之後的訪問運作問題。

在多任務的實時系統中,還須要注意多個任務會對寄存器進行寫操作。

這時候就須要保護操作。比方採用關中斷,信号量或者互相排斥保護的辦法。

比方task1(低優先級)和task2(高優先級)都會讀寫MAC位址寄存器(兩個word長的寄存器MAC_LOW_REG和MAC_HI_REG)。當task1剛寫完MAC位址低四位元組寄存器時,task2開始運作,然後向MAC位址寄存器寫了不同的内容。task2運作完之後再切換回task1運作,task1繼續寫MAC位址寄存器(MAC_HI_REG)。這種後果就是MAC位址寄存器中寫人了非法的内容。

在無線網卡固件中對寄存器操作的基本原則是:

  • 系統初始化讀寫的寄存器一般不須要保護。運作一遍就能夠了。
  • 對任務(或線程)中對寄存器寫操作採用信号量的保護方式。并對寄存器的訪問按功能進行分類。把整個功能端進行保護,防止詳細功能運作一半操作時被打斷。同一時候盡量讓功能端的代碼不要太長。
  • 對僅僅在某個線程中訪問的寄存器能夠不用保護,但原則上還是採用上面一條。
  • 對uart輸出,因僅僅是debug時調用,release的代碼不包括這部分,是以不進行保護。實際使用也沒發現影響系統。

2          動态管理的結構體變量原子操作同步訪問

無線網卡固件會有一些動态配置設定和釋放的結構體變量,比方網站管理。牽涉到多個線程的訪問和釋放。對這種結構體相同須要保護。

防止結構體釋放之後。還有線程會對該結構體進行訪問操作。對已釋放的空間讀寫資料的bug在調試階段比較難發現和定位,是以程式設計階段就須要預防出現這種問題。

程式設計中保護的方法是採用原子操作的方式。詳細的代碼和操作過程例如以下:

嵌入式系統程式設計和調試技巧

       在實作網站管理時,

  • 在增加一個網站時則調用kref_init(sta->kref),初始化原子變量為1
  • 須要訪問這個網站是則調用kref_get(sta->kref),則原子變量為2.
  • 訪問結束之後調用kref_put(sta->kref, release)。則原子變量減為1.
  • 當釋放時則再次調用kref_put(sta->kref, release), 原子變量減為0,調用release函數釋放網站記憶體。
  • 對于多個線程的訪問。由于都是採用kref_get,kref_put對,不會有問題。
  • 對于task2調用釋放網站的函數,假設這時候有task1線程剛擷取了kref_get,則task2釋放網站的函數不會調用release函數。僅僅有task1線程調用kref_put之後,才會真正釋放網站記憶體。這樣就實作了不會對已經釋放記憶體的網站空間進行操作了。

上面的代碼參考了linux核心源碼,對于原子操作須要自己實作,ucos沒有原子操作的函數。對于ARM7和ARM9能夠採用開關中斷的方式實作。對于Cortex-M3能夠採用ARM的原子操作彙編指令實作。

3          雙向連結清單的同步訪問

網卡固件非常多地方都採用了雙向連結清單進行管理,比方網站管理,收發資料管理。對雙向連結清單的操作包含增加一個節點,删除一個節點。除非該連結清單僅僅在一個線程中使用,否則都採用信号量進行保護訪問的方式。

(七)      添加些列印統計資訊

1          輸出統計資訊輔助查找BUG。

嵌入式系統程式設計和調試技巧

在項目的Debug版本号中。利用實時系統建立的第一個任務start task周期性的列印這些統計資訊,為了不影響功能和性能,間隔時間設為30秒。

能夠列印輸出CPU的占用率,上下文切換次數,收發統計,動态記憶體配置設定次數的總的配置設定大小。

在早期系統調試的時候因問題比較多。這些資訊列印能非常快幫助定位問題,比方receive frame count總是固定不變,并且數字為某個特定的值,基本能夠推斷接收停止了。并且為什麼停止。

并且通過接收的總次數和釋放的總次數,以及接收之後的資料流向的個數,推斷是否有未釋放的,是那個子產品處理的時候未釋放。

通過動态記憶體配置設定的資訊列印能夠推斷是否有記憶體洩漏,系統須要的堆大概須要多大。

ucos有CPU占用率的函數,直接調用就能夠擷取了。有些實時系統沒有這種函數。自己能夠實作一個,原理就是實時系統有個系統時鐘,每次觸發,總的tick計數加1,系統空暇則,空暇(idle)任務會把自己的tick次數加1。(1-空暇任務tick數/總的tick數)就是cpu占用率。

2          輸出實時系統的任務相關資訊。

實時系統調試時,須要關注各個任務(線程)任務棧的使用情況。是否存在任務棧配置設定過大和過小的情況。

過大浪費記憶體,過小會棧溢出。了解任務棧的使用情況,配置設定一個合适大小的棧。

ucos有計數棧使用大小的函數,編譯配置時一般不使能。 由于計數會影響性能。原理就是把某個任務棧初始化全0。任務棧使用之後,這塊記憶體區使用的地方就變為非0的值了。由于是棧,是以計算時,從棧頂向下,計數為0的個數直到碰到非0的記憶體位址。

棧大小減去剩下的0的位元組數就是棧的使用大小了。

嵌入式系統程式設計和調試技巧

(八)      按子產品控制列印輸出

無線網卡固件在設計時分多個子產品(線程,接口)。每一個子產品使用專門的列印輸出宏。并定義一個全局變量wl_debug_components用來控制那個子產品須要列印輸出。

比方初始化設定為:

#if DEBUG

u32    wl_debug_components = COMP_INFO | COMP_TX | COMP_RX;

#endif

則代碼執行是對調用COMP_INFO,COMP_TX, COMP_RX級别的列印語句輸出資訊列印。并且能夠通過外部控制,如通過外部接口發送指令,改動wl_debug_components值,讓程式輸出列印和不列印某個子產品的資訊列印。

列印輸出的設計要求:

  • 按子產品列印
  • 外部能夠控制列印輸出
  • debug版本号把列印代碼編譯進去,release版本号不編譯進去。
嵌入式系統程式設計和調試技巧

(九)      巧用開發環境和調試工具

1.       ARM的semihost機制

semihost機制是ARM的特點之中的一個。能夠利用JTAG在沒有序列槽的情況下和調試環境的Command窗體收發相關資訊,如寫個菜單程式,在command窗體輸出菜單,使用者選擇之後讓程式做對應的操作。利用semihost優點是使用友善。在做一些功能性的測試時非常實用,但printf的代碼運作性能比用序列槽更差。

2.       ARM調試器斷點設定工具。

Realview是個非常強力的調試工具,不不過設定斷點。在代碼運作到設定斷點處停下來,還能夠通過設定斷點表達式。讓CPU在指定條件下停下來。

1)在對指定記憶體位址讀寫資料時觸發斷點。在調試過程中,常常會碰到某個區域被寫了非法内容,想知道哪行代碼在運作時進行了這種操作嗎?

嵌入式系統程式設計和調試技巧

該斷點觸發的條件是對位址0x0E001800寫操作的時候,CPU停止運作,這樣就能看到代碼運作到哪兒了。command還能夠讓CPU在停止時再運作某些指令,比方向控制台列印些消息啊,運作某個函數,等等。

2)對某個函數運作n次。或者某行代碼運作n次後停下。

嵌入式系統程式設計和調試技巧

3)條件運作停止。能夠設定斷點,讓全局變量setchar = 10時停止。

這樣就能夠在設定某個指定條件是觸發斷點。讓程式停止運作。

(十)      利用CPU的特性來定位BUG

1          利用ARM CPU的異常模式定位bug

通常能夠ARM CPU的指令異常:預取指異常。資料訪問異常,沒有定義指令異常。比方運作一條非法指令(要麼程式飛了,要麼代碼區破壞了)。非法向僅僅讀區寫資料。預取指異常指向未獲得正确訪問權限的地方取指。一旦出現這種問題,能夠通過檢視正常模式(user模式或者管理模式)的R14連接配接寄存器的值确定運作代碼傳回位址,結合編譯器生成的.map函數映射檔案,确定代碼在運作什麼函數,大概什麼位置出現異常的。這個方案有非常大的機會定位發生故障的地方。

2          利用ARM9 CPU的MPU或者MMU結合上面一條定位BUG。

ARM946E CPU是帶有MPU的,程式設計時,能夠把代碼段和RO字段放在一個區,設定為僅僅讀屬性,RW字段和ZI字段放在另外一個區,設定為讀寫屬性。

這樣在代碼中出現的向僅僅讀區寫資料的問題都能夠捕捉到。非常多bug出現故障時會向0x0位址區寫資料。比方使用設定初值為0的指針型變量,進行寫操作時就會産生資料訪問異常。

(十一)       調試的軟硬體配合

  • 序列槽是個非常好的列印輸出輔助調試裝置。
  • 能夠考慮IO口的輸入輸出。比方button,LED亮燈
  • 利用系統的定時器。對于實時系統,能夠使用os的系統定時器。輸出精度為1ms或者10ms。對于us精度要求的,能夠直接使用CPU上的定時器。比方須要計算某段代碼的運作時間,或者看看錯誤出現的時間,出現的間隔時間。
  • 能夠配合示波器或者邏輯分析儀輸出運作操作的時刻和間隔。

二       結論

對于一個嵌入式開發人員來說,不斷學習和經驗積累。拓寬知識面對提高開發效率幫助非常大。

熟悉自己使用的工具,熟悉CPU的體系結構。細緻閱讀開發工具的幫助手冊,細緻閱讀ARM公司免費的CPU體系結構文檔。免費的ARM公司的編譯工具文檔。這些文檔比書店賣的ARM開發的書有價值的多。

閱讀優秀的代碼,積累程式設計技巧和調試手段。

不管是核心開發。windows驅動開發,linux驅動開發還是嵌入式固件開發。非常多技巧和技術是相通的。

閱讀晶片手冊,包含晶片開發手冊。積累軟硬體配合的設計技巧,結合晶片代碼了解事實上現機制。

調試時關注現象、細節,你的知識面能夠幫助你從現象中非常easy定位問題。