硬件实现I2C案例(寄存器实现)

一、需求分析

二、硬件电路设计

本次案例需求与前面软件模拟案例一致,这里不再赘述,不清楚可参见下面文章:软件模拟I2C案例(寄存器实现)-CSDN博客

        值得注意的是,前面是软件模拟I2C,所以并没有复用I2C模块功能,只是使用了GPIO通用功能,而这里我们将直接硬件实现I2C,因此会使用到I2C复用功能,其中用到的也是前面案例所用引脚PB10和PB11,复用I2C2模块功能

三、软件设计

       由于本次是使用寄存器方式实现硬件I2C,所以不免使用一些I2C外设相关的寄存器,对此的必要介绍可参见前面的文章硬件实现I2C常用寄存器简单介绍-CSDN博客

       本次案例在软件模拟I2C案例基础上进行修改,因此这里不再赘述工程创建于配置过程,直接进入VSCode开始编写代码。

3.1 i2c.h

       通过硬件实现I2C,则不需要我们自己模拟时序,故基本不用自定义宏定义,而是我们配置好寄存器后,底层硬件自动执行协议相关功能。

       当然,前面介绍I2C相关寄存器时谈到,一些信号在寄存器中做的只是一种设置,考虑到设置时可能相关主设备并没有占用总线或者总线正被占用等情况,设置好的信号可能会相当于延时发出,因此这个过程我们需要进行循环等待判断是否真的已经发出,同时为了避免一直等待,我们还可以设置一个超时时间timeout来避免一直等待。

因此,这里我们可以宏定义一个OK和FAIL表示信号成功发出的返回值0和未能成功发出(即超时)1。

// 宏定义
#define OK 0
#define FAIL 1

然后是一些必要的函数声明:

主要根据I2C协议相关规范得出的相关函数声明以及分析。

1、I2C的初始化 void I2C_Init(void)

      这个函数主要用于GPIO的相关配置和I2C外设的基本配置。与前面软件模拟的差别在于本次需要配置好I2C外设相关寄存器,选择好合适的模式、时钟频率以及传输速率等,并开启I2C使能。

2、设置起始信号 uint8_t I2C_Start(void)

       前面已经说到,I2C的寄存器只能设置START信号,并不会直接发出,因为需要考虑此时从机是否正在占用总线,如果总线不空闲则底层硬件无法让主机发出起始信号。所以该函数主要用于设置起始信号并等待信号真正发出,同时可借助一个超时变量来限制等待一直进行而阻塞CPU,最后直接返回一个值表示信号发送的情况OK / fail。

3、设置停止信号 void I2C_Stop(void)

       设置停止信号以后主设备32就不用再管了,因为主设备在数据总线上负责一直传输数据就行了,当需要停止时把停止信号设置好就可以不用管了,该停止就直接停止然后释放数据总线即可。

       后面需要注意的主要是进行数据传递时停止信号的设置时间,毕竟考虑到设置时可能正在传输数据或者发送地址,所以他并不是设置好就马上产生,而是等传递完一个字节后才会被发出。这里只是提一下,在读写数据时会详细介绍。

4、设置使能应答信号 void I2C_Ack(void)

       前面介绍寄存器也说过,响应的设置不代表立马就会产生,而是当一个字节数据或者设备地址发送完成后才会被发出。由于应答咱主设备自己设置好了,该发的时候发出去就完事了,这只是数据传递过程中进行的事情,所以不用像起始信号那样考虑那么多。

      也就是说,应答的设置主要是在进行数据传递时需要注意什么时候设置好,关于这个在后面实现数据收发时详细说。

5、设置使能非应答信号 void I2C_Nack(void)

与应答信号设置基本类似,这里不再赘述。

6、写入一个设备地址 uint8_t I2C_SendAddr(uint8_t addr) 

7、写入一个字节数据 uint8_t I2C_SendByte(uint8_t byte)

       根据前面介绍的I2C的控制寄存器描述,我们发现关于设备地址的写入和数据的写入完成被分别设置了标志位,也就是Addr和BTF标志位,分别用于标志设备地址写入完成和字节写入完成。而且寄存器对这俩标志位的描述是在这俩传输完成后收到应答时被置位,这就意味着我们可以在写入操作函数中把设备地址和数据写入成功后直接等待应答,然后收到应答的标志直接用Addr和BTF进行表示即可。那么既然要判断这个应答的话,所以势必也要有一个返回值来区别收到应答OK或者未收到应答FAIL。

所以我们编写的函数将传输数据和设备地址分开实现最为合适。

