TMF8801 reading junk values [SOLVED in post #11]

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
User avatar
ardthusiast
Posts: 25
Joined: Tue Feb 08, 2022 4:13 pm

TMF8801 reading junk values [SOLVED in post #11]

Post by ardthusiast » Sun Apr 24, 2022 9:24 pm

Hello,

I have been doing a project involving three TMF8801 sensors for distance measurement. These sensors use I2C, and since the board I was using had only one I2C port exposed, I was putting one sensor on the hardware I2C bus and the other two on software buses. The software buses seemed to be giving some issues, so I switched to the Teensy 4.0 which has three hardware I2C buses. However, now all three sensors are giving me a distance of 7162mm, which from past testing I know is a junk value (also, this is way outside the advertised maximum range of the sensor, 2500mm). Moving my hand in front of the sensor doesn't change the distance like it normally would, but it instead continues to read 7162. Any ideas what could be going wrong?

My Micropython code for interfacing with the sensor:

Code: Select all

from micropython import const
from time import sleep_ms

from event_handler import EventHandler

# Constants
_DEFAULT_ADDR = const(0x41)
_CPU_TIMEOUT = const(200)
_APP_TIMEOUT = const(200)
_COMMAND_RESULT = const(0x55)
_APP_DIST = const(0xC0)
_APP_BOOT = const(0x80)
_CHIP_ID = const(0x07)
_CALIBRATION = bytearray([0xC1, 0x22, 0x0, 0x1C, 0x9, 0x40, 0x8C, 0x98, 0xA, 0x15, 0xCE, 0x9C, 0x1, 0xFC])
_ALG_STATE = bytearray([0xB1, 0xA9, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
_CMD_DATA_VALUES = bytearray([0x03, 0x23, 0x44, 0x00, 0x00, 0x64, 0xD8, 0xA4])

# Commands
_CMD_SET_ADDR = const(0x49)
_CMD_CAL = const(0x0B)
_CMD_MEASURE = const(0x02)
_CMD_STOP = const(0xFF)
_CMD_DATA0 = const(0x0F)
_CMD_DATA1 = const(0x0E)
_CMD_RAM_REMAP_RST = const(0x11)

# CPU status
_CPU_RESET = const(0x07)
_CPU_READY = const(0x06)

# Registers
_REG_COMMAND = const(0x10)
_REG_DATA = const(0x20)
_REG_ENABLE = const(0xE0)
_REG_ID = const(0xE3)
_REG_APPID = const(0x00)
_REG_APPREQID = const(0x02)
_REG_FACTORY_CAL = const(0x20)
_REG_STATE_DATA = const(0x2E)
_REG_CMD_DATA7 = const(0x08)
_REG_REGISTER_CONTENTS = const(0x1E)
_REG_BL_CMD_STAT = const(0x08)
_REG_BL_SIZE = const(0x09)
_REG_BL_CSUM = const(0x8B)
_REG_STATUS = const(0x0D)


class TMF8801:
    """A class to manage the TOF distance sensor."""

    def __init__(self, i2c: object, close_dist = 4, inches = True, address = _DEFAULT_ADDR, enable = None):
        """Initialize the sensor."""
        self.i2c = i2c
        self.inches = inches
        self.address = _DEFAULT_ADDR
        self.enable = enable

        self.close = EventHandler()
        self.c_trig = False
        self.block = EventHandler()
        self.b_trig = False
        self.path_detected = EventHandler()
        self.p_trig = False
        self.close_dist = close_dist

        if int.from_bytes(self.i2c.readfrom_mem(self.address, _REG_APPID, 1), "big") != 0xC0:
            self.begin()

    @property
    def measurement(self) -> bytearray:
        """Get the result number, result info, and distance."""
        buffer = bytearray(4)
        self.i2c.readfrom_mem_into(self.address, _REG_DATA, buffer)
        return buffer

    def get_distance(self) -> int:
        """Get the distance."""
        distance = self.measurement[3] << 8
        distance += self.measurement[2]

        converted_distance = ((distance / 25.4) if self.inches else distance)

        if converted_distance <= 1 and not self.b_trig:
            self.block()
            self.b_trig = True
        elif converted_distance > 1 and self.b_trig:
            self.block(1)
            self.b_trig = False
        if converted_distance <= self.close_dist and not self.c_trig:
            self.close()
            self.c_trig = True
        elif converted_distance > self.close_dist and self.c_trig:
            self.close(1)
            self.c_trig = False
        if (converted_distance >= 7 or converted_distance == 0) and not self.p_trig:
            self.path_detected()
            self.p_trig = True
        elif (converted_distance < 7 and converted_distance != 0) and self.p_trig:
            self.path_detected(1)
            self.p_trig = False

        return converted_distance

    def cpu_ready(self) -> bool:
        """Make sure the CPU is ready."""
        counter = 0

        if not self.__bit_set(_REG_ENABLE, _CPU_READY):
            counter += 1
            sleep_ms(100)
        else:
            return True

        while counter <= _CPU_TIMEOUT:
            if not self.__bit_set(_REG_ENABLE, _CPU_READY):
                counter += 1
                sleep_ms(100)
            else:
                return True
        else:
            return False

    def app_ready(self, app: int) -> bool:
        """Make sure the measurement app is ready."""
        counter = 0

        if not (int.from_bytes(self.i2c.readfrom_mem(self.address, _REG_APPID, 1), "big") == app):
            counter += 1
            sleep_ms(100)
        else:
            return True

        while counter <= _APP_TIMEOUT:
            if not (int.from_bytes(self.i2c.readfrom_mem(self.address, _REG_APPID, 1), "big") == app):
                counter += 1
                sleep_ms(100)
            else:
                return True
        else:
            return False

    def data_ready(self) -> bool:
        """Check if new data is ready."""
        return int.from_bytes(self.i2c.readfrom_mem(self.address, _REG_REGISTER_CONTENTS, 1), "big") == _COMMAND_RESULT

    def set_address(self, new_address: int):    # FIXME: Does not work.
        """Set the i2c address of the sensor."""
        print(self.i2c.readfrom_mem(self.address, _REG_STATUS, 1))
        self.i2c.writeto_mem(self.address, _REG_APPREQID, bytearray([_APP_BOOT]))
        if not self.app_ready(_APP_BOOT):
            raise RuntimeError("Unable to load bootloader.")

        self.i2c.writeto_mem(self.address, _REG_BL_CMD_STAT, bytearray([_CMD_RAM_REMAP_RST]))
        self.i2c.writeto_mem(self.address, _REG_BL_SIZE, bytearray([0x00]))
        self.i2c.writeto_mem(self.address, _REG_BL_CSUM, bytearray([0xEE]))

        __new_address = new_address << 1
        self.i2c.writeto_mem(self.address, _CMD_DATA1, bytearray([__new_address]))
        self.i2c.writeto_mem(self.address, _CMD_DATA0, bytearray([0x00]))
        self.i2c.writeto_mem(self.address, _REG_COMMAND, bytearray([_CMD_SET_ADDR]))

        print(self.i2c.readfrom_mem(self.address, _REG_STATUS, 1))

        self.i2c.writeto_mem(self.address, _REG_COMMAND, bytearray([_CMD_STOP]))

        print(self.i2c.scan())
        print(self.i2c.readfrom_mem(self.address, _REG_STATUS, 1))
        self.address = new_address

    def wake(self) -> bool:
        """Wake the sensor."""
        self.i2c.writeto_mem(self.address, _REG_ENABLE, bytearray([0x01]))
        result = self.i2c.readfrom_mem(self.address, _REG_ENABLE, 1)
        sleep_ms(100)

        while result != 0x41:
            self.i2c.writeto_mem(self.address, _REG_ENABLE, bytearray([0x01]))
            result = self.i2c.readfrom_mem(self.address, _REG_ENABLE, 1)
            sleep_ms(100)

        return True

    def __bit_set(self, register_address: int, bit_position: int) -> bool:
        """Check if a particular bit is set."""
        value = int.from_bytes(self.i2c.readfrom_mem(self.address, register_address, 1), "big")
        mask = int(1 << bit_position)
        if (value & mask) != 0:
            return True
        else:
            return False

    def is_connected(self):
        """Check if the device is connected."""
        if self.address not in self.i2c.scan():
            return False
        else:
            return True

    def begin(self):
        """Initialize the sensor."""
        if self.address not in self.i2c.scan():
            raise RuntimeError("Failed to get response from device.")

        # Reset the CPU and make sure it is ready
        self.i2c.writeto_mem(self.address, _REG_ENABLE, bytearray([_CPU_RESET]))

        if not self.cpu_ready():
            raise RuntimeError("CPU timed out.")

        # Make sure the device is a TMF8801
        if int.from_bytes(self.i2c.readfrom_mem(self.address, _REG_ID, 1), "big") != _CHIP_ID:
            raise RuntimeError("Wrong chip ID.")

        if self.address not in self.i2c.scan():
            raise RuntimeError("Failed to get response from device.")

        # Load the distance measurement application
        self.i2c.writeto_mem(self.address, _REG_APPREQID, bytearray([_APP_DIST]))
        if not self.app_ready(_APP_DIST):
            raise RuntimeError("Unable to load measurement application.")

        # Set the calibration data
        self.i2c.writeto_mem(self.address, _REG_COMMAND, bytearray([_CMD_CAL]))
        self.i2c.writeto_mem(self.address, _REG_FACTORY_CAL, _CALIBRATION)
        self.i2c.writeto_mem(self.address, _REG_STATE_DATA, _ALG_STATE)
        self.i2c.writeto_mem(self.address, _REG_CMD_DATA7, _CMD_DATA_VALUES)

        # Start the measurement application
        self.i2c.writeto_mem(self.address, _REG_COMMAND, bytearray([_CMD_MEASURE]))

Edit: Using Teensy 4.0. Same code works on RP2040. I'm not using the RP2040 because I need to use three sensors, and I can't change the I2C address. RP2040 has only two hardware I2C buses, and I've had problems with software I2C.
Last edited by ardthusiast on Mon Apr 25, 2022 4:15 pm, edited 2 times in total.

User avatar
Roberthh
Posts: 3668
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: TMF8801 reading junk values

Post by Roberthh » Mon Apr 25, 2022 5:56 am

Do you have the ability to hook up a simple logic analyzer to the wires and check, whether the data in the memory is identical to the data on the wires?

User avatar
ardthusiast
Posts: 25
Joined: Tue Feb 08, 2022 4:13 pm

Re: TMF8801 reading junk values

Post by ardthusiast » Mon Apr 25, 2022 12:24 pm

I do not have one of those.

User avatar
Roberthh
Posts: 3668
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: TMF8801 reading junk values

Post by Roberthh » Mon Apr 25, 2022 2:08 pm

I do not have this sensor, so I tried the Teensy 4.0 I2C with a BME680 device. That works well. Do you use a certain breakout board, and are the pull-up resistors in place?

User avatar
ardthusiast
Posts: 25
Joined: Tue Feb 08, 2022 4:13 pm

Re: TMF8801 reading junk values

Post by ardthusiast » Mon Apr 25, 2022 2:48 pm

I'm using the SparkX board. It does appear to have the pull-up resistors in place.

I don't think the sensor is at fault, since the sensor that was on the RP2040's hardware I2C worked just fine. That same sensor is now not working on the Teensy. I've used the exact same code on both boards.

User avatar
ardthusiast
Posts: 25
Joined: Tue Feb 08, 2022 4:13 pm

Re: TMF8801 reading junk values

Post by ardthusiast » Mon Apr 25, 2022 2:55 pm

Maybe it has something to do with how I'm initializing the I2C buses in my main code? This is how I was doing it for the RP2040:

Code: Select all

i2c = I2C(0, scl = Pin(17), sda = Pin(16))
i2c_soft_0 = SoftI2C(Pin(7), Pin(6))
i2c_soft_1 = SoftI2C(Pin(9), Pin(8))
ir_front = TMF8801(i2c)
ir_right = TMF8801(i2c_soft_0, close_dist = 2)
ir_left = TMF8801(i2c_soft_1, close_dist = 2)
And this is how I'm doing it for the Teensy:

Code: Select all

i2c_0 = I2C(0, freq = 400000)
i2c_1 = I2C(1, freq = 400000)
i2c_2 = I2C(2, freq = 400000)
ir_front = TMF8801(i2c_2)
ir_right = TMF8801(i2c_0, close_dist = 2)
ir_left = TMF8801(i2c_1, close_dist = 2)
I have SDA and SCL wired correctly for all three sensors, otherwise it would throw an exception and I wouldn't get any values.

User avatar
Roberthh
Posts: 3668
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: TMF8801 reading junk values

Post by Roberthh » Mon Apr 25, 2022 3:19 pm

The initialization is fine. Since 400000 is the default frequency, you do not have to specify it., but it's no fault in doing so.
The difference I see between RP2040 and Teensy is, that the RP2040 runs at 250kHz when specified to 400kHz, and the Teensy shows 384kHz when set to 400kHz. So you could try to reduce the frequency for the Teensy to tell, if that makes a difference.
The other difference I see is the duty rate of the SCK signal, which slightly differs.
How long are the wires that you use? I2C is designed for short wires.

User avatar
ardthusiast
Posts: 25
Joined: Tue Feb 08, 2022 4:13 pm

Re: TMF8801 reading junk values

Post by ardthusiast » Mon Apr 25, 2022 3:30 pm

Roberthh wrote:
Mon Apr 25, 2022 3:19 pm
The difference I see between RP2040 and Teensy is, that the RP2040 runs at 250kHz when specified to 400kHz, and the Teensy shows 384kHz when set to 400kHz. So you could try to reduce the frequency for the Teensy to tell, if that makes a difference.
The other difference I see is the duty rate of the SCK signal, which slightly differs.
So, youre saying to try setting the frequency to 100kHz instead of 400kHz or something like that? And if it's the duty rate of the SCK signal that's causing the problem, how might I work around that?
Roberthh wrote:
Mon Apr 25, 2022 3:19 pm
How long are the wires that you use? I2C is designed for short wires.
The longest wire I have is 15cm.

User avatar
Roberthh
Posts: 3668
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: TMF8801 reading junk values

Post by Roberthh » Mon Apr 25, 2022 3:44 pm

The duty rate should not be a problem. And 15cm is short. Yes, try to set the frequency to a lower value, if that makes a difference. According to the data sheet, the sensor should be able to work up to 1MHz clock at 3.3V. So 400kHz should be fine. But that's the only major difference I see. The duty rate of the Teensy cannot be changed. For SoftI2C it is 48% for hard I2C it is 63%. The RP2040 is at 55%.
Does i2c.scan() work well with the sensors?

User avatar
Roberthh
Posts: 3668
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: TMF8801 reading junk values

Post by Roberthh » Mon Apr 25, 2022 4:00 pm

I checked the Teensy timing against the I2C specs. It is within bounds. Nevertheless, I made a build for Teensy with a new NXP library version, and that has a more symmetric SCL signal, 64% duty instead of 63%, and 1.20µs high time instead of 0.9µs. The spec requires a high time > 0.6µs at 400 kHz.

Post Reply