2017年8月7日 星期一

MicroPython on ESP8266 (十七) : 液晶顯示器 1602A 測試

這幾天在零件箱裡找到之前購買的兩個 1602 LCD 顯示器, 一個是 5V 的, 另一個是 3.3V 的, 同時還發現剛好有買過兩個 1602 I2C 介面模組, 與 1602 搭配可以減少 GPIO 的耗用, 因為直接控制 1602 需要用掉 7 個 GPIO, 而使用 I2C 則只需兩個即可 (連接 I2C 設備的 SDA 與 SCL), 可節省 5 個 IO 腳. 由於 1602 I2C 模組買來時已經焊好排針 (1*16), 所以我跑去禾樺買了兩個 1*16 的排母焊在 1602 背面, 這樣 1602 I2C 模組就可以插在背面的排母了, 不用焊死, 如下圖所示 :



注意, 上面那塊是 3.3V 的 1602, 下面那塊則是 5V 的, 差別是 3.3V 的在背面左邊圈起來處有多一顆 IC 與 2 顆電容, 至於 1602 I2C 模組則不論 3.3V 或 5V 均可以運作. 因此上面那塊 1602 I2C 模組的 VCC 就接 3.3V; 而下面這塊的 VCC 則接 5V. 我有試過將 5V 版的 1602 + I2C 接 3.3V 電源還是可用, 但是要將對比調到最大才看得清楚顯示的字.

事實上這個 1602 I2C 模組是利用一顆 PCF8574 IC 來將兩個 GPIO 腳 (即 SCL 與 SDA) 擴展為 8 個 IO 來控制 1602, 這樣就大幅減少微控器 GPIO 腳的耗用, 此外還可以利用板上的 A0, A1, A3 端子 (未焊), 多工選取多個 PCF8574, 最多可接 8 個 PCF8574. 此模組一端有兩支腳用來控制是否要連接背光電源, 預設用跳帽連起來表示有接上背光電源. 另一端是接微控器的 VCC, GND, SCL, 與 SDA 的 I2C 介面. 板上的藍色可變電阻是用來調整背光對比的, 順時針旋轉增加對比, 逆時針是減少對比. 注意, 3.3V 版的 1602 對比不可調太弱, 否則調節背光的 IC 會很燙, 參考 :

# 【DIY_LAB#790】Arduino IIC/I2C介面LCD1602/2004轉接板 $30
# 【傑森創工】藍底 1602 LCD 顯示器 已焊2004轉接板 $95
# Arduino IO擴展模組 PCF8574T模組 電子積木 $105
# PCF8574 IIC LCD1602 轉接板 轉接模組 W70[276507-045] $52

1602 I2C 模組上的 PCF8574 晶片有兩種, PCF8574T 的預設位址是 0x27; 而 PCF8574AT 則是 0x3F, 寫程式時需注意 1602 I2C 模組所使用的晶片是 PCF8574T 還是 PCF8574AT, 參考下面這篇的說明 :

# 【盼盼22】 LCD 1602 I2C 轉接板 1602液晶顯示器 IIC 界面接口 $29

關於 Arduino 版 1602 操作參考下列這篇, 不過我那時不是使用 I2C, 而是直接使用 Arduino 的 DIO 腳去控制1602, 本篇則是要探索在 ESP8266 上如何用 MicroPython 控制 1602 的顯示 :

# Arduino 液晶顯示器測試

本系列之前的測試紀錄參考 :

MicroPython on ESP8266 (二) : 數值型別測試
MicroPython on ESP8266 (三) : 序列型別測試
MicroPython on ESP8266 (四) : 字典與集合型別測試
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
MicroPython on ESP8266 (六) : 檔案系統測試
MicroPython on ESP8266 (七) : 時間日期測試
MicroPython on ESP8266 (八) : GPIO 測試
MicroPython on ESP8266 (九) : PIR 紅外線移動偵測
MicroPython v1.9.1 版韌體測試
MicroPython on ESP8266 (十) : socket 模組測試
MicroPython on ESP8266 (十一) : urllib.urequest 模組測試
MicroPython on ESP8266 (十二) : urequests 模組測試
MicroPython on ESP8266 (十三) : DHT11 溫溼度感測器測試
MicroPython on ESP8266 (十四) : 網頁伺服器測試
# WeMOS D1 Mini 開發板測試
MicroPython on ESP8266 (十五) : 光敏電阻與 ADC 測試
# MicroPython on ESP8266 (十六) : 蜂鳴器測試

MicroPython 文件參考 :

http://docs.micropython.org/en/latest/micropython-esp8266.pdf
http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket

所謂的 I2C 其實是 IIC (Inter-Integrated Circuit) 通訊協定的另一種寫法, I2C 被設計用來在系統內各晶片之間交換資料, 例如 CPU 從顯示晶片取得解析度等參數即透過 I2C 協定; 物聯網感測模組如陀螺儀, 加速度計, 顯示器, 或 RTC 等模組等也大都使用 I2C 介面通訊, I2C 協定較少用在設備間通訊.

