常见的几种通信接口
I2C总线定义
定义
I2C - Inter-Integrated Circuit:两线式 串行总线:说明处理器和外设之间只需两根信号线,分别是SCL时钟控制信号线和SDA数据线
SCL(serial clock line)
时钟控制信号线,永远只能由CPU控制,用于实现数据的同步,就四个字:低放高取
- SCL为低电平的时候将数据放到SDA数据线上
- SCL为高电平的时候从数据线SDA上获取数据
SDA
数据线,用于传输数据,双方都可以控制
- 如果处理器给外设发送数据,SDA由处理器控制
- 如果外设给处理器发送数据,SDA由外设控制
注意:SCL和SDA必须要分别连接一个上拉电阻,所以他们默认的电平都是高电平
架构
I2C总线基于主从架构
- 其中一个设备作为主设备(master)
负责发起通信控制总线时序
- 其它的设备为从设备(slave)
负责响应和数据传输
速率
- 标准模式 - 100kbps
- 快速模式 - 400kbps
- 高速模式 - 3.4Mbps
串行
由于数据线就一根SDA,必然是串行,又由于有时钟控制信号线SCL,所以数据传输是一个时钟周期传输一个bit位,I2C数据传输从高位开始,I2C数据传输一次传输一个字节,如果传输多个字节,需要分拆着来传
传输特点:
- 一位一周期
- 一次一字节
- 传输从高位
- 速度看时钟(SCL)
- 时钟看外设
I2C总线的应用领域
I2C总线协议相关概念
- START信号:又称起始信号,此信号永远只能由CPU发起,表示CPU开始要访问外设
时序为:SCL为高电平,SDA由高电平向低电平跳变产生START信号
- STOP信号:又称结束信号,此信号永远只能由CPU发起,表示CPU结束对总线的访问
时序为:SCL为高电平,SDA由低电平向高电平跳变产生STOP信号
- R/W读写位:用于表示CPU到底是向外设写入数据还是从外设读取数据
有效位数为1个bit位,CPU读取数据:R/W=1,CPU向外设写入数据:R/W=0
- 设备地址:用于表示外设在总线上的唯一性,也就是同一个I2C总线上,不同的外设具有唯一的一个设备地址,也就是如果CPU要想访问某个外设,CPU只需向总线上发送这个外设的设备地址即可设备地址的有效位数为7位或者10位(极其少见),设备地址不包含读写位!
- 设备地址由原理图和芯片手册共同来决定:
I2C总线数据传输的流程(协议)
设备地址
- 读设备地址=设备地址<<1|R/W=1
- 写设备地址=设备地址<<1|R/W=0
问:为何需要读或者写设备地址呢?
答:I2C总线协议规定,数据传输一次一个字节(8位),而设备地址本身7位不够1字节,正巧R/W为一个bit位,所以报团取暖凑够一字节,将来CPU要想访问某个外设只需发送读或者写设备地址,即可找到这个外 设也可以告诉外设到底读还是写!一箭双雕!
ACK信号
又称应答信号,表示双方数据传输的反馈,有效位数为1个bit位,低电平有效
片内寄存器
切记:任何I2C外设芯片内部都集成了一堆的寄存器,此类寄存器又称片内寄存器
- 这些寄存器同样也有地址,地址编号从0x00开始
- 虽然这些寄存器都有唯一的地址,但是CPU不能直接以指针的形式访问, 必须要严格按照,读写时序进行访问
结论
CPU访问I2C外设本质就是访问I2C外设内部的寄存器!
所以I2C外设本身只需关注三点:
- I2C外设片内寄存器的特性
- I2C外设片内寄存器的基地址
- I2C外设片内寄存器的读写时序
以CPU访问MMA8653三轴加速度传感器为例
读取单字节数据
写入单字节数据
读取多字节数据
写入多字节数据
总结
AT24C02的访问操作
以CPU访问AT24C02存储器为例
写数据
读数据
时序图
AT24C02
概况
AT24C02是一个2K位串行EEPROM, 内部含有256个8位字节的存储单元,掉电数据不丢失
AT24C02的存储容量分成32页,每页8Byte,共256Byte
AT24C02寻址范围为00~FF,共256个寻址单位
硬件设计
SCL和SDA均接有上拉电阻,连接到STM32的PB6和PB7管脚上
时序细节
IIC代码编写
STM32中无直接调用IIC的底层库,需要手撸代码实现IIC的数据收发
初始化
void IIC_Init(void)
{// 1.打开SCL/SDA时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);// 2.配置SCL - 推挽输出, 50MHzGPIO_InitTypeDef GPIO_Config;GPIO_Config.GPIO_Pin = IIC_SCL_PIN;GPIO_Config.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Config.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出GPIO_Init(IIC_SCL_PORT, &GPIO_Config);// 3.配置SDA - 推挽输出, 50MHzGPIO_Config.GPIO_Pin = IIC_SDA_PIN;GPIO_Config.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Config.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出GPIO_Init(IIC_SDA_PORT, &GPIO_Config);// 4.拉高SCL/SDA IIC_SDA = 1;IIC_SCL = 1;
}
配置SDA为推挽输出, 50MHz
// 配置SDA为推挽输出, 50MHz
static void SDA_OUT(void){GPIO_InitTypeDef GPIO_Config;GPIO_Config.GPIO_Pin = IIC_SDA_PIN;GPIO_Config.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Config.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出GPIO_Init(IIC_SDA_PORT, &GPIO_Config);
}
配置SDA为上拉输入
// 配置SDA为上拉输入
static void SDA_IN(void){GPIO_InitTypeDef GPIO_Config;GPIO_Config.GPIO_Pin = IIC_SDA_PIN;GPIO_Config.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(IIC_SDA_PORT, &GPIO_Config);
}
起始和终止条件
开始步骤
- 1.配置SDA为输出模式
- 2.配置SDA/SCL为高电平
- 3.保持至少4.7us
- 4.拉低SDA
- 5.保持至少4us
------>已经完成发送开始信号
void IIC_Start(void){SDA_OUT();// 配置SDA为输出模式 IIC_SCL = 1; // 时钟线拉高IIC_SDA = 1; // 数据线拉高 delay_us(6); // 延时6us, >=4.7us IIC_SDA = 0; // 拉低SDAdelay_us(6); // 延时6us, >= 4us//-------->发送了开始信号 IIC_SCL = 0; // 将SCL拉低,便于下一次数据数据
}
终止步骤
- 1.配置SDA为输出模式
- 2.配置SCL为高电平,SDA为低电平
- 3.保持至少4us
- 4.拉高SDA
- 5.保持至少4.7us
void IIC_Stop(void){SDA_OUT(); // 配置SDA为输出模式 IIC_SDA = 0; // 数据线拉低 IIC_SCL = 1; // 时钟线拉高 delay_us(6); // 延时6us IIC_SDA = 1; // 数据线拉高 delay_us(6); // 延时>4.7us
}
处理ack
CPU等待ack
- CPU等待外设发送ack信号
- 返回值 - 判断是否收到了ack
- 收到ack ,0; 没收到ack, 返回1
u8 IIC_Wait_Ack(void){u32 tempTime = 0; // 等待的次数 // 将时钟拉低, 方便外设放入数据 IIC_SCL = 0; delay_us(6); // 保持6us SDA_IN(); // 配置输入模式 // 将时钟线拉高, 为了让CPU来读取数据 IIC_SCL = 1;delay_us(6); // 如何判断SDA的高低电平呢? - READ_SDA // 如果外设发送了ack, 发送低电平, SDA = 0// 如果外设没发送ack, 上拉输入, SDA = 1while(READ_SDA){tempTime++;if(tempTime > 250){// 没有收到ack,结束传输 IIC_Stop(); return 1;// 没收到ack }}IIC_SCL = 0; //准备下一次数据传输return 0; // 收到ack
}
发送ack信号
void IIC_Ack(void){IIC_SCL = 0; // 将SCL拉低SDA_OUT(); // 配置为输出模式 IIC_SDA = 0; // 将低电平放到SDA, 发送ackdelay_us(6); // 保持低电平的周期 IIC_SCL = 1; // 将时钟线拉高, 让外设在此时读取SDA数据 delay_us(6); IIC_SCL = 0; // 拉低准备下一次数据传输
}
发送nack信号
void IIC_NAck(void){IIC_SCL = 0; // 将SCL拉低SDA_OUT(); // 配置为输出模式 IIC_SDA = 1; // 将高电平放到SDA, 发送nackdelay_us(6); // 保持低电平的周期 IIC_SCL = 1; // 将时钟线拉高, 让外设在此时读取SDA数据 delay_us(6); IIC_SCL = 0; // 拉低准备下一次数据传输
}
CPU收发数据
CPU发送单字节
void IIC_Send_Byte(u8 TxData){u8 i;SDA_OUT(); // 配置为输出模式IIC_SCL = 0; // 为了将数据放到SDA上 for(i = 0; i < 8; i++){if(TxData & 0x80)IIC_SDA = 1;else IIC_SDA = 0;TxData <<= 1;delay_us(6); // 低电平的时钟周期 IIC_SCL = 1; // 拉高,让外设读delay_us(6); IIC_SCL = 0; }
}
CPU读取单字节
// 返回读取到的数据
// 参数 :
// 1, 回复ack信号;
// 0, 回复nack信号;
u8 IIC_Read_Byte(u8 ack){u8 i = 0, data = 0; // data保存读取到的数据 SDA_IN(); // 配置为输入模式 for(i = 0; i < 8; i++){IIC_SCL = 0; // 拉低SCL为了让外设放入数据 delay_us(6);IIC_SCL = 1; // 拉高为了获取数据data |= READ_SDA << (7 - i);delay_us(6); }// 回复ack/nackif(!ack)IIC_NAck();elseIIC_Ack();return data; // 返回读取到的数据
}
AT24C02代码
// @file at24c02.c
#include "at24c02.h"
#include "iic.h"
#include "systick.h"
#include "stdio.h" // printfvoid AT24C02_Init(void){IIC_Init();
}// 参数:要读取的寄存器的地址
// 返回值 : 返回读取到的数据
// AT24C02_ID
u8 AT24C02_ReadByte(u16 ReadAddr){u8 temp;// 1.发送开始信号IIC_Start();// 2.发送写设备地址IIC_Send_Byte(AT24C02_ID << 1 | 0);// 3.等待ackIIC_Wait_Ack();// 4.发送要读取的寄存器地址IIC_Send_Byte(ReadAddr);// 5.等待ackIIC_Wait_Ack();// 6.发送开始信号IIC_Start();// 7.发送读设备地址IIC_Send_Byte(AT24C02_ID << 1 | 1);// 8.等待ackIIC_Wait_Ack();// 9.读取外设数据 + 回复nacktemp = IIC_Read_Byte(0);// 10.发送结束信号IIC_Stop();return temp;
}
// 功能 : 发送单字节数据
// 参数 :
// WriteAddr : 要写入的寄存器地址
// data : 要写入的数据
void AT24C02_WriteByte(u16 WriteAddr, u8 data){// 1.发送开始信号IIC_Start();// 2.发送写设备地址IIC_Send_Byte(AT24C02_ID << 1 | 0);// 3.等待ackIIC_Wait_Ack();// 4.发送要写入的寄存器地址IIC_Send_Byte(WriteAddr);// 5.等待ackIIC_Wait_Ack();// 6.发送要写入的数据IIC_Send_Byte(data);// 7.等待ackIIC_Wait_Ack();// 8.发送结束信号 IIC_Stop();
}
// 功能 : 读取多字节
// 参数 :
// ReadAddr : 要读取的寄存器的首地址
// pBuffer : 要读取数据存储的首地址
// Len : 要读取的数据个数
// char buf[1024]; char* pBuffer = buf;
// 11 12 13 14 寄存器地址
void AT24C02_ReadBlockData(u16 ReadAddr, u8* pBuffer, u16 Len){while(Len){*pBuffer++ = AT24C02_ReadByte(ReadAddr++);Len--;}
}
// 功能 : 写入多字节
// 参数 :
// WriteAddr : 要写入的寄存器的首地址
// pBuffer : 要写入数据存储的首地址
// Len : 要写入的数据个数
// char buf[1024]; char* pBuffer = buf;
// buf数组 : xx xx xx xx
// 11 12 13 14 寄存器地址
void AT24C02_WriteBlockData(u16 WriteAddr, u8* pBuffer, u16 Len){while(Len){AT24C02_WriteByte(WriteAddr, *pBuffer);WriteAddr++;pBuffer++;Len--;}delay_us(20);
}// 测试函数 : 后续进行命令匹配使用
void AT24C02_ReadOne(void){// 读取地址0x00寄存器数据 printf("READ DATA : %#X\n", AT24C02_ReadByte(0x00));
}
void AT24C02_WriteOne(void){// 将数据0XAA写入到地址0x00寄存器中 AT24C02_WriteByte(0X00, 0XAA);
}
void AT24C02_ReadMul(void){u8 data[5] = {0};// 从地址0x00开始连续读取5个数据到data数组中 AT24C02_ReadBlockData(0x00, data, 5);// 打印输出 u8 i;for(i = 0; i < 5; i++)printf("ADDR[%d], DATA[%#x]\n", i, data[i]);
}
void AT24C02_WriteMul(void){u8 Data[5] = {1, 2, 3, 4, 5};// 将5个数据分别写入到地址0 1 2 3 4寄存器中 AT24C02_WriteBlockData(0x00, Data, 5);
}
实验结果
通过串口工具向内部写入单字节,读取单字节
通过串口工具向内部写入多字节,读取多字节