ガイド · 読了まで19分

Arduinoで障害物を避けるロボット:HC-SR04とサーボのガイド

Arduinoで障害物を避けるロボットを作ります。サーボでHC-SR04を振るスキャン、L298NやL293Dの配線、止まる・下がる・曲がるロジック、赤外線センサー、Bluetoothの手動モードまで解説します。

障害物を避けるロボットは、前方の壁、椅子、箱にぶつかる前に気づき、新しい道を見つけて進む自律走行のロボットカーです。最もよくある構成は、Arduino Uno、HC-SR04超音波距離センサー、センサーを左右に向けるSG90サーボモーター、ギア付きDCモーター2個、そしてL298N(またはL293D)モータードライバです。ロボットはまっすぐ進み、障害物に近づくと止まり、少し下がって、頭を左右に向けて距離を測り、最も開けた方向へ進みます。

このガイドでは、Arduinoのロボットカーの記事で作った2WDの車を、同じピン配置のまま自律ロボットに変えます。配線表、距離のしきい値の計算方法、3段階で育てていくコード、センサーが見えない面とその穴を赤外線センサーで埋める方法、そして最後にスマホからBluetoothで手動モードに切り替える方法を、ステップごとに解説します。同じロボットをESP32やRaspberry Pi Picoで動かすコードは記事の最後にあります。

HC-SR04超音波距離センサー
HC-SR04:Tと書かれた振動子が40 kHzの音を出し、Rと書かれた振動子が反響を聞き取ります。これが障害物を避けるロボットの目で、約15度の狭い角度の中で2〜400 cmを測ります。

障害物を避けるロボットはどう動く?

どの自律ロボットも、同じ3段階のループを1秒間に何十回も繰り返しています。感知、判断、行動です。感知を担うのはHC-SR04です。耳には聞こえない40 kHzの短い音を出し、反響が戻るまでの時間を測ります。その時間(マイクロ秒)を58で割ると、距離がセンチメートルで求まります。判断を担うのはArduinoで、測った距離を設定したしきい値と比べます。行動はL298Nとモーターが担当します。センサーがサーボの上にあるので、ロボットは体の向きを変えずに左右を「見る」ことができ、センサーが固定されたロボットよりずっとよい曲がり方を判断できます。センサーの仕組みの詳細はHC-SR04距離センサーの記事にあります。

部品役割備考
Arduino Uno(またはNano)頭脳:測り、判断し、モーターに命令するESP32やPicoでも可
HC-SR04目:前方の障害物までの距離を測る5 Vで動作、2〜400 cm
SG90サーボ首:センサーを左右に向けるセンサーはプラスチックのブラケットでサーボのホーンに固定する
L298NまたはL293Dシールド筋肉:モーターに方向と速度を与えるArduinoのピンではモーターの電流を供給できない
ギア付きTTモーター2個とキャスター差動駆動2つの車輪を逆方向に回して曲がる
18650セル × 2(7.4 V)電源9 V電池では足りない
赤外線障害物センサー2個(任意)角や側面の障害物を捉えるFC-51などLM393を使ったモジュール
HC-05(任意)スマホからの手動操作Androidのみ対応
Arduino Uno
Arduino Uno
L298N
L298N
TTモーター
TTモーター
18650リチウムイオン電池
18650リチウムイオン電池

電源:サーボでロボットをリセットさせない

障害物を避けるロボットはモーターにサーボが加わるので、普通のロボットカーより電源の配分が重要です。SG90は静止時に約10 mAを消費しますが、動き出した瞬間に100〜250 mAに跳ね上がり、負荷がかかると0.5 A近くになります。この突入電流がモーターの起動電流と重なると、Arduinoの5 Vラインが一瞬落ちてボードが再起動します。解決法は簡単です。モーターは電池からL298Nを通して、ArduinoはVINを通して電池から動かし、サーボ、HC-SR04、赤外線センサー、HC-05はArduinoの5Vピンを共有します。そしてサーボの電源線の近くに470 µFのコンデンサを置きます。サーボが動いている間でも5 Vラインの合計負荷は0.5 A未満にとどまり、一瞬のことなのでUnoのレギュレーターで対応できます。自分の数値で電池の持ちを計算するには電池の持続時間ツールを使ってください。

部分電源の供給元一般的な電流
TTモーター2個L298Nの12V端子 ← 18650 × 2(7.4 V)走行中0.3〜0.6 A、起動時は最大2 A
Arduino UnoVIN ← 電池のプラス(スイッチの後)約50 mA
SG90サーボArduinoの5V + 470 µFコンデンサ静止時約10 mA、動作中100〜250 mA
HC-SR04Arduinoの5V約15 mA
赤外線センサー2個Arduinoの5V1個あたり20〜40 mA
HC-05Arduinoの5Vペアリング中30〜40 mA、接続後約10 mA
GNDはすべてつないでください(電池のマイナス、L298N、Arduino、サーボ、センサー)。共通グランドがないと、センサーがでたらめな距離を読み、サーボが震えます。サーボをモーターと同じ細いジャンパーワイヤーで給電しないでください。MG90SやMG996Rのような強力なサーボを使う場合は、Arduinoからではなく、5 Vに設定した別の降圧モジュール(たとえばLM2596)から給電します。

配線図:Arduino Uno、ESP32、Pico

モーターのピンはロボットカーのガイドと同じなので、車を分解せずにセンサーヘッドを追加するだけでこのプロジェクトに進めます。速度のピンを5と6にしたのには理由があります。UnoではServoライブラリがTimer1を使うため、9番と10番ピンのPWMが無効になります。モーターの速度をこれらのピンにすると、サーボを接続した途端に速度制御が効かなくなります。HC-SR04と赤外線センサーはA0〜A3につなぎます。Unoのアナログピンは必要に応じてデジタルピン14〜19としても使えます。この選び方なら、L293Dシールドでも同じコードを使いやすくなります。シールドはこれらのピンを使わないからです。ボードのピンはピン配置ツールで確認できます。

接続Arduino UnoESP32Raspberry Pi Pico
L298N ENA(左モーターの速度)D5GPIO 25GP15
L298N IN1、IN2D7、D8GPIO 27、33GP14、GP13
L298N ENB(右モーターの速度)D6GPIO 26GP10
L298N IN3、IN4D9、D4GPIO 32、4GP12、GP11
サーボの信号(オレンジ)D3GPIO 13GP16
サーボの+(赤)、−(茶)5V、GNDL298Nの5V、GNDL298Nの5V、GND
HC-SR04 TRIGA0GPIO 18GP17
HC-SR04 ECHOA1(直結)GPIO 19、1 kΩ / 2 kΩの分圧回路を通すGP18、1 kΩ / 2 kΩの分圧回路を通す
HC-SR04 VCC5VL298Nの5VL298Nの5V
左の赤外線OUT、右の赤外線OUTA2、A3GPIO 23、22GP19、GP20
赤外線センサーのVCC5V3V33V3
HC-05 TXD、RXDD10、D11(RXDは分圧回路を通す)不要(Bluetooth内蔵)GP1、GP0(UART0)
ボードの電源VIN ← 電池(スイッチの後)5V(VIN) ← L298Nの5VVSYS ← L298Nの5V
HC-SR04のECHOピンは5 Vの信号を出しますが、ESP32とPicoのピンは3.3 Vで動作し、5 Vには耐えられません。ECHO → 1 kΩ → ボードへ行く点 → 2 kΩ → GNDという形で分圧回路を作ります。5 V × 2 / 3 ≈ 3.3 Vです。TRIGには分圧回路は不要で、3.3 Vの信号でセンサーをトリガーできます。ESP32とPicoでは、ボード、サーボ、HC-SR04が同じL298Nの5V出力を共有するので、この小さなレギュレーターの約0.5 Aの上限に近づくことがあります。リセットが起きる場合は、サーボを別の5 V電源から給電してください。

センサーヘッドの取り付けとテスト

SG90マイクロサーボモーター
SG90:4.8 Vで約0.1秒に60度回る、重さ9グラムのマイクロサーボです。HC-SR04を支えるプラスチックのブラケットをサーボのホーンにねじ止めします。
  1. ホーンを付けずにサーボをつなぎ、下のテスト用スケッチを書き込みます。コードは毎回のスイープの最後にサーボを90度に動かして2秒待ちます。その間にUSBケーブルを抜くと、サーボはその位置で止まります。センサーがまっすぐ前を向くようにホーンを軸に取り付けます。こうすると90度が「正面」になります。
  2. HC-SR04をプラスチックのブラケットかホットボンドでホーンに固定します。センサーの配線は、サーボが回っても突っ張らない長さが必要です。140度のスイープ中にケーブルがシャーシに引っかかるのはよくある不具合です。
  3. センサーは床から8〜12 cmの高さで、車体の最も前に近い位置に取り付けます。低すぎると床やカーペットの凹凸からの反響を拾い、高すぎると低い障害物が見えません。
  4. センサーを3〜5度上向きに傾けます。完全に水平で低い位置のセンサーは、特に凹凸のある床で、床からの反響を障害物と間違えることがあります。
  5. シリアルモニタを9600ボーで開きます。センサーの左側に手をかざしたときに125度と160度の行で小さな値が出れば、左側が大きい角度です。逆の場合は、発展版のスケッチでSERVO_DIRを-1にし、最初のスケッチではLEFT_ANGLEとRIGHT_ANGLEの値を入れ替えます。
C++ (Arduino IDE)
1#include <Servo.h>
2
3const int SERVO_PIN = 3;
4const int TRIG = A0, ECHO = A1;        // analog pinler dijital pin olarak / analog pins used as digital pins
5Servo head;
6
7int pingCm() {
8  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
9  digitalWrite(TRIG, HIGH); delayMicroseconds(10);   // 10 µs tetik darbesi / 10 µs trigger pulse
10  digitalWrite(TRIG, LOW);
11  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);    // en fazla 25 ms bekle (~4,3 m) / wait at most 25 ms (~4.3 m)
12  return t / 58;                                     // 0 = yankı yok / 0 = no echo
13}
14
15void setup() {
16  Serial.begin(9600);
17  pinMode(TRIG, OUTPUT);
18  pinMode(ECHO, INPUT);
19  head.attach(SERVO_PIN);
20  head.write(90);                                    // önce ortala / center first
21  delay(1000);
22}
23
24void loop() {
25  const int angles[] = {20, 55, 90, 125, 160};
26  for (int a : angles) {
27    head.write(a);
28    delay(400);                                      // servo yerine otursun / let the servo settle
29    Serial.print(a);
30    Serial.print(F(" derece / degrees: "));
31    Serial.print(pingCm());
32    Serial.println(F(" cm"));
33  }
34  Serial.println();
35  head.write(90);
36  delay(2000);
37}

各行には角度とその方向の距離が表示されます。センサーから約4メートル以内に何もなければ0 cmと表示されます。25 ms以内に反響が届かないとpulseIn()は0を返すからです。メインのコードでは0を「道が空いている」として扱いますが、配線が緩んでいたりTRIGとECHOが逆だったりするセンサーもたいてい0を返します。そのため、ロボットを床に置く前に10、30、60 cmの位置に手をかざし、値が本当に変わるか確認してください。このテストをせずに走らせたロボットは、減速せずに壁へ突っ込みます。

判断のアルゴリズム:止まる、下がる、最も開けた方向へ曲がる

ロボットの振る舞いを距離によってゾーンに分けると、コードがシンプルで確実になります。前に障害物がなければ全速で進み、障害物が近づくにつれて減速し、停止のしきい値に達したら止まってスキャンします。スキャン中、センサーは左に70度と35度、右に35度と70度を見ます。各方向で3回測り、その中央値を使います。ロボットは最も距離が長い方向へ、その角度だけ曲がります。どの方向も開いていない場合、たとえば角に入り込んだときは、少し下がって180度回ります。

測った距離ゾーンロボットの動作
60 cm超(または反響なし)空き全速前進(PWM 200)
25〜60 cm接近距離に比例して減速(PWM 200から130まで)
12〜25 cm障害物止まり、値を確認し、左右をスキャンし、最も開けた方向へ曲がる
12 cm未満近すぎるまず350 ms下がってからスキャンする
すべての方向が35 cm未満行き止まり下がって180度回る

これらのしきい値は適当に選んだものではありません。停止距離は3つの余裕の合計です。反応の余裕(2回の測定の間に進む距離)、ブレーキの余裕(モーターを止めてから惰性で進む距離)、旋回の余裕(その場で回るときに鼻先が障害物に触れないため)です。6 VのTTモーターの車は約0.5〜0.7 m/sで進み、60 msの測定間隔で3〜4 cm進みます。ブレーキの余裕は約5 cm、その場で回る幅15 cmの車体なら旋回の余裕は約10 cmです。合計すると約18〜20 cmになり、そこに数センチの安全マージンを足すと、STOP_CM = 25がよい出発点です。ロボットが速い場合や床が滑りやすい場合は、しきい値を30〜35 cmに上げてください。逆に狭い廊下では、しきい値が大きすぎるとロボットがしょっちゅう止まってスキャンするので、最終的な調整は自分のコースで行います。

ステップごとのコード1:最初の障害物を避けるロボット

最初のバージョンはアルゴリズムの骨組みです。前に進み、25 cmで止まり、下がり、左右を見て、より開けた側へ曲がります。モーターの関数drive(left, right)はロボットカーのガイドと同じで、-255〜255の値を取ります。プラスは前進、マイナスは後退です。コードを書き込んだら、まず車輪が浮くように車を箱の上に置き、センサーに手を近づけたときにどう止まってどう曲がるかを観察します。問題がなさそうならロボットを床に置きます。

C++ (Arduino IDE)
1#include <Servo.h>
2
3const int ENA = 5, IN1 = 7, IN2 = 8;            // sol motor / left motor
4const int ENB = 6, IN3 = 9, IN4 = 4;            // sağ motor / right motor
5const int SERVO_PIN = 3;
6const int TRIG = A0, ECHO = A1;
7
8const int STOP_CM = 25;                         // bu mesafede dur / stop at this distance
9const int SPEED = 170;                          // düz gidiş hızı / cruising speed
10const int TURN = 170;                           // dönüş hızı / turning speed
11const int LEFT_ANGLE = 160, RIGHT_ANGLE = 20;   // ters ise yer değiştir / swap if reversed
12
13Servo head;
14
15void drive(int left, int right) {               // her biri -255 … 255 / each -255 … 255
16  digitalWrite(IN1, left > 0);  digitalWrite(IN2, left < 0);
17  digitalWrite(IN3, right > 0); digitalWrite(IN4, right < 0);
18  analogWrite(ENA, abs(left));
19  analogWrite(ENB, abs(right));
20}
21
22int pingCm() {
23  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
24  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
25  digitalWrite(TRIG, LOW);
26  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);
27  if (t == 0) return 400;                       // yankı yok: önü açık say / no echo: treat as open
28  return t / 58;
29}
30
31int lookAt(int angle) {
32  head.write(angle);
33  delay(400);                                   // servo dönsün ve dursun / let the servo turn and settle
34  return pingCm();
35}
36
37void setup() {
38  int pins[] = {ENA, IN1, IN2, ENB, IN3, IN4, TRIG};
39  for (int p : pins) pinMode(p, OUTPUT);
40  pinMode(ECHO, INPUT);
41  drive(0, 0);
42  head.attach(SERVO_PIN);
43  head.write(90);
44  delay(3000);                                  // yere koyman için süre / time to put it on the floor
45}
46
47void loop() {
48  int front = pingCm();
49  if (front > STOP_CM) {
50    drive(SPEED, SPEED);                        // yol açık: ileri / path clear: forward
51    delay(60);                                  // ölçümler arası 60 ms / 60 ms between readings
52    return;
53  }
54
55  drive(0, 0);                                  // 1) dur / 1) stop
56  delay(150);
57  drive(-SPEED, -SPEED);                        // 2) biraz geri çekil / 2) back off a little
58  delay(300);
59  drive(0, 0);
60
61  int left = lookAt(LEFT_ANGLE);                // 3) iki yana bak / 3) look both ways
62  int right = lookAt(RIGHT_ANGLE);
63  head.write(90);
64  delay(300);
65
66  if (left < STOP_CM && right < STOP_CM) {      // iki yan kapalı: geri dön / both blocked: turn around
67    drive(-TURN, TURN);
68    delay(700);
69  } else if (left > right) {                    // 4) daha açık tarafa dön / 4) turn to the more open side
70    drive(-TURN, TURN);
71    delay(350);
72  } else {
73    drive(TURN, -TURN);
74    delay(350);
75  }
76  drive(0, 0);
77  delay(100);
78}