I2C 是一個兩線的的串列通訊協定 (2-wire serial protocol), 只需要 SCL (Serial Clock Line) 與 SDA (Serial Data Line) 兩條線即可通訊, 不過與 RS-232 串列通訊不同之處是兩端不需要設定相同的傳輸速率, 因為 I2C 是透過 SCL 這條線來協調通訊速率的, 通常使用標準速率 100KHz 傳輸, 快速模式下可達 400KHz.

MicroPython on ESP8266 實作之 I2C 函式庫參見 :

class I2C – a two-wire serial protocol
# i2c-bus

下面用來控制 1602 液晶顯示器的 MicroPython 函式庫主要是參考下面這篇文章 :

# LCD 1602 - Library

其中 mcauser 先生的回覆內容有具體的做法指引 :

https://forum.micropython.org/viewtopic.php?t=2858#p16925

以下實驗我使用 WeMOS D1 Mini, 其 D1 腳 (GPIO 5) 即用作 I2C 的 SCL 線, 而 D2 腳 (GPIO 4) 即為 SDA 線, 接線如下 :


因我使用 I2C 模組控制 1602 液晶模組, 所以需要下面這兩個檔案 :

for I2C backpack:
https://github.com/dhylands/python_lcd/ ... i2c_lcd.py
https://github.com/dhylands/python_lcd/ ... cd_test.py

其實應該到這兩個檔案的上層 GitHub 去下載全部檔案比較完整 :

https://github.com/dhylands/python_lcd

點右邊的 "Clone or download" 鈕可下載專案的全部檔案 zip 檔 :


解壓縮後從 python_lcd-master\lcd 目錄下複製 lcd_api.py 與 esp8266_i2c_lcd.py 這兩個 1602 LCD 函式庫檔案到我們的測試目錄 D:\ESP8266\test 下, 準備上傳 ESP8266.


其中 lcd_api.py 是 MicroPython 與 1602 顯示晶片 HD44780 溝通的 API, 而 esp8266_i2c_lcd.py 則是在此 API 基礎上實作了 i2clcd 物件的函式庫以方便操控 1602, 其完整物件方法寫在 lcd_api.py 檔案中, 整理如下 :

 I2cLcd 物件的方法 說明
 move_to(x, y) 移動游標至座標 (x, y)
 putchar(str) 從游標目前位置開始顯示字串 str, 然後將游標移到字串結束之下一位置
 putchar(char) 從游標目前位置開始顯示字元 char, 然後將游標移到下一位置
 custom_char(location, charmap) 將字元圖樣 charmap 寫入 CGRAM 記憶體位址 location=0~7 
 clear() 清除 LCD 螢幕並將游標移至左上角座標 (0, 0)
 show_cursor() 顯示游標 (底線)
 hide_cursor() 隱藏游標
 blink_cursor_on() 開啟游標閃爍功能
 blink_cursor_off() 關閉游標閃爍功能
 display_on() 開啟顯示功能
 display_off() 關閉顯示功能 (即顯示白屏)
 backlight_on() 開啟背光
 backlight_off() 關閉背光

這裡面值得一提的是 custom_char() 方法, 此方法可讓我們儲存自定義字元於 CGRAM 中, 其位址為 0x0 至 0x7 共 8 個 byte 可用. 1602 液晶顯示器是以日立 HD44780 液晶顯示晶片為控制器, 裡面包含了 CGROM (儲存 ASCII 碼的 bitmap), DDRAM (80 Bytes 顯示記憶體, 儲存欲顯示的字元), 以及我們可以控制的 CGRAM (8 Bytes 擴展字元記憶體, 儲存自定義字元的 bitmap), 關於 1602 的內部記憶體參考 :

【51单片机】1602 CGRAM、CGROM及DDRAM的作用
新手必看1602字符液晶显示原理+实例详解
1602 字符液晶使用说明 (pdf)

總之, 透過上面這些方法就可以很方便地操控 1602 了.

欲操控 1602 首先必須從 machine 模組匯入 I2C 與 Pin 函數, 並從 sp8266_i2c_lcd 模組匯入 I2cLcd 函數 :

from machine import I2C, Pin
from esp8266_i2c_lcd import I2cLcd

這樣便能呼叫 I2C() 建構式指定 I2C 通訊所使用的 GPIO 腳與速率來建立 I2C 物件 :

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)  

此指令指定 GPIO 5 為 SCL 線, GPIO 4 為 SDA 線, 通訊速率為 400KHz 的 I2C 匯流排. 關於 I2C 物件參考 MicroPython 官方教學文件 :

http://docs.micropython.org/en/v1.8.7/esp8266/esp8266/quickref.html#i2c-bus
class I2C – a two-wire serial protocol (for WIPY)

接下來不需要利用 I2C 物件的讀寫指令存取 I2C 匯流排, 而是呼叫 esp8266_i2c_lcd.py 模組中的 I2cLcd() 建構式以建立 I2cLcd 物件, 這樣就能利用上述的 I2cLcd 物件方法來操控 1602 了, 不須直接處理 I2C Bus 上低階的 raw data, 這就是 API 包裝的好處 :

lcd=I2cLcd(i2c, 0x27, 2, 16) 

