1. 准备工作与环境搭建

大家好,今天我来分享一个在STM32上通过模拟IIC驱动SHT30温湿度传感器的完整实战过程。这个方案特别适合那些硬件资源受限或者需要灵活定制IIC时序的场景。我自己在多个项目中都用过这个方案,实测下来非常稳定可靠。

SHT30是一款高精度的数字温湿度传感器,采用IIC接口通信,测量范围广,精度高,非常适合各种环境监测应用。相比于常见的DHT11,SHT30的精度和稳定性都要好很多,当然价格也稍高一些。

在开始之前,你需要准备以下材料:

  • 一块STM32开发板(我用的STM32F103系列,其他系列也类似)
  • SHT30温湿度传感器模块
  • 杜邦线若干
  • 两个4.7kΩ的上拉电阻(非常重要!)
  • STM32开发环境(我用的Keil MDK,其他IDE也可以)

接线方面,SHT30的VCC接3.3V,GND接地,SCL和SDA分别接STM32的GPIO引脚。这里特别注意:IIC总线需要上拉电阻,否则通信会失败。我选择的是PB14作为SDA,PB15作为SCL,你也可以根据实际情况选择其他引脚。

2. GPIO配置与寄存器设置

配置GPIO是模拟IIC的第一步,也是很多新手容易出错的地方。因为我们要模拟IIC时序,所以需要把使用的GPIO配置为开漏输出模式,并且使能内部上拉或者外接上拉电阻。

我以STM32F103为例,使用PB14和PB15这两个引脚。在标准库中,配置代码如下:

void GPIO_Configuration(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    
    // SCL引脚配置
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;  // 开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    // SDA引脚配置
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    // 初始状态置高
    GPIO_SetBits(GPIOB, GPIO_Pin_14 | GPIO_Pin_15);
}

如果你用的是HAL库,配置方式会有些不同:

void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    // SCL引脚配置
    GPIO_InitStruct.Pin = GPIO_PIN_15;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    // SDA引脚配置
    GPIO_InitStruct.Pin = GPIO_PIN_14;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    // 初始状态置高
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14|GPIO_PIN_15, GPIO_PIN_SET);
}

这里有个重要的知识点:为什么IIC总线要使用开漏输出?因为IIC总线是多个设备共享的,开漏输出可以避免多个设备同时驱动总线时产生冲突。当总线空闲时,上拉电阻确保总线保持高电平。

如果你需要更改到其他引脚,比如PA5和PA6,需要注意寄存器配置的不同。对于GPIOA,引脚0-7由CRL寄存器控制,8-15由CRH寄存器控制。具体配置方法可以参考STM32参考手册中关于GPIO寄存器的说明。

3. IIC时序模拟详解

模拟IIC时序是整个项目的核心,也是最容易出问题的地方。IIC协议虽然简单,但对时序要求很严格。下面我把自己调试好的IIC驱动函数分享给大家,这些函数都是经过实际项目验证的。

首先是起始信号和停止信号:

// 产生IIC起始信号
void IIC_Start(void)
{
    SDA_OUT();     // 设置SDA为输出
    IIC_SDA = 1;
    IIC_SCL = 1;
    delay_us(4);     // 延时满足时序要求
    IIC_SDA = 0;    // START信号:当SCL为高时,SDA从高变低
    delay_us(4);
    IIC_SCL = 0;    // 钳住I2C总线,准备发送或接收数据
}

// 产生IIC停止信号
void IIC_Stop(void)
{
    SDA_OUT();    // 设置SDA为输出
    IIC_SCL = 0;
    IIC_SDA = 0;    // STOP信号:当SCL为高时,SDA从低变高
    delay_us(4);
    IIC_SCL = 1;
    IIC_SDA = 1;
    delay_us(4);
}

接下来是发送一个字节的函数:

// IIC发送一个字节
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
    SDA_OUT();    // 设置SDA为输出
    
    IIC_SCL = 0;    // 拉低时钟开始数据传输
    
    for(t = 0; t < 8; t++)
    {              
        if((txd & 0x80) >> 7)
            IIC_SDA = 1;
        else
            IIC_SDA = 0;
            
        txd <<= 1;       
        delay_us(2);   // 延时满足时序要求
        IIC_SCL = 1;
        delay_us(2); 
        IIC_SCL = 0;    
        delay_us(2);
    }     
}

读取一个字节的函数:

// 读取一个字节
u8 IIC_Read_Byte(unsigned char ack)
{
    unsigned char i, receive = 0;
    
    SDA_IN();    // 设置SDA为输入
    
    for(i = 0; i < 8; i++)
    {
        IIC_SCL = 0; 
        delay_us(2);
        IIC_SCL = 1;
        receive <<= 1;
        
        if(READ_SDA) 
            receive++;   
            
        delay_us(1); 
    }                     
    
    if (!ack)
        IIC_NAck();    // 发送nACK
    else
        IIC_Ack();     // 发送ACK   
        
    return receive;
}

等待应答信号的函数:

// 等待应答信号
u8 IIC_Wait_Ack(void)
{
    u8 ucErrTime = 0;
    
    SDA_IN();       // SDA设置为输入  
    IIC_SDA = 1;
    delay_us(1);       
    IIC_SCL = 1;
    delay_us(1);     
    
    while(READ_SDA)
    {
        ucErrTime++;
        if(ucErrTime > 250)
        {
            IIC_Stop();
            return 1;
        }
    }
    
    IIC_SCL = 0;    // 时钟输出0        
    return 0;  
}

在实际调试过程中,我发现延时时间对IIC通信的成功率影响很大。不同的STM32主频需要调整延时时间,我提供的延时参数是针对72MHz主频优化的。如果你的系统主频不同,需要适当调整这些延时参数。

4. SHT30驱动实现

有了IIC基础函数,接下来就可以实现SHT30的具体驱动了。SHT30的通信过程相对简单,主要是发送测量命令,然后读取测量结果。

首先定义一些必要的宏:

#define SHT30_ADDR 0x44    // SHT30的IIC地址
#define IIC_Write 0x00     // 写模式
#define IIC_Read 0x01      // 读模式

// 测量命令
#define CMD_MEASUREMENT 0x2C06

SHT30的初始化函数:

void SHT30_Init(void)
{
    GPIO_Configuration();    // 初始化GPIO
    delay_init();           // 初始化延时函数
}

读取温湿度的核心函数:

void SHT30_Read_Result(void)
{
    u8 buff[6];
    u16 temp_raw, hum_raw;
    float temperature, humidity;
    
    // 发送测量命令
    IIC_Start();
    IIC_Send_Byte(SHT30_ADDR << 1 | IIC_Write);
    IIC_Wait_Ack();
    IIC_Send_Byte(0x2C);
    IIC_Wait_Ack();
    IIC_Send_Byte(0x06);
    IIC_Wait_Ack();
    IIC_Stop();
    
    // 等待测量完成
    delay_ms(50);
    
    // 读取测量结果
    IIC_Start();
    IIC_Send_Byte(SHT30_ADDR << 1 | IIC_Read);
    
    if(IIC_Wait_Ack() == 0)
    {
        buff[0] = IIC_Read_Byte(1);    // 温度高8位
        buff[1] = IIC_Read_Byte(1);    // 温度低8位
        buff[2] = IIC_Read_Byte(1);    // 温度CRC校验
        buff[3] = IIC_Read_Byte(1);    // 湿度高8位
        buff[4] = IIC_Read_Byte(1);    // 湿度低8位
        buff[5] = IIC_Read_Byte(0);    // 湿度CRC校验
        
        IIC_Stop();
        
        // 数据拼接
        temp_raw = (buff[0] << 8) | buff[1];
        hum_raw = (buff[3] << 8) | buff[4];
        
        // 转换为实际值
        temperature = -45 + 175 * (float)temp_raw / 65535.0;
        humidity = 100 * (float)hum_raw / 65535.0;
        
        // 这里可以添加数据处理和输出代码
        printf("Temperature: %.2f C, Humidity: %.2f %%\r\n", temperature, humidity);
    }
}

在实际使用中,我建议添加CRC校验功能来确保数据的准确性。SHT30每个数据帧都带有CRC校验码,我们可以通过校验来排除传输错误。

CRC校验函数:

u8 SHT30_Check_Crc(u8 *data, u8 nbrOfBytes, u8 checksum)
{
    u8 crc = 0xFF;
    u8 byteCtr, bit;
    
    for(byteCtr = 0; byteCtr < nbrOfBytes; byteCtr++)
    {
        crc ^= data[byteCtr];
        
        for(bit = 8; bit > 0; bit--)
        {
            if(crc & 0x80)
            {
                crc = (crc << 1) ^ 0x31;
            }
            else
            {
                crc = (crc << 1);
            }
        }
    }
    
    return (crc == checksum) ? 0 : 1;
}

5. 常见问题与调试技巧

在调试SHT30的过程中,我遇到过不少问题,这里分享几个常见的坑和解决方法。

第一个常见问题是IIC通信完全没有响应。这时候首先要检查硬件连接:电源是否正常,上拉电阻是否接好,引脚连接是否正确。可以用示波器或者逻辑分析仪观察IIC总线上的波形,看看是否有起始信号和地址传输。

