Besucher seit 10/07/2004
kostenlose counter

Im WWW Suchen

Benutzerdefinierte Suche

Rechnen in Assembler

Für viele ist das Rechnen in Assembler ein rotes Tuch. Die meisten Programmierer einer Hochsprache schreiben einfach C=A*B in ihren Code und lassen den Kompiler den Rest erledigen. Sie machen sich keine Gedanken wie der AVR das intern erledigt. Warum sollten sie auch. Gerade deshalb wurden ja Hochsprachen erfunden.
Jedoch, gerade in Assembler kann man viele Vorteile nutzen, wenn man dem AVR etwas entgegenkommt und die Zahlen die multipliziert oder dividiert  werden sollen genauer anschaut und überlegt  auf welche Art die Berechnung durchgeführt werden soll. Deshalb möchte ich hier nicht nur Grundroutinen zum multiplizieren und dividieren vorstellen, sondern auch verschiedene Rechenwege Aufzeigen. Hat man diese verstanden,  weiß man sehr genau, wann welche Rechenart Vorteilhaft ist und wird letztendlich, durch einen schnellen und kompakten Code belohnt.
Da es hier um Programmieren auf unterster Ebene geht, rechnen wir hier nur mit Bits und Bytes. Das bedeutet aber nicht, dass wir nur mit ganzen Zahlen rechnen können. Im Gegenteil. Wir bestimmen selbst, wie viele Stellen nach dem Komma benötigt werden und belasten unseren AVR nicht mit Berechnungen die 10 Stellen nach dem Komma liefern und werfen dann 8 davon weg.
Meine Rechenbeispiele sind alle mit  AVR-Studio 4.19 programmiert. Möchte man die Beispiele nachvollziehen, sollte man sich mit dem Debugger (Simulator) im AVR-Studio vertraut machen.

Anmerkung: Die Codebeispiele beziehen sich auf meinen jetzigen Wissensstand (05/2014). Ich programmiere jetzt zwar schon mehrere Jahre Assembler auf den AVRs, aber gerade bei Berechnungen lerne ich ab und an immer noch etwas dazu. Verbesserungsvorschläge zu meinen Routinen per Email sind also durchaus erwünscht.

Basics

Multiplizieren und dividieren durch wiederholtes addieren/subtrahieren

Bei dieser Technik wird in einer Schleife fortwährend der Multiplikant zum Ergebnis addiert. Den Schleifenzähler bildet der Multiplikator. Natürlich wird diese Technik kaum angewendet, da die Laufzeit bei grösseren Zahlen ins unermessliche steigt und man kleine Zahlen durch linksschieben effektiver multiplizieren kann.
Das dividieren funktioniert ähnlich. Hier wird in einer Schleife gezählt, wie oft der Dividend vom Divisor abgezogen werden kann, bis ein Unterlauf auftritt. Das Ergebnis (der Quotient) ist dann der Schleifenzähler -1.
Wenn man die Letzte Subtraktion Rückgängig macht erhält man im Dividend den Rest. Diese Art der Division wird häufig bei der Wandlung einer Zahl in einen String angewendet.

Anwendungsbeispiel:

    ;-------------------------------------------------------------------------------
    ;16Bitwert (max. 9999) in einzelne (Dezimal) Ziffern zerlegen
    ;-------------------------------------------------------------------------------
pos:    ldi        temp1,-1             ;1000er = -1
sub1000:inc        temp1                ;1000er = +1
        subi       temp2,low(1000)      ;TSicwert-1000
        sbci       temp3,high(1000)
        brcc       sub1000              ;Solange 1000 abziehen bis Unterlauf
        mov        chr4,temp1           ;1000er wegspeichern

        ldi        temp1,10             ;100er = 10
add100: dec        temp1                ;100er - 1
        subi       temp2,low(-100)      ;TSicwert +100
        sbci       temp3,high(-100)
        brcs       add100               ;Solange +100 bis Überlauf
        mov        chr3,temp1           ;Zehner

        ldi        temp1,-1
sub10:  inc        temp1
        subi       temp2,10             ;-10
        brcc       sub10
        subi       temp2,-10
        mov        chr2,temp1           ;Einer
        mov        chr1, temp2          ;Rest = 10tel

Will man eine Zahl auf einem Display Dezimal ausgeben, muss man diese in einzelne Ziffern zerlegen. Dies geschieht im Codebeispiel oben. Die einzelnen Ziffern werden zwecks späterer Ausgabe auf einer Siebensegmentanzeige benötigt und deshalb in den Registern chr1..4 Zwischengespeichert.   Die Zahl liegt in den Registern temp2 / temp3 (low/high) als 16-Bit Wert vor und übersteigt 9999 nicht, sodass sichergestellt ist, das 4 Stellen genügen um diese darzustellen. Um die erste Ziffer (die 1000er Stelle) zu erhalten, muss temp2/temp3  durch 1000dez  dividiert werden. Der Rest wird aufbewahrt um daraus später die 100er, 10er und 1er zu berechnen. Da der Schleifenzähler temp1 mit -1 initialisiert wird, hinkt dieser der Division um 1 hinterher damit beim Ende der Schleife (beim Unterlauf) keine 1 mehr abgezogen werden muss.
Nun sind ja durch den Unterlauf 1000 zuviel abgezogen worden und dadurch temp2/temp3 ins negative gerutscht. Normalerweise würde man durch aufaddieren von 1000 die letzte Subtraktion Rückgängig machen. Im Code oben spare ich mir die (16-Bit) Addition und addiere bei der 100er Stelle 100 bis zum Überlauf. Zu beachten ist, das der Zähler mit 10 initialisiert wird und in der Schleife heruntergezählt wird. Da wir jetzt wieder im positiven Bereich sind, wird bei den 10ern wieder abgezogen. Die 1er Stelle ist der Rest der Division durch 10, deshalb wird in diesem Fall, die letzte Subtraktion durch aufaddieren von 10 (subi temp2,-10) wieder Rückgängig gemacht. Bei den 10ern rechne ich, wie man sieht, nur noch mit 8-Bit.

Die einzelnen Ziffern in chr1..4, können nun als Index auf eine Tabelle mit Siebensegmentcodes verwendet werden. Bei der Ausgabe auf ein LCD muss einfach noch zu jedem Wert der Offset des LCD-Zeichen '0' addiert werden, bevor man es zum Display schickt.

Multiplizieren durch Linksschieben.

Soll eine Konstante mit einem Register Multipliziert werden, ist linksschieben die einfachste Art dies durchzuführen.

Beispiel: In Register R16 befindet sich die Zahl 1. Nach ausführen des Befehl lsl r16, wird von links eine 0 eingeschoben und  r16 enthält als Ergebnis eine 2. Das Register wurde mit 2 multipliziert. Jeder weitere lsl r16 Befehl verdoppelt die Konstante: Anders gesagt, der Exponent der 2er Potenz wird um 1 erhöht. Wird also 3 mal Links geschoben wurde mit 8 multipliziert (2^3 =8). Das Funktioniert auch mit 16-Bit (oder noch höheren) Zahlen indem der Überlauf über das Carry-Flag, ins nächste Register geschoben wird. Es sind dann 2 Befehle Notwendig, nämlich lsr r16 mit nachfolgendem rol r17. Beschränkt man sich auf die Schiebebefehle, können so nur 2er Potenzen multipliziert werden. Der Trick, X-Beliebige Zahlen als Multiplikant zu benutzen, besteht darin, das man sich Zwischenergebnisse merkt und diese an geeigneter Stelle addiert oder subtrahiert. Soll z.B r16 mit 14 multipliziert werden gibt es mehrere Möglichkeiten.

Anfangswert merken, 3 mal schieben(*8), Anfangswert abziehen(*7), 1 mal schieben(*14). ODER. 1 mal schieben (*2), merken, 3 mal schieben (*16), abziehen (*14).

So wird klar, das der Code in Größe und Laufzeit von der verwendeten Zahl des Multiplikanten abhängt.

Bytes anfügen

Auch grössere Zahlen können durch schieben multipliziert werden. Ein Anfügen einens Nullbytes entspricht 8* nach links schieben.
Ähnlich wie im Dezimalsystem das Komma bei einer Multiplikation mit 10 um eine Stelle nach rechts verschoben wird, kann im Binärsystem eine Multiplikation mit 256 (0x100) einfach erfolgen, indem man einfach ein Nullbyte anhängt. Soll also z.B. ein Byte mit 128 multipliziert werden, hängt man zuerst ein Nullbyte an und schiebt dann das 16-Bit Paket einmal nach rechts welches den 256fachen Wert durch 2 dividiert.

Beispiel:

In r20/r21 befindet sich der Wert 10 als 16 Bit Wert (0x000A). Dieser soll mit 256 (0x100) multipliziert werden. Dieses kann man erreichen indem man r20/r21 8 mal nach links schiebt. Die Befehle dazu währen 8 mal hintereinander lsl r20 gefolgt von rol r21. Als Ergebniss hätten wir wieder 10 (0x0a) im Highbyte (r21) und 0 im Lowbyte (r21) was dem Dezimalen Ergebniss von 2560 (0x0A00) entspricht. Die ganze Schieberei kann man also Abkürzen in dem man im Beispiel oben r20 nach r21 kopiert und r20 löscht (auf 0 setzt). Noch kürzer, wird das ganze, wenn einfach r19 gelöscht wird und r20 als Highbyte und r19 als Lowbyte angesehen wird. Das anfügen von Bytes kann, wie im Dezimalsystem das rechtsschieben des Kommas *10 bewirkt,  fortgeführt werden. Das anfügen von r18 hätte also eine weitere Multiplikation mit 256 zur Folge, wenn r20..r18 als 24-Bit Zahl angesehen würde. Das Funktioniert natürlich auch rückwärts. Das entfernen des niederwertigsten Bytes kommt einer Division mit 256 gleich.

Manchmal ist es auch notwendig die ursprünglichen Werte zu erhalten und ein oder mehrere Ergebnisregister zu definieren. Bei einer Multiplikation von der Konstante 256 ist das besonders einfach. Wenn der Wert R20*256 in r22 und r23 gespeichert werden soll,  braucht nur r22 gelöscht zu werden (clr r22) und r20 nach r23 (mov r23,r20) kopiert zu werden.
Es lohnt sich bei einer Multiplikation also immer, nachzuprüfen ob diese mit Schiebebefehlen und/oder hinzufügen von Nullbytes Zeit und Codesparend durgeführt werden kann. Das kann unter Umständen schneller und kürzer sein als, als wenn man die Rechnung mit dem Hardwaremultiplizierer durchführen lässt.

Beispiel: Multiplikation mit 250

;    Temperatur*10=(TSic_Wert*250)/256-500
;---------------------------------------------------------------------------------------
        clr        temp1            ;temp3/2/1 enthält durch zufügen von Temp1 (0)
        mov        temp2,rawlow     ;den TSicwert * 256
        mov        temp3,rawhigh
        lsl        rawlow           ;*2
        rol        rawhigh
        add        rawlow,temp2     ;*3
        adc        rawhigh,temp3
        lsl        rawlow           ;*6
        rol        rawhigh
        sub        temp1,rawlow     ;*256 - *6 = *250
        sbc        temp2,rawhigh    ;TSic_Wert*250 steht jetzt in temp1/temp2/temp3
        sbci       temp3,0          ;Durch verwerfen von temp1 wird durch 256 geteilt   
        subi       temp2,low(500)   ; 500 (50.0) abziehen
        sbci       temp3,high(500)

 ;Nun steht die Temperatur(*10) in temp2 (low) und temp3 (high)

Im Code oben soll der Rohwert eines TSIC206 Temperetursensors in  C° umgerechnet werden. Die Ausleseroutine liefert den 16-Bit Rohwert in rawlow/rawhigh. Der Wert hat eine Range von 0..0x7ff und soll zunächst mit 250dez multipliziert werden. Dazu wird der Wert in die 3 Ergebnissregister temp1..3 kopiert und gleich um 8 Bit nach links verschoben, was einer Multiplikation von 256 gleichkommt. Die nächsten 6 Befehle, multiplizieren den Rohwert durch schieben und addieren mit 6. Nach dem subtrahieren vom 24-Bitwert (*256) , ist das Produkt der Multiplikation (rawlow/rawhigh*250) in den Registern Temp1..3 gespeichert.
Durch verwerfen (wegdenken) von temp1 wird durch 256 geteilt. Die Subtraktion von 500 laut Formel ist trivial und muss hier nicht weiter erklärt werden.
Mit dem Codeschnippsel oben, wollte ich zeigen das durch Bitschieben durchaus effizient multipliziert werden kann wenn die Zahlen passen. Herauszufinden ob die Zahlen passen, ist die Aufgabe des Programmierers. Übrigens könnte oben noch 1 Befehl eingespart werden, wenn man die 2 mov Befehle durch einen movw Befehl zusammenfassen würde.

Multiplizieren mit einer Tabelle (LUT)

