一、I2C总线背景知识
SOC芯片平台的外设分为:
- 一级外设:外设控制器集成在SOC芯片内部
- 二级外设:外设控制器由另一块芯片负责,通过一些通讯总线与SOC芯片相连
Inter-Integrated Circuit: 字面意思是用于“集成电路之间”的通信总线,简写:IIC(或者I2C)
i2c传输的要点就是: 传输一个字节 后面必然紧跟一个"响应"信号----应答信号.这个响应信号可能来自主机,或者是从机,具体是谁,就要看传输方向。 传输方向分两种情况(每种情况又有两种可能: A无应答和 B有应答):
1.主机->从机,主机对从机发一个字节之后,主机要读取从机的响应信号(主机读SDA线)
A) 主机读SDA为高电平,说明从机无应答(意味着从机接收完毕,主机发送停止信号)
B) 主机读SDA为低电平,说明从机有应答。(可继续发送下一个字节)
2.从机->主机, 主机读取从机一个字节之后,主机要向从机发送一个响应信号(主机写SDA线)
A) 主机写SDA为高电平,从机收到主机的无应答信号之后,从机停止传输,等待主机的停止信号。
B) 主机写SDA为低电平,从机收到主机的应答信号之后,从机继续输出下一字节
二、Exynos4412 I2C收发实现之裸机版
I2CCON寄存器:控制寄存器
- 第7位:决定是否允许产生应答信号,无论发送还是接收前,需置1
- 第6位:传输时时钟线分频,一般选置1
- 第5位:决定是否开启发送或接收结束时发通知,无论发送还是接收前,需置1
- 第4位:接收或发送是否完毕可以通过检查此位是否为1,接收或发送完毕后需置0
I2CSTAT寄存器:状态寄存器
- 第6、7位:每次传输前需选择传输模式
- 第5位:置0产生将产生终止信号,传输前置1产生起始信号
- 第4位:使能数据输出,传输前需置1
I2CDS寄存器:数据寄存器,发送前被发送的数据存放处,接收后结果也从此处读取
2.1 发送
void iic_write (unsigned char slave_addr, unsigned char addr, unsigned char data)
{
I2C5.I2CDS = slave_addr;
I2C5.I2CCON = 1<<7 | 1<<6 | 1<<5;
I2C5.I2CSTAT = 0x3 << 6 | 1<<5 | 1<<4;
while(!(I2C5.I2CCON & (1<<4)));
I2C5.I2CDS = addr;
I2C5.I2CCON &= ~(1<<4);
while(!(I2C5.I2CCON & (1<<4)));
I2C5.I2CDS = data;
I2C5.I2CCON &= ~(1<<4);
while(!(I2C5.I2CCON & (1<<4)));
I2C5.I2CSTAT = 0xD0;
I2C5.I2CCON &= ~(1<<4);
mydelay_ms(10);
}
2.2 接收
void iic_read(unsigned char slave_addr, unsigned char addr, unsigned char *data)
{
I2C5.I2CDS = slave_addr;
I2C5.I2CCON = 1<<7 | 1<<6 | 1<<5;
I2C5.I2CSTAT = 0x3 << 6 | 1<<5 | 1<<4;
while(!(I2C5.I2CCON & (1<<4)));
I2C5.I2CDS = addr;
I2C5.I2CCON &= ~(1<<4);
while(!(I2C5.I2CCON & (1<<4)));
I2C5.I2CSTAT = 0xD0;
I2C5.I2CDS = slave_addr | 0x01;
I2C5.I2CCON = 1<<7 | 1<<6 | 1<<5;
I2C5.I2CSTAT = 2<<6 | 1<<5 | 1<<4;
while(!(I2C5.I2CCON & (1<<4)));
I2C5.I2CCON &= ~((1<<7) | (1<<4));
while(!(I2C5.I2CCON & (1<<4)));
I2C5.I2CSTAT = 0x90;
I2C5.I2CCON &= ~(1<<4);
*data = I2C5.I2CDS;
mydelay_ms(10);
}
三、Linux内核对I2C总线的支持
I2C设备驱动 :即挂接在I2C总线上的二级外设的驱动,也称客户(client)驱动,实现对二级外设的各种操作,二级外设的几乎所有操作全部依赖于对其自身内部寄存器的读写,对这些二级外设寄存器的读写又依赖于I2C总线的发送和接收I2C总线驱动 :即对I2C总线自身控制器的驱动,一般SOC芯片都会提供多个I2C总线控制器,每个I2C总线控制器提供一组I2C总线(SDA一根+SCL一根),每一组被称为一个I2C通道,Linux内核里将I2C总线控制器叫做适配器(adapter),适配器驱动主要工作就是提供通过本组I2C总线与二级外设进行数据传输的接口,每个二级外设驱动里必须能够获得其对应的adapter对象才能实现数据传输I2C核心 :承上启下,为I2C设备驱动和I2C总线驱动开发提供接口,为I2C设备驱动层提供管理多个i2c_driver、i2c_client对象的数据结构,为I2C总线驱动层提供多个i2c_algorithm、i2c_adapter对象的数据结构
四大核心对象之间的关系图
i2c二级外设驱动开发涉及到核心结构体及其相关接口函数:
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short flags;
unsigned short addr;
void *platform_data;
struct dev_archdata *archdata;
struct device_node *of_node;
int irq;
};
struct i2c_client {
unsigned short flags;
unsigned short addr;
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter;
struct i2c_driver *driver;
struct device dev;
int irq;
struct list_head detected;
};
struct i2c_adapter *i2c_get_adapter(int nr);
void i2c_put_adapter(struct i2c_adapter *adap);
struct i2c_client * i2c_new_probed_device
(
struct i2c_adapter *adap,
struct i2c_board_info *info,
unsigned short const *addr_list,
int (*probe)(struct i2c_adapter *, unsigned short addr)
);
void i2c_unregister_device(struct i2c_client *pclt)
struct i2c_client * i2c_new_device
(
struct i2c_adapter *padap,
struct i2c_board_info const *pinfo
);
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
void (*shutdown)(struct i2c_client *);
int (*suspend)(struct i2c_client *, pm_message_t mesg);
int (*resume)(struct i2c_client *);
void (*alert)(struct i2c_client *, 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;
};
#define i2c_add_driver(driver) i2c_register_driver(THIS_MODULE, driver)
int i2c_register_driver(struct module *owner, struct i2c_driver *driver);
void i2c_del_driver(struct i2c_driver *driver);
struct i2c_msg {
__u16 addr;
__u16 flags;
#define I2C_M_TEN 0x0010
#define I2C_M_RD 0x0001
__u16 len;
__u8 *buf;
};
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
int i2c_master_recv(const struct i2c_client *client, char *buf, int count)
int i2c_master_send(const struct i2c_client *client, const char *buf, int count)
四、MPU6050
三轴角速度+三轴加速度+温度传感器
#define SMPLRT_DIV 0x19
#define CONFIG 0x1A
#define GYRO_CONFIG 0x1B
#define ACCEL_CONFIG 0x1C
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48
#define PWR_MGMT_1 0x6B
五、应用层直接使用I2C通道
5.1 预备工作:
5.1.1 exynos4412平台每个i2c通道的信息是通过设备树提供的, 因此需要首先在exynos4412-fs4412.dts 中增加5通道的节点: 不要忘记:
- 回内核源码顶层目录执行:
make dtbs - 将新生成的dtb拷贝到
/tftpboot
5.1.2 i2c总线驱动层提供了一个字符设备驱动,以便于应用层可以直接通过它去使用i2c总线通讯去操作二级外设,但需要内核编译时添加此字符设备驱动代码(i2c-dev.c),因此需要修改make menuconfig的配置: 不要忘记:
- 回内核源码顶层目录执行:
make uImage - 将新生成的uImage拷贝到
/tftpboot
5.2 应用层直接使用i2c总线的代码实现
缺点:
- 需要应用程序开发人员查阅原理图和芯片手册,增加了他们的开发负担
- 开发出的应用程序缺乏可移植性
六、I2C总线二级外设驱动开发方法
- 查阅原理图以便得知二级外设挂在哪条I2C总线上、二级外设的身份标识(二级外设自身的地址)
- 参照platform样式搭建二级外设驱动框架
- 查询二级外设芯片手册以便得知驱动需要用到的寄存器地址
注意:
- 此处寄存器是指二级外设内部的寄存器,每个寄存器在芯片手册里有个对应编号(也被称为地址),但不是内存地址,特别提醒此寄存器不是SOC芯片内部参与内存统一编址的寄存器,更不是ARM核-CPU的寄存器
- 通过调用i2c_tranfer函数完成与相应寄存器的数据交互
- 参照字符驱动完成其余代码编写
- 创建对应的i2c_client对象
linux-3.14\Documentation\i2c\instantiating-devices 匹配方式:
- 名称匹配
- 设备树匹配
- ACPI匹配
Advanced Configuration and Power Management Interface 高级配置和电源管理接口
PC机平台采用的一种硬件配置接口
i2c二级外设驱动框架:
static int mpu6050_probe(struct i2c_client *pclt,const struct i2c_device_id *pid)
{
}
static int mpu6050_remove(struct i2c_client *pclt)
{
}
static struct i2c_device_id mpu6050_ids =
{
{"mpu6050",0},
{}
};
static struct of_device_id mpu6050_dts =
{
{.compatible = "invensense,mpu6050"},
{}
};
struct i2c_driver mpu6050_driver =
{
.driver = {
.name = "mpu6050",
.owner = THIS_MODULE,
.of_match_table = mpu6050_dts,
},
.probe = mpu6050_probe,
.remove = mpu6050_remove,
.id_table = mpu6050_ids,
};
module_i2c_driver(mpu6050_driver);
MODULE_LICENSE("GPL");
七、I2C总线二级外设驱动开发之名称匹配
这种匹配方式需要自己创建i2c_client对象
创建i2c_client对象有三种方式:
i2c_register_board_info
1.当开发板上电内核跑起来的时候,肯定是架构相关的程序首先运行,也就是mach-xxx.c
2. mach-xxx.c文件里首先会定义i2c_board_info的结构体数组,在mach-xxx.c的初始化函数里调用
i2c_register_board_info函数把i2c_board_inifo链接进内核的i2c_board_list链表当中去
3.在驱动i2c目录下和开发板板对应的驱动文件i2c-xxx.c里,创建i2c_adapter对象
4.这种方式严重依赖平台,缺乏灵活性,基本会被遗弃
i2c_new_device :明确二级外设地址的情况下可用 i2c二级外设client框架:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/i2c.h>
static struct i2c_board_info mpu6050_info =
{
I2C_BOARD_INFO("mpu6050",二级外设地址)
};
static struct i2c_client *mpu6050_client;
static int __init mpu6050_dev_init(void)
{
struct i2c_adapter *padp = NULL;
padp = i2c_get_adapter(i2c通道编号);
mpu6050_client = i2c_new_device(padp,&mpu6050_info);
i2c_put_adapter(padp);
return 0;
}
module_init(mpu6050_dev_init);
static void __exit mpu6050_dev_exit(void)
{
i2c_unregister_device(mpu6050_client);
}
module_exit(mpu6050_dev_exit);
MODULE_LICENSE("GPL");
i2c_new_probed_device i2c二级外设client框架:不明确二级外设地址,但是知道是可能几个值之一的情况下可用#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/i2c.h>
static const unsigned short addr_list[] =
{
0x68,
I2C_CLIENT_END
};
static struct i2c_client *mpu6050_client;
static int __init mpu6050_dev_init(void)
{
struct i2c_adapter *padp = NULL;
struct i2c_board_info mpu6050_info = {""};
strcpy(mpu6050_info.type,"mpu6050");
padp = i2c_get_adapter(i2c通道编号);
mpu6050_client = i2c_new_probed_device(padp,&mpu6050_info,addr_list,NULL);
i2c_put_adapter(padp);
if(mpu6050_client != NULL)
{
return 0;
}
else
{
return -ENODEV;
}
}
module_init(mpu6050_dev_init);
static void __exit mpu6050_dev_exit(void)
{
i2c_unregister_device(mpu6050_client);
}
module_exit(mpu6050_dev_exit);
MODULE_LICENSE("GPL");
到这里就结束啦!
|