软件模拟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-11
    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、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寄存器开发流程和编码步骤。

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


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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【redis】数据类型之hash

Redis中的Hash数据类型是一种用于存储键值对集合的数据结构。与Redis的String类型不同&#xff0c;Hash类型允许你将多个字段&#xff08;field&#xff09;和值&#xff08;value&#xff09;存储在一个单独的key下&#xff0c;从而避免了将多个相关数据存储为多个独立的key。…

5.2Internet及其作用

5.2.1Internet概述 Internet称为互联网&#xff0c;又称英特网&#xff0c;始于1969年的美国ARPANET&#xff08;阿帕网&#xff09;&#xff0c;是全球性的网络。 互连网指的是两个或多个不同类型的网络通过路由器等网络设备连接起来&#xff0c;形成一个更大的网络结构。互连…

深度学习模型蒸馏技术的发展与应用

随着人工智能技术的快速发展&#xff0c;大型语言模型和深度学习模型在各个领域展现出惊人的能力。然而&#xff0c;这些模型的规模和复杂度也带来了显著的部署挑战。模型蒸馏技术作为一种优化解决方案&#xff0c;正在成为连接学术研究和产业应用的重要桥梁。本文将深入探讨模…

网络与数据安全

目录 数据加密对称加密&#xff08;Symmetric Encryption&#xff09;非对称加密&#xff08;Asymmetric Encryption&#xff09;哈希算法&#xff08;Hash Functions&#xff09;数字签名&#xff08;Digital Signature&#xff09;密钥管理&#xff08;Key Management&#x…

< OS 有关 > 利用 google-drive-ocamlfuse 工具,在 Ubuntu 24 系统上 加载 Google DRIVE 网盘

Created by Dave On 8Feb.2025 起因&#xff1a; 想下载 StableDiffusion&#xff0c;清理系统文件时把 i/o 搞到 100%&#xff0c;已经删除到 apt 缓存&#xff0c;还差 89MB&#xff0c;只能另想办法。 在网上找能不能挂在 Google 网盘&#xff0c;百度网盘&#xff0c;或 …

05vue3实战-----配置项目代码规范

05vue3实战-----配置项目代码规范 1.集成editorconfig配置2.使用prettier工具2.1安装prettier2.2配置.prettierrc文件&#xff1a;2.3创建.prettierignore忽略文件2.4VSCode需要安装prettier的插件2.5VSCod中的配置2.6测试prettier是否生效 3.使用ESLint检测3.1VSCode需要安装E…

【漫话机器学习系列】084.偏差和方差的权衡(Bias-Variance Tradeoff)

偏差和方差的权衡&#xff08;Bias-Variance Tradeoff&#xff09; 1. 引言 在机器学习模型的训练过程中&#xff0c;我们常常面临一个重要的挑战&#xff1a;如何平衡 偏差&#xff08;Bias&#xff09; 和 方差&#xff08;Variance&#xff09;&#xff0c;以提升模型的泛…

23.PPT:校摄影社团-摄影比赛作品【5】

目录 NO12345​ NO6 NO7/8/9/10​ 单元格背景填充表格背景填充文本框背景填充幻灯片背景格式设置添加考生文件夹下的版式 NO12345 插入幻灯片和放入图片☞快速&#xff1a;插入→相册→新建相册→文件→图片版式→相框形状→调整边框宽度左下角背景图片&#xff1a;视图→…

OpenCV:图像修复

目录 简述 1. 原理说明 1.1 Navier-Stokes方法&#xff08;INPAINT_NS&#xff09; 1.2 快速行进方法&#xff08;INPAINT_TELEA&#xff09; 2. 实现步骤 2.1 输入图像和掩膜&#xff08;Mask&#xff09; 2.2 调用cv2.inpaint()函数 2.3 完整代码示例 2.4 运行结果 …

快速建立私有化知识库(私有化训练DeepSeek,通过ollama方式)

简介 什么&#xff1f;&#xff01;老是有人问你需求&#xff0c;不同版本的需求你记不清还得去扒拉过程文档、设计文档&#xff1f; 什么&#xff1f;&#xff01;领导会询问功能使用情况、用户相关数据&#xff0c;你每次还得手动查询反馈&#xff1f; 什么&#xff1f;&…

