Eliminacja obliczeń zmiennoprzecinkowych AVR

0

Mam trzy liczby typu uint16_t: x, y, z. Wartości którą mogą one przechowywać wahają się oczywiście od 0-65535. Muszę w procku Atmega8 obliczyć coś takiego:

1/32 * (y - x) / (z - x) * 1000000

i teraz tak: wolałbym obliczać to stałoprzecinkowo bo jak powszechnie wiadomo z uwagi na to że atmega8 nie dysponuje jednostką zmiennoprzecinkową obliczenia takie na zmiennoprzecinkowych liczbach trwałyby o wiele dłużej nie mówiąc już o objętości kodu.

Proszę o zasugerowanie rozwiązania. Założenia są takie że wynik będzie z przedziału 0-40000 a znak wyniku ma być przechowywany w odrębnej zmiennej.

Zatem doszedłem już do oczywistej rzeczy że wynik obliczeń można wpierw ocenić czy wychodzi dodatni czy ujemny i do tej dodatkowej zmiennej zapisać kod znaku (szacowanie odbywa się porównując po prostu liczby z licznika i mianownika i na podstawie tego ustalić znak).

Jednak proszę o zasugerowanie rozwiązania na podstawie którego wynik nie byłby obarczony zbyt dużym błędem i tak aby zmusić procek do obliczania stałoprzecinkowego.

Jeśli wynik wyjdzie większy niż 40000 będzie traktowany i tak jako nieważny bo taki jest zakres dopuszczalnego wyniku.

0

No to jest dość znane wiec:

  1. Sam implementujesz operacje stałopozycyjne
  2. Korzystasz z fixed-point w gcc(_fract,_accum), tu uwaga w avr-studio to jest(--enable-fixed-point) ale zazwyczaj w distrach linuksowych ta flaga jest nie włączona więc na linuksie trzeba sobie samemu zbudować gcc.
  3. Użyć bibliotek stał opozycyjnych, np https://sourceforge.net/projects/fixedptc/

takie na zmiennoprzecinkowych liczbach trwałyby o wiele dłużej nie mówiąc już o objętości kodu.

Nie jest to takie oczywiste jak ci się zdaje. Kiedyś porównywałem operacje na _fract i _accum kontra float i co do wielkości nie zawsze wszystko tak fajnie jest.

0

No to jeszcze z podstawówki wiadomo że:

1/32 * (y - x) / (z - x) * 1000000 == (y - x) * 31250 / (z - x)

I teraz jeśli upierasz się na uint16_t, to niestety ale z wyrażenia (y - x) lub (z - x) może wyjść wartość ujemna. Jeśli nie ma sensu (bo ponoć zakres wyników 0-40000), to trzeba ten fakt wykrywać logiką programu.
Jeśli jednak ma (oba wyrażenia w nawiasach mogą być ujemne i w wyniku będzie wartość dodatnia), to znów logiką wykryj ten fakt i "grubiańsko zmień znaki" :-)

1

Dzięki Panowie za sugestie. @Mokrowski - też tak właśnie zrobiłem - znak wykrywam odpowiednimi warunkami logicznymi, problemem są jednak ciągle dla mnie te operacje, i sorki za te 1000000/32 = 31250.
A co Panowie myślicie o tym aby zrobić to tak:

uint32_t temp = (31250UL * (y - x)) / (z - x);

i potem tak:

uint16_t wynik = (uint16_t)(temp);

Będzie to ok?

0

poznajdowałem gdzieś stare wyniki dla fixed-point vs float na atmega 16(dwa lata temu). I z tego co widzę dużo wyższa szybkość działania i mniejszy rozmiar dla operacji dodawania i odejmowania a dzielnie i mnożenie to już zbliża się do float bardziej(pamiętajmy też że użyłem volatile!). A jak wprowadzisz saturację to jest nawet wolniej i więcej miejsca potrzeba. Generalnie dużo zależy co appka robi. BTW. skoro wchodzisz w jakieś przetwarzanie sygnałów to nie lepiej coś jak ARM m4F z fpu? A poniżej prymitywny przykład dla atmegi i _accum

#include <avr/io.h>
#include <avr/delay.h>
#include "hd44780.h"
#include <stdlib.h>
#include <stdfix.h>

