PRUを使えばLinuxを使っていても真のリアルタイム処理が実現できる
リアルタイム処理/制御が求められる時、LinuxなどのOSには、どうしても超えられない壁があります。
汎用OSの宿命ではありますが、様々な機能が盛り込まれたOSは、様々な処理を並行して行わなければならないため、アプリケーションがリアルタイム処理を行いたくても、どうしても遅延が発生してしまいます。
それを緩和する手段の一つは、以前紹介しました。
割り込みドライバからシグナル(ソフト割り込み)経由でアプリケーションに渡す方法です。
でもこれは、あくまでもLinux上で実現しているので、最大遅延を1ms程度に抑えるのが限界です。
割り込みが増えるともっと延びてしまうでしょう。
さて、そこで。
今回は、Linuxを使っていながら(RTOSを超えるほどの)真のリアルタイム制御を実現する方法をご紹介。
「PRU」(Programmable Real-time Unit)を使います。
見逃せない、PRUの利点
TIのARMプロセッサ AM572xシリーズ(AM5728など)や AM335xシリーズ(AM3358など)、及び
同プロセッサを内蔵したOSD3358が搭載されている BeagleBone シリーズ(BeagleBoneBlack や PocketBeagle)などに、
PRU(PRU-ICSS)が搭載されています。
PRUとは、プロセッサの中に内蔵された、ARMコアとは別のCPUのことです。
例えば、Linuxを起動している手元のボード(PocketBeagle)では、内蔵されている2つのPRUは何もせずヒマしています。
200MHzの実力を持ったCPU 2つが、何の仕事もない状態で、余っている状態です。
今どきのCPUはGHzがあたりまえですから、200MHzは見劣りします。
しかし、長年組み込み業界に携わっているエンジニアの皆さんや、ArduinoやSTM、RL、SHシリーズなどでいろいろ作っている皆さんは、これがいかに凄くてもったいないことか、分かると思います。
Linuxを動かしてるARMコアと違い、PRUはヒマですから、ちょっと命令を与えれば、集中してその命令を忠実に一切脇目も振らず、劇的な高速処理で実行します。
例えば、1us(1msの1000分の1)のパルスを、1usも遅延することなく、吐き出すことができます。
例えば、GPIOに入力された信号などを演算処理し、結果を別のGPIOへ出力する。その所要時間が1us以下。
これはRTOSでも困難な(現実的には無理な)数字です。
そして、ARMコアと連携できる、というところがポイントです。
PRUが単にヒマなだけのCPUであれば、複数のCPUを用いるのと大差ありません。
しかし、1つのプロセッサの中にARMコアと一緒に内蔵され、それらが色々な方法で内部接続されていると、途端に利用価値が跳ね上がります。
例えば、UIや複雑な処理はARMコア(Linux)に任せ、リアルタイム制御が必要な部分だけPRUに指示を飛ばして、PRUで実行する、ということができるわけです。
うまく作れば「Linux使ってるのに完璧なリアルタイム処理が実現できてる」という状態に仕上げることができるのです。
いつもながら前置きが長いですね。(今日も訪問先でやらかしたばかりでした)
早速、PRUを使ってみましょう。
PRUの実行方法
PocketBeagle(AM3358)でLinuxとPRUを実行してみます。
SDカードにOSイメージ(bone-debian-9.3-iot-armhf-2018-03-05-4gb か bone-debian-9.3-lxqt-armhf-2018-01-28-4gb)を書いてLinuxを起動しました。
初回は、以下の作業が必要です。
TIのページに図などが載っていますので、併せて参照してください。
- 起動設定を書き換え、PRU(LinuxにおけるPRU制御モジュール)を有効にします。
/boot/uEnv.txt に以下を追記。
uboot_overlay_pru=/lib/firmware/AM335X-PRU-RPROC-4-9-TI-00A0.dtbo
(kernelバージョンが異なる場合は、別のdtboファイルに変わる場合があります。
今回は以下の様に4.9だったので、上記ファイルを指定しました。)
# uname -r
4.9.78-ti-r94 - 起動設定を書き換えたので、再起動します。
- コンパイラなどのパスを通すため、シンボリックリンクを作ります。
ln -s /usr/bin/ /usr/share/ti/cgt-pru/bin - PRU用のプログラム(後述)をコンパイルし、バイナリファイルを /lib/firmware に配置します。
(makeでコンパイル、installを付けると配置まで行います)
make install - 配置したバイナリファイルをPRUその1を関連付けます。
echo ‘am335x-pru0-fw’ > /sys/class/remoteproc/remoteproc1/firmware
実行は以下の様に行います。2回目以降は以下の作業だけでOKです。
- 実行。
echo ‘start’ > /sys/class/remoteproc/remoteproc1/state - 必要に応じ、各ピンをPRUの入出力につなぐ。
例:
config-pin P1_36 pruout
config-pin P2_18 pruin
PRU用のプログラムを変更したときは、以下の作業で反映/即実行開始されます。
- コンパイル/再配置。(cleanを付けると、毎回フルコンパイルします)
make clean install - 一旦止めて、再実行。
echo ‘stop’ > /sys/class/remoteproc/remoteproc1/state
echo ‘start’ > /sys/class/remoteproc/remoteproc1/state
これで、1つ目のPRU(PRU0)が動きました。
2つ目のPRU(PRU1)を動かす場合は、am335x-pru1-fw と /sys/class/remoteproc/remoteproc2/ を使用します。
PRU用のプログラムをコーディング/コンパイル
サンプルプログラム:gpio_pru.tar.gz
これを解凍して前述のコマンドでコンパイル/実行すると、
GPIO入力 P2_18 が1(High)の間、GPIO出力 P1_36 にパルスを出力します。
(前述の様に、config-pin でピンを入力で使うか出力で使うか設定しています。)
PRUはARMとは異なるCPUですので、コンパイラも別の物を使用します。
(Makefileを見ると、gcc ではなく clpru を使ってコンパイル/リンクしていることが分かります)
ソース抜粋:
volatile register unsigned __R30; // GPIO output in PRU
volatile register unsigned __R31; // GPIO input in PRU
#define OUTPUT_0 0 // P1_36 PRU0 - 0
#define INPUT_0 15 // P2_18 PRU0 - 15
void main(void)
{
int pol = 0;
while (1)
{
if(__R31 & (1 << INPUT_0))
{
pol = 0;
__R30 = __R30 & ~(1 << OUTPUT_0);
}
else
{
if(pol)
{
pol = 0;
__R30 = __R30 & ~(1 << OUTPUT_0);
}
else
{
pol = 1;
__R30 = __R30 | (1 << OUTPUT_0);
}
__delay_cycles(1000);
}
}
}
レジスタR30(__R30)がGPIO出力になっています。
レジスタR31(__R31)がGPIO入力になっています。
R30/31のbit0がPRUのGPIO0、bit2がGPIO1…bit15がGPIO15です。
__delay_cycles()の数値で、休む時間を指定します。
単位はクロック数です。
つまり、これを小さくするとnsオーダー(1usの1000分の1)の世界に。
手元に高性能なオシロが無かったので1000にしてます。
オシロで波形を見ても、まったく遅延や乱れが見られません。
LEDで見てもチラツキや瞬きは見られません。
同じようなことをLinuxやRTOSで行うと、GPIO出力が遅延し、パルスのタイミングが乱れます。
理屈はともかく、実際に使うまでは「ホントに?一切邪魔されないの?」と多少疑ってましたが…
ホントに完璧です。
これは使えますね。
次はLinuxアプリとの連携を試しましょう。