PicoのPIOコードをGeminiにMicroPythonへ移植させてみたら...
以前からGoogleのGeminiは、Raspberry Pi Picoシリーズ(RP2040/RP2350)のPIOを少し苦手にしてるようだと思っていましたが、ちょっとした機会があってGeminiを使ってCで書いたPIO使用ライブラリをMicroPythonに移植させてみたところ、なんとなくPIOを苦手にしている理由がわかったような気がするので、少しまとめてみようと思います。Geminiしか使っていないので、AI全般に言えることかどうかはわからないですが、PIOに引っかかることは、AIコーディングの弱点を象徴する現象の一つかなとも思えるので、何かの参考になるかもしれません。
お題はPicoシリーズ用のTM1637 PIO Driver
Geminiに命じてMicroPythonに移植させてみたのは、こちらのRaspberry Pi Picoシリーズ向けTM1637 PIO Driverです。AmazonのブラックフライデーセールでTM1637を使用した4桁の7セグメントLEDディスプレイモジュールを安く買えたので、手慰みにライブラリを書いてみたという感じ。ついでに、この書籍のおまけコンテンツにできればいいかなあと。
TM1637は、I
- アドレスがない。バスに1つのTM1637しか接続できません
- LSb First。送受信するビット順がI
Cの逆で最下位ビットから送受信します{^2}
データシートを眺めた感じ、I
具体的なコードはtm1637out.pioを見てもらえばいいかと思いますが、PIOのFIFOにプッシュした値の下位8bitをI
SDA(TM1637のピン名はDIO)とCLKは連続したGPIOに接続する仕様としました。2つのGPIOを内部プルアップしておいて、Stop Conditionでバスを解放したら両ピンを入力に切り替え、高インピーダンスとすることでHighに遷移させて解放する仕様のためです。set命令のpindirsで、SDAとCLKの入出力を制御するため、隣り合ったGPIOピンである必要があるというわけですね。
FIFOにプッシュする値の第9bitがオンならば、Stop Conditionに移行してバスを解放します。第9bitをオフにしておけば、Stop Conditionを挟まずに連続したデータをTM1637に送信できます。下位8bitをデータとして、上位ビットにバスを制御するフラグを持たせる、このアイデアは、RP2040/RP2350のI
また、ACKをチェックして、NACKならばirq命令でステートマシン番号のIRQフラグをオンにしています。ただ、ライブラリ本体側のCコードでは、IRQフラグを監視していません。
というのも、テストしてみた限りNACKが観測できない一方、仮に割り込みでIRQフラグを厳重にチェックするとなると、かなり複雑になってしまいます。テストで観測できない程度の事象への対応としては、あまりに効率が悪く複雑すぎるので、バッサリ削除しました。必要ならば、IRQフラグを適宜チェックするという形で十分です(多分)。
というわけで、このPIOコードで次の図のようにきれいにTM1637とのI