其 API 為 :

I2cLcd(i2c, i2c_addr, num_lines, num_columns)

此處四個參數 :

i2c :  I2C 物件
i2c addr : PCF8574 IO 擴展晶片位址 (預設是 0x27, 即使用 PCF8574T 晶片)
num lines : LCD 列數
num_columns : LCD 欄數

因我使用的是 1602, 所以列數為 2, 欄數為 16.  這樣便建立了一個位址為 0x27 的 I2C Slave 設備, 即 1602 I2C 模組, 注意, 若 1602 I2C 模組使用的是 PCF8574AT 晶片, YK6
則位址必須改為 0x3F.

這時若呼叫 I2C 的 scan() 方法掃描 I2C 匯流排上的設備將回傳 10 進位的設備位址串列, 因目前只有 0x27 位址, 10 進位是 39, 因此回傳 [39] :

>>> i2c.scan()
[39]

建立了一個 I2cLcd 物件後可以呼叫上表中的方法操控 1602 了.

測試 1 : 在 1602 上顯示 Hello World

#main.py
from machine import I2C, Pin
from esp8266_i2c_lcd import I2cLcd

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)    #指定 I2C 介面之 GPIO 與傳輸速率
lcd=I2cLcd(i2c, 0x27, 2, 16)                               #指定 I2C Slave 設備位址與顯示器之列數, 行數
lcd.putstr("Hello World!\nIt's working!")            #顯示字串
lcd.move_to(0,1)                                                  #移到游標至第二列第一行位置 (跳行)
lcd.putstr("It's working!")                                     #顯示字串


上面的程式中 lcd.putstr("Hello World!") 執行後, 游標會停在 "!" 後面, 須先用 move_to() 將游標移到座標 (0,1), 即第二列第一行, 再顯示第二列字串, 若沒有用 move_to(), 就會在目前游標位置 ("!" 後面) 繼續輸出, 結果如下 :


其實跳行動作不需要用到 move_to(0, 1), 直接使用跳脫字元 "\n" 也有跳行效果, 因此上述程式也可改為如下 :

#main.py
from machine import I2C, Pin
from esp8266_i2c_lcd import I2cLcd

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)    #指定 I2C 介面之 GPIO 與傳輸速率
lcd=I2cLcd(i2c, 0x27, 2, 16)                               #指定 I2C Slave 設備位址與顯示器之列數, 行數
lcd.putstr("Hello World!\nIt's working!")            #顯示字串 (使用 "\n" 跳行)

接下來我想連線 NTP 伺服器, 取得目前的網路時間顯示在液晶螢幕上, 這就必須匯入 MicroPython 內建的 ntptime 與 time 模組, 參考 :

# MicroPython on ESP8266 (七) : 時間日期測試


測試 2 : 從 NTP 伺服器取得 UTC 時間轉成台灣時間顯示於 1602 上

#main.py
import time, ntptime
from machine import I2C, Pin
from esp8266_i2c_lcd import I2cLcd

def fill_zero(n):
    if n<10: p="">        return '0' + str(n)
    else:
        return str(n)

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)
lcd=I2cLcd(i2c, 0x27, 2, 16)
ntptime.settime()     #從 NTP 伺服器取得 UTC 時間後設定本地時鐘

while True:
    utc_epoch=time.mktime(time.localtime())     #將本地時鐘轉成 UTC 之時戳
    Y,M,D,H,m,S,ms,W=time.localtime(utc_epoch + 28800)      #UTC 加 8 小時=台灣時間
    YMD='%s-%s-%s' % (str(Y),fill_zero(M),fill_zero(D))          #日期字串
    HmS='%s:%s:%s' % (fill_zero(H),fill_zero(m),fill_zero(S))   #時間字串
    lcd.move_to(0, 0)                                                                      #移動游標到第一列第一行
    lcd.putstr(YMD)                                                                        #顯示日期
    lcd.move_to(0, 1)                                                                      #移動游標到第二列第一行
    lcd.putstr(HmS)                                                                         #顯示時間
    time.sleep(1)                                                                              #暫停 1 秒

此程式一開始執行時便利用 ntptime.settime() 透過網路從 NTP 伺服器取得 GMT 時戳 (單位:毫秒) , 並用此時戳來設定 ESP8266 內部時鐘, 接著就跳進無限迴圈每秒自內建時鐘讀取此時戳, 加上 28800 毫秒即得台灣時間時戳 (因台灣為 GMT + 8 小時, 8*60*60=28800), 然後用自訂函數 fill_zero() 將日期時間格式標準化, 即個位數字前面補 0, 再分別將日期與時間顯示於 1602 的第一列與第二列 :



從影片中可知在 38:00 時出現跳秒現象 (一次跳兩秒), 直接從 38:00 跳躍到 38:02. 雖然每秒去讀取時戳, 但由於運算需要些微時間, 累積起來就會造成秒差, 觀察大約每 10 秒就會出現跳秒現象.

注意, 上面這個測試是假定 ESP8266 已可連線到無線基地台上網際網路, ESP8266 會記住最近一次的 SSID 與 PWD, 一開機即可自動連線, 所以上面程式可直接存成 main.py 上傳 ESP8266. 如果需要修改連線之基地台, 例如移動到另一個環境無法連線最近一次設定之基地台, 可將上面程式存成 myapp.py, 然後參考下列這篇裡面的 main.py, 拿掉最後兩列前面的 # :

# WeMOS D1 Mini 開發板測試

#main.py
import time
WAIT_FOR_CONNECT=8

def set_ap():
    html="""
    <!DOCTYPE html>
    <html>
      <head><title>AP Setup</title></head>
      <body>
        %s
      </body>
    </html>
    """
    form="""
        <form method=get action='/update_ap'>
          <table border="0">
            <tr>
              <td>SSID</td>
              <td><input name=ssid type=text></td>
            </tr>
            <tr>
              <td>PWD </td>
              <td><input name=pwd type=text></td>
            </tr>
            <tr>
              <td></td>
              <td align=right><input type=submit value=Connect></td>
            </tr>
          </table>
        </form>
    """
    import socket
    addr=socket.getaddrinfo('192.168.4.1', 80)[0][-1]
    s=socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(addr)
    s.listen(5)
    print('listening on', addr)
    while True:
        cs, addr=s.accept()
        print('client connected from', addr)
        data=cs.recv(1024)        
        request=str(data,'utf8')
        print(request, end='\n')
        if request.find('update_ap?') == 5:
            para=request[request.find('ssid='):request.find(' HTTP/')]
            ssid=para.split('&')[0].split('=')[1]
            pwd=para.split('&')[1].split('=')[1]
            sta.connect(ssid,pwd)
            while not sta.isconnected():
                pass
            print('Connected:IP=',sta.ifconfig()[0])
            cs.send(html % 'Connected:IP=' + sta.ifconfig()[0])
        else:
            cs.send(html % form)
        cs.close()
    s.close()

import network
sta=network.WLAN(network.STA_IF)
sta.active(True)
print('Connecting to AP ...')
time.sleep(WAIT_FOR_CONNECT)
if not sta.isconnected():
    set_ap()
else:
    print('Connected:IP=', sta.ifconfig()[0])
    #Application code is written here or import from a separate file
    import myapp  
    myapp.main()  

將此 main.py 與測試 2 的 myapp.py 上傳 ESP8266, 然後打開手機 WiFi 連線 ESP8266 內建 AP, 其 SSID 為 "MicroPython-" 開頭 (後面是 MAC 後 6 碼), 預設密碼 "micropythoN". 接著用手機瀏覽器 (建議用 Chrome) 連線網址 192.168.4.1 即可設定欲連線之無線基地台了.

D:\ESP8266\test>ampy --port COM8 put main.py
D:\ESP8266\test>ampy --port COM8 put myapp.py

REPL 輸出如下 :

PYB: soft reboot
#14 ets_task(40100164, 3, 3fff829c, 4)
WebREPL is not configured, run 'import webrepl_setup'
Connecting to AP ...
Connected:IP= 192.168.43.227
(2017, 8, 7, 1, 16, 32, 0, 219)

上面這個 8 元素的 tuple 是 ntptime.settime() 的傳回值, 分別表示 (年, 月, 日, 時, 分, 秒, 星期, 年日), 倒數第二個的星期值為 0~6, 其中 0 表示星期一, 6 表示星期天; 而年日表示這是一年中的第幾天, 參考 :


從上面這個因誤差造成跳秒問題延伸, 上述程式只在程式一開始與 NTP 伺服器對時 (更新內部時鐘), 然後就自己跑自己的, 這樣時間久了之後也會因 ESP8266 震盪晶體準確度問題而與 NTP 時鐘產生誤差, 因此比較好的做法是固定周期 (例如每小時) 再與 NTP 伺服器對時一次更新 ESP8266 內部時鐘, 如下列測試 3 :

測試 3 : 每小時與 NTP 伺服器對時一次顯示台灣時間顯示於 1602 上

import time,ntptime
from machine import I2C, Pin
from esp8266_i2c_lcd import I2cLcd

def fill_zero(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)
lcd=I2cLcd(i2c, 0x27, 2, 16)
ntptime.settime()   #與 NTP 伺服器初次對時
n=0    #設定重新對時計數器初始值

while True:  
    utc_epoch=time.mktime(time.localtime())
    Y,M,D,H,m,S,ms,W=time.localtime(utc_epoch + 28800)
    YMD='%s-%s-%s' % (str(Y),fill_zero(M),fill_zero(D))
    HmS='%s:%s:%s' % (fill_zero(H),fill_zero(m),fill_zero(S))
    lcd.move_to(0, 0)
    lcd.putstr(YMD)
    lcd.move_to(0, 1)
    lcd.putstr(HmS)
    time.sleep(1)
    n=n+1                           #計數器增量
    if n >= 3600:           #超過 1 小時 (3600 秒)
        ntptime.settime()      #重新對時 1 次
        n=0                           #計數器歸零重新計數

