ESP32の低レベルGPIOアクセス その1 デジタル入力・出力

概要

Arduino Core for ESP32のライブラリ実装を確認しながら、テクニカルリファレンスマニュアルと読み合わせをしていきたいと思います。

今回はdigitalWrite()とdigitalRead()を調べてみました。

デジタル出力

digitalWrite()関数の実装

extern void IRAM_ATTR __digitalWrite(uint8_t pin, uint8_t val)
{
    if(val) {
        if(pin < 32) {
            GPIO.out_w1ts = ((uint32_t)1 << pin);
        } else if(pin < 34) {
            GPIO.out1_w1ts.val = ((uint32_t)1 << (pin - 32));
        }
    } else {
        if(pin < 32) {
            GPIO.out_w1tc = ((uint32_t)1 << pin);
        } else if(pin < 34) {
            GPIO.out1_w1tc.val = ((uint32_t)1 << (pin - 32));
        }
    }
}

上記のソースコードでした。32より小さい場合(0-31)と、34未満(32-33)で書き込んでいる場所が違っています。ちなみに34以降は入力専用PINのためなにも処理をしていないみたいですね。

valがHIGHかLOWなので、出力によっても書き込んでいる場所が違っています。

GPIOHIGHLOW
0-31GPIO.out_w1tsGPIO.out_w1tc
32, 33GPIO.out1_w1ts.valGPIO.out1_w1tc.val
34-39処理なし処理なし

上記の関係ですね。では実際にテクニカルリファレンスマニュアルで該当部分をみてみたいと思います。

テクニカルリファレンスマニュアル

GPIO.out_w1ts(GPIO0-31のHIGH)

GPIO0-31にたいしてHIGH出力する場合に設定するレジスタアドレスで、該当するGPIOのビットを1にすることで出力に設定しているようです。一般的にArduinoの場合にはPIN単位で操作しますが、レジスタに書き込んだ場合には複数のGPIOを一度に操作できそうです。

GPIO.out1_w1ts.val(GPIO32, 33のHIGH)

GGPIO32-39にたいしてHIGH出力する場合に設定するレジスタアドレスです。実際にはGPIO34-39は入力専用PINのため設定しても出力はされません。

0-31とちがってvalという変数に設定していますが、この違いはのちほど説明したいと思います。

GPIO.out_w1tc(GPIO0-31のLOW)

こちらも該当ビットに1を設定するとLOWに出力(クリア)されるようです。

GPIO.out1_w1tc.val(GPIO32, 33のLOW)

こちらも構造的には同じですね。

リファレンスでアドレスを確認

W1TSで検索したところ、上記の表がありました。Arduinoでも確認してみたいと思います。

Arduinoでも確認

void setup() {
  Serial.begin(115200);
  delay(100);

  Serial.printf("GPIO               : %p\n", &GPIO);
  Serial.printf("GPIO.out_w1ts      : %p\n", &GPIO.out_w1ts);
  Serial.printf("GPIO.out1_w1ts.val : %p\n", &GPIO.out1_w1ts.val);
  Serial.printf("GPIO.out_w1tc      : %p\n", &GPIO.out_w1tc);
  Serial.printf("GPIO.out1_w1tc.val : %p\n", &GPIO.out1_w1tc.val);
}

void loop() {
}

上記のコードで実際に書き込んでいるアドレスを表示してみます。

GPIO               : 0x3ff44000
GPIO.out_w1ts      : 0x3ff44008
GPIO.out1_w1ts.val : 0x3ff44014
GPIO.out_w1tc      : 0x3ff4400c
GPIO.out1_w1tc.val : 0x3ff44018

あたりまえですが、リファレンスと同じアドレスですね。GPIOの先頭アドレスは0x3ff44000からのようです。

GPIO構造体のアドレス指定方法

PROVIDE ( GPIO = 0x3ff44000 );

\esp32-1.0.4\tools\sdk\ld\esp32.peripherals.ldにて、上記の指定がありました。リンカの設定で開始アドレスを設定しているようです。

GPIO構造体とは?

typedef volatile struct {
    uint32_t bt_select;                             /*NA*/
    uint32_t out;                                   /*GPIO0~31 output value*/
    uint32_t out_w1ts;                              /*GPIO0~31 output value write 1 to set*/
    uint32_t out_w1tc;                              /*GPIO0~31 output value write 1 to clear*/
    union {
        struct {
            uint32_t data:       8;                 /*GPIO32~39 output value*/
            uint32_t reserved8: 24;
        };
        uint32_t val;
    } out1;
    union {
        struct {
            uint32_t data:       8;                 /*GPIO32~39 output value write 1 to set*/
            uint32_t reserved8: 24;
        };
        uint32_t val;
    } out1_w1ts;
    union {
        struct {
            uint32_t data:       8;                 /*GPIO32~39 output value write 1 to clear*/
            uint32_t reserved8: 24;
        };
        uint32_t val;
    } out1_w1tc;

(略)

} gpio_dev_t;
extern gpio_dev_t GPIO;

\esp32-1.0.4\tools\sdk\include\soc\soc\gpio_struct.hで宣言されています。ちょっと長いので途中を省略してあります。

先頭アドレスは.ldにて指定してあり、uint32_tでレジスタ単位で変数が並んでいます。.valでアクセスしていたところをみると、unionで宣言されています。

    union {
        struct {
            uint32_t data:       8;                 /*GPIO32~39 output value write 1 to set*/
            uint32_t reserved8: 24;
        };
        uint32_t val;
    } out1_w1ts;

上記の場合、8ビットはdataで、残り24ビットがreserved8となっています。全体の32ビットにアクセスする場合にはvalとなります。

32ビットすべて同じ目的で利用する場合にはunionがないので直接アクセスをして、unionがあるアドレスは、unionで用途別の変数か、全体をあらわすvalにアクセスをする必要がありそうです。

実験

void setup() {
  pinMode(10, OUTPUT);
}

void loop() {
  digitalWrite(10, HIGH);
  delay(500);
  digitalWrite(10, LOW);
  delay(500);
}

GPIO10にLEDを接続している端末でLチカをしてみたいと思います。これを直接アクセスで実現してみたいと思います。

void setup() {
  pinMode(10, OUTPUT);
}

void loop() {
  GPIO.out_w1ts = 1 << 10;
  delay(500);
  GPIO.out_w1tc = 1 << 10;
  delay(500);
}

なんのひねりもありませんが、上記のようなコードになりました。

void setup() {
  pinMode(10, OUTPUT);
}

void loop() {
  uint32_t *p;

  // GPIO.out_w1ts
  p = (uint32_t*)0x3ff44008;
  *p = 1 << 10;

  delay(500);

  // GPIO.out_w1tc
  p = (uint32_t*)0x3ff4400c;
  *p = 1 << 10;

  delay(500);
}

ポインタアクセスに書き直しました。

デジタル入力

digitalRead()関数の実装

extern int IRAM_ATTR __digitalRead(uint8_t pin)
{
    if(pin < 32) {
        return (GPIO.in >> pin) & 0x1;
    } else if(pin < 40) {
        return (GPIO.in1.val >> (pin - 32)) & 0x1;
    }
    return 0;
}

こちらの方がシンプルですね。GPIO0-32と33-39で読み込むレジスタが違い、範囲外は常に0を返却しています。

テクニカルリファレンスマニュアル

こちらも出力と同じような構造ですね。リードオンリーのレジスタから該当GPIOのビットが立っているかで確認することができます。

アドレスは上記でした。GPIOの先頭アドレスの0x3ff44000に、個別ページに書いてあるオフセット値を足した値ですね。

実験

void setup() {
  Serial.begin(115200);
  delay(100);

  pinMode(37, INPUT);
}

void loop() {
  Serial.printf( "Input : %d\n", digitalRead(37));
  delay(500);
}

上記のコードと同じ動きを実現したいと思います。

void setup() {
  Serial.begin(115200);
  delay(100);

  pinMode(37, INPUT);
}

void loop() {
  Serial.printf( "Input : %d\n", (GPIO.in1.val >> (37 - 32)) & 0x1);
  delay(500);
}

こちらも32より大きいかでアクセスするレジスタを変更するのと、32以上の場合には32引いた値をシフトするのを忘れなければ単純ですね。

  Serial.printf( "Input : %d\n", GPIO.in1.val & (1 << (37 - 32)));

雑な判定でいいのであれば、上記のほうがシンプルな判定になります。ただこの場合には0か32が判定されますのでif文の中などでは問題ありませんが、返却値を使いたい場合には0か1かが返ってくる方が安全だと思います。

まとめ

デジタル入力と出力は比較的単純でしたね。pinMode()関数はちょっと複雑なので今後調べてみたいと思います。

続編

コメント

  1. rain より:

    非常に参考になります。直接レジスタにアクセスする方法に関して、初心者向けの記事があまりないので、また機会があれば投稿していただける嬉しいです。

    • たなかまさゆき より:

      Arduinoのボードライブラリのバージョンがあがって、結構昔とコードが変わっているはずです
      少し時間できたら今のバージョンも確認してみたいと思います