前回は超音波センサを使って障害物の回避プログラムを作成しました。
今回は光センサを使って黒い線を読みながら自動で走行するライントレースプログラムを行ってみます。

光センサの概要
皆さんが使っているスマートフォンは、暗い場所に行くと画面が明るくなり、逆に晴れた日の外では画面が暗くなったりします。
これはスマートフォンに内蔵されている「照度センサー」の働きによるものです。
照度センサーは、周囲の明るさを感知して自動で画面の明るさを調整する仕組みを持っています。
今回使用する照度センサーには「フォトトランジスタ」という部品が使われています。
フォトトランジスタは、光に反応する「フォトダイオード」と、微小な電流を増幅する「トランジスタ」を組み合わせたものです。
下の図にあるように、フォトトランジスタは受けた光の明るさに応じて電流を増幅する性質を持っています。
販売元リンク: 照度センサー販売ページ

フォトトランジスタのしくみ
フォトダイオードは光を受けるとわずかな電圧を発生させます。
そしてトランジスタはそのわずかな電圧をもとに、別の電源から大きな電圧を作り出す性質があります。
この2つを組み合わせることで、フォトトランジスタは受ける光の明るさに応じて大きな電圧を生じさせることができます。
これにより、周囲の明るさに応じた信号を作り出すことができます。

照度センサー(フォトトランジスター) 550nm NJL7302L-F5
LEDを使った回路の仕組み
今回の回路では、フォトトランジスタの出力に応じてLEDが発光するようになっています。
下の回路図を見てください。

