於 Espressif ESP32-S2 Saola 1 開發板燒錄並使用 CircuitPython 6.0.0

Alan Wang
20 min readSep 4, 2020
Photo by Hitesh Choudhary on Unsplash

2019 年 7 月,安可信(Espressif)發表了 ESP32-S2,是 ESP8266 及 ESP32 家族的下一個產品。將對於 ESP32 的雙核心處理器,它使用單核心 Xtensa 32-bit LX7(240 MHz),記憶體稍少並拿掉了藍芽功能,因此一般認為它是要當成 ESP8266 的替代或升級品。不過,腳位數量倒以 43 個壓倒 ESP32 的 34 個,且深度睡眠的耗電量更低。(詳細規格可參考這裡。)

一直到 2020 年上半,才開始有些廠商做出一些 ESP32-S2 開發板出來,但儘管如此,相關的開發環境支援仍幾乎不存在。唯一的例外是 Adafruit 的 CircuitPython — — 而且很有趣,CircuitPython 開發社群非常積極的在此語言的 6.0.0 正式版納入對 ESP32-S2 的完整支援,這跟其他語言社群的態度真是天差地遠。

CircuitPython 很早就放棄了對 ESP8266 及 ESP32 的支援,理由是這些板子沒有內建 USB 介面,但 ESP32-S2 卻有 USB OTG,因此可以用 UF2 bootloader 讓使用者直接存取板子的儲存空間和寫入程式、以及用來模擬滑鼠與鍵盤。 我想這群開發者也很清楚,他們能藉此使 CircuitPython 獲得更多注意吧。MicroPython 那邊還沒有想支援的意思,至於 Arduino IDE 則仍只有非官方的支援,據說會與 ESP32 的衝突。

下面我們就來看看,如何在 ESP32-S2 燒錄目前在 beta 版階段的 CircuirPython 6.0.0。

本篇安裝部分的參考來源為『在 ESP32-S2 上试用 circuitpython』及『ESP32-S2 运行 circuitpython』。此外,這篇可能會隨著新版本的推出而繼續更新。

Photo by James Fitzgerald on Unsplash

準備 USB 母座轉接板

這裡我們用的板子是安可信做的 Saola 1,處理器為 ESP32-S2-WROVER,我是透過貿澤電子的網站訂的。這板子仍然感覺比較像個原型產品,有點太大(在 400 孔麵包板上只能留下一邊腳位的接線空間,和某些 ESP32 一樣)。板子上沒有內建普通 LED 燈,反而是在腳位 18 接了一顆 NeoPixel(WS2812)RGB LED,並用該燈的顏色來反映處理器的某些狀況。這點可說非常有 Adafruit 家產品的風格。

目前的兩種處理器,ESP32-S2-WROVER 和 ESP32-S2-WROOM 的主要差異似乎在前者有 2MB PSRAM,而後者沒有。

此外,雖說 ESP32-S2 有原生 USB 介面,但這板子並沒有將之接出來。反而,他們仍透過一個 CP2102 接出一個傳統的序列埠 USB 接口。為什麼呢?國外有人問了,並得到非常老實的答案:

翻譯:「由於這是我們第一次在 ESP32-S2 使用 USB 功能,我們仍在試著改良相關工具與驅動程式。因此,我們的第一代 ESP32-S2 開發板仍使用序列埠橋接晶片。等到不同平台的相關軟體與驅動程式支援完成後,USB 介面就會慢慢開放,不過透過序列埠(燒錄/除錯)仍是可使用的選項。」

因此,你需要一個 USB 母座轉接板(在電子材料行有售):

在電子材料行有,並有幾種不同的接頭可選。

板子和開發板的腳位接法如下:

  • VBUS → 5V
  • GPIO20 → D+
  • GPIO19 → D-
  • GND → GND
Photo by Lucian Alexe on Unsplash

燒錄 CircuitPython 韌體

