16チャンネルPWMコントローラーPCA9685キットのサーボモーター制御プログラム例

PWMコントローラ
PWMコントローラ
PCA9685キット

SXP社のPCA9685を使用した16Chのサーボ&PWM駆動キット(ここでは秋月電子通商製)を使い、Raspberry Piで複数のサーボモーター(DS3218)を同時に制御するプログラム例を紹介する。
PCA9685はI2Cバスを使って16ChのLEDをコントローㇽICなので、プログラムで様々なPWM信号を生成することができる。しかし、サーボモーターの駆動には、PWMの周波数とデユーティ比を決めれば駆動できるので、それに特化したモジュールを作成した。

●使用したハードウェア
PWMコントローラ
PWMコントローラ
PCA9685キット

I2C接続16チャンネル サーボ&PWM駆動キット

\950(秋月電子通商製)
■特長
・NXP社のPCA9685を使用した16チャンネル/サーボ&PWM駆動キット
・I2Cを使い16個のサーボを個別に制御可能
・各チャンネルは最大25mA出力することが出来る為、LEDドライバとしても使用可能
・A0~A5のアドレスを設定することにより最大62ユニット(992チャンネル)を連結することが可能
■主な仕様
・電源電圧:2.3~5.5V
・サーボ供給電源:5.0V(3A以上推奨)
・端子電流:25mA
・通信形式:I2C
・搭載チップ:PCA9685

●PWMコントローラーへの外部電源の接続

サーボモーター用電源は、Raspberry Piの5V電源を使うことも可能だが、電流容量を必要とするサーボモーターや、複数台のサーボモーターを使う場合には、電流容量の大きい外部電源を使う必要がある。制御電源はRaspberry Piの3V電源、I2C通信のための、SDA、SCLを接続する。

●PWMの発生方法
PWMタイミングチャート
PWMタイミングチャート(NXP社のデータシートより)

PCA8685自体は外部クロックを使えるが、このPWM駆動キットは25MHzの内部オシレーターを使って矩形波を発生させている。PWMアウトプット周波数は、次の計算式で計算したprescale valueを、PRE_SCALEレジスターにセットすると得られる。ここで低い周波数の場合、正確な周波数を得るためには、オシロスコープをモニターして、最後の-1を、0~+7に変える必要がある。

 \( Prescale Value = round \left( \displaystyle \frac{osc\_clock}{4096 \times update\_rate} \displaystyle \right) – 1\)
    \(osc\_clock\) : 内部発信器周波数
    \(update\_rate\) : PWMアウトプット周波数

Default値は、update_rate=200Hzで、prescale valueは30(0x1E)となっている。
そのアウトプット周波数の1サイクルを4096分割し、LEDn_ONとLEDn_OFFを12bits値で指定し、必要なデューティ比の波形を生成する。このICは本来LEDをドライブするためのもので、ONとOFFの決め方で様々な波形を生成することができるが、サーボモーター駆動のための波形生成は、LEDn_ON=0として、LEDn_OFFタイミングだけを変えれば得られる。

●PWM駆動モジュール

1個のPWM駆動キットで16チャンネルのPWMを発生できる。I2Cアドレスを変えることで、最大62ユニット(992チャンネル)を連結することが可能となるが、ここでは1ユニット(16チャンネル)だけ使用する。

①init()
DefaultのMODE1レジスタ値に対して、Register Auto-Increment Enable(bit5=1)を設定し、他のI2Cと競合しないように、ALL CALLの I2C-Busアドレス(0x70)を無効(bit0=0)にしている。

②setPwmFreq(freq)
PWMアウトプット周波数(Hz)をセットする。サーボモーター(DS3218)の場合は50(Hz)をセット。設定可能な周波数は、24Hz~1526Hz(pre_scale = 0xFF~0x03)

③clear()
16ch全てのON、OFFを「0」にする。

④setPWM(ch, duty)
chのデューティ比(duty/4096)となるOFFタイミング(0~4095)をセットする。ch:0~15、duty:0~4095。このモジュールの実行時間は、約0.6msec。

⑤setPWMblock(block, dutyList)
4chのブロック毎にデューティ比(duty/4096)となるOFFタイミング(0~4095)をセットする。block:0~3、dutyList:[0~4095, 0~4095, 0~4095, 0~4095]。このモジュールの実行時間は、約2.6msec。1ch当たりの実行時間は0.65msecで、④setPWMとほぼ同じ。サーボモータ(DS3218)の速度0.16sec/60degなので、複数のモーターにPWM信号を送った場合、0.2deg/0.6msecずつ遅れて起動することになる。
(蛇足説明)
ここではwrite_i2c_block_data関数を使って、各chのOFF時間のみを2バイト連続(各ch間は不連続)で書き込みを行っているが、各chのON時間とOFF時間の連続する4バイトを、4ch分16バイトを連続してwrite_i2c_block_dataで書き込んでも、1ch当たりの実行時間はほぼ同じになる。連続するバイト数が多いほど書込み効率が良いが、書込み不要なデータが半分含まれてしまうので、実質的な書込み効率は変わらなくなるからだ。

