驱动之I2C驱动

总览

本文使用 linux-2.6.22.6 内核, 由于jz2440开发板没有板载I2C设备, 因此源码部分无法实际测试.

I2C驱动框架分析

I2C协议本身不是太复杂, 但Linux内核为了通用化, 搞了一套复杂的总线系统.

layer.png

最要理解i2c框架, 尝试理解 i2c_add_adapteri2c_add_driver 就行了.

  • i2c_adapter 对i2c主机的抽象概念, 与 i2c_add_adapter 相关
    • 这部分的架构都是已经搭好的, 由CPU厂商完成.
    • 针对特定的开发板, 一般情况只会有一个 i2c_adapter. 会在/drivers/i2c/busses下选取一个
    • 但特殊情况, 如果需要用不同的数据预处理方式, 如 i2c-algo-bit, 那么也可以抽象出多个 i2c_adapter
  • i2c_driver 对i2c从机的抽象概念, 与 i2c_add_driver 相关
    • Linux内核给了很多i2c芯片的驱动范例. 我们所说的开发i2c驱动, 是位于这一端的.
    • 一个真实的i2c从机设备并非对应唯一的 i2c_driver.
    • 譬如i2c芯片24cXX. 可以对APP端抽象出多种概念:
      • linux内核为了让APP端能直接操作i2c, 通过 i2c-dev.c 实现了一个 i2c_driver
      • 系统里的eeprom.c, 帮我们实现了通用eeprom的操作. 就是另一个 i2c_driver
      • 我们自己也可以写一个驱动, 将24cXX认为是一块加密芯片. 就是第三个 i2c_driver
      • 这样, i2c从机端的底层都是一样的, 但上层的抽象概念是不同的. 或许, 这也是将主机端取名为 i2c_adapter 的原因, 它只是一个通讯适配器. 将APP层的不同抽象概念适配到一个个具体的i2c芯片上.
  • i2c_adapteri2c_driver 的关联方式
    • 就是 platform 总线架构, 两个链表有新加内容后, 循环查找匹配.
    • 是否匹配有两个要点:
      • 一是 i2c_adapter.nri2c_client_address_data 里的设置是否一样
      • 这里基本都不用这个值去匹配的. 总线驱动也没去设置 i2c_adapter.nr. 设备端驱动直接设置为 ANY_I2C_BUS 即可.
      • 二是 i2c 的物理地址, 根据物理地址实际通讯一下, 来进行匹配.
      • 如果用了 i2c_client_address_data.force, 那么物理地址的检测过程也将被忽略.

相关文件

  • ./drivers/i2c/i2c-core.c 这个文件实现了I2C核心的功能以及/proc/bus/i2c*接口。
    • 此文件就是 i2c核心层 作用是承上启下:
    • 对上, 提供统一的调用接口, 屏蔽硬件差异. 如提供 read write 函数.
    • 对下, 抽象出i2c操作通用的部分, 简化i2c的硬件驱动开发.
  •  ./drivers/i2c/busses 包含了各个芯片厂商的I2C总线的驱动
    • i2c-s3c2410.c 针对S3C系列处理器的I2C控制器驱动.
  • ./drivers/i2c/i2c-dev.c 实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备.
    • 把这个文件理解为系统提供的一个i2c设备驱动程序即可. 需要手动加载.
    • 此文件会调用 i2c_add_driver, 系统默认注册的一个i2c设备, 可供app端直接调用.
    • i2c芯片另外需要自己的驱动程序, 去调用 i2c_add_driver, 并注册设备.
  • ./drivers/i2c/algos 文件夹实现了一些I2C总线适配器的algorithm.
    • algorithm 这个词让人容易误解. 我的理解是数据预处理方式的不同.
    • i2c-algo-pca.c. 可参考 PCF8584 I2C-bus controller
    • i2c-algo-pcf.c. 可参考 PCA9564 Parallel bus to I2C-bus controller
    • i2c-algo-sgi.c. 应该针对给2款早已过时的PC机用的.
    • I2C_ALGO_XXX 的宏定义可以在 ./include/linux/i2c-id.h 下找到

