2 Tested Solutions – [Errno 121] Remote I/O error

Recently while trying to develop Python drivers for our decibel sensor module, I frequently ran into [Errno 121] Remote I/O error when using the smbus2 library for communicating over the I2C interface. I was using the latest official OS image for my Raspberry Pi Zero W and Python 3 for running the driver and test program.

Errno 121 Remote IO error on Pi Zero W
Errno 121 Remote IO error, Raspberry Pi Zero W

Potential Cause of [Errno 121] Remote IO error (on RPi Zero W)

With some initial research on the topic, I found a bunch of forum posts and articles that say that the error code is caused by “loose connections” on the I2C bus (this is one such thread out of MANY). Some people also suspected the pull-up being inadequate or loose.

In my case however, the decibel sensor was connected with a high quality JST cable and I was absolutely sure that the I2C lines were good and signal integrity was excellent. The obvious reason for that was the fact that a C program was able to communicate very well with the sensor. But the Python3 script based on smbus2 library was failing!

With some more observation and tweaking the code a few times, I noticed another pattern – [Errno 121] only seems to come up when smbus2 block read/write operations are called consecutively with no delay between the calls.

I also found some notes in the smbus2.py source code when investigating. It may not be related in any way to the issue at hand, but this does indicate that there could be driver support issues.

smbus block data not supported raspberry pi
Some interesting notes in the smbus2 library

Let us consider an example here, using the dbmeter library for our decibel sensor modules.
Note that the function dbmeter.set_weighting() calls bus.write_byte_data() to write one byte to the sensor.
dbmeter.set_tavg() calls bus.bus.write_i2c_block_data() to write 2 bytes to the sensor.

import dbmeter
import time

DBM_ADDR = 0x48

dbmeter.set_weighting (DBM_ADDR, dbmeter.WEIGHTING_A)
dbmeter.set_tavg (DBM_ADDR, 250)

The above code has no delay between the two function calls and produces the following output.

dbmeter.set_tavg, I2C: [Errno 121] Remote I/O error

However, as soon as you insert a delay longer than 0.1 second the problem usually resolves. The following code works fine.

import dbmeter
import time

DBM_ADDR = 0x48

dbmeter.set_weighting (DBM_ADDR, dbmeter.WEIGHTING_A)
time.sleep (0.1)
dbmeter.set_tavg (DBM_ADDR, 250)

We know for sure that the electrical state of the I2C bus is fine (because it works with a C program, and looks good on an oscilloscope probing the sensor module).

This leads me to suspect that the python library is probably not waiting for the previous operation to finish completely and the I2C instance is not “freed” by the underlying driver to be used by python again. It is also possible that the underlying I2C driver does not mark the bus as free for a few milliseconds even after the python block read/write operation is complete.

Temporary Solution to Prevent [Errno 121] - Add a Delay!

Based on the above observation, the obvious solution was to add some delay. If you are using a python library which in turn uses the smbus library, adding a delay between function calls is the quickest fix that you can have.

A Reliable Solution - use i2c_rdwr

Based on the fact that C code has no trouble making frequent high speed access to the I2C bus, I stumbled upon a solution that has worked really well for me so far. You can continue using the smbus2 library.
However, instead of using the regular function calls, use the smbus2.i2c_rdwr() function.

We have implemented this approach for our decibel sensor python drivers and it has been working well so far.

# Function to read data from I2C bus using i2c_rdwr
# devaddr: I2C device address
# regaddr: Slave register address (8-bit)
# n: Number of bytes to read and return as array
def i2c_read (devaddr, regaddr, n):
	try:
		wr = smbus2.i2c_msg.write (devaddr, [regaddr])
		rd = smbus2.i2c_msg.read (devaddr, n)
		bus.i2c_rdwr (wr, rd)
		return list(rd)
	except Exception as e:
		print (f"dbmeter.i2c_read, I2C: {e}")
		return None
		
# Function to write data to I2C bus using i2c_rdwr
# devaddr: I2C device address
# regaddr: Slave register address (8-bit)
# regdata: Data array to write
def i2c_write (devaddr, regaddr, regdata):
	try:
		wrdata = [regaddr] + regdata
		wr = smbus2.i2c_msg.write (devaddr, wrdata)
		bus.i2c_rdwr (wr)
		return True
	except Exception as e:
		print (f"dbmeter.i2c_write, I2C: {e}")
		return None

To gain a better understanding of the implementation, you can refer to the official documentation of smbus2 library.
Using i2c_rdwr also has the added benefit of allowing you to write bulk data longer than the 32-byte limit for SMBus.

Change Log

  • 2 June 2024
    – Initial release

You may also like

Leave a Comment

16 − four =