ENG | Raspberry Pi Pico, MicroPython, SHT40 sensor
Exploring I2C Communication with Raspberry Pi Pico and MicroPython: A Practical Guide to Sensor Interfacing.
[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
- SHT40 Product page - application notes, guides, …
- SHT40 Datasheet