数据结构

  • struct i2c_driver 提供 probe remove 等函数接口. i2c从机设备驱动使用
    • i2c_add_driver 函数使用. 与 i2c_adapter 对应, 两者需要匹配.
  • struct i2c_adapter 适配器. 就是将多种多样的底层I2C硬件需求(不同地址, 不同通讯方法)给一个统一的方法接入到I2C核心层.
    • 指定通讯方式(i2c_algorithm)
    • 指定i2c设备(i2c_client)
    • i2c_add_adapter 函数使用. 与 i2c_driver 对应, 两者需要匹配.
  • struct i2c_client 描述了真实设备的所有必要信息, 如 i2c addr, 设备名称, 中断号等等.
    • 除了提供给 i2c_adapter 外, 还直接和 i2c_driver 想关联.
    • 原因应该是内核层和应用层都需要方便的读取真实i2c设备的必要信息
  • struct i2c_algorithm 通讯方法. 其中两个函数指针是由底层硬件实现的. 相当于 i2c核心层和底层的接口
    • algorithm 这个词让人容易误解. 我的理解是数据预处理方式的不同.
    • 只和 i2c_adapter 相关, 给i2c主机提供收发功能
    • .master_xfer 发送函数, 需要底层实现.
    • struct i2c_msg 用于存放通讯时的地址, 数据buf, 长度等信息
    • .functionality 驱动支持的功能, 需要底层明确.
    • 底层没有接收函数. 因为i2c通讯必须由主机发起并提供时钟, 发送的同时就会接收数据.

源码分析

function.png

// =========== 从 i2c_add_driver 看 ==========
i2c_add_driver // I2C 设备驱动会调用, 如自己写的驱动
i2c_register_driver
driver->driver.bus = &i2c_bus_type;
driver_register(&driver->driver);
list_add_tail(&driver->list,&drivers); // 将 i2c_driver 放到链表尾部

list_for_each_entry(adapter, &adapters, list) {
driver->attach_adapter(adapter); // 尝试匹配 i2c_adapter
// driver->attach_adapter 就会去调用驱动里指定的 attach_adapter 函数.
// 一般的, 就是直接调用 i2c_probe. "i2c-dev.c" 除外, 它关联所有的 "i2c_adapter".
}


i2c_probe(adapter, &addr_data, eeprom_detect); // adapter 是系统传过来的
adap_id = i2c_adapter_id(adapter) // i2c_adapter.nr 作为判断.
// 判断 address_data 里是否有 forces.类型匹配即可. 强制类型不会检查I2C从设备是否存在
if (address_data->forces) {
// force里的类型与 i2c_adapter.nr 一致, 或者是 ANY_I2C_BUS
if (forces[kind][i] == adap_id || forces[kind][i] == ANY_I2C_BUS) {
i2c_probe_address(); // 调用 i2c_probe_address
}
}
// address_data.probe 里的地址和类型不受ignore影响.
// probe 的数据格式也必须是 {I2C_BUS_ID, ADDR, I2C_BUS_ID, ADDR, I2C_CLIENT_END}
if (address_data->probe[i] == adap_id || address_data->probe[i] == ANY_I2C_BUS) {
i2c_probe_address(); // 调用 i2c_probe_address
}

// address_data.normal_i2c, 就是排除 .ignore 后, 进行 i2c_probe_address


i2c_probe_address // 发出S信号,发出设备地址(来自addr_data)
i2c_smbus_xfer
i2c_smbus_xfer_emulated
i2c_transfer
adap->algo->master_xfer // 就是调用 s3c24xx_i2c_xfer
found_proc(adapter, addr, kind);
// 回调用户设置的的 detection 函数, 告知匹配成功. 可以做一些收发数据的初始化准备.



// =========== 从 i2c_add_adapter 看 ==========
i2c_add_adapter // I2C 总线驱动会调用, 如 "i2c-s3c2410.c"
i2c_register_adapter
device_register(&adap->dev); // 在 i2c-adapter 下注册 i2c-X
list_for_each(item,&drivers) {
driver = list_entry(item, struct i2c_driver, list);
driver->attach_adapter(adap); // 尝试匹配 i2c_adapter
// driver->attach_adapter 就会去调用i2c设备驱动里指定的 attach_adapter 函数.
// 后续过程和后面的 i2c_add_driver 一样, 略过不表
}

// 关于 i2c_adapter.nr 应该是由这里的驱动在设定, 作为对接的依据之一.
// 但实际上, 大多数CPU厂商都没有用这个 i2c_adapter.nr 去作为 I2C_BUS 的ID. 默认值应该是0
// 所以最终是否能匹配就变成了根据 I2C 的地址, 实际检测一下设备是否存在...

编写I2C设备驱动

一般的, I2C总线驱动也由芯片公司完成了.
因此, 当外接了某个i2c设备时, 只需要编写一下设备驱动就可以了.
linux内核还包含了常用的 i2c 设备如eeprom. 可以在 ./drivers/i2c/chips 下看看.

核心步骤如下:

  • 分配一个i2c_driver结构体
  • 设置:
    - `attach_adapter`, 它直接调用 i2c_probe (adap, 设备地址, 发现这个设备后要调用的函数)
    - `detach_client`,  卸载这个驱动后,如果之前发现能够支持的设备,则调用它来清理
    
  • 注册: i2c_add_driver
  • 注册为字符设备或其它. 如 input系统 块设备, 并实现对应的操作函数.

