【问题标题】:Floating point numbers and the effect on 8-bit microcontrollers memory浮点数及其对 8 位微控制器内存的影响
【发布时间】:2020-07-12 11:14:12
【问题描述】:

我目前正在从事一个项目,该项目包括在 stm-8 微控制器上使用 linux 中的 SDCC 编译器进行裸机编程。芯片中的内存非常低,所以我试图保持精简。我已经使用了 8 位和 16 位变量,一切都很顺利。但最近我遇到了一个问题,我真的需要一个浮点变量。因此,我编写了一个函数,该函数将 16 位值转换为浮点数,执行我需要的数学运算并返回一个 8 位数字。这导致我在 MCU 上的最终编译代码从 1198 字节变为 3462 字节。现在我明白了使用浮点是内存密集型的,并且可能需要调用许多函数来处理浮点数的使用,但是将程序的大小增加这么多似乎很疯狂。我需要一些帮助来理解为什么会这样以及到底发生了什么。

规格:MCU stm8151f2 编译器:带有 --opt_code_size 选项的 SDCC

int roundNo(uint16_t bit_input) 
{ 
    float num = (((float)bit_input) - ADC_MIN)/124.0;
    return num < 0 ? num - 0.5 : num + 0.5; 
}

【问题讨论】:

  • @supercat 输入的数字是无符号的,但这就是我删除评论的原因。但是,在进行减法之后,仍然可以使用类似的方法。如果(bit_input - ADC_MIN)&gt;= 0,则可以通过(bit_input - ADC_MIN + 62) / 124; 进行舍入。这有效地添加了一半(62 / 124)除法之前,在负数时使用类似的操作。
  • @EugeneSh。我发现它是默认生成的地图,里面有很多很好的信息。我想这只是一件正常的事情吗?当您进行浮点运算时,它会带来很多负担吗?虽然我仍然认为超过 2K 的行李很多。
  • 是的,那种包袱很正常。
  • 与其问为什么 FP 如此耗费资源,不如问一下如何在不需要 FP 的情况下实现该功能不是更有用吗?完全没有必要在浮点中实现这个相当微不足道的功能。
  • 我同意 Clifford 的观点:不使用浮点运算更容易编写代码。例如,使用小数类型(即几个整数)和小数运算通常比使用浮点数(在没有 FPU 的 CPU 上)更快、更轻量级。

标签: c embedded sdcc


【解决方案1】:

要确定为什么代码在您的特定工具链上如此之大,您需要查看生成的汇编代码,查看 FP 支持调用它的代码,然后查看映射文件以确定每个代码的大小那些功能。

作为Godbolt 的示例,AVR 使用 GCC 5.4.0 和 -Os(Godbolt 不支持 STM8 或 SDCC,因此这是作为 8 位架构进行比较)您的代码生成 6364 字节,而对于 4081 字节一个空函数。所以代码体所需的附加代码是2283字节。现在考虑到您同时使用不同的编译器和体系结构这一事实,这些与您的结果并没有什么不同。在生成的代码(如下)中查看 rcalls 到子例程,例如 __divsf3 - 这些是大部分代码所在的位置,我怀疑 FP 除法是迄今为止更大的贡献者。

roundNo(unsigned int):
        push r12
        push r13
        push r14
        push r15
        mov r22,r24
        mov r23,r25
        ldi r24,0
        ldi r25,0
        rcall __floatunsisf
        ldi r18,0
        ldi r19,0
        ldi r20,0
        ldi r21,lo8(69)
        rcall __subsf3
        ldi r18,0
        ldi r19,0
        ldi r20,lo8(-8)
        ldi r21,lo8(66)
        rcall __divsf3
        mov r12,r22
        mov r13,r23
        mov r14,r24
        mov r15,r25
        ldi r18,0
        ldi r19,0
        ldi r20,0
        ldi r21,0
        rcall __ltsf2
        ldi r18,0
        ldi r19,0
        ldi r20,0
        ldi r21,lo8(63)
        sbrs r24,7
        rjmp .L6
        mov r25,r15
        mov r24,r14
        mov r23,r13
        mov r22,r12
        rcall __subsf3
        rjmp .L7
.L6:
        mov r25,r15
        mov r24,r14
        mov r23,r13
        mov r22,r12
        rcall __addsf3
.L7:
        rcall __fixsfsi
        mov r24,r22
        mov r25,r23
        pop r15
        pop r14
        pop r13
        pop r12
        ret

您需要对工具链生成的代码执行相同的分析才能回答您的问题。毫无疑问,SDCC 能够生成汇编列表和映射文件,这将允许您准确确定正在生成和链接的代码和 FP 支持。

虽然你在这种情况下使用 FP 是完全没有必要的:

int roundNo(uint16_t bit_input) 
{ 
  int s = (bit_input - ADC_MIN) ;
  s += s < 0 ? -62 : 62 ;
  return s / 124 ;
}

Godbolt 2283 字节与空函数相比。仍然有点大,但最有可能的问题是 AVR 缺少 DIV 指令,因此调用 __divmodhi4。 STM8 有一个DIV 用于 16 位除数和 8 位除数,因此它在您的目标上可能会更小(并且更快)。

