Forum: Mikrocontroller und Digitale Elektronik Assembler 16bit signed Multiplikation


von StefanK (Gast)


Lesenswert?

Hallo,

ich habe kurz eine Frage... ich habe die im Listing angehängte Assembler 
Funktion geschrieben um eine A(3,12) (signed, 12bit Nachkomma) 
Multiplikation durchzuführen. Die Assembler Funktion wird extern in mein 
C Projekt eingebunden.
Die Multiplikation findet auf einem 8bit AVR statt.

Ich brauche damit incl. Funktionsaufruf 59 cycle während die C 
implementierung gut 160 cycles benötigt.

Die Routine funktioniert bei den bisher getesteten Argumenten. Bezüglich 
der temprären Register habe ich nach 
http://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_reg_usage (What 
registers are used by the C compiler?) sogenannte Call-used register 
verwendet.

Kann bei meiner Implementierung dann noch etwas schief gehen (bezüglich 
Registersicherung oder andere nicht bedachte Umstände)?

Falls nicht: habt ihr Verbesserungsvorschläge?

C:
1
fixRes = (int16_t)(((int32_t)a * b)>>12);

Assembler:
1
.global mul_s16_fixQ12
2
.func mul_s16_fixQ12
3
mul_s16_fixQ12:
4
  ;int16 a: r25|r24
5
  ;int16 b: r23|r22
6
  movw r20, r24          ; mulsu needs r16-r23
7
  clr r2                 ; clear r2 cause it is used to add carries
8
  muls r23, r21          ; (signed)aMSB * (signed)bMSB
9
  movw r24, r0           ; store result R25:R24
10
  mul r22, r20           ; aLSB * bLSB
11
  movw r26, r0           ; store result R26:R27
12
  mulsu r23, r20         ; (signed)aMSB * bLSB
13
  sbc r25, r2
14
  add r27, r0
15
  adc r24, r1
16
  adc r25, r2
17
  mulsu r21, r22         ; (signed)bMSB * aLSB
18
  sbc r25, r2
19
  add r27, r0
20
  adc r24, r1
21
  adc r25, r2
22
  ; result is now in R25:R24:R27:R26
23
  ;                  MSB         LSB
24
  ; fixedPoint with precision 12 -> drop R26 (8bit)
25
  ; still 4 shifts left
26
  lsr r25
27
  ror r24
28
  ror r27
29
30
  lsr r25
31
  ror r24
32
  ror r27
33
  
34
  lsr r25
35
  ror r24
36
  ror r27
37
  
38
  lsr r25
39
  ror r24
40
  ror r27
41
  
42
  adc r27, r2           ; use last bit shifted out to round 
43
  
44
  mov r25, r24          ; store result in 16bit return registers
45
  mov r24, r27
46
  ret
47
.endfunc

von Karl H. (kbuchegg)


Lesenswert?

Lies nochmal nach, was du mit den Register r0, r1 und r2 machen musst.
r0 ist egal, aber r1 und r2 darfst du nicht einfach zerstören.

von Anja (Gast)


Lesenswert?

StefanK schrieb:
> adc r27, r2           ; use last bit shifted out to round

was machst Du wenn bei der Addition wieder ein Übertrag auftritt?

Ausserdem mußt Du Sicherstellen daß das Produkt immer kleiner als 7.999 
ist ansonsten gibt es unter Umständen ein falsches Vorzeichen.

(die C-Implementierung ist aber an der Stelle auch nicht besser).

Gruß Anja

von StefanK (Gast)


Lesenswert?

Karl Heinz Buchegger schrieb:
> Lies nochmal nach, was du mit den Register r0, r1 und r2 machen musst.
> r0 ist egal, aber r1 und r2 darfst du nicht einfach zerstören.

Vielen Dank, hab ich doch glatt überlesen. R2 änder ich nicht, aber für 
R1 muss ich noch das clr einfügen.

Anja schrieb:
> was machst Du wenn bei der Addition wieder ein Übertrag auftritt?
>
> Ausserdem mußt Du Sicherstellen daß das Produkt immer kleiner als 7.999
> ist ansonsten gibt es unter Umständen ein falsches Vorzeichen.

