SSブログ
English Version

CH32V203で遊ぶ(その6)クロックアップ [CH32V]

 前回の記事で CH32V203 に TFT 液晶 MSP2807(320 x 240 dot)を接続した環境でのライン描画の高速化から端を発して新たな高速ライン描画法を提案しました。
 今回はレジスタ直書込みによる高速化と CH32V203 のクロックアップによる変貌ぶりについて記録して置きたいと思います。


1.はじめに
 以前から認識はしていたのですが、CH32V203 は今回使用している開発環境(MounRiver Studio)のディフォルト設定では外部クロックを使用する設定になっていて、今回のように外部クロックを接続していない状態では内部クロックを使って 8MHz で動作します。
 言わばこの大リングボール養成ギブスを付けた状態で今までグラフィック描画の高速化を試みてきました。今回はレジスタの直たたきによる高速化を行った後に、クロックアップにより羊の皮を脱ぎ捨てた CH32C203 の真の実力について記載します。


2.レジスタ直接制御による高速化
 サンプルプログラムの記述に従ってC言語で開発するとペリフェラルのステータス確認の度にサブルーチンコールし、サブルーチンの中では対応ビットをANDチェック後の状態(ゼロか否か)により、リターン値を設定するようなまどろっこしい処理になってしまいます。特にライン描画のような繰り返し回数が多い処理ではなるべくステップ数の少ない実装にしたいものです。
 そこでステータスの確認とデータの書き込みをレジスタ直アクセスにしてみました。レジスタのアドレスやステータスのビットの定義についてはディバッガでトレースすれば容易に確認でき、これらは ch32v20x.h や ch32v20x_spi.h のファイル内で定義されています。
 参考として SPI1 関連のレジスタとステータスレジスタのビット構成を下表に示します。

SPI1 関連レジスタ

SPI1 ステータス


 下図がレジスタ直制御対応前と対応後のロジアナ波形です。SPI のデータ出力の間隔が短くなり、高速化できていることが判ります。末尾の「4.まとめ」に記載しているようにレジスタを直接制御することで前回の記事に掲載した 512 個の線分を描画するテストプログラムの実行時間は従来の処理から 16% 程速くなりました。

レジスタ直制御対応前のSPI波形

レジスタ直制御対応後のSPI波形



3.クロックアップ
 下図は CH32V203 のクロック系の構成図で、これを見ると HSI(内部クロック)の 8MHz を PLL で18倍まで逓倍したものを SYSCLK にすることができます。

CH32V203 のクロック構成


 今まではディフォルト設定で SYSCLK は 8MHz で、言わばこの大リーグボール養成ギブスを付けた状態で色々高速化を試みてきました。高速化の対処も一通りやった感があるので、この辺で CH32V203 の羊の皮を取り去り本来の実力を見てみたいと思います。
 クロックの設定は system_ch32v20x.c の下記の部分の define 設定を変更することで簡単にできました。

system_ch32v20x.c
/* 
* Uncomment the line corresponding to the desired System clock (SYSCLK) frequency (after 
* reset the HSI is used as SYSCLK source).
* If none of the define below is enabled, the HSI is used as System clock source. 
*/
//#define SYSCLK_FREQ_HSE    HSE_VALUE
//#define SYSCLK_FREQ_48MHz_HSE  48000000
//#define SYSCLK_FREQ_56MHz_HSE  56000000
//#define SYSCLK_FREQ_72MHz_HSE  72000000
/////#define SYSCLK_FREQ_96MHz_HSE  96000000
//#define SYSCLK_FREQ_120MHz_HSE  120000000
//#define SYSCLK_FREQ_144MHz_HSE  144000000
//#define SYSCLK_FREQ_HSI    HSI_VALUE
//#define SYSCLK_FREQ_48MHz_HSI  48000000
//#define SYSCLK_FREQ_56MHz_HSI  56000000
//#define SYSCLK_FREQ_72MHz_HSI  72000000
//#define SYSCLK_FREQ_96MHz_HSI  96000000
//#define SYSCLK_FREQ_120MHz_HSI  120000000
#define SYSCLK_FREQ_144MHz_HSI  144000000


 sysclk が従来の 8MHz から一挙に 144MHz にアップし、SPI のプリスケーラーは2に設定しているので SPI のクロックも 4MHz から 72MHz に変わることになります。流石に SPI のプリスケーラーの設定は変更が必要と思っていましたが、なんとそのままで動作しました。
 こうなると私の安いロジアナ(サンプリングの上限が100MSa/s)では波形を確認できません。最高サンプリング 1GSa/s のオシロで観測した SPI の CLK(水色)とデータ(黄色)が下図です。プローブが安物のせいか波形が綺麗には見えませんが、カーソルで示しているように確かに 72MHz 程度のクロックが出ているようです。ブレッドボード環境でこの周波数で動作していることもすごいですね。

SPI CLK(水色) と MOSI(黄色)



4.まとめ
 数回に渡り、高速化検討をしてきましたが、高速化を一通りやった感があるので今回はクロックを 8MHZ から一挙に上限の 144MHz にアップし、CH32V203 の羊の皮を脱がせて真の実力を見てみたところグラフィック描画はとんでもない速さになりました^^
 120円の CPU でこの速さはとてつもないと思います。接続した TFT 液晶 MSP2807 もすごいですね。

 最後に今回の高速化対応での線分描画時間の測定結果を下表に示します。

Nomethodtime[s]speed ratio
1Before13.1091.00
2Register direct11.3041.16
3Clockup(144MHz)0.62620.94


 X(旧Twitter)に投稿したメッセージに添付した動画を貼っておきます。




[TOP] [ 前へ ] 連載記事一覧 [ 次へ ]
nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

CH32V203で遊ぶ(その5)高速なライン描画法 [CH32V]

 前回の記事では DMA での高速転送機能によって TFT液晶 MSP2807(320 x 240 dot)での塗りつぶし処理を高速化したことについて書きました。この時のデモ表示を見ているとラインの描画が目で見えるほど遅いので何とか高速化できないものか考えてみました。
 ディバッガで追ってみると速度のネックはライン描画アルゴリズムよりもドット描画自体にあり、SPI や PIO での I/O 制御部がC言語での標準な制御方法ではまどろっこしい程の処理なのでレジスタ直書きやアセンブラ化すればかなり速くなりそうです。
 今回はライン描画処理の高速化を検討する中で、上記のような I/O 制御自体の高速化では無く、ライン描画アルゴリズム自体の高速化を思い付いた(以降、傾き予測法(Slope prediction method)と記す)ので記録しておきたいと思います。
 尚、後で気が付きましたが「ポケコン(G850)高速ライン描画」の記事でも引用していた「ラインルーチンの高速化の手法」のページにライン描画の高速化について判り易くまとめられていて、ここにダブルステップの手法が書かれていますが評価結果として高速化はできなかったとの結論でした。今回提案する傾き予測法は本ページの末尾にまとめたように実際に高速化への効果が確かめられた描画法です。


1.今回のお品書き
 ライン描画手法について下記の内容を記載します。
  1. 標準的なライン描画処理
  2. 両端からの同時描画
  3. 今回提案の傾き予測法
最後にまとめとして上記手法の評価結果を記載します。


2.高速ライン描画手法
 以降にライン描画の各手法について記載します。


 2-1.標準的なライン描画処理
 高速化のために整数だけの処理でラインを描画する手法でブレゼンハムのアルゴリズムとして広く知られています。
 下記のソースはネット上のソースを参照せずに独自に起こしたものですが、ブレゼンハムのアルゴリズムと同等の処理になっています。
 高速化の観点からループ内での条件分岐を最小限にするために傾きの大きさにより場合分けし、二つのループ処理に分割しています。PSET は1ピクセルを描画する外部処理です。

標準的なライン描画処理(C言語)
void LineStd( int16_t xst, int16_t yst, int16_t xen, int16_t yen ) { int16_t x,y,difx,dify; int16_t err, xtail, ytail; LcdXPos = xen; LcdYPos = yen; difx = abs( xen - xst ) << 1; dify = abs( yen - yst ) << 1; if( ( (difx >= dify) && (xen > xst) ) || ( (difx < dify) && (yen > yst) ) ) { x = xst; y = yst; xtail = xen; ytail = yen; } else { x = xen; y = yen; xtail = xst; ytail = yst; } if( difx >= dify ) { int16_t dy; dy = ytail > y ? 1 : -1; err = difx >> 1; for( ; x <= xtail; x++ ) { PSET( x, y ); err += dify; if( err >= difx ) { err -= difx; y += dy; } } } else { int16_t dx; dx = xtail > x ? 1 : -1; err = dify >> 1; for( ; y <= ytail; y++ ) { PSET( x, y ); err += difx; if( err >= dify ) { err -= dify; x += dx; } } } }
・2024/08/16 中心部のポイントが乱れる場合がある問題に対処



 2-2.両端からの同時描画
 上記の改良版でラインが中心点から見て点対称なので始点側と同時に終点側も描画することでループ回数を1/2にして高速化しています。ループ終了後の PSET は始点~終点間の距離が偶数の場合、中心のポイントの描画が欠落してしまうことに対する対処です。

両端からの同時描画(C言語)
void LineBoth( int16_t xst, int16_t yst, int16_t xen, int16_t yen ) { int16_t x,y,difx,dify; int16_t err, xtail, ytail; LcdXPos = xen; LcdYPos = yen; difx = abs( xen - xst ) << 1; dify = abs( yen - yst ) << 1; if( ( (difx >= dify) && (xen > xst) ) || ( (difx < dify) && (yen > yst) ) ) { x = xst; y = yst; xtail = xen; ytail = yen; } else { x = xen; y = yen; xtail = xst; ytail = yst; } if( difx >= dify ) { int16_t dy; dy = ytail > y ? 1 : -1; err = difx >> 1; for( ; x < xtail; x++, xtail-- ) { PSET( x, y ); PSET( xtail, ytail ); err += dify; if( err >= difx ) { err -= difx; y += dy; ytail -= dy; } } if( x == xtail ) { PSET( x, y ); } } else { int16_t dx; dx = xtail > x ? 1 : -1; err = dify >> 1; for( ; y < ytail; y++, ytail-- ) { PSET( x, y ); PSET( xtail, ytail ); err += difx; if( err >= dify ) { err -= dify; x += dx; xtail -= dx; } } if( y == ytail ) { PSET( x, y ); } } }
・2024/08/16 中心部のポイントが乱れる場合がある問題に対処



 2-3.今回提案の傾き予測法
 上記の描画では傾きが1(45°)以下の場合と1より大きい場合の二通りに場合分けして処理しましたが、傾き予測法では傾きの大きさによりループ処理を更に2分割します。
 下図に示すように傾きの大きさと移動方向の条件により、次の移動方向を決定できるケースがあり、これを利用して高速化しています。前述の両端からの描画手法も併用します。

  • 傾きが 0 ~ 0.5 の場合
     縦移動/横移動 は1/2以下なので下図の上側の緑色の■のように縦移動直後は必ず横移動になります。

  • 傾きが 0.5 ~ 1.0 の場合
     縦移動/横移動 は1/2以上なので下図の下側の緑色の■のように横移動直後は必ず縦移動になります。

傾き予測法での予測方法


 例えば、上記の「標準的なライン描画処理」において単純にループ内で2ドット分の進みを処理すれば、ループ回数が 1/2 になるので for ループのための条件ジャンプの比率が1/2になり効果は少ないですが若干高速化します(ループ処理の展開による高速化)。逆に言うと1回のループ処理で多くのドットを処理してもそれに比例して条件分岐が増えれば高速化できません(当然処理量が比例して多くなっても高速化できません)。従って高速化するためには上図のようにループ内の条件ジャンプの増加を抑えつつ、より多くのドットを処理するようにする必要があります。

傾き予測法によるライン描画処理(C言語)
// draw line Ver 0.02b void LcdLine( int16_t xst, int16_t yst, int16_t xen, int16_t yen ) { int16_t x,y,difx,dify,until; int16_t err, xtail, ytail; LcdXPos = xen; LcdYPos = yen; difx = abs( xen - xst ) << 1; dify = abs( yen - yst ) << 1; if( ( (difx >= dify) && (xen > xst) ) || ( (difx < dify) && (yen > yst) ) ) { x = xst; y = yst; xtail = xen; ytail = yen; } else { x = xen; y = yen; xtail = xst; ytail = yst; } if( difx >= dify ) { int16_t dy; dy = ytail > y ? 1 : -1; err = difx >> 1; until = x + (difx>>1) + ( (difx&1) ? 0 : -1); if( difx >= (dify<<1) ) { // slope:[0,0.5] for( ; x < until; x++, xtail-- ) { PSET( x, y ); PSET( xtail, ytail ); err += dify; if( err >= difx ) { err += dify - difx; x++; xtail--; y += dy; ytail -= dy; PSET( x, y ); PSET( xtail, ytail ); } } } else { // slope:(0.5,1] for( ; x < until; x++, xtail-- ) { PSET( x, y ); PSET( xtail, ytail ); err += dify; if( err >= difx ) { err -= difx; y += dy; ytail -= dy; } else { x++; xtail--; PSET( x, y ); PSET( xtail, ytail ); err += dify - difx; y += dy; ytail -= dy; } } } for( ; x <= xtail; x++ ) { PSET( x, y ); err += dify; if( err >= difx ) { err -= difx; y += dy; } } } else { int16_t dx; dx = xtail > x ? 1 : -1; err = dify >> 1; until = y + (dify>>1) + ( (dify&1) ? 0 : -1); if( dify >= (difx<<1) ) { for( ; y < until; y++, ytail-- ) { PSET( x, y ); PSET( xtail, ytail ); err += difx; if( err >= dify ) { err += difx - dify; x += dx; xtail -= dx; y++; ytail--; PSET( x, y ); PSET( xtail, ytail ); } } } else { for( ; y < until; y++, ytail-- ) { PSET( x, y ); PSET( xtail, ytail ); err += difx; if( err >= dify ) { err -= dify; x += dx; xtail -= dx; } else { y++; ytail--; PSET( x, y ); PSET( xtail, ytail ); err += difx - dify; x += dx; xtail -= dx; } } } for( ; y <= ytail; y++ ) { PSET( x, y ); err += difx; if( err >= dify ) { err -= dify; x += dx; } } } }
・Ver 0.02 2024/08/03 中心部のポイントが描画されない場合がある問題に対処し、下記の実行時間も再測定
・Ver 0.02a 2024/08/16 中心部のポイントが乱れる場合がある問題に対処
・Ver 0.02b 2024/09/04 傾きが0.5以上で線が太くなる問題に対処


★追記 2024/08/06 {
 上記は速度優先で同様な処理が2カ所にありましたが、速度低下を最小限にしてコードを短くしたバージョンを追記します。

傾き予測法によるライン描画処理[shorter code ver](C言語)
// draw line Ver 0.03b void LcdLine( int16_t xst, int16_t yst, int16_t xen, int16_t yen ) { int16_t x,y,difx,dify,until; int16_t err, xtail, ytail,dy; int16_t *px,*py,*pxtail, *pytail; LcdXPos = xen; LcdYPos = yen; difx = abs( xen - xst ) << 1; dify = abs( yen - yst ) << 1; if( ( (difx >= dify) && (xen > xst) ) || ( (difx < dify) && (yen > yst) ) ) { x = xst; y = yst; xtail = xen; ytail = yen; } else { x = xen; y = yen; xtail = xst; ytail = yst; } if( difx >= dify ) { px = &x; py = &y; pxtail = &xtail; pytail = &ytail; } else { int16_t tmp; px = &y; py = &x; pxtail = &ytail; pytail = &xtail; tmp = difx; difx = dify; dify = tmp; } dy = *pytail > *py ? 1 : -1; err = difx >> 1; until = *px + (difx>>1) + ( (difx&1) ? 0 : -1); if( difx >= (dify<<1) ) { // slope:[0,0.5] for( ; *px < until; (*px)++, (*pxtail)-- ) { PSET( x, y ); PSET( xtail, ytail ); err += dify; if( err >= difx ) { err += dify - difx; (*px)++; (*pxtail)--; *py += dy; *pytail -= dy; PSET( x, y ); PSET( xtail, ytail ); } } } else { // slope:(0.5,1] for( ; *px < until; (*px)++, (*pxtail)-- ) { PSET( x, y ); PSET( xtail, ytail ); err += dify; if( err >= difx ) { err -= difx; *py += dy; *pytail -= dy; } else { (*px)++; (*pxtail)--; PSET( x, y ); PSET( xtail, ytail ); err += dify - difx; *py += dy; *pytail -= dy; } } } for( ; *px <= *pxtail; (*px)++ ) { PSET( x, y ); err += dify; if( err >= difx ) { err -= difx; *py += dy; } } }
・Ver 0.03a 2024/08/16 中心部のポイントが乱れる場合がある問題に対処
・Ver 0.03b 2024/09/04 傾きが0.5以上で線が太くなる問題に対処
}


3.各ライン描画法の評価
 上述のライン描画法の評価環境として、前回の記事でも使用した CH32V203(8MHz動作)に TFT液晶 MSP2807(320 x 240 dot)を接続した環境を使いました。評価方法としては事前に乱数で生成した 512 本のラインデータに従ってライン描画処理を実行した際の時間を計測しました。時間計測は systick タイマの内部カウンタだけでも計測できそうでしたが、今回は systick を使った 1ms 毎の割込み処理でインクリメントするカウンタ値で時間を計測しています。コンパイル時のオプティマイズはディフォルトのままの -Os(サイズ優先)です。

 下図は評価時の LCD 表示です。

ライン描画法評価時の LCD 画面


 計測結果を下表に示します。

No.methodtime[s]
1標準的なライン描画13.185
2両端からの同時描画13.121
3今回提案の傾き予測法13.109


 上記の表では殆ど時間差が無いように見えますが、冒頭で書いたようにドット描画処理(PSET)が重いので PSET を何もしないダミー関数にし、ライン描画アルゴリズムの処理自体の時間を計測し直した結果が下表です。

No.methodtime[s]speed ratio
1標準的なライン描画0.2001.00
2両端からの同時描画0.1241.61
3今回提案の傾き予測法0.1121.79


 期待通り、今回提案の傾き予測法は標準的なライン描画より約 1.8 倍速いという結果になりました。


[TOP] [ 前へ ] 連載記事一覧 [ 次へ ]
nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

CH32V203で遊ぶ(その4)DMAで高速描画 [CH32V]

 「CH32V203で遊ぶ(その1)SPIでLCD」の記事の末尾に追記したように SPI インターフェースを使って 2.8インチTFT液晶 MSP2807(320 x 240 dot)に描画できるようになりましたが、画面クリアに時間が掛かることから DMA を使って高速化したのでメモっておきたいと思います。


1.今回の目標
 2.8インチTFT液晶 MSP2807(320 x 240 dot)への連続したデータ書き込み処理部分を DMA 機能を使って高速化します。


2.方針
 下記の変更/追加を行います。
  1. SPI のクロックアップ
     SPI のクロックは従来 2MHz でしたが上限の 4MHz にアップします

  2. 連続データ書き込み処理の DMA 化
     LCD への連続したデータ書き込みが発生する塗りつぶし処理(画面クリア含む)を DMA 機能で実現します

  3. 塗りつぶし機能追加
     円と四角形の塗りつぶし機能を実装します


3.対処内容
 以下に対処した内容について記載します。DMA を利用すると言っても DMA の設定の追加と LCD へのデータ書き込み部分のコードを若干変更するだけなので本記事の内容がスカスカになってしまうのを防ぐためにデータシートからの引用等を含め、CH32V203 の DMA 機能自体の説明もある程度記載します。


 3-1.SPI のクロックアップ
 SPI の初期化の時のプリスケーラの設定を変更するだけです。簡単ですね。


SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // 16:500kHz



 3-2.連続データ書き込み処理の DMA 化
 今回使用している CPU(CH32V203)のブロック図を下図に示します。DMA 機能は左上の部分にあり、8チャンネル構成で FLASH, SRAM 及びペリフェラルのバスに接続されています。機能としてはメモリ to メモリ、メモリ to ペリフェラル及び ペリフェラル to メモリの高速なデータ転送を実現します。

CPU(CH32V203)のブロック図


 ペリフェラルと言ってもそれを制御するペリフェラル用のレジスタは下図のようにメモリにマッピングされています。

CPU(CH32V203)のメモリマップ


 周辺機器とのインターフェースにはそれぞれ固有の機能があるので DMA 側も各機能に合わせてカスタマイズされている関係上、DMA のチャンネルとペリフェラルは下図のように対応付けされています。

DMA リクエストマッピング


 この対応付けを表にしたものが下表になります。今回は LCD 通信の送信(SPI1_TX)を DMA 化するので DMA の チェンネル3を使用することになります。

DMA チャンネル vs ペリフェラル対応表


 一つの DMA チャンネルで複数のリクエストに対応している関係上、下記の様にリクエスト要求側でリクエストを有効にする設定が必要になります。


SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);



 DMA の制御レジスタのビット構成を下図に示します。あとはサンプルソースを参照して設定することで容易に動作しました。

DMA 制御レジスタ


 唯一悩ましかったのが次の点でした。
 今回は LCD の画面消去等の塗りつぶし(2バイトデータの連続書込み)が DMA 対象であり、この場合のペリフェラルサイズ(PSIZE)とメモリサイズ(MSIZE)の設定で悩みました。SPI 側のデータサイズはバイトに設定していて、書き込みデータはメモリ上に2バイトデータで存在するので
  • PSIZE : DMA_PeripheralDataSize_Byte
  • MSIZE : DMA_MemoryDataSize_HalfWord
と設定したところ、メモリ上のデータの1バイト目だけしか転送されませんでした。最終的には PSIZE を HalfWord に変更し、かつ DMA 使用時は SPI のデータサイズも 16bit に変更することでうまく動作するようになりました。

 参考として DMA 設定部分のソースを以下に貼っておきます。ppadr と memadr の型が uint32_t になっていて、これは void * の方が妥当だと思いますがサンプルのままで弄っていません。

DMA 初期化部分のソース
// init Dma void DMA_Tx_Init(DMA_Channel_TypeDef *DMA_CHx, uint32_t ppadr, uint32_t memadr, uint16_t bufsize) { DMA_InitTypeDef DMA_InitStructure = {0}; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA_CHx); DMA_InitStructure.DMA_PeripheralBaseAddr = ppadr; DMA_InitStructure.DMA_MemoryBaseAddr = memadr; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = bufsize; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable; // nenory not incriment DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; 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_Init(DMA_CHx, &DMA_InitStructure); }


 連続書込み部分のソースは下記になります。DMA での1回の転送データ数は 0xffff 個が上限(0x0000 設定時は転送しない)なので転送数が 0xffff より大きい場合は分割して転送するようにしています。ループ内に if 文を入れれば一つのループにできますが速度優先で二つのループにしています(実質的な速度は殆ど変わらないとは思いますが)。

DMA での連続書込み部分のソース
// write same data // data <- write data // len <- how many data void LcdWrFill( uint16_t data, uint32_t len ) { LcdWrDotCmd(); // send mem write command(on SpiCs) #ifndef USE_DMA // not USE DMA while( len-- ) { LcdWrWord( data ); } LcdCsOff(); #else SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; // 16bit mode msbfirst SPI_Init( LCD_SPI, &SPI_InitStructure ); while( LCD_SPIWRSTS() == RESET ) {} // wait until can write PrevSort = DATA; while( LCD_SPIWRBSY() == SET ) {} // wait until write complete LCD_BSET( LCD_CMD ); while( len > 0xffff ) { DMA_Tx_Init( DMA1_Channel3, (u32)&SPI1->DATAR, (uint32_t)&data, 0xffff ); DMA_Cmd(DMA1_Channel3, ENABLE); len -= 0xffff; while(!DMA_GetFlagStatus(DMA1_FLAG_TC3)) {}; } if( len ) { DMA_Tx_Init( DMA1_Channel3, (u32)&SPI1->DATAR, (uint32_t)&data, (uint16_t)len ); DMA_Cmd(DMA1_Channel3, ENABLE); while(!DMA_GetFlagStatus(DMA1_FLAG_TC3)) {}; } LcdCsOff(); SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8bit mode msbfirst SPI_Init( LCD_SPI, &SPI_InitStructure ); #endif }


 下図は DMA 化前の画面クリア時(0x0000の連続書込み)のロジアナ波形です。SPI 出力間に隙間時間がかなりあります。

DMA 対応前の画面クリア時のロジアナ波形


 下図は DMA 化後のロジアナ波形です。隙間なく SPI 出力されています。

DMA 対応後の画面クリア時のロジアナ波形



 3-3.塗りつぶし機能追加
 上記の DMA 化した連続書込み処理を利用して、円と四角形の塗りつぶし描画処理を追加しました。


4.まとめ
 SPI のクロックが MHz レベルになるとマイコンのソフト処理では流石に転送と転送の間に隙間時間が発生してしまいます。このため多くのマイコンが DMA 機能を有しており、効率よくテータ転送ができるようになっています。
 今回の LCD 表示での塗りつぶし処理も DMA 機能を使用することで隙間時間なく転送できるようになり、処理時間を短縮出来ました。
 更に LCD コントローラの書き込み対象の四角形範囲を指定できる機能により、四角形の塗りつぶしはかなり高速化しました。
 また、今回は使用していませんが、DMA の転送中間時点と転送終了時点で割込みを発生できるので、LCD 書込みと並行して別の処理をすることもソフトウェア次第で可能になります。

 最後に DMA 化後のデモ画面を貼っておきます。

DMA 描画デモ画面例


 X(旧Twitter)のメッセージに添付した動画も貼っておきます。



[TOP] [ 前へ ] 連載記事一覧 [ 次へ ]
nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

CH32V203で遊ぶ(その3)USART割込みでLiDAR接続(その2)高速化 [CH32V]

 前回の記事の末尾に追記したように以前格安で購入した LiDAR は不安定要素が残存するもののセンスしたデータをきれいに描画できるようになりました。
 しかし、秋月電子通商さんで¥120で購入できる CH32C203(clockは144MHz)で処理していて LiDAR からの最大8個の測位データが含まれる通信データの塊(package)を5個中1個の割合でした処理できていませんでした。今回はこの処理を高速化してもっと多くの package を処理できるようにしたいと思います。下の写真は開発環境で CH32V203 を取り付けたブレッドボードにLCD(MSP2807、320 x 240 dot)と LiDAR を接続しています。

開発環境



1.今回の目標
 C言語レベルでの最適化を行い LiDAR から送信されてくる package をなるべく多く処理できるようにします。


2.高速化項目
 具体的な高速化ポイントは下記のとおりです。
  1. 三角関数をテーブルにして、整数演算処理化する
  2. RISC-V は LiDAR からのデータと同様、リトルエンディアンなので LiDAR からの受信データからデータ抽出をせず直接使用する
  3. LiDAR からの package 受信バッファをダブルバッファ化して高速化する


 2-1.三角関数のテーブル化と整数演算処理化
 三角関数テーブルとしてが sin の値を0~90度の区間分持つことにしました。この sin テーブルのデータ形式は LiDAR からのデータ形式及び受信データ表示用のLCDの解像度(320 x 240)を考慮し、下記のようにしました。
  • 角度分解能
     LiDAR からの角度データは dgree の64倍で送られてきます。sin テーブルの角度に関しては dgree の4倍とし、LiDAR のデータ内の角度からの変換はシフト処理のみでできるようにしました。テーブル要素数は 90 x 4 で 360 要素の配列を持つことになります。
  • テーブル要素の値
     メモリを節約し配列要素は1バイトとし、値がゼロの場合は 0x0100 とみなすようにします。配列上は角度0の場合もゼロの値になりますが、角度0の場合は配列を参照せずに結果はゼロと判断するようにしました。

 配列用データの生成は下記の BASIC プロフラムで行いました。
1' create sin table Ver 0.01 2024/07/10 by skyriver 100 DELTA=90*4 110 PRINT "const uint8_t SinTable[] = {"; 120 FOR I=0 to DELTA - 1 130 IF (I MOD 8)=0 THEN PRINT:PRINT CHR$(9); 140 PRINT "0x";RIGHT$( "00" + HEX$(INT(SIN(I/DELTA*3.14159/2)*256+0.5)), 2);","; 150 NEXT 160 PRINT:PRINT "};"

 作成した sin テーブルのソースは下記になります。

sin テーブルのソース
/* * sin table 0-90 degree : 360 values * Ver 0.01 2024/07/20 by skyriver */ #define SINDEGPAR (4) // Angles are digit fixed point degree const uint8_t SinTable[] = { 0x00,0x01,0x02,0x03,0x04,0x06,0x07,0x08 ,0x09,0x0A,0x0B,0x0C,0x0D,0x0F,0x10,0x11 ,0x12,0x13,0x14,0x15,0x16,0x17,0x19,0x1A ,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x23 ,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B ,0x2C,0x2E,0x2F,0x30,0x31,0x32,0x33,0x34 ,0x35,0x36,0x37,0x38,0x3A,0x3B,0x3C,0x3D ,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45 ,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E ,0x4F,0x50,0x51,0x52,0x53,0x54,0x55,0x57 ,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F ,0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67 ,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F ,0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77 ,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x7F ,0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87 ,0x88,0x89,0x8A,0x8A,0x8B,0x8C,0x8D,0x8E ,0x8F,0x90,0x91,0x92,0x93,0x94,0x95,0x96 ,0x96,0x97,0x98,0x99,0x9A,0x9B,0x9C,0x9D ,0x9E,0x9E,0x9F,0xA0,0xA1,0xA2,0xA3,0xA4 ,0xA5,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xAA ,0xAB,0xAC,0xAD,0xAE,0xAF,0xAF,0xB0,0xB1 ,0xB2,0xB3,0xB3,0xB4,0xB5,0xB6,0xB7,0xB7 ,0xB8,0xB9,0xBA,0xBA,0xBB,0xBC,0xBD,0xBD ,0xBE,0xBF,0xC0,0xC0,0xC1,0xC2,0xC3,0xC3 ,0xC4,0xC5,0xC6,0xC6,0xC7,0xC8,0xC8,0xC9 ,0xCA,0xCA,0xCB,0xCC,0xCC,0xCD,0xCE,0xCE ,0xCF,0xD0,0xD0,0xD1,0xD2,0xD2,0xD3,0xD4 ,0xD4,0xD5,0xD5,0xD6,0xD7,0xD7,0xD8,0xD9 ,0xD9,0xDA,0xDA,0xDB,0xDB,0xDC,0xDD,0xDD ,0xDE,0xDE,0xDF,0xDF,0xE0,0xE0,0xE1,0xE2 ,0xE2,0xE3,0xE3,0xE4,0xE4,0xE5,0xE5,0xE6 ,0xE6,0xE7,0xE7,0xE8,0xE8,0xE8,0xE9,0xE9 ,0xEA,0xEA,0xEB,0xEB,0xEC,0xEC,0xED,0xED ,0xED,0xEE,0xEE,0xEF,0xEF,0xEF,0xF0,0xF0 ,0xF1,0xF1,0xF1,0xF2,0xF2,0xF2,0xF3,0xF3 ,0xF3,0xF4,0xF4,0xF4,0xF5,0xF5,0xF5,0xF6 ,0xF6,0xF6,0xF7,0xF7,0xF7,0xF8,0xF8,0xF8 ,0xF8,0xF9,0xF9,0xF9,0xF9,0xFA,0xFA,0xFA ,0xFA,0xFB,0xFB,0xFB,0xFB,0xFC,0xFC,0xFC ,0xFC,0xFC,0xFC,0xFD,0xFD,0xFD,0xFD,0xFD ,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE ,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF ,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00 ,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };


 上記の sin テーブルを持つことで LiDAR からのデータ処理を浮動小数点を使わず、全て整数処理で行うことができるようになり、かなり高速化できました。

 下図は従来からの浮動小数点演算処理時のロジアナ波形の再掲ですが、5個の package 中、1個だけ処理ができています。マーカー0がデータ抽出+1ポイント描画の時間でマーカー1が1ポイントの時間なのでデータ抽出には 1ms 程の時間が掛かっていることになります。この package では8個のポイントを描画しています。

浮動小数点演算での処理(従来方式)


 下図は整数演算処理化した改善版のロジアナ波形です。3 package 中1個の package が処理できるようになったので処理率は従来の20%から33%に改善されました。SpiClk が発生している部分がLCD描画している箇所ですが、従来は8回の描画に隙間が見えましたが、高速化したことにより、隙間が見えなくなっています(マーカー1の部分)。マーカー0の部分は LiDAR のデータから必要なデータを抜き取っている部分ですが、800us 程の時間が掛かっていることが判ります。

整数演算で高速化した処理(改善版)



 2-2.データ抽出処理の高速化
 今回使用している CH32V203 は 32 ビット RISC-V をベースに設計されていて、LiDAR からのデータと同様にリトルエンディアンです。従来の処理では LiDAR からのデータの受信バッファからバイト単位でデータを抜き取り、シフト処理でワードデータ等を抽出していました。高速化のために受信バッファ内のポインタをキャストすることでデータ抽出を行わず直接参照するようにしました。

 高速化後のロジアナ波形が下図になります。マーカー0がデータ抽出処理ですが、前述のロジアナ波形では 800us 程度かかっていたデータ抽出処理が 71us 程度に高速化できました。尚、ロジアナ波形の最後に追加した"receiving"はLiDARからのデータを割込み処理で受信中であることを示すディバッグのための信号です。受信データ処理中は LiDAR からのデータを受信できていないことが判ります。

データ抽出部改善後



 2-3.ダブルバッファ化
 LiDAR からのデータを受信するための受信バッファをダブルバッファー化し、処理中でも LiDAR から出力されるデータを受信できるようにすることで受信待ち時間を最小にして高速化しました。
 下図がダブルバッファー化後のロジアナ波形です。処理が追い付かず稀に未処理の package が発生していますが、package の処理率はほぼ100%に改善できました

ダブルバッファー化後



3.まとめ
 最近弄りだした120円の32ビットマイコンの CH32V203 に4年前に購入し、お蔵入りしていた格安の LiDAR を繋いでデータ抽出し、LCD上にリアルタイムで表示できるようになりました。
 CPUパフォーマンスの関係で初めは LiDAR からのデータの20%程度しか表示処理できませんでしたが、今回高速化の改善を行った結果、LiDAR からのデータのほぼ100%を処理し表示できるようになりました。
 ネット上で見かける LiDAR データの表示は M5Stack(32bit 240MHz デュアルコア)等の例が多く、数百円レベルのマイコンでの例は見かけません。
 今回は秋月電子通商さんで120円で購入できるCH32V203(32bit 144MHz)を使って下の写真のように LiDAR からのデータをLCDに表示できるようになりました。
 「非力なマイコンで工夫をしてパフォーマンスを引き出す」というのはマイコン弄りの醍醐味の一つではないでしょうか?

LiDAR データのLCD表示例


★追記 2024/07/21
 X(旧Twitter)に投稿したメッセージに添付した動画を貼っておきます。




★追記 2024/07/24
 X(旧Twitter)で最近話題になっている安価な小型の LiDAR が着荷したので試してみました。「UnknownLiDARMini_M5StackCore2」を参考にさせて頂き、従来のソフトウェアを若干変更することで120円マイコン(CH32V203)で LCD にデータをリアルタイムで表示できました。


新 LiDAR(表面)


新 LiDAR(裏面)


新 LiDAR の試験環境




[TOP] [ 前へ ] 連載記事一覧[ 次へ ]

nice!(0)  コメント(0) 
共通テーマ:趣味・カルチャー

CH32V203で遊ぶ(その2)USART割込みでLiDAR接続 [CH32V]

 前回の記事では SPI インターフェースを使い LCD を制御しました。今回はシリアル通信と割込み機能を試してみたいと思います。

1.今回の目標
 割込みを使ったシリアル受信機能を実装します。更に以前(2020年8月頃)購入した安価な LiDAR のデータを受信し、LCD上に表示することに挑戦してみたいと思います。


2.割込みを使ったシリアル受信の実装
 サンプルを参照して実装は簡単にできました。ヘッダファイルと割込み処理のコードを貼っておきます。説明する部分はあまりありませんが、受信バッファは2のべき乗サイズにしてリングバッファにしています。また、バッファからの読出し時には割込み処理との競合防止のため、受信割込み禁止にしています。

シリアル受信のヘッダファイル
/******************************************* USART interrupt hander Ver 0.01 2024/07/15 by skyriver *******************************************/ /* * usard receive interrupt routine * USART2:Tx(PA2),Rx(PA3) */ #define RiSIO USART2 #define RiPORT GPIOA #define RiSIOTx GPIO_Pin_2 #define RiSIORx GPIO_Pin_3 #define RXBUFSIZE 128 // receive buffer size #define BUFIDXMASK (RXBUFSIZE - 1) void InitSio( void ); uint8_t SioGetc( void ); extern volatile uint8_t RxCnt;


シリアル受信処理
/******************************************* USART interrupt hander Ver 0.01 2024/07/15 by skyriver *******************************************/ #include "debug.h" #include <stdint.h> #include "serial.h" voratile uint8_t RxBuf[ RXBUFSIZE ] = {0}; volatile uint8_t RxCnt = 0, RxWrIdx = 0; volatile uint8_t RxRdIdx = 0; void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); /* * configlate USARTn * return : none */ void InitSio( void ) { GPIO_InitTypeDef GPIO_InitStructure = {0}; USART_InitTypeDef USART_InitStructure = {0}; NVIC_InitTypeDef NVIC_InitStructure = {0}; RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // uart name RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // port name /* USART TX Rx */ GPIO_InitStructure.GPIO_Pin = RiSIOTx; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(RiPORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = RiSIORx; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(RiPORT, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_Init(RiSIO, &USART_InitStructure); USART_ITConfig(RiSIO, USART_IT_RXNE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; // uart name NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_Cmd(RiSIO, ENABLE); } /* * Rx interrupt handler */ void USART2_IRQHandler( void ) { if( USART_GetITStatus( RiSIO, USART_IT_RXNE ) != RESET ) { if( RxCnt < (RXBUFSIZE - 1) ) { // not buffer full RxCnt++; RxBuf[RxWrIdx++] = USART_ReceiveData(RiSIO); RxWrIdx &= BUFIDXMASK; } else { USART_ReceiveData(RiSIO); } } } /* * get character from Rx buffer * return -> data */ uint8_t SioGetc( void ) { uint8_t data; while( RxCnt == 0 ) {} USART_ITConfig(RiSIO, USART_IT_RXNE, DISABLE); // disable Rx interrupt data = RxBuf[ RxRdIdx++ ]; RxRdIdx &= BUFIDXMASK; RxCnt--; USART_ITConfig(RiSIO, USART_IT_RXNE, ENABLE); // enable Rx interrupt return( data ); }



3.LiDAR との接続
 旧 Twitter のタイムラインで見かけて AliExpress さんから 2020年8月頃に購入したものです。下の写真は裏側にある基板の部分です。左下の元々あったコネクタのピッチが狭いため、左上のコネクタを瞬間接着剤でプリント基板に接着しています。
 尚、中央部にある裏側への配線は元々あったものです。

手持ちの LiDAR 写真(裏面)


 下の写真が表側で417と書いてあります(この LiDAR の型番?)。

手持ちの LiDAR 写真(表面)


 カバーを外した状態が下の写真です。なぜカバーを外したかというとこの個体は調子が悪かった(特に角度情報)ためです。まぁカバーを外したところで修理できる訳ではないのですが・・

手持ちの LiDAR 写真(カバー外し後)


 LiDAR 側の電源は 5V なので、CH32V203 を入れたブレッドボードは LinkE から 3.3V を給電し、LiDAR には安定化電源から 5V を給電しました。安定化電源の電流値は 400mA 程度でした。LCD は前回からの継続で 320 x 240 dot でカラーの MSP2807 を接続しています。

 LiDAR との接続を下表に示します。LiDAR のシリアル出力は 3.3V のレベルに変換されて出力されているので CH32V203 に直接接続可能です。

CH32V203K8Y6LiDARMSP2807WCH-LinkE
1:3.3V 1:VCC
5:3.3V 1:VCC
7:RA1 8:LED
8:USART2_Tx
9:USART2_Rx LiDAR_Tx
10:RA4 5:DC
11:SPI_CK 7:CLK
12:SPI_MISO
13:SPI_MOSI 6:DIN
16:GND GND 2:GND 8:GND
17:3.3V 1:VCC
19:USART1_Tx 1:RX
20:USART1_Rx
21:RA11 4:RESET
22:RA12 3:CS
23:SWDIO 7:SWDIO
24:SWCLK 6:SWCLK
32:GND GND 2:GND 8:GND


★追記 2024/07/20 {
 オシロで確認した LiDAR のシリアル出力信号波形が下図です。

LiDAR のシリアル波形例
}

 ロジアナで確認した LiDAR の出力も貼っておきます。

LiDAR の出力サンプル


 ヘッダの値が 0x55,0xaa,0x03,0x08 であることを頼りにネットで探したところ、Github の camsense-X1 のページにこのタイプの LiDAR が出力する package のフォーマットと処理方法に関する情報があったので参考にさせて頂きました。package 内のデータのフォーマットは次のようになるとのことです。

/* package format (36 bytes)
 *
 * <0x55><0xAA><0x03><0x08>
 * <speedL><speedH>
 * <startAngleL><startAngleH>
 * <distance0L><distance0H><quality0>
 * <distance1L><distance1H><quality1>
 * <distance2L><distance2H><quality2>
 * <distance3L><distance3H><quality3>
 * <distance4L><distance4H><quality4>
 * <distance5L><distance5H><quality5>
 * <distance6L><distance6H><quality6>
 * <distance7L><distance7H><quality7>
 * <endAngleL><endAngleH>
 * <unknown><unknown> could be a CRC
 */


 LiDAR からのシリアルデータは 115200bps で 36 バイト構成の package データ( 8個の測定データが含まれる)が立て続けに来る(1回転で50 package 程度)ので全データの処理は難しいことから、割込み処理は package を受信したら受信フラグをたて、package 解析&表示処理側で package の処理が終了したら 受信フラグを消すようにしました。このように解析中に受信したデータは捨てることで欠損のない package データを処理できるようにしています。



4.LiDAR 接続での問題点
 LiDAR の出力情報を受信し、LCDに表示する処理を作る上で次の問題が発生しました。
  1. 三角関数の未定義エラー
  2. 処理時間
  3. LiDAR の不調

 4-1.三角関数の未定義エラー
 「undefined reference to 'sin'」というエラーが発生し、しかもソース上のエラー指摘行が sin を使用していない行です。math.h をインクルード済みでこの中で宣言されていますがエラーがでます。ソースファイルの先頭部でプロトタイプ宣言しても状況変わらず・・
 下図のオプション設定を追加することでエラーが解消されました。reference が無いというエラーなのでリンク時のエラーだったんですね。

三角関数未定義エラーの対処設定


 4-2.処理時間
 処理に時間が掛かり LiDAR からの多くの package を読み飛ばしてしまっていました。処理中のロジアナ波形例を下図に示します。上述したように割込み処理で package の全データを受信してから受信フラグを立てるので、下図のマーカーペア0が package 内のデータの取り出し+1つのポイント描画の時間で、マーカーペア1が1つのポイント描画の時間なのでデータ取り出し処理の時間はその差分の 1ms 程です。package には最大で有効データが8個存在します。下図の例では5package につき一つの package を処理していることになります。
 尚、マーカーペア1が package 受信に要する時間です。

LiDAR からのpackageデータ処理中のロジアナ波形例


 仮に整数のみで処理した場合(三角関数はテーブル引き)の想定ロジアナ波形が下図になります。下図から package 2~3個中1個を処理できるようになりそうです。MounRiver Studioでは最適化オプションがディフォルトではサイズ優先(-Os)になっていますが、速度優先(-Ofast)に変更してもあまり変わりませんでした。まずは整数化せずに進めることにします。

LiDAR からのpackageデータ処理中のロジアナ波形(整数のみで処理した想定の場合)


 4-3.LiDAR の不調
 この LiDAR の不調が今回最も頭を悩ました問題です。特に角度情報が更新されずに全て 0xa000 になったりして非常に不安定です。ロジアナで採取した波形から連続した package データを並べたものが下図のサンプルです。
 サンプル2を見ると Flag 部分のビットパターンより、距離データが ( DistanceL, DistanceH, Flag ) ではなく、( DistanceL, Flag, DistanceH ) なのではないかと思えてきますが、その並びで処理してもきれいな表示にはなりませんでした。

packageデータサンプル1

packageデータサンプル2


 現状での LiDAR データの LCD 表示例は下図になります。LiDAR は机の上に置いている状態なので周りにはモニタディスプレイや半田ごて等、雑多な物がある状態です。LiDAR が不安定なので時と場合より表示状態が変化します。

LiDAR データ表示例



5.まとめ
 CH32V203 での割込みによるシリアル受信の実装は早々にできたのですが、4年前に購入した格安の LiDAR の不調で悩みました。
 CH32V の割込み処理もサンプルを参照しながら割合簡単に実装できることが判りました。最終的には LiDAR のデータを奇麗に描けなかったので別の格安 LiDAR で再挑戦してみたい気もします。


★追記 2024/07/20
 データ抽出処理部のバグを修正しました。まだ LiDAR が不安定になる場合がありますが、調子の良い時にはかなりきれいに描画できるようになりました。
 ¥120のマイコンでもほぼリアルタイムに表示できるのが嬉しいですね。


LiDAR データ表示例(改善版)




[TOP] [ 前へ ] 連載記事一覧 [ 次へ ]

nice!(0)  コメント(1) 
共通テーマ:趣味・カルチャー