i2c节点一般表示i2c控制器, 它会被转换为platform_device, 全志H3的总线设备驱动位于busses/i2c-mv64xxx.c ,该文件有对应的platform_driver;在总线驱动器代码中的platform_driver的probe函数中会调用i2c_add_numbered_adapter,来增加一个i2c适配器。
i2c_adap_s3c_init
s3c24xx_i2c_probe
i2c_add_numbered_adapter // drivers/i2c/i2c-core-base.c
__i2c_add_numbered_adapter
i2c_register_adapter
of_i2c_register_devices(adap); // drivers/i2c/i2c-core-of.c i2c板级信息在设备树中描述
for_each_available_child_of_node(bus, node) { //在i2c节点下遍历所有子节点
client = of_i2c_register_device(adap, node);
client = i2c_new_device(adap, &info); // 设备树中的i2c子节点被转换为i2c_client
由调用流程可以得知,i2c节点在of_i2c_register_devices函数中遍历其下的子节点,转换成一个个i2c_client。由此可以得出结论在i2c总线驱动程序中,才会处理I2C的设备树节点,找到处理代码, 通过i2c_new_device(adap, &info)创建i2c设备。
void of_i2c_register_devices(struct i2c_adapter *adap)
{
struct device_node *bus, *node;
struct i2c_client *client;
/* Only register child devices if the adapter has a node pointer set */
if (!adap->dev.of_node)
return;
dev_dbg(&adap->dev, "of_i2c: walking child nodes\n");
bus = of_get_child_by_name(adap->dev.of_node, "i2c-bus");
if (!bus)
bus = of_node_get(adap->dev.of_node);
for_each_available_child_of_node(bus, node) {
if (of_node_test_and_set_flag(node, OF_POPULATED))
continue;
client = of_i2c_register_device(adap, node);
if (IS_ERR(client)) {
dev_err(&adap->dev,
"Failed to create I2C device for %pOF\n",
node);
of_node_clear_flag(node, OF_POPULATED);
}
}
of_node_put(bus);
}
由以上分析可以初步得出设备树是如何处理解析i2c节点。在本文我们通过一个实例来实现一个i2c设备驱动。在nanopi上挂接一个光线传感器GY30的i2c设备于i2c适配器0上。由数据手册得出,当光线传感器的add脚接低电平是i2c地址为0x23,为高时地址为0x5c。
SDA ======>PA12 数据线
SCL ======>PA11 时钟线
- i2c-tools使用
进行I2C相关程序开发时,很多时候我们需要确认硬件是否正常连接,设备是否正常工作,设备的地址是多少等等,这里我们就需要使用一个用于测试I2C总线的工具——i2c tools,
i2c-tools编译,安装步骤
wget https://mirrors.edge.kernel.org/pub/software/utils/i2c-tools/i2c-tools-4.0.tar.gz //下载i2c-tools-4.0
tar -xzvf i2c-tools-4.0.tar.gz //解压
Makefile中的CC该为使用的交叉编译器
Make
编译好后,挂载NFS,出现错误,提示缺少共享库
root@wu:/mnt/i2c-tools-4.0/tools# ./i2cget
./i2cget: error while loading shared libraries: libi2c.so.0: cannot open shared object file: No such file or directory
将i2c-tools-4.0/lib/的共享库 拷贝到开发板 lib 目录下
cp -rfd *.so.* /lib
然后使用i2cdetect检测,出现了4个i2c适配器
root@wu:/mnt/i2c-tools-4.0/tools# ./i2cdetect -l
i2c-3 i2c DesignWare HDMI I2C adapter
i2c-1 i2c mv64xxx_i2c adapter I2C adapter
i2c-2 i2c mv64xxx_i2c adapter I2C adapter
i2c-0 i2c mv64xxx_i2c adapter
现在在适配器0上接了光线传感器,使用i2cdetect -y指定适配器编号就可以检测到该i2c设备的地址,由命令也可以得出设备地址为0x23
root@wu:~# i2cdetect -y 0
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- 23 -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
- 传感器操作流程
普遍采用的工作方式为 Continuously H-Resolution Mode,即连续高分辨率模式。通过i2c接口设置若干命令就可以从gy30传感器得到光照强度信息。
1 初始化 写寄存器0x01 上电
2 设置 0x11 设置成高精度模式 Continuously H-Resolution Mode,即连续高分辨率模式。
3 读数据 unsigned char Buf[3] data=Buf[0]; data=(data<<8)+Buf[1] tmp=data/1.2
- 使用用户空间直接访问i2c接口的光线传感器
在内核文档Documentation/i2c/instantiating-devices中给出了编写i2c设备驱动程序的方法。
Method 1:
Method 1a: Declare the I2C devices by bus number
Method 1b: Declare the I2C devices via devicetree
Method 1c: Declare the I2C devices via ACPI
Method 2: Instantiate the devices explicitly
Method 3: Probe an I2C bus for certain devices
Method 4: Instantiate from user-space
其中方法1使用最普遍,1a这种方法在以前的文章中阐述过,也就是把设备平台信息放在i2c_board_info,然后通过i2c_register_board_info。1b方法大同小异,只是把i2c设备信息放在i2c设备节点的子节点中,然后在驱动加载时去解析。这里我选择方法4通过用户层创建一个i2c设备。
直接通过用户空间就可以操作设备。在内核配置中需要使能I2C device interface
代码很简单,首先打开设备文件然后设置i2c设备的地址,然后按照gy30数据手册中操作流程,连续写0x01和0x11,就可以读出光照信息
#include <stdio.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <errno.h>
#define I2C_ADDR 0x23
int main(void)
{
int fd;
char buf[3];
char val,value;
float flight;
fd=open("/dev/i2c-0",O_RDWR);
if(fd<0)
{
printf("打开文件错误:%s\r\n",strerror(errno)); return 1;
}
if(ioctl( fd,I2C_SLAVE,I2C_ADDR)<0 )
{
printf("ioctl 错误 : %s\r\n",strerror(errno));return 1;
}
val=0x01;
if(write(fd,&val,1)<0)
{
printf("上电失败\r\n");
}
val=0x11;
if(write(fd,&val,1)<0)
{
printf("开启高分辨率模式2\r\n");
}
usleep(200000);
while(1)
{
if(read(fd,&buf,3)){
flight=(buf[0]*256+buf[1])*0.5/1.2;
printf("光照度: %6.2flx\r\n",flight);
}
else{
printf("读取错误\r\n");
}
sleep(5);
}
}
- 基于设备树的驱动程序
在设备树文件sun8i-h3-nanopi-m1.dts引用i2c0节点,并添加一个gy@23子节点,指定reg属性(设备地址)和compatible 属性值
132 &i2c0 {
133 status = "okay";
134 gy@23{
135 compatible = "gy,bh1750fvi";
136 reg = <0x23>;
137 };
138 };
编译设备树,sun8i-h3-nanopi-m1.dtb拷贝到启动卡的第一个分区,重启开发板进入文件系统,就会增加一个i2c设备0-0023和这个设备在设备树中相关资源
root@wu:~# ls /sys/bus/i2c/devices/
0-0023 0-0068 2-003c i2c-0 i2c-1 i2c-2 i2c-3
root@wu:~# cat /sys/bus/i2c/devices/0-0023/of_node/compatible
gy,bh1750fviroot@wu:~#
给gy@23编写驱动程序
- 构造一个platform_driver,其中的of_match_table字段需要与 gy@23节点的compatible属性值一致,当匹配时则调用platform_driver的probe函数
static const struct of_device_id ids[]=
{
{.compatible="gy,bh1750fvi"},
{}
};
/* 1. 分配/设置i2c_driver */
static struct i2c_driver gy_sensor_driver = {
.driver = {
.name = "wu",
.owner = THIS_MODULE,
.of_match_table=ids,
},
.probe = gy_sensor_probe,
.remove = gy_sensor_remove,
};
static int gy_sensor_drv_init(void)
{
/* 2. 注册i2c_driver */
i2c_add_driver(&gy_sensor_driver);
return 0;
}
- 在i2c_driver的probe函数中得到在总线驱动程序中解析得到的i2c_client,并为该光线传感器注册一个字符设备
static int gy_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
gy_sensor_client = client;
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "gy_sensor", &gy_sensor_fops);
class = class_create(THIS_MODULE, "gy_sensor");
device_create(class, NULL, MKDEV(major, 0), NULL, "gy_sensor"); /* /dev/gy_sensor */
return 0;
}
- 填充字符设备中的file_operations结构体 ,在驱动的open函数中,初始化光线传感器 ,在驱动的read函数中,读出传感器的两个字节,怎么转换成光强值由应用程序来负责。
int gy_sensor_open (struct inode *inode, struct file *file)
{
printk("open gy_sensor\n");
gy_sensor_write_reg(0x01); //power up
gy_sensor_write_reg(0x11);
return 0;
}
static ssize_t gy_sensor_read(struct file * file, char __user *buf, size_t count, loff_t *off)
{
unsigned char addr=0, data[2];
gy_sensor_read_reg(data);
copy_to_user(buf, data, 2);
return 1;
}
- 构造i2c_msg通过这个client调用i2c_tansfer来读写
static int gy_sensor_write_reg(unsigned char addr)
{
int ret = -1;
struct i2c_msg msgs;
printk("gy_sensor_client -> addr=%d\n",gy_sensor_client -> addr);
msgs.addr = gy_sensor_client -> addr;////GY30_ADDR,直接封装于i2c_msg
msgs.buf = &addr;
msgs.len = 1; //长度1 byte
msgs.flags = 0; //表示写
ret = i2c_transfer(gy_sensor_client ->adapter, &msgs, 1);//这里都封装好了,本来根据i2c协议写数据需要先写入器件写地址,然后才能读
if (ret < 0)
{
printk("i2c_transfer write err\n");
return -1;
}
return 0;
}
static int gy_sensor_read_reg(unsigned char *buf)
{
int ret = -1;
struct i2c_msg msg;
msg.addr = gy_sensor_client -> addr;//GY30_ADDR
msg.buf = buf;
msg.len = 2; //长度1 byte
msg.flags = I2C_M_RD; //表示读
ret = i2c_transfer(gy_sensor_client ->adapter, &msg, 1);//这里都封装好了,本来根据i2c协议读数据需要先写入读地址,然后才能读
if (ret < 0)
{
printk("i2c_transfer write err\n");
return -1;
}
return 0;
}
- 在platform_driver的remove函数中,注销该字符设备
static int gy_sensor_remove(struct i2c_client *client)
{
//printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(class, MKDEV(major, 0));
class_destroy(class);
unregister_chrdev(major, "gy_sensor");
return 0;
}
编写测试程序来读写光线传感器的光照强度
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int fd;
char val;
unsigned char buf[3];
float flight;
fd = open("/dev/gy_sensor", O_RDWR);
if (fd < 0)
{
printf("can't open /dev/gy_sensor\n");
return -1;
}
usleep(200000);
while(1)
{
if(read(fd,&buf,3)){
flight=(buf[0]*256+buf[1])*0.5/1.2;
printf("light: %6.2flx\r\n",flight);
}
else{
printf("read err!\r\n");
}
sleep(4);
}
return 0;
}