電子工作」カテゴリーアーカイブ

サーボモータを利用したPan/Tiltカメラ台の制御(GPIOライブラリーでは正確な制御は無理?)

サーボモータでPan/Tiltカメラ台を制御する実験をやってみたが、意図した方向から少し外れては正しい方向へカメラ台が回転する現象が頻発している。ステッピングモータの時には、このような現象に遭遇した記憶がない。サーボモータは、制御信号のパルス幅に対応した角度まで回転するように作られているので、意図しない回転の原因は、次のように推定できる。

  • 制御信号のパルス幅がバラついている
  • サーボモータの精度が悪い(制御信号のパルスに正確に追従していない)
  • その他?(上記の両方、またはその他の原因)

安価なサーボモータを使っているので、最初はサーボモータの精度の問題と思ったりしたが、制御信号のパルス幅をオシロで観測してみることにした。オシロには測定した値の統計情報を表示する機能があり、パルス幅の評価にはこれを利用することにした。

サーボモータの制御は、次のPythonスクリプトでRaspberry pi4bのGPIOからPWM信号を発生させて行っている。500HzでDuty50%の矩形波を生成。

#GPIOの初期設定
import RPi.GPIO as GPIO
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
PAN=27
GPIO.setup(PAN, GPIO.OUT)
PWM_pan = GPIO.PWM(PAN, 500)
PWM_pan.start(0.0)

PWM_pan.ChangeDutyCycle(50)
while True:
    pass

オシロで観測した波形と統計情報

赤枠部分に表示された情報を信用すると、パルス幅の平均値が1.08mS、最大値が2.34mS、標準偏差が88.96μなどどなっている。ここで、利用したサーボモータは0.5mSから2.5mSの制御信号のパルス幅に対応して、270度の範囲で回転することになっている。回転角度を1度変更するにために必要なパルス幅の変化は、約4.7μS(=2mS/270)。ところが、観測したパルス幅の標準偏差が88.96μということなので、サーボモータが制御信号に忠実に反応した場合、回転角度が数度以上変動することになる。実際に、この制御信号をサーボモータへ与えると、モータは静止せずに微動を繰り返す状態となった。

制御パルス生成にArduino Microを用いた場合

PIN6からPWM信号を生成

int PAN = 6;

void setup() {
  pinMode(PAN, OUTPUT);
  analogWrite(PAN,128);
}

void loop() {
}

オシロで観測した様子

なんと標準偏差が1桁のnSオーダー。Raspberry Pi4の1/1000以下となりました。

サーボモーターを用いたPan/Tiltカメラマウント

以前、ステッピングモータでPan/Tilt制御可能なカメラマウントを作ってみましたが、今回はサーボモータを利用してみました。サーボモータは、手軽にドライブできる利点はありものの、角度の情報を取り出す仕組がないという利用目的次第では致命的な欠点もあることに気が付きました。特にTilt方向が問題で、気を付けないとマウントに搭載しているカメラが座台と干渉し、カメラなどを破損する可能性があります。

ステッピングモータでPan/Tiltの制御し、画像の重心を追尾する実験の様子。

Pan/Tiltの台座

Pan/Tiltの台座を購入してみました。ネジが曲がっていたり数が合わないのは想定内?パーツの不具合を補うため、3Dプリンターでパーツを作る。

組み立ての途中の様子。組み立ててみたら回転の動きが少し怪しい。ベアリングの精度の問題か?この上に、Tilt制御用のサーボを搭載する予定。

ToF(Time of Flight)デバイス

ToF(Time of Flight)デバイスVL530Xを利用して、超近接レーダもどきを作ってみた。

主要なパーツ

  • VL53L0X     距離センサモジュール
  • Arduino nano  CPU
  • A4998     ステッピングモータドライバー
  • Stepping motor 
  • Slip ring

VL53L0X Time-of-Flight 距離センサモジュールの概要は、例えばこちらのページを参照。

