1 I2C原理 1.1 硬件电路 I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。
SDA(串行数据线)和SCL(串行时钟线)都是双向I/O线,需通过上拉电阻接电源VCC.当总线空闲时.两根线都是高电平。
I2C 总线标准模式下速度可以达到 100Kb/S,快速模式下可以达到 400Kb/S。SDA 和 SCL 这两根线必须要接一个上拉电阻,一般是 4.7K。
1.2 i2c协议 传输过程如下:
1 2 3 4 5 6 7 8 1. 主控发送start讯号(S)2. 主控发送从设备地址(slave dev addr)3. 主控发送方向(W/R)4. 从设备应答(ack)5. 主控(or从设备)发送数据(data)6. 从设备(or主控)应答(ack)... 7. 主控发送停止信号(P)
s3c2440 一次i2c读写过程:
1.2.1 S/P信号
start信号:SCL是高电平,SDA被主控拉低。 stop信号:SCL是高电平,SDA被主控拉高。
示波器测量出start信号:
示波器测量出stop信号:
1.2.2 ACK信号
第9个时钟周期,SDA被拉低表示ack讯号。
1.2.3 DATA格式
用 9个clk传输8bit数据(7bit 从设备地址 + 1bit方向 ),MSB高位先出。第9个clk是ack讯号。
1.2.4 数据有效性 SDA 线上的数据必须在SCL高电平周期保持稳定,在 SCL 低电平时才能允许改变 。
1.3 一次完整的I2C数据传输举例
主控发送了S信号;
发送地址0x34,包含读写位;
发送数据0x30, 0x00, 0x01共3个字节数据;
最后SDA被拉高发送P信号。
这里我是用了带I2C解码的示波器,能将I2C协议解码出来方便调试者阅读分析。
1.4 一条SDA上实现双向传输的原理 电路设计内部结构使用开极电路 。如下图:
条件 :
主设备发送时,从设备不发送(通过SCL控制即可,比如让前8个clk主控发送数据到SDA,让第9个clk从设备发送数据到SDA)
主设备发送数据时,从设备的“发送引脚”不能影响SDA数据。反之,从设备发送数据时,主设备的”发送引脚”不能影响到SDA数据。那么如何做到?
从上图知道:
当A,B都为低电平时,三极管不导通,SDA的电平取决于外部电路,这里SDA有上拉电阻,所以对应高电平;
当主控拉高A时,三极管导通,此时SDA接地,电平被拉低
同理,当从设备拉高B时,三极管导通,此时SDA接地,电平被拉低
那么电平真值表如下:
所以,要实现双向传输:
1 2 3 如果要master-> slave进行数据传输,那么让主控驱动三极管,拉低SDA。 如果要slave-> master进行数据传输,那么让从设备驱动三极管,拉低SDA。 否则,都不驱动三极管,SDA一直输出高电平,处于idle状态。
从下面的例子可以看看数据是怎么传的(实现双向传输)。
举例:主设备发送(8bit)给从设备:
1 2 3 4 5 6 前 8 个 clk ◼ 从设备不要影响 SDA,从设备不驱动三极管 ◼ 主设备决定数据,主设备要发送 1 时不驱动三极管,要发送 0 时驱动三极管 第 9 个 clk,由从设备决定数据 ◼ 主设备不驱动三极管 ◼ 从设备决定数据,要发出回应信号的话,就驱动三极管让 SDA 变为 0
从这里也可以知道 ACK 信号是低电平从上面的例子,就可以知道怎样在一条线上实现双向传输,这就是 SDA 上要使用上拉电阻的原因。
为何 SCL 也要使用上拉电阻?在第 9 个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把 SCL 拉低。
当 SCL 为低电平时候,大家都不应该使用 IIC 总线,只有当 SCL 从低电平变为高电平的时候,IIC 总线才能被使用。当它就绪后,就可以不再驱动三极管,这是上拉电阻把 SCL 变为高电平,其他设备就可以继续使用 I2C 总线了。
1.5 SCL被从设备拉低表示busy状态
在第9个clk 后i2c会产生中断,此时SCL被拉低,表示busy状态 ,表示谁都不允许再使用i2c, 然后等到中断处理结束了,也就是处于idle状态了,此时会释放出SCL,那么主控可以继续发送SCL讯号表示可以继续进行i2c通信了。
2 I2C控制器 2.1 I2c主从设备关系
对于写操作,主控作为transmitter,从设备作为receiver。 对于读操作,主控作为receiver, 从设备作为transmitter。
2.2 s3c2440 I2C控制器 2.2.1 控制器框图
1 2 3 4 5 6 7 8 9 10 11 Pclk = 50 Mhz, 经过prescaler分频,可以得到SCL。 IICSTAT: 发出S(start)信号或者P(stop)信号。 Data Bus可以把数据写入IICDS寄存器,然后会自动产生SCL,并且会将8 位数据从SDA同步给slave dev, 在数据发送出去后,在第9 个SCL时钟,会受到slave dev的ack应答,可以通过查询IICSTAT来判断是否有ACK回应。 当slave dev回应ACK后,那么又可以继续发送数据,继续写入据到IICDS。 当主控想结束,设置IICSTAT发出P信号。
2.2.2 寄存器介绍 2.2.2.1 IICCON-时钟配置
1 2 3 4 5 6 7 8 9 Bit[7 ]: 对于发送模式,不需要配置ack信号,ack是接收者发送回来的应答。对于接受模式,设置成1 ,让它在第9 个CLK发出ack讯号(拉低sda)。 Bit[6 ]:SCL时钟源,pclk分频即可 Bit[5 ]:中断使能,使用i2c时要去enable Bit[4 ]:中断状态标识 表示中断有没有结束,当该bit读出来是1 时,SCL被拉低表示busy,也就是i2c中断还在处理中。当i2c中断处理结束后,可以将该bit 清0 ,释放出SCL。 Bit[3 :0 ]:i2c时钟分频系数配置,SCL时钟 = IICCLK/(IICCON[3 :0 ]+1 )
2.2.2.2 IICSTAT-模式配置
1 2 3 4 5 6 7 8 9 bit[7 :6 ]:模式选择 Bit[5 ]:当读的时候,0 表示not busy,1 表示busy, 当写的时候,0 表示写入STOP, 1 表示写入START Bit[4 ] : 数据输出使能,0 :表示disable, 1 表示enable Bit[3 ]:仲裁flag Bit[0 ]:表示i2c总线上的第9 个时钟周期有没有ack,1 表示有ack, 0 表示无ack
2.2.2.3 IICADD-从机地址配置
2.2.2.4 IICDS-数据寄存器
3 I2C读写操作流程 The following steps must be executed before any IIC Tx/Rx operations.
Write own slave address on IICADD register, if needed.
Set IICCON register.
Enable interrupt
Define SCL period
Set IICSTAT to enable Serial Output
在操作tx,rx前,要先执行以下几步骤:
IICADD写入从设备地址
设置IICCON,设置时钟,使能中断
设置IICSTAT,使能传输
3.1 I2C操作模式 The S3C2440A IIC-bus interface has four operation modes:— Master transmitter mode — Master receive mode — Slave transmitter mode — Slave receive mode
3.1.1 主发Master/Transmitter Mode
1 2 3 4 5 6 7 8 9 10 11 12 13 1. 配置成master tx mode (也就是IICSTAT[7 :6 ]配置成11 ) 2. 把从设备地址写入IICDS,(第一次传输地址) 3. IICSTAT写入0xF0(使能传输,发S信号,使能tx/rx) 3. IICDS中配置的数据(从设备地址7bit + 读写位1bit)就被发送出去了(每传输完一个数据将产生一个中断) 5. 判断第9个clk从设备是否有ack 5.1 如果从设备有ack,恢复i2c传输 IICDS = buf Clear pending bit 数据被发送出去,继续i2c传输 5.2 如果没有ack, stop,返回错误 IICSTAT = 0xd0 Clear pending bit(IICCON[4 ]) Delay一会儿等待停止条件生效
3.1.2 主收Master/Receiver Mode
1 2 3 4 5 6 7 8 9 10 11 12 13 1. 配置成master rx mode (也就是IICSTAT[7 :6 ]配置成10 ) 2. 把从设备地址写入IICDS,(第一次传输地址) 3. IICSTAT写入0xB0(使能传输) 4. IICDS中配置的数据(从设备地址7bit + 读写位1bit)就被发送出去了(每传输完一个数据将产生一个中断) 5. 判断第9个clk从设备是否有ack 5.1 如果从设备有ack,恢复i2c传输 Buf = IICDS Clear pending bit 数据被接受到,继续i2c传输 5.2 如果没有ack, stop,返回错误 IICSTAT = 0x90 Clear pending bit Delay一会儿
3.1.3 从发Slave/Transmitter Mode
3.1.4 从收Slave/Receiver Mode
4 I2C程序示例 4.1 I2C从设备介绍 IIC控制器只提供了传输数据的能力,至于数据有什么含义,IIC控制器并不知道,数据的含义有外部i2c从设备,我们需要阅读芯片手册,才知道IIC控制器应该发出怎样的数据。
4.1.1 AT24CXX EEPROM AT24Cxx系列EEPROM是由美国Mcrochip公司出品,1-512K位的支持I2C总线数据传送协议的串行CMOS E2PROM。I2c传输规则如下:
4.2 程序框架 我们的程序应该分为两层(IIC设备层,IIC控制器层),框架如下图所示:
1 2 3 4 5 6 7 最上层是i2c_test层,用来对i2c的功能进行测试和验证。 第2层是i2c设备层,用来对具体某一型号的从设备进行i2c读写。 第3层是通用i2c控制器层,用来提供对具体某一型号的i2c主控进行管理操作。 最底层是i2c控制器具体的型号层。
在通用i2c控制层,我们提供一个统一的接口i2c_transfer,不关使用哪个芯片,他最终都会调用i2c_transfer,来选择某一款I2C控制器,把数据发送出去,或者从I2c设备读到数据。这种层次分明的架构是作为软件开发人员必备的素养和技能。这里也是借鉴了linux内核I2C子系统的模型。
4.2.1 i2c_msg结构体 我们借鉴Linux I2C子系统的数据结构定义。对于每一次传输的数据都可以用一个i2c_msg结构体来表示。但是,读某个地址的数据时,就要用两个i2c_msg结构体来描述它,因为一个i2c_msg结构体只能描述一个传输方向(读/写),我们读取ac24ccxx某个地址上的数据时,要先写出要读取的地址,然后来读取设备地址上的数据。
4.2.2 i2c_test.c 1 2 3 4 void i2c_test (void ) { }
这个菜单会调用到at24cxx.c里面的函数进行i2c外设读写。
4.2.3 at24cxx.c 定义描述at24cxx外设,并且实现该外设的操作,里面会使用标准的接口i2c_transfer来启动I2C传输。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #define AT24CXX_ADDR 0x50 int at24cxx_write (unsigned int addr, unsigned char *data, int len) { i2c_msg msg; int i; int err; unsigned char buf[2 ]; for (i = 0 ; i < len; i++) { buf[0 ] = addr++; buf[1 ] = data[i]; msg.addr = AT24CXX_ADDR; msg.flags = 0 ; msg.len = 2 ; msg.buf = buf; msg.err = 0 ; msg.cnt_transferred = -1 ; err = i2c_transfer(&msg, 1 ); if (err) return err; } return 0 ; } int at24cxx_read (unsigned int addr, unsigned char *data, int len) { i2c_msg msg[2 ]; int err; msg[0 ].addr = AT24CXX_ADDR; msg[0 ].flags = 0 ; msg[0 ].len = 1 ; msg[0 ].buf = &addr; msg[0 ].err = 0 ; msg[0 ].cnt_transferred = -1 ; msg[1 ].addr = AT24CXX_ADDR; msg[1 ].lags = 1 ; msg[1 ].len = len; msg[1 ].buf = data; msg[1 ].err = 0 ; msg[1 ].cnt_transferred = -1 ; err = i2c_transfer(&msg, 2 ); if (err) return err; return 0 ; }
4.2.4 i2c_controller.h 1 2 3 4 5 6 7 8 9 10 11 12 typedef struct i2c_msg { unsigned int addr; int flags; int len; int cnt_transferred; unsigned char *buf; }i2c_msg, *p_i2c_msg; typedef struct i2c_controller { int (*int )(void ); int (*master_xfer)(i2c_msg msgs, int num); char *name; }i2c_controller, *p_i2c_controller;
构造i2c_msg和i2c_controller结构。
4.2.5 i2c_controller.c 实现通用i2c控制器管理,用来注册具体i2c控制器,调用具体控制器去做i2c通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #define I2C_CONTROLLER_NUM 10 static p_i2c_controller p_i2c_controllers[I2C_CONTROLLER_NUM];static p_i2c_controller p_i2c_con_selected;void register_i2c_controller (p_i2c_controller *p) { int i; for (i = 0 ; i < I2C_CONTROLLER_NUM; i++) { if (!p_i2c_controllers[i]) { p_i2c_controllers[i] = p; return ; } } } int select_i2c_controller (char *name) { int i; for (i = 0 ; i < I2C_CONTROLLER_NUM; i++) { if (p_i2c_controllers[i] && !strcmp (name, p_i2c_controllers[i]->name)) { p_i2c_con_selected = p_i2c_controllers[i]; return 0 ; } } return -1 ; } int i2c_transfer (i2c_msg msgs, int num) { return p_i2c_con_selected->master_xfer(msgs, num); } void i2c_init (void ) { s3c2440_i2c_con_add(); select_i2c_controller("s3c2440" ); p_i2c_con_selected->init(); }
4.2.6 s3c2440_i2c_controller.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 void i2c_interrupt_func (int irq ) { } void s3c2440_i2c_con_init (void ){ GPECON &= ~((3 <<28 ) | (3 <<30 )); GPECON |= ((2 <<28 ) | (2 <<30 )); IICCON = (1 <<7 ) | (0 <<6 ) | (1 <<5 ) | (30 <<0 ); register_irq(27 , i2c_interrupt_func); } int do_master_tx (p_i2c_msg msg ){ p_cur_msg = msg; msg->cnt_transferred = -1 ; msg->err = 0 ; IICCON |= (1 <<7 ); IICSTAT = (1 <<4 ); IICDS = msg->addr<<1 ; IICSTAT = 0xf0 ; while (!msg->err && msg->cnt_transferred != msg->len); if (msg->err) return -1 ; else return 0 ; } int do_master_rx (p_i2c_msg msg ){ p_cur_msg = msg; msg->cnt_transferred = -1 ; msg->err = 0 ; IICCON |= (1 <<7 ); IICSTAT = (1 <<4 ); IICDS = (msg->addr<<1 )|(1 <<0 ); IICSTAT = 0xb0 ; while (!msg->err && msg->cnt_transferred != msg->len); if (msg->err) return -1 ; else return 0 ; } int s3c2440_master_xfer (p_i2c_msg msgs, int num ){ int i; int err; for (i = 0 ; i < num; i++) { if (msgs[i].flags == 0 ) err = do_master_tx(&msgs[i]); else err = do_master_rx(&msgs[i]); if (err) return err; } return 0 ; } void s3c2440_i2c_con_add (void ){ register_i2c_controller(&s3c2440_i2c_con); } static i2c_controller s3c2440_i2c_con = { .name = "s3c2440" , .init = s3c2440_i2c_con_init, .master_xfer = s3c2440_master_xfer, };
s3c2440_i2c_con_add函数:注册 s3c2440的i2c控制器, 当调用i2c_init就会对选中的这款控制器初始化,也就是调用s3c2440_i2c_con_init。
s3c2440_i2c_con_init函数:
1 2 1 ).IICCON = (0 <<6 ) | (1 <<5 ) | (30 <<0 ); 设置IICCON控制寄存器。选择发送时钟,使能中断。设置ACK应答使能,bit[7 ]。2 ).register_irq(27 , i2c_interrupt_func):注册中断处理函数,当发生I2C中断的时候就会调用i2c_interrupt_func中断处理函数。
s3c2440_master_xfer函数:
当发起i2c传输时,调用i2c_transfer,进而调用s3c2440_master_xfer进行数据传输。写的话do_master_tx,读的话do_master_rx。
do_master_rx函数:
1 IICDS = (msg->addr<<1 )|(1 <<0 ):把从设备地址写入IICDS,前7 位是从机地址,第8 位表示传输方向(0 表示写操作,1 表示读操作)。
do_master_tx函数:
1 2 1. IICDS = msg->addr<<1 : 把从机地址(高7 位,所以需要向右移一位)写入到IICDS寄存器中。2. IICSTAT = 0xf0 :设置IICSTAT寄存器,将s3c2440设为主机发送器,并发出S信号后,紧接着就发出从机地址。后续的传输工作将在中断服务程序中完成。
4.3 程序框架总结 对应程序框架的4层架构。
4.4 I2C中断服务程序 Start信号之后,发出设备地址,在第9个时钟就会产生第一个中断,我们根据i2c的流程图来编写中断程序。每传输完一个数据将又产生一个中断,I2C操作的主体在中断服务程序 ,它可以分为两部分:写操作,读操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 static p_i2c_msg p_cur_msg; int isLastData (void ) { if (p_cur_msg->cnt_transferred == p_cur_msg->len - 1 ) return 1 ; else return 0 ; } void resume_iic_with_ack (void ) { unsigned int iiccon = IICCON; iiccon |= (1 <<7 ); iiccon &= ~(1 <<4 ); IICCON = iiccon; } void resume_iic_without_ack (void ) { unsigned int iiccon = IICCON; iiccon &= ~((1 <<7 ) | (1 <<4 )); IICCON = iiccon; } void i2c_interrupt_func (int irq) { int index; unsigned int iicstat = IICSTAT; unsigned int iiccon; p_cur_msg->cnt_transferred++; if (p_cur_msg->flags == 0 ) { if (p_cur_msg->cnt_transferred == 0 ) { if (iicstat & (1 <<0 )) { IICSTAT = 0xd0 ; IICCON &= ~(1 <<4 ); p_cur_msg->err = -1 ; printf ("tx err, no ack\n\r" ); delay(1000 ); return ; } } if (p_cur_msg->cnt_transferred < p_cur_msg->len) { IICDS = p_cur_msg->buf[p_cur_msg->cnt_transferred]; IICCON &= ~(1 <<4 ); } else { IICSTAT = 0xd0 ; IICCON &= ~(1 <<4 ); delay(1000 ); } } else { if (p_cur_msg->cnt_transferred == 0 ) { if (iicstat & (1 <<0 )) { IICSTAT = 0x90 ; IICCON &= ~(1 <<4 ); p_cur_msg->err = -1 ; printf ("rx err, no ack\n\r" ); delay(1000 ); return ; } else { if (isLastData()) resume_iic_without_ack(); else resume_iic_with_ack(); return ; } } if (p_cur_msg->cnt_transferred < p_cur_msg->len) { index = p_cur_msg->cnt_transferred - 1 ; p_cur_msg->buf[index] = IICDS; if (isLastData()) resume_iic_without_ack(); else resume_iic_with_ack(); } else { IICSTAT = 0x90 ; IICCON &= ~(1 <<4 ); delay(1000 ); } } }
4.4.1 写操作 1 2 3 4 5 6 7 8 9 10 1. p_cur_msg->cnt_transferred初始值为-1(do_master_tx启动时设置)。 2. p_cur_msg->cnt_transferred == 0表示是第一次传输数据完后产生的中断,即发送从设备地址产生的中断。 3. iicstat & (1<<0)表示主机没有接受到ACK信号(即发出的设备地址不存在),需要停止传输。 4. IICSTAT = 0xd0置IICSTAT寄存器的[5]写为0,产生P信号。但是由于这时IICCON[4]仍为1,P信号没有实际发出,当执行IICCON &= ~(1<<4);清除IICCON[4]后,P信号才真正发出。 5. 等待一段时间,确保P信号已经发送完毕。 1).假如if (p_cur_msg->cnt_transferred < p_cur_msg->len)条件成立,表示数据还没有发送完毕,需要继续发送数据。 2).执行IICDS = p_cur_msg->buf[p_cur_msg->cnt_transferred]把要发送的数据写入到IICDS寄存器中,经过执行IICCON &= ~(1<<4);清除中断标志后后,紧接着就自动把数据发送出去了,这将触发下一个中断。 3).如果条件不成立表示数据传输完毕,发出P信号,停止数据的传输。
4.4.2 读操作 见注释。
4.5 测试 4.5.1 i2c_test.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 void do_write_at24cxx (void ) { unsigned int addr; unsigned char str[100 ]; int err; printf ("Enter the address of sector to write: " ); addr = get_uint(); if (addr > 256 ) { printf ("address > 256, error!\n\r" ); return ; } printf ("Enter the string to write: " ); gets(str); printf ("writing ...\n\r" ); err = at24cxx_write(addr, str, strlen (str)+1 ); printf ("at24cxx_write ret = %d\n\r" , err); } void do_read_at24cxx (void ) { unsigned int addr; int i, j; unsigned char c; unsigned char data[100 ]; unsigned char str[16 ]; int len; int err; int cnt = 0 ; printf ("Enter the address to read: " ); addr = get_uint(); if (addr > 256 ) { printf ("address > 256, error!\n\r" ); return ; } printf ("Enter the length to read: " ); len = get_int(); err = at24cxx_read(addr, data, len); printf ("at24cxx_read ret = %d\n\r" , err); printf ("Data : \n\r" ); for (i = 0 ; i < 4 ; i++) { for (j = 0 ; j < 16 ; j++) { c = data[cnt++]; str[j] = c; printf ("%02x " , c); } printf (" ; " ); for (j = 0 ; j < 16 ; j++) { if (str[j] < 0x20 || str[j] > 0x7e ) putchar ('.' ); else putchar (str[j]); } printf ("\n\r" ); } } void i2c_test (void ) { char c; i2c_init(); while (1 ) { printf ("[w] Write at24cxx\n\r" ); printf ("[r] Read at24cxx\n\r" ); printf ("[q] quit\n\r" ); printf ("Enter selection: " ); c = getchar(); printf ("%c\n\r" , c); switch (c) { case 'q' : case 'Q' : return ; break ; case 'w' : case 'W' : do_write_at24cxx(); break ; case 'r' : case 'R' : do_read_at24cxx(); break ; default : break ; } } }