|
1 |
| -# Adafruit_MicroPython_Register |
2 |
| -Python data descriptor classes to represent hardware registers on I2C and SPI devices. |
| 1 | +# Adafruit's Register library |
| 2 | +This library provides a variety of data descriptor class for Adafruit's |
| 3 | +MicroPython that makes it really simple to write a device driver for an I2C or |
| 4 | +SPI register based device. Data descriptors act like basic attributes from the |
| 5 | +outside which makes using them really easy. |
| 6 | + |
| 7 | +## Creating a driver |
| 8 | +Creating a driver with the register library is really easy. First, import the register modules you need from the [available modules](adafruit_register/index.html): |
| 9 | + |
| 10 | + from adafruit_register import i2c_bit |
| 11 | + |
| 12 | +Next, define where the bit is located in the device's memory map: |
| 13 | + |
| 14 | + class HelloWorldDevice: |
| 15 | + """Device with two bits to control when the words 'hello' and 'world' are lit.""" |
| 16 | + |
| 17 | + hello = i2c_bit.RWBit(0x0, 0x0) |
| 18 | + """Bit to indicate if hello is lit.""" |
| 19 | + |
| 20 | + world = i2c_bit.RWBit(0x1, 0x0) |
| 21 | + """Bit to indicate if world is lit.""" |
| 22 | + |
| 23 | +Lastly, we need to add two instance members `i2c` and `device_address` so that |
| 24 | +the register classes know how to talk to the device. Make sure their names are |
| 25 | +exact, otherwise the registers will not be able to find them. Also, make sure |
| 26 | +that the i2c device implements the `nativeio.I2C` interface. |
| 27 | + |
| 28 | + def __init__(self, i2c, device_address=0x0): |
| 29 | + self.i2c = i2c |
| 30 | + self.device_address = device_address |
| 31 | + |
| 32 | +Thats it! Now we have a class we can use to talk to those registers: |
| 33 | + |
| 34 | + import nativeio |
| 35 | + from board import * |
| 36 | + |
| 37 | + with nativeio.I2C(SCL, SDA) as i2c: |
| 38 | + device = HelloWorldDevice(i2c) |
| 39 | + device.hello = True |
| 40 | + device.world = True |
| 41 | + |
| 42 | +## Adding register types |
| 43 | +Adding a new register type is a little more complicated because you need to be careful and minimize the amount of memory the class will take. If you don't, then a driver with five registers of your type could take up five times more extra memory. |
| 44 | + |
| 45 | +First, determine whether the new register class should go in an existing module or not. When in doubt choose a new module. The more finer grained the modules are, the fewer extra classes a driver needs to load in. |
| 46 | + |
| 47 | +Here is the start of the `RWBit` class: |
| 48 | + |
| 49 | + class RWBit: |
| 50 | + """ |
| 51 | + Single bit register that is readable and writeable. |
| 52 | + |
| 53 | + Values are `bool` |
| 54 | + |
| 55 | + :param int register_address: The register address to read the bit from |
| 56 | + :param type bit: The bit index within the byte at ``register_address`` |
| 57 | + """ |
| 58 | + def __init__(self, register_address, bit): |
| 59 | + self.bit_mask = 1 << bit |
| 60 | + self.buffer = bytearray(2) |
| 61 | + self.buffer[0] = register_address |
| 62 | + |
| 63 | +The first thing done is writing an RST formatted class comment that explains the |
| 64 | +functionality of the register class and any requirements of the register layout. |
| 65 | +It also documents the parameters passed into the constructor (`__init__`) which |
| 66 | +configure the register location in the device map. It does not include the |
| 67 | +device address or the i2c object because its shared on the device class instance |
| 68 | +instead. That way if you have multiple of the same device on the same bus, the |
| 69 | +register classes will be shared. |
| 70 | + |
| 71 | +In `__init__` we only use two member variable because each costs 8 bytes of |
| 72 | +memory plus the memory for the value. And remember this gets multiplied by the |
| 73 | +number of registers of this type in a device! Thats why we pack both the |
| 74 | +register address and data byte into one bytearray. We could use two byte arrays |
| 75 | +of size one but each MicroPython object is 16 bytes minimum due to the garbage |
| 76 | +collector. So, by sharing a byte array we keep it to the 16 byte minimum instead |
| 77 | +of 32 bytes. `memoryview`s also cost 16 bytes minimum so we avoid them too. |
| 78 | + |
| 79 | +Another thing we could do is allocate the `bytearray` only when we need it. This |
| 80 | +has the advantage of taking less memory up front but the cost of allocating it |
| 81 | +every access and risking it failing. If you want to add a version of `Foo` that |
| 82 | +lazily allocates the underlying buffer call it `FooLazy`. |
| 83 | + |
| 84 | +Ok, onward. To make a [data descriptor](https://docs.python.org/3/howto/descriptor.html) |
| 85 | +we must implement `__get__` and `__set__`. |
| 86 | + |
| 87 | + def __get__(self, obj, objtype=None): |
| 88 | + obj.i2c.writeto(obj.device_address, self.buffer, end=1, stop=False) |
| 89 | + obj.i2c.readfrom_into(obj.device_address, self.buffer, start=1) |
| 90 | + return bool(self.buffer[1] & self.bit_mask) |
| 91 | + |
| 92 | + def __set__(self, obj, value): |
| 93 | + obj.i2c.writeto(obj.device_address, self.buffer, end=1, stop=False) |
| 94 | + obj.i2c.readfrom_into(obj.device_address, self.buffer, start=1) |
| 95 | + if value: |
| 96 | + self.buffer[1] |= self.bit_mask |
| 97 | + else: |
| 98 | + self.buffer[1] &= ~self.bit_mask |
| 99 | + obj.i2c.writeto(obj.device_address, self.buffer) |
| 100 | + |
| 101 | +As you can see, we have two places to get state from. First, `self` stores the |
| 102 | +register class members which locate the register within the device memory map. |
| 103 | +Second, `obj` is the device class that uses the register class which must by |
| 104 | +definition provide a `nativeio.I2C` compatible object as ``i2c`` and the 7-bit |
| 105 | +device address as ``device_address``. |
| 106 | + |
| 107 | +Note that we take heavy advantage of the ``start`` and ``end`` parameters to the |
| 108 | +i2c functions to slice the buffer without actually allocating anything extra. |
| 109 | +They function just like ``self.buffer[start:end]`` without the extra allocation. |
| 110 | + |
| 111 | +Thats it! Now you can use your new register class like the example above. Just |
| 112 | +remember to keep the number of members to a minimum because the class may be |
| 113 | +used a bunch of times. |
0 commit comments