Das Multiplizieren mit einer Tabelle (Spickzettelmethode ) muss eigentlich nichts rechnen, sondern sie muss nur wissen wo das Ergebniss steht. Multiplizieren mit einer LUT kann durchaus sinvoll sein. Dies trifft 1. zu, wenn der Bereich des Multiplikanten klein ist, oder 2. eine besonders schnelle Laufzeit  erwünscht ist. In der Tabelle könnten natürlich auch Festkommawerte eingetragen werden. Im Beispiel unten wird  700 mit 8 multipliziert.

.def    temp1   =   r16
.def    null    =   r2

      clr     null
      ldi     temp1,8             ;Testwert, in diesem Fall 8*700
      ;------------------------------------------------------------------------------
      lsl     temp1               ;Tabellenzeiger*2, da Words in der Tab stehen
      ldi     zl,low(mul700<<1)   ;Zeiger auf ersten Tabelleneintrag
      ldi     zh,high(mul700<<1)
      add     zl,temp1            ;Offset addieren (in diesem Fall 16 Bytes überspringen)
      adc     zh,null             ;Carry addieren.
      lpm     yh,z+               ;16 Bit Wert (5600) aus der Tabelle nach Y holen
      lpm     yl,z
      ;-------------------------------------------------------------------------------
stop: rjmp    stop

;Mit dieser kleinen Tab. kann die Ziffer  0..13 (8 Bit in temp1) mit 700 multipliziert werden

mul700: .dw 0,700,1400,2100,2800,3500,4200,4900,5600,6300,7000,7700,8400,9100

Natürlich muss die die Tabelle den höchsten Wert die der Multiplikant annehmen kann abdecken. Die Vorteile der LUT ist die enorme Geschwindigkeit die unabhangig vom Wert des Multiplikators immer gleich bleibt. Die Nachteile sind bei grösseren Tabellen der Flashspeicherverbrauch. Mit Tabellen können alle möglichen Rechenoperationen durchgeführt werden. Das Beispiel oben zeigt nur eine einfache Variante. Es könnten zum Beispiel anstatt des 16-Bitwert auch gleich der Siebensegmentcode oder ASCII-Zeichen für ein LCD in die Tabelle eingetragen werden.

Multiplizieren in Hardware

Im Gegensatz zu den Tiny AVRs ist in den ATMegas der mul Befehl enthalten, der 2 Bytes miteinander multipliziert und das 16 Bit Ergebniss in den beiden Registern R0 und R1 liefert. In der Appnote 201 von Atmel, kann man nachlesen, wie man mit mehreren 8-Bit Multiplikationen und anschließendem addieren, 2 16-Bit Zahlen miteinander Multipliziert. Besser ist es jedoch, wenn man sich etwas mit der Materie beschäftigt und selbst weiß, wie die einzelnen mit mul multiplizierten Bytes zu den Ergebnissregistern addiert werden um so z.B 24 * 8 Bit Multiplikationen oder beliebig andere Kombinationen ausführen zu lassen.

Beispiel:

Die Register r16, r17,r18 sollen mit r19 multipliziert werden. Das ist eine 24*8 Bit Multiplikation und benötigt daher 4 Register um das 32-Bit Ergebnis zu speichern. Wir nehmen hier einfach r20..r23. Wie man schon vermuten kann benötigt man 3 8-Bit Multiplikationen um die Rechnung durchzuführen. Die einzelnen Teilmultiplikationen werden alle mehr oder weniger verschoben dem Ergebnis hinzugefügt (addiert, gemovt). Zunächst wird das niederwertigste Byte r16, durch mul r16,r19 multipliziert. Das Ergebnis, welches sich schon nach 2 Taktzyklen  in r0 und r1 befindet, wird in die Ergebnisregister r20 und r21 kopiert.

;Multiplikation 24*8 Bit by Juergen@avr-projekte.de
.equ    f1  =$abcdef
.equ    f2  =$f8
.org 0
        clr     r2
        ldi     r16,byte1(f1)       ;Testwerte laden
        ldi     r17,byte2(f1)
        ldi     r18,byte3(f1)
        ldi     r19,f2          
        ;---------------------------------------
        ;Multiplikation r20..r23 = r16..r18 * r19
        ;---------------------------------------
mul24_8:clr     r22
        clr     r23
        mul     r16,r19             ;byte1 * r19
        movw    r20:r21,r0:r1       ;Ohne verschieben ins Ergebniss
        mul     r17,r19             ;byte2 * r19
        add     r21,r0              ;Um 1 Byte verschoben addieren (* 0x100)
        adc     r22,r1
        adc     r23,null
        mul     r18,r19             ;byte3 * r19
        add     r22,r0              ;Um 2 Byte verschoben addieren (*0x100 * 0x100)
        adc     r23,r1

brk:    rjmp    brk

Da r17 an 2. Stelle im 24Bit Wert steht, müssen wir hier in Gedanken ein Nullbyte anhängen . Das Nullbyte denken wir uns als an R17 angehängt und multiplizieren das Highbyte (r17) von unserem gedachten 16-Bit Wert mit r19. An das Ergebnis in R0 und R1 kommt also noch ein weiteres gedachtes Register Rx welches Null (eine Zahl mal 0 ist immer null) enthält und somit das Niederwertigste Byte im 24 Bit Ergebnis r1,r0 und rx darstellt. Diese 3 Register werden nun zum Ergebniss addiert.
Die Erklärung oben, mit den gedachten Registern läuft im Endeffekt darauf hinaus, dass wir das Ergebnis in r0 und r1 um um eine Stelle (1 Byte/8 Bit) nach links zum Ergebnis addieren, wenn r17 eine Stelle im Multiplikant nach links verschoben ist.  Diese Verschiebung bewirkt, das das Ergebnis nochmals mit 256dez (0x100) multipliziert wird.
Bei r18 * r19 müssen 2 Nullbytes angehängt werden und das Ergebnis in r0 und r1, sind dann eben Byte3 und Byte4 des 32 Bit Ergebnis. Wir verschieben die Addition also um 2 Bytes, wenn im Multiplikant rechts noch 2 Bytes vorhanden sind.

Im Code oben, werden die einzelnen Bytemultiplikationen der Reihe nach durchgeführt. Damit Zwischenergebnisse später hinzuaddiert werden können, wird am Anfang r22 und r23 gelöscht. Da die Reihenfolge der Teilmultiplikationen keine Rolle spielt, kann man durch eine geschickte Festlegung dieser Reihenfolge noch etwas Code sparen. Im Code unten wurde diese so verändert, das nur noch eine Multiplikation addiert werden muss und die clr Befehle wegfallen können. Das lässt den eh schon kleinen Code der Multiplikation, von 11 auf 8 Befehle schrumpfen.

;Multiplikation 24*8 Bit by Juergen@avr-projekte.de
.equ    f1  =$abcdef
.equ    f2  =$f8
.org 0
        clr     r2                  ;Wird zum Carry addieren benutzt
        ldi     r16,byte1(f1)       ;Testwerte laden
        ldi     r17,byte2(f1)
        ldi     r18,byte3(f1)
        ldi     r19,f2          
        ;---------------------------------------
        ;Multiplikation r20..r23 = r16..r18 * r19
        ;---------------------------------------
mul24_8:mul     r16,r19             ;byte1 * r19
        movw    r20:r21,r0:r1       ;Ohne verschieben ins Ergebniss
        mul     r18,r19             ;byte3 * r19
        movw    r22:r23,r0:r1       ;Um 2 Byte verschoben ins Ergebniss (*0x10000)
        mul     r17,r19             ;byte2 * r19
        add     r21,r0              ;Um 1 Byte verschoben addieren (* 0x100)
        adc     r22,r1
        adc     r23,null

brk:    rjmp    brk

In der folgenden Tabelle wird als Beispiel eine 24Bit Zahl mit einer 16 Bit Zahl Multipliziert. Das 40 Bit-Ergebnis wird den Registern E1 bis E5 hinzugefügt (gemovt oder addiert). Da links vom Malzeichen der Multiplikation 3 Bytes und rechts davon 2 Bytes stehen sind insgesamt (3*2) 6 einzelne 8-Bit Multiplikationen notwendig. Da 5 Register für die Faktoren gebraucht werden, sind auch 5 Register für das Ergebnis notwendig.

mul Befehl

Unabhängig von der Bitbreite der Faktoren kann man also sagen:

Abschießend zur Hardwaremultiplikation möchte ich noch die Befehle muls und mulsu erwähnen, welche mit einer oder zwei negativen Zahlen multiplizieren. Ich hatte bisher noch keine Verwendung für diese Befehle, da ich bisher die Formeln immer so drehen konnte, dass ich im positiven Bereich rechnen konnte.

Multiplikation in Software

Haben wir einen Tiny AVR am Start, können wir den mul Befehl leider nicht nutzen, da dieser nur bei den Megas verfügbar ist. Hier bietet sich an, den mul Befehl in Software nachzubilden um diesen als Unterprogramm oder Makro nachzubilden. Im Beispiel unten ist erst einmal ein Codeschnipsel der r16 mit r17 multipliziert. Das Ergebnis (Produkt) ist wie beim mul Befehl im Registerpaar r0/r1. Der Algorithmus erinnert stark an das multiplizieren wie es in der Grundschule im Dezimalsystem gelehrt wird. Es ist eigentlich im Binärsystem noch einfacher, da nur mit 0 und 1 multipliziert wird.

;Multiplikation 8*8 Bit by Juergen@avr-projekte.de

;        r16=137  * r17=12
;        -------------------
;        10001001 * 00001100  LSB(r16)
;        -------------------  ___
;        ------->00001100     |1| (8 mal rechts schieben)
;               00000000      |0| (7)
;              00000000       |0| (6)
;             00001100        |1| (5)
;            00000000         |0| (4)
;           00000000          |0| (3)
;          00000000           |0| (2)
;         00001100            |1| (1 mal rechts schieben)
;        ----------------    ---
;        0000011001101100     MSB(r16)
;        |--R1--||--R0--|
;           1644dez      

#include "tn2313def.inc"

        ldi     r16,200 ;Testwerte laden
        ldi     r17,150
        ;----------------------------------
        ;Multiplikation r0..r1 = r16 * r17
        ;----------------------------------
_mul:   clr     r1      ;Ergebnis = 0
        ldi     xl,8    ;8 mal schieben
mul1:   lsr     r16     ;Bit (von 0..7 Niederwertige zuerst) ins Carry (Multiplikand)
        brcc    mul2    ;Wenn 0, Addition überspringen
        add     r1,r17  ;Ansonsten Multiplikator ins Highbyte addieren
mul2:   ror     r1      ;Produkt mit evt. Übertrag nach rechts schieben
        ror     r0
        dec     xl      ;Das ganze 8 mal
        brne    mul1
        ;----------------------------------
        ;Ende mul
        ;----------------------------------
brk:    rjmp    brk

Viele dieser Routinen schieben das MSB zuerst ins Carry und schieben dann das Ergebnis der Addition in einer Schleife nach links. Beispiel http://www.mikrocontroller.net/articles/AVR-Tutorial:_Arithmetik8#Multiplikation_in_Software. Die Routine oben, nimmt den Umgekehrten Weg. Zuerst wird das LSB durch rechtsschieben  ins Carry geholt. Ist dieses gesetzt, wird der Multiplikator zum Highbyte des Ergebnis (r1) addiert und anschließend nach rechts geschoben. Im weiteren Verlauf der Schleife, die ja 8 mal durchlaufen wird, wandert das Ergebniss immer weiter nach rechts (in r0) und befindet sich am Ende an der richtigen Position.
Bei jedem weiteren Durchlauf, wird das nächsthöhere Bit ins Carry geschoben, welches wieder bestimmt, ob addiert werden muß (1), oder nicht (0).
Vergleicht man den Code mit dem herkömmlichen Algorithmus, wird man feststellen, das ein paar Befehle eingespart werden können. Es braucht nur noch ein clr für das Highbyte. Der adc Befehl der normalerweise dem add folgt, kann auch entfallen. Dies ist besonders angenehm, da sich dieser in der Schleife befindet und somit auch ein bisschen Laufzeit spart.

Will man den Code oben als Ersatz für den mul Befehl 1:1 als Makro ausführen, hat dieser 2 Schönheitsfehler. 1) Es wird beim Aufruf r16 zerstört. Dieses muss, um mit dem mul Befehl Kompatibel zu sein gepusht werden. 2) Es wird ein Schleifenzähler benutzt. Dieses Register kann also nicht als Multiplikant herhalten. Diesen könnte man zwar auch pushen und umkopieren, doch es gibt noch eine andere Möglichkeit die auch ohne einen Schleifenzähler auskommt.

;Multiplikation 8*8 Bit by Juergen@avr-projekte.de
#include "tn2313def.inc"

.macro _mul
        ;---------------------------------
        ;mul Befehl in Software als Makro
        ;---------------------------------
        clr     r1      ;Ergebnis = 0
        push    @0      ;Multiplikant wegspeichern
        sec             ;Multiplikant ersetzt gleichzeitig den Schleifenzähler
        ror     @0      ;Eine 1 rein, LSB raus
        rjmp    mul2    ;Beim 1. Durchgang, gleich LSB auswerten
mul1:   lsr     @0      ;Bit (von 1..7 Niederwertige zuerst) ins Carry (Multiplikand)
        breq    mul4    ;Wenn die 1 von sec (oben) wieder im Carry ist, Ende
