当前位置: 首页 > news >正文

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 - 推挽输出, 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);
}

实验结果

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

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


http://www.mrgr.cn/news/5063.html

相关文章:

  • 物联网设备字符串转串口指令-SAAS本地化及未来之窗行业应用跨平台架构
  • 【数据结构】堆排序
  • 如何优化企业网站的索引情况?
  • 常用API:object
  • Linux入门——09 共享内存
  • 计算机网络——TCP协议与UDP协议详解(下)
  • Ps:首选项 - 文件处理
  • PXE 高效批量网络装机
  • 未来城市的科技展望
  • 搭建Node.js后端
  • 【TB作品】普中V2,数字时钟万年历显示,音乐闹钟,流水灯,Proteus仿真
  • Go开发桌面客户端软件小试:网站Sitemap生成
  • Zookeeper用作服务发现~记当牛马的日子
  • 回归预测|基于北方苍鹰优化NGO-Transformer-BiLSTM组合模型的数据预测Matlab程序多特征输入单输出
  • CSS的:horizontal和:vertical伪类:方向性样式的精准选择
  • UDP 和TCP的应用
  • http request-03-Ajax 的替代方案 axios.js 入门介绍
  • makefile文件基本语法
  • python构建一个web程序
  • 用TensorFlow实现线性回归