初期化の冒頭をロジアナで測定
Start/Stop Conditionともにほぼ完璧じゃないかなと思います(わかんないけど)。TX FIFO JOINでFIFOを連結して8エントリとしているので、TM1637の最大である6桁表示でもCPUをストールさせずにデータを送出できるでしょう。PIOを使う最大の利点が、それですよね。
MicroPythonに移植しよう
このPIOを使ったライブラリを、MicroPythonで利用できるようにしてほしいというリクエストがありました。MicroPythonでGPIOをドライブしてI
移植といっても、PIOコード部分をMicroPython記法に変えて、あとはちょちょいとクラスを書けばいいだけなんで、ものの1時間もあれば終わりそう。だけどちょっと作業としてはつまらなそうな感じ。
基本的な設計としては、TM1637クラスを書いて、生成されたインスタンスとステートマシン番号を関連付ければいいかなと。RP2040でステートマシンは最大8なので、8を超えるインスタンスが生成されようとしたらエラーにすれば良さげ。
Pythonにはあまり詳しくない(そもそも何事にも対して詳しい訳では無い)ので、Geminiに相談。
私:Gemini君さ、Pythonで生成されるインスタンス数を制限するいい方法ってある?
Gemini君:「__new__()を使うといいでしょう! Pythonにはインスタンスを生成する前に__new__()を呼ぶメカニズムがあるので、そこで禁止すればインスタンスの生成を完全に抑止できリソースの消費を抑えられます」
私:ほうほう、__new__()なんてあるんだ勉強になるなー。それって(サブセットである)MicroPythonでも利用できるメカニズムなの?
Gemini君:「問題ないです!」
私:じゃあステートマシン番号とインスタンスを関連付けて、インスタンス数が8を超えたら__new__()で例外をスローすれば良さそうだね。
Gemini君:「完璧ですね!」
私:その方向で「これ」をMicroPythonに移植してくれない?(ここにある4つのファイルを貼る)
Gemini君:(数秒後)「できました!」
という感じのやり取りで、1行のコードも書かずに、Gemini君がMicroPythonに移植してくれたのだけれど……
ここが駄目だよGemini君
案の定というかなんというか、Gemini君が提出してくれたコードは、まともには動きません。クラスの主要部分は、おおむね問題ないかなという感じではあるけれど、問題はPIOコードの部分。動いているPIOコードで、ロジアナでタイミングもチェックして問題ないことを確認しているとGeminiに念押ししているにも関わらず、どいうわけかPIOコードをイジってしまうんですね。
まず第一に、Gemini君は一見すると無駄そうに見える命令を削ろうとする傾向がある。ただ、言うまでもなくPIOコードはTM1637とのやりとりのタイミングを制御しているので、無駄そうに見える命令も削ればタイミングが変わり、場合によっては新たに検討する必要がある事項が出てくる可能性があります。したがって動くコードから命令は削ってほしくないんだけれど、Gemini君はコードの効率性を優先し、タイミングは考慮しないようです。このことはPIOのコーディングにとって致命的とも言える傾向かもしれない。
また、PIOコードのロジックを正しく読み取れていないと思わせるミスが2箇所ほどありました。下は動くように修正したコードですが、Gemini君がすっ飛ばしたりミスしていた主要な部分を示します。
@rp2.asm_pio(
out_init=(rp2.PIO.IN_HIGH), # 1.Gemini君はここの初期化子の指定が間違っていた
set_init=(rp2.PIO.IN_HIGH),
sideset_init=(rp2.PIO.OUT_HIGH),
out_shiftdir=rp2.PIO.SHIFT_RIGHT,
autopull=False,
fifo_join=rp2.PIO.JOIN_TX
)
def _tm1637_pio():
wrap_target()
# --- Start Condition ---
pull(block)
set(pindirs, 0b00) # SDA/SCL input
nop() [7]
set(pindirs, 0b01) # SDA output/SCL input
set(pins, 0) # SDA is Low
nop() [7]
set(pindirs, 0b11) # SDA/SCL output
set(x, 7) .side(0) # CLK is Low
jmp("output")
label("byte_loop")
set(x, 7) .side(0) # CLK is Low
pull(block) # 2.Gemini君はこの命令を削ってしまっていた
label("output")
nop() [3]
out(pins, 1) [4] # LSB output
nop() .side(1) # CLK is High
nop() [7]
nop() .side(0) # 3.この命令も削ってしまっていた
jmp(x_dec, "output")
# --- ACK Check ---
set(pindirs, 0b10) [7] # SDA input/SCL output
nop() .side(1) # SCL = High
jmp(pin, "nack") # SDA High (NACK) なら分岐
jmp("ack")
label("nack")
irq(rel(0)) # NACK通知
label("ack")
nop() [6]
nop() .side(0) # CLK is Low (ACK cycle end)
set(pindirs, 0b11) [7] # SDA/SCL Output
out(x, 1) # 9bit目を読み取って判定
jmp(not_x, "byte_loop") # 0なら次のバイトへ
# --- Stop Condition ---
set(pins, 0) [7] # SDA is Low
nop() .side(1) # SCL is High
nop() [7]
set(pindirs, 0b00) [7] # SDA/SCL Input (STOP)
wrap()
まず1.の初期化子ですが、Gemini君はset_init=とout_init=を入れていなかったため、SDAピンのALTがPIOに切り替わりません。もっとも、@rp2.asm_pio()のパラメータは私も調整に戸惑ったところなので、Gemini君だけが駄目というわけじゃなく、私も駄目ではあったのですけどね。この指定をきっちりやっておかないと、SDA/SCLを入力に切り替えたときにプルアップ=Highが正しく機能しない、ということがわかるまでに少し時間がかかりました。
2.の部分は、Stop Conditionを挟まないときに戻って来るループの先頭で、続きのデータをpullしないとならないですが、Gemini君はなぜかpullを削ってしまっていました。Start Conditionの前にpullがあるため、このpullは無駄もしくはダブりと判断したのかもしれません。
もっとも、Gemini君はCのコードに対して
ご提示いただいたC SDKのソースコードを拝見しました。非常に丁寧に作り込まれたライブラリですね! 特に、
out x, 1を使って継続か終了(STOP)かを判定する仕組みや、ACKチェックのロジックが明確です。
などとお世辞を言っているので、この部分のロジックが正しく読めてないのは妙ですが、先に述べた通り効率を優先するというGemini君の特性からpullを無駄もしくはダブりと判断した可能性は高いと思います。
3.は致命的で、CLKをLowに落とす命令をすっ飛ばしていました。なぜオリジナルのPIOコードに入っている命令を削除したかはわかりませんが、Gemini君はTM1637のI
このように、PIOコードの思わぬ箇所を微妙に削ったり変えたりしていたので、1行1行チェックし修正していくのに結構手間がかかりました。数秒で書いてくれたのは良かったけれど、かかった時間をトータルすると自分で移植してもたいして変わらなかったかもしれない。まあ、__new__()を使ったロジックは、私は知らなかったので功罪半ばといったところですかね。
というわけで、修正の後に下の図のようにMicroPythonバージョンもしっかり動作するようになりました。

MicroPython版をロジアナで測定
さすがMicroPythonで、データセットアップに時間がかかりPIOの駆動が間欠的に見えますね。でもPIOにデータ転送処理を移すことで表示の負荷がだいぶ軽くできるでしょう。
Geminiしか使っていないので、AI全般に言えるかはわかりませんが、Gemini君にPIOのコードを相談するときには十分な注意が必要だろうと思います。Gemini Code AssistantでもPIOのロジックがちゃんと読めていないのかなという経験を過去にしています。また、Gemini 3になってからはわかりませんが、以前はGemini君にPIOのコードをスクラッチで書かせると、ありもしない算術演算命令を使用たり(incとか)結構めちゃくちゃでした。AI全般に言えることですが、滅茶苦茶なくせにわかったようなことをいうので初心者は特に要注意でしょう。
Discussion