mul2:   brcc    mul3    ;Wenn 0, Addition überspringen
        add     r1,@1   ;Ansonsten Multiplikator ins Highbyte addieren
mul3:   ror     r1      ;Produkt mit evt. Übertrag nach rechts schieben
        ror     r0
        rjmp    mul1    ;Noch ne Runde (von insgesamt 8)
mul4:   pop     @0      ;Multiplikant zurückholen
.endm
        ;---------------------------------
        ;Ende Makros
        ;---------------------------------
        ldi     r16,250 ;Testwerte laden
        ldi     r17,150
        _mul    r16,r17 ;Makroaufruf
        
brk:    rjmp    brk

Dazu wird vor der Schleife eine 1 in den Multiplikator geschoben. Die 1 wandert in der Schleife dann (lsr Befehl) durch das komplette Register. Ist die 1 dann wieder im Carry angelangt, wurden durch den lsr Befehl 8 Nullen eingeschoben, die Zeile < breq mul4 > greift und springt aus der Schleife.
Das Makro ist in der Laufzeit zwar etwas länger als die Version weiter oben, dafür aber (fast) Kompatibel mit dem mul Befehl, da es ohne zusätzliche Register auskommt. Das fast bedeutet, dass r0 und r1 nicht miteinander multipliziert werden dürfen, beim Hardwaremultiplizierer ist das möglich. Wer auch das möchte, muss den Code durch weitere push/pop Befehle erweitern. Das Makro ist geeignet, wenn ein Programm, das von einem ATMega stammt auf einen Tiny angepasst werden soll bei dem der mul Befehl häufig zum Einsatz kommt. Kürzer und auch schneller ist es allerdings, wenn man gleich Routinen benutzt die in der Bitbreite zur jeweiligen Aufgabe passen. Unten habe ich zwei Beispiele für 16*16 Bit Multiplikationen. Die erste schiebt vom LSB nach links, die zweite schiebt (wie oben erklärt) nach rechts. Hat man den Algorithmus erst einmal verstanden, ist es ein leichtes die Routinen auf andere Bitbreiten anzupassen.

Nach links

;-------------------------------------------------------------------------
;Multiplikation 16*16=32 Bit. Faktor1=Y, Faktor2=Z, Ergebniss temp1..temp4
;-------------------------------------------------------------------------
mul16:  ldi        temp5,16        ;16*schieben
        clr        temp1           ;Ergebnis (Produkt)löschen
        clr        temp2
        clr        temp3   
        clr        temp4
mul01:  lsl        temp1           ;Ergebniss schieben
        rol        temp2
        rol        temp3
        rol        temp4
        lsl        yl              ;Herausgeschobenes Bit aus Multiplikator (ADC)..
        rol        yh              ;..entscheidet ob addiert wird
        brcc       mul02           ;Wenn Bit=0 nicht addieren
        add        temp1,zl        ;Wenn Bit=1 Multiplikant (Formel) zum Ergebniss addieren
        adc        temp2,zh
        adc        temp3,null
        adc        temp4,null
mul02:  dec        temp5            ;Alle 16 Bit durch ?
        brne    mul01
        ret

Nach rechts

;Multiplikation 16*16 Bit by Juergen@avr-projekte.de
#include "tn2313def.inc"

.equ    zahl1   =   40000
.equ    zahl2   =   50000

        ldi     r16,low(zahl1) ;Testwerte laden
        ldi     r17,high(zahl1)
        ldi     r18,low(zahl2) ;Testwerte laden
        ldi     r19,high(zahl2)
        ;------------------------------------------
        ;Multiplikation r0..r3 = r16:r17 * r18:r19
        ;           (low...high)(low/high)(low/high)
        ;------------------------------------------
_mul:   clr     r3      ;Ergebnis = 0
        clr     r2
        ldi     xl,16
mul1:   lsr     r17     ;Bit (Niederwertige zuerst) ins Carry (Multiplikand)
        ror     r16
        brcc    mul2    ;Wenn 0, Addition überspringen
        add     r2,r18  ;Ansonsten Multiplikator ins Highbyte addieren
        adc     r3,r19
mul2:   ror     r3      ;Produkt mit evt. Übertrag nach rechts schieben
        ror     r2
        ror     r1
        ror     r0
        dec     xl      ;Das ganze 8 mal
        brne    mul1
        ;----------------------------------
        ;Ende mul
        ;----------------------------------
brk:    rjmp    brk 

Dividieren

Dividieren durch rechtsschieben

Das Dividieren durch rechtsschieben funktioniert nur bei Zweierpotenzen. Hier kann man leider nicht, wie beim Multiplizieren Zwischenwerte addieren oder subtrahieren. Zweierpotenzen kommen aber gerade im Assemblercode recht häufig vor. Beim anlegen eines mehrdimensionalen Array (LUT, Tabelle) ist es oft von Vorteil, wenn die Anzahl der Einträge eines Datenblocks auf z.B. 2,4 oder 8 anlegt, da so der Zeiger (Z-Register) einen schnellen Zugriff ermöglicht. Der Programmierer, der einen schnellen und unkomplizierten Zugriff wünscht, provoziert hier geradezu eine Zweierpotenz und opfert bei kleineren Tabellen  lieber ein Füllbyte in der Tabelle, als das er er mit 7 multipliziert oder dividiert.

Rest

Oft wird der Rest benötigt. Der kann bei einer Division die durch schieben erfolgt, vor der eigentlichen Division berechnet werden. Beispiel: Ein Byte in r16 sol durch 8 geteilt werden, der Rest wird in r17 gespeichert.

#include "m48def.inc"

.equ    dividend    =   100

        ldi     r16,dividend    ;Dividend
        mov     r17,r16         ;Rest nach r17
        andi    r17,0b111       ;Rest ausmaskieren
        lsr     r16             ;r16 /2
        lsr     r16             ;/4
        lsr     r16             ;/8

Da man bei einer Division durch 8 weiß, das 3 mal geschoben wird, werden einfach die unteren 3 Bits ausmaskiert. Also: Die Bits, die später herausgeschoben werden, müssen beim ausmaskieren stehen bleiben.

Runden

Das Auf- und Abrunden einer Division, kann erfolgen indem man einfach den halben Divisor zum Dividenden addiert. Um beim Beispiel von oben zu bleiben, wird im Code unten zum Dividend der durch 8 geteilt werden soll, 4 addiert.

#include "m48def.inc"

.equ    dividend    =   100

        ldi     r16,dividend    ;Dividend
        add     r16,4           ;Den halben Dividend (8/2) addieren
        lsr     r16             ;r16 /2
        lsr     r16             ;/4
        lsr     r16             ;/8

