Negative Zahl in Bytes prüfen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Dennis89
User
Beiträge: 1706
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo zusammen,

dieses mal geht es bei mir wieder um Bytes.
Ich bekomme zum Beispiel:

Code: Alles auswählen

b'qX\x1b\x00\x00\xff\xff\xff'
`b'q'`ist zur Prüfung, ob es sich um die Nachricht handelt, die ich erwarte. Die 3 `xff` werden immer zum Schluss gesendet und dazwischen befindet sich meine Zahl im little Endian.

Code: Alles auswählen

>> value = b'qX\x1b\x00\x00\xff\xff\xff'
>> int.from_bytes(value[1:5], 'little')
7000
Die Zahl -7000 wird mir so zurück gegeben:

Code: Alles auswählen

b'q\xa8\xe4\xff\xff\xff\xff\xff']/code]

[code="python"]>> value = b'q\xa8\xe4\xff\xff\xff\xff\xff'
>> int.from_bytes(value[1:5], 'little')
4294960296
Wenn ich das richtig verstanden habe, dann beginnt eine negative Zahl mit `xff` und da ich little Endian habe und "alle" negativen Zahlen, die ich getestet habe, mit `xff` enden, bin ich auf dem richtigen Weg(denke ich).
Es wäre doch aber falsch, nur dieses Byte gegen `xff` zu prüfen? Denn dasss ist ja grundsätzlich erst mal valide, weil ich vielleicht die Zahl 4294967295 benötige. Oder sagt man, dass man mit der Anzahl an Bytes nur eine gewisse größe an Zahl darstellen kann und wenn es größer ist, ist es negativ?


Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18376
Registriert: Sonntag 21. Oktober 2012, 17:20

Negative Zahlen werden durch das Zweierkomplement dargestellt. Du mußt Dich also entscheiden, ob Du vorzeichenbehaftete Zahlen hast, oder nicht. Und wenn das feststeht, ist auch eindeutig klar, ob Du eine sehr große Zahl oder eine negative Zahl hast.

Code: Alles auswählen

int.from_bytes(value[1:5], 'little', signed=True)
Benutzeravatar
__blackjack__
User
Beiträge: 14344
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Auf 0xFF prüfen wäre auch deshalb falsch, weil es ja nicht zwingend dieser Wert sein muss, auch bei einer negativen Zahl nicht:

Code: Alles auswählen

In [5]: value = b'q\xa8\xe4\xff\xf0\xff\xff\xff'

In [6]: value[1:5]
Out[6]: b'\xa8\xe4\xff\xf0'

In [7]: int.from_bytes(value[1:5], "little", signed=True)
Out[7]: -251665240
“It is easier to optimize correct code than to correct optimized code.” — Bill Harlan
Benutzeravatar
Dennis89
User
Beiträge: 1706
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die Erklärungen, hat mir sehr weitergeholfen!


Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1706
Registriert: Freitag 11. Dezember 2020, 15:13

Es wird nicht besser bei mir. Hatte das alles nur am PC getestet. MicroPython ist da etwas anders:

https://docs.micropython.org/en/latest/ ... -parameter
Das gilt auch für `from_bytes()`

Habe mich gefragt, ob ich das mit dem Zweierkomplement jetzt selbst schreiben muss, dann habe ich das auf Github entdeckt:
https://github.com/micropython/micropython/issues/15399

Wenn ich das selbst schreiben könnte, dann wäre es vermutlich schon längst implementiert.

Sollte das meine Programmierkenntnisse überschreiten, dann würde ich die Hex-Darstellung an den Server senden, da wandeln und wenn es negativ ist, eine entsprechende Antwort an den ESP senden.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14344
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Zweierkomplement negieren ist einfach. Alle Bits ”kippen” und dann 1 addieren. Für die -7000 aus dem Beispiel:

Code: Alles auswählen

>>> 4294960296 ^ 0xFFFFFFFF
6999

>>> (4294960296 ^ 0xFFFFFFFF) + 1
7000

>>> -((4294960296 ^ 0xFFFFFFFF) + 1)
-7000
Und testen ob das umrechnen nötig ist, also ob man eine negative Zahl hat, ist ein einfacher Test ob das höchstwertige Bit gesetzt ist (negativ) oder nicht (positiv).

Code: Alles auswählen

>>> (4294960296 & 0x80000000) != 0
True
Alles Operationen die MicroPython können sollte.

Code: Alles auswählen

    data = ...
    value = int.from_bytes(data[1:5], "little")
    if value & 0x80000000:
        value = -((value ^ 0xFFFFFFFF) + 1)
    print(value)
Edit: Wenn man Zahlen haben kann die grösser sein können, als das was man mit den hier vier Bytes darstellen kann, dann kann man auch auch das hier machen:

Code: Alles auswählen

    value = int.from_bytes(data[1:5], "little")
    if value >= 0x80000000:
        value -= 0x100000000
