2018年11月14日

ArduinoとフルカラードットマトリクスLEDでオーディオスペアナを作ったよ

前回から引き続きフルカラードットマトリクスLEDものです。
前回の記事では、フルカラードットマトリクスLEDをSPIから簡単に表示できるようにするLEDパネルコントローラを作成し、それを用いて動画プレイヤーを作成しました。
が、私が作成したLEDパネルコントローラは動画再生だけでなく、フルカラードットマトリクスLEDをSPI接続のグラフィックディスプレイのように扱える仕様となっており、より汎用的な使い方が可能です。
そこで今回は64*32ピクセルのフルカラードットマトリクスLEDとLEDパネルコントローラ、Arduino nanoでオーディオスペクトラムアナライザを作成してみることにしました。

Arduinoとスペクトラムアナライザで調べたところ、MSGEQ7を用いた7バンドのものが一般的なようです。
しかし、7バンドでは横64ピクセルのドットマトリクスLEDは到底活かしきれません。
そこで今回は、Arduinoに直接音声信号を取り込み、Arduino内部でFFTを行うこととしました。
最初はArduinoに搭載されている16MHzなATmega328pではリアルタイムでFFTを処理させるのはかなり荷が重いのではないか?思ったようなパフォーマンスが出せないのではないか?などと危惧しておりましたが、後述する非常に高速なFFTライブラリを使用することにより結果的に満足のいくパフォーマンスを発揮することができました。

それでは内容に入る前に、とりあえずの成果をご覧いただこうと思います。

いかがでしょうか?十分な応答速度を以てして音声に機敏に反応していることがお分かりいただけるかと思います。
これだけの応答速度を確保できたのは、高速なFFTライブラリとLEDパネルコントローラによる描画コストの大幅な削減によるところがかなり大きいです。

ではようやく内容に入っていこうと思います。
何はともあれ、まずはハードウェアです。
先ほど音声信号をArduinoに直接取り込むと書きましたが、厳密にはこれは誤りなんですよね。
というのも、Arduinoに入力できる電圧範囲は0[V]から5[V]の範囲であることは周知の事実ですが、パソコンのイヤホン端子などから出力される音声信号の電圧範囲は-0.447[V]から0.447[V]のようなのです。(一般的なラインレベルが-10dBVというところから来ていますが、間違ってるかもしれません。)
つまり、電圧がマイナス側にも振れてしまうこと、振幅が小さすぎることの2点でArduinoに直接音声信号を入力することができません。
よって今回の回路では、まず音声信号に5[V]の半分の電圧である2.5[V]だけ電圧を引き上げ、その後振幅を大きくするため増幅回路を通したあとにArduinoのアナログピンに入力するという構成になっています。
では以下に回路図を示します。
回路図_1
|翕静徹冥転/非反転増幅回路
X1とX2がステレオミニジャックです。単に並列につながっているだけなのでどちらを入力にも出力にもできます。
見ての通り、ステレオ音声信号のうち片方の音声しか取り込んでいません。まぁご愛嬌ってことで・・・・
R17はスペアナの感度調整用です。
C1を通って、R17の片側を中点電位に接続してあります。こうすることで入力された音声信号に中点電位分だけの電圧を重畳させることができます。以降は何の変哲もない非反転増幅回路です。音量がかなり小さい時でも元気よくスペアナを振らせるため、101倍とかなりでかいゲイン設定にしてあります。
なお、オペアンプはレールtoレール出力(フルスイング出力)のものを使用する必要があります。

中点電位生成回路(レールスプリッタ)
R9、R10、R11で電源電圧を半分に分圧し、ボルテージフォロワに入力しています。R11は中点電位の微調整用です。ここは多回転半固定抵抗を使うといいと思います。

アナログ電源LPF回路
R6とC3で極端にカットオフ周波数が低いローパスフィルタを構成し、オペアンプの電源として利用しています。こうすることで大元の電源から混入するノイズをカットしています。
電源に直列にR6が入っている関係でVCCの電圧は+5Vより低下するため、この電圧をArduinoのアナログ基準電圧ピンに入力しています。プログラムのほうで基準電圧源を外部に設定します。

LEDパネルコントローラ用SPI分圧回路
ここにLEDパネルコントローラを接続しますが、LEDパネルコントローラに入力できる電圧は3.3Vであるため分圧抵抗を入れてあります。
なお、LEDパネルコントローラについては以下の過去記事を参照してください。
LEDパネルコントローラ完成
フルカラードットマトリクスLEDパネル用のSPIインターフェイスを作る

ハードウェアとしてはこんなものです。簡単な回路なため、作成難易度も低いと思われます。
なんか足りないよなぁ・・・と思ってたけど、そういえば作成した基板の画像載せるの忘れてました。
こちらが基板表になります。
基板表
実は若干要らない部品がついてたりしますが、あまり気にしないでください。

こちらが基板裏です。
基板裏
ユニバーサル基板裏の配線に対するこだわりを失ったため、かなり汚いことになっていますがご了承ください。
あとこれは小技ですが、ボリュームの金属ケースに鈴メッキ線などをはんだ付けしGNDに落としてやるとノイズ軽減に効果的です。

こちらが全体画像になります。
全体画像
緑色の基板が以前作成したLEDパネルコントローラです。

つづいて、ソフトウェアです。
なお、スケッチをコンパイルする前に以下のサイトよりArduino FHT libraryをダウンロードし、ArduinoIDEにライブラリを追加しておいてください。
Arduino FHT Library
ライブラリの追加方法については他のサイトにわかりやすい解説がたくさんあるため、ここでは説明はしません。
それでは以下にArduinoのスケッチを示します。
かなり汚いコードでごめんなさい・・・
#define LOG_OUT 1 // use the log output function
#define FHT_N 256 // set to 256 point fht

#define SS 10
#define RST 9

#define MATRIXLED_Y_COUNT 32
#define MATRIXLED_X_COUNT 64
#define MATRIXLED_COLOR_COUNT 3

#define ADC_OFFSET 0x0200
#define FFT_OFFSET 0xa0
#define MOVING_AVERAGE_COUNT 2
#define MOVING_AVERAGE_COUNT_INDEX 1

#define RESET 0
#define SET 1

#include <FHT.h> // include the library
#include <SPI.h>

//周波数軸を対数に変換する用の配列
const unsigned char conv_x_freq[MATRIXLED_X_COUNT] = {
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x08, 0x09, 0x09, 0x0A, 0x0B,
0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x13, 0x14, 0x16, 0x17, 0x19, 0x1B, 0x1D, 0x20, 0x22, 0x25, 0x28, 0x2B, 0x2F, 0x32, 0x37, 0x3B, 0x40, 0x45, 0x4A, 0x50, 0x56, 0x5D, 0x65, 0x6D, 0x76, 0x7F
};

//グラデーションのやつ
//BGRの順に並んでる 24bitビットマップのデータ部だけのやつ互換
const unsigned char color_palette[MATRIXLED_X_COUNT * MATRIXLED_COLOR_COUNT] = {
/*
//グラデ RGB 輝度一定
0xFF, 0x42, 0x3D, 0xFE, 0x4A, 0x36, 0xFD, 0x52, 0x2F, 0xFC, 0x59, 0x29, 0xFA, 0x61, 0x23, 0xF7, 0x6A, 0x1E, 0xF4, 0x72, 0x18, 0xF0, 0x7B, 0x13, 0xEC, 0x83, 0x10, 0xE7, 0x8C, 0x0B, 0xE2, 0x94,
0x08, 0xDD, 0x9C, 0x06, 0xD7, 0xA5, 0x03, 0xD0, 0xAC, 0x02, 0xCA, 0xB4, 0x01, 0xC3, 0xBC, 0x00, 0xBB, 0xC3, 0x00, 0xB4, 0xCA, 0x01, 0xAC, 0xD1, 0x02, 0xA4, 0xD7, 0x04, 0x9C, 0xDD, 0x05, 0x94,
0xE2, 0x08, 0x8B, 0xE8, 0x0C, 0x83, 0xEC, 0x0F, 0x7B, 0xF0, 0x14, 0x72, 0xF4, 0x19, 0x6A, 0xF7, 0x1E, 0x62, 0xFA, 0x23, 0x5A, 0xFC, 0x29, 0x52, 0xFD, 0x2F, 0x4A, 0xFE, 0x36, 0x42, 0xFF, 0x3D,
0x3B, 0xFF, 0x44, 0x34, 0xFE, 0x4C, 0x2E, 0xFD, 0x54, 0x27, 0xFB, 0x5B, 0x22, 0xF9, 0x64, 0x1C, 0xF6, 0x6D, 0x17, 0xF3, 0x75, 0x12, 0xEF, 0x7D, 0x0E, 0xEB, 0x86, 0x0B, 0xE6, 0x8E, 0x08, 0xE1,
0x96, 0x05, 0xDB, 0x9F, 0x03, 0xD5, 0xA7, 0x02, 0xCF, 0xAE, 0x00, 0xC8, 0xB6, 0x00, 0xC1, 0xBE, 0x00, 0xBA, 0xC5, 0x01, 0xB2, 0xCC, 0x02, 0xAA, 0xD2, 0x04, 0xA2, 0xD8, 0x06, 0x9A, 0xDE, 0x09,
0x92, 0xE3, 0x0C, 0x89, 0xE9, 0x10, 0x81, 0xED, 0x15, 0x79, 0xF1, 0x1A, 0x70, 0xF5, 0x1F, 0x68, 0xF8, 0x25, 0x60, 0xFB, 0x2B, 0x58, 0xFC, 0x31, 0x50, 0xFE, 0x38, 0x48, 0xFF, 0x3F, 0x41, 0xFF
*/
/*
//グラデ RGB 輝度グラデ
0x5C, 0x15, 0x19, 0x5F, 0x19, 0x16, 0x61, 0x1D, 0x14, 0x64, 0x21, 0x12, 0x66, 0x25, 0x11, 0x69, 0x2A, 0x0E, 0x6B, 0x2F, 0x0C, 0x6E, 0x35, 0x0B, 0x71, 0x3B, 0x09, 0x73, 0x42, 0x07, 0x76, 0x49,
0x05, 0x78, 0x51, 0x04, 0x7B, 0x5A, 0x03, 0x7D, 0x63, 0x02, 0x80, 0x6D, 0x01, 0x83, 0x79, 0x00, 0x84, 0x84, 0x00, 0x7D, 0x88, 0x00, 0x75, 0x8A, 0x01, 0x6F, 0x8D, 0x02, 0x68, 0x8F, 0x03, 0x62,
0x92, 0x05, 0x5B, 0x94, 0x07, 0x55, 0x97, 0x09, 0x50, 0x9A, 0x0C, 0x4A, 0x9C, 0x0F, 0x44, 0x9F, 0x13, 0x40, 0xA1, 0x16, 0x3A, 0xA4, 0x1A, 0x36, 0xA6, 0x1F, 0x31, 0xA9, 0x24, 0x2C, 0xAB, 0x29,
0x28, 0xAE, 0x2F, 0x24, 0xB1, 0x36, 0x20, 0xB3, 0x3C, 0x1C, 0xB6, 0x43, 0x18, 0xB8, 0x4C, 0x15, 0xBB, 0x54, 0x11, 0xBD, 0x5C, 0x0E, 0xC0, 0x67, 0x0B, 0xC2, 0x72, 0x08, 0xC5, 0x7D, 0x06, 0xC8,
0x89, 0x04, 0xCA, 0x97, 0x02, 0xCD, 0xA6, 0x01, 0xCF, 0xB5, 0x00, 0xD2, 0xC7, 0x00, 0xCF, 0xD4, 0x01, 0xC1, 0xD7, 0x02, 0xB5, 0xDA, 0x03, 0xAA, 0xDC, 0x05, 0x9E, 0xDF, 0x08, 0x94, 0xE1, 0x0C,
0x88, 0xE4, 0x0F, 0x80, 0xE6, 0x13, 0x76, 0xE9, 0x18, 0x6E, 0xEB, 0x1E, 0x64, 0xEE, 0x23, 0x5D, 0xF1, 0x29, 0x54, 0xF3, 0x2F, 0x4E, 0xF6, 0x37, 0x46, 0xF8, 0x3E, 0x3F, 0xFB, 0x47, 0x39, 0xFD
*/

//グラデ サーモグラフ風
0x70, 0x00, 0x0F, 0x73, 0x00, 0x14, 0x79, 0x00, 0x1D, 0x7F, 0x00, 0x28, 0x85, 0x00, 0x33, 0x8A, 0x00, 0x3D, 0x8E, 0x01, 0x47, 0x90, 0x01, 0x51, 0x92, 0x01, 0x5C, 0x93, 0x01, 0x67, 0x94, 0x01,
0x71, 0x94, 0x01, 0x7B, 0x94, 0x01, 0x85, 0x94, 0x01, 0x8F, 0x93, 0x02, 0x98, 0x92, 0x02, 0xA0, 0x90, 0x02, 0xA7, 0x8F, 0x03, 0xAD, 0x8E, 0x05, 0xB3, 0x8C, 0x07, 0xB8, 0x88, 0x0A, 0xBD, 0x82,
0x0E, 0xC2, 0x7C, 0x13, 0xC6, 0x74, 0x19, 0xCA, 0x6A, 0x1F, 0xCF, 0x5E, 0x26, 0xD3, 0x50, 0x2C, 0xD8, 0x42, 0x32, 0xDC, 0x33, 0x38, 0xDE, 0x25, 0x40, 0xE0, 0x1A, 0x47, 0xE2, 0x11, 0x4D, 0xE5,
0x0B, 0x53, 0xE7, 0x07, 0x59, 0xE9, 0x05, 0x5E, 0xEB, 0x04, 0x64, 0xEC, 0x04, 0x69, 0xEE, 0x04, 0x70, 0xF0, 0x04, 0x76, 0xF2, 0x04, 0x7C, 0xF4, 0x05, 0x83, 0xF5, 0x06, 0x89, 0xF6, 0x06, 0x90,
0xF7, 0x06, 0x97, 0xF8, 0x06, 0x9E, 0xF9, 0x06, 0xA6, 0xFA, 0x06, 0xAD, 0xFA, 0x06, 0xB3, 0xFB, 0x06, 0xBA, 0xFB, 0x07, 0xC1, 0xFB, 0x08, 0xC7, 0xFB, 0x0B, 0xCD, 0xFB, 0x11, 0xD2, 0xFC, 0x1A,
0xD8, 0xFC, 0x26, 0xDC, 0xFD, 0x35, 0xE1, 0xFD, 0x49, 0xE5, 0xFD, 0x5F, 0xEA, 0xFD, 0x77, 0xED, 0xFD, 0x8F, 0xF0, 0xFC, 0xA7, 0xF3, 0xFC, 0xBE, 0xF6, 0xFC, 0xD0, 0xF7, 0xFD, 0xDA, 0xF8, 0xFD

/*
//グラデ RGB
0xFF, 0x02, 0x03, 0xFF, 0x0F, 0x00, 0xFF, 0x20, 0x00, 0xFF, 0x30, 0x00, 0xFF, 0x41, 0x00, 0xFF, 0x51, 0x00, 0xFF, 0x62, 0x00, 0xFF, 0x72, 0x00, 0xFF, 0x83, 0x00, 0xFF, 0x93, 0x00, 0xFF, 0xA4,
0x00, 0xFF, 0xB4, 0x00, 0xFF, 0xC5, 0x00, 0xFF, 0xD5, 0x00, 0xFF, 0xE6, 0x00, 0xFF, 0xF6, 0x00, 0xF7, 0xFF, 0x00, 0xE7, 0xFF, 0x00, 0xD6, 0xFF, 0x00, 0xC6, 0xFF, 0x00, 0xB5, 0xFF, 0x00, 0xA5,
0xFF, 0x00, 0x94, 0xFF, 0x00, 0x84, 0xFF, 0x00, 0x73, 0xFF, 0x00, 0x63, 0xFF, 0x00, 0x52, 0xFF, 0x00, 0x42, 0xFF, 0x00, 0x31, 0xFF, 0x00, 0x21, 0xFF, 0x00, 0x10, 0xFF, 0x00, 0x02, 0xFF, 0x02,
0x00, 0xFF, 0x11, 0x00, 0xFF, 0x21, 0x00, 0xFF, 0x31, 0x00, 0xFF, 0x42, 0x00, 0xFF, 0x52, 0x00, 0xFF, 0x63, 0x00, 0xFF, 0x73, 0x00, 0xFF, 0x84, 0x00, 0xFF, 0x94, 0x00, 0xFF, 0xA5, 0x00, 0xFF,
0xB5, 0x00, 0xFF, 0xC6, 0x00, 0xFF, 0xD6, 0x00, 0xFF, 0xE7, 0x00, 0xFF, 0xF7, 0x00, 0xF6, 0xFF, 0x00, 0xE6, 0xFF, 0x00, 0xD5, 0xFF, 0x00, 0xC5, 0xFF, 0x00, 0xB4, 0xFF, 0x00, 0xA4, 0xFF, 0x00,
0x93, 0xFF, 0x00, 0x83, 0xFF, 0x00, 0x72, 0xFF, 0x00, 0x62, 0xFF, 0x00, 0x51, 0xFF, 0x00, 0x41, 0xFF, 0x00, 0x30, 0xFF, 0x00, 0x20, 0xFF, 0x00, 0x0F, 0xFF, 0x00, 0x02, 0xFF, 0x00, 0x00, 0xFF
*/
};

