STM32 软件I2C读写

news2025/1/5 23:29:49
单片机学习!

目录

前言

一、软件I2C读写代码框架

二、I2C初始化

三、六个时序基本单元

3.1 引脚操作的封装和改名   

3.2 起始条件执行逻辑

3.3 终止条件执行逻辑

3.4 发送一个字节

3.5 接收一个字节

3.5 发送应答&接收应答

3.5.1 发送应答

3.5.2 接收应答

总结


前言

        本文介绍了软件I2C读写代码,I2C协议层重点关注的是使用的两个引脚、I2C配置、时序的高低电平等协议相关的内容。


一、软件I2C读写代码框架

        代码整体框架:首先建立I2C通信层的.c和.h模块,在通信层里写好I2C底层的GPIO初始化和6个时序基本单元,也就是起始、终止、发送一个字节、接收一个字节、发送应答和接收应答。

        由于本代码使用软件I2C,所以I2C的库函数暂时不用看,软件I2C只需要用GPIO的读写函数就行了。

软件I2C初始化要做两个任务:

  • 第一个任务,把SCL和SDA都初始化为开漏输出模式;
  • 第二个任务,把SCL和SDA置高电平。

    

二、I2C初始化

        当前接线SCL是PB10,SDA是PB11。所以要开启GPIOB,PB10和PB11都要配置成开漏输出的模式。虽然开漏输出名字上带了个输出,但并不代表它只能输出,开漏输出模式仍然可以输入。输入时先输出1再直接读取输入数据寄存器就行了。这个过程在之前博文讲I2C硬件规定时介绍过。

代码示例:

void MyI2C_Init(void)
{

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启时钟
    
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Mode= GPIO_Mode_Out_OD;
    GPIO_InitStruct.GPIO_Pin= GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&GPIO_InitStruct);
    
    GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);//把GPIOB的PB10和PB11都置高电平

    }

        调用MyI2C_Init函数,PB10和PB11两个端口就被初始化为开漏输出模式,然后释放总线。SCL和SDA处于高电平,此时I2C总线处于空闲状态。

三、六个时序基本单元

3.1 引脚操作的封装和改名   

        第一个基本单元,起始条件。根据波形图,首先把SCL和SDA都确保释放。然后先拉低SDA,再拉低SCL,这样就能产生起始条件了。

        在这里可以不断地调用SetBits和ResetBit函数,来手动翻转高低电平。但是这样做会在后面的程序中出现非常多的地方来指定这个GPIO端口号。一方面,这样做语义不是很明显;另一方面,如果之后需要换一个端口那就需要改动非常多的地方。所以这时就需要在上面做个定义,把端口号统一替换一个名字,这样无论是语义还是端口的修改都会非常方便。

        给端口号换一个名字有很多方法都能实现功能,一种简单的替换方法就是宏定义,
 

#define SCL_PORT       GPIOB    
#define SCL_PIN        GPIO_Pin_10

        之后如果想释放SCL,就调用SetBits函数将GPIOB替换为SCL_PORT;将GPIO_Pin_10替换为SCL_PIN。

GPIO_SetBits(GPIOB,GPIO_Pin_10);
GPIO_SetBits(SCL_PORT,SCL_PIN);

        这样语义比较明确,而且修改引脚的时候直接在上面修改一下宏定义,下面所有引用宏定义的地方都会自动更改。但是这样宏定义的方法如果换到一个主频很高的单片机中,需要对软件时序进行延时操作的时候,也不太方便进一步修改。所以这里也可以直接一点,定义函数对操作端口的库函数进行封装,这样既容易理解,又方便加软件延时。

定义函数对操作端口的库函数进行封装代码示例:

void MyI2C_W_SCL(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
}

        MyI2C_W_SCL这个W代表写的意思,函数里调用GPIO_WriteBit函数,第三个参数给BitValue强转为BitAction类型。

        这样套一个函数替换之后,后面再调用MyI2C_W_SCL函数,参数给1或0,就可以释放或拉低SCL了。


        如果要把这个程序移植到别的单片机,就可以把这个函数里的操作替换为其他单片机对应的操作。

比如SCL是51单片机的P10口,就可以把

GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);

上面这句替换为下面这句

