I2C协议
1. 本节介绍
📝本节您将学习如何通过开发板使用I2C协议
🏆学习目标
1️⃣ 了解I2C协议的原理和注意事项
2️⃣ 通过I2C设备 SHT20温湿度传感器
,了解I2C协议的具体通信流程
2. I2C协议介绍
IIC(Inter-Integrated Circuit)协议也称为I2C总线,是一种串行通信协议,通常用于连接低速外设。它由Philips(现在的NXP Semiconductors)公司于1980年代初开发,现在已经成为一个标准。IIC总线只需要两条数据线,分别是串行数据线(SDA)和串行时钟线(SCL),这使得它成为一种非常简单的接口。它适用基于芯片的通信,例如连接传感器、存储器或数字信号处理器等。
在IIC协议中,总线上有一个主设备和多个从设备。主设备掌控着总线上的通信过程,负责发起、控制、停止通信。而从设备则需要等待主设备的请求,接收或发送数据。主设备和从设备之间的数据交换采用帧格式,每个帧通常包含地址、数据和控制信息。主设备根据从设备的地址来选中要通信的设备,从设备则根据控制信息进行相应的操作。IIC协议可以支持多个从设备连接到同一个主设备,为系统设计提供了更大的灵活性。
![]() |
---|
I2C的硬件实现
I2C总线通常使用两种电压电平,即高电平(VH)和低电平(VL)。高电平为2.5V至5.5V,低电平为0V至0.3V;这些电压电平范围是根据I2C规范确定的。I2C总线有不同的传输速率可选,包括标准模式(100 kbps)、快速模式(400 kbps)以及高速模式。传输速率的选择取决于应用的需求和设备的支持能力。为避免信号冲突,微处理器(MCU)必须只能驱动SDA和 SCL在低电平,即开漏输出。设置为开漏模式主要是为了保护器件和防止干扰。
防止干扰
:多个器件共享同一条数据线(SDA)和同一条时钟线(SCL),如果采用推挽输出模式,多个器件的输出将会叠加在数据线上,造成信号干扰,严重时会损坏器件或导致通信错误。而采用开漏输出模式,则各个器件的输出只有拉低数据线的部分,不会干扰彼此,从而提高了总线的可靠性和抗干扰能力。防止短路
:在开漏输出模式下,由于器件的输出只有拉低数据线的部分,如果两个或多个器件同时输出,也不会造成短路。而如果采用推挽输出模式,两个或多个器件同时输出时,可能会形成短路。比如主设备输出高电平,从设备输出低电平。- 因设置为开漏模式,需要连接一个外部的上拉电阻(例如:10k)将信号提拉至高电平。故I2C总线中的SDA(数据线)和SCL(时钟线)通常都连接了上拉电阻,以确保逻辑高电平的稳定性。上拉电阻的阻值通常在2.2kΩ至10kΩ之间,具体取决于总线的电容负载和通信距离。
I2C总线的最大线缆长度和传输容量受到一定限制。在标准模式下,最大线缆长度大约在1米左右,而在快速模式下,最大线缆长度约为0.3米。此外,线缆上的总线容量也会对传输速率产生影响。
![]() |
---|
I2C数据传输
I2C只有两根通信线,因此它数据传输是基于时钟信号的。各个设备在时钟信号的控制下进行数据的收发操作。下面是I2C总线的几个重要的时序:
起始信号
:SCL在高电平的状态下,SDA的电平由高转低,表示开始一次通信。
void IIC_Start(void)
{
SDA_OUT();//设置SDA为输出模式
SDA(1);
SCL(1);
delay_us(5);
SDA(0);
delay_us(5);
SCL(0);
delay_us(5);
}
2
3
4
5
6
7
8
9
10
11
停止信号
:SCL在高电平的状态下,SDA的电平由低转高,表示结束这次通信。主设备在发送停止信号后不能再向从设备发送任何数据,除非再次发送起始信号。
void IIC_Stop(void)
{
SDA_OUT();
SCL(0);
SDA(0);
SCL(1);
delay_us(5);
SDA(1);
delay_us(5);
}
2
3
4
5
6
7
8
9
10
数据传输
:主设备和从设备进行数据的传输,可以是一个或多个字节的数据,发送和接收都是基于地址选择的。
//发送一个字节
void IIC_Send_Byte(uint8_t dat)
{
int i = 0;
SDA_OUT();
SCL(0);
for( i = 0; i < 8; i++ )
{
SDA( (dat & 0x80) >> 7 );
delay_us(1);
SCL(1);
delay_us(5);
SCL(0);
delay_us(5);
dat<<=1;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//接收一个字节
unsigned char IIC_Read_Byte(void)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
SCL(0);
delay_us(5);
SCL(1);
delay_us(5);
receive<<=1;
if( SDA_GET() )
{
receive |= 1;
}
}
SCL(0);
return receive;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
I2C还提供了一种称为“ACK/NACK”(应答/非应答)的确认机制。如果一个设备接收到数据,它将通过在SDA线上拉低电平来发送一个应答信号以通知发送方数据已被接收。相反,如果数据被损坏或未接收,接收设备将发送非应答信号。(在SDA上保持高电平)。
void IIC_Send_Ack(void)
{
SDA_OUT();
SCL(0);
SDA(1);
SDA(0);
SCL(1);
delay_us(5);
SCL(0);
SDA(1);
}
2
3
4
5
6
7
8
9
10
11
void IIC_Send_Nack(void)
{
SDA_OUT();
SCL(0);
SDA(0);
SDA(1);
SCL(1);
delay_us(5);
SCL(0);
SDA(0);
}
2
3
4
5
6
7
8
9
10
11
//主机等待从机的应答信号
//函 数 返 回:1=无应答 0=有应答
uint8_t IIC_Wait_Ack(void)
{
char ack = 0;
unsigned char ack_flag = 10;
SDA_IN();
SCL(0);
delay_us(5);
SCL(1);
delay_us(5);
while( (SDA_GET()==1) && ( ack_flag ) )
{
ack_flag--;
delay_us(5);
}
if( ack_flag <= 0 )
{
IIC_Stop();
return 1;
}
else
{
SCL(0);
SDA_OUT();
}
return ack;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
在I2C总线中,时钟线由主设备控制,每个数据位在时钟边沿更新,传输的最高速率取决于总线上最慢的设备。一般来讲,I2C总线的通信速率比较慢,通常在几百kbps的范围内。如果需要更高的传输速率,可以采用其他通信协议,如SPI协议、CAN协议等。
I2C常用通信过程
I2C通信流程按照以下步骤进行:
- 主控向总线发送开始信号。
- 主控将要通信的设备地址和读写位(R/W)发送到总线上。
- 设备接收到地址后发送应答信号,主控接收到应答信号后发送数据或继续发送地址。
- 设备接收到数据后发送应答信号,主控接收到应答信号后可以继续发送数据或者停止通信。
- 主控向总线发送停止信号。
I2C基本参数
速率: I2C总线有标准模式(100 kbit/s)和快速模式(400 kbit/s)两种传输模式,还有更快的扩展模式和高速模式可供选择。
器件地址: 每个设备都有唯一的7位或10位地址,可以通过地址选择来确定与谁进行通信。
总线状态: I2C总线有五种状态,分别是空闲状态
、起始信号
、结束信号
、响应信号
、数据传输
。
数据格式: I2C总线有两种数据格式,标准格式和快速格式。标准格式是8位数据字节加上1位ack/nack(应答/非应答)位,快速格式允许两个字节同时传输。
由于SCL和SDA线是双向的,它们也可能会由于外部原因(比如线路中的电容等)出现电平误差,而从而导致通信出错。因此,在I2C总线中,通常使用上拉电阻来保证信号线在空闲状态下的电平为高电平。
3. RP2350的I2C介绍
RP2350有两个硬件I2C,I2C0和I2C1。硬件I2C通常是限制在固定的引脚上使用,具体的哪一个引脚可以使用见下方我们总结的引脚功能定义图。
![]() |
---|
RP2350的I2C特性如下:
- 支持
主机
或者从机
模式 (默认为主机模式) - 从机地址默认为
0X55
- 通信速度上支持
标准
、快速
和快速
plus 模式 - 主机模式下支持
10位
地址寻址模式 - 自带收发缓冲区 (深度各16个元素)
- 支持DMA和中断
4. 软件I2C与硬件I2C
I2C协议可以通过软件实现或者硬件实现。这两种方式的区别在于实现的方法和所需的硬件资源。
软件I2C
软件I2C是指通过在程序中编写代码来实现I2C通信协议。它利用通用输入输出(GPIO)引脚来模拟I2C的数据线(SDA)和时钟线(SCL),通过软件控制引脚的电平变化来传输数据和生成时序信号。与硬件I2C相比,软件I2C的优势在于不需要特定的硬件支持,可以在任何支持GPIO功能的微控制器上实现。它利用了微控制器的通用IO引脚来实现I2C通信协议。
软件I2C的实现通过编程方式来模拟I2C的主机和从机设备。通过逐位地读取和写入GPIO引脚的状态,并根据I2C协议的时序要求进行相应的操作,实现数据的传输和通信。软件I2C的灵活性较高,可以根据应用需求进行定制和扩展。它可以处理多个从机设备,并支持多主机环境。因此,软件I2C广泛应用于资源受限的MCU系统,特别是那些需要与多个外部设备进行通信的应用。
尽管软件I2C的性能相对于硬件I2C较低,但在一些低速通信和简单通信需求的场景下,软件I2C是一种经济实用的解决方案。
硬件I2C
硬件I2C是指通过专门的硬件模块来处理I2C通信协议。大多数现代微控制器和一些外部设备已经集成了硬件I2C模块,这些硬件模块负责处理I2C通信的细节,包括生成正确的时序信号、自动处理信号冲突、数据传输和错误检测等。可以直接使用硬件引脚连接,无需编写时序的代码。
使用硬件I2C通常相对简单,开发者无需编写复杂的代码来处理通信协议的细节。硬件模块可以直接与外部设备连接,通过专用的引脚进行数据和时钟传输,从而实现高效且可靠的通信。
在选择软件I2C还是硬件I2C时,需要考虑应用需求和硬件资源。软件I2C适用于资源受限的系统,可以在任何支持GPIO的微控制器上实现,但相对性能较低。硬件I2C通常性能更好,但需要硬件支持,并且可能占据一些特定的引脚资源。
5. I2C在Mpy的使用方式
硬件和软件 I2C
是通过 machine.I2C
和 machine.SoftI2C
实现的 。
硬件 I2C
使用系统的底层硬件外设支持来执行读/写,通常高效且快速,但对可以使用的引脚有限制。
软件 I2C
是通过软件代码 位组合
实现的,可以在任何引脚上使用,但效率不高。
这些 类
具有相同的可用方法,主要区别在于它们的构造方式。
使用machine.I2C
该类是用于 硬件I2C
的初始化。
在 MicroPython
中,使用 machine.I2C
类可以方便地操作RP2350的 硬件I2C
功能。使用时需要导入该类。
from machine import I2C
其构造函数如下:
machine.I2C(id, *, scl, sda, freq=400000)
参数说明
功能:使用以下参数构造并返回一个新的 I2C 对象。
id
标识特定的 I2C
外设。允许的值取决于为 0
和 1
。
scl
应该是一个 pin
对象,指定用于 SCL
的 pin
,I2C0的默认引脚为 scl=Pin(9)。
sda
应该是一个 pin
对象,指定用于 SDA
的 pin
,I2C0的默认引脚为 sda=Pin(8)。
freq
应该是一个整数,用于设置 SCL
的最大频率。
示例:
from machine import Pin, I2C
i2c = I2C(0) # default assignment: scl=Pin(9), sda=Pin(8)
#i2c = I2C(1, scl=Pin(3), sda=Pin(2), freq=400_000)
2
3
4
使用machine.SoftI2C
该类是用于 软件I2C
的初始化。
在 MicroPython
中,使用 machine.SoftI2C
类可以方便地操作RP2350的 软件I2C
功能。使用时需要导入该类。
from machine import SoftI2C
其构造函数如下:
machine.SoftI2C(scl, sda, *, freq=400000, timeout=255)
参数说明
功能:使用以下参数构造并返回一个新的 软件I2C 对象。
scl
应该是一个 pin
对象,指定用于 SCL
的 pin
。
sda
应该是一个 pin
对象,指定用于 SDA
的 pin
。
freq
应该是一个整数,用于设置 SCL
的最大频率。
timeout
是等待时钟延长(SCL 被总线上的另一个设备保持为低电平)的最长时间(以微秒为单位),超过该时间之后会引发 OSError(ETIMEDOUT)
异常。
示例:
from machine import Pin, SoftI2C
i2c = SoftI2C(scl=Pin(5), sda=Pin(4), freq=100_000)
2
3
方法总结
重新初始化I2C总线
I2C.init(scl, sda, *, freq=400000)
使用给定的参数重新初始化 I2C 总线:
scl
是 SCL
线的引脚对象
sda
是 SDA
线的引脚对象
freq
是 SCL
时钟频率
扫描总线上I2C设备
I2C.scan()
扫描 0x08
和 0x77
之间的所有 I2C
地址,并返回响应的列表。如果在总线上发送其地址(包括写位)后将 SDA
线拉低,则设备会做出响应。
默认会右移一位,返回的是7位地址。
发送起始信号
仅在machine.SoftI2C类上可用。
I2C.start()
在总线上产生一个 START 条件(SCL 为高时 SDA 转换为低)。
发送停止信号
仅在machine.SoftI2C类上可用。
I2C.stop()
在总线上产生一个 STOP 条件(SCL 为高时 SDA 转换为高)。
读取数据并存储
仅在machine.SoftI2C类上可用。
I2C.readinto(buf, nack=True, /)
从总线读取字节并将它们存储到buf 中。读取的字节数是buf的长度。收到除最后一个字节以外的所有字节后,将在总线上发送 ACK。
接收到最后一个字节后,如果 nack
为真,则将发送 NACK
信号,否则将发送 ACK
(在这种情况下,从设备假定将在以后的调用中读取更多字节)。
发送数据
仅在machine.SoftI2C类上可用。
I2C.write(buf)
将字节从buf写入总线。检查每个字节后是否收到 ACK,如果收到 NACK,则停止传输剩余的字节。该函数返回接收到的 ACK 数。
返回从指定地址中读取指定长度的数据
I2C.readfrom(addr, nbytes, stop=True, /)
从由 addr
指定的外设地址中读取 nbytes
长度的数据。如果 stop
为 true
,则在传输结束时生成 stop
条件。
返回一个包含所读取数据的 bytes
对象。
从指定地址读取指定长度数据保存
I2C.readfrom_into(addr, buf, stop=True, /)
从 addr
指定的外设地址中读入 buf
。读取的字节数将是 buf
的长度。如果 stop
为真,则在传输结束时生成 STOP
条件。
该方法返回None
。
将数据缓冲区中的数据发到指定地址
I2C.writeto(addr, buf, stop=True, /)
将 buf
中的字节写入 addr
地址。如果在从 buf
写入一个字节后收到 NACK
,则不会发送剩余的字节。
如果 stop
为真,则在传输结束时生成 STOP
条件,即使收到 NACK
也是如此。
该函数返回接收到的 ACK
数。
6. I2C实验介绍
以SHT20温湿度传感器
作为实验案例。通过硬件I2C的方式与其进行通信,获取周围环境的温度与湿度状况。
模块来源
模块介绍
SHT20是一种数字式温湿度传感器,它采用电容式测量技术,具有高准确度和稳定性,并采用标准的I2C数字接口进行通信。SHT20的测量范围涵盖了温度-40到+125°C和相对湿度0到100%RH。它广泛应用于空气质量监测、气象监测、恒温恒湿控制、食品贮藏等领域。
SHT20温湿度传感器的相关参数,见下图:
![]() | ![]() |
---|---|
规格参数
工作电压: 2.1~3.6V
工作电流: 0.1~1000uA
温度精度: ±0.3℃
温度范围: -40~125℃
湿度范围: 0~100 %RH
湿度精度: ±3%RH
输出方式: IIC
管脚数量: 4 Pin
实验硬件连接
SHT20 | 开发板 |
---|---|
SCL | 13 |
SDA | 12 |
GND | GND |
VCC | 3V3 |
SHT20的开发
我们要实现的是通过案例SHT20温湿度传感器读取周围环境的温湿度情况。而要进行I2C通信,我们就需要知道其器件地址,并且了解如何与其进行通信。
SHT20温湿度传感器的I2C 8位地址
为0x80
。
对于SHT20传感器,其地址的最高7位已经预设为固定值0b1000_000
(0b
代表二进制)。最低一位用于标识是读操作
还是写操作
,读操作为1,写操作为0。
因此,SHT20的I2C地址在写操作时可以表示为0b1000_0000(0x80)
,读操作时表示为0b1000_0001(0x81)
。
测量命令
SHT20有两种测量模式可以选择,分别是主机模式
与非主机模式
。
在主机模式下
,在测量的过程中,SCL线被封锁(由传感器进行控制),我们无法操控I2C总线;在非主机模式下
,当传感器在执行测量任务时,SCL线仍然保持开放状态,可进行其他通讯。非主机模式允许传感器进行测量时在总线上处理其他I2C总线通讯任务。
在这两种模式下,触发测量的命令也不相同,例如在主机模式下测量温度命令为0b1110_0011
,即0xe3
;在非主机模式下测量温度命令为0b1111_0011
,即0xf3
。
说明
本案例使用的是非主机模式
,故后续的测量温度和湿度命令分别为 0b1111_0011(0xf3)
和 0b1111_0101(0xf5)
。
通信案例
在SHT20的数据手册中,有相关的使用流程。
因为本案例是使用非主机模式
,故只说明非主机模式下的使用流程,关于主机模式的使用流程,可以查阅SHT20的数据手册。
在非主机模式下,MCU需要对传感器状态进行查询。此过程通过发送一个I2C起始信号,告诉总线开始通信。然后发送器件地址+写操作(0x80,对应图中的bit1~bit8
),等待传感器应答。如果传感器没有应答,请注意设备的实物连线情况是否正确。
待传感器应答之后发送对应的命令(对应图中的bit10~bit17
),具体命令说明见 【图 命令集说明】 。
之后紧接着是如下图【非主机模式通信流程】所示的IC首字节(0b1000_0001
)来完成。如果内部处理工作完成,单片机查询到传感器发出的确认信号后,相关数据就可以通过MCU进行读取。如果测量处理工作没有完成,传感器无确认位(ACK)输出,此时必须重新发送启动传输时序。
无论哪种传输模式,由于测量的最大分辨率为14位,第二个字节SDA上的后两位LSBs( bit43和44)用来传输相关的状态信息。两个LSBs 中的bit1表明测量的类型('0':温度;‘1':湿度)。bit0位当前没有赋值即无意义。
读取温湿度配置
根据数据手册中的通信案例,我们可以验证实现读取温湿度功能。我们以读取非主机模式的湿度为例。
- 根据 【图 非主机模式通信流程】 所示,在开始通信之前,我们需要发送
起始信号S
和器件地址+写(图中的bit1~bit8)
,发送完成之后,等待从机应答,应答之后再发送对应的湿度读取命令0xf5
。这个操作在microPython中可以直接简化为一句代码:
# 发送测量命令
i2c.writeto(0x40, bytearray([0xf5]) )
2
SHT20的I2C地址明明写的是0X80
,为什么这里发送的是0x40
?
这个是因为在MicroPython中为了只读取地址,而不读取读/写位,它会将地址右移一位。那么0X80
右移一位就是0X40
。
为什么发送命令要加上bytearray([])
?
i2c.writeto
发送的数据是数组类型
,而我们只要发送一个命令0xf5
。故将单个字节转为数组 bytearray([0xf5])
。
[0xf5] 表示数组的第一个数据并且只有一个数据,bytearray()
表示转换为字节数组。
- 重新发送起始信号与器件地址加读0X81(对应图中的bit19~bit26),告诉传感器要开始读取温度数据。传感器接收到后,就开始采集工作。如果传感器内部处理工作完成,单片机查询到传感器发出的确认信号ACK(bit27)后,相关数据就可以通过MCU进行读取。接收的数据有3位,最后一位是校验位,这里不考虑校验位。
这个在MicroPython中,也可以简化为一句代码:
# 读取测量数据 (2 字节)
data = i2c.readfrom(0x40, 2)
2
SHT20的I2C地址明明写的是0X80
,为什么这里发送的是0x40
?
这个是因为在MicroPython中为了只读取地址,而不读取读/写位,它会将地址右移一位。那么0X80
右移一位就是0X40
。
这个时候的 data
是一个数组,data[0]为数据高8位,data[1]为数据低8位。
- 将接收到的数据进行整合为16位数据,并且根据数据手册的要求,将最后两位的状态位清零并且换算位实际数据。
data = i2c.readfrom(0x40, 2)
# readfrom 返回的数据为 bytes 格式
# 将 bytes 对象转换为 bytearray
data_bytearray = bytearray(data)
# 清除 data_bytearray[1] 的最后两位
data_bytearray[1] &= ~0x03
2
3
4
5
6
7
8
数据换算。根据数据手册换算出实际的温度数据。
相对湿度的转换公式为:
温度的转换公式为:
其中的 S(T)
和 S(RH)
表示的是传感器输出的高低位整合后的16位数据。
# 计算温度或湿度值 (根据 SHT20 的数据手册)
if command == 0xf3:
temperature = ((data_bytearray[0] << 8) + data_bytearray[1]) / 2**16 * 175.72 - 46.85
return temperature
else:
humidity = ((data_bytearray[0] << 8) + data_bytearray[1]) / 2**16 *125 - 6
return humidity
2
3
4
5
6
7
将以上步骤的代码进行整合封装,得到以下函数。
"""
读取 SHT20 的数据
参数:command (int): 要读取的命令 (温度=0XF3 湿度=0XF5)
返回:读取到的数据
"""
def read_sht20_data(command):
# 发送测量命令
i2c.writeto(SHT20_ADDRESS, bytearray([command]))
# 等待测量完成
time.sleep_ms(85)
# 读取测量数据 (2 字节)
data = i2c.readfrom(SHT20_ADDRESS, 2)
# 将 bytes 对象转换为 bytearray
data_bytearray = bytearray(data)
# 清除 data_bytearray[1] 的最后两位
data_bytearray[1] &= ~0x03
# 计算温度或湿度值 (根据 SHT20 的数据手册)
if command == SHT20_MEASURE_TEMPERATURE:
temperature = ((data_bytearray[0] << 8) + data_bytearray[1]) / 2**16 * 175.72 - 46.85
return temperature
else:
humidity = ((data_bytearray[0] << 8) + data_bytearray[1]) / 2**16 *125 - 6
return humidity
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
7. 实验验证
from machine import I2C, Pin
import time
# 定义 SHT20 的 I2C 地址
SHT20_ADDRESS = 0x40
# 定义 SHT20 的命令
SHT20_MEASURE_TEMPERATURE = 0xF3
SHT20_MEASURE_HUMIDITY = 0xF5
# 创建 I2C 对象
i2c = machine.I2C(0, sda=Pin(12), scl=Pin(13))
"""
读取 SHT20 的数据
参数:command (int): 要读取的命令 (温度=0XF3 湿度=0XF5)
返回:读取到的数据
"""
def read_sht20_data(command):
# 发送测量命令
i2c.writeto(SHT20_ADDRESS, bytearray([command]))
# 等待测量完成
time.sleep_ms(85)
# 读取测量数据 (2 字节)
data = i2c.readfrom(SHT20_ADDRESS, 2)
# 将 bytes 对象转换为 bytearray
data_bytearray = bytearray(data)
# 清除 data_bytearray[1] 的最后两位
data_bytearray[1] &= ~0x03
# 计算温度或湿度值 (根据 SHT20 的数据手册)
if command == SHT20_MEASURE_TEMPERATURE:
temperature = ((data_bytearray[0] << 8) + data_bytearray[1]) / 2**16 * 175.72 - 46.85
return temperature
else:
humidity = ((data_bytearray[0] << 8) + data_bytearray[1]) / 2**16 *125 - 6
return humidity
# 扫描I2C设备
dev = i2c.scan()
print( hex( dev[0] ) )
# 读取温度和湿度
temperature = read_sht20_data(SHT20_MEASURE_TEMPERATURE)
humidity = read_sht20_data(SHT20_MEASURE_HUMIDITY)
# 打印结果
print("温度:", temperature, "°C")
print("湿度:", humidity, "%")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
效果: