Raspberry Pi 5 で "Lチカ" する

"Lチカ" というのは、 "LEDをチカチカさせる" の略称だそうです。

LEDをチカチカさせるプログラムは、電子工作の Hello World のようなもので、もっとも基本的な入門プログラム。 今日は Raspberry Pi 5 で Lチカ を組んでみます。

入門課題ということで、チャッピーの言う通りに進めればカンタンにできるだろうと思って始めたのですが、意外と躓くところが多くて、いくらかの試行錯誤をしました。その甲斐あって、すんなりできていたら通らなかっただろう学びがたくさん得られ、なかなか内容の濃い Hello World ができた気がします。

試行錯誤で得た雑知識は記事の後ろの方にメモしていきます。

"Lチカ" をやってみる

LEDをチカチカさせるので、Raspberry Pi にLEDなどのハードを接続して、回路を作成する必要があります。ウェブ検索するとサンプルや手順を説明している先人の記事がたくさん出てきますが、今回はあえてチャッピー( ChatGPT ) に聞きながら回路を組んでみようと思います。お願いチャッピー!

使用する部品

まず、Lチカ回路で接続する部品などを確認していきます。

LED

LEDは、大昔に買ったまま押し入れに眠っていたものがいくつかあったので、それを使います。

抵抗器

LEDの他に、抵抗器が必要です。チャッピーいわく「抵抗は 220Ω〜1kΩ の範囲でOK(330Ωがちょうどよい)」とのこと。回路図では、 330Ω となっています。

大昔に買った部品の中に抵抗器もいくつかあったのですが、 ちょうどいい範囲に当たるものはなかったので、新しく抵抗器の詰め合わせセットを購入しました。そこから選んで使いたいと思います。

結構な数の抵抗器のセットですが、このくらい↓の箱に収められていました。 抵抗の大きさは、本体に印刷されている帯の色で判別できるそうですが、カラーコードの読み方表もつけてくれていて、大変親切です。

あけてみると、種類ごとにテープで留められていて、抵抗の大きさをメモしてくれています。山盛りの抵抗器のなかからほしい1つを探さなくていいし、カラーコードを読めない初心者でもすぐに選ぶことができました。大変親切です。

ブレッドボード

ブレッドボードは、簡易的に回路を接続するための導線板みたいなもので、回路の土台になる部品です。昔買った中にあったので、ありあわせで使いました。

ジャンパーワイヤー

ブレッドボードの穴と穴をつなぐ導線をジャンパーワイヤーといいます。

オス-オスなものと、メス-オスなものがあります。 Raspberry Pi は、基盤から生えているピンがオスなので、一方がメスになっているワイヤーが必要です。

ブレッドボードの穴と穴をつなぐときは オス-オス が必要なので、どちらも揃えておくとよいと思います。

ジャンパーワイヤーも昔買った中にあったので、ありあわせで使いました。

その他

もちろん、Raspberry Pi 5 本体と、起動や操作するための電源、モニター、マウスとキーボードなどが必要です。

回路図

Lチカするための配線を設計します。

  (6) GND ---------------------------------+
                                           |
                                          LED(-)
                                         (短い脚)

  (11) GPIO17 ---------[ 330Ω 抵抗 ]----> LED(+)
                                          (長い脚)

これは、LEDをプログラム制御でチカチカさせる最小構成です。

GNDとは? GPIOとは? とか、謎ワードが含まれていて最初は意味がわからなかったですが、調べながら進みました。このへんは記事の後半でまとめます。

Raspberry Pi の公式のドキュメントに、ピンの配置の図があります。
Raspberry Pi 公式の説明図

6番のピンに、 Ground と書かれています。 GND は Ground の略だそうで。

そして、 11番のピンに GPIO 17 が割り当てられていることが、この図からわかります。

そこにジャンパーワイヤーを挿し込めばいいんですね!

組んでみる

実際に、Raspberry Pi 5 とブレッドボードを使って、部品をつなげていきます。

こういう感じ↓になりました。

ブレッドボードのサイズに合わせて挿すので、LEDや抵抗器の脚はちょうどいい長さに折り曲げて使います。LEDは向きが重要なので、短い脚がどちらかわからなくならないように、長さを変えて曲げました。

プログラムコード

さて、実際にLEDの点滅を制御するプログラムコードを作成しますが、特に躓いたポイントはこの工程でした。ググると出てくる先人のサンプルは、もっとうんと短いコードで書かれていますが、私のコードは結構な行数になっています。