P10=BitValue; 

        另外如果单片机主频比较快,函数里也非常方便加一些延时,比如这里要求每次操作引脚之后都要延时10us,就可以在引脚操作之后调用延时函数进行引脚延时操作了。I2C可以慢一些,多慢都行,但是快的话还是要看一下手册里对时序时间的要求。

void MyI2C_W_SCL(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
    Delay_us(10);
}

同理封装一下SDA:

//SDA封装
void MyI2C_W_SDA(uint8_t BitValue) 
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}

        另外还要再来个读SDA的函数,因为STM32库函数中,读和写不是同一个寄存器

uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

        函数MyI2C_R_SDA中R代表读的意思,读出SDA之后也延时10us,返回读到SDA线的电平。


        有了以上三个函数的封装就实现了函数名称、端口号的替换。同时也可以很方便地修改时序的延时。当需要替换端口时,或者把这个程序移植到别的单片机中时就只需要对这前4个函数里的操作对应更改,后面的函数都调用这里封装的新名称进行操作,这样在移植的时候后面的部分就不需要再进行修改了。

        以上关于引脚操作的封装和改名就完成了。


3.2 起始条件执行逻辑

        在起始条件里需要先把SCL和SDA都释放,也就是都输出1.然后先拉低SDA,再拉低SCL。

代码示例:

void MyI2C_Start(void)
{
    MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}

        代码中需注意,最好将释放SDA的放在最前面,这样更符合起始条件的波形,这样保险一些。

        如果起始条件之前的SCL和SDA已经是高电平了,那不管先释放哪一个都是一样的效果。但是看下图中Sr这里,Start还要兼容这里的重复起始条件Sr,Sr最开始SCL是低电平,SDA电平不敢确定。所以保险起见,趁着SCL是低电平,先释放SDA;再释放SCL。这时SDA和SCL都是高电平。

        然后再拉低SDA,拉低SCL。这样Start就可以兼容起始条件和重复起始条件了。


 

3.3 终止条件执行逻辑

        当Stop开始,如果SDA和SCL都已经是低电平了,那就先释放SCL,再释放SDA就行了。但是在时序单元开始时,SDA并不一定是低电平。所以为了确保之后释放SDA能产生上升沿,要在时序单元开始时,先拉低SDA,然后再释放SCL、释放SDA。

        所以在程序里Stop的执行逻辑是:先拉低SDA,再释放SCL,再释放SDA。

void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

        终止条件后,SCL和SDA都回归到高电平。

3.4 发送一个字节

        函数的参数是要发送的一个字节。发送一个字节时序开始时,SCL是低电平。实际上除了终止条件,SCL以高电平结束,所有的单元都会保证SCL以低电平结束,这样方便各个单元的拼接。

        

        上图所示,SCL低电平,变换数据;SCL高电平,保持数据稳定。由于是高位先行,所以变换数据的时候按照先放最高位,再放次高位,最后最低位,这样的顺序依次把一个字节的每一位放在SDA线上。每放完一位后,执行释放SCL,拉低SCL的操作,驱动时钟运转。

        在程序中的操作就是,首先趁SCL低电平,先把Byte的最高位放在SDA线上,写SDA,写1还是写0取决于Byte的最高位。这里需要取出Byte的最高位,可以用 (Byte & 0x80) ,这是一个单片机中非常常见的操作。就是用按位与的方式取出数据的某一位或某几位。

按位与取出数据某一位的运行逻辑:

  • Byte可以是任意的数据   xxxx xxxx
  • 0x80就是                       1000 0000
  • Byte与0x80按位与结果  x000 0000

低7位因为和0相与,所以结果不受Byte数据的影响,始终是0;

最高位和1相与,所以结果取决于Byte的最高位。

  • 如果Byte的最高位是1,结果就是1000 0000,也就是0x80;
  • 如果Byte最高位是0,结果就是0000 0000,也就是0x00.

