0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

麦克泰技术 来源:麦克泰技术 作者:麦克泰技术 2023-05-16 15:59 次阅读

# 作者:Roff Segger,麦克泰技术测试、翻译和编写

我们使用SEGGER公司的Embedded Studio开发环境进行测试:在一个Cortex-M微控制器上,看看需要使用多少Flash存储器才能够完成一个LED灯的闪烁?

目标:

· 使用少于100个字节的程序完成一个闪烁应用

· 使用人眼容易看到的切换频率(即1-5Hz范围)

· 主程序用C/C++语言编写

· 使用方便得到的硬件

· 不使用或禁用工具链的运行时系统初始化

本文将大致介绍我们要使用的每一个字节和每一条指令。这是一个了解系统启动时到底发生了什么,即在到达main()函数之前“底层”发生什么的好途径。

简而言之:使用Embedded Studio开发环境可以在使用不到100字节的程序内完成这个工作。

01硬件

我们使用的硬件是一块STM32跟踪参考板。它非常简单,只有一个STM32F407微控制器、3个LED、一个调试/跟踪接口和一个USB供电端口

4f44b80a-f0c9-11ed-90ce-dac502259ad0.jpg

每个J-Trace仿真器交付中包含该开发板,然而,在这里,我仅仅使用常规的J-Link功能下载和调试程序。用户也可以选择任何带LED的硬件测试。

02生成项目

非常简单,打开Embedded Studio开发环境,从菜单中选择File -> New Project,选择第一个选项,创建可执行文件。

4f6b8fb6-f0c9-11ed-90ce-dac502259ad0.png

根据提示,选择使用默认值,单击next几次后,我最终得到了一个小项目,如下面的Project Explorer窗口中所示。

4f9a400e-f0c9-11ed-90ce-dac502259ad0.png

选择Build->Build Mini或按F7构建我们的程序。

Debug -> Go或F5启动调试器。

我们现在没有连接硬件,所以Embedded Studio要求我们使用内置模拟器。

4fb7ddb2-f0c9-11ed-90ce-dac502259ad0.png

点击Yes或点击Enter启动模拟器。

调试器停在main()函数处,这是一个标准的 “Hello world”应用程序。

4fd6ff44-f0c9-11ed-90ce-dac502259ad0.png

现在,为了实现最小的应用程序,我们将其简化为一个基本上是空的循环。

int main(void) {
 int i; 
 
 do {
  i++;
 } while (1);
5000f498-f0c9-11ed-90ce-dac502259ad0.png

结果只占用了158字节的Flash。这已经非常不错了,但是在添加实际LED闪烁功能之前,我需要了解内存的占用,以及如何使我的程序最小化。

为了做到这一点,我可以查看Memory Usage Window、链接器映射文件、生成的ELF文件,或者简单地查看Project Explorer。

从Project Explorer窗口可以知道,这个可执行文件由3个源程序文件构成,以及它们使用了多少Code + RO空间。请注意,这些是编译器生成对像的数值。对于最终的可执行文件,链接器可以消除未使用的功能,或者在必要时添加一些结合层代码(从Flash跳到RAM或从Thumb指令跳到ARM指令)和填充(如:保证4字节对齐)。

另一个使用Flash存储器的地方,可能是从库中链接进来的代码,例如:C运行时库。然而,我们的小项目并没有使用库函数,因此我们不必考虑库代码的空间占用。

502279e2-f0c9-11ed-90ce-dac502259ad0.png

而且,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。它揭示了主程序只有一个分支。

5045cfc8-f0c9-11ed-90ce-dac502259ad0.png

这是我们的主应用程序。我们已经没有办法再简化它了。

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个字节。

5061ec3a-f0c9-11ed-90ce-dac502259ad0.png5083fc58-f0c9-11ed-90ce-dac502259ad0.png

让我们看看输出的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个字节。

50a46984-f0c9-11ed-90ce-dac502259ad0.png50c193f6-f0c9-11ed-90ce-dac502259ad0.png

因为应用中没有使用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

    文章

    6812

    浏览量

    147648
  • led灯
    +关注

    关注

    22

    文章

    1554

    浏览量

    107032
  • 存储器
    +关注

    关注

    38

    文章

    7151

    浏览量

    162001
  • usb
    usb
    +关注

    关注

    59

    文章

    7436

    浏览量

    258207
  • STM32
    +关注

    关注

    2240

    文章

    10674

    浏览量

    348806

原文标题:挑战用一百个字节写一个闪烁灯程序!

文章出处:【微信号:麦克泰技术,微信公众号:麦克泰技术】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    8同时闪烁怎么做到?

    大家好,我是新手学习C语言,我的是普中开发实验板,想通过P0口点亮所以同时闪烁,怎么做
    发表于 04-17 16:15

    求助:帮忙让布尔里面圆形指示每秒闪烁50次的程序 谢谢

    感觉自己学的太水,能不能请会的同学帮忙让布尔里面圆形指示每秒闪烁50次的程序 谢谢!可以
    发表于 07-15 17:06

    关于2个字节合并成一个字的问题

    请教各位大神,我看网上不少程序将2个字节合并成一个字是这样的unsigned int x;unsigned char h,l;x=h
    发表于 09-09 21:52

    verilog的RS232收发3个字节的代码啊!

    最近在学verilog,看到了特权同学的uart代码,但是只能收发1个字节,请问有大神有收发3个字节的代码么~最好是基于特权同学的的,或者有详细注释也行,本人比较笨~
    发表于 10-09 17:06

    怎么labview设计TCP/IP的通讯程序,从客户端向另外客户端发送8个字节的数

    labview设计TCP/IP的通讯程序,从客户端向另外客户端发送8
    发表于 11-24 11:42

    OMAPL138访问FSRAM的奇怪问题,1个字节会改变4个字节

    专家你好:我OMAPL138处理器ARM核控制FM28V100(FSRAM)的时候遇到问题,片选(CS2),地址线(EMA[12-0]),数据线D[7-0],都已经配置,在SR
    发表于 06-21 05:17

    modbus协议为什么发送时是个字节表示的寄存器数量,返回时却是一个字节字节数?

    从机响应格式为:从机地址|功能码|字节数|数据1高位|数据1低位|...|CRC高字节|CRC低字节为什么发送时是个字节表示的寄存器数量
    发表于 11-06 08:43

    为什么板子上显示的是11个字节

    的发烧友mini版,在官方提供的8266程序作了修改,发现在将接收区的USART2_RX_BUF和特定字符串进行比较,没有办法执行判断语句,只能根据字符串长度来判断。另外+IPD,0,3:明明是九个字节,为嘛板子上显示是11
    发表于 06-13 04:35

    请问ATT_WriteCharValue如何个字节的数据?

    主机通过GATT_WriteCharValue,只能写一个字节。在从机程序里面怎么设置特征值可以个字节???
    发表于 03-18 09:55

    怎么EEPROM上写入一个字节

    在EEPROM上写入了一个字节,并且我能够读取它(除了我试图读取些值……)。无论我如何可以单词并读它,要么
    发表于 04-13 11:03

    STM32F10xUART接收多个字节连续发送的信息出现程序卡死的原因?

    STM32F10xUART接收多个字节连续发送的信息出现程序卡死的原因?
    发表于 12-09 07:19

    为什么UARTBootloader代码运行后发送出来多了一个字节

    如何用UART去段Bootloader代码呢?为什么UARTBootloader代码运行后发送出来多了一个字节?怎样解决?
    发表于 12-14 06:21

    串口通信是一个字节一个字节的发送与接收怎么解决?

    题目: 8XX51单片机的双机通信系统波特率为9600bps,fosc=12MHz,中断方式编写程序,将甲机片内RAM 30H ~ 3FH的数据块通过串行口传送到乙机的片内RAM
    发表于 02-18 06:31

    Bluedroid如何使用超过20个字节的特性?

    使用 Bluedroid,我设法创建了具有 2 特征的服务。我可以使用 BLE 终端连接到我的设备,我可以为第一个特征些东西。WRITE_EVT 在
    发表于 04-14 07:49

    Proteus的51单片机仿真和程序一百个实例说明

    本文档的主要内容详细介绍的是Proteus的51单片机仿真和程序一百个实例说明。
    发表于 01-07 08:00 82次下载
    Proteus的51单片机仿真和<b class='flag-5'>程序</b>的<b class='flag-5'>一百个</b>实例说明