unsigned char X_loop_count, Y_loop_count;

unsigned char bar_height[MATRIXLED_X_COUNT];
unsigned int bar_height_temp;

unsigned char moving_average[MOVING_AVERAGE_COUNT][MATRIXLED_X_COUNT];
unsigned char moving_average_loop = 0;
unsigned char moving_average_add_loop;
unsigned char search_max_value_loop;
unsigned char max_value_temp = 0;

volatile unsigned char ADC_conv_count = 0;
int ADC_result[FHT_N];

SPISettings mySPISettings = SPISettings(8000000, MSBFIRST, SPI_MODE0);

//ADC変換完了割り込み
ISR(ADC_vect){
int adcresult_temp = ADCL;
adcresult_temp |= ADCH << 8;
ADC_result[ADC_conv_count] = adcresult_temp;
ADC_conv_count++;
if(ADC_conv_count >= FHT_N){
ADCSRA = 0; // turn off ADC
}
}

void setup() {
//AD変換関係の初期化 レジスタ直叩き
TIMSK0 = 0; // turn off timer0 for lower jitter
ADCSRB = 0x00;
ADCSRA = 0xfd;
ADMUX = 0x40; // use adc0
DIDR0 = 0x01; // turn off the digital input for adc0

//LEDパネルコントローラ関係の初期化
pinMode(SS, OUTPUT);
digitalWrite(SS, HIGH);
pinMode(RST, OUTPUT);
digitalWrite(RST, LOW);
SPI.begin();
SPI.beginTransaction(mySPISettings);

//ADC基準電圧は外部
analogReference(EXTERNAL);
}

void loop() {
while(ADC_conv_count < (FHT_N - 1)); //ADCが指定の回数変換するまで待機
ADC_conv_count = 0;
for (int i = 0 ; i < FHT_N ; i++){
fht_input[i] = (ADC_result[i] - ADC_OFFSET) << 3;
}
ADCSRA = 0xfe; // restart adc
fht_window(); // window the data for better frequency response
fht_reorder(); // reorder the data before doing the fht
fht_run(); // process the data in the fht
fht_mag_log(); // take the output of the fht

//振幅データ加工のためのループ
for(X_loop_count = 0;X_loop_count < MATRIXLED_X_COUNT;X_loop_count++){

//周波数軸を対数にして要素と要素の間の値から最大値を探して選択する
max_value_temp = 0;
if((X_loop_count > 0) && (conv_x_freq[X_loop_count] > (conv_x_freq[X_loop_count - 1] + 1))){
for(search_max_value_loop = (conv_x_freq[X_loop_count - 1] + 1);search_max_value_loop <= conv_x_freq[X_loop_count];search_max_value_loop++){
if(max_value_temp < fht_log_out[search_max_value_loop]){
max_value_temp = fht_log_out[search_max_value_loop];
}
}
}else{
max_value_temp = fht_log_out[conv_x_freq[X_loop_count]];
}

//振幅を2倍にしてドットマトリクスに表示できる大きさにして最小値最大値のクリッピング
if(((FFT_OFFSET - max_value_temp) >> 2) <= 0){
moving_average[moving_average_loop][X_loop_count] = 0;
}else if(((FFT_OFFSET - max_value_temp) >> 2) > MATRIXLED_Y_COUNT){
moving_average[moving_average_loop][X_loop_count] = MATRIXLED_Y_COUNT;
}else{
moving_average[moving_average_loop][X_loop_count] = (FFT_OFFSET - max_value_temp) >> 2;
}

//移動平均をとる
bar_height_temp = 0;
for(moving_average_add_loop = 0;moving_average_add_loop < MOVING_AVERAGE_COUNT;moving_average_add_loop++){
bar_height_temp += moving_average[moving_average_add_loop][X_loop_count];
}
bar_height[X_loop_count] = (unsigned char)(bar_height_temp >> MOVING_AVERAGE_COUNT_INDEX);

if(bar_height[X_loop_count] >= MATRIXLED_Y_COUNT){
bar_height[X_loop_count] = MATRIXLED_Y_COUNT - 1;
}

}

//移動平均用のカウンタ的な
moving_average_loop++;
if(moving_average_loop >= MOVING_AVERAGE_COUNT){
moving_average_loop = 0;
}

//ドットマトリクスLEDコントローラへのデータ送信
digitalWrite(SS, LOW);
for(Y_loop_count = 0;Y_loop_count < MATRIXLED_Y_COUNT;Y_loop_count++){
for(X_loop_count = 0;X_loop_count < MATRIXLED_X_COUNT;X_loop_count++){
if(bar_height[X_loop_count] <= Y_loop_count){
SPI.transfer(color_palette[(MATRIXLED_X_COUNT - 1 - (bar_height[X_loop_count] * 2)) * MATRIXLED_COLOR_COUNT + 2]);
SPI.transfer(color_palette[(MATRIXLED_X_COUNT - 1 - (bar_height[X_loop_count] * 2)) * MATRIXLED_COLOR_COUNT + 1]);
SPI.transfer(color_palette[(MATRIXLED_X_COUNT - 1 - (bar_height[X_loop_count] * 2)) * MATRIXLED_COLOR_COUNT + 0]);
}else{
SPI.transfer(0x00);
SPI.transfer(0x00);
SPI.transfer(0x00);
}
}
}
digitalWrite(SS, HIGH);

digitalWrite(RST, HIGH);
delayMicroseconds(5);
digitalWrite(RST, LOW);
delayMicroseconds(5);
}

プログラムの詳細な解説入れようかと思ったけど、力尽きた・・・
ので簡単に動作だけ解説します。
まず、ArduinoのADCは連続変換動作を行うようにしています。1回変換が終了するとAD変換終了割り込みが発生し、その後すぐにAD変換を開始します。
AD変換終了割り込みではAD変換した結果を配列に順番に格納していきます。
AD変換結果を割り込み処理内で取得することにより、AD変換中の待機時間を有効活用することができ、結果的に動作速度の向上につながっています。
AD変換結果が必要個数分揃うと、FHTライブラリの入力配列に中身を移動させ、AD変換を再開させます。
その後、FHTを行い、周波数を対数に並べ替え、周波数ビンの要素の大きさをいい感じにし、移動平均を行い、LEDパネルコントローラにデータを送信するという流れになっています。

プログラムの解説については、そのうちちゃんと書き直します・・・

wata_net at 03:06コメント(0) この記事をクリップ!

2018年08月07日

LEDパネルコントローラ完成

前回の更新からフルカラードットマトリクスLEDパネルコントローラは大幅に進化を遂げました。
まずはこれを見てください。画質を上げて60FPSでの視聴をおすすめします。ヌルヌル感が違います。

動画を再生している下のパネルは、64*32ピクセルのLEDパネルを4枚、LEDパネルコントローラを2個、動画再生用にSTM32F401REを搭載しているNucleoボードを使用し、MicroSDカードの中に保存されている動画ファイルと音声ファイルを60FPSで同時に再生しています。普通に動画プレイヤーですね。
上にくっついているのは、64*32ピクセルのLEDパネルを1枚、LEDパネルコントローラを1個使用し、Arduino nanoで簡易的なオーディオレベルメーターを作ったものです。即席で作ったので、入力回路がモノラルでしか取り込んでなかったりなどちょっと雑&適当です。

