软件模拟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.mzph.cn/diannao/69486.shtml

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

相关文章

爬虫技巧汇总

一、UA大列表 USER_AGENT_LIST 是一个包含多个用户代理字符串的列表&#xff0c;用于模拟不同浏览器和设备的请求。以下是一些常见的用户代理字符串&#xff1a; USER_AGENT_LIST [Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; Hot Lingo 2.0),Mozilla…

35~37.ppt

目录 35.张秘书-《会计行业中长期人才发展规划》 题目​ 解析 36.颐和园公园&#xff08;25张PPT) 题目​ 解析 37.颐和园公园&#xff08;22张PPT) 题目 解析 35.张秘书-《会计行业中长期人才发展规划》 题目 解析 插入自定义的幻灯片&#xff1a;新建幻灯片→重用…

【Android开发AI实战】基于CNN混合YOLOV实现多车牌颜色区分且针对车牌进行矫正识别(含源码)

文章目录 引言单层卷积神经网络&#xff08;Single-layer CNN&#xff09;&#x1f4cc; 单层 CNN 的基本结构&#x1f4cc; 单层 CNN 计算流程图像 透视变换矫正车牌c实现&#x1fa84;关键代码实现&#xff1a;&#x1fa84;crnn结构图 使用jni实现高级Android开发&#x1f3…

DeepSeek Window本地私有化部署

前言 最近大火的国产AI大模型Deepseek大家应该都不陌生。除了在手机上安装APP或通过官网在线体验&#xff0c;其实我们完全可以在Windows电脑上进行本地部署&#xff0c;从而带来更加便捷的使用体验。 之前也提到过&#xff0c;本地部署AI模型有很多好处&#xff0c;比如&…

STM32G474--Whetstone程序移植(单精度)笔记

1 准备基本工程代码 参考这篇笔记从我的仓库中选择合适的基本工程&#xff0c;进行程序移植。这里我用的是stm32g474的基本工程。 使用git clone一个指定文件或者目录 2 移植程序 2.1 修改Whetstone.c 主要修改原本变量定义的类型&#xff0c;以及函数接口全部更换为单精度…

【专题】2024-2025人工智能代理深度剖析:GenAI 前沿、LangChain 现状及演进影响与发展趋势报告汇总PDF洞察(附原数据表)

原文链接&#xff1a;https://tecdat.cn/?p39630 在科技飞速发展的当下&#xff0c;人工智能代理正经历着深刻的变革&#xff0c;其能力演变已然成为重塑各行业格局的关键力量。从早期简单的规则执行&#xff0c;到如今复杂的自主决策与多智能体协作&#xff0c;人工智能代理…

QT修仙之路1-1--遇见QT

文章目录 遇见QT二、QT概述2.1 定义与功能2.2 跨平台特性2.3 优点汇总 三、软件安装四、QT工具介绍(重要)4.1 Assistant4.2 Designer4.3 uic.exe4.4 moc.exe4.5 rcc.exe4.6 qmake4.7 QTcreater 五、QT工程项目解析(作业)5.1 配置文件&#xff08;.pro&#xff09;5.2 头文件&am…

Linux——基础命令1

$&#xff1a;普通用户 #&#xff1a;超级用户 cd 切换目录 cd 目录 &#xff08;进入目录&#xff09; cd ../ &#xff08;返回上一级目录&#xff09; cd ~ &#xff08;切换到当前用户的家目录&#xff09; cd - &#xff08;返回上次目录&#xff09; pwd 输出当前目录…

Office/WPS接入DeepSeek等多个AI工具,开启办公新模式!

在现代职场中&#xff0c;Office办公套件已成为工作和学习的必备工具&#xff0c;其功能强大但复杂&#xff0c;熟练掌握需要系统的学习。为了简化操作&#xff0c;使每个人都能轻松使用各种功能&#xff0c;市场上涌现出各类办公插件。这些插件不仅提升了用户体验&#xff0c;…

【提示词工程】探索大语言模型的参数设置:优化提示词交互的技巧

在与大语言模型(Large Language Model, LLM)进行交互时,提示词的设计和参数设置直接影响生成内容的质量和效果。无论是通过 API 调用还是直接使用模型,掌握模型的参数配置方法都至关重要。本文将为您详细解析常见的参数设置及其应用场景,帮助您更高效地利用大语言模型。 …

