User Tools

Site Tools


developersguide:i2cdriverprogramming

I2C Device Driver Programming

This guide walks you through the steps of creating your own device driver for an I2C device in C on Minix3 using the i2cdriver library. The current version of this guide documents the features used in git commit 75bd300 and later. If you update this document because of changes to MINIX 3, please mention the commit ID of the change in the wiki comment.

This guide is for Minix device drivers which run as services and interace with the bus driver directly via Minix IPC. You should also be aware that Minix supports the same /dev/i2c interface that is provided by NetBSD and OpenBSD. That interface is defined here: dev/i2c/i2c_io.h. Depending on your needs, it may be easier or more appropriate to use the /dev interface. Usage is documented on the I2C /dev interface page.

General Overview

Developing an I2C device driver isn't too difficult because the bus interface is simple and straightforward, and Minix provides an i2cdriver library to handle tasks that would be repeated in every driver, including the more advanced stuff like querying DS for the bus' endpoint. This guide will show you how to add your own driver to the system and use libi2cdriver. It uses the TDA19988 driver as an example. Before you begin, it is suggested that you read I2C Subsystem Internals and The I2C Protocol as those will give you additional insight into the Minix I2C subsystem.

Service Definition

In order for our driver to function properly, it must be defined in etc/system.conf. That file defines what each service/driver can do. Add an entry for your driver. Here I'm specifying that ipc between the driver, system, reincarnation server, data store server, and the i2c driver should be allowed.

service tda19988
{
	ipc SYSTEM RS DS i2c;
};

A cleaner alternative is for the driver to have its own file in /etc/system.conf.d/.

Build System

The first step is to create a directory for your driver's source code under the drivers directory.

mkdir drivers/tda19988

Next, we need to create drivers/tda19988/Makefile to instruct the build system on how it should be built.

# Makefile for the tda19988 HDMI framer found on the BeagleBone Black.
PROG=	tda19988
SRCS=	tda19988.c

DPADD+=	${LIBI2CDRIVER} ${LIBSYS} ${LIBTIMERS}
LDADD+=	-li2cdriver -lsys -ltimers

MAN=

CPPFLAGS+=	-I${NETBSDSRCDIR}

.include <minix.service.mk>

Then, we need to edit drivers/Makefile so that our driver is included in the build. Find the list of drivers that get built on ARM, and add your driver to that list. It goes in the ARM-only section for now as ARM is the only Minix platform with I2C support. Note, there are two of these and you'll want to add your driver to the longer one towards the bottom in the ${MKIMAGEONLY} != “yes” section.

  *if ${MACHINE_ARCH} == "earm"
SUBDIR=  cat24c256 fb gpio i2c mmc log tda19988 tty random
  *endif

We also need to tell the build system about the binaries we wish to be included in the installation. This is done by adding a line to distrib/sets/lists/minix/md.evbarm listing the installation path of the driver.

./service/tda19988				minix-sys

Editing those Makefiles and adding the install path to the file list is all that is needed to add a driver to the build system. Now we're ready to start writing some code.

Driver Skeleton

The following section goes step by step through the code that you should have in almost any I2C driver and explains a bit about what it does and how it works. All lines are being added to our driver's source: drivers/tda19988/tda19988.c

You should include minix/i2c.h which defines the minix_i2c_ioctl_exec_t structure which will hold your read/write requests and the results of the operation. You will also need to include minix/i2cdriver.h which defines the library functions available. The minix/drivers.h and minix/ds.h headers should also be included. Additionally, you may want to include headers for library functions (stdlib.h, string.h, etc).

#include <minix/ds.h>
#include <minix/drivers.h>
#include <minix/i2c.h>
#include <minix/i2cdriver.h>

This next step is optional but highly recommended. Minix provides a simple logging framework which is very flexible. You simply include the minix/log.h header, define an instance of struct log, and then use the log_*(&log, “”) functions in your code.

#include <minix/log.h>

