I2C

The phyCORE-RT1170 module provides support for Inter-Integrated Circuit (I2C) communication, enabling connections with a variety of external I2C-compatible devices. This standard two-wire bus is commonly used for low-speed communication between integrated circuits on the same board or across short distances.

The i.MX RT1170 processor includes two I2C controllers. For in-depth technical information, refer to the I2C Interface chapter in the Hardware Manual.

I2C Buses

The phyBOARD-Atlas RT1170 exposes two I2C buses through the carrier board:

  • lpi2c2

  • lpi2c5

Each bus is connected to specific onboard peripherals, as shown below.

lpi2c2 Devices

Interface

Address (7-bit)

EEPROM

0x50

EEPROM Identification Page

0x58

lpi2c5 Devices

Interface

Address (7-bit)

Audio Codec

0x18

Accelerometer

0x69

Accessing I2C Devices via Shell

To interact with I2C devices from the command line, begin by scanning the bus for connected peripherals. The example below scans the lpi2c5 bus:

uart:~$ i2c scan lpi2c5
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:             -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- 18 -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
1 devices found on lpi2c5

In this case, the scan reveals that the audio codec is available at address 0x18. To read a register from this device, use the i2c read command followed by the bus name, device address, and register offset:

uart:~$ i2c read lpi2c5 0x18 0
00000000: 00 00 01 56 00 11 04 00  00 00 00 01 01 00 80 80 |...V.... ........|

Note

Direct register access via the I2C shell commands is primarily intended for debugging. For production use, it is strongly recommended to access I2C devices through dedicated drivers whenever possible.

EEPROM Uptime Counter Example

The uptime_counter() function implements a simple persistent uptime counter using the Zephyr EEPROM API. It reads a struct from EEPROM at startup, verifies its integrity using a magic number, and then increments a stored uptime value every minute, writing it back to EEPROM.

EEPROM API usage

The code begins by retrieving the EEPROM device from the devicetree using the DEVICE_DT_GET_OR_NULL() macro. This references the eeprom0 alias, which must be defined in the board’s devicetree:

#define EEPROM0_NODE	DT_ALIAS(eeprom0)
static const struct device *eeprom = DEVICE_DT_GET_OR_NULL(EEPROM0_NODE);

Before accessing the EEPROM, the function checks if the device is ready using device_is_ready(). If the device is not ready, it logs an error and exits.

if (!device_is_ready(eeprom)) {
    LOG_ERR("Device \"%s\" is not ready.", eeprom->name);
    return;
}

To read previously stored values, the eeprom_read() function is used. It attempts to read a packed struct from offset 0 in EEPROM:

ret = eeprom_read(eeprom, UPTIME_COUNTER_OFFSET, &values, sizeof(values));

If the read fails, the function logs an error and returns. If the read is successful, it checks the magic field to determine whether the EEPROM is empty (0xFFFFFFFF) or has valid data (UPTIME_COUNTER_MAGIC). If the magic value does not match either expected case, the function logs an error and exits.

Once the structure is initialized or validated, the program logs the current stored uptime and enters a loop. Every minute, it writes the updated struct back to EEPROM using eeprom_write():

ret = eeprom_write(eeprom, UPTIME_COUNTER_OFFSET, &values, sizeof(values));

This loop continues indefinitely, with the seconds field in the struct being incremented by 60 seconds after each successful write. This ensures that the counter value is regularly updated and preserved across reboots.

Source Code

#include <zephyr/kernel.h>
#include <zephyr/drivers/eeprom.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(uptime_counter, CONFIG_APP_LOG_LEVEL);

#define SLEEP_TIME_S			60

/* size of stack area used by each thread */
#define UPTIME_COUNTER_STACKSIZE	1024

/* scheduling priority used by each thread */
#define UPTIME_COUNTER_PRIORITY		5

#define UPTIME_COUNTER_OFFSET		0
#define UPTIME_COUNTER_MAGIC		0xA3EE9703
#define UPTIME_COUNTER_EMPTY		0xFFFFFFFF

struct perisistant_values {
	uint32_t magic;
	uint64_t seconds;
} __attribute__((packed));

/*
 * Get EEPROM from the devicetree eeprom0 alias. This is mandatory.
 */
#define EEPROM0_NODE	DT_ALIAS(eeprom0)
#if !DT_NODE_HAS_STATUS_OKAY(EEPROM0_NODE)
#error "Unsupported board: eeprom0 devicetree alias is not defined"
#endif
static const struct device *eeprom = DEVICE_DT_GET_OR_NULL(EEPROM0_NODE);


void uptime_counter(void)
{
	struct perisistant_values values;
	int ret;

	if (!device_is_ready(eeprom)) {
		LOG_ERR("Device \"%s\" is not ready.", eeprom->name);
		return;
	}

	ret = eeprom_read(eeprom, UPTIME_COUNTER_OFFSET, &values, sizeof(values));
	if (ret != 0) {
		LOG_ERR("Couldn't read EEPROM: %d", ret);
		return;
	}

	if (values.magic == UPTIME_COUNTER_EMPTY) {
		LOG_INF("EEPROM is empty.");
		values.magic = UPTIME_COUNTER_MAGIC;
		values.seconds = 0;
	} else if (values.magic != UPTIME_COUNTER_MAGIC) {
		LOG_ERR("The EEPROM is either not empty or the required MAGIC header is missing.");
		return;
	}

	LOG_INF("Device uptime: %lld hours and %lld minutes", values.seconds / 3600,
		(values.seconds % 3600) / 60);

	while (1) {
		ret = eeprom_write(eeprom, UPTIME_COUNTER_OFFSET, &values,
				   sizeof(values));
		if (ret != 0) {
			LOG_ERR("Couldn't write eeprom: %d", ret);
			return;
		}

		k_sleep(K_SECONDS(SLEEP_TIME_S));
		values.seconds += SLEEP_TIME_S;
	}

	return;
}

K_THREAD_DEFINE(uptime_counter_tid, UPTIME_COUNTER_STACKSIZE, uptime_counter,
		NULL, NULL, NULL, UPTIME_COUNTER_PRIORITY, 0, 0);

For more information see the TODO! file.