嵌入式软件--stm32 DAY7 I2C通讯上
iic协议由飞利浦公司开发,是非常常用的一种通信协议,拥有两条总线的他支持多个设备共用同一条总线。常用于oled屏幕的外设驱动和手机PD协议通信中。了解和掌握iic协议是十分有必要的。
1.IIC概念
1.1 IIC的两条线和协议细节
双向两线制,同步半双工
1.1.1 三种速率模式
标准模式 100kbps
快速模式 400kbps
高速模式 3.4Mbps
不占用总线,上拉电阻把总线拉成高电平。
一般使用开漏模式而不是用推挽输出的原因是,避免多设备间冲突(多个设备共用同一条主线,如果有高有低,就会造成冲突),实现多主仲裁。
1.1.2协议细节
起始信号:SCL保持高电平,SDA下降沿(电平由高到低变换)
数据位:SCL保持高电平,SDA维持稳定。
终止信号:SCL保持高电平,SDA上升沿(电平由低到高变换)
确认信号:IIC以字节为单位传输数据,每传输一个字节,都要向发送方发送一个确认信号。ACK NACK
数据的有效性:SDA高低电平在SCL时钟信号是低电平时才能改变,SDA线上的数据必须在SCL高电平周期保持稳定。
响应:接收方发送数据后要给发送方响应,应答响应和非应答响应。
应答响应ACK:给发送方一个低电平,非应答响应NACK:给发送方一个高电平。
2. iic案例1:软件模拟IIC
2.1 EEPROM
型号:M24C02
M24C02的scl和sda与stm32的iic引脚相连,结合上拉电阻,构成IIC总线,通过iic进行交互。
eeprom芯片的设备地址一共七位,高四位固定1010,低三位则由E3/E2/E1信号线决定eeprom的设备地址。
R/W是读写方向控制位,与地址无关。
从设备7位地址+读写控制位
1 0 1 0 E3 E2 E1 R/W
2.2操作时序图
2.2.1单字节写入
写入一个字节,一个字节一个字节的写入。
start 从设备地址+写操作位 ACK 写入字节地址 ACK 写入字节 ACK stop
2.2.2单字节读出
读出一个字节时序:
start 从设备地址+写操作位 ACK 写入字节地址 ACK start 从设备地址+读操作位 ACK 读字节 NACK(不应答,发送方就不会继续发) stop
单次写入多个字节时序:
start 从设备地址+写操作位 ACK 写入字节地址 ACK 写入字节1 ACK 写入字节2 ACK ····写入字节N ACK stop
多字节写入叫页写,页写一次最多写入16个字节。超过16个字节,eeprom会重新第一个字节覆盖写入。
读的时候,要写一个字节再读一个字节,这叫假写真读。写的字节是地址,真正读的是数据。读字节没有限制,可以读取任意多个。
假写真读多个字节序:发start信号 设备地址+写操作位 ACK 写入内部地址 ACK 发start信号 设备地址加读操作位 ACK 读字节1 ACK 读字节2 ACK... 读字节N NO NACK STOP信号
2.3 代码实战IIC
软件模拟IIC,通过向EEPROM发送一段数据,再读取出来,最后发送到串口,核对是否读写正确。
复制上一个项目文件夹,重命名为iic_software_register,意为iic软件模拟寄存器写法。
删除几个文件,只留下基本文件start,user,hardware和keil源文件.在hardware中添加文件夹IIC,新建两个文件iic.c和iic.h。
涉及到读写的EEPROM放到interface,建立两个文件m24c02.h,m24c02.c。
在keil里面将.c文件和.h文件路径添加到位。
关联vscode:
这是寄存器写法,用了PB10,PB11两个引脚。
通信过程:
在.h文件里写预处理函数和函数名。
加上重定向函数,方便打印看结果。
2.3.1 iic初始化配置
//初始化
void I2C_Init(void)
{//1.配置时钟RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;//2.GPIO配置,PB10\PB11通用开漏输出,MODE-11,CNF-01GPIOB->CRH|=(GPIO_CRH_MODE10|GPIO_CRH_MODE11);GPIOB->CRH&=~(GPIO_CRH_CNF10_1|GPIO_CRH_CNF11_1);GPIOB->CRH|=(GPIO_CRH_CNF10_0|GPIO_CRH_CNF11_0);
}
2.3.2 IIC时序
(1)起始信号 SCL处于高电平,SDA下降沿,先拉高后拉低
//主机发出起始信号
void I2C_Start(void)
{//1.拉高SCL/SDASCL_HIGH;SDA_HIGH;I2C_DELAY;//2.拉低SDA,产生下降沿SCL_LOW;I2C_DELAY;}
(2)SDA低,SCL高,SDA上升沿
//主机发出停止信号
void I2C_Stop(void)
{//1.拉高SCL,拉低SDASCL_HIGH;SDA_LOW;I2C_DELAY;//2.拉高SDA,产生上升沿SDA_HIGH;I2C_DELAY;
}
(3)接收应答非应答信号
《1》拉高SDA,拉低SCL,等待SDA反转
《2》SDA拉低,翻转数据,输出应答信号
《3》SCL拉高,保持SDA信号,做数据采样
《4》SCL拉低,数据采样结束
《5》SDA拉高,应答输出结束,释放SDA
//主机发送应答/非应答信号
void I2C_ACK(void)
{//1.拉高SDA/拉低SCL,准备反转SDASDA_HIGH;SCL_LOW;I2C_DELAY;//《2》SDA拉低,翻转数据,输出应答信号
SDA_LOW;
I2C_DELAY;
//《3》SCL拉高,保持SDA信号,做数据采样
SCL_HIGH;
I2C_DELAY;
//《4》SCL拉低,数据采样结束
SCL_LOW;
I2C_DELAY;
//《5》SDA拉高,应答输出结束,释放SDA总线
SDA_HIGH;
I2C_DELAY;
}
(4)接收非应答
//1.拉低SCL,拉高SDA,准备发送非应答信号
//2.保持SDA,拉高SCL信号,做数据采样
//3.保持SDA,拉低SCL,数据采样结束
void I2C_NACK(void)
{//1.拉低SCL,拉高SDA,准备发送非应答信号
SCL_LOW;
SDA_HIGH;
I2C_DELAY;
//2.保持SDA,拉高SCL信号,做数据采样
SCL_HIGH;
I2C_DELAY;
//3.保持SDA,拉低SCL,数据采样结束
SCL_LOW;
I2C_DELAY;
}
(5)
//主机从SDA读取响应信号,等待应答
uint8_t I2C_Wait4ACK(void)
{//1.拉高SDA/拉低SCL,准备反转SDASDA_HIGH;SCL_LOW;I2C_DELAY;//2.SCL拉高,保持SDA信号,做数据采样
SCL_HIGH;
I2C_DELAY;//3.读取SDA上的电平
uint8_t ack=READ_SDA;//4.SCL拉低,数据采样结束
SCL_LOW;
I2C_DELAY;
return ack?NACK:ACK;
}
(6)
//主机发送一个字节
void I2C_SendByte(uint8_t byte)
{for(uint8_t i=0;i<8;i++){//1.SCL拉低,等待可能的反转SDA信号,准备发送数据SCL_LOW;I2C_DELAY;//2.将要发送的位传到SDA上,MSB先行if(byte&0x80)// 10000000 取最高位{SDA_HIGH;}else{SDA_LOW;}I2C_DELAY;//3.SCL拉高,保持SDA信号,让从设备做数据采样SCL_HIGH;I2C_DELAY;//4.SCL拉低,数据采样结束SCL_LOW;I2C_DELAY;//5.左移一位,为下一次发送做准备byte<<=1;}
}//主机从SDA读取一个字节
uint8_t I2C_ReadByte(void)
{//定义变量,用来保存读取的字节uint8_t data=0;for (size_t i = 0; i < 8; i++){//1.拉高SDA,拉低SCL,释放SDA总线SDA_HIGH;SCL_LOW;I2C_DELAY;//2.SCL拉高,保持SDA信号,做数据采样SCL_HIGH;I2C_DELAY;//3.先左移一位,为下次接收做准备data<<=1;//4.读取SDA上的电平,放到数据的最低位if (READ_SDA){data|=0x01; }//5.SCL拉低,数据采样结束SCL_LOW;I2C_DELAY;}}
2.4 接口层
.h文件
2.4.1 初始化
接口层没有什么可以初始化的,只要收发逻辑完成即可,因此只要IIC初始化即可。
2.4.2 写入字节,字写和页写
2.4.3 读取字节
假写真读的过程
连续多个字节读取
//连续读取多个字节(连续读)
void M24C02_ReadBytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)
{//1.发出开始信号I2C_Start();//2.发送写地址(假写)I2C_SendByte(W_ADDR);//3.等待应答I2C_Wait4ACK();//4.发送内部地址I2C_SendByte(innerAddr);//5.等待应答I2C_Wait4ACK();//6.发出开始信号I2C_Start();//7.发送读地址(真读)I2C_SendByte(R_ADDR);//8.等待应答I2C_Wait4ACK();for (uint8_t i = 0; i < size; i++){//9.循环读取一个字节的数据bytes[i]= I2C_ReadByte();//10.如果没结束,发送一个应答;如果最后一个,就给非应答if(i<size-1){I2C_ACK();}else{I2C_NACK();}}//11.发送结束信号I2C_Stop();return bytes;
}
之前未考虑到,写入和读取字节,EEPROM有自己的耗时,需要5ms.在之前的代码中自行添加。
2.4.4 应用测试:读取单字节
#include "usart.h"
#include "delay.h"
#include "m24c02.h"int main(void)
{//1.初始化USART_Init();M24C02_Init();printf("WO SHI CHINESE!\n");//2.写入单个字符M24C02_WriteByte(0x00,'a');M24C02_WriteByte(0x01,'b');M24C02_WriteByte(0x02,'c');//3.读取单个字符uint8_t byte1=M24C02_ReadByte(0x00);uint8_t byte2=M24C02_ReadByte(0x01);uint8_t byte3=M24C02_ReadByte(0x02);//4.串口输出打印printf("byte1=%c\n",byte1);printf("byte2=%c\n",byte2);printf("byte3=%c\n",byte3);while(1){}}
#include "usart.h"
#include "delay.h"
#include "m24c02.h"
#include <string.h>int main(void)
{//1.初始化USART_Init();M24C02_Init();printf("WO SHI CHINESE!\n");//2.写入单个字符M24C02_WriteByte(0x00,'a');M24C02_WriteByte(0x01,'b');M24C02_WriteByte(0x02,'c');//3.读取单个字符uint8_t byte1=M24C02_ReadByte(0x00);uint8_t byte2=M24C02_ReadByte(0x01);uint8_t byte3=M24C02_ReadByte(0x02);//4.串口输出打印printf("byte1=%c\n",byte1);printf("byte2=%c\n",byte2);printf("byte3=%c\n",byte3);//5.页写M24C02_WriteBytes(0x00,"123456",6);//6.读取多个字符uint8_t buff[50];M24C02_ReadBytes(0x00,buff,6);//7.串口输出打印printf("buff=%s\n",buff);//清除缓冲区memset(buff,0,sizeof(buff));//长度超出16的页写M24C02_WriteBytes(0x00,"1234567890123456",20);//如果从0X05开始读,就可以读到尾M24C02_ReadBytes(0x00,buff,20);printf("buff=%s\n",buff);while(1){}}