STM32使用硬件IIC驱动SHT30温湿度传感器

本文最后更新于:2023年7月23日 凌晨

STM32使用硬件IIC驱动SHT30温湿度传感器

喜报,终于写完了

当时网上 xjb乱逛,突然看见有人讨论小米温湿度传感器,我当时也买了个这玩意,于是突然感兴趣查了一下用的传感器——SHT30,协议是IIC。当时想着能不能自己也山寨一个小米温湿度传感器 ,然后下单了SHT30之后就再也没动过 。这会放假了,想起来这玩意,就整一整吧,顺便再 学习一遍IIC协议 。图个方便,这次先用硬件IIC。

IIC 协议

I2C协议介绍

(1)I2C协议简介

【省流版】:

串行通信协议的一种,属于串行通信协议中的半双工同步通信。

IIC是一种被定义好的标准,采用二线制,一条数据线,一条时钟线;

数据线传输数据,一位一位传输;时钟线控制传输速率。

I2C总线是Philips公司在八十年代初推出的一种串行、半双工的总线,主要用于近距离、低速的芯片之间的通信。

每个IIC器件有两条线路:

  • SDA是供主器件和节点发送和接收数据的线路。
  • SCL是承载时钟信号的线路。SCL总是由I2C主器件生成。规范对时钟信号的低相位和高相位有最短周期要求。

I2C总线仅使用两条双向线路:每个器件的SDA和SCL用于简单的IC间通信。

硬件最重要的注意是在SDA和SCL上加入上拉电阻。I2C器件通过开集或开漏引脚连接到总线,将线路拉低。当没有数据传输时,I2C总线处于高电平空闲状态;线路被被动拉高。要传输数据,须切换线路,即先拉低再释放(又变为高电平)。数据位在时钟下降沿传输。

开漏输出需要一个上拉电阻(Rp)才能正确输出高电平。上拉电阻连接在输出引脚和高电平所需的输出电压(VDD)之间。

对于VCC和VDD (5 V)的典型值,4700 Ω是最常用的上拉电阻值。

I2C总线是一种主从结构(Master/Slave)总线, I2C总线上的每一个设备都可以作为主设备或者从设备,但一个总线上一般只有一个主设备,可以带多个从设备。其中主设备用来产生允许传输的时钟信号,并初始化总线的数据传输,所以主设备通常是CPU,而从设备只能被动响应主设备发起的通信请求,所以各种I2C接口芯片将作为从设备使用。

(2)I2C从设备地址

因为一个I2C总线上可以有多个从设备,这样主设备需要通过地址来确定与哪个器件进行通信。I2C总线上每个从设备都有一个唯一的7bit地址物理识别,这个地址固化在芯片内部,并可以从芯片datasheet上找到。

I2C有16个保留地址。

其中I2C的从器件地址(本次SHT30只用到了7位的)的组成如下:

1byte = 7bit地址 + 1bit读写标志

注:1bit读写标志中,0-发送数据(写),1-请求数据(读)

有些时候一个总线上可能需要挂多个同一芯片,这样有些芯片还需要引出一个或几个引脚,由开发板设计电路来决定其具体地址,从而让不同芯片具有不同的7bit物理地址。

I2C通信时序

在I2C总线上传送的每一位数据都由一个同步时钟脉冲相对应,即在SCL串行时钟的配合下,数据在SDA上从高位向低位依次串行传送每一位的数据。IIC通信时序图:

(1)起始位

I2C总线在空闲时SDA和SCL都处于高电平状态(由上拉电阻拉成高电平),当主设备要开始一次I2C通信时就发送一个START(S)信号,这个起始位就可以告诉所有I2C从机, “我”要开始进行I2C通信了;当要结束一次I2C通信时,则发送一个STOP信号结束本次通信。

START(S):当SCL保持高电平时候,SDA出现下降沿,产生一个起始位,注意SCL一定要在高电平。
STOP( P ):当SCL保持高电平时候,SDA出现上升沿,产生一个停止位,注意SCL一定要在高电平。

(2)读写地址

主机在发送START信号之后,第2个时序应该立刻给出要通信的目标从机物理地址。此外,I2C总线是一种能够实现半双工通信的同步串行通信协议,站在主设备的角度来看应该具有读/写从设备的功能。

这时候I2C的读写地址除了7bit物理地址以外,还有1bit用来标识读/写方向位。这样I2C的从设备读写地址通常是一个字节,其中高7bit是上面描述的物理地址,最低位用来表示读写方向(0为写操作, 1为读操作)

(3)I2C应答信号

主机往I2C总线上传输器件地址,所有的从机接收到这个地址后与自己的地址相比较若相同则发出一个应答ACK(Acknowledge)信号,主机收到这个应答信号后通讯连接建立成功,若未收到应答信号则表示寻址失败。

此外,主/从机在之后的数据通信中,数据接收方(可能是主机也可能是从机)收到传输的一个字节数据后,需要给出响应,此时处在第九个时钟,发送端释放SDA线控制权,将SDA电平拉高,由接收方控制。

  • 若希望继续,则给出“应答(ACK, Acknowledge)”信号,即SDA为低电平
  • 若不希望继续,则给出“非应答(NACK,Not Acknowledge) ”信号,即SDA为高电平

(4)数据位发送与接收

主机在收到从机的应答信号之后,开始给从机发送数据。SDA数据线上的每个字节必须是8位,每次传输的字节数量没有限制,每个字节发送完成之后,从机必须跟一个应答信号。

I2C总线通信时数据位传输采用MSB(最高位优先)方式发送,其中高电平表示数据位1,低电平表示数据位0。

当传输的数据位需要改变时(如上一个位发送的是1,下一个位要发送0),必须发生在SCL为低电平期间。另外在传输过程中, SDA上的数据位在SCL高电平期间必须保持稳定不变。

假设SCL在高电平,想一下是不是就会触发起始位或者终止位。想一想起始信号与停止信号是怎么发送的就会明白为什么SCL一定要在高电平才能改变SDA。

I2C协议主机收发数据流程

(1)主机发送数据

主机在检测到总线为“空闲状态”(即 SDA、SCL 线均为高电平)时,发送一个启动信号“S”,开始一次通信的开始;

主机接着发送一个从设备地址,它由7bit物理地址和1bit读写控制位R/W组成(此时R/W=0);
相对应的从机收到命令字节后向主机回馈应答信号 ACK(ACK=0);

主机收到从机的应答信号后开始发送第一个字节的数据;

从机收到数据后返回一个应答信号 ACK;

主机收到应答信号后再发送下一个数据字节;

当主机发送最后一个数据字节并收到从机的 ACK 后,通过向从机发送一个停止信号P结束本次通信并释放总线。从机收到P信号后也退出与主机之间的通信。

注意

  • 主机通过发送地址码与对应的从机建立了通信关系,而挂接在总线上的其它从机虽然同时也收到了地址码,但因为与其自身的地址不相符合,因此提前退出与主机的通信;
  • 主机的一次发送通信,其发送的数据数量不受限制。主机是通过 P 信号通知发送的结束,从机收到 P 信号后退出本次通信;
  • 主机的每一次发送后都是通过从机的 ACK 信号了解从机的接收状况,如果应答错误则重发。

(2)主机接收数据

  • 主机发送起始信号后,接着发送地址字节(其中R/W=1);
  • 对应的从机收到地址字节后,返回一个应答信号并向主机发送数据;
  • 主机收到数据后向从机反馈一个应答信号ACK;
  • 从机收到应答信号后再向主机发送下一个数据;
  • 当主机完成接收数据后,向从机发送一个NAK,从机收到非应答信号后便停止发送;
  • 主机发送非应答信号后,再发送一个停止信号,释放总线结束通信。

注意

  • 主机所接收数据的数量是由主机自身决定,当发送“非应答信号NAK”时从机便结束传送并释放总线。
  • 非应答信号的两个作用:前一个数据接收成功,停止从机的再次发送。

SHT30 温湿度传感器

)睡醒再写……

SHT3X数据手册下载: [Datasheet SHT3x-DIS]

SHT30简介

SHT30数字温湿度传感器采用业内知名的瑞士Sensirion公司推出的新一代SHT30温湿度传感器芯片,它能够提供极高的可靠性和出色的长期稳定性,具有功耗低、反应快、抗干扰能力强等优点。IIC通讯,兼容3.3V/5V,可以非常容易的集成到智能楼宇、天气站、仓库存储、养殖、孵化等应用场景中,其中小米的温湿度传感器使用的也是SHT30。

  • 高精度,内部自动校准,数字输出
  • 低功耗、响应速度快、抗干扰能力强
  • 兼容3.3V/5V控制器

SHT30工作原理

SHT30 芯片有八个引脚,利用I2C进行数据传输,具有两个可选地址,宽电源电压从2.4V到5.5V。下面是引脚说明:

管脚 名称 备注
1 SDA I2C数据引脚,输入/输出
2 ADDR 地址引脚,输入
3 ALENT 报警引脚,输出;不使用时悬空
4 SCL I2C时钟引脚,输入/输出
5 VDD 电源引脚,输入
6 nRESET 复位引脚,输入
7 R 无用引脚,接地
8 VSS 接地引脚

SHT30地址选择

SHT3x有两个地址,可以将ADDR接到不同电平进行切换:

SHT3x-DIS I2C地址 条件
I2C地址A 0x44(默认) ADDR引脚接电平
I2C地址B 0x45 ADDR引脚接电平

SHT3x电路图

因为是开漏,所以记得将SDA,SCL两个引脚拉高。(因为直接买的模块,所以就不用操心了。

电路图

SHT30数据采集

SHT3x有单次测量命令数据采集模式和周期性测量命令数据采集模式,这里选择周期性测量命令数据采集模式

定期数据采集模式的测量命令

在这种模式下,一个发出的测量命令会产生一个数据对流。每个数据对由一个16位温度值和一个16位湿度值(按此顺序)组成。在周期模式下,可以选择不同的测量命令。对应的16位命令如表9所示。它们在可重复性(低、中、高)和数据采集频率(每秒0.5、1、2、4和10次测量值,mps)方面存在差异。在此模式下不能选择时钟拉伸。数据采集频率和可重复性设置会影响传感器的测量持续时间和电流消耗。

根据表格的通信时序,主机发送起始信号,然后发送从机地址加写指令命令,等待从机应答,再发送命令的高位(MSB),等待从机应答,再发送命令的低位(LSB)。

周期模式下测量结果的读数

读数据的命令是: 0xE000

读数可以通过上表所示的命令启动。如果没有测量数据,I2C读取头用NACK而不是ACK(表中的位9)响应,通信停止。

空白的数据块是由32发送,黑色的数据块是由SHT30发送。

根据表中的通信时序,获取SHT30的温湿度数据,获取到的是16位的温度值和16位的湿度值,温度和湿度后都有CRC-8校验。

SHT30复位

SHT30的系统复位有两种方式,一种是发出命令(软重置),一种是通过外部向专用复位引脚(nRESET)发送脉冲。我们选择软重置

软重置的命令是: 0x30A2

差错校验

CRC8校验 - 简书 (jianshu.com) (原理进行一个参考链接的抄)

CRC覆盖了先前传输的两个数据字节的内容。 为了计算校验和,仅使用这两个先前发送的数据字节。

温湿度转换

测量数据始终以 16 位值(无符号整数)形式传输。 这些值已经线性化并补偿了温度和电源电压的影响。 可以使用以下公式将这些原始值转换为物理标度。

相对湿度换算公式(结果以%RH为单位):

$$RH=100\times \frac{S_{RH}}{2^{16}-1}$$

温度换算公式(结果以 °C 和 °F 为单位):

$$T[°C]=-45+175\times\frac{S_T}{2^{16}-1}$$

$$T[°F]=-49+315\times\frac{S_T}{2^{16}-1}$$

$S_{RH}$ 和 $S_{T}$ 分别表示湿度和温度的原始传感器输出。 仅当$S_{RH}$ 和 $S_{T}$ 以十进制表示形式使用时,这些公式才能正确工作。

代码驱动SHT30

首先使用 STM32CubeMX进行一些常规配置,时钟,调试选项……

然后配置串口: STM32&UART串口通信(使用CubeMX)

在I2C2中选择I2C,打开硬件I2C。

HAL库中I2C发送接收数据函数

(1)HAL_I2C_Master_Transmit()

  1. 函数原型

    HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
  2. 函数功能:IIC发送数据,主机需要将数据通过IIC发送过去

  3. 参数介绍:

    • *hi2c 设置使用的是那个IIC
    • DevAddress 写入的地址,设置写入数据的地址
    • *pData 需要写入的数据
    • Size 要发送的字节数
    • Timeout 最大传输时间,超过传输时间将自动退出传输函数
  4. 使用到的函数参数讲解(🌰):

    HAL_I2C_Master_Transmit(&hi2c2, SHT30_ADDR_WR, (uint8_t*)buf, 2, 0xFFFF);
    • &hi2c2:我们使用的是:hi2c2,传地址&hi2c2
    • SHT30_ADDR_WR:我们宏定义了写的地址,传写的地址#define SHT30_ADDR_WR (SHT30_ADDR<<1)
    • (uint8_t*)buf:我们将需要传的数据保存在buf
    • 2:传2个字节,16个位
    • 0xFFFF超时:0xFFFF(4 294 967 295也就是无符号整型所能表示的最大值)

HAL_I2C_Master_Receive()

  1. 函数原型

    HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
  2. 函数功能:IIC接收数据,从机发送给主机,主机需要将数据通过IIC接收

  3. 函数参数和HAL_I2C_Master_Transmit()大概是一样的,只是取到的数据保存在pData中。

  4. 使用到的函数参数讲解(🌰):

    HAL_I2C_Master_Receive(&hi2c2, SHT30_ADDR_RD, buf, SHT30_DATA_SIZE, 0xFFFF);
    • &hi2c2:我们使用的是:hi2c2,传地址&hi2c2
    • SHT30_ADDR_RD:我们宏定义了读的地址,传读的地址#define SHT30_ADDR_RD ((SHT30_ADDR<<1) | 0x01)
    • buf:我们将获取到的数据保存在buf
    • SHT30_DATA_SIZE:宏定义,6个字节#define SHT30_DATA_SIZE 6
    • 0xFFFF超时:0xFFFF(4 294 967 295也就是无符号整型所能表示的最大值)

头文件 sht30.h

在头文件中我们提前定义好相关命令与函数原型。

enum用于定义枚举类型,它本质上是一种整数类型。

#ifndef INC_SHT30_H_
#define INC_SHT30_H_

#include "stm32f4xx_hal.h"

#define SHT30_ADDR 0x44

#define SHT30_ADDR_WR (SHT30_ADDR<<1)
#define SHT30_ADDR_RD ((SHT30_ADDR<<1) | 0x01)

#define SHT30_DATA_SIZE 6

typedef enum
{
    /* 软件复位命令 */
    SOFT_RESET_CMD = 0x30A2,    
    /*
    单次测量模式
    命名格式:Repeatability_CS_CMD
    CS: Clock stretching
    */
    HIGH_ENABLED_CMD    = 0x2C06,
    MEDIUM_ENABLED_CMD  = 0x2C0D,
    LOW_ENABLED_CMD     = 0x2C10,
    HIGH_DISABLED_CMD   = 0x2400,
    MEDIUM_DISABLED_CMD = 0x240B,
    LOW_DISABLED_CMD    = 0x2416,

    /*
    周期测量模式
    命名格式:Repeatability_MPS_CMD
    MPS:measurement per second
    */
    HIGH_0_5_CMD   = 0x2032,
    MEDIUM_0_5_CMD = 0x2024,
    LOW_0_5_CMD    = 0x202F,
    HIGH_1_CMD     = 0x2130,
    MEDIUM_1_CMD   = 0x2126,
    LOW_1_CMD      = 0x212D,
    HIGH_2_CMD     = 0x2236,
    MEDIUM_2_CMD   = 0x2220,
    LOW_2_CMD      = 0x222B,
    HIGH_4_CMD     = 0x2334,
    MEDIUM_4_CMD   = 0x2322,
    LOW_4_CMD      = 0x2329,
    HIGH_10_CMD    = 0x2737,
    MEDIUM_10_CMD  = 0x2721,
    LOW_10_CMD     = 0x272A,
    
    /* 周期测量模式读取数据命令 */
    READOUT_FOR_PERIODIC_MODE = 0xE000,
} SHT30_CMD;

extern void sht30_soft_reset(void);
extern uint8_t SHT30_Init(void);
extern uint8_t SHT30_Read_Dat(uint8_t* dat);
extern uint8_t CheckCrc8(uint8_t* const message, uint8_t initial_value);
extern uint8_t SHT30_Dat_To_Float(uint8_t* const dat, float* temperature, float* humidity);
#endif /* INC_SHT30_H_ */

主要文件 sht30.c

先包含主要的一些头文件

#include "stdio.h"
#include "i2c.h"
#include "sht30.h"

发送指令

/**
 * @brief    向SHT30发送一条指令(16bit)
 * @param    cmd —— SHT30指令(在SHT30_MODE中枚举定义)
 * @retval    成功返回HAL_OK
*/
static int sht30_send_cmd(SHT30_CMD cmd) {
	uint8_t buf[2];
	buf[0] = cmd >> 8;
	buf[1] = cmd;
	return HAL_I2C_Master_Transmit(&hi2c2, SHT30_ADDR_WR, (uint8_t*)buf, 2, 0xFFFF);
}

复位SHT30

/**
 * @brief    复位SHT30
 * @param    none
 * @retval    none
*/
void sht30_soft_reset(void) {
	sht30_send_cmd(SOFT_RESET_CMD);
	HAL_Delay(20);
}

初始化SHT30

/**
 * @brief    初始化SHT30
 * @param    none
 * @retval    成功返回HAL_OK
 * @note    周期测量模式
*/
uint8_t SHT30_Init(void) {
	return sht30_send_cmd(MEDIUM_2_CMD);
}

读取数据

/**
 * @brief    从SHT30读取一次数据
 * @param    dat —— 存储读取数据的地址(6个字节数组)
 * @retval    成功 —— 返回HAL_OK
*/
uint8_t SHT30_Read_Dat(uint8_t* dat) {
    sht30_send_cmd(READOUT_FOR_PERIODIC_MODE);
    return HAL_I2C_Master_Receive(&hi2c2, SHT30_ADDR_RD, dat, 6, 0xFFFF);
}

CRC-8校验

#define CRC8_POLYNOMIAL 0x31
uint8_t CheckCrc8(uint8_t* const message, uint8_t initial_value) {
    uint8_t  remainder;        //余数
    uint8_t  i = 0, j = 0;  //循环变量
    /* 初始化 */
    remainder = initial_value;
    for(j = 0; j < 2;j++) {
        remainder ^= message[j];
        /* 从最高位开始依次计算  */
        for (i = 0; i < 8; i++) {
            if (remainder & 0x80) {
                remainder = (remainder << 1)^CRC8_POLYNOMIAL;
            }
            else {
                remainder = (remainder << 1);
            }
        }
    }
    /* 返回计算的CRC码 */
    return remainder;
}

温湿度转换

/**
 * @brief    将SHT30接收的6个字节数据进行CRC校验,并转换为温度值和湿度值
 * @param    dat  —— 存储接收数据的地址(6个字节数组)
 * @retval    校验成功  —— 返回0
 *             校验失败  —— 返回1,并设置温度值和湿度值为0
*/
uint8_t SHT30_Dat_To_Float(uint8_t* const dat, float* temperature, float* humidity) {
    uint16_t recv_temperature = 0;
    uint16_t recv_humidity = 0;
    
    /* 校验温度数据和湿度数据是否接收正确 */
    if(CheckCrc8(dat, 0xFF) != dat[2] || CheckCrc8(&dat[3], 0xFF) != dat[5])
        return 1;
    
    /* 转换温度数据 */
    recv_temperature = ((uint16_t)dat[0]<<8)|dat[1];
    *temperature = -45 + 175*((float)recv_temperature/65535);
    
    /* 转换湿度数据 */
    recv_humidity = ((uint16_t)dat[3]<<8)|dat[4];
    *humidity = 100 * ((float)recv_humidity / 65535);
    
    return 0;
} 

main.c中使用SHT30

int main()外定义格式化函数:

void report_tempRH_json(uint8_t* dat, float *temperature, float *humidity);

int main()while (1)前定义相关变量与SHT30初始化:

uint8_t dat[6] = {0};
float temperature = 0.0;
float humidity = 0.0;
sht30_soft_reset();
if(SHT30_Init() == HAL_OK) printf("sht30 init is ok!\r\n");
else printf("sht30 init is not ok!\r\n");

while(1)内调用格式化函数,每次调用后进行适当延时。

report_tempRH_json(dat, &temperature, &humidity);
HAL_Delay(1000);

格式化函数

将格式化函数写在 /* USER CODE BEGIN 4 *//* USER CODE END 4 */ 之中。

字符串格式化输出 sprintf()

int sprintf(char * s, const char * format, ...);定义在stdio.h中,适用于 string 版本的格式化输出,其目标不是控制台,而是一个字符串。

sprintf() 格式化一个字符串,例如:

char buf[256];
sprintf(buf, "Name:%s,Age:%d,Weight:%.2f\n", "LiMing", 30, 1.68);

运行后目标buf格式化为:Name:LiMing,Age:30,Weight:1.68

sprintf()对于字符串的处理非常有用。

将SHT30温湿度数据格式化并通过串口输出:

void report_tempRH_json(uint8_t* dat, float *temperature, float *humidity) {
	if(SHT30_Read_Dat(dat) == HAL_OK) {
		
		if(SHT30_Dat_To_Float(dat,temperature,humidity) == HAL_OK) {
			char buf[128];
			memset(buf, 0, sizeof(buf));
			snprintf(buf, sizeof(buf), "{\"Temperature\":\"%.2f\", \"Humidity\":\"%.2f\"}", *temperature, *humidity);
			HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 0xFFFF);                
		}
		else printf("sht30 check failed!\r\n");
	}
	else printf("sht30 read date failed!\r\n");
}

