魔法書之1:利用底層代碼釋放Arduino空間

我是潘,曾經是個工程師。這是為Ardui.Co 製作的 “Arduino 魔法書” 系列的專欄。從現在開始,我將嘗試通過各種方式,去挖掘Arduino的潛能極限。本篇將探討如何利用底層代碼,讓程序變得更小、讓 Arduino 變得更高效,進而讓產品更可靠。

任何機器只能認1和0,二進制是最基礎的機器語言,但從人類的角度來看,幾乎不可能用二進制代碼來編程,效率太低。所以需要更通俗易懂的“高級”語言,C語言就是其中一種,而Arduino的編程語言就是通過C語言的簡化、演變過來。

當編寫好程序後,編譯器(內置在Arduino IDE中)就可以把高級語言,翻譯成機器能懂的二進制代碼。但這個過程存在一個弊端,編譯器總是無法翻譯出最優的代碼,要麼體積臃腫,要麼各種資源浪費。如果我們希望程序變得更高效,就必須往底層深入。

但別誤會,我們不會直接寫二進制代碼。其實,C語言已非常接近底層,只要通過 Arduino 兼容C語言的特性,即可讓程序得到不錯的優化,讓Arduino 的資源有效利用,大幅提升執行效率。

Arduino UNO 的 ROM 空間只有 32K 極為有限,如果你編寫程序時,發現空間不夠,別急著買更高級的板子,而是先看看你的代碼,是否還有壓縮和改進的空間。現在我們嘗試一下,將最簡單的示例程序 Blink (LED閃爍)優化,看看能壓縮多少空間出來。

編譯後,Arduino IDE 會給出二進制代碼的大小,因此我們能直接看到改進的空間:

魔法書之1:利用底層代碼釋放Arduino空間

示例Blink 程序編譯後,二進制代碼的大小是928字節,佔用了Arduino UNO 總空間的2%。

壓縮這個程序前,先看看它是怎樣運作的:

1、配置數字引腳 D13 為輸出;

2、改變這個輸出引腳的狀態,LED亮或滅;

3、暫停一下讓人感知亮/滅時間,然後回到第2步。

這個程序為了初學者明白易懂,所以沒有做任何優化,每一步都清晰簡單:點亮LED,停留一段時間,關閉LED,停留一段時間,再重複。不過,點亮和關閉也可以用一個抽象概念來代替——翻轉LED的狀態。明白這點後,這程序起碼可以壓縮一半。

必須要 pinMode() 嗎?

我們先拿pinMode()函數來開刀,如果把它註釋掉,程序從928字節,減少到864字節,也就是說pinMode() 佔用了64字節的空間。

但沒有pinMode() 引腳就會一直處於輸入狀態。此時,引腳抗阻極高,即使其電平可以被控制,但也無法驅動LED。用什麼底層代碼來實現 pinMode() 的效果呢?首先,我們先得了解pinMode() 的工作原理。

Arduino 基於 Atmel AVR 單片機簡化而來。不同的 AVR 單片機具有不同數量的端口,每個端口有一定數量的引腳。同時,每個端口也關聯著一個數據方向寄存器(data direction register,即DDRx)。

DDRx 決定了端口上不同引腳的輸入輸出狀態。例如,端口 A 有 6 個引腳,給寄存器賦值 000001 的話,前面4個引腳,都是輸入狀態,最後一個引腳則為輸出狀態。pinMode() 簡化將指定端口、寄存器賦值兩個過程完全省略了。我們只要告訴它,某個引腳的狀態,它就會設定設定好。

現在用 bitSet(DDRB, 5) 來代替它。LED的引腳D13 對應 AVR 的是端口B,第5個引腳,因此,我們設置端口B的寄存器 DDRB的第5位為1,即可以設置D13為輸出狀態。

魔法書之1:利用底層代碼釋放Arduino空間

此時D13為輸出狀態了,但程序空間只增加了2字節。不過,bitSet() 為何那麼神奇呢?因為他不是一個函數,而只是一個宏定義。他被包含在編譯器的 wiring.h 頭文件中,它的定義和Arduino 的代碼一起被打包。

這個函數需要兩個參數:值(value)和位(bit)。bitSet(DDRB, 5),在Arduino 編譯時,會直接替換成C語言: DDRB |= 1<<5,其含義是,將DDRB的第5位置位(賦值1)。“I=” 是一個複合賦值運算符,表示按“位”運算,“<

簡單的語句替換,就把笨重的pinMode()佔用的空間節省出來了。 不過,前提是你很熟悉 Arduino 的端口、引腳和C語言的表達。

digitalWrite() 太費力

digitalWrite()雖然很方便,但是代價卻相當大,如果把它註釋掉,足足釋放了226個字節的空間:

魔法書之1:利用底層代碼釋放Arduino空間

Blink 程序的原理是點亮LED,停留一段時間,再熄滅,停留一段時間,再點亮。這個亮-滅交替的過程可以看作是輸出引腳在翻轉。

對於 ATmage168 或者以上型號的芯片,AVR的翻轉方式很簡單,只要向輸入寄存器的地址寫入一個1即可,但對於老的型號ATmega8/16/32則無效。

如果將digitalWrite()函數都換成bitSet(PINB,5);那麼空間可以大幅縮少至:644個字節,只用了4個字節就完成 digitalWrite() 的功能。

魔法書之1:利用底層代碼釋放Arduino空間

程序足夠精簡了嗎?不,loop() 本身就是一個循環,每次循環 LED 都可以翻轉,即亮滅替換,因此loop() 中可以進一步精簡,去掉一個 bitSet(PINB, 5)和delay(1000),效果完全是一樣的,但節約了44字節。

更有效地浪費時間

delay() 純粹為了浪費時間,但也非常浪費系統空間,註釋掉之後系統減少了152個字節,堪比 digitalWrite()。

while(timer0_millis < 1000); // 如果 timer0_millis每一毫秒加1,如果不到1000,就一直執行循環;timer0_millis = 0; // 將 timer0_millis 復位置0,下一個loop() 時重新計數;

這個計數方法使得millis()、delay() 函數完全失效了,因為這些函數都是基於timer0來實現的。

魔法書之1:利用底層代碼釋放Arduino空間

經這一番壓縮,最後Blink 從 928字節壓縮到490字節,差不多隻有原來一半大小,現在你還急著購買空間更大、價格更高的芯片嗎?


分享到:


相關文章: