2017年7月21日 星期五

WeMOS D1 Mini 開發板測試

昨晚把前陣子在露天買的 WeMOS NodeMCU v2 與 D1 Mini 開發板找出來, 這兩個都是散裝, 必須自行焊接針腳, 因為已過 11 點, 焊下去會搞到 12 點後, 就先收起來. 今天吃過晚飯後開始動工, 順便將自走車要用的 18650 降壓板接線都一併焊好.

之前有問過賣家, 這兩塊板子都是 4MB Flash, WeMOS NodeMCU v2 這塊出廠搭載 NodeMCU, 而 D1 Mini 則無, 所以焊好後就來給 D1 Mini 灌 MicroPython 測試看看. WeMOS D1 Mini 的微控器是 ESP-12EX 模組, 運作於 3.3V 電壓位準, 拉出 9 個 GPIO 輸出入腳, 2 個 UART 腳, 以及一個 ADC(A0) 類比輸入腳, 板子左下方還有一個 Reset 按鈕, 規格詳見官網 :

https://wiki.wemos.cc/products:d1:d1_mini

ADC(A0) 的類比輸入準位為 0~3.3V, 經 ESP8266 內部 ADC 轉換為 0~1023 的整數, 高於 3.3V 的類比信號須經電阻分壓電路降壓至最大信號時為 3.3V. 另外, 在 MicroPython 中控制 GPIO 腳須以 ESP8266 腳位編號為對象, 不可用 D1 Mini 板子上的 D0~D8, 其對照如下 :

 板子腳位 ESP8266 腳位 功能
 D0 GPIO16 IO
 D1 GPIO5 IO, SCL 
 D2 GPIO4 IO, SDA 
 D3 GPIO0 IO, 內建 10K 上拉電阻
 D4 GPIO2 IO, 內建 10K 上拉電阻與 LED
 D5 GPIO14 IO, SCK
 D6 GPIO12 IO, MISO
 D7 GPIO13 IO, MOSI
 D8 GPIO15 IO, SS, 內建 10K 上拉電阻
 TX TXD UART 送端
 RX RXD UART 收端
 A0 ADC 類比輸入 (0~3.3V)

注意, 在燒錄 MicroPython 韌體前必須將 D3 (GPIO0) 接地再插入 Micro USB 開機, 這樣才會進入 Flash 燒錄模式. 然後開啟 ESP8266Flasher 軟體, 選定所接的 COM 埠, 指定要燒錄之韌體位置與 4MB 的 Flash 大小後, 按 Flash 鈕即會看到 ESP8266 的 MAC 位址顯示並開始燒錄, 過程中 D1 Mini 板上的藍色 LED 燈會閃爍, 表示韌體正寫入 Flash 中, 完成後左下角的 NODEMCU TEAM 前面會打勾 :




注意, ESP-12 系列的 Flash 記憶體都是 4MB.

韌體燒錄說明參見 :

# MicroPython on ESP8266 (一) : 燒錄韌體
# MicroPython v1.9.1 版韌體測試

燒錄完成後拔除 D3 (GPIO0) 的接地線重開機 (按 Reset 鈕或插拔 USB), 開啟 PuTTY 用 Serial 速率 115200 連線 D1 Mini, 按 Ctrl+B 即進入 REPL 介面, 用 os.listdir() 可知檔案目錄下面只有一個 boot.py 檔案 :

MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>> import os
>>> os.listdir()
['boot.py']
>>> f=open('boot.py','r')
>>> lines=f.readlines()
>>> for line in lines:
...     print(line)
...
...
...
# This file is executed on every boot (including wake-boot from deepsleep)

#import esp

#esp.osdebug(None)

import gc

#import webrepl

#webrepl.start()

gc.collect()

>>>

然後開啟 Web REPL 功能 :

>>> import webrepl_setup
WebREPL daemon auto-start status: disabled

Would you like to (E)nable or (D)isable it running on boot?
(Empty line to quit)
> E    (開啟 webrepl 功能)
To enable WebREPL, you must set password for it
New password: 123456
Confirm password: 123456
Changes will be activated after reboot
Would you like to reboot now? (y/n) Y
Would you like to reboot now? (y/n) y  (要用小寫 y)

寫入完成後會要求重啟 :

l▒▒|▒▒#4 ets_task(40100164, 3, 3fff829c, 4)
WebREPL daemon started on ws://192.168.4.1:8266
Started webrepl in normal mode
OSError: [Errno 2] ENOENT

MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.

更多檔案操作參考 :

# MicroPython on ESP8266 (六) : 檔案系統測試

接下來參考之前伺服器測試這篇 :

