ESP32 の SPI_MODE が修正。HSPI , VSPI , 複数SPIデバイス制御 , SPI高速化などについて

記事公開日:2017年5月10日
最終修正日:2017年5月24日

スポンサーリンク

こんばんは。

今回は、ESP32 ( ESP-WROOM-32 ) の SPI ( Serial Peripheral Interface )通信について報告したいと思います。
Arduino core for the ESP32 についてです。

まず、今までずーっとバグっていた SPI_MODE2 と SPI_MODE3 が最新版 Arduino core for the ESP32 で修正されました
( 2017/5/8 時点 )

これは、get.exe をクリックしただけでは修正されませんので注意してください
新たに GitHub から ZIP ファイルをダウンロードしてインストールし直さなければなりません。
これについては後で述べます。

また、ESP32 – DevKitC ( ESP-WROOM-32 開発ボード ) の SPI ( Serial Peripheral Interface )通信には VSPI と HSPI がありますが、その使い分けがようやくできました。
これができると、複数の SPI デバイスを制御するのに役立ちます。
特に micro SDHC カードスロットと併用する場合に良い効果が得られそうです。
I2C 通信よりも格段に高速なので、SPIの複数デバイス制御は必須ですね。

また、Arduino core for the ESP32 の SPI 標準ライブラリを使う場合に、関数をうまく使い分けると SPI 通信の高速化ができます。
残念ながら、レジスタを直接叩く GPIO ダイレクトアクセスは今回使えませんでした。
SPI 標準ライブラリだけで、十分高速です。
そもそも、SPI ライブラリ自信がGPIO を直接レジスタ制御するコマンドで出来ているので、当たり前といえば当たり前です。
現在ある情報でGPIO ダイレクトアクセスを試してみたら、digitalWrite と大差ありませんでしたし・・・。

それと、最近気づいたのですが、Arduino core for the ESP32 の CPU 周波数は、今まで最高 80 MHz だと勘違いしていて、実は既に 240 MHz で動いておりました。
フラッシュメモリの周波数の方が最大 80MHz ということでした。
これについても後述します。

検証で使うもの

今回の検証で使うものは以下の通りです。

ESP32 – DevKitC

私は秋月電子通商さんで購入しましたが、Amazonさんでも販売していますね。

フルカラー OLED SSD1331 モジュール

SparkFun マイクロSDカードスロット・ピッチ変換基板

10kΩ固定抵抗

1/4W で良いです。
micro SDHC カードの信号ピンのプルアップに使います。

サンハヤト ブレッドボード SAD-101

100MHz 以上のオシロスコープ

SPI 信号の周波数24MHz くらいの波形を見るので、最低 100MHz 以上のオシロスコープでないと正確な波形は見られません。
私は、日本製のこれを使っています。
持ち運びに便利で重宝しています。
因みに、プローブは別売りです。結構お高いです。

Arduino core for the ESP32 の再インストール

先にも述べましたが、get.exe をクリックしただけでは SPI_MODE は修正されませんのでご注意ください。
必ず、GitHub から ZIP ファイルをダウンロードして再インストールしなければなりません

しかし、この get.exe は毎回セキュリティーソフトで弾かれてしまいますね。
その場合はファイアウォール設定を解除するしかありません。
怪しい動作をしていないのなら、ESPRESSIF 社がセキュリティーソフト会社に申告してもらいたいものですね。

では話を戻しまして、Arduino core for the ESP32 を再インストールします。

その前に、以下のフォルダ内のファイルを全て削除してください
( Windows 10 の場合 )

C:\Users\ユーザー名\Documents\Arduino\hardware\espressif\esp32

その後、以下の記事を参照して、Arduino core for the ESP32 を再インストールしてください。

Arduino core for the ESP32 のインストール方法

ESP32 ( ESP-WROOM-32 ) のCPUクロック周波数の確認

先にも述べましたが、私がずっと勘違いしておりました。
Arduino IDE で開発する場合の ESP32 ( ESP-WROOM-32 ) のCPU クロック周波数は最大の 240MHz で動作していたようです。
Windows 10 の場合、以下のアドレスのフォルダを開いて下さい。

C:\Users\ユーザー名\Documents\Arduino\hardware\espressif\esp32

そこの、boards.txt というファイルをテキストエディタで開くと、ESP32 Dev Module のところに、

esp32.build.f_cpu=240000000L

という記述がありました。
つまり、240 MHz 設定でビルド(コンパイル)していたということです。
大変失礼しました。
m(_ _)m

ということは、最も電力を食う、最速のCPU で動いていたということですので、SPI のクロック設定もそれを念頭に置かねばなりません。
SPI 周波数は CPU クロック周波数で割り切れる周波数で使う原則があるためです。

SPIクロック周波数設定の決め方

では、デバイスによる、SPIクロックの周波数の私なりの我流の決め方を説明します。

通常、Arduino の場合では SPIクロックは分周器 ( ClockDivider )を使って、システムクロックの1/2, 1/4, 1/8, 1/16 と2の累乗で決めていきますが、ESP32 では setFrequency 関数があり、SPIクロック周波数をユーザーが直接決めることができます。
ただ、分周器 ( ClockDivider )を使わずに直接周波数を決めてしまう、この使い方が正しいかどうかは分かりません。
分周器 ( ClockDivider )を使うと、あまり高速にできないのです。
ClockDivider を何回か使って試してみたのですが、これは外部クロック用なのでしょうか、イマイチ速度が上げられませんでした。

私が実験したところ、setFrequency 関数で直接周波数を指定してもエラーが出ていなく、高速化できるので、今回はこれで説明していきます。
こちらの方が周波数を微調整できるので便利です。
setClockDivider関数は使いません。

micro SDHC カードの場合

電子工作レベルでできる micro SDHC カードの読み書きは SPI モードまでです。
これは理論値で24 MHz 程度だそうです。
もっと高速の読み書きをするには、ライセンス料を支払わねばならないようです。
(参考記事)
ESP32 ( ESP-WROOM-32 ) で micro SDHC メモリカードを使う場合の注意点

よって、ESP32 の CPU が240MHz なので、micro SDHC カードのSPI周波数設定は最大 24MHz となります。
ClockDivider を使う場合は1/16 分周で 16MHz となってしまうのでしょうか?
それでは遅くなってしまいますね。

SD.h ライブラリの場合は デフォルトで VSPI を使うようになっています。
周波数設定は下のように begin 関数内で設定します。

SD.begin(5, SPI, 24000000, “/sd”);

OLED SSD1331 モジュールの場合

データシートによると、SPIクロックサイクルの最小値が 150ns なので、6.6666…. MHz です。
つまり、240で割り切れる近似値とすると、6 MHz 設定となります。
setFrequency関数を使うとこういう指定になります。

SPI.setFrequency(6000000);

これをオシロスコープで見てみるとこうなります。

HSPI_VSPI_010

ただ、これでは最小の150ns よりも長くなっています。
そこで、240で割り切れない 7 MHz としてみると、下図のようになります。

HSPI_VSPI_011

ほぼピッタリ 150 ns となりました。
ClockDivider よりも細かく微調整できますね。
ただ、CPUクロックで割り切れないということについて何か弊害があるかもしれませんので、6 MHz とした方が無難かもしれませんね。
何か問題があればぜひ教えていただきたいものです。

修正された SPI_MODE の動作確認

では、最新の Arduino core for the ESP32 の SPI_MODE が修正されているか確認してみます。

以下のスケッチを入力して、コンパイル書き込みしてみてください。
10100110
というビットを送信した場合です。
これは SPI_MODE3 にしています。

/*
 * VSPI
 * SCLK #18
 * MOSI #23
 * MISO #19
 * CS(SS) #5
 */
#include <SPI.h>

byte b=0b10100110;

void setup() {
  SPI.begin();
  SPI.setFrequency(7000000); //SSD1331 のSPI Clock Cycle Time 最低150ns
  SPI.setDataMode(SPI_MODE3);
}

void loop() {
  SPI.write(b);
}

SCLK と MOSI ( Master Output Slave Input ) 信号をオシロスコープで確認してみるとこうなりました。

HSPI_VSPI_02

ちゃんと、本来の SPI MODE 3 に直っていましたね。
念のため、SPI_MODE2 にしてみます。

HSPI_VSPI_03

これもちゃんと SPI MODE 2 に修正されていますね。

これで、過去のソースコードを全て修正しなければならなくなりました。
ただ、誤ったMODE でもあまり違和感なく普通に動いているのが悩ましいところです。
電子工作系ブログ記事の場合、こういう修正がエライ大変ですね。
これからブログを立ち上げる方は電子工作系はやめておいた方が良いですよ~・・・。

VSPI や HSPI とは

そもそも、VSPI と HSPI とは何ぞや? ということですが、いろいろ調べてみて何となく分かったことがあります。
ESP32 ( ESP-WROOM-32 )には、Dual SPI や Quad SPI デバイスを動かすために、SPI インターフェースを4つ装備しているようです。

Quad SPI は4つの SPI を同時通信して、外部デバイスのSRAM や Flash を高速で読み書きするためのものだそうです。
ESP-WROOM-32 のデータシートをよ~く見てみると、Parallel QSPI ( Quad SPI ) と書いてあるところがそれです。

その Quad SPI 端子のうち、SHD/SD2 とか、SDO/SD0 のような SD カードっぽい GPIO 名称があります。
ESP32 – DevKitC では、下図の様な赤色囲いのところです。

HSPI_VSPI_10

GitHub の以下のページの下の方にもピンアサインがありますのでそれも参照してみてください。

https://github.com/espressif/arduino-esp32

これから、赤色のところの SDカードインターフェースは、HS ( Hi-Speed ) 通信用端子であることが分かりました。
つまり、以前の記事で説明した SDアソシエーション という団体にライセンス料を払って開発する高速通信モード用端子のようです。
この端子も通常のSPIモードで使えるかも知れませんが、まだ試していません。

ということで、現時点で Arduino core for the ESP32 で使える SPI 端子は、 VSPI と HSPI 端子ということのようです。

VSPI の ‘ V ‘  や HSPI の ‘ H ‘ の意味は良く分かりません。
Twitter で Vertical , Horizontal ではないかという意見もありました。
でも、機能的には通常の SPI と何ら変わりありませんでした。
Hがついているからといって、Hi-Speed ではありませんよ・・・。

HSPI で動かす

OLED SSD1331 を VSPI で動かす方法は以前の記事で説明しました。

では、これを HSPI で動かす場合を説明します。
HSPI 端子は以下のようになります。

GPIO #14  —– SCLK ( SPI Clock )
GPIO #13  —– MOSI ( Master Output Slave Input )
GPIO #12  —– MISO ( Master Input Slave Output )
GPIO #15  —– CS ( Chip Select )

まず、ESP32 – DevKitC に何も接続せずに、以下のスケッチを入力してみてください。

/*
 * HSPI
 * SCLK #14
 * MOSI #13
 * MISO #12
 * CS(SS) #15
 */
#include <SPI.h>

byte b=0b10100110;

SPIClass hspi(HSPI);

void setup() {
  pinMode(15, OUTPUT);
  digitalWrite(15, HIGH);
  
  hspi.begin(14, 12, 13, 15);
  hspi.setFrequency(7000000); //SSD1331 のSPI Clock Cycle Time 最低150ns
  hspi.setDataMode(SPI_MODE3);
}

void loop() {
  digitalWrite(15, LOW);
  hspi.write(b);
  digitalWrite(15, HIGH);
}

これで、HSPI 端子から信号が出力されていると思いますので、オシロスコープやロジックアナライザーで確認してみてください。

このスケッチでは、CS ( Chip Select )ピンを digitalWrite で HIGH-LOW を切り替えていますが、実はこれを使わなくても良い方法があります。
それは、 setHwCs 関数を使うことです。
以下のスケッチを入力してみてください。

