11-I2C

声明:

本文是本人在韦东山笔记的基础上加了一些注释,方便理解。原文地址:

目录

  • 声明:
  • 第001节_I2C协议与EEPROM
    • 1.1 I2C协议原理
      • 1.1.1 IIC传输数据的格式
  • 第002节_S3C2440的I2C控制器
      • ICCON寄存器(Multi-masterIIC-buscontrol)
      • IICSTAT寄存器(Multi-masterIIC-buscontrol/status)
      • IICADD寄存器(Multi-masterIlC-busaddress)
      • IICDS寄存器(Multi-masterIIC-busTx/Rxdatashift)
  • 第003节_程序框架
    • 3.1 IIC控制器的功能
    • 3.1 程序框架
      • i2c_test.c文件
      • at24cxx.c文件
      • i2c_controller.c文件
      • s3c2440_i2c_controller.c文件
  • 第004节_I2C控制器编程_框架
      • 2c_controller.h文件
      • i2c_controller.c文件
      • s3c2440_i2c_controller.c文件
  • 第005节_I2C控制器编程_中断
  • 第006节_EEPROM编程和测试的代码
    • 6.1 从设备的读写函数
    • 6.2 测试代码
  • 第007节_测试
    • 7.1问题一
    • 7.2问题二

第001节_I2C协议与EEPROM

1.1 I2C协议原理

I2C在硬件上的接法如下图所示,主控芯片(ARM)引出SCL、SDA线两条线,在一条I2C总线上可以接很多I2C设备(各设备如下图所示进行连接),我们还会放一个上拉电阻(放一个上拉电阻的原因以后我们再说)。
11-I2C_第1张图片
我们怎么传输数据,我们需要发数据从主设备发送到从设备上去,也需要把数据从从设备传送到主设备上去,数据涉及到双向传输。

1.1.1 IIC传输数据的格式

1.写操作的格式(或者说是步骤):
刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(即表达你读或写,0表示写,1表示读)。

回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。

每传输一个数据,接受方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号。
11-I2C_第2张图片
2.读操作的格式(或者说是步骤):
刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是从哪一个芯片读取数据),方向(读/写,0表示写,1表示读)。

回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后在传输下一个数据。

每传输一个数据,接受方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号。
11-I2C_第3张图片
注:不管是写还是读操作,设备地址加方向的位数是8位,而数据也是8位的。也就是说传输是以8位进行传输的。设备地址加方向其实也是数据。

传输是以8位为单元数据传输的,先传输最高位(MSB),主芯片发出start信号之后,然后发出9个时钟去传输数据(前8个时钟的每个时钟传输一位数据,第9个时钟用于等待(发出)回应位。即9个时钟用于发送8位数据)。

下面看一下IIC总线的传输过程图:
(1)开始信号(S):SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
(2)结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
(3)响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA
另外需要知道的是:SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化。
11-I2C_第4张图片
好了,看了前面的知识,我们有几个问题:
1.问题:如何在SDA上实现双向传输(IIC为半双工,即要么主到从,要么从到主)?
答:主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚)。

2.问题:主设备(从设备)发送数据时,从设备(主设备)不应该发送(半双工嘛),怎么做到的?
答:通过SCL进行控制的。比如前8个时钟的每个时钟传输一位数据,第9个时钟用于等待(发出)回应位。

3.问题:主设备(从设备)发送数据时,从设备(主设备)的发送引脚的存在 不影响 主设备(从设备)数据的发送,怎么做到呢?
答:里面放一个三极管,使用开极(极电集开发出去作为输出)电路,如下图:
答:如果A要发送数据,则只需要让B输出低电平(B是三极管,则不导通)就失效了。
11-I2C_第5张图片
好,让接收端的三极管失效解决了。那么A呢,怎么输出高低电平的?
答:首先你看,我们的数据在SDA线上无非就是0或1吧。
在看下面这个图:
并且你要知道的是,在上面这个图的SDA线上有一个上拉电阻,使得AB都为1时SDA为1。而这就是上拉电阻的作用。
也就是说让A输出0,则SDA输出1;
让A输出1,则SDA输出0。
好,这个问题解决了。同样B传输数据A接收数据的时候反过来。
11-I2C_第6张图片
好,举个例子吧,从下面的例子可以看看数据是怎么传的(实现双向传输),比如:
主设备发送(8bit)给从设备 :

1.前8个clk
	从设备不要影响,从设备不驱动三极管;
	主设备决定数据;
2.第9个clk,由从设备决定数据
	主设备不驱动三极管;
	从设备决定数据;

从上面的例子,就可以知道,怎样在一条线上实现,双向传输的办法。这就是为什么在SDA,SCL上放上拉电阻的原因。

补充一点:看从这往上数第三幅图的数据传输图,在第9个时钟之后,如果有某一方处于繁忙状态,它可以一直把SCL拉低,当SCL为低电平时候,大家都不应该使用IIC总线,只有当SCL从低电平变为高电平的时候,IIC总线才能被使用。

说明一点:从这往上数第三幅图我们也可以知道ACK信号应该是低电平。主设备不驱动三极管,如果从设备不驱动三极端的化SDA应该是高电平,当从设备接收数据之后,发出回应信号的时候,就会驱动三极管,让SDA变为低电平。所以说:ACK信号是低电平。

再说明一点:对于IIC协议它只能规定怎么传输数据,数据什么含义它完全不能够控制,数据的含义由从设备决定。

第002节_S3C2440的I2C控制器

在嵌入式系统里面的主控芯片一般都会有I2C控制器,要是没有可以根据I2C协议用GPIO管脚模拟,但是非常麻烦,先不用管这个。我们来看有I2C控制器的情况:
我们要发送数据时,可以把数据放到I2C控制器的某个寄存器,它就会自动的发出时钟,并且把数据发送给从设备,同时会等待从设备会返回回应信号。
当我们想接收一个数据的时候,要设置某个寄存器启动传输,它也一样会产生时钟,然后从设备就会把数据通过SDA传到I2C控制器里面,组装进某个寄存器里面,最终寄存器会把接收到的8位数据返回给我们的程序。
从这里可以看到I2C控制器简化了I2C的操作。
简短电路连接图,如图:
11-I2C_第7张图片
好,来看下面这个图,其是I2C控制器的内部结构原理图
根据下图,我们首先设置IICCON(来设置时钟),时钟源是PCLK(是50MHZ),但其太快了,我们需要设置这个分频系数,把时钟降低,降低到我们想要的SCL。
然后我们要发出start信号,我们需要设置寄存器IICSTAT发出start信号。
之后我们需要发出数据啊,我们的程序可以把数据写入到IICDS寄存器,一写入就会自动的发出时钟,并且把这8位数据从SDA发送给从设备。
数据发送之后,在第九个时钟会收到回应信号,可以查询IICSTAT是否有ACK,有ACK表示数据发送成功了,可以继续发送数据。
等发完数据之后,再来设置IICSTAT让它发出P信号。
11-I2C_第8张图片
好,再往下看。
我们知道,在第九个CLK,就会产生一个中断,在中断处理过程中SCL被拉为低电平,谁都不能再使用IIC总线,等待中断处理完成。那么怎样处理中断,或者说这个ACK中断是干嘛的?

写操作:
	若无ACK,则说明出错,然后发出P信号结束。
	若有ACK信号,则表示上一个字节成功发送出去。
		若仍有数据你想要去发送,则写入IICDS寄存器,然后清中断,一清中断就会释放SCL信号,继续发出时钟,把数
		据再次发送出去。
		若没有数据想发送了,你就发出P信号结束。
读操作:
	读到8位数时,应该回应给设备一个ACK信号。
	如果还想读数据,就先清中断,从而启动了传输。等它再次发生ACK中断时,再来读取IICDS寄存器,得到数据。
	不想读取数据的话,就发出P信号结束。

重点: 发生中断时,我们的IIC控制器会把SCL拉低,阻止任何设备再使用IIC总线,清中断之后才能继续使用,这种机制
就给我们中断服务程序的执行提供了时间。

那么,我们想要发送或接收,我们具体怎么操作,如下:


在发送模式:
	1.往寄存器IICDS寄存器放入一个val值。
	2.发完,产生中断,并且会把 SCL拉低。
	3.在中断程序里,你去编程判断是否要继续发送数据,,如果想继续发送则往IICDS里面写入下一个数据,一旦写入
	下一个数据,则IIC继续操作,若再次发完,就会再次产生中断。

在接受模式:
	1.你写的程序发起传输,去接受数据。
	2.接收到数据之后,产生中断,SCL被拉低。
	3.中断程序里,判断数据是否要继续接受等,如果还想继续接受的话,再次设置,设置好之后去读IICDS寄存器,一但读出来IIC则继续接受下一个数据,收到新数据之后,又会产生一个中断(就是这样循环操作)。

下面是IIC的几个重要寄存器:

ICCON寄存器(Multi-masterIIC-buscontrol)

IICCON寄存器用于控制是否发出ACK信号、设置发送器的时钟、开启,i2c中断,并标识中断是否发生。它的各位含义如表:
使用IICCON寄存器时,有如下注意事项。
1.发送模式的时钟频率由位[6]、位[3:0]联合决定,另外,llCCON[6]=0,IICCON[3:0] 不能取0或10
2.12c中断在以下3种情况下发生:当发出地址信息或接收到一个从机(从机及所说的外部设备)地址并且吻合时、当总线仲裁失败时、当发送/接收完一个字节的数据(包括响应位)时。
3.基于SDA、SCL线上时间特性的考虑,要发送数据时,先将数据写入IICDS寄存器,然后再清除中断。
4.如果IICCON[5]=0,IICCON[4]将不能正常工作。所以,即使不使用12c中断,也要将IICCON[5]设为1。
11-I2C_第9张图片

IICSTAT寄存器(Multi-masterIIC-buscontrol/status)

IICSTAT寄存器用于选择12c接口的工作模式,发出S信号、P信号,使能接收/发送功能,并标识各种状态,比如总线仲裁是否成功、作为从机时是否被寻址、是否接收到0地址、是否接收到ACK信号等。IICSTAT寄存器的各位如表:

IICADD寄存器(Multi-masterIlC-busaddress)

用到IICADD寄存器的位[7:11],表示从机地址。IICADD寄存器在串行输出使能位 IICSTAT[4]为0时,才可以写入:在任何时间都可以读出。IICADD寄存器的各位如表:
11-I2C_第10张图片

IICDS寄存器(Multi-masterIIC-busTx/Rxdatashift)

用到IICDS寄存器的位[7:0],其中保存的是发送或己经接收的数据。
IICDS寄存器在 串行输出使能位IICSTAT[1](?????)为1时,才可以写入;
在任何时间都可以读出。
IICDS寄存器的各位如表:
11-I2C_第11张图片
再往下看芯片手册,是读写操作流程图,我们编程按照这个来就行:
读写操作流程图

主机发送器模式操作:
11-I2C_第12张图片
主机接收器模式操作:
11-I2C_第13张图片

第003节_程序框架

写程序之前 考虑好程序的框架,我们想写出一个结构比较好,比较容易扩展的程序。因此我们先要考虑清楚框架的设计。

3.1 IIC控制器的功能

IIC会做什么事情呢?
答:对于IIC控制器,它负责传输数据,不知道数据的含义,但是它要实现写 / 读操作,如下。
读操作 :
11-I2C_第14张图片
写操作 :11-I2C_第15张图片

很显然,IIC控制器提供了传输数据的能力,至于数据有什么含义,IIC控制器并不知道,数据的含义由外接的IIC芯片决定
我们需要阅读AT24cxx芯片手册,才知道IIC控制器应该发出怎样的数据,
AT24cxx的操作方法:
11-I2C_第16张图片
11-I2C_第17张图片

3.1 程序框架

现在来写框架
11-I2C_第18张图片
我们提供一个统一的接口i2c_transfer,不关使用哪个芯片,他最终都会调用i2c_transfer,来选择某一款I2C控制器,把数据发送出去,或者从I2c设备读到数据。
对于每一次i2c_transfer()传输的数据都可以用一个i2c_msg结构体来表示。但是值得注意的是,某个地址的数据时要用两个i2c_msg结构体来描述它,这是因为一个i2c_msg结构体只能描述一个传输方向(读/写),我们读取ac24ccxx某个地址上的数据时,要先写出要读取的地址,然后来读取设备地址上的数据。

我们想设计出以一个结构体比较容易扩展的框架。对于具体型号的I2C控制器(如2440的)我们要抽象出一个结构体i2c_controller,我们构造这个结构体之后,把这个这个结构体,告诉上层(I2C控制器那一层),上层有个管理者i2c_contreller.c文件。

我们在s3c2440_i2c_controller.c这个文件中我们构造出一个i2c_controller结构体,把它放入上层文件中的数组里,以后就根据结构体的名字,把这个结构体取出来使用。(这样做的好处是,如果你有多种型号(如2440)的板子,以后每种型号直接存入数组就行)。
假设我们有一个TI的开发板,在ti_i2c_controller.c文件中,也要构造出一个i2c_controller结构体,同样们也会把这个结构体放入上层的结构体数组(i2c_contreller.c文件中)中,以后根据名字先出来使用。

对于设备层中的at24cxx芯片我们写出at24cxx.c文件在这个文件实现读写函数:
1.at24cxx_write函数
2.at24cxx_read。
读写函数都会调用i2c_transfer发起IIC传输,所以我们写程序的时候主要的暂时会涉及到三个文件:
at24cxx.c, s3c2440_i2c_controller.c,i2c_contreller.c。
在最上层会写出一个i2c_test.c文件,它会提供菜单供我们选择来测试。

下面我们自顶而下地写一个程序框架,涉及到的文件有:i2c_test.c、at24cxx.c、i2c_controller.c、s3c2440_i2c_controller.c

i2c_test.c文件

该文件的内容如下:

void i2c_test(void)
{
	/* 初始化: 选择I2C控制器 */

	/* 提供菜单供测试 */
}

这个菜单最终会调用到at24cxx.c里面的函数。

at24cxx.c文件

读写函数会使用标准的接口i2c_transfer来启动I2C传输。该文件的内容如下:

int at24cxx_write(unsigned int addr, unsigned char *data, int len)
{
	/* 构造i2c_msg */

	/* 调用i2c_transfer */
}

int at24cxx_read(unsigned int addr, unsigned char *data, int len)
{
	/* 构造i2c_msg */

	/* 调用i2c_transfer */
}

i2c_controller.c文件

该文件的内容如下:

/* 注册---有一个i2c_controller数组用来存放各种不同芯片的操作结构体 */
void register_i2c_controller()
{
}

/* 根据名字来选择某款I2C控制器 */
void select_i2c_controller(char *name)
{
}

/* 实现 i2c_transfer 接口函数 */
int i2c_transfer(i2c_msg msgs, int num)
{
}

select_i2c_controller函数根据名字来选择某款I2C控制器后,以后就会使用被选择的I2C控制器来启动传输。

有数组一定有注册函数register_i2c_controller会把下面实现的I2C控制器结构体i2c_controller放到i2c_controller数组里面。

s3c2440_i2c_controller.c文件

对于具体的芯片,要实现自己的i2c_controller。该文件的内容如下:

/* 实现i2c_controller结构体
          .init
          .master_xfer
          .name
 */

第004节_I2C控制器编程_框架

我们现在来讲I2C控制器怎么写,它是I2C程序中最核心的地方,我们要先构造几个结构体,这几个结构体放在i2c_controller.h里面。

2c_controller.h文件

我们要发出I2c传输时,要构造出i2c_msg,把构造出的i2c_msg扔给下面的i2c_controller.c,i2c_controller.c会选择某一个i2c控制器,使用里面的master_xfer来传输数据, 所以我们需要构造出一个i2c_controller结构体。
2c_controller.h文件如下:

#ifndef _I2C_CONTROLLER_H
#define _I2C_CONTROLLER_H

typedef struct i2c_msg {
	unsigned int addr;  /* 7bits */
	int flags;  /* 0 - write, 1 - read */
	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;


#endif /* _I2C_CONTROLLER_H */
解析:我们构造这两个结构体,我们要把它放在i2c_controller.c把它用起来,

i2c_controller.c文件

include "i2c_controller.h"

#define I2C_CONTROLLER_NUM 10 //10种芯片

/* 有一个i2c_controller数组用来存放各种不同芯片的操作结构体 */
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;
		}
	}
}
解析:register_i2c_controller函数用于把参数中的结构体指针,注册到p_i2c_controllers指针数组中。

/* 根据名字来选择某款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;
}
解析:select_i2c_controller函数根据参数中的名字(name) 从p_i2c_controllers指针数组中取出对应的结构体指
针复制给p_i2c_con_selected结构体指针(静态全局变量)/* 实现 i2c_transfer 接口函数 */
int i2c_transfer(i2c_msg msgs, int num)
{
	return p_i2c_con_selected->master_xfer(msgs, num);
}
解析:i2c_transfer接口函数,调用选择的p_i2c_con_selected成员中master_xfer函数。


void i2c_init(void)
{
	/* 注册下面的I2C控制器 */
	s3c2440_i2c_con_add();

	/* 选择某款I2C控制器 */

	/* 调用它的init函数 */
}
解析:s3c2440_i2c_con_add()函数,用于把定义的s3c2440_i2c_con结构体(在s3c2440_i2c_controller.c文件
中)注册到p_i2c_controllers数组中。

s3c2440_i2c_controller.c文件

我们定义一个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_con结构体注册到上层的i2c_controller数组中:

void s3c2440_i2c_con_add(void)
{
	register_i2c_controller(&s3c2440_i2c_con);
}
注意了:s3c2440_i2c_con_add()在i2c_controller文件的i2c_init()函数中被引用。其实就是把2440地结构体注册
到i2c_controller文件的数组中。

好,现在来写s3c2440_i2c_con结构体中的初始化函数s3c2440_i2c_con_init():

s3c2440_i2c_con_init函数,用来初始化I2C,控制器代码如下:
void s3c2440_i2c_con_init(void)
{
	/*IICCON寄存器设置*/ 
	/* 设置时钟 */
	/* [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)。注100hz是根据外设芯片手册查到的
	 */
	IICCON = (0<<6) | (1<<5) | (30<<0);//30是根据公式算出的

	/* 注册中断处理函数 */
	register_irq(27, i2c_interrupt_func);
}
解析:
1).IICCON = (0<<6) | (1<<5) | (30<<0); 设置IICCON控制寄存器。选择发送时钟,使能中断。
2).register_irq(27, i2c_interrupt_func):注册中断处理函数,当发生I2C中断的时候就会调用
i2c_interrupt_func中断处理函数。

中断服务函数,当发成中断时,就会调用中断服务函数,代码如下:

void i2c_interrupt_func(int irq)
{
	/* 每传输完一个数据将产生一个中断 */

	/* 对于每次传输, 第1个中断是"已经发出了设备地址" */
}

下载再来写s3c2440传输函数,作用是:根据标志位flags,来指明是读还是写(1:读 0:写)。代码如下:

int s3c2440_master_xfer(p_i2c_msg msgs, int num)
{
	int i;
	for (i = 0; i < num; i++)	//对每一个msg逐个操作
	{
		if (msgs[i].flags == 0)/* 说明就是write */
			do_master_tx(&msgs[i]);    //do_master_tx()函数在下面实现
		else                     /*读*/
			do_master_rx(&msgs[i]);    //do_master_rx()函数在下面实现
	}
}

初始化完成后,s3c2440_master_xfer()就可以调用do_master_tx写I2C从机了,这个函数仅仅启动I2C传输,然后等待,直到数据在中断服务程序中传输完毕后再返回。函数代码如下:

void do_master_tx(p_i2c_msg msg)
{
	msg->cnt_transferred = 0;/*cnt_transferred是表示传输的字节数的,当
	cnt_transferred等于传输长度len时传输完毕*/
	
	/* 设置寄存器启动传输。看2440写操作流程图(本笔记上面有)。从流程图得知有以下三个步骤 */
	/* 1. 配置为 master tx mode */
		//这一步,3步骤的IICSTAT = 0xf0;已经设置了
	/* 2. 把从设备地址写入IICDS */
	IICDS = msg->addr<<1;
	
	/* 3. IICSTAT = 0xf0 , 数据即被发送出去, 将导致中断产生 */
	IICSTAT = 0xf0;


	/* 后续的传输由中断驱动 */

	/* 循环等待中断处理完毕 */
	while (msg->cnt_transferred != msg->len);
}
解析:
1).IICDS = msg->addr<<1: 把从机地址(高7位,所以需要向右移一位)写入到IICDS寄存器中。
2).IICSTAT = 0xf0:设置IICSTAT寄存器,将s3c2440设为主机发送器,并发出S信号后,紧接着就发出从机地址。后续的传输工作将在中断服务程序中完成。

do_master_rx函数的实现和do_master_tx函数类似,代码如下:

void do_master_rx(p_i2c_msg msg)
{
	msg->cnt_transferred = 0;
	
	/* 设置寄存器启动传输 */
	/* 1. 配置为 Master Rx mode */
		
	/* 2. 把从设备地址写入IICDS */
	IICDS = (msg->addr<<1)|(1<<0);
	
	/* 3. IICSTAT = 0xb0 , 从设备地址即被发送出去, 将导致中断产生 */
	IICSTAT = 0xb0;
	

	/* 后续的传输由中断驱动 */

	/* 循环等待中断处理完毕 */
	while (msg->cnt_transferred != msg->len);
}
解析: 1).IICDS = (msg->addr<<1)|(1<<0):把从设备地址写入IICDS,前7位是从机地址,第8
位表示传输方向(0表示写操作,1表示读操作)

这节课的i2c_interrupt_func()函数还没有讲。这个函数很重要,也比较麻烦,我们下节课讲。

第005节_I2C控制器编程_中断