この回路図では、LEDと並列に可変抵抗が接続されています。
可変抵抗の抵抗値を大きくすると、LEDに流れる電流が増えます。
また、抵抗値が大きくなるとピンにかかる電圧が減少します。
この可変抵抗を回すことで、ライントレースの動作を調整することが出来ます。
黒と白の判定
さらに、黒色は光を吸収し、白色は光を反射する性質があります。
この性質を利用して、地面の色が黒か白かを判定することができます。
これにより、照度センサーを活用したライントレースが可能になります。
照度センサーを試してみよう
下部基板には2つのライントレース用の照度センサーが搭載されています。
これを使って車体下の色を認識してみましょう。
この照度センサーはRPi PicoのADC0とADC1に接続されています。
(RPi Picoにはもう1つADCポートがありますので、他の用途でADCを追加で使いたい場合はそちらを利用できます。)
from machine import Pin, ADC
import time
led = Pin("LED", Pin.OUT) # LEDのピン設定
photoref = ADC(0) # ADC0を使用(括弧内の数字はGPIO番号ではありません)
# LEDの状態を1(オン)に初期化
led.value(1)
def read_sensor():
# センサーの値を0〜1に正規化して読み取る
photoref_out = photoref.read_u16() / 65535 # 16ビットの範囲を0〜1に変換
print(photoref_out)
while True:
read_sensor() # センサーの値を読み取る
time.sleep(0.05) # 50ミリ秒待機
16ビットの入力
照度センサーからの入力は16ビット(0〜65535)の範囲で返ってきます。
この値を0〜1の範囲に変換(正規化)して使いやすくしています。
これにより、出力値を0〜1で直感的に把握することができます。
出力の確認
プログラムを実行すると、0〜1の値が連続して表示されるはずです。
これが照度センサーが読み取った明るさの割合です。
もし値が全く変化しない場合は、どこかに不具合がある可能性があります。
値に応じた電流が前方の2つのLEDに流れており、LEDの明るさは電気的に変化しますが、この部分の変化はプログラムでは制御していません。
問題1
前回学習したLCDに文字を表示するプログラムをライブラリ化し、ラインセンサの値をLCDに表示するプログラムを作成してください。
解答例はこちら
ライブラリ:lcd_library.py
from machine import Pin, I2C
import ssd1306
import time
class lcd_display:
def __init__(self):
# I2C通信の初期化
self.i2c = I2C(0, sda=Pin(20), scl=Pin(21), freq=400000)
# 接続されているI2Cデバイスをスキャン
addr = self.i2c.scan()
if not addr:
raise ValueError(f"I2Cデバイスが見つかりません。接続を確認してください。指定されたアドレス: {hex(address)}")
# OLEDディスプレイの初期化
try:
self.oled = ssd1306.SSD1306_I2C(128, 64, self.i2c, addr=0x3C)
except Exception as e:
raise RuntimeError(f"OLEDの初期化に失敗しました: {e}")
# 初期化時に画面をクリア
self.clear()
def clear(self, sleep_time=1):
"""OLEDディスプレイをクリアします。"""
time.sleep(sleep_time)
self.oled.fill(0)
self.oled.show()
メインプログラム:main.py
from machine import Pin, ADC, I2C
import ssd1306
import time
from lcd_library import lcd_display
lcd_instance = lcd_display()
led = Pin("LED", Pin.OUT) # LEDのピン設定
photoref_left = ADC(1) # ADC0を使う. ()内はGPIOの番号ではない
photoref_right = ADC(0) # ADC1を使う. ()内はGPIOの番号ではない
# LEDの状態を1(オン)に初期化
led.value(1)
def read_sensor():
# センサー下の明度をパーセント表示する
photoref_out_left = photoref_left.read_u16() / 65535 # 0〜1の範囲に正規化
photoref_out_right = photoref_right.read_u16() / 65535 # 0〜1の範囲に正規化
# タプル形式で値をまとめて返す
return (photoref_out_left, photoref_out_right)
try:
while True:
lcd_instance.clear(0.5)
data = read_sensor() # センサーの値を読み取る
lcd_instance.oled.text('left :' + str(data[0]), 0, 16) # センサーの値を位置(0, 16)に表示
lcd_instance.oled.text('right:' + str(data[1]), 0, 32) # センサーの値を位置(0, 32)に表示
lcd_instance.oled.show() # ディスプレイを更新する関数
time.sleep(0.05) # 50ミリ秒待機
finally:
# OLEDディスプレイを黒で塗りつぶす
lcd_instance.oled.fill(0)
lcd_instance.oled.show()
ライントレースの概要
次に、ライントレースについて説明します。ライントレースとは、走行するロボットが地面の黒線を認識して、その上を進む動作のことです。YouTubeなどで、黒い線に沿って走行するロボットの動画を見たことがあるかもしれませんが、それがライントレースの一例です。
ライントレースの仕組みは、使うセンサーの数によって異なりますが、今回は左右に2つの照度センサーを使用します。この2つのセンサーは、黒と白の境界を検知し、ロボットの動きを制御します。例えば、左側のセンサーが白を検知した場合、左側のタイヤを進行させます。逆に黒を検知した場合は左のタイヤを反転させます。同様に、右側のセンサーも制御することで、地面の色を認識しながらジグザグに走行することができます。
2つの照度センサーを使ってライントレースを行う
以下のコードは、左右の照度センサーから値を取得し、それに応じてモーターを制御する基本的なプログラムです。
センサーの値を取得するプログラム
左右の照度センサーから値を受け取り, タプル形式で値を返します。
from machine import Pin, ADC
import utime
led = Pin("LED", Pin.OUT)
# LED の状態を 1 (オン) に初期化します.
led.value(1)
# パーセンテージの閾値
Thr_num_left = 0.55
Thr_num_right = 0.55
# ピンの入出力設定
photoref_left = ADC(1) # ADC0を使う. ()内はGPIOの番号ではない
photoref_right = ADC(0) # ADC1を使う. ()内はGPIOの番号ではない
# モーター用のピンの入出力設定
MotorL_plus = Pin(0, Pin.OUT)
MotorL_minus = Pin(1, Pin.OUT)
MotorR_plus = Pin(2, Pin.OUT)
MotorR_minus = Pin(3, Pin.OUT)
# センサーが取得する明るさ(=明度)を読み取る
def read_sensor():
# センサー下の明度をパーセント表示する
photoref_out_left = photoref_left.read_u16() / 65535 # 0〜1の範囲に正規化
photoref_out_right = photoref_right.read_u16() / 65535 # 0〜1の範囲に正規化
# タプル形式で値をまとめて返す
return (photoref_out_left, photoref_out_right)
# 1秒停止
utime.sleep(1)
try:
while True:
utime.sleep(0.1)
# 取得したセンサーからのデータを格納する
data = read_sensor()
finally:
stop()
モーターを制御するプログラム
次に、取得したセンサーの値に応じてモーターを制御します。これがライントレースの基本です。
以下のプログラムを参考に、先ほどのプログラムに追記してプログラムを完成させましょう。
from machine import Pin, ADC
import utime
~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~
# 左側のモーターを前進させる関数
def left_go():
MotorL_plus.value(1)
MotorL_minus.value(0)
# 右側のモーターを前進させる関数
def right_go():
MotorR_plus.value(1)
MotorR_minus.value(0)
# 左側のモーターを後退させる関数
def left_back():
MotorL_plus.value(0)
MotorL_minus.value(1)
# 右側のモーターを後退させる関数
def right_back():
MotorR_plus.value(0)
MotorR_minus.value(1)
# モーターを停止する関数
def stop():
MotorL_plus.value(1)
MotorL_minus.value(1)
MotorR_plus.value(1)
MotorR_minus.value(1)
# 1秒待機
utime.sleep(1)
try:
while True:
utime.sleep(0.1) # 0.1秒待機
data = read_sensor() # 照度センサーの値を取得
print(data[0], data[1]) # センサーの値を確認
# 左のセンサーが閾値より低い(黒を認識)場合
if data[0] <= Thr_num_left:
left_go() # 左のタイヤを前進
else:
left_back() # 左のタイヤを後退
# 右のセンサーが閾値より低い(黒を認識)場合
if data[1] <= Thr_num_right:
right_go() # 右のタイヤを前進
else:
right_back() # 右のタイヤを後退
# プログラムが終了する際にモーターを停止
finally:
stop()
実行上の注意点
- 閾値と可変抵抗の調整
照度センサーから読み取った明るさの値(0〜1の範囲)を基に黒と白を判別するために、閾値を適切に設定してください。
また、可変抵抗は電気的に入力される電圧を調整するものなので、最初に可変抵抗を調整して黒と白の差が明確になるように設定しましょう。
その後、閾値を調整して正しく動作するようにします。 - 動作確認
正しく認識されるようになったら、プログラムをRPi Picoに書き込み、USBケーブルを外してライントレースのフィールドに配置してください。

問題2
ライントレース中に障害物を検出したら停止するプログラムを作成してください。
前方の物体との距離を測定し、その距離に応じてロボットが前進するか停止するか判断します。
前方の距離を測定する仕組みは、前回の障害物回避の内容を活用できます。
解答例はこちら
こちらのプログラムはロボットの走行にライブラリは使用しておりませんので、ライブラリを使用したものに書き換えてください。
from machine import Pin, ADC
import utime
led = Pin("LED", Pin.OUT)
# LED の状態を 1 (オン) に初期化します.
led.value(1)
trigger = Pin(5, Pin.OUT) # GPIO 5番ピンをトリガーとして設定
echo = Pin(4, Pin.IN) # GPIO 4番ピンをエコーとして設定
# パーセンテージの閾値
Thr_num_left = 0.55
Thr_num_right = 0.55
# ピンの入出力設定
photoref_left = ADC(1) # ADC0を使う. ()内はGPIOの番号ではない
photoref_right = ADC(0) # ADC1を使う. ()内はGPIOの番号ではない
# モーター用のピンの入出力設定
MotorL_plus = Pin(0, Pin.OUT)
MotorL_minus = Pin(1, Pin.OUT)
MotorR_plus = Pin(2, Pin.OUT)
MotorR_minus = Pin(3, Pin.OUT)
# センサーが取得する明るさ(=明度)を読み取る
def read_sensor():
# センサー下の明度をパーセント表示する
photoref_out_left = photoref_left.read_u16() / 65535 # 0〜1の範囲に正規化
photoref_out_right = photoref_right.read_u16() / 65535 # 0〜1の範囲に正規化
# タプル形式で値をまとめて返す
return (photoref_out_left, photoref_out_right)
# 1秒停止
# 左側のモーターを前進させる関数
def left_go():
MotorL_plus.value(1)
MotorL_minus.value(0)
# 右側のモーターを前進させる関数
def right_go():
MotorR_plus.value(1)
MotorR_minus.value(0)
# 左側のモーターを後退させる関数
def left_back():
MotorL_plus.value(0)
MotorL_minus.value(1)
# 右側のモーターを後退させる関数
def right_back():
MotorR_plus.value(0)
MotorR_minus.value(1)
# モーターを停止する関数
def stop():
MotorL_plus.value(1)
MotorL_minus.value(1)
MotorR_plus.value(1)
MotorR_minus.value(1)
def read_distance():
trigger.low() # トリガーをLowにして準備
utime.sleep_us(2) # 2マイクロ秒待機
trigger.high() # トリガーをHighにして超音波を発信
utime.sleep(0.00001) # 10マイクロ秒間超音波を発信
trigger.low() # 再度トリガーをLowにする
# エコーが反応を返すまでの時間を計測
while echo.value() == 0: # エコー信号が来るのを待つ
signaloff = utime.ticks_us() # 反応なしの時間を記録
while echo.value() == 1: # エコー信号が来るのを待つ
signalon = utime.ticks_us() # 反応が返ってきた時間を記録
# 反応時間を計算し、距離を求める
timepassed = float(signalon) - float(signaloff)
distance = (timepassed * 0.0343) / 2 # 距離を計算(音速は約0.0343 cm/μs、片道なので2で割る)
# 測定した距離を表示
return distance
try:
while True:
utime.sleep(0.1) # 0.1秒待機
data = read_sensor() # 照度センサーの値を取得
distance1=read_distance()
print(data[0], data[1],distance1) # センサーの値を確認
# 左のセンサーが閾値より低い(黒を認識)場合
if data[0] <= Thr_num_left:
left_go() # 左のタイヤを前進
else:
left_back() # 左のタイヤを後退
# 右のセンサーが閾値より低い(黒を認識)場合
if data[1] <= Thr_num_right:
right_go() # 右のタイヤを前進
else:
right_back() # 右のタイヤを後退
if distance1 <= 10:
stop()
# プログラムが終了する際にモーターを停止
finally:
stop()
次回はこちら