“第019课 I2C”的版本间的差异
Baiwen root(讨论 | 贡献) |
(电平->高电平) |
||
(未显示3个用户的19个中间版本) | |||
第1行: | 第1行: | ||
+ | <div body style="width:800px;"> | ||
= 第001节_I2C协议与EEPROM = | = 第001节_I2C协议与EEPROM = | ||
== I2C协议 == | == I2C协议 == | ||
I2C在硬件上的接法如下(图19-1)所示,主控芯片引出两条线SCL,SDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻(放一个上拉电阻的原因以后我们再说)。 | I2C在硬件上的接法如下(图19-1)所示,主控芯片引出两条线SCL,SDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻(放一个上拉电阻的原因以后我们再说)。 | ||
− | |||
− | + | [[File:chapter19_lesson1_001.jpg|700px]] | |
− | [[File:chapter19_lesson1_002.jpg| | + | |
+ | 我们怎么传输数据,我们需要发数据从主设备发送到从设备上去,也需要把数据从从设备传送到主设备上去,数据涉及到双向传输。 | ||
+ | |||
+ | 举个例子: | ||
+ | |||
+ | [[File:chapter19_lesson1_002.jpg|700px]] | ||
+ | |||
体育老师:可以把球发给学生,也可以把球从学生中接过来。 | 体育老师:可以把球发给学生,也可以把球从学生中接过来。 | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | 1.发球:a.老师说:注意了(start) | |
− | + | b.老师对A学生说我要球发给你(地址)。 | |
− | + | :: c.老师就把球发出去了(传输)。 | |
− | + | :: d.A收到球之后,应该告诉老师一声(回应)。 | |
− | + | :: e.老师说下课(停止) | |
− | e | + | |
+ | 2.接球: | ||
+ | :: a.老师说注意了(start), | ||
+ | :: b.老师说:B把球发给我(地址) | ||
+ | :: c.B就把球发给老师(传输) | ||
+ | :: d.老师收到球之后,给B说一声,表示收到球了(回应)。 | ||
+ | :: e.老师说下课(停止) | ||
+ | |||
我们就使用这个简单的例子,来解释一下IIC的传输协议。 | 我们就使用这个简单的例子,来解释一下IIC的传输协议。 | ||
− | 老师说注意了,表示开始信号(start) | + | * 老师说注意了,表示开始信号(start) |
− | 老师告诉某个学生,表示发送地址(address) | + | * 老师告诉某个学生,表示发送地址(address) |
− | 老师发球/接球,表示数据的传输 | + | * 老师发球/接球,表示数据的传输 |
− | 老师/学生收到球,回应表示:回应信号(ACK) | + | * 老师/学生收到球,回应表示:回应信号(ACK) |
− | 老师说下课,表示IIC传输接受(P) | + | * 老师说下课,表示IIC传输接受(P) |
== IIC传输数据的格式 == | == IIC传输数据的格式 == | ||
− | ''' | + | '''1.写操作:''' |
+ | |||
刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)。 | 刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)。 | ||
+ | |||
回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。 | 回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。 | ||
+ | |||
每传输一个数据,接受方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号。 | 每传输一个数据,接受方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号。 | ||
− | |||
− | |||
− | ''' | + | :: 白色背景:主→从 |
+ | :: 灰色背景:从→主 | ||
+ | |||
+ | [[File:chapter19_lesson1_003.jpg|700px]] | ||
+ | |||
+ | '''2.读操作:''' | ||
+ | |||
刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是从哪一个芯片读取数据),方向(读/写,0表示写,1表示读)。 | 刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是从哪一个芯片读取数据),方向(读/写,0表示写,1表示读)。 | ||
+ | |||
回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后在传输下一个数据。 | 回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后在传输下一个数据。 | ||
− | 每传输一个数据,接受方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号。 | + | |
− | + | 每传输一个数据,接受方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号。 | |
− | [[File:chapter19_lesson1_004.jpg| | + | |
+ | :: 白色背景:主→从 | ||
+ | :: 灰色背景:从→主 | ||
+ | |||
+ | [[File:chapter19_lesson1_004.jpg|700px]] | ||
+ | |||
传输是以8位为单元数据传输的,先传输最高位(MSB),主芯片发出start信号之后,然后发出9个时钟传输数据。 | 传输是以8位为单元数据传输的,先传输最高位(MSB),主芯片发出start信号之后,然后发出9个时钟传输数据。 | ||
− | + | ||
− | + | (1)开始信号(S):SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。 | |
+ | |||
+ | (2)结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。 | ||
+ | |||
(3)响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA | (3)响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA | ||
+ | |||
SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化。如图 | SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化。如图 | ||
− | [[File:chapter19_lesson1_005.jpg| | + | [[File:chapter19_lesson1_005.jpg|700px]] |
+ | |||
+ | |||
+ | 1.问题:如何在SDA上实现双向传输? | ||
− | |||
答:主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚)。 | 答:主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚)。 | ||
− | + | ||
+ | 2.问题:主设备(从设备)发送数据时,从设备(主设备)的发送引脚,不影响数据的发送,怎么做到呢? | ||
+ | |||
答:里面放一个三极管,使用开极(极电集开发出去作为输出)电路,如下图 | 答:里面放一个三极管,使用开极(极电集开发出去作为输出)电路,如下图 | ||
− | [[File:chapter19_lesson1_006.jpg| | + | [[File:chapter19_lesson1_006.jpg|700px]] |
+ | |||
下面画一个真值表: | 下面画一个真值表: | ||
第64行: | 第94行: | ||
* 想输出低电平,就驱动三极管。 | * 想输出低电平,就驱动三极管。 | ||
− | + | 从下面的例子可以看看数据是怎么传的(实现双向传输),比如:主设备发送(8bit)给从设备 | |
− | + | 1.前8个clk | |
− | |||
* 从设备不要影响,从设备不驱动三极管; | * 从设备不要影响,从设备不驱动三极管; | ||
* 主设备决定数据; | * 主设备决定数据; | ||
− | + | 2.第9个clk,由从设备决定数据 | |
* 主设备不驱动三极管; | * 主设备不驱动三极管; | ||
* 从设备决定数据; | * 从设备决定数据; | ||
第77行: | 第106行: | ||
在第9个时钟之后,如果有某一方处于繁忙状态,它可以一直把SCL拉低当SCL为低电平时候,大家都不应该使用IIC总线,只有当SCL从低电平变为高电平的时候,IIC总线才能被使用。 | 在第9个时钟之后,如果有某一方处于繁忙状态,它可以一直把SCL拉低当SCL为低电平时候,大家都不应该使用IIC总线,只有当SCL从低电平变为高电平的时候,IIC总线才能被使用。 | ||
− | + | ||
− | + | 从前图我们也可以知道ACK信号应该是低电平。主设备不驱动三极管,如果从设备不驱动三极端的化SDA应该是高电平,当从设备接收数据之后,发出回应信号的时候,就会驱动三极管,让SDA变为低电平。所以说:ACK信号是低电平。 | |
+ | |||
对于IIC协议它只能规定怎么传输数据,数据什么含义它完全不能够控制,数据的含义有从设备决定。 | 对于IIC协议它只能规定怎么传输数据,数据什么含义它完全不能够控制,数据的含义有从设备决定。 | ||
= 第002节_S3C2440的I2C控制器 = | = 第002节_S3C2440的I2C控制器 = | ||
− | + | 在嵌入式系统里面的主控芯片一般都会有I2C控制器,要是没有可以根据I2C协议用GPIO管脚模拟,但是非常麻烦,我们要发送数据时,可以把数据放到某个寄存器,它就会自动的发出时钟,并且把数据发送给从设备,同时会等待从设备会返回回应信号。 | |
− | [[File:chapter19_lesson2_001.jpg| | + | |
+ | 当我们想发送一个数据的时候,要设置某个寄存器启动传输,它也一样会产生时钟,然后从设备就会把数据通过SDA传到I2C控制器里面,组装进某个寄存器里面,最终寄存器会把接收到的8位数据返回给我们的程序,从这里可以看到I2C控制器简化了I2C的操作。简短电路连接图,如图: | ||
+ | |||
+ | [[File:chapter19_lesson2_001.jpg|700px]] | ||
+ | |||
+ | [[File:chapter19_lesson2_002.jpg|700px]] | ||
+ | |||
− | |||
根据上图,我们首先设置IICCON(来设置时钟),时钟源是PCLK(是50MHZ)太快了我们需要设置这个分频系数,把时钟降低,降低到我们想要的SCL,然后我们要发出start信号,我们需要设置寄存器发出start信号,之后我们需要发出数据啊,我们的程序可以把数据写入到IICDS寄存器,一写入就会自动的发出时钟,并且把这8位数据从SDA发送给从设备,数据发送之后,在第九个时钟会收到回应信号,可以查询IICSTAT是否有ACK(有ACK表示数据发送成功了),可以继续发送数据,等发完数据之后,再来设置IICSTAT让它发出P信号。 | 根据上图,我们首先设置IICCON(来设置时钟),时钟源是PCLK(是50MHZ)太快了我们需要设置这个分频系数,把时钟降低,降低到我们想要的SCL,然后我们要发出start信号,我们需要设置寄存器发出start信号,之后我们需要发出数据啊,我们的程序可以把数据写入到IICDS寄存器,一写入就会自动的发出时钟,并且把这8位数据从SDA发送给从设备,数据发送之后,在第九个时钟会收到回应信号,可以查询IICSTAT是否有ACK(有ACK表示数据发送成功了),可以继续发送数据,等发完数据之后,再来设置IICSTAT让它发出P信号。 | ||
+ | |||
在第九个CLK,就会产生一个中断,在中断处理过程中SCL被拉为低电平,谁都不能再使用IIC总线,等待中断处理完成. | 在第九个CLK,就会产生一个中断,在中断处理过程中SCL被拉为低电平,谁都不能再使用IIC总线,等待中断处理完成. | ||
+ | |||
'''怎样处理中断?''' | '''怎样处理中断?''' | ||
+ | |||
* 写操作: | * 写操作: | ||
若无ACK,出错,然后发出P信号结束, | 若无ACK,出错,然后发出P信号结束, | ||
− | + | :: 若有ACK信号表示上一个字节成功发送出去 | |
− | + | :: 若仍有数据,写入IICDS寄存器,然后清中断,一清中断就会释放SCL信号,继续发出时钟,把数据再次发送出去。 | |
− | + | :: 若没有数据了,发出P信号结束。 | |
+ | |||
* 读操作: | * 读操作: | ||
读到8位数时,应该回应一个ACK信号。 | 读到8位数时,应该回应一个ACK信号。 | ||
− | + | :: 还想读数据,清中断,启动传输。等它再次发生中断时,再来读取IICDS寄存器,得到数据。不想读取数据,发出P信号结束。 | |
'''重点:''' 发生中断时,我们的IIC控制器会把SCL拉低,阻止任何设备再使用IIC总线,清中断之后才能继续使用,这种机制就给我们中断服务程序的执行提供了时间。 | '''重点:''' 发生中断时,我们的IIC控制器会把SCL拉低,阻止任何设备再使用IIC总线,清中断之后才能继续使用,这种机制就给我们中断服务程序的执行提供了时间。 | ||
+ | |||
'''读-写操作''' | '''读-写操作''' | ||
+ | |||
* 在发送模式: | * 在发送模式: | ||
− | + | :: 1.往寄存器IICDS寄存器放入一个val值。 | |
− | + | :: 2.发完,产生中断,并且会把 SCL拉低。 | |
− | + | :: 3.在中断程序里,判断状态,然后往IICDS里面写入下一个数据,一旦写入下一个数据IIC继续操作,若再次发完,就会再次产生中断。 | |
* 在接受模式: | * 在接受模式: | ||
− | + | :: 1.我的程序发起传输,接受数据。 | |
− | + | :: 2.接收到数据之后,产生中断,SCL被拉低。 | |
− | + | :: 3.中断程序里,判断数据是否要继续接受等,如果还有继续接受的话,再次设置,设置好之后读IICDS寄存器,一但读出来IIC。 | |
− | + | :: 继续接受下一个数据,收到新数据之后,又会产生一个中断(就是这样循环操作)。 | |
+ | |||
'''(l)IICCON寄存器(Multi-masterIIC-buscontrol)''' | '''(l)IICCON寄存器(Multi-masterIIC-buscontrol)''' | ||
− | |||
− | |||
− | + | IICCON寄存器用于控制是否发出ACK信号、设置发送器的时钟、开启,i2c中断,并标识中断是否发生。它的各位含义如表: | |
− | + | ||
+ | [[File:chapter19_lesson2_003.jpg|700px]] | ||
+ | |||
+ | 使用IICCON寄存器时,有如下注意事项。 | ||
+ | |||
+ | 1.发送模式的时钟频率由位[6]、位[3:0]联合决定,另外,llCCON[6]=0,IICCON[3:0] | ||
不能取0或10 | 不能取0或10 | ||
− | + | ||
− | 3 | + | 2.12c中断在以下3种情况下发生:当发出地址信息或接收到一个从机地址并且吻合时,当总线仲裁失败时,当发送/接收完一个字节的数据(包括响应位)时。 |
− | 4 | + | |
+ | 3.基于SDA、SCL线上时间特性的考虑,要发送数据时,先将数据写入IICDS寄存器,然后再清除中断。 | ||
+ | |||
+ | 4.如果IICCON[5]=0,IICCON14]将不能正常工作。所以,即使不使用12c中断,也要将IICCON[5]设为1。 | ||
+ | |||
'''(2)IICSTAT寄存器(Multi-masterIIC-buscontrol/status)''' | '''(2)IICSTAT寄存器(Multi-masterIIC-buscontrol/status)''' | ||
− | + | ||
− | [[File:chapter19_lesson2_004.jpg| | + | IICSTAT寄存器用于选择12c接口的工作模式,发出S信号、P信号,使能接收/发送功能,并标识各种状态,比如总线仲裁是否成功、作为从机时是否被寻址、是否接收到0地址、是否接收到ACK信号等。IICSTAT寄存器的各位如表: |
+ | |||
+ | [[File:chapter19_lesson2_004.jpg|700px]] | ||
+ | |||
'''(3)IICADD寄存器(Multi-masterIlC-busaddress)''' | '''(3)IICADD寄存器(Multi-masterIlC-busaddress)''' | ||
− | + | ||
+ | 用到IICADD寄存器的位[7:11],表示从机地址。IICADD寄存器在串行输出使能位 | ||
IICSTAT[4]为0时,才可以写入:在任何时间都可以读出。IICADD寄存器的各位如表: | IICSTAT[4]为0时,才可以写入:在任何时间都可以读出。IICADD寄存器的各位如表: | ||
− | [[File:chapter19_lesson2_005.jpg| | + | [[File:chapter19_lesson2_005.jpg|700px]] |
+ | |||
'''(4)IICDS寄存器(Multi-masterIIC-busTx/Rxdatashift)''' | '''(4)IICDS寄存器(Multi-masterIIC-busTx/Rxdatashift)''' | ||
+ | |||
用到IICDS寄存器的位丨7:0],其中保存的是要发送或己经接收的数据。IICDS寄存器在串行输出使能位IICSTAT()1为1时,叼可以写入;在任何时间都可以读出。IICDS寄存器的各位如表: | 用到IICDS寄存器的位丨7:0],其中保存的是要发送或己经接收的数据。IICDS寄存器在串行输出使能位IICSTAT()1为1时,叼可以写入;在任何时间都可以读出。IICDS寄存器的各位如表: | ||
− | [[File:chapter19_lesson2_006.jpg| | + | |
+ | [[File:chapter19_lesson2_006.jpg|700px]] | ||
+ | |||
+ | |||
'''读写操作流程图''' | '''读写操作流程图''' | ||
+ | |||
主机发送器模式操作: | 主机发送器模式操作: | ||
− | [[File:chapter19_lesson2_007.jpg| | + | |
+ | [[File:chapter19_lesson2_007.jpg|700px]] | ||
+ | |||
主机接收器模式操作: | 主机接收器模式操作: | ||
− | [[File:chapter19_lesson2_008.jpg| | + | |
+ | [[File:chapter19_lesson2_008.jpg|700px]] | ||
= 第003节_程序框架 = | = 第003节_程序框架 = | ||
写程序之前 考虑好程序的框架,我们想写出一个结构比较好,比较容易扩展的程序 | 写程序之前 考虑好程序的框架,我们想写出一个结构比较好,比较容易扩展的程序 | ||
+ | |||
我们先要考虑清楚框架的设计。 | 我们先要考虑清楚框架的设计。 | ||
第150行: | 第213行: | ||
IIC会做什么事情呢? | IIC会做什么事情呢? | ||
+ | |||
对于IIC控制器,它负责传输数据,不知道数据的含义,但是它要实现写/读操作 | 对于IIC控制器,它负责传输数据,不知道数据的含义,但是它要实现写/读操作 | ||
− | ''读操作''' | + | '''读操作''' |
[[File:chapter19_lesson3_001.jpg|800px]] | [[File:chapter19_lesson3_001.jpg|800px]] | ||
− | ''写操作''' | + | '''写操作''' |
[[File:chapter19_lesson3_002.jpg|800px]] | [[File:chapter19_lesson3_002.jpg|800px]] | ||
第171行: | 第235行: | ||
我们提供一个统一的接口i2c_transfer,不关使用哪个芯片,他最终都会调用i2c_transfer,来选择某一款I2C控制器,把数据发送出去,或者从I2c设备读到数据,对于每一次传输的数据都可以用一个i2c_msg结构体来表示。但是,读某个地址的数据时,就要用两个i2c_msg结构体来描述它,因为一个i2c_msg结构体只能描述一个传输方向(读/写),我们读取ac24ccxx某个地址上的数据时,要先写出要读取的地址,然后来读取设备地址上的数据。 | 我们提供一个统一的接口i2c_transfer,不关使用哪个芯片,他最终都会调用i2c_transfer,来选择某一款I2C控制器,把数据发送出去,或者从I2c设备读到数据,对于每一次传输的数据都可以用一个i2c_msg结构体来表示。但是,读某个地址的数据时,就要用两个i2c_msg结构体来描述它,因为一个i2c_msg结构体只能描述一个传输方向(读/写),我们读取ac24ccxx某个地址上的数据时,要先写出要读取的地址,然后来读取设备地址上的数据。 | ||
− | 我们想设计出以一个结构体比较容易扩展的框架,对于I2C控制器我们要抽象出一个结构体i2c_controller,我们构造这个结构体之后,把这个这个结构体,告诉上层(I2C控制器那一层),上层有个管理者i2c_contreller. | + | 我们想设计出以一个结构体比较容易扩展的框架,对于I2C控制器我们要抽象出一个结构体i2c_controller,我们构造这个结构体之后,把这个这个结构体,告诉上层(I2C控制器那一层),上层有个管理者i2c_contreller.c文件。 |
+ | |||
+ | 我们在s3c2440_i2c_controller.c这个文件中我们构造出一个i2c_controller结构体,把它放入上层文件中的数组里,以后就根据结构体的名字,把这个结构体取出来使用。 | ||
+ | |||
+ | 假设我们有一个TI的开发板,在ti_i2c_controller.c文件中,也要构造出一个i2c_controller结构体,同样们也会把这个结构体放入上层的结构体数组(i2c_contreller.c文件中)中,以后根据名字先出来使用。 | ||
+ | |||
对于设备层中的at24cxx芯片我们写出at24cxx.c文件在这个文件实现读写函数: | 对于设备层中的at24cxx芯片我们写出at24cxx.c文件在这个文件实现读写函数: | ||
− | |||
− | 下面我们写一个程序框架,涉及到的文件有:i2c_test. | + | 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文件''' | '''i2c_test.c文件''' | ||
+ | |||
该文件的内容如下: | 该文件的内容如下: | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第188行: | 第269行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
这个菜单最终会调用到at24cxx.c里面的函数。 | 这个菜单最终会调用到at24cxx.c里面的函数。 | ||
+ | |||
'''at24cxx.c文件''' | '''at24cxx.c文件''' | ||
+ | |||
在里面会使用标准的接口i2c_transfer来启动I2C传输。该文件的内容如下: | 在里面会使用标准的接口i2c_transfer来启动I2C传输。该文件的内容如下: | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第207行: | 第290行: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
'''i2c_controller.c文件''' | '''i2c_controller.c文件''' | ||
+ | |||
该文件的内容如下: | 该文件的内容如下: | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第228行: | 第313行: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | ||
+ | select_i2c_controller函数根据名字来选择某款I2C控制器后,以后就会使用被选择的I2C控制器来启动传输。 | ||
+ | |||
+ | 有数组一定有注册函数register_i2c_controller会把下面实现的I2C控制器结构体i2c_controller放到i2c_controller数组里面。 | ||
+ | |||
'''s3c2440_i2c_controller.c文件''' | '''s3c2440_i2c_controller.c文件''' | ||
+ | |||
对于具体的芯片,要实现自己的i2c_controller。该文件的内容如下: | 对于具体的芯片,要实现自己的i2c_controller。该文件的内容如下: | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第241行: | 第331行: | ||
= 第004节_I2C控制器编程_框架 = | = 第004节_I2C控制器编程_框架 = | ||
− | 我们现在来讲I2C控制器怎么写,它是I2C程序中最核心的地方,我们要先构造几个结构体,这几个结构体放在i2c_controller. | + | 我们现在来讲I2C控制器怎么写,它是I2C程序中最核心的地方,我们要先构造几个结构体,这几个结构体放在i2c_controller.h里面。 |
+ | |||
+ | 我们要发出I2c传输时,要构造出i2c_msg,把构造出的i2c_msg扔给下面的i2c_controller.c,i2c_controller.c会选择某一个i2c控制器,使用里面的master_xfer来传输数据, 所以我们需要构造出一个i2c_controller结构体。 | ||
+ | |||
+ | |||
'''i2c_controller.h文件''' | '''i2c_controller.h文件''' | ||
+ | |||
文件的内容如下所示: | 文件的内容如下所示: | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第266行: | 第361行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
解析:我们构造这两个结构体,我们要把它放在i2c_controller.c把它用起来, | 解析:我们构造这两个结构体,我们要把它放在i2c_controller.c把它用起来, | ||
+ | |||
'''i2c_controller.c文件''' | '''i2c_controller.c文件''' | ||
+ | |||
文件的内容如下所示: | 文件的内容如下所示: | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第293行: | 第390行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
解析:register_i2c_controller函数用于把参数中的结构体指针,注册到p_i2c_controllers指针数组中。 | 解析:register_i2c_controller函数用于把参数中的结构体指针,注册到p_i2c_controllers指针数组中。 | ||
+ | |||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第311行: | 第409行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
解析:select_i2c_controller函数根据参数中的名字(name) 从p_i2c_controllers指针数组中取出对应的结构体指针复制给p_i2c_con_selected结构体指针(静态全局变量)。 | 解析:select_i2c_controller函数根据参数中的名字(name) 从p_i2c_controllers指针数组中取出对应的结构体指针复制给p_i2c_con_selected结构体指针(静态全局变量)。 | ||
+ | |||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第321行: | 第420行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
解析:i2c_transfer接口函数,调用选择的p_i2c_con_selected成员中master_xfer函数。 | 解析:i2c_transfer接口函数,调用选择的p_i2c_con_selected成员中master_xfer函数。 | ||
+ | |||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第337行: | 第437行: | ||
'''s3c2440_i2c_controller.c文件''' | '''s3c2440_i2c_controller.c文件''' | ||
+ | |||
中断服务函数,当发成中断是,就会调用中断服务函数,代码如下 | 中断服务函数,当发成中断是,就会调用中断服务函数,代码如下 | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
第344行: | 第445行: | ||
/* 对于每次传输, 第1个中断是"已经发出了设备地址" */ | /* 对于每次传输, 第1个中断是"已经发出了设备地址" */ | ||
− | |||
− | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
第369行: | 第468行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
解析: | 解析: | ||
− | 1) | + | |
− | + | 1).IICCON = (0<<6) | (1<<5) | (30<<0); 设置IICCON控制寄存器。选择发送时钟,使能中断。 | |
+ | |||
+ | 2).register_irq(27, i2c_interrupt_func):注册中断处理函数,当发生I2C中断的时候就会调用i2c_interrupt_func中断处理函数。 | ||
第396行: | 第497行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
解析: | 解析: | ||
− | + | ||
− | + | 1).IICDS = msg->addr<<1: 把从机地址(高7位,所以需要向右移一位)写入到IICDS寄存器中。 | |
+ | |||
+ | 2).IICSTAT = 0xf0:设置IICSTAT寄存器,将s3c2440设为主机发送器,并发出S信号后,紧接着就发出从机地址。后续的传输工作将在中断服务程序中完成。 | ||
+ | |||
do_master_rx函数的实现和do_master_tx函数类似,代码如下: | do_master_rx函数的实现和do_master_tx函数类似,代码如下: | ||
第422行: | 第526行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
解析: | 解析: | ||
− | + | 1).IICDS = (msg->addr<<1)|(1<<0):把从设备地址写入IICDS,前7位是从机地址,第8位表示传输方向(0表示写操作,1表示读操作)。 | |
+ | |||
s3c2440传输函数,根据标志位flags,来指明是读/写(1:读 0:写)。代码如下: | s3c2440传输函数,根据标志位flags,来指明是读/写(1:读 0:写)。代码如下: | ||
第456行: | 第561行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | = 第005节_I2C控制器编程_中断 = | ||
+ | 中断控制器是IIC程序中的核心中的核心。 | ||
+ | |||
+ | Start信号之后,发出设备地址,在第9个时钟就会产生一个中断,我们根据i2c的流程图来编写中断程序。 | ||
− | + | 每传输完一个数据将产生一个中断,I2C操作的主体在中断服务程序,它可以分为两部分:写操作,读操作。 | |
− | + | ||
+ | 先分析写操作,代码如下: | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
void i2c_interrupt_func(int irq) | void i2c_interrupt_func(int irq) | ||
第491行: | 第601行: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | 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信号已经发送完毕。 | |
第522行: | 第632行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | 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) | + | 3).如果条件不成立表示数据传输完毕,发出P信号,停止数据的传输。 |
写操作:I2C读操作的处理与写操作类似,我们就不进行分析了,代码如下: | 写操作:I2C读操作的处理与写操作类似,我们就不进行分析了,代码如下: | ||
第618行: | 第728行: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | 1).<code>#define AT24CXX_ADDR 0x50</code>宏定义设备地址。 | |
− | + | 2).我们每次只写一个字节,所以我们需要构造出len个msg。 | |
− | + | 3).调用i2c接口函数,传输构造i2C_msg结构体,我们传输指针只需要传输四个字节,我们需要把以前的参数都改成传输指针的格式。 | |
− | + | ||
+ | 从设备读函数和写函数类似,读函数需要构造两个i2c_msg(每个i2c_msg只能表示一个传输方向) ,因为在读操作之前,需要把要读的地址告诉从设备。 | ||
+ | |||
+ | 代码如下: | ||
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
int at24cxx_read(unsigned int addr, unsigned char *data, int len) | int at24cxx_read(unsigned int addr, unsigned char *data, int len) | ||
第702行: | 第815行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | 1).调用i2c_controller.c里面的 i2c_init()初始化函数,在这个函数中需要添加一些功能,i2c_init()代码如下所示: | |
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
void i2c_init(void) | void i2c_init(void) | ||
第719行: | 第832行: | ||
* p_i2c_con_selected->init()调用s3c2440的i2c控制器结构体中init初始化函数,初始化s3c2440的i2c控制器。 | * p_i2c_con_selected->init()调用s3c2440的i2c控制器结构体中init初始化函数,初始化s3c2440的i2c控制器。 | ||
− | + | 2).执行do_write_at24cxx()函数用于往at24cxx设备中写入数据,do_write_at24cxx()函数的代码如下所示: | |
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
{ | { | ||
第748行: | 第861行: | ||
* at24cxx_write(addr, str, strlen(str)+1)调用at24cxx_write函数把输入的数据str,,放在输入的地址addr中。 | * at24cxx_write(addr, str, strlen(str)+1)调用at24cxx_write函数把输入的数据str,,放在输入的地址addr中。 | ||
− | + | 3).执行do_read_at24cxx()函数从at24cxx中读取数据,do_read_at24cxx()函数的代码如下所示: | |
<syntaxhighlight lang="c" > | <syntaxhighlight lang="c" > | ||
void do_read_at24cxx(void) | void do_read_at24cxx(void) | ||
第810行: | 第923行: | ||
在测试中,出现问题和解决办法: | 在测试中,出现问题和解决办法: | ||
− | a 中断没产生 : 未配置GPIO用于IIC功能 | + | * a 中断没产生 : 未配置GPIO用于IIC功能 |
+ | |||
解决方法: 配置引脚用于I2C | 解决方法: 配置引脚用于I2C | ||
− | b. 只产生了一次中断, 并且出错 : tx err, no ack | + | * b. 只产生了一次中断, 并且出错 : tx err, no ack |
+ | |||
解决方法: 启动传输之前 IICSTAT=(1<<4) | 解决方法: 启动传输之前 IICSTAT=(1<<4) | ||
− | c. 第1次读OK,再次写卡死,复位再写仍卡死,重新上电再写OK: | + | * c. 第1次读OK,再次写卡死,复位再写仍卡死,重新上电再写OK: |
+ | |||
解决方法: 读最后一个数据时,不要回应ACK给AT24CXX | 解决方法: 读最后一个数据时,不要回应ACK给AT24CXX | ||
+ | |||
程序框架如下图所示: | 程序框架如下图所示: | ||
− | [[File:chapter19_lesson7_001.jpg| | + | [[File:chapter19_lesson7_001.jpg|1180px]] |
− | + | </div> | |
='''《《所有章节目录》》'''= | ='''《《所有章节目录》》'''= | ||
<categorytree mode=all background*color:white;">ARM裸机加强版</categorytree> | <categorytree mode=all background*color:white;">ARM裸机加强版</categorytree> | ||
[[Category:ARM裸机加强版 ]] | [[Category:ARM裸机加强版 ]] |
2018年7月12日 (四) 14:29的最新版本
目录
第001节_I2C协议与EEPROM
I2C协议
I2C在硬件上的接法如下(图19-1)所示,主控芯片引出两条线SCL,SDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻(放一个上拉电阻的原因以后我们再说)。
我们怎么传输数据,我们需要发数据从主设备发送到从设备上去,也需要把数据从从设备传送到主设备上去,数据涉及到双向传输。
举个例子:
体育老师:可以把球发给学生,也可以把球从学生中接过来。
1.发球:a.老师说:注意了(start) b.老师对A学生说我要球发给你(地址)。
- c.老师就把球发出去了(传输)。
- d.A收到球之后,应该告诉老师一声(回应)。
- e.老师说下课(停止)
2.接球:
- a.老师说注意了(start),
- b.老师说:B把球发给我(地址)
- c.B就把球发给老师(传输)
- d.老师收到球之后,给B说一声,表示收到球了(回应)。
- e.老师说下课(停止)
我们就使用这个简单的例子,来解释一下IIC的传输协议。
- 老师说注意了,表示开始信号(start)
- 老师告诉某个学生,表示发送地址(address)
- 老师发球/接球,表示数据的传输
- 老师/学生收到球,回应表示:回应信号(ACK)
- 老师说下课,表示IIC传输接受(P)
IIC传输数据的格式
1.写操作:
刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)。
回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
每传输一个数据,接受方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号。
- 白色背景:主→从
- 灰色背景:从→主
2.读操作:
刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是从哪一个芯片读取数据),方向(读/写,0表示写,1表示读)。
回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后在传输下一个数据。
每传输一个数据,接受方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号。
- 白色背景:主→从
- 灰色背景:从→主
传输是以8位为单元数据传输的,先传输最高位(MSB),主芯片发出start信号之后,然后发出9个时钟传输数据。
(1)开始信号(S):SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
(2)结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
(3)响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期,拉低SDA
SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化。如图
1.问题:如何在SDA上实现双向传输?
答:主芯片通过一根SDA线既可以把数据发给从设备,也可以从SDA上读取数据,连接SDA线的引脚里面必然有两个引脚(发送引脚/接受引脚)。
2.问题:主设备(从设备)发送数据时,从设备(主设备)的发送引脚,不影响数据的发送,怎么做到呢?
答:里面放一个三极管,使用开极(极电集开发出去作为输出)电路,如下图
- 从真值表和电路图我们可以知道,当某一个芯片不行影响SDA线时,那就不驱动这个三极管。
- 想输出高电平时;都不驱动(高电平就由上拉电阻决定)。
- 想输出低电平,就驱动三极管。
从下面的例子可以看看数据是怎么传的(实现双向传输),比如:主设备发送(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管脚模拟,但是非常麻烦,我们要发送数据时,可以把数据放到某个寄存器,它就会自动的发出时钟,并且把数据发送给从设备,同时会等待从设备会返回回应信号。
当我们想发送一个数据的时候,要设置某个寄存器启动传输,它也一样会产生时钟,然后从设备就会把数据通过SDA传到I2C控制器里面,组装进某个寄存器里面,最终寄存器会把接收到的8位数据返回给我们的程序,从这里可以看到I2C控制器简化了I2C的操作。简短电路连接图,如图:
根据上图,我们首先设置IICCON(来设置时钟),时钟源是PCLK(是50MHZ)太快了我们需要设置这个分频系数,把时钟降低,降低到我们想要的SCL,然后我们要发出start信号,我们需要设置寄存器发出start信号,之后我们需要发出数据啊,我们的程序可以把数据写入到IICDS寄存器,一写入就会自动的发出时钟,并且把这8位数据从SDA发送给从设备,数据发送之后,在第九个时钟会收到回应信号,可以查询IICSTAT是否有ACK(有ACK表示数据发送成功了),可以继续发送数据,等发完数据之后,再来设置IICSTAT让它发出P信号。
在第九个CLK,就会产生一个中断,在中断处理过程中SCL被拉为低电平,谁都不能再使用IIC总线,等待中断处理完成.
怎样处理中断?
- 写操作:
若无ACK,出错,然后发出P信号结束,
- 若有ACK信号表示上一个字节成功发送出去
- 若仍有数据,写入IICDS寄存器,然后清中断,一清中断就会释放SCL信号,继续发出时钟,把数据再次发送出去。
- 若没有数据了,发出P信号结束。
- 读操作:
读到8位数时,应该回应一个ACK信号。
- 还想读数据,清中断,启动传输。等它再次发生中断时,再来读取IICDS寄存器,得到数据。不想读取数据,发出P信号结束。
重点: 发生中断时,我们的IIC控制器会把SCL拉低,阻止任何设备再使用IIC总线,清中断之后才能继续使用,这种机制就给我们中断服务程序的执行提供了时间。
读-写操作
- 在发送模式:
- 1.往寄存器IICDS寄存器放入一个val值。
- 2.发完,产生中断,并且会把 SCL拉低。
- 3.在中断程序里,判断状态,然后往IICDS里面写入下一个数据,一旦写入下一个数据IIC继续操作,若再次发完,就会再次产生中断。
- 在接受模式:
- 1.我的程序发起传输,接受数据。
- 2.接收到数据之后,产生中断,SCL被拉低。
- 3.中断程序里,判断数据是否要继续接受等,如果还有继续接受的话,再次设置,设置好之后读IICDS寄存器,一但读出来IIC。
- 继续接受下一个数据,收到新数据之后,又会产生一个中断(就是这样循环操作)。
(l)IICCON寄存器(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,IICCON14]将不能正常工作。所以,即使不使用12c中断,也要将IICCON[5]设为1。
(2)IICSTAT寄存器(Multi-masterIIC-buscontrol/status)
IICSTAT寄存器用于选择12c接口的工作模式,发出S信号、P信号,使能接收/发送功能,并标识各种状态,比如总线仲裁是否成功、作为从机时是否被寻址、是否接收到0地址、是否接收到ACK信号等。IICSTAT寄存器的各位如表:
(3)IICADD寄存器(Multi-masterIlC-busaddress)
用到IICADD寄存器的位[7:11],表示从机地址。IICADD寄存器在串行输出使能位 IICSTAT[4]为0时,才可以写入:在任何时间都可以读出。IICADD寄存器的各位如表:
(4)IICDS寄存器(Multi-masterIIC-busTx/Rxdatashift)
用到IICDS寄存器的位丨7:0],其中保存的是要发送或己经接收的数据。IICDS寄存器在串行输出使能位IICSTAT()1为1时,叼可以写入;在任何时间都可以读出。IICDS寄存器的各位如表:
读写操作流程图
主机发送器模式操作:
主机接收器模式操作:
第003节_程序框架
写程序之前 考虑好程序的框架,我们想写出一个结构比较好,比较容易扩展的程序
我们先要考虑清楚框架的设计。
IIC控制器的功能
IIC会做什么事情呢?
对于IIC控制器,它负责传输数据,不知道数据的含义,但是它要实现写/读操作
IIC设备的功能
很显然,IIC控制器提供了传输数据的能力,至于数据有什么含义,IIC控制器并不知道,数据的含义有外接的IIC芯片决定,我们需要阅读芯片手册,才知道IIC控制器应该发出怎样的数据,
显然我们的程序应该分为两层(IIC设备层,IIC控制器层),框架如下图所示:
我们提供一个统一的接口i2c_transfer,不关使用哪个芯片,他最终都会调用i2c_transfer,来选择某一款I2C控制器,把数据发送出去,或者从I2c设备读到数据,对于每一次传输的数据都可以用一个i2c_msg结构体来表示。但是,读某个地址的数据时,就要用两个i2c_msg结构体来描述它,因为一个i2c_msg结构体只能描述一个传输方向(读/写),我们读取ac24ccxx某个地址上的数据时,要先写出要读取的地址,然后来读取设备地址上的数据。
我们想设计出以一个结构体比较容易扩展的框架,对于I2C控制器我们要抽象出一个结构体i2c_controller,我们构造这个结构体之后,把这个这个结构体,告诉上层(I2C控制器那一层),上层有个管理者i2c_contreller.c文件。
我们在s3c2440_i2c_controller.c这个文件中我们构造出一个i2c_controller结构体,把它放入上层文件中的数组里,以后就根据结构体的名字,把这个结构体取出来使用。
假设我们有一个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里面。
我们要发出I2c传输时,要构造出i2c_msg,把构造出的i2c_msg扔给下面的i2c_controller.c,i2c_controller.c会选择某一个i2c控制器,使用里面的master_xfer来传输数据, 所以我们需要构造出一个i2c_controller结构体。
i2c_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
/* 有一个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结构体注册到p_i2c_controllers数组中。
s3c2440_i2c_controller.c文件
中断服务函数,当发成中断是,就会调用中断服务函数,代码如下
void i2c_interrupt_func(int irq)
{
/* 每传输完一个数据将产生一个中断 */
/* 对于每次传输, 第1个中断是"已经发出了设备地址" */
}
s3c2440_i2c_con_init函数,用来初始化I2C,控制器代码如下:
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)
*/
IICCON = (0<<6) | (1<<5) | (30<<0);
/* 注册中断处理函数 */
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中断处理函数。
初始化完成后,就可以调用do_master_tx写I2C从机了,这个函数仅仅启动I2C传输,然后等待,直到数据在中断服务程序中传输完毕后再返回。函数代码如下:
void do_master_tx(p_i2c_msg msg)
{
msg->cnt_transferred = 0;
/* 设置寄存器启动传输 */
/* 1. 配置为 master tx mode */
/* 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表示读操作)。
s3c2440传输函数,根据标志位flags,来指明是读/写(1:读 0:写)。代码如下:
int s3c2440_master_xfer(p_i2c_msg msgs, int num)
{
int i;
for (i = 0; i < num; i++)
{
if (msgs[i]->flags == 0)/* write */
do_master_tx(msgs[i]);
else
do_master_rx(msgs[i]);
}
}
我们定义一个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);
}
第005节_I2C控制器编程_中断
中断控制器是IIC程序中的核心中的核心。
Start信号之后,发出设备地址,在第9个时钟就会产生一个中断,我们根据i2c的流程图来编写中断程序。
每传输完一个数据将产生一个中断,I2C操作的主体在中断服务程序,它可以分为两部分:写操作,读操作。
先分析写操作,代码如下:
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信号已经发送完毕。
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);
p_cur_msg->err = -1;
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信号,停止数据的传输。
写操作:I2C读操作的处理与写操作类似,我们就不进行分析了,代码如下:
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);
}
}
}
我们还要在s3c2440_i2c_con_init函数中设置IICCON寄存器,设置ACK应答使能。
IICCON = (1<<7) | (0<<6) | (1<<5) | (30<<0);
第006节_EEPROM编程和测试代码
从设备程序,只涉及到两个函数分别是:从设备的写函数,从设备的读函数。下面下分析从设备的写函数,代码如下:
#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.lags = 0; /* write */
msg.len = 2;
msg.buf = buf;
msg.err = 0;
msg.cnt_transferred = -1;
/* 调用i2c_transfer */
err = i2c_transfer(&msg, 1);
if (err)
return err;
}
return 0;
}
1).#define AT24CXX_ADDR 0x50
宏定义设备地址。
2).我们每次只写一个字节,所以我们需要构造出len个msg。
3).调用i2c接口函数,传输构造i2C_msg结构体,我们传输指针只需要传输四个字节,我们需要把以前的参数都改成传输指针的格式。
从设备读函数和写函数类似,读函数需要构造两个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].lags = 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].lags = 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;
}
I2c_test测试程序如下所示:
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;
}
}
}
1).调用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控制器。
2).执行do_write_at24cxx()函数用于往at24cxx设备中写入数据,do_write_at24cxx()函数的代码如下所示:
{
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);
}
- addr = get_uint()用于把输入的地址赋值给addr。
- gets(str)用于把输入的字符串存在str字符数组中。
- at24cxx_write(addr, str, strlen(str)+1)调用at24cxx_write函数把输入的数据str,,放在输入的地址addr中。
3).执行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