“It is easier to optimize correct code than to correct optimized code.” — Bill Harlan
Sirius3
User
Beiträge: 18376
Registriert: Sonntag 21. Oktober 2012, 17:20

Wenn micropython das Argument nicht kennt, dann benutze statt dessen struct:

Code: Alles auswählen

struct.unpack_from("<i", value, 1)[0]
Benutzeravatar
Dennis89
User
Beiträge: 1706
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die Antworten.

Mir fällt es schwer die Logik zu verstehen.
Ich kippe die Bits, also aus allen 1en mache ich 0en und andersrum. Das heißt, dass Bit das mir sagt ob es eine positive oder negative Zahl ist, ist jetzt, im Falle einer negativen Zahl, 0. Daher muss ich 1 addieren um wieder meinen ursprünglichen Wert zu haben. Das ist der Teil, den ich meine, verstanden zu haben.
Ich verstehe nicht wieso das funktioniert. Wieso kann ich immer die Bits kippen und bekomme meine Zahl? Ich verändere dadurch doch komplett alles. Das verwirrt mich gerade ziemlich.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14344
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Das funktioniert weil das so definiert ist. Könnte man auch anders definieren, dann sind die Schritte die Repräsentation einer Zahl zu negieren halt andere. Man könnte auch sagen, dass bei n Bits das höchstwertige Bit das Vorzeichen ist, und die anderen n-1 Bits den Zahlwert ganz normal als Binärzahl darstellen. Oder statt dem Zweierkomplement das Einerkomplement nehmen, also nur alle Bits umdrehen. Dann hätte man für 0 zwei Werte. Eine positive 0 wo kein Bit gesetzt ist und eine negative 0 wo alle Bits gesetzt sind.

Das Zweierkomplement hat die schöne Eigenschaft, dass man beispielsweise bei ganz normaler Bitweise Addition und Subtraktion von n-Bit langen Zahlen wo die Bits an Stellen grösser als n einfach weg fallen, die erwarteten Ergebnisse bekommt, ohne das man sich bei der Operation selbst Gedanken darüber machen muss ob die beteiligten Zahlen vorzeichenlos sein sollen oder nicht. Also wenn n beispielsweise 8 ist, also man ein Byte hat, dann ist ``(x + 255) & 0b1111_1111`` oder ``(x + -1) & 0b1111_1111`` ist als Code und Bitmuster des zweiten Operanden das gleiche, weil das Bitmuster des Ergebnisses das gleiche ist.
“It is easier to optimize correct code than to correct optimized code.” — Bill Harlan
Benutzeravatar
Dennis89
User
Beiträge: 1706
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die Antwort.

Es ging mir jetzt weniger um die Definition, welches Bit die negative Zahl darstellt.
Ich glaube jetzt hat es klick gemacht. Das mit dem Bits kippen funktioniert nur mit einer negativen Zahl. Wenn das höchste Bit gesetzt ist, dann ist das ja deswegen erst mal eine große Zahl, weil ja "ganz vorne" noch die 1 steht und wenn ich mir zum Beispiel 7000 in Binär darstellen lasse, dann werden per default die ganzen führenden 0en gar nicht angezeigt, die aber, bei einer definierten Länge, da sind.
Ich hatte das Verständnisproblem, dass ich dachte man kippt die Bits immer, egal ob positiv oder negativ, aber das ist Quatsch und ich weiß gar nicht wieso ich da drauf gekommen bin.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 14344
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Bei Python's `int` kann man sich bei positiven Zahlen in der Binärdarstellung immer unendlich viele führende 0en vorstellen, und bei negativen Zahlen unendlich viele führende 1en.

Code: Alles auswählen

In [551]: -1 & (1 << 1000) != 0
Out[551]: True

In [552]: 1 & (1 << 1000) != 0
Out[552]: False

In [553]: 1 & (1 << 10000) != 0
Out[553]: False

In [554]: 1 & (1 << 1000000) != 0
Out[554]: False

In [555]: -1 & (1 << 1000) != 0
Out[555]: True

In [556]: -1 & (1 << 10000) != 0
Out[556]: True

In [557]: -1 & (1 << 100000) != 0
Out[557]: True
Es ging mir bei der Beschreibung der Varianten nicht nur das Bit an dem man eine negative Zahl erkennen kann, sondern insgesamt um die Kodierung von negativen Zahlen. Das sind so drei mehr oder weniger naheliegende Möglichkeiten negative Zahlen mit Bits zu kodieren.

Mal als Übersicht wie das mit 4 Bits aussieht. Erste Spalte die „sign extension“ die man machen muss, wenn man mehr als 4 Bit verwendet. Dann die Bits selbst. Der Dezimalwert der vier Bits ohne Vorzeichen, und dann der Wert, der das Bitmuster bei den drei Varianten hätte. Das Programm:

Code: Alles auswählen

#!/usr/bin/env python3


def is_negative(value):
    return bool(value & 0b1000)


def negative_bit(value):
    return (value & 0b0111) * (-1 if is_negative(value) else 1)


