Числа с плавающей запятой и влияние на память 8-битных микроконтроллеров - PullRequest
1 голос
/ 31 марта 2020

В настоящее время я работаю над проектом, который включает программирование с нуля на микроконтроллере stm-8 с использованием компилятора SD CC в linux. Память в чипе довольно низкая, поэтому я стараюсь держать все как есть. Я справился с использованием 8-битных и 16-битных переменных, и все прошло хорошо. Но недавно я столкнулся с проблемой, где мне действительно нужна переменная типа float. Поэтому я написал функцию, которая принимает 16-битное значение, преобразует в число с плавающей запятой, выполняет математику, которая мне нужна, и возвращает 8-битное число. Это приводит к тому, что мой окончательный скомпилированный код на MCU до go с 1198 байт до 3462 байт. Теперь я понимаю, что использование чисел с плавающей запятой требует большого объема памяти и что для обработки использования числа с плавающей запятой может потребоваться множество функций, но кажется, что безумно увеличивать размер программы на столько. Я хотел бы помочь понять, почему это происходит и что именно произошло.

Спецификации: MCU stm8151f2 Компилятор: SD CC с параметром --opt_code_size

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

Ответы [ 3 ]

2 голосов
/ 01 апреля 2020

Чтобы определить, почему код настолько велик в вашей конкретной цепочке инструментов, вам нужно взглянуть на сгенерированный код сборки и посмотреть, что вызывает поддержка FP, а затем посмотреть на файл карты, чтобы определить размер каждого из эти функции.

В качестве примера на Godbolt для AVR с использованием G CC 5.4.0 с -Os (Godbolt не поддерживает STM8 или SD CC, так что это для сравнения как 8-битная архитектура) ваш код генерирует 6364 байта по сравнению с 4081 байтом для пустой функции. Таким образом, дополнительный код, необходимый для тела кода, составляет 2283 байта. Теперь, учитывая тот факт, что вы используете другой компилятор и архитектуру, они ничем не отличаются от ваших результатов. Посмотрите в сгенерированном коде (ниже) подпрограммы rcall s, такие как __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

Вам необходимо выполнить тот же анализ кода, сгенерированного цепочкой инструментов, чтобы ответить на ваш вопрос. Без сомнения, SD CC способен генерировать список сборок и файл карты, который позволит вам точно определить, какой код и поддержка FP генерируются и связываются.

В конечном счете, хотя вы используете FP в этом случае совершенно не нужно:

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

При Годболт 2283 байта по сравнению с пустой функцией. Все еще несколько большой, но проблема там, скорее всего, заключается в том, что в AVR отсутствует инструкция DIV, поэтому она вызывает __divmodhi4. STM8 имеет DIV для 16-битного делителя и 8-битного делителя, поэтому он, вероятно, будет значительно меньше (и быстрее) для вашей цели.

0 голосов
/ 01 апреля 2020

(обновление) После написания этого я заметил, что @Clifford упомянул, что ваш микроконтроллер изначально поддерживает эту инструкцию DIV, и в этом случае выполнение этого является избыточным. В любом случае, я оставлю это как концепцию, которая может применяться в случаях, когда DIV реализован как внешний вызов, или в случаях, когда DIV занимает слишком много циклов и цель состоит в том, чтобы ускорить вычисления.

В любом случае, сдвиг и сложение, скорее всего, будут быстрее деления, если вам когда-нибудь понадобится сжать несколько дополнительных циклов. Поэтому, если вы начнете с того факта, что 124 почти равен 4096/33 (коэффициент ошибок равен 0,00098, т. Е. 0,098%, то есть меньше 1 на 1000), вы можете выполнить деление с одним умножением с 33 и сдвиг на 12 бит (деление на 4096). Кроме того, 33 равно 32+1, то есть умножение на 33 равно сдвигу влево на 5 и добавлению ввода снова.

Пример: вы хотите разделить 5000 на 124 и 5000/124 составляет ок. 40.323. То, что мы будем делать, это:

  1. 5,000 << 5 = 160,000 </li>
  2. 160,000 + 5000 = 165,000
  3. 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;
}

G CC AVR с оптимизацией по размеру выдает это , то есть все вызовы функций 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;
}
0 голосов
/ 01 апреля 2020

ОК, версия с фиксированной точкой, которая действительно работает:

// 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-битное целое) вместо этого, если округлить и переполнить можно осторожно.

или go получить лучшую полнофункциональную библиотеку Fixed Point из Интернета :)

...