这就相当于把Byte的最高位取出来了。但是注意,Byte & 0x80 这个式子计算结果是0x80或0x00,而不是1或0.不过上文函数将参数BitValue强转为BitAction类型,就是非0即1,所以即使传入0x80也相当于传入了1,代码中可以直接写 (Byte & 0x80)。

        上面方法步骤将最高位数据放好后,再释放SCL,再拉低SCL,驱动时钟走一个脉冲。

	MyI2C_W_SDA(Byte & 0x80);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);

        当释放SCL之后,从机就会立刻把放好在SDA的数据读走,再拉低SCL,然后就可以放下一个数据了,下一位是次高位。

    MyI2C_W_SDA(Byte & 0x40);
    MyI2C_W_SCL(1);
    MyI2C_W_SCL(0);    

        写SDA,数据与0x40,取出次高位,再驱动SCL,来一个时钟。

        之后继续,写SDA,数据与0x20,取出再下一位,再驱动SCL,来一个时钟。

    MyI2C_W_SDA(Byte & 0x20);
    MyI2C_W_SCL(1);
    MyI2C_W_SCL(0);  

        这样来8次这个操作,就可以写入一个字节。不过可以套个for循环,循环8次减少代码量。

void MyI2C_SendByte(uint8_t Byte);
{
	uint8_t i;
	for(i=0;i<8;i++)
	{
		MyI2C_W_SDA(Byte & (0x80 >>i));
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

定义一个迭代变量i,循环八次,然后把上述重复的操作单元放在里面。

第一次循环 i=0,需要 (Byte & 0x80) 

第二次循环 i=1,需要 (Byte & 0x40) 

第三次循环 i=2,需要 (Byte & 0x20) 

...

所以这里的通式就是(Byte & (0x80 >>i)) 注意要加个括号,确保优先级。

3.5 接收一个字节

        接收一个字节时序开始时,SCL低电平,此时从机需要把数据放到SDA上,为了防止主机干扰从机写入数据,主机需要先释放SDA,释放SDA也相当于切换为输入模式。那在SCL低电平时,从机会把数据放到SDA。

  • 如果从机想发1,就释放SDA;
  • 如果从机想发0,就拉低SDA;

        然后主机释放SCL,在SCL高电平期间,读取SDA,再拉低SCL。SCL低电平期间从机就会把下一位数据放到SDA上。这样重复八次主机就能得到一个字节了。

        在这里可以发现,SCL低电平变换数据,高电平读取数据。实际上就是一种读写分离的设计:

  • 低电平时间定义为写的时间;
  • 高电平时间定义为读的时间。

        那在SCL高电平期间,如果非要动SDA来破坏读写规则的话,那这个信号就是起始条件和终止条件。SCL高电平时,SDA下降沿为起始条件,SDA上升沿为终止条件。这个设计也保证了起始条件和终止条件的特异性,能够在连续不断的波形中快速地定位起始和终止。因为起始终止与数据传输的波形有本质区别:

  • 数据传输SCL高电平不许动SDA;
  • 起始终止SCL高电平必须动SDA。

这就是这个设计的巧妙之处。

进接收一个字节的时序之后,SCL是低电平,主机释放SDA。从机把数据放到SDA时,主机释放SCL,SCL高电平时,主机就能读取数据了。

uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t Byte = 0x00;
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	if(MyI2C_R_SDA() == 1)
	{
		Byte |= 0x80;
	}
	MyI2C_W_SCL(0);
	
}

        读取数据用MyI2C_R_SDA函数,套个if,如果读SDA为1,if成立,就知道接收这一位为1了。先定义一个数据Byte,给初始值0x00.

  • 如果第一次读SDA为1,就Byte |= 0x80; 把Byte最高位置1;
  • 如果第一次读SDA为0,if条件不成立,Byte默认为0x00,就相当于写如0了。

        读取一位之后,再把SCL拉低。这时从机就会把下一位数据放到SDA上。再执行下方代码相同的流程8次就能接收一个字节了。

	MyI2C_W_SCL(1);
	if(MyI2C_R_SDA() == 1)
	{
		Byte |= 0x80;
	}
	MyI2C_W_SCL(0);

        可以用个for循环,把上方代码放进去,循环8次,依次从高位到低位进行判断。所以在写个和发送一个字节一样的移位操作。就可以接收一个字节了。最后return Byte; 把接收的Byte返回去。

3.5 发送应答&接收应答

        发送应答和接收应答其实就是发送一个字节和接收一个字节的简化版:

  • 发送一个字节是发8位,发送应答是发1位;
  • 接收一个字节是收8位,接收应答是收1位。

所以程序这里可以参照发送一个字节和接收一个字节来修改。

3.5.1 发送应答

        将发送一个字节的代码中的for循环去掉,修改一下。

void MyI2C_SendAck(uint8_t AckBit)
{
		MyI2C_W_SDA(AckBit);
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
}

        现在的逻辑是:函数进来时,SCL低电平。

  • 主机把AckBit放到SDA上。
  • SCL高电平,从机读取应答。
  • SCL低电平,进入下一个时序单元。

3.5.2 接收应答

        将接收一个字节的代码中的for循环去掉,修改一下。读SDA时,直接把读到的值,赋值给AckBit就行了。最后返回读AckBit。

uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
    MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return AckBit;
}

        现在的逻辑是:函数进来时,SCL低电平。

  • 主机释放SDA,防止干扰从机,同时从机把应答位放在SDA上。
  • SCL高电平,主机读取应答位。
  • SCL低电平,进入下一个时序单元。


代码疑问:

        1.程序里主机先把SDA置1了,然后再读取SDA,应答位就肯定是1吗?

可以从两点分析:

  • 第一,I2C的引脚都是开漏输出+弱上拉的配置,主机输出1并不是强置SDA为高电平,而是释放SDA。
  • 第二,I2C是在进行通信,主机释放了SDA,那从机如果在的话,从机是有义务把SDA再拉低的。

所以即使主机在前面把SDA置1了,之后再读取SDA,读到的值也可能是0.

  • 读到0代表从机给了应答。
  • 读到1代表从机没给应答。

        2.接收一个字节的代码里不断读取SDA,但是for循环中又没写过SDA,那SDA读出来应该始终是一个值吗?

        I2C进行通信是有从机的,当主机不断驱动SCL时钟时,从机就有义务去改变SDA的电平。所以主机每次循环读取SDA的时候,这个读取到的数据是从机控制的,这个读取到的数据也正是从机想要给我们发送的数据。这也就是这个时序叫做接收一个字节。

        通信是有时序的,有些引脚的电平之前读和之后读,读的值就是不一样的。


四、总代码示例

.c代码示例:

#include "stm32f10x.h"                  // Device header
#include "Delay.h" 


//CL封装
void MyI2C_W_SCL(uint8_t BitValue) 
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}

//SDA封装
void MyI2C_W_SDA(uint8_t BitValue) 
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}

//读SDA的函数
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

void MyI2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启时钟
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode= GPIO_Mode_Out_OD;
	GPIO_InitStruct.GPIO_Pin= GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed= GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);//把GPIOB的PB10和PB11都置高电平

	}

//六个时序基本单元
	
//起始条件	
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}
	
//终止条件
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}
	
	
//发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for(i=0;i<8;i++)
	{
		MyI2C_W_SDA(Byte & (0x80 >>i));
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}
	
//接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	MyI2C_W_SDA(1);
	for(i = 0 ; i < 8 ; i++)
	{
		MyI2C_W_SCL(1);
		if(MyI2C_R_SDA() == 1)
		{
			Byte |= (0x80 >> i);
		}
		MyI2C_W_SCL(0);
	}
	return Byte;
}

//发送应答
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
}
	
//接收应答
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
    MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return AckBit;
}

.h代码示例:

#ifndef __MYI2C_H__
#define __MYI2C_H__

void MyI2C_Init(void);

//起始条件	
void MyI2C_Start(void);
	
//终止条件
void MyI2C_Stop(void);
	
//发送一个字节
void MyI2C_SendByte(uint8_t Byte);
	
//接收一个字节
uint8_t MyI2C_ReceiveByte(void);

//发送应答
void MyI2C_SendAck(uint8_t AckBit);
	
//接收应答
uint8_t MyI2C_ReceiveAck(void);



#endif


总结

        以上就是今天要讲的内容,本文仅仅简单介绍了软件I2C读写代码。其中有使用的两个引脚、I2C配置、时序的高低电平等协议相关内容的代码配置细节。

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

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

相关文章

多光谱图像的处理和分析方法有哪些?

