2017年4月29日 星期六

MicroPython on ESP8266 (二) : 數值型別測試

前幾天偶然發現了 MicroPython 這個專案後, 嘗試在之前拿來做為 Arduino 連網模組的 ESP-01 板子上燒錄 MicroPython for ESP8266 韌體, 發現這塊 512KB FLASH 的 Python 功能雖然受到一些限制, 而且只接出了 GPIO0 與 GPIO2 兩個輸出入埠, 但以其內建 WiFi 網路能力與 MicroPython 加持下, 在成本與體積方面, 作為小型 IOT 端設備 (兩個輸出入腳) 還是比 Arduino 方便與便宜 (ESP-01 一片才 60~80 元). 事實上 ESP8266 本身是 32 位元核心處理器, 屈就於充當 Arduino 的上網模組確實是可惜了些, ESP-01 模組本身就可以當微控器啊!

如何燒錄 MicroPython 韌體到 ESP-01 參考 :

# MicroPython on ESP8266 (一) : 燒錄韌體
韌體燒錄完畢後, 關掉 ESP-01 模組的電源, 拔掉 GPIO0 的接地線後再送電, 開啟 Putty 以 115200 速率連線 ESP-01 模組所連接的串列埠, 就會進入 MicroPython 的 REPL 命令列 (即 Python 解譯器) 介面了.

下面是我測試 512KB MicroPython 的紀錄, 主要參考了下面兩本書 :

Python 程式設計入門 (博碩出版, 葉難) :

這本是我看過 Python 中文入門書籍寫得最深入詳盡的一本 (不是之一).


# 精通 Python-運用簡單的套件進行現代運算 (碁峰出版, 賴屹民譯) :

Source : 金石堂

這本書原文就寫得很棒, 原作者 Bill Lubanovic 筆調非常風趣, 譯者賴屹民翻譯功夫也是了得, 我覺得這本是最適合入門者看的第一本 Python 書, 原文書是歐萊禮出版的 "Introducing Python" :

Source : 歐萊禮

Python 的資料型態分為數值資料型態與容器資料型態, 數值資料型態有下列四種 :
  1. int (整數)
  2. float (浮點數)
  3. bool (布林)
  4. complex (複數, MicroPython 由於記憶體限制沒有實作)  
容器類型則有五種 :
  1. str (字串)
  2. tuple (元組)
  3. list (串列)
  4. dict (字典)
  5. set (集合)  
其中數值型態與容器類型中的 tuple 與 str 是不可變 (immutable) 的資料型態, 而 list, dict 與 set 三種為可變 (mutable) 的資料型態. 所謂不可變是指物件一旦建立, 其值即不可再更改.

直接輸入 int, float, str 等資料類型名稱可檢查該類型有無實作 :

<<< int
<<< float
<<< bool
<<< str
<<< complex
Traceback (most recent call last):
  File "NameError: name 'complex' is not defined

Python 的內建函數 type() 則可檢查資料的型態 :

<<< type(1)
<<< type(1.0),type(2e10)
(<<< type(True), type(False)
(<<< type('Hello'), type("Hello")
(<<< type(1+2j)
Traceback (most recent call last):
  File "SyntaxError: complex values not supported

Python 的數值資料型態有三種 : 整數 (int), 浮點數 (float), 以及複數 (complex). 支援複數使得 Python 在科學運算上具有其他語言沒有的優勢, 不過因為 MicroPython 是 Python 3 在嵌入式設備上的精簡版, 受大小限制不支援複數 :

>>> 1+1j
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: complex values not supported

另外 MicroPython 也沒有實作 decimal 函式庫 :

>>> from decimal import *
Traceback (most recent call last):
  File "ImportError: no module named 'decimal'


一. 整數 (int) :

Python 3 的整數具有無限精準度, 可以表示極大整數直到記憶體容量的極限為止. MicroPython 的整數可以表示到多大呢? 表示 Googol (10 的 100 次方) 是沒問題的 :

>>> googol=10**100
>>> googol
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

表示 10**200 也沒問題 :

>>> googol*googol
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

但是 googol 的 googol 次方就不行了, 好像是記憶體溢位導致系統重置 (reset) :

>>> googol ** googol

 ets Jan  8 2013,rst cause:1, boot mode:(3,7)

load 0x40100000, len 31888, room 16
tail 0
chksum 0x65
load 0x3ffe8000, len 1072, room 8
tail 8
chksum 0xa4
load 0x3ffe8430, len 3000, room 0
tail 8
chksum 0x0c
csum 0x0c
......
......
lŒŒŽbl`‚‚lû#4 ets_task(40100164, 3, 3fff827c, 4)
OSError: [Errno 2] ENOENT
OSError: [Errno 2] ENOENT

MicroPython v1.8.7-662-gf85fd79 on 2017-04-25; ESP module with ESP8266
Type "help()" for more information.
>>>

到底對 512KB 的 ESP-01 模組而言所能表示的整數是多少? 經過用二分法逐步測試發現, 10**2129 沒問題, 但 10**2130 就不行了.

>>> 10**2030

 ets Jan  8 2013,rst cause:1, boot mode:(3,7)

load 0x40100000, len 31888, room 16
tail 0
chksum 0x65
load 0x3ffe8000, len 1072, room 8


Python  的算術運算子有如下七個 :

運算子 說明
 x + y 加法運算
 x - y 減法運算
 x * y 乘法運算
 x / y 除法運算
 x // y 整數除法運算
 x ** y 冪次運算
 x % y 取餘數運算

基本的算術運算測試如下 :

>>> 1+2+3+4+5+6+7+8+9+10
55
>>> 1 - 2-3
-4
>>> 2*3
6
>>> 1+2*3      #先乘除後加減
7
>>> (1+2)*3   #用括號改變優先順序
9
>>> 11/3    #一般除法 (只精確到小數後第 5 位四捨五入)
3.66667
>>> 11//3   #整數除法 (求商)
3
>>> 11%3  #整數除法 (求餘數)
2
>>> divmod(11,3)  #求商與餘數 (傳回 tuple, 前為商, 後為餘數)
(3, 2)
>>> 11/0     #除以 0 會導致例外
Traceback (most recent call last):
  File "ZeroDivisionError: division by zero

>>> 10**3     #次方
1000

除了上述的四則運算外, Python 還有相對應的複合運算子 (也適用於浮點數運算) :

>>> a=10
>>> a += 10   #運算前 a=10
>>> a
20
>>> a -= 10    #運算前 a=20
>>> a
10
>>> a *= 10   #運算前 a=10
>>> a
100
>>> a //= 10    #運算前 a=100
>>> a
10
>>> a /= 10     #運算前 a=10
>>> a
1.0


Python 的整數字面值 (Linteral) 預設是 10 進位表示法, 除此之外也可以用 2 進位, 8 進位或 16 進位等基數系統來表示, 以 0b 或 0B 開頭的整數為 2 進位; 0o 或 0O 開頭為 8 進位; 0x 或 0X 開頭則為 16 進位, 如下所示 :

>>> 0b10   
2
>>> 0B10
2
>>> 0o10
8
>>> 0O10
8
>>> 0x10
16
>>> 0X10
16
>>> 0x1f
31


以前在 Python 2.x 時代以 0 開頭的整數會被認為是 8 進位 (即與 0o 或 0O 開頭一樣) :

C:\Users\Tony>py -2
Python 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:19:22) [MSC v.1500 32 bit (
Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 012    #被認為是 8 進位整數
10

但這在 Python 3 已經被廢棄了, 整數前面的 0 會被忽略去除  :

>>> 012    #被認為是 10 進位整數

12  

Python 內建函數 bin(), oct(), 與 hex() 可將 10 進位整數分別轉成 2, 8, 與 16 進位表示法 : 

>>> bin(255)
'0b11111111'
>>> oct(255)
'0o377'
>>> hex(255)
'0xff'

除了使用字面值 (Literal) 直接產生整數物件外, 也可以用 Python 內建函數 int() 來建立 int 物件, 此函數能將整數或字串轉成 int 物件, 它可以傳入一或兩個參數 :

int(p1 [,p2])

第一個參數可以是整數字面值或字串, 只有當第一參數是字串時可以用第二參數指定要用哪種基數轉換, 而且不限 2, 8, 16 進位, 可以是任何進位, 最多可到 36 進位  (0~9, A~Z, 共 36 個符號, 大小寫不分), 可用 0, 2~36, 其中 0 表示依字串格式 : 

>>> int(3.14159)
3
>>> int("0b1111",2), int("0b1111")
(15, 15)
>>> int("0o20",8), int("0o20")
(16, 16)
>>> int("0xffff",16), int("0xffff")
(65535, 65535)
>>> int('0xff',0)
255
>>> int('ABCXYZ1230',36)     #36 進位轉成 10 進位
1047646094138316
>>> int('ABCXYZ1230',37)     #第二個參數只能 2~36
Traceback (most recent call last):
  File "ValueError: int() arg 2 must be >= 2 and <= 36

但是如果格式不符合要求會產生錯誤 :

>>> int("0o20",10)
Traceback (most recent call last):
  File "ValueError: invalid syntax for integer with base 10
>>> int("0o20",16)
Traceback (most recent call last):
  File "ValueError: invalid syntax for integer with base 16


其中 ob/0B 開頭字串的 int() 是 bin() 的反運算; 0o/0O 開頭的是 oct() 的反運算; 0x/0X 開頭的是 hex() 的反運算.

與整數有關的還有位元運算, 位元運算子有六個, 其運算元只能是整數 :

 運算子 說明 
 ~x 位元 NOT 運算, 每一個位元做 0 變 1 與 1 變 0 運算
 x< 位元左移運算, 運算元 x 的每一個位元往左移 y 次, 右方補 0
 x>>y 位元右移運算, 運算元 x 的每一個位元往右移 y 次, 左方補 0
 x&y 位元 AND 運算, 運算元 x 與 y 的相對位元做 AND 運算
 x|y 位元 OR 運算, 運算元 x 與 y 的相對位元做 OR 運算
 x^y 位元 XOR 運算, 運算元 x 與 y 的相對位元做 XOR 運算

左移運算值會變大, 每左移一位元增大 2 倍 (乘以 2); 而右移運算值會變小, 每右移一位元減小 2 倍 (除以 2), 例如 :

>>> 2<<1
4
>>> 4<<1
8
>>> 8<<1
16
>>> 16>>1
8
>>> 8>>1
4
>>> 4>>1
2
>>> 2>>1
1
>>> 1>>1
0
>>> 2<<3
16



二. 浮點數 (float)  :

浮點數 (float 型別) 可以用小數點與科學表示法 e 或 E (10 的次方之意, 不是自然指數的 e) 來表示, 但小數在 MicroPython 只能表示到小數點後 5 位四捨五入 (
Win7 64 位元 PC 上可到小數後第 16 位) :

>>> 1.23456789
1.23457
>>> 3.1415926    #圓周率
3.14159
>>> 6.02e23    #亞佛加厥數
6.02e+23
>>> 6.626070040e-34     #普郎克常數
6.62607e-34
>>> 1.38064852e-23     #波茲曼常數
1.38065e-23

MicroPython 浮點數絕對值用科學表示法最大到 3e48, 最小約為 1e-40 :


>>> 3.4e38
3.4e+38
>>> 3.41e38
inf
>>> 1e-39
1e-39
>>> 1e-40
9.99967e-41      #開始有誤差
>>> 1e-43
9.52883e-44      #誤差變大
>>> 1e-44
5.60519e-45      #誤差更大
>>> 1e-45

0.0                     #太小變 0 了

由於浮點數是以二進位儲存, 大部分的浮點數其實無法以二進位完整精確表示 (事實上只有整數可以精確表示), 而只是非常接近理論值的近似值而已, 實際運算結果與理論值會有些微誤差. 例如下列程式理論上應該得到 1.0 的結果才對, 但實際上卻是 0.999999 :

>>> s=0;
>>> for i in range(10):
...     s += 0.1
...
...
...
>>> sum==1
False          #竟然不是 1
>>> sum==1.0
False         #竟然不是 1
>>> s
0.999999      #理論值為 1.0, 實際上是 0.999999

因此浮點數不應該直接拿來比較, 而是應該比較與理論值的差是否在容許的誤差內才對, 例如 :

>>> (s-1) <= 0.0000000001    #與理論值的差比較小於容許誤差就可認為相等
True

除了用字面值建立浮點數物件外, 還可以用內建函數 float() 來建立 float 物件, 它只有一個參數, 可傳入整數或浮點數字面值, 或者是一個字串, 傳回一個 float 物件 :

>>> float()      #沒有傳入參數預設為 0.0
0.0    
>>> float(123), float(-123)
(123.0, -123.0)
>>> float(3.1415926)    #精確度為小數點後五位
3.14159
>>> float('3.1415926')   #傳入字串
3.14159
>>> float('1.23456789e23')
1.23457e+23
>>> float(3.4e38)          #科學表示法最大為 3.4e38
3.4e+38
>>> float(3.41e38)
inf
>>> float(1e-38)
1e-38
>>> float(1e-39)
1e-39
>>> float(1e-40)            #開始有誤差
9.99967e-41
>>> float(1e-43)
9.52883e-44
>>> float(1e-44)
5.60519e-45
>>> float(1e-45)            #太小變 0 了
0.0    

與浮點數運算相關最常用的內建函數是 round(), 可以傳入一或兩個參數, 其中第二參數是指定四捨五入到小數點後第幾位, MicroPython 只能表示到小數後 5 位, 故第二參數大於 5 之後結果都一樣, 直到 39 時會傳回一個特殊值 nan (非數字, not a number), 例如 :

<<< round(3.1415926)
3
<<< round(3.1415926,1)
3.1
<<< round(3.1415926,2)
3.14
<<< round(3.1415926,3)
3.142
<<< round(3.1415926,4)
3.1416
<<< round(3.1415926,5)
3.14159
<<< round(3.1415926,6)
3.14159
<<< round(3.1415926,38)
3.14159
<<< round(3.1415926,39)
nan

更奇怪的是, round() 在 Python 3 有向偶數靠攏的怪癖 (參考上述葉難書中的 3-1 節), 例如 :

<<< round(1.5),round(-1.5)
(2, -2)
<<< round(2.5),round(-2.5)
(2, -2)
<<< round(3.5),round(-3.5)
(4, -4)
<<< round(4.5),round(-4.5)
(4, -4)
<<< round(5.5),round(-5.5)
(6, -6)
<<< round(6.5),round(-6.5)
(6, -6)

藍色部分是預期要四捨五入進位到個位數得到 (3, -3), (5, -5), 以及 (7,-7) 的, 但結果卻是捨去 .5 而倒向偶數. 此特性在使用 round() 進行數學計算時必須注意, 否則會得到非預期結果.

而在 Python 2.x 執行結果卻不是這樣 :

C:\Users\Tony>py -2
Python 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:24:40) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> round(1.5),round(-1.5)
(2.0, -2.0)
>>> round(2.5),round(-2.5)
(3.0, -3.0)
>>> round(3.5),round(-3.5)
(4.0, -4.0)
>>> round(4.5),round(-4.5)
(5.0, -5.0)
>>> round(5.5),round(-5.5)
(6.0, -6.0)
>>> round(6.5),round(-6.5)
(7.0, -7.0)

感覺 Python 2.x 的 round() 處理四捨五入方式較合理, 但據說 Python 3 改成這樣其實是符合 IEEE 754 預設規範的, 參考 :

# Python 3.x rounding behavior

此文的回應中提到 IEEE 754 裡面有五種處理浮點數 rounding 的方式, 而 Python 2.x 所用的四捨五入只是其中一種. 英文 Rounding 一般翻成四捨五入其實是有偏見的, 正確的翻譯應該是 "湊整", 就是湊成一個鄰近的整數.

Python 3 所採用的是 IEEE 754 的預設湊整方式 : rounding to even (湊整到最近的偶數). 四捨五入法 (rounding up) 是原數加 0.5 的方式進行湊整, 此法在大部分數據趨向於兩個整數中間值 (x.4, x.5, x.6 附近) 時會造成平均值與其他統計量的較明顯偏差. 參考 :

What's the difference between round up, round down and round off?

"There are two schools of thought on this. One is to round UP in this situation. Rounding off is then analogous to adding 0.5 to the number and rounding DOWN the result. This is simple and computers often use this method of rounding off. However, when dealing with a large set of numbers (many being half way between two integers), this process will introduce a bias which would affect the mean of the numbers and other statistics.

The other school of thought is to round OFF to the nearest EVEN integer. This is not that difficult to code in a computer and eliminates any bias that might be introduced in the rounding process."

另外有三個特殊的浮點數 float('nan'), float('inf'), 以及 float('-inf'), 分別用來表示非數值 (nan), 正無窮大 (inf), 以及負無窮大 (-inf). 當浮點數超過硬體所能表示最大值時就會得到 inf, 在 ESP-01 大於 3.4e34 就被認為是 inf. 而 nan 會在無法進行的數學運算時產生, 例如 :

>>> 3.4e38
3.4e+38
>>> 3.41e38
inf
>>> -3.4e38
-3.4e+38
>>> -3.41e38
-inf
>>> 3.41e38/3.41e38
nan


三. 布林值 (bool) :

布林 (bool) 是 Python 最簡單的資料型態, 其值只有 True 與 False 兩個 (注意首字母需大寫, true 與 false 都不是布林值), 布林 True 與 False 的值 (value) 其實就是整數 1 與 0, 用值的比較運算子  == 去比較會傳回真, 但是 True/False 與 1/0 的物件參考不同, 因此用參考比較運算子 is 去比較時都會傳回 False (因參考不同, 記憶體位址也不同之故), 例如 :

<<< True==1       #True 與 1 的值相同
True
<<< False==0      #False 與 0 的值相同
True
<<< True is 1       #True 與 1 的物件參考不同
False
<<< False is 0      #False 與 0 的物件參考不同
False

利用內建函數 int() 函數可以將布林值轉成 1 與 0, 轉換後值與參考均相同, 例如 :

<<< int(True)           #True 會轉成 1 的 int 物件
1               
<<< int(False)          #False 會轉成 0 的 int 物件
0
<<< int(True)==1     #值相同
True
<<< int(False)==0    #值相同
True
<<< int(True) is 1     #物件參考相同
True
<<< int(False) is 0    #物件參考相同
True
因為 True 與 False 的值為 1 與 0, 因此可以進行四則運算, 雖然看起來沒甚麼意義 :

<<< True + 1
2
<<< False + 1
1
<<< True + 1.1
2.1
<<< False + 1.1
1.1

與布林值相關的運算子有邏輯運算與關係運算, 邏輯運算子有三個 :

 運算子 說明
 x and y 邏輯 AND 運算, 須 x 與 y 均為 True 時才傳回 True, 否則為 False
 x or y 邏輯 OR 運算, 只要 x 或 y 有一個為 True 時就傳回 True
 not x 邏輯 NOT 運算, 若 x 為 True 就傳回 False, 否則傳回 True

例如 :

>>> not True
False
>>> not False
True
>>> True and False
False
>>> True and True
True
>>> False and True
False
>>> False and False
False
>>> True or False
True
>>> True or True
True
>>> False or True
True
>>> False or False
False

Python 沒有提供邏輯互斥或運算子 xor (但有提供互斥或運算子 ^), 不過互斥或運算子可以用 and 與 or 運算來組合, 其真值是只有當兩個運算元相異時才傳回真, 此為互斥名稱由來 :

x xor y=(x or y) and not (x and y)

參考 : 邏輯異或

例如 :

>>> x,y=True,True
>>> (x or y) and not (x and y)
False
>>> x,y=False,False
>>> (x or y) and not (x and y)
False
>>> x,y=True,False
>>> (x or y) and not (x and y)
True
>>> x,y=False,True
>>> (x or y) and not (x and y)
True

Python 內建函數 bool() 可以將其他資料類型轉換成布林型態, 除了空值 (空字串, 0, None) 以外都傳回 True, 例如 :

>>> bool(None)   #無值
False
>>> bool('')           #空字串
False
>>> bool("")         #空字串
False
>>> bool(())          #空元組
False
>>> bool([])          #空串列
False
>>> bool(0)
False
>>> bool(1)
True
>>> bool(2)
True

而關係運算子則有下列六個 :

 運算子 說明
 x > y 大於運算
 x < y 小於運算
 x >= y 大於等於運算
 x <= y 小於等於運算
 x != y 不等於運算
 x == y 等於運算

例如 :

>>> 1==1
True
>>> 1>1
False
>>> 2>1
True
>>> 1<1
False
>>> 1<2
True
>>> 1>=1
True
>>> 1>=2
False
>>> 1<=1
True
>>> 1<=0
False
>>> 1!=2
True
>>> 1!=1
False
>>> 1==1
True
>>> 1==2
False

四. 複數 (compound)  :

支援複數是 Python 的特色, 主要用在科學計算上面. 但對於用在嵌入式設備上的 MicroPython 來說用不到, 所以未支援 :

>>> 1+1j
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: complex values not supported

複數由實部 Real 與虛部 Imagine 組成 : R+ Ij, 其中 j 代表虛部, j 為 -1 的平方根, R 與 I 都是浮點數, 不過虛部若為 1 時不能省略, 否則 j 會被視為未宣告的變數而出現錯誤, 以下是在 CPython 上做的測試 :

C:\Users\Tony>python
Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
<<< 1+1j
(1+1j)
<<< 1+j       #虛部為 1 也必須寫出來
Traceback (most recent call last):
  File "NameError: name 'j' is not defined
<<< 1+j1     #j 必須放在最後面
Traceback (most recent call last):
  File "NameError: name 'j1' is not defined
<<< 1j**2    #j 的平方是 -1
(-1+0j)
<<< (1+j)*(1-j)       #虛部為 1 也必須寫出來
Traceback (most recent call last):
  File "NameError: name 'j' is not defined
<<< (1+1j)*(1-1j)   #複數相乘運算
(2+0j)



2017年4月27日 星期四

買 OPPO A39 手機

菁菁的手機又壞了, 這回是掉到馬桶裡進到水, 烘乾後還是開不了機, 只好買一支新的給她. 這幾天看了好幾個露天賣家, 大部分都標有貨, 實際下標後才又要求取消交易, 說原廠出新款 A57, 這款舊的要停產了, 缺貨.

今天看到這家九如一路近科工館的賣家回應說有貨, 吃過晚飯就載菁菁去買, 哪知到店裡後老闆才說有在臉書回應供貨商調高售價為 6200 元, 哇咧, 比標價高了 700 元, 我說我要考慮, 但往回騎到果菜市場附近又猶豫起來, 因為 6200 還是比公司福利社的 7290 便宜 1090 元 (原先預期便宜 1800), 又折回去請老闆叫貨.

# 樂pad殺手堂(現貨)OPPO A39 5.2吋 雙卡雙待 八核心 3G+32G $5500

回家時經過全家去領 A39 的皮套 (一金一粉共 620 元), 總計花了 6200+620=6820 元. 

2017年4月26日 星期三

MicroPython on ESP8266 (一) : 燒錄韌體

好久沒玩 ESP8266 了, 之前有想要在 ESP-01 模組上灌 NodeMCU 都一直沒時間. 最近在努力學習 Python, 今天突發奇想 : 可以在 ESP8266 上跑 Python 嗎? 我在 Google 輸入 "ESP8266 Python" 進行搜尋, 想看看這兩個我最有興趣的一硬一軟詞彙是否交集, 呵呵, 還真的找到了 MicrPython 這個專案, 這是一個包含 Python 3 標準函式庫的精簡子集實作, 為資源受限的嵌入式設備上提供了一個低階的作業系統環境 (Python OS) 以執行 Python 3 解譯器.

MicroPython 是澳洲的 Damien Geoge 在 Kickstarter 上所發起的一個開源專案 :

參考 :

MicroPython tutorial for ESP8266  (官方教學)
# An Introduction to MicroPython
ESP8266 and MicroPython - Part 1
https://learn.adafruit.com/micropython-basics-load-files-and-run-code
https://learn.adafruit.com/building-and-running-micropython-on-the-esp8266/overview

MicroPython 提供了許多 Python 的高階功能, 例如互動式命令提示介面 (稱為 REPL, Read Evaluate Print Loop), 無限精準度整數, 閉包 (Closure), 串列功能, 以及例外處理等等. 其目標是盡量做到與電腦上完整的CPython 相容, 使得桌上型電腦上的 Python 程式碼可以很容易地移植到微控器等嵌入式設備中執行.  MicroPython 除了實作了 Python 核心函式庫外, 也加入了稱為 machine 的函式庫以存取低階的硬體資源. 參考 MicroPython 的 WiKi 與文件 :

http://wiki.micropython.org/Home
http://docs.micropython.org/en/latest/pyboard/
MicroPython Documentation (v1.8.6 PDF)
MicroPython tutorial for ESP8266  (官方教學)
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
https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)

MicroPython 是用 C99 寫的, 與 CPython 之間的差異參考 :

http://docs.micropython.org/en/latest/pyboard/genrst/index.html

MicroPython 目前可以在 ESP8266 與 Pyboard 等板子上執行, Pyboard 是以 STM32F405RG 微控器為核心的開發板, 處理器架構為 ARM Cortex-M4 32 位元, 具有 1MB 的 FLASH ROM 與 192KB 的 RAM, 內建三軸加速器, 參考官網與 Adafruit 介紹 :




不過 PyBoard 小小一片不便宜, 露天都 1800 元以上 :

# 【樹莓派Raspberry pi 專營店】MicroPython pyboard $2150

露天賣家 Proteus 則有一塊外觀像 Arduino, 但用 ESP-12F 做的開發板, 可直接使用 Arduino IDE 進行開發 :

# ESP8266 Arduino IDE 開發板 $486

而 Adafruit 也有一塊 HUZZAH 開發板, 要價 16.95 美金 (約 508 元台幣), 此板具有 4MB FLASH 記憶體, 9 個 GPIO 腳, 內建 CP2104 USB 轉換晶片 (但無 USB 插槽), 露天也有賣 :

# Adafruit Feather HUZZAH with ESP8266 WiFi 
# 《德源科技》Adafruit HUZZAH ESP8266 Breakout(ada2471) $441
# 《iCshop1》HUZZAH ESP8266 Breakout【80 MHz,3.3V】$450

我比較有興趣的是如何在 ESP8266 上跑 MicroPython, 因為我手邊還有好幾片 ESP8266 ESP-01 模組. 只要用 TTL-UART 模組將 MicroPython 韌體燒錄到 ESP-01 裡, 就可以在 ESP-01 上執行 Python 指令碼了. Adafruit 網站有介紹如何燒錄 MicroPython 韌體到 ESP8266 上, 參考 :


MicroPython 有各板子的論壇可發問求助, ESP8266 板的 MicroPython 論壇位址如下 :

https://forum.micropython.org/viewforum.php?f=16

首先是到 MicroPython 官網下載韌體 :


因為我手邊的是 512KB 藍板的 ESP-01, 所以要下載 512K 的韌體, 當然跟 1MB 的韌體的差別是有些功能被取消了, 這樣才能把韌體大小瘦身下來 :


接下來是將 ESP-01 模組接上 USB-TTL 上傳線, 我使用的是網路上很便宜的 PL2303HX (最便宜約 29 元, 參考 PL2303HX USB轉TTL 線材版), 燒錄前要將 GPIO0 接地才行 (須在 power up 之前, 因 ESP8266 是在電源打開時偵測 GPIO 以便決定是否進入燒錄模式), 燒錄完成後再拔掉接地線 :


參考 : 

但要注意這款上傳線的驅動程式在 Win8 以上電腦要安裝舊版 (2009 年以前的) 才會動, 參考 :

# 安裝 PL2303HX USB 轉 TTL 驅動程式
# 關於 PL2303HX 在 win8 無法驅動的問題
安裝 PL2303HX USB 轉 TTL 驅動程式

燒錄軟體官網介紹的是使用 Python 的 esptools 套件, 安裝指令如下 :

D:\test>pip install esptool
Collecting esptool
  Downloading esptool-1.3.tar.gz
Collecting pyserial>=2.5 (from esptool)
  Using cached pyserial-3.3-py2.py3-none-any.whl
Installing collected packages: pyserial, esptool
  Running setup.py install for esptool ... done
Successfully installed esptool-1.3 pyserial-3.3

但我看了一下它的燒錄指令, 發現似乎是給 MacOS 上用的, Windows 上要怎麼下沒時間去研究, 所以還是用以前燒錄 AT 指令韌體的 ESP8266 Flasher :

# 下載 ESP8266 Flasher (參考 ESP8266: Reflash Dance!)
Esp8266 Firmware Update

這個軟體操作很簡單, 就是選定 COM 埠, 指定要燒錄的 bin 韌體, 按 Download 即可, 燒錄結束時會出現 Failed to leave flash mode 不要理它, 那是 bug :


注意, 如果按 "Download" 鈕都沒反應, 但檢查裝置管理員 COM 埠又是正常, 那麼只要插拔一下 USB 插頭就應該可以抓到串列埠了.

也可以使用另一款 ESP8266Flasher.exe 來燒錄, 此程式可以指定燒錄速度 : 


執行 ESP8266Flasher.exe 後先在 Operation 頁籤指定 COM 埠 :


然後在 Config 頁籤中按齒輪鈕指定 MicroPython 韌體 :


在 Advanced 頁籤中選擇速率 115200, Flash Size 為 512KB :


最後返回 Operation 頁籤按下右邊的 Flash 鈕即開始燒錄 : 


燒錄進度滿格後, 檢查 Log 頁籤, 若左下角出現打勾表示燒錄成功, 否則失敗 :


我原先下載到 1MB 的韌體, 結果都是燒錄到約 89% 就停掉了, 重複試了好幾次都是這樣, 才想到我的 ESP-01 是舊版的 512KB, 用 1MB 的韌體當然燒不進去囉. 同樣地, 若按 "Flash" 鈕都沒反應, 但檢查裝置管理員 COM 埠又是正常, 切到 Log 這邊通常會看到 Serial port not connectted 訊息, 只要插拔一下 USB 插頭就應該可以抓到串列埠了

燒錄完成後關電, 拔掉 GPIO0 的接地線, 先開啟 Putty, 選擇 Serial 串列通訊, 115200 速率與所接的 COM 埠, 開啟連線即進入 Python 執行環境  (Linux 環境則是使用 screen) :


這時再給 ESP-01 送電就可以順利進入 REPL 命令模式了 :


預設是三個大於符號的 REPL 介面 (即 Python 解譯器提示符號). 輸入 help() 會顯示操作說明 :

>>> help()
Welcome to MicroPython!

For online docs please visit http://docs.micropython.org/en/latest/esp8266/ .
For diagnostic information to include in bug reports execute 'import port_diag'.

Basic WiFi configuration:

import network
sta_if = network.WLAN(network.STA_IF); sta_if.active(True)
sta_if.scan()                             # Scan for available access points
sta_if.connect("sta_if.isconnected()                      # Check for successful connection
# Change name/password of ESP8266's AP:
ap_if = network.WLAN(network.AP_IF)
ap_if.config(essid="
Control commands:
  CTRL-A        -- on a blank line, enter raw REPL mode
  CTRL-B        -- on a blank line, enter normal REPL mode
  CTRL-C        -- interrupt a running program
  CTRL-D        -- on a blank line, do a soft reset of the board
  CTRL-E        -- on a blank line, enter paste mode

For further help on a specific object, type help(obj)

按 CTRL+A 進入一個大於符號的原始 REPL 介面, 這是給了解 MicroPython 實作細節較了解的工程師使用的. 按 CTRL+B 則是進入正常的 REPL 介面, 也就是 Python 解譯器介面. 按 CTRL+C 可中斷執行中的程式, 按 CTRL+D 會使板子重置 (reset), CTRL+E 進入張貼模式. 


輸入 print("Hello Worl") 可正常執行; 但 print "Hello World" 卻錯誤, 可見確實是 Python3 環境. 此 REPL 介面會儲存前 6 次指令, 可按向上鍵叫回之前所下的指令修改為新指令, 這對測試程式功能相當方便.

我用一個 USB 電壓電流計量取 5V-3.3V 電源板所吃電流大約是 40mA 左右, 這包括了電源板本身的消耗, 所以估計 ESP8266 大約消耗 30mA 左右.

參考 :

【一起玩esp8266】简单调试


2017-05-16 補充 :

上週購買的五顆 ESP-01 1MB Flash 板收到後一直到昨晚才有時間測試, 但忘了將 USB-TTL 轉接線接地與將 CHPD 接 VCC, 搞了十分鐘才順利將 1MB 韌體燒錄完畢. 如果 CHPD 沒接 HIGH, 則 LOG 會一直顯示 Finding ESP8266, 如果有顯示 MAC 就表示已順利找到 ESP8266, 才能順利燒錄.





完成後開機顯示如下訊息, 跟 512MB 版確實不同 :

b▒#4 ets_task(40100164, 3, 3fff8398, 4)
could not open file 'boot.py' for reading
could not open file 'main.py' for reading

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>>

參考下列文章, 以 import port_diag 指令可檢視板子的設定情形 :

https://github.com/micropython/micropython/issues/2255

>>> import port_diag
FlashROM:
Flash ID: 1440c8 (Vendor: c8 Device: 4014)
Flash bootloader data:
Byte @2: 02
Byte @3: 20 (Flash size: 1MB Flash freq: 40MHZ)
Firmware checksum:
size: 586684
md5: 2c7ce68f0e1a5690257698e5e42caf16
True

Networking:
STA ifconfig: ('0.0.0.0', '0.0.0.0', '0.0.0.0', '208.67.222.222')
AP ifconfig: ('192.168.4.1', '255.255.255.0', '192.168.4.1', '208.67.222.222')
Free WiFi driver buffers of type:
0: 8 (1,2 TX)
1: 0 (4 Mngmt TX(len: 0x41-0x100))
2: 8 (5 Mngmt TX (len: 0-0x40))
3: 4 (7)
4: 7 (8 RX)
lwIP PCBs:
Active PCB states:
Listen PCB states:
TIME-WAIT PCB states:

另外, 可用 esp 函式庫檢查韌體是否有問題 :

>>> import esp
>>> esp.check_fw()
size: 586684
md5: 2c7ce68f0e1a5690257698e5e42caf16
True

最後顯示 True 表示韌體正常, 毋須重燒.  參考 :


2017-05-19 補充 :

除了下載現成的韌體燒錄到 ESP8266 板上外, 還可以自行從原始碼編譯韌體, 好處是可以挑選自己想要的功能或擴充現成韌體沒有包含的功能, 參考 :


另外, MicroPython on ESP8266 目前也還未支援 SD 卡 :

"MicroPython on the ESP8266 does not currently support running Python code off a SD card or other file system like other more mature MicroPython boards!"

參考 :


Adafruit 有賣一款 SD 卡擴充板, 問題是 ESP-01 能用嗎? 

# micropython-upip 1.2 (安裝擴充套件)

2017-06-18 補充 :

官網建議用 1M Bytes 的 ESP8266 模組 :


"The minimum requirement for flash size is 1Mbyte. There is also a special build for boards with 512KB, but it is highly limited comparing to the normal build: there is no support for filesystem, and thus features which depend on it won’t work (WebREPL, upip, etc.). As such, 512KB build will be more interesting for users who build from source and fine-tune parameters for their particular application."

2017-06-26 補充 :

今天在 "MemoryError: memory allocation failed, allocating" 看到 os.uname() 指令, 它會傳回  6 元素 tuple 顯示系統資訊如韌體版本 : 

>>> import os
>>> os.uname()
(sysname='esp8266', nodename='esp8266', release='2.0.0(5a875ba)', version='v1.9.1-8-g7213e78d on 2017-06-12', machine='ESP module with ESP8266')

2017-07-05 補充 :

今天找到這個 Python 語言學習網站, 值得參考 :


2017-09-09 補充 :

今天看到下面這個網站, 才知道 MicroPython 部分資助來自歐洲太空總署, 而且只有一位專職核心開發者, 兩名業餘貢獻者, 雖然還是屬於開發中的技術, 但持續成熟中 ! 


2017年4月25日 星期二

使用 Python 控制 Arduino

本周從市圖借到這本 "Python x Arduino 物聯網整合開發實戰 (碁峰, 曾吉弘譯)",  此書翻譯自 Packt 出版的 "Python Programming for Arduino (Pratik Desai)", 主要是利用 Firmata 通訊協定讓電腦 (Windows, Linux, MacOS) 透過序列埠控制 Arduino, 使用 Python 豐富的函式庫 (視覺化, GUI, 遠端存取等) 實作 Arduino 的物聯網應用, 熟悉 Python 語言的人可駕輕就熟進入 IoT 領域.

Source : CAVEDU

不過這本書所介紹的技術不是要取代 Arduino C 原生程式碼的功能 (也不能), 而只是將 Firmata 協定所寫的韌體燒入 Arduino 當中間人, 讓電腦中的 Python 程式能透過 USB 串列埠與 Arduino 溝通而已. 這種模式下 Arduino 並非獨立運作, 而是接受電腦的 Python 程式控制. 獨立運作的 Arduino 還是必須使用 C 語言編寫原生碼 (韌體), 而非 Firmata.

以下即依照書中描述, 實際測試 Firmata 的功能. Arduino IDE 已經內建了各種應用的 Firmata 韌體, 放置在 "檔案/範例/Firmata" 裡, 測試 Firmata 功能的最簡單方式是開啟 "檔案/範例/Firmata/StandardFirmata" 這個範例程式 :


開啟後不要做任何修改, 直接上傳 Arduino 即可, 編譯後記憶體占用情形如下 (1.8.1 版) :

草稿碼使用了 11134 bytes (36%) 的程式儲存空間。上限為 30720 bytes。
全域變數使用了 1029 bytes (50%) 的動態記憶體,剩餘 1019 bytes 給區域變數。上限為 2048 bytes 。

韌體上傳 Arduino 後, 接下來就是要在電腦端執行 Firmata 測試程式, 連線 Firmata 官網下載 Windows 版的 firmata_test.exe :

# http://www.firmata.org


執行 firmata_test.exe, 點選上方的 Port 選單, 選擇 Arduino 所接的 USB COM 埠 (參考 Arduino Nano 開機測試) :


選定後就會出現 Arduino 每一個 Port 的目前狀態, 其中 6 個 Analog 因為接腳浮接的關係, 其值是隨機的亂數, 因此數值不斷閃爍. 按下 D13 的按鈕切換至 HIGH 狀態時, Arduino 板子上內建的 D13 LED 會亮起來, 切回 LOW 就熄滅, 這就是 Firmata 的主要功能, 即不需要撰寫 Arduino 草稿碼也可以控制 Arduino :


上面的 firmata_test.exe 是現成的電腦主機端軟體, 透過 USB 串列埠與 Arduino 裡的 StandardFirmata 草稿碼互動. 如果要用 Python 自行撰寫程式與 Arduino 程式碼互動, 必須在電腦中安裝 Python 的 pySerial 套件 :

C:\Users\Tony>pip3 install pyserial
Collecting pyserial
  Downloading pyserial-3.3-py2.py3-none-any.whl (189kB)
Installing collected packages: pyserial
Successfully installed pyserial-3.3

如果是離線安裝, 先下載 pyserial 的 whl 或 gz 檔 :

https://pypi.python.org/pypi/pyserial#downloads

C:\Users\Tony>pip3 install d:\python\pyserial-3.3-py2.py3-none-any.whl
Processing d:\python\pyserial-3.3-py2.py3-none-any.whl
Installing collected packages: pyserial
Successfully installed pyserial-3.3

關於 PySerial 函式庫的 API 用法參考 :

http://pyserial.readthedocs.io/en/latest/pyserial_api.html

接下來到 Arduino IDE 開啟 "檔案/範例/01.Basics/DigitReadSerial" 草稿碼 :


其內容如下 :

// digital pin 2 has a pushbutton attached to it. Give it a name:
int pushButton = 2;

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);
  // make the pushbutton's pin an input:
  pinMode(pushButton, INPUT);
}

// the loop routine runs over and over again forever:
void loop() {
  // read the input pin:
  int buttonState = digitalRead(pushButton);
  // print out the state of the button:
  Serial.println(buttonState);
  delay(1);        // delay in between reads for stability
}

程式只是從 Pin2 接腳讀取按鈕輸入, 然後送到序列埠去, 此程式不用修改直接上傳 Arduino 即可, 我們接下來要在電腦這端使用 Python 程式透過 PySerial 套件讀取 Arduino 送到串列埠的數據. 在 IDLE 輸入如下指令 :

Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import serial
>>> s=serial.Serial("com3",9600)
>>> while True:
        print(s.readline())

此程式利用 serial.Serial() 函數建立 com3 (或 COM3, 大小寫無關) 的序列埠物件, 再以此物件的 readline() 讀取連接至 COM3 的 Arduino 序列埠數據. 這時畫面會不斷顯示從序列埠讀取到 0, 這是因為 Arduino 的 Pin2 並未真正接上按鈕之故.


上面是我照書本上的範例所做的測試, 從 Arduiono 輸出數據 (0) 到 USB 串列埠, 再由 Python 程式透過 PySerial 去讀取. 接下來我想反方向測試, 由 Python 程式經序列埠傳送資料給 Arduino. Python 程式如下 :

import serial
s=serial.Serial("com3",9600)
from time import sleep
while True:
s.write('H'.encode())
sleep(1)
s.write('L'.encode())
sleep(1)
此程式從 time 套件匯入 sleep() 函數用來暫停程式執行 (即休眠), 其參數單位為秒, 程式會在無限迴圈中持續對序列埠送出 'H' 與 'L', 間隔一秒, 注意, 因為 Python 3 字串都以 Unicode 表示, 因此在輸出到序列埠時必須呼叫 encode() 將其轉成 byte 編碼. 參考 :

# python3 pySerial TypeError: unicode strings are not supported, please encode to bytes

而 Arduino 端程式則是持續讀取序列埠, 若收到 'H' 就點亮 D13 LED; 否則就熄滅它 :

int LED=13;
void setup() {
  Serial.begin(9600);
  }

void loop() {
  if (Serial.available() > 0) {
    if (Serial.read()=='H') {digitalWrite(LED, HIGH);}
    else {digitalWrite(LED, LOW);}
    }
  delay(1);    
  }

將此 Arduino 草稿碼上傳後, 於 PC 端執行 Python 程式, Arduino 板上的 D13 LED 將亮一秒, 滅一秒反覆交替. 從這個範例可以看出 PySerial 的角色是在 Arduino 與 PC 間透過 USB 串列埠搭起雙向溝通的橋樑. 不過 PySerial 功能較為樸素, 不具有 Firmata 功能, 因此除了 PC 端要寫 Python 程式外, 對於不同的應用, Arduino 端也必須配合不斷改寫上傳, 殊為不便.

有沒有兼具 Firmata 與 PySerial 功能的函式庫, 可以讓 Arduino 端指上傳韌體一次即可, 我們只要專注在 PC 端即可呢? 有的, 那就是 PyFirmata 函式庫, 此函式庫是在 PySerial 基礎上加入 Firmata 功能, 可以用 pip3 install pyfirmata 指令安裝此套件 :

C:\Users\Tony>pip3 install pyfirmata
Collecting pyfirmata
  Downloading pyFirmata-1.0.3-py2.py3-none-any.whl
Requirement already satisfied: pyserial in c:\python36\lib\site-packages (from pyfirmata)
Installing collected packages: pyfirmata
Successfully installed pyfirmata-1.0.3

若需離線安裝, 可先下載 pyfirmata 的 whl 或 gz 檔 :

https://pypi.python.org/pypi/pyFirmata

C:\Users\Tony>pip3 install d:\python\pyFirmata-1.0.3-py2.py3-none-any.whl
Processing d:\python\pyfirmata-1.0.3-py2.py3-none-any.whl
Requirement already satisfied: pyserial in c:\python36\lib\site-packages (from p
yFirmata==1.0.3)
Installing collected packages: pyFirmata
Successfully installed pyFirmata-1.0.3

關於 PyFirmata API 使用說明參考 :

http://pyfirmata.readthedocs.io/en/latest/

欲測試 PyFirmata 功能, 需從 Arduino IDE 的範例中再次上傳 StandardFirmata 草稿碼, 然後就可以在 PC 端用 Python 程式透過序列埠操控 Arduino 了, 程式如下 :

import pyfirmata
from time import sleep
LED=13
PORT="COM3"
board=pyfirmata.Arduino(PORT)
while True:
board.digital[LED].write(1)
sleep(1)
board.digital[LED].write(0)
sleep(1)

這裡要先匯入 pyfirmata 函式庫, 然後呼叫 Arduino() 函數來指定兩方溝通的序列埠名稱, 傳回Arduino 板子物件, 此物件的 digital 串列物件可用來指定 Arduino 腳位, write() 函數可用來輸出準位. 執行後同樣可看到 Arduino 板上的 D13 LED 間隔一秒明滅. 與使用 PySerial 不同的是, 如果要更改應用程式功能, 不需要 (也不可以) 動 Arduino 端韌體 (不須再上傳草稿碼), 全部在電腦端用 Python 程式就可以搞定了.

哇! 這番測試下來發現 Python 這語言真是好用, 甚麼樣功能的函式庫都有高手寫好了, 就怕我們不知道而已. 我在想, 以前 Arduino 要跟電腦互動大都使用 Processing 程式, 有了 PyFirmata 後是否可以用 Python 完全取代 Processing 呢?

參考 :

Arduino:在Windows裡使用Python語言經由Firmata協定控制Arduino開發板
# ARDUINO筆記(十三):使用FIRMATA 協定,ARDUINO 也可以執行 PYTHON