1、思路:需要使用SDA管脚下降沿中断,通过I2C的通信协议可知,主机发送开始信号时,会先把SDA管脚拉低,所以从机在SDA下降沿会进入中断,然后做相关的操作。
2、优点:可以让没有硬件I2C的单片机通过此方式模拟通信;实测从机收发数据正常,无丢包现象;90%的代码都注释,避免初学者看不明白。
3、缺点:由于是模拟的方式,所以主机的速度不能太快,否则单片机处理不过来;SDA管脚的中断优先级尽量调到最高,避免接收数据时进入其它中断,导致丢包;接收过程在中断里面处理,所以占用中断的时间受传输速度和数据长度的影响。
4、补充:代码中有几个部分需要做超时等待处理,比如i2c_slave_wait_for_scl函数里面,建议超时时间是I2C时钟的1~2个周期以上(太短的话会通信错误),比如I2C的时钟是100K时,周期是10us。那么怎么能够得到10us的时间呢,可以在主循环里面通过管脚输出不同的电平,然后用示波器或者逻辑分析仪来看该管脚的高电平时间,如下代码所示,查看PB0的高电平时间就是i2c_slave_wait_for_scl函数耗时时间(测试时为了时间更准确,建议关闭所有中断)。
void mian()
{
while(1)
{
//让PB0管脚输出高电平
PB0=1;
//SCL管脚确保一直是高,所以不为低时会等待超时才退出
i2c_slave_wait_for_scl(0);
//让PB0管脚输出低电平
PB0=0;
//SCL管脚确保一直是高,所以不为低时会等待超时才退出
i2c_slave_wait_for_scl(0);
}
}
以下是I2C从机的完整的代码
i2c_slave.c
#include "i2c_slave.h"
//===================================================================
// 变量定义
//===================================================================
i2c_slave_t i2c_slave;
//I2C从机初始化(调用此函数初始化I2C从机)
void i2c_slave_init()
{
I2C_SDA = 1; //设置高电平
I2C_SDA_PC = 1; //输入模式
I2C_SDA_PU = 1; //打开上拉电阻
I2C_SCL = 1; //设置高电平
I2C_SCL_PC = 1; //输入模式
I2C_SCL_PU = 1; //打开上拉电阻
//初始化I2C为空闲状态
i2c_slave.state = I2C_STATE_IDLE;
i2c_slave.flag.isSuccess = 0;
//设置SDA管脚下降沿中断(不同单片机根据对应的语法设置即可)
_integ = 0B00000010; //INT0管脚下降沿中断
_int0f = 0; //清除INT0中断标志
_int0e = 1; //使能INT0中断
//给出默认的发送数据(测试用)
uint8_t i;
for(i=0;i<I2C_SLAVE_TX_DATA_SIZE;i++)
{
i2c_slave.tx_data[i]=i;
}
}
//SDA管脚中断函数(此函数在SDA下降沿中断里面调用)
void i2c_slave_sda_interrupt_callback()
{
//关闭INT0中断(关闭SDA管脚的中断,每次通信中断一次即可)
_int0e = 0;
//等待SCL管脚输出低电平
i2c_slave_wait_for_scl(0);
if(!i2c_slave.flag.isSuccess)
{
//超时->退出
goto end;
}
//检测到开始信号
i2c_slave.state = I2C_STATE_START;
//接收计次清0
i2c_slave.rx_offs = 0;
while(i2c_slave.state == I2C_STATE_START)
{
volatile uint8_t val;
//读从机地址 + 读/写位
val = i2c_slave_read_byte();
//是否读结束
if(!i2c_slave.flag.isSuccess)
{
//是否检测到开始信号
if(i2c_slave.state == I2C_STATE_START)
{
continue;
}
//超时、停止->退出
goto end;
}
//判断地址是否匹配
if((val&0xFE) != I2C_SLAVE_ADDRESS)
{
//地址不匹配->退出
goto end;
}
i2c_slave.state = I2C_STATE_DEVICE;
//发送ACK
i2c_slave_send_ack();
if(!i2c_slave.flag.isSuccess)
{
//超时->退出
goto end;
}
//当前为主机读操作
if((val&1) != 0)
{
i2c_slave_send_data();
if(!i2c_slave.flag.isSuccess)
{
if(i2c_slave.state == I2C_STATE_START)
{
continue;
}
else if(i2c_slave.state == I2C_STATE_NACK)
{
//主机接收/从机发送成功
i2c_slave.flag.TxFinish = 1;
//可以在主循环里面判断此标志做相关的发送成功处理
goto end;
}
else
{
goto end;
}
}
}
//当前为主机写操作
else
{
i2c_slave_receive_data();
if(!i2c_slave.flag.isSuccess)
{
//主机发送开始信号
if(i2c_slave.state == I2C_STATE_START)
{
continue;
}
//主机发送停止信号
else if(i2c_slave.state == I2C_STATE_STOP)
{
//主机发送/从机接收成功
i2c_slave.flag.RxFinish = 1;
//可以在主循环里面判断此标志做相关的接收成功处理
//接收的数据存储在i2c_slave.rx_data
//接收的个数为i2c_slave.rx_offs
goto end;
}
//超时或者其他错误
else
{
goto end;
}
}
}
}
end:
//退出时把管脚恢复到上拉输入模式,确保总线为空闲状态
I2C_SDA = 1;
I2C_SDA_PC = 1;
I2C_SDA_PU = 1;
I2C_SCL = 1;
I2C_SCL_PC = 1;
I2C_SCL_PU = 1;
//等待SCK和SDA拉高(出错退出时可能主机还在操作总线,此处会一直等待,如果主机一直不释放总线
//则会卡死在中断,如果不加等待,则退出中断函数之后可能马上又进入中断,所以根据需要添加)
//总线有多个设备时需要去掉以下代码
while (I2C_SDA==0 || I2C_SCL==0);
//I2C状态为空闲
i2c_slave.state = I2C_STATE_IDLE;
//清除中断标志
_int0f = 0;
//重新使能SDA管脚中断
_int0e = 1;
}
//等待SCL管脚出现需要的电平
void i2c_slave_wait_for_scl(uint8_t level)
{
uint8_t count = 0;
//等待SCL管脚出现对应的电平
while(I2C_SCL != level)
{
count ++;
//此处等待的时间建议是I2C的1~2个周期,比如I2C的时钟是100K时,周期是10us
if(count >= 50)
{
//超时(没有在规定的时间内等待到需要的电平)
i2c_slave.state = I2C_STATE_TIMEOUT;
//标志失败
i2c_slave.flag.isSuccess = 0;
return;
}
}
//标志成功
i2c_slave.flag.isSuccess = 1;
}
//I2C从机写1个字节
void i2c_slave_write_byte(uint8_t val)
{
uint8_t i;
for(i=0; i<8; i++)
{
//先写高位,所以与0x80
if((val&0x80) != 0)
{
//配置SDA管脚输出高电平
I2C_SDA_PC = 0;
I2C_SDA = 1;
}
else
{
//配置SDA管脚输出低电平
I2C_SDA_PC = 0;
I2C_SDA = 0;
}
val = val << 1;
//等待主机读取(SCL上升沿读取)
i2c_slave_wait_for_scl(1);
if(!i2c_slave.flag.isSuccess) return;
//等待SCL低电平出现
i2c_slave_wait_for_scl(0);
if(!i2c_slave.flag.isSuccess) return;
}
//从机释放SDA管脚
I2C_SDA_PC = 1;
I2C_SDA_PU = 1;
I2C_SDA = 1;
}
//I2C从机读1个字节
uint8_t i2c_slave_read_byte()
{
uint8_t i;
uint8_t val=0;
for(i=0; i<8; i++)
{
//等待SCL高电平出现
i2c_slave_wait_for_scl(1);
if(!i2c_slave.flag.isSuccess) return 0;
//保存数据,先收最高位,所以先左移后保存最低位
val = val << 1;
if(I2C_SDA)
{
val |= 0x01;
}
uint8_t count = 0;
//等待SCL低电平出现
while(I2C_SCL)
{
count++;
//此处等待的时间建议是I2C的1~2个周期,比如I2C的时钟是100K时,周期是10us
if(count >= 50)
{
i2c_slave.state = I2C_STATE_TIMEOUT;
i2c_slave.flag.isSuccess = 0;
return 0;
}
//获取SDA管脚状态
uint8_t temp = 0;
if(I2C_SDA)
{
temp=1;
}
//SCL是否出现低电平
if(I2C_SCL == 0)
{
//回去循环接收
break;
}
//SDA管脚是否发生变化
if((val & 1) != temp)
{
//SDA当前是高电平
if(temp)
{
i2c_slave.state = I2C_STATE_STOP;
}
//SDA当前是低电平
else
{
i2c_slave.state = I2C_STATE_START;
}
i2c_slave.flag.isSuccess = 0;
return 0;
}
}
}
return val;
}
//I2C从机发送ACK
void i2c_slave_send_ack()
{
//从机的SDA管脚输出低做为ACK信号
I2C_SDA_PC = 0;
I2C_SDA = 0;
//等待主机读取(SCL上升沿读取)
i2c_slave_wait_for_scl(1);
if(!i2c_slave.flag.isSuccess) return;
//等待SCL低电平出现
i2c_slave_wait_for_scl(0);
if(!i2c_slave.flag.isSuccess) return;
i2c_slave.flag.isSuccess = 1;
//从机释放SDA管脚
I2C_SDA_PC = 1;
I2C_SDA_PU = 1;
I2C_SDA = 1;
}
//I2C从机读取ACK
void i2c_slave_read_ack()
{
//等待SCL高电平出现
i2c_slave_wait_for_scl(1);
if(!i2c_slave.flag.isSuccess) return;
//读ACK
uint8_t val=0;
if(I2C_SDA)
{
val = 0x01;
}
uint8_t count = 0;
//等待SCL低电平出现
while(I2C_SCL)
{
count++;
//此处等待的时间建议是I2C的1~2个周期,比如I2C的时钟是100K时,周期是10us
if(count >= 50)
{
i2c_slave.state = I2C_STATE_TIMEOUT;
i2c_slave.flag.isSuccess = 0;
return;
}
//获取SDA管脚状态
uint8_t temp = 0;
if(I2C_SDA)
{
temp=1;
}
//SCL是否出现低电平
if(I2C_SCL == 0)
{
//回去循环接收
break;
}
//SDA管脚是否发生变化
if((val & 1) != temp)
{
//SDA当前是高电平
if(temp)
{
i2c_slave.state = I2C_STATE_STOP;
}
//SDA当前是低电平
else
{
i2c_slave.state = I2C_STATE_START;
}
i2c_slave.flag.isSuccess = 0;
return;
}
}
if(val == 0x0)
{
i2c_slave.flag.isSuccess = 1;
}
else
{
i2c_slave.flag.isSuccess = 0;
i2c_slave.state = I2C_STATE_NACK;
}
}
//I2C从机发送一个字节
void i2c_slave_send_data()
{
i2c_slave.tx_offs = 0;
loop:
i2c_slave_write_byte(i2c_slave.tx_data[i2c_slave.tx_offs]);
if(i2c_slave.tx_offs < I2C_SLAVE_TX_DATA_SIZE) i2c_slave.tx_offs++;
if(!i2c_slave.flag.isSuccess)
{
return;
}
i2c_slave_read_ack();
if(i2c_slave.flag.isSuccess)
{
goto loop;
}
}
//I2C从机接收一个字节
void i2c_slave_receive_data()
{
volatile uint8_t data;
loop:
data = i2c_slave_read_byte();
//是否读失败
if(!i2c_slave.flag.isSuccess) return;
i2c_slave_send_ack();
i2c_slave.rx_data[i2c_slave.rx_offs] = data;
if(i2c_slave.rx_offs < I2C_SLAVE_RX_DATA_SIZE) i2c_slave.rx_offs++;
if(i2c_slave.flag.isSuccess)
{
goto loop;
}
i2c_slave.flag.isSuccess = 1;
}
对应的头文件
i2c_slave.h
#ifndef I2C_SLAVE_H_
#define I2C_SLAVE_H_
//===================================================================
// I2C管脚定义
//===================================================================
#define I2C_SDA _pb2 //管脚状态寄存器, 1:高电平 0:低电平
#define I2C_SDA_PC _pbc2 //管脚模式寄存器, 1:输入模式 0:输出模式
#define I2C_SDA_PU _pbpu2 //管脚上拉电阻寄存器, 1:使能 0:禁止
#define I2C_SCL _pb1 //管脚状态寄存器, 1:高电平 0:低电平
#define I2C_SCL_PC _pbc1 //管脚模式寄存器, 1:输入模式 0:输出模式
#define I2C_SCL_PU _pbpu1 //管脚上拉电阻寄存器, 1:使能 0:禁止
//I2C的工作状态
#define I2C_STATE_IDLE 0
#define I2C_STATE_NACK 1
#define I2C_STATE_ACK 2
#define I2C_STATE_START 3
#define I2C_STATE_STOP 4
#define I2C_STATE_DEVICE 5
#define I2C_STATE_TIMEOUT 6
#define I2C_SLAVE_ADDRESS 0x5C //从机地址
#define I2C_SLAVE_RX_DATA_SIZE 10 //接收缓存大小
#define I2C_SLAVE_TX_DATA_SIZE 10 //发送缓存大小
//===================================================================
// 数据类型定义
//===================================================================
typedef struct
{
uint8_t state; //状态
uint8_t rx_data[I2C_SLAVE_RX_DATA_SIZE];
uint8_t rx_offs;
uint8_t tx_data[I2C_SLAVE_TX_DATA_SIZE];
uint8_t tx_offs;
struct
{
uint8_t isSuccess : 1; //操作成功标志
uint8_t RxFinish : 1; //接收成功标志
uint8_t TxFinish : 1; //发送成功标志
}flag;
}i2c_slave_t;
//===================================================================
// 变量声明
//===================================================================
extern i2c_slave_t i2c_slave;
//===================================================================
// 函数声明
//===================================================================
void i2c_slave_init();
void i2c_slave_sda_interrupt_callback();
uint8_t i2c_slave_read_byte();
void i2c_slave_send_ack();
void i2c_slave_write_byte(uint8_t val);
void i2c_slave_receive_data();
void i2c_slave_send_data();
void i2c_slave_wait_for_scl(uint8_t level);
#endif
版权声明:本文为dear_Wally原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。