理由は、 "ライブラリを上手く使えなかった" からです。

まず、先人たちのコードはわりと Python で書かれていることが多いようです。が、今回は JavaScript (NodeJS) で書きました。わたしが Python よりも JavaScript のほうが慣れているということに配慮してくれたチャッピーが最初に提示してくれたコードが JavaScript でして、これにいたく感動したからです。

しかし、チャッピーのコードはライブラリを使用したもので、これが上手く動きませんでした。どうも、Raspberry Pi が 5 になってから、各部品へのアクセスの仕方が変更になったらしく、チャッピーはここのところがよくわかってない様子でした。

試行錯誤の過程で、コッピー(GithubCopilot)の知恵も借りながら onoffnode-libgpiodlgpionode-gpiod などのライブラリを試しましたが、結果的に、ライブラリを介さず gpioset コマンドを使う方法に落ち着いたのが、↓こちらのコードです。

たぶん、設定やパラメータをどうにかすると、上記のライブラリでもっとシンプルに書けそうな気がするので、追って調べてみようと思います。

const { execSync, spawn } = require('child_process');
const GPIO_PIN = 17;
let gpioProcess = null;

// GPIO値を書き込む関数
function writeGPIO(value) {
  try {
    // gpioset を使用してGPIOピンに値を書き込む
    // Raspberry Pi 5ではgpiochip0 (chip 0)を使用
    // gpiosetは実行直後にGPIO値が書き込まれ、プロセス終了後も値は保持される
    gpioProcess = spawn('gpioset', [
      '-c', '0',
      `${GPIO_PIN}=${value ? '1' : '0'}`
    ], {
      stdio: 'ignore',
      detached: false
    });

    // GPIO値が書き込まれるまで少し待機
    const start = Date.now();
    while (Date.now() - start < 10) {
      // 10ms待機
    }

    // 値が書き込まれたのでプロセスを終了
    if (gpioProcess) {
      try {
        gpioProcess.kill('SIGTERM');
      } catch (e) {}
      gpioProcess = null;
    }

    return true;
  } catch (err) {
    console.error('GPIO write error:', err.message);
    return false;
  }
}

// GPIOクリーンアップ関数
function cleanupGPIO() {
  try {
    // gpioプロセスを終了
    if (gpioProcess) {
      gpioProcess.kill();
      gpioProcess = null;
    }
    // すべてのgpiosetプロセスをクリーンアップ
    try {
      execSync(`pkill -f "gpioset.*${GPIO_PIN}="`, { stdio: 'ignore' });
    } catch (e) {}
    console.log('\nGPIO cleaned up');
  } catch (err) {
    console.error('GPIO cleanup error:', err.message);
  }
}


let value = 0;

const interval = setInterval(() => {
  value = value ^ 1; // Toggle 0/1
  writeGPIO(value);
}, 500); // 0.5 second interval

// Handle program termination
process.on('SIGINT', () => {
  clearInterval(interval);
  writeGPIO(0); // 終了時に0を書き込む
  cleanupGPIO();
  process.exit();
});
writeGPIO()cleanupGPIO() を別ファイルに分離したら、ライブラリを使ったのと大差ない行数になるかな。

実行するまえに、nodenpm をインストールしておきます。

sudo apt-get install nodejs
sudo apt-get install npm

$ node -v
v20.19.2

$ npm -v
9.2.0

gpioset コマンドは、GPIOピンに情報を書き込むコマンドのようです。対応する読み込み用のコマンドに gpioget があります。

gpioset は、書き込んだあと、プロセスが自動的に終了しないようだったので、 10ミリ秒後に kill する処理を入れています。

あ、ちなみに、 gpioset コマンドを使うには、別途インストールが必要かも知れません。(試行錯誤の中で使ったので、いつ入ったのかわからなかった・・・)

ググってみると、 libgpiod パッケージに含まれているらしいので、↓このコマンドでインストールされたっぽいですたぶん。

sudo apt-get install libgpiod-dev

実行する

実行したところが↓これです。

動いた!!

ボタンを押している間だけ光らせる

Lチカでは、ピンに対して命令を書き込む体験をしましたが、ピンから情報を読み取る練習もしておきたいと思います。情報を読み取れると、さまざまなセンサーをくっつけて処理できるようになるので、夢広がります。

