yoshiyuki's blog

Arduino/Teensy/Raspberry pi pico を利用した I2C, SPI 通信アプリを紹介します

Teensy 4.0 / GPIO の制御

Teensy 4.0 の GPIO を制御して High/Low 信号やパターン信号を任意に作れるようにしようとして苦労した話です。

設定

苦労しなくても GPIO なんて digitalWrite と digitalRead で制御できると思うのですが、ここでも I2C の時と同じくレジスタの読み書きで制御を行います。趣味です。

端子の選択

GPIO として使用する端子を選びます。電源端子等の例外を除くと基本的にはどの端子も GPIO として使用できるので、ここでは単純に番号の小さい方から 2番と3番と出力、4番と5番と入力として使うことにします。

f:id:ysin1128:20210208114524p:plain

上で選んだ基板上の端子に対応するマイコンの端子名を回路図から確認します。

f:id:ysin1128:20210208114640p:plain

#2 EMC_04
#3 EMC_05
#4 EMC_06
#5 EMC_08

これらの端子に対して設定を行っていきます。

Multiplexer

Teensy の本体であるマイコン IMXRT1060 の各端子には色々な機能を任意に割り当てられるようになっているので、まずは上で選んだ端子に対して GPIO を割り当てるように設定する必要があります。I2C の時と同様に Multiplexer (MUX) の設定で GPIO モジュールと各端子とを繋げてやります。以下が設定の記述となります。

IMXRT_REGISTER32_t *port_iomuxc;

void setup() {
  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000

  // #2 = D2, GPIO_EMC_04
  port_iomuxc->offset024 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO04)

  // #3 = D3, GPIO_EMC_05
  port_iomuxc->offset028 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO05)

  // #4 = D4, GPIO_EMC_06
  port_iomuxc->offset02C = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO06)

  // #5 = D5, GPIO_EMC_08
  port_iomuxc->offset034 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO08)
}

port_iomuxc->offsetxxx で特定のレジスタを指定できる仕組みは I2C の時にも説明していますが、簡単に言うと Arduino IDE がこの記述で対象のレジスタを指定できるように下準備してくれているからです。
レジスタの詳細、つまり、どのレジスタがどの端子のどんな設定に対応しているかは IMXRT1060 の Reference Manual 参照です。
この記述で指定した各レジスタはそれぞれの端子の MUX の設定に対応し、2-0 bit に 101 = 0x5 を書き込むことで接続先に GPIO モジュールを選択できます。また 4 bit に 1 を書き込むことで端子の MUX の選択が固定されます。
ちなみに、4端子とも接続先は GPIO4 モジュールとなっていました。

アナログ設定

続いて端子のアナログ設定を行います。これは端子そのものの動作速度や Pull-up/Pull-down 抵抗の設定となります。以下は、上の MUX の記述にアナログ設定の記述を追加したものです。

IMXRT_REGISTER32_t *port_iomuxc;

void setup() {
  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000

  // #2 = D2, GPIO_EMC_04
  port_iomuxc->offset024 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO04)
  port_iomuxc->offset214 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #3 = D3, GPIO_EMC_05
  port_iomuxc->offset028 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO05)
  port_iomuxc->offset218 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #4 = D4, GPIO_EMC_06
  port_iomuxc->offset02C = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO06)
  port_iomuxc->offset21C = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #5 = D5, GPIO_EMC_08
  port_iomuxc->offset034 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO08)
  port_iomuxc->offset224 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)
}

12 bit が Pull-up/Pull-down/Keeper の有効化ですが、今回はこれら機能を無効にするので 0 を書き込んでいます。ちなみに、有効にする場合は 15-13 bit でさらに設定が必要です。
11 bit が Open Drain の有効化で I2C 端子として使用する場合は必要ですが、今回はロジック信号の制御なので 0 を書き込んで無効化しています。
7-6 bit は端子の動作速度の設定です。値を 10 とすると 150 MHz に対応します。
5-3 bit は High/Low を出力するドライバのオン抵抗を設定します。値が大きいほどオン抵抗は小さくなります。
0 bit は Slew Rate の設定です。 0 は Slow、1 は Fast となります。
今のところ用途があるわけではなく、 7-0 bit にどんな設定が必要か分からないので、これらは Default の値に設定しています。

以上で端子の設定は完了です。

Clock の設定

