# 作者:Roff Segger,麥克泰技術(shù)測試、翻譯和編寫
我們使用SEGGER公司的Embedded Studio開發(fā)環(huán)境進(jìn)行測試:在一個(gè)Cortex-M微控制器上,看看需要使用多少Flash存儲器才能夠完成一個(gè)LED燈的閃爍?
目標(biāo):
· 使用少于100個(gè)字節(jié)的程序完成一個(gè)閃爍應(yīng)用
· 使用人眼容易看到的切換頻率(即1-5Hz范圍)
· 主程序用C/C++語言編寫
· 使用方便得到的硬件
· 不使用或禁用工具鏈的運(yùn)行時(shí)系統(tǒng)初始化
本文將大致介紹我們要使用的每一個(gè)字節(jié)和每一條指令。這是一個(gè)了解系統(tǒng)啟動(dòng)時(shí)到底發(fā)生了什么,即在到達(dá)main()函數(shù)之前“底層”發(fā)生什么的好途徑。
簡而言之:使用Embedded Studio開發(fā)環(huán)境可以在使用不到100字節(jié)的程序內(nèi)完成這個(gè)工作。
01硬件
我們使用的硬件是一塊STM32跟蹤參考板。它非常簡單,只有一個(gè)STM32F407微控制器、3個(gè)LED、一個(gè)調(diào)試/跟蹤接口和一個(gè)USB供電端口。
每個(gè)J-Trace仿真器交付中包含該開發(fā)板,然而,在這里,我僅僅使用常規(guī)的J-Link功能下載和調(diào)試程序。用戶也可以選擇任何帶LED的硬件測試。
02生成項(xiàng)目
非常簡單,打開Embedded Studio開發(fā)環(huán)境,從菜單中選擇File -> New Project,選擇第一個(gè)選項(xiàng),創(chuàng)建可執(zhí)行文件。
根據(jù)提示,選擇使用默認(rèn)值,單擊next幾次后,我最終得到了一個(gè)小項(xiàng)目,如下面的Project Explorer窗口中所示。
選擇Build->Build Mini或按F7構(gòu)建我們的程序。
Debug -> Go或F5啟動(dòng)調(diào)試器。
我們現(xiàn)在沒有連接硬件,所以Embedded Studio要求我們使用內(nèi)置模擬器。
點(diǎn)擊Yes或點(diǎn)擊Enter啟動(dòng)模擬器。
調(diào)試器停在main()函數(shù)處,這是一個(gè)標(biāo)準(zhǔn)的 “Hello world”應(yīng)用程序。
現(xiàn)在,為了實(shí)現(xiàn)最小的應(yīng)用程序,我們將其簡化為一個(gè)基本上是空的循環(huán)。
int main(void) { int i; do { i++; } while (1);

結(jié)果只占用了158字節(jié)的Flash。這已經(jīng)非常不錯(cuò)了,但是在添加實(shí)際LED閃爍功能之前,我需要了解內(nèi)存的占用,以及如何使我的程序最小化。
為了做到這一點(diǎn),我可以查看Memory Usage Window、鏈接器映射文件、生成的ELF文件,或者簡單地查看Project Explorer。
從Project Explorer窗口可以知道,這個(gè)可執(zhí)行文件由3個(gè)源程序文件構(gòu)成,以及它們使用了多少Code + RO空間。請注意,這些是編譯器生成對像的數(shù)值。對于最終的可執(zhí)行文件,鏈接器可以消除未使用的功能,或者在必要時(shí)添加一些結(jié)合層代碼(從Flash跳到RAM或從Thumb指令跳到ARM指令)和填充(如:保證4字節(jié)對齊)。
另一個(gè)使用Flash存儲器的地方,可能是從庫中鏈接進(jìn)來的代碼,例如:C運(yùn)行時(shí)庫。然而,我們的小項(xiàng)目并沒有使用庫函數(shù),因此我們不必考慮庫代碼的空間占用。
而且,Project Explorer展示了每個(gè)源文件的內(nèi)存使用情況(2、128和24字節(jié))和項(xiàng)目可執(zhí)行文件總的內(nèi)存使用情況:158字節(jié)。這和我們在Output窗口中看到的數(shù)值相同。
03理解項(xiàng)目結(jié)構(gòu)
這三個(gè)文件的用途?我們的應(yīng)用程序只是一個(gè)簡單的main()函數(shù)。為什么我還需要另外兩個(gè)文件呢?
main.c – 應(yīng)用程序。
C ortex_M_Startup.s – CPU相關(guān)代碼,包含中斷向量表。
SEGGER_THUMB_Startup.s – 應(yīng)用編程人員不需要修改的代碼。
讓我們更詳細(xì)地了解它們,以揭開大家都想知道的謎團(tuán):啟動(dòng)代碼是如何工作的?
有了這些知識,讓我們看看如何縮小我們的應(yīng)用程序。
04main.c
main.c包含我們的應(yīng)用,一個(gè)最簡單的main()函數(shù)。
我們的編譯器足夠智能。它可以看出這個(gè)程序什么都不做,并將其優(yōu)化為只使用一條指令或兩個(gè)字節(jié)代碼的空循環(huán)。
我怎么知道的?我們可以看看main.o,這是編譯器產(chǎn)生的輸出。在Project Explorer中,右鍵單擊main.c->Show Disassembly,或者展開它,雙擊Output files中的main.o。它揭示了主程序只有一個(gè)分支。
這是我們的主應(yīng)用程序。我們已經(jīng)沒有辦法再簡化它了。
05Cortex_M_Startup.s
Cortex_M_Startup.s包含了應(yīng)用程序在Cortex-M硬件上執(zhí)行所需要的CPU相關(guān)代碼。它包含中斷向量表和上電或復(fù)位時(shí)執(zhí)行的函數(shù):Reset_Handler。
此文件使用了大部分Flash空間。讓我們仔細(xì)看看它產(chǎn)生了什么。
Cortex_M_Startup.o顯示其包含中斷向量表 .vectors段及默認(rèn)的異常處理程序?qū)崿F(xiàn)。
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
這就是罪魁禍?zhǔn)住?/p>
ARM內(nèi)核定義了向量表中的前16個(gè)表項(xiàng),然后是設(shè)備外部中斷的表項(xiàng)。該文件提供了一個(gè)有16個(gè)表項(xiàng)(或64個(gè)字節(jié))的向量表。這些條目僅用于該表。
在應(yīng)用程序中,我們沒有處理任何故障或中斷,實(shí)際上我們只需要Reset_Handler,這是復(fù)位立即執(zhí)行的代碼。我們還需要向量表中的第一個(gè)表項(xiàng),它在復(fù)位時(shí)完成堆棧指針(SP)的初始化。
因此,我們刪除所有不必要的表項(xiàng),將此表刪減為兩個(gè)表項(xiàng),同時(shí)將消除默認(rèn)的異常處理程序。
我們重新生成應(yīng)用程序。不錯(cuò)!現(xiàn)在應(yīng)用減少為42個(gè)字節(jié)。
讓我們看看輸出的elf文件的內(nèi)容。
從0x0000 0000開始的8個(gè)字節(jié):向量表,包含初始化SP和指向Reset_Handler的指針。
從0x0000 001E 開始的8個(gè)字節(jié): Reset_Handler,兩條4字節(jié)指令。鏈接器插入的一條nop指令,代替SystemInit的調(diào)用(在應(yīng)用程序中不存在),以及一個(gè)跳轉(zhuǎn)到_start的指令。
從0x0000 0008開始的20字節(jié):SEGGER_THUMB_Startup.s的通用運(yùn)行時(shí)初始化,它執(zhí)行鏈接器生成的對來自SEGGER_init_table的初始化函數(shù)的調(diào)用,然后,調(diào)用main,如果main返回,則停在exit循環(huán)中。
從0x0000 0028開始4字節(jié):鏈接器生成了SEGGER_init_table,
其中包含需要在main之前調(diào)用的初始化函數(shù)。它可能包含段初始化(復(fù)制初始化的數(shù)據(jù))、段填充(用于0初始化的靜態(tài)變量或堆棧的預(yù)填充)、堆初始化或全局C++對象的構(gòu)造函數(shù)調(diào)用。這些都沒有在我們的應(yīng)用程序中使用。
最后一條(唯一)指令是跳到運(yùn)行時(shí)初始化的末尾,調(diào)用main函數(shù)。
加上從0x00000026開始的為對齊SEGGER_int_table的 2個(gè)字節(jié)的填充,總共是42個(gè)字節(jié)。
因?yàn)閼?yīng)用中沒有使用SystemInit功能,所以我們可以刪除bl SystemInit語句,并用nop取代,以節(jié)省4個(gè)字節(jié),并減少到38+2=40個(gè)字節(jié)。
我們的應(yīng)用程序已經(jīng)是盡可能小了。下面我們開始添加閃爍代碼!
06添加閃爍代碼
我們編寫了一些用于初始化和控制參考板上LED的代碼和一個(gè)簡單的延遲函數(shù)。
有了這些代碼,我們就可以創(chuàng)建帶有閃爍功能的主應(yīng)用程序了,如下所示:
/**************************************** * * main() * * Function description * Application entry point. */ int main(void) { _InitLED(); for (;;) { _SetLED(); _Delay(NUM_DELAY_LOOPS); _ClrLED(); _Delay(NUM_DELAY_LOOPS); } }
完整的源代碼工程可以訪問(可點(diǎn)擊“閱讀原文”):https://blog.segger.com/wp-content/uploads/2020/08/Blinky_Mini.zip
讓我們重新構(gòu)建并檢查輸出。
成功了!應(yīng)用程序的大小只有96個(gè)字節(jié)(需要使用release模式構(gòu)建,使用debug模式代碼體積會(huì)比較大)。
它真的可以運(yùn)行嗎?讓我們試一試。我們將電路板連接到J-Link,并將J-Link連接到我們的計(jì)算機(jī)。按F5鍵運(yùn)行它。就像這個(gè)項(xiàng)目開始時(shí)一樣,調(diào)試會(huì)話開始并運(yùn)行到main函數(shù),只是這次是在實(shí)際硬件而非模擬器上。當(dāng)我們再次點(diǎn)擊F5繼續(xù)執(zhí)行時(shí),我們可以看到開發(fā)板上的LED0在閃爍。
07小結(jié)
用C語言寫的閃爍程序確實(shí)可以放在不到100字節(jié)的程序(或者更準(zhǔn)確地說是只讀)存儲器中。
啟動(dòng)代碼不需要那么復(fù)雜。它只是完成了硬件的初始化(SystemInit的用途)和運(yùn)行時(shí)系統(tǒng)的初始化。
運(yùn)行時(shí)系統(tǒng)初始化由Embedded Studio和SEGGER鏈接器負(fù)責(zé)。它確保只包含必要的代碼,以使生成的可執(zhí)行文件盡可能小。
SEGGER鏈接器還能夠包括特定的初始化,例如:在需要的時(shí)候,完成堆的初始化和調(diào)用構(gòu)造函數(shù)。這些功能是由鏈接器中的腳本控制。
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鏈接器生成的啟動(dòng)代碼非常小,并且易于理解。聯(lián)合高效的SEGGER編譯器與模塊化的運(yùn)行庫和主機(jī)端輸出printf()函數(shù),我們就可以傲視群雄了。
看看電腦上簡單的“Hello World”程序的大小,也許我們還應(yīng)該提供一個(gè)可以在電腦上生成相同小程序的SEGGER Studio。
你程序還能更精簡嗎?用你的工具鏈試試,挑戰(zhàn)用100字節(jié)寫一個(gè)閃爍程序!我相信,在同樣的硬件上,這將是很難被擊敗的。
08這個(gè)項(xiàng)目的代碼還能更緊湊嗎?
令人驚訝的答案是:是的。
首先:一些微控制器具有切換寄存器,這允許將循環(huán)切割為_ToggleLED() / Delay()。
還有,初始化內(nèi)容需要的代碼量各不相同,在其他硬件上可能會(huì)更小。
但是即使在相同的硬件上,我們也可以進(jìn)一步減小程序大小。
我們可以將_start放入向量表中,這樣程序就可以在通用啟動(dòng)代碼中開始執(zhí)行,從而節(jié)省了4字節(jié)的跳轉(zhuǎn)空間。
我們可以刪除exit() 和2字節(jié)的分支,因?yàn)槲覀冎續(xù)ain()程序中永遠(yuǎn)不會(huì)返回。
因?yàn)槲抑幌胍坏?00個(gè)字節(jié)的程序,所以,讓我們到此為止吧。
祝大家編碼快樂!
-
微控制器
+關(guān)注
關(guān)注
48文章
7943瀏覽量
154622 -
led燈
+關(guān)注
關(guān)注
22文章
1596瀏覽量
109689 -
存儲器
+關(guān)注
關(guān)注
38文章
7647瀏覽量
167161 -
usb
+關(guān)注
關(guān)注
60文章
8173瀏覽量
272439 -
STM32
+關(guān)注
關(guān)注
2293文章
11031瀏覽量
364298
原文標(biāo)題:挑戰(zhàn)用一百個(gè)字節(jié)寫一個(gè)閃爍燈程序!
文章出處:【微信號:麥克泰技術(shù),微信公眾號:麥克泰技術(shù)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
示波器基礎(chǔ)知識一百個(gè)問答
寫一個(gè)字節(jié)到24c02中(源程序)
UART 發(fā)送數(shù)據(jù)丟失最后一個(gè)字節(jié)
Proteus的51單片機(jī)仿真和程序的一百個(gè)實(shí)例說明

使用51單片機(jī)進(jìn)行EEPROM AT24c02存儲讀取一個(gè)字節(jié)的程序免費(fèi)下載

淺談STM32串口通信(一)基本介紹和一個(gè)字節(jié)傳輸?shù)膶?shí)現(xiàn)

UART發(fā)送數(shù)據(jù)丟失最后一個(gè)字節(jié)

評論