立创开源链接:https://oshwhub.com/vrxiaojie/temperature-and-humidity-meter-7508164a

1、介绍

在日常生活中,无论是家庭、办公室还是工作室,适宜的温湿度环境对于人体舒适度、工作效率乃至家居设备的保养都至关重要。然而,传统的温湿度计存在精度不足、需频繁更换一次性电池等问题,既不方便也不环保。

于是,我萌生了设计一款集低功耗、高精度、低成本于一身的桌面温湿度计的想法。这款产品的核心优势在于它巧妙地平衡了性能与成本,提供一个既实用又环保的温湿度解决方案。它具有以下特点:

(1)低功耗:考虑到长期使用的需求,本次设计选择低功耗的硬件组件和优化的软件算法。STM32G030K6T6作为MCU,以其出色的能效比和丰富的外设资源,成为这款传感器的理想之选。通过精细的电源管理和休眠模式设计,能大幅度降低能耗,延长单次充电后的使用时间。

(2)高精度测量:为了确保温湿度数据的准确性,本次设计选用了瑞士盛思锐SHT40作为温湿度传感器。SHT40以其卓越的测量精度和稳定性著称,能够在广泛的温度和湿度范围内提供可靠的数据。

(3)低成本:通过精选高性价比的元器件和优化设计方案,温湿度传感器成本整体控制在30元以内 (不含运费,立创商城价格)

(4)可充电:采用18650锂电池作为电源,不仅避免了频繁更换电池带来的不便,还减少了废旧电池对环境的污染。

2、硬件部分

2.1电源部分

电源框图如下图所示:

电源框图.png
设计思路

官方设计是使用的两节1.5V干电池,由于需要经常更换电池,废弃电池也会造成污染,因此考虑使用锂电池供电。

经过查阅数据手册得知,STM32G030K6T6供电范围 2V-3.6V、SHT40 1.08V-3.6V、而锂电池满电时能有4V以上,超过了它们允许的最大允许的电源电压,因此需要采用先升压后降压的方式,稳定电压在3.3V。

因为要兼顾整机功耗和成本,经过挑选,使用MT3608B做升压、TLV70233做降压。

其中,MT3608B在电流小于100mA时的效率约92%。其输出电压是使用电阻分压反馈方式,VOUT=(1+R2/R1) * VREF。手册里写VREF=0.6V,我取R2=91KΩ,R1=13KΩ得到VOUT=4.8V。这里两个分压电阻大一点好,这样流过它们的电流小,它们所耗的功率也会变小。这里的电感4.7uH是按芯片手册来的,建议选择一个等效直流电阻更低的电感,这样也能提高效率。这里的续流二极管必选肖特基二极管,建议用SS14就行,我用SS34是因为手头上正好有这个。

image.png

Layout注意事项

输入端的电容C15要尽可能贴近DC-DC芯片放置,而输出端C14要与芯片地的回路最短。

image.png

两个反馈电阻要远离电感,避免高频干扰

image.png

LDO这边,输入和输出的滤波电容需要贴近LDO放置

image.png

2.2 温湿度显示

设计思路

温湿度分别显示在两片共阴极数码管上。数码管通过三个74HC595移位寄存器控制。移位寄存器的功能框图如下图所示

image.png
两个数码管的阴极总共是6个阴极,全部连接在其中一个SN74HC595上,通过这一个595芯片,可以指定某一个位导通,同时,两个数码管的阳极,又分别连接在另外两个595芯片上,通过这两个芯片配合,就可以实现单个位显示数据。

image.png

Layout注意事项

电源滤波电容靠近移位寄存器VCC放置

image.png

2.3 STM32外围电路

(1)ADC电压采集

使用了两个10KΩ薄膜电阻串联分压,其精度误差为±0.1%,可以基本保证电压采样的准确性。采样点在中间位置,最终需要在程序内乘以2,才能得到近似准确的电池电压。例如由程序ADC转换后的电压为1.92V,则实际电压为3.84V。旁路增加一个100nF电容接地也是为了消除一些干扰。

image.png

(2)按键中断唤醒

使用一个按键开关实现,一端接STM32的WAKE引脚(需要自己定义),一端接地。当按下后,WAKE引脚从高电平到低电平转换,就产生了下降沿,只需要读到这个下降沿即可执行后续的中断操作,这在后面软件设计部分会细说。
image.png

2.4 SHT40模块

设计思路

立创商城中可以买到SHT40模块,它带有一个滤波电容和传感器,可以发现少了两个I2C通信线上的上拉电阻,我们需要在原理图加上

image.png

image.png

原理图注意事项

请注意原理图中SCL与SDA的位置,一定要根据模块的引脚功能仔细对照!我设计的板子是将传感器朝上放置,如果你想改为朝下放置,则需要改原理图,将引脚镜像对调。

传感器模块各引脚定义如下图所示
ABEEB11A2AFD5237CEABF774795440C9.jpg

Layout注意事项

SHT40这个传感器真的非常非常灵敏,在布局时需要让模块尽量远离发热器件,如充电模块、STM32等,因此我将其布局在了PCB的右上角,底下不铺铜,以减少其他器件散热对它造成的影响。在阻焊颜色选择上,我选用吸热较少的白色,同样是为了减少外界对传感器的影响。

3、软件部分

3.1 软件实现

部分软件代码参考官方项目文档
代码已开源并上传到附件

3.1.1数码管显示数字

数码管本身不含有任何控制单元,它只是由几个有序排列的LED组成的器件,所以我们需要控制移位寄存器74HC595,通过它来驱动数码管显示数字。74HC595的时序图如下图所示

image.png

从时序图可以知道74HC595的使用流程为:

①拉高SCLR(10脚)。如果不用,设计原理图时可以直接拉高。

②控制SI(14引脚)、SCK(11脚)把移位寄存器的值赋好(使用8个上升沿)。

③给RCK(12引脚)一个上升沿。

④拉低G(13脚)。

首先定义函数SN74HC595_Send_Data,它有两个参数:sn_num: 表示选择哪个数码管或设备,值有SN_LED1SN_LED2SN_DIGsendValue: 需要发送的数据。
这里sn_num一共有三种情况,只挑值为SN_LED1的来讨论,其他两个内容都是类似的

image.png

①选择第一个数码管 (SN_LED1):
c if(sn_num == SN_LED1)

②使用for循环遍历8次(因为74HC595是8位移位寄存器):
c for(i = 0; i < 8; i++)

③检查sendValue的每一位,如果该位是1,则设置相应的引脚为高电平,否则为低电平:
c if(((sendValue << i) & 0x80) != 0)

④产生一个SCLK上升沿,时钟信号用于将数据从串行输入移位到寄存器中:
c HAL_GPIO_WritePin(LED1_SCLK_GPIO_Port, LED1_SCLK_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(LED1_SCLK_GPIO_Port, LED1_SCLK_Pin, GPIO_PIN_SET);

⑤在循环结束后,产生一个RCLK上升沿,用于将移位寄存器中的数据锁存到输出寄存器中:
c HAL_GPIO_WritePin(LED1_RCLK_GPIO_Port, LED1_RCLK_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(LED1_RCLK_GPIO_Port, LED1_RCLK_Pin, GPIO_PIN_SET);

3.1.2 ADC采集电池电压

前面硬件设计时提到过,用两个10KΩ薄膜电阻实现分压,STM32采集中间点的电压值,将其转换为ADC值,然后经过一系列运算最终得到电压的准确值。

首先要在STM32CubeMX中配置好ADC引脚,点击PB1引脚,选择ADC1_IN9

image.png

来到NVIC中,使能ADC1中断

image.png

再点进Code Generation,按如图所示勾选即可

image.png

最后点击GENERATE CODE,打开Keil项目,开始编写ADC代码

这里将ADC代码拆分成三块讲解

image.png

①调用HAL库函数,初始化、启动ADC转换,并等待其转换完成

②判断是否获取到数值,由于STM32G030K6T6是12bit ADC,也就是会有2的12次方个ADC数值,ADC数值从0到4095。将其归一化处理,并乘上STM32的输入电压。输入电压我用3.324V是拿万用表测得的,不需要太精确的话写3.3就可以了。归一化后的数据即为一半的电池电压值,变量为Data,比如说1.92V。

③由于需要显示在数码管上,我们把Data放大100倍再乘以2,这样就得到了一个整数,比如384,可以用取十进制数每一位的值的方法,得到三个数:3 8 4,并赋值给device_paramter结构体中的成员Voltage数组
main.h头文件中,device_paramter结构体定义如下

struct DEVICE_PARAMTER
{
	volatile uint8_t KeyStatus;
	volatile uint8_t sleepStatus;
	uint16_t Temp;
	uint16_t Humi;
	uint8_t Voltage[3];
};

在gpio.c文件中,编写了一个显示电压的函数,这样就方便一键调用了

void ShowVoltage(){
	SN74HC595_Send_Data(SN_DIG,0xFE);
	SN74HC595_Send_Data(SN_LED1,sgh_value[device_paramter.Voltage[0]]|0x80);
	SysCtlDelay(1000);
	SN74HC595_Send_Data(SN_LED1,0x00);	//消影,防止错位
	SN74HC595_Send_Data(SN_DIG,0xFD);
	SN74HC595_Send_Data(SN_LED1,(sgh_value[device_paramter.Voltage[1]]));
	SysCtlDelay(1000);
	SN74HC595_Send_Data(SN_LED1,0x00);	//消影,防止错位
	SN74HC595_Send_Data(SN_DIG,0xFB);
	SN74HC595_Send_Data(SN_LED1,sgh_value[device_paramter.Voltage[2]]);
	SysCtlDelay(1000);
	SN74HC595_Send_Data(SN_LED1,0x00);	//消影,防止错位
}

3.1.3 模拟I2C(软件I2C)

由于我在设计时把SCL和SDA画反(目前EDA编辑器里的板子已更正,设计为传感器模块正面向上插入),导致我需要飞线或者用软件I2C解决通信问题。经过查阅大量资料、参考代码,我自行修改并匹配了本项目所使用的传感器和MCU,实现了使用HAL库模拟I2C通信。如果你和我一样无法直接使用硬件I2C,别着急,只需要修改softiic.h内SDA和SCL对应的GPIO即可。

image.png

由于软件I2C篇幅较长,所以放到第3.2节细讲

3.1.4 中断唤醒

(1)STM32CubeMX参数设置

打开STM32CubeMX,设置PB5为GPIO_EXTI5,GPIO模式选择“下降沿触发检测的外部中断”,GPIO上拉,命名标签为WAKE_KEY

image.png

在NVIC中,设置EXTI line 4 to 15 interrupts使能并将优先级设为1;然后进到Code generation中,勾选对应的生成代码等复选框

image.png
image.png

在Timers- TIM14中使能TIM14 全局中断

image.png

OK,点击右上角的生成代码,打开Keil,编辑代码。

(2)中断代码

①本段程序在tim.c中,是回调函数。当定时器被触发时,HAL库自动调用该段函数。
函数主要做了两件事:显示温湿度、显示电池电压。变量flag用于计时以及判断是否该调整休眠标志sleep_flag

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance == TIM14)
	{
		HAL_TIM_Base_Stop_IT(&htim14);
		updata_flag++;
		if(updata_flag <= 1000)
		{
			ShowNum(1,1,(device_paramter.Temp/100));
			ShowNum(1,2,(device_paramter.Temp / 10 % 10));
			ShowNum(1,3,device_paramter.Temp%10);
			ShowNum(2,1,(device_paramter.Humi/100));
			ShowNum(2,2,(device_paramter.Humi / 10 % 10));
			ShowNum(2,3,device_paramter.Humi%10);
		}
		else if(updata_flag <= 2000)
		{
			ShowVoltage();
		}
		else
		{
			updata_flag = 0;
			sleep_flag++;
		}
		
		__HAL_TIM_SetCounter(&htim14,0);
		if(sleep_flag >= 1)
		{
			sleep_flag = 0;
			device_paramter.sleepStatus = 1;
			SN74HC595_Send_Data(SN_DIG,0xFF);
			SN74HC595_Send_Data(SN_LED1,0x00);
			SN74HC595_Send_Data(SN_LED2,0x00);
		}
		else{
			HAL_TIM_Base_Start_IT(&htim14);          
		}			
	}
}

②本段程序在main.cwhile(1)循环中,这里把SHT40温湿度采集与ADC电压采集的代码删掉,只保留该节要讲的内容。

循环一直判断按键的状态,当读取到PB5引脚为低电平(GPIO_PIN_REST)时,进入下一级循环,然后执行HAL_TIM_Base_Start_IT()启动定时器,它会自动调用上面的HAL_TIM_PeriodElapsedCallback,待回调函数内执行完毕,睡眠标志到来时,将退出回调函数,回到这一段代码,继续把休眠标志和按下标志清除掉,MCU进入休眠,可以进行下一次唤醒。


if(device_paramter.KeyStatus == KEY_SHAKE_STATE)
{
    HAL_Delay(10);
    if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5) == GPIO_PIN_RESET)
    {
        while(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5) == GPIO_PIN_RESET);

        /* 
        SHT40采集温湿度
        */
        /* 
        ADC采集电压
        */

        HAL_TIM_Base_Start_IT(&htim14);         	//开始定时器,显示数据 
        device_paramter.sleepStatus = 0;						//清除休眠标志
        device_paramter.KeyStatus = KEY_NO_PRESS;	//清除按下标志

    }
}
else if(device_paramter.sleepStatus == 1)		//显示结束,进入休眠
{
    HAL_SuspendTick();	//暂停滴答定时器,防止通过滴答定时器中断唤醒
    HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);  //进入停止模式
}

3.2 软件I2C详解

3.2.1 为什么要用软件I2C?

I2C通信协议是本项目的关键所在,SHT40支持I2C通信,只有搞懂I2C通信的全过程,才能给SHT40传感器发送读数据命令、正确接收温湿度数据。据说STM32的硬件I2C存在bug,再加上本人被迫需要学习软件I2C,因此花了一天的时间来学习调试,在此记录、解析一下I2C的通信过程及其如何实现获取SHT40的温湿度数据的。

3.2.2 I2C通信原理解释

(1)空闲状态

当SCL和SDA都为高电平时

image.png
(2)起始、结束条件

起始条件:当SCL为高电平时,SDA有一个下降沿

结束条件:当SCL为高电平时,SDA有一个上升沿

image.png

(3)从机地址、寄存器地址

I2C通信就如同广播找人一样,你叫到某个人的名字(从机的地址),它才会给你回应。因此,需要在开始条件后紧跟着发送一个从机地址。从机地址由7bit组成,第8位是读写标志位,0表示写,1表示读

image.png

举例:当你广播找人,找到了那个人(从机),他回应你了(ACK),你要他口袋里(寄存器地址)的钥匙(数据)
(4)字节格式
开始条件之后,每8个bit为一个字节传输,当SCL为高电平时,SDA也是高电平,此时代表逻辑1;SCL为高电平,SDA是低电平时,代表逻辑0。
image.png

(5)应答位

I2C的数据都是以8bit传送的,发送器每发送一个字节,就在时钟脉冲9期间释放数据线SDA,由接收器反应一个应答信号。当应答信号为低电平时,规定为有效应答位(ACK),表示接收器已经成功接收该字节。当应答信号为高电平时,规定为非应答位(NACK),表示接收器接收该字节没有成功。

举个例子:这就像是你给别人说你的电话号码,你一次性把11位电话号说给他,他可能一下子记不住,你分成三段,第一段说139,他说 好的(有效应答位ACK);第二段你说1234,他说 嗯(有效应答位ACK);第三段你说5678,这时一辆大车鸣笛经过,淹没了你的声音,他就没听清,没回应你(非应答位NACK),此时你看他没反应,你就要再说一遍5678,他回答好的(有效应答位ACK)。

3.2.3 I2C通信过程

(1)主机写入数据的流程

image.png

(2)主机读取数据的流程

image.png

3.2.4 I2C代码实现方法

有了上述理论基础,就可以开始编写代码了。这里我单独创建了一个C源文件叫softiic.c,下面将逐个函数讲解其作用。

(1)使用HAL库时自定义的delay_us函数

由于HAL库官方给的HAL_Delay是以ms级别的,我们要产生模拟I2C信号需要有延迟,通常是5us。ST论坛有大佬给出了解决方法,这里就直接照搬了。注意修改时钟主频为STM32G030K6T6的64MHz

#define CPU_FREQUENCY_MHZ    64		// STM32时钟主频 MHz
void delay_us(__IO uint32_t delay)
{
    int last, curr, val;
    int temp;

    while (delay != 0)
    {
        temp = delay > 900 ? 900 : delay;
        last = SysTick->VAL;
        curr = last - CPU_FREQUENCY_MHZ * temp;
        if (curr >= 0)
        {
            do
            {
                val = SysTick->VAL;
            }
            while ((val < last) && (val >= curr));
        }
        else
        {
            curr += CPU_FREQUENCY_MHZ * 1000;
            do
            {
                val = SysTick->VAL;
            }
            while ((val <= last) || (val > curr));
        }
        delay -= temp;
    }
}

(2)配置切换SDA引脚输入或输出模式的函数

使用HAL库的配置方法。注意,输出模式为开漏(OD)输出

static void SDA_OUT(void){
    GPIO_InitTypeDef SOFT_IIC_GPIO_STRUCT;
    SOFT_IIC_GPIO_STRUCT.Mode = GPIO_MODE_OUTPUT_OD;
    SOFT_IIC_GPIO_STRUCT.Pin = SDA_PIN;
    SOFT_IIC_GPIO_STRUCT.Speed = GPIO_SPEED_FREQ_HIGH;

    HAL_GPIO_Init(SDA_PORT, &SOFT_IIC_GPIO_STRUCT);
}

static void SDA_IN(void)
{
    GPIO_InitTypeDef SOFT_IIC_GPIO_STRUCT;
    SOFT_IIC_GPIO_STRUCT.Mode = GPIO_MODE_INPUT;
    SOFT_IIC_GPIO_STRUCT.Pin = SDA_PIN;
    SOFT_IIC_GPIO_STRUCT.Speed = GPIO_SPEED_FREQ_HIGH;

    HAL_GPIO_Init(SDA_PORT, &SOFT_IIC_GPIO_STRUCT);
}

(3)I2C延迟时间定义

前面有了自定义函数delay_us,这里我们填5,意味着每次调用函数,延迟5us

void IIC_Delay(void) {
	delay_us(5);
}

(4)I2C开始信号

先将SDA设置为输出模式,SDA和SCL同时为高电平(空闲状态),经过一点延时,SDA拉低,SCL过5us后再拉低,完成开始信号的设置。

void IIC_Start(void) {
    SDA_OUT();
    SDA_HIGH();
    SCL_HIGH();
    IIC_Delay();
    SDA_LOW();
    IIC_Delay();
    SCL_LOW();
}

(5)I2C结束信号

先将SDA设置为输出模式,SDA和SCL同时为低电平,经过一点延时,SCL先拉高,SDA过5us后再拉高,完成结束信号的设置。

void IIC_Stop(void) {
    SDA_OUT();
    SCL_LOW();
    SDA_LOW();
    IIC_Delay();
    SCL_HIGH();
    IIC_Delay();
    SDA_HIGH();
    IIC_Delay();
}

(6)应答位