⑥readPwmValues(chList)
chListで指定したchの12bitsのOFFタイミング(0~4095)を読み出す。リターン値は、[ch, duty(0~4095)]のリスト値。

●PWM9685制御モジュールとプログラムサンプル

・PWM9685制御モジュール(PWM9685.py)

# -*- coding: utf-8 -*-
import smbus
import time

I2C = smbus.SMBus(1)
ADR9685 = 0x40		# I2C address of PCA9685 PWM Control board
MODE1 = 0x00	# Reg. address of PCA9685 MODE1
MODE1_DEFAULT = 0x11	# Default Data of MODE1 Reg. 
PRE_SCALE = 0xFE	# Reg. address of PCA9685 prescale
PWM_DATA = 0x06 # Reg. Address of first PWM ON time
PWM_OFF_DATA = 0x08 # Reg. Address of first PWM OFF time
OSC_CLOCK = 25000000	# internal clock = 25Mhz

def init():
    setData = (MODE1_DEFAULT | 0x20) & 0xE0
        # Bit5=1: Auto increment
        # Bit4=0: Normal, oscillator ON
        # Bit3~0=0: Other I2C not responsed
    I2C.write_byte_data(ADR9685, MODE1, setData)

def setPwmFreq(freq):
    if freq < 24:
        freq = 24
    if freq > 1526:
        freq = 1526
    preScale = int(round(OSC_CLOCK/4096/freq) - 1)
    oldReg = I2C.read_byte_data(ADR9685, MODE1)
    newReg = oldReg | 0x10		# Sleep bit4=1:oscillator OFF
    I2C.write_byte_data(ADR9685, MODE1, newReg)	# go to sleep
    I2C.write_byte_data(ADR9685, PRE_SCALE, preScale)	# set prescale
    time.sleep(0.01)
    I2C.write_byte_data(ADR9685, MODE1, oldReg)
    time.sleep(0.01)

def clear():
    for ch in range(16):
        chAdr = PWM_DATA + 4 * ch	# ch = 0 to 15 (each 4 bytes)
        I2C.write_i2c_block_data (ADR9685, chAdr, [0, 0, 0, 0])

def setPWM(ch, duty):
    #ch = 0 to 15, duty = 0 to 4095
    chAdr = PWM_OFF_DATA + 4 * ch	# ch = 0 to 15 (each 4 bytes)
    outVals = [duty & 0xFF, (duty >> 8) & 0x0F]
    I2C.write_i2c_block_data (ADR9685, chAdr, outVals)

def setPWMblock(block, dutyList):
    #block = 0 to 3 (each block: 4ch), duty = 0 to 4095
    blockAdr = PWM_OFF_DATA + block * 16	#  block = 4ch x 4bytes
    for ch in range(4):
        chAdr = blockAdr + ch * 4	#  block = 4ch x 4bytes
        outVals = [dutyList[ch] & 0xFF, (dutyList[ch] >> 8) & 0x0F]
        I2C.write_i2c_block_data (ADR9685, chAdr, outVals)

def readPwmValues(chList):
    # chList=[ch No, ch No-n, .....] readするch No リスト
    pwmList = []
    for i in range(len(chList)):
        chAdr = PWM_OFF_DATA + 4 * chList[i]	# ch = 0 to 15 (each 4 bytes)
        inVals = I2C.read_i2c_block_data (ADR9685, chAdr, 2)
        pwmList.append([chList[i], inVals[0] + ((inVals[1] & 0x0F) << 8)])
            # pwmList = [[ch0, pwm0], [ch1, pwm1], ....]
    return pwmList 


if __name__ == '__main__':
    import time
    import PWM9685 as PWM
    
    PWM.init()
    PWM.setPwmFreq(50)	# 50Hz for Servo Motor(DS3218MG)
    PWM.clear()

    #Servo Motor Moves from 0deg to 270deg
        # 	duty= 2.5% to 12.5% (=102 to 512)
    PWM.setPWM(0, 102)	# move to 0 deg
    time.sleep(0.5)
    for duty in range(102, 513, 10):	# move 0 to 270 deg
        PWM.setPWM(0, duty)
        time.sleep(0.1)
    PWM.setPWM(0, 102)

    # 4 motors :  block 0 (ch.0 to ch.4)
    PWM.setPWMblock(0, [102, 102, 102, 102])
    time.sleep(0.5)
    for duty in range(102, 513, 10):
        PWM.setPWMblock(0, [duty, duty, duty, duty])
        pwmList = PWM.readPwmValues([0,1,2,3])
        strPWM = "ch-duty "
        for i in range(len(pwmList)):
            strPWM = strPWM + "{:2d}-{:03x}    ".format(pwmList[i][0], pwmList[i][1])
        print(strPWM)
    PWM.setPWMblock(0, [102, 102, 102, 102])