面向32位MCU的编译策略

时间:2022-09-19 08:53:53

借助于数百MHz的最大时钟频率、多级流水线、DMA、缓存和快速环境切换等特性,32位mcu(微控制器)能够提供超过1.3DMIPS/MHz的指令处理能力。32位MCu通常具有32个寄存器以及大量的线性存储空间,不再需要复杂的、针对特定设备的寻址和配置。开发者可以依赖编译器来生成不含汇编语言指令的代码。事实上,由于32位设备远比8/16位同类设备更复杂,跟踪寄存器和变量的任务也变得更加艰巨。因此,当代码被移植到32位MCU上时,编译器的作用是必不可少的。而问题的关键在于工程师如何开发程序以充分利用32位设备的强劲潜力。

发挥32位设备最大性能的一个方法是选择一种编译方法,它能够从架构中获益,同时也能为现有代码提供无缝的上/下移植。通常而言,有两种编译方法:传统的编译方法为每个独立的程序进行模块优化并生成目标代码,而“全知(omniscient)”编译方法可以基于所有程序模块组成的整体,在整个程序范围内对代码做优化。

传统编译技术

嵌入式程序通常由C语言编写,为了加快开发速度,这些程序常常被划分为若干小的模块,在分别编译之后再与预编译的库一同进行链接,生成最终的程序。传统的编译器仅在每个单独的代码模块内进行优化,而不能整体优化。由于每个模块都独立编译,编译器无法获得优化寄存器使用、代码重入或在整个程序中使用的变量声明相关信息,如图1所示。

这种方法会导致若干问题。变量与其它对象在各个程序模块之间可能缺乏一致的定义。传统的编译器无法在生成目标代码之前检查这个问题。尽管链接器会检查不同模块间不兼容的变量重复声明,但这增加了复杂度,并且不是总能解决问题,因为链接器可能没有足够的信息来检测人为错误。

许多8/16位嵌入式处理器存储空间是比较复杂的、非线性的,经常有不同的地址宽度。32位MCU通常只有一个线性地址空间,可以更好地与C语言保持一致。在8/16位架构中,如果使用传统的编译器,软件工程师必须使用C语言非标准的扩展来利用存储器架构。传统编译器无法了解一个变量在存储器中的确切位置,也无法了解一个给定的指针究竟指向何处。

为了最有效地利用存储器和寄存器,程序员总是被迫手工撰写汇编语言来优化程序,以及使用C语言的扩展来获得最佳效果。这总是会大大损害程序的可移植性。移植旧有的8/16位代码几乎和重写一样。

全知代码生成消除了汇编语言和C扩展

全知代码生成(OCG)是一种新的编译技术,它不再需要C语言扩展或汇编代码,由于优化了寄存器使用并具有更短的中断响应,它的代码体积更小,执行周期更短。像传统编译器一样,OCG编译器允许代码在不同模块间传递,并且不同模块也是各自独立编译为中间文件。但不同之处在于,这些中间文件是在代码生成阶段之前产生,而不是在链接之前,如图2所示。根据这些中间文件,编译器可以推导出调用关系图,它可以用来识别同时在主代码和中断函数中被调用的函数。这些函数被认为是重入函数,必须要么使用动态堆栈空间来存储局部变量,要么以其它方式管理以确保对其的重入调用不会覆盖已有数据。全知编译器以一种对程序员完全透明的方式自动管理这些函数,不需要任何非标准的C语言扩展。

这种代码生成器也会搜索中间代码库,根据需要找到被程序使用的库函数。一旦调用关系图完成,从未被调用的函数将会被移除,从而精简总代码量。

如果编译栈被使用,代码生成器将会检查所有程序模块中的寄存器、指针、变量、函数和对象。基于对整个程序的全局检查,代码生成器在各方面都能够应用优化技术,为编译栈分配存储器。在代码生成之前,OCG就确切地知道编译栈需要多少空间,定位于存储器何处。

指针引用图

一旦调用关系图完成,编译器就可以在整个程序中为每一个指针建立引用关系图。

确定每一个指针指向的存储器空间是OCG最重要的功能之一。算法利用每一个已经被分配地址的变量,以及每一个指针所指向的地址,建立一张数据或指针引用图,它鉴别出所有可能被指针指向的对象。这些信息被用来确定指针需要访问哪些存储空间,如图3所示。

在被使用的变量和指针集合统计完毕后,OCG为所有对象分配存储器。由于存在多处存储空间(例如,由分组RAM组成的架构),被访问最频繁的变量会被分配在访问代价最低的存储地址。

每一个指针变量现在都有需要访问的地址空间集合。这允许每个指针能够依据特定的架构,以最有效的方式改变字长或重新编码。整个过程都是自动的,无需程序员介入。

机器代码的生成将从调用关系图的底部开始,首先是从那些不调用其它函数的函数开始。如果需要的话,这些函数会被自动内联化。在任何情况下,代码的生成都不会受到太严格的规则限制。然后代码生成将向调用关系图的上部继续。调用规则可以被修改,以适应寄存器的使用和函数的参数类型,而不需要严格遵守模式。

通过动态寄存器调用机制提高32位性能

在典型的RISC架构中,超过30%的执行指令是加载或存储指令,它们都比基于寄存器的指令耗费更长的时间。降低这些指令的数量将有助于提高性能。

嵌入式程序通常由大量彼此调用的函数构建而成。只有当编译器了解变量或参数在整个程序中被使用的频率,它才能有效地利用32位控制器中的寄存器。传统的编译技术对程序整体缺乏了解,在嵌入式软件和编译中,其模块化特性使它不能真正地优化寄存器的使用。

由于覆盖一个寄存器可能会在系统中引发灾难性的后果,因此,传统的编译器针对如何传递参数和返回值制定了一些调用规程。每一条调用规程都包含一系列规则,定义了哪些CPU寄存器需要在调用过程中被保留。

但问题在于传统的编译器仅能了解哪些函数在模块内被调用,无法了解函数在跨模块间的调用情况。它无法知道某个寄存器是否即将被调用函数使用,也无法知道当函数被调用时某个寄存器是否可用。因此它只能使用静态调用机制来避免对寄存器意外的覆盖。然而这些规则可能会导致在寄存器和RAM之间大量不必要的数据转移,有可能会增加时钟周期。当调用关系图很复杂时,这种情况可能会更加严重。

当然,如果编译器更聪明一点,它就可以动态地分配寄存器,从而有可能减少指令、执行周期和SRAM资源。

使用全知代码生成技术的动态调用机制

OCG编译器不再依赖静态调用机制在未知数量的空闲寄存器间分配数据,而是直到对整个程序有

上一篇:便携式系统的RF功率测量方法 下一篇:从FTF 2008看行业未来发展潮流