點擊上方,選擇“置頂/星標公眾號”
前期的自測試能夠更早的發現錯誤,相應的修復成本也會很低,如果你不徹底測試自己的代碼,恐怕你開發的就不只是代碼,可能還會聲名狼藉。
優質嵌入式C程序跟優質的基礎元素關系密切,可以將函數作為基礎元素,我們的測試正是從最基本的函數開始。
判斷哪些函數需要測試需要一定的經驗積累,雖然代碼行數跟邏輯復雜度并不成正比,但如果你不能判斷某個函數是否要測試,一個簡單粗暴的方法是:當函數有效代碼超過20行,就測試它。
程序員對自己的代碼以及邏輯關系十分清楚,測試時,按照每一個邏輯分支全面測試。
很多錯誤發生在我們認為不會出錯的地方,所以即便某個邏輯分支很簡單,也建議測試一遍。
第一個原因是我們自己看自己的代碼總是不容易發現錯誤,而測試能暴露這些錯誤;
另一方面,語法正確、邏輯正確的代碼,經過編譯器編譯后,生成的匯編代碼很可能與你的邏輯相差甚遠。
比如我們使用volatile以及不使用volatile關鍵字編譯后生成的匯編代碼,再比如我們用低優化級別編譯和使用高優化級別編譯后生成的匯編代碼,都可能相差很大,實際運行測試,可以暴漏這些隱含錯誤。
最后,雖然可能性極小,編譯器本身也可能有BUG,特別是構造復雜表達式的情況下(應極力避免復雜表達式)。
使用硬件調試器(比如J-link)測試是最通用的手段。可以單步運行、設置斷點,可以很方便的查看當前寄存器、變量的值。在尋找缺陷方面,使用硬件調試器測試是最簡單卻又最有效的手段。
硬件調試器已經在公司普遍使用,這方面的測試不做介紹,想必大家都已經很熟悉了。
就像沒有一種方法能完美解決所有問題,在實際項目中,硬件調試器也有難以觸及的地方。可以舉幾個例子說明:
比如公司使用lwIP協議棧,如果跟蹤數據的處理過程,需要從接收數據開始一直到應用層處理數據,之間會經過驅動層、IP層、TCP層和應用層,會經過十幾個文件幾十個函數,使用硬件調試器跟蹤費時費力;
有一些缺陷,可能是不定時出現的,有可能是幾分鐘出現,也有可能是幾個小時甚至幾天才出現,像這樣的缺陷很難用硬件調試器捕捉到;
需要外界一系列有時間限制的輸入條件觸發,但這一過程中有缺陷
比如我們用組合鍵來完成某個功能,規定按下按鍵1不小于3秒后松開,然后在6秒內分別按下按鍵2、按鍵3、按鍵4這三個按鍵來執行我們的特定程序,要測試類似這種過程,硬件調試器很難做到;
除了測試缺陷需要,有時候我們在做穩定性測試時,需要知道軟件每時每刻運行到那些分支、執行了哪些操作、我們關心的變量當前值是什么等等,這些都表明,我們還需要一種和硬件調試器互補的測試手段。
這個測試手段就是在程序中增加額外調試語句,當程序運行時,通過這些調試語句將運行信息輸出到可以方便查看的設備上,可以是PC機、LCD顯示屏、存儲卡等等。
以串口輸出到PC機為例,下面提供完整的測試思路。在此之前,我們先對這種測試手段提一些要求:
必須簡單易用
我們在初學C語言的時候,都接觸過printf函數,這個函數可以方便的輸出信息,并可以將各種變量格式化為指定格式的字符串,我們應當提供類似的函數;
在編碼階段,我們可能會往程序中加入大量的調試語句,但是程序發布時,需要將這些調試語句從代碼中移除,這將是件恐怖的過程。
我們必須提供一種策略,可以方便的移除這些調試語句。
I>初始化串口
II>重構fputc函數,printf函數會調用fputc函數執行底層串口的數據發送。
1. /**
2. * @brief 將C庫中的printf函數重定向到指定的串口.
3. * @param ch:要發送的字符
4. * @param f :文件指針
5. */
6. int fputc(int ch, FILE *f)
7. {
8.
9. /*這里是一個跟硬件相關函數,將一個字符寫到UART */
10. //舉例:USART_SendData(UART_COM1, (uint8_t) ch);
11.
12. return ch;
13. }
III> 在Options for Targer窗口,Targer標簽欄下,勾選Use MicroLIB前的復選框以便避免使用半主機功能。
(注:標準C庫printf函數默認開啟半主機功能,如果非要使用標準C庫,請自行查閱資料)
使用庫函數比較方便,但也少了一些靈活性,不利于隨心所欲的定制輸出格式。自己編寫類似printf函數則會更靈活一些,而且不依賴任何編譯器。
下面給出一個完整的類printf函數實現,該函數支持有限的格式參數,使用方法與庫函數一致。同庫函數類似,該也需要提供一個底層串口發送函數(原型為:int32_t UARTwrite(const uint8_t *pcBuf, uint32_t ulLen)),用來發送指定數目的字符,并返回最終發送的字符個數。
1. #include <stdarg.h> /*支持函數接收不定量參數*/
2.
3.constchar * const g_pcHex = "0123456789abcdef";
4.
5./**
6. * 簡介: 一個簡單的printf函數,支持\%c, \%d, \%p, \%s, \%u,\%x, and \%X.
7. */
8.void UARTprintf(const uint8_t *pcString, ...)
9. {
10. uint32_t ulIdx;
11. uint32_t ulValue; //保存從不定量參數堆棧中取出的數值型變量
12. uint32_t ulPos, ulCount;
13. uint32_t ulBase; //保存進制基數,如十進制則為10,十六進制數則為16
14. uint32_t ulNeg; //為1表示從變量為負數
15. uint8_t *pcStr; //保存從不定量參數堆棧中取出的字符型變量
16. uint8_t pcBuf[32]; //保存數值型變量字符化后的字符
17. uint8_t cFill; //'%08x'->不足8個字符用'0'填充,cFill='0';
18. //'%8x '->不足8個字符用空格填充,cFill=' '
19. va_list vaArgP;
20.
21. va_start(vaArgP, pcString);
22. while(*pcString)
23. {
24. // 首先搜尋非%核字符串結束字符
25. for(ulIdx = 0; (pcString[ulIdx] != '%') && (pcString[ulIdx] != '\0'); ulIdx++)
26. { }
27. UARTwrite(pcString, ulIdx);
28.
29. pcString += ulIdx;
30. if(*pcString == '%')
31. {
32. pcString++;
33.
34. ulCount = 0;
35. cFill = ' ';
36. again:
37. switch(*pcString++)
38. {
39. case'0': case'1': case'2': case'3': case'4':
40. case'5': case'6': case'7': case'8': case'9':
41. {
42. // 如果第一個數字為0, 則使用0做填充,則用空格填充)
43. if((pcString[-1] == '0') && (ulCount == 0))
44. {
45. cFill = '0';
46. }
47. ulCount *= 10;
48. ulCount += pcString[-1] - '0';
49. goto again;
50. }
51. case'c':
52. {
53. ulValue = va_arg(vaArgP, unsignedlong);
54. UARTwrite((unsignedchar *)&ulValue, 1);
55. break;
56. }
57. case'd':
58. {
59. ulValue = va_arg(vaArgP, unsignedlong);
60. ulPos = 0;
61.
62. if((long)ulValue < 0)
63. {
64. ulValue = -(long)ulValue;
65. ulNeg = 1;
66. }
67. else
68. {
69. ulNeg = 0;
70. }
71. ulBase = 10;
72. goto convert;
73. }
74. case's':
75. {
76. pcStr = va_arg(vaArgP, unsignedchar *);
77.
78. for(ulIdx = 0; pcStr[ulIdx] != '\0'; ulIdx++)
79. {
80. }
81. UARTwrite(pcStr, ulIdx);
82.
83. if(ulCount > ulIdx)
84. {
85. ulCount -= ulIdx;
86. while(ulCount--)
87. {
88. UARTwrite(" ", 1);
89. }
90. }
91. break;
92. }
93. case'u':
94. {
95. ulValue = va_arg(vaArgP, unsignedlong);
96. ulPos = 0;
97. ulBase = 10;
98. ulNeg = 0;
99. goto convert;
100. }
101. case'x': case'X': case'p':
102. {
103. ulValue = va_arg(vaArgP, unsignedlong);
104. ulPos = 0;
105. ulBase = 16;
106. ulNeg = 0;
107. convert: //將數值轉換成字符
108. for(ulIdx = 1; (((ulIdx * ulBase) <= ulValue) &&(((ulIdx * ulBase) / ulBase) == ulIdx)); ulIdx *= ulBase, ulCount--)
109. { }
110. if(ulNeg)
111. {
112. ulCount--;
113. }
114. if(ulNeg && (cFill == '0'))
115. {
116. pcBuf[ulPos++] = '-';
117. ulNeg = 0;
118. }
119. if((ulCount > 1) && (ulCount < 16))
120. {
121. for(ulCount--; ulCount; ulCount--)
122. {
123. pcBuf[ulPos++] = cFill;
124. }
125. }
126.
127. if(ulNeg)
128. {
129. pcBuf[ulPos++] = '-';
130. }
131.
132. for(; ulIdx; ulIdx /= ulBase)
133. {
134. pcBuf[ulPos++] = g_pcHex[(ulValue / ulIdx) % ulBase];
135. }
136. UARTwrite(pcBuf, ulPos);
137. break;
138. }
139. case'%':
140. {
141. UARTwrite(pcString - 1, 1);
142. break;
143. }
144. default:
145. {
146. UARTwrite("ERROR", 5);
147. break;
148. }
149. }
150. }
151. }
152. //可變參數處理結束
153. va_end(vaArgP);
154. }
上文說到,我們增加的調試語句應能很方便的從最終發行版中去掉,因此我們不能直接調用printf或者自定義的UARTprintf函數,需要將這些調試函數做一層封裝,以便隨時從代碼中去除這些調試語句。參考方法如下:
1. #ifdef MY_DEBUG
2. #define MY_DEBUGF(message) do { \
3. {UARTprintf message;} \
4. } while(0)
5. #else
6. #define MY_DEBUGF(message)
7. #endif /* PLC_DEBUG */
在我們編碼測試期間,定義宏MY_DEBUG,并使用宏MY_DEBUGF(注意比前面那個宏多了一個‘F’)輸出調試信息。
經過預處理后,宏MY_DEBUGF(message)會被UARTprintf message代替,從而實現了調試信息的輸出;
當正式發布時,只需要將宏MY_DEBUG注釋掉,經過預處理后,所有MY_DEBUGF(message)語句都會被空格代替,而從將調試信息從代碼中去除掉。
版權聲明:本文來源網絡,版權歸原作者所有。版權問題,請聯系刪除。
??兄弟們!一個人單打獨斗確實能沖得挺快,但要想走得更遠、更穩,還得靠一群志同道合的伙伴啊!
?? 麥鴿的知識星球現在已經聚集了一波人,大家都在這里互相學習、共同進步。
如果你也想找個靠譜的學習圈子,趕緊 戳鏈接 ?? 加入我們吧!
在這里,你能讀到星球專欄的干貨,優質教程,練手項目,隨時向麥鴿提問,還能幫你定制學習計劃。別猶豫了,兄弟,一起沖!??
往期推薦
嵌入式C語言防御性編程必知的15個黃金法則,讓你的代碼穩如磐石!
國產算力天花板!這款嵌入式AI開發板很能打
電子工程師必考,示波器100問:答對10題才算剛入門!
90%嵌入式工程師中招!12個C語言開發的"血淚陷阱",你踩過幾個?