这里主要是IIC_Wait_Ack函数需要讲一下,IIC_Ack和IIC_NAck是模拟主机发送应答或非应答信号,看着前面I2C的定义图应该很清楚,这里就不逐行啰嗦了。

IIC_Wait_Ack函数中,要将SDA设置为输入模式,以便于读取SDA信号线上电平的变化,其中READ_SDA()是由宏定义为 HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN),如果高电平,则返回值为1;若低电平,返回值为0。回顾一下,ACK是要求从机有一个拉低SDA的操作,即READ_SDA()返回值为0,方可跳出while循环,继续下面拉低SCL的操作。

在循环内,wait变量是用来判断是否超时,只要超过200个周期,就判定超时,停止I2C通信.

uint8_t IIC_Wait_Ack(void)
{
    uint8_t wait;
    SDA_IN();
		IIC_Delay();
		SCL_HIGH();
		IIC_Delay();
    while (READ_SDA())
    {
        wait++;
        if (wait > 200)
        {
            IIC_Stop();
            return 1;
        }
    }
    SCL_LOW();
    return 0;
}

void IIC_Ack(void) {
    SCL_LOW();
    SDA_OUT();
    SDA_LOW();
    IIC_Delay();
    SCL_HIGH();
    IIC_Delay();
    SCL_LOW();
}


void IIC_NAck(void) {
    SCL_LOW();
    SDA_OUT();
    SDA_HIGH();
    IIC_Delay();
    SCL_HIGH();
    IIC_Delay();
    SCL_LOW();
}

(7)I2C发送一个字节

这里就不一行行去解读了,主要看一下for循环里面的内容在干什么。

①因为一个字节包含8位,而I2C通信中数据是以位为单位传输的。因此,for (uint8_t i = 0; i < 8; i++) ,从0到7遍历一个字节的每一位。

②将byte向左移动i位,这样原本的第i位就移动到了最高位(即第7位,对应于十六进制的0x80)。然后,使用&操作符与0x80进行按位与操作。如果结果为非零(即true),说明byte的第i位是1;如果结果为0(即false),则第i位是0。

③设置SDA线的电平:根据上一步的结果,通过SDA_HIGH()SDA_LOW()宏或函数来设置SDA线的电平。如果当前位是1,则调用SDA_HIGH()将SDA线设置为高电平;如果当前位是0,则调用SDA_LOW()将SDA线设置为低电平。这样,就按照byte变量的值,一位一位地将数据通过SDA线发送出去。

void IIC_Send_Byte(uint8_t byte) {
    SDA_OUT();
		SCL_LOW();
    for (uint8_t i = 0; i < 8; i++) {
        if ((byte << i) & 0x80) {
            SDA_HIGH();
        } else {
            SDA_LOW();
        }
        IIC_Delay();
        SCL_HIGH();
        IIC_Delay();
        SCL_LOW();
        IIC_Delay();
    }
}

(8)I2C接收一个字节

与发送一字节类似,可以对照着来看,这里就不再啰嗦了。

uint8_t IIC_Read_Byte(uint8_t ack) {
    uint8_t byte = 0;
    SDA_IN();
    for (uint8_t i = 0; i < 8; i++) {
        SCL_LOW();
        IIC_Delay();
        SCL_HIGH();
        byte <<= 1;
        if (READ_SDA()) {
            byte |= 0x01;
        }
        IIC_Delay();
    }
    return byte;
}

3.2.5 实现与温湿度传感器的I2C通信

这一块主要包含softiic.c中的3个函数,以及一个在main.c中调用的函数块。

SHT40的地址和发送的命令在softiic.h中先进行了宏定义:

#define SHT40_ADDRESS 0x44 // SHT40的I2C地址
#define SHT40_COMMAND_MEASURE_HIGH_PRECISION 0xFD  //要发送的命令 0XFD

(1)SHT40_Start_Measurement开始测量温湿度函数

其实是把下面的写入命令函数加了延时10ms而已,可以合并进写命令函数,我个人认为那样不直观。