このコードでも動きますが、いくつか弱点があります。1回の誤った測定でロボットが無駄に止まる、障害物にいつも同じ速度で近づく、2方向しか見ない、旋回時間が角度によらない、という点です。またdelay()で待つので、スキャン中は何にも反応できません。待ち方の考え方の詳細はmillis()とdelay()の記事にあります。次のバージョンでこうした問題のほとんどを解決します。

ステップごとのコード2:フィルター、段階的な減速、赤外線、Bluetooth

発展版は同じ配線で動き、赤外線センサーやHC-05をまだ付けていなくても問題ありません。赤外線の入力をINPUT_PULLUPで設定しているので、つないでいないセンサーは「障害物なし」(HIGH)を読みます。HC-05がなければ、ロボットはそのまま自動モードで始まります。下の表で、コードの各部分が何をしているかを説明します。

C++ (Arduino IDE)
1#include <Servo.h>
2#include <SoftwareSerial.h>
3
4// Pinler / Pins
5const int ENA = 5, IN1 = 7, IN2 = 8;     // sol motor / left motor
6const int ENB = 6, IN3 = 9, IN4 = 4;     // sağ motor / right motor
7const int SERVO_PIN = 3;
8const int TRIG = A0, ECHO = A1;
9const int IR_LEFT = A2, IR_RIGHT = A3;   // LOW = engel (takılı değilse HIGH) / LOW = obstacle (HIGH if not fitted)
10SoftwareSerial bt(10, 11);               // RX, TX → HC-05 TXD, RXD (RXD bölücüyle / RXD via divider)
11
12// Ayarlar / Settings
13const int BACK_CM = 12;                  // daha yakınsa geri çekil / closer: back off first
14const int STOP_CM = 25;                  // dur ve tara / stop and scan
15const int SLOW_CM = 60;                  // yavaşlamaya başla / start slowing down
16const int GUARD_CM = 15;                 // manuel modda çarpışma koruması / collision guard in manual mode
17const int MAX_CM = 400;
18const int CRUISE = 200, SLOW = 130, TURN = 170;   // PWM hızları / PWM speeds
19const int SERVO_CENTER = 90;             // sensör öne bakmıyorsa düzelt / adjust if not facing ahead
20const int SERVO_DIR = 1;                 // +70 sola bakmıyorsa -1 yap / set to -1 if +70 doesn't look left
21const float MS_PER_DEG = 4.0;            // derece başına ms, kalibre et / ms per degree, calibrate
22const unsigned long BT_TIMEOUT = 600;    // manuelde komut kesilirse dur / stop if commands stop
23
24Servo head;
25bool autoMode = true;
26int manualLeft = 0, manualRight = 0, manualSpeed = 200;
27unsigned long lastCmd = 0, lastPing = 0;
28
29void drive(int left, int right) {        // her biri -255 … 255 / each -255 … 255
30  left = constrain(left, -255, 255);
31  right = constrain(right, -255, 255);
32  digitalWrite(IN1, left > 0);  digitalWrite(IN2, left < 0);
33  digitalWrite(IN3, right > 0); digitalWrite(IN4, right < 0);
34  analogWrite(ENA, abs(left));
35  analogWrite(ENB, abs(right));
36}
37
38int pingCm() {
39  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
40  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
41  digitalWrite(TRIG, LOW);
42  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);  // ~4,3 m zaman aşımı / ~4.3 m timeout
43  return t == 0 ? MAX_CM : (int)(t / 58);          // yankı yok = açık alan / no echo = open space
44}
45
46int distanceCm() {                       // 3 ölçümün ortancası / median of 3 readings
47  int a = pingCm(); delay(60);
48  int b = pingCm(); delay(60);
49  int c = pingCm();
50  return max(min(a, b), min(max(a, b), c));
51}
52
53bool irBlocked(int pin) {
54  return digitalRead(pin) == LOW;
55}
56
57int lookAt(int offset) {                 // + sol, - sağ (derece) / + left, - right (degrees)
58  head.write(SERVO_CENTER + SERVO_DIR * offset);
59  delay(300);                            // servo yerine otursun / let the servo settle
60  return distanceCm();
61}
62
63void turnBy(int deg) {                   // + sola, - sağa / + left, - right
64  if (deg > 0) drive(-TURN, TURN);
65  else drive(TURN, -TURN);
66  delay((unsigned long)(abs(deg) * MS_PER_DEG));
67  drive(0, 0);
68  delay(100);
69}
70
71void avoid() {
72  drive(0, 0);                           // 1) dur / 1) stop
73  delay(120);
74  int front = distanceCm();              // ortancayla doğrula / confirm with the median
75  bool sides = irBlocked(IR_LEFT) || irBlocked(IR_RIGHT);
76  if (front >= STOP_CM && !sides) return;          // yanlış alarm / false alarm
77
78  if (front < BACK_CM || sides) {        // 2) çok yakın: geri çekil / 2) too close: back off
79    drive(-TURN, -TURN);
80    delay(350);
81    drive(0, 0);
82  }
83
84  const int offsets[] = {70, 35, -35, -70};        // 3) tara / 3) scan
85  int bestOffset = 0, bestCm = 0;
86  for (int off : offsets) {
87    int cm = lookAt(off);
88    if (cm > bestCm) { bestCm = cm; bestOffset = off; }
89  }
90  head.write(SERVO_CENTER);
91  delay(200);
92
93  if (bestCm < STOP_CM + 10) {           // her yön kapalı: çıkmaz / every way blocked: dead end
94    drive(-TURN, -TURN);
95    delay(400);
96    turnBy(180);
97  } else {
98    turnBy(bestOffset);                  // 4) en açık yöne dön / 4) turn to the most open direction
99  }
100}
101
102void autoStep() {
103  if (millis() - lastPing < 60) return;  // 60 ms'de bir ölç / measure every 60 ms
104  lastPing = millis();
105  int d = pingCm();
106  bool l = irBlocked(IR_LEFT), r = irBlocked(IR_RIGHT);
107
108  if (d < STOP_CM || (l && r)) {
109    avoid();
110  } else if (l) {                        // sol köşede engel: sağa kaç / front-left: dodge right
111    drive(TURN, -TURN);
112    delay(150);
113  } else if (r) {                        // sağ köşede engel: sola kaç / front-right: dodge left
114    drive(-TURN, TURN);
115    delay(150);
116  } else if (d < SLOW_CM) {
117    int s = map(d, STOP_CM, SLOW_CM, SLOW, CRUISE);   // yaklaştıkça yavaşla / slow down as you get closer
118    drive(s, s);
119  } else {
120    drive(CRUISE, CRUISE);
121  }
122}
123
124void manualStep() {
125  if (millis() - lastCmd > BT_TIMEOUT) {           // komut yok: dur / no commands: stop
126    manualLeft = 0;
127    manualRight = 0;
128  }
129  static int front = MAX_CM;
130  if (millis() - lastPing >= 60) {
131    lastPing = millis();
132    front = pingCm();
133  }
134  bool forward = manualLeft > 0 && manualRight > 0;
135  if (forward && front < GUARD_CM) drive(0, 0);    // çarpışma koruması / collision guard
136  else drive(manualLeft, manualRight);
137}
138
139void handleBluetooth() {
140  while (bt.available()) {
141    char c = bt.read();
142    Serial.println(c);                   // gelen harfi göster / show the incoming letter
143    lastCmd = millis();
144    if (c == 'A') {                      // otomatik mod / auto mode
145      autoMode = true;
146      continue;
147    }
148    if (c == 'M') {                      // manuel mod / manual mode
149      autoMode = false;
150      manualLeft = manualRight = 0;
151      drive(0, 0);
152      head.write(SERVO_CENTER);
153      continue;
154    }
155    if (autoMode) continue;              // otomatikte yön harflerini yok say / ignore in auto mode
156    int s = manualSpeed;
157    switch (c) {
158      case 'F': manualLeft = s;  manualRight = s;  break;   // ileri / forward
159      case 'B': manualLeft = -s; manualRight = -s; break;   // geri / backward
160      case 'L': manualLeft = -s; manualRight = s;  break;   // yerinde sola / spin left
161      case 'R': manualLeft = s;  manualRight = -s; break;   // yerinde sağa / spin right
162      case 'S': manualLeft = 0;  manualRight = 0;  break;   // dur / stop
163      default:
164        if (c >= '0' && c <= '9') manualSpeed = map(c - '0', 0, 9, 100, 240);   // hız kademesi / speed level
165    }
166  }
167}
168
169void setup() {
170  int outs[] = {ENA, IN1, IN2, ENB, IN3, IN4, TRIG};
171  for (int p : outs) pinMode(p, OUTPUT);
172  drive(0, 0);
173  pinMode(ECHO, INPUT);
174  pinMode(IR_LEFT, INPUT_PULLUP);
175  pinMode(IR_RIGHT, INPUT_PULLUP);
176  Serial.begin(9600);
177  bt.begin(9600);                        // HC-05 veri modu hızı / HC-05 data mode baud rate
178  head.attach(SERVO_PIN);
179  head.write(SERVO_CENTER);
180  delay(3000);                           // yere koyman için süre / time to put it on the floor
181}
182
183void loop() {
184  handleBluetooth();
185  if (autoMode) autoStep();
186  else manualStep();
187}
関数または設定役割
pingCm()1回測定する。25 ms以内に反響がなければ400 cm(空間が開いている)を返す
distanceCm()60 ms間隔で3回測り、中央値を返す。外れ値を1つ除外できる
autoStep()60 msごとに測定し、距離と赤外線センサーに応じて進む、減速する、よける、またはavoid()を呼ぶ
avoid()止まり、値を確認し、必要なら下がり、4方向を見て最も開けた方向へ曲がる
turnBy(degrees)ロボットをその場で指定の角度だけ回す。時間はMS_PER_DEGで計算する
manualStep()スマホから送られた方向を実行する。前進中は15 cmで止まり、600 msコマンドが来なければ止まる
SERVO_CENTER、SERVO_DIRセンサーが正面を向く角度と、どちらが左か
STOP_CM、SLOW_CM、BACK_CM停止、減速、後退のしきい値

障害物を避けるロボットには、平均より中央値のフィルターが向いています。24、26、400 cmのような3つの値の平均は150 cmになってロボットを惑わせますが、中央値なら26 cmを選びます。さらに詳しく知りたい場合はセンサーデータのフィルタリングの記事を参照してください。自動モードと手動モードの切り替えも、実は小さなステートマシンです。この考え方はArduinoのステートマシンのガイドで詳しく解説しています。コードを書き込んだら、次の順番でロボットを自分の部屋に合わせて調整します。

  1. サーボの中心:平らな壁にロボットを正対させたとき、センサーがまっすぐ壁を向いていなければ、SERVO_CENTERを85や95のように少しずつ変えます。
  2. 旋回時間:一時的にsetup()の最後にturnBy(360);の行を追加し、ロボットがちょうど1回転するか観察します。360度ではなく300度しか回らなければ、MS_PER_DEGを4.0 × 360 / 300 = 4.8にします。この値は電池や床によって変わるので、満充電の電池で、ロボットが走る床の上で調整してください。
  3. 停止のしきい値:ロボットを壁に向かって走らせ、止まった位置で鼻先が壁からどれだけ離れているか測ります。10 cm未満なら、STOP_CMを5 cm増やします。
  4. 低速の速度:SLOWがモーターの起動しきい値より低いと、ロボットは接近ゾーンで止まってしまいます。その場合はSLOWを150に上げます。

HC-SR04の死角:柔らかい面、斜めの壁、細い障害物

超音波センサーは音をよく反射する面は見えますが、すべての障害物が音をセンサーに返すわけではありません。メーカーは、よい結果を得るには0.5 m²以上のできるだけ平らな対象を推奨しています。しかし実際の部屋では、ロボットはカーテン、ソファ、椅子の脚、壁の角に出くわします。音はビリヤードの球のように、当たった角度で跳ね返ります。壁に正対したセンサーは強い反響を受けますが、視線が垂直から約30〜40度以上ずれると、音は別の方向へそれて戻ってきません。その場合pulseIn()は0を返し、コードは道が空いていると判断します。これがロボットが壁をこする最もよくある原因です。

障害物または面見えない理由対策
カーテン、ソファ、クッション、衣類、ペットの毛柔らかく多孔質な面が音を吸収し、反響がとても弱くなる近距離用に赤外線センサーを追加し、ロボットをゆっくり走らせる
壁に斜めに近づく音がそれて反射し、反響がセンサーに届かない前の角に外向きの赤外線センサーを付け、より細かい角度でスキャンする
椅子や机の脚、細い柱反射する面がとても小さく、反響が弱い速度を下げ、中央値のフィルターを使い、側面に赤外線センサーを付ける
センサーより低い障害物(ケーブル、敷居、スリッパ)音の円錐の下に隠れる下向きの赤外線センサーか、前のバンパーにマイクロスイッチを付ける
机の端、階段超音波センサーは前を向いているので、床の切れ目が見えない床に向けたTCRT5000で落下を検知する
2 cmより近い物センサーの不感帯:音を出し終わる前に反響が戻ってくる12 cmで下がるようにロボットを設定する
角や狭い廊下音が2つの壁の間で跳ね返って遅れて戻り、距離が長く見える中央値のフィルターと複数の角度からの測定
1つのセンサーが見えないものを別のセンサーで補うほうが、1つのセンサーを完璧にしようとするより常に効果的です。ただし最初にやるべきことは減速です。0.3 m/sで走るロボットは、同じ誤った測定のあとでも0.7 m/sのロボットほど強くぶつからず、2回目の測定をする時間も残ります。さまざまな距離センサーの長所と短所はArduinoの距離センサーの比較の記事にまとめています。

赤外線センサーを追加する

超音波センサーと赤外線センサーは、互いの弱点を補い合います。FC-51のような赤外線障害物センサーは、赤外線LEDと受光部でできています。反射光が十分に強いと、LM393コンパレーターが出力をLOWにし、基板上のLEDが点きます。検知距離は半固定抵抗(トリマー)で約2〜30 cmに調整できますが、距離を測るのではなく「障害物あり」か「なし」かを知らせるだけです。ロボットの前の角に、30〜45度外向きに赤外線センサーを2個付けると、超音波センサーの狭い円錐から外れる椅子の脚や斜めの壁を捉えます。発展版のコードでは、左の赤外線が反応するとロボットは少し右へよけ、右が反応すると左へよけます。両方が同時に反応したら、前に障害物があるときと同じように止まってスキャンします。

項目HC-SR04(超音波)FC-51(赤外線障害物)TCRT5000(赤外線反射)
出力距離(cm)あり・なし(デジタル)アナログ値とデジタル出力
範囲2〜400 cm約2〜30 cm(調整可能)約1〜15 mm
検知角度約15°約35°とても狭い
弱点柔らかい面、斜めの面、細い面黒くつやのない面、日光高さの変化、日光
よく見えるものガラス、黒い物、大きく平らな面布、角、近くの障害物床の色と机の端
ロボット上の位置メインの目、サーボで振る前の角前のバンパーの下、床に向けて
TCRT5000赤外線反射センサー
TCRT5000:床から5〜10 mmの高さに下向きに付けると、机の端や階段に気づきます。反射が消えた瞬間に、ロボットは止まって下がれます。

机の上を走るロボットにとって、最も危険な障害物は実は段差です。前のバンパーの下に床向きのTCRT5000モジュールを付ければ、床がなくなった瞬間がわかります。モジュールのデジタル出力を、Unoでまだ空いているD2につなぎます。まずシリアルモニタで床がないときに出力がどちらのレベルになるかを確認し、autoStep()の一番最初でこのピンを読み、床がなければすぐにモーターを止めてロボットを下がらせます。この確認は距離の測定より先に行う必要があります。落ちるほうがぶつかるより常に被害が大きいからです。

Bluetoothで手動モードに切り替える

自律ロボットを手で運転したいこともあります。部屋から部屋へ移動させたいとき、障害物の前に連れて行って反応を試したいとき、あるいはただ遊びたいときです。発展版のコードのHC-05の接続でそれが可能になります。スマホからAの文字を送るとロボットは自動モードに、Mを送ると手動モードに切り替わります。手動モードではロボットカーのガイドと同じ操作の文字(F、B、L、R、S)を使いますが、違いが1つあります。前進中に15 cmより近い障害物を見つけると、命令に関係なく止まります。この衝突ガードのおかげで、手動運転でも自律ロボットのセンサーの恩恵を受けられます。HC-05のペアリング手順とATコマンドはHC-05 Bluetoothモジュールの記事にあります。

文字動作どのモードで?
A自動モードに切り替える両方のモード
M手動モードに切り替え、ロボットを止める両方のモード
F / B前進 / 後退手動
L / Rその場で左 / 右に回る手動
S停止手動
0〜9速度レベル(PWM 100〜240)手動
  1. HC-05のTXDピンをD10に、RXDピンを1 kΩ / 2 kΩの分圧回路を通してD11に、VCCを5Vに、GNDをGNDにつなぎます。
  2. AndroidスマホのBluetooth設定で、PIN 1234(うまくいかなければ0000)でHC-05とペアリングします。
  3. PlayストアからBluetoothカーのリモコンアプリをインストールし、HC-05に接続します。ArduinoをUSBにつないだままシリアルモニタを9600ボーで開くと、コードはスマホから届いたすべての文字を表示します。
  4. 多くのリモコンアプリにはライトやクラクションなどの追加ボタンがあり、それぞれ独自の文字を送ります。シリアルモニタでその文字を調べ、コードの'A'と'M'の代わりに入れてください。ボタンを自分で決めたい場合は、App InventorでArduinoとBluetoothのガイドで自分のリモコンを作れます。
  5. USBケーブルを抜いてスイッチを入れます。3秒後にロボットは自動モードで走り出します。手動モードに切り替えて、スマホから運転してみましょう。
Unoでは、SoftwareSerialが1バイトを受信する間、約1 msほかの割り込みを止めます。その間にServoライブラリのパルスが伸び、センサーヘッドが少しピクッと動くことがあります。ロボットの動作には影響しませんが、気になる場合はESP32の内蔵Bluetoothに移れます。HC-05とHC-06はiPhoneでは使えません。iPhoneにはBLEモジュールか、BLE用にプログラムしたESP32が必要です。なお、技適マークのないBluetoothモジュールの使用は、日本では電波法で制限されています。

L293Dモーターシールドで作る

L293Dモータードライバシールド
L293Dモーターシールド:Arduino Unoに直接挿せます。4個のDCモーターをM1〜M4の端子に、2個のサーボを角のピンヘッダーにつなぎます。

配線がごちゃごちゃするのが嫌なら、L298Nの代わりに、Arduino Unoに直接挿せるL293Dモーターシールド(Adafruit Motor Shield V1とその互換品)を使えます。シールドは4個のDCモーターを駆動でき、サーボ用のピンヘッダーが2つあり、サーボにはArduinoの5 Vラインから給電します。モーターはArduinoのピンに直接ではなく、基板上の74HC595シフトレジスタを通してつながっているので、ライブラリマネージャからインストールしたAdafruit Motor Shield library(V2用ではなくV1用)、つまりAFMotorで動かします。シールドはモーターにD3、D4、D5、D6、D7、D8、D11、D12を、サーボにD9とD10を使い、A0〜A5、D2、D13は空いたままです。HC-SR04と赤外線センサーはすでにA0〜A3にあるので、変更が必要なのはモーターのコード、サーボのピン、HC-05のピンだけです。

設定L298Nの場合L293Dシールドの場合
モーターの接続OUT1〜OUT4端子M1(左)とM2(右)の端子
モーターのコードdrive()内のdigitalWriteとanalogWritedrive()内のAFMotor
SERVO_PIN3サーボを挿すヘッダーに応じて9か10
HC-05D10、D11A4(RX)とA5(TX):SoftwareSerial bt(A4, A5)
1チャンネルあたりの電流連続2 A連続0.6 A、ピーク1.2 A
モーターの電源12V端子EXT_PWR端子。PWRジャンパーを付けるとArduinoも同じ電源で動く
C++ (Arduino IDE)
1// L293D shield: ENA–IN4 tanımlarını ve setup()'taki pinMode listesinden bu pinleri sil, drive() fonksiyonunu aşağıdakiyle değiştir
2// L293D shield: delete the ENA–IN4 definitions, remove them from the pinMode list in setup() and replace drive() with the one below
3// Ayrıca / Also: SERVO_PIN = 9 ya da / or 10, SoftwareSerial bt(A4, A5)
4#include <AFMotor.h>                     // Adafruit Motor Shield V1 kütüphanesi / library
5
6AF_DCMotor leftMotor(1);                 // M1 klemensi / M1 terminal
7AF_DCMotor rightMotor(2);                // M2 klemensi / M2 terminal
8
9void drive(int left, int right) {        // her biri -255 … 255 / each -255 … 255
10  leftMotor.setSpeed(min(abs(left), 255));
11  rightMotor.setSpeed(min(abs(right), 255));
12  leftMotor.run(left > 0 ? FORWARD : (left < 0 ? BACKWARD : RELEASE));
13  rightMotor.run(right > 0 ? FORWARD : (right < 0 ? BACKWARD : RELEASE));
14}

