【IIC】IIC通信与温湿度传感器AHT20(DHT20)

我们已经一起学习了STM32中最常用的通信方式:串口,本次我们来学习嵌入式领域另一种常见的通信:IIC通信,并且尝试使用IIC通信与开发板上的AHT20传感器进行交互,获取你房间的温度与湿度。

首先,我们来看一下IIC的通信原理,这一块知识对于初学者来说大致了解就好。实际应用中并不需要大家深刻理解。大家一定还记得,除了共地线以外,串口使用两根数据线进行通信,一根连接设备A的发送IO口与设备B的接收IO口,另一根则相反。

从连线上来看,IIC也是除共地外,使用两根线来完成通信,不过IIC与串口可是有着很大的区别。首先,别看IIC有两条线,但其实只有一条可以用来传递数据,我们通常称其为SDA(Serial Data),而另一条则是用于提供同步时钟脉冲的时钟线,我们称之为SCL(Serial Clock)。串口的两根线就像双向车道,可以同时进行通信,因而我们称其为全双工通信。而IIC唯一的数据线SDA虽然也允许进行双向通信,但同一时刻却只能一端发送不能同时进行,因此我们称IIC为"半双工"通信。

由于IIC同一时刻只能进行一个方向的通信,为了避免冲突,IIC采用了主从模式,也就是一台设备为主机,另一台(多台)为从机,只能由主机先发起通信,从机才能根据主机的指令回复相应的信息,而正是由于这种一问一答的主从模式,使得IIC可以支持多设备通信。例如在开发板上就使用STM32的IIC1,同时连接了AHT20温湿度模块与OLED屏幕,像IIC这种支持多个设备进行通信的通信协议,我们称其为总线协议。在IIC总线上,每个从机都有其唯一的设备地址,例如AHT20的地址是0x70,OLED屏幕的地址是0x7A,当作为主机的STM32需要操作时AHT20时,只需要在发送数据的最开始发送AHT20的设备地址,AHT20就会知道此数据是发送给自己的,从而做出反应,而其他设备则明白此数据与自己无关。选择性失聪。

在串口中,通信的双方首先约定好通信的速度,也就是比特率。然后双方按照这一速度,在合适的时机去设置或者读取数据线上的高低电平,这种模式我们称其为异步模式。因为这种模式下对通信速度,也就是对数据线操作的时机的选取,是基于双方各自的时钟,这一通信模式的好处是比较便捷,但缺点就是通信的双方必须保证各自的系统时钟是精确的。

若有一方的时钟有些问题,就会卡不上节奏,双方不知所云。

考虑到许多小型传感器并没有精确的晶振提供时钟基准等情况,IIC选择了与异步通信相对的另外一种模式,同步通信,也就是由主机通过时钟线发送固定频率的脉冲信号,来作为IIC总线上所有设备通信的统一时钟源。

下面我们来举个例子,更细致地描绘一下一次完整的IIC通信,刚入门的初学者了解即可。

假设我们要读取AHT20模块中的温湿度信息,根据资料包中提供的AHT20文档,这一过程应该是这样的。首先,IIC的数据线与时钟线往往都有上拉电阻进行上拉,所以在未开始通信时,数据线与时钟线都处于高电平,这时身为主机的STM32,发送IIC通信启动信号,也就是在时钟线依旧是高电平时,将数据线提前下拉,这时所有从机就竖起"耳朵"准备接受命令了。然后正式的IIC通信就开始了,主机会在时钟线上产生一个恒定频率的时钟脉冲信号,主机与从机依靠时钟线上的脉冲信号来同步对数据线的读写,准确点说,就是当时钟线处于低电平时,主机设置数据线的电平,而时钟线处于高电平时,从机读取数据线的电平。显而易见,从机读取到的电平,就是主机在时钟线低电平时设置的电平。

按照AHT20的文档,主机应该首先发送AHT20的数据地址0x70,但是大家可以发现我们发送的其实是0x71.这时为何,我们稍后再谈。

现在,数据线与时钟线的电平变化是这样的,时钟线低电平时,主机先给数据线设置低电平,然后时钟线高电平,从机读取到0。然后时钟线低电平,主机给数据线设置高电平,然后时钟线高电平,从机读取到1。如此循环,直到主机发完8位,也就是1字节数据。

随后,按照IIC协议的规定,数据的接收方需要发送一个ACK信号(应答信号)确认自己已经收到数据。当然,说的好听,其实所谓ACK信号,就是在时钟线低电平时,由接收方也就是此处的从机将数据线拉低一下。

然后按照AHT20的手册,接下来就是作为从机的AHT20的Show Time了。时钟线上依旧是由主机STM32产生的时钟脉冲信号,不过数据线的控制权,现在交到AHT20的手中。AHT20也像之前的主机一样,在时钟线低电平时设置数据线,主机则也是在时钟线高电平时,读取数据线上的数据,如此反复,直到发送完1字节。然后再由现在的接收方,也就是主机来一次ACK信号。然后AHT20继续发送,如此反复,直到发送完所有数据。

然后主机会在时钟线处于高电平时,将数据线拉高,也就是发送IIC通信结束信号,宣告整段通信的完成。可以注意到的是,在整个IIC通信过程中,只有主机发送开始和结束信号时,才会在时钟线为高时控制数据线;其他阶段,都只能在时钟线为低时设置数据线。

大家可能有些晕,不过没关系,实际上只要大家不是想要从头到尾自己实现一遍IIC通信,上面的知识点大家了解一下就好了。等真用到了,再来学习也不迟。

IIC的原理就了解到这里,接下来我们进行实战。

首先熟练地创建一个工程,

工程名称就叫做iic

来到Cube MX界面,首先先把老朋友USART2打开,用于一会通过串口查看数据

然后我们点开本次的主角I2C1,将其配置为标准的IIC模式。

下面的配置保持默认即可,一般不需要调节。

这次的实战,我们就不把代码全部写到main.c文件了,而是专门为AHT20写一下驱动文件。这样的话,我们最好来到Project Manager(项目管理),点击Code Generator(代码生成器),勾选上为每个外设生成一对.c/.h文件,这样我们就可以在其他文件中include相应的头文件,就能拿到huart2或者hi2c1这类的外设操作句柄了。

那么,我们按下Ctrl+s保持并生成代码。

点开项目浏览器中IIC项目的Core文件夹下的IncSrc文件夹,可以看到与以前的项目相比,多出了gpioi2cusart.h.c文件

点开也可以发现,许多以前生成在main.c中的函数与变量,也分别挪到了这些对应的文件中。接下来我们也定义一对我们自己的AHT20的.c/.h文件。在Inc文件夹上右键,新建Header File(头文件)

文件名设置为aht20.h

新建的文件中Cube IDE贴心地为我们写好了这种define语句

这种语句在几乎所有的.h文件中都有,可以防止头文件被多个地方include引用后,出现重复定义的问题。

我们首先可以在aht20.h文件中#include "i2c.h",然后来到Src上右键,新建Source File(源代码文件)

文件名为aht20.c

然后#include "aht20.h",按住Ctrl点击#include "aht20.h"就跳转到了 "aht20.h"头文件,所以在"aht20.c"文件中包含"aht20.h"头文件,也就相当于包含了"i2c.h"头文件了。可以使用"i2c.h"中声明的变量与函数。最主要的就是一会我们要使用的hi2c1,它与我们之前使用的huart2``huart3一样,都是用于对某外设进行操作的句柄。i2c.h中还包含了main.hmain.h中包含了stm32f1xx_hal.h,它里面又包含了stm32f1xx_hal_conf.h,这stm32f1xx_hal_conf文件中又引用了好多头文件,其中就包含stm32f1xx_hal_i2c.h,层层递进下来,也就相当于我们在aht20.c中包含了stm32f1xx_hal_i2c.h文件。

aht20.c ->aht20.h ->i2c.h ->main.h -> stm32f1xx_hal.h -> stm32f1xx_hal_conf.h -> stm32f1xx_hal_i2c.h

aht20.c ->stm32f1xx_hal_i2c.h

而在这文件中就声明了我们一会要用到的I2C相关的函数,绕的有些远,回到我们的aht20.c文件。然后打开数据包中为大家准备的AHT20数据手册。照着手册写代码,前面的内容我们可以略过,直接来到手册的第8页,5.4传感器读取流程。

首先第一条是说AHT20上电后要等待40ms才能与之通信,并且要先向AHT20发送0x71,AHT20会返回1字节的状态信息,如果此状态信息的第三位是1,那就一切正常,如果不是,就要发送0xBE命令,并且携带0x08 0x00这两个参数,来重新初始化AHT20。

这里我们要重点关注一下这个0x71,他其实就是AHT20作为IIC从机的地址。但是我们刚刚不是说AHT20的IIC地址是0x70吗?其实按照AHT20手册,AHT20的IIC地址是0x38。这是什么原因呢?其实IIC通信一般使用7位地址码,那么AHT20的地址0111000按理说就是0x38,但是IIC通信中每次发送都是1字节,也就是8位,所以规定从机地址要向左移一位,那多出来的这一位如果是0的话,整个8位数据就是0x70了。而IIC协议规定,如果主机发起通信的目的是为了设置(写)从机,那么这一位就为0,如果主机发起通信的目的是为了从从机读取数据,那么这一位就为1。不过对于这一位的设置,HAL的相关函数会自动帮我们处理,所以我们一般也就认为这一位是默认为0就好了。那么也就有了AHT20的地址是0x70。

所以我们回到代码,首先来一个define宏定义,定义一下AHT20的设备地址是0x70

复制代码
#define AHT20_ADDRESS 0x70

然后我们来写手册中所描绘的初始化过程,定义一个函数AHT20_Init,先定义一个uint8_t的变量readBuffer用于一会儿接收状态信息,然后按照手册,先延迟40ms,在然后我们今天的主角之一IIC读取函数,HAL_I2C_Master_Receive登场,第一个参数是外设操作句柄的指针,也就是hi2c1的指针。第二个参数是要读取的从机地址,也就是我们刚刚宏定义的AHT20_ADDRESS,注意:由于这个命令是去读取从机的数据,所以实际上0x70会被HAL库改为0x71使用,不过我们不必自己操心。然后下一个参数是用于接收数据的变量的指针,也就是readBuffer指针。第四个参数是会读多少字节数据,按照手册所说,读1字节数据。最后一个参数是超时时间,也就是跟我们之前串口相关的函数一样,如果多久之内没有发送成功,就不愿意等待了,我们这里就填写永久等待吧。

复制代码
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

功能:
HAL_I2C_Master_Receive 是 STM32 HAL 库中用于 I2C 主机接收数据的函数。它的作用是从指定的 I2C 设备地址读取数据

参数:
I2C_HandleTypeDef *hi2c:I2C 句柄,指定要使用的 I2C 总线和配置。
uint16_t DevAddress:从设备地址,7 位或 10 位地址,根据设备规范。
uint8_t *pData:指向接收数据的缓冲区。
uint16_t Size:要接收的数据字节数。
uint32_t Timeout:超时时间,单位为嘀嗒信号节拍数。

返回值:
HAL_StatusTypeDef:函数执行状态,可能的返回值包括:
HAL_OK:操作成功。
HAL_ERROR:操作失败。
HAL_BUSY:总线忙。
HAL_TIMEOUT:操作超时。

注意事项:
设备地址:DevAddress 参数应为设备的写操作地址,HAL 库会根据读写操作类型自动选择正确的地址。
超时时间:Timeout 参数设置为 HAL_MAX_DELAY 可以使函数在数据传输完成前一直等待,但可能会导致系统卡顿。在实际应用中,建议根据具体需求设置合理的超时时间。
中断和 DMA:如果需要非阻塞操作,可以使用 HAL_I2C_Master_Receive_IT 或 HAL_I2C_Master_Receive_DMA 函数。

然后按照手册所说,我们要判断第3位是否为1,判断方式有很多,比如我们将取得的数据与0x80进行一个二进制的按位与。按位与会将两个数据均为1的位保留为1,只有一个是1,或者两个都是0的,就记为0。所以如果返回值第3位不为1的话,与0x08按位与后的计算结果是0x00,这种情况下就要发送0xBE指令进行初始化,定义一个uint8_t类型的数组sendBuffer长度是3,内容就是手册里说的指令0xBE与指令参数0x80 0x00。然后第二个主角IIC发送函数登场。HAL_I2C_Master_Transmit,第一个参数外设操作句柄的指针,也就是hi2c1的指针,第二个参数依旧是从机地址,我们写宏定义AHT20_ADDRESS,第三个参数是要发送的数组的指针sendBuffer,第四个参数是要发送的数据长度3,最后一个参数也是超时时间。

复制代码
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

功能:
HAL_I2C_Master_Transmit 是 STM32 HAL 库中用于 I2C 主机发送数据的函数。它的作用是将一个数据块发送到指定的从设备地址

参数:
hi2c:指向 I2C_HandleTypeDef 结构的指针,该结构包含了 I2C 外设的配置信息。
DevAddress:目标设备地址,通常是 7 位地址左移一位得到的 8 位地址。
pData:指向要发送的数据缓冲区的指针。
Size:要发送的数据字节数。
Timeout:超时时间(毫秒),如果在此时间内没有完成传输,则会返回超时错误。

返回值:
HAL_StatusTypeDef:函数执行状态,可能的返回值包括:
HAL_OK:操作成功。
HAL_ERROR:操作失败。
HAL_BUSY:总线忙。
HAL_TIMEOUT:操作超时。

