lp6m’s blog

いろいろかきます

AIに頼ってPX-W3U4を修理?した

PX-W3U4が壊れた?

地デジとBS/CSが2chずつ同時視聴できるチューナーPX-W3U4をRaspberry Piに繋げて使用している。
https://www.plex-net.co.jp/product/px-w3u4/
引越し後、久しぶりに構築したら地デジだけ見れなくなっていた。recisdbで信号レベルを確認しても1-2dBしか出ていない。
引越し後初めて起動したので、マンションの同軸から出ている信号がわるいのか、PX-W3U4が壊れたのかはよくわからない。
マンションの部屋の壁の同軸ケーブルを4分波器に繋いで、テレビとPX-W3U4にそれぞれ繋いでいた。テレビでは地デジもBSも問題なく視聴できる。
Amazon | ホーリック アンテナダブル分波器 【4K8K放送(3224MHz)/BS/CS/地デジ/CATV 対応】 ケーブル一体型 50cm ホワイト AWE-653WH | ホーリック(HORIC) | BS・CSアンテナ
よくわからず簡易ブースターを買ってみたが、信号レベルは4dBくらいにしかならない。
Amazon.co.jp: ホーリック アンテナブースター 室内・地デジ(UHF/VHF)専用 中継タイプ HAT-ABS024 : 産業・研究開発用品
とりあえず分解して様子を見ることにした。

外観が怪しい部品を発見

顕微鏡で見ていたら、外観が明らかに怪しい部品が基板の裏面にも表面にもある。

裏面と表面に1つずつ実装されている素子。左が裏面(地デジ)で、右が表面(BS)。

左:地デジ・右:BS なんと右は生きている

左側(地デジ)は左下が少し欠けているし、全体に薄く亀裂のようなものが入っている。右側(BS)は明らかにGNDがエグれすぎ。今回壊れたのは地デジだけなので、なんと右側の素子は生きているらしい。

AI・Googleに画像検索をかけたが、何の素子なのかはまったくわからなかった。
とりあえずYoutuberの見様見真似でテスターで基板上の他の部品をチェックしたが、明らかに壊れてるダイオードとかコンデンサはなかったので、この部品かな、と思った。
とりあえず、回路を追って、この素子の前後がどうなっているかと、素子の周りの部品がどうつながっているかの情報をもとにAIさんに聞いたところ、2分配のパッシブ・パワーデバイダ(ウィルキンソン型スプリッタ)で間違いないらしい。

地デジ側の素子のアイソレーション抵抗(R199)の両端をテスターで測るとショートしていた。明らかに壊れている。抵抗を半田ごてで取り外してもショートは解消せず、素子が壊れていることは確定。ちなみにアイソレーション抵抗の抵抗値は地デジは100Ω、BS側は66Ωだった。

とりあえず素子も取り外して、「部品同定するか互換品探すしかないな・・・」と思っていたら気がついたことがある。

そもそも地デジ側は分配素子がいらない

上のClaudeとのチャットにも書いてあるが、そもそも分配素子の2出力のうち、片方は実装されていないのだ。
これはPX-W3U4が、その上位機種であるPX-Q3U4と基板を流用しているみたい。地デジ用周波数帯向けのrafaelmicro R850 ICにはループ出力?があるみたいで、1つのR850の出力の先に、もう1つのR850が存在している。

分配先に部品が実装されていない
R850の出力が別のR850の入力になっている。

R850のデータシートは見つけられなかったが、似た個体のR836のデータシートをみると、確かに6pinがLoop Thorough outputになっていた。
https://dvbpro.ru/wp-content/uploads/2017/10/Rafael_Micro_R836_High_Performance_DTV_Silicon_Tuner_Datasheet.pdf

そもそも分配する意味ないのなら、普通にジャンパすれば動くんじゃね?となった
と言うわけで、ワイヤーでジャンパ接続。

治った!が、変な運用に

recisdbで信号レベルを計測したら34dB出ている!。2ch同時視聴もOK。治ったときは嬉しかった。LLMのおかげで素人修理成功だ。
ちなみに、本来パッシブ・パワーデバイダがあると分配後は3dB程度弱まるらしく、設計よりも強い信号がR850に入力されてしまっているが、とりあえず今は動いている。

組み立て配線綺麗にするか〜とやっていたら、実はブースターに電源を入れない状態でしか信号レベルがまともに入らないことがわかった。

  • 壁からPX-W3U4の地デジに直結(分配器なし) 7dB
  • 4分配器経由: 12dB
  • 4分配器+ブースター電源なし: 34dB
  • 4分配器+ブースター電源あり :4dB

以下の通りだそうです。。じゃあ壊れていなくても受信できていなかったことになる。

BS側の分配素子もいつか壊れそう。BS側はなぜかPX-W3U4でもループ出力になっておらず、分配素子の出力をそれぞれRT710に繋いでいるので、壊れたらジャンパしてBSは1chだけ使えます運用すれば良いかな。

AIさんのおかげで、ほとんど知識がなくても修理することができて、嬉しかった。偶然にもなんの修理部品も使っていない。
分配器が壊れてて、分配器がそもそも不要だから取り除いただけ。

AIに暴言を吐くのは良くないね

スマートロックを作った。

スマートロックを自作する話は検索すると結構ヒットする。
n番煎じではあるが、自分も作りたくなったので作った。既製品は高いし、買うのはつまらないと思ったから。

これが完成品。スマートとは言い難い見た目のゴツさだが、まあいいでしょう。

サーボモーターの音がかなり大きいが、いい感じに動作している。

基本的なアイデア

  • 鍵の施錠・解錠をサーボモーターで行う。手動でもサムターンを回せるようにギアを使う(後述)
  • ドアの開閉を距離センサで検知する。ドアが閉まると自動施錠などができる。
  • 制御はM5Atom S3で行う。
  • バッテリー駆動は諦める。既製品は半年近くバッテリーが持つらしく、省電力性に優れているらしい。
  • できる限り見た目をまともにする。できる限りで。

サムターンを手動でも回せる機構

多くの記事ではサーボモーターを被せて直接サムターンを回しているが、手動で回せなくなるのは何かと不便だと思った。
機構については以下の記事からアイデアを拝借した。この方法だとギアを設計する必要があり、外装は大きくなるが、手動で回せるメリットには変えられない。

dreamerdream.hateblo.jp

部材

早く作りたかったのでAmazonから多くの部材を買ったが、aliexpressだともっと低価格に抑えられると思う。

スマートロック本体の設計

Fusion360で頑張って設計する。ギアを使った設計は初めてだったので、苦戦した。
ギアの歯が必要以上に大きいと思う、多分こんなに大きくなくていい・・が、まあ動いたのでヨシとしよう。


3Dプリントする部品は全部で4点で、ベースのプレートにギアを2つ乗せて、サーボモーターを上のカバーで固定して、カバーとベースのプレートをねじ止めする。最後にサーボモーターとギアをねじ止めする。

部品が正しく配置できるようにする & 印刷が楽になるような構成にすることを考えて設計したが、組み立てやすさを何も考えていなかったため、組み立ては非常に難しかった。
取り付ける玄関ドアとギアの間には若干の隙間が出るように作ってはいるが、保護するような設計になっていないので、ギアの底面とドアの表面が擦れてしまう可能性がある。サムターンのギアは、サムターンに被っているだけなので、若干回転時にぶれてしまう。ギアの回転軸がブレないようにする補助ギアのようなものを入れた方がよかったような気がしている。この辺りの反省点は、次に何かしら設計するときに学んでみようと思う。

動作確認をしている様子:

今回の設計で自分なりに気に入っているのは、Atom S3が簡単に取り外せるようにしたこと。簡単に外せるようにしているので、プログラムの書き換えも楽にできる。(OTAアップデートに対応させれば取り外す必要すらないだろうけど)

回路に関しては書くほどでもないので少しだけ。外部電源5VをサーボモーターとM5 Atomに入力、サーボの信号線をG5に接続。
サーボが回転したときに大きい電流が流れてM5が不安定になることがあるらしいので、一応100uFのコンデンサを5VとGNDの間に挟んでいる。距離センサのI2CはM5 AtomのGroveコネクタに直結していた。(後でハブ経由にする)

NFCリーダーの接続

M5 Atom S3はWiFi接続可能なので、ソフトウェアさえ書けばスマホからインターネット経由で解錠・施錠できる。
これでも十分だけど、せっかく自作したんだから機能を追加したほうが楽しいよね、ということでNFCリーダーから解錠できるようにした。
なお、スマホNFCはカードがエミュレーションされているため、固定のIDを返すことはない。そのためNFCリーダーをつけてもスマホを使って解錠することはできない。(エミュレーションアプリを書けば別だが、多分個人は開発できないはず)
ホストベースのカード エミュレーションの概要  |  Connectivity  |  Android Developers

Amazonで購入したPN532モジュールはFelicaもTypeAも読める。SPI/UART/I2Cでの接続が可能だが、距離センサがI2Cで接続されていたので、NFCモジュールもI2Cで接続することにした。

NFCモジュールの使い方については以下記事が参考になった。
gijin77.blog.jp
以下記事に倣って一応コンデンサを追加しておいた。
NFC モジュールの動作安定性を改善する #RaspberryPi - Qiita

NFCリーダーはドアの外で、スマートロック本体はドアの内側にある。市販のスマートロックでは基本的に無線で接続されており「スマート」だが、設計が複雑&バッテリー駆動にする必要がある、今回は普通にドアの間をI2Cの線で結ぶことにする。そうすると、薄いケーブルが必要になる。

ドアの隙間にいれるような強化されたLANケーブルを使うのがおそらく最も良い気がするが、LANケーブルの加工が面倒だった&地味に高かったので今回は使わなかった。
エレコム LANケーブル 0.5m 屋外対応 すきま用 アルミ強化 シルバー LD-VAPF/SV05

かわりに、薄いUSB2.0延長ケーブルを見つけたので、USBコネクタを取り外してI2Cの4本の信号を繋げることにした。(これが良かったのかはわからない、とりあえず動いているが)

プライバシーの都合上、ドアの外の写真は載せないが、こちらもケースを作成して粘着テープでドアに固定した。

ソフトウェアの作成

今のところは以下のような機能を持っている。

  • WifiNFCモジュールの接続遮断時の自動再起動機能
  • AWS IoT CoreのトピックをListenして解錠・施錠(AWS Lambda経由でトピックへのPublishが可能)
  • ローカルのUDPブロードキャストパケットをListenして解錠・施錠
  • 登録済みのNFCカードを読み取った時に解錠
  • 「自動ドア開閉待機モード」の時にドアの開閉を検知すると、自動で施錠

* これはM5Atomのボタンを押したときや、コマンド経由で解錠されたときのみ有効になる
* 外出する時はボタンを押して出れば自動で施錠されるし、帰宅時に解錠コマンドで解錠したあと、ドアが閉まると施錠される
* 一瞬外に出るだけの時は自動施錠してほしくないので、外出の時はボタンを押す操作を求めるようにしている。

ハード側が完成したことが嬉しすぎて、ソフト側は今は適当。ほぼ全部Claudeに書かせて全然チェックしていないため、NFCの読み取り処理でブロックされてボタンを押す処理が反応しないことがある。これから自分で修正する。まあソフトウェアは後から別にどうとでもなるからね。

一応STLファイルとソースコードを公開している。
github.com


後はNFCタグシールを玄関に貼って、スマホ側でNFCタグを読み込んだらスマホからLambdaを叩くように設定して解錠しようと思う。

以前作ったインターホンの遠隔操作と併用していい感じにオートメーションできて満足。GitHub - lp6m/InterphoneIoT: インターホンを非破壊でIoT化する

市販品はすごいなと思った。いつか入手したら分解してみたい。バッテリー駆動やPCB設計も、いつか挑戦できるといいな。

WindowsとUbuntuのブートを選択できる物理スイッチを作った

小ネタです。

モチベーション

デスクトップPCには最初にWindowsをインストールし、その後別のSSDにUbuntuをインストールしてデュアルブート環境を構築している。(実際には3つだが)
BIOSでのブートディスクの優先順位をUbuntu側のSSDにしておくと、PC起動時にGRUBメニューが表示され、どのOSを起動するか選択できる。

こんな感じのやつ。

