查看: 1931|回复: 0
打印 上一主题 下一主题

标准模式 IIC 模拟...

[复制链接]
跳转到指定楼层
沙发
发表于 2015-9-24 08:50:36 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

本帖最后由 3htech 于 2013-1-10 08:43 编辑


一,为了那“可恶”的目的
2011 年初,小白菜的工作比较清闲了,于是小白菜就开始IIC 学习之路。
之前在用 IIC 总线(口线模拟)时,总感觉IIC 移植时不够简介易懂,使用时,函数不
能够适应所有IIC 操作,于是小白菜想改写一下IIC 总线操作,使之成为真正的万能IIC 总
线驱动。于是小白菜找到目标了:

一是编写一个移植性极其好的 IIC 总线操作,只需要简单的改动就能完成移植。
二是编写两个函数,一个是向器件发送数据函数,一个是从器件中读取数据函数。并且
这两个函数真正适应所有不同的IIC 器伯的操作。

二,过程抽象
为了达到以上的目的,小白菜需要先对 IIC 进行一次抽象。

1 单片机向器件发送数据时的抽象过程

(这里的抽象指的是抽象出不依赖具体器件的 IIC 操作)
发送时的流程:
(1) MCU 启动总线
(2) 发送器件IIC 地址? 接收 ACK 信号
(3) 发送寄存器地址1 ? 接收 ACK 信号? [发送寄存器地址2 ? 接收 ACK 信号]
有些器件可能没有寄存器地址(小白菜还没有遇到过),所以该步可能不需要。
(4) 发送数据1 ? 接收 ACK 信号
? 发送数据2 ? 接收 ACK 信号
? 发送数据3 ? 接收 ACK 信号
……
(5) MCU 关闭总线,发送完成。

我们来具体的分析一下,

第(1)步启动总线,
第(2)步发送 1B 数据(IIC 地址),接收一个ACK 信号
第(3)步发送 1B 数据(寄存器地址1),接收一个ACK 信号,发送1B 数据(寄存器地址2),
接收一个ACK 信号
第(4)步发送 1B 数据(数据1),接收一个ACK 信号,发送1B 数据(数据2),接收一个
ACK 信号……
第(5)步关闭总线

写到这里,有些人可能看出来了,其实发送时不论数据或地址,都是一个相同过程,启
动总线后,MCU 发送一个字节,等一个ACK 信号,再发送一个字节,等一个ACK 信号,
发送……,哎、发送完了,得了,关闭总线。

继续进行抽象,我们可以把第(2)、(3)、(4)步进行合并,得到IIC 写操作抽象:

第(1)步启动总线,
第(2) (3) (4)步发送 1B 数据,接收一个ACK 信号,直到发送完成
第(5)步关闭总线

到这里之后,似乎是大功告成了,但是小白菜一想,这么多数据,参数肯定是用指针+
数据字节数还进行传递,可是像读写EEPROM 这样的器件,地址就丙三字节,但一次写入
的数据可能有很多个字节,于是小白菜把(2) (3) 合在一起,把数据发送部分(4)单独拿出来。

小白菜得到了最终的 IIC 写操作的抽象

第(1)步启动总线,
第(2) (3)步发送 1B 数据,接收一个ACK 信号,直到发送完成(发送地址)
第(4)步发送 1B 数据,接收一个ACK 信号,直到发送完成 (发送数据)
第(5)步关闭总线

小白菜据此写出了函数名及形参和流程图(就是上面的抽象,所以嘛,就不写了)。

extern uint8 IIC_MCU_Send_Str(uint8 *PAddr, uint8 AddrNum, uint8 *PDataAddr, uint16 DataNum)

*PAddr :I 第1 批发送的数据的首地址。这部分IC 地址以及子地址。PAddr[0]中存放IIC
地址,后面的存放子地址。
AddrNum :IIC 以及子地址的字节数。不可为0.
*PDataAddr :第2 批发送的数据的首地址。这部分是发送的数据。
DataNum :第2 批要发送的字节数(最大为65536 个字节)。为0 时不发送这一部分。

2 单片机从有寄存器的IIC 器件读取数据时的抽象过程:

发送时的流程:

(1) MCU 启动总线
(2) 发送器件IIC 地址? 接收 ACK 信号
(3) 发送寄存器地址1 ? 接收 ACK 信号? [发送寄存器地址2 ? 接收 ACK 信号]
有些器件可能没有寄存器地址(小白菜还没有遇到过),所以该步可能不需要。
(4) MCU 重新启动总线
(5) 发送器件IIC 地址(最低位置1 以表明是读操作) ? 接收 ACK 信号
(6) 接收数据1 ? 发送 ACK 信号
接收数据2 ? 发送 ACK 信号
接收数据3 ? 发送 ACK 信号
……
(7) 接收最后一字节数据? 发送非ACK 信号
(8) MCU 关闭总线,接收完成。

