| 本帖最后由 1084504793 于 2024-1-6 17:56 编辑 
 在智能家居和工业控制等领域等多个场景下都会需要一款体积小巧且可以通过电信号快速获取指定地方温湿度、光照强度和磁场强度等的模块。常见的温度模块有DS18B20和DHT11等。这些模块大多数是通过单总线的通信方式进行通信,单总线通信方式的优点是占用MCU的I/O口资源非常少,只需要一根数据线便可进行通信,而且总线下可以挂载多个器件获取不同的数据。但是其最大的缺点是通信速率不能太高,一般是16.3Kbps,最大可达142Kbps,通常是在100Kbps下进行通信。这样就会限制总线下的从设备的数量。1980年飞利浦发明了集成电路总线,即IIC。IIC相对于单总线通信来说增加是时钟线(SCL)。在通信速率上IIC的速率可以达到3.4Mbps,但是一般都是使用快速模式的400Kbps先进行通信,因为在该速率下拥有相对较远的通信距离。IIC的最远通信距离可以达到15M,当然通信速率也是要用更小的模块确保数据传输的有效性。IIC因其占用引脚资源少,逻辑电平也与MCU的逻辑电平一致,通信速率范围也比较广,代码移植简单等优点,一直是MCU必备的外设通信模块。因IIC的控制电路简单且成本低,所以有很多的小数据量的模块都是采用IIC进行通信。如本文要介绍的AHT20温湿度传感器。
 
 AHT20参数介绍
 
 AHT20模块如下图所示
 
 AHT20温湿度传感器是AHT10的升级款。相对于AHT10来说,AHT20主要在芯片体积和供电电压宽度做了提升。AHT20芯片的体积更小,供电电压宽度更大,供电电压支持2.2-5.5 VDC。AHT20的电气特性如下
 
 其中的精度误差典型值为±0.3,可见其精度值可以满足我们一些常见的开发需求的。工作范围-40至80℃也是和MCU(一般是-40至85℃)基本一致的。        电气特性表中最重要的一点是其供电电压。因为有些模块的供电电压最大值小于5V,而我们会因惯性思维而直接给模块的VCC接5V,这样会烧毁模块芯片的。因AHT20的供电电压最大值为5.5V,所以可以用5V进行供电,这个我也进行尝试过的。
         温度典型误差和最大误差如下从图中可以看出当温度处于极限值(-40或80℃)时,误差值最大,但是最大不会超过2℃。 
         湿度特性表如下从表中可以看出其工作范围为0至100%,完全满足我们的研发需求。误差典型值也是±2,相对于人而言误差影响基本感觉不到。 
         25℃时相对湿度的误差如下图同样是在极值的情况下,误差值会比较大。 
 
 AHT20接线方式    
          AHT20与MCU的通信方式为标准的IIC通信。官方给的接线方式如下 
 
         由于我买的是已将SCL、SDA、VCC和GND引脚引出来的开发模块,所以只需要用杜邦线将M61开发板与模块连接好就行,连接示意图如下 
         若是需要重新画PCB板,则芯片的各个引脚说明如下 
 AHT20驱动编写    
        需要注意的是,手册上也写明在芯片上电后的5ms之后才可以设置SCL和SDA为高电平。这个在后续的代码验证中,认为M61上电后经过board_init 初始化后在初始化IIC引脚是可以的。若大家在使用的时候发现有问题,可以在初始化board_init 后延时5ms再初始化IIC的引脚。
         若大家用自己写IIC驱动的时候,记得一定要注意一下AHT20支持的IIC速率,因为在IIC通信过程中,若SDA的电平保持时间过短则会出现AHT20不能收到正确的指令,从而导致无法读取到数据。IIC通信的一些参数如下所示         但是本文档会用博流SDK的IIC驱动介绍如何与AHT20进行通信获取温湿度数据。         AHT20的IIC设备地址用的是八位即一个字节来表示。由于AHT20没有硬件地址(通过芯片引脚说明中可以看出没有地址引脚A[0:2]),所以在给AHT20发送地址时固定值为0x38(忽略读写位)。也说明同一个IIC总线下只能接一个AHT20模块或者说不能接其他设备地址为0x38的模块,否则访问0x38设备时接收到的数据会异常。官方给的IIC地址字节解析如下图所示。         设备地址的最低字节表示对设备进行读还是写的操作,一般最后一位为低电平(逻辑为0)时为写指令,为高电平(逻辑为1)时为写指令。在用官方给的IIC发送读写指令时,我认为结构体中bflb_i2c_msg_s 的元素addr 为包含读写指令的地址,所以在赋值时把读写指令的位也包含在内。但是官方的用意是只需要告诉设备的高七位的地址值就可以了,在发送设备地址时会自动在地址的最后一位加一位读写控制位。所以addr 需要赋值为0x38。 
         AHT20的驱动代码文件主要是AHT20.c和AHT20.h。AHT20.c主要定义的是有关AHT20控制函数的定义;AHT20.h主要是指令的宏定义,状态的枚举和函数的声明等。 
         数据传输是有发送和接收数据的,一般将发送数据和接收数据封包成两个函数来实现,所以我也封成两个读写函数。 
         首先是发送数据函数,函数定义代码如下
 复制代码void AHT20_SenCmd(uint8_t _Cmd, uint8_t *Sendbuf, uint16_t _length)
{
    msgs[0].addr = AHT20_ADDR;
    msgs[0].flags = I2C_M_NOSTOP;
    msgs[0].buffer = &_Cmd;
    msgs[0].length = 1;
        if (_length != 0)
        {
                /* 有数据的发送 */
             msgs[1].addr = AHT20_ADDR;
             msgs[1].flags = 0;
             msgs[1].buffer = Sendbuf;
             msgs[1].length = _length;
             bflb_i2c_transfer(i2c, msgs, 2);
        }
        else
        {
                /* 只发一个字节的数据 */
                msgs[0].flags = 0;
                bflb_i2c_transfer(i2c, msgs, 1);
        }
}
       其中参数有指令内容_Cmd ,发送数据指针*Sendbuf 和发送长度_length 。改函数可以满足任意长度的数据发送。当长度为0时,只发送设备地址和命令字;若长度不为0,则发送完设备地址和指令后也会发送数据内容,且中间没有任何的停止信号。自己在调试过程中也发现,官方IIC的发送数据是有握手的判断。在发送地址后,若没有收到握手信号,则不会继续发送数据。结构体中的flags 元素决定着这些数据发送完成后是否会发送一个终止信号。若赋值为0(.h文件是没有定义的,但是论坛教程都有说明的),则在发送完数据后产生一个停止位;若赋值为I2C_M_NOSTOP ,则发送完数据后不会产生一个停止位。其他的参数赋值都是比较简单的,在此不做过多的介绍。 
         接下来是接收数据函数,函数定义代码如下
 复制代码void AHT20_RecData(uint8_t *Recbuf, uint16_t _length)
{
    msgs[0].addr = AHT20_ADDR;
    msgs[0].flags = I2C_M_READ;
    msgs[0].buffer = Recbuf;
    msgs[0].length = _length;
    bflb_i2c_transfer(i2c, msgs, 1);
}
       函数比较简单,参数为接收数据保存的指针*Recbuf 和接收长度_length 。
         AHT20的各个命令及解释说明如下,我们在控制AHT20也是通过发送这些指令进行控制。        针对这些指令我在ATH20.h进行一些宏定义,宏定义如下
 复制代码#define AHT20_CMD_INIT             0xBE       /* 初始化AHT20芯片指令 */
