软件模拟I2C案例(寄存器实现)

引言

       在经过前面对I2C基础知识的理解,对支持I2C通讯的EEPROM芯片M24C02的简单介绍以及涉及到的时序操作做了整理。接下来,我们就正式进入该案例的实现环节了。本次案例是基于寄存器开发方式通过软件模拟I2C通讯协议,然后去实现相关的需求。

       阅读本篇文章前,建议初次接触的朋友先理解一下几篇文章,然后再来阅读本篇文章可能会更加容易。

I2C基础知识-CSDN博客

软件模拟I2C案例前提须知——EEPROM芯片之M24C02_24c02 i2c-CSDN博客

模拟I2C通讯之时序图整理-CSDN博客


一、需求描述

       EEPROM芯片最常用的通讯方式就是I2C协议,本次使用的芯片是M24C02

       我们向E2PROM写入一段数据,再读取出来,最后发送到串口,核对是否读写正确。

二、硬件电路设计

2.1 EEPROM电路原理图

       根据M24C02芯片的电路连接可知,其设备地址为7位,已经固定为1010000。由于进行I2C通讯时传递的设备地址码后面还会紧跟一位读写方向位WR(写-0 读-1),因此易知最终传输的设备地址码为【写地址】0xA0【读地址】0xA1两种。

       WC#端口:写保护,可看做写入使能,低电平有效。有图可知已经固定低电平,即一直可写

      I2C相关端口:SCL与SDA引脚,连接主机(STM32芯片)I2C相关端口,可见引脚网络名I2C2...

2.2 端口原理图

        由端口原理图可见,涉及到的GPIO口为PB10与PB11,PB10对应SCL,PB11对应SDA。由于本次案例软件模拟I2C,故不会用到STM32芯片内置硬件I2C模块,即只使用GPIO引脚的通用输入输出功能给高低电平即可。

       同时由于I2C通讯方式为总线连接方式,即多个设备同时挂在一根总线上进行通讯,因此GPIO工作模式将使用通用功能的开漏输出模式

三、软件设计

3.1 工程创建

       按照以往工程创建方式应该算是轻车熟路了,这里不再赘述。值得注意的是,本次案例本质上是借助模拟出来的I2C通讯协议实现STM32与EEPROM间的数据传递,所以I2C通讯协议模拟部分代码属于硬件层实现,而与EEPROM通讯的过程实际上是直接调用I2C协议接口的逻辑,这部分属于接口层实现。故本次将在工程目录中多增加一个目录【Interface】,放调用相关接口的代码文件。

创建好后的效果如下

3.2 工程配置

在本地创建好工程后,在keil中打开此工程进行相关配置。

       首先,在【品】中添加【group】和【file】,主要是我们本次工程新增的目录和文件

效果如下:

       其次,进入【魔法棒】,在【C/C++】中的【include path】添加新增文件路径,以及配置【debug】调试工具

如上图效果即可。这样,本工程就配置完毕了。


3.3 程序实现

接下来,在VSCode中打开该工程,开始编写代码。

3.3.1 I2C协议部分

       首先,编写I2C部分的代码,主要是通过软件模拟出I2C通讯相关时序操作。

3.3.1.1 i2c.h

1、头文件基本格式不要忘

防止头文件重复编译,通常编写头文件内容时初始会有统一的框架,然后在内部添加代码。

#ifndef __I2C_H
#define __I2C_H#endif

2、引用必要头文件

(1)进行32寄存器开发,势必使用到32中的一些宏定义,故stm32f10xx.h要引入;

(2)模拟I2C通讯的一些时序,会涉及到高低电平的维持,通常会用到延时来实现“维持”效果,故Delay.h要引入。

#include "stm32f10x.h"
#include "Delay.h"

3、实现I2C协议的一些基本宏定义

       宏定义起到一个全局替换的效果,经过宏定义,我们可以将某些复杂代码利用简洁移动的语句进行代替,增强代码可读性和编写效率。

(1)由于I2C协议涉及到应答ACK和非应答NACK响应,分别由低电平0和高电平1表示,为增强可读性,这里选择使用宏定义代替。

#define ACK 0
#define NACK 1

(2)由于后面模拟I2C时序操作时,会频繁涉及到SCL和SDA线上信号的拉低/拉高,而这些电平的产生涉及到PB10和PB11端口的输出由于语句较长,故这里将对相关代码利用简洁易懂的宏定义。同时防止与其他语句共用时出现执行歧义,我们用括号括起来进行替换。

// SCL、SDA线拉低拉高
#define SCL_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR10)
#define SCL_HIGH (GPIOB->ODR |= GPIO_ODR_ODR10)#define SDA_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR11)
#define SDA_HIGH (GPIOB->ODR |= GPIO_ODR_ODR11)

(3)后面主机(STM32)获取从机(EEPROM)的数据或者发出的响应时,需要在SDA线上进行数据采样获得,此时相当于读取PB11端口输出的电平,这里也是进行简单的宏定义。

// 主机读取从机信号
#define READ_SDA (GPIOB->IDR & GPIO_IDR_IDR11)

(4)I2C协议模拟时用到的延时调用也可用宏定义I2C_DEALY替换。本次模拟I2C通讯的传输速率使用标准模式的100kbit/s,反映在时序图上相当于以100k的频率进行电平的传递,换算为时间周期即1/100k = 10^(-5) s,也就是10us的延时即可。

// I2C通讯基本延时
#define I2C_DELAY (Delay_us(10))

4、可能用到的函数声明

(1)I2C的初始化函数。任何模块的调用都少不了起初的配置,由于将借助GPIO引脚输出不同电平模拟I2C时序,故GPIO相关配置少不了,我们把配置部分归于I2C的初始化部分。

// 初始化
void I2C_Init(void);

(2)I2C通讯的起始信号和停止信号函数。从I2C协议所涉及的时序操作思考,首先会有主机发出的起始信号以及最后的停止信号时序需要模拟实现。

// 起始信号
void I2C_Start(void);
// 停止信号
void I2C_Stop(void);

(3)主机发出的I2C应答响应和非应答响应函数。其次还涉及到I2C通讯的响应时序操作的模拟实现,即主机向从机发出ACK和NACK响应信号。

// 主机发出应答响应
void I2C_Ack(void);
// 主机发出非应答响应
void I2C_Nack(void);

(4)主机等待从机的响应信号函数。既然有主机向从机发出的,则也会有从机发给主机的响应,以主机32为视角,我们是接收从机响应,这个过程相当于等待从机发出的响应信号,直到读取到则结束。

// 主机等待从机发出响应
uint8_t I2C_Wait4Ack(void);

(5)主机向从机写入/读取一个字节数据函数。最后,进行I2C通讯目的就是数据传递,所以还有写入和读取数据的函数,而I2C通讯规定了位传输和响应,一般每传1个字节就会进行一次响应过程,故这里只要读写单字节数据的函数即可。

// 主机向从机写入一个字节的数据(发送)
void I2C_SendByte(uint8_t byte);
// 主机向从机读取一个字节的数据(接收)
uint8_t I2C_ReadByte(void);

这样,i2c头文件就编写完毕。 

3.3.1.2 i2c.c

编写完i2c头文件后,接下来编写I2C的源文件,对其中的函数进行实现。

1、初始化函数I2C_Init()

       前面说了,I2C初始化部分就是一些配置,这里软件模拟I2C就是配置一下相关的GPIO端口即PB10和PB11就OK了,涉及到两部分:GPIO时钟配置和工作模式的配置。

(1)GPIO时钟配置

       不记得对应寄存器的可以去查查STM32F10xx系列参考手册的存储器地址映像,容易发现用到的寄存器是RCC的APB2ENR寄存器。

参考代码如下:

 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

(2)GPIO工作模式

       分析硬件电路设计的时候说到了,用到的PB10和PB11两个端口,由于I2C通讯是一种总线的连接方式,故均使用高速通用开漏输出模式就行。涉及的寄存器可在参考手册中查阅,即端口配置寄存器

参考代码如下:

    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);

所以I2C初始化函数参考如下:

