お父にゃんの電子工作 -25ページ目

お父にゃんの電子工作

暇なおじさんが、電子工作(主にラジオ製作)をして勝手な感想を書く

前回は、Raspberry Pi ZeroでST7789を使った320×240dotのカラーLCDで表示できるようになった。↓

 

今回もRaspberry Pi Zero 2をいじって、基本的な使い方を調べてみたい。

と、いうことで、今回は、ロータリーエンコーダーを使って回転を検出し、液晶表示を更新するところまでやってみたい。

今後、Webラジオに仕立てていこうと思っているので、その準備作業でもある。

 

とりあえず、ロータリーエンコーダーをこのように接続した。

GPIO端子は内部プルアップを指定できるので、外付けプルアップ抵抗は無し。

 

 

例によって、ChatGPTにお願いしてロータリーエンコーダーの読み取りサンプルプログラムを書いてもらった。

import RPi.GPIO as GPIO
import time
 
# --- GPIO ピン番号設定(例: A=17, B=18) ---
CLK = 14  # エンコーダーのA相
DT  = 15  # エンコーダーのB相
 
# --- 初期設定 ---
GPIO.setmode(GPIO.BCM)
GPIO.setup(CLK, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(DT,  GPIO.IN, pull_up_down=GPIO.PUD_UP)
 
counter = 0
last_clk = GPIO.input(CLK)
 
def rotary_callback(channel):
    global counter, last_clk
    clk_state = GPIO.input(CLK)
    dt_state  = GPIO.input(DT)
 
    if clk_state != last_clk:  # A相が変化したときだけ処理
        if dt_state != clk_state:
            counter += 1   # 時計回り
        else:
            counter -= 1   # 反時計回り
        print("Counter:", counter)
 
    last_clk = clk_state
 
# --- 割り込み設定(5ms デバウンス) ---
GPIO.add_event_detect(CLK, GPIO.BOTH, callback=rotary_callback, bouncetime=5)
 
try:
    print("ロータリーエンコーダーを回してみてください。Ctrl+Cで終了。")
    while True:
        time.sleep(0.1)
 
except KeyboardInterrupt:
    print("\n終了します。")
finally:
    GPIO.cleanup()

 

Raspberry Pi ZeroをReal VNCでリモート接続して、Thonnyを立ち上げてコピペして動かしてみる。

ところが、

GPIO.add_event_detect(CLK, GPIO.BOTH, callback=rotary_callback, bouncetime=5)
のところで、 RuntimeError : Failed to add edge detectionになってしまう。
ChatGPTに、なんで?と聞いてみたが、一向に解決しない。
どうした?ChatGPT

素直に、ネットで調べたらここ↓に解決策があった。

 

具体的にはこうする↓とよくなった。

sudo apt remove python3-rpi.gpio
sudo apt update (←これはあってもなくてもいいみたい)
sudo apt install python3-rpi-lgpio 

 

Raspberry Pi OS の最近の環境だと従来の RPi.GPIO は古くて、内部的に /dev/gpiomem まわりの制御がうまくいかず、代わりに新しい rpi-lgpio ベースの実装を入れるとちゃんと動く、 ということらしい。詳しいことは おじさんには分からない。

 

ChatGPTの学習データが古いのか、ChatGPTにネット検索して解決策を探らるように指示する方が良いのかも・・

 

で、とりあえずエラーにはならなくなったが、動作が安定しないし、1クリックで2つ進む等使い物にならない。

で、ESP32で作った時のプログラムをChatGPTに送って、同じように書いてくれ、とお願いしたら、書いてくれた。

import RPi.GPIO as GPIO
import time
 
encoder_a = 14
encoder_b = 15
 
GPIO.setmode(GPIO.BCM)
GPIO.setup(encoder_a, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(encoder_b, GPIO.IN, pull_up_down=GPIO.PUD_UP)
 
encoder_old = GPIO.HIGH
encoder_count = 0
encorder_rapTime = 0
chattering_ignore_time = 5   # ms単位
 
def millis():
    return int(time.monotonic() * 1000)
 
def encoder_callback(channel):
    global encoder_old, encoder_count, encorder_rapTime
 
    if GPIO.input(encoder_a) == GPIO.LOW:
        if encoder_old == GPIO.HIGH:  # 前回はHighだった?
            if (millis() - encorder_rapTime) > chattering_ignore_time:
                if GPIO.input(encoder_b) == GPIO.LOW:
                    encoder_count = 1   # CW
                else:
                    encoder_count = -1  # CCW
            encoder_old = GPIO.LOW
    else:
        if encoder_old == GPIO.LOW:   # 前回はLowだった?
            encorder_rapTime = millis()
        encoder_old = GPIO.HIGH
 
# 割り込み設定 (立下り/立上り両方)
GPIO.add_event_detect(encoder_a, GPIO.BOTH, callback=encoder_callback, bouncetime=0)
 
try:
    while True:
        if encoder_count != 0:
            print("回転:", encoder_count)
            encoder_count = 0
        time.sleep(0.01)
 
except KeyboardInterrupt:
    GPIO.cleanup()

 

GPIO割り込みのチャタリング防止のbouncetime設定はあまり期待通りにはなってないようで、ソフトで論理が切り替わる時に、無視する時間を設定して、その時間内の論理変化はすべて無視するようにした。

今度は、比較的安定してエンコーダーの信号を認識するようになった。

 

次に、Webラジオ風に、ロータリーエンコーダーを回すとCh.ナンバーとステーション名が切り替わるように書いてみた。

最初は、LCDの書き換えが遅くて、もっさりした動きだった。どうやらSPIの転送速度を指定しないとデフォルトの8MHzになってしまうらしい。

設定限界の52MHzにしてみる↓

# SPI設定
serial = spi(port=0, device=0, gpio_DC=24, gpio_RST=25,bus_speed_hz=52000000)

 

今度は表示更新が高速になった。

それでも、TrueTypeフォントの展開に時間がかかる?のか、前に作ったESP32S3ベースのWebラジオより、動作がやや遅い。

 

参考までに、今回のコードを載せておきます。

from luma.core.interface.serial import spi
from luma.lcd.device import st7789
from PIL import Image, ImageDraw, ImageFont
import RPi.GPIO as GPIO
import time
 
GPIO.setwarnings(False)
 
# --- GPIO ピン番号設定(例: A=17, B=18) ---
encoder_a = 14
encoder_b = 15
 
# --- 初期設定 ---
GPIO.setmode(GPIO.BCM)
GPIO.setup(encoder_a, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(encoder_b, GPIO.IN, pull_up_down=GPIO.PUD_UP)
 
# SPI設定
serial = spi(port=0, device=0, gpio_DC=24, gpio_RST=25,bus_speed_hz=52000000)
device = st7789(serial, width=320, height=240, rotate=0)
 
# 描画用キャンバス
image = Image.new("RGB", (320, 240), "black")
draw = ImageDraw.Draw(image)
 
# フォント設定
font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
font_bold = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"
font40 = ImageFont.truetype(font_bold, 40)
font32 = ImageFont.truetype(font_bold, 32)
font28 = ImageFont.truetype(font_path, 28)
font28_bold = ImageFont.truetype(font_bold, 28)
font20 = ImageFont.truetype(font_bold, 20)
 
#global
ch_no = 0
volume_level = 10
bit_rate = 128
station_name = ""
streamtitle = "Poppin'party(戸山香澄(愛美)、花園たえ(大塚紗英)、牛込りみ(西本りみ)、山吹沙綾(大橋彩香)、市ヶ谷有咲(伊藤彩沙)) - 開けたらDream!"
encorder_flag = False
 
counter = 0
encoder_old = GPIO.HIGH
encoder_count = 0
encorder_rapTime = 0
chattering_ignore_time = 5   # ms単位
 
Station_data = (
     #name , url
  ["181.FM - Power 181  (Top 40)  US", "http://www.181.fm/stream/pls/181-power.pls"],
  ["181.FM - UK top 40  New York",   "http://www.181.fm/stream/pls/181-uktop40.pls"],
  ["181.FM - Rock 40    (Rock & Roll)",   "http://www.181.fm/stream/pls/181-rock40.pls"],
  ["Antenne Bayern -    Top 40  Germany",   "http://mp3channels.webradio.antenne.de/top-40"],
  ["100hitz - Top 40    Antelope, Ca, US",   "http://pureplay.cdnstream1.com/6025_128.mp3?"],
  ["Top Hits   US" ,   "https://api.onlyhit.us/tophits"],
  ["Jazz Radio  Classic Jazz    France",    "http://jazz-wr01.ice.infomaniak.ch/jazz-wr01-128.mp3"],
  ["181.FM - Fusion Jazz",    "http://www.181.fm/stream/pls/181-fusionjazz.pls"],
  ["181.FM -  Classical Jazz",    "http://listen.181fm.com/181-classicaljazz_128k.mp3"],
  ["Radio Art -    Jazz Piano",    "https://live.radioart.com/fJazz_piano.mp3"],
  ["Jazz Radio    Piano Jazz  FR",    "http://jzr-piano.ice.infomaniak.ch/jzr-piano.mp3"],
  ["100 GREATEST JAZZ LOUNGE BAR     CA",    "https://cast1.torontocast.com:4640/stream"],
  ["Jazz London Radio",    "http://radio.canstream.co.uk:8075/live.mp3"],
  ["Epic Lounge - PIANO & JAZZ BAR  Germany",   "https://stream.epic-lounge.com/piano-jazz-bar?ref=radiobrowser"],
  ["Radio Art - Mellow Piano Jazz   Greece",   "https://live.radioart.com/fMellow_piano_jazz.mp3"],
  ["SOLO PIANO by Epic Piano   Germany ",   "https://stream.epic-piano.com/solo-piano?ref=radiobrowser"],
  ["Music Radio FR","https://radio.musicradio.ai/listen/musicradio.ai/radio.mp3"],
  )
   
#Station_dataの数を調べる
Station_data_max = len(Station_data) - 1
 
# 折り返し関数
def wrap_text(text, font, draw, max_width):
    lines = []
    line = ""
    for char in text:
        test_line = line + char
        w = draw.textbbox((0, 0), test_line, font=font)[2]
        if w <= max_width:
            line = test_line
        else:
            lines.append(line)
            line = char
    if line:
        lines.append(line)
    return lines
 
def show_ch():
    global ch_no , font40
    ch = "Ch:"+str(ch_no)
    draw.rectangle((0, 0, 139, 48), fill="black")
    draw.text((0, 0), ch, font=font40, fill="yellow")
 
def show_vol():
    global volume_level ,font32
    vol = "Vol:"+str(volume_level)
    draw.rectangle((140, 0, 259, 48), fill="black")
    draw.text((140, 10), vol, font=font32, fill="cyan")
 
def show_bitrate():
    global bit_rate ,font20
    bitr = str(bit_rate) + "K"
    draw.rectangle((240, 0, 320, 52), fill="black")
    draw.text((260, 0), bitr, font=font20, fill="magenta")
    draw.text((260, 20), "bps", font=font20, fill="magenta")
 
def show_station():
    global font28_bold,ch_no
    draw.rectangle((0, 58, 320, 127), fill="black")
 
    station = Station_data[ch_no][0]
    lines = wrap_text(station, font28_bold, draw, max_width=315)
    y = 54
    for line in lines:
        draw.text((0, y), line, font=font28_bold, fill="lime")
        bbox = font28.getbbox(line)
        line_height = bbox[3] - bbox[1]
        y += line_height + 2 # 行間0
 
def show_streamtitle():
    global font28
    draw.rectangle((0, 129, 320, 240), fill="black")
 
    stream = streamtitle
    lines = wrap_text(stream, font28, draw, max_width=315)
    y = 129
    for line in lines:
        draw.text((0, y), line, font=font28, fill="white")
        bbox = font28.getbbox(line)
        line_height = bbox[3] - bbox[1]
        y += line_height  # 行間0
       
def millis():
    return int(time.monotonic() * 1000)
 
def encoder_callback(channel):
    global encoder_old, encoder_count, encorder_rapTime
 
    if GPIO.input(encoder_a) == GPIO.LOW:
        if encoder_old == GPIO.HIGH:  # 前回はHighだった?
            if (millis() - encorder_rapTime) > chattering_ignore_time:
                if GPIO.input(encoder_b) == GPIO.LOW:
                    encoder_count = 1   # CW
                else:
                    encoder_count = -1  # CCW
            encoder_old = GPIO.LOW
    else:
        if encoder_old == GPIO.LOW:   # 前回はLowだった?
            encorder_rapTime = millis()
        encoder_old = GPIO.HIGH
 
# 割り込み設定 (立下り/立上り両方)
GPIO.add_event_detect(encoder_a, GPIO.BOTH, callback=encoder_callback)
 
def encorder_check_and_action():
    global encoder_count,  ch_no
    if encoder_count != 0:
        ch_no += encoder_count
        encoder_count = 0
        # --- 範囲チェック ---
        if ch_no < 0:
            ch_no = Station_data_max
        elif ch_no > Station_data_max:
            ch_no = 0
               
        show_ch()
        show_station()
        device.display(image)
 
#イメージの作成
draw.line((0, 56, 320, 56), fill="orange", width=2)
draw.line((0, 128, 320, 128), fill="orange", width=2)
show_ch()
show_vol()
show_bitrate()
show_station()
show_streamtitle()
device.display(image)
 
# 表示
try:
    while True:
        encorder_check_and_action()
 
except KeyboardInterrupt:
        GPIO.cleanup()
 
 
今回も一筋縄ではいかなかった。
エンコーダーひとつで苦労するとは、なかなか奥が深い。
逃げ出したくなる気分になる
 
 
「おいらも逃げ出すニャ」
あんたのスピードなら、すぐに捕まえられるけどね