/* logging - use with log_warn(), log_info(), log_debug(), log_trace(), etc */
static struct log log = {
	.name = "tda19988",
	.log_level = LEVEL_INFO,
	.log_func = default_log
};

Next, we'll define a list of valid slave addresses for the device. Typically, I2C devices have a predefined slave address or can be configured to have an address that's within a range of predefined slave addresses. The list of valid addresses is used to validate that the user is pointing the driver at the right device. The list ends with a special end of list value, 0x00. Here the driver has 4 possible addresses; the last two bits are configurable by setting pins high or low on our particular device, the TDA19988. Again, take note that the list ends with a NULL byte, 0x00.

static i2c_addr_t valid_addrs[5] = {
	0x34, 0x35, 0x36, 0x37, 0x00
};

There are a few global variables that we need to define. Those relate to the slave address of the device that this instance of the driver is driving as well as the bus that the particular device we want to drive is on. If you hadn't guessed it already, one aspect of the design is that there will be 1 instance of the driver per I2C device. This simplifies the programming model and helps isolate the damage a sick driver can cause.

/* the bus that this device is on (counting starting at 1) */
static uint32_t bus;

/* slave address of the device */
static i2c_addr_t address;

/* endpoint for the driver for the bus itself. */
static endpoint_t bus_endpoint;

Now we'll move on to the main() function and discuss driver initialization. Since the driver is flexible and can be used to drive devices on different buses, the user needs a way to point the driver at a specific device on a specific bus. To do that, the user passes the bus number and slave address to the driver via the service(8) command using the -args option. Minix provides a nice set of argument parsing functions, so all you have to do is call env_setargs() and then i2cdriver_env_parse() to extract the bus and slave address, and validate the slave address. A negative return code is used to indicate that the bus or slave address parameters were either not specified or totally invalid. A positive return code is used to indicate that the address was not in the expected address list. It's up to the driver author to decide what to do in that case (maybe the user knows what he/she is doing and you accept the input?). A return code of 0 means the bus and slave addresses were valid.

