嵌入式软件--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-01
    GPIOB->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/SDA
    SCL_HIGH;
    SDA_HIGH;
    I2C_DELAY;
    //2.拉低SDA,产生下降沿
    SCL_LOW;
    I2C_DELAY;

}

(2)SDA低,SCL高,SDA上升沿

复制代码
//主机发出停止信号
void I2C_Stop(void)
{
    //1.拉高SCL,拉低SDA
    SCL_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

cpp 复制代码
//主机发送应答/非应答信号
void I2C_ACK(void)
{
    //1.拉高SDA/拉低SCL,准备反转SDA
    SDA_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,数据采样结束

cpp 复制代码
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,准备反转SDA
    SDA_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)
	{
		
	
	}
	
}
相关推荐
芯岭技术6 分钟前
PY32系列单片机离线烧录器,可配置选项字节和上机台批量烧录
单片机·嵌入式硬件
O。o.尊都假都3 小时前
socket套接字的超时控制
单片机·嵌入式硬件·网络协议
欢乐熊嵌入式编程4 小时前
智能手表项目的《项目背景与目标说明书》样本文档
嵌入式硬件·目标跟踪·规格说明书·智能手表
海尔辛5 小时前
学习黑客BitLocker与TPM详解
stm32·单片机·学习
海染棠花6 小时前
vscode+platformIO开发STM32(八)
ide·vscode·stm32
上海合宙LuatOS6 小时前
全栈工程师实战手册:LuatOS日志系统开发指南!
java·开发语言·单片机·嵌入式硬件·物联网·php·硬件工程
公子无缘6 小时前
【嵌入式】记一次解决VScode+PlatformIO安装卡死的经历
vscode·stm32·单片机·mcu·platformio
昊昊昊昊昊明6 小时前
十天学会嵌入式技术之51单片机—day-9
单片机·嵌入式硬件·51单片机
欢乐熊嵌入式编程7 小时前
智能手表蓝牙 GATT 通讯协议文档
嵌入式硬件·目标跟踪·规格说明书·智能手表