LEGO 76112 Batman Part III - 自製蝙蝠車搖桿3

  終於要把這個系列的貼文做個結束了, 因為工作忙, 加上有一些技術問題要解決, 所以從今年初抽空慢慢做, 到最近才完工。簡單的說, LEGO 76112 Batmobile這台車是由手機透過BLE來控制的, 但是媽媽不給小孩碰手機! 我只好想辦法自己做一個搖桿來搖控這台車。


  這系列貼文的前兩篇:
1. 自製蝙蝠車搖桿1
2. 自製蝙蝠車搖桿2

  經過考慮, 決定只用一個BLE晶片來做和batmobile溝通的工作, 這是最直接省錢的。當然另外還要有一個搖桿, 本來想用兩個搖桿的, 因為這台車是有兩個馬逹, 官方手機APP的操作界面也是設計兩個按鈕來控制這兩個馬逹。但後來, 硬體的工作弄得有點累, 就先偷懶一下了。
圖1. 正面
  圖1, 可見一個BLE晶片的板子, 左上角有Nordic字樣的藍色板子, 這是nRF52840 Dongle。右邊則是搖桿, 基本上和PS2搖桿或XBOX搖桿上所採用的搖桿零件是一模一樣的。不信, 自己拆解看看, 也可以看iFix it的網站資料, 真的是一樣的零件哦! 這個搖桿在電子賣埸都找得到, 有不同價位, 便宜的台幣25元, 貴的100+, 200+, 我用起來, 發覺都一樣! 建議可以買最便宜的就好! 至於最大塊的綠色洞洞板, 只是用來當骨架, 方便固定零件。因為可能日後還要把零件拿下來, 所以都只是用電線假固定。
圖2. 側面
圖2, 側面可以看到nRF52840 Dongle, 焊接了3個藍色的排針和3個黃色的排針, 從上面往下:
  • P0.02
  • P0.29
  • P0.31
  • GND
  • VDD OUT
  • VBUS
用到了P0.02, P0.29, GND, VDD OUT; 另2支針腳沒有使用! nRF52840 Dongle板子後面的SB2要切斷 (open), SB1要焊起來 (close)。P0.02, P0.29用來連接搖桿的類比輸出。GND接地, VDD OUT則接電池正極3V。
圖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提到:
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
 

要玩的時候, 只要先按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


留言

這個網誌中的熱門文章

D-BUS學習筆記

關於藍牙裝置找尋(inquiry, scan)兩三事

Cisco Switch學習筆記: EtherChannel