别再手动模拟时序了!用Linux内核I2C子系统驱动AT24Cxx EEPROM(附完整代码)
·
深度解析Linux内核I2C子系统驱动AT24Cxx EEPROM实战
1. 嵌入式开发者的效率革命:告别GPIO模拟I2C
在嵌入式Linux开发中,GPIO模拟I2C曾是许多开发者的无奈选择。这种方式虽然简单直接,却存在诸多痛点:时序控制不精确、CPU占用率高、多设备管理复杂,且难以保证跨平台兼容性。内核提供的I2C子系统正是为解决这些问题而生。
GPIO模拟 vs 硬件I2C核心差异 :
- 时序精度:硬件I2C由专用控制器生成精确时钟
- 资源占用:DMA支持减少CPU干预
- 错误处理:自动ACK检测和重传机制
- 多主机支持:硬件仲裁避免总线冲突
// 典型GPIO模拟I2C写操作(对比示例)
void gpio_i2c_write(unsigned char addr, unsigned char data) {
gpio_set(SDA, 0); // 起始条件
gpio_set(SCL, 0);
for(int i=0; i<8; i++) { // 逐位发送
gpio_set(SDA, (addr >> (7-i)) & 0x01);
gpio_set(SCL, 1);
udelay(5);
gpio_set(SCL, 0);
}
// ...省略ACK检测和数据传输部分
}
硬件I2C的优势不仅体现在性能上,更在于其与内核生态的无缝集成。通过标准的sysfs接口和设备模型,开发者可以:
- 统一管理多个I2C设备
- 利用内核的电源管理机制
- 实现用户空间直接访问(通过i2c-dev)
- 复用现有驱动框架
2. Linux I2C子系统架构解析
2.1 三层架构设计
Linux I2C子系统采用经典的分层设计:
硬件抽象层 :
- i2c_adapter:描述物理控制器
- i2c_algorithm:实现总线通信协议
- 包含start/stop条件生成、ACK处理等底层操作
核心层 :
- 提供注册/注销接口(i2c_add_adapter)
- 实现设备发现机制
- 管理总线时钟和超时设置
- 提供SMBus兼容接口
设备驱动层 :
- i2c_client:描述从设备特性
- i2c_driver:实现设备特定功能
- 支持标准文件操作接口
graph TD
A[用户空间] -->|系统调用| B[I2C设备驱动]
B -->|i2c_transfer| C[I2C核心]
C -->|master_xfer| D[I2C适配器驱动]
D -->|寄存器操作| E[物理I2C控制器]
2.2 关键数据结构
i2c_msg结构体 :
struct i2c_msg {
__u16 addr; /* 从设备地址 */
__u16 flags; /* 读写标志 */
__u16 len; /* 消息长度 */
__u8 *buf; /* 数据缓冲区 */
};
i2c_driver注册示例 :
static struct i2c_driver at24cxx_driver = {
.driver = {
.name = "at24cxx",
.owner = THIS_MODULE,
},
.probe = at24cxx_probe,
.remove = at24cxx_remove,
.id_table = at24cxx_id_table,
};
3. AT24Cxx驱动实战开发
3.1 设备树配置
现代Linux内核推荐使用设备树描述硬件:
i2c1: i2c@40005400 {
compatible = "st,stm32-i2c";
reg = <0x40005400 0x400>;
interrupts = <31>;
clocks = <&rcc 0 STM32F4_APB1_CLOCK(I2C1)>;
#address-cells = <1>;
#size-cells = <0>;
eeprom: at24c08@50 {
compatible = "atmel,at24c08";
reg = <0x50>;
pagesize = <16>;
};
};
3.2 驱动实现核心逻辑
初始化流程 :
- 注册字符设备
- 配置I2C客户端
- 实现文件操作接口
static int at24cxx_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct at24cxx_data *data;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
i2c_set_clientdata(client, data);
mutex_init(&data->lock);
/* 注册字符设备 */
alloc_chrdev_region(&data->devno, 0, 1, "at24cxx");
cdev_init(&data->cdev, &at24cxx_fops);
cdev_add(&data->cdev, data->devno, 1);
/* 创建sysfs节点 */
device_create(class, NULL, data->devno, NULL, "at24cxx");
return 0;
}
读写操作实现 :
static ssize_t at24cxx_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct i2c_client *client = ...;
struct i2c_msg msg[2];
unsigned char addr;
int ret;
if (copy_from_user(&addr, buf, 1))
return -EFAULT;
/* 设置读取地址 */
msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].len = 1;
msg[0].buf = &addr;
/* 读取数据 */
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;
msg[1].len = 1;
msg[1].buf = buf;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret == 2) {
if (copy_to_user(buf, msg[1].buf, 1))
return -EFAULT;
return 1;
}
return -EIO;
}
4. 性能优化与高级技巧
4.1 页写入优化
AT24Cxx系列支持页写入(通常16/32字节):
static int at24cxx_page_write(struct i2c_client *client,
unsigned char addr,
unsigned char *buf, int len)
{
unsigned char *tmp_buf;
int ret;
tmp_buf = kmalloc(len + 1, GFP_KERNEL);
if (!tmp_buf)
return -ENOMEM;
tmp_buf[0] = addr;
memcpy(tmp_buf + 1, buf, len);
ret = i2c_master_send(client, tmp_buf, len + 1);
kfree(tmp_buf);
/* 等待写入完成 */
msleep(10);
return ret == len + 1 ? 0 : -EIO;
}
4.2 用户空间直接访问
通过i2c-dev接口无需编写内核驱动:
# 查看可用I2C总线
ls /dev/i2c-*
# 检测连接设备
i2cdetect -y 1
C语言示例:
#include <linux/i2c-dev.h>
int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x50); // 设置从地址
// 随机读取
i2c_smbus_write_byte(fd, 0x00); // 设置读取地址
unsigned char data = i2c_smbus_read_byte(fd);
// 页写入
unsigned char buf[17] = {0};
buf[0] = 0x00; // 起始地址
for(int i=0; i<16; i++)
buf[i+1] = i; // 测试数据
write(fd, buf, sizeof(buf));
5. 调试与问题排查
5.1 常见问题解决方案
无ACK响应 :
- 检查物理连接和上拉电阻(通常4.7kΩ)
- 确认设备地址(7位地址需左移1位)
- 验证电源电压(AT24Cxx典型3.3V/5V)
数据损坏 :
- 增加写入后的延时(典型5ms)
- 实现写保护控制(如有WP引脚)
- 检查总线负载电容(规范要求<400pF)
5.2 调试工具推荐
- 逻辑分析仪 :分析实际波形时序
- i2c-tools 套件:
# 安装工具包 sudo apt install i2c-tools # 总线扫描 i2cdetect -y 1 # 寄存器dump i2cdump -y 1 0x50 # 交互式访问 i2cget/i2cset - 内核动态调试 :
echo 8 > /proc/sys/kernel/printk dmesg -wH
6. 现代内核开发实践
6.1 设备树绑定
标准AT24Cxx设备树属性:
| 属性名 | 必需 | 说明 |
|---|---|---|
| compatible | 是 | "atmel,at24cXX"格式 |
| reg | 是 | I2C从地址(7位) |
| pagesize | 否 | 页大小(字节),默认为平台默认值 |
| read-only | 否 | 标记为只读设备 |
| address-width | 否 | 地址位宽(通常8/16) |
6.2 使用regmap API
简化寄存器访问:
static const struct regmap_config at24cxx_regmap_config = {
.reg_bits = 16,
.val_bits = 8,
.max_register = 0x1FFF,
};
static int at24cxx_probe(struct i2c_client *client)
{
struct regmap *regmap;
regmap = devm_regmap_init_i2c(client, &at24cxx_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
// 使用regmap读写
regmap_read(regmap, offset, &val);
regmap_write(regmap, offset, val);
}
7. 实战案例:完整驱动实现
7.1 驱动源码架构
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define AT24CXX_PAGE_SIZE 16
#define AT24CXX_MAX_ADDR 0x1FFF
struct at24cxx_data {
struct i2c_client *client;
struct cdev cdev;
dev_t devno;
struct mutex lock;
};
static int at24cxx_open(struct inode *inode, struct file *filp) {...}
static ssize_t at24cxx_read(struct file *filp, char __user *buf,...) {...}
static ssize_t at24cxx_write(struct file *filp, const char __user *buf,...) {...}
static const struct file_operations at24cxx_fops = {
.owner = THIS_MODULE,
.open = at24cxx_open,
.read = at24cxx_read,
.write = at24cxx_write,
};
static int at24cxx_probe(struct i2c_client *client,...)
{
/* 初始化数据结构 */
/* 注册字符设备 */
/* 创建sysfs节点 */
}
static int at24cxx_remove(struct i2c_client *client)
{
/* 释放资源 */
}
static const struct of_device_id at24cxx_of_match[] = {
{ .compatible = "atmel,at24c08" },
{},
};
MODULE_DEVICE_TABLE(of, at24cxx_of_match);
static struct i2c_driver at24cxx_driver = {
.driver = {
.name = "at24cxx",
.of_match_table = at24cxx_of_match,
},
.probe = at24cxx_probe,
.remove = at24cxx_remove,
};
module_i2c_driver(at24cxx_driver);
7.2 Makefile示例
obj-m := at24cxx.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
8. 进阶话题:多设备管理与电源优化
8.1 多设备协同工作
当系统中存在多个AT24Cxx设备时:
static int at24cxx_probe(struct i2c_client *client)
{
/* 通过client->addr区分不同设备 */
switch (client->addr) {
case 0x50:
/* 设备1特定配置 */
break;
case 0x51:
/* 设备2特定配置 */
break;
}
}
8.2 电源管理实现
static int __maybe_unused at24cxx_suspend(struct device *dev)
{
struct i2c_client *client = to_i2c_client(dev);
/* 进入低功耗模式 */
return 0;
}
static int __maybe_unused at24cxx_resume(struct device *dev)
{
/* 恢复工作状态 */
return 0;
}
static const struct dev_pm_ops at24cxx_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(at24cxx_suspend, at24cxx_resume)
};
static struct i2c_driver at24cxx_driver = {
.driver = {
.pm = &at24cxx_pm_ops,
},
};
更多推荐


所有评论(0)