8、读取一个字节数据 uint8_t I2C_ReadByte(void)

       主机读取一个字节数据主要就是将从机发出在总线上的数据获取到然后返回就行了,所以该函数声明没有什么变化。

头文件参考代码如下

#ifndef __I2C_H
#define __I2C_H#include "stm32f10x.h"// 宏定义
#define OK 0
#define FAIL 1// 初始化
void I2C_Init(void);// 主设备设置起始信号
uint8_t I2C_Start(void);// 主设备设置停止信号
void I2C_Stop(void);// 主设备设置使能应答信号
void I2C_Ack(void);// 主设备设置使能非应答信号
void I2C_Nack(void);// 主机向从机写入一个设备地址(发送),并等待应答
uint8_t I2C_SendAddr(uint8_t addr);// 主机向从机写入一个字节的数据(发送),并等待应答
uint8_t I2C_SendByte(uint8_t byte);// 主机向从机读取一个字节的数据(接收)
uint8_t I2C_ReadByte(void);#endif

3.2 i2c.c

写好I2C头文件后,接下来进入其源文件实现那些函数。

1、I2C_Init()函数

       介绍I2C初始化函数声明时已经分析,这里主要配置GPIO以及I2C的基本配置。由于GPIO在本次案例中会复用I2C2模块,所以相比前面还需要多开一个I2C2外设时钟,我们知道I2C两个外设均连在APB1低速外设总线上,所以配置时需要注意。

       然后I2C的配置,根据前面寄存器介绍,我们知道I2C外设兼容了两种模式,可通过CR1中的SMBUS位进行置位选择,显然我们使用I2C模式,所以置0即可;选择I2C模式后,要确定是标准模式还是快速模式,这影响了后面时钟频率和周期的计算。一般选择标准模式对应CR1中的FS位置0即可;紧接着就可以设置输入的时钟频率了,我们直接给36MHz即可,对应CR2中的FREQ位,由于对应低位,所以直接给CR2寄存器36即可设置好FREQ了;然后要开始对输入的时钟频率进行分频操作使得数据传输速率匹配标准模式下的100kb/s,即配置CCR寄存器中的CCR,由于输入时钟频率为36,然后标准模式下高电平时钟周期时间大概占5us,所以CCR设置为5/(1/36) = 180即可,因为CCR位对应CCR寄存器低位,所以直接给寄存器180即可配好CCR了;然后还需要设置上升沿最大时间TRISE,根据前面寄存器介绍可知,设置36+1=37即可;最后再开启I2C外设使能就配置好I2C2了。

参考代码如下

// 初始化
void I2C_Init(void)
{// 1. 配置时钟 GPIO与I2C2外设时钟RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;// 2. 设置GPIO工作模式 复用开漏输出 cnf-11 mode-11GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11);// 3. 配置I2C// 3.1 选择I2C标准模式 I2C2->CR1 &= ~I2C_CR1_SMBUS;I2C2->CCR &= ~I2C_CCR_FS;// 3.2 设置输入的时钟频率I2C2->CR2 = 36;// 3.3 配置CCR,匹配传输速率 5/(1/36)I2C2->CCR = 180;// 3.4 设置上升沿最大时间 +1I2C2->TRISE = 37;// 3.5 开启I2C模块使能I2C2->CR1 |= I2C_CR1_PE;
}

2、uint8_t I2C_Start(void)函数

       设置起始信号,首先在寄存器CR1中配置START位产生一个起始信号,考虑到可能总线被其他设备占用没有空闲,所以主机不会马上发出起始信号,所以我们需要循环等待,什么时候结束等待呢?SR1寄存器中SB位描述起始信号发出后会被置1,未发出时置0,所以可以用SB位为0做循环条件,当置1时就结束循环表示起始信号被发出。为了避免CPU持续等待,我们引入一个超时时间16位值timeout限制循环作用时间,即当起始信号被发出或者超时的时候结束等待,然后返回信号发出结果(发出则返回OK的正常返回值0,未发出即超时则返回FAIL的1)。

参考代码如下

// 主设备设置起始信号
uint8_t I2C_Start(void)
{// 1. 产生一个起始信号I2C2->CR1 |= I2C_CR1_START;// 引入一个超时时间uint16_t timeout = 0xffff;// 2. 等待起始信号发出while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout){timeout --;}return timeout ? OK : FAIL;
}

