当前位置: 首页 > news >正文

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

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


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

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

相关文章:

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

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

开源CodeGPT + DeepSeek-R1 是否可以替代商业付费代码辅助工具

开源CodeGPT + DeepSeek-R1 是否可以替代商业付费代码辅助工具 背景与研究目的 在快速发展的软件开发领域,代码辅助工具已成为提高开发效率和质量的关键。然而,商业付费工具如通义灵码和腾讯AI代码助手,尽管功能强大,但其高昂的成本和许可证限制,使得许多企业寻求更具吸…...

c++:list

1.list的使用 1.1构造 1.2迭代器遍历 &#xff08;1&#xff09;迭代器是算法和容器链接起来的桥梁 容器就是链表&#xff0c;顺序表等数据结构&#xff0c;他们有各自的特点&#xff0c;所以底层结构是不同的。在不用迭代器的前提下&#xff0c;如果我们的算法要作用在容器上…...

Websocket从原理到实战

引言 WebSocket 是一种在单个 TCP 连接上进行全双工通信的网络协议&#xff0c;它使得客户端和服务器之间能够进行实时、双向的通信&#xff0c;既然是通信协议一定要从发展历史到协议内容到应用场景最后到实战全方位了解 发展历史 WebSocket 最初是为了解决 HTTP 协议在实时…...

探索robots.txt:网站管理者的搜索引擎指南

在数字时代&#xff0c;网站如同企业的在线名片&#xff0c;其内容和结构对搜索引擎的可见性至关重要。而在这背后&#xff0c;有一个默默工作的文件——robots.txt&#xff0c;它扮演着搜索引擎与网站之间沟通桥梁的角色。本文将深入探讨robots.txt的功能、编写方法及其在现代…...

ubuntu中如何在vscode的终端目录后显示(当前的git分支名) 实测有用

效果展示 配置过程&#xff1a; 在 Ubuntu 中&#xff0c;如果你想在 VS Code 的终端提示符后显示当前的 Git 分支名&#xff0c;可以通过修改 Shell 配置文件&#xff08;如 ~/.bashrc 或 ~/.zshrc&#xff09;来实现。以下是具体步骤&#xff1a; 1. 确定使用的 Shell 首…...

0012—数组

存取一组数据&#xff0c;使用数组。 数组是一组相同类型元素的集合。 要存储1-10的数字&#xff0c;怎么存储&#xff1f; C语言中给了数组的定义&#xff1a;一组相同类型元素的集合。 创建一个空间创建一组数&#xff1a; 一、数组的定义 int arr[10] {1,2,3,4,5,6,7,8,…...

决策树算法相关文献

决策树是一种基于树状结构的机器学习算法&#xff0c;广泛应用于分类和回归任务。尽管决策树算法已经非常成熟&#xff0c;但研究者们仍在不断探索新的方法和技术&#xff0c;以进一步提升其性能、适应性和可解释性。 以下是当前研究者对决策树算法的最新研究方向和内容&#x…...

DeepSeek在FPGA/IC开发中的创新应用与未来潜力

随着人工智能技术的飞速发展&#xff0c;以DeepSeek为代表的大语言模型&#xff08;LLM&#xff09;正在逐步渗透到传统硬件开发领域。在FPGA&#xff08;现场可编程门阵列&#xff09;和IC&#xff08;集成电路&#xff09;开发这一技术密集型行业中&#xff0c;DeepSeek凭借其…...

学习threejs,使用Lensflare模拟镜头眩光

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.Lensflare 二、&…...

RuoYi-Vue-Oracle的oracle driver驱动配置问题ojdbc8-12.2.0.1.jar的解决

RuoYi-Vue-Oracle的oracle driver驱动配置问题ojdbc8-12.2.0.1.jar的解决 1、报错情况 下载&#xff1a;https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle 用idea打开&#xff0c;启动&#xff1a; 日志有报错&#xff1a; 点右侧m图标&#xff0c;maven有以下报误 &…...

