ステッピングモータードライバーL6470の2台同時制御プログラム例
STマイクロ製ステッピングモータードライバーL6470を組み入れたドライブキット(ここでは秋月電子通商製)を使い、2台のステッピングモーター(SM-42BYG011)を同時に制御するプログラムを紹介する。
L6470はSPI通信で制御するが、それぞれのモーターに順番に制御コマンドを送ると、タイムラグが生じ、2台同時に起動させることができない。CS信号の立上げでコマンドが実行されるため、同時起動の方法としては、別々に制御コマンドを送った後、CS信号を同時に送信する方法や、デージーチェーンの方法が紹介されているが、ここでは、CS信号をほぼ同時(5μsecのタイムラグあり)に送信する方法とした。この方法では、2台のモーターを別々に起動させたり、同時に起動させたりできる。CLK信号とCS信号のタイミングを合わせるために、spidevモジュールを使わず、RPi.GPIOモジュールを使って作成した。
●使用したハードウェア
L6470使用のステッピングモータードライブキット
\1,800(秋月電子通商)
<主な仕様>
・モータ電圧:8V~45V
・ロジック電圧:内部レギュレータの3Vの使用、または外部電源(最大5.5V)の使用
・最大駆動電流:3A(ピーク7A)
バイポーラ ステッピングモーター(SM-42BYG011)
\1,380(秋月電子通商)
<主な仕様>
・相数:2
・ステップ角:1.8度±5%
・1回転ステップ数:200
・入力定格電圧:12V
・定格電流:0.33A/相
・静止トルク:0.23N・M
●ドライバーとモーターの結線
制御用電源は内部レギュレーター3Vを使う。
2台のモータードライバー(MD)の⑤SDO、⑥CK、⑦SDI、③GND端子は、Raspberry PiのSPIMISO⑨、SPICLK⑪、SPIMOSI⑩、GND端子に共通接続する。2台のMD(ch0/ch1)の⑧CS端子は、それぞれ、Raspberry PiのSPICS⑧、SPICS⑦端子に接続する。(ストロベリーリナックス社のL6470ステップモータードライブキットとは端子配列番号が違っている。)
●L6470モータードライバー(MD)とRaspberry PiのSPI通信手順
MDの読み書き(Read/Write)は1バイト単位で行う。CSをLowにして、MSBから順に8ビット書込み/読込み後に、CSをHighにすることで1バイトの読み書きが終わる。
タイミングとして、SDI(MDへの書込み)はCKの立上りでサンプルされ、最低20nsec保持した後、次のビット書込みサイクルに移るが、最後のLSBビットの書込み後、CSをHighにした後に、MDの内部処理のため、最低800nsecのインターバルが必要となる。SDO(MDからの読込み)はCKの立下りでラッチされ、CK=Lowになって最大57nsec後に読取りデータが有効になる。
Raspberry PiのGPIO.outputのサイクルは約5μsecであるため、いずれの処理もタイミングを合わせるためにtime.sleep()を入れる必要はない。尚、以下に記述するMDへの1バイトのread/writeモジュールの実行時間は132μsec。
●L6470制御モジュール
以下のモジュールを使う際、引数chには、2台のMDを個別に設定する場合には、ch=0またはch=1とし、2台同時に書き込みをする場合には、ch=2を使う。
<MDのread/writeモジュール>
①open()
RPi.GPIOモジュールのGPIOの入出力を定義
②readDriver(ch, byteLen)
ch(0/1)のMDから、byteLenバイト数を読取り、結果をリストで返す。ch=2は使えない。
このモジュールの実行時間は132μsec(Raspberry Pi 3B使用時)
③writeDriver(ch, byteList)
ch(0/1/2)のMDに、バイトリストbyteListを書き込む。CSをHighにすることでコマンドが実行されるので、2台同時(ch=2)の場合には、2台のCSをLowにした後、コマンドとそれに必要なデータを同時に書き込んで、最後に、2台のCSをHighにしている。最後のCSをHighにするGPIO.output処理はch.0とch.1をシリーズで行っているので、実際には、モーターのスタートには5μsecのタイムラグがあるが、実用上は無視できる時間である。このモジュールの実行時間は132μsec(Raspberry Pi 3B使用時)。
<MDコマンドに関するモジュール>
④resetDevice(ch)
ch(0/1/2)のモータードライバーMDを初期化する。NOP_DEVICE(0x00)を3回送って、コマンドの引数をフラッシュした後、RESET_DEVICEコマンドを送っている。
⑤setKval(ch, kHold, kRun, kAcc, kDec)
モーターを保持、回転、加減速する際のトルク(電圧レベル=PWMデューティ比)はDefault値として、0x29(= Vs * 0x29 / 256 = 0.16Vs)が設定されているが、モーターによってはトルク不足となるので、変更する必要がある。
⑥setMoveVal(ch, sAcc, sDec, sMxSpd, sMnSpd, sFsSpd)
モータの加速度、減速度、最大速度、起動最低速度、フルステップ速度(マイクロステップ動作からフルステップ動作になる速度)で、それぞれDefault値は、0x08A, 0x08A, 0x041, 0x000, 0x027となっている。
注意すべきことは、それぞれ1単位の分解能が異なることである。
- 加速度sAcc/減速度sDec : 12bits、分解能14.55 step/sec2 、最小/最大(14.55 ~59,590 step/sec2)
- 最大速度sMxSpd : 10bits、分解能15.25 step/sec、最小/最大(15.25 ~15,610 step/sec)
- 最低速度sMnSpd : 13bits、分解能0.238 step/sec、最小/最大(0 ~976.3 step/sec)
- フルステップ速度sFsSpd : 10bits、分解能15.25 step/sec、最小/最大(14.55 ~59,590 step/sec)
尚、回転時の速度レジスタ(SPEED)の分解能は、MAX_SPEEDの1/1024倍となる。
速度SPEED(0x04) : 20 bits、分解能0.015 step/sec、最小/最大(0 ~15,625 step/sec)
⑦runDirSpeed(ch, sDir, sSpd)
ch(0/1/2)のモーターを、sDir(FWD=1 / REV=0)方向に、sSpd速度で回転する。
ここで、sSpdは上記のSPEED(0x04)レジスタの値と同じ分解能(0.015 step/sec)、最小/最大値となっている。ch=2の場合、2台が同じ指定速度、方向となる。
⑧moveDirStep(ch, sDir, sStep)
ch(0/1/2)のモーターを、sDir(FWD=1 / REV=0)方向に、sStepステップ数回転して停止する。
この時のsStepはマイクロステップ数(Defaultの1単位は1/128ステップ)。1回転200ステップのモーターの場合、1回転するためには、sStep = 200 × 128 = 25,600となる。
⑨turnDirStep(sDir, sStep)
このモジュールは、左右の2台のモーターで駆動するマウスを、その場でスピン回転することを想定したもの。
左右のモーターを、sDir(LEFT=0 / RIGHT=1)方向に、sStepステップ数スピン回転して停止する。90度スピン回転するには、φ65mmの車輪が、車輪中心間距離130mmで左右に配置されている場合、次のステップ数だけ左右の車輪を逆方向に回転すればよい。
\(\displaystyle \frac{130\pi}{4} \div 65\pi \times 200 \times 128 = 12,800\)
左右のモーターの回転スタートが異なると、スピン中心が移動するので、その防止のため、このモジュールを作成した。
⑩goToAbsPos(ch, sPos)
ch(0/1/2)のモーターを、絶対位置sPosまで、最短方向に回転して停止する。
この時のsPosはマイクロステップ数(Defaultの1単位は1/128ステップ)で表す絶対位置。
⑪goToDirAbsPos(ch, sDir, sPos)
ch(0/1/2)のモーターを、sDir(FWD=1 / REV=0)方向に、絶対位置sPosまで回転して停止する。⑩goToAbsPosとの違いは、回転方向を指定するかしないかであるが、近くに目標位置sPosがあるにも関わらず、逆方向の指示をすると、目標位置sPosに到達するためには時間がかかることになる。
⑫getStatus(ch)
ch(0/1)のSTATUSレジスタ(0x19)を読取り、16bitsデータで返す。STATUSの値はSTマイクロ社の資料に書かれているが、要点を纏めると次の表のようになる。この中で、特にMOT_STATUS、BUSYステータスは役立つ。
⑬readAbsPos(ch)
ch(0/1)の絶対位置(ABS_POS:0x01)を、20bitsで返す。
この時のreturn値はマイクロステップ数(Defaultの1単位は1/128ステップ)。
●MD6470制御モジュールとプログラムサンプル
・プログラムサンプル(stepMotorSample.py)
# -*- coding: utf-8 -*- import time import MD6470 as MD def stopStat(timeOut): sTime = time.time() while True: try: statMD = (MD.getStatus(0) | MD.getStatus(1)) if not(statMD & 0x0060): # if both motor STOP, exit sStat = True break elif time.time() - sTime > timeOut: sStat = False break except KeyboardInterrupt: sStat = False break return sStat def main(): MD.open() # ドライバー設定 MD.resetDevice(2) # 初期化 time.sleep(0.1) MD.setKval(2, 0x60, 0xB4, 0xB4, 0xB4) # トルクコントロールパラメーター設定 MD.setMoveVal(2, 0x1A, 0x1A, 0x14, 0x00, 0x027) #モーションパラメータ設定 sSpd = 13333 # sSpd = 1回転/sec = 1.0 * 200step / 0.015step/s = 13,333 sStp = 25600 # 1回転 = 200 * 128 = 25,600 steps MD.runDirSpeed(2, MD.FWD, sSpd) #2台同時、速度sSpdで前進(継続移動) if not stopStat(3.0): MD.softStop(2) # 加減速してソフト停止 print("Soft-Stopped by Timeout") MD.moveDirStep(2, MD.FWD, sStp) # 2台同時、ステップ数sStp、前進 if stopStat(5.0): print("Stopped after moveDirStep") MD.turnDirStep(MD.LEFT, sStp) # ステップ数sStp、左スピン回転 if stopStat(5.0): print("Stopped after turnDirStop") MD.resetAbsPos(2) # 2台同時、絶対位置リセット MD.goToAbsPos(2, 20000) # 2台同時、絶対位置20,000に移動 if stopStat(5.0): print("Stopped after goToAbsPos") print("Absolute Position = ", MD.readAbsPos(0), MD.readAbsPos(1)) # 絶対位置の読み出し MD.resetAbsPos(2) # 2台同時、絶対位置リセット MD.goToDirAbsPos(2, MD.FWD, 20000) # 2台同時、前進で絶対位置20,000まで移動 if stopStat(5.0): print("Stopped after goToDirAbsPos") print("Absolute Position = ", MD.readAbsPos(0), MD.readAbsPos(1)) # 絶対位置の読み出し print("MD Status ({:d}) = {:016b}".format(0, MD.getStatus(0))) # ステータスの読み出し print("MD Status ({:d}) = {:016b}".format(1, MD.getStatus(1))) MD.close() # ドライバーの設定解除 if __name__ == '__main__': main()
・MD6470制御モジュール(MD6470.py)
# -*- coding: utf-8 -*- import RPi.GPIO as GPIO # ピンの名前を変数として定義 SPICLK = 11 SPIMOSI = 10 SPIMISO = 9 SPICS = [8,7] #MOTOR DRIVER CS(ch=0/1) # 変数 FWD = 1 REV = 0 LEFT = 0 RIGHT = 1 # Motor Driver Register Name & Address ACC_REG = 0x05 # Acceleration DEC_REG = 0x06 # Deceleration MAX_SPEED_REG = 0x07 # Maximum speed MIN_SPEED_REG = 0x08 # Minimum speed FS_SPD_REG = 0x15 # Full step speed STEP_MODE_REG = 0x16 # Step mode ALARM_EN_REG = 0x17 # Alarms enable KVAL_HOLD = 0x09 # Holding Kval KVAL_RUN = 0x0A # Constant speed Kval KVAL_ACC = 0x0B # Acceleration starting Kval KVAL_DEC = 0x0C # Deceleration starting Kval # Motor Driver Command NOP_DEVICE = 0x00 # Nothing SET_PARAM = 0x00 # Write VALUE in PARAM register GET_PARAM = 0x20 # Returns the stored value in PARAM register RUN_DIR = 0x50 # Sets the target speed and the motor direction MOVE_STEP = 0x40 # Makes N_STEP steps in DIR direction GO_TO_ABS = 0x60 # Brings motor in ABS_POS position(min. path) GO_DIR_ABS = 0x68 # Brings motor in ABS_POS position(DIR direction) GO_HOME = 0x70 # Brings the motor in HOME position RESET_POS = 0xD8 # Reset the ABS_POS register RESET_DEVICE = 0xC0 # Device is reset to power-up conditions SOFT_STOP = 0xB0 # Stops motor with a DEC HARD_STOP = 0xB8 # Stops motor immediatelly GET_STATUS = 0xD0 # Returns the status register value ABS_POS = 0x01 # Absolute Position(22bits) #================================================================= def open(): # SPI通信用の入出力を定義 GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) GPIO.setup(SPICLK, GPIO.OUT) GPIO.setup(SPIMOSI, GPIO.OUT) GPIO.setup(SPIMISO, GPIO.IN) GPIO.setup(SPICS[0], GPIO.OUT, initial=GPIO.HIGH) #CS Stand-By GPIO.setup(SPICS[1], GPIO.OUT, initial=GPIO.HIGH) #CS Stand-By #====== Motor Driver L6470 -> readDriver(ch, byteLen) ======== def readDriver(ch, byteLen): # ch=0/1 byteData = [] if byteLen <= 0 or byteLen >3: return byteData for n in range(byteLen): GPIO.output(SPICS[ch], GPIO.HIGH) GPIO.output(SPICLK, GPIO.HIGH) GPIO.output(SPICS[ch], GPIO.LOW) # CS Active bitData = 0 for bt in range(8): # MSBから8ビット受信 GPIO.output(SPICLK, GPIO.LOW) GPIO.output(SPICLK, GPIO.HIGH) bitData <<= 1 if GPIO.input(SPIMISO) == GPIO.HIGH: bitData |= 0x1 byteData.append(bitData) GPIO.output(SPICS[ch], GPIO.HIGH) #CS Stand-By return byteData #====== Motor Driver L6470 -> writeDriver(ch, byteList) ======== def writeDriver(ch, byteList): # ch=0/1/2, ch=2: both 0 and 1 for n in range(len(byteList)): if ch == 2: GPIO.output(SPICS[0], GPIO.HIGH) GPIO.output(SPICS[1], GPIO.HIGH) GPIO.output(SPICLK, GPIO.HIGH) GPIO.output(SPICS[0], GPIO.LOW) # CS Active GPIO.output(SPICS[1], GPIO.LOW) # CS Active else: GPIO.output(SPICS[ch], GPIO.HIGH) GPIO.output(SPICLK, GPIO.HIGH) GPIO.output(SPICS[ch], GPIO.LOW) # CS Active outBit = byteList[n] for bt in range(8): # MSBから8ビット送信 GPIO.output(SPICLK, GPIO.LOW) if outBit & 0x80: GPIO.output(SPIMOSI, GPIO.HIGH) else: GPIO.output(SPIMOSI, GPIO.LOW) GPIO.output(SPICLK, GPIO.HIGH) outBit <<= 1 if ch == 2: GPIO.output(SPICS[0], GPIO.HIGH) #CS Stand-By GPIO.output(SPICS[1], GPIO.HIGH) #CS Stand-By else: GPIO.output(SPICS[ch], GPIO.HIGH) #CS Stand-By return #================================================================= def resetDevice(ch): #Reset motor driver writeDriver(ch, [NOP_DEVICE, NOP_DEVICE, NOP_DEVICE, RESET_DEVICE]) #STEP_MODE:0x00(Full step).....0x07(1/128microstep) writeDriver(ch, [STEP_MODE_REG, 0x07]) # Default def setKval(ch, kHold, kRun, kAcc, kDec): #KVAL_HOLD(8bits):Default=0x29(0.16Vs) writeDriver(ch, [SET_PARAM | KVAL_HOLD, kHold]) #KVAL_RUN(8bits):Default=0x29(0.16Vs) writeDriver(ch, [SET_PARAM | KVAL_RUN, kRun]) #KVAL_ACC(8bits):Default=0x29(0.16Vs) writeDriver(ch, [SET_PARAM | KVAL_ACC, kAcc]) #KVAL_DEC(8bits):Default=0x29(0.16Vs) writeDriver(ch, [SET_PARAM | KVAL_DEC, kDec]) def getKval(ch): #KVAL_HOLD(8bits) writeDriver(ch, [GET_PARAM | KVAL_HOLD]) kHold = readDriver(ch, 1) #KVAL_RUN(8bits) writeDriver(ch, [GET_PARAM | KVAL_RUN]) kRun = readDriver(ch, 1) #KVAL_ACC(8bits) writeDriver(ch, [GET_PARAM | KVAL_ACC]) kAcc = readDriver(ch, 1) #KVAL_DEC(8bits) writeDriver(ch, [GET_PARAM | KVAL_DEC]) kDec = readDriver(ch, 1) return [kHold, kRun, kAcc, kDec] def setMoveVal(ch, sAcc, sDec, sMxSpd, sMnSpd, sFsSpd): #ACC(12bits):Default=0x08A(resolution=14.55steps/s2) writeDriver(ch, [SET_PARAM | ACC_REG, (sAcc >> 8) & 0x0F, sAcc & 0xFF]) #DEC(12bits):Default=0x08A(resolution=14.55steps/s2) writeDriver(ch, [SET_PARAM | DEC_REG, (sDec >> 8) & 0x0F, sDec & 0xFF]) #MAX_SPEED(10bits):Default=0x041(resolution=15.25steps/s) writeDriver(ch, [SET_PARAM | MAX_SPEED_REG, (sMxSpd >> 8) & 0x03, sMxSpd & 0xFF]) #MIN_SPEED(13bits):Default=0x000(resolution=0.238steps/s) writeDriver(ch, [SET_PARAM | MIN_SPEED_REG, (sMnSpd >> 8) & 0x1F, sMnSpd & 0xFF]) #FS_SPD(10bits):Default=0x027(resolution=15.25steps/s2) writeDriver(ch, [SET_PARAM | FS_SPD_REG, (sFsSpd >> 8) & 0x03, sFsSpd & 0xFF]) def getMoveVal(ch): #ACC(12bits) writeDriver(ch, [GET_PARAM | ACC_REG]) rDataList = readDriver(ch, 2) rAcc = (rDataList[0] & 0x0F) << 8 | rDataList[1] #DEC(12bits) writeDriver(ch, [GET_PARAM | DEC_REG]) rDataList = readDriver(ch, 2) rDec = (rDataList[0] & 0x0F) << 8 | rDataList[1] #MAX_SPEED(10Bits) writeDriver(ch, [GET_PARAM | MAX_SPEED_REG]) rDataList = readDriver(ch, 2) rMax = (rDataList[0] & 0x03) << 8 | rDataList[1] #MIN_SPEED(13Bits) writeDriver(ch, [GET_PARAM | MIN_SPEED_REG]) rDataList = readDriver(ch, 2) rMin = (rDataList[0] & 0x1F) << 8 | rDataList[1] #FS_SPD(10Bits) writeDriver(ch, [GET_PARAM | FS_SPD_REG]) rDataList = readDriver(ch, 2) rFsSpd = (rDataList[0] & 0x3F) << 8 | rDataList[1] return [rAcc, rDec, rMax, rMin, rFsSpd] def runDirSpeed(ch, sDir, sSpd): # 方向(FWD/REV)を決めて、指定速度で移動 # RUN_DIR | sDir(1:FWD) + 3byte(20bits) # ch=2: Ch0 & ch1 start simultaneusly setCmd = RUN_DIR | sDir spdH = (sSpd >> 16) & 0x0f # Bit7-4 : XXXX spdM = (sSpd >> 8 ) & 0xff spdL = sSpd & 0xff writeDriver(ch, [setCmd, spdH, spdM, spdL]) def moveDirStep(ch, sDir, sStep): # 指定(sDir:FWD/REV)方向に、指定sStep数移動(LEFT and RIGHT) # MOVE_STEP | sDir(1:FWD) + sStep(3byte:22bits) # ch=2: Ch0 & ch1 start simultaneusly setCmd = MOVE_STEP | sDir stepH = (sStep >> 16) & 0x3f # Bit7-6 :XX stepM = (sStep >> 8 ) & 0xff stepL = sStep & 0xff writeDriver(ch, [setCmd, stepH, stepM, stepL]) def turnDirStep(sDir, sStep): # 左右(sDir)方向に、sStep数回転(左右モーター同時起動) # sDir = LEFT/RIGHT # MOVE_STEP | sDir(1:FWD) + sStep(3byte:22bits) # ch=2: Ch0 & ch1 start simultaneusly if sDir == LEFT: setCmd0 = MOVE_STEP | REV setCmd1 = MOVE_STEP | FWD else: setCmd0 = MOVE_STEP | FWD setCmd1 = MOVE_STEP | REV writeDriver(0, [setCmd0]) writeDriver(1, [setCmd1]) stepH = (sStep >> 16) & 0x3f # Bit7-6 :XX stepM = (sStep >> 8 ) & 0xff stepL = sStep & 0xff writeDriver(2, [stepH, stepM, stepH]) def goToAbsPos(ch, sPos): # 絶対位置sPosまで最短パスで移動 # GO_TO_ABS + sPos(3bytes:22bits) # ch=2: Ch0 & ch1 start simultaneusly if sPos < 0: sPos = (~(-sPos-1)) & 0x3FFFFF setCmd = GO_TO_ABS sPosH = (sPos >> 16) & 0x3f # Bit7-6 : XX sPosM = (sPos >> 8 ) & 0xff sPosL = sPos & 0xff writeDriver(ch, [setCmd, sPosH, sPosM, sPosL]) def goToDirAbsPos(ch, sDir, sPos): # 方向(FWD/REV)を決めて、絶対位置sPosまで移動 # GO_TO_DIR | sDir(0:REV,1:FWD) + sPos(3bytes:22bits) # ch=2: Ch0 & ch1 start simultaneusly if sPos < 0: sPos = (~(-sPos-1)) & 0x3FFFFF setCmd = GO_DIR_ABS | sDir sPosH = (sPos >> 16) & 0x3f # Bit7-6 : XX sPosM = (sPos >> 8 ) & 0xff sPosL = sPos & 0xff writeDriver(ch, [setCmd, sPosH, sPosM, sPosL]) def resetAbsPos(ch): # ABS_POS データを初期化 writeDriver(ch, [RESET_POS]) def softStop(ch): # 加減速ありの停止(Soft Stop) writeDriver(ch, [SOFT_STOP]) def hardStop(ch): # 加減速なし停止(Hard Stop) writeDriver(ch, [HARD_STOP]) def getStatus(ch): #STATUSレジスターの読み取り #GET STATUS command(0xD0)->2bytes return writeDriver(ch, [GET_STATUS]) rDataList = readDriver(ch, 2) rData = (rDataList[0] << 8) | rDataList[1] #MSB...16Bits return rData def readAbsPos(ch): # ABS_POS の読み出し writeDriver(ch, [GET_PARAM | ABS_POS]) # Get ABS_POS data rDataList = readDriver(ch, 3) rSign = rDataList[0] & 0x3F #Bit22 to Bit17 rData = rSign << 8 #Bit22 to Bit17 rData = (rData | rDataList[1]) << 8 #Bit16 to Bit9 rData = rData | rDataList[2] #Bit8 to Bit 0 if rSign & 0x20: # minus sign rData = -((~rData) & 0x3FFFFF) - 1 return rData def close(): # MDをpower-up conditionに resetDevice(2) # SPI通信用のGPIOをcleanup GPIO.cleanup()