# MicroPython on ESP8266 (十四) : 網頁伺服器測試

用記事本編輯如下 main.py 檔案 :

#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 後用 ampy 將其上傳到 D1 Mini 裡 :

D:\test>ampy --port COM4 put main.py

然後按 Ctrl+D 軟開機 :

PYB: soft reboot
#9 ets_task(40100164, 3, 3fff829c, 4)
WebREPL is not configured, run 'import webrepl_setup'
Connecting to AP ...   (連線到最近一次設定之無線基地台, 等候 8 秒)
Connected:IP= 192.168.2.111    (連線成功)
MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>>

如果顯示 "listening on 192.168.4.1", 表示上次設定的 AP 目前收不到訊號無法連線, 這時打開手機 WiFi 功能, 找尋 D1 Mini 的 ESP8266 內建的 AP, 其 SSID 是以 "MicroPython-" 開頭 (後面是 MAC 後 6 碼), 以內建密碼 "micropythoN" 連線此 AP :

SSID=MicroPython-c111d1
PWD=micropythoN

再打開手機瀏覽器連線 192.168.4.1, 會看到一個 AP 設定網頁, 輸入欲連線之 AP 的 SSID 與 PWD 後按 Connect 即可, 這時 D1 Mini 重開機即可連線至該無線基地台了.

接著修改上面 main.py 的最後兩行, 拿掉前面的 #, 變成 :

#myapp.py
import myapp
myapp.main()

重新上傳 main.py, 然後參考下面這篇的測試 4 : "建立網頁伺服器顯示 GPIO 輸入腳的狀態" :

MicroPython on ESP8266 (十四) : 網頁伺服器測試

但要修改第二行的 pins 串列內的元組, 因為那篇是針對 ESP-01 模組, 它只有 GPIO0 與 GPIO2 兩支輸出入腳, 而 D1 Mini 卻有 9 支 GPIO 腳, 然後存成 myapp.py :

#myapp.py
import machine
pins=[machine.Pin(i, machine.Pin.IN) for i in (0, 2, 4, 5, 12, 13, 14, 15, 16)]  

html="""<!DOCTYPE html>
<html>
    <head> <title>ESP8266 Pins</title> </head>
    <body> <h1>ESP8266 Pins</h1>
        <table border="1"> <tr><th>Pin</th><th>Value</th></tr> %s </table>
    </body>
</html>
"""

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)
    print(str(data,'utf8'), end='\n')
    rows=['<tr><td>%s</td><td>%d</td></tr>' % (str(p), p.value()) for p in pins]
    response=html % '\n'.join(rows)
    cs.send(response)
    cs.close()

s.close()

將此 myapp 上傳至 D1 Mini :

D:\test>ampy --port COM4 put myapp.py

按 Ctrl+D 軟開機, 然後手機連線 ESP8266 本身內建的 AP, 再用連覽器連線 192.168.4.1 便可看到 D1 Mini 各 GPIO 腳的狀態了 :


可見除 GPIO15 與 GPIO16 外, 其餘拉出來的 IO 腳都被 Pull-up 到 HIGH 位準了.

我覺得 D1 Mini 比 ESP-01 模組似乎更穩, 而且大小也沒有大多少, 價格也只貴了 20~30 元左右, 但 D1 Mini 有內建 CH340G USB 晶片與 Micro USB 插槽, 光是加這兩個元件就超過 30 元了, 而且還比 ESP-01 多出 7 個 GPIO 埠與 1 個 ADC 埠, 最重要的是, Flash 記憶體大小 D1 Mini 是 ESP-01 的四倍 (=4MB), 便利性與功能性都比 ESP-01 模組強. 如果是簡單的物聯網應用, GPIO 需求不超過 2 個的話, 用 ESP-01 即可; 否則用 D1 Mini 也沒多多少成本.

 項目 ESP-01 模組 WeMOS D1 Mini
 價格 約 80 元 約 110 元
 GPIO 數 2 9
 Flash 大小 1M Bytes 4M Bytes
 ADC 腳 無 有 1 個
 USB 介面 無 (須外接轉換線) Micro USB
 Reset 鈕 無 有

除了 D1 Mini 外, WeMOS 還有 D1 Mini Pro, Lite, 以及 Lolin 32 等開發板, 參考 :

https://wiki.wemos.cc/products:d1:d1_mini_lite
https://wiki.wemos.cc/products:d1:d1_mini
https://wiki.wemos.cc/products:d1:d1_mini_pro
https://wiki.wemos.cc/products:lolin32:lolin32

