鴻蒙Hi3861測溫濕度顯示一個(gè)新手開發(fā)調(diào)試過程
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
https://harmonyos.51cto.com/#zz
使用的器件:Hi3861 + AHT20 + SSD1306
配好開發(fā)環(huán)境輕松做完點(diǎn)燈任務(wù)后,便想搞個(gè)像樣點(diǎn)的應(yīng)用,然后……決定做一個(gè)測量溫濕度計(jì),開始覺得這個(gè)實(shí)現(xiàn)比較簡單,通過Hi3861讀取AHT20測出的溫濕度值,然后通過OLED顯示出來。
首先我的嵌入式開發(fā)知識(shí)非常少,曾經(jīng)做過幾年Java開發(fā),玩過一點(diǎn)51單片機(jī),在Linux上部署過一些Web服務(wù)。僅此而已,所以我的知識(shí)無法宏觀上把這個(gè)事情想的很清楚,只能一步一步的試。如果你也是新入手嵌入式開發(fā)的朋友,那我的過程可能會(huì)對(duì)你有借鑒。
先構(gòu)建一個(gè)技術(shù)輪廓:每種電子傳感器都是包括3類端口,(1)第1類電源:必需要2根電源線給模塊供電,正負(fù)極,有的模塊有多組供電;(2)第2類控制:然后有的模塊會(huì)有一些控制端口,每種模塊都不經(jīng)相同,有的模塊直接讀數(shù)是沒有這種控制端口的;(3)第3類通信線:跟核心單片機(jī)通信的信號(hào)端口,根據(jù)采用的通信協(xié)議不同,端口數(shù)量不同。要使用的AHT20和SSD1306采用的都是I2C通信協(xié)議,所以都是2根通信線。
我所用到的模塊都很簡單,主要涵蓋的都是這3類端口,所以不管看到模塊打扮成什么樣,所要解決的主要問題都是類似的。主要做的就是通過通信端口向模塊讀寫數(shù)據(jù)。
模塊的配置:各種模塊控制和存儲(chǔ)數(shù)據(jù)都是由一組組8位的寄存器控制的,每個(gè)寄存器里有8位,每一位可以存儲(chǔ)1或者0,組成1個(gè)字節(jié)值,每種模塊都有自己的功能設(shè)置和存儲(chǔ)設(shè)置,可以想象成高階語言里的關(guān)鍵字,寄存器值就是它本來的樣子,一組組數(shù)字直接看是不會(huì)看懂它代表什么意思的,所以要依靠模塊提供的技術(shù)手冊(cè)做指導(dǎo),一邊看手冊(cè)一邊設(shè)置,單片機(jī)開發(fā)就這是這么樸實(shí)無華。
關(guān)于通信協(xié)議:要使用的這2個(gè)模塊采用的都是I2C通信協(xié)議,2根線一根信號(hào)一根時(shí)鐘,通信雙方就是通過互相占用通信線,相互發(fā)送高低電平傳遞消息,就是他們不能同步通信的,一方發(fā)送一方只能接收。因?yàn)橛玫木€少所以通信過程非常繁瑣,一方喊話問某地址的模塊是否在線,然后等待,對(duì)方如果收到喊話,然后給個(gè)應(yīng)答,當(dāng)收到應(yīng)答,再發(fā)送指令告訴他準(zhǔn)備干什么,然后等待確認(rèn),模塊收到后發(fā)確認(rèn)…………,這個(gè)過程我在51上模擬過,好痛苦啊,一個(gè)時(shí)鐘信號(hào)一個(gè)數(shù)據(jù)信號(hào)的數(shù)……,但是!!!在鴻蒙上所有的繁瑣過程都被封裝好了,我們只需要簡單的調(diào)用系統(tǒng)提供的I2C操作方法,具體過程完全不用考慮,經(jīng)過使用,真的好用,非常好用,好簡單啊!所以I2C基本流程熟悉一下即可,在開發(fā)過程中具體的工作非常少。
SSD1306
首先是點(diǎn)亮屏幕,一旦能使用屏幕了,等于單片機(jī)對(duì)你打開了一扇窗戶。SSD1306并不是OLED,它是驅(qū)動(dòng)OLED顯示的控制芯片,很多模塊本身就是一個(gè)復(fù)雜的單片機(jī),我們用的OLED屏幕是128*64像素組成的,本質(zhì)上你可以簡單的理解為高階點(diǎn)燈。對(duì)SSD1306的控制也是通過I2C實(shí)現(xiàn)的,雖然它支持很多種通信協(xié)議,但是惜端如金的Hi3861采用了端口占用最少的I2C。
我們只是需要向SSD1306發(fā)送數(shù)據(jù),沒有反饋值。所以通信過程比較簡單SSD1306的地址0x78,0x00為接收命令,0x40為接收數(shù)據(jù)。把這個(gè)高度重復(fù)的過程做到1個(gè)函數(shù)里,直接調(diào)用就好。
- // I2C協(xié)議 讀寫函數(shù) 只有寫需求, cd = 0 寫指令 cd = 1 寫數(shù)據(jù) byt 要寫入的值
- void SSD1306_I2C_W(unsigned char cd, unsigned char byt)
- {
- unsigned int state = 0; // I2C 運(yùn)行 狀態(tài)
- WifiIotI2cIdx id = WIFI_IOT_I2C_IDX_0; //I2C 通道 0
- unsigned short deviceAddr = 0x78; // SSD1306 地址
- WifiIotI2cData i2cData = {0}; // 接收發(fā)送信息的數(shù)組 查 wifiiot_i2c.h 看詳細(xì)說明
- unsigned char buf[] = {0x00, byt}; //默認(rèn) 0x00 寫入 指令集 byt 要寫入的指令
- if(cd == 1) // 輸入 數(shù)據(jù)
- {
- buf[0] = 0x40; // 0x40 表示寫入的是數(shù)據(jù) byt 就是要寫入的數(shù)據(jù)
- }
- 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); // 如果狀態(tài)異常 就打印 錯(cuò)誤信息
- }
- // return state; // 也可以作為返回值
- }
驅(qū)動(dòng)命令比較多,這是遇到的第一個(gè)障礙,看了手冊(cè),還有網(wǎng)絡(luò)上各種例子,各式各樣,雖然大同小異但是更是一頭霧水。然后……以手冊(cè)流程圖為準(zhǔn)自己寫。不要怕,大膽試,好不好用試了才知道。
有的設(shè)置是需要成對(duì)出現(xiàn)的,一個(gè)命令配一個(gè)參數(shù),但是很多例子全部放在一起,一邊看參數(shù)一邊對(duì)照命令表……崩潰,雖然我現(xiàn)在也沒搞懂有些命令的功能,但是以手冊(cè)默認(rèn)值為準(zhǔn),最后運(yùn)行的很好。哈。
驅(qū)動(dòng)流程:

大多例子都是默認(rèn)用頁顯示的方式,開始我也是用頁顯示的方式,用用就根據(jù)自己的需要改成水平方式了,建1個(gè)2維數(shù)組存放顯示的信息,顯示函數(shù)跟畫面函數(shù)分離,這樣做畫面的時(shí)候?qū)W⒆霎嬅妗_@樣還有個(gè)好處,就是以后代碼的重用會(huì)比較方便。這樣做還是為了簡單實(shí)現(xiàn)在任意坐標(biāo)顯示,以后畫個(gè)波顯示更方便一點(diǎn)。
這里補(bǔ)充1點(diǎn),我開始按以前做小游戲的習(xí)慣做的畫面控制,單片機(jī)還是模塊好像都吃不消,看來還是越簡單越好。

然后要用到1個(gè)輔助工具,PCtoLCD2002完美版-(字符模式),這個(gè)字模工具超好用,這里向作者表示由衷的感情,讓最繁瑣的工作變得非常輕松。使用的時(shí)候注意點(diǎn)選項(xiàng)設(shè)置,主要是方向,寫段代碼測一下就好了。

SSD1306一次接收1個(gè)字節(jié)的數(shù)據(jù),表示對(duì)1列8個(gè)像素的開關(guān)控制,每個(gè)字節(jié)數(shù)據(jù)轉(zhuǎn)成二進(jìn)制代碼,比如0xFF二進(jìn)制1111 1111,每個(gè)1都代表點(diǎn)亮1個(gè)像素。0x00二進(jìn)制0000 0000,就是關(guān)閉8個(gè)像素。
AHT20
先看一下AHT20的技術(shù)手冊(cè),這個(gè)手冊(cè)可以百度到(國內(nèi)最小的半導(dǎo)體溫濕度傳感器AHT20研發(fā)成功,百度的結(jié)果,哈哈),在官網(wǎng)還可以下載到它的例程,這個(gè)模塊功能很簡單,所以手冊(cè)看的很輕松。
列一下工作流程:
1、上電等待40ms
2、發(fā)送0x71 查看AHT20狀態(tài)指令
查看狀態(tài)值 [3] 是否為 1
如果為1可以發(fā)送測量指令
如果為0需要初始化:發(fā)送0xBE + 0x08 + 0x00初始化,初始化過程需要等待10ms
3、發(fā)送0xAC + 0x33 + 0x00測量指令,測量過程需要等待80ms
再發(fā)送0x71查看狀態(tài)值[7]是否為0,如果為0表示測量完畢,否者等待。
接收測量結(jié)果,收到7個(gè)字節(jié)的數(shù)據(jù)。
主要2個(gè)步驟,查狀態(tài),以及測量讀結(jié)果。
我看到一個(gè)文章講I2C協(xié)議是有專利權(quán)的,所以一般的產(chǎn)品使用這個(gè)協(xié)議都會(huì)或多或少的改一點(diǎn),但是基本過程是一樣的,并不會(huì)影響使用,這只是傳聞我并沒有證實(shí)。
AHT20地址0x38換成2進(jìn)制格式 111000,向左移1位結(jié)果是1110000,如果在最后1位就是[0]位設(shè)為0(1110000),就是發(fā)送讀信息,如果[0]位設(shè)置為1(1110001)就是寫信息。
公式:0x38<<1 | 0x1 = 0x70 寫地址; 0x38<<1 |0x0 = 0x71 讀地址;
- // 每個(gè)參數(shù) 都寫在函數(shù)里 是為了方便理解閱讀 最后做最終版 要盡量減少冗余操作
- // i2c寫入、讀出操作; rw=0 寫入 rw=1 讀出; *buff 數(shù)據(jù)數(shù)組,讀入就是指令集,返回就是空數(shù)組; leng 數(shù)組的長度 不可以為0;
- void AHT20_I2C_RW(unsigned char rw, unsigned char *buff, unsigned int leng)
- {
- unsigned int state = 0; // I2C 運(yùn)行 狀態(tài)值 ,單列出來是為了方便作為返回值 做判斷
- WifiIotI2cIdx id = WIFI_IOT_I2C_IDX_0; //設(shè)置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* 發(fā)送 數(shù)據(jù) 指針
- i2cData.sendLen = leng; //unsigned int 發(fā)送 數(shù)據(jù) 長度
- state = I2cWrite(id, writAddr, &i2cData); //i2c寫入方法 會(huì)有1個(gè)狀態(tài)返回值 WIFI_IOT_SUCCESS = 0 代表成功 出錯(cuò)會(huì)返回錯(cuò)誤代碼 需要加入 wifiiot_errno.h 頭文件
- }
- else if(rw == 1) // 讀出
- {
- i2cData.receiveBuf = buff; //unsigned char* 接收 數(shù)據(jù) 指針
- i2cData.receiveLen = leng; //unsigned int 接收 數(shù)據(jù) 長度
- state = I2cRead(id, readAddr, &i2cData); //i2c讀出方法
- }
- if(state != WIFI_IOT_SUCCESS) // 如果返回值 不等于 WIFI_IOT_SUCCESS 打印state 查詢 wifiiot_errno.h 看錯(cuò)哪了
- {
- printf("[AHT20_I2C_RW] ERROR !!! %d : %d \r\n", rw, state); // 打印 錯(cuò)誤信息
- }
- }
這里要重點(diǎn)!重點(diǎn)!重點(diǎn)!的說一下,接收狀態(tài)值,不要!不要!不要!再發(fā)送0x71指令了,直接I2C讀,就會(huì)給你傳1個(gè)狀態(tài)值,默認(rèn)的狀態(tài)值的第[7]位是0,當(dāng)你發(fā)送測量指令的時(shí)候,會(huì)變成1,進(jìn)入測量狀態(tài),當(dāng)測量完以后會(huì)重新置為0。開始因?yàn)橐恢睙o法讀取到正確的狀態(tài)值……#¥%&#¥%#@%#,一言難盡啊,已經(jīng)過去了。
- // 返回 AHT20 的狀態(tài)值 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; //返回狀態(tài)值 0、1
- s = (buff[0] >> i) & 0x01; //狀態(tài)值是1個(gè)字節(jié)數(shù)據(jù),我們只需要知道某位的具體值就行 i就是第幾位
- //printf("[AHT20_Status] AHT20_Status 0x%x [%d : %d] !!! \r\n", buff4[0], i, s); // 此行 調(diào)試用 打印 狀態(tài)值
- return s;
- }
結(jié)果會(huì)返回7個(gè)字節(jié)數(shù)據(jù),第1個(gè)字節(jié)是狀態(tài)信息,第2個(gè)、第3個(gè)、以及第4個(gè)字節(jié)的高4位[7][6][5][4]共同組成了濕度值,第4個(gè)字節(jié)的低4位[3][2][1][0]、第5位、第6位組成溫度值,最后第7個(gè)字節(jié)是效驗(yàn)位。
然后效驗(yàn),主要是目的是檢驗(yàn)接收到的數(shù)據(jù)是否在傳輸?shù)倪^程中出現(xiàn)錯(cuò)誤,具體原理和公式以及代碼不仔細(xì)說了,過程太碎了,幾次測試,代碼沒問題,具體過程百度一下好多資料,檢驗(yàn)的時(shí)候一定要包括第1個(gè)狀態(tài)字節(jié)。
檢驗(yàn)這步驟不是必須的,但是我開始的時(shí)候發(fā)現(xiàn)讀到的數(shù)錯(cuò)的離譜,開始不清楚是我代碼寫的問題還是模塊本身的問題,然后就把檢驗(yàn)這個(gè)步驟也寫上了,最后發(fā)現(xiàn)模塊沒問題,代碼也沒問題,問題是模塊上有個(gè)氣體傳感器會(huì)發(fā)熱,所以溫度值高。真的好暈。
最后一步就是就是把讀到的數(shù)值轉(zhuǎn)化為正常的10進(jìn)制值。
技術(shù)手冊(cè)濕度的公式后面有個(gè)%號(hào),那個(gè)是濕度百分比的意思,我還糾結(jié)過×100%有啥意義呢?當(dāng)你沒最后完全走通的時(shí)候就像在黑暗里摸索,一個(gè)小坑都能把你絆的夠嗆。
轉(zhuǎn)化的過程要注意數(shù)值的類型,本來這個(gè)取值就是取很小的零頭,如果類型用錯(cuò)了,就給抹掉了。哈。
- // AHT20 測量溫濕度值
- unsigned char AHT20_Measure(float *ht)
- {
- // 發(fā)送 測量指令
- unsigned char buff1[] = {0xac, 0x33, 0x00};
- unsigned char leng1 = 3;
- AHT20_I2C_RW(0, buff1, leng1); // 默認(rèn)狀態(tài)值第[7]位是0,發(fā)送完測量指令后狀態(tài)值[7]會(huì)置1
- usleep(80*1000); //等待 80ms 時(shí)鐘偏快的 所以這個(gè)時(shí)間內(nèi) 總是不能完成測量
- unsigned char t = 10; //等待時(shí)間的值
- while(AHT20_Status(7) != 0) //檢查 狀態(tài)值第[7]位是否從1變?yōu)?,如果沒變就等待5ms,如果已經(jīng)置0說明測量完成
- {
- usleep(10*1000); //10ms 這個(gè)時(shí)間不要設(shè)置太長,也不要設(shè)置太短,太長時(shí)間,小器件很難長時(shí)間存儲(chǔ)測量結(jié)果,太短反復(fù)應(yīng)答也能會(huì)影響測量的穩(wěn)定性,一般情況等1次就會(huì)過
- if(--t == 0) // 如果等待時(shí)間很長依然沒有變0,說明設(shè)備可能出現(xiàn)異常,為了避免死機(jī),返回0,重新測量 這個(gè)情況我還沒遇到
- {
- return 0;
- }
- }
- // 接收 測量結(jié)果
- unsigned char buff2[7] = {0};
- unsigned char leng2 = 7;
- AHT20_I2C_RW(1, buff2, leng2); // 讀返回結(jié)果,一共7個(gè)字節(jié),第1個(gè)字節(jié)是狀態(tài)值 最后1個(gè)字節(jié)是效驗(yàn)值
- unsigned char i, j;
- unsigned char crc = 0xFF; // 效驗(yàn) 初值
- // CRC 效驗(yàn) 固定算法
- 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值不對(duì) 說明傳輸過程可能有干擾 出錯(cuò)了
- {
- //printf(" CRC8 NO \r\n");
- return 0; // 效驗(yàn)不正確 回執(zhí)1個(gè)錯(cuò)誤信息
- }
- 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大段搬來搬去的 主要是因?yàn)?nbsp;buff2[3] 前4位屬于濕度 后4位屬于 溫度
- // 單片機(jī)處理能力有限,主要是針對(duì)寄存器值的處理,使用位運(yùn)算,這樣能節(jié)省算力
- // 處理數(shù)據(jù) 單列出來 便于理解,代碼寫的太簡練 不容易看懂 最終 不需要這么繁瑣
- float hum = 0; // 溫度
- float tem = 0; // 濕度
- // 2^20=1048576 要先類型轉(zhuǎn)換 暫時(shí)先這么寫 以后再改得順溜點(diǎn)
- hum = ((float)dat1 / (float)1048576) * (float)100; // 濕度
- tem = ((float)dat2 / (float)1048576) * (float)200 - (float)50; // 溫度
- ht[0] = hum;
- ht[1] = tem;
- return 1; // 測量完畢
- }
軟復(fù)位,無需關(guān)閉再次打開電源的情況下重新啟動(dòng)傳感器。就是軟重啟,長時(shí)間停用的再次訪問的時(shí)候使用,我這個(gè)小應(yīng)用基本用不到,但是還是寫上吧。輸入指令0xBA需要等待20ms。
剩下工作就是把AHT20的代碼和SSD1306的代碼整合到一起。這里要說1點(diǎn),I2C支持串聯(lián)多個(gè)設(shè)備,所以AHT20和SSD1306在一條I2C線上共同使用是沒有問題的。