/*
 * HSPI
 * SCLK #14
 * MOSI #13
 * MISO #12
 * CS(SS) #15
 */
#include <SPI.h>

byte b=0b10100110;

SPIClass hspi(HSPI);

void setup() {
  hspi.begin(14, 12, 13, 15);
  hspi.setFrequency(7000000); //SSD1331 のSPI Clock Cycle Time 最低150ns
  hspi.setDataMode(SPI_MODE3);
  hspi.setHwCs(true);
}

void loop() {
  hspi.write(b);
}

setHwCs 関数は、ハードウェアCSピンを使うか使わないか選択できる関数です。
begin関数で指定したCSピンがハードウェアCSピンです。

Arduino IDE 系の digitalWrite 関数は速度が遅いということで有名ですよね。
ロジックアナライザーを使ってdigitalWriteを使った様子を見てみたいと思います。
因みに、私のロジックアナライザーは ZEROPLUS LAP-C を使っています。

これはロジアナとしては安いのに 100MHz という優れものです。

では、CSピンを digitalWrite で High – Low 切り替えた場合の波形はこんな感じです。

HSPI_VSPI_12

ちょっと見にくくて申し訳ないのですが、SCK や MOSI 信号発信が無いところでも、しばらくCSピンがLOWになったままですね。

では、setHwCs 関数を使うとこうなります。

HSPI_VSPI_13

見事に SCK や MOSI 信号が発信されたところだけピンポイントで CS が LOW になっていますね。
ただ、これを使ったからと言って、バイト毎の送信間隔はあまり変わらないので、高速化は望めないようです。
でも、CS ( Chip Select )ピンをdigitalWriteで High – Low に切り替えなくて済むので、プログラムを節約できることが、この関数を使う最大の利点です。
これは有り難いですね。

では、実際に フルカラー OLED ( 有機EL ) SSD1331 を HSPI 接続して動作させてみましょう。

接続はこんな感じです。

HSPI_VSPI_11

VSPI 接続の以前の記事の、ブルー画面スケッチを HSPI 接続用に変更したものが以下のスケッチになります。

/*
 * HSPI
 * SCLK #14
 * MOSI #13
 * MISO #12
 * CS(SS) #15
 */
#include <SPI.h>

const uint8_t SCLK_OLED =  14; //SCLK (SPI Clock)
const uint8_t MOSI_OLED =  13; //MOSI (Master Output Slave Input)
const uint8_t MISO_OLED =  12; //SSD1331では実際は使わない。MISO (Master Input Slave Output)
const uint8_t CS_OLED = 15; //OLED ChipSelect
const uint8_t DC_OLED =  16; //OLED DC (Data/Command)
const uint8_t RST_OLED =  4; //OLED Reset

SPIClass hspi(HSPI);
 
void setup() {
  delay(1000);
  int i, j;
  uint8_t R, G, B, Dot1, Dot2;
   
  SSD1331_Init(SCLK_OLED, MISO_OLED, MOSI_OLED, CS_OLED, DC_OLED, RST_OLED);
 
  R = 7; G = 0; B = 0; //256 color : R (0-7), G (0-7), B (0-3) 
  Dot1 = (R << 5) | (G << 2) | B;
   
  R = 0; G = 0; B = 3; //256 color : R (0-7), G (0-7), B (0-3) 
  Dot2 = (R << 5) | (G << 2) | B;
   
  for(j=0; j<64; j++){
    for(i=0; i<96; i++){
      if(j<8 && i<16) {
        DataWrite(Dot1);
      }else{
        DataWrite(Dot2);
      }
    }
  }
}
 
void loop() {
   
}
 
//************ 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);
  hspi.write(b);
}
//********** SPI データ出力 ****************************
void DataWrite(uint8_t b){
  digitalWrite(DC_OLED, HIGH);
  hspi.write(b);
}

