# 作者:Roff Segger,麥克泰技術測試、翻譯和編寫
我們使用SEGGER公司的Embedded Studio開發環境進行測試:在一個Cortex-M微控制器上,看看需要使用多少Flash存儲器才能夠完成一個LED燈的閃爍?
目標:
· 使用少于100個字節的程序完成一個閃爍應用
· 使用人眼容易看到的切換頻率(即1-5Hz范圍)
· 主程序用C/C++語言編寫
· 使用方便得到的硬件
· 不使用或禁用工具鏈的運行時系統初始化
本文將大致介紹我們要使用的每一個字節和每一條指令。這是一個了解系統啟動時到底發生了什么,即在到達main()函數之前“底層”發生什么的好途徑。
簡而言之:使用Embedded Studio開發環境可以在使用不到100字節的程序內完成這個工作。
01硬件
我們使用的硬件是一塊STM32跟蹤參考板。它非常簡單,只有一個STM32F407微控制器、3個LED、一個調試/跟蹤接口和一個USB供電端口。
每個J-Trace仿真器交付中包含該開發板,然而,在這里,我僅僅使用常規的J-Link功能下載和調試程序。用戶也可以選擇任何帶LED的硬件測試。
02生成項目
非常簡單,打開Embedded Studio開發環境,從菜單中選擇File -> New Project,選擇第一個選項,創建可執行文件。
根據提示,選擇使用默認值,單擊next幾次后,我最終得到了一個小項目,如下面的Project Explorer窗口中所示。
選擇Build->Build Mini或按F7構建我們的程序。
Debug -> Go或F5啟動調試器。
我們現在沒有連接硬件,所以Embedded Studio要求我們使用內置模擬器。
點擊Yes或點擊Enter啟動模擬器。
調試器停在main()函數處,這是一個標準的 “Hello world”應用程序。
現在,為了實現最小的應用程序,我們將其簡化為一個基本上是空的循環。
int main(void) { int i; do { i++; } while (1);
結果只占用了158字節的Flash。這已經非常不錯了,但是在添加實際LED閃爍功能之前,我需要了解內存的占用,以及如何使我的程序最小化。
為了做到這一點,我可以查看Memory Usage Window、鏈接器映射文件、生成的ELF文件,或者簡單地查看Project Explorer。
從Project Explorer窗口可以知道,這個可執行文件由3個源程序文件構成,以及它們使用了多少Code + RO空間。請注意,這些是編譯器生成對像的數值。對于最終的可執行文件,鏈接器可以消除未使用的功能,或者在必要時添加一些結合層代碼(從Flash跳到RAM或從Thumb指令跳到ARM指令)和填充(如:保證4字節對齊)。
另一個使用Flash存儲器的地方,可能是從庫中鏈接進來的代碼,例如:C運行時庫。然而,我們的小項目并沒有使用庫函數,因此我們不必考慮庫代碼的空間占用。
而且,Project Explorer展示了每個源文件的內存使用情況(2、128和24字節)和項目可執行文件總的內存使用情況:158字節。這和我們在Output窗口中看到的數值相同。
03理解項目結構
這三個文件的用途?我們的應用程序只是一個簡單的main()函數。為什么我還需要另外兩個文件呢?
main.c – 應用程序。
C ortex_M_Startup.s – CPU相關代碼,包含中斷向量表。
SEGGER_THUMB_Startup.s – 應用編程人員不需要修改的代碼。
讓我們更詳細地了解它們,以揭開大家都想知道的謎團:啟動代碼是如何工作的?
有了這些知識,讓我們看看如何縮小我們的應用程序。
04main.c
main.c包含我們的應用,一個最簡單的main()函數。
我們的編譯器足夠智能。它可以看出這個程序什么都不做,并將其優化為只使用一條指令或兩個字節代碼的空循環。
我怎么知道的?我們可以看看main.o,這是編譯器產生的輸出。在Project Explorer中,右鍵單擊main.c->Show Disassembly,或者展開它,雙擊Output files中的main.o。它揭示了主程序只有一個分支。
這是我們的主應用程序。我們已經沒有辦法再簡化它了。
05Cortex_M_Startup.s
Cortex_M_Startup.s包含了應用程序在Cortex-M硬件上執行所需要的CPU相關代碼。它包含中斷向量表和上電或復位時執行的函數:Reset_Handler。
此文件使用了大部分Flash空間。讓我們仔細看看它產生了什么。
Cortex_M_Startup.o顯示其包含中斷向量表 .vectors段及默認的異常處理程序實現。
section .vectors <_vectors> 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 00000000 .word 0x00000000 section .init.NMI_Handler E7FE b 0x00000000 section .init.MemManage_Handler E7FE b 0x00000000 section .init.BusFault_Handler E7FE b 0x00000000 section .init.UsageFault_Handler E7FE b 0x00000000 section .init.SVC_Handler E7FE b 0x00000000 section .init.DebugMon_Handler E7FE b 0x00000000 section .init.PendSV_Handler E7FE b 0x00000000 section .init.SysTick_Handler E7FE b 0x00000000 section .init.Reset_Handler F7FFFFFE bl 0x00000000 F7FFBFFE b.w 0x00000004 section .init.HardFault_Handler 4908 ldr r1, 680A ldr r2, [r1] 2A00 cmp r2, #0 D4FE bmi 0x00000006 F01E0F04 tst lr, #4 BF0C ite eq F3EF8008 mrseq r0, msp F3EF8009 mrsne r0, psp F0424200 orr r2, r2, #0x80000000 600A str r2, [r1] 6981 ldr r1, [r0, #24] 3102 adds r1, #2 6181 str r1, [r0, #24] 4770 bx lr E000ED2C .word 0xE000ED2C
這就是罪魁禍首。
ARM內核定義了向量表中的前16個表項,然后是設備外部中斷的表項。該文件提供了一個有16個表項(或64個字節)的向量表。這些條目僅用于該表。
在應用程序中,我們沒有處理任何故障或中斷,實際上我們只需要Reset_Handler,這是復位立即執行的代碼。我們還需要向量表中的第一個表項,它在復位時完成堆棧指針(SP)的初始化。
因此,我們刪除所有不必要的表項,將此表刪減為兩個表項,同時將消除默認的異常處理程序。
我們重新生成應用程序。不錯!現在應用減少為42個字節。
讓我們看看輸出的elf文件的內容。
從0x0000 0000開始的8個字節:向量表,包含初始化SP和指向Reset_Handler的指針。
從0x0000 001E 開始的8個字節: Reset_Handler,兩條4字節指令。鏈接器插入的一條nop指令,代替SystemInit的調用(在應用程序中不存在),以及一個跳轉到_start的指令。
從0x0000 0008開始的20字節:SEGGER_THUMB_Startup.s的通用運行時初始化,它執行鏈接器生成的對來自SEGGER_init_table的初始化函數的調用,然后,調用main,如果main返回,則停在exit循環中。
從0x0000 0028開始4字節:鏈接器生成了SEGGER_init_table,
其中包含需要在main之前調用的初始化函數。它可能包含段初始化(復制初始化的數據)、段填充(用于0初始化的靜態變量或堆棧的預填充)、堆初始化或全局C++對象的構造函數調用。這些都沒有在我們的應用程序中使用。
最后一條(唯一)指令是跳到運行時初始化的末尾,調用main函數。
加上從0x00000026開始的為對齊SEGGER_int_table的 2個字節的填充,總共是42個字節。
因為應用中沒有使用SystemInit功能,所以我們可以刪除bl SystemInit語句,并用nop取代,以節省4個字節,并減少到38+2=40個字節。
我們的應用程序已經是盡可能小了。下面我們開始添加閃爍代碼!
06添加閃爍代碼
我們編寫了一些用于初始化和控制參考板上LED的代碼和一個簡單的延遲函數。
有了這些代碼,我們就可以創建帶有閃爍功能的主應用程序了,如下所示:
/**************************************** * * main() * * Function description * Application entry point. */ int main(void) { _InitLED(); for (;;) { _SetLED(); _Delay(NUM_DELAY_LOOPS); _ClrLED(); _Delay(NUM_DELAY_LOOPS); } }
完整的源代碼工程可以訪問(可點擊“閱讀原文”):https://blog.segger.com/wp-content/uploads/2020/08/Blinky_Mini.zip
讓我們重新構建并檢查輸出。
成功了!應用程序的大小只有96個字節(需要使用release模式構建,使用debug模式代碼體積會比較大)。
它真的可以運行嗎?讓我們試一試。我們將電路板連接到J-Link,并將J-Link連接到我們的計算機。按F5鍵運行它。就像這個項目開始時一樣,調試會話開始并運行到main函數,只是這次是在實際硬件而非模擬器上。當我們再次點擊F5繼續執行時,我們可以看到開發板上的LED0在閃爍。
07小結
用C語言寫的閃爍程序確實可以放在不到100字節的程序(或者更準確地說是只讀)存儲器中。
啟動代碼不需要那么復雜。它只是完成了硬件的初始化(SystemInit的用途)和運行時系統的初始化。
運行時系統初始化由Embedded Studio和SEGGER鏈接器負責。它確保只包含必要的代碼,以使生成的可執行文件盡可能小。
SEGGER鏈接器還能夠包括特定的初始化,例如:在需要的時候,完成堆的初始化和調用構造函數。這些功能是由鏈接器中的腳本控制。
initialize by symbol __SEGGER_init_heap { block heap }; // Init the heap if there is one initialize by symbol __SEGGER_init_ctors { block ctors }; // Call constructors for globalobjects which need
SEGGER鏈接器生成的啟動代碼非常小,并且易于理解。聯合高效的SEGGER編譯器與模塊化的運行庫和主機端輸出printf()函數,我們就可以傲視群雄了。
看看電腦上簡單的“Hello World”程序的大小,也許我們還應該提供一個可以在電腦上生成相同小程序的SEGGER Studio。
你程序還能更精簡嗎?用你的工具鏈試試,挑戰用100字節寫一個閃爍程序!我相信,在同樣的硬件上,這將是很難被擊敗的。
08這個項目的代碼還能更緊湊嗎?
令人驚訝的答案是:是的。
首先:一些微控制器具有切換寄存器,這允許將循環切割為_ToggleLED() / Delay()。
還有,初始化內容需要的代碼量各不相同,在其他硬件上可能會更小。
但是即使在相同的硬件上,我們也可以進一步減小程序大小。
我們可以將_start放入向量表中,這樣程序就可以在通用啟動代碼中開始執行,從而節省了4字節的跳轉空間。
我們可以刪除exit() 和2字節的分支,因為我們知道main()程序中永遠不會返回。
因為我只想要不到100個字節的程序,所以,讓我們到此為止吧。
祝大家編碼快樂!
-
微控制器
+關注
關注
48文章
7489瀏覽量
151049 -
led燈
+關注
關注
22文章
1592瀏覽量
107837 -
存儲器
+關注
關注
38文章
7452瀏覽量
163606 -
usb
+關注
關注
60文章
7896瀏覽量
263989 -
STM32
+關注
關注
2266文章
10871瀏覽量
354812
原文標題:挑戰用一百個字節寫一個閃爍燈程序!
文章出處:【微信號:麥克泰技術,微信公眾號:麥克泰技術】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論