LEGO 76112 Batman Part III - 自製蝙蝠車搖桿3
終於要把這個系列的貼文做個結束了, 因為工作忙, 加上有一些技術問題要解決, 所以從今年初抽空慢慢做, 到最近才完工。簡單的說, LEGO 76112 Batmobile這台車是由手機透過BLE來控制的, 但是媽媽不給小孩碰手機! 我只好想辦法自己做一個搖桿來搖控這台車。
這系列貼文的前兩篇:
1. 自製蝙蝠車搖桿1
2. 自製蝙蝠車搖桿2
經過考慮, 決定只用一個BLE晶片來做和batmobile溝通的工作, 這是最直接省錢的。當然另外還要有一個搖桿, 本來想用兩個搖桿的, 因為這台車是有兩個馬逹, 官方手機APP的操作界面也是設計兩個按鈕來控制這兩個馬逹。但後來, 硬體的工作弄得有點累, 就先偷懶一下了。
圖1, 可見一個BLE晶片的板子, 左上角有Nordic字樣的藍色板子, 這是nRF52840 Dongle。右邊則是搖桿, 基本上和PS2搖桿或XBOX搖桿上所採用的搖桿零件是一模一樣的。不信, 自己拆解看看, 也可以看iFix it的網站資料, 真的是一樣的零件哦! 這個搖桿在電子賣埸都找得到, 有不同價位, 便宜的台幣25元, 貴的100+, 200+, 我用起來, 發覺都一樣! 建議可以買最便宜的就好! 至於最大塊的綠色洞洞板, 只是用來當骨架, 方便固定零件。因為可能日後還要把零件拿下來, 所以都只是用電線假固定。
圖2, 側面可以看到nRF52840 Dongle, 焊接了3個藍色的排針和3個黃色的排針, 從上面往下:
圖3, 將CR電池座用雙面膠固定; 這樣的組裝, 我試了, 剛好適合右手單手握持。接線, 沒有幾條, 電池, 搖桿, nRF52840 Dongle的電源接在一起(VCC接VCC, GND接GND); 然後就只有搖桿的兩軸類比輸出接到nRF52840 Dongle的類比輸入, 就這樣沒有了。
操作方式, 目前的設定是, 搖桿往前推batmobile就前進(2個馬逹同速往前轉), 搖桿往後推就後退(2個馬逹同速後後轉), 搖桿往左推, 那就左轉(右馬逹往前轉), 搖桿往右推就右轉(左馬逹往前轉)。當然, 推到頂的時候馬逹轉速最快, 推到一半, 馬逹的轉速就只有一半。
在組裝之前, 要先把程式燒錄到nRF52840 Dongle, 我使用Micropython 1.12來開發這個小程式。所以要先將Micropython的環境燒錄上去。做法在之前的貼文有講:
micropython-112nrf52840-dongle
然後就可以寫自己的程式了, 寫好的程式可以儲存在BLE晶片的Flash裡面。因為我們的程式是持續的檢查搖桿, 一有變動就要送出控制訊號。為了之後容易維護程式, 不要一通電就去執行我們的程式, 否則怕會無法把程式停下來, 會影響程式修改的工作。因此, 在通電開機時, 先檢查按鈕SW1的狀態, 如果是按下的, 才去執行我們的程式; 如果沒有按下, 就只去點亮LED1。這樣, 想要維護程式時就不要按SW1, 只去重開機就可以。要玩的時候, 再按著SW1, 重開機就行了。這個行為可以用以下boot.py來實現:
--- boot.py ----
from board import LED
from machine import Pin
import my
sw1 = Pin('P38', Pin.IN, Pin.PULL_UP)
if sw1() == 1:
LED(1).on()
else:
# sw1 was pressed
LED(2).on()
my.run('batman.py')
其中的my.py可以在我之前的貼文裡找到: micropython-on-nucleo-f401re-4-mypy
只是對nrf平台的micropython來說,os物件的實作有點不同,所以要小改一下,但這裡用不到,大家就不要用my.ls()就是。順便提一下這裡的SW1, 如下圖是對應到GPIO P1.06, 那在micropython的程式裡要如何寫才能存取到它呢?就如上文的boot.py程式裡的方法,使用Pin類別,在建構式的第一個傳入參數,用"P38"即可存取到P1.06 (32 + 6 = 38)。此外,52840 User Guide p.13提到:
我們的程式, 則由micropython網站的example code修改一下, 就可以了:
--- batman.py ----
import time
from board import LED
from machine import RTCounter
from machine import ADC, Pin
from ubluepy import Peripheral, Scanner, constants
def leds_off():
for i in range(4):
LED(i+1).off()
def bytes_to_str(bytes):
string = ""
for b in bytes:
string += chr(b)
return string
def get_device_names(scan_entries):
dev_names = []
for e in scan_entries:
scan = e.getScanData()
if scan:
for s in scan:
if s[0] == constants.ad_types.AD_TYPE_COMPLETE_LOCAL_NAME:
dev_names.append((e, bytes_to_str(s[2])))
return dev_names
def find_device_by_name(name):
s = Scanner()
scan_res = s.scan(1000)
device_names = get_device_names(scan_res)
for dev in device_names:
if name == dev[1]:
return dev[0]
# led2 on
leds_off()
LED(2).on()
# scan smart hub
dev = None
while not dev:
dev = find_device_by_name("Smart Hub")
time.sleep_ms(100)
# led 4 on
leds_off()
LED(4).on()
# connect smart hub
p = Peripheral()
p.connect(dev.addr())
s = p.getServices()
s1 = s[2]
char_1 = s1.getCharacteristics()[0]
# prepare adc
adc = ADC(Pin(2))
mid = adc.value()
adc_turn = ADC(Pin(29))
mid_turn = adc_turn.value()
l_code = bytearray([0x08,0x00,0x81,0x01,0x11,0x51,0x00,0xda])
r_code = bytearray([0x08,0x00,0x81,0x00,0x11,0x51,0x00,0xda])
old_j_value = 0
old_j_turn_value = 0
def check_adc(timer_id):
global old_j_value
global old_j_turn_value
global adc
global mid
global adc_turn
global mid_turn
global l_code
global r_code
global char_1
j_value = int((adc.value() - mid)/120*100)
if abs(j_value) < 10:
j_value = 0
j_turn_value = int((adc_turn.value() - mid_turn)/120*100)
if abs(j_turn_value) < 50:
j_turn_value = 0
if j_turn_value != old_j_turn_value:
if j_turn_value > 0:
r_code[7] = j_turn_value
char_1.write(r_code)
elif j_turn_value < 0:
l_code[7] = j_turn_value
char_1.write(l_code)
else:
r_code[7] = j_turn_value
char_1.write(r_code)
l_code[7] = j_turn_value
char_1.write(l_code)
old_j_turn_value = j_turn_value
if j_value != old_j_value:
l_code[7] = j_value * -1
r_code[7] = j_value
char_1.write(l_code)
char_1.write(r_code)
old_j_value = j_value
# use RTC1 as RTC0 is used by bluetooth stack
# set up RTC callback every 0.1 second
rtc = RTCounter(1, period=1, mode=RTCounter.PERIODIC, callback=check_adc)
rtc.start()
# while True:
# time.sleep_ms(100)
# j_value = int((adc.value() - mid)/120*100)
# if j_value != old_j_value:
# l_code[7] = j_value
# r_code[7] = j_value * -1
# char_1.write(l_code)
# char_1.write(r_code)
# old_j_value = j_value
要玩的時候, 只要先按SW1不放, 再重開機, 看到LED2 (RED)點亮, 再去按batmobile的電源按鈕, 等我們的Dongle上的LED4 (BLUE)點亮, 就可以開始用搖桿來控制batmobile了。哈哈!
-----------------------------------------------------------------------
Q: 如何把寫好的micropython程式存到Flash?
A: 建議大家用Thonny, 專為python程式開發打造的免費整合式開發環境,很容易使用,可以由COM埠連線到安裝有micropython環境的開發板, 然後寫好程式就可File -> Save Copy, 選擇存到開發板,會看到一些錯誤訊息,但不要緊!其實會存成功!
Q: 所以52840 Dongle上會有幾支micropython程式?
A: 至少有3支: boot.py, my.py, batman.py
這系列貼文的前兩篇:
1. 自製蝙蝠車搖桿1
2. 自製蝙蝠車搖桿2
經過考慮, 決定只用一個BLE晶片來做和batmobile溝通的工作, 這是最直接省錢的。當然另外還要有一個搖桿, 本來想用兩個搖桿的, 因為這台車是有兩個馬逹, 官方手機APP的操作界面也是設計兩個按鈕來控制這兩個馬逹。但後來, 硬體的工作弄得有點累, 就先偷懶一下了。
圖1. 正面 |
圖2. 側面 |
- P0.02
- P0.29
- P0.31
- GND
- VDD OUT
- VBUS
圖3. 背面 |
操作方式, 目前的設定是, 搖桿往前推batmobile就前進(2個馬逹同速往前轉), 搖桿往後推就後退(2個馬逹同速後後轉), 搖桿往左推, 那就左轉(右馬逹往前轉), 搖桿往右推就右轉(左馬逹往前轉)。當然, 推到頂的時候馬逹轉速最快, 推到一半, 馬逹的轉速就只有一半。
在組裝之前, 要先把程式燒錄到nRF52840 Dongle, 我使用Micropython 1.12來開發這個小程式。所以要先將Micropython的環境燒錄上去。做法在之前的貼文有講:
micropython-112nrf52840-dongle
然後就可以寫自己的程式了, 寫好的程式可以儲存在BLE晶片的Flash裡面。因為我們的程式是持續的檢查搖桿, 一有變動就要送出控制訊號。為了之後容易維護程式, 不要一通電就去執行我們的程式, 否則怕會無法把程式停下來, 會影響程式修改的工作。因此, 在通電開機時, 先檢查按鈕SW1的狀態, 如果是按下的, 才去執行我們的程式; 如果沒有按下, 就只去點亮LED1。這樣, 想要維護程式時就不要按SW1, 只去重開機就可以。要玩的時候, 再按著SW1, 重開機就行了。這個行為可以用以下boot.py來實現:
--- boot.py ----
from board import LED
from machine import Pin
import my
sw1 = Pin('P38', Pin.IN, Pin.PULL_UP)
if sw1() == 1:
LED(1).on()
else:
# sw1 was pressed
LED(2).on()
my.run('batman.py')
只是對nrf平台的micropython來說,os物件的實作有點不同,所以要小改一下,但這裡用不到,大家就不要用my.ls()就是。順便提一下這裡的SW1, 如下圖是對應到GPIO P1.06, 那在micropython的程式裡要如何寫才能存取到它呢?就如上文的boot.py程式裡的方法,使用Pin類別,在建構式的第一個傳入參數,用"P38"即可存取到P1.06 (32 + 6 = 38)。此外,52840 User Guide p.13提到:
The buttons are active low, which means that the input will be connected to ground when the button is activated. The SW1 button has no external pull-up resistor, but the reset button (SW2) has a 10 k pull-up resistor. To use SW1, P1.06 must be configured as an input with an internal pull-up resistor.所以Pin建構式的第二個和第三個參數才會是Pin.IN, Pin.PULL_UP
摘自52840 Dongle User Guide p.12 |
摘自52840 Dongle User Guide p.13 |
我們的程式, 則由micropython網站的example code修改一下, 就可以了:
--- batman.py ----
import time
from board import LED
from machine import RTCounter
from machine import ADC, Pin
from ubluepy import Peripheral, Scanner, constants
def leds_off():
for i in range(4):
LED(i+1).off()
def bytes_to_str(bytes):
string = ""
for b in bytes:
string += chr(b)
return string
def get_device_names(scan_entries):
dev_names = []
for e in scan_entries:
scan = e.getScanData()
if scan:
for s in scan:
if s[0] == constants.ad_types.AD_TYPE_COMPLETE_LOCAL_NAME:
dev_names.append((e, bytes_to_str(s[2])))
return dev_names
def find_device_by_name(name):
s = Scanner()
scan_res = s.scan(1000)
device_names = get_device_names(scan_res)
for dev in device_names:
if name == dev[1]:
return dev[0]
# led2 on
leds_off()
LED(2).on()
# scan smart hub
dev = None
while not dev:
dev = find_device_by_name("Smart Hub")
time.sleep_ms(100)
# led 4 on
leds_off()
LED(4).on()
# connect smart hub
p = Peripheral()
p.connect(dev.addr())
s = p.getServices()
s1 = s[2]
char_1 = s1.getCharacteristics()[0]
# prepare adc
adc = ADC(Pin(2))
mid = adc.value()
adc_turn = ADC(Pin(29))
mid_turn = adc_turn.value()
l_code = bytearray([0x08,0x00,0x81,0x01,0x11,0x51,0x00,0xda])
r_code = bytearray([0x08,0x00,0x81,0x00,0x11,0x51,0x00,0xda])
old_j_value = 0
old_j_turn_value = 0
def check_adc(timer_id):
global old_j_value
global old_j_turn_value
global adc
global mid
global adc_turn
global mid_turn
global l_code
global r_code
global char_1
j_value = int((adc.value() - mid)/120*100)
if abs(j_value) < 10:
j_value = 0
j_turn_value = int((adc_turn.value() - mid_turn)/120*100)
if abs(j_turn_value) < 50:
j_turn_value = 0
if j_turn_value != old_j_turn_value:
if j_turn_value > 0:
r_code[7] = j_turn_value
char_1.write(r_code)
elif j_turn_value < 0:
l_code[7] = j_turn_value
char_1.write(l_code)
else:
r_code[7] = j_turn_value
char_1.write(r_code)
l_code[7] = j_turn_value
char_1.write(l_code)
old_j_turn_value = j_turn_value
if j_value != old_j_value:
l_code[7] = j_value * -1
r_code[7] = j_value
char_1.write(l_code)
char_1.write(r_code)
old_j_value = j_value
# use RTC1 as RTC0 is used by bluetooth stack
# set up RTC callback every 0.1 second
rtc = RTCounter(1, period=1, mode=RTCounter.PERIODIC, callback=check_adc)
rtc.start()
# while True:
# time.sleep_ms(100)
# j_value = int((adc.value() - mid)/120*100)
# if j_value != old_j_value:
# l_code[7] = j_value
# r_code[7] = j_value * -1
# char_1.write(l_code)
# char_1.write(r_code)
# old_j_value = j_value
-----------------------------------------------------------------------
Q: 如何把寫好的micropython程式存到Flash?
A: 建議大家用Thonny, 專為python程式開發打造的免費整合式開發環境,很容易使用,可以由COM埠連線到安裝有micropython環境的開發板, 然後寫好程式就可File -> Save Copy, 選擇存到開發板,會看到一些錯誤訊息,但不要緊!其實會存成功!
Q: 所以52840 Dongle上會有幾支micropython程式?
A: 至少有3支: boot.py, my.py, batman.py
留言