#2020征文-开发板# Hi3861测温湿度显示一个新手开发调试过程 精华

细嗅蔷薇05
发布于 2021-1-10 20:24
浏览
11收藏

        使用的器件:Hi3861 + AHT20 + SSD1306 

 

        配好开发环境轻松做完点灯任务后,便想搞个像样点的应用,然后……决定做一个测量温湿度计,开始觉得这个实现比较简单,通过Hi3861读取AHT20测出的温湿度值,然后通过OLED显示出来。

 

        首先我的嵌入式开发知识非常少,曾经做过几年Java开发,玩过一点51单片机,在Linux上部署过一些Web服务。仅此而已,所以我的知识无法宏观上把这个事情想的很清楚,只能一步一步的试。如果你也是新入手嵌入式开发的朋友,那我的过程可能会对你有借鉴。

 

        先构建一个技术轮廓:每种电子传感器都是包括3类端口,(1)第1类电源:必需要2根电源线给模块供电,正负极,有的模块有多组供电;(2)第2类控制:然后有的模块会有一些控制端口,每种模块都不经相同,有的模块直接读数是没有这种控制端口的;(3)第3类通信线:跟核心单片机通信的信号端口,根据采用的通信协议不同,端口数量不同。要使用的AHT20和SSD1306采用的都是I2C通信协议,所以都是2根通信线。

我所用到的模块都很简单,主要涵盖的都是这3类端口,所以不管看到模块打扮成什么样,所要解决的主要问题都是类似的。主要做的就是通过通信端口向模块读写数据。

 

        模块的配置:各种模块控制和存储数据都是由一组组8位的寄存器控制的,每个寄存器里有8位,每一位可以存储1或者0,组成1个字节值,每种模块都有自己的功能设置和存储设置,可以想象成高阶语言里的关键字,寄存器值就是它本来的样子,一组组数字直接看是不会看懂它代表什么意思的,所以要依靠模块提供的技术手册做指导,一边看手册一边设置,单片机开发就这是这么朴实无华。

 

        关于通信协议:要使用的这2个模块采用的都是I2C通信协议,2根线一根信号一根时钟,通信双方就是通过互相占用通信线,相互发送高低电平传递消息,就是他们不能同步通信的,一方发送一方只能接收。因为用的线少所以通信过程非常繁琐,一方喊话问某地址的模块是否在线,然后等待,对方如果收到喊话,然后给个应答,当收到应答,再发送指令告诉他准备干什么,然后等待确认,模块收到后发确认…………,这个过程我在51上模拟过,好痛苦啊,一个时钟信号一个数据信号的数……,但是!!!在鸿蒙上所有的繁琐过程都被封装好了,我们只需要简单的调用系统提供的I2C操作方法,具体过程完全不用考虑,经过使用,真的好用,非常好用,好简单啊!所以I2C基本流程熟悉一下即可,在开发过程中具体的工作非常少。

 

        SSD1306

 

        首先是点亮屏幕,一旦能使用屏幕了,等于单片机对你打开了一扇窗户。SSD1306并不是OLED,它是驱动OLED显示的控制芯片,很多模块本身就是一个复杂的单片机,我们用的OLED屏幕是128*64像素组成的,本质上你可以简单的理解为高阶点灯。对SSD1306的控制也是通过I2C实现的,虽然它支持很多种通信协议,但是惜端如金的Hi3861采用了端口占用最少的I2C。

 

        我们只是需要向SSD1306发送数据,没有反馈值。所以通信过程比较简单SSD1306的地址0x78,0x00为接收命令,0x40为接收数据。把这个高度重复的过程做到1个函数里,直接调用就好。

 

// I2C协议 读写函数 只有写需求, cd = 0 写指令 cd = 1 写数据 byt 要写入的值 
void SSD1306_I2C_W(unsigned char cd, unsigned char byt)
{

    unsigned int state = 0; // I2C 运行 状态

    WifiIotI2cIdx id = WIFI_IOT_I2C_IDX_0;  //I2C 通道 0

    unsigned short deviceAddr = 0x78; // SSD1306 地址 

    WifiIotI2cData i2cData = {0};   // 接收发送信息的数组 查 wifiiot_i2c.h 看详细说明

    unsigned char buf[] = {0x00, byt};  //默认 0x00 写入 指令集 byt 要写入的指令  

    if(cd == 1)     // 输入 数据
    {
        buf[0] = 0x40;  // 0x40 表示写入的是数据 byt 就是要写入的数据              
    }

    i2cData.sendBuf = buf;
    i2cData.sendLen = 2;

    state = I2cWrite(id, deviceAddr, &i2cData);
    
    if(state != WIFI_IOT_SUCCESS)
    {
        printf("[SSD1306_I2C_W] write error : < %d > !!! \r\n", state);   // 如果状态异常 就打印 错误信息
    }

    // return state;    // 也可以作为返回值 

}

        驱动命令比较多,这是遇到的第一个障碍,看了手册,还有网络上各种例子,各式各样,虽然大同小异但是更是一头雾水。然后……以手册流程图为准自己写。不要怕,大胆试,好不好用试了才知道。

 

        有的设置是需要成对出现的,一个命令配一个参数,但是很多例子全部放在一起,一边看参数一边对照命令表……崩溃,虽然我现在也没搞懂有些命令的功能,但是以手册默认值为准,最后运行的很好。哈。

 

        驱动流程:

#2020征文-开发板# Hi3861测温湿度显示一个新手开发调试过程-鸿蒙开发者社区

        大多例子都是默认用页显示的方式,开始我也是用页显示的方式,用用就根据自己的需要改成水平方式了,建1个2维数组存放显示的信息,显示函数跟画面函数分离,这样做画面的时候专注做画面。这样还有个好处,就是以后代码的重用会比较方便。这样做还是为了简单实现在任意坐标显示,以后画个波显示更方便一点。

        这里补充1点,我开始按以前做小游戏的习惯做的画面控制,单片机还是模块好像都吃不消,看来还是越简单越好。

#2020征文-开发板# Hi3861测温湿度显示一个新手开发调试过程-鸿蒙开发者社区

        然后要用到1个辅助工具,PCtoLCD2002完美版-(字符模式),这个字模工具超好用,这里向作者表示由衷的感情,让最繁琐的工作变得非常轻松。使用的时候注意点选项设置,主要是方向,写段代码测一下就好了。

#2020征文-开发板# Hi3861测温湿度显示一个新手开发调试过程-鸿蒙开发者社区

        SSD1306一次接收1个字节的数据,表示对1列8个像素的开关控制,每个字节数据转成二进制代码,比如0xFF二进制1111 1111,每个1都代表点亮1个像素。0x00二进制0000 0000,就是关闭8个像素。

 

AHT20

 

          先看一下AHT20的技术手册,这个手册可以百度到(国内最小的半导体温湿度传感器AHT20研发成功,百度的结果,哈哈),在官网还可以下载到它的例程,这个模块功能很简单,所以手册看的很轻松。

 

        列一下工作流程:

 

        1、上电等待40ms

 

        2、发送0x71 查看AHT20状态指令

              查看状态值 [3] 是否为 1

              如果为1可以发送测量指令

             如果为0需要初始化:发送0xBE + 0x08 + 0x00初始化,初始化过程需要等待10ms

 

        3、发送0xAC + 0x33 + 0x00测量指令,测量过程需要等待80ms

              再发送0x71查看状态值[7]是否为0,如果为0表示测量完毕,否者等待。

              接收测量结果,收到7个字节的数据。

 

        主要2个步骤,查状态,以及测量读结果。

 

        我看到一个文章讲I2C协议是有专利权的,所以一般的产品使用这个协议都会或多或少的改一点,但是基本过程是一样的,并不会影响使用,这只是传闻我并没有证实。

 

        AHT20地址0x38换成2进制格式 111000,向左移1位结果是1110000,如果在最后1位就是[0]位设为0(1110000),就是发送读信息,如果[0]位设置为1(1110001)就是写信息。

        公式:0x38<<1 | 0x1 = 0x70 写地址; 0x38<<1 |0x0 = 0x71 读地址;

 

// 每个参数 都写在函数里 是为了方便理解阅读 最后做最终版 要尽量减少冗余操作