I2C で学びましたが Teensy のマイコン IMXRT1060 は省電力のために使わないモジュールへの Clock の供給を停止する機能があり、Arduino IDE が気を利かせてその機能で Clock を停止している場合があるので、必要な Clock が供給されるようしっかりと設定しておく必要があります。
今回、使用する端子に繋がっているのが GPIO4 モジュールであることは上で確認済みなので、GPIO4 モジュール への Clock の供給を制御する Clock Gating を以下の記述で設定します。

  CCM_CCGR3 |= 0x00003000; // CG6 = 11, enable GPIO4 clock

CCM_CCGR3 の 13-12 bit の値に 11 を書き込むと GPIO4 モジュールに Clock が供給されます。供給される Clock の周波数は 108 MHz のはずですが、この Clock 周波数が決まるまでの設定に関しては面倒だったのであまりよく確認しておらず、間違っているかもしれません。

GPIO モジュールの設定

残るは GPIO モジュールの中身の設定です。とは言え実は、GPIO モジュールの中では IO の選択くらいしか設定することがありません。

IMXRT_REGISTER32_t *port_gpio;

void setup() {
  port_gpio = &IMXRT_GPIO4; //0x401C4000

  port_gpio->offset004 = 0x00000030; // GPIO4_IO04/05 = Output, GPIO4_IO06/08 = Input
}

今回、使用するモジュールは GPIO4 となっており、port_gpio->offset004 で GPIO4 モジュールの各 IO を入力、または、出力に設定します。port_gpio->offset004 は 32 bit のレジスタで、各 bit が GPIO4 モジュールの 32個の IO の設定に対応し、値を 1 にすると出力、 0 にすると入力に設定されます。
ここでは基板上の 2番、3番の端子を出力に設定します。2番、3番の端子は GPIO4 モジュールの 04 番 と 05 番の IO に繋がるので port_gpio->offset004 の 5-4 bit の値に 1 を書き込んで出力に設定しています。

GPIO の制御

最後に GPIO の制御方法の確認です。ここでは出力端子の出力の High/Low を制御する方法と、各端子の High/Low 状態を読み出す方法を確認します。

uint32_t uintTMP;

IMXRT_REGISTER32_t *port_gpio;

void setup() {
  port_gpio = &IMXRT_GPIO4; //0x401C4000

  port_gpio->offset004 = 0x00000030; // GPIO4_IO04/05 = Output, GPIO4_IO06/08 = Input
}

void loop() {
  port_gpio->offset000 = 0x00000030; // GPIO4_IO04/05 = High
  uintTMP = port_gpio->offset008;

  delay(1000);
}

出力の High/Low は port_gpio->offset000 への値の書き込みで制御します。port_gpio->offset000 は 32 bit のレジスタで、各 Bit が GPIO4 モジュールの各 IO の High/Low 制御に相当します。
今回の制御対象は port_gpio->offset004 で出力に設定した 04, 05 番の IO なので、上の記述では対応する Bit に 1 を書き込むことで 04, 05 番の出力を High に設定しています。反対にこれらの Bit に 0 を書き込むと 04, 05 番の出力は Low になります。
端子の High/Low 状態は port_gpio->offset008 のレジスタ値で確認できます。port_gpio->offset008 は 32 bit のレジスタで、各 Bit が GPIO4 モジュールの各 IO の High/Low 状態を示しています。port_gpio->offset004 で入力に設定しても出力に設定しても関係無く High/Low 状態を読み取ることができます。
今回は基板上の 4番, 5番端子を入力端子として使用し、それに対応するのは GPIO4 モジュールの 06 番, 08 番 の IO となるので、port_gpio->offset008 の 8 bit と 6 bit が、レジスタを読み出した瞬間に各端子に入力されている信号の High/Low 状態を示します。また、この時に 5-4 bit で基板上の 2番, 3番端子の High/Low の状態を読み出すことができます。これらは出力に設定しているので基本的には port_gpio->offset000 で設定した High/Low 状態と同じものが読み取れます。

実験と失敗

ここまでで作った設定を使って GPIO の動作確認をしてみました。結果を先に言うと失敗でした。

実験用 Sketch