これはこれで便利なのだが、一つ問題があった。
自分はKVM機能が付いているDellのモニタ (U3821DW) を使っている。キーボード他の周辺機器をモニターに接続しておけば、映像入力に合わせて周辺機器の接続先がPC間で切り替わるので便利だ。

しかし、「PCの電源を入れる → モニタの映像入力をデスクトップPCに切り替える → キーボードがデスクトップPCに認識される」というプロセスには数秒かかる。(しかも一瞬出力がオフになるせいで、信号なしと判断されて別の映像入力に強制的に切り替わったり・・・)その間にGRUBの選択タイムアウトを過ぎてしまい、起動したいOSを選べないことが多々あった。

「PCの電源を入れる前に、物理的なスイッチで起動OSを切り替えられたら最高に便利なのにな…」とは昔から思っていた。

Geminiに相談したらGRUBのスクリプトをカスタマイズすることで、簡単にできるらしいのでやってみた。

  • GRUBは起動時にUSBデバイスにアクセスできる
  • GRUBスクリプトで、デフォルトで選択されるOSを動的に変更できる

これならいける。あとは、物理スイッチの状態に応じてUSBの中身(特定のファイルの有無)を切り替えられるデバイスを作ればいいだけだ。というわけでやってみる。

GRUBスクリプトで起動OSを動的に切り替える

まずはPC側の設定から。GRUBは起動時に/etc/grub.d/ディレクトリにあるスクリプトを読み込んで、設定ファイル(/boot/grub/grub.cfg)を生成する。なので、ここに自作のスクリプトを追加する。

1. カスタムスクリプトの作成

/etc/grub.d/09_custom_boot_selectという名前で以下のスクリプトを作成する。(先頭の数字が小さいほど先に実行される)

  #!/bin/sh
  # 以下の cat << 'EOF' から EOF までの内容が、そのまま grub.cfg に出力される
  cat << 'EOF'
  # --- Start of Custom USB Boot Logic ---
  echo "Searching for boot key on USB devices..."
  search --no-floppy --fs-uuid --set=usbkey A1B2-C3D4   # ★ RP2040を接続して調べたUUIDに書き換える

  if [ -n "$usbkey" ]; then
      if [ -f ($usbkey)/windows.key ]; then
          echo "USB key found. Booting Windows."
          set default="Windows Boot Manager (on /dev/nvme0n1p1)"   # ★ 調べたメニューエントリ名に書き換える
      else
          echo "USB key not found. Proceeding with normal boot."
      fi
  else
      echo "USB device not found."
  fi
  # --- End of Custom USB Boot Logic ---
  EOF
2. 必要な情報を調べてスクリプトを編集

スクリプト内の2箇所(の部分)を自分の環境に合わせて書き換える必要がある。

  • USBのUUIDを調べる

自分は最初は適当なUSBメモリを使って手動で実験してみた。後の作業でRP2040側にCircuitPythonをインストールした後で、もう一度UUIDを調べて書き換える必要がある。

lsblk -f
  • Windowsのメニューエントリー名の調べ方

以下のコマンドで、自分のPCの正確なエントリー名を調べる。`'`で囲まれた部分全体をコピーする。

grep 'menuentry ' /boot/grub/grub.cfg
3. スクリプトの適用

最後に、作成したスクリプトに実行権限を与えて、GRUBの設定を更新すれば完了。

sudo chmod +x /etc/grub.d/09_custom_boot_select
sudo update-grub

これでPC側の準備はOK。

物理スイッチでUSBの内容を書き換える

次に、物理スイッチとなるUSBデバイスを作る。
マイコンがPCに対して「自分はUSBメモリです」と振る舞う必要があるが、Raspberry Pi Pico (RP2040)はネイティブUSBに対応しているので、容易に実装できる。

今回は手元に転がっていた&小さかった**RP2040-zero**というボードを使ったが、Pico系なら何でもOK。
1000円弱で買えるのでかなり安い。

www.waveshare.com

CircuitPythonというものが使えるらしいので使ってみた。環境構築は非常に簡単で、BOOTボタンを押しながらuf2ファイルを書き込むだけ。参考:
Installing CircuitPython | Welcome to CircuitPython! | Adafruit Learning System


配線は、トグルスイッチの片方をGP15に、もう片方を3V3(OUT)に接続するだけ。
`code.py`に以下のコードを書き込む。

import board
import digitalio
import storage
import os

# --- 設定 ---
# スイッチを接続するGPIOピン
SWITCH_PIN = board.GP15 

# --- 初期設定 ---
# スイッチのピンを入力モードに設定し、内部プルダウン抵抗を有効にする
switch = digitalio.DigitalInOut(SWITCH_PIN)
switch.direction = digitalio.Direction.INPUT
switch.pull = digitalio.Pull.DOWN

# PCからの書き込みに備え、一度書き込み可能にリマウント
storage.remount("/", readonly=False)

# 念のため、既にあるキーファイルを削除しておく
try:
    os.remove("/windows.key")
except OSError:
    # ファイルがなくてもエラーにしない
    pass 

# スイッチの状態を読み取る (ONならTrue, OFFならFalse)
if switch.value:
    print("Switch is ON. Creating windows.key")
    # スイッチがONなら、windows.keyファイルを作成する
    with open("/windows.key", "w") as f:
        f.write("This is a boot flag file.\n")
else:
    print("Switch is OFF. Not creating any file.")

# 安全のため、ファイルシステムを読み取り専用に戻す
storage.remount("/", readonly=True)

print("Setup complete. The device is now a USB drive.")

これで、Picoは起動時にスイッチがONになっていればwindows.keyというファイルを持つUSBメモリとして、OFFならwindows.keyは削除される。

正しくGRUBのデフォルト選択項目が選ばれるには、該当するGRUBスクリプトが実行されるまでにRP2040上でのCircuitPythonの処理が完了している必要がある。今回は細かいことを考えずに作っているが、実際に使ってみたところ100%成功したので、まあRP2040での処理完了のほうが速いからOKということにしておこう。

なおスクリプトとコードは全部Geminiに書かせた。Gemini特有のコメント文がいっぱい刺さってるね。

3Dプリンタでケースを作る。

最後に、RP2040-zeroとスライドスイッチを収めるケースを3Dプリンタで作成した。Fusion360力がいつまでたっても向上しないが。。
※なおよく見るとRESETボタンが破壊されているが、使わないのでまあヨシとする・・何度か取り外す時にペンチで掴んだせい。



