最近运用SN7326做了一个I2C驱动,由于没有多的I2C,所以用GPIO口模拟了I2C来通信,做一个记录,便于以后复习运用。
I2C最麻烦的就是时序问题,调试起来有点困难,最好是按照标准时序来调试,不然 会出现一些莫名其妙的错误,比如收到的数据为0xFF,比如没有ack回复,收不到数据等等。
下面这是I2C时序图:
大概的原理是这样的,I2C一般是两根线,一根是SCL,一根是SDA,scl为时针线,sda为数据线,一般如果I2C不工作的时候SDA、SCL为一直保持高电平。
我做的因为是GPIO模拟I2C,所以每次读取的时候要把端口配置为输出,写的时候要配置成输入。这个要注意一下。
当INT中断来了之后,会有一个低电平的跳变, I2C总线的开始时序,将SDA数据线拉高,将SCL时钟线拉高,延时一段时间,在SCL为高电平时,拉低SDA,延时一段时间,就是在SCL为高电平时,在SDA上给一个下降沿,表示开始信号。
I2C总线的停止时序,将SDA数据线拉低,在SCL为高电平时,将SDA数据线拉高,延时一段时间,所以在SCL为高电平时,在SDA上给一下上升沿,表示停止信号。
I2C总线应答时序,首先是需要将模拟数据线的SDA口配置为输入口,将时钟线SCL拉高,延时一段时间,等待从设备应答,从设备应答时,SDA上会产生一个高电平,主设备的GPIO口检测到高电平,则表示应答成功,如果检测到应答信号,则拉SCL时钟线拉低。延时一段时间,将模拟SDA口的GPIO口配置为输出。
I2C写数据时序,由于I2C属于串行总线,所以数据都是一位一位的传输,将SCL时钟线拉高,准备传输数据,延时一段时间,将I2CBuf的最高位传输到SDA数据线上,拉低SCL时钟线,延时一段时间,所以是当SCL有一个上升沿时,传输数据。
以上三步表示传输完一位数据。然后将I2CBuf左移一位,再重复以上三步。
最后,将SCL位低,延时一段时间,再将SDA位高,延时一段时间。此操作是为了释放时钟线SCL和数据线SDA。
I2C读数据函数的时序,首先需要将模拟SDA数据线的GPIO口配置为输入,将时钟线SCL拉高,延时一段时间,读取数据线SDA上的电平,然后写入I2CBuf的最低位。拉低SCL时钟线,延时一段时间。所以当SCL有一个下降沿时,读取数据。
以上三步表示读取一位数据,读一个字节,需要重复以上操作步骤8次。
接下来是写寄存器时序:
I2C写寄存器函数的时序,主设备先发出一个开始信号,然后写入从设备地址。每一个从设备的地址是唯一的。,主设备接收应答信号,主设备向从设备写入寄存器的地址,主设备接收应答信号,主设备向从设备写入数据,主设备接收应答信号,主设备发出停止信号。
然后是读寄存器时序:
I2C读寄存器函数的时序:
主设备发出开始信号,写入从设备地址,主设备接收从设备发来的应答信号,写入寄存器地址,主设备接收应答信号,主设备再次发出开始信号,主设备向从设备写入(从设备的地址+1),此时加1表示要从寄存器中读数据,从设备给出应答信号,从从设备的寄存器中读出数据传给变量,主设备发出停止信号。最后崽返回读出的数据。
以上就是时序的大概的原理。
接下来是代码运用,我写的还有一点问题,就是用逻辑分析仪采样的时候是正常的,但是代码接收的时候是0xFF,以后有修改再更新,先贴上:
这是读取的细节:
接下来是代码实现,一般代码分为这几个步骤:初始化为I2C模式,配置I2C的工作方式,比如地址,时钟源,分频,然后启动I2C控制器。
#include "SN7326.h"
#include
#include // for STL string class
#include "define.h"
extern bool(__stdcall *InitializeWinIo)(void);
extern void(__stdcall *ShutdownWinIo)(void);
extern PBYTE(__stdcall *MapPhysToLin)(PBYTE pbPhysAddr, DWORD dwPhysSize, HANDLE *pPhysicalMemoryHandle);
extern bool(__stdcall *UnmapPhysicalMemory)(HANDLE PhysicalMemoryHandle, PBYTE pbLinAddr);
extern bool(__stdcall *GetPhysLong)(PBYTE pbPhysAddr, PDWORD pdwPhysVal);
extern bool(__stdcall *SetPhysLong)(PBYTE pbPhysAddr, DWORD dwPhysVal);
extern bool(__stdcall *GetPortVal)(WORD wPortAddr, PDWORD pdwPortVal, BYTE bSize);
extern bool(__stdcall *SetPortVal)(WORD wPortAddr, DWORD dwPortVal, BYTE bSize);
extern bool(__stdcall *InstallWinIoDriver)(PSTR pszWinIoDriverPath, bool IsDemandLoaded);
extern bool(__stdcall *RemoveWinIoDriver)(void);
/*
*FunctionName: I2CDelay
* Purpose : I2C时序模拟SCL时间间隔(周期),需要根据Slave性能及单片机工作频率调整
* Parameters : 无
*/
static void I2C_Delay(void) {
__asm {
nop
nop
nop
nop
}
}
/*
* FunctionName: I2CStart
* Purpose: 模拟I2C开始信号, SCL电平拉高,SDA由高变低,I2C开始传输数据
* Parameters: 无
*/
static void SN7325_i2c_START(void) {
SN7325_I2C_SCL_H;
SN7325_I2C_SDA_H;
I2C_Delay();
SN7325_I2C_SDA_L;
SN7325_I2C_SCL_L;
}
static void SN7325_i2c_RESTART(void) {
SN7325_I2C_SCL_L;
SN7325_I2C_SCL_H;
SN7325_I2C_SDA_H;
I2C_Delay();
SN7325_I2C_SDA_L;
SN7325_I2C_SCL_L;
}
/*
* FunctionName: I2CStop
* Purpose: 模拟I2C结束信号,SCL电平拉高,SDA由低边高,I2C停止传输数据
* Parameters: 无
*/
static void SN7325_i2c_STOP(void) {
int i;
SN7325_I2C_SCL_H;
SN7325_I2C_SDA_L;
I2C_Delay();
SN7325_I2C_SDA_H;
}
/*
* FunctionName: I2CSendNoACK
* Purpose: 模拟I2C无ACK响应
* Parameters: 无
*/
static void SN7325_i2c_NOACK(void) {
SN7325_I2C_SDA_H;
SN7325_I2C_SCL_H;
SN7325_I2C_SCL_L;
}
/*
* FunctionName: I2CCheckACK
* Purpose:把串口设置为输入模式,测试串口是否有响应
* Parameters: 无
*/
static bool SN7325_i2c_WAITACK(void) {
unsigned long tempACK = 0;
unsigned long res = 0;
unsigned long time_out = 0;
SN7325_I2C_SDA_L;
SetPhysLong((PBYTE)KEY_IO5_ADDR, 0x00908200);//输入模式
SN7325_I2C_SCL_H;
do {
GetPhysLong((PBYTE)KEY_IO5_ADDR, (PDWORD)&tempACK);//获取ack
if (tempACK)
res =1;
else
res = 0;
time_out++;
if (time_out > 3)//超时处理
{
res = 0;
}
} while (( !res )||(time_out> 3));
SN7325_I2C_SCL_L;
return res;
}
/*
* FunctionName: I2CTWRITE8BIT
* Purpose:模拟I2C发送一个字节数据,从低位开始写
* Parameters: sendData-发送的一个字节数据
*/
static void SN7325_i2c_WRITE8B(unsigned char input) {
unsigned char serialNum;
for (serialNum = 8; serialNum >= 1; serialNum--)
{
if (input & 0x80) {//判断最高位是否为1
SN7325_I2C_SDA_H;
}
else {
SN7325_I2C_SDA_L;
}
SN7325_I2C_SCL_H;
I2C_Delay();
SN7325_I2C_SCL_L;
input <<= 1;//数据左移一位
}
}
/*
* FunctionName: I2CTREAD8BIT
* Purpose:模拟I2C接收一个字节数据
* Parameters: 无
*/
static unsigned char SN7325_i2c_READ8B(void)
{
unsigned char serialNum;
unsigned char I2CBUF = 0;
unsigned long SDAStatus = 0;
SetPhysLong((PBYTE)KEY_IO5_ADDR, 0x00908200); //设置SDA/串口为输入模式
for (serialNum = 8; serialNum >=1; serialNum--) {
I2CBUF <<= 1;
SN7325_I2C_SCL_H;
GetPhysLong((PBYTE)KEY_IO5_ADDR, (PDWORD)&SDAStatus);
if (SDAStatus) {
I2CBUF |= 0x01;
//I2CBUF++;//表示读取数据
}
SN7325_I2C_SCL_L;
}
printf("I2CBUF = %x", I2CBUF);
return I2CBUF;
}
/*
写寄存器的标准流程为
1. Master发起START
2. Master发送I2C addr(7bit)和w操作0(1bit),等待ACK
3. Slave发送ACK
4. Master发送reg addr(8bit),等待ACK
5. Slave发送ACK
6. Master发送data(8bit),即要写入寄存器中的数据,等待ACK
7. Slave发送ACK
8. 第6步和第7步可以重复多次,即顺序写多个寄存器
9. Master发起STOP
*/
void SN7325_Write_i2c(unsigned char WriteDeviceAddress, unsigned char Command, unsigned char Wdata)
{
SN7325_i2c_START();
SN7325_i2c_WRITE8B(WriteDeviceAddress);
SN7325_i2c_WAITACK();
SN7325_i2c_WRITE8B(Command);
SN7325_i2c_WAITACK();
SN7325_i2c_WRITE8B(Wdata);
SN7325_i2c_NOACK();
SN7325_i2c_STOP();
}
/*
I2C读寄存器函数的时序:
1、主设备发出开始信号
2、写入从设备地址
3、主设备接收从设备发来的应答信号
4、写入寄存器地址
5、主设备接收应答信号
6、主设备再次发出开始信号
7、主设备向从设备写入(从设备的地址+1),此时加1表示要从寄存器中读数据
8、从设备给出应答信号
9、从从设备的寄存器中读出数据传给变量
10、主设备发出停止信号
11、返回读出的数据
*/
unsigned short SN7325_Read_i2c(unsigned char ReadDeviceAddress, unsigned char Command)
{
unsigned char r_byte;
SN7325_i2c_START();
SN7325_i2c_WRITE8B(ReadDeviceAddress - 1); //先写入从设备的地址。表示要向从设备中写地址和数据
SN7325_i2c_WAITACK();
SN7325_i2c_WRITE8B(Command); //写入寄存器的地址,数据将从些地址中读出
SN7325_i2c_WAITACK();
//SN7325_i2c_RESTART();
SN7325_i2c_START();
SN7325_i2c_WRITE8B(ReadDeviceAddress); //发送器件地址,地址LSB最后一位为0代表写入,1代表读取
SN7325_i2c_WAITACK();
r_byte = SN7325_i2c_READ8B();
//SN7325_i2c_WAITACK();//lsx add
SN7325_i2c_NOACK();
SN7325_i2c_STOP();
//r_byte = SN7325_i2c_READ8B(); //从地址regaddr中读出数据
return r_byte;
}
应用的话一般是先写,注意的是SN7325_Write_i2c(unsigned char WriteDeviceAddress, unsigned char Command, unsigned char Wdata)第一个是从机的地址,第二个是你要写入的寄存器地址,第三个是你要写入的值。根据个人所用的器件不同,具体查询器件的数据手册。
然后再判断接收到INT之后读取解析。
暂时就这么多,以后再深入运用的话再深入的探讨分享,有问题欢迎提出一起探讨。