GPIO の動作実験を以下の記述で行いました。
setup には上で作った設定を放り込んでいます。
loop は PC から Arduino IDE のシリアルモニタと通信を行うことを想定して、シリアルモニタから 1 から 3 の数字を受け取ったら基板上の 2番端子、3番端子の High/Low を操作して、基板上の 2番から5番までの端子の High/Low 状態をシリアルモニタに返すようにしています。また、念のためにシリアルモニタからどの数字を受け取ったかをシリアルモニタに返すようにしています。1 から 3 の数字以外のものを受け取った場合は 2番端子、3番端子の出力をどちらも Low にします。

byte inDAT;
int cntDAT;

IMXRT_REGISTER32_t *port_iomuxc;
IMXRT_REGISTER32_t *port_gpio;

void setup() {
  Serial.begin(9600);

  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000
  port_gpio = &IMXRT_GPIO4; //0x401C4000

  CCM_CCGR3 |= 0x00003000; // CG6 = 11, enable GPIO4 clock

  // #2 = D2, GPIO_EMC_04
  port_iomuxc->offset024 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO04)
  port_iomuxc->offset214 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #3 = D3, GPIO_EMC_05
  port_iomuxc->offset028 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO05)
  port_iomuxc->offset218 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #4 = D4, GPIO_EMC_06
  port_iomuxc->offset02C = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO06)
  port_iomuxc->offset21C = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #5 = D5, GPIO_EMC_08
  port_iomuxc->offset034 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4_IO08)
  port_iomuxc->offset224 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // GPIO module
  port_gpio->offset004 = 0x00000030; // GPIO4_IO04/05 = Output, GPIO4_IO06/08 = Input
}

void loop() {
  cntDAT = 0;

  while(Serial.available()){
    inDAT = Serial.read();
    cntDAT++;
    delay(5);
  }

  if(cntDAT > 0){
    inDAT = inDAT - 0x30;
    
    if(inDAT == 1){
      port_gpio->offset000 = 0x00000010;
      Serial.print("1-");
    }
    else if(inDAT == 2){
      port_gpio->offset000 = 0x00000020;      
      Serial.print("2-");
    }
    else if(inDAT == 3){
      port_gpio->offset000 = 0x00000030;            
      Serial.print("3-");
    }
    else{
      port_gpio->offset000 = 0x00000000;                  
      Serial.print("0-");
    }

    delay(1);

    Serial.print(port_gpio->offset008 & 0x00000170);
    Serial.print("\n");    
  }

  delay(1000);
}

配線

実験として、2番端子 (出力) を 4番端子 (入力) に、3番端子 (出力) を 5番端子 (入力) に接続します。これで High/Low状態の読み出しにおいて 2番と4番、3番と5番それぞれの値が一致するはずです。

f:id:ysin1128:20210315150118p:plain

実験結果

f:id:ysin1128:20210315150311p:plain

シリアルモニタから 0, 1, 2, 3 の数字を1文字ずつ順番に送った結果です。
各行ともハイフン (-) 以前の数字が変わっているので期待通りの動作はしてくれているようです。なので、おかしいのは GPIO の High/Low 状態を読み出した値であるハイフン以後の数字が各行とも 0 であることです。シリアルモニタから送る数字によって 2番、3番端子の High/Low 状態は変わるはずなのですが、シリアルモニタから送った数字に関わらず (さらにその数字が正しく認識されているにも関わらず) 2番から5番の High/Low 状態を読み出した値が 0、つまり全て Low になっています。
実際の出力はどうかと 2番, 3番端子の電圧をテスターで測ってみましたが、シリアルモニタから 3 の数字を送って両方とも High を出力するように設定しても両端子の出力は Low のままでした。
逆に 4 番端子に外部から 3.3 V = High を印加した状態でシリアルモニタから 3 の数字を送ると、シリアルモニタに返ってきた数字は 0-64 となり、64 = 0x00000040 から 4番端子 = 06番の IO が High になっていることが確認できました。

というわけで失敗です。
問題は GPIO の出力の制御の方のようですが、制御といっても端子を出力に設定して High/Low を指示するだけだったので、うまく動かなかった理由が分かりません。ここから長い試行錯誤の繰り返しでした。

修正と実験成功

以下が実験に成功した Sketch と実験結果です。

byte inDAT;
int cntDAT;

IMXRT_REGISTER32_t *port_iomuxc;
IMXRT_REGISTER32_t *port_gpio;