hab jetzt ein weiteres adc hinzugefügt für den eventuellen Übertrag. 
Vielen Dank für den Hinweis.

Bei dem sicherstellen des Produktes < 7.999 bin ich leider überfragt. 
Ich hab durch Simulationen geprüft, dass mein Wertebereich im Regelfall 
nicht größer ist.
Wie könnte ich im Assembler Code möglichst geschickt einen Überlauf 
erkennen und darauf reagieren?
Ich bin ja eigentlich eher in C zuhause, da würd ich den sowas in der 
Richtung machen
1
bool mult( int16_t* pRes, int16_t a, int16_t b )
2
{ 
3
    (... vor shift)
4
    bool temp;
5
    if(R25 == 0x00 || R25 == 0xFF )
6
        temp = true;
7
    else
8
        temp = false;
9
10
    (... shift und zuweisung...)
11
    return temp;
12
}

leider bin ich in Assembler noch nicht zu sowas fähig. Die Überprüfung 
selbst ist für mich mit entsprechendem cycle-aufwand machbar (wenn ich 
kein Denkfehler gemacht habe). Probleme bereiten mir so Dinge wir 
Übergabe des Werts als Zeiger und die Bool Geschichte.


Mit Hilfe der beiden anderen Anmerkungen korrigierter ASM
1
  ;(... vorheriges wie gehabt...)
2
  adc r27, r2           ; use last bit shifted out to round
3
  adc r24, r2           ; add carry
4
  
5
  mov r25, r24          ; store result in 16bit return registers
6
  mov r24, r27
7
8
  clr r1
9
10
  ret
11
.endfunc

von Karl H. (kbuchegg)


Lesenswert?

StefanK schrieb:
> Karl Heinz Buchegger schrieb:
>> Lies nochmal nach, was du mit den Register r0, r1 und r2 machen musst.
>> r0 ist egal, aber r1 und r2 darfst du nicht einfach zerstören.
>
> Vielen Dank, hab ich doch glatt überlesen. R2 änder ich nicht,

echt?

>   adc r27, r2           ; use last bit shifted out to round
>   adc r24, r2           ; add carry


Hmmm

von spess53 (Gast)


Lesenswert?

Hi

>>   adc r27, r2           ; use last bit shifted out to round
>>   adc r24, r2           ; add carry

>Hmmm

Und was ändern die an r2?

MfG Spess

von Karl H. (kbuchegg)


Lesenswert?

spess53 schrieb:
> Hi
>
>>>   adc r27, r2           ; use last bit shifted out to round
>>>   adc r24, r2           ; add carry
>
>>Hmmm
>
> Und was ändern die an r2?

Hast recht. War auch nur ein Schnellschuss, damit er sich Gedanken macht 
wie er da ein r2 verwenden kann, wo er doch r2 gar nicht verändert :-)

Aber das hier

  clr r2                 ; clear r2 cause it is used to add carries

bedingt einen push/pop

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

StefanK schrieb:
> R2 änder ich nicht, ...

Lügt, ohne rot zu werden:

> clr r2

Ist der unigned-shift am Ende ok? Soll doch eine signed-Berechnung sein.

Übrigens: avr-gcc kann inzwischen auch 16*16=32 Multiplikationen 
effizient ausführen, ohne die Operanden auf 32 Bit aufzublasen und dann 
die ganze Multiplikation auf 32-Bit Ebene zu machen :-)

FYI, hier der Code:
http://gcc.gnu.org/viewcvs/trunk/gcc/config/avr/libgcc.S?r1=175620&r2=175619&pathrev=175620

von Karl H. (kbuchegg)


Lesenswert?

Johann L. schrieb:

> Übrigens: avr-gcc kann inzwischen auch 16*16=32 Multiplikationen
> effizient ausführen, ohne die Operanden auf 32 Bit aufzublasen und dann
> die ganze Multiplikation auf 32-Bit Ebene zu machen :-)

Wie schreibt sich das dann auf C Ebene?

uint16_t a, b;
uint32_t c;


   c = a * b;

ist ja eigentlich dann nicht richtig. Die 'Überlaufbits' in der 
Multiplkation müssten ignoriert werden.


   c = (uint32_t)a * b;

