挑战用一百个字节写一个闪烁灯程序!

嵌入式大杂烩 2023-05-12 21:41

本文字数较多,通篇阅读时间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 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x0000000000000000 .word 0x00000000 section .init.NMI_HandlerE7FE b 0x00000000 section .init.MemManage_HandlerE7FE b 0x00000000 section .init.BusFault_HandlerE7FE b 0x00000000 section .init.UsageFault_HandlerE7FE b 0x00000000 section .init.SVC_HandlerE7FE b 0x00000000 section .init.DebugMon_HandlerE7FE b 0x00000000 section .init.PendSV_HandlerE7FE b 0x00000000 section .init.SysTick_HandlerE7FE b 0x00000000 section .init.Reset_HandlerF7FFFFFE bl 0x00000000F7FFBFFE b.w 0x00000004 section .init.HardFault_Handler4908 ldr r1,680A ldr r2, [r1]2A00 cmp r2, #0D4FE bmi 0x00000006F01E0F04 tst lr, #4BF0C ite eqF3EF8008 mrseq r0, mspF3EF8009 mrsne r0, pspF0424200 orr r2, r2, #0x80000000600A str r2, [r1]6981 ldr r1, [r0, #24]3102 adds r1, #26181 str r1, [r0, #24]4770 bx lrE000ED2C .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 oneinitialize 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简明基础知识

在公众号聊天界面回复1024,可获取嵌入式资源;回复 ,可查看文章汇总