python脚本实现windows电脑内存监控内存清理(类似rammap清空工作集功能)

import ctypes import psutil import time import sys import os from datetime import datetime import pyautogui# 检查管理员权限 def is_admin():try:return ctypes.windll.shell32.IsUserAnAdmin()except:return False# 内存清理核心功能 def cleanup_memory(aggressivene…

网络安全:挑战、技术与未来发展

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 1. 引言 在数字化时代&#xff0c;网络安全已成为全球关注的焦点。随着互联网的普及和信息技术的高速发展&#xff0c;网络攻击的…

Verilog语言学习总结

Verilog语言学习&#xff01; 目录 文章目录 前言 一、Verilog语言是什么&#xff1f; 1.1 Verilog简介 1.2 Verilog 和 C 的区别 1.3 Verilog 学习 二、Verilog基础知识 2.1 Verilog 的逻辑值 2.2 数字进制 2.3 Verilog标识符 2.4 Verilog 的数据类型 2.4.1 寄存器类型 2.4.2 …

35.Word:公积金管理中心文员小谢【37】

目录 Word1.docx ​ Word2.docx Word2.docx ​ 注意本套题还是与上一套存在不同之处 Word1.docx 布局样式的应用设计页眉页脚位置在水平/垂直方向上均相对于外边距居中排列&#xff1a;格式→大小对话框→位置→水平/垂直 按下表所列要求将原文中的手动纯文本编号分别替换…

Python----Python高级(并发编程:协程Coroutines,事件循环,Task对象,协程间通信,协程同步,将协程分布到线程池/进程池中)

一、协程 1.1、协程 协程&#xff0c;Coroutines&#xff0c;也叫作纤程(Fiber) 协程&#xff0c;全称是“协同程序”&#xff0c;用来实现任务协作。是一种在线程中&#xff0c;比线程更加轻量级的存在&#xff0c;由程序员自己写程序来管理。 当出现IO阻塞时&#xff0c;…

amis组件crud使用踩坑

crud注意 过滤条件参数同步地址栏 默认 CRUD 会将过滤条件参数同步至浏览器地址栏中&#xff0c;比如搜索条件、当前页数&#xff0c;这也做的目的是刷新页面的时候还能进入之前的分页。 但也会导致地址栏中的参数数据合并到顶层的数据链中&#xff0c;例如&#xff1a;自动…

机器学习8-卷积和卷积核1

机器学习8-卷积和卷积核1 卷积与图像去噪卷积的定义与性质定义性质卷积的原理卷积步骤卷积的示例与应用卷积的优缺点优点缺点 总结 高斯卷积核卷积核尺寸的设置依据任务类型考虑数据特性实验与调优 高斯函数标准差的设置依据平滑需求结合卷积核尺寸实际应用场景 总结 图像噪声与…

Dubbo 3.x源码(29)—Dubbo Consumer服务调用源码(1)服务调用入口

基于Dubbo 3.1&#xff0c;详细介绍了Dubbo Consumer服务调用源码。 此前我们学习了Dubbo服务的导出和引入的源码&#xff0c;现在我们来学习Dubbo服务调用的源码。 此前的文章中我们讲过了最上层代理的调用逻辑(服务引用bean的获取以及懒加载原理)&#xff1a;业务引入的接口…

【信息系统项目管理师-案例真题】2016下半年案例分析答案和详解

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 试题一【问题1】4 分【问题2】12 分【问题3】3 分【问题4】6 分试题二【问题1】3 分【问题2】4 分【问题3】8 分【问题4】5 分【问题5】5 分试题三【问题1】4 分【问题2】8 分【问题3】5 分【问题4】8 分试题一…

pytest-xdist 进行多进程并发测试!

在软件开发过程中&#xff0c;测试是确保代码质量和可靠性的关键步骤。随着项目规模的扩大和复杂性的增加&#xff0c;测试用例的执行效率变得尤为重要。为了加速测试过程&#xff0c;特别是对于一些可以并行执行的测试用 例&#xff0c;pytest-xdist 提供了一种强大的工具&…