Неожиданное поведение с плавающей точкой в ​​C с AVR atmega8 - PullRequest
4 голосов
/ 06 февраля 2012

Я пытался выяснить, почему я не могу получить разумное значение путем умножения беззнакового целого на значение с плавающей запятой.

Выполнение чего-то вроде 65535 * 0.1 работает, как и ожидалось, но умножая число с плавающей запятой на uintиз памяти создает безумные ценности.У меня есть функция, которая читает АЦП и возвращает uin16_t.С этим значением я печатаю его на 4-значный светодиодный дисплей, который работает нормально.
Умножение одного и того же значения на 1,0 возвращает что-то совершенно другое (оно слишком велико для моего дисплея, поэтому я не знаю, что именноis).

Мой код находится ниже, но область спора находится внизу в main ().Любая помощь будет отличной.Спасибо

main.c:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <stdint.h>
#define BAUD 9600
#include <util/setbaud.h>
#define DISP_BRIGHT_CMD     'z'
#define DISP_RESET          'v'

#define ADC_AVG            3 

volatile uint8_t  hi,lo;
volatile uint16_t result; 

ISR(ADC_vect)
{ 
    lo = ADCL; 
    hi = ADCH; 
    MCUCR &= ~_BV(SE); //Clear enable sleep 
} 


void initSerial(void)
{
    // set baud rate
    UBRR0H = UBRRH_VALUE;
    UBRR0L = UBRRL_VALUE;
    // set frame format
    UCSR0C |= (0x3 << UCSZ00); // 8n1
    // set enable tx/rx
    UCSR0B = _BV(RXEN0) | _BV(TXEN0);
}

void initADC(void)
{
    // AVCC and ADC0
    ADMUX   = _BV(REFS0); 
    // Enable, div128, + 1st setup
    ADCSRA  |= _BV(ADEN)|_BV(ADSC)|_BV(ADPS2)|_BV(ADPS1)|_BV(ADPS0)|_BV(ADIE);
}

uint16_t readADC(void)
{
    uint16_t average=0;
    // Start Conversion
    ADCSRA |= _BV(ADSC);

    for (char i=0;i<ADC_AVG;i++) {
        MCUCR   |= _BV(SE);
        ADCSRA  |= _BV(ADSC);
        __asm volatile("sleep");
        MCUCR   &= ~_BV(SE);
        result  = (hi<<8);
        result  |= lo;
        average += result;
    }
    average /= ADC_AVG;
    return average;    
}

void sendByte(char val)
{
    while (! (UCSR0A & (1<<UDRE0)) ); //wait until tx is complete
    UDR0 = val;
}

/*
 * Convert voltage to temperature based on a negative coefficient for MAX6613
 */
uint16_t analogToTemp(uint16_t val)
{
  uint16_t temp;
  //v     = 5 * (val/1023.0);
  //temp  = (1.8455 - (5.0*(val/1023.0)))/0.01123;
  temp  = (1.8455 - (5.0*(val/1023.0)))*89;
  //temp = val * M_PI;
  //v     = 5 * ( val/1024);
  //temp  = (2 - v) * 89;

  return temp;
}

void initDisplay()
{
    sendByte(DISP_RESET);
    sendByte(DISP_BRIGHT_CMD);
    sendByte(0);
}

void serialSegments(uint16_t val) 
{  
  // 4 digit display
  sendByte(val / 1000);
  sendByte((val / 100) % 10);
  sendByte((val / 10) % 10);
  sendByte(val % 10);  
}

int main(void)
{
    uint16_t calc=0,sense=0;

    DDRB    |= _BV(DDB5);
    PORTB   |= _BV(PORTB5);
    initSerial();
    initADC();
    initDisplay();
    sei();
    MCUCR   |= (1 << SM0); // Setting sleep mode to "ADC Noise Reduction" 
    MCUCR   |= (1 << SE);  // Sleep enable 
    for(;;) {
        //PORTB   ^= _BV(PORTB5);
        if (calc>=9999){ // I can't see the real value. Max val on display is 9999
        //if (sense>=330){
            PORTB |= _BV(PORTB5);
        } else {
            PORTB &= ~_BV(PORTB5);
        }

        sense   = readADC();
        //calc    = sense*1.0;      // refuses to calculate properly
    calc    = analogToTemp(sense);  // a bunch of zeroes
        //calc = 65535*0.1;         // a-ok

        serialSegments(calc);
        _delay_ms(500);
        serialSegments(sense);
        _delay_ms(500);
    }
    return 0;
}

Makefile:

# AVR-GCC Makefile
PROJECT=Temp_Display
SOURCES=main.c
CC=avr-gcc
OBJCOPY=avr-objcopy
MMCU=atmega328p
OSC_HZ=16000000UL
OPTIMISATION=2
PORT=/dev/ttyUSB0

CFLAGS=-mmcu=${MMCU} -std=gnu99 -Wall -O${OPTIMISATION}  -DF_CPU=${OSC_HZ} -lm -lc 

${PROJECT}.hex: ${PROJECT}.out
    ${OBJCOPY} -j .text -O ihex ${PROJECT}.out ${PROJECT}.hex
    avr-size ${PROJECT}.out

