Bitwise operations and characters

Masks with AND, OR and XOR, logical, arithmetic and circular shifts, and characters as codes in ASCII and Unicode.

A7.5Data representationA level30 min

Covers OCR H446 1.4.1 Data Types · AQA 7517 4.5.5.1 Character form of a decimal digit · AQA 7517 4.5.5.2 ASCII and Unicode · Eduqas A500QS 2.3 Data representation and data types

Do this lesson in the simulator

A byte is often not one number but several small fields packed together: a status byte with a flag per bit, a colour with red, green and blue squeezed into 16 bits, a character code whose bits mean something. Bitwise operations work on those bits directly. This lesson uses them to pick bytes apart, then looks at characters, where the same tricks turn a digit character into its value.

AND, OR and XOR on whole bytes

At GCSE you met AND, OR and XOR as gates on single bits. A bitwise operation applies the gate to each pair of bits in the same position. The second operand is called a mask: it chooses which bits are affected.

  • AND (& in Python): a 1 in the mask keeps a bit and a 0 clears it, so AND is for testing or clearing bits. 10110010 AND 00000010 gives 00000010, so bit 1 is set.
  • OR (the vertical bar in Python): a 1 in the mask sets a bit and a 0 leaves it alone, so OR is for setting bits. 10110010 OR 00000001 gives 10110011.
  • XOR (^ in Python): a 1 in the mask flips a bit and a 0 leaves it alone, so XOR is for toggling bits. 10110010 XOR 11110000 gives 01000010.

NOT (~ in Python) flips every bit. In Python ~ works on its unlimited-width integers, so mask the result back to 8 bits: ~x & 0xFF.

10110010 with AND 00000010, OR 00000001 and XOR 11110000, bit by bit, with the bits under a mask 1 picked outAND1011001000000001000000011 keeps, 0 clearsOR1011001000000001101100111 sets, 0 leavesXOR1011001011110000010000101 flips, 0 leavesbytemaskresulta 0 in the mask: AND clears the bit, OR and XOR leave it alone
Each column is worked out on its own. The mask's 1s (green) mark the bits the operation acts on: AND keeps them and clears the rest, which is how it tests bit 1; OR sets them; XOR flips them.
status = 0b10110010
BUMPED = 1 << 1           # a mask with only bit 1 set: 00000010
LOW_BATTERY = 1 << 0      # 00000001

print(format(status & BUMPED, "08b"), "bumped" if status & BUMPED else "clear")
status = status | LOW_BATTERY        # set bit 0
status = status & ~BUMPED & 0xFF     # clear bit 1
status = status ^ 0b11110000         # flip the top four bits
print(format(status, "08b"))

Run this in the simulator

Shifts

A shift moves every bit left or right.

  • Logical shift left by n: bits move left, 0s come in on the right, and bits pushed off the left are lost. For unsigned values it multiplies by 2ⁿ, if nothing is lost.
  • Logical shift right by n: bits move right, 0s come in on the left. For unsigned values it divides by 2ⁿ, throwing away the remainder.
  • Arithmetic shift right: the sign bit is copied in on the left instead of a 0, so a two's complement number keeps its sign. 11110100 (-12) shifted right once is 11111010 (-6).
  • Circular shift (rotate): the bits pushed off one end come back in at the other. 10110010 rotated left once is 01100101.
Logical shift right 2 of 10110010, arithmetic shift right 1 of 11110100, and circular shift left 1 of 10110010, with arrows from each bit to where it landslogical shift right 210110010lost17844 = 178 ÷ 40s come in00101100arithmetic shift right 111110100lost-12-6sign bit copied in11111010circular shift left 110110010the top bit comesround to the bottom01100101
A logical shift fills with 0s. An arithmetic shift right copies the sign bit, so -12 halves to -6 and stays negative. A circular shift loses nothing: the bit pushed off one end comes back at the other.
Start Operation Result
10110010 logical shift right 2 00101100
10110010 logical shift left 1 (8 bits) 01100100
11110100 (-12) arithmetic shift right 1 11111010 (-6)
10110010 circular shift left 1 01100101

In Python << and >> shift; >> on a negative integer is arithmetic. Python integers have no fixed width, so mask with & 0xFF to keep a result to 8 bits.

Shifts and masks together extract a field: shift right until the field is at the bottom, then AND with a mask as wide as the field. The robot's LED takes 8 bits per colour, but small displays often store a colour as RGB565: 5 bits red, 6 bits green, 5 bits blue in one 16-bit value.

