浮点数和对 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
)。此外,33
是 32+1
,意味着乘以 33
等于左移 5 并再次添加输入。
示例:您想 divide 5000
乘以 124
,而 5000/124
大约是。 40.323
。我们要做的是:
- 5,000 << 5 = 160,000
- 160,000 + 5,000 = 165,000
- 165,000 >> 12 = 40
请注意,这仅适用于正数。另请注意,如果您真的在整个代码中进行大量乘法运算,那么只有一个 extern mul
或 div
函数可能会导致长 运行 中的整体代码更小,尤其是在编译器不是特别擅长优化的情况下。如果编译器可以在这里发出一条 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;
}
我目前正在开展一个项目,其中包括使用 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
)。此外,33
是 32+1
,意味着乘以 33
等于左移 5 并再次添加输入。
示例:您想 divide 5000
乘以 124
,而 5000/124
大约是。 40.323
。我们要做的是:
- 5,000 << 5 = 160,000
- 160,000 + 5,000 = 165,000
- 165,000 >> 12 = 40
请注意,这仅适用于正数。另请注意,如果您真的在整个代码中进行大量乘法运算,那么只有一个 extern mul
或 div
函数可能会导致长 运行 中的整体代码更小,尤其是在编译器不是特别擅长优化的情况下。如果编译器可以在这里发出一条 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;
}