int
main(int argc, char *argv[])
{
	int r;

	env_setargs(argc, argv);

	r = i2cdriver_env_parse(&bus, &address, valid_addrs);
	if (r < 0) {
		log_warn(&log, "Expecting -args 'bus=X address=0xYY'\n");
		log_warn(&log, "Example -args 'bus=1 address=0x34'\n");
		return EXIT_FAILURE;
	} else if (r > 0) {
		log_warn(&log,
		    "Invalid slave address for device, expecting 0x34-0x37\n");
		return EXIT_FAILURE;
	}

	...

The next thing we want to do in main() is call a function that we'll define soon called sef_local_startup(). It will initialize the System Event Framework (SEF) for our driver. SEF is a framework that handles the live updating and self-healing aspects of Minix. I won't discuss the whole thing here, but more info is in the SEF link above as well as in the general driver programming guide.

	...

	sef_local_startup();

	...

Add a function prototype at the top of your C file.

static void sef_local_startup(void);

Then implement the function.

static void
sef_local_startup(void)
{
	/*
	 * Register init callbacks. Use the same function for all event types
	 */
	sef_setcb_init_fresh(sef_cb_init);
	sef_setcb_init_lu(sef_cb_init);
	sef_setcb_init_restart(sef_cb_init);

	/*
	 * Register live update callbacks.
	 */
	/* Agree to update immediately when LU is requested in a valid state. */
	sef_setcb_lu_prepare(sef_cb_lu_prepare_always_ready);
	/* Support live update starting from any standard state. */
	sef_setcb_lu_state_isvalid(sef_cb_lu_state_isvalid_standard);
	/* Register a custom routine to save the state. */
	sef_setcb_lu_state_save(sef_cb_lu_state_save);

	/* Let SEF perform startup. */
	sef_startup();
}

Next, we'll implement the state save and state restore functions. List the prototypes at the top of the file.

static int sef_cb_lu_state_save(int);
static int lu_state_restore(void);

Here we're just saving the bus and address information and restoring it using the Data Store API. The bus endpoint will be re-queried during an update/restart so it doesn't need to be preserved.

static int
sef_cb_lu_state_save(int UNUSED(state))
{
	ds_publish_u32("bus", bus, DSF_OVERWRITE);
	ds_publish_u32("address", address, DSF_OVERWRITE);
	return OK;
}

static int
lu_state_restore(void)
{
	/* Restore the state. */
	u32_t value;

	ds_retrieve_u32("bus", &value);
	ds_delete_u32("bus");
	bus = (int) value;

	ds_retrieve_u32("address", &value);
	ds_delete_u32("address");
	address = (int) value;

	return OK;
}

Finally, we're at the last SEF related function, sef_cb_init(). This callback is to (re-)initialize the driver. Here's the prototype to put at the top of the file.

static int sef_cb_init(int type, sef_init_info_t * info);

The initialization function uses the i2cdriver library to perform a number of setup tasks. The endpoint of the bus driver is queried and set with i2cdriver_bus_endpoint(). A reservation is made with the bus driver so that only this driver can access the specified slave address with i2cdriver_reserve_device(). Next, a call to i2cdriver_subscribe_bus_updates() adds a subscription for updates from the bus driver about restarts. This will be used later to automatically update the bus driver's endpoint if it is restarted. It will allow your driver to survive the bus driver being restarted. Finally, i2cdriver_announce() is called. This announces that the driver is up and helps the bus driver deal with I2C device drivers being restarted. You'll want to replace “DO DRIVER SPECIFIC INIT HERE” with code to initialize any internal data structures your driver uses.

static int
sef_cb_init(int type, sef_init_info_t * UNUSED(info))
{
	int r;

	if (type == SEF_INIT_LU) {
		/* Restore the state. */
		lu_state_restore();
	}


	/** DO DRIVER SPECIFIC INIT HERE **/


	/* look-up the endpoint for the bus driver */
	bus_endpoint = i2cdriver_bus_endpoint(bus);
	if (bus_endpoint == 0) {
		log_warn(&log, "Couldn't find bus driver.\n");
		return EXIT_FAILURE;
	}

	/* claim the device */
	r = i2cdriver_reserve_device(bus_endpoint, address);
	if (r != OK) {
		log_warn(&log, "Couldn't reserve device 0x%x (r=%d)\n",
		    address, r);
		return EXIT_FAILURE;
	}

	if (type != SEF_INIT_LU) {

		/* sign up for updates about the i2c bus going down/up */
		r = i2cdriver_subscribe_bus_updates(bus);
		if (r != OK) {
			log_warn(&log, "Couldn't subscribe to bus updates\n");
			return EXIT_FAILURE;
		}

		i2cdriver_announce(bus);
		log_debug(&log, "announced\n");
	}

	return OK;
}

That covers all of the initial setup that you'll have to do. The next part depends on your specific needs.

Choosing Driver Interface Model

Now it's time to start thinking about how your driver will be used. Will other services or drivers be sending Minix IPC messages to it? Will it be accessed through a device file? Your choice here sort of dictates how you develop the rest of your driver. For the particular driver that is the focus of this tutorial, the TDA19988, it doesn't make a lot of sense to provide a device file interface (what can a user program do with an HDMI framer?), but it does make a lot of sense to provide a Minix IPC interface since the frame buffer driver will need to access it.

If you need documentation for block device drivers see: The Block Device protocol. If you need documentation for character device drivers see: Programming Device Drivers in Minix. Some simple examples of each are: the cat24c256 driver (block) and the i2c bus driver (char).

Main Message Loop

Nearly all Minix drivers spend their time blocked, waiting for a Minix IPC message to arrive. They decode the message, perform some work, and most of the time they send a reply with the result. For character device drivers and block device drivers, there is a specific set of messages that need to be implemented. In these cases and a few others (network drivers for instance), libraries provide main message loop functions that call callbacks (example: chardriver_task() and blockdriver_task()). There is no i2cdriver_task() because there are no universal actions that apply to all I2C devices. Instead, we have to develop our own main message loop.

Here we're going to add some more code to the main() function. This is our main message loop. It doesn't do much now, but we'll be adding to it later once the message types are defined for our driver. Currently it just replies with OK. You'll also notice that notifications from DS get special handling. Earlier, in the init code, we subscribed to updates from DS about the bus driver. Here is where those get processed. It will help our driver survive if the i2c bus driver restarts.

...

	endpoint_t user, caller;
	message m;
	int ipc_status;

...

	while (TRUE) {

		/* Receive Message */
		r = sef_receive_status(ANY, &m, &ipc_status);
		if (r != OK) {
			log_warn(&log, "sef_receive_status() failed\n");
			continue;
		}

		if (is_ipc_notify(ipc_status)) {

			if (m.m_source == DS_PROC_NR) {
				/* bus driver changed state, update endpoint */
				i2cdriver_handle_bus_update(&bus_endpoint, bus, address);
			}

			/* Do not reply to notifications. */
			continue;
		}

		caller = m.m_source;
		user = m.USER_ENDPT;

		/* TODO/XXX: HANDLE MESSAGES HERE */

		/* Send Reply */
		m.m_type = TASK_REPLY;
		m.REP_ENDPT = user;
		m.REP_STATUS = OK;

		r = sendnb(caller, &m);
		if (r != OK) {
			log_warn(&log, "sendnb() failed\n");
			continue;
		}
	}

	return 0;
}

