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

devtools/2025/2/8 14:50:36/

一、需求分析

二、硬件电路设计

本次案例需求与前面软件模拟案例一致,这里不再赘述,不清楚可参见下面文章:软件模拟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.ppmy.cn/devtools/157109.html

相关文章

东方财富股吧发帖与评论爬虫

东方财富股吧发帖与评论爬虫 东方财富股吧爬虫 写在开头项目介绍主要功能文件介绍爬取逻辑 a. 爬取帖子信息b. 爬取评论信息 使用步骤 1. 下载代码2. MongoDB 安装3. Webdriver 安装4. 运行 main.py5. 查看数据 踩过的坑附录&#xff08;运行结果&#xff09; 东方财富股吧爬…

DeepSeek-R1 云环境搭建部署流程

DeepSeek横空出世&#xff0c;在国际AI圈备受关注&#xff0c;作为个人开发者&#xff0c;AI的应用可以有效地提高个人开发效率。除此之外&#xff0c;DeepSeek的思考过程、思考能力是开放的&#xff0c;这对我们对结果调优有很好的帮助效果。 DeepSeek是一个基于人工智能技术…

计算机组成原理——指令系统(五)

在这片广袤无垠的宇宙中&#xff0c;每一颗星辰都在诉说着自己的故事&#xff0c;每一次日出都是新的希望的开始。我们每个人都是自己命运的舵手&#xff0c;航行在未知的大海上。尽管波涛汹涌&#xff0c;风暴肆虐&#xff0c;但正是这些挑战塑造了我们的灵魂&#xff0c;让我…

反向代理模块vk

1 概念 1.1 反向代理概念 反向代理是指以代理服务器来接收客户端的请求&#xff0c;然后将请求转发给内部网络上的服务器&#xff0c;将从服务器上得到的结果返回给客户端&#xff0c;此时代理服务器对外表现为一个反向代理服务器。 对于客户端来说&#xff0c;反向代理就相当于…

调用腾讯云批量文本翻译API翻译srt字幕

上一篇文章介绍了调用百度翻译API翻译日文srt字幕的方法。百度翻译API是get方式调用&#xff0c;参数都放在ur中&#xff0c;每次调用翻译文本长度除了接口限制外&#xff0c;还有url长度限制&#xff0c;而日文字符通过ur转码后会占9个字符长度&#xff0c;其实从这个角度来讲…

.Net Core笔记知识点(跨域、缓存)

设置前端跨域配置示例&#xff1a; builder.Services.AddCors(option > {option.AddDefaultPolicy(policy > {policy.WithOrigins(originUrls).AllowAnyMethod().AllowAnyHeader().AllowCredentials();});});var app builder.Build();app.UseCors(); 【客户端缓存】接…

从零开始:OpenCV 图像处理快速入门教程

文章大纲 第1章 OpenCV 概述 1.1 OpenCV的模块与功能  1.2 OpenCV的发展 1.3 OpenCV的应用 第2章 基本数据类型 2.1 cv::Vec类 2.2 cv&#xff1a;&#xff1a;Point类 2.3 cv&#xff1a;&#xff1a;Rng类 2.4 cv&#xff1a;&#xff1a;Size类 2.5 cv&#xff1a;&…

微信小程序~django Petting pets(爱抚宠物)小程序

博主介绍&#xff1a;✌程序猿徐师兄、8年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…