arduino nanoでステッピングモータを回転させながら、ToFデバイスで光を反射する物体までの距離を測定します。モータの回転角度と距離をUSB I/FでPCへ転送し、PC側でPython(Jupyter notebook)で可視化しています。

Jupyter notebookで可視化した様子。

Jupyter notebookで可視化

# -*- coding: utf-8 -*-
import sys
import  glob

import serial
import asyncio, sys
import cv2
import numpy as np
import pygame
import math
from pygame.locals import *

def main():
    hist=350
    buf={}
    (x,y) = (1024,1024)   # 画面サイズ

    pygame.init()       # pygame初期化
    pygame.display.set_mode((x, y), 0, 32)  # 画面設定
    pygame.display.set_caption('ToF VL53L0X ranging...')
    screen = pygame.display.get_surface()
    fontFace =cv2.FONT_HERSHEY_SIMPLEX
    dev=glob.glob("/dev/ttyUSB*")[0]
    dev=dev.replace('[','')
    dev=dev.replace(']','')
    ser =serial.Serial(dev, 115200) 
    
    for i in range(5):
        line=ser.readline()
        
    font = pygame.font.Font(None, 55)
    #print(buf)
    while (1):
        for i in range(1,3):
            try:
                deg,distance=ser.readline().decode().split(',')
                deg=int(deg)
                distance=int(distance)
                buf[deg]=distance
                # ゴミデータ対策
                for k in range(4):
                    tmp=deg-k
                    if  tmp<0:
                        tmp=tmp+360
                    buf[deg-k]=distance
            except:
                continue


        # レーダー画面の背景描画
        pygame.draw.circle(screen, (0, 200, 0), (int(x/2), int(y/2)), int(x/2), 1)
        pygame.draw.circle(screen, (0, 200, 0), (int(x/2), int(y/2)), int(x/4), 1)
        pygame.draw.line(screen, (0, 200, 0), (0, int(y/2)), (x, int(y/2)))
        pygame.draw.line(screen, (0, 200, 0), (int(x/2), 0), (int(x/2), y))
        # レーダービームの軌跡描画
        dx = x/2 + x/2 * math.cos(math.radians(deg))
        dy = y/2 + x/2 * math.sin(math.radians(deg))
        pygame.draw.line(screen, (0, 255, 0), (int(x/2), int(y/2)), (int(dx), int(dy)))
        for k in range(hist):
            tmp = deg-k
            if tmp<0:
                tmp=tmp+360
            if tmp in buf:
                x0 = x/2 + buf[tmp]*0.8*math.cos(math.radians(tmp))
                y0=  y/2 + buf[tmp]*0.8*math.sin(math.radians(tmp))
                blip=int(255*(hist-k)/hist)
                pygame.draw.line(screen, (blip, 0, 0), (int(x/2), int(y/2)), (int(x0), int(y0)))
                pygame.draw.circle(screen, (blip, 0, 0), (int(x0), int(y0)), 4, 2)
                
        pygame.display.update()     # 画面更新
        pygame.time.wait(10)        # 更新時間間隔
        screen.fill((0, 20, 0, 0))  # 画面の背景色

        # イベント
        for event in pygame.event.get():
            if event.type == QUIT:      # 閉じるボタンが押されたら終了
                pygame.quit()           # Pygameの終了(画面閉じられる)
                ser.close()
                sys.exit()
                
if __name__ == "__main__":
        main()

Arduinoで(ステッピングモータと距離センサーの制御)

#include <vl53l0xTOFA.h>
#include <Wire.h>

#include <Stepper.h>
//#define PIN_xDIR   D3 //direction
//#define PIN_xSTEP  D4 //step
#define PIN_xDIR   PD3 //direction
#define PIN_xSTEP  PD2 //step
#define PIN_MODE   A1 // MODE
//#define LED       A2
#define ST        4
VL53L0xTOFA sensor;

// for your motor
int val = 0;
// initialize the stepper library on pins 8 through 11:
//Stepper myStepper(stepsPerRevolution, D4, D3);

float dist_TOF(int pos, int n) {
  float sum = 0;
  for ( int j = 0; j < n; j++) {
    sensor.readTOFA();
    sum += sensor.tofa.distancemm;
  }
  int distance = int(sum / n);
  int deg = int(pos *  36.0 / 40.0);
  //  Serial.print(pos);
  //  Serial.print(',');
  Serial.print(deg);
  Serial.print(',');
  Serial.println(distance);
}

void setup() {
  pinMode(PIN_xDIR, OUTPUT);
  pinMode(PIN_xSTEP, OUTPUT);
  //  pinMode(LED, OUTPUT);
  pinMode(PIN_MODE, INPUT_PULLUP);
  // set the speed at 60 rpm:
  //  myStepper.setSpeed(60);
  // initialize the serial port:
  Serial.begin(115200);
  delay(1000);
  Wire.begin();
  delay(1000);

  sensor.setTimeout(500);
  if (!sensor.init())
  {
    Serial.println("Failed to detect and initialize sensor!");
    while (1) {}
  }

  sensor.startContinuous(5);

  Serial.println("Radar Start");
}

void rot(int dir, int n) {
  for (int i = 0; i < n; i++) {
    for (int j = 0; j < 2 * ST; j++) {
      digitalWrite(PIN_xSTEP, !(digitalRead(PIN_xSTEP)));
      delayMicroseconds(800);
    }
    dist_TOF(i, 1);
    delayMicroseconds(800);
  }
}

void loop() {
  if (digitalRead(PIN_MODE) == LOW) {
    digitalWrite(PIN_xDIR, LOW);
    rot(1, 400);
  } else {
    digitalWrite(PIN_xDIR, HIGH);
    rot(0, 400);
  }
}

ArduinoでEFレンズを制御する実験(2)

注文していたマクロアダプターが追跡情報の更新がないまま届きました。13/21/31mmの3個がセットとなっています。左側がアダプター、右側がZWOのCCDカメラで、色が良く似ています。

 本来、レンズとカメラボディの間に取り付けるアダプターなので、CCDカメラと接続するには工夫が必要です。13mmと21mmのアダプターを使って、CCDカメラと接続してみたところ、望遠端付近ではピントが出ました。広角側ではピントが出ませんでした。

強度などの問題があるかもしれませんが、3DプリンターでマクロアダプターとCCDカメラを接続するアダプターを作ってみました。2つのパーツで構成し、M3ネジで接続しています。CCDカメラとは3Dプリンターで作成したM42,0.75Pのネジで接続しています。CADソフトはホビー用途であれば無料で利用できるFusion360を使っています。

CCDカメラ側のパーツを取り付けるまえに、マクロアダプターの接点にリード線を半田づけして引き出し、レンズの制御と電源を供給するコネクタと接続します。マクロアダプターとこのパーツを固定するために、6mmのネジで締め付けています。何回か試作を繰り返した結果、青のフィラメントが無くなったので、手持ちの白にチェンジ!

EFレンズに接続した様子と、フォーカスのハンド・コントローラ。
ソースコードの最新版は、こちら

組み立ててはみたものの、残念ながら動作が不安定です。今のところ、EF-S 18-55mmと同ISは、それなりに制御できますが、EF 35-80mmとEF70-200 1:28Lはまったく動きません。ロジックアナライザーでデコードしたSPIのデータ観測すると、どのレンズでも同じように見えるのですが、、、
 本来、SPIの信号はボード内の短い距離の伝送用なので、コントローラの線が長すぎるのかもしれません。今後、配線の短縮またはバッファーやレベル変換を追加して、改善するかどうか確認したいと思っています。
(追記)ケーブルを約70cmから約25cmに変更したら、手持ちのレンズ全てが一応動くようになりました。ただし、まだ不安定な面もあるので、さらに短くする必要がありそうです。SPIの信号を生成するマイクロプロセッサー(Arduino nano)を、レンズに近接して配置できるよう取り付け方法の変更が必要かもしれません。この場合、ピントの調整を操作するSW付きのロータリーエンコーダの部分だけ、ケーブルで接続した別の箱に格納した方が良さそうです。

ArduinoでEFレンズを制御する実験

ArduinoでEFレンズを制御する実験のメモです。カメラレンズで星雲などを撮影しようとすると、手動でピントを合わせる必要がありますが、微妙な操作が必要でなかなかピントを合わせるのに苦労します。そこで、ステッピングモータでピントを調整しようと、3Dプリンターでこんなものを作ってみました。そうこうしているうちに、CANONのEFレンズをArduinoで制御できるらしいとの情報を得て、自分でも実験してみることに。

レンズの制御信号を取り出せるマウントアダプターを注文したのですが、到着にはしばらく時間がかかりそうなので、手持ちのカメラレンズ(EF-S 18-55mm USM)から信号を取り出すために、7本のリード線を半田づけします。(あくまでも自己責任で、、)
この作業には、こちらのリンクが非常に参考になりました。大型のレンズでは、レンズを駆動するモータ用として6Vの別電源が必要のようですが、実験に使ったレンズではロジック回路と同じ5Vで問題なく駆動できています。

主な材料
秋月の2色LED付きロータリーエンコーダ
OLED  128×64 I2C ディスプレイ
Arduino NANO

回路図

実験に使ったArduno NANOのスケッチ
不用な変数などが残っていますが、とりあえず公開します。(実用化には、まだまだ課題が残っています。)制御の対象はフォーカスと絞りの2種類です。ロータリーエンコーダのSWの長押しで、赤と緑のLEDがトグルで点灯します。赤の点灯中にSWを押してフォーカスを変更し、緑のLED点灯中にSWを押すと絞り値を変更できます。

/*
   ロータリーエンコーダの参照コード
   https://jumbleat.com/2016/12/17/encoder_1/
*/
/*
   EFレンズの制御関連参考リンク
   ASCOM EF Lens Controller
   http://www.indilib.org/media/kunena/attachments/3728/ascom_efEN.pdf

   EFレンズから信号線の引き出の参照資料
   How to Move Canon EF Lenses Yosuke Bando
   http://web.media.mit.edu/~bandy/invariant/move_lens.pdf

   Canon EFレンズのArduino制御
   http://otobs.org/hiki/?EOS_model
   Technical aspects of the Canon EF lens mount
   http://www.eflens.com/lens_articles/ef_lens_mount.html
*/
#include <SPI.h>
#include <EEPROM.h>
#include <math.h>

#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
//Adafruit_SSD1306 display(1);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define RUN

#define LED_SW   7
#define ENC_A  2
#define ENC_B  3
#define LED_Red 14
#define LED_Green 15

volatile byte pos;
volatile int  enc_count;
boolean    sw = false;
int mode = 0;
int mode_counter[2];
int focuserPosition, targetPos, apValue, offset, apAddr, x, y;
String targetStr, apStr, gStr;
boolean IsMoving, IsFirstConnect;
char inStr[6];

void InitLens()
{
  SPI.transfer(0x0A);
  delay(30);
  SPI.transfer(0x00);
  delay(30);
  SPI.transfer(0x0A);
  delay(30);
  SPI.transfer(0x00);
  delay(30);
}

int ENC_COUNT(int incoming) {
  static int enc_old = enc_count;
  int val_change = enc_count - enc_old;

  if (val_change != 0)
  {
    incoming += val_change;
    enc_old   = enc_count;
  }
  return incoming;
}

void ENC_READ() {
  byte cur = (!digitalRead(ENC_B) << 1) + !digitalRead(ENC_A);
  byte old = pos & B00000011;
  byte dir = (pos & B00110000) >> 4;

  if (cur == 3) cur = 2;
  else if (cur == 2) cur = 3;

  if (cur != old)
  {
    if (dir == 0)
    {
      if (cur == 1 || cur == 3) dir = cur;
    } else {
      if (cur == 0)
      {
        if (dir == 1 && old == 3) enc_count++;
        else if (dir == 3 && old == 1) enc_count--;
        dir = 0;
      }
    }
    pos = (dir << 4) + (old << 2) + cur;
  }
}

