本文字数较多,通篇阅读时间10分钟
# 作者: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 – 应用编程人员不需要修改的代码。
让我们更详细地了解它们,以揭开大家都想知道的谜团:启动代码是如何工作的?
有了这些知识,让我们看看如何缩小我们的应用程序。
04
main.c
main.c包含我们的应用,一个最简单的main()函数。
我们的编译器足够智能。它可以看出这个程序什么都不做,并将其优化为只使用一条指令或两个字节代码的空循环。
我怎么知道的?我们可以看看main.o,这是编译器产生的输出。在Project Explorer中,右键单击main.c->Show Disassembly,或者展开它,双击Output files中的main.o。它揭示了主程序只有一个分支。
这是我们的主应用程序。我们已经没有办法再简化它了。
05
Cortex_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个字节的程序,所以,让我们到此为止吧。
祝大家编码快乐!
本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。
由于微信公众号近期改变了推送规则,为了防止找不到,可以星标置顶,这样每次推送的文章才会出现在您的订阅列表里。
猜你喜欢:
嵌入式工程师面试,如何应对HR这些提问?
Hello系列 | cmake简明基础知识