int main(void)
{
	TCCR1B |= ((1 << CS10) );
	uint16_t czas =0;
	char temp [4];
	volatile  sat accum x3, x1=1.3k,x2=1.5k;
	volatile float y3, y1=1.3,y2=1.5;
	lcd_init();
	LCD_DISPLAY(LCDDISPLAY);
	LCD_CLEAR;
	LCD_LOCATE(0,0);

	while (1)
	{
		x3=x1+x2;
		czas=TCNT1;
		utoa(czas,temp,10);
		LCD_LOCATE(0,0);
		lcd_puts(temp);
		TCNT1=0;
		y3=y1+y2;
		czas=TCNT1;
		utoa(czas,temp,10);
		LCD_LOCATE(5,0);
		lcd_puts(temp);
		TCNT1=0;

		TCNT1=0;
		x3=x2-x1;
		czas=TCNT1;
		utoa(czas,temp,10);
		LCD_LOCATE(0,1);
		lcd_puts(temp);
		TCNT1=0;
		y3=y2-y1;
		czas=TCNT1;
		utoa(czas,temp,10);
		LCD_LOCATE(5,1);
		lcd_puts(temp);
		TCNT1=0;
/* Druga tura
		x3=x1*x2;
		czas=TCNT1;
		utoa(czas,temp,10);
		LCD_LOCATE(0,0);
		lcd_puts(temp);
		TCNT1=0;
		y3=y1*y2;
		czas=TCNT1;
		utoa(czas,temp,10);
		LCD_LOCATE(5,0);
		lcd_puts(temp);
		TCNT1=0;
		
		TCNT1=0;
		x3=x2/x1;
		czas=TCNT1;
		utoa(czas,temp,10);
		LCD_LOCATE(0,1);
		lcd_puts(temp);
		TCNT1=0;
		y3=y2/y1;
		czas=TCNT1;
		utoa(czas,temp,10);
		LCD_LOCATE(5,1);
		lcd_puts(temp);
		_delay_ms(1000);
		LCD_CLEAR;
		*/
		TCNT1=0;
		
	}
}
		
	}
}
0

Pytanie zasadnicze: jesteś pewien, że float Ci do pamięci nie wejdzie/będzie działać zbyt wolno, tzn. mierzyłeś, czy opierasz się na tym, że napisali, że będzie wolniej (tzn. raczej na pewno będzie, ale być może nie ma to znaczenia dla Ciebie...)?

0
Adamos19 napisał(a):
1/32 * (y - x) / (z - x) * 1000000

Nikt chyba nie zwrócił uwagi że 1/32 * x to jest to samo co x >> 5.

0

Zwrócił, ale gcc wygeneruje dla obu operacji ten sam kod (a przynajmniej dwa lata temu tak było)

0

Panowie, wracając do dyskusji, udało mi się zrobić pewne próby z Atmega8 i wyszło na to że operacje których chciałem dokonać działają zaimplementowane jako:

uint32_t x;
x = 31250UL * (tx - toff);
x /= (tref - toff);
uint16_t wynik = (uint16_t)(x >> 16);

Wyniki są prawidłowe bez żadnych błędów - mi bowiem zależy tylko na części całkowitej ale wynik nie może w części całkowitej zawierać żadnych błędów, ten sposób obliczenia wydaje się być poprawny i działa - testowałem to.
Z drugiej strony zrobiłem także próby na float:

float y;
y = 31250.0 * (tx - toff);
y /= (tref - toff);
uint16_t wynik = (uint16_t)y;

Wynik również okazał się poprawny.
Porównałem zajętość programu i wyszło na to że kod pierwszy zajmuje 1172 bajty, kod drugi zajmuje 4614 bajtów (w obydwu przypadkach optymalizator był ustawiony na optymalizacje pod kątem rozmiaru kodu). Dodatkowo informuję że po wycięciu obliczeń i rzutowania na wynik kod bazowy zawiera 826 bajtów, zatem można chyba przyjąć że :

  • rozmiar kodu obliczeniowego dla operacji stałoprzecinkowych i rzutowania to 1172 - 826 = 346 bajtów
  • rozmiar kodu obliczeniowego dla operacji zmiennoprzecinkowych i rzutowania to 4614 - 826 = 3788 bajtów
    Dodatkowo zmierzyłem ile się to impulsów zegarowych wykonuje i wyszło że:
  • w przypadku pierwszym procesor potrzebował 691 taktów
  • w przypadku drugim 4290 taktów
    Pomiarów dokonałem przy zastosowaniu timera1 który został wyzerowany bezpośrednio przez dokonaniem obliczeń i którego stan został zapamiętany tuż po zrzutowaniu na zmienną wynik zatem chyba też można tym pomiarom ufać.

Wniosek: w takim przypadku jak moje potrzeby czyli w celu określenia wyniku wyrażenia:
wynik = 31250 * (y - x) / (z - x)
najlepiej będzie zastosować sposób zaprezentowany przeze mnie w jednym z poprzednich postów, czyli sposób oparty o tym stałoprzecinkowy uint32_t.

Dzięki wszystkim za cenne sugestie. Mam jednak dodatkowe pytanie do Szanownych kolegów.
@revcorey napisał że korzystając z biblioteki <stdfix.h> można implementować zmienne typu: volatile sat accum x3, chciałem zapytać co tak naprawdę oznacza sat accum. Podejrzewam że chodzi o typ saturacyjny czyli taki że jak osiągnie max to nawet jak potem mu się coś doda to już nie zmieni tej wartości max. Podobnie z dolnym zakresem. Ale co oznacza accum. Nie mam czasu wszystkiego studiować, zlitujcie się. Poza tym jak taki typ może w ogóle być implementowany jeśli przecież taka "saturacja" wymagałaby jakieś "if - ologii" (inaczej : jak to napisać żeby taki typ powstał, dajcie jakiś kod please).

0

Dobra _fract i _accum bez saturacji zachowują się standardowo, czyli np. przepełniłeś 32768 to wskakujesz na -32768.
W przypadku saturacji po prostu zostajesz na maksie lub minimum(zależy z której strony przekraczałeś), typy z saturacją są jednak wolniejsze nieco. i zajmują więcej miejsca.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.