まとめ

Searching for boot key on USB devices...の表示のあと、自動的にWindowsが選択されている様子。

今回の小ネタ、既にやってる人が多そうだなと思ったけど意外と似た記事は無かった気がする。
これで、PCの電源を入れる前に物理スイッチを切り替えておくだけで、快適にOSを選択できるようになった。やったね!
(もっと楽な方法あるのかな?)

VexRiscvでBRAMのかわりにDRAMを使いたい(失敗)

前回のKV260でVexRiscv動作させた - lp6m’s blogでは、VexRiscvのコアのメモリをBRAMで実装していた。 AXI BRAMを使ってPSから読み書きができた。
Xilinx DPUとVexriscvを両方載せようとすると、BRAMリソースが制約を受けてメモリサイズを小さくせざるを得ない。
パフォーマンスを犠牲にしてよいので、BRAMの変わりにDRAMを使いたい。VexRiscvの命令バス(IBus)とデータバス(DBus)はAXIプロトコルなので、適当に繋いだらできるのでは?と思って繋いだ。
ブロックデザインは以下の通り。


アドレスマップは特に何もいじらず、自動で設定されたものを使用した。

教えてもらうまで知らなかったのだが、PS/PLはどのDDRアドレスにでもアクセスできるのではなくて、PS側にはDDR Low(0x0000_0000から0x7FFF_FFFF)にしか割り当てられていない。
VexRiscvにはこのDDR Low領域に読み書きしてもらわないと、PS側からアクセスできないみたい。


VexRiscvは命令メモリの開始アドレスをハードコーディングするのでresetVectorを0x40000000lに修正した。
github.com


petalinuxプロジェクトのxilinx-kv260-starterkit-2022.1/project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi を修正する。
reserved-memoryでDDR Low領域の一部空間(今回は0x40000000から0x4FFFFFFF)をLinuxに使用されないようにブロックしておく。
CMA領域に連続したメモリアドレスを確保する。この書き方は以下を参考にさせてもらった。
github.com

最後に、確保した空間を簡単に読み書きするために`u-dma-buf`を使用させてもらう。
今までudmabufはXilinx AXI DMA IPのためだけに存在するバッファだと完全に勘違いしていたが、純粋にデータのやりとりのバッファとして使えることを理解した気がする。

/include/ "system-conf.dtsi"
/ {
	chosen {
                bootargs = "earlycon console=ttyPS1,115200 clk_ignore_unused init_fatal_sh=1 cma=512M uio_pdrv_genirq.of_id=generic-uio";
                stdout-path = "serial1:115200n8";
        };
        reserved-memory {
                #address-cells = <2>;
                #size-cells = <2>;
                ranges;
                riscv_buf: riscv_buf@40000000 {
                        compatible = "shared-dma-pool";
                        reusable;
                        reg = <0x0 0x40000000 0x0 0x10000000>;
                        label = "riscv_buf";
                };
        };
        udmabuf@40000000 {
                compatible = "ikwzm,u-dma-buf";
                device-name = "udmabuf0";
                size = <0x10000000>;
                memory-region = <&riscv_buf>;
        };
};

この内容で起動用SDカードを作成した。

petalinux-build
petalinux-package --boot --u-boot --force
petalinux-package --wic --images-dir images/linux/ --bootfiles "ramdisk.cpio.gz.u-boot,boot.scr,Image,system.dtb,system-zynqmp-sck-kv-g-revB.dtb" --disk-name "mmcblk1"

pl.dtsiの作成

xmutilでデバイスツリーオーバレイするためのdtboファイルを作成する。
PL側のノードがないため、エラーになった。 xlnx_rel_v2022.2を指定することでエラーは出なかった。以下リンクと同じエラー。
createdts fails for KV260 XSA · Issue #310 · Xilinx/Vitis-Tutorials · GitHub

xsct
createdts -hw vivado/riscv_base_prj/riscv_base_prj.xsa -zocl -platform-name mydevice -git-branch xlnx_rel_v2022.2 -overlay -compile -out mydevice
exit

生成されたpl.dtsiの内容は以下の通りだった。PLのノードがないためにクロックの情報などがかかれたノードがないがこれでいいのか?

/dts-v1/;
/plugin/;
/ {
	fragment@0 {
		target = <&fpga_full>;
		overlay0: __overlay__ {
			#address-cells = <2>;
			#size-cells = <2>;
			firmware-name = "riscv_base_prj.bit.bin";
			resets = <&zynqmp_reset 116>,<&zynqmp_reset 117>;
		};
	};
};

u-dma-buf.koの作成

u-dma-bufを使用するために、petalinuxでカーネルモジュールをビルドする。以下記事を参考にビルドした。
FPGAの部屋 udmabufをPetaLinux 2018.2でビルドする

起動確認

SDカードイメージを書き込んで起動確認をし、u-dma-buf.koを読み込んだ。

xilinx-kv260-starterkit-20221:~$ dmesg | grep cma
[    0.000000] cma: Reserved 512 MiB at 0x0000000057800000
[    0.000000] Kernel command line: earlycon console=ttyPS1,115200 clk_ignore_unused init_fatal_sh=1 cma=512M uio_pdrv_genirq.of_id=generic-uio
[    0.000000] Memory: 3213632K/4193280K available (14528K kernel code, 1012K rwdata, 4060K rodata, 2176K init, 571K bss, 193216K reserved, 786432K cma-reserved)
xilinx-kv260-starterkit-20221:~$ sudo insmod u-dma-buf.ko
xilinx-kv260-starterkit-20221:~$ dmesg | grep u-dma-buf
[  117.342056] u-dma-buf udmabuf@40000000: driver probe start.
[  117.342904] u-dma-buf udmabuf@40000000: assigned reserved memory node riscv_buf@40000000
[  117.405800] u-dma-buf udmabuf0: driver version = 4.0.0
[  117.405816] u-dma-buf udmabuf0: major number   = 237
[  117.405821] u-dma-buf udmabuf0: minor number   = 0
[  117.405826] u-dma-buf udmabuf0: phys address   = 0x0000000040000000
[  117.405831] u-dma-buf udmabuf0: buffer size    = 268435456
[  117.405838] u-dma-buf udmabuf@40000000: driver installed.

