s

コア間のデータ転送機能(リングバッファ)を実装し、データの解析やデバッグ作業に役立てる


図
図1:メインコアとサブコアの間でデータを転送できるリングバッファを理解する

ソフトウェア開発では、常に効率の良いデバッグ手法が求められています。第5回ではJTAG-ICEデバッガを使って、メインコア上で実行されているプログラムの内部状態や処理対象のデータを可視化する方法について解説しました。それでは、SPRESENSE™のサブコア上で実行されているプログラムのデバッグは、どうすれば良いでしょうか。

今回の初心者講座では、サブコアの内部状態や処理対象となったデータの断片を、順序付けてメインコアへと送出できる『リングバッファ』について紹介いたします。なお、今回紹介する機能に対応したC言語のソースコードはGitHubにて公開しています。解説だけでなく、ソースコード・リーディングも活用し、コア間の連携方法への理解を深めましょう。

また、リングバッファは同期オブジェクト(ミューテックスロック、共有メモリ)を組み合わせた非同期型の通信オブジェクトです。特にマルチコア・アーキテクチャでは、デバッグ用途に限らず、コア間のデータ共有・転送機能としても活用されています。それではSPRESENSE™を片手に、最後までお付き合いください。

図

▼目次

リングバッファとは

リングバッファは、メッセージの送信元が任意のタイミングでEnqueue(情報をリングに格納)し、受信先が適当なタイミングDequeue(情報をリングから採取)することのできる非同期型の通信オブジェクトです(図2の①)。リングという名前の通り、末尾までデータが格納された後(図2の②)は、先頭に戻ってデータを格納します(図2の③)。

なお、リングに格納されている有効なデータの範囲はHeadとTailによって管理されます。先頭を『head(次にDequeueする位置)』と呼び、末尾を『tail(次にEnqueueされる予定の位置)』と呼びます。

図
図2:データ交換用のオブジェクト「リングバッファ」

リングバッファの実装方法を理解する

今回のサンプルコードには、サブコアまたはメインコアいずれからもEnqueue/Dequeueできるリングバッファが実装されています。debugring.cは、メインコアのソースコードフォルダ(aps_multicore)と、サブコアのソースコードフォルダ(aps_multicore_worker)のそれぞれに格納され、Enqueue/Dequeue操作用の関数を提供します。これらの関数を呼び出すことにより、メインコアからサブコアへ、サブコアからメインコアへデータを送信できます。

Enqueueするためのソースコード(サブコア・メインコア共に同じ)

debugring.c 内の関数「enqueue_debugring」

Dequeueするためのソースコード(サブコア・メインコア共に同じ)

debugring.c 内の関数「dequeue_debugring」

リングバッファに格納できる情報とは