此程式添加了一個重新對時計數器 n 來記錄離上一次對時已經過了幾秒, 每次迴圈結束前會增量此計數器, 並檢查是否超過 3600, 是的話表示離上次與 NTP 對時已 1 小時 (因每個迴圈 1 秒, 3600 秒即 1 小時), 再次呼叫 ntptime.settime() 對時一次以更新 ESP8266 內部時鐘. 

下面測試 4 我想加上 DHT11 溫溼度模組與光敏電阻來顯示溫溼度與亮度, 參考之前這篇的測試 5 (但現在還沒有要紀錄到 ThingSpeak 網站, 只是純粹顯示在 1602 上面而已) : 


此外我想加入星期訊息, 從上面 REPL 輸出可知 ntptime.settime() 傳回的 tuple 倒數第二個為星期訊息, 0~6 分別代表星期一到星期六. 但液晶螢幕上的顯示區域有限, 所以我打算只顯示星期的英文前兩個字母當簡碼, 因為一個不足以辨認, 例如 Tuesday 與 Thursday, Saturday 與 Sunday 如果只用 T 與 S 表示就分不清楚.    

 傳回值 0 1 2 3 4 5 6
星期 MondayTuesday  Wednesday  Thursday Friday Saturday Sunday 
簡碼 MoTu We  Th FrSa Su 

我將 1602 的顯示區域整體規劃如下 :


第一列先顯示日期, 緊接著是兩個字元的星期代碼, 然後空一格顯示亮度比率 (100% 為最亮). 第二列則是時間訊息後空一個顯示攝氏溫度, 再空一格顯示濕度. 

測試 4 : 顯示日期時間, 溫溼度, 亮度於 1602 液晶顯示器  

import time, ntptime, dht
from machine import I2C, Pin, ADC
from esp8266_i2c_lcd import I2cLcd

def fill_zero(n):            #個位數前面補 0
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def fill_blank(n):           #個位數前面補空格
    if n<10:
        return ' ' + str(n)
    else:
        return str(n)

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)
lcd=I2cLcd(i2c, 0x27, 2, 16)
ntptime.settime()
n=0

DHTPIN=Pin(16, Pin.IN)
d=dht.DHT11(DHTPIN)
adc=ADC(0) 
week={0:'Mo',1:'Tu',2:'We',3:'Th',4:'Fr',5:'Sa',6:'Su'}    

while True: 
    d.measure()                 
    t=d.temperature()                     #溫度
    h=d.humidity()                         #濕度
    a=adc.read()                             #讀取光敏電阻 ADC 值 (0~1024) 
    a=int(0.3*a + 0.7*adc.read())  #積分濾波
    a=round(a*100/1024)              #轉成百分數

    utc_epoch=time.mktime(time.localtime())
    Y,M,D,H,m,S,W,dy=time.localtime(utc_epoch + 28800)
    YMDW='%s-%s-%s%s' % (str(Y),fill_zero(M),fill_zero(D),week[W])
    HmS='%s:%s:%s' % (fill_zero(H),fill_zero(m),fill_zero(S))
    A='%s%%' % (fill_blank(a))    #轉成亮度字串
    T='%sC' % (fill_blank(t))         #轉成溫度字串
    H='%sH' % (fill_blank(h))        #轉成濕度字串
    lcd.move_to(0, 0)
    lcd.putstr(YMDW)          #顯示日期與星期
    lcd.move_to(13, 0)
    lcd.putstr(A)                    #顯示亮度 (%)
    lcd.move_to(0, 1)
    lcd.putstr(HmS)               #顯示時間
    lcd.move_to(9, 1)
    lcd.putstr(T)                     #顯示攝氏溫度
    lcd.move_to(13, 1)
    lcd.putstr(H)                     #顯示濕度 
    time.sleep(1)
    n=n+1   
    if n >= 3600:
        ntptime.settime()
        n=0

此程式新增了 fill_blank() 函數來處理亮度, 溫度, 濕度是個位數時的對其問題, 這時會在前面補空格以免重覆出現單位字元例如 6%% , 2CC, 或 8HH 等奇怪輸出. 而星期簡碼與傳回值的對應則使用字典來儲存並與 YMD 結合為 YMDW.


接下來要測試 1602 HD44780 內 CGRAM 的自定義記憶體的用法, 希望能將上圖中的攝氏溫度單位由 C 改為右上角一個小圈圈的度數符號, ASCII 中沒有此符號, 必須利用 CGRAM 的 custom_char() 功能自製字元的 Bitmap.

自訂字元用法參考上面 Github 下載檔案 i2c_lcd_test.py 裡面的範例, 是透過呼叫 I2cLcd 物件的 custom_char() 方法將 8 個 bytes 的二進位陣列寫入 0~7 的 CGRAM 位址中, 然後用 Python 內建函數 chr() 傳入 0~7 位址可提取此自訂字元.

https://github.com/dhylands/python_lcd

1602 LCD 的每一個字元解析度是 5*8=40 px (不過最後一列似乎沒有使用), 我製作了兩種攝氏度數符號的 bitmap 如下, 一個靠左, 一個靠右 :


其儲存指令分別為 :