使用场景:
HAL_I2C_Master_Transmit 适用于简单的数据块传输,比如发送一个命令序列或者一组数据给一个没有内部地址空间的设备。

注意事项:
设备地址:DevAddress 参数应为设备的写操作地址,HAL 库会根据读写操作类型自动选择正确的地址。
超时时间:Timeout 参数设置为 HAL_MAX_DELAY 可以使函数在数据传输完成前一直等待,但可能会导致系统卡顿。在实际应用中,建议根据具体需求设置合理的超时时间。
中断和 DMA:如果需要非阻塞操作,可以使用 HAL_I2C_Master_Transmit_IT 或 HAL_I2C_Master_Transmit_DMA 函数。

void AHT20_Init()
{
    uint8_t readBuffer;
    HAL_Delay(40);
    HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDRESS, &readBuffer, 1, HAL_MAX_Delay);
    if((readBuffer & 0x08) == 0x00)
    {
        uint8_t sendBuffer[3] == {0xBE, 0x08, 0x00 };
        HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS, sendBuffer, 3, HAL_MAX_Delay);
    }
}

大功告成,这样一个AHT20的初始化函数就写好了,先不着急去用,我们再来写用AHT20读取温湿度的函数,函数名称不妨叫做AHT20_Read,因为需要一次性读出温度与湿度两种,所以我们给函数设置两个float指针类型的入参,用于把读取到的数据回传给调用者,温度 Temperature ``湿度 Humidity

然后来看手册,要触发测量的话,可以发送0xAC命令并且携带0x33 ``0x00参数,然后等待75ms后,就可以再次通过读取指令,获取共6字节的当前状态与温湿度数据。

那我们先来触发测量,定义一个sendBuffer,里面是0xAC命令与其参数。然后这次是一个控制的指令,所以使用HAL_I2C_Master_Transmit,所有的参数内容都与之前一模一样。然后按照手册,等待测量完成75ms,随后按照手册内容与示意图,我们要通过读取指令读取6字节数据。所以定义一个大小为6的数组readBuffer,然后使用读数据函数HAL_I2C_Master_Receive,因为是数组,所以去掉取地址符,直接使用数组名就是此数组的指针。由于我们刚刚已经用0xAC触发过测量,所以按照手册本次会返回6字节数据。然后我们要判断第0字节的第7位是否为0,如果是就说明确实是获取的刚刚测量完成的数据,判断的原理与刚刚初始化函数中的判断相同,不过是从第3位的0x08,换成了第7位为1的0x80。 如果读取到的数据第7位为我们想要的0,则按位与后应该是0x00

此时,我们再来进行温湿度数据的计算。按照手册,温度数据和湿度数据各占两个半字节,所以为了对数据进行拼接。我们首先定义一个uint32_t的临时变量叫data,然后湿度数据的拼接应该是这样的,第3字节的高4位是湿度数据,去掉低4位的温度数据后,应该向右移四位补齐,然后第2字节应当向左移4位,到达与刚刚4位对齐的更高位,第1字节的数据也要向左移12位到更高位上去,然后三者相加,就能组成完整的湿度数据。按照这一原理,我们将它们拼接到data变量中,首先在移位的过程中要先将原本uint8_t的数据转为uint32_t,不然会出现移出界而丢失的情况。

在然后手册中还有一个对此数据转换为相对湿度数据的公式,比较简单,就是此数据除以2^20就好,当然,还要乘上100%来变成百分数的表达形式。所以湿度的值就等于data乘以100.0再除以2^20。这里写100.0而不是直接写100是为了将计算转换为浮点数的计算,否则后面的除法只是整数计算的话会丢失掉小数部分。2^20的简易算法,就是将0x01向左移20位。还要注意,为了能够将数据传递到AHT20_Read函数的调用方,函数形参这里传入的温湿度入参都是以指针形式传入的,所以Humidity变量前要加解引用符号或者说取值符,温度数据的解析也是如此,同样是首先对数据进行位移拼接,然后再根据手册提供的公式转换成摄氏度温度值。

复制代码
void AHT20_Read(float *Temperatuer, float *Humidity)
{
    uint8_t sendBuffer[3] = {0xAC, 0x33, 0x00 };
     uint8_t readBuffer[6];
    
    HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDRESS, sendBuffer, 3, HAL_MAX_Delay);
    HAL_Delay(75);
    HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDRESS, readBuffer, 6, HAL_MAX_Delay);
    if((readBuffer[0] & 0x80) == 0x00)
    {
        uint32_t data = 0;
        data = ((uint32_t)readBuffer[3] >> 4) + ((uint32_t)readBuffer[2] << 4) + ((uint32_t)readBuffer[1] << 12);
        *Humidity = data * 100.0f /(1 << 20);

        data = ((uint32_t)readBuffer[3] << 16) + ((uint32_t)readBuffer[4] << 8) + (uint32_t)readBuffer[5] ;
        *Temperatuer = data * 200.0f / (1 << 20) - 50;
    }
}