这节课讲i2c_interrupt_func()函数,中断控制器是IIC程序中的核心中的核心。

Start信号之后,发出设备地址,在第9个时钟就会产生一个中断,我们根据i2c的流程图来编写中断程序。
每传输完一个数据也将产生一个中断,I2C操作的主体在中断服务程序,它可以分为两部分:写操作,读操作。

先分析写操作,代码如下:

#include "i2c_controller.h"
#include "../s3c2440_soc.h"

static p_i2c_msg p_cur_msg;
/* 定义一个全局结构体变量p_cur_msg,在do_master_tx或do_master_dx中被初始化。
也就是说,把当前正在传输的那个消息赋给这个全局结构体p_cur_msg。如:
int do_master_tx(p_i2c_msg msg)
{
	p_cur_msg = msg;
*/

void i2c_interrupt_func(int irq)
{
	int index;
	unsigned int iicstat = IICSTAT;

	p_cur_msg->cnt_transferred++;
	
	/* 每传输完一个数据将产生一个中断 */

	/* 对于每次传输, 第1个中断是"已经发出了设备地址" */

	if (p_cur_msg->flags == 0)	/* write */
	{
		/* 对于第1个中断, 它是发送出设备地址后产生的
		 * 需要判断是否有ACK
		 * 有ACK : 设备存在
		 * 无ACK : 无设备, 出错, 直接结束传输
		 */
		if (p_cur_msg->cnt_transferred == 0)  /* 第1次中断 */
		{
			if (iicstat & (1<<0))
			{ /* no ack */
				/* 停止传输。如下具体根据流程图来的 */
				IICSTAT = 0xd0;
				IICCON &= ~(1<<4);
				p_cur_msg->err = -1;
				delay(1000);
				return;
			}
		}
	}
/*		
1).p_cur_msg->cnt_transferred初始值为-1(我们后面设置)。
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信号已经发送完毕。
*/

else /* read */
	{
		/* 对于第1个中断, 它是发送出设备地址后产生的
		 * 需要判断是否有ACK
		 * 有ACK : 设备存在, 恢复I2C传输, 这样在下一个中断才可以得到第1个数据
		 * 无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;
				delay(1000);
				return;
			}
			else  /* ack  */
			{
				/* 恢复I2C传输 */
				IICCON &= ~(1<<4);
				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传输  */
			IICCON &= ~(1<<4);
		}
		else
		{
			/* 发出停止信号 如下具体根据流程图来的 */
			IICSTAT = 0x90;
			IICCON &= ~(1<<4);
			delay(1000);
		}
/*
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信号,停止数据的传输。
*/
	}
}


void s3c2440_i2c_con_init(void)
{
	/* 设置时钟 */
	/* [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)
	 */
	 /*----------------------本程序修改处--------------------------
	*我们从外设读数据的时候,在第9个时钟,2440需要发出回应,因此需要把bit7设置为1
	*/
	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;
	
	/*----------------------本程序修改处--------------------------*/
	/*cnt_transferred为字节数,设置为-1,*/
	/*疑问处:这里的msg->cnt_transferred = -1;而上面用到的是
	p_cur_msg->cnt_transferred = -1;,怎么回事,这不是一个东西啊????????
	msg->err = 0;也是这个问题
	难道应该把	p_cur_msg = msg;写在下面两个语句的下面?
	*/
	msg->cnt_transferred = -1;
	msg->err = 0;
	
	/* 设置寄存器启动传输 */
	/* 1. 配置为 master tx mode */
	
	/* 2. 把从设备地址写入IICDS */
	IICDS = msg->addr<<1;
	
	/* 3. IICSTAT = 0xf0 , 数据即被发送出去, 将导致中断产生 */
	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;
	
	/* 设置寄存器启动传输 */
	/* 1. 配置为 Master Rx mode */
		
	/* 2. 把从设备地址写入IICDS */
	IICDS = (msg->addr<<1)|(1<<0);
	
	/* 3. IICSTAT = 0xb0 , 从设备地址即被发送出去, 将导致中断产生 */
	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)/* write */
			err = do_master_tx(msgs[i]);
		else
			err = do_master_rx(msgs[i]);
		if (err)
			return err;
	}
	return 0;
}

/* 实现i2c_controller
          .init
          .master_xfer
          .name
 */

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);
}

第006节_EEPROM编程和测试的代码

6.1 从设备的读写函数

从设备程序,只涉及到两个函数分别是:从设备的写函数,从设备的读函数。下面下分析从设备的写函数,代码如下:

#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];
		
		/* 构造i2c_msg */
		msg.addr  = AT24CXX_ADDR;
		msg.flags = 0; /* write */
		msg.len   = 2;/*这个语句里的len形参中的len是你用户要传输多少个字节数,
		而形参里面的len是指你一共要写的字节数*/
		msg.buf   = buf;//把addr和data都给msg.buf
		msg.err   = 0;
		msg.cnt_transferred = -1;

		/* 调用i2c_transfer */
		err = i2c_transfer(&msg, 1);//把msg结构体发出,一次发出一个msg,len个msg
		if (err)
			return err;
	}
	return 0;
}
1).#define AT24CXX_ADDR 0x50宏定义设备地址。(查AT24CXX芯片手册)
2).我们每次只写一个字节,所以我们需要构造出len个msg。
3).调用i2c接口函数,传输构造i2C_msg结构体,我们 传输指针 只需要传输四个字节,我们需要
把以前的参数都改成传输指针的格式。修改如下:
把int i2c_transfer(i2c_msg msgs,int num){}改为
int i2c_transfer(p_i2c_msg msgs,int num){}typedef struct i2c_controller {
	...
	int (*master_xfer)(i2c_msg msgs, int num);
	...
}i2c_controller, *p_i2c_controller;
改为
把typedef struct i2c_controller {
	...
	int (*master_xfer)(p_i2c_msg msgs, int num);
	...
}i2c_controller, *p_i2c_controller;
s3c2440_master_xfer函数应为int s3c2440_master_xfer(_i2c_msg msgs,int num){}改为

从设备读函数和写函数类似,但读函数需要构造两个i2c_msg(每个i2c_msg只能表示一个传输方向) ,因为在读操作之前,需要把要读的地址告诉从设备。
代码如下:

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;
}

6.2 测试代码

I2c_test.c测试程序如下所示:

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;
		}
	}
}

调用i2c_controller.c里面的 i2c_init()初始化函数,在这个函数中需要添加一些功能,i2c_init()代码如下所示:

void i2c_init(void)
{
	/* 注册下面的I2C控制器 */
	s3c2440_i2c_con_add();

	/* 选择某款I2C控制器 */
	select_i2c_controller("s3c2440");

	/* 调用它的init函数 */
	p_i2c_con_selected->init();
}
select_i2c_controller("s3c2440")用于选择s3c2440的i2c控制器。
p_i2c_con_selected->init()调用s3c2440的i2c控制器结构体中init初始化函数,初始化s3c2440
的i2c控制器。

do_write_at24cxx()函数用于往at24cxx设备中写入数据,do_write_at24cxx()函数的代码如下所示:

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)//地址不可以大于256,这是因为看外设芯片,其最大只有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);
}
addr = get_uint()用于把输入的地址赋值给addr。
gets(str)用于把输入的字符串存在str字符数组中。
at24cxx_write(addr, str, strlen(str)+1)调用at24cxx_write函数把输入的数据str,,写到输
入的地址addr中。

do_read_at24cxx()函数从at24cxx中读取数据,do_read_at24cxx()函数的代码如下所示:

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");
	/* 长度固定为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");
	}
}
调用at24cxx_read(addr, data, len)函数,从addr地址中读取len长度的字节数据,放在data字
符数组中,后面的代码就是把读取得到的数据,打印出来。

第007节_测试

先给出你这一节遇到的三个问题与解决方案。然后你再往下细看问题即如何调试如何解决的:

在测试中,出现问题和解决办法:
a:中断没产生 : 未配置GPIO用于IIC功能
解决方法: 配置引脚用于I2C。
b. 只产生了一次中断, 并且出错 : tx err, no ack
解决方法: 启动传输之前 IICSTAT=(1<<4)
c.1次读OK,再次写卡死,复位再写仍卡死,重新上电再写OK:
解决方法: 读最后一个数据时,不要回应ACK给AT24CXX

7.1问题一

编译运行,我们去读,发现程序卡死了,如下图:
11-I2C_第19张图片
我们在i2c_interrupt_func加一个打印语句,看看中断产生了吗

void i2c_interrupt_func(int irq)
{
	int index;
	unsigned int iicstat = IICSTAT;
	unsigned int iiccon;
	
/*---------------调试打印代码--------------*/
	printf("i2c_interrupt_func! flags = %d\n\r", p_cur_msg->flags);

	p_cur_msg->cnt_transferred++;
	
	/* 每传输完一个数据将产生一个中断 */
		......