如果没有专业仪器,也可以用简单的办法:用LED指示灯来监控总线状态,或者通过串口打印调试信息。我曾经用这种方法解决过一个棘手的通信问题,发现是因为上拉电阻阻值太大导致上升沿太慢。

第二个问题是能通信但读取的数据全为0。这通常是时序问题,特别是延时时间不准确。不同主频的STM32需要调整延时参数,我建议先用示波器测量实际波形,然后对照SHT30的时序要求调整延时时间。

第三个问题是数据偶尔出错。这时候需要添加CRC校验功能,确保数据的完整性。我在一个工业项目中就遇到过因为电磁干扰导致数据出错的情况,添加CRC校验后问题就解决了。

第四个问题是功耗方面的考虑。如果需要低功耗应用,可以在不测量时将SHT30设置为休眠模式。SHT30的功耗本来就很低,但进一步优化可以延长电池供电设备的续航时间。

调试时我习惯添加详细的日志输出,这样能快速定位问题所在:

#define DEBUG_SHT30 1

#if DEBUG_SHT30
#define SHT30_DEBUG(...) printf(__VA_ARGS__)
#else
#define SHT30_DEBUG(...)
#endif

void SHT30_Read_Result(void)
{
    // ... 其他代码不变
    
    #if DEBUG_SHT30
    printf("Raw data: ");
    for(int i = 0; i < 6; i++)
    {
        printf("%02X ", buff[i]);
    }
    printf("\r\n");
    #endif
    
    // ... 其他代码不变
}

6. 实际应用与优化建议

在实际项目中使用SHT30时,有几个优化技巧值得分享。首先是测量频率的优化,SHT30支持不同的测量模式,包括单次测量和周期测量。对于大多数应用,单次测量模式就足够了,功耗也更低。

如果需要更高精度的测量,可以在读取数据后进行软件滤波。我常用的方法是通过多次测量取平均值,或者使用滑动平均滤波:

#define FILTER_SIZE 10

float temperature_filter[FILTER_SIZE] = {0};
float humidity_filter[FILTER_SIZE] = {0};
u8 filter_index = 0;

void SHT30_Update_Filter(float temp, float hum)
{
    temperature_filter[filter_index] = temp;
    humidity_filter[filter_index] = hum;
    filter_index = (filter_index + 1) % FILTER_SIZE;
}

float SHT30_Get_Filtered_Temperature(void)
{
    float sum = 0;
    for(int i = 0; i < FILTER_SIZE; i++)
    {
        sum += temperature_filter[i];
    }
    return sum / FILTER_SIZE;
}

另一个优化点是功耗。如果使用电池供电,可以通过间歇测量的方式降低平均功耗:

void SHT30_Low_Power_Mode(void)
{
    // 进入低功耗模式
    IIC_Start();
    IIC_Send_Byte(SHT30_ADDR << 1 | IIC_Write);
    IIC_Wait_Ack();
    IIC_Send_Byte(0xB0);
    IIC_Wait_Ack();
    IIC_Send_Byte(0x98);
    IIC_Wait_Ack();
    IIC_Stop();
}

对于需要长期运行的应用,我还建议添加自检功能,定期检查传感器是否正常工作:

u8 SHT30_Self_Test(void)
{
    u8 status[2];
    
    // 读取状态寄存器
    IIC_Start();
    IIC_Send_Byte(SHT30_ADDR << 1 | IIC_Write);
    IIC_Wait_Ack();
    IIC_Send_Byte(0xF3);
    IIC_Wait_Ack();
    IIC_Send_Byte(0x2D);
    IIC_Wait_Ack();
    
    IIC_Start();
    IIC_Send_Byte(SHT30_ADDR << 1 | IIC_Read);
    IIC_Wait_Ack();
    
    status[0] = IIC_Read_Byte(1);  // 状态高字节
    status[1] = IIC_Read_Byte(0);  // 状态低字节
    
    IIC_Stop();
    
    // 检查报警状态和校验位
    if((status[1] & 0x02) || (status[1] & 0x04))
    {
        return 0;  // 传感器异常
    }
    
    return 1;  // 传感器正常
}

最后,在工程实践中,我建议将SHT30的驱动封装成独立的模块,提供清晰的接口函数,这样在不同的项目中可以快速移植和重用。

通过这个完整的实战指南,你应该能够在STM32上成功驱动SHT30温湿度传感器了。在实际项目中,我用的就是这个方案,稳定性很好,精度也满足要求。

更多推荐