ということで、ボタンパーツを接続して、押されている状態を読み取る回路を組んでみました。

  • ボタンが押されている間はLEDが光る。
  • ボタンを放すと、LEDも消灯する。

というプログラム制御を実装してみます。

回路図

LEDの回路はそのまま使い、ボタンの回路を追加しました。

  (6) GND ---------------------------------+
                                           |
                                          LED(-)
                                         (短い脚)

  (11) GPIO17 ---------[ 330Ω 抵抗 ]----> LED(+)
                                          (長い脚)
  (7) GPIO4 ----------+
                      |
                    ボタン
                      |
  (9) GND ------------+

GPIO 4 を読み取ることで、ボタンが押されているか、放されているか を判定できるようになります。

ボタン部品は、押し入れから発掘した↓これを使いました。

この脚をブレッドボードに挿し込んで接続します。

オン/オフが取れればいいので、脚は2本で良さそうですが、このボタンには4本生えてますね?

4本脚がどう使えるものなのかよくわかってないですが、大は小を兼ねる ということで、まぁ、きっと使えるでしょう。

プログラムコード

Lチカのコードをベースにして、 gpioget を通じてピンの状態を取得する関数を追加し、GPIO 4 (ボタン) の状態に応じて GPIO 17 (LED) への出力を決めるようにしたのが、主な変更点です。
const { execSync, spawn } = require('child_process');
const GPIO_LED = 17;  // LED用GPIO
const GPIO_SWITCH = 4;  // スイッチ用GPIO
let gpioSetProcess = null;
let gpioGetProcess = null;

// GPIO値を書き込む関数
function writeGPIO(pin, value) {
  try {
    // gpioset を使用してGPIOピンに値を書き込む
    // Raspberry Pi 5ではgpiochip0 (chip 0)を使用
    gpioSetProcess = spawn('gpioset', [
      '-c', '0',
      `${pin}=${value ? '1' : '0'}`
    ], {
      stdio: 'ignore',
      detached: false
    });

    // GPIO値が書き込まれるまで少し待機
    const start = Date.now();
    while (Date.now() - start < 10) {
      // 10ms待機
    }

    // 値が書き込まれたのでプロセスを終了
    if (gpioSetProcess) {
      try {
        gpioSetProcess.kill('SIGTERM');
      } catch (e) {}
      gpioSetProcess = null;
    }

    return true;
  } catch (err) {
    console.error('GPIO write error:', err.message);
    return false;
  }
}

// GPIO値を読み込む関数
function readGPIO(pin) {
  try {
    // gpioget を使用してGPIOピンの値を読み込む
    // 出力形式: "4"=inactive または "4"=active
    const result = execSync(`gpioget -c 0 ${pin}`, { encoding: 'utf8' });
    const trimmed = result.trim();
    // "=active"で終わっていれば1、そうでなければ0
    return trimmed.endsWith('=active') ? 1 : 0;
  } catch (err) {
    console.error('GPIO read error:', err.message);
    return 0;
  }
}

// GPIOクリーンアップ関数
function cleanupGPIO() {
  try {
    // gpioプロセスを終了
    if (gpioSetProcess) {
      gpioSetProcess.kill();
      gpioSetProcess = null;
    }
    if (gpioGetProcess) {
      gpioGetProcess.kill();
      gpioGetProcess = null;
    }
    // すべてのgpiosetプロセスをクリーンアップ
    try {
      execSync(`pkill -f "gpioset.*${GPIO_LED}="`, { stdio: 'ignore' });
    } catch (e) {}
    console.log('\nGPIO cleaned up');
  } catch (err) {
    console.error('GPIO cleanup error:', err.message);
  }
}

console.log('GPIO Switch & LED Controller started');
console.log(`Switch: GPIO ${GPIO_SWITCH}, LED: GPIO ${GPIO_LED}`);
console.log('Press Ctrl+C to exit');

// スイッチの状態を監視してLEDを制御
const interval = setInterval(() => {
  const switchState = readGPIO(GPIO_SWITCH);
  // スイッチが押されている(HIGH)ならLEDを点灯、そうでなければ消灯
  writeGPIO(GPIO_LED, !switchState);
}, 100); // 100ms間隔でチェック

// Handle program termination
process.on('SIGINT', () => {
  clearInterval(interval);
  writeGPIO(GPIO_LED, 0); // 終了時にLEDを消灯
  cleanupGPIO();
  process.exit();
});