That concludes the generic code that you need for an I2C device driver. The next task is to design the messages that your driver will handle, define the message types in include/minix/com.h, add a switch statement in main on m.m_type, and handle the messages.

Helpful i2cdriver Library Functions

The minix/i2cdriver.h header defines the set of functions provided by the i2cdriver library. This library contains common functions useful throughout many I2C drivers.

i2cdriver_exec

The i2cdriver_exec function does the heavy lifting when it comes to accessing the bus. Given the endpoint for an i2c bus driver, it can send a minix_i2c_ioctl_exec_t to the bus, and return the response. As described earlier, your driver can look-up the bus endpoint using i2cdriver_bus_endpoint().

i2creg_read/write/set/clear

Filling a minix_i2c_ioctl_exec_t for every read/write operation requires too much work. Additionally, most I2C devices work in a similar way; the device has registers which are addressed before a read or write. To make things simple, the i2cdriver library provides several functions for reading and writing from and to I2C device registers. Below are some examples from existing drivers:

i2creg_read8 performs a 1 byte read of the given register. Here, the TDA19988 driver is reading the CEC_STATUS_REG register and storing the value in val.

i2creg_read8(cec_bus_endpoint, cec_address, CEC_STATUS_REG, &val);

i2creg_write8 performs a one byte write to the given register. Here, the TDA19988 driver is writing CEC_ENABLE_ALL_MASK to the CEC_ENABLE_REG.

i2creg_write8(cec_bus_endpoint, cec_address, CEC_ENABLE_REG, CEC_ENABLE_ALL_MASK);

i2creg_read24 is similar to the i2creg_read8 above, but instead of reading just one byte, it reads 3 bytes. There is also a i2creg_read16. Here, the BMP085 driver is reading a 24-bit value from SENSOR_VAL_MSB_REG and storing the result in 'up'.

i2creg_read24(bus_endpoint, address, SENSOR_VAL_MSB_REG, &up)

i2creg_set_bits8 performs a read of the given register, sets the specified bits, and writes the value back to the register. Here, the TPS65950 driver is setting the STOP_RTC_BIT bit in the RTC_CTRL_REG to 1.

i2creg_set_bits8(bus_endpoint, addresses[ID4], RTC_CTRL_REG, (1 << STOP_RTC_BIT));