で、どういう風に進化したのかといいますと、
リフレッシュレートが大幅に向上しました。
64*64ピクセルのLEDパネルでも最低で62FPS、最高で125FPS出るようになりました。
128*32ピクセルのLEDパネルでは、最低で97FPS、最高で129FPS出ます。
64*32ピクセルでは最低で122FPS、最高で249FPS出ます。
描画可能な色数が4096色から6229504色になりました。グラデーションも非常に美しく描画できます。
LEDパネルコントローラ内部でガンマ補正を行い、液晶モニタと比較しても遜色ない発色が可能になりました。
LEDパネルの輝度がかなり明るくなりました。視認性がかなり向上しました。
マイコンの出力にレベルシフタを接続することによってノイズのない安定した表示が可能になりました。
SPI通信でのコマンド制御に対応し、LEDパネル全体の明るさ、描画開始ピクセルと描画終了ピクセルを変更できるようになりました。
ジャンパ設定によって使用するLEDパネルの解像度が変更できるようになりました。
と、以上のような感じです。

上の動画でまず注目していただきたいのが、画面のフリッカ(LEDパネルのリフレッシュレートのせいで撮影した動画がちらついて見える現象)が全く見えないという点です。これは動画撮影するときのカメラの設定でも左右されるのですが、FPSが大幅に向上したことにより以前のLEDパネルコントローラではフリッカなしではどうあがいても撮影できなかったものが、完全にフリッカ無しで撮影できるようになっています。
ちなみにたまに動きが大きいと横筋が入ることがありますが、これは完全にカメラのせいです。肉眼では全く見えません。
もう一点注目していただきたいのが、背景の空の色です。以前のLEDパネルコントローラでは発色数が4096色であったために空の色などの微妙なグラデーションは色が分離して見えてしまっていましたが、新しいLEDパネルコントローラでは6229504色と表現できる色数が圧倒的に増えたため、微妙なグラデーションも非常に美しく描画できています。

個人的には、自分の技術力とSTM32F103C8T6でできる限界まで来たかなと思っています。

基板も作成し、こんな感じになりました。
DSC_2833
裏面にレベルシフタ用の74HCT245が2つ引っ付いています。
ハードウェアはシンプルです。一応外部にI2CとかUARTとかCAN/USBとか出てきているのですが、おそらく使用することはないでしょう・・・
というのも、LEDパネルが128*64ピクセルで1ピクセル当たり24bitなので60FPSで動画再生するには約12Mbpsもの通信速度が必要になります。I2CやCANやUARTでは通信速度が圧倒的に足りないです。
ちなみに動画のLEDディスプレイでは21Mbpsで通信しています。通信速度が高速なだけあって、配線が結構シビアでした。

今回、このLEDパネルコントローラの取説みたいなのを作ってみました。興味がありましたら是非ご覧ください。
必要なことはすべて書いてあると思います。
こちらからダウンロードできます。
一応ですが、間違いなどあるかもしれませんのでご了承ください。もし間違いなどを見つけた場合はこのブログ記事のコメント欄にて報告お願いいたします。
LEDパネルコントローラ取扱説明書v0.81
下にプレビューも置いときます。

LEDパネルコントローラ取扱説明書v0.81 by wata_net on Scribd



LEDパネルコントローラを使えばArduinoで簡単にLEDパネルを表示できるんだぞっていうのをアッピルするためにArduinoで基本図形を描画するサンプルプログラムを作成しました。
すいません、たぶんこれもウンコードだと思います・・・
プログラミングできる人は自分で書いたほうが良いかもしれませんね!
一応ここからダウンロードできます。LEDパネル解像度64*32ピクセル用です。#defineのところの大きさ変えると他の解像度でも対応できるかもしれません。
これも間違いなどあるかもしれませんのでご了承ください。もし間違いなどを見つけた場合はこのブログ記事のコメント欄にて報告お願いいたします。
LEDパネルコントローラサンプルプログラムforArduinoV0.8
下にプレビューも置いておきます。(シンタックスハイライトに縦スクロールバーつけるやつを学んだ)
どうやらprism.jsとかいうシンタックスハイライター、行数が長くなりすぎるとブログ記事ごとお亡くなりになされてしまうようです?最初理由が分からなくてブチギレそうになりましたが、うまく読み込まないブログ記事編集ページをダウンロードし、これまで書き込んだ内容を復活させるとともにシンタックスハイライターの中身を一部省略することで事なきを得ました。
また一から書き直さないといけなくなるかと思った。
#include <SPI.h>

#define SS 10 //LEDパネルスレーブセレクトピン
#define RST 9 //LEDパネルリセットピン

#define MATRIXLED_Y_COUNT 32 //LEDパネルの縦解像度
#define MATRIXLED_X_COUNT 64 //LEDパネルの横解像度
#define MATRIXLED_COLOR_COUNT 3 //LEDパネルの色数(RGBなので3)

#define FONT_DATA_PIXEL_SIZE_X 5 //フォントデータの横解像度
#define FONT_DATA_PIXEL_SIZE_Y 8 //フォントデータの縦解像度

//フォントデータ
const PROGMEM unsigned char Font_Data[192][5] =
{
//省略
};

//サンプル画像データ(64*32)
const PROGMEM unsigned char image[64*32*3] = {
//省略
};

SPISettings mySPISettings = SPISettings(8000000, MSBFIRST, SPI_MODE0);

void setup() {
pinMode(SS, OUTPUT);
digitalWrite(SS, HIGH);
pinMode(RST, OUTPUT);
digitalWrite(RST, LOW);

SPI.begin();
SPI.beginTransaction(mySPISettings);

draw_image_for_24bitBMP(0, 0, MATRIXLED_X_COUNT, MATRIXLED_Y_COUNT, image, 0xff);
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, 0xff);
}

void loop() {
unsigned char char_buff[12];
unsigned char char_buff1[12];
static unsigned char loop_count;

unsigned char x_start, y_start, x_end, y_end, radius;
unsigned char rand_0, rand_1, rand_2, rand_3;

unsigned char display_brightness = 0;

/*
* 点描画サンプル
*/
for(loop_count = 0;loop_count < 200;loop_count++){
sprintf(char_buff, "Dot %3u", loop_count + 1);
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);

x_start = (rand() % MATRIXLED_X_COUNT);
y_start = (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 1)) + FONT_DATA_PIXEL_SIZE_Y + 1;

draw_pixel(x_start, y_start, (rand() % (0xff + 1)), (rand() % (0xff + 1)), (rand() % (0xff + 1)), display_brightness);
delay(5);
}
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);

/*
* 直線描画サンプル
*/
for(loop_count = 0;loop_count < 50;loop_count++){
sprintf(char_buff, "Line %3u", loop_count + 1);
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);

x_start = (rand() % MATRIXLED_X_COUNT);
y_start = (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 1)) + FONT_DATA_PIXEL_SIZE_Y + 1;
x_end = (rand() % MATRIXLED_X_COUNT);
y_end = (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 1)) + FONT_DATA_PIXEL_SIZE_Y + 1;

draw_line(x_start, y_start, x_end, y_end, (rand() % (0xff + 1)), (rand() % (0xff + 1)), (rand() % (0xff + 1)), display_brightness);
delay(50);
}
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);

/*
* 四角形描画サンプル
*/
for(loop_count = 0;loop_count < 50;loop_count++){
sprintf(char_buff, "Rect %3u", loop_count + 1);
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);

rand_0 = (rand() % MATRIXLED_X_COUNT);
rand_1 = (rand() % MATRIXLED_X_COUNT);
rand_2 = (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 1)) + FONT_DATA_PIXEL_SIZE_Y + 1;
rand_3 = (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 1)) + FONT_DATA_PIXEL_SIZE_Y + 1;

if(rand_0 <= rand_1){
x_start = rand_0;
x_end = rand_1;
}else{
x_start = rand_1;
x_end = rand_0;
}
if(rand_2 <= rand_3){
y_start = rand_2;
y_end = rand_3;
}else{
y_start = rand_3;
y_end = rand_2;
}

draw_rectangle(x_start, y_start, x_end, y_end, (rand() % (0xff + 1)), (rand() % (0xff + 1)), (rand() % (0xff + 1)), display_brightness);
delay(50);
}
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);

/*
* 四角形(中埋め)描画サンプル
*/
for(loop_count = 0;loop_count < 50;loop_count++){
sprintf(char_buff, "RectFill%2u", loop_count + 1);
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);

rand_0 = (rand() % MATRIXLED_X_COUNT);
rand_1 = (rand() % MATRIXLED_X_COUNT);
rand_2 = (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 1)) + FONT_DATA_PIXEL_SIZE_Y + 1;
rand_3 = (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 1)) + FONT_DATA_PIXEL_SIZE_Y + 1;

if(rand_0 <= rand_1){
x_start = rand_0;
x_end = rand_1;
}else{
x_start = rand_1;
x_end = rand_0;
}
if(rand_2 <= rand_3){
y_start = rand_2;
y_end = rand_3;
}else{
y_start = rand_3;
y_end = rand_2;
}

draw_rectangle_fill(x_start, y_start, x_end, y_end, (rand() % (0xff + 1)), (rand() % (0xff + 1)), (rand() % (0xff + 1)), display_brightness);
delay(50);
}
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);

/*
* 円描画サンプル
*/
for(loop_count = 0;loop_count < 50;loop_count++){
sprintf(char_buff, "Circle %2u", loop_count + 1);
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);

if((MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y) < MATRIXLED_X_COUNT){
radius = (rand() % ((MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 2) >> 1)) + 1;
}else{
radius = (rand() % ((MATRIXLED_X_COUNT - 2) >> 1)) + 1;
}

x_start = radius + (rand() % (MATRIXLED_X_COUNT - radius * 2));
y_start = radius + FONT_DATA_PIXEL_SIZE_Y + (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - radius * 2 - 1)) + 1;

draw_circle(x_start, y_start, radius, (rand() % (0xff + 1)), (rand() % (0xff + 1)), (rand() % (0xff + 1)), display_brightness);
delay(50);
}
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);

/*
* 円(中埋め)描画サンプル
*/
for(loop_count = 0;loop_count < 50;loop_count++){
sprintf(char_buff, "CircFill%2u", loop_count + 1);
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);

if((MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y) < MATRIXLED_X_COUNT){
radius = (rand() % ((MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - 2) >> 1)) + 1;
}else{
radius = (rand() % ((MATRIXLED_X_COUNT - 2) >> 1)) + 1;
}

x_start = radius + (rand() % (MATRIXLED_X_COUNT - radius * 2));
y_start = radius + FONT_DATA_PIXEL_SIZE_Y + (rand() % (MATRIXLED_Y_COUNT - FONT_DATA_PIXEL_SIZE_Y - radius * 2 - 1)) + 1;

draw_circle_fill(x_start, y_start, radius, (rand() % (0xff + 1)), (rand() % (0xff + 1)), (rand() % (0xff + 1)), display_brightness);
delay(50);
}
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);

/*
* テキスト描画サンプル
*/
for(loop_count = 0;loop_count < 50;loop_count++){
sprintf(char_buff, "Text %2u", loop_count + 1);
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);

x_start = (rand() % (MATRIXLED_X_COUNT - (FONT_DATA_PIXEL_SIZE_X + 1) * 2));
y_start = FONT_DATA_PIXEL_SIZE_Y + 1 + (rand() % (MATRIXLED_Y_COUNT + 1 - 2 * (FONT_DATA_PIXEL_SIZE_Y + 1)));

sprintf(char_buff1, "%2u", loop_count);
draw_text(x_start, y_start, char_buff1, (rand() % (0xff + 1)), (rand() % (0xff + 1)), (rand() % (0xff + 1)), display_brightness);
delay(50);
}
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);

/*
* テキスト(背景単色埋め)描画サンプル
*/
for(loop_count = 0;loop_count < 50;loop_count++){
sprintf(char_buff, "TextFill%2u", loop_count + 1);
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);

x_start = (rand() % (MATRIXLED_X_COUNT - (FONT_DATA_PIXEL_SIZE_X + 1) * 2));
y_start = FONT_DATA_PIXEL_SIZE_Y + 1 + (rand() % (MATRIXLED_Y_COUNT + 1 - 2 * (FONT_DATA_PIXEL_SIZE_Y + 1)));

sprintf(char_buff1, "%2u", loop_count);
draw_text_fill_back(x_start, y_start, char_buff1, 0xff, 0xff, 0xff, (rand() % (0xff + 1)), (rand() % (0xff + 1)), (rand() % (0xff + 1)), display_brightness);
delay(50);
}
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);

