Understanding Linux I2C, SMBus, and Platform Subsystems: A Comprehensive Guide

Linux provides a robust framework for hardware interaction, enabling developers to write device drivers for various subsystems. This article explores the Linux I2C (Inter-Integrated Circuit), SMBus (System Management Bus), and platform subsystems, providing an overview of their architecture and implementation. We also introduce sysfs for hardware monitoring and show a practical example of an I2C sensor driver.


Introduction to Linux I2C and SMBus

I2C Overview

The I2C protocol is a simple, efficient, and widely used bus protocol for communication between a master device (e.g., microcontroller) and multiple slave devices (e.g., sensors, EEPROMs). I2C operates using two bidirectional lines:

  • SDA (Serial Data Line): For data transfer.
  • SCL (Serial Clock Line): For clock synchronization.

The Linux kernel provides a standardized framework to implement I2C bus and device drivers.

SMBus Overview

SMBus, built atop the I2C protocol, is specifically designed for system management tasks, such as monitoring voltages, temperatures, and fan speeds. It supports stricter timing and message formatting requirements compared to I2C, ensuring compatibility with system management devices.


Sysfs in the Linux Kernel

sysfs is a virtual filesystem in Linux, providing a user-space interface to kernel objects and their attributes. For hardware devices:

  • sysfs exposes attributes in /sys/class or /sys/devices.
  • It enables user-space applications to query or modify hardware settings.

For example, the hwmon subsystem uses sysfs to expose sensor readings like temperature, voltage, and fan speed.


Platform Subsystem in Linux

The platform subsystem is used for devices integrated directly into the system’s motherboard or SoC. Unlike hot-pluggable buses like USB, platform devices are usually initialized during boot.

  • Platform Device: Represents the hardware component.
  • Platform Driver: Interfaces with the platform device, enabling kernel interaction.

Platform devices and drivers communicate via platform data, often defined in the Device Tree or ACPI tables.


hwmon Subsystem

The hwmon (Hardware Monitoring) subsystem is responsible for monitoring system hardware like temperatures, voltages, and fan speeds. It uses:

  • I2C or SMBus drivers to read sensor data.
  • sysfs to expose readings to user-space.

Writing an I2C Driver

I2C Bus Driver

The bus driver manages the physical I2C controller. A basic I2C adapter registration looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <linux/i2c.h>
#include <linux/module.h>

static struct i2c_adapter my_i2c_adapter = {
.owner = THIS_MODULE,
.name = "my_i2c_adapter",
};

static int __init my_i2c_bus_init(void)
{
return i2c_add_adapter(&my_i2c_adapter);
}

static void __exit my_i2c_bus_exit(void)
{
i2c_del_adapter(&my_i2c_adapter);
}

module_init(my_i2c_bus_init);
module_exit(my_i2c_bus_exit);

MODULE_AUTHOR("Author");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Basic I2C Bus Driver");

I2C Device Driver

The device driver communicates with the specific I2C slave device. A typical implementation includes probing the device and defining read/write functions.

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
31
32
33
34
#include <linux/i2c.h>
#include <linux/module.h>

static int my_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
pr_info("Probing device: %s\n", client->name);
return 0;
}

static int my_i2c_remove(struct i2c_client *client)
{
pr_info("Removing device: %s\n", client->name);
return 0;
}

static const struct i2c_device_id my_i2c_id[] = {
{"my_device", 0},
{}
};

static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_i2c_driver",
},
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.id_table = my_i2c_id,
};

module_i2c_driver(my_i2c_driver);

MODULE_AUTHOR("Author");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Basic I2C Device Driver");

Example: I2C Sensor Driver (LM87)

The LM87 driver is an excellent example of integrating an I2C sensor with the hwmon subsystem.

Key Functions in lm87.c

1. Probe Function

Registers the device with the hwmon subsystem:

1
2
3
4
5
6
7
static int lm87_probe(struct i2c_client *client)
{
// Initialization and hardware setup
// ...
hwmon_device_register_with_info(client->dev, "lm87", data, &lm87_chip_info, NULL);
return 0;
}

2. Sysfs Interface

Exposes sensor data through sysfs:

1
2
3
4
5
6
static ssize_t show_temp_input(struct device *dev, struct device_attribute *attr, char *buf)
{
// Read temperature from sensor
// ...
return sprintf(buf, "%d\n", temp_value);
}

3. Device Initialization

Defines the I2C client and ID table:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static const struct i2c_device_id lm87_id[] = {
{ "lm87", 0 },
{ }
};

static struct i2c_driver lm87_driver = {
.driver = {
.name = "lm87",
},
.probe = lm87_probe,
.id_table = lm87_id,
};

module_i2c_driver(lm87_driver);

Conclusion

The Linux I2C, SMBus, and platform subsystems provide a robust framework for hardware monitoring and device driver development. The lm87.c driver demonstrates effective use of these subsystems, enabling developers to integrate I2C sensors seamlessly. Understanding these components equips developers to write efficient and maintainable drivers for a variety of devices.