CMA領域が正しく確保されており、u-dma-bufのロードも正しくできているように見える。

VexRiscv動作確認

VexRiscv_Ultra96/test.cpp at dev · lp6m/VexRiscv_Ultra96 · GitHubをベースにudmabufを使用するように修正する。
リセットのGPIOは前回と同じくgpio-172を使用する。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <stdlib.h>
#include <fcntl.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>

#define REG(address) *(volatile unsigned int*)(address)


int pl_resetn_1(){
    int fd;
    char attr[32];

    DIR *dir = opendir("/sys/class/gpio/gpio172");
    if (!dir) {
        fd = open("/sys/class/gpio/export", O_WRONLY);
        if (fd < 0) {
            perror("open(/sys/class/gpio/export)");
            return -1;
        }
        strcpy(attr, "172");
        write(fd, attr, strlen(attr));
        close(fd);
        dir = opendir("/sys/class/gpio/gpio172");
        if (!dir) {
            return -1;
        }
    }
    closedir(dir);

    fd = open("/sys/class/gpio/gpio172/direction", O_WRONLY);
    if (fd < 0) {
        perror("open(/sys/class/gpio/gpio172/direction)");
        return -1;
    }
    strcpy(attr, "out");
    write(fd, attr, strlen(attr));
    close(fd);

    fd = open("/sys/class/gpio/gpio172/value", O_WRONLY);
    if (fd < 0) {
        perror("open(/sys/class/gpio/gpio172/value)");
        return -1;
    }
    sprintf(attr, "%d", 0);
    write(fd, attr, strlen(attr));

    sprintf(attr, "%d", 1);
    write(fd, attr, strlen(attr));
    close(fd);
    return 0;
}

unsigned int float_as_uint(float f){
    union {float f; unsigned int i; } union_a;
    union_a.f = f;
    return union_a.i;
}

float uint_as_float(unsigned int i){
    union {float f; unsigned int i; } union_a;
    union_a.i = i;
    return union_a.f;
}

