Arduino(ATMega328)で直接ポートを操作する

##はじめに
Arduinoで直接ポートを操作する為の覚書です。ポートを直接使うというのはどういう事か？ 通常、ArduinoでGPIOを使って入出力を行う場合には、

```C
pinMode(11, INPUT);
a = digitalRead(11);
pinMode(12, OUTPUT);
digitalWrite(12, HIGH);
```

などとして、指定したピンに入力、もしくはHIGH/LOWのステートを出力します。Arduinoの流儀に従えば、これは各ピン、１ピンずつこの設定を行うことになります。__一般的にArduinoでプログラムする場合にはこの流儀に従ってください。__(プログラムの見やすさや、互換性などを考えるとArduinoの流儀に従う方がメリットが大きいです)
しかし、__このプログラミングはスピードを犠牲にしています。どうしてもスピードが必要だったり、複数のI/Oを同時に変化させたい場合には直接ポートを操作__して、入出力を行う方法があります。

##公開動画
以下の動画をあげていますのでご興味のある方はご覧ください。
[<img width = 400 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2428358/0ccc1752-a39c-2a7a-5d6e-9ca9fca51813.png">](https://youtu.be/YZ8MZE0Y0yc)
__注）私の勘違いにより動画内のPORTCのアサインが違っています。
PC6にはA6ではなくRESETがPC7にはA7ではなく、何もアサインされていません。
下の方にピンアサインが載せてあるのでそれを参照してください。__

##PORTに付けられた名前
PORTを直接操作するにはポートが割り当てられたレジスタ・アドレスから直接読み出し、書き込みを行います。しかしアドレスの即値をプログラマーが知る必要はありません。名前が定義されているので、あたかも変数に値を代入するかの様に使えます。使える名前は以下のものになります。

<font color="Blue">DDRB</font> ：ポートBの方向レジスタ。入出力方向(INPUT/OUTPUTを設定する)
<font color="Blue">DDRC</font> ：ポートCの方向レジスタ。入出力方向(INPUT/OUTPUTを設定する)
<font color="Blue">DDRD</font> ：ポートDの方向レジスタ。入出力方向(INPUT/OUTPUTを設定する)
<font color="Blue">PORTB</font> ：ポートBの出力レジスタ。出力するデータを書き込む
<font color="Blue">PORTC</font> ：ポートCの出力レジスタ。出力するデータを書き込む
<font color="Blue">PORTD</font> ：ポートDの出力レジスタ。出力するデータを書き込む
<font color="Blue">PINB</font> ：ポートBの入力レジスタ、入力したデータが保存されている
<font color="Blue">PINC</font> ：ポートCの入力レジスタ、入力したデータが保存されている
<font color="Blue">PIND</font> ：ポートDの入力レジスタ、入力したデータが保存されている

各ポートのレジスタには１つで最大8個のGPIOがアサインされています。

##ポートにどの様に設定するか
上記で示しているレジスタは8ビットのレジスタです。従って、8bitをまとめて設定することになります。これはArduinoが8bit CPUであることに由来します。8bit CPUは「8bitのデータをまとめて取り扱える」という能力を有したCPUです。1bitづつのデータを取り扱う事はできますが、それなりの処理が必要になります。

なので8bitまとめて設定する場合は簡単です。

```C
PORTB = 0b10101010;
```
ポートBにアサインされている8個のI/Oにまとめて出力するステートを設定します。このやり方の場合、8bitまとめて設定されます。これで各bitにアサインされているGPIOのステートをまとめて変更できます。特定のbitだけを変更したい場合は後述します。

入出力方向はDDR(Data Direction Register)で設定します。

```C
DDRB = 0b10101010;
```
これで0に設定したbitはINPUT、1に設定したbitはOUTPUTに設定されます。
<font color="Blue">0:INPUT
1:OUTPUT</font>
です。

入力はPINレジスタを読み出す事で実現できます。

```C
uint8_t input_data = PINB;
```
でPORTBにアサインされているGPIO全てが読み出されて、8bitのデータinput_dataに代入されます。

##ポートにはどの様にGPIOがアサインされているか？
ArduinoNanoで見ると各ポートは以下の様に対応します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2428358/c4bb5f13-69ff-7725-e23e-c25882f1f8d7.png)
また、各ポートに対してbitのアサインは以下の様になります。
ただしPC6をI/Oピンとして使用する場合にはFuseを書き換える必要があるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2428358/97eb41c7-e4e5-665b-aa5f-e8f16689ce58.png)
例えばD13であれば、PORTBのbit5が対応します。
#プログラミング
D13-11をOUTPUTにD10-D8をINPUTに設定し、D13=High, D12=Low, D11=Highに設定する場合。

```C
DDRB  = 0b111000;
PORTB = 0b101000;
```
となります。
1bitづつ設定したいのであれば、bitシフトと、bit演算を用いて

```C
PORTB = (PORTB & 0b011111) | (0b1 << 5); // bit5だけHighにする
```
とします。
入力は

```C
uint8_t d10_8 = PINB & 0x000111;
```
でD10-8のデータがbit2-0に入ります。
1bitづつ入力したいのであれば出力の時と同様、bitシフトと、bit演算で

```C
uint8_t d10 = (PINB >> 2) & 0b1; // bit2だけを読み出す
uint8_t  d9 = (PINB >> 1) & 0b1; // bit1だけを読み出す
uint8_t  d8 = (PINB >> 0) & 0b1; // bit0だけを読み出す
```

とします。