i2creg_clear_bits8 performs a read of the given register, clears the specified bits, and writes the value back to the register. Here, the TPS65950 driver is clearing the STOP_RTC_BIT bit in the RTC_CTRL_REG to 0.

i2creg_clear_bits8(bus_endpoint, addresses[ID4], RTC_CTRL_REG, (1 << STOP_RTC_BIT));

i2creg_raw_read8 and i2creg_raw_write8 work like i2creg_read8 and i2creg_write8 but do not send the register address. Here are some examples from the TSL2550 and BMP085 drivers:

i2creg_raw_read8(bus_endpoint, address, &val);
i2creg_raw_write8(bus_endpoint, address, CMD_SOFT_RESET);

Code Formatting

There is a Coding Style guide which explains how your code should be formatted. In short, it's the NetBSD style (based on Kernel Normal Form). You should use the style for all new code.

If you want to ensure that your code conforms to that style, there is an indent.pro file that can be used with the indent utility. If you're using GNU indent, not all of the options in the official indent.pro file are accepted (GNU indent is different than the indent that ships with NetBSD and Minix), so I use the following options in my ~/.indent.pro file for GNU indent.

-bap
-br
-ce
-ci4
-cli0
-d0
-di0
-i8
-ip8
-l79
-nbc
-ncdb
-ndj
-nfc1
-nlp
-npcs
-psl
-sc
-sob

Compiling your Code

Instructions for compiling Minix/arm are available on the Minix on ARM page.

Starting Your Driver

Assuming your driver compiled okay, you can boot up Minix and start up an instance of the driver. Remember to follow the labelling suggestions in the I2C internals guide.

Manually

With no device file:

service up /service/tda19988 -label tda19988.1.34 -args 'bus=1 address=0x34'

With a device file:

service up /service/cat24c256 -dev /dev/eepromb1s50 -label cat24c256.1.50 -args 'bus=1 address=0x50'

Automatically at Boot

Inside etc/usr/rc there is a section that starts up the I2C bus drivers and the drivers for the specific board that Minix is running on. To get your driver to start at boot, simply add it in the CASE statement for the specific boards that the chip is used on.

You'll notice that 'up' is a function that calls the service command. It fills in the path to the driver, so you don't specify the /usr/sbin path like you do when you manually bring up the driver.

With no device file:

up tda19988 -label tda19988.1.34 -args 'bus=1 address=0x34'

With a device file:

up cat24c256 -dev /dev/eepromb1s50 -label cat24c256.1.50 -args 'bus=1 address=0x50'

Contributing your Driver to Minix

If you develop a new I2C driver for Minix, please consider contributing it to the Minix project so that everyone can benefit from your work. If you have trouble with the contribution process, contact the Minix3 project using the resources listed on this page.

Documentation

If you contribute your driver to Minix, we would also welcome some documentation along with it. As English isn't everyone's native language, feel free to ask for some help on this part if you aren't entirely comfortable writing long texts.

Wiki

If the driver is to be accessed by other drivers, create a wiki page describing the valid message types and possible responses. An example is the I2C Protocol document. It lists the messages that the I2C bus driver will accept and what reply codes could be returned.

Source Code

Try to use comments to explain your code. Don't go overboard, but at least explain the tricky parts. Also, a brief intro at the top of the main C file explaining what the driver is to be used for is nice.

You can add a README.txt file to the source directory for your driver explaining a bit about it, maybe where to find documentation on the hardware, any limitations (hardware or otherwise), and possible future improvements if you wish.

Other Things Not Covered Here

Device Files

The TDA19988 doesn't have a device file interface. In the event that your driver needs one. Here are the steps for defining the device file.

  • Add the definition to include/minix/dmap.h so others know the device number is in use.
  • Teach MAKEDEV how to make the device file by editing commands/MAKEDEV/MAKEDEV.sh
developersguide/i2cdriverprogramming.txt · Last modified: 2021/07/10 17:01 by stux