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アプリとの連携を試しましょう。

Next → PRUとLinuxアプリを連携してリアルタイム制御(任意波形を出力)