gpioset は実行後にプロセスが自動的に終了しないが、gpioget は結果を出力して終了するようだったので、明示的に kill する処理は入ってないです。

出力値は "4"=inactive または "4"=active が返ってきます。放した状態が active で、ボタンを押すと inactive になりました。なんだか直感に反する挙動ですが、区別できてるのでひとまずよしとします。

実行する

上手にできました。

試行錯誤で拾った学びメモ

躓いて試行錯誤する中で、チャッピーやコッピーに教えてもらったことがいっぱいあったので、ここからメモしておきたいと思います。

LEDの向き: アノードとカソード

LEDには向きがあって、正しい方向で接続しないと光りません。向きは、脚の長さでわかるようになっています。

  • 脚が長い方: アノード (+)
  • 脚が短い方: カソード (-)

ピンの種類: GPIO、GND、3.3V、5V ってどういう意味?

Raspberry Pi のピンの配列の図を見ると、各ピンは GPIO、GND、3.3V、5V などの種類に分かれています。

3.3V5V は電源です。常に表示の通りの電圧で電源を供給しています。

GNDGround の略で、常に 0V だそうです。

電源はプラス極で、流れる先 0V がマイナス極で、これをつなぐと、電流が常に流れます。

なので、次の回路だと、LEDは常に点灯状態になるはずです。

3.3V ----> 抵抗器 ----> (+) LED (-) ----> GND

それで、 GPIO はというと、General Purpose Input / Output の略で、「入出力に自由に使える」ピンです。

自由に使えるピンといわれてもすぐにはピンと来ないですが、電圧を0Vにしたり、3.3Vの電源にしたりということをプログラムから制御できるようになっているということみたいです。

さきほどの、常に点灯状態になる回路の電源を、GPIOに置き換えてみるとこうなります↓。

GPIO ----> 抵抗器 ----> (+) LED (-) ----> GND

GPIOの出力はプログラムで制御できるので、 0V にすれば電流は流れなくなってLEDは消灯します。3.3V にすれば電源になるので LEDが点灯します。

これをプログラム制御で交互に切り替える処理をしたのが "Lチカ" ということになるんでしょう。

LチカはGPIOを出力で使う例でしたが、 General Purpose Input / Output は 汎用出力 なので、センサーやスイッチからの入力を受け取ることもできます。

入力を受け取る が具体的にどういうことかというと、どうもピンにかかっている電圧を調べること みたいです。

今回、ボタンを 押した/放した を判定する回路を作ってみました。ボタンを押したときに、回路が繋がって電流が流れる。放すと回路が切れて電流は流れなくなる。このときに電圧が変化するので、この状態を検出しているということですね、たぶん。

ちなみに、Raspberry Pi の起動時点では、GPIO は入力状態から始まります。

電流が流れていなくても、電圧はかかる

自分はここが根本的にわかっておらず、電圧は電流を流したときにかかるものだと思っていたのですが、それは間違いみたいです。

水道につけたホースの先をつまんで出口を塞いだまま、蛇口を開けた状態 をイメージしたら理解できました。水は流れていないけど、ホースには大きな水圧がかかります。それと同じです。

今作で、ボタンスイッチのオン/オフを取得するとき、ボタンを押したら inactive、放したら active と判定されたのが直感に反すると思いました。

↓この部分です。

  (7) GPIO4 ----------+
                      |
                    ボタン
                      |
  (9) GND ------------+

が、これがどういうことかは水道の例えで理解できます。

GPIOには、入力モードのときにも、ごくごく微量の電流が流れているそうです。つまり、少し蛇口が開いている状態。スイッチがオフのとき、この微量の電流が流れる先がないので、電圧が上がっていきます。この電圧を検出して active と判定される。

スイッチをオンにすると、回路が繋がってGNDと接続され、ごくごく微量な電流は流れ始めます。水圧でパンパンになったホースの先を解放したら、水が流れて水圧は下がります。これと同じで、流してしまえば電圧は下がる。これを判定して inactive となったわけです。

プルアップ/プルダウン

これについては、わたしはまだよく理解できてないけど、いずれ必要になる気がするので、キーワードだけでもメモしておきます。

GPIO の内側の仕組みで、ごくごく微量な電流を出しているということですが、これは 3.3V 電源との間に大きな抵抗器を挟んだ仕組みになっているそうで、この抵抗にプルアップとプルダウンという種類があり、プログラムコードから設定ができるそうです。