Das Runden durch Addition des halben Divisors funktioniert bei allen Arten der Division genauso.

Division vermeiden

Man kann eigentlich jede Division vermeiden, indem man mit dem Kehrwert multipliziert. Dazu muss der Divisor allerdings bekannt sein.

Beispiel:

Ein Byte soll durch (die dem ASM-Programmierer unsympathische ) 7 dividiert werden. Wir benutzen den Taschenrechner und rechnen 1/7. Das Ergebnis ist 0,1428571428571429. Eine Multiplikation mit dieser Zahl teilt unser Byte also durch 7. Leider passt die Zahl kein AVR-Register, wir müssen also noch etwas daran drehen. Zuerst multiplizieren wir 0,1428571428571429 mit 256 und sehen uns das Ergebnis  von 36,57142857142857 an. Wir könnten nun auf oder abrunden und unser Byte mit 36 oder 37 multiplizieren und das Ergebnis wieder durch 256 Teilen. Die Division durch 256 ist für den AVR ein Klacks. Es wird einfach 8 mal nach rechts geschoben oder wie schon oben beschrieben das Lowbyte verworfen (wir denken uns das ganz einfach weg). Damit kommen wir dem richtigen Ergebnis schon recht nahe.
Die Zahl hinter dem Komma ist ausschlaggebend. 36,57142857142857 ist recht ungünstig, da eine 5 hinter dem Komma steht. Gut ist eine 1 Optimal währe eine Null. Wir multiplizieren die 36,57142857142857 nochmals mit 2 und erhalten 73,14285714285714. Besser. Nun wissen wir das wir abrunden können und unser Byte wird mit 73 multipliziert. Das Ergebnis wird einmal nach rechts geschoben (durch 2 geteilt) und wieder das Lowbyte verworfen.

Das Programm dazu ist einfach.

.equ    dividend    =   100

        ldi     r16,73          ;Kehrwert von 7 *256 *2
        ldi     r17,dividend
        mul     r16,r17         ;Multiplizieren
        lsr     r1              ;Durch 2 teilen und r0 verwerfen

Der Quotient von r17 durch 7 ist nun in r1. Natürlich muss das im Simulator mit mehreren Dividenden geprüft und nachgerechnet werden. Gute Dienste leiste hier der Windows Rechner, der im Programmierer-Modus auch mit Hex und Binärzahlen umgehen kann. Ist das Ergebnis noch nicht zufriedenstellend, wird ein weiteres Byte angehängt und mit 65536 (0x10000) multipliziert und wieder evtl. geschoben. Es werden dann eben die unteren 2 Bytes verworfen. So kann man für fast jeden Dividenden einen passenden Multiplikanden finden. Man muss nur aufpassen, das man das, was man vorher mit der Zahl am Taschenrechner anstellt, später im Code wieder rückgängig macht. Das ganze auf grössere Zahlen zu übertragen ist einfach. Das multiplizieren von grösseren Zahlen ist ja oben schon erklärt.
Das Ergebnis oben ist abgerundet. Möchte man gerundete Ergebnisse  erhalten,  addiert man auch hier einfach vor der Division den halben Divisor (einmal rechts schieben) in diesen Fall 3 zum Dividenden. Komplizierter als beim schieben, wird es wenn der Rest benötigt wird. Hier muss der Quotient mit dem Divisor multipliziert werden und vom Dividend subtrahiert.
Der Code dazu:

#include "m48def.inc"

.equ    dividend    =   100

        ldi     r16,73          ;Kehrwert von 7 *256 *2
        ldi     r17,dividend
        mul     r16,r17         ;Multiplizieren
        lsr     r1              ;Durch 2 teilen und r0 verwerfen
        mov     r16,r1          ;Ergebnis retten
        ldi     r18,7           ;Quotient * 7
        mul     r18,r1
        sub     r17,r0          ;Divident - (Quotient*7)

Das Ergebnis befindet sich am Ende in r16 und der Rest in r17. Wird der Rest benötigt und wir haben einen Tiny am Start, für welchen "mul" ein Fremdwort ist, sollte man überlegen ob nicht eine Division über Software Sinnvoll wäre, da eine solche Routine den Rest automatisch liefert.

Division in Software

Leider gibt es bei den AVRs keinen Hardwaredividierer, jedoch ist es kein Problem in Software zu dividieren. Ähnlich wie in der Grundschule, holen wir von links die erste Ziffer aus dem Dividenden (im Binärsystem ja nur 0 oder 1)und schauen ob diese grösser oder gleich unserem Divisor ist. Ist der Divisor kleiner,  tragen wir eine 0 in den Quotienten ein. Das müssen Grundschüler bei den ersten Ziffern zwar nicht, beim Binären dividieren steht jedoch die Anzahl der Bits im Quotienten fest. Nun holen wir die nächste Zahl "herunter", schieben also die nächste Ziffer dazu. Dieser Vorgang wird solange wiederholt, bis unser Divisor in die Ziffernfolge (Binärmuster) "geht". Jetzt tragen wir eine 1 in den Quotienten ein und Subtrahieren den Divisor von unserer Ziffernfolge. Danach geht es weiter wie gehabt. Bei einer 16/8 Bit Division, wie sie unten zu sehen ist, machen wir das 16 mal und die Division ist fertig.

#include "m48def.inc"
.def    temp1   =   r16
.def    temp2   =   r17
.def    temp3   =   r18
.def    temp4   =   r19

.equ    dividend    =   4711

        ldi     temp2,low(dividend)
        ldi     temp3,high(dividend)
        rcall   div
brk:    rjmp    brk
        ;--------------------------------------------------------------------------------
        ;    16Bit Division durch 8Bit Konstante. In diesem Fall 10.
        ;    Übergaberegister & Ergebniss in temp2/temp3 (low/high), Rest = temp1
        ;--------------------------------------------------------------------------------
div:    ldi     temp4,16    ;16* schieben
        clr     temp1       ;Rest =0
dv1:    lsl     temp2       ;Ein Bit aus dem Dividend ...  
        rol     temp3       ;ins Carry schieben
        rol     temp1       ;Carry in den Rest schieben
        ;brcs    dv3         ;Bei Überlauf von Rest, Sprung
        cpi     temp1,10    ;Solange Bits in temp1 (Rest) schieben bis >= 10
        brcs    dv2         ;Wenn Rest <10, das 0-Bit in temp2 (LSL, oben) stehen lassen
dv3:    inc     temp2       ;Ansonsten Bit0 setzen und 10 vom Rest abziehen
        subi    temp1,10  
dv2:    dec     temp4       ;Das ganze 16 mal
        brne    dv1
        ret
        ;-------------------------------------------------
        ;        Ende Div Routine.
        ;-------------------------------------------------