#define AHT20_CMD_SOFT_RES         0xBA       /* 软复位AHT20芯片指令 */
#define AHT20_CMD_START            0xAC       /* 启动AHT20芯片工作指令 */
       官方也给了芯片正常工作的整个流程,官方给的流程如下          在看完如何驱动AHT20的官方流程后可以先不要着急写驱动函数,因为这个描述比较片面,还无法写。         设备返回值的说明如下         其中表中说明接收的数据为一个字节,bit[3]为校准使能位,该位说明AHT20芯片是否校准过,一般在出厂后都是进行校准过的,校准后该位便一直为1;bit[7]为AHT20状态位,这位比较重要,说明数据是否采集成功,只有数据采集成功后,设备处于空闲状态,该位为0,表明可以读取数据。在这里可以先写一个返回值判断的函数,函数代码如下
 复制代码
AHT20Sta AHT20_ReadSta(void)
{
        uint8_t ReadStaVal = 0;
        AHT20_RecData(&ReadStaVal, 1);
        if((ReadStaVal & 0x68) == 0x08) 
        {
                //printf("Chip Sta is OK\r\n");
                return AHT20_STA_READY;
        }
        else
        {
            //printf("Chip State is Err !!!Receive State Value is %d\r\n", ReadStaVal);
                return AHT20_STA_ERR;
        }
}
       函数返回值是一个枚举值,在AHT20.h文件中有定义,定义如下
 复制代码/* 芯片状态 */