// 初始化
void I2C_Init(void)
{// 1. 配置时钟RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;// 2. 设置GPIO工作模式 通用开漏输出 cnf-01 mode-11GPIOB->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、I2C起始信号I2C_Start() 和停止信号函数I2C_Stop()

       起始信号和停止信号函数的实现我们需要根据对应时序操作图实现,如下图

(1)根据时序图可知,主机发出起始信号的过程为:

【SDA拉高、SCL拉高,等待数据翻转】->【维持10us】->【SDA拉低、SCL保持不变】->【维持10us】-> 起始信号产生

(2)主机发出停止信号的过程为:

【SDA拉低、SCL拉高,等待数据翻转】->【维持10us】->【SDA拉高、SCL保持不变】->【维持10us】-> 停止信号产生

参考代码如下

// 主设备发出起始信号
void I2C_Start(void)
{// 1. SCL、SDA拉高SDA_HIGH;SCL_HIGH;I2C_DELAY;// 2. SCL保持不变、SDA拉低,发出起始信号SDA_LOW;I2C_DELAY;
}// 主设备发出停止信号
void I2C_Stop(void)
{// 1. SCL拉高、SDA拉低SDA_LOW;SCL_HIGH;I2C_DELAY;// 2. SCL保持不变、SDA拉高SDA_HIGH;I2C_DELAY;
}

3、主机发出I2C应答I2C_Ack()或非应答响应函数I2C_Nack()

I2C响应对应时序操作图如下

        如上图前两个时序为不同状态下的数据总线SDA第三条时序为主控制的时钟时序SCL。此时主机发送响应给从机,则此时主机控制后两条时序操作。

       也就是说此时SCL先为低电平,不进行SDA线上信号的采样,然后SDA线先是默认高电平,一段时间后主机发出响应被拉低/拉高一段时间,接着SCL拉高一段时间进行SDA线上的信号采样,最后SCL拉低结束信号采样,一段时间后SDA拉高,释放数据总线即可。

(1)主机发出应答的过程:

【SDA拉高、SCL拉低】->【维持10us】->【SDA拉低、SCL保持不变】->【维持10us】->【SDA不变、SCL拉高,从机开始采集主机发出的应答信号】->【维持10us】->【SDA不变、SCL拉低,结束信号采集】->【维持10us】->【SDA拉高、SCL保持不变,释放数据总线】->【维持10us】->过程结束

(2)主机发出非应答的过程:

【SDA拉高、SCL拉低】->【维持10us】->【SDA不变、SCL拉高,从机开始采集主机发出的应答信号】->【维持10us】->【SDA不变、SCL拉低,结束信号采集】->【维持10us】->过程结束

参考代码如下:

// 主设备发出应答响应
void I2C_Ack(void)
{// 1. SDA拉高、SCL拉低SDA_HIGH;SCL_LOW;I2C_DELAY;// 2. SCL保持不变、SDA拉低,主机发出应答SDA_LOW;I2C_DELAY;// 3. SCL拉高、SDA保持不变,开始信号采样SCL_HIGH;I2C_DELAY;// 4. SCL拉低、SDA保持不变,结束信号采样SCL_LOW;I2C_DELAY;// 5. SDA拉高,释放数据总线SDA_HIGH;I2C_DELAY;
}// 主设备发出非应答响应
void I2C_Nack(void)
{// 1. SDA拉高、SCL拉低SDA_HIGH;SCL_LOW;I2C_DELAY;// 2. SDA保持不变、SCL拉高,开始非应答信号采样SCL_HIGH;I2C_DELAY;// 3. SDA保持不变、SCL拉低,结束信号采样SCL_LOW;I2C_DELAY;
}

4、主机等待从机发出响应uint8_t I2C_Wait4Ack()

       仍是响应,不过角色互换了,这时候相当于主机采集从机发出的响应信号,这时候就会出现两种情况,可能是应答响应,也可能是非应答响应。

        我们总以主机32为视角,由于从机发出响应信号,因此这时候数据总线SDA上的信号不受主机32控制,所以这时候主机应该释放数据总线,然后控制SCL的变化就行。

        即首先SDA线会空闲,SCL会拉低一段时间,然后SCL被拉高,主机32就要开始采集数据总线上的信号了,一段时间后结束信号采样,SCL就被拉低一段时间,然后返回获取到的信号就OK了。

主机等待从机响应的过程为:

【SCL拉低、SDA拉高,主机释放数据总线】->【维持10us】->【SCL拉高,主机开始采集SDA线上的信号】->【存采集到的数据】->【维持10us】->【SCL拉低,结束数据采样】->【维持10us】-> 返回采集的信号

        需要注意的是,我们采集到的响应信号是16位的数据,而实际的响应只是一位的数据,所以最后返回的值我们将借助三元条件运算【exp1 ? exp2 : exp3】区分出应答于非应答信号后返回理应的一位数据。

参考代码如下

// 主机等待从机发出响应
uint8_t I2C_Wait4Ack(void)
{// 1. SCL拉低、SDA拉高、主机释放数据总线 SCL_LOW;SDA_HIGH;I2C_DELAY;// 2. SCL拉高、开始信号采样 从机控制SDA,主机不用管其状态SCL_HIGH;I2C_DELAY;// 3. 获取采集的响应uint16_t ack = READ_SDA;  // 4. SCL拉低,结束信号采样 数据总线由从机控制,主机设备不用管SDA线上的情况SCL_LOW;I2C_DELAY;return ack ? NACK : ACK;
}

5、主机向从机写入一个字节数据I2C_Sendbyte(uint8_t byte)

I2C通讯进行数据的读写时存在数据的有效性时序操作图,如下图所示

       前面介绍时序图时说过,数据的有效性指的是在SCL线为高电平时,SDA线上的信号要维持周期稳定。由于I2C通讯的数据传输时一种位传输的形式,且为高位先行。

        那么如何获取一个字节数据的高位呢?可以利用位与运算,由于一个字节是8位的数据,所以只需要让数据和1000 0000作位与运算即可得到,即byte & 0x80。

       所以传输一个字节的数据就意味着要循环8次去恰好在满足以上时序的情况下进行才有效。理解了时序图,其实代码也比较好写的。

主机写入单字节数据的过程为:

【SDA拉低、SCL拉低,EEPROM准备数据采样】->【维持10us】->【开始写入数据,获取单字节高位高位】->【转换成SDA线上的高低电平信号】->【维持10us】->【数据左移一位,获取低1位数据】->【SCL拉高,SDA保持不变,EEPROM开始数据采样】->【维持10us】->【SCL拉低,结束数据采样】->【维持10us】->循环过程8次后,主机写入单字节完成

参考代码如下: 

// 主机向从机写入一个字节的数据(发送)
void I2C_SendByte(uint8_t byte)
{for (uint8_t i = 0; i < 8; i++){// 1. SCL拉低、SDA拉低,准备数据采样SDA_LOW;SCL_LOW;I2C_DELAY;// 2. 获取单字节数据最高位if (byte & 0x80){SDA_HIGH;}else{SDA_LOW;}I2C_DELAY;// 3. SCL拉高,开始数据采样SCL_HIGH;I2C_DELAY;// 4. SCL拉低,结束数据采样SCL_LOW;I2C_DELAY;// 5. 左移一位byte <<= 1;}
}

6、主机向从机读取一个字节的数据uint8_t I2C_ReadByte()

读取操作同样会涉及到数据有效性,所以时序图与写入时一样如下

       主机读取从机一个字节的数据,就是相当于主机不是给数据的一方,而是接收数据的一方。换句话说,主机读取一个字节的数据就是在有效数采样过程中主机逐位读取从机发在SDA线上产生的信号,也就相当于是读取端口PB10上的电平,此时SDA线上数据的传递可理解为由EEPROM控制,所以此时我们只需控制时钟线SCL来采集从机传递的数据就行。读取和写入的区别主要就是在于数据采用时操作的不同,其他基本类似。

主机读取从机单字节数据的过程如下:

       创建8位数据类型的变量byte临时存放采集数据,【SCL拉低,等待数据翻转】->【维持10us】->【SCL拉高,开始采集从机发在SDA线上的信号】->【byte左移一位】->【byte从低位开始逐个存放获取的位数据】->【维持10us】->【SCL拉低,结束采样】-> 【维持10us】-> 前面循环8次后,返回byte即可

        值得注意的是,读取单字节数据时,我们需要先左移再存放,原因是避免第八次左移时将最高位数据移出缓冲区而出现错误,大家可以自己简单琢磨一下。

参考代码如下:

// 主机向从机读取一个字节的数据(接收)
uint8_t I2C_ReadByte(void)
{uint8_t byte = 0;for (uint8_t i = 0; i < 8; i++){// 1. SCL拉低,等待数据翻转SCL_LOW;I2C_DELAY;// 2. SCL拉高,开始从机的数据采样SCL_HIGH;I2C_DELAY;// 3. 读取从机数据 byte <<= 1;if (READ_SDA){byte |= 0x01; }// 4. SCL拉低,结束数据采样SCL_LOW;I2C_DELAY;}return byte;
}

这样,I2C通讯协议就实现完成了。


3.3.2 M24C02部分

3.3.2.1 m24c02.h

       接下来,我们来借助模拟的I2C协议实现32与m24c02直接的数据传递,首先是编写一下头文件。

1、头文件基本格式不要忘

防止头文件重复编译,通常编写头文件内容时初始会有统一的框架,然后在内部添加代码。

#ifndef __M24C02_H
#define __M24C02_H#endif

2、引用必要头文件

由于M24C02是直接借助模拟的I2C协议即可,同时i2c.h中已经引入了32的头文件,所以这里我们只需要引入I2C的头文件即可。

#include "i2c.h"

3、增加M24C02用到的宏定义

       根据前面对M24C02的读写时序操作介绍我们知道,对其进行读写操作时涉及到传递内部地址(byte address),用来指明写入数据到EEPROM的那一块内存单元或者从哪一块地址读取数据给主设备。由于读地址和写地址根据前面硬件电路的介绍已知已经固定下来,所以这里我们使用宏定义R_ADDR和W_ADDR来分别表示固定不变的读地址和写地址。

// 宏定义
#define W_ADDR (0xA0)
#define R_ADDR (0xA1)

4、可能调用的函数声明

       首先肯定会有一个M24C02的初始化函数。其次既然我们是用STM32作为主机与M24C02进行数据传递,那么自然会涉及到读写操作,也就是主机向M24C02写入/读取数据函数(包括单字节和多字节)。关于读写操作在M24C02的芯片手册中以及前面介绍M24C02中的读写操作时序时也是有所提到过的。

       总结一下涉及到的M24C02函数声明总共有5个,分别是【M24C02的初始化】、【向M24C02写入一个字节数据】、【向M24C02读取一个字节数据】、【向M24C02连续写入多个字节的数据】、【向M24C02连续读取多个字节的数据】

参考代码如下:

// 初始化
void M24C02_Init(void);// 写入一个字节的数据
void M24C02_Writebyte(uint8_t innerAddr, uint8_t byte);// 读取一个字节的数据
uint8_t M24C02_Readbyte(uint8_t innerAddr);// 连续写入多个字节的数据(页写)
void M24C02_Writebytes(uint8_t innerAddr, uint8_t * bytes, uint8_t size);// 连续读取多个字节的数据
void M24C02_Readbytes(uint8_t innerAddr, uint8_t * buffer, uint8_t size);

这样,关于M24C02的头文件就完成了。

m24c02.h参考代码如下

#ifndef __M24C02_H
#define __M24C02_H#include "i2c.h"// 宏定义
#define W_ADDR (0xA0)
#define R_ADDR (0xA1)// 初始化
void M24C02_Init(void);// 写入一个字节的数据
void M24C02_Writebyte(uint8_t innerAddr, uint8_t byte);// 读取一个字节的数据
uint8_t M24C02_Readbyte(uint8_t innerAddr);// 连续写入多个字节的数据(页写)
void M24C02_Writebytes(uint8_t innerAddr, uint8_t * bytes, uint8_t size);// 连续读取多个字节的数据
void M24C02_Readbytes(uint8_t innerAddr, uint8_t * buffer, uint8_t size);#endif

3.3.2.2 m24c02.c

       接下来,我们开始在M24C02源文件中完善这些函数。当然了,由于这些函数都是读写操作,所以均会涉及到相关时序,故编写过程中将不断对照M24C02读写操作的时序图,因此笔者建议在这之前一定要先理解清楚相关时序图的含义,然后再往下阅读!!!

1、M24C02的初始化M24C02_Init()

       因为M24C02和STM32间的通讯只是依赖I2C通讯协议,并没有使用其他硬件模块,因此其初始化只需要初始化一下I2C即可。

// 初始化
void M24C02_Init(void)
{I2C_Init();
}

2、向M24C02写入单字节数据M24C02_Writebyte(uint8_t innerAddr, uint8_t byte)

M24C02芯片手册中关于字节写入操作提供了相应的时序操作图如下

首先,WC写保护,这里前面硬件设计为固定一直保持可写状态,所以不用管;其次看写入的操作时序,在介绍时序图文章中对上图也做了比较详细的讲述,还算简单。

       由图可知,主机32发出起始信号后,最先传递的是设备地址,用于从机的匹配作用,对应的从机会自动对应上,同时紧跟写信号0表示此时对从机进行写入操作。然后等待从机应答,然后再传输内部地址给出写入数据的内存单元并等待从机应答。接着主机开始传输向从机写入的一字节具体数据,最后等待从机不应答结束数据写入,然后主机发出停止信号结束本次写入操作,最后延时5ms保证写入周期结束即可。

参考代码如下

// 主机写入一个字节的数据
void M24C02_Writebyte(uint8_t innerAddr, uint8_t byte)
{// 1. 主机发出起始信号I2C_Start();// 2. 主机传输设备地址,从机对应I2C_SendByte(W_ADDR);// 3. 等待m24c02应答uint8_t ack = I2C_Wait4Ack();if (ack == ACK){// 4. 主机传输内部地址I2C_SendByte(innerAddr);// 5. 等待从机应答I2C_Wait4Ack();// 6. 主机写入具体数据I2C_SendByte(byte);// 7. 等待应答I2C_Wait4Ack();// 8. 主机发出停止信号,结束写入数据I2C_Stop();}// 9. 延时等待字节写入周期结束Delay_ms(5);
}

大家会发现,关于等待从机应答并没有做详细的判断,主要原因如下:        

       这里我们简单起见,并没有对从机发出的应答信号做检查,也就是一致认为应答信号是没有问题的。因为实际山我们没有比较合适的调试方式去进行判断,同时及时出现响应异常主要是受自己控制,我们程序认为其没有问题即可,因此这里我们默认认为从机来的响应是正确的。

3、向M24C02读取单字节数据uint8_t M24C02_Readbyte(uint8_t innerAddr)

       同理这里放一个读取单字节的时序操作图以及相关解释(图中右数第二个ACK解释有误,应该是从机应答而不是主机应答)

       如上图,可见的是M24C02读操作会麻烦一些,但过程理解起来并不难。这是一个随机地址读取方式,主要是为了实现读取咱指定的内部地址的数据,所以在真正开始读取前要进行一个“假写”操作,即给出内部地址,使地址计数器(address counter)指向给的内部地址,但并不进行具体数据的写入。然后然后开始进行实际读取操作。即“假写真读”的操作。

       需要注意的是,读取操作是从机把数据给到主机,这意味着这个过程从机会控制数据总线然后主机响应是否收到从机传到数据总线上的信号。

       整个过程按照前面理解时序的思路可以很快的进行代码实现,这里图中也进行了详细解释,故直接放代码如下:

// 读取一个字节的数据
uint8_t M24C02_Readbyte(uint8_t innerAddr)
{// 1. 主机发出起始信号 I2C_Start();// 2. 主机传输设备地址(假写),从机对应I2C_SendByte(W_ADDR);// 3. 等待m24c02应答uint8_t ack = I2C_Wait4Ack();// 4. 主机传输内部地址I2C_SendByte(innerAddr);// 5. 等待m24c02应答I2C_Wait4Ack();// 6. 主机再次发出起始信号 I2C_Start();// 7. 主机传输设备地址(真读),m24c02对应I2C_SendByte(R_ADDR);// 8. 等待m24c02应答,m24c02开始控制数据总线I2C_Wait4Ack();// 9. 获取m24c02读取的数据uint8_t data = I2C_ReadByte();// 10. 主机发出非应答,m24c02释放数据总线I2C_Nack();// 11. 主机发出停止信号,结束数据读取I2C_Stop();return data;
}

4、向M24C02连续写入多个字节数据 M24C02_Writebytes(uint8_t innerAddr, uint8_t * bytes, uint8_t size) (也称页写)

同理,对照M24C02芯片手册中提供的连续写入操作时序图如下

       可以看出,连续写入实际上就是写入具体数据的过程被循环了N次,这个N代表了字节数。由于从机的响应这里简单默认视作都是对的,所以均等待从机响应就OK了。然后其余部分基本类似,没有啥变化,不好理解的话可以回头再看看写入单字节过程。

这里参考代码如下

// 连续写入多个字节的数据(页写)
void M24C02_Writebytes(uint8_t innerAddr, uint8_t * bytes, uint8_t size)
{// 1. 主机发出起始信号I2C_Start();// 2. 主机传输设备地址,从机对应I2C_SendByte(W_ADDR);// 3. 等待m24c02应答uint8_t ack = I2C_Wait4Ack();if (ack == ACK){// 4. 主机传输内部地址I2C_SendByte(innerAddr);// 5. 等待从机应答I2C_Wait4Ack();for (uint8_t i = 0; i < size; i++){// 6. 主机写入具体数据I2C_SendByte(bytes[i]);// 7. 等待应答I2C_Wait4Ack();}// 8. 主机发出停止信号,结束写入数据I2C_Stop();}// 9. 延时等待字节写入周期结束Delay_ms(5);
}

5、向M24C02连续读取多个字节数据 M24C02_Readbytes(uint8_t innerAddr, uint8_t * buffer, uint8_t size)        

       读取连续多字节数据的函数,我们不采用返回值的方式,因为字符串返回值是传指针的形式,相对毕竟麻烦容易出错,所以这里我们利用形参传递缓冲区buffer[]地址,实现字符串的获取。

同理,这里对照连续读取操作的时序图

       很显然,连续读取和读取单字节的区别就在于从机在SDA线上传的次数不同,连续就是重复的去传,即使用循环实现。不过这里要注意的是:要连续传的话主机要给出应答,使得从机知道还要继续传数据。直到主机给出非应答,从机才停止传输,然后释放数据总线,最后主机控制SDA并发出停止信号结束连续读取操作。

参考代码如下

// 连续读取多个字节的数据
void M24C02_Readbytes(uint8_t innerAddr, uint8_t * buffer, uint8_t size)
{// 1. 主机发出起始信号 I2C_Start();// 2. 主机传输设备地址(假写),从机对应I2C_SendByte(W_ADDR);// 3. 等待m24c02应答uint8_t ack = I2C_Wait4Ack();// 4. 主机传输内部地址I2C_SendByte(innerAddr);// 5. 等待m24c02应答I2C_Wait4Ack();// 6. 主机再次发出起始信号 I2C_Start();// 7. 主机传输设备地址(真读),m24c02对应I2C_SendByte(R_ADDR);// 8. 等待m24c02应答,开始控制数据总线I2C_Wait4Ack();for (uint8_t i = 0; i < size; i++){// 9. 获取m24c02读取的数据buffer[i] = I2C_ReadByte();// 10. 主机发出响应if (i < size - 1){I2C_Ack();}else{// 11. 主机发出非应答,m24c02释放数据总线I2C_Nack();}}// 12. 主机发出停止信号,结束数据读取I2C_Stop();
}

到这里的话,关于M24C02的代码也就完成了。


3.3.3 main中测试

       各个功能代码都写完了,接下来直接进入main.c中进行测试,该引入的头文件要引入,因文章篇幅有限,这里不在赘述。

本次主要按照需求将一些功能进行测试一下:

    1、读写一个字节的数据并发送到串口打印

    2、读写多个字节数据并到串口输出打印

    3、测试写入超过页的范围的情况是否符合手册所述

       要注意的是,我们这个工程是经过前面printf重定向工程进行改编的,所以关于串口输出打印的功能代码并没有直接展示,大家如果不清楚的可以参考下面文章中展示的寄存器实现代码STM32调试手段:重定向printf串口_printf 重定义-CSDN博客https://blog.csdn.net/2301_79475128/article/details/145305160?spm=1001.2014.3001.5501

参考代码如下

#include "usart.h"
#include "m24c02.h"
#include <string.h>int main(void)
{// 1. 初始化USART_Init();M24C02_Init();printf("software I2C will start...\n");// 2. 向m24c02中写入单字符M24C02_Writebyte(0x00, 'a');M24C02_Writebyte(0x01, 'b');M24C02_Writebyte(0x02, 'c');// 3. 向m24c02读取数据uint8_t byte1 = M24C02_Readbyte(0x00);uint8_t byte2 = M24C02_Readbyte(0x01);uint8_t byte3 = M24C02_Readbyte(0x02);// 4. 串口输出打印printf("byte1 = %c\t byte2 = %c\t byte3 = %c\n", byte1, byte2, byte3);// 5. 向m24c02写入字符串M24C02_Writebytes(0x00, "123456", 6);// 6. 向m24c02读取数据uint8_t buffer[100] = {0};M24C02_Readbytes(0x00, buffer, 6);// 7. 串口输出打印printf("buffer = %s\n", buffer);// 8. 测试页写超过数据范围// 缓冲区清零memset(buffer, 0, sizeof(buffer));M24C02_Writebytes(0x00, "1234567890abcdefghijk", 21);M24C02_Readbytes(0x00, buffer, 21);printf("test -> buffer = %s\n", buffer);// 死循环保持状态while(1){		}
}

        测试代码中可能用到了C语言相关的语法和函数,大家不清楚的自行去查阅,这里不再赘述。还需多多自己动手才能有所收获!

然后,编译了在串口助手看看效果吧:

三种测试显然是成功了。

对第三个测试,主要是为了验证手册中关于页写的相关描述

       大致意思就是说:对于同一页进行写入的时候,一次最多写入16个字节,一旦超过16个字节,那么剩余的字节将从该页最前面开始继续逐字节覆盖写入。

        我们看看第三个的测试现象:我们对某一页写入了1234567890abcdefghijk这21个字节的数据,然后读取后打印到串口助手上显示的仍然时16个字节,其中超过16字节后面的ghijk覆盖从头数的5字节数据,成功验证了手册中所述的结论。


四、总结

(1)本次案例基于STM32寄存器开发方式,用软件成功模拟I2C通讯协议;

(2)并实现了STM32与EEPROM间的I2C通讯,实现了一个字节或多个字节的写入和读取操作;

(3)进一步理解了I2C通讯的底层原理和时序操作过程,熟悉了STM32寄存器开发流程和编码步骤。

最后,欢迎各位在评论区分享自己的问题和思考,共同学习,谢谢!


以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!

鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/15483.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

脚手架开发【实战教程】prompts + fs-extra

创建项目 新建文件夹 mycli_demo 在文件夹 mycli_demo 内新建文件 package.json {"name": "mycli_demo","version": "1.0.0","bin": {"mycli": "index.js"},"author": "","l…

【大模型】DeepSeek-V3技术报告总结

系列综述&#xff1a; &#x1f49e;目的&#xff1a;本系列是个人整理为了学习DeepSeek相关知识的&#xff0c;整理期间苛求每个知识点&#xff0c;平衡理解简易度与深入程度。 &#x1f970;来源&#xff1a;材料主要源于DeepSeek官方技术报告进行的&#xff0c;每个知识点的…

只需三步!5分钟本地部署deep seek——MAC环境

MAC本地部署deep seek 第一步:下载Ollama第二步:下载deepseek-r1模型第三步&#xff1a;安装谷歌浏览器插件 第一步:下载Ollama 打开此网址&#xff1a;https://ollama.com/&#xff0c;点击下载即可&#xff0c;如果网络比较慢可使用文末百度网盘链接 注&#xff1a;Ollama是…

力扣hot100刷题第一天

哈希 1. 两数之和 题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用两次相同的元素。你可以按任意…

Linux(CentOS)安装 Nginx

CentOS版本&#xff1a;CentOS 7 Nginx版本&#xff1a;1.24.0 两种安装方式&#xff1a; 一、通过 yum 安装&#xff0c;最简单&#xff0c;一键安装&#xff0c;全程无忧。 二、通过编译源码包安装&#xff0c;需具备配置相关操作。 最后附&#xff1a;设置 Nginx 服务开…

项目6:基于大数据校园一卡通数据分析和可视化

1、项目简介 本项目是基于大数据的清华校园卡数据分析系统&#xff0c;通过Hadoop&#xff0c;spark等技术处理校园卡交易、卡号和商户信息数据。系统实现消费类别、男女消费差异、学院消费排行和年级对比等分析&#xff0c;并通过Web后端和可视化前端展示结果。项目运行便捷&…

Django项目中创建app并快速上手(pycharm Windows)

1.打开终端 我选择的是第二个 2.运行命令 python manage.py startapp 名称 例如&#xff1a; python manage.py startapp app01 回车&#xff0c;等待一下&#xff0c;出现app01的文件夹说明创建成功 3.快速上手 1.app注册 增加一行 "app01.apps.App01Config"&#…

使用Docker + Ollama在Ubuntu中部署deepseek

1、安装docker 这里建议用docker来部署&#xff0c;方便简单 安装教程需要自己找详细的&#xff0c;会用到跳过 如果你没有安装 Docker&#xff0c;可以按照以下步骤安装&#xff1a; sudo apt update sudo apt install apt-transport-https ca-certificates curl software-p…

信创领域的PostgreSQL管理员认证

信创产业&#xff0c;全称为信息技术应用创新产业&#xff0c;是中国为应对国际技术竞争、保障信息安全、实现科技自立而重点发展的战略性新兴产业。其核心目标是通过自主研发和生态构建&#xff0c;逐步替代国外信息技术产品&#xff0c;形成自主可控的国产化信息技术体系。 发…

jemalloc的malloc案例来分析GOT表和PLT表有关流程

一、背景 在之前的博客 跟踪jemalloc 5.3.0的第一次malloc的源头原因及jemalloc相关初始化细节拓展-CSDN博客 里&#xff0c;我们分析了在preload jemalloc的库之后&#xff0c;main之前的一次malloc分配&#xff08;分配72704字节&#xff09;的源头原因并做了jemalloc的初始…

Centos Ollama + Deepseek-r1+Chatbox运行环境搭建

Centos Ollama Deepseek-r1Chatbox运行环境搭建 内容介绍下载ollama在Ollama运行DeepSeek-r1模型使用chatbox连接ollama api 内容介绍 你好&#xff01; 这篇文章简单讲述一下如何在linux环境搭建 Ollama Deepseek-r1。并在本地安装的Chatbox中进行远程调用 下载ollama 登…

使用sunshine和moonlight串流时的音频输出问题

设备&#xff1a;电脑和平板串流&#xff0c;把平板当副屏使用 1.如果启用安装steam音频驱动程序&#xff0c;则平板有声&#xff0c;电脑无声&#xff0c;在moonlight端可以设置平板和电脑同时发声&#xff0c;但是有点卡 2.只想电脑发声&#xff0c;平板无声 禁用安装steam…

微信小程序案例2——天气微信小程序(学会绑定数据)

文章目录 一、项目步骤1 创建一个weather项目2 进入index.wxml、index.js、index.wxss文件,清空所有内容,进入App.json,修改导航栏标题为“中国天气网”。3进入index.wxml,进行当天天气情况的界面布局,包括温度、最低温、最高温、天气情况、城市、星期、风行情况,代码如下…

如何在WPS和Word/Excel中直接使用DeepSeek功能

以下是将DeepSeek功能集成到WPS中的详细步骤&#xff0c;无需本地部署模型&#xff0c;直接通过官网连接使用&#xff1a;1. 下载并安装OfficeAI插件 &#xff08;1&#xff09;访问OfficeAI插件下载地址&#xff1a;OfficeAI助手 - 免费办公智能AI助手, AI写作&#xff0c;下载…

数字电路-基础逻辑门实验

基础逻辑门是数字电路设计的核心元件&#xff0c;它们执行的是基本的逻辑运算。通过这些基本运算&#xff0c;可以构建出更为复杂的逻辑功能。常见的基础逻辑门包括与门&#xff08;AND&#xff09;、或门&#xff08;OR&#xff09;、非门&#xff08;NOT&#xff09;、异或门…

哪吒闹海!SCI算法+分解组合+四模型原创对比首发!SGMD-FATA-Transformer-LSTM多变量时序预测

哪吒闹海&#xff01;SCI算法分解组合四模型原创对比首发&#xff01;SGMD-FATA-Transformer-LSTM多变量时序预测 目录 哪吒闹海&#xff01;SCI算法分解组合四模型原创对比首发&#xff01;SGMD-FATA-Transformer-LSTM多变量时序预测效果一览基本介绍程序设计参考资料 效果一览…

C++,STL 迭代器简介:概念、分类、操作

文章目录 引言一、迭代器的基本概念1.1 什么是迭代器?1.2 迭代器的意义二、迭代器的分类2.1 示意图:迭代器能力层级2.2 示例:不同迭代器的操作三、迭代器的常用操作3.1 基本操作3.2 随机访问迭代器专用操作示例代码:随机访问迭代器四、迭代器的通用用法4.1 遍历容器4.2 配合…

EasyExcel 导出合并层级单元格

EasyExcel 导出合并层级单元格 一、案例 案例一 1.相同订单号单元格进行合并 合并结果 案例二 1.相同订单号的单元格进行合并2.相同订单号的总数和总金额进行合并 合并结果 案例三 1.相同订单号的单元格进行合并2.相同订单号的商品分类进行合并3.相同订单号的总数和总金额…

常用的python库-安装与使用

常用的python库函数 yield关键字openslide库openslide对象的常用属性 cv2库numpy库ASAP库-multiresolutionimageinterface库ASAP库的安装ASAP库的使用 concurrent.futures.ThreadPoolExecutorxml.etree.ElementTree库skimage库PIL.Image库 PIL.Image.Imagedetectron2库数据增强…

C++基础系列【8】如何解决编译器报的错误

博主介绍&#xff1a;程序喵大人 35- 资深C/C/Rust/Android/iOS客户端开发10年大厂工作经验嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手《C20高级编程》《C23高级编程》等多本书籍著译者更多原创精品文章&#xff0c;首发gzh&#xff0c;见文末&#x1f447;&#x1f…