Skip to content

Bitwise operations for device registers#

Many devices have registers. These are a small number of binary numbers stored in the device and used to control the device or read its sensors.

Often, several values are stored in one register. This means we have to do some programming to manipulate the separate values stored together in one register. For example a display may have an adjustable brightness and and adjustable contrast. These may be stored together in one register.

The problem arises when we have primitives to read or write an entire register, but we only want to read or write one of the values stored in the register.

For this we use a binary "mask". The mask indicates with '1' or '0' which bits we want to process. Mostly people use '1' to indicate the bits that should be processed, but either works. You just have to know which you are using.

Before we can explain the use of a mask we have to explain the bitwise boolean operations.

The "and" operation#

The boolean bitwise "and" operation is an operation on two binary numbers that gives a result. It is written:

result = left & right

For each position in the binary number, the result is '1' only if both the inputs have '1' in that position. So:

  • 0 & 0 == 0
  • 0 & 1 == 0
  • 1 & 0 == 0
  • 1 & 1 == 1

For a single digit binary number the "&" operation is like multiplication, usually written with "*" on computers.

  • 0 * 0 == 0
  • 0 * 1 == 0
  • 1 * 0 == 0
  • 1 * 1 == 1

We can use the bitwise "and" operation with a mask value as follows. Let's say we want to change bits 3 to 7 in a value (bits are counted from the right, starting with 0). We can make a mask that has ones in positions 3 to 7. Binary numbers are written with 0b in Toit, and we can insert underscores anywhere we want to improve readability.

MASK := 0b000000000_1111_000

This mask value has zeros everywhere except in the area we are interested in, positions 3 to 7. For every mask there is the opposite mask, with all the bits reversed:

ANTI_MASK ::= 0b111111111_0000_111

If we have one mask, we can make the other one with the tilde (~) operator:

ANTI_MASK ::= ~MASK // This does the same.

Now if we want to change the bits 3-7 in a value to be 1010 regardless of what they were before we start by zeroing bits 3-7. This can be done with the anti-mask:

// This can also be written:  value &= ANTI_MASK
value = value & ANTI_MASK
value      101010101_0101_010
antimask   111111111_0000_111
           ------------------  & operation ("and")
new value  101010101_0000_010
                     ^^^^------- the zeroed bits are here.

The masking operation has zeroed the bits from 3-7, while the other bits in the value are untouched.

The "or" operation#

To set the bits to a desired value, we use the "or" operation which is the pendant to the "and" operation, and is written with the vertical pipe sign "|".

  • 0 | 0 == 0
  • 0 | 1 == 1
  • 1 | 0 == 1
  • 1 | 1 == 1

Let's say we want to set bits 3-7 to 1100. We would use an "or" operation on a number with the bits in that place and zeroes in all other places.

value = value | 0b000000000_1100_000
new value  101010101_0000_010
number     000000000_1100_000
           ------------------  | operation ("or")
new2 value 101010101_1100_010
                     ^^^^------ the 1100 has been inserted here.

Putting it all together#

Perhaps we have a device with an 8-bit register that has two fields. The low 4 bits control brightness, and the high 4 control contrast. There are some constants:

BRIGHTNESS_0 ::= 0b0000
BRIGHTNESS_1 ::= 0b0001
BRIGHTNESS_2 ::= 0b0010
BRIGHTNESS_3 ::= 0b0011
BRIGHTNESS_4 ::= 0b0100
BRIGHTNESS_5 ::= 0b0101
BRIGHTNESS_6 ::= 0b0110
BRIGHTNESS_7 ::= 0b0111
BRIGHTNESS_8 ::= 0b1000
BRIGHTNESS_9 ::= 0b1001
BRIGHTNESS_10 ::= 0b1010
BRIGHTNESS_11 ::= 0b1011
BRIGHTNESS_12 ::= 0b1100
BRIGHTNESS_13 ::= 0b1101
BRIGHTNESS_14 ::= 0b1110
BRIGHTNESS_15 ::= 0b1111

CONTRAST_0 ::=  0b0000_0000
CONTRAST_1 ::=  0b0001_0000
CONTRAST_2 ::=  0b0010_0000
CONTRAST_3 ::=  0b0011_0000
CONTRAST_4 ::=  0b0100_0000
CONTRAST_5 ::=  0b0101_0000
CONTRAST_6 ::=  0b0110_0000
CONTRAST_7 ::=  0b0111_0000
CONTRAST_8 ::=  0b1000_0000
CONTRAST_9 ::=  0b1001_0000
CONTRAST_10 ::= 0b1010_0000
CONTRAST_11 ::= 0b1011_0000
CONTRAST_12 ::= 0b1100_0000
CONTRAST_13 ::= 0b1101_0000
CONTRAST_14 ::= 0b1110_0000
CONTRAST_15 ::= 0b1111_0000

We only have the operations read_register and write_register, but we want to adjust the two values independently:

class MyDevice extends OneRegisterDevice:

  // Constructor...

  get_brightness -> int:
    reg := read_register
    return reg & BRIGHTNESS_MASK_

  get_contrast -> int:
    reg := read_register
    return reg & CONTRAST_MASK_

  set_brightness brightness/int -> none:
    reg := read_register
    // Remove the old brightness with the mask.
    reg &= ~BRIGHTNESS_MASK_
    // Insert the new brightness with the "or" operation.
    reg |= brightness
    // Write back the old contrast and the new brightness.
    write_register reg

  set_contrast contrast/int -> none:
    reg := read_register
    // Remove the old brightness with the mask.
    reg &= ~CONTRAST_MASK_
    // Insert the new constrast with the "or" operation.
    reg |= constrast
    // Write back the new contrast and the old brightness.
    write_register reg

  BRIGHTNESS_MASK_ ::= 0b0000_1111
  CONTRAST_MASK_   ::= 0b1111_0000

Using the shift operations#

In the example above, the user of the driver could choose between 16 discrete contrast levels, but perhaps the user wants to do a calculation. In that case they have perhaps calculated number between 0 and 15, and would like to use that. We can do that with the help of the shift operators, "<<" and ">>".

The shift operations move the bits of a binary number around. For example, the ">>" operator moves them to the right.

value :=        0b00101000000
value = value >> 3
// Now value == 0b00000101000
value <<= 4
// Now value == 0b01010000000

Using this operation we could rewrite our MyDevice to take values from 0 to 15 instead of the constants:

class MyDevice extends OneRegisterDevice:
  // Constructors...

  /// Get the current brightness from 0-15.
  get_brightness -> int:
    reg := read_register
    return (reg & BRIGHTNESS_MASK_) >> BRIGHTNESS_SHIFT_

  /// Get the current contrast from 0-15.
  get_contrast -> int:
    reg := read_register
    return (reg & CONTRAST_MASK_) >> CONTRAST_SHIFT_

  /// Set the current brightness from 0-15.
  set_brightness brightness/int -> none:
    assert: 0 <= brightness <= 15
    reg := read_register
    // Remove the old brightness with the mask.
    reg &= ~BRIGHTNESS_MASK_
    // Insert the new brightness with the "or" operation.
    reg |= brightness << BRIGHTNESS_SHIFT_
    // Write back the old contrast and the new brightness.
    write_register reg

  /// Set the current contrast from 0-15.
  set_contrast contrast/int -> none:
    assert: 0 <= contrast <= 15
    reg := read_register
    // Remove the old contrast with the mask.
    reg &= ~CONTRAST_MASK_
    // Insert the new contrast with the "or" operation.
    reg |= contrast << CONTRAST_SHIFT_
    // Write back the new contrast and the old brightness.
    write_register reg

  // The brightness is stored in the low 4 bits, and the
  // contrast is stored in the high 4 bits.
  BRIGHTNESS_MASK_  ::= 0b0000_1111
  CONTRAST_MASK_    ::= 0b1111_0000
  BRIGHTNESS_SHIFT_ ::= 0
  CONTRAST_SHIFT_   ::= 4