Forum: Compiler & IDEs Wurzelfunktion für 16bit Fixed Point Werte


von StefanK (Gast)


Lesenswert?

In Beitrag "Wurzefunktion für uint32_t in C" werden verschiedene 
Algorithmen zum Wurzelziehen aus 32bit Werten angeboten.
Insbesondere die Assembler Funktion von Johann L. ist sehr effektiv.

Möchte man die Wurzel nun aus einem Fixed Point Wert U(a,b) mit b 
Nachkommastellen ziehen kann man diese Funktion verwenden:

Pseudocode für b=8
1
uint16_t fixQ8val = X;         // mit X = float * 2^b
2
uint16_t res;
3
res = sqrt32(val);
4
res = mult_u16_fixQ8(res, Y);  // mit Y = sqrt(2^b)*2^b

Im vorliegenden Sonderfall von b=8 ist eine einfache (nicht fixed point) 
Multiplikation möglich. Da sqrt(2^8)=16 kann man ohne 
Genauigkeitsverlust direkt Multiplizieren.
Die Abarbeitung ist ebenfalls recht flott (sqrt32~400cycle; mul_u16~50).
Der Nachteil hierbei ist die mangelnde Genauigkeit, da die sqrt32 
Funktion den Wert gerundet zurückgibt und u.U. zusätzliche 
Rundungsfehler beim umwandeln der Konstante sqrt(2^b) in fixed point 
Repräsentation entstehen.

Eine genauere Funktion wurde von Turkowski (Turkowski, Ken, Fixed Point 
Square Root, p. 22-24, code: p. 23 - 
http://tog.acm.org/resources/GraphicsGems/) für 32bit fixed point Werte 
veröffentlicht.
Für 16bit fixed Point Werte sieht die Funktion so aus:
1
int16_t sqrt16_fixQ8(int16_t a)
2
{
3
    uint16_t testDiv = 0;
4
    uint16_t root = 0;
5
    uint16_t remHi = 0;
6
    uint16_t remLo = (int16_t)a;
7
    uint8_t count = 11;  //(15 + b)/2
8
    do
9
    {
10
        remHi =(remHi << 2) | (remLo >> 14);
11
        remLo <<= 2;
12
        root <<= 1;
13
        testDiv = (root << 1) + 1;
14
        if (remHi >= testDiv)
15
        {
16
            remHi -= testDiv;
17
            root += 1;
18
        }
19
    }
20
    while (count-- != 0);
21
    return (int16_t)root;
22
}

Für andere Werte von b muss lediglich die Variable count entsprechend 
initialisiert werden.
Das Ergebnis ist deutlich genauer als das mit vorheriger vorgehensweise 
allerdings braucht der von gcc erstellte Code bei mir ~1700cycles!

Ich habe die Funktion deshalb in Assembler umgesetzt
1
.global sqrt16_fixQ8
2
.func sqrt16_fixQ8
3
;--------------------------------------------------------------------
4
;  R25:R24 = SQRT (R25:R24) for fixed point values of precision U(a, b) 
5
;  The precision of U(a, b) determines how many times the loop is gone 
6
;  through. With b being the number of fractional bits:
7
;  N = (16+b)/2
8
;  in this case N = (16+8)/2 = 12
9
;--------------------------------------------------------------------
10
11
#define TESTDIV0  r18
12
#define TESTDIV1  r19
13
14
#define ROOT0    r20
15
#define ROOT1    r21
16
17
#define REMHI0    r22
18
#define REMHI1    r23
19
20
#define REMLO0    r24
21
#define REMLO1    r25
22
23
#define TEMP0    r26
24
25
#define CNT      r27
26
27
            
28
29
30
sqrt16_fixQ8:           clr REMHI0                       ; Initialize: REMLO=argument(R25:R24); REMHI=0; ROOT=0; 
31
                        clr REMHI1
32
                        clr ROOT0
33
                        clr ROOT1
34
                        ldi CNT,12                       ; set number of loops N            
35
sqrt16_fixQ8_loop:      lsl REMHI0                       ; remhi = remhi<<2
36
                        rol REMHI1
37
                        lsl REMHI0
38
                        rol REMHI1
39
40
                        mov TEMP0, REMLO1                ; remlo>>14 -> trunk REMLO0
41
                        lsr TEMP0                        ; still 6 shifts left
42
                        lsr TEMP0
43
                        lsr TEMP0
44
                        lsr TEMP0
45
                        lsr TEMP0
46
                        lsr TEMP0
47
48
                        or REMHI0, TEMP0                 ; remhi = remhi<<2(which is REMHI1:REMHI0) | remlo>>14(which is 0x00:TEMP0)
49
                                                         ; REMHI1 stays REMHI1, since or with 0x00
50
51
                        lsl REMLO0                       ; remlo <<= 2
52
                        rol REMLO1
53
                        lsl REMLO0
54
                        rol REMLO1
55
56
                        lsl ROOT0                        ; root <<= 1
57
                        rol ROOT1
58
59
                        movw TESTDIV0, ROOT0             ; testdiv = (root<<1) + 1
60
                        lsl TESTDIV0
61
                        rol TESTDIV1
62
                        subi TESTDIV0,-1                 ; addi 1
63
                        sbci TESTDIV1,-1                 ; adci 0
64
65
                                                         ; if(remhi >= testdiv)  
66
                        cp REMHI1, TESTDIV1              ; carry set if REMHI1 < TESTDIV1, Z set if REMHI1 == TESTDIV1
67
                        breq sqrt16_fixQ8_test_low       ; when Z is set test low byte
68
                        brsh sqrt16_fixQ8_same_higher    ; when carry is not set REMHI1 >= TESTDIV, 
69
                                                         ; the case == is covered above so reaching this means REMHI1 > TESTDIV
70
                        rjmp sqrt16_fixQ8_lower          ; when carry was set REMHI1 is smaller
71
72
sqrt16_fixQ8_test_low:  cp REMHI0, TESTDIV0              ; carry is set if REMHI0 < TESTDIV0
73
                        brlo sqrt16_fixQ8_lower          ; leave if carry was set
74
                        rjmp sqrt16_fixQ8_same_higher    ; when carry was not set REMHI0 >= TESTDIV0
75
76
sqrt16_fixQ8_same_higher:  sub REMHI0, TESTDIV0          ; remhi -= testdiv
77
                           sbc REMHI1, TESTDIV1
78
79
                           subi ROOT0,-1                 ; root += 1
80
                           sbci ROOT1,-1
81
              
82
83
sqrt16_fixQ8_lower:        dec CNT
84
                           brne sqrt16_fixQ8_loop
85
86
                           movw r24, ROOT0               ; store result in R25:R24
87
                           ret
88
89
#undef TESTDIV0  
90
#undef TESTDIV1  
91
92
#undef ROOT0  
93
#undef ROOT1  
94
95
#undef REMLO0  
96
#undef REMLO1  
97
98
#undef REMHI0  
99
#undef REMHI1  
100
101
#undef TEMP0  
102
#undef TEMP1  
103
104
#undef CNT    
105
106
.endfunc

Sie läuft mit ~500cycle fast so schnell wie die zuerst genannte 
Möglichkeit bei höherer Genauigkeit.
Ich hoffe die Funktion ist für jemanden nützlich. Über 
Verbesserungsvorschläge im Assemblercode oder Anmerkungen zu versteckten 
Fehlern (bin Assembler Neuling) würde ich mich freuen.

Viele Grüße

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

StefanK schrieb:
> In Beitrag "Wurzefunktion für uint32_t in C" werden verschiedene
> Algorithmen zum Wurzelziehen aus 32bit Werten angeboten.
> Insbesondere die Assembler Funktion von Johann L. ist sehr effektiv.

Die Implementierung ist von Ruud v. Gessel, ich hab sie nur 
avr-gcc-tauglich gemacht.

http://www.mikrocontroller.net/articles/AVR_Arithmetik#avr-gcc_Implementierung_.2832_Bit.29

> Möchte man die Wurzel nun aus einem Fixed Point Wert U(a,b) mit b
> Nachkommastellen ziehen kann man diese Funktion verwenden:
>
> Pseudocode für b=8
>
1
> uint16_t fixQ8val = X;         // mit X = float * 2^b
2
> uint16_t res;
3
> res = sqrt32(val);
4
> res = mult_u16_fixQ8(res, Y);  // mit Y = sqrt(2^b)*2^b
5
>

Spricht bei dir was gegen 4^b? Dass ist einfacher und zudem ohne Fehler 
zu wurzeln.

> Im vorliegenden Sonderfall von b=8 ist eine einfache (nicht fixed point)
> Multiplikation möglich. Da sqrt(2^8)=16 kann man ohne
> Genauigkeitsverlust direkt Multiplizieren.
> Die Abarbeitung ist ebenfalls recht flott (sqrt32~400cycle; mul_u16~50).

mul_u16~50??? Doch hoffentlich auf einer Maschine ohne MUL*.

> Der Nachteil hierbei ist die mangelnde Genauigkeit, da die sqrt32
> Funktion den Wert gerundet zurückgibt und u.U. zusätzliche
> Rundungsfehler beim umwandeln der Konstante sqrt(2^b) in fixed point
> Repräsentation entstehen.
>
> Eine genauere Funktion wurde von Turkowski (Turkowski, Ken, Fixed Point
> Square Root, p. 22-24, code: p. 23 -
> http://tog.acm.org/resources/GraphicsGems/) für 32bit fixed point Werte
> veröffentlicht.
> Für 16bit fixed Point Werte sieht die Funktion so aus:
>
> [c]
> int16_t sqrt16_fixQ8(int16_t a)
> {
>     uint16_t testDiv = 0;
>     uint16_t root = 0;
>     uint16_t remHi = 0;
>     uint16_t remLo = (int16_t)a;
>     uint8_t count = 11;  //(15 + b)/2
>     do
>     {
>         remHi =(remHi << 2) | (remLo >> 14);
>         remLo <<= 2;

Das geht wesentlich schmerzfreier mit
1
LSL  remLo.lo
2
ROL  remLo.hi
3
ROL  remHi.lo
4
ROL  remHi.hi
5
6
LSL  remLo.lo
7
ROL  remLo.hi
8
ROL  remHi.lo
9
ROL  remHi.hi

>         root <<= 1;
>         testDiv = (root << 1) + 1;

Das ist schlicht (da Bit 0 vor der Addition = 0 ist):
1
SEC
2
ROL  root.lo
3
ROL  root.hi
4
;; oder
5
LSL  root.lo
6
ROL  root.hi
7
INC  root.lo

Ich hab nicht auf's Interface geschaut, ist das avr-gcc ABI?
Wär sinnvoll...

von StefanK (Gast)


Lesenswert?

Johann L. schrieb:
> Spricht bei dir was gegen 4^b? Dass ist einfacher und zudem ohne Fehler
> zu wurzeln.

Dabei kann ich dir nicht ganz folgen, was meinst du damit?

Johann L. schrieb:
>mul_u16~50??? Doch hoffentlich auf einer Maschine ohne MUL*.

mh ne ist mit mul, aber ich ich seh grad das ich in meinem test die 
signed multiplikation erwischt habe.

Johann L. schrieb:
> LSL  remLo.lo
> ROL  remLo.hi
> ROL  remHi.lo
> ROL  remHi.hi
>
> LSL  remLo.lo
> ROL  remLo.hi
> ROL  remHi.lo
> ROL  remHi.hi

Da kann ich dir wieder nicht folgen.. kann ich das so in den C code 
reinschreiben?

Johann L. schrieb:
> Ich hab nicht auf's Interface geschaut, ist das avr-gcc ABI?
> Wär sinnvoll...

Als Neuling kann ich damit auch net so viel anfangen... ich hab 
AVRStudio 5 installiert.

Viele Grüße

von StefanK (Gast)


Lesenswert?

StefanK schrieb:
> Johann L. schrieb:
>> LSL  remLo.lo
>> ROL  remLo.hi
>> ROL  remHi.lo
>> ROL  remHi.hi
>>
>> LSL  remLo.lo
>> ROL  remLo.hi
>> ROL  remHi.lo
>> ROL  remHi.hi

Ach du meinst im Assember code... jetzt seh ichs...
das ist ja tatsächlich deutlich angenehmer. Danke!

Wenn ich sowas auch nur mal sehen würde... bei mir werden das immer ganz 
wirre konstrukte

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

StefanK schrieb:
> Johann L. schrieb:
>> Spricht bei dir was gegen 4^b? Dass ist einfacher und zudem ohne Fehler
>> zu wurzeln.
>
> Dabei kann ich dir nicht ganz folgen, was meinst du damit?

Oben schreibst du was von Rundingsfehlern bei der Darstellung von
wenn b gerade ist, was bei dir offenbar so ist, dann schreibt sich b als 
b = 2·b' und also
Ich sehe nicht, wo's da einen Rundungsfehler hat, denn die Darstellung 
ist im 2er-System. Auch negative b' sind ohne Rundungsfehler 
darzustellen. Bei der Division (ein Links-Shift, da b' < 0) gibt's auch 
keine Rundungsfehler.

Freilich verlierst du damit wie korrekt angemerkt Genauigkeit, bzw. sie 
ist durch den 32-Bit-Wurzler garnicht vorhanden.

> Johann L. schrieb:
>>mul_u16~50??? Doch hoffentlich auf einer Maschine ohne MUL*.
>
> mh ne ist mit mul, aber ich ich seh grad das ich in meinem test die
> signed multiplikation erwischt habe.

50 Ticks für ne 16-bit signed-Multiplikation ist immer nocht stolz. Du 
hast Optimierung aktiviert?

> Johann L. schrieb:
>> Ich hab nicht auf's Interface geschaut, ist das avr-gcc ABI?
>> Wär sinnvoll...
>
> Als Neuling kann ich damit auch net so viel anfangen... ich hab
> AVRStudio 5 installiert.

Vielleicht nur eine Fehlinterpretation von mir: Du verwendest ein Mix 
von Assembler und C? Oder ist es ein reines Assembler-Projekt.

Im zweiten Falle ist das ABI egal.
Im ersten Falle will man idR die Assembler-Funktion von C aus aufrufen, 
weil man sich nicht den Wolf machen will das komplette Projekt in asm zu 
klöppeln.  Dann muss allerdings die Aufruf-Konvention von avr-gcc 
eingehalten werden; siehe zB

http://www.rn-wissen.de/index.php/Avr-gcc/Interna#Registerverwendung

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

StefanK schrieb:

>     uint16_t testDiv = 0;
>     uint16_t root = 0;
>     uint16_t remHi = 0;
>         root <<= 1;
>         if (remHi >= testDiv)
>         {
>             remHi -= testDiv;
>             root += 1;
>         }

Auch diese Stelle ist sehr kompliziert umgesetzt. Das geht:
1
LSL  root.lo
2
ROL  root.hi
3
4
CP   remHi.lo, testDiv.lo
5
CPC  remHi.hi, testDiv.hi
6
7
BRLO 0f
8
9
SUB  remHi.lo, testDiv.lo
10
SBC  remHi.hi, testDiv.hi
11
INC  root.Lo
12
13
0:

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Johann L. schrieb:

> LSL  root.lo
> ROL  root.hi
>
> CP   remHi.lo, testDiv.lo
> CPC  remHi.hi, testDiv.hi
>
> BRLO 0f
>
> SUB  remHi.lo, testDiv.lo
> SBC  remHi.hi, testDiv.hi
> INC  root.lo
>
> 0:

Eine Instruktion kann man da noch sparen wenn ich mich net irre:
1
LSL  root.lo
2
ROL  root.hi
3
4
SUB  remHi.lo, testDiv.lo
5
SBC  remHi.hi, testDiv.hi
6
7
BRSH 0f
8
9
MOVW remHi, testDiv
10
11
0:
12
SBCI  root.lo, -1

Allerdings muss root.lo dann in einem Register >= R16 sein und man 
brauch ein Device mit MOVW. In dem Falle geht dann auch die 
Initialisierung mit 0 kürzer: Anstatt CLR, CLR, CLR, CLR anfangs geht 
CLR, CLR, MOVW.

von StefanK (Gast)


Lesenswert?

Johann L. schrieb:
> wenn b gerade ist, was bei dir offenbar so ist, dann schreibt sich b als
> b = 2·b'

Ach, alles klar, das geht - hab ich verstanden. Bleibt das Problem, dass 
sqrt32 immernoch gerundete Werte zurückgibt. Das kann man mit dem o.g. 
Algorithmus umgehen.

Dank deiner Verbesserungen ist er sogar flotter als sqrt32 alleine.

Johann L. schrieb:
> Dann muss allerdings die Aufruf-Konvention von avr-gcc
> eingehalten werden; siehe zB

Ja das beachte ich.


Johann L. schrieb:
> Eine Instruktion kann man da noch sparen wenn ich mich net irre:
> LSL  root.lo
> ROL  root.hi
>
> SUB  remHi.lo, testDiv.lo
> SBC  remHi.hi, testDiv.hi
>
> BRSH 0f
>
> MOVW remHi, testDiv
>
> 0:
> SBCI  root.lo, -1
geht hier nicht, für den fall das remhi<testdiv, mein remhi verloren? 
movw setzt ja einfach testdiv ein, nicht das alte remhi?

Johann L. schrieb:
> 50 Ticks für ne 16-bit signed-Multiplikation ist immer nocht stolz. Du
> hast Optimierung aktiviert?

Nein Optimierung ist nicht aktiv. Die Multiplikation ist allerdings auch 
eine Assembler Routine (32bit = 16bit*16bit). Die Zeit zum Aufrufen 
(Registerzuweisungsgeplänkel von gcc) ist allerdings in den 50 cycles 
dabei.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.