lcd.custom_char(0, bytearray([0x07,0x05,0x07,0x00,0x00,0x00,0x00,0x00]))    #靠右
lcd.custom_char(0, bytearray([0x1C,0x14,0x1C,0x00,0x00,0x00,0x00,0x00]))   #靠左

在下面測試 5 程式中, 我在 CGRAM 位址 0 加入了此攝氏度數符號字元, 修改了變數 T 的輸出格式用此自訂字元替換測試 4 中的 'C' 字元 :

測試 5 : 顯示日期時間, 溫溼度, 亮度於 1602 液晶顯示器 (使用自定義字元)

#myapp.py
import time, ntptime, dht
from machine import I2C, Pin, ADC
from esp8266_i2c_lcd import I2cLcd

def fill_zero(n):            #個位數前面補 0
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def fill_blank(n):           #個位數前面補空格
    if n<10:
        return ' ' + str(n)
    else:
        return str(n)

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)
lcd=I2cLcd(i2c, 0x27, 2, 16)
lcd.custom_char(0, bytearray([0x1C,0x14,0x1C,0x00,0x00,0x00,0x00,0x00]))  

DHTPIN=Pin(16, Pin.IN)
LEDPIN=Pin(14, Pin.OUT)
d=dht.DHT11(DHTPIN)
adc=ADC(0)
week={0:'Mo',1:'Tu',2:'We',3:'Th',4:'Fr',5:'Sa',6:'Su'}

try:  
    ntptime.settime()
except:
    pass
n=0
while True:
    d.measure()              
    t=d.temperature()
    h=d.humidity()
    a=adc.read()
    a=int(0.3*a + 0.7*adc.read())
    a=round(a*100/1024)

    utc_epoch=time.mktime(time.localtime())
    Y,M,D,H,m,S,W,DY=time.localtime(utc_epoch + 28800)
    YMDW='%s-%s-%s%s' % (str(Y),fill_zero(M),fill_zero(D),week[W])
    HmS='%s:%s:%s' % (fill_zero(H),fill_zero(m),fill_zero(S))
    A='%s%%' % (fill_blank(a))
    T='%s%s' % (fill_blank(t),chr(0))
    H='%sH' % (fill_blank(h))
    lcd.move_to(0, 0)
    lcd.putstr(YMDW)
    lcd.move_to(13, 0)
    lcd.putstr(A)  
    lcd.move_to(0, 1)
    lcd.putstr(HmS)
    lcd.move_to(9, 1)
    lcd.putstr(T)
    lcd.move_to(13, 1)
    lcd.putstr(H)
    time.sleep(1)
    n=n+1
    if n >= 3600:
        try:
            ntptime.settime()
        except:
            pass
        n=0



測試結果顯示, 似乎靠左跟度數近一點較好. 另外, 上面的程式碼中的 ntptime.settime() 改用 try except 包起來, 因為若網路中斷時呼叫 settime() 會出現例外 (OSError: -2) 而使程式當掉停止執行, 必須做例外處理, 當網路無法存取時就跳過 :

PYB: sof#9 ets_task(40100164, 3, 3fff829c, 4)
WebREPL is not configured, run 'import webrepl_setup'
Connecting to AP ...
Connected:IP= 192.168.43.227
Traceback (most recent call last):
  File "main.py", line 86, in <module>
  File "myapp.py", line 13, in <module>
  File "ntptime.py", line 30, in settime
  File "ntptime.py", line 18, in time
OSError: -2

觀察發現程式碼多了後, 跳秒情形似乎更明顯了, 大概每 4~6 秒就會跳一格. 其實秒數並不是很重要, 既然很難避免跳秒, 不如乾脆去除秒數, 只要顯示到分就好, 這樣就可以把時分與年月日這兩個時間相關訊息一起放在第一列剛好塞滿, 亮度就移到第二列顯示. 少了秒數後第二列就有點空, 所以我把星期簡碼由兩個字元改為三個字元, 星期字典也修改為 :

week={0:'Mon',1:'Tue',2:'Wed',3:'Thu',4:'Fri',5:'Sat',6:'Sun'}

 傳回值 0 1 2 3 4 5 6
星期 MondayTuesday  Wednesday  Thursday Friday Saturday Sunday 
簡碼  MonTue Wed  Thu FriSat Sun 

LCD 顯示區域重新規劃如下 :


我將測試 5 程式修改如下, 修改部分以藍色標示 :

測試 6 : 顯示日期時間, 溫溼度, 亮度於 1602 液晶顯示器 (不顯示秒數)

#myapp.py
import time, ntptime, dht
from machine import I2C, Pin, ADC
from esp8266_i2c_lcd import I2cLcd

def fill_zero(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def fill_blank(n):        
    if n<10:
        return ' ' + str(n)
    else:
        return str(n)

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)
lcd=I2cLcd(i2c, 0x27, 2, 16)
lcd.custom_char(0, bytearray([0x1C,0x14,0x1C,0x00,0x00,0x00,0x00,0x00]))  

DHTPIN=Pin(16, Pin.IN)
LEDPIN=Pin(14, Pin.OUT)
d=dht.DHT11(DHTPIN)
adc=ADC(0)