def ones_complement(value):
    return -(value ^ 0b1111) if is_negative(value) else value


def twos_complement(value):
    return -((value ^ 0b1111) + 1) if is_negative(value) else value


def main():
    print("sgn.ext bits   u -bit  1's  2's")
    print("===============================")
    for i in range(16):
        print(
            f" ...{'111' if is_negative(i) else '000'}"  # sign extend
            f" {i:04b}"  # bit pattern
            f"  {i:2}"  # unsigned decimal value
            f"  {negative_bit(i):3}"
            f"  {ones_complement(i):3}"
            f"  {twos_complement(i):3}"
        )


if __name__ == "__main__":
    main()
Die ausgegebene Tabelle:

Code: Alles auswählen

sgn.ext bits   u -bit  1's  2's
===============================
 ...000 0000   0    0    0    0
 ...000 0001   1    1    1    1
 ...000 0010   2    2    2    2
 ...000 0011   3    3    3    3
 ...000 0100   4    4    4    4
 ...000 0101   5    5    5    5
 ...000 0110   6    6    6    6
 ...000 0111   7    7    7    7
 ...111 1000   8    0   -7   -8
 ...111 1001   9   -1   -6   -7
 ...111 1010  10   -2   -5   -6
 ...111 1011  11   -3   -4   -5
 ...111 1100  12   -4   -3   -4
 ...111 1101  13   -5   -2   -3
 ...111 1110  14   -6   -1   -2
 ...111 1111  15   -7    0   -1
Der Grund warum negative ganze Zahlen in Digitalrechnern in aller Regel als Zweierkomplement kodiert sind, und nicht beispielsweise als Einerkomplement oder als positive Zahlen mit einem Vorzeichenbit, ist das die simpelste Addiererschaltung sowohl für Zahlen ohne Vorzeichen als auch für welche mit Vorzeichen funktioniert. Für die anderen Darstellungen müsste man sonst entweder in Hard- oder Software zusätzliche Schritte machen und zwischen „signed“ und „unsigned“ Additionen unterscheiden.

Beispiel von einem ARM 16-Bit Assembler-Programm das Bytewerte addiert und die Rechnung in Binärdarstellung ausgibt:

Code: Alles auswählen

        .syntax unified
        .global _start
@
@ Add bytes and print the calculation as binary numbers.
@
@
@ r0-r2 and r7 stay the same throughout the program up until the exit
@ instruction sequence.  r3 is a pointer to the current number pair.
@
        .thumb_func
_start:
        movs    r7, #4                  @ Only system call is write.
        movs    r0, #1                  @ Write to stdout.
        ldr     r1, =character          @ Address of the character to write.
        movs    r2, #1                  @ Write one character.
        adr     r3, numbers             @ Set pointer to first number pair.

loop:
        movs    r4, #' '                @ Print space followed by first number.
        strb    r4, [r1]
        svc     0

        ldrb    r6, [r3, #0]
        bl      print_binary

        movs    r4, #'+'                @ Print '+' followed by second number.
        strb    r4, [r1]
        svc     0

        ldrb    r6, [r3, #1]
        bl      print_binary

        movs    r4, #'='                @ Print '=', add numbers,
        strb    r4, [r1]                @ and print result.
        svc     0
        ldrb    r4, [r3, #0]
        adds    r6, r4
        bl      print_binary

        movs    r4, #10                 @ Print line feed.
        strb    r4, [r1]
        svc     0

        adds    r3, #2                  @ Advance number pair pointer and check
        adr     r6, numbers_end         @ if loop is done.
        cmp     r6, r3
        bne     loop

        movs    r7, #1                  @ System call to exit program
        movs    r0, #0                  @ with exit code 0.
        svc     0
@
@ Print number in r6 as binary number.
@
print_binary:
        movs    r5, #128                @ Init bit mask for current bit = MSB.
1:      tst     r6, r5                  @ Test bit and print '0' or '1'.
        ite     eq
        moveq   r4, #'0'
        movne   r4, #'1'
        strb    r4, [r1]
        svc     0
        lsrs    r5, #1                  @ Shift bit mask to the next bit.
        bne     1b

        movs    r4, #10                 @ Print line feed.
        strb    r4, [r1]
        svc     0

        blx     lr

@ Number pairs to be added.
numbers:
        .byte   3,   5
        .byte  -2,   7
        .byte  12,  -1
        .byte   5, -10
numbers_end:

.bss
character:
        .space 1                        @ Character to be printed.
Ausgabe:

Code: Alles auswählen

 00000011
+00000101
=00001000

 11111110
+00000111
=00000101

 00001100
+11111111
=00001011

 00000101
+11110110
=11111011
Bei den Ergebnissen ist es egal ob man die jetzt als vorzeichenlos oder mit Vorzeichen in Zweierkomplmentdarstellung betrachtet.
“It is easier to optimize correct code than to correct optimized code.” — Bill Harlan
Antworten