简介:STM32驱动SHT3X温湿度传感器工程包,基于STM32F103C8T6微控制器,实现了对SHT30、SHT31等SHT3X系列数字温湿度传感器的数据采集,并通过串口输出当前环境温度与湿度值,适合嵌入式学习者、电子竞赛参赛者以及需要快速集成环境监测功能的开发者。整个zip压缩包包含72个文件,其中35个.h头文件与33个.c源文件构成了完整的代码框架,覆盖传感器驱动、串口通信、延时和系统配置等模块,另有启动文件与Keil工程文件辅助编译调试,资源总计292KB,轻量而完整。目前已有1095人学习,受到了嵌入式初学者的广泛关注。代码采用模块化设计,展示了SHT3X的启动配置、测量命令发送、响应数据解析与温湿度换算等关键步骤,同时融入UART串口通信的初始化与收发处理,能够帮助读者将理论知识点落到实际工程中,尤其适合对照数据手册逐行理解驱动原理,或作为项目基础进行二次开发。
1. SHT3X 驱动卡住,往往不是传感器的问题,而是 I2C 时序
一个 stm32 驱动 SHT3X 温湿度传感器的工程 zip 里,最容易被忽略的反而是keilkilll.bat和CORE目录下的启动文件。SHT3X(常用型号 SHT30)不像 DHT11 那样单总线时序相对宽松,它对 I2C 地址、起始条件、ACK/NACK 和 CRC 校验都很敏感。很多人第一次接触 SHT30 时,把传感器接好、程序烧进去,串口输出的却是 -45 度和 0%RH,问题往往出在读取到了无效 raw 值,或者传感器根本没有应答。这个项目把 SHT3X 驱动单独放在HARDWARE目录,与I2C_HAL底层解耦,最终通过串口打印温湿度,是标准外设库工程里非常适合拆解和移植的样例。
2. STM32 标准外设库工程骨架:SHT3X 挂在哪条总线上
2.1 zip 目录到底在说什么
拿到 zip 后,先别急着打开main.c。这个目录组织方式本身就能说明问题:
| 目录/文件 | 典型作用 |
|---|---|
CORE/core_cm3.c、startup_stm32f10x_hd.s | Cortex-M3 内核访问函数与中断向量表 |
SYSTEM/delay、usart、sys | 延时、串口驱动和 SysTick 辅助代码 |
HARDWARE/I2C_HAL、SHT3X | I2C 底层接口和 SHT3X 上层驱动 |
USER/main.c、stm32f10x_it.c | 主流程与中断服务函数 |
OBJ | Keil 编译输出目录 |
keilkilll.bat | 清理.o、.axf等构建产物,一般用于把工程压缩发出去之前瘦身 |
这种分层方式和 STM32 标准外设库教程里的常见套路一致:底层寄存器相关代码、中间层驱动、应用层main分开。keilkilll.bat的出现说明作者在打包前是直接删过OBJ中间文件的,所以拿到手后第一次编译会完整重建,这是正常现象,不是工程缺失。
需要注意startup_stm32f10x_hd.s。STM32F103C8T6 是 64KB Flash,属于 medium-density,更合适的启动文件是startup_stm32f10x_md.s。实际工程里用hd.s也能编译过、main也可能跑起来,但中断向量表会和芯片型号不完全对应,后续一旦用到 I2C 中断或者串口中断,就可能出现 HardFault。如果你要在自己的板子上复现,建议直接把CORE里的启动文件换成startup_stm32f10x_md.s,这也是一个最容易踩的坑。
2.2 SHT3X 的 I2C 地址、单次测量命令和数据格式
SHT3X 是 Sensirion SHT3x 系列的基础数字温湿度传感器,提供 I2C 接口。它的 7-bit 从机地址由 ADDR 引脚决定:ADDR 接 GND 时为0x44,接 VDD 时为0x45。在软件模拟 I2C 代码里,写地址一般是0x44 << 1即0x88,读地址是0x88 | 0x01即0x89。这个左移一位的操作很容易漏,漏了之后传感器完全无应答。
SHT3X 的命令是 16 位的,不像一些气体传感器那样先写寄存器地址再写数据。常用的单次测量命令是0x2C 0x06,对应高重复性测量。发送完命令后需要等待至少 15ms,然后主机发起读操作,连续读 6 字节:温度高字节、温度低字节、温度 CRC、湿度高字节、湿度低字节、湿度 CRC。两个 16-bit raw 值的换算关系如下:
| 物理量 | 换算公式 |
|---|---|
| 温度 | -45 + 175 * raw_t / 65535 |
| 湿度 | 100 * raw_h / 65535 |
因此只要 raw 值不正确,温度就可能落在 -45 度附近,这是判断 I2C 读出假数据的典型特征。
2.3 GPIO、时钟和 I2C 外设选型
这个项目里I2C_HAL目录的名称容易让人误以为是 ST 官方 HAL 库,实际上在标准外设库工程里,它通常是作者自己封装的一层软件模拟 I2C。常见做法是把 SCL 和 SDA 放在任意 GPIO 上,比如 PB6/PB7:
#define SHT3X_I2C_ADDR 0x44 #define SHT3X_CMD_MEAS 0x2C06 void SHT3X_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); }上面这段把 PB6/PB7 配置为开漏输出。开漏模式是 I2C 总线的基本要求,因为 ACK 信号需要从机把 SDA 拉低,主机必须释放总线。GPIO 外部需要有 4.7kΩ 左右的上拉电阻到 3.3V,很多 STM32 核心板上已经集成,但没有的话就会出现 SDA 一直为高、读不到数据的问题。
时钟方面,system_stm32f10x.c里的HSE_VALUE默认是 8000000,也就是 8MHz 外部晶振。如果板子上是 12MHz 晶振,而没有同步修改宏定义,SystemInit 计算的系统时钟不是 72MHz,串口波特率会错乱,delay 也会卡住。这部分和stm32 延时函数delay卡死热搜里大多数人遇到的情况一致,后面第 4 章会再展开。
3. 软件模拟 I2C 读写 SHT3X:ACK 位与 CRC 校验
3.1 软件模拟 I2C 的底层接口
STM32F103 的硬件 I2C 外设可以用,但在标准外设库环境下调试时,不少人会被 ERRATA 里的问题搞得头大。我一般更倾向在这个项目里先跑软件模拟 I2C,因为一眼能看出主机是在哪一步丢 ACK。
一个最小可用的 I2C 底层需要I2C_Start、I2C_Stop、I2C_WriteByte和I2C_ReadByte。核心代码可以这样写:
static void I2C_Start(void) { SDA_1; SCL_1; SDA_0; SCL_0; } static void I2C_Stop(void) { SCL_0; SDA_0; SCL_1; SDA_1; } static void I2C_WriteByte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { if (dat & 0x80) SDA_1; else SDA_0; dat <<= 1; SCL_1; SCL_0; } SDA_1; SCL_1; while (SDA_READ); // 等待从机 ACK SCL_0; } static uint8_t I2C_ReadByte(uint8_t ack) { uint8_t dat = 0; SDA_1; for (uint8_t i = 0; i < 8; i++) { dat <<= 1; SCL_1; if (SDA_READ) dat |= 0x01; SCL_0; } if (ack) SDA_0; // 发送 ACK else SDA_1; // 发送 NACK SCL_1; SCL_0; return dat; }这里的SDA_1、SDA_0、SCL_1、SCL_0是 GPIO 置位/清零宏,实际使用中可以根据I2C_HAL目录下的实现替换。I2C_WriteByte最后那段 ACK 等待需要加超时判断,否则一旦从机不拉低 SDA,代码会死循环。常见做法是把等待次数限制在 10 次左右,超过后返回错误。
I2C_ReadByte的ack参数表示主机在读完当前字节后回 ACK 还是 NACK。SHT3X 连续读取 6 字节时,前 5 字节要回 ACK,最后一个字节要回 NACK,否则传感器会认为主机还想继续读,总线时序就会错位。
3.2 发送 SHT3X 命令并读取 6 字节
有了底层接口后,SHT3X 的命令发送和读数据就可以封装成一层:
void SHT3X_SendCommand(uint16_t cmd) { I2C_Start(); I2C_WriteByte(SHT3X_I2C_ADDR << 1); // 写地址 0x88 I2C_WriteByte(cmd >> 8); I2C_WriteByte(cmd & 0xFF); I2C_Stop(); } uint8_t SHT3X_ReadTempHum(float *temp, float *hum) { uint8_t buf[6]; uint16_t raw_t, raw_h; SHT3X_SendCommand(SHT3X_CMD_MEAS); delay_ms(20); // 等待测量完成 I2C_Start(); I2C_WriteByte((SHT3X_I2C_ADDR << 1) | 0x01); // 读地址 0x89 for (int i = 0; i < 6; i++) { if (i == 5) buf[i] = I2C_ReadByte(0); // 最后一个字节回 NACK else buf[i] = I2C_ReadByte(1); // 前面字节回 ACK } I2C_Stop(); if (SHT3X_CRC8(&buf[0], 2) != buf[2]) return 2; // 温度 CRC 错误 if (SHT3X_CRC8(&buf[3], 2) != buf[5]) return 3; // 湿度 CRC 错误 raw_t = ((uint16_t)buf[0] << 8) | buf[1]; raw_h = ((uint16_t)buf[3] << 8) | buf[4]; *temp = -45.0f + 175.0f * raw_t / 65535.0f; *hum = 100.0f * raw_h / 65535.0f; return 0; }注意I2C_WriteByte在写读地址后,从机会回一个 ACK,这个 ACK 已经被I2C_WriteByte内部消费掉了。紧接着的I2C_ReadByte(1)才进入第一个数据字节。整个流程是:启动 → 写地址 → 写命令高字节 → 写命令低字节 → 停止 → 延时 → 启动 → 写读地址 → 读 6 字节 → 停止。如果省略掉中间的停止,或者把读地址错写成0x88,传感器就不会返回数据。
3.3 SHT3X 的 CRC-8 校验
SHT3X 的每个测量值后面带一个 CRC-8,多项式是0x31,初始值是0xFF,输入输出不反转。标准库工程里一般自己实现一个短函数就够了:
uint8_t SHT3X_CRC8(const uint8_t *data, uint8_t len) { uint8_t crc = 0xFF; while (len--) { crc ^= *data++; for (uint8_t i = 0; i < 8; i++) { if (crc & 0x80) crc = (crc << 1) ^ 0x31; else crc <<= 1; } } return crc; }这个 CRC 函数的参数是原始数据指针和长度。对温度来说,传入&buf[0]和2,返回值与buf[2]比较;对湿度来说,传入&buf[3]和2,返回值与buf[5]比较。如果忽略 CRC,只算换算公式,遇到偶发总线干扰时可能会看到温湿度突然跳变,而且没有提示。加上 CRC 后,即使数据错了,程序也知道是读失败,而不是把错误数据打印出去。
3.4 串口输出与 printf 重定向
项目里SYSTEM/usart已经提供了串口初始化的基础代码,通常用 USART1。为了让printf直接输出到串口,需要重写fputc:
int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }同时要在 Keil 的 Target 选项里勾选Use MicroLIB,否则printf会进半主机模式,程序运行到第一次打印就卡住。串口参数一般设置成115200 8-N-1,即波特率 115200、8 位数据、无校验、1 位停止位。这块在标准外设库例程里的配置方式很固定,唯一要确认的是USART1_Init里的波特率寄存器和HSE_VALUE是否匹配,否则打印出来全是乱码。
4. main.c 主流程、初始化顺序与串口输出排错
4.1 初始化顺序和主循环
驱动封装好之后,main.c要做的就是把时钟、延时、串口和 GPIO 按顺序初始化,然后循环读取。常见初始化顺序如下:
int main(void) { delay_init(); NVIC_Configuration(); USART1_Init(115200); SHT3X_GPIO_Config(); while (1) { float temp = 0.0f, hum = 0.0f; uint8_t ret = SHT3X_ReadTempHum(&temp, &hum); if (ret == 0) printf("Temp: %.2f C, Hum: %.2f %%RH\r\n", temp, hum); else printf("SHT3X read error: %d\r\n", ret); delay_ms(1000); } }delay_init()依赖 SysTick,而 SysTick 的时钟源又依赖系统时钟。如果系统时钟不是 72MHz,delay_us和delay_ms的延时时间就不准,可能表现为睡眠时间明显变长或卡死。NVIC_Configuration()不是必须的,如果整个工程没有用到中断,可以去掉。
主循环里每次读取间隔 1 秒,对 SHT3X 来说足够有余量。如果希望响应更快,可以改成 200ms,但要注意 SHT3X 在单次测量模式下,两次测量之间最好间隔 100ms 以上,避免传感器内部还没有完成就再次发命令。
4.2 串口输出温度 -45 度和数据全 0xFF 的排查
把常见故障列成表格,调试时可以按行对号入座:
| 串口输出特征 | 直接原因 | 排查方法 |
|---|---|---|
| 温度恒为 -45.0,湿度 0.0 | raw_t 和 raw_h 全为 0,通常是 I2C 读回来的字节不对 | 用逻辑分析仪抓 SCL/SDA,确认 6 字节都有数据 |
| 数据全是 0xFF | SDA 一直被上拉电阻拉高,主机没有读到有效位 | 检查 GPIO 是否开漏、外部上拉是否接 3.3V |
| 第一次读数正常,之后一直是 0xFF | 读最后字节没有回 NACK,导致总线状态错乱 | 确认I2C_ReadByte在最后一个字节时传入 NACK |
| 温湿度能读到,但 CRC 校验失败 | 总线速率过高,SHT3X 数据输出不稳定 | 软件模拟 I2C 里加入适当延时,SCL 频率降到 100kHz |
| 串口乱码 | 波特率寄存器配置和实际晶振不一致 | 检查HSE_VALUE是不是 8MHz,改用外部晶振后重新编译 |
这几种现象在stm32 温湿度传感器 dht11等老模块上不容易见到,因为 DHT11 没有 CRC,错了也只是数据偏差。SHT30 的 CRC 校验反而是最容易定位问题的入口:如果程序返回2或3,基本可以确定是电气时序问题,而不是传感器坏了。
4.3 delay 卡死和启动文件不匹配
很多人把工程下载到stm32f103c8t6上后发现delay_ms(1000)变成了死循环。这不是 delay 代码本身写错,而是SystemCoreClock变量和实际主频不一致。标准外设库的SystemInit会根据stm32f10x.h里的HSE_VALUE配置 PLL,如果外部晶振是 12MHz,而HSE_VALUE还是8000000,PLL 出来的频率就不是 72MHz,delay_init里fac_us = SystemCoreClock / 8000000计算出错,SysTick 重装载值可能变成 0 或者溢出。
启动文件不匹配也有类似表现。startup_stm32f10x_hd.s是针对高密度芯片的,用在 STM32F103C8T6 上时,中断向量表长度和地址会错位。程序进入 while 循环可能正常,但第一次进串口中断或者 SysTick 中断时,PC 指针跳到错误位置,看起来就是卡死。解决方法是把startup_stm32f10x_hd.s从工程里删除,加入startup_stm32f10x_md.s,并重新编译。这个坑在stm32 标准库新建工程的搜索结果里出现频率非常高,属于典型移植问题。
5. 把 SHT3X 驱动从工程里拆出来复用,并用逻辑分析仪验证时序
5.1 驱动拆成两层,只保留一个硬件接口文件
当你验证完这个 zip 里的工程,下一步通常是把它移到自己的项目里。建议不要把main.c里的代码直接复制粘贴,而是把驱动拆成sht3x.h和sht3x.c两个文件。sht3x.h里只暴露SHT3X_Init、SHT3X_ReadTempHum和必要的宏定义;sht3x.c内部调用I2C_HAL层。
这样做的原因是:不同板子的 SCL/SDA 引脚可能不同。如果在硬件上把 SCL 换到 PB8、SDA 换到 PB9,只需要修改SHT3X_GPIO_Config里的引脚宏,不需要动SHT3X_ReadTempHum里的时序逻辑。项目里HARDWARE目录把I2C_HAL和SHT3X分开,就是为这种复用准备的。
5.2 用串口打印原始 buffer 快速判断时序
在验证阶段,我建议在驱动里临时加一个打印函数,把读回来的 6 字节完整打印出来:
void SHT3X_PrintRaw(const uint8_t *buf) { printf("RAW %02X %02X %02X %02X %02X %02X\r\n", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]); }如果原始数据里温度和湿度字节一直保持不变,比如00 00 00 00 00 00,说明命令没有生效;如果是FF FF ...,说明 SDA 没有被从机驱动;如果 CRC 字节和计算值不符,说明时钟线干扰严重。
5.3 逻辑分析仪看 ACK/NACK 过界点
手头有逻辑分析仪时,直接抓 SCL 和 SDA 波形。重点看读序列:主机发送0x89后,从机会连续输出 6 个字节;在每个字节的第 9 个时钟,SDA 电平应当反映主机的 ACK/NACK。最后一个字节之后,如果 SDA 仍被拉低,说明你在最后调用了I2C_ReadByte(1)而不是I2C_ReadByte(0)。把那个参数改过来,然后再抓一次,确认最后一个字节对应的是高电平 NACK,总线上再产生停止条件,SHT3X 驱动就可以稳定复用了。
本文还有配套的精品资源,点击获取