void SHT40_Start_Measurement(void) {
    Soft_IIC_Write_Command(SHT40_ADDRESS, SHT40_COMMAND_MEASURE_HIGH_PRECISION);
    HAL_Delay(10); // 根据数据手册说明,延时10ms
}

(2)Soft_IIC_Write_Command 发送命令函数

设备地址是0x44,转化为7位二进制为 1000100,由于这里是写入,读写标志位为0(写),因此发送的这个字节为 10001000

“命令”为0xFD,这可以在数据手册中查到,对应的返回值是6字节数据,第1、2字节是温度数据,第3字节是CRC校验和;第4、5是湿度数据,第6位是校验和。这些数据都是高精度的。

image.png

根据原理部分讲的,在发送完地址后就要发送“寄存器地址”,而这里有个小坑,我最开始没绕明白。通常在网上搜到的I2C代码其写入函数参数有:设备地址、寄存器地址、写入的数据,而SHT40这的“命令”其实就是寄存器地址,只是没有写进去的数据罢了。当SHT40接收到这个“命令”后,它就会读取温湿度数据,并存在它的寄存器里,等待主机的读取命令。我这里不需要给SHT40写入数据,也就是发送完寄存器地址后,等待应答然后关闭I2C传输。

void Soft_IIC_Write_Command(uint8_t deviceAddr, uint8_t command) {
    IIC_Start();
    IIC_Send_Byte(deviceAddr << 1); // 发送设备地址和写位
    IIC_Wait_Ack();
    IIC_Send_Byte(command); // 发送命令
    IIC_Wait_Ack();
    IIC_Stop();
}

(3)SHT40_Read_Measurement读取温湿度数据函数

这里给SHT40发送读位,即读写标志位变为1,发送的字节为10001001

然后等SHT40回应,并释放SDA线,让SHT40发送它刚刚读到的温湿度数据,并存到data数组中。

void SHT40_Read_Measurement(uint8_t* data, uint8_t length) {
    IIC_Start();
    IIC_Send_Byte((SHT40_ADDRESS << 1) | 0x01); // 发送设备地址和读位
    IIC_Wait_Ack();
    for (uint8_t i = 0; i < length-1; i++) {
      *data = IIC_Read_Byte(i < (length - 1)); // 读取数据并发送应答信号
			data++;
			IIC_Ack();
    }
		*data = IIC_Read_Byte(0);
		IIC_NAck();
    IIC_Stop();
}

(4)main.c中获取温湿度数据的实现

调用刚刚写好的函数SHT40_Start_MeasurementSHT40_Read_Measurement,这里的readData数组要提前初始化好,长度为6。

然后根据数据手册里的伪代码

image.png

把获取到的数值换算成能读得懂的数,最后放大温湿度,以便数码管显示。

/* SHT40采集温湿度*/
SHT40_Start_Measurement();
SHT40_Read_Measurement((uint8_t*)readData,6);
Temperature = (1.0 * 175 * (readData[0] * 256 + readData[1])) / 65535.0 - 45;
Humidity = (1.0 * 125 * (readData[3] * 256 + readData[4])) / 65535.0 - 6.0;
device_paramter.Temp = Temperature * 10;		//放大温湿度
device_paramter.Humi = Humidity * 10;

接下来,编译、烧录、插电开机~用示波器捕捉并解码收发数据过程

image.png

当你捕捉到这些跳动的高低电平那一刻,成就感瞬间爆棚啊这就是电子的魅力

3.3 优化程序降低功耗的方法

STM32G0系列有四种休眠模式;

● 低功耗运行模式(降低CPU频率,系统仍在运行)

● 睡眠模式(系统进入睡眠,任意中断/事件唤醒)

● 停止模式(系统进入停止,支持任意外部中断和RTC闹钟唤醒)

● 待机模式(系统进入待机,支持RTC闹钟唤醒,WKUP、NRST引脚唤醒以及IWDG复位唤醒,打开了LSI和LSE)

