Sensors use slave-master protocols for interfacing with microcontrollers and microcomputers. Among many different slave-master protocols, I2C and SPI protocols are the common serial communication protocols widely found in embedded devices. Both protocols let the microcontroller/microcomputer take the master role and allow interfacing of several sensors and embedded blocks on a common bus. This tutorial will discuss the implementation of the I2C protocol in MicroPython and explore how sensors are interfaced with ESP8266 and ESP32 using the I2C bus. Learn more about the I2C protocol before continuing this tutorial.
The machine module of MicroPython is responsible for managing basic hardware features of supported ports. The module includes classes for controlling digital input/output, controlling output signals from external devices, pulse width modulation, analog to digital conversion, controlling ADC peripherals, UART, SPI, I2C, I2S, Timer, RTC, Watchdog timer, and managing SD card. It has the I2C class for managing the I2C bus of the supported ports.
I2C is a two-wire protocol. It uses two lines – a data line (SDA) and a clock line (SCL). MicroPython implements both hardware I2C and software I2C. The hardware I2C utilizes the underlying I2C peripheral and bus of supported ports for serial communication as per I2C protocol. As hardware I2C peripherals are bound to specific pins on a port, the implementation of the hardware I2C could be done on those specific pins only.
The hardware I2C is implemented in the I2C class of the machine module. The I2C class is imported in a MicroPython script using the following statement.
from machine import I2C
After importing the I2C class, it is required to instantiate an I2C object. A constructor function does this. The constructor function has the following prototype.
class machine.I2C(id, *, scl, sda, freq=400000)
The constructor function can take four arguments – id, scl, sda, and freq. The id is the identifier of the particular I2C peripheral. If a port has multiple I2C peripherals, it is important to pass the I2C ID. The id can be an integer, string, or tuple. This depends on the particular port. The scl and sda are the pins used for the I2C clock and I2C data, respectively. If the SCL and SDA pins are changeable in a port, these arguments can be passed to assign pins to available I2C peripherals. If scl and sda are not passed as arguments, default pins are assigned for I2C SCL and SDA respectively. The SDA and SCL pins may be fixed in many ports and cannot be changed. The freq sets the maximum frequency for the I2C clock. Following are some valid examples of the I2C constructor.
i2c = I2C(0)
i2c = I2C(1, scl=Pin(5), sda=Pin(4), freq=400000)
i2c = I2C(scl=Pin(5), sda=Pin(4), freq=100000)
The I2C class includes the following methods for configuring and managing data communication.
I2C.init(): This method is used to initialize the I2C bus. It applies to an I2C object, which is already instantiated with an I2c id. The init() method can take sda, scl, and freq as arguments.
I2C.deinit(): This method turns off the I2C bus. It is only available in the WiPy port.
I2C.scan(): This method scans all I2C addresses between 0x08 and 0x77 and returns a list of addresses that respond. This method is useful in listing connected I2C devices. The devices are recognized by their I2C addresses.
I2C.readfrom(addr, nbytes, stop=True): This method reads nbytes from I2C address addr. A stop condition is generated after the read operation is complete if the stop is true. A call to this method returns a byte-type object, which must be stored in a variable.
I2C.readfrom_into(addr, buf, stop=True): This method reads into the I2C buffer from the I2C address addr. The method returns nothing, but all the bytes available on the I2C bus for reading are saved. The stop condition is created after reading into the buffer if the stop is true.
I2C.writeto(addr, buf, stop=True): This method writes bytes from the buffer to the I2c address addr. It returns the number of acknowledgments received. If no acknowledgment is received after sending a byte, the remaining bytes are not sent. If stop is true, the stop condition is generated after sending the bytes.
I2C.writevto(addr, vector, stop=True): This method sends bytes stored in a vector to the I2C address addr. A vector is a list or tuple of objects with a buffer protocol. Using this method, several objects can be sent to the given I2c address with a single call. The method returns the number of acknowledgments received. The method sends the address once, and then the byte objects are sent sequentially. The remaining bytes and objects are not sent if the acknowledgment is not received after sending a byte. If stop is true, the stop condition is generated after sending the vector.
I2C.start(): This method generates a start condition on the I2C bus. The SDA is pulled LOW in start condition while SCL is pulled HIGH.
I2C.stop(): This method generates a stop condition on the I2C bus. The SDA is pulled HIGH in the stop condition while SCL is pulled LOW.
I2C.readinto(buf, nack=True): This method is used to read bytes from the bus and store them in the buffer. The number of bytes read is equal to the size of the buffer. After all bytes are received, acknowledgment is sent; otherwise, no acknowledgment is sent provided nack is set true.
I2C.write(buf): This method is used to write bytes from the buffer to the I2C bus. The method returns the number of acknowledgments received. It is equal to the number of bytes written to the bus successfully. After sending each byte, acknowledgment is received. If no acknowledgment is received, the remaining bytes are not written.
I2C.readfrom_mem(addr, memaddr, nbytes, *, addrsize=8): This method is used to read bytes from the I2C address addr starting from the memory address memaddr. The addrsize specifies the size of the memory address in bits. The number of bytes read are equal to nbytes. The method returns a byte object.
I2C.readfrom_mem_into(addr, memaddr, buf, *, addrsize=8): This method is used to read bytes into the buffer buf from the I2C address addr starting from the memory address memaddr. The addrsize specifies the size of the memory address in bits. The number of bytes read is equal to the size of the buffer.
I2C.writeto_mem(addr, memaddr, buf, *, addrsize=8): This method is used to write a buffer buf to the I2C address addr starting from the memory address memaddr. The method returns nothing. The addrsize specifies the size of the memory address in bits.
Software I2C in MicroPython
The software I2C is implemented in MicroPython using SoftI2C class. The SoftI2C class is imported in a MicroPython script using the following statement.
from machine import SoftI2C
After importing the SoftI2C class, it is required to instantiate an I2C object. A constructor function does this. The constructor function has the following prototype.
class machine.SoftI2C(scl, sda, freq=400000)
All the methods available in the I2C class, the same are also available in the SoftI2C class as it is. The software I2C is implemented using bit banging. It can be generated on any GPIO that is output capable. It is important to note that software I2C is not that efficient compared to hardware I2C. It is possible to communicate with several I2C devices on the same I2C bus, provided each device connected to the bus has a different I2C address. Therefore, even if only one hardware I2C peripheral is available in a port, it must be utilized for I2C communication. The software I2C must be only a last resort.
I2C in ESP8266
There is a single I2C driver in ESP8266. This driver is implemented in software and is available on all GPIO. Since the software implementation of ESP8266 I2C is internal, the I2C class (written for hardware I2C) of MicroPython is used for managing I2C communication in ESP8266. The default I2C pins in ESP8266 are GPIO4 (SDA) and GPIO5 (SCL).
Following is a valid example of using I2C class for data communication in ESP8266.
from machine import Pin, I2C
i2c = I2C(scl=Pin(5), sda=Pin(4), freq=100000)
Hardware I2C in ESP32
There are two hardware I2C peripherals in ESP32. These peripherals are identified by ids – 0 and 1. The default SDA and SCL pins for I2C0 are GPIO19 and GPIO18, respectively. The default SDA and SCL pins for I2C1 are GPIO26 and GPIO25, respectively. However, any GPIO that is output capable can be used as SDA and SCL lines in ESP32. The following is a valid example of using hardware I2C in ESP32.
from machine import Pin, I2C
i2c = I2C(0)
The following example shows using other than default pins for hardware I2C.
from machine import Pin, I2C
i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400000)
Software I2C in ESP32
The software I2C can be used on any GPIO in ESP32 that is output-capable. Learn more about availability of GPIO in ESP8266 and ESP32. Following is a valid example of using software I2C in ESP32.
from machine import Pin, SoftI2C
i2c = SoftI2C(scl=Pin(5), sda=Pin(4), freq=100000)
buf = bytearray(3)
Interfacing ADXL345 with ESP8266 using I2C bus
Now that you are acquainted with implementing the I2C protocol in MicroPython let us get our hands dirty. Sensors commonly use the I2C and SPI protocols for interfacing with embedded controllers and microcomputers. Let us interface an ADXL345 accelerometer sensor with ESP8266 using MicroPython implementation of the I2C protocol.
- ESP8266/ESP32 x1
- ADXL345 accelerometer sensor x1
- Breadboard x1
- Connecting wires/Jumper wires
- Micro USB cable x1
ADXL345 accelerometer sensor can be interfaced with ESP8266 or ESP32 using I2C or SPI protocols. The typical breakout boards of the ADXL345 accelerometer sensor have a pinout for either both interfaces or only for the I2C bus. A breakout board exposing only the I2C bus for interfacing with ADXL345 is shown in the image below.
Connect the VCC and Ground pins of the ADXL sensor with 3V out and GND of the ESP8266, respectively. Connect the SCL, SDA, and CS pins of the ADXL345 breakout board with pins D0 (GPIO16), D1 (GPIO5), and D2 (GPIO4) of the ESP8266. Note that the default I2C pins on the ESP8266 board are D1 (SCL) and D2 (SDA). However, we can use any output-capable GPIO on ESP8266 for SDA and SCL lines. We have chosen D0 for SCL and D1 for SDA.
Note that you must have uploaded MicroPython firmware to ESP8266 and get uPyCraft IDE ready before continuing this project.
About ADXL345 accelerometer
ADXL345 is a 3-axis MEMS accelerometer sensor. It is a digital inertial sensor and uses a capacitive accelerometer design. It has a user-selectable range of up to +/-16g, a maximum output resolution of 13 bits, a sensitivity of 3.9 mg/LSB, and a maximum output data rate of 3200 Hz. The sensor has both I2C and SPI interfaces to communicate with controllers/computers. ADXL345 measures static acceleration due to gravity as well as dynamic acceleration resulting from motion or shock. It can be used to sense linear acceleration in 3 axes and detect an object’s tilt and free fall. It can detect the presence or lack of relative motion by comparing acceleration values to user-defined thresholds.
ADXL345 has built-in registers that can be read and written to configure the sensor’s settings and read acceleration values. ADXL345 offers four measurement ranges: +/-2g, +/-4g, +/-8g, and +/-16g. The default measurement range is +/-2g, which can sense acceleration up to 19.6 m/s2 in either direction along each axis. The maximum resolutions are 10-bit for +/-2g, 11-bit for +/-4g, 12-bit for +/-8g, and 13-bit for the +/-16g range. The default resolution is 10-bit, which for +/-2g (default) range allows a sensitivity of 3.9mg/LSB. The default data rate is 100 Hz. All these configurations can be changed or set by writing data to built-in registers of ADXL345. A controller/computer can read acceleration by reading values from registers 0x32 to 0x37.
How it works
ADXL345 communicates with the microcontroller over I2C or SPI. The ADXL345 breakout board exposes only I2C lines for data communication with the sensor. ADXL345 has an ALT address pin that can be hardwired to set the I2C address of this digital sensor. If the ALT ADDRESS pin is pulled high in a module, the 7-bit I2C address for the device is 0x1D, followed by the R/W bit. This translates to 0x3A for a write and 0x3B for a read. If the ALT ADDRESS pin is connected to ground, the 7-bit I2C address for the device is 0x53 (followed by the R/W bit). This translates to 0xA6 for a write and 0xA7 for a read. The ALT ADDRESS pin is already pulled high or low in a module. The I2C address of the ADXL345 sensor used in this tutorial is 0x53. This is confirmed by scanning the I2C bus using the i2c.scan() method of the MicroPython I2C class.
The internal registers of ADXL345 need to be read and written for setting configurations (like setting measurement range, data transfer rate, sensitivity, and resolution) and reading acceleration values. A table of these registers is given below.
For communicating with ADXL345, first of it, its configuration parameters are set by writing to registers – DATA_FORMAT (0x31), BW_RATE (0x2C), POWER_CTL (0x2D), INT_ENABLE (0x2E), OFSX (0x1E), OFSY (0x1F), and OSFZ (0x20). After writing to the configuration registers over I2C protocols, the acceleration along the x-axis is obtained by reading from registers 0x32 and 0x33 over the I2C bus. The acceleration along the y-axis is obtained by reading from registers 0x34 and 0x35 over the I2C bus. The acceleration along the z-axis is obtained by reading from registers 0x36 and 0x37 over the I2C bus.
The read acceleration values are 16-bit. The values obtained from pairs of registers are converted to a single 16-bit value and 2’s complement to get the final raw acceleration values. These values are multiplied by a factor of 3.9, corresponding to a resolution of +/-4g, to obtain acceleration values in mg.
The MicroPython code begins with importing Pin and I2C class from the machine module. The time module is imported to provide delay, and the ustruct module is imported for formatting acceleration values to 16-bit.
This is followed by the definition of constants representing the configuration registers of ADXL345. The SCL, SDA, and CS pins for the I2C bus are defined, and the CS pin is set as a digital output. An I2C object is instantiated, setting the D0 (GPIO16) and D1 (GPIO5) as SCL and SDA, respectively, and setting the maximum I2C frequency to 10000 Hz. The I2C bus is scanned by calling i2c.scan() method, and the returned list is stored in a list ‘slv’. With the help of a for loop, the values in list ‘slv’ are compared with the known I2c address of ADXL345. A message showing “ADXL345 is found” is printed on the console if the address is found. The matched address is stored in a variable ‘slvAddr’.
A function ‘writeByte()’ is defined to write bytes to the ADXL345 over the I2C bus. A function ‘readByte()’ is defined to read bytes from the ADXL345 over the I2C bus. The writeByte() function is used to write data to configuration registers – DATA_FORMAT, BW_RATE, INT_ENABLE, OFSX, OFSY, OFSZ, and POWER_CTL.
In an infinite while loop, the values of registers 0x32 and 0x33 are read by calling the user-defined readByte() function. The values are formatted to a single 16-bit number using ustruct.unpack() method. This gives the raw acceleration value along the x-axis. The raw value is multiplied by 3.9 to obtain acceleration in mg.
Similarly, the values of registers 0x34 and 0x35 are obtained for deriving acceleration along the y-axis. The values of registers 0x36 and 0x37 are obtained for deriving acceleration along the z-axis. The loop continues endlessly until the execution of the script is terminated from uPyCraft or Thonny IDE.
You may also like:
Filed Under: Tech Articles