at24cxx.c

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/jiffies.h>
#include <linux/i2c.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <asm/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("DRAAPHO");

static unsigned short ignore[] = { I2C_CLIENT_END };
static unsigned short normal_addr[] = { 0x50, I2C_CLIENT_END }; // 地址值是7位

// ignore 数组范例
// static unsigned short ignore[] = { ANY_I2C_BUS, 0x60, I2C_CLIENT_END };

// probe 数组范例
// static unsigned short probe[] = { ANY_I2C_BUS, 0x60, I2C_CLIENT_END };

// force 数组范例
static unsigned short force_addr[] = {ANY_I2C_BUS, 0x60, I2C_CLIENT_END};
static unsigned short *forces[] = {force_addr, NULL};

static struct i2c_client_address_data addr_data = {
.normal_i2c = normal_addr, // 要发出S信号和设备地址并得到ACK信号, 才确认设备存在
.probe = ignore,
.ignore = ignore,
// 一般不用 .forces 的. 由于jz2440没有i2c从设备, 因此这里用一下.
.forces = forces, // 强制认为存在这个设备
};


static int major;
static struct class *cls;
static struct i2c_driver at24cxx_driver; // i2c_driver 结构体, 初始化在后面
struct i2c_client *at24cxx_client; // i2c_client 结构体

static ssize_t at24cxx_read(struct file *file, char __user *buf, size_t size, loff_t * offset)
{
unsigned char address;
unsigned char data;
struct i2c_msg msg[2]; // i2c_msg 结构体
int ret;

if (size != 1) // 只接受1个参数, 表地址.
return -EINVAL;
copy_from_user(&address, buf, 1);

// 读AT24CXX时,要先把要读的存储空间的地址发给它
msg[0].addr = at24cxx_client->addr; // 目的
msg[0].buf = &address; // 源
msg[0].len = 1; // 地址=1 byte
msg[0].flags = 0; // 表示写

// 然后启动读操作
msg[1].addr = at24cxx_client->addr; // 源
msg[1].buf = &data; // 目的
msg[1].len = 1; // 数据=1 byte
msg[1].flags = I2C_M_RD; // 表示读

ret = i2c_transfer(at24cxx_client->adapter, msg, 2); // 发送+接受
if (ret == 2) {
copy_to_user(buf, &data, 1);
return 1;
} else {
return -EIO;
}
}

static ssize_t at24cxx_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
unsigned char val[2];
struct i2c_msg msg[1];
int ret;

if (size != 2) // 只接受2个参数, 表地址和数据.
return -EINVAL;
copy_from_user(val, buf, 2);

msg[0].addr = at24cxx_client->addr; // 目的
msg[0].buf = val; // 源
msg[0].len = 2; // 地址+数据=2 byte
msg[0].flags = 0; // 表示写

ret = i2c_transfer(at24cxx_client->adapter, msg, 1); // 发送
if (ret == 1)
return 2;
else
return -EIO;
}

static struct file_operations at24cxx_fops = {
.owner = THIS_MODULE,
.read = at24cxx_read,
.write = at24cxx_write,
};

static int at24cxx_detect(struct i2c_adapter *adapter, int address, int kind)
{
printk("at24cxx_detect\n");

// 一个i2c_client结构体: 收发数据时会用到它
at24cxx_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);
at24cxx_client->addr = address;
at24cxx_client->adapter = adapter;
at24cxx_client->driver = &at24cxx_driver;
strcpy(at24cxx_client->name, "at24cxx");
i2c_attach_client(at24cxx_client); // 关联到 i2c_driver 和 i2c_adapter

major = register_chrdev(0, "at24cxx", &at24cxx_fops);

cls = class_create(THIS_MODULE, "at24cxx");
class_device_create(cls, NULL, MKDEV(major, 0), NULL, "at24cxx"); // /dev/at24cxx
return 0;
}

static int at24cxx_attach(struct i2c_adapter *adapter)
{
// 主动调用 probe 函数, 符合要求后, 会调用 at24cxx_detect
return i2c_probe(adapter, &addr_data, at24cxx_detect);
}

static int at24cxx_detach(struct i2c_client *client)
{
printk("at24cxx_detach\n");
class_device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
unregister_chrdev(major, "at24cxx");

i2c_detach_client(client);
kfree(i2c_get_clientdata(client));
return 0;
}

static struct i2c_driver at24cxx_driver = { // i2c_driver 结构体
.driver = {
.name = "at24cxx",
},
.attach_adapter = at24cxx_attach,
.detach_client = at24cxx_detach,
};

