S5PV210的I2C控制器有三个主要的寄存器,分别是I2CCON、I2CSTAT、I2CDS。
SOC内部内置了硬件控制器来控制通讯时序,写软件时只需要向控制器中写入配置值即可,控制器会产生适当的时序在通信线上和对方通信。
1. 结构框图
(1)时钟部分,时钟来源是PCLK_PSYS,经过内部分频最终得到I2C控制器的CLK,通信中这个CLK会通过SCL线传给从设备。
(2)I2C总线控制逻辑(前台代表是I2CCON、I2CSTAT这两个寄存器),主要负责产生I2C通信时序。实际编程中要发送起始位、停止位、接收ACK等都是通过这两个寄存器(背后所代表的电路模块)实现的。
(3)移位寄存器(shift register),将代码中要发送的字节数据,通过移位寄存器变成一个位一个位的丢给SDA线上去发送/接收。
(4)地址寄存器+比较器。本I2C控制器做从设备的时候用。(我没用过,理论分析),这个是用于作为从设备时,配置自己的设备地址以及将主机发送过来的设备地址与自己的地址进行比较时用到的。
2. I2C时钟分析
(1)I2C时钟源头来源于PCLK(PCLK_PSYS,等于65MHz),经过了2级分频后得到的。
(2)第一级分频是I2CCON的bit6,可以得到一个中间时钟I2CCLK(等于PCLK/16或者PCLK/512)。
(3)第二级分频是得到最终I2C控制器工作的时钟,以I2CCLK这个中间时钟为来源,分频系数为[1,16]。
(4)最终要得到时钟是2级分频后的时钟,譬如一个可用的设置是:65000KHz/512/4=31KHz
3. 主要寄存器:I2CCON、I2CSTAT、I2CADD、I2CDS
I2CCON + I2CSTAT:主要用来产生通信时序和I2C接口配置。
I2CADD:用来写自己的slave address(作为从设备时的设备地址)
I2CDS:发送/接收的数据都放在这里(发送/接收的数据寄存器)
I2CCON寄存器:
bit[7]: I2C的ACK使能控制位,我们的使能这个位之后,当需要我们的Soc发送 ACK信号的时候控制器就会自动的发送的ACK。
bit[6]: 原始时钟选择位,也就是一级时钟
bit[5]: I2C中断使能控制位
bit[4]: 中断挂起标志位,可读可写;读:0表示没有中断挂起,1表示有中断挂起;
写:写0清除中断挂起标志,不可写1
bit[3:0]: 二级时钟配置
I2CSTAT寄存器:
bit[7:6]: 工作模式选择位,一共有4中模式:从机接收模式、从机发送模式、主机接收模式、主机发送模式。
bit[5]: 可读可写;读:0表示总线不忙,也就是可以进行操作,1表示忙; 写:写0产 生停止信号,写1产生起始信号。
bit[4]: 使能总线的发送/接收功能
bit[3]: 总线仲裁状态位,0表示总线仲裁成功,1表示总此案仲裁失败,也就是发生了 总线错误。
bit[2]: 作为从设备时状态位,当收到了主设备发过了的起始信号/停止信号时该位清0 ;当作为从机时接收到的地址与自己设备地址相同时,该位置1。
bit[1]: 状态位,I2C地址0状态标志;当检测到起始/停止信号条件时该位为0;受到的 从机的地址为0000000b时该位为1.
bit[0]: 发送数据时是否接收到ACK状态标志位,0表示接收到了ACK,1表示没有接收到 ACK。
4. I2C通信中断请求来源有以下3种:
(1)一个字节的数据发送/接收完毕
(2)如果SoC作为从设备时,主设备发过来的地址跟自己的地址相对应时
(3)总线控制错误,无法识别总线的控制权归谁所有(总线仲裁错误)
5. I2C控制器通信流程
(1)S5PV210的主发送模式流程图
(2)S5PV210的主接收模式流程图
所以在内置了I2C控制器的SoC中编程,只需要按照上图中的流程图的顺序即可完成通信,I2C控制器内部电路模块已经实现了通信时序,不需要去关心这些。
6. 裸机程序设计
I2CDS是串行移位寄存器,它存放需要发送的8位数据,以及接收到的8位串行数据。
I2CCON[7] 设置为1时,当S5PV210接收到一个字节之后,将发送ACK信号,如果此位为0,则发送NoACK信号。
I2CCON[6]和I2CCON[3:0] 用来配置SCL线的时钟频率,最大不应超过100KHz
I2CCON[5] 使能I2C产生中断,I2CCON[4] 中断标志位,如果产生了中断,此位为1,将此位清0将清除中断,I2C控制器才能继续工作。
注意: 就算将I2CCON[5]置为1,使能中断,只要中断控制器里的VICxINTENABLE没有将I2C中断打开,就不会产生I2C中断,也就不需要写中断服务程序。
另外,I2CCON[5]如果没有设置为1,I2CCON[4]中断标志位的状态就不是正确值。所以一般初始化时,都会将I2CCON[5]置为1。
I2CSTAT[7:6] 用来选择I2C控制器的工作状态,
11为master transmit状态,此时它会将I2CDS的数据以串行方式发送到SDA线上去,I2C控制器会自动控制SCL,每个CLK发送1BIT,在第9个CLK读取SDA电平到I2CSTAT[0]
10为master receive状态,此时它会采集SDA线的数据并读到I2CDS寄存器里去,I2C控制器会自动控制SCLK,每个CLK读取1BIT,根据I2CCON[7]在第9个CLK控制SDA电平
如果I2CCON[4]中断标志位为1,那么在transmit状态下向I2CDS写数据之后,I2C控制器是不会将I2CDS的数据发送到SDA线上去的,只有当I2CCON[4]清零后,I2C控制器才会继续工作,此时I2CDS的数据才会被发送出去。同理,如果I2CCON[4]中断标志位为1,那么在receive状态下,I2C控制器也不会读取SDA的数据到I2CDS里,只有当I2CCON[4]清零后,I2C控制器才会继续工作,此时I2C控制器才会采集SDA数据到I2CDS里。
I2CSTAT[5],读取此位可以知道当前I2C总线是否处于BUSY状态,一般向EEPROM写数据之后,需要5-10ms才能烧写完毕,此时I2C总线一直处于BUSY状态。读取此位可以知道数据是否烧写完毕。向此位写1将发送START信号,写0将发送STOP信号。
I2CSTAT[4]是数据输出使能控制位,如果为0则禁止发送数据到SDA,数据读取不受此位的控制,一般将此位一直设定为1
I2CSTAT[0]存放着master transmit状态下,从SDA读取的第9BIT电平状态,读取值为0,则代表slave device发送了ACK,读取值为1,则代表slave device没有发送ACK(此时slave device没有处于正常工作状态下,比如EEPROM刚上电没有完成初始化,EEPROM正常执行烧写,正忙着,没法做出反应)
I2C在什么情况下会发生中断?
1) 在master transmit状态时,向I2CDS写数据,并将I2CCON[4]清零,此时I2C控制器会将I2CDS的8位数据发送到SDA线上,并在第9个CLK时读取SDA电平到I2CSTAT[0],然后产生中断,此时I2CCON[4]会变为1,I2C控制器停止工作。
2) 在master receive状态时,将I2CCON[4]清零,此时I2C控制器会采集SDA线的数据并存放到I2CDS里,并在第9个CLK时,根据I2CCON[7]的设定,控制SDA线为高电平或低电平(即接收完8位数据后,向slave device发送ACK/NoACK),然后产生中断,此时I2CCON[4]会变为1,I2C控制器停止工作。
再来看下START/STOP信号的发送,向I2CSTAT[5]写1将发送START信号,注意此时并不会立即发送START信号,需要将I2CCON[4]清零才会启动START信号的发送,同理,向I2CSTAT[5]写0也不会立即发送STOP信号,需要将I2CCON[4]清零才会启动STOP信号的发送。另外,需要注意的是,START信号不会单独发送,I2C控制器在发送START信号之后,会紧跟着立即发送I2CDS的数据到SDA上去,读完第9个CLK时的SDA电平后产生中断。也就是说,就算将I2CSTAT[7:6]设置为master receive模式,I2CSTAT[5]为1,将I2CCON[4]清零后,虽然模式为receive模式,但是,I2C控制器发送START信号之后,仍会继续将I2CDS的数据transmit出去,接下来才会切换到receive模式。
再次重申一遍,设置I2CSTAT[5]为1之后,不管当前是transmit还是receive模式,都会发送完START信号后,立即发送I2CDS的数据,接下来模式才会生效,transmit数据时,先将数据写入I2CDS,然后将I2CCON[4]清零,此时数据会通过SDA发送出去,receive数据时,先等待I2CCON[4]变为1,也就是通过SDA读入数据产生中断之后,再从I2CDS读取数据,然后再将I2CCON[4]清零,I2C控制器继续从SDA读取数据。如果不再操作I2CSTAT,START/STOP信号就不会继续发送。
向I2CSTAT[5]写0并清零I2CCON[4]之后,才会发送STOP信号,信号发送完毕后不会产生中断。
6.1 IIC初始化
void i2c_init()
{
//1.a 初始化中断
INTPND |= (1<<27);
SRCPND |= (1<<27);
INTMSK &= ~(1<<27); //中断掩码
IICCON |= (1<<5);
//1.b 设置scl时钟
IICCON &= ~(1<<6);
IICCON &= ~(0xf<<0);
IICCON |= (0x5<<0);
//2. 设置IICSTAT
IICCON |= (1<<4);
//3.设置引脚功能
GPECON |= (0x2<<28)|(0x2<<30);
GPEUP |= (0x3<<14);
//4.允许产生ACK
IICCON |= (1<<7);
}
6.2. 写数据设计
void write_byte(unsigned char xchar, unsigned char daddr)
{
//1. 设置处理器为主设备+发送模式
IICSTAT |= (3<<6);
//2. 将从设备的地址写入到IICDS寄存器
IICDS = SLAVE_WRITE_ADD;
IICCON &= ~(1<<4);
//3. 写入0xF0写入IICSTAT
IICSTAT = 0xF0;
//4. 等待ACK的产生
while ((IICCON & (1<<4)) == 0 )
delay(100);
//5.1写入字节的地址到IICDS寄存器
IICDS = daddr;
IICCON &= ~(1<<4);
//5.2等待ACK的产生
while ((IICCON & (1<<4)) == 0 )
delay(100);
//6. 将要传输的字节数据写入IICDS寄存器
IICDS = xchar;
IICCON &= ~(1<<4);
//8. 等待ACk的产生
while ((IICCON & (1<<4)) == 0 )
delay(100);
//9. 写入0xD0到IICSTAT
IICSTAT = 0xD0;
//10. 清除中断
IICCON &= ~(1<<4);
delay(100);
}
6.3. 读数据设计
void read_data(unsigned char *buf, unsigned char daddr, int length)
{
int j =0;
unsigned char unusedata;
//1. 设置为主设备发送模式
IICSTAT |= (3<<6);
//写入从设备地址
IICDS = SLAVE_WRITE_ADD;
IICCON &= ~(1<<4);
//写入0xF0到IICSTAT
IICSTAT = 0xF0;
//等待ACK
while ((IICCON & (1<<4)) == 0 )
delay(100);
//写入eeprom内部地址
IICDS = daddr;
IICCON &= ~(1<<4);
//等待ACK
while ((IICCON & (1<<4)) == 0 )
delay(100);
//设置为主设备接收模式
IICSTAT &= ~(3<<6);
IICSTAT |= (2<<6);
//2.写入从设备地址到IICDS
IICDS = SLAVE_READ_ADD;
IICCON &= ~(1<<4);
//3.写入0xB0到IICSTAT开始接收
IICSTAT = 0xb0;
while ((IICCON & (1<<4)) == 0 )
delay(100);
/*写入设备内部地址*/
IICDS = daddr;
IICCON &= ~(1 << 4);
while((IICCON & (1 << 4)) == 0)
{
delay(100);
}
//丢掉收到的第1个字节
unusedata = IICDS;
IICCON &= ~(1<<4);
while ((IICCON & (1<<4)) == 0 )
delay(100);
for(j=0;j<length;j++)
{
if(j == (length -1))
{
IICCON &= ~(1<<7);
}
//5.1 从IICDS里取出数据
buf[j]=IICDS;
//5.2 清除中断
IICCON &= ~(1<<4);
//4.等待中断
while ((IICCON & (1<<4)) == 0 )
delay(100);
}
//写入0x90到IICSTAT
IICSTAT = 0x90;
// 清除中断
IICCON &= ~(1<<4);
}