L293Dの1チャンネルあたり0.6 Aという上限は、TTモーターの2WDロボットの通常走行には十分です。しかし車輪が障害物を押して止まると、モーターは1 A以上を引くことがあり、チップがすぐに熱くなります。そのため、障害物に押し当たる前にロボットを止めるスキャンのアルゴリズムは、シールドの寿命も延ばします。シールドのジャンパー設定と4モーターの車のスケッチはL293Dモーターシールドの記事に、L298Nの内部の仕組みはL298NでDCモーターを制御するガイドにあります。

ESP32で障害物を避けるロボット

ESP32では大きな違いが2つあります。1つ目は、ArduinoのServoライブラリがESP32では動かないので、ボードのPWMユニットであるLEDCで直接サーボの信号を作ることです。50 Hzの信号で0.544〜2.4 msのパルスが0〜180度に対応します。モーターは1 kHz、サーボは50 Hzで動くので、モーターのチャンネルを先に、サーボのチャンネルをあとに設定し、サーボが専用のタイマーを使えるようにします。2つ目は、ESP32はクラシックBluetoothを内蔵しているので、HC-05が不要なことです。ロボットはスマホに「BlueGrays-Robot」として表示され、方向とモードの文字はUno版と同じです。コードはESP32 Arduinoコアのバージョン3.x用(ledcAttachとledcWriteはピン番号で動作)で、クラシックBluetoothが必要なため、ESP32-S3やESP32-C3ではコンパイルできません。

C++ (Arduino IDE)
1// ESP32 Arduino çekirdeği 3.x / ESP32 Arduino core 3.x
2#include "BluetoothSerial.h"
3
4const int ENA = 25, IN1 = 27, IN2 = 33;   // sol motor / left motor
5const int ENB = 26, IN3 = 32, IN4 = 4;    // sağ motor / right motor
6const int SERVO_PIN = 13;
7const int TRIG = 18, ECHO = 19;           // ECHO 1 kΩ / 2 kΩ bölücüyle / ECHO via a 1 kΩ / 2 kΩ divider
8const int IR_LEFT = 23, IR_RIGHT = 22;    // LOW = engel / LOW = obstacle
9
10const int BACK_CM = 12, STOP_CM = 25, SLOW_CM = 60, GUARD_CM = 15, MAX_CM = 400;
11const int CRUISE = 200, SLOW = 130, TURN = 170;
12const int SERVO_CENTER = 90, SERVO_DIR = 1;
13const float MS_PER_DEG = 4.0;             // kalibre et / calibrate
14
15BluetoothSerial bt;
16bool autoMode = true;
17int manualLeft = 0, manualRight = 0;
18unsigned long lastCmd = 0, lastPing = 0;
19
20void drive(int left, int right) {         // her biri -255 … 255 / each -255 … 255
21  left = constrain(left, -255, 255);
22  right = constrain(right, -255, 255);
23  digitalWrite(IN1, left > 0);  digitalWrite(IN2, left < 0);
24  digitalWrite(IN3, right > 0); digitalWrite(IN4, right < 0);
25  ledcWrite(ENA, abs(left));
26  ledcWrite(ENB, abs(right));
27}
28
29void servoWrite(int angle) {              // 0–180° → 544–2400 µs
30  angle = constrain(angle, 0, 180);
31  uint32_t us = map(angle, 0, 180, 544, 2400);
32  ledcWrite(SERVO_PIN, us * 16384 / 20000);   // 14 bit, 20 ms periyot / 14-bit, 20 ms period
33}
34
35int pingCm() {
36  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
37  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
38  digitalWrite(TRIG, LOW);
39  unsigned long t = pulseIn(ECHO, HIGH, 25000UL);
40  return t == 0 ? MAX_CM : (int)(t / 58);
41}
42
43int distanceCm() {                        // 3 ölçümün ortancası / median of 3 readings
44  int a = pingCm(); delay(60);
45  int b = pingCm(); delay(60);
46  int c = pingCm();
47  return max(min(a, b), min(max(a, b), c));
48}
49
50bool irBlocked(int pin) { return digitalRead(pin) == LOW; }
51
52void turnBy(int deg) {                    // + sola, - sağa / + left, - right
53  if (deg > 0) drive(-TURN, TURN);
54  else drive(TURN, -TURN);
55  delay((unsigned long)(abs(deg) * MS_PER_DEG));
56  drive(0, 0);
57  delay(100);
58}
59
60void avoid() {
61  drive(0, 0);
62  delay(120);
63  int front = distanceCm();
64  bool sides = irBlocked(IR_LEFT) || irBlocked(IR_RIGHT);
65  if (front >= STOP_CM && !sides) return;            // yanlış alarm / false alarm
66  if (front < BACK_CM || sides) {                    // geri çekil / back off
67    drive(-TURN, -TURN);
68    delay(350);
69    drive(0, 0);
70  }
71  const int offsets[] = {70, 35, -35, -70};
72  int bestOffset = 0, bestCm = 0;
73  for (int off : offsets) {
74    servoWrite(SERVO_CENTER + SERVO_DIR * off);
75    delay(300);
76    int cm = distanceCm();
77    if (cm > bestCm) { bestCm = cm; bestOffset = off; }
78  }
79  servoWrite(SERVO_CENTER);
80  delay(200);
81  if (bestCm < STOP_CM + 10) {                       // çıkmaz / dead end
82    drive(-TURN, -TURN);
83    delay(400);
84    turnBy(180);
85  } else {
86    turnBy(bestOffset);
87  }
88}
89
90void setup() {
91  int outs[] = {IN1, IN2, IN3, IN4, TRIG};
92  for (int p : outs) pinMode(p, OUTPUT);
93  pinMode(ECHO, INPUT);
94  pinMode(IR_LEFT, INPUT_PULLUP);
95  pinMode(IR_RIGHT, INPUT_PULLUP);
96  ledcAttach(ENA, 1000, 8);               // motor PWM: 1 kHz, 8 bit
97  ledcAttach(ENB, 1000, 8);
98  ledcAttach(SERVO_PIN, 50, 14);          // servo: 50 Hz, 14 bit
99  drive(0, 0);
100  servoWrite(SERVO_CENTER);
101  bt.begin("BlueGrays-Robot");            // telefonda görünecek ad / name shown on the phone
102  delay(3000);
103}
104
105void loop() {
106  while (bt.available()) {
107    char c = bt.read();
108    lastCmd = millis();
109    if (c == 'A') autoMode = true;                                  // otomatik / auto
110    else if (c == 'M') {                                            // manuel / manual
111      autoMode = false;
112      manualLeft = manualRight = 0;
113      servoWrite(SERVO_CENTER);
114    }
115    else if (!autoMode) {
116      if (c == 'F')      { manualLeft = 200;  manualRight = 200; }  // ileri / forward
117      else if (c == 'B') { manualLeft = -200; manualRight = -200; } // geri / backward
118      else if (c == 'L') { manualLeft = -200; manualRight = 200; }  // sola / left
119      else if (c == 'R') { manualLeft = 200;  manualRight = -200; } // sağa / right
120      else if (c == 'S') { manualLeft = 0;    manualRight = 0; }    // dur / stop
121    }
122  }
123
124  if (millis() - lastPing < 60) return;               // 60 ms'de bir / every 60 ms
125  lastPing = millis();
126  int d = pingCm();
127
128  if (!autoMode) {
129    if (millis() - lastCmd > 600) manualLeft = manualRight = 0;     // komut yok: dur / no command: stop
130    bool forward = manualLeft > 0 && manualRight > 0;
131    if (forward && d < GUARD_CM) drive(0, 0);                       // çarpışma koruması / collision guard
132    else drive(manualLeft, manualRight);
133    return;
134  }
135
136  bool l = irBlocked(IR_LEFT), r = irBlocked(IR_RIGHT);
137  if (d < STOP_CM || (l && r)) avoid();
138  else if (l) { drive(TURN, -TURN); delay(150); }
139  else if (r) { drive(-TURN, TURN); delay(150); }
140  else if (d < SLOW_CM) { int s = map(d, STOP_CM, SLOW_CM, SLOW, CRUISE); drive(s, s); }
141  else drive(CRUISE, CRUISE);
142}