/*
* 画像サンプル1
*/
draw_image_for_24bitBMP(0, 0, MATRIXLED_X_COUNT, MATRIXLED_Y_COUNT, image, display_brightness);
sprintf(char_buff, "image64*32");
draw_text_fill_back(0, 0, char_buff, 0xff, 0xff, 0xff, 0x00, 0x40, 0x40, display_brightness);
delay(1000);
draw_rectangle_fill(0, 0, MATRIXLED_X_COUNT - 1, MATRIXLED_Y_COUNT - 1, 0x00, 0x00, 0x00, display_brightness);
}


/*
* コマンド送信関数
* 描画開始ピクセル座標、描画終了ピクセル座標、LEDパネルの輝度を変更する
* 通常はユーザープログラムの中からはこの関数を呼び出す必要はなく、後述の描画関数の中でこの関数を呼び出している
* 引数1:描画開始ピクセルY座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数2:描画終了ピクセルY座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数3:描画開始ピクセルX座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数4:描画終了ピクセルX座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数5:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
*/
void send_command(unsigned char draw_start_pixel_y, unsigned char draw_end_pixel_y, unsigned char draw_start_pixel_x, unsigned char draw_end_pixel_x, unsigned char panel_brightness){
if((draw_start_pixel_y < MATRIXLED_Y_COUNT) && (draw_end_pixel_y < MATRIXLED_Y_COUNT) && (draw_start_pixel_y <= draw_end_pixel_y)){
if((draw_start_pixel_x < MATRIXLED_X_COUNT) && (draw_end_pixel_x < MATRIXLED_X_COUNT) && (((draw_start_pixel_x <= draw_end_pixel_x) && (draw_start_pixel_y == draw_end_pixel_y)) || (draw_start_pixel_y < draw_end_pixel_y))){
digitalWrite(RST, HIGH);
delayMicroseconds(5);
digitalWrite(SS, LOW);
SPI.transfer(draw_start_pixel_y);
SPI.transfer(draw_end_pixel_y);
SPI.transfer(draw_start_pixel_x);
SPI.transfer(draw_end_pixel_x);
SPI.transfer(panel_brightness);
digitalWrite(SS, HIGH);
}
}
}

/*
* 点を描画する関数
* 指定したピクセル座標に任意の色の点を描画する
* 引数1:点を描画するX座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数2:点を描画するY座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数3:赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数4:緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数5:青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数6:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
*/
void draw_pixel(unsigned char pixel_x, unsigned char pixel_y, unsigned char color_r, unsigned char color_g, unsigned char color_b, unsigned char panel_brightness){
if((pixel_x < MATRIXLED_X_COUNT) && (pixel_y < MATRIXLED_Y_COUNT)){
send_command(pixel_y, pixel_y, pixel_x, pixel_x, panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
SPI.transfer(color_r);
SPI.transfer(color_g);
SPI.transfer(color_b);
digitalWrite(SS, HIGH);
}
}

/*
* 直線を描画する関数
* 指定した始点ピクセル座標から終点ピクセル座標まで任意の色で直線を描画する
* 引数1:直線の始点X座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数2:直線の始点Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数3:直線の終点X座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数4:直線の終点Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数5:赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数6:緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数7:青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数8:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
*/
void draw_line(unsigned char start_pixel_x, unsigned char start_pixel_y, unsigned char end_pixel_x, unsigned char end_pixel_y, unsigned char color_r, unsigned char color_g, unsigned char color_b, unsigned char panel_brightness){
unsigned char dx, dy;
signed char sx, sy;
signed char diff = 0;
unsigned char x_loop = 0, y_loop = 0;

unsigned char draw_x_loop;

if((start_pixel_x < MATRIXLED_X_COUNT) && (end_pixel_x < MATRIXLED_X_COUNT) && (start_pixel_y < MATRIXLED_Y_COUNT) && (end_pixel_y < MATRIXLED_Y_COUNT)){
if(start_pixel_y == end_pixel_y){
if(start_pixel_x <= end_pixel_x){
send_command(start_pixel_y, end_pixel_y, start_pixel_x, end_pixel_x, panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_x_loop = start_pixel_x;draw_x_loop <= end_pixel_x;draw_x_loop++){
SPI.transfer(color_r);
SPI.transfer(color_g);
SPI.transfer(color_b);
}
digitalWrite(SS, HIGH);
}else{
send_command(start_pixel_y, end_pixel_y, end_pixel_x, start_pixel_x, panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_x_loop = end_pixel_x;draw_x_loop <= start_pixel_x;draw_x_loop++){
SPI.transfer(color_r);
SPI.transfer(color_g);
SPI.transfer(color_b);
}
digitalWrite(SS, HIGH);
}
}else{
dx = (end_pixel_x > start_pixel_x) ? (end_pixel_x - start_pixel_x) : (start_pixel_x - end_pixel_x);
dy = (end_pixel_y > start_pixel_y) ? (end_pixel_y - start_pixel_y) : (start_pixel_y - end_pixel_y);

sx = (end_pixel_x > start_pixel_x) ? 1 : -1;
sy = (end_pixel_y > start_pixel_y) ? 1 : -1;

if(dx > dy){
diff = -dx;
for(x_loop = 0;x_loop <= dx;x_loop++){
draw_pixel(start_pixel_x, start_pixel_y, color_r, color_g, color_b, panel_brightness);
start_pixel_x += sx;
diff += 2 * dy;
if(diff >= 0){
start_pixel_y += sy;
diff -= 2 * dx;
}
}
}else{
diff = -dy;
for(y_loop = 0;y_loop <= dy;y_loop++){
draw_pixel(start_pixel_x, start_pixel_y, color_r, color_g, color_b, panel_brightness);
start_pixel_y += sy;
diff += 2 * dx;
if(diff >= 0){
start_pixel_x += sx;
diff -= 2 * dy;
}
}
}
}
}
}

/*
* 四角形を描画する関数
* 指定した左上ピクセル座標から右下ピクセル座標まで任意の色で四角形を描画する
* 引数1:四角形の左上X座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数2:四角形の左上Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数3:四角形の右下X座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数4:四角形の右下Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数5:赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数6:緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数7:青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数8:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
* 引数1〜引数4については以下の関係が成り立っている必要がある
* 四角形の左上X座標 <= 四角形の右下X座標
* 四角形の左上Y座標 <= 四角形の右下Y座標
*/
void draw_rectangle(unsigned char start_pixel_x, unsigned char start_pixel_y, unsigned char end_pixel_x, unsigned char end_pixel_y, unsigned char color_r, unsigned char color_g, unsigned char color_b, unsigned char panel_brightness){
unsigned char draw_y_loop, draw_x_loop;

if((end_pixel_x < MATRIXLED_X_COUNT) && (end_pixel_y < MATRIXLED_Y_COUNT) && (start_pixel_x <= end_pixel_x) && (start_pixel_y <= end_pixel_y)){
for(draw_y_loop = start_pixel_y;draw_y_loop <= end_pixel_y;draw_y_loop++){
if((draw_y_loop == start_pixel_y) || (draw_y_loop == end_pixel_y)){
send_command(draw_y_loop, draw_y_loop, start_pixel_x, end_pixel_x, panel_brightness);
digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_x_loop = start_pixel_x;draw_x_loop <= end_pixel_x;draw_x_loop++){
SPI.transfer(color_r);
SPI.transfer(color_g);
SPI.transfer(color_b);
}
digitalWrite(SS, HIGH);
}else{
draw_pixel(start_pixel_x, draw_y_loop, color_r, color_g, color_b, panel_brightness);
draw_pixel(end_pixel_x, draw_y_loop, color_r, color_g, color_b, panel_brightness);
}
}
}
}

/*
* 四角形(内埋め)を描画する関数
* 指定した左上ピクセル座標から右下ピクセル座標まで任意の色で四角形(内埋め)を描画する
* 引数1:四角形の左上X座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数2:四角形の左上Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数3:四角形の右下X座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数4:四角形の右下Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数5:赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数6:緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数7:青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数8:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
* 引数1〜引数4については以下の関係が成り立っている必要がある
* 四角形の左上X座標 <= 四角形の右下X座標
* 四角形の左上Y座標 <= 四角形の右下Y座標
*/
void draw_rectangle_fill(unsigned char start_pixel_x, unsigned char start_pixel_y, unsigned char end_pixel_x, unsigned char end_pixel_y, unsigned char color_r, unsigned char color_g, unsigned char color_b, unsigned char panel_brightness){
unsigned char draw_y_loop, draw_x_loop;

if((end_pixel_x < MATRIXLED_X_COUNT) && (end_pixel_y < MATRIXLED_Y_COUNT) && (start_pixel_x <= end_pixel_x) && (start_pixel_y <= end_pixel_y)){
if((start_pixel_x == 0) && (end_pixel_x == (MATRIXLED_X_COUNT - 1))){
send_command(start_pixel_y, end_pixel_y, start_pixel_x, end_pixel_x, panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_y_loop = start_pixel_y;draw_y_loop <= end_pixel_y;draw_y_loop++){
for(draw_x_loop = start_pixel_x;draw_x_loop <= end_pixel_x;draw_x_loop++){
SPI.transfer(color_r);
SPI.transfer(color_g);
SPI.transfer(color_b);
}
}
digitalWrite(SS, HIGH);
}else{
for(draw_y_loop = start_pixel_y;draw_y_loop <= end_pixel_y;draw_y_loop++){
send_command(draw_y_loop, draw_y_loop, start_pixel_x, end_pixel_x, panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_x_loop = start_pixel_x;draw_x_loop <= end_pixel_x;draw_x_loop++){
SPI.transfer(color_r);
SPI.transfer(color_g);
SPI.transfer(color_b);
}
digitalWrite(SS, HIGH);
}
}
}
}

/*
* 円を描画する関数
* 指定した中心ピクセル座標から指定した半径で任意の色で円を描画する
* 引数1:円の中心X座標 設定可能な数値の範囲:円の半径〜LEDパネル横解像度 - 円の半径 -1
* 引数2:円の中心Y座標 設定可能な数値の範囲:円の半径〜LEDパネル縦解像度 - 円の半径 -1
* 引数3:円の半径 設定可能な数値の範囲:1〜(LEDパネルの小さいほうの縦もしくは横の解像度 - 1) / 2 (小数点以下切り捨て)
* 引数4:赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数5:緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数6:青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数7:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
*/
void draw_circle(unsigned char center_pixel_x, unsigned char center_pixel_y, unsigned char radius, unsigned char color_r, unsigned char color_g, unsigned char color_b, unsigned char panel_brightness){
signed short x, y, r;

if(((center_pixel_x + radius) < MATRIXLED_X_COUNT) && ((center_pixel_y + radius) < MATRIXLED_X_COUNT) && (radius <= center_pixel_x) && (radius <= center_pixel_y)){
r = (signed short)radius;
x = (signed short)radius;
y = 0;
while (x >= y) {
draw_pixel(center_pixel_x + x, center_pixel_y + y, color_r, color_g, color_b, panel_brightness);
draw_pixel(center_pixel_x - x, center_pixel_y - y, color_r, color_g, color_b, panel_brightness);
draw_pixel(center_pixel_x - x, center_pixel_y + y, color_r, color_g, color_b, panel_brightness);
draw_pixel(center_pixel_x + x, center_pixel_y - y, color_r, color_g, color_b, panel_brightness);
draw_pixel(center_pixel_x + y, center_pixel_y + x, color_r, color_g, color_b, panel_brightness);
draw_pixel(center_pixel_x - y, center_pixel_y - x, color_r, color_g, color_b, panel_brightness);
draw_pixel(center_pixel_x - y, center_pixel_y + x, color_r, color_g, color_b, panel_brightness);
draw_pixel(center_pixel_x + y, center_pixel_y - x, color_r, color_g, color_b, panel_brightness);
if ((r -= (y++ << 1) + 1) <= 0) r += --x << 1;
}
}
}