结果对比

随便复制几段单片机串口发过来的:

[00:24:53.824] {"Temperature":"21.36", "Humidity":"53.92"}
[00:24:55.818] {"Temperature":"21.34", "Humidity":"53.85"}
[00:24:57.818] {"Temperature":"21.41", "Humidity":"53.88"}
[00:24:59.815] {"Temperature":"21.33", "Humidity":"53.98"}
[00:25:01.809] {"Temperature":"21.40", "Humidity":"53.89"}
[00:25:03.808] {"Temperature":"21.38", "Humidity":"53.96"}
[00:25:05.804] {"Temperature":"21.38", "Humidity":"53.94"}
[00:25:07.799] {"Temperature":"21.38", "Humidity":"54.02"}
[00:25:09.796] {"Temperature":"21.36", "Humidity":"53.84"}
[00:25:11.807] {"Temperature":"21.37", "Humidity":"53.82"}
[00:25:13.788] {"Temperature":"21.36", "Humidity":"53.71"}

手机上看一眼小米温湿度传感器的:

还行。

参考链接

I2C通信协议:了解I2C Primer、 PMBus和SMBus | 亚德诺半导体 (analog.com)

I2C协议介绍以及HAL库实现I2C对SHT30温湿度采样_hal i2c_HaiQinyanAN的博客-CSDN博客

IIC及配置应用(SHT30读取温湿度)_嵌不入的博客-CSDN博客

STM32物联网项目-SHT30温湿度采集(IIC通信)_ONE_Day|的博客-CSDN博客

[Datasheet SHT3x-DIS]

CRC8校验 - 简书 (jianshu.com)

STM32&UART串口通信(使用CubeMX)


STM32使用硬件IIC驱动SHT30温湿度传感器
https://blog.ksfu.top/posts/6c49/
作者
康师傅
发布于
2023年7月14日
许可协议