2、void I2C_Stop(void)函数

        该函数很简单,直接产生一个停止信号就行即配置好CR1中的STOP位即可。由于主设备只负责向SDA线上传输接口,所以需要停止时,设置好停止信号即可,至于保证正确的时间被发出这件事,主设备并不用管,只需要传输数据时根据规定在合适的时间点设置即可。

参考代码如下

// 主设备设置接收完成之后停止信号
void I2C_Stop(void)
{// 产生一个停止信号I2C2->CR1 |= I2C_CR1_STOP;
}

3、void I2C_Ack(void)函数

4、void I2C_Nack(void)函数

       应答与非应答信号的设置也很简单,只需要设置应答与非应答即可,当一个字节数据传输完毕或者设备地址传完后主机会自动发出应答或非应答。

参考代码如下

// 主设备设置使能应答信号
void I2C_Ack(void)
{I2C2->CR1 |= I2C_CR1_ACK;
}// 主设备设置使能非应答信号
void I2C_Nack(void)
{I2C2->CR1 &= ~I2C_CR1_ACK;
}

5、uint8_t I2C_SendAddr(uint8_t addr)函数

       接下来就要开始进行传输了,首先刚发出起始信号时,还没有任何数据写入DR,所以这时候不需要判断DR是否为空,显然一定是空的。起始信号发出后我们首先会进行设备地址的写入然后供从机设备去进行比较对应地址。所以该函数中首先就是将设备地址给到DR寄存器,然后等待设备地址写入完成,收到应答后该操作才算结束,如何判断设备地址写入完成然后收到了应答呢?前面说过I2C相关的状态寄存器SR1中有一位ADDR在设备地址写入完成收到应答后就会被自动置位,然后同样再结合超时时间控制等待时间有限,即可利用while循环来等待应答。最后返回一个值表示是否收到应答(0-收到应答,1-未收到应答即非应答响应)。

       需要注意的是,每次ADDR被置位后我们需要保证之后ADDR会自动清除,不然可能导致下一次设备地址的传输结果出现错误。比如主机读取从机数据时会进行假写真读,该过程会进行两次设备地址的写入,如果第一次假写时发送设备地址完成收到应答后ADDR置位了然后没有清除,那么下一次传输的设备地址后,等待应答则会因为ADDR没有清除而直接跳过等待认为收到应答,这样的话就不能保证真的发送完成并收到反馈了。所以我们需要注意一下寄存器中对ADDR位的描述

       由上图可知,ADDR不会自动清除,需要读取SR1然后再对SR2进行读操作,才能清除该位。由于等待过程会读取SR1,所以我们最终只要在确实收到应答(timeout非0即没有等待超时)的情况下再访问一下SR2,即可清除ADDR。所以在返回是否收到应答的结果之前要加上ADDR清除操作。

参考代码如下

// 主机向从机写入一个设备地址(发送),并等待应答
uint8_t I2C_SendAddr(uint8_t addr)
{// 写入设备地址I2C2->DR = addr;// 等待应答uint16_t timeout = 0xffff;while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout){timeout --;}// 应答发出后,访问SR2,清除ADDRif (timeout > 0){I2C2->SR2;}return timeout ? OK : FAIL;
}

5、uint8_t I2C_SendByte(uint8_t byte)函数

       发了设备地址,后面就是发内部地址、数据那些了,当然我们将除了设备地址外的都看做数据的发送。那么既然前面进行了设备地址的发送,那么这里我们就要先等待数据寄存器为空呗,为保证与前面等待逻辑一致,也加个超时时间。然后数据寄存器为空了我们就可以往DR里面写数据了。接着就是等待应答,同样的等待,当然这时候等待的条件也是可读取SR1中的一位BTF来作标志,意思是当字节数据发送完成后收到应答时被置位,恰好作为等待应答的循环条件。然后被置位后,同样我们要想一下这一位怎么样被清除,所以看看寄存器中对于该位的描述

        由上图可知,BTF位,在读取SR1后,对DR进行读写操作或者传输中发送起始或者停止信号时可以被清除。首先,读取SR1已经在等待过程的条件中进行,然后由于我们BTF是在传输数据过程中被置位的,而之后的传输无非两种情况:一是继续传输数据,则在下一次用到BTF之前就会进行数据的读写操作,此时BTF会被自动清除;另一种情况是结束主从通信,这时候主设备会在传输中发出停止信号,同样BTF也会被自动清除。因此这里我们不需要手动加一道程序来清除BTF位。即等待应答之后直接返回一下应答的结果即可。

参考代码如下