$(PROJECT).out: $(SOURCES)
    ${CC} ${CFLAGS} -I./ -o ${PROJECT}.out ${SOURCES}

program: ${PROJECT}.hex
    stty -F ${PORT} hupcl
    avrdude -V -F -c arduino -p m168 -b 57600 -P ${PORT} -U flash:w:${PROJECT}.hex

clean:
    rm -f ${PROJECT}.out
    rm -f ${PROJECT}.hex

РЕДАКТИРОВАТЬ: Хорошо, я несколько упростил код

#include <avr/io.h>
#include <util/delay.h>
#include <stdint.h>
#define BAUD 9600
#include <util/setbaud.h>
#define DISP_BRIGHT_CMD     'z'
#define DISP_RESET          'v'


void initSerial(void)
{
    // set baud rate
    UBRR0H = UBRRH_VALUE;
    UBRR0L = UBRRL_VALUE;
    // set frame format
    UCSR0C |= (0x3 << UCSZ00); // 8n1
    // set enable tx/rx
    UCSR0B = _BV(TXEN0);
}


void sendByte(char val)
{
    while (! (UCSR0A & (1<<UDRE0)) ); //wait until tx is complete
    UDR0 = val;
}


void initDisplay()
{
    sendByte(DISP_RESET);
    sendByte(DISP_BRIGHT_CMD);
    sendByte(0);
}

void serialSegments(uint16_t val) {  
  // 4 digit display
  sendByte(val / 1000);
  sendByte((val / 100) % 10);
  sendByte((val / 10) % 10);
  sendByte(val % 10);  
}

int main(void)
{
    uint16_t i=0,val;

    DDRB    |= _BV(DDB5);
    initSerial();
    initDisplay();
    for(;;) {
        val = (uint16_t)(i++ * 1.5);
        serialSegments(i);
        _delay_ms(500);
        serialSegments(val);
        _delay_ms(500);
        if (val > 9999){
            PORTB |= _BV(PORTB5);
        } else {
            PORTB &= ~_BV(PORTB5);
        }
    }
    return 0;
}

Ответы [ 3 ]

1 голос
/ 07 февраля 2012

Не совсем ваш код, может быть, достаточно близко, а может и нет.

Во-первых, когда я отображаю вывод и сравниваю их со строками:

val = (unsigned int)(i++ * 1.5);
...
val = i+(i>>1); i++;

результат тот же. Разборка также показывает несколько вещей. Сначала от avr-gcc

avr-gcc --version
avr-gcc (GCC) 4.3.4
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

использует числа с плавающей запятой, а не двойные, поэтому комментарии о 1.5F против 1.5 в целом вполне допустимы, но здесь они не актуальны. Во-вторых, он генерирует значения с плавающей запятой с одинарной точностью и выполняет вычисления с плавающей запятой, компилятор там не использует ярлык, он конвертирует в число с плавающей запятой, умножает, а затем конвертирует обратно.

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

Я запустил эту задачу, чтобы посмотреть, была ли производительность с плавающей запятой убийственной, и это так, но время меняется в зависимости от того, как я ее тестировал. Код с плавающей точкой занимал в 157 раз больше времени по сравнению с фиксированной точкой. Если я оставлю в вызове serialSegments (), но при этом вызову фиктивной подпрограммы вместо последовательного порта, это будет в 3 раза медленнее для float. Также я построил два разных способа и использовал libc / m, который использовал другой набор подпрограмм с плавающей запятой, подпрограммы с плавающей запятой, выбранные компилятором C, были в 7 раз медленнее, чем libc / libm.a, находящаяся в / usr / lib64 / avr / lib / каталог. Как только вы добавите ожидание на последовательном порту и другие задержки, вы можете не заметить разницу во времени, поэтому этот эксперимент, демонстрирующий, что поплавок довольно болезненный, вероятно, не является курящим пистолетом, даже если ваш код чувствителен ко времени, мы говорим о нескольких миллисекунды.

В дополнение к приведенному ниже коду я тоже попробовал:

для (я = 0; я <9999; я ++) { vala = (без знака int) (i * 1,5); valb = i + (i >> 1); я ++; если (Вала! = valb) { hexstring16 (я); hexstring16 (Вал); hexstring16 (valb); } }

Без сбоев. Я ограничился 9999, потому что serialSegments () только сокращает десятичные дроби от 0 до 9999. Теперь ваш цикл выходит за пределы этого значения до 65535, но вы увидите, что это вызывает проблемы без числа с плавающей точкой, верно?

avr.c

#define UCSRA (*((volatile unsigned char *)(0xC0)))
#define UDR   (*((volatile unsigned char *)(0xC6)))
#define TCCR0A  (*((volatile unsigned char *)(0x44)))
#define TCCR0B  (*((volatile unsigned char *)(0x45)))
#define TCNT0   (*((volatile unsigned char *)(0x46)))