// i2c写入、读出操作; rw=0 写入 rw=1 读出; *buff 数据数组,读入就是指令集,返回就是空数组; leng 数组的长度 不可以为0; 
void AHT20_I2C_RW(unsigned char rw, unsigned char *buff, unsigned int leng)
{
    unsigned int state = 0; // I2C 运行 状态值 ,单列出来是为了方便作为返回值 做判断

    WifiIotI2cIdx id = WIFI_IOT_I2C_IDX_0;  //设置I2C使用的通道

    unsigned short writAddr = 0x70; // aht20 ((0x38<<1)|0x0)  写入地址
    unsigned short readAddr = 0x71; // aht20 ((0x38<<1)|0x1)  读出地址   
    
    WifiIotI2cData i2cData = {0};   // 参考 wiffiiot_i2c.h 的说明 位置在 \base\iot_hardware\interfaces\kits\wifiiot_lite

    if(rw == 0) // 写入
    {
        i2cData.sendBuf = buff;      //unsigned char*    发送 数据 指针
        i2cData.sendLen = leng;      //unsigned int      发送 数据 长度

        state = I2cWrite(id, writAddr, &i2cData);   //i2c写入方法 会有1个状态返回值 WIFI_IOT_SUCCESS = 0 代表成功 出错会返回错误代码 需要加入 wifiiot_errno.h 头文件        
    }
    else if(rw == 1) // 读出
    {
        i2cData.receiveBuf = buff;   //unsigned char*    接收 数据 指针
        i2cData.receiveLen = leng;   //unsigned int      接收 数据 长度

        state = I2cRead(id, readAddr, &i2cData);    //i2c读出方法       
    }

    if(state != WIFI_IOT_SUCCESS)   // 如果返回值 不等于 WIFI_IOT_SUCCESS 打印state 查询 wifiiot_errno.h 看错哪了
    {
        printf("[AHT20_I2C_RW] ERROR !!! %d : %d \r\n", rw, state);    // 打印 错误信息
    }

}

 

        这里要重点!重点!重点!的说一下,接收状态值,不要!不要!不要!再发送0x71指令了,直接I2C读,就会给你传1个状态值,默认的状态值的第[7]位是0,当你发送测量指令的时候,会变成1,进入测量状态,当测量完以后会重新置为0。开始因为一直无法读取到正确的状态值……#¥%&#¥%#@%#,一言难尽啊,已经过去了。

 

// 返回 AHT20 的状态值 i是第几位 0~7 
unsigned char AHT20_Status(unsigned char i)
{
    unsigned char buff[] = {0};
    unsigned char leng = 1;

    AHT20_I2C_RW(1, buff, leng);

    unsigned char s;    //返回状态值 0、1

    s = (buff[0] >> i) & 0x01;  //状态值是1个字节数据,我们只需要知道某位的具体值就行 i就是第几位

    //printf("[AHT20_Status] AHT20_Status 0x%x [%d : %d] !!! \r\n", buff4[0], i, s);    // 此行 调试用 打印 状态值

    return s;
}

 

        结果会返回7个字节数据,第1个字节是状态信息,第2个、第3个、以及第4个字节的高4位[7][6][5][4]共同组成了湿度值,第4个字节的低4位[3][2][1][0]、第5位、第6位组成温度值,最后第7个字节是效验位。

 

        然后效验,主要是目的是检验接收到的数据是否在传输的过程中出现错误,具体原理和公式以及代码不仔细说了,过程太碎了,几次测试,代码没问题,具体过程百度一下好多资料,检验的时候一定要包括第1个状态字节。

        检验这步骤不是必须的,但是我开始的时候发现读到的数错的离谱,开始不清楚是我代码写的问题还是模块本身的问题,然后就把检验这个步骤也写上了,最后发现模块没问题,代码也没问题,问题是模块上有个气体传感器会发热,所以温度值高。真的好晕。

 

        最后一步就是就是把读到的数值转化为正常的10进制值。

        技术手册湿度的公式后面有个%号,那个是湿度百分比的意思,我还纠结过×100%有啥意义呢?当你没最后完全走通的时候就像在黑暗里摸索,一个小坑都能把你绊的够呛。

        转化的过程要注意数值的类型,本来这个取值就是取很小的零头,如果类型用错了,就给抹掉了。哈。

 