kommt da der Optimizer drauf, dass die 32*32 Bit Multiplikation nur 
deswegen entsteht, weil beide Operatoren von 16 auf 32 Bit aufgeblasen 
wurden?

von StefanK (Gast)


Lesenswert?

Vielen Dank an alle,

das push/pop für R2 hat gefehlt. Ich nutze jetzt stattdessen R18, das ja 
verändert werden darf.

Johann L. schrieb:
> Ist der unigned-shift am Ende ok? Soll doch eine signed-Berechnung sein.

Der unsigned shift sollte ok sein, da er ja ja bei signed sowas wie 
0xFF3C hat, das heißt beim shift wird immer die nächsthöhere 1 ins 
relevante Register geschoben (solange der von Anja erwähnte Überlauf 
nicht eintritt).
Der signed shift hält ja die 1 an dem MSbit des Registers (also z.b. das 
MSB des Multiplikationsergebnisses), da bringt es mir ja nix, weil ichs 
das ja verwerfe (wenn ich nicht wieder was überseh...).


Johann L. schrieb:
> Übrigens: avr-gcc kann inzwischen auch 16*16=32 Multiplikationen
> effizient ausführen, ohne die Operanden auf 32 Bit aufzublasen und dann
> die ganze Multiplikation auf 32-Bit Ebene zu machen :-)
>
> FYI, hier der Code:
> http://gcc.gnu.org/viewcvs/trunk/gcc/config/avr/li...

Ich kenn mich leider nicht besonders damit aus.. wie ruf ich sowas denn 
dann in C auf?

Viele Grüße

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Karl Heinz Buchegger schrieb:
> Johann L. schrieb:
>
>> Übrigens: avr-gcc kann inzwischen auch 16*16=32 Multiplikationen
>> effizient ausführen, ohne die Operanden auf 32 Bit aufzublasen und dann
>> die ganze Multiplikation auf 32-Bit Ebene zu machen :-)
>
> Wie schreibt sich das dann auf C Ebene?
>
> uint16_t a, b;
> uint32_t c;
>

So:

>    c = (uint32_t)a * b;
>
> kommt da der Optimizer drauf, dass die 32*32 Bit Multiplikation nur
> deswegen entsteht, weil beide Operatoren von 16 auf 32 Bit aufgeblasen
> wurden?

Ja. Allerdings gibt's das erst in avr-gcc 4.7 und wenn's MUL-Befehle 
hat.
Ohne MUL gewinnt man glaub nicht wirklich was.


StefanK schrieb:
> Johann L. schrieb:
>> Übrigens: avr-gcc kann inzwischen auch 16*16=32 Multiplikationen
>> effizient ausführen, ohne die Operanden auf 32 Bit aufzublasen und dann
>> die ganze Multiplikation auf 32-Bit Ebene zu machen :-)
>>
>> FYI, hier der Code:
>> http://gcc.gnu.org/viewcvs/trunk/gcc/config/avr/li...

Hier nochmal dir Link zur eigentlichen S-Datei, ist besser zu lesen alsn 
Patch.

> Ich kenn mich leider nicht besonders damit aus.. wie ruf ich sowas denn
> dann in C auf?

Puh, da fragst du was... In ANSI-C überhaupt nicht, weil die Funktion en 
(__mulhisi3 bzw. __umulhisi3) nicht dem ABI entsprechen: Sie machten

r25:r22 = r19:r18 * r21:r20

Am einfachsten bekommt man das ABI-konform, indem man am Funktionsanfang
1
movw  A0, r22
2
movw  B0, r24
einfügt und dann C-Prototypen definiert:
1
#include <stdint.h>
2
3
extern uint32_t __umulhisi3 (uint16_t, uint16_t);
4
extern int32_t __mulhisi3 (int16_t, int16_t);
und die Funktionen wie normale C-Funktionen aufruft.

Ein Nachteil ist, daß immer die zwei unnötigen MOVW am Anfang stehen. 
Daher sind die Funktionen auch nicht-ABI-konfirm aufgesetzt.

Um den Code "direkt" verwenden zu können, muss man GNU-C bemühen um das 
Interface ans ABI anzupassen:
1
static inline uint32_t 
2
umulhisi3 (uint16_t a, uint16_t b)
3
{
4
    register uint16_t ra asm ("18") = a;
5
    register uint16_t rb asm ("20") = b;
6
    register uint32_t rc asm ("24");
7
8
    asm ("%~call __umulhisi3"
9
         : "=r" (rc)
10
         : "r" (ra), "r" (rb));
11
12
    return rc;
13
}

Ditto für die signed-Version.

Das erlaubt dem Compiler dann, die Register-Allocation an den Aufruf 
anzupassen, und MOVs können ggf. entfallen, wenn der Alligator die 
Register geschickt wählt.

von StefanK (Gast)


Lesenswert?

Johann L. schrieb:
> Hier nochmal dir Link zur eigentlichen S-Datei, ist besser zu lesen alsn
> Patch.

Ich find den Link leider nicht.

Muss ich den Assembler Code dann aus dem Link dann in eine eigene .s 
kopieren oder existiert der schon in einer Bibliothek die ich einbinden 
muss?

Viele Grüße

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

StefanK schrieb:
> Johann L. schrieb:
>> Hier nochmal dir Link zur eigentlichen S-Datei, ist besser zu lesen alsn
>> Patch.

> Ich find den Link leider nicht.

Oh, die Zeile ging wohl verschütt. Hier nochmal:

http://gcc.gnu.org/viewcvs/trunk/gcc/config/avr/libgcc.S?revision=175620&content-type=text%2Fplain&view=co&pathrev=175620

> Muss ich den Assembler Code dann aus dem Link dann in eine eigene .s
> kopieren oder existiert der schon in einer Bibliothek die ich einbinden
> muss?

Es ist erst ab avr-gcc 4.7 in der Bibliother, und dann brauchst du auch 
keine Verrenkungen zu machen sondern einfach nur C-Code wie Karl Heinz 
ihn oben nachfragte.

Ich bin allerdings davon ausgegeangen, daß du diese Version noch nicht 
einsetzt, da sie erst in der Entwicklung ist.

Den relevanten Teil hab ich mal rauskopiert, den speicherst du nicht 
als *.s sondern als *.sx und verwendest es ansonsten wie ein normales 
C-File, d.h. du wirfst es avr-gcc wie eine c-Datei zum Fraß vor.

Idealerweise machst du Für jede Funktion eine Datei. Ansonsten saugst du 
die beim Verwenden der einen Version immer auch die anderen in den Code.

BTW: Wer hat denn das Syntax-Highlight für "avrasm" verbrochen?
Sieht ja scheusslich aus...


1
/* Copyright (C) 1998, 1999, 2000, 2007, 2008, 2009
2
   Free Software Foundation, Inc.
3
   Contributed by Denis Chertykov <chertykov@gmail.com>
4
5
This file is free software; you can redistribute it and/or modify it
6
under the terms of the GNU General Public License as published by the
7
Free Software Foundation; either version 3, or (at your option) any
8
later version.
9
10
This file is distributed in the hope that it will be useful, but
11
WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13
General Public License for more details.
14
15
Under Section 7 of GPL version 3, you are granted additional
16
permissions described in the GCC Runtime Library Exception, version
17
3.1, as published by the Free Software Foundation.
18
19
You should have received a copy of the GNU General Public License and
20
a copy of the GCC Runtime Library Exception along with this program;
21
see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
22
<http://www.gnu.org/licenses/>.  */
23
24
#define __zero_reg__ r1
25
#define __tmp_reg__ r0
26
27
  .section .text.libgcc, "ax", @progbits
28
29
#if defined (__AVR_HAVE_JMP_CALL__)
30
#define XCALL call
31
#define XJMP  jmp
32
#else
33
#define XCALL rcall
34
#define XJMP  rjmp
35
#endif
36
37
.macro DEFUN name
38
.global \name
39
.func \name
40
\name:
41
.endm
42
43
.macro ENDF name
44
.size \name, .-\name
45
.endfunc
46
.endm
47
48
49
/*******************************************************
50
      Widening Multiplication  32 = 16 x 16
51
*******************************************************/
52
                              
53
DEFUN __mulhisi3
54
#if defined (__AVR_HAVE_MUL__)
55
56
;; r25:r22 = r19:r18 * r21:r20
57
58
#define A0 18
59
#define B0 20
60
#define C0 22
61
62
#define A1 A0+1
63
#define B1 B0+1
64
#define C1 C0+1
65
#define C2 C0+2
66
#define C3 C0+3
67
 
