IIC介绍
一种总线结构,I2C串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。为了避免总线信号的混乱,要求各设备连接到总线的输出端时必须是漏极开路(OD)输出或集电极开路(OC)输出。
原理简介
如图所示,一个CPU可以挂在多个支持IIC协议的设备,在设备操作时,根据各设备地址的不同从而识别设备。
通俗的理解可以理解为老师传球给学生:
在写任务开始,IIC控制器传出一个开始信号,接着是一个传入设备地址,从机根据自己的地址与主机传来的地址自动匹配,相符合的设备回复一个信号给主机,接着进行数据的传输,传输需要操作的设备存储中的地址,接着再传输数据。
需要注意的是:
写操作时,每一个数据的传输,主机需要根据从机的回应判断是否传输成功。
读操作时,主机需要发给从机一个回应信号从而告诉从机是否传输成功(特别注意:在读操作时,主机接收完最后一个数据时不要发送回应信号)
数据格式
注意上图中设备地址是七位,需要将芯片手册中的地址左移一位,再或上读/写(0/1)组成8位的数据进行传输。
开始信号:SDA数据线拉低,SCL时钟信号保持高电平
在传输时用9个周期传输8个数据,从最高位(MSB)开始传输,第9个周期作为回应信号的周期,在回应信号(ACK)时,SDA拉低,作传输准备,之后会延迟一段时间,这段时间不能使用IIC传输数据。
停止信号:SDA拉高,SCL保持高电平。
双向传输
在主机控制器和从机控制器两端使用开极电路,在不适用时,通过上拉电阻拉高。
写操作
ARM比单片机方便之处在寄存器操作上就能体现,单片机中IIC协议需要我们自己写时序(例如读写时序),而JZ2440中有IICCON,IICDS,IICSTATS等多个寄存器,只需要操作寄存器就能实现读写。
如图:首先第一步:设置IICSTAT第4位为1,表示允许读写位。
第二步:拼凑出1个字节8位的从机地址以及读/写操作
第三步:写入0xf0至IICSTAT寄存
第四步:等待传输,从机回应ACK信号,这时产生中断
如果无ACK信号,停止传输,写入0XD0至IICSTAT寄存器,并清除等待(将IICCON第四位置0)。
如果有ACK信号,再次进行数据填充,释放读写,再次传输,重复第四步,直至数据传输完成。
读操作
读操作与写操作类似,只是变换了传输方向,再次提醒,最后一个数据接收完成后不需要给从机发送ACK信号。
读写字节示意图
程序框架
面向硬件,操作硬件,所有程序一定要做到广泛的适用性,所以抽象出共性。
首先我们要测试,需要一个测试函数i2c_test.c
其次我们测试什么东西?一般习惯以设备名字命名:AT24CXX(某款EEPROM设备),这个文件构造所有 需要传输的数据,按要求将数据编排好,传入下层。
再者我们需要让这个设备区做什么,通过什么去做?所以需要一个控制模块:i2c——controller.c!接收上层的任务,实现对下层的命令布置。这个文件抽象出下层的读写都是传输,所以有一个传输函数;这个控制器需要初始化,所以有一个初始化函数;同时还需要满足不同的开发设备的需要,所以抽象出一个开发设备的名字。
最后是做什么:读数据,写数据!这个文件包括具体的读写操作,以及所有时序,中断的函数。
构建结构体
一个数据包里面需要地址,数据,读/写操作。
控制寄存器需要抽象两个函数(初始化和传输函数)以及一个名字(用于将下层的控制器名字与上层传来的需要操作的控制器名字作比对)
从底层开始向上
首先在上层中的初始化函数
void s3c2440_i2c_con_init(void)
{
/* 配置引脚用于I2C*/
GPECON &= ~((3<<28) | (3<<30));
GPECON |= ((2<<28) | (2<<30));
/* 设置时钟 */
/* [7] : IIC-bus acknowledge enable bit, 1-enable in rx mode(接收数据时需要使用)
* [6] : 时钟源, 0: IICCLK = fPCLK /16; 1: IICCLK = fPCLK /512
* [5] : 1-enable interrupt
* [4] : 读出为1时表示中断发生了, 写入0来清除并恢复I2C操作(用来判断中断状态的)
* [3:0] : Tx clock = IICCLK/(IICCON[3:0]+1).
* Tx Clock = 100khz = 50Mhz/16/(IICCON[3:0]+1)--->IICCON[3:0] = 30
*/
IICCON = (1<<7) | (0<<6) | (1<<5) | (30<<0);
/* 注册中断处理函数 */
register_irq(27,i2c_interrupt_func);
}
需要关注CPU中是那两条线链接的SDA和SCL.
其次是打开IIC控制器相应的位,一边中断产生进行处理。
最后需要注册中断函数,中断处理函数命令为i2c_interrupt_func()
最底层函数对EEPROM设备进行直接操作以及中断处理
传输函数
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)/* write */
err = do_master_tx(&msgs[i]);
else
err = do_master_rx(&msgs[i]);
if (err)
return err;
}
return 0;
}
通过判定相应的flag位识别是读操作还是写操作
写操作
int do_master_tx(p_i2c_msg msg)
{
p_cur_msg = msg;
msg->cnt_transferred = -1;
msg->err = 0;
/*设置寄存器启动传输*/
/*1.配置为 Master Tx mode(3<<6) 用到IICSTAT 之后会设置0XF0所以这一步不用写*/
IICCON |= (1<<7);/*tx mode在ACK周期释放SDA*/
IICSTAT = (1<<4);
/*2.写入设备地址 IICDS */
IICDS = (msg->addr<<1) | (0<<0); /*0表示写 1表示读*/
/*3.写0xF0到 IICSTAT*/
IICSTAT = 0xf0;
/*4.等待数据传输完成,产生中断*/
while (!msg->err && msg->cnt_transferred != msg->len);
if (msg->err) /*如果是错误退出则错误返回*/
return -1;
else
return 0;
}
msg->cnt_transferred 用于判定是否是第一次中断,因为第一次中断时地址。
msg->err用于判定传输是否正常结束。
while (!msg->err && msg->cnt_transferred != msg->len);在传输中需要判定是否传输长度已经大于要求的长度。
7位的设备地址加上一位的读写组成8位的数据。
读操作
int do_master_rx(p_i2c_msg msg)
{
p_cur_msg = msg;
msg->cnt_transferred = -1;
msg->err = 0;
/*设置寄存器启动传输Rx mode*/
/*1.配置为 Master Rx mode */
IICCON |= (1<<7);/*rx mode在ACK周期回应ACK*/
IICSTAT = (1<<4);
/*2.写入设备地址 IICDS */
IICDS = (msg->addr<<1) | (1<<0); /*0表示写 1表示读*/
/*3.写0xB0到 IICSTAT*/
IICSTAT = 0xb0;
/*4.等待数据传输完成,产生中断*/
while (!msg->err && msg->cnt_transferred != msg->len);
if (msg->err) /*如果是错误退出则错误返回*/
return -1;
else
return 0;
}
重中之重—中断处理函数
没传输完一个数据,就会产生一个中断,无论是设备地址,存储地址,还是数据都会产生中断,所以我们需要针对三种格式,在中断中识别,并且给与相应的处理方法
void i2c_interrupt_func(int irq)
{
int index;
unsigned int iicstat = IICSTAT;
unsigned int iiccon;
/*每传输完一个数据将产生一个中断*/
p_cur_msg->cnt_transferred++; /*用于判断是否为第一次进入 0表示第一次*/
/*对于每次传输,第一个中断是“已经发出了的设备地址”*/
printf("123\n\r");
if (p_cur_msg->flags == 0) /* write */
{
/* 对于第1个中断, 它是发送出设备地址后产生的
* 需要判断是否有ACK
* 有ACK : 设备存在
* 无ACK : 无设备, 出错, 直接结束传输
*/
/*首先判定是否有ACK*/
if (p_cur_msg->cnt_transferred == 0) /* 第1次中断 */
{
if (iicstat & (1<<0)) /*无ACK*/
{
/*停止传输*/
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 /* read */
{
/* 对于第1个中断, 它是发送出设备地址后产生的
* 需要判断是否有ACK
* 有ACK : 设备存在
* 无ACK : 无设备, 出错, 直接结束传输
*/
/*首先判定是否有ACK*/
if (p_cur_msg->cnt_transferred == 0) /* 第1次中断 */
{
if (iicstat & (1<<0))
{ /* no ack */
/* 停止传输 */
IICSTAT = 0x90;
IICCON &= ~(1<<4);
p_cur_msg->err = -1;
printf("rx err, no ack\n\r");
delay(1000);
return;
}
else /* ack */
{
/* 如果是最后一个数据, 启动传输时要设置为不回应ACK */
/* 恢复I2C传输 */
if (isLastData())
{
resume_iic_without_ack();
}
else
{
resume_iic_with_ack();
}
return;
}
}
/* 非第1个中断, 表示得到了一个新数据
* 从IICDS读出、保存
*/
if(p_cur_msg->cnt_transferred < p_cur_msg->len)
{
index = p_cur_msg->cnt_transferred - 1;
p_cur_msg->buf[index] = IICDS;
/* 恢复I2C传输 */
/* 如果是最后一个数据, 启动传输时要设置为不回应ACK */
/* 恢复I2C传输 */
if (isLastData())
{
resume_iic_without_ack();
}
else
{
resume_iic_with_ack();
}
}
else
{
/*发出停止信号*/
IICSTAT = 0X90;
IICCON &= ~(1<<4); /*清除等待*/
delay(1000);
}
}
}
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); /* 回应ACK */
iiccon &= ~(1<<4); /* 恢复IIC操作 */
IICCON = iiccon;
}
void resume_iic_without_ack(void)
{
unsigned int iiccon = IICCON;
iiccon &= ~((1<<7) | (1<<4)); /* 不回应ACK, 恢复IIC操作 */
IICCON = iiccon;
}
对于这个函数,在其中需要注意的读写操作不一样,退出的方法也不一样。
所以首先是确定是这一次中断产生之前的操作时读还是写。
写:
需要判定是否是第一次中断,在第一次中断中,我们写入的是设备地址,设备回应表示有这个设备,没有回应表示没有设备,没有ACK信号,需要直接采取退出处理,使错误为-1,并清除中断等待模式。
在确定是第二个中断及之后时,我们在确定是否已经传输完成即达到预定的长度,若没有达到,将当前选中的结构体中的buf数据进行地质加加处理即填入下一个数据,并关闭清除中断等待进行下一次传输;若达到饱和,就正常退出(无错误位设置)。
读:
需要判定是否是第一次中断,在第一次中断中,我们写入的是设备地址,设备回应表示有这个设备,没有回应表示没有设备,没有ACK信号,需要直接采取退出处理,使错误为-1,并清除中断等待模式; 有ACK信号时需要注意的是需要判定是否下一个数据是最后一个数据,如是最后一个数据,主机不用回复ACK信号,其余时需要回复ACK信号。
在确定是第二个中断及之后时,需要注意的是需要判定是否下一个数据是最后一个数据,如是最后一个数据,主机不用回复ACK信号,其余时需要回复ACK信号。在读操作我们需要传入一个结构,每次讲IICDS传回的数据读走,然后进行清中断的处理。
底层之上的控制控制函数
接受上层的函数调用,将控制器初始化,以及选用相应的控制器。
初始化
void i2c_init(void)
{
/*注册下面的I2C控制器*/
s3c2440_i2c_con_add();
/*选择某款I2C控制器*/
select_i2c_controller("s3c2440");
/*调用它的init函数*/
p_i2c_con_selected->init();
}
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,
};
void s3c2440_i2c_con_add(void)
{
register_i2c_controller(&s3c2440_i2c_con);
}
控制器会对下层的各个设备进行
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;
}
}
}
注册之后,通过对上层传入的设备名字,进行设备的选择。
这里本来是会进行名称的填入,是通过用户的选择,但是现在只有一个设备,所以就直接写成了s3c2440。
/*根据名字来选择某款i2c控制器*/
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;
}
如果有设备,返回0,没有这个设备,返回-1进行错误处理。
选择设备之后,调用相应设备的初始化函数
void s3c2440_i2c_con_init(void)
{
/* 配置引脚用于I2C*/
GPECON &= ~((3<<28) | (3<<30));
GPECON |= ((2<<28) | (2<<30));
/* 设置时钟 */
/* [7] : IIC-bus acknowledge enable bit, 1-enable in rx mode(接收数据时需要使用)
* [6] : 时钟源, 0: IICCLK = fPCLK /16; 1: IICCLK = fPCLK /512
* [5] : 1-enable interrupt
* [4] : 读出为1时表示中断发生了, 写入0来清除并恢复I2C操作(用来判断中断状态的)
* [3:0] : Tx clock = IICCLK/(IICCON[3:0]+1).
* Tx Clock = 100khz = 50Mhz/16/(IICCON[3:0]+1)--->IICCON[3:0] = 30
*/
IICCON = (1<<7) | (0<<6) | (1<<5) | (30<<0);
/* 注册中断处理函数 */
register_irq(27,i2c_interrupt_func);
}
顶层函数—接收用户的测试指令,进行数据的填充
读操作:
int at24cxx_read(unsigned int addr, unsigned char *data, int len)
{
i2c_msg msg[2];
int err;
/* 构造i2c_msg */
msg[0].addr = AT24CXX_ADDR;
msg[0].flags = 0; /* write */ /* 写进读数据的首地址*/
msg[0].len = 1;
msg[0].buf = &addr;
msg[0].err = 0;
msg[0].cnt_transferred = -1;
msg[1].addr = AT24CXX_ADDR;
msg[1].flags = 1; /* read */
msg[1].len = len;
msg[1].buf = data;
msg[1].err = 0;
msg[1].cnt_transferred = -1;
/* 调用i2c_transfer */
err = i2c_transfer(&msg, 2);
if (err)
return err;
return 0;
}
将设备地址,从哪个笛子开始读,读多少长度等数据填入结构体,并传入下层。
写操作:
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++; /*写进AT24CXX中的哪个地址*/
buf[1] = data[i]; /*传输的数据*/
/*构造I2C_MSG*/
msg.addr = AT24CXX_ADDR; /*设备地址*/
msg.err = 0;
msg.flags = 0; /* write*/
msg.cnt_transferred = -1;
msg.buf = buf;
msg.len = 2;
/*调用i2c_transfer*/
err = i2c_transfer(&msg, 1);
if (err)
{
return err;
}
}
return 0;
}
这里有一点可能会混淆,为什么写入的设备存储中的地址是在数据buf中,这里因为EEPROM设备会识别相应的数据,并且有读写位 设置,所以EEPROM不会混淆相应的处理。
读写操作数据填充完毕后会调用传输函数:
int i2c_transfer(p_i2c_msg msgs, int num)
{
return p_i2c_con_selected->master_xfer(msgs, num);
}
注意master_xfer只是结构中的一个函数名字,相当于结构体中的成员名字。而实际调用的master_xfer=函数,也是我们相应设备中实际的传输函数
static i2c_controller s3c2440_i2c_con = {
.name = "s3c2440",
.init = s3c2440_i2c_con_init,
.master_xfer = s3c2440_master_xfer,
};
s3c2440_master_xfer()这个才是真正进行处理的函数
测试函数
1.打印出测试菜单
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);
/* 测试内容:
* 3. 编写某个地址
* 4. 读某个地址
*/
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;
}
}
}
调用初始化函数,将下面的各层的控制器读入,注册相应的设备。
通过不同的选择执行不同的函数
读:
void do_read_at24cxx(void)
{
unsigned int addr;
int i, j;
unsigned char c;
unsigned char str[16];
unsigned char data[100];
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;
}
/*
while(addr > 256)
{
printf("the address is erro(0-256,please re-input:");
printf("Enter the address to read: ");
addr = get_int();
}
*/
/* 获得长度 */
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");
/* 长度固定为64 */
for (i = 0; i < 4; i++)
{
/* 每行打印16个数据 */
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");
}
}
每次打印64个字节,可视字符正常打印,不可视字符按‘ . ’处理
写:
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);
}
原理与读操作一样,只是在写操作,机器会默认给字符串加上‘\0’的结束符,所以传输长度需要+1!
总结
ARM的操作比单片机简单,但是难点在于思路的清晰以及逻辑的清楚,需要程序员时刻记住自己在哪一层,我的目的是什么!我的条件是什么!