根据该流程,我们可以清楚地得到读取时的抽象:

第(1)步 MCU 启动总线
第(2) (3) 步发送 1B 数据,接收一个ACK 信号,直到发送完成(发送地址)
第(4) 步 MCU 重新启动总线
第(5) 步发送器件IIC 地址(最低位置1 以表明是读操作) ? 接收 ACK 信号
第(6) 步接收前面的字节,发送ACK 信号
第(7) 步接收最后一字节数据,发送非ACK 信号
第(8) 步 MCU 关闭总线,接收完成。

虽然这里步骤多了,但是,函数参数也用不了几个,首先要知道地址吧,还要知道数据读出
来后存放在哪里吧,小白菜想了想,这个函数的参数和写操作函数的参数一样就行了,

于是小白菜据写出了函数名及形参和流程图(就是上面的抽象,所以嘛,你懂得)

extern uint8 IIC_MCU_Rcv_Str(uint8 *PAddr, uint8 AddrNum, uint8 *PDataAddr, uint16
DataNum)
*PAddr :发送的数据的首地址。这部分IC 地址以及子地址。PAddr[0]中存放IIC 地址,
后面的存放子地址。
AddrNum :IIC 以及子地址的字节数。不可为0.
*PDataAddr :存放所接收数据的首地址
DataNum :要接收的字节数。

三 奋笔疾书 + 代码移植

小白菜开始了写代码了。因为是小白菜嘛,所以一开始也不知道哪些地方在移植时需要
修改,于是就开始先写代码,可能在写的时候就能知道了。写啊写,写啊写,终于让小白菜
写完了。

写着写着还真让小白菜找出了哪里需要移植了。口线需要更改吧,不同的单片机,头
文件不一样吧,还得有延时函数需要改吧……
于是小白菜把在移植时需要更改的地方做了一个“表”,放在H 文件中的“移植修改”
部分。这样,在修改时就可以只修改H 文件中的一个地方,就能快速的完成移植。
现在想想,小白菜有的时候也不是那么菜嘛(偷笑 ing)。

到现在为止,小白菜的目的已经达到了。突然,后背一凉,心里一个想法冒出来了,
刚写完的代码还没测试就飘飘然了!哎,被胜利冲昏了头脑。于是小白菜自已测试了一番,
Bug 还真是有,改改更健康~~

四使用说明

4.1 移植修改

移植修改都在 H 文件中的移植修改部分。下面进行具体说明。
//----------------------------------------------------------------------------//
// 编号:1
// 名称:
// 功能:单片机寄存器头文件,例如reg51.h
//----------------------------------------------------------------------------//
#include "ATT703x.H"
4.1.1 这部分是请您把使用的单片机的头文件包含进来。大虾们用过MCU 多了,知道不同
的MCU,其寄存器定义是不一样滴,不是所有的51 单片机都用Reg51.H 或Reg52.H 头文
件的。

//----------------------------------------------------------------------------//
// 编号:2
// 名称:SDA, SCL
// 功能:模拟I2C 数据传送位
//----------------------------------------------------------------------------//
#if defined(IIC_IO_ENABLE)
sbit SDA = P0^0; // 模拟I2C 数据传送位。
sbit SCL = P2^6; // 模拟I2C 时钟控制位。
#endif
4.1.2 SDA 和SCL 口线定义。这里就是您用的口线,如果您告诉我您不知道怎么改,好吧,
你赢了……


//----------------------------------------------------------------------------//
// 编号:3
// 名称:IIC_Delay_1US()
// 功能:精确的1 微秒延时函数。请根据您所用的单片机来正确设置。
// :如果您的系统中有精确的微妙级延时函数,那么您可以直接使用。
// :例如,您的延时函数是Delay_1us(),那么您可以使用下句
// :#define IIC_Delay_1us() Delay_1us()
// :来实现延时。
//----------------------------------------------------------------------------//
#defineIIC_Delay_500ns() _nop_();_nop_();_nop_();_nop_();_nop_();_nop_();
#define IIC_Delay_1US() IIC_Delay_500ns();IIC_Delay_500ns();
4.1.3 软件延时函数,这里是标准IIC,不是快速IIC。1us 的延时怎么做呢?当然是nop 函
数了。如果您不知道一个nop 的执行时间,那么说明您需要好好看看手册了。


4.1.4 好多单片机都需要设置时钟,设置GPIO 状态,所以在使用IIC 之前,请一定确保MCU
先初始化完毕。

4.2 函数说明