static int at24cxx_init(void)
{
i2c_add_driver(&at24cxx_driver); // i2c_add_driver, 会自动去匹配 i2c_add_adapter
return 0;
}

static void at24cxx_exit(void)
{
i2c_del_driver(&at24cxx_driver);
}

module_init(at24cxx_init);
module_exit(at24cxx_exit);

Makefile

TEST_FILE   := i2c_test

obj-m := at24cxx.o
KERN_SRC := /home/draapho/share/jz2440/kernel/linux-2.6.22.6/
PWD := $(shell pwd)

modules:
make -C $(KERN_SRC) M=$(PWD) modules

clean:
make -C $(KERN_SRC) M=$(PWD) clean
rm -f $(TEST_FILE)

test:
arm-linux-gcc $(TEST_FILE).c -o $(TEST_FILE)

i2c_test.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


/* i2c_test r addr
* i2c_test w addr val
*/

void print_usage(char *file)
{
printf("%s r addr\n", file);
printf("%s w addr val\n", file);
}

int main(int argc, char **argv)
{
int fd;
unsigned char buf[2];

if ((argc != 3) && (argc != 4)) {
print_usage(argv[0]);
return -1;
}

fd = open("/dev/at24cxx", O_RDWR);
if (fd < 0) {
printf("can't open /dev/at24cxx\n");
return -1;
}

if (strcmp(argv[1], "r") == 0) {
buf[0] = strtoul(argv[2], NULL, 0);
read(fd, buf, 1);
printf("data: %c, %d, 0x%2x\n", buf[0], buf[0], buf[0]);
} else if (strcmp(argv[1], "w") == 0) {
buf[0] = strtoul(argv[2], NULL, 0);
buf[1] = strtoul(argv[3], NULL, 0);
write(fd, buf, 2);
} else {
print_usage(argv[0]);
return -1;
}

return 0;
}

测试

# Ubuntu 主机端
# pwd = ~/share/jz2440/drivers/i2c/ # i2c驱动目录
$ make modules
$ make test



# 开发板端, 开始测试
$ cat /proc/devices # 注册的驱动, 如调用 "register_chrdev"
$ ls /sys/class/ # 注册的类, 如调用 "class_create"
i2c-adapter # 由 i2c-core.c 生成
# 里面有个 i2c-0 设备, 是i2c主机端概念,
# 由 i2c_add_adapter 生成. 就是 s3c2440-i2c.

$ ls /sys/class/class_name # 注册的设备, 如调用 "device_create"
$ ls /dev/ # mdev根据注册的设备, 使用mknod生成的设备节点


# pwd = ~/share/jz2440//kernel/linux-2.6.22.6/drivers/i2c # i2c驱动目录, nfs
$ insmod i2c-dev.ko # 加载系统自带的i2c
# 源码里会调用 i2c_add_driver 表示一个从机设备, 供APP端直接操作此i2c设备

$ cat /proc/devices
89 i2c # 固定的主设备号89, i2c 从设备
$ ls /sys/class/
i2c-dev # 找到了 i2c-dev 类
$ ls /sys/class/i2c-dev
i2c-0 # 这个i2c0是从机端概念, 由 i2c_add_driver 生成
# ls /dev/i2c*
/dev/i2c-0 # 是 i2c-dev 的 i2c-0


# 继续做实验
# pwd = ~/share/jz2440/drivers/i2c/ # i2c驱动目录, nfs
$ insmod at24cxx.ko # 加载驱动
at24cxx_detect # 使用的强制加载, 因此没有外设也说检测到了

$ cat /proc/devices
89 i2c # APP可以通过这里操作i2c底层
252 at24cxx # APP可以通过这里认为只是在读写eeprom, 虽然底层实现是i2c通讯
$ ls /sys/class/
at24cxx # 由自己的i2c驱动代码生成, 与 i2c_add_driver 相关
i2c-adapter # 由 i2c-core 生成, 与 i2c_add_adapter 相关
i2c-dev # 由 i2c-dev 生成, 与 i2c_add_driver 相关
$ ls /dev/at* /dev/i2c*
/dev/at24cxx /dev/i2c-0 # 两个设备节点.
# i2c-adapter 是不会出现在这里的. 因为只会对i2c从机进行读写操作, 是不会对i2c主机做什么操作的.


# at24c芯片操作.
# pwd = ~/share/jz2440/drivers/i2c/ # i2c驱动目录, nfs
$ ./i2c_test r 0
$ ./i2c_test w 0 0x59
$ ./i2c_test r 0 # 回读应该也是 0x59 才对.
# 断电后再读也应该是0x59. eeprom是非易失性存储器
# 对jz2440肯定是失败的, 因为没有这个外设.

参考资料


原创于 DRA&PHO