Lite, Pro 板子大小與 Mini 大小一樣, 但 Lite 的 Flash 記憶體只有 1 MB, 而 Pro 則是 16MB; Lite 沒使用 ESP 模組, 而是將 ESP8266 與天線直接佈在單一板子上. Pro 則是改用陶瓷天線並附有外接天線座, 同時 USB-TTL 晶片改為 CP2102. Lolin 特點是內建藍芽並提供鋰電池接頭 (使用 PH-2 2.0mm), Flash 則維持 4MB.

 WeMOS D1 Mini Lite D1 Mini D1 Mini Pro Lolin
 微控器 ESP-8285 ESP-8266EX ESP-8266EX ESP-WROOM-32
 最高時脈 80/160MHz 80/160MHz 80/160MHz240MHz
 記憶體大小 1MB 4MB 16MB 4MB
 天線 PCB PCB 陶瓷 + 外接座 PCB
 藍芽 No No No Yes
 USB-TTL CH340G CH340G CH340G CP2102
 鋰電池接頭 No No No Yes

2017-07-30 補充 :

D1 Mini 天線底下有一顆藍色 LED, 開機送電時會閃一下, 根據上面的 GPIO 針腳對應表, 其中 D4 針腳 (GPIO 2) 有接 LED, 指的就是這顆 LED, 在開發物聯網應用時可以利用這個 LED 來顯示系統狀態, 例如 WiFi 尚未連線時快閃, 連線成功後常亮或慢閃; 系統異常時快閃等等, 這是使用 D1 Mini 的一個方便之處 : 不需外接 LED.

2017-08-02 補充 :

D1 Mini 也可以使用 Arduino 開發, 參考  :

# Wemos D1 Mini, ESP8266 Getting Started Guide With Arduino


另外跟同一賣家買的 Wemos NodeMCU V2 板子只是先焊好, 開機接上 COM 埠有顯示 Ready 表示板子 OK 即可, 賣家說預先搭載了 NodeMCU, 因此暫時不燒錄 MicroPython 進去, 等 MicroPython 弄完有空或許會研究看看, 參考 :

# Comparison of ESP8266 NodeMCU development boards
# TOP 6 ESP8266 MODULES FOR IOT PROJECTS
# Mini NodeMCU (Wemos), Prototype Board, NodeMCU (DOIT&LoLin), Arduino like Wemos, Witty


2017-08-14 補充 : 

如果 D1 Mini 有接 1602 LCD 或 SSD1306 OLED 顯示器, 在開機過程中可以將連線 AP 的情況顯示於 LCD 或 OLED 上, 參考 :

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

下面列出無顯示器版, 1602 LCD 版, 以及 SSD1306 OLED 版的 AP 設定程式 main.py :

1.  無顯示器版 AP 設定程式 :

#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()

2.  1602 LCD 版 AP 設定程式 :

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

WAIT_FOR_CONNECT=8

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

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

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])
            lcd.move_to(0, 0)
            lcd.putstr('AP Connected')
            lcd.move_to(0, 1)
            lcd.putstr('IP=' + sta.ifconfig()[0])
        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 ...')
lcd.move_to(0, 0)
lcd.putstr('Connecting to AP')
time.sleep(WAIT_FOR_CONNECT)
if not sta.isconnected():
    set_ap()
else:
    pwm2.deinit()
    Pin(2).value(0)
    print('Connected:IP=', sta.ifconfig()[0])
    lcd.move_to(0, 0)
    lcd.putstr('AP Connected')
    lcd.move_to(0, 1)
    lcd.putstr('IP=' + sta.ifconfig()[0])
    #Application code is written here or import from a separate file
    import myapp
    myapp.main()


3.  SSD1306 OLED 版 AP 設定程式 :

#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('AP connected',0,0)
            oled.text('IP=' + sta.ifconfig()[0],0,1)
            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('AP connected',0,0)
    oled.text('IP=' + sta.ifconfig()[0],0,1)
    #Application code is written here or import from a separate file
    import myapp
    myapp.main()


注意, 顯示器版需下載驅動程式, 1602 LCD 需要下載下列兩個程式 :

# https://github.com/dhylands/python_lcd/blob/master/lcd/lcd_api.py
https://github.com/dhylands/python_lcd/blob/master/lcd/esp8266_i2c_lcd.py

SSD1306 OLED 版需下載下列程式 :

https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py
  
將這些驅動程式與 main.py 一起上傳到 D1 Mini 即可.

D:\ESP8266\test>ampy --port COM8 put main.py
D:\ESP8266\test>ampy --port COM8 put cd_api.py
D:\ESP8266\test>ampy --port COM8 put esp8266_i2c_lcd.py
D:\ESP8266\test>ampy --port COM8 put ssd1306.py

沒有留言 :