浮点数和对 8 位微控制器内存的影响

Floating point numbers and the effect on 8-bit microcontrollers memory

我目前正在开展一个项目,其中包括使用 linux 中的 SDCC 编译器在 stm-8 微控制器上进行裸机编程。芯片中的内存非常低,所以我试图让事情变得非常精简。我已经使用 8 位和 16 位变量,一切顺利。但是最近我 运行 遇到了一个问题,我真的需要一个浮点变量。所以我写了一个函数,它接受一个 16 位值转换为一个浮点数,做我需要的数学和 returns 一个 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; 
}

好的,一个真正有效的定点版本:

// 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 位整数)。

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

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

作为使用 GCC 5.4.0 和 -Os 的 AVR 的 Godbolt 示例(Godbolt 不支持 STM8 或 SDCC,因此这是作为 8 位架构进行比较)您的代码生成6364 字节与空函数的 4081 字节相比。所以代码体需要的additional代码是2283字节。现在考虑到您同时使用不同的编译器和体系结构这一事实,这些与您的结果没有什么不同。在生成的代码(下方)中查看 rcall__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 位除数,因此它在您的目标上可能会小得多(并且更快)。

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

无论如何,如果您需要挤出一些额外的周期,移动和添加可能比 division 更快。因此,如果您从 124 几乎等于 4096/33 这一事实出发(误差因子为 0.00098,即 0.098%,因此小于千分之一),您可以实现 div与 33 的单个乘法和 12 位的移位(division by 4096)。此外,3332+1,意味着乘以 33 等于左移 5 并再次添加输入。

示例:您想 divide 5000 乘以 124,而 5000/124 大约是。 40.323。我们要做的是:

  1. 5,000 << 5 = 160,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 < 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;
}