typedef enum
{
        AHT20_STA_ERR = 0,   /* 芯片忙和为校准 */
        AHT20_STA_READY,     /* 芯片空闲和已校准 */
        AHT20_STA_LAST,
}AHT20Sta;
       该函数首先是读取一个字节的数据,然后与0x68做相与计算,最后判断是否与0x08相等。其实是判断bit[7]是否为0,bit[3]是否为1。在自测的时候发现AHT20返回的空闲且已校准的状态值不是0x08,而是其他位也是有1的,所以与0x68做与运算,滤掉其他位的影响。 
        官方给的温湿度读取流程如下所示
          在这里就可以写AHT20的驱动函数了,也可以结合那些指令分装成不同功能的函数。首先是AHT20初始化的代码,代码定义如下: 复制代码void AHT20_ChipInit(void)
{
        uint8_t SendDataBuff[2] = {0x08, 0x00};
         
        AHT20_SenCmd(AHT20_CMD_INIT, SendDataBuff, 2);
        
    //printf("Chip Init CMD send over\r\n");
}
        发送初始化指令0x8E,发送数据为0x08和0x00。         AHT20开始测量温湿度指令代码定义如下 复制代码void AHT20_ChipStartWork(void)
{
        uint8_t SendDataBuff[2] = {0x33, 0x00};
         
        AHT20_SenCmd(AHT20_CMD_START, SendDataBuff, 2);
        
    //printf("Chip Start CMD send over\r\n");
}
         同样是发送指令0XAC和数据0x33,0x00。          AHT20软复位函数定义如下 复制代码void AHT20_ChipSoftRes(void)
{
        AHT20_SenCmd(AHT20_CMD_SOFT_RES, 0, 0);
        //printf("Chip Soft Res CMD send over\r\n");
}
         软初始化函数只发送指令0XBA。          AHT20温湿度数据获取和处理指令函数定义如下 复制代码AHT20ReadRes AHT20_RecTempAndHumiVal(int32_t *_THVal)
{
        uint8_t RecBuff[10] = {0};
        uint8_t WorkSta = 0;
        uint8_t ReadCount = 0;
        uint32_t RecDateTemp = 0;
        /* 启动芯片开始工作 */
        AHT20_ChipStartWork();
        /* 等到测试完成 */
        bflb_mtimer_delay_ms(80);
        /* 获取芯片状态 */
        AHT20_RecData(&WorkSta, 1);
        /* 检验芯片是否为空闲状态 */
        while ((WorkSta & 0x80) == 0x80)
        {
                bflb_mtimer_delay_ms(10);
                ReadCount++;
                if (ReadCount > 100)
                {
                        /* 等待1S芯片依然处于忙的状态 */
                        return AHT20_READ_ERR;
                }
                /* 重新读取状态 */
                AHT20_RecData(&WorkSta, 1);
        }
        /* 获取温湿度数据 */
         AHT20_RecData(RecBuff, 7);
         /* 湿度 */
         RecDateTemp = 0;
         RecDateTemp = (RecDateTemp | RecBuff[1]) << 8;
         RecDateTemp = (RecDateTemp | RecBuff[2]) << 8; 
         RecDateTemp = RecDateTemp | RecBuff[3];
         RecDateTemp = RecDateTemp >> 4;
         /* 数据转换,转换结果保留小数点后一位且放大10倍 */
         *_THVal++ = RecDateTemp * 1000 /1024 /1024 ;
         
         /* 温度 */
         RecDateTemp = 0;
         RecDateTemp = (RecDateTemp | RecBuff[3]) << 8;
         RecDateTemp = (RecDateTemp | RecBuff[4]) << 8; 
         RecDateTemp = RecDateTemp | RecBuff[5];
         RecDateTemp = RecDateTemp & 0xFFFFF;
         /* 数据转换,转换结果保留小数点后一位且放大10倍 */
         *_THVal = RecDateTemp * 200 * 10 / 1024 /1024 - 500 ;
         return AHT20_READ_OK;
}
       函数返回的枚举值为读取是否正常的结果,枚举定义如下 复制代码/* 温湿度数据读取结果 */
typedef enum
{
        AHT20_READ_ERR = 0,   /* 读取失败 */
        AHT20_READ_OK,        /* 读取成功 */
        AHT20_READ_LAST,
}AHT20ReadRes;
         需要注意的是读取到的数据的第四个字节为温湿度共用的字节。          然后根据转换公式,将读取到的数据转换为温湿度。转换后的温湿度都是放大10,即保留了一个小数。温湿度转换公式如下 
 
          在调用AHT20读取温湿度前一定要对IIC进行初始化,否则程序会崩溃。IIC初始化函数定义如下 复制代码void IIC0_Init(void)
{
        struct bflb_device_s* gpio;
        
        gpio = bflb_device_get_by_name("gpio");
        /* I2C0_SCL */
        bflb_gpio_init(gpio, GPIO_PIN_0, GPIO_FUNC_I2C0 | GPIO_ALTERNATE | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_1);
        /* I2C0_SDA */
        bflb_gpio_init(gpio, GPIO_PIN_1, GPIO_FUNC_I2C0 | GPIO_ALTERNATE | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_1);
    
        /* IIC初始化,速度为400K */
        i2c = bflb_device_get_by_name("i2c0");
        bflb_i2c_init(i2c, 400000);
        
    //printf("IIC0 Init over\r\n");
}
       用的是芯片IIC0外设,也可以换成IIC1外设,但是一定要记住接线方式是否正确。        AHT20上电后也要做一些初始化操作,主要是发送初始化指令和判断AHT20的状态。AHT20的初始化函数定义如下 复制代码AHT20InitRes AHT20_Init()
{
        uint8_t SoftResCount = 0;
        
        IIC0_Init();
        
        /* 延时40ms,等待传感器加载成功 */
        bflb_mtimer_delay_ms(40);
        /* 初始化AHT20 */
        AHT20_ChipInit();
        
        /* 等待AHT20芯片初始化完成 */
        bflb_mtimer_delay_ms(500);
        
        while (AHT20_STA_ERR == AHT20_ReadSta())    /* 直至AHT20为空闲状态且验证校准 */
        {
                /* 软复位AHT20 */
                AHT20_ChipSoftRes();          
                bflb_mtimer_delay_ms(200);
                
                AHT20_ChipInit();
                
                /* 等待AHT20芯片初始化完成 */
                bflb_mtimer_delay_ms(500);
                SoftResCount++;
                if (SoftResCount > 10)
                {
                        /* 初始化10次失败 */
                        return AHT20_INIT_RES_ERR;
                }
        }
        return AHT20_INIT_RES_OK;
}
        函数返回的枚举定义如下 复制代码/* 初始化结果 */