68
    ; C = (signed)A1 * (signed)B1
69
    muls  A1, B1
70
    movw  C2, R0
71
72
    ; C += A0 * B0
73
    mul   A0, B0
74
    movw  C0, R0
75
76
    ; C += (signed)A1 * B0
77
    mulsu A1, B0
78
    sbci  C3, 0
79
    add   C1, R0
80
    adc   C2, R1
81
    clr   __zero_reg__
82
    adc   C3, __zero_reg__
83
84
    ; C += (signed)B1 * A0
85
    mulsu B1, A0
86
    sbci  C3, 0
87
    XJMP  __xmulhisi3_exit
88
89
#undef A0
90
#undef A1
91
#undef B0
92
#undef B1
93
#undef C0
94
#undef C1
95
#undef C2
96
#undef C3
97
98
#else /* !__AVR_HAVE_MUL__ */
99
.error
100
#endif /* __AVR_HAVE_MUL__ */
101
ENDF __mulhisi3
102
103
104
DEFUN __umulhisi3
105
#if defined (__AVR_HAVE_MUL__)
106
107
;; r25:r22 = r19:r18 * r21:r20
108
109
#define A0 18
110
#define B0 20
111
#define C0 22
112
113
#define A1 A0+1
114
#define B1 B0+1
115
#define C1 C0+1
116
#define C2 C0+2
117
#define C3 C0+3
118
119
    ; C = A1 * B1
120
    mul   A1, B1
121
    movw  C2, R0
122
123
    ; C += A0 * B0
124
    mul   A0, B0
125
    movw  C0, R0
126
127
    ; C += A1 * B0
128
    mul   A1, B0
129
    add   C1, R0
130
    adc   C2, R1
131
    clr   __zero_reg__
132
    adc   C3, __zero_reg__
133
134
    ; C += B1 * A0
135
    mul   B1, A0
136
    XJMP  __xmulhisi3_exit
137
138
#undef A0
139
#undef A1
140
#undef B0
141
#undef B1
142
#undef C0
143
#undef C1
144
#undef C2
145
#undef C3
146
147
#else /* !__AVR_HAVE_MUL__ */
148
.error
149
#endif /* __AVR_HAVE_MUL__ */
150
ENDF __umulhisi3
151
152
;;; Helper for __mulhisi3 resp. __umulhisi3.
153
154
#define C0 22
155
#define C1 C0+1
156
#define C2 C0+2
157
#define C3 C0+3
158
159
DEFUN __xmulhisi3_exit
160
    add   C1, R0
161
    adc   C2, R1
162
    clr   __zero_reg__
163
    adc   C3, __zero_reg__
164
    ret
165
ENDF __xmulhisi3_exit
166
167
#undef C0
168
#undef C1
169
#undef C2
170
#undef C3

von StefanK (Gast)


Lesenswert?

Alles klar, vielen Dank.

Ich hätte nur noch eine Frage bezüglich der Endung: Was ist der 
Unterschied zwischen .s und .sx?

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

StefanK schrieb:
> Alles klar, vielen Dank.
>
> Ich hätte nur noch eine Frage bezüglich der Endung: Was ist der
> Unterschied zwischen .s und .sx?

.s: assembler
.sx: assembler-with-cpp

d.h. bei .sx läuft der C-Präprozessor drüber, bei .s nicht.

Unter Linux geht auch .S anstatt .sx. Unter Windoofs würd ich .S aber 
nicht verwenden weil es Probleme mit Groß/Kleinschreibung hat und ich 
schon erlebt habe, daß es zu doof ist, .s von .S zu unterscheiden. Wenn 
man dann eine clean-Rule im Makefile hat für .s, kannst du dir 
vorstellen wie's weitergeht.

Wenn einem diese Endungen nicht passen und man zB .keks lieber mag, gibt 
man einfach ein -x assembler-with-cpp schoko.keks an.

Hab eben mal die Ticks gezählt für 32 = 16*16 (signed), das müsste in 
weniger als 30 gehen (incl. RET), für die unsigned-Version noch was 
fixer.

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.