Raspberry Pi Picoで障害物を避けるロボット(MicroPython)

Raspberry Pi Picoでは、MicroPythonのtime_pulse_us()関数がArduinoのpulseIn()のように働きます。反響が届かないと負の値を返すので、それを400 cm、つまり空間が開いているとして扱います。サーボには50 HzのPWMと、パルス幅をナノ秒で受け取るduty_ns()を使います。GP16のサーボとGP15、GP10のモーターの速度のピンは別々のPWMスライスにあるので、互いの周波数を乱しません。今回は速度をパーセントで指定します。PicoにはBluetoothがないので、手動モードが必要ならロボットカーのガイドと同じようにHC-05をUART0(GP0、GP1)につなげます。

MicroPython
1from machine import Pin, PWM, time_pulse_us
2import time
3
4ena = PWM(Pin(15)); ena.freq(1000)          # sol motor hızı / left motor speed
5enb = PWM(Pin(10)); enb.freq(1000)          # sağ motor hızı / right motor speed
6in1, in2 = Pin(14, Pin.OUT), Pin(13, Pin.OUT)
7in3, in4 = Pin(12, Pin.OUT), Pin(11, Pin.OUT)
8servo = PWM(Pin(16)); servo.freq(50)        # servo sinyali, 50 Hz / servo signal, 50 Hz
9trig = Pin(17, Pin.OUT, value=0)
10echo = Pin(18, Pin.IN)                      # 1 kΩ / 2 kΩ bölücü üzerinden / via a 1 kΩ / 2 kΩ divider
11ir_left = Pin(19, Pin.IN, Pin.PULL_UP)      # 0 = engel / 0 = obstacle
12ir_right = Pin(20, Pin.IN, Pin.PULL_UP)
13
14STOP_CM, BACK_CM = 25, 12
15CRUISE, TURN = 75, 65                       # hız yüzdesi / speed in percent
16MS_PER_DEG = 4                              # kalibre et / calibrate
17SERVO_CENTER = 90                           # öne bakmıyorsa düzelt / adjust if not facing ahead
18
19def drive(left, right):                     # -100 … 100 (%)
20    in1.value(left > 0); in2.value(left < 0)
21    in3.value(right > 0); in4.value(right < 0)
22    ena.duty_u16(int(min(abs(left), 100) * 655))
23    enb.duty_u16(int(min(abs(right), 100) * 655))
24
25def head(offset):                           # + sol, - sağ / + left, - right
26    angle = max(0, min(180, SERVO_CENTER + offset))
27    servo.duty_ns((544 + angle * 1856 // 180) * 1000)   # 0,544–2,4 ms darbe / 0.544–2.4 ms pulse
28
29def ping_cm():
30    trig.value(1); time.sleep_us(10); trig.value(0)     # 10 µs tetik / 10 µs trigger
31    t = time_pulse_us(echo, 1, 25000)
32    return 400 if t < 0 else t // 58                    # negatif = yankı yok / negative = no echo
33
34def distance_cm():                          # 3 ölçümün ortancası / median of 3 readings
35    r = []
36    for _ in range(3):
37        r.append(ping_cm())
38        time.sleep_ms(60)
39    return sorted(r)[1]
40
41def turn_by(deg):                           # + sola, - sağa / + left, - right
42    if deg > 0:
43        drive(-TURN, TURN)
44    else:
45        drive(TURN, -TURN)
46    time.sleep_ms(int(abs(deg) * MS_PER_DEG))
47    drive(0, 0)
48    time.sleep_ms(100)
49
50def avoid():
51    drive(0, 0)
52    time.sleep_ms(120)
53    front = distance_cm()
54    sides = not ir_left.value() or not ir_right.value()
55    if front >= STOP_CM and not sides:
56        return                              # yanlış alarm / false alarm
57    if front < BACK_CM or sides:            # geri çekil / back off
58        drive(-TURN, -TURN); time.sleep_ms(350); drive(0, 0)
59    best_off, best_cm = 0, 0
60    for off in (70, 35, -35, -70):          # tara / scan
61        head(off)
62        time.sleep_ms(300)
63        cm = distance_cm()
64        if cm > best_cm:
65            best_off, best_cm = off, cm
66    head(0)
67    time.sleep_ms(200)
68    if best_cm < STOP_CM + 10:              # çıkmaz / dead end
69        drive(-TURN, -TURN); time.sleep_ms(400)
70        turn_by(180)
71    else:
72        turn_by(best_off)
73
74drive(0, 0)
75head(0)
76time.sleep(3)                               # yere koyman için süre / time to put it down
77while True:
78    d = ping_cm()
79    left_hit, right_hit = not ir_left.value(), not ir_right.value()
80    if d < STOP_CM or (left_hit and right_hit):
81        avoid()
82    elif left_hit:
83        drive(TURN, -TURN); time.sleep_ms(150)   # sağa kaç / dodge right
84    elif right_hit:
85        drive(-TURN, TURN); time.sleep_ms(150)   # sola kaç / dodge left
86    else:
87        drive(CRUISE, CRUISE)
88    time.sleep_ms(60)                        # ölçümler arası / between readings

障害物を避けるロボットのプロジェクトアイデア

  • レーダー表示:スキャンで得た角度と距離をシリアルポートで送り、パソコンに緑色のレーダー画面を描きます。詳しくはArduinoのレーダープロジェクトの記事にあります。
  • ライン + 障害物:ライントレースロボットのようにコースをたどりながら、途中の箱をよけてラインに戻ります。競技会でよくある課題です。
  • 壁に沿って走るロボット:センサーを右に60度向けたまま、壁から一定の20 cmを保って走ります。迷路を解くための第一歩です。
  • ディスプレイ付きロボット:距離とモードを小さなOLEDディスプレイに表示すると、デバッグがずっと楽になります。
  • ロボット掃除機の試作:ランダムな方向選択、バンパースイッチ、落下センサーで家の中を動き回る掃除ロボットの原型を作ります。
  • スマート白杖:同じ距離測定のロジックを、視覚障害のある人向けの振動による警告に変えます。Arduinoのスマート白杖を参照してください。
  • 学校や競技会のプロジェクト:障害物コース、タイム計測、自動・手動モードのデモと一緒に発表します。アイデアはArduinoの科学コンテスト向けプロジェクトアイデアの記事を見てください。

よくある間違いと解決法

問題考えられる原因解決法
ロボットが止まらずに壁へぶつかるセンサーが0を読み(断線、TRIGとECHOが逆)、コードがそれを空き空間として扱っているテスト用スケッチを動かし、手で10〜60 cmを試し、TRIGとECHOのピンを確認する
斜めに近づくと壁をこする音が斜めの面で別の方向へ反射している前の角に赤外線センサーを付け、速度を下げる
スキャンのために止まるとArduinoがリセットされるサーボとモーターの電流で5 Vラインが落ちているサーボに470 µFのコンデンサ、満充電の18650セル。必要ならサーボを別の5 V電源から給電する
ロボットが理由もなく止まってスキャンする1回の誤った測定、または床からの反響センサーを8〜12 cmに上げて少し上向きにし、中央値での確認を使う
ロボットがふさがった側へ曲がるサーボが逆向きに付いていて、左右が入れ替わっているSERVO_DIRを-1にする
旋回が短すぎる、または長すぎるMS_PER_DEGが調整されていない、または電池が少ないturnBy(360)で測り、満充電の電池で調整し直す
角で左右に曲がり続ける両側ともしきい値に近く、判断が入れ替わり続けている行き止まりのしきい値を上げる(STOP_CM + 10)か、最後に曲がった方向を優先させる
センサーヘッドがピクピク動くサーボの電源が弱い、またはSoftwareSerialの割り込みサーボのケーブルを短くし、コンデンサを追加し、Bluetoothのコマンドを必要以上に送らない
問題の原因を最も早く見つける方法は、ロボットをUSBにつないだまま車輪を浮かせて動かし、pingCm()の値をシリアルモニタに表示することです。値が妥当なら、問題は機械か電源の側にあります。Arduinoが再起動を繰り返すそのほかの原因はArduinoがリセットを繰り返すの記事を参照してください。

障害物を避けるロボットは、センサーの読み取り、フィルタリング、判断、モーター制御を1つのプロジェクトにまとめているので、ロボット工学への最適な入口です。まず最初のスケッチで基本の動きを観察し、次に発展版でしきい値を自分の部屋に合わせ、最後に赤外線センサーとBluetoothでロボットを強化しましょう。BlueGraysアプリでは、障害物を避けるロボット、L298NのBluetooth操作カー、HC-SR04での距離測定、駐車センサーのプロジェクトを、回路図とコード付きでステップごとに紹介しています。

よくある質問

障害物を避けるロボットを作るには何が必要?

基本の障害物を避けるロボットには、Arduino Uno、HC-SR04超音波センサー、SG90サーボ、L298NかL293Dのモータードライバ、ギア付きDCモーター2個の2WDシャーシキット、18650セル2本が必要です。角を見るために赤外線障害物センサーを2個、スマホで運転するためにHC-05 Bluetoothモジュールを追加することもできます。

サーボなしで障害物を避けるロボットは作れる?

はい。センサーは正面を向けておきます。障害物を見つけたら、まず一方へ曲がって測り直し、まだふさがっていれば反対へ曲がります。このほうが安上がりですが、試すたびに車体全体を回す必要があるので判断が遅くなります。サーボがあれば、曲がる前に最も開けた方向を見つけられます。

HC-SR04が一部の障害物を見えないのはなぜ?

超音波センサーは反響で測ります。カーテンやソファのような柔らかい面は音を吸収し、斜めの壁は音を別の方向に反射し、椅子の脚のような細い物はとても弱い反響しか返しません。また、約15度の円錐の外や下にある障害物も見えません。こうした穴は赤外線センサーで補えます。

障害物を避けるロボットはどのくらいの距離で止まるべき?

25 cmがよい初期値です。2回の測定の間に進む距離、ブレーキの距離、その場で回るのに必要な余裕を合わせると約20 cmになり、そこに小さな安全マージンを加えた値です。速いロボットや滑りやすい床では30〜35 cmに上げます。障害物が12 cmより近い場合、ロボットはまず下がるべきです。

ロボットにはL298NとL293Dシールドのどちらがいい?

L298Nは1チャンネルあたり2 Aを供給できるので、より頑丈で、ピンを自由に選べます。L293DシールドはUnoに挿せるので配線が減り、4個のモーターを駆動できますが、連続電流は1チャンネルあたり0.6 Aで、多くのピンを使います。TTモーターの小さな2WDロボットなら、どちらでも大丈夫です。

障害物を避けるロボットはスマホから操作できる?

はい。HC-05 Bluetoothモジュールを追加し、Androidスマホのリモコンアプリから文字を送って、自動モードと手動モードを切り替えます。手動モードで超音波センサーを衝突ガードとして使えば、ロボットを壁にぶつけずに済みます。ESP32なら内蔵Bluetoothがあるので、別のモジュールは不要です。

全プロジェクトを見る
HC-SR04で距離を測定(シリアルモニタ表示)
#27初級

HC-SR04で距離を測定(シリアルモニタ表示)

HC-SR04センサーは、コウモリが暗闇で道を見つけるのと同じ仕組みで動作します。人には聞こえない音を出し、その反響を捉えて前方の物体までの距離を測ります。Arduinoは0.5秒ごとに距離をセンチメートル単位でコンピューターの画面に表示し、3〜100センチメートルの測定値を示します。駐車センサーや障害物を避けるロボットなどのプロジェクトの基礎になります。

4.6(46)
71
障害物回避ロボット
#81上級
プレミアム

障害物回避ロボット

自分で進む道を見つける、賢い車を作ってみませんか?障害物回避ロボットは、前面の超音波センサーで周囲を常に調べます。障害物を見つけると停止し、サーボモーターで左右を確認して、最も障害物の少ない方向を選んで進みます。自動運転の基礎を学ぶのにぴったりの入門プロジェクトです。

5.0(38)
49
L298Nで作るBluetooth操作の車
#80上級
プレミアム

L298Nで作るBluetooth操作の車

スマートフォンをリモコンにして、自作のロボットカーを操作できます。ArduinoはBluetoothモジュールを通じてスマートフォンのリモコンアプリから指令を受け取り、モータードライバーで車輪を回します。前進、後退、左右や斜めへの移動ができ、速度もスマートフォンから調整できます。停止ボタンを押すと、短いブレーキ動作でモーターがすぐに止まります。

5.0(38)
65
サーボモーターの使い方
#35初級
プレミアム

サーボモーターの使い方

小型サーボモーターのアームが、一方の端、中央、もう一方の端の3つの位置へ順番に回り、それぞれの位置で2秒間止まります。Arduinoが保持する角度をモーターに指示すると、モーターはその位置まで動いて止まります。ロボットアームや自動開閉するふたが動く仕組みを学べます。

4.7(48)
67
HC-SR04で作る駐車センサー
#28中級
プレミアム

HC-SR04で作る駐車センサー

車の駐車センサーを小さく再現します。物体が近づくほど、ビープ音の間隔が短くなります。距離センサーが前方の物体までの距離を測り、20センチメートルより遠ければ緑のLEDが点灯します。近づくと赤いLEDが点滅してブザーが鳴り、さらに近づくほど警告の間隔が短くなります。車が壁にぶつからずに駐車できる仕組みを、机の上で確かめられます。

4.8(64)
85
TCRT5000で反射(近接)を読み取る
#77中級
プレミアム

TCRT5000で反射(近接)を読み取る

目に見えない光を出し、その反射量を測る小さなセンサーで、前方の表面を検出できます。物体や明るい色の表面にセンサーを近づけるほど戻ってくる光が増え、Arduinoがその値を0.5秒ごとに画面に数値で表示します。ライントレースロボットや非接触式の水栓には、このようなセンサーが使われています。

4.6(33)
43
TCRT5000で床面の白黒を判別
#78中級
プレミアム

TCRT5000で床面の白黒を判別

ライントレースロボットの目となる小さなセンサーで、床面が黒か白かを判別できます。TCRT5000センサーは目に見えない光を床面に当て、戻ってくる光の量を調べます。白い面では緑のLED、黒い面では赤のLEDが点灯します。自分でロボットを作る前に学んでおきたい大切なステップです。

4.7(40)
49
ESP32とHC-SR04で作る超音波距離計
#116中級

ESP32とHC-SR04で作る超音波距離計

HC-SR04超音波センサーは、発した音波が物体に反射して戻るまでの時間(Time of Flight)を測定して距離を計算します。このプロジェクトでは、ESP32からセンサーのTRIGピンに10マイクロ秒のパルスを送り、ECHOピンからの反射波の持続時間をpulseIn()で読み取ります。計算した距離をcm単位で16x2 I2C LCDに表示します。HC-SR04は5Vで動作するため、ECHOピンには分圧回路が必須です。分圧しないとESP32の3.3V GPIOが破損します。

4.9(28)
45