那么至此我们已经有了AHT20的初始化函数AHT20_Init,AHT20的测量温湿度函数AHT20_Read,AHT20的驱动程序也算是大功告成了。为了可以在其他文件中调用这两个函数,我们将这两个函数在aht20.h文件中进行声明。

然后回到main.c,在include注释对中#include "aht20.h",以及一会我们还要用到的#include <stdio.h>``#include <string.h>

然后来到main函数中,在进入循环前,也就是USER CODE 2注释对中,调用我们写的aht20的初始换函数AHT20_Init。然后定义几个我们一会需要的变量,首先是两个float类型的变量,温度temperature湿度humidity,然后再来一个稍长一点的char类型的数组message,用于一会拼接数据,用串口发出来。

随后来到循环中,调用AHT20_Read来触发AHT20测量,并读取AHT20的测量数据,按照我们为AHT20_Read函数设定的入参,分别填入温度与湿度的变量指针。随后我们将温湿度信息通过串口发送出来方便查看,为了发送温湿度这种存在变量里的动态数据,我们使用sprintf函数来构造字符串。第一个参数当然是message,然后第二个参数是要写入的字符串,变量的地方用占位符%.1f代表保留一位小数,因为%本身会被理解为占位符,所以可以用%%来代表一个真正的%,结尾在加个换行。然后后面的参数依次填写温度和湿度变量。

复制代码
while(1)
{
    AHT20_Read(&temperature, &humidity);
    sprintf(message,"温度:%.1f ℃, 湿度:%.1f %%\r\n",temperature, humidity);
    HAL_UART_Transmit(&huart2, (uint8_t)message,strlen(message),HAL_MAX_Delay );

    HAL_Delay(1000);
}

但是我们会发现写的没问题,但是却有一个红线报错,这是因为STM32Cube IDE默认没有启用编译器对浮点数输出的支持,我们可以按照其报错提示,点开这里的Project->Properties,找到C/C++ Build中的Settings Tool Settings中的MCU Settings,将这两个选项勾选上

Project->Properties->C/C++ Build->Settings ->Tool Settings->MCU Settings->勾选Use float with printf from newlib=nano(-u_printf_float)Use float with scanf from newlib=nano(-u_scanf_float)

随后点击应用并关闭,就可以发现报错消失了,最后就使用我们熟知的串口发送函数HAL_UART_Transmit将其发送出来,记得将char指针类型转为uint8_t指针,可以防止warning,以及使用strlen来获取字符串的长度,超时时间随便填一下。

最后的最后,加上一个延时,控制一下循环测量间隔,例如我们每一秒钟测量一次,全部完成编译下载我们来看一下效果。

首先要保证开发板的串口线已经与电脑连接,然后打开串口助手,连接上你的串口,你房间的温湿度就这样显示出来了,可以尝试用手按在AHT20上,观察数值变化。

相关推荐
WeeJot嵌入式2 小时前
【串口】蓝牙模块与简易数据包解析
stm32·单片机·嵌入式硬件·蓝牙
国科安芯2 小时前
抗辐照DCDC电源模块在商业卫星通信载荷中的应用
网络·人工智能·单片机·嵌入式硬件
国产化创客2 小时前
RuView开源项目Rust构建部署
大数据·物联网·嵌入式·信息与通信·智能硬件·wifi csi
m0_377108142 小时前
物理day4-22
单片机
nuoxin1142 小时前
CYUSB4024-FCAXI 是一款USB 20Gbps 控制器-富利威
网络·人工智能·嵌入式硬件·fpga开发·dsp开发
charlie1145141912 小时前
嵌入式C++开发第17篇:C++23特性收尾 —— 属性、链接与零开销抽象的最终证明
开发语言·c++·stm32·学习·c++23
wwddgod2 小时前
STM32L071 串口唤醒stop低功耗模式笔记
笔记·stm32·单片机·低功耗·串口唤醒
liuluyang5302 小时前
DW I2C寄存器与使用简介
stm32·单片机·嵌入式硬件·dw i2c
Mr..Jackey2 小时前
RA6809 的 HMI(人机交互) 开发:菜单逻辑架构设计与实现详解(4)
单片机·51单片机·人机交互·交互