/*
* 円(中埋め)を描画する関数
* 指定した中心ピクセル座標から指定した半径で任意の色で円(中埋め)を描画する
* 引数1:円の中心X座標 設定可能な数値の範囲:円の半径〜LEDパネル横解像度 - 円の半径 -1
* 引数2:円の中心Y座標 設定可能な数値の範囲:円の半径〜LEDパネル縦解像度 - 円の半径 -1
* 引数3:円の半径 設定可能な数値の範囲:1〜(LEDパネルの小さいほうの縦もしくは横の解像度 - 1) / 2 (小数点以下切り捨て)
* 引数4:赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数5:緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数6:青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数7:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
*/
void draw_circle_fill(unsigned char center_pixel_x, unsigned char center_pixel_y, unsigned char radius, unsigned char color_r, unsigned char color_g, unsigned char color_b, unsigned char panel_brightness){
signed short x, y, r;

if(((center_pixel_x + radius) < MATRIXLED_X_COUNT) && ((center_pixel_y + radius) < MATRIXLED_X_COUNT) && (radius <= center_pixel_x) && (radius <= center_pixel_y)){
r = (signed short)radius;
x = (signed short)radius;
y = 0;
while (x >= y) {
draw_line(center_pixel_x - x, center_pixel_y + y, center_pixel_x + x, center_pixel_y + y, color_r, color_g, color_b, panel_brightness);
draw_line(center_pixel_x - x, center_pixel_y - y, center_pixel_x + x, center_pixel_y - y, color_r, color_g, color_b, panel_brightness);
draw_line(center_pixel_x - y, center_pixel_y + x, center_pixel_x + y, center_pixel_y + x, color_r, color_g, color_b, panel_brightness);
draw_line(center_pixel_x - y, center_pixel_y - x, center_pixel_x + y, center_pixel_y - x, color_r, color_g, color_b, panel_brightness);
if ((r -= (y++ << 1) + 1) <= 0) r += --x << 1;
}
}
}

/*
* テキスト(背景透過)を描画する関数
* 指定した左上ピクセル座標から任意の色でテキスト(背景透過)を描画する
* 引数1:テキスト左上X座標 設定可能な数値の範囲:0〜LEDパネル横解像度 - (テキスト文字数 * 1文字当たりの横フォントサイズ) - 2
* 引数2:テキスト左上Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度 - 1文字当たりの縦フォントサイズ - 2
* 引数3:文字列が格納されている配列の先頭アドレス
* 引数4:フォント赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数5:フォント緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数6:フォント青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数7:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
* テキストは背景透過なため、表示する文字列が変更される場合以前の表示内容の上に重ねて表示される点に注意
*/
void draw_text(unsigned char start_pixel_x, unsigned char start_pixel_y, unsigned char* chara_array, unsigned char font_color_r, unsigned char font_color_g, unsigned char font_color_b, unsigned char panel_brightness){
unsigned char loop = 0, ascii, pixel_count_x = 0;
unsigned char pixel_data[MATRIXLED_X_COUNT + FONT_DATA_PIXEL_SIZE_X] = {};

unsigned char font_data_lead_loop;

unsigned char x_loop, y_loop;

if(start_pixel_x >= MATRIXLED_X_COUNT) return;
if((start_pixel_y + FONT_DATA_PIXEL_SIZE_Y) >= MATRIXLED_Y_COUNT) return;

while((chara_array[loop] |= '\0') && (pixel_count_x < MATRIXLED_X_COUNT))
{
pixel_count_x = loop * (FONT_DATA_PIXEL_SIZE_X + 1) + 1;
ascii = chara_array[loop] - 0x20;
for(font_data_lead_loop = 0;font_data_lead_loop <= FONT_DATA_PIXEL_SIZE_X;font_data_lead_loop++){
if(font_data_lead_loop < FONT_DATA_PIXEL_SIZE_X){
pixel_data[pixel_count_x + font_data_lead_loop] = pgm_read_byte(&Font_Data[ascii][font_data_lead_loop]);
}else{
pixel_data[pixel_count_x + font_data_lead_loop] = 0x00;
}
}
loop++;
}

pixel_count_x += FONT_DATA_PIXEL_SIZE_X;

if((start_pixel_x + pixel_count_x) >= MATRIXLED_X_COUNT) pixel_count_x = MATRIXLED_X_COUNT - start_pixel_x;

for(y_loop = 0;y_loop <= FONT_DATA_PIXEL_SIZE_Y;y_loop++){
for(x_loop = 0;x_loop < pixel_count_x;x_loop++){
if((((pixel_data[x_loop] >> (y_loop - 1)) & 0x01) == 0x01) && (y_loop != 0)){
draw_pixel((start_pixel_x + x_loop), (start_pixel_y + y_loop), font_color_r, font_color_g, font_color_b, panel_brightness);
}
}
digitalWrite(SS, HIGH);
}
}

/*
* テキスト(背景単色埋め)を描画する関数
* 指定した左上ピクセル座標から任意の色でテキスト(背景単色埋め)を描画する
* 引数1:テキスト左上X座標 設定可能な数値の範囲:0〜LEDパネル横解像度 - (テキスト文字数 * 1文字当たりの横フォントサイズ) - 2
* 引数2:テキスト左上Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度 - 1文字当たりの縦フォントサイズ - 2
* 引数3:文字列が格納されている配列の先頭アドレス
* 引数4:フォント赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数5:フォント緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数6:フォント青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数7:背景赤色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数8:背景緑色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数9:背景青色輝度値 設定可能な数値の範囲:0x00〜0xff
* 引数10:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
*/
void draw_text_fill_back(unsigned char start_pixel_x, unsigned char start_pixel_y, unsigned char* chara_array, unsigned char font_color_r, unsigned char font_color_g, unsigned char font_color_b, unsigned char back_color_r, unsigned char back_color_g, unsigned char back_color_b, unsigned char panel_brightness){
unsigned char loop = 0, ascii, pixel_count_x = 0;
unsigned char pixel_data[MATRIXLED_X_COUNT + FONT_DATA_PIXEL_SIZE_X] = {};

unsigned char font_data_lead_loop;

unsigned char x_loop, y_loop;

if(start_pixel_x >= MATRIXLED_X_COUNT) return;
if((start_pixel_y + FONT_DATA_PIXEL_SIZE_Y) >= MATRIXLED_Y_COUNT) return;

while((chara_array[loop] |= '\0') && (pixel_count_x < MATRIXLED_X_COUNT))
{
pixel_count_x = loop * (FONT_DATA_PIXEL_SIZE_X + 1) + 1;
ascii = chara_array[loop] - 0x20;
for(font_data_lead_loop = 0;font_data_lead_loop <= FONT_DATA_PIXEL_SIZE_X;font_data_lead_loop++){
if(font_data_lead_loop < FONT_DATA_PIXEL_SIZE_X){
pixel_data[pixel_count_x + font_data_lead_loop] = pgm_read_byte(&Font_Data[ascii][font_data_lead_loop]);
}else{
pixel_data[pixel_count_x + font_data_lead_loop] = 0x00;
}
}
loop++;
}

pixel_count_x += FONT_DATA_PIXEL_SIZE_X;

if((start_pixel_x + pixel_count_x) >= MATRIXLED_X_COUNT) pixel_count_x = MATRIXLED_X_COUNT - start_pixel_x - 1;

for(y_loop = 0;y_loop <= FONT_DATA_PIXEL_SIZE_Y;y_loop++){
send_command((start_pixel_y + y_loop), (start_pixel_y + y_loop), start_pixel_x, (start_pixel_x + pixel_count_x), panel_brightness);
digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(x_loop = 0;x_loop <= pixel_count_x;x_loop++){
if(y_loop == 0){
SPI.transfer(back_color_r);
SPI.transfer(back_color_g);
SPI.transfer(back_color_b);
}else if(((pixel_data[x_loop] >> (y_loop - 1)) & 0x01) == 0x01){
SPI.transfer(font_color_r);
SPI.transfer(font_color_g);
SPI.transfer(font_color_b);
}else{
SPI.transfer(back_color_r);
SPI.transfer(back_color_g);
SPI.transfer(back_color_b);
}
}
digitalWrite(SS, HIGH);
}
}

/*
* 画像を任意の場所に描画する関数
* 画像の左上のピクセル座標と画像の大きさを指定して任意の場所に画像を表示する
* 画像データは赤色緑色青色の8ビットデータが左上ピクセルから順番に格納された配列
* 引数1:画像の左上X座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数2:画像の左上Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数3:画像の横幅のピクセルサイズ 設定可能な数値の範囲:1〜LEDパネル横解像度 - 画像の左上X座標
* 引数4:画像の縦幅のピクセルサイズ 設定可能な数値の範囲:1〜LEDパネル縦解像度 - 画像の左上Y座標
* 引数5:画像データが入っている配列の先頭のアドレス
* 引数6:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
*/
void draw_image(unsigned char start_pixel_x, unsigned char start_pixel_y, unsigned char image_size_x, unsigned char image_size_y, unsigned char* draw_data, unsigned char panel_brightness){
unsigned char draw_y_loop, draw_x_loop;

if(((start_pixel_x + image_size_x) <= MATRIXLED_X_COUNT) && ((start_pixel_y + image_size_y) <= MATRIXLED_Y_COUNT)){
if((start_pixel_x == 0) && (image_size_x == MATRIXLED_X_COUNT)){
send_command(start_pixel_y, (start_pixel_y + image_size_y - 1), start_pixel_x, (image_size_x - 1), panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_y_loop = 0;draw_y_loop < image_size_y;draw_y_loop++){
for(draw_x_loop = 0;draw_x_loop < image_size_x;draw_x_loop++){
SPI.transfer(*draw_data++);
SPI.transfer(*draw_data++);
SPI.transfer(*draw_data++);
}
}
digitalWrite(SS, HIGH);
}else{
for(draw_y_loop = start_pixel_y;draw_y_loop < (start_pixel_y + image_size_y);draw_y_loop++){
send_command(draw_y_loop, draw_y_loop, start_pixel_x, (start_pixel_x + image_size_x - 1), panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_x_loop = start_pixel_x;draw_x_loop < (start_pixel_x + image_size_x);draw_x_loop++){
SPI.transfer(*draw_data++);
SPI.transfer(*draw_data++);
SPI.transfer(*draw_data++);
}
digitalWrite(SS, HIGH);
}
}
}
}

/*
* 24ビットビットマップ画像を任意の場所に描画する関数
* 画像の左上のピクセル座標と画像の大きさを指定して任意の場所に画像を表示する
* 画像データは24ビットビットマップ画像からヘッダ部分を削除し画像データ部分のみを取り出したもの
* 引数1:画像の左上X座標 設定可能な数値の範囲:0〜LEDパネル横解像度-1
* 引数2:画像の左上Y座標 設定可能な数値の範囲:0〜LEDパネル縦解像度-1
* 引数3:画像の横幅のピクセルサイズ 設定可能な数値の範囲:1〜LEDパネル横解像度 - 画像の左上X座標
* 引数4:画像の縦幅のピクセルサイズ 設定可能な数値の範囲:1〜LEDパネル縦解像度 - 画像の左上Y座標
* 引数5:画像データが入っている配列の先頭のアドレス
* 引数6:LEDパネル輝度 設定可能な数値の範囲:0x01〜0x0fの場合、その値をLEDパネルの輝度に反映 0x00の場合、LEDパネルコントローラのボリュームの値で輝度を設定
*/
void draw_image_for_24bitBMP(unsigned char start_pixel_x, unsigned char start_pixel_y, unsigned char image_size_x, unsigned char image_size_y, unsigned char* draw_data, unsigned char panel_brightness){
unsigned char draw_y_loop, draw_x_loop;

if(((start_pixel_x + image_size_x) <= MATRIXLED_X_COUNT) && ((start_pixel_y + image_size_y) <= MATRIXLED_Y_COUNT)){
if((start_pixel_x == 0) && (image_size_x == MATRIXLED_X_COUNT)){
send_command(start_pixel_y, (start_pixel_y + image_size_y - 1), start_pixel_x, (image_size_x - 1), panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_y_loop = 0;draw_y_loop < image_size_y;draw_y_loop++){
for(draw_x_loop = 0;draw_x_loop < image_size_x;draw_x_loop++){
SPI.transfer(pgm_read_byte(((image_size_y - draw_y_loop - 1) * image_size_x + draw_x_loop) * MATRIXLED_COLOR_COUNT + 2 + draw_data + ((4 - ((image_size_x * MATRIXLED_COLOR_COUNT) % 4)) % 4) * (image_size_y - draw_y_loop - 1)));
SPI.transfer(pgm_read_byte(((image_size_y - draw_y_loop - 1) * image_size_x + draw_x_loop) * MATRIXLED_COLOR_COUNT + 1 + draw_data + ((4 - ((image_size_x * MATRIXLED_COLOR_COUNT) % 4)) % 4) * (image_size_y - draw_y_loop - 1)));
SPI.transfer(pgm_read_byte(((image_size_y - draw_y_loop - 1) * image_size_x + draw_x_loop) * MATRIXLED_COLOR_COUNT + 0 + draw_data + ((4 - ((image_size_x * MATRIXLED_COLOR_COUNT) % 4)) % 4) * (image_size_y - draw_y_loop - 1)));
}
}
digitalWrite(SS, HIGH);
}else{
for(draw_y_loop = 0;draw_y_loop < image_size_y;draw_y_loop++){
send_command((draw_y_loop + start_pixel_y), (draw_y_loop + start_pixel_y), start_pixel_x, (start_pixel_x + image_size_x - 1), panel_brightness);

digitalWrite(RST, LOW);
delayMicroseconds(5);
digitalWrite(SS, LOW);
for(draw_x_loop = 0;draw_x_loop < image_size_x;draw_x_loop++){
SPI.transfer(pgm_read_byte(((image_size_y - draw_y_loop - 1) * image_size_x + draw_x_loop) * MATRIXLED_COLOR_COUNT + 2 + draw_data + ((4 - ((image_size_x * MATRIXLED_COLOR_COUNT) % 4)) % 4) * (image_size_y - draw_y_loop - 1)));
SPI.transfer(pgm_read_byte(((image_size_y - draw_y_loop - 1) * image_size_x + draw_x_loop) * MATRIXLED_COLOR_COUNT + 1 + draw_data + ((4 - ((image_size_x * MATRIXLED_COLOR_COUNT) % 4)) % 4) * (image_size_y - draw_y_loop - 1)));
SPI.transfer(pgm_read_byte(((image_size_y - draw_y_loop - 1) * image_size_x + draw_x_loop) * MATRIXLED_COLOR_COUNT + 0 + draw_data + ((4 - ((image_size_x * MATRIXLED_COLOR_COUNT) % 4)) % 4) * (image_size_y - draw_y_loop - 1)));
}
digitalWrite(SS, HIGH);
}
}
}
}


