目录简述
前言:
一、I2C协议
(1)概述
(2)I2C硬件框架:
(3)I2C软件框架
(4)I2C数据格式
二、SMBus协议
三、I2C系统重要的结构体
四、访问I2C设备(AP3216C)
(1)使用SMBus协议:
(2)使用I2C协议:
(3)I2C-Tools源码分析:
五、编写APP访问EEPROM(AT24C02)
(1)AT24C02访问方法
1.设备地址
2.写数据
3.读数据
(2)使用I2C-Tools编程
1.具体示例:(SMBus)
2.编译、实际效果
前言:
经典环节:我一直深信,带着问题思考和实践,能够更容易理解并学习到。
(1)I2C协议
- I2C是什么?有什么特点?
- I2C传输数据流程具体是什么样的?
- 具体I2C是如何产生开始信号(S)、结束信号(P)、响应信号(ACK)以及发送数据呢?
- I2C是双向传输的,那么是如何实现在SDA上双向传输呢?
(2)SMBus协议
- SMBus协议是什么?
- 它相较于I2C有什么异同?
(3)Linux里I2C实现
- 在Linux里,是如何表示I2C控制器、I2C设备以及要传输的数据?
(4)I2C应用编程实战
- AP3216C
- EEPROM-AT24C02
接下来的文章内容,将详细的解答上面的问题。如果有所帮助,三连关注( ^_^ ),多多支持一下,大家一同进步呀!
一、I2C协议
(1)概述
I2C是什么?它有什么特点?
在消费电子,工业电子等领域,会使用各种类型的芯片,如微控制器,电源管理,显示驱动,传感器,存储器,转换器等,他们有着不同的功能,有时需要快速的进行数据的交互,为了使用最简单的方式使这些芯片互联互通,于是I2C诞生了,I2C(Inter-Integrated Circuit)是一种通用的总线协议。它是由Philips(飞利浦)公司,现NXP(恩智浦)半导体开发的一种简单的双向两线制总线协议标准。
对于硬件设计人员来说,只需要2个管脚,极少的连接线和面积,就可以实现芯片间的通讯,对于软件开发者来说,可以使用同一个I2C驱动库,来实现实现不同器件的驱动,大大减少了软件的开发时间。极低的工作电流,降低了系统的功耗,完善的应答机制大大增强通讯的可靠性。
特点如下:
- I2C是半双工
- I2C支持多主多从模式
- 从GPIO占用上来看,I2C占用两个GPIO
- I2C有应答响应机制,数据可靠性更高
- I2C速率不会太高,最高速率3.4Mbps
- I2C通过器件地址来选择从机,从机数量的增加不会导致GPIO的增加
- I2C在SCL高电平器件进行数据采样。
- 大多应用于板内器件短距离通讯。
(2)I2C硬件框架:
- 在一个芯片(Soc)内部,有一个或多个I2C控制器。
- 在一个I2C控制器上,可以连接一个或多个I2C设备。---一定要知道地址的
- I2C总线只需要两条线:时钟线SCL、数据线SDA。
- 在I2C总线的SCL、SDA线上,都有上拉电阻。
(3)I2C软件框架
在Linux上,APP访问I2C设备是要经由I2C Device Driver解析数据和I2C Controller Driver收发数据。
以 I2C 接口的存储设备 AT24C02 为例:
- APP:
- 提出要求:把字符串"hello world!"写入 AT24C02 地址16开始的地方
- 不关心底层实现的细节,它只需要调用设备驱动程序提供的接口
- AT24C02 驱动:
- 它知道 AT24C02 要求的地址、数据格式
- 它知道发出什么信号才能让 AT24C02 执行擦除、烧写工作
- 它知道怎么判断数据是否烧写成功 它构造好一系列的数据,发给 I2C 控制器
- I2C 控制器驱动
- 它根据 I2C 协议发出各类信号:I2C 设备地址、I2C 存储地址、数据
- 它根据 I2C 协议判断
(4)I2C数据格式
I2C传输数据流程具体是什么样的?
以写操作为例:
- 主芯片要发出一个start信号
- 然后发出一个设备地址(用来确定这个设备是否存在),然后就可以传输数据
- 主设备发送一个字节数据给从设备,并等待回应
- 每传输一字节数据,接收方要由一个回应信号(确定数据是否接受完成),然后再传输下一个数据
- 数据发送完之后,主芯片就会发送一个停止信号。
具体I2C是如何产生开始信号(S)、结束信号(P)、响应信号(ACK)以及发送数据呢?
如下图所示:
- 开始信号:SDA有高到低,SCL保持高电平时,产生开始信号(S)
- 结束信号:SCL保持高电平,SDA从低到高,产生结束信号(P)
- 响应信号:接收器收到8位数据后,在第9个时钟周期会去拉低SDA,产生响应信号(ACK)
具体的数据传输两个核心要点,如下图所示:
- 当SCL高电平时,SDA数据要保持稳定。
- 当SCL低电平时,SDA可以发生变化(SDA高电平(1),低电平(0))。
- 这时在SCL高电平时,查询SDA上电平,获取数据。
从上面我们可以看到,I2C是双向传输的,那么如何实现在SDA上双向传输呢?
- 由上面的情况,是有两个驱动(主设备和从设备),如果是直接接上的话,假设出故障的话,一个输出高电平,一个输出低电平,是会发生电路短路的。
- 双方设备中,某个设备发送数据时,另一方怎样才能不影响SDA上的数据?
这里采用了一个三极管或者CMOS管,来调控和预防这个问题。示例如下:
上面电路对应的真值表如下:
从真值表和电路可以知道:
- 当某一个芯片不想影响SDA线时,那就不驱动三极管
- 想让SDA输出高电平,双方都不驱动三极管
- 想让SDA输出低电平,就驱动三极管
这样的情况下,实现数据传输,就可以是这样:
- 前8个clk,从设备不驱动三极管,主设备决定数据;发送1时不驱动,发送0时驱动三极管。
- 第9个clk,主设备不驱动三极管,从设备决定数据;回应信号,驱动三极管让SDA变为0。
注:为什么要用上拉电阻?
在第 9 个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把SCL拉低。
二、SMBus协议
SMBus:System Management Bus,系统管理总线。
SMBus是基于I2C协议的,它要求更严格,是I2C协议的子集。它被用来连接各种设备,包括电源相关设备,系统传感器,EEPROM通讯设备等等。
SMBus有哪些更严格的要求?跟一般的I2C协议有哪些差别?
-
VDD 的极限值不一样
-
I2C 协议:范围很广,甚至讨论了高达12V的情况
-
SMBus:1.8V~5V
-
-
最小时钟频率、最大的 Clock Stretching(某个设备需要更多时间进行内部处理时,它可以把SCL拉低占住I2C总线)
-
I2C:时钟频率最小值无限制,Clock Stretching 时长也没有限制
-
SMBus:时钟频率最小值是 10KHz,Clock Stretching 的最大时间值也有限制
-
-
地址回应(Address Acknowledge):一个 I2C 设备接收到它的设备地址后,是否必须发出回应信号?
-
I2C 协议:没有强制要求必须发出回应信号
-
SMBus:强制要求必须发出回应信号,这样对方才知道该设备的状态: busy,failed,或是被移除了
-
-
SMBus 协议明确了数据的传输格式
-
I2C 协议:它只定义了怎么传输数据,但是并没有定义数据的格式,这完全由设备来定义
-
SMBus:定义了几种数据格式
-
-
REPEATED START Condition(重复发出S信号)---SMBus
-
在写、读之间,可以不发出 P 信号,而是直接发出 S 信号:这个 S 信号就是REPEATED START。
-
SMBus协议分析,它的具体内容:
SMBus symbols(符号):
它在I2C协议基础上加入了command code(命令字节、一般表示芯片内部的寄存器地址)、byte count(数据长度)、data byte(数据字节,支持8位、16位)以及PEC校验码机制。
以 SMBus Block Read(较为复杂)为例:
-
I2C-tools 中的函数:i2c_smbus_read_block_data()。
- 先发出command code(理解分析:Address为从设备地址,command code一般为芯片内部的寄存器地址)
- 再发起读操作
- 先读到一个字节(Block count),表示后续要读的字节数
- 然后读取全部数据
了解更多的数据格式,参照以下的文章:
Linux系统驱动之SMBus协议_i2c_smbus_read_byte_data_韦东山的博客-CSDN博客
注:在很多设备都实现了 SMBus,而不是更宽泛的 I2C 协议,所以优先使用SMBus。
即使 I2C 控制器没有实现 SMBus,软件方面也是可以使用 I2C 协议来模拟 SMBus。
所以: Linux 建议优先使用 SMBus。
三、I2C系统重要的结构体
由第一块内容I2C的硬件框架,I2C传输着重关注I2C controller、I2C device以及传输的数据。
如何表示I2C Controller?
- 是第几个I2C Controller
- I2C Controller如何收发数据?
这里I2C Controller是用i2c_adapter来表示,具体对应的结构体如下:
- nr表示第几个I2C Controller
- i2c_algorithm,里面有该 I2C BUS 的传输函数,用来收发 I2C 数据
如何表示I2C device?
- 一定有设备地址
- 它是挂载在哪个I2C Controller上 ---即对应的I2C_adapter是什么?
这里I2C device用i2c_client来表示,具体对应的结构体如下:
- addr:设备地址
- adapter:对应上哪个I2C Controller
如何表示要传输的数据?
这里数据用i2c_msg 来表示:
-
i2c_msg 中的 flags 用来表示传输方向:bit 0 等于 I2C_M_RD 表示读,bit 0 等于 0 表示写
-
举例:设备地址为 0x50 的 EEPROM,要读取它里面存储地址为 0x10 的一个字节,要构造 2 个 i2c_msg
- 第一个 i2c_msg 表示写操作,把要访问的存储地址 0x10 发给设备
- 第二个 i2c_msg 表示读操作
-
着重关注,addr(地址)、flags(方向)、len(长度)、buf(数据)
四、访问I2C设备(AP3216C)
APP访问硬件是需要驱动程序,对于I2C设备,这里可以调用内核提供的驱动程序drivers/i2c/i2c-dev.c。通过它可以直接使用I2C Controller Driver里的adapter driver来访问I2C设备(AP3216C)。
AP3216C 是红外、光强、距离三合一的传感器,以读出光强、距离值为例,步骤如下:
- 复位:往寄存器 0 写入 0x4
- 使能:往寄存器 0 写入 0x3
- 读光强:读寄存器 0xC、0xD 得到 2 字节的光强
- 读距离:读寄存器 0xE、0xF 得到 2 字节的距离值
这里可以使用I2C-Tools来操作传感器AP3212C,有两种方式(SMBus以及I2C)访问设备:
(1)使用SMBus协议:
i2cset和i2cget函数用法,后面依次为:
命令(-f、-y)
I2CBUS(0)
设备地址
寄存器地址
数据data
(2)使用I2C协议:
i2ctransfer函数用法,后面依次为:
命令(-f、-y)
I2CBUS(0)
描述符(读写w1/w2 + @设备地址 )
寄存器地址
数据data
(3)I2C-Tools源码分析:
结合上面的流程分析,具体流程步骤:
- 打开/dev/i2c-0节点(open),会访问该 I2C 控制器下的设备。(注:i2c-dev.c 为每个 I2C 控制器(I2C Bus、I2C Adapter)都生成一个设备节点:/dev/i2c-0、/dev/i2c-1 等等;)
- 指定I2C设备的地址(非强制:ioctl(file, I2C_SLAVE, address),强制: ioctl(file, I2C_SLAVE_FORCE, address) )
- 之后采用相应的SMBus数据格式,使用ioctl(file, I2C_SMBUS, &args)函数传输数据。
五、编写APP访问EEPROM(AT24C02)
(1)AT24C02访问方法
1.设备地址
由数据手册和硬件A2A1A0都接地可知,AT24C02的设备地址是0b1010000,即0x50。
2.写数据
这里的数据时序为:
- 开始位 + 设备地址 + 写 + 相应位
- 寄存器地址 + 数据
3.读数据
可以读一个字节,也可以连续读出多个字节。连续多个字节时,芯片内部的地址会自动累加。当地址到达存储空间最后一个地址时,会从0开始。 如下图所示:
(2)使用I2C-Tools编程
1.具体示例:(SMBus)
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>
#include "i2cbusses.h"
#include <time.h>
/* ./at24c02 <i2c_bus_number> w "100ask.taobao.com"
* ./at24c02 <i2c_bus_number> r
*/
int main(int argc, char **argv)
{
unsigned char dev_addr = 0x50;
unsigned char mem_addr = 0;
unsigned char buf[32];
int file;
char filename[20];
unsigned char *str;
int ret;
struct timespec req;
if(argc != 3 && argc != 4)
{
printf("Usage:\n");
printf("%Write EEprom: %s /dev/i2c-0|1|2 w str\n", argv[0]);
printf("%Read EEprom: %s /dev/i2c-0|1|2 r str\n", argv[0]);
return -1;
}
//第一步:打开节点
file = open_i2c_dev(argv[1][0] - '0', filename, sizeof(filename), 0);
if(file < 0)
{
printf("can't open %s\n", filename);
return -1;
}
if(set_slave_addr(file, dev_addr, 1))
{
printf("can't set_slave_addr\n");
return -1;
}
//第二步:I2C读写数据
if(argv[2][0] == 'w')
{
//write
str = argv[3];
//这里添加一定的休眠时间,完成1字节数据传输后,EEPROM会进入一个写循环(需要时间)
req.tv_sec = 0;
req.tv_nsec = 20000000; /* 20ms */
while(*str)
{
//mem_addr, *str
//mem_addr++, str++
ret = i2c_smbus_write_byte_data(file, mem_addr, *str);
if(ret)
{
printf("i2c_smbus_write_byte_data err\n");
return -1;
}
//等待EEPROM写完数据
nanosleep(&req, NULL);
mem_addr++;
str++;
}
ret = i2c_smbus_write_byte_data(file, mem_addr, 0); // string end char
if (ret)
{
printf("i2c_smbus_write_byte_data err\n");
return -1;
}
}
else
{
//read
ret = i2c_smbus_read_i2c_block_data(file, mem_addr, sizeof(buf), buf);
if (ret < 0)
{
printf("i2c_smbus_read_i2c_block_data err\n");
return -1;
}
buf[31] = '\0';
printf("get data: %s\n", buf);
}
}
2.编译、实际效果
a.设置交叉编译工具链
export ARCH=arm
export CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
b.编写Makefile:设置好工具链后,make就会执行程序
all:
$(CROSS_COMPILE)gcc -I ./include -o at24c02_test at24c02_test.c i2cbusses.c smbus.c
这里有用到库文件: i2cbusses.c i2cbusses.h smbus.c
c.上机测试
//nfs挂载
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
//复制、执行程序
cp /mnt/at24c02_test /bin
at24c02_test 0 w helloworld
at24c02_test 0 r