フォトフレーム:SPIのDMA転送にこだわる
雑誌付録のFPGA Lattice XP2基板と、これも雑誌付録のARMプロセッサーSTM32基板(CQ-STARM、但しCPUをSTM32F103VE6に換装)を組み合わせたデジタルフォトフレームプロジェクトは、JPEGファイルの画像描画に成功して、いよいよ最終ゴールである液晶モニターにFPGA基板やCPU基板を実装する段階に入った(第9ステップ、2010/3/11記事参照)。
しかし、JPEGライブラリでデコードされたビットマップデータをSPIでFPGAに送る際のDMA(Direct Memory Access)がどうにも言うことを聞かない。CPUでSPIを動かすのと余り性能的に変らないようなのでこだわることはないのだが意地になっている。
STM32のDMA転送がうまくいかない(1/12/2011)
フォトフレームの現状は、JPEGの画像は何とか出たが、画は逆さまだし、DMAを使ってSPIを動かすと同期のずれた縞模様しか出力しない。しかも、おかしなことにDMAを使ったほうが何故か時間がかかる(2.2秒以上)。
DMAを使わずにSPIをCPUを使って素直に書き出しても、BMPファイル(2.2秒)より早く(1.7秒)、画像が出せたので、もうこのままでも良いのだが、プロセッサーが本来持っている折角の機能を使わずにおくのは、ソフトウエア開発の元プロとしては寝覚めが悪い。しかも一旦やりだしたことを途中で投げ出すのは、負けず嫌い、へそ曲がりの所長としては断じて避けたいところである。
BMPファイルよりJPEGファイルの方が早いのは、ファイルサイズが1/4以下になっているのでデコードのCPU処理が増えても全体としては早くなったからである。これにSPIをDMA転送にすれば、SPIを動かしながら、CPUはJPEGデコードに専念することができるので、今よりさらに早く転送が出来るはずだ。これを活用しない手はない。なんとしてもDMAを動かしておきたい。
しかし、これが言うことを聞かないのである。JPEGがデコードしたビットマップのデータをDMAを使わずそのままSPIで出す画像は、きれいに出力されるのに、一行分のバッファーにそれを移してDMAを通してSPIで送ったものは同期がずれたような縞模様の画像で、全く画にならない。DMAがおかしいことは明白である。
マニュアルをみるとバッファーサイズはバイト数ではなくデータカウント数で、16ビットフレームだから半分にしないといけないはずだが、そうすると画面にデータが送れていないところが残る。元へ戻すと全部描かれるが、どちらも化け化けの映像である。
それにしても謎である。サイズを半分にしたら画面の半分のところで止まる筈なのだが、4/5のあたりで止まっている。例によって、16ビット画像の位置を逆にしたら(リトルエンディアン)少し原画らしいものが見えてきたが4つぐらいが重複した縞だらけの画像になる。良くわからない。
DMAはCPUを経ない転送なので外から様子を調べることが難しい。DMAアクセスと実際のバッファーへの移動がかぶらないように、DMA転送の終了フラグを見るようにしているが、タイマーで測っても数msしか待っておらず、どうもおかしい。1ラインごとにDMAを動かしているので計算上では480ラインでは数百ms待たないとおかしい。
それに、サンプルのソースコードはDMA転送の度に、DMAのDeinit(全ての設定値のクリア)から延々と初期化をしている。これがDMAが遅い原因なのか(CPU内の処理なのでそんなわけはないのだが)と、初期化と開始の関数を、わけて動かしてみるが、ENABLEコマンドだけではDMAは全く動かない。やっぱりDeinitから始まるプロセスでないとDMAは動かない。
やっとのことでDMAが動いた(1/15/2011)
3日間悩んでいた。やはりもうすこし勉強しないと駄目なようだ。プロセッサーのリファレンスマニュアル(RM0008)をダウンロードしてDMAのところを本格的に調べ始めた。
すると、マニュアルの10.3.3 DMA channels(ページ201)の最後の方で、「DMA転送を終えたら、次の転送のためにはチャネルを必ずdisableにしておくこと」(In order to reload a new number of data items to be transferred into the DMA_CNDTRx register, the DMA channel must be disabled.)というのを発見した。いやあ、こんなところに大事なことが隠れていた。
そうか。これが転送の度に初期化ルーチンを動かしている理由なのか。DMA開始関数に、ENABLEの前に、DMA_cmd(DMA1,DISABLE)を入れてみると、やっとDMAが動き出した。しかし画像が改善されるわけではない。
画像の化ける原因がつかめない。ロジックアナライザーでデータをひとつひとつ調べるしかないのか。考えられるのは1行の転送が終わらないうちに次の転送が始まってデータをこわしている可能性である。転送終了フラグは初期化のときにクリアされているはずで、次のDMA転送に行くのにはこのフラグが立つのを待っているが、どうもこのあたりが動いていないような気がしてきた。転送終了を待つ時間が少ないというのも気になる(ただし0ではない)。
DMA開始関数のところに、転送完了フラグをクリアする関数、DMA_ClearFlag(DMA1_FLAG_TC5)を入れてテストしてみる。うーむ、前より少し画像らしくなったが、まだ化けている。画像は縦に拡大され、一部しか出ていない。
あ、あ、あ、わかったぞ。データレングスだ。やっぱりデータサイズはバイト数ではなく、データ数だ。今までは、1行当たりに2倍のデータを送っていたのだ(画像が縦に拡大される)。
DMAに時間がかかっていたのも2倍のデータ転送をしていたからだ。転送終了を待っている時間が0でなかったのは、最初の1行の時だけの待ち時間だ。すべてのこれまでの現象がこれを裏づける。間違いない。あせる手で、ソースを修正する。ビルドする。終わるのが待ち遠しい。終わった。画像を出してみる。
やった、やった。やりました。DMAを通してやっと綺麗なJPEG画像が出た。DMAのステイタスビットは明示的にクリアしないと、初期化では元に戻らないのだ。2つバグが重なっていたためにわけがわからなくなっていた。いやあDMAを動かすのにこんなに手順が沢山必要とは思ってもいなかった。
夕食に、珍しくビールをだして家族と乾杯する。家族も良く分からないまま祝福してくれた。酒を飲むと、そのあと何もしたくなくなるので最近は殆ど飲まないが、今日は嬉しいのであとはどうでもよい。
STM32でSPIをDMAで動かす方法(1/16/2011)
ソースコードは全体が出来てから公開しようと考えていたが、とりあえず、SPIでDMAを動かそうとしておられる方の参考のために、ノウハウをまとめておくことにする。
これからご紹介するソースコードのオリジナルはここで、すべてSTマイクロの標準ペリフェラルライブラリを使うことを前提としている。レジスター単位に設定する方法もあるが、今後の運用性や、移植性を考えると面倒でも、こちらの方が便利だろう。
・まず、関数は2つ用意し、初期設定のルーチンを、DMA_SPI2_Init()、DMAを動かす関数を、DMA_SPI2_Transfer()とする。 (名前は任意。DMA化するSPIはSPI2)
・両方の関数が使用するDMAの初期設定構造体をグローバル変数
volatile static DMA_InitTypeDef DMA_InitStructure;
で定義しておく。
・初期化ルーチンでは、以下のように初期設定をしていく。
void DMA_SPI2_Init(BYTE *data, uint32_t size)
{
//--------------------------------------------------------------
// DMA1のクロック有効化(ARMは省電力化のため最初はクロックを止めている)
// DMA1: AHBに属する。
//--------------------------------------------------------------
RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE);
//--------------------------------------------------------------
// SPI2 TXイベントからのDMA要求はチャネル5(始めから決まっている)
//--------------------------------------------------------------
DMA_DeInit( DMA1_Channel5); // 念のため初期設定値をクリアする
// SPI2のデータレジスタをペリフェラル側にする
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(SPI2->DR);
// メッセージをメモリ側にする
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) data;
// ペリフェラルを転送先にする
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
// 転送データ(RGB565の1行分)を設定。16ビットのデータ数であること
DMA_InitStructure.DMA_BufferSize = size ;
// ペリフェラル側インクリメントは不要
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
// メモリ側インクリメント使用
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
// ペリフェラル側データサイズ16ビット
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
// メモリ側データサイズ16ビット
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
// サーキュラモード(転送が終わると、最初のアドレスへ戻る)不使用
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
// 優先度: 最高
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
// メモリ間転送は不使用
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
// 以上の設定値で、DMAチャネルを初期化する。
DMA_Init( DMA1_Channel5, &DMA_InitStructure);
// SPI2にDMAで動かすことを知らせる。なお初期化ではDMAをENABLEにしない
SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE);
} // DMAの初期化の終わり。DMAを使う前に実行する。
・実際にDMAを動かす関数は、以下のようにし、処理ループの必要なところでこれを呼ぶ。
void DMA_SPI2_Transfer(void)
{DMA_Cmd( DMA1_Channel5, DISABLE); // 一旦、DISABLEにする
DMA_Init( DMA1_Channel5, &DMA_InitStructure); // 初期化(必要)
DMA_ClearFlag(DMA1_FLAG_TC5 | DMA1_FLAG_HT5); //2つともクリア
DMA_Cmd( DMA1_Channel5, ENABLE); // ここでDMA転送が始まる
}
・DMAは必ず並行処理になるので、DMAが終わったかどうか調べる必要がある。処理ループ内で次のステートメントでDMAの処理の状態をチェックする。これはフラグだが、これ以外にも割り込みで処理する方法もある。
while(DMA_GetFlagStatus(DMA1_FLAG_HT5)== RESET); //HT5..半分TC5.. 完了
・以上で、SPIはDMAを通してデータが送信されるようになる(受信は、全く別のチャネルで新たに設定)。ARMのDMAは、メモリ、ペリフェラル間での多彩な機能を持っているだけに、これを使いこなすのは逆に大変である。なお、SPI側の設定は全く変える必要はない。
・ペリフェラルの種類と転送先で使うDMAチャネルは一意に決まっている(マニュアルに詳しく出ている)。ペリフェラルによっては、DMAを使うということを知らせる必要があり、SPIでは上記のコマンドが必要であるが、必要でないペリフェラルもある。
・また、SPIの場合は、ENABLEにした途端にスタートするが、次のスタートには必ずDISABLEにして再度、ENABLEにしないと始まらない(ここがはまる)。DMAのスタートは、これ以外に色々な方法がある(タイマー、割り込み)。ソフトで開始を始められるペリフェラルもある(ADCなど)。
・さらにDMAステイタスビットは、初期化などではリセットされない。必ずユーザーが明示的にクリアしないとおかしくなる(ここもはまりやすい)。
・データレングスは、バイト数ではない。転送単位のデータ数であることに注意。
気になる処理時間だが、残念ながらDMAによって飛躍的には早くならなかった。DMAによるSPIは、ほぼスペックどおりの速さ(9Mbps)で、CPUを介して動かすときと大差ない。実測すると、JPEGのデコードは1行が2ms程度で、JPEGのデコードが予想以上に早く、全体では200ms程度しか改善されなかった(1.5~1.6秒)。まあ、動かすことが目的だったから、満足しよう。
液晶モニターの電源をDC-DCコンバーターにする(1/18/2011)
最終のゴール、第9ステップは、FPGA基板やCPU基板を液晶モニターケースに固定して、フォトフレームとして完成させることである。基板は、ケースに入らないので、外側に固定する。あまり見栄えはよくないが、自作フォトフレームなのだから許してもらおう。
それより電源が問題である。このあいだ液晶用の12V電源から降圧した5Vをケースの外に出してFPGAを動かしてみたが、5分もたたないうちに電源が不安定になりFPGAが誤動作した。
FPGA基板の電流消費は、150mA以上あり、12Vから5Vに落とす際の消費熱量がレギュレーターの限界を超えているようだ。熱抵抗を計算してみるまでもなさそうである。ヒートシンクをつければ良いのだろうが、それより12Vからただ発熱させて5Vにするのは実に無駄である。といって、たかがフォトフレームくらいでDCアダプターを2つも使うのも非現実的だ。
そこで、思いついたのが、ストロベリーリナックスで面白がって買ったDC-DCコンバーターである。5V入力で、9~20Vを出せる基板(¥945)である。9Vバッテリー(006P)をリチウム充電池で作ろうと買ってあった。少し高かったが、こいつ小さくても出力電流は500mA以上とれる。液晶モニターのインバータ電源(12Vで実測300mA)にしても問題ないはずだ。
早速、基板にピンを立ててブレッドボードでテストした。最初、ブレッドボード付属の5Vアダプターが昔の0.36Aしかとれないやつで、負荷をかけると電圧が上がらず、点灯しなかったが、2Aのアダプターでテストすると問題なく動作した。よし、これで電源周りは効率良く(DC-DCコンバーターの効率は90%近いので発熱もない)なった。
今のところ、ディスプレイのインバーターが5V換算で0.7A、ロジックが0.13A、FPGAが0.15A、STM32が0.04Aなので、合計で1A少々。 2A程度のアダプターなら全部をまかなっても余裕だ。
液晶モニターのコネクター基板の工作を始めた。ついでにBMP画像を出すことをあきらめて、JPEGのため画像のスキャンを下から上に戻す修正もする。DC-DCコンバーターがコネクター基板に載らないので、汎用基板の小片を追加してハンダ付けする。
レギュレーターをはずして配線を変更する。前より簡単になった。通電。うまく画像が出た。この液晶モニターは電源電圧が9Vでも動く。コンバーターの容量が心配なので、電源電圧を10Vに下げて設定し、1時間ほど連続して動かしてみる。問題なさそうだ。
コンバーターのICとコイルは少し熱いが、ずっと持っていても大丈夫なくらいの発熱で、大したことはない。FPGAに電源を供給しても全く問題なし。ただし、アダプター本体が少し熱を持つ。アダプターはこのあいだトラブルを起こした例の秋月の電源アダプターだが、何といっても秋月のは品揃えも多く小型で安価なので他を使う気がしない。2.3Aを買ってきたが、もう少し大きい3Aくらいが必要なようだ。
| 固定リンク
「電子工作」カテゴリの記事
- 生存証明2(2022.10.19)
- 生存証明(2022.01.23)
- パソコン連動テーブルタップの修理を諦めて自作(2021.02.16)
- 半年ぶりのブログ更新に漕ぎつけた(2019.09.19)
- 研究所活動は停滞したままでCCDカメラ顕微鏡導入など(2019.02.08)
「ARM」カテゴリの記事
- 心電計プロジェクト:スケールが出ると心電計らしくなる(2015.01.08)
- 心電計プロジェクト:TFT液晶に念願の心電波形が出た(2014.12.18)
- 心電計プロジェクト:STM32F103の心電波形表示で悪戦苦闘(2014.12.03)
- 心電計プロジェクト:CooCoxでARMの表示系ソフトを開発する(2014.10.16)
- 心電計プロジェクト:表示部のARM基板の開発環境を一新する(2014.09.19)
コメント