编译运行,发现还是一个样:
11-I2C_第20张图片
找到了原因:中断没产生。
来解决。
我们看2440原理图,发现GPE15和GPE16是不是需要配置,经研究发现这两位得配置。如下图:
11-I2C_第21张图片
11-I2C_第22张图片
好,该程序,在s3c2440_i2c_con_init()中加上配置引脚语句:

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
	......

编译运行,产生了中断,但是只产生一个中断,按理说应该产生9个中断(即打印函数打印的i2c_interrupt_func! flags = %d\n\r得运行9次),但我们的串口上只有一个中断:
11-I2C_第23张图片
引出了问题二:

7.2问题二

问题二:只产生了一次中断,按理说应该产生9次的。
我们来调试:在s3c2440_i2c_controller.c文件中的凡是出现err的地方都加上打印语句,i2c_interrupt_func()读处的第一次中断的若无ack处加上打印语句,如下:

else /* read */
	{
		/* 对于第1个中断, 它是发送出设备地址后产生的
	.......
		 */
if (p_cur_msg->cnt_transferred == 0)  /* 第1次中断 */
{
	if (iicstat & (1<<0))
	{ /* no ack */
		/* 停止传输 */
	......
		p_cur_msg->err = -1;
		/*-----------------------*/
		printf("rx err, no ack\n\r");
	......
	}

打印发现出错 : tx err, no ac,如下图:
11-I2C_第24张图片
为什么会出错,韦东山给出了答案:
在2440芯片手册的流程图前面有一段话,这三点必须设置:
第一点我们不需要设置,第二点我们已经设置过了
我们看第三点,即设置IICSTAT寄存器使能Serial Output,我们把IICSTAT的bit设置为1即可,这个问题即解决。
11-I2C_第25张图片

int do_master_tx(p_i2c_msg msg)
{
	......
	/* 设置寄存器启动传输 */
	/* 1. 配置为 master tx mode */
	/*-----------------加入代码----------------*/
	IICSTAT = (1<<4);
	
	/* 2. 把从设备地址写入IICDS */
	IICDS = (msg->addr<<1)|(1<<0);
	

int do_master_rx(p_i2c_msg msg)
{
	p_cur_msg = msg;

	.........	
	/* 设置寄存器启动传输 */
	/* 1. 配置为 Master Rx mode */
		/*-----------------加入代码----------------*/
	IICSTAT = (1<<4);
	
	/* 2. 把从设备地址写入IICDS */
	IICDS = (msg->addr<<1)|(1<<0);

编译运行,问题二解决了:
11-I2C_第26张图片
但是又有一个问题:
第1次读OK,但是再次写卡死,复位再写仍卡死,重新上电再写OK:
来解决:
我们发现at24cxx芯片手册上说:
11-I2C_第27张图片
即,在**读(注意是读)**数据时的最后8位数据(即P指令之前的),不需要回应(NO ACK),其设置在IICCON寄存器中的bit7:
我们需要:
11-I2C_第28张图片
所加代码:

int do_master_tx(p_i2c_msg msg)
{
	......
	/* 设置寄存器启动传输 */
	/* 1. 配置为 master tx mode */
	/*---------------------------更改处----------------------------*/
	IICCON |= (1<<7); /* TX mode, 在ACK周期释放SDA */
	IICSTAT = (1<<4);
	......
	
int do_master_rx(p_i2c_msg msg)
{
	p_cur_msg = msg;

	msg->cnt_transferred = -1;
	msg->err = 0;
	
	/* 设置寄存器启动传输 */
	/* 1. 配置为 Master Rx mode */
	/*-----------------------更改处---------------------------------*/
	IICCON |= (1<<7); /* RX mode, 在ACK周期回应ACK */
	IICSTAT = (1<<4);

然后我们需要判断是否为最后一次读,如果为最后一次读就不发出回应。

else /* read */
	{
		/* 对于第1个中断, 它是发送出设备地址后产生的
		 * 需要判断是否有ACK
		 * 有ACK : 设备存在, 恢复I2C传输, 这样在下一个中断才可以得到第1个数据
		 * 无ACK : 无设备, 出错, 直接结束传输
		 */
		if (p_cur_msg->cnt_transferred == 0)  /* 第1次中断 */
		{
			if (iicstat & (1<<0))
			{ /* no ack */
				/* 停止传输 */
				......
			}
			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;

			/* 如果是最后一个数据, 启动传输时要设置为不回应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;
}

你可能感兴趣的:(S3C2440裸机)