#define TCCR1A  (*((volatile unsigned char *)(0x80)))
#define TCCR1B  (*((volatile unsigned char *)(0x81)))
#define TCNT1L  (*((volatile unsigned char *)(0x84)))
#define TCNT1H  (*((volatile unsigned char *)(0x85)))

void dummy ( unsigned int );
void uart_putc ( unsigned char c )
{
    while(1) if(UCSRA&0x20) break;
    UDR=c;
}
void hexstring16 ( unsigned int d )
{
    unsigned int rb;
    unsigned int rc;

    rb=16;
    while(1)
    {
        rb-=4;
        rc=(d>>rb)&0xF;
        if(rc>9) rc+=0x37; else rc+=0x30;
        uart_putc(rc);
        if(rb==0) break;
    }
    uart_putc(0x0D);
    uart_putc(0x0A);
}

#ifdef SEGMENTS

void sendByte(char val)
{
    uart_putc(0x30+val);
}


void serialSegments(unsigned int val) {
  // 4 digit display
  dummy(val / 1000);
  dummy((val / 100) % 10);
  dummy((val / 10) % 10);
  dummy(val % 10);
}

//void serialSegments(unsigned int val) {
  //// 4 digit display
  //sendByte(val / 1000);
  //sendByte((val / 100) % 10);
  //sendByte((val / 10) % 10);
  //sendByte(val % 10);
  //uart_putc(0x0D);
  //uart_putc(0x0A);
//}



#else

void serialSegments(unsigned int val)
{
    dummy(val);
}

//void serialSegments(unsigned int val)
//{
    //hexstring(val);
//}


#endif

int main(void)
{
    unsigned int i,val;
    volatile unsigned int xal,xbl,xcl;
    volatile unsigned int xah,xbh,xch;

    hexstring16(0x1234);

    TCCR1A = 0x00;
    TCCR1B = 0x05;

    xal=TCNT1L;
    xah=TCNT1H;
    for(i=0;i<9999;)
    {
        val = (unsigned int)(i++ * 1.5);
        //serialSegments(val);
        //hexstring16(val);
        dummy(val);
    }
    xbl=TCNT1L;
    xbh=TCNT1H;
    for(i=0;i<9999;)
    {
        val = i+(i>>1); i++;
        //serialSegments(val);
        //hexstring16(val);
        dummy(val);
    }
    xcl=TCNT1L;
    xch=TCNT1H;
    xal|=xah<<8;
    xbl|=xbh<<8;
    xcl|=xch<<8;
    hexstring16(xal);
    hexstring16(xbl);
    hexstring16(xcl);
    hexstring16(xbl-xal);
    hexstring16(xcl-xbl);
    return 0;
}

dummy.s

.globl dummy
dummy:
    ret

vectors.s

.globl _start
_start:
    rjmp reset


reset:
    rcall main
1:
    rjmp 1b

.globl dummy
dummy:
    ret

Makefile

all : avrone.hex avrtwo.hex

avrone.hex : avr.c dummy.s
    avr-as dummy.s -o dummy.o
    avr-gcc avr.c dummy.o -o avrone.elf -mmcu=atmega328p -std=gnu99 -Wall -O2 -DSEGMENTS
    avr-objdump -D avrone.elf > avrone.list 
    avr-objcopy avrone.elf -O ihex avrone.hex

a    vrtwo.hex : avr.c vectors.s
    avr-as vectors.s -o vectors.o
    avr-as dummy.s -o dummy.o
    avr-gcc -c avr.c -o avrtwo.o -mmcu=atmega328p -std=gnu99 -Wall -O2 -nostartfiles 
    avr-ld vectors.o avrtwo.o -o avrtwo.elf  libc.a libm.a 
    avr-objdump -D avrtwo.elf > avrtwo.list 
    avr-objcopy avrtwo.elf -O ihex avrtwo.hex



clean :
    rm -f *.hex
    rm -f *.elf

Все это работало на Arduino Pro Mini (Atmega328P).

1 голос
/ 06 февраля 2012

Нефиксированная константа с плавающей точкой имеет тип double, а не float.

Используйте суффикс f, чтобы иметь литерал float, например, 0.1f

Это можетЭто приводит к огромным накладным расходам, поскольку такие микроконтроллеры, как atmega8, не имеют модуля с плавающей запятой, и все операции с плавающей запятой должны быть реализованы в микропрограммном обеспечении реализацией.

На небольших устройствах, таких как atmega8, обычно стараются избегать использования float операции, так как без FPU они очень дороги в циклах ЦП.

Теперь нет причин, по которым реализация не могла бы правильно перевести выражение вроде:

calc = sense * 1.0;

когда calc и sense имеют тип uint16_t.

0 голосов
/ 06 февраля 2012
  1. Умножение 65535 * 0.1 работает, потому что оно оптимизировано и предварительно рассчитано компилятором, поэтому оно преобразуется в 6554.
  2. calc и sense переменныетипа uint16_t , а ваша функция analogToTemp () относится к тому же типу и также возвращает этот тип.Ваш расчет времени выполнения внутри этой функции равен uint16_t .Это должно быть сделано с помощью float, а затем усечено до целочисленной части и приведено к результату uint16 _t функции.
...