Ollama + AnythingLLM + Deepseek r1 实现本地知识库

1、Ollama&#xff1a;‌是一个开源的大型语言模型 (LLM)服务工具&#xff0c;旨在简化在本地运行大语言模型的过程&#xff0c;降低使用大语言模型的门槛‌。 2、AnythingLLM&#xff1a;是由Mintplex Labs Inc. 开发的一款全栈应用程序&#xff0c;旨在构建一个高效、可定制、…

伪分布式Spark3.4.4安装

参考&#xff1a;Spark2.1.0入门&#xff1a;Spark的安装和使用_厦大数据库实验室博客 我的版本&#xff1a; hadoop 3.1.3 hbase 2.2.2 java openjdk version "1.8.0_432" 问了chatgpt,建议下载Spark3.4.4&#xff0c;不适合下载Spark 2.1.0: step1 Spark下载…

从运输到植保:DeepSeek大模型探索无人机智能作业技术详解

DeepSeek&#xff0c;作为一家专注于深度学习与人工智能技术研究的企业&#xff0c;近年来在AI领域取得了显著成果&#xff0c;尤其在无人机智能作业技术方面展现了其大模型的强大能力。以下是从运输到植保领域&#xff0c;DeepSeek大模型探索无人机智能作业技术的详解&#xf…

免费windows pdf编辑工具Epdf

Epdf&#xff08;完全免费&#xff09; 作者&#xff1a;不染心 时间&#xff1a;2025/2/6 Github: https://github.com/dog-tired/Epdf Epdf Epdf 是一款使用 Rust 编写的 PDF 编辑器&#xff0c;目前仍在开发中。它提供了一系列实用的命令行选项&#xff0c;方便用户对 PDF …

基于深度学习的人工智能量化衰老模型构建与全流程应用研究

一、引言 1.1 研究背景与意义 1.1.1 人口老龄化现状与挑战 人口老龄化是当今全球面临的重要社会趋势之一,其发展态势迅猛且影响深远。根据联合国的相关数据,1980 年,全球 65 岁及以上人口数量仅为 2.6 亿,到 2021 年,这一数字已翻番,达到 7.61 亿,而预计到 2050 年,…

UnityShader学习笔记——深度与法线纹理

——内容源自唐老狮的shader课程 目录 1.概述 1.1.分别指什么 1.2.如何获取 1.2.1.对摄像机赋值 1.2.2.在Shader中声明 1.2.3.获取深度值 1.2.4.获取法线纹理 1.3.背后的原理 1.3.1.深度纹理中存储的是什么信息 1.3.2.法线纹理中存储的是什么信息 1.3.3.unity是如何…

基于STM32的智能鱼缸水质净化系统设计

&#x1f91e;&#x1f91e;大家好&#xff0c;这里是5132单片机毕设设计项目分享&#xff0c;今天给大家分享的是智能鱼缸水质净化系统。 目录 1、设计要求 2、系统功能 3、演示视频和实物 4、系统设计框图 5、软件设计流程图 6、原理图 7、主程序 8、总结 1、设计要求…

如何打造一个更友好的网站结构?

在SEO优化中&#xff0c;网站的结构往往被忽略&#xff0c;但它其实是决定谷歌爬虫抓取效率的关键因素之一。一个清晰、逻辑合理的网站结构&#xff0c;不仅能让用户更方便地找到他们需要的信息&#xff0c;还能提升搜索引擎的抓取效率 理想的网站结构应该像一棵树&#xff0c;…

尝试在Excel里调用硅基流动上的免费大语言模型

我个人觉得通过api而不是直接浏览器客户端聊天调用大语言模型是使用人工智能大模型的一个相对进阶的阶段。 于是就尝试了一下。我用的是老师木 袁进辉博士新创的硅基流动云上的免费的大模型。——虽然自己获赠了不少免费token&#xff0c;但测试阶段用不上。 具体步骤如下&am…

“公路养护新利器!公路 AI 智慧巡检系统

家人们&#xff0c;咱日常开车出行&#xff0c;最烦的就是遇到路面坑洼、道路破损的情况&#xff0c;不仅颠簸难受&#xff0c;还存在安全隐患。其实&#xff0c;这些问题都得靠公路养护人员及时发现并处理。但以往的公路巡检工作可不容易&#xff0c;现在好了&#xff0c;有了…