Post

ENG | Raspberry Pi Pico, MicroPython, SHT40 sensor

Exploring I2C Communication with Raspberry Pi Pico and MicroPython: A Practical Guide to Sensor Interfacing.

ENG | Raspberry Pi Pico, MicroPython, SHT40 sensor

[Raspberry Pi Pico and MicroPython] In spring I briefly tried Raspberry Pi Pico and very basics, for example how to setup project and use SPI display.

One of the standout features of the Raspberry Pi Pico is its ability to write and test code directly in the Python shell without the need to reupload the program. This makes it an excellent choice for rapid prototyping and development - all you need is to establish serial communication!

The article effectively demonstrates:

  • How to initialize I2C communication
  • How to scan for I2C devices
  • How to interact with a specific sensor (SHT40)
    • Practical code examples with incremental complexity
    • Some advanced techniques like bit manipulation
    • SHT40 sensor decontamination

I2C Communication

I2C scan example

To begin, let’s scan the I2C bus to detect connected devices. Here’s a simple example:

1
2
3
4
>>> from machine import Pin, I2C
>>> i2c = I2C(0, sda=Pin(0), scl=Pin(1))
>>> i2c.scan()
[68, 118]

This code initializes the I2C interface and scans for connected devices, returning their addresses.

SHT40 temperature and humidity sensor

Next, let’s communicate with an SHT40 high-precision temperature and humidity sensor. We’ll use a MicroPython script based on a GitHub example to read data from the sensor.

This unintentionally grown from the intended scope (i2c demonstration) to quite complex article within article.

Trying code from GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> import struct
>>> buffer=bytearray(6)
>>> buffer[0]= 0xfd
>>> i2c.writeto(68, buffer)
>>> buffer=i2c.readfrom(68, 6)
2
>>> buffer
bytearray(b'a\xae\xc3\x81\xed\xdb')
>>> temp_data = buffer[0:2]
>>> temperature = struct.unpack_from(">H", temp_data)[0]
>>> temperature
25006
>>> temperature = -45.0 + 175.0 * temperature / 65535.0
>>> temperature
21.77425
>>> humidity_data = buffer[3:5]
>>> humidity = struct.unpack_from(">H", humidity_data)[0]
>>> humidity
33261
>>> humidity = -6.0 + 125.0 * humidity / 65535.0
>>> humidity
57.44129

Simplifying the code

We can simplify the code using bit manipulation techniques from the official C++ code. We will get temperature and humidity in 1/1000 fixed point units and we may also remove struct library and simplify I2C communication.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> i2c.writeto(68, bytes([0xfd]))
1
>>> buffer=i2c.readfrom(68, 6)
>>> buffer
b'a\x04\xe4\x848]'
>>> temperature = (buffer[0] << 8) + buffer[1]
>>> temperature
25006
>>> temperature = ((21875 * temperature) >> 13) - 45000
>>> temperature
21773
>>> humidity=(buffer[3]<<8) + buffer[4]
>>> humidity
33261
>>> humidity = ((15625 * humidity) >> 13) - 6000
>>> humidity
57440

This says we have 21.77C and 57.44% relative humidity (I guess reported humidity is suspiciously high, more or that later).

Side note: CRC Verification

To ensure data integrity, we ChatGPT can implement a CRC-8 checksum function based on the sensor’s datasheet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def crc8(data, poly=0x31, init=0xFF, xor_out=0x00):
    """
    Calculate CRC-8 checksum for a 16-bit message.

    Args:
        data: The input 16-bit message as an integer (e.g., 0xBEEF).
        poly: The CRC polynomial (default: 0x31).
        init: The initialization value (default: 0xFF).
        xor_out: The final XOR value (default: 0x00).

    Returns:
        The 8-bit CRC value as an integer.
    """
    crc = init
    # Process 16 bits of the input data
    for byte in [(data >> 8) & 0xFF, data & 0xFF]:  # Split into MSB and LSB
        crc ^= byte  # XOR with input byte
        for _ in range(8):  # Process each bit
            if crc & 0x80:  # If the MSB is set
                crc = (crc << 1) ^ poly  # Shift and XOR with polynomial
            else:
                crc <<= 1  # Just shift
            crc &= 0xFF  # Ensure 8-bit result

    return crc ^ xor_out

# Example usage
message = 0xBEEF
result = crc8(message)
print(f"CRC(0x{message:04X}) = 0x{result:02X}")

Let’s try it:

1
2
3
4
5
6
7
8
9
CRC(0xBEEF) = 0x92
>>> crc8(25006)
195
>>> 0xc3
195
>>> crc8(33261)
219
>>> 0xdb
219

It indeed correspond to 3rd and 6th byte of the buffer.

Complete Function to Read Temperature and Humidity

1
2
3
4
5
6
7
8
9
10
11
12
13
import time

def read_temp_hum():
    # Request raw temperature and humidity data
    i2c.writeto(0x44, bytes([0xfd]))
    time.sleep_ms(10)
    buffer = i2c.readfrom(0x44, 6)
    # Extract and convert temperature and humidity data
    temperature = (buffer[0] << 8) + buffer[1]
    humidity    = (buffer[3] << 8) + buffer[4]
    temperature = ((21875 * temperature) >> 13) - 45000
    humidity    = ((15625 * humidity) >> 13) - 6000
    return [temperature, humidity]

Demonstrating the Heater Function

The SHT40 includes a unique built-in heater function. We can demonstrate its effect by activating the heater and observing temperature changes:

1
2
3
4
5
6
7
8
9
10
11
12
>>> print(read_temp_hum())
[22835, 54306]
>>> i2c.writeto(68, bytes([0x2f]))
1
>>> i2c.writeto(68, bytes([0x2f]))
1
>>> i2c.writeto(68, bytes([0x2f]))
1
>>> i2c.writeto(68, bytes([0x2f]))
1
>>> print(read_temp_hum())
[35952, 33590]

Complete MicroPython code

After trying program in console (where pasting function can be inconvenient), complete code in IDE can look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from machine import Pin, I2C
import time

def sht40_read(i2c):
    # Request raw temperature and humidity data
    i2c.writeto(0x44, bytes([0xfd]))
    time.sleep_ms(10)
    buffer = i2c.readfrom(0x44, 6)
    # Extract and convert temperature and humidity data
    temperature = (buffer[0] << 8) + buffer[1]
    humidity    = (buffer[3] << 8) + buffer[4]
    temperature = ((21875 * temperature) >> 13) - 45000
    humidity    = ((15625 * humidity) >> 13) - 6000
    return [temperature, humidity]

i2c = I2C(0, sda=Pin(0), scl=Pin(1))
sht40_temp, sht40_rh = sht40_read(i2c)
print(f"SHT40 temperature: {sht40_temp*.001:.2f}, humidity: {sht40_rh * .001:.2f}")

With example output

1
SHT40 temperature: 21.41, humidity: 56.15

Burn-in / decontamination program

Now let’s troubleshoot the problem with relative huminity measurement.

There’s application note about decontamination sensor from volatile organic compounds. It states that something like acetone vapors can severely affect sensor precision and it demonstrates decontamination process by heating sensor to 110C for 80 minutes. Then it states that time and success may vary. I guess it’s worth trying, otherwise sensor is basically useless.

This is the program I used for burn-in to hopefully evaporate contamination of the sensor and made humidity measurements more accurate. I was able to reach only 98-102C with 22C ambient temperature depending on a draft and I was able to raise temperature to 115C using 3D printer bed and a paper box to raise ambient temperature to 40C.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from machine import Pin, I2C
import time

def sht40_command(i2c, command):
    # Request raw temperature and humidity data
    time.sleep_ms(10)
    i2c.writeto(0x44, bytes([command]))
    time.sleep_ms(1200)
    buffer = i2c.readfrom(0x44, 6)
    # Extract and convert temperature and humidity data
    temperature = (buffer[0] << 8) + buffer[1]
    humidity    = (buffer[3] << 8) + buffer[4]
    temperature = ((21875 * temperature) >> 13) - 45000
    humidity    = ((15625 * humidity) >> 13) - 6000
    return [temperature, humidity]


i2c = I2C(0, sda=Pin(0), scl=Pin(1))
sht40_temp, sht40_rh = sht40_command(i2c, 0xfd)
i = 0
while i < 3600:
    command = 0xfd     # Read sensor data
    if sht40_temp < 120*1000:
        command = 0x2f # 110mW, 1s heating pulse plus read
    if sht40_temp < 115*1000:
        command = 0x39 # 200mW, 1s --------- "" ----------
    sht40_temp, sht40_rh = sht40_command(i2c, command)
    print(f"{i:04d}, \"0x{command:02x}\", {sht40_temp*.001:.2f}, {sht40_rh * .001:.2f}")
    i += 1

And after roughly one hour of heating to 100-120C - when huminity at 100C dropped from 30% to 17%:

1
SHT40 temperature: 21.04, humidity: 46.40

And after another burn-in cycle it went to 8.5% at 115C, which is still not ideal:

1
SHT40 temperature: 21.90, humidity: 41.35

And after two other cycles to 4.7% at 115C:

1
SHT40 temperature: 21.50, humidity: 40.05

Still some change, although rather small.

BMP280 sensor

Here are just some code fragments.

Read chip id.

1
2
3
4
5
6
7
>>> i2c.writeto(118,bytes([0xd0]))
1
>>> a=i2c.readfrom(118,1)
>>> a
b'X'
>>> hex(a[0])
0x58

Read calibration data.

1
2
3
4
5
6
>>> i2c.writeto(118,bytes([0x88]))
1
>>> bytearr=i2c.readfrom(118,26)
>>> hex_string = ' '.join(f'{byte:02x}' for byte in bytearr)
>>> print(hex_string)
5e 6d 07 68 18 fc 01 93 f5 d5 d0 0b b2 08 be 00 f9 ff 8c 3c f8 c6 70 17 00 00

Read measurement (it’s not valid without setting of some control registers, last two bytes are valid only for BME280, not BMP280)

1
2
3
4
5
6
>>> i2c.writeto(118,bytes([0xF7]))
1
>>> bytearr=i2c.readfrom(118,8)
>>> hex_string = ' '.join(f'{byte:02x}' for byte in bytearr)
>>> print(hex_string)
80 00 00 80 00 00 8c 70

More code is on my github

Conclusion

The Raspberry Pi Pico, combined with MicroPython, offers a powerful and flexible platform for rapid prototyping and sensor interfacing. The ability to write and test code directly in the Python shell makes it an excellent choice for developers looking to quickly iterate and test their ideas.

Changelog

  • 2024-12-04: Initial version
  • 2024-12-08: SHT40 sensor decontamination
  • 2024-12-19: Some BMP280 code

References

General MicroPython reference

SHT40 Sensor

This post is licensed under CC BY 4.0 by the author.