C語言從來不是我主要的使用語言,所以超級(jí)菜啊,一邊寫一邊看C 語言教程。裝個(gè)Dev-C++編譯器,有些功能先寫個(gè)測試代碼看看。開始寫代碼,不要考慮效率問題,就只想怎么更適合閱讀。開始寫主要的目的是試錯(cuò),寫一步編譯一步,不要一次寫全所有功能。
我覺得鴻蒙系統(tǒng)編譯報(bào)錯(cuò)功能非常好,我的每個(gè)錯(cuò)誤都能被準(zhǔn)確的指出來。開始專用的查BUG功能對(duì)新手來說很難,可以用printf串口打印功能就行,真的好用,因?yàn)轼櫭墒嵌嗳蝿?wù)系統(tǒng),所有的功能都是一個(gè)單獨(dú)的任務(wù),即使你的代碼運(yùn)行跑壞了,但是系統(tǒng)不會(huì)崩,打印功能依然會(huì)給你打印出信息來。第一次用真覺得好高級(jí),能直接看到單片機(jī)的回話了。
最后不斷對(duì)代碼迭代優(yōu)化,最終的目的讓代碼可以更好的被重用。以后還要用的嘛,重復(fù)的工作就不要做了。編程過程對(duì)我這種新手來說,真是經(jīng)歷情況太多,開發(fā)過程發(fā)生的各種事以后以后單開一篇碎碎念再講吧。
知識(shí)有限又剛剛嘗試,所以肯定會(huì)有很多錯(cuò)誤,歡迎給我指正。開始覺得弄這個(gè)很簡單,但是很快被現(xiàn)實(shí)教育,然后開始認(rèn)真讀各位大佬的教程,收獲很大,這里由衷的感謝。
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
https://harmonyos.51cto.com/#zz