week={0:'Mon',1:'Tue',2:'Wed',3:'Thu',4:'Fri',5:'Sat',6:'Sun'}

try:
    ntptime.settime()
except:
    pass
n=0
while True:
    d.measure()              
    t=d.temperature()
    h=d.humidity()
    a=adc.read()
    a=int(0.3*a + 0.7*adc.read())
    a=round(a*100/1024)

    utc_epoch=time.mktime(time.localtime())
    Y,M,D,H,m,S,W,DY=time.localtime(utc_epoch + 28800)
    YMD='%s-%s-%s' % (str(Y),fill_zero(M),fill_zero(D))  
    Hm='%s:%s' % (fill_zero(H),fill_zero(m))
    A='%s%%' % (fill_blank(a))
    T='%s%s' % (fill_blank(t),chr(0))
    H='%sH' % (fill_blank(h))
    lcd.move_to(0, 0)
    lcd.putstr(YMD)
    lcd.move_to(11, 0)  
    lcd.putstr(Hm)    
    lcd.move_to(0, 1)
    lcd.putstr(week[W])
    lcd.move_to(5, 1)    
    lcd.putstr(T)
    lcd.move_to(9, 1)
    lcd.putstr(H)
    lcd.move_to(13, 1)
    lcd.putstr(A)
    time.sleep(1)  
    n=n+1
    if n >= 3600:
        try:
            ntptime.settime()
        except:
            pass
        n=0


呵呵, 這個配置看起來比較好看, 溫溼度亮度等環境資訊都在第二列, 而且我們通常只要知道幾點幾分就夠了, 秒數並不是很重要.

接下來要測試背光函數 backlight_on() 與 backlight_off(). 開啟背光功能大約會增加 20mA 左右的耗電, 基於省電考量, 我希望只有在偵測到附近有人經過時才開啟背光, 否則就關閉以節省用電, 這就需要一個 PIR 紅外線感測器, 關於 PIR 用法參考 :

# Arduino 測試 : PIR 紅外線移動偵測 (一)
# Arduino 測試 : PIR 紅外線移動偵測 (二)
MicroPython on ESP8266 (八) : GPIO 測試
MicroPython on ESP8266 (九) : PIR 紅外線移動偵測

注意, PIR 感測器的 VCC 上面是標 +5V, 雖然接 D1 Mini 的 3.3V 接腳似乎也可以運作, 但是感覺較易受雜訊影響, 建議接在 D1 Mini 的 5V 接腳較穩.

我將上面測試 6 程式加上了 PIR 感測器程式碼如下, 修改與增加部分以藍色標示 :

測試 7 : 顯示日期時間, 溫溼度, 亮度於 1602 液晶顯示器 (利用 PIR 紅外線控制背光)

#myapp.py
import time, ntptime, dht
from machine import I2C, Pin, ADC
from esp8266_i2c_lcd import I2cLcd

def fill_zero(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def fill_blank(n):        
    if n<10:
        return ' ' + str(n)
    else:
        return str(n)

def int0(p):                         
    global backlight_state                    
    global triggerCount       
    global triggerLimit        
    backlight_state=1                          
    if triggerCount < triggerLimit:        
        triggerCount=triggerCount + 2   

PIRPIN=Pin(0, Pin.IN)
PIRPIN.irq(trigger=Pin.IRQ_RISING, handler=int0)
backlight_state=0
triggerCount=0       
triggerLimit=5  

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)
lcd=I2cLcd(i2c, 0x27, 2, 16)
lcd.custom_char(0, bytearray([0x1C,0x14,0x1C,0x00,0x00,0x00,0x00,0x00]))  

DHTPIN=Pin(16, Pin.IN)
LEDPIN=Pin(2, Pin.OUT)
d=dht.DHT11(DHTPIN)
adc=ADC(0)
week={0:'Mon',1:'Tue',2:'Wed',3:'Thu',4:'Fri',5:'Sat',6:'Sun'}

try:
    ntptime.settime()
except:
    pass
n=0
while True:
    d.measure()              
    t=d.temperature()
    h=d.humidity()
    a=adc.read()
    a=int(0.3*a + 0.7*adc.read())
    a=round(a*100/1024)

    utc_epoch=time.mktime(time.localtime())
    Y,M,D,H,m,S,W,DY=time.localtime(utc_epoch + 28800)
    YMD='%s-%s-%s' % (str(Y),fill_zero(M),fill_zero(D))
    Hm='%s:%s' % (fill_zero(H),fill_zero(m))
    A='%s%%' % (fill_blank(a))
    T='%s%s' % (fill_blank(t),chr(0))
    H='%sH' % (fill_blank(h))
    lcd.move_to(0, 0)
    lcd.putstr(YMD)
    lcd.move_to(11, 0)
    lcd.putstr(Hm)
    lcd.move_to(0, 1)
    lcd.putstr(week[W])
    lcd.move_to(5, 1)
    lcd.putstr(T)
    lcd.move_to(9, 1)
    lcd.putstr(H)
    lcd.move_to(13, 1)
    lcd.putstr(A)

    if backlight_state==1:
        lcd.backlight_on()
        LEDPIN.value(0)
        triggerCount=triggerCount - 1
        if triggerCount <= 0:                       
            backlight_state=0
    else:
        lcd.backlight_off() 
        LEDPIN.value(1)
    print("IRQ Trigger Count:",triggerCount,'Backlight State:',backlight_state)

    time.sleep(1)
    n=n+1
    if n >= 3600:
        try:
            ntptime.settime()
        except:
            pass
        n=0


