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

I2C通信

前言:

IIC( Inter-Integrated Circuit,or I2C)协议是由飞利浦半导体(现在的恩智浦半导体)开发,并于1982年发布的一种串行、半双工总线,主要用于近距离,低速的芯片之间的通信;I2C总线由两根双向的信号线,一根数据线SDA(serial data)用于收发数据,一根时钟线SCL(serial clock)L用于通信双方时钟的同步;I2C总线硬件结构简单,成本较低,因此在各领域得到了广泛的应用。

IC2原理:

I2C 总线,分别由SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成。

通信原理是通过对SCL和SDA线高低电平时序的控制,来 产生I2C总线协议所需要的信号进行数据的传递。在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。

I2C主要特征如下:

  • IIC用2根信号线通信:串行数据线 SDA(数据传输)、串行时钟线 SCL(同步时钟信号);
  • IIC总线上所有器件的SDA、SCL引脚输出驱动都为 开漏(OD) 结构,通过外接上拉电阻实现总线上所有节点SDA、SCL信号的线与逻辑关系;
  • 总线上的所有设备通过软件寻址且具有唯一的地址(7位或10位)。7位“从机专用地址码”,其高4位为由生产厂家制定的设备类型地址,低3位为器件引脚定义地址(由使用者定义);10位地址不常见;
  • 任何时刻都只存在简单的主从关系,按数据传输的方向,主机可以是主发送器或主接收器;
  • 支持多主机。在总线上存在多个主机时,通过冲突检测和仲裁机制防止多个主机同时发起数据传输时存在的冲突;
  • IIC总线上所有器件都具有“自动应答”功能,保证数据传输的正确性; 主机和从机的区别在于对SCL的发送权,只有主机才能发送SCL;

I2C物理拓扑如下:

I2C 总线的稳定通信,离不开合适的上拉电阻。它需要在速度功耗驱动能力之间取得平衡。下面是一个快速选型参考:

通信模式速率典型总线电容推荐上拉电阻值 (Vcc=3.3V/5V)主要考虑因素
标准模式≤100 kbps≤400 pF4.7 kΩ 或 10 kΩ稳定性、功耗
快速模式≤400 kbps≤200 pF2.2 kΩ 或 1.5 kΩ上升时间、速度要求
高速模式≤3.4 Mbps≤100 pF≤1.5 kΩ严格的上升时间要求
低功耗设备低速较小10 kΩ 或更大最小化静态电流,延长电池寿命
多设备总线依模式而定较大适当减小阻值(如用2.2kΩ)总线电容增大,需保证上升时间

常见值起步​:

  • 3.3V 系统​:从 ​4.7kΩ​ 或 ​2.2kΩ​ 开始尝试。
  • 5V 系统​:从 ​4.7kΩ​ 或 ​10kΩ​ 开始尝试。

I2C协议层:

起始和结束:

I2C协议规定,总线上数据的传输必须以一个起始信号作为开始条件,以一个结束信号作为传输的停止条件。

起始和结束信号总是由主设备产生。

  • 总线在空闲状态 时,SCL和SDA都保持着高电平,
  • 当SCL为高而SDA由高到低的跳变,表示产生一个起始条件;
  • 当SCL为高而SDA由低到高的跳变,表示产生一个 停止条件。
  • 在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线;
  • 而在停止条件产生后,本次数据传输的主从设备将释放总线,总线再次处于空闲状态。

数据有效性:

IIC 的数据读取动作都在 SCL为高 时产生,SCL为低时是数据改变的时期,无论SDA如何变化都不影响读取。所以,传输数据的过程中,当SCL为高时,数据应当保持稳定,避免数据的采集出错。

特性描述重要性/影响
数据稳定SCL 高电平期间,SDA 上的数据必须保持稳定。确保接收方在时钟上升沿能采样到正确数据。
数据变化时机SDA 上的数据状态只能在 SCL 为低电平时改变。为下一个数据位的传输做准备,避免在数据有效期内产生干扰。
起始条件当 SCL 为高电平时,SDA 从高电平跳变到低电平。标志一次数据传输的开始,唤醒总线上的从设备。
停止条件当 SCL 为高电平时,SDA 从低电平跳变到高电平。标志一次数据传输的结束,总线进入空闲状态。
应答信号(ACK)​接收设备在传输第9个时钟周期将SDA拉低。确认已成功接收一个字节的数据。
非应答信号(NACK)​接收设备在第9个时钟周期保持SDA为高。表示未成功接收数据或希望结束传输。

高低电平表示​:在 SCL 高电平期间,SDA 为高电平表示数据 ​​'1'​,​低电平表示数据 ​​'0'​

具体的取值过程如下:

SCL低电平不取数据,作为下一位数据的电平转换间隔

SCL高电平才会取数据

响应:

数据传输必须带响应,相关的响应SCL时钟脉冲由主机产生,在响应的时钟脉冲期间,发送器释放 SDA 线(输出高阻态使SDA线被上拉电阻拉高)。在响应的时钟脉冲期间,接收器必须将 SDA 线拉低,使它在这个时钟脉冲的高电平期间保持稳定的低电平。 必须考虑建立和保持时间。

I2C 协议中的应答信号(ACK)​​ 和非应答信号(NACK)​​ 是确保数据可靠传输的关键机制。每次传输完一个字节(8位数据)后,都会紧跟一个应答位,使得实际传输一帧数据需要9个时钟脉冲

特性应答信号 (ACK)非应答信号 (NACK)
电平状态SDA 为低电平SDA 为高电平
产生时机每传输完8位数据后的第9个时钟周期每传输完8位数据后的第9个时钟周期
发送方接收数据的一方​(可能是主设备或从设备)接收数据的一方​(可能是主设备或从设备)
含义成功接收字节,请求继续传输未成功接收字节或希望终止传输
典型场景从设备地址匹配、数据正确接收从设备地址不匹配、接收方繁忙、主机读取数据时希望停止

对应的常见场景:

  • 地址应答​:主设备发送从设备地址后,地址匹配的从设备会回复 ACK,否则总线保持高电平(NACK)。
  • 数据应答​:成功接收一个数据字节后,接收方会回复 ACK。
  • 读数据终止​:主设备作为接收方从从设备读取数据时,在收到最后一个字节后,会发送 NACK 通知从设备结束发送,随后主设备产生停止信号。

