二十四、 IIC实验
以SHT20温湿度传感器作为实验案例。通过软件IIC的方式与其进行通信,获取周围环境的温度与湿度状况。
1. 实验介绍
SHT20是一种数字式温湿度传感器,它采用电容式测量技术,具有高准确度和稳定性,并采用标准的I2C数字接口进行通信。SHT20的测量范围涵盖了温度-40到+125°C和相对湿度0到100%RH。它广泛应用于空气质量监测、气象监测、恒温恒湿控制、食品贮藏等领域。
SHT20温湿度传感器的相关参数,见下图:
1.1 硬件连接
接线图:
2. 软件I2C配置
一般使用软件I2C功能,都需要有以下几个步骤。
- 配置引脚
- 配置I2C时序
- 确定通信步骤
2.1 配置引脚
在使用软件实现I2C通信时,需要选择合适的引脚来作为数据线(SDA)和时钟线(SCL)。通常情况下,可以选择任何可编程的通用输入输出(GPIO)引脚作为软件I2C的引脚。对于软件I2C,需要至少两个引脚用于数据线(SDA)和时钟线(SCL),并确保这些引脚能够满足I2C通信协议的时序要求。以下是一般的引脚说明:
- 数据线(SDA):用于传输数据的引脚。在软件I2C中,需要将该引脚设置为输出模式(用于主设备发送数据)与输入模式(用于主设备接收数据)。在通信期间,需要通过控制数据线的电平变化来实现数据的传输。
- 时钟线(SCL):用于控制数据传输的时钟信号的引脚。在软件I2C中,需要将该引脚设置为输出模式,通过控制时钟线的电平变化来生成时钟脉冲,以控制数据线的传输。
需要注意的是,选取合适的引脚时要考虑以下几个方面:
- 支持输入/输出配置:引脚需要支持在软件中配置为输入或输出模式,并能够通过程序动态地进行切换。
- 硬件限制和冲突:确保选取的引脚没有被分配给其他硬件功能或外设,以避免冲突。
- 电气特性:引脚的电气特性应满足I2C总线的标准要求,例如正确的电平和驱动能力。
需要注意的是,软件I2C的实现需要更多的程序代码和计算,相对于硬件I2C,软件I2C在处理器效能和时序控制方面更加敏感。因此,在选择引脚时,还需要考虑处理器的性能和可编程性能。
为了保证代码的可维护性和可移植性,这里将相关的功能进行的宏定义。
SDA引脚和SCL引脚的宏定义如下:
/*
* 立创开发板软硬件资料与相关扩展板软硬件资料官网全部开源
* 开发板官网:www.lckfb.com
* 技术支持常驻论坛,任何技术问题欢迎随时交流学习
* 立创论坛:https://oshwhub.com/forum
* 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
* 不靠卖板赚钱,以培养中国工程师为己任
* Change Logs:
* Date Author Notes
* 2024-04-11 LCKFB-LP first version
*/
#ifndef __BSP_SHT20_H__
#define __BSP_SHT20_H__
#include "hc32_ll.h"
#define PORT_SCL GPIO_PORT_B
#define GPIO_SCL GPIO_PIN_06
#define PORT_SDA GPIO_PORT_B
#define GPIO_SDA GPIO_PIN_07
#define SDA_IN() {\
stc_gpio_init_t stcGpioInit;\
(void)GPIO_StructInit(&stcGpioInit);\
stcGpioInit.u16PinState = PIN_STAT_SET;\
stcGpioInit.u16PinDir = PIN_DIR_IN;\
stcGpioInit.u16PinDrv = PIN_HIGH_DRV;\
stcGpioInit.u16PinAttr = PIN_ATTR_DIGITAL;\
stcGpioInit.u16Latch = PIN_LATCH_OFF;\
stcGpioInit.u16PullUp = PIN_PU_ON;\
stcGpioInit.u16Invert = PIN_INVT_OFF;\
stcGpioInit.u16ExtInt = PIN_EXTINT_OFF;\
stcGpioInit.u16PinOutputType = PIN_OUT_TYPE_NMOS;\
stcGpioInit.u16PinInputType = PIN_IN_TYPE_SMT;\
(void)GPIO_Init(PORT_SDA, GPIO_SDA, &stcGpioInit);\
}
#define SDA_OUT() {\
stc_gpio_init_t stcGpioInit;\
(void)GPIO_StructInit(&stcGpioInit);\
stcGpioInit.u16PinState = PIN_STAT_SET;\
stcGpioInit.u16PinDir = PIN_DIR_OUT;\
stcGpioInit.u16PinDrv = PIN_HIGH_DRV;\
stcGpioInit.u16PinAttr = PIN_ATTR_DIGITAL;\
stcGpioInit.u16Latch = PIN_LATCH_OFF;\
stcGpioInit.u16PullUp = PIN_PU_ON;\
stcGpioInit.u16Invert = PIN_INVT_OFF;\
stcGpioInit.u16ExtInt = PIN_EXTINT_OFF;\
stcGpioInit.u16PinOutputType = PIN_OUT_TYPE_NMOS;\
stcGpioInit.u16PinInputType = PIN_IN_TYPE_SMT;\
(void)GPIO_Init(PORT_SDA, GPIO_SDA, &stcGpioInit);\
}
#define SCL(BIT) ( BIT ? GPIO_SetPins(PORT_SCL,GPIO_SCL) : GPIO_ResetPins(PORT_SCL,GPIO_SCL) )
#define SDA(BIT) ( BIT ? GPIO_SetPins(PORT_SDA,GPIO_SDA) : GPIO_ResetPins(PORT_SDA,GPIO_SDA) )
#define SDA_GET() GPIO_ReadInputPins(PORT_SDA, GPIO_SDA)
void SHT20_GPIO_INIT(void);
float SHT20_Read(unsigned char regaddr);
#endif
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
54
55
56
57
58
59
60
61
要操作 GPIO 引脚,必不可少的就是开启时钟、配置模式、配置输出、设置功能等,还是这一系列的操作。
引脚初始化配置如下:
void SHT20_GPIO_INIT(void)
{
stc_gpio_init_t stcGpioInit; // 定义GPIO结构体
// 关闭寄存器写保护
LL_PERIPH_WE(LL_PERIPH_ALL);
(void)GPIO_StructInit(&stcGpioInit);
stcGpioInit.u16PinState = PIN_STAT_SET; // 状态选择高电平
stcGpioInit.u16PinDir = PIN_DIR_OUT; // 输出模式
stcGpioInit.u16PinDrv = PIN_HIGH_DRV; // 选择高驱动力
stcGpioInit.u16PinAttr = PIN_ATTR_DIGITAL; // 数字模式
stcGpioInit.u16Latch = PIN_LATCH_OFF; // 锁存器关闭
stcGpioInit.u16PullUp = PIN_PU_ON; // 上拉开启
stcGpioInit.u16Invert = PIN_INVT_OFF; // 高低电平反转关闭
stcGpioInit.u16ExtInt = PIN_EXTINT_OFF; // 中断关闭
stcGpioInit.u16PinOutputType = PIN_OUT_TYPE_NMOS; // 开漏输出
stcGpioInit.u16PinInputType = PIN_IN_TYPE_SMT;
// 初始化SCL引脚
(void)GPIO_Init(GPIO_PORT_B, GPIO_PIN_06, &stcGpioInit);
// 初始化SDA引脚
(void)GPIO_Init(GPIO_PORT_B, GPIO_PIN_07, &stcGpioInit);
}
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
2.2 配置I2C时序
关于I2C的时序部分,已在章节【IIC数据传输】进行说明,这里不在进行叙述。
/********************************\*\*********************************
- 函 数 名 称:IIC_Start
- 函 数 说 明:IIC起始信号
- 函 数 形 参:无
- 函 数 返 回:无
- 作 者:LC
- 备 注:无
********************************\*\*********************************/
void IIC_Start(void)
{
SDA_OUT();
SCL(0);
SDA(1);
SCL(1);
delay_us(5);
SDA(0);
delay_us(5);
SCL(0);
delay_us(5);
}
/********************************\*\*********************************
- 函 数 名 称:IIC_Stop
- 函 数 说 明:IIC停止信号
- 函 数 形 参:无
- 函 数 返 回:无
- 作 者:LC
- 备 注:无
********************************\*\*********************************/
void IIC_Stop(void)
{
SDA_OUT();
SCL(0);
SDA(0);
SCL(1);
delay_us(5);
SDA(1);
delay_us(5);
}
/********************************\*\*********************************
- 函 数 名 称:IIC_Send_Ack
- 函 数 说 明:主机发送应答
- 函 数 形 参:0应答 1非应答
- 函 数 返 回:无
- 作 者:LC
- 备 注:无
********************************\*\*********************************/
void IIC_Send_Ack(uint8_t ack)
{
SDA_OUT();
SCL(0);
SDA(0);
delay_us(5);
if(!ack) SDA(0);
else SDA(1);
SCL(1);
delay_us(5);
SCL(0);
SDA(1);
}
/********************************\*\*********************************
- 函 数 名 称:IIC_Wait_Ack
- 函 数 说 明:等待从机应答
- 函 数 形 参:无
- 函 数 返 回:1=无应答 0=有应答
- 作 者:LC
- 备 注:无
********************************\*\*********************************/
uint8_t IIC_Wait_Ack(void)
{
char ack = 0;
unsigned char ack_flag = 10;
SDA_IN();
SDA(1);
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;
}
/********************************\*\*********************************
- 函 数 名 称:IIC_Write
- 函 数 说 明:IIC写一个字节
- 函 数 形 参:dat写入的数据
- 函 数 返 回:无
- 作 者:LC
- 备 注:无
********************************\*\*********************************/
void IIC_Write(uint8_t data)
{
int i = 0;
SDA_OUT();
SCL(0);//拉低时钟开始数据传输
for( i = 0; i < 8; i++ )
{
SDA( (data & 0x80) >> 7 );
delay_us(2);
data<<=1;
delay_us(6);
SCL(1);
delay_us(4);
SCL(0);
delay_us(4);
}
}
/********************************\*\*********************************
- 函 数 名 称:IIC_Read
- 函 数 说 明:IIC读1个字节
- 函 数 形 参:无
- 函 数 返 回:读出的1个字节数据
- 作 者:LC
- 备 注:无
********************************\*\*********************************/
uint8_t IIC_Read(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;
}
delay_us(5);
}
return receive;
}
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
2.3 确定通信步骤
我们要实现的是通过案例SHT20温湿度传感器读取周围环境的温湿度情况。要进行I2C通信,需要知道其器件地址。并且了解如何与其进行通信。
SHT20温湿度传感器的I2C地址为0x80。I2C地址是一个用于在总线上识别设备的7位地址。对于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,对应图2-3-3中的bit1~bit8),等待传感器应答。如果传感器没有应答,请注意设备的实物连线情况是否正确。待传感器应答之后发送对应的命令(对应图2-3-3中的bit10~bit17),具体命令见【图2-3-2 命令集说明】。
之后紧接着是如图2-3-3所示的IC首字节(0b1000_0001)来完成。如果内部处理工作完成,单片机查询到传感器发出的确认信号后,相关数据就可以通过MCU进行读取。如果测量处理工作没有完成,传感器无确认位(ACK)输出,此时必须重新发送启动传输时序。
无论哪种传输模式,由于测量的最大分辨率为14位,第二个字节SDA上的后两位LSBs( bit43和44)用来传输相关的状态信息。两个LSBs 中的bit1表明测量的类型('0':温度;‘1':湿度)。bit0位当前没有赋值即无意义。
读取温湿度配置
根据数据手册中的通信案例,我们可以验证实现读取温湿度功能。我们以读取非主机模式的温度为例。
- 根据 【非主机模式通信流程】 所示,在开始通信之前,我们需要发送起始信号S和器件地址+写(图中的bit1~bit8),发送完成之后,等待从机应答。
IIC_Start();//发送起始信号
IIC_Send_Byte(0X80);//发送器件地址加写
//如果返回非应答,则串口提示失败-1
if( IIC_Wait_Ack() == 1 ) printf("receive fail -1\r\n");
2
3
4
- 发送对应的命令(对应图2-3-3中的bit10~bit17),具体命令见【命令集说明】,本案例发送的是测量温度命令,对应代码中的0xf3。发送完成后,等待从机应答,确定发送成功。
IIC_Send_Byte(0Xf3);//发送非主机模式读取温度命令
//如果返回非应答,则串口提示失败-2
if( IIC_Wait_Ack() == 1 )
{
printf("receive fail -2\r\n");
}
2
3
4
5
6
- 重新发送起始信号与器件地址加读0X81(对应图中的bit19~bit26),告诉传感器要开始读取温度数据。传感器接收到后,就开始采集工作。如果传感器内部处理工作完成,单片机查询到传感器发出的确认信号ACK(bit27)后,相关数据就可以通过MCU进行读取。如果测量处理工作没有完成,传感器输出非应答位NACK(bit27),此时必须重新发送启动传输时序。
//发送起始信号与器件地址+读
do
{
delay_us(10);//等待传感器采集
IIC_Start(); //起始信号
IIC_Send_Byte(0X81);//器件地址+读
} //如果返回非应答信号,则重复发送起始信号和器件地址+读
while( IIC_Wait_Ack() == 1 ); //等待应答
2
3
4
5
6
7
8
- 接收传感器输出的数据高8位和低8位。代码中的变量data_msb、data_lsb和check分别代表数据高8位、数据低8位、校验数据。
data_msb = IIC_Read_Byte();//读取第一个字节
IIC_Send_Ack(); //发送应答
data_lsb = IIC_Read_Byte();//读取第二个字节
IIC_Send_Ack(); //发送应答
check = IIC_Read_Byte(); //读取第三个字节
IIC_Send_Nack(); //发送非应答
IIC_Stop(); //发送停止信号
2
3
4
5
6
7
- 将接收到的数据进行整合为16位数据,并且根据数据手册的要求,将最后两位的状态位清零。即低8位的最后两位状态位清零。Dat为16位的变量。
//数据整合
dat = data_msb<<8; //整合高8位
dat = dat | data_lsb;//整合低8位
dat &= ~(0x03); //将最后两位状态位清除6. 数据换算。根据数据手册换算出实际的温度数据。
2
3
4
- 数据换算。根据数据手册换算出实际的温度数据。 相对湿度的转换公式为:
温度的转换公式为:
其中的 S(T)和S(RH )表示的是传感器输出的高低位整合后的16位数据。
//数据换算
//temp为浮点型变量,2的16次方为65536
temp = ( dat / 65536.0 ) \* 175.72 - 46.85;
2
3
将以上步骤的代码进行整合封装,并添加获取湿度部分,得到以下函数。
/********************************\*\*********************************
- 函 数 名 称:SHT20_Read
- 函 数 说 明:测量温湿度
- 函 数 形 参:regaddr寄存器地址 regaddr=0xf3测量温度 =0xf5测量湿度
- 函 数 返 回:regaddr=0xf3时返回温度,regaddr=0xf5时返回湿度
- 作 者:LC
- 备 注:无
********************************\*\*********************************/
float SHT20_Read(uint8_t regaddr)
{
unsigned char data_H = 0;
unsigned char data_L = 0;
float temp = 0;
IIC_Start();
IIC_Write(0x80|0);
if( IIC_Wait_Ack() == 1 ) printf("error -1\r\n");
IIC_Write(regaddr);
if( IIC_Wait_Ack() == 1 ) printf("error -2\r\n");
do{
delay_us(10);
IIC_Start();
IIC_Write(0x80|1);
}while( IIC_Wait_Ack() == 1 );
delay_us(20);
data_H = IIC_Read();
IIC_Send_Ack(0);
data_L = IIC_Read();
IIC_Send_Ack(1);
IIC_Stop();
if( regaddr == 0xf3 )
{
temp = ((data_H<<8)|data_L) / 65536.0 * 175.72 - 46.85;
}
if( regaddr == 0xf5 )
{
temp = ((data_H<<8)|data_L) / 65536.0 * 125.0 - 6;
}
return temp;
}
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
3. 软件I2C验证
在main.c中调用SHT20_Read函数,采集周围环境的温湿度。
/*
* 立创开发板软硬件资料与相关扩展板软硬件资料官网全部开源
* 开发板官网:www.lckfb.com
* 技术支持常驻论坛,任何技术问题欢迎随时交流学习
* 立创论坛:https://oshwhub.com/forum
* 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
* 不靠卖板赚钱,以培养中国工程师为己任
* Change Logs:
* Date Author Notes
* 2024-04-11 LCKFB-LP first version
*/
#include "board.h"
#include "bsp_uart.h"
#include <stdio.h>
#include "bsp_sht20.h"
int32_t main(void)
{
board_init();
uart1_init(115200U);
//引脚初始化
SHT20_GPIO_INIT();
//等待传感器上电初始化完成
delay_ms(20);
while(1)
{
//采集温度
printf("temp = %.2f\r\n", SHT20_Read(0xf3) );
//采集湿度
printf("humi = %.2f\r\n", SHT20_Read(0xf5) );
printf("\r\n");
delay_ms(500);
}
}
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
串口输出结果: