简介:面向嵌入式开发者的 SHT30 温湿度传感器 STM32F130ZET6 HAL 库例程,以完整工程方式提供了一套可运行代码。例程从传感器特性、芯片外设入手,利用 STM32 HAL 库快速搭建 I2C 通信,涵盖数据读取、温度与湿度换算以及串口输出等环节,适合学习 I2C 驱动开发或作为环境监测项目的基础模板。包体共 231 个文件,主要包含 C 语言源文件与头文件、IAR/Keil 工程配置以及编译输出文件,整体大小仅 10.22MB,目录结构清晰,便于直接导入查看;目前已有 1981 人学习使用。工程内不仅包含 HAL 库的初始化与读写函数调用,还详细实现了 SHT30 的测量命令、读取时序与数据校验,并针对 SHT30 的温度、湿度换算公式做了代码标注;同时给出了硬件接线、上拉电阻、中断处理等注意事项,可帮助读者规避常见错误,缩短调试周期。无论是用于智能家居、环境监测,还是作为 STM32 中级进阶练习,这套资源都具备不错的参考价值。
1. 从一颗温湿度传感器到完整数据链路:SHT30 与 STM32F130ZET6 的 HAL 库实战
如果你做过环境监测类的单片机项目,一定遇到过这种尴尬:传感器硬件买回来了,例程却只有某款 51 开发板的寄存器版本,换到 STM32 上就得从 I2C 时序开始重写。第一次碰 SHT30 时,我盯着逻辑分析仪看了一个下午,最后发现问题根本不在时序,而是我没把 HAL 库的 I2C 复用引脚配到同一组。这套例程的价值就是替你排掉这些坑:它把 SHT30 接到 STM32F130ZET6 上,全程用 HAL 库编码,从 CubeMX 工程、sht30.c 驱动到主循环读数据、串口打印,都给出了能直接编译下板验证的完整代码。适合两类人:一类是想尽快让 SHT30 出数、跑通 I2C 链路的入门开发者;另一类是想基于官方例程改成周期采样、低功耗或挂多颗传感器的老手。下面从硬件连接开始,一层层拆开这个工程。
2. 硬件与协议基础:I2C 总线上的 SHT30 通信
2.1 SHT30 传感器特性与引脚定义
SHT30 是数字温湿度传感器,I2C 接口,供电范围 2.4V 到 5.5V,测量范围湿度 0~100%RH、温度 -40 到 125℃,典型精度能做到 ±2%RH 和 ±0.2℃。这些参数意味着它可以直接工作在 3.3V 下,由 STM32F130ZET6 的供电引脚驱动,不需要额外的电平转换。SHT30 的 I2C 从机地址由 ADDR 引脚决定:ADDR 接地时地址是 0x44,接高电平变为 0x45,这一点在写驱动时要格外留意。很多例程里直接把地址写死成 0x44,换了一块 ADDR 被拉高的模块就读不到数据,表现就是 HAL_I2C_Master_Transmit 返回 HAL_ERROR。
功能引脚只有四根:VCC、GND、SCL、SDA。部分模块额外引出 ADDR 和 ALERT,ALERT 是报警输出,一般例程用不到。例程中实际的接线关系可以归结为下面这张表:
| 引脚 | 方向 | 功能 | 例程接线 |
|---|---|---|---|
| VCC | 电源 | 供电 3.3V | 接 3.3V |
| GND | 地 | 接地 | 接 GND |
| SCL | I2C 时钟 | 时钟线 | 接 PB6(I2C1_SCL) |
| SDA | I2C 数据 | 数据线 | 接 PB7(I2C1_SDA) |
| ADDR | 地址选择 | 0x44 或 0x45 | 接 GND,选 0x44 |
PB6 和 PB7 在 STM32F130ZET6 上默认复用为 I2C1 的 SCL 和 SDA。例程在 CubeMX 里选的就是这两个引脚。如果你用的是自己的板子,引脚可以换,但前提是换到带 I2C 复用功能的引脚上,并且在 CubeMX 的 Pinout 视图里把这个引脚的 Alternate Function 配成 I2C1_SCL / I2C1_SDA。这里最容易出现的低级错误是:只点了 GPIO 输出,没选复用功能,结果 IOC 文件里看似配置了引脚,实际外设根本没接通。
2.2 STM32F130ZET6 的 I2C 外设与 HAL 库抽象
STM32F130ZET6 集成多个 I2C 外设,例程使用 I2C1。HAL 库把寄存器操作封装成了 HAL_I2C_Init、HAL_I2C_Master_Transmit、HAL_I2C_Master_Receive 等函数,对写应用的开发者来说,只需要关心两个层面:第一,在 CubeMX 里把外设和引脚配置对;第二,调用 HAL 库函数完成字节收发。例程用的是最直接的轮询方式,阻塞等待传输完成。代价是等待期间 CPU 空转,好处是代码直观,不会在初始化阶段引入中断优先级和回调函数方面的额外变量。
I2C 速率在例程里配置为 100kHz,也就是标准模式。SHT30 手册标注支持最高 1MHz,但实际调试中常见的现象是:模块用杜邦线飞线连接,上拉电阻偏弱,高速通信时数据错位,读出来的温湿度剧烈跳变。优先用 100kHz 跑通链路,再逐步提速,是减少这类玄学问题最稳妥的路径。CubeMX 里可以设置的参数包括 ClockSpeed(时钟频率)、AddressingMode(7 位或 10 位地址)、DutyCycle(占空比)等,这些都汇总进 I2C_InitTypeDef 结构体,由 HAL_I2C_Init 函数统一写入寄存器。
2.3 例程配置:硬件连接与初始化参数
在 CubeMX 里新建工程时,需要做几处核心配置。首先是时钟树:在 RCC 里打开 HSE 外部晶振,然后在 Clock Configuration 里把 SYSCLK 配到 72MHz,这是 STM32F130ZET6 的主频。I2C1 挂在 APB1 总线上,APB1 默认是 36MHz。CubeMX 里 ClockSpeed 填 100000 时,HAL 库会根据 36MHz 输入时钟自动计算分频系数,不需要手动改寄存器。
接着在 Pinout 视图里把 PB6 和 PB7 分别配置为 I2C1_SCL 和 I2C1_SDA。打开 I2C1 配置面板,时钟速度填 100000,寻址模式选 7 bit。生成代码后,HAL_I2C_MspInit 会完成 GPIO 时钟使能、复用配置和上下拉设置。这里有一个很容易被忽略的细节:I2C 总线需要上拉电阻。很多现成的传感器模块板上已经焊了 4.7k 欧姆上拉,如果你的板子没有,SCL 和 SDA 电平拉不起来。更麻烦的是这种故障不会在 HAL_I2C_Init 时报错,只有真正调用传输函数时才会超时。例程在 GPIO 配置里选择了 Pull-up,用 MCU 内部弱上拉作为兜底,但我不建议长期依赖内部弱上拉,外部上拉电阻的驱动能力更稳定。
提示:判断 I2C 总线是否正常,最直接的办法是量电压。空闲状态下 SCL 和 SDA 都应该被拉到高电平;如果其中一根是低电平,说明有器件占着总线,或者某条线对地短路了。
这一层的配置全景可以概括为:硬件四根线加外部上拉,软件上 CubeMX 把时钟和引脚全部配置好,应用层只需要关心 sht30.c 里的函数。剩下的问题就是读懂驱动源码,把它跑起来。
3. 工程结构与核心代码:从 CubeMX 工程到传感器驱动
3.1 工程文件组成与目录结构
这套例程是基于 CubeMX 生成的 HAL 库工程,代码分两层:底层是 ST 官方生成的初始化代码,上层是围绕 sht30.c 封装的传感器驱动。工程用 Keil MDK 打开编译,顶层结构如下:
Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── i2c.h │ │ ├── usart.h │ │ └── sht30.h │ └── Src/ │ ├── main.c │ ├── i2c.c │ ├── usart.c │ └── sht30.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── MDK-ARM/ │ └── project.uvprojx └── SHT30.iocCore/Src 里最关键的是三个文件:i2c.c 是 I2C1 初始化代码,usart.c 是串口 1 的初始化代码,sht30.c 封装了 SHT30 的软复位、单次测量和数据读取。main.c 把三者串起来,在 while(1) 循环里周期发起测量,并把温湿度通过串口打印。Drivers 目录是 HAL 库源码,正常开发不需要改动;MDK-ARM 里的 .uvprojx 是工程入口,双击就能打开。下面这张表标出了每个文件的实际职责:
| 文件 | 职责 | 是否需要修改 |
|---|---|---|
| i2c.c | 初始化 I2C1 外设,配置引脚复用 | 改引脚时才动 |
| usart.c | 初始化串口 1,用于打印调试信息 | 改波特率或引脚时动 |
| sht30.c | SHT30 驱动,含测量命令、数据读取、CRC 校验 | 核心文件,二次开发的主战场 |
| main.c | 主循环调用驱动,组织读取和输出 | 改应用逻辑时动 |
3.2 I2C 底层初始化与 HAL 库通信函数
打开 i2c.c,核心是 MX_I2C1_Init 函数。它填充一个 I2C 初始化结构体,然后调用 HAL_I2C_Init,参数直接决定总线的时序行为:
static void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; // 100kHz,标准模式 hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 占空比 2:1,标准模式默认 hi2c1.Init.OwnAddress1 = 0; // 主机模式下本机地址无需关心 hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } }ClockSpeed 填 100000 代表 100kHz,这是与 SHT30 通信的起点。NoStretchMode 务必保持 DISABLE,意思是允许从机拉伸时钟。如果误开成 ENABLE,SHT30 在转换期间不能把 SCL 拉低来延迟时序,数据极可能错位。初始化完成后,sht30.c 里封装了两个底层操作,上层不用关心字节序和地址移位:
static HAL_StatusTypeDef SHT30_WriteCmd(uint16_t cmd) { uint8_t buf[2]; buf[0] = cmd >> 8; // 命令高字节在前 buf[1] = cmd & 0xFF; // 命令低字节在后 return HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR << 1, buf, 2, 100); } static HAL_StatusTypeDef SHT30_ReadData(uint8_t *data, uint8_t len) { return HAL_I2C_Master_Receive(&hi2c1, SHT30_ADDR << 1, data, len, 100); }SHT30 的从机地址是 7 位 0x44,而 HAL 库要求传入 8 位地址,所以左移一位变成 0x88,最低位由 HAL 库根据读或写自动填充。命令字固定两个字节,先高后低,这是 SHT30 手册对命令帧格式的硬性要求。最后一个参数 100 是超时毫秒数,如果总线上没有设备应答,HAL 库在 100ms 后返回 HAL_TIMEOUT,调用方可以基于返回值决定是否重试,而不是让程序死等。
3.3 SHT30 核心驱动:测量命令、数据读取与转换
sht30.c 里的两个关键函数是 SHT30_Init 和 SHT30_ReadTempHum。SHT30_Init 先发一条软复位命令 0x0006,把传感器寄存器恢复到默认状态,再等待 10ms。单次测量命令用 0x2C06:高字节 0x2C 表示单次测量模式,低字节 0x06 表示高重复度并启用时钟拉伸。SHT30 支持低、中、高三种重复度,对应不同的采样时间和功耗,例程选择高重复度,数据分辨率和稳定性最好。
#define SHT30_ADDR 0x44 #define SHT30_CMD_SOFTRESET 0x0006 #define SHT30_CMD_MEAS_HIGH 0x2C06 // 单次测量,高重复度 uint8_t SHT30_Init(void) { if (SHT30_WriteCmd(SHT30_CMD_SOFTRESET) != HAL_OK) return 0; HAL_Delay(10); // 软复位命令执行约需 1ms,这里留足余量 return 1; } uint8_t SHT30_ReadTempHum(float *temp, float *hum) { uint8_t buf[6]; if (SHT30_WriteCmd(SHT30_CMD_MEAS_HIGH) != HAL_OK) return 0; HAL_Delay(30); // 高重复度测量约 15ms,等待转换完成 if (SHT30_ReadData(buf, 6) != HAL_OK) return 0; *temp = SHT30_CalcTemp(buf[0], buf[1]); *hum = SHT30_CalcHum(buf[3], buf[4]); return 1; }测量命令发出后不能立刻读数据,SHT30 完成一次高重复度转换大约需要 15ms。这里用 HAL_Delay(30) 是保守做法,优点是逻辑简单,缺点是这个时间段 CPU 完全等待。如果你对响应时间敏感,可以改用 0x2032 这类带时钟拉伸的测量命令,或者把延时缩短并配合重试机制。读回的 buf 一共 6 字节,布局是:温度高字节、温度低字节、温度 CRC、湿度高字节、湿度低字节、湿度 CRC。因此转换温度用 buf[0] 和 buf[1],湿度用 buf[3] 和 buf[4],中间的 buf[2] 是温度校验值。
新手最容易犯的错误是把 buf[2] 当成湿度高位,只要这么一错,湿度计算出来的数值完全不可信。这个数据布局源于 SHT30 手册的标准输出格式,每两个数据字节后跟一个 CRC 校验字节,读取时必须按 6 字节整体解析。上面的代码目前只做了读取和转换,还没有 CRC 校验。下一章我会把完整的校验逻辑补上,并解释为什么校验失败时要丢弃本次数据而不是继续用。
3.4 主循环:采集周期与串口打印
main.c 的主循环是例程的入口。它依次完成 HAL 初始化、时钟配置、GPIO、I2C 和串口初始化,然后调用 SHT30_Init 做软复位,最后进入循环读取数据:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); float temp, hum; if (SHT30_Init()) { char msg[] = "SHT30 init ok\r\n"; HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), 100); } while (1) { if (SHT30_ReadTempHum(&temp, &hum)) { char buf[64]; snprintf(buf, sizeof(buf), "temp: %.2f C, hum: %.2f %%RH\r\n", temp, hum); HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100); } HAL_Delay(1000); // 1 秒刷新一次,可按场景调整 } }串口初始化由 usart.c 完成,默认波特率 115200,数据格式 8N1。如果串口没有任何输出,先确认串口助手波特率是否一致,再检查 main 函数里有没有漏掉 MX_USART1_UART_Init。主循环里的 HAL_Delay(1000) 决定数据刷新频率。环境监测场景放到 2 到 5 秒没问题,做快速响应实验改成 100ms 也可以,但要注意此时 I2C 总线上每秒钟发起约 7 次单次测量,对低频外设来说已经算比较密集,不要再同时挂大量慢速设备。
到这里,整条链路已经通了:系统时钟与 I2C 初始化完成后,sht30.c 发出测量命令,等待转换结束,读回 6 字节原始数据,换算成温湿度后经串口输出。剩下最关键的是数据转换公式和 CRC 校验,它们直接决定你看到的温度是否可靠。下一章把这一层细节彻底展开。
3.5 编译烧录与串口验证
拿到工程后在 Keil 里打开 MDK-ARM 目录下的 .uvprojx,首次编译需要确认 Device 芯片型号选的是 STM32F130ZET6。在 Options for Target 的 Debug 页选择你的下载器,我用的是 ST-Link,选 ST-Link Debugger 后进入 Settings 确认 SW 模式能识别到芯片。编译前检查 C/C++ 页的 Define 里有没有 STM32F103xE,这个宏影响 HAL 库对芯片存储容量的裁剪,缺了会导致部分外设的定义不一致,但 I2C 和串口通常不受影响。
烧录成功后,把板子用 USB-TTL 接到电脑,打开串口助手,波特率设 115200。正常现象是先打印一行 SHT30 init ok,之后每秒出现一行 temp 和 hum。如果只看到 init ok 但后续没有数据,说明 SHT30_ReadTempHum 的返回值一直是 0,问题多半出在测量命令发送或数据读取阶段,下一章的避坑清单会逐个分析这类现象。第一次跑通之后,你得到的就不再是 I2C 例程,而是一个可以自由改写的温湿度采集框架。
4. 数据解析与避坑实战:从原始字节到温湿度浮点值
4.1 测量数据格式与 CRC 校验
SHT30 返回的 6 字节数据不是简单的原码,每两个字节后面跟一个 CRC8 校验值。温度原始值是 16 位无符号数,湿度同理,校验多项式是 x^8 + x^5 + x^4 + 1,也就是常见的 CRC-8 多项式 0x31,初值 0xFF。例程里的校验函数长这样:
uint8_t SHT30_CheckCRC(uint8_t *data, uint8_t len) { uint8_t crc = 0xFF; for (uint8_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 0x80) crc = (crc << 1) ^ 0x31; else crc <<= 1; } } return crc; }调用时分别对前两个字节和后两个字节做校验,与 buf[2] 和 buf[5] 比对。如果温度校验失败,buf[2] 不等于 SHT30_CheckCRC(&buf[0], 2),说明温度数据在传输中被污染,应当直接丢弃本次读数;湿度校验同理。很多例程为了省代码省略了这一步,在信号质量差的环境里,偶尔跳变的 1 位就会造成温度偏差几十度。把 CRC 加进读取流程后,能拦截掉大部分坏数据,剩下的异常再交给滤波处理。
4.2 温湿度计算公式与边界条件
SHT30 手册给出了线性换算公式。16 位原始值 ST 对应 0~100%RH 的湿度范围,原始值 SRAW 与真实值的对应关系是线性映射:
float SHT30_CalcTemp(uint8_t msb, uint8_t lsb) { uint16_t raw = (msb << 8) | lsb; return -45.0f + 175.0f * (float)raw / 65535.0f; } float SHT30_CalcHum(uint8_t msb, uint8_t lsb) { uint16_t raw = (msb << 8) | lsb; return 100.0f * (float)raw / 65535.0f; }温度公式用 -45 和 175 两个常数,是因为 SHT30 的温度测量范围是 -40 到 125℃,对应原始值 0 到 65535 的线性映射,换算后实际输出范围比标称略宽。湿度公式更直接,原始值 0 对应 0%RH,65535 对应 100%RH。注意强转 float 的位置,如果先做整数除法再赋值,结果永远是 0。这是 C 语言例程里非常经典的翻车点。
边界情况也需要处理:当 raw 等于 0 时,温度理论值是 -45℃,但传感器实际最低只能测到 -40℃,此时输出会有偏差;当 raw 接近 65535 时,湿度会超过 100%RH,这两个边界值在高湿环境下偶尔会出现。如果应用对数据范围有严格要求,可以在转换后加一道饱和处理,把超过 100 的湿度强制钳到 100。例程里没有做钳位,保留原始计算值,方便使用者自行决定如何过滤。
4.3 避坑记录:读不到数据与数据异常的典型场景
以下是调试这套例程时最常见的四类问题,每一条我都实际踩过。
现象一:调用 SHT30_WriteCmd 后一直返回 HAL_TIMEOUT。原因是总线上根本没有设备应答。最常见的根源是 SHT30 的 ADDR 引脚电平和你代码里的地址不一致。代码里写死 0x44,但模块上 ADDR 被拉高,实际地址是 0x45。解决方法是先用万用表量 ADDR 引脚电平,再核对驱动里的宏定义,或者干脆把地址定义成可配置项。
现象二:SHT30_Init 成功,但读回来的 buf 全是 0x00。原因是 SDA 线没有正确连接到 MCU,或者连接点接触不良,导致主机读到的始终是低电平。我遇到过杜邦线虚焊,看起来插上了,实际金属端子没夹紧。解决的办法是重新压线并对着原理图逐脚量通断,而不是反复改代码。
现象三:读到的温湿度偶尔跳变到异常值,比如温度突然变成 -40℃。原因是数据在传输中产生了位错误,而例程没做 CRC 校验,坏数据被当成有效值参与计算。解决方法是把 4.1 小节的校验函数接入读取流程,校验失败时直接丢弃本次读数。加入校验后,这类异常基本绝迹。
现象四:运行一段时间后卡死在 HAL_I2C_Master_Receive。原因是 I2C 总线被某种异常状态锁住,从机拉低了 SCL 或 SDA,主机的轮询传输无法完成。常见诱因是传感器供电不稳,或者读数据过程中被中断优先级更高的事件打断,导致时序中断。解决方法是给读取流程增加重试机制,连续失败时发送软复位命令重新初始化 I2C,而不是让程序停留在等待超时的状态。
提示:排查 I2C 问题,逻辑分析仪比示波器更好用。把 SCL 和 SDA 同时接到分析仪上抓一次读写,能直接看到地址应答位、数据字节和时钟拉伸的实际情况,比靠猜快得多。
5. 二次开发与移植:把例程改成周期采样与多传感器应用
5.1 用状态机替代固定延时
例程里 HAL_Delay(30) 虽然简单,但每次测量 CPU 都要空等 30ms。改造的第一步是把测量命令和读数据拆开,用状态机管理采集流程:
typedef enum { SHT30_START = 0, SHT30_WAIT, SHT30_READ } sht30_state_t; sht30_state_t state = SHT30_START; uint8_t sensor_buf[6]; void SHT30_Task(void) { static uint32_t tick = 0; switch (state) { case SHT30_START: SHT30_WriteCmd(SHT30_CMD_MEAS_HIGH); tick = HAL_GetTick(); state = SHT30_WAIT; break; case SHT30_WAIT: if (HAL_GetTick() - tick >= 15) // 测量完成 state = SHT30_READ; break; case SHT30_READ: SHT30_ReadData(sensor_buf, 6); state = SHT30_START; break; } }改造后不需要阻塞延时,可以在 WAIT 状态里执行 LED 翻转、按键扫描之类的事务。状态机的核心条件是等待 15ms,这个时间来自高重复度测量时长,改用低重复度命令时记得同步改小。
5.2 挂多颗 SHT30 并切换地址
如果要做多点测量,硬件上有两种方案:一是把多颗传感器的 ADDR 引脚分别接 GND 和高电平,地址分别设为 0x44 和 0x45,这样一条 I2C 总线上最多挂两颗;二是给每颗传感器加 I2C 开关芯片,分时选通。软件上最简单的做法是把地址做成函数参数:
uint8_t SHT30_ReadTempHumAt(uint8_t addr, float *temp, float *hum) { uint8_t buf[6]; if (HAL_I2C_Master_Transmit(&hi2c1, addr << 1, cmd, 2, 100) != HAL_OK) return 0; HAL_Delay(30); if (HAL_I2C_Master_Receive(&hi2c1, addr << 1, buf, 6, 100) != HAL_OK) return 0; // 计算和校验 }调用时传 0x44 或 0x45,就能在一条总线上区分两颗传感器。注意在同一时刻只能有一个地址相同,如果两块模块的 ADDR 都接地,它们会同时响应,I2C 协议上会冲突。
5.3 数据滤波与异常标记
对温湿度数据做一次简单移动平均,比裸读数值稳定得多。我的做法是保留最近五次读数,取平均值输出,同时统计 CRC 失败次数,连续十次失败就置一个传感器异常标志,由上层决定是否报警或重启传感器。这套做法让采集从“能读到数”变成“稳定可信赖”。从那以后,我每次移植传感器例程,都强制把校验、重试和状态标志提前设计进去,而不是等数据跳了再去查时序。希望这些细节能帮你在自己的板子上少走几个弯路。
本文还有配套的精品资源,点击获取