​遇到 NACK 通常表示通信出现了问题,可能的原因包括:

  • 从设备地址错误​:总线上无此地址的从设备。
  • 从设备忙​:从设备可能正在进行其他操作(如内部写入),无法及时响应。
  • 硬件连接问题​:如线路接触不良、上拉电阻不合适等。
  • 协议错误​:如时序不符合规范。

寻址:

I2C 总线通过独特的寻址机制实现主设备与特定从设备间的通信。每个从设备都有唯一地址,主设备通过发送该地址来选中目标从设备进行数据交换。

寻址分为7位寻址模式和10位寻址模式,具体区别如下

特性7位寻址模式10位寻址模式
地址长度7位10位
地址空间128个 (0x00 到 0x7F)1024个 (0x000 到 0x3FF)
通信开销较低较高 (需两个字节传输地址)
兼容性广泛支持,最常用与7位寻址兼容,可在同一总线上混合使用
典型应用传感器、EEPROM、RTC等常见外设,设备数量适中的系统复杂系统、大型传感器网络等需要连接大量设备的场合
地址字节结构第一字节:高7位为地址,最低位 (LSB) 为 R/W 位第一字节:高5位为 11110,随后2位为地址高两位,LSB为 R/W 位;第二字节:地址低8位

7位寻址模式如下:

在起始条件 S 后 ,发送了一个从机地址SLAVE ADDRESS, 这个地址共有 7 位,紧接着的第 8 位是数据方向位[R/W], 0 表示写,1表示读。接下来的一个bit是应答位NACK/ACK,当这个帧中前面8bits发送完后,接收端获得SDA控制权,此时接收设备应该在第9个时钟脉冲之前回复一个ACK(将SDA拉低)以表示接收正常,如果接收设备没有将SDA拉低,则说明接收设备可能没有收到数据(如寻址的设备不存在或设备忙)或无法解析收到的消息,如果是这样,则由master来决定如何处理(stop或repeated start condition)。

10位寻址模式如下:

两个寻址代码区别:

// 假设 device_address 存储了目标从设备的地址
if (device_address <= 0x7F) {// 使用7位地址模式send_7bit_address(device_address); // 发送一个字节,包含7位地址和R/W位
} else {// 使用10位地址模式send_10bit_address_first_byte(0xF0 | ((device_address >> 8) & 0x03)); // 发送第一个字节,固定前缀11110 + 地址高2位 + R/W位(通常先设为写)send_10bit_address_second_byte(device_address & 0xFF); // 发送第二个字节,地址低8位
}

数据传输:

数据传输主要分为如下两种

  • 主机写-从机收,传输方向不变
  • 主机读-从机发,传输方向改变
传输阶段主导方关键动作/信号备注
起始条件主设备SCL为高电平时,SDA产生一个从高到低的下降沿​。标志一次传输的开始,唤醒所有从设备。
发送地址帧主设备发送7位或10位从设备地址​ + ​1位读写控制位​(0写/1读)。总线上所有从设备将自己的地址与此比较。
等待地址应答从设备地址匹配的从设备在第9个时钟周期将SDA拉低,返回ACK​。地址不匹配或总线出问题时,SDA保持高电平,即NACK​。
数据传输主/从设备每个数据字节8位,高位(MSB)在前。​每个字节后必须跟一个应答位​(ACK或NACK)。数据在SCL高电平时必须稳定,只能在SCL低电平时变化。
写操作​:主设备发送数据,从设备每接收一个字节后回复ACK。
读操作​:从设备发送数据,主设备每接收一个字节后回复ACK;主设备在收到最后一个字节后发送NACK以示读取结束。
时钟拉伸从设备从设备若未准备好,可在ACK周期后将SCL拉低,强制主设备等待,直到从设备释放SCL。一种简单的流控制机制。
停止条件主设备SCL为高电平时,SDA产生一个从低到高的上升沿​。标志一次传输的结束,总线进入空闲状态(SDA和SCL均保持高电平)

主设备向从设备写数据:

  1. 主设备发送起始条件(Start)。
  2. 主设备发送从设备地址(7位或10位) + 写控制位(0)。此处需要注意,发送数据时,无法发送7bit数据,此处发送了7bit地址+1bit读写选择位,即发送7bit+R/W。最低位为1表示读,为0表示写。
  3. 从设备返回ACK。
  4. 主设备发送数据字节。
  5. 从设备每收到一个字节,回复一个ACK。
  6. 重复步骤4-5直至数据发送完毕。
  7. 主设备发送停止条件(Stop)。

​主设备从从设备读数据:

主机需要先向从机发送一次信号,告诉从机”我要读取数据“,然后重开一次通信,等待从机主动返回数据。以示例来讲解,发送 “[1-Byte]开始字节(写) + [1-Byte]要读取的寄存器的地址”,之后结束通信,或者重开始,来进入到第二次通信中,先发送 [1-Byte]开始字节(读),然后等待读取从机发送过来的 [1-Byte]数据 即可。

  1. 主设备发送起始条件(Start)。
  2. 主设备发送从设备地址(7位或10位) + 读控制位(1)。
  3. 从设备返回ACK。
  4. 从设备发送数据字节。
  5. 主设备每收到一个字节,回复一个ACK(最后一个字节除外)。
  6. 主设备在收到最后一个字节后,回复NACK。
  7. 主设备发送停止条件(Stop)

单片机I2C通信:

这里以AHT20+BMP280 温湿度气压传感器模块为例

AHT20 模块:

首先查看AHT20传感器的芯片手册,重要的信息如下

上述可以知道设备地址为0x38,初始化命令0xBE,开始测量0xAC;软复位0xBA

对应的返回状态如下

对应的状态含义如下:

比特位(从高到低)含义
7Busy 状态​:1 表示传感器正忙,0 表示空闲。
6当前工作模式:NOR;CYC;CMD
5保留位
4保留位
3校准使能位 (Calibration Enable)​​:1 表示已校准,0 未校准。
2保留位
1保留位
0数据就绪状态​:1 表示数据可用,0 表示数据未就绪。
特性NOR 模式 (Normal Mode)CYC 模式 (Cycle Mode)CMD 模式 (Command Mode)
触发方式外部主机发送命令来触发单次测量自动周期性地进行测量和休眠通过发送特定命令进行模式切换或配置
功耗特点休眠时功耗极低​ (约 0.1μA),仅保持接口电路休眠功耗略高于 NOR 模式 (约 0.2μA),周期性唤醒测量取决于具体执行的命令和芯片状态
数据更新测量完成后更新数据,之后进入休眠,数据保持不变直至下次测量自动更新数据,用户可在任意时刻读取,但需注意数据是否为最新依据所发送的命令决定
适用场景对功耗敏感,​采样率低,或需与主机同步操作的应用需要持续自动监测,主机不想频繁发送命令的应用用于配置芯片参数切换模式等操作
工作流程命令 → 测量 → 数据更新 → 休眠 → 等待下一个命令休眠 → 自动唤醒 → 测量 → 数据更新 → 休眠接收并执行特定命令(如寄存器读写、模式切换)

代码如下:

#define ATH20_SLAVE_ADDRESS    0x38		/* I2C从机地址 *///****************************************
// 定义 AHT20 内部地址
//****************************************
#define	INIT		    0xBE	//初始化
#define	SoftReset		0xBA	//软复位
#define	StartTest		0xAC	//开始测试

这里需要注意:0x38 = 0011 1000

但是这里需要左移一位,第八位为读取还是写入字符,即如下

写:0111 0001:0x71

读:0111 0000:0x70

详细的测量流程如下

对应代码步奏如下:

第一步初始化,首先等待40ms,刚开始上电的时候开始初始化,初始化使用0xBE命令进行初始化,然后使用ATH20_Read_Cal_Enable方法判断返回值,参数是0x08和0x00,判断返回值是否有效,如果有效则进入下一步,对模式的判断要求为已经校验的NOR模式

if((val & 0x68) == 0x08)

  • 0x68 的二进制表示:​​ 0110 1000
  • 0x08 的二进制表示:​​ 0000 1000
  • 这意味着它关注(屏蔽)了第 7、6、5、3 位(从最高位 7 到最低位 0),但更具体地说,它主要检查第 6、5 和 3 位​(因为二进制中为1的那些位)。

对应的第三位模式位;第五六位为模式位,要求为已经校验的NOR模式

//读取AHT10的状态寄存器
uint8_t ATH20_Read_Status(void)
{uint8_t Byte_first;Sensors_I2C_ReadRegister(ATH20_SLAVE_ADDRESS, 0x00, 1,&Byte_first);return Byte_first;
}uint8_t ATH20_Read_Cal_Enable(void)
{//ret = 0uint8_t val = 0;val = ATH20_Read_Status();//判断NOR模式和校准输出是否有效if((val & 0x68) == 0x08)  return 1;elsereturn 0;
}uint8_t count;
uint8_t ATH20_Init(void)
{uint8_t tmp[10];SoftDelay_ms(40);tmp[0] = 0x08;tmp[1] = 0x00;//P0 口中断不使能Sensors_I2C_WriteRegister(ATH20_SLAVE_ADDRESS,INIT, 2, tmp);  SoftDelay_ms(500);count = 0;//需要等待状态字status的Bit[3]=1时才去读数据。如果Bit[3]不等于1 ,//发软件复位0xBA给AHT10,再重新初始化AHT10,直至Bit[3]=1while(ATH20_Read_Cal_Enable() == 0){Sensors_I2C_WriteRegister(ATH20_SLAVE_ADDRESS,SoftReset, 0, tmp);SoftDelay_ms(200);Sensors_I2C_WriteRegister(ATH20_SLAVE_ADDRESS,INIT, 2, tmp);count++;if(count >= 10)return 0;SoftDelay_ms(500);}return 1;
}

第二步开始请求获取对应的值,首先发送0xAC命令对应0x33和0x00数据然后等待75ms后读取数据,返回的状态主要读取Bit[7],判断是不是0,如果是1则为设备还在测量则重复读取数据,然后就是读取数据,读取数据需要参考如下官方图

安装上面的分类可以分为如下

  • Data[0]​: 状态字(包含繁忙位和校准状态等信息)
  • Data[1]​: 湿度数据高字节
  • Data[2]​: 湿度数据中字节
  • Data[3]​: 湿度数据低字节(高4位)和温度数据高字节(低4位)
  • Data[4]​: 温度数据中字节
  • Data[5]​: 温度数据低字节

所以温度是20位,湿度为20位,总共为40位,对应的uint32位的RetuData存储温湿度数据,值来源为unit8位的Data原始数据

湿度为(Data[1]​+Data[2]+Data[3]​)>> 4

温度为(Data[3]​+Data[4]+Data[5]​)& 0xfffff

最后就是对获取的值进行换算,对应的换算公式如下

对应的换算

  • 实际湿度值​ = (ct[0] * 100) / 1048576

    • 公式含义:湿度百分比 = (原始值 / 2^20) * 100%
    • 示例:ct[0] = 733473, 则 湿度 = (733473 * 100) / 1048576 ≈ 69.94%
  • 实际温度值​ = (ct[1] * 200) / 1048576 - 50

    • 公式含义:温度摄氏度 = (原始值 / 2^20) * 200 - 50
    • 示例:ct[1] = 512000, 则 温度 = (512000 * 200) / 1048576 - 50 ≈ 47.65°C

对应的代码如下

