STM中的I2C

常见的几种通信接口

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 - 推挽输出, 50MHz
	GPIO_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 - 推挽输出, 50MHz
	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);
	
	// 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; // 拉低SDA
	delay_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 = 1
	
	while(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, 发送ack
	delay_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, 发送nack
	delay_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/nack
	if(!ack)
		IIC_NAck();
	else
		IIC_Ack();
	
	return data; // 返回读取到的数据 
}

 AT24C02代码

// @file at24c02.c
#include "at24c02.h"
#include "iic.h"
#include "systick.h"
#include "stdio.h" // printf

void 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.等待ack
	IIC_Wait_Ack();
	// 4.发送要读取的寄存器地址
	IIC_Send_Byte(ReadAddr);
	// 5.等待ack
	IIC_Wait_Ack();
	// 6.发送开始信号
	IIC_Start();
	// 7.发送读设备地址
	IIC_Send_Byte(AT24C02_ID << 1 | 1);
	// 8.等待ack
	IIC_Wait_Ack();
	// 9.读取外设数据 + 回复nack
	temp = 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.等待ack
	IIC_Wait_Ack();
	// 4.发送要写入的寄存器地址
	IIC_Send_Byte(WriteAddr);
	// 5.等待ack
	IIC_Wait_Ack();
	// 6.发送要写入的数据
	IIC_Send_Byte(data);
	// 7.等待ack
	IIC_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);
}

实验结果

 通过串口工具向内部写入单字节,读取单字节

通过串口工具向内部写入多字节,读取多字节

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值