【讨论】:

  • 请注意:我认为知道宏 ADC_MIN 的值(例如,如果它是浮点数或整数等)对于重写没有浮点数的函数很重要做了。
  • @Clifford 感谢您的解释,是的,完全没有必要使用 FP 编号我被要求发布代码,所以我这样做了。我已经删除了 FP 的使用。但是你的答案是我一直在寻找的我从来没有意识到添加像双精度和浮点数这样的东西需要多少内存。感谢您的深入解释。我将继续研究子例程和链接器映射文件以了解更多信息。
  • @LucaPolito :如果所需的结果是 float 我同意,但因为它是 int 并且该函数的唯一目的似乎是缩放和舍入,似乎不太可能使ADC_MIN 成为float 将有任何用处。
  • @cameroony :请注意,124 可能是一个不幸的红利。如果您可以使用 128,它将解析为 7 位移位,从而使 Godbolt 上的边际代码大小达到 940 字节 - 可能与 STM8 DIV 相当,但更具确定性 - DIV 需要 2 到 17 个机器周期,而恰好 2 STM8 上的 SRAW(右移 16 位)。
  • @cameroony:一个警告:如果 int 是 16 位的(我怀疑它在这个架构上是 32 位的),那么 bit_input - ADC_MIN 将始终是无符号的(并导致无符号溢出如果bit_input 小于ADC_MIN),则转换为int 是实现定义的。它很可能会按预期工作,但我要么先检查这种情况,要么完全删除s &lt; 0 部分,如果在所有情况下都知道bit_input &gt; ADC_MIN。如果bit_input &lt; ADC_MIN 是读取错误,那么if (bit_input &lt; ADC_MIN) return 0; 也可能有意义。
【解决方案2】:

好的,一个实际工作的定点版本:

// Assume a 28.4 format for math.  12.4 can be used, but roundoff may occur.

// Input should be a literal float (Note that the multiply here will be handled by the  
// compiler and not generate FP asm code. 
#define TO_FIXED(x) (int)((x * 16))

// Takes a fixed and converts to an int - should turn into a right shift 4.
#define TO_INT(x)   (int)((x / 16))

typedef int FIXED;
const uint16_t ADC_MIN = 32768;

int roundNo(uint16_t bit_input) 
{ 
  FIXED num = (TO_FIXED(bit_input - ADC_MIN)) / 124;
  num += num < 0 ? TO_FIXED(-0.5) : TO_FIXED(0.5);
  return TO_INT(num);
}

int main()
{
  printf("%d", roundNo(0));

  return 0;
}

请注意,我们在这里使用了一些 32 位值,因此它会大于您当前的值。不过要小心,如果可以仔细管理舍入和溢出,它可能会转换回 12.4(16 位 int)。

或者从网上获取一个更好的全功能定点库:)

【讨论】:

  • 虽然这可能是他应该做的,但这并不是对所提问题的回答。而是问题是为什么 FP 代码如此之大。除此之外,还有一个更简单的解决方案;它甚至不需要定点。
【解决方案3】:

(更新) 写完这篇文章后,我注意到@Clifford 提到你的微控制器本身就支持这个DIV 指令,在这种情况下这样做是多余的。无论如何,我将把它作为一个概念,它可以应用于DIV 被实现为外部调用的情况,或者DIV 需要太多周期并且目标是加快计算速度的情况。

无论如何,如果您需要压缩一些额外的周期,移位和加法可能比除法更快。因此,如果从124 几乎等于4096/33 的事实开始(误差因子为0.00098,即0.098%,因此小于千分之一),则可以通过与33 的单次乘法来实现除法并移位 12 位(除以 4096)。此外,3332+1,意思是乘以33 等于左移5 并再次添加输入。

示例:您想将5000 除以124,而5000/124 约为。 40.323。我们要做的是:

  1. 5,000
  2. 160,000 + 5,000 = 165,000
  3. 165,000 >> 12 = 40

请注意,这仅适用于正数。 另请注意,如果您确实在整个代码中进行了大量乘法运算,那么只有一个 extern muldiv 函数从长远来看,可能会导致整体代码更小,尤其是在编译器不是特别擅长优化的情况下。如果编译器可以在这里发出一个DIV 指令,那么你唯一能得到的就是一点点速度的提高,所以不要为此烦恼。

#include <stdint.h>
#define ADC_MIN 2048

uint16_t roundNo(uint16_t bit_input) 
{ 
    // input too low, return zero
    if (bit_input < ADC_MIN)
        return 0;

    bit_input -= (ADC_MIN - 62);
    uint32_t x = bit_input;

    // this gets us x = x * 33        
    x <<= 5;
    x += bit_input;

    // this gets us x = x / 4096
    x >>= 12;

    return (uint16_t)x;
}

具有大小优化的 GCC AVR 产生this,即所有对 extern mul 或 div 函数的调用都消失了,但似乎 AVR 不支持在单个指令中移动多个位(它发出循环移动 5 次和分别为 12 次)。我不知道你的编译器会做什么。

如果您还需要处理bit_input &lt; ADC_MIN的情况,我会单独处理这部分,即:

#include <stdint.h>
#include <stdbool.h>
#define ADC_MIN 2048

int16_t roundNo(uint16_t bit_input) 
{ 
    // if subtraction would result in a negative value,
    // handle it properly
    bool negative = (bit_input < ADC_MIN);
    bit_input = negative ? (ADC_MIN - bit_input) : (bit_input - ADC_MIN);

    // we are always positive from this point on
    bit_input -= (ADC_MIN - 62);

    uint32_t x = bit_input;
    x <<= 5;
    x += bit_input;
    x >>= 12;

    return negative ? -(int16_t)x : (int16_t)x;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-24
    • 2015-01-10
    • 1970-01-01
    • 1970-01-01
    • 2011-09-19
    • 2013-09-12
    • 1970-01-01
    相关资源
    最近更新 更多