Im Code oben. wandern die Bits des Dividenden (temp2:temp3) solange in den Rest (temp1), bis der Rest grösser oder gleich dem Divisor (10) ist. Ist dies der Fall, wird Bit 0 im Quotient gesetzt. Der Quotient braucht kein eigenes Register und landet auch in temp1:temp2 welches am Anfang den Dividenden enthält. Dies ist möglich, da ja das MSB durch linksschieben zuerst aus dem Dividenden geholt wird.Das LSB ist zu diesem Zeitpunkt noch Null und wird erst nach dem Vergleich durch inc temp2  gesetzt, oder eben nicht.
Der Divisor in der Routine oben, darf in dieser Form max. 127 betragen und ist oben mit 10 definiert. Soll auch der Divisor variabel sein, ändert man einfach cpi temp1,10 in cp temp1,divisor und subi temp1,10 in sub temp1,divisor.
Ist der Divisor grösser als 127 (Bit 7 ist gesetzt), kann während der Laufzeit der Rest bei rol temp1 überlaufen. Deshalb muss noch der Befehl brcs dv3 aktiviert werden. Dieser überspringt bei einem Überlauf den nachfolgenden Vergleich und setzt Bit 0 im Quotient. Keine Angst, die Subtraktion von unserem 9 Bit Wert, stimmt (Im Simulator testen).
Ist genügend Flashspeicher vorhanden, könnte man noch die Laufzeit der Routine verringern. Da wir wissen, das den Divisor 10 beträgt und die Binäre 10 vierstellig ist, könnte man schon vor der Schleife ohne zu vergleichen, 4 mal vom Dividenden in den Rest schieben und hätten dann nur noch 12 Durchgänge in der Schleife.

Auch diese Routine lässt sich leicht an andere Bitbreiten anpassen. Unten ein Beispiel einer 32/16 Bit Divisionsroutine mit variablem Divisor.

#include "m48def.inc"
.def    temp1   =   r16
.def    temp2   =   r17
.def    temp3   =   r18
.def    temp4   =   r19
.def    temp5   =   r20

.equ    dividend    =   0x12345678
.equ    divisor     =   0xaffe

        ldi     temp1,byte1(dividend)   ;Testwerte laden
        ldi     temp2,byte2(dividend)
        ldi     temp3,byte3(dividend)
        ldi     temp4,byte4(dividend)
        ldi     yl,low(divisor)
        ldi     yh,high(divisor)
        rcall   div                     ;Dividieren
                                               
brk:    rjmp    brk                     ;Ende
        ;--------------------------------------------------------------------------------
        ;    32Bit Division durch 16Bit.
        ;    Übergaberegister & Ergebniss in Temp1-4 (low-high), Divisor = Y Rest = Z
        ;--------------------------------------------------------------------------------
div:    ldi     temp5,32    ;32* schieben
        clr     zl          ;Rest =0
        clr     zh
dv1:    lsl     temp1       ;Ein Bit aus dem Dividend ...  
        rol     temp2       ;ins Carry schieben
        rol     temp3
        rol     temp4
        rol     zl          ;Carry in den Rest schieben
        rol     zh
        brcs    dv3         ;Bei Überlauf von Rest, Sprung
        cp      zl,yl       ;Solange Bits in temp1 (Rest) schieben bis >= divisor
        cpc     zh,yh
        brcs    dv2         ;Bei Rest<Divisor, die eingeschobene 0 in temp1 stehen lassen
dv3:    inc     temp1       ;Ansonsten Bit0 setzen und den Divisor vom Rest abziehen
        sub     zl,yl
        sbc     zh,yh
dv2:    dec     temp5       ;Das ganze 32 mal
        brne    dv1
        ret
        ;-------------------------------------------------
        ;        Ende Div Routine.
        ;-------------------------------------------------

Rechnen mit Festkomma

Das ganze beruht darauf, dass man z.B. Euro auf dem Display ausgibt, aber mit Cent rechnet und ein Dezimalpunkt in die Ausgaberoutine hineinschummelt.

Typisches Beispiel

Wir lesen mit dem AD-Wandler einen Wert aus, den wir in Volt ausgeben möchten. Damit man das ohne Hardware durchspielen kann, soll unser einfaches (theoretisches) Voltmeter von 0 - 5 Volt mit 2 Nachkommastellen anzeigen. Der interne AD-Wandler hat eine Auflösung von 10 Bit. Wenn wir vref auf 5 Volt einstellen, können wir am ADC Eingang zwischen 0 und 5V messen, die der ADC als Werte zwischen 0..1023 liefert. Das sind 1024 Werte in Schritten von (5/1024 =) 0,0048828125 Volt. Der ADC-Wert muss also mit 0,0048828125 multipliziert werden um die momentane Spannung zu berechnen. Da wir 2 Stellen hinter dem Komma anzeigen wollen wird zuerst mit 100 multipliziert.
0,0048828125 * 100 = 0,48828125. Mit dieser Zahl, kann unser AVR noch nichts anfangen,wir multiplizieren mit 256.
0,48828125 * 256 = 125. Prima, so glatt geht das selten aus. Wenn wir also jetzt den ausgelesenen ADC-Wert mit 125 multiplizieren und im 24 Bit Produkt das niederste Byte verwerfen, erhalten wir die Spannung*100 als 16 Bit Zahl. Es muss bei der Ausgabe nur noch ein Komma vor die beiden letzten Ziffern gesetzt werden und wir erhalten eine Anzeige in Volt mit 2 Nachkommastellen. Diese wollen wir noch runden. Wenn im Niederwertigsten (zu verwerfenden) Byte, Bit 7 gesetzt ist soll Auf-, ansonsten Abgerundet werden. Das geht am einfachsten wenn man zum Ergebnis 128 addiert.
Anschließend soll die Errechnete Spannung im SRam als String abgelegt werden um ihn z.B. auf einem LCD auszugeben oder über die Serielle Schnittstelle zu senden. Ganz oben im ersten Beispiel, wird gezeigt wie eine 16Bit Zahl durch Divison von 1000,100,10 und 1 in einzelne Ziffern zerlegt und so zur Ausgabe vorbereitet wird. In Beispiel unten, möchte ich eine noch andere Möglichkeit Aufzeigen. Man kann den String auch bilden, indem die Zahl immer wieder durch 10 dividiert wird und den Rest von hinten her in den String schreibt. Beispiel: die Zahl ist 345.
345/10=34 Rest 5 (5 in den String). 34/10=3 Rest 4 (4 in den String) .3/10 = 0 Rest 3. Die letzte Division könnte man also auch sparen, wenn man den Dividend der letzten Division (3/10) in den String schreibt.

