2017年8月17日 星期四

組裝可調式直流電源供應器 (二)

上周六收到露天賣家寄來的變壓器, 因為要回鄉下所以就擱著. 周一晚上取出測量電壓, 功能均正常, 其二次端有七條線, 共三組輸出 : 白色是 4V 輸出, 綠色是 28.5V 輸出, 而黃色有兩條對黑線都是 11.4 輸出 :

白色 4V

綠色 28.5V

黃色 11.4V

週二下班去禾樺買直流輸出用的博士端子, 1 公尺長電源線, 黑色塑膠外殼, 以及 7P 公電源連接線花了 152 元, 加上小北買的電源插頭含線 79 元, 組這個電源供應器已經花了 200+80+152+79=511 元.

週二晚上吃過飯就進行組裝, 同時在輸出端加焊 LED 電壓計, 測試正常可調 0~30V 輸出, 但因電壓計最低工作電壓 4.5V, 所以低於此電壓就無法顯示 :


接下來就是要在外殼上鑽洞把電路板與變壓器鎖在機箱內, 先用油性筆訂出鑽孔位置, 等本周回鄉下再用鑽孔機施工.

2017年8月16日 星期三

Goodfello, Bengio, Courville 的深度學習電子書

今天在 GitHub 上找到 Goodfello, Bengio, 與 Courville 合寫的深度學習免費電子書 "Deep Learning", 此書屬於理論書籍, 從線性代數, 機率統計與資訊理論帶入機器學習與深度學習等 AI 主題的理論介紹, PDF 檔可在 GitHub 下載 :

https://github.com/HFTrader/DeepLearningBook

此書作者 Goodfello 目前任職 Google, 是 Google Brain 成員之一, 而 Bengio 與 Courville 則是加拿大蒙特婁大學教授, 與此書相關之習題, 教學投影片可在下列網站下載 :

www.deeplearningbook.org

參考 :

‘Deep Learning’ by Goodfellow, Bengio, and Courville


2017年8月13日 星期日

2017 年第 32 周記事

週六晚上我在改程式時, 姊姊走過來說, 她發現這個禮拜是開學前能回鄉下的最後一周, 也是, 下周六要去海港聚餐慶祝姊姊考上大學與父親節, 接下來兩周要去動漫展擺攤, 然後就要準備開學了. 以後就只有一個月回來一次, 想到離巢期開始襲來, 心裡有點恐慌. 好快啊! 18 年就在不經意中溜走了, 想想初為人父, 半夜起來兩三次餵奶似乎還是昨日之事, 一回神姐姐已經要上大學了. 這些日子到底是怎麼溜走的呢?

早上經過市圖進去撈到五本書 :

  1. Arduino穿戴式裝置專案製作
  2. 8051語音互動專題製作與應用
  3. 電路板設計快速上手 :從EAGLETM開始學設計原理到電路板實作
  4. C基礎講座
  5. C語言程式設計入門與實作 
  6. C語言程式設計與應用
三本 C 語言的書是要教二哥 C 語言的陣列, 函數, 指標這三個最重要單元用的.

今日中午正昏昏欲睡進入夢鄉未久就被外面車子聲吵醒, 原來是阿泉伯的兒子帶他朋友來幫我家芒果接枝, 其實我覺得我家的海頓就很好吃了, 不管是愛文, 玉文等比不上. 下午帶了水果跟姊姊去文昌宮還願, 雖然凡事得靠自己努力, 但機運卻是努力不來的.

2017年8月12日 星期六

MicroPython on ESP8266 (十八) : SSD1306 液晶顯示器測試

做完 MicroPython 的 1602 LCD 顯示器實驗後, 我又在零件箱裡找到之前買的 NOKIA 5110 LCD 顯示器與 0.91 吋 128*32 解析度的 SSD1306 OLED 顯示器, 原本是想測試 NOKIA 5110, 但是一直不得法, 所以就把目標瞄向 SSD1306 顯示器了.

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

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 on ESP8266 (十七) : 液晶顯示器 1602A 測試

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

SSD1306 是中國顯示晶片設計商晶門 (Solomon System, 港交所 2878) 為小型 OLED/PLED 共陰極顯示器所設計的單晶片 CMOS 128*64 點矩陣式驅動 IC, 具有 256 階對比控制, 128*64 bits SRAM 顯示記憶體以及內建震盪器, 大幅簡化了外部電路與降低了功率消耗, 最大電流僅 15mA, 參考 :

http://www.solomon-systech.com/zh/product/display-ic/oled-driver-controller/ssd1306/

其規格書如下 :

https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf

我手上這塊模組的面板非常小巧, 只有 0.91 吋, 解析度 128*32, 為 I2C 介面只有 VCC (3.3V), GND, 以及 SCL 與 SDA 四支腳, 方便插在麵包板上做實驗. 硬體接線非常簡單. 當時在露天買花了 90 元, 但現在似乎漲到 100 元 :

向 tsai_pl 與 bluetaipei 購買零組件兩批
0.91吋 OLED 128x32 Arduino SSD1306 3.3V $100

在 AliExpree 這塊模組最低賣 US$2.68  (一件免運費), 折合台幣 80 元 :

# Free Shipping 0.91 Inch 128x32 IIC I2C White OLED LCD Display DIY Oled Module SSD1306 Driver IC DC 3.3V 5V For Arduino PIC US$2.68

淘寶網賣人民幣 8.8 元, 折合台幣 41 元, 但運費選擇淘寶集運每公斤 ¥14 人民幣起跳, 合計 ¥22.8 元合台幣 108 元, 要買兩件以上才會跟 AliExpress 或露天打平.

0.91寸OLED液晶模塊 IIC接口 128*32點陣 SSD1306驅動  ¥ 8.80

買 5 個是 ¥44 元, 大陸國內段運費 ¥10 合計 ¥54 元, 折合台幣約 246 元, 台灣集運運費不計的話平均每個 49 元. 若選擇淘寶 4PX 集運 1KG 內 ¥14 元, 合計是 ¥54+¥14=¥68 元, 合台幣 324 元, 平均每個 65 元, 也比露天便宜. 如果買 10 個含運費是 ¥88+10+14=¥112, 合台幣 533 元, 平均每個 53 元, 即買越多就能把運費攤平了.

另外還有使用 128*64 解析度面板 (與 SSD1306 控制能力一致) 的模組, 大小跟 NOKIA 5110 LCD 模組相同 :

OLED液晶模組 0.96吋 黃藍雙色 12864點陣 SSD1306驅動 3.3V-5V 4線IIC介面 $200

SSD1306 的 MicroPython 驅動程式可從下面兩個 GitHub 下載, 此驅動模組同時支援 I2C 與 SPI 介面, 只要傳入 I2C 或 SPI 物件即可驅動顯示器 :

micropython/drivers/display/ssd1306.py  (MicroPython 官網)

此驅動模組提供下列 SSD1306_I2C 物件函數以方便操控面板的顯示. API 整理如下 :

 SSD1306_I2C 物件的方法 說明
 fill(col) 將顯示記憶體全部畫素填入 col=1 (亮) 或 0 (暗)
 pixel(x, y, col) 在顯示記憶體指定畫素位置 (x, y) 填入 col=1 (亮) 或 0 (暗)
 text(string, x, y, col=1) 在顯示記憶體指定畫素位置 (x, y) 起填入預設 col=1 之字串 string
 show() 將顯示記憶體內容輸出於面板顯示內容
 scroll(dx, dy) 將顯示記憶體畫素內容向上下 (dy) 或向左右 (dx) 捲動

注意, 顯示面板座標 (x,y) 的原點位置 (0, 0) 是在 0.91 吋模組 SDA 腳的那一側. 將驅動模組 ssd1306.py 上傳到 ESP8266 後, 即可參考 Adafruit 網站的用法說明進行測試 :

How to use a SSD1306 OLED display with MicroPython boards

不過這篇文章使用的是 Adafruit 的 Circuit Python 控制板搭配其 FeatherWing OLED SSD1306 模組, 面板一樣是 128x32 OLED, 但模組價格卻非常貴, 要價 US$14.95, 合新台幣約 449 元, 是我買的模組的 4 倍多, 只是搭配 Adafruit 的 ESP8266 Feather 系列開發板較方便而已, 可以上下疊起來 :

FeatherWing OLED - 128x32 OLED Add-on For All Feather Boards US$14.95

Adafruit CircuitPython+FeatherWing OLED 所使用的 SSD1306 驅動模組事實上是從上面 MicroPython 官網版本修改而來, 僅加入適應 CircuitPython 部分 :

https://github.com/adafruit/Adafruit_CircuitPython_SSD1306

在下面的測試中我仍然使用 WeMOS D1 Mini 微控器, 當然 ESP-01 模組, ESP-12 模組或 NodeMCU 開發板也是可以的. SSD1360 模組與 D1 Mini 的硬體接線很簡單, 就是 SCL 接 D1 (GPIO 5), SDA 接 D2 (GPIO 4), 如下圖所示 :


軟體部分, ESP8266 模組需燒錄 MicroPython v1.8.5 版以上韌體, 否則無法執行 ssd1306.py 模組. 首先須匯入已上傳的 ssd1306.py 模組, 另外還須從 machine 模組匯入 Pin 與 I2C :

import ssd1306
from machine import Pin, I2C
i2c=I2C(scl=Pin(5), sda=Pin(4))                   #指定 GPIO 腳建立 I2C 物件
oled=ssd1306.SSD1306_I2C(128, 32, i2c)   #指定解析度建立 SSD1306 物件

此處 SSD1306_I2C() 建構函數的傳入參數為 (width, height, i2c), 分別是 OLED 面板的寬度 128, 高度 32, 以及 I2C 物件, 傳回值為一個 SSD1306_I2C 物件, 這樣便可以呼叫 SSD1306_I2C 物件的方法如 fill(), pixel() 或 text() 等來顯示資訊.

例如 fill(1) 是將顯示記憶體的每一個像素都點亮, 用 show() 輸出後就會使整個螢幕全亮; 而 fill(0) 則是全暗, 相當於 1602 LCD 裡的 clear() 清除螢幕的作用.

oled.fill(1)      #將顯示記憶體每一個畫素填滿 1 (點亮)
oled.show()    #將顯示記憶體內容輸出到面板 (全亮)
oled.fill(0)      #將顯示記憶體每一個畫素填滿 0 (熄滅)
oled.show()    #將顯示記憶體內容輸出到面板 (全暗)


事實上 fill(1) 與 fill(0) 與下列用 pixel() 填像素作用相同 :

    for x in range(128):
        for y in range(32):
            oled.pixel(x, y, 1)

    for x in range(128):
        for y in range(32):
            oled.pixel(x, y, 0)

注意, 不管是 fill(), pixel() 或 text(), 都只是在 SSD1306 內的顯示記憶體上操作而已, 並不是實際輸出到顯示面板上, 執行後面板上不會顯示任何訊息, 必須呼叫 show() 函數將顯示記憶體之內容輸出才會顯示.

測試 1 : 每隔 2 秒點亮與熄滅整個 OLED 面板

#main.py
import ssd1306
from machine import Pin, I2C
import time

i2c=I2C(scl=Pin(5), sda=Pin(4))                
oled=ssd1306.SSD1306_I2C(128, 32, i2c)

while True:
    oled.fill(1)        
    oled.show()  
    time.sleep(2)
    oled.fill(0)      
    oled.show()
    time.sleep(2)  


接下來要測試 pixel(x, y, col) 方法, 此方法是在指定的畫素位置 (x, y) 輸出一個亮點 (col=1) 或暗點 (col=0). 下列測試 2 是要在面板的四周畫出一個長方形框  :

測試 2 : 在 OLED 面板的四周畫出一個長方形框 

#main.py
import ssd1306
from machine import Pin, I2C

i2c=I2C(scl=Pin(5), sda=Pin(4))                
oled=ssd1306.SSD1306_I2C(128, 32, i2c)

for i in range(128):
        oled.pixel(i, 0, 1)             #填上邊框
        oled.pixel(i, 31, 1)           #填下邊框
        if i < 32:
            oled.pixel(0, i, 1)          #填左邊框
            oled.pixel(127, i, 1)      #填右邊框
oled.show()

此程式為了節省時間只使用一個迴圈填畫素, 由於 X 軸座標 0~127, Y 軸座標 0~31, range() 函數會傳回串列 [0,1,....,127], 以 X 軸座標當迴圈運行 128 次, 同時填上邊框與下邊框, 在 i 於 0~31 期間, 也同時填 Y 軸的左右邊框. 結果如下 :


下面測試 3 要測試 text() 方法, 即在 OLED 面板上顯示文字. 此驅動程式的 text() 以 8*8=64 bits 描繪一個 ASCII 碼, 不過實際描繪寬度為 6 bits, 亦即左右各保留 1 px 的間距以免前後字元連在一起. 例如下面是 '0' 的 Bitmap :


因此一列可顯示 128/8=16 個字元, 一行可顯示 32/8=4 列字元, 整個面板可顯示 16*4=64 個字元. 在顯示多列文字時, 下一列 y 座標應往下移 8 才不會黏在一起, 如下面測試 3 所示 :

測試 3 : 在 OLED 面板上顯示文字 

 #main.py
import ssd1306
from machine import Pin, I2C

i2c=I2C(scl=Pin(5), sda=Pin(4))                
oled=ssd1306.SSD1306_I2C(128, 32, i2c)

str='012345678901234567890'

for i in range(5):
   oled.text(str, 0, i*8)
oled.show()


此程式中 str 含有 20 個字元, 卻只顯示 16 個, 可見多出來的字元事實上被驅動模組丟棄了, 並沒有存入顯示記憶體中.

接下來參考 MicroPython 1602 測試的範例 4, 在 OLED 面板上顯示溫溼度與亮度等氣候資訊, 硬體接線參考 :

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

跟 1602 LCD 顯示器一樣, SSD1306 OLED 也是一列只能顯示 16 個字元, 但可以顯示 4 列, 所以我把顯示格式做了些調整, 第一列顯示日期與星期, 第二列顯示時分秒, 第三列顯示環境資訊, 規劃如下 :


不過 SSD1306_I2C 物件的 text() 方法只要指定第一個字元要顯示的開始座標位置即可, 其他字元會自動往後一一顯示, 超過 16 個的字元會被丟棄.

測試 4 : 在 OLED 面板上顯示溫溼度亮度資訊

#main.py
import time, ntptime, dht
from machine import I2C, Pin, ADC
import ssd1306

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))                
oled=ssd1306.SSD1306_I2C(128, 32, i2c)

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))
    WD=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))

    line1='%s %s' % (YMD, WD)
    line2=HmS
    line3='%s %s %s' % (T, H, A)
    oled.fill(0)                    #清除螢幕以免舊資訊殘留疊加
    #oled.show()                 #此指令會造成頁面跳動
    oled.text(line1, 0, 0)     #顯示日期星期
    oled.text(line2, 0, 8)     #顯示時間
    oled.text(line3, 0, 16)   #顯示溫溼度亮度
    oled.show()

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


因為要避免舊資訊殘留疊加, 所以在更新顯示資料前先做清除螢幕動作, 但是這麼一來卻造成換頁時螢幕會跳動一下. 我嘗試用 pixel() 方法只清除要顯示的前三列, 即將上述程式碼中的 oled.fill(0) 改成下列 :

    for x in range(128):
        for y in range(24):
            oled.pixel(x, y, 0)

但這並無法消除換頁跳動現象, 還是跟 fill(0) 一樣.  畢竟 SSD1306 與 1602 不同, 1602 的 putstr() 方法是以字元為單位, 更新時會清除舊的 bitmap 以新的字元之 bitmap 取代, 所以 1602 不需要清除畫面, 也就不會有換頁跳動現象.

2017-08-12 補充 :

晚上回到鄉下後測試發現, 只要單用 oled.fill(0) 即可清除舊畫面, 亦即只要將顯示記憶體全部 reset 就達到清除畫面目的了, 不需要呼叫 oled.show() 去刷新頁面, 就是後面這指令造成換頁跳動現象的.

在上面測試 4 裡我用 'C' 代表攝氏度數的單位, 能不能像 1602 測試中那用 custom_char() 方法自訂右上角一個小圈圈的度數符號呢? 很可惜地, 在 ssd1306.py 模組中並未提供類似的函數, 因此必須自己做, 攝氏度數小圓圈的 bitmap 如下 :

依據上面對於顯示區域的規劃, 此符號之座標位置為 (2,2), 即原本顯示 'C' 之處. 描繪此符號需使用 pixel() 方法將指定之畫素點亮, 上面這個度數小圓圈的亮點畫素座標可用下列元組串列表示, 只要將這 8 個點設為 1 即可 :

bitlist=[(2,0),(3,0),(1,1),(4,1),(1,2),(4,2),(2,3),(3,3)]

這些亮點的實際位置可用字元座標計算而得, 因為每個字元是 8*8 bitmap 所描繪, 因此畫素實際座標就是字元座標乘以 8 再加上字元內亮點座標即得. 例如第一個亮點之字元內座標為 (2,0)經此換算為實際座標 (18, 16), 其中 18=2*8+2, 16=2*8+0.

我模仿 1602 驅動模組的 custom_char() 函數寫了一個 SSD1306 版的函數如下 :

def custom_char(oled, x, y, bitlist, col=1):
   for i in bitlist:
       oled.pixel(x*8+i[0], y*8+i[1], col)

呼叫此函數後自訂字元的 bitmap 亮點就被寫入指定位置的顯示記憶體了. 完整程式如下, 新增的程式碼以藍色標示 :

測試 5 : 在 OLED 面板上顯示溫溼度亮度資訊 (使用自訂之攝氏度數符號)

#main.py
import time, ntptime, dht
from machine import I2C, Pin, ADC
import ssd1306

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 custom_char(oled, x, y, bitlist, col=1):
   for i in bitlist:  
       oled.pixel(x*8+i[0], y*8+i[1], col)  

i2c=I2C(scl=Pin(5), sda=Pin(4))              
oled=ssd1306.SSD1306_I2C(128, 32, i2c)

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))
    WD=week[W]
    HmS='%s:%s:%s' % (fill_zero(H),fill_zero(m),fill_zero(S))
    A='%s%%' % (fill_blank(a))
    T='%s' % (fill_blank(t))
    H='%sH' % (fill_blank(h))

    line1='%s %s' % (YMD, WD)
    line2=HmS
    line3='%s %s %s' % (T, H, A)
    oled.fill(0)  
    oled.text(line1, 0, 0)  
    oled.text(line2, 0, 8)  
    oled.text(line3, 0, 16)
    bitlist=[(2,0),(3,0),(1,1),(4,1),(1,2),(4,2),(2,3),(3,3)]  
    custom_char(oled, 2, 2, bitlist)  
    oled.show()

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



其實我們可以在每一個字元 8*8=64 bits 畫板上製作任何 bitmap 呼叫 text() 方法顯示於 16*4=64 個字元位置上, 在下列測試 6 中我製作了 0%, 20%, 40%, 60%, 80%, 100% 電池電量顯示符號 (包括垂直與水平方向) :

測試 6 : 在 OLED 面板上顯示 WiFi 連線狀態

#main.py
import time
from machine import I2C, Pin
import ssd1306

def custom_char(oled, x, y, bitlist, col=1):
   for i in bitlist:  
       oled.pixel(x*8+i[0], y*8+i[1], col)  

i2c=I2C(scl=Pin(5), sda=Pin(4))              
oled=ssd1306.SSD1306_I2C(128, 32, i2c)

bv0p=[(3,0),(4,0),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),
      (1,2),(6,2),(1,3),(6,3),(1,4),(6,4),(1,5),(6,5),
      (1,6),(6,6),(1,7),(2,7),(3,7),(4,7),(5,7),(6,7)]
bv100p=[(3,0),(4,0),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),
        (2,2),(3,2),(4,2),(5,2),
        (2,3),(3,3),(4,3),(5,3),
        (2,4),(3,4),(4,4),(5,4),
        (2,5),(3,5),(4,5),(5,5),
        (2,6),(3,6),(4,6),(5,6),
        (1,2),(6,2),(1,3),(6,3),(1,4),(6,4),(1,5),(6,5),
        (1,6),(6,6),(1,7),(2,7),(3,7),(4,7),(5,7),(6,7)]
bv20p=[(3,0),(4,0),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),
       (2,6),(3,6),(4,6),(5,6),
       (1,2),(6,2),(1,3),(6,3),(1,4),(6,4),(1,5),(6,5),
       (1,6),(6,6),(1,7),(2,7),(3,7),(4,7),(5,7),(6,7)]
bv40p=[(3,0),(4,0),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),
       (2,5),(3,5),(4,5),(5,5),
       (2,6),(3,6),(4,6),(5,6),
       (1,2),(6,2),(1,3),(6,3),(1,4),(6,4),(1,5),(6,5),
       (1,6),(6,6),(1,7),(2,7),(3,7),(4,7),(5,7),(6,7)]
bv60p=[(3,0),(4,0),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),
       (2,4),(3,4),(4,4),(5,4),
       (2,5),(3,5),(4,5),(5,5),
       (2,6),(3,6),(4,6),(5,6),
       (1,2),(6,2),(1,3),(6,3),(1,4),(6,4),(1,5),(6,5),
       (1,6),(6,6),(1,7),(2,7),(3,7),(4,7),(5,7),(6,7)]
bv80p=[(3,0),(4,0),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),
       (2,3),(3,3),(4,3),(5,3),
       (2,4),(3,4),(4,4),(5,4),
       (2,5),(3,5),(4,5),(5,5),
       (2,6),(3,6),(4,6),(5,6),
       (1,2),(6,2),(1,3),(6,3),(1,4),(6,4),(1,5),(6,5),
       (1,6),(6,6),(1,7),(2,7),(3,7),(4,7),(5,7),(6,7)]
bh100p=[      (1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),
              (1,2),(2,2),(3,2),(4,2),(5,2),(6,2),(7,2),
        (0,3),(1,3),(2,3),(3,3),(4,3),(5,3),(6,3),(7,3),
        (0,4),(1,4),(2,4),(3,4),(4,4),(5,4),(6,4),(7,4),
              (1,5),(2,5),(3,5),(4,5),(5,5),(6,5),(7,5),
              (1,6),(2,6),(3,6),(4,6),(5,6),(6,6),(7,6)]                  
bh80p=[       (1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),
              (1,2),      (3,2),(4,2),(5,2),(6,2),(7,2),
        (0,3),(1,3),      (3,3),(4,3),(5,3),(6,3),(7,3),
        (0,4),(1,4),      (3,4),(4,4),(5,4),(6,4),(7,4),
              (1,5),      (3,5),(4,5),(5,5),(6,5),(7,5),
              (1,6),(2,6),(3,6),(4,6),(5,6),(6,6),(7,6)]
bh60p=[      (1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),
             (1,2),            (4,2),(5,2),(6,2),(7,2),
       (0,3),(1,3),            (4,3),(5,3),(6,3),(7,3),
       (0,4),(1,4),            (4,4),(5,4),(6,4),(7,4),
             (1,5),            (4,5),(5,5),(6,5),(7,5),
             (1,6),(2,6),(3,6),(4,6),(5,6),(6,6),(7,6)]
bh40p=[      (1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),
             (1,2),                  (5,2),(6,2),(7,2),
       (0,3),(1,3),                  (5,3),(6,3),(7,3),
       (0,4),(1,4),                  (5,4),(6,4),(7,4),
             (1,5),                  (5,5),(6,5),(7,5),
             (1,6),(2,6),(3,6),(4,6),(5,6),(6,6),(7,6)]
bh20p=[      (1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),
             (1,2),                        (6,2),(7,2),
       (0,3),(1,3),                        (6,3),(7,3),
       (0,4),(1,4),                        (6,4),(7,4),
             (1,5),                        (6,5),(7,5),
             (1,6),(2,6),(3,6),(4,6),(5,6),(6,6),(7,6)]

bh0p=[       (1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),
             (1,2),                              (7,2),
       (0,3),(1,3),                              (7,3),
       (0,4),(1,4),                              (7,4),
             (1,5),                              (7,5),
             (1,6),(2,6),(3,6),(4,6),(5,6),(6,6),(7,6)]  
custom_char(oled, 0, 0, bv0p)  
custom_char(oled, 1, 0, bv20p)  
custom_char(oled, 2, 0, bv40p)  
custom_char(oled, 3, 0, bv60p)  
custom_char(oled, 4, 0, bv80p)  
custom_char(oled, 5, 0, bv100p)  
custom_char(oled, 0, 1, bh0p)  
custom_char(oled, 1, 1, bh20p)  
custom_char(oled, 2, 1, bh40p)  
custom_char(oled, 3, 1, bh60p)  
custom_char(oled, 4, 1, bh80p)
custom_char(oled, 5, 1, bh100p)  
oled.show()



最後我想修改設定無線網路連線的程式, 讓 ESP8266 模組連線 AP 時的狀態可以在 SSD1306 OLED 面板上, 參考 :

MicroPython v1.9.1 版韌體測試

測試 7 : 在 OLED 面板上顯示 WiFi 連線狀態

#main.py
from machine import Pin,PWM,I2C
import network
import time
import ssd1306  

WAIT_FOR_CONNECT=8

pwm2=PWM(Pin(2), freq=5, duty=512)

i2c=I2C(scl=Pin(5), sda=Pin(4))
oled=ssd1306.SSD1306_I2C(128, 32, i2c)  

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])
            oled.fill(0)
            oled.text('IP=' + sta.ifconfig()[0],0,0)
            oled.show()
        else:
            cs.send(html % form)
        cs.close()
    s.close()

def get_ip():
  return (network.WLAN(network.STA_IF).ifconfig()[0],
  network.WLAN(network.AP_IF).ifconfig()[0])

def ap_on():
  network.WLAN(network.AP_IF).active(True)

def ap_off():
  network.WLAN(network.AP_IF).active(False)

#try connecting to lastest configured AP
sta=network.WLAN(network.STA_IF)
sta.active(True)
print('Connecting to AP ...')
oled.text('Connecting to AP',0,0)
oled.show()  
time.sleep(WAIT_FOR_CONNECT)
if not sta.isconnected():
    set_ap()
else:
    pwm2.deinit()
    Pin(2).value(0)
    print('Connected:IP=', sta.ifconfig()[0])
    oled.fill(0)
    oled.text('IP=' + sta.ifconfig()[0],0,0)
    oled.show()
    #Application code is written here or import from a separate file
    import myapp
    myapp.main()

藍色部分為新增的程式碼, 當 ESP-12 模組或 D1 Mini 開機時  OLED 面板會顯示 'Connecting to AP', 板上 LED 會快閃, 若能連線到之前設定的 AP, 則 8 秒後會顯示 'ap=192.168.xxx.xxx', 板上 LED 熄滅, 並開始執行 myapp.py 應用程式. 若無法連線前次 AP, 可用手機 WiFi 連線 SSID 為 MicroPython_xxxxxx 之 ESP8266 內建基地台 (密碼為 micropythoN), 開啟手機瀏覽器連線 192.168.4.1, 在顯示的網頁中輸入附近可連線之 AP, 然後將 ESP-12 或 D1 Mini 重開機即可.

參考 :

# DIY - ESP8266:SSD1306 0.96吋 I2C OLED 屏電路(十九)
I2C driver for micropython esp8266 ssd1306 OLED (ssd1306a.py is paired down and works with the font.py file to allowing generating text)
# ssd1306 using I2C on the esp8266
# https://github.com/adafruit/micropython-adafruit-ssd1306
# ESP01(ESP8266) driving an SSD1306 display in microPython
A playground for various MicroPython scripts 
# A sensor monitor with OLED in MicroPython
ssd1306 using I2C on the esp8266
I2C driver for micropython esp8266 ssd1306 OLED (另一個驅動程式)

2017年8月11日 星期五

鄉村基礎木作第五課 : 鎖抽屜前板與製作門板

因本周六木樂園公休, 所以昨晚吃過飯後就去上木工第五課. 接續上週進度, 這回是給上週做好的兩個抽屜裝上前板. 由於抽屜箱底下會裝上 5mm 厚的軌道與前檔條, 所以前板下源須往下突出此厚度, 左右對稱後用 F 形夾固定住, 不用上膠 (因有四個螺絲可以鎖很緊),  準備上螺絲 :


因為前板下方的螺絲太裡面, 電鑽須加一個如下圖所示的 90 度轉向頭才好鎖上去 :


到這裡抽屜算是完成了. 接下來是製作門板框, 由厚1.8 cm 寬 6 cm 的長短各一對板材拼合而成, 為了使結構強固, 接合處須鑽洞後以木釘膠合, 所以老師要我先畫線定出要鑽孔的中心線. 先在桌上擺好門框, 將紋路較佳的朝外在四塊板材上用鉛筆做上記號表示此為正面 (這很重要, 否則到後面會搞混), 然後將要接合的板材對齊夾緊即可劃中心線. 厚 1.8cm 故中心點是 0.9cm, 而寬度 6cm 左右 3cm 故從左右各量 1.5cm 劃出中心點 :


畫好後用尖起子在中心點刺出一個小洞, 這樣鑽洞時才不會歪掉. 接下來要調整鑽頭, 下鑽深度必須比木釘的一半再多 1mm, 否則太淺會無法讓板材密合. 橫方向鑽洞時直接將板材放在鑽台上調好高度對準中心點即可鑽洞, 但縱方向就沒辦法了, 須將鑽台鬆開轉到後面, 然後把板材用固定底座夾緊後放在鑽床底盤上才能鑽 :


鑽好洞後先塞木釘把門板組合起來, 看看有否密合, 若接合處高低不平, 可用手持電鑽在反方向將洞加大一些微調. 然後就可以上膠塞木釘組合, 趁膠尚未凝固用夾子上下前後夾緊使接合處密合, 靜置一天後再取下 :


門框這樣就先打住, 等膠乾了後铣好溝再裝中間的門板. 剩下一點時間老師教我用線鋸機鋸底板下方左右兩個裝飾用的造型板. 先用模板在板材上畫線後手持線鋸機沿線裁切, 但這線鋸機有點重量又不好控制, 我一連鋸了三塊還是不甚合格. 最後老師說如果後面沒有鋸出更好的, 就用今天較好的兩個來修.

補記 : 購買姊姊的筆電

姊姊要去台北上大學了, 需要一台電腦, 所以 8/8 放榜當晚就帶她去自由路的燦坤看一下, 對, 原本只是要看一下而已, 然後再研究看看, 結果卻當場決定買了 ACER 這台 VX5-591G-54N2, 因為是父親節特價 31900, 只到 8/8, 所以查了一下網路最低也要 33000, 所以就決定買了.

今天把這台電腦帶去公司開會用, 順便安裝工具軟體, 然後製作映像檔, Win 10 是在控制台/檔案歷程記錄裡面點 "系統映像備份/建立系統映像", 然後會掃描可用來存放映像檔的儲存媒體, 必須備份到不同硬碟才行 (例如外接 USB 硬碟), 參考 :

# 升級 Win10 並建立系統映像檔

2017年8月10日 星期四

C 語言自學手冊

二哥下學期要升上高二了, 新學期的導師是數學老師, 對 Coding 也有涉獵, 在他原先帶的高一班上有組織一個資訊小組學習 C 語言, 高二選組後也有部分同學來到二哥這班, 導師又繼續糾合對寫程式有興趣者加入, 二哥說他也加入了. 最近老師介紹這本 "C 語言自學手冊" 的教材給他們 :

https://hackersir.gitbooks.io/c/content/

我稍微看了一下覺得很不錯, 他說可否印出來方便閱讀, 所以昨天下班前找出高師大附近以前幫我印碩士論文的盧老闆電話, 詢問 192 頁含膠裝要多少錢, 呵呵, 只要 130 元, 叫我現場等一下馬上就可取貨. 盧老闆多年不見還是一樣穿西裝皮鞋上班, 真是非常紳士的人.

2017年8月9日 星期三

向露天 56jkx6p 購買方形變壓器等零件一批

上週組裝的直流電源供應器少了一個 28V 變壓器, 擬向露天賣家 56jkx6p 購買, 為攤分運費同時添購需要用的一些零組件如下 (LM358N 是實驗常用的比較器 IC, DIP 封裝) :

# 方型變壓器(3.9V-0V)(28V-0V)(11V-11V-0V)DB519 $80
# IC LM358N / LM393N /HA17358/C741C/TI9240  $3*10=30
# SM2P對插線LED電源 天花板LED燈(線徑1.7mm 公母2條1組) $9*2=18
# CH2端子台按壓式 電源線快速接頭 快速接線端子(1包10個) $15*2=30

合計 80 + 30 + 18 + 30 =158 + 超商取貨付款 60 共 218 元
 

2017年8月8日 星期二

姊姊考上台師大

今天早上接到姐姐電話, 說考上台師大了, 聽得出來非常高興, 因為這是她填的第一志願, 而且指考名額只有 5 名. 唐朝詩人孟郊在中進士後所做的詩 "登科後" 云 : "春風得意馬蹄疾, 一日看盡長安花", 這應該是姐姐今日心情的寫照吧! 我中午跟爸說了這件事, 晚上打電話時他說高興得午覺睡不著哩.

雖然花了快半年準備指考比較辛苦, 但考上自己夢想的科系也是值得的. 當初繁星她列為第一志願的高師大視設這回卻在選填時排到第 6 了. 查榜發現第二, 三志願的台藝大動畫, 視設依去年錄取成績可上, 但今年分數比較高, 姊姊竟然沒上哩! 所以即使將台藝大列第一第二, 台師大列第三的話, 還是會錄取台師大. 她說國中最要好的同班同學阿媛也是上台師大 (繁星), 這樣去台北讀書也有個伴.

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 測試