一、预处理方法 1、辐射校正&#xff1a; 目的&#xff1a;消除或减少传感器本身、大气条件以及太阳光照等因素对多光谱图像辐射亮度值的影响&#xff0c;使得图像的辐射值能够真实反映地物的反射或发射特性。 方法&#xff1a;包括传感器校正和大气校正。传感器校正主要是根…

服务器数据恢复—离线盘数超过热备盘数导致raidz阵列崩溃的数据恢复

服务器数据恢复环境&故障&#xff1a; 一台配有32块硬盘的服务器在运行过程中突然崩溃不可用。经过初步检测&#xff0c;基本上确定服务器硬件不存在物理故障。管理员重启服务器后问题依旧。需要恢复该服务器中的数据。 服务器数据恢复环境&#xff1a; 1、将服务器中硬盘…

Jenkins管理多版本python环境

场景&#xff1a;项目有用到python3.8和3.9&#xff0c;python环境直接安装在jenkins容器内。 1、进入jenkins容器 docker exec -it jenkins /bin/bash 2、安装前置编译环境 # 提前安装&#xff0c;以便接下来的配置操作 apt-get -y install gcc automake autoconf libtool ma…

《机器学习》从入门到实战——逻辑回归

目录 一、简介 二、逻辑回归的原理 1、线性回归部分 2、逻辑函数&#xff08;Sigmoid函数&#xff09; 3、分类决策 4、转换为概率的形式使用似然函数求解 5、对数似然函数 ​编辑 6、转换为梯度下降任务 三、逻辑回归拓展知识 1、数据标准化 &#xff08;1&#xf…

开发小工具:ping地址

开发小工具&#xff1a;ping地址 import socketdef tcp_port_scan(ip,port):#创建套接字socksocket.socket(socket.AF_INET,socket.SOCK_STREAM)#设置超时sock.settimeout(0.2)try:#发请求result sock.connect_ex((ip,port))if result 0:print(f{ip}--{port}接口连接成功)res…

41.1 预聚合提速实战项目之需求分析和架构设计

本节重点介绍 : 需求分析架构设计 需求分析 使用预聚合提速查询并且降低高基数查询对后端的压力用户无需变更grafana上的查询语句&#xff0c;后端自动替换效果图 架构设计 架构图 解决方案说明 heavy_query对用户侧表现为查询速度慢在服务端会导致资源占用过多甚至打挂…

同三维T80004ES H.265高清SDI编码器

1路SDI 1路3.5音频输入,1路SDI环出 产品简介&#xff1a; 同三维T80004ES高标清SDI音视频编码器支持1路高清或1路标清SDI音视频&#xff0c;1路3.5MM独立音频接口采集功能。编码输出双码流H.265/H.264格式&#xff0c;音频 MP3/AAC格式。编码码率可调&#xff0c;画面质量可控制…

C#高级篇 反射和属性详解【代码之美系列】

&#x1f380;&#x1f380;&#x1f380;代码之美系列目录&#x1f380;&#x1f380;&#x1f380; 一、C# 命名规则规范 二、C# 代码约定规范 三、C# 参数类型约束 四、浅析 B/S 应用程序体系结构原则 五、浅析 C# Async 和 Await 六、浅析 ASP.NET Core SignalR 双工通信 …

MySQL5.7.26-Linux-安装(2024.12)

文章目录 1.下载压缩包1.访问MySQL版本归档2.找到5.7.26并下载3.百度网盘 2.Linux安装1.卸载原来的MySQL8.0.26&#xff08;如果没有则无需在意&#xff09;1.查看所有mysql的包2.批量卸载3.删除残留文件**配置文件**&#xff08;默认路径&#xff09;&#xff1a; 4.**验证卸载…

数据分析思维(六):分析方法——相关分析方法

数据分析并非只是简单的数据分析工具三板斧——Excel、SQL、Python&#xff0c;更重要的是数据分析思维。没有数据分析思维和业务知识&#xff0c;就算拿到一堆数据&#xff0c;也不知道如何下手。 推荐书本《数据分析思维——分析方法和业务知识》&#xff0c;本文内容就是提取…

前 5 名 IPhone 解锁工具/软件