嵌入式大杂烩 专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!
评论
  • 百佳泰特为您整理2024年12月各大Logo的最新规格信息。——————————USB▶ 百佳泰获授权进行 USB Active Cable 认证。▶ 所有符合 USB PD 3.2 标准的产品都有资格获得USB-IF 认证——————————Bluetooth®▶ Remote UPF Testing针对所有低功耗音频(LE Audio)和网格(Mesh)规范的远程互操作性测试已开放,蓝牙会员可使用该测试,这是随时测试产品的又一绝佳途径。——————————PCI Express▶ 2025年
    百佳泰测试实验室 2024-12-20 10:33 139浏览
  • 光耦固态继电器(SSR)作为现代电子控制系统中不可或缺的关键组件,正逐步取代传统机械继电器。通过利用光耦合技术,SSR不仅能够提供更高的可靠性,还能适应更加复杂和严苛的应用环境。在本文中,我们将深入探讨光耦固态继电器的工作原理、优势、挑战以及未来发展趋势。光耦固态继电器:如何工作并打破传统继电器的局限?光耦固态继电器通过光电隔离技术,实现输入信号与负载之间的电气隔离。其工作原理包括三个关键步骤:光激活:LED接收输入电流并发出与其成比例的光信号。光传输:光电传感器(如光电二极管或光电晶体管)接收
    腾恩科技-彭工 2024-12-20 16:30 102浏览
  • 耳机虽看似一个简单的设备,但不仅只是听音乐功能,它已经成为日常生活和专业领域中不可或缺的一部分。从个人娱乐到专业录音,再到公共和私人通讯,耳机的使用无处不在。使用高质量的耳机不仅可以提供优良的声音体验,还能在长时间使用中保护使用者听力健康。耳机产品的质量,除了验证产品是否符合法规标准,也能透过全面性的测试和认证过程,确保耳机在各方面:从音质到耐用性,再到用户舒适度,都能达到或超越行业标准。这不仅保护了消费者的投资,也提升了该公司在整个行业的产品质量和信誉!客户面临到的各种困难一家耳机制造商想要透
    百佳泰测试实验室 2024-12-20 10:37 219浏览
  • 随着工业自动化和智能化的发展,电机控制系统正向更高精度、更快响应和更高稳定性的方向发展。高速光耦作为一种电气隔离与信号传输的核心器件,在现代电机控制中扮演着至关重要的角色。本文将详细介绍高速光耦在电机控制中的应用优势及其在实际工控系统中的重要性。高速光耦的基本原理及优势高速光耦是一种光电耦合器件,通过光信号传递电信号,实现输入输出端的电气隔离。这种隔离可以有效保护电路免受高压、电流浪涌等干扰。相比传统的光耦,高速光耦具备更快的响应速度,通常可以达到几百纳秒到几微秒级别的传输延迟。电气隔离:高速光
    晶台光耦 2024-12-20 10:18 171浏览
  • ALINX 正式发布 AMD Virtex UltraScale+ 系列 FPGA PCIe 3.0 综合开发平台 AXVU13P!这款搭载 AMD 16nm 工艺 XCVU13P 芯片的高性能开发验证平台,凭借卓越的计算能力和灵活的扩展性,专为应对复杂应用场景和高带宽需求而设计,助力技术开发者加速产品创新与部署。随着 5G、人工智能和高性能计算等领域的迅猛发展,各行业对计算能力、灵活性和高速数据传输的需求持续攀升。FPGA 凭借其高度可编程性和实时并行处理能力,已成为解决行业痛点的关
    ALINX 2024-12-20 17:44 124浏览
  • 汽车行业的变革正愈演愈烈,由交通工具到“第三生活空间”。业内逐渐凝聚共识:汽车的下半场在于智能化。而智能化的核心在于集成先进的传感器,以实现高等级的智能驾驶乃至自动驾驶,以及更个性、舒适、交互体验更优的智能座舱。毕马威中国《聚焦电动化下半场 智能座舱白皮书》数据指出,2026年中国智能座舱市场规模将达到2127亿元,5年复合增长率超过17%。2022年到2026年,智能座舱渗透率将从59%上升至82%。近日,在SENSOR CHINA与琻捷电子联合举办的“汽车传感系列交流会-智能传感专场”上,艾
    艾迈斯欧司朗 2024-12-20 19:45 172浏览
  • 光耦合器,也称为光隔离器,是用于电气隔离和信号传输的多功能组件。其应用之一是测量电路中的电压。本文介绍了如何利用光耦合器进行电压测量,阐明了其操作和实际用途。使用光耦合器进行电压测量的工作原理使用光耦合器进行电压测量依赖于其在通过光传输信号的同时隔离输入和输出电路的能力。该过程包括:连接到电压源光耦合器连接在电压源上。输入电压施加到光耦合器的LED,LED发出的光与施加的电压成比例。光电二极管响应LED发出的光由输出侧的光电二极管或光电晶体管检测。随着LED亮度的变化,光电二极管的电阻相应减小,
    腾恩科技-彭工 2024-12-20 16:31 129浏览
  • 国产数字隔离器已成为现代电子产品中的关键部件,以增强的性能和可靠性取代了传统的光耦合器。这些隔离器广泛应用于医疗设备、汽车电子、工业自动化和其他需要强大信号隔离的领域。准确测试这些设备是确保其质量和性能的基本步骤。如何测试数字隔离器测试数字隔离器需要精度和正确的工具集来评估其在各种条件下的功能和性能。以下设备对于这项任务至关重要:示波器:用于可视化信号波形并测量时序特性,如传播延迟、上升时间和下降时间。允许验证输入输出信号的完整性。频谱分析仪:测量电磁干扰(EMI)和其他频域特性。有助于识别信号
    克里雅半导体科技 2024-12-20 16:35 103浏览
  •                                                窗        外       年底将近,空气变得格外寒冷,估计这会儿北方已经是千里
    广州铁金刚 2024-12-23 11:49 62浏览
  • //```c #include "..\..\comm\AI8051U.h"  // 包含头文件,定义了硬件寄存器和常量 #include "stdio.h"              // 标准输入输出库 #include "intrins.h"         &n
    丙丁先生 2024-12-20 10:18 90浏览
  • Supernode与艾迈斯欧司朗携手,通过Belago红外LED实现精准扫地机器人避障;得益于Belago出色的红外补光功能,使扫地机器人能够大大提升其识别物体的能力,实现精准避障;Belago点阵照明器采用迷你封装,兼容标准无铅回流工艺,适用于各种3D传感平台,包括移动设备、物联网设备和机器人。全球领先的光学解决方案供应商艾迈斯欧司朗(瑞士证券交易所股票代码:AMS)近日宣布,与国内领先的多行业三维视觉方案提供商超节点创新科技(Supernode)双方联合推出采用艾迈斯欧司朗先进Belago红
    艾迈斯欧司朗 2024-12-20 18:55 120浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