作成者:Taylor Roorda、最終更新日:2017年3月30日
はじめに
声明書:これは、ベアメタルマイクロコントローラのバックグラウンドを持っていた私の最初の重要なLinuxプロジェクトです。この記事は、同様の立場にある人々を対象としています。経験豊富なLinux開発者は、おそらく別のアプローチを取るのが良いでしょう。
最近、オーディオ処理に興味を持ち、Cirrus LogicのWM8731コーデックのブレークアウトボードであるMikroElektronikaのAudio Codec Proto(Digi-Key品番1471-1228-ND)を購入することにしました。コーデックは、デジタルオーディオデータにI2Sを、制御インターフェースにI2Cを使用します。ホストプロセッサには、使わずにとっておいたRaspberry Pi Zeroを選びました。今回は、Pi Zeroのみでテストしていますので、他のバージョンのPiでは改造しないと動作しない可能性があります。
Raspberry Piコミュニティ内に、生のオーディオデータを操作できるドライバがあることを願っていました。しかし多くは音楽ファイルの再生に重点を置いているようでした。最初の選択肢は、ALSA(Advanced Linux Sound Architecture)を通してサウンドカードのようにコーデックを使用するsnd_soc_wm8731とsnd_soc_rpi_protoモジュールを使用することで、期待できそうでした。しばらく実験してみましたが、結局音声の出し入れはできず、生データを扱う方法も見つけられませんでした。
次に、Raspberry Piのフォーラムでphilpooleが作ったドライバに出会いました。彼のソースコードを見て、PiのI2Sインターフェースの内部構造についてより良いアイデアを得ましたが、それはまだALSAベースで、送信のみで、私が望んでいたものとは全く違っていました。
いろいろ探した結果、Stack Overflowで見たコメントに触発されて、自分のやりたいことだけを実現するドライバを自作することにしました。私の主な要件は、私のセットアップで生のオーディオサンプルを送信および受信することでした。理想は両方を同時に行うことですが、Pi Zeroがそれをうまく処理できるとは思えませんでした。また、通信プロトコルのみを扱う汎用的なものにしたいと思いました。コーデック固有の要件は、外部で簡単に処理することができます。
動作概要
ソフトウェア
このプログラムの中核は、キャラクタドライバと呼ばれるロード可能なカーネルモジュールの一種です。Linuxカーネルからユーザー空間へ、文字(バイト)のストリームでデータを転送することから、「キャラクタ」と呼ばれています。カーネルモジュールとキャラクタドライバの詳細については、「The Linux Kernel Module Programming Guide」をご覧になることを強くお勧めします。少し古いですが、それでも入門にはとても役に立ちます。
基本的にドライバは、使用したいI2Sインターフェースを表す/dev内の特定のファイル、またはノードに対して呼び出されたときに、open、close、readおよびwriteなどのファイル操作がどのように動作するかを記述するだけです。ioctl関数は、デバイスの設定を変更したり、ドライバから他の情報を取得するために使用することもできます。カーネルモジュールはinit_moduleとcleanup_moduleの2つだけが必須関数です。ドライバが最初にLinuxにロードされるとき、init_module関数が呼び出され、ドライバにメジャー番号を割り当て、ドライバが必要とするあらゆるセットアップコードを実行します。メジャー番号は、デバイスファイルとドライバをリンクするために使用されます
処理のほとんどは、device_read関数とdevice_write関数で行われます。これらの関数は、カーネル内のサンプルバッファとユーザー空間で動作するプログラムとの間でデータを転送します。カーネルのバッファサイズは任意に16Kサンプルを選択しました。送信割り込みがトリガされると、対応するバッファからデータが引き出され、ハードウェアFIFOに格納されます。同様に、受信割り込みがトリガされると、ハードウェアFIFOから引き出されてカーネルバッファに格納されます。
ハードウェア
ARMペリフェラルのデータシートには、I2Sペリフェラルが使用するすべてのレジスタとその物理アドレスおよびバスアドレスが記載されています。これらのアドレスはプロセッサが直接使用することができないため、再マップする必要があります。ioremap関数は、物理アドレスとサイズを受け取り、プロセッサがハードウェアへの読み書きに使用可能な仮想アドレスを返します。ARMのデータシートの冒頭には、仮想アドレス、物理アドレスおよびバスアドレスの違いについて、より詳しい情報が掲載されています。
ハードウェアの設定は、i2s_init_default()関数で行います。主な設定項目は、通信モード(マスター/スレーブ)、チャンネル設定および割り込みやDMAの使用などです。デフォルトでは、Piはコーデックからクロックとフレームシンクの信号を入力するスレーブとなります。その理由は、フォーラムで「Piはオーディオに適した周波数を生成できない」という苦情が多かったからです。コーデックボードは独自の発振器を持っており、正しいクロック周波数を生成することができるようになっています。私のコーデックは24ビットなので、デフォルトのチャンネル幅も24ビットで、最上位ビットは各フレームの第2クロック立ち上がりエッジに配置されます。最後に、送信機へのデータの書き込みや受信機からの読み出しが必要な場合に、それを知らせるための割り込みを有効にします。DMAを使えばプロセッサの効率は上がりますが、割り込みの方が設定しやいです。将来の改訂でDMAを取り入れる可能性もありますが、現時点では割り込み駆動で十分機能します。
サポートスクリプト
モジュール本体の他に、モジュールのロードと削除を行うための2つの簡単なBashスクリプトがあります。i2s-install.shスクリプトは、関連するGPIOピンがI2S機能を使用するように設定し、モジュールをビルドします。GPIO alt関数の設定は、Raspberry Pi フォーラムのTim Giles氏のラッパーを使用します。i2s-uninstall.shスクリプトは、GPIOをWiringPiの入力としてデフォルトの機能に戻し、モジュールを削除します。
補足:GPIO ピンの構成はドライバの初期化コードで行うことができますが、GPIO メモリは別のドライバによって要求されます。上記方法でとにかくアクセスできますし、小さな別のプログラムを使用するのと同じくらい簡単です。
また、I2C経由でWM8731コーデックをセットアップするために、非常にシンプルなPythonスクリプトを使用しています。このスクリプトのソースは、このコーデックを使用する他のユーザーのために、リポジトリに含まれています。
使用方法
モジュールをコンパイルするために、Piのどこかにカーネルソースが必要です。通常、これは、/lib/modulesにあります。私はこの部分でかなり長い間行き詰まりましたが、最終的にはrpi-updateによって必要なものが入手できたと思っています。
ファームウェアをアップデートします。
# Run these if you don't have the kernel source on your device
sudo apt-get update && sudo apt-get upgrade
# Verify that there is an directory in /lib/modules that matches what you get from uname -r
uname -r
ls /lib/modules
# You may need to run this too if the above doesn't work
# sudo apt-get install rpi-update
# rpi-update
gpio_altをコンパイルします。
# Credit to TimG for this program
gcc -o gpio_alt gpio_alt.c
sudo chown root:root gpio_alt
sudo chmod u+s gpio_alt
sudo mv gpio_alt /usr/local/bin/
このプログラムは、インストールスクリプトで使用します。
WiringPiを確認します。
gpio -v
これは、アンインストールスクリプトで使用されます。何も表示されない場合は、http://wiringpi.com/download-and-install/ にあるインストール手順を参照してください。
Github(https://github.com/taylor-roorda/raspberry-pi-i2s-driver.git )からドライバのリポジトリをクローンします。必要に応じて、 ${shell pwd}を任意のディレクトリに置き換えてください。
git clone git@github.com:taylor-roorda/raspberry-pi-i2s-driver.git ${shell pwd}
モジュールをビルドしてロードします。
./i2s-install.sh
これでドライバがインストールされ、使用できるようになりました。通常のファイルと同じように、/dev/i2sに対して読み書きができます。以下は基本的な操作の簡略化した例です。
#include <uinstd.h>
int i2s = open("/dev/i2s", O_RDWR);
// Read a single sample
// All samples are stored as 32 bit values so a single sample transfer is always 4 bytes.
int32_t sample;
read(i2s, &sample, 4);
// Write a single sample
write(i2s, &sample, 4);
close(i2s);
Githubのreadmeにはドライバの使い方の詳細が書かれており、また、リポジトリにはdriver_testというC言語のサンプルプログラムもありますので、参考にしてください。
モジュールを削除するにはつぎを行います。
./i2s-uninstall.sh
観測結果
このドライバの欠点は、CPUの使用率が高いことです。私のテストでは、サンプルレート48kHzを使用していましたが、私のテストプログラムがサンプルの読み書き以外のことをしていた時は、バッファオーバーフローとアンダーフローに悩まされました。通常、これはカーネルがハングアップする原因となるため、現在のバージョンではタイムアウト後にインターフェースを無効にするようにしています。DMAを使えば、割り込みの時間を短縮できるため、この問題は解決するはずです。Raspberry Piは大きなモデルほど、処理能力が高いので、よりうまく処理できると思います。最終的には、このようなアプローチは、ベアメタルマイクロコントローラやRTOSを実行するものに適しているでしょう。
Raspberry Piは、ユーザー空間から作業する場合は多くのサンプルやドキュメントがありますが、カーネルレベルで作業する場合はほとんどありません。あったとしても、そのわずかな資料が混乱を招くことがあります。このため、Linuxのベテランでなければ、始めるのが少し難しくなっています。「Xはどの割り込み番号に割り当てられているか」、「どのDMAチャネルが使用可能か」など、ごく簡単な質問に対する答えを見つけるのは、本当に大変なことなのです。
例えば、ARMのデータシートには、I2S割り込みはIRQ 55と書かれています。BroadcomチップのLinuxヘッダでは、platform.hにI2S割り込みはIRQ 81と書かれています。両方を試しても結果が出なかったので、cat /proc/interrupts の出力を見てみました。I2CとSPIを有効にしていたので、それぞれIRQの77と78に割り当てられていることがわかりました。I2S割り込みはその次であるはずです。IRQ79は結局動いたのですが、それが一体なぜなのか、まだよくわかりません。結局、これが割り込みとDMAのどちらを選択するかの決め手となりました。割り込みは、実際に正しいIRQ番号を決定することができたので、取り掛かりやすかったです。
まとめ
ここでご紹介したドライバは、Linuxにすでに入っているデフォルトのドライバと競合するものではありません。これは、PiのI2Sインターフェースを使用したい人に、低レベルのアクセスを提供しているだけです。リアルタイムのオーディオ処理は行いませんが、オーディオファイルの再生や録音、任意波形の生成など、簡単なオーディオ用途に使用することができます。また、I2Sなどのドライバを独自に開発することに興味がある方にも参考になるのではないでしょうか。
外部リンク
このドライバの作業中に出会った便利なドキュメントをいくつか紹介します。
- Githubリポジトリ:https://github.com/taylor-roorda/raspberry-pi-i2s-drive
- Linuxカーネルモジュールのプログラミングガイド:http://www.tldp.org/LDP/lkmpg/2.6/html/
- Linuxデバイスドライバ、第2版:http://www.xml.com/ldd/chapter/book/index.html
- BCM2385 ARMペリフェラルデータシート:https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf
ご質問/ご意見
ご質問やご意見はDigi-KeyのTechForumまでお願いします。