// This program is DMEM[0]+DMEM[1]=DMEM[2]
int main(){
    int fd  = open("/dev/udmabuf0", O_RDWR);
    if (fd < 0) {
        printf("Device Open Error");
        exit(-1);
    }

    volatile unsigned int* MEM_BASE = (volatile unsigned int*) mmap(NULL, 0x10000000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    //set RISC-V Instruction
    MEM_BASE[0] = 0x40020437; //  0: lui s0,0x40020000
    MEM_BASE[1] = 0x00040413; //  4: mv  s0,s0
    MEM_BASE[2] = 0x00042607; //  8: flw  fa2,0(s0) # 0x40020000
    MEM_BASE[3] = 0x00442687; //  C: flw  fa3,4(s0)
    MEM_BASE[4] = 0x00c68753; // 10: fadd fa4,fa2,fa3
    MEM_BASE[5] = 0x00e42427; // 14: fsw  fa4,8(s0) # 0x40020000
    MEM_BASE[6] = 0x0000006f; // 18: j   0x18

    //TEST start
    srand(100);
    int all_ok = 1;

    for(int i = 0; i < 100; i++){
        float a = (rand()%100)/100.0f;
        float b = (rand()%100)/100.0f;
        //set input data
        MEM_BASE[0+4096] = float_as_uint(a);
        MEM_BASE[1+4096] = float_as_uint(b);
        //reset to launch RISC-V core
        pl_resetn_1();
        //wait RISC-V execution completion by waiting some period or using polling
        usleep(100);
        //get output data
        unsigned int _c = MEM_BASE[2+4096];
        float c = uint_as_float(_c);
        printf("%f+%f=%f:", a, b, c);
        if (a + b == c){
            printf("OK\n");
        } else {
            printf("NG\n");
            all_ok = 0;
        }
    }
    if (all_ok) printf("ALL PASSED\n");
    close(fd);
    return 0;
}

全ての結果が0のままでエラーになってしまった。
どこに問題があるのかを考え中。今だにPS/PL間のデータのやり取りの方法やZynqのアーキテクチャを全く理解できていないように思う・・

KV260でVexRiscv動作させた

自分用メモで超手抜き記事です。

rv32imfacアーキテクチャRISC-Vを動作させる。ARMコアからFPGA上に実装したRISC-Vコアを制御する。
Ultra96-V2で動かすための方法は以下リポジトリに(そこそこ詳しく?)まとめています。ほとんど同じです。

github.com

使用するSDイメージ

KV260向けにVitisプラットフォームを作成してDPUを動かす その1 (Vitis 2022.1 + Vitis-AI v2.5) - Qiitaで作ったSDイメージのpetalinuxプロジェクトを基にする。
上記記事のSDイメージではgeneric-uioドライバが有効化されていなかった。後から有効化する方法がよくわからなかったのでとりあえずbootargsを変更して再ビルドしてSDイメージを再生成した。

project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi の修正
bootargsの末尾にuio_pdrv_genirq.of_id=generic-uioを追加
再ビルドしてSDイメージ再生成

petalinux-build
petalinux-package --boot --u-boot --force
petalinux-package --wic --images-dir images/linux/ --bootfiles "ramdisk.cpio.gz.u-boot,boot.scr,Image,system.dtb,system-zynqmp-sck-kv-g-revB.dtb" --disk-name "mmcblk1"

ブロックデザイン

KV260向けにVitisプラットフォームを作成してDPUを動かす その1 (Vitis 2022.1 + Vitis-AI v2.5) - Qiitaで作ったブロックデザインを元にRISC-Vコアを追加した。

ブロックデザイン

pl_clk1は150MHzに設定した。BRAMのサイズやAXI BRAM Controllerなどに設定するメモリアドレスはUltra96-V2のときと同じにした。

プラットフォームをriscv_base_prj.xsaとして生成した。

binの生成

Ultra96-V2のときはpetalinuxで起動時に書き込まれるビットストリームを RISC-Vのものしていたが、KV260ではデバイスツリーオーバレイを使って起動後にビットストリームを書き込むのが標準(?)らしい。
このため手順が異なる。参考: FPGAの部屋 kv260_median_platform のメディアン・フィルタを KV260 の Petalinux から動作させる14

bootgenを使ってビットストリームをbinに変換する。

mkdir bit
cd bit
cp ../vivado/riscv_base_prj/riscv_base_prj.bit system.bit
echo 'all:{system.bit}'>bootgen.bif
bootgen -w -arch zynqmp -process_bitstream bin -image bootgen.bif
mv system.bit.bin riscv_base_prj.bit.bin

pl.dtsiの生成

バイスツリーを生成する。

xsct
createdts -hw vivado/riscv_base_prj/riscv_base_prj.xsa -zocl -platform-name mydevice -git-branch xlnx_rel_v2022.1 -overlay -compile -out mydevice
exit

pl.dtsiの修正

AXI Bram Controllerのデフォルトのドライバはxlnx,axi-bram-ctrl-4.1になっているがgeneric-uioで制御したいのでdtsiファイルを修正する。

mydevice/mydevice/mydevice/psu_cortexa53_0/device_tree_domain/bsp/pl.dtsiを開く

diff pl.dtsi.old ./mydevice/mydevice/mydevice/psu_cortexa53_0/device_tree_domain/bsp/pl.dtsi
54c54
< 				compatible = "xlnx,axi-bram-ctrl-4.1";
---
> 				compatible = "generic-uio";
75c75
< 				compatible = "xlnx,axi-bram-ctrl-4.1";
---
> 				compatible = "generic-uio";

pl.dtsiをコンパイルしてdtboの作成

mkdir device_tree
dtc -@ -O dtb -o mydevice/mydevice/mydevice/psu_cortexa53_0/device_tree_domain/bsp/pl.dtbo mydevice/mydevice/mydevice/psu_cortexa53_0/device_tree_domain/bsp/pl.dtsi
cp mydevice/mydevice/mydevice/psu_cortexa53_0/device_tree_domain/bsp/pl.dtbo device_tree/riscv_base_prj.dtbo

KV260に送るファイルの用意

binファイルとdtboファイル、shell.jsonファイルを1つのディレクトリにまとめる

mkdir riscv_base_prj
cp bit/riscv_base_prj.bit.bin riscv_base_prj
cp device_tree/riscv_base_prj.dtbo riscv_base_prj
touch riscv_base_prj/shell.json

shell.jsonの中身は以下

{
  "shell_type" : "XRT_FLAT",
  "num_slots": "1"
}

KV260に送る

scp -r riscv_base_prj petalinux@192.168.xxx.xxx:~/

KV260でのロード

FPGAビットストリームおよびデバイスツリーを読み込む。

sudo cp -r riscv_base_prj /lib/firmware/xilinx/
sudo xmutil listapps
sudo xmutil unloadapp
sudo xmutil loadapp riscv_base_prj

uioを確認すると、uio4, uio5が新たに増えていた。

RISC-Vコア動作確認

VexRiscv_Ultra96/petalinux at dev · lp6m/VexRiscv_Ultra96 · GitHubでも使用した、floatの足し算を行うテストプログラムを実行しようと思う。
test.cppを2点修正する必要がある。
テストプログラムでは100個のfloatの足し算のテストを行うが、1回のテストごとにRISC-Vコア(+ブロックRAMやAXI Interconnectなどすべて)をリセットする。
上で示したブロックデザインの通り、RISC-Vコアのリセットはpl_rstn1に接続されている。これをPSコアから接続するためのGPIOの番号がUltra96-V2の時と異なる。
参考: lp6m.hatenablog.com

KV260でGPIO情報を見ると以下のように表示される。

xilinx-kv260-starterkit-20221:/home/petalinux# cat /sys/kernel/debug/gpio 
gpiochip1: GPIOs 0-173, parent: platform/ff0a0000.gpio, zynqmp_gpio:
 gpio-0   (QSPI_CLK            )
 gpio-1   (QSPI_DQ1            )
 gpio-2   (QSPI_DQ2            )
 gpio-3   (QSPI_DQ3            )
 gpio-4   (QSPI_DQ0            )
 gpio-5   (QSPI_CS_B           )
 gpio-6   (SPI_CLK             )
 gpio-7   (LED1                |heartbeat           ) out lo 
 gpio-8   (LED2                |vbus_det            ) out hi 

上記ブログ記事を参考にすれば、デバイスに認識されているGPIOの番号がUltra96-V2がgpio-338からgpio-511だったのが、KV260ではgpio-0からgpio-173であることがわかった。(数は同じ174個)
というわけで、pl_rstn1を操作するにはgpio-172を操作すればいい。
下記テストプログラムの510を全て172に変更する。
VexRiscv_Ultra96/test.cpp at dev · lp6m/VexRiscv_Ultra96 · GitHub

また、AXI BRAM Controllerはuio4, uio5として認識されているので、デバイスオープンの/dev/uio0, /dev/uio1/dev/uio4, /dev/uio5に変更する。

FPU含めて動作完了!

rstan がWindowsで動作しない問題の解決

知り合いから頼まれたので備忘録。Rなんて使うことない気がしますが。

  • R-4.2.0 for Windows
  • R Tools 4.2.0
  • R Studio Desktop
  • rstan

qiita.com
こちらの記事を参考に、RStanをインストール、サンプルを実行すると以下のエラーで落ちる。

> x <- rbinom(n = 100, size = 20, prob = 0.8)
> binomial_test <- "
+       data {
+       int N;
+       int n;
+       int x[n];
+   }
+   parameters {
+       real<lower=0, upper=1> p;
+   }
+   model {
+       x ~ binomial(N, p);
+   } "
> d <- list(N = 20, x = x, n = length(x))
> fit <- stan(
+     model_code = binomial_test ,
+     data= d)
make cmd is
  make -f "C:/PROGRA~1/R/R-42~1.0/etc/x64/Makeconf" -f "C:/PROGRA~1/R/R-42~1.0/share/make/winshlib.mk" CXX='$(CXX14) $(CXX14STD)' CXXFLAGS='$(CXX14FLAGS)' CXXPICFLAGS='$(CXX14PICFLAGS)' SHLIB_LDFLAGS='$(SHLIB_CXX14LDFLAGS)' SHLIB_LD='$(SHLIB_CXX14LD)' SHLIB="file131459b7778.dll" WIN=64 TCLBIN= OBJECTS="file131459b7778.o"

make would use
if test "zfile131459b7778.o" != "z"; then \
  if test -e "file131459b7778-win.def"; then \
    echo g++  -shared -s -static-libgcc -o file131459b7778.dll file131459b7778-win.def file131459b7778.o  -L"C:/rtools42/x86_64-w64-mingw32.static.posix/lib/x64" -L"C:/rtools42/x86_64-w64-mingw32.static.posix/lib"  -L"C:/PROGRA~1/R/R-42~1.0/bin/x64" -lR ; \
    g++  -shared -s -static-libgcc -o file131459b7778.dll file131459b7778-win.def file131459b7778.o  -L"C:/rtools42/x86_64-w64-mingw32.static.posix/lib/x64" -L"C:/rtools42/x86_64-w64-mingw32.static.posix/lib"  -L"C:/PROGRA~1/R/R-42~1.0/bin/x64" -lR ; \
  else \
    echo EXPORTS > tmp.def; \
    nm file131459b7778.o | sed -n 's/^.* [BCDRT] / /p' | sed -e '/[.]refptr[.]/d' -e '/[.]weak[.]/d' | sed 's/[^ ][^ ]*/"&"/g'  >> tmp.def; \
    echo g++  -shared -s -static-libgcc -o file131459b7778.dll tmp.def file131459b7778.o  -L"C:/rtools42/x86_64-w64-mingw32.static.posix/lib/x64" -L"C:/rtools42/x86_64-w64-mingw32.static.posix/lib"  -L"C:/PROGRA~1/R/R-42~1.0/bin/x64" -lR ; \
    g++  -shared -s -static-libgcc -o file131459b7778.dll tmp.def file131459b7778.o  -L"C:/rtools42/x86_64-w64-mingw32.static.posix/lib/x64" -L"C:/rtools42/x86_64-w64-mingw32.static.posix/lib"  -L"C:/PROGRA~1/R/R-42~1.0/bin/x64" -lR ; \
    rm -f tmp.def; \
  fi \
fi
Error in compileCode(f, code, language = language, verbose = verbose) : 
  C:\rtools42\x86_64-w64-mingw32.static.posix\bin/ld.exe: file131459b7778.o:file131459b7778.cpp:(.text$_ZN3tbb8internal26task_scheduler_observer_v3D0Ev[_ZN3tbb8internal26task_scheduler_observer_v3D0Ev]+0x1d): undefined reference to `tbb::internal::task_scheduler_observer_v3::observe(bool)'C:\rtools42\x86_64-w64-mingw32.static.posix\bin/ld.exe: file131459b7778.o:file131459b7778.cpp:(.text$_ZN3tbb10interface623task_scheduler_observerD1Ev[_ZN3tbb10interface623task_scheduler_observerD1Ev]+0x1d): undefined reference to `tbb::internal::task_scheduler_observer_v3::observe(bool)'C:\rtools42\x86_64-w64-mingw32.static.posix\bin/ld.exe: file131459b7778.o:file131459b7778.cpp:(.text$_ZN3tbb10interface623task_scheduler_observerD1Ev[_ZN3tbb10interface623task_scheduler_observerD1Ev]+0x3a): undefined reference to `tbb::internal::task_scheduler_observer_v3::observe(bool)'C:\rtools42\x86_64-w64-mingw32.static.posix\bin/ld.exe: file131459b7778.o:file131459b7778.cpp:(.text$_ZN3tbb10interface623task_
Error in sink(type = "output") : invalid connection

色々調べてると以下の情報を得た。
Error running Stan Model with rstan 2.21 and R 4.0.2 - RStan - The Stan Forums

As you all have noticed, there are a large number of people who are having problems with rstan 2.21.x on Windows.

というわけでrstanのバージョンを下げればとりあえず動く。
Rstanのバージョン一覧で2.21より前の最新は2.19.3なのでこれを入れる。
Index of /src/contrib/Archive/rstan
古いバージョンのインストール方法の参考:
https://support.rstudio.com/hc/en-us/articles/219949047-Installing-older-versions-of-packages

packageVersion("rstan")  //2.21.3が入っていることを確認
detach("package:rstan", unload=TRUE) //アンロード
install.packages("devtools")
require(devtools)
install_version("rstan", version = "2.19.3", repos = "http://cran.us.r-project.org")
library(rstan)
packageVersion("rstan") //2.19.3を確認

rstan 2.19.3をインストールした後はRstudioを再起動しないとうまく動作しなかった。
Rstudioを再起動してから再度Qiitaの記事のコードを実行すると問題なく動いた。
Makevars.winなどの修正はなしでとりあえず動いた。

第5回AIエッジコンテストに参加した感想

経産省主催の第5回AIエッジコンテストにチームVerticalBeachとして参加しました。開発したリポジトリの整理などは後ほど行う予定です。
参加した感想を忘れないうちに所感を残しておこうと思います。
詳細な技術情報は提出した以下のレポートに記載しています。
drive.google.com

コンテスト概要

  • 車載動画に対する人物・車のトラッキング処理
  • Ultra96v2またはRISCVボード上に実装
  • ラッキング処理の何らかの処理をRISCVコアを使用することが必須条件
  • 速度処理賞の入賞条件: 評価指標MOTA>0.6
  • イデア賞の入賞条件: 評価指標MOTA>0.3

終結

物体検出はそこそこできているが、物体追跡が甘い。MOTA=0.2807344
1フレームあたりの速度:52.30ms (18fps程度は出そう?)
上が物体検出結果、下が物体追跡結果です。
youtu.be

前回の第4回AIエッジコンテストでは、自分のチーム含め処理速度入賞者の全員がXilinx DPUを使っていて、ほとんどDPUコンテストと化していた。
それを受けてか今回はRISCVコアをFPGA上に実装することが必須条件であり、実装ハードルが高いことは最初からわかっていた。
コンテストの開催期間は4ヶ月で、RISCVの実装経験もなかったのでできるだけ既存の実装を使って、とにかく何が何でも提出する、ということを目標にした。

題材自体はHW実装なしの第3回AIエッジコンテストと同じで、第3回の入賞者レポートを見る限り、MOTA>0.6という精度指標をエッジデバイスでリアルタイム性能を出しながら満たすのは非常に厳しいと思った。
前回のコンテストでXilinx DPUとVitis-AIを使用した経験があったので、物体検出(DPU)+物体追跡(RISCV)の組み合わせで実装することにした。(DNNモデルを動かすためのRISCV追加命令の実装!とかは確実に間に合わなそうだったので)

10月・11月

DPUで動作する物体検出モデルを選定する必要があるが、tinyYOLOv3が動作することは知っていたので、精度が向上したtinyYOLOv4を使用してみることにした。
DarknetからCaffeに変換するスクリプトを修正することで、DPUにオフロードされるsubgraphが1個にできた。経験もあったので特に難しいことはなかった。
前回コンテストではAvnetが用意してくれたHW環境をそのまま使っていたので、HW側の設計はほとんどせず、ただDPUを触っていただけだった。
今回はDPUだけでなくRISCVを搭載する必要があったので、自分でHW環境をビルドする必要があった。前回何も理解していなかったVitisフローを勉強した。 DPUとvector addカーネルの両方を乗せることができた。
Ultra96v2のWIFIを動かすために無駄な時間を食ってしまったが、結局最終的なHW環境ではWIFIは使用せずにUSB-LANを使用することにした。
qiita.com

12月

11/30にコンテスト主催側からRISCVのリファレンス環境が公開された。VexRiscvというRISCV実装が使用されていた。公開されるまではRocketChipを使うかVexRISCVを使うか、あるいはRISC-VとChiselで学ぶ はじめてのCPU自作を使うか迷っていた。
RocketChipはFPGAでは動作周波数が低いことや、リファレンス環境を流用できることからVexRiscvを使用することにした。
GitHub - SpinalHDL/VexRiscv: A FPGA friendly 32 bit RISC-V CPU implementation
リファレンス環境はベアメタルでの動作確認のみだったので、Petalinuxから実行できるか実験した。
qiita.com

ただ、このリファレンス環境には問題があることがわかった。RISCVコアのリセットがGPIO経由で行われていたが、命令バス・データバスのリセットは別で接続されていたので、2回目のリセット以降正常に計算が行われないことがわかった。
RISCVコアのリセットをpl_resetn0に接続して、PS側から制御する方法を学んだ。
lp6m.hatenablog.com

次に、リファレンス実装のRISCVの命令セットはrv32imで、浮動小数点演算が行えないので困った。VexRiscvではプラグインを追加する形でFPUを載せられるので、追加した。
ただ、リファレンス実装ではRISCVコアの命令バス・データバスをAXIバスに接続するために独自モジュールを使っていて、FPUを乗せると命令バスとデータバスのポート数が変わるために独自モジュールが使えなくなり頭を抱えた。
よく見てみるとVexRiscv自体に命令バス・データバスをAXI化する機能があったのでこれを使うことで問題は解決した。(なぜリファレンス実装は独自モジュールを使っていたのか・・)
手順は今後公開する予定です。

年末年始にFPUが動いて嬉しかった。


1月

FPUを搭載したRISCV向けにクロスコンパイルする手法を調べた。cross-ngを使ってクロスコンパイルができることがわかった。RISCVとPSコア間のデータの入出力を実現するのに少しだけリンカスクリプトの勉強をした。
11月に勉強したVitisフローを使ってRISCVとDPUを両方搭載したHW環境を作ることができた。
チームメイトが物体追跡アルゴリズムの勉強・実装調査をしてくれていたので物体追跡はByteTrackを使用することにした。
ByteTrackはReIDモデルを使用しない単純なアルゴリズムにもかかわらずSOTAを実現しているので、何もわかっていないけど期待感だけあった。
GitHub - ifzhang/ByteTrack: ByteTrack: Multi-Object Tracking by Associating Every Detection Box
ByteTrackをそのまま適用したが、MOTAスコアは0.17程度で0.60には遥か遠く希望を失った。
今回物体追跡アルゴリズムについては自分は担当していないので詳しいことはわかっていないが、コンテストのテスト動画のフレームレートが5fpsで、1フレームごとにバウンディングボックスの移動量が非常に大きいのでカルマンフィルタによるバウンディングボックスの位置推定が非常に厳しいようだった。
DPUでの物体検出結果をチームメイトに渡して、チームメイトにはそれをもとにByteTrackの改善をしてもらっていた。チームメイトが開発してくれていたC++製のByteTrackがUltra96のARMコア上で動作することを確認した。
Ultra96のPetalinuxでインストールされていたOpenCVがmp4を開けなかったり(aviに変換して解決)、cv::VideoCaptureがメモリリークを起こしたりしていて(1動画ずつ処理して回避)、厳しかった。

2月

2月に入ってもまだRISCVコアで何を動作させるか決まっていなかった。ByteTrackで使用されているハンガリアン法のアルゴリズムが簡単にRISCV向けにクロスコンパイルできそうということで、これをRISCVで動作させることにした。クロスコンパイルするとRISCVのデータメモリが当初の大きさだと足りないことがわかって、Vitisフローを1からやり直してデータメモリを大きくした。厳しかった。
すべてARMコアで動作させていたByteTrackのソースを改変してDPUとRISCVを使用するようにした。
物体検出と物体追跡処理のマルチスレッド化を実装して、
チームメイトはギリギリまでByteTrackの改善をしてくれて、最終的に評価値は0.2807344まで改善した!感謝。
レポートを書いたり実機評価をして、提出締め切りギリギリに提出した。


提出者が少なかったせいか提出期限が伸びたので、YOLOv4tinyの入力画像サイズを大きくするなどしたが、MOTAが改善することはなかった。

とりあえず提出するという当初の目標は達成できたが、ほとんどVitisフローとVexRiscvの実装方法の勉強に費やしてしまっていた。前回のコンテストもVitis-AIについて学んでいただけだった・・
RISCVの実装・命令の拡張にチャレンジするという余裕は全くなかった・・
上には厳しい気持ちになったとたくさん書いたが、こういった実装コンテストのおかげで色々学べるのはやっぱり楽しかった。
HLSでDNNモデルをHW実装するコンテストがあれば、(少なくとも自分は)VivadoやVitisの使い方を学ぶ時間は少なくてすむので、HW実装に注力できて面白そうだと思った。