見出し画像

Raspberry Piでエンコーダ付きモータと距離センサを使った2輪車両ロボットの製作

 こんにちは、タクト工房へようこそ!今回は、Raspberry Piを用いた二輪車両ロボットを製作しました。そして、距離センサーによって障害物を検知し、障害物との距離が近づくとロボットを停止させる制御を実装しました!


2輪車両ロボットの概要

今回のロボットは、以下の点をポイントに設計しています。

  1. エンコーダでモータの回転速度を測定
    正確な速度制御や位置制御が可能です。
    ※ただし今回はエンコーダは未使用。

  2. 超音波センサで障害物を検出
    障害物が近いと自動で停止、あるいは回避を行います。

  3. L298Nモータドライバを使用
    前進・後退・旋回といった基本動作を制御できます。

ロボットの製作

 下の写真にあるように、二輪車両ロボットを製作しました。配線が少し雑に見えるかもしれませんが、実際に自分で作ってみると愛着がわいてきます。必要な道具が手元になかったり、サイズ選びに悩んだりと苦労もありましたが、その分、完成したときの喜びは格別でした!そして、自分で組み立てたロボットが思いどおりに動く瞬間は、本当にワクワクしますね。今後は、さらにいろいろな動作や改良を試してみる予定です。

実際の走行

 完成した二輪車両ロボットを走行させてみました。今回は、障害物との距離が30cmより近づくと自動的に停止するように設定しています。


 距離センサ(HC-SR04)やエンコーダ付きDCモータの基本的な仕組みや配線方法などについては、以前の記事で解説しています。そちらも合わせて読んでいただけると、より理解が深まり、オリジナルの車両ロボットが製作できるようになります!

以下に具体的な製作例について説明します。

使用した材料と道具

  • Raspberry Pi 4(他のバージョンでも可)

  • Raspberry Pi 4ケース

  • エンコーダ付きDCモータ × 2(6V,50RPM)

  • モータドライバ L298N

  • HC-SR04距離センサ(超音波センサ)

  • ジャンパーワイヤー

  • M3ナット×8

  • M3ボルト×12(長さは10~12mm、ボルトのうち4つは低頭の物が好ましい)

  • M3六角スタンドオフスペーサー×4(長さ50mm)

  • M2ボルト・ナット×3(長さ8mm)

  • ミニブレッドボード

  • 9V電池

  • 電池ボックス

  • モバイルバッテリー

  • 抵抗(1kΩ×1,2kΩ×2)

  • XHコネクタ

  • キャスター

  • 3Dプリンター(PLAロボットの土台を製作)
    ※3Dプリンターがなくても3D出力サービスなどで代替可能

  • 圧着ペンチ

  • ワイヤーストリッパー(必要があれば)

※モータなど購入時にボルトなどが含まれているため、ボルトとナットの数はそのぶんを含めていません。


・モータ
 もともと500RPMのモータを使用していましたが、トルク不足を感じたため、今回50RPMを選択しました(ただし、50RPMは遅すぎる感もあるので、もう少し高回転のものでもいいかもしれません)。

・外部電源、電池ボックス
 
充電できると便利なので充電式の電池を購入しました。

・モータドライバ

・RaspberryPi

・RaspberryPiケース

・距離センサ

・モバイルバッテリー

・XHコネクタ

・ワイヤーストリッパー

・圧着ペンチ

・ミニブレッドボード

・六角スタンドオフスペーサー

・raspberrypiキッド

3Dプリンターでロボットの土台製作

 まずは、3Dプリンターを使ってロボットの土台を作ります。3Dプリンターをお持ちでない場合は、3D出力サービスなどを活用してみてください。今回は creo で3Dデータを作成し、そのSTLファイルを用いてプリントを行いました。

設計した土台

stlデータ

車両ロボットの組み立て

1.モータとキャスターを取り付ける

 モータとキャスターを下図に示す場所にボルトとナットで固定します。

  • キャスターのボルトは、上部にモバイルバッテリーを載せるため、なるべく頭が低いものが望ましいです。

  • モータもキャスターも、ロボットの“下面”に取り付けてください。

2.モータドライバ、電池ボックスの取り付け

下図に示す位置に、モータドライバと電池ボックスを固定します。

  • モータドライバ:M3ナット・ボルトを使用し、土台の上側に取り付ける

  • 電池ボックス:M2ナット・ボルトを使用し、土台の下側に取り付ける

ここで、電池ボックスを留めるボルトは、モバイルバッテリーが上に乗ることを考慮し、8mm程度の長さにするのがおすすめです。あまり飛び出しすぎると、バッテリー固定時に不安定になってしまいます。

3.ミニブレッドボードの取り付け

 ミニブレッドボードは底面に両面テープがあるので、それを使って貼り付けます。サイズの都合上、電源ライン(+と-の2列)を取り外して使ってください。

4.RaspberryPiの取り付け

 最後にRaspberry Piを取り付けます。土台とRaspberry Piケースを六角スタンドオフスペーサーで連結します。なお、先にモータドライバへジャンパーワイヤなどを配線しておくと、後からでも作業がしやすいです。
※Raspberry Piケースのパーツを誤って捨ててしまったため、一部3Dプリンターで代用しました。ケースが異なっていても気にしないでください。

回路設計と配線

回路を下図に示すように配線します。

プログラム

 ここでは、障害物との距離が30cmより近づくと自動的に停止する簡単なプログラムを紹介します。コードがやや長いので、次章ではファイルを分割して管理する方法も解説します。

#include <wiringPi.h>
#include <stdio.h>
#include <math.h>

#define IN1 23        // 右モータ IN1
#define IN2 24        // 右モータ IN2
#define ENA 18        // 右モータ ENA(PWM出力)

#define IN3 27        // 左モータ IN3
#define IN4 22        // 左モータ IN4
#define ENB 19        // 左モータ ENB(PWM出力)

#define ENCODER1_A 17  //右エンコーダA相
#define ENCODER1_B 4   //右エンコーダB相
#define ENCODER2_A 5   //左エンコーダA相
#define ENCODER2_B 6   //左エンコーダB相

#define TRIG 20       // 超音波センサ TRIGピン
#define ECHO 21       // 超音波センサ ECHOピン

#define MAX_PWM 900       // PWMの最大値
#define MIN_PWM 120       //PWMの最小値
#define STOP_DISTANCE 0.3  // 障害物までの停止距離 (m)

#define WHEEL_RADIUS 0.05  // 車輪半径 (m)
#define WHEEL_BASE 0.15    // 車軸幅 (m)
#define PPR 11
#define GEAR_RATIO 15.27
#define DELAY_TIME 2

int target_v=0.1; //並進速度の設定
int target_w=0; //角速度の設定

volatile int pulse_count1 = 0;
volatile int pulse_count2 = 0;
volatile int direction1 = 1;
volatile int direction2 = 1;

// エンコーダ1のパルスカウント
void pulseCounter1() {
    if (digitalRead(ENCODER1_B) == HIGH) {
        direction1 = 1;
    } else {
        direction1 = -1;
    }
    pulse_count1 += direction1;
}

// エンコーダ2のパルスカウント
void pulseCounter2() {
    if (digitalRead(ENCODER2_B) == HIGH) {
        direction2 = 1;
    } else {
        direction2 = -1;
    }
    pulse_count2 += direction2;
}

// 超音波センサで距離を計測する関数
double measureDistance() {
    digitalWrite(TRIG, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIG, LOW);

    while (digitalRead(ECHO) == LOW);
    long startTime = micros();
    while (digitalRead(ECHO) == HIGH);
    long travelTime = micros() - startTime;
    // 1秒あたりのtravelTimeを秒単位に変換
    double travelTimeInSeconds = travelTime / 1000000.0;
    return (travelTimeInSeconds*343.0) /2.0 ;  // 距離(m)
}

// モータを前進させる関数
void forward(int speed) {
    digitalWrite(IN1, HIGH);
    digitalWrite(IN2, LOW);
    pwmWrite(ENA, speed);

    digitalWrite(IN3, LOW);
    digitalWrite(IN4, HIGH);
    pwmWrite(ENB, speed);
}

// モータを後退させる関数
void back(int speed) {
    digitalWrite(IN1, LOW);
    digitalWrite(IN2, HIGH);
    pwmWrite(ENA, speed);

    digitalWrite(IN3, LOW);
    digitalWrite(IN4, HIGH);
    pwmWrite(ENB, speed);
}