実際に動作させてみた動画がこちらになります。

本当はスクロールとか入れてもいいかなと思ったんですが、なんか文字を横スクロールするだけだとあんまり応用が効かないよなぁ・・・とか思っていろいろ考えた挙句面倒くさくなったのでやめました。

そのうちLEDパネルのケースをアクリルで作ったこととか、SDカードから動画と音声を読み込んで再生するやつとか記事にしたいのですが、マイコン動画プレイヤーは完全自分オリジナルプログラム(要するにウンコードってことです)なため、公開できるレベルにするのがちょっと無理かもしれませんね・・・
一応今のところは再生と停止と一時停止と動画ファイル選択はできているのですが、再生するときのフレームレートはプログラムに直接書き込んであって草という感じです。
何とかしないとなぁ・・・

wata_net at 00:22コメント(0) この記事をクリップ!

2018年05月04日

フルカラードットマトリクスLEDパネル用のSPIインターフェイスを作る

追記:プログラムを一新し、より高性能になりました。
こちらを御覧ください。
LEDパネルコントローラ完成
なお、この記事のプログラムは古い方になります。



最近AmazonでフルカラードットマトリクスLEDパネルがかなり安く売っているのを見つけました。
image17
横が64ピクセルで縦が32ピクセルなので合計で2048個ものRGBLEDが使用されていることになります。
それでいて3299円って極端に安いですよね。LED1個あたり1.61円です。
他にも探してみるといくつか見つかりました。

image18
これはLED1個0.81円

image19
こっちは1.76円

というわけで3mmピッチの64*32のパネルを禄に調べもせず2個衝動買いしてしまいました。
更に後から3mmピッチの64*64のパネルも購入しましたが、こちらはまだ届いていません。

これほど安くフルカラードットマトリクスLEDパネルが販売されているのは、おそらくデジタルサイネージの普及に伴ってLEDディスプレイ用として需要が拡大しているからなのでしょうね。

一応Amazonのリンクを貼っておきますが、今後値段が変更されたり取扱がなくなったりする可能性もあるのであしからず。
P3 RGBピクセルパネルHDビデオディスプレイ64×32ドットマトリックスSMD LEDディスプレイモジュール192×96mm
P3 SMD屋内rgb 32 S ledモジュールp3屋内フルカラーのための64 x64ピクセルledビデオ壁ledスクリーン電子スコアボード
P5 RGB ピクセルパネルHDビデオディスプレイ LEDディスプレイモジュール 320x160 1 / 16s 64×32

こちらが実際のフルカラードットマトリクスLEDパネルになります。
64*32のパネルを2枚連結しています。
DSCF1589
DSCF1588

さて、このフルカラードットマトリクスLEDパネルの表示方法ですが、信号規格はHUB75というものが使用されているようです。
HUB75については、ネットで調べれは他にわかりやすいサイトがいくつかあるのでここでの詳細な説明は省きますが、簡単に言うとパネルのLEDをダイナミック点灯させるための単純なシフトレジスタです。
そのためHUB75のパネルを表示させ続けるには常にデータを送り続ける必要があり、また基本的に各ドットのLEDのRGBはONもしくはOFFのみであるためマイコンで他の処理をこなしつつ階調表現まで行うのはかなり厄介です。

そこで、他のマイコンからSPI接続で簡単にフルカラードットマトリクスLEDパネルを表示できるようにするためのインターフェイスを作成しました。
使用したマイコンはSTM32F103C8T6です。このマイコンが載っているBluePillなどと呼ばれているボードがamazonでは500円程度で販売されており、入手しやすく安価であるためこれを選択しました。
DSCF1540

なにかの役に立つかもしれないので回路図と基板外形図も載せておきます。
bluepill_schbluepill_brd

目標仕様は、
SPI送信側は各ピクセルのRGB輝度情報を送信するだけで表示できるようにする。
対応解像度は64*32以上。できれば128*32。
フレームレートは60FPS以上。
色深度は4bit以上。
画面全体の輝度を可変できるようにする。
と定めました。
本当はSPI接続のフルカラー有機ELグラフィックディスプレイと同じような使用感を目指していたのですが、それをマイコンで実現するのはどうやら自分には無理だと感じたので早々に諦めました。
コマンドとデータに分けて送信したかったのですが、SPI受信側では実際に送信されるデータ数やタイミングを知る方法がわかりませんでした。(ポーリングや割り込みだとドットマトリクスパネルの表示に影響が出るためDMAを使うしか無い)そのため、単純にRGB4bitを垂れ流せばそのままパネルに表示される仕様としました。

まずは回路図です。
HUB75_sch
ご覧の通り、極めてシンプルです。ハードウェア的には何の面白みもない回路です。
R1でパネルの輝度を調整します。
SV2にフルカラードットマトリクスLEDパネルを接続します。
SV3にSPIを接続します。
MOSI:マスター出力スレーブ入力
SCK:シリアルクロック
NSS:スレーブセレクト
RST:マイコン内部のSPIのDMA転送をリセットする(このピンをトグルした後に1フレーム分のデータを転送します)
受信オンリースレーブなのでMISOはありません。

次にプログラムです。
自分のSTM32の開発環境は、STM32CubeMXとSW4STM32を使用しています。
開発の流れはまずSTM32CubeMXでマイコンのペリフェラルを設定したあとコード生成し、SW4STM32で生成されたプロジェクトを読み込みプログラムを書いてST-LINK/V2で書き込み兼デバッグを行っています。
以下のプログラムは自動生成された部分を省略して自分が書いたところを抜粋したものになります。
C言語独学マンが生成したウンコードなのでツッコミとかあればコメント欄にお願いします。

main.c
/* 略 ------------------------------------------------------------------------*/

/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdlib.h>
/* USER CODE END Includes */

/* 略 ------------------------------------------------------------------------*/

/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
#define MATRIXLED_R1(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, x))
#define MATRIXLED_G1(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, x))
#define MATRIXLED_B1(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, x))
#define MATRIXLED_R2(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, x))
#define MATRIXLED_G2(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, x))
#define MATRIXLED_B2(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, x))
#define MATRIXLED_ADDR_A(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, x))
#define MATRIXLED_ADDR_B(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, x))
#define MATRIXLED_ADDR_C(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, x))
#define MATRIXLED_ADDR_D(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, x))
#define MATRIXLED_CLOCK(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, x))
#define MATRIXLED_STROBE(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, x))
#define MATRIXLED_OUTENABLE(x) (HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, x))

#define ONBOARD_LED(x) (HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, x))

#define LED_RGB(x) {\\
GPIOB->BRR = (0x3f << 3);\\
GPIOB->BSRR = ((x & 0x3f) << 3);\\
}

#define LED_ADDR(x) {\\
GPIOB->BRR = (0x0f << 12);\\
GPIOB->BSRR = ((x & 0x0f) << 12);\\
}

#define LED_CLK_HIGH {\\
GPIOB->BSRR = (1 << 9);\\
}

#define LED_CLK_LOW {\\
GPIOB->BRR = (1 << 9);\\
}

#define LED_STB_HIGH {\\
GPIOB->BSRR = (1 << 10);\\
}

#define LED_STB_LOW {\\
GPIOB->BRR = (1 << 10);\\
}

#define LED_OE_HIGH {\\
GPIOB->BSRR = (1 << 11);\\
}

#define LED_OE_LOW {\\
GPIOB->BRR = (1 << 11);\\
}

#define ONBOARD_LED_TOGGLE {\\
GPIOC->ODR ^= (1 << 13);\\
}

#define HIGH 1
#define LOW 0

#define COLOR_R 0
#define COLOR_G 1
#define COLOR_B 2

#define R1_SET 0x0001
#define G1_SET 0x0002
#define B1_SET 0x0004
#define R2_SET 0x0008
#define G2_SET 0x0010
#define B2_SET 0x0020

#define MATRIXLED_X_COUNT 64
#define MATRIXLED_Y_COUNT 32
#define MATRIXLED_COLOR_COUNT 3

#define MATRIXLED_MAX_Y_ADDRES 16

#define MATRIXLED_PWM_RESOLUTION 16

#define ADC_DIVIDE_VALUE 445
#define TIM2_ARR_OFFSET_VALUE 5

unsigned char Flame_Buffer[MATRIXLED_Y_COUNT][MATRIXLED_X_COUNT][MATRIXLED_COLOR_COUNT];

volatile unsigned char TIM2_Updated_Flag, TIM3_Updated_Flag;

/* USER CODE END PV */

/* 略 ------------------------------------------------------------------------*/

/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
void fill_flame_buffer_color(unsigned char, unsigned char, unsigned char);
void fill_flame_buffer_random(void);
void fill_flame_buffer_testpattern(void);

/* USER CODE END PFP */

/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
* @brief The application entry point.
*
* @retval None
*/
int main(void)
{
/* USER CODE BEGIN 1 */
unsigned short matrixled_X_loop_count, matrixled_Y_loop_count;
unsigned char pwm_count;
unsigned short led_rgb_temp;

unsigned short ADC_value;

float display_brightness_temp;

/* USER CODE END 1 */

/* 略 ------------------------------------------------------------------------*/

/* USER CODE BEGIN 2 */
fill_flame_buffer_testpattern();

HAL_SPI_Receive_DMA(&hspi1, Flame_Buffer, (MATRIXLED_Y_COUNT * MATRIXLED_X_COUNT * MATRIXLED_COLOR_COUNT));
HAL_TIM_Base_Start_IT(&htim3);

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{

/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
HAL_ADC_Start_DMA(&hadc1, &ADC_value, 1);
display_brightness_temp = (float)ADC_value / ADC_DIVIDE_VALUE;
for(pwm_count = 1;pwm_count < MATRIXLED_PWM_RESOLUTION;pwm_count++){
for(matrixled_Y_loop_count = 0;matrixled_Y_loop_count < MATRIXLED_MAX_Y_ADDRES;matrixled_Y_loop_count++){

if(matrixled_Y_loop_count == 1) TIM2->ARR = (uint16_t)(pwm_count * (display_brightness_temp + 1) + TIM2_ARR_OFFSET_VALUE);
TIM2_Updated_Flag = RESET;
__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
__HAL_TIM_ENABLE(&htim2);
LED_OE_LOW;

for(matrixled_X_loop_count = 0;matrixled_X_loop_count < MATRIXLED_X_COUNT;matrixled_X_loop_count++){
led_rgb_temp = 0;

if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_R] >= pwm_count) led_rgb_temp |= R1_SET;
if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_G] >= pwm_count) led_rgb_temp |= G1_SET;
if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_B] >= pwm_count) led_rgb_temp |= B1_SET;

if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_R] >= pwm_count) led_rgb_temp |= R2_SET;
if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_G] >= pwm_count) led_rgb_temp |= G2_SET;
if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_B] >= pwm_count) led_rgb_temp |= B2_SET;

LED_RGB(led_rgb_temp);

LED_CLK_HIGH;
LED_CLK_LOW;
}
while(TIM2_Updated_Flag == RESET);

LED_ADDR(matrixled_Y_loop_count);
LED_STB_HIGH;
LED_STB_LOW;

//フレームバッファにSPIからDMA転送している最中に読みだそうとすると一瞬フリーズする問題
//TIM2の割り込みがたまに他の割り込みとバッティングする問題
}
}
while(TIM3_Updated_Flag == RESET);
TIM3_Updated_Flag = RESET;

ONBOARD_LED_TOGGLE;
}
/* USER CODE END 3 */

}

/* 略 ------------------------------------------------------------------------*/

/* USER CODE BEGIN 4 */
void fill_flame_buffer_color(unsigned char red, unsigned char green, unsigned char blue){
unsigned char red_temp, green_temp, blue_temp;
unsigned char fill_buffer_loop_x, fill_buffer_loop_y;

if(red < MATRIXLED_PWM_RESOLUTION) red_temp = red;
else red_temp = MATRIXLED_PWM_RESOLUTION - 1;

if(green < MATRIXLED_PWM_RESOLUTION) green_temp = green;
else green_temp = MATRIXLED_PWM_RESOLUTION - 1;

if(blue < MATRIXLED_PWM_RESOLUTION) blue_temp = blue;
else blue_temp = MATRIXLED_PWM_RESOLUTION - 1;

for(fill_buffer_loop_y = 0;fill_buffer_loop_y < MATRIXLED_Y_COUNT;fill_buffer_loop_y++){
for(fill_buffer_loop_x = 0;fill_buffer_loop_x < MATRIXLED_X_COUNT;fill_buffer_loop_x++){
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_R] = red_temp;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_G] = green_temp;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_B] = blue_temp;
}
}
}

void fill_flame_buffer_random(void){
unsigned char fill_buffer_loop_x, fill_buffer_loop_y;

srand(rand());

for(fill_buffer_loop_y = 0;fill_buffer_loop_y < MATRIXLED_Y_COUNT;fill_buffer_loop_y++){
for(fill_buffer_loop_x = 0;fill_buffer_loop_x < MATRIXLED_X_COUNT;fill_buffer_loop_x++){
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_R] = rand() % MATRIXLED_PWM_RESOLUTION;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_G] = rand() % MATRIXLED_PWM_RESOLUTION;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_B] = rand() % MATRIXLED_PWM_RESOLUTION;
}
}
}

void fill_flame_buffer_testpattern(void){
unsigned char fill_buffer_loop_x, fill_buffer_loop_y;

for(fill_buffer_loop_y = 0;fill_buffer_loop_y < MATRIXLED_Y_COUNT;fill_buffer_loop_y++){
for(fill_buffer_loop_x = 0;fill_buffer_loop_x < (MATRIXLED_X_COUNT / 12);fill_buffer_loop_x++){
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_R] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_G] = (MATRIXLED_PWM_RESOLUTION - 1);
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x][COLOR_B] = (MATRIXLED_PWM_RESOLUTION - 1);

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12)][COLOR_R] = (MATRIXLED_PWM_RESOLUTION - 1);
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12)][COLOR_G] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12)][COLOR_B] = (MATRIXLED_PWM_RESOLUTION - 1);

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 2][COLOR_R] = (MATRIXLED_PWM_RESOLUTION - 1);
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 2][COLOR_G] = (MATRIXLED_PWM_RESOLUTION - 1);
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 2][COLOR_B] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 3][COLOR_R] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 3][COLOR_G] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 3][COLOR_B] = (MATRIXLED_PWM_RESOLUTION - 1);

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 4][COLOR_R] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 4][COLOR_G] = (MATRIXLED_PWM_RESOLUTION - 1);
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 4][COLOR_B] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 5][COLOR_R] = (MATRIXLED_PWM_RESOLUTION - 1);
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 5][COLOR_G] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 5][COLOR_B] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 6][COLOR_R] = 0;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 6][COLOR_G] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 6][COLOR_B] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 7][COLOR_R] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 7][COLOR_G] = 0;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 7][COLOR_B] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 8][COLOR_R] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 8][COLOR_G] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 8][COLOR_B] = 0;

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 9][COLOR_R] = 0;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 9][COLOR_G] = 0;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 9][COLOR_B] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 10][COLOR_R] = 0;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 10][COLOR_G] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 10][COLOR_B] = 0;

Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 11][COLOR_R] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 11][COLOR_G] = 0;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 11][COLOR_B] = 0;

if(fill_buffer_loop_x < (MATRIXLED_X_COUNT % 12)){
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 12][COLOR_R] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 12][COLOR_G] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
Flame_Buffer[fill_buffer_loop_y][fill_buffer_loop_x + (MATRIXLED_X_COUNT / 12) * 12][COLOR_B] = fill_buffer_loop_y * (MATRIXLED_PWM_RESOLUTION - 1) / MATRIXLED_Y_COUNT;
}
}
}
}

/* USER CODE END 4 */

/* 略 ------------------------------------------------------------------------*/


stm32f1xx_it.c
/* 略 ------------------------------------------------------------------------*/

/* USER CODE BEGIN 0 */
#include <stdlib.h>

#define LED_OE_HIGH {\\
GPIOB->BSRR = (1 << 11);\\
}

#define LED_OE_LOW {\\
GPIOB->BRR = (1 << 11);\\
}

#define ONBOARD_LED_TOGGLE {\\
GPIOC->ODR ^= (1 << 13);\\
}

#define MATRIXLED_X_COUNT 64
#define MATRIXLED_Y_COUNT 32
#define MATRIXLED_COLOR_COUNT 3

extern unsigned char Flame_Buffer[MATRIXLED_Y_COUNT][MATRIXLED_X_COUNT][MATRIXLED_COLOR_COUNT];
extern unsigned char TIM2_Updated_Flag, TIM3_Updated_Flag;

/* USER CODE END 0 */

/* 略 ------------------------------------------------------------------------*/

/**
* @brief This function handles EXTI line0 interrupt.
*/
void EXTI0_IRQHandler(void)
{
/* USER CODE BEGIN EXTI0_IRQn 0 */
HAL_SPI_DMAStop(&hspi1);
HAL_SPI_Receive_DMA(&hspi1, Flame_Buffer, (MATRIXLED_Y_COUNT * MATRIXLED_X_COUNT * MATRIXLED_COLOR_COUNT));

/* USER CODE END EXTI0_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
/* USER CODE BEGIN EXTI0_IRQn 1 */

/* USER CODE END EXTI0_IRQn 1 */
}

/* 略 ------------------------------------------------------------------------*/

/**
* @brief This function handles TIM2 global interrupt.
*/
void TIM2_IRQHandler(void)
{
/* USER CODE BEGIN TIM2_IRQn 0 */
if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET){
LED_OE_HIGH;
TIM2_Updated_Flag = SET;
}

/* USER CODE END TIM2_IRQn 0 */
HAL_TIM_IRQHandler(&htim2);
/* USER CODE BEGIN TIM2_IRQn 1 */
if(TIM2_Updated_Flag == SET){
HAL_TIM_Base_Stop_IT(&htim2);
}

/* USER CODE END TIM2_IRQn 1 */
}

/**
* @brief This function handles TIM3 global interrupt.
*/
void TIM3_IRQHandler(void)
{
/* USER CODE BEGIN TIM3_IRQn 0 */
if(__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_UPDATE) != RESET){
TIM3_Updated_Flag = SET;
}

/* USER CODE END TIM3_IRQn 0 */
HAL_TIM_IRQHandler(&htim3);
/* USER CODE BEGIN TIM3_IRQn 1 */

/* USER CODE END TIM3_IRQn 1 */
}

/**
* @brief This function handles SPI1 global interrupt.
*/
void SPI1_IRQHandler(void)
{
/* USER CODE BEGIN SPI1_IRQn 0 */
//SPI通信にてエラーが発生した時にここに飛ぶためエラー処理を行う必要がある
ONBOARD_LED_TOGGLE;

/* USER CODE END SPI1_IRQn 0 */
HAL_SPI_IRQHandler(&hspi1);
/* USER CODE BEGIN SPI1_IRQn 1 */

/* USER CODE END SPI1_IRQn 1 */
}

/* 略 ------------------------------------------------------------------------*/


上から順番に抜粋して説明していきます。

main.c
#define MATRIXLED_X_COUNT 64
#define MATRIXLED_Y_COUNT 32
#define MATRIXLED_COLOR_COUNT 3

#define MATRIXLED_MAX_Y_ADDRES 16

#define MATRIXLED_PWM_RESOLUTION 16

#define ADC_DIVIDE_VALUE 445
#define TIM2_ARR_OFFSET_VALUE 5


stm32f1xx_it.c
#define MATRIXLED_X_COUNT 64
#define MATRIXLED_Y_COUNT 32
#define MATRIXLED_COLOR_COUNT 3

ここでマトリクスパネルの解像度を変更出来ます。解像度を変更する際はmain.cとstm32f1xx_it.cのどちらも書き換える必要があります。(プリプロセッサとかヘッダファイルがよく分かっていないのです。)
MATRIXLED_X_COUNT:横解像度 最大128
MATRIXLED_Y_COUNT:縦解像度 最大32
MATRIXLED_COLOR_COUNT:色数 RGBだから3色 ここは弄る必要はない
MATRIXLED_MAX_Y_ADDRES:マトリクスパネルのABCDのアドレス選択の最大値 例えばABCまでであれば8 ABCDEまであるHUB75Eはこのプログラムでは未対応
MATRIXLED_PWM_RESOLUTION:PWM分解能
ADC_DIVIDE_VALUE:輝度調整用の可変抵抗のADC値を割り算する値 値を小さくすると最大輝度がより明るくなるが、小さくしすぎるとLEDの輝度を決定しているTIM2の動作時間がフレームレートを決定しているTIM3の動作周期を超えてしまうためフレームレートが低下する
TIM2_ARR_OFFSET_VALUE:TIM2の最小値 あまり弄らないほうが良い

main.c
void fill_flame_buffer_color(unsigned char, unsigned char, unsigned char);
void fill_flame_buffer_random(void);
void fill_flame_buffer_testpattern(void);

fill_flame_buffer_colorはバッファを引数で渡されたRGB値で埋める関数です。
fill_flame_buffer_randomはバッファをランダム値で埋める関数です。呼び出すたびにランダム値は変化します。
fill_flame_buffer_testpatternはバッファをテストパターンで埋める関数です。コントラストの確認とかによく使います。

main.c
unsigned char Flame_Buffer[MATRIXLED_Y_COUNT][MATRIXLED_X_COUNT][MATRIXLED_COLOR_COUNT];

パネルのRGBピクセルに対応したバッファをRAMに確保しています。
SPIによって受信したピクセルデータはここに直接DMA転送されます。
例えば、MATRIXLED_PWM_RESOLUTIONが16だった場合に一番左上のピクセルを白色に表示させる場合は
Flame_Buffer[0][0][0] = 0x0f;
Flame_Buffer[0][0][1] = 0x0f;
Flame_Buffer[0][0][2] = 0x0f;

とすれば良いことになります。

main.c
if(matrixled_Y_loop_count == 1) TIM2->ARR = (uint16_t)(pwm_count * (display_brightness_temp + 1) + TIM2_ARR_OFFSET_VALUE);

TIM2はLEDのON時間を決定しており、PWM段階に応じてTIM2の自動再ロードレジスタの値を変えることによってLEDのON時間を可変しています。
なめらかな輝度階調表現を得るには必須です。詳細は後述します。

main.c
for(matrixled_X_loop_count = 0;matrixled_X_loop_count < MATRIXLED_X_COUNT;matrixled_X_loop_count++){
led_rgb_temp = 0;

if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_R] >= pwm_count) led_rgb_temp |= R1_SET;
if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_G] >= pwm_count) led_rgb_temp |= G1_SET;
if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_B] >= pwm_count) led_rgb_temp |= B1_SET;