void setup() {
  Serial.begin(9600);

  port_iomuxc = &IMXRT_IOMUXC;  //0x401F8000
  port_gpio = &IMXRT_GPIO9; //0x4200C000

  // CCM_CCGR3 |= 0x00003000; // CG6 = 11, enable GPIO4 clock

  // #2 = D2, GPIO_EMC_04
  port_iomuxc->offset024 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4/9_IO04)
  port_iomuxc->offset214 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #3 = D3, GPIO_EMC_05
  port_iomuxc->offset028 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4/9_IO05)
  port_iomuxc->offset218 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #4 = D4, GPIO_EMC_06
  port_iomuxc->offset02C = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4/9_IO06)
  port_iomuxc->offset21C = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // #5 = D5, GPIO_EMC_08
  port_iomuxc->offset034 = 0x00000015; // SIN = 1, MUX_MODE = 101 (GPIO4/9_IO08)
  port_iomuxc->offset224 = 0x000000B0; // PKE = 0 (disable pull-up/Keeper), Speed = 10 (Fast), DSE = 110, SRE = 0 (Slow)

  // GPIO module
  port_gpio->offset004 = 0x00000030; // GPIO9_IO04/05 = Output, GPIO9_IO06/08 = Input
}

void loop() {
  cntDAT = 0;

  while(Serial.available()){
    inDAT = Serial.read();
    cntDAT++;
    delay(5);
  }

  if(cntDAT > 0){
    inDAT = inDAT - 0x30;
    
    if(inDAT == 1){
      port_gpio->offset000 = 0x00000010;
      Serial.print("1-");
    }
    else if(inDAT == 2){
      port_gpio->offset000 = 0x00000020;      
      Serial.print("2-");
    }
    else if(inDAT == 3){
      port_gpio->offset000 = 0x00000030;            
      Serial.print("3-");
    }
    else{
      port_gpio->offset000 = 0x00000000;                  
      Serial.print("0-");
    }

    delay(1);

    Serial.print(port_gpio->offset008 & 0x00000170);
    Serial.print("\n");    
  }

  delay(1000);
}

f:id:ysin1128:20210315165359p:plain

シリアルモニタから 1 を送ると 80 = 0x00000050 が返ってきています。つまり、6 bit と 4 bit の値が 1 になっているということで、これは2番端子から High が出力されてそれを 2番端子自身が検出し、それに繋がっている 4番端子も High を検出していることを意味します。シリアルモニタから 2, 3 を送って返ってきた値も、該当する Bit の値が 1 になっています。GPIO の制御に成功しました。

修正点

修正したのは以下の記述です。

  port_gpio = &IMXRT_GPIO9; //0x4200C000

  // CCM_CCGR3 |= 0x00003000; // CG6 = 11, enable GPIO4 clock

port_gpio に代入する GPIO モジュールのレジスタ位置のポインタを GPIO4 モジュールのものから GPIO9 モジュールのものに変更しました。
IMXRT1060 は Standard-speed GPIO (GPIO1-5 モジュール) の他に High-speed GPIO (GPIO6-9 モジュール) を備えていて、GPIO1-4 モジュールと GPIO6-9 モジュールは排他使用となっています。IMXRT1060 のデフォルトでは GPIO1-4 が使用されるようになっているのですが Arduino IDE がそれを勝手に GPIO6-9 を使用するように変更していたようです。
LPI2C の Clock Gating でも同じようなことでやられましたが、レジスタの状態は IMXRT1060 の Reference Manual よりも実際のレジスタ値で確認するべきでした・・・。
ともあれ、GPIO4 が上手く動かなかったのは GPIO4 の代わりに GPIO9 が有効になっていたからだったので、レジスタ位置のポインタを GPIO9 モジュールに変更するだけで解決しました。

また、GPIO9 モジュールの Clock は Gating が無い (常に供給されている) ので、不要となった CCM_CCGR3 の行はコメントアウトしています。

GPIO9 モジュールに気付いたのは、残念ながら自力ではなくカンニングです。 Tennsy4.0用の digitalWrite コマンドの記述を見に行ったところ、GPIO6-9 しか使われていなかったことで気付きました。ちなみに下記のファイルです。
/hardware/teensy/avr/cores/teensy4/
digital.c
core_pins.h

それなら最初から digitalWrite を使えという話ですが、全くその通りです。私は趣味で苦労しただけなので、ただ GPIO の制御をしたいだけであれば digitalWrite/digitalRead を使うことをお勧めします。



おまけ。GPIO の他、I2C や SPI も制御できる Sketch とツールを作成しました。
ysin1128.hatenablog.com