#include "m48def.inc"
.def    temp1   =   r16
.def    temp2   =   r17
.def    temp3   =   r18
.def    temp4   =   r19
.def    null    =   r2

.equ adcwert    = 1020   ;Testwert

        ;--------------------------------------------------
        ;Stack, Ports, ADC usw.initialisieren
        ;--------------------------------------------------
init:   nop
        clr     null                ;Wird zum addieren des Carryflags gebraucht
        ;--------------------------------------------------
        ;Hauptprogramm
        ;--------------------------------------------------
main:   rcall   r_adc               ;ADC Auslesen
        rcall   calc                ;In Volt umrechnen
        rcall   string              ;Zur Ausgabe vorbereiten
        rjmp    main
        ;--------------------------------------------------
        ;Dieser Teil soll die ADC-Ausleseroutine Simulieren
        ;--------------------------------------------------
r_adc:  ldi     xl,low(adcwert)     ;Testwert laden
        ldi     xh,high(adcwert)
        ret
        ;--------------------------------------------------------------
        ;In Volt mit 2 Nakommastellen Umrechnen (mul Version)
        ;Dazu mit 125 multiplizieren, runden und letztes Byte verwerfen
        ;--------------------------------------------------------------
/*
calc:   ldi     temp1,125           ;Multiplikator laden
        mul     xh,temp1            ;Highbyte multiplizieren
        movw    temp3:temp4,r0:r1   ;Um 1 Byte Verschoben (*256) ins Produkt
        mul     xl,temp1            ;Lowbyte Multiplizieren
        mov     temp2,r0            ;Lowbyte direkt zum Produkt
        add     temp3,r1            ;Highbyte addieren
        adc     temp4,null
        ;------
        ;Runden
        ;------
        ldi     temp1,128           ;0b10000000 addieren um einen
        add     temp2,temp1         ;Übertrag bei gesetztem Bit 7
        adc     temp3,null          ;zu bewirken (aufrunden)
        adc     temp4,null
        ret
*/
        ;-----------------------------------------------------------------
        ;In Volt mit 2 Nakommastellen Umrechnen (Schiebeversion)
        ;Dazu mit 125 multiplizieren, runden und letztes Byte verwerfen
        ;-----------------------------------------------------------------
calc:   clr     temp2
        movw    temp3:temp4,x       ;temp2..4 enthält jetzt den ADCWert *256
        lsl     xl
        rol     xh                  ;ADC *2
        add     xl,temp3
        adc     xh,temp4            ;ADC *3
        lsr     temp4               ;*256 /2 = *128
        ror     temp3
        ror     temp2
        sub     temp2,xl            ;*128 - *3 = *125
        sbc     temp3,xh
        sbci    temp4,0            
        ;------
        ;Runden
        ;------
        subi    temp2,128           ;0b10000000 addieren um einen
        sbci    temp3,-1            ;Übertrag bei gesetztem Bit 7
        sbci    temp4,-1            ;zu bewirken (aufrunden)
        ret     
        ;--------------------------------------------------------------------
        ;    Einen String aus der berechneten Zahl bilden und im SRam ablegen
        ;--------------------------------------------------------------------
string: ldi     yl,low(sram_start+5)    ;Endadresse des Strings nach Y
        ldi     yh,high(sram_start+5)   ;String von hinten aufbauen
        clr     temp1                   ;String mit 0 Abschliessen
        st      -y,temp1
        rcall   div                     ;Ziffer für Hundertstel in den String
        rcall   div                     ;Zehntel
        ldi     temp1,','
        st      -y,temp1                ;Komma
        rcall   div                     ;Einer
        ret
        ;------------------------------------------------------------------------
        ;    16Bit Division durch 8Bit Konstante. In diesem Fall 10.
        ;    Übergaberegister & Ergebniss in temp3/temp4 (low/high), Rest = temp2
        ;------------------------------------------------------------------------
div:    ldi     temp1,16    ;16* schieben
        clr     temp2       ;Rest =0
dv1:    lsl     temp3       ;Ein Bit aus dem Dividend ...  
        rol     temp4       ;ins Carry schieben
        rol     temp2       ;Carry in den Rest schieben
;        brcs    dv3         ;Bei Überlauf von Rest, Sprung
        cpi     temp2,10    ;Solange Bits in temp1 (Rest) schieben bis >= 10
        brcs    dv2         ;Wenn der Rest <10 ist, das 0-Bit in temp2 (LSL, oben) stehen lassen
dv3:    inc     temp3       ;Ansonsten Bit0 setzen und 10 vom Rest abziehen
        subi    temp2,10  
dv2:    dec     temp1       ;Das ganze 16 mal
        brne    dv1
        ;-------------------------------------------------
        ;        Ende Div Routine.
        ;-------------------------------------------------
        subi     temp2,-'0' ;ASCii Code von "0" zum Rest addieren
        st       -y,temp2   ;ASCii Code der Ziffer in den String
        ret

Da es ja hier nur um die Berechnung gehen soll, wird der ADC nur Simuliert. Der Wert der berechnet werden soll, kann man oben bei adcwert eintragen. Das Unterprogramm calc, welches den ADC-Wert mit 125 multipliziert,  ist 2 mal aufgeführt. Die Auskommentierte Variante nutzt mit dem mul Befehl den Hardwaremultiplizierer. Die Schiebe Variante soll zeigen, das wir hier auch ohne den mul Befehl auskommen würden. Diese Variante braucht zwar ein paar Befehle mehr, ist aber in 12 Zyklen durch und somit immer noch (Sau)schnell.
Auch beim runden (addieren der 128) sind 2 Varianten vorhanden. Die herkömmliche Art, braucht einen Befehl mehr und das zusätzliche Register Null, welches natürlich auch mit 0 initialisiert sein muss, da AVRs keine immediate Additionsbefehle zulassen.
Wie schon oben erwähnt, wird hier der String von hinten aufgebaut. Da wir wissen, das wir 3 Ziffern ein Komma und die Null zum abschließen des Strings haben, wird zum Y Zeiger 5 addiert. Achtung, bei st -y,temp1, wird vor dem speichern von temp1  der Zeiger dekrementiert, sodass der String genau an sram_start beginnt.
Die Divisionsroutine ist schon unter Dividieren in Software beschrieben. Am Ende der Division wird die Ziffer vom Rest noch durch addieren des ASCii Zeichen "0" in ASCii gewandelt und in den String geschoben. Das könnte man bei grösseren Zahlen genau so fortführen. Führende Nullen könnte man vermeiden, indem man vor dem Aufrufen von rcall div den Dividend (temp2, temp3) auf Null prüft und gegebenenfalls ein Leerzeichen einfügt.