SSD1331 の場合は、データやコマンドを送る前に CS ( Chip Select )ピンを Low にして、DCピンを以下のようにします。

コマンド送信  DC — LOW
データ送信    DC — HIGH

今回は HSPI と VSPI に分けて、setHwCs 関数を使ったので、CSピンを切り替える必要は無く、digitalWrite を使うのは DC ( Data/Command )ピンだけで、とてもシンプルになりました。

では、これをコンパイル実行させてみてください。
こんな感じで表示できていればOKです。

HSPI_VSPI_18

HSPI と VSPI を同時使用および複数デバイス制御

では、ちょっとオモシロイことをやってみます。
VSPI と HSPIをうまく使って、同時に使用することができるんです。
すると、こんな芸当もできてしまいます。

例えば、こんな感じのスケッチです。

#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 )で拡大してみると、

HSPI_VSPI_14

プログラム通り、約7MHz の 150ns になっていますね。
そして、SPI_MODE3 になっています。

では、HSPI側を見てみます。

HSPI_VSPI_15

しっかり5 MHz 出ていますね。
そして、SPI_MODE 2 になっています。

もっと大きく見てみるとこんな感じです。

HSPI_VSPI_16

いかがでしょうか。
VSPI とHSPI が共存できて、しかも、異なるSPI周波数とSPI_MODEが共存できていますね。
これができれば、複数の異なる周波数の SPI デバイスを制御できます。

ESP8266 の以前の記事では、micro SDHC カードと OLED ディスプレイで異なる周波数や SPI_MODE 設定をやったことがありました。
でも、ESP32 では、 VSPI と HSPI をうまく使うとプログラムをかなり節約できて有難いですね。

SPI 通信の高速化

上記の様に SPI.write 関数で1バイト毎送信すると、次のバイト送信まで間隔が空き過ぎていることが分かると思います。
実はそれでは処理が遅くなってしまうんですね。

では、もっと間隔を詰めて高速化するにはどうしたらよいのでしょうか?

以下のようなスケッチにしてみてください。

#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 )で波形を見てみましょう。

HSPI_VSPI_17

いかがでしょうか。

write関数で1バイト毎に送信していた時の余分な間隔が無くなり、8バイトを連続して送っていることが分かると思います。
これを使えば、write関数を使うよりも数十倍高速化することができます。

では、今までやってきたことを応用して、 ESP32 – DevKitC と OLED SSD1331 と micro SDHC カードをそれぞれ以下のように接続してみます。
micro SDHC カードスロットが VSPI 接続で、 OLED SSD1331 が HSPI 接続です。

HSPI_VSPI_20

写真ではこうなります。

HSPI_VSPI_21

以前の記事でも述べましたが、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つずつ移動させるプログラムにしました。

では、以下のスケッチを入力してみてください。

#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 関数でバイトをまとめて送信した以下のプログラムに変えてみて下さい。

#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);
}

ではこれをコンパイル書き込み実行してみてください。

これを比較した動画は以下のようになります。

いかがでしょうか。
readBytes の方が比べ物にならないほど爆速ですね。
今まで 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制御はかなり確実にできるようになってきた気がします。
ただ、あくまで我流なので、間違えていたらスイマセン・・・。

ということで、今回はここまでです。
ではまた・・・。

mgo-tec電子工作 関連コンテンツ ( 広告含む )

スポンサーリンク

Amazon.co.jp 当ブログのおすすめ







投稿者:

mgo-tec

Arduino , ESP32 ( ESP-WROOM-32 ) , ESP8266 ( ESP-WROOM-02 )等を使って、主にスマホと連携した電子工作やプログラミング記事を書いてます。ライブラリも作ったりしてます。趣味、独学でやってますので、動作保証はしません。 電子回路やプログラミングの専門家ではありません。 畑違いの仕事をしてます。 でも、少しだけ電気の知識が必要な仕事なので、電気工事士や工事担任者等の資格は持ってます。

コメントを残す

メールアドレスが公開されることはありません。

*画像の文字を入力してください。(スパム防止の為)