|
本帖最后由 sjzwangjh 于 2024-10-10 11:44 编辑
I2C是单片机使用中经常使用的一个通讯端口,很多外设的控制和数据输出,都是以I2C总线的形式挂接在MCU端口上的,下面我们一步一步开始I2C的编程,一条I2C总线上,必然有一个“主控器件”,称之为“主器件”,其它器件都是“从器件”,每个“从器件”有一个唯一的“7bit”地址,所以一个I2C总线上最多可以挂载127个“从器件”,AVR器件中,对“0”地址发送数据是“广播”功能,所有的器件都可以接收数据。
一. 认识I2C的时序
I2C通讯只需要两根线,通常命名为SCL和SDA,SCL是通讯数据“同步时钟”,由“主器件”发出,SDA是IO数据,可以由“主器件”发送,也可以由“从器件”发送,由此I2C的时序共有2种时序,“主写时序”和“主读时序”
由于SDA挂接在所有器件上,并且通讯时会出现“数据竞争”,正常情况下,“输出0”如果直接和“输出1”短路,则会出现“竞争状态,此时总线上会产生“大电流”,总线的电平”既不是高电平,也不是低电平“,取决于两个输出的”强者“,为了避免这种情况的发生,I2C总线规定,必须在SDA和SCK上挂接”上拉电阻“到VCC,”上拉电阻“的大小,取决于”传输速率“,传输速度越高,电阻取值越小。I2C总线只输出”低电平“,高电平由”上拉电阻“产生,从而避免了”数据竞争“的出现,从而实现I2C的”安全通讯“。具体时序如图1~图4所示。
完整的通讯时序如下:
图1 I2C总线设备连接图
图2 I2C SCL和SDA的时序关系图
图3 I2C 开始通讯(START)和结束通讯(STOP)时序图
图4 I2C发送地址时序图
二. 编写程序
1. 为了方便程序的移植,一般首先用宏定义实现端口的“基本控制”,然后在程序种用“宏定义”代替“具体的端口操作”,从而实现程序的可移植性,举例如下:
// 预备宏定义,为了方便移植,初始定义“越少越好”,宏定义给我们提供了“凡是重复皆可宏”的使用方式,
#ifndef DEFINE_CONCAT //* 判断 下面的宏定义 在其它文件中是否出现过,确保 “只包含一次”
#define DEFINE_CONCAT(a, b) a##b //* 粘接给定的两个字符串
#define DEFINE_CONCAT_EXPANDED(a, b) DEFINE_CONCAT(a, b) //* 鉴于宏定义不能嵌套,所以定义了第二级粘贴
//*基于端口的宏定义,组合出
#define DEFINE_DIRECT(name,index) DEFINE_CONCAT_EXPANDED(TRIS, DEFINE_CONCAT(name, index)) //* 将 TRIS 粘接到 用户端口上
#define DEFINE_UPLOAD(name,index) DEFINE_CONCAT_EXPANDED(WPU, DEFINE_CONCAT(name, index)) //* 将 TRIS 粘接到 用户端口上
#define DEFINE_DWLOAD(name,index) DEFINE_CONCAT_EXPANDED(WPD, DEFINE_CONCAT(name, index)) //* 将 TRIS 粘接到 用户端口上
#define DEFINE_IO(name, index) DEFINE_CONCAT_EXPANDED(P, DEFINE_CONCAT(name, index)) //* 用于组装类似 PA4 的端口 Bit访问变量
#define DEFINE_VALUE(name) DEFINE_CONCAT_EXPANDED(PORT, name) // PA端口
#define DEFINE_PSINK(name,index) DEFINE_CONCAT_EXPANDED(PSINK,DEFINE_CONCAT(name, index)) // 组装PSINKA0
#define DEFINE_IO_MODE(index) DEFINE_CONCAT(ANSEL,index) // 组装ANSEL0~ANSEL7的字符串
#endif // DEFINE_CONCAT
// 基于以上宏定义,可以只定义出端口的基本属性即可,此部分可称为:硬件定义
// I2C1 GPIO Configuration
// PB6 ------> I2C1_SCL
// PB7 ------> I2C1_SDA
#define I2C1_SCL_PORT A
#define I2C1_SCL_BIT_INDEX 6
#define I2C1_SDA_PORT B
#define I2C1_SDA_BIT_INDEX 7
#define I2C1_START_DELAY_US 4
#define I2C1_CLK_DELAY_US 1
#define I2C1_ACK_DELAY_US 2
//基于I2C的基本IO口定义,可以扩展出如下定义
#define SCL_DIR_SET_IN DEFINE_DIRECT(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 1 // 设置SCL输入
#define SCL_DIR_SET_OUT DEFINE_DIRECT(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 0 // 设置SCL输出
#define SCL_UPLOAD_SET_OPEN DEFINE_UPLOAD(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 1 // 打开上拉
#define SCL_UPLOAD_SET_CLOSE DEFINE_UPLOAD(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 0 // 关闭上拉
#define SCL_SET_0 DEFINE_IO(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 0 // 设置端口值
#define SCL_SET_1 DEFINE_IO(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX) = 1 // 设置端口值
#define SCL_GET (DEFINE_IO(I2C1_SCL_PORT,I2C1_SCL_BIT_INDEX)) // 读取端口值
#define SDA_DIR_SET_IN DEFINE_DIRECT(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 1 // 设置SDA输入
#define SDA_DIR_SET_OUT DEFINE_DIRECT(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 0 // 设置SDA输出
#define SDA_UPLOAD_SET_OPEN DEFINE_UPLOAD(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 1 // 打开上拉
#define SDA_UPLOAD_SET_CLOSE DEFINE_UPLOAD(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 0 // 关闭上拉
#define SDA_SET_0 DEFINE_IO(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 0 // 设置端口值
#define SDA_SET_1 DEFINE_IO(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX) = 1 // 设置端口值
#define SDA_GET (DEFINE_IO(I2C1_SDA_PORT,I2C1_SDA_BIT_INDEX)) // 读取端口值
//进而扩展出编程中使用的通用“宏”IO操作函数
#define IIC_INIT SCL_DIR_SET_IN;SCL_SET_0;SDA_DIR_SET_IN;SDA_SET_0 // 端口初始化,都设置为输入,在上拉的作用下输出高电平
#define IIC_SCL_1 SCL_DIR_SET_IN
#define IIC_SCL_0 SCL_DIR_SET_OUT
#define IIC_SDA_1 SDA_DIR_SET_IN
#define IIC_SDA_0 SDA_DIR_SET_OUT
#define READ_SDA SDA_GET
// I2C通讯通常只需要7个函数,列举如下:
//初始化IIC
void IIC_Init(void)
{
IIC_INIT;
}
//产生IIC起始信号
void IIC_Start(void)
{
SDA_DIR_SET_OUT; //sda线输出
IIC_SDA_1;
IIC_SCL_1;
delay_us(I2C1_START_DELAY_US);
IIC_SDA_0; //START:when CLK is high,DATA change form high to low
delay_us(I2C1_START_DELAY_US);
IIC_SCL_0; //钳住I2C总线,准备发送或接收数据
}
//产生IIC停止信号
void IIC_Stop(void)
{
SDA_DIR_SET_OUT; //sda线输出
IIC_SCL_0;
IIC_SDA_0;
delay_us(I2C1_START_DELAY_US);
IIC_SCL_1; //STOP:when CLK is high DATA change form low to high
delay_us(I2C1_START_DELAY_US);
IIC_SDA_1; //发送I2C总线结束信号
}
//等待应答信号到来
//返回值:0,接收应答失败
// 1,接收应答成功
uint8_t IIC_Wait_Ack(void)
{
uint8_t ucErrTime=0;
SDA_DIR_SET_IN; //SDA设置为输入
IIC_SDA_1;delay_us(I2C1_CLK_DELAY_US);
IIC_SCL_1;delay_us(I2C1_CLK_DELAY_US);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{ // 超时
IIC_Stop();
return 0; // 读取超时,返回0
}
}
IIC_SCL_0;//时钟输出0
return 1; // 正常返回1
}
//产生ACK应答
void IIC_Ack(void)
{
IIC_SCL_0;
SDA_DIR_SET_OUT;
IIC_SDA_0;
delay_us(I2C1_ACK_DELAY_US);
IIC_SCL_1;
delay_us(I2C1_ACK_DELAY_US);
IIC_SCL_0;
}
//不产生ACK应答
void IIC_NAck(void)
{
IIC_SCL_0;
SDA_DIR_SET_OUT;
IIC_SDA_1;
delay_us(I2C1_ACK_DELAY_US);
IIC_SCL_1;
delay_us(I2C1_ACK_DELAY_US);
IIC_SCL_0;
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
uint8_t IIC_Send_Byte(uint8_t txd)
{
uint8_t t;
SDA_DIR_SET_OUT;
IIC_SCL_0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
if(txd & 0x80) IIC_SDA_1;
else IIC_SDA_0;
txd<<=1;
delay_us(I2C1_ACK_DELAY_US); //对TEA5767这三个延时都是必须的
IIC_SCL_1;
delay_us(I2C1_ACK_DELAY_US);
IIC_SCL_0;
delay_us(I2C1_ACK_DELAY_US);
}
return IIC_Wait_Ack();
}
// IIC读1个字节,
// ack=1时,发送ACK,
// ack=0,发送nACK
uint8_t IIC_Read_Byte(uint8_t ack)
{
uint8_t i,receive=0;
SDA_DIR_SET_IN;//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL_0;
delay_us(I2C1_ACK_DELAY_US);
IIC_SCL_1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(I2C1_CLK_DELAY_US);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|