typedef enum
{
        AHT20_INIT_RES_ERR = 0,   /* 芯片初始化失败 */
        AHT20_INIT_RES_OK,       /* 芯片初始化成功 */
        AHT20_INIT_RES_LAST,
}AHT20InitRes;
        需要注意的是,初始化失败后需要查一下问题,如何排查问题在文末会有总结。 AHT20读取温湿度的调用方法(移植方法)    
          AHT20的调用在main.c的main函数中有示例,main函数定义如下         复制代码int main(void)
{
        int32_t AHT20ReadTempAndHumi[2] = {0};
        LedSta LEDSta = LED_STA_ON;
        
        board_init();    /* 板卡初始化 */
        LED_Init();      /* LED初始化 */
        if(AHT20_INIT_RES_OK == AHT20_Init())
        {
                /* AHT20初始化成功 */
                printf("AHT20 Init OK\r\n");
        }
        else
        {
                /* AHT20初始化失败 */
                printf("AHT20 Init Err!!!\r\n");
        }
        LED_SetSta(LEDSta);
    while (1) 
        {
        /* 读取 ATH20 传感器状态 */
        while(AHT20_STA_ERR == AHT20_ReadSta())
        {
                        /* 重新初始化AHT20 */
            AHT20_Init();           
            bflb_mtimer_delay_ms(30);
        }
                /* 获取温湿度数据 */
                if (AHT20_READ_OK == AHT20_RecTempAndHumiVal(AHT20ReadTempAndHumi))
                {
                        /* 获取成功 */
                        printf("AHT20 Read Temperature is:%.1f,Humidity is %.1f\r\n",(float)AHT20ReadTempAndHumi[1]/10, (float)AHT20ReadTempAndHumi[0]/10);
                }
                else
                {
                        /* 获取失败 */
                        printf("Read Err!!!\r\n");
                }
                bflb_mtimer_delay_ms(1000);
                /* LED状态翻转,用来识别MCU是否正常工作 */
                if (LED_STA_ON == LEDSta)
                {
                        LEDSta = LED_STA_OFF;
                }
                else
                {
                        LEDSta = LED_STA_ON;
                }
                
                LED_SetSta(LEDSta);
    }
}
      main函数的主要功能是1S通过AHT20读取一下温湿度值,若读取成功则通过打印输出温湿度值。通过绿灯频率为1S的闪烁告知MCU正常运行。 
       调用AHT20读取温湿度的步骤 
              1、初始化AHT20(包括IIC初始化)AHT20_Init() 
              2、在初始化成功后读取AHT20的状态AHT20_ReadSta()  3、在状态为空闲时读取温湿度数据AHT20_RecTempAndHumiVal() 
              4、读取成功后就可以进行其他操作,示例代码是将温湿度数据打印出来,需要注意的是第一个数据为湿度,第二个为温度,且都放大10倍 
       在移植过程中需要注意的是在CMakeLists.txt文件要加入AHT20的.c/h文件,添加语句为复制代码target_sources(app PRIVATE AHT20.c )
AHT20示例代码验证    在下载好驱动代码后首先是修改Makefile文件SDK的路径,修改如下的值
 然后通过终端编译和下载程序。复制代码BL_SDK_BASE ?= E:/AiPi/AiPi-Open-Kits/aithinker_Ai-M6X_SDK/
通过串口软件可以查看MCU的运行状态和读取到的温湿度值。MCU正常运行和正常读取到温湿度值的打印状态如下图所示
 
 
常见问题解答        1、程序编译和烧写都正常,但是示例程序跑起来绿灯没有闪烁,打印如下图所示 
    这个有可能是IIC初始化失败或没有进行初始化,这也是我在自测的时候发现的         2、在终端输入make或make clean后出现如下图所示 
    这个可能是因为SDK的路径不对,需要修改Makefile文件的变量值BL_SDK_BASE ?=         在每个函数中都有打印信息,在示例代码中将其注释掉,在调试过程中可以将这些打印信息打开,这样就可以方便定位到问题的所在。我这里总结的是我在调试过程中遇到的问题,并且给出问题的原因。还有一些其他的问题可以在评论中发表出来。我也发了问题总结帖子 ,其他问题可以先在这个帖子里查找,以后也会不断补充。 |