// モータを左に旋回させる関数
void left(int speed) {
    digitalWrite(IN1, LOW);
    digitalWrite(IN2, HIGH);
    pwmWrite(ENA, speed);

    digitalWrite(IN3, LOW);
    digitalWrite(IN4, HIGH);
    pwmWrite(ENB, speed);
}

// モータを右に旋回させる関数
void right(int speed) {
    digitalWrite(IN1, HIGH);
    digitalWrite(IN2, LOW);
    pwmWrite(ENA, speed);

    digitalWrite(IN3, HIGH);
    digitalWrite(IN4, LOW);
    pwmWrite(ENB, speed);
}

// モータを停止させる関数
void stop() {
    digitalWrite(IN1, LOW);
    digitalWrite(IN2, LOW);
    pwmWrite(ENA, 0);

    digitalWrite(IN3, LOW);
    digitalWrite(IN4, LOW);
    pwmWrite(ENB, 0);
}

void setup() {
    wiringPiSetupGpio(); // GPIOモードを使用
    // ピンモード設定
    pinMode(TRIG, OUTPUT);
    pinMode(ECHO, INPUT);
    digitalWrite(TRIG, LOW);
    pinMode(IN1, OUTPUT);
    pinMode(IN2, OUTPUT);
    pinMode(ENA, PWM_OUTPUT);
    pinMode(IN3, OUTPUT);
    pinMode(IN4, OUTPUT);
    pinMode(ENB, PWM_OUTPUT);
    
    // エンコーダ1の設定
    pinMode(ENCODER1_A, INPUT);
    pinMode(ENCODER1_B, INPUT);
    pullUpDnControl(ENCODER1_A, PUD_UP);
    pullUpDnControl(ENCODER1_B, PUD_UP);

    // エンコーダ2の設定
    pinMode(ENCODER2_A, INPUT);
    pinMode(ENCODER2_B, INPUT);
    pullUpDnControl(ENCODER2_A, PUD_UP);
    pullUpDnControl(ENCODER2_B, PUD_UP);

    // 割り込み設定
    wiringPiISR(ENCODER1_A, INT_EDGE_RISING, &pulseCounter1);
    wiringPiISR(ENCODER2_A, INT_EDGE_RISING, &pulseCounter2);

    // PWM設定
    pwmSetMode(PWM_MODE_MS);
    pwmSetRange(1024);
    int frequency = 500;  // PWM周波数
    int clockDivisor = 54000000 / (frequency * 1024);
    pwmSetClock(clockDivisor);
    delay(30); // 初期化のための待機時間
}
int main(void) {
    setup();
    //メインループ
    while (1) {
        double distance = measureDistance();
        if (distance < STOP_DISTANCE) {
            // 障害物が近い場合は停止
            printf("障害物検出: %.2f m\n", distance);
            stop();
            break;
        } else {
            printf("障害物検出: %.2f m\n", distance);
            forward(MAX_PWM);
        }

        delay(100);  // 100ms間隔で更新
    }



    return 0;
}

ファイル分割

 先ほどのプログラムはひとつのファイルにまとめていますが、モータ制御・超音波センサ・エンコーダなどの処理をそれぞれ分割すると、再利用性や可読性が向上します。以下はファイル分割の例です。

ファイル構成

 ファイル構成を以下のようにします。新しくフォルダーを作成し、その中にメインプログラムからMakefileを作成します。

robot/
│
├── main.c           // メインプログラム
├── motor_control.c  // モータ制御関連の実装
├── motor_control.h  // モータ制御関連のヘッダファイル
├── encoder.c        // エンコーダ関連の実装
├── encoder.h        // エンコーダ関連のヘッダファイル
├── ultrasonic.c     // 超音波センサ関連の実装
├── ultrasonic.h     // 超音波センサ関連のヘッダファイル
├── config.h         //ピン番号やハードウェア設定をまとめた定義ファイル
└── Makefile         // コンパイル用のMakefile

・main.c(メインプログラム)
 main.cという名前でファイルを作成したら、そのファイルに以下のように書き込んでください。ほかのコードについても同様に行ってください。

#include <stdio.h>
#include <wiringPi.h>
#include "motor_control.h"
#include "ultrasonic.h"

const float STOP_DISTANCE=0.3;  // 障害物までの停止距離 (m)

int main(void) {
    if (wiringPiSetupGpio() == -1) {
        printf("GPIOの初期化に失敗しました\n");
        return 1;
    }

    setupMotors();
    setupUltrasonic();

    while (1) {
        double distance = measureDistance();
        if (distance < STOP_DISTANCE) {
            printf("障害物検出: %.2f m\n", distance);
            motor_R(0);
            motor_L(0);
            break;
        } else {
            printf("障害物: %.2f m\n", distance);
            motor_R(512);
            motor_L(512);
        }
        delay(100);
    }

    return 0;
}

・motor_control.c ( モータ制御関連の実装)

// motor_control.c
#include <wiringPi.h>
#include <stdlib.h>
#include "motor_control.h"
#include "config.h"

void setupMotors()
{
    pinMode(IN1, OUTPUT);
    pinMode(IN2, OUTPUT);
    pinMode(ENA, PWM_OUTPUT);

    pinMode(IN3, OUTPUT);
    pinMode(IN4, OUTPUT);
    pinMode(ENB, PWM_OUTPUT);

    // PWM設定
    pwmSetMode(PWM_MODE_MS);
    pwmSetRange(PWM_RANGE);
    int clockDivisor = 54000000 / (PWM_FREQUENCY * PWM_RANGE);
    pwmSetClock(clockDivisor);

    // 初期値を一応停止状態に
    pwmWrite(ENA, 0);
    pwmWrite(ENB, 0);
    digitalWrite(IN1, LOW);
    digitalWrite(IN2, LOW);
    digitalWrite(IN3, LOW);
    digitalWrite(IN4, LOW);
}

void motor_R(int pwm)
{
    if (pwm >= 0) {
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);
        pwmWrite(ENA, abs(pwm));
    } else {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);
        pwmWrite(ENA, abs(pwm));
    }
}

void motor_L(int pwm)
{
    if (pwm >= 0) {
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, HIGH);
        pwmWrite(ENB, abs(pwm));
    } else {
        digitalWrite(IN3, HIGH);
        digitalWrite(IN4, LOW);
        pwmWrite(ENB, abs(pwm));
    }
}

・motor_control.h (モータ制御関連のヘッダファイル)

#ifndef MOTOR_CONTROL_H
#define MOTOR_CONTROL_H

// モータの初期化(GPIOピンやPWM設定)
void setupMotors();

// 右モータの駆動:PWM値 (正→前進, 負→後進)
void motor_R(int pwm);
// 左モータの駆動:PWM値 (正→前進, 負→後進)
void motor_L(int pwm);


#endif // MOTOR_CONTROL_H

・encoder.c // エンコーダ関連の実装

// encoder.c
#include <wiringPi.h>
#include "encoder.h"
#include "config.h"

volatile int pulse_count_right = 0;
volatile int pulse_count_left  = 0;

// 右エンコーダ割り込みハンドラ
void pulseCounterRightA()
{
    if (digitalRead(ENCODER1_B) == HIGH) {
        pulse_count_right++;
    } else {
        pulse_count_right--;
    }
}

// 左エンコーダ割り込みハンドラ
void pulseCounterLeftA()
{
    if (digitalRead(ENCODER2_B) == HIGH) {
        pulse_count_left++;
    } else {
        pulse_count_left--;
    }
}

void setupEncoder()
{
    pinMode(ENCODER1_A, INPUT);
    pinMode(ENCODER1_B, INPUT);
    pullUpDnControl(ENCODER1_A, PUD_UP);
    pullUpDnControl(ENCODER1_B, PUD_UP);
    wiringPiISR(ENCODER1_A, INT_EDGE_RISING, pulseCounterRightA);

    pinMode(ENCODER2_A, INPUT);
    pinMode(ENCODER2_B, INPUT);
    pullUpDnControl(ENCODER2_A, PUD_UP);
    pullUpDnControl(ENCODER2_B, PUD_UP);
    wiringPiISR(ENCODER2_A, INT_EDGE_RISING, pulseCounterLeftA);
}

int getAndResetRightPulse()
{
    int temp = pulse_count_right;
    pulse_count_right = 0;
    return temp;
}

int getAndResetLeftPulse()
{
    int temp = pulse_count_left;
    pulse_count_left = 0;
    return temp;
}

・encoder.h // エンコーダ関連のヘッダファイル

// encoder.h
#ifndef ENCODER_H
#define ENCODER_H

extern volatile int pulse_count_right;
extern volatile int pulse_count_left;

void setupEncoder();
int getAndResetRightPulse();
int getAndResetLeftPulse();

#endif // ENCODER_H

・ultrasonic.c(超音波センサ関連の実装)

#include <wiringPi.h>
#include "ultrasonic.h"
#include "config.h"

void setupUltrasonic() {
    pinMode(TRIG, OUTPUT);
    pinMode(ECHO, INPUT);
    digitalWrite(TRIG, LOW);
}

double measureDistance() {
    digitalWrite(TRIG, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIG, LOW);

    while (digitalRead(ECHO) == LOW);
    long startTime = micros();
    while (digitalRead(ECHO) == HIGH);
    long travelTime = micros() - startTime;
    // 1秒あたりのtravelTimeを秒単位に変換
    double travelTimeInSeconds = travelTime / 1000000.0;
    return (travelTimeInSeconds*343.0) /2.0 ;  // 距離(m)
}

・ultrasonic.h(超音波センサ関連のヘッダファイル)

#ifndef ULTRASONIC_H
#define ULTRASONIC_H

void setupUltrasonic();
double measureDistance();

#endif // ULTRASONIC_H

・config.h (ピン番号やハードウェア設定をまとめた定義ファイル)

#ifndef CONFIG_H
#define CONFIG_H

// --- モータピン設定 ---
#define IN1  23
#define IN2  24
#define ENA  18  // 右モータPWM
#define IN3  27
#define IN4  22
#define ENB  19  // 左モータPWM

// --- エンコーダピン設定 ---
#define ENCODER1_A 17  // 右モータ エンコーダA相
#define ENCODER1_B 4   // 右モータ エンコーダB相
#define ENCODER2_A 5   // 左モータ エンコーダA相
#define ENCODER2_B 6   // 左モータ エンコーダB相

// --- 超音波センサピン設定 ---
#define TRIG 20
#define ECHO 21

// --- 車輪・エンコーダ設定 ---
#define WHEEL_RADIUS  0.033   // 車輪半径 [m]
#define WHEEL_BASE    0.18   // 車輪間の幅 [m]
#define PPR           11     // エンコーダ1回転あたりのパルス数
#define GEAR_RATIO    163.8  // 減速比

// --- 制御周期など ---
#define CONTROL_INTERVAL 100  // 制御周期 [ms]


// --- PWM関連 ---
#define PWM_RANGE      1024
#define PWM_FREQUENCY  500

#endif // CONFIG_H

・Makefile(コンパイル用のMakefile)

CC = gcc
CFLAGS = -Wall -lwiringPi

TARGET = main

OBJS = main.o motor_control.o encoder.o ultrasonic.o

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

main.o: main.c config.h motor_control.h encoder.h ultrasonic.h
	$(CC) -c main.c $(CFLAGS)

motor_control.o: motor_control.c motor_control.h config.h
	$(CC) -c motor_control.c $(CFLAGS)

encoder.o: encoder.c encoder.h config.h
	$(CC) -c encoder.c $(CFLAGS)

ultrasonic.o: ultrasonic.c ultrasonic.h config.h
	$(CC) -c ultrasonic.c $(CFLAGS)

clean:
	rm -f *.o $(TARGET)



複数ファイルに分割することで、不要なコードを読み飛ばせたり、保守や機能追加がしやすくなったりします。

プログラムの実行

  • 単一ファイルの場合
    いつも通りビルドのタブから「Build」を選択し、「Execute」で実行します。

  • ファイル分割した場合
    それぞれのファイルを同じディレクトリに置き、Makefileがある状態で、main.cを開いたままビルドのタブから「メイク」を選択します。エラーがなければ「Execute」でプログラムを実行できます。

プログラムが動作すると、最初に挙げた動画のように障害物に近づくと停止する動きを確認できます。

まとめ

 以上、Raspberry Piで作る二輪車両ロボットの製作例と、超音波センサーを活用した自動停止機能についてご紹介しました。3Dプリンターでロボットの土台を作ったり、モータやセンサーを組み合わせたりする作業は大変でしたが、そのぶん完成したときの達成感は格別でした。ぜひ皆さんもロボット製作に挑戦してみてください!

いいなと思ったら応援しよう!

この記事が参加している募集