此程式使用 GPIO 0 (D1 Mini 的 D4 腳) 來接 PIR 感測器的輸出, 當 PIR 偵測到人體紅外線時會輸出一個 HIGH 位準, 持續約 0.5 秒左右, 可透過 PIR 背板上的一顆可變電阻調整 HIGH 持續時間, 但最長持續時間也是很有限 (約 3~5 秒), 使得 LCD 上的資訊還沒看仔細, 背光就暗掉了.

為了在持續移動觸發 (閉鎖時間 2.5 秒) 下讓 LCD 背光維持在亮燈狀態, 我參考之前在 PIR 感應照明控制測試中的作法, 利用 PIR 輸出的上升緣 (LOW -> HIGH) 觸發外部硬體中斷 (IRQ) 控制三個全域變數 backlight_state, triggerCount, 以及 triggerLimit 之值, 以便在連續移動觸發時能使背光持續點亮.

不過此處為了不影響每個迴圈大致維持每秒跑一圈, 我沒有使用 time.sleep() 來延長背光持續點亮時間, 而是每次 PIR 觸發時讓 triggerCount 增量 2 而非 1, 這樣雖然每次迴圈會讓 triggerCount 減量 1, 但因增量速度是減量速度的兩倍以上, 所以在持續觸發情況下 triggerCount 不會很快歸零導致背光忽明忽亮, 這樣就能維持背光在點亮狀態. 而 triggerLimit 的作用則是讓持續觸發有個天花板擋著, 讓加倍增量的 triggerCount 能夠收斂, 如同核反應堆中阻止中子數增生的碳棒一樣.

參考 :

Python based library for talking to character based LCDs.
Arduino 使用 1602 IIC(I2C) LCD 點陣液晶模組
I2C Bus 簡介
# I2C Bus 與 SMBUS 有甚麼不同
MicroPython Library for I2C 2x16 LCD Screens (for WiPy/LoPy/SiPy only)
Bucknalla/micropython-i2c-lcd (for WiPy/LoPy/SiPy only)
# https://github.com/Bucknalla 
dhylands/python_lcd

2017-08-10 補充 :

由於本實驗使用 WeMOS D1 mini, 其 A0 (ADC) 接腳接受的電壓是 0~3.3V (與 ESP-12 模組的 0~1V 不同), 對應到 AD 轉換後的 0~1023, 所以此處的亮度偵測使用一個 CdS 光敏電阻與 10K 歐姆電阻分壓, A0 (ADC) 直接讀取 10K 歐姆電阻的壓降後取百分數, 其值 (0~100) 即與光度成正比了, 參考下面這篇的補充說明 :

MicroPython on ESP8266 (十五) : 光敏電阻與 ADC 測試

4 則留言 :

朱小農 提到...

你好,
拜讀你的文章後,覺得你非常厲害
能否請教你一些問題嗎?
材料:
WeMos D1 R2 V2.1 最新版 WiFi 開發板 ESP8266
DHT-11 溫濕度感測器 溫度/濕度二合一
1602 LCD 顯示器 已焊2004轉接板 IIC/I2C
軟體:Arduino IDE(1.83)

問題:
1.我用了WeMos板後,LCD都無法點亮,但WEMOS可以驅動DHT(用序列監控視窗可看到),1602在UNO板也是正常
2.如何將DHT11的溫濕度上傳到電腦或雲端,以便監控資料

抱歉打擾您,我的問題想了很久,如您方便請給我一些範例檔
感激不盡~

Best regards,

Calvin (calvin.chu0902@msa.hinet.net)

Tony Huang 提到...

您好, 您的 1602 是 I2C 還是 GPIO 介面? 若是 I2C 請參考上面的接法, 並用十字起子調一下背光; 若直接用 GPIO 則需要一個可變電阻來調整背光, 參考 :

http://yhhuang1966.blogspot.tw/2015/03/arduino_25.html

將 DHT11 上傳 THINGSPEAK 方法參考 :

http://yhhuang1966.blogspot.tw/2017/06/micropython-on-esp8266-dht11.html

朱小農 提到...

是I2C
我用ESP8266I2C的範本(seraildisplay),接腳D3(SDA),D4(SCL)
終於出現字,努力調整背光到最大,字非常淡才發現
轉回到UNO使用,字卻很清晰
不管如何,至少靠您提醒,終於可以點亮1602
但是LCD功能還大不如UNO好用
是我功力太淺

Tony Huang 提到...

朱兄, 另外還要注意 1602 是 3.3v 版還是 5v 版喔! 若拿 5v 版去給 3.3v 用, 會因為電流太小, 即使背光電阻調到底也只能看到淡淡的字, 這可能是您遇到的情況. 3.3v 版的 1602 在背板有多一顆 ic.