void setup() {
  digitalWrite(13, LOW); // SPI Clock PIN
  pinMode(ENC_A, INPUT_PULLUP);
  pinMode(ENC_B, INPUT_PULLUP);
  pinMode(LED_Red, OUTPUT);
  pinMode(LED_Green, OUTPUT);
  pinMode(LED_SW, INPUT_PULLUP);
//  pinMode(MIN, INPUT_PULLUP);
//  pinMode(MAX, INPUT_PULLUP);
  attachInterrupt(0, ENC_READ, CHANGE);
  attachInterrupt(1, ENC_READ, CHANGE);

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();

  // テキストサイズを設定
  display.setTextSize(3);
  // テキスト色を設定
  display.setTextColor(WHITE);
  display.setCursor(0, 10);
  display.println("EF-LensFocuser");
  display.display();
  delay(1000);
  mode = 0;
  apAddr = 0;
  focuserPosition = 5000;
  SPI.begin();
  SPI.setBitOrder(MSBFIRST);
  SPI.setClockDivider(SPI_CLOCK_DIV128);
  SPI.setDataMode(SPI_MODE3);
  digitalWrite(12, HIGH);
  InitLens();
  digitalWrite(LED_Red, HIGH);
  digitalWrite(LED_Green, LOW);

  apValue=EEPROM.read(apAddr);
  Serial.begin(9600);
  Serial.println(apValue);
  // nothing to do inside the setup
}

void loop() {
  int sw_count;
  short counter_now;
  sw_count = 0;
  while (digitalRead(LED_SW) == LOW) {
    sw_count++;
    if (sw_count > 50) {
      if (mode == 1) {    // 新モードはフォーカス制御
        digitalWrite(LED_Red, HIGH);
        digitalWrite(LED_Green, LOW);
      } else {            // 新モードは絞り制御
        digitalWrite(LED_Green, HIGH);
        digitalWrite(LED_Red, LOW);
      }
    }
    delay(10);
  }
  delay(100);
  if (sw_count > 50) {
    if (mode == 0) {
      mode = 1;   // 絞りモード
      digitalWrite(LED_Green, HIGH);
      digitalWrite(LED_Red, LOW);
    } else {
      mode = 0;   // フォーカスモード
      digitalWrite(LED_Red, HIGH);
      digitalWrite(LED_Green, LOW);
    }
  }
  if (sw_count != 0 && (sw_count < 50) ) {
    if  (mode == 0 ) { // Send command to LENS フォーカス
      targetPos =  mode_counter[mode] ;
      offset = mode_counter[mode] ;
      x = highByte(offset);
      y = lowByte(offset);
      InitLens();
      IsMoving = true;
      Serial.print(offset); Serial.print(",");
      Serial.print(x); Serial.print(",");
      Serial.println(y);
      SPI.transfer(68);       delay(30);
      SPI.transfer(x);        delay(30);
      SPI.transfer(y);        delay(30);
      SPI.transfer(0);        delay(100);
      IsMoving = false;
      focuserPosition = targetPos;
    } else {              // 絞り
      apValue = mode_counter[mode] % 20;
      if (apValue != EEPROM.read(apAddr))
      {
        InitLens();
        Serial.println("AP");
        SPI.transfer(0x07);          delay(10);
        SPI.transfer(0x13);          delay(10);
        SPI.transfer((apValue - EEPROM.read(apAddr)) * 3);
        delay(100);
        SPI.transfer(0x08);          delay(10);
        SPI.transfer(0x00);          delay(10);
        EEPROM.write(apAddr, apValue);
      }
    }
  }

  counter_now = ENC_COUNT(mode_counter[mode]);
  if (mode_counter[mode] != counter_now)
  {
    mode_counter[mode] = counter_now;
  }
  disp_update();
}

void disp_update() {
  display.clearDisplay();
  display.setCursor(0, 10);
  display.print(" F:");
  display.println( mode_counter[0] );
  display.print(" A:");
  display.println(mode_counter[1]);
  display.display();
}