到官網下載最新的 CircuitPython 6.0.0 韌體:(https://circuitpython.org/board/espressif_saola_1_wrover/

補充:大概從 CircuitPython 7.0 版開始,官網對某些板子提供了網頁版安裝工具,所以不需要像下面那樣用 esptool 了。步驟如下:

1. 用 USB 線連上 ESP32S2 自身的 USB 埠(注意你可能得先裝好 CP2102 或 CH340G 晶片驅動程式)

2. 點選 OPEN INSTALLER 然後選 Full CircuitPython x.x.x Install

3. 點 Next 然後點 Connect,在 WebUSB 畫面中選擇板子連線

理論上這會先灌入 bootloader,然後提示你切到 OTG USB 埠來安裝 CircuitPython。當然筆者在測試時從來沒辦法一次到位升級,所以我都會先選 Install Bootloader Only,然後手動下載 CircuitPython 的 .uf2 檔,把它拖拉到 OTG USB 埠跳出的磁碟空間,這樣就能安裝或升級了。

為了燒錄這個 .bin 檔,我們得使用 esptool 3.0。在命令提示字元或終端機輸入以下指令來下載最新的 esptool(請先安裝 Git):

git clone https://github.com/espressif/esptool.git

然後用 pip3 安裝之(你可先安裝 Python 3.x 就會有 pip3):

pip3 install -e esptool

安裝完成後,連接 ESP32-S2 的序列埠 USB 頭(原本的 USB 接頭;記得先安裝 CP2102 驅動程式),到裝置管理員查一下它的 COM 號碼(或者在命令提示字元打 mode),然後你就能用 esptool 來燒錄韌體:

如果你已經燒錄過舊版,先抹除舊韌體

esptool.py --chip auto --port COMxx erase_flash

然後

esptool.py --chip auto --port COMxx -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 40m --flash_size 4MB 0x0000 C:\路徑\adafruit-circuitpython-espressif_saola_1_wrover-en_US-6.0.0-beta.0.bin

將 port 改成正確的 COM 號碼,並將檔名改成你下載的名稱。

你會看到

esptool.py v3.0-dev
Serial port COMxx
Connecting......
Detecting chip type... ESP32-S2
Chip is ESP32-S2
Features: WiFi, ADC and temperature sensor calibration in BLK2 of efuse
Crystal is 40MHz
MAC: 7c:df:a1:01:30:fe
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Compressed 561760 bytes to 328155...
Wrote 561760 bytes (328155 compressed) at 0x00000000 in 8.1 seconds (effective 554.6 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...

這樣就燒錄完成了。

若在 Linux 上,可用 dmesg |grep tty 來查裝置的 port,可能是 /dev/ttyACM0 或 /dev/ttyUSB0。

Photo by Scott Graham on Unsplash

使用 CircuitPython

如同我在之前另一篇文章講過的,燒錄有 CircuitPython 韌體的裝置,只要接上電腦就會出現其 USB 碟空間:(這回你得接 USB 母座轉接板)

「USB 碟」大小是 2 MB,應該是來自那個 2 MB PSRAM。這表示將來有很多空間可以放驅動程式跟多媒體檔案。

打開 Mu editor,到 Serial 模式看看它的 REPL:(先按 Ctrl + C 來中斷,然後按 Enter 進入 REPL。)

出於某種原因,Thonny 編輯器的 CircuitPython 模式就無法順利跟這塊板子或這版本的韌體溝通。

Adafruit CircuitPython 6.0.0-beta.0 on 2020-09-21; Saola 1 w/Wrover with ESP32S2
>>> help('modules')
__main__ errno neopixel_write struct
_os espidf os supervisor
_pixelbuf fontio pulseio sys
_time framebufferio pwmio terminalio
array gamepad random time
bitbangio gc re touchio
board io rtc ulab
builtins ipaddress sdcardio usb_hid
busio json sharpdisplay vectorio
collections math socketpool wifi
digitalio microcontroller ssl
displayio micropython storage

來輸入一點簡單的東西試試看(讀取處理器溫度):

>>> import microcontroller
>>> microcontroller.cpu.temperature
35.6208
Photo by Daniel on Unsplash

驅動 NeoPixel LED

當然,CircuitPython 內建的 neopixel_write 是個相當原始的功能。我們事實上可以使用 CircuitPython 提供的 NeoPixel 驅動程式庫。

到以下網址 https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases 下載 adafruit-circuitpython-bundle-6.x-mpy-xxxxxxxx.zip,解壓後在目錄最底下找到 neopixel.mpy

.mpy 是編譯成二進位碼的 .py 檔。不過你想要的話,上面的網址也能下載 .py 的版本。

在板子的 USB 空間下建立一個 lib 資料夾,將 neopixel.mpy(或者 neopixel.py)複製貼上:

這樣驅動程式就「安裝」好了。夠簡單吧?

撰寫程式

現在,在 Mu 編輯器開一個新檔案,貼上以下程式:(這修改自 Adafruit 的官方範例

import board, time, neopixelpixel_pin = board.IO18
num_pixels = 1
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.3, auto_write=False) # brightness 為亮度 (0~1)def wheel(pos):
if pos < 0 or pos > 255:
return (0, 0, 0)
if pos < 85:
return (255 - pos * 3, pos * 3, 0)
if pos < 170:
pos -= 85
return (0, 255 - pos * 3, pos * 3)
pos -= 170
return (pos * 3, 0, 255 - pos * 3)
def rainbow_cycle(wait):
for j in range(255):
for i in range(num_pixels):
rc_index = (i * 256 // num_pixels) + j
pixels[i] = wheel(rc_index & 255)
pixels.show()
time.sleep(wait)
while True:
rainbow_cycle(0)

以 code.py 的檔名儲存在 ESP32-S2 的 USB 碟下,一儲存就應該能看到板子自身的 NeoPixel 開始亮起七彩燈光。

要是你手邊有外接的 NeoPixel 燈條,也可以接上去玩玩看。下面我就接了個模組在 GPIO21(程式內 pixel_pin 要改成 board.IO21,num_pixels 改成 12 或你的燈條對應數量):

有點奇怪的是板子本身的燈也會跟著亮,應該是 bug 吧。
Photo by Greg Nunes on Unsplash

連接 WiFi 並查詢 API(6.0.0 beta 0 更新)

好,前面的篇幅是 alpha 版時寫的,現在 beta 版出來了,來玩一下它的 WiFi 支援吧,用 World Time API 這個網路時間查詢服務來當示範,順便整合一下相關功能。

注意下面的 adafruit_requests 模組也要去 Adafruit 的驅動程式庫找,把 adafruit_requests.mpy 或 adafruit_requests.py 複製到板子上。

SSID = '' # 你的 WiFi 名稱
PW = '' # 你的 WiFi 密碼
URL = 'http://worldtimeapi.org/api/ip' # http://worldtimeapi.org/
import wifi, socketpool, ssl, adafruit_requests
import board, neopixel, rtc, time
weekday = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')colors = {'off': (0, 0, 0),
'white': (255, 255, 255),
'red': (255, 0, 0),
'yellow': (255, 255, 0),
'green': (0, 255, 0),
'cyan': (0, 255, 255),
'blue': (0, 0, 255),
'purple': (255, 0, 255)}
# 用板子的 NeoPixel 當狀態指示燈
pixel = neopixel.NeoPixel(board.IO18, 1, brightness=0.1, auto_write=False)
def setPixel(color):
pixel.fill(colors.get(color, 'off'))
pixel.show()
print('Scanning WiFi...')
setPixel('white')
# 掃描附近的網路 (目前要先這麼做才連得上 WiFi)
for network in wifi.radio.start_scanning_networks():
print(f'[{network.ssid}] channel: {network.channel}, rssi: {network.rssi}')
wifi.radio.stop_scanning_networks()
print('WiFi scanning completed.\n')
connected = False
time_updated = False
try:

if not SSID == '':
print('Connecting to', SSID, '...')
setPixel('yellow')

wifi.radio.connect(SSID, PW) # 連上 WiFi
connected = True
print('Connected.\n')
setPixel('green')

else:
print('SSID not set.\n')
setPixel('red')
except ConnectionError as e: # 信號太弱或有其他原因可能會導致連線失敗
print('Failed to connect:', e, '\n')
setPixel('red')
time.sleep(1)
r = rtc.RTC() # 建立 RTC (real-time clock) 物件
if connected: # 有連上 WiFi 就查詢 API pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
print('Querying time...')
setPixel('blue')
response = requests.get(URL) # 查詢時間 APIif response.status_code == 200: # 查詢成功, 傳回 200 OK
data = response.json()
unixtime = data['unixtime'] + data['raw_offset']

# 用 Unix time 產生 struct_time 物件來更新板子 RTC 時間
r.datetime = time.localtime(unixtime)
time_updated = True
print('RTC time updated.\n')
setPixel('green')

else:
print('Failed to query time.\n')
setPixel('purple')
time.sleep(1)while time_updated: # 讀取板子 RTC 時間
dt = r.datetime
y, mn, d = dt.tm_year, dt.tm_mon, dt.tm_mday # 年月日
h, mi, s = dt.tm_hour, dt.tm_min, dt.tm_sec # 時分秒
w = weekday[dt.tm_wday] # 星期

# 格式化輸出時間日期
print(f'{y:04d}/{mn:02d}/{d:02d} {w} {h:02d}:{mi:02d}:{s:02d}')

setPixel('cyan')
time.sleep(0.1)

setPixel('off')
time.sleep(0.9)
while not time_updated: # 如果前面更新 RTC 失敗, 就停在這裡
pass
Photo by Morning Brew on Unsplash
Scanning WiFi...
[AlanKrantas] channel: 6, rssi: -61
[FLAG] channel: 6, rssi: -68
[360WiFi-Plus-3F7B57] channel: 6, rssi: -81
[AndroidAP65A3] channel: 6, rssi: -88
[FLAG-N007-B] channel: 1, rssi: -87
[MicroPython-659c3f] channel: 1, rssi: -87
[FLAG-N007-99] channel: 11, rssi: -84
[FLAG-N007-60] channel: 11, rssi: -90
[BUFFALO-E0E204-1] channel: 11, rssi: -91
[FlagPub] channel: 11, rssi: -92
[TP-LINK_7BC2A0] channel: 3, rssi: -72
[FLAGDEMO] channel: 9, rssi: -85
[FLAG_SCHOOL_DEMO] channel: 9, rssi: -85
[ChatBot] channel: 9, rssi: -87
[vYou_DDPai_Mini2S] channel: 4, rssi: -84
[CHT_I040GW1] channel: 8, rssi: -85
[MicroPython-2050b5] channel: 5, rssi: -71
WiFi scanning completed.
Connecting to AlanKrantas ...
Connected.
Querying time...
RTC time updated.
2020/09/23 Wed 11:15:46
2020/09/23 Wed 11:15:47
2020/09/23 Wed 11:15:48
2020/09/23 Wed 11:15:49
2020/09/23 Wed 11:15:50
2020/09/23 Wed 11:15:51
2020/09/23 Wed 11:15:52

這個範例比較複雜一點,但最後已經能取得時間日期的詳細資料。只要接上外部螢幕模組跟使用合適的驅動程式,就能當成時鐘用了。當然測試發現 WiFi 信號不夠強的話,似乎比較容易連線失敗。

CircuitPython 的網路功能 API 比 MicroPython 的複雜一點,但用起來感覺很像。順帶一提,CircuitPython 居然有支援 Python 3.6 的 f-string 格式化字串,但核心同樣基於 Python 3.4 的 MicroPython 就還是只能用 str.format() 了。

至於要怎麼讀上面這些回應呢?在 Mu editor 按 Serial 鈕進入 REPL 畫面後,按 Ctrl+D 來強迫板子重啟(或者你有儲存程式的動作也可以)。有時你可能得先關掉 REPL 視窗和重接一次板子。只要不進入真正的 REPL 模式,你應該就能在該視窗看到 print() 的輸出結果了。

後記

筆者在寫上一篇關於 CircuitPython 的文時,就提到一些對岸新開發板開始支援 CircuitPython 的趨勢。現在 ESP32-S2 也加入了這場賽局中,因此一陣子後這說不定會成為亞洲地區 Stem 教育使用的新語言。

此外,已經有些人開始在做改良的 ESP32-S2 產品。隨便抓一些來展示:

擁有兩個 USB 口(不必再接轉接板)、改用 CH340G 的 nanoESP32-s2
附有 ST7735 螢幕及開機模式切換開關的 MORPHESP 240

值得一提,安可信近日也公布了兩款 ESP32-S2-MINI 處理器,預計 2020 年 12 月量產,所以明年應該可以看到小型化的 ESP32-S2 開發板吧。

尺寸分別是 15.4 x 20 mm(左)及 15.4 x 15.4 mm(右)
甚至,安可信第三季還會發表 ESP32-S3(240 MHz 雙核處理器,據說效能比 ESP32 更好) 及規格未知的 ESP32-C3。感覺很快的又要掀起另一波微控制器大戰了。
Photo by Yolanda Sun on Unsplash

--

--

Alan Wang

Technical writer, former translator and IT editor.