如果按照官方文档的代码,让温湿度计进入睡眠模式,经测量仍有2.6mA的电流。

image.png

IMG_20240717_220545.jpg

而如果设置成停止模式,仍然可以用中断唤醒,但此时电流降低至0.21mA !!

image.png

IMG_20240717_220141.jpg

3.4 程序烧录步骤

(1)从附件中下载SHT40_Project.zip,并解压,进入MDK-ARM文件夹,用Keil打开SHT40_Project.uvprojx

(2)将ST-LINK针对针地与温湿度传感器电路板上的调试口相连接

(3)点击编译(Build),点击下载(Download)

image.png

(4)断开ST-LINK,在电路板背面放入18650锂电池,注意一定不要接反!(有弹簧一侧为负极)

(5)正常来说,上电后数码管不亮,按一下按钮,两个数码管同时显示温湿度5秒,然后左侧数码管显示电池电压4秒,然后进入休眠。

4 故障排除

故障1:上电后只有第一次温湿度读数,而后续读数错误。

故障描述:插电,发现只显示读出第一次的温湿度数值,之后进入while循环的数值就读不到了,数码管显示0.0 9.0。

解决过程:经过一番摸索,在我用示波器表笔接SCL,准备再次查看波形时,发现无波形变化,遂确认是硬件问题而非软件BUG。于是我用万用表电阻档再次测量3.3V与SCL、SDA之间的电阻,发现它们均不为4.7KΩ(上拉至3.3V的电阻值),于是拆下来两个电阻挨个测量,最后发现SCL的上拉的电阻损坏

故障原因:上拉电阻损坏。

故障2:数码管显示0.0和9.0

故障描述:上电后,数码管读不到正确的温湿度值,不同于故障1还有一次可读的数值。

解决过程:显示0.0和9.0是由于初始化的数据接收数组内元素都为0,而通信读取温湿度过程中数组内数值并未改变,代入温湿度的计算公式得到了固定值0.0和9.0。可用测量通断挡位,一支表笔接传感器模块插口,一支表笔接STM32对应引脚,分别测试SCL、SDA、VCC、GND判断是否接触良好。若接触良好,则有可能传感器损坏。

故障原因:接触不良或温湿度传感器损坏

故障3:想再次烧录代码,Keil提示invalid rom table

故障描述:已经烧录好我在附件里的源代码,想改动一点自己的东西,再烧录发现Keil报错。

解决过程:由于有让MCU进入休眠模式的代码,在上电后它会立即进入休眠模式,以保持低功耗运行。此时可以先插好ST-LINK,在Keil程序编译好,然后按下唤醒按钮,在数码管仍在显示数值的过程中,手快速地点击Keil的下载按钮即可。
.
故障原因:MCU进入休眠模式

5 项目属性

首次公开,在官方原理图基础上增加锂电池充电管理电路以及完善PCB布局。未在别的比赛中获奖。

6 开源协议

GPL3.0

7 大赛LOGO验证

温湿度计电路板整体外观展示
IMG_20240718_214156.jpg

显示温度、湿度状态展示
IMG_20240718_214309.jpg

显示电池电压
IMG_20240718_214300.jpg

8 演示您的项目并录制成视频上传

写在后面

第二次参加训练营啦,这次借着温湿度计训练营一同参加了第九届立创电赛。在设计硬件、调试软件的过程中真的能学到很多东西!查资料非常重要,不论是国内国外的资料,在查的过程中也能逐渐理清知识脉络。

在这里要感谢@盛思锐,温湿度传感器很好用,数据手册内容内容非常全面。

参考

  1. https://blog.csdn.net/qq_38575895/article/details/127641344
  2. https://blog.csdn.net/qlexcel/article/details/117159467
  3. https://www.bilibili.com/video/BV1dg4y1H773/?spm_id_from=333.337.search-card.all.click&vd_source=b0a6c01daa3f29bd494dc070c1c1bf14
  4. https://www.yuque.com/wldz/jlceda/ul1wcz7n5dgt6s60