// 主机向从机写入一个字节的数据(发送),并等待应答
uint8_t I2C_SendByte(uint8_t byte)
{uint16_t timeout = 0xffff;// 1. 等待数据为空   while ((I2C2->SR1 & I2C_SR1_TXE) == 0 && timeout){timeout --;}// 2. 写入数据I2C2->DR = byte;// 3. 等待应答 timeout = 0xffff;while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout){timeout --;}return timeout ? OK : FAIL;
}

6、uint8_t I2C_ReadByte(void)函数

        最后,就是读取数据。首先,我们要等待数据寄存器变满,同理加上超时时间,循环等待结束后,如果没有超时则返回读取到的数据寄存器中的数据,否则就返回FAIL就OK了。

参考代码如下

// 主机向从机读取一个字节的数据(接收)
uint8_t I2C_ReadByte(void)
{uint16_t timeout = 0xffff;// 1. 等待数据为满while ((I2C2->SR1 & I2C_SR1_RXNE) == 0 && timeout){timeout --;}return timeout ? I2C2->DR : FAIL;
}

这样,关于I2C协议的部分就实现完毕了!


接下来,就是对M24C02的读写操作进行一个代码编写。

      由于我们本次案例和前面软件模拟I2C案例的区别主要在于I2C协议使用的不同,所以主要是I2C部分的函数变化较大。而在EEPROM部分,其使用的宏定义和函数不需要做出改变,只是其中的实现因为I2C部分的变化需要稍微修改。

3.3 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.4 m24c02.c

接下来,我们就来对头文件中的函数进行一下实现。

1、void M24C02_Init(void)函数

        要使用M24C02与32进行通信,主要是需要用I2C通讯,没有其他硬件要求了,所以其初始化实际上就是初始化一下I2C部分。

参考代码如下

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

2、void M24C02_Writebyte(uint8_t innerAddr, uint8_t byte)函数

       由于本次该函数的编写只是I2C通讯由软件模拟变成硬件模块而产生了一些区别,但本质上还是调用I2C的函数接口,因此实际上我们可以借鉴之前案例中M24C02部分的函数逻辑,然后按照I2C部分的修改对实现逻辑进行修改即可。

        我们思考,按照前面对I2C的函数的实现过程,我们可以发现最大的差别在于我们写入数据被分成了发送设备地址和发送数据两部分,同时相应收到应答的过程被合并在了写入函数中。也就是说,向M24C02写入单字节数据时,过程中的等待应答我们可以不用单独调用了。

        也就是说,对于写入单字节数据的函数,一是不用单独调用等待应答的函数,二是传输设备地址函数改成发送地址的函数即可。(如果大家看不明白,可去结合M24C02读写操作时序图进行理解或者去看看前面软件模拟I2C案例对M24C02部分代码实现的介绍进行理解后再过来看)

参考代码如下

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

3、void M24C02_Writebytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)函数

       单字节的写入做好了,那么连续写入多个字节也是一样的实现,简单对应修改即可,这里不再赘述。

参考代码如下

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

4、uint8_t M24C02_Readbyte(uint8_t innerAddr)函数

       那么同理,传输设备地址时专门使用发送地址的函数、不再单独调用等待应答的函数。同时还需要注意的是,前面咱说过通过寄存器设置停止信号和应答信号后,不是马上就会发出这俩信号,其寄存器对应的描述是

        如上图,描述的是应答将在收到一个字节之后反馈应答、当前字节传输后或者当前起始条件发出后才发出停止信号。这就意味着,从机发送数据到总线上后发出的应答以及之后主机发出的停止信号应该在当前发送数据前设置好才对,换句话说,如果从机发送数据了之后才设置ACK以及STOP的话,会导致这俩信号将在这之后传输的字节接收后才产生,这样的话就对应的就是下一次传输的字节数据的应答与停止了。因此对于向M24C02读取单字节数据过程来说,我们应该在接收读取的数据之前先设置好应答与停止信号才更加合理。

       而且在参考手册中对主机接收的过程实际也有相应的描述如下图(具体可自行查看STM32参考手册)

参考代码如下

uint8_t M24C02_Readbyte(uint8_t innerAddr)
{// 1. 主机发出起始信号 I2C_Start();// 2. 主机传输设备地址(假写),从机对应I2C_SendAddr(W_ADDR);// 3. 主机传输内部地址I2C_SendByte(innerAddr);// 4. 主机再次发出起始信号 I2C_Start();// 5. 主机传输设备地址(真读),m24c02对应I2C_SendAddr(R_ADDR);// 6. 主机设置非应答I2C_Nack();// 7. 主机设置停止信号I2C_Stop();// 8. 获取m24c02读取的数据uint8_t data = I2C_ReadByte();return data;
}

5、void M24C02_Readbytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size)函数

        那么同理对应读取单字节数据函数的实现方式,基于前面案例的实现,修改三个地方即可:一是传输设备地址调用的函数改成专门的传输地址函数、二是不用再单独调用等待应答或者非应答的函数,其已经包含在写入操作的函数中、三是注意主机接受数据时设置ACK与STOP的时机。这里就不再赘述。

所以参考代码如下

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

       OK,到这里,关于M24C02部分的代码实现也就写完了,当然,这里M24C02的函数逻辑由于与前面案例是一模一样的,只是因为I2C部分的变化导致其函数实现有一点点的区别,因此本次案例对M24C02部分的函数实现是基于前面案例进行的,也就是默认大家已经熟悉M24C02读写操作过程了。不过,如果大家发现难以理解,可以先去把前面软件模拟I2C案例中关于M24C02的读写操作部分再多看看、理解一下,然后回头来理解可能就会更加明白了。


3.5 main.c测试程序

和前面的案例测试程序一模一样。直接贴上:

#include "usart.h"
#include "m24c02.h"
#include <string.h>int main(void)
{// 1. 初始化USART_Init();M24C02_Init();printf("hardware 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){		}
}

然后我们开始测试,编译然后烧录,去串口助手看看现象是否与前面测试成功的现象一样。

显然是一样的,说明本次案例成功实现完毕!


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

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

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

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

相关文章

基于SpringBoot养老院平台系统功能实现六

一、前言介绍&#xff1a; 1.1 项目摘要 随着全球人口老龄化的不断加剧&#xff0c;养老服务需求日益增长。特别是在中国&#xff0c;随着经济的快速发展和人民生活水平的提高&#xff0c;老年人口数量不断增加&#xff0c;对养老服务的质量和效率提出了更高的要求。传统的养…

matlab simulink 汽车四分之一模型轮胎带阻尼

1、内容简介 略 matlab simulink121-汽车四分之一模型轮胎带阻尼 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略

w196Spring Boot高校教师科研管理系统设计与实现

&#x1f64a;作者简介&#xff1a;多年一线开发工作经验&#xff0c;原创团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文…

数据分析:企业数字化转型的金钥匙

引言&#xff1a;数字化浪潮下的数据金矿 在数字化浪潮席卷全球的背景下&#xff0c;有研究表明&#xff0c;只有不到30%的企业能够充分利用手中掌握的数据&#xff0c;这是否让人深思&#xff1f;数据已然成为企业最为宝贵的资产之一。然而&#xff0c;企业是否真正准备好从数…

Vue 入门到实战 八

第8章 组合API与响应性 目录 8.1 响应性 8.1.1 什么是响应性 8.1.2 响应性原理 8.2 为什么使用组合API 8.3 setup组件选项 8.3.1 setup函数的参数 8.3.2 setup函数的返回值 8.3.3 使用ref创建响应式引用 8.3.4 setup内部调用生命周期钩子函数 8.4 提供/注入 8.4.1 …

Java使用aspose实现pdf转word

Java使用aspose实现pdf转word 一、下载aspose-pdf-21.6.jar包【下载地址】&#xff0c;存放目录结构如图&#xff1b;配置pom.xml。 <!--pdf to word--> <dependency><groupId>com.aspose</groupId><artifactId>aspose-pdf</artifactId>…

使用Node.js搭配express框架快速构建后端业务接口模块Demo

使用Node.js搭配express框架快速构建后端业务接口模块Demo&#xff01;实际开发中&#xff0c;有很多项目&#xff0c;其实都是可以使用node.js来完成对接mysql数据库的&#xff0c;express确实使用起来非常简单&#xff0c;入手快&#xff0c;效率非常高。下面是一个简单的案例…

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

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

Unity 加载OSGB(webgl直接加载,无需转换格式!)

Unity webgl加载倾斜摄影数据 前言效果图后续不足 前言 Unity加载倾斜摄影数据&#xff0c;有很多的插件方便好用&#xff0c;但是发布到网页端均失败&#xff0c;因为webgl 的限制&#xff0c;IO读取失效。 前不久发现一个开源项目: UnityOSGB-main 通过两种方式在 Unity 中…

【Block总结】PSA,金字塔挤压注意力,解决传统注意力机制在捕获多尺度特征时的局限性

