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
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