设备已禁用并且您无法访问它&#xff1f;如果您无法通过密码解锁&#xff0c;尝试 iPhone 解锁软件可能是最好的解决方案。 虽然市场上有很多免费或付费的 iPhone 解锁工具&#xff0c;但您可能不知道它们之间的区别以及如何选择最适合您的工具。 本文将介绍 5 款iPhone 解锁…

【LeetCode Hot100 回溯】组合、排列、子集、分割、N皇后、单词搜索、括号生成、电话号码的字母组合

回溯 组合问题组合总和全排列子集分割回文串N皇后电话号码的字母组合单词搜索括号生成 组合问题 给定两个整数 n 和 k&#xff0c;返回 1 … n 中所有可能的 k 个数的组合。 示例: 输入: n 4, k 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ] 树形结构&#xff1…

Docker--Docker Container(容器) 之 操作实例

容器的基本操作 容器的操作步骤其实很简单&#xff0c;根据拉取的镜像&#xff0c;进行启动&#xff0c;后可以查看容器&#xff0c;不用时停止容器&#xff0c;删除容器。 下面简单演示操作步骤 1.创建并运行容器 例如&#xff0c;创建一个名为"my-nginx"的交互…

未来网络技术的新征程:5G、物联网与边缘计算(10/10)

一、5G 网络&#xff1a;引领未来通信新潮流 &#xff08;一&#xff09;5G 网络的特点 高速率&#xff1a;5G 依托良好技术架构&#xff0c;提供更高的网络速度&#xff0c;峰值要求不低于 20Gb/s&#xff0c;下载速度最高达 10Gbps。相比 4G 网络&#xff0c;5G 的基站速度…

Python爬虫入门实例:Python7个爬虫小案例(附源码)

引言 随着互联网的快速发展&#xff0c;数据成为了新时代的石油。Python作为一种高效、易学的编程语言&#xff0c;在数据采集领域有着广泛的应用。本文将详细讲解Python爬虫的原理、常用库以及实战案例&#xff0c;帮助读者掌握爬虫技能。 一、爬虫原理 爬虫&#xff0c;又…

LeetCode - 初级算法 数组(只出现一次的数字)

只出现一次的数字 这篇文章讨论如何找到一个数组中只出现一次的数字,确保算法的时间复杂度为线性,且只使用常量额外空间。 免责声明:本文来源于个人知识与公开资料,仅用于学术交流。 描述 给定一个非空整数数组 nums,除了某个元素只出现一次以外,其余每个元素均出现两…

【视频笔记】基于PyTorch从零构建多模态(视觉)大模型 by Umar Jamil【持续更新】

视频链接: 基于PyTorch从零构建多模态(视觉)大模型 by Umar Jamil 从头编写一个视觉语言模型:PloyGamma,是谷歌的一个模型 1:原始图像 2:视觉编码器(本文是viT),通过对比学习进行训练。这个对比学习最开始是CLIP,后来被谷歌改成了SigLIP 3:线性投影层 4:如何将图…

Doris 2.1 Deleting Data 学习笔记

1 Deleting Data with DELETE Command 1.1 Delete by Specifying a Filter Predicate DELETE FROM table_name [table_alias] [PARTITION partition_name | PARTITIONS (partition_name [

基于51单片机(STC12C5A60S2)和8X8彩色点阵屏(WS2812B驱动)的小游戏《贪吃蛇》(普中开发板矩阵按键控制)

目录 系列文章目录前言一、效果展示二、原理分析三、各模块代码1、定时器02、矩阵按键3、8X8彩色点阵屏 四、主函数总结 系列文章目录 前言 《贪吃蛇》&#xff0c;一款经典的、怀旧的小游戏&#xff0c;单片机入门必写程序。 以《贪吃蛇》为载体&#xff0c;熟悉各种屏幕的使…

爆肝1个月:DDR4 的信号完整性(万字长文SI)

前言&#xff1a; 大学里面&#xff0c;总有很多课程&#xff0c;很浪费时间&#xff0c;学了没点用处&#xff0c;问过老师&#xff0c;为什么信号完整性&#xff0c;示波器使用等课程不开呢&#xff0c;这种是对工作真实有帮助的&#xff1f; 老师&#xff1a;因为老师…