1bitづつデータを取り出す場合には少し不便ですね。
8bitのデータまとめて取り出す場合には、例えばPORTDのD0-D7を8bitのデータとして扱いたい場合Arduinoの流儀に従うと、条件演算子を使ってうまく書いたとしても

```C
#define LED_OUTPUT_PIN_0  0
#define LED_OUTPUT_PIN_1  1
#define LED_OUTPUT_PIN_2  2
#define LED_OUTPUT_PIN_3  3
#define LED_OUTPUT_PIN_4  4
#define LED_OUTPUT_PIN_5  5
#define LED_OUTPUT_PIN_6  6
#define LED_OUTPUT_PIN_7  7

void setup() {
  pinMode(LED_OUTPUT_PIN_0, OUTPUT);
  pinMode(LED_OUTPUT_PIN_1, OUTPUT);
  pinMode(LED_OUTPUT_PIN_2, OUTPUT);
  pinMode(LED_OUTPUT_PIN_3, OUTPUT);
  pinMode(LED_OUTPUT_PIN_4, OUTPUT);
  pinMode(LED_OUTPUT_PIN_5, OUTPUT);
  pinMode(LED_OUTPUT_PIN_6, OUTPUT);
  pinMode(LED_OUTPUT_PIN_7, OUTPUT);
}

void binary_counter2(uint8_t number){
  digitalWrite(LED_OUTPUT_PIN_0, ((number & 0b00000001)==0) ? LOW : HIGH);
  digitalWrite(LED_OUTPUT_PIN_1, ((number & 0b00000010)==0) ? LOW : HIGH);
  digitalWrite(LED_OUTPUT_PIN_2, ((number & 0b00000100)==0) ? LOW : HIGH);
  digitalWrite(LED_OUTPUT_PIN_3, ((number & 0b00001000)==0) ? LOW : HIGH);
  digitalWrite(LED_OUTPUT_PIN_4, ((number & 0b00010000)==0) ? LOW : HIGH);
  digitalWrite(LED_OUTPUT_PIN_5, ((number & 0b00100000)==0) ? LOW : HIGH);
  digitalWrite(LED_OUTPUT_PIN_6, ((number & 0b01000000)==0) ? LOW : HIGH);
  digitalWrite(LED_OUTPUT_PIN_7, ((number & 0b10000000)==0) ? LOW : HIGH);
}
```
この様な長いコードになり、かなり面倒です。しかしPORTを使うと

```C
void setup() {
  DDRB  = 0b11111111;
}

void binary_counter2(uint8_t number){
  PORTB = number;
}
```

これだけで済みます。

##shiftOut関数をポートを直接操作した場合の速度向上
Arduino標準のshiftOut関数を用いた場合、転送にかかる時間は105us程度、
(オシロの波形は、黄色HC595 A、シアンHC595 Latch CLK、マゼンタHC595 Shift CLK)
![osc1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2428358/b8f17567-3827-93dc-c1ad-1d48786bbfbd.jpeg)
ポートを直接操作すると転送にかかる時間は12.2usと約8.6倍まで速度が向上します。
![osc2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2428358/71461d7a-c884-0006-c1b9-e30f44741260.jpeg)

波形を確認した時のプログラム

```C
// 高速版ShiftOut出力波形確認用

// ShiftOutで使うピン
#define HC595_A              2  // シリアル入力
#define HC595_LATCH_CLOCK    4  // ラッチ・シグナル
#define HC595_SHIFT_CLOCK    5  // シフト・レジスタ・クロック

void shiftOutFast(uint8_t byte_data);
void latch_toggle(void);

/*
 * 初期化
 */
void setup() {
  DDRD  = 0b00110100;  // D2,D4,D5(ShiftOutで使うピン) = OUTPUT
  PORTD = 0b00000000;
}

/*
 * ループ
 */
void loop() {
  shiftOutFast(0x55);
  latch_toggle();
  delay(10);
}

/*
 * 高速ShiftOutルーチン
 */
void shiftOutFast(uint8_t byte_data){
  static uint8_t data_a_bit;
  static uint8_t no_effective_bit = PORTD & 0b11001011;
  static uint8_t bit_shift[8] = {1,2,4,8,16,32,64,128};

  data_a_bit = no_effective_bit;
  for(uint8_t i=0; i<8; i++){
    if((byte_data & bit_shift[i]) == 0){
      PORTD = data_a_bit;                                 // HC595_SHIFT_CLOCKをLowにする
      PORTD = data_a_bit = 0b00000000 | no_effective_bit; // HC595_Aを0にする
      PORTD              = 0b00100000 | no_effective_bit; // HC595_SHIFT_CLOCKをHighにする
    }
    else{
      PORTD = data_a_bit;                                 // HC595_SHIFT_CLOCKをLowにする
      PORTD = data_a_bit = 0b00000100 | no_effective_bit; // HC595_Aを1にする
      PORTD              = 0b00100100 | no_effective_bit; // HC595_SHIFT_CLOCKをHighにする
    }
  }

  PORTD = no_effective_bit;
}

/*
 * Latch Clock Toggle
 */
void latch_toggle(void){
  static uint8_t no_effective_bit = PORTD & 0b11001011;
  volatile uint8_t a;

  PORTD = 0b00010000 | no_effective_bit;
  a=0;
  PORTD = no_effective_bit;
}
```
##履歴
2022.1.27 プログラムに色つけました。
2022.1.13 Initial release
2022.4.12 PORTのアサイン・テーブルを変更しました。PC6:A6→RESET, PC7:A7→NC