本例で紹介するリングバッファには、EnqueueしたCPUの識別子(メインコアは0、サブコア#1-#5はそれぞれ3~7)、パラメータ情報(Enqueue元が自由に指定できる4byteの情報)、そして非定型なデータを格納するためのバッファ(1KB)のそれぞれに情報を格納することができます。これらの情報はEnqueue完了からDequeue完了まで変質することはありません。

debugring.h 内にて定義している構造体「sDebugRingItem」

debugring.h 内にて定義している構造体「sDebugMemPool」

リングバッファにロック(ミューテックスロック)をかける

今回の実装では、ひとつのリングバッファを複数のCPUコアから操作できるよう、リングのhead情報やtail情報(sDebugRingHeader構造体)の操作を同時にひとつのCPUコアに限定する「ミューテックロック」を利用し、一貫性を担保しています(クリティカル・セクション:図3、図4)。headとtailが複数のCPUから同時に操作できてしまうと、他のCPUがEnqueueしたデータを上書きしてしまったり(データの消失)、他のCPUと同じデータをDequeueできてしまう(意図しない複製)といった問題が発生します。

図
図3:Dequeue処理におけるクリティカル・セクション

図
図4:Enqueue処理におけるクリティカル・セクション

SPRESENSE™のメモリタイルを活用する

続いて、リングバッファをメモリ上に配置する方法について解説します。SPRESENSE™のメモリは、128KBのメモリタイル(メモリの最小構成)12枚から構成されており、CPUコアには128KB単位で共有メモリを割り当てた状態が、最もメモリを有効活用できている状態です。

そこで、本プログラムでは、割り当てた1つのメモリタイルの後半64KBのみを利用しリングバッファを構成しています(図5)。前半の64KB領域は、アプリケーション・プログラムが自由に使う用途を想定し、未使用状態としています(リングバッファ機能が参照・変更することはありません)。

図
図5:本プログラム実行中のメモリの利用状況

	/** Ring Buffer Structs Definitions */
	typedef struct _s_debugring_head {
		int head;
		int tail;
		mpmutex_t *pmutex;
		int _reserved_1;    
	} sDebugRingHead;
	
	typedef struct _s_debugring_item {
		int cpuid;
		int param;
		void *addr;
		int size;    
	} sDebugRingItem;
	
	/* pool buf */
	typedef struct s_mempool_item {
		char buf[SIZE_DEBUGRING_MEMPOOL];
	} sDebugMemPool;
	
	/* Memory Layout */
	typedef struct s_debugring {
		sDebugMemPool memPool[NUM_DEBUGRING_ITEMS];
		sDebugRingItem ring[NUM_DEBUGRING_ITEMS];
		sDebugRingHead header;
	} sDebugRing;

リングバッファがEmpty状態である状況(Dequeue禁止状態)を検出する

リングバッファがEmpty(空)の場合、Dequeue(情報を取り出す)ことはできません。取り出せない状況かどうかは、下記のプログラムで検知することができます。もちろん、リングバッファが初期化された直後はEmpty状態(Head=Tail=0)です。

	if (h == t) {
		/* empty */
		...

図
図6:リングバッファがEmptyの状態におけるHead変数とTail変数の関係

リングバッファがFull状態である状況(Enqueue禁止状態)を検出する

リングバッファがFull(満杯)の場合、Enqueue(情報を格納)ことはできません。もし、格納すると有効なデータのうち最も古い情報が上書きされ、失われてしまいます。格納できない状況かどうかは、下記のプログラムで検知することができます。リングバッファの初期化後、一度もDequeueせずにEnqueueし続けると、Full状態(Head=0, Tail=47[最大])となります。

/* RING CONTROL */
#define NEXT_RING_POS(h) (((h+1) >= NUM_DEBUGRING_ITEMS)? 0 : h+1)
	...
	if (h == NEXT_RING_POS(t)) {
		/* overflow */
		...

図
図7:リングバッファがFullの状態におけるHead変数とTail変数の関係

Enqueue禁止状態状態の扱い方を考える。

Enqueue禁止状態に対するアプリケーションの対処方法は、大別して3つの方法があります。

  1. 最も古いデータを破棄して、強制的にEnqueueする。
  2. Dequeue操作により空きが作られるまで、Enqueueタスクを休眠させる。
  3. Dequeue操作に失敗したことを、読み出し元の関数へreturnする(今回の実装)。

(1)の対処方法は、有効なデータが失われるため極力避けるべきです。ただし、古い情報ほど読み出される可能性が低く、格納された情報の順序性を重視するロギングなどの実装には本方式がフィットします

(2)の対処方法は、開発現場で最も活用される対策方法です。この対策では、操作禁止を検出したタスクが操作可能を検出するまで待ち状態(タスクの休眠:SemaphoreのWait)となり、操作再開のイベントを起こした別のタスクがEnqueue可能通知(SemaphoreのSignal)を発行し、タスクが再開されます。(2)方式のデメリットとしては、セマフォ機能を利用するため、プログラム全体が複雑になりやすいこと、SemaphoreのWait中はタスクが休眠するため、他の処理を先行実行できないこと、などが挙げられます。

(3)は非常に単純な実装であり、失敗を検知した呼び出し元が、再度トライすることにより成功するまで操作を続けることが可能です。また(2)の方式では実現できなかった、空き時間を使った処理の先行実行が可能です。(3)方式のデメリットとしては、むやみに連続して失敗する可能性のある操作を続けると、リングバッファがロックされ続けてしまい、他のタスクがリングを使用できず、失敗要因(Full/Empty)を解消しにくくなるといった課題があります。そのため、(3)の対策を実装する際には、操作に失敗したタスクはミューテックスロックを手放してから、わずかな時間でもSleep関数やWait関数を挟み「他のタスクがミューテックスロックを確保できるよう配慮する」設計が必要となります。

サンプルプログラムの利用方法

今回の初心者講座に対応したソースコードはGitHubにて公開しています。GitHubは、オープンソースソフトウェアの公開に最適なプラットフォームです。バージョン管理機能も提供しているため、今後弊社がソースコードを変更した場合でも、今回の初心者講座に対応したソースコードをいつでも取得、お試しいただけます。

GitHub:今回の説明に対応したリリース(v1.4.0.is.2.0)

開発環境の構築方法と、GitHubにて公開しているソースコードの利用方法は下記のQiita記事をご参照ください。Qiita記事中の【赤字】範囲は、『ソースコードを今回の内容に対応した内容へ切り替える方法』に読み替えて操作してください。

図
図8:Qiitaにて公開している環境構築手順

ソースコードを今回の内容に対応した内容へ切り替える方法

		# SPRESENSEのgitのリリースリストが表示されます
		$ git tag -l
		v1.0.0
		v1.0.1
		...
		v1.4.0.is.2.0
		# ソースコードから"1.4.0.is.2.0"という名前のブランチを生成します
		$ git checkout -b 1.4.0.is.2.0 refs/tags/v1.4.0.is.2.0
		Switched to a new branch '1.4.0.is.2.0'
		# このように切り替わっています
		$ git branch
		* 1.4.0.is.2.0
		  master
		# 1.4.0.is.2.0の初期状態にリセットします
		$ git reset --hard HEAD

サンプルプログラムの実行結果

Visual Studio Code上にて「カーネルのビルド」「アプリケーションのビルド」「ビルドと転送」を実行するとSPRESENSE™上にプログラムが転送され、RTOS「NuttX」の提供するCUI「NuttShell」がVisual Studio Code内のターミナルに開かれます(図9の③、図9の①はメインコア用のプログラム、図9の②はサブコア用のプログラムです)。

図
図9:「aps_multicore」と入力しEnterキーを押します

nsh上でコマンド『aps_multicore』と入力し、Enterを押すと、リングバッファのテストが開始されます。処理内容は以下の通りです。Dequeueに失敗するケース(retが-1となる:リングバッファが空の状態のときDequeueした場合)もテストパターンに含まれています(図10)。

図
図10:ターミナルのログからEnqueue/Dequeue操作の結果を確認できます

図
図11:今回のプログラムはSPRESENSE™単体でも実行可能です

図

さらなる活用方法

今回のプログラムでは、リングバッファそれぞれに1KBの領域を確保、Enqueueの際には短い文字列を格納、パラメータには固定数値を代入しました。リングバッファは、サイズや構成を変えることによりデバッグだけでなく様々な用途に活用できます。

例えば、①リングバッファのパラメータ領域に時刻情報を入れることにより、サブコア内部の負荷の高い処理を特定することができます。また、②リングバッファにサブコアが参照しているデータの断片をコピーすることにより、メインコアが期待するデータを解析できているかを知ることができます。もちろん、③解析対象のデータや解析結果のデータをコア間で交換することもできます(1KB x48組でなく、4KB x12組や、メモリタイルを全面活用し32KBx7組といった構成も可能です)。

まとめ

今回の初心者講座では、マルチコア・プログラミングに必ず登場する「リングバッファ」について解説し、実際にCPUコア間でデータを送受信するプログラムを紹介しました。今回は「デバッグ」というキーワードで説明を始めましたが、コア間でデータを交換する仕組みは様々なアプリケーションに不可欠です。是非、実際のアプリケーションに活用してみましょう。

次回は実際のデータ「音」を扱うプログラムの説明を通して、SPRESENSE™の実践的な開発を学びます。ご期待ください。