それぞれ特徴と振る舞いの違いがあるので、使い分けが必要なわけですが、ちょっとよくわからないので後で勉強することにします。

電流は、電圧が 高い方 から 低い方 へ流れる

中学生くらいのときに勉強熱心だった人は覚えているかも知れませんが、自分はまったく覚えていませんでした。電極はプラスとマイナスで常に固定されているわけではなくて、電圧の高低差によって決まるのです。へぇ〜!

電源ピン 3.3V と GND 0V を繋げば、電源からGND に向かって電流が流れます。もし、3.3V のピンと 1V くらいの何かを繋いでも、流れるということ。

逆に、3.3V を 5V ピンと繋いだら、電圧が逆転しています。この場合、 3.3V 側がマイナス極になり、電流が逆流することになります。(注意: 試してないけど多分壊れるのでやっちゃだめ!)

乾電池を複数入れる機械に、古い電池を混ぜてはいけない と言われるのは、なるほどそういうことかと。機器側よりも古い電池のほうが電圧が低かったら・・・逆流しちゃうので危ないよということなんですね。へぇ〜!

ソース駆動/シンク駆動: GPIO は、電流を "吸える"

電流は、電圧が 高い方 から 低い方 に流れる ので、これを利用して制御を逆向きにする設計もできるそうです。

今回のLチカで組んだ回路は↓こうなっていました。プラス極側に GPIO を繋いでいます。

GPIO ----> 抵抗器 ----> (+) LED (-) ----> GND(0V)

GPIOを電源として使い、電源を供給 する/しない によって装置(LED)を制御しました。この仕組みを "ソース駆動" というそうです。

これに対して、固定の電源供給 3.3V をプラス極に置き、マイナス極側に GPIO を置く という方法があります。
3.3V ----> 抵抗器 ----> (+) LED (-) ----> GPIO

この場合は、GPIO を High (=3.3V) にすると、電源側と釣り合うので電流は流れません。 GPIO が Low (0V) になると、電位差が生まれて電流が流れるようになり、 GPIO はマイナス極として機能するようになります。

この組み方を "シンク駆動" というそうです。

"ソース駆動" と "シンク駆動" にも、得手不得手があるようですが、いまの自分には理解できないので、いずれ勉強しなおします。

ハイインピーダンス (Hi-Z)

意味はわかりません。チャッピーの解説に出てきて、なんか聞いたことある気がして少し心が踊った感じがしたので、単語だけでもメモ。

Googleの要約によると:

「ハイインピーダンス(High Impedance、Hi-Z)」とは、電気回路や機器の「インピーダンス(交流抵抗)」が高い状態や、その特性を持つ機器を指し、オーディオでは多数のスピーカーを長距離に接続する設備音響(100V/70Vライン)に、デジタル回路では信号が「開放状態」で電気的に絶縁された第3の状態を意味します。

ぜんぜんわからん。

抵抗器: カラーコードの読み方

本当は、今回カラーコードを自力で読んでみる練習をしようと思っていたのだけど、他のことをたくさん学んで頭がいっぱいだし、購入した抵抗器が親切設計で、読めなくてもなんとかなったので、また別の機会に練習することにします。

抵抗器セットに同梱してくれていたカラーコード表を活用して!

あとがき

Hello World のわりには程よく躓き多くを学べて、程よく疲れ、満足です。

いろいろ謎のままになっているところがありますが、最初なのでこんなもんですたぶん。

gpiogetgpioset コマンドの使い方は本当にこれで合ってるのか? たしか、オン/オフだけじゃなくて、もっと解像度の高い情報を扱えたはず? 今回使用を断念したライブラリも便利に使える方法がきっとあるぞ? などの食べ残し感がいくらかありますが、これらについても追い追い解決していきたいです。


プロフィール

コヤナギ トモヤ

まったりウェブ系コーダーしてます。PHP製静的CMS Pickles 2 を開発しています。

RSSフィード

  • このサイトは、 コヤナギ トモヤ の個人サイトです。
  • 個人的な主張や、活動の記録などを掲載しています。 所属する企業、団体、その他の意見や立場を代表するものではありません。
  • 掲載された内容は古くなっている可能性があります。 特に古い記事では、現在の筆者の考えと異なる主張をしていることがありますが、記録としてそのまま残しております。 予めご了承ください。
ページの先頭へ戻る