Linux I2C设备驱动开发指南
本文将详细介绍如何在Linux环境下,针对i.MX6ULL mini开发板,为SSD1306 OLED屏幕编写一个I2C设备驱动。我们将基于野火提供的内核源码进行开发,重点在于理解和应用Linux I2C子系统。
SSD1306 OLED与I2C接口
SSD1306 OLED屏幕在嵌入式开发中十分常见,其小巧的尺寸和便捷的I2C通信方式使其成为DIY项目的热门选择。I2C总线作为嵌入式工程师必备的技能之一,其基本原理在此不再赘述。本文的重点在于如何在Linux中使用I2C子系统来操作此类I2C设备。
Linux I2C子系统架构
Linux内核的I2C子系统提供了一套标准的接口来管理I2C总线和设备。I2C总线实际上是构建在Platform总线之上的。要开发一个I2C设备驱动,通常需要修改设备树(Device Tree)和编写相应的驱动程序。以下是几个核心的数据结构,它们都定义在include/linux/i2c.h头文件中:
struct i2c_driver
此结构体用于注册一个I2C设备驱动。它包含了驱动与设备匹配、设备探测(probe)、移除(remove)以及其他回调函数。其中probe回调函数是设备被识别并绑定到驱动时执行的主要入口。
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
int (*probe_new)(struct i2c_client *);
void (*shutdown)(struct i2c_client *);
void (*alert)(struct i2c_client *, enum i2c_alert_protocol protocol, unsigned int data);
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver;
const struct i2c_device_id *id_table;
int (*detect)(struct i2c_client *, struct i2c_board_info *);
const unsigned short *address_list;
struct list_head clients;
bool disable_i2c_core_irq_mapping;
};
struct i2c_client
struct i2c_client代表一个连接到I2C总线的具体I2C设备。它包含了设备的地址、名称以及指向其所连接的I2C适配器(总线控制器)的指针。在设备树机制下,i2c_client的信息通常由I2C核心从设备树中解析得到。
struct i2c_client {
unsigned short flags; /* Device flags */
unsigned short addr; /* 7-bit client address */
char name[I2C_NAME_SIZE]; /* Chip name */
struct i2c_adapter *adapter;/* I2C bus adapter */
struct device dev; /* Device node */
int init_irq; /* Initial IRQ number */
int irq; /* IRQ number */
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; /* Callback for slave mode */
#endif
};
struct i2c_adapter 和 struct i2c_algorithm
struct i2c_adapter代表一个物理I2C控制器(总线)。而struct i2c_algorithm则封装了具体I2C控制器的数据传输方法(如master_xfer, smbus_xfer等)。这部分通常由芯片厂商实现。作为驱动开发者,我们主要关注i2c_driver和i2c_client。
struct i2c_adapter {
struct module *owner;
unsigned int class;
const struct i2c_algorithm *algo; /* Bus access algorithms */
void *algo_data;
/* ... other fields ... */
struct device dev; /* Adapter device */
/* ... other fields ... */
};
struct i2c_algorithm {
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr, unsigned short flags,
char read_write, u8 command, int size, union i2c_smbus_data *data);
u32 (*functionality) (struct i2c_adapter *);
/* ... slave mode callbacks ... */
};
编写SSD1306 I2C驱动示例
下面是一个简化的SSD1306 I2C驱动示例代码。该驱动包含发送命令和数据到OLED屏幕的函数,以及在设备探测时执行的初始化逻辑。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/kernel.h>
#include <linux/delay.h>
// Function to send a command to the SSD1306
static int ssd1306_send_command(struct i2c_client *client, const u8 cmd)
{
// Command byte format: 0x00 for command, followed by the command itself
u8 msg_buffer[2] = {0x00, cmd};
// Use i2c_transfer_buffer to send data
int ret = i2c_transfer_buffer(client, msg_buffer, sizeof(msg_buffer), 0);
if (ret < 0) {
dev_err(&client->dev, "Failed to send command 0x%02x: %d\n", cmd, ret);
}
return ret;
}
// Function to send data to the SSD1306
static int ssd1306_send_data(struct i2c_client *client, const u8 data)
{
// Data byte format: 0x40 for data, followed by the data byte
u8 msg_buffer[2] = {0x40, data};
// Use i2c_transfer_buffer to send data
int ret = i2c_transfer_buffer(client, msg_buffer, sizeof(msg_buffer), 0);
if (ret < 0) {
dev_err(&client->dev, "Failed to send data 0x%02x: %d\n", data, ret);
}
return ret;
}
// Probe function: called when a matching device is found
static int ssd1306_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
dev_info(&client->dev, "SSD1306 detected at address 0x%02x!\n", client->addr);
// --- SSD1306 Initialization Sequence ---
// Introduce a small delay to ensure the device is ready
msleep(100);
// Turn display off
ssd1306_send_command(client, 0xAE);
// Set display clock divide ratio and oscillator frequency
ssd1306_send_command(client, 0xD5); // Command: Set Display Clock
ssd1306_send_command(client, 0x80); // Argument: Default divide ratio (0x80 is default)
// Set multiplex ratio
ssd1306_send_command(client, 0xA8); // Command: Set Multiplex Ratio
ssd1306_send_command(client, 0x3F); // Argument: For 128x64 display, 0x3F (63)
// Set display offset
ssd1306_send_command(client, 0xD3); // Command: Set Display Offset
ssd1306_send_command(client, 0x00); // Argument: No offset
// Set start line
ssd1306_send_command(client, 0x40); // Command: Set Display Start Line (0x40 means start from line 0)
// Set segment re-map (for column 0 to 127)
ssd1306_send_command(client, 0xA1); // Command: Set Segment Re-map (0xA1 remaps P0 to P127)
// Set COM output scan direction (for row 0 to 63)
ssd1306_send_command(client, 0xC8); // Command: Set COM Output Scan Direction (0xC8 scans from 63 down to 0)
// Set COM pins hardware configuration
ssd1306_send_command(client, 0xDA); // Command: Set COM Pins hardware configuration
ssd1306_send_command(client, 0x12); // Argument: For 128x64, 0x12 is common
// Set contrast control
ssd1306_send_command(client, 0x81); // Command: Set Contrast Control
ssd1306_send_command(client, 0x7F); // Argument: Max contrast (0x00 to 0xFF)
// Set pre-charge period
ssd1306_send_command(client, 0xD9); // Command: Set Pre-charge Period
ssd1306_send_command(client, 0xF1); // Argument: Phase 2 period (0xF1 is common)
// Set VCOMH deselect level
ssd1306_send_command(client, 0xDB); // Command: Set VCOMH Deselect Level
ssd1306_send_command(client, 0x40); // Argument: Common value
// Enable internal regulator or external VCC
ssd1306_send_command(client, 0xAD); // Command: Set External VCC enable (0xAD, 0x8B for internal)
ssd1306_send_command(client, 0x8B); // Argument: 0x8B (internal VCC) or 0x89 (external VCC)
// Turn display on
ssd1306_send_command(client, 0xAF); // Command: Turn Display ON
dev_info(&client->dev, "SSD1306 initialized successfully.\n");
return 0; // Success
}
// Device ID table for matching
static const struct i2c_device_id ssd1306_id_table[] = {
{ "ssd1306", 0 },
{ } // Terminating entry
};
MODULE_DEVICE_TABLE(i2c, ssd1306_id_table);
// Device Tree overlay matching table
static const struct of_device_id of_ssd1306_match[] = {
{ .compatible = "ssd1306", },
{ } // Terminating entry
};
MODULE_DEVICE_TABLE(of, of_ssd1306_match);
// The I2C driver structure
static struct i2c_driver ssd1306_driver = {
.probe = ssd1306_probe,
.id_table = ssd1306_id_table,
.driver = {
.name = "ssd1306_drv", // Driver name
.owner = THIS_MODULE,
.of_match_table = of_ssd1306_match, // Match using Device Tree compatible string
},
};
// Module initialization function
static int __init ssd1306_module_init(void)
{
pr_info("Registering SSD1306 I2C driver...\n");
return i2c_add_driver(&ssd1306_driver);
}
// Module exit function
static void __exit ssd1306_module_exit(void)
{
pr_info("Unregistering SSD1306 I2C driver...\n");
i2c_del_driver(&ssd1306_driver);
}
module_init(ssd1306_module_init);
module_exit(ssd1306_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("SSD1306 OLED I2C Driver for Linux");
驱动注册与注销
在驱动的初始化函数ssd1306_module_init中,我们调用i2c_add_driver()来向Linux I2C子系统注册我们的驱动。对应的,在退出函数ssd1306_module_exit中,使用i2c_del_driver()来注销驱动。
数据传输函数如i2c_master_send()和i2c_transfer_buffer()(用于发送数据)在include/linux/i2c.h中声明。i2c_transfer_buffer()是一个更通用的接口,可以发送指定长度的数据。
// Function prototypes from kernel headers (simplified) extern int i2c_add_driver(struct i2c_driver *driver); extern void i2c_del_driver(struct i2c_driver *driver); // Simplified view of i2c_transfer_buffer for clarity int i2c_transfer_buffer(const struct i2c_client *client, char *buf, int count, int flags);
驱动编译与部署
将上述驱动代码(例如命名为ssd1306_i2c.c)放置在Linux内核源码的drivers/char/目录下。修改该目录下的Makefile文件,添加以下行:
obj-m += ssd1306_i2c.o
然后,返回到内核源码的根目录,执行编译命令:
$ make modules
编译成功后,您将在drivers/char/目录下找到ssd1306_i2c.ko模块文件。将此模块文件复制到您的开发板上,并加载它(例如使用insmod ssd1306_i2c.ko)。
设备树配置
为了让I2C核心能够识别并实例化SSD1306设备,您需要在设备树中添加相应的节点。假设您的I2C总线控制器节点名为&i2c1,则可以在其下添加SSD1306设备节点,指定其I2C地址:
// Example snippet within the &i2c1 node in your device tree source (e.g., .dts file)
&i2c1 {
/* ... other i2c1 configurations ... */
ssd1306@3c { // The '@3c' specifies the I2C address 0x3c
compatible = "ssd1306"; // This string matches the of_match_table in the driver
reg = <0x3c>; // Also specifies the I2C address
// Other properties might be needed depending on your specific hardware setup
};
};
请注意,即使使用了设备树,i2c_device_id表也建议保留,尤其是在较旧的Linux内核版本中,它可能仍用于驱动与设备的匹配。
修改完设备树源文件后,需要重新编译设备树(在内核根目录下执行$ make dtbs),并将编译生成的新设备树文件(通常是.dtb格式)替换掉开发板上的设备树文件。重启开发板后,I2C核心将根据设备树实例化i2c_client,并调用我们驱动的probe函数。
验证
如果一切配置正确,在开发板启动并加载模块后,您应该能在内核日志中看到类似"SSD1306 detected at address 0x3c!"和"SSD1306 initialized successfully."的输出。同时,SSD1306 OLED屏幕应该会被点亮并完成初始化。
本文提供了一个基础的Linux I2C设备驱动开发示例。实际应用中可能需要根据SSD1306的具体功能和您的项目需求,进一步完善驱动的API,例如添加显示字符串、绘制图形等功能。