4.2.1 MCU 向IIC 器件发送多字节数据函数
//----------------------------------------------------------------------------//
// MCU 向IIC 器件发送多字节数据函数(对外提供服务)
//函数名称:IIC_MCU_Send_Str
//函数功能:MCU 向IIC 从器件发送多字节数据。本函数是写IIC 从器件的抽象函数。
//入口参数:
// *PAddr: IIC 地址以及子地址。PAddr[0]中存放IIC 地址,后面的存放子地址。
// AddrNum : IIC 以及子地址的字节数。不可为0.
//
// *PDataAddr: 第 2 批发送的数据的首地址。这部分是发送的数据。
// DataNum : 第 2 批要发送的字节数(最大为65536 个字节)。为0 时不发送这一部分。
//出口参数:0 = 操作成功,1 = 操作出错。
//重要说明:这是一个从启动IIC 总线到发送数据再到最后结束总线为止的完整的发送过程。
// 数据发送的顺序是先发送PAddr[0],最后发送PAddr[AddrNum - 1],然后发送
// PDataAddr[0],最后发送PDataAddr[DataNum - 1]。
// 一般地,PAddr 用于发送器件IIC 地址和子地址,PDataAddr 用于发送数据。
// 本函数对有无子地址的IIC 器件都适用。
//----------------------------------------------------------------------------//
extern uint8 IIC_MCU_Send_Str(uint8 *PAddr, uint8 AddrNum, uint8 *PDataAddr, uint16
DataNum)
应用示例:
从 0x00 字节地址开始写AT24C02,写入10 个字节数(这里不考虑页写等待,因为和
IIC 写无关),这10B 数据存放在unsigned char Buf[10]中,写入时要求Buf[0]写入0x00 字节
地址,Buf[1]写入0x01 字节地址……。
A2、A1、A0 全接地。(有人说我没说明WP 的接法……我只有一个问题,你是来砸场
子的么!!!)
首先组织 IIC 地址,设置一数组unsigned char Addr[2],其中Addr[0] = 0xA0,Addr[1] = 0x00;
Addr [0]中存放的是(二进制表示) 1 0 1 0 A2 A1 A0 0(LSB)
Addr [1]中存放的是(二进制表示) a7 a6 a5 a4 a3 a2 a1 a0(LSB)
调用时 IIC_MCU_Send_Str(Addr, 2, Buf, 10);
您还应当查看一下函数的返回值,是0 表示操作成功,否则操作失败。


4.2.1 MCU 从有子地址的IIC 器件中接收多字节函数
//----------------------------------------------------------------------------//
// MCU 从有子地址的IIC 器件中接收多字节函数(对外接口)
//函数名称:IICMCURcvStr
//函数功能:本函数用于有子地址的IIC 器件的读操作。
//入口参数:
// *PAddr: IIC 地址以及子地址。PAddr[0]中存放IIC 地址,后面的存放子地址。
// AddrNum : IIC 以及子地址的字节数。为0 时出错.
// *PDataAddr:存放所接收数据的首地址
// DataNum : 要接收的字节数。合法值1-65535。为0 时出错。
//
//出口参数:0 = 操作成功,1 = 操作出错。
//重要说明:
// 读取的第一个数据存放在PDataAddr[0]中,第一个存放在PDataAddr[1]中……
// 有子地址的IIC 器件的读操作是:
// MCU 先启动总线,然后发送器件的IIC 地址和需要操作的子地址
//(这一部分就是*PAddr),之后重新启动总线,再次发送器件的IIC 地址且最低位置1 以表
明是读
// 操作,等待应答后便开始接收数据(这一部分是*PDataAddr),最后关闭总线。
//----------------------------------------------------------------------------//
extern uint8 IIC_MCU_Rcv_Str(uint8 *PAddr, uint8 AddrNum, uint8 *PDataAddr, uint16
DataNum)
读取的第一个数据存放在 PDataAddr[0]中,第一个存放在PDataAddr[1]中……
应用示例:
从 0x00 字节地址开始读AT24C02,读10 个字节数并且存放在unsigned char Buf[10]中,
A2、A1、A0 全接地。(有人说我没说明WP 的接法……还提这个问题!!)
首先组织 IIC 地址,设置一数组unsigned char Addr[2],其中Addr[0] = 0xA0,Addr[1] = 0x00;
Addr [0]中存放的是(二进制表示) 1 0 1 0 A2 A1 A0 0(LSB)
Addr [1]中存放的是(二进制表示) a7 a6 a5 a4 a3 a2 a1 a0(LSB)
调用时 IIC_MCU_Rcv_Str (Addr, 2, Buf, 10);
这样,Buf[0]是读取到的0x00 字节地址的数据,Buf[1]是0x01 字节地址的数据……
您还应当查看一下函数的返回值,是 0 表示操作成功,否则操作失败。

五 最后的废话
好吧,有人会说这个实现的有点罗嗦,不如直接以IIC 地址,寄存器地址做参数来的方
便;虽然有时候我也这么觉得。
但是、但是、但是什么呢?下期见!

3htech
我是一颗小白菜转载

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 加入中科因仑

本版积分规则

快速回复 返回顶部 返回列表