if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_R] >= pwm_count) led_rgb_temp |= R2_SET;
if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_G] >= pwm_count) led_rgb_temp |= G2_SET;
if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_B] >= pwm_count) led_rgb_temp |= B2_SET;

LED_RGB(led_rgb_temp);

LED_CLK_HIGH;
LED_CLK_LOW;
}

バッファからマトリクスパネルに出力するデータに変換し出力しているところのループです。
まぁ、見たまんまですね。あんまり頭がいい方法とは思えず、なにかもっと高速でいいやり方があるに違いないと思っているのですが、RGBデータから変換しなければならないという縛りがあるためこうせざるを得ない状況です。
RAMがたっぷりあれば、予めRGBデータからGPIOにそのまま出力できるデータに変換してダブルバッファな感じでDMA転送を行えばかなり高速化できると思うのですが、あいにくSTM32F103C8T6はRAMが20KBしか無いため無理でした。
また、フレキシブルメモリコントローラがあればこれを利用できたかもしれませんが、STM32F103C8T6にはありません。
今の所、ここが一番処理に時間がかかってフレームレート向上のネックになっているところです。
次にPWMのループかなと思います。

main.c
//フレームバッファにSPIからDMA転送している最中に読みだそうとすると一瞬フリーズする問題
//TIM2の割り込みがたまに他の割り込みとバッティングする問題

これなんですが、上のコメントはおそらくダブルバッファを採用すれば解決できる問題です。しかしRAM容量が足りないので今の所放置です。
下のコメントは、TIM2の割り込み優先度を最高にすることで一応は解決したと考えています。

stm32f1xx_it.c
/**
* @brief This function handles EXTI line0 interrupt.
*/
void EXTI0_IRQHandler(void)
{
/* USER CODE BEGIN EXTI0_IRQn 0 */
HAL_SPI_DMAStop(&hspi1);
HAL_SPI_Receive_DMA(&hspi1, Flame_Buffer, (MATRIXLED_Y_COUNT * MATRIXLED_X_COUNT * MATRIXLED_COLOR_COUNT));

/* USER CODE END EXTI0_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
/* USER CODE BEGIN EXTI0_IRQn 1 */

/* USER CODE END EXTI0_IRQn 1 */
}

これは外部割り込みが発生した時に呼び出される割り込みハンドラです。
で、どこのピンの外部割り込みなのかというと、上の回路図のSV3のコネクタのRSTピンです。
SPIマスタ側のマイコンからRGBデータが送られてくるわけですが、スレーブ側のマイコンはどれがデータの頭なのかデータは何個なのか知る方法が分からなかったので、データ送信開始前にSPIのDMA転送を停止させ、送信するデータは1フレーム分まとめて送信されるという前提でSPIのDMA転送を開始させています。


HUB75での輝度階調表現について

HUB75は通常では各ピクセルはONもしくはOFFしか出来ず、これを輝度階調表現させるには、LEDのON時間を変化させながら同じフレームを何回も表示させなければいけません。
しかし、単純にピクセルの輝度情報に比例してLEDのON時間を変化させただけでは全く滑らかに輝度が変化しているようには見えないのです。
こんな感じになります。
DSCF1604
暗いところから急に明るくなって、以降はほとんど明るさに変化が無いように見えますね。
なぜならば人間の目の輝度に対する感度は対数的に変化するからです。
それはどういうことかと言うと、輝度が低い時は僅かに輝度が上がっただけでもかなり明るくなったように感じるが、輝度が高い時は輝度が更に上がってもほとんど変化していないように見えるのです。その特性を相殺して明るさがなめらかに変化しているように感じさせるためには、輝度を指数的に変化させなければなりません。
しかし、マイコンに指数計算をやらせるととっても時間がかかって厳しいので今回は二次関数的に変化させることとしました。
二次関数的に変化させたいということは、
ON時間=PWM値^2
という式が成り立つようにしたいということです。
しかし、今回のプログラムではON時間がどんどん累積するので実際のところは
実ON時間=PWM現在値^2+前回ON時間
となります。
ここで前回ON時間は
前回ON時間=PWM前回値^2
PWM前回値=PWM現在値-1
よって
前回ON時間=(PWM現在値-1)^2
つまりON時間は
ON時間=PWM現在値^2+前回ON時間-(PWM現在値-1)^2
として計算すればいいことになります。式をいい感じにすると
ON時間-前回ON時間=2*PWM現在値-1
となります。つまりTIM2の自動再ロードレジスタARRの値は2*PWM現在値-1にすればいいことが分かりますが、この値が小さくなりすぎるとTIM2の割り込み周期が短くなりすぎてまともに動作しないので、定数項-1を内包した形で
TIM2->ARR = 2*PWM現在値+TIM2ARR最小値
としておきます。
また、今回は可変抵抗によってパネルの輝度を変化させたいのでPWM現在値の係数2を内包させた形で輝度係数を与えます。
すると
TIM2->ARR = PWM現在値*輝度係数+TIM2ARR最小値
となります。PWM現在値*輝度係数の項が0になるとPWM現在値にかかわらずTIM2の自動再ロードレジスタARRの値がTIM2ARR最小値で一定になってしまうので
TIM2->ARR = PWM現在値*(輝度係数+1)+TIM2ARR最小値
とします。そうすればプログラム中の
TIM2->ARR = (uint16_t)(pwm_count * (display_brightness_temp + 1) + TIM2_ARR_OFFSET_VALUE);
と同じ形になりました。
実際のところ、こうしてTIM2のON時間を計算してやることによって階調表現の滑らかさがかなり改善します。

追記:2乗ではなく2.2乗で補正すべきです。というのも、Windows標準のガンマ補正値が2.2なためです。
DSCF1601
前の画像と見比べてみると一目瞭然です。
パネルに画像を表示させる場合、コントラストがかなり変化します。

ON時間を1次関数的に変化させた場合
DSCF1567

ON時間を2次関数的に変化させた場合
DSCF1582
本来指数関数的に変化させるべきところを2次関数で代用してるのでまだだいぶ足りない感はありますが、かなりコントラストが改善していることが分かります。


さて、このプログラムで解像度64*32までであれば60FPSで問題なく動作します。
しかし64*32のパネルを2枚直列に接続し解像度を128*32にすると、フレームレートが42FPSくらいまで低下し肉眼でもチラツキが目立つようになりかなり厳しいものがあります。
ここで、MATRIXLED_PWM_RESOLUTIONを8にすると色深度が犠牲になりますが60FPS出るようになります。

色深度4ビットのときの表示がこれです。
DSCF1582

対して色深度3ビットの時の表示がこれです。
DSCF1608
髪のあたりを比較するとわかりやすいですが、色数が少なくなり、色の再現性が悪化しています。

つまり、
解像度を犠牲にして色深度とフレームレートを取るか
フレームレートを犠牲にして解像度と色深度を取るか
色深度を犠牲にして解像度とフレームレートを取るか
の3パターンのトレードオフになるわけです。できればどれもあまり犠牲にはしたくないものです。

そこで、苦肉の策でフレームレートをすこし犠牲にする方向でできるだけちらつきが目立たないようにするため、インターレース方式を採用してみることにしました。
インターレースについては詳しく解説はしませんが、水平走査を1本飛ばしで交互に表示させることによりフレームレートの2倍の周波数でフィールドを更新するため同じフレームレートでもちらつきが目立ちにくくなるというものです。

プログラムは変更した箇所だけ抜粋します。

main.c
  /* USER CODE BEGIN WHILE */
while (1)
{

/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
HAL_ADC_Start_DMA(&hadc1, &ADC_value, 1);
display_brightness_temp = (float)ADC_value / ADC_DIVIDE_VALUE;
for(pwm_count = 1;pwm_count < MATRIXLED_PWM_RESOLUTION;pwm_count++){
for(matrixled_Y_loop_count = interlace_count;matrixled_Y_loop_count < MATRIXLED_MAX_Y_ADDRES;matrixled_Y_loop_count += 2){

if((matrixled_Y_loop_count == 2) || (matrixled_Y_loop_count == 3)) TIM2->ARR = (uint16_t)(pwm_count * (display_brightness_temp + 1) + TIM2_ARR_OFFSET_VALUE);
TIM2_Updated_Flag = RESET;
__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_UPDATE);
__HAL_TIM_ENABLE(&htim2);
LED_OE_LOW;

for(matrixled_X_loop_count = 0;matrixled_X_loop_count < MATRIXLED_X_COUNT;matrixled_X_loop_count++){
led_rgb_temp = 0;

if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_R] >= pwm_count) led_rgb_temp |= R1_SET;
if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_G] >= pwm_count) led_rgb_temp |= G1_SET;
if(Flame_Buffer[matrixled_Y_loop_count][matrixled_X_loop_count][COLOR_B] >= pwm_count) led_rgb_temp |= B1_SET;

if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_R] >= pwm_count) led_rgb_temp |= R2_SET;
if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_G] >= pwm_count) led_rgb_temp |= G2_SET;
if(Flame_Buffer[matrixled_Y_loop_count + MATRIXLED_MAX_Y_ADDRES][matrixled_X_loop_count][COLOR_B] >= pwm_count) led_rgb_temp |= B2_SET;

LED_RGB(led_rgb_temp);

LED_CLK_HIGH;
LED_CLK_LOW;
}
while(TIM2_Updated_Flag == RESET);

LED_ADDR(matrixled_Y_loop_count);
LED_STB_HIGH;
LED_STB_LOW;

//フレームバッファにSPIからDMA転送している最中に読みだそうとすると一瞬フリーズする問題
//TIM2の割り込みがたまに他の割り込みとバッティングする問題
}
}
while(TIM3_Updated_Flag == RESET);
TIM3_Updated_Flag = RESET;

ONBOARD_LED_TOGGLE;

if(interlace_count == 0) interlace_count++;
else interlace_count = 0;
}
/* USER CODE END 3 */

結果は比較的良好で、肉眼ではそれほどちらつきは気にならなくなりました。

以上でフルカラードットマトリクスLEDパネル用のSPIインターフェイスの作成は一区切りとすることにしました。
128*32解像度ではプログレッシブ表示で60FPS出せなかったのがかなり心残りですが、どうやら今の自分のレベルでは無理なようです。
最後に、今回作成したプログラムのプロジェクトファイルを丸揚げしておきます。
ダウンロードして解凍し、STM32CubeMXやSW4STM32などにインポートします。
あわよくば誰かが完璧に改良してくれることを狙っています。
HUB75フルカラードットマトリクスLEDパネル用SPIインターフェイスプログレッシブ版プロジェクトファイル
HUB75フルカラードットマトリクスLEDパネル用SPIインターフェイスインターレース版プロジェクトファイル

DSCF1619
DSCF1578
DSCF1580
DSCF1581

wata_net at 06:48コメント(0) この記事をクリップ!

2018年05月03日

ブログのスタイルを少しだけ変更

このブログを初めたのが2008/02/09、今から10年前のことになります。
その当時自分が使ってたPCは確か富士通のDESKPOWER CシリーズのC3/55Lの中古だったと思います。
C3/55Lは発売が2000年春ということで2008年当時としても骨董PCではありましたが、256MBのPC100メモリを2枚載せたりCPUをPentum3の1GHzに交換したりして使ってたような記憶があります。
で、当時のディスプレイ事情ですが、C3/55Lは15型の1024*768の液晶ディスプレイがくっついているので当然それを使っていました。
他にもブラウン管のCRTモニタがまだ現役で使われていたと思います。
そんな時期に開設されたこのブログは記事の横幅が525pxとなっていました。おそらく2008年当時ならばこれが普通だったのではと思います。
しかし現在はというとモニタは1920*1080のFullHDは当たり前、最近4K解像度もだいぶ普及しつつあります。
そんなご時世に横幅525pxはさすがに窮屈なのではないかと以前から思っておりました。
そこで全く分からないCSSを勘でいじって横幅を広くしてみました。
若干いい感じになったかなと思ったのですが、そうするとデフォルトのフォントが小さすぎる気がします。
そもそもデザインのテンプレートみたいなのを使って今どきな感じに一新すべきかなと思ったりします。
正直今まで生きてきてこういうWebサイトの事について一切関心も興味も持たなかったのでどうすれば良いのかよくわからないです。
まぁ気が向いたらなんとかしましょうかね〜〜

wata_net at 02:16コメント(0) この記事をクリップ!
プロフィール

wata

Recent Comments
記事検索
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

QRコード
QRコード
  • ライブドアブログ