// AHT20 测量温湿度值
unsigned char AHT20_Measure(float *ht)
{
    // 发送 测量指令
    unsigned char buff1[] = {0xac, 0x33, 0x00};
    unsigned char leng1 = 3;

    AHT20_I2C_RW(0, buff1, leng1);  // 默认状态值第[7]位是0,发送完测量指令后状态值[7]会置1

    usleep(80*1000); //等待 80ms 时钟偏快的 所以这个时间内 总是不能完成测量

    unsigned char t = 10;    //等待时间的值

    while(AHT20_Status(7) != 0) //检查 状态值第[7]位是否从1变为0,如果没变就等待5ms,如果已经置0说明测量完成
    {
        usleep(10*1000);   //10ms 这个时间不要设置太长,也不要设置太短,太长时间,小器件很难长时间存储测量结果,太短反复应答也能会影响测量的稳定性,一般情况等1次就会过 

        if(--t == 0)  // 如果等待时间很长依然没有变0,说明设备可能出现异常,为了避免死机,返回0,重新测量 这个情况我还没遇到
        {
            return 0;
        }
    }

    // 接收 测量结果
    unsigned char buff2[7] = {0};
    unsigned char leng2 = 7;

    AHT20_I2C_RW(1, buff2, leng2);  // 读返回结果,一共7个字节,第1个字节是状态值 最后1个字节是效验值

    unsigned char i, j;
    unsigned char crc = 0xFF;   // 效验 初值

    // CRC 效验 固定算法 
    for(i=0; i<6; i++)
	{
		crc ^= (buff2[i]);
		
		for(j=8; j>0; --j)
		{
			if(crc & 0x80)
			{
				crc = (crc << 1) ^ 0x31;
			}
			else
			{
				crc = (crc << 1);
			} 
    	}
  	}

    if(buff2[6] != crc) //CRC值不对 说明传输过程可能有干扰 出错了
    {
        //printf(" CRC8 NO \r\n");
        return 0;   // 效验不正确 回执1个错误信息
    }

    unsigned int dat1 = 0;  // 湿度
    unsigned int dat2 = 0;  // 温度

    dat1 = (dat1 | buff2[1]) << 8;
    dat1 = (dat1 | buff2[2]) << 8;
    dat1 = (dat1 | buff2[3]) >> 4;

    dat2 = (dat2 | buff2[3]) << 8;
    dat2 = (dat2 | buff2[4]) << 8;
    dat2 = (dat2 | buff2[5]) & 0xfffff;

    // 这1大段搬来搬去的 主要是因为 buff2[3] 前4位属于湿度 后4位属于 温度
    // 单片机处理能力有限,主要是针对寄存器值的处理,使用位运算,这样能节省算力
    // 处理数据 单列出来 便于理解,代码写的太简练 不容易看懂 最终 不需要这么繁琐

    float hum = 0;  // 温度
    float tem = 0;  // 湿度

    // 2^20=1048576 要先类型转换 暂时先这么写 以后再改得顺溜点
    hum = ((float)dat1 / (float)1048576) * (float)100;                 // 湿度
    tem = ((float)dat2 / (float)1048576) * (float)200 - (float)50;     // 温度

    ht[0] = hum;
    ht[1] = tem;

    return 1;   // 测量完毕

}

 

        软复位,无需关闭再次打开电源的情况下重新启动传感器。就是软重启,长时间停用的再次访问的时候使用,我这个小应用基本用不到,但是还是写上吧。输入指令0xBA需要等待20ms。

 

      剩下工作就是把AHT20的代码和SSD1306的代码整合到一起。这里要说1点,I2C支持串联多个设备,所以AHT20和SSD1306在一条I2C线上共同使用是没有问题的。

 