bit  15 14 13 12 11 | 10  9  8  7  6  5 |  4  3  2  1  0
     r  r  r  r  r  |  g  g  g  g  g  g |  b  b  b  b  b

Red is (colour >> 11) & 0b11111. To widen a 5-bit field to 8 bits, shift it left 3 and fill the three new low bits with its own top three bits, so full brightness 11111 becomes 11111111 rather than 11111000.

Characters

A character is stored as a number, its character code, and a character set is the agreed table of codes.

ASCII uses 7 bits: 128 codes, enough for English letters, digits, punctuation and control codes such as newline. Extended 8-bit versions add another 128 characters, but different countries filled those differently, so text moved between systems came out garbled.

Unicode gives every character in every writing system its own code point, more than a million possible. Its first 128 code points are the same as ASCII, so ASCII text is valid Unicode. Code points are stored with an encoding: UTF-8 uses 1 to 4 bytes per character (1 byte for ASCII), UTF-16 uses 2 or 4, and UTF-32 always uses 4.

for ch in ["A", "é", "€", "🤖"]:
    print(ch, ord(ch), len(ch.encode("utf-8")), "bytes in UTF-8")

Run this in the simulator

ASCII was arranged so that bit patterns help. Capital A is 65 (1000001) and small a is 97 (1100001): they differ only in the bit worth 32, so XOR with 32 swaps the case of a letter.

A is 1000001 and a is 1100001: they differ only in the bit worth 32, so XOR with 32 swaps the case6432168421A651000010a971000011A XOR a0000001only this bitis different= 32, the maskA XOR 32 = a, and a XOR 32 = A
Capital and small letters sit exactly 32 apart in ASCII, which is one bit. Flipping that bit with XOR swaps the case of any letter without looking it up.

A digit as a character and as a number

The character 7 and the number 7 are different bit patterns:

Denary 7-bit binary
the number 7 (pure binary) 7 0000111
the character 7 (ASCII code) 55 0110111

The digit characters 0 to 9 are codes 48 to 57, so the character's low four bits are the digit's value. Masking with 00001111 (or subtracting 48) turns the character into the number. That is exactly what int("7") does for you, and why "7" + "1" is "71", not 8: the program is joining codes, not adding values.

The characters 0 to 9 have codes 48 to 57; every one starts 0011 and its last four bits are the digit's value'0'4800110000= 0'1'4900110001= 1'2'5000110010= 2'3'5100110011= 3'4'5200110100= 4'5'5300110101= 5'6'5400110110= 6'7'5500110111= 7'8'5600111000= 8'9'5700111001= 9code0011valuecode0011valueAND with 00001111 keeps the green half: the character becomes the number
The digit characters were placed at 48 to 57 so that the high four bits are always 0011 and the low four bits are the digit in pure binary. Masking off the high half, or subtracting 48, gives the value.
reading = "0451"                   # a distance arriving as text from a serial port
value = 0
for ch in reading:
    value = value * 10 + (ord(ch) & 0x0F)    # the character code's low nibble is the digit
print(value + 1)
print(chr(ord("b") ^ 32))           # swap case with the bit worth 32

Run this in the simulator

Task: unpack a colour

colour holds 0xFD20, a 16-bit RGB565 colour: bits 15 to 11 are red, bits 10 to 5 green, bits 4 to 0 blue. Using only shifts (>>, <<) and masks (&, |):

  1. Extract the fields into r5 (0 to 31), g6 (0 to 63) and b5 (0 to 31) and print r5=<r5> g6=<g6> b5=<b5>.
  2. Widen each to 8 bits (0 to 255): r8 is r5 shifted left 3, ORed with r5 shifted right 2; g8 is g6 shifted left 2, ORed with g6 shifted right 4; b8 is made like r8. Print rgb=<r8>,<g8>,<b8> with no spaces.
  3. Light the LED with led(r8, g8, b8).

Do not use //, %, bin or format, and do not type the answers.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

colour = 0xFD20

r5 = 0
g6 = 0
b5 = 0
print("r5=" + str(r5), "g6=" + str(g6), "b5=" + str(b5))

Challenges

  1. Go the other way: pack (255, 128, 64) into RGB565 and print it in hex.
  2. Write rotate_left(byte, n) for an 8-bit circular shift.
  3. Why does masking with 00001111 not work for turning the character A into a number?