kafka服务端之日志磁盘存储

文章目录 页缓存顺序写零拷贝 Kafka依赖于文件系统&#xff08;更底层地来说就是磁盘&#xff09;来存储和缓存消息 。 那么kafka是如何让自身在使用磁盘存储的情况下达到高性能的&#xff1f;接下来主要从3各方面详细解说。 页缓存 页缓存是操作系统实现的一种主要的磁盘缓存…...

deepseek来讲lua

Lua 是一种轻量级、高效、可嵌入的脚本语言&#xff0c;广泛应用于游戏开发、嵌入式系统、Web 服务器等领域。以下是 Lua 的主要特点和一些基本概念&#xff1a; 1. 特点 轻量级&#xff1a;Lua 的核心非常小&#xff0c;适合嵌入到其他应用程序中。高效&#xff1a;Lua 的执…...

C++开发(软件开发)常见面试题

目录 1、C里指针和数组的区别 2、C中空指针请使用nullptr不要使用NULL 3、http/https区别和头部结构&#xff1f; 4、有了mac地址为什么还要ip地址&#xff1f;ip地址的作用 5、有了路由器为什么还要交换机&#xff1f; 6、面向对象三大特性 7、友元函数 8、大端小端 …...

AtCoder Beginner Contest 391(A~E题题解)

A - Lucky Direction 思路&#xff1a;纯模拟的一个水题 #include <bits/stdc.h> using namespace std; #define int long long string s; signed main() { cin>>s;for(int i0;i<s.size();i){char cs[i];if(cN){cout<<"S";}else if(c…...

使用线性回归模型逼近目标模型 | PyTorch 深度学习实战

前一篇文章&#xff0c;计算图 Compute Graph 和自动求导 Autograd | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started 使用线性回归模型逼近目标模型 什么是回归什么是线性回归使用 PyTorch 实现线性回归模型代码执行结…...

解决com.kingbase8.util.KSQLException: This _connection has been closed.

问题描述 一个消息管理系统,系统采用kingbase8数据库,数据库采用单体模式,后台应用也采用springboot单体模式。系统正式上线后,出现几个JDBC响应的异常信息: com.kingbase8.util.KSQLException: An I/O error occurred while sending to the backend.java.net.SocketTime…...

Pyqt 的QTableWidget组件

QTableWidget 是 PyQt6 中的一个表格控件&#xff0c;用于显示和编辑二维表格数据。它继承自 QTableView&#xff0c;提供了更简单的方式来处理表格数据&#xff0c;适合用于需要展示结构化数据的场景。 1. 常用方法 1.1 构造函数 QTableWidget(parent: QWidget None)&#x…...

DeepSeek R1 Distill Llama 70B(免费版)API使用详解

DeepSeek R1 Distill Llama 70B&#xff08;免费版&#xff09;API使用详解 在人工智能领域&#xff0c;随着技术的不断进步&#xff0c;各种新的模型和应用如雨后春笋般涌现。今天&#xff0c;我们要为大家介绍的是OpenRouter平台上提供的DeepSeek R1 Distill Llama 70B&…...

24.ppt:小李-图书策划方案【1】

目录 NO1234​ NO5678​ NO1234 新建PPT两种方式&#x1f447;docx中视图→导航窗格→标题1/2/3ppt新建幻灯片→从大纲→重置开始→版式设计→主题插入→表格 NO5678 SmartArt演示方案&#xff1a;幻灯片放映→自定义幻灯片放映→新建→选中添加...

百科词条创建审核不通过的原因有哪些?

我们知道的国内有名的百科网站有百度百科、快懂百科、搜狗百科、360百科,这些有名的百科网站。一般来说,百科的词条排名都是在第一页的,无论是名人、明星、软件、影视名称,还是其他名称,大多排名都在首页,这就拥有了更多的曝光量和流量,而且由于百科是人们获取信息、查找资料的…...

amis组件crud使用踩坑

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

【docker】Failed to allocate manager object, freezing:兼容兼容 cgroup v1 和 v2

参考大神让系统同时兼容 cgroup v1 和 v2 要解决你系统中只挂载了 cgroup v2 但需要兼容 cgroup v1 的问题,可以通过以下几步来使系统同时兼容 cgroup v1 和 cgroup v2。这样 Docker 和其他服务就可以正常工作了。步骤 1:更新 Grub 配置,启用兼容模式 编辑 GRUB 配置来启用同…...

LeetCode:503.下一个更大元素II

跟着carl学算法&#xff0c;本系列博客仅做个人记录&#xff0c;建议大家都去看carl本人的博客&#xff0c;写的真的很好的&#xff01; 代码随想录 LeetCode&#xff1a;503.下一个更大元素II 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[…...

【AI日记】25.02.08

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】【读书与思考】【AI应用】 探索 AI 应用探索周二有个面试&#xff0c;明后天打算好好准备一下&#xff0c;我打算主要研究下 AI 如何在该行业赋能和应用&#xff0c;以及该行业未来的发展前景和公司痛点&#…...

32. C 语言 安全函数( _s 尾缀)

本章目录 前言什么是安全函数&#xff1f;安全函数的特点主要的安全函数1. 字符串操作安全函数2. 格式化输出安全函数3. 内存操作安全函数4. 其他常用安全函数 安全函数实例示例 1&#xff1a;strcpy_s 和 strcat_s示例 2&#xff1a;memcpy_s示例 3&#xff1a;strtok_s 总结 …...

常用数据结构之String字符串

字符串 在Java编程语言中&#xff0c;字符可以使用基本数据类型char来保存&#xff0c;在 Java 中字符串属于对象&#xff0c;Java 提供了 String 类来创建和操作字符串。 操作字符串常用的有三种类&#xff1a;String、StringBuilder、StringBuffer 接下来看看这三类常见用…...

Android开发获取缓存,删除缓存

Android开发获取缓存&#xff0c;删除缓存 app设置中往往有清理缓存的功能。会显示当前缓存时多少&#xff0c;然后可以点击清理缓存 直接上代码&#xff1a; object CacheHelper {/*** 获取缓存大小* param context* return* throws Exception*/JvmStaticfun getTotalCache…...

网络安全 | 保护智能家居和企业IoT设备的安全策略

网络安全 | 保护智能家居和企业IoT设备的安全策略 一、前言二、智能家居和企业 IoT 设备面临的安全威胁2.1 设备自身安全缺陷2.2 网络通信安全隐患2.3 数据隐私风险2.4 恶意软件和攻击手段 三、保护智能家居和企业 IoT 设备的安全策略3.1 设备安全设计与制造环节的考量3.2 网络…...

掌握API和控制点(从Java到JNI接口)_38 JNI从C调用Java函数 01

1. Why? 将控制点下移到下C/C层 对古典视角的反思 App接近User&#xff0c;所以App在整体架构里&#xff0c;是主导者&#xff0c;拥有控制权。所以&#xff0c; App是架构的控制点所在。Java函数调用C/C层函数&#xff0c;是合理的。 但是EIT造形告诉我们&#xff1a; App…...

vue组件间的数据传递:自定义输入组件(v-model/defineModel)

文章目录 引言I Vue 3.4 开始,推荐使用 defineModel() 宏子组件使用defineModel父组件用 v-model 绑定值底层机制II Vue 3.4之前:自定义输入组件(组件中实现 v-model )前置知识父组件监听自定义事件进行 v-model 的数据绑定子组件通过监听input事件触发自定义的 `update:mo…...

记录一下 在Mac下用pyinstallter 打包 Django项目

安装: pip install pyinstaller 在urls.py from SheepMasterOneToOne import settings from django.conf.urls.static import staticurlpatterns [path("admin/", admin.site.urls),path(generate_report/export/, ReportAdmin(models.Report, admin.site).generat…...

大模型相关概念

文章目录 部署相关数据并行模型并行张量并行管道并行&#xff08;流水线并行&#xff09; 混合并行&#xff08;数据并行模型并行&#xff09;显存优化技术InfiniBand去中心化的All-Reduce操作软件 大模型命名**1. 模型架构相关****2. 模型用途相关****3. 训练方法相关****4. 多…...

【实用教程】在 Android Studio 中连接 MuMu 模拟器

MuMu 模拟器是一个非常流行的安卓模拟器&#xff0c;特别适合开发人员进行应用测试&#xff0c;我使用它的根本原因在于Android Studio自带的AVM实现是太难用了&#xff0c;但是Mumu模拟器启动以后不会自动被Android Studio识别到&#xff0c;但是其他模拟器都是能够正常被Andr…...

Linux 安装 Ollama

1、下载地址 Download Ollama on Linux 2、有网络直接执行 curl -fsSL https://ollama.com/install.sh | sh 命令 3、下载慢的解决方法 1、curl -fsSL https://ollama.com/install.sh -o ollama_install.sh 2、sed -i s|https://ollama.com/download/ollama-linux|https://…...

双亲委派(JVM)

1.双亲委派 在 Java 中&#xff0c;双薪委派通常是指双亲委派模型&#xff0c;它是 Java 类加载器的一种工作模式&#xff0c;用于确保类加载的安全性和一致性。以下是其相关介绍&#xff1a; 定义与作用 定义&#xff1a;双亲委派模型要求除了顶层的启动类加载器外&#xf…...

青少年编程与数学 02-009 Django 5 Web 编程 01课题、概要

青少年编程与数学 02-009 Django 5 Web 编程 01课题、概要 一、Django 5Django 5 的主要特性包括&#xff1a; 二、MVT模式三、官方网站四、内置功能数据库 ORM&#xff08;对象关系映射&#xff09;用户认证和授权表单处理模板引擎URL 路由缓存框架国际化和本地化安全性功能管…...

为AI聊天工具添加一个知识系统 之90 详细设计之31 Derivation 之5-- 神经元变元用它衍生神经网络

本文要点 要点 Derivation 神经元变元衍生模型&#xff08; 衍生 神经网络&#xff09; 整体上说&#xff0c;它&#xff08; Derivation&#xff09;自己充当 整体无意识原型anestor的代言--作为所有神经网络的 共生环境。Derivation 初始断言了 基于最古老的 自然和逻辑树…...

Centos挂载镜像制作本地yum源,并补装图形界面

内网环境centos7.9安装图形页面内网环境制作本地yum源 上传镜像到服务器目录 创建目录并挂载镜像 #创建目录 cd /mnt/ mkdir iso#挂载 mount -o loop ./CentOS-7-x86_64-DVD-2009.iso ./iso #前面镜像所在目录&#xff0c;后面所挂载得目录#检查 [rootlocalhost mnt]# df -h…...

【Python实战练习】Python类中的方法:形式与作用详解

文章目录 Python类中的方法:形式与作用详解1. 实例方法 (Instance Method)定义与使用作用2. 类方法 (Class Method)定义与使用作用3. 静态方法 (Static Method)定义与使用作用4. 特殊方法 (Magic/Dunder Methods)常见的特殊方法定义与使用作用5. 抽象方法 (Abstract Method)定…...

Idea 2024.3 使用CodeGPT插件整合Deepseek

哈喽&#xff0c;大家好&#xff0c;我是浮云&#xff0c;最近国产大模型Deepseek异常火爆&#xff0c;作为程序员我也试着玩了一下&#xff0c;首先作为简单的使用&#xff0c;大家进入官网&#xff0c;点击开始对话即可进行简单的聊天使用&#xff0c;点击获取手机app即可安装…...

神经网络常见激活函数 1-sigmoid函数

sigmoid 1 函数求导 sigmoid函数 σ ( x ) 1 1 e ( − x ) \sigma(x) \frac{1}{1e^{(-x)}} σ(x)1e(−x)1​ sigmoid函数求导 d d x σ ( x ) d d x ( 1 1 e − x ) e − x ( 1 e − x ) 2 ( 1 e − x ) − 1 ( 1 e − x ) 2 1 1 e − x − 1 ( 1 e − x ) 2 …...

Vue(6)

一.路由板块封装 &#xff08;1&#xff09;路由的封装抽离 目标&#xff1a;将路由板块抽离出来 好处&#xff1a;拆分板块&#xff0c;利于维护 // 路由的使用步骤 5 2 // 5个基础步骤 // 1. 下载 v3.6.5 // 2. 引入 // 3. 安装注册 Vue.use(Vue插件) // 4. 创建路由对象…...

深入理解进程优先级

目录 引言 一、进程优先级基础 1.1 什么是进程优先级&#xff1f; 1.2 优先级与系统性能 二、查看进程信息 2.1 使用ps -l命令 2.2 PRI与NI的数学关系 三、深入理解Nice值 3.1 Nice值的特点 3.2 调整优先级实践 四、进程特性全景图 五、优化实践建议 结语 引言 在操…...

机器学习 - 需要了解的条件概率、高斯分布、似然函数

似然函数是连接数据与参数的桥梁&#xff0c;通过“数据反推参数”的逆向思维&#xff0c;成为统计推断的核心工具。理解它的关键在于区分“参数固定时数据的概率”与“数据固定时参数的合理性”&#xff0c;这种视角转换是掌握现代统计学和机器学习的基础。 一、在学习似然函…...

程序诗篇里的灵动笔触:指针绘就数据的梦幻蓝图<7>

大家好啊&#xff0c;我是小象٩(๑ω๑)۶ 我的博客&#xff1a;Xiao Xiangζั͡ޓއއ 很高兴见到大家&#xff0c;希望能够和大家一起交流学习&#xff0c;共同进步。 今天我们一起来学习转移表&#xff0c;回调函数&#xff0c;qsort… 目录 一、转移表1.1 定义与原理1.3…...

4、C#基于.net framework的应用开发实战编程 - 测试(四、一) - 编程手把手系列文章...

四、 测试&#xff1b; 四&#xff0e;一、调试&#xff1b; 开发完应用&#xff0c;就需要对应用进行测试&#xff0c;第一部就从调试开始&#xff0c;特别是一些重要的功能点&#xff0c;还有如果运行时有问题&#xff0c;也需要对应用进行调试&#xff0c;以找出问题。 1、 …...

逻辑回归:Sigmoid函数在分类问题中的应用

欢迎来到我的主页&#xff1a;【Echo-Nie】 本篇文章收录于专栏【机器学习】 1 什么是Sigmoid函数&#xff1f; Sigmoid函数&#xff08;Logistic函数&#xff09;是机器学习中最经典的激活函数之一&#xff0c;是一个在生物学中常见的S型函数&#xff0c;也称为S型生长曲线。…...

使用 SDKMAN! 在 Mac(包括 ARM 架构的 M1/M2 芯片)上安装 Java 8

文章目录 1. 安装 SDKMAN!2. 查找可用的 Java 8 版本3. 安装 Java 84. 验证安装5. 切换 Java 版本&#xff08;可选&#xff09;6. 解决 ARM 架构兼容性问题总结 可以使用 SDKMAN! 在 Mac&#xff08;包括 ARM 架构的 M1/M2 芯片&#xff09;上安装 Java 8。SDKMAN! 是一个强大…...

AUTOSAR汽车电子嵌入式编程精讲300篇-基于FPGA的CAN FD汽车总线数据交互系统设计

目录 前言 汽车总线以及发展趋势 汽车总线技术 汽车总线发展趋势 CAN FD总线国内外研究现状 2 系统方案及CAN FD协议分析 2.1系统控制方案设计 2.2 CAN FD总线帧结构分析 2.2.1数据帧分析 2.2.2远程帧分析 2.2.3过载帧分析 2.2.4错误帧分析 2.2.5帧间隔分析 2.3位…...