#2020征文-开发板# Hi3861测温湿度显示一个新手开发调试过程-鸿蒙开发者社区

        C语言从来不是我主要的使用语言,所以超级菜啊,一边写一边看C 语言教程。装个Dev-C++编译器,有些功能先写个测试代码看看。开始写代码,不要考虑效率问题,就只想怎么更适合阅读。开始写主要的目的是试错,写一步编译一步,不要一次写全所有功能。

        我觉得鸿蒙系统编译报错功能非常好,我的每个错误都能被准确的指出来。开始专用的查BUG功能对新手来说很难,可以用printf串口打印功能就行,真的好用,因为鸿蒙是多任务系统,所有的功能都是一个单独的任务,即使你的代码运行跑坏了,但是系统不会崩,打印功能依然会给你打印出信息来。第一次用真觉得好高级,能直接看到单片机的回话了。

 

      最后不断对代码迭代优化,最终的目的让代码可以更好的被重用。以后还要用的嘛,重复的工作就不要做了。编程过程对我这种新手来说,真是经历情况太多,开发过程发生的各种事以后以后单开一篇碎碎念再讲吧。

 

      知识有限又刚刚尝试,所以肯定会有很多错误,欢迎给我指正。开始觉得弄这个很简单,但是很快被现实教育,然后开始认真读各位大佬的教程,收获很大,这里由衷的感谢。

 

 

分类
aht_oled_demo.zip 9.3K 277次下载
已于2021-1-11 12:50:27修改
12
收藏 11
回复
举报
13条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

可以,楼主描述的很详细

1
回复
2021-1-11 11:24:59
Whyalone
Whyalone

先去试试,有时间再回来交作业

回复
2021-1-11 11:41:17
..._...
..._...

请问怎么知道设备的地址是0x78呢?

回复
2021-7-8 10:45:20
鸿联
鸿联

学习了,很有用

回复
2021-7-8 21:33:18
细嗅蔷薇05
细嗅蔷薇05 回复了 ..._...
请问怎么知道设备的地址是0x78呢?

这个是通过看技术手册得到的,每个元器件都有自己的地址,官方手册都会有说明。

回复
2021-7-13 22:15:41
wx590726886001f
wx590726886001f

楼主您好,麻烦问下润和的这个板子和小熊派的可以通用吗

回复
2021-7-13 22:35:34
细嗅蔷薇05
细嗅蔷薇05 回复了 wx590726886001f
楼主您好,麻烦问下润和的这个板子和小熊派的可以通用吗

他们使用相同的芯片,代码是通用的,唯一注意的就是使用的端口,不同开发者根据自己的情况用的端口不一样。

回复
2021-7-15 14:41:48
txwtech
txwtech

博主好,鸿蒙开发板hi3861 hispark code 2.0 canary金丝雀版本,gpio如何上拉电阻呢?

code 2.0 LTS支持,canary没有这个功能呢

IoSetPull(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_IO_PULL_UP);

谢谢

回复
2021-8-19 09:16:05
细嗅蔷薇05
细嗅蔷薇05 回复了 txwtech
博主好,鸿蒙开发板hi3861 hispark code 2.0 canary金丝雀版本,gpio如何上拉电阻呢? code 2.0 LTS支持,canary没有这个功能呢 IoSetPull(WIFI_IOT_IO_NAME_GPIO_8, WIFI_IOT_IO_PULL_UP); 谢谢

我只有官方3861套件,其他开发板不太清楚,但是芯片是一样的,代码也应该是一样的啊!2.2的系统结构有点变化,引入路径看看对不对,"/base/iot_hardware/interfaces/kits/wifiiot_lite",拉力设置一般在gpio.h的扩展gpio_ex.h下面。

回复
2021-8-20 16:20:21
休止符、
休止符、 回复了 ..._...
请问怎么知道设备的地址是0x78呢?

看AHT20温湿度传感器的手册

回复
2021-9-7 16:30:57
休止符、
休止符、

请问这个代码是1.0的还是2.0的?

 

回复
2021-9-7 16:32:00
细嗅蔷薇05
细嗅蔷薇05 回复了 休止符、
请问这个代码是1.0的还是2.0的?

当时做这个的时候,只有LTS版1.1,Hi3861基于1.1入手是满足用的,如果是Hi3516需要调用的功能组件多,选2.0。马上一季结束了,应该又要有新版本出来了。

回复
2021-9-9 20:52:01
爱吃土豆丝的打工人
爱吃土豆丝的打工人

虽然  不懂硬件开发  但是文章 确实挺好

回复
2021-9-10 09:39:34
回复
    相关推荐