//读取AHT10的温度和湿度数据
void ATH20_Read_CTdata(uint32_t *ct) 
{uint32_t RetuData = 0;uint16_t cnt = 0;uint8_t Data[10];uint8_t tmp[10];tmp[0] = 0x33;tmp[1] = 0x00;//P0 口中断不使能Sensors_I2C_WriteRegister(ATH20_SLAVE_ADDRESS,StartTest, 2, tmp); //等待75msSoftDelay_ms(75);cnt = 0;//等待忙状态结束while(((ATH20_Read_Status()&0x80) == 0x80)){SoftDelay_ms(1);if(cnt++ >= 100){break;}}Sensors_I2C_ReadRegister(ATH20_SLAVE_ADDRESS, 0x00, 7,Data);RetuData = 0;RetuData = (RetuData|Data[1]) << 8;RetuData = (RetuData|Data[2]) << 8;RetuData = (RetuData|Data[3]);RetuData = RetuData >> 4;ct[0] = RetuData;RetuData = 0;RetuData = (RetuData|Data[3]) << 8;RetuData = (RetuData|Data[4]) << 8;RetuData = (RetuData|Data[5]);RetuData = RetuData&0xfffff;ct[1] = RetuData;
}int main(void)
{uint8_t ret = 0;float P,T,ALT;uint32_t CT_data[2];int  c1,t1;uint8_t LED_Stat = 0;ret = ATH20_Init();if(ret == 0){printf("ATH20传感器初始化错误\n");while(1);}while(1){/* 读取 ATH20 传感器数据*/while(ATH20_Read_Cal_Enable() == 0){//如果为0再使能一次ATH20_Init();SoftDelay_ms(30);}//读取温度和湿度ATH20_Read_CTdata(CT_data);  //计算得到湿度值(放大了10倍,如果c1=523,表示现在湿度为52.3%)c1 = CT_data[0] * 1000 / 1024 / 1024;  //计算得到温度值(放大了10倍,如果t1=245,表示现在温度为24.5℃)t1 = CT_data[1] * 200 *10 / 1024 / 1024 - 500;}
}

BMP280模块:

BMP280主要是通过对寄存器的操作来对气压进行读取

主要的寄存器解释如下

寄存器地址 (Hex)寄存器名称主要功能与位定义读写类型注意事项
0xD0芯片ID寄存器存储芯片标识符,​固定值为 0x58​。用于验证通信和器件型号。只读上电后读取该寄存器,若值非0x58,应检查通信线路、地址或电源。
0xE0复位寄存器向该寄存器写入 0xB6​ 可使传感器执行软复位,恢复至上电初始状态。写入其他值无效。只写复位后需等待短暂时间(通常几毫秒)让传感器准备就绪。
0xF3状态寄存器Bit3 (measuring)​​:1表示正在测量,0表示测量完成。
Bit0 (im_update)​​:1表示NVM(校准参数)正在复制,0表示复制完成。
只读读取数据前,建议检查 Bit3 是否为0,以确保数据已就绪,避免读取到旧数据或未完成测量的数据。
0xF4控制测量寄存器Bit7-5 (osrs_t)​​:温度过采样设置(0-5对应关闭/1x/2x/4x/8x/16x)。
Bit4-2 (osrs_p)​​:压力过采样设置(同上)。
Bit1-0 (mode)​​:工作模式(00=休眠,01=强制模式,11=正常模式)。
读写过采样率越高,数据精度越高,但转换时间和功耗也会增加。强制模式:单次测量后返回睡眠模式;正常模式:持续循环测量。
0xF5配置寄存器Bit7-5 (t_sb)​​:在正常模式下的待机时间(0-7对应0.5ms-4000ms)。
Bit4-2 (filter)​​:IIR滤波器系数(0-4对应关闭/2/4/8/16)。
Bit0 (spi3w_en)​​:SPI三线模式使能(I2C模式下保持0)。
读写IIR滤波器可平滑输出数据,减少短期波动。待机时间仅在正常模式下有效,决定了连续测量之间的间隔。
0xF7-0xF9压力数据寄存器存储20位原始压力测量值(ADC值)。需组合 0xF7 (MSB)0xF8 (LSB)0xF9 (XLSB)只读需按 `raw_data = (msb << 12)
0xFA-0xFC温度数据寄存器存储20位原始温度测量值(ADC值)。需组合 0xFA (MSB)0xFB (LSB)0xFC (XLSB)只读组合方式同压力数据。需使用校准参数补偿计算得到真实温度值(通常单位为℃)。
0x88-0x9F校准参数寄存器存储26个出厂校准系数(只读),用于温度和压力数据的补偿计算:
dig_T1 (uint16), dig_T2 (int16), dig_T3 (int16)​​:温度补偿参数。
dig_P1 (uint16), dig_P2-P9 (int16)​​:压力补偿参数。
只读必须在初始化时读取并保存这些参数。后续每次读取的原始ADC值都必须使用这些参数通过特定算法计算,才能得到准确的物理量。

简单起见,先介绍下BMP280 的芯片识别

BMP280 的芯片 ID 寄存器地址为 ​0xD0​。向该寄存器发起读取操作,成功通信后,BMP280 会返回一个字节的数据。这个值对于 BMP280 来说固定为 ​0x58​。因此,​只要成功读回 0x58,就表明你正在与一个 BMP280 通信,并且 I2C 通信基本正常。

#define BMP280_CHIP_ID_REG 0xD0    // 芯片ID寄存器地址
#define BMP280_EXPECTED_ID 0x58    // 期望的芯片ID值/*** @brief  检测BMP280是否存在* @param  hi2c: I2C句柄* @param  devAddr: BMP280的I2C设备地址 (0x76 or 0x77)* @retval HAL_OK: 存在且ID正确; HAL_ERROR: 失败*/
HAL_StatusTypeDef BMP280_CheckID(I2C_HandleTypeDef *hi2c, uint16_t devAddr)
{uint8_t chip_id = 0;// 尝试从芯片ID寄存器读取一个字节HAL_StatusTypeDef status = HAL_I2C_Mem_Read(hi2c, devAddr, BMP280_CHIP_ID_REG, I2C_MEMADD_SIZE_8BIT, &chip_id, 1, HAL_MAX_DELAY);if (status != HAL_OK) {return HAL_ERROR; // I2C通信失败}if (chip_id == BMP280_EXPECTED_ID) {return HAL_OK; // 芯片识别成功} else {return HAL_ERROR; // 读到的ID不是0x58}
}

完整HAL库代码:

输出float类型,首先要加入编译参数:-u_printf_float 

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -fdata-sections -ffunction-sections -u _printf_float")

AHT20.h

#include <stdint.h>#ifndef AHT20BMP280_AHT20_H
#define AHT20BMP280_AHT20_H#define ATH20_SLAVE_ADDRESS    0x38		/* I2C从机地址 *///****************************************
// 定义 AHT20 内部地址
//****************************************
#define	INIT		    0xBE	//初始化
#define	SoftReset		0xBA	//软复位
#define	StartTest		0xAC	//开始测试// 初始化
uint8_t AHT20_Init(void);
//查询cal enable位有没有使能
uint8_t AHT20_Read_Cal_Enable(void);
//读取AHT10的温度和湿度数据
uint8_t AHT20_Read_CTdata(uint32_t *ct);
// 读取AHT10的状态寄存器
uint8_t AHT20_Read_Status(void);#endif //AHT20BMP280_AHT20_H

AHT20.c

//
// Created by GalaxySpaceX on 2025/9/15.
//
#include "AHT20.h"#include "i2c.h"
#include "stm32f1xx_hal.h"
#include "usart.h"// 定义AHT20地址和命令
#define AHT10_SLAVE_ADDRESS (0x38 << 1)  // HAL库要求左移1位[1,4]  0x38 << 1 = 0x70
#define AHT10_CMD_INIT     0xE1
#define AHT10_CMD_MEASURE  0xAC
#define AHT10_CMD_SOFTRESET 0xBA
#define AHT10_CMD_READSTATUS 0x71// 读取AHT20的状态寄存器
uint8_t AHT20_Read_Status(void)
{uint8_t status_byte;uint8_t read_cmd = AHT10_CMD_READSTATUS;// 先发送读取状态命令,然后读取1字节状态数据[1,4](@ref)HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, &read_cmd, 1, HAL_MAX_DELAY);HAL_I2C_Master_Receive(&hi2c1, AHT10_SLAVE_ADDRESS, &status_byte, 1, HAL_MAX_DELAY);return status_byte;
}uint8_t AHT20_Read_Cal_Enable(void)
{uint8_t val = AHT20_Read_Status();// 判断NOR模式和校准输出是否有效[1]if((val & 0x68) == 0x08)return 1;elsereturn 0;
}uint8_t AHT20_Init(void) {uint8_t init_cmd[3] = {AHT10_CMD_INIT, 0x08, 0x00};  // 初始化命令[1,4]uint8_t reset_cmd = AHT10_CMD_SOFTRESET;  // 软复位命令uint8_t count = 0;HAL_Delay(40);// 发送初始化命令if (HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, init_cmd, 3, HAL_MAX_DELAY) != HAL_OK) {return 0;}HAL_Delay(500);while (AHT20_Read_Cal_Enable() == 0) {// 发送软复位命令HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, &reset_cmd, 1, HAL_MAX_DELAY);HAL_Delay(200);// 重新初始化HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, init_cmd, 3, HAL_MAX_DELAY);count++;// 尝试10次后失败if(count >= 10)return 0;HAL_Delay(500);}return 1;
}// 读取AHT20的温度和湿度数据
uint8_t AHT20_Read_CTdata(uint32_t *ct)
{uint8_t data[6];uint8_t measure_cmd[3] = {AHT10_CMD_MEASURE, 0x33, 0x00};  // 触发测量命令[1,4]uint16_t cnt = 0;uint32_t RetuData;// 发送触发测量命令if(HAL_I2C_Master_Transmit(&hi2c1, AHT10_SLAVE_ADDRESS, measure_cmd, 3, HAL_MAX_DELAY) != HAL_OK)return 0;// 等待75ms测量完成[1,4]HAL_Delay(80);// 等待忙状态结束while((AHT20_Read_Status() & 0x80) == 0x80){HAL_Delay(1);if(cnt++ >= 100)return 0;  // 超时退出}// 读取6字节数据[1,4]if(HAL_I2C_Master_Receive(&hi2c1, AHT10_SLAVE_ADDRESS, data, 6, HAL_MAX_DELAY) != HAL_OK)return 0;// 检查状态位(bit7=0表示忙)[4]if(data[0] & 0x80)return 0;RetuData = 0;RetuData = (RetuData|data[1]) << 8;RetuData = (RetuData|data[2]) << 8;RetuData = (RetuData|data[3]);RetuData = RetuData >> 4;ct[0] = RetuData;RetuData = 0;RetuData = (RetuData|data[3]) << 8;RetuData = (RetuData|data[4]) << 8;RetuData = (RetuData|data[5]);RetuData = RetuData&0xfffff;ct[1] = RetuData;return 1;
}

BMP280.h

//
// Created by GalaxySpaceX on 2025/9/16.
//#ifndef AHT20BMP280_BMP280_H
#define AHT20BMP280_BMP280_H#include "stdint.h"
#include "stm32f1xx_hal.h"  // 替换为HAL库头文件
#include <math.h>#define BMP280_SLAVE_ADDRESS    0x77		/* I2C´Ó»úµØÖ· *//*calibration parameters */
#define BMP280_DIG_T1_LSB_REG                0x88
#define BMP280_DIG_T1_MSB_REG                0x89
#define BMP280_DIG_T2_LSB_REG                0x8A
#define BMP280_DIG_T2_MSB_REG                0x8B
#define BMP280_DIG_T3_LSB_REG                0x8C
#define BMP280_DIG_T3_MSB_REG                0x8D
#define BMP280_DIG_P1_LSB_REG                0x8E
#define BMP280_DIG_P1_MSB_REG                0x8F
#define BMP280_DIG_P2_LSB_REG                0x90
#define BMP280_DIG_P2_MSB_REG                0x91
#define BMP280_DIG_P3_LSB_REG                0x92
#define BMP280_DIG_P3_MSB_REG                0x93
#define BMP280_DIG_P4_LSB_REG                0x94
#define BMP280_DIG_P4_MSB_REG                0x95
#define BMP280_DIG_P5_LSB_REG                0x96
#define BMP280_DIG_P5_MSB_REG                0x97
#define BMP280_DIG_P6_LSB_REG                0x98
#define BMP280_DIG_P6_MSB_REG                0x99
#define BMP280_DIG_P7_LSB_REG                0x9A
#define BMP280_DIG_P7_MSB_REG                0x9B
#define BMP280_DIG_P8_LSB_REG                0x9C
#define BMP280_DIG_P8_MSB_REG                0x9D
#define BMP280_DIG_P9_LSB_REG                0x9E
#define BMP280_DIG_P9_MSB_REG                0x9F#define BMP280_CHIPID_REG                    0xD0  /*Chip ID Register */
#define BMP280_RESET_REG                     0xE0  /*Softreset Register */
#define BMP280_STATUS_REG                    0xF3  /*Status Register */
#define BMP280_CTRLMEAS_REG                  0xF4  /*Ctrl Measure Register */
#define BMP280_CONFIG_REG                    0xF5  /*Configuration Register */
#define BMP280_PRESSURE_MSB_REG              0xF7  /*Pressure MSB Register */
#define BMP280_PRESSURE_LSB_REG              0xF8  /*Pressure LSB Register */
#define BMP280_PRESSURE_XLSB_REG             0xF9  /*Pressure XLSB Register */
#define BMP280_TEMPERATURE_MSB_REG           0xFA  /*Temperature MSB Reg */
#define BMP280_TEMPERATURE_LSB_REG           0xFB  /*Temperature LSB Reg */
#define BMP280_TEMPERATURE_XLSB_REG          0xFC  /*Temperature XLSB Reg */#define BMP280_SLEEP_MODE				(0x00)
#define BMP280_FORCED_MODE				(0x01)
#define BMP280_NORMAL_MODE				(0x03)#define BMP280_TEMPERATURE_CALIB_DIG_T1_LSB_REG             (0x88)
#define BMP280_PRESSURE_TEMPERATURE_CALIB_DATA_LENGTH       (24)
#define BMP280_DATA_FRAME_SIZE			(6)#define BMP280_OVERSAMP_SKIPPED			(0x00)
#define BMP280_OVERSAMP_1X				(0x01)
#define BMP280_OVERSAMP_2X				(0x02)
#define BMP280_OVERSAMP_4X				(0x03)
#define BMP280_OVERSAMP_8X				(0x04)
#define BMP280_OVERSAMP_16X				(0x05)/* 使用标准整数类型定义 */
typedef struct
{uint16_t dig_T1;  /* calibration T1 data */int16_t dig_T2;   /* calibration T2 data */int16_t dig_T3;   /* calibration T3 data */uint16_t dig_P1;  /* calibration P1 data */int16_t dig_P2;   /* calibration P2 data */int16_t dig_P3;   /* calibration P3 data */int16_t dig_P4;   /* calibration P4 data */int16_t dig_P5;   /* calibration P5 data */int16_t dig_P6;   /* calibration P6 data */int16_t dig_P7;   /* calibration P7 data */int16_t dig_P8;   /* calibration P8 data */int16_t dig_P9;   /* calibration P9 data */int32_t t_fine;   /* calibration t_fine data */
} bmp280Calib;uint8_t BMP280_Init(void);
void BMP280GetData(float* pressure, float* temperature, float* asl);#endif //AHT20BMP280_BMP280_H

BMP280.c

//
// Created by GalaxySpaceX on 2025/9/16.
//
#include "BMP280.h"
#include "stdint.h"
#include "i2c.h"
#include "stm32f1xx_hal.h"
#include "math.h"static int32_t  bmp280RawPressure = 0;
static int32_t  bmp280RawTemperature = 0;#define BMP280_PRESSURE_OSR			(BMP280_OVERSAMP_8X)
#define BMP280_TEMPERATURE_OSR		(BMP280_OVERSAMP_16X)
#define BMP280_MODE					(BMP280_PRESSURE_OSR << 2 | BMP280_TEMPERATURE_OSR << 5 | BMP280_NORMAL_MODE)bmp280Calib  bmp280Cal;// HAL库I2C读写函数
static HAL_StatusTypeDef BMP280_ReadRegister(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t *data, uint16_t size)
{return HAL_I2C_Mem_Read(hi2c, BMP280_SLAVE_ADDRESS << 1, reg, I2C_MEMADD_SIZE_8BIT, data, size, 100);
}static HAL_StatusTypeDef BMP280_WriteRegister(I2C_HandleTypeDef *hi2c, uint8_t reg, uint8_t *data, uint16_t size)
{return HAL_I2C_Mem_Write(hi2c, BMP280_SLAVE_ADDRESS << 1, reg, I2C_MEMADD_SIZE_8BIT, data, size, 100);
}uint8_t BMP280_Init(void)
{uint8_t bmp280_id;uint8_t tmp[10];HAL_StatusTypeDef status;// 读取芯片IDstatus = BMP280_ReadRegister(&hi2c1, BMP280_CHIPID_REG, &bmp280_id, 1);if(status != HAL_OK)return 0;// 读取校准参数status = BMP280_ReadRegister(&hi2c1, BMP280_DIG_T1_LSB_REG, (uint8_t *)&bmp280Cal, 24);if(status != HAL_OK)return 0;// 配置测量模式tmp[0] = BMP280_MODE;status = BMP280_WriteRegister(&hi2c1, BMP280_CTRLMEAS_REG, tmp, 1);if(status != HAL_OK)return 0;// 配置滤波器tmp[0] = (5 << 2);status = BMP280_WriteRegister(&hi2c1, BMP280_CONFIG_REG, tmp, 1);if(status != HAL_OK)return 0;return bmp280_id;
}static void BMP280GetPressure(void)
{uint8_t  data[BMP280_DATA_FRAME_SIZE];// 读取传感器数据if(BMP280_ReadRegister(&hi2c1, BMP280_PRESSURE_MSB_REG, data, BMP280_DATA_FRAME_SIZE) == HAL_OK){bmp280RawPressure = (int32_t)((((uint32_t)(data[0])) << 12) | (((uint32_t)(data[1])) << 4) | ((uint32_t)data[2] >> 4));bmp280RawTemperature = (int32_t)((((uint32_t)(data[3])) << 12) | (((uint32_t)(data[4])) << 4) | ((uint32_t)data[5] >> 4));}
}// Returns temperature in DegC, resolution is 0.01 DegC. Output value of "5123" equals 51.23 DegC
// t_fine carries fine temperature as global value
// 温度补偿函数
static int32_t  BMP280CompensateT(int32_t  adcT)
{int32_t  var1, var2, T;var1 = ((((adcT >> 3) - ((int32_t )bmp280Cal.dig_T1 << 1))) * ((int32_t )bmp280Cal.dig_T2)) >> 11;var2  = (((((adcT >> 4) - ((int32_t )bmp280Cal.dig_T1)) * ((adcT >> 4) - ((int32_t )bmp280Cal.dig_T1))) >> 12) * ((int32_t )bmp280Cal.dig_T3)) >> 14;bmp280Cal.t_fine = var1 + var2;T = (bmp280Cal.t_fine * 5 + 128) >> 8;return T;
}// Returns pressure in Pa as unsigned 32 bit integer in Q24.8 format (24 integer bits and 8 fractional bits).
// Output value of "24674867" represents 24674867/256 = 96386.2 Pa = 963.862 hPa
static uint32_t BMP280CompensateP(int32_t adcP)
{int64_t var1, var2, p;var1 = ((int64_t)bmp280Cal.t_fine) - 128000;var2 = var1 * var1 * (int64_t)bmp280Cal.dig_P6;var2 = var2 + ((var1*(int64_t)bmp280Cal.dig_P5) << 17);var2 = var2 + (((int64_t)bmp280Cal.dig_P4) << 35);var1 = ((var1 * var1 * (int64_t)bmp280Cal.dig_P3) >> 8) + ((var1 * (int64_t)bmp280Cal.dig_P2) << 12);var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)bmp280Cal.dig_P1) >> 33;if (var1 == 0)return 0;p = 1048576 - adcP;p = (((p << 31) - var2) * 3125) / var1;var1 = (((int64_t)bmp280Cal.dig_P9) * (p >> 13) * (p >> 13)) >> 25;var2 = (((int64_t)bmp280Cal.dig_P8) * p) >> 19;p = ((p + var1 + var2) >> 8) + (((int64_t)bmp280Cal.dig_P7) << 4);return (uint32_t)p;
}#define CONST_PF 0.1902630958	//(1/5.25588f) Pressure factor
#define FIX_TEMP 25				// Fixed Temperature. ASL is a function of pressure and temperature, but as the temperature changes so much (blow a little towards the flie and watch it drop 5 degrees) it corrupts the ASL estimates.// TLDR: Adjusting for temp changes does more harm than good.
/*** Converts pressure to altitude above sea level (ASL) in meters*/
// 压力转海拔函数
static float BMP280PressureToAltitude(float* pressure/*, float* groundPressure, float* groundTemp*/)
{if (*pressure > 0){return ((pow((1015.7f / *pressure), CONST_PF) - 1.0f) * (FIX_TEMP + 273.15f)) / 0.0065f;}else{return 0;}
}#define FILTER_NUM	5
#define FILTER_A	0.1f/*限幅平均滤波法*/
static void presssureFilter(float* in, float* out)
{static uint8_t  i = 0;static float filter_buf[FILTER_NUM] = {0.0};double filter_sum = 0.0;uint8_t  cnt = 0;float deta;if(filter_buf[i] == 0.0f){filter_buf[i] = *in;*out = *in;if(++i >= FILTER_NUM)i=0;}else{if(i)deta = *in-filter_buf[i - 1];elsedeta = *in-filter_buf[FILTER_NUM - 1];if(fabs(deta) < FILTER_A){filter_buf[i] = *in;if(++i >= FILTER_NUM)i = 0;}for(cnt = 0; cnt < FILTER_NUM; cnt++){filter_sum += filter_buf[cnt];}*out = filter_sum / FILTER_NUM;}
}void BMP280GetData(float* pressure, float* temperature, float* asl)
{static float t;static float p;BMP280GetPressure();t = BMP280CompensateT(bmp280RawTemperature) / 100.0;p = BMP280CompensateP(bmp280RawPressure) / 25600.0;presssureFilter(&p, pressure);*temperature = (float)t;/*单位度*/*asl = BMP280PressureToAltitude(pressure);	/*转换成海拔*/
}

main.c

/* USER CODE BEGIN Header */
/********************************************************************************* @file           : main.c* @brief          : Main program body******************************************************************************* @attention** Copyright (c) 2025 STMicroelectronics.* All rights reserved.** This software is licensed under terms that can be found in the LICENSE file* in the root directory of this software component.* If no LICENSE file comes with this software, it is provided AS-IS.********************************************************************************/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"
#include "AHT20.h"
#include "BMP280.h"
#include "stdio.h"
#include "string.h"
#include "stdlib.h"/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes *//* USER CODE END Includes *//* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD *//* USER CODE END PTD *//* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD *//* USER CODE END PD *//* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM *//* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV *//* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP *//* USER CODE END PFP *//* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 *//* USER CODE END 0 *//*** @brief  The application entry point.* @retval int*/
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_I2C1_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */uint8_t ret = 0;int humidity, temperature;char txData[100];uint32_t CT_data[2];int  c1,t1;float P,T,ALT;int temp_int = (int)(T * 100);  // 温度放大100倍int press_int = (int)(P * 100); // 压力放大100倍int alt_int = (int)ALT;         // 海拔取整HAL_Delay(5000);HAL_UART_Transmit(&huart1, "start\r\n", strlen("start\r\n"), HAL_MAX_DELAY);ret = AHT20_Init();if(ret == 0){HAL_UART_Transmit(&huart1, "AHT20 Init Error\r\n", strlen("AHT20 Init Error\r\n"), HAL_MAX_DELAY);HAL_Delay(5000);while(1);}ret = BMP280_Init();if(ret != 0x58){HAL_UART_Transmit(&huart1, "BMP280 Init Error\r\n", strlen("BMP280 Init Error\r\n"), HAL_MAX_DELAY);HAL_Delay(5000);while(1);}/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */// 读取AHT20传感器数据if(AHT20_Read_Cal_Enable() == 0){HAL_UART_Transmit(&huart1, "AHT20_Init\r\n", strlen("AHT20_Init\r\n"), HAL_MAX_DELAY);AHT20_Init();  // 如果校准未使能,重新初始化HAL_Delay(30);}HAL_UART_Transmit(&huart1, "AHT20_Read_CTdata\r\n", strlen("AHT20_Read_CTdata\r\n"), HAL_MAX_DELAY);// 读取温湿度数据if(AHT20_Read_CTdata(CT_data) == 1){c1 = CT_data[0] * 1000 / 1024 / 1024;  //计算得到湿度值(放大了10倍,如果c1=523,表示现在湿度为52.3%)t1 = CT_data[1] * 200 *10 / 1024 / 1024 - 500;//计算得到温度值(放大了10倍,如果t1=245,表示现在温度为24.5℃)//printf("湿度: %.2f%%, 温度: %.2f℃\r\n", humidity, temperature);int length = sprintf(txData, "temperature: %d.%d C, humidity: %d.%d %%\r\n", (t1/10), (t1%10), (c1/10), (c1%10));HAL_UART_Transmit(&huart1, (uint8_t*)txData, length, HAL_MAX_DELAY);}else{//printf("读取传感器数据失败\r\n");HAL_UART_Transmit(&huart1, "AHT20 Read Error\r\n", strlen("AHT20 Read Error\r\n"), HAL_MAX_DELAY);}/* 读取 BMP280 传感器数据*/BMP280GetData(&P, &T, &ALT);// 输出测量结果char buffer[100];int len = snprintf(buffer, sizeof(buffer),"Temp: %.2f C, Press: %.2f hPa, Alt: %.2f m\r\n",T, P, ALT);// int len = snprintf(buffer, sizeof(buffer),//               "Temp: %d.%02d C, Press: %d.%02d hPa, Alt: %d m\r\n",//               temp_int / 100, abs(temp_int % 100),//               press_int / 100, abs(press_int % 100),//               alt_int);HAL_UART_Transmit(&huart1, (uint8_t*)buffer, len, HAL_MAX_DELAY);HAL_Delay(10000);  // 每秒读取一次/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}/*** @brief System Clock Configuration* @retval None*/
void SystemClock_Config(void)
{RCC_OscInitTypeDef RCC_OscInitStruct = {0};RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};/** Initializes the RCC Oscillators according to the specified parameters* in the RCC_OscInitTypeDef structure.*/RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;RCC_OscInitStruct.HSIState = RCC_HSI_ON;RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK){Error_Handler();}/** Initializes the CPU, AHB and APB buses clocks*/RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK){Error_Handler();}
}/* USER CODE BEGIN 4 *//* USER CODE END 4 *//*** @brief  This function is executed in case of error occurrence.* @retval None*/
void Error_Handler(void)
{/* USER CODE BEGIN Error_Handler_Debug *//* User can add his own implementation to report the HAL error return state */__disable_irq();while (1){}/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/*** @brief  Reports the name of the source file and the source line number*         where the assert_param error has occurred.* @param  file: pointer to the source file name* @param  line: assert_param error line source number* @retval None*/
void assert_failed(uint8_t *file, uint32_t line)
{/* USER CODE BEGIN 6 *//* User can add his own implementation to report the file name and line number,ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) *//* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */


文章转载自:

http://XB2Eo6s1.knfgn.cn
http://sM84FSV1.knfgn.cn
http://qDidEY0g.knfgn.cn
http://XS7NktJE.knfgn.cn
http://oKmJy1JU.knfgn.cn
http://Dk8Tsh97.knfgn.cn
http://DV2cssnf.knfgn.cn
http://vnpTY6tG.knfgn.cn
http://zQtK7xVU.knfgn.cn
http://47SEbTPG.knfgn.cn
http://BmSjmY27.knfgn.cn
http://hqZFxhxg.knfgn.cn
http://AHDDjh5f.knfgn.cn
http://V8fx3WOd.knfgn.cn
http://et5plWt9.knfgn.cn
http://Sk0gwDuo.knfgn.cn
http://2wlNYoW8.knfgn.cn
http://M2qP15Nb.knfgn.cn
http://Ook8FVRo.knfgn.cn
http://1FkGOaf7.knfgn.cn
http://fKuqKtC1.knfgn.cn
http://8qJCV4ly.knfgn.cn
http://B8u1Am5H.knfgn.cn
http://vAJKLx6l.knfgn.cn
http://PI8pNhOV.knfgn.cn
http://UwJFBlrN.knfgn.cn
http://8F6QgoZO.knfgn.cn
http://h6Mi7oYn.knfgn.cn
http://PEnSbIFq.knfgn.cn
http://5HgHMN7U.knfgn.cn
http://www.dtcms.com/a/386693.html

相关文章:

  • 经典算法题之x 的平方根
  • 【精品资料鉴赏】RPA财务机器人应用(基于UiPath)教材配套课件
  • 融合A*与蚁群算法的室内送餐机器人多目标路径规划方法研究
  • RustDesk:免费开源的跨平台远程桌面控制软件
  • 超越NAT:如何构建高效、安全的内网穿透隧道
  • RabbitMQ理解
  • 【闪电科创】边缘计算深度学习辅导
  • Linux服务器中Mysql定时备份(清理)数据库
  • 物联网智能网关配置教程:实现注塑机数据经基恩士PLC上传至云平台
  • 搭建第一个Spring Boot项目
  • MyBatis 注解操作
  • InternVL3.5 开源:革新多模态架构,重塑感知与推理的边界​
  • 新手教程—LabelImg标注工具使用与YOLO格式转换及数据集划分教程
  • C++奇异递归模板模式(CRTP)
  • 国产数据库地区分布,北京一骑绝尘
  • 超表面赋能结构光三维重建 | 实现超大视场高精度实时重建
  • 在Oracle\PG\GaussDB库中实现用户甲在其它用户的SCHEMA中创建表的方法及所属属主的差异
  • TDengine IDMP 基本功能——数据可视化
  • SpringMVC静态资源与Servlet容器指南
  • 安卓实现miniLzo压缩算法
  • [deepseek]LNK2001错误即单独编译汇编并链接
  • Interview X,新一代面试工具
  • Oracle sql tuning guide 翻译 Part 6 --- 优化器控制
  • Git 原理与使用
  • 什么是向量数据库
  • 利用postgres_proto和pgproto测试postgres协议访问duckdb
  • 拼多多-----anti_content逆向分析
  • 【一文了解】Unity的协程(Coroutine)与线程(Thread)
  • 贪心算法在网络入侵检测(NID)中的应用
  • 数据搬家后如何处理旧 iPhone