HSPI と VSPI を同時使用および複数デバイス制御
では、ちょっとオモシロイことをやってみます。
VSPI と HSPIをうまく使って、同時に使用することができるんです。
すると、こんな芸当もできてしまいます。
例えば、こんな感じのスケッチです。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
#include <SPI.h> SPIClass hspi(HSPI); byte b=0b10100110; void setup() { SPI.begin(); SPI.setFrequency(7000000); SPI.setDataMode(SPI_MODE3); SPI.setHwCs(true); hspi.begin(14, 12, 13, 15); hspi.setFrequency(5000000); hspi.setDataMode(SPI_MODE2); hspi.setHwCs(true); } void loop() { SPI.write(b); hspi.write(b); }
どうですか?
VSPI と HSPI を同時に載せて、それぞれ異なった SPI クロックで走らせて、しかも異なる SPI_MODE にしているのです。
VSPI の場合は begin 関数内でピン設定をしなくてもOKですが、HSPI の場合はピン設定しなければなりません。
VSPI をロジアナ ( LAP-C )で拡大してみると、
プログラム通り、約7MHz の 150ns になっていますね。
そして、SPI_MODE3 になっています。
では、HSPI側を見てみます。
しっかり5 MHz 出ていますね。
そして、SPI_MODE 2 になっています。
もっと大きく見てみるとこんな感じです。
いかがでしょうか。
VSPI とHSPI が共存できて、しかも、異なるSPI周波数とSPI_MODEが共存できていますね。
これができれば、複数の異なる周波数の SPI デバイスを制御できます。
ESP8266 の以前の記事では、micro SDHC カードと OLED ディスプレイで異なる周波数や SPI_MODE 設定をやったことがありました。
でも、ESP32 では、 VSPI と HSPI をうまく使うとプログラムをかなり節約できて有難いですね。
SPI 通信の高速化
上記の様に SPI.write 関数で1バイト毎送信すると、次のバイト送信まで間隔が空き過ぎていることが分かると思います。
実はそれでは処理が遅くなってしまうんですね。
では、もっと間隔を詰めて高速化するにはどうしたらよいのでしょうか?
以下のようなスケッチにしてみてください。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
#include <SPI.h> SPIClass hspi(HSPI); byte b=0b10100110; byte b_array[8]; void setup() { hspi.begin(14, 12, 13, 15); hspi.setFrequency(7000000); hspi.setDataMode(SPI_MODE3); hspi.setHwCs(true); for(int i=0; i<8; i++){ b_array[i] = b; } } void loop() { hspi.writeBytes(b_array, 8); }
write関数の代わりに writeBytes 関数を使ってみました。
この場合は8バイトまとめて送信することになります。
では、ロジックアナライザー( LAP-C )で波形を見てみましょう。
いかがでしょうか。
write関数で1バイト毎に送信していた時の余分な間隔が無くなり、8バイトを連続して送っていることが分かると思います。
これを使えば、write関数を使うよりも数十倍高速化することができます。
では、今までやってきたことを応用して、 ESP32 – DevKitC と OLED SSD1331 と micro SDHC カードをそれぞれ以下のように接続してみます。
micro SDHC カードスロットが VSPI 接続で、 OLED SSD1331 が HSPI 接続です。
写真ではこうなります。
以前の記事でも述べましたが、micro SDHC カードの MOSI と MISO には必ず 10kΩ程度の抵抗でプルアップしてください。
ただ、SSD1331 については特にプルアップしなくても良いと思われます。
もし、不具合が出るようなら、プルアップしてみてください。
そして、Arduino core for the ESP32 のSD.h ライブラリでは、デフォルトで VSPI 使用となっているので、micro SDHC は必然と VSPI で使うことになります。
では、前回の記事で作った自作フォントを OLED ディスプレイに表示させてスクロールしてみたいと思います。
1文字の半分 8×16 ドット(ピクセル)のフォントを8ドット分スクロールしたら次の半分を micro SDHC カードから読み込むという方式です。
文字が0~9と少ないので、一気にグローバル変数に取り込んだ方が速いのですが、今回は SPI 信号の高速化を測る目的なので、毎回 micro SDHC カードにアクセスするという方式を取ります。
そして、前回の記事で用いた、SSD1331 のグラフィックアクセラレーションコマンドは高速過ぎるので一切使いません。
今回は手動でドットを1つずつ移動させるプログラムにしました。
では、以下のスケッチを入力してみてください。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
#include <SD.h> #include <SPI.h> SPIClass hspi(HSPI); const char* MyFont_file = "/font/MyFont.fnt"; //自作フォントファイル名を定義 const uint8_t CS_SD = 5; //SD card CS ( Chip Select ) const uint8_t SCLK_OLED = 14; //SCLK const uint8_t MOSI_OLED = 13; //MOSI (Master Output Slave Input) const uint8_t MISO_OLED = 12; //これは実際は使っていない。MISO (Master Input Slave Output) const uint8_t CS_OLED = 15; const uint8_t DC_OLED = 16; //OLED DC(Data/Command) const uint8_t RST_OLED = 4; //OLED Reset enum { MaxDispByte = 12 }; uint8_t MyFont_buf[2][16]; uint8_t Next_buf[MaxDispByte][16]; uint8_t Disp_Out_buf[MaxDispByte][16]; uint8_t dummy_buf[16]; uint8_t num_cnt = 0; uint32_t LastTime = 0; uint8_t scl_cnt = 0; uint16_t sj_cnt = 0; File MyF; //**********セットアップ******************************** void setup() { delay(1000); //ESP32が起動するまで待つ Serial.begin(115200); SD.begin(CS_SD, SPI, 24000000, "/sd"); MyF = SD.open(MyFont_file, FILE_READ); if (!MyF) { Serial.print(MyFont_file); Serial.println(" File not found"); return; }else{ Serial.print(MyFont_file); Serial.println(" File read OK!"); } SSD1331_Init( SCLK_OLED, MISO_OLED, MOSI_OLED, CS_OLED, DC_OLED, RST_OLED ); LastTime = millis(); } //****************メインループ************************** void loop() { byte Red = 7, Green = 0, Blue = 1; if( millis() - LastTime > 0){ //ここでスクロールスピード調整する。ゼロは最速 if( scl_cnt == 8 && sj_cnt == 1){ MyFont_SD_Read( MyF, 2, num_cnt, MyFont_buf); num_cnt++; if( num_cnt == 10 ){ num_cnt = 0; } } Scroll( MaxDispByte, Next_buf, Disp_Out_buf, dummy_buf, MyFont_buf, 2, &scl_cnt, &sj_cnt); SSD1331_8x16_Font_DisplayOut( MaxDispByte, 0, 95, 0, 15, Red, Green, Blue, Disp_Out_buf); LastTime = millis(); } } //*********文字スクロール関数 ******************** void Scroll(uint8_t disp_char_max, uint8_t next_buff1[][16], uint8_t scl_buff1[][16], uint8_t Orign_buff1[16], uint8_t font_buf1[2][16], uint16_t Length, uint8_t* scl_cnt1, uint16_t* sj_cnt1){ int8_t i, j; if(*sj_cnt1 == 0 && *scl_cnt1 == 0){ for(i=0; i<16; i++) Orign_buff1[i] = font_buf1[0][i]; } if(*scl_cnt1 == 8){ *scl_cnt1 = 0; (*sj_cnt1)++; if(*sj_cnt1 >= Length){ *sj_cnt1 = 0; } for(i=0; i<16; i++) Orign_buff1[i] = font_buf1[*sj_cnt1][i]; } for(i=15; i>=0; i--){ //まず、一番左側文字をビットシフト next_buff1[disp_char_max-1][i] = ( next_buff1[disp_char_max-1][i] | ( scl_buff1[disp_char_max-1][i] & B10000000 )); scl_buff1[disp_char_max-1][i] = scl_buff1[disp_char_max-1][i]<<1; scl_buff1[disp_char_max-1][i] = ( scl_buff1[disp_char_max-1][i] | (( Orign_buff1[i] & B10000000 )>>7)); Orign_buff1[i] = Orign_buff1[i]<<1; } for(i=disp_char_max-2; i>=0; i--){ //次にその他文字をビットシフト for(j=15; j>=0; j--){ next_buff1[i][j] = ( next_buff1[i][j] | ( scl_buff1[i][j] & B10000000 )); scl_buff1[i][j] = scl_buff1[i][j]<<1; scl_buff1[i][j] = ( scl_buff1[i][j] | (( next_buff1[i+1][j] & B10000000 )>>7)); next_buff1[i+1][j] = next_buff1[i+1][j]<<1; } } (*scl_cnt1)++; } //*********文字ディスプレイ表示**************************** void SSD1331_8x16_Font_DisplayOut(uint8_t txtMax, uint8_t x1, uint8_t x2, uint8_t y1, uint8_t y2, uint8_t red, uint8_t green, uint8_t blue, uint8_t Fnt[][16]){ CommandWrite(0x15); //Set Column Address CommandWrite(x1); CommandWrite(x2); CommandWrite(0x75); //Set Row Address CommandWrite(y1); CommandWrite(y2); int i, j, k; uint8_t bt = 0b10000000; uint16_t cnt = 0; uint8_t Dot = (red << 5) | (green << 2) | blue; for(i=0; i<16; i++){ for(j=0; j<txtMax; j++){ for(k=0; k<8; k++){ if(k==0){ bt = 0b10000000; }else{ bt = bt >> 1; } if((Fnt[j][i] & bt)>0){ DataWrite( Dot ); }else{ DataWrite( 0 ); } cnt++; } } } } //************ SSD1331 初期化 **************************************** void SSD1331_Init(uint8_t sck, uint8_t miso, uint8_t mosi, uint8_t cs, uint8_t dc, uint8_t rst){ pinMode(rst, OUTPUT); //RES pinMode(dc, OUTPUT); //DC digitalWrite(rst, HIGH); digitalWrite(rst, LOW); delay(1); digitalWrite(rst, HIGH); digitalWrite(dc, HIGH); hspi.begin(sck, miso, mosi, cs); hspi.setFrequency(7000000); //SSD1331 のSPI Clock Cycle Time 最低150ns hspi.setDataMode(SPI_MODE3); hspi.setHwCs(true); CommandWrite(0xAE); //Set Display Off CommandWrite(0xA0); //Remap & Color Depth setting CommandWrite(0b00110010); //A[7:6] = 00; 256 color. A[7:6] = 01; 65k color format CommandWrite(0xA1); //Set Display Start Line CommandWrite(0); CommandWrite(0xA2); //Set Display Offset CommandWrite(0); CommandWrite(0xA4); //Set Display Mode (Normal) CommandWrite(0xA8); //Set Multiplex Ratio CommandWrite(0b00111111); //15-63 CommandWrite(0xAD); //Set Master Configration CommandWrite(0b10001110); //a[0]=0 Select external Vcc supply, a[0]=1 Reserved(reset) CommandWrite(0xB0); //Power Save Mode CommandWrite(0b00000000); //0x1A Enable power save mode CommandWrite(0xB1); //Phase 1 and 2 period adjustment CommandWrite(0x74); CommandWrite(0xB3); //Display Clock DIV CommandWrite(0xF0); CommandWrite(0x8A); //Pre Charge A CommandWrite(0x81); CommandWrite(0x8B); //Pre Charge B CommandWrite(0x82); CommandWrite(0x8C); //Pre Charge C CommandWrite(0x83); CommandWrite(0xBB); //Set Pre-charge level CommandWrite(0x3A); CommandWrite(0xBE); //Set VcomH CommandWrite(0x3E); CommandWrite(0x87); //Set Master Current Control CommandWrite(0x06); CommandWrite(0x15); //Set Column Address CommandWrite(0); CommandWrite(95); CommandWrite(0x75); //Set Row Address CommandWrite(0); CommandWrite(63); CommandWrite(0x81); //Set Contrast for Color A CommandWrite(255); CommandWrite(0x82); //Set Contrast for Color B CommandWrite(255); CommandWrite(0x83); //Set Contrast for Color C CommandWrite(255); for(int j=0; j<64; j++){ //Display Black OUT for(int i=0; i<96; i++){ DataWrite(0x00); DataWrite(0x00); DataWrite(0x00); } } CommandWrite(0xAF); //Set Display On delay(110); //ディスプレイONコマンドの後は最低100ms 必要 } //*********** SPI 通信送信 *********************************** void CommandWrite(uint8_t b){ digitalWrite(DC_OLED, LOW);//DC hspi.write(b); } void DataWrite(uint8_t b){ digitalWrite(DC_OLED, HIGH);//DC hspi.write(b); } void CommandWriteBytes(uint8_t *b, uint16_t n){ digitalWrite(DC_OLED, LOW);//DC hspi.writeBytes(b, n); } void DataWriteBytes(uint8_t *b, uint16_t n){ digitalWrite(DC_OLED, HIGH);//DC hspi.writeBytes(b, n); } //*********** 自作フォントをSDカードから読み込む **************** void MyFont_SD_Read(File F, uint8_t ZorH, uint8_t num, uint8_t buf[2][16]){ F.seek(num * (16 * ZorH)); F.read(buf[0], 16); F.read(buf[1], 16); }
これは、write 関数で1バイト毎送信させたものです。
ではこれをコンパイル書き込みして、実行させてみてください。
次に、writeBytes 関数でバイトをまとめて送信した以下のプログラムに変えてみて下さい。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
#include <SD.h> #include <SPI.h> SPIClass hspi(HSPI); const char* MyFont_file = "/font/MyFont.fnt"; //自作フォントファイル名を定義 const uint8_t CS_SD = 5; //SD card CS ( Chip Select ) const uint8_t SCLK_OLED = 14; //SCLK const uint8_t MOSI_OLED = 13; //MOSI (Master Output Slave Input) const uint8_t MISO_OLED = 12; //これは実際は使っていない。MISO (Master Input Slave Output) const uint8_t CS_OLED = 15; const uint8_t DC_OLED = 16; //OLED DC(Data/Command) const uint8_t RST_OLED = 4; //OLED Reset enum { MaxDispByte = 12 }; uint8_t MyFont_buf[2][16]; uint8_t Next_buf[12][16]; uint8_t Disp_Out_buf[12][16]; uint8_t dummy_buf[16]; uint8_t num_cnt = 0; uint32_t LastTime = 0; uint8_t scl_cnt = 0; uint16_t sj_cnt = 0; File MyF; //**********セットアップ******************************** void setup() { delay(1000); //ESP32が起動するまで待つ Serial.begin(115200); SD.begin(CS_SD, SPI, 24000000, "/sd"); MyF = SD.open(MyFont_file, FILE_READ); if (!MyF) { Serial.print(MyFont_file); Serial.println(" File not found"); return; }else{ Serial.print(MyFont_file); Serial.println(" File read OK!"); } SSD1331_Init(SCLK_OLED, MISO_OLED, MOSI_OLED, CS_OLED, DC_OLED, RST_OLED); LastTime = millis(); } //****************メインループ************************** void loop() { byte Red = 7, Green = 0, Blue = 1; if( millis() - LastTime > 0){ //ここでスクロールスピード調整する。ゼロは最速 if( scl_cnt == 8 && sj_cnt == 1){ MyFont_SD_Read( MyF, 2, num_cnt, MyFont_buf); num_cnt++; if( num_cnt == 10 ){ num_cnt = 0; } } Scroll( MaxDispByte, Next_buf, Disp_Out_buf, dummy_buf, MyFont_buf, 2, &scl_cnt, &sj_cnt); SSD1331_8x16_Font_DisplayOut(12, 0, 95, 0, 15, Red, Green, Blue, Disp_Out_buf); LastTime = millis(); } } //*********文字スクロール関数 ******************** void Scroll(uint8_t disp_char_max, uint8_t next_buff1[][16], uint8_t scl_buff1[][16], uint8_t Orign_buff1[16], uint8_t font_buf1[2][16], uint16_t Length, uint8_t* scl_cnt1, uint16_t* sj_cnt1){ int8_t i, j; if(*sj_cnt1 == 0 && *scl_cnt1 == 0){ for(i=0; i<16; i++) Orign_buff1[i] = font_buf1[0][i]; } if(*scl_cnt1 == 8){ *scl_cnt1 = 0; (*sj_cnt1)++; if(*sj_cnt1 >= Length){ *sj_cnt1 = 0; } for(i=0; i<16; i++) Orign_buff1[i] = font_buf1[*sj_cnt1][i]; } for(i=15; i>=0; i--){ //まず、一番左側文字をビットシフト next_buff1[disp_char_max-1][i] = ( next_buff1[disp_char_max-1][i] | ( scl_buff1[disp_char_max-1][i] & B10000000 )); scl_buff1[disp_char_max-1][i] = scl_buff1[disp_char_max-1][i]<<1; scl_buff1[disp_char_max-1][i] = ( scl_buff1[disp_char_max-1][i] | (( Orign_buff1[i] & B10000000 )>>7)); Orign_buff1[i] = Orign_buff1[i]<<1; } for(i=disp_char_max-2; i>=0; i--){ //次にその他文字をビットシフト for(j=15; j>=0; j--){ next_buff1[i][j] = ( next_buff1[i][j] | ( scl_buff1[i][j] & B10000000 )); scl_buff1[i][j] = scl_buff1[i][j]<<1; scl_buff1[i][j] = ( scl_buff1[i][j] | (( next_buff1[i+1][j] & B10000000 )>>7)); next_buff1[i+1][j] = next_buff1[i+1][j]<<1; } } (*scl_cnt1)++; } //*********文字ディスプレイ表示**************************** void SSD1331_8x16_Font_DisplayOut(uint8_t txtMax, uint8_t x1, uint8_t x2, uint8_t y1, uint8_t y2, uint8_t red, uint8_t green, uint8_t blue, uint8_t Fnt[][16]){ uint8_t com[6]; com[0] = 0x15; //Set Column Address com[1] = x1; com[2] = x2; com[3] = 0x75; //Set Row Address com[4] = y1; com[5] = y2; CommandWriteBytes(com, 6); int i, j, k; uint8_t bt = 0b10000000; uint8_t DotDot[txtMax * 16 * 8]; uint16_t cnt = 0; uint8_t Dot = (red << 5) | (green << 2) | blue; for(i=0; i<16; i++){ for(j=0; j<txtMax; j++){ for(k=0; k<8; k++){ if(k==0){ bt = 0b10000000; }else{ bt = bt >> 1; } if((Fnt[j][i] & bt)>0){ DotDot[cnt] = Dot; }else{ DotDot[cnt] = 0; } cnt++; } } } DataWriteBytes(DotDot, (16 * txtMax * 8)); } //************ SSD1331 初期化 **************************************** void SSD1331_Init(uint8_t sck, uint8_t miso, uint8_t mosi, uint8_t cs, uint8_t dc, uint8_t rst){ pinMode(rst, OUTPUT); //RES pinMode(dc, OUTPUT); //DC digitalWrite(rst, HIGH); digitalWrite(rst, LOW); delay(1); digitalWrite(rst, HIGH); digitalWrite(dc, HIGH); hspi.begin(sck, miso, mosi, cs); hspi.setFrequency(7000000); //SSD1331 のSPI Clock Cycle Time 最低150ns hspi.setDataMode(SPI_MODE3); hspi.setHwCs(true); CommandWrite(0xAE); //Set Display Off CommandWrite(0xA0); //Remap & Color Depth setting CommandWrite(0b00110010); //A[7:6] = 00; 256 color. A[7:6] = 01; 65k color format CommandWrite(0xA1); //Set Display Start Line CommandWrite(0); CommandWrite(0xA2); //Set Display Offset CommandWrite(0); CommandWrite(0xA4); //Set Display Mode (Normal) CommandWrite(0xA8); //Set Multiplex Ratio CommandWrite(0b00111111); //15-63 CommandWrite(0xAD); //Set Master Configration CommandWrite(0b10001110); //a[0]=0 Select external Vcc supply, a[0]=1 Reserved(reset) CommandWrite(0xB0); //Power Save Mode CommandWrite(0b00000000); //0x1A Enable power save mode CommandWrite(0xB1); //Phase 1 and 2 period adjustment CommandWrite(0x74); CommandWrite(0xB3); //Display Clock DIV CommandWrite(0xF0); CommandWrite(0x8A); //Pre Charge A CommandWrite(0x81); CommandWrite(0x8B); //Pre Charge B CommandWrite(0x82); CommandWrite(0x8C); //Pre Charge C CommandWrite(0x83); CommandWrite(0xBB); //Set Pre-charge level CommandWrite(0x3A); CommandWrite(0xBE); //Set VcomH CommandWrite(0x3E); CommandWrite(0x87); //Set Master Current Control CommandWrite(0x06); CommandWrite(0x15); //Set Column Address CommandWrite(0); CommandWrite(95); CommandWrite(0x75); //Set Row Address CommandWrite(0); CommandWrite(63); CommandWrite(0x81); //Set Contrast for Color A CommandWrite(255); CommandWrite(0x82); //Set Contrast for Color B CommandWrite(255); CommandWrite(0x83); //Set Contrast for Color C CommandWrite(255); for(int j=0; j<64; j++){ //Display Black OUT for(int i=0; i<96; i++){ DataWrite(0x00); DataWrite(0x00); DataWrite(0x00); } } CommandWrite(0xAF); //Set Display On delay(110); //ディスプレイONコマンドの後は最低100ms 必要 } //*********** SPI 通信送信 *********************************** void CommandWrite(uint8_t b){ digitalWrite(DC_OLED, LOW);//DC hspi.write(b); } void DataWrite(uint8_t b){ digitalWrite(DC_OLED, HIGH);//DC hspi.write(b); } void CommandWriteBytes(uint8_t *b, uint16_t n){ digitalWrite(DC_OLED, LOW);//DC hspi.writeBytes(b, n); } void DataWriteBytes(uint8_t *b, uint16_t n){ digitalWrite(DC_OLED, HIGH);//DC hspi.writeBytes(b, n); } //*********** 自作フォントをSDカードから読み込む **************** void MyFont_SD_Read(File F, uint8_t ZorH, uint8_t num, uint8_t buf[2][16]){ F.seek(num * (16 * ZorH)); F.read(buf[0], 16); F.read(buf[1], 16); }
ではこれをコンパイル書き込み実行してみてください。
これを比較した動画は以下のようになります。
いかがでしょうか。
writeBytes の方が比べ物にならないほど爆速ですね。
今まで write だけで1バイトずつ送信していたのがアホらしくなるほどの違いです。
GPIO をレジスタで直叩きしなくても、標準のSPIライブラリで十分高速化が望めますね。
恐らく、これがこのデバイスにマッチした SPI通信で出来る最高速だと思われます。
SSD1331 の場合、グラフィックアクセラレーションコマンドを使えば更に高速化できると思います。
格安のフルカラー OLED ディスプレイでここまで出来れば、私的には満足です。
まとめ
以上、Arduino core for the ESP32 ( ESP-WROOM-32 ) の SPI 通信についていろいろ実験してみました。
HSPI と VSPI をうまく使えば、複数のSPI デバイスを効率よく制御できることがわかりました。
ESP32 は GPIO が多いのでピン数が多くなっても全く問題ないのがイイですね。
そして、writeBytes を使えばかなりの高速化が可能なことがわかりました。
これで私的には ESP32 ( ESP-WROOM-32 ) のSPI制御はかなり確実にできるようになってきた気がします。
ただ、あくまで我流なので、間違えていたらスイマセン・・・。
ということで、今回はここまでです。
ではまた・・・。
コメント
You have “readBytes is so explosive that it is incomparable.”
That should state writeBytes, not readBytes.
Thanks for finding the mistake.
I fixed that word.
ありがとうございます。とても
参考になりました。
受信側は複数バイト どうなるんでしょうか
ninoさん
記事をご覧いただき、ありがとうございます。
ただ、この記事は3年半以上も前に書いたもので、現在の環境とは異なっていることをご了承ください。
Arduino core for the ESP32 では、だいぶ修正されてきています。
また、この頃は私自身がまだまだ勉強不足で、ただ単に標準的なプログラミング手法を知らなかっただけということもありました。
プロのプログラマーの間では複数バイトをまとめて送ることはあたりまえの作法だったようです。
ESP32のSPI受信はやったことが無いので、想像でしか言えませんが、おそらく1バイトづつ受信するより、まとめて受信した方が断然高速だと思われます。