论文信息 标题: EPSANet: An Efficient Pyramid Squeeze Attention Block on Convolutional Neural Network论文链接: arXivGitHub链接: https://github.com/murufeng/EPSANet 创新点 EPSANet提出了一种新颖的金字塔挤压注意力&#xff08;PSA&#xff09;模块&#xff0c;旨…

【重新认识C语言----结构体篇】

目录 -----------------------------------------begin------------------------------------- 引言 1. 结构体的基本概念 1.1 为什么需要结构体&#xff1f; 1.2 结构体的定义 2. 结构体变量的声明与初始化 2.1 声明结构体变量 2.2 初始化结构体变量 3. 结构体成员的访…

如何在Vscode中接入Deepseek

一、获取Deepseek APIKEY 首先&#xff0c;登录Deepseek官网的开放平台&#xff1a;DeepSeek 选择API开放平台&#xff0c;然后登录Deepseek后台。 点击左侧菜单栏“API keys”&#xff0c;并创建API key。 需要注意的是&#xff0c;生成API key复制保存到本地&#xff0c;丢失…

电脑开机提示按f1原因分析及终极解决方法来了

经常有网友问到一个问题&#xff0c;我电脑开机后提示按f1怎么解决&#xff1f;不管理是台式电脑&#xff0c;还是笔记本&#xff0c;都有可能会遇到开机需要按F1&#xff0c;才能进入系统的问题&#xff0c;引起这个问题的原因比较多&#xff0c;今天小编在这里给大家列举了比…

【高级篇 / IPv6】(7.2) ❀ 04. 在60E上配置ADSL拨号宽带上网(IPv4) ❀ FortiGate 防火墙

【简介】除了单位用户以外&#xff0c;大部分个人用户目前使用的仍然是30E、50E、60E系列防火墙&#xff0c;固件无法达到目前最高版本7.6&#xff0c;这里以最常用的60E为例&#xff0c;演示固件版本7.2下实现ADSL拨号宽带的IPv6上网。由于内容比较多&#xff0c;文章分上、下…

Qt之设置QToolBar上的按钮样式

通常给QAction设置icon后,菜单栏的菜单项和工具栏(QToolBar)上对应的按钮会同时显示该icon。工具栏还可以使用setToolButtonStyle函数设置按钮样式,其参数为枚举值: enum ToolButtonStyle {ToolButtonIconOnly,ToolButtonTextOnly,ToolButtonTextBesideIcon,ToolButtonTe…

【从零开始系列】DeepSeek-R1:(本地部署使用)思维链推理大模型,开源的神!——Windows/Linux本地环境测试 + vLLM远程部署服务

目录 一、环境配置 1.硬件设备评估 2.基础环境安装 3.模型参数下载 (1) huggingface镜像源下载 (2) modelscope魔搭社区下载 &#xff08;推荐&#xff09; 二、基础使用&#xff08;Linux、Window兼容&#xff09; 1.Transformers库自编代码 三、进阶使用&#xff08;仅Lin…

0207作业

思维导图 服务器 enum Type{TYPE_REGIST,TYPE_LOGIN };typedef struct Pack{int size;enum Type type;char buf[2048];}pack_t;typedef struct list{union Data{struct List* tail;char str[64];}data;struct List* next;struct List* prev; }List;List* create_node(){List* …

RabbitMQ 从入门到精通:从工作模式到集群部署实战(五)

#作者&#xff1a;闫乾苓 系列前几篇&#xff1a; 《RabbitMQ 从入门到精通&#xff1a;从工作模式到集群部署实战&#xff08;一&#xff09;》&#xff1a;link 《RabbitMQ 从入门到精通&#xff1a;从工作模式到集群部署实战&#xff08;二&#xff09;》&#xff1a; lin…

nodejs:express + js-mdict 网页查询英汉词典,能播放.spx 声音

向 DeepSeek R1 提问&#xff1a; 我想写一个Web 前端网页&#xff0c;后台用 nodejs js-mdict , 实现在线查询英语单词&#xff0c;并能播放.spx 声音文件 1. 项目结构 首先&#xff0c;创建一个项目目录&#xff0c;结构如下&#xff1a; mydict-app/ ├── public/ │ …

Linux ftrace 内核跟踪入门

文章目录 ftrace介绍开启ftraceftrace使用ftrace跟踪指定内核函数ftrace跟踪指定pid ftrace原理ftrace与stracetrace-cmd 工具KernelShark参考 ftrace介绍 Ftrace is an internal tracer designed to help out developers and designers of systems to find what is going on i…